From 617d0a654f8cf5d1c12c1a282eec4152aac0dbca Mon Sep 17 00:00:00 2001 From: Naman Vyas <165130700+naman47vyas@users.noreply.github.com> Date: Fri, 13 Sep 2024 19:19:37 +0530 Subject: [PATCH] Feature flag for continuous profiling (#15) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore(deps): update gradle/actions action to v3.4.2 (main) (#11598) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * Bump version in api diff compare (#11615) * chore(deps): update graalvm/setup-graalvm action to v1.2.2 (main) (#11624) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * Simplify jetty9 http client instrumentation (#11595) * Merge change log updates from release/v1.33.x (#11628) * fix(deps): update quarkus packages to v3.11.3 (main) (patch) (#11629) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * remove conditional indy test in CI (#11631) * make aws-sdk indy compatible (#11552) * fix(deps): update quarkus packages to v3.12.0 (main) (minor) (#11630) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * Remove aws sqs latest dep limit (#11643) * Fix deprecation warning in build script (#11642) * fix(deps): update dependency org.springframework.boot:spring-boot-starter-web to v3.3.1 (main) (#11644) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * re-use logic for http client configuration (#11620) Co-authored-by: Trask Stalnaker * fix link (#11656) * fix(deps): update dependency net.ltgt.gradle:gradle-errorprone-plugin to v4.0.1 (main) (#11652) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * Influxdb client: don't fill db.statement for create/drop database and write operations (#11557) * Update docs for bundled exporters (#11662) * Improve test error report (#11664) * Use more @ConditionalOnEnabledInstrumentation (#11665) * Fix typo (#11672) * Remove reflection from builder (#11673) * fix(deps): update junit5 monorepo to v5.10.3 (main) (patch) (#11681) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * don't use test keystore for vaadin (#11679) * Add address and port assertions (#11668) * Update http metrics to the latest version (#11659) * fix(deps): update armeria packages to v1.29.1 (main) (patch) (#11686) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * fix(deps): update dependency io.grpc:grpc-bom to v1.65.0 (main) (#11689) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * Fix running ./gradlew tasks (#11692) * chore(deps): update github/codeql-action action to v3.25.11 (main) (#11699) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * fix(deps): update groovy monorepo (main) (patch) (#11706) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * fix build for graalvm native (#11709) * add javalin instrumentation (#11587) * fix(deps): update dependency io.opentelemetry.proto:opentelemetry-proto to v1.3.2-alpha (main) (#11715) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * Fix kafka graalvm native (#11714) * cleanup log filter (#11719) * fix(deps): update dependency org.owasp:dependency-check-gradle to v10 (main) (#11720) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * config properties support for spring starter clients (#11605) * fix(deps): update dependency org.owasp:dependency-check-gradle to v10.0.1 (main) (#11722) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * rebuild when labels change, so that we can trigger the right actions (#11725) * Add GraalVM Kafka hints (#11735) * Fix collector conf for overhead tests (#11730) * fix(deps): update quarkus packages to v3.12.1 (main) (patch) (#11732) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * Convert jsf jakarta tests from groovy to java (#11694) * Fix GraalVM hint (#11737) * Jsf: simplify asserting exception (#11736) * Convert jsf javax tests from groovy to java (#11711) * fix(deps): update dependency commons-logging:commons-logging to v1.3.3 (main) (#11740) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * fix(deps): update dependency org.codehaus.mojo:animal-sniffer-annotations to v1.24 (main) (#11741) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * Replace junit4 @Test annotation with junit5 annotation (#11745) * chore(deps): update actions/upload-artifact action to v4.3.4 (main) (#11748) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * fix(deps): update jackson packages to v2.17.2 (main) (patch) (#11750) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Lauri Tulmin * fix(deps): update dependency org.owasp:dependency-check-gradle to v10.0.2 (main) (#11753) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * Exclude javalin 3.2.0 from muzzle (#11766) * Update the OpenTelemetry SDK version to 1.40.0 (#11752) Co-authored-by: Lauri Tulmin * support otel.instrumentation.common.default-enabled in spring starter (#11746) * fix(deps): update dependency org.codenarc:codenarc to v3.5.0 (main) (#11758) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * Add link to SDK configuration page (#11729) * Build image for testing early jdk8 (#11594) * fix(deps): update byte buddy packages to v1.14.18 (main) (patch) (#11767) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Trask Stalnaker * Support tracing the ClickHouse Java HTTP client (#11660) * support jettyclient 12 (#11519) Co-authored-by: Lauri Tulmin Co-authored-by: Trask Stalnaker * Fix build (#11776) * chore(deps): update gradle/actions action to v3.4.2 (main) (#11770) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * Enable early jdk8 test (#11777) * Add Pulsar MessagingProducerMetrics (#11591) Co-authored-by: Lauri Tulmin Co-authored-by: Steve Rao * chore(deps): update actions/setup-node action to v4.0.3 (main) (#11780) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * fix(deps): update dependency org.assertj:assertj-core to v3.26.3 (main) (#11781) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * fix windows benchmark fail (#11698) * Alter instrumentation suppression behavior (#11640) * Propagate otel context through custom aws client context for lambda direct calls (#11675) Co-authored-by: Lauri Tulmin * fix(deps): update armeria packages to v1.29.2 (main) (patch) (#11785) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * fix(deps): update dependency io.quarkus:quarkus-bom to v3.12.2 (main) (#11787) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * chore(deps): update plugin io.quarkus to v3.12.2 (main) (#11789) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * Spring autoconf deps (#11784) * GraalVM native support for the OpenTelemetry annotations (#11757) Co-authored-by: Trask Stalnaker * Use config properties for spring starter (http server) (#11667) Co-authored-by: Lauri Tulmin Co-authored-by: Trask Stalnaker * Update @EnableOpenTelemetry javadoc (#11799) * chore(deps): update github/codeql-action action to v3.25.12 (main) (#11808) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * fix(deps): update dependency io.grpc:grpc-bom to v1.65.1 (main) (#11804) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * fix(deps): update dockerjavaversion to v3.4.0 (main) (minor) (#11802) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * rename spring autoconfigure to autoconfigure-2 (#11800) * make spring starter stable (#11763) * chore(deps): update dependency gradle to v8.9 (main) (#11798) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * chore(deps): update ubuntu docker tag to v24 (main) (#11771) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * Fix http.server.active_requests metric with async requests (#11638) Co-authored-by: Helen <56097766+heyams@users.noreply.github.com> Co-authored-by: Trask Stalnaker * Rename spring-boot-autoconfigure artifacts (#11815) * fix(deps): update dependency org.owasp:dependency-check-gradle to v10.0.3 (main) (#11834) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * fix(deps): update dependency commons-codec:commons-codec to v1.17.1 (main) (#11830) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * Remove unneeded groovy dependencies from lambda tests (#11828) * Restructure to have a single spring-boot-autoconfigure artifact (#11826) * chore(deps): update gradle/actions action to v3.5.0 (main) (#11824) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * Update change log for v2.6.0 release (#11816) * Update version to 2.7.0-SNAPSHOT (#11839) Co-authored-by: Trask Stalnaker * fix(deps): update testcontainers-java monorepo to v1.20.0 (main) (minor) (#11846) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * fix(deps): update quarkus packages to v3.12.3 (main) (patch) (#11842) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * Merge change log updates from release/v2.6.x (#11840) * Update api diff after release (#11850) * Add span baggage processor (#11697) * Fix ClickHouse tracing when database name not included in connection string (#11852) * fix(deps): update dependency io.netty:netty-bom to v4.1.112.final (main) (#11864) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * chore(deps): update github/codeql-action action to v3.25.13 (main) (#11862) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * fix(deps): update dependency org.springframework.boot:spring-boot-starter-web to v3.3.2 (main) (#11855) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * fix(deps): update errorproneversion to v2.29.2 (main) (minor) (#11836) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * chore(deps): update bellsoft/liberica-openjdk-alpine docker tag to v21.0.4 (main) (#11863) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * fix(deps): update dependency org.apache.commons:commons-lang3 to v3.15.0 (main) (#11848) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * fix(deps): update dependency com.gradle.develocity:com.gradle.develocity.gradle.plugin to v3.17.6 (main) (#11875) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * chore(deps): update plugin com.gradle.develocity to v3.17.6 (main) (#11874) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * chore(deps): update docker/login-action action to v3.3.0 (main) (#11877) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * Consolidate github action renovate PRs (#11829) * Reformat supported libraries doc (#11889) * fix(deps): update quarkus packages to v3.13.0 (main) (minor) (#11894) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * fix(deps): update dependency com.google.protobuf:protobuf-java-util to v3.25.4 (main) (#11897) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * Fix missing throw statement in RestClientWrapper (#11892) (#11893) * Merge change log updates from release/v1.33.x (#11904) * fix(deps): update armeria packages to v1.29.3 (main) (patch) (#11906) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * chore(deps): update github actions (main) (#11910) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * fix(deps): update dependency com.uber.nullaway:nullaway to v0.11.1 (main) (#11916) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * fix(deps): update armeria packages to v1.29.4 (main) (patch) (#11923) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * Fix latest dep tests (#11925) * Convert jsp 2.3 tests from groovy to java (#11827) * fix(deps): update testcontainers-java monorepo to v1.20.1 (main) (patch) (#11931) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * Fix broken link (#11935) * Allow any types in invokedynamic advice method signatures (#11873) Co-authored-by: Lauri Tulmin * Improve tomcat version detection (#11936) * Add @jaydeluca as triager (#11937) * Update kafka docker image (#11938) * chore(deps): update actions/upload-artifact action to v4.3.5 (main) (#11943) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * Fix class cast exception, noop meter does not implement incubating api (#11934) * Executors indy support (#11738) * idempotent license report (#11810) Co-authored-by: Lauri Tulmin * chore(deps): update plugin org.jetbrains.kotlin.jvm to v2.0.10 (main) (#11955) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * fix(deps): update slf4j monorepo to v2.0.14 (main) (patch) (#11957) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Lauri Tulmin * fix(deps): update dependency org.awaitility:awaitility to v4.2.2 (main) (#11960) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * fix(deps): update dependency io.quarkus:quarkus-bom to v3.13.1 (main) (#11962) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * chore(deps): update plugin io.quarkus to v3.13.1 (main) (#11966) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * fix(deps): update dependency org.apache.commons:commons-lang3 to v3.16.0 (main) (#11967) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * fix(deps): update dependency io.grpc:grpc-bom to v1.66.0 (main) (#11972) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * fix(deps): update slf4j monorepo to v2.0.15 (main) (patch) (#11976) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Lauri Tulmin * Update shadow plugin to new version and coordinates (#11979) * Improve akka route handling with java dsl (#11926) * fix(deps): update dependency org.slf4j:slf4j-simple to v2.0.15 (main) (#11981) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * Ignore alibaba fastjson ASMClassLoader (#11954) * fix(deps): update quarkus packages to v3.13.2 (main) (patch) (#11983) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * Convert cdi-testing test from groovy to java (#11982) * fix(deps): update slf4j monorepo to v2.0.16 (main) (patch) (#11989) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Lauri Tulmin * chore(deps): update github actions (main) (#11995) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * chore(deps): update gradle/actions action to v4 (main) (#11996) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * fix(deps): update errorproneversion to v2.30.0 (main) (minor) (#11991) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Lauri Tulmin * Use `aws-lambda-java-serialization` library, which is available by default, while deserializing input and serializing output (#11868) * fix(deps): update armeria packages to v1.30.0 (main) (minor) (#12001) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * Influxdb indy support (#11710) Co-authored-by: Jonas Kunz Co-authored-by: Lauri Tulmin * Force dynamic typing on AssignReturned annotations (#11884) * Update the OpenTelemetry SDK version to 1.41.0 (#11985) Co-authored-by: Lauri Tulmin * Fix building wildfly docker image (#11998) * Add assert inverse to ktor2 muzzle configuration (#11948) * chore(deps): update dependency gradle to v8.10 (main) (#12014) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * Closing a kafka producer/consumer should not disable metrics from other consumers/producers (#11975) * Logback appender: map timestamp in nanoseconds if possible (#11807) (#11974) * Save ILoggingEvent.getArgumentArray() arguments from Logback (#11865) Co-authored-by: Lauri Tulmin * use DefaultHttpClientInstrumenterBuilder and DefaultHttpServerInstrumenterBuilder for armeria (#11797) Co-authored-by: Trask Stalnaker * Fix ending span in Ktor plugin (#11726) * Use stable semconv for Java17 (#11914) Co-authored-by: Lauri Tulmin * fix(deps): update dependency commons-cli:commons-cli to v1.9.0 (main) (#12017) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * Limit vaadin 14 latest dep version (#12025) * Add Pulsar Consumer metrics (#11891) Co-authored-by: Lauri Tulmin Co-authored-by: Steve Rao * Fix logback appender test (#12027) * fix(deps): update junit5 monorepo to v5.11.0 (main) (minor) (#12010) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Lauri Tulmin * fix(deps): update dependency ch.qos.logback:logback-classic to v1.5.7 (main) (#12029) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * chore(deps): update plugin com.github.jk1.dependency-license-report to v2.9 (main) (#12030) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * Fix netty memory leak (#12003) * Update change log (#12019) * fix(deps): update byte buddy packages to v1.14.19 (main) (patch) (#12033) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Trask Stalnaker * Update version to 2.8.0-SNAPSHOT (#12037) Co-authored-by: Lauri Tulmin * Update api diffs (#12045) * chore(deps): update github/codeql-action action to v3.26.2 (main) (#12047) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * fix(deps): update guava packages to v33.3.0-jre (main) (minor) (#12040) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * fix(deps): update dependency com.gradle.develocity:com.gradle.develocity.gradle.plugin to v3.18 (main) (#12051) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * chore(deps): update plugin com.gradle.develocity to v3.18 (main) (#12050) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * fix(deps): update dependency commons-logging:commons-logging to v1.3.4 (main) (#12053) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * fix(deps): update dependency com.uber.nullaway:nullaway to v0.11.2 (main) (#12057) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * Try to fix disabling renovate updates for spring boot (#12059) * [improve][pulsar] Enhance SendCallback to address PIP-363 (#11648) Co-authored-by: Lauri Tulmin Co-authored-by: Lauri Tulmin * Delay loading `InetAddressResolverProvider` until after the agent has started (#11987) Co-authored-by: Lauri Tulmin * Remove unused renovate config (#12060) * fix(deps): update quarkus packages to v3.13.3 (main) (patch) (#12067) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * fix(deps): update otelcontribversion to v1.38.0-alpha (main) (minor) (#12061) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Lauri Tulmin * Fix instrumentation for the latest version of ucp (#12052) * JdbcIgnoredTypesConfigurer support Shardingsphere 5.x (#12066) * fix(deps): update quarkus packages to v3.14.0 (main) (minor) (#12074) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * enforce static imports (#12009) * chore(deps): update plugin org.jetbrains.kotlin.jvm to v2.0.20 (main) (#12081) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * make netty-* indy compatible (#11559) Co-authored-by: Jonas Kunz Co-authored-by: Trask Stalnaker * make hibernate-* indy compatible (#11553) * fix(deps): update dependency org.springframework.boot:spring-boot-starter-web to v3.3.3 (main) (#12087) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * chore(deps): update github/codeql-action action to v3.26.5 (main) (#12105) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * fix(deps): update dependency checkstyle to v10.18.0 (main) (#12103) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * Add workflow to complete 1.33.6 to main (#12107) * fix(deps): update byte buddy packages to v1.15.0 (main) (minor) (#12094) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Lauri Tulmin * Fix rabbitmq NullPointerException (#12109) * Merge change log updates from release/v1.33.x (#12114) * Remove temporary workflow (#12115) * Name is not needed for annotation value parameter (#12113) * Convert apache-httpclient-2.0 tests from groovy to java (#12102) Co-authored-by: Steve Rao * convert spring integration tests to java (#11999) Co-authored-by: Jay DeLuca * Add network peer address attributes to the span for the `okhttp-3.0` instrumentation (#12012) * Add support for cxf 4.0 jaxws (#12077) * fix(deps): update dependency org.mockito:mockito-core to v5.13.0 (main) (#12123) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * fix(deps): update quarkus packages to v3.14.1 (main) (patch) (#12129) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * indy advice docs & migration procedure (#11546) * fix(deps): update errorproneversion to v2.31.0 (main) (minor) (#12134) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * Update units of time based metrics from millis to seconds for Java17 … (#12084) * Added rules for capturing Apache Camel metrics exposed by JMX MBeans (#11901) * Fix play-mvc\methods NullPointerException (#12121) * Spring batch tests to java - basic (#12076) * chore(deps): update plugin com.gradle.plugin-publish to v1.2.2 (main) (#12137) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * Pin github actions by hash (#12140) * fix(deps): update dependency org.apache.commons:commons-lang3 to v3.17.0 (main) (#12145) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * fix(deps): update dependency checkstyle to v10.18.1 (main) (#12150) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * fix(deps): update byte buddy packages to v1.15.1 (main) (patch) (#12141) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Lauri Tulmin * fix(deps): update dependency org.owasp:dependency-check-gradle to v10.0.4 (main) (#12152) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * chore(deps): update github actions (main) (#12155) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * Fix error span status for successful requests in Ktor (#12161) * Convert opentelemetry-api-1.0 tests from groovy to java (#12133) Co-authored-by: Lauri Tulmin * convert module apache-dubbo-2.7 test case from groovy to java (#12008) Co-authored-by: Jay DeLuca * Test whether http client span ends after headers or body is received (#12149) * Remove unused imports (#12165) * Keep junit extensions in static fields (#12166) * fix(deps): update dependency io.netty:netty-bom to v4.1.113.final (main) (#12169) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * fix(deps): update quarkus packages to v3.14.2 (main) (patch) (#12167) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * make internal-reflection indy compatible (#12136) Co-authored-by: Jonas Kunz * Make OpenTelemetry-API Bridge and dependent instrumentations work with indy (#11578) * Make log record asserts similar to trace asserts (#12164) * Enable tests for jedis 2.7.2 (#12175) * Add empty line to separate blocks (#12174) * Fix flaky spring integration test (#12185) * Read timeout in jetty http client tests (#12184) * Make rocketmq span status extractor delegate to default extractor (#12183) * Remove unneeded span status extractor (#12182) * fix(deps): update dependency org.apache.logging.log4j:log4j-bom to v2.24.0 (main) (#12187) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * fix(deps): update dependency ch.qos.logback:logback-classic to v1.5.8 (main) (#12189) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * chore(deps): update dependency gradle to v8.10.1 (main) (#12191) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * chore(deps): update plugin org.graalvm.buildtools.native to v0.10.3 (main) (#12205) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * chore(deps): update plugin com.gradleup.shadow to v8.3.1 (main) (#12208) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * fix(deps): update dependency com.gradleup.shadow:shadow-gradle-plugin to v8.3.1 (main) (#12209) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * chore(deps): update plugin com.gradle.develocity to v3.18.1 (main) (#12211) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * fix(deps): update dependency com.gradle.develocity:com.gradle.develocity.gradle.plugin to v3.18.1 (main) (#12212) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * updating .gitignore to ignore workflows except the middleware one * test fix, and reverted to grpc as default otel protocol * Added feature flag for continuous profiling --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Trask Stalnaker Co-authored-by: Lauri Tulmin Co-authored-by: OpenTelemetry Bot <107717825+opentelemetrybot@users.noreply.github.com> Co-authored-by: SylvainJuge <763082+SylvainJuge@users.noreply.github.com> Co-authored-by: Gregor Zeitlinger Co-authored-by: Jay DeLuca Co-authored-by: Jean Bisutti Co-authored-by: crossoverJie Co-authored-by: Lucas Amoroso Co-authored-by: Liu Ziming Co-authored-by: Lauri Tulmin Co-authored-by: Steve Rao Co-authored-by: John Bley Co-authored-by: Helen <56097766+heyams@users.noreply.github.com> Co-authored-by: kyy1996 Co-authored-by: Jonas Kunz Co-authored-by: César <56847527+LikeTheSalad@users.noreply.github.com> Co-authored-by: jason plumb <75337021+breedx-splk@users.noreply.github.com> Co-authored-by: Serkan ÖZAL Co-authored-by: Jérôme Joslet Co-authored-by: Igor Suhorukov Co-authored-by: Mariia Skripchenko <61115099+marychatte@users.noreply.github.com> Co-authored-by: Robert Niedziela <175605712+robsunday@users.noreply.github.com> Co-authored-by: 道君 Co-authored-by: Shelby Huang <48885776+huange7@users.noreply.github.com> Co-authored-by: xiepuhuan Co-authored-by: Luigi De Masi <5583341+luigidemasi@users.noreply.github.com> Co-authored-by: shalk(xiao kun) --- .github/CODEOWNERS | 2 +- .github/ISSUE_TEMPLATE/bug_report.md | 32 - .github/ISSUE_TEMPLATE/bug_report.yml | 50 + .github/ISSUE_TEMPLATE/config.yml | 10 + .github/ISSUE_TEMPLATE/feature_request.md | 20 - .github/ISSUE_TEMPLATE/feature_request.yml | 24 + .github/dependabot.yml | 100 -- .github/graal-native-docker-compose.yaml | 31 + .github/labeler.yml | 10 + .github/renovate.json5 | 216 +++ .github/repository-settings.md | 58 +- .github/scripts/draft-change-log-entries.sh | 2 +- .../scripts/generate-release-contributors.sh | 2 +- .github/scripts/gha-free-disk-space.sh | 9 + .../scripts/markdown-link-check-config.json | 13 +- .gitignore | 5 +- .java-version | 1 + CHANGELOG.md | 1120 ++++++++++++++- CONTRIBUTING.md | 16 +- ISSUES.md | 33 + README.md | 30 +- RELEASING.md | 48 +- VERSIONING.md | 24 +- benchmark-overhead-jmh/build.gradle.kts | 2 +- benchmark-overhead/Dockerfile-petclinic-base | 2 +- benchmark-overhead/README.md | 75 +- benchmark-overhead/build.gradle.kts | 16 +- .../gradle/wrapper/gradle-wrapper.jar | Bin 61608 -> 43583 bytes .../gradle/wrapper/gradle-wrapper.properties | 4 +- benchmark-overhead/gradlew | 34 +- benchmark-overhead/gradlew.bat | 22 +- .../java/io/opentelemetry/OverheadTests.java | 25 +- .../opentelemetry/containers/K6Container.java | 5 +- .../results/ResultsCollector.java | 6 +- .../util/ContainerNamingConvention.java | 33 + .../util/LocalNamingConvention.java | 33 + .../opentelemetry/util/NamingConvention.java | 33 +- .../opentelemetry/util/NamingConventions.java | 14 +- .../src/test/resources/collector.yaml | 24 +- bom-alpha/build.gradle.kts | 7 + build.gradle.kts | 86 +- buildscripts/checkstyle.xml | 14 +- .../dependency-check-suppressions.xml | 7 + conventions/build.gradle.kts | 31 +- ...ntelemetry.instrumentation.base.gradle.kts | 27 +- ...ation.javaagent-instrumentation.gradle.kts | 2 +- ...rumentation.javaagent-shadowing.gradle.kts | 15 +- ...strumentation.javaagent-testing.gradle.kts | 6 +- ...ntation.library-instrumentation.gradle.kts | 2 +- .../gradle/OtelBomExtension.kt | 5 + .../gradle/OtelJavaExtension.kt | 1 + .../kotlin/otel.bom-conventions.gradle.kts | 9 +- .../otel.errorprone-conventions.gradle.kts | 11 +- .../kotlin/otel.jacoco-conventions.gradle.kts | 4 +- .../otel.japicmp-conventions.gradle.kts | 4 +- .../kotlin/otel.java-conventions.gradle.kts | 114 +- .../otel.javaagent-instrumentation.gradle.kts | 4 + .../kotlin/otel.jmh-conventions.gradle.kts | 4 +- .../otel.protobuf-conventions.gradle.kts | 43 - .../otel.spotless-conventions.gradle.kts | 6 +- custom-checks/build.gradle.kts | 7 + .../CanIgnoreReturnValueSuggesterFactory.java | 19 + .../OtelCanIgnoreReturnValueSuggester.java | 73 + .../customchecks/OtelInternalJavadoc.java | 2 +- dependencyManagement/build.gradle.kts | 92 +- docs/advanced-configuration-options.md | 16 +- docs/agent-features.md | 7 +- ...ntelemetry-instrumentation-annotations.txt | 2 + .../opentelemetry-instrumentation-api.txt | 261 ++++ ...ntelemetry-instrumentation-annotations.txt | 2 +- .../opentelemetry-instrumentation-api.txt | 2 +- ...pentelemetry-spring-boot-autoconfigure.txt | 2 + .../opentelemetry-spring-boot-starter.txt | 2 + docs/contributing/debugging.md | 8 + .../intellij-setup-and-troubleshooting.md | 34 +- docs/contributing/javaagent-structure.md | 32 +- docs/contributing/javaagent-test-infra.md | 8 +- docs/contributing/muzzle.md | 36 +- docs/contributing/running-tests.md | 52 +- docs/contributing/selectModules.kts | 83 ++ docs/contributing/style-guideline.md | 44 +- docs/contributing/using-instrumenter-api.md | 56 +- .../writing-instrumentation-module.md | 188 ++- docs/contributing/writing-instrumentation.md | 8 +- docs/logger-mdc-instrumentation.md | 32 +- docs/misc/interop-design/interop-design.md | 4 +- docs/scope.md | 12 +- docs/supported-libraries.md | 336 +++-- examples/distro/README.md | 22 +- examples/distro/agent/build.gradle | 10 +- examples/distro/build.gradle | 18 +- examples/distro/gradle/instrumentation.gradle | 8 +- examples/distro/gradle/shadow.gradle | 3 +- .../distro/gradle/wrapper/gradle-wrapper.jar | Bin 61608 -> 43583 bytes .../gradle/wrapper/gradle-wrapper.properties | 4 +- examples/distro/gradlew | 34 +- examples/distro/gradlew.bat | 22 +- .../instrumentation/servlet-3/build.gradle | 2 +- .../DemoServlet3InstrumentationTest.java | 7 +- examples/distro/smoke-tests/build.gradle | 14 +- .../javaagent/smoketest/SmokeTest.java | 23 +- .../smoketest/SpringBootSmokeTest.java | 11 +- .../testing/agent-for-testing/build.gradle | 10 +- examples/extension/README.md | 42 +- examples/extension/build.gradle | 30 +- .../gradle/wrapper/gradle-wrapper.jar | Bin 61608 -> 43583 bytes .../gradle/wrapper/gradle-wrapper.properties | 4 +- examples/extension/gradlew | 34 +- examples/extension/gradlew.bat | 22 +- ...moAutoConfigurationCustomizerProvider.java | 1 + .../javaagent/config/EnvironmentConfig.java | 6 + .../com/example/profile/PyroscopeProfile.java | 4 +- .../javaagent/smoketest/IntegrationTest.java | 23 +- .../smoketest/SpringBootIntegrationTest.java | 36 +- gradle-plugins/build.gradle.kts | 10 +- .../gradle/wrapper/gradle-wrapper.jar | Bin 61608 -> 43583 bytes .../gradle/wrapper/gradle-wrapper.properties | 4 +- gradle-plugins/gradlew | 34 +- gradle-plugins/gradlew.bat | 22 +- gradle-plugins/settings.gradle.kts | 6 +- ...ry.instrumentation.muzzle-check.gradle.kts | 31 +- ...strumentation.muzzle-generation.gradle.kts | 1 + .../javaagent/muzzle/AcceptableVersions.kt | 1 + gradle/wrapper/gradle-wrapper.jar | Bin 61608 -> 43583 bytes gradle/wrapper/gradle-wrapper.properties | 4 +- gradlew | 34 +- gradlew.bat | 22 +- .../otelannotations/AbstractWithSpanTest.java | 4 +- .../build.gradle.kts | 3 +- .../support/AnnotationReflectionHelper.java | 2 +- .../MethodSpanAttributesExtractorTest.java | 9 +- .../build.gradle.kts | 43 + .../DefaultHttpClientInstrumenterBuilder.java | 243 ++++ .../DefaultHttpServerInstrumenterBuilder.java | 219 +++ .../config/internal/CommonConfig.java | 125 ++ .../config/internal/EnduserConfig.java | 97 ++ .../internal/InstrumentationConfig.java | 61 +- .../log/LoggingContextConstants.java | 2 +- .../code/CodeAttributesExtractor.java | 15 +- .../semconv}/code/CodeAttributesGetter.java | 2 +- .../semconv}/code/CodeSpanNameExtractor.java | 6 +- .../db/DbClientAttributesExtractor.java | 14 +- .../semconv}/db/DbClientAttributesGetter.java | 2 +- .../db/DbClientCommonAttributesExtractor.java | 20 +- .../db/DbClientCommonAttributesGetter.java | 2 +- .../db/DbClientSpanNameExtractor.java | 4 +- .../semconv}/db/DbConnectionPoolMetrics.java | 4 +- .../semconv}/db/RedisCommandSanitizer.java | 2 +- .../db/SqlClientAttributesExtractor.java | 35 +- .../SqlClientAttributesExtractorBuilder.java | 13 +- .../db/SqlClientAttributesGetter.java | 2 +- .../api/incubator/semconv}/db/SqlDialect.java | 2 +- .../semconv}/db/SqlStatementInfo.java | 2 +- .../semconv}/db/SqlStatementSanitizer.java | 2 +- .../http/HttpClientExperimentalMetrics.java | 100 ++ ...pClientPeerServiceAttributesExtractor.java | 95 ++ .../HttpExperimentalAttributesExtractor.java | 94 ++ .../http/HttpExperimentalMetricsAdvice.java | 67 + .../semconv/http/HttpMessageBodySizeUtil.java | 37 + .../http/HttpServerExperimentalMetrics.java | 117 ++ .../messaging/CapturedMessageHeadersUtil.java | 2 +- .../semconv}/messaging/MessageOperation.java | 8 +- .../MessagingAttributesExtractor.java | 158 +++ .../MessagingAttributesExtractorBuilder.java | 2 +- .../messaging/MessagingAttributesGetter.java | 34 +- .../messaging/MessagingConsumerMetrics.java | 112 ++ .../messaging/MessagingMetricsAdvice.java | 70 + .../messaging/MessagingProducerMetrics.java | 85 ++ .../messaging/MessagingSpanNameExtractor.java | 4 +- .../net/PeerServiceAttributesExtractor.java | 77 + .../semconv/net/PeerServiceResolver.java | 23 + .../semconv/net/PeerServiceResolverImpl.java | 99 ++ .../semconv/net/internal/UrlParser.java | 148 ++ .../semconv}/rpc/RpcAttributesGetter.java | 2 +- .../rpc/RpcClientAttributesExtractor.java | 4 +- .../semconv}/rpc/RpcClientMetrics.java | 18 +- .../rpc/RpcCommonAttributesExtractor.java | 15 +- .../semconv/rpc/RpcMetricsAdvice.java | 60 + .../rpc/RpcServerAttributesExtractor.java | 4 +- .../semconv}/rpc/RpcServerMetrics.java | 18 +- .../semconv}/rpc/RpcSpanNameExtractor.java | 2 +- .../semconv}/util/ClassAndMethod.java | 4 +- .../util/ClassAndMethodAttributesGetter.java | 4 +- .../incubator/semconv}/util/SpanNames.java | 2 +- .../src/main/jflex/SqlSanitizer.jflex | 138 +- .../code/CodeAttributesExtractorTest.java | 8 +- .../code/CodeSpanNameExtractorTest.java | 18 +- .../db/DbClientAttributesExtractorTest.java | 17 +- .../db/DbClientSpanNameExtractorTest.java | 2 +- .../db/RedisCommandSanitizerTest.java | 2 +- .../db/SqlClientAttributesExtractorTest.java | 31 +- .../db/SqlStatementSanitizerTest.java | 89 +- .../HttpClientExperimentalMetricsTest.java | 91 +- ...entPeerServiceAttributesExtractorTest.java | 106 ++ ...tpExperimentalAttributesExtractorTest.java | 60 + .../HttpServerExperimentalMetricsTest.java | 155 +- .../MessagingAttributesExtractorTest.java | 85 +- .../MessagingProducerMetricsTest.java | 125 ++ .../MessagingSpanNameExtractorTest.java | 4 +- .../PeerServiceAttributesExtractorTest.java | 53 +- .../semconv/net/PeerServiceResolverTest.java | 38 + .../semconv/net/internal/UrlParserTest.java | 249 ++++ .../rpc/RpcAttributesExtractorTest.java | 16 +- .../semconv}/rpc/RpcClientMetricsTest.java | 49 +- .../semconv}/rpc/RpcServerMetricsTest.java | 49 +- .../rpc/RpcSpanNameExtractorTest.java | 2 +- instrumentation-api-semconv/build.gradle.kts | 85 -- .../http/ForwardedHeaderParser.java | 112 -- .../api/instrumenter/http/HttpAttributes.java | 27 - .../http/HttpClientAttributesExtractor.java | 202 --- .../HttpClientAttributesExtractorBuilder.java | 75 - .../instrumenter/http/HttpClientMetrics.java | 112 -- .../http/HttpCommonAttributesExtractor.java | 181 --- .../http/HttpMessageBodySizeUtil.java | 48 - .../instrumenter/http/HttpMetricsUtil.java | 50 - .../http/HttpNetworkTransportFilter.java | 35 - .../instrumenter/http/HttpRouteSource.java | 30 - .../http/HttpServerAttributesExtractor.java | 244 ---- .../HttpServerAttributesExtractorBuilder.java | 75 - .../instrumenter/http/HttpServerMetrics.java | 126 -- .../http/HttpSpanNameExtractor.java | 51 - .../http/HttpStatusConverter.java | 34 - .../http/TemporaryMetricsView.java | 127 -- .../MessagingAttributesExtractor.java | 133 -- .../net/NetClientAttributesExtractor.java | 75 - .../net/NetClientAttributesGetter.java | 55 - .../net/NetServerAttributesExtractor.java | 82 -- .../net/NetServerAttributesGetter.java | 61 - .../net/PeerServiceAttributesExtractor.java | 79 -- .../net/internal/FallbackNamePortGetter.java | 26 - .../InternalNetClientAttributesExtractor.java | 58 - .../InternalNetServerAttributesExtractor.java | 67 - .../net/internal/NetAttributes.java | 26 - .../net/internal/NoopNamePortGetter.java | 24 - .../network/ClientAttributesGetter.java | 84 -- .../network/NetworkAttributesGetter.java | 62 - .../network/ServerAttributesExtractor.java | 61 - .../network/ServerAttributesGetter.java | 102 -- .../InternalClientAttributesExtractor.java | 93 -- .../InternalServerAttributesExtractor.java | 163 --- .../network/internal/NetworkAttributes.java | 53 - .../internal/NetworkTransportFilter.java | 25 - .../api/instrumenter/rpc/MetricsView.java | 103 -- .../InternalUrlAttributesExtractor.java | 69 - .../url/internal/UrlAttributes.java | 30 - .../api/server/ServerSpan.java | 33 - .../http/ForwardedHeaderParserTest.java | 292 ---- .../HttpClientAttributesExtractorTest.java | 321 ----- .../http/HttpClientMetricsTest.java | 186 --- .../http/HttpClientResendTest.java | 28 - .../http/HttpClientStatusConverterTest.java | 94 -- .../HttpServerAttributesExtractorTest.java | 342 ----- .../http/HttpServerMetricsTest.java | 331 ----- .../http/HttpServerStatusConverterTest.java | 94 -- .../http/TemporaryMetricsViewTest.java | 241 ---- ...tAddressNetClientAttributesGetterTest.java | 108 -- ...tAddressNetServerAttributesGetterTest.java | 137 -- .../net/NetClientAttributesExtractorTest.java | 276 ---- .../net/NetServerAttributesExtractorTest.java | 287 ---- ...ributesExtractorInetSocketAddressTest.java | 81 -- ...entAttributesExtractorBothSemconvTest.java | 179 --- ...verAttributesExtractorBothSemconvTest.java | 203 --- ...entAttributesExtractorBothSemconvTest.java | 149 -- ...verAttributesExtractorBothSemconvTest.java | 158 --- ...tAttributesExtractorStableSemconvTest.java | 222 --- ...rAttributesExtractorStableSemconvTest.java | 246 ---- ...tAttributesExtractorStableSemconvTest.java | 138 -- ...rAttributesExtractorStableSemconvTest.java | 146 -- instrumentation-api/build.gradle.kts | 7 +- .../instrumenter/InstrumenterBenchmark.java | 25 +- .../api/instrumenter/Instrumenter.java | 69 +- .../api/instrumenter/InstrumenterBuilder.java | 51 +- .../api/instrumenter/SpanSuppressors.java | 26 +- .../api/internal/ClassNames.java | 15 +- .../api/internal/ConfigPropertiesUtil.java | 23 + .../api/internal/ContextPropagationDebug.java | 9 + .../api/internal/HttpConstants.java | 29 + .../api/internal/HttpProtocolUtil.java | 39 + .../api/internal/HttpRouteState.java | 26 +- .../api/internal/InstrumenterAccess.java | 3 + .../internal/InstrumenterBuilderAccess.java | 3 + .../api/internal/InstrumenterUtil.java | 11 + .../api/internal/OperationMetricsUtil.java | 73 + .../api/internal/SchemaUrlProvider.java | 22 + .../api/internal/SemconvStability.java | 54 - .../api/internal/SupportabilityMetrics.java | 24 +- .../cache/weaklockfree/WeakConcurrentMap.java | 2 +- .../http/CapturedHttpHeadersUtil.java | 20 +- .../ForwardedHostAddressAndPortExtractor.java | 97 ++ .../http/ForwardedUrlSchemeProvider.java | 80 ++ .../api/semconv/http/HeaderParsingHelper.java | 28 + .../http/HostAddressAndPortExtractor.java | 36 + .../http/HttpClientAttributesExtractor.java | 148 ++ .../HttpClientAttributesExtractorBuilder.java | 135 ++ .../http/HttpClientAttributesGetter.java | 20 +- .../api/semconv/http/HttpClientMetrics.java | 93 ++ .../http/HttpClientRequestResendCount.java | 28 +- .../http/HttpCommonAttributesExtractor.java | 137 ++ .../http/HttpCommonAttributesGetter.java | 30 +- .../api/semconv/http/HttpMetricsAdvice.java | 59 + .../HttpServerAddressAndPortExtractor.java | 130 ++ .../http/HttpServerAttributesExtractor.java | 116 ++ .../HttpServerAttributesExtractorBuilder.java | 154 ++ .../http/HttpServerAttributesGetter.java | 17 +- .../api/semconv/http/HttpServerMetrics.java | 93 ++ .../api/semconv/http/HttpServerRoute.java | 122 +- .../semconv/http/HttpServerRouteBiGetter.java | 10 +- .../semconv/http/HttpServerRouteBuilder.java | 72 + .../semconv/http/HttpServerRouteGetter.java | 10 +- .../semconv/http/HttpServerRouteSource.java | 45 + .../semconv/http/HttpSpanNameExtractor.java | 105 ++ .../http/HttpSpanNameExtractorBuilder.java | 70 + .../http/HttpSpanStatusExtractor.java | 37 +- .../semconv/http/HttpStatusCodeConverter.java | 30 + .../api/semconv/http/package-info.java | 9 + .../network/ClientAttributesExtractor.java | 34 +- .../network/ClientAttributesGetter.java | 41 + .../network/NetworkAttributesExtractor.java | 21 +- .../network/NetworkAttributesGetter.java | 157 +++ .../network/ServerAttributesExtractor.java | 57 + .../network/ServerAttributesGetter.java | 41 + .../network/internal/AddressAndPort.java | 38 + .../internal/AddressAndPortExtractor.java | 36 + .../ClientAddressAndPortExtractor.java | 38 + .../internal/InetSocketAddressUtil.java | 32 +- .../InternalClientAttributesExtractor.java | 38 + .../InternalNetworkAttributesExtractor.java | 54 +- .../InternalServerAttributesExtractor.java | 37 + .../ServerAddressAndPortExtractor.java | 38 + .../api/semconv/network/package-info.java | 9 + .../api/semconv/package-info.java | 4 + .../semconv}/url/UrlAttributesExtractor.java | 19 +- .../api/semconv}/url/UrlAttributesGetter.java | 4 +- .../InternalUrlAttributesExtractor.java | 47 + .../api/semconv/url/package-info.java | 9 + .../api/instrumenter/InstrumenterTest.java | 76 +- .../SpanSuppressionStrategyTest.java | 21 + .../api/internal/ClassNamesTest.java | 2 +- .../internal/OperationMetricsUtilTest.java | 79 ++ ...wardedHostAddressAndPortExtractorTest.java | 152 ++ .../http/ForwardedUrlSchemeProviderTest.java | 95 ++ .../http/HostAddressAndPortExtractorTest.java | 71 + .../HttpClientAttributesExtractorTest.java | 426 ++++++ .../semconv/http/HttpClientMetricsTest.java | 130 ++ .../HttpClientRequestResendCountTest.java | 28 + ...HttpServerAddressAndPortExtractorTest.java | 150 ++ .../HttpServerAttributesExtractorTest.java | 559 ++++++++ .../semconv/http/HttpServerMetricsTest.java | 183 +++ .../api/semconv/http/HttpServerRouteTest.java | 114 +- .../http/HttpSpanNameExtractorTest.java | 2 +- .../http/HttpSpanStatusExtractorTest.java | 14 +- .../HttpStatusCodeConverterClientTest.java | 93 ++ .../HttpStatusCodeConverterServerTest.java | 93 ++ .../http/ValidRequestMethodsProvider.java | 20 + .../ClientAttributesExtractorTest.java | 44 +- ...ributesExtractorInetSocketAddressTest.java | 76 + .../NetworkAttributesExtractorTest.java | 63 +- .../ServerAttributesExtractorTest.java | 64 +- .../url/UrlAttributesExtractorTest.java | 4 +- .../javaagent/build.gradle.kts | 15 +- .../AkkaForkJoinPoolInstrumentation.java | 3 +- .../akka-http-10.0/javaagent/build.gradle.kts | 34 +- .../akkahttp/AkkaHttpServerJavaRouteTest.java | 64 + .../AkkaHttpClientAttributesGetter.java | 26 +- .../client/AkkaHttpClientSingletons.java | 27 +- .../akkahttp/client/AkkaHttpClientUtil.java | 52 + .../client/AkkaHttpNetAttributesGetter.java | 39 - .../client/HttpExtClientInstrumentation.java | 18 +- .../PoolMasterActorInstrumentation.java | 6 +- .../akkahttp/server/AkkaFlowWrapper.java | 2 + .../AkkaHttpServerAttributesGetter.java | 15 +- .../AkkaHttpServerInstrumentationModule.java | 12 +- .../server/AkkaHttpServerSingletons.java | 41 +- .../AkkaHttpServerSourceInstrumentation.java | 42 + .../server/AkkaNetServerAttributesGetter.java | 42 - ...aHttpServerRouteInstrumentationModule.java | 40 + .../server/route/AkkaRouteHolder.java | 88 ++ .../PathConcatenationInstrumentation.java | 43 + .../route/PathMatcherInstrumentation.java | 47 + .../PathMatcherStaticInstrumentation.java | 65 + .../RouteConcatenationInstrumentation.java | 67 + ...bstractHttpServerInstrumentationTest.scala | 13 +- .../AkkaHttpClientInstrumentationTest.scala | 1 + .../AkkaHttpServerInstrumentationTest.scala | 76 +- .../akkahttp/AkkaHttpServerRouteTest.scala | 113 ++ .../AkkaHttpTestServerSourceWebServer.scala | 124 ++ .../akkahttp/AkkaHttpTestWebServer.scala | 109 +- .../javaagent/build.gradle.kts | 20 + .../v1_0/DruidDataSourceInstrumentation.java | 60 + .../v1_0/DruidInstrumentationModule.java} | 10 +- .../alibabadruid/v1_0/DruidSingletons.java | 21 + .../v1_0/DruidInstrumentationTest.java | 34 + .../library/build.gradle.kts | 10 + .../v1_0/ConnectionPoolMetrics.java | 66 + .../alibabadruid/v1_0/DruidTelemetry.java | 30 + .../v1_0/DruidInstrumentationTest.java | 44 + .../testing/build.gradle.kts | 11 + .../AbstractDruidInstrumentationTest.java | 88 ++ .../v2_0/ApacheDbcpInstrumentationTest.java | 2 +- .../apachedbcp/v2_0/DataSourceMetrics.java | 2 +- .../v2_0/ApacheDbcpInstrumentationTest.java | 2 +- .../javaagent/build.gradle.kts | 28 +- .../v2_7/DubboInstrumentationModule.java | 16 +- .../apachedubbo/v2_7/OpenTelemetryFilter.java | 10 +- .../META-INF}/org.apache.dubbo.rpc.Filter | 0 .../apachedubbo/v2_7/DubboTest.groovy | 11 - .../v2_7/DubboTraceChainTest.groovy | 11 - .../apachedubbo/v2_7/DubboAgentTest.java | 22 + .../v2_7/DubboAgentTraceChainTest.java | 22 + .../apachedubbo/v2_7/DubboHeadersGetter.java | 2 + .../DubboNetworkServerAttributesGetter.java | 29 + .../v2_7/DubboRpcAttributesGetter.java | 2 +- .../v2_7/DubboTelemetryBuilder.java | 64 +- .../apachedubbo/v2_7/TracingFilter.java | 1 + ...> DubboClientNetworkAttributesGetter.java} | 9 +- .../DubboNetServerAttributesGetter.java | 46 - .../apachedubbo/v2_7/DubboTest.groovy | 11 - .../v2_7/DubboTraceChainTest.groovy | 11 - .../apachedubbo/v2_7/DubboTest.java | 21 + .../apachedubbo/v2_7/DubboTraceChainTest.java | 21 + .../apachedubbo/v2_7/AbstractDubboTest.groovy | 203 --- .../v2_7/AbstractDubboTraceChainTest.groovy | 279 ---- .../apachedubbo/v2_7/DubboTestUtil.groovy | 26 - .../apachedubbo/v2_7/AbstractDubboTest.java | 287 ++++ .../v2_7/AbstractDubboTraceChainTest.java | 411 ++++++ .../apachedubbo/v2_7/DubboTestUtil.java | 49 + ...heHttpAsyncClientHttpAttributesGetter.java | 33 +- ...cheHttpAsyncClientNetAttributesGetter.java | 45 - .../ApacheHttpAsyncClientSingletons.java | 32 +- .../ApacheHttpClientRequest.java | 2 +- .../ApacheHttpAsyncClientTest.java | 38 +- .../ApacheHttpClientHttpAttributesGetter.java | 31 +- .../ApacheHttpClientNetAttributesGetter.java | 44 - .../v2_0/ApacheHttpClientSingletons.java | 32 +- .../AbstractCommonsHttpClientTest.groovy | 104 -- .../CommonsHttpClientLatestDepsTest.groovy | 25 - .../test/groovy/CommonsHttpClientTest.groovy | 19 - .../v2_0/AbstractCommonsHttpClientTest.java | 96 ++ .../v2_0/CommonsHttpClientLatestDepsTest.java | 29 + .../v2_0/CommonsHttpClientTest.java | 24 + .../javaagent/build.gradle.kts | 2 + .../ApacheHttpClientHttpAttributesGetter.java | 25 +- .../ApacheHttpClientNetAttributesGetter.java | 37 - .../v4_0/ApacheHttpClientRequest.java | 3 + .../v4_0/ApacheHttpClientSingletons.java | 32 +- .../v4_0/AbstractApacheHttpClientTest.java | 17 +- .../ApacheHttpClientHttpAttributesGetter.java | 34 +- .../ApacheHttpClientNetAttributesGetter.java | 46 - .../v4_3/ApacheHttpClientRequest.java | 5 +- .../ApacheHttpClientTelemetryBuilder.java | 95 +- .../v4_3/TracingProtocolExec.java | 91 +- .../testing/build.gradle.kts | 10 + .../v4_3/AbstractApacheHttpClientTest.java | 65 +- .../javaagent/build.gradle.kts | 6 + .../ApacheHttpClientHttpAttributesGetter.java | 45 +- .../ApacheHttpClientNetAttributesGetter.java | 55 - .../v5_0/ApacheHttpClientSingletons.java | 32 +- .../v5_0/RequestWithHost.java | 10 +- .../v5_0/AbstractApacheHttpClientTest.java | 10 +- .../v5_0/ApacheHttpAsyncClientTest.java | 8 + .../v5_0/ApacheHttpClientTest.java | 26 + .../apache-httpclient-5.2/library/README.md | 60 + .../library/build.gradle.kts | 9 + ...ApacheHttpClient5HttpAttributesGetter.java | 114 ++ .../v5_2/ApacheHttpClient5Request.java | 136 ++ .../v5_2/ApacheHttpClient5Telemetry.java | 58 + .../ApacheHttpClient5TelemetryBuilder.java | 118 ++ .../v5_2/HttpHeaderSetter.java | 22 + .../v5_2/OtelExecChainHandler.java | 99 ++ .../v5_2/AbstractApacheHttpClient5Test.java | 350 +++++ .../v5_2/ApacheHttpClient5Test.java | 62 + .../apachehttpclient/v5_2/HttpUriRequest.java | 27 + instrumentation/apache-shenyu-2.4/README.md | 5 + .../javaagent/build.gradle.kts | 51 + .../ApacheShenYuInstrumentationModule.java | 24 + .../v2_4/ApacheShenYuSingletons.java | 18 + .../v2_4/ContextBuilderInstrumentation.java | 56 + .../apacheshenyu/v2_4/MetaDataHelper.java | 85 ++ .../v2_4/ShenYuBootstrapApplication.java | 12 + .../apacheshenyu/v2_4/ShenYuRouteTest.java | 137 ++ .../ArmeriaHttpClientAttributesGetter.java | 71 - .../ArmeriaHttpServerAttributesGetter.java | 86 -- .../ArmeriaNetServerAttributesGetter.java | 55 - .../armeria/v1_3/ArmeriaTelemetryBuilder.java | 193 --- .../ArmeriaNetClientAttributesGetter.java | 80 -- .../armeria-1.3/javaagent/build.gradle.kts | 7 +- ...eamMessageSubscriptionInstrumentation.java | 0 .../v1_3/ArmeriaHttpResponseMutator.java | 18 + .../v1_3/ArmeriaInstrumentationModule.java | 0 .../ArmeriaServerBuilderInstrumentation.java | 0 .../armeria/v1_3/ArmeriaSingletons.java | 25 +- ...rmeriaWebClientBuilderInstrumentation.java | 0 .../v1_3/CompletableFutureWrapper.java | 0 .../v1_3/ResponseCustomizingDecorator.java | 47 + .../armeria/v1_3/ServerDecorator.java | 19 +- .../armeria/v1_3/SubscriberWrapper.java | 0 .../armeria/v1_3/ArmeriaHttp2ServerTest.java | 17 + .../armeria/v1_3/ArmeriaHttp2Test.java | 136 ++ .../armeria/v1_3/ArmeriaHttpClientTest.java | 0 .../armeria/v1_3/ArmeriaHttpServerTest.java | 7 + .../armeria-1.3/library/build.gradle.kts | 4 +- .../armeria/v1_3/ArmeriaTelemetry.java | 0 .../armeria/v1_3/ArmeriaTelemetryBuilder.java | 260 ++++ .../armeria/v1_3/OpenTelemetryClient.java | 0 .../armeria/v1_3/OpenTelemetryService.java | 0 .../ArmeriaHttpClientAttributesGetter.java | 161 +++ .../ArmeriaHttpServerAttributesGetter.java | 110 ++ .../ArmeriaInstrumenterBuilderFactory.java | 37 + .../ArmeriaInstrumenterBuilderUtil.java | 67 + .../internal}/ClientRequestContextSetter.java | 2 +- .../v1_3/internal/RequestContextAccess.java | 6 +- .../v1_3/internal}/RequestContextGetter.java | 2 +- .../armeria/v1_3/ArmeriaHttp2ServerTest.java | 17 + .../armeria/v1_3/ArmeriaHttpClientTest.java | 0 .../armeria/v1_3/ArmeriaHttpServerTest.java | 8 + .../armeria-1.3/testing/build.gradle.kts | 0 .../v1_3/AbstractArmeriaHttpClientTest.java | 12 + .../v1_3/AbstractArmeriaHttpServerTest.java | 4 +- .../javaagent/build.gradle.kts | 23 + ...meriaGrpcClientBuilderInstrumentation.java | 43 + .../ArmeriaGrpcInstrumentationModule.java | 27 + ...eriaGrpcServiceBuilderInstrumentation.java | 42 + .../armeria/grpc/v1_14/ArmeriaGrpcTest.java | 127 ++ .../javaagent/build.gradle.kts | 12 +- .../AsyncHttpClientHttpAttributesGetter.java | 12 +- .../AsyncHttpClientNetAttributesGetter.java | 37 - .../v1_9/AsyncHttpClientSingletons.java | 32 +- .../test/groovy/AsyncHttpClientTest.groovy | 90 -- .../v1_9/AsyncHttpClientTest.java | 114 ++ .../javaagent/build.gradle.kts | 26 +- ...tpClientAdditionalAttributesExtractor.java | 39 - .../AsyncHttpClientHttpAttributesGetter.java | 63 +- .../AsyncHttpClientNetAttributesGetter.java | 72 - .../v2_0/AsyncHttpClientSingletons.java | 33 +- .../test/groovy/AsyncHttpClientTest.groovy | 66 - .../v2_0/AsyncHttpClientTest.java | 101 ++ instrumentation/aws-lambda/README.md | 8 +- .../javaagent/build.gradle.kts | 1 - .../v1_0/AwsLambdaInstrumentationModule.java | 9 + ...wsLambdaRequestHandlerInstrumentation.java | 10 +- .../AwsLambdaInternalRequestHandler.java | 23 + .../awslambdacore/v1_0/AwsLambdaTest.java | 40 + ...AwsLambdaLegacyInternalRequestHandler.java | 23 + .../aws-lambda-core-1.0/library/README.md | 2 +- .../library/build.gradle.kts | 4 +- .../v1_0/TracingRequestStreamHandler.java | 45 +- .../v1_0/TracingRequestStreamWrapper.java | 5 +- .../AwsLambdaFunctionAttributesExtractor.java | 14 +- .../AwsLambdaFunctionInstrumenter.java | 12 +- .../AwsLambdaFunctionInstrumenterFactory.java | 1 - .../AwsXrayEnvSpanLinksExtractor.java | 84 -- .../v1_0/internal/HeadersFactory.java | 4 +- ...ambdaStreamWrapperHttpPropagationTest.java | 18 +- .../v1_0/AwsLambdaStreamWrapperTest.java | 16 +- .../AwsXrayEnvSpanLinksExtractorTest.java | 128 -- .../internal/InstrumenterExtractionTest.java | 45 + .../testing/build.gradle.kts | 2 - .../v1_0/AbstractAwsLambdaTest.java | 38 +- .../v2_2/AwsLambdaInstrumentationHelper.java | 4 +- .../aws-lambda-events-2.2/library/README.md | 2 +- .../library/build.gradle.kts | 9 + .../v2_2/LambdaParameters.java | 31 + .../v2_2/TracingRequestApiGatewayWrapper.java | 17 +- .../v2_2/TracingRequestWrapper.java | 84 +- .../v2_2/TracingRequestWrapperBase.java | 10 +- .../ApiGatewayProxyAttributesExtractor.java | 46 +- .../AwsLambdaEventsInstrumenterFactory.java | 6 +- .../AwsLambdaSqsInstrumenterFactory.java | 10 +- .../v2_2/{ => internal}/CustomJodaModule.java | 2 +- .../v2_2/internal/SerializationUtil.java | 133 ++ .../internal/SqsEventAttributesExtractor.java | 15 +- .../SqsMessageAttributesExtractor.java | 23 +- .../v2_2/AwsLambdaApiGatewayWrapperTest.java | 56 +- .../v2_2/AwsLambdaSqsEventWrapperTest.java | 18 +- .../v2_2/AwsLambdaSqsMessageHandlerTest.java | 33 +- .../v2_2/AwsLambdaWrapperTest.java | 74 +- .../v2_2/LambdaParametersTest.java | 130 ++ ...acingRequestWrapperStandardEventsTest.java | 398 +++--- .../v2_2/internal/SerializationUtilTest.java | 359 +++++ .../testing/build.gradle.kts | 2 - .../AbstractAwsLambdaSqsEventHandlerTest.java | 21 +- instrumentation/aws-sdk/README.md | 13 +- .../aws-sdk-1.11/javaagent/build.gradle.kts | 70 +- .../awssdk/v1_11/SqsAdviceBridge.java | 15 + .../AbstractAwsSdkInstrumentationModule.java | 67 + .../v1_11/AwsSdkInstrumentationModule.java | 11 +- .../v1_11/SqsInstrumentationModule.java | 38 + .../awssdk/v1_11/TracingRequestHandler.java | 8 +- .../src/test/groovy/S3TracingTest.groovy | 540 +++---- .../src/test/groovy/SnsTracingTest.groovy | 149 +- .../awssdk/v1_11/Aws1ClientTest.groovy | 38 +- .../awssdk/v1_11/SqsTracingTest.groovy | 17 - .../awssdk/v1_11/SqsTracingTest.java | 28 + .../v1_11/SqsSuppressReceiveSpansTest.java | 28 + .../groovy/Aws0ClientTest.groovy | 86 +- .../library-autoconfigure/build.gradle.kts | 41 +- .../autoconfigure/TracingRequestHandler.java | 8 + .../v1_11/instrumentor/SqsTracingTest.groovy | 17 - .../SqsSuppressReceiveSpansTest.java | 28 + .../awssdk/v1_11/SqsTracingTest.java | 28 + .../aws-sdk-1.11/library/build.gradle.kts | 19 +- .../awssdk/v1_11/AbstractSqsRequest.java | 13 + .../v1_11/AwsSdkHttpAttributesGetter.java | 55 +- .../v1_11/AwsSdkInstrumenterFactory.java | 175 ++- .../v1_11/AwsSdkNetAttributesGetter.java | 64 - .../v1_11/AwsSdkRpcAttributesGetter.java | 2 +- .../awssdk/v1_11/AwsSdkSpanKindExtractor.java | 26 - .../awssdk/v1_11/AwsSdkTelemetry.java | 33 +- .../awssdk/v1_11/AwsSdkTelemetryBuilder.java | 35 +- .../awssdk/v1_11/PluginImplUtil.java | 58 + .../awssdk/v1_11/RequestAccess.java | 16 + .../awssdk/v1_11/SnsAttributesExtractor.java | 49 + .../awssdk/v1_11/SqsAccess.java | 45 + .../awssdk/v1_11/SqsAttributesGetter.java | 91 ++ .../instrumentation/awssdk/v1_11/SqsImpl.java | 148 ++ .../awssdk/v1_11/SqsMessage.java | 21 + .../awssdk/v1_11/SqsMessageAccess.java | 63 - .../awssdk/v1_11/SqsMessageImpl.java | 49 + .../awssdk/v1_11/SqsProcessRequest.java | 31 + .../SqsProcessRequestAttributesGetter.java | 91 ++ .../v1_11/SqsReceiveMessageRequestAccess.java | 99 -- .../v1_11/SqsReceiveMessageResultAccess.java | 64 - .../awssdk/v1_11/SqsReceiveRequest.java | 32 + .../SqsReceiveRequestAttributesGetter.java | 94 ++ .../awssdk/v1_11/TracingIterator.java | 99 ++ .../awssdk/v1_11/TracingList.java | 95 ++ .../awssdk/v1_11/TracingRequestHandler.java | 129 +- .../awssdk/v1_11/SqsTracingTest.groovy | 20 - .../v1_11/SqsSuppressReceiveSpansTest.java | 31 + .../awssdk/v1_11/SqsTracingTest.java | 35 + .../v1_11/AbstractAws1ClientTest.groovy | 81 +- .../v1_11/AbstractSqsTracingTest.groovy | 178 --- .../AbstractSqsSuppressReceiveSpansTest.java | 326 +++++ .../awssdk/v1_11/AbstractSqsTracingTest.java | 493 +++++++ .../aws-sdk-2.2/javaagent/build.gradle.kts | 144 +- .../awssdk/v2_2/LambdaAdviceBridge.java | 15 + .../awssdk/v2_2/SnsAdviceBridge.java | 15 + .../awssdk/v2_2/SqsAdviceBridge.java | 6 +- .../AbstractAwsSdkInstrumentationModule.java | 48 +- .../v2_2/AwsSdkInstrumentationModule.java | 39 +- ...tSqsAsyncClientBuilderInstrumentation.java | 39 + ...efaultSqsClientBuilderInstrumentation.java | 39 + .../v2_2/LambdaInstrumentationModule.java | 47 + .../awssdk/v2_2/SnsInstrumentationModule.java | 45 + .../awssdk/v2_2/SqsInstrumentationModule.java | 34 +- .../awssdk/v2_2/S3PresignerTest.java | 71 + .../Aws2SqsSuppressReceiveSpansTest.groovy | 27 + .../src/test/groovy/Aws2SqsTracingTest.groovy | 15 - .../v2_2/Aws2ClientRecordHttpErrorTest.java | 27 + .../awssdk/v2_2/Aws2LambdaTest.java | 35 + .../awssdk/v2_2/Aws2SqsTracingTest.java | 40 + .../awssdk/v2_2/AwsXrayPropagatorTest.java | 55 + .../awssdk/v2_2/QueryProtocolModelTest.java | 27 + .../library-autoconfigure/build.gradle.kts | 23 +- .../v2_2/autoconfigure/AwsSdkSingletons.java | 83 ++ .../TracingExecutionInterceptor.java | 17 +- .../aws-sdk/aws-sdk-2.2/library/README.md | 33 +- .../aws-sdk-2.2/library/build.gradle.kts | 65 +- .../awssdk/v2_2/AbstractSqsRequest.java | 13 + ...AwsSdkExperimentalAttributesExtractor.java | 5 +- .../v2_2/AwsSdkHttpAttributesGetter.java | 28 +- ...pClientSuppressionAttributesExtractor.java | 44 + .../v2_2/AwsSdkInstrumenterFactory.java | 204 ++- .../v2_2/AwsSdkNetAttributesGetter.java | 31 - .../awssdk/v2_2/AwsSdkRequest.java | 2 + .../awssdk/v2_2/AwsSdkRequestType.java | 15 +- .../v2_2/AwsSdkRpcAttributesGetter.java | 2 +- .../awssdk/v2_2/AwsSdkSpanKindExtractor.java | 27 - .../awssdk/v2_2/AwsSdkTelemetry.java | 64 +- .../awssdk/v2_2/AwsSdkTelemetryBuilder.java | 51 +- .../awssdk/v2_2/LambdaAccess.java | 21 + .../awssdk/v2_2/LambdaImpl.java | 102 ++ .../awssdk/v2_2/PluginImplUtil.java | 58 + .../awssdk/v2_2/RequestHeaderSetter.java | 2 +- .../instrumentation/awssdk/v2_2/Response.java | 31 + .../awssdk/v2_2/SnsAccess.java | 23 + .../instrumentation/awssdk/v2_2/SnsImpl.java | 75 + .../awssdk/v2_2/SqsAccess.java | 75 +- .../awssdk/v2_2/SqsAttributesGetter.java | 102 ++ .../instrumentation/awssdk/v2_2/SqsImpl.java | 350 ++++- .../awssdk/v2_2/SqsMessage.java | 24 + .../awssdk/v2_2/SqsMessageImpl.java | 54 + .../awssdk/v2_2/SqsParentContext.java | 19 + .../awssdk/v2_2/SqsProcessRequest.java | 31 + .../SqsProcessRequestAttributesGetter.java | 97 ++ .../v2_2/SqsReceiveMessageRequestAccess.java | 122 -- .../awssdk/v2_2/SqsReceiveRequest.java | 32 + .../SqsReceiveRequestAttributesGetter.java | 99 ++ .../v2_2/SqsSendMessageRequestAccess.java | 23 - .../awssdk/v2_2/SqsTracingContext.java | 37 + .../v2_2/TracingExecutionInterceptor.java | 279 ++-- .../awssdk/v2_2/TracingIterator.java | 104 ++ .../awssdk/v2_2/TracingList.java | 76 + .../awssdk/v2_2/Aws2ClientTest.groovy | 2 +- .../Aws2SqsSuppressReceiveSpansTest.groovy | 109 ++ .../awssdk/v2_2/Aws2SqsTracingTest.groovy | 21 - ...Aws2SqsTracingTestWithW3CPropagator.groovy | 23 - ...tWithW3CPropagatorAndXrayPropagator.groovy | 23 - .../Aws2ClientNotRecordHttpErrorTest.java | 38 + .../v2_2/Aws2SqsDefaultPropagatorTest.java | 48 + .../awssdk/v2_2/Aws2SqsTracingTest.java | 59 + ...SqsW3cPropagatorAndXrayPropagatorTest.java | 19 + .../awssdk/v2_2/Aws2SqsW3cPropagatorTest.java | 28 + .../awssdk/v2_2/FieldMapperTest.java | 13 +- .../awssdk/v2_2/QueryProtocolModelTest.java | 29 + .../awssdk/v2_2/SerializerTest.java | 14 +- .../awssdk/v2_2/Aws2ClientDynamodbTest.groovy | 24 + .../awssdk/v2_2/Aws2LambdaTest.java | 107 ++ .../aws-sdk-2.2/testing/build.gradle.kts | 22 +- .../v2_2/AbstractAws2ClientCoreTest.groovy | 288 ++++ .../awssdk/v2_2/AbstractAws2ClientTest.groovy | 645 ++++----- ...ractAws2SqsSuppressReceiveSpansTest.groovy | 392 ++++++ .../v2_2/AbstractAws2SqsTracingTest.groovy | 206 --- ...AbstractAws2ClientRecordHttpErrorTest.java | 210 +++ .../awssdk/v2_2/AbstractAws2LambdaTest.java | 78 ++ .../v2_2/AbstractAws2SqsTracingTest.java | 627 +++++++++ .../v2_2/AbstractQueryProtocolModelTest.java | 110 ++ .../javaagent/build.gradle.kts | 24 +- .../v1_14/AzureSdkInstrumentationModule.java | 18 +- .../azurecore/v1_14}/AzureSdkTest.java | 2 + .../build.gradle.kts | 3 +- .../javaagent/build.gradle.kts | 24 +- .../v1_19/AzureSdkInstrumentationModule.java | 18 +- .../azurecore/v1_19}/AzureSdkTest.java | 2 + .../build.gradle.kts | 3 +- .../javaagent/build.gradle.kts | 24 +- .../v1_36/AzureHttpClientInstrumentation.java | 33 +- .../v1_36/AzureSdkInstrumentationModule.java | 28 +- ...o.java => SuppressNestedClientHelper.java} | 36 +- .../com.azure.core.util.tracing.Tracer | 1 + .../azurecore/v1_36}/AzureSdkTest.java | 2 + .../azurecore/v1_36/AzureSdkTestOld.java | 62 + .../build.gradle.kts | 5 +- .../OpenTelemetryTracingOptions.java | 16 + .../c3p0/v0_9/C3p0InstrumentationTest.java | 2 +- .../c3p0/v0_9/ConnectionPoolMetrics.java | 2 +- .../c3p0/v0_9/C3p0InstrumentationTest.java | 2 +- instrumentation/camel-2.20/README.md | 6 +- .../javaagent-unit-tests/build.gradle.kts | 2 +- .../decorators/SanitizationTest.groovy | 83 -- .../apachecamel/CamelPropagationUtilTest.java | 0 .../decorators/SanitizationTest.java | 104 ++ .../camel-2.20/javaagent/build.gradle.kts | 11 +- .../decorators/BaseSpanDecorator.java | 4 +- .../decorators/DbSpanDecorator.java | 14 +- .../decorators/DecoratorRegistry.java | 12 +- .../decorators/HttpSpanDecorator.java | 33 +- .../decorators/KafkaSpanDecorator.java | 11 +- .../decorators/MessagingSpanDecorator.java | 7 +- .../apachecamel/DirectCamelTest.groovy | 65 - .../apachecamel/DirectConfig.groovy | 45 - .../apachecamel/MulticastConfig.groovy | 62 - .../MulticastDirectCamelTest.groovy | 76 - .../apachecamel/RestCamelTest.groovy | 126 -- .../apachecamel/RestConfig.groovy | 47 - .../apachecamel/SingleServiceCamelTest.groovy | 74 - .../apachecamel/SingleServiceConfig.groovy | 35 - .../apachecamel/TwoServicesConfig.groovy | 54 - ...woServicesWithDirectClientCamelTest.groovy | 153 -- .../apachecamel/aws/AwsConnector.groovy | 173 --- .../apachecamel/aws/AwsSpan.groovy | 99 -- .../apachecamel/aws/CamelSpan.groovy | 81 -- .../apachecamel/aws/CamelSpringApp.groovy | 57 - .../apachecamel/aws/S3CamelTest.groovy | 109 -- .../apachecamel/aws/S3Config.groovy | 48 - .../apachecamel/aws/SnsCamelTest.groovy | 133 -- .../apachecamel/aws/SnsConfig.groovy | 45 - .../apachecamel/aws/SqsCamelTest.groovy | 138 -- .../apachecamel/aws/SqsConfig.groovy | 64 - .../decorators/CassandraTest.groovy | 114 -- .../apachecamel/DirectCamelTest.java | 82 ++ .../apachecamel/DirectConfig.java | 45 + .../apachecamel/MulticastConfig.java | 62 + .../apachecamel/MulticastDirectCamelTest.java | 88 ++ .../apachecamel/RestCamelTest.java | 139 ++ .../apachecamel/RestConfig.java | 44 + .../apachecamel/SingleServiceCamelTest.java | 78 ++ .../apachecamel/SingleServiceConfig.java | 35 + .../apachecamel/TwoServicesConfig.java | 54 + .../TwoServicesWithDirectClientCamelTest.java | 182 +++ .../apachecamel/aws/AwsConnector.java | 185 +++ .../apachecamel/aws/AwsSpanAssertions.java | 168 +++ .../apachecamel/aws/CamelSpanAssertions.java | 70 + .../aws/CamelSpringApplication.java | 64 + .../apachecamel/aws/S3CamelTest.java | 140 ++ .../apachecamel/aws/S3Config.java | 48 + .../apachecamel/aws/SnsCamelTest.java | 182 +++ .../apachecamel/aws/SnsConfig.java | 45 + .../apachecamel/aws/SqsCamelTest.java | 147 ++ .../apachecamel/aws/SqsConfig.java | 64 + .../decorators/CassandraConfig.java} | 20 +- .../apachecamel/decorators/CassandraTest.java | 125 ++ .../v3_0/CassandraAttributesExtractor.java | 36 + .../v3_0/CassandraNetAttributesGetter.java | 34 - .../CassandraNetworkAttributesGetter.java | 22 + .../cassandra/v3_0/CassandraSingletons.java | 17 +- .../v3_0/CassandraSqlAttributesGetter.java | 6 +- .../src/test/java/CassandraClientTest.java | 152 +- .../testing/build.gradle.kts | 2 +- .../v4/common/AbstractCassandraTest.java | 104 +- .../v4_0/CassandraAttributesExtractor.java | 26 +- ... => CassandraNetworkAttributesGetter.java} | 20 +- .../cassandra/v4_0/CassandraSingletons.java | 16 +- .../v4_0/CassandraSqlAttributesGetter.java | 6 +- .../cassandra/v4_0}/CassandraTest.java | 4 +- .../cassandra/v4_4/CassandraSingletons.java | 4 +- .../v4_4/CassandraAttributesExtractor.java | 83 +- .../v4_4/CassandraNetAttributesGetter.java | 46 - .../CassandraNetworkAttributesGetter.java | 37 + .../v4_4/CassandraSqlAttributesGetter.java | 7 +- .../v4_4/CassandraTelemetryBuilder.java | 15 +- .../v4_4/AbstractCassandra44Test.java | 65 +- .../src/test/groovy/CDIContainerTest.groovy | 29 - .../test/cdi/CdiContainerTest.java | 36 + .../opentelemetry/test/cdi}/TestBean.java | 2 + .../javaagent/build.gradle.kts | 28 + .../ClickHouseAttributesGetter.java | 58 + .../ClickHouseClientInstrumentation.java | 101 ++ .../clickhouse/ClickHouseDbRequest.java | 30 + .../ClickHouseInstrumentationModule.java | 26 + .../ClickHouseNetworkAttributesGetter.java | 22 + .../clickhouse/ClickHouseSingletons.java | 38 + .../clickhouse/ClickHouseClientTest.java | 344 +++++ instrumentation/couchbase/README.md | 4 +- .../javaagent-unit-tests/build.gradle.kts | 5 +- .../v2_0/CouchbaseQuerySanitizerTest.groovy | 36 - .../v2_0/CouchbaseQuerySanitizerTest.java | 83 ++ .../v2_0/CouchbaseQuerySanitizer.java | 10 +- .../couchbase/v2_0/CouchbaseRequestInfo.java | 2 +- .../v2_0/CouchbaseAttributesGetter.java | 6 +- .../v2_0/CouchbaseInstrumentationModule.java | 11 +- ... => CouchbaseNetworkAttributesGetter.java} | 20 +- .../couchbase/v2_0/CouchbaseSingletons.java | 19 +- .../src/test/groovy/CouchbaseSpanUtil.groovy | 17 +- .../build.gradle.kts | 3 +- .../couchbase-3.1/javaagent/build.gradle.kts | 3 +- .../test/groovy/CouchbaseClient31Test.groovy | 83 -- .../couchbase/v3_1/CouchbaseClient31Test.java | 90 ++ .../build.gradle.kts | 2 +- .../test/groovy/CouchbaseClient32Test.groovy | 80 -- .../couchbase/v3_2/CouchbaseClient32Test.java | 83 ++ .../build.gradle.kts | 3 +- .../groovy/util/AbstractCouchbaseTest.groovy | 10 +- .../dropwizard-testing/build.gradle.kts | 8 + .../src/test/groovy/DropwizardTest.groovy | 13 +- .../javaagent/build.gradle.kts | 5 + .../src/test/groovy/ViewRenderTest.groovy | 63 - .../dropwizardviews/ViewRenderTest.java | 67 + instrumentation/elasticsearch/README.md | 14 +- .../javaagent-unit-tests/build.gradle.kts | 8 + .../rest/ElasticsearchEndpointMapTest.java | 124 ++ .../javaagent/build.gradle.kts | 87 ++ ...csearchApiClientInstrumentationModule.java | 45 + .../apiclient/ElasticsearchEndpointMap.java | 883 ++++++++++++ .../elasticsearch/apiclient/EndpointId.java | 28 + .../RestClientHttpClientInstrumentation.java | 79 ++ .../RestClientTransportInstrumentation.java | 56 + .../apiclient/ElasticsearchClientTest.java | 236 ++++ .../v5_0/ElasticsearchRest5Singletons.java | 7 +- .../rest/v5_0/RestClientInstrumentation.java | 4 +- .../rest/v5_0}/ElasticsearchRest5Test.java | 59 +- .../javaagent/build.gradle.kts | 3 - .../v6_4/ElasticsearchRest6Singletons.java | 7 +- .../rest/v6_4/RestClientInstrumentation.java | 4 +- .../rest/v6_4}/ElasticsearchRest6Test.java | 59 +- .../javaagent/build.gradle.kts | 16 +- ...asticsearchRest7InstrumentationModule.java | 19 +- .../v7_0/ElasticsearchRest7Singletons.java | 7 +- .../rest/v7_0/RestClientInstrumentation.java | 29 +- .../test/groovy/ElasticsearchRest7Test.groovy | 171 --- .../rest/v7_0/ElasticsearchRest7Test.java | 200 +++ .../library/build.gradle.kts | 18 + .../v7_0/ElasticsearchRest7Telemetry.java | 46 + .../ElasticsearchRest7TelemetryBuilder.java | 98 ++ .../rest/v7_0/RestClientWrapper.java | 200 +++ .../rest/v7_0/ElasticsearchRest7Test.java | 179 +++ .../javaagent/build.gradle.kts | 3 +- .../ElasticsearchRestAttributesGetter.java | 49 - .../ElasticsearchRestInstrumenterFactory.java | 40 - ...earchRestJavaagentInstrumenterFactory.java | 36 + ...searchRestNetResponseAttributesGetter.java | 47 - .../rest/ElasticsearchRestRequest.java | 20 - .../library/build.gradle.kts | 10 + ...ElasticsearchClientAttributeExtractor.java | 101 ++ .../ElasticsearchDbAttributesGetter.java | 92 ++ .../ElasticsearchEndpointDefinition.java | 191 +++ .../ElasticsearchRestInstrumenterFactory.java | 52 + .../internal/ElasticsearchRestRequest.java | 40 + .../ElasticsearchSpanNameExtractor.java | 27 + .../rest/internal}/RestResponseListener.java | 6 +- .../Elasticsearch5TransportSingletons.java | 6 +- .../Elasticsearch5NodeClientTest.groovy | 30 +- .../Elasticsearch5TransportClientTest.groovy | 61 +- .../javaagent/build.gradle.kts | 9 + .../Elasticsearch53TransportSingletons.java | 6 +- .../Elasticsearch53NodeClientTest.groovy | 30 +- .../Elasticsearch53TransportClientTest.groovy | 61 +- ...Elasticsearch53SpringRepositoryTest.groovy | 78 +- .../Elasticsearch53SpringTemplateTest.groovy | 42 +- ...rch6TransportNetworkAttributesGetter.java} | 20 +- .../Elasticsearch6TransportSingletons.java | 4 +- .../Elasticsearch6NodeClientTest.groovy | 26 +- .../Elasticsearch6TransportClientTest.groovy | 66 +- ...cTransportNetResponseAttributesGetter.java | 56 - ...asticTransportNetworkAttributesGetter.java | 34 + ...lasticsearchTransportAttributesGetter.java | 6 +- ...ticsearchTransportInstrumenterFactory.java | 8 +- instrumentation/executors/README.md | 6 +- .../executors/ContextPropagatingRunnable.java | 44 + .../executors/ExecutorAdviceHelper.java | 34 +- .../executors/javaagent/build.gradle.kts | 10 +- .../AbstractExecutorInstrumentation.java | 144 -- .../executors/ExecutorMatchers.java | 149 ++ .../ExecutorsInstrumentationModule.java | 10 +- .../executors/FutureInstrumentation.java | 2 + .../JavaExecutorInstrumentation.java | 35 +- .../StructuredTaskScopeInstrumentation.java | 57 + ...dPoolExtendingExecutorInstrumentation.java | 58 + .../VirtualThreadInstrumentation.java | 57 + .../LambdaContextPropagationTest.java | 44 + .../executors/ThreadPoolExecutorTest.java | 62 + .../executors/jdk21-testing/build.gradle.kts | 39 + .../executors/StructuredTaskScopeTest.java | 63 + .../executors/VirtualThreadExecutorTest.java | 28 + .../executors/VirtualThreadTest.java | 55 + .../executors/JavaAsyncChild.java | 1 + .../external-annotations/README.md | 8 +- .../javaagent-unit-tests/build.gradle.kts | 1 + .../src/test/groovy/IncludeTest.groovy | 37 - .../extannotations/IncludeTest.java | 53 + .../javaagent/build.gradle.kts | 6 +- .../ExternalAnnotationInstrumentation.java | 15 +- .../ExternalAnnotationSingletons.java | 8 +- .../ConfiguredTraceAnnotationsTest.groovy | 47 - .../test/groovy/TraceAnnotationsTest.groovy | 152 -- .../src/test/groovy/TraceProvidersTest.groovy | 38 - .../groovy/TracedMethodsExclusionTest.groovy | 83 -- .../ConfiguredTraceAnnotationsTest.java | 51 + .../extannotations}/OuterClass.java | 2 + .../extannotations}/SayTracedHello.java | 2 +- .../extannotations/TraceAnnotationsTest.java | 145 ++ .../extannotations/TraceProvidersTest.java | 70 + .../TracedMethodsExclusionTest.java | 89 ++ .../javaagent/build.gradle.kts | 50 + .../finagle/ChannelTransportHelpers.java | 17 + ...enTelemetryChannelInitializerDelegate.java | 22 + .../ChannelTransportInstrumentation.java | 52 + .../FinagleHttpInstrumentationModule.java | 50 + .../finaglehttp/v23_11/Function1Wrapper.java | 24 + ...eamingServerDispatcherInstrumentation.java | 59 + .../H2StreamChannelInitInstrumentation.java | 62 + .../finaglehttp/v23_11/Helpers.java | 110 ++ ...calSchedulerActivationInstrumentation.java | 49 + .../PromiseMonitoredInstrumentation.java | 47 + .../TwitterUtilCoreInstrumentationModule.java | 34 + .../v23_11/AbstractServerTest.java | 100 ++ .../finaglehttp/v23_11/ClientTest.java | 213 +++ .../finaglehttp/v23_11/ServerH1Test.java | 18 + .../finaglehttp/v23_11/ServerH2Test.java | 110 ++ .../finaglehttp/v23_11/Utils.java | 73 + .../finatra-2.9/javaagent/build.gradle.kts | 21 +- .../finatra/FinatraServerLatestTest.scala | 7 +- .../finatra/FinatraCodeAttributesGetter.java | 2 +- .../finatra/FinatraSingletons.java | 11 +- .../finatra/FinatraServerTest.scala | 7 +- .../geode/GeodeDbAttributesGetter.java | 12 +- .../geode/GeodeSingletons.java | 4 +- .../instrumentation/geode/PutGetTest.java | 52 +- .../GoogleHttpClientHttpAttributesGetter.java | 13 +- .../GoogleHttpClientNetAttributesGetter.java | 26 - .../GoogleHttpClientSingletons.java | 32 +- .../AbstractGoogleHttpClientTest.java | 44 +- .../grails-3.0/javaagent/build.gradle.kts | 35 +- .../grails/GrailsServerSpanNaming.java | 4 +- .../instrumentation/grails/HandlerData.java | 2 +- ...ingsInfoHandlerAdapterInstrumentation.java | 8 +- .../src/test/java/test/GrailsTest.java | 15 +- .../src/test/resources/application.yml | 2 +- .../graphql-java-12.0/javaagent/README.md | 5 - .../javaagent/build.gradle.kts | 21 - .../graphql/v12_0/GraphQLTelemetry.java | 65 - instrumentation/graphql-java/README.md | 12 + .../javaagent/build.gradle.kts | 26 + .../graphql/v12_0/GraphqlInstrumentation.java | 2 +- .../v12_0/GraphqlInstrumentationModule.java | 11 + .../graphql/v12_0/GraphqlSingletons.java | 31 + .../graphql/v12_0/GraphqlTest.java | 0 .../graphql-java-12.0/library/README.md | 2 +- .../library/build.gradle.kts | 12 + .../graphql/v12_0/GraphQLTelemetry.java | 42 + .../v12_0/GraphQLTelemetryBuilder.java | 0 .../v12_0/OpenTelemetryInstrumentation.java | 51 + .../graphql/v12_0/GraphqlTest.java | 0 .../javaagent/build.gradle.kts | 34 + .../graphql/v20_0/GraphqlInstrumentation.java | 115 ++ .../v20_0/GraphqlInstrumentationModule.java | 49 + .../graphql/v20_0/GraphqlSingletons.java | 39 + .../graphql/v20_0/GraphqlTest.java | 31 + .../graphql-java-20.0/library/README.md | 40 + .../library/build.gradle.kts | 16 + .../graphql/v20_0/GraphQLTelemetry.java | 50 + .../v20_0/GraphQLTelemetryBuilder.java | 65 + ...ql20OpenTelemetryInstrumentationState.java | 50 + ...GraphqlDataFetcherAttributesExtractor.java | 39 + .../v20_0/GraphqlInstrumenterFactory.java | 35 + .../v20_0/OpenTelemetryInstrumentation.java | 111 ++ .../graphql/v20_0/GraphqlTest.java | 244 ++++ ...OpenTelemetryInstrumentationStateTest.java | 155 ++ .../library/build.gradle.kts | 2 - .../internal}/GraphqlAttributesExtractor.java | 33 +- .../internal/InstrumentationUtil.java} | 28 +- .../OpenTelemetryInstrumentationHelper.java} | 78 +- .../OpenTelemetryInstrumentationState.java | 24 +- .../testing/build.gradle.kts | 0 .../graphql/AbstractGraphqlTest.java | 122 +- .../src/main/resources/schema.graphqls | 0 .../grizzly-2.3/javaagent/build.gradle.kts | 12 +- .../grizzly/GrizzlyHttpAttributesGetter.java | 66 +- .../grizzly/GrizzlyNetAttributesGetter.java | 105 -- .../grizzly/GrizzlySingletons.java | 43 +- .../HttpCodecFilterInstrumentation.java | 6 +- .../GrizzlyFilterchainServerTest.groovy | 5 +- .../src/test/groovy/GrizzlyTest.groovy | 5 +- instrumentation/grpc-1.6/README.md | 8 +- .../grpc-1.6/javaagent/build.gradle.kts | 23 + .../grpc/v1_6/GrpcContextInstrumentation.java | 16 +- .../GrpcServerBuilderInstrumentation.java | 4 +- .../grpc/v1_6/GrpcSingletons.java | 20 +- instrumentation/grpc-1.6/library/README.md | 8 +- .../grpc-1.6/library/build.gradle.kts | 17 + .../grpc/v1_6/GrpcAttributesExtractor.java | 9 +- .../instrumentation/grpc/v1_6/GrpcHelper.java | 16 - ...=> GrpcNetworkServerAttributesGetter.java} | 32 +- .../grpc/v1_6/GrpcRpcAttributesGetter.java | 2 +- .../grpc/v1_6/GrpcSpanStatusExtractor.java | 42 +- .../grpc/v1_6/GrpcTelemetryBuilder.java | 67 +- .../grpc/v1_6/MetadataSetter.java | 4 +- .../grpc/v1_6/TracingClientInterceptor.java | 20 +- .../grpc/v1_6/TracingServerInterceptor.java | 39 +- .../v1_6/internal/ContextStorageBridge.java | 59 +- ...=> GrpcClientNetworkAttributesGetter.java} | 9 +- .../instrumentation/grpc/v1_6/GrpcTest.java | 5 +- .../grpc/v1_6/MetadataSetterTest.java | 67 + .../grpc-1.6/testing/build.gradle.kts | 40 +- .../grpc/v1_6/AbstractGrpcStreamingTest.java | 172 ++- .../grpc/v1_6/AbstractGrpcTest.java | 668 +++++---- instrumentation/guava-10.0/README.md | 4 +- .../guava-10.0/javaagent/build.gradle.kts | 1 + .../GuavaListenableFutureInstrumentation.java | 3 +- .../guava/v10_0/InstrumentationHelper.java | 4 +- .../gwt-2.0/javaagent/build.gradle.kts | 18 +- .../gwt/GwtRpcAttributesGetter.java | 2 +- .../instrumentation/gwt/GwtSingletons.java | 4 +- .../instrumentation/gwt/GwtTest.java | 68 +- .../src/testapp/webapp/greeting.html | 24 +- instrumentation/hibernate/README.md | 4 +- .../v3_3/CriteriaInstrumentation.java | 43 +- .../v3_3/HibernateInstrumentationModule.java | 9 +- .../hibernate/v3_3/QueryInstrumentation.java | 38 +- .../v3_3/SessionInstrumentation.java | 41 +- .../v3_3/TransactionInstrumentation.java | 39 +- .../test/groovy/AbstractHibernateTest.groovy | 40 - .../src/test/groovy/CriteriaTest.groovy | 83 -- .../src/test/groovy/QueryTest.groovy | 206 --- .../src/test/groovy/SessionTest.groovy | 612 -------- .../hibernate/v3_3/AbstractHibernateTest.java | 109 ++ .../hibernate/v3_3/CriteriaTest.java | 70 + .../hibernate/v3_3/QueryTest.java | 154 ++ .../hibernate/v3_3/SessionTest.java | 614 ++++++++ .../hibernate/v3_3}/Value.java | 2 + .../src/test/resources/hibernate.cfg.xml | 2 +- .../hibernate-4.0/javaagent/build.gradle.kts | 7 +- .../v4_0/CriteriaInstrumentation.java | 43 +- .../v4_0/HibernateInstrumentationModule.java | 9 +- .../hibernate/v4_0/QueryInstrumentation.java | 38 +- .../v4_0/SessionInstrumentation.java | 41 +- .../v4_0/TransactionInstrumentation.java | 39 +- .../test/groovy/AbstractHibernateTest.groovy | 39 - .../src/test/groovy/CriteriaTest.groovy | 83 -- .../src/test/groovy/EntityManagerTest.groovy | 234 ---- .../src/test/groovy/QueryTest.groovy | 206 --- .../src/test/groovy/SessionTest.groovy | 605 -------- .../src/test/groovy/SpringJpaTest.groovy | 385 ----- .../hibernate/v4_0/AbstractHibernateTest.java | 50 + .../hibernate/v4_0/CriteriaTest.java | 97 ++ .../hibernate/v4_0/EntityManagerTest.java | 353 +++++ .../hibernate/v4_0/QueryTest.java | 227 +++ .../hibernate/v4_0/SessionTest.java | 852 +++++++++++ .../hibernate/v4_0}/Value.java | 2 + .../src/test/java/spring/jpa/Customer.java | 4 +- .../test/java/spring/jpa/SpringJpaTest.java | 459 ++++++ .../test/resources/META-INF/persistence.xml | 4 +- .../src/test/resources/hibernate.cfg.xml | 2 +- .../v6_0/HibernateInstrumentationModule.java | 11 +- .../hibernate/v6_0/QueryInstrumentation.java | 40 +- .../v6_0/SessionInstrumentation.java | 41 +- .../v6_0/TransactionInstrumentation.java | 39 +- .../hibernate/v6_0/CriteriaTest.java | 77 +- .../hibernate/v6_0/EntityManagerTest.java | 85 +- .../hibernate/v6_0/ProcedureCallTest.java | 144 +- .../hibernate/v6_0/SessionTest.java | 339 +++-- .../spring-testing/build.gradle.kts | 4 + .../src/test/groovy/SpringJpaTest.groovy | 141 +- .../src/test/java/spring/jpa/Customer.java | 4 +- .../javaagent/build.gradle.kts | 4 - .../HibernateInstrumenterFactory.java | 4 +- .../hibernate/HibernateOperationScope.java | 88 ++ .../hibernate/OperationNameUtil.java | 8 +- .../v4_3/HibernateInstrumentationModule.java | 9 +- .../v4_3/ProcedureCallInstrumentation.java | 40 +- .../src/test/groovy/ProcedureCallTest.groovy | 175 --- .../hibernate/v4_3/ProcedureCallTest.java | 190 +++ .../hibernate/v4_3}/Value.java | 2 + .../src/test/resources/hibernate.cfg.xml | 2 +- .../javaagent/build.gradle.kts | 92 ++ .../reactive/v1_0/HibernateReactiveTest.java | 321 +++++ .../hibernate/reactive/v1_0/Value.java | 43 + .../resources/META-INF/persistence.xml | 20 + .../reactive/v2_0/HibernateReactiveTest.java | 313 +++++ .../hibernate/reactive/v2_0/Value.java | 43 + .../resources/META-INF/persistence.xml | 20 + .../reactive/v1_0/mutiny/ContextOperator.java | 72 + ...teReactiveMutinyInstrumentationModule.java | 26 + .../MutinySessionFactoryInstrumentation.java | 42 + .../v1_0/stage/CompletionStageWrapper.java | 40 + .../reactive/v1_0/stage/FunctionWrapper.java | 39 + ...ateReactiveStageInstrumentationModule.java | 26 + .../StageSessionFactoryInstrumentation.java | 65 + .../StageSessionImplInstrumentation.java | 50 + .../v3_0/HikariPoolInstrumentation.java | 2 +- .../OpenTelemetryMetricsTrackerFactory.java | 2 +- .../HttpMethodAttributeExtractor.java | 29 +- .../HttpUrlConnectionSingletons.java | 43 +- .../HttpUrlHttpAttributesGetter.java | 26 +- .../HttpUrlNetAttributesGetter.java | 37 - ...HttpUrlConnectionResponseCodeOnlyTest.java | 1 + .../HttpUrlConnectionTest.java | 171 ++- .../HttpUrlConnectionUseCachesFalseTest.java | 6 +- .../httpurlconnection/UrlConnectionTest.java | 74 - .../hystrix-1.4/javaagent/README.md | 4 +- .../hystrix/HystrixInstrumentationModule.java | 9 +- .../hystrix/HystrixSingletons.java | 4 +- .../groovy/HystrixObservableChainTest.groovy | 108 -- .../test/groovy/HystrixObservableTest.groovy | 315 ----- .../src/test/groovy/HystrixTest.groovy | 148 -- .../hystrix/HystrixObservableChainTest.java | 123 ++ .../hystrix/HystrixObservableTest.java | 466 ++++++ .../instrumentation/hystrix/HystrixTest.java | 180 +++ .../influxdb-2.4/javaagent/build.gradle.kts | 54 + .../v2_4/InfluxDbAttributesGetter.java | 51 + .../v2_4/InfluxDbImplInstrumentation.java | 173 +++ .../v2_4/InfluxDbInstrumentationModule.java | 26 + .../v2_4/InfluxDbNetworkAttributesGetter.java | 21 + .../influxdb/v2_4/InfluxDbObjetWrapper.java | 44 + .../influxdb/v2_4/InfluxDbRequest.java | 35 + .../influxdb/v2_4/InfluxDbScope.java | 39 + .../influxdb/v2_4/InfluxDbSingletons.java | 38 + .../influxdb/v2_4/InfluxDbClientTest.java | 331 +++++ .../influxdb/v2_4/InfluxDbClient24Test.java | 159 +++ ...plicationLoggingInstrumentationModule.java | 3 +- .../TestInstrumentationModule.java | 5 + .../test/groovy/ResourceInjectionTest.groovy | 3 +- .../ClassLoaderInstrumentationModule.java | 5 + .../src/test/groovy/ClassLoadingTest.groovy | 44 - .../test/groovy/JBossClassloadingTest.groovy | 39 - .../test/groovy/OSGIClassloadingTest.groovy | 73 - .../test/groovy/TomcatClassloadingTest.groovy | 71 - .../classloader/ClassLoadingTest.java | 57 + .../classloader/JbossClassloadingTest.java | 45 + .../classloader/OsgiClassloadingTest.java | 80 ++ .../classloader/TomcatClassloadingTest.java | 99 ++ .../osgi/EclipseOsgiInstrumentation.java | 9 +- ...ClassLambdaMetafactoryInstrumentation.java | 3 +- .../lambda/LambdaInstrumentationModule.java | 5 + .../groovy/LambdaInstrumentationTest.groovy | 21 - .../lambda/LambdaInstrumentationTest.java | 24 + .../internal/lambda}/TestLambda.java | 2 + .../src/test/groovy/ReflectionTest.groovy | 58 - .../src/test/java/ReflectionTest.java | 48 + .../reflection/ClassInstrumentation.java | 3 +- .../reflection/ReflectionInstrumentation.java | 22 +- .../ReflectionInstrumentationModule.java | 17 +- .../src/test/groovy/AddUrlTest.groovy | 51 - .../src/test/java/AddUrlTest.java | 56 + .../httpclient/JavaHttpClientSingletons.java | 18 +- .../httpclient/JavaHttpClientTest.java | 37 +- .../JavaHttpClientTelemetryBuilder.java | 80 +- .../JavaHttpClientAttributesGetter.java | 45 +- ...aHttpClientInstrumenterBuilderFactory.java | 27 + .../JavaHttpClientInstrumenterFactory.java | 53 - .../JavaHttpClientNetAttributesGetter.java | 59 - .../httpclient/JavaHttpClientTest.java | 37 +- .../AbstractJavaHttpClientTest.java | 17 +- .../java-util-logging/javaagent/README.md | 6 +- .../jul/JavaUtilLoggingHelper.java | 17 +- .../jul/JavaUtilLoggingTest.java | 17 +- .../javalin-5.0/javaagent/build.gradle.kts | 24 + .../javalin/v5_0/JavalinInstrumentation.java | 48 + .../v5_0/JavalinInstrumentationModule.java | 34 + .../javalin/v5_0/JavalinTest.java | 170 +++ .../v5_0/TestJavalinJavaApplication.java | 30 + .../jaxrs-client-1.1-testing/build.gradle.kts | 9 + .../src/test/groovy/JaxRsClientV1Test.groovy | 47 +- .../javaagent/build.gradle.kts | 18 - .../v1_1/ClientHandlerInstrumentation.java | 76 - .../v1_1/ClientRequestHeaderSetter.java | 18 - .../v1_1/JaxRsClientHttpAttributesGetter.java | 55 - .../v1_1/JaxRsClientNetAttributesGetter.java | 26 - .../v1_1/JaxRsClientSingletons.java | 51 - .../src/test/groovy/JaxRsClientTest.groovy | 39 +- .../groovy/ResteasyProxyClientTest.groovy | 5 + instrumentation/jaxrs/README.md | 4 +- .../jaxrs-1.0/javaagent/build.gradle.kts | 4 + .../v1_0/JaxrsAnnotationsInstrumentation.java | 8 +- .../jaxrs/v1_0/JaxrsCodeAttributesGetter.java | 2 +- .../v1_0/JaxrsInstrumentationModule.java | 10 + .../jaxrs/v1_0/JaxrsServerSpanNaming.java | 4 +- .../jaxrs/v1_0/JaxrsSingletons.java | 4 +- ...axRsAnnotations1InstrumentationTest.groovy | 16 +- .../src/test/groovy/JerseyTest.groovy | 22 +- .../javaagent/build.gradle.kts | 6 + .../DefaultRequestContextInstrumentation.java | 8 +- .../v2_0/JaxrsAnnotationsInstrumentation.java | 8 +- ...JaxrsAnnotationsInstrumentationModule.java | 10 + ...JaxrsAnnotationsInstrumentationTest.groovy | 16 +- .../main/groovy/JaxRsHttpServerTest.groovy | 5 + .../groovy/JaxRsJettyHttpServerTest.groovy | 10 + .../main/groovy/test/JaxRsTestResource.groovy | 4 +- .../javaagent/build.gradle.kts | 3 + .../jaxrs/v2_0/CxfSpanName.java | 10 +- .../src/test/groovy/CxfHttpServerTest.groovy | 5 + .../test/groovy/CxfJettyHttpServerTest.groovy | 5 + .../javaagent/build.gradle.kts | 1 + .../v2_0/JerseyInstrumentationModule.java | 8 + .../jaxrs/v2_0/JerseySpanName.java | 10 +- .../test/groovy/JerseyHttpServerTest.groovy | 15 + .../groovy/JerseyJettyHttpServerTest.groovy | 5 + .../jaxrs-2.0-payara-testing/build.gradle.kts | 6 + .../javaagent/build.gradle.kts | 1 + .../test/groovy/ResteasyHttpServerTest.groovy | 15 + .../groovy/ResteasyJettyHttpServerTest.groovy | 5 + .../javaagent/build.gradle.kts | 1 + .../test/groovy/ResteasyHttpServerTest.groovy | 15 + .../groovy/ResteasyJettyHttpServerTest.groovy | 4 + .../jaxrs/v2_0/ResteasySpanName.java | 11 +- .../jaxrs-2.0-tomee-testing/build.gradle.kts | 1 + .../build.gradle.kts | 9 +- .../javaagent/build.gradle.kts | 6 + .../DefaultRequestContextInstrumentation.java | 8 +- .../v3_0/JaxrsAnnotationsInstrumentation.java | 8 +- ...JaxrsAnnotationsInstrumentationModule.java | 10 + ...JaxrsAnnotationsInstrumentationTest.groovy | 16 +- .../main/groovy/JaxRsHttpServerTest.groovy | 5 + .../groovy/JaxRsJettyHttpServerTest.groovy | 10 + .../main/groovy/test/JaxRsTestResource.groovy | 4 +- .../javaagent/build.gradle.kts | 1 + .../jaxrs/v3_0/JerseySpanName.java | 10 +- .../test/groovy/JerseyHttpServerTest.groovy | 15 + .../groovy/JerseyJettyHttpServerTest.groovy | 5 + .../javaagent/build.gradle.kts | 1 + .../jaxrs/v3_0/ResteasySpanName.java | 11 +- .../test/groovy/ResteasyHttpServerTest.groovy | 15 + .../groovy/ResteasyJettyHttpServerTest.groovy | 5 + .../jaxrs/JaxrsCodeAttributesGetter.java | 2 +- .../instrumentation/jaxrs/JaxrsConfig.java | 4 +- .../jaxrs/JaxrsInstrumenterFactory.java | 4 +- .../jaxrs/JaxrsServerSpanNaming.java | 4 +- .../jaxrs/RequestContextHelper.java | 8 +- .../groovy/AbstractJaxRsFilterTest.groovy | 21 +- .../groovy/AbstractJaxRsHttpServerTest.groovy | 53 +- .../java/AbstractArquillianJaxWsTest.java | 6 +- .../javaagent/build.gradle.kts | 1 + .../test/java/test/CustomJaxWsDeployer.java | 1 + .../jaxws-2.0-common-testing/build.gradle.kts | 4 - .../src/main/groovy/AbstractJaxWsTest.groovy | 6 +- .../cxf/CxfServerSpanNaming.java | 39 - .../build.gradle.kts | 25 +- .../src/test/groovy/MetroJaxWsTest.groovy | 0 .../resources/test-app/WEB-INF/sun-jaxws.xml | 0 .../test/resources/test-app/WEB-INF/web.xml | 0 .../metro/MetroServerSpanNaming.java | 50 - .../jaxws-2.0-tomee-testing/build.gradle.kts | 3 +- .../build.gradle.kts | 11 +- .../jaxws-2.0/javaagent/build.gradle.kts | 4 + .../jaxws/v2_0/JaxWsAnnotationsTest.groovy | 6 +- .../jaxws-3.0-common-testing/build.gradle.kts | 24 + .../src/main/groovy/AbstractJaxWsTest.groovy | 187 +++ .../main/groovy/hello/BaseHelloService.groovy | 16 + .../src/main/groovy/hello/HelloService.groovy | 24 + .../main/groovy/hello/HelloServiceImpl.groovy | 19 + .../src/main/schema/hello.xsd | 35 + .../build.gradle.kts | 27 + .../src/test/groovy/CxfJaxWsTest.groovy | 0 .../src/test/groovy/TestWsServlet.groovy | 37 + .../test/resources/test-app/WEB-INF/web.xml | 0 .../build.gradle.kts | 28 + .../src/test/groovy/MetroJaxWsTest.groovy} | 2 +- .../resources/test-app/WEB-INF/sun-jaxws.xml | 6 + .../test/resources/test-app/WEB-INF/web.xml | 21 + .../common/JaxWsCodeAttributesGetter.java | 2 +- .../common/JaxWsInstrumenterFactory.java | 4 +- .../javaagent-unit-tests/build.gradle.kts | 2 +- .../cxf/TracingStartInInterceptorTest.java | 0 .../javaagent/build.gradle.kts | 6 +- .../instrumentation/cxf/CxfHelper.java | 0 .../cxf/CxfInstrumentationModule.java | 0 .../instrumentation/cxf/CxfRequest.java | 0 .../cxf/CxfServerSpanNaming.java | 66 + .../instrumentation/cxf/CxfSingletons.java | 2 +- ...JaxWsServerFactoryBeanInstrumentation.java | 0 .../cxf/TracingEndInInterceptor.java | 0 .../cxf/TracingOutFaultInterceptor.java | 0 .../cxf/TracingStartInInterceptor.java | 0 .../src/test/groovy/CxfJaxWsTest.groovy} | 2 +- .../src/test/groovy/TestWsServlet.groovy | 0 .../test/resources/test-app/WEB-INF/web.xml | 17 + .../javaagent/build.gradle.kts | 4 + .../jws/v1_1/JwsInstrumentationModule.java | 8 + .../jaxws/jws/v1_1/JwsAnnotationsTest.groovy | 14 +- .../javaagent/build.gradle.kts | 26 + .../instrumentation/metro/MetroHelper.java | 5 +- .../metro/MetroInstrumentationModule.java | 2 +- .../instrumentation/metro/MetroRequest.java | 0 .../metro/MetroServerSpanNameUpdater.java | 166 +++ .../metro/MetroSingletons.java | 2 +- ...erTubeAssemblerContextInstrumentation.java | 0 .../SoapFaultBuilderInstrumentation.java | 0 .../instrumentation/metro/TracingTube.java | 0 instrumentation/jboss-logmanager/README.md | 6 + .../javaagent/build.gradle.kts | 8 + .../appender/v1_1/LoggingEventMapper.java | 24 +- .../appender/v1_1/JbossLogmanagerTest.java | 106 +- .../javaagent/build.gradle.kts | 8 + .../JbossExtLogRecordInstrumentation.java | 46 +- .../mdc/v1_1/JbossLogmanagerMdcTest.java | 2 +- instrumentation/jdbc/README.md | 5 + .../jdbc/bootstrap/build.gradle.kts | 2 +- .../jdbc/JdbcIgnoredTypesConfigurer.java | 2 + .../instrumentation/jdbc/JdbcSingletons.java | 33 +- .../PreparedStatementInstrumentation.java | 9 +- .../datasource/DataSourceInstrumentation.java | 25 +- .../groovy/JdbcInstrumentationTest.groovy | 269 ++-- .../jdbc/test/ProxyStatementFactory.java | 24 + .../scalaexecutors/SlickTest.scala | 22 +- instrumentation/jdbc/library/README.md | 6 +- instrumentation/jdbc/library/build.gradle.kts | 2 +- .../jdbc/OpenTelemetryDriver.java | 36 +- .../jdbc/datasource/JdbcTelemetry.java | 41 + .../jdbc/datasource/JdbcTelemetryBuilder.java | 53 + .../datasource/OpenTelemetryDataSource.java | 45 +- .../DataSourceCodeAttributesGetter.java | 5 +- .../DataSourceDbAttributesExtractor.java | 52 + .../DataSourceInstrumenterFactory.java | 32 - .../jdbc/internal/JdbcAttributesGetter.java | 2 +- .../internal/JdbcConnectionUrlParser.java | 93 +- .../internal/JdbcInstrumenterFactory.java | 40 +- ....java => JdbcNetworkAttributesGetter.java} | 4 +- .../jdbc/internal/JdbcUtils.java | 18 +- .../OpenTelemetryCallableStatement.java | 8 +- .../internal/OpenTelemetryConnection.java | 33 +- .../OpenTelemetryPreparedStatement.java | 8 +- .../jdbc/internal/OpenTelemetryStatement.java | 20 +- .../jdbc/OpenTelemetryConnectionTest.groovy | 212 --- .../JdbcConnectionUrlParserTest.groovy | 224 --- .../jdbc/datasource/JdbcTelemetryTest.java | 135 ++ .../OpenTelemetryDataSourceTest.java | 102 +- .../internal/JdbcConnectionUrlParserTest.java | 1221 ++++++++++++++++ .../internal/OpenTelemetryConnectionTest.java | 201 +++ .../jedis-1.4/javaagent/build.gradle.kts | 19 +- .../v1_4/JedisConnectionInstrumentation.java | 20 +- .../jedis/v1_4/JedisDbAttributesGetter.java | 12 +- ...java => JedisNetworkAttributesGetter.java} | 4 +- .../jedis/v1_4/JedisSingletons.java | 16 +- .../jedis/v1_4/JedisClientTest.java | 127 +- .../jedis/v2_7_2/JedisClientTest.java | 10 + .../jedis/jedis-1.4/testing/build.gradle.kts | 9 + .../jedis/AbstractJedisTest.java | 137 ++ .../jedis/v3_0/JedisDbAttributesGetter.java | 6 +- ...java => JedisNetworkAttributesGetter.java} | 8 +- .../jedis/v3_0/JedisRequest.java | 6 +- .../jedis/v3_0/JedisSingletons.java | 18 +- .../jedis/v3_0/Jedis30ClientTest.java | 102 +- .../jedis/v4_0/JedisDbAttributesGetter.java | 6 +- ...java => JedisNetworkAttributesGetter.java} | 18 +- .../jedis/v4_0/JedisRequest.java | 6 +- .../jedis/v4_0/JedisSingletons.java | 15 +- .../jedis/v4_0/Jedis40ClientTest.java | 87 +- .../javaagent/build.gradle.kts | 25 + ...ent12ResponseListenersInstrumentation.java | 108 ++ .../JettyHttpClient12Instrumentation.java | 107 ++ ...ettyHttpClient12InstrumentationModule.java | 27 + .../v12_0/JettyHttpClientSingletons.java | 28 + .../v12_0/JettyHttpClient12AgentTest.java | 31 + .../library/build.gradle.kts | 13 + .../v12_0/JettyClientTelemetry.java | 37 + .../v12_0/JettyClientTelemetryBuilder.java | 131 ++ .../httpclient/v12_0/TracingHttpClient.java | 60 + .../httpclient/v12_0/TracingHttpRequest.java | 99 ++ .../v12_0/internal/HttpHeaderSetter.java | 24 + .../JettyClientHttpAttributesGetter.java | 84 ++ .../internal/JettyClientTracingListener.java | 63 + ...yHttpClientInstrumenterBuilderFactory.java | 28 + .../v12_0/JettyHttpClient12LibraryTest.java | 39 + .../testing/build.gradle.kts | 11 + .../v12_0/AbstractJettyClient12Test.java | 125 ++ .../javaagent/build.gradle.kts | 2 + .../v9_2/JettyHttpClient9Instrumentation.java | 14 +- .../v9_2/JettyHttpClientSingletons.java | 16 +- .../v9_2/JettyHttpClient9AgentTest.groovy | 24 - .../v9_2/JettyHttpClient9AgentTest.java | 29 + .../v9_2/JettyClientTelemetryBuilder.java | 61 +- .../httpclient/v9_2/TracingHttpClient.java | 15 +- .../JettyClientHttpAttributesGetter.java | 44 +- .../JettyClientInstrumenterBuilder.java | 75 - .../internal/JettyClientTracingListener.java | 133 ++ .../v9_2/internal/JettyClientWrapUtil.java | 2 +- .../JettyHttpClient9TracingInterceptor.java | 182 --- ...yHttpClientInstrumenterBuilderFactory.java | 28 + .../JettyHttpClientNetAttributesGetter.java | 58 - .../v9_2/JettyHttpClient9LibraryTest.groovy | 31 - .../v9_2/JettyHttpClient9LibraryTest.java | 39 + .../testing/build.gradle.kts | 4 - .../v9_2/AbstractJettyClient9Test.groovy | 145 -- .../v9_2/AbstractJettyClient9Test.java | 115 ++ instrumentation/jetty/README.md | 7 +- .../jetty-11.0/javaagent/build.gradle.kts | 5 +- .../jetty/v11_0/Jetty11Singletons.java | 1 + .../jetty/v11_0/JettyHandlerTest.java | 22 +- .../jetty-12.0/javaagent/build.gradle.kts | 27 + .../jetty/v12_0/Jetty12Helper.java | 62 + .../v12_0/Jetty12HttpAttributesGetter.java | 99 ++ .../v12_0/Jetty12IgnoredTypesConfigurer.java | 22 + .../v12_0/Jetty12InstrumentationModule.java | 33 + .../jetty/v12_0/Jetty12ResponseMutator.java | 18 + .../v12_0/Jetty12ServerInstrumentation.java | 82 ++ .../jetty/v12_0/Jetty12Singletons.java | 71 + .../jetty/v12_0/Jetty12TextMapGetter.java | 23 + .../jetty/v12_0/Jetty12HandlerTest.java | 137 ++ .../jetty-8.0/javaagent/build.gradle.kts | 1 + .../jetty/v8_0/Jetty8Singletons.java | 1 + .../JettyQueuedThreadPoolInstrumentation.java | 3 +- .../jetty/v8_0/JavaLambdaMaker.java | 2 +- .../jetty/v8_0/JettyHandlerTest.java | 22 +- .../jms/jms-1.1/javaagent/build.gradle.kts | 32 +- .../src/jms2Test/groovy/Jms2Test.groovy | 317 ----- .../jms/v1_1/Jms2InstrumentationTest.java | 331 +++++ .../JmsMessageConsumerInstrumentation.java | 13 +- .../jms/v1_1/JmsSingletons.java | 2 +- .../javaagent/src/test/groovy/Jms1Test.groovy | 382 ----- .../jms/v1_1/AbstractJms1Test.java | 363 +++++ .../jms/v1_1/Jms1InstrumentationTest.java | 90 ++ .../v1_1/Jms1SuppressReceiveSpansTest.java | 82 ++ .../jms/jms-3.0/javaagent/build.gradle.kts | 16 + .../JmsMessageConsumerInstrumentation.java | 13 +- .../jms/v3_0/JmsSingletons.java | 2 +- .../jms/v3_0/AbstractJms3Test.java | 315 +++++ .../jms/v3_0/Jms3InstrumentationTest.java | 308 +--- .../v3_0/Jms3SuppressReceiveSpansTest.java | 85 ++ .../jms/jms-common/bootstrap/build.gradle.kts | 3 + .../jms/JmsReceiveContextHolder.java | 47 + .../javaagent-unit-tests/build.gradle.kts | 2 +- .../jms/jms-common/javaagent/build.gradle.kts | 2 + .../jms/JmsInstrumenterFactory.java | 58 +- .../jms/JmsMessageAttributesGetter.java | 30 +- .../jms/JmsReceiveSpanUtil.java | 51 + .../jms/MessageWithDestination.java | 12 +- .../jmx-metrics/javaagent/README.md | 1 + .../jmx-metrics/javaagent/activemq.md | 26 +- .../jmx-metrics/javaagent/camel.md | 51 + .../jmx-metrics/javaagent/hadoop.md | 2 +- .../jmx-metrics/javaagent/jetty.md | 2 +- .../jmx-metrics/javaagent/kafka-broker.md | 5 +- .../jmx/JmxMetricInsightInstaller.java | 14 +- .../main/resources/jmx/rules/activemq.yaml | 19 +- .../src/main/resources/jmx/rules/camel.yaml | 263 ++++ .../src/main/resources/jmx/rules/hadoop.yaml | 122 +- .../src/main/resources/jmx/rules/jetty.yaml | 5 +- .../resources/jmx/rules/kafka-broker.yaml | 400 +++--- .../src/main/resources/jmx/rules/tomcat.yaml | 201 ++- .../src/main/resources/jmx/rules/wildfly.yaml | 162 +-- .../jmx/JmxMetricInsightInstallerTest.java | 1 + .../jmx-metrics/javaagent/tomcat.md | 2 +- .../jmx-metrics/javaagent/wildfly.md | 2 +- .../jmx/engine/BeanFinder.java | 22 +- .../javaagent-unit-tests/build.gradle.kts | 10 + .../JoddHttpHttpAttributesGetterTest.java | 0 .../jodd-http-4.2/javaagent/build.gradle.kts | 2 +- .../v4_2/JoddHttpHttpAttributesGetter.java | 33 +- .../v4_2/JoddHttpNetAttributesGetter.java | 46 - .../joddhttp/v4_2/JoddHttpSingletons.java | 28 +- .../joddhttp/v4_2/JoddHttpTest.java | 6 +- .../src/main/groovy/BaseJsfTest.groovy | 263 ---- .../src/main/groovy/ExceptionFilter.groovy | 38 - .../src/main/groovy/GreetingForm.groovy | 29 - .../jsf/jakarta/BaseJsfTest.java | 287 ++++ .../jsf/jakarta/ExceptionFilter.java | 40 + .../jsf/jakarta/GreetingForm.java | 31 + .../test-app/WEB-INF/faces-config.xml | 2 +- .../main/resources/test-app/WEB-INF/web.xml | 2 +- .../main/resources/test-app/greeting.xhtml | 16 +- .../src/main/resources/test-app/hello.xhtml | 16 +- .../src/main/groovy/BaseJsfTest.groovy | 274 ---- .../src/main/groovy/ExceptionFilter.groovy | 38 - .../src/main/groovy/GreetingForm.groovy | 29 - .../jsf/javax/BaseJsfTest.java | 296 ++++ .../jsf/javax/ExceptionFilter.java | 40 + .../jsf/javax/GreetingForm.java | 31 + .../test-app-1.2/WEB-INF/faces-config.xml | 4 +- .../resources/test-app-1.2/WEB-INF/web.xml | 4 +- .../resources/test-app-1.2/greeting.xhtml | 14 +- .../main/resources/test-app-1.2/hello.xhtml | 7 +- .../test-app-2/WEB-INF/faces-config.xml | 4 +- .../main/resources/test-app-2/WEB-INF/web.xml | 4 +- .../main/resources/test-app-2/greeting.xhtml | 16 +- .../src/main/resources/test-app-2/hello.xhtml | 16 +- .../javaagent/build.gradle.kts | 3 + .../mojarra12Test/groovy/Mojarra12Test.groovy | 11 - .../mojarra/Mojarra12Test.java | 15 + .../mojarra2Test/groovy/Mojarra2Test.groovy | 11 - .../instrumentation/mojarra/Mojarra2Test.java | 15 + .../javaagent/build.gradle.kts | 6 + .../mojarra/v3_0/Mojarra3Test.java | 10 + .../javaagent/build.gradle.kts | 5 + .../myfaces12Test/groovy/Myfaces12Test.groovy | 11 - .../myfaces/Myfaces12Test.java | 15 + .../myfaces2Test/groovy/Myfaces2Test.groovy | 11 - .../instrumentation/myfaces/Myfaces2Test.java | 15 + .../javaagent/build.gradle.kts | 6 + .../myfaces/v3_0/Myfaces3Test.java | 10 + instrumentation/jsp-2.3/README.md | 4 +- .../jsp-2.3/javaagent/build.gradle.kts | 2 + .../HttpJspPageInstrumentationSingletons.java | 6 +- ...ationContextInstrumentationSingletons.java | 4 +- .../JspInstrumentationBasicTests.groovy | 538 ------- .../JspInstrumentationForwardTests.groovy | 489 ------- .../jsp/JspInstrumentationBasicTests.java | 488 +++++++ .../jsp/JspInstrumentationForwardTests.java | 454 ++++++ .../instrumentation/jsp/JspSpan.java | 92 ++ .../jsp/JspSpanAssertionBuilder.java | 79 ++ .../jsp/JspSpanAssertions.java | 155 ++ .../webapps/jsptest/common/hello.html | 12 +- instrumentation/kafka/README.md | 7 + .../KafkaClientsConsumerProcessTracing.java | 6 + .../javaagent/build.gradle.kts | 1 + .../v0_11/ConsumerRecordsInstrumentation.java | 17 +- .../v0_11/KafkaConsumerInstrumentation.java | 30 +- .../v0_11/KafkaProducerInstrumentation.java | 28 - .../kafkaclients/v0_11/KafkaSingletons.java | 51 +- .../kafkaclients/v0_11/TracingIterable.java | 47 - .../KafkaMetricsConsumerInstrumentation.java | 61 + .../KafkaMetricsInstrumentationModule.java | 43 + .../KafkaMetricsProducerInstrumentation.java | 61 + .../v0_11/metrics/KafkaMetricsUtil.java | 65 + .../v0_11/KafkaClientDefaultTest.java | 6 +- .../KafkaClientPropagationDisabledTest.java | 4 +- .../KafkaClientSuppressReceiveSpansTest.java | 6 +- .../OpenTelemetryMetricsReporterTest.java | 31 + ...tractOpenTelemetryMetricsReporterTest.java | 112 +- .../kafka/internal/KafkaClientBaseTest.java | 82 +- .../clients/consumer/KafkaConsumerAccess.java | 18 - .../clients/producer/KafkaProducerAccess.java | 18 - .../kafka-clients-2.6/library/README.md | 380 +++-- .../library/build.gradle.kts | 21 + .../kafkaclients/v2_6/KafkaTelemetry.java | 82 +- .../v2_6/KafkaTelemetryBuilder.java | 46 +- .../v2_6/TracingConsumerInterceptor.java | 23 +- .../v2_6/AbstractInterceptorsTest.java | 76 + .../v2_6/AbstractWrapperTest.java | 82 ++ .../InterceptorsSuppressReceiveSpansTest.java | 74 + .../kafkaclients/v2_6/InterceptorsTest.java | 192 ++- .../v2_6/WrapperSuppressReceiveSpansTest.java | 112 ++ .../kafkaclients/v2_6/WrapperTest.java | 187 ++- .../OpenTelemetryMetricsReporterTest.java | 3 + .../KafkaConsumerAttributesExtractor.java | 34 +- .../KafkaConsumerAttributesGetter.java | 35 +- .../kafka/internal/KafkaConsumerContext.java | 11 +- .../internal/KafkaConsumerContextUtil.java | 66 +- .../internal/KafkaInstrumenterFactory.java | 23 +- .../kafka/internal/KafkaProcessRequest.java | 4 +- .../KafkaProducerAttributesExtractor.java | 22 +- .../KafkaProducerAttributesGetter.java | 32 +- .../KafkaReceiveAttributesExtractor.java | 18 +- .../KafkaReceiveAttributesGetter.java | 32 +- .../kafka/internal/KafkaReceiveRequest.java | 4 +- .../kafka/internal/MetricsReporterList.java | 34 + .../OpenTelemetryMetricsReporter.java | 29 +- .../kafka/internal/TracingIterable.java | 62 + .../kafka/internal}/TracingIterator.java | 38 +- .../kafka/internal}/TracingList.java | 27 +- .../kafkastreams/KafkaStreamsSingletons.java | 4 +- .../test/groovy/KafkaStreamsBaseTest.groovy | 2 +- .../groovy/KafkaStreamsDefaultTest.groovy | 94 +- ...afkaStreamsSuppressReceiveSpansTest.groovy | 68 +- .../javaagent/gradle.properties | 1 - .../javaagent/build.gradle.kts | 54 + ...linCoroutineDispatcherInstrumentation.java | 48 + .../KotlinCoroutinesInstrumentation.java | 0 ...KotlinCoroutinesInstrumentationHelper.java | 0 ...KotlinCoroutinesInstrumentationModule.java | 40 + .../kotlinxcoroutines/RunnableWrapper.java | 22 + .../AnnotationInstrumentationHelper.java | 169 +++ .../AnnotationInstrumentationModule.java | 52 + .../AnnotationSingletons.java | 42 + .../ExpandFramesClassVisitor.java | 148 ++ ...otlinCoroutinesIgnoredTypesConfigurer.java | 20 + .../MethodRequest.java | 44 + .../MethodRequestCodeAttributesGetter.java | 22 + .../SpanAttributeUtil.java | 99 ++ .../WithSpanInstrumentation.java | 523 +++++++ .../ClazzWithDefaultConstructorArguments.kt | 18 + .../KotlinCoroutinesInstrumentationTest.kt | 267 ++-- .../javaagent-kotlin/build.gradle.kts | 21 + .../kotlinxcoroutines/flow/FlowUtil.kt | 17 + .../javaagent/build.gradle.kts | 25 +- .../flow/AbstractFlowInstrumentation.java | 45 + .../flow/FlowInstrumentationHelper.java | 44 + ...inCoroutinesFlowInstrumentationModule.java | 26 + .../KotlinCoroutines13InstrumentationTest.kt | 218 +++ .../flow/FlowWithSpanTest.kt | 66 + .../ktor/ktor-1.0/library/README.md | 2 +- .../ktor/ktor-1.0/library/build.gradle.kts | 17 +- .../v1_0/KtorHttpServerAttributesGetter.kt | 17 +- .../v1_0/KtorNetServerAttributesGetter.kt | 36 - .../ktor/v1_0/KtorServerTracing.kt | 37 +- .../ktor/v1_0/KtorHttpServerTest.kt | 12 +- .../ktor/ktor-2.0/javaagent/build.gradle.kts | 43 + .../ktor/v2_0/HttpClientInstrumentation.java | 66 + .../v2_0/KtorClientInstrumentationModule.java | 31 + .../KtorServerInstrumentationModule.java} | 10 +- .../ktor/v2_0/ServerInstrumentation.java | 60 + .../ktor/v2_0/client/KtorHttpClientTest.kt | 22 + .../ktor/v2_0/server/KtorHttpServerTest.kt | 33 + .../ktor/ktor-2.0/library/README.md | 4 +- .../ktor/ktor-2.0/library/build.gradle.kts | 14 +- .../ktor/v2_0/client/KtorClientTracing.kt | 25 +- .../v2_0/client/KtorClientTracingBuilder.kt | 168 ++- .../client/KtorHttpClientAttributesGetter.kt | 33 +- .../client/KtorNetClientAttributesGetter.kt | 25 - .../server/KtorHttpServerAttributesGetter.kt | 17 +- .../server/KtorNetServerAttributesGetter.kt | 36 - .../ktor/v2_0/server/KtorServerTracing.kt | 181 ++- .../ktor/v2_0/client/KtorHttpClientTest.kt | 76 +- .../ktor/v2_0/server/KtorHttpServerTest.kt | 128 +- .../server/KtorServerSpanKindExtractorTest.kt | 13 +- .../ktor/v2_0/server/KtorTestUtil.kt | 4 +- .../ktor/ktor-2.0/testing/build.gradle.kts | 28 + .../v2_0/client/AbstractKtorHttpClientTest.kt | 75 + .../client/KtorHttpClientSingleConnection.kt | 0 .../v2_0/server/AbstractKtorHttpServerTest.kt | 141 ++ .../ktor/ktor-common/library/build.gradle.kts | 13 +- .../instrumentation/ktor/IsIpAddress.kt | 33 +- .../kubernetes-client-7.0/README.md | 4 +- .../groovy/KubernetesRequestUtilsTest.groovy | 87 -- .../KubernetesRequestUtilsTest.java | 192 +++ .../javaagent/build.gradle.kts | 24 +- .../ApiClientInstrumentation.java | 2 +- .../KubernetesClientSingletons.java | 18 +- .../KubernetesHttpAttributesGetter.java | 12 +- .../KubernetesNetAttributesGetter.java | 23 - .../KubernetesRequestDigest.java | 2 +- .../kubernetesclient/KubernetesResource.java | 2 +- .../test/groovy/KubernetesClientTest.groovy | 222 --- .../KubernetesClientTest.java | 277 ++++ .../KubernetesClientVer20Test.java | 279 ++++ instrumentation/lettuce/README.md | 5 +- .../lettuce-4.0/javaagent/build.gradle.kts | 1 + .../lettuce/v4_0/InstrumentationPoints.java | 4 +- .../LettuceAsyncCommandInstrumentation.java | 3 +- .../LettuceConnectAttributesExtractor.java | 6 +- ...ettuceConnectNetworkAttributesGetter.java} | 4 +- .../v4_0/LettuceDbAttributesGetter.java | 6 +- .../lettuce/v4_0/LettuceSingletons.java | 21 +- .../test/groovy/LettuceAsyncClientTest.groovy | 529 ------- .../test/groovy/LettuceSyncClientTest.groovy | 314 ----- .../lettuce/v4_0/LettuceAsyncClientTest.java | 531 +++++++ .../lettuce/v4_0/LettuceSyncClientTest.java | 328 +++++ .../lettuce-5.0/javaagent/build.gradle.kts | 1 + .../v5_0/EndCommandAsyncBiFunction.java | 4 +- .../v5_0/EndConnectAsyncBiFunction.java | 4 +- .../LettuceAsyncCommandInstrumentation.java | 3 +- .../LettuceConnectAttributesExtractor.java | 6 +- ...ettuceConnectNetworkAttributesGetter.java} | 4 +- .../v5_0/LettuceDbAttributesGetter.java | 12 +- .../lettuce/v5_0/LettuceSingletons.java | 24 +- .../rx/LettuceFluxTerminationRunnable.java | 4 +- .../test/groovy/LettuceAsyncClientTest.groovy | 544 ------- .../groovy/LettuceReactiveClientTest.groovy | 430 ------ .../test/groovy/LettuceSyncClientTest.groovy | 323 ----- .../v5_0/AbstractLettuceClientTest.java | 81 ++ .../lettuce/v5_0/LettuceAsyncClientTest.java | 506 +++++++ .../v5_0/LettuceReactiveClientTest.java | 376 +++++ .../lettuce/v5_0/LettuceSyncClientTest.java | 298 ++++ .../LettuceAsyncCommandInstrumentation.java | 3 +- .../v5_1/LettuceInstrumentationModule.java | 5 + .../lettuce/v5_1/TracingHolder.java | 6 +- .../v5_1/LettuceAsyncClientTest.groovy | 17 - .../v5_1/LettuceReactiveClientTest.groovy | 79 -- .../v5_1/LettuceSyncClientAuthTest.groovy | 17 - .../lettuce/v5_1/LettuceSyncClientTest.groovy | 17 - .../lettuce/v5_1/LettuceAsyncClientTest.java | 32 + .../v5_1/LettuceReactiveClientTest.java | 81 ++ .../v5_1/LettuceSyncClientAuthTest.java | 27 + .../lettuce/v5_1/LettuceSyncClientTest.java | 27 + .../core/protocol/OtelCommandArgsUtil.java | 49 + .../v5_1/LettuceNetAttributesGetter.java | 34 - .../v5_1/LettuceServerAttributesGetter.java | 42 + .../lettuce/v5_1/LettuceTelemetry.java | 2 +- .../lettuce/v5_1/OpenTelemetryTracing.java | 38 +- .../v5_1/LettuceAsyncClientTest.groovy | 27 - .../v5_1/LettuceReactiveClientTest.groovy | 34 - .../v5_1/LettuceSyncClientAuthTest.groovy | 21 - .../lettuce/v5_1/LettuceSyncClientTest.groovy | 21 - .../lettuce/v5_1/LettuceAsyncClientTest.java | 39 + .../v5_1/LettuceReactiveClientTest.java | 48 + .../v5_1/LettuceSyncClientAuthTest.java | 33 + .../lettuce/v5_1/LettuceSyncClientTest.java | 33 + .../lettuce-5.1/testing/build.gradle.kts | 5 +- .../AbstractLettuceAsyncClientTest.groovy | 449 ------ .../AbstractLettuceReactiveClientTest.groovy | 398 ------ .../AbstractLettuceSyncClientAuthTest.groovy | 85 -- .../v5_1/AbstractLettuceSyncClientTest.groovy | 490 ------- .../lettuce/v5_1/LettuceTestUtil.groovy | 30 - .../v5_1/AbstractLettuceAsyncClientTest.java | 437 ++++++ .../v5_1/AbstractLettuceClientTest.java | 75 + .../AbstractLettuceReactiveClientTest.java | 360 +++++ .../AbstractLettuceSyncClientAuthTest.java | 135 ++ .../v5_1/AbstractLettuceSyncClientTest.java | 497 +++++++ .../lettuce/v5_1/LettuceTestUtil.java | 27 + .../common/LettuceArgSplitterTest.groovy | 28 - .../common/LettuceArgSplitterTest.java | 54 + .../http/channel/HttpRequestMessage.java | 5 +- .../liberty/LibertySingletons.java | 1 + ...LibertyDispatcherHttpAttributesGetter.java | 47 +- .../LibertyDispatcherNetAttributesGetter.java | 69 - .../LibertyDispatcherSingletons.java | 44 +- .../liberty/dispatcher/LibertyRequest.java | 4 - .../javaagent/build.gradle.kts | 2 + .../log4j/appender/v1_2/LogEventMapper.java | 23 +- .../log4j/appender/v1_2/Log4j1Test.java | 105 +- .../log4j-appender-2.17/javaagent/README.md | 14 +- .../javaagent/build.gradle.kts | 7 +- .../log4j/appender/v2_17/Log4jHelper.java | 23 +- .../log4j/appender/v2_17/Log4j2Test.java | 189 +-- .../appender/v2_17/Slf4jToLog4jTest.java | 108 +- .../log4j-appender-2.17/library/README.md | 46 +- .../library/build.gradle.kts | 9 + .../appender/v2_17/LogEventToReplay.java | 210 +++ .../appender/v2_17/OpenTelemetryAppender.java | 126 +- .../v2_17/internal/LogEventMapper.java | 24 +- .../AbstractOpenTelemetryAppenderTest.java | 239 ++++ .../LogReplayOpenTelemetryAppenderTest.java | 113 ++ .../OpenTelemetryAppenderConfigTest.java | 57 - .../OpenTelemetryAppenderConfigTestBase.java | 197 --- ...ryAppenderConfigWithOpenTelemetryTest.java | 78 -- .../v2_17/OpenTelemetryAppenderTest.java | 49 + .../v2_17/internal/LogEventMapperTest.java | 7 +- .../resources/{log4j2-test.xml => log4j2.xml} | 5 +- .../javaagent/README.md | 9 + .../javaagent/build.gradle.kts | 40 +- .../v2_17/Log4j2InstrumentationModule.java | 14 +- .../src/test/groovy/AutoLog4j2Test.groovy | 9 - .../contextdata/v2_17/AutoLog4j2Test.java | 45 + .../v2_17/AutoLog4jBaggageTest.java | 21 + .../v2_17/AutoLog4jLoggingKeysTest.java | 21 + .../library-autoconfigure/README.md | 14 + .../library-autoconfigure/build.gradle.kts | 31 + .../OpenTelemetryContextDataProvider.java | 70 +- .../src/test/groovy/LibraryLog4j2Test.groovy | 9 - .../v2_17/LibraryLog4j2BaggageTest.java | 21 + .../v2_17/LibraryLog4j2LoggingKeysTest.java | 21 + .../contextdata/v2_17/LibraryLog4j2Test.java | 21 + .../javaagent/build.gradle.kts | 31 + .../SpanDecoratingContextDataInjector.java | 63 +- .../src/test/groovy/Log4j27Test.groovy | 9 - .../contextdata/v2_7/Log4j27BaggageTest.java | 21 + .../v2_7/Log4j27LoggingKeysTest.java | 21 + .../log4j/contextdata/v2_7/Log4j27Test.java | 45 + .../testing/build.gradle.kts | 2 - .../testing/src/main/groovy/Log4j2Test.groovy | 75 - .../log4j/contextdata/Log4j2BaggageTest.java | 13 + .../contextdata/Log4j2LoggingKeysTest.java | 13 + .../log4j/contextdata/Log4j2Test.java | 135 ++ .../src/main/resources/log4j2-test.xml | 4 +- .../log4j/log4j-mdc-1.2/javaagent/README.md | 8 + .../log4j-mdc-1.2/javaagent/build.gradle.kts | 6 + .../mdc/v1_2/LoggingEventInstrumentation.java | 31 +- .../log4j/mdc/v1_2/Log4j1MdcTest.java | 15 + .../logback-appender-1.0/javaagent/README.md | 18 +- .../appender/v1_0/LogbackInstrumentation.java | 3 +- .../appender/v1_0/LogbackSingletons.java | 27 +- .../logback/appender/v1_0/LogbackTest.java | 139 +- .../logback-appender-1.0/library/README.md | 50 +- .../library/build.gradle.kts | 14 + .../appender/v1_0/LoggingEventToReplay.java | 136 ++ .../appender/v1_0/OpenTelemetryAppender.java | 145 +- .../v1_0/internal/LoggingEventMapper.java | 222 ++- .../logback/appender/v1_0/Slf4j2Test.java | 125 +- .../slf4j2ApiTest/resources/logback-test.xml | 1 + .../AbstractOpenTelemetryAppenderTest.java | 188 +++ .../LogReplayOpenTelemetryAppenderTest.java | 79 ++ .../v1_0/OpenTelemetryAppenderConfigTest.java | 173 --- .../v1_0/OpenTelemetryAppenderTest.java | 62 + .../v1_0/internal/LoggingEventMapperTest.java | 14 +- .../src/test/resources/logback-test.xml | 2 + .../logback-mdc-1.0/javaagent/README.md | 10 +- .../javaagent/build.gradle.kts | 16 + .../v1_0/LogbackWithBaggageTest.groovy | 12 - .../logback/v1_0/LogbackWithBaggageTest.java | 27 + .../v1_0/LogbackWithLoggingKeysTest.java | 27 + .../src/loggingKeysTest/resources/logback.xml | 19 + .../logback/mdc/v1_0/LogbackSingletons.java | 21 +- .../mdc/v1_0/LoggingEventInstrumentation.java | 17 +- .../logback/v1_0/LogbackTest.groovy | 12 - .../logback/v1_0/LogbackTest.java | 43 + .../logback/logback-mdc-1.0/library/README.md | 27 +- .../logback-mdc-1.0/library/build.gradle.kts | 12 + .../mdc/v1_0/LogbackWithBaggageTest.groovy | 12 - .../mdc/v1_0/LogbackWithBaggageTest.java | 26 + .../mdc/v1_0/LogbackWithLoggingKeysTest.java | 26 + .../src/loggingKeysTest/resources/logback.xml | 33 + .../mdc/v1_0/OpenTelemetryAppender.java | 31 +- .../logback/mdc/v1_0/LogbackTest.groovy | 11 - .../mdc/v1_0/internal/UnionMapTest.groovy | 81 -- .../logback/mdc/v1_0/LogbackTest.java | 21 + .../mdc/v1_0/internal/UnionMapTest.java | 104 ++ .../logback-mdc-1.0/testing/build.gradle.kts | 3 - .../mdc/v1_0/AbstractLogbackTest.groovy | 120 -- .../AbstractLogbackWithBaggageTest.groovy | 13 - .../logback/mdc/v1_0/AbstractLogbackTest.java | 145 ++ instrumentation/methods/README.md | 8 +- .../methods/MethodInstrumentation.java | 5 +- .../methods/MethodInstrumentationModule.java | 4 +- .../methods/MethodSingletons.java | 8 +- .../instrumentation/methods/MethodTest.java | 4 +- .../micrometer-1.5/{ => javaagent}/README.md | 6 +- .../micrometer-1.5/javaagent/build.gradle.kts | 4 + .../v1_5/MicrometerInstrumentationModule.java | 7 + .../micrometer/v1_5/MicrometerSingletons.java | 5 +- .../micrometer-1.5/library/README.md | 38 + .../micrometer-1.5/library/build.gradle.kts | 2 +- .../DistributionStatisticConfigModifier.java | 2 +- .../v1_5/DoubleMeasurementRecorder.java | 6 +- .../micrometer/v1_5/HistogramAdviceUtil.java | 17 +- .../v1_5/LongMeasurementRecorder.java | 6 +- .../v1_5/MeasurementRecorderUtil.java | 21 + .../v1_5/PrometheusModeNamingConvention.java | 2 +- .../v1_5/AbstractDistributionSummaryTest.java | 8 +- .../v1_5/AbstractFunctionCounterTest.java | 59 +- .../v1_5/AbstractFunctionTimerTest.java | 85 ++ .../micrometer/v1_5/AbstractGaugeTest.java | 72 +- .../v1_5/AbstractPrometheusModeTest.java | 6 +- .../micrometer/v1_5/AbstractTimerTest.java | 8 +- instrumentation/mongo/README.md | 5 + .../v3_1/MongoInstrumentationSingletons.java | 8 +- .../mongo/mongo-3.1/library/README.md | 45 + .../mongo/v3_1/MongoAttributesExtractor.java | 6 +- .../mongo/v3_1/MongoDbAttributesGetter.java | 8 +- .../mongo/v3_1/MongoInstrumenterFactory.java | 10 +- ...java => MongoNetworkAttributesGetter.java} | 4 +- .../v3_1/AbstractMongo31ClientTest.groovy | 10 +- .../v3_7/MongoInstrumentationSingletons.java | 8 +- .../src/test/groovy/MongoClientTest.groovy | 10 +- .../v4_0/BaseClusterInstrumentation.java | 18 + ...aultConnectionPoolTaskInstrumentation.java | 43 + .../MongoClientInstrumentationModule.java | 1 + .../v4_0/MongoInstrumentationSingletons.java | 8 +- .../mongo/v4_0/TaskWrapper.java | 27 + .../groovy/Mongo4ReactiveClientTest.groovy | 54 +- .../src/test/groovy/MongoClientTest.groovy | 6 +- .../v3_3/MongoInstrumentationSingletons.java | 8 +- .../test/groovy/MongoAsyncClientTest.groovy | 6 +- .../testing/AbstractMongoClientTest.groovy | 27 +- .../mybatis-3.2/javaagent/build.gradle.kts | 26 + .../v3_2/MapperMethodInstrumentation.java | 71 + .../v3_2/MyBatisInstrumentationModule.java | 32 + .../mybatis/v3_2/MyBatisSingletons.java | 38 + .../v3_2/SqlCommandInstrumentation.java | 44 + .../mybatis/v3_2/SqlCommandUtil.java | 29 + .../mybatis/v3_2/MyBatisTest.java | 66 + .../mybatis/v3_2/TestMapper.java | 14 + instrumentation/netty/README.md | 2 +- .../v3_8/NettyChannelInstrumentation.java | 43 +- .../NettyChannelPipelineInstrumentation.java | 25 +- .../v3_8/NettyInstrumentationModule.java | 9 +- .../netty/v3_8/NettyScope.java | 36 + .../v3_8/client/NettyClientSingletons.java | 48 +- .../NettyConnectHttpAttributesGetter.java | 44 +- .../NettyConnectNetAttributesGetter.java | 66 - .../NettyHttpClientAttributesGetter.java | 51 +- .../NettyNetClientAttributesGetter.java | 71 - .../NettyHttpServerAttributesGetter.java | 50 +- .../NettyNetServerAttributesGetter.java | 81 -- .../v3_8/server/NettyServerSingletons.java | 48 +- .../netty/v3_8/client/Netty38ClientTest.java | 12 +- .../netty/v3_8/server/Netty38ServerTest.java | 4 +- ...ctNettyChannelPipelineInstrumentation.java | 92 +- .../v4/common/FutureListenerWrappers.java | 2 +- .../v4/common/NettyFutureInstrumentation.java | 45 +- .../netty/v4/common/NettyScope.java | 55 + .../NettyClientInstrumenterFactory.java | 155 +- .../NettyConnectHttpAttributesGetter.java | 44 +- .../NettyConnectNetAttributesGetter.java | 66 - .../NettyConnectionInstrumentationFlag.java | 20 + .../NettyHttpClientAttributesGetter.java | 51 +- .../NettyNetClientAttributesGetter.java | 71 - .../NettySslInstrumentationHandler.java | 3 +- .../client/NettySslNetAttributesGetter.java | 28 +- .../client/NoopConnectionInstrumenter.java | 34 + .../internal/client/NoopSslInstrumenter.java | 28 + .../NettyHttpServerAttributesGetter.java | 50 +- .../NettyNetServerAttributesGetter.java | 81 -- .../NettyServerInstrumenterFactory.java | 62 +- .../netty-4.0/javaagent/build.gradle.kts | 1 + .../netty/v4_0/BootstrapInstrumentation.java | 33 +- .../NettyChannelPipelineInstrumentation.java | 11 +- .../v4_0/NettyInstrumentationModule.java | 9 +- .../v4_0/client/NettyClientSingletons.java | 45 +- .../v4_0/server/NettyServerSingletons.java | 12 +- .../src/test/groovy/ChannelFutureTest.groovy | 74 - .../test/groovy/ChannelPipelineTest.groovy | 136 -- .../test/groovy/Netty40ClientSslTest.groovy | 215 --- .../src/test/groovy/Netty40ClientTest.groovy | 150 -- .../groovy/Netty40ConnectionSpanTest.groovy | 165 --- .../src/test/groovy/Netty40ServerTest.groovy | 153 -- .../netty/v4_0/ChannelFutureTest.java | 82 ++ .../netty/v4_0/ChannelPipelineTest.java | 228 +++ .../netty/v4_0/client}/ClientHandler.java | 22 +- .../v4_0/client/Netty40ClientSslTest.java | 249 ++++ .../netty/v4_0/client/Netty40ClientTest.java | 181 +++ .../client/Netty40ConnectionSpanTest.java | 175 +++ .../netty/v4_0/server/Netty40ServerTest.java | 222 +++ .../netty-4.1/javaagent/build.gradle.kts | 5 +- ...tChannelHandlerContextInstrumentation.java | 8 +- .../netty/v4_1/BootstrapInstrumentation.java | 40 +- .../NettyChannelPipelineInstrumentation.java | 39 +- .../netty/v4_1/NettyClientSingletons.java | 54 +- .../v4_1/NettyInstrumentationModule.java | 12 +- .../netty/v4_1/NettyServerSingletons.java | 31 +- ...gleThreadEventExecutorInstrumentation.java | 51 + .../test/groovy/ChannelPipelineTest.groovy | 39 +- .../test/groovy/Netty41ClientSslTest.groovy | 50 +- .../groovy/Netty41ConnectionSpanTest.groovy | 34 +- .../netty/netty-4.1/library/build.gradle.kts | 3 + .../netty/v4_1/NettyClientTelemetry.java | 23 +- .../v4_1/NettyClientTelemetryBuilder.java | 96 +- .../netty/v4_1/NettyServerTelemetry.java | 24 +- .../v4_1/NettyServerTelemetryBuilder.java | 86 +- .../netty/v4_1/internal/AttributeKeys.java | 7 +- .../v4_1/internal/ProtocolEventHandler.java | 45 + .../v4_1/internal/ProtocolSpecificEvent.java | 67 + .../netty/v4_1/internal/ServerContext.java | 31 + .../netty/v4_1/internal/ServerContexts.java | 79 ++ .../HttpClientResponseTracingHandler.java | 57 +- .../client/HttpClientTracingHandler.java | 7 +- .../client/NettyClientHandlerFactory.java | 60 + .../HttpServerRequestTracingHandler.java | 46 +- .../HttpServerResponseTracingHandler.java | 92 +- .../server/HttpServerTracingHandler.java | 7 +- .../netty/v4_1/AbstractNetty41ClientTest.java | 7 +- .../netty/v4_1/AbstractNetty41ServerTest.java | 4 +- .../v2_2/OkHttp2HttpAttributesGetter.java | 49 +- .../v2_2/OkHttp2NetAttributesGetter.java | 62 - .../okhttp/v2_2/OkHttp2Singletons.java | 35 +- .../okhttp/v2_2/OkHttp2Test.java | 10 +- .../okhttp-3.0/javaagent/build.gradle.kts | 24 + .../okhttp/v3_0/OkHttp3Http2Test.java | 38 + .../OkHttp3DispatcherInstrumentation.java | 4 +- .../okhttp/v3_0/OkHttp3Singletons.java | 21 +- .../okhttp/v3_0/OkHttp3Test.java | 5 +- .../okhttp-3.0/library/build.gradle.kts | 25 + .../okhttp/v3_0/OkHttp3Http2Test.java | 42 + .../okhttp/v3_0/ContextInterceptor.java | 4 +- .../okhttp/v3_0/OkHttpTelemetry.java | 6 +- .../okhttp/v3_0/OkHttpTelemetryBuilder.java | 78 +- .../ConnectionErrorSpanInterceptor.java | 12 +- .../v3_0/internal/OkHttpAttributesGetter.java | 102 +- ...kHttpClientInstrumenterBuilderFactory.java | 27 + .../internal/OkHttpInstrumenterFactory.java | 52 - .../internal/OkHttpNetAttributesGetter.java | 66 - .../v3_0/internal/TracingInterceptor.java | 10 +- .../okhttp/v3_0/OkHttp3Test.java | 13 +- .../okhttp/v3_0/AbstractOkHttp3Test.java | 16 +- .../JavaagentInstrumentationTest.java | 39 +- instrumentation/opensearch/README.md | 6 +- .../src/test/java/OpenSearchRestTest.java | 53 +- .../rest/OpenSearchRestAttributesGetter.java | 6 +- .../OpenSearchRestInstrumenterFactory.java | 13 +- ...SearchRestNetResponseAttributesGetter.java | 33 +- .../javaagent/build.gradle.kts | 11 +- ...OpenTelemetryApiInstrumentationModule.java | 18 +- .../InstrumentationApiContextBridging.java | 63 +- .../trace/ApplicationSpan.java | 14 + .../opentelemetryapi/trace/Bridging.java | 12 + .../src/test/groovy/ContextBridgeTest.groovy | 169 --- .../src/test/groovy/ContextTest.groovy | 72 - .../src/test/groovy/TracerTest.groovy | 357 ----- .../opentelemetryapi/ContextBridgeTest.java | 197 +++ .../opentelemetryapi/ContextTest.java | 90 ++ .../opentelemetryapi/TracerTest.java | 328 +++++ .../javaagent/build.gradle.kts | 8 + ...OpenTelemetryApiInstrumentationModule.java | 9 +- .../ApplicationDoubleCounterBuilder.java | 5 +- .../ApplicationDoubleGaugeBuilder.java | 5 +- .../ApplicationDoubleHistogramBuilder.java | 4 +- ...ApplicationDoubleUpDownCounterBuilder.java | 4 +- .../ApplicationLongCounterBuilder.java | 5 +- .../metrics/ApplicationLongGaugeBuilder.java | 5 +- .../ApplicationLongHistogramBuilder.java | 5 +- .../ApplicationLongUpDownCounterBuilder.java | 4 +- .../v1_10/metrics/MeterTest.java | 23 +- .../javaagent/build.gradle.kts | 8 + ...OpenTelemetryApiInstrumentationModule.java | 9 +- .../v1_15/metrics/ApplicationMeter115.java | 4 +- .../javaagent/build.gradle.kts | 8 + .../v1_27/ApplicationOpenTelemetry127.java | 60 +- ...OpenTelemetryApiInstrumentationModule.java | 9 +- .../javaagent/build.gradle.kts | 48 + ...OpenTelemetryApiInstrumentationModule.java | 40 + .../OpenTelemetryInstrumentation.java | 40 + .../ApplicationDoubleCounterBuilder131.java | 31 + .../ApplicationDoubleGaugeBuilder131.java | 55 + .../ApplicationDoubleHistogramBuilder131.java | 44 + ...licationDoubleUpDownCounterBuilder131.java | 31 + .../ApplicationLongCounterBuilder131.java | 36 + .../ApplicationLongGaugeBuilder131.java | 48 + .../ApplicationLongHistogramBuilder131.java | 38 + ...pplicationLongUpDownCounterBuilder131.java | 37 + .../metrics/ApplicationMeter131.java | 64 + .../metrics/ApplicationMeterFactory131.java | 18 + .../v1_31/metrics/NoopTest.java | 43 + .../v1_31/metrics/MeterTest.java | 467 ++++++ .../javaagent/build.gradle.kts | 54 + .../v1_32/incubator/metrics/MeterTest.java | 467 ++++++ ...OpenTelemetryApiInstrumentationModule.java | 44 + .../v1_32/OpenTelemetryInstrumentation.java | 40 + ...etryApiIncubatorInstrumentationModule.java | 42 + ...OpenTelemetryIncubatorInstrumentation.java | 40 + ...ionDoubleHistogramBuilder132Incubator.java | 44 + ...ationLongHistogramBuilder132Incubator.java | 38 + .../metrics/ApplicationMeter132Incubator.java | 29 + .../ApplicationMeterFactory132Incubator.java | 18 + .../ApplicationDoubleHistogramBuilder132.java | 33 + .../ApplicationLongHistogramBuilder132.java | 27 + .../v1_32/metrics/ApplicationMeter132.java | 24 + .../metrics/ApplicationMeterFactory132.java | 18 + .../v1_32/incubator/metrics/NoopTest.java | 43 + .../v1_32/metrics/MeterTest.java | 116 ++ .../javaagent/build.gradle.kts | 70 + .../v1_37/incubator/metrics/MeterTest.java | 467 ++++++ ...OpenTelemetryApiInstrumentationModule.java | 40 + .../OpenTelemetryInstrumentation.java | 40 + .../ApplicationDoubleCounterBuilder137.java | 31 + .../ApplicationDoubleGaugeBuilder137.java | 55 + .../ApplicationDoubleHistogramBuilder137.java | 44 + ...licationDoubleUpDownCounterBuilder137.java | 31 + .../ApplicationLongCounterBuilder137.java | 36 + .../ApplicationLongGaugeBuilder137.java | 48 + .../ApplicationLongHistogramBuilder137.java | 38 + ...pplicationLongUpDownCounterBuilder137.java | 37 + .../metrics/ApplicationMeter137.java | 28 + .../metrics/ApplicationMeterFactory137.java | 18 + .../metrics/BaseApplicationMeter137.java | 54 + .../v1_37/incubator/metrics/NoopTest.java | 43 + .../v1_37/incubator/metrics/MeterTest.java | 467 ++++++ .../v1_37/metrics/MeterTest.java | 116 ++ .../javaagent/build.gradle.kts | 62 + .../v1_38/incubator/metrics/MeterTest.java | 134 ++ ...OpenTelemetryApiInstrumentationModule.java | 44 + .../v1_38/OpenTelemetryInstrumentation.java | 40 + ...etryApiIncubatorInstrumentationModule.java | 42 + ...OpenTelemetryIncubatorInstrumentation.java | 40 + ...icationDoubleGaugeBuilder138Incubator.java | 37 + ...plicationLongGaugeBuilder138Incubator.java | 31 + .../metrics/ApplicationMeter138Incubator.java | 29 + .../ApplicationMeterFactory138Incubator.java | 18 + .../ApplicationDoubleGaugeBuilder138.java | 55 + .../ApplicationLongGaugeBuilder138.java | 48 + .../v1_38/metrics/ApplicationMeter138.java | 24 + .../metrics/ApplicationMeterFactory138.java | 18 + .../v1_38/incubator/metrics/NoopTest.java | 43 + .../v1_38/metrics/MeterTest.java | 125 ++ .../javaagent/build.gradle.kts | 8 + ...OpenTelemetryApiInstrumentationModule.java | 9 +- .../README.md | 6 +- .../MethodCodeAttributesGetter.java | 2 +- .../MethodRequestCodeAttributesGetter.java | 2 +- .../WithSpanInstrumentation.java | 4 +- .../WithSpanSingletons.java | 4 +- .../WithSpanInstrumentationTest.java | 89 +- .../javaagent/build.gradle.kts | 10 +- ...ContextExtensionInstrumentationModule.java | 9 +- .../ContextExtensionInstrumentationTest.kt | 6 +- .../README.md | 6 +- .../AnnotationExcludedMethods.java | 8 +- .../AnnotationInstrumentationModule.java | 2 +- .../AnnotationSingletons.java | 4 +- .../KotlinCoroutineUtil.java | 32 + .../MethodCodeAttributesGetter.java | 2 +- .../MethodRequestCodeAttributesGetter.java | 2 +- .../WithSpanInstrumentation.java | 7 +- ...dingSpanAttributesInstrumentationTest.java | 8 +- .../WithSpanInstrumentationTest.java | 88 +- .../javaagent/build.gradle.kts | 12 +- .../HttpRouteStateInstrumentation.java | 20 + ...strumentationApiInstrumentationModule.java | 9 +- .../instrumentationapi/ContextBridgeTest.java | 25 +- .../testing/AgentSpanTestingInstrumenter.java | 13 +- .../MockHttpServerAttributesGetter.java | 2 +- .../MockNetServerAttributesGetter.java | 26 - .../javaagent/build.gradle.kts | 7 +- ...niversalConnectionPoolInstrumentation.java | 21 +- .../oracle-ucp-11.2/library/build.gradle.kts | 7 +- .../v11_2/ConnectionPoolMetrics.java | 2 +- .../oracle-ucp-11.2/testing/build.gradle.kts | 3 +- .../AbstractOracleUcpInstrumentationTest.java | 49 +- instrumentation/oshi/README.md | 9 + .../oshi/MetricsRegistration.java | 25 +- .../oshi/OshiMetricsInstaller.java | 3 +- .../oshi/SystemInfoInstrumentation.java | 3 +- instrumentation/oshi/library/build.gradle.kts | 6 +- .../instrumentation/oshi/ProcessMetrics.java | 51 +- .../instrumentation/oshi/SystemMetrics.java | 204 +-- .../oshi/ProcessMetricsTest.java | 33 +- .../oshi/SystemMetricsTest.java | 33 +- .../StandardWrapperInstrumentation.java | 3 +- .../javaagent/build.gradle.kts | 38 + .../v1_0/PekkoActorCellInstrumentation.java | 76 + .../v1_0/PekkoActorInstrumentationModule.java | 28 + ...aultSystemMessageQueueInstrumentation.java | 65 + .../v1_0/PekkoDispatcherInstrumentation.java | 59 + .../v1_0/PekkoIgnoredTypesConfigurer.java | 23 + .../pekkoactor/v1_0/PekkoActorTest.scala | 135 ++ .../pekkoactor/v1_0/PekkoActors.scala | 146 ++ .../pekko-http-1.0/javaagent/build.gradle.kts | 54 + .../pekkohttp/v1_0/PekkoHttpUtil.java | 52 + .../pekkohttp/v1_0/client/FutureWrapper.java | 37 + .../client/HttpExtClientInstrumentation.java | 84 ++ .../v1_0/client/HttpHeaderSetter.java | 53 + .../v1_0/client/OnCompleteHandler.java | 34 + .../PekkoHttpClientAttributesGetter.java | 68 + .../PekkoHttpClientInstrumentationModule.java | 25 + .../client/PekkoHttpClientSingletons.java | 37 + .../PoolMasterActorInstrumentation.java | 50 + .../GraphInterpreterInstrumentation.java | 52 + .../server/HttpExtServerInstrumentation.java | 43 + .../v1_0/server/PekkoFlowWrapper.java | 210 +++ .../v1_0/server/PekkoHttpResponseMutator.java | 27 + .../PekkoHttpServerAttributesGetter.java | 70 + .../v1_0/server/PekkoHttpServerHeaders.java | 30 + .../PekkoHttpServerInstrumentationModule.java | 44 + .../server/PekkoHttpServerSingletons.java | 63 + .../PekkoHttpServerSourceInstrumentation.java | 42 + .../PekkoServerIgnoredTypesConfigurer.java | 22 + .../PathConcatenationInstrumentation.java | 41 + .../route/PathMatcherInstrumentation.java | 47 + .../PathMatcherStaticInstrumentation.java | 67 + ...oHttpServerRouteInstrumentationModule.java | 40 + .../v1_0/server/route/PekkoRouteHolder.java | 87 ++ .../RouteConcatenationInstrumentation.java | 55 + .../v1_0/PekkoHttpServerJavaRouteTest.java | 64 + .../src/test/resources/application.conf | 11 + ...bstractHttpServerInstrumentationTest.scala | 47 + .../PekkoHttpClientInstrumentationTest.scala | 129 ++ .../PekkoHttpServerInstrumentationTest.scala | 109 ++ ...koHttpServerInstrumentationTestAsync.scala | 35 + ...kkoHttpServerInstrumentationTestSync.scala | 36 + .../v1_0/PekkoHttpServerRouteTest.scala | 113 ++ .../v1_0/PekkoHttpTestAsyncWebServer.scala | 81 ++ .../PekkoHttpTestServerSourceWebServer.scala | 124 ++ .../v1_0/PekkoHttpTestSyncWebServer.scala | 78 ++ .../v1_0/PekkoHttpTestWebServer.scala | 117 ++ .../play-mvc-2.4/javaagent/build.gradle.kts | 3 + .../play/v2_4/ActionInstrumentation.java | 3 + .../play/v2_4/Play24Singletons.java | 8 +- .../groovy/client/PlayWsClientTest.groovy | 6 +- .../groovy/server/PlayServerTest.groovy | 9 +- .../groovy/client/PlayWsClientTest.groovy | 17 +- .../test/groovy/server/PlayServerTest.groovy | 9 +- .../play-mvc-2.6/javaagent/build.gradle.kts | 13 +- .../groovy/server/PlayServerTest.groovy | 6 + .../play/v2_6/ActionInstrumentation.java | 3 + .../play/v2_6/Play26Singletons.java | 6 +- .../test/groovy/server/PlayServerTest.groovy | 6 + .../PlayWsClientHttpAttributesGetter.java | 24 +- .../PlayWsClientInstrumenterFactory.java | 28 +- .../PlayWsClientNetAttributesGetter.java | 37 - .../main/groovy/PlayWsClientTestBase.groovy | 52 +- .../groovy/PlayWsClientTestBaseBase.groovy | 46 +- instrumentation/pulsar/pulsar-2.8/README.md | 6 +- .../pulsar/v2_8/UrlParserTest.java | 7 + .../pulsar-2.8/javaagent/build.gradle.kts | 23 +- .../v2_8/ConsumerBaseInstrumentation.java | 50 + .../v2_8/ConsumerImplInstrumentation.java | 10 +- .../pulsar/v2_8/MessageInstrumentation.java | 2 +- .../v2_8/MessageListenerInstrumentation.java | 2 +- .../v2_8/ProducerImplInstrumentation.java | 61 +- .../v2_8/PulsarInstrumentationModule.java | 4 +- .../pulsar/v2_8/SendCallbackData.java | 23 + .../v2_8/SendCallbackInstrumentation.java | 75 + .../pulsar/v2_8/UrlParser.java | 9 +- .../pulsar/v2_8/VirtualFieldStore.java | 14 + .../telemetry/MessageListenerContext.java | 31 + .../PulsarBatchMessagingAttributesGetter.java | 39 +- .../v2_8/telemetry/PulsarBatchRequest.java | 6 +- .../PulsarMessagingAttributesGetter.java | 41 +- .../PulsarNetClientAttributesGetter.java | 9 +- .../v2_8/telemetry/PulsarSingletons.java | 107 +- .../pulsar/v2_8/PulsarClientTest.groovy | 670 --------- .../pulsar/v2_8/AbstractPulsarClientTest.java | 446 ++++++ .../PulsarClientSuppressReceiveSpansTest.java | 346 +++++ .../pulsar/v2_8/PulsarClientTest.java | 674 +++++++++ .../reactive/AbstractQuarkusJaxRsTest.java | 21 + .../javaagent/build.gradle.kts | 3 +- ...esteasyReactiveContextInstrumentation.java | 51 + .../InvocationHandlerInstrumentation.java | 7 +- .../resteasy/reactive/OtelRequestContext.java | 40 + ...ResteasyReactiveInstrumentationModule.java | 6 +- .../reactive/ResteasyReactiveSpanName.java | 13 +- .../quarkus2-testing/build.gradle.kts | 3 + .../resteasy/reactive/v2_0/TestFilter.java | 27 + .../quarkus3-testing/build.gradle.kts | 5 +- .../resteasy/reactive/v3_0/TestFilter.java | 27 + instrumentation/quartz-2.0/README.md | 5 + .../quartz/v2_0/QuartzSingletons.java | 4 +- .../v2_0/QuartzCodeAttributesGetter.java | 2 +- .../quartz/v2_0/QuartzTelemetryBuilder.java | 27 +- .../quartz/v2_0/AbstractQuartzTest.java | 11 +- instrumentation/r2dbc-1.0/README.md | 5 + .../r2dbc/v1_0/R2dbcSingletons.java | 18 +- .../build.gradle.kts | 20 +- .../r2dbc-1.0/library/build.gradle.kts | 2 +- .../r2dbc/v1_0/R2dbcTelemetryBuilder.java | 16 +- .../r2dbc/v1_0/internal/DbExecution.java | 6 +- .../internal/R2dbcInstrumenterBuilder.java | 24 +- .../internal/R2dbcNetAttributesGetter.java | 4 +- .../internal/R2dbcSqlAttributesGetter.java | 2 +- .../r2dbc-1.0/testing/build.gradle.kts | 2 +- .../v1_0/AbstractR2dbcStatementTest.java | 31 +- instrumentation/rabbitmq-2.7/README.md | 4 +- .../rabbitmq-2.7/javaagent/build.gradle.kts | 3 - .../rabbitmq/DeliveryRequest.java | 14 +- .../RabbitChannelAttributesGetter.java | 29 +- .../RabbitChannelInstrumentation.java | 16 +- .../RabbitChannelNetAttributesGetter.java | 33 +- .../RabbitDeliveryAttributesGetter.java | 29 +- ...abbitDeliveryExtraAttributesExtractor.java | 5 +- .../RabbitDeliveryNetAttributesGetter.java | 40 + .../rabbitmq/RabbitInstrumenterHelper.java | 13 +- .../RabbitReceiveAttributesGetter.java | 29 +- .../RabbitReceiveNetAttributesGetter.java | 33 +- .../rabbitmq/RabbitSingletons.java | 36 +- .../rabbitmq/TracedDelegatingConsumer.java | 7 +- .../src/test/groovy/RabbitMqTest.groovy | 466 ------ .../test/groovy/ReactorRabbitMqTest.groovy | 54 - .../src/test/groovy/WithRabbitMqTrait.groovy | 38 - .../rabbitmq/AbstractRabbitMqTest.java | 59 + .../rabbitmq/RabbitMqTest.java | 912 ++++++++++++ .../rabbitmq/ReactorRabbitMqTest.java | 66 + .../ratpack-1.4/javaagent/build.gradle.kts | 10 + .../DefaultExecStarterInstrumentation.java | 5 +- .../ratpack/RatpackInstrumentationModule.java | 10 +- .../ratpack/RatpackSingletons.java | 7 +- .../ratpack/TracingHandler.java | 14 +- .../ratpack/RatpackForkedHttpClientTest.java | 6 +- .../ratpack/RatpackHttpClientTest.java | 6 +- .../ratpack/RatpackPooledHttpClientTest.java | 6 +- .../AbstractRatpackHttpServerTest.groovy | 2 +- .../server/AbstractRatpackRoutesTest.groovy | 37 +- .../client/AbstractRatpackHttpClientTest.java | 8 +- .../ratpack-1.7/library/build.gradle.kts | 8 + .../ratpack/v1_7/OpenTelemetryHttpClient.java | 4 +- .../v1_7/OpenTelemetryServerHandler.java | 6 +- .../v1_7/RatpackHttpAttributesGetter.java | 27 +- .../RatpackHttpClientAttributesGetter.java | 13 +- .../ratpack/v1_7/RatpackTelemetryBuilder.java | 161 ++- .../RatpackNetClientAttributesGetter.java | 30 - .../RatpackNetServerAttributesGetter.java | 68 - .../client/InstrumentedHttpClientTest.groovy | 188 +-- .../RatpackServerApplicationTest.groovy | 24 +- .../v1_7/server/RatpackServerTest.groovy | 27 +- .../v1_7/AbstractRatpackHttpClientTest.java | 8 +- instrumentation/reactor/reactor-3.1/README.md | 4 +- .../reactor-3.1/javaagent/build.gradle.kts | 15 +- .../reactor/v3_1/HooksInstrumentation.java | 4 +- .../v3_1/ReactorInstrumentationModule.java | 10 +- ...extPropagationOperatorInstrumentation.java | 6 +- ...pagationOperatorInstrumentationModule.java | 10 +- .../reactor/v3_1/BaseFluxWithSpanTest.java | 4 +- .../reactor/v3_1/BaseMonoWithSpanTest.java | 9 +- .../reactor-3.1/library/build.gradle.kts | 13 +- .../v3_1/ContextPropagationOperator.java | 62 + .../reactor/v3_1/TracingSubscriber.java | 35 +- .../reactor/v3_1/HooksTest.java | 2 +- .../reactor/v3_1/ReactorCoreTest.java | 17 +- .../reactor-3.4/javaagent/build.gradle.kts | 34 + ...tPropagationOperator34Instrumentation.java | 66 + ...gationOperator34InstrumentationModule.java | 42 + ...pagationOperator34InstrumentationTest.java | 55 + .../javaagent/build.gradle.kts | 29 +- .../kafka/v1_0/InstrumentedKafkaReceiver.java | 23 +- ...Access.java => KafkaReceiver13Access.java} | 15 +- .../kafka/v1_0/ReactorKafkaSingletons.java | 4 +- .../ReactorKafka1321InstrumentationTest.java | 33 + .../kafka/v1_0/AbstractReactorKafkaTest.java | 69 +- .../reactor/reactor-netty/README.md | 2 +- .../javaagent/build.gradle.kts | 1 + .../AbstractReactorNettyHttpClientTest.java | 10 +- .../v0_9/ReactorNettyConnectionSpanTest.java | 42 +- .../javaagent-unit-tests/build.gradle.kts | 1 + .../v1_0/FailedRequestWithUrlMakerTest.java | 94 ++ .../javaagent/build.gradle.kts | 6 +- .../reactornetty/v1_0/DecoratorFunctions.java | 11 +- .../v1_0/FailedRequestWithUrlMaker.java | 90 ++ .../HttpClientConnectInstrumentation.java | 53 + .../HttpResponseReceiverInstrumenter.java | 159 +-- .../v1_0/InstrumentationContexts.java | 104 ++ .../reactornetty/v1_0/ReactorContextKeys.java | 6 +- ...eactorNettyHttpClientAttributesGetter.java | 121 +- .../ReactorNettyInstrumentationModule.java | 16 +- ...ReactorNettyNetClientAttributesGetter.java | 97 -- .../v1_0/ReactorNettySingletons.java | 67 +- .../TransportConnectorInstrumentation.java | 9 +- .../http/client/HttpClientConfigBuddy.java | 25 + .../AbstractReactorNettyHttpClientTest.java | 42 +- .../v1_0/CustomNameResolverGroup.java | 2 + .../v1_0/ReactorNettyBaseUrlOnlyTest.java | 27 +- .../v1_0/ReactorNettyClientSslTest.java | 125 +- .../v1_0/ReactorNettyConnectionSpanTest.java | 92 +- ...torNettyHttpClientDeferredHeadersTest.java | 56 + .../v1_0/ReactorNettyHttpClientTest.java | 6 +- .../rediscala-1.8/javaagent/build.gradle.kts | 9 +- .../rediscala/RediscalaAttributesGetter.java | 6 +- .../rediscala/RediscalaSingletons.java | 4 +- .../rediscala/RequestInstrumentation.java | 17 +- .../test/groovy/RediscalaClientTest.groovy | 57 +- .../v3_0/RedissonAsyncClientTest.java | 10 + .../redisson/v3_0/RedissonClientTest.java | 17 + .../groovy/RedissonAsyncClientTest.groovy | 12 - .../src/test/groovy/RedissonClientTest.groovy | 12 - .../v3_17/RedissonAsyncClientTest.java | 16 + .../redisson/v3_17/RedissonClientTest.java | 20 + .../redisson/RedissonDbAttributesGetter.java | 6 +- .../redisson/RedissonInstrumenterFactory.java | 8 +- .../redisson/RedissonNetAttributesGetter.java | 19 +- .../redisson/RedissonRequest.java | 6 +- .../redisson-common/testing/build.gradle.kts | 2 - .../AbstractRedissonAsyncClientTest.groovy | 238 ---- .../groovy/AbstractRedissonClientTest.groovy | 388 ----- .../AbstractRedissonAsyncClientTest.java | 267 ++++ .../redisson/AbstractRedissonClientTest.java | 439 ++++++ instrumentation/resources/library/README.md | 18 +- .../resources/library/build.gradle.kts | 12 +- .../resources/AttributeProvider.java | 28 + .../resources/AttributeResourceProvider.java | 110 ++ .../CgroupV2ContainerIdExtractor.java | 47 +- .../resources/ContainerResource.java | 14 +- .../resources/HostIdResourceProvider.java | 185 +++ .../resources/HostResource.java | 13 +- .../resources/JarServiceNameDetector.java | 98 +- .../resources/MainJarPathFinder.java | 89 ++ .../resources/MainJarPathHolder.java | 20 + .../resources/ManifestResourceProvider.java | 86 ++ .../instrumentation/resources/OsResource.java | 53 +- .../resources/ProcessResource.java | 22 +- .../resources/ProcessRuntimeResource.java | 17 +- .../CgroupV2ContainerIdExtractorTest.java | 70 +- .../resources/ContainerResourceTest.java | 2 +- .../resources/HostIdResourceProviderTest.java | 125 ++ .../resources/HostResourceTest.java | 11 +- .../resources/JarServiceNameDetectorTest.java | 43 +- .../ManifestResourceProviderTest.java | 106 ++ .../resources/OsResourceTest.java | 97 +- .../resources/ProcessResourceTest.java | 26 +- .../resources/ProcessRuntimeResourceTest.java | 12 +- .../library/src/test/resources/MANIFEST.MF | 3 + .../resources/containerd_proc_self_mountinfo | 38 + .../test/resources/crio_proc_self_mountinfo | 26 + .../test/resources/crio_proc_self_mountinfo1 | 39 + .../test/resources/crio_proc_self_mountinfo2 | 38 + .../test/resources/docker_proc_self_mountinfo | 2 - .../resources/docker_proc_self_mountinfo1 | 36 + .../src/test/resources/empty-MANIFEST.MF | 1 + .../test/resources/podman_proc_self_mountinfo | 2 - .../restlet-1.1/javaagent/build.gradle.kts | 4 + .../restlet/v1_1/RestletSingletons.java | 11 +- .../restlet/v1_1/RouteInstrumentation.java | 6 +- .../restlet/v1_1/ServerInstrumentation.java | 6 +- .../restlet/v1_1/RestletServerTest.groovy | 4 +- .../restlet-1.1/library/build.gradle.kts | 4 + .../v1_1/RestletHttpAttributesGetter.java | 54 +- .../v1_1/RestletNetAttributesGetter.java | 80 -- .../restlet/v1_1/RestletTelemetryBuilder.java | 93 +- .../restlet/v1_1/TracingFilter.java | 6 +- .../v1_1/AbstractRestletServerTest.groovy | 9 +- .../v1_1/AbstractServletServerTest.groovy | 14 +- .../spring/AbstractSpringServerTest.groovy | 4 + .../restlet-2.0/javaagent/build.gradle.kts | 4 + .../restlet/v2_0/RestletSingletons.java | 28 +- .../restlet/v2_0/RouteInstrumentation.java | 6 +- .../restlet/v2_0/ServerInstrumentation.java | 6 +- .../restlet/v2_0/RestletServerTest.groovy | 4 +- .../restlet-2.0/library/build.gradle.kts | 4 + .../restlet/v2_0/RestletTelemetryBuilder.java | 75 +- .../restlet/v2_0/TracingFilter.java | 6 +- .../internal/RestletHttpAttributesGetter.java | 31 +- .../internal/RestletInstrumenterFactory.java | 38 +- ...butesGetter.java => ServerCallAccess.java} | 80 +- .../v2_0/AbstractRestletServerTest.groovy | 9 +- .../spring/AbstractSpringServerTest.groovy | 14 + .../rmi/client/RmiClientAttributesGetter.java | 2 +- .../rmi/client/RmiClientSingletons.java | 4 +- ...ntextPropagationInstrumentationModule.java | 7 + .../server/RemoteServerInstrumentation.java | 2 +- .../rmi/server/RmiServerAttributesGetter.java | 4 +- .../rmi/server/RmiServerSingletons.java | 6 +- .../instrumentation/rmi/RmiTest.java | 52 +- .../rocketmq-client-4.8/README.md | 4 +- .../javaagent/build.gradle.kts | 5 + .../v4_8/RocketMqClientHooks.java | 22 +- .../v4_8/RocketMqClientTest.groovy | 22 - .../v4_8/RocketMqClientTest.java | 29 + .../library/build.gradle.kts | 3 + .../v4_8/RocketMqConsumerAttributeGetter.java | 29 +- ...onsumerExperimentalAttributeExtractor.java | 7 +- .../v4_8/RocketMqInstrumenterFactory.java | 48 +- .../v4_8/RocketMqProducerAttributeGetter.java | 39 +- ...roducerExperimentalAttributeExtractor.java | 7 +- .../v4_8/RocketMqTelemetry.java | 7 +- .../v4_8/RocketMqTelemetryBuilder.java | 19 +- .../v4_8/RocketMqClientTest.groovy | 32 - .../v4_8/RocketMqClientTest.java | 53 + .../testing/build.gradle.kts | 2 - .../v4_8/AbstractRocketMqClientTest.groovy | 355 ----- .../v4_8/TracingMessageListener.groovy | 43 - .../v4_8/AbstractRocketMqClientTest.java | 511 +++++++ .../v4_8/TracingMessageListener.java | 45 + .../javaagent/build.gradle.kts | 1 + .../v5_0/ReceiveSpanFinishingCallback.java | 3 +- ...etMqConsumerProcessAttributeExtractor.java | 10 +- ...ocketMqConsumerProcessAttributeGetter.java | 30 +- ...etMqConsumerReceiveAttributeExtractor.java | 2 +- ...ocketMqConsumerReceiveAttributeGetter.java | 30 +- .../v5_0/RocketMqInstrumenterFactory.java | 16 +- .../RocketMqProducerAttributeExtractor.java | 31 +- .../v5_0/RocketMqProducerAttributeGetter.java | 30 +- .../testing/build.gradle.kts | 2 +- ...RocketMqClientSuppressReceiveSpanTest.java | 34 +- .../v5_0/AbstractRocketMqClientTest.java | 70 +- .../v5_0/RocketMqProxyContainer.java | 19 +- instrumentation/runtime-telemetry/README.md | 9 + .../java17/Java17RuntimeMetricsInstaller.java | 21 +- .../test/groovy/JmxRuntimeMetricsTest.groovy | 37 - .../java17/JfrRuntimeMetricsTest.java | 35 +- .../java17/JmxRuntimeMetricsTest.java | 38 + .../library/README.md | 27 +- .../java17/RuntimeMetricsBuilder.java | 22 +- .../java17/internal/Constants.java | 80 +- .../java17/internal/DurationUtil.java | 9 +- .../buffer/DirectBufferStatisticsHandler.java | 17 +- .../classes/ClassesLoadedHandler.java | 20 +- .../ContainerConfigurationHandler.java | 2 +- .../cpu/ContextSwitchRateHandler.java | 2 +- .../java17/internal/cpu/LongLockHandler.java | 6 +- .../internal/cpu/OverallCpuLoadHandler.java | 9 +- .../G1GarbageCollectionHandler.java | 13 +- .../OldGarbageCollectionHandler.java | 12 +- .../YoungGarbageCollectionHandler.java | 12 +- .../memory/CodeCacheConfigurationHandler.java | 3 +- .../internal/memory/G1HeapSummaryHandler.java | 9 +- .../memory/MetaspaceSummaryHandler.java | 8 +- .../memory/ParallelHeapSummaryHandler.java | 12 +- .../internal/network/NetworkReadHandler.java | 4 +- .../internal/network/NetworkWriteHandler.java | 4 +- .../internal/threads/ThreadCountHandler.java | 2 +- .../java17/BufferMetricTest.java | 17 +- .../java17/G1GcMemoryMetricTest.java | 20 +- .../java17/JfrClassesLoadedCountTest.java | 12 +- .../runtimemetrics/java17/JfrCpuLockTest.java | 6 +- .../java17/JfrOverallCpuLoadHandlerTest.java | 9 +- .../java17/JfrThreadCountTest.java | 7 +- .../MetaspaceMemoryCommittedMetricTest.java | 3 +- .../MetaspaceMemoryLimitMetricTest.java | 3 +- .../MetaspaceMemoryUsageMetricTest.java | 2 +- .../java17/PsGcMemoryMetricTest.java | 21 +- .../java17/SerialGcMemoryMetricTest.java | 16 +- .../java17/internal/DurationUtilTest.java | 26 + .../javaagent/build.gradle.kts | 7 + .../runtimemetrics/java8/JarAnalyzer.java | 244 ++++ .../java8/JarAnalyzerInstaller.java | 38 + .../runtimemetrics/java8/JarDetails.java | 276 ++++ .../java8/Java8RuntimeMetricsInstaller.java | 25 +- .../java8/JarAnalyzerInstallerTest.java | 76 + .../runtime-telemetry-java8/library/README.md | 103 +- .../library/build.gradle.kts | 1 - .../runtimemetrics/java8/Classes.java | 26 +- .../runtimemetrics/java8/Cpu.java | 128 +- .../java8/GarbageCollector.java | 52 +- .../runtimemetrics/java8/MemoryPools.java | 93 +- .../runtimemetrics/java8/Threads.java | 100 +- .../java8/internal/CpuMethods.java | 103 ++ .../ExperimentalBufferPools.java} | 48 +- .../java8/internal/ExperimentalCpu.java | 72 + .../internal/ExperimentalMemoryPools.java | 86 ++ .../java8/internal/JmxRuntimeMetricsUtil.java | 9 + ...est.java => ClassesStableSemconvTest.java} | 14 +- .../java8/CpuStableSemconvTest.java | 78 ++ .../java8/GarbageCollectorTest.java | 23 +- ...java => MemoryPoolsStableSemconvTest.java} | 120 +- .../runtimemetrics/java8/ScopeUtil.java | 5 +- .../java8/ThreadsStableSemconvTest.java | 149 ++ .../runtimemetrics/java8/ThreadsTest.java | 62 - .../ExperimentalBufferPoolsTest.java} | 40 +- .../ExperimentalCpuTest.java} | 34 +- .../internal/ExperimentalMemoryPoolsTest.java | 88 ++ .../testing/build.gradle.kts | 42 + .../testapp/DummyApplication.java} | 5 +- .../runtimemetrics/java8/JarAnalyzerTest.java | 158 +++ instrumentation/rxjava/README.md | 4 +- .../v2_0/TracingAssemblyActivation.java | 4 +- .../test/java/BaseRxJava2WithSpanTest.java | 4 +- .../test/java/RxJava2SubscriptionTest.java | 2 +- .../library/src/test/java/RxJava2Test.java | 2 +- .../common/AbstractRxJava3WithSpanTest.java | 4 +- .../v3_0/TracingAssemblyActivation.java | 4 +- .../test/java/RxJava3SubscriptionTest.java | 2 +- .../library/src/test/java/RxJava3Test.java | 2 +- .../v3_1_1/TracingAssemblyActivation.java | 4 +- .../test/java/RxJava3SubscriptionTest.java | 2 +- .../library/src/test/java/RxJava3Test.java | 2 +- .../ScalaForkJoinPoolInstrumentation.java | 3 +- instrumentation/servlet/README.md | 10 +- ...et2HttpServletResponseInstrumentation.java | 4 +- .../v2_2/Servlet2ResponseSendAdvice.java | 2 +- .../servlet/v2_2/Servlet2Singletons.java | 2 +- .../v2_2/Servlet2SpanNameExtractor.java | 27 +- .../groovy/HttpServletResponseTest.groovy | 14 +- .../src/test/groovy/JettyServlet2Test.groovy | 10 +- .../v3_0/snippet/SnippetPrintWriterTest.java | 16 + .../SnippetServletOutputStreamTest.java | 19 + ...terSnippetInjectionWithOtherHeadStyle.html | 11 + ...oreSnippetInjectionWithOtherHeadStyle.html | 10 + .../servlet-3.0/javaagent/build.gradle.kts | 12 +- .../servlet/v3_0/Servlet3Accessor.java | 4 +- .../v3_0/Servlet3InstrumentationModule.java | 6 + .../v3_0/Servlet3ResponseSendAdvice.java | 2 +- .../servlet/v3_0/Servlet3Singletons.java | 2 +- ...rvlet3SnippetInjectingResponseWrapper.java | 18 +- .../test/groovy/AbstractServlet3Test.groovy | 26 +- .../groovy/HttpServletResponseTest.groovy | 14 +- .../groovy/JettyServletHandlerTest.groovy | 4 +- .../src/test/groovy/TomcatServlet3Test.groovy | 36 +- .../v5_0/snippet/SnippetPrintWriterTest.java | 16 + .../SnippetServletOutputStreamTest.java | 19 + ...terSnippetInjectionWithOtherHeadStyle.html | 11 + ...oreSnippetInjectionWithOtherHeadStyle.html | 10 + .../servlet-5.0/javaagent/build.gradle.kts | 11 +- .../servlet/v5_0/Servlet5Accessor.java | 14 +- .../servlet/v5_0/Servlet5Singletons.java | 2 +- .../v5_0/response/ResponseSendAdvice.java | 2 +- ...rvlet5SnippetInjectingResponseWrapper.java | 18 +- .../src/test/groovy/JettyServlet5Test.groovy | 3 + .../groovy/JettyServletHandlerTest.groovy | 6 +- .../src/test/groovy/TomcatServlet5Test.groovy | 40 +- .../jetty12-testing/build.gradle.kts | 24 + .../test/groovy/Jetty12Servlet5Test.groovy | 241 ++++ .../servlet-5.0/testing/build.gradle.kts | 10 + .../groovy/test}/AbstractServlet5Test.groovy | 28 +- .../src/main/groovy/test}/TestServlet5.groovy | 2 + .../v5_0}/RequestDispatcherServlet.java | 9 +- .../bootstrap/servlet/InjectionState.java | 9 +- .../servlet/BaseServletHelper.java | 23 +- .../servlet/ServletAccessor.java | 4 - .../ServletAdditionalAttributesExtractor.java | 19 +- .../servlet/ServletHelper.java | 2 +- .../servlet/ServletHttpAttributesGetter.java | 58 +- .../servlet/ServletInstrumenterBuilder.java | 51 +- .../servlet/ServletNetAttributesGetter.java | 88 -- .../ServletRequestParametersExtractor.java | 9 +- .../servlet/ServletSpanNameProvider.java | 4 +- .../HttpServletResponseAdviceHelper.java | 2 +- .../response/ResponseInstrumenterFactory.java | 8 +- .../servlet/javax/JavaxServletAccessor.java | 10 - .../sparkjava/SparkRouteUpdater.java | 7 +- .../sparkjava/SparkJavaBasedTest.java | 59 +- instrumentation/spring/README.md | 984 +------------ .../SpringBatchInstrumentationConfig.java | 6 +- .../spring/batch/v3_0/job/JobSingletons.java | 4 +- .../src/test/groovy/SpringBatchTest.groovy | 346 ----- .../v3_0/basic/JavaConfigBatchJobTest.java | 23 + .../v3_0/basic/JsrConfigBatchJobTest.java | 22 + .../batch/v3_0/basic/SpringBatchTest.java | 279 ++++ .../v3_0/basic/XmlConfigBatchJobTest.java | 21 + .../v3_0/jsr/CustomEventChunkListener.java | 26 + .../jsr/CustomEventItemProcessListener.java | 26 + .../v3_0/jsr/CustomEventItemReadListener.java | 26 + .../jsr/CustomEventItemWriteListener.java | 27 + .../v3_0/jsr/CustomEventJobListener.java | 21 + .../v3_0/jsr/CustomEventStepListener.java | 21 + .../batch/v3_0/jsr/SingleItemReader.java | 34 + .../spring/batch/v3_0/jsr/TestBatchlet.java | 36 + .../spring/batch/v3_0/jsr/TestDecider.java | 16 + .../batch/v3_0/jsr/TestItemProcessor.java | 16 + .../spring/batch/v3_0/jsr/TestItemReader.java | 44 + .../spring/batch/v3_0/jsr/TestItemWriter.java | 34 + .../v3_0/jsr/TestPartitionedItemReader.java | 80 ++ .../v3_0/runner/ApplicationConfigRunner.java | 62 + .../v3_0/runner/JavaxBatchConfigRunner.java | 34 + .../spring/batch/v3_0/runner/JobRunner.java | 19 + .../v3_0/runner/SpringBatchApplication.java | 274 ++++ .../springbatch/CustomEventChunkListener.java | 27 + .../CustomEventItemProcessListener.java | 26 + .../CustomEventItemReadListener.java | 26 + .../CustomEventItemWriteListener.java | 27 + .../springbatch/CustomEventJobListener.java | 22 + .../springbatch/CustomEventStepListener.java | 24 + .../v3_0/springbatch/SingleItemReader.java | 18 + .../batch/v3_0/springbatch/TestDecider.java | 18 + .../v3_0/springbatch/TestItemProcessor.java | 15 + .../v3_0/springbatch/TestItemReader.java | 16 + .../v3_0/springbatch/TestItemWriter.java | 21 + .../TestPartitionedItemReader.java | 37 + .../v3_0/springbatch/TestPartitioner.java | 22 + .../v3_0/springbatch/TestSyncItemReader.java | 28 + .../batch/v3_0/springbatch/TestTasklet.java | 23 + .../batch-jobs/customSpanEventsItemsJob.xml | 23 +- .../META-INF/batch-jobs/decisionJob.xml | 15 +- .../resources/META-INF/batch-jobs/flowJob.xml | 9 +- .../batch-jobs/itemsAndTaskletJob.xml | 15 +- .../META-INF/batch-jobs/partitionedJob.xml | 11 +- .../META-INF/batch-jobs/splitJob.xml | 9 +- .../META-INF/batch-jobs/taskletJob.xml | 9 +- .../src/test/resources/spring-batch.xml | 32 +- .../javaagent/build.gradle.kts | 5 + ...ringBootActuatorInstrumentationModule.java | 28 +- .../spring-boot-autoconfigure/README.md | 445 +----- .../build.gradle.kts | 171 ++- .../gradle.properties | 1 + .../autoconfigure/EnableOpenTelemetry.java | 20 - .../autoconfigure/MetricExportProperties.java | 25 - .../OpenTelemetryAutoConfiguration.java | 217 +-- .../autoconfigure/SamplerProperties.java | 33 - .../aspects/SdkExtensionWithSpanAspect.java | 32 - .../aspects/TraceAspectAutoConfiguration.java | 47 - .../aspects/TraceAspectProperties.java | 22 - .../JaegerSpanExporterAutoConfiguration.java | 44 - .../jaeger/JaegerSpanExporterProperties.java | 53 - .../logging/LoggingExporterProperties.java | 49 - ...oggingMetricExporterAutoConfiguration.java | 45 - .../LoggingSpanExporterAutoConfiguration.java | 46 - .../otlp/OtlpExporterProperties.java | 102 -- .../OtlpLoggerExporterAutoConfiguration.java | 52 - .../OtlpMetricExporterAutoConfiguration.java | 53 - .../OtlpSpanExporterAutoConfiguration.java | 58 - .../ZipkinSpanExporterAutoConfiguration.java | 41 - .../zipkin/ZipkinSpanExporterProperties.java | 38 - .../httpclients/HttpClientsProperties.java | 27 - .../RestTemplateAutoConfiguration.java | 37 - .../RestTemplateBeanPostProcessor.java | 48 - .../webclient/WebClientAutoConfiguration.java | 37 - .../webclient/WebClientBeanPostProcessor.java | 50 - .../ConditionalOnEnabledInstrumentation.java | 28 + .../InstrumentationPropertyEnabled.java | 37 + .../autoconfigure/internal/MapConverter.java | 34 + .../autoconfigure/internal/SdkEnabled.java | 22 + ...mentationAnnotationsAutoConfiguration.java | 36 + .../InstrumentationWithSpanAspect.java | 2 +- .../annotations}/JoinPointRequest.java | 25 +- .../JointPointCodeAttributesExtractor.java | 4 +- .../annotations}/WithSpanAspect.java | 7 +- ...spectParameterAttributeNamesExtractor.java | 16 +- .../jdbc/DataSourcePostProcessor.java | 53 + .../JdbcInstrumentationAutoConfiguration.java | 39 + ...ListenerContainerFactoryPostProcessor.java | 47 + ...KafkaInstrumentationAutoConfiguration.java | 28 +- .../LogbackAppenderApplicationListener.java | 76 + .../logging/LogbackAppenderInstaller.java | 156 +++ ...penTelemetryAppenderAutoConfiguration.java | 53 + .../MicrometerBridgeAutoConfiguration.java} | 18 +- ...lientInstrumentationAutoConfiguration.java | 42 + ...R2dbcInstrumentationAutoConfiguration.java | 37 + .../R2dbcInstrumentingPostProcessor.java | 58 + .../web/RestTemplateBeanPostProcessor.java | 38 + .../web/RestTemplateInstrumentation.java | 42 + ...ngWebInstrumentationAutoConfiguration.java | 49 + ...bfluxInstrumentationAutoConfiguration.java | 46 + .../webflux/WebClientBeanPostProcessor.java | 62 + ...bMvc5InstrumentationAutoConfiguration.java | 40 + .../properties/ConfigPropertiesBridge.java | 105 ++ .../properties/InstrumentationConfigUtil.java | 63 + .../properties}/OtelResourceProperties.java | 8 +- .../properties/OtlpExporterProperties.java | 52 + .../properties/PropagationProperties.java | 28 + .../properties/SpringConfigProperties.java | 168 +++ .../DistroVersionResourceProvider.java | 38 + .../resources/SpringResourceProvider.java | 47 + ...ListenerContainerFactoryPostProcessor.java | 35 - .../kafka/KafkaInstrumentationProperties.java | 22 - .../metrics/MicrometerShimProperties.java | 22 - .../CompositeTextMapPropagatorFactory.java | 98 -- .../PropagationAutoConfiguration.java | 49 - .../propagators/PropagationProperties.java | 25 - .../OtelResourceAutoConfiguration.java | 67 - .../SpringResourceConfigProperties.java | 75 - .../resources/SpringResourceProvider.java | 40 - .../webmvc/WebMvcFilterAutoConfiguration.java | 32 - .../WebMvcFilterAutoConfigurationSpring6.java | 32 - .../webmvc/WebMvcProperties.java | 27 - .../OpenTelemetryAnnotationsRuntimeHints.java | 24 + .../web/RestClientBeanPostProcessor.java | 66 + ...lientInstrumentationAutoConfiguration.java | 50 + ...bMvc6InstrumentationAutoConfiguration.java | 40 + ...itional-spring-configuration-metadata.json | 756 ++++++++++ .../resource-config.json | 9 + .../main/resources/META-INF/spring.factories | 27 +- .../resources/META-INF/spring/aot.factories | 2 + ...ot.autoconfigure.AutoConfiguration.imports | 25 +- .../OpenTelemetryAutoConfigurationTest.java | 157 +-- .../InstrumentationWithSpanAspectTest.java | 79 -- .../SdkExtensionWithSpanAspectTest.java | 78 -- .../TraceAspectAutoConfigurationTest.java | 65 - .../MetricExporterAutoConfigurationTest.java | 55 - .../SpanExporterAutoConfigurationTest.java | 54 - ...egerSpanExporterAutoConfigurationTest.java | 80 -- ...ngMetricExporterAutoConfigurationTest.java | 72 - ...gingSpanExporterAutoConfigurationTest.java | 78 -- .../OtlpLogExporterAutoConfigurationTest.java | 80 -- ...lpMetricExporterAutoConfigurationTest.java | 74 - ...OtlpSpanExporterAutoConfigurationTest.java | 77 - ...pkinSpanExporterAutoConfigurationTest.java | 77 - .../RestTemplateAutoConfigurationTest.java | 67 - .../RestTemplateBeanPostProcessorTest.java | 103 -- .../WebClientAutoConfigurationTest.java | 67 - .../WebClientBeanPostProcessorTest.java | 141 -- ...ationAnnotationsAutoConfigurationTest.java | 41 + .../InstrumentationWithSpanAspectTest.java} | 86 +- ...MicrometerBridgeAutoConfigurationTest.java | 58 + ...cInstrumentationAutoConfigurationTest.java | 63 + ...bInstrumentationAutoConfigurationTest.java | 101 ++ ...xInstrumentationAutoConfigurationTest.java | 59 + .../WebClientBeanPostProcessorTest.java | 118 ++ ...Instrumentation5AutoConfigurationTest.java | 56 + .../OtlpExporterPropertiesTest.java | 98 ++ .../DistroVersionResourceProviderTest.java | 51 + .../resources/SpringConfigPropertiesTest.java | 67 + .../resources/SpringResourceProviderTest.java | 80 ++ .../kafka/KafkaIntegrationTest.java | 165 --- .../MicrometerShimAutoConfigurationTest.java | 70 - .../PropagationAutoConfigurationTest.java | 114 -- .../PropagationPropertiesTest.java | 55 - .../OtelResourceAutoConfigurationTest.java | 55 - .../resources/OtelResourcePropertiesTest.java | 60 - .../SpringResourceConfigPropertiesTest.java | 46 - ...MvcFilterAutoConfigurationSpring6Test.java | 64 - .../WebMvcFilterAutoConfigurationTest.java | 63 - .../logging/LogbackAppenderTest.java | 113 ++ .../resources/logback-test.xml | 21 + .../logging/LogbackMissingTest.java | 26 + ...tInstrumentationAutoConfigurationTest.java | 87 ++ ...Instrumentation6AutoConfigurationTest.java | 56 + .../build.gradle.kts | 2 +- .../resources/TestBootInfClassesResource.java | 5 +- .../{library => javaagent}/build.gradle.kts | 0 .../SpringBootServiceNameDetector.java | 154 +- .../SpringBootServiceVersionDetector.java | 71 + .../spring/resources/SystemHelper.java | 70 + .../SpringBootServiceNameDetectorTest.java | 81 +- .../SpringBootServiceVersionDetectorTest.java | 61 + .../resources/META-INF/build-info.properties | 3 + .../src/test/resources/application-multi.yml | 3 +- .../src/test/resources/application.properties | 0 .../src/test/resources/application.yml | 5 +- .../src/test/resources/bootstrap-multi.yml | 9 + .../src/test/resources/bootstrap.properties | 1 + .../src/test/resources/bootstrap.yaml | 3 + .../src/test/resources/bootstrap.yml | 3 + .../src/test/resources/build-info.properties | 2 + .../spring/spring-cloud-gateway/README.md | 5 + .../javaagent/build.gradle.kts | 46 + .../v2_0/GatewayInstrumentationModule.java | 40 + .../gateway/v2_0/GatewaySingletons.java | 18 + .../v2_0/HandlerAdapterInstrumentation.java | 68 + .../gateway/v2_0/ServerWebExchangeHelper.java | 99 ++ .../gateway/v2_0/GatewayRouteMappingTest.java | 77 + .../gateway/v2_0/GatewayTestApplication.java | 45 + .../javaagent/src/test/resources/logback.xml | 20 + .../testing/build.gradle.kts | 45 + .../v2_2/Gateway22RouteMappingTest.java | 44 + .../v2_2/Gateway22TestApplication.java | 18 + .../src/test/resources/application.yml | 8 + .../testing/build.gradle.kts | 9 + .../common/AbstractRouteMappingTest.java | 66 + ...impleAsyncTaskExecutorInstrumentation.java | 3 +- ...eAsyncTaskExecutorInstrumentationTest.java | 14 +- .../javaagent/build.gradle.kts | 2 + .../v1_8/SpringDataInstrumentationModule.java | 7 +- .../data/v1_8/SpringDataSingletons.java | 8 +- .../spring/data/v1_8}/SpringJpaTest.java | 24 +- .../src/test/java/spring/jpa/JpaCustomer.java | 4 +- .../spring/jpa/JpaCustomerRepository.java | 3 + .../spring-data-3.0/testing/build.gradle.kts | 25 + .../data/v3_0/ReactiveSpringDataTest.java | 93 ++ .../spring/data/v3_0/repository/Customer.java | 75 + .../v3_0/repository/CustomerRepository.java | 10 + .../v3_0/repository/PersistenceConfig.java | 67 + .../spring/data/v3_0/SpringJpaTest.java} | 26 +- .../src/test/java/spring/jpa/JpaCustomer.java | 4 +- .../spring/jpa/JpaCustomerRepository.java | 3 + .../src/main/java/AbstractSpringJpaTest.java | 322 ----- .../spring/data/AbstractSpringJpaTest.java | 367 +++++ .../v4_1/SpringIntegrationSingletons.java | 6 +- .../test/groovy/ComplexPropagationTest.groovy | 13 - .../SpringCloudStreamProducerTest.groovy | 13 - .../groovy/SpringCloudStreamRabbitTest.groovy | 13 - .../SpringIntegrationAndRabbitTest.groovy | 118 -- .../SpringIntegrationTelemetryTest.groovy | 13 - .../v4_1/ComplexPropagationTest.java | 20 + .../v4_1/SpringCloudStreamProducerTest.java | 20 + .../v4_1/SpringCloudStreamRabbitTest.java | 20 + .../v4_1/SpringIntegrationAndRabbitTest.java | 155 ++ .../v4_1/SpringIntegrationTelemetryTest.java | 20 + .../library/build.gradle.kts | 4 + .../SpringIntegrationTelemetryBuilder.java | 10 +- .../v4_1/SpringMessagingAttributesGetter.java | 31 +- .../test/groovy/ComplexPropagationTest.groovy | 13 - .../GlobalInterceptorSpringConfig.groovy | 26 - ...rceptorWithProducerSpanSpringConfig.groovy | 24 - .../SpringCloudStreamProducerTest.groovy | 13 - .../groovy/SpringCloudStreamRabbitTest.groovy | 13 - .../SpringIntegrationTelemetryTest.groovy | 13 - .../v4_1/ComplexPropagationTest.java | 20 + .../v4_1/GlobalInterceptorSpringConfig.java | 28 + ...terceptorWithProducerSpanSpringConfig.java | 26 + .../v4_1/SpringCloudStreamProducerTest.java | 19 + .../v4_1/SpringCloudStreamRabbitTest.java | 19 + .../v4_1/SpringIntegrationTelemetryTest.java | 19 + .../AbstractComplexPropagationTest.groovy | 151 -- ...stractSpringCloudStreamProducerTest.groovy | 54 - ...AbstractSpringCloudStreamRabbitTest.groovy | 44 - ...bstractSpringIntegrationTracingTest.groovy | 284 ---- .../groovy/CapturingMessageHandler.groovy | 27 - .../WithRabbitProducerConsumerTrait.groovy | 103 -- .../v4_1/AbstractComplexPropagationTest.java | 162 +++ ...AbstractSpringCloudStreamProducerTest.java | 53 + .../AbstractSpringCloudStreamRabbitTest.java | 42 + .../AbstractSpringIntegrationTracingTest.java | 257 ++++ .../v4_1/CapturingMessageHandler.java | 29 + .../integration/v4_1/RabbitExtension.java | 135 ++ .../spring-jms-2.0/javaagent/build.gradle.kts | 3 + ...ssageListenerContainerInstrumentation.java | 53 + ...JmsDestinationAccessorInstrumentation.java | 59 + .../v2_0/SpringJmsInstrumentationModule.java | 7 +- ...ringJmsMessageListenerInstrumentation.java | 5 + .../spring/jms/v2_0/SpringJmsSingletons.java | 26 +- .../src/test/groovy/SpringListenerTest.groovy | 99 -- .../src/test/groovy/SpringTemplateTest.groovy | 182 --- .../groovy/listener/AbstractConfig.groovy | 71 - .../listener/AnnotatedListenerConfig.groovy | 14 - .../listener/ManualListenerConfig.groovy | 45 - .../test/groovy/listener/TestListener.groovy | 18 - .../spring/jms/v2_0/AbstractConfig.java | 81 ++ .../jms/v2_0/AnnotatedListenerConfig.java | 13 + .../spring/jms/v2_0/ManualListenerConfig.java | 51 + .../spring/jms/v2_0/SpringListenerTest.java | 68 + .../spring/jms/v2_0/SpringTemplateTest.java | 257 ++++ .../spring/jms/v2_0/TestListener.java | 28 + ...ingListenerSuppressReceiveSpansTest.groovy | 33 - ...pringListenerSuppressReceiveSpansTest.java | 45 + .../spring-jms-2.0/testing/build.gradle.kts | 7 + .../spring/jms/v2_0/AbstractJmsTest.java | 117 ++ .../spring-jms-6.0/javaagent/build.gradle.kts | 19 +- ...ssageListenerContainerInstrumentation.java | 53 + ...JmsDestinationAccessorInstrumentation.java | 59 + .../v6_0/SpringJmsInstrumentationModule.java | 7 +- ...ringJmsMessageListenerInstrumentation.java | 5 + .../spring/jms/v6_0/SpringJmsSingletons.java | 26 +- .../v6_0/AbstractSpringJmsListenerTest.java | 114 ++ .../jms/v6_0/SpringJmsListenerTest.java | 203 +-- ...pringListenerSuppressReceiveSpansTest.java | 53 + .../javaagent/build.gradle.kts | 1 + ...ssageListenerContainerInstrumentation.java | 30 +- .../v2_7/ListenerConsumerInstrumentation.java | 50 + .../kafka/v2_7/SpringKafkaSingletons.java | 28 +- .../spring/kafka/v2_7/SpringKafkaTest.java | 411 +++--- .../SpringKafkaNoReceiveTelemetryTest.java | 5 + .../spring-kafka-2.7/library/build.gradle.kts | 4 +- .../v2_7/InstrumentedBatchInterceptor.java | 52 +- .../v2_7/InstrumentedRecordInterceptor.java | 42 +- .../v2_7/SpringKafkaTelemetryBuilder.java | 7 + .../SpringKafkaErrorCauseExtractor.java | 8 +- .../SpringKafkaNoReceiveTelemetryTest.java | 5 + ...ractSpringKafkaNoReceiveTelemetryTest.java | 289 ++-- .../testing/AbstractSpringKafkaTest.java | 2 +- .../testing/BatchRecordListener.java | 4 +- .../opentelemetry/testing/ConsumerConfig.java | 4 - .../testing/ErrorHandlerSetter.java | 24 + .../testing/SingleRecordListener.java | 4 +- .../SpringRabbitMessageAttributesGetter.java | 31 +- .../rabbit/v1_0/SpringRabbitSingletons.java | 6 +- .../test/groovy/ContextPropagationTest.groovy | 199 --- .../rabbit/v1_0/SpringRabbitMqTest.java | 276 ++++ .../spring-rmi-4.0/javaagent/build.gradle.kts | 1 + .../v4_0/SpringRmiIgnoredTypesConfigurer.java | 22 + .../spring/rmi/v4_0/SpringRmiSingletons.java | 8 +- .../v4_0/client/ClientAttributesGetter.java | 2 +- .../v4_0/client/ClientInstrumentation.java | 4 +- .../v4_0/server/ServerAttributesGetter.java | 4 +- .../v4_0/server/ServerInstrumentation.java | 2 +- .../src/test/groovy/SpringRmiTest.groovy | 181 --- .../spring/rmi/v4_0/SpringRmiTest.java | 268 ++++ .../app/ejb/SpringRmiGreeterEjb.java | 13 + .../app/ejb/SpringRmiGreeterEjbRemote.java | 12 + .../src/test/resources/spring-rmi.xml | 13 + .../SpringSchedulingCodeAttributesGetter.java | 2 +- .../v3_1/SpringSchedulingSingletons.java | 8 +- .../test/groovy/SpringSchedulingTest.groovy | 163 --- .../scheduling/v3_1/SpringSchedulingTest.java | 178 +++ .../v3_1/spring/component}/IntervalTask.java | 2 + .../v3_1/spring/component}/OneTimeTask.java | 2 + .../v3_1/spring/component}/TaskWithError.java | 2 + .../v3_1/spring/component}/TriggerTask.java | 2 + .../config}/EnhancedClassTaskConfig.java | 2 + .../spring/config}/IntervalTaskConfig.java | 3 + .../v3_1/spring/config}/LambdaTaskConfig.java | 3 + .../spring/config}/OneTimeTaskConfig.java | 3 + .../spring/config}/TaskWithErrorConfig.java | 3 + .../spring/config}/TriggerTaskConfig.java | 3 + .../spring/service}/LambdaTaskConfigurer.java | 2 + .../javaagent/README.md | 14 + .../javaagent/build.gradle.kts | 38 + .../EnduserAttributesCapturerSingletons.java | 46 + .../servlet/HttpSecurityInstrumentation.java | 46 + ...ityConfigServletInstrumentationModule.java | 54 + .../ServerHttpSecurityInstrumentation.java | 46 + ...ityConfigWebFluxInstrumentationModule.java | 42 + .../HttpSecurityInstrumentationTest.java | 62 + ...ServerHttpSecurityInstrumentationTest.java | 38 + .../library/README.md | 77 + .../library/build.gradle.kts | 20 + .../v6_0/EnduserAttributesCapturer.java | 155 ++ ...duserAttributesCapturingServletFilter.java | 46 + ...duserAttributesHttpSecurityCustomizer.java | 37 + .../EnduserAttributesCapturingWebFilter.java | 51 + ...ttributesServerHttpSecurityCustomizer.java | 34 + .../v6_0/EnduserAttributesCapturerTest.java | 218 +++ ...rAttributesCapturingServletFilterTest.java | 82 ++ ...rAttributesHttpSecurityCustomizerTest.java | 61 + ...duserAttributesCapturingWebFilterTest.java | 80 ++ ...butesServerHttpSecurityCustomizerTest.java | 39 + .../v3_1/SpringWebHttpAttributesGetter.java | 13 +- .../v3_1/SpringWebNetAttributesGetter.java | 26 - .../spring/web/v3_1/SpringWebTelemetry.java | 2 +- .../web/v3_1/SpringWebTelemetryBuilder.java | 101 +- .../web/v3_1/internal/WebTelemetryUtil.java | 41 + .../v3_1/SpringWebInstrumentationTest.java | 8 +- .../spring/web/SpringRestTemplateTest.java | 25 +- .../javaagent/build.gradle.kts | 3 + .../webflux/v5_0/SpringWebfluxConfig.java | 4 +- .../webflux/v5_0/client/WebClientHelper.java | 20 +- .../webflux/v5_0/server/AdviceUtils.java | 30 +- .../DispatcherHandlerInstrumentation.java | 16 + .../server/HandlerAdapterInstrumentation.java | 13 +- .../server/HandlerCodeAttributesGetter.java | 2 +- .../webflux/v5_0/server/RouteOnSuccess.java | 6 +- .../v5_0/server/WebfluxSingletons.java | 4 +- .../v5_0/server/WebfluxSpanNameExtractor.java | 4 +- .../ContextHandlerInstrumentation.java | 12 +- .../HttpTrafficHandlerInstrumentation.java | 13 +- .../ReactorNettyInstrumentationModule.java | 10 +- .../SingleThreadedSpringWebfluxTest.groovy | 38 - .../SpringThreadedSpringWebfluxTest.java | 45 + .../v5_0/server/SpringWebfluxTest.java | 488 +++---- .../ControllerSpringWebFluxServerTest.java | 28 +- ...ayedControllerSpringWebFluxServerTest.java | 2 +- ...DelayedHandlerSpringWebFluxServerTest.java | 9 +- .../base/HandlerSpringWebFluxServerTest.java | 33 +- ...iateControllerSpringWebFluxServerTest.java | 2 +- ...mediateHandlerSpringWebFluxServerTest.java | 4 +- .../server/base/ServerTestController.java | 4 +- .../server/base/ServerTestRouteFactory.java | 4 +- .../server/base/SpringWebFluxServerTest.java | 6 +- .../v5_0}/server/base/package-info.java | 2 +- .../server/SpringWebFluxTestApplication.java | 25 +- .../src/test/java/server/TestController.java | 5 + .../testing/build.gradle.kts | 11 + .../IpcSingleThreadNettyCustomizer.java | 17 + .../spring-webflux-5.3/library/README.md | 5 + .../library/build.gradle.kts | 2 + .../v5_3/SpringWebfluxTelemetryBuilder.java | 166 ++- .../v5_3/TelemetryProducingWebFilter.java | 11 +- .../WebfluxServerHttpAttributesGetter.java | 19 +- .../WebfluxServerNetAttributesGetter.java | 42 - .../internal/SpringWebfluxBuilderUtil.java | 64 + .../SpringWebfluxTelemetryClientBuilder.java | 121 -- ...ClientExperimentalAttributesExtractor.java | 43 - .../WebClientHttpAttributesGetter.java | 31 +- .../WebClientNetAttributesGetter.java | 30 - ...pringWebfluxServerInstrumentationTest.java | 25 +- .../v5_3/TestWebfluxSpringBootApp.java | 31 +- .../testing/build.gradle.kts | 2 + ...pringWebfluxClientInstrumentationTest.java | 60 +- .../server/SingleThreadNettyCustomizer.java | 17 + .../javaagent/build.gradle.kts | 4 + .../v3_1/HandlerAdapterInstrumentation.java | 6 +- .../SpringWebMvcInstrumentationModule.java | 5 + .../v3_1/SpringWebMvcServerSpanNaming.java | 4 +- .../OpenTelemetryHandlerMappingFilter.java | 79 +- .../groovy/test/boot/SecurityConfig.groovy | 61 - .../test/boot/SpringBootBasedTest.groovy | 15 - .../test/filter/ServletFilterConfig.groovy | 90 -- .../test/filter/ServletFilterTest.groovy | 20 - .../webmvc/v3_1/boot/SecurityConfig.java | 67 + .../webmvc/v3_1/boot/SpringBootBasedTest.java | 62 + .../v3_1/filter/ServletFilterConfig.java | 85 ++ .../webmvc/v3_1/filter/ServletFilterTest.java | 50 + .../wildfly-testing/build.gradle.kts | 11 +- .../spring-webmvc-5.3/library/README.md | 4 +- .../library/build.gradle.kts | 3 +- .../SpringWebMvcHttpAttributesGetter.java | 50 +- .../v5_3/SpringWebMvcNetAttributesGetter.java | 75 - .../v5_3/SpringWebMvcTelemetryBuilder.java | 101 +- .../v5_3/WebMvcTelemetryProducingFilter.java | 106 +- .../v5_3/internal/SpringMvcBuilderUtil.java | 41 + .../webmvc/v5_3/TestWebSpringBootApp.java | 25 + .../webmvc/v5_3/WebMvcHttpServerTest.java | 27 +- .../javaagent/build.gradle.kts | 4 + .../v6_0/HandlerAdapterInstrumentation.java | 6 +- .../SpringWebMvcInstrumentationModule.java | 5 + .../v6_0/SpringWebMvcServerSpanNaming.java | 4 +- .../OpenTelemetryHandlerMappingFilter.java | 59 +- .../src/test/groovy/SecurityConfig.groovy | 57 - .../test/groovy/ServletFilterConfig.groovy | 88 -- .../src/test/groovy/ServletFilterTest.groovy | 17 - .../test/groovy/SpringBootBasedTest.groovy | 13 - .../webmvc/v6_0/boot/SecurityConfig.java | 57 + .../webmvc/v6_0/boot/SpringBootBasedTest.java | 110 ++ .../v6_0/filter/ServletFilterConfig.java | 85 ++ .../webmvc/v6_0/filter/ServletFilterTest.java | 101 ++ .../spring-webmvc-6.0/library/README.md | 4 +- .../library/build.gradle.kts | 13 + .../SpringWebMvcHttpAttributesGetter.java | 50 +- .../v6_0/SpringWebMvcNetAttributesGetter.java | 75 - .../v6_0/SpringWebMvcTelemetryBuilder.java | 100 +- .../v6_0/WebMvcTelemetryProducingFilter.java | 106 +- .../v6_0/internal/SpringMvcBuilderUtil.java | 41 + .../webmvc/v6_0/TestWebSpringBootApp.java | 25 + .../webmvc/v6_0/WebMvcHttpServerTest.java | 29 +- .../webmvc/HandlerSpanNameExtractor.java | 2 +- .../ModelAndViewAttributesExtractor.java | 4 +- .../testing/build.gradle.kts | 2 - .../boot/AbstractSpringBootBasedTest.groovy | 208 --- .../boot/SavingAuthenticationProvider.groovy | 75 - .../main/groovy/boot/TestController.groovy | 108 -- .../filter/AbstractServletFilterTest.groovy | 107 -- .../groovy/filter/FilteredAppConfig.groovy | 132 -- .../main/groovy/filter/TestController.groovy | 76 - .../boot/AbstractSpringBootBasedTest.java | 229 +++ .../spring/webmvc/boot/AppConfig.java} | 8 +- .../boot/SavingAuthenticationProvider.java | 34 + .../spring/webmvc/boot/TestController.java | 108 ++ .../spring/webmvc/boot/TestUserDetails.java | 58 + .../filter/AbstractServletFilterTest.java | 116 ++ .../webmvc/filter/FilteredAppConfig.java | 135 ++ .../spring/webmvc/filter/TestController.java | 74 + .../spring-ws-2.0/javaagent/build.gradle.kts | 1 + .../ws/v2_0/SpringWsCodeAttributesGetter.java | 2 +- .../v2_0/SpringWsInstrumentationModule.java | 8 + .../spring/ws/v2_0/SpringWsSingletons.java | 4 +- .../groovy/test/boot/HelloEndpoint.groovy | 50 - .../test/groovy/test/boot/SpringWsTest.groovy | 160 --- .../groovy/test/boot/WebServiceConfig.groovy | 46 - .../spring/ws/v2_0/AppConfig.java} | 10 +- .../spring/ws/v2_0/HelloEndpoint.java | 52 + .../spring/ws/v2_0/SpringWsTest.java | 179 +++ .../spring/ws/v2_0/WebServiceConfig.java | 47 + .../jaeger-spring-boot-starter/README.md | 34 - .../build.gradle.kts | 15 - .../starters/spring-boot-starter/README.md | 39 +- .../spring-boot-starter/build.gradle.kts | 16 +- .../spring-boot-starter/gradle.properties | 1 + .../zipkin-spring-boot-starter/README.md | 35 +- .../build.gradle.kts | 3 +- instrumentation/spymemcached-2.12/README.md | 4 +- .../spymemcached/CompletionListener.java | 4 +- .../SpymemcachedAttributesGetter.java | 2 +- .../spymemcached/SpymemcachedSingletons.java | 4 +- .../src/test/groovy/SpymemcachedTest.groovy | 13 +- .../struts-2.3/javaagent/build.gradle.kts | 4 + .../ActionInvocationInstrumentation.java | 6 +- .../struts2/StrutsCodeAttributesGetter.java | 2 +- .../struts2/StrutsServerSpanNaming.java | 4 +- .../struts2/StrutsSingletons.java | 4 +- .../test/groovy/Struts2ActionSpanTest.groovy | 168 --- .../struts2}/GreetingAction.java | 2 +- .../struts2}/GreetingServlet.java | 2 +- .../struts2/Struts2ActionSpanTest.java | 173 +++ .../javaagent/src/test/resources/struts.xml | 18 +- .../tapestry-5.4/javaagent/build.gradle.kts | 4 + ...itializeActivePageNameInstrumentation.java | 8 +- .../tapestry/TapestryServerSpanNaming.java | 4 +- .../src/test/groovy/TapestryTest.groovy | 168 --- .../tapestry/TapestryTest.java | 153 ++ .../tomcat-10.0/javaagent/build.gradle.kts | 17 +- .../v10_0/Tomcat10InstrumentationModule.java | 8 +- .../tomcat/v10_0/AsyncServlet.groovy | 83 -- .../tomcat/v10_0/TomcatAsyncTest.groovy | 133 -- .../tomcat/v10_0/TomcatHandlerTest.groovy | 120 -- .../tomcat/v10_0/AsyncServlet.java | 82 ++ .../tomcat/v10_0/ErrorHandlerValve.java | 26 + .../tomcat/v10_0/TestServlet.java | 2 +- .../tomcat/v10_0/TomcatAsyncTest.java | 129 ++ .../tomcat/v10_0/TomcatHandlerTest.java | 126 ++ .../tomcat-7.0/javaagent/build.gradle.kts | 15 +- .../v7_0/Tomcat7InstrumentationModule.java | 4 +- .../tomcat/v7_0/AsyncServlet.groovy | 82 -- .../tomcat/v7_0/ThreadPoolExecutorTest.groovy | 90 -- .../tomcat/v7_0/TomcatAsyncTest.groovy | 133 -- .../tomcat/v7_0/TomcatHandlerTest.groovy | 119 -- .../tomcat/v7_0/AsyncServlet.java | 82 ++ .../tomcat/v7_0/ErrorHandlerValve.java | 26 + .../tomcat/v7_0/TestServlet.java | 2 +- .../tomcat/v7_0/ThreadPoolExecutorTest.java | 86 ++ .../tomcat/v7_0/TomcatAsyncTest.java | 124 ++ .../tomcat/v7_0/TomcatHandlerTest.java | 126 ++ .../common/TomcatHttpAttributesGetter.java | 65 +- .../common/TomcatInstrumenterFactory.java | 70 +- .../common/TomcatNetAttributesGetter.java | 76 - .../jdbc/TomcatConnectionPoolMetrics.java | 2 +- .../jdbc/TomcatJdbcInstrumentationTest.java | 2 +- instrumentation/twilio-6.6/README.md | 4 +- .../twilio/TwilioSingletons.java | 6 +- .../test/groovy/test/TwilioClientTest.groovy | 64 +- .../undertow-1.4/javaagent/build.gradle.kts | 4 + .../undertow/HandlerInstrumentation.java | 4 +- ... HttpServerConnectionInstrumentation.java} | 30 +- .../HttpServerExchangeInstrumentation.java | 3 +- .../undertow/UndertowHelper.java | 9 + .../UndertowHttpAttributesGetter.java | 37 +- .../UndertowInstrumentationModule.java | 2 +- .../undertow/UndertowNetAttributesGetter.java | 63 - .../undertow/UndertowSingletons.java | 43 +- .../groovy/UndertowHttp2ServerTest.groovy | 18 + .../groovy/UndertowServerDispatchTest.groovy | 4 +- .../src/test/groovy/UndertowServerTest.groovy | 202 +-- .../vaadin-14.2/javaagent/build.gradle.kts | 10 +- .../ClientCallableCodeAttributesGetter.java | 2 +- .../vaadin/VaadinHandlerRequest.java | 2 +- .../instrumentation/vaadin/VaadinHelper.java | 16 +- .../vaadin/VaadinServiceRequest.java | 2 +- .../vaadin/VaadinSingletons.java | 6 +- .../instrumentation/vaadin/Vaadin16Test.java | 15 +- .../vaadin/VaadinLatestTest.java | 15 +- .../vaadin/AbstractVaadin14Test.java | 9 +- .../vaadin/AbstractVaadinTest.java | 11 +- .../src/main/java/test/vaadin/MainView.java | 13 +- .../javaagent/build.gradle.kts | 2 +- .../client/Vertx3HttpAttributesGetter.java | 25 +- .../client/Vertx3NetAttributesGetter.java | 50 - .../v3_0/client/VertxClientSingletons.java | 4 +- .../groovy/client/VertxHttpClientTest.groovy | 105 -- .../v3_0/client/VertxHttpClientTest.java | 108 ++ .../client/Vertx4HttpAttributesGetter.java | 62 + .../client/Vertx4NetAttributesGetter.java | 86 -- .../v4_0/client/VertxClientSingletons.java | 4 +- .../AbstractVertxHttpAttributesGetter.java | 2 +- .../VertxClientInstrumenterFactory.java | 33 +- .../kafka/v3_6/VertxKafkaSingletons.java | 4 +- .../v3_6/BatchRecordsVertxKafkaTest.java | 6 +- .../v3_6/SingleRecordVertxKafkaTest.java | 4 +- ...veTelemetryBatchRecordsVertxKafkaTest.java | 6 +- ...veTelemetrySingleRecordVertxKafkaTest.java | 4 +- .../kafka/v3_6/AbstractVertxKafkaTest.java | 69 +- .../javaagent/build.gradle.kts | 27 + .../redis/CommandImplInstrumentation.java | 38 + ...edisConnectionProviderInstrumentation.java | 68 + ...isStandaloneConnectionInstrumentation.java | 103 ++ .../VertxRedisClientAttributesExtractor.java | 34 + .../VertxRedisClientAttributesGetter.java | 54 + ...VertxRedisClientInstrumentationModule.java | 42 + .../VertxRedisClientNetAttributesGetter.java | 41 + .../v4_0/redis/VertxRedisClientRequest.java | 65 + .../redis/VertxRedisClientSingletons.java | 109 ++ .../vertx/redis/client/impl/RequestUtil.java | 22 + .../v4_0/redis/VertxRedisClientTest.java | 214 +++ .../javaagent/build.gradle.kts | 14 +- .../VertxReactivePropagationTest.groovy | 94 +- .../VertxRxCircuitBreakerWebClientTest.groovy | 5 - .../groovy/client/VertxRxWebClientTest.groovy | 5 - .../server/VertxRxHttpServerTest.groovy | 11 +- .../VertxReactivePropagationTest.groovy | 93 +- .../VertxRxCircuitBreakerWebClientTest.groovy | 22 +- .../groovy/client/VertxRxWebClientTest.groovy | 22 +- .../server/VertxRxHttpServerTest.groovy | 11 +- .../javaagent/build.gradle.kts | 25 +- .../vertx/v4_0/sql/HandlerWrapper.java | 35 + .../vertx/v4_0/sql/PoolInstrumentation.java | 43 +- .../sql/QueryExecutorInstrumentation.java | 7 +- .../QueryResultBuilderInstrumentation.java | 16 +- .../sql/SqlClientBaseInstrumentation.java | 1 + .../sql/TransactionImplInstrumentation.java | 39 + .../sql/VertxSqlClientAttributesGetter.java | 2 +- .../VertxSqlClientInstrumentationModule.java | 13 +- .../VertxSqlClientNetAttributesGetter.java | 10 +- .../v4_0/sql/VertxSqlClientSingletons.java | 92 +- .../vertx/v4_0/sql/VertxSqlClientTest.java | 218 ++- .../vertx-web-3.0/javaagent/build.gradle.kts | 14 +- .../server/VertxLatestHttpServerTest.groovy | 1 + .../java/server/VertxLatestWebServer.java | 4 +- .../vertx/RoutingContextHandlerWrapper.java | 23 +- .../java/server/VertxWebServer.java | 4 +- .../server/AbstractVertxHttpServerTest.groovy | 19 +- .../v11_0/ConnectionPoolMetrics.java | 2 +- .../common-testing/build.gradle.kts | 9 + .../src/main/java/hello/ExceptionPage.java | 16 + .../src/main/java/hello/HelloApplication.java | 29 + .../src/main/java/hello/HelloPage.java | 17 + .../wicket/AbstractWicketTest.java | 73 + .../src/main}/resources/hello/HelloPage.html | 6 +- .../wicket-8.0/javaagent/build.gradle.kts | 8 - ...RequestHandlerExecutorInstrumentation.java | 6 +- .../wicket/WicketServerSpanNaming.java | 4 +- .../src/test/groovy/WicketTest.groovy | 102 -- .../test/groovy/hello/ExceptionPage.groovy | 14 - .../test/groovy/hello/HelloApplication.groovy | 29 - .../src/test/groovy/hello/HelloPage.groovy | 15 - .../wicket10-testing/build.gradle.kts | 20 + .../instrumentation/wicket/WicketTest.java | 49 + .../wicket8-testing/build.gradle.kts | 27 + .../instrumentation/wicket/WicketTest.java | 50 + instrumentation/xxl-job/README.md | 5 + .../xxl-job-1.9.2/javaagent/build.gradle.kts | 36 + .../v1_9_2/GlueJobHandlerInstrumentation.java | 67 + .../ScriptJobHandlerInstrumentation.java | 69 + .../SimpleJobHandlerInstrumentation.java | 76 + .../v1_9_2/XxlJobInstrumentationModule.java | 38 + .../xxljob/v1_9_2/XxlJobSingletons.java | 42 + .../xxljob/v1_9_2/XxlJobTest.java | 69 + .../xxl-job-2.1.2/javaagent/build.gradle.kts | 34 + .../v2_1_2/GlueJobHandlerInstrumentation.java | 65 + .../MethodJobHandlerInstrumentation.java | 66 + .../ScriptJobHandlerInstrumentation.java | 66 + .../SimpleJobHandlerInstrumentation.java | 74 + .../v2_1_2/XxlJobInstrumentationModule.java | 40 + .../xxljob/v2_1_2/XxlJobSingletons.java | 34 + .../xxljob/v2_1_2/XxlJobTest.java | 81 ++ .../xxl-job-2.3.0/javaagent/build.gradle.kts | 31 + .../v2_3_0/GlueJobHandlerInstrumentation.java | 65 + .../MethodJobHandlerInstrumentation.java | 65 + .../ScriptJobHandlerInstrumentation.java | 66 + .../SimpleJobHandlerInstrumentation.java | 73 + .../v2_3_0/XxlJobInstrumentationModule.java | 38 + .../xxljob/v2_3_0/XxlJobSingletons.java | 38 + .../v2_3_0/CustomizedFailedHandler.java | 17 + .../v2_3_0/SimpleCustomizedHandler.java | 17 + .../xxljob/v2_3_0/XxlJobTest.java | 79 ++ .../xxl-job-common/javaagent/build.gradle.kts | 6 + .../common/XxlJobCodeAttributesGetter.java | 33 + .../xxljob/common/XxlJobConstants.java | 17 + .../XxlJobExperimentalAttributeExtractor.java | 44 + .../xxljob/common/XxlJobHelper.java | 57 + .../common/XxlJobInstrumenterFactory.java | 45 + .../xxljob/common/XxlJobProcessRequest.java | 76 + .../common/XxlJobSpanNameExtractor.java | 31 + .../xxl-job-common/testing/build.gradle.kts | 11 + .../xxljob/AbstractXxlJobTest.java | 170 +++ .../xxljob/CustomizedFailedHandler.java | 17 + .../xxljob/ReflectiveMethodsFactory.java | 60 + .../xxljob/SimpleCustomizedHandler.java | 17 + .../xxljob/XxlJobTestingConstants.java | 46 + .../zio/zio-2.0/javaagent/build.gradle.kts | 4 + .../v2_0/ZioRuntimeInstrumentationTest.scala | 2 - .../javaagent/bootstrap/AgentInitializer.java | 28 +- .../javaagent/bootstrap/ExceptionLogger.java | 8 + .../bootstrap/IndyBootstrapDispatcher.java | 80 ++ .../IndyBootstrapDispatcherTest.java | 57 + javaagent-extension-api/build.gradle.kts | 1 + .../bootstrap/internal/AgentCommonConfig.java | 22 + .../internal/AgentInstrumentationConfig.java | 47 + .../bootstrap/internal/CommonConfig.java | 71 - .../ConfiguredResourceAttributesHolder.java | 48 + .../internal/DeprecatedConfigProperties.java | 20 +- .../internal/EmptyInstrumentationConfig.java | 3 +- .../internal/ExperimentalConfig.java | 11 +- .../JavaagentHttpClientInstrumenters.java | 64 + .../InstrumentationModule.java | 28 + .../instrumentation/internal/AsmApi.java | 18 + .../ExperimentalInstrumentationModule.java | 64 + .../internal/injection/ClassInjector.java | 39 + .../internal/injection/InjectionMode.java | 32 + .../injection/ProxyInjectionBuilder.java | 15 + .../ClassLoaderHasClassesNamedMatcher.java | 15 - .../application/ApplicationLoggerFactory.java | 1 + .../logging/application/InMemoryLogStore.java | 16 + .../ApplicationLoggerFactoryTest.java | 1 + .../build.gradle.kts | 3 +- .../simple/Slf4jSimpleLoggingCustomizer.java | 2 + javaagent-tooling/build.gradle.kts | 28 +- .../jdk18-testing/build.gradle.kts | 11 + .../java/testing/TestResourceProvider.java | 30 + .../inetaddress/InetAddressResolverTest.java | 28 + .../inetaddress/TestAddressResolver.java | 39 + .../TestAddressResolverProvider.java | 22 + .../java.net.spi.InetAddressResolverProvider | 1 + .../AddThreadDetailsSpanProcessor.java | 6 +- .../javaagent/tooling/AgentInstaller.java | 23 +- .../javaagent/tooling/AgentStarterImpl.java | 69 +- ...ava => DistroVersionResourceProvider.java} | 16 +- .../tooling/ExtensionClassLoader.java | 15 +- .../tooling/OpenTelemetryInstaller.java | 7 +- .../tooling/RemappingUrlConnection.java | 7 +- .../tooling/bytebuddy/ExceptionHandlers.java | 3 + .../javaagent/tooling/config/AgentConfig.java | 14 +- .../config/ConfigPropertiesBridge.java | 4 +- .../config/MethodsConfigurationParser.java | 18 +- .../OtlpProtocolPropertiesSupplier.java | 27 + .../FieldBackedImplementationInstaller.java | 12 +- ...opVirtualFieldImplementationInstaller.java | 6 + .../tooling/field/RealFieldInjector.java | 3 +- ...ntimeFieldBasedImplementationSupplier.java | 12 +- .../field/VirtualFieldFindRewriter.java | 5 +- .../VirtualFieldImplementationInstaller.java | 9 +- .../VirtualFieldImplementationsGenerator.java | 3 +- ...ditionalLibraryIgnoredTypesConfigurer.java | 34 +- .../CommonLibraryIgnoredTypesConfigurer.java | 25 + .../ignore/GlobalIgnoredTypesConfigurer.java | 6 +- .../tooling/ignore/IgnoredTypesMatcher.java | 4 +- .../UserExcludedClassLoadersConfigurer.java | 34 + .../InstrumentationModuleInstaller.java | 174 ++- .../instrumentation/MuzzleMatcher.java | 15 +- .../instrumentation/TypeTransformerImpl.java | 10 +- .../indy/AdviceSignatureEraser.java | 106 ++ .../indy/AdviceTransformer.java | 897 ++++++++++++ .../indy/ClassInjectorImpl.java | 72 + ...DynamicallyTypedAssignReturnedFactory.java | 136 ++ .../instrumentation/indy/IndyBootstrap.java | 299 ++++ .../indy/IndyModuleRegistry.java | 128 ++ .../indy/IndyProxyFactory.java | 181 +++ .../indy/IndyTypeTransformerImpl.java | 125 ++ .../InstrumentationModuleClassLoader.java | 387 +++++ .../instrumentation/indy/LookupExposer.java | 27 + .../indy/OriginalDescriptor.java | 10 + .../indy/PatchByteCodeVersionTransformer.java | 103 ++ .../tooling/util/ClassLoaderMap.java | 89 ++ .../tooling/util/ClassLoaderValue.java | 31 + .../autoconfigure/SdkAutoconfigureAccess.java | 16 + .../agent/builder/AgentBuilderUtil.java | 10 + .../javaagent/test/HelperInjectionTest.groovy | 7 +- .../AddThreadDetailsSpanProcessorTest.groovy | 6 +- .../tooling/OpenTelemetryInstallerTest.groovy | 15 +- .../MethodsConfigurationParserTest.groovy | 4 +- .../tooling/config/AgentConfigTest.java | 44 +- .../ConfigurationPropertiesSupplierTest.java | 8 +- .../OtlpProtocolPropertiesSupplierTest.java | 53 + ...serExcludedClassLoadersConfigurerTest.java | 55 + ...micallyTypedAssignReturnedFactoryTest.java | 113 ++ .../indy/IndyProxyFactoryTest.java | 330 +++++ .../InstrumentationModuleClassLoaderTest.java | 294 ++++ .../instrumentation/indy/dummies/Bar.java | 8 + .../indy/dummies/DummyAnnotation.java | 14 + .../instrumentation/indy/dummies/Foo.java | 14 + .../tooling/util/ClassLoaderValueTest.java | 66 + ...re.spi.AutoConfigurationCustomizerProvider | 1 + .../indy/ComputeFramesAsmVisitorWrapper.java | 36 + .../instrumentation/indy/OldBytecode.java | 150 ++ .../indy/PatchBytecodeVersionTest.java | 172 +++ .../instrumentation/indy/PatchTestAdvice.java | 20 + javaagent/build.gradle.kts | 87 +- .../classloading/ClassLoadingTest.groovy | 5 +- .../javaagent/ResourceProviderTest.java | 43 + .../opentelemetry/javaagent/util/GcUtils.java | 22 - .../META-INF/LICENSE | 0 .../META-INF/NOTICE | 0 .../META-INF/LICENSE | 0 .../META-INF/NOTICE | 21 + .../META-INF/LICENSE | 5 +- .../META-INF/NOTICE | 0 .../META-INF/LICENSE | 5 +- .../META-INF/NOTICE | 21 + .../META-INF/LICENSE | 2 +- .../META-INF/NOTICE | 4 + licenses/licenses.md | 286 ++-- .../okhttp3/internal/publicsuffix/NOTICE | 0 .../META-INF/LICENSE.txt | 2 +- .../META-INF/LICENSE.txt | 2 +- .../META-INF/LICENSE | 0 .../META-INF/LICENSE | 211 +++ .../META-INF/LICENSE | 211 +++ muzzle/build.gradle.kts | 1 + .../javaagent/tooling/ByteArrayUrl.java | 104 ++ .../javaagent/tooling/BytecodeWithUrl.java | 195 +++ .../tooling/HelperClassDefinition.java | 53 + .../javaagent/tooling/HelperInjector.java | 293 ++-- .../muzzle/AgentCachingPoolStrategy.java | 4 +- .../ReferenceCollectingClassVisitor.java | 12 +- .../tooling/muzzle/ReferenceMatcher.java | 10 +- .../generation/MuzzleCodeGenerator.java | 3 +- .../javaagent/tooling/ByteArrayUrlTest.java | 30 + .../muzzle/MuzzleWeakReferenceTestUtil.java | 7 +- .../muzzle/ReferenceCollectorTest.java | 22 +- muzzle/src/test/java/muzzle/TestClasses.java | 1 + .../java/muzzle/other/OtherTestClasses.java | 26 + .../build.gradle.kts | 104 +- .../build.gradle.kts | 3 +- .../build.gradle.kts | 3 +- .../build.gradle.kts | 3 +- sdk-autoconfigure-support/build.gradle.kts | 13 + .../ResourceProviderPropertiesCustomizer.java | 100 ++ ...re.spi.AutoConfigurationCustomizerProvider | 1 + ...ourceProviderPropertiesCustomizerTest.java | 140 ++ .../autoconfigure/SdkAutoconfigureAccess.java | 16 + ...try.sdk.autoconfigure.spi.ResourceProvider | 1 + settings.gradle.kts | 986 +++++++------ .../spring-boot-2/build.gradle.kts | 36 + ...OtelSpringStarterSmokeTestApplication.java | 19 + .../KafkaSpringStarterSmokeTest.java | 11 + .../MongoSpringStarterSmokeTest.java | 11 + .../smoketest/OtelSpringStarterSmokeTest.java | 21 + .../spring-boot-3.2/build.gradle.kts | 77 + ...OtelSpringStarterSmokeTestApplication.java | 21 + .../spring/smoketest/RuntimeHints.java | 31 + .../smoketest/OtelSpringStarterSmokeTest.java | 58 + .../spring-boot-3/build.gradle.kts | 80 ++ ...OtelSpringStarterSmokeTestApplication.java | 21 + .../spring/smoketest/RuntimeHints.java | 29 + ...alVmNativeKafkaSpringStarterSmokeTest.java | 32 + ...VmNativeMongodbSpringStarterSmokeTest.java | 29 + .../KafkaSpringStarterSmokeTest.java | 11 + .../MongoSpringStarterSmokeTest.java | 11 + .../OtelSpringStarterDisabledSmokeTest.java | 26 + .../smoketest/OtelSpringStarterSmokeTest.java | 21 + ...elSpringStarterUserDataSourceBeanTest.java | 14 + .../spring-boot-common/build.gradle.kts | 32 + ...bstractJvmKafkaSpringStarterSmokeTest.java | 77 + ...tractJvmMongodbSpringStarterSmokeTest.java | 65 + .../AbstractKafkaSpringStarterSmokeTest.java | 128 ++ ...AbstractMongodbSpringStarterSmokeTest.java | 38 + .../AbstractOtelSpringStarterSmokeTest.java | 240 ++++ .../spring/smoketest/DatasourceConfig.java | 28 + .../OtelSpringStarterSmokeTestController.java | 38 + .../spring/smoketest/SpringComponent.java | 18 + .../META-INF/native-image/reflect-config.json | 1248 +++++++++++++++++ .../src/main/resources/application.yaml | 30 + .../spring-boot-reactive-2/build.gradle.kts | 33 + .../OtelReactiveSpringStarterSmokeTest.java | 8 + .../spring-boot-reactive-3/build.gradle.kts | 78 ++ .../OtelReactiveSpringStarterSmokeTest.java | 8 + .../build.gradle.kts | 22 + ...actOtelReactiveSpringStarterSmokeTest.java | 71 + ...tiveSpringStarterSmokeTestApplication.java | 19 + ...ctiveSpringStarterSmokeTestController.java | 28 + .../spring/smoketest/Player.java | 34 + .../spring/smoketest/PlayerRepository.java | 10 + .../src/main/resources/application.yaml | 17 + .../src/main/resources/schema.sql | 1 + .../spring-smoke-testing/build.gradle.kts | 21 + .../AbstractSpringStarterSmokeTest.java | 71 + .../spring/smoketest/HttpSpanDataAssert.java | 62 + .../smoketest/RequiresDockerCompose.java | 25 + .../SpringSmokeOtelConfiguration.java | 94 ++ .../smoketest/SpringSmokeTestRunner.java | 78 ++ smoke-tests/build.gradle.kts | 6 +- smoke-tests/images/early-jdk8/Dockerfile | 16 + .../images/early-jdk8/build.gradle.kts | 35 + .../images/fake-backend/build.gradle.kts | 9 +- smoke-tests/images/grpc/build.gradle.kts | 6 +- smoke-tests/images/play/build.gradle.kts | 4 +- smoke-tests/images/quarkus/build.gradle.kts | 4 +- smoke-tests/images/servlet/build.gradle.kts | 70 +- .../matrix/AsyncGreetingServlet.java | 28 +- .../matrix/AsyncGreetingServlet.java | 29 +- .../images/servlet/src/jetty.dockerfile | 1 + .../servlet/src/jetty.windows.dockerfile | 3 +- .../images/servlet/src/wildfly.dockerfile | 2 +- .../images/spring-boot/build.gradle.kts | 11 +- .../smoketest/AppServerTest.groovy | 70 +- .../smoketest/CrashEarlyJdk8Test.groovy | 2 +- .../smoketest/GrpcSmokeTest.groovy | 2 +- .../smoketest/JettyJpmsSmokeTest.groovy | 9 +- .../smoketest/JettySmokeTest.groovy | 90 +- .../LibertyServletOnlySmokeTest.groovy | 2 +- .../smoketest/LibertySmokeTest.groovy | 72 +- .../smoketest/LogsSmokeTest.groovy | 5 - .../smoketest/PayaraSmokeTest.groovy | 28 +- .../smoketest/PlaySmokeTest.groovy | 4 +- .../smoketest/PrometheusSmokeTest.groovy | 4 +- .../smoketest/PropagationTest.groovy | 8 - .../smoketest/QuarkusSmokeTest.groovy | 23 +- .../smoketest/SpringBootSmokeTest.groovy | 29 +- .../SpringBootWithSamplingSmokeTest.groovy | 1 - .../smoketest/TomcatSmokeTest.groovy | 92 +- .../smoketest/TomeeSmokeTest.groovy | 58 +- .../smoketest/WildflySmokeTest.groovy | 98 +- .../AbstractTestContainerManager.java | 14 +- .../windows/WindowsTestContainerManager.java | 2 + testing-common/build.gradle.kts | 5 +- .../integration-tests/build.gradle.kts | 20 +- ...IbmResourceLevelInstrumentationModule.java | 28 + .../IndyResourceLevelInstrumentation.java | 34 + ...bmResourceLevelInstrumentationModule.java} | 8 +- ...> InlineResourceLevelInstrumentation.java} | 2 +- .../java/field/VirtualFieldTestHelper.java | 36 + ...VirtualFieldTestInstrumentationModule.java | 59 + .../src/main/java/indy/ProxyMe.java | 21 + .../IndyInstrumentationTestModule.java | 210 +++ ...entInstrumentationSpecificationTest.groovy | 16 +- .../FieldBackedImplementationTest.groovy | 3 +- .../src/test/java/field/VirtualFieldTest.java | 22 + .../java/indy/IndyInstrumentationTest.java | 170 +++ .../main/java/library/MyProxySuperclass.java | 12 + .../java/library/VirtualFieldTestClass.java | 5 +- .../test/asserts/SpanAssert.groovy | 18 +- .../test/base/HttpClientTest.groovy | 53 +- .../test/base/HttpServerTest.groovy | 85 +- .../instrumentation/test/utils/GcUtils.java | 29 +- .../instrumentation/test/utils/PortUtils.java | 1 + .../testing/AgentTestRunner.java | 2 + .../testing/InstrumentationTestRunner.java | 71 +- .../testing/LibraryTestRunner.java | 14 +- .../testing/TestInstrumenters.java | 31 +- .../junit/InstrumentationExtension.java | 54 +- .../junit/http/AbstractHttpClientTest.java | 351 +++-- .../junit/http/AbstractHttpServerTest.java | 297 ++-- .../http/AbstractHttpServerUsingTest.java | 22 +- .../junit/http/HttpClientTestOptions.java | 83 +- .../junit/http/HttpClientTestServer.java | 27 +- .../junit/http/HttpServerTestOptions.java | 59 +- .../testing/junit/http/ServerEndpoint.java | 8 +- .../testing/util/TelemetryDataUtil.java | 18 +- .../common/TestAgentListenerAccess.java | 5 + .../LibraryInstrumentationExtensionTest.java | 9 +- testing/agent-exporter/build.gradle.kts | 2 +- .../exporter/AgentTestingCustomizer.java | 30 +- ...CapturedHttpHeadersTestConfigSupplier.java | 8 +- testing/agent-for-testing/build.gradle.kts | 1 - .../build.gradle.kts | 6 +- version.gradle.kts | 4 +- 3535 files changed, 129932 insertions(+), 61278 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/bug_report.yml create mode 100644 .github/ISSUE_TEMPLATE/config.yml delete mode 100644 .github/ISSUE_TEMPLATE/feature_request.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.yml delete mode 100644 .github/dependabot.yml create mode 100644 .github/graal-native-docker-compose.yaml create mode 100644 .github/labeler.yml create mode 100644 .github/renovate.json5 create mode 100755 .github/scripts/gha-free-disk-space.sh create mode 100644 .java-version create mode 100644 ISSUES.md create mode 100644 benchmark-overhead/src/test/java/io/opentelemetry/util/ContainerNamingConvention.java create mode 100644 benchmark-overhead/src/test/java/io/opentelemetry/util/LocalNamingConvention.java delete mode 100644 conventions/src/main/kotlin/otel.protobuf-conventions.gradle.kts create mode 100644 custom-checks/src/main/java/com/google/errorprone/bugpatterns/checkreturnvalue/CanIgnoreReturnValueSuggesterFactory.java create mode 100644 custom-checks/src/main/java/io/opentelemetry/javaagent/customchecks/OtelCanIgnoreReturnValueSuggester.java create mode 100644 docs/apidiffs/2.0.0_vs_1.32.0/opentelemetry-instrumentation-annotations.txt create mode 100644 docs/apidiffs/2.0.0_vs_1.32.0/opentelemetry-instrumentation-api.txt create mode 100644 docs/apidiffs/current_vs_latest/opentelemetry-spring-boot-autoconfigure.txt create mode 100644 docs/apidiffs/current_vs_latest/opentelemetry-spring-boot-starter.txt create mode 100755 docs/contributing/selectModules.kts create mode 100644 instrumentation-api-incubator/build.gradle.kts create mode 100644 instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/builder/internal/DefaultHttpClientInstrumenterBuilder.java create mode 100644 instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/builder/internal/DefaultHttpServerInstrumenterBuilder.java create mode 100644 instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/config/internal/CommonConfig.java create mode 100644 instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/config/internal/EnduserConfig.java rename {javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/bootstrap => instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/config}/internal/InstrumentationConfig.java (63%) rename {instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api => instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator}/log/LoggingContextConstants.java (93%) rename {instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter => instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv}/code/CodeAttributesExtractor.java (67%) rename {instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter => instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv}/code/CodeAttributesGetter.java (89%) rename {instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter => instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv}/code/CodeSpanNameExtractor.java (87%) rename {instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter => instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv}/db/DbClientAttributesExtractor.java (67%) rename {instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter => instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv}/db/DbClientAttributesGetter.java (92%) rename {instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter => instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv}/db/DbClientCommonAttributesExtractor.java (62%) rename {instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter => instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv}/db/DbClientCommonAttributesGetter.java (87%) rename {instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter => instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv}/db/DbClientSpanNameExtractor.java (94%) rename {instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/metrics => instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv}/db/DbConnectionPoolMetrics.java (95%) rename {instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api => instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv}/db/RedisCommandSanitizer.java (99%) rename {instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter => instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv}/db/SqlClientAttributesExtractor.java (61%) rename {instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter => instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv}/db/SqlClientAttributesExtractorBuilder.java (81%) rename {instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter => instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv}/db/SqlClientAttributesGetter.java (93%) rename {instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api => instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv}/db/SqlDialect.java (81%) rename {instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api => instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv}/db/SqlStatementInfo.java (90%) rename {instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api => instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv}/db/SqlStatementSanitizer.java (96%) create mode 100644 instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/http/HttpClientExperimentalMetrics.java create mode 100644 instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/http/HttpClientPeerServiceAttributesExtractor.java create mode 100644 instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/http/HttpExperimentalAttributesExtractor.java create mode 100644 instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/http/HttpExperimentalMetricsAdvice.java create mode 100644 instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/http/HttpMessageBodySizeUtil.java create mode 100644 instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/http/HttpServerExperimentalMetrics.java rename {instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter => instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv}/messaging/CapturedMessageHeadersUtil.java (94%) rename {instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter => instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv}/messaging/MessageOperation.java (50%) create mode 100644 instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/messaging/MessagingAttributesExtractor.java rename {instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter => instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv}/messaging/MessagingAttributesExtractorBuilder.java (95%) rename {instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter => instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv}/messaging/MessagingAttributesGetter.java (62%) create mode 100644 instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/messaging/MessagingConsumerMetrics.java create mode 100644 instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/messaging/MessagingMetricsAdvice.java create mode 100644 instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/messaging/MessagingProducerMetrics.java rename {instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter => instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv}/messaging/MessagingSpanNameExtractor.java (87%) create mode 100644 instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/net/PeerServiceAttributesExtractor.java create mode 100644 instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/net/PeerServiceResolver.java create mode 100644 instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/net/PeerServiceResolverImpl.java create mode 100644 instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/net/internal/UrlParser.java rename {instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter => instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv}/rpc/RpcAttributesGetter.java (90%) rename {instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter => instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv}/rpc/RpcClientAttributesExtractor.java (86%) rename {instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter => instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv}/rpc/RpcClientMetrics.java (78%) rename {instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter => instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv}/rpc/RpcCommonAttributesExtractor.java (61%) create mode 100644 instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/rpc/RpcMetricsAdvice.java rename {instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter => instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv}/rpc/RpcServerAttributesExtractor.java (86%) rename {instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter => instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv}/rpc/RpcServerMetrics.java (78%) rename {instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter => instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv}/rpc/RpcSpanNameExtractor.java (94%) rename {instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter => instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv}/util/ClassAndMethod.java (77%) rename {instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter => instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv}/util/ClassAndMethodAttributesGetter.java (75%) rename {instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter => instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv}/util/SpanNames.java (95%) rename {instrumentation-api-semconv => instrumentation-api-incubator}/src/main/jflex/SqlSanitizer.jflex (74%) rename {instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter => instrumentation-api-incubator/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv}/code/CodeAttributesExtractorTest.java (88%) rename {instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter => instrumentation-api-incubator/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv}/code/CodeSpanNameExtractorTest.java (71%) rename {instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter => instrumentation-api-incubator/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv}/db/DbClientAttributesExtractorTest.java (81%) rename {instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter => instrumentation-api-incubator/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv}/db/DbClientSpanNameExtractorTest.java (98%) rename {instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api => instrumentation-api-incubator/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv}/db/RedisCommandSanitizerTest.java (99%) rename {instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter => instrumentation-api-incubator/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv}/db/SqlClientAttributesExtractorTest.java (76%) rename {instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api => instrumentation-api-incubator/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv}/db/SqlStatementSanitizerTest.java (79%) rename instrumentation-api-semconv/src/testStableHttpSemconv/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpClientMetricsStableSemconvTest.java => instrumentation-api-incubator/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv/http/HttpClientExperimentalMetricsTest.java (58%) create mode 100644 instrumentation-api-incubator/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv/http/HttpClientPeerServiceAttributesExtractorTest.java create mode 100644 instrumentation-api-incubator/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv/http/HttpExperimentalAttributesExtractorTest.java rename instrumentation-api-semconv/src/testStableHttpSemconv/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpServerMetricsStableSemconvTest.java => instrumentation-api-incubator/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv/http/HttpServerExperimentalMetricsTest.java (61%) rename {instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter => instrumentation-api-incubator/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv}/messaging/MessagingAttributesExtractorTest.java (58%) create mode 100644 instrumentation-api-incubator/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv/messaging/MessagingProducerMetricsTest.java rename {instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter => instrumentation-api-incubator/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv}/messaging/MessagingSpanNameExtractorTest.java (90%) rename {instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter => instrumentation-api-incubator/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv}/net/PeerServiceAttributesExtractorTest.java (62%) create mode 100644 instrumentation-api-incubator/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv/net/PeerServiceResolverTest.java create mode 100644 instrumentation-api-incubator/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv/net/internal/UrlParserTest.java rename {instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter => instrumentation-api-incubator/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv}/rpc/RpcAttributesExtractorTest.java (76%) rename {instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter => instrumentation-api-incubator/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv}/rpc/RpcClientMetricsTest.java (66%) rename {instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter => instrumentation-api-incubator/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv}/rpc/RpcServerMetricsTest.java (67%) rename {instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter => instrumentation-api-incubator/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv}/rpc/RpcSpanNameExtractorTest.java (95%) delete mode 100644 instrumentation-api-semconv/build.gradle.kts delete mode 100644 instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/ForwardedHeaderParser.java delete mode 100644 instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpAttributes.java delete mode 100644 instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpClientAttributesExtractor.java delete mode 100644 instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpClientAttributesExtractorBuilder.java delete mode 100644 instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpClientMetrics.java delete mode 100644 instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpCommonAttributesExtractor.java delete mode 100644 instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpMessageBodySizeUtil.java delete mode 100644 instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpMetricsUtil.java delete mode 100644 instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpNetworkTransportFilter.java delete mode 100644 instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpRouteSource.java delete mode 100644 instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpServerAttributesExtractor.java delete mode 100644 instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpServerAttributesExtractorBuilder.java delete mode 100644 instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpServerMetrics.java delete mode 100644 instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpSpanNameExtractor.java delete mode 100644 instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpStatusConverter.java delete mode 100644 instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/TemporaryMetricsView.java delete mode 100644 instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/messaging/MessagingAttributesExtractor.java delete mode 100644 instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/net/NetClientAttributesExtractor.java delete mode 100644 instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/net/NetClientAttributesGetter.java delete mode 100644 instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/net/NetServerAttributesExtractor.java delete mode 100644 instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/net/NetServerAttributesGetter.java delete mode 100644 instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/net/PeerServiceAttributesExtractor.java delete mode 100644 instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/net/internal/FallbackNamePortGetter.java delete mode 100644 instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/net/internal/InternalNetClientAttributesExtractor.java delete mode 100644 instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/net/internal/InternalNetServerAttributesExtractor.java delete mode 100644 instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/net/internal/NetAttributes.java delete mode 100644 instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/net/internal/NoopNamePortGetter.java delete mode 100644 instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/network/ClientAttributesGetter.java delete mode 100644 instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/network/NetworkAttributesGetter.java delete mode 100644 instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/network/ServerAttributesExtractor.java delete mode 100644 instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/network/ServerAttributesGetter.java delete mode 100644 instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/network/internal/InternalClientAttributesExtractor.java delete mode 100644 instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/network/internal/InternalServerAttributesExtractor.java delete mode 100644 instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/network/internal/NetworkAttributes.java delete mode 100644 instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/network/internal/NetworkTransportFilter.java delete mode 100644 instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/rpc/MetricsView.java delete mode 100644 instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/url/internal/InternalUrlAttributesExtractor.java delete mode 100644 instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/url/internal/UrlAttributes.java delete mode 100644 instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/server/ServerSpan.java delete mode 100644 instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/http/ForwardedHeaderParserTest.java delete mode 100644 instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpClientAttributesExtractorTest.java delete mode 100644 instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpClientMetricsTest.java delete mode 100644 instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpClientResendTest.java delete mode 100644 instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpClientStatusConverterTest.java delete mode 100644 instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpServerAttributesExtractorTest.java delete mode 100644 instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpServerMetricsTest.java delete mode 100644 instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpServerStatusConverterTest.java delete mode 100644 instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/http/TemporaryMetricsViewTest.java delete mode 100644 instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/net/InetSocketAddressNetClientAttributesGetterTest.java delete mode 100644 instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/net/InetSocketAddressNetServerAttributesGetterTest.java delete mode 100644 instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/net/NetClientAttributesExtractorTest.java delete mode 100644 instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/net/NetServerAttributesExtractorTest.java delete mode 100644 instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/network/ServerAttributesExtractorInetSocketAddressTest.java delete mode 100644 instrumentation-api-semconv/src/testBothHttpSemconv/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpClientAttributesExtractorBothSemconvTest.java delete mode 100644 instrumentation-api-semconv/src/testBothHttpSemconv/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpServerAttributesExtractorBothSemconvTest.java delete mode 100644 instrumentation-api-semconv/src/testBothHttpSemconv/java/io/opentelemetry/instrumentation/api/instrumenter/net/NetClientAttributesExtractorBothSemconvTest.java delete mode 100644 instrumentation-api-semconv/src/testBothHttpSemconv/java/io/opentelemetry/instrumentation/api/instrumenter/net/NetServerAttributesExtractorBothSemconvTest.java delete mode 100644 instrumentation-api-semconv/src/testStableHttpSemconv/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpClientAttributesExtractorStableSemconvTest.java delete mode 100644 instrumentation-api-semconv/src/testStableHttpSemconv/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpServerAttributesExtractorStableSemconvTest.java delete mode 100644 instrumentation-api-semconv/src/testStableHttpSemconv/java/io/opentelemetry/instrumentation/api/instrumenter/net/NetClientAttributesExtractorStableSemconvTest.java delete mode 100644 instrumentation-api-semconv/src/testStableHttpSemconv/java/io/opentelemetry/instrumentation/api/instrumenter/net/NetServerAttributesExtractorStableSemconvTest.java create mode 100644 instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/HttpConstants.java create mode 100644 instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/HttpProtocolUtil.java create mode 100644 instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/OperationMetricsUtil.java create mode 100644 instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/SchemaUrlProvider.java delete mode 100644 instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/SemconvStability.java rename {instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter => instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv}/http/CapturedHttpHeadersUtil.java (68%) create mode 100644 instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/http/ForwardedHostAddressAndPortExtractor.java create mode 100644 instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/http/ForwardedUrlSchemeProvider.java create mode 100644 instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/http/HeaderParsingHelper.java create mode 100644 instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/http/HostAddressAndPortExtractor.java create mode 100644 instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/http/HttpClientAttributesExtractor.java create mode 100644 instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/http/HttpClientAttributesExtractorBuilder.java rename {instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter => instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv}/http/HttpClientAttributesGetter.java (58%) create mode 100644 instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/http/HttpClientMetrics.java rename instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpClientResend.java => instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/http/HttpClientRequestResendCount.java (58%) create mode 100644 instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/http/HttpCommonAttributesExtractor.java rename {instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter => instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv}/http/HttpCommonAttributesGetter.java (61%) create mode 100644 instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/http/HttpMetricsAdvice.java create mode 100644 instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/http/HttpServerAddressAndPortExtractor.java create mode 100644 instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/http/HttpServerAttributesExtractor.java create mode 100644 instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/http/HttpServerAttributesExtractorBuilder.java rename {instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter => instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv}/http/HttpServerAttributesGetter.java (59%) create mode 100644 instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/http/HttpServerMetrics.java rename instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpRouteHolder.java => instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/http/HttpServerRoute.java (53%) rename instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpRouteBiGetter.java => instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/http/HttpServerRouteBiGetter.java (67%) create mode 100644 instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/http/HttpServerRouteBuilder.java rename instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpRouteGetter.java => instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/http/HttpServerRouteGetter.java (66%) create mode 100644 instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/http/HttpServerRouteSource.java create mode 100644 instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/http/HttpSpanNameExtractor.java create mode 100644 instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/http/HttpSpanNameExtractorBuilder.java rename {instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter => instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv}/http/HttpSpanStatusExtractor.java (55%) create mode 100644 instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/http/HttpStatusCodeConverter.java create mode 100644 instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/http/package-info.java rename {instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter => instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv}/network/ClientAttributesExtractor.java (51%) create mode 100644 instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/network/ClientAttributesGetter.java rename {instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter => instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv}/network/NetworkAttributesExtractor.java (66%) create mode 100644 instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/network/NetworkAttributesGetter.java create mode 100644 instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/network/ServerAttributesExtractor.java create mode 100644 instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/network/ServerAttributesGetter.java create mode 100644 instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/network/internal/AddressAndPort.java create mode 100644 instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/network/internal/AddressAndPortExtractor.java create mode 100644 instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/network/internal/ClientAddressAndPortExtractor.java rename {instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/net => instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/network}/internal/InetSocketAddressUtil.java (63%) create mode 100644 instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/network/internal/InternalClientAttributesExtractor.java rename {instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter => instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv}/network/internal/InternalNetworkAttributesExtractor.java (50%) create mode 100644 instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/network/internal/InternalServerAttributesExtractor.java create mode 100644 instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/network/internal/ServerAddressAndPortExtractor.java create mode 100644 instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/network/package-info.java create mode 100644 instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/package-info.java rename {instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter => instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv}/url/UrlAttributesExtractor.java (69%) rename {instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter => instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv}/url/UrlAttributesGetter.java (94%) create mode 100644 instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/url/internal/InternalUrlAttributesExtractor.java create mode 100644 instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/url/package-info.java rename {instrumentation-api-semconv => instrumentation-api}/src/test/java/io/opentelemetry/instrumentation/api/internal/ClassNamesTest.java (97%) create mode 100644 instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/internal/OperationMetricsUtilTest.java create mode 100644 instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/semconv/http/ForwardedHostAddressAndPortExtractorTest.java create mode 100644 instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/semconv/http/ForwardedUrlSchemeProviderTest.java create mode 100644 instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/semconv/http/HostAddressAndPortExtractorTest.java create mode 100644 instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/semconv/http/HttpClientAttributesExtractorTest.java create mode 100644 instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/semconv/http/HttpClientMetricsTest.java create mode 100644 instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/semconv/http/HttpClientRequestResendCountTest.java create mode 100644 instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/semconv/http/HttpServerAddressAndPortExtractorTest.java create mode 100644 instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/semconv/http/HttpServerAttributesExtractorTest.java create mode 100644 instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/semconv/http/HttpServerMetricsTest.java rename instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpRouteHolderTest.java => instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/semconv/http/HttpServerRouteTest.java (52%) rename {instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter => instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/semconv}/http/HttpSpanNameExtractorTest.java (96%) rename {instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter => instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/semconv}/http/HttpSpanStatusExtractorTest.java (90%) create mode 100644 instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/semconv/http/HttpStatusCodeConverterClientTest.java create mode 100644 instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/semconv/http/HttpStatusCodeConverterServerTest.java create mode 100644 instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/semconv/http/ValidRequestMethodsProvider.java rename {instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter => instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/semconv}/network/ClientAttributesExtractorTest.java (68%) create mode 100644 instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/semconv/network/NetworkAttributesExtractorInetSocketAddressTest.java rename {instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter => instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/semconv}/network/NetworkAttributesExtractorTest.java (56%) rename {instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter => instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/semconv}/network/ServerAttributesExtractorTest.java (59%) rename {instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter => instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/semconv}/url/UrlAttributesExtractorTest.java (94%) create mode 100644 instrumentation/akka/akka-http-10.0/javaagent/src/javaRouteTest/java/io/opentelemetry/javaagent/instrumentation/akkahttp/AkkaHttpServerJavaRouteTest.java create mode 100644 instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/client/AkkaHttpClientUtil.java delete mode 100644 instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/client/AkkaHttpNetAttributesGetter.java create mode 100644 instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/server/AkkaHttpServerSourceInstrumentation.java delete mode 100644 instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/server/AkkaNetServerAttributesGetter.java create mode 100644 instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/server/route/AkkaHttpServerRouteInstrumentationModule.java create mode 100644 instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/server/route/AkkaRouteHolder.java create mode 100644 instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/server/route/PathConcatenationInstrumentation.java create mode 100644 instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/server/route/PathMatcherInstrumentation.java create mode 100644 instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/server/route/PathMatcherStaticInstrumentation.java create mode 100644 instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/server/route/RouteConcatenationInstrumentation.java create mode 100644 instrumentation/akka/akka-http-10.0/javaagent/src/test/scala/io/opentelemetry/javaagent/instrumentation/akkahttp/AkkaHttpServerRouteTest.scala create mode 100644 instrumentation/akka/akka-http-10.0/javaagent/src/test/scala/io/opentelemetry/javaagent/instrumentation/akkahttp/AkkaHttpTestServerSourceWebServer.scala create mode 100644 instrumentation/alibaba-druid-1.0/javaagent/build.gradle.kts create mode 100644 instrumentation/alibaba-druid-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/alibabadruid/v1_0/DruidDataSourceInstrumentation.java rename instrumentation/{jaxrs-client/jaxrs-client-1.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrsclient/v1_1/JaxRsClientInstrumentationModule.java => alibaba-druid-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/alibabadruid/v1_0/DruidInstrumentationModule.java} (62%) create mode 100644 instrumentation/alibaba-druid-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/alibabadruid/v1_0/DruidSingletons.java create mode 100644 instrumentation/alibaba-druid-1.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/alibabadruid/v1_0/DruidInstrumentationTest.java create mode 100644 instrumentation/alibaba-druid-1.0/library/build.gradle.kts create mode 100644 instrumentation/alibaba-druid-1.0/library/src/main/java/io/opentelemetry/instrumentation/alibabadruid/v1_0/ConnectionPoolMetrics.java create mode 100644 instrumentation/alibaba-druid-1.0/library/src/main/java/io/opentelemetry/instrumentation/alibabadruid/v1_0/DruidTelemetry.java create mode 100644 instrumentation/alibaba-druid-1.0/library/src/test/java/io/opentelemetry/instrumentation/alibabadruid/v1_0/DruidInstrumentationTest.java create mode 100644 instrumentation/alibaba-druid-1.0/testing/build.gradle.kts create mode 100644 instrumentation/alibaba-druid-1.0/testing/src/main/java/io/opentelemetry/instrumentation/alibabadruid/AbstractDruidInstrumentationTest.java rename instrumentation/apache-dubbo-2.7/javaagent/src/main/resources/{spi-to-inject => apache-dubbo-2.7/META-INF}/org.apache.dubbo.rpc.Filter (100%) delete mode 100644 instrumentation/apache-dubbo-2.7/javaagent/src/test/groovy/io/opentelemetry/instrumentation/apachedubbo/v2_7/DubboTest.groovy delete mode 100644 instrumentation/apache-dubbo-2.7/javaagent/src/test/groovy/io/opentelemetry/instrumentation/apachedubbo/v2_7/DubboTraceChainTest.groovy create mode 100644 instrumentation/apache-dubbo-2.7/javaagent/src/testDubbo/java/io/opentelemetry/javaagent/instrumentation/apachedubbo/v2_7/DubboAgentTest.java create mode 100644 instrumentation/apache-dubbo-2.7/javaagent/src/testDubbo/java/io/opentelemetry/javaagent/instrumentation/apachedubbo/v2_7/DubboAgentTraceChainTest.java create mode 100644 instrumentation/apache-dubbo-2.7/library-autoconfigure/src/main/java/io/opentelemetry/instrumentation/apachedubbo/v2_7/DubboNetworkServerAttributesGetter.java rename instrumentation/apache-dubbo-2.7/library-autoconfigure/src/main/java/io/opentelemetry/instrumentation/apachedubbo/v2_7/internal/{DubboNetClientAttributesGetter.java => DubboClientNetworkAttributesGetter.java} (67%) delete mode 100644 instrumentation/apache-dubbo-2.7/library-autoconfigure/src/main/java/io/opentelemetry/instrumentation/apachedubbo/v2_7/internal/DubboNetServerAttributesGetter.java delete mode 100644 instrumentation/apache-dubbo-2.7/library-autoconfigure/src/test/groovy/io/opentelemetry/instrumentation/apachedubbo/v2_7/DubboTest.groovy delete mode 100644 instrumentation/apache-dubbo-2.7/library-autoconfigure/src/test/groovy/io/opentelemetry/instrumentation/apachedubbo/v2_7/DubboTraceChainTest.groovy create mode 100644 instrumentation/apache-dubbo-2.7/library-autoconfigure/src/test/java/io/opentelemetry/instrumentation/apachedubbo/v2_7/DubboTest.java create mode 100644 instrumentation/apache-dubbo-2.7/library-autoconfigure/src/test/java/io/opentelemetry/instrumentation/apachedubbo/v2_7/DubboTraceChainTest.java delete mode 100644 instrumentation/apache-dubbo-2.7/testing/src/main/groovy/io/opentelemetry/instrumentation/apachedubbo/v2_7/AbstractDubboTest.groovy delete mode 100644 instrumentation/apache-dubbo-2.7/testing/src/main/groovy/io/opentelemetry/instrumentation/apachedubbo/v2_7/AbstractDubboTraceChainTest.groovy delete mode 100644 instrumentation/apache-dubbo-2.7/testing/src/main/groovy/io/opentelemetry/instrumentation/apachedubbo/v2_7/DubboTestUtil.groovy create mode 100644 instrumentation/apache-dubbo-2.7/testing/src/main/java/io/opentelemetry/instrumentation/apachedubbo/v2_7/AbstractDubboTest.java create mode 100644 instrumentation/apache-dubbo-2.7/testing/src/main/java/io/opentelemetry/instrumentation/apachedubbo/v2_7/AbstractDubboTraceChainTest.java create mode 100644 instrumentation/apache-dubbo-2.7/testing/src/main/java/io/opentelemetry/instrumentation/apachedubbo/v2_7/DubboTestUtil.java delete mode 100644 instrumentation/apache-httpasyncclient-4.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpasyncclient/ApacheHttpAsyncClientNetAttributesGetter.java delete mode 100644 instrumentation/apache-httpclient/apache-httpclient-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v2_0/ApacheHttpClientNetAttributesGetter.java delete mode 100644 instrumentation/apache-httpclient/apache-httpclient-2.0/javaagent/src/test/groovy/AbstractCommonsHttpClientTest.groovy delete mode 100644 instrumentation/apache-httpclient/apache-httpclient-2.0/javaagent/src/test/groovy/CommonsHttpClientLatestDepsTest.groovy delete mode 100644 instrumentation/apache-httpclient/apache-httpclient-2.0/javaagent/src/test/groovy/CommonsHttpClientTest.groovy create mode 100644 instrumentation/apache-httpclient/apache-httpclient-2.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v2_0/AbstractCommonsHttpClientTest.java create mode 100644 instrumentation/apache-httpclient/apache-httpclient-2.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v2_0/CommonsHttpClientLatestDepsTest.java create mode 100644 instrumentation/apache-httpclient/apache-httpclient-2.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v2_0/CommonsHttpClientTest.java delete mode 100644 instrumentation/apache-httpclient/apache-httpclient-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v4_0/ApacheHttpClientNetAttributesGetter.java delete mode 100644 instrumentation/apache-httpclient/apache-httpclient-4.3/library/src/main/java/io/opentelemetry/instrumentation/apachehttpclient/v4_3/ApacheHttpClientNetAttributesGetter.java delete mode 100644 instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v5_0/ApacheHttpClientNetAttributesGetter.java create mode 100644 instrumentation/apache-httpclient/apache-httpclient-5.2/library/README.md create mode 100644 instrumentation/apache-httpclient/apache-httpclient-5.2/library/build.gradle.kts create mode 100644 instrumentation/apache-httpclient/apache-httpclient-5.2/library/src/main/java/io/opentelemetry/instrumentation/apachehttpclient/v5_2/ApacheHttpClient5HttpAttributesGetter.java create mode 100644 instrumentation/apache-httpclient/apache-httpclient-5.2/library/src/main/java/io/opentelemetry/instrumentation/apachehttpclient/v5_2/ApacheHttpClient5Request.java create mode 100644 instrumentation/apache-httpclient/apache-httpclient-5.2/library/src/main/java/io/opentelemetry/instrumentation/apachehttpclient/v5_2/ApacheHttpClient5Telemetry.java create mode 100644 instrumentation/apache-httpclient/apache-httpclient-5.2/library/src/main/java/io/opentelemetry/instrumentation/apachehttpclient/v5_2/ApacheHttpClient5TelemetryBuilder.java create mode 100644 instrumentation/apache-httpclient/apache-httpclient-5.2/library/src/main/java/io/opentelemetry/instrumentation/apachehttpclient/v5_2/HttpHeaderSetter.java create mode 100644 instrumentation/apache-httpclient/apache-httpclient-5.2/library/src/main/java/io/opentelemetry/instrumentation/apachehttpclient/v5_2/OtelExecChainHandler.java create mode 100644 instrumentation/apache-httpclient/apache-httpclient-5.2/library/src/test/java/io/opentelemetry/instrumentation/apachehttpclient/v5_2/AbstractApacheHttpClient5Test.java create mode 100644 instrumentation/apache-httpclient/apache-httpclient-5.2/library/src/test/java/io/opentelemetry/instrumentation/apachehttpclient/v5_2/ApacheHttpClient5Test.java create mode 100644 instrumentation/apache-httpclient/apache-httpclient-5.2/library/src/test/java/io/opentelemetry/instrumentation/apachehttpclient/v5_2/HttpUriRequest.java create mode 100644 instrumentation/apache-shenyu-2.4/README.md create mode 100644 instrumentation/apache-shenyu-2.4/javaagent/build.gradle.kts create mode 100644 instrumentation/apache-shenyu-2.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apacheshenyu/v2_4/ApacheShenYuInstrumentationModule.java create mode 100644 instrumentation/apache-shenyu-2.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apacheshenyu/v2_4/ApacheShenYuSingletons.java create mode 100644 instrumentation/apache-shenyu-2.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apacheshenyu/v2_4/ContextBuilderInstrumentation.java create mode 100644 instrumentation/apache-shenyu-2.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apacheshenyu/v2_4/MetaDataHelper.java create mode 100644 instrumentation/apache-shenyu-2.4/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apacheshenyu/v2_4/ShenYuBootstrapApplication.java create mode 100644 instrumentation/apache-shenyu-2.4/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apacheshenyu/v2_4/ShenYuRouteTest.java delete mode 100644 instrumentation/armeria-1.3/library/src/main/java/io/opentelemetry/instrumentation/armeria/v1_3/ArmeriaHttpClientAttributesGetter.java delete mode 100644 instrumentation/armeria-1.3/library/src/main/java/io/opentelemetry/instrumentation/armeria/v1_3/ArmeriaHttpServerAttributesGetter.java delete mode 100644 instrumentation/armeria-1.3/library/src/main/java/io/opentelemetry/instrumentation/armeria/v1_3/ArmeriaNetServerAttributesGetter.java delete mode 100644 instrumentation/armeria-1.3/library/src/main/java/io/opentelemetry/instrumentation/armeria/v1_3/ArmeriaTelemetryBuilder.java delete mode 100644 instrumentation/armeria-1.3/library/src/main/java/io/opentelemetry/instrumentation/armeria/v1_3/internal/ArmeriaNetClientAttributesGetter.java rename instrumentation/{ => armeria}/armeria-1.3/javaagent/build.gradle.kts (63%) rename instrumentation/{ => armeria}/armeria-1.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/armeria/v1_3/AbstractStreamMessageSubscriptionInstrumentation.java (100%) create mode 100644 instrumentation/armeria/armeria-1.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/armeria/v1_3/ArmeriaHttpResponseMutator.java rename instrumentation/{ => armeria}/armeria-1.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/armeria/v1_3/ArmeriaInstrumentationModule.java (100%) rename instrumentation/{ => armeria}/armeria-1.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/armeria/v1_3/ArmeriaServerBuilderInstrumentation.java (100%) rename instrumentation/{ => armeria}/armeria-1.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/armeria/v1_3/ArmeriaSingletons.java (51%) rename instrumentation/{ => armeria}/armeria-1.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/armeria/v1_3/ArmeriaWebClientBuilderInstrumentation.java (100%) rename instrumentation/{ => armeria}/armeria-1.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/armeria/v1_3/CompletableFutureWrapper.java (100%) create mode 100644 instrumentation/armeria/armeria-1.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/armeria/v1_3/ResponseCustomizingDecorator.java rename instrumentation/{ => armeria}/armeria-1.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/armeria/v1_3/ServerDecorator.java (61%) rename instrumentation/{ => armeria}/armeria-1.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/armeria/v1_3/SubscriberWrapper.java (100%) create mode 100644 instrumentation/armeria/armeria-1.3/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/armeria/v1_3/ArmeriaHttp2ServerTest.java create mode 100644 instrumentation/armeria/armeria-1.3/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/armeria/v1_3/ArmeriaHttp2Test.java rename instrumentation/{ => armeria}/armeria-1.3/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/armeria/v1_3/ArmeriaHttpClientTest.java (100%) rename instrumentation/{ => armeria}/armeria-1.3/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/armeria/v1_3/ArmeriaHttpServerTest.java (81%) rename instrumentation/{ => armeria}/armeria-1.3/library/build.gradle.kts (68%) rename instrumentation/{ => armeria}/armeria-1.3/library/src/main/java/io/opentelemetry/instrumentation/armeria/v1_3/ArmeriaTelemetry.java (100%) create mode 100644 instrumentation/armeria/armeria-1.3/library/src/main/java/io/opentelemetry/instrumentation/armeria/v1_3/ArmeriaTelemetryBuilder.java rename instrumentation/{ => armeria}/armeria-1.3/library/src/main/java/io/opentelemetry/instrumentation/armeria/v1_3/OpenTelemetryClient.java (100%) rename instrumentation/{ => armeria}/armeria-1.3/library/src/main/java/io/opentelemetry/instrumentation/armeria/v1_3/OpenTelemetryService.java (100%) create mode 100644 instrumentation/armeria/armeria-1.3/library/src/main/java/io/opentelemetry/instrumentation/armeria/v1_3/internal/ArmeriaHttpClientAttributesGetter.java create mode 100644 instrumentation/armeria/armeria-1.3/library/src/main/java/io/opentelemetry/instrumentation/armeria/v1_3/internal/ArmeriaHttpServerAttributesGetter.java create mode 100644 instrumentation/armeria/armeria-1.3/library/src/main/java/io/opentelemetry/instrumentation/armeria/v1_3/internal/ArmeriaInstrumenterBuilderFactory.java create mode 100644 instrumentation/armeria/armeria-1.3/library/src/main/java/io/opentelemetry/instrumentation/armeria/v1_3/internal/ArmeriaInstrumenterBuilderUtil.java rename instrumentation/{armeria-1.3/library/src/main/java/io/opentelemetry/instrumentation/armeria/v1_3 => armeria/armeria-1.3/library/src/main/java/io/opentelemetry/instrumentation/armeria/v1_3/internal}/ClientRequestContextSetter.java (89%) rename instrumentation/{ => armeria}/armeria-1.3/library/src/main/java/io/opentelemetry/instrumentation/armeria/v1_3/internal/RequestContextAccess.java (92%) rename instrumentation/{armeria-1.3/library/src/main/java/io/opentelemetry/instrumentation/armeria/v1_3 => armeria/armeria-1.3/library/src/main/java/io/opentelemetry/instrumentation/armeria/v1_3/internal}/RequestContextGetter.java (93%) create mode 100644 instrumentation/armeria/armeria-1.3/library/src/test/java/io/opentelemetry/instrumentation/armeria/v1_3/ArmeriaHttp2ServerTest.java rename instrumentation/{ => armeria}/armeria-1.3/library/src/test/java/io/opentelemetry/instrumentation/armeria/v1_3/ArmeriaHttpClientTest.java (100%) rename instrumentation/{ => armeria}/armeria-1.3/library/src/test/java/io/opentelemetry/instrumentation/armeria/v1_3/ArmeriaHttpServerTest.java (80%) rename instrumentation/{ => armeria}/armeria-1.3/testing/build.gradle.kts (100%) rename instrumentation/{ => armeria}/armeria-1.3/testing/src/main/java/io/opentelemetry/instrumentation/armeria/v1_3/AbstractArmeriaHttpClientTest.java (89%) rename instrumentation/{ => armeria}/armeria-1.3/testing/src/main/java/io/opentelemetry/instrumentation/armeria/v1_3/AbstractArmeriaHttpServerTest.java (98%) create mode 100644 instrumentation/armeria/armeria-grpc-1.14/javaagent/build.gradle.kts create mode 100644 instrumentation/armeria/armeria-grpc-1.14/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/armeria/grpc/v1_14/ArmeriaGrpcClientBuilderInstrumentation.java create mode 100644 instrumentation/armeria/armeria-grpc-1.14/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/armeria/grpc/v1_14/ArmeriaGrpcInstrumentationModule.java create mode 100644 instrumentation/armeria/armeria-grpc-1.14/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/armeria/grpc/v1_14/ArmeriaGrpcServiceBuilderInstrumentation.java create mode 100644 instrumentation/armeria/armeria-grpc-1.14/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/armeria/grpc/v1_14/ArmeriaGrpcTest.java delete mode 100644 instrumentation/async-http-client/async-http-client-1.9/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/asynchttpclient/v1_9/AsyncHttpClientNetAttributesGetter.java delete mode 100644 instrumentation/async-http-client/async-http-client-1.9/javaagent/src/test/groovy/AsyncHttpClientTest.groovy create mode 100644 instrumentation/async-http-client/async-http-client-1.9/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/asynchttpclient/v1_9/AsyncHttpClientTest.java delete mode 100644 instrumentation/async-http-client/async-http-client-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/asynchttpclient/v2_0/AsyncHttpClientAdditionalAttributesExtractor.java delete mode 100644 instrumentation/async-http-client/async-http-client-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/asynchttpclient/v2_0/AsyncHttpClientNetAttributesGetter.java delete mode 100644 instrumentation/async-http-client/async-http-client-2.0/javaagent/src/test/groovy/AsyncHttpClientTest.groovy create mode 100644 instrumentation/async-http-client/async-http-client-2.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/asynchttpclient/v2_0/AsyncHttpClientTest.java create mode 100644 instrumentation/aws-lambda/aws-lambda-core-1.0/javaagent/src/test/java/com/amazonaws/services/lambda/runtime/api/client/AwsLambdaInternalRequestHandler.java create mode 100644 instrumentation/aws-lambda/aws-lambda-core-1.0/javaagent/src/test/java/lambdainternal/AwsLambdaLegacyInternalRequestHandler.java delete mode 100644 instrumentation/aws-lambda/aws-lambda-core-1.0/library/src/main/java/io/opentelemetry/instrumentation/awslambdacore/v1_0/internal/AwsXrayEnvSpanLinksExtractor.java delete mode 100644 instrumentation/aws-lambda/aws-lambda-core-1.0/library/src/test/java/io/opentelemetry/instrumentation/awslambdacore/v1_0/internal/AwsXrayEnvSpanLinksExtractorTest.java create mode 100644 instrumentation/aws-lambda/aws-lambda-core-1.0/library/src/test/java/io/opentelemetry/instrumentation/awslambdacore/v1_0/internal/InstrumenterExtractionTest.java rename instrumentation/aws-lambda/aws-lambda-events-2.2/library/src/main/java/io/opentelemetry/instrumentation/awslambdaevents/v2_2/{ => internal}/CustomJodaModule.java (96%) create mode 100644 instrumentation/aws-lambda/aws-lambda-events-2.2/library/src/main/java/io/opentelemetry/instrumentation/awslambdaevents/v2_2/internal/SerializationUtil.java create mode 100644 instrumentation/aws-lambda/aws-lambda-events-2.2/library/src/test/java/io/opentelemetry/instrumentation/awslambdaevents/v2_2/internal/SerializationUtilTest.java create mode 100644 instrumentation/aws-sdk/aws-sdk-1.11/javaagent/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/SqsAdviceBridge.java create mode 100644 instrumentation/aws-sdk/aws-sdk-1.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/awssdk/v1_11/AbstractAwsSdkInstrumentationModule.java create mode 100644 instrumentation/aws-sdk/aws-sdk-1.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/awssdk/v1_11/SqsInstrumentationModule.java delete mode 100644 instrumentation/aws-sdk/aws-sdk-1.11/javaagent/src/testSqs/groovy/io/opentelemetry/javaagent/instrumentation/awssdk/v1_11/SqsTracingTest.groovy create mode 100644 instrumentation/aws-sdk/aws-sdk-1.11/javaagent/src/testSqs/java/io/opentelemetry/javaagent/instrumentation/awssdk/v1_11/SqsTracingTest.java create mode 100644 instrumentation/aws-sdk/aws-sdk-1.11/javaagent/src/testSqsNoReceiveTelemetry/java/io/opentelemetry/javaagent/instrumentation/awssdk/v1_11/SqsSuppressReceiveSpansTest.java delete mode 100644 instrumentation/aws-sdk/aws-sdk-1.11/library-autoconfigure/src/test/groovy/io/opentelemetry/instrumentation/awssdk/v1_11/instrumentor/SqsTracingTest.groovy create mode 100644 instrumentation/aws-sdk/aws-sdk-1.11/library-autoconfigure/src/test/java/io/opentelemetry/instrumentation/awssdk/v1_11/instrumentor/SqsSuppressReceiveSpansTest.java create mode 100644 instrumentation/aws-sdk/aws-sdk-1.11/library-autoconfigure/src/test/java/io/opentelemetry/javaagent/instrumentation/awssdk/v1_11/SqsTracingTest.java create mode 100644 instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AbstractSqsRequest.java delete mode 100644 instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AwsSdkNetAttributesGetter.java delete mode 100644 instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AwsSdkSpanKindExtractor.java create mode 100644 instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/PluginImplUtil.java create mode 100644 instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/SnsAttributesExtractor.java create mode 100644 instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/SqsAccess.java create mode 100644 instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/SqsAttributesGetter.java create mode 100644 instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/SqsImpl.java create mode 100644 instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/SqsMessage.java delete mode 100644 instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/SqsMessageAccess.java create mode 100644 instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/SqsMessageImpl.java create mode 100644 instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/SqsProcessRequest.java create mode 100644 instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/SqsProcessRequestAttributesGetter.java delete mode 100644 instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/SqsReceiveMessageRequestAccess.java delete mode 100644 instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/SqsReceiveMessageResultAccess.java create mode 100644 instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/SqsReceiveRequest.java create mode 100644 instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/SqsReceiveRequestAttributesGetter.java create mode 100644 instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/TracingIterator.java create mode 100644 instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/TracingList.java delete mode 100644 instrumentation/aws-sdk/aws-sdk-1.11/library/src/test/groovy/io/opentelemetry/instrumentation/awssdk/v1_11/SqsTracingTest.groovy create mode 100644 instrumentation/aws-sdk/aws-sdk-1.11/library/src/test/java/io/opentelemetry/instrumentation/awssdk/v1_11/SqsSuppressReceiveSpansTest.java create mode 100644 instrumentation/aws-sdk/aws-sdk-1.11/library/src/test/java/io/opentelemetry/instrumentation/awssdk/v1_11/SqsTracingTest.java delete mode 100644 instrumentation/aws-sdk/aws-sdk-1.11/testing/src/main/groovy/io/opentelemetry/instrumentation/awssdk/v1_11/AbstractSqsTracingTest.groovy create mode 100644 instrumentation/aws-sdk/aws-sdk-1.11/testing/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AbstractSqsSuppressReceiveSpansTest.java create mode 100644 instrumentation/aws-sdk/aws-sdk-1.11/testing/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AbstractSqsTracingTest.java create mode 100644 instrumentation/aws-sdk/aws-sdk-2.2/javaagent/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/LambdaAdviceBridge.java create mode 100644 instrumentation/aws-sdk/aws-sdk-2.2/javaagent/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/SnsAdviceBridge.java rename instrumentation/aws-sdk/aws-sdk-2.2/{library => javaagent}/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/SqsAdviceBridge.java (53%) create mode 100644 instrumentation/aws-sdk/aws-sdk-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/awssdk/v2_2/DefaultSqsAsyncClientBuilderInstrumentation.java create mode 100644 instrumentation/aws-sdk/aws-sdk-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/awssdk/v2_2/DefaultSqsClientBuilderInstrumentation.java create mode 100644 instrumentation/aws-sdk/aws-sdk-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/awssdk/v2_2/LambdaInstrumentationModule.java create mode 100644 instrumentation/aws-sdk/aws-sdk-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/awssdk/v2_2/SnsInstrumentationModule.java create mode 100644 instrumentation/aws-sdk/aws-sdk-2.2/javaagent/src/s3PresignerTest/java/io/opentelemetry/javaagent/instrumentation/awssdk/v2_2/S3PresignerTest.java create mode 100644 instrumentation/aws-sdk/aws-sdk-2.2/javaagent/src/test/groovy/Aws2SqsSuppressReceiveSpansTest.groovy delete mode 100644 instrumentation/aws-sdk/aws-sdk-2.2/javaagent/src/test/groovy/Aws2SqsTracingTest.groovy create mode 100644 instrumentation/aws-sdk/aws-sdk-2.2/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/awssdk/v2_2/Aws2ClientRecordHttpErrorTest.java create mode 100644 instrumentation/aws-sdk/aws-sdk-2.2/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/awssdk/v2_2/Aws2LambdaTest.java create mode 100644 instrumentation/aws-sdk/aws-sdk-2.2/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/awssdk/v2_2/Aws2SqsTracingTest.java create mode 100644 instrumentation/aws-sdk/aws-sdk-2.2/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/awssdk/v2_2/AwsXrayPropagatorTest.java create mode 100644 instrumentation/aws-sdk/aws-sdk-2.2/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/awssdk/v2_2/QueryProtocolModelTest.java create mode 100644 instrumentation/aws-sdk/aws-sdk-2.2/library-autoconfigure/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/autoconfigure/AwsSdkSingletons.java create mode 100644 instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AbstractSqsRequest.java create mode 100644 instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AwsSdkHttpClientSuppressionAttributesExtractor.java delete mode 100644 instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AwsSdkNetAttributesGetter.java delete mode 100644 instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AwsSdkSpanKindExtractor.java create mode 100644 instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/LambdaAccess.java create mode 100644 instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/LambdaImpl.java create mode 100644 instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/PluginImplUtil.java create mode 100644 instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/Response.java create mode 100644 instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/SnsAccess.java create mode 100644 instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/SnsImpl.java create mode 100644 instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/SqsAttributesGetter.java create mode 100644 instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/SqsMessage.java create mode 100644 instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/SqsMessageImpl.java create mode 100644 instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/SqsProcessRequest.java create mode 100644 instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/SqsProcessRequestAttributesGetter.java delete mode 100644 instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/SqsReceiveMessageRequestAccess.java create mode 100644 instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/SqsReceiveRequest.java create mode 100644 instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/SqsReceiveRequestAttributesGetter.java delete mode 100644 instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/SqsSendMessageRequestAccess.java create mode 100644 instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/SqsTracingContext.java create mode 100644 instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/TracingIterator.java create mode 100644 instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/TracingList.java create mode 100644 instrumentation/aws-sdk/aws-sdk-2.2/library/src/test/groovy/io/opentelemetry/instrumentation/awssdk/v2_2/Aws2SqsSuppressReceiveSpansTest.groovy delete mode 100644 instrumentation/aws-sdk/aws-sdk-2.2/library/src/test/groovy/io/opentelemetry/instrumentation/awssdk/v2_2/Aws2SqsTracingTest.groovy delete mode 100644 instrumentation/aws-sdk/aws-sdk-2.2/library/src/test/groovy/io/opentelemetry/instrumentation/awssdk/v2_2/Aws2SqsTracingTestWithW3CPropagator.groovy delete mode 100644 instrumentation/aws-sdk/aws-sdk-2.2/library/src/test/groovy/io/opentelemetry/instrumentation/awssdk/v2_2/Aws2SqsTracingTestWithW3CPropagatorAndXrayPropagator.groovy create mode 100644 instrumentation/aws-sdk/aws-sdk-2.2/library/src/test/java/io/opentelemetry/instrumentation/awssdk/v2_2/Aws2ClientNotRecordHttpErrorTest.java create mode 100644 instrumentation/aws-sdk/aws-sdk-2.2/library/src/test/java/io/opentelemetry/instrumentation/awssdk/v2_2/Aws2SqsDefaultPropagatorTest.java create mode 100644 instrumentation/aws-sdk/aws-sdk-2.2/library/src/test/java/io/opentelemetry/instrumentation/awssdk/v2_2/Aws2SqsTracingTest.java create mode 100644 instrumentation/aws-sdk/aws-sdk-2.2/library/src/test/java/io/opentelemetry/instrumentation/awssdk/v2_2/Aws2SqsW3cPropagatorAndXrayPropagatorTest.java create mode 100644 instrumentation/aws-sdk/aws-sdk-2.2/library/src/test/java/io/opentelemetry/instrumentation/awssdk/v2_2/Aws2SqsW3cPropagatorTest.java create mode 100644 instrumentation/aws-sdk/aws-sdk-2.2/library/src/test/java/io/opentelemetry/instrumentation/awssdk/v2_2/QueryProtocolModelTest.java create mode 100644 instrumentation/aws-sdk/aws-sdk-2.2/library/src/testCoreOnly/groovy/io/opentelemetry/instrumentation/awssdk/v2_2/Aws2ClientDynamodbTest.groovy create mode 100644 instrumentation/aws-sdk/aws-sdk-2.2/library/src/testLambda/java/io/opentelemetry/instrumentation/awssdk/v2_2/Aws2LambdaTest.java create mode 100644 instrumentation/aws-sdk/aws-sdk-2.2/testing/src/main/groovy/io/opentelemetry/instrumentation/awssdk/v2_2/AbstractAws2ClientCoreTest.groovy create mode 100644 instrumentation/aws-sdk/aws-sdk-2.2/testing/src/main/groovy/io/opentelemetry/instrumentation/awssdk/v2_2/AbstractAws2SqsSuppressReceiveSpansTest.groovy delete mode 100644 instrumentation/aws-sdk/aws-sdk-2.2/testing/src/main/groovy/io/opentelemetry/instrumentation/awssdk/v2_2/AbstractAws2SqsTracingTest.groovy create mode 100644 instrumentation/aws-sdk/aws-sdk-2.2/testing/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AbstractAws2ClientRecordHttpErrorTest.java create mode 100644 instrumentation/aws-sdk/aws-sdk-2.2/testing/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AbstractAws2LambdaTest.java create mode 100644 instrumentation/aws-sdk/aws-sdk-2.2/testing/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AbstractAws2SqsTracingTest.java create mode 100644 instrumentation/aws-sdk/aws-sdk-2.2/testing/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AbstractQueryProtocolModelTest.java rename instrumentation/azure-core/azure-core-1.14/javaagent/src/{test/java => testAzure/java/io/opentelemetry/javaagent/instrumentation/azurecore/v1_14}/AzureSdkTest.java (96%) rename instrumentation/azure-core/azure-core-1.19/javaagent/src/{test/java => testAzure/java/io/opentelemetry/javaagent/instrumentation/azurecore/v1_19}/AzureSdkTest.java (96%) rename instrumentation/azure-core/azure-core-1.36/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/azurecore/v1_36/{SuppressNestedClientMono.java => SuppressNestedClientHelper.java} (59%) create mode 100644 instrumentation/azure-core/azure-core-1.36/javaagent/src/main/resources/azure-core-1.36/META-INF/services/com.azure.core.util.tracing.Tracer rename instrumentation/azure-core/azure-core-1.36/javaagent/src/{test/java => testAzure/java/io/opentelemetry/javaagent/instrumentation/azurecore/v1_36}/AzureSdkTest.java (96%) create mode 100644 instrumentation/azure-core/azure-core-1.36/javaagent/src/testAzure/java/io/opentelemetry/javaagent/instrumentation/azurecore/v1_36/AzureSdkTestOld.java create mode 100644 instrumentation/azure-core/azure-core-1.36/library-instrumentation-shaded/src/main/java/com/azure/core/tracing/opentelemetry/OpenTelemetryTracingOptions.java delete mode 100644 instrumentation/camel-2.20/javaagent-unit-tests/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/decorators/SanitizationTest.groovy rename instrumentation/camel-2.20/javaagent-unit-tests/src/test/{groovy => java}/io/opentelemetry/javaagent/instrumentation/apachecamel/CamelPropagationUtilTest.java (100%) create mode 100644 instrumentation/camel-2.20/javaagent-unit-tests/src/test/java/io/opentelemetry/javaagent/instrumentation/apachecamel/decorators/SanitizationTest.java delete mode 100644 instrumentation/camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/DirectCamelTest.groovy delete mode 100644 instrumentation/camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/DirectConfig.groovy delete mode 100644 instrumentation/camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/MulticastConfig.groovy delete mode 100644 instrumentation/camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/MulticastDirectCamelTest.groovy delete mode 100644 instrumentation/camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/RestCamelTest.groovy delete mode 100644 instrumentation/camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/RestConfig.groovy delete mode 100644 instrumentation/camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/SingleServiceCamelTest.groovy delete mode 100644 instrumentation/camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/SingleServiceConfig.groovy delete mode 100644 instrumentation/camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/TwoServicesConfig.groovy delete mode 100644 instrumentation/camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/TwoServicesWithDirectClientCamelTest.groovy delete mode 100644 instrumentation/camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/aws/AwsConnector.groovy delete mode 100644 instrumentation/camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/aws/AwsSpan.groovy delete mode 100644 instrumentation/camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/aws/CamelSpan.groovy delete mode 100644 instrumentation/camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/aws/CamelSpringApp.groovy delete mode 100644 instrumentation/camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/aws/S3CamelTest.groovy delete mode 100644 instrumentation/camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/aws/S3Config.groovy delete mode 100644 instrumentation/camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/aws/SnsCamelTest.groovy delete mode 100644 instrumentation/camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/aws/SnsConfig.groovy delete mode 100644 instrumentation/camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/aws/SqsCamelTest.groovy delete mode 100644 instrumentation/camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/aws/SqsConfig.groovy delete mode 100644 instrumentation/camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/decorators/CassandraTest.groovy create mode 100644 instrumentation/camel-2.20/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apachecamel/DirectCamelTest.java create mode 100644 instrumentation/camel-2.20/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apachecamel/DirectConfig.java create mode 100644 instrumentation/camel-2.20/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apachecamel/MulticastConfig.java create mode 100644 instrumentation/camel-2.20/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apachecamel/MulticastDirectCamelTest.java create mode 100644 instrumentation/camel-2.20/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apachecamel/RestCamelTest.java create mode 100644 instrumentation/camel-2.20/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apachecamel/RestConfig.java create mode 100644 instrumentation/camel-2.20/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apachecamel/SingleServiceCamelTest.java create mode 100644 instrumentation/camel-2.20/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apachecamel/SingleServiceConfig.java create mode 100644 instrumentation/camel-2.20/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apachecamel/TwoServicesConfig.java create mode 100644 instrumentation/camel-2.20/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apachecamel/TwoServicesWithDirectClientCamelTest.java create mode 100644 instrumentation/camel-2.20/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apachecamel/aws/AwsConnector.java create mode 100644 instrumentation/camel-2.20/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apachecamel/aws/AwsSpanAssertions.java create mode 100644 instrumentation/camel-2.20/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apachecamel/aws/CamelSpanAssertions.java create mode 100644 instrumentation/camel-2.20/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apachecamel/aws/CamelSpringApplication.java create mode 100644 instrumentation/camel-2.20/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apachecamel/aws/S3CamelTest.java create mode 100644 instrumentation/camel-2.20/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apachecamel/aws/S3Config.java create mode 100644 instrumentation/camel-2.20/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apachecamel/aws/SnsCamelTest.java create mode 100644 instrumentation/camel-2.20/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apachecamel/aws/SnsConfig.java create mode 100644 instrumentation/camel-2.20/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apachecamel/aws/SqsCamelTest.java create mode 100644 instrumentation/camel-2.20/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apachecamel/aws/SqsConfig.java rename instrumentation/camel-2.20/javaagent/src/test/{groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/decorators/CassandraConfig.groovy => java/io/opentelemetry/javaagent/instrumentation/apachecamel/decorators/CassandraConfig.java} (51%) create mode 100644 instrumentation/camel-2.20/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apachecamel/decorators/CassandraTest.java create mode 100644 instrumentation/cassandra/cassandra-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cassandra/v3_0/CassandraAttributesExtractor.java delete mode 100644 instrumentation/cassandra/cassandra-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cassandra/v3_0/CassandraNetAttributesGetter.java create mode 100644 instrumentation/cassandra/cassandra-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cassandra/v3_0/CassandraNetworkAttributesGetter.java rename instrumentation/cassandra/cassandra-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cassandra/v4_0/{CassandraNetAttributesGetter.java => CassandraNetworkAttributesGetter.java} (66%) rename instrumentation/cassandra/cassandra-4.0/javaagent/src/test/java/{ => io/opentelemetry/javaagent/instrumentation/cassandra/v4_0}/CassandraTest.java (82%) delete mode 100644 instrumentation/cassandra/cassandra-4.4/library/src/main/java/io/opentelemetry/instrumentation/cassandra/v4_4/CassandraNetAttributesGetter.java create mode 100644 instrumentation/cassandra/cassandra-4.4/library/src/main/java/io/opentelemetry/instrumentation/cassandra/v4_4/CassandraNetworkAttributesGetter.java delete mode 100644 instrumentation/cdi-testing/src/test/groovy/CDIContainerTest.groovy create mode 100644 instrumentation/cdi-testing/src/test/java/io/opentelemetry/test/cdi/CdiContainerTest.java rename instrumentation/cdi-testing/src/test/java/{ => io/opentelemetry/test/cdi}/TestBean.java (88%) create mode 100644 instrumentation/clickhouse-client-0.5/javaagent/build.gradle.kts create mode 100644 instrumentation/clickhouse-client-0.5/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/clickhouse/ClickHouseAttributesGetter.java create mode 100644 instrumentation/clickhouse-client-0.5/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/clickhouse/ClickHouseClientInstrumentation.java create mode 100644 instrumentation/clickhouse-client-0.5/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/clickhouse/ClickHouseDbRequest.java create mode 100644 instrumentation/clickhouse-client-0.5/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/clickhouse/ClickHouseInstrumentationModule.java create mode 100644 instrumentation/clickhouse-client-0.5/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/clickhouse/ClickHouseNetworkAttributesGetter.java create mode 100644 instrumentation/clickhouse-client-0.5/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/clickhouse/ClickHouseSingletons.java create mode 100644 instrumentation/clickhouse-client-0.5/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/clickhouse/ClickHouseClientTest.java delete mode 100644 instrumentation/couchbase/couchbase-2-common/javaagent-unit-tests/src/test/groovy/io/opentelemetry/javaagent/instrumentation/couchbase/v2_0/CouchbaseQuerySanitizerTest.groovy create mode 100644 instrumentation/couchbase/couchbase-2-common/javaagent-unit-tests/src/test/java/io/opentelemetry/javaagent/instrumentation/couchbase/v2_0/CouchbaseQuerySanitizerTest.java rename instrumentation/couchbase/couchbase-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/couchbase/v2_0/{CouchbaseNetAttributesGetter.java => CouchbaseNetworkAttributesGetter.java} (52%) delete mode 100644 instrumentation/couchbase/couchbase-3.1/javaagent/src/test/groovy/CouchbaseClient31Test.groovy create mode 100644 instrumentation/couchbase/couchbase-3.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/couchbase/v3_1/CouchbaseClient31Test.java delete mode 100644 instrumentation/couchbase/couchbase-3.2/javaagent/src/test/groovy/CouchbaseClient32Test.groovy create mode 100644 instrumentation/couchbase/couchbase-3.2/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/couchbase/v3_2/CouchbaseClient32Test.java delete mode 100644 instrumentation/dropwizard/dropwizard-views-0.7/javaagent/src/test/groovy/ViewRenderTest.groovy create mode 100644 instrumentation/dropwizard/dropwizard-views-0.7/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/dropwizardviews/ViewRenderTest.java create mode 100644 instrumentation/elasticsearch/elasticsearch-api-client-7.16/javaagent-unit-tests/build.gradle.kts create mode 100644 instrumentation/elasticsearch/elasticsearch-api-client-7.16/javaagent-unit-tests/src/test/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/rest/ElasticsearchEndpointMapTest.java create mode 100644 instrumentation/elasticsearch/elasticsearch-api-client-7.16/javaagent/build.gradle.kts create mode 100644 instrumentation/elasticsearch/elasticsearch-api-client-7.16/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/apiclient/ElasticsearchApiClientInstrumentationModule.java create mode 100644 instrumentation/elasticsearch/elasticsearch-api-client-7.16/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/apiclient/ElasticsearchEndpointMap.java create mode 100644 instrumentation/elasticsearch/elasticsearch-api-client-7.16/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/apiclient/EndpointId.java create mode 100644 instrumentation/elasticsearch/elasticsearch-api-client-7.16/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/apiclient/RestClientHttpClientInstrumentation.java create mode 100644 instrumentation/elasticsearch/elasticsearch-api-client-7.16/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/apiclient/RestClientTransportInstrumentation.java create mode 100644 instrumentation/elasticsearch/elasticsearch-api-client-7.16/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/apiclient/ElasticsearchClientTest.java rename instrumentation/elasticsearch/elasticsearch-rest-5.0/javaagent/src/test/java/{ => io/opentelemetry/javaagent/instrumentation/elasticsearch/rest/v5_0}/ElasticsearchRest5Test.java (71%) rename instrumentation/elasticsearch/elasticsearch-rest-6.4/javaagent/src/test/java/{ => io/opentelemetry/javaagent/instrumentation/elasticsearch/rest/v6_4}/ElasticsearchRest6Test.java (69%) delete mode 100644 instrumentation/elasticsearch/elasticsearch-rest-7.0/javaagent/src/test/groovy/ElasticsearchRest7Test.groovy create mode 100644 instrumentation/elasticsearch/elasticsearch-rest-7.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/rest/v7_0/ElasticsearchRest7Test.java create mode 100644 instrumentation/elasticsearch/elasticsearch-rest-7.0/library/build.gradle.kts create mode 100644 instrumentation/elasticsearch/elasticsearch-rest-7.0/library/src/main/java/io/opentelemetry/instrumentation/elasticsearch/rest/v7_0/ElasticsearchRest7Telemetry.java create mode 100644 instrumentation/elasticsearch/elasticsearch-rest-7.0/library/src/main/java/io/opentelemetry/instrumentation/elasticsearch/rest/v7_0/ElasticsearchRest7TelemetryBuilder.java create mode 100644 instrumentation/elasticsearch/elasticsearch-rest-7.0/library/src/main/java/io/opentelemetry/instrumentation/elasticsearch/rest/v7_0/RestClientWrapper.java create mode 100644 instrumentation/elasticsearch/elasticsearch-rest-7.0/library/src/test/java/io/opentelemetry/instrumentation/elasticsearch/rest/v7_0/ElasticsearchRest7Test.java delete mode 100644 instrumentation/elasticsearch/elasticsearch-rest-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/rest/ElasticsearchRestAttributesGetter.java delete mode 100644 instrumentation/elasticsearch/elasticsearch-rest-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/rest/ElasticsearchRestInstrumenterFactory.java create mode 100644 instrumentation/elasticsearch/elasticsearch-rest-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/rest/ElasticsearchRestJavaagentInstrumenterFactory.java delete mode 100644 instrumentation/elasticsearch/elasticsearch-rest-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/rest/ElasticsearchRestNetResponseAttributesGetter.java delete mode 100644 instrumentation/elasticsearch/elasticsearch-rest-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/rest/ElasticsearchRestRequest.java create mode 100644 instrumentation/elasticsearch/elasticsearch-rest-common/library/build.gradle.kts create mode 100644 instrumentation/elasticsearch/elasticsearch-rest-common/library/src/main/java/io/opentelemetry/instrumentation/elasticsearch/rest/internal/ElasticsearchClientAttributeExtractor.java create mode 100644 instrumentation/elasticsearch/elasticsearch-rest-common/library/src/main/java/io/opentelemetry/instrumentation/elasticsearch/rest/internal/ElasticsearchDbAttributesGetter.java create mode 100644 instrumentation/elasticsearch/elasticsearch-rest-common/library/src/main/java/io/opentelemetry/instrumentation/elasticsearch/rest/internal/ElasticsearchEndpointDefinition.java create mode 100644 instrumentation/elasticsearch/elasticsearch-rest-common/library/src/main/java/io/opentelemetry/instrumentation/elasticsearch/rest/internal/ElasticsearchRestInstrumenterFactory.java create mode 100644 instrumentation/elasticsearch/elasticsearch-rest-common/library/src/main/java/io/opentelemetry/instrumentation/elasticsearch/rest/internal/ElasticsearchRestRequest.java create mode 100644 instrumentation/elasticsearch/elasticsearch-rest-common/library/src/main/java/io/opentelemetry/instrumentation/elasticsearch/rest/internal/ElasticsearchSpanNameExtractor.java rename instrumentation/elasticsearch/elasticsearch-rest-common/{javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/rest => library/src/main/java/io/opentelemetry/instrumentation/elasticsearch/rest/internal}/RestResponseListener.java (88%) rename instrumentation/elasticsearch/elasticsearch-transport-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/transport/v6_0/{Elasticsearch6TransportNetAttributesGetter.java => Elasticsearch6TransportNetworkAttributesGetter.java} (55%) delete mode 100644 instrumentation/elasticsearch/elasticsearch-transport-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/transport/ElasticTransportNetResponseAttributesGetter.java create mode 100644 instrumentation/elasticsearch/elasticsearch-transport-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/transport/ElasticTransportNetworkAttributesGetter.java create mode 100644 instrumentation/executors/bootstrap/src/main/java/io/opentelemetry/javaagent/bootstrap/executors/ContextPropagatingRunnable.java delete mode 100644 instrumentation/executors/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/executors/AbstractExecutorInstrumentation.java create mode 100644 instrumentation/executors/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/executors/ExecutorMatchers.java create mode 100644 instrumentation/executors/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/executors/StructuredTaskScopeInstrumentation.java create mode 100644 instrumentation/executors/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/executors/ThreadPoolExtendingExecutorInstrumentation.java create mode 100644 instrumentation/executors/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/executors/VirtualThreadInstrumentation.java create mode 100644 instrumentation/executors/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/executors/LambdaContextPropagationTest.java create mode 100644 instrumentation/executors/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/executors/ThreadPoolExecutorTest.java create mode 100644 instrumentation/executors/jdk21-testing/build.gradle.kts create mode 100644 instrumentation/executors/jdk21-testing/src/test/java/io/opentelemetry/javaagent/instrumentation/executors/StructuredTaskScopeTest.java create mode 100644 instrumentation/executors/jdk21-testing/src/test/java/io/opentelemetry/javaagent/instrumentation/executors/VirtualThreadExecutorTest.java create mode 100644 instrumentation/executors/jdk21-testing/src/test/java/io/opentelemetry/javaagent/instrumentation/executors/VirtualThreadTest.java rename instrumentation/executors/{javaagent/src/test => testing/src/main}/java/io/opentelemetry/javaagent/instrumentation/executors/JavaAsyncChild.java (98%) delete mode 100644 instrumentation/external-annotations/javaagent-unit-tests/src/test/groovy/IncludeTest.groovy create mode 100644 instrumentation/external-annotations/javaagent-unit-tests/src/test/java/io/opentelemetry/javaagent/instrumentation/extannotations/IncludeTest.java delete mode 100644 instrumentation/external-annotations/javaagent/src/test/groovy/ConfiguredTraceAnnotationsTest.groovy delete mode 100644 instrumentation/external-annotations/javaagent/src/test/groovy/TraceAnnotationsTest.groovy delete mode 100644 instrumentation/external-annotations/javaagent/src/test/groovy/TraceProvidersTest.groovy delete mode 100644 instrumentation/external-annotations/javaagent/src/test/groovy/TracedMethodsExclusionTest.groovy create mode 100644 instrumentation/external-annotations/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/extannotations/ConfiguredTraceAnnotationsTest.java rename instrumentation/external-annotations/javaagent/src/test/java/{ => io/opentelemetry/javaagent/instrumentation/extannotations}/OuterClass.java (85%) rename instrumentation/external-annotations/javaagent/src/test/java/io/opentelemetry/{test/annotation => javaagent/instrumentation/extannotations}/SayTracedHello.java (97%) create mode 100644 instrumentation/external-annotations/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/extannotations/TraceAnnotationsTest.java create mode 100644 instrumentation/external-annotations/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/extannotations/TraceProvidersTest.java create mode 100644 instrumentation/external-annotations/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/extannotations/TracedMethodsExclusionTest.java create mode 100644 instrumentation/finagle-http-23.11/javaagent/build.gradle.kts create mode 100644 instrumentation/finagle-http-23.11/javaagent/src/main/java/com/twitter/finagle/ChannelTransportHelpers.java create mode 100644 instrumentation/finagle-http-23.11/javaagent/src/main/java/io/netty/channel/OpenTelemetryChannelInitializerDelegate.java create mode 100644 instrumentation/finagle-http-23.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/finaglehttp/v23_11/ChannelTransportInstrumentation.java create mode 100644 instrumentation/finagle-http-23.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/finaglehttp/v23_11/FinagleHttpInstrumentationModule.java create mode 100644 instrumentation/finagle-http-23.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/finaglehttp/v23_11/Function1Wrapper.java create mode 100644 instrumentation/finagle-http-23.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/finaglehttp/v23_11/GenStreamingServerDispatcherInstrumentation.java create mode 100644 instrumentation/finagle-http-23.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/finaglehttp/v23_11/H2StreamChannelInitInstrumentation.java create mode 100644 instrumentation/finagle-http-23.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/finaglehttp/v23_11/Helpers.java create mode 100644 instrumentation/finagle-http-23.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/finaglehttp/v23_11/LocalSchedulerActivationInstrumentation.java create mode 100644 instrumentation/finagle-http-23.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/finaglehttp/v23_11/PromiseMonitoredInstrumentation.java create mode 100644 instrumentation/finagle-http-23.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/finaglehttp/v23_11/TwitterUtilCoreInstrumentationModule.java create mode 100644 instrumentation/finagle-http-23.11/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/finaglehttp/v23_11/AbstractServerTest.java create mode 100644 instrumentation/finagle-http-23.11/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/finaglehttp/v23_11/ClientTest.java create mode 100644 instrumentation/finagle-http-23.11/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/finaglehttp/v23_11/ServerH1Test.java create mode 100644 instrumentation/finagle-http-23.11/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/finaglehttp/v23_11/ServerH2Test.java create mode 100644 instrumentation/finagle-http-23.11/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/finaglehttp/v23_11/Utils.java delete mode 100644 instrumentation/google-http-client-1.19/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/googlehttpclient/GoogleHttpClientNetAttributesGetter.java delete mode 100644 instrumentation/graphql-java-12.0/javaagent/README.md delete mode 100644 instrumentation/graphql-java-12.0/javaagent/build.gradle.kts delete mode 100644 instrumentation/graphql-java-12.0/library/src/main/java/io/opentelemetry/instrumentation/graphql/v12_0/GraphQLTelemetry.java create mode 100644 instrumentation/graphql-java/README.md create mode 100644 instrumentation/graphql-java/graphql-java-12.0/javaagent/build.gradle.kts rename instrumentation/{ => graphql-java}/graphql-java-12.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/graphql/v12_0/GraphqlInstrumentation.java (95%) rename instrumentation/{ => graphql-java}/graphql-java-12.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/graphql/v12_0/GraphqlInstrumentationModule.java (65%) create mode 100644 instrumentation/graphql-java/graphql-java-12.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/graphql/v12_0/GraphqlSingletons.java rename instrumentation/{ => graphql-java}/graphql-java-12.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/graphql/v12_0/GraphqlTest.java (100%) rename instrumentation/{ => graphql-java}/graphql-java-12.0/library/README.md (94%) create mode 100644 instrumentation/graphql-java/graphql-java-12.0/library/build.gradle.kts create mode 100644 instrumentation/graphql-java/graphql-java-12.0/library/src/main/java/io/opentelemetry/instrumentation/graphql/v12_0/GraphQLTelemetry.java rename instrumentation/{ => graphql-java}/graphql-java-12.0/library/src/main/java/io/opentelemetry/instrumentation/graphql/v12_0/GraphQLTelemetryBuilder.java (100%) create mode 100644 instrumentation/graphql-java/graphql-java-12.0/library/src/main/java/io/opentelemetry/instrumentation/graphql/v12_0/OpenTelemetryInstrumentation.java rename instrumentation/{ => graphql-java}/graphql-java-12.0/library/src/test/java/io/opentelemetry/instrumentation/graphql/v12_0/GraphqlTest.java (100%) create mode 100644 instrumentation/graphql-java/graphql-java-20.0/javaagent/build.gradle.kts create mode 100644 instrumentation/graphql-java/graphql-java-20.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/graphql/v20_0/GraphqlInstrumentation.java create mode 100644 instrumentation/graphql-java/graphql-java-20.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/graphql/v20_0/GraphqlInstrumentationModule.java create mode 100644 instrumentation/graphql-java/graphql-java-20.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/graphql/v20_0/GraphqlSingletons.java create mode 100644 instrumentation/graphql-java/graphql-java-20.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/graphql/v20_0/GraphqlTest.java create mode 100644 instrumentation/graphql-java/graphql-java-20.0/library/README.md create mode 100644 instrumentation/graphql-java/graphql-java-20.0/library/build.gradle.kts create mode 100644 instrumentation/graphql-java/graphql-java-20.0/library/src/main/java/io/opentelemetry/instrumentation/graphql/v20_0/GraphQLTelemetry.java create mode 100644 instrumentation/graphql-java/graphql-java-20.0/library/src/main/java/io/opentelemetry/instrumentation/graphql/v20_0/GraphQLTelemetryBuilder.java create mode 100644 instrumentation/graphql-java/graphql-java-20.0/library/src/main/java/io/opentelemetry/instrumentation/graphql/v20_0/Graphql20OpenTelemetryInstrumentationState.java create mode 100644 instrumentation/graphql-java/graphql-java-20.0/library/src/main/java/io/opentelemetry/instrumentation/graphql/v20_0/GraphqlDataFetcherAttributesExtractor.java create mode 100644 instrumentation/graphql-java/graphql-java-20.0/library/src/main/java/io/opentelemetry/instrumentation/graphql/v20_0/GraphqlInstrumenterFactory.java create mode 100644 instrumentation/graphql-java/graphql-java-20.0/library/src/main/java/io/opentelemetry/instrumentation/graphql/v20_0/OpenTelemetryInstrumentation.java create mode 100644 instrumentation/graphql-java/graphql-java-20.0/library/src/test/java/io/opentelemetry/instrumentation/graphql/v20_0/GraphqlTest.java create mode 100644 instrumentation/graphql-java/graphql-java-20.0/library/src/test/java/io/opentelemetry/instrumentation/graphql/v20_0/OpenTelemetryInstrumentationStateTest.java rename instrumentation/{graphql-java-12.0 => graphql-java/graphql-java-common}/library/build.gradle.kts (59%) rename instrumentation/{graphql-java-12.0/library/src/main/java/io/opentelemetry/instrumentation/graphql/v12_0 => graphql-java/graphql-java-common/library/src/main/java/io/opentelemetry/instrumentation/graphql/internal}/GraphqlAttributesExtractor.java (50%) rename instrumentation/{graphql-java-12.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/graphql/v12_0/GraphqlSingletons.java => graphql-java/graphql-java-common/library/src/main/java/io/opentelemetry/instrumentation/graphql/internal/InstrumentationUtil.java} (54%) rename instrumentation/{graphql-java-12.0/library/src/main/java/io/opentelemetry/instrumentation/graphql/v12_0/OpenTelemetryInstrumentation.java => graphql-java/graphql-java-common/library/src/main/java/io/opentelemetry/instrumentation/graphql/internal/OpenTelemetryInstrumentationHelper.java} (61%) rename instrumentation/{graphql-java-12.0/library/src/main/java/io/opentelemetry/instrumentation/graphql/v12_0 => graphql-java/graphql-java-common/library/src/main/java/io/opentelemetry/instrumentation/graphql/internal}/OpenTelemetryInstrumentationState.java (51%) rename instrumentation/{graphql-java-12.0 => graphql-java/graphql-java-common}/testing/build.gradle.kts (100%) rename instrumentation/{graphql-java-12.0 => graphql-java/graphql-java-common}/testing/src/main/java/io/opentelemetry/instrumentation/graphql/AbstractGraphqlTest.java (68%) rename instrumentation/{graphql-java-12.0 => graphql-java/graphql-java-common}/testing/src/main/resources/schema.graphqls (100%) delete mode 100644 instrumentation/grizzly-2.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/grizzly/GrizzlyNetAttributesGetter.java delete mode 100644 instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/GrpcHelper.java rename instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/{internal/GrpcNetServerAttributesGetter.java => GrpcNetworkServerAttributesGetter.java} (62%) rename instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/internal/{GrpcNetClientAttributesGetter.java => GrpcClientNetworkAttributesGetter.java} (71%) create mode 100644 instrumentation/grpc-1.6/library/src/test/java/io/opentelemetry/instrumentation/grpc/v1_6/MetadataSetterTest.java delete mode 100644 instrumentation/hibernate/hibernate-3.3/javaagent/src/test/groovy/AbstractHibernateTest.groovy delete mode 100644 instrumentation/hibernate/hibernate-3.3/javaagent/src/test/groovy/CriteriaTest.groovy delete mode 100644 instrumentation/hibernate/hibernate-3.3/javaagent/src/test/groovy/QueryTest.groovy delete mode 100644 instrumentation/hibernate/hibernate-3.3/javaagent/src/test/groovy/SessionTest.groovy create mode 100644 instrumentation/hibernate/hibernate-3.3/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/hibernate/v3_3/AbstractHibernateTest.java create mode 100644 instrumentation/hibernate/hibernate-3.3/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/hibernate/v3_3/CriteriaTest.java create mode 100644 instrumentation/hibernate/hibernate-3.3/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/hibernate/v3_3/QueryTest.java create mode 100644 instrumentation/hibernate/hibernate-3.3/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/hibernate/v3_3/SessionTest.java rename instrumentation/hibernate/hibernate-3.3/javaagent/src/test/java/{ => io/opentelemetry/javaagent/instrumentation/hibernate/v3_3}/Value.java (92%) delete mode 100644 instrumentation/hibernate/hibernate-4.0/javaagent/src/test/groovy/AbstractHibernateTest.groovy delete mode 100644 instrumentation/hibernate/hibernate-4.0/javaagent/src/test/groovy/CriteriaTest.groovy delete mode 100644 instrumentation/hibernate/hibernate-4.0/javaagent/src/test/groovy/EntityManagerTest.groovy delete mode 100644 instrumentation/hibernate/hibernate-4.0/javaagent/src/test/groovy/QueryTest.groovy delete mode 100644 instrumentation/hibernate/hibernate-4.0/javaagent/src/test/groovy/SessionTest.groovy delete mode 100644 instrumentation/hibernate/hibernate-4.0/javaagent/src/test/groovy/SpringJpaTest.groovy create mode 100644 instrumentation/hibernate/hibernate-4.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/hibernate/v4_0/AbstractHibernateTest.java create mode 100644 instrumentation/hibernate/hibernate-4.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/hibernate/v4_0/CriteriaTest.java create mode 100644 instrumentation/hibernate/hibernate-4.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/hibernate/v4_0/EntityManagerTest.java create mode 100644 instrumentation/hibernate/hibernate-4.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/hibernate/v4_0/QueryTest.java create mode 100644 instrumentation/hibernate/hibernate-4.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/hibernate/v4_0/SessionTest.java rename instrumentation/hibernate/hibernate-4.0/javaagent/src/test/java/{ => io/opentelemetry/javaagent/instrumentation/hibernate/v4_0}/Value.java (92%) create mode 100644 instrumentation/hibernate/hibernate-4.0/javaagent/src/test/java/spring/jpa/SpringJpaTest.java create mode 100644 instrumentation/hibernate/hibernate-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/HibernateOperationScope.java delete mode 100644 instrumentation/hibernate/hibernate-procedure-call-4.3/javaagent/src/test/groovy/ProcedureCallTest.groovy create mode 100644 instrumentation/hibernate/hibernate-procedure-call-4.3/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/hibernate/v4_3/ProcedureCallTest.java rename instrumentation/hibernate/hibernate-procedure-call-4.3/javaagent/src/test/java/{ => io/opentelemetry/javaagent/instrumentation/hibernate/v4_3}/Value.java (92%) create mode 100644 instrumentation/hibernate/hibernate-reactive-1.0/javaagent/build.gradle.kts create mode 100644 instrumentation/hibernate/hibernate-reactive-1.0/javaagent/src/hibernateReactive1Test/java/io/opentelemetry/javaagent/instrumentation/hibernate/reactive/v1_0/HibernateReactiveTest.java create mode 100644 instrumentation/hibernate/hibernate-reactive-1.0/javaagent/src/hibernateReactive1Test/java/io/opentelemetry/javaagent/instrumentation/hibernate/reactive/v1_0/Value.java create mode 100644 instrumentation/hibernate/hibernate-reactive-1.0/javaagent/src/hibernateReactive1Test/resources/META-INF/persistence.xml create mode 100644 instrumentation/hibernate/hibernate-reactive-1.0/javaagent/src/hibernateReactive2Test/java/io/opentelemetry/javaagent/instrumentation/hibernate/reactive/v2_0/HibernateReactiveTest.java create mode 100644 instrumentation/hibernate/hibernate-reactive-1.0/javaagent/src/hibernateReactive2Test/java/io/opentelemetry/javaagent/instrumentation/hibernate/reactive/v2_0/Value.java create mode 100644 instrumentation/hibernate/hibernate-reactive-1.0/javaagent/src/hibernateReactive2Test/resources/META-INF/persistence.xml create mode 100644 instrumentation/hibernate/hibernate-reactive-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/reactive/v1_0/mutiny/ContextOperator.java create mode 100644 instrumentation/hibernate/hibernate-reactive-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/reactive/v1_0/mutiny/HibernateReactiveMutinyInstrumentationModule.java create mode 100644 instrumentation/hibernate/hibernate-reactive-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/reactive/v1_0/mutiny/MutinySessionFactoryInstrumentation.java create mode 100644 instrumentation/hibernate/hibernate-reactive-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/reactive/v1_0/stage/CompletionStageWrapper.java create mode 100644 instrumentation/hibernate/hibernate-reactive-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/reactive/v1_0/stage/FunctionWrapper.java create mode 100644 instrumentation/hibernate/hibernate-reactive-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/reactive/v1_0/stage/HibernateReactiveStageInstrumentationModule.java create mode 100644 instrumentation/hibernate/hibernate-reactive-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/reactive/v1_0/stage/StageSessionFactoryInstrumentation.java create mode 100644 instrumentation/hibernate/hibernate-reactive-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/reactive/v1_0/stage/StageSessionImplInstrumentation.java delete mode 100644 instrumentation/http-url-connection/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/httpurlconnection/HttpUrlNetAttributesGetter.java delete mode 100644 instrumentation/http-url-connection/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/httpurlconnection/UrlConnectionTest.java delete mode 100644 instrumentation/hystrix-1.4/javaagent/src/test/groovy/HystrixObservableChainTest.groovy delete mode 100644 instrumentation/hystrix-1.4/javaagent/src/test/groovy/HystrixObservableTest.groovy delete mode 100644 instrumentation/hystrix-1.4/javaagent/src/test/groovy/HystrixTest.groovy create mode 100644 instrumentation/hystrix-1.4/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/hystrix/HystrixObservableChainTest.java create mode 100644 instrumentation/hystrix-1.4/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/hystrix/HystrixObservableTest.java create mode 100644 instrumentation/hystrix-1.4/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/hystrix/HystrixTest.java create mode 100644 instrumentation/influxdb-2.4/javaagent/build.gradle.kts create mode 100644 instrumentation/influxdb-2.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/influxdb/v2_4/InfluxDbAttributesGetter.java create mode 100644 instrumentation/influxdb-2.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/influxdb/v2_4/InfluxDbImplInstrumentation.java create mode 100644 instrumentation/influxdb-2.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/influxdb/v2_4/InfluxDbInstrumentationModule.java create mode 100644 instrumentation/influxdb-2.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/influxdb/v2_4/InfluxDbNetworkAttributesGetter.java create mode 100644 instrumentation/influxdb-2.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/influxdb/v2_4/InfluxDbObjetWrapper.java create mode 100644 instrumentation/influxdb-2.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/influxdb/v2_4/InfluxDbRequest.java create mode 100644 instrumentation/influxdb-2.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/influxdb/v2_4/InfluxDbScope.java create mode 100644 instrumentation/influxdb-2.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/influxdb/v2_4/InfluxDbSingletons.java create mode 100644 instrumentation/influxdb-2.4/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/influxdb/v2_4/InfluxDbClientTest.java create mode 100644 instrumentation/influxdb-2.4/javaagent/src/test24/java/io/opentelemetry/javaagent/instrumentation/influxdb/v2_4/InfluxDbClient24Test.java delete mode 100644 instrumentation/internal/internal-class-loader/javaagent/src/test/groovy/ClassLoadingTest.groovy delete mode 100644 instrumentation/internal/internal-class-loader/javaagent/src/test/groovy/JBossClassloadingTest.groovy delete mode 100644 instrumentation/internal/internal-class-loader/javaagent/src/test/groovy/OSGIClassloadingTest.groovy delete mode 100644 instrumentation/internal/internal-class-loader/javaagent/src/test/groovy/TomcatClassloadingTest.groovy create mode 100644 instrumentation/internal/internal-class-loader/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/internal/classloader/ClassLoadingTest.java create mode 100644 instrumentation/internal/internal-class-loader/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/internal/classloader/JbossClassloadingTest.java create mode 100644 instrumentation/internal/internal-class-loader/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/internal/classloader/OsgiClassloadingTest.java create mode 100644 instrumentation/internal/internal-class-loader/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/internal/classloader/TomcatClassloadingTest.java delete mode 100644 instrumentation/internal/internal-lambda/javaagent/src/test/groovy/LambdaInstrumentationTest.groovy create mode 100644 instrumentation/internal/internal-lambda/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/internal/lambda/LambdaInstrumentationTest.java rename instrumentation/internal/internal-lambda/javaagent/src/test/java/{ => io/opentelemetry/javaagent/instrumentation/internal/lambda}/TestLambda.java (74%) delete mode 100644 instrumentation/internal/internal-reflection/javaagent-integration-tests/src/test/groovy/ReflectionTest.groovy create mode 100644 instrumentation/internal/internal-reflection/javaagent-integration-tests/src/test/java/ReflectionTest.java delete mode 100644 instrumentation/internal/internal-url-class-loader/javaagent-integration-tests/src/test/groovy/AddUrlTest.groovy create mode 100644 instrumentation/internal/internal-url-class-loader/javaagent-integration-tests/src/test/java/AddUrlTest.java create mode 100644 instrumentation/java-http-client/library/src/main/java/io/opentelemetry/instrumentation/httpclient/internal/JavaHttpClientInstrumenterBuilderFactory.java delete mode 100644 instrumentation/java-http-client/library/src/main/java/io/opentelemetry/instrumentation/httpclient/internal/JavaHttpClientInstrumenterFactory.java delete mode 100644 instrumentation/java-http-client/library/src/main/java/io/opentelemetry/instrumentation/httpclient/internal/JavaHttpClientNetAttributesGetter.java create mode 100644 instrumentation/javalin-5.0/javaagent/build.gradle.kts create mode 100644 instrumentation/javalin-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/javalin/v5_0/JavalinInstrumentation.java create mode 100644 instrumentation/javalin-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/javalin/v5_0/JavalinInstrumentationModule.java create mode 100644 instrumentation/javalin-5.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/javalin/v5_0/JavalinTest.java create mode 100644 instrumentation/javalin-5.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/javalin/v5_0/TestJavalinJavaApplication.java create mode 100644 instrumentation/jaxrs-client/jaxrs-client-1.1-testing/build.gradle.kts rename instrumentation/jaxrs-client/{jaxrs-client-1.1/javaagent => jaxrs-client-1.1-testing}/src/test/groovy/JaxRsClientV1Test.groovy (60%) delete mode 100644 instrumentation/jaxrs-client/jaxrs-client-1.1/javaagent/build.gradle.kts delete mode 100644 instrumentation/jaxrs-client/jaxrs-client-1.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrsclient/v1_1/ClientHandlerInstrumentation.java delete mode 100644 instrumentation/jaxrs-client/jaxrs-client-1.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrsclient/v1_1/ClientRequestHeaderSetter.java delete mode 100644 instrumentation/jaxrs-client/jaxrs-client-1.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrsclient/v1_1/JaxRsClientHttpAttributesGetter.java delete mode 100644 instrumentation/jaxrs-client/jaxrs-client-1.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrsclient/v1_1/JaxRsClientNetAttributesGetter.java delete mode 100644 instrumentation/jaxrs-client/jaxrs-client-1.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrsclient/v1_1/JaxRsClientSingletons.java delete mode 100644 instrumentation/jaxws/jaxws-2.0-cxf-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cxf/CxfServerSpanNaming.java rename instrumentation/jaxws/{jaxws-2.0-metro-2.2/javaagent => jaxws-2.0-metro-2.2-testing}/build.gradle.kts (63%) rename instrumentation/jaxws/{jaxws-2.0-metro-2.2/javaagent => jaxws-2.0-metro-2.2-testing}/src/test/groovy/MetroJaxWsTest.groovy (100%) rename instrumentation/jaxws/{jaxws-2.0-metro-2.2/javaagent => jaxws-2.0-metro-2.2-testing}/src/test/resources/test-app/WEB-INF/sun-jaxws.xml (100%) rename instrumentation/jaxws/{jaxws-2.0-metro-2.2/javaagent => jaxws-2.0-metro-2.2-testing}/src/test/resources/test-app/WEB-INF/web.xml (100%) delete mode 100644 instrumentation/jaxws/jaxws-2.0-metro-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/metro/MetroServerSpanNaming.java create mode 100644 instrumentation/jaxws/jaxws-3.0-common-testing/build.gradle.kts create mode 100644 instrumentation/jaxws/jaxws-3.0-common-testing/src/main/groovy/AbstractJaxWsTest.groovy create mode 100644 instrumentation/jaxws/jaxws-3.0-common-testing/src/main/groovy/hello/BaseHelloService.groovy create mode 100644 instrumentation/jaxws/jaxws-3.0-common-testing/src/main/groovy/hello/HelloService.groovy create mode 100644 instrumentation/jaxws/jaxws-3.0-common-testing/src/main/groovy/hello/HelloServiceImpl.groovy create mode 100644 instrumentation/jaxws/jaxws-3.0-common-testing/src/main/schema/hello.xsd create mode 100644 instrumentation/jaxws/jaxws-3.0-cxf-4.0-testing/build.gradle.kts rename instrumentation/jaxws/{jaxws-2.0-cxf-3.0/javaagent => jaxws-3.0-cxf-4.0-testing}/src/test/groovy/CxfJaxWsTest.groovy (100%) create mode 100644 instrumentation/jaxws/jaxws-3.0-cxf-4.0-testing/src/test/groovy/TestWsServlet.groovy rename instrumentation/jaxws/{jaxws-2.0-cxf-3.0/javaagent => jaxws-3.0-cxf-4.0-testing}/src/test/resources/test-app/WEB-INF/web.xml (100%) create mode 100644 instrumentation/jaxws/jaxws-3.0-metro-2.2-testing/build.gradle.kts rename instrumentation/{jsf/jsf-myfaces-3.0/javaagent/src/test/groovy/Myfaces3Test.groovy => jaxws/jaxws-3.0-metro-2.2-testing/src/test/groovy/MetroJaxWsTest.groovy} (64%) create mode 100644 instrumentation/jaxws/jaxws-3.0-metro-2.2-testing/src/test/resources/test-app/WEB-INF/sun-jaxws.xml create mode 100644 instrumentation/jaxws/jaxws-3.0-metro-2.2-testing/src/test/resources/test-app/WEB-INF/web.xml rename instrumentation/jaxws/{jaxws-2.0-cxf-3.0 => jaxws-cxf-3.0}/javaagent-unit-tests/build.gradle.kts (78%) rename instrumentation/jaxws/{jaxws-2.0-cxf-3.0 => jaxws-cxf-3.0}/javaagent-unit-tests/src/test/java/io/opentelemetry/javaagent/instrumentation/cxf/TracingStartInInterceptorTest.java (100%) rename instrumentation/jaxws/{jaxws-2.0-cxf-3.0 => jaxws-cxf-3.0}/javaagent/build.gradle.kts (86%) rename instrumentation/jaxws/{jaxws-2.0-cxf-3.0 => jaxws-cxf-3.0}/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cxf/CxfHelper.java (100%) rename instrumentation/jaxws/{jaxws-2.0-cxf-3.0 => jaxws-cxf-3.0}/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cxf/CxfInstrumentationModule.java (100%) rename instrumentation/jaxws/{jaxws-2.0-cxf-3.0 => jaxws-cxf-3.0}/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cxf/CxfRequest.java (100%) create mode 100644 instrumentation/jaxws/jaxws-cxf-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cxf/CxfServerSpanNaming.java rename instrumentation/jaxws/{jaxws-2.0-cxf-3.0 => jaxws-cxf-3.0}/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cxf/CxfSingletons.java (97%) rename instrumentation/jaxws/{jaxws-2.0-cxf-3.0 => jaxws-cxf-3.0}/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cxf/JaxWsServerFactoryBeanInstrumentation.java (100%) rename instrumentation/jaxws/{jaxws-2.0-cxf-3.0 => jaxws-cxf-3.0}/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cxf/TracingEndInInterceptor.java (100%) rename instrumentation/jaxws/{jaxws-2.0-cxf-3.0 => jaxws-cxf-3.0}/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cxf/TracingOutFaultInterceptor.java (100%) rename instrumentation/jaxws/{jaxws-2.0-cxf-3.0 => jaxws-cxf-3.0}/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cxf/TracingStartInInterceptor.java (100%) rename instrumentation/{jsf/jsf-mojarra-3.0/javaagent/src/test/groovy/Mojarra3Test.groovy => jaxws/jaxws-cxf-3.0/javaagent/src/test/groovy/CxfJaxWsTest.groovy} (65%) rename instrumentation/jaxws/{jaxws-2.0-cxf-3.0 => jaxws-cxf-3.0}/javaagent/src/test/groovy/TestWsServlet.groovy (100%) create mode 100644 instrumentation/jaxws/jaxws-cxf-3.0/javaagent/src/test/resources/test-app/WEB-INF/web.xml create mode 100644 instrumentation/jaxws/jaxws-metro-2.2/javaagent/build.gradle.kts rename instrumentation/jaxws/{jaxws-2.0-metro-2.2 => jaxws-metro-2.2}/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/metro/MetroHelper.java (92%) rename instrumentation/jaxws/{jaxws-2.0-metro-2.2 => jaxws-metro-2.2}/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/metro/MetroInstrumentationModule.java (92%) rename instrumentation/jaxws/{jaxws-2.0-metro-2.2 => jaxws-metro-2.2}/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/metro/MetroRequest.java (100%) create mode 100644 instrumentation/jaxws/jaxws-metro-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/metro/MetroServerSpanNameUpdater.java rename instrumentation/jaxws/{jaxws-2.0-metro-2.2 => jaxws-metro-2.2}/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/metro/MetroSingletons.java (96%) rename instrumentation/jaxws/{jaxws-2.0-metro-2.2 => jaxws-metro-2.2}/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/metro/ServerTubeAssemblerContextInstrumentation.java (100%) rename instrumentation/jaxws/{jaxws-2.0-metro-2.2 => jaxws-metro-2.2}/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/metro/SoapFaultBuilderInstrumentation.java (100%) rename instrumentation/jaxws/{jaxws-2.0-metro-2.2 => jaxws-metro-2.2}/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/metro/TracingTube.java (100%) create mode 100644 instrumentation/jboss-logmanager/README.md create mode 100644 instrumentation/jdbc/README.md create mode 100644 instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/datasource/JdbcTelemetry.java create mode 100644 instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/datasource/JdbcTelemetryBuilder.java create mode 100644 instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/DataSourceDbAttributesExtractor.java delete mode 100644 instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/DataSourceInstrumenterFactory.java rename instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/{JdbcNetAttributesGetter.java => JdbcNetworkAttributesGetter.java} (74%) delete mode 100644 instrumentation/jdbc/library/src/test/groovy/io/opentelemetry/instrumentation/jdbc/OpenTelemetryConnectionTest.groovy delete mode 100644 instrumentation/jdbc/library/src/test/groovy/io/opentelemetry/instrumentation/jdbc/internal/JdbcConnectionUrlParserTest.groovy create mode 100644 instrumentation/jdbc/library/src/test/java/io/opentelemetry/instrumentation/jdbc/datasource/JdbcTelemetryTest.java create mode 100644 instrumentation/jdbc/library/src/test/java/io/opentelemetry/instrumentation/jdbc/internal/JdbcConnectionUrlParserTest.java create mode 100644 instrumentation/jdbc/library/src/test/java/io/opentelemetry/instrumentation/jdbc/internal/OpenTelemetryConnectionTest.java rename instrumentation/jedis/jedis-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v1_4/{JedisNetAttributesGetter.java => JedisNetworkAttributesGetter.java} (68%) create mode 100644 instrumentation/jedis/jedis-1.4/javaagent/src/version272/java/io/opentelemetry/javaagent/instrumentation/jedis/v2_7_2/JedisClientTest.java create mode 100644 instrumentation/jedis/jedis-1.4/testing/build.gradle.kts create mode 100644 instrumentation/jedis/jedis-1.4/testing/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/AbstractJedisTest.java rename instrumentation/jedis/jedis-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v3_0/{JedisNetAttributesGetter.java => JedisNetworkAttributesGetter.java} (69%) rename instrumentation/jedis/jedis-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v4_0/{JedisNetAttributesGetter.java => JedisNetworkAttributesGetter.java} (54%) create mode 100644 instrumentation/jetty-httpclient/jetty-httpclient-12.0/javaagent/build.gradle.kts create mode 100644 instrumentation/jetty-httpclient/jetty-httpclient-12.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jetty/httpclient/v12_0/JettyClient12ResponseListenersInstrumentation.java create mode 100644 instrumentation/jetty-httpclient/jetty-httpclient-12.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jetty/httpclient/v12_0/JettyHttpClient12Instrumentation.java create mode 100644 instrumentation/jetty-httpclient/jetty-httpclient-12.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jetty/httpclient/v12_0/JettyHttpClient12InstrumentationModule.java create mode 100644 instrumentation/jetty-httpclient/jetty-httpclient-12.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jetty/httpclient/v12_0/JettyHttpClientSingletons.java create mode 100644 instrumentation/jetty-httpclient/jetty-httpclient-12.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/jetty/httpclient/v12_0/JettyHttpClient12AgentTest.java create mode 100644 instrumentation/jetty-httpclient/jetty-httpclient-12.0/library/build.gradle.kts create mode 100644 instrumentation/jetty-httpclient/jetty-httpclient-12.0/library/src/main/java/io/opentelemetry/instrumentation/jetty/httpclient/v12_0/JettyClientTelemetry.java create mode 100644 instrumentation/jetty-httpclient/jetty-httpclient-12.0/library/src/main/java/io/opentelemetry/instrumentation/jetty/httpclient/v12_0/JettyClientTelemetryBuilder.java create mode 100644 instrumentation/jetty-httpclient/jetty-httpclient-12.0/library/src/main/java/io/opentelemetry/instrumentation/jetty/httpclient/v12_0/TracingHttpClient.java create mode 100644 instrumentation/jetty-httpclient/jetty-httpclient-12.0/library/src/main/java/io/opentelemetry/instrumentation/jetty/httpclient/v12_0/TracingHttpRequest.java create mode 100644 instrumentation/jetty-httpclient/jetty-httpclient-12.0/library/src/main/java/io/opentelemetry/instrumentation/jetty/httpclient/v12_0/internal/HttpHeaderSetter.java create mode 100644 instrumentation/jetty-httpclient/jetty-httpclient-12.0/library/src/main/java/io/opentelemetry/instrumentation/jetty/httpclient/v12_0/internal/JettyClientHttpAttributesGetter.java create mode 100644 instrumentation/jetty-httpclient/jetty-httpclient-12.0/library/src/main/java/io/opentelemetry/instrumentation/jetty/httpclient/v12_0/internal/JettyClientTracingListener.java create mode 100644 instrumentation/jetty-httpclient/jetty-httpclient-12.0/library/src/main/java/io/opentelemetry/instrumentation/jetty/httpclient/v12_0/internal/JettyHttpClientInstrumenterBuilderFactory.java create mode 100644 instrumentation/jetty-httpclient/jetty-httpclient-12.0/library/src/test/java/io/opentelemetry/instrumentation/jetty/httpclient/v12_0/JettyHttpClient12LibraryTest.java create mode 100644 instrumentation/jetty-httpclient/jetty-httpclient-12.0/testing/build.gradle.kts create mode 100644 instrumentation/jetty-httpclient/jetty-httpclient-12.0/testing/src/main/java/io/opentelemetry/instrumentation/jetty/httpclient/v12_0/AbstractJettyClient12Test.java delete mode 100644 instrumentation/jetty-httpclient/jetty-httpclient-9.2/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/jetty/httpclient/v9_2/JettyHttpClient9AgentTest.groovy create mode 100644 instrumentation/jetty-httpclient/jetty-httpclient-9.2/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/jetty/httpclient/v9_2/JettyHttpClient9AgentTest.java delete mode 100644 instrumentation/jetty-httpclient/jetty-httpclient-9.2/library/src/main/java/io/opentelemetry/instrumentation/jetty/httpclient/v9_2/internal/JettyClientInstrumenterBuilder.java create mode 100644 instrumentation/jetty-httpclient/jetty-httpclient-9.2/library/src/main/java/io/opentelemetry/instrumentation/jetty/httpclient/v9_2/internal/JettyClientTracingListener.java delete mode 100644 instrumentation/jetty-httpclient/jetty-httpclient-9.2/library/src/main/java/io/opentelemetry/instrumentation/jetty/httpclient/v9_2/internal/JettyHttpClient9TracingInterceptor.java create mode 100644 instrumentation/jetty-httpclient/jetty-httpclient-9.2/library/src/main/java/io/opentelemetry/instrumentation/jetty/httpclient/v9_2/internal/JettyHttpClientInstrumenterBuilderFactory.java delete mode 100644 instrumentation/jetty-httpclient/jetty-httpclient-9.2/library/src/main/java/io/opentelemetry/instrumentation/jetty/httpclient/v9_2/internal/JettyHttpClientNetAttributesGetter.java delete mode 100644 instrumentation/jetty-httpclient/jetty-httpclient-9.2/library/src/test/groovy/io/opentelemetry/instrumentation/jetty/httpclient/v9_2/JettyHttpClient9LibraryTest.groovy create mode 100644 instrumentation/jetty-httpclient/jetty-httpclient-9.2/library/src/test/java/io/opentelemetry/instrumentation/jetty/httpclient/v9_2/JettyHttpClient9LibraryTest.java delete mode 100644 instrumentation/jetty-httpclient/jetty-httpclient-9.2/testing/src/main/groovy/io/opentelemetry/instrumentation/jetty/httpclient/v9_2/AbstractJettyClient9Test.groovy create mode 100644 instrumentation/jetty-httpclient/jetty-httpclient-9.2/testing/src/main/java/io/opentelemetry/instrumentation/jetty/httpclient/v9_2/AbstractJettyClient9Test.java create mode 100644 instrumentation/jetty/jetty-12.0/javaagent/build.gradle.kts create mode 100644 instrumentation/jetty/jetty-12.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jetty/v12_0/Jetty12Helper.java create mode 100644 instrumentation/jetty/jetty-12.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jetty/v12_0/Jetty12HttpAttributesGetter.java create mode 100644 instrumentation/jetty/jetty-12.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jetty/v12_0/Jetty12IgnoredTypesConfigurer.java create mode 100644 instrumentation/jetty/jetty-12.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jetty/v12_0/Jetty12InstrumentationModule.java create mode 100644 instrumentation/jetty/jetty-12.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jetty/v12_0/Jetty12ResponseMutator.java create mode 100644 instrumentation/jetty/jetty-12.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jetty/v12_0/Jetty12ServerInstrumentation.java create mode 100644 instrumentation/jetty/jetty-12.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jetty/v12_0/Jetty12Singletons.java create mode 100644 instrumentation/jetty/jetty-12.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jetty/v12_0/Jetty12TextMapGetter.java create mode 100644 instrumentation/jetty/jetty-12.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/jetty/v12_0/Jetty12HandlerTest.java delete mode 100644 instrumentation/jms/jms-1.1/javaagent/src/jms2Test/groovy/Jms2Test.groovy create mode 100644 instrumentation/jms/jms-1.1/javaagent/src/jms2Test/java/io/opentelemetry/javaagent/instrumentation/jms/v1_1/Jms2InstrumentationTest.java delete mode 100644 instrumentation/jms/jms-1.1/javaagent/src/test/groovy/Jms1Test.groovy create mode 100644 instrumentation/jms/jms-1.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/jms/v1_1/AbstractJms1Test.java create mode 100644 instrumentation/jms/jms-1.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/jms/v1_1/Jms1InstrumentationTest.java create mode 100644 instrumentation/jms/jms-1.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/jms/v1_1/Jms1SuppressReceiveSpansTest.java create mode 100644 instrumentation/jms/jms-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/jms/v3_0/AbstractJms3Test.java create mode 100644 instrumentation/jms/jms-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/jms/v3_0/Jms3SuppressReceiveSpansTest.java create mode 100644 instrumentation/jms/jms-common/bootstrap/build.gradle.kts create mode 100644 instrumentation/jms/jms-common/bootstrap/src/main/java/io/opentelemetry/javaagent/bootstrap/jms/JmsReceiveContextHolder.java create mode 100644 instrumentation/jms/jms-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jms/JmsReceiveSpanUtil.java create mode 100644 instrumentation/jmx-metrics/javaagent/camel.md create mode 100644 instrumentation/jmx-metrics/javaagent/src/main/resources/jmx/rules/camel.yaml create mode 100644 instrumentation/jodd-http-4.2/javaagent-unit-tests/build.gradle.kts rename instrumentation/jodd-http-4.2/{javaagent => javaagent-unit-tests}/src/test/java/io/opentelemetry/javaagent/instrumentation/joddhttp/v4_2/JoddHttpHttpAttributesGetterTest.java (100%) delete mode 100644 instrumentation/jodd-http-4.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/joddhttp/v4_2/JoddHttpNetAttributesGetter.java delete mode 100644 instrumentation/jsf/jsf-jakarta-common/testing/src/main/groovy/BaseJsfTest.groovy delete mode 100644 instrumentation/jsf/jsf-jakarta-common/testing/src/main/groovy/ExceptionFilter.groovy delete mode 100644 instrumentation/jsf/jsf-jakarta-common/testing/src/main/groovy/GreetingForm.groovy create mode 100644 instrumentation/jsf/jsf-jakarta-common/testing/src/main/java/io/opentelemetry/javaagent/instrumentation/jsf/jakarta/BaseJsfTest.java create mode 100644 instrumentation/jsf/jsf-jakarta-common/testing/src/main/java/io/opentelemetry/javaagent/instrumentation/jsf/jakarta/ExceptionFilter.java create mode 100644 instrumentation/jsf/jsf-jakarta-common/testing/src/main/java/io/opentelemetry/javaagent/instrumentation/jsf/jakarta/GreetingForm.java delete mode 100644 instrumentation/jsf/jsf-javax-common/testing/src/main/groovy/BaseJsfTest.groovy delete mode 100644 instrumentation/jsf/jsf-javax-common/testing/src/main/groovy/ExceptionFilter.groovy delete mode 100644 instrumentation/jsf/jsf-javax-common/testing/src/main/groovy/GreetingForm.groovy create mode 100644 instrumentation/jsf/jsf-javax-common/testing/src/main/java/io/opentelemetry/javaagent/instrumentation/jsf/javax/BaseJsfTest.java create mode 100644 instrumentation/jsf/jsf-javax-common/testing/src/main/java/io/opentelemetry/javaagent/instrumentation/jsf/javax/ExceptionFilter.java create mode 100644 instrumentation/jsf/jsf-javax-common/testing/src/main/java/io/opentelemetry/javaagent/instrumentation/jsf/javax/GreetingForm.java delete mode 100644 instrumentation/jsf/jsf-mojarra-1.2/javaagent/src/mojarra12Test/groovy/Mojarra12Test.groovy create mode 100644 instrumentation/jsf/jsf-mojarra-1.2/javaagent/src/mojarra12Test/java/io/opentelemetry/javaagent/instrumentation/mojarra/Mojarra12Test.java delete mode 100644 instrumentation/jsf/jsf-mojarra-1.2/javaagent/src/mojarra2Test/groovy/Mojarra2Test.groovy create mode 100644 instrumentation/jsf/jsf-mojarra-1.2/javaagent/src/mojarra2Test/java/io/opentelemetry/javaagent/instrumentation/mojarra/Mojarra2Test.java create mode 100644 instrumentation/jsf/jsf-mojarra-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/mojarra/v3_0/Mojarra3Test.java delete mode 100644 instrumentation/jsf/jsf-myfaces-1.2/javaagent/src/myfaces12Test/groovy/Myfaces12Test.groovy create mode 100644 instrumentation/jsf/jsf-myfaces-1.2/javaagent/src/myfaces12Test/java/io/opentelemetry/javaagent/instrumentation/myfaces/Myfaces12Test.java delete mode 100644 instrumentation/jsf/jsf-myfaces-1.2/javaagent/src/myfaces2Test/groovy/Myfaces2Test.groovy create mode 100644 instrumentation/jsf/jsf-myfaces-1.2/javaagent/src/myfaces2Test/java/io/opentelemetry/javaagent/instrumentation/myfaces/Myfaces2Test.java create mode 100644 instrumentation/jsf/jsf-myfaces-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/myfaces/v3_0/Myfaces3Test.java delete mode 100644 instrumentation/jsp-2.3/javaagent/src/test/groovy/JspInstrumentationBasicTests.groovy delete mode 100644 instrumentation/jsp-2.3/javaagent/src/test/groovy/JspInstrumentationForwardTests.groovy create mode 100644 instrumentation/jsp-2.3/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/jsp/JspInstrumentationBasicTests.java create mode 100644 instrumentation/jsp-2.3/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/jsp/JspInstrumentationForwardTests.java create mode 100644 instrumentation/jsp-2.3/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/jsp/JspSpan.java create mode 100644 instrumentation/jsp-2.3/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/jsp/JspSpanAssertionBuilder.java create mode 100644 instrumentation/jsp-2.3/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/jsp/JspSpanAssertions.java create mode 100644 instrumentation/kafka/README.md delete mode 100644 instrumentation/kafka/kafka-clients/kafka-clients-0.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kafkaclients/v0_11/TracingIterable.java create mode 100644 instrumentation/kafka/kafka-clients/kafka-clients-0.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kafkaclients/v0_11/metrics/KafkaMetricsConsumerInstrumentation.java create mode 100644 instrumentation/kafka/kafka-clients/kafka-clients-0.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kafkaclients/v0_11/metrics/KafkaMetricsInstrumentationModule.java create mode 100644 instrumentation/kafka/kafka-clients/kafka-clients-0.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kafkaclients/v0_11/metrics/KafkaMetricsProducerInstrumentation.java create mode 100644 instrumentation/kafka/kafka-clients/kafka-clients-0.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kafkaclients/v0_11/metrics/KafkaMetricsUtil.java delete mode 100644 instrumentation/kafka/kafka-clients/kafka-clients-0.11/testing/src/main/java/org/apache/kafka/clients/consumer/KafkaConsumerAccess.java delete mode 100644 instrumentation/kafka/kafka-clients/kafka-clients-0.11/testing/src/main/java/org/apache/kafka/clients/producer/KafkaProducerAccess.java create mode 100644 instrumentation/kafka/kafka-clients/kafka-clients-2.6/library/src/test/java/io/opentelemetry/instrumentation/kafkaclients/v2_6/AbstractInterceptorsTest.java create mode 100644 instrumentation/kafka/kafka-clients/kafka-clients-2.6/library/src/test/java/io/opentelemetry/instrumentation/kafkaclients/v2_6/AbstractWrapperTest.java create mode 100644 instrumentation/kafka/kafka-clients/kafka-clients-2.6/library/src/test/java/io/opentelemetry/instrumentation/kafkaclients/v2_6/InterceptorsSuppressReceiveSpansTest.java create mode 100644 instrumentation/kafka/kafka-clients/kafka-clients-2.6/library/src/test/java/io/opentelemetry/instrumentation/kafkaclients/v2_6/WrapperSuppressReceiveSpansTest.java create mode 100644 instrumentation/kafka/kafka-clients/kafka-clients-common/library/src/main/java/io/opentelemetry/instrumentation/kafka/internal/MetricsReporterList.java create mode 100644 instrumentation/kafka/kafka-clients/kafka-clients-common/library/src/main/java/io/opentelemetry/instrumentation/kafka/internal/TracingIterable.java rename instrumentation/kafka/kafka-clients/{kafka-clients-0.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kafkaclients/v0_11 => kafka-clients-common/library/src/main/java/io/opentelemetry/instrumentation/kafka/internal}/TracingIterator.java (65%) rename instrumentation/kafka/kafka-clients/{kafka-clients-0.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kafkaclients/v0_11 => kafka-clients-common/library/src/main/java/io/opentelemetry/instrumentation/kafka/internal}/TracingList.java (78%) delete mode 100644 instrumentation/kotlinx-coroutines/javaagent/gradle.properties create mode 100644 instrumentation/kotlinx-coroutines/kotlinx-coroutines-1.0/javaagent/build.gradle.kts create mode 100644 instrumentation/kotlinx-coroutines/kotlinx-coroutines-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kotlinxcoroutines/KotlinCoroutineDispatcherInstrumentation.java rename instrumentation/kotlinx-coroutines/{ => kotlinx-coroutines-1.0}/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kotlinxcoroutines/KotlinCoroutinesInstrumentation.java (100%) rename instrumentation/kotlinx-coroutines/{ => kotlinx-coroutines-1.0}/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kotlinxcoroutines/KotlinCoroutinesInstrumentationHelper.java (100%) create mode 100644 instrumentation/kotlinx-coroutines/kotlinx-coroutines-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kotlinxcoroutines/KotlinCoroutinesInstrumentationModule.java create mode 100644 instrumentation/kotlinx-coroutines/kotlinx-coroutines-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kotlinxcoroutines/RunnableWrapper.java create mode 100644 instrumentation/kotlinx-coroutines/kotlinx-coroutines-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kotlinxcoroutines/instrumentationannotations/AnnotationInstrumentationHelper.java create mode 100644 instrumentation/kotlinx-coroutines/kotlinx-coroutines-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kotlinxcoroutines/instrumentationannotations/AnnotationInstrumentationModule.java create mode 100644 instrumentation/kotlinx-coroutines/kotlinx-coroutines-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kotlinxcoroutines/instrumentationannotations/AnnotationSingletons.java create mode 100644 instrumentation/kotlinx-coroutines/kotlinx-coroutines-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kotlinxcoroutines/instrumentationannotations/ExpandFramesClassVisitor.java create mode 100644 instrumentation/kotlinx-coroutines/kotlinx-coroutines-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kotlinxcoroutines/instrumentationannotations/KotlinCoroutinesIgnoredTypesConfigurer.java create mode 100644 instrumentation/kotlinx-coroutines/kotlinx-coroutines-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kotlinxcoroutines/instrumentationannotations/MethodRequest.java create mode 100644 instrumentation/kotlinx-coroutines/kotlinx-coroutines-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kotlinxcoroutines/instrumentationannotations/MethodRequestCodeAttributesGetter.java create mode 100644 instrumentation/kotlinx-coroutines/kotlinx-coroutines-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kotlinxcoroutines/instrumentationannotations/SpanAttributeUtil.java create mode 100644 instrumentation/kotlinx-coroutines/kotlinx-coroutines-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kotlinxcoroutines/instrumentationannotations/WithSpanInstrumentation.java create mode 100644 instrumentation/kotlinx-coroutines/kotlinx-coroutines-1.0/javaagent/src/test/kotlin/io/opentelemetry/javaagent/instrumentation/kotlinxcoroutines/ClazzWithDefaultConstructorArguments.kt rename instrumentation/kotlinx-coroutines/{ => kotlinx-coroutines-1.0}/javaagent/src/test/kotlin/io/opentelemetry/javaagent/instrumentation/kotlinxcoroutines/KotlinCoroutinesInstrumentationTest.kt (73%) create mode 100644 instrumentation/kotlinx-coroutines/kotlinx-coroutines-flow-1.3/javaagent-kotlin/build.gradle.kts create mode 100644 instrumentation/kotlinx-coroutines/kotlinx-coroutines-flow-1.3/javaagent-kotlin/src/main/kotlin/io/opentelemetry/javaagent/instrumentation/kotlinxcoroutines/flow/FlowUtil.kt rename instrumentation/kotlinx-coroutines/{ => kotlinx-coroutines-flow-1.3}/javaagent/build.gradle.kts (56%) create mode 100644 instrumentation/kotlinx-coroutines/kotlinx-coroutines-flow-1.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kotlinxcoroutines/flow/AbstractFlowInstrumentation.java create mode 100644 instrumentation/kotlinx-coroutines/kotlinx-coroutines-flow-1.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kotlinxcoroutines/flow/FlowInstrumentationHelper.java create mode 100644 instrumentation/kotlinx-coroutines/kotlinx-coroutines-flow-1.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kotlinxcoroutines/flow/KotlinCoroutinesFlowInstrumentationModule.java create mode 100644 instrumentation/kotlinx-coroutines/kotlinx-coroutines-flow-1.3/javaagent/src/test/kotlin/io/opentelemetry/javaagent/instrumentation/kotlinxcoroutines/KotlinCoroutines13InstrumentationTest.kt create mode 100644 instrumentation/kotlinx-coroutines/kotlinx-coroutines-flow-1.3/javaagent/src/test/kotlin/io/opentelemetry/javaagent/instrumentation/kotlinxcoroutines/flow/FlowWithSpanTest.kt delete mode 100644 instrumentation/ktor/ktor-1.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v1_0/KtorNetServerAttributesGetter.kt create mode 100644 instrumentation/ktor/ktor-2.0/javaagent/build.gradle.kts create mode 100644 instrumentation/ktor/ktor-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/ktor/v2_0/HttpClientInstrumentation.java create mode 100644 instrumentation/ktor/ktor-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/ktor/v2_0/KtorClientInstrumentationModule.java rename instrumentation/{kotlinx-coroutines/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kotlinxcoroutines/KotlinCoroutinesInstrumentationModule.java => ktor/ktor-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/ktor/v2_0/KtorServerInstrumentationModule.java} (67%) create mode 100644 instrumentation/ktor/ktor-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/ktor/v2_0/ServerInstrumentation.java create mode 100644 instrumentation/ktor/ktor-2.0/javaagent/src/test/java/io/opentelemetry/instrumentation/ktor/v2_0/client/KtorHttpClientTest.kt create mode 100644 instrumentation/ktor/ktor-2.0/javaagent/src/test/java/io/opentelemetry/instrumentation/ktor/v2_0/server/KtorHttpServerTest.kt delete mode 100644 instrumentation/ktor/ktor-2.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/client/KtorNetClientAttributesGetter.kt delete mode 100644 instrumentation/ktor/ktor-2.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/server/KtorNetServerAttributesGetter.kt create mode 100644 instrumentation/ktor/ktor-2.0/testing/build.gradle.kts create mode 100644 instrumentation/ktor/ktor-2.0/testing/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/client/AbstractKtorHttpClientTest.kt rename instrumentation/ktor/ktor-2.0/{library/src/test => testing/src/main}/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/client/KtorHttpClientSingleConnection.kt (100%) create mode 100644 instrumentation/ktor/ktor-2.0/testing/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/server/AbstractKtorHttpServerTest.kt delete mode 100644 instrumentation/kubernetes-client-7.0/javaagent-unit-tests/src/test/groovy/KubernetesRequestUtilsTest.groovy create mode 100644 instrumentation/kubernetes-client-7.0/javaagent-unit-tests/src/test/java/io/opentelemetry/javaagent/instrumentation/kubernetesclient/KubernetesRequestUtilsTest.java delete mode 100644 instrumentation/kubernetes-client-7.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kubernetesclient/KubernetesNetAttributesGetter.java delete mode 100644 instrumentation/kubernetes-client-7.0/javaagent/src/test/groovy/KubernetesClientTest.groovy create mode 100644 instrumentation/kubernetes-client-7.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/kubernetesclient/KubernetesClientTest.java create mode 100644 instrumentation/kubernetes-client-7.0/javaagent/src/version20Test/java/io/opentelemetry/javaagent/instrumentation/kubernetesclient/KubernetesClientVer20Test.java rename instrumentation/lettuce/lettuce-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v4_0/{LettuceConnectNetAttributesGetter.java => LettuceConnectNetworkAttributesGetter.java} (67%) delete mode 100644 instrumentation/lettuce/lettuce-4.0/javaagent/src/test/groovy/LettuceAsyncClientTest.groovy delete mode 100644 instrumentation/lettuce/lettuce-4.0/javaagent/src/test/groovy/LettuceSyncClientTest.groovy create mode 100644 instrumentation/lettuce/lettuce-4.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/lettuce/v4_0/LettuceAsyncClientTest.java create mode 100644 instrumentation/lettuce/lettuce-4.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/lettuce/v4_0/LettuceSyncClientTest.java rename instrumentation/lettuce/lettuce-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v5_0/{LettuceConnectNetAttributesGetter.java => LettuceConnectNetworkAttributesGetter.java} (67%) delete mode 100644 instrumentation/lettuce/lettuce-5.0/javaagent/src/test/groovy/LettuceAsyncClientTest.groovy delete mode 100644 instrumentation/lettuce/lettuce-5.0/javaagent/src/test/groovy/LettuceReactiveClientTest.groovy delete mode 100644 instrumentation/lettuce/lettuce-5.0/javaagent/src/test/groovy/LettuceSyncClientTest.groovy create mode 100644 instrumentation/lettuce/lettuce-5.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/lettuce/v5_0/AbstractLettuceClientTest.java create mode 100644 instrumentation/lettuce/lettuce-5.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/lettuce/v5_0/LettuceAsyncClientTest.java create mode 100644 instrumentation/lettuce/lettuce-5.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/lettuce/v5_0/LettuceReactiveClientTest.java create mode 100644 instrumentation/lettuce/lettuce-5.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/lettuce/v5_0/LettuceSyncClientTest.java delete mode 100644 instrumentation/lettuce/lettuce-5.1/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/lettuce/v5_1/LettuceAsyncClientTest.groovy delete mode 100644 instrumentation/lettuce/lettuce-5.1/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/lettuce/v5_1/LettuceReactiveClientTest.groovy delete mode 100644 instrumentation/lettuce/lettuce-5.1/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/lettuce/v5_1/LettuceSyncClientAuthTest.groovy delete mode 100644 instrumentation/lettuce/lettuce-5.1/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/lettuce/v5_1/LettuceSyncClientTest.groovy create mode 100644 instrumentation/lettuce/lettuce-5.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/lettuce/v5_1/LettuceAsyncClientTest.java create mode 100644 instrumentation/lettuce/lettuce-5.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/lettuce/v5_1/LettuceReactiveClientTest.java create mode 100644 instrumentation/lettuce/lettuce-5.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/lettuce/v5_1/LettuceSyncClientAuthTest.java create mode 100644 instrumentation/lettuce/lettuce-5.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/lettuce/v5_1/LettuceSyncClientTest.java create mode 100644 instrumentation/lettuce/lettuce-5.1/library/src/main/java/io/lettuce/core/protocol/OtelCommandArgsUtil.java delete mode 100644 instrumentation/lettuce/lettuce-5.1/library/src/main/java/io/opentelemetry/instrumentation/lettuce/v5_1/LettuceNetAttributesGetter.java create mode 100644 instrumentation/lettuce/lettuce-5.1/library/src/main/java/io/opentelemetry/instrumentation/lettuce/v5_1/LettuceServerAttributesGetter.java delete mode 100644 instrumentation/lettuce/lettuce-5.1/library/src/test/groovy/io/opentelemetry/instrumentation/lettuce/v5_1/LettuceAsyncClientTest.groovy delete mode 100644 instrumentation/lettuce/lettuce-5.1/library/src/test/groovy/io/opentelemetry/instrumentation/lettuce/v5_1/LettuceReactiveClientTest.groovy delete mode 100644 instrumentation/lettuce/lettuce-5.1/library/src/test/groovy/io/opentelemetry/instrumentation/lettuce/v5_1/LettuceSyncClientAuthTest.groovy delete mode 100644 instrumentation/lettuce/lettuce-5.1/library/src/test/groovy/io/opentelemetry/instrumentation/lettuce/v5_1/LettuceSyncClientTest.groovy create mode 100644 instrumentation/lettuce/lettuce-5.1/library/src/test/java/io/opentelemetry/instrumentation/lettuce/v5_1/LettuceAsyncClientTest.java create mode 100644 instrumentation/lettuce/lettuce-5.1/library/src/test/java/io/opentelemetry/instrumentation/lettuce/v5_1/LettuceReactiveClientTest.java create mode 100644 instrumentation/lettuce/lettuce-5.1/library/src/test/java/io/opentelemetry/instrumentation/lettuce/v5_1/LettuceSyncClientAuthTest.java create mode 100644 instrumentation/lettuce/lettuce-5.1/library/src/test/java/io/opentelemetry/instrumentation/lettuce/v5_1/LettuceSyncClientTest.java delete mode 100644 instrumentation/lettuce/lettuce-5.1/testing/src/main/groovy/io/opentelemetry/instrumentation/lettuce/v5_1/AbstractLettuceAsyncClientTest.groovy delete mode 100644 instrumentation/lettuce/lettuce-5.1/testing/src/main/groovy/io/opentelemetry/instrumentation/lettuce/v5_1/AbstractLettuceReactiveClientTest.groovy delete mode 100644 instrumentation/lettuce/lettuce-5.1/testing/src/main/groovy/io/opentelemetry/instrumentation/lettuce/v5_1/AbstractLettuceSyncClientAuthTest.groovy delete mode 100644 instrumentation/lettuce/lettuce-5.1/testing/src/main/groovy/io/opentelemetry/instrumentation/lettuce/v5_1/AbstractLettuceSyncClientTest.groovy delete mode 100644 instrumentation/lettuce/lettuce-5.1/testing/src/main/groovy/io/opentelemetry/instrumentation/lettuce/v5_1/LettuceTestUtil.groovy create mode 100644 instrumentation/lettuce/lettuce-5.1/testing/src/main/java/io/opentelemetry/instrumentation/lettuce/v5_1/AbstractLettuceAsyncClientTest.java create mode 100644 instrumentation/lettuce/lettuce-5.1/testing/src/main/java/io/opentelemetry/instrumentation/lettuce/v5_1/AbstractLettuceClientTest.java create mode 100644 instrumentation/lettuce/lettuce-5.1/testing/src/main/java/io/opentelemetry/instrumentation/lettuce/v5_1/AbstractLettuceReactiveClientTest.java create mode 100644 instrumentation/lettuce/lettuce-5.1/testing/src/main/java/io/opentelemetry/instrumentation/lettuce/v5_1/AbstractLettuceSyncClientAuthTest.java create mode 100644 instrumentation/lettuce/lettuce-5.1/testing/src/main/java/io/opentelemetry/instrumentation/lettuce/v5_1/AbstractLettuceSyncClientTest.java create mode 100644 instrumentation/lettuce/lettuce-5.1/testing/src/main/java/io/opentelemetry/instrumentation/lettuce/v5_1/LettuceTestUtil.java delete mode 100644 instrumentation/lettuce/lettuce-common/library/src/test/groovy/io/opentelemetry/instrumentation/lettuce/common/LettuceArgSplitterTest.groovy create mode 100644 instrumentation/lettuce/lettuce-common/library/src/test/java/io/opentelemetry/instrumentation/lettuce/common/LettuceArgSplitterTest.java delete mode 100644 instrumentation/liberty/liberty-dispatcher-20.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/liberty/dispatcher/LibertyDispatcherNetAttributesGetter.java create mode 100644 instrumentation/log4j/log4j-appender-2.17/library/src/main/java/io/opentelemetry/instrumentation/log4j/appender/v2_17/LogEventToReplay.java create mode 100644 instrumentation/log4j/log4j-appender-2.17/library/src/test/java/io/opentelemetry/instrumentation/log4j/appender/v2_17/AbstractOpenTelemetryAppenderTest.java create mode 100644 instrumentation/log4j/log4j-appender-2.17/library/src/test/java/io/opentelemetry/instrumentation/log4j/appender/v2_17/LogReplayOpenTelemetryAppenderTest.java delete mode 100644 instrumentation/log4j/log4j-appender-2.17/library/src/test/java/io/opentelemetry/instrumentation/log4j/appender/v2_17/OpenTelemetryAppenderConfigTest.java delete mode 100644 instrumentation/log4j/log4j-appender-2.17/library/src/test/java/io/opentelemetry/instrumentation/log4j/appender/v2_17/OpenTelemetryAppenderConfigTestBase.java delete mode 100644 instrumentation/log4j/log4j-appender-2.17/library/src/test/java/io/opentelemetry/instrumentation/log4j/appender/v2_17/OpenTelemetryAppenderConfigWithOpenTelemetryTest.java create mode 100644 instrumentation/log4j/log4j-appender-2.17/library/src/test/java/io/opentelemetry/instrumentation/log4j/appender/v2_17/OpenTelemetryAppenderTest.java rename instrumentation/log4j/log4j-appender-2.17/library/src/test/resources/{log4j2-test.xml => log4j2.xml} (70%) create mode 100644 instrumentation/log4j/log4j-context-data/log4j-context-data-2.17/javaagent/README.md delete mode 100644 instrumentation/log4j/log4j-context-data/log4j-context-data-2.17/javaagent/src/test/groovy/AutoLog4j2Test.groovy create mode 100644 instrumentation/log4j/log4j-context-data/log4j-context-data-2.17/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/log4j/contextdata/v2_17/AutoLog4j2Test.java create mode 100644 instrumentation/log4j/log4j-context-data/log4j-context-data-2.17/javaagent/src/testAddBaggage/java/io/opentelemetry/javaagent/instrumentation/log4j/contextdata/v2_17/AutoLog4jBaggageTest.java create mode 100644 instrumentation/log4j/log4j-context-data/log4j-context-data-2.17/javaagent/src/testLoggingKeys/java/io/opentelemetry/javaagent/instrumentation/log4j/contextdata/v2_17/AutoLog4jLoggingKeysTest.java delete mode 100644 instrumentation/log4j/log4j-context-data/log4j-context-data-2.17/library-autoconfigure/src/test/groovy/LibraryLog4j2Test.groovy create mode 100644 instrumentation/log4j/log4j-context-data/log4j-context-data-2.17/library-autoconfigure/src/test/java/io/opentelemetry/instrumentation/log4j/contextdata/v2_17/LibraryLog4j2BaggageTest.java create mode 100644 instrumentation/log4j/log4j-context-data/log4j-context-data-2.17/library-autoconfigure/src/test/java/io/opentelemetry/instrumentation/log4j/contextdata/v2_17/LibraryLog4j2LoggingKeysTest.java create mode 100644 instrumentation/log4j/log4j-context-data/log4j-context-data-2.17/library-autoconfigure/src/test/java/io/opentelemetry/instrumentation/log4j/contextdata/v2_17/LibraryLog4j2Test.java delete mode 100644 instrumentation/log4j/log4j-context-data/log4j-context-data-2.7/javaagent/src/test/groovy/Log4j27Test.groovy create mode 100644 instrumentation/log4j/log4j-context-data/log4j-context-data-2.7/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/log4j/contextdata/v2_7/Log4j27BaggageTest.java create mode 100644 instrumentation/log4j/log4j-context-data/log4j-context-data-2.7/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/log4j/contextdata/v2_7/Log4j27LoggingKeysTest.java create mode 100644 instrumentation/log4j/log4j-context-data/log4j-context-data-2.7/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/log4j/contextdata/v2_7/Log4j27Test.java delete mode 100644 instrumentation/log4j/log4j-context-data/log4j-context-data-common/testing/src/main/groovy/Log4j2Test.groovy create mode 100644 instrumentation/log4j/log4j-context-data/log4j-context-data-common/testing/src/main/java/io/opentelemetry/instrumentation/log4j/contextdata/Log4j2BaggageTest.java create mode 100644 instrumentation/log4j/log4j-context-data/log4j-context-data-common/testing/src/main/java/io/opentelemetry/instrumentation/log4j/contextdata/Log4j2LoggingKeysTest.java create mode 100644 instrumentation/log4j/log4j-context-data/log4j-context-data-common/testing/src/main/java/io/opentelemetry/instrumentation/log4j/contextdata/Log4j2Test.java create mode 100644 instrumentation/log4j/log4j-mdc-1.2/javaagent/README.md create mode 100644 instrumentation/logback/logback-appender-1.0/library/src/main/java/io/opentelemetry/instrumentation/logback/appender/v1_0/LoggingEventToReplay.java create mode 100644 instrumentation/logback/logback-appender-1.0/library/src/test/java/io/opentelemetry/instrumentation/logback/appender/v1_0/AbstractOpenTelemetryAppenderTest.java create mode 100644 instrumentation/logback/logback-appender-1.0/library/src/test/java/io/opentelemetry/instrumentation/logback/appender/v1_0/LogReplayOpenTelemetryAppenderTest.java delete mode 100644 instrumentation/logback/logback-appender-1.0/library/src/test/java/io/opentelemetry/instrumentation/logback/appender/v1_0/OpenTelemetryAppenderConfigTest.java create mode 100644 instrumentation/logback/logback-appender-1.0/library/src/test/java/io/opentelemetry/instrumentation/logback/appender/v1_0/OpenTelemetryAppenderTest.java delete mode 100644 instrumentation/logback/logback-mdc-1.0/javaagent/src/addBaggageTest/groovy/io/opentelemetry/javaagent/instrumentation/logback/v1_0/LogbackWithBaggageTest.groovy create mode 100644 instrumentation/logback/logback-mdc-1.0/javaagent/src/addBaggageTest/java/io/opentelemetry/javaagent/instrumentation/logback/v1_0/LogbackWithBaggageTest.java create mode 100644 instrumentation/logback/logback-mdc-1.0/javaagent/src/loggingKeysTest/java/io/opentelemetry/javaagent/instrumentation/logback/v1_0/LogbackWithLoggingKeysTest.java create mode 100644 instrumentation/logback/logback-mdc-1.0/javaagent/src/loggingKeysTest/resources/logback.xml delete mode 100644 instrumentation/logback/logback-mdc-1.0/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/logback/v1_0/LogbackTest.groovy create mode 100644 instrumentation/logback/logback-mdc-1.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/logback/v1_0/LogbackTest.java delete mode 100644 instrumentation/logback/logback-mdc-1.0/library/src/addBaggageTest/groovy/io/opentelemetry/instrumentation/logback/mdc/v1_0/LogbackWithBaggageTest.groovy create mode 100644 instrumentation/logback/logback-mdc-1.0/library/src/addBaggageTest/java/io/opentelemetry/instrumentation/logback/mdc/v1_0/LogbackWithBaggageTest.java create mode 100644 instrumentation/logback/logback-mdc-1.0/library/src/loggingKeysTest/java/io/opentelemetry/instrumentation/logback/mdc/v1_0/LogbackWithLoggingKeysTest.java create mode 100644 instrumentation/logback/logback-mdc-1.0/library/src/loggingKeysTest/resources/logback.xml delete mode 100644 instrumentation/logback/logback-mdc-1.0/library/src/test/groovy/io/opentelemetry/instrumentation/logback/mdc/v1_0/LogbackTest.groovy delete mode 100644 instrumentation/logback/logback-mdc-1.0/library/src/test/groovy/io/opentelemetry/instrumentation/logback/mdc/v1_0/internal/UnionMapTest.groovy create mode 100644 instrumentation/logback/logback-mdc-1.0/library/src/test/java/io/opentelemetry/instrumentation/logback/mdc/v1_0/LogbackTest.java create mode 100644 instrumentation/logback/logback-mdc-1.0/library/src/test/java/io/opentelemetry/instrumentation/logback/mdc/v1_0/internal/UnionMapTest.java delete mode 100644 instrumentation/logback/logback-mdc-1.0/testing/src/main/groovy/io/opentelemetry/instrumentation/logback/mdc/v1_0/AbstractLogbackTest.groovy delete mode 100644 instrumentation/logback/logback-mdc-1.0/testing/src/main/groovy/io/opentelemetry/instrumentation/logback/mdc/v1_0/AbstractLogbackWithBaggageTest.groovy create mode 100644 instrumentation/logback/logback-mdc-1.0/testing/src/main/java/io/opentelemetry/instrumentation/logback/mdc/v1_0/AbstractLogbackTest.java rename instrumentation/micrometer/micrometer-1.5/{ => javaagent}/README.md (76%) create mode 100644 instrumentation/micrometer/micrometer-1.5/library/README.md create mode 100644 instrumentation/micrometer/micrometer-1.5/library/src/main/java/io/opentelemetry/instrumentation/micrometer/v1_5/MeasurementRecorderUtil.java create mode 100644 instrumentation/mongo/README.md create mode 100644 instrumentation/mongo/mongo-3.1/library/README.md rename instrumentation/mongo/mongo-3.1/library/src/main/java/io/opentelemetry/instrumentation/mongo/v3_1/{MongoNetAttributesGetter.java => MongoNetworkAttributesGetter.java} (82%) create mode 100644 instrumentation/mongo/mongo-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mongo/v4_0/DefaultConnectionPoolTaskInstrumentation.java create mode 100644 instrumentation/mongo/mongo-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mongo/v4_0/TaskWrapper.java create mode 100644 instrumentation/mybatis-3.2/javaagent/build.gradle.kts create mode 100644 instrumentation/mybatis-3.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mybatis/v3_2/MapperMethodInstrumentation.java create mode 100644 instrumentation/mybatis-3.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mybatis/v3_2/MyBatisInstrumentationModule.java create mode 100644 instrumentation/mybatis-3.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mybatis/v3_2/MyBatisSingletons.java create mode 100644 instrumentation/mybatis-3.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mybatis/v3_2/SqlCommandInstrumentation.java create mode 100644 instrumentation/mybatis-3.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mybatis/v3_2/SqlCommandUtil.java create mode 100644 instrumentation/mybatis-3.2/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/mybatis/v3_2/MyBatisTest.java create mode 100644 instrumentation/mybatis-3.2/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/mybatis/v3_2/TestMapper.java create mode 100644 instrumentation/netty/netty-3.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v3_8/NettyScope.java delete mode 100644 instrumentation/netty/netty-3.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v3_8/client/NettyConnectNetAttributesGetter.java delete mode 100644 instrumentation/netty/netty-3.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v3_8/client/NettyNetClientAttributesGetter.java delete mode 100644 instrumentation/netty/netty-3.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v3_8/server/NettyNetServerAttributesGetter.java create mode 100644 instrumentation/netty/netty-4-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4/common/NettyScope.java delete mode 100644 instrumentation/netty/netty-4-common/library/src/main/java/io/opentelemetry/instrumentation/netty/v4/common/internal/client/NettyConnectNetAttributesGetter.java create mode 100644 instrumentation/netty/netty-4-common/library/src/main/java/io/opentelemetry/instrumentation/netty/v4/common/internal/client/NettyConnectionInstrumentationFlag.java delete mode 100644 instrumentation/netty/netty-4-common/library/src/main/java/io/opentelemetry/instrumentation/netty/v4/common/internal/client/NettyNetClientAttributesGetter.java create mode 100644 instrumentation/netty/netty-4-common/library/src/main/java/io/opentelemetry/instrumentation/netty/v4/common/internal/client/NoopConnectionInstrumenter.java create mode 100644 instrumentation/netty/netty-4-common/library/src/main/java/io/opentelemetry/instrumentation/netty/v4/common/internal/client/NoopSslInstrumenter.java delete mode 100644 instrumentation/netty/netty-4-common/library/src/main/java/io/opentelemetry/instrumentation/netty/v4/common/internal/server/NettyNetServerAttributesGetter.java delete mode 100644 instrumentation/netty/netty-4.0/javaagent/src/test/groovy/ChannelFutureTest.groovy delete mode 100644 instrumentation/netty/netty-4.0/javaagent/src/test/groovy/ChannelPipelineTest.groovy delete mode 100644 instrumentation/netty/netty-4.0/javaagent/src/test/groovy/Netty40ClientSslTest.groovy delete mode 100644 instrumentation/netty/netty-4.0/javaagent/src/test/groovy/Netty40ClientTest.groovy delete mode 100644 instrumentation/netty/netty-4.0/javaagent/src/test/groovy/Netty40ConnectionSpanTest.groovy delete mode 100644 instrumentation/netty/netty-4.0/javaagent/src/test/groovy/Netty40ServerTest.groovy create mode 100644 instrumentation/netty/netty-4.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/netty/v4_0/ChannelFutureTest.java create mode 100644 instrumentation/netty/netty-4.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/netty/v4_0/ChannelPipelineTest.java rename instrumentation/netty/netty-4.0/javaagent/src/test/{groovy => java/io/opentelemetry/javaagent/instrumentation/netty/v4_0/client}/ClientHandler.java (54%) create mode 100644 instrumentation/netty/netty-4.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/netty/v4_0/client/Netty40ClientSslTest.java create mode 100644 instrumentation/netty/netty-4.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/netty/v4_0/client/Netty40ClientTest.java create mode 100644 instrumentation/netty/netty-4.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/netty/v4_0/client/Netty40ConnectionSpanTest.java create mode 100644 instrumentation/netty/netty-4.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/netty/v4_0/server/Netty40ServerTest.java create mode 100644 instrumentation/netty/netty-4.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4_1/SingleThreadEventExecutorInstrumentation.java create mode 100644 instrumentation/netty/netty-4.1/library/src/main/java/io/opentelemetry/instrumentation/netty/v4_1/internal/ProtocolEventHandler.java create mode 100644 instrumentation/netty/netty-4.1/library/src/main/java/io/opentelemetry/instrumentation/netty/v4_1/internal/ProtocolSpecificEvent.java create mode 100644 instrumentation/netty/netty-4.1/library/src/main/java/io/opentelemetry/instrumentation/netty/v4_1/internal/ServerContext.java create mode 100644 instrumentation/netty/netty-4.1/library/src/main/java/io/opentelemetry/instrumentation/netty/v4_1/internal/ServerContexts.java create mode 100644 instrumentation/netty/netty-4.1/library/src/main/java/io/opentelemetry/instrumentation/netty/v4_1/internal/client/NettyClientHandlerFactory.java delete mode 100644 instrumentation/okhttp/okhttp-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/okhttp/v2_2/OkHttp2NetAttributesGetter.java create mode 100644 instrumentation/okhttp/okhttp-3.0/javaagent/src/http2Test/java/io/opentelemetry/javaagent/instrumentation/okhttp/v3_0/OkHttp3Http2Test.java create mode 100644 instrumentation/okhttp/okhttp-3.0/library/src/http2Test/java/io/opentelemetry/instrumentation/okhttp/v3_0/OkHttp3Http2Test.java create mode 100644 instrumentation/okhttp/okhttp-3.0/library/src/main/java/io/opentelemetry/instrumentation/okhttp/v3_0/internal/OkHttpClientInstrumenterBuilderFactory.java delete mode 100644 instrumentation/okhttp/okhttp-3.0/library/src/main/java/io/opentelemetry/instrumentation/okhttp/v3_0/internal/OkHttpInstrumenterFactory.java delete mode 100644 instrumentation/okhttp/okhttp-3.0/library/src/main/java/io/opentelemetry/instrumentation/okhttp/v3_0/internal/OkHttpNetAttributesGetter.java delete mode 100644 instrumentation/opentelemetry-api/opentelemetry-api-1.0/javaagent/src/test/groovy/ContextBridgeTest.groovy delete mode 100644 instrumentation/opentelemetry-api/opentelemetry-api-1.0/javaagent/src/test/groovy/ContextTest.groovy delete mode 100644 instrumentation/opentelemetry-api/opentelemetry-api-1.0/javaagent/src/test/groovy/TracerTest.groovy create mode 100644 instrumentation/opentelemetry-api/opentelemetry-api-1.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/ContextBridgeTest.java create mode 100644 instrumentation/opentelemetry-api/opentelemetry-api-1.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/ContextTest.java create mode 100644 instrumentation/opentelemetry-api/opentelemetry-api-1.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/TracerTest.java create mode 100644 instrumentation/opentelemetry-api/opentelemetry-api-1.31/javaagent/build.gradle.kts create mode 100644 instrumentation/opentelemetry-api/opentelemetry-api-1.31/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_31/incubator/OpenTelemetryApiInstrumentationModule.java create mode 100644 instrumentation/opentelemetry-api/opentelemetry-api-1.31/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_31/incubator/OpenTelemetryInstrumentation.java create mode 100644 instrumentation/opentelemetry-api/opentelemetry-api-1.31/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_31/incubator/metrics/ApplicationDoubleCounterBuilder131.java create mode 100644 instrumentation/opentelemetry-api/opentelemetry-api-1.31/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_31/incubator/metrics/ApplicationDoubleGaugeBuilder131.java create mode 100644 instrumentation/opentelemetry-api/opentelemetry-api-1.31/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_31/incubator/metrics/ApplicationDoubleHistogramBuilder131.java create mode 100644 instrumentation/opentelemetry-api/opentelemetry-api-1.31/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_31/incubator/metrics/ApplicationDoubleUpDownCounterBuilder131.java create mode 100644 instrumentation/opentelemetry-api/opentelemetry-api-1.31/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_31/incubator/metrics/ApplicationLongCounterBuilder131.java create mode 100644 instrumentation/opentelemetry-api/opentelemetry-api-1.31/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_31/incubator/metrics/ApplicationLongGaugeBuilder131.java create mode 100644 instrumentation/opentelemetry-api/opentelemetry-api-1.31/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_31/incubator/metrics/ApplicationLongHistogramBuilder131.java create mode 100644 instrumentation/opentelemetry-api/opentelemetry-api-1.31/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_31/incubator/metrics/ApplicationLongUpDownCounterBuilder131.java create mode 100644 instrumentation/opentelemetry-api/opentelemetry-api-1.31/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_31/incubator/metrics/ApplicationMeter131.java create mode 100644 instrumentation/opentelemetry-api/opentelemetry-api-1.31/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_31/incubator/metrics/ApplicationMeterFactory131.java create mode 100644 instrumentation/opentelemetry-api/opentelemetry-api-1.31/javaagent/src/noopTest/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_31/metrics/NoopTest.java create mode 100644 instrumentation/opentelemetry-api/opentelemetry-api-1.31/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_31/metrics/MeterTest.java create mode 100644 instrumentation/opentelemetry-api/opentelemetry-api-1.32/javaagent/build.gradle.kts create mode 100644 instrumentation/opentelemetry-api/opentelemetry-api-1.32/javaagent/src/incubatorTest/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_32/incubator/metrics/MeterTest.java create mode 100644 instrumentation/opentelemetry-api/opentelemetry-api-1.32/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_32/OpenTelemetryApiInstrumentationModule.java create mode 100644 instrumentation/opentelemetry-api/opentelemetry-api-1.32/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_32/OpenTelemetryInstrumentation.java create mode 100644 instrumentation/opentelemetry-api/opentelemetry-api-1.32/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_32/incubator/OpenTelemetryApiIncubatorInstrumentationModule.java create mode 100644 instrumentation/opentelemetry-api/opentelemetry-api-1.32/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_32/incubator/OpenTelemetryIncubatorInstrumentation.java create mode 100644 instrumentation/opentelemetry-api/opentelemetry-api-1.32/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_32/incubator/metrics/ApplicationDoubleHistogramBuilder132Incubator.java create mode 100644 instrumentation/opentelemetry-api/opentelemetry-api-1.32/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_32/incubator/metrics/ApplicationLongHistogramBuilder132Incubator.java create mode 100644 instrumentation/opentelemetry-api/opentelemetry-api-1.32/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_32/incubator/metrics/ApplicationMeter132Incubator.java create mode 100644 instrumentation/opentelemetry-api/opentelemetry-api-1.32/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_32/incubator/metrics/ApplicationMeterFactory132Incubator.java create mode 100644 instrumentation/opentelemetry-api/opentelemetry-api-1.32/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_32/metrics/ApplicationDoubleHistogramBuilder132.java create mode 100644 instrumentation/opentelemetry-api/opentelemetry-api-1.32/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_32/metrics/ApplicationLongHistogramBuilder132.java create mode 100644 instrumentation/opentelemetry-api/opentelemetry-api-1.32/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_32/metrics/ApplicationMeter132.java create mode 100644 instrumentation/opentelemetry-api/opentelemetry-api-1.32/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_32/metrics/ApplicationMeterFactory132.java create mode 100644 instrumentation/opentelemetry-api/opentelemetry-api-1.32/javaagent/src/noopTest/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_32/incubator/metrics/NoopTest.java create mode 100644 instrumentation/opentelemetry-api/opentelemetry-api-1.32/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_32/metrics/MeterTest.java create mode 100644 instrumentation/opentelemetry-api/opentelemetry-api-1.37/javaagent/build.gradle.kts create mode 100644 instrumentation/opentelemetry-api/opentelemetry-api-1.37/javaagent/src/incubatorTest/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_37/incubator/metrics/MeterTest.java create mode 100644 instrumentation/opentelemetry-api/opentelemetry-api-1.37/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_37/incubator/OpenTelemetryApiInstrumentationModule.java create mode 100644 instrumentation/opentelemetry-api/opentelemetry-api-1.37/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_37/incubator/OpenTelemetryInstrumentation.java create mode 100644 instrumentation/opentelemetry-api/opentelemetry-api-1.37/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_37/incubator/metrics/ApplicationDoubleCounterBuilder137.java create mode 100644 instrumentation/opentelemetry-api/opentelemetry-api-1.37/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_37/incubator/metrics/ApplicationDoubleGaugeBuilder137.java create mode 100644 instrumentation/opentelemetry-api/opentelemetry-api-1.37/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_37/incubator/metrics/ApplicationDoubleHistogramBuilder137.java create mode 100644 instrumentation/opentelemetry-api/opentelemetry-api-1.37/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_37/incubator/metrics/ApplicationDoubleUpDownCounterBuilder137.java create mode 100644 instrumentation/opentelemetry-api/opentelemetry-api-1.37/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_37/incubator/metrics/ApplicationLongCounterBuilder137.java create mode 100644 instrumentation/opentelemetry-api/opentelemetry-api-1.37/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_37/incubator/metrics/ApplicationLongGaugeBuilder137.java create mode 100644 instrumentation/opentelemetry-api/opentelemetry-api-1.37/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_37/incubator/metrics/ApplicationLongHistogramBuilder137.java create mode 100644 instrumentation/opentelemetry-api/opentelemetry-api-1.37/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_37/incubator/metrics/ApplicationLongUpDownCounterBuilder137.java create mode 100644 instrumentation/opentelemetry-api/opentelemetry-api-1.37/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_37/incubator/metrics/ApplicationMeter137.java create mode 100644 instrumentation/opentelemetry-api/opentelemetry-api-1.37/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_37/incubator/metrics/ApplicationMeterFactory137.java create mode 100644 instrumentation/opentelemetry-api/opentelemetry-api-1.37/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_37/incubator/metrics/BaseApplicationMeter137.java create mode 100644 instrumentation/opentelemetry-api/opentelemetry-api-1.37/javaagent/src/noopTest/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_37/incubator/metrics/NoopTest.java create mode 100644 instrumentation/opentelemetry-api/opentelemetry-api-1.37/javaagent/src/oldAndNewIncubatorTest/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_37/incubator/metrics/MeterTest.java create mode 100644 instrumentation/opentelemetry-api/opentelemetry-api-1.37/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_37/metrics/MeterTest.java create mode 100644 instrumentation/opentelemetry-api/opentelemetry-api-1.38/javaagent/build.gradle.kts create mode 100644 instrumentation/opentelemetry-api/opentelemetry-api-1.38/javaagent/src/incubatorTest/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_38/incubator/metrics/MeterTest.java create mode 100644 instrumentation/opentelemetry-api/opentelemetry-api-1.38/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_38/OpenTelemetryApiInstrumentationModule.java create mode 100644 instrumentation/opentelemetry-api/opentelemetry-api-1.38/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_38/OpenTelemetryInstrumentation.java create mode 100644 instrumentation/opentelemetry-api/opentelemetry-api-1.38/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_38/incubator/OpenTelemetryApiIncubatorInstrumentationModule.java create mode 100644 instrumentation/opentelemetry-api/opentelemetry-api-1.38/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_38/incubator/OpenTelemetryIncubatorInstrumentation.java create mode 100644 instrumentation/opentelemetry-api/opentelemetry-api-1.38/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_38/incubator/metrics/ApplicationDoubleGaugeBuilder138Incubator.java create mode 100644 instrumentation/opentelemetry-api/opentelemetry-api-1.38/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_38/incubator/metrics/ApplicationLongGaugeBuilder138Incubator.java create mode 100644 instrumentation/opentelemetry-api/opentelemetry-api-1.38/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_38/incubator/metrics/ApplicationMeter138Incubator.java create mode 100644 instrumentation/opentelemetry-api/opentelemetry-api-1.38/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_38/incubator/metrics/ApplicationMeterFactory138Incubator.java create mode 100644 instrumentation/opentelemetry-api/opentelemetry-api-1.38/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_38/metrics/ApplicationDoubleGaugeBuilder138.java create mode 100644 instrumentation/opentelemetry-api/opentelemetry-api-1.38/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_38/metrics/ApplicationLongGaugeBuilder138.java create mode 100644 instrumentation/opentelemetry-api/opentelemetry-api-1.38/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_38/metrics/ApplicationMeter138.java create mode 100644 instrumentation/opentelemetry-api/opentelemetry-api-1.38/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_38/metrics/ApplicationMeterFactory138.java create mode 100644 instrumentation/opentelemetry-api/opentelemetry-api-1.38/javaagent/src/noopTest/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_38/incubator/metrics/NoopTest.java create mode 100644 instrumentation/opentelemetry-api/opentelemetry-api-1.38/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_38/metrics/MeterTest.java create mode 100644 instrumentation/opentelemetry-instrumentation-annotations-1.16/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationannotations/KotlinCoroutineUtil.java delete mode 100644 instrumentation/opentelemetry-instrumentation-api/testing/src/main/java/io/opentelemetry/javaagent/instrumentation/testing/MockNetServerAttributesGetter.java create mode 100644 instrumentation/oshi/README.md create mode 100644 instrumentation/pekko/pekko-actor-1.0/javaagent/build.gradle.kts create mode 100644 instrumentation/pekko/pekko-actor-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkoactor/v1_0/PekkoActorCellInstrumentation.java create mode 100644 instrumentation/pekko/pekko-actor-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkoactor/v1_0/PekkoActorInstrumentationModule.java create mode 100644 instrumentation/pekko/pekko-actor-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkoactor/v1_0/PekkoDefaultSystemMessageQueueInstrumentation.java create mode 100644 instrumentation/pekko/pekko-actor-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkoactor/v1_0/PekkoDispatcherInstrumentation.java create mode 100644 instrumentation/pekko/pekko-actor-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkoactor/v1_0/PekkoIgnoredTypesConfigurer.java create mode 100644 instrumentation/pekko/pekko-actor-1.0/javaagent/src/test/scala/io/opentelemetry/javaagent/instrumentation/pekkoactor/v1_0/PekkoActorTest.scala create mode 100644 instrumentation/pekko/pekko-actor-1.0/javaagent/src/test/scala/io/opentelemetry/javaagent/instrumentation/pekkoactor/v1_0/PekkoActors.scala create mode 100644 instrumentation/pekko/pekko-http-1.0/javaagent/build.gradle.kts create mode 100644 instrumentation/pekko/pekko-http-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/PekkoHttpUtil.java create mode 100644 instrumentation/pekko/pekko-http-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/client/FutureWrapper.java create mode 100644 instrumentation/pekko/pekko-http-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/client/HttpExtClientInstrumentation.java create mode 100644 instrumentation/pekko/pekko-http-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/client/HttpHeaderSetter.java create mode 100644 instrumentation/pekko/pekko-http-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/client/OnCompleteHandler.java create mode 100644 instrumentation/pekko/pekko-http-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/client/PekkoHttpClientAttributesGetter.java create mode 100644 instrumentation/pekko/pekko-http-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/client/PekkoHttpClientInstrumentationModule.java create mode 100644 instrumentation/pekko/pekko-http-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/client/PekkoHttpClientSingletons.java create mode 100644 instrumentation/pekko/pekko-http-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/client/PoolMasterActorInstrumentation.java create mode 100644 instrumentation/pekko/pekko-http-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/server/GraphInterpreterInstrumentation.java create mode 100644 instrumentation/pekko/pekko-http-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/server/HttpExtServerInstrumentation.java create mode 100644 instrumentation/pekko/pekko-http-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/server/PekkoFlowWrapper.java create mode 100644 instrumentation/pekko/pekko-http-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/server/PekkoHttpResponseMutator.java create mode 100644 instrumentation/pekko/pekko-http-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/server/PekkoHttpServerAttributesGetter.java create mode 100644 instrumentation/pekko/pekko-http-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/server/PekkoHttpServerHeaders.java create mode 100644 instrumentation/pekko/pekko-http-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/server/PekkoHttpServerInstrumentationModule.java create mode 100644 instrumentation/pekko/pekko-http-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/server/PekkoHttpServerSingletons.java create mode 100644 instrumentation/pekko/pekko-http-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/server/PekkoHttpServerSourceInstrumentation.java create mode 100644 instrumentation/pekko/pekko-http-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/server/PekkoServerIgnoredTypesConfigurer.java create mode 100644 instrumentation/pekko/pekko-http-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/server/route/PathConcatenationInstrumentation.java create mode 100644 instrumentation/pekko/pekko-http-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/server/route/PathMatcherInstrumentation.java create mode 100644 instrumentation/pekko/pekko-http-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/server/route/PathMatcherStaticInstrumentation.java create mode 100644 instrumentation/pekko/pekko-http-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/server/route/PekkoHttpServerRouteInstrumentationModule.java create mode 100644 instrumentation/pekko/pekko-http-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/server/route/PekkoRouteHolder.java create mode 100644 instrumentation/pekko/pekko-http-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/server/route/RouteConcatenationInstrumentation.java create mode 100644 instrumentation/pekko/pekko-http-1.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/PekkoHttpServerJavaRouteTest.java create mode 100644 instrumentation/pekko/pekko-http-1.0/javaagent/src/test/resources/application.conf create mode 100644 instrumentation/pekko/pekko-http-1.0/javaagent/src/test/scala/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/AbstractHttpServerInstrumentationTest.scala create mode 100644 instrumentation/pekko/pekko-http-1.0/javaagent/src/test/scala/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/PekkoHttpClientInstrumentationTest.scala create mode 100644 instrumentation/pekko/pekko-http-1.0/javaagent/src/test/scala/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/PekkoHttpServerInstrumentationTest.scala create mode 100644 instrumentation/pekko/pekko-http-1.0/javaagent/src/test/scala/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/PekkoHttpServerInstrumentationTestAsync.scala create mode 100644 instrumentation/pekko/pekko-http-1.0/javaagent/src/test/scala/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/PekkoHttpServerInstrumentationTestSync.scala create mode 100644 instrumentation/pekko/pekko-http-1.0/javaagent/src/test/scala/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/PekkoHttpServerRouteTest.scala create mode 100644 instrumentation/pekko/pekko-http-1.0/javaagent/src/test/scala/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/PekkoHttpTestAsyncWebServer.scala create mode 100644 instrumentation/pekko/pekko-http-1.0/javaagent/src/test/scala/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/PekkoHttpTestServerSourceWebServer.scala create mode 100644 instrumentation/pekko/pekko-http-1.0/javaagent/src/test/scala/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/PekkoHttpTestSyncWebServer.scala create mode 100644 instrumentation/pekko/pekko-http-1.0/javaagent/src/test/scala/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/PekkoHttpTestWebServer.scala delete mode 100644 instrumentation/play/play-ws/play-ws-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/playws/PlayWsClientNetAttributesGetter.java create mode 100644 instrumentation/pulsar/pulsar-2.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pulsar/v2_8/ConsumerBaseInstrumentation.java create mode 100644 instrumentation/pulsar/pulsar-2.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pulsar/v2_8/SendCallbackData.java create mode 100644 instrumentation/pulsar/pulsar-2.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pulsar/v2_8/SendCallbackInstrumentation.java create mode 100644 instrumentation/pulsar/pulsar-2.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pulsar/v2_8/telemetry/MessageListenerContext.java delete mode 100644 instrumentation/pulsar/pulsar-2.8/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/pulsar/v2_8/PulsarClientTest.groovy create mode 100644 instrumentation/pulsar/pulsar-2.8/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/pulsar/v2_8/AbstractPulsarClientTest.java create mode 100644 instrumentation/pulsar/pulsar-2.8/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/pulsar/v2_8/PulsarClientSuppressReceiveSpansTest.java create mode 100644 instrumentation/pulsar/pulsar-2.8/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/pulsar/v2_8/PulsarClientTest.java create mode 100644 instrumentation/quarkus-resteasy-reactive/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/quarkus/resteasy/reactive/AbstractResteasyReactiveContextInstrumentation.java create mode 100644 instrumentation/quarkus-resteasy-reactive/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/quarkus/resteasy/reactive/OtelRequestContext.java create mode 100644 instrumentation/quarkus-resteasy-reactive/quarkus2-testing/src/main/java/io/opentelemetry/instrumentation/quarkus/resteasy/reactive/v2_0/TestFilter.java create mode 100644 instrumentation/quarkus-resteasy-reactive/quarkus3-testing/src/main/java/io/opentelemetry/instrumentation/quarkus/resteasy/reactive/v3_0/TestFilter.java create mode 100644 instrumentation/quartz-2.0/README.md create mode 100644 instrumentation/r2dbc-1.0/README.md create mode 100644 instrumentation/rabbitmq-2.7/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rabbitmq/RabbitDeliveryNetAttributesGetter.java delete mode 100644 instrumentation/rabbitmq-2.7/javaagent/src/test/groovy/RabbitMqTest.groovy delete mode 100644 instrumentation/rabbitmq-2.7/javaagent/src/test/groovy/ReactorRabbitMqTest.groovy delete mode 100644 instrumentation/rabbitmq-2.7/javaagent/src/test/groovy/WithRabbitMqTrait.groovy create mode 100644 instrumentation/rabbitmq-2.7/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/rabbitmq/AbstractRabbitMqTest.java create mode 100644 instrumentation/rabbitmq-2.7/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/rabbitmq/RabbitMqTest.java create mode 100644 instrumentation/rabbitmq-2.7/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/rabbitmq/ReactorRabbitMqTest.java delete mode 100644 instrumentation/ratpack/ratpack-1.7/library/src/main/java/io/opentelemetry/instrumentation/ratpack/v1_7/internal/RatpackNetClientAttributesGetter.java delete mode 100644 instrumentation/ratpack/ratpack-1.7/library/src/main/java/io/opentelemetry/instrumentation/ratpack/v1_7/internal/RatpackNetServerAttributesGetter.java create mode 100644 instrumentation/reactor/reactor-3.4/javaagent/build.gradle.kts create mode 100644 instrumentation/reactor/reactor-3.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/reactor/v3_4/operator/ContextPropagationOperator34Instrumentation.java create mode 100644 instrumentation/reactor/reactor-3.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/reactor/v3_4/operator/ContextPropagationOperator34InstrumentationModule.java create mode 100644 instrumentation/reactor/reactor-3.4/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/reactor/v3_4/ContextPropagationOperator34InstrumentationTest.java rename instrumentation/reactor/reactor-kafka-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/reactor/kafka/v1_0/{KafkaReceiver133Access.java => KafkaReceiver13Access.java} (75%) create mode 100644 instrumentation/reactor/reactor-kafka-1.0/javaagent/src/testV1_3_21/java/io/opentelemetry/javaagent/instrumentation/reactor/kafka/v1_0/ReactorKafka1321InstrumentationTest.java create mode 100644 instrumentation/reactor/reactor-netty/reactor-netty-1.0/javaagent-unit-tests/src/test/java/io/opentelemetry/javaagent/instrumentation/reactornetty/v1_0/FailedRequestWithUrlMakerTest.java create mode 100644 instrumentation/reactor/reactor-netty/reactor-netty-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/reactornetty/v1_0/FailedRequestWithUrlMaker.java create mode 100644 instrumentation/reactor/reactor-netty/reactor-netty-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/reactornetty/v1_0/HttpClientConnectInstrumentation.java create mode 100644 instrumentation/reactor/reactor-netty/reactor-netty-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/reactornetty/v1_0/InstrumentationContexts.java delete mode 100644 instrumentation/reactor/reactor-netty/reactor-netty-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/reactornetty/v1_0/ReactorNettyNetClientAttributesGetter.java create mode 100644 instrumentation/reactor/reactor-netty/reactor-netty-1.0/javaagent/src/main/java/reactor/netty/http/client/HttpClientConfigBuddy.java create mode 100644 instrumentation/reactor/reactor-netty/reactor-netty-1.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/reactornetty/v1_0/ReactorNettyHttpClientDeferredHeadersTest.java create mode 100644 instrumentation/redisson/redisson-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/redisson/v3_0/RedissonAsyncClientTest.java create mode 100644 instrumentation/redisson/redisson-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/redisson/v3_0/RedissonClientTest.java delete mode 100644 instrumentation/redisson/redisson-3.17/javaagent/src/test/groovy/RedissonAsyncClientTest.groovy delete mode 100644 instrumentation/redisson/redisson-3.17/javaagent/src/test/groovy/RedissonClientTest.groovy create mode 100644 instrumentation/redisson/redisson-3.17/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/redisson/v3_17/RedissonAsyncClientTest.java create mode 100644 instrumentation/redisson/redisson-3.17/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/redisson/v3_17/RedissonClientTest.java delete mode 100644 instrumentation/redisson/redisson-common/testing/src/main/groovy/AbstractRedissonAsyncClientTest.groovy delete mode 100644 instrumentation/redisson/redisson-common/testing/src/main/groovy/AbstractRedissonClientTest.groovy create mode 100644 instrumentation/redisson/redisson-common/testing/src/main/java/io/opentelemetry/javaagent/instrumentation/redisson/AbstractRedissonAsyncClientTest.java create mode 100644 instrumentation/redisson/redisson-common/testing/src/main/java/io/opentelemetry/javaagent/instrumentation/redisson/AbstractRedissonClientTest.java create mode 100644 instrumentation/resources/library/src/main/java/io/opentelemetry/instrumentation/resources/AttributeProvider.java create mode 100644 instrumentation/resources/library/src/main/java/io/opentelemetry/instrumentation/resources/AttributeResourceProvider.java create mode 100644 instrumentation/resources/library/src/main/java/io/opentelemetry/instrumentation/resources/HostIdResourceProvider.java create mode 100644 instrumentation/resources/library/src/main/java/io/opentelemetry/instrumentation/resources/MainJarPathFinder.java create mode 100644 instrumentation/resources/library/src/main/java/io/opentelemetry/instrumentation/resources/MainJarPathHolder.java create mode 100644 instrumentation/resources/library/src/main/java/io/opentelemetry/instrumentation/resources/ManifestResourceProvider.java create mode 100644 instrumentation/resources/library/src/test/java/io/opentelemetry/instrumentation/resources/HostIdResourceProviderTest.java create mode 100644 instrumentation/resources/library/src/test/java/io/opentelemetry/instrumentation/resources/ManifestResourceProviderTest.java create mode 100644 instrumentation/resources/library/src/test/resources/MANIFEST.MF create mode 100644 instrumentation/resources/library/src/test/resources/containerd_proc_self_mountinfo create mode 100644 instrumentation/resources/library/src/test/resources/crio_proc_self_mountinfo create mode 100644 instrumentation/resources/library/src/test/resources/crio_proc_self_mountinfo1 create mode 100644 instrumentation/resources/library/src/test/resources/crio_proc_self_mountinfo2 create mode 100644 instrumentation/resources/library/src/test/resources/docker_proc_self_mountinfo1 create mode 100644 instrumentation/resources/library/src/test/resources/empty-MANIFEST.MF delete mode 100644 instrumentation/restlet/restlet-1.1/library/src/main/java/io/opentelemetry/instrumentation/restlet/v1_1/RestletNetAttributesGetter.java rename instrumentation/restlet/restlet-2.0/library/src/main/java/io/opentelemetry/instrumentation/restlet/v2_0/internal/{RestletNetAttributesGetter.java => ServerCallAccess.java} (53%) delete mode 100644 instrumentation/rocketmq/rocketmq-client/rocketmq-client-4.8/javaagent/src/test/groovy/io/opentelemetry/instrumentation/rocketmqclient/v4_8/RocketMqClientTest.groovy create mode 100644 instrumentation/rocketmq/rocketmq-client/rocketmq-client-4.8/javaagent/src/test/java/io/opentelemetry/instrumentation/rocketmqclient/v4_8/RocketMqClientTest.java delete mode 100644 instrumentation/rocketmq/rocketmq-client/rocketmq-client-4.8/library/src/test/groovy/io/opentelemetry/instrumentation/rocketmqclient/v4_8/RocketMqClientTest.groovy create mode 100644 instrumentation/rocketmq/rocketmq-client/rocketmq-client-4.8/library/src/test/java/io/opentelemetry/instrumentation/rocketmqclient/v4_8/RocketMqClientTest.java delete mode 100644 instrumentation/rocketmq/rocketmq-client/rocketmq-client-4.8/testing/src/main/groovy/io/opentelemetry/instrumentation/rocketmqclient/v4_8/AbstractRocketMqClientTest.groovy delete mode 100644 instrumentation/rocketmq/rocketmq-client/rocketmq-client-4.8/testing/src/main/groovy/io/opentelemetry/instrumentation/rocketmqclient/v4_8/TracingMessageListener.groovy create mode 100644 instrumentation/rocketmq/rocketmq-client/rocketmq-client-4.8/testing/src/main/java/io/opentelemetry/instrumentation/rocketmqclient/v4_8/AbstractRocketMqClientTest.java create mode 100644 instrumentation/rocketmq/rocketmq-client/rocketmq-client-4.8/testing/src/main/java/io/opentelemetry/instrumentation/rocketmqclient/v4_8/TracingMessageListener.java create mode 100644 instrumentation/runtime-telemetry/README.md delete mode 100644 instrumentation/runtime-telemetry/runtime-telemetry-java17/javaagent/src/test/groovy/JmxRuntimeMetricsTest.groovy create mode 100644 instrumentation/runtime-telemetry/runtime-telemetry-java17/javaagent/src/test/java/io/opentelemetry/instrumentation/javaagent/runtimemetrics/java17/JmxRuntimeMetricsTest.java create mode 100644 instrumentation/runtime-telemetry/runtime-telemetry-java17/library/src/test/java/io/opentelemetry/instrumentation/runtimemetrics/java17/internal/DurationUtilTest.java create mode 100644 instrumentation/runtime-telemetry/runtime-telemetry-java8/javaagent/src/main/java/io/opentelemetry/instrumentation/javaagent/runtimemetrics/java8/JarAnalyzer.java create mode 100644 instrumentation/runtime-telemetry/runtime-telemetry-java8/javaagent/src/main/java/io/opentelemetry/instrumentation/javaagent/runtimemetrics/java8/JarAnalyzerInstaller.java create mode 100644 instrumentation/runtime-telemetry/runtime-telemetry-java8/javaagent/src/main/java/io/opentelemetry/instrumentation/javaagent/runtimemetrics/java8/JarDetails.java create mode 100644 instrumentation/runtime-telemetry/runtime-telemetry-java8/javaagent/src/test/java/io/opentelemetry/instrumentation/javaagent/runtimemetrics/java8/JarAnalyzerInstallerTest.java create mode 100644 instrumentation/runtime-telemetry/runtime-telemetry-java8/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java8/internal/CpuMethods.java rename instrumentation/runtime-telemetry/runtime-telemetry-java8/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java8/{BufferPools.java => internal/ExperimentalBufferPools.java} (62%) create mode 100644 instrumentation/runtime-telemetry/runtime-telemetry-java8/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java8/internal/ExperimentalCpu.java create mode 100644 instrumentation/runtime-telemetry/runtime-telemetry-java8/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java8/internal/ExperimentalMemoryPools.java rename instrumentation/runtime-telemetry/runtime-telemetry-java8/library/src/test/java/io/opentelemetry/instrumentation/runtimemetrics/java8/{ClassesTest.java => ClassesStableSemconvTest.java} (94%) create mode 100644 instrumentation/runtime-telemetry/runtime-telemetry-java8/library/src/test/java/io/opentelemetry/instrumentation/runtimemetrics/java8/CpuStableSemconvTest.java rename instrumentation/runtime-telemetry/runtime-telemetry-java8/library/src/test/java/io/opentelemetry/instrumentation/runtimemetrics/java8/{MemoryPoolsTest.java => MemoryPoolsStableSemconvTest.java} (66%) create mode 100644 instrumentation/runtime-telemetry/runtime-telemetry-java8/library/src/test/java/io/opentelemetry/instrumentation/runtimemetrics/java8/ThreadsStableSemconvTest.java delete mode 100644 instrumentation/runtime-telemetry/runtime-telemetry-java8/library/src/test/java/io/opentelemetry/instrumentation/runtimemetrics/java8/ThreadsTest.java rename instrumentation/runtime-telemetry/runtime-telemetry-java8/library/src/test/java/io/opentelemetry/instrumentation/runtimemetrics/java8/{BufferPoolsTest.java => internal/ExperimentalBufferPoolsTest.java} (77%) rename instrumentation/runtime-telemetry/runtime-telemetry-java8/library/src/test/java/io/opentelemetry/instrumentation/runtimemetrics/java8/{CpuTest.java => internal/ExperimentalCpuTest.java} (61%) create mode 100644 instrumentation/runtime-telemetry/runtime-telemetry-java8/library/src/test/java/io/opentelemetry/instrumentation/runtimemetrics/java8/internal/ExperimentalMemoryPoolsTest.java create mode 100644 instrumentation/runtime-telemetry/runtime-telemetry-java8/testing/build.gradle.kts rename instrumentation/{redisson/redisson-3.0/javaagent/src/test/groovy/RedissonAsyncClientTest.groovy => runtime-telemetry/runtime-telemetry-java8/testing/src/main/java/io/opentelemetry/instrumentation/testapp/DummyApplication.java} (50%) create mode 100644 instrumentation/runtime-telemetry/runtime-telemetry-java8/testing/src/test/java/io/opentelemetry/instrumentation/javaagent/runtimemetrics/java8/JarAnalyzerTest.java create mode 100644 instrumentation/servlet/servlet-3.0/javaagent-unit-tests/src/test/resources/afterSnippetInjectionWithOtherHeadStyle.html create mode 100644 instrumentation/servlet/servlet-3.0/javaagent-unit-tests/src/test/resources/beforeSnippetInjectionWithOtherHeadStyle.html create mode 100644 instrumentation/servlet/servlet-5.0/javaagent-unit-tests/src/test/resources/afterSnippetInjectionWithOtherHeadStyle.html create mode 100644 instrumentation/servlet/servlet-5.0/javaagent-unit-tests/src/test/resources/beforeSnippetInjectionWithOtherHeadStyle.html create mode 100644 instrumentation/servlet/servlet-5.0/jetty12-testing/build.gradle.kts create mode 100644 instrumentation/servlet/servlet-5.0/jetty12-testing/src/test/groovy/Jetty12Servlet5Test.groovy create mode 100644 instrumentation/servlet/servlet-5.0/testing/build.gradle.kts rename instrumentation/servlet/servlet-5.0/{javaagent/src/test/groovy => testing/src/main/groovy/test}/AbstractServlet5Test.groovy (91%) rename instrumentation/servlet/servlet-5.0/{javaagent/src/test/groovy => testing/src/main/groovy/test}/TestServlet5.groovy (99%) rename instrumentation/servlet/servlet-5.0/{javaagent/src/test/java => testing/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/v5_0}/RequestDispatcherServlet.java (92%) delete mode 100644 instrumentation/servlet/servlet-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/ServletNetAttributesGetter.java delete mode 100644 instrumentation/spring/spring-batch-3.0/javaagent/src/test/groovy/SpringBatchTest.groovy create mode 100644 instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/basic/JavaConfigBatchJobTest.java create mode 100644 instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/basic/JsrConfigBatchJobTest.java create mode 100644 instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/basic/SpringBatchTest.java create mode 100644 instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/basic/XmlConfigBatchJobTest.java create mode 100644 instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/jsr/CustomEventChunkListener.java create mode 100644 instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/jsr/CustomEventItemProcessListener.java create mode 100644 instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/jsr/CustomEventItemReadListener.java create mode 100644 instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/jsr/CustomEventItemWriteListener.java create mode 100644 instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/jsr/CustomEventJobListener.java create mode 100644 instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/jsr/CustomEventStepListener.java create mode 100644 instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/jsr/SingleItemReader.java create mode 100644 instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/jsr/TestBatchlet.java create mode 100644 instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/jsr/TestDecider.java create mode 100644 instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/jsr/TestItemProcessor.java create mode 100644 instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/jsr/TestItemReader.java create mode 100644 instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/jsr/TestItemWriter.java create mode 100644 instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/jsr/TestPartitionedItemReader.java create mode 100644 instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/runner/ApplicationConfigRunner.java create mode 100644 instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/runner/JavaxBatchConfigRunner.java create mode 100644 instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/runner/JobRunner.java create mode 100644 instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/runner/SpringBatchApplication.java create mode 100644 instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/springbatch/CustomEventChunkListener.java create mode 100644 instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/springbatch/CustomEventItemProcessListener.java create mode 100644 instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/springbatch/CustomEventItemReadListener.java create mode 100644 instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/springbatch/CustomEventItemWriteListener.java create mode 100644 instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/springbatch/CustomEventJobListener.java create mode 100644 instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/springbatch/CustomEventStepListener.java create mode 100644 instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/springbatch/SingleItemReader.java create mode 100644 instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/springbatch/TestDecider.java create mode 100644 instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/springbatch/TestItemProcessor.java create mode 100644 instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/springbatch/TestItemReader.java create mode 100644 instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/springbatch/TestItemWriter.java create mode 100644 instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/springbatch/TestPartitionedItemReader.java create mode 100644 instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/springbatch/TestPartitioner.java create mode 100644 instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/springbatch/TestSyncItemReader.java create mode 100644 instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/springbatch/TestTasklet.java create mode 100644 instrumentation/spring/spring-boot-autoconfigure/gradle.properties delete mode 100644 instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/EnableOpenTelemetry.java delete mode 100644 instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/MetricExportProperties.java delete mode 100644 instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/SamplerProperties.java delete mode 100644 instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/aspects/SdkExtensionWithSpanAspect.java delete mode 100644 instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/aspects/TraceAspectAutoConfiguration.java delete mode 100644 instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/aspects/TraceAspectProperties.java delete mode 100644 instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/exporters/jaeger/JaegerSpanExporterAutoConfiguration.java delete mode 100644 instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/exporters/jaeger/JaegerSpanExporterProperties.java delete mode 100644 instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/exporters/logging/LoggingExporterProperties.java delete mode 100644 instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/exporters/logging/LoggingMetricExporterAutoConfiguration.java delete mode 100644 instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/exporters/logging/LoggingSpanExporterAutoConfiguration.java delete mode 100644 instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/exporters/otlp/OtlpExporterProperties.java delete mode 100644 instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/exporters/otlp/OtlpLoggerExporterAutoConfiguration.java delete mode 100644 instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/exporters/otlp/OtlpMetricExporterAutoConfiguration.java delete mode 100644 instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/exporters/otlp/OtlpSpanExporterAutoConfiguration.java delete mode 100644 instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/exporters/zipkin/ZipkinSpanExporterAutoConfiguration.java delete mode 100644 instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/exporters/zipkin/ZipkinSpanExporterProperties.java delete mode 100644 instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/httpclients/HttpClientsProperties.java delete mode 100644 instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/httpclients/resttemplate/RestTemplateAutoConfiguration.java delete mode 100644 instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/httpclients/resttemplate/RestTemplateBeanPostProcessor.java delete mode 100644 instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/httpclients/webclient/WebClientAutoConfiguration.java delete mode 100644 instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/httpclients/webclient/WebClientBeanPostProcessor.java create mode 100644 instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/ConditionalOnEnabledInstrumentation.java create mode 100644 instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/InstrumentationPropertyEnabled.java create mode 100644 instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/MapConverter.java create mode 100644 instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/SdkEnabled.java create mode 100644 instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/annotations/InstrumentationAnnotationsAutoConfiguration.java rename instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/{aspects => internal/instrumentation/annotations}/InstrumentationWithSpanAspect.java (90%) rename instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/{aspects => internal/instrumentation/annotations}/JoinPointRequest.java (61%) rename instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/{aspects => internal/instrumentation/annotations}/JointPointCodeAttributesExtractor.java (71%) rename instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/{aspects => internal/instrumentation/annotations}/WithSpanAspect.java (90%) rename instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/{aspects => internal/instrumentation/annotations}/WithSpanAspectParameterAttributeNamesExtractor.java (79%) create mode 100644 instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/jdbc/DataSourcePostProcessor.java create mode 100644 instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/jdbc/JdbcInstrumentationAutoConfiguration.java create mode 100644 instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/kafka/ConcurrentKafkaListenerContainerFactoryPostProcessor.java rename instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/{ => internal/instrumentation}/kafka/KafkaInstrumentationAutoConfiguration.java (51%) create mode 100644 instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/logging/LogbackAppenderApplicationListener.java create mode 100644 instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/logging/LogbackAppenderInstaller.java create mode 100644 instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/logging/OpenTelemetryAppenderAutoConfiguration.java rename instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/{metrics/MicrometerShimAutoConfiguration.java => internal/instrumentation/micrometer/MicrometerBridgeAutoConfiguration.java} (68%) create mode 100644 instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/mongo/MongoClientInstrumentationAutoConfiguration.java create mode 100644 instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/r2dbc/R2dbcInstrumentationAutoConfiguration.java create mode 100644 instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/r2dbc/R2dbcInstrumentingPostProcessor.java create mode 100644 instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/web/RestTemplateBeanPostProcessor.java create mode 100644 instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/web/RestTemplateInstrumentation.java create mode 100644 instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/web/SpringWebInstrumentationAutoConfiguration.java create mode 100644 instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/webflux/SpringWebfluxInstrumentationAutoConfiguration.java create mode 100644 instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/webflux/WebClientBeanPostProcessor.java create mode 100644 instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/webmvc/SpringWebMvc5InstrumentationAutoConfiguration.java create mode 100644 instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/properties/ConfigPropertiesBridge.java create mode 100644 instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/properties/InstrumentationConfigUtil.java rename instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/{resources => internal/properties}/OtelResourceProperties.java (66%) create mode 100644 instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/properties/OtlpExporterProperties.java create mode 100644 instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/properties/PropagationProperties.java create mode 100644 instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/properties/SpringConfigProperties.java create mode 100644 instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/resources/DistroVersionResourceProvider.java create mode 100644 instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/resources/SpringResourceProvider.java delete mode 100644 instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/kafka/ConcurrentKafkaListenerContainerFactoryPostProcessor.java delete mode 100644 instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/kafka/KafkaInstrumentationProperties.java delete mode 100644 instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/metrics/MicrometerShimProperties.java delete mode 100644 instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/propagators/CompositeTextMapPropagatorFactory.java delete mode 100644 instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/propagators/PropagationAutoConfiguration.java delete mode 100644 instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/propagators/PropagationProperties.java delete mode 100644 instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/resources/OtelResourceAutoConfiguration.java delete mode 100644 instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/resources/SpringResourceConfigProperties.java delete mode 100644 instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/resources/SpringResourceProvider.java delete mode 100644 instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/webmvc/WebMvcFilterAutoConfiguration.java delete mode 100644 instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/webmvc/WebMvcFilterAutoConfigurationSpring6.java delete mode 100644 instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/webmvc/WebMvcProperties.java create mode 100644 instrumentation/spring/spring-boot-autoconfigure/src/main/javaSpring3/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/annotations/OpenTelemetryAnnotationsRuntimeHints.java create mode 100644 instrumentation/spring/spring-boot-autoconfigure/src/main/javaSpring3/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/web/RestClientBeanPostProcessor.java create mode 100644 instrumentation/spring/spring-boot-autoconfigure/src/main/javaSpring3/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/web/RestClientInstrumentationAutoConfiguration.java create mode 100644 instrumentation/spring/spring-boot-autoconfigure/src/main/javaSpring3/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/webmvc/SpringWebMvc6InstrumentationAutoConfiguration.java create mode 100644 instrumentation/spring/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json create mode 100644 instrumentation/spring/spring-boot-autoconfigure/src/main/resources/META-INF/native-image/io.opentelemetry.instrumentation/opentelemetry-spring-boot/resource-config.json create mode 100644 instrumentation/spring/spring-boot-autoconfigure/src/main/resources/META-INF/spring/aot.factories delete mode 100644 instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/aspects/InstrumentationWithSpanAspectTest.java delete mode 100644 instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/aspects/SdkExtensionWithSpanAspectTest.java delete mode 100644 instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/aspects/TraceAspectAutoConfigurationTest.java delete mode 100644 instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/exporters/MetricExporterAutoConfigurationTest.java delete mode 100644 instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/exporters/SpanExporterAutoConfigurationTest.java delete mode 100644 instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/exporters/jaeger/JaegerSpanExporterAutoConfigurationTest.java delete mode 100644 instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/exporters/logging/LoggingMetricExporterAutoConfigurationTest.java delete mode 100644 instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/exporters/logging/LoggingSpanExporterAutoConfigurationTest.java delete mode 100644 instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/exporters/otlp/OtlpLogExporterAutoConfigurationTest.java delete mode 100644 instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/exporters/otlp/OtlpMetricExporterAutoConfigurationTest.java delete mode 100644 instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/exporters/otlp/OtlpSpanExporterAutoConfigurationTest.java delete mode 100644 instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/exporters/zipkin/ZipkinSpanExporterAutoConfigurationTest.java delete mode 100644 instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/httpclients/resttemplate/RestTemplateAutoConfigurationTest.java delete mode 100644 instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/httpclients/resttemplate/RestTemplateBeanPostProcessorTest.java delete mode 100644 instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/httpclients/webclient/WebClientAutoConfigurationTest.java delete mode 100644 instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/httpclients/webclient/WebClientBeanPostProcessorTest.java create mode 100644 instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/annotations/InstrumentationAnnotationsAutoConfigurationTest.java rename instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/{aspects/AbstractWithSpanAspectTest.java => internal/instrumentation/annotations/InstrumentationWithSpanAspectTest.java} (90%) create mode 100644 instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/micrometer/MicrometerBridgeAutoConfigurationTest.java create mode 100644 instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/r2dbc/R2DbcInstrumentationAutoConfigurationTest.java create mode 100644 instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/web/SpringWebInstrumentationAutoConfigurationTest.java create mode 100644 instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/webflux/SpringWebfluxInstrumentationAutoConfigurationTest.java create mode 100644 instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/webflux/WebClientBeanPostProcessorTest.java create mode 100644 instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/webmvc/SpringWebMvcInstrumentation5AutoConfigurationTest.java create mode 100644 instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/properties/OtlpExporterPropertiesTest.java create mode 100644 instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/resources/DistroVersionResourceProviderTest.java create mode 100644 instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/resources/SpringConfigPropertiesTest.java create mode 100644 instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/resources/SpringResourceProviderTest.java delete mode 100644 instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/kafka/KafkaIntegrationTest.java delete mode 100644 instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/metrics/MicrometerShimAutoConfigurationTest.java delete mode 100644 instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/propagators/PropagationAutoConfigurationTest.java delete mode 100644 instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/propagators/PropagationPropertiesTest.java delete mode 100644 instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/resources/OtelResourceAutoConfigurationTest.java delete mode 100644 instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/resources/OtelResourcePropertiesTest.java delete mode 100644 instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/resources/SpringResourceConfigPropertiesTest.java delete mode 100644 instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/webmvc/WebMvcFilterAutoConfigurationSpring6Test.java delete mode 100644 instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/webmvc/WebMvcFilterAutoConfigurationTest.java create mode 100644 instrumentation/spring/spring-boot-autoconfigure/src/testLogbackAppender/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/logging/LogbackAppenderTest.java create mode 100644 instrumentation/spring/spring-boot-autoconfigure/src/testLogbackAppender/resources/logback-test.xml create mode 100644 instrumentation/spring/spring-boot-autoconfigure/src/testLogbackMissing/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/logging/LogbackMissingTest.java create mode 100644 instrumentation/spring/spring-boot-autoconfigure/src/testSpring3/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/web/RestClientInstrumentationAutoConfigurationTest.java create mode 100644 instrumentation/spring/spring-boot-autoconfigure/src/testSpring3/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/webmvc/SpringWebMvcInstrumentation6AutoConfigurationTest.java rename instrumentation/spring/spring-boot-resources/{testing => javaagent-unit-tests}/build.gradle.kts (94%) rename instrumentation/spring/spring-boot-resources/{testing => javaagent-unit-tests}/src/test/java/io/opentelemetry/instrumentation/spring/resources/TestBootInfClassesResource.java (85%) rename instrumentation/spring/spring-boot-resources/{library => javaagent}/build.gradle.kts (100%) rename instrumentation/spring/spring-boot-resources/{library => javaagent}/src/main/java/io/opentelemetry/instrumentation/spring/resources/SpringBootServiceNameDetector.java (68%) create mode 100644 instrumentation/spring/spring-boot-resources/javaagent/src/main/java/io/opentelemetry/instrumentation/spring/resources/SpringBootServiceVersionDetector.java create mode 100644 instrumentation/spring/spring-boot-resources/javaagent/src/main/java/io/opentelemetry/instrumentation/spring/resources/SystemHelper.java rename instrumentation/spring/spring-boot-resources/{library => javaagent}/src/test/java/io/opentelemetry/instrumentation/spring/resources/SpringBootServiceNameDetectorTest.java (65%) create mode 100644 instrumentation/spring/spring-boot-resources/javaagent/src/test/java/io/opentelemetry/instrumentation/spring/resources/SpringBootServiceVersionDetectorTest.java create mode 100644 instrumentation/spring/spring-boot-resources/javaagent/src/test/resources/META-INF/build-info.properties rename instrumentation/spring/spring-boot-resources/{library => javaagent}/src/test/resources/application-multi.yml (88%) rename instrumentation/spring/spring-boot-resources/{library => javaagent}/src/test/resources/application.properties (100%) rename instrumentation/spring/spring-boot-resources/{library => javaagent}/src/test/resources/application.yml (70%) create mode 100644 instrumentation/spring/spring-boot-resources/javaagent/src/test/resources/bootstrap-multi.yml create mode 100644 instrumentation/spring/spring-boot-resources/javaagent/src/test/resources/bootstrap.properties create mode 100644 instrumentation/spring/spring-boot-resources/javaagent/src/test/resources/bootstrap.yaml create mode 100644 instrumentation/spring/spring-boot-resources/javaagent/src/test/resources/bootstrap.yml create mode 100644 instrumentation/spring/spring-boot-resources/javaagent/src/test/resources/build-info.properties create mode 100644 instrumentation/spring/spring-cloud-gateway/README.md create mode 100644 instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-2.0/javaagent/build.gradle.kts create mode 100644 instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/gateway/v2_0/GatewayInstrumentationModule.java create mode 100644 instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/gateway/v2_0/GatewaySingletons.java create mode 100644 instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/gateway/v2_0/HandlerAdapterInstrumentation.java create mode 100644 instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/gateway/v2_0/ServerWebExchangeHelper.java create mode 100644 instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-2.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/gateway/v2_0/GatewayRouteMappingTest.java create mode 100644 instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-2.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/gateway/v2_0/GatewayTestApplication.java create mode 100644 instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-2.0/javaagent/src/test/resources/logback.xml create mode 100644 instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-2.2/testing/build.gradle.kts create mode 100644 instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-2.2/testing/src/test/java/io/opentelemetry/instrumentation/spring/gateway/v2_2/Gateway22RouteMappingTest.java create mode 100644 instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-2.2/testing/src/test/java/io/opentelemetry/instrumentation/spring/gateway/v2_2/Gateway22TestApplication.java create mode 100644 instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-2.2/testing/src/test/resources/application.yml create mode 100644 instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-common/testing/build.gradle.kts create mode 100644 instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-common/testing/src/main/java/io/opentelemetry/instrumentation/spring/gateway/common/AbstractRouteMappingTest.java rename instrumentation/spring/spring-data/{spring-data-3.0/testing/src/test/java => spring-data-1.8/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/data/v1_8}/SpringJpaTest.java (56%) create mode 100644 instrumentation/spring/spring-data/spring-data-3.0/testing/src/reactiveTest/java/io/opentelemetry/javaagent/instrumentation/spring/data/v3_0/ReactiveSpringDataTest.java create mode 100644 instrumentation/spring/spring-data/spring-data-3.0/testing/src/reactiveTest/java/io/opentelemetry/javaagent/instrumentation/spring/data/v3_0/repository/Customer.java create mode 100644 instrumentation/spring/spring-data/spring-data-3.0/testing/src/reactiveTest/java/io/opentelemetry/javaagent/instrumentation/spring/data/v3_0/repository/CustomerRepository.java create mode 100644 instrumentation/spring/spring-data/spring-data-3.0/testing/src/reactiveTest/java/io/opentelemetry/javaagent/instrumentation/spring/data/v3_0/repository/PersistenceConfig.java rename instrumentation/spring/spring-data/{spring-data-1.8/javaagent/src/test/java/SprintJpaTest.java => spring-data-3.0/testing/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/data/v3_0/SpringJpaTest.java} (53%) delete mode 100644 instrumentation/spring/spring-data/spring-data-common/testing/src/main/java/AbstractSpringJpaTest.java create mode 100644 instrumentation/spring/spring-data/spring-data-common/testing/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/data/AbstractSpringJpaTest.java delete mode 100644 instrumentation/spring/spring-integration-4.1/javaagent/src/test/groovy/ComplexPropagationTest.groovy delete mode 100644 instrumentation/spring/spring-integration-4.1/javaagent/src/test/groovy/SpringCloudStreamProducerTest.groovy delete mode 100644 instrumentation/spring/spring-integration-4.1/javaagent/src/test/groovy/SpringCloudStreamRabbitTest.groovy delete mode 100644 instrumentation/spring/spring-integration-4.1/javaagent/src/test/groovy/SpringIntegrationAndRabbitTest.groovy delete mode 100644 instrumentation/spring/spring-integration-4.1/javaagent/src/test/groovy/SpringIntegrationTelemetryTest.groovy create mode 100644 instrumentation/spring/spring-integration-4.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/integration/v4_1/ComplexPropagationTest.java create mode 100644 instrumentation/spring/spring-integration-4.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/integration/v4_1/SpringCloudStreamProducerTest.java create mode 100644 instrumentation/spring/spring-integration-4.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/integration/v4_1/SpringCloudStreamRabbitTest.java create mode 100644 instrumentation/spring/spring-integration-4.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/integration/v4_1/SpringIntegrationAndRabbitTest.java create mode 100644 instrumentation/spring/spring-integration-4.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/integration/v4_1/SpringIntegrationTelemetryTest.java delete mode 100644 instrumentation/spring/spring-integration-4.1/library/src/test/groovy/ComplexPropagationTest.groovy delete mode 100644 instrumentation/spring/spring-integration-4.1/library/src/test/groovy/GlobalInterceptorSpringConfig.groovy delete mode 100644 instrumentation/spring/spring-integration-4.1/library/src/test/groovy/GlobalInterceptorWithProducerSpanSpringConfig.groovy delete mode 100644 instrumentation/spring/spring-integration-4.1/library/src/test/groovy/SpringCloudStreamProducerTest.groovy delete mode 100644 instrumentation/spring/spring-integration-4.1/library/src/test/groovy/SpringCloudStreamRabbitTest.groovy delete mode 100644 instrumentation/spring/spring-integration-4.1/library/src/test/groovy/SpringIntegrationTelemetryTest.groovy create mode 100644 instrumentation/spring/spring-integration-4.1/library/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/integration/v4_1/ComplexPropagationTest.java create mode 100644 instrumentation/spring/spring-integration-4.1/library/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/integration/v4_1/GlobalInterceptorSpringConfig.java create mode 100644 instrumentation/spring/spring-integration-4.1/library/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/integration/v4_1/GlobalInterceptorWithProducerSpanSpringConfig.java create mode 100644 instrumentation/spring/spring-integration-4.1/library/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/integration/v4_1/SpringCloudStreamProducerTest.java create mode 100644 instrumentation/spring/spring-integration-4.1/library/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/integration/v4_1/SpringCloudStreamRabbitTest.java create mode 100644 instrumentation/spring/spring-integration-4.1/library/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/integration/v4_1/SpringIntegrationTelemetryTest.java delete mode 100644 instrumentation/spring/spring-integration-4.1/testing/src/main/groovy/AbstractComplexPropagationTest.groovy delete mode 100644 instrumentation/spring/spring-integration-4.1/testing/src/main/groovy/AbstractSpringCloudStreamProducerTest.groovy delete mode 100644 instrumentation/spring/spring-integration-4.1/testing/src/main/groovy/AbstractSpringCloudStreamRabbitTest.groovy delete mode 100644 instrumentation/spring/spring-integration-4.1/testing/src/main/groovy/AbstractSpringIntegrationTracingTest.groovy delete mode 100644 instrumentation/spring/spring-integration-4.1/testing/src/main/groovy/CapturingMessageHandler.groovy delete mode 100644 instrumentation/spring/spring-integration-4.1/testing/src/main/groovy/WithRabbitProducerConsumerTrait.groovy create mode 100644 instrumentation/spring/spring-integration-4.1/testing/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/integration/v4_1/AbstractComplexPropagationTest.java create mode 100644 instrumentation/spring/spring-integration-4.1/testing/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/integration/v4_1/AbstractSpringCloudStreamProducerTest.java create mode 100644 instrumentation/spring/spring-integration-4.1/testing/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/integration/v4_1/AbstractSpringCloudStreamRabbitTest.java create mode 100644 instrumentation/spring/spring-integration-4.1/testing/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/integration/v4_1/AbstractSpringIntegrationTracingTest.java create mode 100644 instrumentation/spring/spring-integration-4.1/testing/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/integration/v4_1/CapturingMessageHandler.java create mode 100644 instrumentation/spring/spring-integration-4.1/testing/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/integration/v4_1/RabbitExtension.java create mode 100644 instrumentation/spring/spring-jms/spring-jms-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/jms/v2_0/AbstractPollingMessageListenerContainerInstrumentation.java create mode 100644 instrumentation/spring/spring-jms/spring-jms-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/jms/v2_0/JmsDestinationAccessorInstrumentation.java delete mode 100644 instrumentation/spring/spring-jms/spring-jms-2.0/javaagent/src/test/groovy/SpringListenerTest.groovy delete mode 100644 instrumentation/spring/spring-jms/spring-jms-2.0/javaagent/src/test/groovy/SpringTemplateTest.groovy delete mode 100644 instrumentation/spring/spring-jms/spring-jms-2.0/javaagent/src/test/groovy/listener/AbstractConfig.groovy delete mode 100644 instrumentation/spring/spring-jms/spring-jms-2.0/javaagent/src/test/groovy/listener/AnnotatedListenerConfig.groovy delete mode 100644 instrumentation/spring/spring-jms/spring-jms-2.0/javaagent/src/test/groovy/listener/ManualListenerConfig.groovy delete mode 100644 instrumentation/spring/spring-jms/spring-jms-2.0/javaagent/src/test/groovy/listener/TestListener.groovy create mode 100644 instrumentation/spring/spring-jms/spring-jms-2.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/jms/v2_0/AbstractConfig.java create mode 100644 instrumentation/spring/spring-jms/spring-jms-2.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/jms/v2_0/AnnotatedListenerConfig.java create mode 100644 instrumentation/spring/spring-jms/spring-jms-2.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/jms/v2_0/ManualListenerConfig.java create mode 100644 instrumentation/spring/spring-jms/spring-jms-2.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/jms/v2_0/SpringListenerTest.java create mode 100644 instrumentation/spring/spring-jms/spring-jms-2.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/jms/v2_0/SpringTemplateTest.java create mode 100644 instrumentation/spring/spring-jms/spring-jms-2.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/jms/v2_0/TestListener.java delete mode 100644 instrumentation/spring/spring-jms/spring-jms-2.0/javaagent/src/testReceiveSpansDisabled/groovy/SpringListenerSuppressReceiveSpansTest.groovy create mode 100644 instrumentation/spring/spring-jms/spring-jms-2.0/javaagent/src/testReceiveSpansDisabled/java/io/opentelemetry/javaagent/instrumentation/spring/jms/v2_0/SpringListenerSuppressReceiveSpansTest.java create mode 100644 instrumentation/spring/spring-jms/spring-jms-2.0/testing/build.gradle.kts create mode 100644 instrumentation/spring/spring-jms/spring-jms-2.0/testing/src/main/java/io/opentelemetry/instrumentation/spring/jms/v2_0/AbstractJmsTest.java create mode 100644 instrumentation/spring/spring-jms/spring-jms-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/jms/v6_0/AbstractPollingMessageListenerContainerInstrumentation.java create mode 100644 instrumentation/spring/spring-jms/spring-jms-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/jms/v6_0/JmsDestinationAccessorInstrumentation.java create mode 100644 instrumentation/spring/spring-jms/spring-jms-6.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/jms/v6_0/AbstractSpringJmsListenerTest.java create mode 100644 instrumentation/spring/spring-jms/spring-jms-6.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/jms/v6_0/SpringListenerSuppressReceiveSpansTest.java rename instrumentation/spring/spring-kafka-2.7/library/src/main/java/io/opentelemetry/instrumentation/spring/kafka/v2_7/{ => internal}/SpringKafkaErrorCauseExtractor.java (65%) create mode 100644 instrumentation/spring/spring-kafka-2.7/testing/src/main/java/io/opentelemetry/testing/ErrorHandlerSetter.java delete mode 100644 instrumentation/spring/spring-rabbit-1.0/javaagent/src/test/groovy/ContextPropagationTest.groovy create mode 100644 instrumentation/spring/spring-rabbit-1.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/rabbit/v1_0/SpringRabbitMqTest.java create mode 100644 instrumentation/spring/spring-rmi-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/rmi/v4_0/SpringRmiIgnoredTypesConfigurer.java delete mode 100644 instrumentation/spring/spring-rmi-4.0/javaagent/src/test/groovy/SpringRmiTest.groovy create mode 100644 instrumentation/spring/spring-rmi-4.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/rmi/v4_0/SpringRmiTest.java create mode 100644 instrumentation/spring/spring-rmi-4.0/javaagent/src/test/java/springrmi/app/ejb/SpringRmiGreeterEjb.java create mode 100644 instrumentation/spring/spring-rmi-4.0/javaagent/src/test/java/springrmi/app/ejb/SpringRmiGreeterEjbRemote.java create mode 100644 instrumentation/spring/spring-rmi-4.0/javaagent/src/test/resources/spring-rmi.xml delete mode 100644 instrumentation/spring/spring-scheduling-3.1/javaagent/src/test/groovy/SpringSchedulingTest.groovy create mode 100644 instrumentation/spring/spring-scheduling-3.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/scheduling/v3_1/SpringSchedulingTest.java rename instrumentation/spring/spring-scheduling-3.1/javaagent/src/test/java/{ => io/opentelemetry/javaagent/instrumentation/spring/scheduling/v3_1/spring/component}/IntervalTask.java (86%) rename instrumentation/spring/spring-scheduling-3.1/javaagent/src/test/java/{ => io/opentelemetry/javaagent/instrumentation/spring/scheduling/v3_1/spring/component}/OneTimeTask.java (84%) rename instrumentation/spring/spring-scheduling-3.1/javaagent/src/test/java/{ => io/opentelemetry/javaagent/instrumentation/spring/scheduling/v3_1/spring/component}/TaskWithError.java (87%) rename instrumentation/spring/spring-scheduling-3.1/javaagent/src/test/java/{ => io/opentelemetry/javaagent/instrumentation/spring/scheduling/v3_1/spring/component}/TriggerTask.java (86%) rename instrumentation/spring/spring-scheduling-3.1/javaagent/src/test/java/{ => io/opentelemetry/javaagent/instrumentation/spring/scheduling/v3_1/spring/config}/EnhancedClassTaskConfig.java (88%) rename instrumentation/spring/spring-scheduling-3.1/javaagent/src/test/java/{ => io/opentelemetry/javaagent/instrumentation/spring/scheduling/v3_1/spring/config}/IntervalTaskConfig.java (68%) rename instrumentation/spring/spring-scheduling-3.1/javaagent/src/test/java/{ => io/opentelemetry/javaagent/instrumentation/spring/scheduling/v3_1/spring/config}/LambdaTaskConfig.java (68%) rename instrumentation/spring/spring-scheduling-3.1/javaagent/src/test/java/{ => io/opentelemetry/javaagent/instrumentation/spring/scheduling/v3_1/spring/config}/OneTimeTaskConfig.java (68%) rename instrumentation/spring/spring-scheduling-3.1/javaagent/src/test/java/{ => io/opentelemetry/javaagent/instrumentation/spring/scheduling/v3_1/spring/config}/TaskWithErrorConfig.java (81%) rename instrumentation/spring/spring-scheduling-3.1/javaagent/src/test/java/{ => io/opentelemetry/javaagent/instrumentation/spring/scheduling/v3_1/spring/config}/TriggerTaskConfig.java (68%) rename instrumentation/spring/spring-scheduling-3.1/javaagent/src/test/java/{ => io/opentelemetry/javaagent/instrumentation/spring/scheduling/v3_1/spring/service}/LambdaTaskConfigurer.java (87%) create mode 100644 instrumentation/spring/spring-security-config-6.0/javaagent/README.md create mode 100644 instrumentation/spring/spring-security-config-6.0/javaagent/build.gradle.kts create mode 100644 instrumentation/spring/spring-security-config-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/security/config/v6_0/EnduserAttributesCapturerSingletons.java create mode 100644 instrumentation/spring/spring-security-config-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/security/config/v6_0/servlet/HttpSecurityInstrumentation.java create mode 100644 instrumentation/spring/spring-security-config-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/security/config/v6_0/servlet/SpringSecurityConfigServletInstrumentationModule.java create mode 100644 instrumentation/spring/spring-security-config-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/security/config/v6_0/webflux/ServerHttpSecurityInstrumentation.java create mode 100644 instrumentation/spring/spring-security-config-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/security/config/v6_0/webflux/SpringSecurityConfigWebFluxInstrumentationModule.java create mode 100644 instrumentation/spring/spring-security-config-6.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/security/config/v6_0/servlet/HttpSecurityInstrumentationTest.java create mode 100644 instrumentation/spring/spring-security-config-6.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/security/config/v6_0/webflux/ServerHttpSecurityInstrumentationTest.java create mode 100644 instrumentation/spring/spring-security-config-6.0/library/README.md create mode 100644 instrumentation/spring/spring-security-config-6.0/library/build.gradle.kts create mode 100644 instrumentation/spring/spring-security-config-6.0/library/src/main/java/io/opentelemetry/instrumentation/spring/security/config/v6_0/EnduserAttributesCapturer.java create mode 100644 instrumentation/spring/spring-security-config-6.0/library/src/main/java/io/opentelemetry/instrumentation/spring/security/config/v6_0/servlet/EnduserAttributesCapturingServletFilter.java create mode 100644 instrumentation/spring/spring-security-config-6.0/library/src/main/java/io/opentelemetry/instrumentation/spring/security/config/v6_0/servlet/EnduserAttributesHttpSecurityCustomizer.java create mode 100644 instrumentation/spring/spring-security-config-6.0/library/src/main/java/io/opentelemetry/instrumentation/spring/security/config/v6_0/webflux/EnduserAttributesCapturingWebFilter.java create mode 100644 instrumentation/spring/spring-security-config-6.0/library/src/main/java/io/opentelemetry/instrumentation/spring/security/config/v6_0/webflux/EnduserAttributesServerHttpSecurityCustomizer.java create mode 100644 instrumentation/spring/spring-security-config-6.0/library/src/test/java/io/opentelemetry/instrumentation/spring/security/config/v6_0/EnduserAttributesCapturerTest.java create mode 100644 instrumentation/spring/spring-security-config-6.0/library/src/test/java/io/opentelemetry/instrumentation/spring/security/config/v6_0/servlet/EnduserAttributesCapturingServletFilterTest.java create mode 100644 instrumentation/spring/spring-security-config-6.0/library/src/test/java/io/opentelemetry/instrumentation/spring/security/config/v6_0/servlet/EnduserAttributesHttpSecurityCustomizerTest.java create mode 100644 instrumentation/spring/spring-security-config-6.0/library/src/test/java/io/opentelemetry/instrumentation/spring/security/config/v6_0/webflux/EnduserAttributesCapturingWebFilterTest.java create mode 100644 instrumentation/spring/spring-security-config-6.0/library/src/test/java/io/opentelemetry/instrumentation/spring/security/config/v6_0/webflux/EnduserAttributesServerHttpSecurityCustomizerTest.java delete mode 100644 instrumentation/spring/spring-web/spring-web-3.1/library/src/main/java/io/opentelemetry/instrumentation/spring/web/v3_1/SpringWebNetAttributesGetter.java create mode 100644 instrumentation/spring/spring-web/spring-web-3.1/library/src/main/java/io/opentelemetry/instrumentation/spring/web/v3_1/internal/WebTelemetryUtil.java delete mode 100644 instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/test/groovy/SingleThreadedSpringWebfluxTest.groovy create mode 100644 instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v5_0/server/SpringThreadedSpringWebfluxTest.java rename instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/test/java/{ => io/opentelemetry/javaagent/instrumentation/spring/webflux/v5_0}/server/base/ControllerSpringWebFluxServerTest.java (76%) rename instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/test/java/{ => io/opentelemetry/javaagent/instrumentation/spring/webflux/v5_0}/server/base/DelayedControllerSpringWebFluxServerTest.java (95%) rename instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/test/java/{ => io/opentelemetry/javaagent/instrumentation/spring/webflux/v5_0}/server/base/DelayedHandlerSpringWebFluxServerTest.java (87%) rename instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/test/java/{ => io/opentelemetry/javaagent/instrumentation/spring/webflux/v5_0}/server/base/HandlerSpringWebFluxServerTest.java (73%) rename instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/test/java/{ => io/opentelemetry/javaagent/instrumentation/spring/webflux/v5_0}/server/base/ImmediateControllerSpringWebFluxServerTest.java (95%) rename instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/test/java/{ => io/opentelemetry/javaagent/instrumentation/spring/webflux/v5_0}/server/base/ImmediateHandlerSpringWebFluxServerTest.java (94%) rename instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/test/java/{ => io/opentelemetry/javaagent/instrumentation/spring/webflux/v5_0}/server/base/ServerTestController.java (95%) rename instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/test/java/{ => io/opentelemetry/javaagent/instrumentation/spring/webflux/v5_0}/server/base/ServerTestRouteFactory.java (95%) rename instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/test/java/{ => io/opentelemetry/javaagent/instrumentation/spring/webflux/v5_0}/server/base/SpringWebFluxServerTest.java (92%) rename instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/test/java/{ => io/opentelemetry/javaagent/instrumentation/spring/webflux/v5_0}/server/base/package-info.java (62%) create mode 100644 instrumentation/spring/spring-webflux/spring-webflux-5.0/testing/build.gradle.kts create mode 100644 instrumentation/spring/spring-webflux/spring-webflux-5.0/testing/src/main/java/io/opentelemetry/instrumentation/spring/webflux/IpcSingleThreadNettyCustomizer.java delete mode 100644 instrumentation/spring/spring-webflux/spring-webflux-5.3/library/src/main/java/io/opentelemetry/instrumentation/spring/webflux/v5_3/WebfluxServerNetAttributesGetter.java create mode 100644 instrumentation/spring/spring-webflux/spring-webflux-5.3/library/src/main/java/io/opentelemetry/instrumentation/spring/webflux/v5_3/internal/SpringWebfluxBuilderUtil.java delete mode 100644 instrumentation/spring/spring-webflux/spring-webflux-5.3/library/src/main/java/io/opentelemetry/instrumentation/spring/webflux/v5_3/internal/SpringWebfluxTelemetryClientBuilder.java delete mode 100644 instrumentation/spring/spring-webflux/spring-webflux-5.3/library/src/main/java/io/opentelemetry/instrumentation/spring/webflux/v5_3/internal/WebClientExperimentalAttributesExtractor.java delete mode 100644 instrumentation/spring/spring-webflux/spring-webflux-5.3/library/src/main/java/io/opentelemetry/instrumentation/spring/webflux/v5_3/internal/WebClientNetAttributesGetter.java create mode 100644 instrumentation/spring/spring-webflux/spring-webflux-5.3/testing/src/main/java/io/opentelemetry/instrumentation/spring/webflux/server/SingleThreadNettyCustomizer.java delete mode 100644 instrumentation/spring/spring-webmvc/spring-webmvc-3.1/javaagent/src/test/groovy/test/boot/SecurityConfig.groovy delete mode 100644 instrumentation/spring/spring-webmvc/spring-webmvc-3.1/javaagent/src/test/groovy/test/boot/SpringBootBasedTest.groovy delete mode 100644 instrumentation/spring/spring-webmvc/spring-webmvc-3.1/javaagent/src/test/groovy/test/filter/ServletFilterConfig.groovy delete mode 100644 instrumentation/spring/spring-webmvc/spring-webmvc-3.1/javaagent/src/test/groovy/test/filter/ServletFilterTest.groovy create mode 100644 instrumentation/spring/spring-webmvc/spring-webmvc-3.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webmvc/v3_1/boot/SecurityConfig.java create mode 100644 instrumentation/spring/spring-webmvc/spring-webmvc-3.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webmvc/v3_1/boot/SpringBootBasedTest.java create mode 100644 instrumentation/spring/spring-webmvc/spring-webmvc-3.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webmvc/v3_1/filter/ServletFilterConfig.java create mode 100644 instrumentation/spring/spring-webmvc/spring-webmvc-3.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webmvc/v3_1/filter/ServletFilterTest.java delete mode 100644 instrumentation/spring/spring-webmvc/spring-webmvc-5.3/library/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/v5_3/SpringWebMvcNetAttributesGetter.java create mode 100644 instrumentation/spring/spring-webmvc/spring-webmvc-5.3/library/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/v5_3/internal/SpringMvcBuilderUtil.java delete mode 100644 instrumentation/spring/spring-webmvc/spring-webmvc-6.0/javaagent/src/test/groovy/SecurityConfig.groovy delete mode 100644 instrumentation/spring/spring-webmvc/spring-webmvc-6.0/javaagent/src/test/groovy/ServletFilterConfig.groovy delete mode 100644 instrumentation/spring/spring-webmvc/spring-webmvc-6.0/javaagent/src/test/groovy/ServletFilterTest.groovy delete mode 100644 instrumentation/spring/spring-webmvc/spring-webmvc-6.0/javaagent/src/test/groovy/SpringBootBasedTest.groovy create mode 100644 instrumentation/spring/spring-webmvc/spring-webmvc-6.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webmvc/v6_0/boot/SecurityConfig.java create mode 100644 instrumentation/spring/spring-webmvc/spring-webmvc-6.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webmvc/v6_0/boot/SpringBootBasedTest.java create mode 100644 instrumentation/spring/spring-webmvc/spring-webmvc-6.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webmvc/v6_0/filter/ServletFilterConfig.java create mode 100644 instrumentation/spring/spring-webmvc/spring-webmvc-6.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webmvc/v6_0/filter/ServletFilterTest.java delete mode 100644 instrumentation/spring/spring-webmvc/spring-webmvc-6.0/library/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/v6_0/SpringWebMvcNetAttributesGetter.java create mode 100644 instrumentation/spring/spring-webmvc/spring-webmvc-6.0/library/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/v6_0/internal/SpringMvcBuilderUtil.java delete mode 100644 instrumentation/spring/spring-webmvc/spring-webmvc-common/testing/src/main/groovy/boot/AbstractSpringBootBasedTest.groovy delete mode 100644 instrumentation/spring/spring-webmvc/spring-webmvc-common/testing/src/main/groovy/boot/SavingAuthenticationProvider.groovy delete mode 100644 instrumentation/spring/spring-webmvc/spring-webmvc-common/testing/src/main/groovy/boot/TestController.groovy delete mode 100644 instrumentation/spring/spring-webmvc/spring-webmvc-common/testing/src/main/groovy/filter/AbstractServletFilterTest.groovy delete mode 100644 instrumentation/spring/spring-webmvc/spring-webmvc-common/testing/src/main/groovy/filter/FilteredAppConfig.groovy delete mode 100644 instrumentation/spring/spring-webmvc/spring-webmvc-common/testing/src/main/groovy/filter/TestController.groovy create mode 100644 instrumentation/spring/spring-webmvc/spring-webmvc-common/testing/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/boot/AbstractSpringBootBasedTest.java rename instrumentation/spring/spring-webmvc/spring-webmvc-common/testing/src/main/{groovy/boot/AppConfig.groovy => java/io/opentelemetry/instrumentation/spring/webmvc/boot/AppConfig.java} (65%) create mode 100644 instrumentation/spring/spring-webmvc/spring-webmvc-common/testing/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/boot/SavingAuthenticationProvider.java create mode 100644 instrumentation/spring/spring-webmvc/spring-webmvc-common/testing/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/boot/TestController.java create mode 100644 instrumentation/spring/spring-webmvc/spring-webmvc-common/testing/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/boot/TestUserDetails.java create mode 100644 instrumentation/spring/spring-webmvc/spring-webmvc-common/testing/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/filter/AbstractServletFilterTest.java create mode 100644 instrumentation/spring/spring-webmvc/spring-webmvc-common/testing/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/filter/FilteredAppConfig.java create mode 100644 instrumentation/spring/spring-webmvc/spring-webmvc-common/testing/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/filter/TestController.java delete mode 100644 instrumentation/spring/spring-ws-2.0/javaagent/src/test/groovy/test/boot/HelloEndpoint.groovy delete mode 100644 instrumentation/spring/spring-ws-2.0/javaagent/src/test/groovy/test/boot/SpringWsTest.groovy delete mode 100644 instrumentation/spring/spring-ws-2.0/javaagent/src/test/groovy/test/boot/WebServiceConfig.groovy rename instrumentation/spring/spring-ws-2.0/javaagent/src/test/{groovy/test/boot/AppConfig.groovy => java/io/opentelemetry/javaagent/instrumentation/spring/ws/v2_0/AppConfig.java} (63%) create mode 100644 instrumentation/spring/spring-ws-2.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/ws/v2_0/HelloEndpoint.java create mode 100644 instrumentation/spring/spring-ws-2.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/ws/v2_0/SpringWsTest.java create mode 100644 instrumentation/spring/spring-ws-2.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/ws/v2_0/WebServiceConfig.java delete mode 100644 instrumentation/spring/starters/jaeger-spring-boot-starter/README.md delete mode 100644 instrumentation/spring/starters/jaeger-spring-boot-starter/build.gradle.kts create mode 100644 instrumentation/spring/starters/spring-boot-starter/gradle.properties delete mode 100644 instrumentation/struts-2.3/javaagent/src/test/groovy/Struts2ActionSpanTest.groovy rename instrumentation/struts-2.3/javaagent/src/test/java/io/opentelemetry/{struts => javaagent/instrumentation/struts2}/GreetingAction.java (97%) rename instrumentation/struts-2.3/javaagent/src/test/java/io/opentelemetry/{struts => javaagent/instrumentation/struts2}/GreetingServlet.java (89%) create mode 100644 instrumentation/struts-2.3/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/struts2/Struts2ActionSpanTest.java delete mode 100644 instrumentation/tapestry-5.4/javaagent/src/test/groovy/TapestryTest.groovy create mode 100644 instrumentation/tapestry-5.4/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/tapestry/TapestryTest.java delete mode 100644 instrumentation/tomcat/tomcat-10.0/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/tomcat/v10_0/AsyncServlet.groovy delete mode 100644 instrumentation/tomcat/tomcat-10.0/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/tomcat/v10_0/TomcatAsyncTest.groovy delete mode 100644 instrumentation/tomcat/tomcat-10.0/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/tomcat/v10_0/TomcatHandlerTest.groovy create mode 100644 instrumentation/tomcat/tomcat-10.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/tomcat/v10_0/AsyncServlet.java create mode 100644 instrumentation/tomcat/tomcat-10.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/tomcat/v10_0/ErrorHandlerValve.java rename instrumentation/tomcat/tomcat-10.0/javaagent/src/test/{groovy => java}/io/opentelemetry/javaagent/instrumentation/tomcat/v10_0/TestServlet.java (97%) create mode 100644 instrumentation/tomcat/tomcat-10.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/tomcat/v10_0/TomcatAsyncTest.java create mode 100644 instrumentation/tomcat/tomcat-10.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/tomcat/v10_0/TomcatHandlerTest.java delete mode 100644 instrumentation/tomcat/tomcat-7.0/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/tomcat/v7_0/AsyncServlet.groovy delete mode 100644 instrumentation/tomcat/tomcat-7.0/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/tomcat/v7_0/ThreadPoolExecutorTest.groovy delete mode 100644 instrumentation/tomcat/tomcat-7.0/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/tomcat/v7_0/TomcatAsyncTest.groovy delete mode 100644 instrumentation/tomcat/tomcat-7.0/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/tomcat/v7_0/TomcatHandlerTest.groovy create mode 100644 instrumentation/tomcat/tomcat-7.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/tomcat/v7_0/AsyncServlet.java create mode 100644 instrumentation/tomcat/tomcat-7.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/tomcat/v7_0/ErrorHandlerValve.java rename instrumentation/tomcat/tomcat-7.0/javaagent/src/test/{groovy => java}/io/opentelemetry/javaagent/instrumentation/tomcat/v7_0/TestServlet.java (97%) create mode 100644 instrumentation/tomcat/tomcat-7.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/tomcat/v7_0/ThreadPoolExecutorTest.java create mode 100644 instrumentation/tomcat/tomcat-7.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/tomcat/v7_0/TomcatAsyncTest.java create mode 100644 instrumentation/tomcat/tomcat-7.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/tomcat/v7_0/TomcatHandlerTest.java delete mode 100644 instrumentation/tomcat/tomcat-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/tomcat/common/TomcatNetAttributesGetter.java rename instrumentation/undertow-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/undertow/{HttpTransferEncodingInstrumentation.java => HttpServerConnectionInstrumentation.java} (57%) delete mode 100644 instrumentation/undertow-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/undertow/UndertowNetAttributesGetter.java create mode 100644 instrumentation/undertow-1.4/javaagent/src/test/groovy/UndertowHttp2ServerTest.groovy delete mode 100644 instrumentation/vertx/vertx-http-client/vertx-http-client-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v3_0/client/Vertx3NetAttributesGetter.java delete mode 100644 instrumentation/vertx/vertx-http-client/vertx-http-client-3.0/javaagent/src/test/groovy/client/VertxHttpClientTest.groovy create mode 100644 instrumentation/vertx/vertx-http-client/vertx-http-client-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/vertx/v3_0/client/VertxHttpClientTest.java delete mode 100644 instrumentation/vertx/vertx-http-client/vertx-http-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/client/Vertx4NetAttributesGetter.java create mode 100644 instrumentation/vertx/vertx-redis-client-4.0/javaagent/build.gradle.kts create mode 100644 instrumentation/vertx/vertx-redis-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/redis/CommandImplInstrumentation.java create mode 100644 instrumentation/vertx/vertx-redis-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/redis/RedisConnectionProviderInstrumentation.java create mode 100644 instrumentation/vertx/vertx-redis-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/redis/RedisStandaloneConnectionInstrumentation.java create mode 100644 instrumentation/vertx/vertx-redis-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/redis/VertxRedisClientAttributesExtractor.java create mode 100644 instrumentation/vertx/vertx-redis-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/redis/VertxRedisClientAttributesGetter.java create mode 100644 instrumentation/vertx/vertx-redis-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/redis/VertxRedisClientInstrumentationModule.java create mode 100644 instrumentation/vertx/vertx-redis-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/redis/VertxRedisClientNetAttributesGetter.java create mode 100644 instrumentation/vertx/vertx-redis-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/redis/VertxRedisClientRequest.java create mode 100644 instrumentation/vertx/vertx-redis-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/redis/VertxRedisClientSingletons.java create mode 100644 instrumentation/vertx/vertx-redis-client-4.0/javaagent/src/main/java/io/vertx/redis/client/impl/RequestUtil.java create mode 100644 instrumentation/vertx/vertx-redis-client-4.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/redis/VertxRedisClientTest.java create mode 100644 instrumentation/vertx/vertx-sql-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/sql/HandlerWrapper.java create mode 100644 instrumentation/vertx/vertx-sql-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/sql/TransactionImplInstrumentation.java create mode 100644 instrumentation/wicket-8.0/common-testing/build.gradle.kts create mode 100644 instrumentation/wicket-8.0/common-testing/src/main/java/hello/ExceptionPage.java create mode 100644 instrumentation/wicket-8.0/common-testing/src/main/java/hello/HelloApplication.java create mode 100644 instrumentation/wicket-8.0/common-testing/src/main/java/hello/HelloPage.java create mode 100644 instrumentation/wicket-8.0/common-testing/src/main/java/io/opentelemetry/javaagent/instrumentation/wicket/AbstractWicketTest.java rename instrumentation/wicket-8.0/{javaagent/src/test => common-testing/src/main}/resources/hello/HelloPage.html (60%) delete mode 100644 instrumentation/wicket-8.0/javaagent/src/test/groovy/WicketTest.groovy delete mode 100644 instrumentation/wicket-8.0/javaagent/src/test/groovy/hello/ExceptionPage.groovy delete mode 100644 instrumentation/wicket-8.0/javaagent/src/test/groovy/hello/HelloApplication.groovy delete mode 100644 instrumentation/wicket-8.0/javaagent/src/test/groovy/hello/HelloPage.groovy create mode 100644 instrumentation/wicket-8.0/wicket10-testing/build.gradle.kts create mode 100644 instrumentation/wicket-8.0/wicket10-testing/src/test/java/io/opentelemetry/javaagent/instrumentation/wicket/WicketTest.java create mode 100644 instrumentation/wicket-8.0/wicket8-testing/build.gradle.kts create mode 100644 instrumentation/wicket-8.0/wicket8-testing/src/test/java/io/opentelemetry/javaagent/instrumentation/wicket/WicketTest.java create mode 100644 instrumentation/xxl-job/README.md create mode 100644 instrumentation/xxl-job/xxl-job-1.9.2/javaagent/build.gradle.kts create mode 100644 instrumentation/xxl-job/xxl-job-1.9.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/xxljob/v1_9_2/GlueJobHandlerInstrumentation.java create mode 100644 instrumentation/xxl-job/xxl-job-1.9.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/xxljob/v1_9_2/ScriptJobHandlerInstrumentation.java create mode 100644 instrumentation/xxl-job/xxl-job-1.9.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/xxljob/v1_9_2/SimpleJobHandlerInstrumentation.java create mode 100644 instrumentation/xxl-job/xxl-job-1.9.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/xxljob/v1_9_2/XxlJobInstrumentationModule.java create mode 100644 instrumentation/xxl-job/xxl-job-1.9.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/xxljob/v1_9_2/XxlJobSingletons.java create mode 100644 instrumentation/xxl-job/xxl-job-1.9.2/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/xxljob/v1_9_2/XxlJobTest.java create mode 100644 instrumentation/xxl-job/xxl-job-2.1.2/javaagent/build.gradle.kts create mode 100644 instrumentation/xxl-job/xxl-job-2.1.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/xxljob/v2_1_2/GlueJobHandlerInstrumentation.java create mode 100644 instrumentation/xxl-job/xxl-job-2.1.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/xxljob/v2_1_2/MethodJobHandlerInstrumentation.java create mode 100644 instrumentation/xxl-job/xxl-job-2.1.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/xxljob/v2_1_2/ScriptJobHandlerInstrumentation.java create mode 100644 instrumentation/xxl-job/xxl-job-2.1.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/xxljob/v2_1_2/SimpleJobHandlerInstrumentation.java create mode 100644 instrumentation/xxl-job/xxl-job-2.1.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/xxljob/v2_1_2/XxlJobInstrumentationModule.java create mode 100644 instrumentation/xxl-job/xxl-job-2.1.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/xxljob/v2_1_2/XxlJobSingletons.java create mode 100644 instrumentation/xxl-job/xxl-job-2.1.2/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/xxljob/v2_1_2/XxlJobTest.java create mode 100644 instrumentation/xxl-job/xxl-job-2.3.0/javaagent/build.gradle.kts create mode 100644 instrumentation/xxl-job/xxl-job-2.3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/xxljob/v2_3_0/GlueJobHandlerInstrumentation.java create mode 100644 instrumentation/xxl-job/xxl-job-2.3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/xxljob/v2_3_0/MethodJobHandlerInstrumentation.java create mode 100644 instrumentation/xxl-job/xxl-job-2.3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/xxljob/v2_3_0/ScriptJobHandlerInstrumentation.java create mode 100644 instrumentation/xxl-job/xxl-job-2.3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/xxljob/v2_3_0/SimpleJobHandlerInstrumentation.java create mode 100644 instrumentation/xxl-job/xxl-job-2.3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/xxljob/v2_3_0/XxlJobInstrumentationModule.java create mode 100644 instrumentation/xxl-job/xxl-job-2.3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/xxljob/v2_3_0/XxlJobSingletons.java create mode 100644 instrumentation/xxl-job/xxl-job-2.3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/xxljob/v2_3_0/CustomizedFailedHandler.java create mode 100644 instrumentation/xxl-job/xxl-job-2.3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/xxljob/v2_3_0/SimpleCustomizedHandler.java create mode 100644 instrumentation/xxl-job/xxl-job-2.3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/xxljob/v2_3_0/XxlJobTest.java create mode 100644 instrumentation/xxl-job/xxl-job-common/javaagent/build.gradle.kts create mode 100644 instrumentation/xxl-job/xxl-job-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/xxljob/common/XxlJobCodeAttributesGetter.java create mode 100644 instrumentation/xxl-job/xxl-job-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/xxljob/common/XxlJobConstants.java create mode 100644 instrumentation/xxl-job/xxl-job-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/xxljob/common/XxlJobExperimentalAttributeExtractor.java create mode 100644 instrumentation/xxl-job/xxl-job-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/xxljob/common/XxlJobHelper.java create mode 100644 instrumentation/xxl-job/xxl-job-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/xxljob/common/XxlJobInstrumenterFactory.java create mode 100644 instrumentation/xxl-job/xxl-job-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/xxljob/common/XxlJobProcessRequest.java create mode 100644 instrumentation/xxl-job/xxl-job-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/xxljob/common/XxlJobSpanNameExtractor.java create mode 100644 instrumentation/xxl-job/xxl-job-common/testing/build.gradle.kts create mode 100644 instrumentation/xxl-job/xxl-job-common/testing/src/main/java/io/opentelemetry/instrumentation/xxljob/AbstractXxlJobTest.java create mode 100644 instrumentation/xxl-job/xxl-job-common/testing/src/main/java/io/opentelemetry/instrumentation/xxljob/CustomizedFailedHandler.java create mode 100644 instrumentation/xxl-job/xxl-job-common/testing/src/main/java/io/opentelemetry/instrumentation/xxljob/ReflectiveMethodsFactory.java create mode 100644 instrumentation/xxl-job/xxl-job-common/testing/src/main/java/io/opentelemetry/instrumentation/xxljob/SimpleCustomizedHandler.java create mode 100644 instrumentation/xxl-job/xxl-job-common/testing/src/main/java/io/opentelemetry/instrumentation/xxljob/XxlJobTestingConstants.java create mode 100644 javaagent-bootstrap/src/main/java/io/opentelemetry/javaagent/bootstrap/IndyBootstrapDispatcher.java create mode 100644 javaagent-bootstrap/src/test/java/io/opentelemetry/javaagent/bootstrap/IndyBootstrapDispatcherTest.java create mode 100644 javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/bootstrap/internal/AgentCommonConfig.java create mode 100644 javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/bootstrap/internal/AgentInstrumentationConfig.java delete mode 100644 javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/bootstrap/internal/CommonConfig.java create mode 100644 javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/bootstrap/internal/ConfiguredResourceAttributesHolder.java create mode 100644 javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/bootstrap/internal/JavaagentHttpClientInstrumenters.java create mode 100644 javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/extension/instrumentation/internal/AsmApi.java create mode 100644 javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/extension/instrumentation/internal/ExperimentalInstrumentationModule.java create mode 100644 javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/extension/instrumentation/internal/injection/ClassInjector.java create mode 100644 javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/extension/instrumentation/internal/injection/InjectionMode.java create mode 100644 javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/extension/instrumentation/internal/injection/ProxyInjectionBuilder.java create mode 100644 javaagent-tooling/jdk18-testing/build.gradle.kts create mode 100644 javaagent-tooling/jdk18-testing/src/main/java/testing/TestResourceProvider.java create mode 100644 javaagent-tooling/jdk18-testing/src/test/java/io/opentelemetry/javaagent/tooling/inetaddress/InetAddressResolverTest.java create mode 100644 javaagent-tooling/jdk18-testing/src/test/java/io/opentelemetry/javaagent/tooling/inetaddress/TestAddressResolver.java create mode 100644 javaagent-tooling/jdk18-testing/src/test/java/io/opentelemetry/javaagent/tooling/inetaddress/TestAddressResolverProvider.java create mode 100644 javaagent-tooling/jdk18-testing/src/test/resources/META-INF/services/java.net.spi.InetAddressResolverProvider rename javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/{AutoVersionResourceProvider.java => DistroVersionResourceProvider.java} (53%) create mode 100644 javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/config/OtlpProtocolPropertiesSupplier.java create mode 100644 javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/ignore/CommonLibraryIgnoredTypesConfigurer.java create mode 100644 javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/ignore/UserExcludedClassLoadersConfigurer.java create mode 100644 javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/instrumentation/indy/AdviceSignatureEraser.java create mode 100644 javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/instrumentation/indy/AdviceTransformer.java create mode 100644 javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/instrumentation/indy/ClassInjectorImpl.java create mode 100644 javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/instrumentation/indy/ForceDynamicallyTypedAssignReturnedFactory.java create mode 100644 javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/instrumentation/indy/IndyBootstrap.java create mode 100644 javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/instrumentation/indy/IndyModuleRegistry.java create mode 100644 javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/instrumentation/indy/IndyProxyFactory.java create mode 100644 javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/instrumentation/indy/IndyTypeTransformerImpl.java create mode 100644 javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/instrumentation/indy/InstrumentationModuleClassLoader.java create mode 100644 javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/instrumentation/indy/LookupExposer.java create mode 100644 javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/instrumentation/indy/OriginalDescriptor.java create mode 100644 javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/instrumentation/indy/PatchByteCodeVersionTransformer.java create mode 100644 javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/util/ClassLoaderMap.java create mode 100644 javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/util/ClassLoaderValue.java create mode 100644 javaagent-tooling/src/main/java/io/opentelemetry/sdk/autoconfigure/SdkAutoconfigureAccess.java create mode 100644 javaagent-tooling/src/test/java/io/opentelemetry/javaagent/tooling/config/OtlpProtocolPropertiesSupplierTest.java create mode 100644 javaagent-tooling/src/test/java/io/opentelemetry/javaagent/tooling/ignore/UserExcludedClassLoadersConfigurerTest.java create mode 100644 javaagent-tooling/src/test/java/io/opentelemetry/javaagent/tooling/instrumentation/indy/ForceDynamicallyTypedAssignReturnedFactoryTest.java create mode 100644 javaagent-tooling/src/test/java/io/opentelemetry/javaagent/tooling/instrumentation/indy/IndyProxyFactoryTest.java create mode 100644 javaagent-tooling/src/test/java/io/opentelemetry/javaagent/tooling/instrumentation/indy/InstrumentationModuleClassLoaderTest.java create mode 100644 javaagent-tooling/src/test/java/io/opentelemetry/javaagent/tooling/instrumentation/indy/dummies/Bar.java create mode 100644 javaagent-tooling/src/test/java/io/opentelemetry/javaagent/tooling/instrumentation/indy/dummies/DummyAnnotation.java create mode 100644 javaagent-tooling/src/test/java/io/opentelemetry/javaagent/tooling/instrumentation/indy/dummies/Foo.java create mode 100644 javaagent-tooling/src/test/java/io/opentelemetry/javaagent/tooling/util/ClassLoaderValueTest.java create mode 100644 javaagent-tooling/src/testPatchBytecodeVersion/java/io/opentelemetry/javaagent/tooling/instrumentation/indy/ComputeFramesAsmVisitorWrapper.java create mode 100644 javaagent-tooling/src/testPatchBytecodeVersion/java/io/opentelemetry/javaagent/tooling/instrumentation/indy/OldBytecode.java create mode 100644 javaagent-tooling/src/testPatchBytecodeVersion/java/io/opentelemetry/javaagent/tooling/instrumentation/indy/PatchBytecodeVersionTest.java create mode 100644 javaagent-tooling/src/testPatchBytecodeVersion/java/io/opentelemetry/javaagent/tooling/instrumentation/indy/PatchTestAdvice.java create mode 100644 javaagent/src/test/java/io/opentelemetry/javaagent/ResourceProviderTest.java delete mode 100644 javaagent/src/test/java/io/opentelemetry/javaagent/util/GcUtils.java rename licenses/{byte-buddy-dep-1.14.5.jar => byte-buddy-dep-1.15.1.jar}/META-INF/LICENSE (100%) rename licenses/{byte-buddy-dep-1.14.5.jar => byte-buddy-dep-1.15.1.jar}/META-INF/NOTICE (100%) rename licenses/{jackson-core-2.15.2.jar => jackson-annotations-2.17.2.jar}/META-INF/LICENSE (100%) create mode 100644 licenses/jackson-annotations-2.17.2.jar/META-INF/NOTICE rename licenses/{zipkin-reporter-2.16.3.jar => jackson-core-2.17.2.jar}/META-INF/LICENSE (99%) rename licenses/{jackson-core-2.15.2.jar => jackson-core-2.17.2.jar}/META-INF/NOTICE (100%) rename licenses/{zipkin-sender-okhttp3-2.16.3.jar => jackson-databind-2.17.2.jar}/META-INF/LICENSE (99%) create mode 100644 licenses/jackson-databind-2.17.2.jar/META-INF/NOTICE rename licenses/{jackson-jr-objects-2.15.2.jar => jackson-dataformat-yaml-2.17.2.jar}/META-INF/LICENSE (78%) rename licenses/{jackson-jr-objects-2.15.2.jar => jackson-dataformat-yaml-2.17.2.jar}/META-INF/NOTICE (89%) rename licenses/{okhttp-4.11.0.jar => okhttp-4.12.0.jar}/okhttp3/internal/publicsuffix/NOTICE (100%) rename licenses/{slf4j-simple-2.0.7.jar => slf4j-api-2.0.16.jar}/META-INF/LICENSE.txt (95%) rename licenses/{slf4j-api-2.0.7.jar => slf4j-simple-2.0.16.jar}/META-INF/LICENSE.txt (95%) rename licenses/{zipkin-2.23.2.jar => zipkin-2.27.1.jar}/META-INF/LICENSE (100%) create mode 100644 licenses/zipkin-reporter-3.4.0.jar/META-INF/LICENSE create mode 100644 licenses/zipkin-sender-okhttp3-3.4.0.jar/META-INF/LICENSE create mode 100644 muzzle/src/main/java/io/opentelemetry/javaagent/tooling/ByteArrayUrl.java create mode 100644 muzzle/src/main/java/io/opentelemetry/javaagent/tooling/BytecodeWithUrl.java create mode 100644 muzzle/src/main/java/io/opentelemetry/javaagent/tooling/HelperClassDefinition.java create mode 100644 muzzle/src/test/java/io/opentelemetry/javaagent/tooling/ByteArrayUrlTest.java create mode 100644 muzzle/src/test/java/muzzle/other/OtherTestClasses.java create mode 100644 sdk-autoconfigure-support/build.gradle.kts create mode 100644 sdk-autoconfigure-support/src/main/java/io/opentelemetry/instrumentation/resources/ResourceProviderPropertiesCustomizer.java create mode 100644 sdk-autoconfigure-support/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizerProvider create mode 100644 sdk-autoconfigure-support/src/test/java/io/opentelemetry/instrumentation/resources/ResourceProviderPropertiesCustomizerTest.java create mode 100644 sdk-autoconfigure-support/src/test/java/io/opentelemetry/sdk/autoconfigure/SdkAutoconfigureAccess.java create mode 100644 sdk-autoconfigure-support/src/test/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.ResourceProvider create mode 100644 smoke-tests-otel-starter/spring-boot-2/build.gradle.kts create mode 100644 smoke-tests-otel-starter/spring-boot-2/src/main/java/io/opentelemetry/spring/smoketest/OtelSpringStarterSmokeTestApplication.java create mode 100644 smoke-tests-otel-starter/spring-boot-2/src/test/java/io/opentelemetry/spring/smoketest/KafkaSpringStarterSmokeTest.java create mode 100644 smoke-tests-otel-starter/spring-boot-2/src/test/java/io/opentelemetry/spring/smoketest/MongoSpringStarterSmokeTest.java create mode 100644 smoke-tests-otel-starter/spring-boot-2/src/test/java/io/opentelemetry/spring/smoketest/OtelSpringStarterSmokeTest.java create mode 100644 smoke-tests-otel-starter/spring-boot-3.2/build.gradle.kts create mode 100644 smoke-tests-otel-starter/spring-boot-3.2/src/main/java/io/opentelemetry/spring/smoketest/OtelSpringStarterSmokeTestApplication.java create mode 100644 smoke-tests-otel-starter/spring-boot-3.2/src/main/java/io/opentelemetry/spring/smoketest/RuntimeHints.java create mode 100644 smoke-tests-otel-starter/spring-boot-3.2/src/test/java/io/opentelemetry/spring/smoketest/OtelSpringStarterSmokeTest.java create mode 100644 smoke-tests-otel-starter/spring-boot-3/build.gradle.kts create mode 100644 smoke-tests-otel-starter/spring-boot-3/src/main/java/io/opentelemetry/spring/smoketest/OtelSpringStarterSmokeTestApplication.java create mode 100644 smoke-tests-otel-starter/spring-boot-3/src/main/java/io/opentelemetry/spring/smoketest/RuntimeHints.java create mode 100644 smoke-tests-otel-starter/spring-boot-3/src/test/java/io/opentelemetry/spring/smoketest/GraalVmNativeKafkaSpringStarterSmokeTest.java create mode 100644 smoke-tests-otel-starter/spring-boot-3/src/test/java/io/opentelemetry/spring/smoketest/GraalVmNativeMongodbSpringStarterSmokeTest.java create mode 100644 smoke-tests-otel-starter/spring-boot-3/src/test/java/io/opentelemetry/spring/smoketest/KafkaSpringStarterSmokeTest.java create mode 100644 smoke-tests-otel-starter/spring-boot-3/src/test/java/io/opentelemetry/spring/smoketest/MongoSpringStarterSmokeTest.java create mode 100644 smoke-tests-otel-starter/spring-boot-3/src/test/java/io/opentelemetry/spring/smoketest/OtelSpringStarterDisabledSmokeTest.java create mode 100644 smoke-tests-otel-starter/spring-boot-3/src/test/java/io/opentelemetry/spring/smoketest/OtelSpringStarterSmokeTest.java create mode 100644 smoke-tests-otel-starter/spring-boot-3/src/test/java/io/opentelemetry/spring/smoketest/OtelSpringStarterUserDataSourceBeanTest.java create mode 100644 smoke-tests-otel-starter/spring-boot-common/build.gradle.kts create mode 100644 smoke-tests-otel-starter/spring-boot-common/src/main/java/io/opentelemetry/spring/smoketest/AbstractJvmKafkaSpringStarterSmokeTest.java create mode 100644 smoke-tests-otel-starter/spring-boot-common/src/main/java/io/opentelemetry/spring/smoketest/AbstractJvmMongodbSpringStarterSmokeTest.java create mode 100644 smoke-tests-otel-starter/spring-boot-common/src/main/java/io/opentelemetry/spring/smoketest/AbstractKafkaSpringStarterSmokeTest.java create mode 100644 smoke-tests-otel-starter/spring-boot-common/src/main/java/io/opentelemetry/spring/smoketest/AbstractMongodbSpringStarterSmokeTest.java create mode 100644 smoke-tests-otel-starter/spring-boot-common/src/main/java/io/opentelemetry/spring/smoketest/AbstractOtelSpringStarterSmokeTest.java create mode 100644 smoke-tests-otel-starter/spring-boot-common/src/main/java/io/opentelemetry/spring/smoketest/DatasourceConfig.java create mode 100644 smoke-tests-otel-starter/spring-boot-common/src/main/java/io/opentelemetry/spring/smoketest/OtelSpringStarterSmokeTestController.java create mode 100644 smoke-tests-otel-starter/spring-boot-common/src/main/java/io/opentelemetry/spring/smoketest/SpringComponent.java create mode 100644 smoke-tests-otel-starter/spring-boot-common/src/main/resources/META-INF/native-image/reflect-config.json create mode 100644 smoke-tests-otel-starter/spring-boot-common/src/main/resources/application.yaml create mode 100644 smoke-tests-otel-starter/spring-boot-reactive-2/build.gradle.kts create mode 100644 smoke-tests-otel-starter/spring-boot-reactive-2/src/test/java/io/opentelemetry/spring/smoketest/OtelReactiveSpringStarterSmokeTest.java create mode 100644 smoke-tests-otel-starter/spring-boot-reactive-3/build.gradle.kts create mode 100644 smoke-tests-otel-starter/spring-boot-reactive-3/src/test/java/io/opentelemetry/spring/smoketest/OtelReactiveSpringStarterSmokeTest.java create mode 100644 smoke-tests-otel-starter/spring-boot-reactive-common/build.gradle.kts create mode 100644 smoke-tests-otel-starter/spring-boot-reactive-common/src/main/java/io/opentelemetry/spring/smoketest/AbstractOtelReactiveSpringStarterSmokeTest.java create mode 100644 smoke-tests-otel-starter/spring-boot-reactive-common/src/main/java/io/opentelemetry/spring/smoketest/OtelReactiveSpringStarterSmokeTestApplication.java create mode 100644 smoke-tests-otel-starter/spring-boot-reactive-common/src/main/java/io/opentelemetry/spring/smoketest/OtelReactiveSpringStarterSmokeTestController.java create mode 100644 smoke-tests-otel-starter/spring-boot-reactive-common/src/main/java/io/opentelemetry/spring/smoketest/Player.java create mode 100644 smoke-tests-otel-starter/spring-boot-reactive-common/src/main/java/io/opentelemetry/spring/smoketest/PlayerRepository.java create mode 100644 smoke-tests-otel-starter/spring-boot-reactive-common/src/main/resources/application.yaml create mode 100644 smoke-tests-otel-starter/spring-boot-reactive-common/src/main/resources/schema.sql create mode 100644 smoke-tests-otel-starter/spring-smoke-testing/build.gradle.kts create mode 100644 smoke-tests-otel-starter/spring-smoke-testing/src/main/java/io/opentelemetry/spring/smoketest/AbstractSpringStarterSmokeTest.java create mode 100644 smoke-tests-otel-starter/spring-smoke-testing/src/main/java/io/opentelemetry/spring/smoketest/HttpSpanDataAssert.java create mode 100644 smoke-tests-otel-starter/spring-smoke-testing/src/main/java/io/opentelemetry/spring/smoketest/RequiresDockerCompose.java create mode 100644 smoke-tests-otel-starter/spring-smoke-testing/src/main/java/io/opentelemetry/spring/smoketest/SpringSmokeOtelConfiguration.java create mode 100644 smoke-tests-otel-starter/spring-smoke-testing/src/main/java/io/opentelemetry/spring/smoketest/SpringSmokeTestRunner.java create mode 100644 smoke-tests/images/early-jdk8/Dockerfile create mode 100644 smoke-tests/images/early-jdk8/build.gradle.kts create mode 100644 testing-common/integration-tests/src/main/java/IndyIbmResourceLevelInstrumentationModule.java create mode 100644 testing-common/integration-tests/src/main/java/IndyResourceLevelInstrumentation.java rename testing-common/integration-tests/src/main/java/{IbmResourceLevelInstrumentationModule.java => InlineIbmResourceLevelInstrumentationModule.java} (65%) rename testing-common/integration-tests/src/main/java/{ResourceLevelInstrumentation.java => InlineResourceLevelInstrumentation.java} (92%) create mode 100644 testing-common/integration-tests/src/main/java/field/VirtualFieldTestHelper.java create mode 100644 testing-common/integration-tests/src/main/java/field/VirtualFieldTestInstrumentationModule.java create mode 100644 testing-common/integration-tests/src/main/java/indy/ProxyMe.java create mode 100644 testing-common/integration-tests/src/main/java/io/opentelemetry/javaagent/IndyInstrumentationTestModule.java create mode 100644 testing-common/integration-tests/src/test/java/field/VirtualFieldTest.java create mode 100644 testing-common/integration-tests/src/test/java/indy/IndyInstrumentationTest.java create mode 100644 testing-common/library-for-integration-tests/src/main/java/library/MyProxySuperclass.java rename instrumentation/redisson/redisson-3.0/javaagent/src/test/groovy/RedissonClientTest.groovy => testing-common/library-for-integration-tests/src/main/java/library/VirtualFieldTestClass.java (57%) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 7971d1daf5b2..e1c0e8066f1a 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,6 +1,6 @@ # # Learn about membership in OpenTelemetry community: -# https://github.com/open-telemetry/community/blob/main/community-membership.md +# https://github.com/open-telemetry/community/blob/main/guides/contributor/membership.md # # # Learn about CODEOWNERS file format: diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index 25db8e92b3a8..000000000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -name: Bug report -about: Create a report to help us improve -title: '' -labels: bug -assignees: '' - ---- - -**Describe the bug** -A clear and concise description of what the bug is. - -**Steps to reproduce** -If possible, provide a recipe for reproducing the error. - -**What did you expect to see?** -A clear and concise description of what you expected to see. - -**What did you see instead?** -A clear and concise description of what you saw instead. - -**What version are you using?** -(e.g., `v0.9.0`, `393e4a2`, etc) - -**Environment** -Compiler: (e.g., "AdoptOpenJDK 11.0.6") -OS: (e.g., "Ubuntu 20.04") -Runtime (if different from JDK above): (e.g., "Oracle JRE 8u251") -OS (if different from OS compiled on): (e.g., "Windows Server 2019") - -**Additional context** -Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 000000000000..69009d589fc6 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,50 @@ +name: Bug report +description: Create a report to help us improve +labels: ["bug", "needs triage"] +body: + - type: textarea + attributes: + label: Describe the bug + description: A clear and concise description of what the bug is. + validations: + required: true + - type: textarea + attributes: + label: Steps to reproduce + description: A recipe for reproducing the error. If possible, provide a link to a repository containing an executable repro scenario. + validations: + required: true + - type: textarea + attributes: + label: Expected behavior + description: A clear and concise description of what you expected to see. + validations: + required: true + - type: textarea + attributes: + label: Actual behavior + description: A clear and concise description of what you saw instead. + validations: + required: true + - type: input + attributes: + label: Javaagent or library instrumentation version + description: (e.g., `v0.9.0`, `393e4a2`, etc) + validations: + required: true + - type: textarea + attributes: + label: Environment + description: | + Runtime environment information, including the runtime JDK distribution, OS, used library versions, javaagent configuration, etc. + Example: + > JDK: Temurin 17.0.7 + > OS: Ubuntu 20.04 + > Spring Boot 2.7.15, Hibernate 5.0.0, ... + value: | + **JDK**: + **OS**: + - type: textarea + attributes: + label: Additional context + description: Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 000000000000..6fc1351d76ec --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,10 @@ +contact_links: + - name: GitHub Discussions + url: https://github.com/open-telemetry/opentelemetry-java-instrumentation/discussions/new/choose + about: Please ask questions here. + - name: StackOverflow + url: https://stackoverflow.com/questions/ask?tags=open-telemetry+java + about: Or on StackOverflow. + - name: Slack + url: https://cloud-native.slack.com/archives/C014L2KCTE3 + about: Or the `#otel-java` channel in the CNCF Slack instance. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md deleted file mode 100644 index 19f881377d6e..000000000000 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -name: Feature request -about: Suggest an idea for this project -title: '' -labels: enhancement -assignees: '' - ---- - -**Is your feature request related to a problem? Please describe.** -A clear and concise description of what the problem is. - -**Describe the solution you'd like** -A clear and concise description of what you want to happen. - -**Describe alternatives you've considered** -A clear and concise description of any alternative solutions or features you've considered. - -**Additional context** -Add any other context or screenshots about the feature request here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 000000000000..35a6dbeefb7b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,24 @@ +name: Feature request +description: Suggest an idea for this project +labels: ["enhancement", "needs triage"] +body: + - type: textarea + attributes: + label: Is your feature request related to a problem? Please describe. + description: A clear and concise description of what the problem is. + validations: + required: true + - type: textarea + attributes: + label: Describe the solution you'd like + description: A clear and concise description of what you want to happen. + validations: + required: true + - type: textarea + attributes: + label: Describe alternatives you've considered + description: A clear and concise description of any alternative solutions or features you've considered. + - type: textarea + attributes: + label: Additional context + description: Add any other context or screenshots about the feature request here. diff --git a/.github/dependabot.yml b/.github/dependabot.yml deleted file mode 100644 index e495fb2ef7e4..000000000000 --- a/.github/dependabot.yml +++ /dev/null @@ -1,100 +0,0 @@ -version: 2 -registries: - gradle-plugin-portal: - type: maven-repository - url: https://plugins.gradle.org/m2 - username: dummy # Required by dependabot - password: dummy # Required by dependabot -updates: - - package-ecosystem: "github-actions" - directory: "/" - rebase-strategy: "disabled" - schedule: - interval: "daily" - labels: # overriding the default which is to add both "dependencies" and "github_actions" - - "dependencies" - - - package-ecosystem: "gradle" - directory: "/" - registries: - - gradle-plugin-portal - ignore: - - dependency-name: "io.opentelemetry:*" - # OpenTelemetry SDK updates are handled by auto-update-otel-sdk.yml - versions: [ "(,)" ] - - dependency-name: "ch.qos.logback:logback-classic" - # logback 1.4+ requires Java 11 - versions: [ "[1.4,)" ] - - dependency-name: "com.bmuschko.docker-remote-api" - # Publishing Servlet images for smoke tests is failing starting from 9.0.0 - # (see https://github.com/open-telemetry/opentelemetry-java-instrumentation/issues/7185) - # in particular, these commands are failing (reproducible locally): - # ./gradlew :smoke-tests:images:servlet:buildLinuxTestImages pushMatrix -PsmokeTestServer=jetty - # ./gradlew :smoke-tests:images:servlet:buildWindowsTestImages pushMatrix -PsmokeTestServer=jetty - versions: [ "[9,)" ] - - dependency-name: "net.sf.jt400:jt400" - # using old version of this obscure artifact to test instrumentation of Java 1.1 bytecode - versions: [ "(,)" ] - - dependency-name: "org.mockito:*" - # mockito 5 requires Java 11 - versions: [ "[5,)" ] - - dependency-name: "org.junit-pioneer:junit-pioneer" - # junit-pioneer 2.x requires Java 11 - versions: [ "[1,)" ] - rebase-strategy: "disabled" - schedule: - interval: "daily" - labels: # overriding the default which is to add both "dependencies" and "java" - - "dependencies" - open-pull-requests-limit: 10 - - - package-ecosystem: "gradle" - directory: "/examples/distro" - ignore: - - dependency-name: "javax.servlet:javax.servlet-api" - # examples are intentionally compiled and tested against Servlet 3.0 - versions: [ "[3.1,)" ] - - dependency-name: "org.eclipse.jetty:jetty-server" - # examples are intentionally compiled and tested against Servlet 3.0 - # "9-alpha" is needed to precede all 9.0.0.v{DATE} versions - versions: [ "[9-alpha,)" ] - - dependency-name: "org.eclipse.jetty:jetty-servlet" - # examples are intentionally compiled and tested against Servlet 3.0 - # "9-alpha" is needed to precede all 9.0.0.v{DATE} versions - versions: [ "[9-alpha,)" ] - rebase-strategy: "disabled" - schedule: - interval: "daily" - labels: # overriding the default which is to add both "dependencies" and "java" - - "dependencies" - open-pull-requests-limit: 10 - - - package-ecosystem: "gradle" - directory: "/examples/extension" - ignore: - - dependency-name: "javax.servlet:javax.servlet-api" - # examples are intentionally compiled and tested against Servlet 3.0 - versions: [ "[3.1,)" ] - - dependency-name: "org.eclipse.jetty:jetty-server" - # examples are intentionally compiled and tested against Servlet 3.0 - # "9-alpha" is needed to precede all 9.0.0.v{DATE} versions - versions: [ "[9-alpha,)" ] - - dependency-name: "org.eclipse.jetty:jetty-servlet" - # examples are intentionally compiled and tested against Servlet 3.0 - # "9-alpha" is needed to precede all 9.0.0.v{DATE} versions - versions: [ "[9-alpha,)" ] - rebase-strategy: "disabled" - schedule: - interval: "daily" - labels: # overriding the default which is to add both "dependencies" and "java" - - "dependencies" - open-pull-requests-limit: 10 - - - package-ecosystem: "gradle" - directory: "/benchmark-overhead" - rebase-strategy: "disabled" - schedule: - interval: "daily" - labels: # overriding the default which is to add both "dependencies" and "java" - - "dependencies" - open-pull-requests-limit: 10 diff --git a/.github/graal-native-docker-compose.yaml b/.github/graal-native-docker-compose.yaml new file mode 100644 index 000000000000..bdcf87f31317 --- /dev/null +++ b/.github/graal-native-docker-compose.yaml @@ -0,0 +1,31 @@ +services: + mongodb: + image: mongo:4.0 + ports: + - "27017:27017" + + zookeeper: + image: confluentinc/cp-zookeeper:6.2.10 + environment: + ZOOKEEPER_CLIENT_PORT: 2181 + ZOOKEEPER_TICK_TIME: 2000 + ports: + - "22181:2181" + + kafka: + image: confluentinc/cp-kafka:6.2.10 + ports: + - 9094:9094 + depends_on: + - zookeeper + environment: + KAFKA_BROKER_ID: 1 + KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181 + KAFKA_LISTENERS: INTERNAL://0.0.0.0:9092,OUTSIDE://0.0.0.0:9094 + KAFKA_ADVERTISED_LISTENERS: INTERNAL://kafka:9092,OUTSIDE://localhost:9094 + KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: INTERNAL:PLAINTEXT,OUTSIDE:PLAINTEXT + KAFKA_INTER_BROKER_LISTENER_NAME: INTERNAL + KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 + KAFKA_DEFAULT_REPLICATION_FACTOR: 1 + KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1 + KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1 diff --git a/.github/labeler.yml b/.github/labeler.yml new file mode 100644 index 000000000000..6581c76306bd --- /dev/null +++ b/.github/labeler.yml @@ -0,0 +1,10 @@ +test native: +- all: + - changed-files: + - any-glob-to-any-file: + - instrumentation/logback/logback-appender-10/library/** + - instrumentation/jdbc/library/** + - instrumentation/spring/** + - smoke-tests-otel-starter/** + - dependencyManagement/build.gradle.kts + - all-globs-to-all-files: '!instrumentation/spring/**/javaagent/**' diff --git a/.github/renovate.json5 b/.github/renovate.json5 new file mode 100644 index 000000000000..db5d36725457 --- /dev/null +++ b/.github/renovate.json5 @@ -0,0 +1,216 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": [ + "config:base" + ], + "ignorePaths": ["instrumentation/**"], + "baseBranches": ["main", "release/v1.33.x"], + // needed in order to get patch-only updates in package rules below + // unfortunately you can't combine updateTypes and separateMinorPatch in the same package rule + // so we have to apply it globally here, see + // https://github.com/renovatebot/renovate/discussions/8399#discussioncomment-305798 + "separateMinorPatch": true, + "packageRules": [ + { + // this is to reduce the number of renovate PRs by consolidating them into a weekly batch + "matchManagers": ["github-actions"], + "extends": ["schedule:weekly"], + "groupName": "github actions", + "separateMinorPatch": false // overrides separateMinorPatch specified above + }, + { + "matchPackageNames": [ + "io.opentelemetry.contrib:opentelemetry-aws-resources", + "io.opentelemetry.contrib:opentelemetry-aws-xray-propagator", + "io.opentelemetry.contrib:opentelemetry-gcp-resources", + "io.opentelemetry.contrib:opentelemetry-baggage-processor", + "io.opentelemetry.proto:opentelemetry-proto", + "io.opentelemetry.semconv:opentelemetry-semconv" + ], + // Renovate's default behavior is only to update from unstable -> unstable if it's for the + // major.minor.patch, under the assumption that you would want to update to the stable version + // of that release instead of the unstable version for a future release + // (TODO remove once the artifacts above release stable versions) + "ignoreUnstable": false, + "allowedVersions": "!/\\-SNAPSHOT$/" + }, + { + // we are not going to update the semconv in 1.x branch + "matchPackageNames": ["io.opentelemetry.semconv:opentelemetry-semconv"], + "matchUpdateTypes": ["minor"], + "matchBaseBranches": ["release/v1.33.x"], + "enabled": false + }, + { + "matchPackagePrefixes": ["ch.qos.logback:"], + "groupName": "logback packages" + }, + { + "matchPackagePrefixes": ["com.google.guava:"], + "groupName": "guava packages" + }, + { + "matchPackagePrefixes": ["io.quarkus"], + "groupName": "quarkus packages" + }, + { + "matchPackagePrefixes": ["com.gradle.enterprise"], + "groupName": "gradle enterprise packages" + }, + { + "matchPackagePrefixes": ["org.eclipse.jetty:"], + "groupName": "jetty packages" + }, + { + "matchPackagePrefixes": ["com.linecorp.armeria:"], + "groupName": "armeria packages" + }, + { + "matchPackagePrefixes": ["com.diffplug.spotless"], + "groupName": "spotless packages" + }, + { + "matchPackagePrefixes": ["net.bytebuddy:"], + "groupName": "byte buddy packages" + }, + { + "matchPackagePrefixes": ["com.fasterxml.jackson"], + "groupName": "jackson packages" + }, + { + // prevent update to 2.4-groovy-4.0-SNAPSHOT + "matchPackageNames": ["org.spockframework:spock-bom"], + "allowedVersions": "!/\\-SNAPSHOT$/" + }, + { + // prevent 3.0.1u2 -> 3.0.1 + "matchPackageNames": ["com.google.code.findbugs:annotations"], + "allowedVersions": "!/3\\.0\\.1$/" + }, + { + // OpenTelemetry SDK updates are handled by auto-update-otel-sdk.yml + "matchPackagePrefixes": ["io.opentelemetry:"], + "enabled": false + }, + { + // junit-pioneer 2+ requires Java 11+ + "matchPackageNames": ["org.junit-pioneer:junit-pioneer"], + "matchUpdateTypes": ["major"], + "enabled": false + }, + { + // mockito 5+ requires Java 11+ + "matchPackagePrefixes": ["org.mockito:"], + "matchUpdateTypes": ["major"], + "enabled": false + }, + { + // system-stubs-jupiter 2.1+ requires Java 11+ + "matchPackageNames": ["uk.org.webcompere:system-stubs-jupiter"], + "matchUpdateTypes": ["major", "minor"], + "enabled": false + }, + { + // intentionally using Spring Boot 2 in this smoke tests + // new versions of Spring Boot 3 are tested with + // https://github.com/open-telemetry/opentelemetry-java-instrumentation/blob/dc4330e0a3060bd7d8c4090ad0b8fc4727e68113/settings.gradle.kts#L43-L45 + "matchFileNames": [ + "smoke-tests/images/spring-boot/build.gradle.kts", + "smoke-tests-otel-starter/spring-boot-2/build.gradle.kts", + "smoke-tests-otel-starter/spring-boot-common/build.gradle.kts", + "smoke-tests-otel-starter/spring-boot-reactive-2/build.gradle.kts", + "smoke-tests-otel-starter/spring-boot-reactive-common/build.gradle.kts", + "smoke-tests-otel-starter/spring-smoke-testing/build.gradle.kts" + ], + "matchPackageNames": [ + "org.slf4j:slf4j-api", + "org.springframework.boot:org.springframework.boot.gradle.plugin", // this is for plugin id "org.springframework.boot" + "org.springframework.boot:spring-boot-dependencies"], + "matchUpdateTypes": ["major", "minor"], + "enabled": false, + }, + { + // intentionally using Spring Boot 2 in this smoke tests + "matchFileNames": [ + "smoke-tests-otel-starter/spring-boot-2/build.gradle.kts", + "smoke-tests-otel-starter/spring-boot-reactive-2/build.gradle.kts" + ], + "matchPackageNames": ["ch.qos.logback:logback-classic"], + "matchUpdateTypes": ["minor"], + "enabled": false, + }, + { + // intentionally using logback 1.2 in this smoke tests + "matchFileNames": ["smoke-tests/images/spring-boot/build.gradle.kts"], + "matchPackagePrefixes": ["ch.qos.logback:"], + "matchUpdateTypes": ["major", "minor"], + "enabled": false + }, + { + // intentionally using slf4j 1 in this smoke tests + "matchFileNames": ["smoke-tests/images/spring-boot/build.gradle.kts"], + "matchPackagePrefixes": ["org.slf4j:"], + "matchUpdateTypes": ["major"], + "enabled": false + }, + { + // intentionally aligning both netty 4.0 and 4.1 version in this convention + "matchFileNames": ["conventions/src/main/kotlin/otel.java-conventions.gradle.kts"], + "matchPackageNames": ["io.netty:netty-bom"], + "matchUpdateTypes": ["major", "minor"], + "enabled": false + }, + { + // intentionally using scala 2.11 in otel.scala-conventions.gradle.kts + "matchFileNames": ["conventions/src/main/kotlin/otel.scala-conventions.gradle.kts"], + "matchPackageNames": ["org.scala-lang:scala-library"], + "matchUpdateTypes": ["major", "minor"], + "enabled": false + }, + { + // intentionally using Java 11 in some examples + // not using matchUpdateTypes "major", because renovate wants to bump "11-jre" to "11.0.19_7-jre" + "matchPackageNames": ["eclipse-temurin"], + "enabled": false + }, + { + // using old version of this obscure artifact to test instrumentation of Java 1.1 bytecode + "matchPackageNames": ["net.sf.jt400:jt400"], + "matchCurrentVersion": "6.1", + "enabled": false + }, + { + // pinned version for compatibility + "matchPackageNames": ["javax.servlet:javax.servlet-api"], + "matchCurrentVersion": "3.0.1", + "enabled": false + }, + { + // pinned version for compatibility + "matchPackageNames": ["jakarta.servlet:jakarta.servlet-api"], + "matchCurrentVersion": "5.0.0", + "enabled": false + }, + { + // intentionally using logback 1.3 in dependency management (for Java 8 support) + "matchFileNames": ["dependencyManagement/build.gradle.kts"], + "matchPackagePrefixes": ["ch.qos.logback:"], + "matchUpdateTypes": ["major", "minor"], + "enabled": false + }, + { + // intentionally using Spring Boot 2 in dependency management (for Java 8 support) + "matchFileNames": ["dependencyManagement/build.gradle.kts"], + "matchPackagePrefixes": ["org.springframework.boot:spring-boot-dependencies"], + "matchUpdateTypes": ["major"], + "enabled": false + }, + { + // pinned version to Jetty 8 (Servlet 3.0) for compatibility + "matchFileNames": ["examples/distro/instrumentation/servlet-3/build.gradle"], + "matchPackagePrefixes": ["org.eclipse.jetty:"], + "matchUpdateTypes": ["major"], + "enabled": false + } + ] +} diff --git a/.github/repository-settings.md b/.github/repository-settings.md index a427bf26708c..630b62297d9a 100644 --- a/.github/repository-settings.md +++ b/.github/repository-settings.md @@ -6,18 +6,13 @@ settings](https://github.com/open-telemetry/community/blob/main/docs/how-to-conf ## General > Pull Requests -* Allow squash merging > Default to pull request title +- Allow squash merging > Default to pull request title -* Allow auto-merge - -## Collaborators and teams - -* [@opentelemetrybot](https://github.com/opentelemetrybot) has write permission in order to - enable [comment-driven PR automation](workflows/comment-driven-pr-automation.yml). +- Allow auto-merge ## Actions > General -* Fork pull request workflows from outside collaborators: +- Fork pull request workflows from outside collaborators: "Require approval for first-time contributors who are new to GitHub" (To reduce friction for new contributors, @@ -25,29 +20,39 @@ settings](https://github.com/open-telemetry/community/blob/main/docs/how-to-conf ## Branch protections +The order of branch protection rules +[can be important](https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/defining-the-mergeability-of-pull-requests/managing-a-branch-protection-rule#about-branch-protection-rules). +The branch protection rules below should be added before the `**/**` branch protection rule +(this may require deleting the `**/**` rule and recreating it at the end). + ### `main` -* Require branches to be up to date before merging: UNCHECKED +- Require branches to be up to date before merging: UNCHECKED (PR jobs take too long, and leaving this unchecked has not been a significant problem) -* Status checks that are required: +- Status checks that are required: - * EasyCLA - * required-status-check + - EasyCLA + - required-status-check ### `release/*` Same settings as above for [`main`](#main). -### `opentelemetrybot/**/**` and `gradlew-update-*` +### `cloudfoundry` + +Same settings as above for [`main`](#main), +except for the `required-status-check` required status check. + +### `renovate/**/**` and `opentelemetrybot/**/**` Same settings as for [`dependabot/**/**`](https://github.com/open-telemetry/community/blob/main/docs/how-to-configure-new-repository.md#branch-protection-rule-dependabot) ### `gh-pages` -* Everything UNCHECKED +- Everything UNCHECKED (This branch is currently only used for directly pushing benchmarking results from the [Nightly overhead benchmark](https://github.com/open-telemetry/opentelemetry-java-instrumentation/actions/workflows/nightly-benchmark-overhead.yml) @@ -55,20 +60,19 @@ for [`dependabot/**/**`](https://github.com/open-telemetry/community/blob/main/d ## Code security and analysis -* Secret scanning: Enabled +- Secret scanning: Enabled ## Secrets and variables > Actions -* `GE_CACHE_PASSWORD` -* `GE_CACHE_USERNAME` -* `GPG_PASSWORD` - stored in OpenTelemetry-Java 1Password -* `GPG_PRIVATE_KEY` - stored in OpenTelemetry-Java 1Password -* `GRADLE_ENTERPRISE_ACCESS_KEY` - owned by [@trask](https://github.com/trask) - * Generated at https://ge.opentelemetry.io > My settings > Access keys - * format of env var is `ge.opentelemetry.io=`, +- `GPG_PASSWORD` - stored in OpenTelemetry-Java 1Password +- `GPG_PRIVATE_KEY` - stored in OpenTelemetry-Java 1Password +- `GRADLE_ENTERPRISE_ACCESS_KEY` - owned by [@trask](https://github.com/trask) + - Generated at https://ge.opentelemetry.io > My settings > Access keys + - format of env var is `ge.opentelemetry.io=`, see [docs](https://docs.gradle.com/enterprise/gradle-plugin/#via_environment_variable) -* `GRADLE_PUBLISH_KEY` -* `GRADLE_PUBLISH_SECRET` -* `OPENTELEMETRYBOT_GITHUB_TOKEN` - owned by [@trask](https://github.com/trask) -* `SONATYPE_KEY` - owned by [@trask](https://github.com/trask) -* `SONATYPE_USER` - owned by [@trask](https://github.com/trask) +- `GRADLE_PUBLISH_KEY` +- `GRADLE_PUBLISH_SECRET` +- `NVD_API_KEY` - stored in OpenTelemetry-Java 1Password +- `OPENTELEMETRYBOT_GITHUB_TOKEN` - owned by [@trask](https://github.com/trask) +- `SONATYPE_KEY` - owned by [@trask](https://github.com/trask) +- `SONATYPE_USER` - owned by [@trask](https://github.com/trask) diff --git a/.github/scripts/draft-change-log-entries.sh b/.github/scripts/draft-change-log-entries.sh index dd68cdcdc228..7ebb9448355d 100755 --- a/.github/scripts/draft-change-log-entries.sh +++ b/.github/scripts/draft-change-log-entries.sh @@ -45,7 +45,7 @@ echo git log --reverse \ --perl-regexp \ - --author='^(?!dependabot\[bot\] )' \ + --author='^(?!renovate\[bot\] )' \ --pretty=format:"- %s" \ "$range" \ | sed -E 's,\(#([0-9]+)\)$,\n ([#\1](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/\1)),' diff --git a/.github/scripts/generate-release-contributors.sh b/.github/scripts/generate-release-contributors.sh index 70ee0d5d9668..e5a9cc605b42 100755 --- a/.github/scripts/generate-release-contributors.sh +++ b/.github/scripts/generate-release-contributors.sh @@ -83,6 +83,6 @@ echo "$contributors1" "$contributors2" \ | sort -uf \ | grep -v linux-foundation-easycla \ | grep -v github-actions \ - | grep -v dependabot \ + | grep -v renovate \ | grep -v opentelemetrybot \ | sed 's/^/@/' diff --git a/.github/scripts/gha-free-disk-space.sh b/.github/scripts/gha-free-disk-space.sh new file mode 100755 index 000000000000..5283984cd284 --- /dev/null +++ b/.github/scripts/gha-free-disk-space.sh @@ -0,0 +1,9 @@ +#!/bin/bash -e + +# GitHub Actions runners have only provide 14 GB of disk space which we have been exceeding regularly +# https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners#supported-runners-and-hardware-resources + +df -h +sudo rm -rf /usr/local/lib/android +sudo rm -rf /usr/share/dotnet +df -h diff --git a/.github/scripts/markdown-link-check-config.json b/.github/scripts/markdown-link-check-config.json index fcafb7041346..e132f40b6183 100644 --- a/.github/scripts/markdown-link-check-config.json +++ b/.github/scripts/markdown-link-check-config.json @@ -6,13 +6,22 @@ ], "ignorePatterns": [ { - "pattern": "https://github.com/open-telemetry/opentelemetry-java-instrumentation/network/updates" + "pattern": "^https://developer\\.mend\\.io/github/open-telemetry/opentelemetry-java-instrumentation$" }, { - "pattern": "^https://github.com/open-telemetry/opentelemetry-java-contrib/pulls/app%2Fdependabot$" + "pattern": "^https://github.com/open-telemetry/opentelemetry-java-instrumentation/pulls/app%2Frenovate" }, { "pattern": "^https://kotlinlang\\.org/docs/coroutines-overview\\.html$" + }, + { + "pattern": "^http(s)?://logback\\.qos\\.ch" + }, + { + "pattern": "^https://micrometer\\.io" + }, + { + "pattern": "^https://central\\.sonatype\\.com" } ] } diff --git a/.gitignore b/.gitignore index 6c4640f68103..b41b8d7d3387 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ target !**/gradle/wrapper/* .gradle **/build/ +**/generated/ examples/**/build/ # Eclipse # @@ -53,10 +54,12 @@ out/ /workspace java-agent/benchmark-integration/perf-test-settings.rc derby.log -.java-version hs_err_pid* replay_pid* .attach_pid* !java-agent/benchmark/releases/*.jar +.github/workflows/* +!.github/workflows/middleware-javaagent.yml + diff --git a/.java-version b/.java-version new file mode 100644 index 000000000000..b5045cc4046d --- /dev/null +++ b/.java-version @@ -0,0 +1 @@ +21 \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index eb545e90df7d..c83a89d393da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,1094 @@ ## Unreleased +## Version 1.33.6 (2024-08-26) + +### 📈 Enhancements + +- Backport: Update the OpenTelemetry SDK version to 1.41.0 + ([#12071](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/12071)) + +## Version 2.7.0 (2024-08-16) + +### 📈 Enhancements + +- Add span baggage processor + ([#11697](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/11697)) +- Improve tomcat version detection + ([#11936](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/11936)) +- Improve akka route handling with java dsl + ([#11926](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/11926)) +- Ignore Alibaba fastjson ASMClassLoader + ([#11954](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/11954)) +- Use `aws-lambda-java-serialization` library, which is available by default, while deserializing input and serializing output + ([#11868](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/11868)) +- Logback appender: map timestamp in nanoseconds if possible + ([#11974](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/11974)) +- Save ILoggingEvent.getArgumentArray() arguments from Logback + ([#11865](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/11865)) +- Update Java 17-based metrics to stable semconv + ([#11914](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/11914)) +- Add Pulsar Consumer metrics + ([#11891](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/11891)) + +### 🛠️ Bug fixes + +- Fix missing throw statement in RestClientWrapper + ([#11893](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/11893)) +- Fix ClickHouse tracing when database name not included in connection string + ([#11852](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/11852)) +- Fix class cast exception, noop meter does not implement incubating API + ([#11934](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/11934)) +- Closing a kafka producer/consumer should not disable metrics from other consumers/producers + ([#11975](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/11975)) +- Fix ending span in Ktor plugin + ([#11726](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/11726)) +- Fix netty memory leak + ([#12003](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/12003)) + +## Version 1.33.5 (2024-07-25) + +### 📈 Enhancements + +- Backport: Update the OpenTelemetry SDK version to 1.40.0 + ([#11879](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/11879)) + +## Version 2.6.0 (2024-07-17) + +The Spring Boot Starter (`opentelemetry-spring-boot-starter`) is now stable. + +### Migration notes + +- The `opentelemetry-spring-boot` and `opentelemetry-spring-boot-3` artifacts have been merged + into a single artifact named `opentelemetry-spring-boot-autoconfigure` + which supports both Spring Boot 2 and Spring Boot 3 +- Two experimental HTTP metrics have been renamed: + - `http.server.request.size` → `http.server.request.body.size`, + - `http.server.response.size` → `http.server.response.body.size` + +### 🌟 New javaagent instrumentation + +- Javalin + ([#11587](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/11587)) +- ClickHouse + ([#11660](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/11660)) + +### 📈 Enhancements + +- Support HTTP client instrumentation configuration in Spring starter + ([#11620](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/11620)) +- Influxdb client: don't fill `db.statement` for create/drop database and write operations + ([#11557](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/11557)) +- Support `otel.instrumentation.common.default-enabled` in the Spring starter + ([#11746](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/11746)) +- Support Jetty HTTP client 12 + ([#11519](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/11519)) +- Add Pulsar `messaging.producer.duration` metric + ([#11591](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/11591)) +- Improve instrumentation suppression behavior + ([#11640](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/11640)) +- Propagate OpenTelemetry context through custom AWS client context for Lambda direct calls + ([#11675](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/11675)) +- Spring Native support for `@WithSpan` + ([#11757](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/11757)) +- Support HTTP server instrumentation config properties in the Spring starter + ([#11667](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/11667)) + +### 🛠️ Bug fixes + +- Fix `http.server.active_requests` metric with async requests + ([#11638](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/11638)) + +## Version 1.33.4 (2024-06-19) + +### 📈 Enhancements + +- Backport: Undertow, run response customizer on all ServerConnection implementations + ([#11548](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/11548)) +- Backport: Improve security manager support + ([#11606](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/11606)) +- Backport: Update the OpenTelemetry SDK version to 1.39.0 + ([#11603](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/11603)) + +### 🛠️ Bug fixes + +- Backport: Avoid NullPointerException when JMS destination is not available + ([#11577](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/11577)) +- Backport: Fix Spring Kafka instrumentation closing the trace too early + ([#11592](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/11592)) +- Backport: Fix gRPC instrumentation adding duplicates to metadata instead of overwriting + ([#11604](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/11604)) +- Backport: Fix request header capture corrupting tomcat request + ([#11607](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/11607)) + +## Version 2.5.0 (2024-06-17) + +### 📈 Enhancements + +- Add support for Informix connection string parsing in JDBC instrumentation + ([#11542](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/11542)) +- Generate an SBOM for the javaagent artifact + ([#11075](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/11075)) +- Extract sql operation even when the sanitizer is disabled + ([#11472](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/11472)) +- Improve security manager support + ([#11466](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/11466)) +- Generate Log4j2Plugin.dat for OpenTelemetryAppender + ([#11503](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/11503)) +- Stop kotlin coroutine dispatcher from propagating context + ([#11500](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/11500)) +- Handle Vert.x sub routes + ([#11535](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/11535)) +- Undertow: run response customizer on all ServerConnection implementations + ([#11539](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/11539)) +- Allow configuring MDC key names for trace_id, span_id, trace_flags + ([#11329](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/11329)) +- Apply async end strategy to all kotlin coroutine flows + ([#11583](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/11583)) + +### 🛠️ Bug fixes + +- Fix container.id issue in some crio scenarios + ([#11382](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/11382)) +- Fix Finagle http client context propagation + ([#11400](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/11400)) +- Fix sporadically failing finagle test + ([#11441](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/11441)) +- Fix request header capture corrupting tomcat request + ([#11469](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/11469)) +- Fix Ktor server instrumentation when Ktor client library is not present + ([#11454](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/11454)) +- Fix gRPC instrumentation adding duplicates to metadata instead of overwriting + ([#11308](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/11308)) +- Avoid NullPointerException when JMS destination is not available + ([#11570](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/11570)) +- Fix Spring Kafka instrumentation closing the trace too early + ([#11471](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/11471)) + +## Version 1.33.3 (2024-05-21) + +### 📈 Enhancements + +- Backport: Fix the logic to get container.id resource attribute + ([#11333](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/11333)) +- Backport: Update the OpenTelemetry SDK version to 1.38.0 + ([#11386](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/11386)) +- Backport: Fix a bug in undertow instrumentation related to HTTP/2 + ([#11387](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/11387)) +- Backport: Fix container.id issue in some crio scenarios + ([#11405](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/11405)) + +## Version 2.4.0 (2024-05-18) + +### 🌟 New javaagent instrumentation + +- InfluxDB + ([#10850](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/10850)) +- Armeria gRPC + ([#11351](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/11351)) +- Apache ShenYu + ([#11260](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/11260)) + +### 📈 Enhancements + +- Instrument ConnectionSource in Akka/Pekko HTTP Servers + ([#11103](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/11103)) +- Use constant span name when using Spring AMQP AnonymousQueues + ([#11141](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/11141)) +- Add support for `RestClient` in Spring starter + ([#11038](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/11038)) +- Add support for WebFlux server in Spring starter + ([#11185](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/11185)) +- Add async operation end strategy for Kotlin coroutines flow + ([#11168](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/11168)) +- Add automatic JDBC instrumentation to the Spring starter + ([#11258](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/11258)) +- Add `StructuredTaskScope` instrumentation + ([#11202](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/11202)) +- Allow reading OTel context from reactor ContextView + ([#11235](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/11235)) +- Add spring starter r2dbc support + ([#11221](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/11221)) +- Enable instrumentation of Spring EJB clients + ([#11104](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/11104)) +- Support `otel.instrumentation.kafka.experimental-span-attributes` in Spring starter + ([#11263](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/11263)) +- Remove incubating semconv dependency from library instrumentation + ([#11324](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/11324)) +- Add extension functions for Ktor plugins + ([#10963](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/10963)) +- Add dedicated flag for R2DBC statement sanitizer + ([#11384](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/11384)) +- Allow library instrumentations to override span name + ([#11355](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/11355)) +- Don't sanitize PostgreSQL parameter markers + ([#11388](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/11388)) +- Make statement sanitizer configurable for Spring Boot + ([#11350](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/11350)) + +### 🛠️ Bug fixes + +- Fix GraphQL instrumentation to work with latest version + ([#11142](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/11142)) +- Fix jmx-metrics on WildFly + ([#11151](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/11151)) +- End gRPC server span in onComplete instead of close + ([#11170](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/11170)) +- Fix a bug in undertow instrumentation related to HTTP/2 + ([#11361](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/11361)) +- Armeria http client reports wrong protocol version + ([#11334](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/11334)) +- Use daemon thread for scheduling in jmx-metrics BeanFinder + ([#11337](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/11337)) + +## Version 1.33.2 (2024-04-20) + +### 📈 Enhancements + +- Backport: elasticsearch-java 7.17.20 has native instrumentation + ([#11098](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/11098)) +- Update the OpenTelemetry SDK version to 1.37.0 + ([#11118](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/11118)) +- Backport: graphql-java-22.0 support + ([#11171](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/11171)) + +## Version 2.3.0 (2024-04-12) + +### 📈 Enhancements + +- Handle async requests in spring mvc library instrumentation + ([#10868](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/10868)) +- Support statement sanitizer enabled flag in lettuce 5.1 instrumentation + ([#10922](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/10922)) +- Remove AWS Active Tracing span linking + ([#10930](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/10930)) +- Make spring boot honor the standard environment variables for maps + ([#11000](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/11000)) +- Pulsar: use span links when receive telemetry is enabled + ([#10650](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/10650)) +- Rename `messaging.kafka.destination.partition` to `messaging.destination.partition.id` + ([#11086](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/11086)) +- Support `service.instance.id` in spring starter + ([#11071](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/11071)) +- Add library instrumentation for RestTemplateBuilder + ([#11054](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/11054)) +- Add cloud resource providers in spring starter + ([#11014](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/11014)) + +### 🛠️ Bug fixes + +- Fix disabling virtual thread context propagation + ([#10881](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/10881)) +- Fix virtual thread instrumentation for jdk 21 ea versions + ([#10887](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/10887)) +- Fix spring kafka interceptor wrappers not delegating some methods + ([#10935](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/10935)) +- AWS Lambda Runtime legacy internal handlers need to be ignored from being instrumented and so traced … + ([#10942](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/10942)) +- Metro: ignore UnsupportedOperationException when updating span name + ([#10996](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/10996)) +- Fix jedis plugin for 2.7.2 + ([#10982](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/10982)) +- Fix idle in druid instrumentation + ([#11079](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/11079)) + +## Version 1.33.1 (2024-03-20) + +### 📈 Enhancements + +- Backport: Capture `http.route` for akka-http + ([#10777](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/10777)) +- Update the OpenTelemetry SDK version to 1.36.0 + ([#10866](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/10866)) + +## Version 2.2.0 (2024-03-14) + +### Migration notes + +- Remove deprecated spring properties + ([#10454](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/10454)) + +### 🌟 New javaagent instrumentation + +- Add cloud resource detectors in javaagent, but keep them disabled by default + ([#10754](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/10754)) +- Add support for XXL-JOB + ([#10421](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/10421)) + +### 📈 Enhancements + +- Don't fill network peer for cassandra SniEndPoint + ([#10573](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/10573)) +- Spring boot starter: add service.version detection, improve service.name detection + ([#10457](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/10457)) +- Always create a JMS consumer span + ([#10604](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/10604)) +- Ability to disable the automatic Logback appender addition + ([#10629](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/10629)) +- Allow excluding all methods of a class + ([#10753](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/10753)) +- Preserve attribute type for logback key value pairs + ([#10781](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/10781)) +- Add instrumentation for graphql 20 that does not use deprecated methods + ([#10779](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/10779)) +- Capture http.route for pekko-http + ([#10799](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/10799)) +- Normalize SQL IN(?, ?, ...) statements to "in(?)" to reduce cardinality of db.statement attribute + ([#10564](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/10564)) +- Capture `db.operation` for CREATE/DROP/ALTER SQL statement + ([#10020](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/10020)) +- Ignore AWS Lambda Runtime internal handlers + ([#10736](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/10736)) +- Spring use SDK autoconfig + ([#10453](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/10453)) +- Add manifest resource detector + ([#10621](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/10621)) +- Add instrumentation for jetty 12 + ([#10575](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/10575)) +- add host.id resource provider + ([#10627](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/10627)) +- Spring starter includes annotation dependency + ([#10613](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/10613)) + +### 🛠️ Bug fixes + +- Don't fail spring application startup if sdk is disabled + ([#10602](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/10602)) +- Fix shading aws propagator + ([#10669](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/10669)) +- Disable http and rpc metrics when advice can not be applied + ([#10671](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/10671)) +- Fix native tests + ([#10685](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/10685)) +- Fix tomcat instrumentation when user includes wrong servlet api + ([#10757](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/10757)) +- Override xray trace header instead of appending + ([#10766](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/10766)) +- Fix spring boot starter failing without logback + ([#10802](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/10802)) +- Fix spring kafka context leak when batch listener is retried + ([#10741](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/10741)) +- Fix the logic to get container.id resource attribute + ([#10737](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/10737)) +- Configure kafka metrics reporter as class + ([#10855](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/10855)) +- Disable context propagation when virtual thread is switched to the carrier thread + ([#10854](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/10854)) + +## Version 1.33.0 (2024-02-28) + +### Migration notes + +- The deprecated Jaeger exporter has been removed + ([#10524](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/10524)) + +### 📈 Enhancements + +- Backport: Set route only on the SERVER span + ([#10580](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/10580)) +- Update the OpenTelemetry SDK version to 1.35.0 + ([#10524](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/10524)) + +## Version 2.1.0 (2024-02-16) + +### Migration notes + +- Deprecated config properties have been removed in favor of the new names: + - `otel.instrumentation.kafka.client-propagation.enabled` -> + `otel.instrumentation.kafka.producer-propagation.enabled` + - `otel.instrumentation.netty.always-create-connect-span` -> + `otel.instrumentation.netty.connection-telemetry.enabled` + - `otel.instrumentation.http.capture-headers.client.request` -> + `otel.instrumentation.http.client.capture-request-headers` + - `otel.instrumentation.http.capture-headers.client.response` -> + `otel.instrumentation.http.client.capture-response-headers` + - `otel.instrumentation.http.capture-headers.server.request` -> + `otel.instrumentation.http.server.capture-request-headers` + - `otel.instrumentation.http.capture-headers.server.response` -> + `otel.instrumentation.http.server.capture-response-headers` + - `otel.instrumentation.http.client.emit-experimental-metrics` -> + `otel.instrumentation.http.client.emit-experimental-telemetry` + - `otel.instrumentation.http.server.emit-experimental-metrics` -> + `otel.instrumentation.http.server.emit-experimental-telemetry` + ([#10349](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/10349)) +- The deprecated Jaeger exporter has been removed + ([#10241](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/10241)) +- Actuator instrumentation has been disabled by default. + You can enable using `OTEL_INSTRUMENTATION_SPRING_BOOT_ACTUATOR_AUTOCONFIGURE_ENABLED=true` + or `-Dotel.instrumentation.spring-boot-actuator-autoconfigure.enabled=true`. + ([#10394](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/10394)) +- Spring starter: removed support for the deprecated @io.opentelemetry.extension.annotations.WithSpan + annotation. Use @io.opentelemetry.instrumentation.annotations.WithSpan annotation instead. + ([#10530](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/10530)) + +### 🌟 New javaagent instrumentation + +- MyBatis framework instrumentation + ([#10258](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/10258)) +- Finagle instrumentation + ([#10141](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/10141)) + +### 🌟 New library instrumentation + +- Apache HttpClient 5 instrumentation + ([#10100](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/10100)) + +### 📈 Enhancements + +- Spring starter: add distro version resource attribute + ([#10276](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/10276)) +- Add context propagation for rector schedulers + ([#10311](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/10311)) +- Spring starter: automatic addition of the OTel Logback appender + ([#10306](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/10306)) +- Spring starter: add resource detectors + ([#10277](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/10277)) +- Allow closing the observables for System and Process metrics gathered by OSHI + ([#10364](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/10364)) +- Spring starter: Allow to configure the OTel Logback appender from system properties + ([#10355](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/10355)) +- Spring starter: re-use sdk logic for configuring otlp exporters + ([#10292](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/10292)) + - All available spring starter properties (including the new properties) can be found + [here](https://github.com/open-telemetry/opentelemetry-java-instrumentation/blob/v2.1.0/instrumentation/spring/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json) + - You can also use auto-completion in your IDE to see the available properties in + `application.properties` or `application.yml` +- Spring starter: add SystemOutLogRecordExporter + ([#10420](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/10420)) +- Spring starter: use duration parser of config properties + ([#10512](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/10512)) +- Spring starter: support `otel.propagators` + ([#10408](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/10408)) +- Set route only on the SERVER span + ([#10290](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/10290)) +- Convert Apache HttpClient 4.3 library instrumentation to "low-level" HTTP instrumentation + ([#10253](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/10253)) + +### 🛠️ Bug fixes + +- Fix log replay of the Log4j 2 appender + ([#10243](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/10243)) +- Fix Netty addListener instrumentation + ([#10254](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/10254)) +- Fix Calling shutdown() multiple times warning in spring starter + ([#10222](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/10222)) +- Correctly fix NPE in servlet AsyncListener + ([#10250](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/10250)) +- add @ConditionalOnMissingBean to LoggingMetricExporter + ([#10283](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/10283)) +- Make Netty Instrumentation HttpServerRequestTracingHandler propagate "Channel Inactive" event + to downstream according to parent contract + ([#10303](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/10303)) +- Improve rediscala instrumentation to address sporadic test failure + ([#10301](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/10301)) +- Undertow: restore attached context only when it is for different trace + ([#10336](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/10336)) +- Reactor kafka wrapper delegates to wrong method + ([#10333](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/10333)) +- Spring starter: add missing LoggingMetricExporterAutoConfiguration to spring factories + ([#10282](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/10282)) +- Spring starter: Fix MapConverter does not get initialized if some exporters are turned off + ([#10346](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/10346)) +- Update azure-core-tracing-opentelemetry version and fix double-collection for synchronous + HTTP requests + ([#10350](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/10350)) +- Allow OSGI dynamic import for `io.opentelemetry` package when matching + ([#10385](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/10385)) +- Use direct peer address in `client.address` when X-Forwarded-For is not present + ([#10370](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/10370)) +- Netty: don't expose tracing handler in handlers map + ([#10410](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/10410)) +- Wrap request to avoid modifying attributes of the original request + ([#10389](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/10389)) +- Fix JarAnalyzer warnings on Payara + ([#10458](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/10458)) +- Return wrapped connection from `Statement.getConnection()` + ([#10554](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/10554)) +- Spring starter: Fix `otel.propagators` + ([#10559](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/10559)) +- Populate `server.address` and `server.port` in Cassandra instrumentation + ([#10357](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/10357)) + +### 🧰 Tooling + +- Allow multiple invokedynamic InstrumentationModules to share classloaders + ([#10015](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/10015)) + +## Version 1.32.1 (2024-02-02) + +### 📈 Enhancements + +- Backport: update jackson packages to v2.16.1 + ([#10198](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/10198), + [#10199](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/10199)) +- Backport: implement forEach support for aws sqs tracing list + ([#10195](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/10195)) +- Backport: Bridge metric advice in OpenTelemetry API 1.32 + ([#10026](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/10026)) +- Update the OpenTelemetry SDK version to 1.34.1 + ([#10320](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/10320)) + +### 🛠️ Bug fixes + +- Backport: Handle authority from request when HttpHost is null + ([#10204](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/10204)) +- Backport: Null check for nullable response object in aws sdk 1.1 instrumentation + ([#10029](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/10029)) +- Backport: Make Netty Instrumentation HttpServerRequestTracingHandler propagate "Channel Inactive" event to downstream according to parent contract + ([#10303](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/10303)) +- Backport: Fix Netty addListener instrumentation + ([#10254](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/10254)) +- Backport: Update azure-core-tracing-opentelemetry version and fix sync suppression + ([#10350](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/10350)) + +## Version 2.0.0 (2024-01-12) + +The 2.0.0 release contains significant breaking changes that will most likely affect all users, +please be sure to read the breaking changes below carefully. + +Note: 1.32.x will be security patched for at least 6 months in case some of the changes below are +too disruptive to adopt right away. + +### ⚠️⚠️ Breaking changes ⚠️⚠️ + +- The default OTLP protocol has been changed from `grpc` to `http/protobuf` in order to align with + the [specification](https://github.com/open-telemetry/opentelemetry-specification/blob/v1.28.0/specification/protocol/exporter.md#specify-protocol). + You can switch to the `grpc` protocol using `OTEL_EXPORTER_OTLP_PROTOCOL=grpc` + or `-Dotel.exporter.otlp.protocol=grpc`. +- Micrometer metric bridge has been disabled by default. You can enable it using + `OTEL_INSTRUMENTATION_MICROMETER_ENABLED=true` + or `-Dotel.instrumentation.micrometer.enabled=true`. +- The OTLP logs exporter is now enabled by default. You can disable it using + `OTEL_LOGS_EXPORTER=none` or `-Dotel.logs.exporter=none`. +- Controller spans are now disabled by default. You can enable them using + `OTEL_INSTRUMENTATION_COMMON_EXPERIMENTAL_CONTROLLER_TELEMETRY_ENABLED=true` + or `-Dotel.instrumentation.common.experimental.controller-telemetry.enabled=true`. +- View spans are now disabled by default. You can enable them using + `OTEL_INSTRUMENTATION_COMMON_EXPERIMENTAL_VIEW_TELEMETRY_ENABLED=true` + or `-Dotel.instrumentation.common.experimental.view-telemetry.enabled=true`. +- ⚠️⚠️ Stable HTTP semantic conventions are now emitted ⚠️⚠️ + - TOO MANY CHANGES TO LIST HERE, be sure to review the full + [list of changes](https://github.com/open-telemetry/semantic-conventions/blob/main/docs/http/migration-guide.md#summary-of-changes). +- Stable JVM semantic conventions are now emitted. + - Memory metrics + - `process.runtime.jvm.memory.usage` renamed to `jvm.memory.used` + - `process.runtime.jvm.memory.committed` renamed to `jvm.memory.committed` + - `process.runtime.jvm.memory.limit` renamed to `jvm.memory.limit` + - `process.runtime.jvm.memory.usage_after_last_gc` renamed to `jvm.memory.used_after_last_gc` + - `process.runtime.jvm.memory.init` renamed to `jvm.memory.init` (still experimental) + - Metric attributes + - `type` renamed to `jvm.memory.type` + - `pool` renamed to `jvm.memory.pool.name` + - Garbage collection metrics + - `process.runtime.jvm.gc.duration` renamed to `jvm.gc.duration` + - Metric attributes + - `name` renamed to `jvm.gc.name` + - `action` renamed to `jvm.gc.action` + - Thread metrics + - `process.runtime.jvm.threads.count` renamed to `jvm.threads.count` + - Metric attributes + - `daemon` renamed to `jvm.thread.daemon` + - Classes metrics + - `process.runtime.jvm.classes.loaded` renamed to `jvm.classes.loaded` + - `process.runtime.jvm.classes.unloaded` renamed to `jvm.classes.unloaded` + - `process.runtime.jvm.classes.current_loaded` renamed to `jvm.classes.count` + - CPU metrics + - `process.runtime.jvm.cpu.utilization` renamed to `jvm.cpu.recent_utilization` + - `process.runtime.jvm.system.cpu.load_1m` renamed to `jvm.system.cpu.load_1m` (still experimental) + - `process.runtime.jvm.system.cpu.utilization` renamed to `jvm.system.cpu.utilization` (still experimental) + - Buffer metrics + - `process.runtime.jvm.buffer.limit` renamed to `jvm.buffer.memory.limit` (still experimental) + - `process.runtime.jvm.buffer.count` renamed to `jvm.buffer.count` (still experimental) + - `process.runtime.jvm.buffer.usage` renamed to `jvm.buffer.memory.usage` (still experimental) + - Metric attributes + - `pool` renamed to `jvm.buffer.pool.name` + +### More migration notes + +- Lettuce CONNECT spans are now disabled by default. You can enable them using + `OTEL_INSTRUMENTATION_LETTUCE_CONNECTION_TELEMETRY_ENABLED=true` + or `-Dotel.instrumentation.lettuce.connection-telemetry.enabled=true`. +- The configuration property + `otel.instrumentation.log4j-appender.experimental.capture-context-data-attributes` has been + renamed to `otel.instrumentation.log4j-appender.experimental.capture-mdc-attributes`. +- MDC attribute prefixes (`log4j.mdc.` and `logback.mdc.*`) have been removed. +- The artifact `instrumentation-api-semconv` has been renamed to `instrumentation-api-incubator`. +- HTTP classes have been moved from `instrumentation-api-incubator` to `instrumentation-api` + and as a result are now stable. + +### 🌟 New javaagent instrumentation + +- Vert.x redis client + ([#9838](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/9838)) + +### 📈 Enhancements + +- Reduce reactor stack trace depth + ([#9923](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/9923)) +- Implement `error.type` in `spring-webflux` and `reactor-netty` instrumentations + ([#9967](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/9967)) +- Bridge metric advice in OpenTelemetry API 1.32 + ([#10026](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/10026)) +- Capture http.route for akka-http + ([#10039](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/10039)) +- Rename `telemetry.auto.version` to `telemetry.distro.version` and add `telemetry.distro.name` + ([#9065](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/9065)) +- Implement forEach support for aws sqs tracing list + ([#10062](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/10062)) +- Add http client response attributes to aws sqs process spans + ([#10074](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/10074)) +- Add support for `OTEL_RESOURCE_ATTRIBUTES`, `OTEL_SERVICE_NAME`, `OTEL_EXPORTER_OTLP_HEADERS`, + and `OTEL_EXPORTER_OTLP_PROTOCOL` for spring boot starter + ([#9950](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/9950)) +- Add elasticsearch-api-client as instrumentation name to elasticsearch-api-client-7.16 + ([#10102](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/10102)) +- Add instrumentation for druid connection pool + ([#9935](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/9935)) +- Remove deprecated rocketmq setting + ([#10125](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/10125)) +- JMX metrics for Tomcat with 'Tomcat' JMX domain + ([#10115](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/10115)) +- Capture the SNS topic ARN under the 'messaging.destination.name' span attribute. + ([#10096](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/10096)) +- Add network attributes to rabbitmq process spans + ([#10210](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/10210)) +- Add UserExcludedClassloadersConfigurer + ([#10134](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/10134)) +- Apply both server attributes & network attributes to Lettuce 5.1 + ([#10197](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/10197)) + +### 🛠️ Bug fixes + +- Fix aws propagator presence check in spring boot starter + ([#9924](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/9924)) +- Capture authority from apache httpclient request when HttpHost is null + ([#9990](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/9990)) +- Fix NoSuchBeanDefinitionException with the JDBC driver configuration in spring boot starter + ([#9978](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/9978)) +- Null check for nullable response object in aws sdk 1.1 instrumentation + ([#10029](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/10029)) +- Fix using opentelemetry-spring-boot with Java 8 and Gradle + ([#10066](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/10066)) +- Fix transforming Java record types + ([#10052](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/10052)) +- Fix warnings from the spring boot starter + ([#10086](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/10086)) +- Resolve `ParameterNameDiscoverer` Bean Conflict in `spring-boot-autoconfigure` + ([#10105](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/10105)) + +## Version 1.32.0 (2023-11-18) + +### Migration notes + +- Old server/client socket getter methods deprecated + ([#9716](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/9716)) + +### 📈 Enhancements + +- Allow enabling receive telemetry in kafka library instrumentation + ([#9693](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/9693)) +- Add JdbcTelemetry and JdbcTelemetryBuilder + ([#9685](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/9685)) +- Rename http.resend_count to http.request.resend_count + ([#9700](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/9700)) +- Define `url.scheme` in terms of logical operation in HTTP server semconv + (when opting in to new semconv) + ([#9698](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/9698)) +- Generate only consumer span for sqs receive message + ([#9652](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/9652)) +- Replace `(client|server).socket.(address|port)` attributes with `network.(peer|local).(address|port)` + (when opting in to new semconv) + ([#9676](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/9676)) +- Add capability for invokedynamic InstrumentationModules to inject proxies + ([#9565](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/9565)) +- Make `network.transport` and `network.type` opt-in (when opting in to new semconv) + ([#9719](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/9719)) +- Factor in `X-Forwarded-Host`/`Forwarded` when capturing `server.address` and `server.port` + (when opting in to new semconv) + ([#9721](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/9721)) +- Move class that should've been internal to internal package + ([#9725](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/9725)) +- Only set `server.port` when `server.address` is set (when opting in to new semconv) + ([#9737](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/9737)) +- Add messaging conventions to sqs spans + ([#9712](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/9712)) +- Make the JDBC driver config work with the OTel starter + ([#9625](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/9625)) +- Don't normalize the '-' character in HTTP header names when opting in to new semconv + ([#9735](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/9735)) +- Add instrumentation for jaxws metro 3.0+ + ([#9705](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/9705)) +- Change `user_agent.original` from recommended to opt-in on HTTP client spans + ([#9776](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/9776)) +- Change the precedence between `:authority` and `Host` headers + ([#9774](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/9774)) +- Move capturing enduser.id attribute behind a flag + ([#9751](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/9751), + [#9788](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/9788)) +- Remove conditional requirement on `network.peer.address` and `network.peer.port` + (when opting in to new semconv) + ([#9775](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/9775)) +- Change `client.port` from recommended to opt-in on HTTP server spans + (when opting in to new semconv) + ([#9786](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/9786)) +- Make `url.scheme` opt in for HTTP client metrics and make `server.port` required + (when opting in to new semconv) + ([#9784](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/9784)) +- Change `http.request.body.size` and `http.response.body.size` attributes from recommended to opt-in + (when opting in to new semconv) + ([#9799](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/9799)) +- Capture `http.route` in spring-cloud-gateway + ([#9597](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/9597)) +- Always set messaging operation + ([#9791](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/9791)) +- Change `network.protocol.name` from opt-in to conditionally required + (when opting in to new semconv) + ([#9797](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/9797)) +- Support specifying `spring.application.name` in the `bootstrap.properties`, `bootstrap.yml` + and `bootstrap.yaml` + ([#9801](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/9801)) +- Add process spans to aws-1 sqs instrumentation + ([#9796](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/9796)) +- Implement capturing message headers for aws1 sqs spans + ([#9824](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/9824)) +- Add process spans to aws2 sqs instrumentation + ([#9778](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/9778)) +- Add `service.name` to MDC + ([#9647](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/9647)) +- Capture enduser attributes in Spring Security + ([#9777](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/9777)) +- Capture message id in aws1 sqs instrumentation + ([#9841](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/9841)) +- Implement capturing message headers for aws2 sqs spans + ([#9842](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/9842)) +- Move kafka metrics to separate instrumentation module + ([#9862](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/9862)) +- Capture logback logger context properties + ([#9553](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/9553)) +- Stable JVM semconv implementation: classes + ([#9821](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/9821)) +- Stable JVM semconv implementation: threads + ([#9839](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/9839)) +- Stable JVM semconv implementation: GC + ([#9890](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/9890)) +- Bridge incubator metrics apis + ([#9884](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/9884)) +- Ability to instrument logs before OTel injection into OTel appenders + ([#9798](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/9798)) +- Suppress instrumentation based on suppress Context key + ([#9739](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/9739)) +- Stable JVM semconv implementation: the rest + ([#9896](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/9896)) + +### 🛠️ Bug fixes + +- Fix armeria server instrumentation for http2 + ([#9723](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/9723)) +- Guard against null list from aws SQS lib + ([#9710](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/9710)) +- Fix parsing port from mariadb jdbc url + ([#9863](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/9863)) + +### 🧰 Tooling + +- Improve disableShadowRelocate + ([#9715](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/9715)) +- Allow injection of helper bytecode as resources + ([#9752](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/9752)) +- Rewrite @Advice.Enter for indy advice + ([#9887](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/9887)) + +## Version 1.31.0 (2023-10-12) + +### 🌟 New javaagent instrumentation + +- Apache Pekko + ([#9527](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/9527)) + +### 📈 Enhancements + +- Add instrumentation for vert.x sql client withTransaction method + ([#9462](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/9462)) +- Improve hibernate reactive instrumentation + ([#9486](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/9486)) +- Support application.yaml files in SpringBootServiceNameDetector + ([#9515](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/9515)) +- Add Spring Boot service version finder / ResourceProvider + ([#9480](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/9480)) +- Split hibernate reactive instrumentation + ([#9531](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/9531)) +- Rework reactor netty context tracking + ([#9286](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/9286)) +- Improve spring data reactive instrumentation + ([#9561](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/9561)) +- Support akka http latest version + ([#9573](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/9573)) +- Enhance AWS SDK Instrumentation with Detailed HTTP Error Information + ([#9448](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/9448)) +- Update HTTP metrics' descriptions + ([#9635](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/9635)) +- Remove server.socket.address from HTTP/RPC metrics + ([#9633](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/9633)) +- Remove 0 bucket from stable HTTP metrics + ([#9631](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/9631)) +- Suppress nested http client spans in aws2 instrumentation + ([#9634](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/9634)) +- Implement spec changes for grpc server span error status + ([#9641](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/9641)) +- Improve vertx-sql client context propagation + ([#9640](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/9640)) +- Add url.scheme to HTTP client metrics + ([#9642](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/9642)) +- Add support for newVirtualThreadPerTaskExecutor + ([#9616](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/9616)) +- Implement error.type attribute in HTTP semconv + ([#9466](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/9466)) +- Emit package events + ([#9301](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/9301)) +- Added Automatic-Module-Name header to MANIFEST.MF in instrumentation libraries + ([#9140](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/9140)) +- Support paths in `peer.service` mappings + ([#9061](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/9061)) +- Compile ktor library instrumentation for earlier kotlin version + ([#9661](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/9661)) + +### 🛠️ Bug fixes + +- Fix NPE happening when .headersWhen() is used (reactor-netty) + ([#9511](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/9511)) +- Spring webflux: add user spans as children of the controller span + ([#9572](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/9572)) +- Make netty ChannelPipeline removeLast return user handler + ([#9584](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/9584)) + +## Version 1.30.0 (2023-09-14) + +### Migration notes + +- Experimental HTTP server metrics have been split out from `HttpServerMetrics` into a separate + class `HttpServerExperimentalMetrics` + ([#9259](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/9259)) +- `HttpClientResend` has been renamed to `HttpClientResendCount`, `HttpRouteHolder` + has been renamed to `HttpServerRoute` + ([#9280](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/9280)) +- The previously deprecated `otel.javaagent.experimental.extensions` configuration was removed + (it is replaced by `otel.javaagent.extensions`) + ([#9378](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/9378)) + +### 🌟 New javaagent instrumentation + +- Add instrumentation for hibernate reactive + ([#9304](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/9304)) + +### 📈 Enhancements + +- Don't log stack trace for expected exceptions in AWS SDK instrumentation + ([#9279](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/9279)) +- Add support for the AWS Secrets Manager JDBC URLs + ([#9335](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/9335)) +- More support for upcoming semantic convention changes + ([#9346](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/9346) + [#9345](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/9345), + [#9320](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/9320), + [#9355](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/9355), + [#9381](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/9381), + [#9441](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/9441)) +- Unwrap Runnable in ThreadPoolExecutor before/after methods + ([#9326](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/9326)) +- Add javaagent to instrumentation bom + ([#9026](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/9026)) +- Add support for multiple headers in AlternateUrlSchemeProvider + ([#9389](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/9389)) +- Skip PreparedStatement wrappers + ([#9399](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/9399)) +- Disable Elasticsearch instrumentation for ES clients 8.10+ + ([#9337](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/9337)) +- Disable elasticsearch-rest-7.0 instrumentation when elasticsearch-java 8.10+ is present + ([#9450](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/9450)) +- Use attributes advice for HTTP & RPC metrics + ([#9440](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/9440)) +- Update Messaging semantic conventions to 1.21.0 + ([#9408](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/9408)) +- Add semconv to alpha bom + ([#9425](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/9425)) + +### 🛠️ Bug fixes + +- Ensure .class files aren't present in the resources library MR jar + ([#9245](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/9245)) +- Fixed getDefinedPackage lookup for OpenJ9 (8) + ([#9272](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/9272)) +- Ignore aws sdk v2 presign requests + ([#9275](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/9275)) +- Add logging timestamp for log4j1 appender instrument + ([#9309](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/9309)) +- Fix VerifyError with kotlin @WithSpan instrumentation + ([#9313](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/9313)) +- Fix serializing key to string in Lettuce instrumentation + ([#9347](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/9347)) +- Auto-instrumentation with JMX not working without a trigger + ([#9362](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/9362)) +- Return default DbInfo when connection is null + ([#9413](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/9413)) +- Fix instrumentation for reactor kafka 1.3.21 + ([#9445](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/9445)) + +## Version 1.29.0 (2023-08-17) + +### Migration notes + +- `NetClientAttributesExtractor` and `NetServerAttributesExtractor` + have been deprecated + ([#9165](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/9165), + [#9156](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/9156)) +- `HttpClientAttributesGetter` now extends `NetClientAttributesGetter` + and `HttpServerAttributesGetter` extends `NetServerAttributesGetter` + ([#9015](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/9015), + [#9088](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/9088)) +- A couple of Spring starter configuration options have been renamed to match Java agent options: + - `otel.springboot.httpclients.enabled` -> `otel.instrumentation.spring-webmvc.enabled` + or `otel.instrumentation.spring-webmvc.enabled` depending on the underlying http client + - `otel.springboot.aspects.enabled` -> `otel.instrumentation.annotations.enabled` + ([#8950](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/8950)) +- Previously deprecated suppression key `executor` was removed from executors module, + the new suppression key is `executors` + ([#9064](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/9064)) + +### 🌟 New javaagent instrumentation + +- Ktor + ([#9149](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/9149)) + +### 🌟 New library instrumentation + +- Elasticsearch rest client + ([#8911](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/8911)) + +### 📈 Enhancements + +- Include OpenTelemetry logging appenders in the Spring Starter + ([#8945](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/8945)) +- Support RPC metrics under "stable" http semconv opt-in + ([#8948](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/8948)) +- Support the `http.request.method_original` attribute under "stable" semconv opt-in + ([#8779](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/8779)) +- Make `server.socket.*` attributes on the HTTP server side opt-in + ([#8747](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/8747)) +- Fill additional db.* attributes on DataSource#getConnection() + ([#8966](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/8966)) +- Lettuce instrumentation - optimization to avoid extra toString() + ([#8984](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/8984)) +- Allow overriding span name in spring web library instrumentation + ([#8933](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/8933)) +- Implement HTTP resend spec for Reactor Netty (excl CONNECT spans) + ([#8111](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/8111)) +- Set `process.runtime.jvm.system.cpu.load_1m` metric unit to `{run_queue_item}` + ([#8777](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/8777)) +- Update elasticsearch instrumentation to work with latest version + ([#9066](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/9066)) +- Reactor Netty: emit actual HTTP client spans on connection errors + ([#9063](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/9063)) +- Rename `http.*.duration` to `http.*.request.duration` under "stable" semconv opt-in + ([#9089](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/9089)) +- Snippet inject support for non-empty head tags + ([#8736](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/8736)) +- Update network.protocol.version `2.0` -> `2` and `3.0` -> `3` + ([#9145](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/9145)) +- @WithSpan support for kotlin coroutines + ([#8870](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/8870)) + +### 🛠️ Bug fixes + +- Fix incompatibility between quarkus resteasy reactive and vertx-web instrumentations + ([#8998](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/8998)) +- Fix `IllegalArgumentException` in `MetroServerSpanNaming` + ([#9075](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/9075)) +- Fix rector netty instrumentation under concurrency + ([#9081](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/9081)) +- Improve grpc cancellation propagation + ([#8957](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/8957)) +- Add missing timestamp for jboss logmanager instrumentation + ([#9159](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/9159)) +- Handle kafka `METRIC_REPORTER_CLASSES_CONFIG` being set to a List + ([#9155](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/9155)) +- Fix NullPointerException with Pulsar and SSL + ([#9166](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/9166)) +- Netty 4.1: handle closing connection before the request completes + ([#9157](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/9157)) +- Micrometer bridge: use app's thread context class loader for callbacks into app + ([#9000](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/9000)) +- Fix context propagation in Executor#execute() for non-capturing lambdas + ([#9179](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/9179)) +- Ensure reactor netty spans are ended in the order they were started + ([#9203](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/9203)) + +## Version 1.28.0 (2023-07-12) + +### Migration notes + +- Rename HTTP configuration settings + ([#8758](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/8758)) + - `otel.instrumentation.http.capture-headers.client.request` + → `otel.instrumentation.http.client.capture-request-headers` + - `otel.instrumentation.http.capture-headers.client.response` + → `otel.instrumentation.http.client.capture-response-headers` + - `otel.instrumentation.http.capture-headers.server.request` + → `otel.instrumentation.http.server.capture-request-headers` + - `otel.instrumentation.http.capture-headers.server.response` + → `otel.instrumentation.http.server.capture-response-headers` + +### 📈 Enhancements + +- Support latest armeria release + ([#8745](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/8745)) +- Support latest mongo release + ([#8785](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/8785)) +- Remove `server.{address,port}` from HTTP server metrics + ([#8771](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/8771)) +- aws-sdk-2.2.: Support injection into SQS.SendMessageBatch message attributes + ([#8798](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/8798)) +- Log4j and Logback appenders opt-in to using GlobalOpenTelemetry + ([#8791](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/8791)) +- aws-sdk-2.2: SNS.Publish support with experimental messaging propagator flag + ([#8830](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/8830)) +- support for adding baggage to log4j 2 ContextData + ([#8810](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/8810)) +- Micrometer bridge: interpret no SLO config as no buckets advice + ([#8856](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/8856)) +- Instrumentation for Elasticsearch 8+ + ([#8799](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/8799)) +- Add support for schemaUrls auto-computed from `AttributesExtrator`s + ([#8864](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/8864)) +- Initialize appenders in the spring boot starter + ([#8888](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/8888)) +- Support reactor-netty 1.0.34+ + ([#8922](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/8922)) +- Rename messaging operation "send" to "publish" per spec + ([#8929](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/8929)) +- Extract query arguments without regex on lettuce 6 + ([#8932](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/8932)) + +### 🛠️ Bug fixes + +- Fix logging timestamp + ([#8761](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/8761)) +- Minor fixes to the `server.*` attributes extrator + ([#8772](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/8772)) +- Fix context leak on call to AmazonS3.generatePresignedUrl + ([#8815](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/8815)) +- Fix exception when pulsar has multiple service addresses + ([#8816](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/8816)) +- Fix NPE in aws instrumentation on duplicate TracingExecutionInterceptor + ([#8896](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/8896)) +- (micrometer) don't add . to empty unit with prometheus naming conventions + ([#8872](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/8872)) +- Set server span name for aborted requests in quarkus resteasy native + ([#8891](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/8891)) +- Fix instrumentation of Azure SDK EventHubs library + ([#8916](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/8916)) +- Fix http attributes of AWS SDK V2 instrumentation + ([#8931](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/8931)) + ## Version 1.27.0 (2023-06-14) ### Migration notes @@ -12,16 +1100,16 @@ - The `opentelemetry-runtime-metrics` artifact has been renamed and split into `opentelemetry-runtime-telemetry-java8` and `opentelemetry-runtime-telemetry-java17` ([#8165](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/8165), - [#8715](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/8715)) + [#8715](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/8715)) - `InetSocketAddressNetServerAttributesGetter` and `InetSocketAddressNetClientAttributesGetter` have been deprecated ([#8341](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/8341), - [#8591](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/8591)) + [#8591](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/8591)) - The new HTTP and network semantic conventions can be opted into using either the system property `otel.semconv-stability.opt-in` or the environment variable `OTEL_SEMCONV_STABILITY_OPT_IN`, which support two values: - `http` - emit the new, stable HTTP and networking attributes, and stop emitting the old - experimental HTTP and networking attributes that the instrumentation emitted previously. + experimental HTTP and networking attributes that the instrumentation emitted previously. - `http/dup` - emit both the old and the stable HTTP and networking attributes, allowing for a more seamless transition. - The default behavior (in the absence of one of these values) is to continue emitting @@ -37,7 +1125,7 @@ ([#8487](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/8487)) - Reactor Kafka ([#8439](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/8439), - [#8529](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/8529)) + [#8529](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/8529)) ### 📈 Enhancements @@ -219,7 +1307,7 @@ ([#8174](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/8174)) - Spring scheduling: run error handler with the same context as task ([#8220](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/8220)) -- Switch from http.flavor to net.protocol.* +- Switch from http.flavor to net.protocol.\* ([#8131](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/8131), [#8244](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/8244)) - Support latest Armeria release @@ -531,7 +1619,7 @@ ### 🧰 Tooling -- Muzzle logs should be logged using the io.opentelemetry.* logger name +- Muzzle logs should be logged using the io.opentelemetry.\* logger name ([#7446](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/7446)) ## Version 1.21.0 (2022-12-13) @@ -773,12 +1861,12 @@ The `opentelemetry-instrumentation-api` artifact is declared stable in this rele - There were a few late-breaking changes in `opentelemetry-instrumentation-api`, prior to it being declared stable: - * `InstrumenterBuilder.addAttributesExtractors(AttributesExtractor...)` was removed, use instead + - `InstrumenterBuilder.addAttributesExtractors(AttributesExtractor...)` was removed, use instead `addAttributesExtractors(AttributesExtractor)` or `addAttributesExtractors(Iterable)` - * `SpanLinksExtractor.extractFromRequest()` was removed, use instead manual extraction - * `ErrorCauseExtractor.jdk()` was renamed to `ErrorCauseExtractor.getDefault()` - * `ClassNames` utility was removed with no direct replacement + - `SpanLinksExtractor.extractFromRequest()` was removed, use instead manual extraction + - `ErrorCauseExtractor.jdk()` was renamed to `ErrorCauseExtractor.getDefault()` + - `ClassNames` utility was removed with no direct replacement - The deprecated `io.opentelemetry.instrumentation.api.config.Config` and related classes have been removed ([#6501](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/6501)) @@ -1141,18 +2229,18 @@ The `opentelemetry-instrumentation-api` artifact is declared stable in this rele - Micrometer instrumentation is now automatically applied to spring-boot-actuator apps - Some configuration properties have been renamed: - * `otel.instrumentation.common.experimental.suppress-controller-spans` + - `otel.instrumentation.common.experimental.suppress-controller-spans` → `otel.instrumentation.common.experimental.controller-telemetry.enabled` (important: note that the meaning is inverted) - * `otel.instrumentation.common.experimental.suppress-view-spans` + - `otel.instrumentation.common.experimental.suppress-view-spans` → `otel.instrumentation.common.experimental.view-telemetry.enabled` (important: note that the meaning is inverted) - * `otel.instrumentation.netty.always-create-connect-span` + - `otel.instrumentation.netty.always-create-connect-span` → `otel.instrumentation.netty.connection-telemetry.enabled` - * `otel.instrumentation.reactor-netty.always-create-connect-span` + - `otel.instrumentation.reactor-netty.always-create-connect-span` → `otel.instrumentation.reactor-netty.connection-telemetry.enabled` - Runtime memory metric names were updated to reflect - [semantic conventions](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/semantic_conventions/runtime-environment-metrics.md#jvm-metrics) + [semantic conventions](https://github.com/open-telemetry/opentelemetry-specification/blob/v1.13.0/specification/metrics/semantic_conventions/runtime-environment-metrics.md#jvm-metrics) - Micrometer library instrumentation has been deprecated as it has been moved to the core repo and is now published under `io.opentelemetry:opentelemetry-micrometer1-shim` - Library instrumentation entry points have been renamed from `*Tracing` to `*Telemetry` @@ -1452,7 +2540,7 @@ The `opentelemetry-instrumentation-api` artifact is declared stable in this rele ([#5112](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/5112)) - Parameterize VirtualField field type ([#5165](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/5165)) -- Remove old TraceUtils and use InstrumentationTestRunner#run*Span() (almost) everywhere +- Remove old TraceUtils and use InstrumentationTestRunner#run\*Span() (almost) everywhere ([#5160](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/5160)) - Remove deprecated tracer API ([#5175](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/5175)) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index be1585073c64..6e25d48220ad 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -8,7 +8,7 @@ and discuss your ideas or propose the changes you wish to make. ## Building -In order to build and test this whole repository you need JDK 17 or higher. +This project requires Java 21 to build and run tests. Newer JDK's may work, but this version is used in CI. 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. @@ -22,7 +22,7 @@ the Sonatype OSS snapshots repository at `https://oss.sonatype.org/content/repos ### Building from source -Build using Java 17+: +Build using Java 21: ```bash java -version @@ -79,12 +79,20 @@ See [Understanding Muzzle](docs/contributing/muzzle.md) ## Troubleshooting PR build failures The build logs are very long and there is a lot of parallelization, so the logs can be hard to -decipher, but if you scroll to the bottom you should see something like: +decipher, but if you expand the "Build scan" step, you should see something like: ``` -Publishing build scan... +Run cat build-scan.txt https://gradle.com/s/ila4qwp5lcf5s ``` Opening the build scan link can sometimes take several seconds (it's a large build), but it typically makes it a lot clearer what's failing. + +### Draft PRs + +Draft PRs are welcome, especially when exploring new ideas or experimenting with a hypothesis. +However, draft PRs may not receive the same degree of attention, feedback, or scrutiny unless +requested directly. In order to help keep the PR backlog maintainable, drafts older than 6 months +will be closed by the project maintainers. This should not be interpreted as a rejection. Closed +PRs may be reopened by the author when time or interest allows. diff --git a/ISSUES.md b/ISSUES.md new file mode 100644 index 000000000000..356908140c55 --- /dev/null +++ b/ISSUES.md @@ -0,0 +1,33 @@ +# Issues + +## Labels + +### `needs author feedback` + +This is something that needs additional clarification from the author. + +Note: issues with this label are automatically marked stale if there is no activity for 7 days, +and then closed automatically if there is no response from the author within 7 days after that. + +### `needs repro` + +Repros are helpful for a lot of issues (not only for bugs). + +They allow the maintainers (and others) to much better understand exactly what the +issue author is trying to do, and what they expect to happen. + +Ideally repros should be minimal, should be posted to a github repository, +and should include a `README.md` which explains exactly how to build and run the repro, +what the output of the repro is, and what you expect (or would like) the output of +the repro to be. + +Note: issues with this label are automatically marked stale if there is no activity for 30 days, +and then closed automatically if there is no response from the author within 30 days after that. + +### `contribution welcome` + +This is something that fits the scope of the project. +The maintainers aren't planning to implement it, but would welcome and help review if +someone else wants to implement and contribute it to the project. + +Note: issues with this label are automatically closed if there is no activity for 365 days. diff --git a/README.md b/README.md index 1cf77667875f..3abe2c6d7aab 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,7 @@ This repository also publishes standalone instrumentation for several libraries that can be used if you prefer that over using the Java agent. Please see the standalone library instrumentation column on [Supported Libraries](docs/supported-libraries.md#libraries--frameworks). -if you are looking for documentation on using those. +If you are looking for documentation on using those. ## Getting Started @@ -81,7 +81,7 @@ configured to send data to at `http://localhost:4317`. Configuration parameters are passed as Java system properties (`-D` flags) or -as environment variables. See [the configuration documentation][config] +as environment variables. See [the configuration documentation][config-agent] for the full list of configuration items. For example: ``` @@ -93,11 +93,14 @@ java -javaagent:path/to/opentelemetry-javaagent.jar \ ## Configuring the Agent -The agent is [highly configurable][config]! Many aspects of the agent's behavior can be +The agent is highly configurable! Many aspects of the agent's behavior can be configured for your needs, such as exporter choice, exporter config (like where data is sent), trace context propagation headers, and much more. -[Click here to see the detailed list of configuration environment variables and system properties][config]. +For a detailed list of agent configuration options, see the [agent configuration docs][config-agent]. + +For a detailed list of additional SDK configuration environment variables and system properties, +see the [SDK configuration docs][config-sdk]. *Note: Config parameter names are very likely to change over time, so please check back here when trying out a new version! @@ -149,25 +152,34 @@ Debug logging negatively impacts the performance of your application. See [CONTRIBUTING.md](CONTRIBUTING.md). +Triagers ([@open-telemetry/java-instrumentation-triagers](https://github.com/orgs/open-telemetry/teams/java-instrumentation-triagers)): + +- [Jay DeLuca](https://github.com/jaydeluca) +- [Jonas Kunz](https://github.com/JonasKunz), Elastic +- [Steve Rao](https://github.com/steverao), Alibaba +- [Sylvain Juge](https://github.com/SylvainJuge), Elastic + Approvers ([@open-telemetry/java-instrumentation-approvers](https://github.com/orgs/open-telemetry/teams/java-instrumentation-approvers)): +- [Gregor Zietlinger](https://github.com/zeitlinger), Grafana - [Jack Berg](https://github.com/jack-berg), New Relic - [Jason Plumb](https://github.com/breedx-splk), Splunk +- [Jean Bisutti](https://github.com/jeanbisutti), Microsoft - [John Watson](https://github.com/jkwatson), Verta.ai Maintainers ([@open-telemetry/java-instrumentation-maintainers](https://github.com/orgs/open-telemetry/teams/java-instrumentation-maintainers)): - [Lauri Tulmin](https://github.com/laurit), Splunk -- [Mateusz Rzeszutek](https://github.com/mateuszrzeszutek), Splunk - [Trask Stalnaker](https://github.com/trask), Microsoft Emeritus maintainers: -- [Nikita Salnikov-Tarnovski](https://github.com/iNikem), Splunk +- [Mateusz Rzeszutek](https://github.com/mateuszrzeszutek) +- [Nikita Salnikov-Tarnovski](https://github.com/iNikem) - [Tyler Benson](https://github.com/tylerbenson) Learn more about roles in -the [community repository](https://github.com/open-telemetry/community/blob/main/community-membership.md). +the [community repository](https://github.com/open-telemetry/community/blob/main/guides/contributor/membership.md). Thanks to all the people who already contributed! @@ -175,7 +187,9 @@ Thanks to all the people who already contributed! -[config]: https://opentelemetry.io/docs/instrumentation/java/automatic/agent-config/ +[config-agent]: https://opentelemetry.io/docs/zero-code/java/agent/configuration/ + +[config-sdk]: https://opentelemetry.io/docs/languages/java/configuration/ [manual]: https://opentelemetry.io/docs/instrumentation/java/manual/ diff --git a/RELEASING.md b/RELEASING.md index 2c4a7d8ad5c4..33176080fee6 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -18,20 +18,20 @@ the second Monday of the month (roughly a few of days after the monthly minor re ## Preparing a new major or minor release -* Check that - [dependabot has run](https://github.com/open-telemetry/opentelemetry-java-instrumentation/network/updates) +- Check that + [renovate has run](https://developer.mend.io/github/open-telemetry/opentelemetry-java-instrumentation) sometime in the past day (this link is only accessible if you have write access to the repository), and check that all - [dependabot PRs](https://github.com/open-telemetry/opentelemetry-java-contrib/pulls/app%2Fdependabot) + [renovate PRs](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pulls/app%2Frenovate) have been merged. -* Close the [release milestone](https://github.com/open-telemetry/opentelemetry-java-instrumentation/milestones) +- Close the [release milestone](https://github.com/open-telemetry/opentelemetry-java-instrumentation/milestones) if there is one. -* Merge a pull request to `main` updating the `CHANGELOG.md`. - * The heading for the unreleased entries should be `## Unreleased`. - * Use `.github/scripts/draft-change-log-entries.sh` as a starting point for writing the change log. -* Run the [Prepare release branch workflow](https://github.com/open-telemetry/opentelemetry-java-instrumentation/actions/workflows/prepare-release-branch.yml). - * Press the "Run workflow" button, and leave the default branch `main` selected. - * Review and merge the two pull requests that it creates +- Merge a pull request to `main` updating the `CHANGELOG.md`. + - The heading for the unreleased entries should be `## Unreleased`. + - Use `.github/scripts/draft-change-log-entries.sh` as a starting point for writing the change log. +- Run the [Prepare release branch workflow](https://github.com/open-telemetry/opentelemetry-java-instrumentation/actions/workflows/prepare-release-branch.yml). + - Press the "Run workflow" button, and leave the default branch `main` selected. + - Review and merge the two pull requests that it creates (one is targeted to the release branch and one is targeted to `main`). ## Preparing a new patch release @@ -41,31 +41,31 @@ All patch releases should include only bug-fixes, and must avoid adding/modifyin In general, patch releases are only made for regressions, security vulnerabilities, memory leaks and deadlocks. -* Backport pull request(s) to the release branch. - * Run the [Backport workflow](https://github.com/open-telemetry/opentelemetry-java-instrumentation/actions/workflows/backport.yml). - * Press the "Run workflow" button, then select the release branch from the dropdown list, +- Backport pull request(s) to the release branch. + - Run the [Backport workflow](https://github.com/open-telemetry/opentelemetry-java-instrumentation/actions/workflows/backport.yml). + - Press the "Run workflow" button, then select the release branch from the dropdown list, e.g. `release/v1.9.x`, then enter the pull request number that you want to backport, then click the "Run workflow" button below that. - * Review and merge the backport pull request that it generates. - * Note: if the PR contains any changes to workflow files, it will have to be manually backported, + - Review and merge the backport pull request that it generates. + - Note: if the PR contains any changes to workflow files, it will have to be manually backported, because the default `GITHUB_TOKEN` does not have permission to update workflow files (and the `opentelemetrybot` token doesn't have write permission to this repository at all, so while it can be used to open a PR, it can't be used to push to a local branch). -* Merge a pull request to the release branch updating the `CHANGELOG.md`. - * The heading for the unreleased entries should be `## Unreleased`. -* Run the [Prepare patch release workflow](https://github.com/open-telemetry/opentelemetry-java-instrumentation/actions/workflows/prepare-patch-release.yml). - * Press the "Run workflow" button, then select the release branch from the dropdown list, +- Merge a pull request to the release branch updating the `CHANGELOG.md`. + - The heading for the unreleased entries should be `## Unreleased`. +- Run the [Prepare patch release workflow](https://github.com/open-telemetry/opentelemetry-java-instrumentation/actions/workflows/prepare-patch-release.yml). + - Press the "Run workflow" button, then select the release branch from the dropdown list, e.g. `release/v1.9.x`, and click the "Run workflow" button below that. - * Review and merge the pull request that it creates for updating the version. + - Review and merge the pull request that it creates for updating the version. ## Making the release -* Run the [Release workflow](https://github.com/open-telemetry/opentelemetry-java-instrumentation/actions/workflows/release.yml). - * Press the "Run workflow" button, then select the release branch from the dropdown list, +- Run the [Release workflow](https://github.com/open-telemetry/opentelemetry-java-instrumentation/actions/workflows/release.yml). + - Press the "Run workflow" button, then select the release branch from the dropdown list, e.g. `release/v1.9.x`, and click the "Run workflow" button below that. - * This workflow will publish the artifacts to maven central and will publish a GitHub release + - This workflow will publish the artifacts to maven central and will publish a GitHub release with release notes based on the change log and with the javaagent jar attached. - * Review and merge the pull request that it creates for updating the change log in main + - Review and merge the pull request that it creates for updating the change log in main (note that if this is not a patch release then the change log on main may already be up-to-date, in which case no pull request will be created). diff --git a/VERSIONING.md b/VERSIONING.md index b2475a72a265..5c1d2605757b 100644 --- a/VERSIONING.md +++ b/VERSIONING.md @@ -9,15 +9,15 @@ Artifacts in this repository follow the same compatibility requirements describe EXCEPT for the following incompatible changes which are allowed in stable artifacts in this repository: -* Changes to the telemetry produced by instrumentation +- Changes to the telemetry produced by instrumentation (there will be some guarantees about telemetry stability in the future, see discussions in ) -* Changes to configuration properties that contain the word `experimental` -* Changes to configuration properties under the namespace `otel.javaagent.testing` +- 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` +- 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) @@ -39,24 +39,24 @@ where versioning stability is important. Bumping the minimum supported library version for library instrumentation is generally acceptable if there's a good reason because: -* Users of library instrumentation have to integrate the library instrumentation during build-time +- Users of library instrumentation have to integrate the library instrumentation during build-time of their application, and so have the option to bump the library version if they are using an unsupported library version. -* Users have the option of staying with the old version of library instrumentation, without being +- Users have the option of staying with the old version of library instrumentation, without being pinned on an old version of the OpenTelemetry API and SDK. -* Bumping the minimum library version changes the artifact name, so it is not technically a breaking +- Bumping the minimum library version changes the artifact name, so it is not technically a breaking change. ### Javaagent instrumentation The situation is much trickier for javaagent instrumentation: -* A common use case of the javaagent is applying instrumentation at deployment-time (including +- A common use case of the javaagent is applying instrumentation at deployment-time (including to third-party applications), where bumping the library version is frequently not an option. -* While users have the option of staying with the old version of javaagent, that pins them on +- While users have the option of staying with the old version of javaagent, that pins them on an old version of the OpenTelemetry API and SDK, which is problematic for the OpenTelemetry ecosystem. -* While bumping the minimum library version changes the instrumentation module name, it does not +- While bumping the minimum library version changes the instrumentation module name, it does not change the "aggregated" javaagent artifact name which most users depend on, so could be considered a breaking change for some users (though this is not a breaking change that we currently make any guarantees about). @@ -68,8 +68,8 @@ When there is functionality in a new library version that requires changes to th instrumentation which are incompatible with the current javaagent base library version, some options that do not require bumping the minimum supported library version include: -* Access the new functionality via reflection. This is a good technique only for very small changes. -* Create a new javaagent instrumentation module to support the new library version. This requires +- Access the new functionality via reflection. This is a good technique only for very small changes. +- Create a new javaagent instrumentation module to support the new library version. This requires configuring non-overlapping versions in the muzzle check and applying `assertInverse` to confirm that the two instrumentations are never be applied to the same library version (see [class loader matchers](https://github.com/open-telemetry/opentelemetry-java-instrumentation/blob/main/docs/contributing/writing-instrumentation-module.md#restrict-the-criteria-for-applying-the-instrumentation-by-extending-the-classloadermatcher-method) diff --git a/benchmark-overhead-jmh/build.gradle.kts b/benchmark-overhead-jmh/build.gradle.kts index 5f78b9846b55..99a723959eec 100644 --- a/benchmark-overhead-jmh/build.gradle.kts +++ b/benchmark-overhead-jmh/build.gradle.kts @@ -8,7 +8,7 @@ plugins { } dependencies { - jmhImplementation("org.springframework.boot:spring-boot-starter-web:3.1.0") + jmhImplementation("org.springframework.boot:spring-boot-starter-web:3.3.3") } tasks { diff --git a/benchmark-overhead/Dockerfile-petclinic-base b/benchmark-overhead/Dockerfile-petclinic-base index 51c4ab5f7d84..154833d54187 100644 --- a/benchmark-overhead/Dockerfile-petclinic-base +++ b/benchmark-overhead/Dockerfile-petclinic-base @@ -13,7 +13,7 @@ RUN git checkout 8aa4d49 RUN ./mvnw package -Dmaven.test.skip=true RUN cp target/spring-petclinic-rest*.jar /app/spring-petclinic-rest.jar -FROM bellsoft/liberica-openjdk-alpine:17 +FROM bellsoft/liberica-openjdk-alpine:21.0.4 COPY --from=app-build /app/spring-petclinic-rest.jar /app/spring-petclinic-rest.jar WORKDIR /app EXPOSE 9966 diff --git a/benchmark-overhead/README.md b/benchmark-overhead/README.md index fe3fd11321f3..5aaef1af19bd 100644 --- a/benchmark-overhead/README.md +++ b/benchmark-overhead/README.md @@ -1,13 +1,12 @@ - # Overhead tests -* [Process](#process) -* [What do we measure?](#what-do-we-measure) -* [Config](#config) -* [Agents](#agents) -* [Automation](#automation) -* [Setup and Usage](#setup-and-usage) -* [Visualization](#visualization) +- [Process](#process) +- [What do we measure?](#what-do-we-measure) +- [Config](#config) +- [Agents](#agents) +- [Automation](#automation) +- [Setup and Usage](#setup-and-usage) +- [Visualization](#visualization) This directory will contain tools and utilities that help us to measure the performance overhead introduced by @@ -42,44 +41,44 @@ After all the tests are complete, the results are collected and committed back t For each test pass, we record the following metrics in order to compare agents and determine relative overhead. -| metric name | units | description | -|--------------------------|------------|----------------------------------------------------------| -| Startup time | ms | How long it takes for the spring app to report "healthy" -| Total allocated mem | bytes | Across the life of the application -| Heap (min) | bytes | Smallest observed heap size -| Heap (max) | bytes | Largest observed heap size -| Thread switch rate | # / s | Max observed thread context switch rate -| GC time | ms | Total amount of time spent paused for garbage collection -| Request mean | ms | Average time to handle a single web request (measured at the caller) -| Request p95 | ms | 95th percentile time to handle a single web requ4st (measured at the caller) -| Iteration mean | ms | average time to do a single pass through the k6 test script -| Iteration p95 | ms | 95th percentile time to do a single pass through the k6 test script -| Peak threads | # | Highest number of running threads in the VM, including agent threads -| Network read mean | bits/s | Average network read rate -| Network write mean | bits/s | Average network write rate -| Average JVM user CPU | % | Average observed user CPU (range 0.0-1.0) -| Max JVM user CPU | % | Max observed user CPU used (range 0.0-1.0) -| Average machine tot. CPU | % | Average percentage of machine CPU used (range 0.0-1.0) -| Total GC pause nanos | ns | JVM time spent paused due to GC -| Run duration ms | ms | Duration of the test run, in ms +| metric name | units | description | +| ------------------------ | ------ | ---------------------------------------------------------------------------- | +| Startup time | ms | How long it takes for the spring app to report "healthy" | +| Total allocated mem | bytes | Across the life of the application | +| Heap (min) | bytes | Smallest observed heap size | +| Heap (max) | bytes | Largest observed heap size | +| Thread switch rate | # / s | Max observed thread context switch rate | +| GC time | ms | Total amount of time spent paused for garbage collection | +| Request mean | ms | Average time to handle a single web request (measured at the caller) | +| Request p95 | ms | 95th percentile time to handle a single web requ4st (measured at the caller) | +| Iteration mean | ms | average time to do a single pass through the k6 test script | +| Iteration p95 | ms | 95th percentile time to do a single pass through the k6 test script | +| Peak threads | # | Highest number of running threads in the VM, including agent threads | +| Network read mean | bits/s | Average network read rate | +| Network write mean | bits/s | Average network write rate | +| Average JVM user CPU | % | Average observed user CPU (range 0.0-1.0) | +| Max JVM user CPU | % | Max observed user CPU used (range 0.0-1.0) | +| Average machine tot. CPU | % | Average percentage of machine CPU used (range 0.0-1.0) | +| Total GC pause nanos | ns | JVM time spent paused due to GC | +| Run duration ms | ms | Duration of the test run, in ms | ## Config Each config contains the following: -* name -* description -* list of agents (see below) -* maxRequestRate (optional, used to throttle traffic) -* concurrentConnections (number of concurrent virtual users [VUs]) -* totalIterations - the number of passes to make through the k6 test script -* warmupSeconds - how long to wait before starting conducting measurements +- name +- description +- list of agents (see below) +- maxRequestRate (optional, used to throttle traffic) +- concurrentConnections (number of concurrent virtual users [VUs]) +- totalIterations - the number of passes to make through the k6 test script +- warmupSeconds - how long to wait before starting conducting measurements Currently, we test: -* no agent versus latest released agent -* no agent versus latest snapshot -* latest release vs. latest snapshot +- no agent versus latest released agent +- no agent versus latest snapshot +- latest release vs. latest snapshot Additional configurations can be created by submitting a PR against the `Configs` class. diff --git a/benchmark-overhead/build.gradle.kts b/benchmark-overhead/build.gradle.kts index fa83bfbae85f..e43bab0693eb 100644 --- a/benchmark-overhead/build.gradle.kts +++ b/benchmark-overhead/build.gradle.kts @@ -1,6 +1,6 @@ plugins { id("java") - id("com.diffplug.spotless") version "6.19.0" + id("com.diffplug.spotless") version "6.25.0" } spotless { @@ -16,16 +16,16 @@ repositories { } dependencies { - implementation(enforcedPlatform("org.junit:junit-bom:5.9.3")) + implementation(enforcedPlatform("org.junit:junit-bom:5.11.0")) - testImplementation("org.testcontainers:testcontainers:1.18.3") - testImplementation("org.testcontainers:postgresql:1.18.3") + testImplementation("org.testcontainers:testcontainers:1.20.1") + testImplementation("org.testcontainers:postgresql:1.20.1") testImplementation("org.junit.jupiter:junit-jupiter-api") testImplementation("org.junit.jupiter:junit-jupiter-params") - testImplementation("com.squareup.okhttp3:okhttp:4.11.0") - testImplementation("org.jooq:joox:2.0.0") - testImplementation("com.jayway.jsonpath:json-path:2.8.0") - testImplementation("org.slf4j:slf4j-simple:2.0.7") + testImplementation("com.squareup.okhttp3:okhttp:4.12.0") + testImplementation("org.jooq:joox:2.0.1") + testImplementation("com.jayway.jsonpath:json-path:2.9.0") + testImplementation("org.slf4j:slf4j-simple:2.0.16") testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine") } diff --git a/benchmark-overhead/gradle/wrapper/gradle-wrapper.jar b/benchmark-overhead/gradle/wrapper/gradle-wrapper.jar index ccebba7710deaf9f98673a68957ea02138b60d0a..a4b76b9530d66f5e68d973ea569d8e19de379189 100644 GIT binary patch literal 43583 zcma&N1CXTcmMvW9vTb(Rwr$&4wr$(C?dmSu>@vG-+vuvg^_??!{yS%8zW-#zn-LkA z5&1^$^{lnmUON?}LBF8_K|(?T0Ra(xUH{($5eN!MR#ZihR#HxkUPe+_R8Cn`RRs(P z_^*#_XlXmGv7!4;*Y%p4nw?{bNp@UZHv1?Um8r6)Fei3p@ClJn0ECfg1hkeuUU@Or zDaPa;U3fE=3L}DooL;8f;P0ipPt0Z~9P0)lbStMS)ag54=uL9ia-Lm3nh|@(Y?B`; zx_#arJIpXH!U{fbCbI^17}6Ri*H<>OLR%c|^mh8+)*h~K8Z!9)DPf zR2h?lbDZQ`p9P;&DQ4F0sur@TMa!Y}S8irn(%d-gi0*WxxCSk*A?3lGh=gcYN?FGl z7D=Js!i~0=u3rox^eO3i@$0=n{K1lPNU zwmfjRVmLOCRfe=seV&P*1Iq=^i`502keY8Uy-WNPwVNNtJFx?IwAyRPZo2Wo1+S(xF37LJZ~%i)kpFQ3Fw=mXfd@>%+)RpYQLnr}B~~zoof(JVm^^&f zxKV^+3D3$A1G;qh4gPVjhrC8e(VYUHv#dy^)(RoUFM?o%W-EHxufuWf(l*@-l+7vt z=l`qmR56K~F|v<^Pd*p~1_y^P0P^aPC##d8+HqX4IR1gu+7w#~TBFphJxF)T$2WEa zxa?H&6=Qe7d(#tha?_1uQys2KtHQ{)Qco)qwGjrdNL7thd^G5i8Os)CHqc>iOidS} z%nFEDdm=GXBw=yXe1W-ShHHFb?Cc70+$W~z_+}nAoHFYI1MV1wZegw*0y^tC*s%3h zhD3tN8b=Gv&rj}!SUM6|ajSPp*58KR7MPpI{oAJCtY~JECm)*m_x>AZEu>DFgUcby z1Qaw8lU4jZpQ_$;*7RME+gq1KySGG#Wql>aL~k9tLrSO()LWn*q&YxHEuzmwd1?aAtI zBJ>P=&$=l1efe1CDU;`Fd+_;&wI07?V0aAIgc(!{a z0Jg6Y=inXc3^n!U0Atk`iCFIQooHqcWhO(qrieUOW8X(x?(RD}iYDLMjSwffH2~tB z)oDgNBLB^AJBM1M^c5HdRx6fBfka`(LD-qrlh5jqH~);#nw|iyp)()xVYak3;Ybik z0j`(+69aK*B>)e_p%=wu8XC&9e{AO4c~O1U`5X9}?0mrd*m$_EUek{R?DNSh(=br# z#Q61gBzEpmy`$pA*6!87 zSDD+=@fTY7<4A?GLqpA?Pb2z$pbCc4B4zL{BeZ?F-8`s$?>*lXXtn*NC61>|*w7J* z$?!iB{6R-0=KFmyp1nnEmLsA-H0a6l+1uaH^g%c(p{iT&YFrbQ$&PRb8Up#X3@Zsk zD^^&LK~111%cqlP%!_gFNa^dTYT?rhkGl}5=fL{a`UViaXWI$k-UcHJwmaH1s=S$4 z%4)PdWJX;hh5UoK?6aWoyLxX&NhNRqKam7tcOkLh{%j3K^4Mgx1@i|Pi&}<^5>hs5 zm8?uOS>%)NzT(%PjVPGa?X%`N2TQCKbeH2l;cTnHiHppPSJ<7y-yEIiC!P*ikl&!B z%+?>VttCOQM@ShFguHVjxX^?mHX^hSaO_;pnyh^v9EumqSZTi+#f&_Vaija0Q-e*| z7ulQj6Fs*bbmsWp{`auM04gGwsYYdNNZcg|ph0OgD>7O}Asn7^Z=eI>`$2*v78;sj-}oMoEj&@)9+ycEOo92xSyY344^ z11Hb8^kdOvbf^GNAK++bYioknrpdN>+u8R?JxG=!2Kd9r=YWCOJYXYuM0cOq^FhEd zBg2puKy__7VT3-r*dG4c62Wgxi52EMCQ`bKgf*#*ou(D4-ZN$+mg&7$u!! z-^+Z%;-3IDwqZ|K=ah85OLwkO zKxNBh+4QHh)u9D?MFtpbl)us}9+V!D%w9jfAMYEb>%$A;u)rrI zuBudh;5PN}_6J_}l55P3l_)&RMlH{m!)ai-i$g)&*M`eN$XQMw{v^r@-125^RRCF0 z^2>|DxhQw(mtNEI2Kj(;KblC7x=JlK$@78`O~>V!`|1Lm-^JR$-5pUANAnb(5}B}JGjBsliK4& zk6y(;$e&h)lh2)L=bvZKbvh@>vLlreBdH8No2>$#%_Wp1U0N7Ank!6$dFSi#xzh|( zRi{Uw%-4W!{IXZ)fWx@XX6;&(m_F%c6~X8hx=BN1&q}*( zoaNjWabE{oUPb!Bt$eyd#$5j9rItB-h*5JiNi(v^e|XKAj*8(k<5-2$&ZBR5fF|JA z9&m4fbzNQnAU}r8ab>fFV%J0z5awe#UZ|bz?Ur)U9bCIKWEzi2%A+5CLqh?}K4JHi z4vtM;+uPsVz{Lfr;78W78gC;z*yTch~4YkLr&m-7%-xc ztw6Mh2d>_iO*$Rd8(-Cr1_V8EO1f*^@wRoSozS) zy1UoC@pruAaC8Z_7~_w4Q6n*&B0AjOmMWa;sIav&gu z|J5&|{=a@vR!~k-OjKEgPFCzcJ>#A1uL&7xTDn;{XBdeM}V=l3B8fE1--DHjSaxoSjNKEM9|U9#m2<3>n{Iuo`r3UZp;>GkT2YBNAh|b z^jTq-hJp(ebZh#Lk8hVBP%qXwv-@vbvoREX$TqRGTgEi$%_F9tZES@z8Bx}$#5eeG zk^UsLBH{bc2VBW)*EdS({yw=?qmevwi?BL6*=12k9zM5gJv1>y#ML4!)iiPzVaH9% zgSImetD@dam~e>{LvVh!phhzpW+iFvWpGT#CVE5TQ40n%F|p(sP5mXxna+Ev7PDwA zamaV4m*^~*xV+&p;W749xhb_X=$|LD;FHuB&JL5?*Y2-oIT(wYY2;73<^#46S~Gx| z^cez%V7x$81}UWqS13Gz80379Rj;6~WdiXWOSsdmzY39L;Hg3MH43o*y8ibNBBH`(av4|u;YPq%{R;IuYow<+GEsf@R?=@tT@!}?#>zIIn0CoyV!hq3mw zHj>OOjfJM3F{RG#6ujzo?y32m^tgSXf@v=J$ELdJ+=5j|=F-~hP$G&}tDZsZE?5rX ztGj`!S>)CFmdkccxM9eGIcGnS2AfK#gXwj%esuIBNJQP1WV~b~+D7PJTmWGTSDrR` zEAu4B8l>NPuhsk5a`rReSya2nfV1EK01+G!x8aBdTs3Io$u5!6n6KX%uv@DxAp3F@{4UYg4SWJtQ-W~0MDb|j-$lwVn znAm*Pl!?Ps&3wO=R115RWKb*JKoexo*)uhhHBncEDMSVa_PyA>k{Zm2(wMQ(5NM3# z)jkza|GoWEQo4^s*wE(gHz?Xsg4`}HUAcs42cM1-qq_=+=!Gk^y710j=66(cSWqUe zklbm8+zB_syQv5A2rj!Vbw8;|$@C!vfNmNV!yJIWDQ>{+2x zKjuFX`~~HKG~^6h5FntRpnnHt=D&rq0>IJ9#F0eM)Y-)GpRjiN7gkA8wvnG#K=q{q z9dBn8_~wm4J<3J_vl|9H{7q6u2A!cW{bp#r*-f{gOV^e=8S{nc1DxMHFwuM$;aVI^ zz6A*}m8N-&x8;aunp1w7_vtB*pa+OYBw=TMc6QK=mbA-|Cf* zvyh8D4LRJImooUaSb7t*fVfih<97Gf@VE0|z>NcBwBQze);Rh!k3K_sfunToZY;f2 z^HmC4KjHRVg+eKYj;PRN^|E0>Gj_zagfRbrki68I^#~6-HaHg3BUW%+clM1xQEdPYt_g<2K+z!$>*$9nQ>; zf9Bei{?zY^-e{q_*|W#2rJG`2fy@{%6u0i_VEWTq$*(ZN37|8lFFFt)nCG({r!q#9 z5VK_kkSJ3?zOH)OezMT{!YkCuSSn!K#-Rhl$uUM(bq*jY? zi1xbMVthJ`E>d>(f3)~fozjg^@eheMF6<)I`oeJYx4*+M&%c9VArn(OM-wp%M<-`x z7sLP1&3^%Nld9Dhm@$3f2}87!quhI@nwd@3~fZl_3LYW-B?Ia>ui`ELg z&Qfe!7m6ze=mZ`Ia9$z|ARSw|IdMpooY4YiPN8K z4B(ts3p%2i(Td=tgEHX z0UQ_>URBtG+-?0E;E7Ld^dyZ;jjw0}XZ(}-QzC6+NN=40oDb2^v!L1g9xRvE#@IBR zO!b-2N7wVfLV;mhEaXQ9XAU+>=XVA6f&T4Z-@AX!leJ8obP^P^wP0aICND?~w&NykJ#54x3_@r7IDMdRNy4Hh;h*!u(Ol(#0bJdwEo$5437-UBjQ+j=Ic>Q2z` zJNDf0yO6@mr6y1#n3)s(W|$iE_i8r@Gd@!DWDqZ7J&~gAm1#~maIGJ1sls^gxL9LLG_NhU!pTGty!TbhzQnu)I*S^54U6Yu%ZeCg`R>Q zhBv$n5j0v%O_j{QYWG!R9W?5_b&67KB$t}&e2LdMvd(PxN6Ir!H4>PNlerpBL>Zvyy!yw z-SOo8caEpDt(}|gKPBd$qND5#a5nju^O>V&;f890?yEOfkSG^HQVmEbM3Ugzu+UtH zC(INPDdraBN?P%kE;*Ae%Wto&sgw(crfZ#Qy(<4nk;S|hD3j{IQRI6Yq|f^basLY; z-HB&Je%Gg}Jt@={_C{L$!RM;$$|iD6vu#3w?v?*;&()uB|I-XqEKqZPS!reW9JkLewLb!70T7n`i!gNtb1%vN- zySZj{8-1>6E%H&=V}LM#xmt`J3XQoaD|@XygXjdZ1+P77-=;=eYpoEQ01B@L*a(uW zrZeZz?HJsw_4g0vhUgkg@VF8<-X$B8pOqCuWAl28uB|@r`19DTUQQsb^pfqB6QtiT z*`_UZ`fT}vtUY#%sq2{rchyfu*pCg;uec2$-$N_xgjZcoumE5vSI{+s@iLWoz^Mf; zuI8kDP{!XY6OP~q5}%1&L}CtfH^N<3o4L@J@zg1-mt{9L`s^z$Vgb|mr{@WiwAqKg zp#t-lhrU>F8o0s1q_9y`gQNf~Vb!F%70f}$>i7o4ho$`uciNf=xgJ>&!gSt0g;M>*x4-`U)ysFW&Vs^Vk6m%?iuWU+o&m(2Jm26Y(3%TL; zA7T)BP{WS!&xmxNw%J=$MPfn(9*^*TV;$JwRy8Zl*yUZi8jWYF>==j~&S|Xinsb%c z2?B+kpet*muEW7@AzjBA^wAJBY8i|#C{WtO_or&Nj2{=6JTTX05}|H>N2B|Wf!*3_ z7hW*j6p3TvpghEc6-wufFiY!%-GvOx*bZrhZu+7?iSrZL5q9}igiF^*R3%DE4aCHZ zqu>xS8LkW+Auv%z-<1Xs92u23R$nk@Pk}MU5!gT|c7vGlEA%G^2th&Q*zfg%-D^=f z&J_}jskj|Q;73NP4<4k*Y%pXPU2Thoqr+5uH1yEYM|VtBPW6lXaetokD0u z9qVek6Q&wk)tFbQ8(^HGf3Wp16gKmr>G;#G(HRBx?F`9AIRboK+;OfHaLJ(P>IP0w zyTbTkx_THEOs%Q&aPrxbZrJlio+hCC_HK<4%f3ZoSAyG7Dn`=X=&h@m*|UYO-4Hq0 z-Bq&+Ie!S##4A6OGoC~>ZW`Y5J)*ouaFl_e9GA*VSL!O_@xGiBw!AF}1{tB)z(w%c zS1Hmrb9OC8>0a_$BzeiN?rkPLc9%&;1CZW*4}CDDNr2gcl_3z+WC15&H1Zc2{o~i) z)LLW=WQ{?ricmC`G1GfJ0Yp4Dy~Ba;j6ZV4r{8xRs`13{dD!xXmr^Aga|C=iSmor% z8hi|pTXH)5Yf&v~exp3o+sY4B^^b*eYkkCYl*T{*=-0HniSA_1F53eCb{x~1k3*`W zr~};p1A`k{1DV9=UPnLDgz{aJH=-LQo<5%+Em!DNN252xwIf*wF_zS^!(XSm(9eoj z=*dXG&n0>)_)N5oc6v!>-bd(2ragD8O=M|wGW z!xJQS<)u70m&6OmrF0WSsr@I%T*c#Qo#Ha4d3COcX+9}hM5!7JIGF>7<~C(Ear^Sn zm^ZFkV6~Ula6+8S?oOROOA6$C&q&dp`>oR-2Ym3(HT@O7Sd5c~+kjrmM)YmgPH*tL zX+znN>`tv;5eOfX?h{AuX^LK~V#gPCu=)Tigtq9&?7Xh$qN|%A$?V*v=&-2F$zTUv z`C#WyIrChS5|Kgm_GeudCFf;)!WH7FI60j^0o#65o6`w*S7R@)88n$1nrgU(oU0M9 zx+EuMkC>(4j1;m6NoGqEkpJYJ?vc|B zOlwT3t&UgL!pX_P*6g36`ZXQ; z9~Cv}ANFnJGp(;ZhS(@FT;3e)0)Kp;h^x;$*xZn*k0U6-&FwI=uOGaODdrsp-!K$Ac32^c{+FhI-HkYd5v=`PGsg%6I`4d9Jy)uW0y%) zm&j^9WBAp*P8#kGJUhB!L?a%h$hJgQrx!6KCB_TRo%9{t0J7KW8!o1B!NC)VGLM5! zpZy5Jc{`r{1e(jd%jsG7k%I+m#CGS*BPA65ZVW~fLYw0dA-H_}O zrkGFL&P1PG9p2(%QiEWm6x;U-U&I#;Em$nx-_I^wtgw3xUPVVu zqSuKnx&dIT-XT+T10p;yjo1Y)z(x1fb8Dzfn8e yu?e%!_ptzGB|8GrCfu%p?(_ zQccdaaVK$5bz;*rnyK{_SQYM>;aES6Qs^lj9lEs6_J+%nIiuQC*fN;z8md>r_~Mfl zU%p5Dt_YT>gQqfr@`cR!$NWr~+`CZb%dn;WtzrAOI>P_JtsB76PYe*<%H(y>qx-`Kq!X_; z<{RpAqYhE=L1r*M)gNF3B8r(<%8mo*SR2hu zccLRZwGARt)Hlo1euqTyM>^!HK*!Q2P;4UYrysje@;(<|$&%vQekbn|0Ruu_Io(w4#%p6ld2Yp7tlA`Y$cciThP zKzNGIMPXX%&Ud0uQh!uQZz|FB`4KGD?3!ND?wQt6!n*f4EmCoJUh&b?;B{|lxs#F- z31~HQ`SF4x$&v00@(P+j1pAaj5!s`)b2RDBp*PB=2IB>oBF!*6vwr7Dp%zpAx*dPr zb@Zjq^XjN?O4QcZ*O+8>)|HlrR>oD*?WQl5ri3R#2?*W6iJ>>kH%KnnME&TT@ZzrHS$Q%LC?n|e>V+D+8D zYc4)QddFz7I8#}y#Wj6>4P%34dZH~OUDb?uP%-E zwjXM(?Sg~1!|wI(RVuxbu)-rH+O=igSho_pDCw(c6b=P zKk4ATlB?bj9+HHlh<_!&z0rx13K3ZrAR8W)!@Y}o`?a*JJsD+twZIv`W)@Y?Amu_u zz``@-e2X}27$i(2=9rvIu5uTUOVhzwu%mNazS|lZb&PT;XE2|B&W1>=B58#*!~D&) zfVmJGg8UdP*fx(>Cj^?yS^zH#o-$Q-*$SnK(ZVFkw+er=>N^7!)FtP3y~Xxnu^nzY zikgB>Nj0%;WOltWIob|}%lo?_C7<``a5hEkx&1ku$|)i>Rh6@3h*`slY=9U}(Ql_< zaNG*J8vb&@zpdhAvv`?{=zDedJ23TD&Zg__snRAH4eh~^oawdYi6A3w8<Ozh@Kw)#bdktM^GVb zrG08?0bG?|NG+w^&JvD*7LAbjED{_Zkc`3H!My>0u5Q}m!+6VokMLXxl`Mkd=g&Xx z-a>m*#G3SLlhbKB!)tnzfWOBV;u;ftU}S!NdD5+YtOjLg?X}dl>7m^gOpihrf1;PY zvll&>dIuUGs{Qnd- zwIR3oIrct8Va^Tm0t#(bJD7c$Z7DO9*7NnRZorrSm`b`cxz>OIC;jSE3DO8`hX955ui`s%||YQtt2 z5DNA&pG-V+4oI2s*x^>-$6J?p=I>C|9wZF8z;VjR??Icg?1w2v5Me+FgAeGGa8(3S z4vg*$>zC-WIVZtJ7}o9{D-7d>zCe|z#<9>CFve-OPAYsneTb^JH!Enaza#j}^mXy1 z+ULn^10+rWLF6j2>Ya@@Kq?26>AqK{A_| zQKb*~F1>sE*=d?A?W7N2j?L09_7n+HGi{VY;MoTGr_)G9)ot$p!-UY5zZ2Xtbm=t z@dpPSGwgH=QtIcEulQNI>S-#ifbnO5EWkI;$A|pxJd885oM+ zGZ0_0gDvG8q2xebj+fbCHYfAXuZStH2j~|d^sBAzo46(K8n59+T6rzBwK)^rfPT+B zyIFw)9YC-V^rhtK`!3jrhmW-sTmM+tPH+;nwjL#-SjQPUZ53L@A>y*rt(#M(qsiB2 zx6B)dI}6Wlsw%bJ8h|(lhkJVogQZA&n{?Vgs6gNSXzuZpEyu*xySy8ro07QZ7Vk1!3tJphN_5V7qOiyK8p z#@jcDD8nmtYi1^l8ml;AF<#IPK?!pqf9D4moYk>d99Im}Jtwj6c#+A;f)CQ*f-hZ< z=p_T86jog%!p)D&5g9taSwYi&eP z#JuEK%+NULWus;0w32-SYFku#i}d~+{Pkho&^{;RxzP&0!RCm3-9K6`>KZpnzS6?L z^H^V*s!8<>x8bomvD%rh>Zp3>Db%kyin;qtl+jAv8Oo~1g~mqGAC&Qi_wy|xEt2iz zWAJEfTV%cl2Cs<1L&DLRVVH05EDq`pH7Oh7sR`NNkL%wi}8n>IXcO40hp+J+sC!W?!krJf!GJNE8uj zg-y~Ns-<~D?yqbzVRB}G>0A^f0!^N7l=$m0OdZuqAOQqLc zX?AEGr1Ht+inZ-Qiwnl@Z0qukd__a!C*CKuGdy5#nD7VUBM^6OCpxCa2A(X;e0&V4 zM&WR8+wErQ7UIc6LY~Q9x%Sn*Tn>>P`^t&idaOEnOd(Ufw#>NoR^1QdhJ8s`h^|R_ zXX`c5*O~Xdvh%q;7L!_!ohf$NfEBmCde|#uVZvEo>OfEq%+Ns7&_f$OR9xsihRpBb z+cjk8LyDm@U{YN>+r46?nn{7Gh(;WhFw6GAxtcKD+YWV?uge>;+q#Xx4!GpRkVZYu zzsF}1)7$?%s9g9CH=Zs+B%M_)+~*j3L0&Q9u7!|+T`^O{xE6qvAP?XWv9_MrZKdo& z%IyU)$Q95AB4!#hT!_dA>4e@zjOBD*Y=XjtMm)V|+IXzjuM;(l+8aA5#Kaz_$rR6! zj>#&^DidYD$nUY(D$mH`9eb|dtV0b{S>H6FBfq>t5`;OxA4Nn{J(+XihF(stSche7$es&~N$epi&PDM_N`As;*9D^L==2Q7Z2zD+CiU(|+-kL*VG+&9!Yb3LgPy?A zm7Z&^qRG_JIxK7-FBzZI3Q<;{`DIxtc48k> zc|0dmX;Z=W$+)qE)~`yn6MdoJ4co;%!`ddy+FV538Y)j(vg}5*k(WK)KWZ3WaOG!8 z!syGn=s{H$odtpqFrT#JGM*utN7B((abXnpDM6w56nhw}OY}0TiTG1#f*VFZr+^-g zbP10`$LPq_;PvrA1XXlyx2uM^mrjTzX}w{yuLo-cOClE8MMk47T25G8M!9Z5ypOSV zAJUBGEg5L2fY)ZGJb^E34R2zJ?}Vf>{~gB!8=5Z) z9y$>5c)=;o0HeHHSuE4U)#vG&KF|I%-cF6f$~pdYJWk_dD}iOA>iA$O$+4%@>JU08 zS`ep)$XLPJ+n0_i@PkF#ri6T8?ZeAot$6JIYHm&P6EB=BiaNY|aA$W0I+nz*zkz_z zkEru!tj!QUffq%)8y0y`T&`fuus-1p>=^hnBiBqD^hXrPs`PY9tU3m0np~rISY09> z`P3s=-kt_cYcxWd{de@}TwSqg*xVhp;E9zCsnXo6z z?f&Sv^U7n4`xr=mXle94HzOdN!2kB~4=%)u&N!+2;z6UYKUDqi-s6AZ!haB;@&B`? z_TRX0%@suz^TRdCb?!vNJYPY8L_}&07uySH9%W^Tc&1pia6y1q#?*Drf}GjGbPjBS zbOPcUY#*$3sL2x4v_i*Y=N7E$mR}J%|GUI(>WEr+28+V z%v5{#e!UF*6~G&%;l*q*$V?&r$Pp^sE^i-0$+RH3ERUUdQ0>rAq2(2QAbG}$y{de( z>{qD~GGuOk559Y@%$?N^1ApVL_a704>8OD%8Y%8B;FCt%AoPu8*D1 zLB5X>b}Syz81pn;xnB}%0FnwazlWfUV)Z-~rZg6~b z6!9J$EcE&sEbzcy?CI~=boWA&eeIa%z(7SE^qgVLz??1Vbc1*aRvc%Mri)AJaAG!p z$X!_9Ds;Zz)f+;%s&dRcJt2==P{^j3bf0M=nJd&xwUGlUFn?H=2W(*2I2Gdu zv!gYCwM10aeus)`RIZSrCK=&oKaO_Ry~D1B5!y0R=%!i2*KfXGYX&gNv_u+n9wiR5 z*e$Zjju&ODRW3phN925%S(jL+bCHv6rZtc?!*`1TyYXT6%Ju=|X;6D@lq$8T zW{Y|e39ioPez(pBH%k)HzFITXHvnD6hw^lIoUMA;qAJ^CU?top1fo@s7xT13Fvn1H z6JWa-6+FJF#x>~+A;D~;VDs26>^oH0EI`IYT2iagy23?nyJ==i{g4%HrAf1-*v zK1)~@&(KkwR7TL}L(A@C_S0G;-GMDy=MJn2$FP5s<%wC)4jC5PXoxrQBFZ_k0P{{s@sz+gX`-!=T8rcB(=7vW}^K6oLWMmp(rwDh}b zwaGGd>yEy6fHv%jM$yJXo5oMAQ>c9j`**}F?MCry;T@47@r?&sKHgVe$MCqk#Z_3S z1GZI~nOEN*P~+UaFGnj{{Jo@16`(qVNtbU>O0Hf57-P>x8Jikp=`s8xWs^dAJ9lCQ z)GFm+=OV%AMVqVATtN@|vp61VVAHRn87}%PC^RAzJ%JngmZTasWBAWsoAqBU+8L8u z4A&Pe?fmTm0?mK-BL9t+{y7o(7jm+RpOhL9KnY#E&qu^}B6=K_dB}*VlSEiC9fn)+V=J;OnN)Ta5v66ic1rG+dGAJ1 z1%Zb_+!$=tQ~lxQrzv3x#CPb?CekEkA}0MYSgx$Jdd}q8+R=ma$|&1a#)TQ=l$1tQ z=tL9&_^vJ)Pk}EDO-va`UCT1m#Uty1{v^A3P~83_#v^ozH}6*9mIjIr;t3Uv%@VeW zGL6(CwCUp)Jq%G0bIG%?{_*Y#5IHf*5M@wPo6A{$Um++Co$wLC=J1aoG93&T7Ho}P z=mGEPP7GbvoG!uD$k(H3A$Z))+i{Hy?QHdk>3xSBXR0j!11O^mEe9RHmw!pvzv?Ua~2_l2Yh~_!s1qS`|0~0)YsbHSz8!mG)WiJE| z2f($6TQtt6L_f~ApQYQKSb=`053LgrQq7G@98#igV>y#i==-nEjQ!XNu9 z~;mE+gtj4IDDNQJ~JVk5Ux6&LCSFL!y=>79kE9=V}J7tD==Ga+IW zX)r7>VZ9dY=V&}DR))xUoV!u(Z|%3ciQi_2jl}3=$Agc(`RPb z8kEBpvY>1FGQ9W$n>Cq=DIpski};nE)`p3IUw1Oz0|wxll^)4dq3;CCY@RyJgFgc# zKouFh!`?Xuo{IMz^xi-h=StCis_M7yq$u) z?XHvw*HP0VgR+KR6wI)jEMX|ssqYvSf*_3W8zVTQzD?3>H!#>InzpSO)@SC8q*ii- z%%h}_#0{4JG;Jm`4zg};BPTGkYamx$Xo#O~lBirRY)q=5M45n{GCfV7h9qwyu1NxOMoP4)jjZMxmT|IQQh0U7C$EbnMN<3)Kk?fFHYq$d|ICu>KbY_hO zTZM+uKHe(cIZfEqyzyYSUBZa8;Fcut-GN!HSA9ius`ltNebF46ZX_BbZNU}}ZOm{M2&nANL9@0qvih15(|`S~z}m&h!u4x~(%MAO$jHRWNfuxWF#B)E&g3ghSQ9|> z(MFaLQj)NE0lowyjvg8z0#m6FIuKE9lDO~Glg}nSb7`~^&#(Lw{}GVOS>U)m8bF}x zVjbXljBm34Cs-yM6TVusr+3kYFjr28STT3g056y3cH5Tmge~ASxBj z%|yb>$eF;WgrcOZf569sDZOVwoo%8>XO>XQOX1OyN9I-SQgrm;U;+#3OI(zrWyow3 zk==|{lt2xrQ%FIXOTejR>;wv(Pb8u8}BUpx?yd(Abh6? zsoO3VYWkeLnF43&@*#MQ9-i-d0t*xN-UEyNKeyNMHw|A(k(_6QKO=nKMCxD(W(Yop zsRQ)QeL4X3Lxp^L%wzi2-WVSsf61dqliPUM7srDB?Wm6Lzn0&{*}|IsKQW;02(Y&| zaTKv|`U(pSzuvR6Rduu$wzK_W-Y-7>7s?G$)U}&uK;<>vU}^^ns@Z!p+9?St1s)dG zK%y6xkPyyS1$~&6v{kl?Md6gwM|>mt6Upm>oa8RLD^8T{0?HC!Z>;(Bob7el(DV6x zi`I)$&E&ngwFS@bi4^xFLAn`=fzTC;aimE^!cMI2n@Vo%Ae-ne`RF((&5y6xsjjAZ zVguVoQ?Z9uk$2ON;ersE%PU*xGO@T*;j1BO5#TuZKEf(mB7|g7pcEA=nYJ{s3vlbg zd4-DUlD{*6o%Gc^N!Nptgay>j6E5;3psI+C3Q!1ZIbeCubW%w4pq9)MSDyB{HLm|k zxv-{$$A*pS@csolri$Ge<4VZ}e~78JOL-EVyrbxKra^d{?|NnPp86!q>t<&IP07?Z z^>~IK^k#OEKgRH+LjllZXk7iA>2cfH6+(e&9ku5poo~6y{GC5>(bRK7hwjiurqAiZ zg*DmtgY}v83IjE&AbiWgMyFbaRUPZ{lYiz$U^&Zt2YjG<%m((&_JUbZcfJ22(>bi5 z!J?<7AySj0JZ&<-qXX;mcV!f~>G=sB0KnjWca4}vrtunD^1TrpfeS^4dvFr!65knK zZh`d;*VOkPs4*-9kL>$GP0`(M!j~B;#x?Ba~&s6CopvO86oM?-? zOw#dIRc;6A6T?B`Qp%^<U5 z19x(ywSH$_N+Io!6;e?`tWaM$`=Db!gzx|lQ${DG!zb1Zl&|{kX0y6xvO1o z220r<-oaS^^R2pEyY;=Qllqpmue|5yI~D|iI!IGt@iod{Opz@*ml^w2bNs)p`M(Io z|E;;m*Xpjd9l)4G#KaWfV(t8YUn@A;nK^#xgv=LtnArX|vWQVuw3}B${h+frU2>9^ z!l6)!Uo4`5k`<<;E(ido7M6lKTgWezNLq>U*=uz&s=cc$1%>VrAeOoUtA|T6gO4>UNqsdK=NF*8|~*sl&wI=x9-EGiq*aqV!(VVXA57 zw9*o6Ir8Lj1npUXvlevtn(_+^X5rzdR>#(}4YcB9O50q97%rW2me5_L=%ffYPUSRc z!vv?Kv>dH994Qi>U(a<0KF6NH5b16enCp+mw^Hb3Xs1^tThFpz!3QuN#}KBbww`(h z7GO)1olDqy6?T$()R7y%NYx*B0k_2IBiZ14&8|JPFxeMF{vW>HF-Vi3+ZOI=+qP}n zw(+!WcTd~4ZJX1!ZM&y!+uyt=&i!+~d(V%GjH;-NsEEv6nS1TERt|RHh!0>W4+4pp z1-*EzAM~i`+1f(VEHI8So`S`akPfPTfq*`l{Fz`hS%k#JS0cjT2mS0#QLGf=J?1`he3W*;m4)ce8*WFq1sdP=~$5RlH1EdWm|~dCvKOi4*I_96{^95p#B<(n!d?B z=o`0{t+&OMwKcxiBECznJcfH!fL(z3OvmxP#oWd48|mMjpE||zdiTBdWelj8&Qosv zZFp@&UgXuvJw5y=q6*28AtxZzo-UUpkRW%ne+Ylf!V-0+uQXBW=5S1o#6LXNtY5!I z%Rkz#(S8Pjz*P7bqB6L|M#Er{|QLae-Y{KA>`^} z@lPjeX>90X|34S-7}ZVXe{wEei1<{*e8T-Nbj8JmD4iwcE+Hg_zhkPVm#=@b$;)h6 z<<6y`nPa`f3I6`!28d@kdM{uJOgM%`EvlQ5B2bL)Sl=|y@YB3KeOzz=9cUW3clPAU z^sYc}xf9{4Oj?L5MOlYxR{+>w=vJjvbyO5}ptT(o6dR|ygO$)nVCvNGnq(6;bHlBd zl?w-|plD8spjDF03g5ip;W3Z z><0{BCq!Dw;h5~#1BuQilq*TwEu)qy50@+BE4bX28+7erX{BD4H)N+7U`AVEuREE8 z;X?~fyhF-x_sRfHIj~6f(+^@H)D=ngP;mwJjxhQUbUdzk8f94Ab%59-eRIq?ZKrwD z(BFI=)xrUlgu(b|hAysqK<}8bslmNNeD=#JW*}^~Nrswn^xw*nL@Tx!49bfJecV&KC2G4q5a!NSv)06A_5N3Y?veAz;Gv+@U3R% z)~UA8-0LvVE{}8LVDOHzp~2twReqf}ODIyXMM6=W>kL|OHcx9P%+aJGYi_Om)b!xe zF40Vntn0+VP>o<$AtP&JANjXBn7$}C@{+@3I@cqlwR2MdwGhVPxlTIcRVu@Ho-wO` z_~Or~IMG)A_`6-p)KPS@cT9mu9RGA>dVh5wY$NM9-^c@N=hcNaw4ITjm;iWSP^ZX| z)_XpaI61<+La+U&&%2a z0za$)-wZP@mwSELo#3!PGTt$uy0C(nTT@9NX*r3Ctw6J~7A(m#8fE)0RBd`TdKfAT zCf@$MAxjP`O(u9s@c0Fd@|}UQ6qp)O5Q5DPCeE6mSIh|Rj{$cAVIWsA=xPKVKxdhg zLzPZ`3CS+KIO;T}0Ip!fAUaNU>++ZJZRk@I(h<)RsJUhZ&Ru9*!4Ptn;gX^~4E8W^TSR&~3BAZc#HquXn)OW|TJ`CTahk+{qe`5+ixON^zA9IFd8)kc%*!AiLu z>`SFoZ5bW-%7}xZ>gpJcx_hpF$2l+533{gW{a7ce^B9sIdmLrI0)4yivZ^(Vh@-1q zFT!NQK$Iz^xu%|EOK=n>ug;(7J4OnS$;yWmq>A;hsD_0oAbLYhW^1Vdt9>;(JIYjf zdb+&f&D4@4AS?!*XpH>8egQvSVX`36jMd>$+RgI|pEg))^djhGSo&#lhS~9%NuWfX zDDH;3T*GzRT@5=7ibO>N-6_XPBYxno@mD_3I#rDD?iADxX`! zh*v8^i*JEMzyN#bGEBz7;UYXki*Xr(9xXax(_1qVW=Ml)kSuvK$coq2A(5ZGhs_pF z$*w}FbN6+QDseuB9=fdp_MTs)nQf!2SlROQ!gBJBCXD&@-VurqHj0wm@LWX-TDmS= z71M__vAok|@!qgi#H&H%Vg-((ZfxPAL8AI{x|VV!9)ZE}_l>iWk8UPTGHs*?u7RfP z5MC&=c6X;XlUzrz5q?(!eO@~* zoh2I*%J7dF!!_!vXoSIn5o|wj1#_>K*&CIn{qSaRc&iFVxt*^20ngCL;QonIS>I5^ zMw8HXm>W0PGd*}Ko)f|~dDd%;Wu_RWI_d;&2g6R3S63Uzjd7dn%Svu-OKpx*o|N>F zZg=-~qLb~VRLpv`k zWSdfHh@?dp=s_X`{yxOlxE$4iuyS;Z-x!*E6eqmEm*j2bE@=ZI0YZ5%Yj29!5+J$4h{s($nakA`xgbO8w zi=*r}PWz#lTL_DSAu1?f%-2OjD}NHXp4pXOsCW;DS@BC3h-q4_l`<))8WgzkdXg3! zs1WMt32kS2E#L0p_|x+x**TFV=gn`m9BWlzF{b%6j-odf4{7a4y4Uaef@YaeuPhU8 zHBvRqN^;$Jizy+ z=zW{E5<>2gp$pH{M@S*!sJVQU)b*J5*bX4h>5VJve#Q6ga}cQ&iL#=(u+KroWrxa%8&~p{WEUF0il=db;-$=A;&9M{Rq`ouZ5m%BHT6%st%saGsD6)fQgLN}x@d3q>FC;=f%O3Cyg=Ke@Gh`XW za@RajqOE9UB6eE=zhG%|dYS)IW)&y&Id2n7r)6p_)vlRP7NJL(x4UbhlcFXWT8?K=%s7;z?Vjts?y2+r|uk8Wt(DM*73^W%pAkZa1Jd zNoE)8FvQA>Z`eR5Z@Ig6kS5?0h;`Y&OL2D&xnnAUzQz{YSdh0k zB3exx%A2TyI)M*EM6htrxSlep!Kk(P(VP`$p0G~f$smld6W1r_Z+o?=IB@^weq>5VYsYZZR@` z&XJFxd5{|KPZmVOSxc@^%71C@;z}}WhbF9p!%yLj3j%YOlPL5s>7I3vj25 z@xmf=*z%Wb4;Va6SDk9cv|r*lhZ`(y_*M@>q;wrn)oQx%B(2A$9(74>;$zmQ!4fN; z>XurIk-7@wZys<+7XL@0Fhe-f%*=(weaQEdR9Eh6>Kl-EcI({qoZqyzziGwpg-GM#251sK_ z=3|kitS!j%;fpc@oWn65SEL73^N&t>Ix37xgs= zYG%eQDJc|rqHFia0!_sm7`@lvcv)gfy(+KXA@E{3t1DaZ$DijWAcA)E0@X?2ziJ{v z&KOYZ|DdkM{}t+@{@*6ge}m%xfjIxi%qh`=^2Rwz@w0cCvZ&Tc#UmCDbVwABrON^x zEBK43FO@weA8s7zggCOWhMvGGE`baZ62cC)VHyy!5Zbt%ieH+XN|OLbAFPZWyC6)p z4P3%8sq9HdS3=ih^0OOlqTPbKuzQ?lBEI{w^ReUO{V?@`ARsL|S*%yOS=Z%sF)>-y z(LAQdhgAcuF6LQjRYfdbD1g4o%tV4EiK&ElLB&^VZHbrV1K>tHTO{#XTo>)2UMm`2 z^t4s;vnMQgf-njU-RVBRw0P0-m#d-u`(kq7NL&2T)TjI_@iKuPAK-@oH(J8?%(e!0Ir$yG32@CGUPn5w4)+9@8c&pGx z+K3GKESI4*`tYlmMHt@br;jBWTei&(a=iYslc^c#RU3Q&sYp zSG){)V<(g7+8W!Wxeb5zJb4XE{I|&Y4UrFWr%LHkdQ;~XU zgy^dH-Z3lmY+0G~?DrC_S4@=>0oM8Isw%g(id10gWkoz2Q%7W$bFk@mIzTCcIB(K8 zc<5h&ZzCdT=9n-D>&a8vl+=ZF*`uTvQviG_bLde*k>{^)&0o*b05x$MO3gVLUx`xZ z43j+>!u?XV)Yp@MmG%Y`+COH2?nQcMrQ%k~6#O%PeD_WvFO~Kct za4XoCM_X!c5vhRkIdV=xUB3xI2NNStK*8_Zl!cFjOvp-AY=D;5{uXj}GV{LK1~IE2 z|KffUiBaStRr;10R~K2VVtf{TzM7FaPm;Y(zQjILn+tIPSrJh&EMf6evaBKIvi42-WYU9Vhj~3< zZSM-B;E`g_o8_XTM9IzEL=9Lb^SPhe(f(-`Yh=X6O7+6ALXnTcUFpI>ekl6v)ZQeNCg2 z^H|{SKXHU*%nBQ@I3It0m^h+6tvI@FS=MYS$ZpBaG7j#V@P2ZuYySbp@hA# ze(kc;P4i_-_UDP?%<6>%tTRih6VBgScKU^BV6Aoeg6Uh(W^#J^V$Xo^4#Ekp ztqQVK^g9gKMTHvV7nb64UU7p~!B?>Y0oFH5T7#BSW#YfSB@5PtE~#SCCg3p^o=NkMk$<8- z6PT*yIKGrvne7+y3}_!AC8NNeI?iTY(&nakN>>U-zT0wzZf-RuyZk^X9H-DT_*wk= z;&0}6LsGtfVa1q)CEUPlx#(ED@-?H<1_FrHU#z5^P3lEB|qsxEyn%FOpjx z3S?~gvoXy~L(Q{Jh6*i~=f%9kM1>RGjBzQh_SaIDfSU_9!<>*Pm>l)cJD@wlyxpBV z4Fmhc2q=R_wHCEK69<*wG%}mgD1=FHi4h!98B-*vMu4ZGW~%IrYSLGU{^TuseqVgV zLP<%wirIL`VLyJv9XG_p8w@Q4HzNt-o;U@Au{7%Ji;53!7V8Rv0^Lu^Vf*sL>R(;c zQG_ZuFl)Mh-xEIkGu}?_(HwkB2jS;HdPLSxVU&Jxy9*XRG~^HY(f0g8Q}iqnVmgjI zfd=``2&8GsycjR?M%(zMjn;tn9agcq;&rR!Hp z$B*gzHsQ~aXw8c|a(L^LW(|`yGc!qOnV(ZjU_Q-4z1&0;jG&vAKuNG=F|H?@m5^N@ zq{E!1n;)kNTJ>|Hb2ODt-7U~-MOIFo%9I)_@7fnX+eMMNh>)V$IXesJpBn|uo8f~#aOFytCT zf9&%MCLf8mp4kwHTcojWmM3LU=#|{3L>E}SKwOd?%{HogCZ_Z1BSA}P#O(%H$;z7XyJ^sjGX;j5 zrzp>|Ud;*&VAU3x#f{CKwY7Vc{%TKKqmB@oTHA9;>?!nvMA;8+Jh=cambHz#J18x~ zs!dF>$*AnsQ{{82r5Aw&^7eRCdvcgyxH?*DV5(I$qXh^zS>us*I66_MbL8y4d3ULj z{S(ipo+T3Ag!+5`NU2sc+@*m{_X|&p#O-SAqF&g_n7ObB82~$p%fXA5GLHMC+#qqL zdt`sJC&6C2)=juQ_!NeD>U8lDVpAOkW*khf7MCcs$A(wiIl#B9HM%~GtQ^}yBPjT@ z+E=|A!Z?A(rwzZ;T}o6pOVqHzTr*i;Wrc%&36kc@jXq~+w8kVrs;%=IFdACoLAcCAmhFNpbP8;s`zG|HC2Gv?I~w4ITy=g$`0qMQdkijLSOtX6xW%Z9Nw<;M- zMN`c7=$QxN00DiSjbVt9Mi6-pjv*j(_8PyV-il8Q-&TwBwH1gz1uoxs6~uU}PrgWB zIAE_I-a1EqlIaGQNbcp@iI8W1sm9fBBNOk(k&iLBe%MCo#?xI$%ZmGA?=)M9D=0t7 zc)Q0LnI)kCy{`jCGy9lYX%mUsDWwsY`;jE(;Us@gmWPqjmXL+Hu#^;k%eT>{nMtzj zsV`Iy6leTA8-PndszF;N^X@CJrTw5IIm!GPeu)H2#FQitR{1p;MasQVAG3*+=9FYK zw*k!HT(YQorfQj+1*mCV458(T5=fH`um$gS38hw(OqVMyunQ;rW5aPbF##A3fGH6h z@W)i9Uff?qz`YbK4c}JzQpuxuE3pcQO)%xBRZp{zJ^-*|oryTxJ-rR+MXJ)!f=+pp z10H|DdGd2exhi+hftcYbM0_}C0ZI-2vh+$fU1acsB-YXid7O|=9L!3e@$H*6?G*Zp z%qFB(sgl=FcC=E4CYGp4CN>=M8#5r!RU!u+FJVlH6=gI5xHVD&k;Ta*M28BsxfMV~ zLz+@6TxnfLhF@5=yQo^1&S}cmTN@m!7*c6z;}~*!hNBjuE>NLVl2EwN!F+)0$R1S! zR|lF%n!9fkZ@gPW|x|B={V6x3`=jS*$Pu0+5OWf?wnIy>Y1MbbGSncpKO0qE(qO=ts z!~@&!N`10S593pVQu4FzpOh!tvg}p%zCU(aV5=~K#bKi zHdJ1>tQSrhW%KOky;iW+O_n;`l9~omqM%sdxdLtI`TrJzN6BQz+7xOl*rM>xVI2~# z)7FJ^Dc{DC<%~VS?@WXzuOG$YPLC;>#vUJ^MmtbSL`_yXtNKa$Hk+l-c!aC7gn(Cg ze?YPYZ(2Jw{SF6MiO5(%_pTo7j@&DHNW`|lD`~{iH+_eSTS&OC*2WTT*a`?|9w1dh zh1nh@$a}T#WE5$7Od~NvSEU)T(W$p$s5fe^GpG+7fdJ9=enRT9$wEk+ZaB>G3$KQO zgq?-rZZnIv!p#>Ty~}c*Lb_jxJg$eGM*XwHUwuQ|o^}b3^T6Bxx{!?va8aC@-xK*H ztJBFvFfsSWu89%@b^l3-B~O!CXs)I6Y}y#0C0U0R0WG zybjroj$io0j}3%P7zADXOwHwafT#uu*zfM!oD$6aJx7+WL%t-@6^rD_a_M?S^>c;z zMK580bZXo1f*L$CuMeM4Mp!;P@}b~$cd(s5*q~FP+NHSq;nw3fbWyH)i2)-;gQl{S zZO!T}A}fC}vUdskGSq&{`oxt~0i?0xhr6I47_tBc`fqaSrMOzR4>0H^;A zF)hX1nfHs)%Zb-(YGX;=#2R6C{BG;k=?FfP?9{_uFLri~-~AJ;jw({4MU7e*d)?P@ zXX*GkNY9ItFjhwgAIWq7Y!ksbMzfqpG)IrqKx9q{zu%Mdl+{Dis#p9q`02pr1LG8R z@As?eG!>IoROgS!@J*to<27coFc1zpkh?w=)h9CbYe%^Q!Ui46Y*HO0mr% zEff-*$ndMNw}H2a5@BsGj5oFfd!T(F&0$<{GO!Qdd?McKkorh=5{EIjDTHU`So>8V zBA-fqVLb2;u7UhDV1xMI?y>fe3~4urv3%PX)lDw+HYa;HFkaLqi4c~VtCm&Ca+9C~ zge+67hp#R9`+Euq59WhHX&7~RlXn=--m8$iZ~~1C8cv^2(qO#X0?vl91gzUKBeR1J z^p4!!&7)3#@@X&2aF2-)1Ffcc^F8r|RtdL2X%HgN&XU-KH2SLCbpw?J5xJ*!F-ypZ zMG%AJ!Pr&}`LW?E!K~=(NJxuSVTRCGJ$2a*Ao=uUDSys!OFYu!Vs2IT;xQ6EubLIl z+?+nMGeQQhh~??0!s4iQ#gm3!BpMpnY?04kK375e((Uc7B3RMj;wE?BCoQGu=UlZt!EZ1Q*auI)dj3Jj{Ujgt zW5hd~-HWBLI_3HuO) zNrb^XzPsTIb=*a69wAAA3J6AAZZ1VsYbIG}a`=d6?PjM)3EPaDpW2YP$|GrBX{q*! z$KBHNif)OKMBCFP5>!1d=DK>8u+Upm-{hj5o|Wn$vh1&K!lVfDB&47lw$tJ?d5|=B z^(_9=(1T3Fte)z^>|3**n}mIX;mMN5v2F#l(q*CvU{Ga`@VMp#%rQkDBy7kYbmb-q z<5!4iuB#Q_lLZ8}h|hPODI^U6`gzLJre9u3k3c#%86IKI*^H-@I48Bi*@avYm4v!n0+v zWu{M{&F8#p9cx+gF0yTB_<2QUrjMPo9*7^-uP#~gGW~y3nfPAoV%amgr>PSyVAd@l)}8#X zR5zV6t*uKJZL}?NYvPVK6J0v4iVpwiN|>+t3aYiZSp;m0!(1`bHO}TEtWR1tY%BPB z(W!0DmXbZAsT$iC13p4f>u*ZAy@JoLAkJhzFf1#4;#1deO8#8d&89}en&z!W&A3++^1(;>0SB1*54d@y&9Pn;^IAf3GiXbfT`_>{R+Xv; zQvgL>+0#8-laO!j#-WB~(I>l0NCMt_;@Gp_f0#^c)t?&#Xh1-7RR0@zPyBz!U#0Av zT?}n({(p?p7!4S2ZBw)#KdCG)uPnZe+U|0{BW!m)9 zi_9$F?m<`2!`JNFv+w8MK_K)qJ^aO@7-Ig>cM4-r0bi=>?B_2mFNJ}aE3<+QCzRr*NA!QjHw# z`1OsvcoD0?%jq{*7b!l|L1+Tw0TTAM4XMq7*ntc-Ived>Sj_ZtS|uVdpfg1_I9knY z2{GM_j5sDC7(W&}#s{jqbybqJWyn?{PW*&cQIU|*v8YGOKKlGl@?c#TCnmnAkAzV- zmK={|1G90zz=YUvC}+fMqts0d4vgA%t6Jhjv?d;(Z}(Ep8fTZfHA9``fdUHkA+z3+ zhh{ohP%Bj?T~{i0sYCQ}uC#5BwN`skI7`|c%kqkyWIQ;!ysvA8H`b-t()n6>GJj6xlYDu~8qX{AFo$Cm3d|XFL=4uvc?Keb zzb0ZmMoXca6Mob>JqkNuoP>B2Z>D`Q(TvrG6m`j}-1rGP!g|qoL=$FVQYxJQjFn33lODt3Wb1j8VR zlR++vIT6^DtYxAv_hxupbLLN3e0%A%a+hWTKDV3!Fjr^cWJ{scsAdfhpI)`Bms^M6 zQG$waKgFr=c|p9Piug=fcJvZ1ThMnNhQvBAg-8~b1?6wL*WyqXhtj^g(Ke}mEfZVM zJuLNTUVh#WsE*a6uqiz`b#9ZYg3+2%=C(6AvZGc=u&<6??!slB1a9K)=VL zY9EL^mfyKnD zSJyYBc_>G;5RRnrNgzJz#Rkn3S1`mZgO`(r5;Hw6MveN(URf_XS-r58Cn80K)ArH4 z#Rrd~LG1W&@ttw85cjp8xV&>$b%nSXH_*W}7Ch2pg$$c0BdEo-HWRTZcxngIBJad> z;C>b{jIXjb_9Jis?NZJsdm^EG}e*pR&DAy0EaSGi3XWTa(>C%tz1n$u?5Fb z1qtl?;_yjYo)(gB^iQq?=jusF%kywm?CJP~zEHi0NbZ);$(H$w(Hy@{i>$wcVRD_X|w-~(0Z9BJyh zhNh;+eQ9BEIs;tPz%jSVnfCP!3L&9YtEP;svoj_bNzeGSQIAjd zBss@A;)R^WAu-37RQrM%{DfBNRx>v!G31Z}8-El9IOJlb_MSoMu2}GDYycNaf>uny z+8xykD-7ONCM!APry_Lw6-yT>5!tR}W;W`C)1>pxSs5o1z#j7%m=&=7O4hz+Lsqm` z*>{+xsabZPr&X=}G@obTb{nPTkccJX8w3CG7X+1+t{JcMabv~UNv+G?txRqXib~c^Mo}`q{$`;EBNJ;#F*{gvS12kV?AZ%O0SFB$^ zn+}!HbmEj}w{Vq(G)OGAzH}R~kS^;(-s&=ectz8vN!_)Yl$$U@HNTI-pV`LSj7Opu zTZ5zZ)-S_{GcEQPIQXLQ#oMS`HPu{`SQiAZ)m1at*Hy%3xma|>o`h%E%8BEbi9p0r zVjcsh<{NBKQ4eKlXU|}@XJ#@uQw*$4BxKn6#W~I4T<^f99~(=}a`&3(ur8R9t+|AQ zWkQx7l}wa48-jO@ft2h+7qn%SJtL%~890FG0s5g*kNbL3I&@brh&f6)TlM`K^(bhr zJWM6N6x3flOw$@|C@kPi7yP&SP?bzP-E|HSXQXG>7gk|R9BTj`e=4de9C6+H7H7n# z#GJeVs1mtHhLDmVO?LkYRQc`DVOJ_vdl8VUihO-j#t=0T3%Fc1f9F73ufJz*adn*p zc%&vi(4NqHu^R>sAT_0EDjVR8bc%wTz#$;%NU-kbDyL_dg0%TFafZwZ?5KZpcuaO54Z9hX zD$u>q!-9`U6-D`E#`W~fIfiIF5_m6{fvM)b1NG3xf4Auw;Go~Fu7cth#DlUn{@~yu z=B;RT*dp?bO}o%4x7k9v{r=Y@^YQ^UUm(Qmliw8brO^=NP+UOohLYiaEB3^DB56&V zK?4jV61B|1Uj_5fBKW;8LdwOFZKWp)g{B%7g1~DgO&N& z#lisxf?R~Z@?3E$Mms$$JK8oe@X`5m98V*aV6Ua}8Xs2#A!{x?IP|N(%nxsH?^c{& z@vY&R1QmQs83BW28qAmJfS7MYi=h(YK??@EhjL-t*5W!p z^gYX!Q6-vBqcv~ruw@oMaU&qp0Fb(dbVzm5xJN%0o_^@fWq$oa3X?9s%+b)x4w-q5Koe(@j6Ez7V@~NRFvd zfBH~)U5!ix3isg`6be__wBJp=1@yfsCMw1C@y+9WYD9_C%{Q~7^0AF2KFryfLlUP# zwrtJEcH)jm48!6tUcxiurAMaiD04C&tPe6DI0#aoqz#Bt0_7_*X*TsF7u*zv(iEfA z;$@?XVu~oX#1YXtceQL{dSneL&*nDug^OW$DSLF0M1Im|sSX8R26&)<0Fbh^*l6!5wfSu8MpMoh=2l z^^0Sr$UpZp*9oqa23fcCfm7`ya2<4wzJ`Axt7e4jJrRFVf?nY~2&tRL* zd;6_njcz01c>$IvN=?K}9ie%Z(BO@JG2J}fT#BJQ+f5LFSgup7i!xWRKw6)iITjZU z%l6hPZia>R!`aZjwCp}I zg)%20;}f+&@t;(%5;RHL>K_&7MH^S+7<|(SZH!u zznW|jz$uA`P9@ZWtJgv$EFp>)K&Gt+4C6#*khZQXS*S~6N%JDT$r`aJDs9|uXWdbg zBwho$phWx}x!qy8&}6y5Vr$G{yGSE*r$^r{}pw zVTZKvikRZ`J_IJrjc=X1uw?estdwm&bEahku&D04HD+0Bm~q#YGS6gp!KLf$A{%Qd z&&yX@Hp>~(wU{|(#U&Bf92+1i&Q*-S+=y=3pSZy$#8Uc$#7oiJUuO{cE6=tsPhwPe| zxQpK>`Dbka`V)$}e6_OXKLB%i76~4N*zA?X+PrhH<&)}prET;kel24kW%+9))G^JI zsq7L{P}^#QsZViX%KgxBvEugr>ZmFqe^oAg?{EI=&_O#e)F3V#rc z8$4}0Zr19qd3tE4#$3_f=Bbx9oV6VO!d3(R===i-7p=Vj`520w0D3W6lQfY48}!D* z&)lZMG;~er2qBoI2gsX+Ts-hnpS~NYRDtPd^FPzn!^&yxRy#CSz(b&E*tL|jIkq|l zf%>)7Dtu>jCf`-7R#*GhGn4FkYf;B$+9IxmqH|lf6$4irg{0ept__%)V*R_OK=T06 zyT_m-o@Kp6U{l5h>W1hGq*X#8*y@<;vsOFqEjTQXFEotR+{3}ODDnj;o0@!bB5x=N z394FojuGOtVKBlVRLtHp%EJv_G5q=AgF)SKyRN5=cGBjDWv4LDn$IL`*=~J7u&Dy5 zrMc83y+w^F&{?X(KOOAl-sWZDb{9X9#jrQtmrEXD?;h-}SYT7yM(X_6qksM=K_a;Z z3u0qT0TtaNvDER_8x*rxXw&C^|h{P1qxK|@pS7vdlZ#P z7PdB7MmC2}%sdzAxt>;WM1s0??`1983O4nFK|hVAbHcZ3x{PzytQLkCVk7hA!Lo` zEJH?4qw|}WH{dc4z%aB=0XqsFW?^p=X}4xnCJXK%c#ItOSjdSO`UXJyuc8bh^Cf}8 z@Ht|vXd^6{Fgai8*tmyRGmD_s_nv~r^Fy7j`Bu`6=G)5H$i7Q7lvQnmea&TGvJp9a|qOrUymZ$6G|Ly z#zOCg++$3iB$!6!>215A4!iryregKuUT344X)jQb3|9qY>c0LO{6Vby05n~VFzd?q zgGZv&FGlkiH*`fTurp>B8v&nSxNz)=5IF$=@rgND4d`!AaaX;_lK~)-U8la_Wa8i?NJC@BURO*sUW)E9oyv3RG^YGfN%BmxzjlT)bp*$<| zX3tt?EAy<&K+bhIuMs-g#=d1}N_?isY)6Ay$mDOKRh z4v1asEGWoAp=srraLW^h&_Uw|6O+r;wns=uwYm=JN4Q!quD8SQRSeEcGh|Eb5Jg8m zOT}u;N|x@aq)=&;wufCc^#)5U^VcZw;d_wwaoh9$p@Xrc{DD6GZUqZ ziC6OT^zSq@-lhbgR8B+e;7_Giv;DK5gn^$bs<6~SUadiosfewWDJu`XsBfOd1|p=q zE>m=zF}!lObA%ePey~gqU8S6h-^J2Y?>7)L2+%8kV}Gp=h`Xm_}rlm)SyUS=`=S7msKu zC|T!gPiI1rWGb1z$Md?0YJQ;%>uPLOXf1Z>N~`~JHJ!^@D5kSXQ4ugnFZ>^`zH8CAiZmp z6Ms|#2gcGsQ{{u7+Nb9sA?U>(0e$5V1|WVwY`Kn)rsnnZ4=1u=7u!4WexZD^IQ1Jk zfF#NLe>W$3m&C^ULjdw+5|)-BSHwpegdyt9NYC{3@QtMfd8GrIWDu`gd0nv-3LpGCh@wgBaG z176tikL!_NXM+Bv#7q^cyn9$XSeZR6#!B4JE@GVH zoobHZN_*RF#@_SVYKkQ_igme-Y5U}cV(hkR#k1c{bQNMji zU7aE`?dHyx=1`kOYZo_8U7?3-7vHOp`Qe%Z*i+FX!s?6huNp0iCEW-Z7E&jRWmUW_ z67j>)Ew!yq)hhG4o?^z}HWH-e=es#xJUhDRc4B51M4~E-l5VZ!&zQq`gWe`?}#b~7w1LH4Xa-UCT5LXkXQWheBa2YJYbyQ zl1pXR%b(KCXMO0OsXgl0P0Og<{(@&z1aokU-Pq`eQq*JYgt8xdFQ6S z6Z3IFSua8W&M#`~*L#r>Jfd6*BzJ?JFdBR#bDv$_0N!_5vnmo@!>vULcDm`MFU823 zpG9pqjqz^FE5zMDoGqhs5OMmC{Y3iVcl>F}5Rs24Y5B^mYQ;1T&ks@pIApHOdrzXF z-SdX}Hf{X;TaSxG_T$0~#RhqKISGKNK47}0*x&nRIPtmdwxc&QT3$8&!3fWu1eZ_P zJveQj^hJL#Sn!*4k`3}(d(aasl&7G0j0-*_2xtAnoX1@9+h zO#c>YQg60Z;o{Bi=3i7S`Ic+ZE>K{(u|#)9y}q*j8uKQ1^>+(BI}m%1v3$=4ojGBc zm+o1*!T&b}-lVvZqIUBc8V}QyFEgm#oyIuC{8WqUNV{Toz`oxhYpP!_p2oHHh5P@iB*NVo~2=GQm+8Yrkm2Xjc_VyHg1c0>+o~@>*Qzo zHVBJS>$$}$_4EniTI;b1WShX<5-p#TPB&!;lP!lBVBbLOOxh6FuYloD%m;n{r|;MU3!q4AVkua~fieeWu2 zQAQ$ue(IklX6+V;F1vCu-&V?I3d42FgWgsb_e^29ol}HYft?{SLf>DrmOp9o!t>I^ zY7fBCk+E8n_|apgM|-;^=#B?6RnFKlN`oR)`e$+;D=yO-(U^jV;rft^G_zl`n7qnM zL z*-Y4Phq+ZI1$j$F-f;`CD#|`-T~OM5Q>x}a>B~Gb3-+9i>Lfr|Ca6S^8g*{*?_5!x zH_N!SoRP=gX1?)q%>QTY!r77e2j9W(I!uAz{T`NdNmPBBUzi2{`XMB^zJGGwFWeA9 z{fk33#*9SO0)DjROug+(M)I-pKA!CX;IY(#gE!UxXVsa)X!UftIN98{pt#4MJHOhY zM$_l}-TJlxY?LS6Nuz1T<44m<4i^8k@D$zuCPrkmz@sdv+{ciyFJG2Zwy&%c7;atIeTdh!a(R^QXnu1Oq1b42*OQFWnyQ zWeQrdvP|w_idy53Wa<{QH^lFmEd+VlJkyiC>6B#s)F;w-{c;aKIm;Kp50HnA-o3lY z9B~F$gJ@yYE#g#X&3ADx&tO+P_@mnQTz9gv30_sTsaGXkfNYXY{$(>*PEN3QL>I!k zp)KibPhrfX3%Z$H6SY`rXGYS~143wZrG2;=FLj50+VM6soI~up_>fU(2Wl@{BRsMi zO%sL3x?2l1cXTF)k&moNsHfQrQ+wu(gBt{sk#CU=UhrvJIncy@tJX5klLjgMn>~h= zg|FR&;@eh|C7`>s_9c~0-{IAPV){l|Ts`i=)AW;d9&KPc3fMeoTS%8@V~D8*h;&(^>yjT84MM}=%#LS7shLAuuj(0VAYoozhWjq z4LEr?wUe2^WGwdTIgWBkDUJa>YP@5d9^Rs$kCXmMRxuF*YMVrn?0NFyPl}>`&dqZb z<5eqR=ZG3>n2{6v6BvJ`YBZeeTtB88TAY(x0a58EWyuf>+^|x8Qa6wA|1Nb_p|nA zWWa}|z8a)--Wj`LqyFk_a3gN2>5{Rl_wbW?#by7&i*^hRknK%jwIH6=dQ8*-_{*x0j^DUfMX0`|K@6C<|1cgZ~D(e5vBFFm;HTZF(!vT8=T$K+|F)x3kqzBV4-=p1V(lzi(s7jdu0>LD#N=$Lk#3HkG!a zIF<7>%B7sRNzJ66KrFV76J<2bdYhxll0y2^_rdG=I%AgW4~)1Nvz=$1UkE^J%BxLo z+lUci`UcU062os*=`-j4IfSQA{w@y|3}Vk?i;&SSdh8n+$iHA#%ERL{;EpXl6u&8@ zzg}?hkEOUOJt?ZL=pWZFJ19mI1@P=$U5*Im1e_8Z${JsM>Ov?nh8Z zP5QvI!{Jy@&BP48%P2{Jr_VgzW;P@7)M9n|lDT|Ep#}7C$&ud&6>C^5ZiwKIg2McPU(4jhM!BD@@L(Gd*Nu$ji(ljZ<{FIeW_1Mmf;76{LU z-ywN~=uNN)Xi6$<12A9y)K%X|(W0p|&>>4OXB?IiYr||WKDOJPxiSe01NSV-h24^L z_>m$;|C+q!Mj**-qQ$L-*++en(g|hw;M!^%_h-iDjFHLo-n3JpB;p?+o2;`*jpvJU zLY^lt)Un4joij^^)O(CKs@7E%*!w>!HA4Q?0}oBJ7Nr8NQ7QmY^4~jvf0-`%waOLn zdNjAPaC0_7c|RVhw)+71NWjRi!y>C+Bl;Z`NiL^zn2*0kmj5gyhCLCxts*cWCdRI| zjsd=sT5BVJc^$GxP~YF$-U{-?kW6r@^vHXB%{CqYzU@1>dzf#3SYedJG-Rm6^RB7s zGM5PR(yKPKR)>?~vpUIeTP7A1sc8-knnJk*9)3t^e%izbdm>Y=W{$wm(cy1RB-19i za#828DMBY+ps#7Y8^6t)=Ea@%Nkt)O6JCx|ybC;Ap}Z@Zw~*}3P>MZLPb4Enxz9Wf zssobT^(R@KuShj8>@!1M7tm|2%-pYYDxz-5`rCbaTCG5{;Uxm z*g=+H1X8{NUvFGzz~wXa%Eo};I;~`37*WrRU&K0dPSB$yk(Z*@K&+mFal^?c zurbqB-+|Kb5|sznT;?Pj!+kgFY1#Dr;_%A(GIQC{3ct|{*Bji%FNa6c-thbpBkA;U zURV!Dr&X{0J}iht#-Qp2=xzuh(fM>zRoiGrYl5ttw2#r34gC41CCOC31m~^UPTK@s z6;A@)7O7_%C)>bnAXerYuAHdE93>j2N}H${zEc6&SbZ|-fiG*-qtGuy-qDelH(|u$ zorf8_T6Zqe#Ub!+e3oSyrskt_HyW_^5lrWt#30l)tHk|j$@YyEkXUOV;6B51L;M@=NIWZXU;GrAa(LGxO%|im%7F<-6N;en0Cr zLH>l*y?pMwt`1*cH~LdBPFY_l;~`N!Clyfr;7w<^X;&(ZiVdF1S5e(+Q%60zgh)s4 zn2yj$+mE=miVERP(g8}G4<85^-5f@qxh2ec?n+$A_`?qN=iyT1?U@t?V6DM~BIlBB z>u~eXm-aE>R0sQy!-I4xtCNi!!qh?R1!kKf6BoH2GG{L4%PAz0{Sh6xpuyI%*~u)s z%rLuFl)uQUCBQAtMyN;%)zFMx4loh7uTfKeB2Xif`lN?2gq6NhWhfz0u5WP9J>=V2 zo{mLtSy&BA!mSzs&CrKWq^y40JF5a&GSXIi2= z{EYb59J4}VwikL4P=>+mc6{($FNE@e=VUwG+KV21;<@lrN`mnz5jYGASyvz7BOG_6(p^eTxD-4O#lROgon;R35=|nj#eHIfJBYPWG>H>`dHKCDZ3`R{-?HO0mE~(5_WYcFmp8sU?wr*UkAQiNDGc6T zA%}GOLXlOWqL?WwfHO8MB#8M8*~Y*gz;1rWWoVSXP&IbKxbQ8+s%4Jnt?kDsq7btI zCDr0PZ)b;B%!lu&CT#RJzm{l{2fq|BcY85`w~3LSK<><@(2EdzFLt9Y_`;WXL6x`0 zDoQ?=?I@Hbr;*VVll1Gmd8*%tiXggMK81a+T(5Gx6;eNb8=uYn z5BG-0g>pP21NPn>$ntBh>`*})Fl|38oC^9Qz>~MAazH%3Q~Qb!ALMf$srexgPZ2@&c~+hxRi1;}+)-06)!#Mq<6GhP z-Q?qmgo${aFBApb5p}$1OJKTClfi8%PpnczyVKkoHw7Ml9e7ikrF0d~UB}i3vizos zXW4DN$SiEV9{faLt5bHy2a>33K%7Td-n5C*N;f&ZqAg#2hIqEb(y<&f4u5BWJ>2^4 z414GosL=Aom#m&=x_v<0-fp1r%oVJ{T-(xnomNJ(Dryv zh?vj+%=II_nV+@NR+(!fZZVM&(W6{6%9cm+o+Z6}KqzLw{(>E86uA1`_K$HqINlb1 zKelh3-jr2I9V?ych`{hta9wQ2c9=MM`2cC{m6^MhlL2{DLv7C^j z$xXBCnDl_;l|bPGMX@*tV)B!c|4oZyftUlP*?$YU9C_eAsuVHJ58?)zpbr30P*C`T z7y#ao`uE-SOG(Pi+`$=e^mle~)pRrdwL5)N;o{gpW21of(QE#U6w%*C~`v-z0QqBML!!5EeYA5IQB0 z^l01c;L6E(iytN!LhL}wfwP7W9PNAkb+)Cst?qg#$n;z41O4&v+8-zPs+XNb-q zIeeBCh#ivnFLUCwfS;p{LC0O7tm+Sf9Jn)~b%uwP{%69;QC)Ok0t%*a5M+=;y8j=v z#!*pp$9@!x;UMIs4~hP#pnfVc!%-D<+wsG@R2+J&%73lK|2G!EQC)O05TCV=&3g)C!lT=czLpZ@Sa%TYuoE?v8T8`V;e$#Zf2_Nj6nvBgh1)2 GZ~q4|mN%#X literal 61608 zcmb5VV{~QRw)Y#`wrv{~+qP{x72B%VwzFc}c2cp;N~)5ZbDrJayPv(!dGEd-##*zr z)#n-$y^sH|_dchh3@8{H5D*j;5D<{i*8l5IFJ|DjL!e)upfGNX(kojugZ3I`oH1PvW`wFW_ske0j@lB9bX zO;2)`y+|!@X(fZ1<2n!Qx*)_^Ai@Cv-dF&(vnudG?0CsddG_&Wtae(n|K59ew)6St z#dj7_(Cfwzh$H$5M!$UDd8=4>IQsD3xV=lXUq($;(h*$0^yd+b{qq63f0r_de#!o_ zXDngc>zy`uor)4A^2M#U*DC~i+dc<)Tb1Tv&~Ev@oM)5iJ4Sn#8iRw16XXuV50BS7 zdBL5Mefch(&^{luE{*5qtCZk$oFr3RH=H!c3wGR=HJ(yKc_re_X9pD` zJ;uxPzUfVpgU>DSq?J;I@a+10l0ONXPcDkiYcihREt5~T5Gb}sT0+6Q;AWHl`S5dV>lv%-p9l#xNNy7ZCr%cyqHY%TZ8Q4 zbp&#ov1*$#grNG#1vgfFOLJCaNG@K|2!W&HSh@3@Y%T?3YI75bJp!VP*$*!< z;(ffNS_;@RJ`=c7yX04!u3JP*<8jeqLHVJu#WV&v6wA!OYJS4h<_}^QI&97-;=ojW zQ-1t)7wnxG*5I%U4)9$wlv5Fr;cIizft@&N+32O%B{R1POm$oap@&f| zh+5J{>U6ftv|vAeKGc|zC=kO(+l7_cLpV}-D#oUltScw})N>~JOZLU_0{Ka2e1evz z{^a*ZrLr+JUj;)K&u2CoCAXLC2=fVScI(m_p~0FmF>>&3DHziouln?;sxW`NB}cSX z8?IsJB)Z=aYRz!X=yJn$kyOWK%rCYf-YarNqKzmWu$ZvkP12b4qH zhS9Q>j<}(*frr?z<%9hl*i^#@*O2q(Z^CN)c2c z>1B~D;@YpG?G!Yk+*yn4vM4sO-_!&m6+`k|3zd;8DJnxsBYtI;W3We+FN@|tQ5EW= z!VU>jtim0Mw#iaT8t_<+qKIEB-WwE04lBd%Letbml9N!?SLrEG$nmn7&W(W`VB@5S zaY=sEw2}i@F_1P4OtEw?xj4@D6>_e=m=797#hg}f*l^`AB|Y0# z9=)o|%TZFCY$SzgSjS|8AI-%J4x}J)!IMxY3_KYze`_I=c1nmrk@E8c9?MVRu)7+Ue79|)rBX7tVB7U|w4*h(;Gi3D9le49B38`wuv zp7{4X^p+K4*$@gU(Tq3K1a#3SmYhvI42)GzG4f|u zwQFT1n_=n|jpi=70-yE9LA+d*T8u z`=VmmXJ_f6WmZveZPct$Cgu^~gFiyL>Lnpj*6ee>*0pz=t$IJ}+rE zsf@>jlcG%Wx;Cp5x)YSVvB1$yyY1l&o zvwX=D7k)Dn;ciX?Z)Pn8$flC8#m`nB&(8?RSdBvr?>T9?E$U3uIX7T?$v4dWCa46 z+&`ot8ZTEgp7G+c52oHJ8nw5}a^dwb_l%MOh(ebVj9>_koQP^$2B~eUfSbw9RY$_< z&DDWf2LW;b0ZDOaZ&2^i^g+5uTd;GwO(-bbo|P^;CNL-%?9mRmxEw~5&z=X^Rvbo^WJW=n_%*7974RY}JhFv46> zd}`2|qkd;89l}R;i~9T)V-Q%K)O=yfVKNM4Gbacc7AOd>#^&W&)Xx!Uy5!BHnp9kh z`a(7MO6+Ren#>R^D0K)1sE{Bv>}s6Rb9MT14u!(NpZOe-?4V=>qZ>}uS)!y~;jEUK z&!U7Fj&{WdgU#L0%bM}SYXRtM5z!6M+kgaMKt%3FkjWYh=#QUpt$XX1!*XkpSq-pl zhMe{muh#knk{9_V3%qdDcWDv}v)m4t9 zQhv{;} zc{}#V^N3H>9mFM8`i`0p+fN@GqX+kl|M94$BK3J-X`Hyj8r!#x6Vt(PXjn?N)qedP z=o1T^#?1^a{;bZ&x`U{f?}TMo8ToN zkHj5v|}r}wDEi7I@)Gj+S1aE-GdnLN+$hw!=DzglMaj#{qjXi_dwpr|HL(gcCXwGLEmi|{4&4#OZ4ChceA zKVd4K!D>_N=_X;{poT~4Q+!Le+ZV>=H7v1*l%w`|`Dx8{)McN@NDlQyln&N3@bFpV z_1w~O4EH3fF@IzJ9kDk@7@QctFq8FbkbaH7K$iX=bV~o#gfh?2JD6lZf(XP>~DACF)fGFt)X%-h1yY~MJU{nA5 ze2zxWMs{YdX3q5XU*9hOH0!_S24DOBA5usB+Ws$6{|AMe*joJ?RxfV}*7AKN9V*~J zK+OMcE@bTD>TG1*yc?*qGqjBN8mgg@h1cJLDv)0!WRPIkC` zZrWXrceVw;fB%3`6kq=a!pq|hFIsQ%ZSlo~)D z|64!aCnw-?>}AG|*iOl44KVf8@|joXi&|)1rB;EQWgm+iHfVbgllP$f!$Wf42%NO5b(j9Bw6L z;0dpUUK$5GX4QbMlTmLM_jJt!ur`_0~$b#BB7FL*%XFf<b__1o)Ao3rlobbN8-(T!1d-bR8D3S0@d zLI!*GMb5s~Q<&sjd}lBb8Nr0>PqE6_!3!2d(KAWFxa{hm`@u|a(%#i(#f8{BP2wbs zt+N_slWF4IF_O|{w`c~)Xvh&R{Au~CFmW#0+}MBd2~X}t9lz6*E7uAD`@EBDe$>7W zzPUkJx<`f$0VA$=>R57^(K^h86>09?>_@M(R4q($!Ck6GG@pnu-x*exAx1jOv|>KH zjNfG5pwm`E-=ydcb+3BJwuU;V&OS=6yM^4Jq{%AVqnTTLwV`AorIDD}T&jWr8pB&j28fVtk_y*JRP^t@l*($UZ z6(B^-PBNZ+z!p?+e8@$&jCv^EWLb$WO=}Scr$6SM*&~B95El~;W_0(Bvoha|uQ1T< zO$%_oLAwf1bW*rKWmlD+@CP&$ObiDy=nh1b2ejz%LO9937N{LDe7gle4i!{}I$;&Y zkexJ9Ybr+lrCmKWg&}p=`2&Gf10orS?4$VrzWidT=*6{KzOGMo?KI0>GL0{iFWc;C z+LPq%VH5g}6V@-tg2m{C!-$fapJ9y}c$U}aUmS{9#0CM*8pC|sfer!)nG7Ji>mfRh z+~6CxNb>6eWKMHBz-w2{mLLwdA7dA-qfTu^A2yG1+9s5k zcF=le_UPYG&q!t5Zd_*E_P3Cf5T6821bO`daa`;DODm8Ih8k89=RN;-asHIigj`n=ux>*f!OC5#;X5i;Q z+V!GUy0|&Y_*8k_QRUA8$lHP;GJ3UUD08P|ALknng|YY13)}!!HW@0z$q+kCH%xet zlWf@BXQ=b=4}QO5eNnN~CzWBbHGUivG=`&eWK}beuV*;?zt=P#pM*eTuy3 zP}c#}AXJ0OIaqXji78l;YrP4sQe#^pOqwZUiiN6^0RCd#D271XCbEKpk`HI0IsN^s zES7YtU#7=8gTn#lkrc~6)R9u&SX6*Jk4GFX7){E)WE?pT8a-%6P+zS6o&A#ml{$WX zABFz#i7`DDlo{34)oo?bOa4Z_lNH>n;f0nbt$JfAl~;4QY@}NH!X|A$KgMmEsd^&Y zt;pi=>AID7ROQfr;MsMtClr5b0)xo|fwhc=qk33wQ|}$@?{}qXcmECh>#kUQ-If0$ zseb{Wf4VFGLNc*Rax#P8ko*=`MwaR-DQ8L8V8r=2N{Gaips2_^cS|oC$+yScRo*uF zUO|5=?Q?{p$inDpx*t#Xyo6=s?bbN}y>NNVxj9NZCdtwRI70jxvm3!5R7yiWjREEd zDUjrsZhS|P&|Ng5r+f^kA6BNN#|Se}_GF>P6sy^e8kBrgMv3#vk%m}9PCwUWJg-AD zFnZ=}lbi*mN-AOm zCs)r=*YQAA!`e#1N>aHF=bb*z*hXH#Wl$z^o}x##ZrUc=kh%OHWhp=7;?8%Xj||@V?1c ziWoaC$^&04;A|T)!Zd9sUzE&$ODyJaBpvqsw19Uiuq{i#VK1!htkdRWBnb z`{rat=nHArT%^R>u#CjjCkw-7%g53|&7z-;X+ewb?OLWiV|#nuc8mp*LuGSi3IP<<*Wyo9GKV7l0Noa4Jr0g3p_$ z*R9{qn=?IXC#WU>48-k5V2Oc_>P;4_)J@bo1|pf=%Rcbgk=5m)CJZ`caHBTm3%!Z9 z_?7LHr_BXbKKr=JD!%?KhwdYSdu8XxPoA{n8^%_lh5cjRHuCY9Zlpz8g+$f@bw@0V z+6DRMT9c|>1^3D|$Vzc(C?M~iZurGH2pXPT%F!JSaAMdO%!5o0uc&iqHx?ImcX6fI zCApkzc~OOnfzAd_+-DcMp&AOQxE_EsMqKM{%dRMI5`5CT&%mQO?-@F6tE*xL?aEGZ z8^wH@wRl`Izx4sDmU>}Ym{ybUm@F83qqZPD6nFm?t?(7>h*?`fw)L3t*l%*iw0Qu#?$5eq!Qc zpQvqgSxrd83NsdO@lL6#{%lsYXWen~d3p4fGBb7&5xqNYJ)yn84!e1PmPo7ChVd%4 zHUsV0Mh?VpzZD=A6%)Qrd~i7 z96*RPbid;BN{Wh?adeD_p8YU``kOrGkNox3D9~!K?w>#kFz!4lzOWR}puS(DmfjJD z`x0z|qB33*^0mZdM&6$|+T>fq>M%yoy(BEjuh9L0>{P&XJ3enGpoQRx`v6$txXt#c z0#N?b5%srj(4xmPvJxrlF3H%OMB!jvfy z;wx8RzU~lb?h_}@V=bh6p8PSb-dG|-T#A?`c&H2`_!u+uenIZe`6f~A7r)`9m8atC zt(b|6Eg#!Q*DfRU=Ix`#B_dK)nnJ_+>Q<1d7W)eynaVn`FNuN~%B;uO2}vXr5^zi2 z!ifIF5@Zlo0^h~8+ixFBGqtweFc`C~JkSq}&*a3C}L?b5Mh-bW=e)({F_g4O3 zb@SFTK3VD9QuFgFnK4Ve_pXc3{S$=+Z;;4+;*{H}Rc;845rP?DLK6G5Y-xdUKkA6E3Dz&5f{F^FjJQ(NSpZ8q-_!L3LL@H* zxbDF{gd^U3uD;)a)sJwAVi}7@%pRM&?5IaUH%+m{E)DlA_$IA1=&jr{KrhD5q&lTC zAa3c)A(K!{#nOvenH6XrR-y>*4M#DpTTOGQEO5Jr6kni9pDW`rvY*fs|ItV;CVITh z=`rxcH2nEJpkQ^(;1c^hfb8vGN;{{oR=qNyKtR1;J>CByul*+=`NydWnSWJR#I2lN zTvgnR|MBx*XFsfdA&;tr^dYaqRZp*2NwkAZE6kV@1f{76e56eUmGrZ>MDId)oqSWw z7d&r3qfazg+W2?bT}F)4jD6sWaw`_fXZGY&wnGm$FRPFL$HzVTH^MYBHWGCOk-89y zA+n+Q6EVSSCpgC~%uHfvyg@ufE^#u?JH?<73A}jj5iILz4Qqk5$+^U(SX(-qv5agK znUkfpke(KDn~dU0>gdKqjTkVk`0`9^0n_wzXO7R!0Thd@S;U`y)VVP&mOd-2 z(hT(|$=>4FY;CBY9#_lB$;|Wd$aOMT5O_3}DYXEHn&Jrc3`2JiB`b6X@EUOD zVl0S{ijm65@n^19T3l%>*;F(?3r3s?zY{thc4%AD30CeL_4{8x6&cN}zN3fE+x<9; zt2j1RRVy5j22-8U8a6$pyT+<`f+x2l$fd_{qEp_bfxfzu>ORJsXaJn4>U6oNJ#|~p z`*ZC&NPXl&=vq2{Ne79AkQncuxvbOG+28*2wU$R=GOmns3W@HE%^r)Fu%Utj=r9t` zd;SVOnA(=MXgnOzI2@3SGKHz8HN~Vpx&!Ea+Df~`*n@8O=0!b4m?7cE^K*~@fqv9q zF*uk#1@6Re_<^9eElgJD!nTA@K9C732tV~;B`hzZ321Ph=^BH?zXddiu{Du5*IPg} zqDM=QxjT!Rp|#Bkp$(mL)aar)f(dOAXUiw81pX0DC|Y4;>Vz>>DMshoips^8Frdv} zlTD=cKa48M>dR<>(YlLPOW%rokJZNF2gp8fwc8b2sN+i6&-pHr?$rj|uFgktK@jg~ zIFS(%=r|QJ=$kvm_~@n=ai1lA{7Z}i+zj&yzY+!t$iGUy|9jH#&oTNJ;JW-3n>DF+ z3aCOzqn|$X-Olu_p7brzn`uk1F*N4@=b=m;S_C?#hy{&NE#3HkATrg?enaVGT^$qIjvgc61y!T$9<1B@?_ibtDZ{G zeXInVr5?OD_nS_O|CK3|RzzMmu+8!#Zb8Ik;rkIAR%6?$pN@d<0dKD2c@k2quB%s( zQL^<_EM6ow8F6^wJN1QcPOm|ehA+dP(!>IX=Euz5qqIq}Y3;ibQtJnkDmZ8c8=Cf3 zu`mJ!Q6wI7EblC5RvP*@)j?}W=WxwCvF3*5Up_`3*a~z$`wHwCy)2risye=1mSp%p zu+tD6NAK3o@)4VBsM!@);qgsjgB$kkCZhaimHg&+k69~drbvRTacWKH;YCK(!rC?8 zP#cK5JPHSw;V;{Yji=55X~S+)%(8fuz}O>*F3)hR;STU`z6T1aM#Wd+FP(M5*@T1P z^06O;I20Sk!bxW<-O;E081KRdHZrtsGJflFRRFS zdi5w9OVDGSL3 zNrC7GVsGN=b;YH9jp8Z2$^!K@h=r-xV(aEH@#JicPy;A0k1>g1g^XeR`YV2HfmqXY zYbRwaxHvf}OlCAwHoVI&QBLr5R|THf?nAevV-=~V8;gCsX>jndvNOcFA+DI+zbh~# zZ7`qNk&w+_+Yp!}j;OYxIfx_{f0-ONc?mHCiCUak=>j>~>YR4#w# zuKz~UhT!L~GfW^CPqG8Lg)&Rc6y^{%3H7iLa%^l}cw_8UuG;8nn9)kbPGXS}p3!L_ zd#9~5CrH8xtUd?{d2y^PJg+z(xIfRU;`}^=OlehGN2=?}9yH$4Rag}*+AWotyxfCJ zHx=r7ZH>j2kV?%7WTtp+-HMa0)_*DBBmC{sd$)np&GEJ__kEd`xB5a2A z*J+yx>4o#ZxwA{;NjhU*1KT~=ZK~GAA;KZHDyBNTaWQ1+;tOFFthnD)DrCn`DjBZ% zk$N5B4^$`n^jNSOr=t(zi8TN4fpaccsb`zOPD~iY=UEK$0Y70bG{idLx@IL)7^(pL z{??Bnu=lDeguDrd%qW1)H)H`9otsOL-f4bSu};o9OXybo6J!Lek`a4ff>*O)BDT_g z<6@SrI|C9klY(>_PfA^qai7A_)VNE4c^ZjFcE$Isp>`e5fLc)rg@8Q_d^Uk24$2bn z9#}6kZ2ZxS9sI(RqT7?El2@B+($>eBQrNi_k#CDJ8D9}8$mmm z4oSKO^F$i+NG)-HE$O6s1--6EzJa?C{x=QgK&c=)b(Q9OVoAXYEEH20G|q$}Hue%~ zO3B^bF=t7t48sN zWh_zA`w~|){-!^g?6Mqf6ieV zFx~aPUOJGR=4{KsW7I?<=J2|lY`NTU=lt=%JE9H1vBpkcn=uq(q~=?iBt_-r(PLBM zP-0dxljJO>4Wq-;stY)CLB4q`-r*T$!K2o}?E-w_i>3_aEbA^MB7P5piwt1dI-6o!qWCy0 ztYy!x9arGTS?kabkkyv*yxvsPQ7Vx)twkS6z2T@kZ|kb8yjm+^$|sEBmvACeqbz)RmxkkDQX-A*K!YFziuhwb|ym>C$}U|J)4y z$(z#)GH%uV6{ec%Zy~AhK|+GtG8u@c884Nq%w`O^wv2#A(&xH@c5M`Vjk*SR_tJnq z0trB#aY)!EKW_}{#L3lph5ow=@|D5LzJYUFD6 z7XnUeo_V0DVSIKMFD_T0AqAO|#VFDc7c?c-Q%#u00F%!_TW1@JVnsfvm@_9HKWflBOUD~)RL``-!P;(bCON_4eVdduMO>?IrQ__*zE@7(OX zUtfH@AX*53&xJW*Pu9zcqxGiM>xol0I~QL5B%Toog3Jlenc^WbVgeBvV8C8AX^Vj& z^I}H})B=VboO%q1;aU5ACMh{yK4J;xlMc`jCnZR^!~LDs_MP&8;dd@4LDWw~*>#OT zeZHwdQWS!tt5MJQI~cw|Ka^b4c|qyd_ly(+Ql2m&AAw^ zQeSXDOOH!!mAgzAp0z)DD>6Xo``b6QwzUV@w%h}Yo>)a|xRi$jGuHQhJVA%>)PUvK zBQ!l0hq<3VZ*RnrDODP)>&iS^wf64C;MGqDvx>|p;35%6(u+IHoNbK z;Gb;TneFo*`zUKS6kwF*&b!U8e5m4YAo03a_e^!5BP42+r)LFhEy?_7U1IR<; z^0v|DhCYMSj<-;MtY%R@Fg;9Kky^pz_t2nJfKWfh5Eu@_l{^ph%1z{jkg5jQrkvD< z#vdK!nku*RrH~TdN~`wDs;d>XY1PH?O<4^U4lmA|wUW{Crrv#r%N>7k#{Gc44Fr|t z@UZP}Y-TrAmnEZ39A*@6;ccsR>)$A)S>$-Cj!=x$rz7IvjHIPM(TB+JFf{ehuIvY$ zsDAwREg*%|=>Hw$`us~RP&3{QJg%}RjJKS^mC_!U;E5u>`X`jW$}P`Mf}?7G7FX#{ zE(9u1SO;3q@ZhDL9O({-RD+SqqPX)`0l5IQu4q)49TUTkxR(czeT}4`WV~pV*KY&i zAl3~X%D2cPVD^B43*~&f%+Op)wl<&|D{;=SZwImydWL6@_RJjxP2g)s=dH)u9Npki zs~z9A+3fj0l?yu4N0^4aC5x)Osnm0qrhz@?nwG_`h(71P znbIewljU%T*cC=~NJy|)#hT+lx#^5MuDDnkaMb*Efw9eThXo|*WOQzJ*#3dmRWm@! zfuSc@#kY{Um^gBc^_Xdxnl!n&y&}R4yAbK&RMc+P^Ti;YIUh|C+K1|=Z^{nZ}}rxH*v{xR!i%qO~o zTr`WDE@k$M9o0r4YUFFeQO7xCu_Zgy)==;fCJ94M_rLAv&~NhfvcLWCoaGg2ao~3e zBG?Ms9B+efMkp}7BhmISGWmJsKI@a8b}4lLI48oWKY|8?zuuNc$lt5Npr+p7a#sWu zh!@2nnLBVJK!$S~>r2-pN||^w|fY`CT{TFnJy`B|e5;=+_v4l8O-fkN&UQbA4NKTyntd zqK{xEKh}U{NHoQUf!M=2(&w+eef77VtYr;xs%^cPfKLObyOV_9q<(%76-J%vR>w9!us-0c-~Y?_EVS%v!* z15s2s3eTs$Osz$JayyH|5nPAIPEX=U;r&p;K14G<1)bvn@?bM5kC{am|C5%hyxv}a z(DeSKI5ZfZ1*%dl8frIX2?);R^^~LuDOpNpk-2R8U1w92HmG1m&|j&J{EK=|p$;f9 z7Rs5|jr4r8k5El&qcuM+YRlKny%t+1CgqEWO>3;BSRZi(LA3U%Jm{@{y+A+w(gzA< z7dBq6a1sEWa4cD0W7=Ld9z0H7RI^Z7vl(bfA;72j?SWCo`#5mVC$l1Q2--%V)-uN* z9ha*s-AdfbDZ8R8*fpwjzx=WvOtmSzGFjC#X)hD%Caeo^OWjS(3h|d9_*U)l%{Ab8 zfv$yoP{OuUl@$(-sEVNt{*=qi5P=lpxWVuz2?I7Dc%BRc+NGNw+323^ z5BXGfS71oP^%apUo(Y#xkxE)y?>BFzEBZ}UBbr~R4$%b7h3iZu3S(|A;&HqBR{nK& z$;GApNnz=kNO^FL&nYcfpB7Qg;hGJPsCW44CbkG1@l9pn0`~oKy5S777uH)l{irK!ru|X+;4&0D;VE*Ii|<3P zUx#xUqvZT5kVQxsF#~MwKnv7;1pR^0;PW@$@T7I?s`_rD1EGUdSA5Q(C<>5SzE!vw z;{L&kKFM-MO>hy#-8z`sdVx})^(Dc-dw;k-h*9O2_YZw}|9^y-|8RQ`BWJUJL(Cer zP5Z@fNc>pTXABbTRY-B5*MphpZv6#i802giwV&SkFCR zGMETyUm(KJbh+&$8X*RB#+{surjr;8^REEt`2&Dubw3$mx>|~B5IKZJ`s_6fw zKAZx9&PwBqW1Oz0r0A4GtnZd7XTKViX2%kPfv+^X3|_}RrQ2e3l=KG_VyY`H?I5&CS+lAX5HbA%TD9u6&s#v!G> zzW9n4J%d5ye7x0y`*{KZvqyXUfMEE^ZIffzI=Hh|3J}^yx7eL=s+TPH(Q2GT-sJ~3 zI463C{(ag7-hS1ETtU;_&+49ABt5!A7CwLwe z=SoA8mYZIQeU;9txI=zcQVbuO%q@E)JI+6Q!3lMc=Gbj(ASg-{V27u>z2e8n;Nc*pf}AqKz1D>p9G#QA+7mqqrEjGfw+85Uyh!=tTFTv3|O z+)-kFe_8FF_EkTw!YzwK^Hi^_dV5x-Ob*UWmD-})qKj9@aE8g240nUh=g|j28^?v7 zHRTBo{0KGaWBbyX2+lx$wgXW{3aUab6Bhm1G1{jTC7ota*JM6t+qy)c5<@ zpc&(jVdTJf(q3xB=JotgF$X>cxh7k*(T`-V~AR+`%e?YOeALQ2Qud( zz35YizXt(aW3qndR}fTw1p()Ol4t!D1pitGNL95{SX4ywzh0SF;=!wf=?Q?_h6!f* zh7<+GFi)q|XBsvXZ^qVCY$LUa{5?!CgwY?EG;*)0ceFe&=A;!~o`ae}Z+6me#^sv- z1F6=WNd6>M(~ z+092z>?Clrcp)lYNQl9jN-JF6n&Y0mp7|I0dpPx+4*RRK+VQI~>en0Dc;Zfl+x z_e_b7s`t1_A`RP3$H}y7F9_na%D7EM+**G_Z0l_nwE+&d_kc35n$Fxkd4r=ltRZhh zr9zER8>j(EdV&Jgh(+i}ltESBK62m0nGH6tCBr90!4)-`HeBmz54p~QP#dsu%nb~W z7sS|(Iydi>C@6ZM(Us!jyIiszMkd)^u<1D+R@~O>HqZIW&kearPWmT>63%_t2B{_G zX{&a(gOYJx!Hq=!T$RZ&<8LDnxsmx9+TBL0gTk$|vz9O5GkK_Yx+55^R=2g!K}NJ3 zW?C;XQCHZl7H`K5^BF!Q5X2^Mj93&0l_O3Ea3!Ave|ixx+~bS@Iv18v2ctpSt4zO{ zp#7pj!AtDmti$T`e9{s^jf(ku&E|83JIJO5Qo9weT6g?@vX!{7)cNwymo1+u(YQ94 zopuz-L@|5=h8A!(g-MXgLJC0MA|CgQF8qlonnu#j z;uCeq9ny9QSD|p)9sp3ebgY3rk#y0DA(SHdh$DUm^?GI<>%e1?&}w(b zdip1;P2Z=1wM+$q=TgLP$}svd!vk+BZ@h<^4R=GS2+sri7Z*2f`9 z5_?i)xj?m#pSVchk-SR!2&uNhzEi+#5t1Z$o0PoLGz*pT64%+|Wa+rd5Z}60(j?X= z{NLjtgRb|W?CUADqOS@(*MA-l|E342NxRaxLTDqsOyfWWe%N(jjBh}G zm7WPel6jXijaTiNita+z(5GCO0NM=Melxud57PP^d_U## zbA;9iVi<@wr0DGB8=T9Ab#2K_#zi=$igyK48@;V|W`fg~7;+!q8)aCOo{HA@vpSy-4`^!ze6-~8|QE||hC{ICKllG9fbg_Y7v z$jn{00!ob3!@~-Z%!rSZ0JO#@>|3k10mLK0JRKP-Cc8UYFu>z93=Ab-r^oL2 zl`-&VBh#=-?{l1TatC;VweM^=M7-DUE>m+xO7Xi6vTEsReyLs8KJ+2GZ&rxw$d4IT zPXy6pu^4#e;;ZTsgmG+ZPx>piodegkx2n0}SM77+Y*j^~ICvp#2wj^BuqRY*&cjmL zcKp78aZt>e{3YBb4!J_2|K~A`lN=u&5j!byw`1itV(+Q_?RvV7&Z5XS1HF)L2v6ji z&kOEPmv+k_lSXb{$)of~(BkO^py&7oOzpjdG>vI1kcm_oPFHy38%D4&A4h_CSo#lX z2#oqMCTEP7UvUR3mwkPxbl8AMW(e{ARi@HCYLPSHE^L<1I}OgZD{I#YH#GKnpRmW3 z2jkz~Sa(D)f?V?$gNi?6)Y;Sm{&?~2p=0&BUl_(@hYeX8YjaRO=IqO7neK0RsSNdYjD zaw$g2sG(>JR=8Iz1SK4`*kqd_3-?;_BIcaaMd^}<@MYbYisWZm2C2|Np_l|8r9yM|JkUngSo@?wci(7&O9a z%|V(4C1c9pps0xxzPbXH=}QTxc2rr7fXk$9`a6TbWKPCz&p=VsB8^W96W=BsB|7bc zf(QR8&Ktj*iz)wK&mW`#V%4XTM&jWNnDF56O+2bo<3|NyUhQ%#OZE8$Uv2a@J>D%t zMVMiHh?es!Ex19q&6eC&L=XDU_BA&uR^^w>fpz2_`U87q_?N2y;!Z!bjoeKrzfC)} z?m^PM=(z{%n9K`p|7Bz$LuC7!>tFOuN74MFELm}OD9?%jpT>38J;=1Y-VWtZAscaI z_8jUZ#GwWz{JqvGEUmL?G#l5E=*m>`cY?m*XOc*yOCNtpuIGD+Z|kn4Xww=BLrNYS zGO=wQh}Gtr|7DGXLF%|`G>J~l{k^*{;S-Zhq|&HO7rC_r;o`gTB7)uMZ|WWIn@e0( zX$MccUMv3ABg^$%_lNrgU{EVi8O^UyGHPNRt%R!1#MQJn41aD|_93NsBQhP80yP<9 zG4(&0u7AtJJXLPcqzjv`S~5;Q|5TVGccN=Uzm}K{v)?f7W!230C<``9(64}D2raRU zAW5bp%}VEo{4Rko`bD%Ehf=0voW?-4Mk#d3_pXTF!-TyIt6U+({6OXWVAa;s-`Ta5 zTqx&8msH3+DLrVmQOTBOAj=uoxKYT3DS1^zBXM?1W+7gI!aQNPYfUl{3;PzS9*F7g zWJN8x?KjBDx^V&6iCY8o_gslO16=kh(|Gp)kz8qlQ`dzxQv;)V&t+B}wwdi~uBs4? zu~G|}y!`3;8#vIMUdyC7YEx6bb^1o}G!Jky4cN?BV9ejBfN<&!4M)L&lRKiuMS#3} z_B}Nkv+zzxhy{dYCW$oGC&J(Ty&7%=5B$sD0bkuPmj7g>|962`(Q{ZZMDv%YMuT^KweiRDvYTEop3IgFv#)(w>1 zSzH>J`q!LK)c(AK>&Ib)A{g`Fdykxqd`Yq@yB}E{gnQV$K!}RsgMGWqC3DKE(=!{}ekB3+(1?g}xF>^icEJbc z5bdxAPkW90atZT+&*7qoLqL#p=>t-(-lsnl2XMpZcYeW|o|a322&)yO_8p(&Sw{|b zn(tY$xn5yS$DD)UYS%sP?c|z>1dp!QUD)l;aW#`%qMtQJjE!s2z`+bTSZmLK7SvCR z=@I4|U^sCwZLQSfd*ACw9B@`1c1|&i^W_OD(570SDLK`MD0wTiR8|$7+%{cF&){$G zU~|$^Ed?TIxyw{1$e|D$050n8AjJvvOWhLtLHbSB|HIfjMp+gu>DraHZJRrdO53(= z+o-f{+qNog+qSLB%KY;5>Av6X(>-qYk3IIEwZ5~6a+P9lMpC^ z8CJ0q>rEpjlsxCvJm=kms@tlN4+sv}He`xkr`S}bGih4t`+#VEIt{1veE z{ZLtb_pSbcfcYPf4=T1+|BtR!x5|X#x2TZEEkUB6kslKAE;x)*0x~ES0kl4Dex4e- zT2P~|lT^vUnMp{7e4OExfxak0EE$Hcw;D$ehTV4a6hqxru0$|Mo``>*a5=1Ym0u>BDJKO|=TEWJ5jZu!W}t$Kv{1!q`4Sn7 zrxRQOt>^6}Iz@%gA3&=5r;Lp=N@WKW;>O!eGIj#J;&>+3va^~GXRHCY2}*g#9ULab zitCJt-OV0*D_Q3Q`p1_+GbPxRtV_T`jyATjax<;zZ?;S+VD}a(aN7j?4<~>BkHK7bO8_Vqfdq1#W&p~2H z&w-gJB4?;Q&pG9%8P(oOGZ#`!m>qAeE)SeL*t8KL|1oe;#+uOK6w&PqSDhw^9-&Fa zuEzbi!!7|YhlWhqmiUm!muO(F8-F7|r#5lU8d0+=;<`{$mS=AnAo4Zb^{%p}*gZL! zeE!#-zg0FWsSnablw!9$<&K(#z!XOW z;*BVx2_+H#`1b@>RtY@=KqD)63brP+`Cm$L1@ArAddNS1oP8UE$p05R=bvZoYz+^6 z<)!v7pRvi!u_-V?!d}XWQR1~0q(H3{d^4JGa=W#^Z<@TvI6J*lk!A zZ*UIKj*hyO#5akL*Bx6iPKvR3_2-^2mw|Rh-3O_SGN3V9GRo52Q;JnW{iTGqb9W99 z7_+F(Op6>~3P-?Q8LTZ-lwB}xh*@J2Ni5HhUI3`ct|*W#pqb>8i*TXOLn~GlYECIj zhLaa_rBH|1jgi(S%~31Xm{NB!30*mcsF_wgOY2N0XjG_`kFB+uQuJbBm3bIM$qhUyE&$_u$gb zpK_r{99svp3N3p4yHHS=#csK@j9ql*>j0X=+cD2dj<^Wiu@i>c_v zK|ovi7}@4sVB#bzq$n3`EgI?~xDmkCW=2&^tD5RuaSNHf@Y!5C(Is$hd6cuyoK|;d zO}w2AqJPS`Zq+(mc*^%6qe>1d&(n&~()6-ZATASNPsJ|XnxelLkz8r1x@c2XS)R*H(_B=IN>JeQUR;T=i3<^~;$<+8W*eRKWGt7c#>N`@;#!`kZ!P!&{9J1>_g8Zj zXEXxmA=^{8A|3=Au+LfxIWra)4p<}1LYd_$1KI0r3o~s1N(x#QYgvL4#2{z8`=mXy zQD#iJ0itk1d@Iy*DtXw)Wz!H@G2St?QZFz zVPkM%H8Cd2EZS?teQN*Ecnu|PrC!a7F_XX}AzfZl3fXfhBtc2-)zaC2eKx*{XdM~QUo4IwcGgVdW69 z1UrSAqqMALf^2|(I}hgo38l|Ur=-SC*^Bo5ej`hb;C$@3%NFxx5{cxXUMnTyaX{>~ zjL~xm;*`d08bG_K3-E+TI>#oqIN2=An(C6aJ*MrKlxj?-;G zICL$hi>`F%{xd%V{$NhisHSL~R>f!F7AWR&7b~TgLu6!3s#~8|VKIX)KtqTH5aZ8j zY?wY)XH~1_a3&>#j7N}0az+HZ;is;Zw(Am{MX}YhDTe(t{ZZ;TG}2qWYO+hdX}vp9 z@uIRR8g#y~-^E`Qyem(31{H0&V?GLdq9LEOb2(ea#e-$_`5Q{T%E?W(6 z(XbX*Ck%TQM;9V2LL}*Tf`yzai{0@pYMwBu%(I@wTY!;kMrzcfq0w?X`+y@0ah510 zQX5SU(I!*Fag4U6a7Lw%LL;L*PQ}2v2WwYF(lHx_Uz2ceI$mnZ7*eZ?RFO8UvKI0H z9Pq-mB`mEqn6n_W9(s~Jt_D~j!Ln9HA)P;owD-l~9FYszs)oEKShF9Zzcmnb8kZ7% zQ`>}ki1kwUO3j~ zEmh140sOkA9v>j@#56ymn_RnSF`p@9cO1XkQy6_Kog?0ivZDb`QWOX@tjMd@^Qr(p z!sFN=A)QZm!sTh(#q%O{Ovl{IxkF!&+A)w2@50=?a-+VuZt6On1;d4YtUDW{YNDN_ zG@_jZi1IlW8cck{uHg^g=H58lPQ^HwnybWy@@8iw%G! zwB9qVGt_?~M*nFAKd|{cGg+8`+w{j_^;nD>IrPf-S%YjBslSEDxgKH{5p)3LNr!lD z4ii)^%d&cCXIU7UK?^ZQwmD(RCd=?OxmY(Ko#+#CsTLT;p#A%{;t5YpHFWgl+@)N1 zZ5VDyB;+TN+g@u~{UrWrv)&#u~k$S&GeW)G{M#&Di)LdYk?{($Cq zZGMKeYW)aMtjmKgvF0Tg>Mmkf9IB#2tYmH-s%D_9y3{tfFmX1BSMtbe<(yqAyWX60 zzkgSgKb3c{QPG2MalYp`7mIrYg|Y<4Jk?XvJK)?|Ecr+)oNf}XLPuTZK%W>;<|r+% zTNViRI|{sf1v7CsWHvFrkQ$F7+FbqPQ#Bj7XX=#M(a~9^80}~l-DueX#;b}Ajn3VE z{BWI}$q{XcQ3g{(p>IOzFcAMDG0xL)H%wA)<(gl3I-oVhK~u_m=hAr&oeo|4lZbf} z+pe)c34Am<=z@5!2;_lwya;l?xV5&kWe}*5uBvckm(d|7R>&(iJNa6Y05SvlZcWBlE{{%2- z`86)Y5?H!**?{QbzGG~|k2O%eA8q=gxx-3}&Csf6<9BsiXC)T;x4YmbBIkNf;0Nd5 z%whM^!K+9zH>on_<&>Ws?^v-EyNE)}4g$Fk?Z#748e+GFp)QrQQETx@u6(1fk2!(W zWiCF~MomG*y4@Zk;h#2H8S@&@xwBIs|82R*^K(i*0MTE%Rz4rgO&$R zo9Neb;}_ulaCcdn3i17MO3NxzyJ=l;LU*N9ztBJ30j=+?6>N4{9YXg$m=^9@Cl9VY zbo^{yS@gU=)EpQ#;UIQBpf&zfCA;00H-ee=1+TRw@(h%W=)7WYSb5a%$UqNS@oI@= zDrq|+Y9e&SmZrH^iA>Of8(9~Cf-G(P^5Xb%dDgMMIl8gk6zdyh`D3OGNVV4P9X|EvIhplXDld8d z^YWtYUz@tpg*38Xys2?zj$F8%ivA47cGSl;hjD23#*62w3+fwxNE7M7zVK?x_`dBSgPK zWY_~wF~OEZi9|~CSH8}Xi>#8G73!QLCAh58W+KMJJC81{60?&~BM_0t-u|VsPBxn* zW7viEKwBBTsn_A{g@1!wnJ8@&h&d>!qAe+j_$$Vk;OJq`hrjzEE8Wjtm)Z>h=*M25 zOgETOM9-8xuuZ&^@rLObtcz>%iWe%!uGV09nUZ*nxJAY%&KAYGY}U1WChFik7HIw% zZP$3Bx|TG_`~19XV7kfi2GaBEhKap&)Q<9`aPs#^!kMjtPb|+-fX66z3^E)iwyXK7 z8)_p<)O{|i&!qxtgBvWXx8*69WO$5zACl++1qa;)0zlXf`eKWl!0zV&I`8?sG)OD2Vy?reNN<{eK+_ za4M;Hh%&IszR%)&gpgRCP}yheQ+l#AS-GnY81M!kzhWxIR?PW`G3G?} z$d%J28uQIuK@QxzGMKU_;r8P0+oIjM+k)&lZ39i#(ntY)*B$fdJnQ3Hw3Lsi8z&V+ zZly2}(Uzpt2aOubRjttzqrvinBFH4jrN)f0hy)tj4__UTwN)#1fj3-&dC_Vh7}ri* zfJ=oqLMJ-_<#rwVyN}_a-rFBe2>U;;1(7UKH!$L??zTbbzP#bvyg7OQBGQklJ~DgP zd<1?RJ<}8lWwSL)`jM53iG+}y2`_yUvC!JkMpbZyb&50V3sR~u+lok zT0uFRS-yx@8q4fPRZ%KIpLp8R#;2%c&Ra4p(GWRT4)qLaPNxa&?8!LRVdOUZ)2vrh zBSx&kB%#Y4!+>~)<&c>D$O}!$o{<1AB$M7-^`h!eW;c(3J~ztoOgy6Ek8Pwu5Y`Xion zFl9fb!k2`3uHPAbd(D^IZmwR5d8D$495nN2`Ue&`W;M-nlb8T-OVKt|fHk zBpjX$a(IR6*-swdNk@#}G?k6F-~c{AE0EWoZ?H|ZpkBxqU<0NUtvubJtwJ1mHV%9v?GdDw; zAyXZiD}f0Zdt-cl9(P1la+vQ$Er0~v}gYJVwQazv zH#+Z%2CIfOf90fNMGos|{zf&N`c0@x0N`tkFv|_9af3~<0z@mnf*e;%r*Fbuwl-IW z{}B3=(mJ#iwLIPiUP`J3SoP~#)6v;aRXJ)A-pD2?_2_CZ#}SAZ<#v7&Vk6{*i(~|5 z9v^nC`T6o`CN*n%&9+bopj^r|E(|pul;|q6m7Tx+U|UMjWK8o-lBSgc3ZF=rP{|l9 zc&R$4+-UG6i}c==!;I#8aDIbAvgLuB66CQLRoTMu~jdw`fPlKy@AKYWS-xyZzPg&JRAa@m-H43*+ne!8B7)HkQY4 zIh}NL4Q79a-`x;I_^>s$Z4J4-Ngq=XNWQ>yAUCoe&SMAYowP>r_O}S=V+3=3&(O=h zNJDYNs*R3Y{WLmBHc?mFEeA4`0Y`_CN%?8qbDvG2m}kMAiqCv`_BK z_6a@n`$#w6Csr@e2YsMx8udNWtNt=kcqDZdWZ-lGA$?1PA*f4?X*)hjn{sSo8!bHz zb&lGdAgBx@iTNPK#T_wy`KvOIZvTWqSHb=gWUCKXAiB5ckQI`1KkPx{{%1R*F2)Oc z(9p@yG{fRSWE*M9cdbrO^)8vQ2U`H6M>V$gK*rz!&f%@3t*d-r3mSW>D;wYxOhUul zk~~&ip5B$mZ~-F1orsq<|1bc3Zpw6)Ws5;4)HilsN;1tx;N6)tuePw& z==OlmaN*ybM&-V`yt|;vDz(_+UZ0m&&9#{9O|?0I|4j1YCMW;fXm}YT$0%EZ5^YEI z4i9WV*JBmEU{qz5O{#bs`R1wU%W$qKx?bC|e-iS&d*Qm7S=l~bMT{~m3iZl+PIXq{ zn-c~|l)*|NWLM%ysfTV-oR0AJ3O>=uB-vpld{V|cWFhI~sx>ciV9sPkC*3i0Gg_9G!=4ar*-W?D9)?EFL1=;O+W8}WGdp8TT!Fgv z{HKD`W>t(`Cds_qliEzuE!r{ihwEv1l5o~iqlgjAyGBi)$%zNvl~fSlg@M=C{TE;V zQkH`zS8b&!ut(m)%4n2E6MB>p*4(oV>+PT51#I{OXs9j1vo>9I<4CL1kv1aurV*AFZ^w_qfVL*G2rG@D2 zrs87oV3#mf8^E5hd_b$IXfH6vHe&lm@7On~Nkcq~YtE!}ad~?5*?X*>y`o;6Q9lkk zmf%TYonZM`{vJg$`lt@MXsg%*&zZZ0uUSse8o=!=bfr&DV)9Y6$c!2$NHyYAQf*Rs zk{^?gl9E z5Im8wlAsvQ6C2?DyG@95gUXZ3?pPijug25g;#(esF_~3uCj3~94}b*L>N2GSk%Qst z=w|Z>UX$m!ZOd(xV*2xvWjN&c5BVEdVZ0wvmk)I+YxnyK%l~caR=7uNQ=+cnNTLZ@&M!I$Mj-r{!P=; z`C2)D=VmvK8@T5S9JZoRtN!S*D_oqOxyy!q6Zk|~4aT|*iRN)fL)c>-yycR>-is0X zKrko-iZw(f(!}dEa?hef5yl%p0-v-8#8CX8!W#n2KNyT--^3hq6r&`)5Y@>}e^4h- zlPiDT^zt}Ynk&x@F8R&=)k8j$=N{w9qUcIc&)Qo9u4Y(Ae@9tA`3oglxjj6c{^pN( zQH+Uds2=9WKjH#KBIwrQI%bbs`mP=7V>rs$KG4|}>dxl_k!}3ZSKeEen4Iswt96GGw`E6^5Ov)VyyY}@itlj&sao|>Sb5 zeY+#1EK(}iaYI~EaHQkh7Uh>DnzcfIKv8ygx1Dv`8N8a6m+AcTa-f;17RiEed>?RT zk=dAksmFYPMV1vIS(Qc6tUO+`1jRZ}tcDP? zt)=7B?yK2RcAd1+Y!$K5*ds=SD;EEqCMG6+OqPoj{&8Y5IqP(&@zq@=A7+X|JBRi4 zMv!czlMPz)gt-St2VZwDD=w_S>gRpc-g zUd*J3>bXeZ?Psjohe;z7k|d<*T21PA1i)AOi8iMRwTBSCd0ses{)Q`9o&p9rsKeLaiY zluBw{1r_IFKR76YCAfl&_S1*(yFW8HM^T()&p#6y%{(j7Qu56^ZJx1LnN`-RTwimdnuo*M8N1ISl+$C-%=HLG-s} zc99>IXRG#FEWqSV9@GFW$V8!{>=lSO%v@X*pz*7()xb>=yz{E$3VE;e)_Ok@A*~El zV$sYm=}uNlUxV~6e<6LtYli1!^X!Ii$L~j4e{sI$tq_A(OkGquC$+>Rw3NFObV2Z)3Rt~Jr{oYGnZaFZ^g5TDZlg;gaeIP} z!7;T{(9h7mv{s@piF{-35L=Ea%kOp;^j|b5ZC#xvD^^n#vPH=)lopYz1n?Kt;vZmJ z!FP>Gs7=W{sva+aO9S}jh0vBs+|(B6Jf7t4F^jO3su;M13I{2rd8PJjQe1JyBUJ5v zcT%>D?8^Kp-70bP8*rulxlm)SySQhG$Pz*bo@mb5bvpLAEp${?r^2!Wl*6d7+0Hs_ zGPaC~w0E!bf1qFLDM@}zso7i~(``)H)zRgcExT_2#!YOPtBVN5Hf5~Ll3f~rWZ(UsJtM?O*cA1_W0)&qz%{bDoA}{$S&-r;0iIkIjbY~ zaAqH45I&ALpP=9Vof4OapFB`+_PLDd-0hMqCQq08>6G+C;9R~}Ug_nm?hhdkK$xpI zgXl24{4jq(!gPr2bGtq+hyd3%Fg%nofK`psHMs}EFh@}sdWCd!5NMs)eZg`ZlS#O0 zru6b8#NClS(25tXqnl{|Ax@RvzEG!+esNW-VRxba(f`}hGoqci$U(g30i}2w9`&z= zb8XjQLGN!REzGx)mg~RSBaU{KCPvQx8)|TNf|Oi8KWgv{7^tu}pZq|BS&S<53fC2K4Fw6>M^s$R$}LD*sUxdy6Pf5YKDbVet;P!bw5Al-8I1Nr(`SAubX5^D9hk6$agWpF}T#Bdf{b9-F#2WVO*5N zp+5uGgADy7m!hAcFz{-sS0kM7O)qq*rC!>W@St~^OW@R1wr{ajyYZq5H!T?P0e+)a zaQ%IL@X_`hzp~vRH0yUblo`#g`LMC%9}P;TGt+I7qNcBSe&tLGL4zqZqB!Bfl%SUa z6-J_XLrnm*WA`34&mF+&e1sPCP9=deazrM=Pc4Bn(nV;X%HG^4%Afv4CI~&l!Sjzb z{rHZ3od0!Al{}oBO>F*mOFAJrz>gX-vs!7>+_G%BB(ljWh$252j1h;9p~xVA=9_`P z5KoFiz96_QsTK%B&>MSXEYh`|U5PjX1(+4b#1PufXRJ*uZ*KWdth1<0 zsAmgjT%bowLyNDv7bTUGy|g~N34I-?lqxOUtFpTLSV6?o?<7-UFy*`-BEUsrdANh} zBWkDt2SAcGHRiqz)x!iVoB~&t?$yn6b#T=SP6Ou8lW=B>=>@ik93LaBL56ub`>Uo!>0@O8?e)$t(sgy$I z6tk3nS@yFFBC#aFf?!d_3;%>wHR;A3f2SP?Na8~$r5C1N(>-ME@HOpv4B|Ty7%jAv zR}GJwsiJZ5@H+D$^Cwj#0XA_(m^COZl8y7Vv(k=iav1=%QgBOVzeAiw zaDzzdrxzj%sE^c9_uM5D;$A_7)Ln}BvBx^=)fO+${ou%B*u$(IzVr-gH3=zL6La;G zu0Kzy5CLyNGoKRtK=G0-w|tnwI)puPDOakRzG(}R9fl7#<|oQEX;E#yCWVg95 z;NzWbyF&wGg_k+_4x4=z1GUcn6JrdX4nOVGaAQ8#^Ga>aFvajQN{!+9rgO-dHP zIp@%&ebVg}IqnRWwZRTNxLds+gz2@~VU(HI=?Epw>?yiEdZ>MjajqlO>2KDxA>)cj z2|k%dhh%d8SijIo1~20*5YT1eZTDkN2rc^zWr!2`5}f<2f%M_$to*3?Ok>e9$X>AV z2jYmfAd)s|(h?|B(XYrIfl=Wa_lBvk9R1KaP{90-z{xKi+&8=dI$W0+qzX|ZovWGOotP+vvYR(o=jo?k1=oG?%;pSqxcU* zWVGVMw?z__XQ9mnP!hziHC`ChGD{k#SqEn*ph6l46PZVkm>JF^Q{p&0=MKy_6apts z`}%_y+Tl_dSP(;Ja&sih$>qBH;bG;4;75)jUoVqw^}ee=ciV;0#t09AOhB^Py7`NC z-m+ybq1>_OO+V*Z>dhk}QFKA8V?9Mc4WSpzj{6IWfFpF7l^au#r7&^BK2Ac7vCkCn{m0uuN93Ee&rXfl1NBY4NnO9lFUp zY++C1I;_{#OH#TeP2Dp?l4KOF8ub?m6zE@XOB5Aiu$E~QNBM@;r+A5mF2W1-c7>ex zHiB=WJ&|`6wDq*+xv8UNLVUy4uW1OT>ey~Xgj@MMpS@wQbHAh>ysYvdl-1YH@&+Q! z075(Qd4C!V`9Q9jI4 zSt{HJRvZec>vaL_brKhQQwbpQd4_Lmmr0@1GdUeU-QcC{{8o=@nwwf>+dIKFVzPriGNX4VjHCa zTbL9w{Y2V87c2ofX%`(48A+4~mYTiFFl!e{3K^C_k%{&QTsgOd0*95KmWN)P}m zTRr{`f7@=v#+z_&fKYkQT!mJn{*crj%ZJz#(+c?>cD&2Lo~FFAWy&UG*Op^pV`BR^I|g?T>4l5;b|5OQ@t*?_Slp`*~Y3`&RfKD^1uLezIW(cE-Dq2z%I zBi8bWsz0857`6e!ahet}1>`9cYyIa{pe53Kl?8|Qg2RGrx@AlvG3HAL-^9c^1GW;)vQt8IK+ zM>!IW*~682A~MDlyCukldMd;8P|JCZ&oNL(;HZgJ>ie1PlaInK7C@Jg{3kMKYui?e!b`(&?t6PTb5UPrW-6DVU%^@^E`*y-Fd(p|`+JH&MzfEq;kikdse ziFOiDWH(D< zyV7Rxt^D0_N{v?O53N$a2gu%1pxbeK;&ua`ZkgSic~$+zvt~|1Yb=UfKJW2F7wC^evlPf(*El+#}ZBy0d4kbVJsK- z05>;>?HZO(YBF&v5tNv_WcI@O@LKFl*VO?L(!BAd!KbkVzo;v@~3v`-816GG?P zY+H3ujC>5=Am3RIZDdT#0G5A6xe`vGCNq88ZC1aVXafJkUlcYmHE^+Z{*S->ol%-O znm9R0TYTr2w*N8Vs#s-5=^w*{Y}qp5GG)Yt1oLNsH7y~N@>Eghms|K*Sdt_u!&I}$ z+GSdFTpbz%KH+?B%Ncy;C`uW6oWI46(tk>r|5|-K6)?O0d_neghUUOa9BXHP*>vi; z={&jIGMn-92HvInCMJcyXwHTJ42FZp&Wxu+9Rx;1x(EcIQwPUQ@YEQQ`bbMy4q3hP zNFoq~Qd0=|xS-R}k1Im3;8s{BnS!iaHIMLx)aITl)+)?Yt#fov|Eh>}dv@o6R{tG>uHsy&jGmWN5+*wAik|78(b?jtysPHC#e+Bzz~V zS3eEXv7!Qn4uWi!FS3B?afdD*{fr9>B~&tc671fi--V}~E4un;Q|PzZRwk-azprM$4AesvUb5`S`(5x#5VJ~4%ET6&%GR$}muHV-5lTsCi_R|6KM(g2PCD@|yOpKluT zakH!1V7nKN)?6JmC-zJoA#ciFux8!)ajiY%K#RtEg$gm1#oKUKX_Ms^%hvKWi|B=~ zLbl-L)-=`bfhl`>m!^sRR{}cP`Oim-{7}oz4p@>Y(FF5FUEOfMwO!ft6YytF`iZRq zfFr{!&0Efqa{1k|bZ4KLox;&V@ZW$997;+Ld8Yle91he{BfjRhjFTFv&^YuBr^&Pe zswA|Bn$vtifycN8Lxr`D7!Kygd7CuQyWqf}Q_PM}cX~S1$-6xUD%-jrSi24sBTFNz(Fy{QL2AmNbaVggWOhP;UY4D>S zqKr!UggZ9Pl9Nh_H;qI`-WoH{ceXj?m8y==MGY`AOJ7l0Uu z)>M%?dtaz2rjn1SW3k+p`1vs&lwb%msw8R!5nLS;upDSxViY98IIbxnh{}mRfEp=9 zbrPl>HEJeN7J=KnB6?dwEA6YMs~chHNG?pJsEj#&iUubdf3JJwu=C(t?JpE6xMyhA3e}SRhunDC zn-~83*9=mADUsk^sCc%&&G1q5T^HR9$P#2DejaG`Ui*z1hI#h7dwpIXg)C{8s< z%^#@uQRAg-$z&fmnYc$Duw63_Zopx|n{Bv*9Xau{a)2%?H<6D>kYY7_)e>OFT<6TT z0A}MQLgXbC2uf`;67`mhlcUhtXd)Kbc$PMm=|V}h;*_%vCw4L6r>3Vi)lE5`8hkSg zNGmW-BAOO)(W((6*e_tW&I>Nt9B$xynx|sj^ux~?q?J@F$L4;rnm_xy8E*JYwO-02u9_@@W0_2@?B@1J{y~Q39N3NX^t7#`=34Wh)X~sU&uZWgS1Z09%_k|EjA4w_QqPdY`oIdv$dJZ;(!k)#U8L+|y~gCzn+6WmFt#d{OUuKHqh1-uX_p*Af8pFYkYvKPKBxyid4KHc}H` z*KcyY;=@wzXYR{`d{6RYPhapShXIV?0cg_?ahZ7do)Ot#mxgXYJYx}<%E1pX;zqHd zf!c(onm{~#!O$2`VIXezECAHVd|`vyP)Uyt^-075X@NZDBaQt<>trA3nY-Dayki4S zZ^j6CCmx1r46`4G9794j-WC0&R9(G7kskS>=y${j-2;(BuIZTLDmAyWTG~`0)Bxqk zd{NkDe9ug|ms@0A>JVmB-IDuse9h?z9nw!U6tr7t-Lri5H`?TjpV~8(gZWFq4Vru4 z!86bDB;3lpV%{rZ`3gtmcRH1hjj!loI9jN>6stN6A*ujt!~s!2Q+U1(EFQEQb(h4E z6VKuRouEH`G6+8Qv2C)K@^;ldIuMVXdDDu}-!7FS8~k^&+}e9EXgx~)4V4~o6P^52 z)a|`J-fOirL^oK}tqD@pqBZi_;7N43%{IQ{v&G9^Y^1?SesL`;Z(dt!nn9Oj5Odde%opv&t zxJ><~b#m+^KV&b?R#)fRi;eyqAJ_0(nL*61yPkJGt;gZxSHY#t>ATnEl-E%q$E16% zZdQfvhm5B((y4E3Hk6cBdwGdDy?i5CqBlCVHZr-rI$B#>Tbi4}Gcvyg_~2=6O9D-8 zY2|tKrNzbVR$h57R?Pe+gUU_il}ZaWu|Az#QO@};=|(L-RVf0AIW zq#pO+RfM7tdV`9lI6g;{qABNId`fG%U9Va^ravVT^)CklDcx)YJKeJdGpM{W1v8jg z@&N+mR?BPB=K1}kNwXk_pj44sd>&^;d!Z~P>O78emE@Qp@&8PyB^^4^2f7e)gekMv z2aZNvP@;%i{+_~>jK7*2wQc6nseT^n6St9KG#1~Y@$~zR_=AcO2hF5lCoH|M&c{vR zSp(GRVVl=T*m~dIA;HvYm8HOdCkW&&4M~UDd^H)`p__!4k+6b)yG0Zcek8OLw$C^K z3-BbLiG_%qX|ZYpXJ$(c@aa7b4-*IQkDF}=gZSV`*ljP|5mWuHSCcf$5qqhZTv&P?I$z^>}qP(q!Aku2yA5vu38d8x*q{6-1`%PrE_r0-9Qo?a#7Zbz#iGI7K<(@k^|i4QJ1H z4jx?{rZbgV!me2VT72@nBjucoT zUM9;Y%TCoDop?Q5fEQ35bCYk7!;gH*;t9t-QHLXGmUF;|vm365#X)6b2Njsyf1h9JW#x$;@x5Nx2$K$Z-O3txa%;OEbOn6xBzd4n4v)Va=sj5 z%rb#j7{_??Tjb8(Hac<^&s^V{yO-BL*uSUk2;X4xt%NC8SjO-3?;Lzld{gM5A=9AV z)DBu-Z8rRvXXwSVDH|dL-3FODWhfe1C_iF``F05e{dl(MmS|W%k-j)!7(ARkV?6r~ zF=o42y+VapxdZn;GnzZfGu<6oG-gQ7j7Zvgo7Am@jYxC2FpS@I;Jb%EyaJDBQC(q% zKlZ}TVu!>;i3t~OAgl@QYy1X|T~D{HOyaS*Bh}A}S#a9MYS{XV{R-|niEB*W%GPW! zP^NU(L<}>Uab<;)#H)rYbnqt|dOK(-DCnY==%d~y(1*{D{Eo1cqIV8*iMfx&J*%yh zx=+WHjt0q2m*pLx8=--UqfM6ZWjkev>W-*}_*$Y(bikH`#-Gn#!6_ zIA&kxn;XYI;eN9yvqztK-a113A%97in5CL5Z&#VsQ4=fyf&3MeKu70)(x^z_uw*RG zo2Pv&+81u*DjMO6>Mrr7vKE2CONqR6C0(*;@4FBM;jPIiuTuhQ-0&C)JIzo_k>TaS zN_hB;_G=JJJvGGpB?uGgSeKaix~AkNtYky4P7GDTW6{rW{}V9K)Cn^vBYKe*OmP!; zohJs=l-0sv5&pL6-bowk~(swtdRBZQHh8)m^r2+qTtZ zt4m$B?OQYNyfBA0E)g28a*{)a=%%f-?{F;++-Xs#5|7kSHTD*E9@$V ztE%7zX4A(L`n)FY8Y4pOnKC|Pf)j$iR#yP;V0+|Hki+D;t4I4BjkfdYliK9Gf6RYw z;3px$Ud5aTd`yq$N7*WOs!{X91hZZ;AJ9iQOH%p;v$R%OQum_h#rq9*{ve(++|24z zh2P;{-Z?u#rOqd0)D^_Ponv(Y9KMB9#?}nJdUX&r_rxF0%3__#8~ZwsyrSPmtWY27 z-54ZquV2t_W!*+%uwC=h-&_q~&nQer0(FL74to%&t^byl^C?wTaZ-IS9OssaQFP)1 zAov0o{?IRAcCf+PjMWSdmP42gysh|c9Ma&Q^?_+>>+-yrC8WR;*XmJ;>r9v*>=W}tgWG;WIt{~L8`gk8DP{dSdG z4SDM7g5ahMHYHHk*|mh9{AKh-qW7X+GEQybJt9A@RV{gaHUAva+=lSroK^NUJYEiL z?X6l9ABpd)9zzA^;FdZ$QQs#uD@hdcaN^;Q=AXlbHv511Meye`p>P4Y2nblEDEeZo}-$@g&L98Aih6tgLz--${eKTxymIipy0xSYgZZ zq^yyS4yNPTtPj-sM?R8@9Q1gtXPqv{$lb5i|C1yymwnGdfYV3nA-;5!Wl zD0fayn!B^grdE?q^}ba{-LIv*Z}+hZm_F9c$$cW!bx2DgJD&6|bBIcL@=}kQA1^Eh zXTEznqk)!!IcTl>ey?V;X8k<+C^DRA{F?T*j0wV`fflrLBQq!l7cbkAUE*6}WabyF zgpb+|tv=aWg0i}9kBL8ZCObYqHEycr5tpc-$|vdvaBsu#lXD@u_e1iL z{h>xMRS0a7KvW?VttrJFpX^5DC4Bv4cp6gNG6#8)7r7IxXfSNSp6)_6tZ4l>(D+0I zPhU)N!sKywaBusHdVE!yo5$20JAU8V_XcW{QmO!p*~ns8{2~bhjydnmA&=r zX9NSM9QYogYMDZ~kS#Qx`mt>AmeR3p@K$`fbJ%LQ1c5lEOz<%BS<}2DL+$>MFcE%e zlxC)heZ7#i80u?32eOJI9oQRz0z;JW@7Th4q}YmQ-`Z?@y3ia^_)7f37QMwDw~<-@ zT)B6fftmK_6YS!?{uaj5lLxyR++u*ZY2Mphm5cd7PA5=%rd)95hJ9+aGSNfjy>Ylc zoI0nGIT3sKmwX8h=6CbvhVO+ehFIR155h8iRuXZx^cW>rq5K4z_dvM#hRER=WR@THs%WELI9uYK9HN44Em2$#@k)hD zicqRPKV#yB;UlcsTL_}zCMK0T;eXHfu`y2(dfwm(v)IBbh|#R>`2cot{m7}8_X&oD zr@94PkMCl%d3FsC4pil=#{3uv^+)pvxfwmPUr)T)T|GcZVD$wVj$mjkjDs`5cm8N! zXVq2CvL;gWGpPI4;9j;2&hS*o+LNp&C5Ac=OXx*W5y6Z^az)^?G0)!_iAfjH5wiSE zD(F}hQZB#tF5iEx@0sS+dP70DbZ*<=5X^)Pxo^8aKzOzuyc2rq=<0-k;Y_ID1>9^v z+)nc36}?>jen*1%OX3R*KRASj${u$gZ$27Hpcj=95kK^aLzxhW6jj_$w6}%#1*$5D zG1H_vYFrCSwrRqYw*9<}OYAOQT)u%9lC`$IjZV<4`9Sc;j{Qv_6+uHrYifK&On4V_7yMil!0Yv55z@dFyD{U@Sy>|vTX=P_( zRm<2xj*Z}B30VAu@0e+}at*y?wXTz|rPalwo?4ZZc>hS0Ky6~mi@kv#?xP2a;yt?5=(-CqvP_3&$KdjB7Ku;# z`GLE*jW1QJB5d&E?IJO?1+!Q8HQMGvv^RuFoi=mM4+^tOqvX%X&viB%Ko2o-v4~~J z267ui;gsW?J=qS=D*@*xJvAy3IOop5bEvfR4MZC>9Y4Z$rGI|EHNNZ7KX;Ix{xSvm z-)Cau-xuTm|7`4kUdXvd_d^E=po(76ELfq5OgxIt3aqDy#zBfIy-5<3gpn{Ce`-ha z<;6y@{Bgqw?c~h*&j{FozQCh=`Lv-5Iw!KdSt;%GDOq%=(V!dJ-}|}|0o5G2kJj6{ z`jCSPs$9Fe8O(+qALZiJ$WtR=<@GvsdM)IJ`7XrBfW0iyYE#Vy^e@zbysg*B5Z_kSL6<)vqoaH zQ{!9!*{e9UZo^h+qZ`T@LfVwAEwc&+9{C8c%oj41q#hyn<&zA9IIur~V|{mmu`n5W z8)-Ou$YgjQ*PMIqHhZ_9E?(uoK0XM5aQkarcp}WT^7b^FC#^i>#8LGZ9puDuXUYas z7caX)V5U6uY-L5Wl%)j$qRkR;7@3T*N64YK_!`Fw=>CAwe~2loI1<>DZW&sb7Q)X;6E08&$h! z2=c1i4UOO{R4TmkTz+o9n`}+%d%blR6P;5{`qjtxlN$~I%tMMDCY`~e{+mRF!rj5( z3ywv)P_PUUqREu)TioPkg&5RKjY6z%pRxQPQ{#GNMTPag^S8(8l{!{WGNs2U1JA-O zq02VeYcArhTAS;v3);k(&6ayCH8SXN@r;1NQeJ*y^NHM+zOd;?t&c!Hq^SR_w6twGV8dl>j zjS+Zc&Yp7cYj&c1y3IxQ%*kWiYypvoh(k8g`HrY<_Bi-r%m-@SLfy-6mobxkWHxyS z>TtM2M4;Uqqy|+8Q++VcEq$PwomV1D4UzNA*Tgkg9#Gpz#~&iPf|Czx!J?qss?e|3 z4gTua75-P{2X7w9eeK3~GE0ip-D;%%gTi)8bR~Ez@)$gpuS~jZs`CrO5SR-Xy7bkA z89fr~mY}u4A$|r1$fe-;T{yJh#9Ime1iRu8eo?uY9@yqAU3P!rx~SsP;LTBL zeoMK(!;(Zt8313 z3)V)q_%eflKW?BnMZa}6E0c7t!$-mC$qt44OME5F(6B$E8w*TUN-h}0dOiXI+TH zYFrr&k1(yO(|J0vP|{22@Z}bxm@7BkjO)f)&^fv|?_JX+s)1*|7X7HH(W?b3QZ3!V|~m?8}uJsF>NvE4@fik zjyyh+U*tt`g6v>k9ub88a;ySvS1QawGn7}aaR**$rJA=a#eUT~ngUbJ%V=qsFIekLbv!YkqjTG{_$F;$w19$(ivIs*1>?2ka%uMOx@B9`LD zhm~)z@u4x*zcM1WhiX)!U{qOjJHt1xs{G1S?rYe)L)ntUu^-(o_dfqZu)}W(X%Uu| zN*qI@&R2fB#Jh|Mi+eMrZDtbNvYD3|v0Kx>E#Ss;Be*T$@DC!2A|mb%d}TTN3J+c= zu@1gTOXFYy972S+=C;#~)Z{Swr0VI5&}WYzH22un_Yg5o%f9fvV(`6!{C<(ZigQ2`wso)cj z9O12k)15^Wuv#rHpe*k5#4vb%c znP+Gjr<-p%01d<+^yrSoG?}F=eI8X;?=Fo2a~HUiJ>L!oE#9tXRp!adg-b9D;(6$E zeW0tH$US04zTX$OxM&X+2ip>KdFM?iG_fgOD-qB|uFng8*#Z5jgqGY=zLU?4!OlO#~YBTB9b9#~H@nqQ#5 z6bV));d?IJTVBC+79>rGuy1JgxPLy$dA7;_^^L)02m}XLjFR*qH`eI~+eJo(7D`LH z(W%lGnGK+Vk_3kyF*zpgO=1MxMg?hxe3}}YI>dVs8l}5eWjYu4=w6MWK09+05 zGdpa#$awd>Q|@aZa*z{5F3xy3n@E4YT9%TmMo0jxW59p0bI?&S}M+ z&^NG%rf7h*m9~p#b19|`wO5OMY-=^XT+=yrfGNpl<&~~FGsx_`IaFn+sEgF$hgOa~oAVAiu^a$jHcqkE=dj`ze z=axsfrzzh6VGD0x#6Ff=t%+VTiq!n6^gv*uIUD<9fOhvR;al5kcY${uunn}-!74<7 zmP^3cl-kyN(QY!!Z-^PY-OUkh=3ZWk6>le$_Q&xk4cgH{?i)C%2RM@pX5Q{jdSlo! zVau5v44cQX5|zQlQDt;dCg)oM0B<=P1CR!W%!^m$!{pKx;bn9DePJjWBX)q!`$;0K zqJIIyD#aK;#-3&Nf=&IhtbV|?ZGYHSphp~6th`p2rkw&((%kBV7<{siEOU7AxJj+FuRdDu$ zcmTW8usU_u!r)#jg|J=Gt{##7;uf4A5cdt6Y02}f(d2)z~ z)CH~gVAOwBLk$ZiIOn}NzDjvfw(w$u|BdCBI#)3xB-Ot?nz?iR38ayCm48M=_#9r7 zw8%pwQ<9mbEs5~_>pN3~#+Er~Q86J+2TDXM6umCbukd-X6pRIr5tF?VauT8jW> zY^#)log>jtJs2s3xoiPB7~8#1ZMv>Zx0}H58k-@H2huNyw~wsl0B8j)H5)H9c7y&i zp8^0;rKbxC1eEZ-#Qxvz)Xv$((8lK9I>BspPajluysw^f#t9P;OUis43mmEzX+lk* zc4T-Ms9_687GR+~QS#0~vxK#DSGN=a-m(@eZTqw2<+lN9>R~gK2)3;sT4%nI%Y|0m zX9SPR!>?~s=j5H4WMqeTW8QaLZ=1bWS5I3xZ&$(ypc=tHrv+hX@s)VG(tc!yvLM7n zshN=C#v={X1r;)xn0Pow_1eMhkn!{;x$BJ#PIz)m585&%cmzk;btQzZAN_^zis;n? z?6I~bN?s;7vg_dtoTc4A5Ow*Rb}No#UYl)sN|RmoYo}k^cKLXd8F`44?RrokkPvN5 ztUrx;U~B;jbE_qGd3n0j2i}A{enJvJ?gSF~NQj~EP5vM-w4@;QQ5n(Npic}XNW6B0 zq9F4T%6kp7qGhd0vpQcz+nMk8GOAmbz8Bt4@GtewGr6_>Xj>ge)SyfY}nu>Y!a@HoIx(StD zx`!>RT&}tpBL%nOF%7XIFW?n1AP*xthCMzhrU6G!U6?m4!CPWTvn#Yaoi_95CT2!L z|B=5zeRW30&ANGN>J9#GtCm&3SF6n4TqDz<-{@ZXkrkRDCpV$DwCtI^e&3i1A{Ar&JZtS^c+lyPa6 z%JJr42S_;eFC#M~bdtQePhOU32WDiZ4@H&af)z#$Y|hnQNb)8(3?1Ad>5uaZ1z zU~!jt3XUI@gpWb8tWTyH7DGvKvzYfqNIy3P{9vpwz_C-QL&`+8Io$F5PS-@YQJoEO z17D9P(+sXajWSH_8&C?fn>rTLX+(?KiwX#JNV)xE0!Q@>Tid$V2#r4y6fkph?YZ>^ z(o^q(0*P->3?I0cELXJn(N|#qTm6 zAPIL~n)m!50;*?5=MOOc4Wk;w(0c$(!e?vpV23S|n|Y7?nyc8)fD8t-KI&nTklH&BzqQ}D(1gH3P+5zGUzIjT~x`;e8JH=86&5&l-DP% z)F+Et(h|GJ?rMy-Zrf>Rv@<3^OrCJ1xv_N*_@-K5=)-jP(}h1Rts44H&ou8!G_C1E zhTfUDASJ2vu!4@j58{NN;78i?6__xR75QEDC4JN{>RmgcNrn-EOpEOcyR<8FS@RB@ zH!R7J=`KK^u06eeI|X@}KvQmdKE3AmAy8 zM4IIvde#e4O(iwag`UL5yQo>6&7^=D4yE-Eo9$9R2hR} zn;Z9i-d=R-xZl4@?s%8|m1M`$J6lW1r0Y)+8q$}Vn4qyR1jqTjGH;@Z!2KiGun2~x zaiEfzVT<|_b6t}~XPeflAm8hvCHP3Bp*tl{^y_e{Jsn@w+KP{7}bH_s=1S2E1sj=18a39*Ag~lbkT^_OQuYQey=b zW^{0xlQ@O$^cSxUZ8l(Mspg8z0cL*?yH4;X2}TdN)uN31A%$3$a=4;{S@h#Y(~i%) zc=K7Ggl=&2hYVic*W65gpSPE70pU;FN@3k?BYdNDKv6wlsBAF^);qiqI zhklsX4TaWiC%VbnZ|yqL+Pcc;(#&E*{+Rx&<&R{uTYCn^OD|mAk4%Q7gbbgMnZwE{ zy7QMK%jIjU@ye?0; z;0--&xVeD}m_hq9A8a}c9WkI2YKj8t!Mkk!o%AQ?|CCBL9}n570}OmZ(w)YI6#QS&p<={tcek*D{CPR%eVA1WBGUXf z%gO2vL7iVDr1$!LAW)1@H>GoIl=&yyZ7=*9;wrOYQ}O}u>h}4FWL?N2ivURlUi11- zl{G0fo`9?$iAEN<4kxa#9e0SZPqa{pw?K=tdN5tRc7HDX-~Ta6_+#s9W&d`6PB7dF*G@|!Mc}i zc=9&T+edI(@la}QU2An#wlkJ&7RmTEMhyC_A8hWM54?s1WldCFuBmT5*I3K9=1aj= z6V@93P-lUou`xmB!ATp0(We$?)p*oQs;(Kku15~q9`-LSl{(Efm&@%(zj?aK2;5}P z{6<@-3^k^5FCDT@Z%XABEcuPoumYkiD&)-8z2Q}HO9OVEU3WM;V^$5r4q>h^m73XF z5!hZ7SCjfxDcXyj(({vg8FU(m2_}36L_yR>fnW)u=`1t@mPa76`2@%8v@2@$N@TE` z)kYhGY1jD;B9V=Dv1>BZhR9IJmB?X9Wj99f@MvJ2Fim*R`rsRilvz_3n!nPFLmj({EP!@CGkY5R*Y_dSO{qto~WerlG}DMw9k+n}pk z*nL~7R2gB{_9=zpqX|*vkU-dx)(j+83uvYGP?K{hr*j2pQsfXn<_As6z%-z+wFLqI zMhTkG>2M}#BLIOZ(ya1y8#W<+uUo@(43=^4@?CX{-hAuaJki(_A(uXD(>`lzuM~M;3XA48ZEN@HRV{1nvt?CV)t;|*dow0Ue2`B*iA&!rI`fZQ=b28= z_dxF}iUQ8}nq0SA4NK@^EQ%=)OY;3fC<$goJ&Kp|APQ@qVbS-MtJQBc)^aO8mYFsbhafeRKdHPW&s^&;%>v zlTz`YE}CuQ@_X&mqm{+{!h2r)fPGeM_Ge4RRYQkrma`&G<>RW<>S(?#LJ}O-t)d$< zf}b0svP^Zu@)MqwEV^Fb_j zPYYs~vmEC~cOIE6Nc^@b@nyL!w5o?nQ!$mGq(Pa|1-MD}K0si<&}eag=}WLSDO zE4+eA~!J(K}605x&4 zT72P7J^)Y)b(3g2MZ@1bv%o1ggwU4Yb!DhQ=uu-;vX+Ix8>#y6wgNKuobvrPNx?$3 zI{BbX<=Y-cBtvY&#MpGTgOLYU4W+csqWZx!=AVMb)Z;8%#1*x_(-)teF>45TCRwi1 z)Nn>hy3_lo44n-4A@=L2gI$yXCK0lPmMuldhLxR8aI;VrHIS{Dk}yp= zwjhB6v@0DN=Hnm~3t>`CtnPzvA*Kumfn5OLg&-m&fObRD};c}Hf?n&mS< z%$wztc%kjWjCf-?+q(bZh9k~(gs?i4`XVfqMXvPVkUWfm4+EBF(nOkg!}4u)6I)JT zU6IXqQk?p1a2(bz^S;6ZH3Wy9!JvbiSr7%c$#G1eK2^=~z1WX+VW)CPD#G~)13~pX zErO(>x$J_4qu-)lNlZkLj2}y$OiKn0ad5Imu5p-2dnt)(YI|b7rJ3TBUQ8FB8=&ym50*ibd2NAbj z;JA&hJ$AJlldM+tO;Yl3rBOFiP8fDdF?t(`gkRpmT9inR@uX{bThYNmxx-LN5K8h0 ztS%w*;V%b`%;-NARbNXn9he&AO4$rvmkB#;aaOx?Wk|yBCmN{oMTK&E)`s&APR<-5 z#;_e75z;LJ)gBG~h<^`SGmw<$Z3p`KG|I@7Pd)sTJnouZ1hRvm3}V+#lPGk4b&A#Y z4VSNi8(R1z7-t=L^%;*;iMTIAjrXl;h106hFrR{n9o8vlz?+*a1P{rEZ2ie{luQs} zr6t746>eoqiO5)^y;4H%2~&FT*Qc*9_oC2$+&syHWsA=rn3B~4#QEW zf4GT3i_@)f(Fj}gAZj`7205M8!B&HhmbgyZB& z+COyAVNxql#DwfP;H48Yc+Y~ChV6b9auLnfXXvpjr<~lQ@>VbCpQvWz=lyVf1??_c zAo3C^otZD@(v?X)UX*@w?TF|F8KF>l7%!Dzu+hksSA^akEkx8QD(V(lK+HBCw6C}2onVExW)f$ zncm*HI(_H;jF@)6eu}Tln!t?ynRkcqBA5MitIM@L^(4_Ke}vy7c%$w{(`&7Rn=u>oDM+Z^RUYcbSOPwT(ONyq76R>$V6_M_UP4vs=__I#io{{((| zy5=k=oVr-Qt$FImP~+&sN8rf2UH*vRMpwohPc@9?id17La4weIfBNa>1Djy+1=ugn z@}Zs;eFY1OC}WBDxDF=i=On_33(jWE-QYV)HbQ^VM!n>Ci9_W0Zofz7!m>do@KH;S z4k}FqEAU2)b%B_B-QcPnM5Zh=dQ+4|DJoJwo?)f2nWBuZE@^>a(gP~ObzMuyNJTgJFUPcH`%9UFA(P23iaKgo0)CI!SZ>35LpFaD7 z)C2sW$ltSEYNW%%j8F;yK{iHI2Q^}coF@LX`=EvxZb*_O;2Z0Z5 z7 zlccxmCfCI;_^awp|G748%Wx%?t9Sh8!V9Y(9$B?9R`G)Nd&snX1j+VpuQ@GGk=y(W zK|<$O`Cad`Y4#W3GKXgs%lZduAd1t1<7LwG4*zaStE*S)XXPFDyKdgiaVXG2)LvDn zf}eQ_S(&2!H0Mq1Yt&WpM1!7b#yt_ie7naOfX129_E=)beKj|p1VW9q>>+e$3@G$K zrB%i_TT1DHjOf7IQ8)Wu4#K%ZSCDGMP7Ab|Kvjq7*~@ewPm~h_-8d4jmNH<&mNZC@CI zKxG5O08|@<4(6IEC@L-lcrrvix&_Dj4tBvl=8A}2UX|)~v#V$L22U}UHk`B-1MF(t zU6aVJWR!>Y0@4m0UA%Sq9B5;4hZvsOu=>L`IU4#3r_t}os|vSDVMA??h>QJ1FD1vR z*@rclvfD!Iqoxh>VP+?b9TVH8g@KjYR@rRWQy44A`f6doIi+8VTP~pa%`(Oa@5?=h z8>YxNvA##a3D0)^P|2|+0~f|UsAJV=q(S>eq-dehQ+T>*Q@qN zU8@kdpU5gGk%ozt?%c8oM6neA?GuSsOfU_b1U)uiEP8eRn~>M$p*R z43nSZs@^ahO78s zulbK@@{3=2=@^yZ)DuIC$ki;`2WNbD_#`LOHN9iMsrgzt-T<8aeh z(oXrqI$Kgt6)Icu=?11NWs>{)_ed1wh>)wv6RYNUA-C&bejw{cBE_5Wzeo!AHdTd+ z)d(_IKN7z^n|As~3XS=cCB_TgM7rK;X586re`{~Foml$aKs zb!4Pe7hEP|370EWwn$HKPM!kL94UPZ1%8B^e5fB+=Iw^6=?5n3tZGYjov83CLB&OQ++p)WCMeshCv_9-~G9C_2x`LxTDjUcW$l6e!6-&a^fM3oP9*g(H zmCk0nGt1UMdU#pfg1G0um5|sc|KO<+qU1E4iBF~RvN*+`7uNHH^gu{?nw2DSCjig% zI@ymKZSK=PhHJa(jW&xeApv&JcfSmNJ4uQ|pY=Lcc>=J|{>5Ug3@x#R_b@55xFgfs za^ANzWdD$ZYtFs$d7+oiw0ZmPk2&l|< zc8()wfiJx@EGpQT zG$8iLkQZ-086doF1R zh<#9cz_vRsJdoXbD=QgOtpm}cFAJX8c}>Jew;PQJSXSb^;wlC zxXLHTS|!GZ-VK_4wV<9bk4RUmlsByzW_^b>)$6R+jQ}^wco1nMA`9Lncs;&QGp!`5Tx#aXXU?}5_RrtUY zx(EMzDhl-a^y^f5yfFLMnOO#u)l69&4M?|ne|2EV>zQ}4JQCBel?~2I4?D|>L$%H(peOOII!U}i z-j)*h1rODe9{0`xmhG;`AKqw1p0_KhEIU8)DoGnEn9wAhXPaxO_(jNSij~J5m$P*$ z9Mt(t;eV}2+i|kjQpBFcNb7_(VbuF<;RQB~R~p>2*Lg>a&7DEEuq*I%Ls4{zHeUDq z+M0&YhEn^C*9-B4Q7HJ$xj)dORCXPK+)ZtLOa0o&)Sl+f(Y{p*68$-#yagW5^HQnQ z0pWpoQpxg8<&gx9im(>=x6v#&RbQ7^AsjxeSDA? zi4MEJUC~ByG!PiBjq7$pK&FA^5 z=Y@dtQnuy%IfsaR`TVP0q^3mixl&J-3!$H!ua#{A>0Z1JdLq#d4UV9nlYm641ZHl zH6mK~iI6lR3OUEVL}Z5{ONZ_6{Nk%Bv03ag<1HVN?R%w2^aR5@E>6(r>}IoMl$wRF zWr-DItN*k7T$NTT8B)+23c?171sADhjInb2Xb>GhFYGC&3{b>huvLlaS4O z^{j5q+b5H?Z)yuy%AByaVl2yj9cnalY1sMQ zXI#e%*CLajxGxP!K6xf9RD2pMHOfAa1d^Lr6kE`IBpxOiGXfNcoQ*FI6wsNtLD!T+ zC4r2q>5qz0f}UY^RY#1^0*FPO*Zp-U1h9U|qWjwqJaDB(pZ`<`U-xo7+JB$zvwV}^ z2>$0&Q5k#l|Er7*PPG1ycj4BGz zg&`d*?nUi1Q!OB>{V@T$A;)8@h;*Rb1{xk_8X<34L`s}xkH-rQZvjM`jI=jaJRGRg zeEcjYChf-78|RLrao%4HyZBfnAx5KaE~@Sx+o-2MLJ>j-6uDb!U`odj*=)0k)K75l zo^)8-iz{_k7-_qy{Ko~N#B`n@o#A22YbKiA>0f3k=p-B~XX=`Ug>jl$e7>I=hph0&AK z?ya;(NaKY_!od=tFUcGU5Kwt!c9EPUQLi;JDCT*{90O@Wc>b| zI;&GIY$JlQW^9?R$-OEUG|3sp+hn+TL(YK?S@ZW<4PQa}=IcUAn_wW3d!r#$B}n08 z*&lf(YN21NDJ74DqwV`l`RX(4zJ<(E4D}N0@QaE-hnfdPDku~@yhb^AeZL73RgovX z6=e>!`&e^l@1WA5h!}}PwwL*Gjg!LbC5g0|qb8H$^S{eGs%cc?4vTyVFW=s6KtfW? z@&Xm+E(uz(qDbwDvRQI9DdB<2sW}FYK9sg*f%-i*>*n{t-_wXvg~N7gM|a91B!x|K zyLbJ~6!!JZpZ`#HpCB8g#Q*~VU47Rp$NyZb3WhEgg3ivSwnjGJgi0BEV?!H}Z@QF| zrO`Kx*52;FR#J-V-;`oR-pr!t>bYf)UYcixN=(FUR6$fhN@~i09^3WeP3*)D*`*mJ z1u%klAbzQ=P4s%|FnVTZv%|@(HDB+ap5S#cFSJUSGkyI*Y>9Lwx|0lTs%uhoCW(f1 zi+|a9;vDPfh3nS<7m~wqTM6+pEm(&z-Ll;lFH!w#(Uk#2>Iv~2Hu}lITn7hnOny`~ z*Vj=r<&Nwpq^@g5m`u&QTBRoK*}plAuHg$L$~NO#wF0!*r0OfcS%)k0A??uY*@B^C zJe9WdU(w){rTIf<;rwJt^_35^d<A@$FqEZW6kwyfAo2x0T$Ye2MZox6Z7<%Qbu$}}u{rtE+h2M+Z}T4I zxF1cwJ(Uvp!T#mogWkhb(?SxD4_#tV(Sc8N4Gu*{Fh#})Pvb^ef%jrlnG*&Ie+J5 zsly5oo?1((um&lLDxn(DkYtk`My>lgKTp3Y4?hTQ4_`YNOFtjF-FUY#d#(EQd(rfz zB8z%Vi;?x)ZM$3c>yc5H8KBvSevnWNdCbAj?QCac)6-K~Xz@EZp}~N9q)5*Ufjz3C z6kkOeI{3H(^VO8hKDrVjy2DXd;5wr4nb`19yJi0DO@607MSx+7F$ zz3F7sl8JV@@sM$6`#JmSilqI%Bs)}Py2eFT;TjcG5?8$zwV60b(_5A>b#uk~7U^bO z>y|6SCrP2IGST(8HFuX|XQUXPLt2gL_hm|uj1Ws`O2VW>SyL^uXkl>Zvkcpi?@!F7 z%svLoT@{R#XrIh^*dE~$YhMwC+b7JE09NAS47kT%Ew zD!XjxA@1+KOAyu`H2z#h+pGm!lG>WI0v745l+Fd><3dh{ATq%h?JSdEt zu%J*zfFUx%Tx&0DS5WSbE)vwZSoAGT=;W#(DoiL($BcK;U*w`xA&kheyMLI673HCb7fGkp{_vdV2uo;vSoAH z9BuLM#Vzwt#rJH>58=KXa#O;*)_N{$>l7`umacQ0g$pI3iW4=L--O;Wiq0zy7OKp`j2r^y3`7X!?sq9rr5B{41BkBr1fEd1#Q3 z-dXc2RSb4U>FvpVhlQCIzQ-hs=8420z=7F2F(^xD;^RXgpjlh8S6*xCP#Gj2+Q0bAg?XARw3dnlQ*Lz3vk}m`HXmCgN=?bIL{T zi}Ds-xn|P)dxhraT@XY$ZQ&^%x8y!o+?n#+>+dZ1c{hYwNTNRke@3enT(a@}V*X{! z81+{Jc2UR;+Zcbc6cUlafh4DFKwp>;M}8SGD+YnW3Q_)*9Z_pny_z+MeYQmz?r%EVaN0d!NE*FVPq&U@vo{ef6wkMIDEWLbDs zz91$($XbGnQ?4WHjB~4xgPgKZts{p|g1B{-4##}#c5aL5C6_RJ_(*5>85B1}U!_<``}q-97Q7~u)(&lsb(WT^(*n7H%33%@_b zO5(?-v??s??33b19xiB7t_YT!q8!qAzN1#RD@3;kYAli%kazt#YN7}MhVu=ljuz27 z1`<+g8oVwy57&$`CiHeaM)tz(OSt4E# zJ@P6E*e504oUw~RD(=9WP8QdW^6wRdFbKII!GAWecJ(?{`EzTR@?j!3g?$@LLCt;U={>!9z7DU!(1Jq zqEwdx5q?W1Ncm7mXP8MFwAr?nw5$H%cb>Q><9j{Tk2RY9ngGvaJgWXx^r!ywk{ph- zs2PFto4@IIwBh{oXe;yMZJYlS?3%a-CJ#js90hoh5W5d^OMwCFmpryHFr|mG+*ZP$ zqyS5BW@s}|3xUO0PR<^{a2M(gkP5BDGxvkWkPudSV*TMRK5Qm4?~VuqVAOerffRt$HGAvp;M++Iq$E6alB z;ykBr-eZ6v_H^1Wip56Czj&=`mb^TsX|FPN#-gnlP03AkiJDM=?y|LzER1M93R4sC z*HT(;EV=*F*>!+Z{r!KG?6ODMGvkt3viG=@kQJHNMYd}bS4KrrHf4`&*(0m0R5Hqz zEk)r=sFeS?MZRvn<@Z0&bDw)XkMnw+_xqgp=W{;ioX`6;G-P9N%wfoYJ$-m$L#MC% z^sH?tSzA|WWP(cN3({~_*X$l{M*;1V{l$;T6b){#l4pswDTid26HaXgKed}13YIP= zJRvA3nmx{}R$Lr&S4!kWU3`~dxM}>VXWu6Xd(VP}z1->h&f%82eXD_TuTs@=c;l0T z|LHmWKJ+?7hkY=YM>t}zvb4|lV;!ARMtWFp!E^J=Asu9w&kVF*i{T#}sY++-qnVh! z5TQ|=>)+vutf{&qB+LO9^jm#rD7E5+tcorr^Fn5Xb0B;)f^$7Ev#}G_`r==ea294V z--v4LwjswWlSq9ba6i?IXr8M_VEGQ$H%hCqJTFQ3+1B9tmxDUhnNU%dy4+zbqYJ|o z3!N{b?A@{;cG2~nb-`|z;gEDL5ffF@oc3`R{fGi)0wtMqEkw4tRX3t;LVS3-zAmg^ zgL7Z{hmdPSz9oA@t>tZ1<|Khn&Lp=_!Q=@a?k+t~H&3jN?dr(}7s;{L+jiKY57?WsFBfW^mu6a03_^VKrdK=9egXw@!nzZ3TbYc*osyQNoCXPYoFS<&Nr97MrQCOK(gO8 z;0@iqRTJy4-RH)PJld5`AJN}n?5r^-enKrHQOR;z>UMfm+e8~4ZL5k>oXMiYq12Bx4eVQv0jFgp_zC#``sjZpywYqISMP}VZ@!~1Mf$!x|opj%mQ98JnSk@`~ zPmmyuPZKtZOnEC!1y!?`TYRsZ!II;d!iln}%e}bk5qIiUADERr*K$3dekgHV9TtBX zi5q!J!6Zgd#cLxRmZN^J`o@Zv{+p+<_#8^nvY)44Hw_2i@?R&5n^q33fpOnDg1nPQ z_r<$hURl~OketX|Tdbvf_7=3x^rSFJtEp@tuDpVB&uq)qW;xUQ7mmkr-@eZwa$l+? zoKk``Vz@TH#>jMce*8>@FZ+@BEUdYa_K0i|{*;j9MW3K%pnM*T;@>|o@lMhgLrpZP5aol(z>g;b4}|e$U~Fn zGL%(}p%Jsl4LxE!VW_Y4T>e}W4e#~F03H_^R!Q)kpJG{lO!@I4{mFo^V#ayHh_5~o zB$O71gcE(G@6xv);#Ky?e(Ed}^O+Ho(t=93T9T3TnEY(OVf_dR-gY@jj+iJSY?q|6prBv(S9A4k=2fNZz!W@S=B@~b?TJRTuBQq448@juN#Y=3q=^VCF>Z}n6wICJ<^^Kn8C;mK zZYiFSN#Z$?NDGV7(#}q2tAZAtE63icK-MY>UQu4MWlGIbJ$AF8Zt-jV;@7P5MPI>% zPWvO!t%1+s>-A%`;0^o8Ezeaa4DMwI8ooQrJ;ax@Qt*6XONWw)dPwOPI9@u*EG&844*1~EoZ2qsAe~M>d`;Bc_CWY zMoDKEmDh-}k9d6*<0g@aQmsnrM1H9IcKYZs)><)d92{|0Hh8?~XbF)7U+UmP@Pw_6geVB?7N$4J4*E0z3EO&5kRS(EE zv92(+e5WxLXMN{h;-|8@!Q#0q247hb^3R%*k3MuMO5*L}$0D#5P*N$aHd54C+=_RToYXTyewugOaDmGsCvb4H1s=@gkfVnzTCWKMa-Mm1v4Wq!t-JIrbV&EWwKDe ze#kJpOq#iRlFz%5#6Fio9IUlKnQ#X&DY8Ux#<-WqxAac-y%U_L+EZZ4Rg5*yNg`f< zSZn&uio@zanUCPqX1l4W&B!;UWs#P7B^|4WwoCxQXl|44n^cBNqu=3Vl*ltAqsUQO z9q_@nD0zq0O8r`coEm>9+|rA3HL#l}X;0##>SJS$cVavOZVCpSGf4mUU1( zWaRCUYc^9QbG9=vpWo%xP}CMFnMb{reA`K7tT(t5DM)d9l}jVPY>qoRzT zE3m-p#=i=$9x*CB`AL>SY}u3agYFl#uULNen#&44H;!L@I{RI=PlWxG8J((f)ma7A z@jLvQ>?Nx`n?3ChRG#HqE3MXP8*o3!Qq`+t8EMt_p)oeKHqPusBxPn!#?R??-=e3e zo73WNs_IZF`WLigre=|`aS2^> zN1zn!7k&Dh28t%VpJ%**&E!eAcB5oLjQFFcJQj*URMia%Ya3@q1UQ18=oWMM6`I}iT_&L1gl?*~6nU4q4Z0`H<5yDp(HeZ+RGf9`mM&= zn-qRp%i!g$R;i1d1aMZ{IewNjE@p2+Z{`x{*xL*x$?WV~{BjJpsP&C&JK0HLoyf z`0z^v&fBQSa!I7FU~9MaQ%e|?RP>sM^2PL!mE^Q1Ig_4M$5BRfi72oMYu6Ke?wmDX z@0a%-V|z}b23K=ye(W+fG#w|jJUnT{=KR5jfuq!RX}<1irTDw(${<&}dWQu4;EuE< z@3u4dBkQaCHHM&;cE0z50_V!(vJ1_V)A8?C#eJuLkt!98Z%|Bgzidc0j|z(&o)TCzYlrgZA zC3@i>L!&Gw_~7`>puB97I2lK)lESZQqVXc_8T^G2O#VHhO?IC$g zOYhXJ7)~C<8l|Xrftka@QuowScM{K&0zskoU$Aw~vIRVRF9TEQ4*3=_5)98B`=t8(N%ZuWqmwlW zllAzq=E5_5!sKDXam@w`ZD(nl%LAPxQuEtDcKPqu9LPJvNIITawU#c^PQ2HmZgs)r zH^+gRwZ?0)8IFQgU)+p@0Iqb^tcEoqcB@zhfz_FaOM&_d<|jnU>q5nSKa<@%9|dje zIupcg1!tRiMP4X=oG<7s4|AW&^-Cw4FL9OuI$t zxjc*y;Uw!G7a|jz>E*2+PlR(CemWebS7m-&*CDwnmxbiRqJvQ&os-sC&4OWt^(2@vG4|jui#Df@-D= zh3D%8Y3R6+jRBStSvH9pt&tCI`NK08J1*pC(?OM0h!bS-JK3I}`pDY-fDIaB_*W6KS+TO0Q*%kkeuN6uWITt=TsCGw6uBE710q; zRluI%j{?@jwhM|l5&TB!-TkQs!A=DXRE>u18t@;zndD0M$U@Igrt?UW2; z7%=dsHIVH_LCkGUU0fW&UMjDnvjcc0Mp(mK&;d~ZJ5EJ)#7@aTZvGDFXzFZg2Lq~s z5PR_LazNN)JD5K_uK*Hy{mXuHTkGGv|9V8KP#iQ$3!G*^>7UiE{|1G1A-qg(xH;Xa>&%f|BZkH zG=J^0pHzSAqv5*5ysQ{Puy^-_|IPrii zKS$mE10Zngf>Sgg@BjpRyJbrHeo zD8Ro0LI*W#+9?^xlOS^c>Z^^n^0I|FH^@^`ZR`{H=$ zjO0_$cnpBM7Zcm?H_RXIu-Lu~qweDSV|tEZBZh!e6hQy->}e;d#osZ1hQj{HhHkC0 zJ|F-HKmeTGgDe979ogBz24;@<|I7;TU!IXb@oWMsMECIETmQy`zPtM`|NP}PjzR_u zKMG1Z{%1kWeMfEf(10U#w!clmQ2)JC8zm(Fv!H4dUHQHCFLikID?hrd{0>kCQt?kP zdqn2ZG0}ytcQJ7t_B3s0ZvH3PYjkjQ`Q%;jV@?MK-+z3etBCGGo4f4`y^|AdCs!DH zThTQ;cL5dM{|tB_1y6K3bVa^hx_<9J(}5`2SDz1^0bT!Vm*JV;9~t&{IC{$DUAVV* z{|E=#yN{wNdTY@$6z{_KNA3&%w|vFu1n9XRcM0Ak>`UW!lQ`ah3D4r%}Z diff --git a/benchmark-overhead/gradle/wrapper/gradle-wrapper.properties b/benchmark-overhead/gradle/wrapper/gradle-wrapper.properties index 37aef8d3f0c9..8e876e1c5571 100644 --- a/benchmark-overhead/gradle/wrapper/gradle-wrapper.properties +++ b/benchmark-overhead/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,8 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-bin.zip +distributionSha256Sum=1541fa36599e12857140465f3c91a97409b4512501c26f9631fb113e392c5bd1 +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.1-bin.zip networkTimeout=10000 +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/benchmark-overhead/gradlew b/benchmark-overhead/gradlew index 79a61d421cc4..f5feea6d6b11 100755 --- a/benchmark-overhead/gradlew +++ b/benchmark-overhead/gradlew @@ -15,6 +15,8 @@ # See the License for the specific language governing permissions and # limitations under the License. # +# SPDX-License-Identifier: Apache-2.0 +# ############################################################################## # @@ -55,7 +57,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. @@ -83,10 +85,9 @@ done # This is normally unused # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} -APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit - -# 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"' +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s +' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum @@ -133,10 +134,13 @@ 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. + if ! command -v java >/dev/null 2>&1 + then + 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 fi # Increase the maximum file descriptors if we can. @@ -144,7 +148,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 + # shellcheck disable=SC2039,SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac @@ -152,7 +156,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then '' | soft) :;; #( *) # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 + # shellcheck disable=SC2039,SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac @@ -197,11 +201,15 @@ if "$cygwin" || "$msys" ; then done fi -# Collect all arguments for the java command; -# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of -# shell script including quotes and variable substitutions, so put them in -# double quotes to make sure that they get re-expanded; and -# * put everything else in single quotes, so that it's not re-expanded. + +# 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"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ diff --git a/benchmark-overhead/gradlew.bat b/benchmark-overhead/gradlew.bat index 93e3f59f135d..9d21a21834d5 100644 --- a/benchmark-overhead/gradlew.bat +++ b/benchmark-overhead/gradlew.bat @@ -13,6 +13,8 @@ @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem @if "%DEBUG%"=="" @echo off @rem ########################################################################## @@ -43,11 +45,11 @@ set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if %ERRORLEVEL% equ 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. +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail @@ -57,11 +59,11 @@ 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. +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail diff --git a/benchmark-overhead/src/test/java/io/opentelemetry/OverheadTests.java b/benchmark-overhead/src/test/java/io/opentelemetry/OverheadTests.java index b69a6d708294..86756a230f39 100644 --- a/benchmark-overhead/src/test/java/io/opentelemetry/OverheadTests.java +++ b/benchmark-overhead/src/test/java/io/opentelemetry/OverheadTests.java @@ -110,7 +110,7 @@ void runAppOnce(TestConfig config, Agent agent) throws Exception { } private void startRecording(Agent agent, GenericContainer petclinic) throws Exception { - Path outFile = namingConventions.container.jfrFile(agent); + String outFile = namingConventions.container.jfrFile(agent); String[] command = { "jcmd", "1", @@ -123,16 +123,27 @@ private void startRecording(Agent agent, GenericContainer petclinic) throws E petclinic.execInContainer(command); } - private void doWarmupPhase(TestConfig testConfig, GenericContainer petclinic) throws IOException, InterruptedException { - System.out.println("Performing startup warming phase for " + testConfig.getWarmupSeconds() + " seconds..."); + private void doWarmupPhase(TestConfig testConfig, GenericContainer petclinic) + throws IOException, InterruptedException { + System.out.println( + "Performing startup warming phase for " + testConfig.getWarmupSeconds() + " seconds..."); // excluding the JFR recording from the warmup causes strange inconsistencies in the results System.out.println("Starting disposable JFR warmup recording..."); - String[] startCommand = {"jcmd", "1", "JFR.start", "settings=/app/overhead.jfc", "dumponexit=true", "name=warmup", "filename=warmup.jfr"}; + String[] startCommand = { + "jcmd", + "1", + "JFR.start", + "settings=/app/overhead.jfc", + "dumponexit=true", + "name=warmup", + "filename=warmup.jfr" + }; petclinic.execInContainer(startCommand); - long deadline = System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(testConfig.getWarmupSeconds()); - while(System.currentTimeMillis() < deadline) { + long deadline = + System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(testConfig.getWarmupSeconds()); + while (System.currentTimeMillis() < deadline) { GenericContainer k6 = new GenericContainer<>(DockerImageName.parse("loadimpact/k6")) .withNetwork(NETWORK) @@ -151,7 +162,7 @@ private void doWarmupPhase(TestConfig testConfig, GenericContainer petclinic) private void writeStartupTimeFile(Agent agent, long start) throws IOException { long delta = System.currentTimeMillis() - start; - Path startupPath = namingConventions.local.startupDurationFile(agent); + Path startupPath = Path.of(namingConventions.local.startupDurationFile(agent)); Files.writeString(startupPath, String.valueOf(delta)); } } diff --git a/benchmark-overhead/src/test/java/io/opentelemetry/containers/K6Container.java b/benchmark-overhead/src/test/java/io/opentelemetry/containers/K6Container.java index ff022970dbb3..7f6994d1a5d7 100644 --- a/benchmark-overhead/src/test/java/io/opentelemetry/containers/K6Container.java +++ b/benchmark-overhead/src/test/java/io/opentelemetry/containers/K6Container.java @@ -8,7 +8,6 @@ import io.opentelemetry.agents.Agent; import io.opentelemetry.config.TestConfig; import io.opentelemetry.util.NamingConventions; -import java.nio.file.Path; import java.time.Duration; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -35,7 +34,7 @@ public K6Container( } public GenericContainer build() { - Path k6OutputFile = namingConventions.container.k6Results(agent); + String k6OutputFile = namingConventions.container.k6Results(agent); return new GenericContainer<>(DockerImageName.parse("loadimpact/k6")) .withNetwork(network) .withNetworkAliases("k6") @@ -52,7 +51,7 @@ public GenericContainer build() { "--rps", String.valueOf(config.getMaxRequestRate()), "--summary-export", - k6OutputFile.toString(), + k6OutputFile, "/app/basic.js") .withStartupCheckStrategy( new OneShotStartupCheckStrategy().withTimeout(Duration.ofMinutes(15))); diff --git a/benchmark-overhead/src/test/java/io/opentelemetry/results/ResultsCollector.java b/benchmark-overhead/src/test/java/io/opentelemetry/results/ResultsCollector.java index c2012cdf41b9..d7b47bb7fbc9 100644 --- a/benchmark-overhead/src/test/java/io/opentelemetry/results/ResultsCollector.java +++ b/benchmark-overhead/src/test/java/io/opentelemetry/results/ResultsCollector.java @@ -54,14 +54,14 @@ private AppPerfResults readAgentResults(Agent agent, TestConfig config) { private AppPerfResults.Builder addStartupTime(AppPerfResults.Builder builder, Agent agent) throws IOException { - Path file = namingConvention.startupDurationFile(agent); + Path file = Path.of(namingConvention.startupDurationFile(agent)); long startupDuration = Long.parseLong(new String(Files.readAllBytes(file)).trim()); return builder.startupDurationMs(startupDuration); } private AppPerfResults.Builder addK6Results(AppPerfResults.Builder builder, Agent agent) throws IOException { - Path k6File = namingConvention.k6Results(agent); + Path k6File = Path.of(namingConvention.k6Results(agent)); String json = new String(Files.readAllBytes(k6File)); double iterationAvg = read(json, "$.metrics.iteration_duration.avg"); double iterationP95 = read(json, "$.metrics.iteration_duration['p(95)']"); @@ -82,7 +82,7 @@ private static double read(String json, String jsonPath) { private AppPerfResults.Builder addJfrResults(AppPerfResults.Builder builder, Agent agent) throws IOException { - Path jfrFile = namingConvention.jfrFile(agent); + Path jfrFile = Path.of(namingConvention.jfrFile(agent)); return builder .totalGCTime(readTotalGCTime(jfrFile)) .totalAllocated(readTotalAllocated(jfrFile)) diff --git a/benchmark-overhead/src/test/java/io/opentelemetry/util/ContainerNamingConvention.java b/benchmark-overhead/src/test/java/io/opentelemetry/util/ContainerNamingConvention.java new file mode 100644 index 000000000000..8866ea28e92c --- /dev/null +++ b/benchmark-overhead/src/test/java/io/opentelemetry/util/ContainerNamingConvention.java @@ -0,0 +1,33 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.util; + +import io.opentelemetry.agents.Agent; + +public class ContainerNamingConvention implements NamingConvention { + private final String dir; + + public ContainerNamingConvention(String dir) { + this.dir = dir; + } + + public String k6Results(Agent agent) { + + return String.join("/", dir, "k6_out_" + agent.getName() + ".json"); + } + + public String jfrFile(Agent agent) { + return String.join("/", dir, "petclinic-" + agent.getName() + ".jfr"); + } + + public String startupDurationFile(Agent agent) { + return String.join("/", dir, "startup-time-" + agent.getName() + ".txt"); + } + + public String root() { + return dir; + } +} diff --git a/benchmark-overhead/src/test/java/io/opentelemetry/util/LocalNamingConvention.java b/benchmark-overhead/src/test/java/io/opentelemetry/util/LocalNamingConvention.java new file mode 100644 index 000000000000..4c4305e849a3 --- /dev/null +++ b/benchmark-overhead/src/test/java/io/opentelemetry/util/LocalNamingConvention.java @@ -0,0 +1,33 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.util; + +import io.opentelemetry.agents.Agent; +import java.nio.file.Paths; + +public class LocalNamingConvention implements NamingConvention { + private final String dir; + + public LocalNamingConvention(String dir) { + this.dir = dir; + } + + public String k6Results(Agent agent) { + return Paths.get(dir, "k6_out_" + agent.getName() + ".json").toString(); + } + + public String jfrFile(Agent agent) { + return Paths.get(dir, "petclinic-" + agent.getName() + ".jfr").toString(); + } + + public String startupDurationFile(Agent agent) { + return Paths.get(dir, "startup-time-" + agent.getName() + ".txt").toString(); + } + + public String root() { + return dir; + } +} diff --git a/benchmark-overhead/src/test/java/io/opentelemetry/util/NamingConvention.java b/benchmark-overhead/src/test/java/io/opentelemetry/util/NamingConvention.java index 96a45eceeef3..afbdd653e9a5 100644 --- a/benchmark-overhead/src/test/java/io/opentelemetry/util/NamingConvention.java +++ b/benchmark-overhead/src/test/java/io/opentelemetry/util/NamingConvention.java @@ -6,51 +6,34 @@ package io.opentelemetry.util; import io.opentelemetry.agents.Agent; -import java.nio.file.Path; -import java.nio.file.Paths; /** * This utility class provides the standard file naming conventions, primarily for files that are * shared between containers and the test runner. It consolidates the naming logic into one place to * ensure consistency, reduce duplication, and decrease errors. */ -public class NamingConvention { - - private final String dir; - - public NamingConvention(String dir) { - this.dir = dir; - } - +public interface NamingConvention { /** - * Returns a path to the location of the k6 results json file. + * Returns a path string to the location of the k6 results json file. * * @param agent The agent to get results file path for */ - public Path k6Results(Agent agent) { - return Paths.get(dir, "k6_out_" + agent.getName() + ".json"); - } + String k6Results(Agent agent); /** - * Returns a path to the location of the jfr output file for a given agent run. + * Returns a path string to the location of the jfr output file for a given agent run. * * @param agent The agent to get the jfr file path for. */ - public Path jfrFile(Agent agent) { - return Paths.get(dir, "petclinic-" + agent.getName() + ".jfr"); - } + String jfrFile(Agent agent); /** - * Returns the path to the file that contains the startup duration for a given agent run. + * Returns the path string to the file that contains the startup duration for a given agent run. * * @param agent The agent to get the startup duration for. */ - public Path startupDurationFile(Agent agent) { - return Paths.get(dir, "startup-time-" + agent.getName() + ".txt"); - } + String startupDurationFile(Agent agent); /** Returns the root path that this naming convention was configured with. */ - public String root() { - return dir; - } + String root(); } diff --git a/benchmark-overhead/src/test/java/io/opentelemetry/util/NamingConventions.java b/benchmark-overhead/src/test/java/io/opentelemetry/util/NamingConventions.java index 6caf28dfef31..4aeca8f7a92e 100644 --- a/benchmark-overhead/src/test/java/io/opentelemetry/util/NamingConventions.java +++ b/benchmark-overhead/src/test/java/io/opentelemetry/util/NamingConventions.java @@ -5,18 +5,22 @@ package io.opentelemetry.util; -/** An container to hold both the local and container naming conventions. */ +/** A container to hold both the local and container naming conventions. */ public class NamingConventions { - public final NamingConvention container = new NamingConvention("/results"); - public final NamingConvention local = new NamingConvention("."); + public final NamingConvention container = new ContainerNamingConvention("/results"); + public final NamingConvention local = new LocalNamingConvention("."); - /** @return Root path for the local naming convention (where results are output) */ + /** + * @return Root path for the local naming convention (where results are output) + */ public String localResults() { return local.root(); } - /** @return Root path for the container naming convention (where results are output) */ + /** + * @return Root path for the container naming convention (where results are output) + */ public String containerResults() { return container.root(); } diff --git a/benchmark-overhead/src/test/resources/collector.yaml b/benchmark-overhead/src/test/resources/collector.yaml index 053e2658734e..99e635ec335c 100644 --- a/benchmark-overhead/src/test/resources/collector.yaml +++ b/benchmark-overhead/src/test/resources/collector.yaml @@ -1,10 +1,14 @@ extensions: health_check: + endpoint: 0.0.0.0:13133 receivers: otlp: protocols: grpc: + endpoint: 0.0.0.0:4317 + http: + endpoint: 0.0.0.0:4318 processors: batch: @@ -18,16 +22,16 @@ exporters: service: pipelines: traces: - receivers: [ otlp ] - processors: [ batch ] - exporters: [ logging/logging_info ] + receivers: [otlp] + processors: [batch] + exporters: [logging/logging_info] metrics: - receivers: [ otlp ] - processors: [ batch ] - exporters: [ logging/logging_info ] + receivers: [otlp] + processors: [batch] + exporters: [logging/logging_info] logs: - receivers: [ otlp ] - processors: [ batch ] - exporters: [ logging/logging_info ] + receivers: [otlp] + processors: [batch] + exporters: [logging/logging_info] - extensions: [ health_check ] + extensions: [health_check] diff --git a/bom-alpha/build.gradle.kts b/bom-alpha/build.gradle.kts index f1860b014e02..f745f44ab731 100644 --- a/bom-alpha/build.gradle.kts +++ b/bom-alpha/build.gradle.kts @@ -14,6 +14,13 @@ dependencies { api(platform("io.opentelemetry:opentelemetry-bom")) api(platform("io.opentelemetry:opentelemetry-bom-alpha")) api(platform(project(":bom"))) + + // Get the semconv version from :dependencyManagement + val semconvConstraint = project(":dependencyManagement").dependencyProject.configurations["api"].allDependencyConstraints + .find { it.group.equals("io.opentelemetry.semconv") + && it.name.equals("opentelemetry-semconv") } + ?: throw Exception("semconv constraint not found") + otelBom.addExtra(semconvConstraint.group, semconvConstraint.name, semconvConstraint.version ?: throw Exception("missing version")) } otelBom.projectFilter.set { it.findProperty("otel.stable") != "true" } diff --git a/build.gradle.kts b/build.gradle.kts index e7c66f38f7a1..36f6b373affd 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -3,9 +3,17 @@ import java.time.Duration plugins { id("idea") - id("com.github.ben-manes.versions") id("io.github.gradle-nexus.publish-plugin") id("otel.spotless-conventions") + /* workaround for + What went wrong: + Could not determine the dependencies of task ':smoke-tests-otel-starter:spring-boot-3.2:bootJar'. + > Could not create task ':smoke-tests-otel-starter:spring-boot-3.2:collectReachabilityMetadata'. + > Cannot set the value of task ':smoke-tests-otel-starter:spring-boot-3.2:collectReachabilityMetadata' property 'metadataService' of type org.graalvm.buildtools.gradle.internal.GraalVMReachabilityMetadataService using a provider of type org.graalvm.buildtools.gradle.internal.GraalVMReachabilityMetadataService. + + See https://github.com/gradle/gradle/issues/17559#issuecomment-1327991512 + */ + id("org.graalvm.buildtools.native") apply false } apply(from = "version.gradle.kts") @@ -41,56 +49,56 @@ if (project.findProperty("skipTests") as String? == "true") { } } -tasks { - val listTestsInPartition by registering { - group = "Help" - description = "List test tasks in given partition" - - // total of 4 partitions (see modulo 4 below) - var testPartition = (project.findProperty("testPartition") as String?)?.toInt() - if (testPartition == null) { - throw GradleException("Test partition must be specified") - } else if (testPartition < 0 || testPartition >= 4) { - throw GradleException("Invalid test partition") - } +if (gradle.startParameter.taskNames.any { it.equals("listTestsInPartition") }) { + tasks { + val listTestsInPartition by registering { + group = "Help" + description = "List test tasks in given partition" - val partitionTasks = ArrayList() - var testPartitionCounter = 0 - subprojects { - // relying on predictable ordering of subprojects - // (see https://docs.gradle.org/current/dsl/org.gradle.api.Project.html#N14CB4) - // since we are splitting these tasks across different github action jobs - val enabled = testPartitionCounter++ % 4 == testPartition - if (enabled) { - tasks.withType().configureEach { - partitionTasks.add(this) + // total of 4 partitions (see modulo 4 below) + var testPartition = (project.findProperty("testPartition") as String?)?.toInt() + if (testPartition == null) { + throw GradleException("Test partition must be specified") + } else if (testPartition < 0 || testPartition >= 4) { + throw GradleException("Invalid test partition") + } + + val partitionTasks = ArrayList() + var testPartitionCounter = 0 + subprojects { + // relying on predictable ordering of subprojects + // (see https://docs.gradle.org/current/dsl/org.gradle.api.Project.html#N14CB4) + // since we are splitting these tasks across different github action jobs + val enabled = testPartitionCounter++ % 4 == testPartition + if (enabled) { + tasks.withType().configureEach { + partitionTasks.add(this) + } } } - } - doLast { - File("test-tasks.txt").printWriter().use { writer -> - partitionTasks.forEach { task -> - var taskPath = task.project.path + ":" + task.name - // smoke tests are run separately - // :instrumentation:test runs all instrumentation tests - if (taskPath != ":smoke-tests:test" && taskPath != ":instrumentation:test") { - writer.println(taskPath) + doLast { + File("test-tasks.txt").printWriter().use { writer -> + partitionTasks.forEach { task -> + var taskPath = task.project.path + ":" + task.name + // smoke tests are run separately + // :instrumentation:test runs all instrumentation tests + if (taskPath != ":smoke-tests:test" && taskPath != ":instrumentation:test") { + writer.println(taskPath) + } } } } - } - // disable all tasks to stop build - subprojects { - tasks.configureEach { - enabled = false + // disable all tasks to stop build + subprojects { + tasks.configureEach { + enabled = false + } } } } -} -if (gradle.startParameter.taskNames.any { it.equals("listTestsInPartition") }) { // disable all tasks to stop build project.tasks.configureEach { if (this.name != "listTestsInPartition") { diff --git a/buildscripts/checkstyle.xml b/buildscripts/checkstyle.xml index 58d67e2c2a25..a3a4ff7d3512 100644 --- a/buildscripts/checkstyle.xml +++ b/buildscripts/checkstyle.xml @@ -50,6 +50,18 @@ --> + + + + + + + + @@ -335,7 +347,7 @@ value="CLASS_DEF, INTERFACE_DEF, ENUM_DEF, METHOD_DEF, CTOR_DEF, VARIABLE_DEF"/> - + diff --git a/buildscripts/dependency-check-suppressions.xml b/buildscripts/dependency-check-suppressions.xml index 694a465537d7..af431800c05b 100644 --- a/buildscripts/dependency-check-suppressions.xml +++ b/buildscripts/dependency-check-suppressions.xml @@ -6,4 +6,11 @@ ^pkg:maven/io\.opentelemetry[./].* ^CVE-.* + + + ^pkg:maven/com\.google\.cloud\.opentelemetry/detector-resources-support@.* + CVE-2023-43810 + CVE-2023-45142 + CVE-2023-47108 + diff --git a/conventions/build.gradle.kts b/conventions/build.gradle.kts index 585ae4cd0c18..f7fbfa738e68 100644 --- a/conventions/build.gradle.kts +++ b/conventions/build.gradle.kts @@ -1,7 +1,7 @@ plugins { `kotlin-dsl` // When updating, update below in dependencies too - id("com.diffplug.spotless") version "6.19.0" + id("com.diffplug.spotless") version "6.25.0" } spotless { @@ -54,24 +54,25 @@ dependencies { implementation("org.apache.maven:maven-aether-provider:3.3.9") // When updating, update above in plugins too - implementation("com.diffplug.spotless:spotless-plugin-gradle:6.19.0") - implementation("com.google.guava:guava:32.0.1-jre") + implementation("com.diffplug.spotless:spotless-plugin-gradle:6.25.0") + implementation("com.google.guava:guava:33.3.0-jre") implementation("gradle.plugin.com.google.protobuf:protobuf-gradle-plugin:0.8.18") - implementation("com.github.johnrengelman:shadow:8.1.1") + implementation("com.gradleup.shadow:shadow-gradle-plugin:8.3.1") implementation("org.apache.httpcomponents:httpclient:4.5.14") - implementation("com.gradle.enterprise:com.gradle.enterprise.gradle.plugin:3.13.4") - implementation("org.owasp:dependency-check-gradle:8.2.1") - implementation("ru.vyarus:gradle-animalsniffer-plugin:1.7.0") + implementation("com.gradle.develocity:com.gradle.develocity.gradle.plugin:3.18.1") + implementation("org.owasp:dependency-check-gradle:10.0.4") + implementation("ru.vyarus:gradle-animalsniffer-plugin:1.7.1") + implementation("org.spdx:spdx-gradle-plugin:0.8.0") // When updating, also update dependencyManagement/build.gradle.kts - implementation("net.bytebuddy:byte-buddy-gradle-plugin:1.14.5") - implementation("gradle.plugin.io.morethan.jmhreport:gradle-jmh-report:0.9.0") - implementation("me.champeau.jmh:jmh-gradle-plugin:0.7.1") - implementation("net.ltgt.gradle:gradle-errorprone-plugin:3.1.0") - implementation("net.ltgt.gradle:gradle-nullaway-plugin:1.6.0") - implementation("me.champeau.gradle:japicmp-gradle-plugin:0.4.1") + implementation("net.bytebuddy:byte-buddy-gradle-plugin:1.15.1") + implementation("gradle.plugin.io.morethan.jmhreport:gradle-jmh-report:0.9.6") + implementation("me.champeau.jmh:jmh-gradle-plugin:0.7.2") + implementation("net.ltgt.gradle:gradle-errorprone-plugin:4.0.1") + implementation("net.ltgt.gradle:gradle-nullaway-plugin:2.0.0") + implementation("me.champeau.gradle:japicmp-gradle-plugin:0.4.3") - testImplementation(enforcedPlatform("org.junit:junit-bom:5.9.3")) + testImplementation(enforcedPlatform("org.junit:junit-bom:5.11.0")) testImplementation("org.junit.jupiter:junit-jupiter-api") testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine") - testImplementation("org.assertj:assertj-core:3.24.2") + testImplementation("org.assertj:assertj-core:3.26.3") } diff --git a/conventions/src/main/kotlin/io.opentelemetry.instrumentation.base.gradle.kts b/conventions/src/main/kotlin/io.opentelemetry.instrumentation.base.gradle.kts index ec47814c6998..1ed0dab1e620 100644 --- a/conventions/src/main/kotlin/io.opentelemetry.instrumentation.base.gradle.kts +++ b/conventions/src/main/kotlin/io.opentelemetry.instrumentation.base.gradle.kts @@ -34,17 +34,26 @@ extra["testLatestDeps"] = testLatestDeps abstract class TestLatestDepsRule : ComponentMetadataRule { override fun execute(context: ComponentMetadataContext) { val version = context.details.id.version - if (version.contains("-alpha", true) || - version.contains("-beta", true) || - version.contains("-rc", true) || - version.contains("-m", true) || // e.g. spring milestones are published to grails repo - version.contains(".alpha", true) || // e.g. netty - version.contains(".beta", true) || // e.g. hibernate - version.contains(".cr", true) // e.g. hibernate + if (version.contains("-alpha", true) + || version.contains("-beta", true) + || version.contains("-rc", true) + || version.contains("-m", true) // e.g. spring milestones are published to grails repo + || version.contains(".m", true) // e.g. lettuce + || version.contains(".alpha", true) // e.g. netty + || version.contains(".beta", true) // e.g. hibernate + || version.contains(".cr", true) // e.g. hibernate + || version.endsWith("-nf-execution") // graphql + || GIT_SHA_PATTERN.matches(version) // graphql + || DATETIME_PATTERN.matches(version) // graphql ) { context.details.status = "milestone" } } + + companion object { + private val GIT_SHA_PATTERN = Regex("^.*-[0-9a-f]{7,}$") + private val DATETIME_PATTERN = Regex("^\\d{4}-\\d{2}-\\d{2}T\\d{2}-\\d{2}-\\d{2}.*$") + } } configurations { @@ -143,11 +152,11 @@ tasks { inputs.property("instrumentation.name", name) inputs.property("instrumentation.version", version) - val propertiesDir = File(project.buildDir, "generated/instrumentationVersion/META-INF/io/opentelemetry/instrumentation/") + val propertiesDir = layout.buildDirectory.dir("generated/instrumentationVersion/META-INF/io/opentelemetry/instrumentation/") outputs.dir(propertiesDir) doLast { - File(propertiesDir, "$name.properties").writeText("version=$version") + File(propertiesDir.get().asFile, "$name.properties").writeText("version=$version") } } } diff --git a/conventions/src/main/kotlin/io.opentelemetry.instrumentation.javaagent-instrumentation.gradle.kts b/conventions/src/main/kotlin/io.opentelemetry.instrumentation.javaagent-instrumentation.gradle.kts index 24a561e63d9d..e70130463405 100644 --- a/conventions/src/main/kotlin/io.opentelemetry.instrumentation.javaagent-instrumentation.gradle.kts +++ b/conventions/src/main/kotlin/io.opentelemetry.instrumentation.javaagent-instrumentation.gradle.kts @@ -6,7 +6,7 @@ plugins { dependencies { add("muzzleBootstrap", "io.opentelemetry.instrumentation:opentelemetry-instrumentation-api") - add("muzzleBootstrap", "io.opentelemetry.instrumentation:opentelemetry-instrumentation-api-semconv") + add("muzzleBootstrap", "io.opentelemetry.instrumentation:opentelemetry-instrumentation-api-incubator") add("muzzleBootstrap", "io.opentelemetry.instrumentation:opentelemetry-instrumentation-annotations-support") add("muzzleTooling", "io.opentelemetry.javaagent:opentelemetry-javaagent-extension-api") add("muzzleTooling", "io.opentelemetry.javaagent:opentelemetry-javaagent-tooling") diff --git a/conventions/src/main/kotlin/io.opentelemetry.instrumentation.javaagent-shadowing.gradle.kts b/conventions/src/main/kotlin/io.opentelemetry.instrumentation.javaagent-shadowing.gradle.kts index c8441b862456..7b4e59d77d1d 100644 --- a/conventions/src/main/kotlin/io.opentelemetry.instrumentation.javaagent-shadowing.gradle.kts +++ b/conventions/src/main/kotlin/io.opentelemetry.instrumentation.javaagent-shadowing.gradle.kts @@ -1,7 +1,7 @@ import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar plugins { - id("com.github.johnrengelman.shadow") + id("com.gradleup.shadow") } // NOTE: any modifications below should also be made in @@ -24,18 +24,17 @@ tasks.withType().configureEach { exclude("io.opentelemetry.instrumentation.resources.*") exclude("io.opentelemetry.instrumentation.spring.resources.*") } - } - // relocate(OpenTelemetry API) since these classes live in the bootstrap class loader - 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("io.opentelemetry.extension.incubator", "io.opentelemetry.javaagent.shaded.io.opentelemetry.extension.incubator") + // relocate(OpenTelemetry API) since these classes live in the bootstrap class loader + 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.contrib.awsxray", "io.opentelemetry.javaagent.shaded.io.opentelemetry.contrib.awsxray") relocate("io.opentelemetry.extension.kotlin", "io.opentelemetry.javaagent.shaded.io.opentelemetry.extension.kotlin") // this is for instrumentation of opentelemetry-api and opentelemetry-instrumentation-api diff --git a/conventions/src/main/kotlin/io.opentelemetry.instrumentation.javaagent-testing.gradle.kts b/conventions/src/main/kotlin/io.opentelemetry.instrumentation.javaagent-testing.gradle.kts index d06428c99f16..4a5b87c0e2e4 100644 --- a/conventions/src/main/kotlin/io.opentelemetry.instrumentation.javaagent-testing.gradle.kts +++ b/conventions/src/main/kotlin/io.opentelemetry.instrumentation.javaagent-testing.gradle.kts @@ -19,7 +19,7 @@ dependencies { // Integration tests may need to define custom instrumentation modules so we include the standard // instrumentation infrastructure for testing too. compileOnly("io.opentelemetry.instrumentation:opentelemetry-instrumentation-api") - compileOnly("io.opentelemetry.instrumentation:opentelemetry-instrumentation-api-semconv") + compileOnly("io.opentelemetry.instrumentation:opentelemetry-instrumentation-api-incubator") compileOnly("io.opentelemetry.javaagent:opentelemetry-javaagent-bootstrap") // Apply common dependencies for instrumentation. compileOnly("io.opentelemetry.javaagent:opentelemetry-javaagent-extension-api") { @@ -85,7 +85,7 @@ class JavaagentTestArgumentsProvider( // Reduce noise in assertion messages since we don't need to verify this in most tests. We check // in smoke tests instead. "-Dotel.javaagent.add-thread-details=false", - "-Dotel.metrics.exporter=otlp", + "-Dotel.javaagent.experimental.indy=${findProperty("testIndy") == "true"}", // suppress repeated logging of "No metric data to export - skipping export." // since PeriodicMetricReader is configured with a short interval "-Dio.opentelemetry.javaagent.slf4j.simpleLogger.log.io.opentelemetry.sdk.metrics.export.PeriodicMetricReader=INFO", @@ -113,7 +113,7 @@ afterEvaluate { // 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 { - if (file("$buildDir/resources/main").equals(it) || file("$buildDir/classes/java/main").equals(it)) { + if (file(layout.buildDirectory.dir("resources/main")).equals(it) || file(layout.buildDirectory.dir("classes/java/main")).equals(it)) { // The sources are packaged into the testing jar, so we need to exclude them from the test // classpath, which automatically inherits them, to ensure our shaded versions are used. return@filter false diff --git a/conventions/src/main/kotlin/io.opentelemetry.instrumentation.library-instrumentation.gradle.kts b/conventions/src/main/kotlin/io.opentelemetry.instrumentation.library-instrumentation.gradle.kts index 87dcdef00350..d58298f41b73 100644 --- a/conventions/src/main/kotlin/io.opentelemetry.instrumentation.library-instrumentation.gradle.kts +++ b/conventions/src/main/kotlin/io.opentelemetry.instrumentation.library-instrumentation.gradle.kts @@ -4,7 +4,7 @@ plugins { dependencies { api("io.opentelemetry.instrumentation:opentelemetry-instrumentation-api") - api("io.opentelemetry.instrumentation:opentelemetry-instrumentation-api-semconv") + api("io.opentelemetry.instrumentation:opentelemetry-instrumentation-api-incubator") api("io.opentelemetry:opentelemetry-api") diff --git a/conventions/src/main/kotlin/io/opentelemetry/instrumentation/gradle/OtelBomExtension.kt b/conventions/src/main/kotlin/io/opentelemetry/instrumentation/gradle/OtelBomExtension.kt index 85029753de39..861e4754ab4c 100644 --- a/conventions/src/main/kotlin/io/opentelemetry/instrumentation/gradle/OtelBomExtension.kt +++ b/conventions/src/main/kotlin/io/opentelemetry/instrumentation/gradle/OtelBomExtension.kt @@ -11,4 +11,9 @@ import java.util.function.Predicate abstract class OtelBomExtension { abstract val projectFilter: Property> + val additionalDependencies: MutableSet = hashSetOf() + + fun addExtra(groupId: String, artifactId: String, version: String) { + this.additionalDependencies.add(groupId + ":" + artifactId + ":" + version) + } } diff --git a/conventions/src/main/kotlin/io/opentelemetry/instrumentation/gradle/OtelJavaExtension.kt b/conventions/src/main/kotlin/io/opentelemetry/instrumentation/gradle/OtelJavaExtension.kt index 195da08a4709..100184a111b4 100644 --- a/conventions/src/main/kotlin/io/opentelemetry/instrumentation/gradle/OtelJavaExtension.kt +++ b/conventions/src/main/kotlin/io/opentelemetry/instrumentation/gradle/OtelJavaExtension.kt @@ -10,6 +10,7 @@ import org.gradle.api.provider.Property abstract class OtelJavaExtension { abstract val minJavaVersionSupported: Property + abstract val maxJavaVersionSupported: Property abstract val maxJavaVersionForTests: Property diff --git a/conventions/src/main/kotlin/otel.bom-conventions.gradle.kts b/conventions/src/main/kotlin/otel.bom-conventions.gradle.kts index 1ba7a3c5d429..dc084cc1f2e0 100644 --- a/conventions/src/main/kotlin/otel.bom-conventions.gradle.kts +++ b/conventions/src/main/kotlin/otel.bom-conventions.gradle.kts @@ -21,7 +21,6 @@ afterEvaluate { val bomProjects = rootProject.subprojects .sortedBy { it.findProperty("archivesName") as String? } .filter { !it.name.startsWith("bom") } - .filter { !it.name.equals("javaagent") } .filter(otelBom.projectFilter.get()::test) .filter { it.plugins.hasPlugin("maven-publish") } @@ -32,8 +31,16 @@ afterEvaluate { } } } + otelBom.additionalDependencies.forEach { dependency -> + dependencies { + constraints { + api(dependency) + } + } + } } +// this applies version numbers to the SDK bom and SDK alpha bom which are dependencies of the instrumentation boms evaluationDependsOn(":dependencyManagement") val dependencyManagementConf = configurations.create("dependencyManagement") { isCanBeConsumed = false diff --git a/conventions/src/main/kotlin/otel.errorprone-conventions.gradle.kts b/conventions/src/main/kotlin/otel.errorprone-conventions.gradle.kts index a6e0f4de5401..2129bc956a2b 100644 --- a/conventions/src/main/kotlin/otel.errorprone-conventions.gradle.kts +++ b/conventions/src/main/kotlin/otel.errorprone-conventions.gradle.kts @@ -105,8 +105,9 @@ tasks { // some moving. disable("DefaultPackage") - // we use modified OtelPrivateConstructorForUtilityClass which ignores *Advice classes + // we use modified Otel* checks which ignore *Advice classes disable("PrivateConstructorForUtilityClass") + disable("CanIgnoreReturnValueSuggester") // TODO(anuraaga): Remove this, probably after instrumenter API migration instead of dealing // with older APIs. @@ -115,17 +116,21 @@ tasks { // lots of low level APIs use arrays disable("AvoidObjectArrays") + disable("BanClassLoader") + // YodaConditions may improve safety in some cases. The argument of increased // cognitive load is dubious. disable("YodaCondition") + disable("NonFinalStaticField") + if (name.contains("Jmh") || name.contains("Test")) { // Allow underscore in test-type method names disable("MemberName") } - if (project.path.endsWith(":testing") || name.contains("Test")) { + if ((project.path.endsWith(":testing") || name.contains("Test")) && !project.name.equals("custom-checks")) { // This check causes too many failures, ignore the ones in tests - disable("CanIgnoreReturnValueSuggester") + disable("OtelCanIgnoreReturnValueSuggester") } } } diff --git a/conventions/src/main/kotlin/otel.jacoco-conventions.gradle.kts b/conventions/src/main/kotlin/otel.jacoco-conventions.gradle.kts index 2ff70b43ba52..7ba6c17bcc38 100644 --- a/conventions/src/main/kotlin/otel.jacoco-conventions.gradle.kts +++ b/conventions/src/main/kotlin/otel.jacoco-conventions.gradle.kts @@ -3,7 +3,7 @@ plugins { } jacoco { - toolVersion = "0.8.8" + toolVersion = "0.8.12" } tasks { @@ -13,7 +13,7 @@ tasks { reports { xml.required.set(true) csv.required.set(false) - html.outputLocation.set(file("$buildDir/reports/jacoco/")) + html.outputLocation.set(layout.buildDirectory.dir("reports/jacoco/")) } } } diff --git a/conventions/src/main/kotlin/otel.japicmp-conventions.gradle.kts b/conventions/src/main/kotlin/otel.japicmp-conventions.gradle.kts index 941ebc40cfee..e3fd6ed5d559 100644 --- a/conventions/src/main/kotlin/otel.japicmp-conventions.gradle.kts +++ b/conventions/src/main/kotlin/otel.japicmp-conventions.gradle.kts @@ -16,8 +16,8 @@ plugins { val latestReleasedVersion: String by lazy { // hack to find the current released version of the project val temp: Configuration = configurations.create("tempConfig") - // pick the agent, since it's always there. - dependencies.add(temp.name, "io.opentelemetry.javaagent:opentelemetry-javaagent:latest.release") + // pick the bom, since we don't use dependency substitution on it. + dependencies.add(temp.name, "io.opentelemetry.instrumentation:opentelemetry-instrumentation-bom:latest.release") val moduleVersion = configurations["tempConfig"].resolvedConfiguration.firstLevelModuleDependencies.elementAt(0).moduleVersion configurations.remove(temp) diff --git a/conventions/src/main/kotlin/otel.java-conventions.gradle.kts b/conventions/src/main/kotlin/otel.java-conventions.gradle.kts index 918b767e2d5d..324400151278 100644 --- a/conventions/src/main/kotlin/otel.java-conventions.gradle.kts +++ b/conventions/src/main/kotlin/otel.java-conventions.gradle.kts @@ -1,4 +1,3 @@ -import com.gradle.enterprise.gradleplugin.testretry.retry import io.opentelemetry.instrumentation.gradle.OtelJavaExtension import org.gradle.api.tasks.testing.logging.TestExceptionFormat import java.time.Duration @@ -27,12 +26,15 @@ afterEvaluate { } // Version to use to compile code and run tests. -val DEFAULT_JAVA_VERSION = JavaVersion.VERSION_17 +val DEFAULT_JAVA_VERSION = JavaVersion.VERSION_21 java { toolchain { languageVersion.set( - otelJava.minJavaVersionSupported.map { JavaLanguageVersion.of(Math.max(it.majorVersion.toInt(), DEFAULT_JAVA_VERSION.majorVersion.toInt())) } + otelJava.minJavaVersionSupported.map { + val defaultJavaVersion = otelJava.maxJavaVersionSupported.getOrElse(DEFAULT_JAVA_VERSION).majorVersion.toInt() + JavaLanguageVersion.of(Math.max(it.majorVersion.toInt(), defaultJavaVersion)) + } ) } @@ -69,11 +71,14 @@ tasks.withType().configureEach { "-Xlint:-processing", // We suppress the "options" warning because it prevents compilation on modern JDKs "-Xlint:-options", - - // Fail build on any warning - "-Werror" + // jdk21 generates more serial warnings than previous versions + "-Xlint:-serial" ) ) + if (System.getProperty("dev") != "true") { + // Fail build on any warning + compilerArgs.add("-Werror") + } } encoding = "UTF-8" @@ -81,6 +86,11 @@ tasks.withType().configureEach { if (name.contains("Test")) { // serialVersionUID is basically guaranteed to be useless in tests compilerArgs.add("-Xlint:-serial") + // when code is compiled with jdk 21 and executed with jdk 8, the -parameters flag is needed to avoid + // java.lang.reflect.MalformedParametersException: Invalid parameter name "" + // when junit calls java.lang.reflect.Executable.getParameters() on the constructor of a + // non-static nested test class + compilerArgs.add("-parameters") } } } @@ -99,6 +109,17 @@ afterEvaluate { sourceCompatibility = otelJava.minJavaVersionSupported.get().majorVersion targetCompatibility = otelJava.minJavaVersionSupported.get().majorVersion } + tasks.withType().configureEach { + with(options) { + source = otelJava.minJavaVersionSupported.get().majorVersion + } + } + tasks.withType().configureEach { + if (javaCompiler.isPresent && javaCompiler.get().metadata.languageVersion.canCompileOrRun(21)) { + // new warning in jdk21 + options.compilerArgs.add("-Xlint:-this-escape") + } + } } evaluationDependsOn(":dependencyManagement") @@ -122,7 +143,7 @@ abstract class NettyAlignmentRule : ComponentMetadataRule { 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) + belongsTo("io.netty:netty-bom:4.1.113.Final", false) } else if (id.version.startsWith("4.0.")) { belongsTo("io.netty:netty-bom:4.0.56.Final", false) } @@ -139,8 +160,16 @@ dependencies { compileOnly("com.google.code.findbugs:jsr305") compileOnly("com.google.errorprone:error_prone_annotations") - codenarc("org.codenarc:CodeNarc:2.2.0") - codenarc(platform("org.codehaus.groovy:groovy-bom:3.0.9")) + codenarc("org.codenarc:CodeNarc:3.5.0") + codenarc(platform("org.codehaus.groovy:groovy-bom:3.0.22")) + + modules { + // checkstyle uses the very old google-collections which causes Java 9 module conflict with + // guava which is also on the classpath + module("com.google.collections:google-collections") { + replacedBy("com.google.guava:guava", "google-collections is now part of Guava") + } + } } testing { @@ -186,6 +215,41 @@ testing { } } +var path = project.path +if (path.startsWith(":instrumentation:")) { + // remove segments that are a prefix of the next segment + // for example :instrumentation:log4j:log4j-context-data:log4j-context-data-2.17 is transformed to log4j-context-data-2.17 + var tmpPath = path + val suffix = tmpPath.substringAfterLast(':') + var prefix = ":instrumentation:" + if (suffix == "library") { + // strip ":library" suffix + tmpPath = tmpPath.substringBeforeLast(':') + } else if (suffix == "library-autoconfigure") { + // replace ":library-autoconfigure" with "-autoconfigure" + tmpPath = tmpPath.substringBeforeLast(':') + "-autoconfigure" + } else if (suffix == "javaagent") { + // strip ":javaagent" suffix and add it to prefix + prefix += "javaagent:" + tmpPath = tmpPath.substringBeforeLast(':') + } + val segments = tmpPath.substring(":instrumentation:".length).split(':') + var newPath = "" + var done = false + for (s in segments) { + if (!done && (newPath.isEmpty() || s.startsWith(newPath))) { + newPath = s + } else { + newPath += ":$s" + done = true + } + } + if (newPath.isNotEmpty()) { + path = prefix + newPath + } +} +var javaModuleName = "io.opentelemetry" + path.replace(".", "_").replace("-", "_").replace(":", ".") + tasks { named("jar") { // By default Gradle Jar task can put multiple files with the same name @@ -202,7 +266,8 @@ tasks { "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" + "Implementation-URL" to "https://github.com/open-telemetry/opentelemetry-java-instrumentation", + "Automatic-Module-Name" to javaModuleName ) } } @@ -229,8 +294,12 @@ tasks { withType().configureEach { isPreserveFileTimestamps = false isReproducibleFileOrder = true - dirMode = Integer.parseInt("0755", 8) - fileMode = Integer.parseInt("0644", 8) + dirPermissions { + unix("755") + } + filePermissions { + unix("644") + } } // Convenient when updating errorprone @@ -279,14 +348,22 @@ tasks.withType().configureEach { // propagation. jvmArgs("-Dio.opentelemetry.context.enableStrictContext=${rootProject.findProperty("enableStrictContext") ?: true}") // TODO(anuraaga): Have agent map unshaded to shaded. - jvmArgs("-Dio.opentelemetry.javaagent.shaded.io.opentelemetry.context.enableStrictContext=${rootProject.findProperty("enableStrictContext") ?: true}") + if (project.findProperty("disableShadowRelocate") != "true") { + jvmArgs("-Dio.opentelemetry.javaagent.shaded.io.opentelemetry.context.enableStrictContext=${rootProject.findProperty("enableStrictContext") ?: true}") + } else { + jvmArgs("-Dotel.instrumentation.opentelemetry-api.enabled=false") + jvmArgs("-Dotel.instrumentation.opentelemetry-instrumentation-api.enabled=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") // Work around payara not working when this is set for some reason. - if (project.name != "jaxrs-2.0-payara-testing") { + // Don't set for: + // - camel as we have tests that interact with AWS and need normal trustStore + // - vaadin as tests need to be able to download nodejs when not cached in ~/.vaadin/ + if (project.name != "jaxrs-2.0-payara-testing" && !project.path.contains("vaadin") && project.description != "camel-2-20") { jvmArgumentProviders.add(KeystoreArgumentsProvider(trustStore)) } @@ -294,7 +371,7 @@ tasks.withType().configureEach { // 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 { + develocity.testRetry { // You can see tests that were retried by this mechanism in the collected test reports and build scans. if (System.getenv().containsKey("CI") || rootProject.hasProperty("retryTests")) { maxRetries.set(5) @@ -357,7 +434,7 @@ codenarc { checkstyle { configFile = rootProject.file("buildscripts/checkstyle.xml") // this version should match the version of google_checks.xml used as basis for above configuration - toolVersion = "8.37" + toolVersion = "10.18.1" maxWarnings = 0 } @@ -365,6 +442,8 @@ dependencyCheck { skipConfigurations = listOf("errorprone", "checkstyle", "annotationProcessor") suppressionFile = "buildscripts/dependency-check-suppressions.xml" failBuildOnCVSS = 7.0f // fail on high or critical CVE + nvd.apiKey = System.getenv("NVD_API_KEY") + nvd.delay = 3500 // until next dependency check release (https://github.com/jeremylong/DependencyCheck/pull/6333) } idea { @@ -392,7 +471,7 @@ configurations.configureEach { // what modules they add to reference generically. dependencySubstitution { substitute(module("io.opentelemetry.instrumentation:opentelemetry-instrumentation-api")).using(project(":instrumentation-api")) - substitute(module("io.opentelemetry.instrumentation:opentelemetry-instrumentation-api-semconv")).using(project(":instrumentation-api-semconv")) + substitute(module("io.opentelemetry.instrumentation:opentelemetry-instrumentation-api-incubator")).using(project(":instrumentation-api-incubator")) substitute(module("io.opentelemetry.instrumentation:opentelemetry-instrumentation-annotations")).using(project(":instrumentation-annotations")) substitute(module("io.opentelemetry.instrumentation:opentelemetry-instrumentation-annotations-support")).using( project(":instrumentation-annotations-support") @@ -403,6 +482,7 @@ configurations.configureEach { substitute(module("io.opentelemetry.javaagent:opentelemetry-agent-for-testing")).using(project(":testing:agent-for-testing")) substitute(module("io.opentelemetry.javaagent:opentelemetry-testing-common")).using(project(":testing-common")) substitute(module("io.opentelemetry.javaagent:opentelemetry-muzzle")).using(project(":muzzle")) + substitute(module("io.opentelemetry.javaagent:opentelemetry-javaagent")).using(project(":javaagent")) } // The above substitutions ensure dependencies managed by this BOM for external projects refer to this repo's projects here. diff --git a/conventions/src/main/kotlin/otel.javaagent-instrumentation.gradle.kts b/conventions/src/main/kotlin/otel.javaagent-instrumentation.gradle.kts index 24542bc5e37e..2fe5adb596bf 100644 --- a/conventions/src/main/kotlin/otel.javaagent-instrumentation.gradle.kts +++ b/conventions/src/main/kotlin/otel.javaagent-instrumentation.gradle.kts @@ -22,3 +22,7 @@ configurations { extendsFrom(bootstrap) } } + +dependencies { + api("io.opentelemetry.semconv:opentelemetry-semconv-incubating") +} diff --git a/conventions/src/main/kotlin/otel.jmh-conventions.gradle.kts b/conventions/src/main/kotlin/otel.jmh-conventions.gradle.kts index 96823e1c9102..16034e93a054 100644 --- a/conventions/src/main/kotlin/otel.jmh-conventions.gradle.kts +++ b/conventions/src/main/kotlin/otel.jmh-conventions.gradle.kts @@ -25,8 +25,8 @@ jmh { } jmhReport { - jmhResultPath = file("$buildDir/results/jmh/results.json").absolutePath - jmhReportOutput = file("$buildDir/results/jmh").absolutePath + jmhResultPath = layout.buildDirectory.file("results/jmh/results.json").get().asFile.absolutePath + jmhReportOutput = layout.buildDirectory.file("results/jmh").get().asFile.absolutePath } tasks { diff --git a/conventions/src/main/kotlin/otel.protobuf-conventions.gradle.kts b/conventions/src/main/kotlin/otel.protobuf-conventions.gradle.kts deleted file mode 100644 index 1ef7ab8febb5..000000000000 --- a/conventions/src/main/kotlin/otel.protobuf-conventions.gradle.kts +++ /dev/null @@ -1,43 +0,0 @@ -import com.google.protobuf.gradle.* - -plugins { - id("com.google.protobuf") - - id("otel.java-conventions") -} - -protobuf { - protoc { - // The artifact spec for the Protobuf Compiler - 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 { - id("grpc") { - artifact = "io.grpc:protoc-gen-grpc-java:1.6.0" - 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"))) - } -} diff --git a/conventions/src/main/kotlin/otel.spotless-conventions.gradle.kts b/conventions/src/main/kotlin/otel.spotless-conventions.gradle.kts index e08e669fc658..3cb5868df939 100644 --- a/conventions/src/main/kotlin/otel.spotless-conventions.gradle.kts +++ b/conventions/src/main/kotlin/otel.spotless-conventions.gradle.kts @@ -18,7 +18,7 @@ spotless { groovy { licenseHeaderFile( rootProject.file("buildscripts/spotless.license.java"), - "(package|import|class)" + "(package|import|(?:abstract )?class)" ) endWithNewline() } @@ -74,7 +74,9 @@ spotless { // depends on ktlint_standard_wrapping "ktlint_standard_trailing-comma-on-declaration-site" to "disabled", // also very hard to find out where this happens - "ktlint_standard_wrapping" to "disabled" + "ktlint_standard_wrapping" to "disabled", + // we use variable names like v1_10Deps + "ktlint_standard_property-naming" to "disabled" ) ) } diff --git a/custom-checks/build.gradle.kts b/custom-checks/build.gradle.kts index c926329ca708..f96a4c855e6a 100644 --- a/custom-checks/build.gradle.kts +++ b/custom-checks/build.gradle.kts @@ -36,6 +36,8 @@ tasks { "--add-exports", "jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED", "--add-exports", + "jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED", + "--add-exports", "jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED", "--add-exports", "jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED", @@ -60,6 +62,11 @@ tasks.withType().configureEach { jvmArgs("-XX:+IgnoreUnrecognizedVMOptions") } +tasks.withType().configureEach { + // using com.sun.tools.javac.api.JavacTrees breaks javadoc generation + enabled = false +} + // Our conventions apply this project as a dependency in the errorprone configuration, which would cause // a circular dependency if trying to compile this project with that still there. So we filter this // project out. diff --git a/custom-checks/src/main/java/com/google/errorprone/bugpatterns/checkreturnvalue/CanIgnoreReturnValueSuggesterFactory.java b/custom-checks/src/main/java/com/google/errorprone/bugpatterns/checkreturnvalue/CanIgnoreReturnValueSuggesterFactory.java new file mode 100644 index 000000000000..d157d5fe924a --- /dev/null +++ b/custom-checks/src/main/java/com/google/errorprone/bugpatterns/checkreturnvalue/CanIgnoreReturnValueSuggesterFactory.java @@ -0,0 +1,19 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.errorprone.bugpatterns.checkreturnvalue; + +import com.google.errorprone.ErrorProneFlags; + +public final class CanIgnoreReturnValueSuggesterFactory { + + // calls package private constructor of CanIgnoreReturnValueSuggester + public static CanIgnoreReturnValueSuggester createCanIgnoreReturnValueSuggester( + ErrorProneFlags errorProneFlags) { + return new CanIgnoreReturnValueSuggester(errorProneFlags); + } + + private CanIgnoreReturnValueSuggesterFactory() {} +} diff --git a/custom-checks/src/main/java/io/opentelemetry/javaagent/customchecks/OtelCanIgnoreReturnValueSuggester.java b/custom-checks/src/main/java/io/opentelemetry/javaagent/customchecks/OtelCanIgnoreReturnValueSuggester.java new file mode 100644 index 000000000000..55e1d0bfcee0 --- /dev/null +++ b/custom-checks/src/main/java/io/opentelemetry/javaagent/customchecks/OtelCanIgnoreReturnValueSuggester.java @@ -0,0 +1,73 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.customchecks; + +import static com.google.errorprone.matchers.Description.NO_MATCH; + +import com.google.auto.service.AutoService; +import com.google.errorprone.BugPattern; +import com.google.errorprone.ErrorProneFlags; +import com.google.errorprone.VisitorState; +import com.google.errorprone.bugpatterns.BugChecker; +import com.google.errorprone.bugpatterns.checkreturnvalue.CanIgnoreReturnValueSuggester; +import com.google.errorprone.bugpatterns.checkreturnvalue.CanIgnoreReturnValueSuggesterFactory; +import com.google.errorprone.matchers.Description; +import com.sun.source.tree.ClassTree; +import com.sun.source.tree.MethodTree; +import com.sun.source.util.TreePath; +import javax.inject.Inject; + +@AutoService(BugChecker.class) +@BugPattern( + summary = + "Methods with ignorable return values (including methods that always 'return this') should be annotated with @com.google.errorprone.annotations.CanIgnoreReturnValue", + severity = BugPattern.SeverityLevel.WARNING) +public class OtelCanIgnoreReturnValueSuggester extends BugChecker + implements BugChecker.MethodTreeMatcher { + + private static final long serialVersionUID = 1L; + + private final CanIgnoreReturnValueSuggester delegate; + + @Inject + OtelCanIgnoreReturnValueSuggester(ErrorProneFlags errorProneFlags) { + delegate = + CanIgnoreReturnValueSuggesterFactory.createCanIgnoreReturnValueSuggester(errorProneFlags); + } + + public OtelCanIgnoreReturnValueSuggester() { + // https://errorprone.info/docs/plugins + // this constructor is used by ServiceLoader, actual instance will be created with the other + // constructor + delegate = null; + } + + @Override + public Description matchMethod(MethodTree methodTree, VisitorState visitorState) { + ClassTree containerClass = findContainingClass(visitorState.getPath()); + if (containerClass.getSimpleName().toString().endsWith("Advice")) { + return NO_MATCH; + } + Description description = delegate.matchMethod(methodTree, visitorState); + if (description == NO_MATCH) { + return description; + } + return describeMatch(methodTree); + } + + private static ClassTree findContainingClass(TreePath path) { + TreePath parent = path.getParentPath(); + while (parent != null && !(parent.getLeaf() instanceof ClassTree)) { + parent = parent.getParentPath(); + } + if (parent == null) { + throw new IllegalStateException( + "Method is expected to be contained in a class, something must be wrong"); + } + ClassTree containerClass = (ClassTree) parent.getLeaf(); + return containerClass; + } +} diff --git a/custom-checks/src/main/java/io/opentelemetry/javaagent/customchecks/OtelInternalJavadoc.java b/custom-checks/src/main/java/io/opentelemetry/javaagent/customchecks/OtelInternalJavadoc.java index 950632166bd3..3d6693a9574e 100644 --- a/custom-checks/src/main/java/io/opentelemetry/javaagent/customchecks/OtelInternalJavadoc.java +++ b/custom-checks/src/main/java/io/opentelemetry/javaagent/customchecks/OtelInternalJavadoc.java @@ -42,7 +42,7 @@ public class OtelInternalJavadoc extends BugChecker implements BugChecker.ClassT @Override public Description matchClass(ClassTree tree, VisitorState state) { - if (!isPublic(tree) || !isInternal(state)) { + if (!isPublic(tree) || !isInternal(state) || tree.getSimpleName().toString().endsWith("Test")) { return Description.NO_MATCH; } String javadoc = getJavadoc(state); diff --git a/dependencyManagement/build.gradle.kts b/dependencyManagement/build.gradle.kts index 878f5750e433..519458faf39f 100644 --- a/dependencyManagement/build.gradle.kts +++ b/dependencyManagement/build.gradle.kts @@ -1,9 +1,5 @@ -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) @@ -12,11 +8,12 @@ val dependencyVersions = hashMapOf() rootProject.extra["versions"] = dependencyVersions // this line is managed by .github/scripts/update-sdk-version.sh -val otelSdkVersion = "1.27.0" +val otelSdkVersion = "1.41.0" +val otelContribVersion = "1.38.0-alpha" val otelSdkAlphaVersion = otelSdkVersion.replaceFirst("(-SNAPSHOT)?$".toRegex(), "-alpha$1") // Need both BOM and groovy jars -val groovyVersion = "4.0.12" +val groovyVersion = "4.0.22" // 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 @@ -30,26 +27,30 @@ val groovyVersion = "4.0.12" // configurations.testRuntimeClasspath.resolutionStrategy.force "com.google.guava:guava:19.0" val DEPENDENCY_BOMS = listOf( - "com.fasterxml.jackson:jackson-bom:2.15.2", - "com.google.guava:guava-bom:32.0.1-jre", + "com.fasterxml.jackson:jackson-bom:2.17.2", + "com.squareup.okio:okio-bom:3.9.0", // see https://github.com/open-telemetry/opentelemetry-java/issues/5637 + "com.google.guava:guava-bom:33.3.0-jre", "org.apache.groovy:groovy-bom:${groovyVersion}", "io.opentelemetry:opentelemetry-bom:${otelSdkVersion}", "io.opentelemetry:opentelemetry-bom-alpha:${otelSdkAlphaVersion}", - "org.junit:junit-bom:5.9.3", - "org.testcontainers:testcontainers-bom:1.18.3", - "org.spockframework:spock-bom:2.4-M1-groovy-4.0" + "org.junit:junit-bom:5.11.0", + "org.testcontainers:testcontainers-bom:1.20.1", + "org.spockframework:spock-bom:2.4-M4-groovy-4.0" ) val autoServiceVersion = "1.1.1" -val autoValueVersion = "1.10.1" -val errorProneVersion = "2.19.1" -val byteBuddyVersion = "1.14.5" -val asmVersion = "9.5" -val jmhVersion = "1.36" +val autoValueVersion = "1.11.0" +val errorProneVersion = "2.31.0" +val byteBuddyVersion = "1.15.1" +val asmVersion = "9.7" +val jmhVersion = "1.37" val mockitoVersion = "4.11.0" -val slf4jVersion = "2.0.7" +val slf4jVersion = "2.0.16" +val semConvVersion = "1.25.0-alpha" val CORE_DEPENDENCIES = listOf( + "io.opentelemetry.semconv:opentelemetry-semconv:${semConvVersion}", + "io.opentelemetry.semconv:opentelemetry-semconv-incubating:${semConvVersion}", "com.google.auto.service:auto-service:${autoServiceVersion}", "com.google.auto.service:auto-service-annotations:${autoServiceVersion}", "com.google.auto.value:auto-value:${autoValueVersion}", @@ -64,6 +65,7 @@ val CORE_DEPENDENCIES = listOf( "net.bytebuddy:byte-buddy-gradle-plugin:${byteBuddyVersion}", "org.ow2.asm:asm:${asmVersion}", "org.ow2.asm:asm-tree:${asmVersion}", + "org.ow2.asm:asm-util:${asmVersion}", "org.openjdk.jmh:jmh-core:${jmhVersion}", "org.openjdk.jmh:jmh-generator-bytecode:${jmhVersion}", "org.mockito:mockito-core:${mockitoVersion}", @@ -80,37 +82,40 @@ val CORE_DEPENDENCIES = listOf( // There are dependencies included here that appear to have no usages, but are maintained at // this top level to help consistently satisfy large numbers of transitive dependencies. val DEPENDENCIES = listOf( - "ch.qos.logback:logback-classic:1.3.8", // 1.4+ requires Java 11+ + "io.r2dbc:r2dbc-proxy:1.1.5.RELEASE", + "ch.qos.logback:logback-classic:1.3.14", // 1.4+ requires Java 11+ "com.github.stefanbirkner:system-lambda:1.2.1", "com.github.stefanbirkner:system-rules:1.19.0", - "uk.org.webcompere:system-stubs-jupiter:2.0.2", - "com.uber.nullaway:nullaway:0.10.10", + "uk.org.webcompere:system-stubs-jupiter:2.0.3", + "com.uber.nullaway:nullaway:0.11.2", "commons-beanutils:commons-beanutils:1.9.4", - "commons-cli:commons-cli:1.5.0", - "commons-codec:commons-codec:1.15", + "commons-cli:commons-cli:1.9.0", + "commons-codec:commons-codec:1.17.1", "commons-collections:commons-collections:3.2.2", "commons-digester:commons-digester:2.1", "commons-fileupload:commons-fileupload:1.5", - "commons-io:commons-io:2.13.0", + "commons-io:commons-io:2.16.1", "commons-lang:commons-lang:2.6", - "commons-logging:commons-logging:1.2", - "commons-validator:commons-validator:1.7", + "commons-logging:commons-logging:1.3.4", + "commons-validator:commons-validator:1.9.0", "io.netty:netty:3.10.6.Final", - "io.opentelemetry.contrib:opentelemetry-aws-xray-propagator:1.27.0-alpha", - "io.opentelemetry.proto:opentelemetry-proto:0.20.0-alpha", - "org.assertj:assertj-core:3.24.2", - "org.awaitility:awaitility:4.2.0", + "io.opentelemetry.contrib:opentelemetry-aws-resources:${otelContribVersion}", + "io.opentelemetry.contrib:opentelemetry-aws-xray-propagator:${otelContribVersion}", + "io.opentelemetry.contrib:opentelemetry-gcp-resources:${otelContribVersion}", + "io.opentelemetry.contrib:opentelemetry-baggage-processor:${otelContribVersion}", + "io.opentelemetry.proto:opentelemetry-proto:1.3.2-alpha", + "io.opentelemetry:opentelemetry-extension-annotations:1.18.0", // deprecated, no longer part of bom + "org.assertj:assertj-core:3.26.3", + "org.awaitility:awaitility:4.2.2", "com.google.code.findbugs:annotations:3.0.1u2", "com.google.code.findbugs:jsr305:3.0.2", "org.apache.groovy:groovy:${groovyVersion}", "org.apache.groovy:groovy-json:${groovyVersion}", - "org.codehaus.mojo:animal-sniffer-annotations:1.23", + "org.codehaus.mojo:animal-sniffer-annotations:1.24", "org.junit-pioneer:junit-pioneer:1.9.1", - "org.objenesis:objenesis:3.3", - // Note that this is only referenced as "org.springframework.boot" in build files, not the artifact name. - "org.springframework.boot:spring-boot-dependencies:2.7.5", + "org.objenesis:objenesis:3.4", "javax.validation:validation-api:2.0.1.Final", - "org.snakeyaml:snakeyaml-engine:2.6" + "org.snakeyaml:snakeyaml-engine:2.7" ) javaPlatform { @@ -136,22 +141,3 @@ dependencies { } } } - -fun isNonStable(version: String): Boolean { - val stableKeyword = listOf("RELEASE", "FINAL", "GA").any { version.uppercase().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/docs/advanced-configuration-options.md b/docs/advanced-configuration-options.md index 959f4e87bb4d..afcc6dfa90f3 100644 --- a/docs/advanced-configuration-options.md +++ b/docs/advanced-configuration-options.md @@ -14,16 +14,24 @@ Or as a quick workaround for an instrumentation bug, when byte code in one speci This option should not be used lightly, as it can leave some instrumentation partially applied, which could have unknown side-effects. -| 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.*" | +| 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.\*" | + +## Excluding specific classes loaders + +This option can be used to exclude classes loaded by given class loaders from being instrumented. + +| System property | Environment variable | Purpose | +|--------------------------------------|--------------------------------------|---------------------------------------------------------------------------------| +| otel.javaagent.exclude-class-loaders | OTEL_JAVAAGENT_EXCLUDE_CLASS_LOADERS | Ignore the specified class loaders, format is "my.package.MyClass,my.package2." | ## Running application with security manager This option can be used to let agent run with all privileges without being affected by security policy restricting some operations. | System property | Environment variable | Purpose | -|--------------------------------------------------------------|--------------------------------------------------------------|---------------------------------------| +| ------------------------------------------------------------ | ------------------------------------------------------------ | ------------------------------------- | | otel.javaagent.experimental.security-manager-support.enabled | OTEL_JAVAAGENT_EXPERIMENTAL_SECURITY_MANAGER_SUPPORT_ENABLED | Grant all privileges to agent code[1] | [1] Disclaimer: agent can provide application means for escaping security manager sandbox. Do not use diff --git a/docs/agent-features.md b/docs/agent-features.md index 92b494aa6006..22ad81e25be9 100644 --- a/docs/agent-features.md +++ b/docs/agent-features.md @@ -4,9 +4,8 @@ This lists out some of the features specific to java agents that OpenTelemetry A provides. - Bundled exporters - - [OTLP](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/protocol/otlp.md) - - Jaeger gRPC - - Logging + - [OTLP](https://opentelemetry.io/docs/specs/otlp/) + - Console - Zipkin - Bundled propagators - [W3C TraceContext / Baggage](https://www.w3.org/TR/trace-context/) @@ -31,7 +30,7 @@ provides. - 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 + - Can hook into ByteBuddy to customize bytecode manipulation - Noteworthy instrumentation - Log injection of IDs (logback, log4j2, log4j) - Automatic context propagation across `Executor`s diff --git a/docs/apidiffs/2.0.0_vs_1.32.0/opentelemetry-instrumentation-annotations.txt b/docs/apidiffs/2.0.0_vs_1.32.0/opentelemetry-instrumentation-annotations.txt new file mode 100644 index 000000000000..df26146497bf --- /dev/null +++ b/docs/apidiffs/2.0.0_vs_1.32.0/opentelemetry-instrumentation-annotations.txt @@ -0,0 +1,2 @@ +Comparing source compatibility of against +No changes. \ No newline at end of file diff --git a/docs/apidiffs/2.0.0_vs_1.32.0/opentelemetry-instrumentation-api.txt b/docs/apidiffs/2.0.0_vs_1.32.0/opentelemetry-instrumentation-api.txt new file mode 100644 index 000000000000..b66008e3cecc --- /dev/null +++ b/docs/apidiffs/2.0.0_vs_1.32.0/opentelemetry-instrumentation-api.txt @@ -0,0 +1,261 @@ +Comparing source compatibility of against ++++* NEW CLASS: PUBLIC(+) FINAL(+) io.opentelemetry.instrumentation.api.semconv.http.HttpClientAttributesExtractor (not serializable) + +++ CLASS FILE FORMAT VERSION: 52.0 <- n.a. + GENERIC TEMPLATES: +++ REQUEST:java.lang.Object, +++ RESPONSE:java.lang.Object + +++ NEW INTERFACE: io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor + +++ NEW INTERFACE: io.opentelemetry.instrumentation.api.internal.SpanKeyProvider + +++ NEW SUPERCLASS: io.opentelemetry.instrumentation.api.semconv.http.HttpCommonAttributesExtractor + +++* NEW METHOD: PUBLIC(+) STATIC(+) io.opentelemetry.instrumentation.api.semconv.http.HttpClientAttributesExtractorBuilder builder(io.opentelemetry.instrumentation.api.semconv.http.HttpClientAttributesGetter) + GENERIC TEMPLATES: +++ REQUEST:java.lang.Object, +++ RESPONSE:java.lang.Object + +++* NEW METHOD: PUBLIC(+) STATIC(+) io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor create(io.opentelemetry.instrumentation.api.semconv.http.HttpClientAttributesGetter) + GENERIC TEMPLATES: +++ REQUEST:java.lang.Object, +++ RESPONSE:java.lang.Object + +++ NEW METHOD: PUBLIC(+) io.opentelemetry.instrumentation.api.internal.SpanKey internalGetSpanKey() + +++ NEW METHOD: PUBLIC(+) void onEnd(io.opentelemetry.api.common.AttributesBuilder, io.opentelemetry.context.Context, java.lang.Object, java.lang.Object, java.lang.Throwable) + +++ NEW METHOD: PUBLIC(+) void onStart(io.opentelemetry.api.common.AttributesBuilder, io.opentelemetry.context.Context, java.lang.Object) ++++ NEW CLASS: PUBLIC(+) FINAL(+) io.opentelemetry.instrumentation.api.semconv.http.HttpClientAttributesExtractorBuilder (not serializable) + +++ CLASS FILE FORMAT VERSION: 52.0 <- n.a. + GENERIC TEMPLATES: +++ REQUEST:java.lang.Object, +++ RESPONSE:java.lang.Object + +++ NEW SUPERCLASS: java.lang.Object + +++ NEW METHOD: PUBLIC(+) io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor build() + +++ NEW METHOD: PUBLIC(+) io.opentelemetry.instrumentation.api.semconv.http.HttpClientAttributesExtractorBuilder setCapturedRequestHeaders(java.util.List) + +++ NEW METHOD: PUBLIC(+) io.opentelemetry.instrumentation.api.semconv.http.HttpClientAttributesExtractorBuilder setCapturedResponseHeaders(java.util.List) + +++ NEW METHOD: PUBLIC(+) io.opentelemetry.instrumentation.api.semconv.http.HttpClientAttributesExtractorBuilder setKnownMethods(java.util.Set) ++++ NEW INTERFACE: PUBLIC(+) ABSTRACT(+) io.opentelemetry.instrumentation.api.semconv.http.HttpClientAttributesGetter (not serializable) + +++ CLASS FILE FORMAT VERSION: 52.0 <- n.a. + GENERIC TEMPLATES: +++ REQUEST:java.lang.Object, +++ RESPONSE:java.lang.Object + +++ NEW INTERFACE: io.opentelemetry.instrumentation.api.semconv.http.HttpCommonAttributesGetter + +++ NEW INTERFACE: io.opentelemetry.instrumentation.api.semconv.network.NetworkAttributesGetter + +++ NEW INTERFACE: io.opentelemetry.instrumentation.api.semconv.network.ServerAttributesGetter + +++ NEW SUPERCLASS: java.lang.Object + +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) java.lang.String getServerAddress(java.lang.Object) + +++ NEW ANNOTATION: javax.annotation.Nullable + +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) java.lang.Integer getServerPort(java.lang.Object) + +++ NEW ANNOTATION: javax.annotation.Nullable + +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) java.lang.String getUrlFull(java.lang.Object) + +++ NEW ANNOTATION: javax.annotation.Nullable ++++ NEW CLASS: PUBLIC(+) FINAL(+) io.opentelemetry.instrumentation.api.semconv.http.HttpClientMetrics (not serializable) + +++ CLASS FILE FORMAT VERSION: 52.0 <- n.a. + +++ NEW INTERFACE: io.opentelemetry.instrumentation.api.instrumenter.OperationListener + +++ NEW SUPERCLASS: java.lang.Object + +++ NEW METHOD: PUBLIC(+) STATIC(+) io.opentelemetry.instrumentation.api.instrumenter.OperationMetrics get() + +++ NEW METHOD: PUBLIC(+) void onEnd(io.opentelemetry.context.Context, io.opentelemetry.api.common.Attributes, long) + +++ NEW METHOD: PUBLIC(+) io.opentelemetry.context.Context onStart(io.opentelemetry.context.Context, io.opentelemetry.api.common.Attributes, long) ++++ NEW CLASS: PUBLIC(+) FINAL(+) io.opentelemetry.instrumentation.api.semconv.http.HttpClientRequestResendCount (not serializable) + +++ CLASS FILE FORMAT VERSION: 52.0 <- n.a. + +++ NEW SUPERCLASS: java.lang.Object + +++ NEW METHOD: PUBLIC(+) STATIC(+) int get(io.opentelemetry.context.Context) + +++ NEW METHOD: PUBLIC(+) STATIC(+) io.opentelemetry.context.Context initialize(io.opentelemetry.context.Context) ++++ NEW INTERFACE: PUBLIC(+) ABSTRACT(+) io.opentelemetry.instrumentation.api.semconv.http.HttpCommonAttributesGetter (not serializable) + +++ CLASS FILE FORMAT VERSION: 52.0 <- n.a. + GENERIC TEMPLATES: +++ REQUEST:java.lang.Object, +++ RESPONSE:java.lang.Object + +++ NEW SUPERCLASS: java.lang.Object + +++ NEW METHOD: PUBLIC(+) java.lang.String getErrorType(java.lang.Object, java.lang.Object, java.lang.Throwable) + +++ NEW ANNOTATION: javax.annotation.Nullable + +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) java.util.List getHttpRequestHeader(java.lang.Object, java.lang.String) + +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) java.lang.String getHttpRequestMethod(java.lang.Object) + +++ NEW ANNOTATION: javax.annotation.Nullable + +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) java.util.List getHttpResponseHeader(java.lang.Object, java.lang.Object, java.lang.String) + +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) java.lang.Integer getHttpResponseStatusCode(java.lang.Object, java.lang.Object, java.lang.Throwable) + +++ NEW ANNOTATION: javax.annotation.Nullable ++++* NEW CLASS: PUBLIC(+) FINAL(+) io.opentelemetry.instrumentation.api.semconv.http.HttpServerAttributesExtractor (not serializable) + +++ CLASS FILE FORMAT VERSION: 52.0 <- n.a. + GENERIC TEMPLATES: +++ REQUEST:java.lang.Object, +++ RESPONSE:java.lang.Object + +++ NEW INTERFACE: io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor + +++ NEW INTERFACE: io.opentelemetry.instrumentation.api.internal.SpanKeyProvider + +++ NEW SUPERCLASS: io.opentelemetry.instrumentation.api.semconv.http.HttpCommonAttributesExtractor + +++* NEW METHOD: PUBLIC(+) STATIC(+) io.opentelemetry.instrumentation.api.semconv.http.HttpServerAttributesExtractorBuilder builder(io.opentelemetry.instrumentation.api.semconv.http.HttpServerAttributesGetter) + GENERIC TEMPLATES: +++ REQUEST:java.lang.Object, +++ RESPONSE:java.lang.Object + +++* NEW METHOD: PUBLIC(+) STATIC(+) io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor create(io.opentelemetry.instrumentation.api.semconv.http.HttpServerAttributesGetter) + GENERIC TEMPLATES: +++ REQUEST:java.lang.Object, +++ RESPONSE:java.lang.Object + +++ NEW METHOD: PUBLIC(+) io.opentelemetry.instrumentation.api.internal.SpanKey internalGetSpanKey() + +++ NEW METHOD: PUBLIC(+) void onEnd(io.opentelemetry.api.common.AttributesBuilder, io.opentelemetry.context.Context, java.lang.Object, java.lang.Object, java.lang.Throwable) + +++ NEW METHOD: PUBLIC(+) void onStart(io.opentelemetry.api.common.AttributesBuilder, io.opentelemetry.context.Context, java.lang.Object) ++++ NEW CLASS: PUBLIC(+) FINAL(+) io.opentelemetry.instrumentation.api.semconv.http.HttpServerAttributesExtractorBuilder (not serializable) + +++ CLASS FILE FORMAT VERSION: 52.0 <- n.a. + GENERIC TEMPLATES: +++ REQUEST:java.lang.Object, +++ RESPONSE:java.lang.Object + +++ NEW SUPERCLASS: java.lang.Object + +++ NEW METHOD: PUBLIC(+) io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor build() + +++ NEW METHOD: PUBLIC(+) io.opentelemetry.instrumentation.api.semconv.http.HttpServerAttributesExtractorBuilder setCapturedRequestHeaders(java.util.List) + +++ NEW METHOD: PUBLIC(+) io.opentelemetry.instrumentation.api.semconv.http.HttpServerAttributesExtractorBuilder setCapturedResponseHeaders(java.util.List) + +++ NEW METHOD: PUBLIC(+) io.opentelemetry.instrumentation.api.semconv.http.HttpServerAttributesExtractorBuilder setKnownMethods(java.util.Set) ++++ NEW INTERFACE: PUBLIC(+) ABSTRACT(+) io.opentelemetry.instrumentation.api.semconv.http.HttpServerAttributesGetter (not serializable) + +++ CLASS FILE FORMAT VERSION: 52.0 <- n.a. + GENERIC TEMPLATES: +++ REQUEST:java.lang.Object, +++ RESPONSE:java.lang.Object + +++ NEW INTERFACE: io.opentelemetry.instrumentation.api.semconv.http.HttpCommonAttributesGetter + +++ NEW INTERFACE: io.opentelemetry.instrumentation.api.semconv.network.NetworkAttributesGetter + +++ NEW INTERFACE: io.opentelemetry.instrumentation.api.semconv.url.UrlAttributesGetter + +++ NEW INTERFACE: io.opentelemetry.instrumentation.api.semconv.network.ClientAttributesGetter + +++ NEW SUPERCLASS: java.lang.Object + +++ NEW METHOD: PUBLIC(+) java.lang.String getHttpRoute(java.lang.Object) + +++ NEW ANNOTATION: javax.annotation.Nullable + +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) java.lang.String getUrlPath(java.lang.Object) + +++ NEW ANNOTATION: javax.annotation.Nullable + +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) java.lang.String getUrlQuery(java.lang.Object) + +++ NEW ANNOTATION: javax.annotation.Nullable + +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) java.lang.String getUrlScheme(java.lang.Object) + +++ NEW ANNOTATION: javax.annotation.Nullable ++++ NEW CLASS: PUBLIC(+) FINAL(+) io.opentelemetry.instrumentation.api.semconv.http.HttpServerMetrics (not serializable) + +++ CLASS FILE FORMAT VERSION: 52.0 <- n.a. + +++ NEW INTERFACE: io.opentelemetry.instrumentation.api.instrumenter.OperationListener + +++ NEW SUPERCLASS: java.lang.Object + +++ NEW METHOD: PUBLIC(+) STATIC(+) io.opentelemetry.instrumentation.api.instrumenter.OperationMetrics get() + +++ NEW METHOD: PUBLIC(+) void onEnd(io.opentelemetry.context.Context, io.opentelemetry.api.common.Attributes, long) + +++ NEW METHOD: PUBLIC(+) io.opentelemetry.context.Context onStart(io.opentelemetry.context.Context, io.opentelemetry.api.common.Attributes, long) ++++* NEW CLASS: PUBLIC(+) FINAL(+) io.opentelemetry.instrumentation.api.semconv.http.HttpServerRoute (not serializable) + +++ CLASS FILE FORMAT VERSION: 52.0 <- n.a. + +++ NEW SUPERCLASS: java.lang.Object + +++* NEW METHOD: PUBLIC(+) STATIC(+) io.opentelemetry.instrumentation.api.semconv.http.HttpServerRouteBuilder builder(io.opentelemetry.instrumentation.api.semconv.http.HttpServerAttributesGetter) + GENERIC TEMPLATES: +++ REQUEST:java.lang.Object + +++* NEW METHOD: PUBLIC(+) STATIC(+) io.opentelemetry.instrumentation.api.instrumenter.ContextCustomizer create(io.opentelemetry.instrumentation.api.semconv.http.HttpServerAttributesGetter) + GENERIC TEMPLATES: +++ REQUEST:java.lang.Object + +++ NEW METHOD: PUBLIC(+) STATIC(+) void update(io.opentelemetry.context.Context, io.opentelemetry.instrumentation.api.semconv.http.HttpServerRouteSource, java.lang.String) + +++* NEW METHOD: PUBLIC(+) STATIC(+) void update(io.opentelemetry.context.Context, io.opentelemetry.instrumentation.api.semconv.http.HttpServerRouteSource, io.opentelemetry.instrumentation.api.semconv.http.HttpServerRouteGetter, java.lang.Object) + GENERIC TEMPLATES: +++ T:java.lang.Object + +++* NEW METHOD: PUBLIC(+) STATIC(+) void update(io.opentelemetry.context.Context, io.opentelemetry.instrumentation.api.semconv.http.HttpServerRouteSource, io.opentelemetry.instrumentation.api.semconv.http.HttpServerRouteBiGetter, java.lang.Object, java.lang.Object) + GENERIC TEMPLATES: +++ T:java.lang.Object, +++ U:java.lang.Object ++++ NEW INTERFACE: PUBLIC(+) ABSTRACT(+) io.opentelemetry.instrumentation.api.semconv.http.HttpServerRouteBiGetter (not serializable) + +++ CLASS FILE FORMAT VERSION: 52.0 <- n.a. + GENERIC TEMPLATES: +++ T:java.lang.Object, +++ U:java.lang.Object + +++ NEW SUPERCLASS: java.lang.Object + +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) java.lang.String get(io.opentelemetry.context.Context, java.lang.Object, java.lang.Object) + +++ NEW ANNOTATION: javax.annotation.Nullable + +++ NEW ANNOTATION: java.lang.FunctionalInterface ++++ NEW CLASS: PUBLIC(+) FINAL(+) io.opentelemetry.instrumentation.api.semconv.http.HttpServerRouteBuilder (not serializable) + +++ CLASS FILE FORMAT VERSION: 52.0 <- n.a. + GENERIC TEMPLATES: +++ REQUEST:java.lang.Object + +++ NEW SUPERCLASS: java.lang.Object + +++ NEW METHOD: PUBLIC(+) io.opentelemetry.instrumentation.api.instrumenter.ContextCustomizer build() + +++ NEW METHOD: PUBLIC(+) io.opentelemetry.instrumentation.api.semconv.http.HttpServerRouteBuilder setKnownMethods(java.util.Set) ++++ NEW INTERFACE: PUBLIC(+) ABSTRACT(+) io.opentelemetry.instrumentation.api.semconv.http.HttpServerRouteGetter (not serializable) + +++ CLASS FILE FORMAT VERSION: 52.0 <- n.a. + GENERIC TEMPLATES: +++ T:java.lang.Object + +++ NEW SUPERCLASS: java.lang.Object + +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) java.lang.String get(io.opentelemetry.context.Context, java.lang.Object) + +++ NEW ANNOTATION: javax.annotation.Nullable + +++ NEW ANNOTATION: java.lang.FunctionalInterface ++++ NEW ENUM: PUBLIC(+) FINAL(+) io.opentelemetry.instrumentation.api.semconv.http.HttpServerRouteSource (compatible) + +++ CLASS FILE FORMAT VERSION: 52.0 <- n.a. + +++ NEW INTERFACE: java.lang.constant.Constable + +++ NEW INTERFACE: java.lang.Comparable + +++ NEW INTERFACE: java.io.Serializable + +++ NEW SUPERCLASS: java.lang.Enum + +++ NEW FIELD: PUBLIC(+) STATIC(+) FINAL(+) io.opentelemetry.instrumentation.api.semconv.http.HttpServerRouteSource SERVER_FILTER + +++ NEW FIELD: PUBLIC(+) STATIC(+) FINAL(+) io.opentelemetry.instrumentation.api.semconv.http.HttpServerRouteSource CONTROLLER + +++ NEW FIELD: PUBLIC(+) STATIC(+) FINAL(+) io.opentelemetry.instrumentation.api.semconv.http.HttpServerRouteSource SERVER + +++ NEW FIELD: PUBLIC(+) STATIC(+) FINAL(+) io.opentelemetry.instrumentation.api.semconv.http.HttpServerRouteSource NESTED_CONTROLLER + +++ NEW METHOD: PUBLIC(+) STATIC(+) io.opentelemetry.instrumentation.api.semconv.http.HttpServerRouteSource valueOf(java.lang.String) + +++ NEW METHOD: PUBLIC(+) STATIC(+) io.opentelemetry.instrumentation.api.semconv.http.HttpServerRouteSource[] values() ++++* NEW CLASS: PUBLIC(+) FINAL(+) io.opentelemetry.instrumentation.api.semconv.http.HttpSpanNameExtractor (not serializable) + +++ CLASS FILE FORMAT VERSION: 52.0 <- n.a. + +++ NEW SUPERCLASS: java.lang.Object + +++* NEW METHOD: PUBLIC(+) STATIC(+) io.opentelemetry.instrumentation.api.semconv.http.HttpSpanNameExtractorBuilder builder(io.opentelemetry.instrumentation.api.semconv.http.HttpClientAttributesGetter) + GENERIC TEMPLATES: +++ REQUEST:java.lang.Object + +++* NEW METHOD: PUBLIC(+) STATIC(+) io.opentelemetry.instrumentation.api.semconv.http.HttpSpanNameExtractorBuilder builder(io.opentelemetry.instrumentation.api.semconv.http.HttpServerAttributesGetter) + GENERIC TEMPLATES: +++ REQUEST:java.lang.Object + +++* NEW METHOD: PUBLIC(+) STATIC(+) io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor create(io.opentelemetry.instrumentation.api.semconv.http.HttpClientAttributesGetter) + GENERIC TEMPLATES: +++ REQUEST:java.lang.Object + +++* NEW METHOD: PUBLIC(+) STATIC(+) io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor create(io.opentelemetry.instrumentation.api.semconv.http.HttpServerAttributesGetter) + GENERIC TEMPLATES: +++ REQUEST:java.lang.Object ++++ NEW CLASS: PUBLIC(+) FINAL(+) io.opentelemetry.instrumentation.api.semconv.http.HttpSpanNameExtractorBuilder (not serializable) + +++ CLASS FILE FORMAT VERSION: 52.0 <- n.a. + GENERIC TEMPLATES: +++ REQUEST:java.lang.Object + +++ NEW SUPERCLASS: java.lang.Object + +++ NEW CONSTRUCTOR: PUBLIC(+) HttpSpanNameExtractorBuilder(io.opentelemetry.instrumentation.api.semconv.http.HttpClientAttributesGetter, io.opentelemetry.instrumentation.api.semconv.http.HttpServerAttributesGetter) + +++ NEW METHOD: PUBLIC(+) io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor build() + +++ NEW METHOD: PUBLIC(+) io.opentelemetry.instrumentation.api.semconv.http.HttpSpanNameExtractorBuilder setKnownMethods(java.util.Set) ++++* NEW CLASS: PUBLIC(+) FINAL(+) io.opentelemetry.instrumentation.api.semconv.http.HttpSpanStatusExtractor (not serializable) + +++ CLASS FILE FORMAT VERSION: 52.0 <- n.a. + GENERIC TEMPLATES: +++ REQUEST:java.lang.Object, +++ RESPONSE:java.lang.Object + +++ NEW INTERFACE: io.opentelemetry.instrumentation.api.instrumenter.SpanStatusExtractor + +++ NEW SUPERCLASS: java.lang.Object + +++* NEW METHOD: PUBLIC(+) STATIC(+) io.opentelemetry.instrumentation.api.instrumenter.SpanStatusExtractor create(io.opentelemetry.instrumentation.api.semconv.http.HttpClientAttributesGetter) + GENERIC TEMPLATES: +++ REQUEST:java.lang.Object, +++ RESPONSE:java.lang.Object + +++* NEW METHOD: PUBLIC(+) STATIC(+) io.opentelemetry.instrumentation.api.instrumenter.SpanStatusExtractor create(io.opentelemetry.instrumentation.api.semconv.http.HttpServerAttributesGetter) + GENERIC TEMPLATES: +++ REQUEST:java.lang.Object, +++ RESPONSE:java.lang.Object + +++ NEW METHOD: PUBLIC(+) void extract(io.opentelemetry.instrumentation.api.instrumenter.SpanStatusBuilder, java.lang.Object, java.lang.Object, java.lang.Throwable) ++++* NEW CLASS: PUBLIC(+) FINAL(+) io.opentelemetry.instrumentation.api.semconv.network.ClientAttributesExtractor (not serializable) + +++ CLASS FILE FORMAT VERSION: 52.0 <- n.a. + GENERIC TEMPLATES: +++ REQUEST:java.lang.Object, +++ RESPONSE:java.lang.Object + +++ NEW INTERFACE: io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor + +++ NEW SUPERCLASS: java.lang.Object + +++* NEW METHOD: PUBLIC(+) STATIC(+) io.opentelemetry.instrumentation.api.semconv.network.ClientAttributesExtractor create(io.opentelemetry.instrumentation.api.semconv.network.ClientAttributesGetter) + GENERIC TEMPLATES: +++ REQUEST:java.lang.Object, +++ RESPONSE:java.lang.Object + +++ NEW METHOD: PUBLIC(+) void onEnd(io.opentelemetry.api.common.AttributesBuilder, io.opentelemetry.context.Context, java.lang.Object, java.lang.Object, java.lang.Throwable) + +++ NEW METHOD: PUBLIC(+) void onStart(io.opentelemetry.api.common.AttributesBuilder, io.opentelemetry.context.Context, java.lang.Object) ++++ NEW INTERFACE: PUBLIC(+) ABSTRACT(+) io.opentelemetry.instrumentation.api.semconv.network.ClientAttributesGetter (not serializable) + +++ CLASS FILE FORMAT VERSION: 52.0 <- n.a. + GENERIC TEMPLATES: +++ REQUEST:java.lang.Object + +++ NEW SUPERCLASS: java.lang.Object + +++ NEW METHOD: PUBLIC(+) java.lang.String getClientAddress(java.lang.Object) + +++ NEW ANNOTATION: javax.annotation.Nullable + +++ NEW METHOD: PUBLIC(+) java.lang.Integer getClientPort(java.lang.Object) + +++ NEW ANNOTATION: javax.annotation.Nullable ++++* NEW CLASS: PUBLIC(+) FINAL(+) io.opentelemetry.instrumentation.api.semconv.network.NetworkAttributesExtractor (not serializable) + +++ CLASS FILE FORMAT VERSION: 52.0 <- n.a. + GENERIC TEMPLATES: +++ REQUEST:java.lang.Object, +++ RESPONSE:java.lang.Object + +++ NEW INTERFACE: io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor + +++ NEW SUPERCLASS: java.lang.Object + +++* NEW METHOD: PUBLIC(+) STATIC(+) io.opentelemetry.instrumentation.api.semconv.network.NetworkAttributesExtractor create(io.opentelemetry.instrumentation.api.semconv.network.NetworkAttributesGetter) + GENERIC TEMPLATES: +++ REQUEST:java.lang.Object, +++ RESPONSE:java.lang.Object + +++ NEW METHOD: PUBLIC(+) void onEnd(io.opentelemetry.api.common.AttributesBuilder, io.opentelemetry.context.Context, java.lang.Object, java.lang.Object, java.lang.Throwable) + +++ NEW METHOD: PUBLIC(+) void onStart(io.opentelemetry.api.common.AttributesBuilder, io.opentelemetry.context.Context, java.lang.Object) ++++ NEW INTERFACE: PUBLIC(+) ABSTRACT(+) io.opentelemetry.instrumentation.api.semconv.network.NetworkAttributesGetter (not serializable) + +++ CLASS FILE FORMAT VERSION: 52.0 <- n.a. + GENERIC TEMPLATES: +++ REQUEST:java.lang.Object, +++ RESPONSE:java.lang.Object + +++ NEW SUPERCLASS: java.lang.Object + +++ NEW METHOD: PUBLIC(+) java.lang.String getNetworkLocalAddress(java.lang.Object, java.lang.Object) + +++ NEW ANNOTATION: javax.annotation.Nullable + +++ NEW METHOD: PUBLIC(+) java.net.InetSocketAddress getNetworkLocalInetSocketAddress(java.lang.Object, java.lang.Object) + +++ NEW ANNOTATION: javax.annotation.Nullable + +++ NEW METHOD: PUBLIC(+) java.lang.Integer getNetworkLocalPort(java.lang.Object, java.lang.Object) + +++ NEW ANNOTATION: javax.annotation.Nullable + +++ NEW METHOD: PUBLIC(+) java.lang.String getNetworkPeerAddress(java.lang.Object, java.lang.Object) + +++ NEW ANNOTATION: javax.annotation.Nullable + +++ NEW METHOD: PUBLIC(+) java.net.InetSocketAddress getNetworkPeerInetSocketAddress(java.lang.Object, java.lang.Object) + +++ NEW ANNOTATION: javax.annotation.Nullable + +++ NEW METHOD: PUBLIC(+) java.lang.Integer getNetworkPeerPort(java.lang.Object, java.lang.Object) + +++ NEW ANNOTATION: javax.annotation.Nullable + +++ NEW METHOD: PUBLIC(+) java.lang.String getNetworkProtocolName(java.lang.Object, java.lang.Object) + +++ NEW ANNOTATION: javax.annotation.Nullable + +++ NEW METHOD: PUBLIC(+) java.lang.String getNetworkProtocolVersion(java.lang.Object, java.lang.Object) + +++ NEW ANNOTATION: javax.annotation.Nullable + +++ NEW METHOD: PUBLIC(+) java.lang.String getNetworkTransport(java.lang.Object, java.lang.Object) + +++ NEW ANNOTATION: javax.annotation.Nullable + +++ NEW METHOD: PUBLIC(+) java.lang.String getNetworkType(java.lang.Object, java.lang.Object) + +++ NEW ANNOTATION: javax.annotation.Nullable ++++* NEW CLASS: PUBLIC(+) FINAL(+) io.opentelemetry.instrumentation.api.semconv.network.ServerAttributesExtractor (not serializable) + +++ CLASS FILE FORMAT VERSION: 52.0 <- n.a. + GENERIC TEMPLATES: +++ REQUEST:java.lang.Object, +++ RESPONSE:java.lang.Object + +++ NEW INTERFACE: io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor + +++ NEW SUPERCLASS: java.lang.Object + +++* NEW METHOD: PUBLIC(+) STATIC(+) io.opentelemetry.instrumentation.api.semconv.network.ServerAttributesExtractor create(io.opentelemetry.instrumentation.api.semconv.network.ServerAttributesGetter) + GENERIC TEMPLATES: +++ REQUEST:java.lang.Object, +++ RESPONSE:java.lang.Object + +++ NEW METHOD: PUBLIC(+) void onEnd(io.opentelemetry.api.common.AttributesBuilder, io.opentelemetry.context.Context, java.lang.Object, java.lang.Object, java.lang.Throwable) + +++ NEW METHOD: PUBLIC(+) void onStart(io.opentelemetry.api.common.AttributesBuilder, io.opentelemetry.context.Context, java.lang.Object) ++++ NEW INTERFACE: PUBLIC(+) ABSTRACT(+) io.opentelemetry.instrumentation.api.semconv.network.ServerAttributesGetter (not serializable) + +++ CLASS FILE FORMAT VERSION: 52.0 <- n.a. + GENERIC TEMPLATES: +++ REQUEST:java.lang.Object + +++ NEW SUPERCLASS: java.lang.Object + +++ NEW METHOD: PUBLIC(+) java.lang.String getServerAddress(java.lang.Object) + +++ NEW ANNOTATION: javax.annotation.Nullable + +++ NEW METHOD: PUBLIC(+) java.lang.Integer getServerPort(java.lang.Object) + +++ NEW ANNOTATION: javax.annotation.Nullable ++++* NEW CLASS: PUBLIC(+) FINAL(+) io.opentelemetry.instrumentation.api.semconv.url.UrlAttributesExtractor (not serializable) + +++ CLASS FILE FORMAT VERSION: 52.0 <- n.a. + GENERIC TEMPLATES: +++ REQUEST:java.lang.Object, +++ RESPONSE:java.lang.Object + +++ NEW INTERFACE: io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor + +++ NEW SUPERCLASS: java.lang.Object + +++* NEW METHOD: PUBLIC(+) STATIC(+) io.opentelemetry.instrumentation.api.semconv.url.UrlAttributesExtractor create(io.opentelemetry.instrumentation.api.semconv.url.UrlAttributesGetter) + GENERIC TEMPLATES: +++ REQUEST:java.lang.Object, +++ RESPONSE:java.lang.Object + +++ NEW METHOD: PUBLIC(+) void onEnd(io.opentelemetry.api.common.AttributesBuilder, io.opentelemetry.context.Context, java.lang.Object, java.lang.Object, java.lang.Throwable) + +++ NEW METHOD: PUBLIC(+) void onStart(io.opentelemetry.api.common.AttributesBuilder, io.opentelemetry.context.Context, java.lang.Object) ++++ NEW INTERFACE: PUBLIC(+) ABSTRACT(+) io.opentelemetry.instrumentation.api.semconv.url.UrlAttributesGetter (not serializable) + +++ CLASS FILE FORMAT VERSION: 52.0 <- n.a. + GENERIC TEMPLATES: +++ REQUEST:java.lang.Object + +++ NEW SUPERCLASS: java.lang.Object + +++ NEW METHOD: PUBLIC(+) java.lang.String getUrlPath(java.lang.Object) + +++ NEW ANNOTATION: javax.annotation.Nullable + +++ NEW METHOD: PUBLIC(+) java.lang.String getUrlQuery(java.lang.Object) + +++ NEW ANNOTATION: javax.annotation.Nullable + +++ NEW METHOD: PUBLIC(+) java.lang.String getUrlScheme(java.lang.Object) + +++ NEW ANNOTATION: javax.annotation.Nullable diff --git a/docs/apidiffs/current_vs_latest/opentelemetry-instrumentation-annotations.txt b/docs/apidiffs/current_vs_latest/opentelemetry-instrumentation-annotations.txt index df26146497bf..96e3e62e1bc3 100644 --- a/docs/apidiffs/current_vs_latest/opentelemetry-instrumentation-annotations.txt +++ b/docs/apidiffs/current_vs_latest/opentelemetry-instrumentation-annotations.txt @@ -1,2 +1,2 @@ -Comparing source compatibility of against +Comparing source compatibility of opentelemetry-instrumentation-annotations-2.8.0-SNAPSHOT.jar against opentelemetry-instrumentation-annotations-2.7.0.jar No changes. \ No newline at end of file diff --git a/docs/apidiffs/current_vs_latest/opentelemetry-instrumentation-api.txt b/docs/apidiffs/current_vs_latest/opentelemetry-instrumentation-api.txt index df26146497bf..c781552c9423 100644 --- a/docs/apidiffs/current_vs_latest/opentelemetry-instrumentation-api.txt +++ b/docs/apidiffs/current_vs_latest/opentelemetry-instrumentation-api.txt @@ -1,2 +1,2 @@ -Comparing source compatibility of against +Comparing source compatibility of opentelemetry-instrumentation-api-2.8.0-SNAPSHOT.jar against opentelemetry-instrumentation-api-2.7.0.jar No changes. \ No newline at end of file diff --git a/docs/apidiffs/current_vs_latest/opentelemetry-spring-boot-autoconfigure.txt b/docs/apidiffs/current_vs_latest/opentelemetry-spring-boot-autoconfigure.txt new file mode 100644 index 000000000000..b7b92ce09842 --- /dev/null +++ b/docs/apidiffs/current_vs_latest/opentelemetry-spring-boot-autoconfigure.txt @@ -0,0 +1,2 @@ +Comparing source compatibility of opentelemetry-spring-boot-autoconfigure-2.8.0-SNAPSHOT.jar against opentelemetry-spring-boot-autoconfigure-2.7.0.jar +No changes. \ No newline at end of file diff --git a/docs/apidiffs/current_vs_latest/opentelemetry-spring-boot-starter.txt b/docs/apidiffs/current_vs_latest/opentelemetry-spring-boot-starter.txt new file mode 100644 index 000000000000..c9acd1d6034d --- /dev/null +++ b/docs/apidiffs/current_vs_latest/opentelemetry-spring-boot-starter.txt @@ -0,0 +1,2 @@ +Comparing source compatibility of opentelemetry-spring-boot-starter-2.8.0-SNAPSHOT.jar against opentelemetry-spring-boot-starter-2.7.0.jar +No changes. \ No newline at end of file diff --git a/docs/contributing/debugging.md b/docs/contributing/debugging.md index 9bed15a2bbf0..4885c26412f9 100644 --- a/docs/contributing/debugging.md +++ b/docs/contributing/debugging.md @@ -30,6 +30,14 @@ System.out.println(); Thread.dumpStack(); ``` +Byte Buddy can also output the modified class files to a directory which can be decompiled to see +exactly what changes are taking place. Add the following to your JVM startup arguments with +an existing target directory defined: + +```shell +-Dnet.bytebuddy.dump=/some/path +``` + ## Agent initialization code If you want to debug agent initialization code (e.g. `OpenTelemetryAgent`, `AgentInitializer`, diff --git a/docs/contributing/intellij-setup-and-troubleshooting.md b/docs/contributing/intellij-setup-and-troubleshooting.md index 831f47043b1f..4bc57ac18d70 100644 --- a/docs/contributing/intellij-setup-and-troubleshooting.md +++ b/docs/contributing/intellij-setup-and-troubleshooting.md @@ -19,6 +19,28 @@ Configuration: Note: If google-java-format generates errors in Intellij, see . +## Load/Unload Modules + +This project has many modules, which have many dependencies. +Therefore, IntelliJ indexes a lot and consumes a lot of CPU/memory resources. + +To minimize IntelliJ's indexing and resource utilization, +[unload any modules](https://www.jetbrains.com/help/idea/unloading-modules.html) +on which you are not actively working. + +Specifically, unload all modules, and then selectively load the modules on which you need to work. +IntelliJ will prompt you to load additional modules on which the selected modules depend. + +If you are working on a specific instrumentation, you can load only the modules for that instrumentation. +For example, to load the modules for the Spring Boot autoconfigure instrumentation, run: + +```shell +./docs/contributing/selectModules.kts instrumentation/spring/spring-boot-autoconfigure/ +``` + +Install the [Kotlin executable](https://kotlinlang.org/docs/tutorials/command-line.html) +if you don't have it already. + ## Troubleshooting Occasionally, Intellij gets confused, maybe due to the number of modules in this project, @@ -26,14 +48,14 @@ maybe due to other reasons. In any case, here's some things that might help: ### Invalidate Caches > "Just restart" -* Go to File > Invalidate Caches... -* Unselect all the options -* Click the "Just restart" link +- Go to File > Invalidate Caches... +- Unselect all the options +- Click the "Just restart" link This seems to fix more issues than just closing and re-opening Intellij :shrug:. ### Delete your `.idea` directory -* Close Intellij -* Delete the `.idea` directory in the root directory of your local repository -* Open Intellij +- Close Intellij +- Delete the `.idea` directory in the root directory of your local repository +- Open Intellij diff --git a/docs/contributing/javaagent-structure.md b/docs/contributing/javaagent-structure.md index 90271abc0868..6213eda3f97a 100644 --- a/docs/contributing/javaagent-structure.md +++ b/docs/contributing/javaagent-structure.md @@ -3,10 +3,10 @@ The javaagent can be logically divided into several parts, based on the class loader that contains particular classes (and resources) in the runtime: -* The main agent class living in the system class loader. -* Classes that live in the bootstrap class loader. -* Classes that live in the agent class loader. -* Javaagent extensions, and the extension class loader(s). +- The main agent class living in the system class loader. +- Classes that live in the bootstrap class loader. +- Classes that live in the agent class loader. +- Javaagent extensions, and the extension class loader(s). ## System class loader @@ -25,31 +25,31 @@ Inside the javaagent jar, this class is located in the `io/opentelemetry/javaage The bootstrap class loader contains several modules: -* **The `javaagent-bootstrap` module**: +- **The `javaagent-bootstrap` module**: it contains classes that continue the initialization work started by `OpenTelemetryAgent`, as well as some internal javaagent classes and interfaces that must be globally available to the whole application. This module is internal and its APIs are considered unstable. -* **The `instrumentation-api` and `instrumentation-api-semconv` modules**: +- **The `instrumentation-api` and `instrumentation-api-incubator` modules**: these modules contain the [Instrumenter API](using-instrumenter-api.md) and other related utilities. Because they are used by almost all instrumentations, they must be globally available to all classloaders running within the instrumented application. The classes located in these modules are used by both javaagent and library instrumentations - they all must be usable even without the javaagent present. -* **The `instrumentation-annotations-support` module**: +- **The `instrumentation-annotations-support` module**: it contains classes that provide support for annotation-based auto-instrumentation, e.g. the `@WithSpan` annotation. This module is internal and its APIs are considered unstable. -* **The `io.opentelemetry.javaagent.bootstrap` package from the `javaagent-extension-api` module**: +- **The `io.opentelemetry.javaagent.bootstrap` package from the `javaagent-extension-api` module**: this package contains several instrumentation utilities that are only usable when an application is instrumented with the javaagent; for example, the `Java8BytecodeBridge` that should be used inside advice classes. -* All modules using the `otel.javaagent-bootstrap` Gradle plugin: +- All modules using the `otel.javaagent-bootstrap` Gradle plugin: these modules contain instrumentation-specific classes that must be globally available in the bootstrap class loader. For example, classes that are used to coordinate different `InstrumentationModule`s, like the common utilities for storing Servlet context path, or the thread local switch used to coordinate different Kafka consumer instrumentations. By convention, all these modules are named according to this pattern: `:instrumentation:...:bootstrap`. -* The [OpenTelemetry API](https://github.com/open-telemetry/opentelemetry-java/tree/main/api/all). +- The [OpenTelemetry API](https://github.com/open-telemetry/opentelemetry-java/tree/main/api/all). Inside the javaagent jar, these classes are all located under the `io/opentelemetry/javaagent/` directory. Aside from the javaagent-specific `javaagent-bootstrap` and `javaagent-extension-api` @@ -61,27 +61,27 @@ versions of some of our APIs (`opentelemetry-api`, `instrumentation-api`). The agent class loader contains almost everything else not mentioned before, including: -* **The `javaagent-tooling` module**: +- **The `javaagent-tooling` module**: this module picks up the initialization process started by `OpenTelemetryAgent` and `javaagent-bootstrap` and actually finishes the work, starting up the OpenTelemetry SDK and building and installing the `ClassFileTransformer` in the JVM. The javaagent uses [ByteBuddy](https://bytebuddy.net) to configure and construct the `ClassFileTransformer`. This module is internal and its APIs are considered unstable. -* **The `muzzle` module**: +- **The `muzzle` module**: it contains classes that are internally used by [muzzle](muzzle.md), our safety net feature. This module is internal and its APIs are considered unstable. -* **The `io.opentelemetry.javaagent.extension` package from the `javaagent-extension-api` module**: +- **The `io.opentelemetry.javaagent.extension` package from the `javaagent-extension-api` module**: this package contains common extension points and SPIs that can be used to customize the agent behavior. -* All modules using the `otel.javaagent-instrumentation` Gradle plugin: +- All modules using the `otel.javaagent-instrumentation` Gradle plugin: these modules contain actual javaagent instrumentations. Almost all of them implement the `InstrumentationModule`, some of them include a library instrumentation as an `implementation` dependency. You can read more about writing instrumentations [here](writing-instrumentation.md). By convention, all these modules are named according to this pattern: `:instrumentation:...:javaagent`. -* The [OpenTelemetry SDK](https://github.com/open-telemetry/opentelemetry-java/tree/main/sdk/all), +- The [OpenTelemetry SDK](https://github.com/open-telemetry/opentelemetry-java/tree/main/sdk/all), along with various exporters and SDK extensions. -* [ByteBuddy](https://bytebuddy.net). +- [ByteBuddy](https://bytebuddy.net). Inside the javaagent jar, all classes and resources that are meant to be loaded by the `AgentClassLoader` are placed inside the `inst/` directory. All Java class files have diff --git a/docs/contributing/javaagent-test-infra.md b/docs/contributing/javaagent-test-infra.md index 7ece25ae69bf..6249ec53eb45 100644 --- a/docs/contributing/javaagent-test-infra.md +++ b/docs/contributing/javaagent-test-infra.md @@ -7,10 +7,10 @@ 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] +- 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. diff --git a/docs/contributing/muzzle.md b/docs/contributing/muzzle.md index da880970359a..46768dc487fd 100644 --- a/docs/contributing/muzzle.md +++ b/docs/contributing/muzzle.md @@ -9,12 +9,16 @@ symbols on the application classpath. Muzzle will prevent loading an instrumentation if it detects any mismatch or conflict. +Muzzle's dependency graph and class injection are encountered especially during the writing of +[`instrumentation modules`](writing-instrumentation-module.md). This functionality is required if +the packaged instrumentation utilizes `VirtualField`. + ## 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. +- 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 @@ -73,19 +77,19 @@ it's not an optional feature. The gradle plugin defines two tasks: -* `muzzle` task runs the runtime muzzle verification against different library versions: +- `muzzle` task runs the runtime muzzle verification against different library versions: - ```sh - ./gradlew :instrumentation:google-http-client-1.19:javaagent:muzzle - ``` + ```sh + ./gradlew :instrumentation:google-http-client-1.19:javaagent:muzzle + ``` - If a new, incompatible version of the instrumented library is published it fails the build. + 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: +- `printMuzzleReferences` task prints all API references in a given module: - ```sh - ./gradlew :instrumentation:google-http-client-1.19:javaagent:printMuzzleReferences - ``` + ```sh + ./gradlew :instrumentation:google-http-client-1.19:javaagent:printMuzzleReferences + ``` The muzzle plugin needs to be configured in the module's `.gradle` file. Example: @@ -117,14 +121,14 @@ muzzle { } ``` -* Using either `pass` or `fail` directive allows to specify whether muzzle should treat the +- 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 +- `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 +- `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; +- `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. @@ -140,7 +144,7 @@ instrumented application. The easiest way it can be done is by adding `assertInverse.set(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.set(true)` by default when writing instrumentation modules, even for +You SHOULD use `assertInverse.set(true)` 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 diff --git a/docs/contributing/running-tests.md b/docs/contributing/running-tests.md index 4ce4ac44f8a1..836a0f6d900b 100644 --- a/docs/contributing/running-tests.md +++ b/docs/contributing/running-tests.md @@ -38,6 +38,18 @@ To run these tests locally, add `-PtestLatestDeps=true` to your existing `gradle Executing `./gradlew :instrumentation::test --tests ` will run only the selected test. +### How to prevent linting and formatting warnings from failing tests + +During local development, you may want to ignore lint warnings when running tests. + +To ignore warnings, formatting issues, or other non-fatal issues in tests, use + +``` +./gradlew test -Ddev=true -x spotlessCheck -x checkstyleMain +``` + +The `dev` flag will ignore warnings in tests. + ## Smoke tests The smoke tests are not run as part of a global `test` task run since they take a long time and are @@ -55,17 +67,55 @@ If you are on Windows and you want to run the tests using linux containers: USE_LINUX_CONTAINERS=1 ./gradlew :smoke-tests:test -PsmokeTestSuite=payara ``` +If you want to run a specific smoke test: + +``` +./gradlew :smoke-tests:test --tests '*SpringBootSmokeTest*' +``` + +## Smoke OpenTelemetry starter tests + +Smoke tests for the [OpenTelemetry Spring starter](../../instrumentation/spring/starters/spring-boot-starter/README.md). + +You can execute the tests in a JVM (`./gradlew smoke-tests-otel-starter:test`) or as Spring native tests (`./gradlew smoke-tests-otel-starter:nativeTest`). + ## GraalVM native test -Some tests can be executed as GraalVM native executables: +To execute all the instrumentation tests runnable as GraalVM native executables: ``` ./gradlew nativeTest ``` +[A Github workflow](../../.github/workflows/native-tests-daily.yml) executes the native tests every day. ## Docker disk space Some of the instrumentation tests (and all of the smoke tests) spin up docker containers via [testcontainers](https://www.testcontainers.org/). If you run out of space, you may wish to prune old containers, images and volumes using `docker system prune --volumes`. + +## Configuring Docker alternative + +For some container environments, such as rootless Podman or a remotely hosted Docker, +testcontainers may need additional configuration to successfully run the tests. +The following environment variables can be used for configuration: + - `TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE` - The location of the Docker socket on the host. Default is `/var/run/docker.sock` + - `TESTCONTAINERS_HOST_OVERRIDE` - The hostname used for container-to-container communication. Default Docker is `localhost`, but rootless Podman uses `host.containers.internal` + +# Troubleshooting CI Test Failures + +CI test logs are pretty big around 75MB. To make it easier to troubleshoot test failures, you can download the raw logs from the CI job and then look for +`Publishing build scan...` in the logs. Copy the URL from the logs and open it in a browser. This will give you a nice view of the test execution breakdown. + +## How to download the raw logs + +1. Click on the `Details` link in one of the failing CI jobs under `Some checks were not successful` section of your PR. +2. Click on one of the failed tests in the left panel. +3. Click on the `Setting` gear in the top right corner of the logs panel. +4. Right click on 'View raw logs' and then 'Save link as' to save the page as a text file locally. +5. Once the file is downloaded, open it in a text editor and search for `Publishing build scan...` to find the URL. +6. Open the URL in a browser to view the test execution breakdown. It might prompt you to "Activate your Build Scan" the very 1st time, you can use your own email address and activate it via email. + +Unfortunately, the Build Scan service hosted via Develocity has an allowed size limits of the free build scans. Once you exceed the limit, then you won't be able to view the scan anymore. +Then you can just use the raw logs to search for "FAILED" or "Task failed with an exception" to identify the failing tests. diff --git a/docs/contributing/selectModules.kts b/docs/contributing/selectModules.kts new file mode 100755 index 000000000000..d0f299d42125 --- /dev/null +++ b/docs/contributing/selectModules.kts @@ -0,0 +1,83 @@ +#!/usr/bin/env kotlin + +//install kotlin compiler: https://kotlinlang.org/docs/tutorials/command-line.html +import java.io.File + +val includeRegex = Regex("include\\(\"(.*?)\"\\)") +val projectRegex = "project\\(\"([^\"]+)\"(, configuration = \".*\")?\\)".toRegex() +val keepModules = mutableSetOf() +var root = "" + +main(args) + +fun main(args: Array) { + if (args.isEmpty()) { + println("Usage: ./docs/contributing/selectModules.kts instrumentation/spring/spring-boot-autoconfigure/ ...") + return + } + + (args.map { + moduleOfArg( + File(File(it).absolutePath), + "/" + it.trimStart('.', '/').trimEnd('/') + ) + } + listOf(":javaagent")) + .map { Module(it) } + .forEach(Module::addSelfAndChildren) + + File("$root/conventions/src/main/kotlin").listFiles()!! + .filter { it.name.endsWith(".kts") } + .forEach { + children(it).forEach(Module::addSelfAndChildren) + } + + println("removing modules except:\n${keepModules.map { it.name }.sorted().joinToString("\n")}") + + val target = File("$root/settings.gradle.kts") + val text = target.readText().lines().flatMap { line -> + includeRegex.matchEntire(line)?.let { it.groupValues[1] }?.let { module -> + if (Module(module) in keepModules) { + listOf(line) + } else { + emptyList() + } + } ?: listOf(line) + + + }.joinToString("\n") + target.writeText(text) +} + +data class Module(val name: String) { + fun children(): List { + val file = moduleFile() + return children(file) + } + + private fun moduleFile(): File = File("$root/${name.replace(":", "/")}/build.gradle.kts") + + fun addSelfAndChildren() { + if (!keepModules.add(this)) { + return + } + + children().forEach(Module::addSelfAndChildren) + } +} + +fun moduleOfArg(file: File, name: String): String { + val settings = File(file, "settings.gradle.kts") + return if (settings.exists()) { + root = file.absolutePath + name.substringAfter(root).replace("/", ":") + } else { + moduleOfArg(file.parentFile, name) + } +} + +fun children(file: File) = file.readText().lines().flatMap { line -> + projectRegex.find(line)?.let { it.groupValues[1] }?.let { module -> + listOf(Module(module)) + } ?: emptyList() +} + diff --git a/docs/contributing/style-guideline.md b/docs/contributing/style-guideline.md index 6904e5e0d533..42930cc21d25 100644 --- a/docs/contributing/style-guideline.md +++ b/docs/contributing/style-guideline.md @@ -59,23 +59,28 @@ We leverage static imports for many common types of operations. However, not all 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 +- 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 and hopefully immutable) +- `tracer()` methods that expose tracer singleton instances + +Some of these are enforced by checkstyle rules: + +- look for `RegexpSinglelineJava` in `checkstyle.xml` +- use `@SuppressWarnings("checkstyle:RegexpSinglelineJava")` to suppress the checkstyle warning ## 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 +- 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 @@ -137,7 +142,14 @@ It is ok to use `Optional` in places where it does not leak into public API sign Also, avoid `Optional` usage on the hot path (instrumentation code), unless the instrumented library itself uses it. -## `java.util.stream.Stream` usage - -Avoid streams on the hot path (instrumentation code), unless the instrumented library itself uses -them. +## Hot path constraints + +Avoid allocations whenever possible on the hot path (instrumentation code). +This includes `Iterator` allocations from collections; note that +`for(SomeType t : plainJavaArray)` does not allocate an iterator object. +Non-allocating stream api usage on the hot path is acceptable but may not +fit the surrounding code style; this is a judgement call. Note that +some stream apis make it much more difficult to allocate efficiently +(e.g., `collect` with presized sink data structures involves +convoluted `Supplier` code, or lambdas passed to `forEach` might be +capturing/allocating lambdas). diff --git a/docs/contributing/using-instrumenter-api.md b/docs/contributing/using-instrumenter-api.md index b4ebce0044ee..fbe99935e2f7 100644 --- a/docs/contributing/using-instrumenter-api.md +++ b/docs/contributing/using-instrumenter-api.md @@ -68,11 +68,11 @@ span and finishes recording the metrics (if any are registered in the `Instrumen The `end()` method accepts several arguments: -* The OpenTelemetry `Context` that was returned by the `start()` method. -* The `REQUEST` instance that started the processing. -* Optionally, the `RESPONSE` instance that ends the processing - it may be `null` in case it was not +- The OpenTelemetry `Context` that was returned by the `start()` method. +- The `REQUEST` instance that started the processing. +- Optionally, the `RESPONSE` instance that ends the processing - it may be `null` in case it was not received or an error has occurred. -* Optionally, a `Throwable` error that was thrown by the operation. +- Optionally, a `Throwable` error that was thrown by the operation. Consider the following example: @@ -105,13 +105,13 @@ An `Instrumenter` can be obtained by calling its static `builder()` method and u returned `InstrumenterBuilder` to configure captured telemetry and apply customizations. The `builder()` method accepts three arguments: -* An `OpenTelemetry` instance, which is used to obtain the `Tracer` and `Meter` objects. -* The instrumentation name, which indicates the _instrumentation_ library name, not the +- An `OpenTelemetry` instance, which is used to obtain the `Tracer` and `Meter` objects. +- The instrumentation name, which indicates the _instrumentation_ library name, not the _instrumented_ library name. The value passed here should uniquely identify the instrumentation library so that during troubleshooting it's possible to determine where the telemetry came from. Read more about instrumentation libraries in the [OpenTelemetry specification](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/overview.md#instrumentation-libraries). -* A `SpanNameExtractor` that determines the span name. +- A `SpanNameExtractor` that determines the span name. An `Instrumenter` can be built from several smaller components. The following subsections describe all interfaces that can be used to customize an `Instrumenter`. @@ -122,17 +122,17 @@ By setting the instrumentation library version, you let users identify which ver instrumentation produced the telemetry. Make sure you always provide the version to the `Instrumenter`. You can do this in two ways: -* By calling the `setInstrumentationVersion()` method on the `InstrumenterBuilder`. -* By making sure that the JAR file with your instrumentation library contains a properties file in +- By calling the `setInstrumentationVersion()` method on the `InstrumenterBuilder`. +- By making sure that the JAR file with your instrumentation library contains a properties file in the `META-INF/io/opentelemetry/instrumentation/` directory. You must name the file `${instrumentationName}.properties`, where `${instrumentationName}` is the name of the instrumentation library passed to the `Instrumenter#builder()` method. The file must contain a single property, `version`. For example: - ```properties - # META-INF/io/opentelemetry/instrumentation/my-instrumentation.properties - version=1.2.3 - ``` + ```properties + # META-INF/io/opentelemetry/instrumentation/my-instrumentation.properties + version=1.2.3 + ``` The `Instrumenter` automatically detects the properties file and determines the instrumentation version based on its name. @@ -147,7 +147,7 @@ A `SpanNameExtractor` is a simple functional interface that accepts the `REQUEST the span name. For more detailed guidelines on span naming please take a look at the [`Span` specification](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/api.md#span) and the -tracing [semantic conventions](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/README.md). +tracing [semantic conventions](https://github.com/open-telemetry/semantic-conventions/blob/main/docs/general/trace.md). Consider the following example: @@ -171,9 +171,9 @@ method. An `AttributesExtractor` is responsible for extracting span and metric attributes when the processing starts and ends. It contains two methods: -* The `onStart()` method is called when the instrumented operation starts. It accepts two +- The `onStart()` method is called when the instrumented operation starts. It accepts two parameters: an `AttributesBuilder` instance and the incoming `REQUEST` instance. -* The `onEnd()` method is called when the instrumented operation ends. It accepts the same two +- The `onEnd()` method is called when the instrumented operation ends. It accepts the same two parameters as `onStart()` and also an optional `RESPONSE` and an optional `Throwable` error. The aim of both methods is to extract interesting attributes from the received request (and response @@ -253,9 +253,9 @@ the `setSpanStatusExtractor()` method. The `SpanLinksExtractor` interface can be used to add links to other spans when the instrumented operation starts. It has a single `extract()` method that receives the following arguments: -* A `SpanLinkBuilder` that can be used to add the links. -* The parent `Context` that was passed in to `Instrumenter#start()`. -* The `REQUEST` instance that was passed in to `Instrumenter#start()`. +- A `SpanLinkBuilder` that can be used to add the links. +- The parent `Context` that was passed in to `Instrumenter#start()`. +- The `REQUEST` instance that was passed in to `Instrumenter#start()`. You can read more about span links and their use cases [here](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/overview.md#links-between-spans). @@ -326,10 +326,10 @@ and `OperationListener` interfaces. `OperationMetrics` is simply a factory inter the `OperationListener` - it receives an OpenTelemetry `Meter` and returns a new listener. The `OperationListener` contains two methods: -* `onStart()` that gets executed when the instrumented operation starts. It returns a `Context` - it +- `onStart()` that gets executed when the instrumented operation starts. It returns a `Context` - it can be used to store internal metrics state that should be propagated to the `onEnd()` call, if needed. -* `onEnd()` that gets executed when the instrumented operation ends. +- `onEnd()` that gets executed when the instrumented operation ends. Both methods accept a `Context`, an instance of `Attributes` that contains either attributes computed on instrumented operation start or end, and the start and end nanoseconds timestamp that @@ -418,16 +418,16 @@ method for that: passing `false` will turn the newly created `Instrumenter` into The `Instrumenter` creation process ends with calling one of the following `InstrumenterBuilder` methods: -* `newInstrumenter()`: the returned `Instrumenter` will always start spans with kind `INTERNAL`. -* `newInstrumenter(SpanKindExtractor)`: the returned `Instrumenter` will always start spans with +- `buildInstrumenter()`: the returned `Instrumenter` will always start spans with kind `INTERNAL`. +- `buildInstrumenter(SpanKindExtractor)`: the returned `Instrumenter` will always start spans with kind determined by the passed `SpanKindExtractor`. -* `newClientInstrumenter(TextMapSetter)`: the returned `Instrumenter` will always start `CLIENT` +- `buildClientInstrumenter(TextMapSetter)`: the returned `Instrumenter` will always start `CLIENT` spans and will propagate operation context into the outgoing request. -* `newServerInstrumenter(TextMapGetter)`: the returned `Instrumenter` will always start `SERVER` +- `buildServerInstrumenter(TextMapGetter)`: the returned `Instrumenter` will always start `SERVER` spans and will extract the parent span context from the incoming request. -* `newProducerInstrumenter(TextMapSetter)`: the returned `Instrumenter` will always start `PRODUCER` +- `buildProducerInstrumenter(TextMapSetter)`: the returned `Instrumenter` will always start `PRODUCER` spans and will propagate operation context into the outgoing request. -* `newConsumerInstrumenter(TextMapGetter)`: the returned `Instrumenter` will always start `SERVER` +- `buildConsumerInstrumenter(TextMapGetter)`: the returned `Instrumenter` will always start `CONSUMER` spans and will extract the parent span context from the incoming request. The last four variants that create non-`INTERNAL` spans accept either `TextMapSetter` @@ -453,6 +453,6 @@ class MySpanKindExtractor implements SpanKindExtractor { The example `SpanKindExtractor` above decides whether to use `PRODUCER` or `CLIENT` based on how the request is going to be processed. This example reflects a real-life scenario: you might find similar code in a messaging library instrumentation, since according to -the [OpenTelemetry messaging semantic conventions](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/messaging.md#span-kind) +the [OpenTelemetry messaging semantic conventions](https://github.com/open-telemetry/semantic-conventions/blob/main/docs/messaging/messaging-spans.md#span-kind) the span kind should be set to `CLIENT` if sending the message is completely synchronous and waits for the response. diff --git a/docs/contributing/writing-instrumentation-module.md b/docs/contributing/writing-instrumentation-module.md index c8c04b69b61a..550fd6d69de6 100644 --- a/docs/contributing/writing-instrumentation-module.md +++ b/docs/contributing/writing-instrumentation-module.md @@ -190,11 +190,11 @@ This method describes what transformations should be applied to the matched type. The interface `TypeTransformer`, implemented internally by the agent, defines a set of available transformations that you can apply: -* `applyAdviceToMethod(ElementMatcher, String)` lets you apply +- `applyAdviceToMethod(ElementMatcher, String)` lets you apply an advice class (the second parameter) to all matching methods (the first parameter). We suggest to make the method matchers as strict as possible: the type instrumentation should only instrument the code that it targets. -* `applyTransformer(AgentBuilder.Transformer)` lets you to inject an arbitrary ByteBuddy +- `applyTransformer(AgentBuilder.Transformer)` lets you to inject an arbitrary ByteBuddy transformer. This is an advanced, low-level option that is not subjected to muzzle safety checks and helper class detection. Use it responsibly. @@ -238,15 +238,15 @@ the instrumented library class files. You should not treat them as ordinary, pla Unfortunately many standard practices do not apply to advice classes: -* 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 +- 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, constants are not. -* Inner advice classes defined in an `InstrumentationModule` or a `TypeInstrumentation` MUST NOT use +- 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 won't work: create additional helper +- Reusing code by extracting a common method and/or parent class won't work: create additional helper classes to store any reusable code instead. -* They SHOULD NOT contain any methods other than `@Advice`-annotated method. +- They SHOULD NOT contain any methods other than `@Advice`-annotated method. Consider the following example: @@ -341,23 +341,175 @@ limited: the `VirtualField#get()` method must receive class references as its pa work with variables, method params, etc. Both the owner class and the field class must be known at compile time for it to work. -### Why we don't use ByteBuddy @Advice.Origin Method +Use of `VirtualField` requires the `muzzle-generation` gradle plugin. Failing to use the plugin will result in +ClassNotFoundException when trying to access the field. -Instead of +### Avoid using @Advice.Origin Method -``` -@Advice.Origin Method method -``` +You shouldn't use ByteBuddy's @Advice.Origin Method method, as it +inserts a call to `Class.getMethod(...)` in a transformed method. -we prefer to use +Instead, get the declaring class and method name, as loading +constants from a constant pool is a much simpler operation. + +For example: ``` @Advice.Origin("#t") Class declaringClass, @Advice.Origin("#m") String methodName ``` -because the former inserts a call to `Class.getMethod(...)` in transformed method. In contrast, -getting the declaring class and method name is just loading constants from constant pool, which is -a much simpler operation. - [suppress]: https://opentelemetry.io/docs/instrumentation/java/automatic/agent-config/#suppressing-specific-auto-instrumentation + +## Use non-inlined advice code with `invokedynamic` + +Using non-inlined advice code is possible thanks to the `invokedynamic` instruction, this strategy +is referred as "indy" in reference to this. By extension "indy modules" are the instrumentation +modules using this instrumentation strategy. + +The most common way to instrument code with ByteBuddy relies on inlining, this strategy will be +referred as "inlined" strategy as opposed to "indy". + +For inlined advices, the advice code is directly copied into the instrumented method. +In addition, all helper classes are injected into the classloader of the instrumented classes. + +For indy, advice classes are not inlined. Instead, they are loaded alongside all helper classes +into a special `InstrumentationModuleClassloader`, which sees the classes from both the instrumented +application classloader and the agent classloader. +The instrumented classes call the advice classes residing in the `InstrumentationModuleClassloader` via +invokedynamic bytecode instructions. + +Using indy instrumentation has these advantages: + +- allows instrumentations to have breakpoints set in them and be debugged using standard debugging techniques +- provides clean isolation of instrumentation advice from the application and other instrumentations +- allows advice classes to contain static fields and methods which can be accessed from the advice entry points - in fact generally good development practices are enabled (whereas inlined advices are [restricted in how they can be implemented](#use-advice-classes-to-write-code-that-will-get-injected-to-the-instrumented-library-classes)) + +### Indy modules and transition + +Making an instrumentation "indy" compatible (or native "indy") is not as straightforward as making it "inlined". +However, ByteBuddy provides a set of tools and APIs that are mentioned below to make the process as smooth as possible. + +Due to the changes needed on most of the instrumentation modules the migration can't be achieved in a single step, +we thus have to implement it in two steps: + +- `InstrumentationModule#isIndyModule` implementation return `true` (and changes needed to make it indy compatible) +- set `inlined = false` on advice methods annotated with `@Advice.OnMethodEnter` or `@Advice.OnMethodExit` + +The `otel.javaagent.experimental.indy` (default `false`) configuration option allows to opt-in for +using "indy". When set to `true`, the `io.opentelemetry.javaagent.tooling.instrumentation.indy.AdviceTransformer` +will transform advices automatically to make them "indy native". Using this option is temporary and will +be removed once all the instrumentations are "indy native". + +This configuration is automatically enabled in CI with `testIndy*` checks or when the `-PtestIndy=true` parameter is added to gradle. + +In order to preserve compatibility with both instrumentation strategies, we have to omit the `inlined = false` +from the advice method annotations. + +We have three sets of instrumentation modules: +- "inlined only": only compatible with "inlined", `isIndyModule` returns `false`. +- "indy compatible": compatible with both "indy" and "inlined", do not override `isIndyModule`, advices are modified with `AdviceTransformer` to be made "indy native" or "inlined" at runtime. +- "indy native": only compatible with "indy" `isIndyModule` returns `true`. + +The first step of the migration is to move all the "inlined only" to the "indy compatible" category +by refactoring them with the limitations described below. + +Once everything is "indy compatible", we can evaluate changing the default value of `otel.javaagent.experimental.indy` +to `true` and make it non-experimental. + +### Shared classes and common classloader + +By default, all the advices of an instrumentation module will be loaded into isolated classloaders, +one per instrumentation module. Some instrumentations require to use a common classloader in order +to preserve the semantics of `static` fields and to share classes. + +In order to load multiple `InstrumentationModule` implementations in the same classloader, you need to +override the `ExperimentalInstrumentationModule#getModuleGroup` to return an identical value. + +### Classes injected in application classloader + +Injecting classes in the application classloader is possible by implementing the +`ExperimentalInstrumentationModule#injectedClassNames` method. All the class names listed by the +returned value will be loaded in the application classloader instead of the agent or instrumentation +module classloader. + +This allows for example to access package-private methods that would not be accessible otherwise. + +### Advice local variables + +With inlined advices, declaring an advice method argument with `@Advice.Local` allows defining +a variable that is local to the advice execution for communication between the enter and exit advices. + +When advices are not inlined, usage of `@Advice.Local` is not possible. It is however possible to +return a value from the enter advice and get the value in the exit advice with a parameter annotated +with `@Advice.Enter`, for example: + +```java +@Advice.OnMethodEnter(suppress = Throwable.class, inlined = false) +public static Object onEnter(@Advice.Argument(1) Object request) { + return "enterValue"; +} + +@Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class, inlined = false) +public static void onExit(@Advice.Argument(1) Object request, + @Advice.Enter Object enterValue) { + // do something with enterValue +} +``` + +### Modifying method arguments + +With inlined advices, using the `@Advice.Argument` annotation on method parameter with `readOnly = false` +allows modifying instrumented method arguments. + +When using non-inlined advices, reading the argument values is still done with `@Advice.Argument` +annotated parameters, however modifying the values is done through the advice method return value +and `@Advice.AssignReturned.ToArguments` annotation: + +```java +@Advice.OnMethodEnter(suppress = Throwable.class, inlined = false) +@Advice.AssignReturned.ToArguments(@ToArgument(1)) +public static Object onEnter(@Advice.Argument(1) Object request) { + return "hello"; +} +``` + +It is possible to modify multiple arguments at once by using an array, see usages of +`@Advice.AssignReturned.ToArguments` for detailed examples. + +### Modifying method return value + +With inlined advices, using the `@Advice.Return` annotation on method parameter with `readOnly = false` +allows modifying instrumented method return value on exit advice. + +When using non-inlined advices, reading the original return value is still done with the `@Advice.Return` +annotated parameter, however modifying the value is done through the advice method return value +and `@Advice.AssignReturned.ToReturned`. + +```java +@Advice.OnMethodExit(suppress = Throwable.class, inlined = false) +@Advice.AssignReturned.ToReturned +public static Object onExit(@Advice.Return Object returnValue) { + return "hello"; +} +``` + +### Writing to internal class fields + +With inlined advices, using the `@Advice.FieldValue(value = "fieldName", readOnly = false)` annotation +on advice method parameters allows modifying the `fieldName` field of the instrumented class. + +When using non-inlined advices, reading the original field value is still done with the `@Advice.FieldValue` +annotated parameter, however modifying the value is done through the advice method return value +and `@Advice.AssignReturned.ToFields` annotation. + +```java +@Advice.OnMethodEnter(suppress = Throwable.class, inlined = false) +@Advice.AssignReturned.ToFields(@ToField("fieldName")) +public static Object onEnter(@Advice.FieldValue("fieldName") Object originalFieldValue) { + return "newFieldValue"; +} +``` + +It is possible to modify multiple fields at once by using an array, see usages of +`@Advice.AssignReturned.ToFields` for detailed examples. diff --git a/docs/contributing/writing-instrumentation.md b/docs/contributing/writing-instrumentation.md index d320a7aa8b3a..f921a57015b7 100644 --- a/docs/contributing/writing-instrumentation.md +++ b/docs/contributing/writing-instrumentation.md @@ -45,7 +45,7 @@ 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 you are targeting the RPC framework `yarpc` at version `1.0`, you would have a +For example, if you are targeting the RPC framework `yarpc` at minimal supported version `1.0`, you would have a directory tree like the following: ``` @@ -60,7 +60,7 @@ instrumentation -> build.gradle.kts ``` -The top level `settings.gradle.kts` file would contain the following: +The top level `settings.gradle.kts` file would contain the following (please add in alphabetical order): ```kotlin include("instrumentation:yarpc-1.0:javaagent") @@ -360,8 +360,8 @@ instrumentation modules. Some examples of this include: -* Application server instrumentations communicating with Servlet API instrumentations. -* Different high-level Kafka consumer instrumentations suppressing the low-level `kafka-clients` +- Application server instrumentations communicating with Servlet API instrumentations. +- Different high-level Kafka consumer instrumentations suppressing the low-level `kafka-clients` instrumentation. Create a module named `bootstrap` and add a `build.gradle.kts` file with the following content: diff --git a/docs/logger-mdc-instrumentation.md b/docs/logger-mdc-instrumentation.md index 34255a3f4317..49977f948574 100644 --- a/docs/logger-mdc-instrumentation.md +++ b/docs/logger-mdc-instrumentation.md @@ -19,21 +19,27 @@ event's MDC copy: (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. +by specifying them in the pattern/format. This way any services or tools that parse the application +logs can correlate traces/spans with log statements. -Tip: for Spring Boot configuration which uses logback, you can add MDC to log lines by overriding only the `logging.pattern.level`: +> Note: If the current `Span` is invalid, the OpenTelemetry appender will not inject any trace information. -```properties -logging.pattern.level = trace_id=%mdc{trace_id} span_id=%mdc{span_id} trace_flags=%mdc{trace_flags} %5p -``` +## Supported logging libraries -This way any services or tools that parse the application logs can correlate traces/spans with log -statements. +> Note: There are also log appenders for exporting logs to OpenTelemetry, not to be confused with the MDC appenders. -## Supported logging libraries +| Library | Auto-instrumented versions | Standalone Library Instrumentation | +| ------- | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | +| Log4j 1 | 1.2+ | | +| Log4j 2 | 2.7+ | [opentelemetry-log4j-context-data-2.17-autoconfigure](../instrumentation/log4j/log4j-context-data/log4j-context-data-2.17/library-autoconfigure) | +| Logback | 1.0+ | [opentelemetry-logback-mdc-1.0](../instrumentation/logback/logback-mdc-1.0/library) | + +## Frameworks -| Library | Version | -|---------|---------| -| Log4j 1 | 1.2+ | -| Log4j 2 | 2.7+ | -| Logback | 1.0+ | +### Spring Boot + +For Spring Boot configuration which uses logback, you can add MDC to log lines by overriding only the `logging.pattern.level`: + +```properties +logging.pattern.level = trace_id=%mdc{trace_id} span_id=%mdc{span_id} trace_flags=%mdc{trace_flags} %5p +``` diff --git a/docs/misc/interop-design/interop-design.md b/docs/misc/interop-design/interop-design.md index 11cfccfec34e..4bf472210f86 100644 --- a/docs/misc/interop-design/interop-design.md +++ b/docs/misc/interop-design/interop-design.md @@ -4,8 +4,8 @@ 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 +- Instrumentation provided by the Java agent +- Instrumentation provided by the user app, using any 1.0+ version of the OpenTelemetry API ## Design diff --git a/docs/scope.md b/docs/scope.md index be1ed2c17d28..7eb742037e6a 100644 --- a/docs/scope.md +++ b/docs/scope.md @@ -2,9 +2,9 @@ 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 +- 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/docs/supported-libraries.md b/docs/supported-libraries.md index 8a1bd07ba5f9..68065b23894f 100644 --- a/docs/supported-libraries.md +++ b/docs/supported-libraries.md @@ -3,137 +3,155 @@ 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), +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) +- [Libraries / Frameworks](#libraries--frameworks) +- [Application Servers](#application-servers) +- [JVMs and Operating Systems](#jvms-and-operating-systems) +- [Disabled instrumentations](#disabled-instrumentations) ## Libraries / Frameworks These are the supported libraries and frameworks: -| Library/Framework | Auto-instrumented versions | Standalone Library Instrumentation [1] | Semantic Conventions | -|---------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------| -| [Akka Actors](https://doc.akka.io/docs/akka/current/typed/index.html) | 2.5+ | N/A | Context propagation | -| [Akka HTTP](https://doc.akka.io/docs/akka-http/current/index.html) | 10.0+ | N/A | [HTTP Client Spans], [HTTP Client Metrics], [HTTP Server Spans], [HTTP Server Metrics] | -| [Apache Axis2](https://axis.apache.org/axis2/java/core/) | 1.6+ | N/A | Provides `http.route` [2], Controller Spans [3] | -| [Apache Camel](https://camel.apache.org/) | 2.20+ (not including 3.x yet) | N/A | Dependent on components in use | -| [Apache DBCP](https://commons.apache.org/proper/commons-dbcp/) | 2.0+ | [opentelemetry-apache-dbcp-2.0](../instrumentation/apache-dbcp-2.0/library) | [Database Pool Metrics] | -| [Apache CXF JAX-RS](https://cxf.apache.org/) | 3.2+ | N/A | Provides `http.route` [2], Controller Spans [3] | -| [Apache CXF JAX-WS](https://cxf.apache.org/) | 3.0+ | N/A | Provides `http.route` [2], Controller Spans [3] | -| [Apache Dubbo](https://github.com/apache/dubbo/) | 2.7+ | [opentelemetry-apache-dubbo-2.7](../instrumentation/apache-dubbo-2.7/library-autoconfigure) | [RPC Client Spans], [RPC Server Spans] | -| [Apache HttpAsyncClient](https://hc.apache.org/index.html) | 4.1+ | N/A | [HTTP Client Spans], [HTTP Client Metrics] | -| [Apache HttpClient](https://hc.apache.org/index.html) | 2.0+ | [opentelemetry-apache-httpclient-4.3](../instrumentation/apache-httpclient/apache-httpclient-4.3/library) | [HTTP Client Spans], [HTTP Client Metrics] | -| [Apache Kafka Producer/Consumer API](https://kafka.apache.org/documentation/#producerapi) | 0.11+ | [opentelemetry-kafka-clients-2.6](../instrumentation/kafka/kafka-clients/kafka-clients-2.6/library) | [Messaging Spans] | -| [Apache Kafka Streams API](https://kafka.apache.org/documentation/streams/) | 0.11+ | N/A | [Messaging Spans] | -| [Apache MyFaces](https://myfaces.apache.org/) | 1.2+ (not including 3.x yet) | N/A | Provides `http.route` [2], Controller Spans [3] | -| [Apache Pulsar](https://pulsar.apache.org/) | 2.8+ | N/A | [Messaging Spans] | -| [Apache RocketMQ gRPC/Protobuf-based Client](https://rocketmq.apache.org/) | 5.0.0+ | N/A | [Messaging Spans] | -| [Apache RocketMQ Remoting-based Client](https://rocketmq.apache.org/) | 4.8+ | [opentelemetry-rocketmq-client-4.8](../instrumentation/rocketmq/rocketmq-client/rocketmq-client-4.8/library) | [Messaging Spans] | -| [Apache Struts 2](https://github.com/apache/struts) | 2.3+ | N/A | Provides `http.route` [2], Controller Spans [3] | -| [Apache Tapestry](https://tapestry.apache.org/) | 5.4+ | N/A | Provides `http.route` [2], Controller Spans [3] | -| [Apache Wicket](https://wicket.apache.org/) | 8.0+ | N/A | Provides `http.route` [2] | -| [Armeria](https://armeria.dev) | 1.3+ | [opentelemetry-armeria-1.3](../instrumentation/armeria-1.3/library) | [HTTP Client Spans], [HTTP Client Metrics], [HTTP Server Spans], [HTTP Server Metrics] | -| [AsyncHttpClient](https://github.com/AsyncHttpClient/async-http-client) | 1.9+ | N/A | [HTTP Client Spans], [HTTP Client Metrics] | -| [AWS Lambda](https://docs.aws.amazon.com/lambda/latest/dg/java-handler.html) | 1.0+ | [opentelemetry-aws-lambda-core-1.0](../instrumentation/aws-lambda/aws-lambda-core-1.0/library),
[opentelemetry-aws-lambda-events-2.2](../instrumentation/aws-lambda/aws-lambda-events-2.2/library) | [FaaS Server Spans] | -| [AWS SDK](https://aws.amazon.com/sdk-for-java/) | 1.11.x and 2.2.0+ | [opentelemetry-aws-sdk-1.11](../instrumentation/aws-sdk/aws-sdk-1.11/library),
[opentelemetry-aws-sdk-1.11-autoconfigure](../instrumentation/aws-sdk/aws-sdk-1.11/library-autoconfigure),
[opentelemetry-aws-sdk-2.2](../instrumentation/aws-sdk/aws-sdk-2.2/library),
[opentelemetry-aws-sdk-2.2-autoconfigure](../instrumentation/aws-sdk/aws-sdk-2.2/library-autoconfigure) | [Messaging Spans], [Database Client Spans], [HTTP Client Spans] | -| [Azure Core](https://docs.microsoft.com/en-us/java/api/overview/azure/core-readme) | 1.14+ | N/A | Context propagation | -| [Cassandra Driver](https://github.com/datastax/java-driver) | 3.0+ | [opentelemetry-cassandra-4.4](../instrumentation/cassandra/cassandra-4.4/library) | [Database Client Spans] | -| [Couchbase Client](https://github.com/couchbase/couchbase-java-client) | 2.0+ and 3.1+ | N/A | [Database Client Spans] | -| [c3p0](https://github.com/swaldman/c3p0) | 0.9.2+ | [opentelemetry-c3p0-0.9](../instrumentation/c3p0-0.9/library) | [Database Pool Metrics] | -| [Dropwizard Metrics](https://metrics.dropwizard.io/) | 4.0+ (disabled by default) | N/A | none | -| [Dropwizard Views](https://www.dropwizard.io/en/latest/manual/views.html) | 0.7+ | N/A | Controller Spans [3] | -| [Eclipse Grizzly](https://javaee.github.io/grizzly/httpserverframework.html) | 2.3+ | N/A | [HTTP Server Spans], [HTTP Server Metrics] | -| [Eclipse Jersey](https://eclipse-ee4j.github.io/jersey/) | 2.0+ (not including 3.x yet) | N/A | Provides `http.route` [2], Controller Spans [3] | -| [Eclipse Jetty HTTP Client](https://www.eclipse.org/jetty/javadoc/jetty-9/org/eclipse/jetty/client/HttpClient.html) | 9.2+ (not including 10+ yet) | [opentelemetry-jetty-httpclient-9.2](../instrumentation/jetty-httpclient/jetty-httpclient-9.2/library) | [HTTP Client Spans], [HTTP Client Metrics] | -| [Eclipse Metro](https://projects.eclipse.org/projects/ee4j.metro) | 2.2+ (not including 3.x yet) | N/A | Provides `http.route` [2], Controller Spans [3] | -| [Eclipse Mojarra](https://projects.eclipse.org/projects/ee4j.mojarra) | 1.2+ (not including 3.x yet) | N/A | Provides `http.route` [2], Controller Spans [3] | -| [Elasticsearch API](https://www.elastic.co/guide/en/elasticsearch/client/java-api/current/index.html) | 5.0+ | N/A | [Database Client Spans] | -| [Elasticsearch REST Client](https://www.elastic.co/guide/en/elasticsearch/client/java-rest/current/index.html) | 5.0+ | N/A | [Database Client Spans] | -| [Finatra](https://github.com/twitter/finatra) | 2.9+ | N/A | Provides `http.route` [2], Controller Spans [3] | -| [Geode Client](https://geode.apache.org/) | 1.4+ | N/A | [Database Client Spans] | -| [Google HTTP Client](https://github.com/googleapis/google-http-java-client) | 1.19+ | N/A | [HTTP Client Spans], [HTTP Client Metrics] | -| [Grails](https://grails.org/) | 3.0+ | N/A | Provides `http.route` [2], Controller Spans [3] | -| [GraphQL Java](https://www.graphql-java.com/) | 12.0+ | [opentelemetry-graphql-java-12.0](../instrumentation/graphql-java-12.0/library) | [GraphQL Server Spans] | -| [gRPC](https://github.com/grpc/grpc-java) | 1.6+ | [opentelemetry-grpc-1.6](../instrumentation/grpc-1.6/library) | [RPC Client Spans], [RPC Client Metrics], [RPC Server Spans], [RPC Server Metrics] | -| [Guava ListenableFuture](https://guava.dev/releases/snapshot/api/docs/com/google/common/util/concurrent/ListenableFuture.html) | 10.0+ | [opentelemetry-guava-10.0](../instrumentation/guava-10.0/library) | Context propagation | -| [GWT](http://www.gwtproject.org/) | 2.0+ | N/A | [RPC Server Spans] | -| [Hibernate](https://github.com/hibernate/hibernate-orm) | 3.3+ (not including 6.x yet) | N/A | none | -| [HikariCP](https://github.com/brettwooldridge/HikariCP) | 3.0+ | [opentelemetry-hikaricp-3.0](../instrumentation/hikaricp-3.0/library) | [Database Pool Metrics] | -| [HttpURLConnection](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/net/HttpURLConnection.html) | Java 8+ | N/A | [HTTP Client Spans], [HTTP Client Metrics] | -| [Hystrix](https://github.com/Netflix/Hystrix) | 1.4+ | N/A | none | -| [Java Executors](https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/Executor.html) | Java 8+ | N/A | Context propagation | -| [Java Http Client](https://docs.oracle.com/en/java/javase/11/docs/api/java.net.http/java/net/http/package-summary.html) | Java 11+ | [opentelemetry-java-http-client](../instrumentation/java-http-client/library) | [HTTP Client Spans], [HTTP Client Metrics] | -| [java.util.logging](https://docs.oracle.com/javase/8/docs/api/java/util/logging/package-summary.html) | Java 8+ | N/A | none | -| [Java Platform](https://docs.oracle.com/javase/8/docs/api/java/lang/management/ManagementFactory.html) | Java 8+ | [opentelemetry-runtime-telemetry-java8](../instrumentation/runtime-telemetry/runtime-telemetry-java8/library),[opentelemetry-runtime-telemetry-java17](../instrumentation/runtime-telemetry/runtime-telemetry-java17/library),
[opentelemetry-resources](../instrumentation/resources/library) | [JVM Runtime Metrics] | -| [JAX-RS](https://javaee.github.io/javaee-spec/javadocs/javax/ws/rs/package-summary.html) | 0.5+ | N/A | Provides `http.route` [2], Controller Spans [3] | -| [JAX-RS Client](https://javaee.github.io/javaee-spec/javadocs/javax/ws/rs/client/package-summary.html) | 1.1+ | N/A | [HTTP Client Spans], [HTTP Client Metrics] | -| [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) | N/A | Provides `http.route` [2], Controller Spans [3] | -| [JBoss Log Manager](https://github.com/jboss-logging/jboss-logmanager) | 1.1+ | N/A | none | -| [JDBC](https://docs.oracle.com/javase/8/docs/api/java/sql/package-summary.html) | Java 8+ | [opentelemetry-jdbc](../instrumentation/jdbc/library) | [Database Client Spans] | -| [Jedis](https://github.com/xetorthio/jedis) | 1.4+ | N/A | [Database Client Spans] | -| [JMS](https://javaee.github.io/javaee-spec/javadocs/javax/jms/package-summary.html) | 1.1+ | N/A | [Messaging Spans] | -| [Jodd Http](https://javadoc.io/doc/org.jodd/jodd-http/latest/index.html) | 4.2+ | N/A | [HTTP Client Spans], [HTTP Client Metrics] | -| [JSP](https://javaee.github.io/javaee-spec/javadocs/javax/servlet/jsp/package-summary.html) | 2.3+ | N/A | none | -| [Kotlin Coroutines](https://kotlinlang.org/docs/coroutines-overview.html) | 1.0+ | N/A | Context propagation | -| [Ktor](https://github.com/ktorio/ktor) | 1.0+ | [opentelemetry-ktor-1.0](../instrumentation/ktor/ktor-1.0/library),
[opentelemetry-ktor-2.0](../instrumentation/ktor/ktor-2.0/library) | [HTTP Client Spans], [HTTP Client Metrics], [HTTP Server Spans], [HTTP Server Metrics] | -| [Kubernetes Client](https://github.com/kubernetes-client/java) | 7.0+ | N/A | [HTTP Client Spans] | -| [Lettuce](https://github.com/lettuce-io/lettuce-core) | 4.0+ | [opentelemetry-lettuce-5.1](../instrumentation/lettuce/lettuce-5.1/library) | [Database Client Spans] | -| [Log4j 1](https://logging.apache.org/log4j/1.2/) | 1.2+ | N/A | none | -| [Log4j 2](https://logging.apache.org/log4j/2.x/) | 2.11+ | [opentelemetry-log4j-appender-2.17](../instrumentation/log4j/log4j-appender-2.17/library),
[opentelemetry-log4j-context-data-2.17-autoconfigure](../instrumentation/log4j/log4j-context-data/log4j-context-data-2.17/library-autoconfigure) | none | -| [Logback](http://logback.qos.ch/) | 1.0+ | [opentelemetry-logback-appender-1.0](../instrumentation/logback/logback-appender-1.0/library),
[opentelemetry-logback-mdc-1.0](../instrumentation/logback/logback-mdc-1.0/library) | none | -| [Micrometer](https://micrometer.io/) | 1.5+ | [opentelemetry-micrometer-1.5](../instrumentation/micrometer/micrometer-1.5/library) | none | -| [MongoDB Driver](https://mongodb.github.io/mongo-java-driver/) | 3.1+ | [opentelemetry-mongo-3.1](../instrumentation/mongo/mongo-3.1/library) | [Database Client Spans] | -| [Netty](https://github.com/netty/netty) | 3.8+ | [opentelemetry-netty-4.1](../instrumentation/netty/netty-4.1/library) | [HTTP Client Spans], [HTTP Client Metrics], [HTTP Server Spans], [HTTP Server Metrics] | -| [OkHttp](https://github.com/square/okhttp/) | 2.2+ | [opentelemetry-okhttp-3.0](../instrumentation/okhttp/okhttp-3.0/library) | [HTTP Client Spans], [HTTP Client Metrics] | -| [Oracle UCP](https://docs.oracle.com/database/121/JJUCP/) | 11.2+ | [opentelemetry-oracle-ucp-11.2](../instrumentation/oracle-ucp-11.2/library) | [Database Pool Metrics] | -| [OSHI](https://github.com/oshi/oshi/) | 5.3.1+ | [opentelemetry-oshi](../instrumentation/oshi/library) | [System Metrics] (partial support) | -| [Play](https://github.com/playframework/playframework) | 2.4+ | N/A | [HTTP Client Spans], [HTTP Client Metrics], Provides `http.route` [2] | -| [Play WS](https://github.com/playframework/play-ws) | 1.0+ | N/A | [HTTP Client Spans], [HTTP Client Metrics] | -| [Quartz](https://www.quartz-scheduler.org/) | 2.0+ | [opentelemetry-quartz-2.0](../instrumentation/quartz-2.0/library) | none | -| [R2DBC](https://r2dbc.io/) | 1.0+ | [opentelemetry-r2dbc-1.0](../instrumentation/r2dbc-1.0/library) | [Database Client Spans] | -| [RabbitMQ Client](https://github.com/rabbitmq/rabbitmq-java-client) | 2.7+ | N/A | [Messaging Spans] | -| [Ratpack](https://github.com/ratpack/ratpack) | 1.4+ | [opentelemetry-ratpack-1.7](../instrumentation/ratpack/ratpack-1.7/library) | [HTTP Client Spans], [HTTP Client Metrics], [HTTP Server Spans], [HTTP Server Metrics] | -| [Reactor](https://github.com/reactor/reactor-core) | 3.1+ | [opentelemetry-reactor-3.1](../instrumentation/reactor/reactor-3.1/library) | Context propagation | -| [Reactor Netty](https://github.com/reactor/reactor-netty) | 0.9+ | N/A | [HTTP Client Spans], [HTTP Client Metrics] | -| [Rediscala](https://github.com/etaty/rediscala) | 1.8+ | N/A | [Database Client Spans] | -| [Redisson](https://github.com/redisson/redisson) | 3.0+ | N/A | [Database Client Spans] | -| [RESTEasy](https://resteasy.github.io/) | 3.0+ | N/A | Provides `http.route` [2], Controller Spans [3] | -| [Restlet](https://restlet.github.io/) | 1.0+ | [opentelemetry-restlet-1.1](../instrumentation/restlet/restlet-1.1/library),
[opentelemetry-restlet-2.0](../instrumentation/restlet/restlet-2.0/library) | [HTTP Server Spans], [HTTP Server Metrics] | -| [RMI](https://docs.oracle.com/en/java/javase/11/docs/api/java.rmi/java/rmi/package-summary.html) | Java 8+ | | [RPC Client Spans], [RPC Server Spans] | -| [RxJava](https://github.com/ReactiveX/RxJava) | 1.0+ | [opentelemetry-rxjava-1.0](../instrumentation/rxjava/rxjava-1.0/library),
[opentelemetry-rxjava-2.0](../instrumentation/rxjava/rxjava-2.0/library),
[opentelemetry-rxjava-3.0](../instrumentation/rxjava/rxjava-3.0/library),
[opentelemetry-rxjava-3.1.1](../instrumentation/rxjava/rxjava-3.1.1/library) | Context propagation | -| [Scala ForkJoinPool](https://www.scala-lang.org/api/2.12.0/scala/concurrent/forkjoin/package$$ForkJoinPool$.html) | 2.8+ | N/A | Context propagation | -| [Servlet](https://javaee.github.io/javaee-spec/javadocs/javax/servlet/package-summary.html) | 2.2+ | N/A | [HTTP Server Spans], [HTTP Server Metrics] | -| [Spark Web Framework](https://github.com/perwendel/spark) | 2.3+ | N/A | Provides `http.route` [2] | -| [Spring Boot](https://spring.io/projects/spring-boot) | | [opentelemetry-spring-boot-resources](../instrumentation/spring/spring-boot-resources/library) | none | -| [Spring Batch](https://spring.io/projects/spring-batch) | 3.0+ (not including 5.0+ yet) | N/A | none | -| [Spring Data](https://spring.io/projects/spring-data) | 1.8+ | N/A | none | -| [Spring Integration](https://spring.io/projects/spring-integration) | 4.1+ (not including 6.0+ yet) | [opentelemetry-spring-integration-4.1](../instrumentation/spring/spring-integration-4.1/library) | [Messaging Spans] | -| [Spring JMS](https://docs.spring.io/spring-framework/docs/current/reference/html/integration.html#jms) | 2.0+ | N/A | [Messaging Spans] | -| [Spring Kafka](https://spring.io/projects/spring-kafka) | 2.7+ | [opentelemetry-spring-kafka-2.7](../instrumentation/spring/spring-kafka-2.7/library) | [Messaging Spans] | -| [Spring RabbitMQ](https://spring.io/projects/spring-amqp) | 1.0+ | N/A | [Messaging Spans] | -| [Spring Scheduling](https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/scheduling/package-summary.html) | 3.1+ | N/A | none | -| [Spring RestTemplate](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/client/package-summary.html) | 3.1+ | [opentelemetry-spring-web-3.1](../instrumentation/spring/spring-web/spring-web-3.1/library) | [HTTP Client Spans], [HTTP Client Metrics] | -| [Spring Web MVC](https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/web/servlet/mvc/package-summary.html) | 3.1+ | [opentelemetry-spring-webmvc-5.3](../instrumentation/spring/spring-webmvc/spring-webmvc-5.3/library),
[opentelemetry-spring-webmvc-6.0](../instrumentation/spring/spring-webmvc/spring-webmvc-6.0/library) | [HTTP Server Spans], [HTTP Server Metrics] | -| [Spring Web Services](https://spring.io/projects/spring-ws) | 2.0+ | N/A | none | -| [Spring WebFlux](https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/web/reactive/package-summary.html) | 5.3+ | [opentelemetry-spring-webflux-5.3](../instrumentation/spring/spring-webflux/spring-webflux-5.3/library) | [HTTP Client Spans], [HTTP Client Metrics], [HTTP Server Spans], [HTTP Server Metrics] | -| [Spymemcached](https://github.com/couchbase/spymemcached) | 2.12+ | N/A | [Database Client Spans] | -| [Tomcat JDBC Pool](https://tomcat.apache.org/tomcat-7.0-doc/jdbc-pool.html) | 8.5.0+ | N/A | [Database Pool Metrics] | -| [Twilio](https://github.com/twilio/twilio-java) | 6.6+ (not including 8.x yet) | N/A | none | -| [Undertow](https://undertow.io/) | 1.4+ | N/A | [HTTP Server Spans], [HTTP Server Metrics] | -| [Vaadin](https://vaadin.com/) | 14.2+ | N/A | Provides `http.route` [2], Controller Spans [3] | -| [Vert.x Web](https://vertx.io/docs/vertx-web/java/) | 3.0+ | N/A | Provides `http.route` [2] | -| [Vert.x HttpClient](https://vertx.io/docs/apidocs/io/vertx/core/http/HttpClient.html) | 3.0+ | N/A | [HTTP Client Spans], [HTTP Client Metrics] | -| [Vert.x Kafka Client](https://vertx.io/docs/vertx-kafka-client/java/) | 3.6+ | N/A | [Messaging Spans] | -| [Vert.x RxJava2](https://vertx.io/docs/vertx-rx/java2/) | 3.5+ | N/A | context propagation only | -| [Vert.x SQL Client](https://github.com/eclipse-vertx/vertx-sql-client/) | 4.0+ | N/A | [Database Client Spans] | -| [Vibur DBCP](https://www.vibur.org/) | 11.0+ | [opentelemetry-vibur-dbcp-11.0](../instrumentation/vibur-dbcp-11.0/library) | [Database Pool Metrics] | -| [ZIO](https://zio.dev/) | 2.0.0+ | N/A | Context propagation | +| Library/Framework | Auto-instrumented versions | Standalone Library Instrumentation [1] | Semantic Conventions | +|---------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------| +| [Akka Actors](https://doc.akka.io/docs/akka/current/typed/index.html) | 2.3+ | N/A | Context propagation | +| [Akka HTTP](https://doc.akka.io/docs/akka-http/current/index.html) | 10.0+ | N/A | [HTTP Client Spans], [HTTP Client Metrics], [HTTP Server Spans], [HTTP Server Metrics], Provides `http.route` [2] | +| [Alibaba Druid](https://github.com/alibaba/druid) | 1.0+ | [opentelemetry-alibaba-druid-1.0](../instrumentation/alibaba-druid-1.0/library) | [Database Pool Metrics] | +| [Apache Axis2](https://axis.apache.org/axis2/java/core/) | 1.6+ | N/A | Provides `http.route` [2], Controller Spans [3] | +| [Apache Camel](https://camel.apache.org/) | 2.20+ (not including 3.x yet) | N/A | Dependent on components in use | +| [Apache CXF JAX-RS](https://cxf.apache.org/) | 3.2+ | N/A | Provides `http.route` [2], Controller Spans [3] | +| [Apache CXF JAX-WS](https://cxf.apache.org/) | 3.0+ | N/A | Provides `http.route` [2], Controller Spans [3] | +| [Apache DBCP](https://commons.apache.org/proper/commons-dbcp/) | 2.0+ | [opentelemetry-apache-dbcp-2.0](../instrumentation/apache-dbcp-2.0/library) | [Database Pool Metrics] | +| [Apache Dubbo](https://github.com/apache/dubbo/) | 2.7+ | [opentelemetry-apache-dubbo-2.7](../instrumentation/apache-dubbo-2.7/library-autoconfigure) | [RPC Client Spans], [RPC Server Spans] | +| [Apache HttpAsyncClient](https://hc.apache.org/index.html) | 4.1+ | N/A | [HTTP Client Spans], [HTTP Client Metrics] | +| [Apache HttpClient](https://hc.apache.org/index.html) | 2.0+ | [opentelemetry-apache-httpclient-4.3](../instrumentation/apache-httpclient/apache-httpclient-4.3/library),
[opentelemetry-apache-httpclient-5.2](../instrumentation/apache-httpclient/apache-httpclient-5.2/library) | [HTTP Client Spans], [HTTP Client Metrics] | +| [Apache ShenYu](https://shenyu.apache.org/) | 2.4+ | N/A | Provides `http.route` [2] | +| [Apache Kafka Producer/Consumer API](https://kafka.apache.org/documentation/#producerapi) | 0.11+ | [opentelemetry-kafka-clients-2.6](../instrumentation/kafka/kafka-clients/kafka-clients-2.6/library) | [Messaging Spans] | +| [Apache Kafka Streams API](https://kafka.apache.org/documentation/streams/) | 0.11+ | N/A | [Messaging Spans] | +| [Apache MyFaces](https://myfaces.apache.org/) | 1.2+ (not including 3.x yet) | N/A | Provides `http.route` [2], Controller Spans [3] | +| [Apache Pekko Actors](https://pekko.apache.org/) | 1.0+ | N/A | Context propagation | +| [Apache Pekko HTTP](https://pekko.apache.org/) | 1.0+ | N/A | [HTTP Client Spans], [HTTP Client Metrics], [HTTP Server Spans], [HTTP Server Metrics], Provides `http.route` [2] | +| [Apache Pulsar](https://pulsar.apache.org/) | 2.8+ | N/A | [Messaging Spans] | +| [Apache RocketMQ gRPC/Protobuf-based Client](https://rocketmq.apache.org/) | 5.0+ | N/A | [Messaging Spans] | +| [Apache RocketMQ Remoting-based Client](https://rocketmq.apache.org/) | 4.8+ | [opentelemetry-rocketmq-client-4.8](../instrumentation/rocketmq/rocketmq-client/rocketmq-client-4.8/library) | [Messaging Spans] | +| [Apache Struts 2](https://github.com/apache/struts) | 2.3+ | N/A | Provides `http.route` [2], Controller Spans [3] | +| [Apache Tapestry](https://tapestry.apache.org/) | 5.4+ | N/A | Provides `http.route` [2], Controller Spans [3] | +| [Apache Wicket](https://wicket.apache.org/) | 8.0+ | N/A | Provides `http.route` [2] | +| [Armeria](https://armeria.dev) | 1.3+ | [opentelemetry-armeria-1.3](../instrumentation/armeria/armeria-1.3/library) | [HTTP Client Spans], [HTTP Client Metrics], [HTTP Server Spans], [HTTP Server Metrics] | +| [Armeria gRPC](https://armeria.dev) | 1.14+ | | [RPC Client Spans], [RPC Client Metrics], [RPC Server Spans], [RPC Server Metrics] | +| [AsyncHttpClient](https://github.com/AsyncHttpClient/async-http-client) | 1.9+ | N/A | [HTTP Client Spans], [HTTP Client Metrics] | +| [AWS Lambda](https://docs.aws.amazon.com/lambda/latest/dg/java-handler.html) | 1.0+ | [opentelemetry-aws-lambda-core-1.0](../instrumentation/aws-lambda/aws-lambda-core-1.0/library),
[opentelemetry-aws-lambda-events-2.2](../instrumentation/aws-lambda/aws-lambda-events-2.2/library) | [FaaS Server Spans] | +| [AWS SDK](https://aws.amazon.com/sdk-for-java/) | 1.11.x and 2.2+ | [opentelemetry-aws-sdk-1.11](../instrumentation/aws-sdk/aws-sdk-1.11/library),
[opentelemetry-aws-sdk-1.11-autoconfigure](../instrumentation/aws-sdk/aws-sdk-1.11/library-autoconfigure),
[opentelemetry-aws-sdk-2.2](../instrumentation/aws-sdk/aws-sdk-2.2/library),
[opentelemetry-aws-sdk-2.2-autoconfigure](../instrumentation/aws-sdk/aws-sdk-2.2/library-autoconfigure) | [Messaging Spans], [Database Client Spans], [HTTP Client Spans] | +| [Azure Core](https://docs.microsoft.com/en-us/java/api/overview/azure/core-readme) | 1.14+ | N/A | Context propagation | +| [Cassandra Driver](https://github.com/datastax/java-driver) | 3.0+ | [opentelemetry-cassandra-4.4](../instrumentation/cassandra/cassandra-4.4/library) | [Database Client Spans] | +| [Clickhouse Client](https://github.com/ClickHouse/clickhouse-java) | 0.5+ | N/A | [Database Client Spans] | +| [Couchbase Client](https://github.com/couchbase/couchbase-java-client) | 2.0+ and 3.1+ | N/A | [Database Client Spans] | +| [c3p0](https://github.com/swaldman/c3p0) | 0.9.2+ | [opentelemetry-c3p0-0.9](../instrumentation/c3p0-0.9/library) | [Database Pool Metrics] | +| [Dropwizard Metrics](https://metrics.dropwizard.io/) | 4.0+ (disabled by default) | N/A | none | +| [Dropwizard Views](https://www.dropwizard.io/en/latest/manual/views.html) | 0.7+ | N/A | Controller Spans [3] | +| [Eclipse Grizzly](https://javaee.github.io/grizzly/httpserverframework.html) | 2.3+ | N/A | [HTTP Server Spans], [HTTP Server Metrics] | +| [Eclipse Jersey](https://eclipse-ee4j.github.io/jersey/) | 2.0+ | N/A | Provides `http.route` [2], Controller Spans [3] | +| [Eclipse Jetty HTTP Client](https://www.eclipse.org/jetty/javadoc/jetty-9/org/eclipse/jetty/client/HttpClient.html) | 9.2 - 9.4.x,
12.0+ | [opentelemetry-jetty-httpclient-9.2](../instrumentation/jetty-httpclient/jetty-httpclient-9.2/library)
[opentelemetry-jetty-httpclient-12.0](../instrumentation/jetty-httpclient/jetty-httpclient-12.0/library) | [HTTP Client Spans], [HTTP Client Metrics] | +| [Eclipse Metro](https://projects.eclipse.org/projects/ee4j.metro) | 2.2+ | N/A | Provides `http.route` [2], Controller Spans [3] | +| [Eclipse Mojarra](https://projects.eclipse.org/projects/ee4j.mojarra) | 1.2+ | N/A | Provides `http.route` [2], Controller Spans [3] | +| [Elasticsearch API Client](https://www.elastic.co/guide/en/elasticsearch/client/java-api-client/current/index.html) | 7.16 - 7.17.19,
8.0 - 8.9.+ [4] | N/A | [Elasticsearch Client Spans] | +| [Elasticsearch REST Client](https://www.elastic.co/guide/en/elasticsearch/client/java-rest/current/index.html) | 5.0+ | N/A | [Database Client Spans] | +| [Elasticsearch Transport Client](https://www.elastic.co/guide/en/elasticsearch/client/java-api/current/index.html) | 5.0+ | N/A | [Database Client Spans] | +| [Finagle](https://github.com/twitter/finagle) | 23.11+ | N/A | none | +| [Finatra](https://github.com/twitter/finatra) | 2.9+ | N/A | Provides `http.route` [2], Controller Spans [3] | +| [Geode Client](https://geode.apache.org/) | 1.4+ | N/A | [Database Client Spans] | +| [Google HTTP Client](https://github.com/googleapis/google-http-java-client) | 1.19+ | N/A | [HTTP Client Spans], [HTTP Client Metrics] | +| [Grails](https://grails.org/) | 3.0+ | N/A | Provides `http.route` [2], Controller Spans [3] | +| [GraphQL Java](https://www.graphql-java.com/) | 12.0+ | [opentelemetry-graphql-java-12.0](../instrumentation/graphql-java/graphql-java-12.0/library),
[opentelemetry-graphql-java-20.0](../instrumentation/graphql-java/graphql-java-20.0/library) | [GraphQL Server Spans] | +| [gRPC](https://github.com/grpc/grpc-java) | 1.6+ | [opentelemetry-grpc-1.6](../instrumentation/grpc-1.6/library) | [RPC Client Spans], [RPC Client Metrics], [RPC Server Spans], [RPC Server Metrics] | +| [Guava ListenableFuture](https://guava.dev/releases/snapshot/api/docs/com/google/common/util/concurrent/ListenableFuture.html) | 10.0+ | [opentelemetry-guava-10.0](../instrumentation/guava-10.0/library) | Context propagation | +| [GWT](http://www.gwtproject.org/) | 2.0+ | N/A | [RPC Server Spans] | +| [Hibernate](https://github.com/hibernate/hibernate-orm) | 3.3+ | N/A | none | +| [Hibernate Reactive](https://hibernate.org/reactive) | 1.0+ | N/A | none | +| [HikariCP](https://github.com/brettwooldridge/HikariCP) | 3.0+ | [opentelemetry-hikaricp-3.0](../instrumentation/hikaricp-3.0/library) | [Database Pool Metrics] | +| [HttpURLConnection](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/net/HttpURLConnection.html) | Java 8+ | N/A | [HTTP Client Spans], [HTTP Client Metrics] | +| [Hystrix](https://github.com/Netflix/Hystrix) | 1.4+ | N/A | none | +| [InfluxDB Client](https://github.com/influxdata/influxdb-java) | 2.4+ | N/A | [Database Client Spans] | +| [Java Executors](https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/Executor.html) | Java 8+ | N/A | Context propagation | +| [Java Http Client](https://docs.oracle.com/en/java/javase/11/docs/api/java.net.http/java/net/http/package-summary.html) | Java 11+ | [opentelemetry-java-http-client](../instrumentation/java-http-client/library) | [HTTP Client Spans], [HTTP Client Metrics] | +| [java.util.logging](https://docs.oracle.com/javase/8/docs/api/java/util/logging/package-summary.html) | Java 8+ | N/A | none | +| [Java Platform](https://docs.oracle.com/javase/8/docs/api/java/lang/management/ManagementFactory.html) | Java 8+ | [opentelemetry-runtime-telemetry-java8](../instrumentation/runtime-telemetry/runtime-telemetry-java8/library),
[opentelemetry-runtime-telemetry-java17](../instrumentation/runtime-telemetry/runtime-telemetry-java17/library),
[opentelemetry-resources](../instrumentation/resources/library) | [JVM Runtime Metrics] | +| [Javalin](https://javalin.io/) | 5.0+ | N/A | Provides `http.route` [2] | +| [JAX-RS](https://javaee.github.io/javaee-spec/javadocs/javax/ws/rs/package-summary.html) | 0.5+ | N/A | Provides `http.route` [2], Controller Spans [3] | +| [JAX-RS Client](https://javaee.github.io/javaee-spec/javadocs/javax/ws/rs/client/package-summary.html) | 1.1+ | N/A | [HTTP Client Spans], [HTTP Client Metrics] | +| [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) | N/A | Provides `http.route` [2], Controller Spans [3] | +| [JBoss Log Manager](https://github.com/jboss-logging/jboss-logmanager) | 1.1+ | N/A | none | +| [JDBC](https://docs.oracle.com/javase/8/docs/api/java/sql/package-summary.html) | Java 8+ | [opentelemetry-jdbc](../instrumentation/jdbc/library) | [Database Client Spans] | +| [Jedis](https://github.com/xetorthio/jedis) | 1.4+ | N/A | [Database Client Spans] | +| [JMS](https://javaee.github.io/javaee-spec/javadocs/javax/jms/package-summary.html) | 1.1+ | N/A | [Messaging Spans] | +| [Jodd Http](https://http.jodd.org/) | 4.2+ | N/A | [HTTP Client Spans], [HTTP Client Metrics] | +| [JSP](https://javaee.github.io/javaee-spec/javadocs/javax/servlet/jsp/package-summary.html) | 2.3+ | N/A | Controller Spans [3] | +| [Kotlin Coroutines](https://kotlinlang.org/docs/coroutines-overview.html) | 1.0+ | N/A | Context propagation | +| [Ktor](https://github.com/ktorio/ktor) | 1.0+ | [opentelemetry-ktor-1.0](../instrumentation/ktor/ktor-1.0/library),
[opentelemetry-ktor-2.0](../instrumentation/ktor/ktor-2.0/library) | [HTTP Client Spans], [HTTP Client Metrics], [HTTP Server Spans], [HTTP Server Metrics] | +| [Kubernetes Client](https://github.com/kubernetes-client/java) | 7.0+ | N/A | [HTTP Client Spans] | +| [Lettuce](https://github.com/lettuce-io/lettuce-core) | 4.0+ | [opentelemetry-lettuce-5.1](../instrumentation/lettuce/lettuce-5.1/library) | [Database Client Spans] | +| [Log4j 1](https://logging.apache.org/log4j/1.2/) | 1.2+ | N/A | none | +| [Log4j 2](https://logging.apache.org/log4j/2.x/) | 2.11+ | [opentelemetry-log4j-appender-2.17](../instrumentation/log4j/log4j-appender-2.17/library),
[opentelemetry-log4j-context-data-2.17-autoconfigure](../instrumentation/log4j/log4j-context-data/log4j-context-data-2.17/library-autoconfigure) | none | +| [Logback](http://logback.qos.ch/) | 1.0+ | [opentelemetry-logback-appender-1.0](../instrumentation/logback/logback-appender-1.0/library),
[opentelemetry-logback-mdc-1.0](../instrumentation/logback/logback-mdc-1.0/library) | none | +| [Micrometer](https://micrometer.io/) | 1.5+ | [opentelemetry-micrometer-1.5](../instrumentation/micrometer/micrometer-1.5/library) | none | +| [MongoDB Driver](https://mongodb.github.io/mongo-java-driver/) | 3.1+ | [opentelemetry-mongo-3.1](../instrumentation/mongo/mongo-3.1/library) | [Database Client Spans] | +| [MyBatis](https://mybatis.org/mybatis-3/) | 3.2+ | N/A | none | +| [Netty HTTP codec [5]](https://github.com/netty/netty) | 3.8+ | [opentelemetry-netty-4.1](../instrumentation/netty/netty-4.1/library) | [HTTP Client Spans], [HTTP Client Metrics], [HTTP Server Spans], [HTTP Server Metrics] | +| [OkHttp](https://github.com/square/okhttp/) | 2.2+ | [opentelemetry-okhttp-3.0](../instrumentation/okhttp/okhttp-3.0/library) | [HTTP Client Spans], [HTTP Client Metrics] | +| [Oracle UCP](https://docs.oracle.com/database/121/JJUCP/) | 11.2+ | [opentelemetry-oracle-ucp-11.2](../instrumentation/oracle-ucp-11.2/library) | [Database Pool Metrics] | +| [OSHI](https://github.com/oshi/oshi/) | 5.3.1+ | [opentelemetry-oshi](../instrumentation/oshi/library) | [System Metrics] (partial support) | +| [Play MVC](https://github.com/playframework/playframework) | 2.4+ | N/A | Provides `http.route` [2], Controller Spans [3] | +| [Play WS](https://github.com/playframework/play-ws) | 1.0+ | N/A | [HTTP Client Spans], [HTTP Client Metrics] | +| [Quarkus Resteasy Reactive](https://quarkus.io/extensions/io.quarkus/quarkus-resteasy-reactive/) | 2.16.7+ | N/A | Provides `http.route` [2] | +| [Quartz](https://www.quartz-scheduler.org/) | 2.0+ | [opentelemetry-quartz-2.0](../instrumentation/quartz-2.0/library) | none | +| [R2DBC](https://r2dbc.io/) | 1.0+ | [opentelemetry-r2dbc-1.0](../instrumentation/r2dbc-1.0/library) | [Database Client Spans] | +| [RabbitMQ Client](https://github.com/rabbitmq/rabbitmq-java-client) | 2.7+ | N/A | [Messaging Spans] | +| [Ratpack](https://github.com/ratpack/ratpack) | 1.4+ | [opentelemetry-ratpack-1.7](../instrumentation/ratpack/ratpack-1.7/library) | [HTTP Client Spans], [HTTP Client Metrics], [HTTP Server Spans], [HTTP Server Metrics] | +| [Reactor](https://github.com/reactor/reactor-core) | 3.1+ | [opentelemetry-reactor-3.1](../instrumentation/reactor/reactor-3.1/library) | Context propagation | +| [Reactor Netty](https://github.com/reactor/reactor-netty) | 0.9+ | N/A | [HTTP Client Spans], [HTTP Client Metrics] | +| [Rediscala](https://github.com/etaty/rediscala) | 1.8+ | N/A | [Database Client Spans] | +| [Redisson](https://github.com/redisson/redisson) | 3.0+ | N/A | [Database Client Spans] | +| [RESTEasy](https://resteasy.dev/) | 3.0+ | N/A | Provides `http.route` [2], Controller Spans [3] | +| [Restlet](https://restlet.github.io/) | 1.0+ | [opentelemetry-restlet-1.1](../instrumentation/restlet/restlet-1.1/library),
[opentelemetry-restlet-2.0](../instrumentation/restlet/restlet-2.0/library) | [HTTP Server Spans], [HTTP Server Metrics] | +| [RMI](https://docs.oracle.com/en/java/javase/11/docs/api/java.rmi/java/rmi/package-summary.html) | Java 8+ | | [RPC Client Spans], [RPC Server Spans] | +| [RxJava](https://github.com/ReactiveX/RxJava) | 1.0+ | [opentelemetry-rxjava-1.0](../instrumentation/rxjava/rxjava-1.0/library),
[opentelemetry-rxjava-2.0](../instrumentation/rxjava/rxjava-2.0/library),
[opentelemetry-rxjava-3.0](../instrumentation/rxjava/rxjava-3.0/library),
[opentelemetry-rxjava-3.1.1](../instrumentation/rxjava/rxjava-3.1.1/library) | Context propagation | +| [Scala ForkJoinPool](https://www.scala-lang.org/api/2.12.0/scala/concurrent/forkjoin/package$$ForkJoinPool$.html) | 2.8+ | N/A | Context propagation | +| [Servlet](https://javaee.github.io/javaee-spec/javadocs/javax/servlet/package-summary.html) | 2.2+ | N/A | [HTTP Server Spans], [HTTP Server Metrics] | +| [Spark Web Framework](https://github.com/perwendel/spark) | 2.3+ | N/A | Provides `http.route` [2] | +| [Spring Batch](https://spring.io/projects/spring-batch) | 3.0+ (not including 5.0+ yet) | N/A | none | +| [Spring Boot](https://spring.io/projects/spring-boot) | | [opentelemetry-spring-boot-resources](https://opentelemetry.io/docs/zero-code/java/spring-boot/) | none | +| [Spring Cloud Gateway](https://github.com/spring-cloud/spring-cloud-gateway) | 2.0+ | N/A | Provides `http.route` [2] | +| [Spring Core](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/core/package-summary.html) | 2.0+ | N/A | Context propagation | +| [Spring Data](https://spring.io/projects/spring-data) | 1.8+ | N/A | none | +| [Spring Integration](https://spring.io/projects/spring-integration) | 4.1+ (not including 6.0+ yet) | [opentelemetry-spring-integration-4.1](../instrumentation/spring/spring-integration-4.1/library) | [Messaging Spans] | +| [Spring JMS](https://docs.spring.io/spring-framework/docs/current/reference/html/integration.html#jms) | 2.0+ | N/A | [Messaging Spans] | +| [Spring Kafka](https://spring.io/projects/spring-kafka) | 2.7+ | [opentelemetry-spring-kafka-2.7](../instrumentation/spring/spring-kafka-2.7/library) | [Messaging Spans] | +| [Spring RabbitMQ](https://spring.io/projects/spring-amqp) | 1.0+ | N/A | [Messaging Spans] | +| [Spring RestTemplate](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/client/package-summary.html) | 3.1+ | [opentelemetry-spring-web-3.1](../instrumentation/spring/spring-web/spring-web-3.1/library) | [HTTP Client Spans], [HTTP Client Metrics] | +| [Spring RMI](https://docs.spring.io/spring-framework/docs/4.0.x/javadoc-api/org/springframework/remoting/rmi/package-summary.html) | 4.0+ | N/A | [RPC Client Spans], [RPC Server Spans] | +| [Spring Scheduling](https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/scheduling/package-summary.html) | 3.1+ | N/A | none | +| [Spring Web MVC](https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/web/servlet/mvc/package-summary.html) | 3.1+ | [opentelemetry-spring-webmvc-5.3](../instrumentation/spring/spring-webmvc/spring-webmvc-5.3/library),
[opentelemetry-spring-webmvc-6.0](../instrumentation/spring/spring-webmvc/spring-webmvc-6.0/library) | [HTTP Server Spans], [HTTP Server Metrics], Provides `http.route` [2], Controller Spans [3] | +| [Spring Web Services](https://spring.io/projects/spring-ws) | 2.0+ | N/A | none | +| [Spring WebFlux](https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/web/reactive/package-summary.html) | 5.3+ | [opentelemetry-spring-webflux-5.3](../instrumentation/spring/spring-webflux/spring-webflux-5.3/library) | [HTTP Client Spans], [HTTP Client Metrics], [HTTP Server Spans], [HTTP Server Metrics], Provides `http.route` [2], Controller Spans [3] | +| [Spymemcached](https://github.com/couchbase/spymemcached) | 2.12+ | N/A | [Database Client Spans] | +| [Tomcat JDBC Pool](https://tomcat.apache.org/tomcat-7.0-doc/jdbc-pool.html) | 8.5+ | N/A | [Database Pool Metrics] | +| [Twilio](https://github.com/twilio/twilio-java) | 6.6+ (not including 8.x yet) | N/A | none | +| [Undertow](https://undertow.io/) | 1.4+ | N/A | [HTTP Server Spans], [HTTP Server Metrics] | +| [Vaadin](https://vaadin.com/) | 14.2+ | N/A | Provides `http.route` [2], Controller Spans [3] | +| [Vert.x HttpClient](https://vertx.io/docs/apidocs/io/vertx/core/http/HttpClient.html) | 3.0+ | N/A | [HTTP Client Spans], [HTTP Client Metrics] | +| [Vert.x Kafka Client](https://vertx.io/docs/vertx-kafka-client/java/) | 3.6+ | N/A | [Messaging Spans] | +| [Vert.x Redis Client](https://vertx.io/docs/vertx-redis-client/java/) | 4.0+ | N/A | [Database Client Spans] | +| [Vert.x RxJava2](https://vertx.io/docs/vertx-rx/java2/) | 3.5+ | N/A | context propagation only | +| [Vert.x SQL Client](https://github.com/eclipse-vertx/vertx-sql-client/) | 4.0+ | N/A | [Database Client Spans] | +| [Vert.x Web](https://vertx.io/docs/vertx-web/java/) | 3.0+ | N/A | Provides `http.route` [2] | +| [Vibur DBCP](https://www.vibur.org/) | 11.0+ | [opentelemetry-vibur-dbcp-11.0](../instrumentation/vibur-dbcp-11.0/library) | [Database Pool Metrics] | +| [XXL-JOB](https://www.xuxueli.com/xxl-job/en/) | 1.9.2+ | N/A | none | +| [ZIO](https://zio.dev/) | 2.0+ | N/A | Context propagation | **[1]** Standalone library instrumentation refers to instrumentation that can be used without the Java agent. @@ -141,37 +159,51 @@ These are the supported libraries and frameworks: **[3]** Controller Spans are `INTERNAL` spans capturing the controller and/or view execution. See [Suppressing controller and/or view spans](https://opentelemetry.io/docs/instrumentation/java/automatic/agent-config/#suppressing-controller-andor-view-spans). -[HTTP Server Spans]: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/http.md#http-server-semantic-conventions -[HTTP Client Spans]: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/http.md#http-client -[HTTP Server Metrics]: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/semantic_conventions/http-metrics.md#http-server -[HTTP Client Metrics]: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/semantic_conventions/http-metrics.md#http-client -[RPC Server Spans]: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/rpc.md#server-attributes -[RPC Client Spans]: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/rpc.md#common-attributes -[RPC Server Metrics]: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/semantic_conventions/rpc-metrics.md#rpc-server -[RPC Client Metrics]: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/semantic_conventions/rpc-metrics.md#rpc-client -[Messaging Spans]: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/messaging.md -[Database Client Spans]: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/database.md -[Database Pool Metrics]: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/semantic_conventions/database-metrics.md -[JVM Runtime Metrics]: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/semantic_conventions/runtime-environment-metrics.md#jvm-metrics -[System Metrics]: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/semantic_conventions/system-metrics.md -[GraphQL Server Spans]: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/instrumentation/graphql.md -[FaaS Server Spans]: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/faas.md +**[4]** Newer versions of the library have telemetry built-in. + +**[5]** Doesn't currently support capturing HTTP/2 traffic. + +[Elasticsearch Client Spans]: https://github.com/open-telemetry/semantic-conventions/blob/main/docs/database/elasticsearch.md +[HTTP Server Spans]: https://github.com/open-telemetry/semantic-conventions/blob/main/docs/http/http-spans.md#http-server +[HTTP Client Spans]: https://github.com/open-telemetry/semantic-conventions/blob/main/docs/http/http-spans.md#http-client +[HTTP Server Metrics]: https://github.com/open-telemetry/semantic-conventions/blob/main/docs/http/http-metrics.md#http-server +[HTTP Client Metrics]: https://github.com/open-telemetry/semantic-conventions/blob/main/docs/http/http-metrics.md#http-client +[RPC Server Spans]: https://github.com/open-telemetry/semantic-conventions/blob/main/docs/rpc/rpc-spans.md#server-attributes +[RPC Client Spans]: https://github.com/open-telemetry/semantic-conventions/blob/main/docs/rpc/rpc-spans.md#client-attributes +[RPC Server Metrics]: https://github.com/open-telemetry/semantic-conventions/blob/main/docs/rpc/rpc-metrics.md#rpc-server +[RPC Client Metrics]: https://github.com/open-telemetry/semantic-conventions/blob/main/docs/rpc/rpc-metrics.md#rpc-client +[Messaging Spans]: https://github.com/open-telemetry/semantic-conventions/blob/main/docs/messaging/messaging-spans.md +[Database Client Spans]: https://github.com/open-telemetry/semantic-conventions/blob/main/docs/database/database-spans.md +[Database Pool Metrics]: https://github.com/open-telemetry/semantic-conventions/blob/main/docs/database/database-metrics.md +[JVM Runtime Metrics]: https://github.com/open-telemetry/semantic-conventions/blob/main/docs/runtime/jvm-metrics.md +[System Metrics]: https://github.com/open-telemetry/semantic-conventions/blob/main/docs/system/system-metrics.md +[GraphQL Server Spans]: https://github.com/open-telemetry/semantic-conventions/blob/main/docs/graphql/graphql-spans.md +[FaaS Server Spans]: https://github.com/open-telemetry/semantic-conventions/blob/main/docs/faas/faas-spans.md ## Application Servers These are the application servers that the smoke tests are run against: -| Application server | Version | JVM | OS | -| ----------------------------------------------------------------------------------------- | --------------------------- | ----------------- | ------------------------------ | -| [Jetty](https://www.eclipse.org/jetty/) | 9.4.x, 10.0.x, 11.0.x | OpenJDK 8, 11, 17 | [`ubuntu-latest`], [`windows-latest`] | -| [Payara](https://www.payara.fish/) | 5.0.x, 5.1.x | OpenJDK 8, 11 | [`ubuntu-latest`], [`windows-latest`] | -| [Tomcat](http://tomcat.apache.org/) | 7.0.x | OpenJDK 8 | [`ubuntu-latest`], [`windows-latest`] | -| [Tomcat](http://tomcat.apache.org/) | 7.0.x, 8.5.x, 9.0.x, 10.0.x | OpenJDK 8, 11, 17 | [`ubuntu-latest`], [`windows-latest`] | -| [TomEE](https://tomee.apache.org/) | 7.x, 8.x | OpenJDK 8, 11, 17 | [`ubuntu-latest`], [`windows-latest`] | -| [Open Liberty](https://openliberty.io/) | 21.x, 22.x, 23.x | OpenJDK 8, 11, 17 | [`ubuntu-latest`], [`windows-latest`] | -| [Websphere Traditional](https://www.ibm.com/uk-en/cloud/websphere-application-server) | 8.5.5.x, 9.0.x | IBM JDK 8 | Red Hat Enterprise Linux 8.4 | -| [WildFly](https://www.wildfly.org/) | 13.x | OpenJDK 8 | [`ubuntu-latest`], [`windows-latest`] | -| [WildFly](https://www.wildfly.org/) | 17.x, 21.x, 25.x | OpenJDK 8, 11, 17 | [`ubuntu-latest`], [`windows-latest`] | +| Application server | Version | JVM | OS | +|---------------------------------------------------------------------------------------|------------------------------------------|------------------------------------------------|---------------------------------------| +| [Jetty](https://www.eclipse.org/jetty/) | 9.4.53 | OpenJDK 8, 11, 17, 21
OpenJ9 8, 11, 17, 21 | [`ubuntu-latest`], [`windows-latest`] | +| [Jetty](https://www.eclipse.org/jetty/) | 10.0.19, 11.0.19 | OpenJDK 11, 17, 21
OpenJ9 11, 17, 21 | [`ubuntu-latest`], [`windows-latest`] | +| [Jetty](https://www.eclipse.org/jetty/) | 12.0.6 | OpenJDK 17, 21
OpenJ9 17, 21 | [`ubuntu-latest`], [`windows-latest`] | +| [Open Liberty](https://openliberty.io/) | 20.0.0.12 | OpenJDK 8, 11
OpenJ9 8, 11 | [`ubuntu-latest`], [`windows-latest`] | +| [Open Liberty](https://openliberty.io/) | 21.0.0.12, 22.0.0.12 | OpenJDK 8, 11, 17
OpenJ9 8, 11, 17 | [`ubuntu-latest`], [`windows-latest`] | +| [Open Liberty](https://openliberty.io/) | 23.0.0.12 | OpenJDK 8, 11, 17, 20
OpenJ9 8, 11, 17, 20 | [`ubuntu-latest`], [`windows-latest`] | +| [Payara](https://www.payara.fish/) | 5.2020.6, 5.2021.8 | OpenJDK 8, 11
OpenJ9 8, 11 | [`ubuntu-latest`], [`windows-latest`] | +| [Payara](https://www.payara.fish/) | 6.2023.12 | OpenJDK 11, 17
OpenJ9 11, 17, 21 | [`ubuntu-latest`], [`windows-latest`] | +| [Tomcat](http://tomcat.apache.org/) | 7.0.109 | OpenJDK 8
OpenJ9 8 | [`ubuntu-latest`], [`windows-latest`] | +| [Tomcat](http://tomcat.apache.org/) | 8.5.98, 9.0.85 | OpenJDK 8, 11, 17, 21
OpenJ9 8, 11, 17, 21 | [`ubuntu-latest`], [`windows-latest`] | +| [Tomcat](http://tomcat.apache.org/) | 10.1.18 | OpenJDK 11, 17, 21
OpenJ9 11, 17, 21 | [`ubuntu-latest`], [`windows-latest`] | +| [TomEE](https://tomee.apache.org/) | 7.0.9, 7.1.4 | OpenJDK 8
OpenJ9 8 | [`ubuntu-latest`], [`windows-latest`] | +| [TomEE](https://tomee.apache.org/) | 8.0.16 | OpenJDK 8, 11, 17, 21
OpenJ9 8, 11, 17, 21 | [`ubuntu-latest`], [`windows-latest`] | +| [TomEE](https://tomee.apache.org/) | 9.1.2 | OpenJDK 11, 17, 21
OpenJ9 11, 17, 21 | [`ubuntu-latest`], [`windows-latest`] | +| [Websphere Traditional](https://www.ibm.com/uk-en/cloud/websphere-application-server) | 8.5.5.22, 9.0.5.14 | IBM JDK 8 | Red Hat Enterprise Linux 8.4 | +| [WildFly](https://www.wildfly.org/) | 13.0.0.Final | OpenJDK 8
OpenJ9 8 | [`ubuntu-latest`], [`windows-latest`] | +| [WildFly](https://www.wildfly.org/) | 17.0.1.Final, 21.0.0.Final | OpenJDK 8, 11, 17, 21
OpenJ9 8, 11, 17, 21 | [`ubuntu-latest`], [`windows-latest`] | +| [WildFly](https://www.wildfly.org/) | 28.0.1.Final, 29.0.1.Final, 30.0.1.Final | OpenJDK 11, 17, 21
OpenJ9 11, 17, 21 | [`ubuntu-latest`], [`windows-latest`] | [`ubuntu-latest`]: https://github.com/actions/runner-images#available-images [`windows-latest`]: https://github.com/actions/runner-images#available-images @@ -180,10 +212,10 @@ These are the application servers that the smoke tests are run against: These are the JVMs and operating systems that the integration tests are run against: -| JVM | Versions | OS | -| ------------------------------------------------------------------------------------------ | --------- | ------------------------------ | -| [OpenJDK (Eclipse Temurin)](https://adoptium.net/) | 8, 11, 17 | [`ubuntu-latest`], [`windows-latest`] | -| [OpenJ9 (IBM Semeru Runtimes)](https://developer.ibm.com/languages/java/semeru-runtimes/) | 8, 11, 17 | [`ubuntu-latest`] | +| JVM | Versions | OS | +| ----------------------------------------------------------------------------------------- |-------------------| ------------------------------------- | +| [OpenJDK (Eclipse Temurin)](https://adoptium.net/) | 8, 11, 17, 21, 22 | [`ubuntu-latest`], [`windows-latest`] | +| [OpenJ9 (IBM Semeru Runtimes)](https://developer.ibm.com/languages/java/semeru-runtimes/) | 8, 11, 17, 21 | [`ubuntu-latest`] | ## Disabled instrumentations diff --git a/examples/distro/README.md b/examples/distro/README.md index 4ed7a2fe6975..781be60215ae 100644 --- a/examples/distro/README.md +++ b/examples/distro/README.md @@ -11,20 +11,20 @@ its usage. 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 +- `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` -* [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 +- [DemoIdGenerator](custom/src/main/java/com/example/javaagent/DemoIdGenerator.java) - custom `IdGenerator` +- [DemoPropagator](custom/src/main/java/com/example/javaagent/DemoPropagator.java) - custom `TextMapPropagator` +- [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 diff --git a/examples/distro/agent/build.gradle b/examples/distro/agent/build.gradle index dab66be0c884..3045dbaeca8a 100644 --- a/examples/distro/agent/build.gradle +++ b/examples/distro/agent/build.gradle @@ -1,7 +1,7 @@ import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar plugins { - id("com.github.johnrengelman.shadow") + id("com.gradleup.shadow") } apply from: "$rootDir/gradle/shadow.gradle" @@ -73,11 +73,9 @@ tasks { // exclude known bootstrap dependencies - they can't appear in the inst/ directory dependencies { exclude("io.opentelemetry:opentelemetry-api") - exclude("io.opentelemetry:opentelemetry-api-events") exclude("io.opentelemetry:opentelemetry-context") - exclude("io.opentelemetry:opentelemetry-semconv") - // metrics advice API - exclude("io.opentelemetry:opentelemetry-extension-incubator") + // events API and metrics advice API + exclude("io.opentelemetry:opentelemetry-api-incubator") } } @@ -89,7 +87,7 @@ tasks { dependsOn(tasks.relocateJavaagentLibs) with isolateClasses(tasks.relocateJavaagentLibs.outputs.files) - into("$buildDir/isolated/javaagentLibs") + into(layout.buildDirectory.dir("isolated/javaagentLibs")) } // 3. the relocated and isolated javaagent libs are merged together with the bootstrap libs (which undergo relocation diff --git a/examples/distro/build.gradle b/examples/distro/build.gradle index abb4423d7b36..6d97802b1ce6 100644 --- a/examples/distro/build.gradle +++ b/examples/distro/build.gradle @@ -12,9 +12,9 @@ buildscript { } } dependencies { - classpath "com.diffplug.spotless:spotless-plugin-gradle:6.19.0" - classpath "gradle.plugin.com.github.johnrengelman:shadow:8.0.0" - classpath "io.opentelemetry.instrumentation:gradle-plugins:1.28.0-alpha-SNAPSHOT" + classpath "com.diffplug.spotless:spotless-plugin-gradle:6.25.0" + classpath "com.gradleup.shadow:shadow-gradle-plugin:8.3.1" + classpath "io.opentelemetry.instrumentation:gradle-plugins:2.8.0-alpha-SNAPSHOT" } } @@ -27,19 +27,17 @@ subprojects { ext { versions = [ // this line is managed by .github/scripts/update-sdk-version.sh - opentelemetrySdk : "1.27.0", + opentelemetrySdk : "1.41.0", // these lines are managed by .github/scripts/update-version.sh - opentelemetryJavaagent : "1.28.0-SNAPSHOT", - opentelemetryJavaagentAlpha: "1.28.0-alpha-SNAPSHOT", + opentelemetryJavaagent : "2.8.0-SNAPSHOT", + opentelemetryJavaagentAlpha: "2.8.0-alpha-SNAPSHOT", - bytebuddy : "1.14.5", autoservice : "1.1.1", - junit : "5.9.3" + junit : "5.11.0" ] deps = [ - bytebuddy : "net.bytebuddy:byte-buddy-dep:${versions.bytebuddy}", autoservice: [ "com.google.auto.service:auto-service:${versions.autoservice}", "com.google.auto.service:auto-service-annotations:${versions.autoservice}", @@ -70,7 +68,7 @@ subprojects { implementation(platform("io.opentelemetry.instrumentation:opentelemetry-instrumentation-bom:${versions.opentelemetryJavaagent}")) implementation(platform("io.opentelemetry.instrumentation:opentelemetry-instrumentation-bom-alpha:${versions.opentelemetryJavaagentAlpha}")) - testImplementation("org.mockito:mockito-core:5.4.0") + testImplementation("org.mockito:mockito-core:5.13.0") testImplementation(enforcedPlatform("org.junit:junit-bom:${versions.junit}")) testImplementation("org.junit.jupiter:junit-jupiter-api:${versions.junit}") testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:${versions.junit}") diff --git a/examples/distro/gradle/instrumentation.gradle b/examples/distro/gradle/instrumentation.gradle index 9ae88443e4c7..ad484d8d2821 100644 --- a/examples/distro/gradle/instrumentation.gradle +++ b/examples/distro/gradle/instrumentation.gradle @@ -1,5 +1,5 @@ apply plugin: 'java' -apply plugin: 'com.github.johnrengelman.shadow' +apply plugin: 'com.gradleup.shadow' apply plugin: 'io.opentelemetry.instrumentation.muzzle-generation' apply plugin: 'io.opentelemetry.instrumentation.muzzle-check' @@ -17,7 +17,6 @@ dependencies { compileOnly("io.opentelemetry.instrumentation:opentelemetry-instrumentation-api") compileOnly("io.opentelemetry.javaagent:opentelemetry-javaagent-extension-api") - compileOnly deps.bytebuddy annotationProcessor deps.autoservice compileOnly deps.autoservice @@ -26,7 +25,7 @@ dependencies { // test dependencies testImplementation("io.opentelemetry.javaagent:opentelemetry-testing-common") testImplementation("io.opentelemetry:opentelemetry-sdk-testing") - testImplementation("org.assertj:assertj-core:3.19.0") + testImplementation("org.assertj:assertj-core:3.26.3") add("codegen", "io.opentelemetry.javaagent:opentelemetry-javaagent-tooling:${versions.opentelemetryJavaagentAlpha}") add("muzzleBootstrap", "io.opentelemetry.instrumentation:opentelemetry-instrumentation-annotations-support:${versions.opentelemetryJavaagentAlpha}") @@ -53,7 +52,6 @@ tasks.withType(Test).configureEach { 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" - jvmArgs "-Dotel.metrics.exporter=otlp" dependsOn shadowJar dependsOn configurations.testAgent.buildDependencies @@ -61,7 +59,7 @@ tasks.withType(Test).configureEach { // 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")) { + if (it == file(layout.buildDirectory.dir("resources/main")) || it == file(layout.buildDirectory.dir("classes/java/main"))) { return false } return true diff --git a/examples/distro/gradle/shadow.gradle b/examples/distro/gradle/shadow.gradle index 46c19a75c99b..4cbf01b16aee 100644 --- a/examples/distro/gradle/shadow.gradle +++ b/examples/distro/gradle/shadow.gradle @@ -13,11 +13,10 @@ ext.relocatePackages = { shadowJar -> 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") - shadowJar.relocate("io.opentelemetry.extension.incubator", "io.opentelemetry.javaagent.shaded.io.opentelemetry.extension.incubator") // 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.contrib.awsxray", "io.opentelemetry.javaagent.shaded.io.opentelemetry.contrib.awsxray") shadowJar.relocate("io.opentelemetry.extension.kotlin", "io.opentelemetry.javaagent.shaded.io.opentelemetry.extension.kotlin") } diff --git a/examples/distro/gradle/wrapper/gradle-wrapper.jar b/examples/distro/gradle/wrapper/gradle-wrapper.jar index ccebba7710deaf9f98673a68957ea02138b60d0a..a4b76b9530d66f5e68d973ea569d8e19de379189 100644 GIT binary patch literal 43583 zcma&N1CXTcmMvW9vTb(Rwr$&4wr$(C?dmSu>@vG-+vuvg^_??!{yS%8zW-#zn-LkA z5&1^$^{lnmUON?}LBF8_K|(?T0Ra(xUH{($5eN!MR#ZihR#HxkUPe+_R8Cn`RRs(P z_^*#_XlXmGv7!4;*Y%p4nw?{bNp@UZHv1?Um8r6)Fei3p@ClJn0ECfg1hkeuUU@Or zDaPa;U3fE=3L}DooL;8f;P0ipPt0Z~9P0)lbStMS)ag54=uL9ia-Lm3nh|@(Y?B`; zx_#arJIpXH!U{fbCbI^17}6Ri*H<>OLR%c|^mh8+)*h~K8Z!9)DPf zR2h?lbDZQ`p9P;&DQ4F0sur@TMa!Y}S8irn(%d-gi0*WxxCSk*A?3lGh=gcYN?FGl z7D=Js!i~0=u3rox^eO3i@$0=n{K1lPNU zwmfjRVmLOCRfe=seV&P*1Iq=^i`502keY8Uy-WNPwVNNtJFx?IwAyRPZo2Wo1+S(xF37LJZ~%i)kpFQ3Fw=mXfd@>%+)RpYQLnr}B~~zoof(JVm^^&f zxKV^+3D3$A1G;qh4gPVjhrC8e(VYUHv#dy^)(RoUFM?o%W-EHxufuWf(l*@-l+7vt z=l`qmR56K~F|v<^Pd*p~1_y^P0P^aPC##d8+HqX4IR1gu+7w#~TBFphJxF)T$2WEa zxa?H&6=Qe7d(#tha?_1uQys2KtHQ{)Qco)qwGjrdNL7thd^G5i8Os)CHqc>iOidS} z%nFEDdm=GXBw=yXe1W-ShHHFb?Cc70+$W~z_+}nAoHFYI1MV1wZegw*0y^tC*s%3h zhD3tN8b=Gv&rj}!SUM6|ajSPp*58KR7MPpI{oAJCtY~JECm)*m_x>AZEu>DFgUcby z1Qaw8lU4jZpQ_$;*7RME+gq1KySGG#Wql>aL~k9tLrSO()LWn*q&YxHEuzmwd1?aAtI zBJ>P=&$=l1efe1CDU;`Fd+_;&wI07?V0aAIgc(!{a z0Jg6Y=inXc3^n!U0Atk`iCFIQooHqcWhO(qrieUOW8X(x?(RD}iYDLMjSwffH2~tB z)oDgNBLB^AJBM1M^c5HdRx6fBfka`(LD-qrlh5jqH~);#nw|iyp)()xVYak3;Ybik z0j`(+69aK*B>)e_p%=wu8XC&9e{AO4c~O1U`5X9}?0mrd*m$_EUek{R?DNSh(=br# z#Q61gBzEpmy`$pA*6!87 zSDD+=@fTY7<4A?GLqpA?Pb2z$pbCc4B4zL{BeZ?F-8`s$?>*lXXtn*NC61>|*w7J* z$?!iB{6R-0=KFmyp1nnEmLsA-H0a6l+1uaH^g%c(p{iT&YFrbQ$&PRb8Up#X3@Zsk zD^^&LK~111%cqlP%!_gFNa^dTYT?rhkGl}5=fL{a`UViaXWI$k-UcHJwmaH1s=S$4 z%4)PdWJX;hh5UoK?6aWoyLxX&NhNRqKam7tcOkLh{%j3K^4Mgx1@i|Pi&}<^5>hs5 zm8?uOS>%)NzT(%PjVPGa?X%`N2TQCKbeH2l;cTnHiHppPSJ<7y-yEIiC!P*ikl&!B z%+?>VttCOQM@ShFguHVjxX^?mHX^hSaO_;pnyh^v9EumqSZTi+#f&_Vaija0Q-e*| z7ulQj6Fs*bbmsWp{`auM04gGwsYYdNNZcg|ph0OgD>7O}Asn7^Z=eI>`$2*v78;sj-}oMoEj&@)9+ycEOo92xSyY344^ z11Hb8^kdOvbf^GNAK++bYioknrpdN>+u8R?JxG=!2Kd9r=YWCOJYXYuM0cOq^FhEd zBg2puKy__7VT3-r*dG4c62Wgxi52EMCQ`bKgf*#*ou(D4-ZN$+mg&7$u!! z-^+Z%;-3IDwqZ|K=ah85OLwkO zKxNBh+4QHh)u9D?MFtpbl)us}9+V!D%w9jfAMYEb>%$A;u)rrI zuBudh;5PN}_6J_}l55P3l_)&RMlH{m!)ai-i$g)&*M`eN$XQMw{v^r@-125^RRCF0 z^2>|DxhQw(mtNEI2Kj(;KblC7x=JlK$@78`O~>V!`|1Lm-^JR$-5pUANAnb(5}B}JGjBsliK4& zk6y(;$e&h)lh2)L=bvZKbvh@>vLlreBdH8No2>$#%_Wp1U0N7Ank!6$dFSi#xzh|( zRi{Uw%-4W!{IXZ)fWx@XX6;&(m_F%c6~X8hx=BN1&q}*( zoaNjWabE{oUPb!Bt$eyd#$5j9rItB-h*5JiNi(v^e|XKAj*8(k<5-2$&ZBR5fF|JA z9&m4fbzNQnAU}r8ab>fFV%J0z5awe#UZ|bz?Ur)U9bCIKWEzi2%A+5CLqh?}K4JHi z4vtM;+uPsVz{Lfr;78W78gC;z*yTch~4YkLr&m-7%-xc ztw6Mh2d>_iO*$Rd8(-Cr1_V8EO1f*^@wRoSozS) zy1UoC@pruAaC8Z_7~_w4Q6n*&B0AjOmMWa;sIav&gu z|J5&|{=a@vR!~k-OjKEgPFCzcJ>#A1uL&7xTDn;{XBdeM}V=l3B8fE1--DHjSaxoSjNKEM9|U9#m2<3>n{Iuo`r3UZp;>GkT2YBNAh|b z^jTq-hJp(ebZh#Lk8hVBP%qXwv-@vbvoREX$TqRGTgEi$%_F9tZES@z8Bx}$#5eeG zk^UsLBH{bc2VBW)*EdS({yw=?qmevwi?BL6*=12k9zM5gJv1>y#ML4!)iiPzVaH9% zgSImetD@dam~e>{LvVh!phhzpW+iFvWpGT#CVE5TQ40n%F|p(sP5mXxna+Ev7PDwA zamaV4m*^~*xV+&p;W749xhb_X=$|LD;FHuB&JL5?*Y2-oIT(wYY2;73<^#46S~Gx| z^cez%V7x$81}UWqS13Gz80379Rj;6~WdiXWOSsdmzY39L;Hg3MH43o*y8ibNBBH`(av4|u;YPq%{R;IuYow<+GEsf@R?=@tT@!}?#>zIIn0CoyV!hq3mw zHj>OOjfJM3F{RG#6ujzo?y32m^tgSXf@v=J$ELdJ+=5j|=F-~hP$G&}tDZsZE?5rX ztGj`!S>)CFmdkccxM9eGIcGnS2AfK#gXwj%esuIBNJQP1WV~b~+D7PJTmWGTSDrR` zEAu4B8l>NPuhsk5a`rReSya2nfV1EK01+G!x8aBdTs3Io$u5!6n6KX%uv@DxAp3F@{4UYg4SWJtQ-W~0MDb|j-$lwVn znAm*Pl!?Ps&3wO=R115RWKb*JKoexo*)uhhHBncEDMSVa_PyA>k{Zm2(wMQ(5NM3# z)jkza|GoWEQo4^s*wE(gHz?Xsg4`}HUAcs42cM1-qq_=+=!Gk^y710j=66(cSWqUe zklbm8+zB_syQv5A2rj!Vbw8;|$@C!vfNmNV!yJIWDQ>{+2x zKjuFX`~~HKG~^6h5FntRpnnHt=D&rq0>IJ9#F0eM)Y-)GpRjiN7gkA8wvnG#K=q{q z9dBn8_~wm4J<3J_vl|9H{7q6u2A!cW{bp#r*-f{gOV^e=8S{nc1DxMHFwuM$;aVI^ zz6A*}m8N-&x8;aunp1w7_vtB*pa+OYBw=TMc6QK=mbA-|Cf* zvyh8D4LRJImooUaSb7t*fVfih<97Gf@VE0|z>NcBwBQze);Rh!k3K_sfunToZY;f2 z^HmC4KjHRVg+eKYj;PRN^|E0>Gj_zagfRbrki68I^#~6-HaHg3BUW%+clM1xQEdPYt_g<2K+z!$>*$9nQ>; zf9Bei{?zY^-e{q_*|W#2rJG`2fy@{%6u0i_VEWTq$*(ZN37|8lFFFt)nCG({r!q#9 z5VK_kkSJ3?zOH)OezMT{!YkCuSSn!K#-Rhl$uUM(bq*jY? zi1xbMVthJ`E>d>(f3)~fozjg^@eheMF6<)I`oeJYx4*+M&%c9VArn(OM-wp%M<-`x z7sLP1&3^%Nld9Dhm@$3f2}87!quhI@nwd@3~fZl_3LYW-B?Ia>ui`ELg z&Qfe!7m6ze=mZ`Ia9$z|ARSw|IdMpooY4YiPN8K z4B(ts3p%2i(Td=tgEHX z0UQ_>URBtG+-?0E;E7Ld^dyZ;jjw0}XZ(}-QzC6+NN=40oDb2^v!L1g9xRvE#@IBR zO!b-2N7wVfLV;mhEaXQ9XAU+>=XVA6f&T4Z-@AX!leJ8obP^P^wP0aICND?~w&NykJ#54x3_@r7IDMdRNy4Hh;h*!u(Ol(#0bJdwEo$5437-UBjQ+j=Ic>Q2z` zJNDf0yO6@mr6y1#n3)s(W|$iE_i8r@Gd@!DWDqZ7J&~gAm1#~maIGJ1sls^gxL9LLG_NhU!pTGty!TbhzQnu)I*S^54U6Yu%ZeCg`R>Q zhBv$n5j0v%O_j{QYWG!R9W?5_b&67KB$t}&e2LdMvd(PxN6Ir!H4>PNlerpBL>Zvyy!yw z-SOo8caEpDt(}|gKPBd$qND5#a5nju^O>V&;f890?yEOfkSG^HQVmEbM3Ugzu+UtH zC(INPDdraBN?P%kE;*Ae%Wto&sgw(crfZ#Qy(<4nk;S|hD3j{IQRI6Yq|f^basLY; z-HB&Je%Gg}Jt@={_C{L$!RM;$$|iD6vu#3w?v?*;&()uB|I-XqEKqZPS!reW9JkLewLb!70T7n`i!gNtb1%vN- zySZj{8-1>6E%H&=V}LM#xmt`J3XQoaD|@XygXjdZ1+P77-=;=eYpoEQ01B@L*a(uW zrZeZz?HJsw_4g0vhUgkg@VF8<-X$B8pOqCuWAl28uB|@r`19DTUQQsb^pfqB6QtiT z*`_UZ`fT}vtUY#%sq2{rchyfu*pCg;uec2$-$N_xgjZcoumE5vSI{+s@iLWoz^Mf; zuI8kDP{!XY6OP~q5}%1&L}CtfH^N<3o4L@J@zg1-mt{9L`s^z$Vgb|mr{@WiwAqKg zp#t-lhrU>F8o0s1q_9y`gQNf~Vb!F%70f}$>i7o4ho$`uciNf=xgJ>&!gSt0g;M>*x4-`U)ysFW&Vs^Vk6m%?iuWU+o&m(2Jm26Y(3%TL; zA7T)BP{WS!&xmxNw%J=$MPfn(9*^*TV;$JwRy8Zl*yUZi8jWYF>==j~&S|Xinsb%c z2?B+kpet*muEW7@AzjBA^wAJBY8i|#C{WtO_or&Nj2{=6JTTX05}|H>N2B|Wf!*3_ z7hW*j6p3TvpghEc6-wufFiY!%-GvOx*bZrhZu+7?iSrZL5q9}igiF^*R3%DE4aCHZ zqu>xS8LkW+Auv%z-<1Xs92u23R$nk@Pk}MU5!gT|c7vGlEA%G^2th&Q*zfg%-D^=f z&J_}jskj|Q;73NP4<4k*Y%pXPU2Thoqr+5uH1yEYM|VtBPW6lXaetokD0u z9qVek6Q&wk)tFbQ8(^HGf3Wp16gKmr>G;#G(HRBx?F`9AIRboK+;OfHaLJ(P>IP0w zyTbTkx_THEOs%Q&aPrxbZrJlio+hCC_HK<4%f3ZoSAyG7Dn`=X=&h@m*|UYO-4Hq0 z-Bq&+Ie!S##4A6OGoC~>ZW`Y5J)*ouaFl_e9GA*VSL!O_@xGiBw!AF}1{tB)z(w%c zS1Hmrb9OC8>0a_$BzeiN?rkPLc9%&;1CZW*4}CDDNr2gcl_3z+WC15&H1Zc2{o~i) z)LLW=WQ{?ricmC`G1GfJ0Yp4Dy~Ba;j6ZV4r{8xRs`13{dD!xXmr^Aga|C=iSmor% z8hi|pTXH)5Yf&v~exp3o+sY4B^^b*eYkkCYl*T{*=-0HniSA_1F53eCb{x~1k3*`W zr~};p1A`k{1DV9=UPnLDgz{aJH=-LQo<5%+Em!DNN252xwIf*wF_zS^!(XSm(9eoj z=*dXG&n0>)_)N5oc6v!>-bd(2ragD8O=M|wGW z!xJQS<)u70m&6OmrF0WSsr@I%T*c#Qo#Ha4d3COcX+9}hM5!7JIGF>7<~C(Ear^Sn zm^ZFkV6~Ula6+8S?oOROOA6$C&q&dp`>oR-2Ym3(HT@O7Sd5c~+kjrmM)YmgPH*tL zX+znN>`tv;5eOfX?h{AuX^LK~V#gPCu=)Tigtq9&?7Xh$qN|%A$?V*v=&-2F$zTUv z`C#WyIrChS5|Kgm_GeudCFf;)!WH7FI60j^0o#65o6`w*S7R@)88n$1nrgU(oU0M9 zx+EuMkC>(4j1;m6NoGqEkpJYJ?vc|B zOlwT3t&UgL!pX_P*6g36`ZXQ; z9~Cv}ANFnJGp(;ZhS(@FT;3e)0)Kp;h^x;$*xZn*k0U6-&FwI=uOGaODdrsp-!K$Ac32^c{+FhI-HkYd5v=`PGsg%6I`4d9Jy)uW0y%) zm&j^9WBAp*P8#kGJUhB!L?a%h$hJgQrx!6KCB_TRo%9{t0J7KW8!o1B!NC)VGLM5! zpZy5Jc{`r{1e(jd%jsG7k%I+m#CGS*BPA65ZVW~fLYw0dA-H_}O zrkGFL&P1PG9p2(%QiEWm6x;U-U&I#;Em$nx-_I^wtgw3xUPVVu zqSuKnx&dIT-XT+T10p;yjo1Y)z(x1fb8Dzfn8e yu?e%!_ptzGB|8GrCfu%p?(_ zQccdaaVK$5bz;*rnyK{_SQYM>;aES6Qs^lj9lEs6_J+%nIiuQC*fN;z8md>r_~Mfl zU%p5Dt_YT>gQqfr@`cR!$NWr~+`CZb%dn;WtzrAOI>P_JtsB76PYe*<%H(y>qx-`Kq!X_; z<{RpAqYhE=L1r*M)gNF3B8r(<%8mo*SR2hu zccLRZwGARt)Hlo1euqTyM>^!HK*!Q2P;4UYrysje@;(<|$&%vQekbn|0Ruu_Io(w4#%p6ld2Yp7tlA`Y$cciThP zKzNGIMPXX%&Ud0uQh!uQZz|FB`4KGD?3!ND?wQt6!n*f4EmCoJUh&b?;B{|lxs#F- z31~HQ`SF4x$&v00@(P+j1pAaj5!s`)b2RDBp*PB=2IB>oBF!*6vwr7Dp%zpAx*dPr zb@Zjq^XjN?O4QcZ*O+8>)|HlrR>oD*?WQl5ri3R#2?*W6iJ>>kH%KnnME&TT@ZzrHS$Q%LC?n|e>V+D+8D zYc4)QddFz7I8#}y#Wj6>4P%34dZH~OUDb?uP%-E zwjXM(?Sg~1!|wI(RVuxbu)-rH+O=igSho_pDCw(c6b=P zKk4ATlB?bj9+HHlh<_!&z0rx13K3ZrAR8W)!@Y}o`?a*JJsD+twZIv`W)@Y?Amu_u zz``@-e2X}27$i(2=9rvIu5uTUOVhzwu%mNazS|lZb&PT;XE2|B&W1>=B58#*!~D&) zfVmJGg8UdP*fx(>Cj^?yS^zH#o-$Q-*$SnK(ZVFkw+er=>N^7!)FtP3y~Xxnu^nzY zikgB>Nj0%;WOltWIob|}%lo?_C7<``a5hEkx&1ku$|)i>Rh6@3h*`slY=9U}(Ql_< zaNG*J8vb&@zpdhAvv`?{=zDedJ23TD&Zg__snRAH4eh~^oawdYi6A3w8<Ozh@Kw)#bdktM^GVb zrG08?0bG?|NG+w^&JvD*7LAbjED{_Zkc`3H!My>0u5Q}m!+6VokMLXxl`Mkd=g&Xx z-a>m*#G3SLlhbKB!)tnzfWOBV;u;ftU}S!NdD5+YtOjLg?X}dl>7m^gOpihrf1;PY zvll&>dIuUGs{Qnd- zwIR3oIrct8Va^Tm0t#(bJD7c$Z7DO9*7NnRZorrSm`b`cxz>OIC;jSE3DO8`hX955ui`s%||YQtt2 z5DNA&pG-V+4oI2s*x^>-$6J?p=I>C|9wZF8z;VjR??Icg?1w2v5Me+FgAeGGa8(3S z4vg*$>zC-WIVZtJ7}o9{D-7d>zCe|z#<9>CFve-OPAYsneTb^JH!Enaza#j}^mXy1 z+ULn^10+rWLF6j2>Ya@@Kq?26>AqK{A_| zQKb*~F1>sE*=d?A?W7N2j?L09_7n+HGi{VY;MoTGr_)G9)ot$p!-UY5zZ2Xtbm=t z@dpPSGwgH=QtIcEulQNI>S-#ifbnO5EWkI;$A|pxJd885oM+ zGZ0_0gDvG8q2xebj+fbCHYfAXuZStH2j~|d^sBAzo46(K8n59+T6rzBwK)^rfPT+B zyIFw)9YC-V^rhtK`!3jrhmW-sTmM+tPH+;nwjL#-SjQPUZ53L@A>y*rt(#M(qsiB2 zx6B)dI}6Wlsw%bJ8h|(lhkJVogQZA&n{?Vgs6gNSXzuZpEyu*xySy8ro07QZ7Vk1!3tJphN_5V7qOiyK8p z#@jcDD8nmtYi1^l8ml;AF<#IPK?!pqf9D4moYk>d99Im}Jtwj6c#+A;f)CQ*f-hZ< z=p_T86jog%!p)D&5g9taSwYi&eP z#JuEK%+NULWus;0w32-SYFku#i}d~+{Pkho&^{;RxzP&0!RCm3-9K6`>KZpnzS6?L z^H^V*s!8<>x8bomvD%rh>Zp3>Db%kyin;qtl+jAv8Oo~1g~mqGAC&Qi_wy|xEt2iz zWAJEfTV%cl2Cs<1L&DLRVVH05EDq`pH7Oh7sR`NNkL%wi}8n>IXcO40hp+J+sC!W?!krJf!GJNE8uj zg-y~Ns-<~D?yqbzVRB}G>0A^f0!^N7l=$m0OdZuqAOQqLc zX?AEGr1Ht+inZ-Qiwnl@Z0qukd__a!C*CKuGdy5#nD7VUBM^6OCpxCa2A(X;e0&V4 zM&WR8+wErQ7UIc6LY~Q9x%Sn*Tn>>P`^t&idaOEnOd(Ufw#>NoR^1QdhJ8s`h^|R_ zXX`c5*O~Xdvh%q;7L!_!ohf$NfEBmCde|#uVZvEo>OfEq%+Ns7&_f$OR9xsihRpBb z+cjk8LyDm@U{YN>+r46?nn{7Gh(;WhFw6GAxtcKD+YWV?uge>;+q#Xx4!GpRkVZYu zzsF}1)7$?%s9g9CH=Zs+B%M_)+~*j3L0&Q9u7!|+T`^O{xE6qvAP?XWv9_MrZKdo& z%IyU)$Q95AB4!#hT!_dA>4e@zjOBD*Y=XjtMm)V|+IXzjuM;(l+8aA5#Kaz_$rR6! zj>#&^DidYD$nUY(D$mH`9eb|dtV0b{S>H6FBfq>t5`;OxA4Nn{J(+XihF(stSche7$es&~N$epi&PDM_N`As;*9D^L==2Q7Z2zD+CiU(|+-kL*VG+&9!Yb3LgPy?A zm7Z&^qRG_JIxK7-FBzZI3Q<;{`DIxtc48k> zc|0dmX;Z=W$+)qE)~`yn6MdoJ4co;%!`ddy+FV538Y)j(vg}5*k(WK)KWZ3WaOG!8 z!syGn=s{H$odtpqFrT#JGM*utN7B((abXnpDM6w56nhw}OY}0TiTG1#f*VFZr+^-g zbP10`$LPq_;PvrA1XXlyx2uM^mrjTzX}w{yuLo-cOClE8MMk47T25G8M!9Z5ypOSV zAJUBGEg5L2fY)ZGJb^E34R2zJ?}Vf>{~gB!8=5Z) z9y$>5c)=;o0HeHHSuE4U)#vG&KF|I%-cF6f$~pdYJWk_dD}iOA>iA$O$+4%@>JU08 zS`ep)$XLPJ+n0_i@PkF#ri6T8?ZeAot$6JIYHm&P6EB=BiaNY|aA$W0I+nz*zkz_z zkEru!tj!QUffq%)8y0y`T&`fuus-1p>=^hnBiBqD^hXrPs`PY9tU3m0np~rISY09> z`P3s=-kt_cYcxWd{de@}TwSqg*xVhp;E9zCsnXo6z z?f&Sv^U7n4`xr=mXle94HzOdN!2kB~4=%)u&N!+2;z6UYKUDqi-s6AZ!haB;@&B`? z_TRX0%@suz^TRdCb?!vNJYPY8L_}&07uySH9%W^Tc&1pia6y1q#?*Drf}GjGbPjBS zbOPcUY#*$3sL2x4v_i*Y=N7E$mR}J%|GUI(>WEr+28+V z%v5{#e!UF*6~G&%;l*q*$V?&r$Pp^sE^i-0$+RH3ERUUdQ0>rAq2(2QAbG}$y{de( z>{qD~GGuOk559Y@%$?N^1ApVL_a704>8OD%8Y%8B;FCt%AoPu8*D1 zLB5X>b}Syz81pn;xnB}%0FnwazlWfUV)Z-~rZg6~b z6!9J$EcE&sEbzcy?CI~=boWA&eeIa%z(7SE^qgVLz??1Vbc1*aRvc%Mri)AJaAG!p z$X!_9Ds;Zz)f+;%s&dRcJt2==P{^j3bf0M=nJd&xwUGlUFn?H=2W(*2I2Gdu zv!gYCwM10aeus)`RIZSrCK=&oKaO_Ry~D1B5!y0R=%!i2*KfXGYX&gNv_u+n9wiR5 z*e$Zjju&ODRW3phN925%S(jL+bCHv6rZtc?!*`1TyYXT6%Ju=|X;6D@lq$8T zW{Y|e39ioPez(pBH%k)HzFITXHvnD6hw^lIoUMA;qAJ^CU?top1fo@s7xT13Fvn1H z6JWa-6+FJF#x>~+A;D~;VDs26>^oH0EI`IYT2iagy23?nyJ==i{g4%HrAf1-*v zK1)~@&(KkwR7TL}L(A@C_S0G;-GMDy=MJn2$FP5s<%wC)4jC5PXoxrQBFZ_k0P{{s@sz+gX`-!=T8rcB(=7vW}^K6oLWMmp(rwDh}b zwaGGd>yEy6fHv%jM$yJXo5oMAQ>c9j`**}F?MCry;T@47@r?&sKHgVe$MCqk#Z_3S z1GZI~nOEN*P~+UaFGnj{{Jo@16`(qVNtbU>O0Hf57-P>x8Jikp=`s8xWs^dAJ9lCQ z)GFm+=OV%AMVqVATtN@|vp61VVAHRn87}%PC^RAzJ%JngmZTasWBAWsoAqBU+8L8u z4A&Pe?fmTm0?mK-BL9t+{y7o(7jm+RpOhL9KnY#E&qu^}B6=K_dB}*VlSEiC9fn)+V=J;OnN)Ta5v66ic1rG+dGAJ1 z1%Zb_+!$=tQ~lxQrzv3x#CPb?CekEkA}0MYSgx$Jdd}q8+R=ma$|&1a#)TQ=l$1tQ z=tL9&_^vJ)Pk}EDO-va`UCT1m#Uty1{v^A3P~83_#v^ozH}6*9mIjIr;t3Uv%@VeW zGL6(CwCUp)Jq%G0bIG%?{_*Y#5IHf*5M@wPo6A{$Um++Co$wLC=J1aoG93&T7Ho}P z=mGEPP7GbvoG!uD$k(H3A$Z))+i{Hy?QHdk>3xSBXR0j!11O^mEe9RHmw!pvzv?Ua~2_l2Yh~_!s1qS`|0~0)YsbHSz8!mG)WiJE| z2f($6TQtt6L_f~ApQYQKSb=`053LgrQq7G@98#igV>y#i==-nEjQ!XNu9 z~;mE+gtj4IDDNQJ~JVk5Ux6&LCSFL!y=>79kE9=V}J7tD==Ga+IW zX)r7>VZ9dY=V&}DR))xUoV!u(Z|%3ciQi_2jl}3=$Agc(`RPb z8kEBpvY>1FGQ9W$n>Cq=DIpski};nE)`p3IUw1Oz0|wxll^)4dq3;CCY@RyJgFgc# zKouFh!`?Xuo{IMz^xi-h=StCis_M7yq$u) z?XHvw*HP0VgR+KR6wI)jEMX|ssqYvSf*_3W8zVTQzD?3>H!#>InzpSO)@SC8q*ii- z%%h}_#0{4JG;Jm`4zg};BPTGkYamx$Xo#O~lBirRY)q=5M45n{GCfV7h9qwyu1NxOMoP4)jjZMxmT|IQQh0U7C$EbnMN<3)Kk?fFHYq$d|ICu>KbY_hO zTZM+uKHe(cIZfEqyzyYSUBZa8;Fcut-GN!HSA9ius`ltNebF46ZX_BbZNU}}ZOm{M2&nANL9@0qvih15(|`S~z}m&h!u4x~(%MAO$jHRWNfuxWF#B)E&g3ghSQ9|> z(MFaLQj)NE0lowyjvg8z0#m6FIuKE9lDO~Glg}nSb7`~^&#(Lw{}GVOS>U)m8bF}x zVjbXljBm34Cs-yM6TVusr+3kYFjr28STT3g056y3cH5Tmge~ASxBj z%|yb>$eF;WgrcOZf569sDZOVwoo%8>XO>XQOX1OyN9I-SQgrm;U;+#3OI(zrWyow3 zk==|{lt2xrQ%FIXOTejR>;wv(Pb8u8}BUpx?yd(Abh6? zsoO3VYWkeLnF43&@*#MQ9-i-d0t*xN-UEyNKeyNMHw|A(k(_6QKO=nKMCxD(W(Yop zsRQ)QeL4X3Lxp^L%wzi2-WVSsf61dqliPUM7srDB?Wm6Lzn0&{*}|IsKQW;02(Y&| zaTKv|`U(pSzuvR6Rduu$wzK_W-Y-7>7s?G$)U}&uK;<>vU}^^ns@Z!p+9?St1s)dG zK%y6xkPyyS1$~&6v{kl?Md6gwM|>mt6Upm>oa8RLD^8T{0?HC!Z>;(Bob7el(DV6x zi`I)$&E&ngwFS@bi4^xFLAn`=fzTC;aimE^!cMI2n@Vo%Ae-ne`RF((&5y6xsjjAZ zVguVoQ?Z9uk$2ON;ersE%PU*xGO@T*;j1BO5#TuZKEf(mB7|g7pcEA=nYJ{s3vlbg zd4-DUlD{*6o%Gc^N!Nptgay>j6E5;3psI+C3Q!1ZIbeCubW%w4pq9)MSDyB{HLm|k zxv-{$$A*pS@csolri$Ge<4VZ}e~78JOL-EVyrbxKra^d{?|NnPp86!q>t<&IP07?Z z^>~IK^k#OEKgRH+LjllZXk7iA>2cfH6+(e&9ku5poo~6y{GC5>(bRK7hwjiurqAiZ zg*DmtgY}v83IjE&AbiWgMyFbaRUPZ{lYiz$U^&Zt2YjG<%m((&_JUbZcfJ22(>bi5 z!J?<7AySj0JZ&<-qXX;mcV!f~>G=sB0KnjWca4}vrtunD^1TrpfeS^4dvFr!65knK zZh`d;*VOkPs4*-9kL>$GP0`(M!j~B;#x?Ba~&s6CopvO86oM?-? zOw#dIRc;6A6T?B`Qp%^<U5 z19x(ywSH$_N+Io!6;e?`tWaM$`=Db!gzx|lQ${DG!zb1Zl&|{kX0y6xvO1o z220r<-oaS^^R2pEyY;=Qllqpmue|5yI~D|iI!IGt@iod{Opz@*ml^w2bNs)p`M(Io z|E;;m*Xpjd9l)4G#KaWfV(t8YUn@A;nK^#xgv=LtnArX|vWQVuw3}B${h+frU2>9^ z!l6)!Uo4`5k`<<;E(ido7M6lKTgWezNLq>U*=uz&s=cc$1%>VrAeOoUtA|T6gO4>UNqsdK=NF*8|~*sl&wI=x9-EGiq*aqV!(VVXA57 zw9*o6Ir8Lj1npUXvlevtn(_+^X5rzdR>#(}4YcB9O50q97%rW2me5_L=%ffYPUSRc z!vv?Kv>dH994Qi>U(a<0KF6NH5b16enCp+mw^Hb3Xs1^tThFpz!3QuN#}KBbww`(h z7GO)1olDqy6?T$()R7y%NYx*B0k_2IBiZ14&8|JPFxeMF{vW>HF-Vi3+ZOI=+qP}n zw(+!WcTd~4ZJX1!ZM&y!+uyt=&i!+~d(V%GjH;-NsEEv6nS1TERt|RHh!0>W4+4pp z1-*EzAM~i`+1f(VEHI8So`S`akPfPTfq*`l{Fz`hS%k#JS0cjT2mS0#QLGf=J?1`he3W*;m4)ce8*WFq1sdP=~$5RlH1EdWm|~dCvKOi4*I_96{^95p#B<(n!d?B z=o`0{t+&OMwKcxiBECznJcfH!fL(z3OvmxP#oWd48|mMjpE||zdiTBdWelj8&Qosv zZFp@&UgXuvJw5y=q6*28AtxZzo-UUpkRW%ne+Ylf!V-0+uQXBW=5S1o#6LXNtY5!I z%Rkz#(S8Pjz*P7bqB6L|M#Er{|QLae-Y{KA>`^} z@lPjeX>90X|34S-7}ZVXe{wEei1<{*e8T-Nbj8JmD4iwcE+Hg_zhkPVm#=@b$;)h6 z<<6y`nPa`f3I6`!28d@kdM{uJOgM%`EvlQ5B2bL)Sl=|y@YB3KeOzz=9cUW3clPAU z^sYc}xf9{4Oj?L5MOlYxR{+>w=vJjvbyO5}ptT(o6dR|ygO$)nVCvNGnq(6;bHlBd zl?w-|plD8spjDF03g5ip;W3Z z><0{BCq!Dw;h5~#1BuQilq*TwEu)qy50@+BE4bX28+7erX{BD4H)N+7U`AVEuREE8 z;X?~fyhF-x_sRfHIj~6f(+^@H)D=ngP;mwJjxhQUbUdzk8f94Ab%59-eRIq?ZKrwD z(BFI=)xrUlgu(b|hAysqK<}8bslmNNeD=#JW*}^~Nrswn^xw*nL@Tx!49bfJecV&KC2G4q5a!NSv)06A_5N3Y?veAz;Gv+@U3R% z)~UA8-0LvVE{}8LVDOHzp~2twReqf}ODIyXMM6=W>kL|OHcx9P%+aJGYi_Om)b!xe zF40Vntn0+VP>o<$AtP&JANjXBn7$}C@{+@3I@cqlwR2MdwGhVPxlTIcRVu@Ho-wO` z_~Or~IMG)A_`6-p)KPS@cT9mu9RGA>dVh5wY$NM9-^c@N=hcNaw4ITjm;iWSP^ZX| z)_XpaI61<+La+U&&%2a z0za$)-wZP@mwSELo#3!PGTt$uy0C(nTT@9NX*r3Ctw6J~7A(m#8fE)0RBd`TdKfAT zCf@$MAxjP`O(u9s@c0Fd@|}UQ6qp)O5Q5DPCeE6mSIh|Rj{$cAVIWsA=xPKVKxdhg zLzPZ`3CS+KIO;T}0Ip!fAUaNU>++ZJZRk@I(h<)RsJUhZ&Ru9*!4Ptn;gX^~4E8W^TSR&~3BAZc#HquXn)OW|TJ`CTahk+{qe`5+ixON^zA9IFd8)kc%*!AiLu z>`SFoZ5bW-%7}xZ>gpJcx_hpF$2l+533{gW{a7ce^B9sIdmLrI0)4yivZ^(Vh@-1q zFT!NQK$Iz^xu%|EOK=n>ug;(7J4OnS$;yWmq>A;hsD_0oAbLYhW^1Vdt9>;(JIYjf zdb+&f&D4@4AS?!*XpH>8egQvSVX`36jMd>$+RgI|pEg))^djhGSo&#lhS~9%NuWfX zDDH;3T*GzRT@5=7ibO>N-6_XPBYxno@mD_3I#rDD?iADxX`! zh*v8^i*JEMzyN#bGEBz7;UYXki*Xr(9xXax(_1qVW=Ml)kSuvK$coq2A(5ZGhs_pF z$*w}FbN6+QDseuB9=fdp_MTs)nQf!2SlROQ!gBJBCXD&@-VurqHj0wm@LWX-TDmS= z71M__vAok|@!qgi#H&H%Vg-((ZfxPAL8AI{x|VV!9)ZE}_l>iWk8UPTGHs*?u7RfP z5MC&=c6X;XlUzrz5q?(!eO@~* zoh2I*%J7dF!!_!vXoSIn5o|wj1#_>K*&CIn{qSaRc&iFVxt*^20ngCL;QonIS>I5^ zMw8HXm>W0PGd*}Ko)f|~dDd%;Wu_RWI_d;&2g6R3S63Uzjd7dn%Svu-OKpx*o|N>F zZg=-~qLb~VRLpv`k zWSdfHh@?dp=s_X`{yxOlxE$4iuyS;Z-x!*E6eqmEm*j2bE@=ZI0YZ5%Yj29!5+J$4h{s($nakA`xgbO8w zi=*r}PWz#lTL_DSAu1?f%-2OjD}NHXp4pXOsCW;DS@BC3h-q4_l`<))8WgzkdXg3! zs1WMt32kS2E#L0p_|x+x**TFV=gn`m9BWlzF{b%6j-odf4{7a4y4Uaef@YaeuPhU8 zHBvRqN^;$Jizy+ z=zW{E5<>2gp$pH{M@S*!sJVQU)b*J5*bX4h>5VJve#Q6ga}cQ&iL#=(u+KroWrxa%8&~p{WEUF0il=db;-$=A;&9M{Rq`ouZ5m%BHT6%st%saGsD6)fQgLN}x@d3q>FC;=f%O3Cyg=Ke@Gh`XW za@RajqOE9UB6eE=zhG%|dYS)IW)&y&Id2n7r)6p_)vlRP7NJL(x4UbhlcFXWT8?K=%s7;z?Vjts?y2+r|uk8Wt(DM*73^W%pAkZa1Jd zNoE)8FvQA>Z`eR5Z@Ig6kS5?0h;`Y&OL2D&xnnAUzQz{YSdh0k zB3exx%A2TyI)M*EM6htrxSlep!Kk(P(VP`$p0G~f$smld6W1r_Z+o?=IB@^weq>5VYsYZZR@` z&XJFxd5{|KPZmVOSxc@^%71C@;z}}WhbF9p!%yLj3j%YOlPL5s>7I3vj25 z@xmf=*z%Wb4;Va6SDk9cv|r*lhZ`(y_*M@>q;wrn)oQx%B(2A$9(74>;$zmQ!4fN; z>XurIk-7@wZys<+7XL@0Fhe-f%*=(weaQEdR9Eh6>Kl-EcI({qoZqyzziGwpg-GM#251sK_ z=3|kitS!j%;fpc@oWn65SEL73^N&t>Ix37xgs= zYG%eQDJc|rqHFia0!_sm7`@lvcv)gfy(+KXA@E{3t1DaZ$DijWAcA)E0@X?2ziJ{v z&KOYZ|DdkM{}t+@{@*6ge}m%xfjIxi%qh`=^2Rwz@w0cCvZ&Tc#UmCDbVwABrON^x zEBK43FO@weA8s7zggCOWhMvGGE`baZ62cC)VHyy!5Zbt%ieH+XN|OLbAFPZWyC6)p z4P3%8sq9HdS3=ih^0OOlqTPbKuzQ?lBEI{w^ReUO{V?@`ARsL|S*%yOS=Z%sF)>-y z(LAQdhgAcuF6LQjRYfdbD1g4o%tV4EiK&ElLB&^VZHbrV1K>tHTO{#XTo>)2UMm`2 z^t4s;vnMQgf-njU-RVBRw0P0-m#d-u`(kq7NL&2T)TjI_@iKuPAK-@oH(J8?%(e!0Ir$yG32@CGUPn5w4)+9@8c&pGx z+K3GKESI4*`tYlmMHt@br;jBWTei&(a=iYslc^c#RU3Q&sYp zSG){)V<(g7+8W!Wxeb5zJb4XE{I|&Y4UrFWr%LHkdQ;~XU zgy^dH-Z3lmY+0G~?DrC_S4@=>0oM8Isw%g(id10gWkoz2Q%7W$bFk@mIzTCcIB(K8 zc<5h&ZzCdT=9n-D>&a8vl+=ZF*`uTvQviG_bLde*k>{^)&0o*b05x$MO3gVLUx`xZ z43j+>!u?XV)Yp@MmG%Y`+COH2?nQcMrQ%k~6#O%PeD_WvFO~Kct za4XoCM_X!c5vhRkIdV=xUB3xI2NNStK*8_Zl!cFjOvp-AY=D;5{uXj}GV{LK1~IE2 z|KffUiBaStRr;10R~K2VVtf{TzM7FaPm;Y(zQjILn+tIPSrJh&EMf6evaBKIvi42-WYU9Vhj~3< zZSM-B;E`g_o8_XTM9IzEL=9Lb^SPhe(f(-`Yh=X6O7+6ALXnTcUFpI>ekl6v)ZQeNCg2 z^H|{SKXHU*%nBQ@I3It0m^h+6tvI@FS=MYS$ZpBaG7j#V@P2ZuYySbp@hA# ze(kc;P4i_-_UDP?%<6>%tTRih6VBgScKU^BV6Aoeg6Uh(W^#J^V$Xo^4#Ekp ztqQVK^g9gKMTHvV7nb64UU7p~!B?>Y0oFH5T7#BSW#YfSB@5PtE~#SCCg3p^o=NkMk$<8- z6PT*yIKGrvne7+y3}_!AC8NNeI?iTY(&nakN>>U-zT0wzZf-RuyZk^X9H-DT_*wk= z;&0}6LsGtfVa1q)CEUPlx#(ED@-?H<1_FrHU#z5^P3lEB|qsxEyn%FOpjx z3S?~gvoXy~L(Q{Jh6*i~=f%9kM1>RGjBzQh_SaIDfSU_9!<>*Pm>l)cJD@wlyxpBV z4Fmhc2q=R_wHCEK69<*wG%}mgD1=FHi4h!98B-*vMu4ZGW~%IrYSLGU{^TuseqVgV zLP<%wirIL`VLyJv9XG_p8w@Q4HzNt-o;U@Au{7%Ji;53!7V8Rv0^Lu^Vf*sL>R(;c zQG_ZuFl)Mh-xEIkGu}?_(HwkB2jS;HdPLSxVU&Jxy9*XRG~^HY(f0g8Q}iqnVmgjI zfd=``2&8GsycjR?M%(zMjn;tn9agcq;&rR!Hp z$B*gzHsQ~aXw8c|a(L^LW(|`yGc!qOnV(ZjU_Q-4z1&0;jG&vAKuNG=F|H?@m5^N@ zq{E!1n;)kNTJ>|Hb2ODt-7U~-MOIFo%9I)_@7fnX+eMMNh>)V$IXesJpBn|uo8f~#aOFytCT zf9&%MCLf8mp4kwHTcojWmM3LU=#|{3L>E}SKwOd?%{HogCZ_Z1BSA}P#O(%H$;z7XyJ^sjGX;j5 zrzp>|Ud;*&VAU3x#f{CKwY7Vc{%TKKqmB@oTHA9;>?!nvMA;8+Jh=cambHz#J18x~ zs!dF>$*AnsQ{{82r5Aw&^7eRCdvcgyxH?*DV5(I$qXh^zS>us*I66_MbL8y4d3ULj z{S(ipo+T3Ag!+5`NU2sc+@*m{_X|&p#O-SAqF&g_n7ObB82~$p%fXA5GLHMC+#qqL zdt`sJC&6C2)=juQ_!NeD>U8lDVpAOkW*khf7MCcs$A(wiIl#B9HM%~GtQ^}yBPjT@ z+E=|A!Z?A(rwzZ;T}o6pOVqHzTr*i;Wrc%&36kc@jXq~+w8kVrs;%=IFdACoLAcCAmhFNpbP8;s`zG|HC2Gv?I~w4ITy=g$`0qMQdkijLSOtX6xW%Z9Nw<;M- zMN`c7=$QxN00DiSjbVt9Mi6-pjv*j(_8PyV-il8Q-&TwBwH1gz1uoxs6~uU}PrgWB zIAE_I-a1EqlIaGQNbcp@iI8W1sm9fBBNOk(k&iLBe%MCo#?xI$%ZmGA?=)M9D=0t7 zc)Q0LnI)kCy{`jCGy9lYX%mUsDWwsY`;jE(;Us@gmWPqjmXL+Hu#^;k%eT>{nMtzj zsV`Iy6leTA8-PndszF;N^X@CJrTw5IIm!GPeu)H2#FQitR{1p;MasQVAG3*+=9FYK zw*k!HT(YQorfQj+1*mCV458(T5=fH`um$gS38hw(OqVMyunQ;rW5aPbF##A3fGH6h z@W)i9Uff?qz`YbK4c}JzQpuxuE3pcQO)%xBRZp{zJ^-*|oryTxJ-rR+MXJ)!f=+pp z10H|DdGd2exhi+hftcYbM0_}C0ZI-2vh+$fU1acsB-YXid7O|=9L!3e@$H*6?G*Zp z%qFB(sgl=FcC=E4CYGp4CN>=M8#5r!RU!u+FJVlH6=gI5xHVD&k;Ta*M28BsxfMV~ zLz+@6TxnfLhF@5=yQo^1&S}cmTN@m!7*c6z;}~*!hNBjuE>NLVl2EwN!F+)0$R1S! zR|lF%n!9fkZ@gPW|x|B={V6x3`=jS*$Pu0+5OWf?wnIy>Y1MbbGSncpKO0qE(qO=ts z!~@&!N`10S593pVQu4FzpOh!tvg}p%zCU(aV5=~K#bKi zHdJ1>tQSrhW%KOky;iW+O_n;`l9~omqM%sdxdLtI`TrJzN6BQz+7xOl*rM>xVI2~# z)7FJ^Dc{DC<%~VS?@WXzuOG$YPLC;>#vUJ^MmtbSL`_yXtNKa$Hk+l-c!aC7gn(Cg ze?YPYZ(2Jw{SF6MiO5(%_pTo7j@&DHNW`|lD`~{iH+_eSTS&OC*2WTT*a`?|9w1dh zh1nh@$a}T#WE5$7Od~NvSEU)T(W$p$s5fe^GpG+7fdJ9=enRT9$wEk+ZaB>G3$KQO zgq?-rZZnIv!p#>Ty~}c*Lb_jxJg$eGM*XwHUwuQ|o^}b3^T6Bxx{!?va8aC@-xK*H ztJBFvFfsSWu89%@b^l3-B~O!CXs)I6Y}y#0C0U0R0WG zybjroj$io0j}3%P7zADXOwHwafT#uu*zfM!oD$6aJx7+WL%t-@6^rD_a_M?S^>c;z zMK580bZXo1f*L$CuMeM4Mp!;P@}b~$cd(s5*q~FP+NHSq;nw3fbWyH)i2)-;gQl{S zZO!T}A}fC}vUdskGSq&{`oxt~0i?0xhr6I47_tBc`fqaSrMOzR4>0H^;A zF)hX1nfHs)%Zb-(YGX;=#2R6C{BG;k=?FfP?9{_uFLri~-~AJ;jw({4MU7e*d)?P@ zXX*GkNY9ItFjhwgAIWq7Y!ksbMzfqpG)IrqKx9q{zu%Mdl+{Dis#p9q`02pr1LG8R z@As?eG!>IoROgS!@J*to<27coFc1zpkh?w=)h9CbYe%^Q!Ui46Y*HO0mr% zEff-*$ndMNw}H2a5@BsGj5oFfd!T(F&0$<{GO!Qdd?McKkorh=5{EIjDTHU`So>8V zBA-fqVLb2;u7UhDV1xMI?y>fe3~4urv3%PX)lDw+HYa;HFkaLqi4c~VtCm&Ca+9C~ zge+67hp#R9`+Euq59WhHX&7~RlXn=--m8$iZ~~1C8cv^2(qO#X0?vl91gzUKBeR1J z^p4!!&7)3#@@X&2aF2-)1Ffcc^F8r|RtdL2X%HgN&XU-KH2SLCbpw?J5xJ*!F-ypZ zMG%AJ!Pr&}`LW?E!K~=(NJxuSVTRCGJ$2a*Ao=uUDSys!OFYu!Vs2IT;xQ6EubLIl z+?+nMGeQQhh~??0!s4iQ#gm3!BpMpnY?04kK375e((Uc7B3RMj;wE?BCoQGu=UlZt!EZ1Q*auI)dj3Jj{Ujgt zW5hd~-HWBLI_3HuO) zNrb^XzPsTIb=*a69wAAA3J6AAZZ1VsYbIG}a`=d6?PjM)3EPaDpW2YP$|GrBX{q*! z$KBHNif)OKMBCFP5>!1d=DK>8u+Upm-{hj5o|Wn$vh1&K!lVfDB&47lw$tJ?d5|=B z^(_9=(1T3Fte)z^>|3**n}mIX;mMN5v2F#l(q*CvU{Ga`@VMp#%rQkDBy7kYbmb-q z<5!4iuB#Q_lLZ8}h|hPODI^U6`gzLJre9u3k3c#%86IKI*^H-@I48Bi*@avYm4v!n0+v zWu{M{&F8#p9cx+gF0yTB_<2QUrjMPo9*7^-uP#~gGW~y3nfPAoV%amgr>PSyVAd@l)}8#X zR5zV6t*uKJZL}?NYvPVK6J0v4iVpwiN|>+t3aYiZSp;m0!(1`bHO}TEtWR1tY%BPB z(W!0DmXbZAsT$iC13p4f>u*ZAy@JoLAkJhzFf1#4;#1deO8#8d&89}en&z!W&A3++^1(;>0SB1*54d@y&9Pn;^IAf3GiXbfT`_>{R+Xv; zQvgL>+0#8-laO!j#-WB~(I>l0NCMt_;@Gp_f0#^c)t?&#Xh1-7RR0@zPyBz!U#0Av zT?}n({(p?p7!4S2ZBw)#KdCG)uPnZe+U|0{BW!m)9 zi_9$F?m<`2!`JNFv+w8MK_K)qJ^aO@7-Ig>cM4-r0bi=>?B_2mFNJ}aE3<+QCzRr*NA!QjHw# z`1OsvcoD0?%jq{*7b!l|L1+Tw0TTAM4XMq7*ntc-Ived>Sj_ZtS|uVdpfg1_I9knY z2{GM_j5sDC7(W&}#s{jqbybqJWyn?{PW*&cQIU|*v8YGOKKlGl@?c#TCnmnAkAzV- zmK={|1G90zz=YUvC}+fMqts0d4vgA%t6Jhjv?d;(Z}(Ep8fTZfHA9``fdUHkA+z3+ zhh{ohP%Bj?T~{i0sYCQ}uC#5BwN`skI7`|c%kqkyWIQ;!ysvA8H`b-t()n6>GJj6xlYDu~8qX{AFo$Cm3d|XFL=4uvc?Keb zzb0ZmMoXca6Mob>JqkNuoP>B2Z>D`Q(TvrG6m`j}-1rGP!g|qoL=$FVQYxJQjFn33lODt3Wb1j8VR zlR++vIT6^DtYxAv_hxupbLLN3e0%A%a+hWTKDV3!Fjr^cWJ{scsAdfhpI)`Bms^M6 zQG$waKgFr=c|p9Piug=fcJvZ1ThMnNhQvBAg-8~b1?6wL*WyqXhtj^g(Ke}mEfZVM zJuLNTUVh#WsE*a6uqiz`b#9ZYg3+2%=C(6AvZGc=u&<6??!slB1a9K)=VL zY9EL^mfyKnD zSJyYBc_>G;5RRnrNgzJz#Rkn3S1`mZgO`(r5;Hw6MveN(URf_XS-r58Cn80K)ArH4 z#Rrd~LG1W&@ttw85cjp8xV&>$b%nSXH_*W}7Ch2pg$$c0BdEo-HWRTZcxngIBJad> z;C>b{jIXjb_9Jis?NZJsdm^EG}e*pR&DAy0EaSGi3XWTa(>C%tz1n$u?5Fb z1qtl?;_yjYo)(gB^iQq?=jusF%kywm?CJP~zEHi0NbZ);$(H$w(Hy@{i>$wcVRD_X|w-~(0Z9BJyh zhNh;+eQ9BEIs;tPz%jSVnfCP!3L&9YtEP;svoj_bNzeGSQIAjd zBss@A;)R^WAu-37RQrM%{DfBNRx>v!G31Z}8-El9IOJlb_MSoMu2}GDYycNaf>uny z+8xykD-7ONCM!APry_Lw6-yT>5!tR}W;W`C)1>pxSs5o1z#j7%m=&=7O4hz+Lsqm` z*>{+xsabZPr&X=}G@obTb{nPTkccJX8w3CG7X+1+t{JcMabv~UNv+G?txRqXib~c^Mo}`q{$`;EBNJ;#F*{gvS12kV?AZ%O0SFB$^ zn+}!HbmEj}w{Vq(G)OGAzH}R~kS^;(-s&=ectz8vN!_)Yl$$U@HNTI-pV`LSj7Opu zTZ5zZ)-S_{GcEQPIQXLQ#oMS`HPu{`SQiAZ)m1at*Hy%3xma|>o`h%E%8BEbi9p0r zVjcsh<{NBKQ4eKlXU|}@XJ#@uQw*$4BxKn6#W~I4T<^f99~(=}a`&3(ur8R9t+|AQ zWkQx7l}wa48-jO@ft2h+7qn%SJtL%~890FG0s5g*kNbL3I&@brh&f6)TlM`K^(bhr zJWM6N6x3flOw$@|C@kPi7yP&SP?bzP-E|HSXQXG>7gk|R9BTj`e=4de9C6+H7H7n# z#GJeVs1mtHhLDmVO?LkYRQc`DVOJ_vdl8VUihO-j#t=0T3%Fc1f9F73ufJz*adn*p zc%&vi(4NqHu^R>sAT_0EDjVR8bc%wTz#$;%NU-kbDyL_dg0%TFafZwZ?5KZpcuaO54Z9hX zD$u>q!-9`U6-D`E#`W~fIfiIF5_m6{fvM)b1NG3xf4Auw;Go~Fu7cth#DlUn{@~yu z=B;RT*dp?bO}o%4x7k9v{r=Y@^YQ^UUm(Qmliw8brO^=NP+UOohLYiaEB3^DB56&V zK?4jV61B|1Uj_5fBKW;8LdwOFZKWp)g{B%7g1~DgO&N& z#lisxf?R~Z@?3E$Mms$$JK8oe@X`5m98V*aV6Ua}8Xs2#A!{x?IP|N(%nxsH?^c{& z@vY&R1QmQs83BW28qAmJfS7MYi=h(YK??@EhjL-t*5W!p z^gYX!Q6-vBqcv~ruw@oMaU&qp0Fb(dbVzm5xJN%0o_^@fWq$oa3X?9s%+b)x4w-q5Koe(@j6Ez7V@~NRFvd zfBH~)U5!ix3isg`6be__wBJp=1@yfsCMw1C@y+9WYD9_C%{Q~7^0AF2KFryfLlUP# zwrtJEcH)jm48!6tUcxiurAMaiD04C&tPe6DI0#aoqz#Bt0_7_*X*TsF7u*zv(iEfA z;$@?XVu~oX#1YXtceQL{dSneL&*nDug^OW$DSLF0M1Im|sSX8R26&)<0Fbh^*l6!5wfSu8MpMoh=2l z^^0Sr$UpZp*9oqa23fcCfm7`ya2<4wzJ`Axt7e4jJrRFVf?nY~2&tRL* zd;6_njcz01c>$IvN=?K}9ie%Z(BO@JG2J}fT#BJQ+f5LFSgup7i!xWRKw6)iITjZU z%l6hPZia>R!`aZjwCp}I zg)%20;}f+&@t;(%5;RHL>K_&7MH^S+7<|(SZH!u zznW|jz$uA`P9@ZWtJgv$EFp>)K&Gt+4C6#*khZQXS*S~6N%JDT$r`aJDs9|uXWdbg zBwho$phWx}x!qy8&}6y5Vr$G{yGSE*r$^r{}pw zVTZKvikRZ`J_IJrjc=X1uw?estdwm&bEahku&D04HD+0Bm~q#YGS6gp!KLf$A{%Qd z&&yX@Hp>~(wU{|(#U&Bf92+1i&Q*-S+=y=3pSZy$#8Uc$#7oiJUuO{cE6=tsPhwPe| zxQpK>`Dbka`V)$}e6_OXKLB%i76~4N*zA?X+PrhH<&)}prET;kel24kW%+9))G^JI zsq7L{P}^#QsZViX%KgxBvEugr>ZmFqe^oAg?{EI=&_O#e)F3V#rc z8$4}0Zr19qd3tE4#$3_f=Bbx9oV6VO!d3(R===i-7p=Vj`520w0D3W6lQfY48}!D* z&)lZMG;~er2qBoI2gsX+Ts-hnpS~NYRDtPd^FPzn!^&yxRy#CSz(b&E*tL|jIkq|l zf%>)7Dtu>jCf`-7R#*GhGn4FkYf;B$+9IxmqH|lf6$4irg{0ept__%)V*R_OK=T06 zyT_m-o@Kp6U{l5h>W1hGq*X#8*y@<;vsOFqEjTQXFEotR+{3}ODDnj;o0@!bB5x=N z394FojuGOtVKBlVRLtHp%EJv_G5q=AgF)SKyRN5=cGBjDWv4LDn$IL`*=~J7u&Dy5 zrMc83y+w^F&{?X(KOOAl-sWZDb{9X9#jrQtmrEXD?;h-}SYT7yM(X_6qksM=K_a;Z z3u0qT0TtaNvDER_8x*rxXw&C^|h{P1qxK|@pS7vdlZ#P z7PdB7MmC2}%sdzAxt>;WM1s0??`1983O4nFK|hVAbHcZ3x{PzytQLkCVk7hA!Lo` zEJH?4qw|}WH{dc4z%aB=0XqsFW?^p=X}4xnCJXK%c#ItOSjdSO`UXJyuc8bh^Cf}8 z@Ht|vXd^6{Fgai8*tmyRGmD_s_nv~r^Fy7j`Bu`6=G)5H$i7Q7lvQnmea&TGvJp9a|qOrUymZ$6G|Ly z#zOCg++$3iB$!6!>215A4!iryregKuUT344X)jQb3|9qY>c0LO{6Vby05n~VFzd?q zgGZv&FGlkiH*`fTurp>B8v&nSxNz)=5IF$=@rgND4d`!AaaX;_lK~)-U8la_Wa8i?NJC@BURO*sUW)E9oyv3RG^YGfN%BmxzjlT)bp*$<| zX3tt?EAy<&K+bhIuMs-g#=d1}N_?isY)6Ay$mDOKRh z4v1asEGWoAp=srraLW^h&_Uw|6O+r;wns=uwYm=JN4Q!quD8SQRSeEcGh|Eb5Jg8m zOT}u;N|x@aq)=&;wufCc^#)5U^VcZw;d_wwaoh9$p@Xrc{DD6GZUqZ ziC6OT^zSq@-lhbgR8B+e;7_Giv;DK5gn^$bs<6~SUadiosfewWDJu`XsBfOd1|p=q zE>m=zF}!lObA%ePey~gqU8S6h-^J2Y?>7)L2+%8kV}Gp=h`Xm_}rlm)SyUS=`=S7msKu zC|T!gPiI1rWGb1z$Md?0YJQ;%>uPLOXf1Z>N~`~JHJ!^@D5kSXQ4ugnFZ>^`zH8CAiZmp z6Ms|#2gcGsQ{{u7+Nb9sA?U>(0e$5V1|WVwY`Kn)rsnnZ4=1u=7u!4WexZD^IQ1Jk zfF#NLe>W$3m&C^ULjdw+5|)-BSHwpegdyt9NYC{3@QtMfd8GrIWDu`gd0nv-3LpGCh@wgBaG z176tikL!_NXM+Bv#7q^cyn9$XSeZR6#!B4JE@GVH zoobHZN_*RF#@_SVYKkQ_igme-Y5U}cV(hkR#k1c{bQNMji zU7aE`?dHyx=1`kOYZo_8U7?3-7vHOp`Qe%Z*i+FX!s?6huNp0iCEW-Z7E&jRWmUW_ z67j>)Ew!yq)hhG4o?^z}HWH-e=es#xJUhDRc4B51M4~E-l5VZ!&zQq`gWe`?}#b~7w1LH4Xa-UCT5LXkXQWheBa2YJYbyQ zl1pXR%b(KCXMO0OsXgl0P0Og<{(@&z1aokU-Pq`eQq*JYgt8xdFQ6S z6Z3IFSua8W&M#`~*L#r>Jfd6*BzJ?JFdBR#bDv$_0N!_5vnmo@!>vULcDm`MFU823 zpG9pqjqz^FE5zMDoGqhs5OMmC{Y3iVcl>F}5Rs24Y5B^mYQ;1T&ks@pIApHOdrzXF z-SdX}Hf{X;TaSxG_T$0~#RhqKISGKNK47}0*x&nRIPtmdwxc&QT3$8&!3fWu1eZ_P zJveQj^hJL#Sn!*4k`3}(d(aasl&7G0j0-*_2xtAnoX1@9+h zO#c>YQg60Z;o{Bi=3i7S`Ic+ZE>K{(u|#)9y}q*j8uKQ1^>+(BI}m%1v3$=4ojGBc zm+o1*!T&b}-lVvZqIUBc8V}QyFEgm#oyIuC{8WqUNV{Toz`oxhYpP!_p2oHHh5P@iB*NVo~2=GQm+8Yrkm2Xjc_VyHg1c0>+o~@>*Qzo zHVBJS>$$}$_4EniTI;b1WShX<5-p#TPB&!;lP!lBVBbLOOxh6FuYloD%m;n{r|;MU3!q4AVkua~fieeWu2 zQAQ$ue(IklX6+V;F1vCu-&V?I3d42FgWgsb_e^29ol}HYft?{SLf>DrmOp9o!t>I^ zY7fBCk+E8n_|apgM|-;^=#B?6RnFKlN`oR)`e$+;D=yO-(U^jV;rft^G_zl`n7qnM zL z*-Y4Phq+ZI1$j$F-f;`CD#|`-T~OM5Q>x}a>B~Gb3-+9i>Lfr|Ca6S^8g*{*?_5!x zH_N!SoRP=gX1?)q%>QTY!r77e2j9W(I!uAz{T`NdNmPBBUzi2{`XMB^zJGGwFWeA9 z{fk33#*9SO0)DjROug+(M)I-pKA!CX;IY(#gE!UxXVsa)X!UftIN98{pt#4MJHOhY zM$_l}-TJlxY?LS6Nuz1T<44m<4i^8k@D$zuCPrkmz@sdv+{ciyFJG2Zwy&%c7;atIeTdh!a(R^QXnu1Oq1b42*OQFWnyQ zWeQrdvP|w_idy53Wa<{QH^lFmEd+VlJkyiC>6B#s)F;w-{c;aKIm;Kp50HnA-o3lY z9B~F$gJ@yYE#g#X&3ADx&tO+P_@mnQTz9gv30_sTsaGXkfNYXY{$(>*PEN3QL>I!k zp)KibPhrfX3%Z$H6SY`rXGYS~143wZrG2;=FLj50+VM6soI~up_>fU(2Wl@{BRsMi zO%sL3x?2l1cXTF)k&moNsHfQrQ+wu(gBt{sk#CU=UhrvJIncy@tJX5klLjgMn>~h= zg|FR&;@eh|C7`>s_9c~0-{IAPV){l|Ts`i=)AW;d9&KPc3fMeoTS%8@V~D8*h;&(^>yjT84MM}=%#LS7shLAuuj(0VAYoozhWjq z4LEr?wUe2^WGwdTIgWBkDUJa>YP@5d9^Rs$kCXmMRxuF*YMVrn?0NFyPl}>`&dqZb z<5eqR=ZG3>n2{6v6BvJ`YBZeeTtB88TAY(x0a58EWyuf>+^|x8Qa6wA|1Nb_p|nA zWWa}|z8a)--Wj`LqyFk_a3gN2>5{Rl_wbW?#by7&i*^hRknK%jwIH6=dQ8*-_{*x0j^DUfMX0`|K@6C<|1cgZ~D(e5vBFFm;HTZF(!vT8=T$K+|F)x3kqzBV4-=p1V(lzi(s7jdu0>LD#N=$Lk#3HkG!a zIF<7>%B7sRNzJ66KrFV76J<2bdYhxll0y2^_rdG=I%AgW4~)1Nvz=$1UkE^J%BxLo z+lUci`UcU062os*=`-j4IfSQA{w@y|3}Vk?i;&SSdh8n+$iHA#%ERL{;EpXl6u&8@ zzg}?hkEOUOJt?ZL=pWZFJ19mI1@P=$U5*Im1e_8Z${JsM>Ov?nh8Z zP5QvI!{Jy@&BP48%P2{Jr_VgzW;P@7)M9n|lDT|Ep#}7C$&ud&6>C^5ZiwKIg2McPU(4jhM!BD@@L(Gd*Nu$ji(ljZ<{FIeW_1Mmf;76{LU z-ywN~=uNN)Xi6$<12A9y)K%X|(W0p|&>>4OXB?IiYr||WKDOJPxiSe01NSV-h24^L z_>m$;|C+q!Mj**-qQ$L-*++en(g|hw;M!^%_h-iDjFHLo-n3JpB;p?+o2;`*jpvJU zLY^lt)Un4joij^^)O(CKs@7E%*!w>!HA4Q?0}oBJ7Nr8NQ7QmY^4~jvf0-`%waOLn zdNjAPaC0_7c|RVhw)+71NWjRi!y>C+Bl;Z`NiL^zn2*0kmj5gyhCLCxts*cWCdRI| zjsd=sT5BVJc^$GxP~YF$-U{-?kW6r@^vHXB%{CqYzU@1>dzf#3SYedJG-Rm6^RB7s zGM5PR(yKPKR)>?~vpUIeTP7A1sc8-knnJk*9)3t^e%izbdm>Y=W{$wm(cy1RB-19i za#828DMBY+ps#7Y8^6t)=Ea@%Nkt)O6JCx|ybC;Ap}Z@Zw~*}3P>MZLPb4Enxz9Wf zssobT^(R@KuShj8>@!1M7tm|2%-pYYDxz-5`rCbaTCG5{;Uxm z*g=+H1X8{NUvFGzz~wXa%Eo};I;~`37*WrRU&K0dPSB$yk(Z*@K&+mFal^?c zurbqB-+|Kb5|sznT;?Pj!+kgFY1#Dr;_%A(GIQC{3ct|{*Bji%FNa6c-thbpBkA;U zURV!Dr&X{0J}iht#-Qp2=xzuh(fM>zRoiGrYl5ttw2#r34gC41CCOC31m~^UPTK@s z6;A@)7O7_%C)>bnAXerYuAHdE93>j2N}H${zEc6&SbZ|-fiG*-qtGuy-qDelH(|u$ zorf8_T6Zqe#Ub!+e3oSyrskt_HyW_^5lrWt#30l)tHk|j$@YyEkXUOV;6B51L;M@=NIWZXU;GrAa(LGxO%|im%7F<-6N;en0Cr zLH>l*y?pMwt`1*cH~LdBPFY_l;~`N!Clyfr;7w<^X;&(ZiVdF1S5e(+Q%60zgh)s4 zn2yj$+mE=miVERP(g8}G4<85^-5f@qxh2ec?n+$A_`?qN=iyT1?U@t?V6DM~BIlBB z>u~eXm-aE>R0sQy!-I4xtCNi!!qh?R1!kKf6BoH2GG{L4%PAz0{Sh6xpuyI%*~u)s z%rLuFl)uQUCBQAtMyN;%)zFMx4loh7uTfKeB2Xif`lN?2gq6NhWhfz0u5WP9J>=V2 zo{mLtSy&BA!mSzs&CrKWq^y40JF5a&GSXIi2= z{EYb59J4}VwikL4P=>+mc6{($FNE@e=VUwG+KV21;<@lrN`mnz5jYGASyvz7BOG_6(p^eTxD-4O#lROgon;R35=|nj#eHIfJBYPWG>H>`dHKCDZ3`R{-?HO0mE~(5_WYcFmp8sU?wr*UkAQiNDGc6T zA%}GOLXlOWqL?WwfHO8MB#8M8*~Y*gz;1rWWoVSXP&IbKxbQ8+s%4Jnt?kDsq7btI zCDr0PZ)b;B%!lu&CT#RJzm{l{2fq|BcY85`w~3LSK<><@(2EdzFLt9Y_`;WXL6x`0 zDoQ?=?I@Hbr;*VVll1Gmd8*%tiXggMK81a+T(5Gx6;eNb8=uYn z5BG-0g>pP21NPn>$ntBh>`*})Fl|38oC^9Qz>~MAazH%3Q~Qb!ALMf$srexgPZ2@&c~+hxRi1;}+)-06)!#Mq<6GhP z-Q?qmgo${aFBApb5p}$1OJKTClfi8%PpnczyVKkoHw7Ml9e7ikrF0d~UB}i3vizos zXW4DN$SiEV9{faLt5bHy2a>33K%7Td-n5C*N;f&ZqAg#2hIqEb(y<&f4u5BWJ>2^4 z414GosL=Aom#m&=x_v<0-fp1r%oVJ{T-(xnomNJ(Dryv zh?vj+%=II_nV+@NR+(!fZZVM&(W6{6%9cm+o+Z6}KqzLw{(>E86uA1`_K$HqINlb1 zKelh3-jr2I9V?ych`{hta9wQ2c9=MM`2cC{m6^MhlL2{DLv7C^j z$xXBCnDl_;l|bPGMX@*tV)B!c|4oZyftUlP*?$YU9C_eAsuVHJ58?)zpbr30P*C`T z7y#ao`uE-SOG(Pi+`$=e^mle~)pRrdwL5)N;o{gpW21of(QE#U6w%*C~`v-z0QqBML!!5EeYA5IQB0 z^l01c;L6E(iytN!LhL}wfwP7W9PNAkb+)Cst?qg#$n;z41O4&v+8-zPs+XNb-q zIeeBCh#ivnFLUCwfS;p{LC0O7tm+Sf9Jn)~b%uwP{%69;QC)Ok0t%*a5M+=;y8j=v z#!*pp$9@!x;UMIs4~hP#pnfVc!%-D<+wsG@R2+J&%73lK|2G!EQC)O05TCV=&3g)C!lT=czLpZ@Sa%TYuoE?v8T8`V;e$#Zf2_Nj6nvBgh1)2 GZ~q4|mN%#X literal 61608 zcmb5VV{~QRw)Y#`wrv{~+qP{x72B%VwzFc}c2cp;N~)5ZbDrJayPv(!dGEd-##*zr z)#n-$y^sH|_dchh3@8{H5D*j;5D<{i*8l5IFJ|DjL!e)upfGNX(kojugZ3I`oH1PvW`wFW_ske0j@lB9bX zO;2)`y+|!@X(fZ1<2n!Qx*)_^Ai@Cv-dF&(vnudG?0CsddG_&Wtae(n|K59ew)6St z#dj7_(Cfwzh$H$5M!$UDd8=4>IQsD3xV=lXUq($;(h*$0^yd+b{qq63f0r_de#!o_ zXDngc>zy`uor)4A^2M#U*DC~i+dc<)Tb1Tv&~Ev@oM)5iJ4Sn#8iRw16XXuV50BS7 zdBL5Mefch(&^{luE{*5qtCZk$oFr3RH=H!c3wGR=HJ(yKc_re_X9pD` zJ;uxPzUfVpgU>DSq?J;I@a+10l0ONXPcDkiYcihREt5~T5Gb}sT0+6Q;AWHl`S5dV>lv%-p9l#xNNy7ZCr%cyqHY%TZ8Q4 zbp&#ov1*$#grNG#1vgfFOLJCaNG@K|2!W&HSh@3@Y%T?3YI75bJp!VP*$*!< z;(ffNS_;@RJ`=c7yX04!u3JP*<8jeqLHVJu#WV&v6wA!OYJS4h<_}^QI&97-;=ojW zQ-1t)7wnxG*5I%U4)9$wlv5Fr;cIizft@&N+32O%B{R1POm$oap@&f| zh+5J{>U6ftv|vAeKGc|zC=kO(+l7_cLpV}-D#oUltScw})N>~JOZLU_0{Ka2e1evz z{^a*ZrLr+JUj;)K&u2CoCAXLC2=fVScI(m_p~0FmF>>&3DHziouln?;sxW`NB}cSX z8?IsJB)Z=aYRz!X=yJn$kyOWK%rCYf-YarNqKzmWu$ZvkP12b4qH zhS9Q>j<}(*frr?z<%9hl*i^#@*O2q(Z^CN)c2c z>1B~D;@YpG?G!Yk+*yn4vM4sO-_!&m6+`k|3zd;8DJnxsBYtI;W3We+FN@|tQ5EW= z!VU>jtim0Mw#iaT8t_<+qKIEB-WwE04lBd%Letbml9N!?SLrEG$nmn7&W(W`VB@5S zaY=sEw2}i@F_1P4OtEw?xj4@D6>_e=m=797#hg}f*l^`AB|Y0# z9=)o|%TZFCY$SzgSjS|8AI-%J4x}J)!IMxY3_KYze`_I=c1nmrk@E8c9?MVRu)7+Ue79|)rBX7tVB7U|w4*h(;Gi3D9le49B38`wuv zp7{4X^p+K4*$@gU(Tq3K1a#3SmYhvI42)GzG4f|u zwQFT1n_=n|jpi=70-yE9LA+d*T8u z`=VmmXJ_f6WmZveZPct$Cgu^~gFiyL>Lnpj*6ee>*0pz=t$IJ}+rE zsf@>jlcG%Wx;Cp5x)YSVvB1$yyY1l&o zvwX=D7k)Dn;ciX?Z)Pn8$flC8#m`nB&(8?RSdBvr?>T9?E$U3uIX7T?$v4dWCa46 z+&`ot8ZTEgp7G+c52oHJ8nw5}a^dwb_l%MOh(ebVj9>_koQP^$2B~eUfSbw9RY$_< z&DDWf2LW;b0ZDOaZ&2^i^g+5uTd;GwO(-bbo|P^;CNL-%?9mRmxEw~5&z=X^Rvbo^WJW=n_%*7974RY}JhFv46> zd}`2|qkd;89l}R;i~9T)V-Q%K)O=yfVKNM4Gbacc7AOd>#^&W&)Xx!Uy5!BHnp9kh z`a(7MO6+Ren#>R^D0K)1sE{Bv>}s6Rb9MT14u!(NpZOe-?4V=>qZ>}uS)!y~;jEUK z&!U7Fj&{WdgU#L0%bM}SYXRtM5z!6M+kgaMKt%3FkjWYh=#QUpt$XX1!*XkpSq-pl zhMe{muh#knk{9_V3%qdDcWDv}v)m4t9 zQhv{;} zc{}#V^N3H>9mFM8`i`0p+fN@GqX+kl|M94$BK3J-X`Hyj8r!#x6Vt(PXjn?N)qedP z=o1T^#?1^a{;bZ&x`U{f?}TMo8ToN zkHj5v|}r}wDEi7I@)Gj+S1aE-GdnLN+$hw!=DzglMaj#{qjXi_dwpr|HL(gcCXwGLEmi|{4&4#OZ4ChceA zKVd4K!D>_N=_X;{poT~4Q+!Le+ZV>=H7v1*l%w`|`Dx8{)McN@NDlQyln&N3@bFpV z_1w~O4EH3fF@IzJ9kDk@7@QctFq8FbkbaH7K$iX=bV~o#gfh?2JD6lZf(XP>~DACF)fGFt)X%-h1yY~MJU{nA5 ze2zxWMs{YdX3q5XU*9hOH0!_S24DOBA5usB+Ws$6{|AMe*joJ?RxfV}*7AKN9V*~J zK+OMcE@bTD>TG1*yc?*qGqjBN8mgg@h1cJLDv)0!WRPIkC` zZrWXrceVw;fB%3`6kq=a!pq|hFIsQ%ZSlo~)D z|64!aCnw-?>}AG|*iOl44KVf8@|joXi&|)1rB;EQWgm+iHfVbgllP$f!$Wf42%NO5b(j9Bw6L z;0dpUUK$5GX4QbMlTmLM_jJt!ur`_0~$b#BB7FL*%XFf<b__1o)Ao3rlobbN8-(T!1d-bR8D3S0@d zLI!*GMb5s~Q<&sjd}lBb8Nr0>PqE6_!3!2d(KAWFxa{hm`@u|a(%#i(#f8{BP2wbs zt+N_slWF4IF_O|{w`c~)Xvh&R{Au~CFmW#0+}MBd2~X}t9lz6*E7uAD`@EBDe$>7W zzPUkJx<`f$0VA$=>R57^(K^h86>09?>_@M(R4q($!Ck6GG@pnu-x*exAx1jOv|>KH zjNfG5pwm`E-=ydcb+3BJwuU;V&OS=6yM^4Jq{%AVqnTTLwV`AorIDD}T&jWr8pB&j28fVtk_y*JRP^t@l*($UZ z6(B^-PBNZ+z!p?+e8@$&jCv^EWLb$WO=}Scr$6SM*&~B95El~;W_0(Bvoha|uQ1T< zO$%_oLAwf1bW*rKWmlD+@CP&$ObiDy=nh1b2ejz%LO9937N{LDe7gle4i!{}I$;&Y zkexJ9Ybr+lrCmKWg&}p=`2&Gf10orS?4$VrzWidT=*6{KzOGMo?KI0>GL0{iFWc;C z+LPq%VH5g}6V@-tg2m{C!-$fapJ9y}c$U}aUmS{9#0CM*8pC|sfer!)nG7Ji>mfRh z+~6CxNb>6eWKMHBz-w2{mLLwdA7dA-qfTu^A2yG1+9s5k zcF=le_UPYG&q!t5Zd_*E_P3Cf5T6821bO`daa`;DODm8Ih8k89=RN;-asHIigj`n=ux>*f!OC5#;X5i;Q z+V!GUy0|&Y_*8k_QRUA8$lHP;GJ3UUD08P|ALknng|YY13)}!!HW@0z$q+kCH%xet zlWf@BXQ=b=4}QO5eNnN~CzWBbHGUivG=`&eWK}beuV*;?zt=P#pM*eTuy3 zP}c#}AXJ0OIaqXji78l;YrP4sQe#^pOqwZUiiN6^0RCd#D271XCbEKpk`HI0IsN^s zES7YtU#7=8gTn#lkrc~6)R9u&SX6*Jk4GFX7){E)WE?pT8a-%6P+zS6o&A#ml{$WX zABFz#i7`DDlo{34)oo?bOa4Z_lNH>n;f0nbt$JfAl~;4QY@}NH!X|A$KgMmEsd^&Y zt;pi=>AID7ROQfr;MsMtClr5b0)xo|fwhc=qk33wQ|}$@?{}qXcmECh>#kUQ-If0$ zseb{Wf4VFGLNc*Rax#P8ko*=`MwaR-DQ8L8V8r=2N{Gaips2_^cS|oC$+yScRo*uF zUO|5=?Q?{p$inDpx*t#Xyo6=s?bbN}y>NNVxj9NZCdtwRI70jxvm3!5R7yiWjREEd zDUjrsZhS|P&|Ng5r+f^kA6BNN#|Se}_GF>P6sy^e8kBrgMv3#vk%m}9PCwUWJg-AD zFnZ=}lbi*mN-AOm zCs)r=*YQAA!`e#1N>aHF=bb*z*hXH#Wl$z^o}x##ZrUc=kh%OHWhp=7;?8%Xj||@V?1c ziWoaC$^&04;A|T)!Zd9sUzE&$ODyJaBpvqsw19Uiuq{i#VK1!htkdRWBnb z`{rat=nHArT%^R>u#CjjCkw-7%g53|&7z-;X+ewb?OLWiV|#nuc8mp*LuGSi3IP<<*Wyo9GKV7l0Noa4Jr0g3p_$ z*R9{qn=?IXC#WU>48-k5V2Oc_>P;4_)J@bo1|pf=%Rcbgk=5m)CJZ`caHBTm3%!Z9 z_?7LHr_BXbKKr=JD!%?KhwdYSdu8XxPoA{n8^%_lh5cjRHuCY9Zlpz8g+$f@bw@0V z+6DRMT9c|>1^3D|$Vzc(C?M~iZurGH2pXPT%F!JSaAMdO%!5o0uc&iqHx?ImcX6fI zCApkzc~OOnfzAd_+-DcMp&AOQxE_EsMqKM{%dRMI5`5CT&%mQO?-@F6tE*xL?aEGZ z8^wH@wRl`Izx4sDmU>}Ym{ybUm@F83qqZPD6nFm?t?(7>h*?`fw)L3t*l%*iw0Qu#?$5eq!Qc zpQvqgSxrd83NsdO@lL6#{%lsYXWen~d3p4fGBb7&5xqNYJ)yn84!e1PmPo7ChVd%4 zHUsV0Mh?VpzZD=A6%)Qrd~i7 z96*RPbid;BN{Wh?adeD_p8YU``kOrGkNox3D9~!K?w>#kFz!4lzOWR}puS(DmfjJD z`x0z|qB33*^0mZdM&6$|+T>fq>M%yoy(BEjuh9L0>{P&XJ3enGpoQRx`v6$txXt#c z0#N?b5%srj(4xmPvJxrlF3H%OMB!jvfy z;wx8RzU~lb?h_}@V=bh6p8PSb-dG|-T#A?`c&H2`_!u+uenIZe`6f~A7r)`9m8atC zt(b|6Eg#!Q*DfRU=Ix`#B_dK)nnJ_+>Q<1d7W)eynaVn`FNuN~%B;uO2}vXr5^zi2 z!ifIF5@Zlo0^h~8+ixFBGqtweFc`C~JkSq}&*a3C}L?b5Mh-bW=e)({F_g4O3 zb@SFTK3VD9QuFgFnK4Ve_pXc3{S$=+Z;;4+;*{H}Rc;845rP?DLK6G5Y-xdUKkA6E3Dz&5f{F^FjJQ(NSpZ8q-_!L3LL@H* zxbDF{gd^U3uD;)a)sJwAVi}7@%pRM&?5IaUH%+m{E)DlA_$IA1=&jr{KrhD5q&lTC zAa3c)A(K!{#nOvenH6XrR-y>*4M#DpTTOGQEO5Jr6kni9pDW`rvY*fs|ItV;CVITh z=`rxcH2nEJpkQ^(;1c^hfb8vGN;{{oR=qNyKtR1;J>CByul*+=`NydWnSWJR#I2lN zTvgnR|MBx*XFsfdA&;tr^dYaqRZp*2NwkAZE6kV@1f{76e56eUmGrZ>MDId)oqSWw z7d&r3qfazg+W2?bT}F)4jD6sWaw`_fXZGY&wnGm$FRPFL$HzVTH^MYBHWGCOk-89y zA+n+Q6EVSSCpgC~%uHfvyg@ufE^#u?JH?<73A}jj5iILz4Qqk5$+^U(SX(-qv5agK znUkfpke(KDn~dU0>gdKqjTkVk`0`9^0n_wzXO7R!0Thd@S;U`y)VVP&mOd-2 z(hT(|$=>4FY;CBY9#_lB$;|Wd$aOMT5O_3}DYXEHn&Jrc3`2JiB`b6X@EUOD zVl0S{ijm65@n^19T3l%>*;F(?3r3s?zY{thc4%AD30CeL_4{8x6&cN}zN3fE+x<9; zt2j1RRVy5j22-8U8a6$pyT+<`f+x2l$fd_{qEp_bfxfzu>ORJsXaJn4>U6oNJ#|~p z`*ZC&NPXl&=vq2{Ne79AkQncuxvbOG+28*2wU$R=GOmns3W@HE%^r)Fu%Utj=r9t` zd;SVOnA(=MXgnOzI2@3SGKHz8HN~Vpx&!Ea+Df~`*n@8O=0!b4m?7cE^K*~@fqv9q zF*uk#1@6Re_<^9eElgJD!nTA@K9C732tV~;B`hzZ321Ph=^BH?zXddiu{Du5*IPg} zqDM=QxjT!Rp|#Bkp$(mL)aar)f(dOAXUiw81pX0DC|Y4;>Vz>>DMshoips^8Frdv} zlTD=cKa48M>dR<>(YlLPOW%rokJZNF2gp8fwc8b2sN+i6&-pHr?$rj|uFgktK@jg~ zIFS(%=r|QJ=$kvm_~@n=ai1lA{7Z}i+zj&yzY+!t$iGUy|9jH#&oTNJ;JW-3n>DF+ z3aCOzqn|$X-Olu_p7brzn`uk1F*N4@=b=m;S_C?#hy{&NE#3HkATrg?enaVGT^$qIjvgc61y!T$9<1B@?_ibtDZ{G zeXInVr5?OD_nS_O|CK3|RzzMmu+8!#Zb8Ik;rkIAR%6?$pN@d<0dKD2c@k2quB%s( zQL^<_EM6ow8F6^wJN1QcPOm|ehA+dP(!>IX=Euz5qqIq}Y3;ibQtJnkDmZ8c8=Cf3 zu`mJ!Q6wI7EblC5RvP*@)j?}W=WxwCvF3*5Up_`3*a~z$`wHwCy)2risye=1mSp%p zu+tD6NAK3o@)4VBsM!@);qgsjgB$kkCZhaimHg&+k69~drbvRTacWKH;YCK(!rC?8 zP#cK5JPHSw;V;{Yji=55X~S+)%(8fuz}O>*F3)hR;STU`z6T1aM#Wd+FP(M5*@T1P z^06O;I20Sk!bxW<-O;E081KRdHZrtsGJflFRRFS zdi5w9OVDGSL3 zNrC7GVsGN=b;YH9jp8Z2$^!K@h=r-xV(aEH@#JicPy;A0k1>g1g^XeR`YV2HfmqXY zYbRwaxHvf}OlCAwHoVI&QBLr5R|THf?nAevV-=~V8;gCsX>jndvNOcFA+DI+zbh~# zZ7`qNk&w+_+Yp!}j;OYxIfx_{f0-ONc?mHCiCUak=>j>~>YR4#w# zuKz~UhT!L~GfW^CPqG8Lg)&Rc6y^{%3H7iLa%^l}cw_8UuG;8nn9)kbPGXS}p3!L_ zd#9~5CrH8xtUd?{d2y^PJg+z(xIfRU;`}^=OlehGN2=?}9yH$4Rag}*+AWotyxfCJ zHx=r7ZH>j2kV?%7WTtp+-HMa0)_*DBBmC{sd$)np&GEJ__kEd`xB5a2A z*J+yx>4o#ZxwA{;NjhU*1KT~=ZK~GAA;KZHDyBNTaWQ1+;tOFFthnD)DrCn`DjBZ% zk$N5B4^$`n^jNSOr=t(zi8TN4fpaccsb`zOPD~iY=UEK$0Y70bG{idLx@IL)7^(pL z{??Bnu=lDeguDrd%qW1)H)H`9otsOL-f4bSu};o9OXybo6J!Lek`a4ff>*O)BDT_g z<6@SrI|C9klY(>_PfA^qai7A_)VNE4c^ZjFcE$Isp>`e5fLc)rg@8Q_d^Uk24$2bn z9#}6kZ2ZxS9sI(RqT7?El2@B+($>eBQrNi_k#CDJ8D9}8$mmm z4oSKO^F$i+NG)-HE$O6s1--6EzJa?C{x=QgK&c=)b(Q9OVoAXYEEH20G|q$}Hue%~ zO3B^bF=t7t48sN zWh_zA`w~|){-!^g?6Mqf6ieV zFx~aPUOJGR=4{KsW7I?<=J2|lY`NTU=lt=%JE9H1vBpkcn=uq(q~=?iBt_-r(PLBM zP-0dxljJO>4Wq-;stY)CLB4q`-r*T$!K2o}?E-w_i>3_aEbA^MB7P5piwt1dI-6o!qWCy0 ztYy!x9arGTS?kabkkyv*yxvsPQ7Vx)twkS6z2T@kZ|kb8yjm+^$|sEBmvACeqbz)RmxkkDQX-A*K!YFziuhwb|ym>C$}U|J)4y z$(z#)GH%uV6{ec%Zy~AhK|+GtG8u@c884Nq%w`O^wv2#A(&xH@c5M`Vjk*SR_tJnq z0trB#aY)!EKW_}{#L3lph5ow=@|D5LzJYUFD6 z7XnUeo_V0DVSIKMFD_T0AqAO|#VFDc7c?c-Q%#u00F%!_TW1@JVnsfvm@_9HKWflBOUD~)RL``-!P;(bCON_4eVdduMO>?IrQ__*zE@7(OX zUtfH@AX*53&xJW*Pu9zcqxGiM>xol0I~QL5B%Toog3Jlenc^WbVgeBvV8C8AX^Vj& z^I}H})B=VboO%q1;aU5ACMh{yK4J;xlMc`jCnZR^!~LDs_MP&8;dd@4LDWw~*>#OT zeZHwdQWS!tt5MJQI~cw|Ka^b4c|qyd_ly(+Ql2m&AAw^ zQeSXDOOH!!mAgzAp0z)DD>6Xo``b6QwzUV@w%h}Yo>)a|xRi$jGuHQhJVA%>)PUvK zBQ!l0hq<3VZ*RnrDODP)>&iS^wf64C;MGqDvx>|p;35%6(u+IHoNbK z;Gb;TneFo*`zUKS6kwF*&b!U8e5m4YAo03a_e^!5BP42+r)LFhEy?_7U1IR<; z^0v|DhCYMSj<-;MtY%R@Fg;9Kky^pz_t2nJfKWfh5Eu@_l{^ph%1z{jkg5jQrkvD< z#vdK!nku*RrH~TdN~`wDs;d>XY1PH?O<4^U4lmA|wUW{Crrv#r%N>7k#{Gc44Fr|t z@UZP}Y-TrAmnEZ39A*@6;ccsR>)$A)S>$-Cj!=x$rz7IvjHIPM(TB+JFf{ehuIvY$ zsDAwREg*%|=>Hw$`us~RP&3{QJg%}RjJKS^mC_!U;E5u>`X`jW$}P`Mf}?7G7FX#{ zE(9u1SO;3q@ZhDL9O({-RD+SqqPX)`0l5IQu4q)49TUTkxR(czeT}4`WV~pV*KY&i zAl3~X%D2cPVD^B43*~&f%+Op)wl<&|D{;=SZwImydWL6@_RJjxP2g)s=dH)u9Npki zs~z9A+3fj0l?yu4N0^4aC5x)Osnm0qrhz@?nwG_`h(71P znbIewljU%T*cC=~NJy|)#hT+lx#^5MuDDnkaMb*Efw9eThXo|*WOQzJ*#3dmRWm@! zfuSc@#kY{Um^gBc^_Xdxnl!n&y&}R4yAbK&RMc+P^Ti;YIUh|C+K1|=Z^{nZ}}rxH*v{xR!i%qO~o zTr`WDE@k$M9o0r4YUFFeQO7xCu_Zgy)==;fCJ94M_rLAv&~NhfvcLWCoaGg2ao~3e zBG?Ms9B+efMkp}7BhmISGWmJsKI@a8b}4lLI48oWKY|8?zuuNc$lt5Npr+p7a#sWu zh!@2nnLBVJK!$S~>r2-pN||^w|fY`CT{TFnJy`B|e5;=+_v4l8O-fkN&UQbA4NKTyntd zqK{xEKh}U{NHoQUf!M=2(&w+eef77VtYr;xs%^cPfKLObyOV_9q<(%76-J%vR>w9!us-0c-~Y?_EVS%v!* z15s2s3eTs$Osz$JayyH|5nPAIPEX=U;r&p;K14G<1)bvn@?bM5kC{am|C5%hyxv}a z(DeSKI5ZfZ1*%dl8frIX2?);R^^~LuDOpNpk-2R8U1w92HmG1m&|j&J{EK=|p$;f9 z7Rs5|jr4r8k5El&qcuM+YRlKny%t+1CgqEWO>3;BSRZi(LA3U%Jm{@{y+A+w(gzA< z7dBq6a1sEWa4cD0W7=Ld9z0H7RI^Z7vl(bfA;72j?SWCo`#5mVC$l1Q2--%V)-uN* z9ha*s-AdfbDZ8R8*fpwjzx=WvOtmSzGFjC#X)hD%Caeo^OWjS(3h|d9_*U)l%{Ab8 zfv$yoP{OuUl@$(-sEVNt{*=qi5P=lpxWVuz2?I7Dc%BRc+NGNw+323^ z5BXGfS71oP^%apUo(Y#xkxE)y?>BFzEBZ}UBbr~R4$%b7h3iZu3S(|A;&HqBR{nK& z$;GApNnz=kNO^FL&nYcfpB7Qg;hGJPsCW44CbkG1@l9pn0`~oKy5S777uH)l{irK!ru|X+;4&0D;VE*Ii|<3P zUx#xUqvZT5kVQxsF#~MwKnv7;1pR^0;PW@$@T7I?s`_rD1EGUdSA5Q(C<>5SzE!vw z;{L&kKFM-MO>hy#-8z`sdVx})^(Dc-dw;k-h*9O2_YZw}|9^y-|8RQ`BWJUJL(Cer zP5Z@fNc>pTXABbTRY-B5*MphpZv6#i802giwV&SkFCR zGMETyUm(KJbh+&$8X*RB#+{surjr;8^REEt`2&Dubw3$mx>|~B5IKZJ`s_6fw zKAZx9&PwBqW1Oz0r0A4GtnZd7XTKViX2%kPfv+^X3|_}RrQ2e3l=KG_VyY`H?I5&CS+lAX5HbA%TD9u6&s#v!G> zzW9n4J%d5ye7x0y`*{KZvqyXUfMEE^ZIffzI=Hh|3J}^yx7eL=s+TPH(Q2GT-sJ~3 zI463C{(ag7-hS1ETtU;_&+49ABt5!A7CwLwe z=SoA8mYZIQeU;9txI=zcQVbuO%q@E)JI+6Q!3lMc=Gbj(ASg-{V27u>z2e8n;Nc*pf}AqKz1D>p9G#QA+7mqqrEjGfw+85Uyh!=tTFTv3|O z+)-kFe_8FF_EkTw!YzwK^Hi^_dV5x-Ob*UWmD-})qKj9@aE8g240nUh=g|j28^?v7 zHRTBo{0KGaWBbyX2+lx$wgXW{3aUab6Bhm1G1{jTC7ota*JM6t+qy)c5<@ zpc&(jVdTJf(q3xB=JotgF$X>cxh7k*(T`-V~AR+`%e?YOeALQ2Qud( zz35YizXt(aW3qndR}fTw1p()Ol4t!D1pitGNL95{SX4ywzh0SF;=!wf=?Q?_h6!f* zh7<+GFi)q|XBsvXZ^qVCY$LUa{5?!CgwY?EG;*)0ceFe&=A;!~o`ae}Z+6me#^sv- z1F6=WNd6>M(~ z+092z>?Clrcp)lYNQl9jN-JF6n&Y0mp7|I0dpPx+4*RRK+VQI~>en0Dc;Zfl+x z_e_b7s`t1_A`RP3$H}y7F9_na%D7EM+**G_Z0l_nwE+&d_kc35n$Fxkd4r=ltRZhh zr9zER8>j(EdV&Jgh(+i}ltESBK62m0nGH6tCBr90!4)-`HeBmz54p~QP#dsu%nb~W z7sS|(Iydi>C@6ZM(Us!jyIiszMkd)^u<1D+R@~O>HqZIW&kearPWmT>63%_t2B{_G zX{&a(gOYJx!Hq=!T$RZ&<8LDnxsmx9+TBL0gTk$|vz9O5GkK_Yx+55^R=2g!K}NJ3 zW?C;XQCHZl7H`K5^BF!Q5X2^Mj93&0l_O3Ea3!Ave|ixx+~bS@Iv18v2ctpSt4zO{ zp#7pj!AtDmti$T`e9{s^jf(ku&E|83JIJO5Qo9weT6g?@vX!{7)cNwymo1+u(YQ94 zopuz-L@|5=h8A!(g-MXgLJC0MA|CgQF8qlonnu#j z;uCeq9ny9QSD|p)9sp3ebgY3rk#y0DA(SHdh$DUm^?GI<>%e1?&}w(b zdip1;P2Z=1wM+$q=TgLP$}svd!vk+BZ@h<^4R=GS2+sri7Z*2f`9 z5_?i)xj?m#pSVchk-SR!2&uNhzEi+#5t1Z$o0PoLGz*pT64%+|Wa+rd5Z}60(j?X= z{NLjtgRb|W?CUADqOS@(*MA-l|E342NxRaxLTDqsOyfWWe%N(jjBh}G zm7WPel6jXijaTiNita+z(5GCO0NM=Melxud57PP^d_U## zbA;9iVi<@wr0DGB8=T9Ab#2K_#zi=$igyK48@;V|W`fg~7;+!q8)aCOo{HA@vpSy-4`^!ze6-~8|QE||hC{ICKllG9fbg_Y7v z$jn{00!ob3!@~-Z%!rSZ0JO#@>|3k10mLK0JRKP-Cc8UYFu>z93=Ab-r^oL2 zl`-&VBh#=-?{l1TatC;VweM^=M7-DUE>m+xO7Xi6vTEsReyLs8KJ+2GZ&rxw$d4IT zPXy6pu^4#e;;ZTsgmG+ZPx>piodegkx2n0}SM77+Y*j^~ICvp#2wj^BuqRY*&cjmL zcKp78aZt>e{3YBb4!J_2|K~A`lN=u&5j!byw`1itV(+Q_?RvV7&Z5XS1HF)L2v6ji z&kOEPmv+k_lSXb{$)of~(BkO^py&7oOzpjdG>vI1kcm_oPFHy38%D4&A4h_CSo#lX z2#oqMCTEP7UvUR3mwkPxbl8AMW(e{ARi@HCYLPSHE^L<1I}OgZD{I#YH#GKnpRmW3 z2jkz~Sa(D)f?V?$gNi?6)Y;Sm{&?~2p=0&BUl_(@hYeX8YjaRO=IqO7neK0RsSNdYjD zaw$g2sG(>JR=8Iz1SK4`*kqd_3-?;_BIcaaMd^}<@MYbYisWZm2C2|Np_l|8r9yM|JkUngSo@?wci(7&O9a z%|V(4C1c9pps0xxzPbXH=}QTxc2rr7fXk$9`a6TbWKPCz&p=VsB8^W96W=BsB|7bc zf(QR8&Ktj*iz)wK&mW`#V%4XTM&jWNnDF56O+2bo<3|NyUhQ%#OZE8$Uv2a@J>D%t zMVMiHh?es!Ex19q&6eC&L=XDU_BA&uR^^w>fpz2_`U87q_?N2y;!Z!bjoeKrzfC)} z?m^PM=(z{%n9K`p|7Bz$LuC7!>tFOuN74MFELm}OD9?%jpT>38J;=1Y-VWtZAscaI z_8jUZ#GwWz{JqvGEUmL?G#l5E=*m>`cY?m*XOc*yOCNtpuIGD+Z|kn4Xww=BLrNYS zGO=wQh}Gtr|7DGXLF%|`G>J~l{k^*{;S-Zhq|&HO7rC_r;o`gTB7)uMZ|WWIn@e0( zX$MccUMv3ABg^$%_lNrgU{EVi8O^UyGHPNRt%R!1#MQJn41aD|_93NsBQhP80yP<9 zG4(&0u7AtJJXLPcqzjv`S~5;Q|5TVGccN=Uzm}K{v)?f7W!230C<``9(64}D2raRU zAW5bp%}VEo{4Rko`bD%Ehf=0voW?-4Mk#d3_pXTF!-TyIt6U+({6OXWVAa;s-`Ta5 zTqx&8msH3+DLrVmQOTBOAj=uoxKYT3DS1^zBXM?1W+7gI!aQNPYfUl{3;PzS9*F7g zWJN8x?KjBDx^V&6iCY8o_gslO16=kh(|Gp)kz8qlQ`dzxQv;)V&t+B}wwdi~uBs4? zu~G|}y!`3;8#vIMUdyC7YEx6bb^1o}G!Jky4cN?BV9ejBfN<&!4M)L&lRKiuMS#3} z_B}Nkv+zzxhy{dYCW$oGC&J(Ty&7%=5B$sD0bkuPmj7g>|962`(Q{ZZMDv%YMuT^KweiRDvYTEop3IgFv#)(w>1 zSzH>J`q!LK)c(AK>&Ib)A{g`Fdykxqd`Yq@yB}E{gnQV$K!}RsgMGWqC3DKE(=!{}ekB3+(1?g}xF>^icEJbc z5bdxAPkW90atZT+&*7qoLqL#p=>t-(-lsnl2XMpZcYeW|o|a322&)yO_8p(&Sw{|b zn(tY$xn5yS$DD)UYS%sP?c|z>1dp!QUD)l;aW#`%qMtQJjE!s2z`+bTSZmLK7SvCR z=@I4|U^sCwZLQSfd*ACw9B@`1c1|&i^W_OD(570SDLK`MD0wTiR8|$7+%{cF&){$G zU~|$^Ed?TIxyw{1$e|D$050n8AjJvvOWhLtLHbSB|HIfjMp+gu>DraHZJRrdO53(= z+o-f{+qNog+qSLB%KY;5>Av6X(>-qYk3IIEwZ5~6a+P9lMpC^ z8CJ0q>rEpjlsxCvJm=kms@tlN4+sv}He`xkr`S}bGih4t`+#VEIt{1veE z{ZLtb_pSbcfcYPf4=T1+|BtR!x5|X#x2TZEEkUB6kslKAE;x)*0x~ES0kl4Dex4e- zT2P~|lT^vUnMp{7e4OExfxak0EE$Hcw;D$ehTV4a6hqxru0$|Mo``>*a5=1Ym0u>BDJKO|=TEWJ5jZu!W}t$Kv{1!q`4Sn7 zrxRQOt>^6}Iz@%gA3&=5r;Lp=N@WKW;>O!eGIj#J;&>+3va^~GXRHCY2}*g#9ULab zitCJt-OV0*D_Q3Q`p1_+GbPxRtV_T`jyATjax<;zZ?;S+VD}a(aN7j?4<~>BkHK7bO8_Vqfdq1#W&p~2H z&w-gJB4?;Q&pG9%8P(oOGZ#`!m>qAeE)SeL*t8KL|1oe;#+uOK6w&PqSDhw^9-&Fa zuEzbi!!7|YhlWhqmiUm!muO(F8-F7|r#5lU8d0+=;<`{$mS=AnAo4Zb^{%p}*gZL! zeE!#-zg0FWsSnablw!9$<&K(#z!XOW z;*BVx2_+H#`1b@>RtY@=KqD)63brP+`Cm$L1@ArAddNS1oP8UE$p05R=bvZoYz+^6 z<)!v7pRvi!u_-V?!d}XWQR1~0q(H3{d^4JGa=W#^Z<@TvI6J*lk!A zZ*UIKj*hyO#5akL*Bx6iPKvR3_2-^2mw|Rh-3O_SGN3V9GRo52Q;JnW{iTGqb9W99 z7_+F(Op6>~3P-?Q8LTZ-lwB}xh*@J2Ni5HhUI3`ct|*W#pqb>8i*TXOLn~GlYECIj zhLaa_rBH|1jgi(S%~31Xm{NB!30*mcsF_wgOY2N0XjG_`kFB+uQuJbBm3bIM$qhUyE&$_u$gb zpK_r{99svp3N3p4yHHS=#csK@j9ql*>j0X=+cD2dj<^Wiu@i>c_v zK|ovi7}@4sVB#bzq$n3`EgI?~xDmkCW=2&^tD5RuaSNHf@Y!5C(Is$hd6cuyoK|;d zO}w2AqJPS`Zq+(mc*^%6qe>1d&(n&~()6-ZATASNPsJ|XnxelLkz8r1x@c2XS)R*H(_B=IN>JeQUR;T=i3<^~;$<+8W*eRKWGt7c#>N`@;#!`kZ!P!&{9J1>_g8Zj zXEXxmA=^{8A|3=Au+LfxIWra)4p<}1LYd_$1KI0r3o~s1N(x#QYgvL4#2{z8`=mXy zQD#iJ0itk1d@Iy*DtXw)Wz!H@G2St?QZFz zVPkM%H8Cd2EZS?teQN*Ecnu|PrC!a7F_XX}AzfZl3fXfhBtc2-)zaC2eKx*{XdM~QUo4IwcGgVdW69 z1UrSAqqMALf^2|(I}hgo38l|Ur=-SC*^Bo5ej`hb;C$@3%NFxx5{cxXUMnTyaX{>~ zjL~xm;*`d08bG_K3-E+TI>#oqIN2=An(C6aJ*MrKlxj?-;G zICL$hi>`F%{xd%V{$NhisHSL~R>f!F7AWR&7b~TgLu6!3s#~8|VKIX)KtqTH5aZ8j zY?wY)XH~1_a3&>#j7N}0az+HZ;is;Zw(Am{MX}YhDTe(t{ZZ;TG}2qWYO+hdX}vp9 z@uIRR8g#y~-^E`Qyem(31{H0&V?GLdq9LEOb2(ea#e-$_`5Q{T%E?W(6 z(XbX*Ck%TQM;9V2LL}*Tf`yzai{0@pYMwBu%(I@wTY!;kMrzcfq0w?X`+y@0ah510 zQX5SU(I!*Fag4U6a7Lw%LL;L*PQ}2v2WwYF(lHx_Uz2ceI$mnZ7*eZ?RFO8UvKI0H z9Pq-mB`mEqn6n_W9(s~Jt_D~j!Ln9HA)P;owD-l~9FYszs)oEKShF9Zzcmnb8kZ7% zQ`>}ki1kwUO3j~ zEmh140sOkA9v>j@#56ymn_RnSF`p@9cO1XkQy6_Kog?0ivZDb`QWOX@tjMd@^Qr(p z!sFN=A)QZm!sTh(#q%O{Ovl{IxkF!&+A)w2@50=?a-+VuZt6On1;d4YtUDW{YNDN_ zG@_jZi1IlW8cck{uHg^g=H58lPQ^HwnybWy@@8iw%G! zwB9qVGt_?~M*nFAKd|{cGg+8`+w{j_^;nD>IrPf-S%YjBslSEDxgKH{5p)3LNr!lD z4ii)^%d&cCXIU7UK?^ZQwmD(RCd=?OxmY(Ko#+#CsTLT;p#A%{;t5YpHFWgl+@)N1 zZ5VDyB;+TN+g@u~{UrWrv)&#u~k$S&GeW)G{M#&Di)LdYk?{($Cq zZGMKeYW)aMtjmKgvF0Tg>Mmkf9IB#2tYmH-s%D_9y3{tfFmX1BSMtbe<(yqAyWX60 zzkgSgKb3c{QPG2MalYp`7mIrYg|Y<4Jk?XvJK)?|Ecr+)oNf}XLPuTZK%W>;<|r+% zTNViRI|{sf1v7CsWHvFrkQ$F7+FbqPQ#Bj7XX=#M(a~9^80}~l-DueX#;b}Ajn3VE z{BWI}$q{XcQ3g{(p>IOzFcAMDG0xL)H%wA)<(gl3I-oVhK~u_m=hAr&oeo|4lZbf} z+pe)c34Am<=z@5!2;_lwya;l?xV5&kWe}*5uBvckm(d|7R>&(iJNa6Y05SvlZcWBlE{{%2- z`86)Y5?H!**?{QbzGG~|k2O%eA8q=gxx-3}&Csf6<9BsiXC)T;x4YmbBIkNf;0Nd5 z%whM^!K+9zH>on_<&>Ws?^v-EyNE)}4g$Fk?Z#748e+GFp)QrQQETx@u6(1fk2!(W zWiCF~MomG*y4@Zk;h#2H8S@&@xwBIs|82R*^K(i*0MTE%Rz4rgO&$R zo9Neb;}_ulaCcdn3i17MO3NxzyJ=l;LU*N9ztBJ30j=+?6>N4{9YXg$m=^9@Cl9VY zbo^{yS@gU=)EpQ#;UIQBpf&zfCA;00H-ee=1+TRw@(h%W=)7WYSb5a%$UqNS@oI@= zDrq|+Y9e&SmZrH^iA>Of8(9~Cf-G(P^5Xb%dDgMMIl8gk6zdyh`D3OGNVV4P9X|EvIhplXDld8d z^YWtYUz@tpg*38Xys2?zj$F8%ivA47cGSl;hjD23#*62w3+fwxNE7M7zVK?x_`dBSgPK zWY_~wF~OEZi9|~CSH8}Xi>#8G73!QLCAh58W+KMJJC81{60?&~BM_0t-u|VsPBxn* zW7viEKwBBTsn_A{g@1!wnJ8@&h&d>!qAe+j_$$Vk;OJq`hrjzEE8Wjtm)Z>h=*M25 zOgETOM9-8xuuZ&^@rLObtcz>%iWe%!uGV09nUZ*nxJAY%&KAYGY}U1WChFik7HIw% zZP$3Bx|TG_`~19XV7kfi2GaBEhKap&)Q<9`aPs#^!kMjtPb|+-fX66z3^E)iwyXK7 z8)_p<)O{|i&!qxtgBvWXx8*69WO$5zACl++1qa;)0zlXf`eKWl!0zV&I`8?sG)OD2Vy?reNN<{eK+_ za4M;Hh%&IszR%)&gpgRCP}yheQ+l#AS-GnY81M!kzhWxIR?PW`G3G?} z$d%J28uQIuK@QxzGMKU_;r8P0+oIjM+k)&lZ39i#(ntY)*B$fdJnQ3Hw3Lsi8z&V+ zZly2}(Uzpt2aOubRjttzqrvinBFH4jrN)f0hy)tj4__UTwN)#1fj3-&dC_Vh7}ri* zfJ=oqLMJ-_<#rwVyN}_a-rFBe2>U;;1(7UKH!$L??zTbbzP#bvyg7OQBGQklJ~DgP zd<1?RJ<}8lWwSL)`jM53iG+}y2`_yUvC!JkMpbZyb&50V3sR~u+lok zT0uFRS-yx@8q4fPRZ%KIpLp8R#;2%c&Ra4p(GWRT4)qLaPNxa&?8!LRVdOUZ)2vrh zBSx&kB%#Y4!+>~)<&c>D$O}!$o{<1AB$M7-^`h!eW;c(3J~ztoOgy6Ek8Pwu5Y`Xion zFl9fb!k2`3uHPAbd(D^IZmwR5d8D$495nN2`Ue&`W;M-nlb8T-OVKt|fHk zBpjX$a(IR6*-swdNk@#}G?k6F-~c{AE0EWoZ?H|ZpkBxqU<0NUtvubJtwJ1mHV%9v?GdDw; zAyXZiD}f0Zdt-cl9(P1la+vQ$Er0~v}gYJVwQazv zH#+Z%2CIfOf90fNMGos|{zf&N`c0@x0N`tkFv|_9af3~<0z@mnf*e;%r*Fbuwl-IW z{}B3=(mJ#iwLIPiUP`J3SoP~#)6v;aRXJ)A-pD2?_2_CZ#}SAZ<#v7&Vk6{*i(~|5 z9v^nC`T6o`CN*n%&9+bopj^r|E(|pul;|q6m7Tx+U|UMjWK8o-lBSgc3ZF=rP{|l9 zc&R$4+-UG6i}c==!;I#8aDIbAvgLuB66CQLRoTMu~jdw`fPlKy@AKYWS-xyZzPg&JRAa@m-H43*+ne!8B7)HkQY4 zIh}NL4Q79a-`x;I_^>s$Z4J4-Ngq=XNWQ>yAUCoe&SMAYowP>r_O}S=V+3=3&(O=h zNJDYNs*R3Y{WLmBHc?mFEeA4`0Y`_CN%?8qbDvG2m}kMAiqCv`_BK z_6a@n`$#w6Csr@e2YsMx8udNWtNt=kcqDZdWZ-lGA$?1PA*f4?X*)hjn{sSo8!bHz zb&lGdAgBx@iTNPK#T_wy`KvOIZvTWqSHb=gWUCKXAiB5ckQI`1KkPx{{%1R*F2)Oc z(9p@yG{fRSWE*M9cdbrO^)8vQ2U`H6M>V$gK*rz!&f%@3t*d-r3mSW>D;wYxOhUul zk~~&ip5B$mZ~-F1orsq<|1bc3Zpw6)Ws5;4)HilsN;1tx;N6)tuePw& z==OlmaN*ybM&-V`yt|;vDz(_+UZ0m&&9#{9O|?0I|4j1YCMW;fXm}YT$0%EZ5^YEI z4i9WV*JBmEU{qz5O{#bs`R1wU%W$qKx?bC|e-iS&d*Qm7S=l~bMT{~m3iZl+PIXq{ zn-c~|l)*|NWLM%ysfTV-oR0AJ3O>=uB-vpld{V|cWFhI~sx>ciV9sPkC*3i0Gg_9G!=4ar*-W?D9)?EFL1=;O+W8}WGdp8TT!Fgv z{HKD`W>t(`Cds_qliEzuE!r{ihwEv1l5o~iqlgjAyGBi)$%zNvl~fSlg@M=C{TE;V zQkH`zS8b&!ut(m)%4n2E6MB>p*4(oV>+PT51#I{OXs9j1vo>9I<4CL1kv1aurV*AFZ^w_qfVL*G2rG@D2 zrs87oV3#mf8^E5hd_b$IXfH6vHe&lm@7On~Nkcq~YtE!}ad~?5*?X*>y`o;6Q9lkk zmf%TYonZM`{vJg$`lt@MXsg%*&zZZ0uUSse8o=!=bfr&DV)9Y6$c!2$NHyYAQf*Rs zk{^?gl9E z5Im8wlAsvQ6C2?DyG@95gUXZ3?pPijug25g;#(esF_~3uCj3~94}b*L>N2GSk%Qst z=w|Z>UX$m!ZOd(xV*2xvWjN&c5BVEdVZ0wvmk)I+YxnyK%l~caR=7uNQ=+cnNTLZ@&M!I$Mj-r{!P=; z`C2)D=VmvK8@T5S9JZoRtN!S*D_oqOxyy!q6Zk|~4aT|*iRN)fL)c>-yycR>-is0X zKrko-iZw(f(!}dEa?hef5yl%p0-v-8#8CX8!W#n2KNyT--^3hq6r&`)5Y@>}e^4h- zlPiDT^zt}Ynk&x@F8R&=)k8j$=N{w9qUcIc&)Qo9u4Y(Ae@9tA`3oglxjj6c{^pN( zQH+Uds2=9WKjH#KBIwrQI%bbs`mP=7V>rs$KG4|}>dxl_k!}3ZSKeEen4Iswt96GGw`E6^5Ov)VyyY}@itlj&sao|>Sb5 zeY+#1EK(}iaYI~EaHQkh7Uh>DnzcfIKv8ygx1Dv`8N8a6m+AcTa-f;17RiEed>?RT zk=dAksmFYPMV1vIS(Qc6tUO+`1jRZ}tcDP? zt)=7B?yK2RcAd1+Y!$K5*ds=SD;EEqCMG6+OqPoj{&8Y5IqP(&@zq@=A7+X|JBRi4 zMv!czlMPz)gt-St2VZwDD=w_S>gRpc-g zUd*J3>bXeZ?Psjohe;z7k|d<*T21PA1i)AOi8iMRwTBSCd0ses{)Q`9o&p9rsKeLaiY zluBw{1r_IFKR76YCAfl&_S1*(yFW8HM^T()&p#6y%{(j7Qu56^ZJx1LnN`-RTwimdnuo*M8N1ISl+$C-%=HLG-s} zc99>IXRG#FEWqSV9@GFW$V8!{>=lSO%v@X*pz*7()xb>=yz{E$3VE;e)_Ok@A*~El zV$sYm=}uNlUxV~6e<6LtYli1!^X!Ii$L~j4e{sI$tq_A(OkGquC$+>Rw3NFObV2Z)3Rt~Jr{oYGnZaFZ^g5TDZlg;gaeIP} z!7;T{(9h7mv{s@piF{-35L=Ea%kOp;^j|b5ZC#xvD^^n#vPH=)lopYz1n?Kt;vZmJ z!FP>Gs7=W{sva+aO9S}jh0vBs+|(B6Jf7t4F^jO3su;M13I{2rd8PJjQe1JyBUJ5v zcT%>D?8^Kp-70bP8*rulxlm)SySQhG$Pz*bo@mb5bvpLAEp${?r^2!Wl*6d7+0Hs_ zGPaC~w0E!bf1qFLDM@}zso7i~(``)H)zRgcExT_2#!YOPtBVN5Hf5~Ll3f~rWZ(UsJtM?O*cA1_W0)&qz%{bDoA}{$S&-r;0iIkIjbY~ zaAqH45I&ALpP=9Vof4OapFB`+_PLDd-0hMqCQq08>6G+C;9R~}Ug_nm?hhdkK$xpI zgXl24{4jq(!gPr2bGtq+hyd3%Fg%nofK`psHMs}EFh@}sdWCd!5NMs)eZg`ZlS#O0 zru6b8#NClS(25tXqnl{|Ax@RvzEG!+esNW-VRxba(f`}hGoqci$U(g30i}2w9`&z= zb8XjQLGN!REzGx)mg~RSBaU{KCPvQx8)|TNf|Oi8KWgv{7^tu}pZq|BS&S<53fC2K4Fw6>M^s$R$}LD*sUxdy6Pf5YKDbVet;P!bw5Al-8I1Nr(`SAubX5^D9hk6$agWpF}T#Bdf{b9-F#2WVO*5N zp+5uGgADy7m!hAcFz{-sS0kM7O)qq*rC!>W@St~^OW@R1wr{ajyYZq5H!T?P0e+)a zaQ%IL@X_`hzp~vRH0yUblo`#g`LMC%9}P;TGt+I7qNcBSe&tLGL4zqZqB!Bfl%SUa z6-J_XLrnm*WA`34&mF+&e1sPCP9=deazrM=Pc4Bn(nV;X%HG^4%Afv4CI~&l!Sjzb z{rHZ3od0!Al{}oBO>F*mOFAJrz>gX-vs!7>+_G%BB(ljWh$252j1h;9p~xVA=9_`P z5KoFiz96_QsTK%B&>MSXEYh`|U5PjX1(+4b#1PufXRJ*uZ*KWdth1<0 zsAmgjT%bowLyNDv7bTUGy|g~N34I-?lqxOUtFpTLSV6?o?<7-UFy*`-BEUsrdANh} zBWkDt2SAcGHRiqz)x!iVoB~&t?$yn6b#T=SP6Ou8lW=B>=>@ik93LaBL56ub`>Uo!>0@O8?e)$t(sgy$I z6tk3nS@yFFBC#aFf?!d_3;%>wHR;A3f2SP?Na8~$r5C1N(>-ME@HOpv4B|Ty7%jAv zR}GJwsiJZ5@H+D$^Cwj#0XA_(m^COZl8y7Vv(k=iav1=%QgBOVzeAiw zaDzzdrxzj%sE^c9_uM5D;$A_7)Ln}BvBx^=)fO+${ou%B*u$(IzVr-gH3=zL6La;G zu0Kzy5CLyNGoKRtK=G0-w|tnwI)puPDOakRzG(}R9fl7#<|oQEX;E#yCWVg95 z;NzWbyF&wGg_k+_4x4=z1GUcn6JrdX4nOVGaAQ8#^Ga>aFvajQN{!+9rgO-dHP zIp@%&ebVg}IqnRWwZRTNxLds+gz2@~VU(HI=?Epw>?yiEdZ>MjajqlO>2KDxA>)cj z2|k%dhh%d8SijIo1~20*5YT1eZTDkN2rc^zWr!2`5}f<2f%M_$to*3?Ok>e9$X>AV z2jYmfAd)s|(h?|B(XYrIfl=Wa_lBvk9R1KaP{90-z{xKi+&8=dI$W0+qzX|ZovWGOotP+vvYR(o=jo?k1=oG?%;pSqxcU* zWVGVMw?z__XQ9mnP!hziHC`ChGD{k#SqEn*ph6l46PZVkm>JF^Q{p&0=MKy_6apts z`}%_y+Tl_dSP(;Ja&sih$>qBH;bG;4;75)jUoVqw^}ee=ciV;0#t09AOhB^Py7`NC z-m+ybq1>_OO+V*Z>dhk}QFKA8V?9Mc4WSpzj{6IWfFpF7l^au#r7&^BK2Ac7vCkCn{m0uuN93Ee&rXfl1NBY4NnO9lFUp zY++C1I;_{#OH#TeP2Dp?l4KOF8ub?m6zE@XOB5Aiu$E~QNBM@;r+A5mF2W1-c7>ex zHiB=WJ&|`6wDq*+xv8UNLVUy4uW1OT>ey~Xgj@MMpS@wQbHAh>ysYvdl-1YH@&+Q! z075(Qd4C!V`9Q9jI4 zSt{HJRvZec>vaL_brKhQQwbpQd4_Lmmr0@1GdUeU-QcC{{8o=@nwwf>+dIKFVzPriGNX4VjHCa zTbL9w{Y2V87c2ofX%`(48A+4~mYTiFFl!e{3K^C_k%{&QTsgOd0*95KmWN)P}m zTRr{`f7@=v#+z_&fKYkQT!mJn{*crj%ZJz#(+c?>cD&2Lo~FFAWy&UG*Op^pV`BR^I|g?T>4l5;b|5OQ@t*?_Slp`*~Y3`&RfKD^1uLezIW(cE-Dq2z%I zBi8bWsz0857`6e!ahet}1>`9cYyIa{pe53Kl?8|Qg2RGrx@AlvG3HAL-^9c^1GW;)vQt8IK+ zM>!IW*~682A~MDlyCukldMd;8P|JCZ&oNL(;HZgJ>ie1PlaInK7C@Jg{3kMKYui?e!b`(&?t6PTb5UPrW-6DVU%^@^E`*y-Fd(p|`+JH&MzfEq;kikdse ziFOiDWH(D< zyV7Rxt^D0_N{v?O53N$a2gu%1pxbeK;&ua`ZkgSic~$+zvt~|1Yb=UfKJW2F7wC^evlPf(*El+#}ZBy0d4kbVJsK- z05>;>?HZO(YBF&v5tNv_WcI@O@LKFl*VO?L(!BAd!KbkVzo;v@~3v`-816GG?P zY+H3ujC>5=Am3RIZDdT#0G5A6xe`vGCNq88ZC1aVXafJkUlcYmHE^+Z{*S->ol%-O znm9R0TYTr2w*N8Vs#s-5=^w*{Y}qp5GG)Yt1oLNsH7y~N@>Eghms|K*Sdt_u!&I}$ z+GSdFTpbz%KH+?B%Ncy;C`uW6oWI46(tk>r|5|-K6)?O0d_neghUUOa9BXHP*>vi; z={&jIGMn-92HvInCMJcyXwHTJ42FZp&Wxu+9Rx;1x(EcIQwPUQ@YEQQ`bbMy4q3hP zNFoq~Qd0=|xS-R}k1Im3;8s{BnS!iaHIMLx)aITl)+)?Yt#fov|Eh>}dv@o6R{tG>uHsy&jGmWN5+*wAik|78(b?jtysPHC#e+Bzz~V zS3eEXv7!Qn4uWi!FS3B?afdD*{fr9>B~&tc671fi--V}~E4un;Q|PzZRwk-azprM$4AesvUb5`S`(5x#5VJ~4%ET6&%GR$}muHV-5lTsCi_R|6KM(g2PCD@|yOpKluT zakH!1V7nKN)?6JmC-zJoA#ciFux8!)ajiY%K#RtEg$gm1#oKUKX_Ms^%hvKWi|B=~ zLbl-L)-=`bfhl`>m!^sRR{}cP`Oim-{7}oz4p@>Y(FF5FUEOfMwO!ft6YytF`iZRq zfFr{!&0Efqa{1k|bZ4KLox;&V@ZW$997;+Ld8Yle91he{BfjRhjFTFv&^YuBr^&Pe zswA|Bn$vtifycN8Lxr`D7!Kygd7CuQyWqf}Q_PM}cX~S1$-6xUD%-jrSi24sBTFNz(Fy{QL2AmNbaVggWOhP;UY4D>S zqKr!UggZ9Pl9Nh_H;qI`-WoH{ceXj?m8y==MGY`AOJ7l0Uu z)>M%?dtaz2rjn1SW3k+p`1vs&lwb%msw8R!5nLS;upDSxViY98IIbxnh{}mRfEp=9 zbrPl>HEJeN7J=KnB6?dwEA6YMs~chHNG?pJsEj#&iUubdf3JJwu=C(t?JpE6xMyhA3e}SRhunDC zn-~83*9=mADUsk^sCc%&&G1q5T^HR9$P#2DejaG`Ui*z1hI#h7dwpIXg)C{8s< z%^#@uQRAg-$z&fmnYc$Duw63_Zopx|n{Bv*9Xau{a)2%?H<6D>kYY7_)e>OFT<6TT z0A}MQLgXbC2uf`;67`mhlcUhtXd)Kbc$PMm=|V}h;*_%vCw4L6r>3Vi)lE5`8hkSg zNGmW-BAOO)(W((6*e_tW&I>Nt9B$xynx|sj^ux~?q?J@F$L4;rnm_xy8E*JYwO-02u9_@@W0_2@?B@1J{y~Q39N3NX^t7#`=34Wh)X~sU&uZWgS1Z09%_k|EjA4w_QqPdY`oIdv$dJZ;(!k)#U8L+|y~gCzn+6WmFt#d{OUuKHqh1-uX_p*Af8pFYkYvKPKBxyid4KHc}H` z*KcyY;=@wzXYR{`d{6RYPhapShXIV?0cg_?ahZ7do)Ot#mxgXYJYx}<%E1pX;zqHd zf!c(onm{~#!O$2`VIXezECAHVd|`vyP)Uyt^-075X@NZDBaQt<>trA3nY-Dayki4S zZ^j6CCmx1r46`4G9794j-WC0&R9(G7kskS>=y${j-2;(BuIZTLDmAyWTG~`0)Bxqk zd{NkDe9ug|ms@0A>JVmB-IDuse9h?z9nw!U6tr7t-Lri5H`?TjpV~8(gZWFq4Vru4 z!86bDB;3lpV%{rZ`3gtmcRH1hjj!loI9jN>6stN6A*ujt!~s!2Q+U1(EFQEQb(h4E z6VKuRouEH`G6+8Qv2C)K@^;ldIuMVXdDDu}-!7FS8~k^&+}e9EXgx~)4V4~o6P^52 z)a|`J-fOirL^oK}tqD@pqBZi_;7N43%{IQ{v&G9^Y^1?SesL`;Z(dt!nn9Oj5Odde%opv&t zxJ><~b#m+^KV&b?R#)fRi;eyqAJ_0(nL*61yPkJGt;gZxSHY#t>ATnEl-E%q$E16% zZdQfvhm5B((y4E3Hk6cBdwGdDy?i5CqBlCVHZr-rI$B#>Tbi4}Gcvyg_~2=6O9D-8 zY2|tKrNzbVR$h57R?Pe+gUU_il}ZaWu|Az#QO@};=|(L-RVf0AIW zq#pO+RfM7tdV`9lI6g;{qABNId`fG%U9Va^ravVT^)CklDcx)YJKeJdGpM{W1v8jg z@&N+mR?BPB=K1}kNwXk_pj44sd>&^;d!Z~P>O78emE@Qp@&8PyB^^4^2f7e)gekMv z2aZNvP@;%i{+_~>jK7*2wQc6nseT^n6St9KG#1~Y@$~zR_=AcO2hF5lCoH|M&c{vR zSp(GRVVl=T*m~dIA;HvYm8HOdCkW&&4M~UDd^H)`p__!4k+6b)yG0Zcek8OLw$C^K z3-BbLiG_%qX|ZYpXJ$(c@aa7b4-*IQkDF}=gZSV`*ljP|5mWuHSCcf$5qqhZTv&P?I$z^>}qP(q!Aku2yA5vu38d8x*q{6-1`%PrE_r0-9Qo?a#7Zbz#iGI7K<(@k^|i4QJ1H z4jx?{rZbgV!me2VT72@nBjucoT zUM9;Y%TCoDop?Q5fEQ35bCYk7!;gH*;t9t-QHLXGmUF;|vm365#X)6b2Njsyf1h9JW#x$;@x5Nx2$K$Z-O3txa%;OEbOn6xBzd4n4v)Va=sj5 z%rb#j7{_??Tjb8(Hac<^&s^V{yO-BL*uSUk2;X4xt%NC8SjO-3?;Lzld{gM5A=9AV z)DBu-Z8rRvXXwSVDH|dL-3FODWhfe1C_iF``F05e{dl(MmS|W%k-j)!7(ARkV?6r~ zF=o42y+VapxdZn;GnzZfGu<6oG-gQ7j7Zvgo7Am@jYxC2FpS@I;Jb%EyaJDBQC(q% zKlZ}TVu!>;i3t~OAgl@QYy1X|T~D{HOyaS*Bh}A}S#a9MYS{XV{R-|niEB*W%GPW! zP^NU(L<}>Uab<;)#H)rYbnqt|dOK(-DCnY==%d~y(1*{D{Eo1cqIV8*iMfx&J*%yh zx=+WHjt0q2m*pLx8=--UqfM6ZWjkev>W-*}_*$Y(bikH`#-Gn#!6_ zIA&kxn;XYI;eN9yvqztK-a113A%97in5CL5Z&#VsQ4=fyf&3MeKu70)(x^z_uw*RG zo2Pv&+81u*DjMO6>Mrr7vKE2CONqR6C0(*;@4FBM;jPIiuTuhQ-0&C)JIzo_k>TaS zN_hB;_G=JJJvGGpB?uGgSeKaix~AkNtYky4P7GDTW6{rW{}V9K)Cn^vBYKe*OmP!; zohJs=l-0sv5&pL6-bowk~(swtdRBZQHh8)m^r2+qTtZ zt4m$B?OQYNyfBA0E)g28a*{)a=%%f-?{F;++-Xs#5|7kSHTD*E9@$V ztE%7zX4A(L`n)FY8Y4pOnKC|Pf)j$iR#yP;V0+|Hki+D;t4I4BjkfdYliK9Gf6RYw z;3px$Ud5aTd`yq$N7*WOs!{X91hZZ;AJ9iQOH%p;v$R%OQum_h#rq9*{ve(++|24z zh2P;{-Z?u#rOqd0)D^_Ponv(Y9KMB9#?}nJdUX&r_rxF0%3__#8~ZwsyrSPmtWY27 z-54ZquV2t_W!*+%uwC=h-&_q~&nQer0(FL74to%&t^byl^C?wTaZ-IS9OssaQFP)1 zAov0o{?IRAcCf+PjMWSdmP42gysh|c9Ma&Q^?_+>>+-yrC8WR;*XmJ;>r9v*>=W}tgWG;WIt{~L8`gk8DP{dSdG z4SDM7g5ahMHYHHk*|mh9{AKh-qW7X+GEQybJt9A@RV{gaHUAva+=lSroK^NUJYEiL z?X6l9ABpd)9zzA^;FdZ$QQs#uD@hdcaN^;Q=AXlbHv511Meye`p>P4Y2nblEDEeZo}-$@g&L98Aih6tgLz--${eKTxymIipy0xSYgZZ zq^yyS4yNPTtPj-sM?R8@9Q1gtXPqv{$lb5i|C1yymwnGdfYV3nA-;5!Wl zD0fayn!B^grdE?q^}ba{-LIv*Z}+hZm_F9c$$cW!bx2DgJD&6|bBIcL@=}kQA1^Eh zXTEznqk)!!IcTl>ey?V;X8k<+C^DRA{F?T*j0wV`fflrLBQq!l7cbkAUE*6}WabyF zgpb+|tv=aWg0i}9kBL8ZCObYqHEycr5tpc-$|vdvaBsu#lXD@u_e1iL z{h>xMRS0a7KvW?VttrJFpX^5DC4Bv4cp6gNG6#8)7r7IxXfSNSp6)_6tZ4l>(D+0I zPhU)N!sKywaBusHdVE!yo5$20JAU8V_XcW{QmO!p*~ns8{2~bhjydnmA&=r zX9NSM9QYogYMDZ~kS#Qx`mt>AmeR3p@K$`fbJ%LQ1c5lEOz<%BS<}2DL+$>MFcE%e zlxC)heZ7#i80u?32eOJI9oQRz0z;JW@7Th4q}YmQ-`Z?@y3ia^_)7f37QMwDw~<-@ zT)B6fftmK_6YS!?{uaj5lLxyR++u*ZY2Mphm5cd7PA5=%rd)95hJ9+aGSNfjy>Ylc zoI0nGIT3sKmwX8h=6CbvhVO+ehFIR155h8iRuXZx^cW>rq5K4z_dvM#hRER=WR@THs%WELI9uYK9HN44Em2$#@k)hD zicqRPKV#yB;UlcsTL_}zCMK0T;eXHfu`y2(dfwm(v)IBbh|#R>`2cot{m7}8_X&oD zr@94PkMCl%d3FsC4pil=#{3uv^+)pvxfwmPUr)T)T|GcZVD$wVj$mjkjDs`5cm8N! zXVq2CvL;gWGpPI4;9j;2&hS*o+LNp&C5Ac=OXx*W5y6Z^az)^?G0)!_iAfjH5wiSE zD(F}hQZB#tF5iEx@0sS+dP70DbZ*<=5X^)Pxo^8aKzOzuyc2rq=<0-k;Y_ID1>9^v z+)nc36}?>jen*1%OX3R*KRASj${u$gZ$27Hpcj=95kK^aLzxhW6jj_$w6}%#1*$5D zG1H_vYFrCSwrRqYw*9<}OYAOQT)u%9lC`$IjZV<4`9Sc;j{Qv_6+uHrYifK&On4V_7yMil!0Yv55z@dFyD{U@Sy>|vTX=P_( zRm<2xj*Z}B30VAu@0e+}at*y?wXTz|rPalwo?4ZZc>hS0Ky6~mi@kv#?xP2a;yt?5=(-CqvP_3&$KdjB7Ku;# z`GLE*jW1QJB5d&E?IJO?1+!Q8HQMGvv^RuFoi=mM4+^tOqvX%X&viB%Ko2o-v4~~J z267ui;gsW?J=qS=D*@*xJvAy3IOop5bEvfR4MZC>9Y4Z$rGI|EHNNZ7KX;Ix{xSvm z-)Cau-xuTm|7`4kUdXvd_d^E=po(76ELfq5OgxIt3aqDy#zBfIy-5<3gpn{Ce`-ha z<;6y@{Bgqw?c~h*&j{FozQCh=`Lv-5Iw!KdSt;%GDOq%=(V!dJ-}|}|0o5G2kJj6{ z`jCSPs$9Fe8O(+qALZiJ$WtR=<@GvsdM)IJ`7XrBfW0iyYE#Vy^e@zbysg*B5Z_kSL6<)vqoaH zQ{!9!*{e9UZo^h+qZ`T@LfVwAEwc&+9{C8c%oj41q#hyn<&zA9IIur~V|{mmu`n5W z8)-Ou$YgjQ*PMIqHhZ_9E?(uoK0XM5aQkarcp}WT^7b^FC#^i>#8LGZ9puDuXUYas z7caX)V5U6uY-L5Wl%)j$qRkR;7@3T*N64YK_!`Fw=>CAwe~2loI1<>DZW&sb7Q)X;6E08&$h! z2=c1i4UOO{R4TmkTz+o9n`}+%d%blR6P;5{`qjtxlN$~I%tMMDCY`~e{+mRF!rj5( z3ywv)P_PUUqREu)TioPkg&5RKjY6z%pRxQPQ{#GNMTPag^S8(8l{!{WGNs2U1JA-O zq02VeYcArhTAS;v3);k(&6ayCH8SXN@r;1NQeJ*y^NHM+zOd;?t&c!Hq^SR_w6twGV8dl>j zjS+Zc&Yp7cYj&c1y3IxQ%*kWiYypvoh(k8g`HrY<_Bi-r%m-@SLfy-6mobxkWHxyS z>TtM2M4;Uqqy|+8Q++VcEq$PwomV1D4UzNA*Tgkg9#Gpz#~&iPf|Czx!J?qss?e|3 z4gTua75-P{2X7w9eeK3~GE0ip-D;%%gTi)8bR~Ez@)$gpuS~jZs`CrO5SR-Xy7bkA z89fr~mY}u4A$|r1$fe-;T{yJh#9Ime1iRu8eo?uY9@yqAU3P!rx~SsP;LTBL zeoMK(!;(Zt8313 z3)V)q_%eflKW?BnMZa}6E0c7t!$-mC$qt44OME5F(6B$E8w*TUN-h}0dOiXI+TH zYFrr&k1(yO(|J0vP|{22@Z}bxm@7BkjO)f)&^fv|?_JX+s)1*|7X7HH(W?b3QZ3!V|~m?8}uJsF>NvE4@fik zjyyh+U*tt`g6v>k9ub88a;ySvS1QawGn7}aaR**$rJA=a#eUT~ngUbJ%V=qsFIekLbv!YkqjTG{_$F;$w19$(ivIs*1>?2ka%uMOx@B9`LD zhm~)z@u4x*zcM1WhiX)!U{qOjJHt1xs{G1S?rYe)L)ntUu^-(o_dfqZu)}W(X%Uu| zN*qI@&R2fB#Jh|Mi+eMrZDtbNvYD3|v0Kx>E#Ss;Be*T$@DC!2A|mb%d}TTN3J+c= zu@1gTOXFYy972S+=C;#~)Z{Swr0VI5&}WYzH22un_Yg5o%f9fvV(`6!{C<(ZigQ2`wso)cj z9O12k)15^Wuv#rHpe*k5#4vb%c znP+Gjr<-p%01d<+^yrSoG?}F=eI8X;?=Fo2a~HUiJ>L!oE#9tXRp!adg-b9D;(6$E zeW0tH$US04zTX$OxM&X+2ip>KdFM?iG_fgOD-qB|uFng8*#Z5jgqGY=zLU?4!OlO#~YBTB9b9#~H@nqQ#5 z6bV));d?IJTVBC+79>rGuy1JgxPLy$dA7;_^^L)02m}XLjFR*qH`eI~+eJo(7D`LH z(W%lGnGK+Vk_3kyF*zpgO=1MxMg?hxe3}}YI>dVs8l}5eWjYu4=w6MWK09+05 zGdpa#$awd>Q|@aZa*z{5F3xy3n@E4YT9%TmMo0jxW59p0bI?&S}M+ z&^NG%rf7h*m9~p#b19|`wO5OMY-=^XT+=yrfGNpl<&~~FGsx_`IaFn+sEgF$hgOa~oAVAiu^a$jHcqkE=dj`ze z=axsfrzzh6VGD0x#6Ff=t%+VTiq!n6^gv*uIUD<9fOhvR;al5kcY${uunn}-!74<7 zmP^3cl-kyN(QY!!Z-^PY-OUkh=3ZWk6>le$_Q&xk4cgH{?i)C%2RM@pX5Q{jdSlo! zVau5v44cQX5|zQlQDt;dCg)oM0B<=P1CR!W%!^m$!{pKx;bn9DePJjWBX)q!`$;0K zqJIIyD#aK;#-3&Nf=&IhtbV|?ZGYHSphp~6th`p2rkw&((%kBV7<{siEOU7AxJj+FuRdDu$ zcmTW8usU_u!r)#jg|J=Gt{##7;uf4A5cdt6Y02}f(d2)z~ z)CH~gVAOwBLk$ZiIOn}NzDjvfw(w$u|BdCBI#)3xB-Ot?nz?iR38ayCm48M=_#9r7 zw8%pwQ<9mbEs5~_>pN3~#+Er~Q86J+2TDXM6umCbukd-X6pRIr5tF?VauT8jW> zY^#)log>jtJs2s3xoiPB7~8#1ZMv>Zx0}H58k-@H2huNyw~wsl0B8j)H5)H9c7y&i zp8^0;rKbxC1eEZ-#Qxvz)Xv$((8lK9I>BspPajluysw^f#t9P;OUis43mmEzX+lk* zc4T-Ms9_687GR+~QS#0~vxK#DSGN=a-m(@eZTqw2<+lN9>R~gK2)3;sT4%nI%Y|0m zX9SPR!>?~s=j5H4WMqeTW8QaLZ=1bWS5I3xZ&$(ypc=tHrv+hX@s)VG(tc!yvLM7n zshN=C#v={X1r;)xn0Pow_1eMhkn!{;x$BJ#PIz)m585&%cmzk;btQzZAN_^zis;n? z?6I~bN?s;7vg_dtoTc4A5Ow*Rb}No#UYl)sN|RmoYo}k^cKLXd8F`44?RrokkPvN5 ztUrx;U~B;jbE_qGd3n0j2i}A{enJvJ?gSF~NQj~EP5vM-w4@;QQ5n(Npic}XNW6B0 zq9F4T%6kp7qGhd0vpQcz+nMk8GOAmbz8Bt4@GtewGr6_>Xj>ge)SyfY}nu>Y!a@HoIx(StD zx`!>RT&}tpBL%nOF%7XIFW?n1AP*xthCMzhrU6G!U6?m4!CPWTvn#Yaoi_95CT2!L z|B=5zeRW30&ANGN>J9#GtCm&3SF6n4TqDz<-{@ZXkrkRDCpV$DwCtI^e&3i1A{Ar&JZtS^c+lyPa6 z%JJr42S_;eFC#M~bdtQePhOU32WDiZ4@H&af)z#$Y|hnQNb)8(3?1Ad>5uaZ1z zU~!jt3XUI@gpWb8tWTyH7DGvKvzYfqNIy3P{9vpwz_C-QL&`+8Io$F5PS-@YQJoEO z17D9P(+sXajWSH_8&C?fn>rTLX+(?KiwX#JNV)xE0!Q@>Tid$V2#r4y6fkph?YZ>^ z(o^q(0*P->3?I0cELXJn(N|#qTm6 zAPIL~n)m!50;*?5=MOOc4Wk;w(0c$(!e?vpV23S|n|Y7?nyc8)fD8t-KI&nTklH&BzqQ}D(1gH3P+5zGUzIjT~x`;e8JH=86&5&l-DP% z)F+Et(h|GJ?rMy-Zrf>Rv@<3^OrCJ1xv_N*_@-K5=)-jP(}h1Rts44H&ou8!G_C1E zhTfUDASJ2vu!4@j58{NN;78i?6__xR75QEDC4JN{>RmgcNrn-EOpEOcyR<8FS@RB@ zH!R7J=`KK^u06eeI|X@}KvQmdKE3AmAy8 zM4IIvde#e4O(iwag`UL5yQo>6&7^=D4yE-Eo9$9R2hR} zn;Z9i-d=R-xZl4@?s%8|m1M`$J6lW1r0Y)+8q$}Vn4qyR1jqTjGH;@Z!2KiGun2~x zaiEfzVT<|_b6t}~XPeflAm8hvCHP3Bp*tl{^y_e{Jsn@w+KP{7}bH_s=1S2E1sj=18a39*Ag~lbkT^_OQuYQey=b zW^{0xlQ@O$^cSxUZ8l(Mspg8z0cL*?yH4;X2}TdN)uN31A%$3$a=4;{S@h#Y(~i%) zc=K7Ggl=&2hYVic*W65gpSPE70pU;FN@3k?BYdNDKv6wlsBAF^);qiqI zhklsX4TaWiC%VbnZ|yqL+Pcc;(#&E*{+Rx&<&R{uTYCn^OD|mAk4%Q7gbbgMnZwE{ zy7QMK%jIjU@ye?0; z;0--&xVeD}m_hq9A8a}c9WkI2YKj8t!Mkk!o%AQ?|CCBL9}n570}OmZ(w)YI6#QS&p<={tcek*D{CPR%eVA1WBGUXf z%gO2vL7iVDr1$!LAW)1@H>GoIl=&yyZ7=*9;wrOYQ}O}u>h}4FWL?N2ivURlUi11- zl{G0fo`9?$iAEN<4kxa#9e0SZPqa{pw?K=tdN5tRc7HDX-~Ta6_+#s9W&d`6PB7dF*G@|!Mc}i zc=9&T+edI(@la}QU2An#wlkJ&7RmTEMhyC_A8hWM54?s1WldCFuBmT5*I3K9=1aj= z6V@93P-lUou`xmB!ATp0(We$?)p*oQs;(Kku15~q9`-LSl{(Efm&@%(zj?aK2;5}P z{6<@-3^k^5FCDT@Z%XABEcuPoumYkiD&)-8z2Q}HO9OVEU3WM;V^$5r4q>h^m73XF z5!hZ7SCjfxDcXyj(({vg8FU(m2_}36L_yR>fnW)u=`1t@mPa76`2@%8v@2@$N@TE` z)kYhGY1jD;B9V=Dv1>BZhR9IJmB?X9Wj99f@MvJ2Fim*R`rsRilvz_3n!nPFLmj({EP!@CGkY5R*Y_dSO{qto~WerlG}DMw9k+n}pk z*nL~7R2gB{_9=zpqX|*vkU-dx)(j+83uvYGP?K{hr*j2pQsfXn<_As6z%-z+wFLqI zMhTkG>2M}#BLIOZ(ya1y8#W<+uUo@(43=^4@?CX{-hAuaJki(_A(uXD(>`lzuM~M;3XA48ZEN@HRV{1nvt?CV)t;|*dow0Ue2`B*iA&!rI`fZQ=b28= z_dxF}iUQ8}nq0SA4NK@^EQ%=)OY;3fC<$goJ&Kp|APQ@qVbS-MtJQBc)^aO8mYFsbhafeRKdHPW&s^&;%>v zlTz`YE}CuQ@_X&mqm{+{!h2r)fPGeM_Ge4RRYQkrma`&G<>RW<>S(?#LJ}O-t)d$< zf}b0svP^Zu@)MqwEV^Fb_j zPYYs~vmEC~cOIE6Nc^@b@nyL!w5o?nQ!$mGq(Pa|1-MD}K0si<&}eag=}WLSDO zE4+eA~!J(K}605x&4 zT72P7J^)Y)b(3g2MZ@1bv%o1ggwU4Yb!DhQ=uu-;vX+Ix8>#y6wgNKuobvrPNx?$3 zI{BbX<=Y-cBtvY&#MpGTgOLYU4W+csqWZx!=AVMb)Z;8%#1*x_(-)teF>45TCRwi1 z)Nn>hy3_lo44n-4A@=L2gI$yXCK0lPmMuldhLxR8aI;VrHIS{Dk}yp= zwjhB6v@0DN=Hnm~3t>`CtnPzvA*Kumfn5OLg&-m&fObRD};c}Hf?n&mS< z%$wztc%kjWjCf-?+q(bZh9k~(gs?i4`XVfqMXvPVkUWfm4+EBF(nOkg!}4u)6I)JT zU6IXqQk?p1a2(bz^S;6ZH3Wy9!JvbiSr7%c$#G1eK2^=~z1WX+VW)CPD#G~)13~pX zErO(>x$J_4qu-)lNlZkLj2}y$OiKn0ad5Imu5p-2dnt)(YI|b7rJ3TBUQ8FB8=&ym50*ibd2NAbj z;JA&hJ$AJlldM+tO;Yl3rBOFiP8fDdF?t(`gkRpmT9inR@uX{bThYNmxx-LN5K8h0 ztS%w*;V%b`%;-NARbNXn9he&AO4$rvmkB#;aaOx?Wk|yBCmN{oMTK&E)`s&APR<-5 z#;_e75z;LJ)gBG~h<^`SGmw<$Z3p`KG|I@7Pd)sTJnouZ1hRvm3}V+#lPGk4b&A#Y z4VSNi8(R1z7-t=L^%;*;iMTIAjrXl;h106hFrR{n9o8vlz?+*a1P{rEZ2ie{luQs} zr6t746>eoqiO5)^y;4H%2~&FT*Qc*9_oC2$+&syHWsA=rn3B~4#QEW zf4GT3i_@)f(Fj}gAZj`7205M8!B&HhmbgyZB& z+COyAVNxql#DwfP;H48Yc+Y~ChV6b9auLnfXXvpjr<~lQ@>VbCpQvWz=lyVf1??_c zAo3C^otZD@(v?X)UX*@w?TF|F8KF>l7%!Dzu+hksSA^akEkx8QD(V(lK+HBCw6C}2onVExW)f$ zncm*HI(_H;jF@)6eu}Tln!t?ynRkcqBA5MitIM@L^(4_Ke}vy7c%$w{(`&7Rn=u>oDM+Z^RUYcbSOPwT(ONyq76R>$V6_M_UP4vs=__I#io{{((| zy5=k=oVr-Qt$FImP~+&sN8rf2UH*vRMpwohPc@9?id17La4weIfBNa>1Djy+1=ugn z@}Zs;eFY1OC}WBDxDF=i=On_33(jWE-QYV)HbQ^VM!n>Ci9_W0Zofz7!m>do@KH;S z4k}FqEAU2)b%B_B-QcPnM5Zh=dQ+4|DJoJwo?)f2nWBuZE@^>a(gP~ObzMuyNJTgJFUPcH`%9UFA(P23iaKgo0)CI!SZ>35LpFaD7 z)C2sW$ltSEYNW%%j8F;yK{iHI2Q^}coF@LX`=EvxZb*_O;2Z0Z5 z7 zlccxmCfCI;_^awp|G748%Wx%?t9Sh8!V9Y(9$B?9R`G)Nd&snX1j+VpuQ@GGk=y(W zK|<$O`Cad`Y4#W3GKXgs%lZduAd1t1<7LwG4*zaStE*S)XXPFDyKdgiaVXG2)LvDn zf}eQ_S(&2!H0Mq1Yt&WpM1!7b#yt_ie7naOfX129_E=)beKj|p1VW9q>>+e$3@G$K zrB%i_TT1DHjOf7IQ8)Wu4#K%ZSCDGMP7Ab|Kvjq7*~@ewPm~h_-8d4jmNH<&mNZC@CI zKxG5O08|@<4(6IEC@L-lcrrvix&_Dj4tBvl=8A}2UX|)~v#V$L22U}UHk`B-1MF(t zU6aVJWR!>Y0@4m0UA%Sq9B5;4hZvsOu=>L`IU4#3r_t}os|vSDVMA??h>QJ1FD1vR z*@rclvfD!Iqoxh>VP+?b9TVH8g@KjYR@rRWQy44A`f6doIi+8VTP~pa%`(Oa@5?=h z8>YxNvA##a3D0)^P|2|+0~f|UsAJV=q(S>eq-dehQ+T>*Q@qN zU8@kdpU5gGk%ozt?%c8oM6neA?GuSsOfU_b1U)uiEP8eRn~>M$p*R z43nSZs@^ahO78s zulbK@@{3=2=@^yZ)DuIC$ki;`2WNbD_#`LOHN9iMsrgzt-T<8aeh z(oXrqI$Kgt6)Icu=?11NWs>{)_ed1wh>)wv6RYNUA-C&bejw{cBE_5Wzeo!AHdTd+ z)d(_IKN7z^n|As~3XS=cCB_TgM7rK;X586re`{~Foml$aKs zb!4Pe7hEP|370EWwn$HKPM!kL94UPZ1%8B^e5fB+=Iw^6=?5n3tZGYjov83CLB&OQ++p)WCMeshCv_9-~G9C_2x`LxTDjUcW$l6e!6-&a^fM3oP9*g(H zmCk0nGt1UMdU#pfg1G0um5|sc|KO<+qU1E4iBF~RvN*+`7uNHH^gu{?nw2DSCjig% zI@ymKZSK=PhHJa(jW&xeApv&JcfSmNJ4uQ|pY=Lcc>=J|{>5Ug3@x#R_b@55xFgfs za^ANzWdD$ZYtFs$d7+oiw0ZmPk2&l|< zc8()wfiJx@EGpQT zG$8iLkQZ-086doF1R zh<#9cz_vRsJdoXbD=QgOtpm}cFAJX8c}>Jew;PQJSXSb^;wlC zxXLHTS|!GZ-VK_4wV<9bk4RUmlsByzW_^b>)$6R+jQ}^wco1nMA`9Lncs;&QGp!`5Tx#aXXU?}5_RrtUY zx(EMzDhl-a^y^f5yfFLMnOO#u)l69&4M?|ne|2EV>zQ}4JQCBel?~2I4?D|>L$%H(peOOII!U}i z-j)*h1rODe9{0`xmhG;`AKqw1p0_KhEIU8)DoGnEn9wAhXPaxO_(jNSij~J5m$P*$ z9Mt(t;eV}2+i|kjQpBFcNb7_(VbuF<;RQB~R~p>2*Lg>a&7DEEuq*I%Ls4{zHeUDq z+M0&YhEn^C*9-B4Q7HJ$xj)dORCXPK+)ZtLOa0o&)Sl+f(Y{p*68$-#yagW5^HQnQ z0pWpoQpxg8<&gx9im(>=x6v#&RbQ7^AsjxeSDA? zi4MEJUC~ByG!PiBjq7$pK&FA^5 z=Y@dtQnuy%IfsaR`TVP0q^3mixl&J-3!$H!ua#{A>0Z1JdLq#d4UV9nlYm641ZHl zH6mK~iI6lR3OUEVL}Z5{ONZ_6{Nk%Bv03ag<1HVN?R%w2^aR5@E>6(r>}IoMl$wRF zWr-DItN*k7T$NTT8B)+23c?171sADhjInb2Xb>GhFYGC&3{b>huvLlaS4O z^{j5q+b5H?Z)yuy%AByaVl2yj9cnalY1sMQ zXI#e%*CLajxGxP!K6xf9RD2pMHOfAa1d^Lr6kE`IBpxOiGXfNcoQ*FI6wsNtLD!T+ zC4r2q>5qz0f}UY^RY#1^0*FPO*Zp-U1h9U|qWjwqJaDB(pZ`<`U-xo7+JB$zvwV}^ z2>$0&Q5k#l|Er7*PPG1ycj4BGz zg&`d*?nUi1Q!OB>{V@T$A;)8@h;*Rb1{xk_8X<34L`s}xkH-rQZvjM`jI=jaJRGRg zeEcjYChf-78|RLrao%4HyZBfnAx5KaE~@Sx+o-2MLJ>j-6uDb!U`odj*=)0k)K75l zo^)8-iz{_k7-_qy{Ko~N#B`n@o#A22YbKiA>0f3k=p-B~XX=`Ug>jl$e7>I=hph0&AK z?ya;(NaKY_!od=tFUcGU5Kwt!c9EPUQLi;JDCT*{90O@Wc>b| zI;&GIY$JlQW^9?R$-OEUG|3sp+hn+TL(YK?S@ZW<4PQa}=IcUAn_wW3d!r#$B}n08 z*&lf(YN21NDJ74DqwV`l`RX(4zJ<(E4D}N0@QaE-hnfdPDku~@yhb^AeZL73RgovX z6=e>!`&e^l@1WA5h!}}PwwL*Gjg!LbC5g0|qb8H$^S{eGs%cc?4vTyVFW=s6KtfW? z@&Xm+E(uz(qDbwDvRQI9DdB<2sW}FYK9sg*f%-i*>*n{t-_wXvg~N7gM|a91B!x|K zyLbJ~6!!JZpZ`#HpCB8g#Q*~VU47Rp$NyZb3WhEgg3ivSwnjGJgi0BEV?!H}Z@QF| zrO`Kx*52;FR#J-V-;`oR-pr!t>bYf)UYcixN=(FUR6$fhN@~i09^3WeP3*)D*`*mJ z1u%klAbzQ=P4s%|FnVTZv%|@(HDB+ap5S#cFSJUSGkyI*Y>9Lwx|0lTs%uhoCW(f1 zi+|a9;vDPfh3nS<7m~wqTM6+pEm(&z-Ll;lFH!w#(Uk#2>Iv~2Hu}lITn7hnOny`~ z*Vj=r<&Nwpq^@g5m`u&QTBRoK*}plAuHg$L$~NO#wF0!*r0OfcS%)k0A??uY*@B^C zJe9WdU(w){rTIf<;rwJt^_35^d<A@$FqEZW6kwyfAo2x0T$Ye2MZox6Z7<%Qbu$}}u{rtE+h2M+Z}T4I zxF1cwJ(Uvp!T#mogWkhb(?SxD4_#tV(Sc8N4Gu*{Fh#})Pvb^ef%jrlnG*&Ie+J5 zsly5oo?1((um&lLDxn(DkYtk`My>lgKTp3Y4?hTQ4_`YNOFtjF-FUY#d#(EQd(rfz zB8z%Vi;?x)ZM$3c>yc5H8KBvSevnWNdCbAj?QCac)6-K~Xz@EZp}~N9q)5*Ufjz3C z6kkOeI{3H(^VO8hKDrVjy2DXd;5wr4nb`19yJi0DO@607MSx+7F$ zz3F7sl8JV@@sM$6`#JmSilqI%Bs)}Py2eFT;TjcG5?8$zwV60b(_5A>b#uk~7U^bO z>y|6SCrP2IGST(8HFuX|XQUXPLt2gL_hm|uj1Ws`O2VW>SyL^uXkl>Zvkcpi?@!F7 z%svLoT@{R#XrIh^*dE~$YhMwC+b7JE09NAS47kT%Ew zD!XjxA@1+KOAyu`H2z#h+pGm!lG>WI0v745l+Fd><3dh{ATq%h?JSdEt zu%J*zfFUx%Tx&0DS5WSbE)vwZSoAGT=;W#(DoiL($BcK;U*w`xA&kheyMLI673HCb7fGkp{_vdV2uo;vSoAH z9BuLM#Vzwt#rJH>58=KXa#O;*)_N{$>l7`umacQ0g$pI3iW4=L--O;Wiq0zy7OKp`j2r^y3`7X!?sq9rr5B{41BkBr1fEd1#Q3 z-dXc2RSb4U>FvpVhlQCIzQ-hs=8420z=7F2F(^xD;^RXgpjlh8S6*xCP#Gj2+Q0bAg?XARw3dnlQ*Lz3vk}m`HXmCgN=?bIL{T zi}Ds-xn|P)dxhraT@XY$ZQ&^%x8y!o+?n#+>+dZ1c{hYwNTNRke@3enT(a@}V*X{! z81+{Jc2UR;+Zcbc6cUlafh4DFKwp>;M}8SGD+YnW3Q_)*9Z_pny_z+MeYQmz?r%EVaN0d!NE*FVPq&U@vo{ef6wkMIDEWLbDs zz91$($XbGnQ?4WHjB~4xgPgKZts{p|g1B{-4##}#c5aL5C6_RJ_(*5>85B1}U!_<``}q-97Q7~u)(&lsb(WT^(*n7H%33%@_b zO5(?-v??s??33b19xiB7t_YT!q8!qAzN1#RD@3;kYAli%kazt#YN7}MhVu=ljuz27 z1`<+g8oVwy57&$`CiHeaM)tz(OSt4E# zJ@P6E*e504oUw~RD(=9WP8QdW^6wRdFbKII!GAWecJ(?{`EzTR@?j!3g?$@LLCt;U={>!9z7DU!(1Jq zqEwdx5q?W1Ncm7mXP8MFwAr?nw5$H%cb>Q><9j{Tk2RY9ngGvaJgWXx^r!ywk{ph- zs2PFto4@IIwBh{oXe;yMZJYlS?3%a-CJ#js90hoh5W5d^OMwCFmpryHFr|mG+*ZP$ zqyS5BW@s}|3xUO0PR<^{a2M(gkP5BDGxvkWkPudSV*TMRK5Qm4?~VuqVAOerffRt$HGAvp;M++Iq$E6alB z;ykBr-eZ6v_H^1Wip56Czj&=`mb^TsX|FPN#-gnlP03AkiJDM=?y|LzER1M93R4sC z*HT(;EV=*F*>!+Z{r!KG?6ODMGvkt3viG=@kQJHNMYd}bS4KrrHf4`&*(0m0R5Hqz zEk)r=sFeS?MZRvn<@Z0&bDw)XkMnw+_xqgp=W{;ioX`6;G-P9N%wfoYJ$-m$L#MC% z^sH?tSzA|WWP(cN3({~_*X$l{M*;1V{l$;T6b){#l4pswDTid26HaXgKed}13YIP= zJRvA3nmx{}R$Lr&S4!kWU3`~dxM}>VXWu6Xd(VP}z1->h&f%82eXD_TuTs@=c;l0T z|LHmWKJ+?7hkY=YM>t}zvb4|lV;!ARMtWFp!E^J=Asu9w&kVF*i{T#}sY++-qnVh! z5TQ|=>)+vutf{&qB+LO9^jm#rD7E5+tcorr^Fn5Xb0B;)f^$7Ev#}G_`r==ea294V z--v4LwjswWlSq9ba6i?IXr8M_VEGQ$H%hCqJTFQ3+1B9tmxDUhnNU%dy4+zbqYJ|o z3!N{b?A@{;cG2~nb-`|z;gEDL5ffF@oc3`R{fGi)0wtMqEkw4tRX3t;LVS3-zAmg^ zgL7Z{hmdPSz9oA@t>tZ1<|Khn&Lp=_!Q=@a?k+t~H&3jN?dr(}7s;{L+jiKY57?WsFBfW^mu6a03_^VKrdK=9egXw@!nzZ3TbYc*osyQNoCXPYoFS<&Nr97MrQCOK(gO8 z;0@iqRTJy4-RH)PJld5`AJN}n?5r^-enKrHQOR;z>UMfm+e8~4ZL5k>oXMiYq12Bx4eVQv0jFgp_zC#``sjZpywYqISMP}VZ@!~1Mf$!x|opj%mQ98JnSk@`~ zPmmyuPZKtZOnEC!1y!?`TYRsZ!II;d!iln}%e}bk5qIiUADERr*K$3dekgHV9TtBX zi5q!J!6Zgd#cLxRmZN^J`o@Zv{+p+<_#8^nvY)44Hw_2i@?R&5n^q33fpOnDg1nPQ z_r<$hURl~OketX|Tdbvf_7=3x^rSFJtEp@tuDpVB&uq)qW;xUQ7mmkr-@eZwa$l+? zoKk``Vz@TH#>jMce*8>@FZ+@BEUdYa_K0i|{*;j9MW3K%pnM*T;@>|o@lMhgLrpZP5aol(z>g;b4}|e$U~Fn zGL%(}p%Jsl4LxE!VW_Y4T>e}W4e#~F03H_^R!Q)kpJG{lO!@I4{mFo^V#ayHh_5~o zB$O71gcE(G@6xv);#Ky?e(Ed}^O+Ho(t=93T9T3TnEY(OVf_dR-gY@jj+iJSY?q|6prBv(S9A4k=2fNZz!W@S=B@~b?TJRTuBQq448@juN#Y=3q=^VCF>Z}n6wICJ<^^Kn8C;mK zZYiFSN#Z$?NDGV7(#}q2tAZAtE63icK-MY>UQu4MWlGIbJ$AF8Zt-jV;@7P5MPI>% zPWvO!t%1+s>-A%`;0^o8Ezeaa4DMwI8ooQrJ;ax@Qt*6XONWw)dPwOPI9@u*EG&844*1~EoZ2qsAe~M>d`;Bc_CWY zMoDKEmDh-}k9d6*<0g@aQmsnrM1H9IcKYZs)><)d92{|0Hh8?~XbF)7U+UmP@Pw_6geVB?7N$4J4*E0z3EO&5kRS(EE zv92(+e5WxLXMN{h;-|8@!Q#0q247hb^3R%*k3MuMO5*L}$0D#5P*N$aHd54C+=_RToYXTyewugOaDmGsCvb4H1s=@gkfVnzTCWKMa-Mm1v4Wq!t-JIrbV&EWwKDe ze#kJpOq#iRlFz%5#6Fio9IUlKnQ#X&DY8Ux#<-WqxAac-y%U_L+EZZ4Rg5*yNg`f< zSZn&uio@zanUCPqX1l4W&B!;UWs#P7B^|4WwoCxQXl|44n^cBNqu=3Vl*ltAqsUQO z9q_@nD0zq0O8r`coEm>9+|rA3HL#l}X;0##>SJS$cVavOZVCpSGf4mUU1( zWaRCUYc^9QbG9=vpWo%xP}CMFnMb{reA`K7tT(t5DM)d9l}jVPY>qoRzT zE3m-p#=i=$9x*CB`AL>SY}u3agYFl#uULNen#&44H;!L@I{RI=PlWxG8J((f)ma7A z@jLvQ>?Nx`n?3ChRG#HqE3MXP8*o3!Qq`+t8EMt_p)oeKHqPusBxPn!#?R??-=e3e zo73WNs_IZF`WLigre=|`aS2^> zN1zn!7k&Dh28t%VpJ%**&E!eAcB5oLjQFFcJQj*URMia%Ya3@q1UQ18=oWMM6`I}iT_&L1gl?*~6nU4q4Z0`H<5yDp(HeZ+RGf9`mM&= zn-qRp%i!g$R;i1d1aMZ{IewNjE@p2+Z{`x{*xL*x$?WV~{BjJpsP&C&JK0HLoyf z`0z^v&fBQSa!I7FU~9MaQ%e|?RP>sM^2PL!mE^Q1Ig_4M$5BRfi72oMYu6Ke?wmDX z@0a%-V|z}b23K=ye(W+fG#w|jJUnT{=KR5jfuq!RX}<1irTDw(${<&}dWQu4;EuE< z@3u4dBkQaCHHM&;cE0z50_V!(vJ1_V)A8?C#eJuLkt!98Z%|Bgzidc0j|z(&o)TCzYlrgZA zC3@i>L!&Gw_~7`>puB97I2lK)lESZQqVXc_8T^G2O#VHhO?IC$g zOYhXJ7)~C<8l|Xrftka@QuowScM{K&0zskoU$Aw~vIRVRF9TEQ4*3=_5)98B`=t8(N%ZuWqmwlW zllAzq=E5_5!sKDXam@w`ZD(nl%LAPxQuEtDcKPqu9LPJvNIITawU#c^PQ2HmZgs)r zH^+gRwZ?0)8IFQgU)+p@0Iqb^tcEoqcB@zhfz_FaOM&_d<|jnU>q5nSKa<@%9|dje zIupcg1!tRiMP4X=oG<7s4|AW&^-Cw4FL9OuI$t zxjc*y;Uw!G7a|jz>E*2+PlR(CemWebS7m-&*CDwnmxbiRqJvQ&os-sC&4OWt^(2@vG4|jui#Df@-D= zh3D%8Y3R6+jRBStSvH9pt&tCI`NK08J1*pC(?OM0h!bS-JK3I}`pDY-fDIaB_*W6KS+TO0Q*%kkeuN6uWITt=TsCGw6uBE710q; zRluI%j{?@jwhM|l5&TB!-TkQs!A=DXRE>u18t@;zndD0M$U@Igrt?UW2; z7%=dsHIVH_LCkGUU0fW&UMjDnvjcc0Mp(mK&;d~ZJ5EJ)#7@aTZvGDFXzFZg2Lq~s z5PR_LazNN)JD5K_uK*Hy{mXuHTkGGv|9V8KP#iQ$3!G*^>7UiE{|1G1A-qg(xH;Xa>&%f|BZkH zG=J^0pHzSAqv5*5ysQ{Puy^-_|IPrii zKS$mE10Zngf>Sgg@BjpRyJbrHeo zD8Ro0LI*W#+9?^xlOS^c>Z^^n^0I|FH^@^`ZR`{H=$ zjO0_$cnpBM7Zcm?H_RXIu-Lu~qweDSV|tEZBZh!e6hQy->}e;d#osZ1hQj{HhHkC0 zJ|F-HKmeTGgDe979ogBz24;@<|I7;TU!IXb@oWMsMECIETmQy`zPtM`|NP}PjzR_u zKMG1Z{%1kWeMfEf(10U#w!clmQ2)JC8zm(Fv!H4dUHQHCFLikID?hrd{0>kCQt?kP zdqn2ZG0}ytcQJ7t_B3s0ZvH3PYjkjQ`Q%;jV@?MK-+z3etBCGGo4f4`y^|AdCs!DH zThTQ;cL5dM{|tB_1y6K3bVa^hx_<9J(}5`2SDz1^0bT!Vm*JV;9~t&{IC{$DUAVV* z{|E=#yN{wNdTY@$6z{_KNA3&%w|vFu1n9XRcM0Ak>`UW!lQ`ah3D4r%}Z diff --git a/examples/distro/gradle/wrapper/gradle-wrapper.properties b/examples/distro/gradle/wrapper/gradle-wrapper.properties index 37aef8d3f0c9..8e876e1c5571 100644 --- a/examples/distro/gradle/wrapper/gradle-wrapper.properties +++ b/examples/distro/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,8 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-bin.zip +distributionSha256Sum=1541fa36599e12857140465f3c91a97409b4512501c26f9631fb113e392c5bd1 +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.1-bin.zip networkTimeout=10000 +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/examples/distro/gradlew b/examples/distro/gradlew index 79a61d421cc4..f5feea6d6b11 100755 --- a/examples/distro/gradlew +++ b/examples/distro/gradlew @@ -15,6 +15,8 @@ # See the License for the specific language governing permissions and # limitations under the License. # +# SPDX-License-Identifier: Apache-2.0 +# ############################################################################## # @@ -55,7 +57,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. @@ -83,10 +85,9 @@ done # This is normally unused # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} -APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit - -# 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"' +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s +' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum @@ -133,10 +134,13 @@ 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. + if ! command -v java >/dev/null 2>&1 + then + 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 fi # Increase the maximum file descriptors if we can. @@ -144,7 +148,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 + # shellcheck disable=SC2039,SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac @@ -152,7 +156,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then '' | soft) :;; #( *) # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 + # shellcheck disable=SC2039,SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac @@ -197,11 +201,15 @@ if "$cygwin" || "$msys" ; then done fi -# Collect all arguments for the java command; -# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of -# shell script including quotes and variable substitutions, so put them in -# double quotes to make sure that they get re-expanded; and -# * put everything else in single quotes, so that it's not re-expanded. + +# 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"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ diff --git a/examples/distro/gradlew.bat b/examples/distro/gradlew.bat index 93e3f59f135d..9d21a21834d5 100644 --- a/examples/distro/gradlew.bat +++ b/examples/distro/gradlew.bat @@ -13,6 +13,8 @@ @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem @if "%DEBUG%"=="" @echo off @rem ########################################################################## @@ -43,11 +45,11 @@ set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if %ERRORLEVEL% equ 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. +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail @@ -57,11 +59,11 @@ 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. +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail diff --git a/examples/distro/instrumentation/servlet-3/build.gradle b/examples/distro/instrumentation/servlet-3/build.gradle index 3b0dff0c8144..667e25bb3b49 100644 --- a/examples/distro/instrumentation/servlet-3/build.gradle +++ b/examples/distro/instrumentation/servlet-3/build.gradle @@ -27,7 +27,7 @@ dependencies { exclude group: 'org.eclipse.jetty', module: 'jetty-server' } - testImplementation "com.squareup.okhttp3:okhttp:4.11.0" + testImplementation "com.squareup.okhttp3:okhttp:4.12.0" testImplementation "javax.servlet:javax.servlet-api:3.0.1" testImplementation "org.eclipse.jetty:jetty-server:8.2.0.v20160908" testImplementation "org.eclipse.jetty:jetty-servlet:8.2.0.v20160908" diff --git a/examples/distro/instrumentation/servlet-3/src/test/java/com/example/javaagent/instrumentation/DemoServlet3InstrumentationTest.java b/examples/distro/instrumentation/servlet-3/src/test/java/com/example/javaagent/instrumentation/DemoServlet3InstrumentationTest.java index 96039f43ee67..773d01cbad36 100644 --- a/examples/distro/instrumentation/servlet-3/src/test/java/com/example/javaagent/instrumentation/DemoServlet3InstrumentationTest.java +++ b/examples/distro/instrumentation/servlet-3/src/test/java/com/example/javaagent/instrumentation/DemoServlet3InstrumentationTest.java @@ -80,13 +80,10 @@ void shouldAddCustomHeader() throws Exception { assertEquals("result", response.body().string()); assertThat(instrumentation.waitForTraces(1)) - .hasSize(1) .hasTracesSatisfyingExactly( trace -> - trace - .hasSize(1) - .hasSpansSatisfyingExactly( - span -> span.hasName("GET /servlet").hasKind(SpanKind.SERVER))); + trace.hasSpansSatisfyingExactly( + span -> span.hasName("GET /servlet").hasKind(SpanKind.SERVER))); var traceId = instrumentation.spans().get(0).getTraceId(); assertEquals(traceId, response.header("X-server-id")); diff --git a/examples/distro/smoke-tests/build.gradle b/examples/distro/smoke-tests/build.gradle index d65da259ebbf..a2501d260d67 100644 --- a/examples/distro/smoke-tests/build.gradle +++ b/examples/distro/smoke-tests/build.gradle @@ -3,14 +3,14 @@ plugins { } dependencies { - testImplementation("org.testcontainers:testcontainers:1.18.3") - testImplementation("com.fasterxml.jackson.core:jackson-databind:2.15.2") - testImplementation("com.google.protobuf:protobuf-java-util:3.23.3") - testImplementation("com.squareup.okhttp3:okhttp:4.11.0") - testImplementation("io.opentelemetry.proto:opentelemetry-proto:0.20.0-alpha") - testImplementation("io.opentelemetry:opentelemetry-api:${versions.opentelemetry}") + testImplementation("org.testcontainers:testcontainers:1.20.1") + testImplementation("com.fasterxml.jackson.core:jackson-databind:2.17.2") + testImplementation("com.google.protobuf:protobuf-java-util:3.25.4") + testImplementation("com.squareup.okhttp3:okhttp:4.12.0") + testImplementation("io.opentelemetry.proto:opentelemetry-proto:1.3.2-alpha") + testImplementation("io.opentelemetry:opentelemetry-api") - testImplementation("ch.qos.logback:logback-classic:1.4.8") + testImplementation("ch.qos.logback:logback-classic:1.5.8") } tasks.test { diff --git a/examples/distro/smoke-tests/src/test/java/com/example/javaagent/smoketest/SmokeTest.java b/examples/distro/smoke-tests/src/test/java/com/example/javaagent/smoketest/SmokeTest.java index 6d49362b9432..7029b90da9b9 100644 --- a/examples/distro/smoke-tests/src/test/java/com/example/javaagent/smoketest/SmokeTest.java +++ b/examples/distro/smoke-tests/src/test/java/com/example/javaagent/smoketest/SmokeTest.java @@ -31,6 +31,7 @@ import org.testcontainers.containers.Network; import org.testcontainers.containers.output.Slf4jLogConsumer; import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.containers.wait.strategy.WaitStrategy; import org.testcontainers.utility.MountableFile; abstract class SmokeTest { @@ -46,6 +47,8 @@ abstract class SmokeTest { protected abstract String getTargetImage(int jdk); + protected abstract WaitStrategy getTargetWaitStrategy(); + /** Subclasses can override this method to customise target application's environment */ protected Map getExtraEnv() { return Collections.emptyMap(); @@ -54,7 +57,7 @@ protected Map getExtraEnv() { private static GenericContainer backend; @BeforeAll - static void setupSpec() { + static void setup() { backend = new GenericContainer<>( "ghcr.io/open-telemetry/opentelemetry-java-instrumentation/smoke-test-fake-backend:20221127.3559314891") @@ -79,17 +82,23 @@ void startTarget(int jdk) { .withEnv("JAVA_TOOL_OPTIONS", "-javaagent:/opentelemetry-javaagent.jar") .withEnv("OTEL_BSP_MAX_EXPORT_BATCH", "1") .withEnv("OTEL_BSP_SCHEDULE_DELAY", "10") + // TODO (heya) update smoke tests to run using http/protobuf + // in the meantime, force smoke tests to use grpc protocol for all exporters + .withEnv("OTEL_EXPORTER_OTLP_PROTOCOL", "grpc") .withEnv("OTEL_PROPAGATORS", "tracecontext,baggage,demo") - .withEnv(getExtraEnv()); + .withEnv(getExtraEnv()) + .waitingFor(getTargetWaitStrategy()); target.start(); } @AfterEach - void cleanup() throws IOException { + void reset() throws IOException { client .newCall( new Request.Builder() - .url(String.format("http://localhost:%d/clear", backend.getMappedPort(8080))) + .url( + String.format( + "http://%s:%d/clear", backend.getHost(), backend.getMappedPort(8080))) .build()) .execute() .close(); @@ -100,7 +109,7 @@ void stopTarget() { } @AfterAll - static void cleanupSpec() { + static void cleanup() { backend.stop(); } @@ -169,7 +178,9 @@ private String waitForContent() throws IOException, InterruptedException { Request request = new Request.Builder() - .url(String.format("http://localhost:%d/get-traces", backend.getMappedPort(8080))) + .url( + String.format( + "http://%s:%d/get-traces", backend.getHost(), backend.getMappedPort(8080))) .build(); try (ResponseBody body = client.newCall(request).execute().body()) { diff --git a/examples/distro/smoke-tests/src/test/java/com/example/javaagent/smoketest/SpringBootSmokeTest.java b/examples/distro/smoke-tests/src/test/java/com/example/javaagent/smoketest/SpringBootSmokeTest.java index 576488938fe0..a2f29fbaff45 100644 --- a/examples/distro/smoke-tests/src/test/java/com/example/javaagent/smoketest/SpringBootSmokeTest.java +++ b/examples/distro/smoke-tests/src/test/java/com/example/javaagent/smoketest/SpringBootSmokeTest.java @@ -8,6 +8,7 @@ import io.opentelemetry.api.trace.TraceId; import io.opentelemetry.proto.collector.trace.v1.ExportTraceServiceRequest; import java.io.IOException; +import java.time.Duration; import java.util.Collection; import java.util.jar.Attributes; import java.util.jar.JarFile; @@ -15,6 +16,8 @@ import okhttp3.Response; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.containers.wait.strategy.WaitStrategy; class SpringBootSmokeTest extends SmokeTest { @@ -25,6 +28,12 @@ protected String getTargetImage(int jdk) { + "-20211213.1570880324"; } + @Override + protected WaitStrategy getTargetWaitStrategy() { + return Wait.forLogMessage(".*Started SpringbootApplication in.*", 1) + .withStartupTimeout(Duration.ofMinutes(1)); + } + @Test public void springBootSmokeTestOnJDK() throws IOException, InterruptedException { startTarget(8); @@ -52,7 +61,7 @@ public void springBootSmokeTestOnJDK() throws IOException, InterruptedException Assertions.assertEquals(1, countSpansByName(traces, "WebController.withSpan")); Assertions.assertEquals(2, countSpansByAttributeValue(traces, "custom", "demo")); Assertions.assertNotEquals( - 0, countResourcesByValue(traces, "telemetry.auto.version", currentAgentVersion)); + 0, countResourcesByValue(traces, "telemetry.distro.version", currentAgentVersion)); Assertions.assertNotEquals(0, countResourcesByValue(traces, "custom.resource", "demo")); stopTarget(); diff --git a/examples/distro/testing/agent-for-testing/build.gradle b/examples/distro/testing/agent-for-testing/build.gradle index 47dae7e74019..5eec782efd4c 100644 --- a/examples/distro/testing/agent-for-testing/build.gradle +++ b/examples/distro/testing/agent-for-testing/build.gradle @@ -1,7 +1,7 @@ import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar plugins { - id("com.github.johnrengelman.shadow") + id("com.gradleup.shadow") } apply from: "$rootDir/gradle/shadow.gradle" @@ -70,11 +70,9 @@ tasks { // exclude known bootstrap dependencies - they can't appear in the inst/ directory dependencies { exclude("io.opentelemetry:opentelemetry-api") - exclude("io.opentelemetry:opentelemetry-api-events") exclude("io.opentelemetry:opentelemetry-context") - exclude("io.opentelemetry:opentelemetry-semconv") - // metrics advice API - exclude("io.opentelemetry:opentelemetry-extension-incubator") + // events API and metrics advice API + exclude("io.opentelemetry:opentelemetry-api-incubator") } } @@ -86,7 +84,7 @@ tasks { dependsOn(tasks.relocateJavaagentLibs) with isolateClasses(tasks.relocateJavaagentLibs.outputs.files) - into("$buildDir/isolated/javaagentLibs") + into(layout.buildDirectory.dir("isolated/javaagentLibs")) } // 3. the relocated and isolated javaagent libs are merged together with the bootstrap libs (which undergo relocation diff --git a/examples/extension/README.md b/examples/extension/README.md index 35d5af4b6b4e..c82a33472dda 100644 --- a/examples/extension/README.md +++ b/examples/extension/README.md @@ -17,11 +17,11 @@ To add the extension to the instrumentation agent: 1. Copy the jar file to a host that is running an application to which you've attached the OpenTelemetry Java instrumentation. 2. Modify the startup command to add the full path to the extension file. For example: - ```bash - java -javaagent:path/to/opentelemetry-javaagent.jar \ - -Dotel.javaagent.extensions=build/libs/opentelemetry-java-instrumentation-extension-demo-1.0-all.jar - -jar myapp.jar - ``` + ```bash + java -javaagent:path/to/opentelemetry-javaagent.jar \ + -Dotel.javaagent.extensions=build/libs/opentelemetry-java-instrumentation-extension-demo-1.0-all.jar + -jar myapp.jar + ``` Note: to load multiple extensions, you can specify a comma-separated list of extension jars or directories (that contain extension jars) for the `otel.javaagent.extensions` value. @@ -30,16 +30,34 @@ contain extension jars) for the `otel.javaagent.extensions` value. To simplify deployment, you can embed extensions into the OpenTelemetry Java Agent to produce a single jar file. With an integrated extension, you no longer need the `-Dotel.javaagent.extensions` command line option. -For more information, see the `extendedAgent` task in [build.gradle](build.gradle). +For more information, see the `extendedAgent` task in [build.gradle](https://github.com/open-telemetry/opentelemetry-java-instrumentation/blob/main/examples/extension/build.gradle#:~:text=extendedAgent). ## Extensions examples -* Custom `IdGenerator`: [DemoIdGenerator](src/main/java/com/example/javaagent/DemoIdGenerator.java) -* Custom `TextMapPropagator`: [DemoPropagator](src/main/java/com/example/javaagent/DemoPropagator.java) -* Custom `Sampler`: [DemoSampler](src/main/java/com/example/javaagent/DemoSampler.java) -* Custom `SpanProcessor`: [DemoSpanProcessor](src/main/java/com/example/javaagent/DemoSpanProcessor.java) -* Custom `SpanExporter`: [DemoSpanExporter](src/main/java/com/example/javaagent/DemoSpanExporter.java) -* Additional instrumentation: [DemoServlet3InstrumentationModule](src/main/java/com/example/javaagent/instrumentation/DemoServlet3InstrumentationModule.java) +[DemoAutoConfigurationCustomizerProvider]: src/main/java/com/example/javaagent/DemoAutoConfigurationCustomizerProvider.java +[DemoIdGenerator]: src/main/java/com/example/javaagent/DemoIdGenerator.java +[DemoPropagator]: src/main/java/com/example/javaagent/DemoPropagator.java +[DemoSampler]: src/main/java/com/example/javaagent/DemoSampler.java +[DemoSpanProcessor]: src/main/java/com/example/javaagent/DemoSpanProcessor.java +[DemoSpanExporter]: src/main/java/com/example/javaagent/DemoSpanExporter.java +[DemoServlet3InstrumentationModule]: src/main/java/com/example/javaagent/instrumentation/DemoServlet3InstrumentationModule.java + +- Custom `AutoConfigurationCustomizer`: [DemoAutoConfigurationCustomizerProvider][DemoAutoConfigurationCustomizerProvider] +- Custom `IdGenerator`: [DemoIdGenerator][DemoIdGenerator] +- Custom `TextMapPropagator`: [DemoPropagator][DemoPropagator] +- Custom `Sampler`: [DemoSampler][DemoSampler] +- Custom `SpanProcessor`: [DemoSpanProcessor][DemoSpanProcessor] +- Custom `SpanExporter`: [DemoSpanExporter][DemoSpanExporter] +- Additional instrumentation: [DemoServlet3InstrumentationModule][DemoServlet3InstrumentationModule] + +`ConfigurablePropagatorProvider` and `AutoConfigurationCustomizer` implementations and custom +instrumentation (`InstrumentationModule`) need the correct SPI (through `@AutoService`) in +order to be loaded by the agent. Once a `ConfigurablePropagatorProvider` is added, it can be +referenced by name in the `OTEL_PROPAGATORS` setting. `AutoConfigurationCustomizer` and +instrumentation will be applied automatically. To apply the other extension classes to the Java +Agent, include an `AutoConfigurationCustomizer` in your extension. +See [DemoAutoConfigurationCustomizerProvider][DemoAutoConfigurationCustomizerProvider] for an +example. ## Sample use cases diff --git a/examples/extension/build.gradle b/examples/extension/build.gradle index c7befd421135..6d32507258a5 100644 --- a/examples/extension/build.gradle +++ b/examples/extension/build.gradle @@ -10,11 +10,11 @@ plugins { into a single jar. See https://imperceptiblethoughts.com/shadow/ for more details about Shadow plugin. */ - id "com.github.johnrengelman.shadow" version "8.1.1" - id "com.diffplug.spotless" version "6.19.0" + id "com.gradleup.shadow" version "8.3.1" + id "com.diffplug.spotless" version "6.25.0" - id "io.opentelemetry.instrumentation.muzzle-generation" version "1.28.0-alpha-SNAPSHOT" - id "io.opentelemetry.instrumentation.muzzle-check" version "1.28.0-alpha-SNAPSHOT" + id "io.opentelemetry.instrumentation.muzzle-generation" version "2.8.0-alpha-SNAPSHOT" + id "io.opentelemetry.instrumentation.muzzle-check" version "2.8.0-alpha-SNAPSHOT" } group 'io.opentelemetry.example' @@ -24,13 +24,13 @@ ext { versions = [ pyroscopeVersion : "0.12.2", // this line is managed by .github/scripts/update-sdk-version.sh - opentelemetrySdk : "1.27.0", + opentelemetrySdk : "1.41.0", // these lines are managed by .github/scripts/update-version.sh - opentelemetryJavaagent : "1.28.0-SNAPSHOT", - opentelemetryJavaagentAlpha: "1.28.0-alpha-SNAPSHOT", + opentelemetryJavaagent : "2.8.0-SNAPSHOT", + opentelemetryJavaagentAlpha: "2.8.0-alpha-SNAPSHOT", - junit : "5.9.3" + junit : "5.11.0" ] deps = [ @@ -100,19 +100,19 @@ dependencies { 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.12.0' + implementation 'org.apache.commons:commons-lang3:3.17.0' //All dependencies below are only for tests - testImplementation("org.testcontainers:testcontainers:1.18.3") - testImplementation("com.fasterxml.jackson.core:jackson-databind:2.15.2") - testImplementation("com.google.protobuf:protobuf-java-util:3.23.3") - testImplementation("com.squareup.okhttp3:okhttp:4.11.0") + testImplementation("org.testcontainers:testcontainers:1.20.1") + testImplementation("com.fasterxml.jackson.core:jackson-databind:2.17.2") + testImplementation("com.google.protobuf:protobuf-java-util:3.25.4") + testImplementation("com.squareup.okhttp3:okhttp:4.12.0") testImplementation("io.opentelemetry:opentelemetry-api") - testImplementation("io.opentelemetry.proto:opentelemetry-proto:0.20.0-alpha") + testImplementation("io.opentelemetry.proto:opentelemetry-proto:1.3.2-alpha") testImplementation("org.junit.jupiter:junit-jupiter-api:${versions.junit}") testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:${versions.junit}") - testRuntimeOnly("ch.qos.logback:logback-classic:1.4.8") + testRuntimeOnly("ch.qos.logback:logback-classic:1.5.8") //Otel Java instrumentation that we use and extend during integration tests otel("io.opentelemetry.javaagent:opentelemetry-javaagent:${versions.opentelemetryJavaagent}") diff --git a/examples/extension/gradle/wrapper/gradle-wrapper.jar b/examples/extension/gradle/wrapper/gradle-wrapper.jar index ccebba7710deaf9f98673a68957ea02138b60d0a..a4b76b9530d66f5e68d973ea569d8e19de379189 100644 GIT binary patch literal 43583 zcma&N1CXTcmMvW9vTb(Rwr$&4wr$(C?dmSu>@vG-+vuvg^_??!{yS%8zW-#zn-LkA z5&1^$^{lnmUON?}LBF8_K|(?T0Ra(xUH{($5eN!MR#ZihR#HxkUPe+_R8Cn`RRs(P z_^*#_XlXmGv7!4;*Y%p4nw?{bNp@UZHv1?Um8r6)Fei3p@ClJn0ECfg1hkeuUU@Or zDaPa;U3fE=3L}DooL;8f;P0ipPt0Z~9P0)lbStMS)ag54=uL9ia-Lm3nh|@(Y?B`; zx_#arJIpXH!U{fbCbI^17}6Ri*H<>OLR%c|^mh8+)*h~K8Z!9)DPf zR2h?lbDZQ`p9P;&DQ4F0sur@TMa!Y}S8irn(%d-gi0*WxxCSk*A?3lGh=gcYN?FGl z7D=Js!i~0=u3rox^eO3i@$0=n{K1lPNU zwmfjRVmLOCRfe=seV&P*1Iq=^i`502keY8Uy-WNPwVNNtJFx?IwAyRPZo2Wo1+S(xF37LJZ~%i)kpFQ3Fw=mXfd@>%+)RpYQLnr}B~~zoof(JVm^^&f zxKV^+3D3$A1G;qh4gPVjhrC8e(VYUHv#dy^)(RoUFM?o%W-EHxufuWf(l*@-l+7vt z=l`qmR56K~F|v<^Pd*p~1_y^P0P^aPC##d8+HqX4IR1gu+7w#~TBFphJxF)T$2WEa zxa?H&6=Qe7d(#tha?_1uQys2KtHQ{)Qco)qwGjrdNL7thd^G5i8Os)CHqc>iOidS} z%nFEDdm=GXBw=yXe1W-ShHHFb?Cc70+$W~z_+}nAoHFYI1MV1wZegw*0y^tC*s%3h zhD3tN8b=Gv&rj}!SUM6|ajSPp*58KR7MPpI{oAJCtY~JECm)*m_x>AZEu>DFgUcby z1Qaw8lU4jZpQ_$;*7RME+gq1KySGG#Wql>aL~k9tLrSO()LWn*q&YxHEuzmwd1?aAtI zBJ>P=&$=l1efe1CDU;`Fd+_;&wI07?V0aAIgc(!{a z0Jg6Y=inXc3^n!U0Atk`iCFIQooHqcWhO(qrieUOW8X(x?(RD}iYDLMjSwffH2~tB z)oDgNBLB^AJBM1M^c5HdRx6fBfka`(LD-qrlh5jqH~);#nw|iyp)()xVYak3;Ybik z0j`(+69aK*B>)e_p%=wu8XC&9e{AO4c~O1U`5X9}?0mrd*m$_EUek{R?DNSh(=br# z#Q61gBzEpmy`$pA*6!87 zSDD+=@fTY7<4A?GLqpA?Pb2z$pbCc4B4zL{BeZ?F-8`s$?>*lXXtn*NC61>|*w7J* z$?!iB{6R-0=KFmyp1nnEmLsA-H0a6l+1uaH^g%c(p{iT&YFrbQ$&PRb8Up#X3@Zsk zD^^&LK~111%cqlP%!_gFNa^dTYT?rhkGl}5=fL{a`UViaXWI$k-UcHJwmaH1s=S$4 z%4)PdWJX;hh5UoK?6aWoyLxX&NhNRqKam7tcOkLh{%j3K^4Mgx1@i|Pi&}<^5>hs5 zm8?uOS>%)NzT(%PjVPGa?X%`N2TQCKbeH2l;cTnHiHppPSJ<7y-yEIiC!P*ikl&!B z%+?>VttCOQM@ShFguHVjxX^?mHX^hSaO_;pnyh^v9EumqSZTi+#f&_Vaija0Q-e*| z7ulQj6Fs*bbmsWp{`auM04gGwsYYdNNZcg|ph0OgD>7O}Asn7^Z=eI>`$2*v78;sj-}oMoEj&@)9+ycEOo92xSyY344^ z11Hb8^kdOvbf^GNAK++bYioknrpdN>+u8R?JxG=!2Kd9r=YWCOJYXYuM0cOq^FhEd zBg2puKy__7VT3-r*dG4c62Wgxi52EMCQ`bKgf*#*ou(D4-ZN$+mg&7$u!! z-^+Z%;-3IDwqZ|K=ah85OLwkO zKxNBh+4QHh)u9D?MFtpbl)us}9+V!D%w9jfAMYEb>%$A;u)rrI zuBudh;5PN}_6J_}l55P3l_)&RMlH{m!)ai-i$g)&*M`eN$XQMw{v^r@-125^RRCF0 z^2>|DxhQw(mtNEI2Kj(;KblC7x=JlK$@78`O~>V!`|1Lm-^JR$-5pUANAnb(5}B}JGjBsliK4& zk6y(;$e&h)lh2)L=bvZKbvh@>vLlreBdH8No2>$#%_Wp1U0N7Ank!6$dFSi#xzh|( zRi{Uw%-4W!{IXZ)fWx@XX6;&(m_F%c6~X8hx=BN1&q}*( zoaNjWabE{oUPb!Bt$eyd#$5j9rItB-h*5JiNi(v^e|XKAj*8(k<5-2$&ZBR5fF|JA z9&m4fbzNQnAU}r8ab>fFV%J0z5awe#UZ|bz?Ur)U9bCIKWEzi2%A+5CLqh?}K4JHi z4vtM;+uPsVz{Lfr;78W78gC;z*yTch~4YkLr&m-7%-xc ztw6Mh2d>_iO*$Rd8(-Cr1_V8EO1f*^@wRoSozS) zy1UoC@pruAaC8Z_7~_w4Q6n*&B0AjOmMWa;sIav&gu z|J5&|{=a@vR!~k-OjKEgPFCzcJ>#A1uL&7xTDn;{XBdeM}V=l3B8fE1--DHjSaxoSjNKEM9|U9#m2<3>n{Iuo`r3UZp;>GkT2YBNAh|b z^jTq-hJp(ebZh#Lk8hVBP%qXwv-@vbvoREX$TqRGTgEi$%_F9tZES@z8Bx}$#5eeG zk^UsLBH{bc2VBW)*EdS({yw=?qmevwi?BL6*=12k9zM5gJv1>y#ML4!)iiPzVaH9% zgSImetD@dam~e>{LvVh!phhzpW+iFvWpGT#CVE5TQ40n%F|p(sP5mXxna+Ev7PDwA zamaV4m*^~*xV+&p;W749xhb_X=$|LD;FHuB&JL5?*Y2-oIT(wYY2;73<^#46S~Gx| z^cez%V7x$81}UWqS13Gz80379Rj;6~WdiXWOSsdmzY39L;Hg3MH43o*y8ibNBBH`(av4|u;YPq%{R;IuYow<+GEsf@R?=@tT@!}?#>zIIn0CoyV!hq3mw zHj>OOjfJM3F{RG#6ujzo?y32m^tgSXf@v=J$ELdJ+=5j|=F-~hP$G&}tDZsZE?5rX ztGj`!S>)CFmdkccxM9eGIcGnS2AfK#gXwj%esuIBNJQP1WV~b~+D7PJTmWGTSDrR` zEAu4B8l>NPuhsk5a`rReSya2nfV1EK01+G!x8aBdTs3Io$u5!6n6KX%uv@DxAp3F@{4UYg4SWJtQ-W~0MDb|j-$lwVn znAm*Pl!?Ps&3wO=R115RWKb*JKoexo*)uhhHBncEDMSVa_PyA>k{Zm2(wMQ(5NM3# z)jkza|GoWEQo4^s*wE(gHz?Xsg4`}HUAcs42cM1-qq_=+=!Gk^y710j=66(cSWqUe zklbm8+zB_syQv5A2rj!Vbw8;|$@C!vfNmNV!yJIWDQ>{+2x zKjuFX`~~HKG~^6h5FntRpnnHt=D&rq0>IJ9#F0eM)Y-)GpRjiN7gkA8wvnG#K=q{q z9dBn8_~wm4J<3J_vl|9H{7q6u2A!cW{bp#r*-f{gOV^e=8S{nc1DxMHFwuM$;aVI^ zz6A*}m8N-&x8;aunp1w7_vtB*pa+OYBw=TMc6QK=mbA-|Cf* zvyh8D4LRJImooUaSb7t*fVfih<97Gf@VE0|z>NcBwBQze);Rh!k3K_sfunToZY;f2 z^HmC4KjHRVg+eKYj;PRN^|E0>Gj_zagfRbrki68I^#~6-HaHg3BUW%+clM1xQEdPYt_g<2K+z!$>*$9nQ>; zf9Bei{?zY^-e{q_*|W#2rJG`2fy@{%6u0i_VEWTq$*(ZN37|8lFFFt)nCG({r!q#9 z5VK_kkSJ3?zOH)OezMT{!YkCuSSn!K#-Rhl$uUM(bq*jY? zi1xbMVthJ`E>d>(f3)~fozjg^@eheMF6<)I`oeJYx4*+M&%c9VArn(OM-wp%M<-`x z7sLP1&3^%Nld9Dhm@$3f2}87!quhI@nwd@3~fZl_3LYW-B?Ia>ui`ELg z&Qfe!7m6ze=mZ`Ia9$z|ARSw|IdMpooY4YiPN8K z4B(ts3p%2i(Td=tgEHX z0UQ_>URBtG+-?0E;E7Ld^dyZ;jjw0}XZ(}-QzC6+NN=40oDb2^v!L1g9xRvE#@IBR zO!b-2N7wVfLV;mhEaXQ9XAU+>=XVA6f&T4Z-@AX!leJ8obP^P^wP0aICND?~w&NykJ#54x3_@r7IDMdRNy4Hh;h*!u(Ol(#0bJdwEo$5437-UBjQ+j=Ic>Q2z` zJNDf0yO6@mr6y1#n3)s(W|$iE_i8r@Gd@!DWDqZ7J&~gAm1#~maIGJ1sls^gxL9LLG_NhU!pTGty!TbhzQnu)I*S^54U6Yu%ZeCg`R>Q zhBv$n5j0v%O_j{QYWG!R9W?5_b&67KB$t}&e2LdMvd(PxN6Ir!H4>PNlerpBL>Zvyy!yw z-SOo8caEpDt(}|gKPBd$qND5#a5nju^O>V&;f890?yEOfkSG^HQVmEbM3Ugzu+UtH zC(INPDdraBN?P%kE;*Ae%Wto&sgw(crfZ#Qy(<4nk;S|hD3j{IQRI6Yq|f^basLY; z-HB&Je%Gg}Jt@={_C{L$!RM;$$|iD6vu#3w?v?*;&()uB|I-XqEKqZPS!reW9JkLewLb!70T7n`i!gNtb1%vN- zySZj{8-1>6E%H&=V}LM#xmt`J3XQoaD|@XygXjdZ1+P77-=;=eYpoEQ01B@L*a(uW zrZeZz?HJsw_4g0vhUgkg@VF8<-X$B8pOqCuWAl28uB|@r`19DTUQQsb^pfqB6QtiT z*`_UZ`fT}vtUY#%sq2{rchyfu*pCg;uec2$-$N_xgjZcoumE5vSI{+s@iLWoz^Mf; zuI8kDP{!XY6OP~q5}%1&L}CtfH^N<3o4L@J@zg1-mt{9L`s^z$Vgb|mr{@WiwAqKg zp#t-lhrU>F8o0s1q_9y`gQNf~Vb!F%70f}$>i7o4ho$`uciNf=xgJ>&!gSt0g;M>*x4-`U)ysFW&Vs^Vk6m%?iuWU+o&m(2Jm26Y(3%TL; zA7T)BP{WS!&xmxNw%J=$MPfn(9*^*TV;$JwRy8Zl*yUZi8jWYF>==j~&S|Xinsb%c z2?B+kpet*muEW7@AzjBA^wAJBY8i|#C{WtO_or&Nj2{=6JTTX05}|H>N2B|Wf!*3_ z7hW*j6p3TvpghEc6-wufFiY!%-GvOx*bZrhZu+7?iSrZL5q9}igiF^*R3%DE4aCHZ zqu>xS8LkW+Auv%z-<1Xs92u23R$nk@Pk}MU5!gT|c7vGlEA%G^2th&Q*zfg%-D^=f z&J_}jskj|Q;73NP4<4k*Y%pXPU2Thoqr+5uH1yEYM|VtBPW6lXaetokD0u z9qVek6Q&wk)tFbQ8(^HGf3Wp16gKmr>G;#G(HRBx?F`9AIRboK+;OfHaLJ(P>IP0w zyTbTkx_THEOs%Q&aPrxbZrJlio+hCC_HK<4%f3ZoSAyG7Dn`=X=&h@m*|UYO-4Hq0 z-Bq&+Ie!S##4A6OGoC~>ZW`Y5J)*ouaFl_e9GA*VSL!O_@xGiBw!AF}1{tB)z(w%c zS1Hmrb9OC8>0a_$BzeiN?rkPLc9%&;1CZW*4}CDDNr2gcl_3z+WC15&H1Zc2{o~i) z)LLW=WQ{?ricmC`G1GfJ0Yp4Dy~Ba;j6ZV4r{8xRs`13{dD!xXmr^Aga|C=iSmor% z8hi|pTXH)5Yf&v~exp3o+sY4B^^b*eYkkCYl*T{*=-0HniSA_1F53eCb{x~1k3*`W zr~};p1A`k{1DV9=UPnLDgz{aJH=-LQo<5%+Em!DNN252xwIf*wF_zS^!(XSm(9eoj z=*dXG&n0>)_)N5oc6v!>-bd(2ragD8O=M|wGW z!xJQS<)u70m&6OmrF0WSsr@I%T*c#Qo#Ha4d3COcX+9}hM5!7JIGF>7<~C(Ear^Sn zm^ZFkV6~Ula6+8S?oOROOA6$C&q&dp`>oR-2Ym3(HT@O7Sd5c~+kjrmM)YmgPH*tL zX+znN>`tv;5eOfX?h{AuX^LK~V#gPCu=)Tigtq9&?7Xh$qN|%A$?V*v=&-2F$zTUv z`C#WyIrChS5|Kgm_GeudCFf;)!WH7FI60j^0o#65o6`w*S7R@)88n$1nrgU(oU0M9 zx+EuMkC>(4j1;m6NoGqEkpJYJ?vc|B zOlwT3t&UgL!pX_P*6g36`ZXQ; z9~Cv}ANFnJGp(;ZhS(@FT;3e)0)Kp;h^x;$*xZn*k0U6-&FwI=uOGaODdrsp-!K$Ac32^c{+FhI-HkYd5v=`PGsg%6I`4d9Jy)uW0y%) zm&j^9WBAp*P8#kGJUhB!L?a%h$hJgQrx!6KCB_TRo%9{t0J7KW8!o1B!NC)VGLM5! zpZy5Jc{`r{1e(jd%jsG7k%I+m#CGS*BPA65ZVW~fLYw0dA-H_}O zrkGFL&P1PG9p2(%QiEWm6x;U-U&I#;Em$nx-_I^wtgw3xUPVVu zqSuKnx&dIT-XT+T10p;yjo1Y)z(x1fb8Dzfn8e yu?e%!_ptzGB|8GrCfu%p?(_ zQccdaaVK$5bz;*rnyK{_SQYM>;aES6Qs^lj9lEs6_J+%nIiuQC*fN;z8md>r_~Mfl zU%p5Dt_YT>gQqfr@`cR!$NWr~+`CZb%dn;WtzrAOI>P_JtsB76PYe*<%H(y>qx-`Kq!X_; z<{RpAqYhE=L1r*M)gNF3B8r(<%8mo*SR2hu zccLRZwGARt)Hlo1euqTyM>^!HK*!Q2P;4UYrysje@;(<|$&%vQekbn|0Ruu_Io(w4#%p6ld2Yp7tlA`Y$cciThP zKzNGIMPXX%&Ud0uQh!uQZz|FB`4KGD?3!ND?wQt6!n*f4EmCoJUh&b?;B{|lxs#F- z31~HQ`SF4x$&v00@(P+j1pAaj5!s`)b2RDBp*PB=2IB>oBF!*6vwr7Dp%zpAx*dPr zb@Zjq^XjN?O4QcZ*O+8>)|HlrR>oD*?WQl5ri3R#2?*W6iJ>>kH%KnnME&TT@ZzrHS$Q%LC?n|e>V+D+8D zYc4)QddFz7I8#}y#Wj6>4P%34dZH~OUDb?uP%-E zwjXM(?Sg~1!|wI(RVuxbu)-rH+O=igSho_pDCw(c6b=P zKk4ATlB?bj9+HHlh<_!&z0rx13K3ZrAR8W)!@Y}o`?a*JJsD+twZIv`W)@Y?Amu_u zz``@-e2X}27$i(2=9rvIu5uTUOVhzwu%mNazS|lZb&PT;XE2|B&W1>=B58#*!~D&) zfVmJGg8UdP*fx(>Cj^?yS^zH#o-$Q-*$SnK(ZVFkw+er=>N^7!)FtP3y~Xxnu^nzY zikgB>Nj0%;WOltWIob|}%lo?_C7<``a5hEkx&1ku$|)i>Rh6@3h*`slY=9U}(Ql_< zaNG*J8vb&@zpdhAvv`?{=zDedJ23TD&Zg__snRAH4eh~^oawdYi6A3w8<Ozh@Kw)#bdktM^GVb zrG08?0bG?|NG+w^&JvD*7LAbjED{_Zkc`3H!My>0u5Q}m!+6VokMLXxl`Mkd=g&Xx z-a>m*#G3SLlhbKB!)tnzfWOBV;u;ftU}S!NdD5+YtOjLg?X}dl>7m^gOpihrf1;PY zvll&>dIuUGs{Qnd- zwIR3oIrct8Va^Tm0t#(bJD7c$Z7DO9*7NnRZorrSm`b`cxz>OIC;jSE3DO8`hX955ui`s%||YQtt2 z5DNA&pG-V+4oI2s*x^>-$6J?p=I>C|9wZF8z;VjR??Icg?1w2v5Me+FgAeGGa8(3S z4vg*$>zC-WIVZtJ7}o9{D-7d>zCe|z#<9>CFve-OPAYsneTb^JH!Enaza#j}^mXy1 z+ULn^10+rWLF6j2>Ya@@Kq?26>AqK{A_| zQKb*~F1>sE*=d?A?W7N2j?L09_7n+HGi{VY;MoTGr_)G9)ot$p!-UY5zZ2Xtbm=t z@dpPSGwgH=QtIcEulQNI>S-#ifbnO5EWkI;$A|pxJd885oM+ zGZ0_0gDvG8q2xebj+fbCHYfAXuZStH2j~|d^sBAzo46(K8n59+T6rzBwK)^rfPT+B zyIFw)9YC-V^rhtK`!3jrhmW-sTmM+tPH+;nwjL#-SjQPUZ53L@A>y*rt(#M(qsiB2 zx6B)dI}6Wlsw%bJ8h|(lhkJVogQZA&n{?Vgs6gNSXzuZpEyu*xySy8ro07QZ7Vk1!3tJphN_5V7qOiyK8p z#@jcDD8nmtYi1^l8ml;AF<#IPK?!pqf9D4moYk>d99Im}Jtwj6c#+A;f)CQ*f-hZ< z=p_T86jog%!p)D&5g9taSwYi&eP z#JuEK%+NULWus;0w32-SYFku#i}d~+{Pkho&^{;RxzP&0!RCm3-9K6`>KZpnzS6?L z^H^V*s!8<>x8bomvD%rh>Zp3>Db%kyin;qtl+jAv8Oo~1g~mqGAC&Qi_wy|xEt2iz zWAJEfTV%cl2Cs<1L&DLRVVH05EDq`pH7Oh7sR`NNkL%wi}8n>IXcO40hp+J+sC!W?!krJf!GJNE8uj zg-y~Ns-<~D?yqbzVRB}G>0A^f0!^N7l=$m0OdZuqAOQqLc zX?AEGr1Ht+inZ-Qiwnl@Z0qukd__a!C*CKuGdy5#nD7VUBM^6OCpxCa2A(X;e0&V4 zM&WR8+wErQ7UIc6LY~Q9x%Sn*Tn>>P`^t&idaOEnOd(Ufw#>NoR^1QdhJ8s`h^|R_ zXX`c5*O~Xdvh%q;7L!_!ohf$NfEBmCde|#uVZvEo>OfEq%+Ns7&_f$OR9xsihRpBb z+cjk8LyDm@U{YN>+r46?nn{7Gh(;WhFw6GAxtcKD+YWV?uge>;+q#Xx4!GpRkVZYu zzsF}1)7$?%s9g9CH=Zs+B%M_)+~*j3L0&Q9u7!|+T`^O{xE6qvAP?XWv9_MrZKdo& z%IyU)$Q95AB4!#hT!_dA>4e@zjOBD*Y=XjtMm)V|+IXzjuM;(l+8aA5#Kaz_$rR6! zj>#&^DidYD$nUY(D$mH`9eb|dtV0b{S>H6FBfq>t5`;OxA4Nn{J(+XihF(stSche7$es&~N$epi&PDM_N`As;*9D^L==2Q7Z2zD+CiU(|+-kL*VG+&9!Yb3LgPy?A zm7Z&^qRG_JIxK7-FBzZI3Q<;{`DIxtc48k> zc|0dmX;Z=W$+)qE)~`yn6MdoJ4co;%!`ddy+FV538Y)j(vg}5*k(WK)KWZ3WaOG!8 z!syGn=s{H$odtpqFrT#JGM*utN7B((abXnpDM6w56nhw}OY}0TiTG1#f*VFZr+^-g zbP10`$LPq_;PvrA1XXlyx2uM^mrjTzX}w{yuLo-cOClE8MMk47T25G8M!9Z5ypOSV zAJUBGEg5L2fY)ZGJb^E34R2zJ?}Vf>{~gB!8=5Z) z9y$>5c)=;o0HeHHSuE4U)#vG&KF|I%-cF6f$~pdYJWk_dD}iOA>iA$O$+4%@>JU08 zS`ep)$XLPJ+n0_i@PkF#ri6T8?ZeAot$6JIYHm&P6EB=BiaNY|aA$W0I+nz*zkz_z zkEru!tj!QUffq%)8y0y`T&`fuus-1p>=^hnBiBqD^hXrPs`PY9tU3m0np~rISY09> z`P3s=-kt_cYcxWd{de@}TwSqg*xVhp;E9zCsnXo6z z?f&Sv^U7n4`xr=mXle94HzOdN!2kB~4=%)u&N!+2;z6UYKUDqi-s6AZ!haB;@&B`? z_TRX0%@suz^TRdCb?!vNJYPY8L_}&07uySH9%W^Tc&1pia6y1q#?*Drf}GjGbPjBS zbOPcUY#*$3sL2x4v_i*Y=N7E$mR}J%|GUI(>WEr+28+V z%v5{#e!UF*6~G&%;l*q*$V?&r$Pp^sE^i-0$+RH3ERUUdQ0>rAq2(2QAbG}$y{de( z>{qD~GGuOk559Y@%$?N^1ApVL_a704>8OD%8Y%8B;FCt%AoPu8*D1 zLB5X>b}Syz81pn;xnB}%0FnwazlWfUV)Z-~rZg6~b z6!9J$EcE&sEbzcy?CI~=boWA&eeIa%z(7SE^qgVLz??1Vbc1*aRvc%Mri)AJaAG!p z$X!_9Ds;Zz)f+;%s&dRcJt2==P{^j3bf0M=nJd&xwUGlUFn?H=2W(*2I2Gdu zv!gYCwM10aeus)`RIZSrCK=&oKaO_Ry~D1B5!y0R=%!i2*KfXGYX&gNv_u+n9wiR5 z*e$Zjju&ODRW3phN925%S(jL+bCHv6rZtc?!*`1TyYXT6%Ju=|X;6D@lq$8T zW{Y|e39ioPez(pBH%k)HzFITXHvnD6hw^lIoUMA;qAJ^CU?top1fo@s7xT13Fvn1H z6JWa-6+FJF#x>~+A;D~;VDs26>^oH0EI`IYT2iagy23?nyJ==i{g4%HrAf1-*v zK1)~@&(KkwR7TL}L(A@C_S0G;-GMDy=MJn2$FP5s<%wC)4jC5PXoxrQBFZ_k0P{{s@sz+gX`-!=T8rcB(=7vW}^K6oLWMmp(rwDh}b zwaGGd>yEy6fHv%jM$yJXo5oMAQ>c9j`**}F?MCry;T@47@r?&sKHgVe$MCqk#Z_3S z1GZI~nOEN*P~+UaFGnj{{Jo@16`(qVNtbU>O0Hf57-P>x8Jikp=`s8xWs^dAJ9lCQ z)GFm+=OV%AMVqVATtN@|vp61VVAHRn87}%PC^RAzJ%JngmZTasWBAWsoAqBU+8L8u z4A&Pe?fmTm0?mK-BL9t+{y7o(7jm+RpOhL9KnY#E&qu^}B6=K_dB}*VlSEiC9fn)+V=J;OnN)Ta5v66ic1rG+dGAJ1 z1%Zb_+!$=tQ~lxQrzv3x#CPb?CekEkA}0MYSgx$Jdd}q8+R=ma$|&1a#)TQ=l$1tQ z=tL9&_^vJ)Pk}EDO-va`UCT1m#Uty1{v^A3P~83_#v^ozH}6*9mIjIr;t3Uv%@VeW zGL6(CwCUp)Jq%G0bIG%?{_*Y#5IHf*5M@wPo6A{$Um++Co$wLC=J1aoG93&T7Ho}P z=mGEPP7GbvoG!uD$k(H3A$Z))+i{Hy?QHdk>3xSBXR0j!11O^mEe9RHmw!pvzv?Ua~2_l2Yh~_!s1qS`|0~0)YsbHSz8!mG)WiJE| z2f($6TQtt6L_f~ApQYQKSb=`053LgrQq7G@98#igV>y#i==-nEjQ!XNu9 z~;mE+gtj4IDDNQJ~JVk5Ux6&LCSFL!y=>79kE9=V}J7tD==Ga+IW zX)r7>VZ9dY=V&}DR))xUoV!u(Z|%3ciQi_2jl}3=$Agc(`RPb z8kEBpvY>1FGQ9W$n>Cq=DIpski};nE)`p3IUw1Oz0|wxll^)4dq3;CCY@RyJgFgc# zKouFh!`?Xuo{IMz^xi-h=StCis_M7yq$u) z?XHvw*HP0VgR+KR6wI)jEMX|ssqYvSf*_3W8zVTQzD?3>H!#>InzpSO)@SC8q*ii- z%%h}_#0{4JG;Jm`4zg};BPTGkYamx$Xo#O~lBirRY)q=5M45n{GCfV7h9qwyu1NxOMoP4)jjZMxmT|IQQh0U7C$EbnMN<3)Kk?fFHYq$d|ICu>KbY_hO zTZM+uKHe(cIZfEqyzyYSUBZa8;Fcut-GN!HSA9ius`ltNebF46ZX_BbZNU}}ZOm{M2&nANL9@0qvih15(|`S~z}m&h!u4x~(%MAO$jHRWNfuxWF#B)E&g3ghSQ9|> z(MFaLQj)NE0lowyjvg8z0#m6FIuKE9lDO~Glg}nSb7`~^&#(Lw{}GVOS>U)m8bF}x zVjbXljBm34Cs-yM6TVusr+3kYFjr28STT3g056y3cH5Tmge~ASxBj z%|yb>$eF;WgrcOZf569sDZOVwoo%8>XO>XQOX1OyN9I-SQgrm;U;+#3OI(zrWyow3 zk==|{lt2xrQ%FIXOTejR>;wv(Pb8u8}BUpx?yd(Abh6? zsoO3VYWkeLnF43&@*#MQ9-i-d0t*xN-UEyNKeyNMHw|A(k(_6QKO=nKMCxD(W(Yop zsRQ)QeL4X3Lxp^L%wzi2-WVSsf61dqliPUM7srDB?Wm6Lzn0&{*}|IsKQW;02(Y&| zaTKv|`U(pSzuvR6Rduu$wzK_W-Y-7>7s?G$)U}&uK;<>vU}^^ns@Z!p+9?St1s)dG zK%y6xkPyyS1$~&6v{kl?Md6gwM|>mt6Upm>oa8RLD^8T{0?HC!Z>;(Bob7el(DV6x zi`I)$&E&ngwFS@bi4^xFLAn`=fzTC;aimE^!cMI2n@Vo%Ae-ne`RF((&5y6xsjjAZ zVguVoQ?Z9uk$2ON;ersE%PU*xGO@T*;j1BO5#TuZKEf(mB7|g7pcEA=nYJ{s3vlbg zd4-DUlD{*6o%Gc^N!Nptgay>j6E5;3psI+C3Q!1ZIbeCubW%w4pq9)MSDyB{HLm|k zxv-{$$A*pS@csolri$Ge<4VZ}e~78JOL-EVyrbxKra^d{?|NnPp86!q>t<&IP07?Z z^>~IK^k#OEKgRH+LjllZXk7iA>2cfH6+(e&9ku5poo~6y{GC5>(bRK7hwjiurqAiZ zg*DmtgY}v83IjE&AbiWgMyFbaRUPZ{lYiz$U^&Zt2YjG<%m((&_JUbZcfJ22(>bi5 z!J?<7AySj0JZ&<-qXX;mcV!f~>G=sB0KnjWca4}vrtunD^1TrpfeS^4dvFr!65knK zZh`d;*VOkPs4*-9kL>$GP0`(M!j~B;#x?Ba~&s6CopvO86oM?-? zOw#dIRc;6A6T?B`Qp%^<U5 z19x(ywSH$_N+Io!6;e?`tWaM$`=Db!gzx|lQ${DG!zb1Zl&|{kX0y6xvO1o z220r<-oaS^^R2pEyY;=Qllqpmue|5yI~D|iI!IGt@iod{Opz@*ml^w2bNs)p`M(Io z|E;;m*Xpjd9l)4G#KaWfV(t8YUn@A;nK^#xgv=LtnArX|vWQVuw3}B${h+frU2>9^ z!l6)!Uo4`5k`<<;E(ido7M6lKTgWezNLq>U*=uz&s=cc$1%>VrAeOoUtA|T6gO4>UNqsdK=NF*8|~*sl&wI=x9-EGiq*aqV!(VVXA57 zw9*o6Ir8Lj1npUXvlevtn(_+^X5rzdR>#(}4YcB9O50q97%rW2me5_L=%ffYPUSRc z!vv?Kv>dH994Qi>U(a<0KF6NH5b16enCp+mw^Hb3Xs1^tThFpz!3QuN#}KBbww`(h z7GO)1olDqy6?T$()R7y%NYx*B0k_2IBiZ14&8|JPFxeMF{vW>HF-Vi3+ZOI=+qP}n zw(+!WcTd~4ZJX1!ZM&y!+uyt=&i!+~d(V%GjH;-NsEEv6nS1TERt|RHh!0>W4+4pp z1-*EzAM~i`+1f(VEHI8So`S`akPfPTfq*`l{Fz`hS%k#JS0cjT2mS0#QLGf=J?1`he3W*;m4)ce8*WFq1sdP=~$5RlH1EdWm|~dCvKOi4*I_96{^95p#B<(n!d?B z=o`0{t+&OMwKcxiBECznJcfH!fL(z3OvmxP#oWd48|mMjpE||zdiTBdWelj8&Qosv zZFp@&UgXuvJw5y=q6*28AtxZzo-UUpkRW%ne+Ylf!V-0+uQXBW=5S1o#6LXNtY5!I z%Rkz#(S8Pjz*P7bqB6L|M#Er{|QLae-Y{KA>`^} z@lPjeX>90X|34S-7}ZVXe{wEei1<{*e8T-Nbj8JmD4iwcE+Hg_zhkPVm#=@b$;)h6 z<<6y`nPa`f3I6`!28d@kdM{uJOgM%`EvlQ5B2bL)Sl=|y@YB3KeOzz=9cUW3clPAU z^sYc}xf9{4Oj?L5MOlYxR{+>w=vJjvbyO5}ptT(o6dR|ygO$)nVCvNGnq(6;bHlBd zl?w-|plD8spjDF03g5ip;W3Z z><0{BCq!Dw;h5~#1BuQilq*TwEu)qy50@+BE4bX28+7erX{BD4H)N+7U`AVEuREE8 z;X?~fyhF-x_sRfHIj~6f(+^@H)D=ngP;mwJjxhQUbUdzk8f94Ab%59-eRIq?ZKrwD z(BFI=)xrUlgu(b|hAysqK<}8bslmNNeD=#JW*}^~Nrswn^xw*nL@Tx!49bfJecV&KC2G4q5a!NSv)06A_5N3Y?veAz;Gv+@U3R% z)~UA8-0LvVE{}8LVDOHzp~2twReqf}ODIyXMM6=W>kL|OHcx9P%+aJGYi_Om)b!xe zF40Vntn0+VP>o<$AtP&JANjXBn7$}C@{+@3I@cqlwR2MdwGhVPxlTIcRVu@Ho-wO` z_~Or~IMG)A_`6-p)KPS@cT9mu9RGA>dVh5wY$NM9-^c@N=hcNaw4ITjm;iWSP^ZX| z)_XpaI61<+La+U&&%2a z0za$)-wZP@mwSELo#3!PGTt$uy0C(nTT@9NX*r3Ctw6J~7A(m#8fE)0RBd`TdKfAT zCf@$MAxjP`O(u9s@c0Fd@|}UQ6qp)O5Q5DPCeE6mSIh|Rj{$cAVIWsA=xPKVKxdhg zLzPZ`3CS+KIO;T}0Ip!fAUaNU>++ZJZRk@I(h<)RsJUhZ&Ru9*!4Ptn;gX^~4E8W^TSR&~3BAZc#HquXn)OW|TJ`CTahk+{qe`5+ixON^zA9IFd8)kc%*!AiLu z>`SFoZ5bW-%7}xZ>gpJcx_hpF$2l+533{gW{a7ce^B9sIdmLrI0)4yivZ^(Vh@-1q zFT!NQK$Iz^xu%|EOK=n>ug;(7J4OnS$;yWmq>A;hsD_0oAbLYhW^1Vdt9>;(JIYjf zdb+&f&D4@4AS?!*XpH>8egQvSVX`36jMd>$+RgI|pEg))^djhGSo&#lhS~9%NuWfX zDDH;3T*GzRT@5=7ibO>N-6_XPBYxno@mD_3I#rDD?iADxX`! zh*v8^i*JEMzyN#bGEBz7;UYXki*Xr(9xXax(_1qVW=Ml)kSuvK$coq2A(5ZGhs_pF z$*w}FbN6+QDseuB9=fdp_MTs)nQf!2SlROQ!gBJBCXD&@-VurqHj0wm@LWX-TDmS= z71M__vAok|@!qgi#H&H%Vg-((ZfxPAL8AI{x|VV!9)ZE}_l>iWk8UPTGHs*?u7RfP z5MC&=c6X;XlUzrz5q?(!eO@~* zoh2I*%J7dF!!_!vXoSIn5o|wj1#_>K*&CIn{qSaRc&iFVxt*^20ngCL;QonIS>I5^ zMw8HXm>W0PGd*}Ko)f|~dDd%;Wu_RWI_d;&2g6R3S63Uzjd7dn%Svu-OKpx*o|N>F zZg=-~qLb~VRLpv`k zWSdfHh@?dp=s_X`{yxOlxE$4iuyS;Z-x!*E6eqmEm*j2bE@=ZI0YZ5%Yj29!5+J$4h{s($nakA`xgbO8w zi=*r}PWz#lTL_DSAu1?f%-2OjD}NHXp4pXOsCW;DS@BC3h-q4_l`<))8WgzkdXg3! zs1WMt32kS2E#L0p_|x+x**TFV=gn`m9BWlzF{b%6j-odf4{7a4y4Uaef@YaeuPhU8 zHBvRqN^;$Jizy+ z=zW{E5<>2gp$pH{M@S*!sJVQU)b*J5*bX4h>5VJve#Q6ga}cQ&iL#=(u+KroWrxa%8&~p{WEUF0il=db;-$=A;&9M{Rq`ouZ5m%BHT6%st%saGsD6)fQgLN}x@d3q>FC;=f%O3Cyg=Ke@Gh`XW za@RajqOE9UB6eE=zhG%|dYS)IW)&y&Id2n7r)6p_)vlRP7NJL(x4UbhlcFXWT8?K=%s7;z?Vjts?y2+r|uk8Wt(DM*73^W%pAkZa1Jd zNoE)8FvQA>Z`eR5Z@Ig6kS5?0h;`Y&OL2D&xnnAUzQz{YSdh0k zB3exx%A2TyI)M*EM6htrxSlep!Kk(P(VP`$p0G~f$smld6W1r_Z+o?=IB@^weq>5VYsYZZR@` z&XJFxd5{|KPZmVOSxc@^%71C@;z}}WhbF9p!%yLj3j%YOlPL5s>7I3vj25 z@xmf=*z%Wb4;Va6SDk9cv|r*lhZ`(y_*M@>q;wrn)oQx%B(2A$9(74>;$zmQ!4fN; z>XurIk-7@wZys<+7XL@0Fhe-f%*=(weaQEdR9Eh6>Kl-EcI({qoZqyzziGwpg-GM#251sK_ z=3|kitS!j%;fpc@oWn65SEL73^N&t>Ix37xgs= zYG%eQDJc|rqHFia0!_sm7`@lvcv)gfy(+KXA@E{3t1DaZ$DijWAcA)E0@X?2ziJ{v z&KOYZ|DdkM{}t+@{@*6ge}m%xfjIxi%qh`=^2Rwz@w0cCvZ&Tc#UmCDbVwABrON^x zEBK43FO@weA8s7zggCOWhMvGGE`baZ62cC)VHyy!5Zbt%ieH+XN|OLbAFPZWyC6)p z4P3%8sq9HdS3=ih^0OOlqTPbKuzQ?lBEI{w^ReUO{V?@`ARsL|S*%yOS=Z%sF)>-y z(LAQdhgAcuF6LQjRYfdbD1g4o%tV4EiK&ElLB&^VZHbrV1K>tHTO{#XTo>)2UMm`2 z^t4s;vnMQgf-njU-RVBRw0P0-m#d-u`(kq7NL&2T)TjI_@iKuPAK-@oH(J8?%(e!0Ir$yG32@CGUPn5w4)+9@8c&pGx z+K3GKESI4*`tYlmMHt@br;jBWTei&(a=iYslc^c#RU3Q&sYp zSG){)V<(g7+8W!Wxeb5zJb4XE{I|&Y4UrFWr%LHkdQ;~XU zgy^dH-Z3lmY+0G~?DrC_S4@=>0oM8Isw%g(id10gWkoz2Q%7W$bFk@mIzTCcIB(K8 zc<5h&ZzCdT=9n-D>&a8vl+=ZF*`uTvQviG_bLde*k>{^)&0o*b05x$MO3gVLUx`xZ z43j+>!u?XV)Yp@MmG%Y`+COH2?nQcMrQ%k~6#O%PeD_WvFO~Kct za4XoCM_X!c5vhRkIdV=xUB3xI2NNStK*8_Zl!cFjOvp-AY=D;5{uXj}GV{LK1~IE2 z|KffUiBaStRr;10R~K2VVtf{TzM7FaPm;Y(zQjILn+tIPSrJh&EMf6evaBKIvi42-WYU9Vhj~3< zZSM-B;E`g_o8_XTM9IzEL=9Lb^SPhe(f(-`Yh=X6O7+6ALXnTcUFpI>ekl6v)ZQeNCg2 z^H|{SKXHU*%nBQ@I3It0m^h+6tvI@FS=MYS$ZpBaG7j#V@P2ZuYySbp@hA# ze(kc;P4i_-_UDP?%<6>%tTRih6VBgScKU^BV6Aoeg6Uh(W^#J^V$Xo^4#Ekp ztqQVK^g9gKMTHvV7nb64UU7p~!B?>Y0oFH5T7#BSW#YfSB@5PtE~#SCCg3p^o=NkMk$<8- z6PT*yIKGrvne7+y3}_!AC8NNeI?iTY(&nakN>>U-zT0wzZf-RuyZk^X9H-DT_*wk= z;&0}6LsGtfVa1q)CEUPlx#(ED@-?H<1_FrHU#z5^P3lEB|qsxEyn%FOpjx z3S?~gvoXy~L(Q{Jh6*i~=f%9kM1>RGjBzQh_SaIDfSU_9!<>*Pm>l)cJD@wlyxpBV z4Fmhc2q=R_wHCEK69<*wG%}mgD1=FHi4h!98B-*vMu4ZGW~%IrYSLGU{^TuseqVgV zLP<%wirIL`VLyJv9XG_p8w@Q4HzNt-o;U@Au{7%Ji;53!7V8Rv0^Lu^Vf*sL>R(;c zQG_ZuFl)Mh-xEIkGu}?_(HwkB2jS;HdPLSxVU&Jxy9*XRG~^HY(f0g8Q}iqnVmgjI zfd=``2&8GsycjR?M%(zMjn;tn9agcq;&rR!Hp z$B*gzHsQ~aXw8c|a(L^LW(|`yGc!qOnV(ZjU_Q-4z1&0;jG&vAKuNG=F|H?@m5^N@ zq{E!1n;)kNTJ>|Hb2ODt-7U~-MOIFo%9I)_@7fnX+eMMNh>)V$IXesJpBn|uo8f~#aOFytCT zf9&%MCLf8mp4kwHTcojWmM3LU=#|{3L>E}SKwOd?%{HogCZ_Z1BSA}P#O(%H$;z7XyJ^sjGX;j5 zrzp>|Ud;*&VAU3x#f{CKwY7Vc{%TKKqmB@oTHA9;>?!nvMA;8+Jh=cambHz#J18x~ zs!dF>$*AnsQ{{82r5Aw&^7eRCdvcgyxH?*DV5(I$qXh^zS>us*I66_MbL8y4d3ULj z{S(ipo+T3Ag!+5`NU2sc+@*m{_X|&p#O-SAqF&g_n7ObB82~$p%fXA5GLHMC+#qqL zdt`sJC&6C2)=juQ_!NeD>U8lDVpAOkW*khf7MCcs$A(wiIl#B9HM%~GtQ^}yBPjT@ z+E=|A!Z?A(rwzZ;T}o6pOVqHzTr*i;Wrc%&36kc@jXq~+w8kVrs;%=IFdACoLAcCAmhFNpbP8;s`zG|HC2Gv?I~w4ITy=g$`0qMQdkijLSOtX6xW%Z9Nw<;M- zMN`c7=$QxN00DiSjbVt9Mi6-pjv*j(_8PyV-il8Q-&TwBwH1gz1uoxs6~uU}PrgWB zIAE_I-a1EqlIaGQNbcp@iI8W1sm9fBBNOk(k&iLBe%MCo#?xI$%ZmGA?=)M9D=0t7 zc)Q0LnI)kCy{`jCGy9lYX%mUsDWwsY`;jE(;Us@gmWPqjmXL+Hu#^;k%eT>{nMtzj zsV`Iy6leTA8-PndszF;N^X@CJrTw5IIm!GPeu)H2#FQitR{1p;MasQVAG3*+=9FYK zw*k!HT(YQorfQj+1*mCV458(T5=fH`um$gS38hw(OqVMyunQ;rW5aPbF##A3fGH6h z@W)i9Uff?qz`YbK4c}JzQpuxuE3pcQO)%xBRZp{zJ^-*|oryTxJ-rR+MXJ)!f=+pp z10H|DdGd2exhi+hftcYbM0_}C0ZI-2vh+$fU1acsB-YXid7O|=9L!3e@$H*6?G*Zp z%qFB(sgl=FcC=E4CYGp4CN>=M8#5r!RU!u+FJVlH6=gI5xHVD&k;Ta*M28BsxfMV~ zLz+@6TxnfLhF@5=yQo^1&S}cmTN@m!7*c6z;}~*!hNBjuE>NLVl2EwN!F+)0$R1S! zR|lF%n!9fkZ@gPW|x|B={V6x3`=jS*$Pu0+5OWf?wnIy>Y1MbbGSncpKO0qE(qO=ts z!~@&!N`10S593pVQu4FzpOh!tvg}p%zCU(aV5=~K#bKi zHdJ1>tQSrhW%KOky;iW+O_n;`l9~omqM%sdxdLtI`TrJzN6BQz+7xOl*rM>xVI2~# z)7FJ^Dc{DC<%~VS?@WXzuOG$YPLC;>#vUJ^MmtbSL`_yXtNKa$Hk+l-c!aC7gn(Cg ze?YPYZ(2Jw{SF6MiO5(%_pTo7j@&DHNW`|lD`~{iH+_eSTS&OC*2WTT*a`?|9w1dh zh1nh@$a}T#WE5$7Od~NvSEU)T(W$p$s5fe^GpG+7fdJ9=enRT9$wEk+ZaB>G3$KQO zgq?-rZZnIv!p#>Ty~}c*Lb_jxJg$eGM*XwHUwuQ|o^}b3^T6Bxx{!?va8aC@-xK*H ztJBFvFfsSWu89%@b^l3-B~O!CXs)I6Y}y#0C0U0R0WG zybjroj$io0j}3%P7zADXOwHwafT#uu*zfM!oD$6aJx7+WL%t-@6^rD_a_M?S^>c;z zMK580bZXo1f*L$CuMeM4Mp!;P@}b~$cd(s5*q~FP+NHSq;nw3fbWyH)i2)-;gQl{S zZO!T}A}fC}vUdskGSq&{`oxt~0i?0xhr6I47_tBc`fqaSrMOzR4>0H^;A zF)hX1nfHs)%Zb-(YGX;=#2R6C{BG;k=?FfP?9{_uFLri~-~AJ;jw({4MU7e*d)?P@ zXX*GkNY9ItFjhwgAIWq7Y!ksbMzfqpG)IrqKx9q{zu%Mdl+{Dis#p9q`02pr1LG8R z@As?eG!>IoROgS!@J*to<27coFc1zpkh?w=)h9CbYe%^Q!Ui46Y*HO0mr% zEff-*$ndMNw}H2a5@BsGj5oFfd!T(F&0$<{GO!Qdd?McKkorh=5{EIjDTHU`So>8V zBA-fqVLb2;u7UhDV1xMI?y>fe3~4urv3%PX)lDw+HYa;HFkaLqi4c~VtCm&Ca+9C~ zge+67hp#R9`+Euq59WhHX&7~RlXn=--m8$iZ~~1C8cv^2(qO#X0?vl91gzUKBeR1J z^p4!!&7)3#@@X&2aF2-)1Ffcc^F8r|RtdL2X%HgN&XU-KH2SLCbpw?J5xJ*!F-ypZ zMG%AJ!Pr&}`LW?E!K~=(NJxuSVTRCGJ$2a*Ao=uUDSys!OFYu!Vs2IT;xQ6EubLIl z+?+nMGeQQhh~??0!s4iQ#gm3!BpMpnY?04kK375e((Uc7B3RMj;wE?BCoQGu=UlZt!EZ1Q*auI)dj3Jj{Ujgt zW5hd~-HWBLI_3HuO) zNrb^XzPsTIb=*a69wAAA3J6AAZZ1VsYbIG}a`=d6?PjM)3EPaDpW2YP$|GrBX{q*! z$KBHNif)OKMBCFP5>!1d=DK>8u+Upm-{hj5o|Wn$vh1&K!lVfDB&47lw$tJ?d5|=B z^(_9=(1T3Fte)z^>|3**n}mIX;mMN5v2F#l(q*CvU{Ga`@VMp#%rQkDBy7kYbmb-q z<5!4iuB#Q_lLZ8}h|hPODI^U6`gzLJre9u3k3c#%86IKI*^H-@I48Bi*@avYm4v!n0+v zWu{M{&F8#p9cx+gF0yTB_<2QUrjMPo9*7^-uP#~gGW~y3nfPAoV%amgr>PSyVAd@l)}8#X zR5zV6t*uKJZL}?NYvPVK6J0v4iVpwiN|>+t3aYiZSp;m0!(1`bHO}TEtWR1tY%BPB z(W!0DmXbZAsT$iC13p4f>u*ZAy@JoLAkJhzFf1#4;#1deO8#8d&89}en&z!W&A3++^1(;>0SB1*54d@y&9Pn;^IAf3GiXbfT`_>{R+Xv; zQvgL>+0#8-laO!j#-WB~(I>l0NCMt_;@Gp_f0#^c)t?&#Xh1-7RR0@zPyBz!U#0Av zT?}n({(p?p7!4S2ZBw)#KdCG)uPnZe+U|0{BW!m)9 zi_9$F?m<`2!`JNFv+w8MK_K)qJ^aO@7-Ig>cM4-r0bi=>?B_2mFNJ}aE3<+QCzRr*NA!QjHw# z`1OsvcoD0?%jq{*7b!l|L1+Tw0TTAM4XMq7*ntc-Ived>Sj_ZtS|uVdpfg1_I9knY z2{GM_j5sDC7(W&}#s{jqbybqJWyn?{PW*&cQIU|*v8YGOKKlGl@?c#TCnmnAkAzV- zmK={|1G90zz=YUvC}+fMqts0d4vgA%t6Jhjv?d;(Z}(Ep8fTZfHA9``fdUHkA+z3+ zhh{ohP%Bj?T~{i0sYCQ}uC#5BwN`skI7`|c%kqkyWIQ;!ysvA8H`b-t()n6>GJj6xlYDu~8qX{AFo$Cm3d|XFL=4uvc?Keb zzb0ZmMoXca6Mob>JqkNuoP>B2Z>D`Q(TvrG6m`j}-1rGP!g|qoL=$FVQYxJQjFn33lODt3Wb1j8VR zlR++vIT6^DtYxAv_hxupbLLN3e0%A%a+hWTKDV3!Fjr^cWJ{scsAdfhpI)`Bms^M6 zQG$waKgFr=c|p9Piug=fcJvZ1ThMnNhQvBAg-8~b1?6wL*WyqXhtj^g(Ke}mEfZVM zJuLNTUVh#WsE*a6uqiz`b#9ZYg3+2%=C(6AvZGc=u&<6??!slB1a9K)=VL zY9EL^mfyKnD zSJyYBc_>G;5RRnrNgzJz#Rkn3S1`mZgO`(r5;Hw6MveN(URf_XS-r58Cn80K)ArH4 z#Rrd~LG1W&@ttw85cjp8xV&>$b%nSXH_*W}7Ch2pg$$c0BdEo-HWRTZcxngIBJad> z;C>b{jIXjb_9Jis?NZJsdm^EG}e*pR&DAy0EaSGi3XWTa(>C%tz1n$u?5Fb z1qtl?;_yjYo)(gB^iQq?=jusF%kywm?CJP~zEHi0NbZ);$(H$w(Hy@{i>$wcVRD_X|w-~(0Z9BJyh zhNh;+eQ9BEIs;tPz%jSVnfCP!3L&9YtEP;svoj_bNzeGSQIAjd zBss@A;)R^WAu-37RQrM%{DfBNRx>v!G31Z}8-El9IOJlb_MSoMu2}GDYycNaf>uny z+8xykD-7ONCM!APry_Lw6-yT>5!tR}W;W`C)1>pxSs5o1z#j7%m=&=7O4hz+Lsqm` z*>{+xsabZPr&X=}G@obTb{nPTkccJX8w3CG7X+1+t{JcMabv~UNv+G?txRqXib~c^Mo}`q{$`;EBNJ;#F*{gvS12kV?AZ%O0SFB$^ zn+}!HbmEj}w{Vq(G)OGAzH}R~kS^;(-s&=ectz8vN!_)Yl$$U@HNTI-pV`LSj7Opu zTZ5zZ)-S_{GcEQPIQXLQ#oMS`HPu{`SQiAZ)m1at*Hy%3xma|>o`h%E%8BEbi9p0r zVjcsh<{NBKQ4eKlXU|}@XJ#@uQw*$4BxKn6#W~I4T<^f99~(=}a`&3(ur8R9t+|AQ zWkQx7l}wa48-jO@ft2h+7qn%SJtL%~890FG0s5g*kNbL3I&@brh&f6)TlM`K^(bhr zJWM6N6x3flOw$@|C@kPi7yP&SP?bzP-E|HSXQXG>7gk|R9BTj`e=4de9C6+H7H7n# z#GJeVs1mtHhLDmVO?LkYRQc`DVOJ_vdl8VUihO-j#t=0T3%Fc1f9F73ufJz*adn*p zc%&vi(4NqHu^R>sAT_0EDjVR8bc%wTz#$;%NU-kbDyL_dg0%TFafZwZ?5KZpcuaO54Z9hX zD$u>q!-9`U6-D`E#`W~fIfiIF5_m6{fvM)b1NG3xf4Auw;Go~Fu7cth#DlUn{@~yu z=B;RT*dp?bO}o%4x7k9v{r=Y@^YQ^UUm(Qmliw8brO^=NP+UOohLYiaEB3^DB56&V zK?4jV61B|1Uj_5fBKW;8LdwOFZKWp)g{B%7g1~DgO&N& z#lisxf?R~Z@?3E$Mms$$JK8oe@X`5m98V*aV6Ua}8Xs2#A!{x?IP|N(%nxsH?^c{& z@vY&R1QmQs83BW28qAmJfS7MYi=h(YK??@EhjL-t*5W!p z^gYX!Q6-vBqcv~ruw@oMaU&qp0Fb(dbVzm5xJN%0o_^@fWq$oa3X?9s%+b)x4w-q5Koe(@j6Ez7V@~NRFvd zfBH~)U5!ix3isg`6be__wBJp=1@yfsCMw1C@y+9WYD9_C%{Q~7^0AF2KFryfLlUP# zwrtJEcH)jm48!6tUcxiurAMaiD04C&tPe6DI0#aoqz#Bt0_7_*X*TsF7u*zv(iEfA z;$@?XVu~oX#1YXtceQL{dSneL&*nDug^OW$DSLF0M1Im|sSX8R26&)<0Fbh^*l6!5wfSu8MpMoh=2l z^^0Sr$UpZp*9oqa23fcCfm7`ya2<4wzJ`Axt7e4jJrRFVf?nY~2&tRL* zd;6_njcz01c>$IvN=?K}9ie%Z(BO@JG2J}fT#BJQ+f5LFSgup7i!xWRKw6)iITjZU z%l6hPZia>R!`aZjwCp}I zg)%20;}f+&@t;(%5;RHL>K_&7MH^S+7<|(SZH!u zznW|jz$uA`P9@ZWtJgv$EFp>)K&Gt+4C6#*khZQXS*S~6N%JDT$r`aJDs9|uXWdbg zBwho$phWx}x!qy8&}6y5Vr$G{yGSE*r$^r{}pw zVTZKvikRZ`J_IJrjc=X1uw?estdwm&bEahku&D04HD+0Bm~q#YGS6gp!KLf$A{%Qd z&&yX@Hp>~(wU{|(#U&Bf92+1i&Q*-S+=y=3pSZy$#8Uc$#7oiJUuO{cE6=tsPhwPe| zxQpK>`Dbka`V)$}e6_OXKLB%i76~4N*zA?X+PrhH<&)}prET;kel24kW%+9))G^JI zsq7L{P}^#QsZViX%KgxBvEugr>ZmFqe^oAg?{EI=&_O#e)F3V#rc z8$4}0Zr19qd3tE4#$3_f=Bbx9oV6VO!d3(R===i-7p=Vj`520w0D3W6lQfY48}!D* z&)lZMG;~er2qBoI2gsX+Ts-hnpS~NYRDtPd^FPzn!^&yxRy#CSz(b&E*tL|jIkq|l zf%>)7Dtu>jCf`-7R#*GhGn4FkYf;B$+9IxmqH|lf6$4irg{0ept__%)V*R_OK=T06 zyT_m-o@Kp6U{l5h>W1hGq*X#8*y@<;vsOFqEjTQXFEotR+{3}ODDnj;o0@!bB5x=N z394FojuGOtVKBlVRLtHp%EJv_G5q=AgF)SKyRN5=cGBjDWv4LDn$IL`*=~J7u&Dy5 zrMc83y+w^F&{?X(KOOAl-sWZDb{9X9#jrQtmrEXD?;h-}SYT7yM(X_6qksM=K_a;Z z3u0qT0TtaNvDER_8x*rxXw&C^|h{P1qxK|@pS7vdlZ#P z7PdB7MmC2}%sdzAxt>;WM1s0??`1983O4nFK|hVAbHcZ3x{PzytQLkCVk7hA!Lo` zEJH?4qw|}WH{dc4z%aB=0XqsFW?^p=X}4xnCJXK%c#ItOSjdSO`UXJyuc8bh^Cf}8 z@Ht|vXd^6{Fgai8*tmyRGmD_s_nv~r^Fy7j`Bu`6=G)5H$i7Q7lvQnmea&TGvJp9a|qOrUymZ$6G|Ly z#zOCg++$3iB$!6!>215A4!iryregKuUT344X)jQb3|9qY>c0LO{6Vby05n~VFzd?q zgGZv&FGlkiH*`fTurp>B8v&nSxNz)=5IF$=@rgND4d`!AaaX;_lK~)-U8la_Wa8i?NJC@BURO*sUW)E9oyv3RG^YGfN%BmxzjlT)bp*$<| zX3tt?EAy<&K+bhIuMs-g#=d1}N_?isY)6Ay$mDOKRh z4v1asEGWoAp=srraLW^h&_Uw|6O+r;wns=uwYm=JN4Q!quD8SQRSeEcGh|Eb5Jg8m zOT}u;N|x@aq)=&;wufCc^#)5U^VcZw;d_wwaoh9$p@Xrc{DD6GZUqZ ziC6OT^zSq@-lhbgR8B+e;7_Giv;DK5gn^$bs<6~SUadiosfewWDJu`XsBfOd1|p=q zE>m=zF}!lObA%ePey~gqU8S6h-^J2Y?>7)L2+%8kV}Gp=h`Xm_}rlm)SyUS=`=S7msKu zC|T!gPiI1rWGb1z$Md?0YJQ;%>uPLOXf1Z>N~`~JHJ!^@D5kSXQ4ugnFZ>^`zH8CAiZmp z6Ms|#2gcGsQ{{u7+Nb9sA?U>(0e$5V1|WVwY`Kn)rsnnZ4=1u=7u!4WexZD^IQ1Jk zfF#NLe>W$3m&C^ULjdw+5|)-BSHwpegdyt9NYC{3@QtMfd8GrIWDu`gd0nv-3LpGCh@wgBaG z176tikL!_NXM+Bv#7q^cyn9$XSeZR6#!B4JE@GVH zoobHZN_*RF#@_SVYKkQ_igme-Y5U}cV(hkR#k1c{bQNMji zU7aE`?dHyx=1`kOYZo_8U7?3-7vHOp`Qe%Z*i+FX!s?6huNp0iCEW-Z7E&jRWmUW_ z67j>)Ew!yq)hhG4o?^z}HWH-e=es#xJUhDRc4B51M4~E-l5VZ!&zQq`gWe`?}#b~7w1LH4Xa-UCT5LXkXQWheBa2YJYbyQ zl1pXR%b(KCXMO0OsXgl0P0Og<{(@&z1aokU-Pq`eQq*JYgt8xdFQ6S z6Z3IFSua8W&M#`~*L#r>Jfd6*BzJ?JFdBR#bDv$_0N!_5vnmo@!>vULcDm`MFU823 zpG9pqjqz^FE5zMDoGqhs5OMmC{Y3iVcl>F}5Rs24Y5B^mYQ;1T&ks@pIApHOdrzXF z-SdX}Hf{X;TaSxG_T$0~#RhqKISGKNK47}0*x&nRIPtmdwxc&QT3$8&!3fWu1eZ_P zJveQj^hJL#Sn!*4k`3}(d(aasl&7G0j0-*_2xtAnoX1@9+h zO#c>YQg60Z;o{Bi=3i7S`Ic+ZE>K{(u|#)9y}q*j8uKQ1^>+(BI}m%1v3$=4ojGBc zm+o1*!T&b}-lVvZqIUBc8V}QyFEgm#oyIuC{8WqUNV{Toz`oxhYpP!_p2oHHh5P@iB*NVo~2=GQm+8Yrkm2Xjc_VyHg1c0>+o~@>*Qzo zHVBJS>$$}$_4EniTI;b1WShX<5-p#TPB&!;lP!lBVBbLOOxh6FuYloD%m;n{r|;MU3!q4AVkua~fieeWu2 zQAQ$ue(IklX6+V;F1vCu-&V?I3d42FgWgsb_e^29ol}HYft?{SLf>DrmOp9o!t>I^ zY7fBCk+E8n_|apgM|-;^=#B?6RnFKlN`oR)`e$+;D=yO-(U^jV;rft^G_zl`n7qnM zL z*-Y4Phq+ZI1$j$F-f;`CD#|`-T~OM5Q>x}a>B~Gb3-+9i>Lfr|Ca6S^8g*{*?_5!x zH_N!SoRP=gX1?)q%>QTY!r77e2j9W(I!uAz{T`NdNmPBBUzi2{`XMB^zJGGwFWeA9 z{fk33#*9SO0)DjROug+(M)I-pKA!CX;IY(#gE!UxXVsa)X!UftIN98{pt#4MJHOhY zM$_l}-TJlxY?LS6Nuz1T<44m<4i^8k@D$zuCPrkmz@sdv+{ciyFJG2Zwy&%c7;atIeTdh!a(R^QXnu1Oq1b42*OQFWnyQ zWeQrdvP|w_idy53Wa<{QH^lFmEd+VlJkyiC>6B#s)F;w-{c;aKIm;Kp50HnA-o3lY z9B~F$gJ@yYE#g#X&3ADx&tO+P_@mnQTz9gv30_sTsaGXkfNYXY{$(>*PEN3QL>I!k zp)KibPhrfX3%Z$H6SY`rXGYS~143wZrG2;=FLj50+VM6soI~up_>fU(2Wl@{BRsMi zO%sL3x?2l1cXTF)k&moNsHfQrQ+wu(gBt{sk#CU=UhrvJIncy@tJX5klLjgMn>~h= zg|FR&;@eh|C7`>s_9c~0-{IAPV){l|Ts`i=)AW;d9&KPc3fMeoTS%8@V~D8*h;&(^>yjT84MM}=%#LS7shLAuuj(0VAYoozhWjq z4LEr?wUe2^WGwdTIgWBkDUJa>YP@5d9^Rs$kCXmMRxuF*YMVrn?0NFyPl}>`&dqZb z<5eqR=ZG3>n2{6v6BvJ`YBZeeTtB88TAY(x0a58EWyuf>+^|x8Qa6wA|1Nb_p|nA zWWa}|z8a)--Wj`LqyFk_a3gN2>5{Rl_wbW?#by7&i*^hRknK%jwIH6=dQ8*-_{*x0j^DUfMX0`|K@6C<|1cgZ~D(e5vBFFm;HTZF(!vT8=T$K+|F)x3kqzBV4-=p1V(lzi(s7jdu0>LD#N=$Lk#3HkG!a zIF<7>%B7sRNzJ66KrFV76J<2bdYhxll0y2^_rdG=I%AgW4~)1Nvz=$1UkE^J%BxLo z+lUci`UcU062os*=`-j4IfSQA{w@y|3}Vk?i;&SSdh8n+$iHA#%ERL{;EpXl6u&8@ zzg}?hkEOUOJt?ZL=pWZFJ19mI1@P=$U5*Im1e_8Z${JsM>Ov?nh8Z zP5QvI!{Jy@&BP48%P2{Jr_VgzW;P@7)M9n|lDT|Ep#}7C$&ud&6>C^5ZiwKIg2McPU(4jhM!BD@@L(Gd*Nu$ji(ljZ<{FIeW_1Mmf;76{LU z-ywN~=uNN)Xi6$<12A9y)K%X|(W0p|&>>4OXB?IiYr||WKDOJPxiSe01NSV-h24^L z_>m$;|C+q!Mj**-qQ$L-*++en(g|hw;M!^%_h-iDjFHLo-n3JpB;p?+o2;`*jpvJU zLY^lt)Un4joij^^)O(CKs@7E%*!w>!HA4Q?0}oBJ7Nr8NQ7QmY^4~jvf0-`%waOLn zdNjAPaC0_7c|RVhw)+71NWjRi!y>C+Bl;Z`NiL^zn2*0kmj5gyhCLCxts*cWCdRI| zjsd=sT5BVJc^$GxP~YF$-U{-?kW6r@^vHXB%{CqYzU@1>dzf#3SYedJG-Rm6^RB7s zGM5PR(yKPKR)>?~vpUIeTP7A1sc8-knnJk*9)3t^e%izbdm>Y=W{$wm(cy1RB-19i za#828DMBY+ps#7Y8^6t)=Ea@%Nkt)O6JCx|ybC;Ap}Z@Zw~*}3P>MZLPb4Enxz9Wf zssobT^(R@KuShj8>@!1M7tm|2%-pYYDxz-5`rCbaTCG5{;Uxm z*g=+H1X8{NUvFGzz~wXa%Eo};I;~`37*WrRU&K0dPSB$yk(Z*@K&+mFal^?c zurbqB-+|Kb5|sznT;?Pj!+kgFY1#Dr;_%A(GIQC{3ct|{*Bji%FNa6c-thbpBkA;U zURV!Dr&X{0J}iht#-Qp2=xzuh(fM>zRoiGrYl5ttw2#r34gC41CCOC31m~^UPTK@s z6;A@)7O7_%C)>bnAXerYuAHdE93>j2N}H${zEc6&SbZ|-fiG*-qtGuy-qDelH(|u$ zorf8_T6Zqe#Ub!+e3oSyrskt_HyW_^5lrWt#30l)tHk|j$@YyEkXUOV;6B51L;M@=NIWZXU;GrAa(LGxO%|im%7F<-6N;en0Cr zLH>l*y?pMwt`1*cH~LdBPFY_l;~`N!Clyfr;7w<^X;&(ZiVdF1S5e(+Q%60zgh)s4 zn2yj$+mE=miVERP(g8}G4<85^-5f@qxh2ec?n+$A_`?qN=iyT1?U@t?V6DM~BIlBB z>u~eXm-aE>R0sQy!-I4xtCNi!!qh?R1!kKf6BoH2GG{L4%PAz0{Sh6xpuyI%*~u)s z%rLuFl)uQUCBQAtMyN;%)zFMx4loh7uTfKeB2Xif`lN?2gq6NhWhfz0u5WP9J>=V2 zo{mLtSy&BA!mSzs&CrKWq^y40JF5a&GSXIi2= z{EYb59J4}VwikL4P=>+mc6{($FNE@e=VUwG+KV21;<@lrN`mnz5jYGASyvz7BOG_6(p^eTxD-4O#lROgon;R35=|nj#eHIfJBYPWG>H>`dHKCDZ3`R{-?HO0mE~(5_WYcFmp8sU?wr*UkAQiNDGc6T zA%}GOLXlOWqL?WwfHO8MB#8M8*~Y*gz;1rWWoVSXP&IbKxbQ8+s%4Jnt?kDsq7btI zCDr0PZ)b;B%!lu&CT#RJzm{l{2fq|BcY85`w~3LSK<><@(2EdzFLt9Y_`;WXL6x`0 zDoQ?=?I@Hbr;*VVll1Gmd8*%tiXggMK81a+T(5Gx6;eNb8=uYn z5BG-0g>pP21NPn>$ntBh>`*})Fl|38oC^9Qz>~MAazH%3Q~Qb!ALMf$srexgPZ2@&c~+hxRi1;}+)-06)!#Mq<6GhP z-Q?qmgo${aFBApb5p}$1OJKTClfi8%PpnczyVKkoHw7Ml9e7ikrF0d~UB}i3vizos zXW4DN$SiEV9{faLt5bHy2a>33K%7Td-n5C*N;f&ZqAg#2hIqEb(y<&f4u5BWJ>2^4 z414GosL=Aom#m&=x_v<0-fp1r%oVJ{T-(xnomNJ(Dryv zh?vj+%=II_nV+@NR+(!fZZVM&(W6{6%9cm+o+Z6}KqzLw{(>E86uA1`_K$HqINlb1 zKelh3-jr2I9V?ych`{hta9wQ2c9=MM`2cC{m6^MhlL2{DLv7C^j z$xXBCnDl_;l|bPGMX@*tV)B!c|4oZyftUlP*?$YU9C_eAsuVHJ58?)zpbr30P*C`T z7y#ao`uE-SOG(Pi+`$=e^mle~)pRrdwL5)N;o{gpW21of(QE#U6w%*C~`v-z0QqBML!!5EeYA5IQB0 z^l01c;L6E(iytN!LhL}wfwP7W9PNAkb+)Cst?qg#$n;z41O4&v+8-zPs+XNb-q zIeeBCh#ivnFLUCwfS;p{LC0O7tm+Sf9Jn)~b%uwP{%69;QC)Ok0t%*a5M+=;y8j=v z#!*pp$9@!x;UMIs4~hP#pnfVc!%-D<+wsG@R2+J&%73lK|2G!EQC)O05TCV=&3g)C!lT=czLpZ@Sa%TYuoE?v8T8`V;e$#Zf2_Nj6nvBgh1)2 GZ~q4|mN%#X literal 61608 zcmb5VV{~QRw)Y#`wrv{~+qP{x72B%VwzFc}c2cp;N~)5ZbDrJayPv(!dGEd-##*zr z)#n-$y^sH|_dchh3@8{H5D*j;5D<{i*8l5IFJ|DjL!e)upfGNX(kojugZ3I`oH1PvW`wFW_ske0j@lB9bX zO;2)`y+|!@X(fZ1<2n!Qx*)_^Ai@Cv-dF&(vnudG?0CsddG_&Wtae(n|K59ew)6St z#dj7_(Cfwzh$H$5M!$UDd8=4>IQsD3xV=lXUq($;(h*$0^yd+b{qq63f0r_de#!o_ zXDngc>zy`uor)4A^2M#U*DC~i+dc<)Tb1Tv&~Ev@oM)5iJ4Sn#8iRw16XXuV50BS7 zdBL5Mefch(&^{luE{*5qtCZk$oFr3RH=H!c3wGR=HJ(yKc_re_X9pD` zJ;uxPzUfVpgU>DSq?J;I@a+10l0ONXPcDkiYcihREt5~T5Gb}sT0+6Q;AWHl`S5dV>lv%-p9l#xNNy7ZCr%cyqHY%TZ8Q4 zbp&#ov1*$#grNG#1vgfFOLJCaNG@K|2!W&HSh@3@Y%T?3YI75bJp!VP*$*!< z;(ffNS_;@RJ`=c7yX04!u3JP*<8jeqLHVJu#WV&v6wA!OYJS4h<_}^QI&97-;=ojW zQ-1t)7wnxG*5I%U4)9$wlv5Fr;cIizft@&N+32O%B{R1POm$oap@&f| zh+5J{>U6ftv|vAeKGc|zC=kO(+l7_cLpV}-D#oUltScw})N>~JOZLU_0{Ka2e1evz z{^a*ZrLr+JUj;)K&u2CoCAXLC2=fVScI(m_p~0FmF>>&3DHziouln?;sxW`NB}cSX z8?IsJB)Z=aYRz!X=yJn$kyOWK%rCYf-YarNqKzmWu$ZvkP12b4qH zhS9Q>j<}(*frr?z<%9hl*i^#@*O2q(Z^CN)c2c z>1B~D;@YpG?G!Yk+*yn4vM4sO-_!&m6+`k|3zd;8DJnxsBYtI;W3We+FN@|tQ5EW= z!VU>jtim0Mw#iaT8t_<+qKIEB-WwE04lBd%Letbml9N!?SLrEG$nmn7&W(W`VB@5S zaY=sEw2}i@F_1P4OtEw?xj4@D6>_e=m=797#hg}f*l^`AB|Y0# z9=)o|%TZFCY$SzgSjS|8AI-%J4x}J)!IMxY3_KYze`_I=c1nmrk@E8c9?MVRu)7+Ue79|)rBX7tVB7U|w4*h(;Gi3D9le49B38`wuv zp7{4X^p+K4*$@gU(Tq3K1a#3SmYhvI42)GzG4f|u zwQFT1n_=n|jpi=70-yE9LA+d*T8u z`=VmmXJ_f6WmZveZPct$Cgu^~gFiyL>Lnpj*6ee>*0pz=t$IJ}+rE zsf@>jlcG%Wx;Cp5x)YSVvB1$yyY1l&o zvwX=D7k)Dn;ciX?Z)Pn8$flC8#m`nB&(8?RSdBvr?>T9?E$U3uIX7T?$v4dWCa46 z+&`ot8ZTEgp7G+c52oHJ8nw5}a^dwb_l%MOh(ebVj9>_koQP^$2B~eUfSbw9RY$_< z&DDWf2LW;b0ZDOaZ&2^i^g+5uTd;GwO(-bbo|P^;CNL-%?9mRmxEw~5&z=X^Rvbo^WJW=n_%*7974RY}JhFv46> zd}`2|qkd;89l}R;i~9T)V-Q%K)O=yfVKNM4Gbacc7AOd>#^&W&)Xx!Uy5!BHnp9kh z`a(7MO6+Ren#>R^D0K)1sE{Bv>}s6Rb9MT14u!(NpZOe-?4V=>qZ>}uS)!y~;jEUK z&!U7Fj&{WdgU#L0%bM}SYXRtM5z!6M+kgaMKt%3FkjWYh=#QUpt$XX1!*XkpSq-pl zhMe{muh#knk{9_V3%qdDcWDv}v)m4t9 zQhv{;} zc{}#V^N3H>9mFM8`i`0p+fN@GqX+kl|M94$BK3J-X`Hyj8r!#x6Vt(PXjn?N)qedP z=o1T^#?1^a{;bZ&x`U{f?}TMo8ToN zkHj5v|}r}wDEi7I@)Gj+S1aE-GdnLN+$hw!=DzglMaj#{qjXi_dwpr|HL(gcCXwGLEmi|{4&4#OZ4ChceA zKVd4K!D>_N=_X;{poT~4Q+!Le+ZV>=H7v1*l%w`|`Dx8{)McN@NDlQyln&N3@bFpV z_1w~O4EH3fF@IzJ9kDk@7@QctFq8FbkbaH7K$iX=bV~o#gfh?2JD6lZf(XP>~DACF)fGFt)X%-h1yY~MJU{nA5 ze2zxWMs{YdX3q5XU*9hOH0!_S24DOBA5usB+Ws$6{|AMe*joJ?RxfV}*7AKN9V*~J zK+OMcE@bTD>TG1*yc?*qGqjBN8mgg@h1cJLDv)0!WRPIkC` zZrWXrceVw;fB%3`6kq=a!pq|hFIsQ%ZSlo~)D z|64!aCnw-?>}AG|*iOl44KVf8@|joXi&|)1rB;EQWgm+iHfVbgllP$f!$Wf42%NO5b(j9Bw6L z;0dpUUK$5GX4QbMlTmLM_jJt!ur`_0~$b#BB7FL*%XFf<b__1o)Ao3rlobbN8-(T!1d-bR8D3S0@d zLI!*GMb5s~Q<&sjd}lBb8Nr0>PqE6_!3!2d(KAWFxa{hm`@u|a(%#i(#f8{BP2wbs zt+N_slWF4IF_O|{w`c~)Xvh&R{Au~CFmW#0+}MBd2~X}t9lz6*E7uAD`@EBDe$>7W zzPUkJx<`f$0VA$=>R57^(K^h86>09?>_@M(R4q($!Ck6GG@pnu-x*exAx1jOv|>KH zjNfG5pwm`E-=ydcb+3BJwuU;V&OS=6yM^4Jq{%AVqnTTLwV`AorIDD}T&jWr8pB&j28fVtk_y*JRP^t@l*($UZ z6(B^-PBNZ+z!p?+e8@$&jCv^EWLb$WO=}Scr$6SM*&~B95El~;W_0(Bvoha|uQ1T< zO$%_oLAwf1bW*rKWmlD+@CP&$ObiDy=nh1b2ejz%LO9937N{LDe7gle4i!{}I$;&Y zkexJ9Ybr+lrCmKWg&}p=`2&Gf10orS?4$VrzWidT=*6{KzOGMo?KI0>GL0{iFWc;C z+LPq%VH5g}6V@-tg2m{C!-$fapJ9y}c$U}aUmS{9#0CM*8pC|sfer!)nG7Ji>mfRh z+~6CxNb>6eWKMHBz-w2{mLLwdA7dA-qfTu^A2yG1+9s5k zcF=le_UPYG&q!t5Zd_*E_P3Cf5T6821bO`daa`;DODm8Ih8k89=RN;-asHIigj`n=ux>*f!OC5#;X5i;Q z+V!GUy0|&Y_*8k_QRUA8$lHP;GJ3UUD08P|ALknng|YY13)}!!HW@0z$q+kCH%xet zlWf@BXQ=b=4}QO5eNnN~CzWBbHGUivG=`&eWK}beuV*;?zt=P#pM*eTuy3 zP}c#}AXJ0OIaqXji78l;YrP4sQe#^pOqwZUiiN6^0RCd#D271XCbEKpk`HI0IsN^s zES7YtU#7=8gTn#lkrc~6)R9u&SX6*Jk4GFX7){E)WE?pT8a-%6P+zS6o&A#ml{$WX zABFz#i7`DDlo{34)oo?bOa4Z_lNH>n;f0nbt$JfAl~;4QY@}NH!X|A$KgMmEsd^&Y zt;pi=>AID7ROQfr;MsMtClr5b0)xo|fwhc=qk33wQ|}$@?{}qXcmECh>#kUQ-If0$ zseb{Wf4VFGLNc*Rax#P8ko*=`MwaR-DQ8L8V8r=2N{Gaips2_^cS|oC$+yScRo*uF zUO|5=?Q?{p$inDpx*t#Xyo6=s?bbN}y>NNVxj9NZCdtwRI70jxvm3!5R7yiWjREEd zDUjrsZhS|P&|Ng5r+f^kA6BNN#|Se}_GF>P6sy^e8kBrgMv3#vk%m}9PCwUWJg-AD zFnZ=}lbi*mN-AOm zCs)r=*YQAA!`e#1N>aHF=bb*z*hXH#Wl$z^o}x##ZrUc=kh%OHWhp=7;?8%Xj||@V?1c ziWoaC$^&04;A|T)!Zd9sUzE&$ODyJaBpvqsw19Uiuq{i#VK1!htkdRWBnb z`{rat=nHArT%^R>u#CjjCkw-7%g53|&7z-;X+ewb?OLWiV|#nuc8mp*LuGSi3IP<<*Wyo9GKV7l0Noa4Jr0g3p_$ z*R9{qn=?IXC#WU>48-k5V2Oc_>P;4_)J@bo1|pf=%Rcbgk=5m)CJZ`caHBTm3%!Z9 z_?7LHr_BXbKKr=JD!%?KhwdYSdu8XxPoA{n8^%_lh5cjRHuCY9Zlpz8g+$f@bw@0V z+6DRMT9c|>1^3D|$Vzc(C?M~iZurGH2pXPT%F!JSaAMdO%!5o0uc&iqHx?ImcX6fI zCApkzc~OOnfzAd_+-DcMp&AOQxE_EsMqKM{%dRMI5`5CT&%mQO?-@F6tE*xL?aEGZ z8^wH@wRl`Izx4sDmU>}Ym{ybUm@F83qqZPD6nFm?t?(7>h*?`fw)L3t*l%*iw0Qu#?$5eq!Qc zpQvqgSxrd83NsdO@lL6#{%lsYXWen~d3p4fGBb7&5xqNYJ)yn84!e1PmPo7ChVd%4 zHUsV0Mh?VpzZD=A6%)Qrd~i7 z96*RPbid;BN{Wh?adeD_p8YU``kOrGkNox3D9~!K?w>#kFz!4lzOWR}puS(DmfjJD z`x0z|qB33*^0mZdM&6$|+T>fq>M%yoy(BEjuh9L0>{P&XJ3enGpoQRx`v6$txXt#c z0#N?b5%srj(4xmPvJxrlF3H%OMB!jvfy z;wx8RzU~lb?h_}@V=bh6p8PSb-dG|-T#A?`c&H2`_!u+uenIZe`6f~A7r)`9m8atC zt(b|6Eg#!Q*DfRU=Ix`#B_dK)nnJ_+>Q<1d7W)eynaVn`FNuN~%B;uO2}vXr5^zi2 z!ifIF5@Zlo0^h~8+ixFBGqtweFc`C~JkSq}&*a3C}L?b5Mh-bW=e)({F_g4O3 zb@SFTK3VD9QuFgFnK4Ve_pXc3{S$=+Z;;4+;*{H}Rc;845rP?DLK6G5Y-xdUKkA6E3Dz&5f{F^FjJQ(NSpZ8q-_!L3LL@H* zxbDF{gd^U3uD;)a)sJwAVi}7@%pRM&?5IaUH%+m{E)DlA_$IA1=&jr{KrhD5q&lTC zAa3c)A(K!{#nOvenH6XrR-y>*4M#DpTTOGQEO5Jr6kni9pDW`rvY*fs|ItV;CVITh z=`rxcH2nEJpkQ^(;1c^hfb8vGN;{{oR=qNyKtR1;J>CByul*+=`NydWnSWJR#I2lN zTvgnR|MBx*XFsfdA&;tr^dYaqRZp*2NwkAZE6kV@1f{76e56eUmGrZ>MDId)oqSWw z7d&r3qfazg+W2?bT}F)4jD6sWaw`_fXZGY&wnGm$FRPFL$HzVTH^MYBHWGCOk-89y zA+n+Q6EVSSCpgC~%uHfvyg@ufE^#u?JH?<73A}jj5iILz4Qqk5$+^U(SX(-qv5agK znUkfpke(KDn~dU0>gdKqjTkVk`0`9^0n_wzXO7R!0Thd@S;U`y)VVP&mOd-2 z(hT(|$=>4FY;CBY9#_lB$;|Wd$aOMT5O_3}DYXEHn&Jrc3`2JiB`b6X@EUOD zVl0S{ijm65@n^19T3l%>*;F(?3r3s?zY{thc4%AD30CeL_4{8x6&cN}zN3fE+x<9; zt2j1RRVy5j22-8U8a6$pyT+<`f+x2l$fd_{qEp_bfxfzu>ORJsXaJn4>U6oNJ#|~p z`*ZC&NPXl&=vq2{Ne79AkQncuxvbOG+28*2wU$R=GOmns3W@HE%^r)Fu%Utj=r9t` zd;SVOnA(=MXgnOzI2@3SGKHz8HN~Vpx&!Ea+Df~`*n@8O=0!b4m?7cE^K*~@fqv9q zF*uk#1@6Re_<^9eElgJD!nTA@K9C732tV~;B`hzZ321Ph=^BH?zXddiu{Du5*IPg} zqDM=QxjT!Rp|#Bkp$(mL)aar)f(dOAXUiw81pX0DC|Y4;>Vz>>DMshoips^8Frdv} zlTD=cKa48M>dR<>(YlLPOW%rokJZNF2gp8fwc8b2sN+i6&-pHr?$rj|uFgktK@jg~ zIFS(%=r|QJ=$kvm_~@n=ai1lA{7Z}i+zj&yzY+!t$iGUy|9jH#&oTNJ;JW-3n>DF+ z3aCOzqn|$X-Olu_p7brzn`uk1F*N4@=b=m;S_C?#hy{&NE#3HkATrg?enaVGT^$qIjvgc61y!T$9<1B@?_ibtDZ{G zeXInVr5?OD_nS_O|CK3|RzzMmu+8!#Zb8Ik;rkIAR%6?$pN@d<0dKD2c@k2quB%s( zQL^<_EM6ow8F6^wJN1QcPOm|ehA+dP(!>IX=Euz5qqIq}Y3;ibQtJnkDmZ8c8=Cf3 zu`mJ!Q6wI7EblC5RvP*@)j?}W=WxwCvF3*5Up_`3*a~z$`wHwCy)2risye=1mSp%p zu+tD6NAK3o@)4VBsM!@);qgsjgB$kkCZhaimHg&+k69~drbvRTacWKH;YCK(!rC?8 zP#cK5JPHSw;V;{Yji=55X~S+)%(8fuz}O>*F3)hR;STU`z6T1aM#Wd+FP(M5*@T1P z^06O;I20Sk!bxW<-O;E081KRdHZrtsGJflFRRFS zdi5w9OVDGSL3 zNrC7GVsGN=b;YH9jp8Z2$^!K@h=r-xV(aEH@#JicPy;A0k1>g1g^XeR`YV2HfmqXY zYbRwaxHvf}OlCAwHoVI&QBLr5R|THf?nAevV-=~V8;gCsX>jndvNOcFA+DI+zbh~# zZ7`qNk&w+_+Yp!}j;OYxIfx_{f0-ONc?mHCiCUak=>j>~>YR4#w# zuKz~UhT!L~GfW^CPqG8Lg)&Rc6y^{%3H7iLa%^l}cw_8UuG;8nn9)kbPGXS}p3!L_ zd#9~5CrH8xtUd?{d2y^PJg+z(xIfRU;`}^=OlehGN2=?}9yH$4Rag}*+AWotyxfCJ zHx=r7ZH>j2kV?%7WTtp+-HMa0)_*DBBmC{sd$)np&GEJ__kEd`xB5a2A z*J+yx>4o#ZxwA{;NjhU*1KT~=ZK~GAA;KZHDyBNTaWQ1+;tOFFthnD)DrCn`DjBZ% zk$N5B4^$`n^jNSOr=t(zi8TN4fpaccsb`zOPD~iY=UEK$0Y70bG{idLx@IL)7^(pL z{??Bnu=lDeguDrd%qW1)H)H`9otsOL-f4bSu};o9OXybo6J!Lek`a4ff>*O)BDT_g z<6@SrI|C9klY(>_PfA^qai7A_)VNE4c^ZjFcE$Isp>`e5fLc)rg@8Q_d^Uk24$2bn z9#}6kZ2ZxS9sI(RqT7?El2@B+($>eBQrNi_k#CDJ8D9}8$mmm z4oSKO^F$i+NG)-HE$O6s1--6EzJa?C{x=QgK&c=)b(Q9OVoAXYEEH20G|q$}Hue%~ zO3B^bF=t7t48sN zWh_zA`w~|){-!^g?6Mqf6ieV zFx~aPUOJGR=4{KsW7I?<=J2|lY`NTU=lt=%JE9H1vBpkcn=uq(q~=?iBt_-r(PLBM zP-0dxljJO>4Wq-;stY)CLB4q`-r*T$!K2o}?E-w_i>3_aEbA^MB7P5piwt1dI-6o!qWCy0 ztYy!x9arGTS?kabkkyv*yxvsPQ7Vx)twkS6z2T@kZ|kb8yjm+^$|sEBmvACeqbz)RmxkkDQX-A*K!YFziuhwb|ym>C$}U|J)4y z$(z#)GH%uV6{ec%Zy~AhK|+GtG8u@c884Nq%w`O^wv2#A(&xH@c5M`Vjk*SR_tJnq z0trB#aY)!EKW_}{#L3lph5ow=@|D5LzJYUFD6 z7XnUeo_V0DVSIKMFD_T0AqAO|#VFDc7c?c-Q%#u00F%!_TW1@JVnsfvm@_9HKWflBOUD~)RL``-!P;(bCON_4eVdduMO>?IrQ__*zE@7(OX zUtfH@AX*53&xJW*Pu9zcqxGiM>xol0I~QL5B%Toog3Jlenc^WbVgeBvV8C8AX^Vj& z^I}H})B=VboO%q1;aU5ACMh{yK4J;xlMc`jCnZR^!~LDs_MP&8;dd@4LDWw~*>#OT zeZHwdQWS!tt5MJQI~cw|Ka^b4c|qyd_ly(+Ql2m&AAw^ zQeSXDOOH!!mAgzAp0z)DD>6Xo``b6QwzUV@w%h}Yo>)a|xRi$jGuHQhJVA%>)PUvK zBQ!l0hq<3VZ*RnrDODP)>&iS^wf64C;MGqDvx>|p;35%6(u+IHoNbK z;Gb;TneFo*`zUKS6kwF*&b!U8e5m4YAo03a_e^!5BP42+r)LFhEy?_7U1IR<; z^0v|DhCYMSj<-;MtY%R@Fg;9Kky^pz_t2nJfKWfh5Eu@_l{^ph%1z{jkg5jQrkvD< z#vdK!nku*RrH~TdN~`wDs;d>XY1PH?O<4^U4lmA|wUW{Crrv#r%N>7k#{Gc44Fr|t z@UZP}Y-TrAmnEZ39A*@6;ccsR>)$A)S>$-Cj!=x$rz7IvjHIPM(TB+JFf{ehuIvY$ zsDAwREg*%|=>Hw$`us~RP&3{QJg%}RjJKS^mC_!U;E5u>`X`jW$}P`Mf}?7G7FX#{ zE(9u1SO;3q@ZhDL9O({-RD+SqqPX)`0l5IQu4q)49TUTkxR(czeT}4`WV~pV*KY&i zAl3~X%D2cPVD^B43*~&f%+Op)wl<&|D{;=SZwImydWL6@_RJjxP2g)s=dH)u9Npki zs~z9A+3fj0l?yu4N0^4aC5x)Osnm0qrhz@?nwG_`h(71P znbIewljU%T*cC=~NJy|)#hT+lx#^5MuDDnkaMb*Efw9eThXo|*WOQzJ*#3dmRWm@! zfuSc@#kY{Um^gBc^_Xdxnl!n&y&}R4yAbK&RMc+P^Ti;YIUh|C+K1|=Z^{nZ}}rxH*v{xR!i%qO~o zTr`WDE@k$M9o0r4YUFFeQO7xCu_Zgy)==;fCJ94M_rLAv&~NhfvcLWCoaGg2ao~3e zBG?Ms9B+efMkp}7BhmISGWmJsKI@a8b}4lLI48oWKY|8?zuuNc$lt5Npr+p7a#sWu zh!@2nnLBVJK!$S~>r2-pN||^w|fY`CT{TFnJy`B|e5;=+_v4l8O-fkN&UQbA4NKTyntd zqK{xEKh}U{NHoQUf!M=2(&w+eef77VtYr;xs%^cPfKLObyOV_9q<(%76-J%vR>w9!us-0c-~Y?_EVS%v!* z15s2s3eTs$Osz$JayyH|5nPAIPEX=U;r&p;K14G<1)bvn@?bM5kC{am|C5%hyxv}a z(DeSKI5ZfZ1*%dl8frIX2?);R^^~LuDOpNpk-2R8U1w92HmG1m&|j&J{EK=|p$;f9 z7Rs5|jr4r8k5El&qcuM+YRlKny%t+1CgqEWO>3;BSRZi(LA3U%Jm{@{y+A+w(gzA< z7dBq6a1sEWa4cD0W7=Ld9z0H7RI^Z7vl(bfA;72j?SWCo`#5mVC$l1Q2--%V)-uN* z9ha*s-AdfbDZ8R8*fpwjzx=WvOtmSzGFjC#X)hD%Caeo^OWjS(3h|d9_*U)l%{Ab8 zfv$yoP{OuUl@$(-sEVNt{*=qi5P=lpxWVuz2?I7Dc%BRc+NGNw+323^ z5BXGfS71oP^%apUo(Y#xkxE)y?>BFzEBZ}UBbr~R4$%b7h3iZu3S(|A;&HqBR{nK& z$;GApNnz=kNO^FL&nYcfpB7Qg;hGJPsCW44CbkG1@l9pn0`~oKy5S777uH)l{irK!ru|X+;4&0D;VE*Ii|<3P zUx#xUqvZT5kVQxsF#~MwKnv7;1pR^0;PW@$@T7I?s`_rD1EGUdSA5Q(C<>5SzE!vw z;{L&kKFM-MO>hy#-8z`sdVx})^(Dc-dw;k-h*9O2_YZw}|9^y-|8RQ`BWJUJL(Cer zP5Z@fNc>pTXABbTRY-B5*MphpZv6#i802giwV&SkFCR zGMETyUm(KJbh+&$8X*RB#+{surjr;8^REEt`2&Dubw3$mx>|~B5IKZJ`s_6fw zKAZx9&PwBqW1Oz0r0A4GtnZd7XTKViX2%kPfv+^X3|_}RrQ2e3l=KG_VyY`H?I5&CS+lAX5HbA%TD9u6&s#v!G> zzW9n4J%d5ye7x0y`*{KZvqyXUfMEE^ZIffzI=Hh|3J}^yx7eL=s+TPH(Q2GT-sJ~3 zI463C{(ag7-hS1ETtU;_&+49ABt5!A7CwLwe z=SoA8mYZIQeU;9txI=zcQVbuO%q@E)JI+6Q!3lMc=Gbj(ASg-{V27u>z2e8n;Nc*pf}AqKz1D>p9G#QA+7mqqrEjGfw+85Uyh!=tTFTv3|O z+)-kFe_8FF_EkTw!YzwK^Hi^_dV5x-Ob*UWmD-})qKj9@aE8g240nUh=g|j28^?v7 zHRTBo{0KGaWBbyX2+lx$wgXW{3aUab6Bhm1G1{jTC7ota*JM6t+qy)c5<@ zpc&(jVdTJf(q3xB=JotgF$X>cxh7k*(T`-V~AR+`%e?YOeALQ2Qud( zz35YizXt(aW3qndR}fTw1p()Ol4t!D1pitGNL95{SX4ywzh0SF;=!wf=?Q?_h6!f* zh7<+GFi)q|XBsvXZ^qVCY$LUa{5?!CgwY?EG;*)0ceFe&=A;!~o`ae}Z+6me#^sv- z1F6=WNd6>M(~ z+092z>?Clrcp)lYNQl9jN-JF6n&Y0mp7|I0dpPx+4*RRK+VQI~>en0Dc;Zfl+x z_e_b7s`t1_A`RP3$H}y7F9_na%D7EM+**G_Z0l_nwE+&d_kc35n$Fxkd4r=ltRZhh zr9zER8>j(EdV&Jgh(+i}ltESBK62m0nGH6tCBr90!4)-`HeBmz54p~QP#dsu%nb~W z7sS|(Iydi>C@6ZM(Us!jyIiszMkd)^u<1D+R@~O>HqZIW&kearPWmT>63%_t2B{_G zX{&a(gOYJx!Hq=!T$RZ&<8LDnxsmx9+TBL0gTk$|vz9O5GkK_Yx+55^R=2g!K}NJ3 zW?C;XQCHZl7H`K5^BF!Q5X2^Mj93&0l_O3Ea3!Ave|ixx+~bS@Iv18v2ctpSt4zO{ zp#7pj!AtDmti$T`e9{s^jf(ku&E|83JIJO5Qo9weT6g?@vX!{7)cNwymo1+u(YQ94 zopuz-L@|5=h8A!(g-MXgLJC0MA|CgQF8qlonnu#j z;uCeq9ny9QSD|p)9sp3ebgY3rk#y0DA(SHdh$DUm^?GI<>%e1?&}w(b zdip1;P2Z=1wM+$q=TgLP$}svd!vk+BZ@h<^4R=GS2+sri7Z*2f`9 z5_?i)xj?m#pSVchk-SR!2&uNhzEi+#5t1Z$o0PoLGz*pT64%+|Wa+rd5Z}60(j?X= z{NLjtgRb|W?CUADqOS@(*MA-l|E342NxRaxLTDqsOyfWWe%N(jjBh}G zm7WPel6jXijaTiNita+z(5GCO0NM=Melxud57PP^d_U## zbA;9iVi<@wr0DGB8=T9Ab#2K_#zi=$igyK48@;V|W`fg~7;+!q8)aCOo{HA@vpSy-4`^!ze6-~8|QE||hC{ICKllG9fbg_Y7v z$jn{00!ob3!@~-Z%!rSZ0JO#@>|3k10mLK0JRKP-Cc8UYFu>z93=Ab-r^oL2 zl`-&VBh#=-?{l1TatC;VweM^=M7-DUE>m+xO7Xi6vTEsReyLs8KJ+2GZ&rxw$d4IT zPXy6pu^4#e;;ZTsgmG+ZPx>piodegkx2n0}SM77+Y*j^~ICvp#2wj^BuqRY*&cjmL zcKp78aZt>e{3YBb4!J_2|K~A`lN=u&5j!byw`1itV(+Q_?RvV7&Z5XS1HF)L2v6ji z&kOEPmv+k_lSXb{$)of~(BkO^py&7oOzpjdG>vI1kcm_oPFHy38%D4&A4h_CSo#lX z2#oqMCTEP7UvUR3mwkPxbl8AMW(e{ARi@HCYLPSHE^L<1I}OgZD{I#YH#GKnpRmW3 z2jkz~Sa(D)f?V?$gNi?6)Y;Sm{&?~2p=0&BUl_(@hYeX8YjaRO=IqO7neK0RsSNdYjD zaw$g2sG(>JR=8Iz1SK4`*kqd_3-?;_BIcaaMd^}<@MYbYisWZm2C2|Np_l|8r9yM|JkUngSo@?wci(7&O9a z%|V(4C1c9pps0xxzPbXH=}QTxc2rr7fXk$9`a6TbWKPCz&p=VsB8^W96W=BsB|7bc zf(QR8&Ktj*iz)wK&mW`#V%4XTM&jWNnDF56O+2bo<3|NyUhQ%#OZE8$Uv2a@J>D%t zMVMiHh?es!Ex19q&6eC&L=XDU_BA&uR^^w>fpz2_`U87q_?N2y;!Z!bjoeKrzfC)} z?m^PM=(z{%n9K`p|7Bz$LuC7!>tFOuN74MFELm}OD9?%jpT>38J;=1Y-VWtZAscaI z_8jUZ#GwWz{JqvGEUmL?G#l5E=*m>`cY?m*XOc*yOCNtpuIGD+Z|kn4Xww=BLrNYS zGO=wQh}Gtr|7DGXLF%|`G>J~l{k^*{;S-Zhq|&HO7rC_r;o`gTB7)uMZ|WWIn@e0( zX$MccUMv3ABg^$%_lNrgU{EVi8O^UyGHPNRt%R!1#MQJn41aD|_93NsBQhP80yP<9 zG4(&0u7AtJJXLPcqzjv`S~5;Q|5TVGccN=Uzm}K{v)?f7W!230C<``9(64}D2raRU zAW5bp%}VEo{4Rko`bD%Ehf=0voW?-4Mk#d3_pXTF!-TyIt6U+({6OXWVAa;s-`Ta5 zTqx&8msH3+DLrVmQOTBOAj=uoxKYT3DS1^zBXM?1W+7gI!aQNPYfUl{3;PzS9*F7g zWJN8x?KjBDx^V&6iCY8o_gslO16=kh(|Gp)kz8qlQ`dzxQv;)V&t+B}wwdi~uBs4? zu~G|}y!`3;8#vIMUdyC7YEx6bb^1o}G!Jky4cN?BV9ejBfN<&!4M)L&lRKiuMS#3} z_B}Nkv+zzxhy{dYCW$oGC&J(Ty&7%=5B$sD0bkuPmj7g>|962`(Q{ZZMDv%YMuT^KweiRDvYTEop3IgFv#)(w>1 zSzH>J`q!LK)c(AK>&Ib)A{g`Fdykxqd`Yq@yB}E{gnQV$K!}RsgMGWqC3DKE(=!{}ekB3+(1?g}xF>^icEJbc z5bdxAPkW90atZT+&*7qoLqL#p=>t-(-lsnl2XMpZcYeW|o|a322&)yO_8p(&Sw{|b zn(tY$xn5yS$DD)UYS%sP?c|z>1dp!QUD)l;aW#`%qMtQJjE!s2z`+bTSZmLK7SvCR z=@I4|U^sCwZLQSfd*ACw9B@`1c1|&i^W_OD(570SDLK`MD0wTiR8|$7+%{cF&){$G zU~|$^Ed?TIxyw{1$e|D$050n8AjJvvOWhLtLHbSB|HIfjMp+gu>DraHZJRrdO53(= z+o-f{+qNog+qSLB%KY;5>Av6X(>-qYk3IIEwZ5~6a+P9lMpC^ z8CJ0q>rEpjlsxCvJm=kms@tlN4+sv}He`xkr`S}bGih4t`+#VEIt{1veE z{ZLtb_pSbcfcYPf4=T1+|BtR!x5|X#x2TZEEkUB6kslKAE;x)*0x~ES0kl4Dex4e- zT2P~|lT^vUnMp{7e4OExfxak0EE$Hcw;D$ehTV4a6hqxru0$|Mo``>*a5=1Ym0u>BDJKO|=TEWJ5jZu!W}t$Kv{1!q`4Sn7 zrxRQOt>^6}Iz@%gA3&=5r;Lp=N@WKW;>O!eGIj#J;&>+3va^~GXRHCY2}*g#9ULab zitCJt-OV0*D_Q3Q`p1_+GbPxRtV_T`jyATjax<;zZ?;S+VD}a(aN7j?4<~>BkHK7bO8_Vqfdq1#W&p~2H z&w-gJB4?;Q&pG9%8P(oOGZ#`!m>qAeE)SeL*t8KL|1oe;#+uOK6w&PqSDhw^9-&Fa zuEzbi!!7|YhlWhqmiUm!muO(F8-F7|r#5lU8d0+=;<`{$mS=AnAo4Zb^{%p}*gZL! zeE!#-zg0FWsSnablw!9$<&K(#z!XOW z;*BVx2_+H#`1b@>RtY@=KqD)63brP+`Cm$L1@ArAddNS1oP8UE$p05R=bvZoYz+^6 z<)!v7pRvi!u_-V?!d}XWQR1~0q(H3{d^4JGa=W#^Z<@TvI6J*lk!A zZ*UIKj*hyO#5akL*Bx6iPKvR3_2-^2mw|Rh-3O_SGN3V9GRo52Q;JnW{iTGqb9W99 z7_+F(Op6>~3P-?Q8LTZ-lwB}xh*@J2Ni5HhUI3`ct|*W#pqb>8i*TXOLn~GlYECIj zhLaa_rBH|1jgi(S%~31Xm{NB!30*mcsF_wgOY2N0XjG_`kFB+uQuJbBm3bIM$qhUyE&$_u$gb zpK_r{99svp3N3p4yHHS=#csK@j9ql*>j0X=+cD2dj<^Wiu@i>c_v zK|ovi7}@4sVB#bzq$n3`EgI?~xDmkCW=2&^tD5RuaSNHf@Y!5C(Is$hd6cuyoK|;d zO}w2AqJPS`Zq+(mc*^%6qe>1d&(n&~()6-ZATASNPsJ|XnxelLkz8r1x@c2XS)R*H(_B=IN>JeQUR;T=i3<^~;$<+8W*eRKWGt7c#>N`@;#!`kZ!P!&{9J1>_g8Zj zXEXxmA=^{8A|3=Au+LfxIWra)4p<}1LYd_$1KI0r3o~s1N(x#QYgvL4#2{z8`=mXy zQD#iJ0itk1d@Iy*DtXw)Wz!H@G2St?QZFz zVPkM%H8Cd2EZS?teQN*Ecnu|PrC!a7F_XX}AzfZl3fXfhBtc2-)zaC2eKx*{XdM~QUo4IwcGgVdW69 z1UrSAqqMALf^2|(I}hgo38l|Ur=-SC*^Bo5ej`hb;C$@3%NFxx5{cxXUMnTyaX{>~ zjL~xm;*`d08bG_K3-E+TI>#oqIN2=An(C6aJ*MrKlxj?-;G zICL$hi>`F%{xd%V{$NhisHSL~R>f!F7AWR&7b~TgLu6!3s#~8|VKIX)KtqTH5aZ8j zY?wY)XH~1_a3&>#j7N}0az+HZ;is;Zw(Am{MX}YhDTe(t{ZZ;TG}2qWYO+hdX}vp9 z@uIRR8g#y~-^E`Qyem(31{H0&V?GLdq9LEOb2(ea#e-$_`5Q{T%E?W(6 z(XbX*Ck%TQM;9V2LL}*Tf`yzai{0@pYMwBu%(I@wTY!;kMrzcfq0w?X`+y@0ah510 zQX5SU(I!*Fag4U6a7Lw%LL;L*PQ}2v2WwYF(lHx_Uz2ceI$mnZ7*eZ?RFO8UvKI0H z9Pq-mB`mEqn6n_W9(s~Jt_D~j!Ln9HA)P;owD-l~9FYszs)oEKShF9Zzcmnb8kZ7% zQ`>}ki1kwUO3j~ zEmh140sOkA9v>j@#56ymn_RnSF`p@9cO1XkQy6_Kog?0ivZDb`QWOX@tjMd@^Qr(p z!sFN=A)QZm!sTh(#q%O{Ovl{IxkF!&+A)w2@50=?a-+VuZt6On1;d4YtUDW{YNDN_ zG@_jZi1IlW8cck{uHg^g=H58lPQ^HwnybWy@@8iw%G! zwB9qVGt_?~M*nFAKd|{cGg+8`+w{j_^;nD>IrPf-S%YjBslSEDxgKH{5p)3LNr!lD z4ii)^%d&cCXIU7UK?^ZQwmD(RCd=?OxmY(Ko#+#CsTLT;p#A%{;t5YpHFWgl+@)N1 zZ5VDyB;+TN+g@u~{UrWrv)&#u~k$S&GeW)G{M#&Di)LdYk?{($Cq zZGMKeYW)aMtjmKgvF0Tg>Mmkf9IB#2tYmH-s%D_9y3{tfFmX1BSMtbe<(yqAyWX60 zzkgSgKb3c{QPG2MalYp`7mIrYg|Y<4Jk?XvJK)?|Ecr+)oNf}XLPuTZK%W>;<|r+% zTNViRI|{sf1v7CsWHvFrkQ$F7+FbqPQ#Bj7XX=#M(a~9^80}~l-DueX#;b}Ajn3VE z{BWI}$q{XcQ3g{(p>IOzFcAMDG0xL)H%wA)<(gl3I-oVhK~u_m=hAr&oeo|4lZbf} z+pe)c34Am<=z@5!2;_lwya;l?xV5&kWe}*5uBvckm(d|7R>&(iJNa6Y05SvlZcWBlE{{%2- z`86)Y5?H!**?{QbzGG~|k2O%eA8q=gxx-3}&Csf6<9BsiXC)T;x4YmbBIkNf;0Nd5 z%whM^!K+9zH>on_<&>Ws?^v-EyNE)}4g$Fk?Z#748e+GFp)QrQQETx@u6(1fk2!(W zWiCF~MomG*y4@Zk;h#2H8S@&@xwBIs|82R*^K(i*0MTE%Rz4rgO&$R zo9Neb;}_ulaCcdn3i17MO3NxzyJ=l;LU*N9ztBJ30j=+?6>N4{9YXg$m=^9@Cl9VY zbo^{yS@gU=)EpQ#;UIQBpf&zfCA;00H-ee=1+TRw@(h%W=)7WYSb5a%$UqNS@oI@= zDrq|+Y9e&SmZrH^iA>Of8(9~Cf-G(P^5Xb%dDgMMIl8gk6zdyh`D3OGNVV4P9X|EvIhplXDld8d z^YWtYUz@tpg*38Xys2?zj$F8%ivA47cGSl;hjD23#*62w3+fwxNE7M7zVK?x_`dBSgPK zWY_~wF~OEZi9|~CSH8}Xi>#8G73!QLCAh58W+KMJJC81{60?&~BM_0t-u|VsPBxn* zW7viEKwBBTsn_A{g@1!wnJ8@&h&d>!qAe+j_$$Vk;OJq`hrjzEE8Wjtm)Z>h=*M25 zOgETOM9-8xuuZ&^@rLObtcz>%iWe%!uGV09nUZ*nxJAY%&KAYGY}U1WChFik7HIw% zZP$3Bx|TG_`~19XV7kfi2GaBEhKap&)Q<9`aPs#^!kMjtPb|+-fX66z3^E)iwyXK7 z8)_p<)O{|i&!qxtgBvWXx8*69WO$5zACl++1qa;)0zlXf`eKWl!0zV&I`8?sG)OD2Vy?reNN<{eK+_ za4M;Hh%&IszR%)&gpgRCP}yheQ+l#AS-GnY81M!kzhWxIR?PW`G3G?} z$d%J28uQIuK@QxzGMKU_;r8P0+oIjM+k)&lZ39i#(ntY)*B$fdJnQ3Hw3Lsi8z&V+ zZly2}(Uzpt2aOubRjttzqrvinBFH4jrN)f0hy)tj4__UTwN)#1fj3-&dC_Vh7}ri* zfJ=oqLMJ-_<#rwVyN}_a-rFBe2>U;;1(7UKH!$L??zTbbzP#bvyg7OQBGQklJ~DgP zd<1?RJ<}8lWwSL)`jM53iG+}y2`_yUvC!JkMpbZyb&50V3sR~u+lok zT0uFRS-yx@8q4fPRZ%KIpLp8R#;2%c&Ra4p(GWRT4)qLaPNxa&?8!LRVdOUZ)2vrh zBSx&kB%#Y4!+>~)<&c>D$O}!$o{<1AB$M7-^`h!eW;c(3J~ztoOgy6Ek8Pwu5Y`Xion zFl9fb!k2`3uHPAbd(D^IZmwR5d8D$495nN2`Ue&`W;M-nlb8T-OVKt|fHk zBpjX$a(IR6*-swdNk@#}G?k6F-~c{AE0EWoZ?H|ZpkBxqU<0NUtvubJtwJ1mHV%9v?GdDw; zAyXZiD}f0Zdt-cl9(P1la+vQ$Er0~v}gYJVwQazv zH#+Z%2CIfOf90fNMGos|{zf&N`c0@x0N`tkFv|_9af3~<0z@mnf*e;%r*Fbuwl-IW z{}B3=(mJ#iwLIPiUP`J3SoP~#)6v;aRXJ)A-pD2?_2_CZ#}SAZ<#v7&Vk6{*i(~|5 z9v^nC`T6o`CN*n%&9+bopj^r|E(|pul;|q6m7Tx+U|UMjWK8o-lBSgc3ZF=rP{|l9 zc&R$4+-UG6i}c==!;I#8aDIbAvgLuB66CQLRoTMu~jdw`fPlKy@AKYWS-xyZzPg&JRAa@m-H43*+ne!8B7)HkQY4 zIh}NL4Q79a-`x;I_^>s$Z4J4-Ngq=XNWQ>yAUCoe&SMAYowP>r_O}S=V+3=3&(O=h zNJDYNs*R3Y{WLmBHc?mFEeA4`0Y`_CN%?8qbDvG2m}kMAiqCv`_BK z_6a@n`$#w6Csr@e2YsMx8udNWtNt=kcqDZdWZ-lGA$?1PA*f4?X*)hjn{sSo8!bHz zb&lGdAgBx@iTNPK#T_wy`KvOIZvTWqSHb=gWUCKXAiB5ckQI`1KkPx{{%1R*F2)Oc z(9p@yG{fRSWE*M9cdbrO^)8vQ2U`H6M>V$gK*rz!&f%@3t*d-r3mSW>D;wYxOhUul zk~~&ip5B$mZ~-F1orsq<|1bc3Zpw6)Ws5;4)HilsN;1tx;N6)tuePw& z==OlmaN*ybM&-V`yt|;vDz(_+UZ0m&&9#{9O|?0I|4j1YCMW;fXm}YT$0%EZ5^YEI z4i9WV*JBmEU{qz5O{#bs`R1wU%W$qKx?bC|e-iS&d*Qm7S=l~bMT{~m3iZl+PIXq{ zn-c~|l)*|NWLM%ysfTV-oR0AJ3O>=uB-vpld{V|cWFhI~sx>ciV9sPkC*3i0Gg_9G!=4ar*-W?D9)?EFL1=;O+W8}WGdp8TT!Fgv z{HKD`W>t(`Cds_qliEzuE!r{ihwEv1l5o~iqlgjAyGBi)$%zNvl~fSlg@M=C{TE;V zQkH`zS8b&!ut(m)%4n2E6MB>p*4(oV>+PT51#I{OXs9j1vo>9I<4CL1kv1aurV*AFZ^w_qfVL*G2rG@D2 zrs87oV3#mf8^E5hd_b$IXfH6vHe&lm@7On~Nkcq~YtE!}ad~?5*?X*>y`o;6Q9lkk zmf%TYonZM`{vJg$`lt@MXsg%*&zZZ0uUSse8o=!=bfr&DV)9Y6$c!2$NHyYAQf*Rs zk{^?gl9E z5Im8wlAsvQ6C2?DyG@95gUXZ3?pPijug25g;#(esF_~3uCj3~94}b*L>N2GSk%Qst z=w|Z>UX$m!ZOd(xV*2xvWjN&c5BVEdVZ0wvmk)I+YxnyK%l~caR=7uNQ=+cnNTLZ@&M!I$Mj-r{!P=; z`C2)D=VmvK8@T5S9JZoRtN!S*D_oqOxyy!q6Zk|~4aT|*iRN)fL)c>-yycR>-is0X zKrko-iZw(f(!}dEa?hef5yl%p0-v-8#8CX8!W#n2KNyT--^3hq6r&`)5Y@>}e^4h- zlPiDT^zt}Ynk&x@F8R&=)k8j$=N{w9qUcIc&)Qo9u4Y(Ae@9tA`3oglxjj6c{^pN( zQH+Uds2=9WKjH#KBIwrQI%bbs`mP=7V>rs$KG4|}>dxl_k!}3ZSKeEen4Iswt96GGw`E6^5Ov)VyyY}@itlj&sao|>Sb5 zeY+#1EK(}iaYI~EaHQkh7Uh>DnzcfIKv8ygx1Dv`8N8a6m+AcTa-f;17RiEed>?RT zk=dAksmFYPMV1vIS(Qc6tUO+`1jRZ}tcDP? zt)=7B?yK2RcAd1+Y!$K5*ds=SD;EEqCMG6+OqPoj{&8Y5IqP(&@zq@=A7+X|JBRi4 zMv!czlMPz)gt-St2VZwDD=w_S>gRpc-g zUd*J3>bXeZ?Psjohe;z7k|d<*T21PA1i)AOi8iMRwTBSCd0ses{)Q`9o&p9rsKeLaiY zluBw{1r_IFKR76YCAfl&_S1*(yFW8HM^T()&p#6y%{(j7Qu56^ZJx1LnN`-RTwimdnuo*M8N1ISl+$C-%=HLG-s} zc99>IXRG#FEWqSV9@GFW$V8!{>=lSO%v@X*pz*7()xb>=yz{E$3VE;e)_Ok@A*~El zV$sYm=}uNlUxV~6e<6LtYli1!^X!Ii$L~j4e{sI$tq_A(OkGquC$+>Rw3NFObV2Z)3Rt~Jr{oYGnZaFZ^g5TDZlg;gaeIP} z!7;T{(9h7mv{s@piF{-35L=Ea%kOp;^j|b5ZC#xvD^^n#vPH=)lopYz1n?Kt;vZmJ z!FP>Gs7=W{sva+aO9S}jh0vBs+|(B6Jf7t4F^jO3su;M13I{2rd8PJjQe1JyBUJ5v zcT%>D?8^Kp-70bP8*rulxlm)SySQhG$Pz*bo@mb5bvpLAEp${?r^2!Wl*6d7+0Hs_ zGPaC~w0E!bf1qFLDM@}zso7i~(``)H)zRgcExT_2#!YOPtBVN5Hf5~Ll3f~rWZ(UsJtM?O*cA1_W0)&qz%{bDoA}{$S&-r;0iIkIjbY~ zaAqH45I&ALpP=9Vof4OapFB`+_PLDd-0hMqCQq08>6G+C;9R~}Ug_nm?hhdkK$xpI zgXl24{4jq(!gPr2bGtq+hyd3%Fg%nofK`psHMs}EFh@}sdWCd!5NMs)eZg`ZlS#O0 zru6b8#NClS(25tXqnl{|Ax@RvzEG!+esNW-VRxba(f`}hGoqci$U(g30i}2w9`&z= zb8XjQLGN!REzGx)mg~RSBaU{KCPvQx8)|TNf|Oi8KWgv{7^tu}pZq|BS&S<53fC2K4Fw6>M^s$R$}LD*sUxdy6Pf5YKDbVet;P!bw5Al-8I1Nr(`SAubX5^D9hk6$agWpF}T#Bdf{b9-F#2WVO*5N zp+5uGgADy7m!hAcFz{-sS0kM7O)qq*rC!>W@St~^OW@R1wr{ajyYZq5H!T?P0e+)a zaQ%IL@X_`hzp~vRH0yUblo`#g`LMC%9}P;TGt+I7qNcBSe&tLGL4zqZqB!Bfl%SUa z6-J_XLrnm*WA`34&mF+&e1sPCP9=deazrM=Pc4Bn(nV;X%HG^4%Afv4CI~&l!Sjzb z{rHZ3od0!Al{}oBO>F*mOFAJrz>gX-vs!7>+_G%BB(ljWh$252j1h;9p~xVA=9_`P z5KoFiz96_QsTK%B&>MSXEYh`|U5PjX1(+4b#1PufXRJ*uZ*KWdth1<0 zsAmgjT%bowLyNDv7bTUGy|g~N34I-?lqxOUtFpTLSV6?o?<7-UFy*`-BEUsrdANh} zBWkDt2SAcGHRiqz)x!iVoB~&t?$yn6b#T=SP6Ou8lW=B>=>@ik93LaBL56ub`>Uo!>0@O8?e)$t(sgy$I z6tk3nS@yFFBC#aFf?!d_3;%>wHR;A3f2SP?Na8~$r5C1N(>-ME@HOpv4B|Ty7%jAv zR}GJwsiJZ5@H+D$^Cwj#0XA_(m^COZl8y7Vv(k=iav1=%QgBOVzeAiw zaDzzdrxzj%sE^c9_uM5D;$A_7)Ln}BvBx^=)fO+${ou%B*u$(IzVr-gH3=zL6La;G zu0Kzy5CLyNGoKRtK=G0-w|tnwI)puPDOakRzG(}R9fl7#<|oQEX;E#yCWVg95 z;NzWbyF&wGg_k+_4x4=z1GUcn6JrdX4nOVGaAQ8#^Ga>aFvajQN{!+9rgO-dHP zIp@%&ebVg}IqnRWwZRTNxLds+gz2@~VU(HI=?Epw>?yiEdZ>MjajqlO>2KDxA>)cj z2|k%dhh%d8SijIo1~20*5YT1eZTDkN2rc^zWr!2`5}f<2f%M_$to*3?Ok>e9$X>AV z2jYmfAd)s|(h?|B(XYrIfl=Wa_lBvk9R1KaP{90-z{xKi+&8=dI$W0+qzX|ZovWGOotP+vvYR(o=jo?k1=oG?%;pSqxcU* zWVGVMw?z__XQ9mnP!hziHC`ChGD{k#SqEn*ph6l46PZVkm>JF^Q{p&0=MKy_6apts z`}%_y+Tl_dSP(;Ja&sih$>qBH;bG;4;75)jUoVqw^}ee=ciV;0#t09AOhB^Py7`NC z-m+ybq1>_OO+V*Z>dhk}QFKA8V?9Mc4WSpzj{6IWfFpF7l^au#r7&^BK2Ac7vCkCn{m0uuN93Ee&rXfl1NBY4NnO9lFUp zY++C1I;_{#OH#TeP2Dp?l4KOF8ub?m6zE@XOB5Aiu$E~QNBM@;r+A5mF2W1-c7>ex zHiB=WJ&|`6wDq*+xv8UNLVUy4uW1OT>ey~Xgj@MMpS@wQbHAh>ysYvdl-1YH@&+Q! z075(Qd4C!V`9Q9jI4 zSt{HJRvZec>vaL_brKhQQwbpQd4_Lmmr0@1GdUeU-QcC{{8o=@nwwf>+dIKFVzPriGNX4VjHCa zTbL9w{Y2V87c2ofX%`(48A+4~mYTiFFl!e{3K^C_k%{&QTsgOd0*95KmWN)P}m zTRr{`f7@=v#+z_&fKYkQT!mJn{*crj%ZJz#(+c?>cD&2Lo~FFAWy&UG*Op^pV`BR^I|g?T>4l5;b|5OQ@t*?_Slp`*~Y3`&RfKD^1uLezIW(cE-Dq2z%I zBi8bWsz0857`6e!ahet}1>`9cYyIa{pe53Kl?8|Qg2RGrx@AlvG3HAL-^9c^1GW;)vQt8IK+ zM>!IW*~682A~MDlyCukldMd;8P|JCZ&oNL(;HZgJ>ie1PlaInK7C@Jg{3kMKYui?e!b`(&?t6PTb5UPrW-6DVU%^@^E`*y-Fd(p|`+JH&MzfEq;kikdse ziFOiDWH(D< zyV7Rxt^D0_N{v?O53N$a2gu%1pxbeK;&ua`ZkgSic~$+zvt~|1Yb=UfKJW2F7wC^evlPf(*El+#}ZBy0d4kbVJsK- z05>;>?HZO(YBF&v5tNv_WcI@O@LKFl*VO?L(!BAd!KbkVzo;v@~3v`-816GG?P zY+H3ujC>5=Am3RIZDdT#0G5A6xe`vGCNq88ZC1aVXafJkUlcYmHE^+Z{*S->ol%-O znm9R0TYTr2w*N8Vs#s-5=^w*{Y}qp5GG)Yt1oLNsH7y~N@>Eghms|K*Sdt_u!&I}$ z+GSdFTpbz%KH+?B%Ncy;C`uW6oWI46(tk>r|5|-K6)?O0d_neghUUOa9BXHP*>vi; z={&jIGMn-92HvInCMJcyXwHTJ42FZp&Wxu+9Rx;1x(EcIQwPUQ@YEQQ`bbMy4q3hP zNFoq~Qd0=|xS-R}k1Im3;8s{BnS!iaHIMLx)aITl)+)?Yt#fov|Eh>}dv@o6R{tG>uHsy&jGmWN5+*wAik|78(b?jtysPHC#e+Bzz~V zS3eEXv7!Qn4uWi!FS3B?afdD*{fr9>B~&tc671fi--V}~E4un;Q|PzZRwk-azprM$4AesvUb5`S`(5x#5VJ~4%ET6&%GR$}muHV-5lTsCi_R|6KM(g2PCD@|yOpKluT zakH!1V7nKN)?6JmC-zJoA#ciFux8!)ajiY%K#RtEg$gm1#oKUKX_Ms^%hvKWi|B=~ zLbl-L)-=`bfhl`>m!^sRR{}cP`Oim-{7}oz4p@>Y(FF5FUEOfMwO!ft6YytF`iZRq zfFr{!&0Efqa{1k|bZ4KLox;&V@ZW$997;+Ld8Yle91he{BfjRhjFTFv&^YuBr^&Pe zswA|Bn$vtifycN8Lxr`D7!Kygd7CuQyWqf}Q_PM}cX~S1$-6xUD%-jrSi24sBTFNz(Fy{QL2AmNbaVggWOhP;UY4D>S zqKr!UggZ9Pl9Nh_H;qI`-WoH{ceXj?m8y==MGY`AOJ7l0Uu z)>M%?dtaz2rjn1SW3k+p`1vs&lwb%msw8R!5nLS;upDSxViY98IIbxnh{}mRfEp=9 zbrPl>HEJeN7J=KnB6?dwEA6YMs~chHNG?pJsEj#&iUubdf3JJwu=C(t?JpE6xMyhA3e}SRhunDC zn-~83*9=mADUsk^sCc%&&G1q5T^HR9$P#2DejaG`Ui*z1hI#h7dwpIXg)C{8s< z%^#@uQRAg-$z&fmnYc$Duw63_Zopx|n{Bv*9Xau{a)2%?H<6D>kYY7_)e>OFT<6TT z0A}MQLgXbC2uf`;67`mhlcUhtXd)Kbc$PMm=|V}h;*_%vCw4L6r>3Vi)lE5`8hkSg zNGmW-BAOO)(W((6*e_tW&I>Nt9B$xynx|sj^ux~?q?J@F$L4;rnm_xy8E*JYwO-02u9_@@W0_2@?B@1J{y~Q39N3NX^t7#`=34Wh)X~sU&uZWgS1Z09%_k|EjA4w_QqPdY`oIdv$dJZ;(!k)#U8L+|y~gCzn+6WmFt#d{OUuKHqh1-uX_p*Af8pFYkYvKPKBxyid4KHc}H` z*KcyY;=@wzXYR{`d{6RYPhapShXIV?0cg_?ahZ7do)Ot#mxgXYJYx}<%E1pX;zqHd zf!c(onm{~#!O$2`VIXezECAHVd|`vyP)Uyt^-075X@NZDBaQt<>trA3nY-Dayki4S zZ^j6CCmx1r46`4G9794j-WC0&R9(G7kskS>=y${j-2;(BuIZTLDmAyWTG~`0)Bxqk zd{NkDe9ug|ms@0A>JVmB-IDuse9h?z9nw!U6tr7t-Lri5H`?TjpV~8(gZWFq4Vru4 z!86bDB;3lpV%{rZ`3gtmcRH1hjj!loI9jN>6stN6A*ujt!~s!2Q+U1(EFQEQb(h4E z6VKuRouEH`G6+8Qv2C)K@^;ldIuMVXdDDu}-!7FS8~k^&+}e9EXgx~)4V4~o6P^52 z)a|`J-fOirL^oK}tqD@pqBZi_;7N43%{IQ{v&G9^Y^1?SesL`;Z(dt!nn9Oj5Odde%opv&t zxJ><~b#m+^KV&b?R#)fRi;eyqAJ_0(nL*61yPkJGt;gZxSHY#t>ATnEl-E%q$E16% zZdQfvhm5B((y4E3Hk6cBdwGdDy?i5CqBlCVHZr-rI$B#>Tbi4}Gcvyg_~2=6O9D-8 zY2|tKrNzbVR$h57R?Pe+gUU_il}ZaWu|Az#QO@};=|(L-RVf0AIW zq#pO+RfM7tdV`9lI6g;{qABNId`fG%U9Va^ravVT^)CklDcx)YJKeJdGpM{W1v8jg z@&N+mR?BPB=K1}kNwXk_pj44sd>&^;d!Z~P>O78emE@Qp@&8PyB^^4^2f7e)gekMv z2aZNvP@;%i{+_~>jK7*2wQc6nseT^n6St9KG#1~Y@$~zR_=AcO2hF5lCoH|M&c{vR zSp(GRVVl=T*m~dIA;HvYm8HOdCkW&&4M~UDd^H)`p__!4k+6b)yG0Zcek8OLw$C^K z3-BbLiG_%qX|ZYpXJ$(c@aa7b4-*IQkDF}=gZSV`*ljP|5mWuHSCcf$5qqhZTv&P?I$z^>}qP(q!Aku2yA5vu38d8x*q{6-1`%PrE_r0-9Qo?a#7Zbz#iGI7K<(@k^|i4QJ1H z4jx?{rZbgV!me2VT72@nBjucoT zUM9;Y%TCoDop?Q5fEQ35bCYk7!;gH*;t9t-QHLXGmUF;|vm365#X)6b2Njsyf1h9JW#x$;@x5Nx2$K$Z-O3txa%;OEbOn6xBzd4n4v)Va=sj5 z%rb#j7{_??Tjb8(Hac<^&s^V{yO-BL*uSUk2;X4xt%NC8SjO-3?;Lzld{gM5A=9AV z)DBu-Z8rRvXXwSVDH|dL-3FODWhfe1C_iF``F05e{dl(MmS|W%k-j)!7(ARkV?6r~ zF=o42y+VapxdZn;GnzZfGu<6oG-gQ7j7Zvgo7Am@jYxC2FpS@I;Jb%EyaJDBQC(q% zKlZ}TVu!>;i3t~OAgl@QYy1X|T~D{HOyaS*Bh}A}S#a9MYS{XV{R-|niEB*W%GPW! zP^NU(L<}>Uab<;)#H)rYbnqt|dOK(-DCnY==%d~y(1*{D{Eo1cqIV8*iMfx&J*%yh zx=+WHjt0q2m*pLx8=--UqfM6ZWjkev>W-*}_*$Y(bikH`#-Gn#!6_ zIA&kxn;XYI;eN9yvqztK-a113A%97in5CL5Z&#VsQ4=fyf&3MeKu70)(x^z_uw*RG zo2Pv&+81u*DjMO6>Mrr7vKE2CONqR6C0(*;@4FBM;jPIiuTuhQ-0&C)JIzo_k>TaS zN_hB;_G=JJJvGGpB?uGgSeKaix~AkNtYky4P7GDTW6{rW{}V9K)Cn^vBYKe*OmP!; zohJs=l-0sv5&pL6-bowk~(swtdRBZQHh8)m^r2+qTtZ zt4m$B?OQYNyfBA0E)g28a*{)a=%%f-?{F;++-Xs#5|7kSHTD*E9@$V ztE%7zX4A(L`n)FY8Y4pOnKC|Pf)j$iR#yP;V0+|Hki+D;t4I4BjkfdYliK9Gf6RYw z;3px$Ud5aTd`yq$N7*WOs!{X91hZZ;AJ9iQOH%p;v$R%OQum_h#rq9*{ve(++|24z zh2P;{-Z?u#rOqd0)D^_Ponv(Y9KMB9#?}nJdUX&r_rxF0%3__#8~ZwsyrSPmtWY27 z-54ZquV2t_W!*+%uwC=h-&_q~&nQer0(FL74to%&t^byl^C?wTaZ-IS9OssaQFP)1 zAov0o{?IRAcCf+PjMWSdmP42gysh|c9Ma&Q^?_+>>+-yrC8WR;*XmJ;>r9v*>=W}tgWG;WIt{~L8`gk8DP{dSdG z4SDM7g5ahMHYHHk*|mh9{AKh-qW7X+GEQybJt9A@RV{gaHUAva+=lSroK^NUJYEiL z?X6l9ABpd)9zzA^;FdZ$QQs#uD@hdcaN^;Q=AXlbHv511Meye`p>P4Y2nblEDEeZo}-$@g&L98Aih6tgLz--${eKTxymIipy0xSYgZZ zq^yyS4yNPTtPj-sM?R8@9Q1gtXPqv{$lb5i|C1yymwnGdfYV3nA-;5!Wl zD0fayn!B^grdE?q^}ba{-LIv*Z}+hZm_F9c$$cW!bx2DgJD&6|bBIcL@=}kQA1^Eh zXTEznqk)!!IcTl>ey?V;X8k<+C^DRA{F?T*j0wV`fflrLBQq!l7cbkAUE*6}WabyF zgpb+|tv=aWg0i}9kBL8ZCObYqHEycr5tpc-$|vdvaBsu#lXD@u_e1iL z{h>xMRS0a7KvW?VttrJFpX^5DC4Bv4cp6gNG6#8)7r7IxXfSNSp6)_6tZ4l>(D+0I zPhU)N!sKywaBusHdVE!yo5$20JAU8V_XcW{QmO!p*~ns8{2~bhjydnmA&=r zX9NSM9QYogYMDZ~kS#Qx`mt>AmeR3p@K$`fbJ%LQ1c5lEOz<%BS<}2DL+$>MFcE%e zlxC)heZ7#i80u?32eOJI9oQRz0z;JW@7Th4q}YmQ-`Z?@y3ia^_)7f37QMwDw~<-@ zT)B6fftmK_6YS!?{uaj5lLxyR++u*ZY2Mphm5cd7PA5=%rd)95hJ9+aGSNfjy>Ylc zoI0nGIT3sKmwX8h=6CbvhVO+ehFIR155h8iRuXZx^cW>rq5K4z_dvM#hRER=WR@THs%WELI9uYK9HN44Em2$#@k)hD zicqRPKV#yB;UlcsTL_}zCMK0T;eXHfu`y2(dfwm(v)IBbh|#R>`2cot{m7}8_X&oD zr@94PkMCl%d3FsC4pil=#{3uv^+)pvxfwmPUr)T)T|GcZVD$wVj$mjkjDs`5cm8N! zXVq2CvL;gWGpPI4;9j;2&hS*o+LNp&C5Ac=OXx*W5y6Z^az)^?G0)!_iAfjH5wiSE zD(F}hQZB#tF5iEx@0sS+dP70DbZ*<=5X^)Pxo^8aKzOzuyc2rq=<0-k;Y_ID1>9^v z+)nc36}?>jen*1%OX3R*KRASj${u$gZ$27Hpcj=95kK^aLzxhW6jj_$w6}%#1*$5D zG1H_vYFrCSwrRqYw*9<}OYAOQT)u%9lC`$IjZV<4`9Sc;j{Qv_6+uHrYifK&On4V_7yMil!0Yv55z@dFyD{U@Sy>|vTX=P_( zRm<2xj*Z}B30VAu@0e+}at*y?wXTz|rPalwo?4ZZc>hS0Ky6~mi@kv#?xP2a;yt?5=(-CqvP_3&$KdjB7Ku;# z`GLE*jW1QJB5d&E?IJO?1+!Q8HQMGvv^RuFoi=mM4+^tOqvX%X&viB%Ko2o-v4~~J z267ui;gsW?J=qS=D*@*xJvAy3IOop5bEvfR4MZC>9Y4Z$rGI|EHNNZ7KX;Ix{xSvm z-)Cau-xuTm|7`4kUdXvd_d^E=po(76ELfq5OgxIt3aqDy#zBfIy-5<3gpn{Ce`-ha z<;6y@{Bgqw?c~h*&j{FozQCh=`Lv-5Iw!KdSt;%GDOq%=(V!dJ-}|}|0o5G2kJj6{ z`jCSPs$9Fe8O(+qALZiJ$WtR=<@GvsdM)IJ`7XrBfW0iyYE#Vy^e@zbysg*B5Z_kSL6<)vqoaH zQ{!9!*{e9UZo^h+qZ`T@LfVwAEwc&+9{C8c%oj41q#hyn<&zA9IIur~V|{mmu`n5W z8)-Ou$YgjQ*PMIqHhZ_9E?(uoK0XM5aQkarcp}WT^7b^FC#^i>#8LGZ9puDuXUYas z7caX)V5U6uY-L5Wl%)j$qRkR;7@3T*N64YK_!`Fw=>CAwe~2loI1<>DZW&sb7Q)X;6E08&$h! z2=c1i4UOO{R4TmkTz+o9n`}+%d%blR6P;5{`qjtxlN$~I%tMMDCY`~e{+mRF!rj5( z3ywv)P_PUUqREu)TioPkg&5RKjY6z%pRxQPQ{#GNMTPag^S8(8l{!{WGNs2U1JA-O zq02VeYcArhTAS;v3);k(&6ayCH8SXN@r;1NQeJ*y^NHM+zOd;?t&c!Hq^SR_w6twGV8dl>j zjS+Zc&Yp7cYj&c1y3IxQ%*kWiYypvoh(k8g`HrY<_Bi-r%m-@SLfy-6mobxkWHxyS z>TtM2M4;Uqqy|+8Q++VcEq$PwomV1D4UzNA*Tgkg9#Gpz#~&iPf|Czx!J?qss?e|3 z4gTua75-P{2X7w9eeK3~GE0ip-D;%%gTi)8bR~Ez@)$gpuS~jZs`CrO5SR-Xy7bkA z89fr~mY}u4A$|r1$fe-;T{yJh#9Ime1iRu8eo?uY9@yqAU3P!rx~SsP;LTBL zeoMK(!;(Zt8313 z3)V)q_%eflKW?BnMZa}6E0c7t!$-mC$qt44OME5F(6B$E8w*TUN-h}0dOiXI+TH zYFrr&k1(yO(|J0vP|{22@Z}bxm@7BkjO)f)&^fv|?_JX+s)1*|7X7HH(W?b3QZ3!V|~m?8}uJsF>NvE4@fik zjyyh+U*tt`g6v>k9ub88a;ySvS1QawGn7}aaR**$rJA=a#eUT~ngUbJ%V=qsFIekLbv!YkqjTG{_$F;$w19$(ivIs*1>?2ka%uMOx@B9`LD zhm~)z@u4x*zcM1WhiX)!U{qOjJHt1xs{G1S?rYe)L)ntUu^-(o_dfqZu)}W(X%Uu| zN*qI@&R2fB#Jh|Mi+eMrZDtbNvYD3|v0Kx>E#Ss;Be*T$@DC!2A|mb%d}TTN3J+c= zu@1gTOXFYy972S+=C;#~)Z{Swr0VI5&}WYzH22un_Yg5o%f9fvV(`6!{C<(ZigQ2`wso)cj z9O12k)15^Wuv#rHpe*k5#4vb%c znP+Gjr<-p%01d<+^yrSoG?}F=eI8X;?=Fo2a~HUiJ>L!oE#9tXRp!adg-b9D;(6$E zeW0tH$US04zTX$OxM&X+2ip>KdFM?iG_fgOD-qB|uFng8*#Z5jgqGY=zLU?4!OlO#~YBTB9b9#~H@nqQ#5 z6bV));d?IJTVBC+79>rGuy1JgxPLy$dA7;_^^L)02m}XLjFR*qH`eI~+eJo(7D`LH z(W%lGnGK+Vk_3kyF*zpgO=1MxMg?hxe3}}YI>dVs8l}5eWjYu4=w6MWK09+05 zGdpa#$awd>Q|@aZa*z{5F3xy3n@E4YT9%TmMo0jxW59p0bI?&S}M+ z&^NG%rf7h*m9~p#b19|`wO5OMY-=^XT+=yrfGNpl<&~~FGsx_`IaFn+sEgF$hgOa~oAVAiu^a$jHcqkE=dj`ze z=axsfrzzh6VGD0x#6Ff=t%+VTiq!n6^gv*uIUD<9fOhvR;al5kcY${uunn}-!74<7 zmP^3cl-kyN(QY!!Z-^PY-OUkh=3ZWk6>le$_Q&xk4cgH{?i)C%2RM@pX5Q{jdSlo! zVau5v44cQX5|zQlQDt;dCg)oM0B<=P1CR!W%!^m$!{pKx;bn9DePJjWBX)q!`$;0K zqJIIyD#aK;#-3&Nf=&IhtbV|?ZGYHSphp~6th`p2rkw&((%kBV7<{siEOU7AxJj+FuRdDu$ zcmTW8usU_u!r)#jg|J=Gt{##7;uf4A5cdt6Y02}f(d2)z~ z)CH~gVAOwBLk$ZiIOn}NzDjvfw(w$u|BdCBI#)3xB-Ot?nz?iR38ayCm48M=_#9r7 zw8%pwQ<9mbEs5~_>pN3~#+Er~Q86J+2TDXM6umCbukd-X6pRIr5tF?VauT8jW> zY^#)log>jtJs2s3xoiPB7~8#1ZMv>Zx0}H58k-@H2huNyw~wsl0B8j)H5)H9c7y&i zp8^0;rKbxC1eEZ-#Qxvz)Xv$((8lK9I>BspPajluysw^f#t9P;OUis43mmEzX+lk* zc4T-Ms9_687GR+~QS#0~vxK#DSGN=a-m(@eZTqw2<+lN9>R~gK2)3;sT4%nI%Y|0m zX9SPR!>?~s=j5H4WMqeTW8QaLZ=1bWS5I3xZ&$(ypc=tHrv+hX@s)VG(tc!yvLM7n zshN=C#v={X1r;)xn0Pow_1eMhkn!{;x$BJ#PIz)m585&%cmzk;btQzZAN_^zis;n? z?6I~bN?s;7vg_dtoTc4A5Ow*Rb}No#UYl)sN|RmoYo}k^cKLXd8F`44?RrokkPvN5 ztUrx;U~B;jbE_qGd3n0j2i}A{enJvJ?gSF~NQj~EP5vM-w4@;QQ5n(Npic}XNW6B0 zq9F4T%6kp7qGhd0vpQcz+nMk8GOAmbz8Bt4@GtewGr6_>Xj>ge)SyfY}nu>Y!a@HoIx(StD zx`!>RT&}tpBL%nOF%7XIFW?n1AP*xthCMzhrU6G!U6?m4!CPWTvn#Yaoi_95CT2!L z|B=5zeRW30&ANGN>J9#GtCm&3SF6n4TqDz<-{@ZXkrkRDCpV$DwCtI^e&3i1A{Ar&JZtS^c+lyPa6 z%JJr42S_;eFC#M~bdtQePhOU32WDiZ4@H&af)z#$Y|hnQNb)8(3?1Ad>5uaZ1z zU~!jt3XUI@gpWb8tWTyH7DGvKvzYfqNIy3P{9vpwz_C-QL&`+8Io$F5PS-@YQJoEO z17D9P(+sXajWSH_8&C?fn>rTLX+(?KiwX#JNV)xE0!Q@>Tid$V2#r4y6fkph?YZ>^ z(o^q(0*P->3?I0cELXJn(N|#qTm6 zAPIL~n)m!50;*?5=MOOc4Wk;w(0c$(!e?vpV23S|n|Y7?nyc8)fD8t-KI&nTklH&BzqQ}D(1gH3P+5zGUzIjT~x`;e8JH=86&5&l-DP% z)F+Et(h|GJ?rMy-Zrf>Rv@<3^OrCJ1xv_N*_@-K5=)-jP(}h1Rts44H&ou8!G_C1E zhTfUDASJ2vu!4@j58{NN;78i?6__xR75QEDC4JN{>RmgcNrn-EOpEOcyR<8FS@RB@ zH!R7J=`KK^u06eeI|X@}KvQmdKE3AmAy8 zM4IIvde#e4O(iwag`UL5yQo>6&7^=D4yE-Eo9$9R2hR} zn;Z9i-d=R-xZl4@?s%8|m1M`$J6lW1r0Y)+8q$}Vn4qyR1jqTjGH;@Z!2KiGun2~x zaiEfzVT<|_b6t}~XPeflAm8hvCHP3Bp*tl{^y_e{Jsn@w+KP{7}bH_s=1S2E1sj=18a39*Ag~lbkT^_OQuYQey=b zW^{0xlQ@O$^cSxUZ8l(Mspg8z0cL*?yH4;X2}TdN)uN31A%$3$a=4;{S@h#Y(~i%) zc=K7Ggl=&2hYVic*W65gpSPE70pU;FN@3k?BYdNDKv6wlsBAF^);qiqI zhklsX4TaWiC%VbnZ|yqL+Pcc;(#&E*{+Rx&<&R{uTYCn^OD|mAk4%Q7gbbgMnZwE{ zy7QMK%jIjU@ye?0; z;0--&xVeD}m_hq9A8a}c9WkI2YKj8t!Mkk!o%AQ?|CCBL9}n570}OmZ(w)YI6#QS&p<={tcek*D{CPR%eVA1WBGUXf z%gO2vL7iVDr1$!LAW)1@H>GoIl=&yyZ7=*9;wrOYQ}O}u>h}4FWL?N2ivURlUi11- zl{G0fo`9?$iAEN<4kxa#9e0SZPqa{pw?K=tdN5tRc7HDX-~Ta6_+#s9W&d`6PB7dF*G@|!Mc}i zc=9&T+edI(@la}QU2An#wlkJ&7RmTEMhyC_A8hWM54?s1WldCFuBmT5*I3K9=1aj= z6V@93P-lUou`xmB!ATp0(We$?)p*oQs;(Kku15~q9`-LSl{(Efm&@%(zj?aK2;5}P z{6<@-3^k^5FCDT@Z%XABEcuPoumYkiD&)-8z2Q}HO9OVEU3WM;V^$5r4q>h^m73XF z5!hZ7SCjfxDcXyj(({vg8FU(m2_}36L_yR>fnW)u=`1t@mPa76`2@%8v@2@$N@TE` z)kYhGY1jD;B9V=Dv1>BZhR9IJmB?X9Wj99f@MvJ2Fim*R`rsRilvz_3n!nPFLmj({EP!@CGkY5R*Y_dSO{qto~WerlG}DMw9k+n}pk z*nL~7R2gB{_9=zpqX|*vkU-dx)(j+83uvYGP?K{hr*j2pQsfXn<_As6z%-z+wFLqI zMhTkG>2M}#BLIOZ(ya1y8#W<+uUo@(43=^4@?CX{-hAuaJki(_A(uXD(>`lzuM~M;3XA48ZEN@HRV{1nvt?CV)t;|*dow0Ue2`B*iA&!rI`fZQ=b28= z_dxF}iUQ8}nq0SA4NK@^EQ%=)OY;3fC<$goJ&Kp|APQ@qVbS-MtJQBc)^aO8mYFsbhafeRKdHPW&s^&;%>v zlTz`YE}CuQ@_X&mqm{+{!h2r)fPGeM_Ge4RRYQkrma`&G<>RW<>S(?#LJ}O-t)d$< zf}b0svP^Zu@)MqwEV^Fb_j zPYYs~vmEC~cOIE6Nc^@b@nyL!w5o?nQ!$mGq(Pa|1-MD}K0si<&}eag=}WLSDO zE4+eA~!J(K}605x&4 zT72P7J^)Y)b(3g2MZ@1bv%o1ggwU4Yb!DhQ=uu-;vX+Ix8>#y6wgNKuobvrPNx?$3 zI{BbX<=Y-cBtvY&#MpGTgOLYU4W+csqWZx!=AVMb)Z;8%#1*x_(-)teF>45TCRwi1 z)Nn>hy3_lo44n-4A@=L2gI$yXCK0lPmMuldhLxR8aI;VrHIS{Dk}yp= zwjhB6v@0DN=Hnm~3t>`CtnPzvA*Kumfn5OLg&-m&fObRD};c}Hf?n&mS< z%$wztc%kjWjCf-?+q(bZh9k~(gs?i4`XVfqMXvPVkUWfm4+EBF(nOkg!}4u)6I)JT zU6IXqQk?p1a2(bz^S;6ZH3Wy9!JvbiSr7%c$#G1eK2^=~z1WX+VW)CPD#G~)13~pX zErO(>x$J_4qu-)lNlZkLj2}y$OiKn0ad5Imu5p-2dnt)(YI|b7rJ3TBUQ8FB8=&ym50*ibd2NAbj z;JA&hJ$AJlldM+tO;Yl3rBOFiP8fDdF?t(`gkRpmT9inR@uX{bThYNmxx-LN5K8h0 ztS%w*;V%b`%;-NARbNXn9he&AO4$rvmkB#;aaOx?Wk|yBCmN{oMTK&E)`s&APR<-5 z#;_e75z;LJ)gBG~h<^`SGmw<$Z3p`KG|I@7Pd)sTJnouZ1hRvm3}V+#lPGk4b&A#Y z4VSNi8(R1z7-t=L^%;*;iMTIAjrXl;h106hFrR{n9o8vlz?+*a1P{rEZ2ie{luQs} zr6t746>eoqiO5)^y;4H%2~&FT*Qc*9_oC2$+&syHWsA=rn3B~4#QEW zf4GT3i_@)f(Fj}gAZj`7205M8!B&HhmbgyZB& z+COyAVNxql#DwfP;H48Yc+Y~ChV6b9auLnfXXvpjr<~lQ@>VbCpQvWz=lyVf1??_c zAo3C^otZD@(v?X)UX*@w?TF|F8KF>l7%!Dzu+hksSA^akEkx8QD(V(lK+HBCw6C}2onVExW)f$ zncm*HI(_H;jF@)6eu}Tln!t?ynRkcqBA5MitIM@L^(4_Ke}vy7c%$w{(`&7Rn=u>oDM+Z^RUYcbSOPwT(ONyq76R>$V6_M_UP4vs=__I#io{{((| zy5=k=oVr-Qt$FImP~+&sN8rf2UH*vRMpwohPc@9?id17La4weIfBNa>1Djy+1=ugn z@}Zs;eFY1OC}WBDxDF=i=On_33(jWE-QYV)HbQ^VM!n>Ci9_W0Zofz7!m>do@KH;S z4k}FqEAU2)b%B_B-QcPnM5Zh=dQ+4|DJoJwo?)f2nWBuZE@^>a(gP~ObzMuyNJTgJFUPcH`%9UFA(P23iaKgo0)CI!SZ>35LpFaD7 z)C2sW$ltSEYNW%%j8F;yK{iHI2Q^}coF@LX`=EvxZb*_O;2Z0Z5 z7 zlccxmCfCI;_^awp|G748%Wx%?t9Sh8!V9Y(9$B?9R`G)Nd&snX1j+VpuQ@GGk=y(W zK|<$O`Cad`Y4#W3GKXgs%lZduAd1t1<7LwG4*zaStE*S)XXPFDyKdgiaVXG2)LvDn zf}eQ_S(&2!H0Mq1Yt&WpM1!7b#yt_ie7naOfX129_E=)beKj|p1VW9q>>+e$3@G$K zrB%i_TT1DHjOf7IQ8)Wu4#K%ZSCDGMP7Ab|Kvjq7*~@ewPm~h_-8d4jmNH<&mNZC@CI zKxG5O08|@<4(6IEC@L-lcrrvix&_Dj4tBvl=8A}2UX|)~v#V$L22U}UHk`B-1MF(t zU6aVJWR!>Y0@4m0UA%Sq9B5;4hZvsOu=>L`IU4#3r_t}os|vSDVMA??h>QJ1FD1vR z*@rclvfD!Iqoxh>VP+?b9TVH8g@KjYR@rRWQy44A`f6doIi+8VTP~pa%`(Oa@5?=h z8>YxNvA##a3D0)^P|2|+0~f|UsAJV=q(S>eq-dehQ+T>*Q@qN zU8@kdpU5gGk%ozt?%c8oM6neA?GuSsOfU_b1U)uiEP8eRn~>M$p*R z43nSZs@^ahO78s zulbK@@{3=2=@^yZ)DuIC$ki;`2WNbD_#`LOHN9iMsrgzt-T<8aeh z(oXrqI$Kgt6)Icu=?11NWs>{)_ed1wh>)wv6RYNUA-C&bejw{cBE_5Wzeo!AHdTd+ z)d(_IKN7z^n|As~3XS=cCB_TgM7rK;X586re`{~Foml$aKs zb!4Pe7hEP|370EWwn$HKPM!kL94UPZ1%8B^e5fB+=Iw^6=?5n3tZGYjov83CLB&OQ++p)WCMeshCv_9-~G9C_2x`LxTDjUcW$l6e!6-&a^fM3oP9*g(H zmCk0nGt1UMdU#pfg1G0um5|sc|KO<+qU1E4iBF~RvN*+`7uNHH^gu{?nw2DSCjig% zI@ymKZSK=PhHJa(jW&xeApv&JcfSmNJ4uQ|pY=Lcc>=J|{>5Ug3@x#R_b@55xFgfs za^ANzWdD$ZYtFs$d7+oiw0ZmPk2&l|< zc8()wfiJx@EGpQT zG$8iLkQZ-086doF1R zh<#9cz_vRsJdoXbD=QgOtpm}cFAJX8c}>Jew;PQJSXSb^;wlC zxXLHTS|!GZ-VK_4wV<9bk4RUmlsByzW_^b>)$6R+jQ}^wco1nMA`9Lncs;&QGp!`5Tx#aXXU?}5_RrtUY zx(EMzDhl-a^y^f5yfFLMnOO#u)l69&4M?|ne|2EV>zQ}4JQCBel?~2I4?D|>L$%H(peOOII!U}i z-j)*h1rODe9{0`xmhG;`AKqw1p0_KhEIU8)DoGnEn9wAhXPaxO_(jNSij~J5m$P*$ z9Mt(t;eV}2+i|kjQpBFcNb7_(VbuF<;RQB~R~p>2*Lg>a&7DEEuq*I%Ls4{zHeUDq z+M0&YhEn^C*9-B4Q7HJ$xj)dORCXPK+)ZtLOa0o&)Sl+f(Y{p*68$-#yagW5^HQnQ z0pWpoQpxg8<&gx9im(>=x6v#&RbQ7^AsjxeSDA? zi4MEJUC~ByG!PiBjq7$pK&FA^5 z=Y@dtQnuy%IfsaR`TVP0q^3mixl&J-3!$H!ua#{A>0Z1JdLq#d4UV9nlYm641ZHl zH6mK~iI6lR3OUEVL}Z5{ONZ_6{Nk%Bv03ag<1HVN?R%w2^aR5@E>6(r>}IoMl$wRF zWr-DItN*k7T$NTT8B)+23c?171sADhjInb2Xb>GhFYGC&3{b>huvLlaS4O z^{j5q+b5H?Z)yuy%AByaVl2yj9cnalY1sMQ zXI#e%*CLajxGxP!K6xf9RD2pMHOfAa1d^Lr6kE`IBpxOiGXfNcoQ*FI6wsNtLD!T+ zC4r2q>5qz0f}UY^RY#1^0*FPO*Zp-U1h9U|qWjwqJaDB(pZ`<`U-xo7+JB$zvwV}^ z2>$0&Q5k#l|Er7*PPG1ycj4BGz zg&`d*?nUi1Q!OB>{V@T$A;)8@h;*Rb1{xk_8X<34L`s}xkH-rQZvjM`jI=jaJRGRg zeEcjYChf-78|RLrao%4HyZBfnAx5KaE~@Sx+o-2MLJ>j-6uDb!U`odj*=)0k)K75l zo^)8-iz{_k7-_qy{Ko~N#B`n@o#A22YbKiA>0f3k=p-B~XX=`Ug>jl$e7>I=hph0&AK z?ya;(NaKY_!od=tFUcGU5Kwt!c9EPUQLi;JDCT*{90O@Wc>b| zI;&GIY$JlQW^9?R$-OEUG|3sp+hn+TL(YK?S@ZW<4PQa}=IcUAn_wW3d!r#$B}n08 z*&lf(YN21NDJ74DqwV`l`RX(4zJ<(E4D}N0@QaE-hnfdPDku~@yhb^AeZL73RgovX z6=e>!`&e^l@1WA5h!}}PwwL*Gjg!LbC5g0|qb8H$^S{eGs%cc?4vTyVFW=s6KtfW? z@&Xm+E(uz(qDbwDvRQI9DdB<2sW}FYK9sg*f%-i*>*n{t-_wXvg~N7gM|a91B!x|K zyLbJ~6!!JZpZ`#HpCB8g#Q*~VU47Rp$NyZb3WhEgg3ivSwnjGJgi0BEV?!H}Z@QF| zrO`Kx*52;FR#J-V-;`oR-pr!t>bYf)UYcixN=(FUR6$fhN@~i09^3WeP3*)D*`*mJ z1u%klAbzQ=P4s%|FnVTZv%|@(HDB+ap5S#cFSJUSGkyI*Y>9Lwx|0lTs%uhoCW(f1 zi+|a9;vDPfh3nS<7m~wqTM6+pEm(&z-Ll;lFH!w#(Uk#2>Iv~2Hu}lITn7hnOny`~ z*Vj=r<&Nwpq^@g5m`u&QTBRoK*}plAuHg$L$~NO#wF0!*r0OfcS%)k0A??uY*@B^C zJe9WdU(w){rTIf<;rwJt^_35^d<A@$FqEZW6kwyfAo2x0T$Ye2MZox6Z7<%Qbu$}}u{rtE+h2M+Z}T4I zxF1cwJ(Uvp!T#mogWkhb(?SxD4_#tV(Sc8N4Gu*{Fh#})Pvb^ef%jrlnG*&Ie+J5 zsly5oo?1((um&lLDxn(DkYtk`My>lgKTp3Y4?hTQ4_`YNOFtjF-FUY#d#(EQd(rfz zB8z%Vi;?x)ZM$3c>yc5H8KBvSevnWNdCbAj?QCac)6-K~Xz@EZp}~N9q)5*Ufjz3C z6kkOeI{3H(^VO8hKDrVjy2DXd;5wr4nb`19yJi0DO@607MSx+7F$ zz3F7sl8JV@@sM$6`#JmSilqI%Bs)}Py2eFT;TjcG5?8$zwV60b(_5A>b#uk~7U^bO z>y|6SCrP2IGST(8HFuX|XQUXPLt2gL_hm|uj1Ws`O2VW>SyL^uXkl>Zvkcpi?@!F7 z%svLoT@{R#XrIh^*dE~$YhMwC+b7JE09NAS47kT%Ew zD!XjxA@1+KOAyu`H2z#h+pGm!lG>WI0v745l+Fd><3dh{ATq%h?JSdEt zu%J*zfFUx%Tx&0DS5WSbE)vwZSoAGT=;W#(DoiL($BcK;U*w`xA&kheyMLI673HCb7fGkp{_vdV2uo;vSoAH z9BuLM#Vzwt#rJH>58=KXa#O;*)_N{$>l7`umacQ0g$pI3iW4=L--O;Wiq0zy7OKp`j2r^y3`7X!?sq9rr5B{41BkBr1fEd1#Q3 z-dXc2RSb4U>FvpVhlQCIzQ-hs=8420z=7F2F(^xD;^RXgpjlh8S6*xCP#Gj2+Q0bAg?XARw3dnlQ*Lz3vk}m`HXmCgN=?bIL{T zi}Ds-xn|P)dxhraT@XY$ZQ&^%x8y!o+?n#+>+dZ1c{hYwNTNRke@3enT(a@}V*X{! z81+{Jc2UR;+Zcbc6cUlafh4DFKwp>;M}8SGD+YnW3Q_)*9Z_pny_z+MeYQmz?r%EVaN0d!NE*FVPq&U@vo{ef6wkMIDEWLbDs zz91$($XbGnQ?4WHjB~4xgPgKZts{p|g1B{-4##}#c5aL5C6_RJ_(*5>85B1}U!_<``}q-97Q7~u)(&lsb(WT^(*n7H%33%@_b zO5(?-v??s??33b19xiB7t_YT!q8!qAzN1#RD@3;kYAli%kazt#YN7}MhVu=ljuz27 z1`<+g8oVwy57&$`CiHeaM)tz(OSt4E# zJ@P6E*e504oUw~RD(=9WP8QdW^6wRdFbKII!GAWecJ(?{`EzTR@?j!3g?$@LLCt;U={>!9z7DU!(1Jq zqEwdx5q?W1Ncm7mXP8MFwAr?nw5$H%cb>Q><9j{Tk2RY9ngGvaJgWXx^r!ywk{ph- zs2PFto4@IIwBh{oXe;yMZJYlS?3%a-CJ#js90hoh5W5d^OMwCFmpryHFr|mG+*ZP$ zqyS5BW@s}|3xUO0PR<^{a2M(gkP5BDGxvkWkPudSV*TMRK5Qm4?~VuqVAOerffRt$HGAvp;M++Iq$E6alB z;ykBr-eZ6v_H^1Wip56Czj&=`mb^TsX|FPN#-gnlP03AkiJDM=?y|LzER1M93R4sC z*HT(;EV=*F*>!+Z{r!KG?6ODMGvkt3viG=@kQJHNMYd}bS4KrrHf4`&*(0m0R5Hqz zEk)r=sFeS?MZRvn<@Z0&bDw)XkMnw+_xqgp=W{;ioX`6;G-P9N%wfoYJ$-m$L#MC% z^sH?tSzA|WWP(cN3({~_*X$l{M*;1V{l$;T6b){#l4pswDTid26HaXgKed}13YIP= zJRvA3nmx{}R$Lr&S4!kWU3`~dxM}>VXWu6Xd(VP}z1->h&f%82eXD_TuTs@=c;l0T z|LHmWKJ+?7hkY=YM>t}zvb4|lV;!ARMtWFp!E^J=Asu9w&kVF*i{T#}sY++-qnVh! z5TQ|=>)+vutf{&qB+LO9^jm#rD7E5+tcorr^Fn5Xb0B;)f^$7Ev#}G_`r==ea294V z--v4LwjswWlSq9ba6i?IXr8M_VEGQ$H%hCqJTFQ3+1B9tmxDUhnNU%dy4+zbqYJ|o z3!N{b?A@{;cG2~nb-`|z;gEDL5ffF@oc3`R{fGi)0wtMqEkw4tRX3t;LVS3-zAmg^ zgL7Z{hmdPSz9oA@t>tZ1<|Khn&Lp=_!Q=@a?k+t~H&3jN?dr(}7s;{L+jiKY57?WsFBfW^mu6a03_^VKrdK=9egXw@!nzZ3TbYc*osyQNoCXPYoFS<&Nr97MrQCOK(gO8 z;0@iqRTJy4-RH)PJld5`AJN}n?5r^-enKrHQOR;z>UMfm+e8~4ZL5k>oXMiYq12Bx4eVQv0jFgp_zC#``sjZpywYqISMP}VZ@!~1Mf$!x|opj%mQ98JnSk@`~ zPmmyuPZKtZOnEC!1y!?`TYRsZ!II;d!iln}%e}bk5qIiUADERr*K$3dekgHV9TtBX zi5q!J!6Zgd#cLxRmZN^J`o@Zv{+p+<_#8^nvY)44Hw_2i@?R&5n^q33fpOnDg1nPQ z_r<$hURl~OketX|Tdbvf_7=3x^rSFJtEp@tuDpVB&uq)qW;xUQ7mmkr-@eZwa$l+? zoKk``Vz@TH#>jMce*8>@FZ+@BEUdYa_K0i|{*;j9MW3K%pnM*T;@>|o@lMhgLrpZP5aol(z>g;b4}|e$U~Fn zGL%(}p%Jsl4LxE!VW_Y4T>e}W4e#~F03H_^R!Q)kpJG{lO!@I4{mFo^V#ayHh_5~o zB$O71gcE(G@6xv);#Ky?e(Ed}^O+Ho(t=93T9T3TnEY(OVf_dR-gY@jj+iJSY?q|6prBv(S9A4k=2fNZz!W@S=B@~b?TJRTuBQq448@juN#Y=3q=^VCF>Z}n6wICJ<^^Kn8C;mK zZYiFSN#Z$?NDGV7(#}q2tAZAtE63icK-MY>UQu4MWlGIbJ$AF8Zt-jV;@7P5MPI>% zPWvO!t%1+s>-A%`;0^o8Ezeaa4DMwI8ooQrJ;ax@Qt*6XONWw)dPwOPI9@u*EG&844*1~EoZ2qsAe~M>d`;Bc_CWY zMoDKEmDh-}k9d6*<0g@aQmsnrM1H9IcKYZs)><)d92{|0Hh8?~XbF)7U+UmP@Pw_6geVB?7N$4J4*E0z3EO&5kRS(EE zv92(+e5WxLXMN{h;-|8@!Q#0q247hb^3R%*k3MuMO5*L}$0D#5P*N$aHd54C+=_RToYXTyewugOaDmGsCvb4H1s=@gkfVnzTCWKMa-Mm1v4Wq!t-JIrbV&EWwKDe ze#kJpOq#iRlFz%5#6Fio9IUlKnQ#X&DY8Ux#<-WqxAac-y%U_L+EZZ4Rg5*yNg`f< zSZn&uio@zanUCPqX1l4W&B!;UWs#P7B^|4WwoCxQXl|44n^cBNqu=3Vl*ltAqsUQO z9q_@nD0zq0O8r`coEm>9+|rA3HL#l}X;0##>SJS$cVavOZVCpSGf4mUU1( zWaRCUYc^9QbG9=vpWo%xP}CMFnMb{reA`K7tT(t5DM)d9l}jVPY>qoRzT zE3m-p#=i=$9x*CB`AL>SY}u3agYFl#uULNen#&44H;!L@I{RI=PlWxG8J((f)ma7A z@jLvQ>?Nx`n?3ChRG#HqE3MXP8*o3!Qq`+t8EMt_p)oeKHqPusBxPn!#?R??-=e3e zo73WNs_IZF`WLigre=|`aS2^> zN1zn!7k&Dh28t%VpJ%**&E!eAcB5oLjQFFcJQj*URMia%Ya3@q1UQ18=oWMM6`I}iT_&L1gl?*~6nU4q4Z0`H<5yDp(HeZ+RGf9`mM&= zn-qRp%i!g$R;i1d1aMZ{IewNjE@p2+Z{`x{*xL*x$?WV~{BjJpsP&C&JK0HLoyf z`0z^v&fBQSa!I7FU~9MaQ%e|?RP>sM^2PL!mE^Q1Ig_4M$5BRfi72oMYu6Ke?wmDX z@0a%-V|z}b23K=ye(W+fG#w|jJUnT{=KR5jfuq!RX}<1irTDw(${<&}dWQu4;EuE< z@3u4dBkQaCHHM&;cE0z50_V!(vJ1_V)A8?C#eJuLkt!98Z%|Bgzidc0j|z(&o)TCzYlrgZA zC3@i>L!&Gw_~7`>puB97I2lK)lESZQqVXc_8T^G2O#VHhO?IC$g zOYhXJ7)~C<8l|Xrftka@QuowScM{K&0zskoU$Aw~vIRVRF9TEQ4*3=_5)98B`=t8(N%ZuWqmwlW zllAzq=E5_5!sKDXam@w`ZD(nl%LAPxQuEtDcKPqu9LPJvNIITawU#c^PQ2HmZgs)r zH^+gRwZ?0)8IFQgU)+p@0Iqb^tcEoqcB@zhfz_FaOM&_d<|jnU>q5nSKa<@%9|dje zIupcg1!tRiMP4X=oG<7s4|AW&^-Cw4FL9OuI$t zxjc*y;Uw!G7a|jz>E*2+PlR(CemWebS7m-&*CDwnmxbiRqJvQ&os-sC&4OWt^(2@vG4|jui#Df@-D= zh3D%8Y3R6+jRBStSvH9pt&tCI`NK08J1*pC(?OM0h!bS-JK3I}`pDY-fDIaB_*W6KS+TO0Q*%kkeuN6uWITt=TsCGw6uBE710q; zRluI%j{?@jwhM|l5&TB!-TkQs!A=DXRE>u18t@;zndD0M$U@Igrt?UW2; z7%=dsHIVH_LCkGUU0fW&UMjDnvjcc0Mp(mK&;d~ZJ5EJ)#7@aTZvGDFXzFZg2Lq~s z5PR_LazNN)JD5K_uK*Hy{mXuHTkGGv|9V8KP#iQ$3!G*^>7UiE{|1G1A-qg(xH;Xa>&%f|BZkH zG=J^0pHzSAqv5*5ysQ{Puy^-_|IPrii zKS$mE10Zngf>Sgg@BjpRyJbrHeo zD8Ro0LI*W#+9?^xlOS^c>Z^^n^0I|FH^@^`ZR`{H=$ zjO0_$cnpBM7Zcm?H_RXIu-Lu~qweDSV|tEZBZh!e6hQy->}e;d#osZ1hQj{HhHkC0 zJ|F-HKmeTGgDe979ogBz24;@<|I7;TU!IXb@oWMsMECIETmQy`zPtM`|NP}PjzR_u zKMG1Z{%1kWeMfEf(10U#w!clmQ2)JC8zm(Fv!H4dUHQHCFLikID?hrd{0>kCQt?kP zdqn2ZG0}ytcQJ7t_B3s0ZvH3PYjkjQ`Q%;jV@?MK-+z3etBCGGo4f4`y^|AdCs!DH zThTQ;cL5dM{|tB_1y6K3bVa^hx_<9J(}5`2SDz1^0bT!Vm*JV;9~t&{IC{$DUAVV* z{|E=#yN{wNdTY@$6z{_KNA3&%w|vFu1n9XRcM0Ak>`UW!lQ`ah3D4r%}Z diff --git a/examples/extension/gradle/wrapper/gradle-wrapper.properties b/examples/extension/gradle/wrapper/gradle-wrapper.properties index 37aef8d3f0c9..8e876e1c5571 100644 --- a/examples/extension/gradle/wrapper/gradle-wrapper.properties +++ b/examples/extension/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,8 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-bin.zip +distributionSha256Sum=1541fa36599e12857140465f3c91a97409b4512501c26f9631fb113e392c5bd1 +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.1-bin.zip networkTimeout=10000 +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/examples/extension/gradlew b/examples/extension/gradlew index 79a61d421cc4..f5feea6d6b11 100755 --- a/examples/extension/gradlew +++ b/examples/extension/gradlew @@ -15,6 +15,8 @@ # See the License for the specific language governing permissions and # limitations under the License. # +# SPDX-License-Identifier: Apache-2.0 +# ############################################################################## # @@ -55,7 +57,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. @@ -83,10 +85,9 @@ done # This is normally unused # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} -APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit - -# 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"' +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s +' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum @@ -133,10 +134,13 @@ 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. + if ! command -v java >/dev/null 2>&1 + then + 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 fi # Increase the maximum file descriptors if we can. @@ -144,7 +148,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 + # shellcheck disable=SC2039,SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac @@ -152,7 +156,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then '' | soft) :;; #( *) # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 + # shellcheck disable=SC2039,SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac @@ -197,11 +201,15 @@ if "$cygwin" || "$msys" ; then done fi -# Collect all arguments for the java command; -# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of -# shell script including quotes and variable substitutions, so put them in -# double quotes to make sure that they get re-expanded; and -# * put everything else in single quotes, so that it's not re-expanded. + +# 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"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ diff --git a/examples/extension/gradlew.bat b/examples/extension/gradlew.bat index 93e3f59f135d..9d21a21834d5 100644 --- a/examples/extension/gradlew.bat +++ b/examples/extension/gradlew.bat @@ -13,6 +13,8 @@ @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem @if "%DEBUG%"=="" @echo off @rem ########################################################################## @@ -43,11 +45,11 @@ set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if %ERRORLEVEL% equ 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. +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail @@ -57,11 +59,11 @@ 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. +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail diff --git a/examples/extension/src/main/java/com/example/javaagent/DemoAutoConfigurationCustomizerProvider.java b/examples/extension/src/main/java/com/example/javaagent/DemoAutoConfigurationCustomizerProvider.java index 77838a528a32..85a9c4755f51 100644 --- a/examples/extension/src/main/java/com/example/javaagent/DemoAutoConfigurationCustomizerProvider.java +++ b/examples/extension/src/main/java/com/example/javaagent/DemoAutoConfigurationCustomizerProvider.java @@ -72,6 +72,7 @@ private Map getDefaultProperties() { "otel.exporter.otlp.endpoint", "http://" + EnvironmentConfig.MW_AGENT_SERVICE + ":9319"); properties.put("otel.metrics.exporter", "otlp"); properties.put("otel.logs.exporter", "otlp"); + properties.put("otel.exporter.otlp.protocol", "grpc"); properties.put("otel.instrumentation.runtime-telemetry-java17.enable-all", "true"); return properties; } diff --git a/examples/extension/src/main/java/com/example/javaagent/config/EnvironmentConfig.java b/examples/extension/src/main/java/com/example/javaagent/config/EnvironmentConfig.java index 937566e84a69..5017733cce4a 100644 --- a/examples/extension/src/main/java/com/example/javaagent/config/EnvironmentConfig.java +++ b/examples/extension/src/main/java/com/example/javaagent/config/EnvironmentConfig.java @@ -5,6 +5,8 @@ package com.example.javaagent.config; +import java.util.Arrays; + /** Environmental configurations. */ public class EnvironmentConfig { @@ -23,5 +25,9 @@ public class EnvironmentConfig { public static final String MW_AUTH_URL = System.getenv().getOrDefault("MW_AUTH_URL", "https://app.middleware.io/api/v1/auth"); + public static final boolean MW_APM_COLLECT_PROFILING = + Arrays.asList("true", "1", "t", "y", "yes") + .contains(System.getenv().getOrDefault("MW_APM_COLLECT_PROFILING", "True").toLowerCase()); + public static final String MW_API_KEY = System.getenv().getOrDefault("MW_API_KEY", null); } diff --git a/examples/extension/src/main/java/com/example/profile/PyroscopeProfile.java b/examples/extension/src/main/java/com/example/profile/PyroscopeProfile.java index afdb2215b56a..7f915606579e 100644 --- a/examples/extension/src/main/java/com/example/profile/PyroscopeProfile.java +++ b/examples/extension/src/main/java/com/example/profile/PyroscopeProfile.java @@ -32,7 +32,7 @@ public class PyroscopeProfile { public static void startProfiling() { try { String tenantId = authenticateAndGetTenantId(); - if (tenantId != null) { + if (tenantId != null && EnvironmentConfig.MW_APM_COLLECT_PROFILING) { String profilingServerUrl = EnvironmentConfig.MW_PROFILING_SERVER_URL; if (profilingServerUrl == null) { profilingServerUrl = "https://" + tenantId + ".middleware.io/profiling"; @@ -46,6 +46,8 @@ public static void startProfiling() { .setServerAddress(profilingServerUrl) .setTenantID(tenantId) .build()); + } else if (!EnvironmentConfig.MW_APM_COLLECT_PROFILING) { + logger.warning("Profiling is not initiated as MW_APM_COLLECT_PROFILE is disabled"); } else { logger.warning("Profiling is not initiated as authentication is failed"); } diff --git a/examples/extension/src/test/java/com/example/javaagent/smoketest/IntegrationTest.java b/examples/extension/src/test/java/com/example/javaagent/smoketest/IntegrationTest.java index 124144bd71bb..5346990bc298 100644 --- a/examples/extension/src/test/java/com/example/javaagent/smoketest/IntegrationTest.java +++ b/examples/extension/src/test/java/com/example/javaagent/smoketest/IntegrationTest.java @@ -31,6 +31,7 @@ import org.testcontainers.containers.Network; import org.testcontainers.containers.output.Slf4jLogConsumer; import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.containers.wait.strategy.WaitStrategy; import org.testcontainers.utility.MountableFile; abstract class IntegrationTest { @@ -51,6 +52,8 @@ abstract class IntegrationTest { protected abstract String getTargetImage(int jdk); + protected abstract WaitStrategy getTargetWaitStrategy(); + /** Subclasses can override this method to customise target application's environment */ protected Map getExtraEnv() { return Collections.emptyMap(); @@ -59,7 +62,7 @@ protected Map getExtraEnv() { private static GenericContainer backend; @BeforeAll - static void setupSpec() { + static void setup() { backend = new GenericContainer<>( "ghcr.io/open-telemetry/opentelemetry-java-instrumentation/smoke-test-fake-backend:20221127.3559314891") @@ -98,7 +101,11 @@ private GenericContainer buildTargetContainer(String agentPath, String extens .withEnv("OTEL_BSP_MAX_EXPORT_BATCH", "1") .withEnv("OTEL_BSP_SCHEDULE_DELAY", "10") .withEnv("OTEL_PROPAGATORS", "tracecontext,baggage,demo") - .withEnv(getExtraEnv()); + // TODO (heya) update smoke tests to run using http/protobuf + // in the meantime, force smoke tests to use grpc protocol for all exporters + .withEnv("OTEL_EXPORTER_OTLP_PROTOCOL", "grpc") + .withEnv(getExtraEnv()) + .waitingFor(getTargetWaitStrategy()); // If external extensions are requested if (extensionLocation != null) { // Asks instrumentation agent to include extensions from given location into its runtime @@ -112,11 +119,13 @@ private GenericContainer buildTargetContainer(String agentPath, String extens } @AfterEach - void cleanup() throws IOException { + void reset() throws IOException { client .newCall( new Request.Builder() - .url(String.format("http://localhost:%d/clear", backend.getMappedPort(8080))) + .url( + String.format( + "http://%s:%d/clear", backend.getHost(), backend.getMappedPort(8080))) .build()) .execute() .close(); @@ -127,7 +136,7 @@ void stopTarget() { } @AfterAll - static void cleanupSpec() { + static void cleanup() { backend.stop(); } @@ -194,7 +203,9 @@ private String waitForContent() throws IOException, InterruptedException { Request request = new Request.Builder() - .url(String.format("http://localhost:%d/get-traces", backend.getMappedPort(8080))) + .url( + String.format( + "http://%s:%d/get-traces", backend.getHost(), backend.getMappedPort(8080))) .build(); try (ResponseBody body = client.newCall(request).execute().body()) { diff --git a/examples/extension/src/test/java/com/example/javaagent/smoketest/SpringBootIntegrationTest.java b/examples/extension/src/test/java/com/example/javaagent/smoketest/SpringBootIntegrationTest.java index e1301f120ccd..73ff70ebcc7c 100644 --- a/examples/extension/src/test/java/com/example/javaagent/smoketest/SpringBootIntegrationTest.java +++ b/examples/extension/src/test/java/com/example/javaagent/smoketest/SpringBootIntegrationTest.java @@ -14,9 +14,10 @@ import okhttp3.Request; import okhttp3.Response; import org.junit.jupiter.api.Assertions; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.containers.wait.strategy.WaitStrategy; class SpringBootIntegrationTest extends IntegrationTest { - @Override protected String getTargetImage(int jdk) { return "ghcr.io/open-telemetry/opentelemetry-java-instrumentation/smoke-test-spring-boot:jdk" @@ -24,48 +25,43 @@ protected String getTargetImage(int jdk) { + "-20211213.1570880324"; } + @Override + protected WaitStrategy getTargetWaitStrategy() { + return Wait.forHttp("/").forPort(8080).forStatusCode(200); + } + // @Test // public void extensionsAreLoadedFromJar() throws IOException, InterruptedException { - // startTarget("/opentelemetry-extensions.jar"); - - // testAndVerify(); - - // stopTarget(); + // startTarget("/opentelemetry-extensions.jar"); + // testAndVerify(); + // stopTarget(); // } // @Test // public void extensionsAreLoadedFromFolder() throws IOException, InterruptedException { - // startTarget("/"); - - // testAndVerify(); - - // stopTarget(); + // startTarget("/"); + // testAndVerify(); + // stopTarget(); // } // @Test // public void extensionsAreLoadedFromJavaagent() throws IOException, InterruptedException { - // startTargetWithExtendedAgent(); - - // testAndVerify(); - - // stopTarget(); + // 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"))); diff --git a/gradle-plugins/build.gradle.kts b/gradle-plugins/build.gradle.kts index d9e0c3d8cfa0..ea07420a98fc 100644 --- a/gradle-plugins/build.gradle.kts +++ b/gradle-plugins/build.gradle.kts @@ -24,11 +24,11 @@ configurations.named("compileOnly") { extendsFrom(bbGradlePlugin) } -val byteBuddyVersion = "1.14.5" +val byteBuddyVersion = "1.15.1" val aetherVersion = "1.1.0" dependencies { - implementation("com.google.guava:guava:32.0.1-jre") + implementation("com.google.guava:guava:33.3.0-jre") // we need to use byte buddy variant that does not shade asm implementation("net.bytebuddy:byte-buddy-gradle-plugin:${byteBuddyVersion}") { exclude(group = "net.bytebuddy", module = "byte-buddy") @@ -39,11 +39,11 @@ dependencies { implementation("org.eclipse.aether:aether-transport-http:${aetherVersion}") implementation("org.apache.maven:maven-aether-provider:3.3.9") - implementation("gradle.plugin.com.github.johnrengelman:shadow:8.0.0") + implementation("com.gradleup.shadow:shadow-gradle-plugin:8.3.1") - testImplementation("org.assertj:assertj-core:3.24.2") + testImplementation("org.assertj:assertj-core:3.26.3") - testImplementation(enforcedPlatform("org.junit:junit-bom:5.9.3")) + testImplementation(enforcedPlatform("org.junit:junit-bom:5.11.0")) testImplementation("org.junit.jupiter:junit-jupiter-api") testImplementation("org.junit.jupiter:junit-jupiter-params") testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine") diff --git a/gradle-plugins/gradle/wrapper/gradle-wrapper.jar b/gradle-plugins/gradle/wrapper/gradle-wrapper.jar index ccebba7710deaf9f98673a68957ea02138b60d0a..a4b76b9530d66f5e68d973ea569d8e19de379189 100644 GIT binary patch literal 43583 zcma&N1CXTcmMvW9vTb(Rwr$&4wr$(C?dmSu>@vG-+vuvg^_??!{yS%8zW-#zn-LkA z5&1^$^{lnmUON?}LBF8_K|(?T0Ra(xUH{($5eN!MR#ZihR#HxkUPe+_R8Cn`RRs(P z_^*#_XlXmGv7!4;*Y%p4nw?{bNp@UZHv1?Um8r6)Fei3p@ClJn0ECfg1hkeuUU@Or zDaPa;U3fE=3L}DooL;8f;P0ipPt0Z~9P0)lbStMS)ag54=uL9ia-Lm3nh|@(Y?B`; zx_#arJIpXH!U{fbCbI^17}6Ri*H<>OLR%c|^mh8+)*h~K8Z!9)DPf zR2h?lbDZQ`p9P;&DQ4F0sur@TMa!Y}S8irn(%d-gi0*WxxCSk*A?3lGh=gcYN?FGl z7D=Js!i~0=u3rox^eO3i@$0=n{K1lPNU zwmfjRVmLOCRfe=seV&P*1Iq=^i`502keY8Uy-WNPwVNNtJFx?IwAyRPZo2Wo1+S(xF37LJZ~%i)kpFQ3Fw=mXfd@>%+)RpYQLnr}B~~zoof(JVm^^&f zxKV^+3D3$A1G;qh4gPVjhrC8e(VYUHv#dy^)(RoUFM?o%W-EHxufuWf(l*@-l+7vt z=l`qmR56K~F|v<^Pd*p~1_y^P0P^aPC##d8+HqX4IR1gu+7w#~TBFphJxF)T$2WEa zxa?H&6=Qe7d(#tha?_1uQys2KtHQ{)Qco)qwGjrdNL7thd^G5i8Os)CHqc>iOidS} z%nFEDdm=GXBw=yXe1W-ShHHFb?Cc70+$W~z_+}nAoHFYI1MV1wZegw*0y^tC*s%3h zhD3tN8b=Gv&rj}!SUM6|ajSPp*58KR7MPpI{oAJCtY~JECm)*m_x>AZEu>DFgUcby z1Qaw8lU4jZpQ_$;*7RME+gq1KySGG#Wql>aL~k9tLrSO()LWn*q&YxHEuzmwd1?aAtI zBJ>P=&$=l1efe1CDU;`Fd+_;&wI07?V0aAIgc(!{a z0Jg6Y=inXc3^n!U0Atk`iCFIQooHqcWhO(qrieUOW8X(x?(RD}iYDLMjSwffH2~tB z)oDgNBLB^AJBM1M^c5HdRx6fBfka`(LD-qrlh5jqH~);#nw|iyp)()xVYak3;Ybik z0j`(+69aK*B>)e_p%=wu8XC&9e{AO4c~O1U`5X9}?0mrd*m$_EUek{R?DNSh(=br# z#Q61gBzEpmy`$pA*6!87 zSDD+=@fTY7<4A?GLqpA?Pb2z$pbCc4B4zL{BeZ?F-8`s$?>*lXXtn*NC61>|*w7J* z$?!iB{6R-0=KFmyp1nnEmLsA-H0a6l+1uaH^g%c(p{iT&YFrbQ$&PRb8Up#X3@Zsk zD^^&LK~111%cqlP%!_gFNa^dTYT?rhkGl}5=fL{a`UViaXWI$k-UcHJwmaH1s=S$4 z%4)PdWJX;hh5UoK?6aWoyLxX&NhNRqKam7tcOkLh{%j3K^4Mgx1@i|Pi&}<^5>hs5 zm8?uOS>%)NzT(%PjVPGa?X%`N2TQCKbeH2l;cTnHiHppPSJ<7y-yEIiC!P*ikl&!B z%+?>VttCOQM@ShFguHVjxX^?mHX^hSaO_;pnyh^v9EumqSZTi+#f&_Vaija0Q-e*| z7ulQj6Fs*bbmsWp{`auM04gGwsYYdNNZcg|ph0OgD>7O}Asn7^Z=eI>`$2*v78;sj-}oMoEj&@)9+ycEOo92xSyY344^ z11Hb8^kdOvbf^GNAK++bYioknrpdN>+u8R?JxG=!2Kd9r=YWCOJYXYuM0cOq^FhEd zBg2puKy__7VT3-r*dG4c62Wgxi52EMCQ`bKgf*#*ou(D4-ZN$+mg&7$u!! z-^+Z%;-3IDwqZ|K=ah85OLwkO zKxNBh+4QHh)u9D?MFtpbl)us}9+V!D%w9jfAMYEb>%$A;u)rrI zuBudh;5PN}_6J_}l55P3l_)&RMlH{m!)ai-i$g)&*M`eN$XQMw{v^r@-125^RRCF0 z^2>|DxhQw(mtNEI2Kj(;KblC7x=JlK$@78`O~>V!`|1Lm-^JR$-5pUANAnb(5}B}JGjBsliK4& zk6y(;$e&h)lh2)L=bvZKbvh@>vLlreBdH8No2>$#%_Wp1U0N7Ank!6$dFSi#xzh|( zRi{Uw%-4W!{IXZ)fWx@XX6;&(m_F%c6~X8hx=BN1&q}*( zoaNjWabE{oUPb!Bt$eyd#$5j9rItB-h*5JiNi(v^e|XKAj*8(k<5-2$&ZBR5fF|JA z9&m4fbzNQnAU}r8ab>fFV%J0z5awe#UZ|bz?Ur)U9bCIKWEzi2%A+5CLqh?}K4JHi z4vtM;+uPsVz{Lfr;78W78gC;z*yTch~4YkLr&m-7%-xc ztw6Mh2d>_iO*$Rd8(-Cr1_V8EO1f*^@wRoSozS) zy1UoC@pruAaC8Z_7~_w4Q6n*&B0AjOmMWa;sIav&gu z|J5&|{=a@vR!~k-OjKEgPFCzcJ>#A1uL&7xTDn;{XBdeM}V=l3B8fE1--DHjSaxoSjNKEM9|U9#m2<3>n{Iuo`r3UZp;>GkT2YBNAh|b z^jTq-hJp(ebZh#Lk8hVBP%qXwv-@vbvoREX$TqRGTgEi$%_F9tZES@z8Bx}$#5eeG zk^UsLBH{bc2VBW)*EdS({yw=?qmevwi?BL6*=12k9zM5gJv1>y#ML4!)iiPzVaH9% zgSImetD@dam~e>{LvVh!phhzpW+iFvWpGT#CVE5TQ40n%F|p(sP5mXxna+Ev7PDwA zamaV4m*^~*xV+&p;W749xhb_X=$|LD;FHuB&JL5?*Y2-oIT(wYY2;73<^#46S~Gx| z^cez%V7x$81}UWqS13Gz80379Rj;6~WdiXWOSsdmzY39L;Hg3MH43o*y8ibNBBH`(av4|u;YPq%{R;IuYow<+GEsf@R?=@tT@!}?#>zIIn0CoyV!hq3mw zHj>OOjfJM3F{RG#6ujzo?y32m^tgSXf@v=J$ELdJ+=5j|=F-~hP$G&}tDZsZE?5rX ztGj`!S>)CFmdkccxM9eGIcGnS2AfK#gXwj%esuIBNJQP1WV~b~+D7PJTmWGTSDrR` zEAu4B8l>NPuhsk5a`rReSya2nfV1EK01+G!x8aBdTs3Io$u5!6n6KX%uv@DxAp3F@{4UYg4SWJtQ-W~0MDb|j-$lwVn znAm*Pl!?Ps&3wO=R115RWKb*JKoexo*)uhhHBncEDMSVa_PyA>k{Zm2(wMQ(5NM3# z)jkza|GoWEQo4^s*wE(gHz?Xsg4`}HUAcs42cM1-qq_=+=!Gk^y710j=66(cSWqUe zklbm8+zB_syQv5A2rj!Vbw8;|$@C!vfNmNV!yJIWDQ>{+2x zKjuFX`~~HKG~^6h5FntRpnnHt=D&rq0>IJ9#F0eM)Y-)GpRjiN7gkA8wvnG#K=q{q z9dBn8_~wm4J<3J_vl|9H{7q6u2A!cW{bp#r*-f{gOV^e=8S{nc1DxMHFwuM$;aVI^ zz6A*}m8N-&x8;aunp1w7_vtB*pa+OYBw=TMc6QK=mbA-|Cf* zvyh8D4LRJImooUaSb7t*fVfih<97Gf@VE0|z>NcBwBQze);Rh!k3K_sfunToZY;f2 z^HmC4KjHRVg+eKYj;PRN^|E0>Gj_zagfRbrki68I^#~6-HaHg3BUW%+clM1xQEdPYt_g<2K+z!$>*$9nQ>; zf9Bei{?zY^-e{q_*|W#2rJG`2fy@{%6u0i_VEWTq$*(ZN37|8lFFFt)nCG({r!q#9 z5VK_kkSJ3?zOH)OezMT{!YkCuSSn!K#-Rhl$uUM(bq*jY? zi1xbMVthJ`E>d>(f3)~fozjg^@eheMF6<)I`oeJYx4*+M&%c9VArn(OM-wp%M<-`x z7sLP1&3^%Nld9Dhm@$3f2}87!quhI@nwd@3~fZl_3LYW-B?Ia>ui`ELg z&Qfe!7m6ze=mZ`Ia9$z|ARSw|IdMpooY4YiPN8K z4B(ts3p%2i(Td=tgEHX z0UQ_>URBtG+-?0E;E7Ld^dyZ;jjw0}XZ(}-QzC6+NN=40oDb2^v!L1g9xRvE#@IBR zO!b-2N7wVfLV;mhEaXQ9XAU+>=XVA6f&T4Z-@AX!leJ8obP^P^wP0aICND?~w&NykJ#54x3_@r7IDMdRNy4Hh;h*!u(Ol(#0bJdwEo$5437-UBjQ+j=Ic>Q2z` zJNDf0yO6@mr6y1#n3)s(W|$iE_i8r@Gd@!DWDqZ7J&~gAm1#~maIGJ1sls^gxL9LLG_NhU!pTGty!TbhzQnu)I*S^54U6Yu%ZeCg`R>Q zhBv$n5j0v%O_j{QYWG!R9W?5_b&67KB$t}&e2LdMvd(PxN6Ir!H4>PNlerpBL>Zvyy!yw z-SOo8caEpDt(}|gKPBd$qND5#a5nju^O>V&;f890?yEOfkSG^HQVmEbM3Ugzu+UtH zC(INPDdraBN?P%kE;*Ae%Wto&sgw(crfZ#Qy(<4nk;S|hD3j{IQRI6Yq|f^basLY; z-HB&Je%Gg}Jt@={_C{L$!RM;$$|iD6vu#3w?v?*;&()uB|I-XqEKqZPS!reW9JkLewLb!70T7n`i!gNtb1%vN- zySZj{8-1>6E%H&=V}LM#xmt`J3XQoaD|@XygXjdZ1+P77-=;=eYpoEQ01B@L*a(uW zrZeZz?HJsw_4g0vhUgkg@VF8<-X$B8pOqCuWAl28uB|@r`19DTUQQsb^pfqB6QtiT z*`_UZ`fT}vtUY#%sq2{rchyfu*pCg;uec2$-$N_xgjZcoumE5vSI{+s@iLWoz^Mf; zuI8kDP{!XY6OP~q5}%1&L}CtfH^N<3o4L@J@zg1-mt{9L`s^z$Vgb|mr{@WiwAqKg zp#t-lhrU>F8o0s1q_9y`gQNf~Vb!F%70f}$>i7o4ho$`uciNf=xgJ>&!gSt0g;M>*x4-`U)ysFW&Vs^Vk6m%?iuWU+o&m(2Jm26Y(3%TL; zA7T)BP{WS!&xmxNw%J=$MPfn(9*^*TV;$JwRy8Zl*yUZi8jWYF>==j~&S|Xinsb%c z2?B+kpet*muEW7@AzjBA^wAJBY8i|#C{WtO_or&Nj2{=6JTTX05}|H>N2B|Wf!*3_ z7hW*j6p3TvpghEc6-wufFiY!%-GvOx*bZrhZu+7?iSrZL5q9}igiF^*R3%DE4aCHZ zqu>xS8LkW+Auv%z-<1Xs92u23R$nk@Pk}MU5!gT|c7vGlEA%G^2th&Q*zfg%-D^=f z&J_}jskj|Q;73NP4<4k*Y%pXPU2Thoqr+5uH1yEYM|VtBPW6lXaetokD0u z9qVek6Q&wk)tFbQ8(^HGf3Wp16gKmr>G;#G(HRBx?F`9AIRboK+;OfHaLJ(P>IP0w zyTbTkx_THEOs%Q&aPrxbZrJlio+hCC_HK<4%f3ZoSAyG7Dn`=X=&h@m*|UYO-4Hq0 z-Bq&+Ie!S##4A6OGoC~>ZW`Y5J)*ouaFl_e9GA*VSL!O_@xGiBw!AF}1{tB)z(w%c zS1Hmrb9OC8>0a_$BzeiN?rkPLc9%&;1CZW*4}CDDNr2gcl_3z+WC15&H1Zc2{o~i) z)LLW=WQ{?ricmC`G1GfJ0Yp4Dy~Ba;j6ZV4r{8xRs`13{dD!xXmr^Aga|C=iSmor% z8hi|pTXH)5Yf&v~exp3o+sY4B^^b*eYkkCYl*T{*=-0HniSA_1F53eCb{x~1k3*`W zr~};p1A`k{1DV9=UPnLDgz{aJH=-LQo<5%+Em!DNN252xwIf*wF_zS^!(XSm(9eoj z=*dXG&n0>)_)N5oc6v!>-bd(2ragD8O=M|wGW z!xJQS<)u70m&6OmrF0WSsr@I%T*c#Qo#Ha4d3COcX+9}hM5!7JIGF>7<~C(Ear^Sn zm^ZFkV6~Ula6+8S?oOROOA6$C&q&dp`>oR-2Ym3(HT@O7Sd5c~+kjrmM)YmgPH*tL zX+znN>`tv;5eOfX?h{AuX^LK~V#gPCu=)Tigtq9&?7Xh$qN|%A$?V*v=&-2F$zTUv z`C#WyIrChS5|Kgm_GeudCFf;)!WH7FI60j^0o#65o6`w*S7R@)88n$1nrgU(oU0M9 zx+EuMkC>(4j1;m6NoGqEkpJYJ?vc|B zOlwT3t&UgL!pX_P*6g36`ZXQ; z9~Cv}ANFnJGp(;ZhS(@FT;3e)0)Kp;h^x;$*xZn*k0U6-&FwI=uOGaODdrsp-!K$Ac32^c{+FhI-HkYd5v=`PGsg%6I`4d9Jy)uW0y%) zm&j^9WBAp*P8#kGJUhB!L?a%h$hJgQrx!6KCB_TRo%9{t0J7KW8!o1B!NC)VGLM5! zpZy5Jc{`r{1e(jd%jsG7k%I+m#CGS*BPA65ZVW~fLYw0dA-H_}O zrkGFL&P1PG9p2(%QiEWm6x;U-U&I#;Em$nx-_I^wtgw3xUPVVu zqSuKnx&dIT-XT+T10p;yjo1Y)z(x1fb8Dzfn8e yu?e%!_ptzGB|8GrCfu%p?(_ zQccdaaVK$5bz;*rnyK{_SQYM>;aES6Qs^lj9lEs6_J+%nIiuQC*fN;z8md>r_~Mfl zU%p5Dt_YT>gQqfr@`cR!$NWr~+`CZb%dn;WtzrAOI>P_JtsB76PYe*<%H(y>qx-`Kq!X_; z<{RpAqYhE=L1r*M)gNF3B8r(<%8mo*SR2hu zccLRZwGARt)Hlo1euqTyM>^!HK*!Q2P;4UYrysje@;(<|$&%vQekbn|0Ruu_Io(w4#%p6ld2Yp7tlA`Y$cciThP zKzNGIMPXX%&Ud0uQh!uQZz|FB`4KGD?3!ND?wQt6!n*f4EmCoJUh&b?;B{|lxs#F- z31~HQ`SF4x$&v00@(P+j1pAaj5!s`)b2RDBp*PB=2IB>oBF!*6vwr7Dp%zpAx*dPr zb@Zjq^XjN?O4QcZ*O+8>)|HlrR>oD*?WQl5ri3R#2?*W6iJ>>kH%KnnME&TT@ZzrHS$Q%LC?n|e>V+D+8D zYc4)QddFz7I8#}y#Wj6>4P%34dZH~OUDb?uP%-E zwjXM(?Sg~1!|wI(RVuxbu)-rH+O=igSho_pDCw(c6b=P zKk4ATlB?bj9+HHlh<_!&z0rx13K3ZrAR8W)!@Y}o`?a*JJsD+twZIv`W)@Y?Amu_u zz``@-e2X}27$i(2=9rvIu5uTUOVhzwu%mNazS|lZb&PT;XE2|B&W1>=B58#*!~D&) zfVmJGg8UdP*fx(>Cj^?yS^zH#o-$Q-*$SnK(ZVFkw+er=>N^7!)FtP3y~Xxnu^nzY zikgB>Nj0%;WOltWIob|}%lo?_C7<``a5hEkx&1ku$|)i>Rh6@3h*`slY=9U}(Ql_< zaNG*J8vb&@zpdhAvv`?{=zDedJ23TD&Zg__snRAH4eh~^oawdYi6A3w8<Ozh@Kw)#bdktM^GVb zrG08?0bG?|NG+w^&JvD*7LAbjED{_Zkc`3H!My>0u5Q}m!+6VokMLXxl`Mkd=g&Xx z-a>m*#G3SLlhbKB!)tnzfWOBV;u;ftU}S!NdD5+YtOjLg?X}dl>7m^gOpihrf1;PY zvll&>dIuUGs{Qnd- zwIR3oIrct8Va^Tm0t#(bJD7c$Z7DO9*7NnRZorrSm`b`cxz>OIC;jSE3DO8`hX955ui`s%||YQtt2 z5DNA&pG-V+4oI2s*x^>-$6J?p=I>C|9wZF8z;VjR??Icg?1w2v5Me+FgAeGGa8(3S z4vg*$>zC-WIVZtJ7}o9{D-7d>zCe|z#<9>CFve-OPAYsneTb^JH!Enaza#j}^mXy1 z+ULn^10+rWLF6j2>Ya@@Kq?26>AqK{A_| zQKb*~F1>sE*=d?A?W7N2j?L09_7n+HGi{VY;MoTGr_)G9)ot$p!-UY5zZ2Xtbm=t z@dpPSGwgH=QtIcEulQNI>S-#ifbnO5EWkI;$A|pxJd885oM+ zGZ0_0gDvG8q2xebj+fbCHYfAXuZStH2j~|d^sBAzo46(K8n59+T6rzBwK)^rfPT+B zyIFw)9YC-V^rhtK`!3jrhmW-sTmM+tPH+;nwjL#-SjQPUZ53L@A>y*rt(#M(qsiB2 zx6B)dI}6Wlsw%bJ8h|(lhkJVogQZA&n{?Vgs6gNSXzuZpEyu*xySy8ro07QZ7Vk1!3tJphN_5V7qOiyK8p z#@jcDD8nmtYi1^l8ml;AF<#IPK?!pqf9D4moYk>d99Im}Jtwj6c#+A;f)CQ*f-hZ< z=p_T86jog%!p)D&5g9taSwYi&eP z#JuEK%+NULWus;0w32-SYFku#i}d~+{Pkho&^{;RxzP&0!RCm3-9K6`>KZpnzS6?L z^H^V*s!8<>x8bomvD%rh>Zp3>Db%kyin;qtl+jAv8Oo~1g~mqGAC&Qi_wy|xEt2iz zWAJEfTV%cl2Cs<1L&DLRVVH05EDq`pH7Oh7sR`NNkL%wi}8n>IXcO40hp+J+sC!W?!krJf!GJNE8uj zg-y~Ns-<~D?yqbzVRB}G>0A^f0!^N7l=$m0OdZuqAOQqLc zX?AEGr1Ht+inZ-Qiwnl@Z0qukd__a!C*CKuGdy5#nD7VUBM^6OCpxCa2A(X;e0&V4 zM&WR8+wErQ7UIc6LY~Q9x%Sn*Tn>>P`^t&idaOEnOd(Ufw#>NoR^1QdhJ8s`h^|R_ zXX`c5*O~Xdvh%q;7L!_!ohf$NfEBmCde|#uVZvEo>OfEq%+Ns7&_f$OR9xsihRpBb z+cjk8LyDm@U{YN>+r46?nn{7Gh(;WhFw6GAxtcKD+YWV?uge>;+q#Xx4!GpRkVZYu zzsF}1)7$?%s9g9CH=Zs+B%M_)+~*j3L0&Q9u7!|+T`^O{xE6qvAP?XWv9_MrZKdo& z%IyU)$Q95AB4!#hT!_dA>4e@zjOBD*Y=XjtMm)V|+IXzjuM;(l+8aA5#Kaz_$rR6! zj>#&^DidYD$nUY(D$mH`9eb|dtV0b{S>H6FBfq>t5`;OxA4Nn{J(+XihF(stSche7$es&~N$epi&PDM_N`As;*9D^L==2Q7Z2zD+CiU(|+-kL*VG+&9!Yb3LgPy?A zm7Z&^qRG_JIxK7-FBzZI3Q<;{`DIxtc48k> zc|0dmX;Z=W$+)qE)~`yn6MdoJ4co;%!`ddy+FV538Y)j(vg}5*k(WK)KWZ3WaOG!8 z!syGn=s{H$odtpqFrT#JGM*utN7B((abXnpDM6w56nhw}OY}0TiTG1#f*VFZr+^-g zbP10`$LPq_;PvrA1XXlyx2uM^mrjTzX}w{yuLo-cOClE8MMk47T25G8M!9Z5ypOSV zAJUBGEg5L2fY)ZGJb^E34R2zJ?}Vf>{~gB!8=5Z) z9y$>5c)=;o0HeHHSuE4U)#vG&KF|I%-cF6f$~pdYJWk_dD}iOA>iA$O$+4%@>JU08 zS`ep)$XLPJ+n0_i@PkF#ri6T8?ZeAot$6JIYHm&P6EB=BiaNY|aA$W0I+nz*zkz_z zkEru!tj!QUffq%)8y0y`T&`fuus-1p>=^hnBiBqD^hXrPs`PY9tU3m0np~rISY09> z`P3s=-kt_cYcxWd{de@}TwSqg*xVhp;E9zCsnXo6z z?f&Sv^U7n4`xr=mXle94HzOdN!2kB~4=%)u&N!+2;z6UYKUDqi-s6AZ!haB;@&B`? z_TRX0%@suz^TRdCb?!vNJYPY8L_}&07uySH9%W^Tc&1pia6y1q#?*Drf}GjGbPjBS zbOPcUY#*$3sL2x4v_i*Y=N7E$mR}J%|GUI(>WEr+28+V z%v5{#e!UF*6~G&%;l*q*$V?&r$Pp^sE^i-0$+RH3ERUUdQ0>rAq2(2QAbG}$y{de( z>{qD~GGuOk559Y@%$?N^1ApVL_a704>8OD%8Y%8B;FCt%AoPu8*D1 zLB5X>b}Syz81pn;xnB}%0FnwazlWfUV)Z-~rZg6~b z6!9J$EcE&sEbzcy?CI~=boWA&eeIa%z(7SE^qgVLz??1Vbc1*aRvc%Mri)AJaAG!p z$X!_9Ds;Zz)f+;%s&dRcJt2==P{^j3bf0M=nJd&xwUGlUFn?H=2W(*2I2Gdu zv!gYCwM10aeus)`RIZSrCK=&oKaO_Ry~D1B5!y0R=%!i2*KfXGYX&gNv_u+n9wiR5 z*e$Zjju&ODRW3phN925%S(jL+bCHv6rZtc?!*`1TyYXT6%Ju=|X;6D@lq$8T zW{Y|e39ioPez(pBH%k)HzFITXHvnD6hw^lIoUMA;qAJ^CU?top1fo@s7xT13Fvn1H z6JWa-6+FJF#x>~+A;D~;VDs26>^oH0EI`IYT2iagy23?nyJ==i{g4%HrAf1-*v zK1)~@&(KkwR7TL}L(A@C_S0G;-GMDy=MJn2$FP5s<%wC)4jC5PXoxrQBFZ_k0P{{s@sz+gX`-!=T8rcB(=7vW}^K6oLWMmp(rwDh}b zwaGGd>yEy6fHv%jM$yJXo5oMAQ>c9j`**}F?MCry;T@47@r?&sKHgVe$MCqk#Z_3S z1GZI~nOEN*P~+UaFGnj{{Jo@16`(qVNtbU>O0Hf57-P>x8Jikp=`s8xWs^dAJ9lCQ z)GFm+=OV%AMVqVATtN@|vp61VVAHRn87}%PC^RAzJ%JngmZTasWBAWsoAqBU+8L8u z4A&Pe?fmTm0?mK-BL9t+{y7o(7jm+RpOhL9KnY#E&qu^}B6=K_dB}*VlSEiC9fn)+V=J;OnN)Ta5v66ic1rG+dGAJ1 z1%Zb_+!$=tQ~lxQrzv3x#CPb?CekEkA}0MYSgx$Jdd}q8+R=ma$|&1a#)TQ=l$1tQ z=tL9&_^vJ)Pk}EDO-va`UCT1m#Uty1{v^A3P~83_#v^ozH}6*9mIjIr;t3Uv%@VeW zGL6(CwCUp)Jq%G0bIG%?{_*Y#5IHf*5M@wPo6A{$Um++Co$wLC=J1aoG93&T7Ho}P z=mGEPP7GbvoG!uD$k(H3A$Z))+i{Hy?QHdk>3xSBXR0j!11O^mEe9RHmw!pvzv?Ua~2_l2Yh~_!s1qS`|0~0)YsbHSz8!mG)WiJE| z2f($6TQtt6L_f~ApQYQKSb=`053LgrQq7G@98#igV>y#i==-nEjQ!XNu9 z~;mE+gtj4IDDNQJ~JVk5Ux6&LCSFL!y=>79kE9=V}J7tD==Ga+IW zX)r7>VZ9dY=V&}DR))xUoV!u(Z|%3ciQi_2jl}3=$Agc(`RPb z8kEBpvY>1FGQ9W$n>Cq=DIpski};nE)`p3IUw1Oz0|wxll^)4dq3;CCY@RyJgFgc# zKouFh!`?Xuo{IMz^xi-h=StCis_M7yq$u) z?XHvw*HP0VgR+KR6wI)jEMX|ssqYvSf*_3W8zVTQzD?3>H!#>InzpSO)@SC8q*ii- z%%h}_#0{4JG;Jm`4zg};BPTGkYamx$Xo#O~lBirRY)q=5M45n{GCfV7h9qwyu1NxOMoP4)jjZMxmT|IQQh0U7C$EbnMN<3)Kk?fFHYq$d|ICu>KbY_hO zTZM+uKHe(cIZfEqyzyYSUBZa8;Fcut-GN!HSA9ius`ltNebF46ZX_BbZNU}}ZOm{M2&nANL9@0qvih15(|`S~z}m&h!u4x~(%MAO$jHRWNfuxWF#B)E&g3ghSQ9|> z(MFaLQj)NE0lowyjvg8z0#m6FIuKE9lDO~Glg}nSb7`~^&#(Lw{}GVOS>U)m8bF}x zVjbXljBm34Cs-yM6TVusr+3kYFjr28STT3g056y3cH5Tmge~ASxBj z%|yb>$eF;WgrcOZf569sDZOVwoo%8>XO>XQOX1OyN9I-SQgrm;U;+#3OI(zrWyow3 zk==|{lt2xrQ%FIXOTejR>;wv(Pb8u8}BUpx?yd(Abh6? zsoO3VYWkeLnF43&@*#MQ9-i-d0t*xN-UEyNKeyNMHw|A(k(_6QKO=nKMCxD(W(Yop zsRQ)QeL4X3Lxp^L%wzi2-WVSsf61dqliPUM7srDB?Wm6Lzn0&{*}|IsKQW;02(Y&| zaTKv|`U(pSzuvR6Rduu$wzK_W-Y-7>7s?G$)U}&uK;<>vU}^^ns@Z!p+9?St1s)dG zK%y6xkPyyS1$~&6v{kl?Md6gwM|>mt6Upm>oa8RLD^8T{0?HC!Z>;(Bob7el(DV6x zi`I)$&E&ngwFS@bi4^xFLAn`=fzTC;aimE^!cMI2n@Vo%Ae-ne`RF((&5y6xsjjAZ zVguVoQ?Z9uk$2ON;ersE%PU*xGO@T*;j1BO5#TuZKEf(mB7|g7pcEA=nYJ{s3vlbg zd4-DUlD{*6o%Gc^N!Nptgay>j6E5;3psI+C3Q!1ZIbeCubW%w4pq9)MSDyB{HLm|k zxv-{$$A*pS@csolri$Ge<4VZ}e~78JOL-EVyrbxKra^d{?|NnPp86!q>t<&IP07?Z z^>~IK^k#OEKgRH+LjllZXk7iA>2cfH6+(e&9ku5poo~6y{GC5>(bRK7hwjiurqAiZ zg*DmtgY}v83IjE&AbiWgMyFbaRUPZ{lYiz$U^&Zt2YjG<%m((&_JUbZcfJ22(>bi5 z!J?<7AySj0JZ&<-qXX;mcV!f~>G=sB0KnjWca4}vrtunD^1TrpfeS^4dvFr!65knK zZh`d;*VOkPs4*-9kL>$GP0`(M!j~B;#x?Ba~&s6CopvO86oM?-? zOw#dIRc;6A6T?B`Qp%^<U5 z19x(ywSH$_N+Io!6;e?`tWaM$`=Db!gzx|lQ${DG!zb1Zl&|{kX0y6xvO1o z220r<-oaS^^R2pEyY;=Qllqpmue|5yI~D|iI!IGt@iod{Opz@*ml^w2bNs)p`M(Io z|E;;m*Xpjd9l)4G#KaWfV(t8YUn@A;nK^#xgv=LtnArX|vWQVuw3}B${h+frU2>9^ z!l6)!Uo4`5k`<<;E(ido7M6lKTgWezNLq>U*=uz&s=cc$1%>VrAeOoUtA|T6gO4>UNqsdK=NF*8|~*sl&wI=x9-EGiq*aqV!(VVXA57 zw9*o6Ir8Lj1npUXvlevtn(_+^X5rzdR>#(}4YcB9O50q97%rW2me5_L=%ffYPUSRc z!vv?Kv>dH994Qi>U(a<0KF6NH5b16enCp+mw^Hb3Xs1^tThFpz!3QuN#}KBbww`(h z7GO)1olDqy6?T$()R7y%NYx*B0k_2IBiZ14&8|JPFxeMF{vW>HF-Vi3+ZOI=+qP}n zw(+!WcTd~4ZJX1!ZM&y!+uyt=&i!+~d(V%GjH;-NsEEv6nS1TERt|RHh!0>W4+4pp z1-*EzAM~i`+1f(VEHI8So`S`akPfPTfq*`l{Fz`hS%k#JS0cjT2mS0#QLGf=J?1`he3W*;m4)ce8*WFq1sdP=~$5RlH1EdWm|~dCvKOi4*I_96{^95p#B<(n!d?B z=o`0{t+&OMwKcxiBECznJcfH!fL(z3OvmxP#oWd48|mMjpE||zdiTBdWelj8&Qosv zZFp@&UgXuvJw5y=q6*28AtxZzo-UUpkRW%ne+Ylf!V-0+uQXBW=5S1o#6LXNtY5!I z%Rkz#(S8Pjz*P7bqB6L|M#Er{|QLae-Y{KA>`^} z@lPjeX>90X|34S-7}ZVXe{wEei1<{*e8T-Nbj8JmD4iwcE+Hg_zhkPVm#=@b$;)h6 z<<6y`nPa`f3I6`!28d@kdM{uJOgM%`EvlQ5B2bL)Sl=|y@YB3KeOzz=9cUW3clPAU z^sYc}xf9{4Oj?L5MOlYxR{+>w=vJjvbyO5}ptT(o6dR|ygO$)nVCvNGnq(6;bHlBd zl?w-|plD8spjDF03g5ip;W3Z z><0{BCq!Dw;h5~#1BuQilq*TwEu)qy50@+BE4bX28+7erX{BD4H)N+7U`AVEuREE8 z;X?~fyhF-x_sRfHIj~6f(+^@H)D=ngP;mwJjxhQUbUdzk8f94Ab%59-eRIq?ZKrwD z(BFI=)xrUlgu(b|hAysqK<}8bslmNNeD=#JW*}^~Nrswn^xw*nL@Tx!49bfJecV&KC2G4q5a!NSv)06A_5N3Y?veAz;Gv+@U3R% z)~UA8-0LvVE{}8LVDOHzp~2twReqf}ODIyXMM6=W>kL|OHcx9P%+aJGYi_Om)b!xe zF40Vntn0+VP>o<$AtP&JANjXBn7$}C@{+@3I@cqlwR2MdwGhVPxlTIcRVu@Ho-wO` z_~Or~IMG)A_`6-p)KPS@cT9mu9RGA>dVh5wY$NM9-^c@N=hcNaw4ITjm;iWSP^ZX| z)_XpaI61<+La+U&&%2a z0za$)-wZP@mwSELo#3!PGTt$uy0C(nTT@9NX*r3Ctw6J~7A(m#8fE)0RBd`TdKfAT zCf@$MAxjP`O(u9s@c0Fd@|}UQ6qp)O5Q5DPCeE6mSIh|Rj{$cAVIWsA=xPKVKxdhg zLzPZ`3CS+KIO;T}0Ip!fAUaNU>++ZJZRk@I(h<)RsJUhZ&Ru9*!4Ptn;gX^~4E8W^TSR&~3BAZc#HquXn)OW|TJ`CTahk+{qe`5+ixON^zA9IFd8)kc%*!AiLu z>`SFoZ5bW-%7}xZ>gpJcx_hpF$2l+533{gW{a7ce^B9sIdmLrI0)4yivZ^(Vh@-1q zFT!NQK$Iz^xu%|EOK=n>ug;(7J4OnS$;yWmq>A;hsD_0oAbLYhW^1Vdt9>;(JIYjf zdb+&f&D4@4AS?!*XpH>8egQvSVX`36jMd>$+RgI|pEg))^djhGSo&#lhS~9%NuWfX zDDH;3T*GzRT@5=7ibO>N-6_XPBYxno@mD_3I#rDD?iADxX`! zh*v8^i*JEMzyN#bGEBz7;UYXki*Xr(9xXax(_1qVW=Ml)kSuvK$coq2A(5ZGhs_pF z$*w}FbN6+QDseuB9=fdp_MTs)nQf!2SlROQ!gBJBCXD&@-VurqHj0wm@LWX-TDmS= z71M__vAok|@!qgi#H&H%Vg-((ZfxPAL8AI{x|VV!9)ZE}_l>iWk8UPTGHs*?u7RfP z5MC&=c6X;XlUzrz5q?(!eO@~* zoh2I*%J7dF!!_!vXoSIn5o|wj1#_>K*&CIn{qSaRc&iFVxt*^20ngCL;QonIS>I5^ zMw8HXm>W0PGd*}Ko)f|~dDd%;Wu_RWI_d;&2g6R3S63Uzjd7dn%Svu-OKpx*o|N>F zZg=-~qLb~VRLpv`k zWSdfHh@?dp=s_X`{yxOlxE$4iuyS;Z-x!*E6eqmEm*j2bE@=ZI0YZ5%Yj29!5+J$4h{s($nakA`xgbO8w zi=*r}PWz#lTL_DSAu1?f%-2OjD}NHXp4pXOsCW;DS@BC3h-q4_l`<))8WgzkdXg3! zs1WMt32kS2E#L0p_|x+x**TFV=gn`m9BWlzF{b%6j-odf4{7a4y4Uaef@YaeuPhU8 zHBvRqN^;$Jizy+ z=zW{E5<>2gp$pH{M@S*!sJVQU)b*J5*bX4h>5VJve#Q6ga}cQ&iL#=(u+KroWrxa%8&~p{WEUF0il=db;-$=A;&9M{Rq`ouZ5m%BHT6%st%saGsD6)fQgLN}x@d3q>FC;=f%O3Cyg=Ke@Gh`XW za@RajqOE9UB6eE=zhG%|dYS)IW)&y&Id2n7r)6p_)vlRP7NJL(x4UbhlcFXWT8?K=%s7;z?Vjts?y2+r|uk8Wt(DM*73^W%pAkZa1Jd zNoE)8FvQA>Z`eR5Z@Ig6kS5?0h;`Y&OL2D&xnnAUzQz{YSdh0k zB3exx%A2TyI)M*EM6htrxSlep!Kk(P(VP`$p0G~f$smld6W1r_Z+o?=IB@^weq>5VYsYZZR@` z&XJFxd5{|KPZmVOSxc@^%71C@;z}}WhbF9p!%yLj3j%YOlPL5s>7I3vj25 z@xmf=*z%Wb4;Va6SDk9cv|r*lhZ`(y_*M@>q;wrn)oQx%B(2A$9(74>;$zmQ!4fN; z>XurIk-7@wZys<+7XL@0Fhe-f%*=(weaQEdR9Eh6>Kl-EcI({qoZqyzziGwpg-GM#251sK_ z=3|kitS!j%;fpc@oWn65SEL73^N&t>Ix37xgs= zYG%eQDJc|rqHFia0!_sm7`@lvcv)gfy(+KXA@E{3t1DaZ$DijWAcA)E0@X?2ziJ{v z&KOYZ|DdkM{}t+@{@*6ge}m%xfjIxi%qh`=^2Rwz@w0cCvZ&Tc#UmCDbVwABrON^x zEBK43FO@weA8s7zggCOWhMvGGE`baZ62cC)VHyy!5Zbt%ieH+XN|OLbAFPZWyC6)p z4P3%8sq9HdS3=ih^0OOlqTPbKuzQ?lBEI{w^ReUO{V?@`ARsL|S*%yOS=Z%sF)>-y z(LAQdhgAcuF6LQjRYfdbD1g4o%tV4EiK&ElLB&^VZHbrV1K>tHTO{#XTo>)2UMm`2 z^t4s;vnMQgf-njU-RVBRw0P0-m#d-u`(kq7NL&2T)TjI_@iKuPAK-@oH(J8?%(e!0Ir$yG32@CGUPn5w4)+9@8c&pGx z+K3GKESI4*`tYlmMHt@br;jBWTei&(a=iYslc^c#RU3Q&sYp zSG){)V<(g7+8W!Wxeb5zJb4XE{I|&Y4UrFWr%LHkdQ;~XU zgy^dH-Z3lmY+0G~?DrC_S4@=>0oM8Isw%g(id10gWkoz2Q%7W$bFk@mIzTCcIB(K8 zc<5h&ZzCdT=9n-D>&a8vl+=ZF*`uTvQviG_bLde*k>{^)&0o*b05x$MO3gVLUx`xZ z43j+>!u?XV)Yp@MmG%Y`+COH2?nQcMrQ%k~6#O%PeD_WvFO~Kct za4XoCM_X!c5vhRkIdV=xUB3xI2NNStK*8_Zl!cFjOvp-AY=D;5{uXj}GV{LK1~IE2 z|KffUiBaStRr;10R~K2VVtf{TzM7FaPm;Y(zQjILn+tIPSrJh&EMf6evaBKIvi42-WYU9Vhj~3< zZSM-B;E`g_o8_XTM9IzEL=9Lb^SPhe(f(-`Yh=X6O7+6ALXnTcUFpI>ekl6v)ZQeNCg2 z^H|{SKXHU*%nBQ@I3It0m^h+6tvI@FS=MYS$ZpBaG7j#V@P2ZuYySbp@hA# ze(kc;P4i_-_UDP?%<6>%tTRih6VBgScKU^BV6Aoeg6Uh(W^#J^V$Xo^4#Ekp ztqQVK^g9gKMTHvV7nb64UU7p~!B?>Y0oFH5T7#BSW#YfSB@5PtE~#SCCg3p^o=NkMk$<8- z6PT*yIKGrvne7+y3}_!AC8NNeI?iTY(&nakN>>U-zT0wzZf-RuyZk^X9H-DT_*wk= z;&0}6LsGtfVa1q)CEUPlx#(ED@-?H<1_FrHU#z5^P3lEB|qsxEyn%FOpjx z3S?~gvoXy~L(Q{Jh6*i~=f%9kM1>RGjBzQh_SaIDfSU_9!<>*Pm>l)cJD@wlyxpBV z4Fmhc2q=R_wHCEK69<*wG%}mgD1=FHi4h!98B-*vMu4ZGW~%IrYSLGU{^TuseqVgV zLP<%wirIL`VLyJv9XG_p8w@Q4HzNt-o;U@Au{7%Ji;53!7V8Rv0^Lu^Vf*sL>R(;c zQG_ZuFl)Mh-xEIkGu}?_(HwkB2jS;HdPLSxVU&Jxy9*XRG~^HY(f0g8Q}iqnVmgjI zfd=``2&8GsycjR?M%(zMjn;tn9agcq;&rR!Hp z$B*gzHsQ~aXw8c|a(L^LW(|`yGc!qOnV(ZjU_Q-4z1&0;jG&vAKuNG=F|H?@m5^N@ zq{E!1n;)kNTJ>|Hb2ODt-7U~-MOIFo%9I)_@7fnX+eMMNh>)V$IXesJpBn|uo8f~#aOFytCT zf9&%MCLf8mp4kwHTcojWmM3LU=#|{3L>E}SKwOd?%{HogCZ_Z1BSA}P#O(%H$;z7XyJ^sjGX;j5 zrzp>|Ud;*&VAU3x#f{CKwY7Vc{%TKKqmB@oTHA9;>?!nvMA;8+Jh=cambHz#J18x~ zs!dF>$*AnsQ{{82r5Aw&^7eRCdvcgyxH?*DV5(I$qXh^zS>us*I66_MbL8y4d3ULj z{S(ipo+T3Ag!+5`NU2sc+@*m{_X|&p#O-SAqF&g_n7ObB82~$p%fXA5GLHMC+#qqL zdt`sJC&6C2)=juQ_!NeD>U8lDVpAOkW*khf7MCcs$A(wiIl#B9HM%~GtQ^}yBPjT@ z+E=|A!Z?A(rwzZ;T}o6pOVqHzTr*i;Wrc%&36kc@jXq~+w8kVrs;%=IFdACoLAcCAmhFNpbP8;s`zG|HC2Gv?I~w4ITy=g$`0qMQdkijLSOtX6xW%Z9Nw<;M- zMN`c7=$QxN00DiSjbVt9Mi6-pjv*j(_8PyV-il8Q-&TwBwH1gz1uoxs6~uU}PrgWB zIAE_I-a1EqlIaGQNbcp@iI8W1sm9fBBNOk(k&iLBe%MCo#?xI$%ZmGA?=)M9D=0t7 zc)Q0LnI)kCy{`jCGy9lYX%mUsDWwsY`;jE(;Us@gmWPqjmXL+Hu#^;k%eT>{nMtzj zsV`Iy6leTA8-PndszF;N^X@CJrTw5IIm!GPeu)H2#FQitR{1p;MasQVAG3*+=9FYK zw*k!HT(YQorfQj+1*mCV458(T5=fH`um$gS38hw(OqVMyunQ;rW5aPbF##A3fGH6h z@W)i9Uff?qz`YbK4c}JzQpuxuE3pcQO)%xBRZp{zJ^-*|oryTxJ-rR+MXJ)!f=+pp z10H|DdGd2exhi+hftcYbM0_}C0ZI-2vh+$fU1acsB-YXid7O|=9L!3e@$H*6?G*Zp z%qFB(sgl=FcC=E4CYGp4CN>=M8#5r!RU!u+FJVlH6=gI5xHVD&k;Ta*M28BsxfMV~ zLz+@6TxnfLhF@5=yQo^1&S}cmTN@m!7*c6z;}~*!hNBjuE>NLVl2EwN!F+)0$R1S! zR|lF%n!9fkZ@gPW|x|B={V6x3`=jS*$Pu0+5OWf?wnIy>Y1MbbGSncpKO0qE(qO=ts z!~@&!N`10S593pVQu4FzpOh!tvg}p%zCU(aV5=~K#bKi zHdJ1>tQSrhW%KOky;iW+O_n;`l9~omqM%sdxdLtI`TrJzN6BQz+7xOl*rM>xVI2~# z)7FJ^Dc{DC<%~VS?@WXzuOG$YPLC;>#vUJ^MmtbSL`_yXtNKa$Hk+l-c!aC7gn(Cg ze?YPYZ(2Jw{SF6MiO5(%_pTo7j@&DHNW`|lD`~{iH+_eSTS&OC*2WTT*a`?|9w1dh zh1nh@$a}T#WE5$7Od~NvSEU)T(W$p$s5fe^GpG+7fdJ9=enRT9$wEk+ZaB>G3$KQO zgq?-rZZnIv!p#>Ty~}c*Lb_jxJg$eGM*XwHUwuQ|o^}b3^T6Bxx{!?va8aC@-xK*H ztJBFvFfsSWu89%@b^l3-B~O!CXs)I6Y}y#0C0U0R0WG zybjroj$io0j}3%P7zADXOwHwafT#uu*zfM!oD$6aJx7+WL%t-@6^rD_a_M?S^>c;z zMK580bZXo1f*L$CuMeM4Mp!;P@}b~$cd(s5*q~FP+NHSq;nw3fbWyH)i2)-;gQl{S zZO!T}A}fC}vUdskGSq&{`oxt~0i?0xhr6I47_tBc`fqaSrMOzR4>0H^;A zF)hX1nfHs)%Zb-(YGX;=#2R6C{BG;k=?FfP?9{_uFLri~-~AJ;jw({4MU7e*d)?P@ zXX*GkNY9ItFjhwgAIWq7Y!ksbMzfqpG)IrqKx9q{zu%Mdl+{Dis#p9q`02pr1LG8R z@As?eG!>IoROgS!@J*to<27coFc1zpkh?w=)h9CbYe%^Q!Ui46Y*HO0mr% zEff-*$ndMNw}H2a5@BsGj5oFfd!T(F&0$<{GO!Qdd?McKkorh=5{EIjDTHU`So>8V zBA-fqVLb2;u7UhDV1xMI?y>fe3~4urv3%PX)lDw+HYa;HFkaLqi4c~VtCm&Ca+9C~ zge+67hp#R9`+Euq59WhHX&7~RlXn=--m8$iZ~~1C8cv^2(qO#X0?vl91gzUKBeR1J z^p4!!&7)3#@@X&2aF2-)1Ffcc^F8r|RtdL2X%HgN&XU-KH2SLCbpw?J5xJ*!F-ypZ zMG%AJ!Pr&}`LW?E!K~=(NJxuSVTRCGJ$2a*Ao=uUDSys!OFYu!Vs2IT;xQ6EubLIl z+?+nMGeQQhh~??0!s4iQ#gm3!BpMpnY?04kK375e((Uc7B3RMj;wE?BCoQGu=UlZt!EZ1Q*auI)dj3Jj{Ujgt zW5hd~-HWBLI_3HuO) zNrb^XzPsTIb=*a69wAAA3J6AAZZ1VsYbIG}a`=d6?PjM)3EPaDpW2YP$|GrBX{q*! z$KBHNif)OKMBCFP5>!1d=DK>8u+Upm-{hj5o|Wn$vh1&K!lVfDB&47lw$tJ?d5|=B z^(_9=(1T3Fte)z^>|3**n}mIX;mMN5v2F#l(q*CvU{Ga`@VMp#%rQkDBy7kYbmb-q z<5!4iuB#Q_lLZ8}h|hPODI^U6`gzLJre9u3k3c#%86IKI*^H-@I48Bi*@avYm4v!n0+v zWu{M{&F8#p9cx+gF0yTB_<2QUrjMPo9*7^-uP#~gGW~y3nfPAoV%amgr>PSyVAd@l)}8#X zR5zV6t*uKJZL}?NYvPVK6J0v4iVpwiN|>+t3aYiZSp;m0!(1`bHO}TEtWR1tY%BPB z(W!0DmXbZAsT$iC13p4f>u*ZAy@JoLAkJhzFf1#4;#1deO8#8d&89}en&z!W&A3++^1(;>0SB1*54d@y&9Pn;^IAf3GiXbfT`_>{R+Xv; zQvgL>+0#8-laO!j#-WB~(I>l0NCMt_;@Gp_f0#^c)t?&#Xh1-7RR0@zPyBz!U#0Av zT?}n({(p?p7!4S2ZBw)#KdCG)uPnZe+U|0{BW!m)9 zi_9$F?m<`2!`JNFv+w8MK_K)qJ^aO@7-Ig>cM4-r0bi=>?B_2mFNJ}aE3<+QCzRr*NA!QjHw# z`1OsvcoD0?%jq{*7b!l|L1+Tw0TTAM4XMq7*ntc-Ived>Sj_ZtS|uVdpfg1_I9knY z2{GM_j5sDC7(W&}#s{jqbybqJWyn?{PW*&cQIU|*v8YGOKKlGl@?c#TCnmnAkAzV- zmK={|1G90zz=YUvC}+fMqts0d4vgA%t6Jhjv?d;(Z}(Ep8fTZfHA9``fdUHkA+z3+ zhh{ohP%Bj?T~{i0sYCQ}uC#5BwN`skI7`|c%kqkyWIQ;!ysvA8H`b-t()n6>GJj6xlYDu~8qX{AFo$Cm3d|XFL=4uvc?Keb zzb0ZmMoXca6Mob>JqkNuoP>B2Z>D`Q(TvrG6m`j}-1rGP!g|qoL=$FVQYxJQjFn33lODt3Wb1j8VR zlR++vIT6^DtYxAv_hxupbLLN3e0%A%a+hWTKDV3!Fjr^cWJ{scsAdfhpI)`Bms^M6 zQG$waKgFr=c|p9Piug=fcJvZ1ThMnNhQvBAg-8~b1?6wL*WyqXhtj^g(Ke}mEfZVM zJuLNTUVh#WsE*a6uqiz`b#9ZYg3+2%=C(6AvZGc=u&<6??!slB1a9K)=VL zY9EL^mfyKnD zSJyYBc_>G;5RRnrNgzJz#Rkn3S1`mZgO`(r5;Hw6MveN(URf_XS-r58Cn80K)ArH4 z#Rrd~LG1W&@ttw85cjp8xV&>$b%nSXH_*W}7Ch2pg$$c0BdEo-HWRTZcxngIBJad> z;C>b{jIXjb_9Jis?NZJsdm^EG}e*pR&DAy0EaSGi3XWTa(>C%tz1n$u?5Fb z1qtl?;_yjYo)(gB^iQq?=jusF%kywm?CJP~zEHi0NbZ);$(H$w(Hy@{i>$wcVRD_X|w-~(0Z9BJyh zhNh;+eQ9BEIs;tPz%jSVnfCP!3L&9YtEP;svoj_bNzeGSQIAjd zBss@A;)R^WAu-37RQrM%{DfBNRx>v!G31Z}8-El9IOJlb_MSoMu2}GDYycNaf>uny z+8xykD-7ONCM!APry_Lw6-yT>5!tR}W;W`C)1>pxSs5o1z#j7%m=&=7O4hz+Lsqm` z*>{+xsabZPr&X=}G@obTb{nPTkccJX8w3CG7X+1+t{JcMabv~UNv+G?txRqXib~c^Mo}`q{$`;EBNJ;#F*{gvS12kV?AZ%O0SFB$^ zn+}!HbmEj}w{Vq(G)OGAzH}R~kS^;(-s&=ectz8vN!_)Yl$$U@HNTI-pV`LSj7Opu zTZ5zZ)-S_{GcEQPIQXLQ#oMS`HPu{`SQiAZ)m1at*Hy%3xma|>o`h%E%8BEbi9p0r zVjcsh<{NBKQ4eKlXU|}@XJ#@uQw*$4BxKn6#W~I4T<^f99~(=}a`&3(ur8R9t+|AQ zWkQx7l}wa48-jO@ft2h+7qn%SJtL%~890FG0s5g*kNbL3I&@brh&f6)TlM`K^(bhr zJWM6N6x3flOw$@|C@kPi7yP&SP?bzP-E|HSXQXG>7gk|R9BTj`e=4de9C6+H7H7n# z#GJeVs1mtHhLDmVO?LkYRQc`DVOJ_vdl8VUihO-j#t=0T3%Fc1f9F73ufJz*adn*p zc%&vi(4NqHu^R>sAT_0EDjVR8bc%wTz#$;%NU-kbDyL_dg0%TFafZwZ?5KZpcuaO54Z9hX zD$u>q!-9`U6-D`E#`W~fIfiIF5_m6{fvM)b1NG3xf4Auw;Go~Fu7cth#DlUn{@~yu z=B;RT*dp?bO}o%4x7k9v{r=Y@^YQ^UUm(Qmliw8brO^=NP+UOohLYiaEB3^DB56&V zK?4jV61B|1Uj_5fBKW;8LdwOFZKWp)g{B%7g1~DgO&N& z#lisxf?R~Z@?3E$Mms$$JK8oe@X`5m98V*aV6Ua}8Xs2#A!{x?IP|N(%nxsH?^c{& z@vY&R1QmQs83BW28qAmJfS7MYi=h(YK??@EhjL-t*5W!p z^gYX!Q6-vBqcv~ruw@oMaU&qp0Fb(dbVzm5xJN%0o_^@fWq$oa3X?9s%+b)x4w-q5Koe(@j6Ez7V@~NRFvd zfBH~)U5!ix3isg`6be__wBJp=1@yfsCMw1C@y+9WYD9_C%{Q~7^0AF2KFryfLlUP# zwrtJEcH)jm48!6tUcxiurAMaiD04C&tPe6DI0#aoqz#Bt0_7_*X*TsF7u*zv(iEfA z;$@?XVu~oX#1YXtceQL{dSneL&*nDug^OW$DSLF0M1Im|sSX8R26&)<0Fbh^*l6!5wfSu8MpMoh=2l z^^0Sr$UpZp*9oqa23fcCfm7`ya2<4wzJ`Axt7e4jJrRFVf?nY~2&tRL* zd;6_njcz01c>$IvN=?K}9ie%Z(BO@JG2J}fT#BJQ+f5LFSgup7i!xWRKw6)iITjZU z%l6hPZia>R!`aZjwCp}I zg)%20;}f+&@t;(%5;RHL>K_&7MH^S+7<|(SZH!u zznW|jz$uA`P9@ZWtJgv$EFp>)K&Gt+4C6#*khZQXS*S~6N%JDT$r`aJDs9|uXWdbg zBwho$phWx}x!qy8&}6y5Vr$G{yGSE*r$^r{}pw zVTZKvikRZ`J_IJrjc=X1uw?estdwm&bEahku&D04HD+0Bm~q#YGS6gp!KLf$A{%Qd z&&yX@Hp>~(wU{|(#U&Bf92+1i&Q*-S+=y=3pSZy$#8Uc$#7oiJUuO{cE6=tsPhwPe| zxQpK>`Dbka`V)$}e6_OXKLB%i76~4N*zA?X+PrhH<&)}prET;kel24kW%+9))G^JI zsq7L{P}^#QsZViX%KgxBvEugr>ZmFqe^oAg?{EI=&_O#e)F3V#rc z8$4}0Zr19qd3tE4#$3_f=Bbx9oV6VO!d3(R===i-7p=Vj`520w0D3W6lQfY48}!D* z&)lZMG;~er2qBoI2gsX+Ts-hnpS~NYRDtPd^FPzn!^&yxRy#CSz(b&E*tL|jIkq|l zf%>)7Dtu>jCf`-7R#*GhGn4FkYf;B$+9IxmqH|lf6$4irg{0ept__%)V*R_OK=T06 zyT_m-o@Kp6U{l5h>W1hGq*X#8*y@<;vsOFqEjTQXFEotR+{3}ODDnj;o0@!bB5x=N z394FojuGOtVKBlVRLtHp%EJv_G5q=AgF)SKyRN5=cGBjDWv4LDn$IL`*=~J7u&Dy5 zrMc83y+w^F&{?X(KOOAl-sWZDb{9X9#jrQtmrEXD?;h-}SYT7yM(X_6qksM=K_a;Z z3u0qT0TtaNvDER_8x*rxXw&C^|h{P1qxK|@pS7vdlZ#P z7PdB7MmC2}%sdzAxt>;WM1s0??`1983O4nFK|hVAbHcZ3x{PzytQLkCVk7hA!Lo` zEJH?4qw|}WH{dc4z%aB=0XqsFW?^p=X}4xnCJXK%c#ItOSjdSO`UXJyuc8bh^Cf}8 z@Ht|vXd^6{Fgai8*tmyRGmD_s_nv~r^Fy7j`Bu`6=G)5H$i7Q7lvQnmea&TGvJp9a|qOrUymZ$6G|Ly z#zOCg++$3iB$!6!>215A4!iryregKuUT344X)jQb3|9qY>c0LO{6Vby05n~VFzd?q zgGZv&FGlkiH*`fTurp>B8v&nSxNz)=5IF$=@rgND4d`!AaaX;_lK~)-U8la_Wa8i?NJC@BURO*sUW)E9oyv3RG^YGfN%BmxzjlT)bp*$<| zX3tt?EAy<&K+bhIuMs-g#=d1}N_?isY)6Ay$mDOKRh z4v1asEGWoAp=srraLW^h&_Uw|6O+r;wns=uwYm=JN4Q!quD8SQRSeEcGh|Eb5Jg8m zOT}u;N|x@aq)=&;wufCc^#)5U^VcZw;d_wwaoh9$p@Xrc{DD6GZUqZ ziC6OT^zSq@-lhbgR8B+e;7_Giv;DK5gn^$bs<6~SUadiosfewWDJu`XsBfOd1|p=q zE>m=zF}!lObA%ePey~gqU8S6h-^J2Y?>7)L2+%8kV}Gp=h`Xm_}rlm)SyUS=`=S7msKu zC|T!gPiI1rWGb1z$Md?0YJQ;%>uPLOXf1Z>N~`~JHJ!^@D5kSXQ4ugnFZ>^`zH8CAiZmp z6Ms|#2gcGsQ{{u7+Nb9sA?U>(0e$5V1|WVwY`Kn)rsnnZ4=1u=7u!4WexZD^IQ1Jk zfF#NLe>W$3m&C^ULjdw+5|)-BSHwpegdyt9NYC{3@QtMfd8GrIWDu`gd0nv-3LpGCh@wgBaG z176tikL!_NXM+Bv#7q^cyn9$XSeZR6#!B4JE@GVH zoobHZN_*RF#@_SVYKkQ_igme-Y5U}cV(hkR#k1c{bQNMji zU7aE`?dHyx=1`kOYZo_8U7?3-7vHOp`Qe%Z*i+FX!s?6huNp0iCEW-Z7E&jRWmUW_ z67j>)Ew!yq)hhG4o?^z}HWH-e=es#xJUhDRc4B51M4~E-l5VZ!&zQq`gWe`?}#b~7w1LH4Xa-UCT5LXkXQWheBa2YJYbyQ zl1pXR%b(KCXMO0OsXgl0P0Og<{(@&z1aokU-Pq`eQq*JYgt8xdFQ6S z6Z3IFSua8W&M#`~*L#r>Jfd6*BzJ?JFdBR#bDv$_0N!_5vnmo@!>vULcDm`MFU823 zpG9pqjqz^FE5zMDoGqhs5OMmC{Y3iVcl>F}5Rs24Y5B^mYQ;1T&ks@pIApHOdrzXF z-SdX}Hf{X;TaSxG_T$0~#RhqKISGKNK47}0*x&nRIPtmdwxc&QT3$8&!3fWu1eZ_P zJveQj^hJL#Sn!*4k`3}(d(aasl&7G0j0-*_2xtAnoX1@9+h zO#c>YQg60Z;o{Bi=3i7S`Ic+ZE>K{(u|#)9y}q*j8uKQ1^>+(BI}m%1v3$=4ojGBc zm+o1*!T&b}-lVvZqIUBc8V}QyFEgm#oyIuC{8WqUNV{Toz`oxhYpP!_p2oHHh5P@iB*NVo~2=GQm+8Yrkm2Xjc_VyHg1c0>+o~@>*Qzo zHVBJS>$$}$_4EniTI;b1WShX<5-p#TPB&!;lP!lBVBbLOOxh6FuYloD%m;n{r|;MU3!q4AVkua~fieeWu2 zQAQ$ue(IklX6+V;F1vCu-&V?I3d42FgWgsb_e^29ol}HYft?{SLf>DrmOp9o!t>I^ zY7fBCk+E8n_|apgM|-;^=#B?6RnFKlN`oR)`e$+;D=yO-(U^jV;rft^G_zl`n7qnM zL z*-Y4Phq+ZI1$j$F-f;`CD#|`-T~OM5Q>x}a>B~Gb3-+9i>Lfr|Ca6S^8g*{*?_5!x zH_N!SoRP=gX1?)q%>QTY!r77e2j9W(I!uAz{T`NdNmPBBUzi2{`XMB^zJGGwFWeA9 z{fk33#*9SO0)DjROug+(M)I-pKA!CX;IY(#gE!UxXVsa)X!UftIN98{pt#4MJHOhY zM$_l}-TJlxY?LS6Nuz1T<44m<4i^8k@D$zuCPrkmz@sdv+{ciyFJG2Zwy&%c7;atIeTdh!a(R^QXnu1Oq1b42*OQFWnyQ zWeQrdvP|w_idy53Wa<{QH^lFmEd+VlJkyiC>6B#s)F;w-{c;aKIm;Kp50HnA-o3lY z9B~F$gJ@yYE#g#X&3ADx&tO+P_@mnQTz9gv30_sTsaGXkfNYXY{$(>*PEN3QL>I!k zp)KibPhrfX3%Z$H6SY`rXGYS~143wZrG2;=FLj50+VM6soI~up_>fU(2Wl@{BRsMi zO%sL3x?2l1cXTF)k&moNsHfQrQ+wu(gBt{sk#CU=UhrvJIncy@tJX5klLjgMn>~h= zg|FR&;@eh|C7`>s_9c~0-{IAPV){l|Ts`i=)AW;d9&KPc3fMeoTS%8@V~D8*h;&(^>yjT84MM}=%#LS7shLAuuj(0VAYoozhWjq z4LEr?wUe2^WGwdTIgWBkDUJa>YP@5d9^Rs$kCXmMRxuF*YMVrn?0NFyPl}>`&dqZb z<5eqR=ZG3>n2{6v6BvJ`YBZeeTtB88TAY(x0a58EWyuf>+^|x8Qa6wA|1Nb_p|nA zWWa}|z8a)--Wj`LqyFk_a3gN2>5{Rl_wbW?#by7&i*^hRknK%jwIH6=dQ8*-_{*x0j^DUfMX0`|K@6C<|1cgZ~D(e5vBFFm;HTZF(!vT8=T$K+|F)x3kqzBV4-=p1V(lzi(s7jdu0>LD#N=$Lk#3HkG!a zIF<7>%B7sRNzJ66KrFV76J<2bdYhxll0y2^_rdG=I%AgW4~)1Nvz=$1UkE^J%BxLo z+lUci`UcU062os*=`-j4IfSQA{w@y|3}Vk?i;&SSdh8n+$iHA#%ERL{;EpXl6u&8@ zzg}?hkEOUOJt?ZL=pWZFJ19mI1@P=$U5*Im1e_8Z${JsM>Ov?nh8Z zP5QvI!{Jy@&BP48%P2{Jr_VgzW;P@7)M9n|lDT|Ep#}7C$&ud&6>C^5ZiwKIg2McPU(4jhM!BD@@L(Gd*Nu$ji(ljZ<{FIeW_1Mmf;76{LU z-ywN~=uNN)Xi6$<12A9y)K%X|(W0p|&>>4OXB?IiYr||WKDOJPxiSe01NSV-h24^L z_>m$;|C+q!Mj**-qQ$L-*++en(g|hw;M!^%_h-iDjFHLo-n3JpB;p?+o2;`*jpvJU zLY^lt)Un4joij^^)O(CKs@7E%*!w>!HA4Q?0}oBJ7Nr8NQ7QmY^4~jvf0-`%waOLn zdNjAPaC0_7c|RVhw)+71NWjRi!y>C+Bl;Z`NiL^zn2*0kmj5gyhCLCxts*cWCdRI| zjsd=sT5BVJc^$GxP~YF$-U{-?kW6r@^vHXB%{CqYzU@1>dzf#3SYedJG-Rm6^RB7s zGM5PR(yKPKR)>?~vpUIeTP7A1sc8-knnJk*9)3t^e%izbdm>Y=W{$wm(cy1RB-19i za#828DMBY+ps#7Y8^6t)=Ea@%Nkt)O6JCx|ybC;Ap}Z@Zw~*}3P>MZLPb4Enxz9Wf zssobT^(R@KuShj8>@!1M7tm|2%-pYYDxz-5`rCbaTCG5{;Uxm z*g=+H1X8{NUvFGzz~wXa%Eo};I;~`37*WrRU&K0dPSB$yk(Z*@K&+mFal^?c zurbqB-+|Kb5|sznT;?Pj!+kgFY1#Dr;_%A(GIQC{3ct|{*Bji%FNa6c-thbpBkA;U zURV!Dr&X{0J}iht#-Qp2=xzuh(fM>zRoiGrYl5ttw2#r34gC41CCOC31m~^UPTK@s z6;A@)7O7_%C)>bnAXerYuAHdE93>j2N}H${zEc6&SbZ|-fiG*-qtGuy-qDelH(|u$ zorf8_T6Zqe#Ub!+e3oSyrskt_HyW_^5lrWt#30l)tHk|j$@YyEkXUOV;6B51L;M@=NIWZXU;GrAa(LGxO%|im%7F<-6N;en0Cr zLH>l*y?pMwt`1*cH~LdBPFY_l;~`N!Clyfr;7w<^X;&(ZiVdF1S5e(+Q%60zgh)s4 zn2yj$+mE=miVERP(g8}G4<85^-5f@qxh2ec?n+$A_`?qN=iyT1?U@t?V6DM~BIlBB z>u~eXm-aE>R0sQy!-I4xtCNi!!qh?R1!kKf6BoH2GG{L4%PAz0{Sh6xpuyI%*~u)s z%rLuFl)uQUCBQAtMyN;%)zFMx4loh7uTfKeB2Xif`lN?2gq6NhWhfz0u5WP9J>=V2 zo{mLtSy&BA!mSzs&CrKWq^y40JF5a&GSXIi2= z{EYb59J4}VwikL4P=>+mc6{($FNE@e=VUwG+KV21;<@lrN`mnz5jYGASyvz7BOG_6(p^eTxD-4O#lROgon;R35=|nj#eHIfJBYPWG>H>`dHKCDZ3`R{-?HO0mE~(5_WYcFmp8sU?wr*UkAQiNDGc6T zA%}GOLXlOWqL?WwfHO8MB#8M8*~Y*gz;1rWWoVSXP&IbKxbQ8+s%4Jnt?kDsq7btI zCDr0PZ)b;B%!lu&CT#RJzm{l{2fq|BcY85`w~3LSK<><@(2EdzFLt9Y_`;WXL6x`0 zDoQ?=?I@Hbr;*VVll1Gmd8*%tiXggMK81a+T(5Gx6;eNb8=uYn z5BG-0g>pP21NPn>$ntBh>`*})Fl|38oC^9Qz>~MAazH%3Q~Qb!ALMf$srexgPZ2@&c~+hxRi1;}+)-06)!#Mq<6GhP z-Q?qmgo${aFBApb5p}$1OJKTClfi8%PpnczyVKkoHw7Ml9e7ikrF0d~UB}i3vizos zXW4DN$SiEV9{faLt5bHy2a>33K%7Td-n5C*N;f&ZqAg#2hIqEb(y<&f4u5BWJ>2^4 z414GosL=Aom#m&=x_v<0-fp1r%oVJ{T-(xnomNJ(Dryv zh?vj+%=II_nV+@NR+(!fZZVM&(W6{6%9cm+o+Z6}KqzLw{(>E86uA1`_K$HqINlb1 zKelh3-jr2I9V?ych`{hta9wQ2c9=MM`2cC{m6^MhlL2{DLv7C^j z$xXBCnDl_;l|bPGMX@*tV)B!c|4oZyftUlP*?$YU9C_eAsuVHJ58?)zpbr30P*C`T z7y#ao`uE-SOG(Pi+`$=e^mle~)pRrdwL5)N;o{gpW21of(QE#U6w%*C~`v-z0QqBML!!5EeYA5IQB0 z^l01c;L6E(iytN!LhL}wfwP7W9PNAkb+)Cst?qg#$n;z41O4&v+8-zPs+XNb-q zIeeBCh#ivnFLUCwfS;p{LC0O7tm+Sf9Jn)~b%uwP{%69;QC)Ok0t%*a5M+=;y8j=v z#!*pp$9@!x;UMIs4~hP#pnfVc!%-D<+wsG@R2+J&%73lK|2G!EQC)O05TCV=&3g)C!lT=czLpZ@Sa%TYuoE?v8T8`V;e$#Zf2_Nj6nvBgh1)2 GZ~q4|mN%#X literal 61608 zcmb5VV{~QRw)Y#`wrv{~+qP{x72B%VwzFc}c2cp;N~)5ZbDrJayPv(!dGEd-##*zr z)#n-$y^sH|_dchh3@8{H5D*j;5D<{i*8l5IFJ|DjL!e)upfGNX(kojugZ3I`oH1PvW`wFW_ske0j@lB9bX zO;2)`y+|!@X(fZ1<2n!Qx*)_^Ai@Cv-dF&(vnudG?0CsddG_&Wtae(n|K59ew)6St z#dj7_(Cfwzh$H$5M!$UDd8=4>IQsD3xV=lXUq($;(h*$0^yd+b{qq63f0r_de#!o_ zXDngc>zy`uor)4A^2M#U*DC~i+dc<)Tb1Tv&~Ev@oM)5iJ4Sn#8iRw16XXuV50BS7 zdBL5Mefch(&^{luE{*5qtCZk$oFr3RH=H!c3wGR=HJ(yKc_re_X9pD` zJ;uxPzUfVpgU>DSq?J;I@a+10l0ONXPcDkiYcihREt5~T5Gb}sT0+6Q;AWHl`S5dV>lv%-p9l#xNNy7ZCr%cyqHY%TZ8Q4 zbp&#ov1*$#grNG#1vgfFOLJCaNG@K|2!W&HSh@3@Y%T?3YI75bJp!VP*$*!< z;(ffNS_;@RJ`=c7yX04!u3JP*<8jeqLHVJu#WV&v6wA!OYJS4h<_}^QI&97-;=ojW zQ-1t)7wnxG*5I%U4)9$wlv5Fr;cIizft@&N+32O%B{R1POm$oap@&f| zh+5J{>U6ftv|vAeKGc|zC=kO(+l7_cLpV}-D#oUltScw})N>~JOZLU_0{Ka2e1evz z{^a*ZrLr+JUj;)K&u2CoCAXLC2=fVScI(m_p~0FmF>>&3DHziouln?;sxW`NB}cSX z8?IsJB)Z=aYRz!X=yJn$kyOWK%rCYf-YarNqKzmWu$ZvkP12b4qH zhS9Q>j<}(*frr?z<%9hl*i^#@*O2q(Z^CN)c2c z>1B~D;@YpG?G!Yk+*yn4vM4sO-_!&m6+`k|3zd;8DJnxsBYtI;W3We+FN@|tQ5EW= z!VU>jtim0Mw#iaT8t_<+qKIEB-WwE04lBd%Letbml9N!?SLrEG$nmn7&W(W`VB@5S zaY=sEw2}i@F_1P4OtEw?xj4@D6>_e=m=797#hg}f*l^`AB|Y0# z9=)o|%TZFCY$SzgSjS|8AI-%J4x}J)!IMxY3_KYze`_I=c1nmrk@E8c9?MVRu)7+Ue79|)rBX7tVB7U|w4*h(;Gi3D9le49B38`wuv zp7{4X^p+K4*$@gU(Tq3K1a#3SmYhvI42)GzG4f|u zwQFT1n_=n|jpi=70-yE9LA+d*T8u z`=VmmXJ_f6WmZveZPct$Cgu^~gFiyL>Lnpj*6ee>*0pz=t$IJ}+rE zsf@>jlcG%Wx;Cp5x)YSVvB1$yyY1l&o zvwX=D7k)Dn;ciX?Z)Pn8$flC8#m`nB&(8?RSdBvr?>T9?E$U3uIX7T?$v4dWCa46 z+&`ot8ZTEgp7G+c52oHJ8nw5}a^dwb_l%MOh(ebVj9>_koQP^$2B~eUfSbw9RY$_< z&DDWf2LW;b0ZDOaZ&2^i^g+5uTd;GwO(-bbo|P^;CNL-%?9mRmxEw~5&z=X^Rvbo^WJW=n_%*7974RY}JhFv46> zd}`2|qkd;89l}R;i~9T)V-Q%K)O=yfVKNM4Gbacc7AOd>#^&W&)Xx!Uy5!BHnp9kh z`a(7MO6+Ren#>R^D0K)1sE{Bv>}s6Rb9MT14u!(NpZOe-?4V=>qZ>}uS)!y~;jEUK z&!U7Fj&{WdgU#L0%bM}SYXRtM5z!6M+kgaMKt%3FkjWYh=#QUpt$XX1!*XkpSq-pl zhMe{muh#knk{9_V3%qdDcWDv}v)m4t9 zQhv{;} zc{}#V^N3H>9mFM8`i`0p+fN@GqX+kl|M94$BK3J-X`Hyj8r!#x6Vt(PXjn?N)qedP z=o1T^#?1^a{;bZ&x`U{f?}TMo8ToN zkHj5v|}r}wDEi7I@)Gj+S1aE-GdnLN+$hw!=DzglMaj#{qjXi_dwpr|HL(gcCXwGLEmi|{4&4#OZ4ChceA zKVd4K!D>_N=_X;{poT~4Q+!Le+ZV>=H7v1*l%w`|`Dx8{)McN@NDlQyln&N3@bFpV z_1w~O4EH3fF@IzJ9kDk@7@QctFq8FbkbaH7K$iX=bV~o#gfh?2JD6lZf(XP>~DACF)fGFt)X%-h1yY~MJU{nA5 ze2zxWMs{YdX3q5XU*9hOH0!_S24DOBA5usB+Ws$6{|AMe*joJ?RxfV}*7AKN9V*~J zK+OMcE@bTD>TG1*yc?*qGqjBN8mgg@h1cJLDv)0!WRPIkC` zZrWXrceVw;fB%3`6kq=a!pq|hFIsQ%ZSlo~)D z|64!aCnw-?>}AG|*iOl44KVf8@|joXi&|)1rB;EQWgm+iHfVbgllP$f!$Wf42%NO5b(j9Bw6L z;0dpUUK$5GX4QbMlTmLM_jJt!ur`_0~$b#BB7FL*%XFf<b__1o)Ao3rlobbN8-(T!1d-bR8D3S0@d zLI!*GMb5s~Q<&sjd}lBb8Nr0>PqE6_!3!2d(KAWFxa{hm`@u|a(%#i(#f8{BP2wbs zt+N_slWF4IF_O|{w`c~)Xvh&R{Au~CFmW#0+}MBd2~X}t9lz6*E7uAD`@EBDe$>7W zzPUkJx<`f$0VA$=>R57^(K^h86>09?>_@M(R4q($!Ck6GG@pnu-x*exAx1jOv|>KH zjNfG5pwm`E-=ydcb+3BJwuU;V&OS=6yM^4Jq{%AVqnTTLwV`AorIDD}T&jWr8pB&j28fVtk_y*JRP^t@l*($UZ z6(B^-PBNZ+z!p?+e8@$&jCv^EWLb$WO=}Scr$6SM*&~B95El~;W_0(Bvoha|uQ1T< zO$%_oLAwf1bW*rKWmlD+@CP&$ObiDy=nh1b2ejz%LO9937N{LDe7gle4i!{}I$;&Y zkexJ9Ybr+lrCmKWg&}p=`2&Gf10orS?4$VrzWidT=*6{KzOGMo?KI0>GL0{iFWc;C z+LPq%VH5g}6V@-tg2m{C!-$fapJ9y}c$U}aUmS{9#0CM*8pC|sfer!)nG7Ji>mfRh z+~6CxNb>6eWKMHBz-w2{mLLwdA7dA-qfTu^A2yG1+9s5k zcF=le_UPYG&q!t5Zd_*E_P3Cf5T6821bO`daa`;DODm8Ih8k89=RN;-asHIigj`n=ux>*f!OC5#;X5i;Q z+V!GUy0|&Y_*8k_QRUA8$lHP;GJ3UUD08P|ALknng|YY13)}!!HW@0z$q+kCH%xet zlWf@BXQ=b=4}QO5eNnN~CzWBbHGUivG=`&eWK}beuV*;?zt=P#pM*eTuy3 zP}c#}AXJ0OIaqXji78l;YrP4sQe#^pOqwZUiiN6^0RCd#D271XCbEKpk`HI0IsN^s zES7YtU#7=8gTn#lkrc~6)R9u&SX6*Jk4GFX7){E)WE?pT8a-%6P+zS6o&A#ml{$WX zABFz#i7`DDlo{34)oo?bOa4Z_lNH>n;f0nbt$JfAl~;4QY@}NH!X|A$KgMmEsd^&Y zt;pi=>AID7ROQfr;MsMtClr5b0)xo|fwhc=qk33wQ|}$@?{}qXcmECh>#kUQ-If0$ zseb{Wf4VFGLNc*Rax#P8ko*=`MwaR-DQ8L8V8r=2N{Gaips2_^cS|oC$+yScRo*uF zUO|5=?Q?{p$inDpx*t#Xyo6=s?bbN}y>NNVxj9NZCdtwRI70jxvm3!5R7yiWjREEd zDUjrsZhS|P&|Ng5r+f^kA6BNN#|Se}_GF>P6sy^e8kBrgMv3#vk%m}9PCwUWJg-AD zFnZ=}lbi*mN-AOm zCs)r=*YQAA!`e#1N>aHF=bb*z*hXH#Wl$z^o}x##ZrUc=kh%OHWhp=7;?8%Xj||@V?1c ziWoaC$^&04;A|T)!Zd9sUzE&$ODyJaBpvqsw19Uiuq{i#VK1!htkdRWBnb z`{rat=nHArT%^R>u#CjjCkw-7%g53|&7z-;X+ewb?OLWiV|#nuc8mp*LuGSi3IP<<*Wyo9GKV7l0Noa4Jr0g3p_$ z*R9{qn=?IXC#WU>48-k5V2Oc_>P;4_)J@bo1|pf=%Rcbgk=5m)CJZ`caHBTm3%!Z9 z_?7LHr_BXbKKr=JD!%?KhwdYSdu8XxPoA{n8^%_lh5cjRHuCY9Zlpz8g+$f@bw@0V z+6DRMT9c|>1^3D|$Vzc(C?M~iZurGH2pXPT%F!JSaAMdO%!5o0uc&iqHx?ImcX6fI zCApkzc~OOnfzAd_+-DcMp&AOQxE_EsMqKM{%dRMI5`5CT&%mQO?-@F6tE*xL?aEGZ z8^wH@wRl`Izx4sDmU>}Ym{ybUm@F83qqZPD6nFm?t?(7>h*?`fw)L3t*l%*iw0Qu#?$5eq!Qc zpQvqgSxrd83NsdO@lL6#{%lsYXWen~d3p4fGBb7&5xqNYJ)yn84!e1PmPo7ChVd%4 zHUsV0Mh?VpzZD=A6%)Qrd~i7 z96*RPbid;BN{Wh?adeD_p8YU``kOrGkNox3D9~!K?w>#kFz!4lzOWR}puS(DmfjJD z`x0z|qB33*^0mZdM&6$|+T>fq>M%yoy(BEjuh9L0>{P&XJ3enGpoQRx`v6$txXt#c z0#N?b5%srj(4xmPvJxrlF3H%OMB!jvfy z;wx8RzU~lb?h_}@V=bh6p8PSb-dG|-T#A?`c&H2`_!u+uenIZe`6f~A7r)`9m8atC zt(b|6Eg#!Q*DfRU=Ix`#B_dK)nnJ_+>Q<1d7W)eynaVn`FNuN~%B;uO2}vXr5^zi2 z!ifIF5@Zlo0^h~8+ixFBGqtweFc`C~JkSq}&*a3C}L?b5Mh-bW=e)({F_g4O3 zb@SFTK3VD9QuFgFnK4Ve_pXc3{S$=+Z;;4+;*{H}Rc;845rP?DLK6G5Y-xdUKkA6E3Dz&5f{F^FjJQ(NSpZ8q-_!L3LL@H* zxbDF{gd^U3uD;)a)sJwAVi}7@%pRM&?5IaUH%+m{E)DlA_$IA1=&jr{KrhD5q&lTC zAa3c)A(K!{#nOvenH6XrR-y>*4M#DpTTOGQEO5Jr6kni9pDW`rvY*fs|ItV;CVITh z=`rxcH2nEJpkQ^(;1c^hfb8vGN;{{oR=qNyKtR1;J>CByul*+=`NydWnSWJR#I2lN zTvgnR|MBx*XFsfdA&;tr^dYaqRZp*2NwkAZE6kV@1f{76e56eUmGrZ>MDId)oqSWw z7d&r3qfazg+W2?bT}F)4jD6sWaw`_fXZGY&wnGm$FRPFL$HzVTH^MYBHWGCOk-89y zA+n+Q6EVSSCpgC~%uHfvyg@ufE^#u?JH?<73A}jj5iILz4Qqk5$+^U(SX(-qv5agK znUkfpke(KDn~dU0>gdKqjTkVk`0`9^0n_wzXO7R!0Thd@S;U`y)VVP&mOd-2 z(hT(|$=>4FY;CBY9#_lB$;|Wd$aOMT5O_3}DYXEHn&Jrc3`2JiB`b6X@EUOD zVl0S{ijm65@n^19T3l%>*;F(?3r3s?zY{thc4%AD30CeL_4{8x6&cN}zN3fE+x<9; zt2j1RRVy5j22-8U8a6$pyT+<`f+x2l$fd_{qEp_bfxfzu>ORJsXaJn4>U6oNJ#|~p z`*ZC&NPXl&=vq2{Ne79AkQncuxvbOG+28*2wU$R=GOmns3W@HE%^r)Fu%Utj=r9t` zd;SVOnA(=MXgnOzI2@3SGKHz8HN~Vpx&!Ea+Df~`*n@8O=0!b4m?7cE^K*~@fqv9q zF*uk#1@6Re_<^9eElgJD!nTA@K9C732tV~;B`hzZ321Ph=^BH?zXddiu{Du5*IPg} zqDM=QxjT!Rp|#Bkp$(mL)aar)f(dOAXUiw81pX0DC|Y4;>Vz>>DMshoips^8Frdv} zlTD=cKa48M>dR<>(YlLPOW%rokJZNF2gp8fwc8b2sN+i6&-pHr?$rj|uFgktK@jg~ zIFS(%=r|QJ=$kvm_~@n=ai1lA{7Z}i+zj&yzY+!t$iGUy|9jH#&oTNJ;JW-3n>DF+ z3aCOzqn|$X-Olu_p7brzn`uk1F*N4@=b=m;S_C?#hy{&NE#3HkATrg?enaVGT^$qIjvgc61y!T$9<1B@?_ibtDZ{G zeXInVr5?OD_nS_O|CK3|RzzMmu+8!#Zb8Ik;rkIAR%6?$pN@d<0dKD2c@k2quB%s( zQL^<_EM6ow8F6^wJN1QcPOm|ehA+dP(!>IX=Euz5qqIq}Y3;ibQtJnkDmZ8c8=Cf3 zu`mJ!Q6wI7EblC5RvP*@)j?}W=WxwCvF3*5Up_`3*a~z$`wHwCy)2risye=1mSp%p zu+tD6NAK3o@)4VBsM!@);qgsjgB$kkCZhaimHg&+k69~drbvRTacWKH;YCK(!rC?8 zP#cK5JPHSw;V;{Yji=55X~S+)%(8fuz}O>*F3)hR;STU`z6T1aM#Wd+FP(M5*@T1P z^06O;I20Sk!bxW<-O;E081KRdHZrtsGJflFRRFS zdi5w9OVDGSL3 zNrC7GVsGN=b;YH9jp8Z2$^!K@h=r-xV(aEH@#JicPy;A0k1>g1g^XeR`YV2HfmqXY zYbRwaxHvf}OlCAwHoVI&QBLr5R|THf?nAevV-=~V8;gCsX>jndvNOcFA+DI+zbh~# zZ7`qNk&w+_+Yp!}j;OYxIfx_{f0-ONc?mHCiCUak=>j>~>YR4#w# zuKz~UhT!L~GfW^CPqG8Lg)&Rc6y^{%3H7iLa%^l}cw_8UuG;8nn9)kbPGXS}p3!L_ zd#9~5CrH8xtUd?{d2y^PJg+z(xIfRU;`}^=OlehGN2=?}9yH$4Rag}*+AWotyxfCJ zHx=r7ZH>j2kV?%7WTtp+-HMa0)_*DBBmC{sd$)np&GEJ__kEd`xB5a2A z*J+yx>4o#ZxwA{;NjhU*1KT~=ZK~GAA;KZHDyBNTaWQ1+;tOFFthnD)DrCn`DjBZ% zk$N5B4^$`n^jNSOr=t(zi8TN4fpaccsb`zOPD~iY=UEK$0Y70bG{idLx@IL)7^(pL z{??Bnu=lDeguDrd%qW1)H)H`9otsOL-f4bSu};o9OXybo6J!Lek`a4ff>*O)BDT_g z<6@SrI|C9klY(>_PfA^qai7A_)VNE4c^ZjFcE$Isp>`e5fLc)rg@8Q_d^Uk24$2bn z9#}6kZ2ZxS9sI(RqT7?El2@B+($>eBQrNi_k#CDJ8D9}8$mmm z4oSKO^F$i+NG)-HE$O6s1--6EzJa?C{x=QgK&c=)b(Q9OVoAXYEEH20G|q$}Hue%~ zO3B^bF=t7t48sN zWh_zA`w~|){-!^g?6Mqf6ieV zFx~aPUOJGR=4{KsW7I?<=J2|lY`NTU=lt=%JE9H1vBpkcn=uq(q~=?iBt_-r(PLBM zP-0dxljJO>4Wq-;stY)CLB4q`-r*T$!K2o}?E-w_i>3_aEbA^MB7P5piwt1dI-6o!qWCy0 ztYy!x9arGTS?kabkkyv*yxvsPQ7Vx)twkS6z2T@kZ|kb8yjm+^$|sEBmvACeqbz)RmxkkDQX-A*K!YFziuhwb|ym>C$}U|J)4y z$(z#)GH%uV6{ec%Zy~AhK|+GtG8u@c884Nq%w`O^wv2#A(&xH@c5M`Vjk*SR_tJnq z0trB#aY)!EKW_}{#L3lph5ow=@|D5LzJYUFD6 z7XnUeo_V0DVSIKMFD_T0AqAO|#VFDc7c?c-Q%#u00F%!_TW1@JVnsfvm@_9HKWflBOUD~)RL``-!P;(bCON_4eVdduMO>?IrQ__*zE@7(OX zUtfH@AX*53&xJW*Pu9zcqxGiM>xol0I~QL5B%Toog3Jlenc^WbVgeBvV8C8AX^Vj& z^I}H})B=VboO%q1;aU5ACMh{yK4J;xlMc`jCnZR^!~LDs_MP&8;dd@4LDWw~*>#OT zeZHwdQWS!tt5MJQI~cw|Ka^b4c|qyd_ly(+Ql2m&AAw^ zQeSXDOOH!!mAgzAp0z)DD>6Xo``b6QwzUV@w%h}Yo>)a|xRi$jGuHQhJVA%>)PUvK zBQ!l0hq<3VZ*RnrDODP)>&iS^wf64C;MGqDvx>|p;35%6(u+IHoNbK z;Gb;TneFo*`zUKS6kwF*&b!U8e5m4YAo03a_e^!5BP42+r)LFhEy?_7U1IR<; z^0v|DhCYMSj<-;MtY%R@Fg;9Kky^pz_t2nJfKWfh5Eu@_l{^ph%1z{jkg5jQrkvD< z#vdK!nku*RrH~TdN~`wDs;d>XY1PH?O<4^U4lmA|wUW{Crrv#r%N>7k#{Gc44Fr|t z@UZP}Y-TrAmnEZ39A*@6;ccsR>)$A)S>$-Cj!=x$rz7IvjHIPM(TB+JFf{ehuIvY$ zsDAwREg*%|=>Hw$`us~RP&3{QJg%}RjJKS^mC_!U;E5u>`X`jW$}P`Mf}?7G7FX#{ zE(9u1SO;3q@ZhDL9O({-RD+SqqPX)`0l5IQu4q)49TUTkxR(czeT}4`WV~pV*KY&i zAl3~X%D2cPVD^B43*~&f%+Op)wl<&|D{;=SZwImydWL6@_RJjxP2g)s=dH)u9Npki zs~z9A+3fj0l?yu4N0^4aC5x)Osnm0qrhz@?nwG_`h(71P znbIewljU%T*cC=~NJy|)#hT+lx#^5MuDDnkaMb*Efw9eThXo|*WOQzJ*#3dmRWm@! zfuSc@#kY{Um^gBc^_Xdxnl!n&y&}R4yAbK&RMc+P^Ti;YIUh|C+K1|=Z^{nZ}}rxH*v{xR!i%qO~o zTr`WDE@k$M9o0r4YUFFeQO7xCu_Zgy)==;fCJ94M_rLAv&~NhfvcLWCoaGg2ao~3e zBG?Ms9B+efMkp}7BhmISGWmJsKI@a8b}4lLI48oWKY|8?zuuNc$lt5Npr+p7a#sWu zh!@2nnLBVJK!$S~>r2-pN||^w|fY`CT{TFnJy`B|e5;=+_v4l8O-fkN&UQbA4NKTyntd zqK{xEKh}U{NHoQUf!M=2(&w+eef77VtYr;xs%^cPfKLObyOV_9q<(%76-J%vR>w9!us-0c-~Y?_EVS%v!* z15s2s3eTs$Osz$JayyH|5nPAIPEX=U;r&p;K14G<1)bvn@?bM5kC{am|C5%hyxv}a z(DeSKI5ZfZ1*%dl8frIX2?);R^^~LuDOpNpk-2R8U1w92HmG1m&|j&J{EK=|p$;f9 z7Rs5|jr4r8k5El&qcuM+YRlKny%t+1CgqEWO>3;BSRZi(LA3U%Jm{@{y+A+w(gzA< z7dBq6a1sEWa4cD0W7=Ld9z0H7RI^Z7vl(bfA;72j?SWCo`#5mVC$l1Q2--%V)-uN* z9ha*s-AdfbDZ8R8*fpwjzx=WvOtmSzGFjC#X)hD%Caeo^OWjS(3h|d9_*U)l%{Ab8 zfv$yoP{OuUl@$(-sEVNt{*=qi5P=lpxWVuz2?I7Dc%BRc+NGNw+323^ z5BXGfS71oP^%apUo(Y#xkxE)y?>BFzEBZ}UBbr~R4$%b7h3iZu3S(|A;&HqBR{nK& z$;GApNnz=kNO^FL&nYcfpB7Qg;hGJPsCW44CbkG1@l9pn0`~oKy5S777uH)l{irK!ru|X+;4&0D;VE*Ii|<3P zUx#xUqvZT5kVQxsF#~MwKnv7;1pR^0;PW@$@T7I?s`_rD1EGUdSA5Q(C<>5SzE!vw z;{L&kKFM-MO>hy#-8z`sdVx})^(Dc-dw;k-h*9O2_YZw}|9^y-|8RQ`BWJUJL(Cer zP5Z@fNc>pTXABbTRY-B5*MphpZv6#i802giwV&SkFCR zGMETyUm(KJbh+&$8X*RB#+{surjr;8^REEt`2&Dubw3$mx>|~B5IKZJ`s_6fw zKAZx9&PwBqW1Oz0r0A4GtnZd7XTKViX2%kPfv+^X3|_}RrQ2e3l=KG_VyY`H?I5&CS+lAX5HbA%TD9u6&s#v!G> zzW9n4J%d5ye7x0y`*{KZvqyXUfMEE^ZIffzI=Hh|3J}^yx7eL=s+TPH(Q2GT-sJ~3 zI463C{(ag7-hS1ETtU;_&+49ABt5!A7CwLwe z=SoA8mYZIQeU;9txI=zcQVbuO%q@E)JI+6Q!3lMc=Gbj(ASg-{V27u>z2e8n;Nc*pf}AqKz1D>p9G#QA+7mqqrEjGfw+85Uyh!=tTFTv3|O z+)-kFe_8FF_EkTw!YzwK^Hi^_dV5x-Ob*UWmD-})qKj9@aE8g240nUh=g|j28^?v7 zHRTBo{0KGaWBbyX2+lx$wgXW{3aUab6Bhm1G1{jTC7ota*JM6t+qy)c5<@ zpc&(jVdTJf(q3xB=JotgF$X>cxh7k*(T`-V~AR+`%e?YOeALQ2Qud( zz35YizXt(aW3qndR}fTw1p()Ol4t!D1pitGNL95{SX4ywzh0SF;=!wf=?Q?_h6!f* zh7<+GFi)q|XBsvXZ^qVCY$LUa{5?!CgwY?EG;*)0ceFe&=A;!~o`ae}Z+6me#^sv- z1F6=WNd6>M(~ z+092z>?Clrcp)lYNQl9jN-JF6n&Y0mp7|I0dpPx+4*RRK+VQI~>en0Dc;Zfl+x z_e_b7s`t1_A`RP3$H}y7F9_na%D7EM+**G_Z0l_nwE+&d_kc35n$Fxkd4r=ltRZhh zr9zER8>j(EdV&Jgh(+i}ltESBK62m0nGH6tCBr90!4)-`HeBmz54p~QP#dsu%nb~W z7sS|(Iydi>C@6ZM(Us!jyIiszMkd)^u<1D+R@~O>HqZIW&kearPWmT>63%_t2B{_G zX{&a(gOYJx!Hq=!T$RZ&<8LDnxsmx9+TBL0gTk$|vz9O5GkK_Yx+55^R=2g!K}NJ3 zW?C;XQCHZl7H`K5^BF!Q5X2^Mj93&0l_O3Ea3!Ave|ixx+~bS@Iv18v2ctpSt4zO{ zp#7pj!AtDmti$T`e9{s^jf(ku&E|83JIJO5Qo9weT6g?@vX!{7)cNwymo1+u(YQ94 zopuz-L@|5=h8A!(g-MXgLJC0MA|CgQF8qlonnu#j z;uCeq9ny9QSD|p)9sp3ebgY3rk#y0DA(SHdh$DUm^?GI<>%e1?&}w(b zdip1;P2Z=1wM+$q=TgLP$}svd!vk+BZ@h<^4R=GS2+sri7Z*2f`9 z5_?i)xj?m#pSVchk-SR!2&uNhzEi+#5t1Z$o0PoLGz*pT64%+|Wa+rd5Z}60(j?X= z{NLjtgRb|W?CUADqOS@(*MA-l|E342NxRaxLTDqsOyfWWe%N(jjBh}G zm7WPel6jXijaTiNita+z(5GCO0NM=Melxud57PP^d_U## zbA;9iVi<@wr0DGB8=T9Ab#2K_#zi=$igyK48@;V|W`fg~7;+!q8)aCOo{HA@vpSy-4`^!ze6-~8|QE||hC{ICKllG9fbg_Y7v z$jn{00!ob3!@~-Z%!rSZ0JO#@>|3k10mLK0JRKP-Cc8UYFu>z93=Ab-r^oL2 zl`-&VBh#=-?{l1TatC;VweM^=M7-DUE>m+xO7Xi6vTEsReyLs8KJ+2GZ&rxw$d4IT zPXy6pu^4#e;;ZTsgmG+ZPx>piodegkx2n0}SM77+Y*j^~ICvp#2wj^BuqRY*&cjmL zcKp78aZt>e{3YBb4!J_2|K~A`lN=u&5j!byw`1itV(+Q_?RvV7&Z5XS1HF)L2v6ji z&kOEPmv+k_lSXb{$)of~(BkO^py&7oOzpjdG>vI1kcm_oPFHy38%D4&A4h_CSo#lX z2#oqMCTEP7UvUR3mwkPxbl8AMW(e{ARi@HCYLPSHE^L<1I}OgZD{I#YH#GKnpRmW3 z2jkz~Sa(D)f?V?$gNi?6)Y;Sm{&?~2p=0&BUl_(@hYeX8YjaRO=IqO7neK0RsSNdYjD zaw$g2sG(>JR=8Iz1SK4`*kqd_3-?;_BIcaaMd^}<@MYbYisWZm2C2|Np_l|8r9yM|JkUngSo@?wci(7&O9a z%|V(4C1c9pps0xxzPbXH=}QTxc2rr7fXk$9`a6TbWKPCz&p=VsB8^W96W=BsB|7bc zf(QR8&Ktj*iz)wK&mW`#V%4XTM&jWNnDF56O+2bo<3|NyUhQ%#OZE8$Uv2a@J>D%t zMVMiHh?es!Ex19q&6eC&L=XDU_BA&uR^^w>fpz2_`U87q_?N2y;!Z!bjoeKrzfC)} z?m^PM=(z{%n9K`p|7Bz$LuC7!>tFOuN74MFELm}OD9?%jpT>38J;=1Y-VWtZAscaI z_8jUZ#GwWz{JqvGEUmL?G#l5E=*m>`cY?m*XOc*yOCNtpuIGD+Z|kn4Xww=BLrNYS zGO=wQh}Gtr|7DGXLF%|`G>J~l{k^*{;S-Zhq|&HO7rC_r;o`gTB7)uMZ|WWIn@e0( zX$MccUMv3ABg^$%_lNrgU{EVi8O^UyGHPNRt%R!1#MQJn41aD|_93NsBQhP80yP<9 zG4(&0u7AtJJXLPcqzjv`S~5;Q|5TVGccN=Uzm}K{v)?f7W!230C<``9(64}D2raRU zAW5bp%}VEo{4Rko`bD%Ehf=0voW?-4Mk#d3_pXTF!-TyIt6U+({6OXWVAa;s-`Ta5 zTqx&8msH3+DLrVmQOTBOAj=uoxKYT3DS1^zBXM?1W+7gI!aQNPYfUl{3;PzS9*F7g zWJN8x?KjBDx^V&6iCY8o_gslO16=kh(|Gp)kz8qlQ`dzxQv;)V&t+B}wwdi~uBs4? zu~G|}y!`3;8#vIMUdyC7YEx6bb^1o}G!Jky4cN?BV9ejBfN<&!4M)L&lRKiuMS#3} z_B}Nkv+zzxhy{dYCW$oGC&J(Ty&7%=5B$sD0bkuPmj7g>|962`(Q{ZZMDv%YMuT^KweiRDvYTEop3IgFv#)(w>1 zSzH>J`q!LK)c(AK>&Ib)A{g`Fdykxqd`Yq@yB}E{gnQV$K!}RsgMGWqC3DKE(=!{}ekB3+(1?g}xF>^icEJbc z5bdxAPkW90atZT+&*7qoLqL#p=>t-(-lsnl2XMpZcYeW|o|a322&)yO_8p(&Sw{|b zn(tY$xn5yS$DD)UYS%sP?c|z>1dp!QUD)l;aW#`%qMtQJjE!s2z`+bTSZmLK7SvCR z=@I4|U^sCwZLQSfd*ACw9B@`1c1|&i^W_OD(570SDLK`MD0wTiR8|$7+%{cF&){$G zU~|$^Ed?TIxyw{1$e|D$050n8AjJvvOWhLtLHbSB|HIfjMp+gu>DraHZJRrdO53(= z+o-f{+qNog+qSLB%KY;5>Av6X(>-qYk3IIEwZ5~6a+P9lMpC^ z8CJ0q>rEpjlsxCvJm=kms@tlN4+sv}He`xkr`S}bGih4t`+#VEIt{1veE z{ZLtb_pSbcfcYPf4=T1+|BtR!x5|X#x2TZEEkUB6kslKAE;x)*0x~ES0kl4Dex4e- zT2P~|lT^vUnMp{7e4OExfxak0EE$Hcw;D$ehTV4a6hqxru0$|Mo``>*a5=1Ym0u>BDJKO|=TEWJ5jZu!W}t$Kv{1!q`4Sn7 zrxRQOt>^6}Iz@%gA3&=5r;Lp=N@WKW;>O!eGIj#J;&>+3va^~GXRHCY2}*g#9ULab zitCJt-OV0*D_Q3Q`p1_+GbPxRtV_T`jyATjax<;zZ?;S+VD}a(aN7j?4<~>BkHK7bO8_Vqfdq1#W&p~2H z&w-gJB4?;Q&pG9%8P(oOGZ#`!m>qAeE)SeL*t8KL|1oe;#+uOK6w&PqSDhw^9-&Fa zuEzbi!!7|YhlWhqmiUm!muO(F8-F7|r#5lU8d0+=;<`{$mS=AnAo4Zb^{%p}*gZL! zeE!#-zg0FWsSnablw!9$<&K(#z!XOW z;*BVx2_+H#`1b@>RtY@=KqD)63brP+`Cm$L1@ArAddNS1oP8UE$p05R=bvZoYz+^6 z<)!v7pRvi!u_-V?!d}XWQR1~0q(H3{d^4JGa=W#^Z<@TvI6J*lk!A zZ*UIKj*hyO#5akL*Bx6iPKvR3_2-^2mw|Rh-3O_SGN3V9GRo52Q;JnW{iTGqb9W99 z7_+F(Op6>~3P-?Q8LTZ-lwB}xh*@J2Ni5HhUI3`ct|*W#pqb>8i*TXOLn~GlYECIj zhLaa_rBH|1jgi(S%~31Xm{NB!30*mcsF_wgOY2N0XjG_`kFB+uQuJbBm3bIM$qhUyE&$_u$gb zpK_r{99svp3N3p4yHHS=#csK@j9ql*>j0X=+cD2dj<^Wiu@i>c_v zK|ovi7}@4sVB#bzq$n3`EgI?~xDmkCW=2&^tD5RuaSNHf@Y!5C(Is$hd6cuyoK|;d zO}w2AqJPS`Zq+(mc*^%6qe>1d&(n&~()6-ZATASNPsJ|XnxelLkz8r1x@c2XS)R*H(_B=IN>JeQUR;T=i3<^~;$<+8W*eRKWGt7c#>N`@;#!`kZ!P!&{9J1>_g8Zj zXEXxmA=^{8A|3=Au+LfxIWra)4p<}1LYd_$1KI0r3o~s1N(x#QYgvL4#2{z8`=mXy zQD#iJ0itk1d@Iy*DtXw)Wz!H@G2St?QZFz zVPkM%H8Cd2EZS?teQN*Ecnu|PrC!a7F_XX}AzfZl3fXfhBtc2-)zaC2eKx*{XdM~QUo4IwcGgVdW69 z1UrSAqqMALf^2|(I}hgo38l|Ur=-SC*^Bo5ej`hb;C$@3%NFxx5{cxXUMnTyaX{>~ zjL~xm;*`d08bG_K3-E+TI>#oqIN2=An(C6aJ*MrKlxj?-;G zICL$hi>`F%{xd%V{$NhisHSL~R>f!F7AWR&7b~TgLu6!3s#~8|VKIX)KtqTH5aZ8j zY?wY)XH~1_a3&>#j7N}0az+HZ;is;Zw(Am{MX}YhDTe(t{ZZ;TG}2qWYO+hdX}vp9 z@uIRR8g#y~-^E`Qyem(31{H0&V?GLdq9LEOb2(ea#e-$_`5Q{T%E?W(6 z(XbX*Ck%TQM;9V2LL}*Tf`yzai{0@pYMwBu%(I@wTY!;kMrzcfq0w?X`+y@0ah510 zQX5SU(I!*Fag4U6a7Lw%LL;L*PQ}2v2WwYF(lHx_Uz2ceI$mnZ7*eZ?RFO8UvKI0H z9Pq-mB`mEqn6n_W9(s~Jt_D~j!Ln9HA)P;owD-l~9FYszs)oEKShF9Zzcmnb8kZ7% zQ`>}ki1kwUO3j~ zEmh140sOkA9v>j@#56ymn_RnSF`p@9cO1XkQy6_Kog?0ivZDb`QWOX@tjMd@^Qr(p z!sFN=A)QZm!sTh(#q%O{Ovl{IxkF!&+A)w2@50=?a-+VuZt6On1;d4YtUDW{YNDN_ zG@_jZi1IlW8cck{uHg^g=H58lPQ^HwnybWy@@8iw%G! zwB9qVGt_?~M*nFAKd|{cGg+8`+w{j_^;nD>IrPf-S%YjBslSEDxgKH{5p)3LNr!lD z4ii)^%d&cCXIU7UK?^ZQwmD(RCd=?OxmY(Ko#+#CsTLT;p#A%{;t5YpHFWgl+@)N1 zZ5VDyB;+TN+g@u~{UrWrv)&#u~k$S&GeW)G{M#&Di)LdYk?{($Cq zZGMKeYW)aMtjmKgvF0Tg>Mmkf9IB#2tYmH-s%D_9y3{tfFmX1BSMtbe<(yqAyWX60 zzkgSgKb3c{QPG2MalYp`7mIrYg|Y<4Jk?XvJK)?|Ecr+)oNf}XLPuTZK%W>;<|r+% zTNViRI|{sf1v7CsWHvFrkQ$F7+FbqPQ#Bj7XX=#M(a~9^80}~l-DueX#;b}Ajn3VE z{BWI}$q{XcQ3g{(p>IOzFcAMDG0xL)H%wA)<(gl3I-oVhK~u_m=hAr&oeo|4lZbf} z+pe)c34Am<=z@5!2;_lwya;l?xV5&kWe}*5uBvckm(d|7R>&(iJNa6Y05SvlZcWBlE{{%2- z`86)Y5?H!**?{QbzGG~|k2O%eA8q=gxx-3}&Csf6<9BsiXC)T;x4YmbBIkNf;0Nd5 z%whM^!K+9zH>on_<&>Ws?^v-EyNE)}4g$Fk?Z#748e+GFp)QrQQETx@u6(1fk2!(W zWiCF~MomG*y4@Zk;h#2H8S@&@xwBIs|82R*^K(i*0MTE%Rz4rgO&$R zo9Neb;}_ulaCcdn3i17MO3NxzyJ=l;LU*N9ztBJ30j=+?6>N4{9YXg$m=^9@Cl9VY zbo^{yS@gU=)EpQ#;UIQBpf&zfCA;00H-ee=1+TRw@(h%W=)7WYSb5a%$UqNS@oI@= zDrq|+Y9e&SmZrH^iA>Of8(9~Cf-G(P^5Xb%dDgMMIl8gk6zdyh`D3OGNVV4P9X|EvIhplXDld8d z^YWtYUz@tpg*38Xys2?zj$F8%ivA47cGSl;hjD23#*62w3+fwxNE7M7zVK?x_`dBSgPK zWY_~wF~OEZi9|~CSH8}Xi>#8G73!QLCAh58W+KMJJC81{60?&~BM_0t-u|VsPBxn* zW7viEKwBBTsn_A{g@1!wnJ8@&h&d>!qAe+j_$$Vk;OJq`hrjzEE8Wjtm)Z>h=*M25 zOgETOM9-8xuuZ&^@rLObtcz>%iWe%!uGV09nUZ*nxJAY%&KAYGY}U1WChFik7HIw% zZP$3Bx|TG_`~19XV7kfi2GaBEhKap&)Q<9`aPs#^!kMjtPb|+-fX66z3^E)iwyXK7 z8)_p<)O{|i&!qxtgBvWXx8*69WO$5zACl++1qa;)0zlXf`eKWl!0zV&I`8?sG)OD2Vy?reNN<{eK+_ za4M;Hh%&IszR%)&gpgRCP}yheQ+l#AS-GnY81M!kzhWxIR?PW`G3G?} z$d%J28uQIuK@QxzGMKU_;r8P0+oIjM+k)&lZ39i#(ntY)*B$fdJnQ3Hw3Lsi8z&V+ zZly2}(Uzpt2aOubRjttzqrvinBFH4jrN)f0hy)tj4__UTwN)#1fj3-&dC_Vh7}ri* zfJ=oqLMJ-_<#rwVyN}_a-rFBe2>U;;1(7UKH!$L??zTbbzP#bvyg7OQBGQklJ~DgP zd<1?RJ<}8lWwSL)`jM53iG+}y2`_yUvC!JkMpbZyb&50V3sR~u+lok zT0uFRS-yx@8q4fPRZ%KIpLp8R#;2%c&Ra4p(GWRT4)qLaPNxa&?8!LRVdOUZ)2vrh zBSx&kB%#Y4!+>~)<&c>D$O}!$o{<1AB$M7-^`h!eW;c(3J~ztoOgy6Ek8Pwu5Y`Xion zFl9fb!k2`3uHPAbd(D^IZmwR5d8D$495nN2`Ue&`W;M-nlb8T-OVKt|fHk zBpjX$a(IR6*-swdNk@#}G?k6F-~c{AE0EWoZ?H|ZpkBxqU<0NUtvubJtwJ1mHV%9v?GdDw; zAyXZiD}f0Zdt-cl9(P1la+vQ$Er0~v}gYJVwQazv zH#+Z%2CIfOf90fNMGos|{zf&N`c0@x0N`tkFv|_9af3~<0z@mnf*e;%r*Fbuwl-IW z{}B3=(mJ#iwLIPiUP`J3SoP~#)6v;aRXJ)A-pD2?_2_CZ#}SAZ<#v7&Vk6{*i(~|5 z9v^nC`T6o`CN*n%&9+bopj^r|E(|pul;|q6m7Tx+U|UMjWK8o-lBSgc3ZF=rP{|l9 zc&R$4+-UG6i}c==!;I#8aDIbAvgLuB66CQLRoTMu~jdw`fPlKy@AKYWS-xyZzPg&JRAa@m-H43*+ne!8B7)HkQY4 zIh}NL4Q79a-`x;I_^>s$Z4J4-Ngq=XNWQ>yAUCoe&SMAYowP>r_O}S=V+3=3&(O=h zNJDYNs*R3Y{WLmBHc?mFEeA4`0Y`_CN%?8qbDvG2m}kMAiqCv`_BK z_6a@n`$#w6Csr@e2YsMx8udNWtNt=kcqDZdWZ-lGA$?1PA*f4?X*)hjn{sSo8!bHz zb&lGdAgBx@iTNPK#T_wy`KvOIZvTWqSHb=gWUCKXAiB5ckQI`1KkPx{{%1R*F2)Oc z(9p@yG{fRSWE*M9cdbrO^)8vQ2U`H6M>V$gK*rz!&f%@3t*d-r3mSW>D;wYxOhUul zk~~&ip5B$mZ~-F1orsq<|1bc3Zpw6)Ws5;4)HilsN;1tx;N6)tuePw& z==OlmaN*ybM&-V`yt|;vDz(_+UZ0m&&9#{9O|?0I|4j1YCMW;fXm}YT$0%EZ5^YEI z4i9WV*JBmEU{qz5O{#bs`R1wU%W$qKx?bC|e-iS&d*Qm7S=l~bMT{~m3iZl+PIXq{ zn-c~|l)*|NWLM%ysfTV-oR0AJ3O>=uB-vpld{V|cWFhI~sx>ciV9sPkC*3i0Gg_9G!=4ar*-W?D9)?EFL1=;O+W8}WGdp8TT!Fgv z{HKD`W>t(`Cds_qliEzuE!r{ihwEv1l5o~iqlgjAyGBi)$%zNvl~fSlg@M=C{TE;V zQkH`zS8b&!ut(m)%4n2E6MB>p*4(oV>+PT51#I{OXs9j1vo>9I<4CL1kv1aurV*AFZ^w_qfVL*G2rG@D2 zrs87oV3#mf8^E5hd_b$IXfH6vHe&lm@7On~Nkcq~YtE!}ad~?5*?X*>y`o;6Q9lkk zmf%TYonZM`{vJg$`lt@MXsg%*&zZZ0uUSse8o=!=bfr&DV)9Y6$c!2$NHyYAQf*Rs zk{^?gl9E z5Im8wlAsvQ6C2?DyG@95gUXZ3?pPijug25g;#(esF_~3uCj3~94}b*L>N2GSk%Qst z=w|Z>UX$m!ZOd(xV*2xvWjN&c5BVEdVZ0wvmk)I+YxnyK%l~caR=7uNQ=+cnNTLZ@&M!I$Mj-r{!P=; z`C2)D=VmvK8@T5S9JZoRtN!S*D_oqOxyy!q6Zk|~4aT|*iRN)fL)c>-yycR>-is0X zKrko-iZw(f(!}dEa?hef5yl%p0-v-8#8CX8!W#n2KNyT--^3hq6r&`)5Y@>}e^4h- zlPiDT^zt}Ynk&x@F8R&=)k8j$=N{w9qUcIc&)Qo9u4Y(Ae@9tA`3oglxjj6c{^pN( zQH+Uds2=9WKjH#KBIwrQI%bbs`mP=7V>rs$KG4|}>dxl_k!}3ZSKeEen4Iswt96GGw`E6^5Ov)VyyY}@itlj&sao|>Sb5 zeY+#1EK(}iaYI~EaHQkh7Uh>DnzcfIKv8ygx1Dv`8N8a6m+AcTa-f;17RiEed>?RT zk=dAksmFYPMV1vIS(Qc6tUO+`1jRZ}tcDP? zt)=7B?yK2RcAd1+Y!$K5*ds=SD;EEqCMG6+OqPoj{&8Y5IqP(&@zq@=A7+X|JBRi4 zMv!czlMPz)gt-St2VZwDD=w_S>gRpc-g zUd*J3>bXeZ?Psjohe;z7k|d<*T21PA1i)AOi8iMRwTBSCd0ses{)Q`9o&p9rsKeLaiY zluBw{1r_IFKR76YCAfl&_S1*(yFW8HM^T()&p#6y%{(j7Qu56^ZJx1LnN`-RTwimdnuo*M8N1ISl+$C-%=HLG-s} zc99>IXRG#FEWqSV9@GFW$V8!{>=lSO%v@X*pz*7()xb>=yz{E$3VE;e)_Ok@A*~El zV$sYm=}uNlUxV~6e<6LtYli1!^X!Ii$L~j4e{sI$tq_A(OkGquC$+>Rw3NFObV2Z)3Rt~Jr{oYGnZaFZ^g5TDZlg;gaeIP} z!7;T{(9h7mv{s@piF{-35L=Ea%kOp;^j|b5ZC#xvD^^n#vPH=)lopYz1n?Kt;vZmJ z!FP>Gs7=W{sva+aO9S}jh0vBs+|(B6Jf7t4F^jO3su;M13I{2rd8PJjQe1JyBUJ5v zcT%>D?8^Kp-70bP8*rulxlm)SySQhG$Pz*bo@mb5bvpLAEp${?r^2!Wl*6d7+0Hs_ zGPaC~w0E!bf1qFLDM@}zso7i~(``)H)zRgcExT_2#!YOPtBVN5Hf5~Ll3f~rWZ(UsJtM?O*cA1_W0)&qz%{bDoA}{$S&-r;0iIkIjbY~ zaAqH45I&ALpP=9Vof4OapFB`+_PLDd-0hMqCQq08>6G+C;9R~}Ug_nm?hhdkK$xpI zgXl24{4jq(!gPr2bGtq+hyd3%Fg%nofK`psHMs}EFh@}sdWCd!5NMs)eZg`ZlS#O0 zru6b8#NClS(25tXqnl{|Ax@RvzEG!+esNW-VRxba(f`}hGoqci$U(g30i}2w9`&z= zb8XjQLGN!REzGx)mg~RSBaU{KCPvQx8)|TNf|Oi8KWgv{7^tu}pZq|BS&S<53fC2K4Fw6>M^s$R$}LD*sUxdy6Pf5YKDbVet;P!bw5Al-8I1Nr(`SAubX5^D9hk6$agWpF}T#Bdf{b9-F#2WVO*5N zp+5uGgADy7m!hAcFz{-sS0kM7O)qq*rC!>W@St~^OW@R1wr{ajyYZq5H!T?P0e+)a zaQ%IL@X_`hzp~vRH0yUblo`#g`LMC%9}P;TGt+I7qNcBSe&tLGL4zqZqB!Bfl%SUa z6-J_XLrnm*WA`34&mF+&e1sPCP9=deazrM=Pc4Bn(nV;X%HG^4%Afv4CI~&l!Sjzb z{rHZ3od0!Al{}oBO>F*mOFAJrz>gX-vs!7>+_G%BB(ljWh$252j1h;9p~xVA=9_`P z5KoFiz96_QsTK%B&>MSXEYh`|U5PjX1(+4b#1PufXRJ*uZ*KWdth1<0 zsAmgjT%bowLyNDv7bTUGy|g~N34I-?lqxOUtFpTLSV6?o?<7-UFy*`-BEUsrdANh} zBWkDt2SAcGHRiqz)x!iVoB~&t?$yn6b#T=SP6Ou8lW=B>=>@ik93LaBL56ub`>Uo!>0@O8?e)$t(sgy$I z6tk3nS@yFFBC#aFf?!d_3;%>wHR;A3f2SP?Na8~$r5C1N(>-ME@HOpv4B|Ty7%jAv zR}GJwsiJZ5@H+D$^Cwj#0XA_(m^COZl8y7Vv(k=iav1=%QgBOVzeAiw zaDzzdrxzj%sE^c9_uM5D;$A_7)Ln}BvBx^=)fO+${ou%B*u$(IzVr-gH3=zL6La;G zu0Kzy5CLyNGoKRtK=G0-w|tnwI)puPDOakRzG(}R9fl7#<|oQEX;E#yCWVg95 z;NzWbyF&wGg_k+_4x4=z1GUcn6JrdX4nOVGaAQ8#^Ga>aFvajQN{!+9rgO-dHP zIp@%&ebVg}IqnRWwZRTNxLds+gz2@~VU(HI=?Epw>?yiEdZ>MjajqlO>2KDxA>)cj z2|k%dhh%d8SijIo1~20*5YT1eZTDkN2rc^zWr!2`5}f<2f%M_$to*3?Ok>e9$X>AV z2jYmfAd)s|(h?|B(XYrIfl=Wa_lBvk9R1KaP{90-z{xKi+&8=dI$W0+qzX|ZovWGOotP+vvYR(o=jo?k1=oG?%;pSqxcU* zWVGVMw?z__XQ9mnP!hziHC`ChGD{k#SqEn*ph6l46PZVkm>JF^Q{p&0=MKy_6apts z`}%_y+Tl_dSP(;Ja&sih$>qBH;bG;4;75)jUoVqw^}ee=ciV;0#t09AOhB^Py7`NC z-m+ybq1>_OO+V*Z>dhk}QFKA8V?9Mc4WSpzj{6IWfFpF7l^au#r7&^BK2Ac7vCkCn{m0uuN93Ee&rXfl1NBY4NnO9lFUp zY++C1I;_{#OH#TeP2Dp?l4KOF8ub?m6zE@XOB5Aiu$E~QNBM@;r+A5mF2W1-c7>ex zHiB=WJ&|`6wDq*+xv8UNLVUy4uW1OT>ey~Xgj@MMpS@wQbHAh>ysYvdl-1YH@&+Q! z075(Qd4C!V`9Q9jI4 zSt{HJRvZec>vaL_brKhQQwbpQd4_Lmmr0@1GdUeU-QcC{{8o=@nwwf>+dIKFVzPriGNX4VjHCa zTbL9w{Y2V87c2ofX%`(48A+4~mYTiFFl!e{3K^C_k%{&QTsgOd0*95KmWN)P}m zTRr{`f7@=v#+z_&fKYkQT!mJn{*crj%ZJz#(+c?>cD&2Lo~FFAWy&UG*Op^pV`BR^I|g?T>4l5;b|5OQ@t*?_Slp`*~Y3`&RfKD^1uLezIW(cE-Dq2z%I zBi8bWsz0857`6e!ahet}1>`9cYyIa{pe53Kl?8|Qg2RGrx@AlvG3HAL-^9c^1GW;)vQt8IK+ zM>!IW*~682A~MDlyCukldMd;8P|JCZ&oNL(;HZgJ>ie1PlaInK7C@Jg{3kMKYui?e!b`(&?t6PTb5UPrW-6DVU%^@^E`*y-Fd(p|`+JH&MzfEq;kikdse ziFOiDWH(D< zyV7Rxt^D0_N{v?O53N$a2gu%1pxbeK;&ua`ZkgSic~$+zvt~|1Yb=UfKJW2F7wC^evlPf(*El+#}ZBy0d4kbVJsK- z05>;>?HZO(YBF&v5tNv_WcI@O@LKFl*VO?L(!BAd!KbkVzo;v@~3v`-816GG?P zY+H3ujC>5=Am3RIZDdT#0G5A6xe`vGCNq88ZC1aVXafJkUlcYmHE^+Z{*S->ol%-O znm9R0TYTr2w*N8Vs#s-5=^w*{Y}qp5GG)Yt1oLNsH7y~N@>Eghms|K*Sdt_u!&I}$ z+GSdFTpbz%KH+?B%Ncy;C`uW6oWI46(tk>r|5|-K6)?O0d_neghUUOa9BXHP*>vi; z={&jIGMn-92HvInCMJcyXwHTJ42FZp&Wxu+9Rx;1x(EcIQwPUQ@YEQQ`bbMy4q3hP zNFoq~Qd0=|xS-R}k1Im3;8s{BnS!iaHIMLx)aITl)+)?Yt#fov|Eh>}dv@o6R{tG>uHsy&jGmWN5+*wAik|78(b?jtysPHC#e+Bzz~V zS3eEXv7!Qn4uWi!FS3B?afdD*{fr9>B~&tc671fi--V}~E4un;Q|PzZRwk-azprM$4AesvUb5`S`(5x#5VJ~4%ET6&%GR$}muHV-5lTsCi_R|6KM(g2PCD@|yOpKluT zakH!1V7nKN)?6JmC-zJoA#ciFux8!)ajiY%K#RtEg$gm1#oKUKX_Ms^%hvKWi|B=~ zLbl-L)-=`bfhl`>m!^sRR{}cP`Oim-{7}oz4p@>Y(FF5FUEOfMwO!ft6YytF`iZRq zfFr{!&0Efqa{1k|bZ4KLox;&V@ZW$997;+Ld8Yle91he{BfjRhjFTFv&^YuBr^&Pe zswA|Bn$vtifycN8Lxr`D7!Kygd7CuQyWqf}Q_PM}cX~S1$-6xUD%-jrSi24sBTFNz(Fy{QL2AmNbaVggWOhP;UY4D>S zqKr!UggZ9Pl9Nh_H;qI`-WoH{ceXj?m8y==MGY`AOJ7l0Uu z)>M%?dtaz2rjn1SW3k+p`1vs&lwb%msw8R!5nLS;upDSxViY98IIbxnh{}mRfEp=9 zbrPl>HEJeN7J=KnB6?dwEA6YMs~chHNG?pJsEj#&iUubdf3JJwu=C(t?JpE6xMyhA3e}SRhunDC zn-~83*9=mADUsk^sCc%&&G1q5T^HR9$P#2DejaG`Ui*z1hI#h7dwpIXg)C{8s< z%^#@uQRAg-$z&fmnYc$Duw63_Zopx|n{Bv*9Xau{a)2%?H<6D>kYY7_)e>OFT<6TT z0A}MQLgXbC2uf`;67`mhlcUhtXd)Kbc$PMm=|V}h;*_%vCw4L6r>3Vi)lE5`8hkSg zNGmW-BAOO)(W((6*e_tW&I>Nt9B$xynx|sj^ux~?q?J@F$L4;rnm_xy8E*JYwO-02u9_@@W0_2@?B@1J{y~Q39N3NX^t7#`=34Wh)X~sU&uZWgS1Z09%_k|EjA4w_QqPdY`oIdv$dJZ;(!k)#U8L+|y~gCzn+6WmFt#d{OUuKHqh1-uX_p*Af8pFYkYvKPKBxyid4KHc}H` z*KcyY;=@wzXYR{`d{6RYPhapShXIV?0cg_?ahZ7do)Ot#mxgXYJYx}<%E1pX;zqHd zf!c(onm{~#!O$2`VIXezECAHVd|`vyP)Uyt^-075X@NZDBaQt<>trA3nY-Dayki4S zZ^j6CCmx1r46`4G9794j-WC0&R9(G7kskS>=y${j-2;(BuIZTLDmAyWTG~`0)Bxqk zd{NkDe9ug|ms@0A>JVmB-IDuse9h?z9nw!U6tr7t-Lri5H`?TjpV~8(gZWFq4Vru4 z!86bDB;3lpV%{rZ`3gtmcRH1hjj!loI9jN>6stN6A*ujt!~s!2Q+U1(EFQEQb(h4E z6VKuRouEH`G6+8Qv2C)K@^;ldIuMVXdDDu}-!7FS8~k^&+}e9EXgx~)4V4~o6P^52 z)a|`J-fOirL^oK}tqD@pqBZi_;7N43%{IQ{v&G9^Y^1?SesL`;Z(dt!nn9Oj5Odde%opv&t zxJ><~b#m+^KV&b?R#)fRi;eyqAJ_0(nL*61yPkJGt;gZxSHY#t>ATnEl-E%q$E16% zZdQfvhm5B((y4E3Hk6cBdwGdDy?i5CqBlCVHZr-rI$B#>Tbi4}Gcvyg_~2=6O9D-8 zY2|tKrNzbVR$h57R?Pe+gUU_il}ZaWu|Az#QO@};=|(L-RVf0AIW zq#pO+RfM7tdV`9lI6g;{qABNId`fG%U9Va^ravVT^)CklDcx)YJKeJdGpM{W1v8jg z@&N+mR?BPB=K1}kNwXk_pj44sd>&^;d!Z~P>O78emE@Qp@&8PyB^^4^2f7e)gekMv z2aZNvP@;%i{+_~>jK7*2wQc6nseT^n6St9KG#1~Y@$~zR_=AcO2hF5lCoH|M&c{vR zSp(GRVVl=T*m~dIA;HvYm8HOdCkW&&4M~UDd^H)`p__!4k+6b)yG0Zcek8OLw$C^K z3-BbLiG_%qX|ZYpXJ$(c@aa7b4-*IQkDF}=gZSV`*ljP|5mWuHSCcf$5qqhZTv&P?I$z^>}qP(q!Aku2yA5vu38d8x*q{6-1`%PrE_r0-9Qo?a#7Zbz#iGI7K<(@k^|i4QJ1H z4jx?{rZbgV!me2VT72@nBjucoT zUM9;Y%TCoDop?Q5fEQ35bCYk7!;gH*;t9t-QHLXGmUF;|vm365#X)6b2Njsyf1h9JW#x$;@x5Nx2$K$Z-O3txa%;OEbOn6xBzd4n4v)Va=sj5 z%rb#j7{_??Tjb8(Hac<^&s^V{yO-BL*uSUk2;X4xt%NC8SjO-3?;Lzld{gM5A=9AV z)DBu-Z8rRvXXwSVDH|dL-3FODWhfe1C_iF``F05e{dl(MmS|W%k-j)!7(ARkV?6r~ zF=o42y+VapxdZn;GnzZfGu<6oG-gQ7j7Zvgo7Am@jYxC2FpS@I;Jb%EyaJDBQC(q% zKlZ}TVu!>;i3t~OAgl@QYy1X|T~D{HOyaS*Bh}A}S#a9MYS{XV{R-|niEB*W%GPW! zP^NU(L<}>Uab<;)#H)rYbnqt|dOK(-DCnY==%d~y(1*{D{Eo1cqIV8*iMfx&J*%yh zx=+WHjt0q2m*pLx8=--UqfM6ZWjkev>W-*}_*$Y(bikH`#-Gn#!6_ zIA&kxn;XYI;eN9yvqztK-a113A%97in5CL5Z&#VsQ4=fyf&3MeKu70)(x^z_uw*RG zo2Pv&+81u*DjMO6>Mrr7vKE2CONqR6C0(*;@4FBM;jPIiuTuhQ-0&C)JIzo_k>TaS zN_hB;_G=JJJvGGpB?uGgSeKaix~AkNtYky4P7GDTW6{rW{}V9K)Cn^vBYKe*OmP!; zohJs=l-0sv5&pL6-bowk~(swtdRBZQHh8)m^r2+qTtZ zt4m$B?OQYNyfBA0E)g28a*{)a=%%f-?{F;++-Xs#5|7kSHTD*E9@$V ztE%7zX4A(L`n)FY8Y4pOnKC|Pf)j$iR#yP;V0+|Hki+D;t4I4BjkfdYliK9Gf6RYw z;3px$Ud5aTd`yq$N7*WOs!{X91hZZ;AJ9iQOH%p;v$R%OQum_h#rq9*{ve(++|24z zh2P;{-Z?u#rOqd0)D^_Ponv(Y9KMB9#?}nJdUX&r_rxF0%3__#8~ZwsyrSPmtWY27 z-54ZquV2t_W!*+%uwC=h-&_q~&nQer0(FL74to%&t^byl^C?wTaZ-IS9OssaQFP)1 zAov0o{?IRAcCf+PjMWSdmP42gysh|c9Ma&Q^?_+>>+-yrC8WR;*XmJ;>r9v*>=W}tgWG;WIt{~L8`gk8DP{dSdG z4SDM7g5ahMHYHHk*|mh9{AKh-qW7X+GEQybJt9A@RV{gaHUAva+=lSroK^NUJYEiL z?X6l9ABpd)9zzA^;FdZ$QQs#uD@hdcaN^;Q=AXlbHv511Meye`p>P4Y2nblEDEeZo}-$@g&L98Aih6tgLz--${eKTxymIipy0xSYgZZ zq^yyS4yNPTtPj-sM?R8@9Q1gtXPqv{$lb5i|C1yymwnGdfYV3nA-;5!Wl zD0fayn!B^grdE?q^}ba{-LIv*Z}+hZm_F9c$$cW!bx2DgJD&6|bBIcL@=}kQA1^Eh zXTEznqk)!!IcTl>ey?V;X8k<+C^DRA{F?T*j0wV`fflrLBQq!l7cbkAUE*6}WabyF zgpb+|tv=aWg0i}9kBL8ZCObYqHEycr5tpc-$|vdvaBsu#lXD@u_e1iL z{h>xMRS0a7KvW?VttrJFpX^5DC4Bv4cp6gNG6#8)7r7IxXfSNSp6)_6tZ4l>(D+0I zPhU)N!sKywaBusHdVE!yo5$20JAU8V_XcW{QmO!p*~ns8{2~bhjydnmA&=r zX9NSM9QYogYMDZ~kS#Qx`mt>AmeR3p@K$`fbJ%LQ1c5lEOz<%BS<}2DL+$>MFcE%e zlxC)heZ7#i80u?32eOJI9oQRz0z;JW@7Th4q}YmQ-`Z?@y3ia^_)7f37QMwDw~<-@ zT)B6fftmK_6YS!?{uaj5lLxyR++u*ZY2Mphm5cd7PA5=%rd)95hJ9+aGSNfjy>Ylc zoI0nGIT3sKmwX8h=6CbvhVO+ehFIR155h8iRuXZx^cW>rq5K4z_dvM#hRER=WR@THs%WELI9uYK9HN44Em2$#@k)hD zicqRPKV#yB;UlcsTL_}zCMK0T;eXHfu`y2(dfwm(v)IBbh|#R>`2cot{m7}8_X&oD zr@94PkMCl%d3FsC4pil=#{3uv^+)pvxfwmPUr)T)T|GcZVD$wVj$mjkjDs`5cm8N! zXVq2CvL;gWGpPI4;9j;2&hS*o+LNp&C5Ac=OXx*W5y6Z^az)^?G0)!_iAfjH5wiSE zD(F}hQZB#tF5iEx@0sS+dP70DbZ*<=5X^)Pxo^8aKzOzuyc2rq=<0-k;Y_ID1>9^v z+)nc36}?>jen*1%OX3R*KRASj${u$gZ$27Hpcj=95kK^aLzxhW6jj_$w6}%#1*$5D zG1H_vYFrCSwrRqYw*9<}OYAOQT)u%9lC`$IjZV<4`9Sc;j{Qv_6+uHrYifK&On4V_7yMil!0Yv55z@dFyD{U@Sy>|vTX=P_( zRm<2xj*Z}B30VAu@0e+}at*y?wXTz|rPalwo?4ZZc>hS0Ky6~mi@kv#?xP2a;yt?5=(-CqvP_3&$KdjB7Ku;# z`GLE*jW1QJB5d&E?IJO?1+!Q8HQMGvv^RuFoi=mM4+^tOqvX%X&viB%Ko2o-v4~~J z267ui;gsW?J=qS=D*@*xJvAy3IOop5bEvfR4MZC>9Y4Z$rGI|EHNNZ7KX;Ix{xSvm z-)Cau-xuTm|7`4kUdXvd_d^E=po(76ELfq5OgxIt3aqDy#zBfIy-5<3gpn{Ce`-ha z<;6y@{Bgqw?c~h*&j{FozQCh=`Lv-5Iw!KdSt;%GDOq%=(V!dJ-}|}|0o5G2kJj6{ z`jCSPs$9Fe8O(+qALZiJ$WtR=<@GvsdM)IJ`7XrBfW0iyYE#Vy^e@zbysg*B5Z_kSL6<)vqoaH zQ{!9!*{e9UZo^h+qZ`T@LfVwAEwc&+9{C8c%oj41q#hyn<&zA9IIur~V|{mmu`n5W z8)-Ou$YgjQ*PMIqHhZ_9E?(uoK0XM5aQkarcp}WT^7b^FC#^i>#8LGZ9puDuXUYas z7caX)V5U6uY-L5Wl%)j$qRkR;7@3T*N64YK_!`Fw=>CAwe~2loI1<>DZW&sb7Q)X;6E08&$h! z2=c1i4UOO{R4TmkTz+o9n`}+%d%blR6P;5{`qjtxlN$~I%tMMDCY`~e{+mRF!rj5( z3ywv)P_PUUqREu)TioPkg&5RKjY6z%pRxQPQ{#GNMTPag^S8(8l{!{WGNs2U1JA-O zq02VeYcArhTAS;v3);k(&6ayCH8SXN@r;1NQeJ*y^NHM+zOd;?t&c!Hq^SR_w6twGV8dl>j zjS+Zc&Yp7cYj&c1y3IxQ%*kWiYypvoh(k8g`HrY<_Bi-r%m-@SLfy-6mobxkWHxyS z>TtM2M4;Uqqy|+8Q++VcEq$PwomV1D4UzNA*Tgkg9#Gpz#~&iPf|Czx!J?qss?e|3 z4gTua75-P{2X7w9eeK3~GE0ip-D;%%gTi)8bR~Ez@)$gpuS~jZs`CrO5SR-Xy7bkA z89fr~mY}u4A$|r1$fe-;T{yJh#9Ime1iRu8eo?uY9@yqAU3P!rx~SsP;LTBL zeoMK(!;(Zt8313 z3)V)q_%eflKW?BnMZa}6E0c7t!$-mC$qt44OME5F(6B$E8w*TUN-h}0dOiXI+TH zYFrr&k1(yO(|J0vP|{22@Z}bxm@7BkjO)f)&^fv|?_JX+s)1*|7X7HH(W?b3QZ3!V|~m?8}uJsF>NvE4@fik zjyyh+U*tt`g6v>k9ub88a;ySvS1QawGn7}aaR**$rJA=a#eUT~ngUbJ%V=qsFIekLbv!YkqjTG{_$F;$w19$(ivIs*1>?2ka%uMOx@B9`LD zhm~)z@u4x*zcM1WhiX)!U{qOjJHt1xs{G1S?rYe)L)ntUu^-(o_dfqZu)}W(X%Uu| zN*qI@&R2fB#Jh|Mi+eMrZDtbNvYD3|v0Kx>E#Ss;Be*T$@DC!2A|mb%d}TTN3J+c= zu@1gTOXFYy972S+=C;#~)Z{Swr0VI5&}WYzH22un_Yg5o%f9fvV(`6!{C<(ZigQ2`wso)cj z9O12k)15^Wuv#rHpe*k5#4vb%c znP+Gjr<-p%01d<+^yrSoG?}F=eI8X;?=Fo2a~HUiJ>L!oE#9tXRp!adg-b9D;(6$E zeW0tH$US04zTX$OxM&X+2ip>KdFM?iG_fgOD-qB|uFng8*#Z5jgqGY=zLU?4!OlO#~YBTB9b9#~H@nqQ#5 z6bV));d?IJTVBC+79>rGuy1JgxPLy$dA7;_^^L)02m}XLjFR*qH`eI~+eJo(7D`LH z(W%lGnGK+Vk_3kyF*zpgO=1MxMg?hxe3}}YI>dVs8l}5eWjYu4=w6MWK09+05 zGdpa#$awd>Q|@aZa*z{5F3xy3n@E4YT9%TmMo0jxW59p0bI?&S}M+ z&^NG%rf7h*m9~p#b19|`wO5OMY-=^XT+=yrfGNpl<&~~FGsx_`IaFn+sEgF$hgOa~oAVAiu^a$jHcqkE=dj`ze z=axsfrzzh6VGD0x#6Ff=t%+VTiq!n6^gv*uIUD<9fOhvR;al5kcY${uunn}-!74<7 zmP^3cl-kyN(QY!!Z-^PY-OUkh=3ZWk6>le$_Q&xk4cgH{?i)C%2RM@pX5Q{jdSlo! zVau5v44cQX5|zQlQDt;dCg)oM0B<=P1CR!W%!^m$!{pKx;bn9DePJjWBX)q!`$;0K zqJIIyD#aK;#-3&Nf=&IhtbV|?ZGYHSphp~6th`p2rkw&((%kBV7<{siEOU7AxJj+FuRdDu$ zcmTW8usU_u!r)#jg|J=Gt{##7;uf4A5cdt6Y02}f(d2)z~ z)CH~gVAOwBLk$ZiIOn}NzDjvfw(w$u|BdCBI#)3xB-Ot?nz?iR38ayCm48M=_#9r7 zw8%pwQ<9mbEs5~_>pN3~#+Er~Q86J+2TDXM6umCbukd-X6pRIr5tF?VauT8jW> zY^#)log>jtJs2s3xoiPB7~8#1ZMv>Zx0}H58k-@H2huNyw~wsl0B8j)H5)H9c7y&i zp8^0;rKbxC1eEZ-#Qxvz)Xv$((8lK9I>BspPajluysw^f#t9P;OUis43mmEzX+lk* zc4T-Ms9_687GR+~QS#0~vxK#DSGN=a-m(@eZTqw2<+lN9>R~gK2)3;sT4%nI%Y|0m zX9SPR!>?~s=j5H4WMqeTW8QaLZ=1bWS5I3xZ&$(ypc=tHrv+hX@s)VG(tc!yvLM7n zshN=C#v={X1r;)xn0Pow_1eMhkn!{;x$BJ#PIz)m585&%cmzk;btQzZAN_^zis;n? z?6I~bN?s;7vg_dtoTc4A5Ow*Rb}No#UYl)sN|RmoYo}k^cKLXd8F`44?RrokkPvN5 ztUrx;U~B;jbE_qGd3n0j2i}A{enJvJ?gSF~NQj~EP5vM-w4@;QQ5n(Npic}XNW6B0 zq9F4T%6kp7qGhd0vpQcz+nMk8GOAmbz8Bt4@GtewGr6_>Xj>ge)SyfY}nu>Y!a@HoIx(StD zx`!>RT&}tpBL%nOF%7XIFW?n1AP*xthCMzhrU6G!U6?m4!CPWTvn#Yaoi_95CT2!L z|B=5zeRW30&ANGN>J9#GtCm&3SF6n4TqDz<-{@ZXkrkRDCpV$DwCtI^e&3i1A{Ar&JZtS^c+lyPa6 z%JJr42S_;eFC#M~bdtQePhOU32WDiZ4@H&af)z#$Y|hnQNb)8(3?1Ad>5uaZ1z zU~!jt3XUI@gpWb8tWTyH7DGvKvzYfqNIy3P{9vpwz_C-QL&`+8Io$F5PS-@YQJoEO z17D9P(+sXajWSH_8&C?fn>rTLX+(?KiwX#JNV)xE0!Q@>Tid$V2#r4y6fkph?YZ>^ z(o^q(0*P->3?I0cELXJn(N|#qTm6 zAPIL~n)m!50;*?5=MOOc4Wk;w(0c$(!e?vpV23S|n|Y7?nyc8)fD8t-KI&nTklH&BzqQ}D(1gH3P+5zGUzIjT~x`;e8JH=86&5&l-DP% z)F+Et(h|GJ?rMy-Zrf>Rv@<3^OrCJ1xv_N*_@-K5=)-jP(}h1Rts44H&ou8!G_C1E zhTfUDASJ2vu!4@j58{NN;78i?6__xR75QEDC4JN{>RmgcNrn-EOpEOcyR<8FS@RB@ zH!R7J=`KK^u06eeI|X@}KvQmdKE3AmAy8 zM4IIvde#e4O(iwag`UL5yQo>6&7^=D4yE-Eo9$9R2hR} zn;Z9i-d=R-xZl4@?s%8|m1M`$J6lW1r0Y)+8q$}Vn4qyR1jqTjGH;@Z!2KiGun2~x zaiEfzVT<|_b6t}~XPeflAm8hvCHP3Bp*tl{^y_e{Jsn@w+KP{7}bH_s=1S2E1sj=18a39*Ag~lbkT^_OQuYQey=b zW^{0xlQ@O$^cSxUZ8l(Mspg8z0cL*?yH4;X2}TdN)uN31A%$3$a=4;{S@h#Y(~i%) zc=K7Ggl=&2hYVic*W65gpSPE70pU;FN@3k?BYdNDKv6wlsBAF^);qiqI zhklsX4TaWiC%VbnZ|yqL+Pcc;(#&E*{+Rx&<&R{uTYCn^OD|mAk4%Q7gbbgMnZwE{ zy7QMK%jIjU@ye?0; z;0--&xVeD}m_hq9A8a}c9WkI2YKj8t!Mkk!o%AQ?|CCBL9}n570}OmZ(w)YI6#QS&p<={tcek*D{CPR%eVA1WBGUXf z%gO2vL7iVDr1$!LAW)1@H>GoIl=&yyZ7=*9;wrOYQ}O}u>h}4FWL?N2ivURlUi11- zl{G0fo`9?$iAEN<4kxa#9e0SZPqa{pw?K=tdN5tRc7HDX-~Ta6_+#s9W&d`6PB7dF*G@|!Mc}i zc=9&T+edI(@la}QU2An#wlkJ&7RmTEMhyC_A8hWM54?s1WldCFuBmT5*I3K9=1aj= z6V@93P-lUou`xmB!ATp0(We$?)p*oQs;(Kku15~q9`-LSl{(Efm&@%(zj?aK2;5}P z{6<@-3^k^5FCDT@Z%XABEcuPoumYkiD&)-8z2Q}HO9OVEU3WM;V^$5r4q>h^m73XF z5!hZ7SCjfxDcXyj(({vg8FU(m2_}36L_yR>fnW)u=`1t@mPa76`2@%8v@2@$N@TE` z)kYhGY1jD;B9V=Dv1>BZhR9IJmB?X9Wj99f@MvJ2Fim*R`rsRilvz_3n!nPFLmj({EP!@CGkY5R*Y_dSO{qto~WerlG}DMw9k+n}pk z*nL~7R2gB{_9=zpqX|*vkU-dx)(j+83uvYGP?K{hr*j2pQsfXn<_As6z%-z+wFLqI zMhTkG>2M}#BLIOZ(ya1y8#W<+uUo@(43=^4@?CX{-hAuaJki(_A(uXD(>`lzuM~M;3XA48ZEN@HRV{1nvt?CV)t;|*dow0Ue2`B*iA&!rI`fZQ=b28= z_dxF}iUQ8}nq0SA4NK@^EQ%=)OY;3fC<$goJ&Kp|APQ@qVbS-MtJQBc)^aO8mYFsbhafeRKdHPW&s^&;%>v zlTz`YE}CuQ@_X&mqm{+{!h2r)fPGeM_Ge4RRYQkrma`&G<>RW<>S(?#LJ}O-t)d$< zf}b0svP^Zu@)MqwEV^Fb_j zPYYs~vmEC~cOIE6Nc^@b@nyL!w5o?nQ!$mGq(Pa|1-MD}K0si<&}eag=}WLSDO zE4+eA~!J(K}605x&4 zT72P7J^)Y)b(3g2MZ@1bv%o1ggwU4Yb!DhQ=uu-;vX+Ix8>#y6wgNKuobvrPNx?$3 zI{BbX<=Y-cBtvY&#MpGTgOLYU4W+csqWZx!=AVMb)Z;8%#1*x_(-)teF>45TCRwi1 z)Nn>hy3_lo44n-4A@=L2gI$yXCK0lPmMuldhLxR8aI;VrHIS{Dk}yp= zwjhB6v@0DN=Hnm~3t>`CtnPzvA*Kumfn5OLg&-m&fObRD};c}Hf?n&mS< z%$wztc%kjWjCf-?+q(bZh9k~(gs?i4`XVfqMXvPVkUWfm4+EBF(nOkg!}4u)6I)JT zU6IXqQk?p1a2(bz^S;6ZH3Wy9!JvbiSr7%c$#G1eK2^=~z1WX+VW)CPD#G~)13~pX zErO(>x$J_4qu-)lNlZkLj2}y$OiKn0ad5Imu5p-2dnt)(YI|b7rJ3TBUQ8FB8=&ym50*ibd2NAbj z;JA&hJ$AJlldM+tO;Yl3rBOFiP8fDdF?t(`gkRpmT9inR@uX{bThYNmxx-LN5K8h0 ztS%w*;V%b`%;-NARbNXn9he&AO4$rvmkB#;aaOx?Wk|yBCmN{oMTK&E)`s&APR<-5 z#;_e75z;LJ)gBG~h<^`SGmw<$Z3p`KG|I@7Pd)sTJnouZ1hRvm3}V+#lPGk4b&A#Y z4VSNi8(R1z7-t=L^%;*;iMTIAjrXl;h106hFrR{n9o8vlz?+*a1P{rEZ2ie{luQs} zr6t746>eoqiO5)^y;4H%2~&FT*Qc*9_oC2$+&syHWsA=rn3B~4#QEW zf4GT3i_@)f(Fj}gAZj`7205M8!B&HhmbgyZB& z+COyAVNxql#DwfP;H48Yc+Y~ChV6b9auLnfXXvpjr<~lQ@>VbCpQvWz=lyVf1??_c zAo3C^otZD@(v?X)UX*@w?TF|F8KF>l7%!Dzu+hksSA^akEkx8QD(V(lK+HBCw6C}2onVExW)f$ zncm*HI(_H;jF@)6eu}Tln!t?ynRkcqBA5MitIM@L^(4_Ke}vy7c%$w{(`&7Rn=u>oDM+Z^RUYcbSOPwT(ONyq76R>$V6_M_UP4vs=__I#io{{((| zy5=k=oVr-Qt$FImP~+&sN8rf2UH*vRMpwohPc@9?id17La4weIfBNa>1Djy+1=ugn z@}Zs;eFY1OC}WBDxDF=i=On_33(jWE-QYV)HbQ^VM!n>Ci9_W0Zofz7!m>do@KH;S z4k}FqEAU2)b%B_B-QcPnM5Zh=dQ+4|DJoJwo?)f2nWBuZE@^>a(gP~ObzMuyNJTgJFUPcH`%9UFA(P23iaKgo0)CI!SZ>35LpFaD7 z)C2sW$ltSEYNW%%j8F;yK{iHI2Q^}coF@LX`=EvxZb*_O;2Z0Z5 z7 zlccxmCfCI;_^awp|G748%Wx%?t9Sh8!V9Y(9$B?9R`G)Nd&snX1j+VpuQ@GGk=y(W zK|<$O`Cad`Y4#W3GKXgs%lZduAd1t1<7LwG4*zaStE*S)XXPFDyKdgiaVXG2)LvDn zf}eQ_S(&2!H0Mq1Yt&WpM1!7b#yt_ie7naOfX129_E=)beKj|p1VW9q>>+e$3@G$K zrB%i_TT1DHjOf7IQ8)Wu4#K%ZSCDGMP7Ab|Kvjq7*~@ewPm~h_-8d4jmNH<&mNZC@CI zKxG5O08|@<4(6IEC@L-lcrrvix&_Dj4tBvl=8A}2UX|)~v#V$L22U}UHk`B-1MF(t zU6aVJWR!>Y0@4m0UA%Sq9B5;4hZvsOu=>L`IU4#3r_t}os|vSDVMA??h>QJ1FD1vR z*@rclvfD!Iqoxh>VP+?b9TVH8g@KjYR@rRWQy44A`f6doIi+8VTP~pa%`(Oa@5?=h z8>YxNvA##a3D0)^P|2|+0~f|UsAJV=q(S>eq-dehQ+T>*Q@qN zU8@kdpU5gGk%ozt?%c8oM6neA?GuSsOfU_b1U)uiEP8eRn~>M$p*R z43nSZs@^ahO78s zulbK@@{3=2=@^yZ)DuIC$ki;`2WNbD_#`LOHN9iMsrgzt-T<8aeh z(oXrqI$Kgt6)Icu=?11NWs>{)_ed1wh>)wv6RYNUA-C&bejw{cBE_5Wzeo!AHdTd+ z)d(_IKN7z^n|As~3XS=cCB_TgM7rK;X586re`{~Foml$aKs zb!4Pe7hEP|370EWwn$HKPM!kL94UPZ1%8B^e5fB+=Iw^6=?5n3tZGYjov83CLB&OQ++p)WCMeshCv_9-~G9C_2x`LxTDjUcW$l6e!6-&a^fM3oP9*g(H zmCk0nGt1UMdU#pfg1G0um5|sc|KO<+qU1E4iBF~RvN*+`7uNHH^gu{?nw2DSCjig% zI@ymKZSK=PhHJa(jW&xeApv&JcfSmNJ4uQ|pY=Lcc>=J|{>5Ug3@x#R_b@55xFgfs za^ANzWdD$ZYtFs$d7+oiw0ZmPk2&l|< zc8()wfiJx@EGpQT zG$8iLkQZ-086doF1R zh<#9cz_vRsJdoXbD=QgOtpm}cFAJX8c}>Jew;PQJSXSb^;wlC zxXLHTS|!GZ-VK_4wV<9bk4RUmlsByzW_^b>)$6R+jQ}^wco1nMA`9Lncs;&QGp!`5Tx#aXXU?}5_RrtUY zx(EMzDhl-a^y^f5yfFLMnOO#u)l69&4M?|ne|2EV>zQ}4JQCBel?~2I4?D|>L$%H(peOOII!U}i z-j)*h1rODe9{0`xmhG;`AKqw1p0_KhEIU8)DoGnEn9wAhXPaxO_(jNSij~J5m$P*$ z9Mt(t;eV}2+i|kjQpBFcNb7_(VbuF<;RQB~R~p>2*Lg>a&7DEEuq*I%Ls4{zHeUDq z+M0&YhEn^C*9-B4Q7HJ$xj)dORCXPK+)ZtLOa0o&)Sl+f(Y{p*68$-#yagW5^HQnQ z0pWpoQpxg8<&gx9im(>=x6v#&RbQ7^AsjxeSDA? zi4MEJUC~ByG!PiBjq7$pK&FA^5 z=Y@dtQnuy%IfsaR`TVP0q^3mixl&J-3!$H!ua#{A>0Z1JdLq#d4UV9nlYm641ZHl zH6mK~iI6lR3OUEVL}Z5{ONZ_6{Nk%Bv03ag<1HVN?R%w2^aR5@E>6(r>}IoMl$wRF zWr-DItN*k7T$NTT8B)+23c?171sADhjInb2Xb>GhFYGC&3{b>huvLlaS4O z^{j5q+b5H?Z)yuy%AByaVl2yj9cnalY1sMQ zXI#e%*CLajxGxP!K6xf9RD2pMHOfAa1d^Lr6kE`IBpxOiGXfNcoQ*FI6wsNtLD!T+ zC4r2q>5qz0f}UY^RY#1^0*FPO*Zp-U1h9U|qWjwqJaDB(pZ`<`U-xo7+JB$zvwV}^ z2>$0&Q5k#l|Er7*PPG1ycj4BGz zg&`d*?nUi1Q!OB>{V@T$A;)8@h;*Rb1{xk_8X<34L`s}xkH-rQZvjM`jI=jaJRGRg zeEcjYChf-78|RLrao%4HyZBfnAx5KaE~@Sx+o-2MLJ>j-6uDb!U`odj*=)0k)K75l zo^)8-iz{_k7-_qy{Ko~N#B`n@o#A22YbKiA>0f3k=p-B~XX=`Ug>jl$e7>I=hph0&AK z?ya;(NaKY_!od=tFUcGU5Kwt!c9EPUQLi;JDCT*{90O@Wc>b| zI;&GIY$JlQW^9?R$-OEUG|3sp+hn+TL(YK?S@ZW<4PQa}=IcUAn_wW3d!r#$B}n08 z*&lf(YN21NDJ74DqwV`l`RX(4zJ<(E4D}N0@QaE-hnfdPDku~@yhb^AeZL73RgovX z6=e>!`&e^l@1WA5h!}}PwwL*Gjg!LbC5g0|qb8H$^S{eGs%cc?4vTyVFW=s6KtfW? z@&Xm+E(uz(qDbwDvRQI9DdB<2sW}FYK9sg*f%-i*>*n{t-_wXvg~N7gM|a91B!x|K zyLbJ~6!!JZpZ`#HpCB8g#Q*~VU47Rp$NyZb3WhEgg3ivSwnjGJgi0BEV?!H}Z@QF| zrO`Kx*52;FR#J-V-;`oR-pr!t>bYf)UYcixN=(FUR6$fhN@~i09^3WeP3*)D*`*mJ z1u%klAbzQ=P4s%|FnVTZv%|@(HDB+ap5S#cFSJUSGkyI*Y>9Lwx|0lTs%uhoCW(f1 zi+|a9;vDPfh3nS<7m~wqTM6+pEm(&z-Ll;lFH!w#(Uk#2>Iv~2Hu}lITn7hnOny`~ z*Vj=r<&Nwpq^@g5m`u&QTBRoK*}plAuHg$L$~NO#wF0!*r0OfcS%)k0A??uY*@B^C zJe9WdU(w){rTIf<;rwJt^_35^d<A@$FqEZW6kwyfAo2x0T$Ye2MZox6Z7<%Qbu$}}u{rtE+h2M+Z}T4I zxF1cwJ(Uvp!T#mogWkhb(?SxD4_#tV(Sc8N4Gu*{Fh#})Pvb^ef%jrlnG*&Ie+J5 zsly5oo?1((um&lLDxn(DkYtk`My>lgKTp3Y4?hTQ4_`YNOFtjF-FUY#d#(EQd(rfz zB8z%Vi;?x)ZM$3c>yc5H8KBvSevnWNdCbAj?QCac)6-K~Xz@EZp}~N9q)5*Ufjz3C z6kkOeI{3H(^VO8hKDrVjy2DXd;5wr4nb`19yJi0DO@607MSx+7F$ zz3F7sl8JV@@sM$6`#JmSilqI%Bs)}Py2eFT;TjcG5?8$zwV60b(_5A>b#uk~7U^bO z>y|6SCrP2IGST(8HFuX|XQUXPLt2gL_hm|uj1Ws`O2VW>SyL^uXkl>Zvkcpi?@!F7 z%svLoT@{R#XrIh^*dE~$YhMwC+b7JE09NAS47kT%Ew zD!XjxA@1+KOAyu`H2z#h+pGm!lG>WI0v745l+Fd><3dh{ATq%h?JSdEt zu%J*zfFUx%Tx&0DS5WSbE)vwZSoAGT=;W#(DoiL($BcK;U*w`xA&kheyMLI673HCb7fGkp{_vdV2uo;vSoAH z9BuLM#Vzwt#rJH>58=KXa#O;*)_N{$>l7`umacQ0g$pI3iW4=L--O;Wiq0zy7OKp`j2r^y3`7X!?sq9rr5B{41BkBr1fEd1#Q3 z-dXc2RSb4U>FvpVhlQCIzQ-hs=8420z=7F2F(^xD;^RXgpjlh8S6*xCP#Gj2+Q0bAg?XARw3dnlQ*Lz3vk}m`HXmCgN=?bIL{T zi}Ds-xn|P)dxhraT@XY$ZQ&^%x8y!o+?n#+>+dZ1c{hYwNTNRke@3enT(a@}V*X{! z81+{Jc2UR;+Zcbc6cUlafh4DFKwp>;M}8SGD+YnW3Q_)*9Z_pny_z+MeYQmz?r%EVaN0d!NE*FVPq&U@vo{ef6wkMIDEWLbDs zz91$($XbGnQ?4WHjB~4xgPgKZts{p|g1B{-4##}#c5aL5C6_RJ_(*5>85B1}U!_<``}q-97Q7~u)(&lsb(WT^(*n7H%33%@_b zO5(?-v??s??33b19xiB7t_YT!q8!qAzN1#RD@3;kYAli%kazt#YN7}MhVu=ljuz27 z1`<+g8oVwy57&$`CiHeaM)tz(OSt4E# zJ@P6E*e504oUw~RD(=9WP8QdW^6wRdFbKII!GAWecJ(?{`EzTR@?j!3g?$@LLCt;U={>!9z7DU!(1Jq zqEwdx5q?W1Ncm7mXP8MFwAr?nw5$H%cb>Q><9j{Tk2RY9ngGvaJgWXx^r!ywk{ph- zs2PFto4@IIwBh{oXe;yMZJYlS?3%a-CJ#js90hoh5W5d^OMwCFmpryHFr|mG+*ZP$ zqyS5BW@s}|3xUO0PR<^{a2M(gkP5BDGxvkWkPudSV*TMRK5Qm4?~VuqVAOerffRt$HGAvp;M++Iq$E6alB z;ykBr-eZ6v_H^1Wip56Czj&=`mb^TsX|FPN#-gnlP03AkiJDM=?y|LzER1M93R4sC z*HT(;EV=*F*>!+Z{r!KG?6ODMGvkt3viG=@kQJHNMYd}bS4KrrHf4`&*(0m0R5Hqz zEk)r=sFeS?MZRvn<@Z0&bDw)XkMnw+_xqgp=W{;ioX`6;G-P9N%wfoYJ$-m$L#MC% z^sH?tSzA|WWP(cN3({~_*X$l{M*;1V{l$;T6b){#l4pswDTid26HaXgKed}13YIP= zJRvA3nmx{}R$Lr&S4!kWU3`~dxM}>VXWu6Xd(VP}z1->h&f%82eXD_TuTs@=c;l0T z|LHmWKJ+?7hkY=YM>t}zvb4|lV;!ARMtWFp!E^J=Asu9w&kVF*i{T#}sY++-qnVh! z5TQ|=>)+vutf{&qB+LO9^jm#rD7E5+tcorr^Fn5Xb0B;)f^$7Ev#}G_`r==ea294V z--v4LwjswWlSq9ba6i?IXr8M_VEGQ$H%hCqJTFQ3+1B9tmxDUhnNU%dy4+zbqYJ|o z3!N{b?A@{;cG2~nb-`|z;gEDL5ffF@oc3`R{fGi)0wtMqEkw4tRX3t;LVS3-zAmg^ zgL7Z{hmdPSz9oA@t>tZ1<|Khn&Lp=_!Q=@a?k+t~H&3jN?dr(}7s;{L+jiKY57?WsFBfW^mu6a03_^VKrdK=9egXw@!nzZ3TbYc*osyQNoCXPYoFS<&Nr97MrQCOK(gO8 z;0@iqRTJy4-RH)PJld5`AJN}n?5r^-enKrHQOR;z>UMfm+e8~4ZL5k>oXMiYq12Bx4eVQv0jFgp_zC#``sjZpywYqISMP}VZ@!~1Mf$!x|opj%mQ98JnSk@`~ zPmmyuPZKtZOnEC!1y!?`TYRsZ!II;d!iln}%e}bk5qIiUADERr*K$3dekgHV9TtBX zi5q!J!6Zgd#cLxRmZN^J`o@Zv{+p+<_#8^nvY)44Hw_2i@?R&5n^q33fpOnDg1nPQ z_r<$hURl~OketX|Tdbvf_7=3x^rSFJtEp@tuDpVB&uq)qW;xUQ7mmkr-@eZwa$l+? zoKk``Vz@TH#>jMce*8>@FZ+@BEUdYa_K0i|{*;j9MW3K%pnM*T;@>|o@lMhgLrpZP5aol(z>g;b4}|e$U~Fn zGL%(}p%Jsl4LxE!VW_Y4T>e}W4e#~F03H_^R!Q)kpJG{lO!@I4{mFo^V#ayHh_5~o zB$O71gcE(G@6xv);#Ky?e(Ed}^O+Ho(t=93T9T3TnEY(OVf_dR-gY@jj+iJSY?q|6prBv(S9A4k=2fNZz!W@S=B@~b?TJRTuBQq448@juN#Y=3q=^VCF>Z}n6wICJ<^^Kn8C;mK zZYiFSN#Z$?NDGV7(#}q2tAZAtE63icK-MY>UQu4MWlGIbJ$AF8Zt-jV;@7P5MPI>% zPWvO!t%1+s>-A%`;0^o8Ezeaa4DMwI8ooQrJ;ax@Qt*6XONWw)dPwOPI9@u*EG&844*1~EoZ2qsAe~M>d`;Bc_CWY zMoDKEmDh-}k9d6*<0g@aQmsnrM1H9IcKYZs)><)d92{|0Hh8?~XbF)7U+UmP@Pw_6geVB?7N$4J4*E0z3EO&5kRS(EE zv92(+e5WxLXMN{h;-|8@!Q#0q247hb^3R%*k3MuMO5*L}$0D#5P*N$aHd54C+=_RToYXTyewugOaDmGsCvb4H1s=@gkfVnzTCWKMa-Mm1v4Wq!t-JIrbV&EWwKDe ze#kJpOq#iRlFz%5#6Fio9IUlKnQ#X&DY8Ux#<-WqxAac-y%U_L+EZZ4Rg5*yNg`f< zSZn&uio@zanUCPqX1l4W&B!;UWs#P7B^|4WwoCxQXl|44n^cBNqu=3Vl*ltAqsUQO z9q_@nD0zq0O8r`coEm>9+|rA3HL#l}X;0##>SJS$cVavOZVCpSGf4mUU1( zWaRCUYc^9QbG9=vpWo%xP}CMFnMb{reA`K7tT(t5DM)d9l}jVPY>qoRzT zE3m-p#=i=$9x*CB`AL>SY}u3agYFl#uULNen#&44H;!L@I{RI=PlWxG8J((f)ma7A z@jLvQ>?Nx`n?3ChRG#HqE3MXP8*o3!Qq`+t8EMt_p)oeKHqPusBxPn!#?R??-=e3e zo73WNs_IZF`WLigre=|`aS2^> zN1zn!7k&Dh28t%VpJ%**&E!eAcB5oLjQFFcJQj*URMia%Ya3@q1UQ18=oWMM6`I}iT_&L1gl?*~6nU4q4Z0`H<5yDp(HeZ+RGf9`mM&= zn-qRp%i!g$R;i1d1aMZ{IewNjE@p2+Z{`x{*xL*x$?WV~{BjJpsP&C&JK0HLoyf z`0z^v&fBQSa!I7FU~9MaQ%e|?RP>sM^2PL!mE^Q1Ig_4M$5BRfi72oMYu6Ke?wmDX z@0a%-V|z}b23K=ye(W+fG#w|jJUnT{=KR5jfuq!RX}<1irTDw(${<&}dWQu4;EuE< z@3u4dBkQaCHHM&;cE0z50_V!(vJ1_V)A8?C#eJuLkt!98Z%|Bgzidc0j|z(&o)TCzYlrgZA zC3@i>L!&Gw_~7`>puB97I2lK)lESZQqVXc_8T^G2O#VHhO?IC$g zOYhXJ7)~C<8l|Xrftka@QuowScM{K&0zskoU$Aw~vIRVRF9TEQ4*3=_5)98B`=t8(N%ZuWqmwlW zllAzq=E5_5!sKDXam@w`ZD(nl%LAPxQuEtDcKPqu9LPJvNIITawU#c^PQ2HmZgs)r zH^+gRwZ?0)8IFQgU)+p@0Iqb^tcEoqcB@zhfz_FaOM&_d<|jnU>q5nSKa<@%9|dje zIupcg1!tRiMP4X=oG<7s4|AW&^-Cw4FL9OuI$t zxjc*y;Uw!G7a|jz>E*2+PlR(CemWebS7m-&*CDwnmxbiRqJvQ&os-sC&4OWt^(2@vG4|jui#Df@-D= zh3D%8Y3R6+jRBStSvH9pt&tCI`NK08J1*pC(?OM0h!bS-JK3I}`pDY-fDIaB_*W6KS+TO0Q*%kkeuN6uWITt=TsCGw6uBE710q; zRluI%j{?@jwhM|l5&TB!-TkQs!A=DXRE>u18t@;zndD0M$U@Igrt?UW2; z7%=dsHIVH_LCkGUU0fW&UMjDnvjcc0Mp(mK&;d~ZJ5EJ)#7@aTZvGDFXzFZg2Lq~s z5PR_LazNN)JD5K_uK*Hy{mXuHTkGGv|9V8KP#iQ$3!G*^>7UiE{|1G1A-qg(xH;Xa>&%f|BZkH zG=J^0pHzSAqv5*5ysQ{Puy^-_|IPrii zKS$mE10Zngf>Sgg@BjpRyJbrHeo zD8Ro0LI*W#+9?^xlOS^c>Z^^n^0I|FH^@^`ZR`{H=$ zjO0_$cnpBM7Zcm?H_RXIu-Lu~qweDSV|tEZBZh!e6hQy->}e;d#osZ1hQj{HhHkC0 zJ|F-HKmeTGgDe979ogBz24;@<|I7;TU!IXb@oWMsMECIETmQy`zPtM`|NP}PjzR_u zKMG1Z{%1kWeMfEf(10U#w!clmQ2)JC8zm(Fv!H4dUHQHCFLikID?hrd{0>kCQt?kP zdqn2ZG0}ytcQJ7t_B3s0ZvH3PYjkjQ`Q%;jV@?MK-+z3etBCGGo4f4`y^|AdCs!DH zThTQ;cL5dM{|tB_1y6K3bVa^hx_<9J(}5`2SDz1^0bT!Vm*JV;9~t&{IC{$DUAVV* z{|E=#yN{wNdTY@$6z{_KNA3&%w|vFu1n9XRcM0Ak>`UW!lQ`ah3D4r%}Z diff --git a/gradle-plugins/gradle/wrapper/gradle-wrapper.properties b/gradle-plugins/gradle/wrapper/gradle-wrapper.properties index 37aef8d3f0c9..8e876e1c5571 100644 --- a/gradle-plugins/gradle/wrapper/gradle-wrapper.properties +++ b/gradle-plugins/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,8 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-bin.zip +distributionSha256Sum=1541fa36599e12857140465f3c91a97409b4512501c26f9631fb113e392c5bd1 +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.1-bin.zip networkTimeout=10000 +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradle-plugins/gradlew b/gradle-plugins/gradlew index 79a61d421cc4..f5feea6d6b11 100755 --- a/gradle-plugins/gradlew +++ b/gradle-plugins/gradlew @@ -15,6 +15,8 @@ # See the License for the specific language governing permissions and # limitations under the License. # +# SPDX-License-Identifier: Apache-2.0 +# ############################################################################## # @@ -55,7 +57,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. @@ -83,10 +85,9 @@ done # This is normally unused # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} -APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit - -# 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"' +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s +' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum @@ -133,10 +134,13 @@ 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. + if ! command -v java >/dev/null 2>&1 + then + 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 fi # Increase the maximum file descriptors if we can. @@ -144,7 +148,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 + # shellcheck disable=SC2039,SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac @@ -152,7 +156,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then '' | soft) :;; #( *) # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 + # shellcheck disable=SC2039,SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac @@ -197,11 +201,15 @@ if "$cygwin" || "$msys" ; then done fi -# Collect all arguments for the java command; -# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of -# shell script including quotes and variable substitutions, so put them in -# double quotes to make sure that they get re-expanded; and -# * put everything else in single quotes, so that it's not re-expanded. + +# 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"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ diff --git a/gradle-plugins/gradlew.bat b/gradle-plugins/gradlew.bat index 93e3f59f135d..9d21a21834d5 100644 --- a/gradle-plugins/gradlew.bat +++ b/gradle-plugins/gradlew.bat @@ -13,6 +13,8 @@ @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem @if "%DEBUG%"=="" @echo off @rem ########################################################################## @@ -43,11 +45,11 @@ set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if %ERRORLEVEL% equ 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. +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail @@ -57,11 +59,11 @@ 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. +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail diff --git a/gradle-plugins/settings.gradle.kts b/gradle-plugins/settings.gradle.kts index 64dafcc81018..7772973bf0f5 100644 --- a/gradle-plugins/settings.gradle.kts +++ b/gradle-plugins/settings.gradle.kts @@ -1,10 +1,10 @@ pluginManagement { plugins { - id("com.gradle.plugin-publish") version "1.2.0" - id("io.github.gradle-nexus.publish-plugin") version "1.3.0" + id("com.gradle.plugin-publish") version "1.2.2" + id("io.github.gradle-nexus.publish-plugin") version "2.0.0" } } plugins { - id("org.gradle.toolchains.foojay-resolver-convention") version "0.5.0" + id("org.gradle.toolchains.foojay-resolver-convention") version "0.8.0" } diff --git a/gradle-plugins/src/main/kotlin/io.opentelemetry.instrumentation.muzzle-check.gradle.kts b/gradle-plugins/src/main/kotlin/io.opentelemetry.instrumentation.muzzle-check.gradle.kts index 23f5b5ed1fcc..c010f21568fa 100644 --- a/gradle-plugins/src/main/kotlin/io.opentelemetry.instrumentation.muzzle-check.gradle.kts +++ b/gradle-plugins/src/main/kotlin/io.opentelemetry.instrumentation.muzzle-check.gradle.kts @@ -28,8 +28,7 @@ import java.util.stream.StreamSupport plugins { `java-library` - - id("com.github.johnrengelman.shadow") + id("com.gradleup.shadow") } // Select a random set of versions to test @@ -85,22 +84,24 @@ tasks.withType().configureEach { // rewrite dependencies calling Logger.getLogger relocate("java.util.logging.Logger", "io.opentelemetry.javaagent.bootstrap.PatchLogger") - // prevents conflict with library instrumentation, since these classes live in the bootstrap class loader - relocate("io.opentelemetry.instrumentation", "io.opentelemetry.javaagent.shaded.instrumentation") { - // Exclude resource providers since they live in the agent class loader - exclude("io.opentelemetry.instrumentation.resources.*") - exclude("io.opentelemetry.instrumentation.spring.resources.*") - } + if (project.findProperty("disableShadowRelocate") != "true") { + // prevents conflict with library instrumentation, since these classes live in the bootstrap class loader + relocate("io.opentelemetry.instrumentation", "io.opentelemetry.javaagent.shaded.instrumentation") { + // Exclude resource providers since they live in the agent class loader + exclude("io.opentelemetry.instrumentation.resources.*") + exclude("io.opentelemetry.instrumentation.spring.resources.*") + } - // relocate(OpenTelemetry API) since these classes live in the bootstrap class loader - 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(OpenTelemetry API) since these classes live in the bootstrap class loader + 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.contrib.awsxray", "io.opentelemetry.javaagent.shaded.io.opentelemetry.contrib.awsxray") relocate("io.opentelemetry.extension.kotlin", "io.opentelemetry.javaagent.shaded.io.opentelemetry.extension.kotlin") // this is for instrumentation of opentelemetry-api and opentelemetry-instrumentation-api @@ -229,8 +230,8 @@ fun newRepositorySystem(): RepositorySystem { } fun newRepositorySystemSession(system: RepositorySystem): RepositorySystemSession { - val muzzleRepo = file("${buildDir}/muzzleRepo") - val localRepo = LocalRepository(muzzleRepo) + val muzzleRepo = layout.buildDirectory.dir("muzzleRepo") + val localRepo = LocalRepository(muzzleRepo.get().asFile) return MavenRepositorySystemUtils.newSession().apply { localRepositoryManager = system.newLocalRepositoryManager(this, localRepo) } diff --git a/gradle-plugins/src/main/kotlin/io.opentelemetry.instrumentation.muzzle-generation.gradle.kts b/gradle-plugins/src/main/kotlin/io.opentelemetry.instrumentation.muzzle-generation.gradle.kts index 634ff0c1a954..fc5fa78fb3bc 100644 --- a/gradle-plugins/src/main/kotlin/io.opentelemetry.instrumentation.muzzle-generation.gradle.kts +++ b/gradle-plugins/src/main/kotlin/io.opentelemetry.instrumentation.muzzle-generation.gradle.kts @@ -67,6 +67,7 @@ fun createLanguageTask( classFileVersion = ClassFileVersion.JAVA_V8 var transformationClassPath = inputClasspath val compileTask = compileTaskProvider.get() + // this does not work for kotlin as compile task does not extend AbstractCompile if (compileTask is AbstractCompile) { val classesDirectory = compileTask.destinationDirectory.asFile.get() val rawClassesDirectory: File = File(classesDirectory.parent, "${classesDirectory.name}raw") diff --git a/gradle-plugins/src/main/kotlin/io/opentelemetry/javaagent/muzzle/AcceptableVersions.kt b/gradle-plugins/src/main/kotlin/io/opentelemetry/javaagent/muzzle/AcceptableVersions.kt index fa43a08e7405..6271152efb47 100644 --- a/gradle-plugins/src/main/kotlin/io/opentelemetry/javaagent/muzzle/AcceptableVersions.kt +++ b/gradle-plugins/src/main/kotlin/io/opentelemetry/javaagent/muzzle/AcceptableVersions.kt @@ -33,6 +33,7 @@ class AcceptableVersions(private val skipVersions: Collection) : || versionString.contains("public_draft") || versionString.contains("snapshot") || versionString.contains("test") + || versionString.startsWith("0.0.0-") || GIT_SHA_PATTERN.matches(versionString) || DATETIME_PATTERN.matches(versionString) return !draftVersion diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index ccebba7710deaf9f98673a68957ea02138b60d0a..a4b76b9530d66f5e68d973ea569d8e19de379189 100644 GIT binary patch literal 43583 zcma&N1CXTcmMvW9vTb(Rwr$&4wr$(C?dmSu>@vG-+vuvg^_??!{yS%8zW-#zn-LkA z5&1^$^{lnmUON?}LBF8_K|(?T0Ra(xUH{($5eN!MR#ZihR#HxkUPe+_R8Cn`RRs(P z_^*#_XlXmGv7!4;*Y%p4nw?{bNp@UZHv1?Um8r6)Fei3p@ClJn0ECfg1hkeuUU@Or zDaPa;U3fE=3L}DooL;8f;P0ipPt0Z~9P0)lbStMS)ag54=uL9ia-Lm3nh|@(Y?B`; zx_#arJIpXH!U{fbCbI^17}6Ri*H<>OLR%c|^mh8+)*h~K8Z!9)DPf zR2h?lbDZQ`p9P;&DQ4F0sur@TMa!Y}S8irn(%d-gi0*WxxCSk*A?3lGh=gcYN?FGl z7D=Js!i~0=u3rox^eO3i@$0=n{K1lPNU zwmfjRVmLOCRfe=seV&P*1Iq=^i`502keY8Uy-WNPwVNNtJFx?IwAyRPZo2Wo1+S(xF37LJZ~%i)kpFQ3Fw=mXfd@>%+)RpYQLnr}B~~zoof(JVm^^&f zxKV^+3D3$A1G;qh4gPVjhrC8e(VYUHv#dy^)(RoUFM?o%W-EHxufuWf(l*@-l+7vt z=l`qmR56K~F|v<^Pd*p~1_y^P0P^aPC##d8+HqX4IR1gu+7w#~TBFphJxF)T$2WEa zxa?H&6=Qe7d(#tha?_1uQys2KtHQ{)Qco)qwGjrdNL7thd^G5i8Os)CHqc>iOidS} z%nFEDdm=GXBw=yXe1W-ShHHFb?Cc70+$W~z_+}nAoHFYI1MV1wZegw*0y^tC*s%3h zhD3tN8b=Gv&rj}!SUM6|ajSPp*58KR7MPpI{oAJCtY~JECm)*m_x>AZEu>DFgUcby z1Qaw8lU4jZpQ_$;*7RME+gq1KySGG#Wql>aL~k9tLrSO()LWn*q&YxHEuzmwd1?aAtI zBJ>P=&$=l1efe1CDU;`Fd+_;&wI07?V0aAIgc(!{a z0Jg6Y=inXc3^n!U0Atk`iCFIQooHqcWhO(qrieUOW8X(x?(RD}iYDLMjSwffH2~tB z)oDgNBLB^AJBM1M^c5HdRx6fBfka`(LD-qrlh5jqH~);#nw|iyp)()xVYak3;Ybik z0j`(+69aK*B>)e_p%=wu8XC&9e{AO4c~O1U`5X9}?0mrd*m$_EUek{R?DNSh(=br# z#Q61gBzEpmy`$pA*6!87 zSDD+=@fTY7<4A?GLqpA?Pb2z$pbCc4B4zL{BeZ?F-8`s$?>*lXXtn*NC61>|*w7J* z$?!iB{6R-0=KFmyp1nnEmLsA-H0a6l+1uaH^g%c(p{iT&YFrbQ$&PRb8Up#X3@Zsk zD^^&LK~111%cqlP%!_gFNa^dTYT?rhkGl}5=fL{a`UViaXWI$k-UcHJwmaH1s=S$4 z%4)PdWJX;hh5UoK?6aWoyLxX&NhNRqKam7tcOkLh{%j3K^4Mgx1@i|Pi&}<^5>hs5 zm8?uOS>%)NzT(%PjVPGa?X%`N2TQCKbeH2l;cTnHiHppPSJ<7y-yEIiC!P*ikl&!B z%+?>VttCOQM@ShFguHVjxX^?mHX^hSaO_;pnyh^v9EumqSZTi+#f&_Vaija0Q-e*| z7ulQj6Fs*bbmsWp{`auM04gGwsYYdNNZcg|ph0OgD>7O}Asn7^Z=eI>`$2*v78;sj-}oMoEj&@)9+ycEOo92xSyY344^ z11Hb8^kdOvbf^GNAK++bYioknrpdN>+u8R?JxG=!2Kd9r=YWCOJYXYuM0cOq^FhEd zBg2puKy__7VT3-r*dG4c62Wgxi52EMCQ`bKgf*#*ou(D4-ZN$+mg&7$u!! z-^+Z%;-3IDwqZ|K=ah85OLwkO zKxNBh+4QHh)u9D?MFtpbl)us}9+V!D%w9jfAMYEb>%$A;u)rrI zuBudh;5PN}_6J_}l55P3l_)&RMlH{m!)ai-i$g)&*M`eN$XQMw{v^r@-125^RRCF0 z^2>|DxhQw(mtNEI2Kj(;KblC7x=JlK$@78`O~>V!`|1Lm-^JR$-5pUANAnb(5}B}JGjBsliK4& zk6y(;$e&h)lh2)L=bvZKbvh@>vLlreBdH8No2>$#%_Wp1U0N7Ank!6$dFSi#xzh|( zRi{Uw%-4W!{IXZ)fWx@XX6;&(m_F%c6~X8hx=BN1&q}*( zoaNjWabE{oUPb!Bt$eyd#$5j9rItB-h*5JiNi(v^e|XKAj*8(k<5-2$&ZBR5fF|JA z9&m4fbzNQnAU}r8ab>fFV%J0z5awe#UZ|bz?Ur)U9bCIKWEzi2%A+5CLqh?}K4JHi z4vtM;+uPsVz{Lfr;78W78gC;z*yTch~4YkLr&m-7%-xc ztw6Mh2d>_iO*$Rd8(-Cr1_V8EO1f*^@wRoSozS) zy1UoC@pruAaC8Z_7~_w4Q6n*&B0AjOmMWa;sIav&gu z|J5&|{=a@vR!~k-OjKEgPFCzcJ>#A1uL&7xTDn;{XBdeM}V=l3B8fE1--DHjSaxoSjNKEM9|U9#m2<3>n{Iuo`r3UZp;>GkT2YBNAh|b z^jTq-hJp(ebZh#Lk8hVBP%qXwv-@vbvoREX$TqRGTgEi$%_F9tZES@z8Bx}$#5eeG zk^UsLBH{bc2VBW)*EdS({yw=?qmevwi?BL6*=12k9zM5gJv1>y#ML4!)iiPzVaH9% zgSImetD@dam~e>{LvVh!phhzpW+iFvWpGT#CVE5TQ40n%F|p(sP5mXxna+Ev7PDwA zamaV4m*^~*xV+&p;W749xhb_X=$|LD;FHuB&JL5?*Y2-oIT(wYY2;73<^#46S~Gx| z^cez%V7x$81}UWqS13Gz80379Rj;6~WdiXWOSsdmzY39L;Hg3MH43o*y8ibNBBH`(av4|u;YPq%{R;IuYow<+GEsf@R?=@tT@!}?#>zIIn0CoyV!hq3mw zHj>OOjfJM3F{RG#6ujzo?y32m^tgSXf@v=J$ELdJ+=5j|=F-~hP$G&}tDZsZE?5rX ztGj`!S>)CFmdkccxM9eGIcGnS2AfK#gXwj%esuIBNJQP1WV~b~+D7PJTmWGTSDrR` zEAu4B8l>NPuhsk5a`rReSya2nfV1EK01+G!x8aBdTs3Io$u5!6n6KX%uv@DxAp3F@{4UYg4SWJtQ-W~0MDb|j-$lwVn znAm*Pl!?Ps&3wO=R115RWKb*JKoexo*)uhhHBncEDMSVa_PyA>k{Zm2(wMQ(5NM3# z)jkza|GoWEQo4^s*wE(gHz?Xsg4`}HUAcs42cM1-qq_=+=!Gk^y710j=66(cSWqUe zklbm8+zB_syQv5A2rj!Vbw8;|$@C!vfNmNV!yJIWDQ>{+2x zKjuFX`~~HKG~^6h5FntRpnnHt=D&rq0>IJ9#F0eM)Y-)GpRjiN7gkA8wvnG#K=q{q z9dBn8_~wm4J<3J_vl|9H{7q6u2A!cW{bp#r*-f{gOV^e=8S{nc1DxMHFwuM$;aVI^ zz6A*}m8N-&x8;aunp1w7_vtB*pa+OYBw=TMc6QK=mbA-|Cf* zvyh8D4LRJImooUaSb7t*fVfih<97Gf@VE0|z>NcBwBQze);Rh!k3K_sfunToZY;f2 z^HmC4KjHRVg+eKYj;PRN^|E0>Gj_zagfRbrki68I^#~6-HaHg3BUW%+clM1xQEdPYt_g<2K+z!$>*$9nQ>; zf9Bei{?zY^-e{q_*|W#2rJG`2fy@{%6u0i_VEWTq$*(ZN37|8lFFFt)nCG({r!q#9 z5VK_kkSJ3?zOH)OezMT{!YkCuSSn!K#-Rhl$uUM(bq*jY? zi1xbMVthJ`E>d>(f3)~fozjg^@eheMF6<)I`oeJYx4*+M&%c9VArn(OM-wp%M<-`x z7sLP1&3^%Nld9Dhm@$3f2}87!quhI@nwd@3~fZl_3LYW-B?Ia>ui`ELg z&Qfe!7m6ze=mZ`Ia9$z|ARSw|IdMpooY4YiPN8K z4B(ts3p%2i(Td=tgEHX z0UQ_>URBtG+-?0E;E7Ld^dyZ;jjw0}XZ(}-QzC6+NN=40oDb2^v!L1g9xRvE#@IBR zO!b-2N7wVfLV;mhEaXQ9XAU+>=XVA6f&T4Z-@AX!leJ8obP^P^wP0aICND?~w&NykJ#54x3_@r7IDMdRNy4Hh;h*!u(Ol(#0bJdwEo$5437-UBjQ+j=Ic>Q2z` zJNDf0yO6@mr6y1#n3)s(W|$iE_i8r@Gd@!DWDqZ7J&~gAm1#~maIGJ1sls^gxL9LLG_NhU!pTGty!TbhzQnu)I*S^54U6Yu%ZeCg`R>Q zhBv$n5j0v%O_j{QYWG!R9W?5_b&67KB$t}&e2LdMvd(PxN6Ir!H4>PNlerpBL>Zvyy!yw z-SOo8caEpDt(}|gKPBd$qND5#a5nju^O>V&;f890?yEOfkSG^HQVmEbM3Ugzu+UtH zC(INPDdraBN?P%kE;*Ae%Wto&sgw(crfZ#Qy(<4nk;S|hD3j{IQRI6Yq|f^basLY; z-HB&Je%Gg}Jt@={_C{L$!RM;$$|iD6vu#3w?v?*;&()uB|I-XqEKqZPS!reW9JkLewLb!70T7n`i!gNtb1%vN- zySZj{8-1>6E%H&=V}LM#xmt`J3XQoaD|@XygXjdZ1+P77-=;=eYpoEQ01B@L*a(uW zrZeZz?HJsw_4g0vhUgkg@VF8<-X$B8pOqCuWAl28uB|@r`19DTUQQsb^pfqB6QtiT z*`_UZ`fT}vtUY#%sq2{rchyfu*pCg;uec2$-$N_xgjZcoumE5vSI{+s@iLWoz^Mf; zuI8kDP{!XY6OP~q5}%1&L}CtfH^N<3o4L@J@zg1-mt{9L`s^z$Vgb|mr{@WiwAqKg zp#t-lhrU>F8o0s1q_9y`gQNf~Vb!F%70f}$>i7o4ho$`uciNf=xgJ>&!gSt0g;M>*x4-`U)ysFW&Vs^Vk6m%?iuWU+o&m(2Jm26Y(3%TL; zA7T)BP{WS!&xmxNw%J=$MPfn(9*^*TV;$JwRy8Zl*yUZi8jWYF>==j~&S|Xinsb%c z2?B+kpet*muEW7@AzjBA^wAJBY8i|#C{WtO_or&Nj2{=6JTTX05}|H>N2B|Wf!*3_ z7hW*j6p3TvpghEc6-wufFiY!%-GvOx*bZrhZu+7?iSrZL5q9}igiF^*R3%DE4aCHZ zqu>xS8LkW+Auv%z-<1Xs92u23R$nk@Pk}MU5!gT|c7vGlEA%G^2th&Q*zfg%-D^=f z&J_}jskj|Q;73NP4<4k*Y%pXPU2Thoqr+5uH1yEYM|VtBPW6lXaetokD0u z9qVek6Q&wk)tFbQ8(^HGf3Wp16gKmr>G;#G(HRBx?F`9AIRboK+;OfHaLJ(P>IP0w zyTbTkx_THEOs%Q&aPrxbZrJlio+hCC_HK<4%f3ZoSAyG7Dn`=X=&h@m*|UYO-4Hq0 z-Bq&+Ie!S##4A6OGoC~>ZW`Y5J)*ouaFl_e9GA*VSL!O_@xGiBw!AF}1{tB)z(w%c zS1Hmrb9OC8>0a_$BzeiN?rkPLc9%&;1CZW*4}CDDNr2gcl_3z+WC15&H1Zc2{o~i) z)LLW=WQ{?ricmC`G1GfJ0Yp4Dy~Ba;j6ZV4r{8xRs`13{dD!xXmr^Aga|C=iSmor% z8hi|pTXH)5Yf&v~exp3o+sY4B^^b*eYkkCYl*T{*=-0HniSA_1F53eCb{x~1k3*`W zr~};p1A`k{1DV9=UPnLDgz{aJH=-LQo<5%+Em!DNN252xwIf*wF_zS^!(XSm(9eoj z=*dXG&n0>)_)N5oc6v!>-bd(2ragD8O=M|wGW z!xJQS<)u70m&6OmrF0WSsr@I%T*c#Qo#Ha4d3COcX+9}hM5!7JIGF>7<~C(Ear^Sn zm^ZFkV6~Ula6+8S?oOROOA6$C&q&dp`>oR-2Ym3(HT@O7Sd5c~+kjrmM)YmgPH*tL zX+znN>`tv;5eOfX?h{AuX^LK~V#gPCu=)Tigtq9&?7Xh$qN|%A$?V*v=&-2F$zTUv z`C#WyIrChS5|Kgm_GeudCFf;)!WH7FI60j^0o#65o6`w*S7R@)88n$1nrgU(oU0M9 zx+EuMkC>(4j1;m6NoGqEkpJYJ?vc|B zOlwT3t&UgL!pX_P*6g36`ZXQ; z9~Cv}ANFnJGp(;ZhS(@FT;3e)0)Kp;h^x;$*xZn*k0U6-&FwI=uOGaODdrsp-!K$Ac32^c{+FhI-HkYd5v=`PGsg%6I`4d9Jy)uW0y%) zm&j^9WBAp*P8#kGJUhB!L?a%h$hJgQrx!6KCB_TRo%9{t0J7KW8!o1B!NC)VGLM5! zpZy5Jc{`r{1e(jd%jsG7k%I+m#CGS*BPA65ZVW~fLYw0dA-H_}O zrkGFL&P1PG9p2(%QiEWm6x;U-U&I#;Em$nx-_I^wtgw3xUPVVu zqSuKnx&dIT-XT+T10p;yjo1Y)z(x1fb8Dzfn8e yu?e%!_ptzGB|8GrCfu%p?(_ zQccdaaVK$5bz;*rnyK{_SQYM>;aES6Qs^lj9lEs6_J+%nIiuQC*fN;z8md>r_~Mfl zU%p5Dt_YT>gQqfr@`cR!$NWr~+`CZb%dn;WtzrAOI>P_JtsB76PYe*<%H(y>qx-`Kq!X_; z<{RpAqYhE=L1r*M)gNF3B8r(<%8mo*SR2hu zccLRZwGARt)Hlo1euqTyM>^!HK*!Q2P;4UYrysje@;(<|$&%vQekbn|0Ruu_Io(w4#%p6ld2Yp7tlA`Y$cciThP zKzNGIMPXX%&Ud0uQh!uQZz|FB`4KGD?3!ND?wQt6!n*f4EmCoJUh&b?;B{|lxs#F- z31~HQ`SF4x$&v00@(P+j1pAaj5!s`)b2RDBp*PB=2IB>oBF!*6vwr7Dp%zpAx*dPr zb@Zjq^XjN?O4QcZ*O+8>)|HlrR>oD*?WQl5ri3R#2?*W6iJ>>kH%KnnME&TT@ZzrHS$Q%LC?n|e>V+D+8D zYc4)QddFz7I8#}y#Wj6>4P%34dZH~OUDb?uP%-E zwjXM(?Sg~1!|wI(RVuxbu)-rH+O=igSho_pDCw(c6b=P zKk4ATlB?bj9+HHlh<_!&z0rx13K3ZrAR8W)!@Y}o`?a*JJsD+twZIv`W)@Y?Amu_u zz``@-e2X}27$i(2=9rvIu5uTUOVhzwu%mNazS|lZb&PT;XE2|B&W1>=B58#*!~D&) zfVmJGg8UdP*fx(>Cj^?yS^zH#o-$Q-*$SnK(ZVFkw+er=>N^7!)FtP3y~Xxnu^nzY zikgB>Nj0%;WOltWIob|}%lo?_C7<``a5hEkx&1ku$|)i>Rh6@3h*`slY=9U}(Ql_< zaNG*J8vb&@zpdhAvv`?{=zDedJ23TD&Zg__snRAH4eh~^oawdYi6A3w8<Ozh@Kw)#bdktM^GVb zrG08?0bG?|NG+w^&JvD*7LAbjED{_Zkc`3H!My>0u5Q}m!+6VokMLXxl`Mkd=g&Xx z-a>m*#G3SLlhbKB!)tnzfWOBV;u;ftU}S!NdD5+YtOjLg?X}dl>7m^gOpihrf1;PY zvll&>dIuUGs{Qnd- zwIR3oIrct8Va^Tm0t#(bJD7c$Z7DO9*7NnRZorrSm`b`cxz>OIC;jSE3DO8`hX955ui`s%||YQtt2 z5DNA&pG-V+4oI2s*x^>-$6J?p=I>C|9wZF8z;VjR??Icg?1w2v5Me+FgAeGGa8(3S z4vg*$>zC-WIVZtJ7}o9{D-7d>zCe|z#<9>CFve-OPAYsneTb^JH!Enaza#j}^mXy1 z+ULn^10+rWLF6j2>Ya@@Kq?26>AqK{A_| zQKb*~F1>sE*=d?A?W7N2j?L09_7n+HGi{VY;MoTGr_)G9)ot$p!-UY5zZ2Xtbm=t z@dpPSGwgH=QtIcEulQNI>S-#ifbnO5EWkI;$A|pxJd885oM+ zGZ0_0gDvG8q2xebj+fbCHYfAXuZStH2j~|d^sBAzo46(K8n59+T6rzBwK)^rfPT+B zyIFw)9YC-V^rhtK`!3jrhmW-sTmM+tPH+;nwjL#-SjQPUZ53L@A>y*rt(#M(qsiB2 zx6B)dI}6Wlsw%bJ8h|(lhkJVogQZA&n{?Vgs6gNSXzuZpEyu*xySy8ro07QZ7Vk1!3tJphN_5V7qOiyK8p z#@jcDD8nmtYi1^l8ml;AF<#IPK?!pqf9D4moYk>d99Im}Jtwj6c#+A;f)CQ*f-hZ< z=p_T86jog%!p)D&5g9taSwYi&eP z#JuEK%+NULWus;0w32-SYFku#i}d~+{Pkho&^{;RxzP&0!RCm3-9K6`>KZpnzS6?L z^H^V*s!8<>x8bomvD%rh>Zp3>Db%kyin;qtl+jAv8Oo~1g~mqGAC&Qi_wy|xEt2iz zWAJEfTV%cl2Cs<1L&DLRVVH05EDq`pH7Oh7sR`NNkL%wi}8n>IXcO40hp+J+sC!W?!krJf!GJNE8uj zg-y~Ns-<~D?yqbzVRB}G>0A^f0!^N7l=$m0OdZuqAOQqLc zX?AEGr1Ht+inZ-Qiwnl@Z0qukd__a!C*CKuGdy5#nD7VUBM^6OCpxCa2A(X;e0&V4 zM&WR8+wErQ7UIc6LY~Q9x%Sn*Tn>>P`^t&idaOEnOd(Ufw#>NoR^1QdhJ8s`h^|R_ zXX`c5*O~Xdvh%q;7L!_!ohf$NfEBmCde|#uVZvEo>OfEq%+Ns7&_f$OR9xsihRpBb z+cjk8LyDm@U{YN>+r46?nn{7Gh(;WhFw6GAxtcKD+YWV?uge>;+q#Xx4!GpRkVZYu zzsF}1)7$?%s9g9CH=Zs+B%M_)+~*j3L0&Q9u7!|+T`^O{xE6qvAP?XWv9_MrZKdo& z%IyU)$Q95AB4!#hT!_dA>4e@zjOBD*Y=XjtMm)V|+IXzjuM;(l+8aA5#Kaz_$rR6! zj>#&^DidYD$nUY(D$mH`9eb|dtV0b{S>H6FBfq>t5`;OxA4Nn{J(+XihF(stSche7$es&~N$epi&PDM_N`As;*9D^L==2Q7Z2zD+CiU(|+-kL*VG+&9!Yb3LgPy?A zm7Z&^qRG_JIxK7-FBzZI3Q<;{`DIxtc48k> zc|0dmX;Z=W$+)qE)~`yn6MdoJ4co;%!`ddy+FV538Y)j(vg}5*k(WK)KWZ3WaOG!8 z!syGn=s{H$odtpqFrT#JGM*utN7B((abXnpDM6w56nhw}OY}0TiTG1#f*VFZr+^-g zbP10`$LPq_;PvrA1XXlyx2uM^mrjTzX}w{yuLo-cOClE8MMk47T25G8M!9Z5ypOSV zAJUBGEg5L2fY)ZGJb^E34R2zJ?}Vf>{~gB!8=5Z) z9y$>5c)=;o0HeHHSuE4U)#vG&KF|I%-cF6f$~pdYJWk_dD}iOA>iA$O$+4%@>JU08 zS`ep)$XLPJ+n0_i@PkF#ri6T8?ZeAot$6JIYHm&P6EB=BiaNY|aA$W0I+nz*zkz_z zkEru!tj!QUffq%)8y0y`T&`fuus-1p>=^hnBiBqD^hXrPs`PY9tU3m0np~rISY09> z`P3s=-kt_cYcxWd{de@}TwSqg*xVhp;E9zCsnXo6z z?f&Sv^U7n4`xr=mXle94HzOdN!2kB~4=%)u&N!+2;z6UYKUDqi-s6AZ!haB;@&B`? z_TRX0%@suz^TRdCb?!vNJYPY8L_}&07uySH9%W^Tc&1pia6y1q#?*Drf}GjGbPjBS zbOPcUY#*$3sL2x4v_i*Y=N7E$mR}J%|GUI(>WEr+28+V z%v5{#e!UF*6~G&%;l*q*$V?&r$Pp^sE^i-0$+RH3ERUUdQ0>rAq2(2QAbG}$y{de( z>{qD~GGuOk559Y@%$?N^1ApVL_a704>8OD%8Y%8B;FCt%AoPu8*D1 zLB5X>b}Syz81pn;xnB}%0FnwazlWfUV)Z-~rZg6~b z6!9J$EcE&sEbzcy?CI~=boWA&eeIa%z(7SE^qgVLz??1Vbc1*aRvc%Mri)AJaAG!p z$X!_9Ds;Zz)f+;%s&dRcJt2==P{^j3bf0M=nJd&xwUGlUFn?H=2W(*2I2Gdu zv!gYCwM10aeus)`RIZSrCK=&oKaO_Ry~D1B5!y0R=%!i2*KfXGYX&gNv_u+n9wiR5 z*e$Zjju&ODRW3phN925%S(jL+bCHv6rZtc?!*`1TyYXT6%Ju=|X;6D@lq$8T zW{Y|e39ioPez(pBH%k)HzFITXHvnD6hw^lIoUMA;qAJ^CU?top1fo@s7xT13Fvn1H z6JWa-6+FJF#x>~+A;D~;VDs26>^oH0EI`IYT2iagy23?nyJ==i{g4%HrAf1-*v zK1)~@&(KkwR7TL}L(A@C_S0G;-GMDy=MJn2$FP5s<%wC)4jC5PXoxrQBFZ_k0P{{s@sz+gX`-!=T8rcB(=7vW}^K6oLWMmp(rwDh}b zwaGGd>yEy6fHv%jM$yJXo5oMAQ>c9j`**}F?MCry;T@47@r?&sKHgVe$MCqk#Z_3S z1GZI~nOEN*P~+UaFGnj{{Jo@16`(qVNtbU>O0Hf57-P>x8Jikp=`s8xWs^dAJ9lCQ z)GFm+=OV%AMVqVATtN@|vp61VVAHRn87}%PC^RAzJ%JngmZTasWBAWsoAqBU+8L8u z4A&Pe?fmTm0?mK-BL9t+{y7o(7jm+RpOhL9KnY#E&qu^}B6=K_dB}*VlSEiC9fn)+V=J;OnN)Ta5v66ic1rG+dGAJ1 z1%Zb_+!$=tQ~lxQrzv3x#CPb?CekEkA}0MYSgx$Jdd}q8+R=ma$|&1a#)TQ=l$1tQ z=tL9&_^vJ)Pk}EDO-va`UCT1m#Uty1{v^A3P~83_#v^ozH}6*9mIjIr;t3Uv%@VeW zGL6(CwCUp)Jq%G0bIG%?{_*Y#5IHf*5M@wPo6A{$Um++Co$wLC=J1aoG93&T7Ho}P z=mGEPP7GbvoG!uD$k(H3A$Z))+i{Hy?QHdk>3xSBXR0j!11O^mEe9RHmw!pvzv?Ua~2_l2Yh~_!s1qS`|0~0)YsbHSz8!mG)WiJE| z2f($6TQtt6L_f~ApQYQKSb=`053LgrQq7G@98#igV>y#i==-nEjQ!XNu9 z~;mE+gtj4IDDNQJ~JVk5Ux6&LCSFL!y=>79kE9=V}J7tD==Ga+IW zX)r7>VZ9dY=V&}DR))xUoV!u(Z|%3ciQi_2jl}3=$Agc(`RPb z8kEBpvY>1FGQ9W$n>Cq=DIpski};nE)`p3IUw1Oz0|wxll^)4dq3;CCY@RyJgFgc# zKouFh!`?Xuo{IMz^xi-h=StCis_M7yq$u) z?XHvw*HP0VgR+KR6wI)jEMX|ssqYvSf*_3W8zVTQzD?3>H!#>InzpSO)@SC8q*ii- z%%h}_#0{4JG;Jm`4zg};BPTGkYamx$Xo#O~lBirRY)q=5M45n{GCfV7h9qwyu1NxOMoP4)jjZMxmT|IQQh0U7C$EbnMN<3)Kk?fFHYq$d|ICu>KbY_hO zTZM+uKHe(cIZfEqyzyYSUBZa8;Fcut-GN!HSA9ius`ltNebF46ZX_BbZNU}}ZOm{M2&nANL9@0qvih15(|`S~z}m&h!u4x~(%MAO$jHRWNfuxWF#B)E&g3ghSQ9|> z(MFaLQj)NE0lowyjvg8z0#m6FIuKE9lDO~Glg}nSb7`~^&#(Lw{}GVOS>U)m8bF}x zVjbXljBm34Cs-yM6TVusr+3kYFjr28STT3g056y3cH5Tmge~ASxBj z%|yb>$eF;WgrcOZf569sDZOVwoo%8>XO>XQOX1OyN9I-SQgrm;U;+#3OI(zrWyow3 zk==|{lt2xrQ%FIXOTejR>;wv(Pb8u8}BUpx?yd(Abh6? zsoO3VYWkeLnF43&@*#MQ9-i-d0t*xN-UEyNKeyNMHw|A(k(_6QKO=nKMCxD(W(Yop zsRQ)QeL4X3Lxp^L%wzi2-WVSsf61dqliPUM7srDB?Wm6Lzn0&{*}|IsKQW;02(Y&| zaTKv|`U(pSzuvR6Rduu$wzK_W-Y-7>7s?G$)U}&uK;<>vU}^^ns@Z!p+9?St1s)dG zK%y6xkPyyS1$~&6v{kl?Md6gwM|>mt6Upm>oa8RLD^8T{0?HC!Z>;(Bob7el(DV6x zi`I)$&E&ngwFS@bi4^xFLAn`=fzTC;aimE^!cMI2n@Vo%Ae-ne`RF((&5y6xsjjAZ zVguVoQ?Z9uk$2ON;ersE%PU*xGO@T*;j1BO5#TuZKEf(mB7|g7pcEA=nYJ{s3vlbg zd4-DUlD{*6o%Gc^N!Nptgay>j6E5;3psI+C3Q!1ZIbeCubW%w4pq9)MSDyB{HLm|k zxv-{$$A*pS@csolri$Ge<4VZ}e~78JOL-EVyrbxKra^d{?|NnPp86!q>t<&IP07?Z z^>~IK^k#OEKgRH+LjllZXk7iA>2cfH6+(e&9ku5poo~6y{GC5>(bRK7hwjiurqAiZ zg*DmtgY}v83IjE&AbiWgMyFbaRUPZ{lYiz$U^&Zt2YjG<%m((&_JUbZcfJ22(>bi5 z!J?<7AySj0JZ&<-qXX;mcV!f~>G=sB0KnjWca4}vrtunD^1TrpfeS^4dvFr!65knK zZh`d;*VOkPs4*-9kL>$GP0`(M!j~B;#x?Ba~&s6CopvO86oM?-? zOw#dIRc;6A6T?B`Qp%^<U5 z19x(ywSH$_N+Io!6;e?`tWaM$`=Db!gzx|lQ${DG!zb1Zl&|{kX0y6xvO1o z220r<-oaS^^R2pEyY;=Qllqpmue|5yI~D|iI!IGt@iod{Opz@*ml^w2bNs)p`M(Io z|E;;m*Xpjd9l)4G#KaWfV(t8YUn@A;nK^#xgv=LtnArX|vWQVuw3}B${h+frU2>9^ z!l6)!Uo4`5k`<<;E(ido7M6lKTgWezNLq>U*=uz&s=cc$1%>VrAeOoUtA|T6gO4>UNqsdK=NF*8|~*sl&wI=x9-EGiq*aqV!(VVXA57 zw9*o6Ir8Lj1npUXvlevtn(_+^X5rzdR>#(}4YcB9O50q97%rW2me5_L=%ffYPUSRc z!vv?Kv>dH994Qi>U(a<0KF6NH5b16enCp+mw^Hb3Xs1^tThFpz!3QuN#}KBbww`(h z7GO)1olDqy6?T$()R7y%NYx*B0k_2IBiZ14&8|JPFxeMF{vW>HF-Vi3+ZOI=+qP}n zw(+!WcTd~4ZJX1!ZM&y!+uyt=&i!+~d(V%GjH;-NsEEv6nS1TERt|RHh!0>W4+4pp z1-*EzAM~i`+1f(VEHI8So`S`akPfPTfq*`l{Fz`hS%k#JS0cjT2mS0#QLGf=J?1`he3W*;m4)ce8*WFq1sdP=~$5RlH1EdWm|~dCvKOi4*I_96{^95p#B<(n!d?B z=o`0{t+&OMwKcxiBECznJcfH!fL(z3OvmxP#oWd48|mMjpE||zdiTBdWelj8&Qosv zZFp@&UgXuvJw5y=q6*28AtxZzo-UUpkRW%ne+Ylf!V-0+uQXBW=5S1o#6LXNtY5!I z%Rkz#(S8Pjz*P7bqB6L|M#Er{|QLae-Y{KA>`^} z@lPjeX>90X|34S-7}ZVXe{wEei1<{*e8T-Nbj8JmD4iwcE+Hg_zhkPVm#=@b$;)h6 z<<6y`nPa`f3I6`!28d@kdM{uJOgM%`EvlQ5B2bL)Sl=|y@YB3KeOzz=9cUW3clPAU z^sYc}xf9{4Oj?L5MOlYxR{+>w=vJjvbyO5}ptT(o6dR|ygO$)nVCvNGnq(6;bHlBd zl?w-|plD8spjDF03g5ip;W3Z z><0{BCq!Dw;h5~#1BuQilq*TwEu)qy50@+BE4bX28+7erX{BD4H)N+7U`AVEuREE8 z;X?~fyhF-x_sRfHIj~6f(+^@H)D=ngP;mwJjxhQUbUdzk8f94Ab%59-eRIq?ZKrwD z(BFI=)xrUlgu(b|hAysqK<}8bslmNNeD=#JW*}^~Nrswn^xw*nL@Tx!49bfJecV&KC2G4q5a!NSv)06A_5N3Y?veAz;Gv+@U3R% z)~UA8-0LvVE{}8LVDOHzp~2twReqf}ODIyXMM6=W>kL|OHcx9P%+aJGYi_Om)b!xe zF40Vntn0+VP>o<$AtP&JANjXBn7$}C@{+@3I@cqlwR2MdwGhVPxlTIcRVu@Ho-wO` z_~Or~IMG)A_`6-p)KPS@cT9mu9RGA>dVh5wY$NM9-^c@N=hcNaw4ITjm;iWSP^ZX| z)_XpaI61<+La+U&&%2a z0za$)-wZP@mwSELo#3!PGTt$uy0C(nTT@9NX*r3Ctw6J~7A(m#8fE)0RBd`TdKfAT zCf@$MAxjP`O(u9s@c0Fd@|}UQ6qp)O5Q5DPCeE6mSIh|Rj{$cAVIWsA=xPKVKxdhg zLzPZ`3CS+KIO;T}0Ip!fAUaNU>++ZJZRk@I(h<)RsJUhZ&Ru9*!4Ptn;gX^~4E8W^TSR&~3BAZc#HquXn)OW|TJ`CTahk+{qe`5+ixON^zA9IFd8)kc%*!AiLu z>`SFoZ5bW-%7}xZ>gpJcx_hpF$2l+533{gW{a7ce^B9sIdmLrI0)4yivZ^(Vh@-1q zFT!NQK$Iz^xu%|EOK=n>ug;(7J4OnS$;yWmq>A;hsD_0oAbLYhW^1Vdt9>;(JIYjf zdb+&f&D4@4AS?!*XpH>8egQvSVX`36jMd>$+RgI|pEg))^djhGSo&#lhS~9%NuWfX zDDH;3T*GzRT@5=7ibO>N-6_XPBYxno@mD_3I#rDD?iADxX`! zh*v8^i*JEMzyN#bGEBz7;UYXki*Xr(9xXax(_1qVW=Ml)kSuvK$coq2A(5ZGhs_pF z$*w}FbN6+QDseuB9=fdp_MTs)nQf!2SlROQ!gBJBCXD&@-VurqHj0wm@LWX-TDmS= z71M__vAok|@!qgi#H&H%Vg-((ZfxPAL8AI{x|VV!9)ZE}_l>iWk8UPTGHs*?u7RfP z5MC&=c6X;XlUzrz5q?(!eO@~* zoh2I*%J7dF!!_!vXoSIn5o|wj1#_>K*&CIn{qSaRc&iFVxt*^20ngCL;QonIS>I5^ zMw8HXm>W0PGd*}Ko)f|~dDd%;Wu_RWI_d;&2g6R3S63Uzjd7dn%Svu-OKpx*o|N>F zZg=-~qLb~VRLpv`k zWSdfHh@?dp=s_X`{yxOlxE$4iuyS;Z-x!*E6eqmEm*j2bE@=ZI0YZ5%Yj29!5+J$4h{s($nakA`xgbO8w zi=*r}PWz#lTL_DSAu1?f%-2OjD}NHXp4pXOsCW;DS@BC3h-q4_l`<))8WgzkdXg3! zs1WMt32kS2E#L0p_|x+x**TFV=gn`m9BWlzF{b%6j-odf4{7a4y4Uaef@YaeuPhU8 zHBvRqN^;$Jizy+ z=zW{E5<>2gp$pH{M@S*!sJVQU)b*J5*bX4h>5VJve#Q6ga}cQ&iL#=(u+KroWrxa%8&~p{WEUF0il=db;-$=A;&9M{Rq`ouZ5m%BHT6%st%saGsD6)fQgLN}x@d3q>FC;=f%O3Cyg=Ke@Gh`XW za@RajqOE9UB6eE=zhG%|dYS)IW)&y&Id2n7r)6p_)vlRP7NJL(x4UbhlcFXWT8?K=%s7;z?Vjts?y2+r|uk8Wt(DM*73^W%pAkZa1Jd zNoE)8FvQA>Z`eR5Z@Ig6kS5?0h;`Y&OL2D&xnnAUzQz{YSdh0k zB3exx%A2TyI)M*EM6htrxSlep!Kk(P(VP`$p0G~f$smld6W1r_Z+o?=IB@^weq>5VYsYZZR@` z&XJFxd5{|KPZmVOSxc@^%71C@;z}}WhbF9p!%yLj3j%YOlPL5s>7I3vj25 z@xmf=*z%Wb4;Va6SDk9cv|r*lhZ`(y_*M@>q;wrn)oQx%B(2A$9(74>;$zmQ!4fN; z>XurIk-7@wZys<+7XL@0Fhe-f%*=(weaQEdR9Eh6>Kl-EcI({qoZqyzziGwpg-GM#251sK_ z=3|kitS!j%;fpc@oWn65SEL73^N&t>Ix37xgs= zYG%eQDJc|rqHFia0!_sm7`@lvcv)gfy(+KXA@E{3t1DaZ$DijWAcA)E0@X?2ziJ{v z&KOYZ|DdkM{}t+@{@*6ge}m%xfjIxi%qh`=^2Rwz@w0cCvZ&Tc#UmCDbVwABrON^x zEBK43FO@weA8s7zggCOWhMvGGE`baZ62cC)VHyy!5Zbt%ieH+XN|OLbAFPZWyC6)p z4P3%8sq9HdS3=ih^0OOlqTPbKuzQ?lBEI{w^ReUO{V?@`ARsL|S*%yOS=Z%sF)>-y z(LAQdhgAcuF6LQjRYfdbD1g4o%tV4EiK&ElLB&^VZHbrV1K>tHTO{#XTo>)2UMm`2 z^t4s;vnMQgf-njU-RVBRw0P0-m#d-u`(kq7NL&2T)TjI_@iKuPAK-@oH(J8?%(e!0Ir$yG32@CGUPn5w4)+9@8c&pGx z+K3GKESI4*`tYlmMHt@br;jBWTei&(a=iYslc^c#RU3Q&sYp zSG){)V<(g7+8W!Wxeb5zJb4XE{I|&Y4UrFWr%LHkdQ;~XU zgy^dH-Z3lmY+0G~?DrC_S4@=>0oM8Isw%g(id10gWkoz2Q%7W$bFk@mIzTCcIB(K8 zc<5h&ZzCdT=9n-D>&a8vl+=ZF*`uTvQviG_bLde*k>{^)&0o*b05x$MO3gVLUx`xZ z43j+>!u?XV)Yp@MmG%Y`+COH2?nQcMrQ%k~6#O%PeD_WvFO~Kct za4XoCM_X!c5vhRkIdV=xUB3xI2NNStK*8_Zl!cFjOvp-AY=D;5{uXj}GV{LK1~IE2 z|KffUiBaStRr;10R~K2VVtf{TzM7FaPm;Y(zQjILn+tIPSrJh&EMf6evaBKIvi42-WYU9Vhj~3< zZSM-B;E`g_o8_XTM9IzEL=9Lb^SPhe(f(-`Yh=X6O7+6ALXnTcUFpI>ekl6v)ZQeNCg2 z^H|{SKXHU*%nBQ@I3It0m^h+6tvI@FS=MYS$ZpBaG7j#V@P2ZuYySbp@hA# ze(kc;P4i_-_UDP?%<6>%tTRih6VBgScKU^BV6Aoeg6Uh(W^#J^V$Xo^4#Ekp ztqQVK^g9gKMTHvV7nb64UU7p~!B?>Y0oFH5T7#BSW#YfSB@5PtE~#SCCg3p^o=NkMk$<8- z6PT*yIKGrvne7+y3}_!AC8NNeI?iTY(&nakN>>U-zT0wzZf-RuyZk^X9H-DT_*wk= z;&0}6LsGtfVa1q)CEUPlx#(ED@-?H<1_FrHU#z5^P3lEB|qsxEyn%FOpjx z3S?~gvoXy~L(Q{Jh6*i~=f%9kM1>RGjBzQh_SaIDfSU_9!<>*Pm>l)cJD@wlyxpBV z4Fmhc2q=R_wHCEK69<*wG%}mgD1=FHi4h!98B-*vMu4ZGW~%IrYSLGU{^TuseqVgV zLP<%wirIL`VLyJv9XG_p8w@Q4HzNt-o;U@Au{7%Ji;53!7V8Rv0^Lu^Vf*sL>R(;c zQG_ZuFl)Mh-xEIkGu}?_(HwkB2jS;HdPLSxVU&Jxy9*XRG~^HY(f0g8Q}iqnVmgjI zfd=``2&8GsycjR?M%(zMjn;tn9agcq;&rR!Hp z$B*gzHsQ~aXw8c|a(L^LW(|`yGc!qOnV(ZjU_Q-4z1&0;jG&vAKuNG=F|H?@m5^N@ zq{E!1n;)kNTJ>|Hb2ODt-7U~-MOIFo%9I)_@7fnX+eMMNh>)V$IXesJpBn|uo8f~#aOFytCT zf9&%MCLf8mp4kwHTcojWmM3LU=#|{3L>E}SKwOd?%{HogCZ_Z1BSA}P#O(%H$;z7XyJ^sjGX;j5 zrzp>|Ud;*&VAU3x#f{CKwY7Vc{%TKKqmB@oTHA9;>?!nvMA;8+Jh=cambHz#J18x~ zs!dF>$*AnsQ{{82r5Aw&^7eRCdvcgyxH?*DV5(I$qXh^zS>us*I66_MbL8y4d3ULj z{S(ipo+T3Ag!+5`NU2sc+@*m{_X|&p#O-SAqF&g_n7ObB82~$p%fXA5GLHMC+#qqL zdt`sJC&6C2)=juQ_!NeD>U8lDVpAOkW*khf7MCcs$A(wiIl#B9HM%~GtQ^}yBPjT@ z+E=|A!Z?A(rwzZ;T}o6pOVqHzTr*i;Wrc%&36kc@jXq~+w8kVrs;%=IFdACoLAcCAmhFNpbP8;s`zG|HC2Gv?I~w4ITy=g$`0qMQdkijLSOtX6xW%Z9Nw<;M- zMN`c7=$QxN00DiSjbVt9Mi6-pjv*j(_8PyV-il8Q-&TwBwH1gz1uoxs6~uU}PrgWB zIAE_I-a1EqlIaGQNbcp@iI8W1sm9fBBNOk(k&iLBe%MCo#?xI$%ZmGA?=)M9D=0t7 zc)Q0LnI)kCy{`jCGy9lYX%mUsDWwsY`;jE(;Us@gmWPqjmXL+Hu#^;k%eT>{nMtzj zsV`Iy6leTA8-PndszF;N^X@CJrTw5IIm!GPeu)H2#FQitR{1p;MasQVAG3*+=9FYK zw*k!HT(YQorfQj+1*mCV458(T5=fH`um$gS38hw(OqVMyunQ;rW5aPbF##A3fGH6h z@W)i9Uff?qz`YbK4c}JzQpuxuE3pcQO)%xBRZp{zJ^-*|oryTxJ-rR+MXJ)!f=+pp z10H|DdGd2exhi+hftcYbM0_}C0ZI-2vh+$fU1acsB-YXid7O|=9L!3e@$H*6?G*Zp z%qFB(sgl=FcC=E4CYGp4CN>=M8#5r!RU!u+FJVlH6=gI5xHVD&k;Ta*M28BsxfMV~ zLz+@6TxnfLhF@5=yQo^1&S}cmTN@m!7*c6z;}~*!hNBjuE>NLVl2EwN!F+)0$R1S! zR|lF%n!9fkZ@gPW|x|B={V6x3`=jS*$Pu0+5OWf?wnIy>Y1MbbGSncpKO0qE(qO=ts z!~@&!N`10S593pVQu4FzpOh!tvg}p%zCU(aV5=~K#bKi zHdJ1>tQSrhW%KOky;iW+O_n;`l9~omqM%sdxdLtI`TrJzN6BQz+7xOl*rM>xVI2~# z)7FJ^Dc{DC<%~VS?@WXzuOG$YPLC;>#vUJ^MmtbSL`_yXtNKa$Hk+l-c!aC7gn(Cg ze?YPYZ(2Jw{SF6MiO5(%_pTo7j@&DHNW`|lD`~{iH+_eSTS&OC*2WTT*a`?|9w1dh zh1nh@$a}T#WE5$7Od~NvSEU)T(W$p$s5fe^GpG+7fdJ9=enRT9$wEk+ZaB>G3$KQO zgq?-rZZnIv!p#>Ty~}c*Lb_jxJg$eGM*XwHUwuQ|o^}b3^T6Bxx{!?va8aC@-xK*H ztJBFvFfsSWu89%@b^l3-B~O!CXs)I6Y}y#0C0U0R0WG zybjroj$io0j}3%P7zADXOwHwafT#uu*zfM!oD$6aJx7+WL%t-@6^rD_a_M?S^>c;z zMK580bZXo1f*L$CuMeM4Mp!;P@}b~$cd(s5*q~FP+NHSq;nw3fbWyH)i2)-;gQl{S zZO!T}A}fC}vUdskGSq&{`oxt~0i?0xhr6I47_tBc`fqaSrMOzR4>0H^;A zF)hX1nfHs)%Zb-(YGX;=#2R6C{BG;k=?FfP?9{_uFLri~-~AJ;jw({4MU7e*d)?P@ zXX*GkNY9ItFjhwgAIWq7Y!ksbMzfqpG)IrqKx9q{zu%Mdl+{Dis#p9q`02pr1LG8R z@As?eG!>IoROgS!@J*to<27coFc1zpkh?w=)h9CbYe%^Q!Ui46Y*HO0mr% zEff-*$ndMNw}H2a5@BsGj5oFfd!T(F&0$<{GO!Qdd?McKkorh=5{EIjDTHU`So>8V zBA-fqVLb2;u7UhDV1xMI?y>fe3~4urv3%PX)lDw+HYa;HFkaLqi4c~VtCm&Ca+9C~ zge+67hp#R9`+Euq59WhHX&7~RlXn=--m8$iZ~~1C8cv^2(qO#X0?vl91gzUKBeR1J z^p4!!&7)3#@@X&2aF2-)1Ffcc^F8r|RtdL2X%HgN&XU-KH2SLCbpw?J5xJ*!F-ypZ zMG%AJ!Pr&}`LW?E!K~=(NJxuSVTRCGJ$2a*Ao=uUDSys!OFYu!Vs2IT;xQ6EubLIl z+?+nMGeQQhh~??0!s4iQ#gm3!BpMpnY?04kK375e((Uc7B3RMj;wE?BCoQGu=UlZt!EZ1Q*auI)dj3Jj{Ujgt zW5hd~-HWBLI_3HuO) zNrb^XzPsTIb=*a69wAAA3J6AAZZ1VsYbIG}a`=d6?PjM)3EPaDpW2YP$|GrBX{q*! z$KBHNif)OKMBCFP5>!1d=DK>8u+Upm-{hj5o|Wn$vh1&K!lVfDB&47lw$tJ?d5|=B z^(_9=(1T3Fte)z^>|3**n}mIX;mMN5v2F#l(q*CvU{Ga`@VMp#%rQkDBy7kYbmb-q z<5!4iuB#Q_lLZ8}h|hPODI^U6`gzLJre9u3k3c#%86IKI*^H-@I48Bi*@avYm4v!n0+v zWu{M{&F8#p9cx+gF0yTB_<2QUrjMPo9*7^-uP#~gGW~y3nfPAoV%amgr>PSyVAd@l)}8#X zR5zV6t*uKJZL}?NYvPVK6J0v4iVpwiN|>+t3aYiZSp;m0!(1`bHO}TEtWR1tY%BPB z(W!0DmXbZAsT$iC13p4f>u*ZAy@JoLAkJhzFf1#4;#1deO8#8d&89}en&z!W&A3++^1(;>0SB1*54d@y&9Pn;^IAf3GiXbfT`_>{R+Xv; zQvgL>+0#8-laO!j#-WB~(I>l0NCMt_;@Gp_f0#^c)t?&#Xh1-7RR0@zPyBz!U#0Av zT?}n({(p?p7!4S2ZBw)#KdCG)uPnZe+U|0{BW!m)9 zi_9$F?m<`2!`JNFv+w8MK_K)qJ^aO@7-Ig>cM4-r0bi=>?B_2mFNJ}aE3<+QCzRr*NA!QjHw# z`1OsvcoD0?%jq{*7b!l|L1+Tw0TTAM4XMq7*ntc-Ived>Sj_ZtS|uVdpfg1_I9knY z2{GM_j5sDC7(W&}#s{jqbybqJWyn?{PW*&cQIU|*v8YGOKKlGl@?c#TCnmnAkAzV- zmK={|1G90zz=YUvC}+fMqts0d4vgA%t6Jhjv?d;(Z}(Ep8fTZfHA9``fdUHkA+z3+ zhh{ohP%Bj?T~{i0sYCQ}uC#5BwN`skI7`|c%kqkyWIQ;!ysvA8H`b-t()n6>GJj6xlYDu~8qX{AFo$Cm3d|XFL=4uvc?Keb zzb0ZmMoXca6Mob>JqkNuoP>B2Z>D`Q(TvrG6m`j}-1rGP!g|qoL=$FVQYxJQjFn33lODt3Wb1j8VR zlR++vIT6^DtYxAv_hxupbLLN3e0%A%a+hWTKDV3!Fjr^cWJ{scsAdfhpI)`Bms^M6 zQG$waKgFr=c|p9Piug=fcJvZ1ThMnNhQvBAg-8~b1?6wL*WyqXhtj^g(Ke}mEfZVM zJuLNTUVh#WsE*a6uqiz`b#9ZYg3+2%=C(6AvZGc=u&<6??!slB1a9K)=VL zY9EL^mfyKnD zSJyYBc_>G;5RRnrNgzJz#Rkn3S1`mZgO`(r5;Hw6MveN(URf_XS-r58Cn80K)ArH4 z#Rrd~LG1W&@ttw85cjp8xV&>$b%nSXH_*W}7Ch2pg$$c0BdEo-HWRTZcxngIBJad> z;C>b{jIXjb_9Jis?NZJsdm^EG}e*pR&DAy0EaSGi3XWTa(>C%tz1n$u?5Fb z1qtl?;_yjYo)(gB^iQq?=jusF%kywm?CJP~zEHi0NbZ);$(H$w(Hy@{i>$wcVRD_X|w-~(0Z9BJyh zhNh;+eQ9BEIs;tPz%jSVnfCP!3L&9YtEP;svoj_bNzeGSQIAjd zBss@A;)R^WAu-37RQrM%{DfBNRx>v!G31Z}8-El9IOJlb_MSoMu2}GDYycNaf>uny z+8xykD-7ONCM!APry_Lw6-yT>5!tR}W;W`C)1>pxSs5o1z#j7%m=&=7O4hz+Lsqm` z*>{+xsabZPr&X=}G@obTb{nPTkccJX8w3CG7X+1+t{JcMabv~UNv+G?txRqXib~c^Mo}`q{$`;EBNJ;#F*{gvS12kV?AZ%O0SFB$^ zn+}!HbmEj}w{Vq(G)OGAzH}R~kS^;(-s&=ectz8vN!_)Yl$$U@HNTI-pV`LSj7Opu zTZ5zZ)-S_{GcEQPIQXLQ#oMS`HPu{`SQiAZ)m1at*Hy%3xma|>o`h%E%8BEbi9p0r zVjcsh<{NBKQ4eKlXU|}@XJ#@uQw*$4BxKn6#W~I4T<^f99~(=}a`&3(ur8R9t+|AQ zWkQx7l}wa48-jO@ft2h+7qn%SJtL%~890FG0s5g*kNbL3I&@brh&f6)TlM`K^(bhr zJWM6N6x3flOw$@|C@kPi7yP&SP?bzP-E|HSXQXG>7gk|R9BTj`e=4de9C6+H7H7n# z#GJeVs1mtHhLDmVO?LkYRQc`DVOJ_vdl8VUihO-j#t=0T3%Fc1f9F73ufJz*adn*p zc%&vi(4NqHu^R>sAT_0EDjVR8bc%wTz#$;%NU-kbDyL_dg0%TFafZwZ?5KZpcuaO54Z9hX zD$u>q!-9`U6-D`E#`W~fIfiIF5_m6{fvM)b1NG3xf4Auw;Go~Fu7cth#DlUn{@~yu z=B;RT*dp?bO}o%4x7k9v{r=Y@^YQ^UUm(Qmliw8brO^=NP+UOohLYiaEB3^DB56&V zK?4jV61B|1Uj_5fBKW;8LdwOFZKWp)g{B%7g1~DgO&N& z#lisxf?R~Z@?3E$Mms$$JK8oe@X`5m98V*aV6Ua}8Xs2#A!{x?IP|N(%nxsH?^c{& z@vY&R1QmQs83BW28qAmJfS7MYi=h(YK??@EhjL-t*5W!p z^gYX!Q6-vBqcv~ruw@oMaU&qp0Fb(dbVzm5xJN%0o_^@fWq$oa3X?9s%+b)x4w-q5Koe(@j6Ez7V@~NRFvd zfBH~)U5!ix3isg`6be__wBJp=1@yfsCMw1C@y+9WYD9_C%{Q~7^0AF2KFryfLlUP# zwrtJEcH)jm48!6tUcxiurAMaiD04C&tPe6DI0#aoqz#Bt0_7_*X*TsF7u*zv(iEfA z;$@?XVu~oX#1YXtceQL{dSneL&*nDug^OW$DSLF0M1Im|sSX8R26&)<0Fbh^*l6!5wfSu8MpMoh=2l z^^0Sr$UpZp*9oqa23fcCfm7`ya2<4wzJ`Axt7e4jJrRFVf?nY~2&tRL* zd;6_njcz01c>$IvN=?K}9ie%Z(BO@JG2J}fT#BJQ+f5LFSgup7i!xWRKw6)iITjZU z%l6hPZia>R!`aZjwCp}I zg)%20;}f+&@t;(%5;RHL>K_&7MH^S+7<|(SZH!u zznW|jz$uA`P9@ZWtJgv$EFp>)K&Gt+4C6#*khZQXS*S~6N%JDT$r`aJDs9|uXWdbg zBwho$phWx}x!qy8&}6y5Vr$G{yGSE*r$^r{}pw zVTZKvikRZ`J_IJrjc=X1uw?estdwm&bEahku&D04HD+0Bm~q#YGS6gp!KLf$A{%Qd z&&yX@Hp>~(wU{|(#U&Bf92+1i&Q*-S+=y=3pSZy$#8Uc$#7oiJUuO{cE6=tsPhwPe| zxQpK>`Dbka`V)$}e6_OXKLB%i76~4N*zA?X+PrhH<&)}prET;kel24kW%+9))G^JI zsq7L{P}^#QsZViX%KgxBvEugr>ZmFqe^oAg?{EI=&_O#e)F3V#rc z8$4}0Zr19qd3tE4#$3_f=Bbx9oV6VO!d3(R===i-7p=Vj`520w0D3W6lQfY48}!D* z&)lZMG;~er2qBoI2gsX+Ts-hnpS~NYRDtPd^FPzn!^&yxRy#CSz(b&E*tL|jIkq|l zf%>)7Dtu>jCf`-7R#*GhGn4FkYf;B$+9IxmqH|lf6$4irg{0ept__%)V*R_OK=T06 zyT_m-o@Kp6U{l5h>W1hGq*X#8*y@<;vsOFqEjTQXFEotR+{3}ODDnj;o0@!bB5x=N z394FojuGOtVKBlVRLtHp%EJv_G5q=AgF)SKyRN5=cGBjDWv4LDn$IL`*=~J7u&Dy5 zrMc83y+w^F&{?X(KOOAl-sWZDb{9X9#jrQtmrEXD?;h-}SYT7yM(X_6qksM=K_a;Z z3u0qT0TtaNvDER_8x*rxXw&C^|h{P1qxK|@pS7vdlZ#P z7PdB7MmC2}%sdzAxt>;WM1s0??`1983O4nFK|hVAbHcZ3x{PzytQLkCVk7hA!Lo` zEJH?4qw|}WH{dc4z%aB=0XqsFW?^p=X}4xnCJXK%c#ItOSjdSO`UXJyuc8bh^Cf}8 z@Ht|vXd^6{Fgai8*tmyRGmD_s_nv~r^Fy7j`Bu`6=G)5H$i7Q7lvQnmea&TGvJp9a|qOrUymZ$6G|Ly z#zOCg++$3iB$!6!>215A4!iryregKuUT344X)jQb3|9qY>c0LO{6Vby05n~VFzd?q zgGZv&FGlkiH*`fTurp>B8v&nSxNz)=5IF$=@rgND4d`!AaaX;_lK~)-U8la_Wa8i?NJC@BURO*sUW)E9oyv3RG^YGfN%BmxzjlT)bp*$<| zX3tt?EAy<&K+bhIuMs-g#=d1}N_?isY)6Ay$mDOKRh z4v1asEGWoAp=srraLW^h&_Uw|6O+r;wns=uwYm=JN4Q!quD8SQRSeEcGh|Eb5Jg8m zOT}u;N|x@aq)=&;wufCc^#)5U^VcZw;d_wwaoh9$p@Xrc{DD6GZUqZ ziC6OT^zSq@-lhbgR8B+e;7_Giv;DK5gn^$bs<6~SUadiosfewWDJu`XsBfOd1|p=q zE>m=zF}!lObA%ePey~gqU8S6h-^J2Y?>7)L2+%8kV}Gp=h`Xm_}rlm)SyUS=`=S7msKu zC|T!gPiI1rWGb1z$Md?0YJQ;%>uPLOXf1Z>N~`~JHJ!^@D5kSXQ4ugnFZ>^`zH8CAiZmp z6Ms|#2gcGsQ{{u7+Nb9sA?U>(0e$5V1|WVwY`Kn)rsnnZ4=1u=7u!4WexZD^IQ1Jk zfF#NLe>W$3m&C^ULjdw+5|)-BSHwpegdyt9NYC{3@QtMfd8GrIWDu`gd0nv-3LpGCh@wgBaG z176tikL!_NXM+Bv#7q^cyn9$XSeZR6#!B4JE@GVH zoobHZN_*RF#@_SVYKkQ_igme-Y5U}cV(hkR#k1c{bQNMji zU7aE`?dHyx=1`kOYZo_8U7?3-7vHOp`Qe%Z*i+FX!s?6huNp0iCEW-Z7E&jRWmUW_ z67j>)Ew!yq)hhG4o?^z}HWH-e=es#xJUhDRc4B51M4~E-l5VZ!&zQq`gWe`?}#b~7w1LH4Xa-UCT5LXkXQWheBa2YJYbyQ zl1pXR%b(KCXMO0OsXgl0P0Og<{(@&z1aokU-Pq`eQq*JYgt8xdFQ6S z6Z3IFSua8W&M#`~*L#r>Jfd6*BzJ?JFdBR#bDv$_0N!_5vnmo@!>vULcDm`MFU823 zpG9pqjqz^FE5zMDoGqhs5OMmC{Y3iVcl>F}5Rs24Y5B^mYQ;1T&ks@pIApHOdrzXF z-SdX}Hf{X;TaSxG_T$0~#RhqKISGKNK47}0*x&nRIPtmdwxc&QT3$8&!3fWu1eZ_P zJveQj^hJL#Sn!*4k`3}(d(aasl&7G0j0-*_2xtAnoX1@9+h zO#c>YQg60Z;o{Bi=3i7S`Ic+ZE>K{(u|#)9y}q*j8uKQ1^>+(BI}m%1v3$=4ojGBc zm+o1*!T&b}-lVvZqIUBc8V}QyFEgm#oyIuC{8WqUNV{Toz`oxhYpP!_p2oHHh5P@iB*NVo~2=GQm+8Yrkm2Xjc_VyHg1c0>+o~@>*Qzo zHVBJS>$$}$_4EniTI;b1WShX<5-p#TPB&!;lP!lBVBbLOOxh6FuYloD%m;n{r|;MU3!q4AVkua~fieeWu2 zQAQ$ue(IklX6+V;F1vCu-&V?I3d42FgWgsb_e^29ol}HYft?{SLf>DrmOp9o!t>I^ zY7fBCk+E8n_|apgM|-;^=#B?6RnFKlN`oR)`e$+;D=yO-(U^jV;rft^G_zl`n7qnM zL z*-Y4Phq+ZI1$j$F-f;`CD#|`-T~OM5Q>x}a>B~Gb3-+9i>Lfr|Ca6S^8g*{*?_5!x zH_N!SoRP=gX1?)q%>QTY!r77e2j9W(I!uAz{T`NdNmPBBUzi2{`XMB^zJGGwFWeA9 z{fk33#*9SO0)DjROug+(M)I-pKA!CX;IY(#gE!UxXVsa)X!UftIN98{pt#4MJHOhY zM$_l}-TJlxY?LS6Nuz1T<44m<4i^8k@D$zuCPrkmz@sdv+{ciyFJG2Zwy&%c7;atIeTdh!a(R^QXnu1Oq1b42*OQFWnyQ zWeQrdvP|w_idy53Wa<{QH^lFmEd+VlJkyiC>6B#s)F;w-{c;aKIm;Kp50HnA-o3lY z9B~F$gJ@yYE#g#X&3ADx&tO+P_@mnQTz9gv30_sTsaGXkfNYXY{$(>*PEN3QL>I!k zp)KibPhrfX3%Z$H6SY`rXGYS~143wZrG2;=FLj50+VM6soI~up_>fU(2Wl@{BRsMi zO%sL3x?2l1cXTF)k&moNsHfQrQ+wu(gBt{sk#CU=UhrvJIncy@tJX5klLjgMn>~h= zg|FR&;@eh|C7`>s_9c~0-{IAPV){l|Ts`i=)AW;d9&KPc3fMeoTS%8@V~D8*h;&(^>yjT84MM}=%#LS7shLAuuj(0VAYoozhWjq z4LEr?wUe2^WGwdTIgWBkDUJa>YP@5d9^Rs$kCXmMRxuF*YMVrn?0NFyPl}>`&dqZb z<5eqR=ZG3>n2{6v6BvJ`YBZeeTtB88TAY(x0a58EWyuf>+^|x8Qa6wA|1Nb_p|nA zWWa}|z8a)--Wj`LqyFk_a3gN2>5{Rl_wbW?#by7&i*^hRknK%jwIH6=dQ8*-_{*x0j^DUfMX0`|K@6C<|1cgZ~D(e5vBFFm;HTZF(!vT8=T$K+|F)x3kqzBV4-=p1V(lzi(s7jdu0>LD#N=$Lk#3HkG!a zIF<7>%B7sRNzJ66KrFV76J<2bdYhxll0y2^_rdG=I%AgW4~)1Nvz=$1UkE^J%BxLo z+lUci`UcU062os*=`-j4IfSQA{w@y|3}Vk?i;&SSdh8n+$iHA#%ERL{;EpXl6u&8@ zzg}?hkEOUOJt?ZL=pWZFJ19mI1@P=$U5*Im1e_8Z${JsM>Ov?nh8Z zP5QvI!{Jy@&BP48%P2{Jr_VgzW;P@7)M9n|lDT|Ep#}7C$&ud&6>C^5ZiwKIg2McPU(4jhM!BD@@L(Gd*Nu$ji(ljZ<{FIeW_1Mmf;76{LU z-ywN~=uNN)Xi6$<12A9y)K%X|(W0p|&>>4OXB?IiYr||WKDOJPxiSe01NSV-h24^L z_>m$;|C+q!Mj**-qQ$L-*++en(g|hw;M!^%_h-iDjFHLo-n3JpB;p?+o2;`*jpvJU zLY^lt)Un4joij^^)O(CKs@7E%*!w>!HA4Q?0}oBJ7Nr8NQ7QmY^4~jvf0-`%waOLn zdNjAPaC0_7c|RVhw)+71NWjRi!y>C+Bl;Z`NiL^zn2*0kmj5gyhCLCxts*cWCdRI| zjsd=sT5BVJc^$GxP~YF$-U{-?kW6r@^vHXB%{CqYzU@1>dzf#3SYedJG-Rm6^RB7s zGM5PR(yKPKR)>?~vpUIeTP7A1sc8-knnJk*9)3t^e%izbdm>Y=W{$wm(cy1RB-19i za#828DMBY+ps#7Y8^6t)=Ea@%Nkt)O6JCx|ybC;Ap}Z@Zw~*}3P>MZLPb4Enxz9Wf zssobT^(R@KuShj8>@!1M7tm|2%-pYYDxz-5`rCbaTCG5{;Uxm z*g=+H1X8{NUvFGzz~wXa%Eo};I;~`37*WrRU&K0dPSB$yk(Z*@K&+mFal^?c zurbqB-+|Kb5|sznT;?Pj!+kgFY1#Dr;_%A(GIQC{3ct|{*Bji%FNa6c-thbpBkA;U zURV!Dr&X{0J}iht#-Qp2=xzuh(fM>zRoiGrYl5ttw2#r34gC41CCOC31m~^UPTK@s z6;A@)7O7_%C)>bnAXerYuAHdE93>j2N}H${zEc6&SbZ|-fiG*-qtGuy-qDelH(|u$ zorf8_T6Zqe#Ub!+e3oSyrskt_HyW_^5lrWt#30l)tHk|j$@YyEkXUOV;6B51L;M@=NIWZXU;GrAa(LGxO%|im%7F<-6N;en0Cr zLH>l*y?pMwt`1*cH~LdBPFY_l;~`N!Clyfr;7w<^X;&(ZiVdF1S5e(+Q%60zgh)s4 zn2yj$+mE=miVERP(g8}G4<85^-5f@qxh2ec?n+$A_`?qN=iyT1?U@t?V6DM~BIlBB z>u~eXm-aE>R0sQy!-I4xtCNi!!qh?R1!kKf6BoH2GG{L4%PAz0{Sh6xpuyI%*~u)s z%rLuFl)uQUCBQAtMyN;%)zFMx4loh7uTfKeB2Xif`lN?2gq6NhWhfz0u5WP9J>=V2 zo{mLtSy&BA!mSzs&CrKWq^y40JF5a&GSXIi2= z{EYb59J4}VwikL4P=>+mc6{($FNE@e=VUwG+KV21;<@lrN`mnz5jYGASyvz7BOG_6(p^eTxD-4O#lROgon;R35=|nj#eHIfJBYPWG>H>`dHKCDZ3`R{-?HO0mE~(5_WYcFmp8sU?wr*UkAQiNDGc6T zA%}GOLXlOWqL?WwfHO8MB#8M8*~Y*gz;1rWWoVSXP&IbKxbQ8+s%4Jnt?kDsq7btI zCDr0PZ)b;B%!lu&CT#RJzm{l{2fq|BcY85`w~3LSK<><@(2EdzFLt9Y_`;WXL6x`0 zDoQ?=?I@Hbr;*VVll1Gmd8*%tiXggMK81a+T(5Gx6;eNb8=uYn z5BG-0g>pP21NPn>$ntBh>`*})Fl|38oC^9Qz>~MAazH%3Q~Qb!ALMf$srexgPZ2@&c~+hxRi1;}+)-06)!#Mq<6GhP z-Q?qmgo${aFBApb5p}$1OJKTClfi8%PpnczyVKkoHw7Ml9e7ikrF0d~UB}i3vizos zXW4DN$SiEV9{faLt5bHy2a>33K%7Td-n5C*N;f&ZqAg#2hIqEb(y<&f4u5BWJ>2^4 z414GosL=Aom#m&=x_v<0-fp1r%oVJ{T-(xnomNJ(Dryv zh?vj+%=II_nV+@NR+(!fZZVM&(W6{6%9cm+o+Z6}KqzLw{(>E86uA1`_K$HqINlb1 zKelh3-jr2I9V?ych`{hta9wQ2c9=MM`2cC{m6^MhlL2{DLv7C^j z$xXBCnDl_;l|bPGMX@*tV)B!c|4oZyftUlP*?$YU9C_eAsuVHJ58?)zpbr30P*C`T z7y#ao`uE-SOG(Pi+`$=e^mle~)pRrdwL5)N;o{gpW21of(QE#U6w%*C~`v-z0QqBML!!5EeYA5IQB0 z^l01c;L6E(iytN!LhL}wfwP7W9PNAkb+)Cst?qg#$n;z41O4&v+8-zPs+XNb-q zIeeBCh#ivnFLUCwfS;p{LC0O7tm+Sf9Jn)~b%uwP{%69;QC)Ok0t%*a5M+=;y8j=v z#!*pp$9@!x;UMIs4~hP#pnfVc!%-D<+wsG@R2+J&%73lK|2G!EQC)O05TCV=&3g)C!lT=czLpZ@Sa%TYuoE?v8T8`V;e$#Zf2_Nj6nvBgh1)2 GZ~q4|mN%#X literal 61608 zcmb5VV{~QRw)Y#`wrv{~+qP{x72B%VwzFc}c2cp;N~)5ZbDrJayPv(!dGEd-##*zr z)#n-$y^sH|_dchh3@8{H5D*j;5D<{i*8l5IFJ|DjL!e)upfGNX(kojugZ3I`oH1PvW`wFW_ske0j@lB9bX zO;2)`y+|!@X(fZ1<2n!Qx*)_^Ai@Cv-dF&(vnudG?0CsddG_&Wtae(n|K59ew)6St z#dj7_(Cfwzh$H$5M!$UDd8=4>IQsD3xV=lXUq($;(h*$0^yd+b{qq63f0r_de#!o_ zXDngc>zy`uor)4A^2M#U*DC~i+dc<)Tb1Tv&~Ev@oM)5iJ4Sn#8iRw16XXuV50BS7 zdBL5Mefch(&^{luE{*5qtCZk$oFr3RH=H!c3wGR=HJ(yKc_re_X9pD` zJ;uxPzUfVpgU>DSq?J;I@a+10l0ONXPcDkiYcihREt5~T5Gb}sT0+6Q;AWHl`S5dV>lv%-p9l#xNNy7ZCr%cyqHY%TZ8Q4 zbp&#ov1*$#grNG#1vgfFOLJCaNG@K|2!W&HSh@3@Y%T?3YI75bJp!VP*$*!< z;(ffNS_;@RJ`=c7yX04!u3JP*<8jeqLHVJu#WV&v6wA!OYJS4h<_}^QI&97-;=ojW zQ-1t)7wnxG*5I%U4)9$wlv5Fr;cIizft@&N+32O%B{R1POm$oap@&f| zh+5J{>U6ftv|vAeKGc|zC=kO(+l7_cLpV}-D#oUltScw})N>~JOZLU_0{Ka2e1evz z{^a*ZrLr+JUj;)K&u2CoCAXLC2=fVScI(m_p~0FmF>>&3DHziouln?;sxW`NB}cSX z8?IsJB)Z=aYRz!X=yJn$kyOWK%rCYf-YarNqKzmWu$ZvkP12b4qH zhS9Q>j<}(*frr?z<%9hl*i^#@*O2q(Z^CN)c2c z>1B~D;@YpG?G!Yk+*yn4vM4sO-_!&m6+`k|3zd;8DJnxsBYtI;W3We+FN@|tQ5EW= z!VU>jtim0Mw#iaT8t_<+qKIEB-WwE04lBd%Letbml9N!?SLrEG$nmn7&W(W`VB@5S zaY=sEw2}i@F_1P4OtEw?xj4@D6>_e=m=797#hg}f*l^`AB|Y0# z9=)o|%TZFCY$SzgSjS|8AI-%J4x}J)!IMxY3_KYze`_I=c1nmrk@E8c9?MVRu)7+Ue79|)rBX7tVB7U|w4*h(;Gi3D9le49B38`wuv zp7{4X^p+K4*$@gU(Tq3K1a#3SmYhvI42)GzG4f|u zwQFT1n_=n|jpi=70-yE9LA+d*T8u z`=VmmXJ_f6WmZveZPct$Cgu^~gFiyL>Lnpj*6ee>*0pz=t$IJ}+rE zsf@>jlcG%Wx;Cp5x)YSVvB1$yyY1l&o zvwX=D7k)Dn;ciX?Z)Pn8$flC8#m`nB&(8?RSdBvr?>T9?E$U3uIX7T?$v4dWCa46 z+&`ot8ZTEgp7G+c52oHJ8nw5}a^dwb_l%MOh(ebVj9>_koQP^$2B~eUfSbw9RY$_< z&DDWf2LW;b0ZDOaZ&2^i^g+5uTd;GwO(-bbo|P^;CNL-%?9mRmxEw~5&z=X^Rvbo^WJW=n_%*7974RY}JhFv46> zd}`2|qkd;89l}R;i~9T)V-Q%K)O=yfVKNM4Gbacc7AOd>#^&W&)Xx!Uy5!BHnp9kh z`a(7MO6+Ren#>R^D0K)1sE{Bv>}s6Rb9MT14u!(NpZOe-?4V=>qZ>}uS)!y~;jEUK z&!U7Fj&{WdgU#L0%bM}SYXRtM5z!6M+kgaMKt%3FkjWYh=#QUpt$XX1!*XkpSq-pl zhMe{muh#knk{9_V3%qdDcWDv}v)m4t9 zQhv{;} zc{}#V^N3H>9mFM8`i`0p+fN@GqX+kl|M94$BK3J-X`Hyj8r!#x6Vt(PXjn?N)qedP z=o1T^#?1^a{;bZ&x`U{f?}TMo8ToN zkHj5v|}r}wDEi7I@)Gj+S1aE-GdnLN+$hw!=DzglMaj#{qjXi_dwpr|HL(gcCXwGLEmi|{4&4#OZ4ChceA zKVd4K!D>_N=_X;{poT~4Q+!Le+ZV>=H7v1*l%w`|`Dx8{)McN@NDlQyln&N3@bFpV z_1w~O4EH3fF@IzJ9kDk@7@QctFq8FbkbaH7K$iX=bV~o#gfh?2JD6lZf(XP>~DACF)fGFt)X%-h1yY~MJU{nA5 ze2zxWMs{YdX3q5XU*9hOH0!_S24DOBA5usB+Ws$6{|AMe*joJ?RxfV}*7AKN9V*~J zK+OMcE@bTD>TG1*yc?*qGqjBN8mgg@h1cJLDv)0!WRPIkC` zZrWXrceVw;fB%3`6kq=a!pq|hFIsQ%ZSlo~)D z|64!aCnw-?>}AG|*iOl44KVf8@|joXi&|)1rB;EQWgm+iHfVbgllP$f!$Wf42%NO5b(j9Bw6L z;0dpUUK$5GX4QbMlTmLM_jJt!ur`_0~$b#BB7FL*%XFf<b__1o)Ao3rlobbN8-(T!1d-bR8D3S0@d zLI!*GMb5s~Q<&sjd}lBb8Nr0>PqE6_!3!2d(KAWFxa{hm`@u|a(%#i(#f8{BP2wbs zt+N_slWF4IF_O|{w`c~)Xvh&R{Au~CFmW#0+}MBd2~X}t9lz6*E7uAD`@EBDe$>7W zzPUkJx<`f$0VA$=>R57^(K^h86>09?>_@M(R4q($!Ck6GG@pnu-x*exAx1jOv|>KH zjNfG5pwm`E-=ydcb+3BJwuU;V&OS=6yM^4Jq{%AVqnTTLwV`AorIDD}T&jWr8pB&j28fVtk_y*JRP^t@l*($UZ z6(B^-PBNZ+z!p?+e8@$&jCv^EWLb$WO=}Scr$6SM*&~B95El~;W_0(Bvoha|uQ1T< zO$%_oLAwf1bW*rKWmlD+@CP&$ObiDy=nh1b2ejz%LO9937N{LDe7gle4i!{}I$;&Y zkexJ9Ybr+lrCmKWg&}p=`2&Gf10orS?4$VrzWidT=*6{KzOGMo?KI0>GL0{iFWc;C z+LPq%VH5g}6V@-tg2m{C!-$fapJ9y}c$U}aUmS{9#0CM*8pC|sfer!)nG7Ji>mfRh z+~6CxNb>6eWKMHBz-w2{mLLwdA7dA-qfTu^A2yG1+9s5k zcF=le_UPYG&q!t5Zd_*E_P3Cf5T6821bO`daa`;DODm8Ih8k89=RN;-asHIigj`n=ux>*f!OC5#;X5i;Q z+V!GUy0|&Y_*8k_QRUA8$lHP;GJ3UUD08P|ALknng|YY13)}!!HW@0z$q+kCH%xet zlWf@BXQ=b=4}QO5eNnN~CzWBbHGUivG=`&eWK}beuV*;?zt=P#pM*eTuy3 zP}c#}AXJ0OIaqXji78l;YrP4sQe#^pOqwZUiiN6^0RCd#D271XCbEKpk`HI0IsN^s zES7YtU#7=8gTn#lkrc~6)R9u&SX6*Jk4GFX7){E)WE?pT8a-%6P+zS6o&A#ml{$WX zABFz#i7`DDlo{34)oo?bOa4Z_lNH>n;f0nbt$JfAl~;4QY@}NH!X|A$KgMmEsd^&Y zt;pi=>AID7ROQfr;MsMtClr5b0)xo|fwhc=qk33wQ|}$@?{}qXcmECh>#kUQ-If0$ zseb{Wf4VFGLNc*Rax#P8ko*=`MwaR-DQ8L8V8r=2N{Gaips2_^cS|oC$+yScRo*uF zUO|5=?Q?{p$inDpx*t#Xyo6=s?bbN}y>NNVxj9NZCdtwRI70jxvm3!5R7yiWjREEd zDUjrsZhS|P&|Ng5r+f^kA6BNN#|Se}_GF>P6sy^e8kBrgMv3#vk%m}9PCwUWJg-AD zFnZ=}lbi*mN-AOm zCs)r=*YQAA!`e#1N>aHF=bb*z*hXH#Wl$z^o}x##ZrUc=kh%OHWhp=7;?8%Xj||@V?1c ziWoaC$^&04;A|T)!Zd9sUzE&$ODyJaBpvqsw19Uiuq{i#VK1!htkdRWBnb z`{rat=nHArT%^R>u#CjjCkw-7%g53|&7z-;X+ewb?OLWiV|#nuc8mp*LuGSi3IP<<*Wyo9GKV7l0Noa4Jr0g3p_$ z*R9{qn=?IXC#WU>48-k5V2Oc_>P;4_)J@bo1|pf=%Rcbgk=5m)CJZ`caHBTm3%!Z9 z_?7LHr_BXbKKr=JD!%?KhwdYSdu8XxPoA{n8^%_lh5cjRHuCY9Zlpz8g+$f@bw@0V z+6DRMT9c|>1^3D|$Vzc(C?M~iZurGH2pXPT%F!JSaAMdO%!5o0uc&iqHx?ImcX6fI zCApkzc~OOnfzAd_+-DcMp&AOQxE_EsMqKM{%dRMI5`5CT&%mQO?-@F6tE*xL?aEGZ z8^wH@wRl`Izx4sDmU>}Ym{ybUm@F83qqZPD6nFm?t?(7>h*?`fw)L3t*l%*iw0Qu#?$5eq!Qc zpQvqgSxrd83NsdO@lL6#{%lsYXWen~d3p4fGBb7&5xqNYJ)yn84!e1PmPo7ChVd%4 zHUsV0Mh?VpzZD=A6%)Qrd~i7 z96*RPbid;BN{Wh?adeD_p8YU``kOrGkNox3D9~!K?w>#kFz!4lzOWR}puS(DmfjJD z`x0z|qB33*^0mZdM&6$|+T>fq>M%yoy(BEjuh9L0>{P&XJ3enGpoQRx`v6$txXt#c z0#N?b5%srj(4xmPvJxrlF3H%OMB!jvfy z;wx8RzU~lb?h_}@V=bh6p8PSb-dG|-T#A?`c&H2`_!u+uenIZe`6f~A7r)`9m8atC zt(b|6Eg#!Q*DfRU=Ix`#B_dK)nnJ_+>Q<1d7W)eynaVn`FNuN~%B;uO2}vXr5^zi2 z!ifIF5@Zlo0^h~8+ixFBGqtweFc`C~JkSq}&*a3C}L?b5Mh-bW=e)({F_g4O3 zb@SFTK3VD9QuFgFnK4Ve_pXc3{S$=+Z;;4+;*{H}Rc;845rP?DLK6G5Y-xdUKkA6E3Dz&5f{F^FjJQ(NSpZ8q-_!L3LL@H* zxbDF{gd^U3uD;)a)sJwAVi}7@%pRM&?5IaUH%+m{E)DlA_$IA1=&jr{KrhD5q&lTC zAa3c)A(K!{#nOvenH6XrR-y>*4M#DpTTOGQEO5Jr6kni9pDW`rvY*fs|ItV;CVITh z=`rxcH2nEJpkQ^(;1c^hfb8vGN;{{oR=qNyKtR1;J>CByul*+=`NydWnSWJR#I2lN zTvgnR|MBx*XFsfdA&;tr^dYaqRZp*2NwkAZE6kV@1f{76e56eUmGrZ>MDId)oqSWw z7d&r3qfazg+W2?bT}F)4jD6sWaw`_fXZGY&wnGm$FRPFL$HzVTH^MYBHWGCOk-89y zA+n+Q6EVSSCpgC~%uHfvyg@ufE^#u?JH?<73A}jj5iILz4Qqk5$+^U(SX(-qv5agK znUkfpke(KDn~dU0>gdKqjTkVk`0`9^0n_wzXO7R!0Thd@S;U`y)VVP&mOd-2 z(hT(|$=>4FY;CBY9#_lB$;|Wd$aOMT5O_3}DYXEHn&Jrc3`2JiB`b6X@EUOD zVl0S{ijm65@n^19T3l%>*;F(?3r3s?zY{thc4%AD30CeL_4{8x6&cN}zN3fE+x<9; zt2j1RRVy5j22-8U8a6$pyT+<`f+x2l$fd_{qEp_bfxfzu>ORJsXaJn4>U6oNJ#|~p z`*ZC&NPXl&=vq2{Ne79AkQncuxvbOG+28*2wU$R=GOmns3W@HE%^r)Fu%Utj=r9t` zd;SVOnA(=MXgnOzI2@3SGKHz8HN~Vpx&!Ea+Df~`*n@8O=0!b4m?7cE^K*~@fqv9q zF*uk#1@6Re_<^9eElgJD!nTA@K9C732tV~;B`hzZ321Ph=^BH?zXddiu{Du5*IPg} zqDM=QxjT!Rp|#Bkp$(mL)aar)f(dOAXUiw81pX0DC|Y4;>Vz>>DMshoips^8Frdv} zlTD=cKa48M>dR<>(YlLPOW%rokJZNF2gp8fwc8b2sN+i6&-pHr?$rj|uFgktK@jg~ zIFS(%=r|QJ=$kvm_~@n=ai1lA{7Z}i+zj&yzY+!t$iGUy|9jH#&oTNJ;JW-3n>DF+ z3aCOzqn|$X-Olu_p7brzn`uk1F*N4@=b=m;S_C?#hy{&NE#3HkATrg?enaVGT^$qIjvgc61y!T$9<1B@?_ibtDZ{G zeXInVr5?OD_nS_O|CK3|RzzMmu+8!#Zb8Ik;rkIAR%6?$pN@d<0dKD2c@k2quB%s( zQL^<_EM6ow8F6^wJN1QcPOm|ehA+dP(!>IX=Euz5qqIq}Y3;ibQtJnkDmZ8c8=Cf3 zu`mJ!Q6wI7EblC5RvP*@)j?}W=WxwCvF3*5Up_`3*a~z$`wHwCy)2risye=1mSp%p zu+tD6NAK3o@)4VBsM!@);qgsjgB$kkCZhaimHg&+k69~drbvRTacWKH;YCK(!rC?8 zP#cK5JPHSw;V;{Yji=55X~S+)%(8fuz}O>*F3)hR;STU`z6T1aM#Wd+FP(M5*@T1P z^06O;I20Sk!bxW<-O;E081KRdHZrtsGJflFRRFS zdi5w9OVDGSL3 zNrC7GVsGN=b;YH9jp8Z2$^!K@h=r-xV(aEH@#JicPy;A0k1>g1g^XeR`YV2HfmqXY zYbRwaxHvf}OlCAwHoVI&QBLr5R|THf?nAevV-=~V8;gCsX>jndvNOcFA+DI+zbh~# zZ7`qNk&w+_+Yp!}j;OYxIfx_{f0-ONc?mHCiCUak=>j>~>YR4#w# zuKz~UhT!L~GfW^CPqG8Lg)&Rc6y^{%3H7iLa%^l}cw_8UuG;8nn9)kbPGXS}p3!L_ zd#9~5CrH8xtUd?{d2y^PJg+z(xIfRU;`}^=OlehGN2=?}9yH$4Rag}*+AWotyxfCJ zHx=r7ZH>j2kV?%7WTtp+-HMa0)_*DBBmC{sd$)np&GEJ__kEd`xB5a2A z*J+yx>4o#ZxwA{;NjhU*1KT~=ZK~GAA;KZHDyBNTaWQ1+;tOFFthnD)DrCn`DjBZ% zk$N5B4^$`n^jNSOr=t(zi8TN4fpaccsb`zOPD~iY=UEK$0Y70bG{idLx@IL)7^(pL z{??Bnu=lDeguDrd%qW1)H)H`9otsOL-f4bSu};o9OXybo6J!Lek`a4ff>*O)BDT_g z<6@SrI|C9klY(>_PfA^qai7A_)VNE4c^ZjFcE$Isp>`e5fLc)rg@8Q_d^Uk24$2bn z9#}6kZ2ZxS9sI(RqT7?El2@B+($>eBQrNi_k#CDJ8D9}8$mmm z4oSKO^F$i+NG)-HE$O6s1--6EzJa?C{x=QgK&c=)b(Q9OVoAXYEEH20G|q$}Hue%~ zO3B^bF=t7t48sN zWh_zA`w~|){-!^g?6Mqf6ieV zFx~aPUOJGR=4{KsW7I?<=J2|lY`NTU=lt=%JE9H1vBpkcn=uq(q~=?iBt_-r(PLBM zP-0dxljJO>4Wq-;stY)CLB4q`-r*T$!K2o}?E-w_i>3_aEbA^MB7P5piwt1dI-6o!qWCy0 ztYy!x9arGTS?kabkkyv*yxvsPQ7Vx)twkS6z2T@kZ|kb8yjm+^$|sEBmvACeqbz)RmxkkDQX-A*K!YFziuhwb|ym>C$}U|J)4y z$(z#)GH%uV6{ec%Zy~AhK|+GtG8u@c884Nq%w`O^wv2#A(&xH@c5M`Vjk*SR_tJnq z0trB#aY)!EKW_}{#L3lph5ow=@|D5LzJYUFD6 z7XnUeo_V0DVSIKMFD_T0AqAO|#VFDc7c?c-Q%#u00F%!_TW1@JVnsfvm@_9HKWflBOUD~)RL``-!P;(bCON_4eVdduMO>?IrQ__*zE@7(OX zUtfH@AX*53&xJW*Pu9zcqxGiM>xol0I~QL5B%Toog3Jlenc^WbVgeBvV8C8AX^Vj& z^I}H})B=VboO%q1;aU5ACMh{yK4J;xlMc`jCnZR^!~LDs_MP&8;dd@4LDWw~*>#OT zeZHwdQWS!tt5MJQI~cw|Ka^b4c|qyd_ly(+Ql2m&AAw^ zQeSXDOOH!!mAgzAp0z)DD>6Xo``b6QwzUV@w%h}Yo>)a|xRi$jGuHQhJVA%>)PUvK zBQ!l0hq<3VZ*RnrDODP)>&iS^wf64C;MGqDvx>|p;35%6(u+IHoNbK z;Gb;TneFo*`zUKS6kwF*&b!U8e5m4YAo03a_e^!5BP42+r)LFhEy?_7U1IR<; z^0v|DhCYMSj<-;MtY%R@Fg;9Kky^pz_t2nJfKWfh5Eu@_l{^ph%1z{jkg5jQrkvD< z#vdK!nku*RrH~TdN~`wDs;d>XY1PH?O<4^U4lmA|wUW{Crrv#r%N>7k#{Gc44Fr|t z@UZP}Y-TrAmnEZ39A*@6;ccsR>)$A)S>$-Cj!=x$rz7IvjHIPM(TB+JFf{ehuIvY$ zsDAwREg*%|=>Hw$`us~RP&3{QJg%}RjJKS^mC_!U;E5u>`X`jW$}P`Mf}?7G7FX#{ zE(9u1SO;3q@ZhDL9O({-RD+SqqPX)`0l5IQu4q)49TUTkxR(czeT}4`WV~pV*KY&i zAl3~X%D2cPVD^B43*~&f%+Op)wl<&|D{;=SZwImydWL6@_RJjxP2g)s=dH)u9Npki zs~z9A+3fj0l?yu4N0^4aC5x)Osnm0qrhz@?nwG_`h(71P znbIewljU%T*cC=~NJy|)#hT+lx#^5MuDDnkaMb*Efw9eThXo|*WOQzJ*#3dmRWm@! zfuSc@#kY{Um^gBc^_Xdxnl!n&y&}R4yAbK&RMc+P^Ti;YIUh|C+K1|=Z^{nZ}}rxH*v{xR!i%qO~o zTr`WDE@k$M9o0r4YUFFeQO7xCu_Zgy)==;fCJ94M_rLAv&~NhfvcLWCoaGg2ao~3e zBG?Ms9B+efMkp}7BhmISGWmJsKI@a8b}4lLI48oWKY|8?zuuNc$lt5Npr+p7a#sWu zh!@2nnLBVJK!$S~>r2-pN||^w|fY`CT{TFnJy`B|e5;=+_v4l8O-fkN&UQbA4NKTyntd zqK{xEKh}U{NHoQUf!M=2(&w+eef77VtYr;xs%^cPfKLObyOV_9q<(%76-J%vR>w9!us-0c-~Y?_EVS%v!* z15s2s3eTs$Osz$JayyH|5nPAIPEX=U;r&p;K14G<1)bvn@?bM5kC{am|C5%hyxv}a z(DeSKI5ZfZ1*%dl8frIX2?);R^^~LuDOpNpk-2R8U1w92HmG1m&|j&J{EK=|p$;f9 z7Rs5|jr4r8k5El&qcuM+YRlKny%t+1CgqEWO>3;BSRZi(LA3U%Jm{@{y+A+w(gzA< z7dBq6a1sEWa4cD0W7=Ld9z0H7RI^Z7vl(bfA;72j?SWCo`#5mVC$l1Q2--%V)-uN* z9ha*s-AdfbDZ8R8*fpwjzx=WvOtmSzGFjC#X)hD%Caeo^OWjS(3h|d9_*U)l%{Ab8 zfv$yoP{OuUl@$(-sEVNt{*=qi5P=lpxWVuz2?I7Dc%BRc+NGNw+323^ z5BXGfS71oP^%apUo(Y#xkxE)y?>BFzEBZ}UBbr~R4$%b7h3iZu3S(|A;&HqBR{nK& z$;GApNnz=kNO^FL&nYcfpB7Qg;hGJPsCW44CbkG1@l9pn0`~oKy5S777uH)l{irK!ru|X+;4&0D;VE*Ii|<3P zUx#xUqvZT5kVQxsF#~MwKnv7;1pR^0;PW@$@T7I?s`_rD1EGUdSA5Q(C<>5SzE!vw z;{L&kKFM-MO>hy#-8z`sdVx})^(Dc-dw;k-h*9O2_YZw}|9^y-|8RQ`BWJUJL(Cer zP5Z@fNc>pTXABbTRY-B5*MphpZv6#i802giwV&SkFCR zGMETyUm(KJbh+&$8X*RB#+{surjr;8^REEt`2&Dubw3$mx>|~B5IKZJ`s_6fw zKAZx9&PwBqW1Oz0r0A4GtnZd7XTKViX2%kPfv+^X3|_}RrQ2e3l=KG_VyY`H?I5&CS+lAX5HbA%TD9u6&s#v!G> zzW9n4J%d5ye7x0y`*{KZvqyXUfMEE^ZIffzI=Hh|3J}^yx7eL=s+TPH(Q2GT-sJ~3 zI463C{(ag7-hS1ETtU;_&+49ABt5!A7CwLwe z=SoA8mYZIQeU;9txI=zcQVbuO%q@E)JI+6Q!3lMc=Gbj(ASg-{V27u>z2e8n;Nc*pf}AqKz1D>p9G#QA+7mqqrEjGfw+85Uyh!=tTFTv3|O z+)-kFe_8FF_EkTw!YzwK^Hi^_dV5x-Ob*UWmD-})qKj9@aE8g240nUh=g|j28^?v7 zHRTBo{0KGaWBbyX2+lx$wgXW{3aUab6Bhm1G1{jTC7ota*JM6t+qy)c5<@ zpc&(jVdTJf(q3xB=JotgF$X>cxh7k*(T`-V~AR+`%e?YOeALQ2Qud( zz35YizXt(aW3qndR}fTw1p()Ol4t!D1pitGNL95{SX4ywzh0SF;=!wf=?Q?_h6!f* zh7<+GFi)q|XBsvXZ^qVCY$LUa{5?!CgwY?EG;*)0ceFe&=A;!~o`ae}Z+6me#^sv- z1F6=WNd6>M(~ z+092z>?Clrcp)lYNQl9jN-JF6n&Y0mp7|I0dpPx+4*RRK+VQI~>en0Dc;Zfl+x z_e_b7s`t1_A`RP3$H}y7F9_na%D7EM+**G_Z0l_nwE+&d_kc35n$Fxkd4r=ltRZhh zr9zER8>j(EdV&Jgh(+i}ltESBK62m0nGH6tCBr90!4)-`HeBmz54p~QP#dsu%nb~W z7sS|(Iydi>C@6ZM(Us!jyIiszMkd)^u<1D+R@~O>HqZIW&kearPWmT>63%_t2B{_G zX{&a(gOYJx!Hq=!T$RZ&<8LDnxsmx9+TBL0gTk$|vz9O5GkK_Yx+55^R=2g!K}NJ3 zW?C;XQCHZl7H`K5^BF!Q5X2^Mj93&0l_O3Ea3!Ave|ixx+~bS@Iv18v2ctpSt4zO{ zp#7pj!AtDmti$T`e9{s^jf(ku&E|83JIJO5Qo9weT6g?@vX!{7)cNwymo1+u(YQ94 zopuz-L@|5=h8A!(g-MXgLJC0MA|CgQF8qlonnu#j z;uCeq9ny9QSD|p)9sp3ebgY3rk#y0DA(SHdh$DUm^?GI<>%e1?&}w(b zdip1;P2Z=1wM+$q=TgLP$}svd!vk+BZ@h<^4R=GS2+sri7Z*2f`9 z5_?i)xj?m#pSVchk-SR!2&uNhzEi+#5t1Z$o0PoLGz*pT64%+|Wa+rd5Z}60(j?X= z{NLjtgRb|W?CUADqOS@(*MA-l|E342NxRaxLTDqsOyfWWe%N(jjBh}G zm7WPel6jXijaTiNita+z(5GCO0NM=Melxud57PP^d_U## zbA;9iVi<@wr0DGB8=T9Ab#2K_#zi=$igyK48@;V|W`fg~7;+!q8)aCOo{HA@vpSy-4`^!ze6-~8|QE||hC{ICKllG9fbg_Y7v z$jn{00!ob3!@~-Z%!rSZ0JO#@>|3k10mLK0JRKP-Cc8UYFu>z93=Ab-r^oL2 zl`-&VBh#=-?{l1TatC;VweM^=M7-DUE>m+xO7Xi6vTEsReyLs8KJ+2GZ&rxw$d4IT zPXy6pu^4#e;;ZTsgmG+ZPx>piodegkx2n0}SM77+Y*j^~ICvp#2wj^BuqRY*&cjmL zcKp78aZt>e{3YBb4!J_2|K~A`lN=u&5j!byw`1itV(+Q_?RvV7&Z5XS1HF)L2v6ji z&kOEPmv+k_lSXb{$)of~(BkO^py&7oOzpjdG>vI1kcm_oPFHy38%D4&A4h_CSo#lX z2#oqMCTEP7UvUR3mwkPxbl8AMW(e{ARi@HCYLPSHE^L<1I}OgZD{I#YH#GKnpRmW3 z2jkz~Sa(D)f?V?$gNi?6)Y;Sm{&?~2p=0&BUl_(@hYeX8YjaRO=IqO7neK0RsSNdYjD zaw$g2sG(>JR=8Iz1SK4`*kqd_3-?;_BIcaaMd^}<@MYbYisWZm2C2|Np_l|8r9yM|JkUngSo@?wci(7&O9a z%|V(4C1c9pps0xxzPbXH=}QTxc2rr7fXk$9`a6TbWKPCz&p=VsB8^W96W=BsB|7bc zf(QR8&Ktj*iz)wK&mW`#V%4XTM&jWNnDF56O+2bo<3|NyUhQ%#OZE8$Uv2a@J>D%t zMVMiHh?es!Ex19q&6eC&L=XDU_BA&uR^^w>fpz2_`U87q_?N2y;!Z!bjoeKrzfC)} z?m^PM=(z{%n9K`p|7Bz$LuC7!>tFOuN74MFELm}OD9?%jpT>38J;=1Y-VWtZAscaI z_8jUZ#GwWz{JqvGEUmL?G#l5E=*m>`cY?m*XOc*yOCNtpuIGD+Z|kn4Xww=BLrNYS zGO=wQh}Gtr|7DGXLF%|`G>J~l{k^*{;S-Zhq|&HO7rC_r;o`gTB7)uMZ|WWIn@e0( zX$MccUMv3ABg^$%_lNrgU{EVi8O^UyGHPNRt%R!1#MQJn41aD|_93NsBQhP80yP<9 zG4(&0u7AtJJXLPcqzjv`S~5;Q|5TVGccN=Uzm}K{v)?f7W!230C<``9(64}D2raRU zAW5bp%}VEo{4Rko`bD%Ehf=0voW?-4Mk#d3_pXTF!-TyIt6U+({6OXWVAa;s-`Ta5 zTqx&8msH3+DLrVmQOTBOAj=uoxKYT3DS1^zBXM?1W+7gI!aQNPYfUl{3;PzS9*F7g zWJN8x?KjBDx^V&6iCY8o_gslO16=kh(|Gp)kz8qlQ`dzxQv;)V&t+B}wwdi~uBs4? zu~G|}y!`3;8#vIMUdyC7YEx6bb^1o}G!Jky4cN?BV9ejBfN<&!4M)L&lRKiuMS#3} z_B}Nkv+zzxhy{dYCW$oGC&J(Ty&7%=5B$sD0bkuPmj7g>|962`(Q{ZZMDv%YMuT^KweiRDvYTEop3IgFv#)(w>1 zSzH>J`q!LK)c(AK>&Ib)A{g`Fdykxqd`Yq@yB}E{gnQV$K!}RsgMGWqC3DKE(=!{}ekB3+(1?g}xF>^icEJbc z5bdxAPkW90atZT+&*7qoLqL#p=>t-(-lsnl2XMpZcYeW|o|a322&)yO_8p(&Sw{|b zn(tY$xn5yS$DD)UYS%sP?c|z>1dp!QUD)l;aW#`%qMtQJjE!s2z`+bTSZmLK7SvCR z=@I4|U^sCwZLQSfd*ACw9B@`1c1|&i^W_OD(570SDLK`MD0wTiR8|$7+%{cF&){$G zU~|$^Ed?TIxyw{1$e|D$050n8AjJvvOWhLtLHbSB|HIfjMp+gu>DraHZJRrdO53(= z+o-f{+qNog+qSLB%KY;5>Av6X(>-qYk3IIEwZ5~6a+P9lMpC^ z8CJ0q>rEpjlsxCvJm=kms@tlN4+sv}He`xkr`S}bGih4t`+#VEIt{1veE z{ZLtb_pSbcfcYPf4=T1+|BtR!x5|X#x2TZEEkUB6kslKAE;x)*0x~ES0kl4Dex4e- zT2P~|lT^vUnMp{7e4OExfxak0EE$Hcw;D$ehTV4a6hqxru0$|Mo``>*a5=1Ym0u>BDJKO|=TEWJ5jZu!W}t$Kv{1!q`4Sn7 zrxRQOt>^6}Iz@%gA3&=5r;Lp=N@WKW;>O!eGIj#J;&>+3va^~GXRHCY2}*g#9ULab zitCJt-OV0*D_Q3Q`p1_+GbPxRtV_T`jyATjax<;zZ?;S+VD}a(aN7j?4<~>BkHK7bO8_Vqfdq1#W&p~2H z&w-gJB4?;Q&pG9%8P(oOGZ#`!m>qAeE)SeL*t8KL|1oe;#+uOK6w&PqSDhw^9-&Fa zuEzbi!!7|YhlWhqmiUm!muO(F8-F7|r#5lU8d0+=;<`{$mS=AnAo4Zb^{%p}*gZL! zeE!#-zg0FWsSnablw!9$<&K(#z!XOW z;*BVx2_+H#`1b@>RtY@=KqD)63brP+`Cm$L1@ArAddNS1oP8UE$p05R=bvZoYz+^6 z<)!v7pRvi!u_-V?!d}XWQR1~0q(H3{d^4JGa=W#^Z<@TvI6J*lk!A zZ*UIKj*hyO#5akL*Bx6iPKvR3_2-^2mw|Rh-3O_SGN3V9GRo52Q;JnW{iTGqb9W99 z7_+F(Op6>~3P-?Q8LTZ-lwB}xh*@J2Ni5HhUI3`ct|*W#pqb>8i*TXOLn~GlYECIj zhLaa_rBH|1jgi(S%~31Xm{NB!30*mcsF_wgOY2N0XjG_`kFB+uQuJbBm3bIM$qhUyE&$_u$gb zpK_r{99svp3N3p4yHHS=#csK@j9ql*>j0X=+cD2dj<^Wiu@i>c_v zK|ovi7}@4sVB#bzq$n3`EgI?~xDmkCW=2&^tD5RuaSNHf@Y!5C(Is$hd6cuyoK|;d zO}w2AqJPS`Zq+(mc*^%6qe>1d&(n&~()6-ZATASNPsJ|XnxelLkz8r1x@c2XS)R*H(_B=IN>JeQUR;T=i3<^~;$<+8W*eRKWGt7c#>N`@;#!`kZ!P!&{9J1>_g8Zj zXEXxmA=^{8A|3=Au+LfxIWra)4p<}1LYd_$1KI0r3o~s1N(x#QYgvL4#2{z8`=mXy zQD#iJ0itk1d@Iy*DtXw)Wz!H@G2St?QZFz zVPkM%H8Cd2EZS?teQN*Ecnu|PrC!a7F_XX}AzfZl3fXfhBtc2-)zaC2eKx*{XdM~QUo4IwcGgVdW69 z1UrSAqqMALf^2|(I}hgo38l|Ur=-SC*^Bo5ej`hb;C$@3%NFxx5{cxXUMnTyaX{>~ zjL~xm;*`d08bG_K3-E+TI>#oqIN2=An(C6aJ*MrKlxj?-;G zICL$hi>`F%{xd%V{$NhisHSL~R>f!F7AWR&7b~TgLu6!3s#~8|VKIX)KtqTH5aZ8j zY?wY)XH~1_a3&>#j7N}0az+HZ;is;Zw(Am{MX}YhDTe(t{ZZ;TG}2qWYO+hdX}vp9 z@uIRR8g#y~-^E`Qyem(31{H0&V?GLdq9LEOb2(ea#e-$_`5Q{T%E?W(6 z(XbX*Ck%TQM;9V2LL}*Tf`yzai{0@pYMwBu%(I@wTY!;kMrzcfq0w?X`+y@0ah510 zQX5SU(I!*Fag4U6a7Lw%LL;L*PQ}2v2WwYF(lHx_Uz2ceI$mnZ7*eZ?RFO8UvKI0H z9Pq-mB`mEqn6n_W9(s~Jt_D~j!Ln9HA)P;owD-l~9FYszs)oEKShF9Zzcmnb8kZ7% zQ`>}ki1kwUO3j~ zEmh140sOkA9v>j@#56ymn_RnSF`p@9cO1XkQy6_Kog?0ivZDb`QWOX@tjMd@^Qr(p z!sFN=A)QZm!sTh(#q%O{Ovl{IxkF!&+A)w2@50=?a-+VuZt6On1;d4YtUDW{YNDN_ zG@_jZi1IlW8cck{uHg^g=H58lPQ^HwnybWy@@8iw%G! zwB9qVGt_?~M*nFAKd|{cGg+8`+w{j_^;nD>IrPf-S%YjBslSEDxgKH{5p)3LNr!lD z4ii)^%d&cCXIU7UK?^ZQwmD(RCd=?OxmY(Ko#+#CsTLT;p#A%{;t5YpHFWgl+@)N1 zZ5VDyB;+TN+g@u~{UrWrv)&#u~k$S&GeW)G{M#&Di)LdYk?{($Cq zZGMKeYW)aMtjmKgvF0Tg>Mmkf9IB#2tYmH-s%D_9y3{tfFmX1BSMtbe<(yqAyWX60 zzkgSgKb3c{QPG2MalYp`7mIrYg|Y<4Jk?XvJK)?|Ecr+)oNf}XLPuTZK%W>;<|r+% zTNViRI|{sf1v7CsWHvFrkQ$F7+FbqPQ#Bj7XX=#M(a~9^80}~l-DueX#;b}Ajn3VE z{BWI}$q{XcQ3g{(p>IOzFcAMDG0xL)H%wA)<(gl3I-oVhK~u_m=hAr&oeo|4lZbf} z+pe)c34Am<=z@5!2;_lwya;l?xV5&kWe}*5uBvckm(d|7R>&(iJNa6Y05SvlZcWBlE{{%2- z`86)Y5?H!**?{QbzGG~|k2O%eA8q=gxx-3}&Csf6<9BsiXC)T;x4YmbBIkNf;0Nd5 z%whM^!K+9zH>on_<&>Ws?^v-EyNE)}4g$Fk?Z#748e+GFp)QrQQETx@u6(1fk2!(W zWiCF~MomG*y4@Zk;h#2H8S@&@xwBIs|82R*^K(i*0MTE%Rz4rgO&$R zo9Neb;}_ulaCcdn3i17MO3NxzyJ=l;LU*N9ztBJ30j=+?6>N4{9YXg$m=^9@Cl9VY zbo^{yS@gU=)EpQ#;UIQBpf&zfCA;00H-ee=1+TRw@(h%W=)7WYSb5a%$UqNS@oI@= zDrq|+Y9e&SmZrH^iA>Of8(9~Cf-G(P^5Xb%dDgMMIl8gk6zdyh`D3OGNVV4P9X|EvIhplXDld8d z^YWtYUz@tpg*38Xys2?zj$F8%ivA47cGSl;hjD23#*62w3+fwxNE7M7zVK?x_`dBSgPK zWY_~wF~OEZi9|~CSH8}Xi>#8G73!QLCAh58W+KMJJC81{60?&~BM_0t-u|VsPBxn* zW7viEKwBBTsn_A{g@1!wnJ8@&h&d>!qAe+j_$$Vk;OJq`hrjzEE8Wjtm)Z>h=*M25 zOgETOM9-8xuuZ&^@rLObtcz>%iWe%!uGV09nUZ*nxJAY%&KAYGY}U1WChFik7HIw% zZP$3Bx|TG_`~19XV7kfi2GaBEhKap&)Q<9`aPs#^!kMjtPb|+-fX66z3^E)iwyXK7 z8)_p<)O{|i&!qxtgBvWXx8*69WO$5zACl++1qa;)0zlXf`eKWl!0zV&I`8?sG)OD2Vy?reNN<{eK+_ za4M;Hh%&IszR%)&gpgRCP}yheQ+l#AS-GnY81M!kzhWxIR?PW`G3G?} z$d%J28uQIuK@QxzGMKU_;r8P0+oIjM+k)&lZ39i#(ntY)*B$fdJnQ3Hw3Lsi8z&V+ zZly2}(Uzpt2aOubRjttzqrvinBFH4jrN)f0hy)tj4__UTwN)#1fj3-&dC_Vh7}ri* zfJ=oqLMJ-_<#rwVyN}_a-rFBe2>U;;1(7UKH!$L??zTbbzP#bvyg7OQBGQklJ~DgP zd<1?RJ<}8lWwSL)`jM53iG+}y2`_yUvC!JkMpbZyb&50V3sR~u+lok zT0uFRS-yx@8q4fPRZ%KIpLp8R#;2%c&Ra4p(GWRT4)qLaPNxa&?8!LRVdOUZ)2vrh zBSx&kB%#Y4!+>~)<&c>D$O}!$o{<1AB$M7-^`h!eW;c(3J~ztoOgy6Ek8Pwu5Y`Xion zFl9fb!k2`3uHPAbd(D^IZmwR5d8D$495nN2`Ue&`W;M-nlb8T-OVKt|fHk zBpjX$a(IR6*-swdNk@#}G?k6F-~c{AE0EWoZ?H|ZpkBxqU<0NUtvubJtwJ1mHV%9v?GdDw; zAyXZiD}f0Zdt-cl9(P1la+vQ$Er0~v}gYJVwQazv zH#+Z%2CIfOf90fNMGos|{zf&N`c0@x0N`tkFv|_9af3~<0z@mnf*e;%r*Fbuwl-IW z{}B3=(mJ#iwLIPiUP`J3SoP~#)6v;aRXJ)A-pD2?_2_CZ#}SAZ<#v7&Vk6{*i(~|5 z9v^nC`T6o`CN*n%&9+bopj^r|E(|pul;|q6m7Tx+U|UMjWK8o-lBSgc3ZF=rP{|l9 zc&R$4+-UG6i}c==!;I#8aDIbAvgLuB66CQLRoTMu~jdw`fPlKy@AKYWS-xyZzPg&JRAa@m-H43*+ne!8B7)HkQY4 zIh}NL4Q79a-`x;I_^>s$Z4J4-Ngq=XNWQ>yAUCoe&SMAYowP>r_O}S=V+3=3&(O=h zNJDYNs*R3Y{WLmBHc?mFEeA4`0Y`_CN%?8qbDvG2m}kMAiqCv`_BK z_6a@n`$#w6Csr@e2YsMx8udNWtNt=kcqDZdWZ-lGA$?1PA*f4?X*)hjn{sSo8!bHz zb&lGdAgBx@iTNPK#T_wy`KvOIZvTWqSHb=gWUCKXAiB5ckQI`1KkPx{{%1R*F2)Oc z(9p@yG{fRSWE*M9cdbrO^)8vQ2U`H6M>V$gK*rz!&f%@3t*d-r3mSW>D;wYxOhUul zk~~&ip5B$mZ~-F1orsq<|1bc3Zpw6)Ws5;4)HilsN;1tx;N6)tuePw& z==OlmaN*ybM&-V`yt|;vDz(_+UZ0m&&9#{9O|?0I|4j1YCMW;fXm}YT$0%EZ5^YEI z4i9WV*JBmEU{qz5O{#bs`R1wU%W$qKx?bC|e-iS&d*Qm7S=l~bMT{~m3iZl+PIXq{ zn-c~|l)*|NWLM%ysfTV-oR0AJ3O>=uB-vpld{V|cWFhI~sx>ciV9sPkC*3i0Gg_9G!=4ar*-W?D9)?EFL1=;O+W8}WGdp8TT!Fgv z{HKD`W>t(`Cds_qliEzuE!r{ihwEv1l5o~iqlgjAyGBi)$%zNvl~fSlg@M=C{TE;V zQkH`zS8b&!ut(m)%4n2E6MB>p*4(oV>+PT51#I{OXs9j1vo>9I<4CL1kv1aurV*AFZ^w_qfVL*G2rG@D2 zrs87oV3#mf8^E5hd_b$IXfH6vHe&lm@7On~Nkcq~YtE!}ad~?5*?X*>y`o;6Q9lkk zmf%TYonZM`{vJg$`lt@MXsg%*&zZZ0uUSse8o=!=bfr&DV)9Y6$c!2$NHyYAQf*Rs zk{^?gl9E z5Im8wlAsvQ6C2?DyG@95gUXZ3?pPijug25g;#(esF_~3uCj3~94}b*L>N2GSk%Qst z=w|Z>UX$m!ZOd(xV*2xvWjN&c5BVEdVZ0wvmk)I+YxnyK%l~caR=7uNQ=+cnNTLZ@&M!I$Mj-r{!P=; z`C2)D=VmvK8@T5S9JZoRtN!S*D_oqOxyy!q6Zk|~4aT|*iRN)fL)c>-yycR>-is0X zKrko-iZw(f(!}dEa?hef5yl%p0-v-8#8CX8!W#n2KNyT--^3hq6r&`)5Y@>}e^4h- zlPiDT^zt}Ynk&x@F8R&=)k8j$=N{w9qUcIc&)Qo9u4Y(Ae@9tA`3oglxjj6c{^pN( zQH+Uds2=9WKjH#KBIwrQI%bbs`mP=7V>rs$KG4|}>dxl_k!}3ZSKeEen4Iswt96GGw`E6^5Ov)VyyY}@itlj&sao|>Sb5 zeY+#1EK(}iaYI~EaHQkh7Uh>DnzcfIKv8ygx1Dv`8N8a6m+AcTa-f;17RiEed>?RT zk=dAksmFYPMV1vIS(Qc6tUO+`1jRZ}tcDP? zt)=7B?yK2RcAd1+Y!$K5*ds=SD;EEqCMG6+OqPoj{&8Y5IqP(&@zq@=A7+X|JBRi4 zMv!czlMPz)gt-St2VZwDD=w_S>gRpc-g zUd*J3>bXeZ?Psjohe;z7k|d<*T21PA1i)AOi8iMRwTBSCd0ses{)Q`9o&p9rsKeLaiY zluBw{1r_IFKR76YCAfl&_S1*(yFW8HM^T()&p#6y%{(j7Qu56^ZJx1LnN`-RTwimdnuo*M8N1ISl+$C-%=HLG-s} zc99>IXRG#FEWqSV9@GFW$V8!{>=lSO%v@X*pz*7()xb>=yz{E$3VE;e)_Ok@A*~El zV$sYm=}uNlUxV~6e<6LtYli1!^X!Ii$L~j4e{sI$tq_A(OkGquC$+>Rw3NFObV2Z)3Rt~Jr{oYGnZaFZ^g5TDZlg;gaeIP} z!7;T{(9h7mv{s@piF{-35L=Ea%kOp;^j|b5ZC#xvD^^n#vPH=)lopYz1n?Kt;vZmJ z!FP>Gs7=W{sva+aO9S}jh0vBs+|(B6Jf7t4F^jO3su;M13I{2rd8PJjQe1JyBUJ5v zcT%>D?8^Kp-70bP8*rulxlm)SySQhG$Pz*bo@mb5bvpLAEp${?r^2!Wl*6d7+0Hs_ zGPaC~w0E!bf1qFLDM@}zso7i~(``)H)zRgcExT_2#!YOPtBVN5Hf5~Ll3f~rWZ(UsJtM?O*cA1_W0)&qz%{bDoA}{$S&-r;0iIkIjbY~ zaAqH45I&ALpP=9Vof4OapFB`+_PLDd-0hMqCQq08>6G+C;9R~}Ug_nm?hhdkK$xpI zgXl24{4jq(!gPr2bGtq+hyd3%Fg%nofK`psHMs}EFh@}sdWCd!5NMs)eZg`ZlS#O0 zru6b8#NClS(25tXqnl{|Ax@RvzEG!+esNW-VRxba(f`}hGoqci$U(g30i}2w9`&z= zb8XjQLGN!REzGx)mg~RSBaU{KCPvQx8)|TNf|Oi8KWgv{7^tu}pZq|BS&S<53fC2K4Fw6>M^s$R$}LD*sUxdy6Pf5YKDbVet;P!bw5Al-8I1Nr(`SAubX5^D9hk6$agWpF}T#Bdf{b9-F#2WVO*5N zp+5uGgADy7m!hAcFz{-sS0kM7O)qq*rC!>W@St~^OW@R1wr{ajyYZq5H!T?P0e+)a zaQ%IL@X_`hzp~vRH0yUblo`#g`LMC%9}P;TGt+I7qNcBSe&tLGL4zqZqB!Bfl%SUa z6-J_XLrnm*WA`34&mF+&e1sPCP9=deazrM=Pc4Bn(nV;X%HG^4%Afv4CI~&l!Sjzb z{rHZ3od0!Al{}oBO>F*mOFAJrz>gX-vs!7>+_G%BB(ljWh$252j1h;9p~xVA=9_`P z5KoFiz96_QsTK%B&>MSXEYh`|U5PjX1(+4b#1PufXRJ*uZ*KWdth1<0 zsAmgjT%bowLyNDv7bTUGy|g~N34I-?lqxOUtFpTLSV6?o?<7-UFy*`-BEUsrdANh} zBWkDt2SAcGHRiqz)x!iVoB~&t?$yn6b#T=SP6Ou8lW=B>=>@ik93LaBL56ub`>Uo!>0@O8?e)$t(sgy$I z6tk3nS@yFFBC#aFf?!d_3;%>wHR;A3f2SP?Na8~$r5C1N(>-ME@HOpv4B|Ty7%jAv zR}GJwsiJZ5@H+D$^Cwj#0XA_(m^COZl8y7Vv(k=iav1=%QgBOVzeAiw zaDzzdrxzj%sE^c9_uM5D;$A_7)Ln}BvBx^=)fO+${ou%B*u$(IzVr-gH3=zL6La;G zu0Kzy5CLyNGoKRtK=G0-w|tnwI)puPDOakRzG(}R9fl7#<|oQEX;E#yCWVg95 z;NzWbyF&wGg_k+_4x4=z1GUcn6JrdX4nOVGaAQ8#^Ga>aFvajQN{!+9rgO-dHP zIp@%&ebVg}IqnRWwZRTNxLds+gz2@~VU(HI=?Epw>?yiEdZ>MjajqlO>2KDxA>)cj z2|k%dhh%d8SijIo1~20*5YT1eZTDkN2rc^zWr!2`5}f<2f%M_$to*3?Ok>e9$X>AV z2jYmfAd)s|(h?|B(XYrIfl=Wa_lBvk9R1KaP{90-z{xKi+&8=dI$W0+qzX|ZovWGOotP+vvYR(o=jo?k1=oG?%;pSqxcU* zWVGVMw?z__XQ9mnP!hziHC`ChGD{k#SqEn*ph6l46PZVkm>JF^Q{p&0=MKy_6apts z`}%_y+Tl_dSP(;Ja&sih$>qBH;bG;4;75)jUoVqw^}ee=ciV;0#t09AOhB^Py7`NC z-m+ybq1>_OO+V*Z>dhk}QFKA8V?9Mc4WSpzj{6IWfFpF7l^au#r7&^BK2Ac7vCkCn{m0uuN93Ee&rXfl1NBY4NnO9lFUp zY++C1I;_{#OH#TeP2Dp?l4KOF8ub?m6zE@XOB5Aiu$E~QNBM@;r+A5mF2W1-c7>ex zHiB=WJ&|`6wDq*+xv8UNLVUy4uW1OT>ey~Xgj@MMpS@wQbHAh>ysYvdl-1YH@&+Q! z075(Qd4C!V`9Q9jI4 zSt{HJRvZec>vaL_brKhQQwbpQd4_Lmmr0@1GdUeU-QcC{{8o=@nwwf>+dIKFVzPriGNX4VjHCa zTbL9w{Y2V87c2ofX%`(48A+4~mYTiFFl!e{3K^C_k%{&QTsgOd0*95KmWN)P}m zTRr{`f7@=v#+z_&fKYkQT!mJn{*crj%ZJz#(+c?>cD&2Lo~FFAWy&UG*Op^pV`BR^I|g?T>4l5;b|5OQ@t*?_Slp`*~Y3`&RfKD^1uLezIW(cE-Dq2z%I zBi8bWsz0857`6e!ahet}1>`9cYyIa{pe53Kl?8|Qg2RGrx@AlvG3HAL-^9c^1GW;)vQt8IK+ zM>!IW*~682A~MDlyCukldMd;8P|JCZ&oNL(;HZgJ>ie1PlaInK7C@Jg{3kMKYui?e!b`(&?t6PTb5UPrW-6DVU%^@^E`*y-Fd(p|`+JH&MzfEq;kikdse ziFOiDWH(D< zyV7Rxt^D0_N{v?O53N$a2gu%1pxbeK;&ua`ZkgSic~$+zvt~|1Yb=UfKJW2F7wC^evlPf(*El+#}ZBy0d4kbVJsK- z05>;>?HZO(YBF&v5tNv_WcI@O@LKFl*VO?L(!BAd!KbkVzo;v@~3v`-816GG?P zY+H3ujC>5=Am3RIZDdT#0G5A6xe`vGCNq88ZC1aVXafJkUlcYmHE^+Z{*S->ol%-O znm9R0TYTr2w*N8Vs#s-5=^w*{Y}qp5GG)Yt1oLNsH7y~N@>Eghms|K*Sdt_u!&I}$ z+GSdFTpbz%KH+?B%Ncy;C`uW6oWI46(tk>r|5|-K6)?O0d_neghUUOa9BXHP*>vi; z={&jIGMn-92HvInCMJcyXwHTJ42FZp&Wxu+9Rx;1x(EcIQwPUQ@YEQQ`bbMy4q3hP zNFoq~Qd0=|xS-R}k1Im3;8s{BnS!iaHIMLx)aITl)+)?Yt#fov|Eh>}dv@o6R{tG>uHsy&jGmWN5+*wAik|78(b?jtysPHC#e+Bzz~V zS3eEXv7!Qn4uWi!FS3B?afdD*{fr9>B~&tc671fi--V}~E4un;Q|PzZRwk-azprM$4AesvUb5`S`(5x#5VJ~4%ET6&%GR$}muHV-5lTsCi_R|6KM(g2PCD@|yOpKluT zakH!1V7nKN)?6JmC-zJoA#ciFux8!)ajiY%K#RtEg$gm1#oKUKX_Ms^%hvKWi|B=~ zLbl-L)-=`bfhl`>m!^sRR{}cP`Oim-{7}oz4p@>Y(FF5FUEOfMwO!ft6YytF`iZRq zfFr{!&0Efqa{1k|bZ4KLox;&V@ZW$997;+Ld8Yle91he{BfjRhjFTFv&^YuBr^&Pe zswA|Bn$vtifycN8Lxr`D7!Kygd7CuQyWqf}Q_PM}cX~S1$-6xUD%-jrSi24sBTFNz(Fy{QL2AmNbaVggWOhP;UY4D>S zqKr!UggZ9Pl9Nh_H;qI`-WoH{ceXj?m8y==MGY`AOJ7l0Uu z)>M%?dtaz2rjn1SW3k+p`1vs&lwb%msw8R!5nLS;upDSxViY98IIbxnh{}mRfEp=9 zbrPl>HEJeN7J=KnB6?dwEA6YMs~chHNG?pJsEj#&iUubdf3JJwu=C(t?JpE6xMyhA3e}SRhunDC zn-~83*9=mADUsk^sCc%&&G1q5T^HR9$P#2DejaG`Ui*z1hI#h7dwpIXg)C{8s< z%^#@uQRAg-$z&fmnYc$Duw63_Zopx|n{Bv*9Xau{a)2%?H<6D>kYY7_)e>OFT<6TT z0A}MQLgXbC2uf`;67`mhlcUhtXd)Kbc$PMm=|V}h;*_%vCw4L6r>3Vi)lE5`8hkSg zNGmW-BAOO)(W((6*e_tW&I>Nt9B$xynx|sj^ux~?q?J@F$L4;rnm_xy8E*JYwO-02u9_@@W0_2@?B@1J{y~Q39N3NX^t7#`=34Wh)X~sU&uZWgS1Z09%_k|EjA4w_QqPdY`oIdv$dJZ;(!k)#U8L+|y~gCzn+6WmFt#d{OUuKHqh1-uX_p*Af8pFYkYvKPKBxyid4KHc}H` z*KcyY;=@wzXYR{`d{6RYPhapShXIV?0cg_?ahZ7do)Ot#mxgXYJYx}<%E1pX;zqHd zf!c(onm{~#!O$2`VIXezECAHVd|`vyP)Uyt^-075X@NZDBaQt<>trA3nY-Dayki4S zZ^j6CCmx1r46`4G9794j-WC0&R9(G7kskS>=y${j-2;(BuIZTLDmAyWTG~`0)Bxqk zd{NkDe9ug|ms@0A>JVmB-IDuse9h?z9nw!U6tr7t-Lri5H`?TjpV~8(gZWFq4Vru4 z!86bDB;3lpV%{rZ`3gtmcRH1hjj!loI9jN>6stN6A*ujt!~s!2Q+U1(EFQEQb(h4E z6VKuRouEH`G6+8Qv2C)K@^;ldIuMVXdDDu}-!7FS8~k^&+}e9EXgx~)4V4~o6P^52 z)a|`J-fOirL^oK}tqD@pqBZi_;7N43%{IQ{v&G9^Y^1?SesL`;Z(dt!nn9Oj5Odde%opv&t zxJ><~b#m+^KV&b?R#)fRi;eyqAJ_0(nL*61yPkJGt;gZxSHY#t>ATnEl-E%q$E16% zZdQfvhm5B((y4E3Hk6cBdwGdDy?i5CqBlCVHZr-rI$B#>Tbi4}Gcvyg_~2=6O9D-8 zY2|tKrNzbVR$h57R?Pe+gUU_il}ZaWu|Az#QO@};=|(L-RVf0AIW zq#pO+RfM7tdV`9lI6g;{qABNId`fG%U9Va^ravVT^)CklDcx)YJKeJdGpM{W1v8jg z@&N+mR?BPB=K1}kNwXk_pj44sd>&^;d!Z~P>O78emE@Qp@&8PyB^^4^2f7e)gekMv z2aZNvP@;%i{+_~>jK7*2wQc6nseT^n6St9KG#1~Y@$~zR_=AcO2hF5lCoH|M&c{vR zSp(GRVVl=T*m~dIA;HvYm8HOdCkW&&4M~UDd^H)`p__!4k+6b)yG0Zcek8OLw$C^K z3-BbLiG_%qX|ZYpXJ$(c@aa7b4-*IQkDF}=gZSV`*ljP|5mWuHSCcf$5qqhZTv&P?I$z^>}qP(q!Aku2yA5vu38d8x*q{6-1`%PrE_r0-9Qo?a#7Zbz#iGI7K<(@k^|i4QJ1H z4jx?{rZbgV!me2VT72@nBjucoT zUM9;Y%TCoDop?Q5fEQ35bCYk7!;gH*;t9t-QHLXGmUF;|vm365#X)6b2Njsyf1h9JW#x$;@x5Nx2$K$Z-O3txa%;OEbOn6xBzd4n4v)Va=sj5 z%rb#j7{_??Tjb8(Hac<^&s^V{yO-BL*uSUk2;X4xt%NC8SjO-3?;Lzld{gM5A=9AV z)DBu-Z8rRvXXwSVDH|dL-3FODWhfe1C_iF``F05e{dl(MmS|W%k-j)!7(ARkV?6r~ zF=o42y+VapxdZn;GnzZfGu<6oG-gQ7j7Zvgo7Am@jYxC2FpS@I;Jb%EyaJDBQC(q% zKlZ}TVu!>;i3t~OAgl@QYy1X|T~D{HOyaS*Bh}A}S#a9MYS{XV{R-|niEB*W%GPW! zP^NU(L<}>Uab<;)#H)rYbnqt|dOK(-DCnY==%d~y(1*{D{Eo1cqIV8*iMfx&J*%yh zx=+WHjt0q2m*pLx8=--UqfM6ZWjkev>W-*}_*$Y(bikH`#-Gn#!6_ zIA&kxn;XYI;eN9yvqztK-a113A%97in5CL5Z&#VsQ4=fyf&3MeKu70)(x^z_uw*RG zo2Pv&+81u*DjMO6>Mrr7vKE2CONqR6C0(*;@4FBM;jPIiuTuhQ-0&C)JIzo_k>TaS zN_hB;_G=JJJvGGpB?uGgSeKaix~AkNtYky4P7GDTW6{rW{}V9K)Cn^vBYKe*OmP!; zohJs=l-0sv5&pL6-bowk~(swtdRBZQHh8)m^r2+qTtZ zt4m$B?OQYNyfBA0E)g28a*{)a=%%f-?{F;++-Xs#5|7kSHTD*E9@$V ztE%7zX4A(L`n)FY8Y4pOnKC|Pf)j$iR#yP;V0+|Hki+D;t4I4BjkfdYliK9Gf6RYw z;3px$Ud5aTd`yq$N7*WOs!{X91hZZ;AJ9iQOH%p;v$R%OQum_h#rq9*{ve(++|24z zh2P;{-Z?u#rOqd0)D^_Ponv(Y9KMB9#?}nJdUX&r_rxF0%3__#8~ZwsyrSPmtWY27 z-54ZquV2t_W!*+%uwC=h-&_q~&nQer0(FL74to%&t^byl^C?wTaZ-IS9OssaQFP)1 zAov0o{?IRAcCf+PjMWSdmP42gysh|c9Ma&Q^?_+>>+-yrC8WR;*XmJ;>r9v*>=W}tgWG;WIt{~L8`gk8DP{dSdG z4SDM7g5ahMHYHHk*|mh9{AKh-qW7X+GEQybJt9A@RV{gaHUAva+=lSroK^NUJYEiL z?X6l9ABpd)9zzA^;FdZ$QQs#uD@hdcaN^;Q=AXlbHv511Meye`p>P4Y2nblEDEeZo}-$@g&L98Aih6tgLz--${eKTxymIipy0xSYgZZ zq^yyS4yNPTtPj-sM?R8@9Q1gtXPqv{$lb5i|C1yymwnGdfYV3nA-;5!Wl zD0fayn!B^grdE?q^}ba{-LIv*Z}+hZm_F9c$$cW!bx2DgJD&6|bBIcL@=}kQA1^Eh zXTEznqk)!!IcTl>ey?V;X8k<+C^DRA{F?T*j0wV`fflrLBQq!l7cbkAUE*6}WabyF zgpb+|tv=aWg0i}9kBL8ZCObYqHEycr5tpc-$|vdvaBsu#lXD@u_e1iL z{h>xMRS0a7KvW?VttrJFpX^5DC4Bv4cp6gNG6#8)7r7IxXfSNSp6)_6tZ4l>(D+0I zPhU)N!sKywaBusHdVE!yo5$20JAU8V_XcW{QmO!p*~ns8{2~bhjydnmA&=r zX9NSM9QYogYMDZ~kS#Qx`mt>AmeR3p@K$`fbJ%LQ1c5lEOz<%BS<}2DL+$>MFcE%e zlxC)heZ7#i80u?32eOJI9oQRz0z;JW@7Th4q}YmQ-`Z?@y3ia^_)7f37QMwDw~<-@ zT)B6fftmK_6YS!?{uaj5lLxyR++u*ZY2Mphm5cd7PA5=%rd)95hJ9+aGSNfjy>Ylc zoI0nGIT3sKmwX8h=6CbvhVO+ehFIR155h8iRuXZx^cW>rq5K4z_dvM#hRER=WR@THs%WELI9uYK9HN44Em2$#@k)hD zicqRPKV#yB;UlcsTL_}zCMK0T;eXHfu`y2(dfwm(v)IBbh|#R>`2cot{m7}8_X&oD zr@94PkMCl%d3FsC4pil=#{3uv^+)pvxfwmPUr)T)T|GcZVD$wVj$mjkjDs`5cm8N! zXVq2CvL;gWGpPI4;9j;2&hS*o+LNp&C5Ac=OXx*W5y6Z^az)^?G0)!_iAfjH5wiSE zD(F}hQZB#tF5iEx@0sS+dP70DbZ*<=5X^)Pxo^8aKzOzuyc2rq=<0-k;Y_ID1>9^v z+)nc36}?>jen*1%OX3R*KRASj${u$gZ$27Hpcj=95kK^aLzxhW6jj_$w6}%#1*$5D zG1H_vYFrCSwrRqYw*9<}OYAOQT)u%9lC`$IjZV<4`9Sc;j{Qv_6+uHrYifK&On4V_7yMil!0Yv55z@dFyD{U@Sy>|vTX=P_( zRm<2xj*Z}B30VAu@0e+}at*y?wXTz|rPalwo?4ZZc>hS0Ky6~mi@kv#?xP2a;yt?5=(-CqvP_3&$KdjB7Ku;# z`GLE*jW1QJB5d&E?IJO?1+!Q8HQMGvv^RuFoi=mM4+^tOqvX%X&viB%Ko2o-v4~~J z267ui;gsW?J=qS=D*@*xJvAy3IOop5bEvfR4MZC>9Y4Z$rGI|EHNNZ7KX;Ix{xSvm z-)Cau-xuTm|7`4kUdXvd_d^E=po(76ELfq5OgxIt3aqDy#zBfIy-5<3gpn{Ce`-ha z<;6y@{Bgqw?c~h*&j{FozQCh=`Lv-5Iw!KdSt;%GDOq%=(V!dJ-}|}|0o5G2kJj6{ z`jCSPs$9Fe8O(+qALZiJ$WtR=<@GvsdM)IJ`7XrBfW0iyYE#Vy^e@zbysg*B5Z_kSL6<)vqoaH zQ{!9!*{e9UZo^h+qZ`T@LfVwAEwc&+9{C8c%oj41q#hyn<&zA9IIur~V|{mmu`n5W z8)-Ou$YgjQ*PMIqHhZ_9E?(uoK0XM5aQkarcp}WT^7b^FC#^i>#8LGZ9puDuXUYas z7caX)V5U6uY-L5Wl%)j$qRkR;7@3T*N64YK_!`Fw=>CAwe~2loI1<>DZW&sb7Q)X;6E08&$h! z2=c1i4UOO{R4TmkTz+o9n`}+%d%blR6P;5{`qjtxlN$~I%tMMDCY`~e{+mRF!rj5( z3ywv)P_PUUqREu)TioPkg&5RKjY6z%pRxQPQ{#GNMTPag^S8(8l{!{WGNs2U1JA-O zq02VeYcArhTAS;v3);k(&6ayCH8SXN@r;1NQeJ*y^NHM+zOd;?t&c!Hq^SR_w6twGV8dl>j zjS+Zc&Yp7cYj&c1y3IxQ%*kWiYypvoh(k8g`HrY<_Bi-r%m-@SLfy-6mobxkWHxyS z>TtM2M4;Uqqy|+8Q++VcEq$PwomV1D4UzNA*Tgkg9#Gpz#~&iPf|Czx!J?qss?e|3 z4gTua75-P{2X7w9eeK3~GE0ip-D;%%gTi)8bR~Ez@)$gpuS~jZs`CrO5SR-Xy7bkA z89fr~mY}u4A$|r1$fe-;T{yJh#9Ime1iRu8eo?uY9@yqAU3P!rx~SsP;LTBL zeoMK(!;(Zt8313 z3)V)q_%eflKW?BnMZa}6E0c7t!$-mC$qt44OME5F(6B$E8w*TUN-h}0dOiXI+TH zYFrr&k1(yO(|J0vP|{22@Z}bxm@7BkjO)f)&^fv|?_JX+s)1*|7X7HH(W?b3QZ3!V|~m?8}uJsF>NvE4@fik zjyyh+U*tt`g6v>k9ub88a;ySvS1QawGn7}aaR**$rJA=a#eUT~ngUbJ%V=qsFIekLbv!YkqjTG{_$F;$w19$(ivIs*1>?2ka%uMOx@B9`LD zhm~)z@u4x*zcM1WhiX)!U{qOjJHt1xs{G1S?rYe)L)ntUu^-(o_dfqZu)}W(X%Uu| zN*qI@&R2fB#Jh|Mi+eMrZDtbNvYD3|v0Kx>E#Ss;Be*T$@DC!2A|mb%d}TTN3J+c= zu@1gTOXFYy972S+=C;#~)Z{Swr0VI5&}WYzH22un_Yg5o%f9fvV(`6!{C<(ZigQ2`wso)cj z9O12k)15^Wuv#rHpe*k5#4vb%c znP+Gjr<-p%01d<+^yrSoG?}F=eI8X;?=Fo2a~HUiJ>L!oE#9tXRp!adg-b9D;(6$E zeW0tH$US04zTX$OxM&X+2ip>KdFM?iG_fgOD-qB|uFng8*#Z5jgqGY=zLU?4!OlO#~YBTB9b9#~H@nqQ#5 z6bV));d?IJTVBC+79>rGuy1JgxPLy$dA7;_^^L)02m}XLjFR*qH`eI~+eJo(7D`LH z(W%lGnGK+Vk_3kyF*zpgO=1MxMg?hxe3}}YI>dVs8l}5eWjYu4=w6MWK09+05 zGdpa#$awd>Q|@aZa*z{5F3xy3n@E4YT9%TmMo0jxW59p0bI?&S}M+ z&^NG%rf7h*m9~p#b19|`wO5OMY-=^XT+=yrfGNpl<&~~FGsx_`IaFn+sEgF$hgOa~oAVAiu^a$jHcqkE=dj`ze z=axsfrzzh6VGD0x#6Ff=t%+VTiq!n6^gv*uIUD<9fOhvR;al5kcY${uunn}-!74<7 zmP^3cl-kyN(QY!!Z-^PY-OUkh=3ZWk6>le$_Q&xk4cgH{?i)C%2RM@pX5Q{jdSlo! zVau5v44cQX5|zQlQDt;dCg)oM0B<=P1CR!W%!^m$!{pKx;bn9DePJjWBX)q!`$;0K zqJIIyD#aK;#-3&Nf=&IhtbV|?ZGYHSphp~6th`p2rkw&((%kBV7<{siEOU7AxJj+FuRdDu$ zcmTW8usU_u!r)#jg|J=Gt{##7;uf4A5cdt6Y02}f(d2)z~ z)CH~gVAOwBLk$ZiIOn}NzDjvfw(w$u|BdCBI#)3xB-Ot?nz?iR38ayCm48M=_#9r7 zw8%pwQ<9mbEs5~_>pN3~#+Er~Q86J+2TDXM6umCbukd-X6pRIr5tF?VauT8jW> zY^#)log>jtJs2s3xoiPB7~8#1ZMv>Zx0}H58k-@H2huNyw~wsl0B8j)H5)H9c7y&i zp8^0;rKbxC1eEZ-#Qxvz)Xv$((8lK9I>BspPajluysw^f#t9P;OUis43mmEzX+lk* zc4T-Ms9_687GR+~QS#0~vxK#DSGN=a-m(@eZTqw2<+lN9>R~gK2)3;sT4%nI%Y|0m zX9SPR!>?~s=j5H4WMqeTW8QaLZ=1bWS5I3xZ&$(ypc=tHrv+hX@s)VG(tc!yvLM7n zshN=C#v={X1r;)xn0Pow_1eMhkn!{;x$BJ#PIz)m585&%cmzk;btQzZAN_^zis;n? z?6I~bN?s;7vg_dtoTc4A5Ow*Rb}No#UYl)sN|RmoYo}k^cKLXd8F`44?RrokkPvN5 ztUrx;U~B;jbE_qGd3n0j2i}A{enJvJ?gSF~NQj~EP5vM-w4@;QQ5n(Npic}XNW6B0 zq9F4T%6kp7qGhd0vpQcz+nMk8GOAmbz8Bt4@GtewGr6_>Xj>ge)SyfY}nu>Y!a@HoIx(StD zx`!>RT&}tpBL%nOF%7XIFW?n1AP*xthCMzhrU6G!U6?m4!CPWTvn#Yaoi_95CT2!L z|B=5zeRW30&ANGN>J9#GtCm&3SF6n4TqDz<-{@ZXkrkRDCpV$DwCtI^e&3i1A{Ar&JZtS^c+lyPa6 z%JJr42S_;eFC#M~bdtQePhOU32WDiZ4@H&af)z#$Y|hnQNb)8(3?1Ad>5uaZ1z zU~!jt3XUI@gpWb8tWTyH7DGvKvzYfqNIy3P{9vpwz_C-QL&`+8Io$F5PS-@YQJoEO z17D9P(+sXajWSH_8&C?fn>rTLX+(?KiwX#JNV)xE0!Q@>Tid$V2#r4y6fkph?YZ>^ z(o^q(0*P->3?I0cELXJn(N|#qTm6 zAPIL~n)m!50;*?5=MOOc4Wk;w(0c$(!e?vpV23S|n|Y7?nyc8)fD8t-KI&nTklH&BzqQ}D(1gH3P+5zGUzIjT~x`;e8JH=86&5&l-DP% z)F+Et(h|GJ?rMy-Zrf>Rv@<3^OrCJ1xv_N*_@-K5=)-jP(}h1Rts44H&ou8!G_C1E zhTfUDASJ2vu!4@j58{NN;78i?6__xR75QEDC4JN{>RmgcNrn-EOpEOcyR<8FS@RB@ zH!R7J=`KK^u06eeI|X@}KvQmdKE3AmAy8 zM4IIvde#e4O(iwag`UL5yQo>6&7^=D4yE-Eo9$9R2hR} zn;Z9i-d=R-xZl4@?s%8|m1M`$J6lW1r0Y)+8q$}Vn4qyR1jqTjGH;@Z!2KiGun2~x zaiEfzVT<|_b6t}~XPeflAm8hvCHP3Bp*tl{^y_e{Jsn@w+KP{7}bH_s=1S2E1sj=18a39*Ag~lbkT^_OQuYQey=b zW^{0xlQ@O$^cSxUZ8l(Mspg8z0cL*?yH4;X2}TdN)uN31A%$3$a=4;{S@h#Y(~i%) zc=K7Ggl=&2hYVic*W65gpSPE70pU;FN@3k?BYdNDKv6wlsBAF^);qiqI zhklsX4TaWiC%VbnZ|yqL+Pcc;(#&E*{+Rx&<&R{uTYCn^OD|mAk4%Q7gbbgMnZwE{ zy7QMK%jIjU@ye?0; z;0--&xVeD}m_hq9A8a}c9WkI2YKj8t!Mkk!o%AQ?|CCBL9}n570}OmZ(w)YI6#QS&p<={tcek*D{CPR%eVA1WBGUXf z%gO2vL7iVDr1$!LAW)1@H>GoIl=&yyZ7=*9;wrOYQ}O}u>h}4FWL?N2ivURlUi11- zl{G0fo`9?$iAEN<4kxa#9e0SZPqa{pw?K=tdN5tRc7HDX-~Ta6_+#s9W&d`6PB7dF*G@|!Mc}i zc=9&T+edI(@la}QU2An#wlkJ&7RmTEMhyC_A8hWM54?s1WldCFuBmT5*I3K9=1aj= z6V@93P-lUou`xmB!ATp0(We$?)p*oQs;(Kku15~q9`-LSl{(Efm&@%(zj?aK2;5}P z{6<@-3^k^5FCDT@Z%XABEcuPoumYkiD&)-8z2Q}HO9OVEU3WM;V^$5r4q>h^m73XF z5!hZ7SCjfxDcXyj(({vg8FU(m2_}36L_yR>fnW)u=`1t@mPa76`2@%8v@2@$N@TE` z)kYhGY1jD;B9V=Dv1>BZhR9IJmB?X9Wj99f@MvJ2Fim*R`rsRilvz_3n!nPFLmj({EP!@CGkY5R*Y_dSO{qto~WerlG}DMw9k+n}pk z*nL~7R2gB{_9=zpqX|*vkU-dx)(j+83uvYGP?K{hr*j2pQsfXn<_As6z%-z+wFLqI zMhTkG>2M}#BLIOZ(ya1y8#W<+uUo@(43=^4@?CX{-hAuaJki(_A(uXD(>`lzuM~M;3XA48ZEN@HRV{1nvt?CV)t;|*dow0Ue2`B*iA&!rI`fZQ=b28= z_dxF}iUQ8}nq0SA4NK@^EQ%=)OY;3fC<$goJ&Kp|APQ@qVbS-MtJQBc)^aO8mYFsbhafeRKdHPW&s^&;%>v zlTz`YE}CuQ@_X&mqm{+{!h2r)fPGeM_Ge4RRYQkrma`&G<>RW<>S(?#LJ}O-t)d$< zf}b0svP^Zu@)MqwEV^Fb_j zPYYs~vmEC~cOIE6Nc^@b@nyL!w5o?nQ!$mGq(Pa|1-MD}K0si<&}eag=}WLSDO zE4+eA~!J(K}605x&4 zT72P7J^)Y)b(3g2MZ@1bv%o1ggwU4Yb!DhQ=uu-;vX+Ix8>#y6wgNKuobvrPNx?$3 zI{BbX<=Y-cBtvY&#MpGTgOLYU4W+csqWZx!=AVMb)Z;8%#1*x_(-)teF>45TCRwi1 z)Nn>hy3_lo44n-4A@=L2gI$yXCK0lPmMuldhLxR8aI;VrHIS{Dk}yp= zwjhB6v@0DN=Hnm~3t>`CtnPzvA*Kumfn5OLg&-m&fObRD};c}Hf?n&mS< z%$wztc%kjWjCf-?+q(bZh9k~(gs?i4`XVfqMXvPVkUWfm4+EBF(nOkg!}4u)6I)JT zU6IXqQk?p1a2(bz^S;6ZH3Wy9!JvbiSr7%c$#G1eK2^=~z1WX+VW)CPD#G~)13~pX zErO(>x$J_4qu-)lNlZkLj2}y$OiKn0ad5Imu5p-2dnt)(YI|b7rJ3TBUQ8FB8=&ym50*ibd2NAbj z;JA&hJ$AJlldM+tO;Yl3rBOFiP8fDdF?t(`gkRpmT9inR@uX{bThYNmxx-LN5K8h0 ztS%w*;V%b`%;-NARbNXn9he&AO4$rvmkB#;aaOx?Wk|yBCmN{oMTK&E)`s&APR<-5 z#;_e75z;LJ)gBG~h<^`SGmw<$Z3p`KG|I@7Pd)sTJnouZ1hRvm3}V+#lPGk4b&A#Y z4VSNi8(R1z7-t=L^%;*;iMTIAjrXl;h106hFrR{n9o8vlz?+*a1P{rEZ2ie{luQs} zr6t746>eoqiO5)^y;4H%2~&FT*Qc*9_oC2$+&syHWsA=rn3B~4#QEW zf4GT3i_@)f(Fj}gAZj`7205M8!B&HhmbgyZB& z+COyAVNxql#DwfP;H48Yc+Y~ChV6b9auLnfXXvpjr<~lQ@>VbCpQvWz=lyVf1??_c zAo3C^otZD@(v?X)UX*@w?TF|F8KF>l7%!Dzu+hksSA^akEkx8QD(V(lK+HBCw6C}2onVExW)f$ zncm*HI(_H;jF@)6eu}Tln!t?ynRkcqBA5MitIM@L^(4_Ke}vy7c%$w{(`&7Rn=u>oDM+Z^RUYcbSOPwT(ONyq76R>$V6_M_UP4vs=__I#io{{((| zy5=k=oVr-Qt$FImP~+&sN8rf2UH*vRMpwohPc@9?id17La4weIfBNa>1Djy+1=ugn z@}Zs;eFY1OC}WBDxDF=i=On_33(jWE-QYV)HbQ^VM!n>Ci9_W0Zofz7!m>do@KH;S z4k}FqEAU2)b%B_B-QcPnM5Zh=dQ+4|DJoJwo?)f2nWBuZE@^>a(gP~ObzMuyNJTgJFUPcH`%9UFA(P23iaKgo0)CI!SZ>35LpFaD7 z)C2sW$ltSEYNW%%j8F;yK{iHI2Q^}coF@LX`=EvxZb*_O;2Z0Z5 z7 zlccxmCfCI;_^awp|G748%Wx%?t9Sh8!V9Y(9$B?9R`G)Nd&snX1j+VpuQ@GGk=y(W zK|<$O`Cad`Y4#W3GKXgs%lZduAd1t1<7LwG4*zaStE*S)XXPFDyKdgiaVXG2)LvDn zf}eQ_S(&2!H0Mq1Yt&WpM1!7b#yt_ie7naOfX129_E=)beKj|p1VW9q>>+e$3@G$K zrB%i_TT1DHjOf7IQ8)Wu4#K%ZSCDGMP7Ab|Kvjq7*~@ewPm~h_-8d4jmNH<&mNZC@CI zKxG5O08|@<4(6IEC@L-lcrrvix&_Dj4tBvl=8A}2UX|)~v#V$L22U}UHk`B-1MF(t zU6aVJWR!>Y0@4m0UA%Sq9B5;4hZvsOu=>L`IU4#3r_t}os|vSDVMA??h>QJ1FD1vR z*@rclvfD!Iqoxh>VP+?b9TVH8g@KjYR@rRWQy44A`f6doIi+8VTP~pa%`(Oa@5?=h z8>YxNvA##a3D0)^P|2|+0~f|UsAJV=q(S>eq-dehQ+T>*Q@qN zU8@kdpU5gGk%ozt?%c8oM6neA?GuSsOfU_b1U)uiEP8eRn~>M$p*R z43nSZs@^ahO78s zulbK@@{3=2=@^yZ)DuIC$ki;`2WNbD_#`LOHN9iMsrgzt-T<8aeh z(oXrqI$Kgt6)Icu=?11NWs>{)_ed1wh>)wv6RYNUA-C&bejw{cBE_5Wzeo!AHdTd+ z)d(_IKN7z^n|As~3XS=cCB_TgM7rK;X586re`{~Foml$aKs zb!4Pe7hEP|370EWwn$HKPM!kL94UPZ1%8B^e5fB+=Iw^6=?5n3tZGYjov83CLB&OQ++p)WCMeshCv_9-~G9C_2x`LxTDjUcW$l6e!6-&a^fM3oP9*g(H zmCk0nGt1UMdU#pfg1G0um5|sc|KO<+qU1E4iBF~RvN*+`7uNHH^gu{?nw2DSCjig% zI@ymKZSK=PhHJa(jW&xeApv&JcfSmNJ4uQ|pY=Lcc>=J|{>5Ug3@x#R_b@55xFgfs za^ANzWdD$ZYtFs$d7+oiw0ZmPk2&l|< zc8()wfiJx@EGpQT zG$8iLkQZ-086doF1R zh<#9cz_vRsJdoXbD=QgOtpm}cFAJX8c}>Jew;PQJSXSb^;wlC zxXLHTS|!GZ-VK_4wV<9bk4RUmlsByzW_^b>)$6R+jQ}^wco1nMA`9Lncs;&QGp!`5Tx#aXXU?}5_RrtUY zx(EMzDhl-a^y^f5yfFLMnOO#u)l69&4M?|ne|2EV>zQ}4JQCBel?~2I4?D|>L$%H(peOOII!U}i z-j)*h1rODe9{0`xmhG;`AKqw1p0_KhEIU8)DoGnEn9wAhXPaxO_(jNSij~J5m$P*$ z9Mt(t;eV}2+i|kjQpBFcNb7_(VbuF<;RQB~R~p>2*Lg>a&7DEEuq*I%Ls4{zHeUDq z+M0&YhEn^C*9-B4Q7HJ$xj)dORCXPK+)ZtLOa0o&)Sl+f(Y{p*68$-#yagW5^HQnQ z0pWpoQpxg8<&gx9im(>=x6v#&RbQ7^AsjxeSDA? zi4MEJUC~ByG!PiBjq7$pK&FA^5 z=Y@dtQnuy%IfsaR`TVP0q^3mixl&J-3!$H!ua#{A>0Z1JdLq#d4UV9nlYm641ZHl zH6mK~iI6lR3OUEVL}Z5{ONZ_6{Nk%Bv03ag<1HVN?R%w2^aR5@E>6(r>}IoMl$wRF zWr-DItN*k7T$NTT8B)+23c?171sADhjInb2Xb>GhFYGC&3{b>huvLlaS4O z^{j5q+b5H?Z)yuy%AByaVl2yj9cnalY1sMQ zXI#e%*CLajxGxP!K6xf9RD2pMHOfAa1d^Lr6kE`IBpxOiGXfNcoQ*FI6wsNtLD!T+ zC4r2q>5qz0f}UY^RY#1^0*FPO*Zp-U1h9U|qWjwqJaDB(pZ`<`U-xo7+JB$zvwV}^ z2>$0&Q5k#l|Er7*PPG1ycj4BGz zg&`d*?nUi1Q!OB>{V@T$A;)8@h;*Rb1{xk_8X<34L`s}xkH-rQZvjM`jI=jaJRGRg zeEcjYChf-78|RLrao%4HyZBfnAx5KaE~@Sx+o-2MLJ>j-6uDb!U`odj*=)0k)K75l zo^)8-iz{_k7-_qy{Ko~N#B`n@o#A22YbKiA>0f3k=p-B~XX=`Ug>jl$e7>I=hph0&AK z?ya;(NaKY_!od=tFUcGU5Kwt!c9EPUQLi;JDCT*{90O@Wc>b| zI;&GIY$JlQW^9?R$-OEUG|3sp+hn+TL(YK?S@ZW<4PQa}=IcUAn_wW3d!r#$B}n08 z*&lf(YN21NDJ74DqwV`l`RX(4zJ<(E4D}N0@QaE-hnfdPDku~@yhb^AeZL73RgovX z6=e>!`&e^l@1WA5h!}}PwwL*Gjg!LbC5g0|qb8H$^S{eGs%cc?4vTyVFW=s6KtfW? z@&Xm+E(uz(qDbwDvRQI9DdB<2sW}FYK9sg*f%-i*>*n{t-_wXvg~N7gM|a91B!x|K zyLbJ~6!!JZpZ`#HpCB8g#Q*~VU47Rp$NyZb3WhEgg3ivSwnjGJgi0BEV?!H}Z@QF| zrO`Kx*52;FR#J-V-;`oR-pr!t>bYf)UYcixN=(FUR6$fhN@~i09^3WeP3*)D*`*mJ z1u%klAbzQ=P4s%|FnVTZv%|@(HDB+ap5S#cFSJUSGkyI*Y>9Lwx|0lTs%uhoCW(f1 zi+|a9;vDPfh3nS<7m~wqTM6+pEm(&z-Ll;lFH!w#(Uk#2>Iv~2Hu}lITn7hnOny`~ z*Vj=r<&Nwpq^@g5m`u&QTBRoK*}plAuHg$L$~NO#wF0!*r0OfcS%)k0A??uY*@B^C zJe9WdU(w){rTIf<;rwJt^_35^d<A@$FqEZW6kwyfAo2x0T$Ye2MZox6Z7<%Qbu$}}u{rtE+h2M+Z}T4I zxF1cwJ(Uvp!T#mogWkhb(?SxD4_#tV(Sc8N4Gu*{Fh#})Pvb^ef%jrlnG*&Ie+J5 zsly5oo?1((um&lLDxn(DkYtk`My>lgKTp3Y4?hTQ4_`YNOFtjF-FUY#d#(EQd(rfz zB8z%Vi;?x)ZM$3c>yc5H8KBvSevnWNdCbAj?QCac)6-K~Xz@EZp}~N9q)5*Ufjz3C z6kkOeI{3H(^VO8hKDrVjy2DXd;5wr4nb`19yJi0DO@607MSx+7F$ zz3F7sl8JV@@sM$6`#JmSilqI%Bs)}Py2eFT;TjcG5?8$zwV60b(_5A>b#uk~7U^bO z>y|6SCrP2IGST(8HFuX|XQUXPLt2gL_hm|uj1Ws`O2VW>SyL^uXkl>Zvkcpi?@!F7 z%svLoT@{R#XrIh^*dE~$YhMwC+b7JE09NAS47kT%Ew zD!XjxA@1+KOAyu`H2z#h+pGm!lG>WI0v745l+Fd><3dh{ATq%h?JSdEt zu%J*zfFUx%Tx&0DS5WSbE)vwZSoAGT=;W#(DoiL($BcK;U*w`xA&kheyMLI673HCb7fGkp{_vdV2uo;vSoAH z9BuLM#Vzwt#rJH>58=KXa#O;*)_N{$>l7`umacQ0g$pI3iW4=L--O;Wiq0zy7OKp`j2r^y3`7X!?sq9rr5B{41BkBr1fEd1#Q3 z-dXc2RSb4U>FvpVhlQCIzQ-hs=8420z=7F2F(^xD;^RXgpjlh8S6*xCP#Gj2+Q0bAg?XARw3dnlQ*Lz3vk}m`HXmCgN=?bIL{T zi}Ds-xn|P)dxhraT@XY$ZQ&^%x8y!o+?n#+>+dZ1c{hYwNTNRke@3enT(a@}V*X{! z81+{Jc2UR;+Zcbc6cUlafh4DFKwp>;M}8SGD+YnW3Q_)*9Z_pny_z+MeYQmz?r%EVaN0d!NE*FVPq&U@vo{ef6wkMIDEWLbDs zz91$($XbGnQ?4WHjB~4xgPgKZts{p|g1B{-4##}#c5aL5C6_RJ_(*5>85B1}U!_<``}q-97Q7~u)(&lsb(WT^(*n7H%33%@_b zO5(?-v??s??33b19xiB7t_YT!q8!qAzN1#RD@3;kYAli%kazt#YN7}MhVu=ljuz27 z1`<+g8oVwy57&$`CiHeaM)tz(OSt4E# zJ@P6E*e504oUw~RD(=9WP8QdW^6wRdFbKII!GAWecJ(?{`EzTR@?j!3g?$@LLCt;U={>!9z7DU!(1Jq zqEwdx5q?W1Ncm7mXP8MFwAr?nw5$H%cb>Q><9j{Tk2RY9ngGvaJgWXx^r!ywk{ph- zs2PFto4@IIwBh{oXe;yMZJYlS?3%a-CJ#js90hoh5W5d^OMwCFmpryHFr|mG+*ZP$ zqyS5BW@s}|3xUO0PR<^{a2M(gkP5BDGxvkWkPudSV*TMRK5Qm4?~VuqVAOerffRt$HGAvp;M++Iq$E6alB z;ykBr-eZ6v_H^1Wip56Czj&=`mb^TsX|FPN#-gnlP03AkiJDM=?y|LzER1M93R4sC z*HT(;EV=*F*>!+Z{r!KG?6ODMGvkt3viG=@kQJHNMYd}bS4KrrHf4`&*(0m0R5Hqz zEk)r=sFeS?MZRvn<@Z0&bDw)XkMnw+_xqgp=W{;ioX`6;G-P9N%wfoYJ$-m$L#MC% z^sH?tSzA|WWP(cN3({~_*X$l{M*;1V{l$;T6b){#l4pswDTid26HaXgKed}13YIP= zJRvA3nmx{}R$Lr&S4!kWU3`~dxM}>VXWu6Xd(VP}z1->h&f%82eXD_TuTs@=c;l0T z|LHmWKJ+?7hkY=YM>t}zvb4|lV;!ARMtWFp!E^J=Asu9w&kVF*i{T#}sY++-qnVh! z5TQ|=>)+vutf{&qB+LO9^jm#rD7E5+tcorr^Fn5Xb0B;)f^$7Ev#}G_`r==ea294V z--v4LwjswWlSq9ba6i?IXr8M_VEGQ$H%hCqJTFQ3+1B9tmxDUhnNU%dy4+zbqYJ|o z3!N{b?A@{;cG2~nb-`|z;gEDL5ffF@oc3`R{fGi)0wtMqEkw4tRX3t;LVS3-zAmg^ zgL7Z{hmdPSz9oA@t>tZ1<|Khn&Lp=_!Q=@a?k+t~H&3jN?dr(}7s;{L+jiKY57?WsFBfW^mu6a03_^VKrdK=9egXw@!nzZ3TbYc*osyQNoCXPYoFS<&Nr97MrQCOK(gO8 z;0@iqRTJy4-RH)PJld5`AJN}n?5r^-enKrHQOR;z>UMfm+e8~4ZL5k>oXMiYq12Bx4eVQv0jFgp_zC#``sjZpywYqISMP}VZ@!~1Mf$!x|opj%mQ98JnSk@`~ zPmmyuPZKtZOnEC!1y!?`TYRsZ!II;d!iln}%e}bk5qIiUADERr*K$3dekgHV9TtBX zi5q!J!6Zgd#cLxRmZN^J`o@Zv{+p+<_#8^nvY)44Hw_2i@?R&5n^q33fpOnDg1nPQ z_r<$hURl~OketX|Tdbvf_7=3x^rSFJtEp@tuDpVB&uq)qW;xUQ7mmkr-@eZwa$l+? zoKk``Vz@TH#>jMce*8>@FZ+@BEUdYa_K0i|{*;j9MW3K%pnM*T;@>|o@lMhgLrpZP5aol(z>g;b4}|e$U~Fn zGL%(}p%Jsl4LxE!VW_Y4T>e}W4e#~F03H_^R!Q)kpJG{lO!@I4{mFo^V#ayHh_5~o zB$O71gcE(G@6xv);#Ky?e(Ed}^O+Ho(t=93T9T3TnEY(OVf_dR-gY@jj+iJSY?q|6prBv(S9A4k=2fNZz!W@S=B@~b?TJRTuBQq448@juN#Y=3q=^VCF>Z}n6wICJ<^^Kn8C;mK zZYiFSN#Z$?NDGV7(#}q2tAZAtE63icK-MY>UQu4MWlGIbJ$AF8Zt-jV;@7P5MPI>% zPWvO!t%1+s>-A%`;0^o8Ezeaa4DMwI8ooQrJ;ax@Qt*6XONWw)dPwOPI9@u*EG&844*1~EoZ2qsAe~M>d`;Bc_CWY zMoDKEmDh-}k9d6*<0g@aQmsnrM1H9IcKYZs)><)d92{|0Hh8?~XbF)7U+UmP@Pw_6geVB?7N$4J4*E0z3EO&5kRS(EE zv92(+e5WxLXMN{h;-|8@!Q#0q247hb^3R%*k3MuMO5*L}$0D#5P*N$aHd54C+=_RToYXTyewugOaDmGsCvb4H1s=@gkfVnzTCWKMa-Mm1v4Wq!t-JIrbV&EWwKDe ze#kJpOq#iRlFz%5#6Fio9IUlKnQ#X&DY8Ux#<-WqxAac-y%U_L+EZZ4Rg5*yNg`f< zSZn&uio@zanUCPqX1l4W&B!;UWs#P7B^|4WwoCxQXl|44n^cBNqu=3Vl*ltAqsUQO z9q_@nD0zq0O8r`coEm>9+|rA3HL#l}X;0##>SJS$cVavOZVCpSGf4mUU1( zWaRCUYc^9QbG9=vpWo%xP}CMFnMb{reA`K7tT(t5DM)d9l}jVPY>qoRzT zE3m-p#=i=$9x*CB`AL>SY}u3agYFl#uULNen#&44H;!L@I{RI=PlWxG8J((f)ma7A z@jLvQ>?Nx`n?3ChRG#HqE3MXP8*o3!Qq`+t8EMt_p)oeKHqPusBxPn!#?R??-=e3e zo73WNs_IZF`WLigre=|`aS2^> zN1zn!7k&Dh28t%VpJ%**&E!eAcB5oLjQFFcJQj*URMia%Ya3@q1UQ18=oWMM6`I}iT_&L1gl?*~6nU4q4Z0`H<5yDp(HeZ+RGf9`mM&= zn-qRp%i!g$R;i1d1aMZ{IewNjE@p2+Z{`x{*xL*x$?WV~{BjJpsP&C&JK0HLoyf z`0z^v&fBQSa!I7FU~9MaQ%e|?RP>sM^2PL!mE^Q1Ig_4M$5BRfi72oMYu6Ke?wmDX z@0a%-V|z}b23K=ye(W+fG#w|jJUnT{=KR5jfuq!RX}<1irTDw(${<&}dWQu4;EuE< z@3u4dBkQaCHHM&;cE0z50_V!(vJ1_V)A8?C#eJuLkt!98Z%|Bgzidc0j|z(&o)TCzYlrgZA zC3@i>L!&Gw_~7`>puB97I2lK)lESZQqVXc_8T^G2O#VHhO?IC$g zOYhXJ7)~C<8l|Xrftka@QuowScM{K&0zskoU$Aw~vIRVRF9TEQ4*3=_5)98B`=t8(N%ZuWqmwlW zllAzq=E5_5!sKDXam@w`ZD(nl%LAPxQuEtDcKPqu9LPJvNIITawU#c^PQ2HmZgs)r zH^+gRwZ?0)8IFQgU)+p@0Iqb^tcEoqcB@zhfz_FaOM&_d<|jnU>q5nSKa<@%9|dje zIupcg1!tRiMP4X=oG<7s4|AW&^-Cw4FL9OuI$t zxjc*y;Uw!G7a|jz>E*2+PlR(CemWebS7m-&*CDwnmxbiRqJvQ&os-sC&4OWt^(2@vG4|jui#Df@-D= zh3D%8Y3R6+jRBStSvH9pt&tCI`NK08J1*pC(?OM0h!bS-JK3I}`pDY-fDIaB_*W6KS+TO0Q*%kkeuN6uWITt=TsCGw6uBE710q; zRluI%j{?@jwhM|l5&TB!-TkQs!A=DXRE>u18t@;zndD0M$U@Igrt?UW2; z7%=dsHIVH_LCkGUU0fW&UMjDnvjcc0Mp(mK&;d~ZJ5EJ)#7@aTZvGDFXzFZg2Lq~s z5PR_LazNN)JD5K_uK*Hy{mXuHTkGGv|9V8KP#iQ$3!G*^>7UiE{|1G1A-qg(xH;Xa>&%f|BZkH zG=J^0pHzSAqv5*5ysQ{Puy^-_|IPrii zKS$mE10Zngf>Sgg@BjpRyJbrHeo zD8Ro0LI*W#+9?^xlOS^c>Z^^n^0I|FH^@^`ZR`{H=$ zjO0_$cnpBM7Zcm?H_RXIu-Lu~qweDSV|tEZBZh!e6hQy->}e;d#osZ1hQj{HhHkC0 zJ|F-HKmeTGgDe979ogBz24;@<|I7;TU!IXb@oWMsMECIETmQy`zPtM`|NP}PjzR_u zKMG1Z{%1kWeMfEf(10U#w!clmQ2)JC8zm(Fv!H4dUHQHCFLikID?hrd{0>kCQt?kP zdqn2ZG0}ytcQJ7t_B3s0ZvH3PYjkjQ`Q%;jV@?MK-+z3etBCGGo4f4`y^|AdCs!DH zThTQ;cL5dM{|tB_1y6K3bVa^hx_<9J(}5`2SDz1^0bT!Vm*JV;9~t&{IC{$DUAVV* z{|E=#yN{wNdTY@$6z{_KNA3&%w|vFu1n9XRcM0Ak>`UW!lQ`ah3D4r%}Z diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 37aef8d3f0c9..8e876e1c5571 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,8 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-bin.zip +distributionSha256Sum=1541fa36599e12857140465f3c91a97409b4512501c26f9631fb113e392c5bd1 +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.1-bin.zip networkTimeout=10000 +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index 79a61d421cc4..f5feea6d6b11 100755 --- a/gradlew +++ b/gradlew @@ -15,6 +15,8 @@ # See the License for the specific language governing permissions and # limitations under the License. # +# SPDX-License-Identifier: Apache-2.0 +# ############################################################################## # @@ -55,7 +57,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. @@ -83,10 +85,9 @@ done # This is normally unused # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} -APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit - -# 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"' +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s +' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum @@ -133,10 +134,13 @@ 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. + if ! command -v java >/dev/null 2>&1 + then + 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 fi # Increase the maximum file descriptors if we can. @@ -144,7 +148,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 + # shellcheck disable=SC2039,SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac @@ -152,7 +156,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then '' | soft) :;; #( *) # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 + # shellcheck disable=SC2039,SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac @@ -197,11 +201,15 @@ if "$cygwin" || "$msys" ; then done fi -# Collect all arguments for the java command; -# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of -# shell script including quotes and variable substitutions, so put them in -# double quotes to make sure that they get re-expanded; and -# * put everything else in single quotes, so that it's not re-expanded. + +# 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"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ diff --git a/gradlew.bat b/gradlew.bat index 93e3f59f135d..9d21a21834d5 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -13,6 +13,8 @@ @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem @if "%DEBUG%"=="" @echo off @rem ########################################################################## @@ -43,11 +45,11 @@ set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if %ERRORLEVEL% equ 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. +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail @@ -57,11 +59,11 @@ 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. +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail diff --git a/instrumentation-annotations-support-testing/src/main/java/io/opentelemetry/javaagent/instrumentation/otelannotations/AbstractWithSpanTest.java b/instrumentation-annotations-support-testing/src/main/java/io/opentelemetry/javaagent/instrumentation/otelannotations/AbstractWithSpanTest.java index 2d1a03aa8193..b76df0283042 100644 --- a/instrumentation-annotations-support-testing/src/main/java/io/opentelemetry/javaagent/instrumentation/otelannotations/AbstractWithSpanTest.java +++ b/instrumentation-annotations-support-testing/src/main/java/io/opentelemetry/javaagent/instrumentation/otelannotations/AbstractWithSpanTest.java @@ -7,8 +7,8 @@ import static io.opentelemetry.api.common.AttributeKey.booleanKey; import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.CODE_FUNCTION; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.CODE_NAMESPACE; +import static io.opentelemetry.semconv.incubating.CodeIncubatingAttributes.CODE_FUNCTION; +import static io.opentelemetry.semconv.incubating.CodeIncubatingAttributes.CODE_NAMESPACE; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.catchThrowable; diff --git a/instrumentation-annotations-support/build.gradle.kts b/instrumentation-annotations-support/build.gradle.kts index b64c96857519..89851aa23a05 100644 --- a/instrumentation-annotations-support/build.gradle.kts +++ b/instrumentation-annotations-support/build.gradle.kts @@ -11,7 +11,8 @@ dependencies { implementation(project(":instrumentation-api")) api("io.opentelemetry:opentelemetry-api") - api("io.opentelemetry:opentelemetry-semconv") + api("io.opentelemetry.semconv:opentelemetry-semconv") + api("io.opentelemetry.semconv:opentelemetry-semconv-incubating") compileOnly("com.google.auto.value:auto-value-annotations") annotationProcessor("com.google.auto.value:auto-value") diff --git a/instrumentation-annotations-support/src/main/java/io/opentelemetry/instrumentation/api/annotation/support/AnnotationReflectionHelper.java b/instrumentation-annotations-support/src/main/java/io/opentelemetry/instrumentation/api/annotation/support/AnnotationReflectionHelper.java index 4695bb3310b6..5bbe2a77a801 100644 --- a/instrumentation-annotations-support/src/main/java/io/opentelemetry/instrumentation/api/annotation/support/AnnotationReflectionHelper.java +++ b/instrumentation-annotations-support/src/main/java/io/opentelemetry/instrumentation/api/annotation/support/AnnotationReflectionHelper.java @@ -14,7 +14,7 @@ import java.util.function.Function; import javax.annotation.Nullable; -/** Helper class for reflecting over annotations at runtime.. */ +/** Helper class for reflecting over annotations at runtime. */ public class AnnotationReflectionHelper { private AnnotationReflectionHelper() {} diff --git a/instrumentation-annotations-support/src/test/java/io/opentelemetry/instrumentation/api/annotation/support/MethodSpanAttributesExtractorTest.java b/instrumentation-annotations-support/src/test/java/io/opentelemetry/instrumentation/api/annotation/support/MethodSpanAttributesExtractorTest.java index d21bdac92fd1..8e5a7af6d4ed 100644 --- a/instrumentation-annotations-support/src/test/java/io/opentelemetry/instrumentation/api/annotation/support/MethodSpanAttributesExtractorTest.java +++ b/instrumentation-annotations-support/src/test/java/io/opentelemetry/instrumentation/api/annotation/support/MethodSpanAttributesExtractorTest.java @@ -41,14 +41,13 @@ class MethodSpanAttributesExtractorTest { @BeforeEach void setup() { lenient() - .doAnswer( + .when(cache.computeIfAbsent(any(), any())) + .thenAnswer( invocation -> { Method m = invocation.getArgument(0); Function fn = invocation.getArgument(1); return fn.apply(m); - }) - .when(cache) - .computeIfAbsent(any(), any()); + }); } @Test @@ -123,6 +122,7 @@ void doesNotExtractAttributeForMethodWithNullArgument() { } @Test + @SuppressWarnings("MockitoDoSetup") void appliesCachedBindings() { AttributeBindings bindings = mock(AttributeBindings.class); when(bindings.isEmpty()).thenReturn(false); @@ -143,6 +143,7 @@ void appliesCachedBindings() { } @Test + @SuppressWarnings("MockitoDoSetup") void doesNotApplyCachedEmptyBindings() { AttributeBindings bindings = mock(AttributeBindings.class); when(bindings.isEmpty()).thenReturn(true); diff --git a/instrumentation-api-incubator/build.gradle.kts b/instrumentation-api-incubator/build.gradle.kts new file mode 100644 index 000000000000..00d887da1b25 --- /dev/null +++ b/instrumentation-api-incubator/build.gradle.kts @@ -0,0 +1,43 @@ +plugins { + id("org.xbib.gradle.plugin.jflex") + + id("otel.java-conventions") + id("otel.animalsniffer-conventions") + id("otel.jacoco-conventions") + id("otel.japicmp-conventions") + id("otel.publish-conventions") +} + +group = "io.opentelemetry.instrumentation" + +dependencies { + api("io.opentelemetry.semconv:opentelemetry-semconv") + api(project(":instrumentation-api")) + implementation("io.opentelemetry:opentelemetry-api-incubator") + + compileOnly("com.google.auto.value:auto-value-annotations") + annotationProcessor("com.google.auto.value:auto-value") + + testImplementation(project(":testing-common")) + testImplementation("io.opentelemetry:opentelemetry-sdk") + testImplementation("io.opentelemetry:opentelemetry-sdk-testing") + testImplementation("io.opentelemetry.semconv:opentelemetry-semconv-incubating") +} + +tasks { + // exclude auto-generated code + named("checkstyleMain") { + exclude("**/AutoSqlSanitizer.java") + } + + // Work around https://github.com/jflex-de/jflex/issues/762 + compileJava { + with(options) { + compilerArgs.add("-Xlint:-fallthrough") + } + } + + sourcesJar { + dependsOn("generateJflex") + } +} diff --git a/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/builder/internal/DefaultHttpClientInstrumenterBuilder.java b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/builder/internal/DefaultHttpClientInstrumenterBuilder.java new file mode 100644 index 000000000000..72ce1b77a701 --- /dev/null +++ b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/builder/internal/DefaultHttpClientInstrumenterBuilder.java @@ -0,0 +1,243 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.incubator.builder.internal; + +import com.google.errorprone.annotations.CanIgnoreReturnValue; +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.context.propagation.TextMapSetter; +import io.opentelemetry.instrumentation.api.incubator.config.internal.CommonConfig; +import io.opentelemetry.instrumentation.api.incubator.semconv.http.HttpClientExperimentalMetrics; +import io.opentelemetry.instrumentation.api.incubator.semconv.http.HttpClientPeerServiceAttributesExtractor; +import io.opentelemetry.instrumentation.api.incubator.semconv.http.HttpExperimentalAttributesExtractor; +import io.opentelemetry.instrumentation.api.incubator.semconv.net.PeerServiceResolver; +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 io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.SpanStatusExtractor; +import io.opentelemetry.instrumentation.api.semconv.http.HttpClientAttributesExtractor; +import io.opentelemetry.instrumentation.api.semconv.http.HttpClientAttributesExtractorBuilder; +import io.opentelemetry.instrumentation.api.semconv.http.HttpClientAttributesGetter; +import io.opentelemetry.instrumentation.api.semconv.http.HttpClientMetrics; +import io.opentelemetry.instrumentation.api.semconv.http.HttpSpanNameExtractor; +import io.opentelemetry.instrumentation.api.semconv.http.HttpSpanNameExtractorBuilder; +import io.opentelemetry.instrumentation.api.semconv.http.HttpSpanStatusExtractor; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; +import javax.annotation.Nullable; + +/** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time. + */ +public final class DefaultHttpClientInstrumenterBuilder { + + // copied from PeerIncubatingAttributes + private static final AttributeKey PEER_SERVICE = AttributeKey.stringKey("peer.service"); + + private final String instrumentationName; + private final OpenTelemetry openTelemetry; + + private final List> additionalExtractors = + new ArrayList<>(); + private Function< + SpanStatusExtractor, + ? extends SpanStatusExtractor> + statusExtractorTransformer = Function.identity(); + private final HttpClientAttributesExtractorBuilder + httpAttributesExtractorBuilder; + private final HttpClientAttributesGetter attributesGetter; + private final HttpSpanNameExtractorBuilder httpSpanNameExtractorBuilder; + + @Nullable private TextMapSetter headerSetter; + private Function, ? extends SpanNameExtractor> + spanNameExtractorTransformer = Function.identity(); + private boolean emitExperimentalHttpClientMetrics = false; + private Consumer> builderCustomizer = b -> {}; + + public DefaultHttpClientInstrumenterBuilder( + String instrumentationName, + OpenTelemetry openTelemetry, + HttpClientAttributesGetter attributesGetter) { + this.instrumentationName = instrumentationName; + this.openTelemetry = openTelemetry; + httpSpanNameExtractorBuilder = HttpSpanNameExtractor.builder(attributesGetter); + httpAttributesExtractorBuilder = HttpClientAttributesExtractor.builder(attributesGetter); + this.attributesGetter = attributesGetter; + } + + /** + * Adds an additional {@link AttributesExtractor} to invoke to set attributes to instrumented + * items. The {@link AttributesExtractor} will be executed after all default extractors. + */ + @CanIgnoreReturnValue + public DefaultHttpClientInstrumenterBuilder addAttributeExtractor( + AttributesExtractor attributesExtractor) { + additionalExtractors.add(attributesExtractor); + return this; + } + + @CanIgnoreReturnValue + public DefaultHttpClientInstrumenterBuilder setStatusExtractor( + Function< + SpanStatusExtractor, + ? extends SpanStatusExtractor> + statusExtractor) { + this.statusExtractorTransformer = statusExtractor; + return this; + } + + /** + * Configures the HTTP request headers that will be captured as span attributes. + * + * @param requestHeaders A list of HTTP header names. + */ + @CanIgnoreReturnValue + public DefaultHttpClientInstrumenterBuilder setCapturedRequestHeaders( + List requestHeaders) { + httpAttributesExtractorBuilder.setCapturedRequestHeaders(requestHeaders); + return this; + } + + /** + * Configures the HTTP response headers that will be captured as span attributes. + * + * @param responseHeaders A list of HTTP header names. + */ + @CanIgnoreReturnValue + public DefaultHttpClientInstrumenterBuilder setCapturedResponseHeaders( + List responseHeaders) { + httpAttributesExtractorBuilder.setCapturedResponseHeaders(responseHeaders); + return this; + } + + /** + * Configures the instrumentation to recognize an alternative set of HTTP request methods. + * + *

By default, this instrumentation defines "known" methods as the ones listed in RFC9110 and the PATCH + * method defined in RFC5789. + * + *

Note: calling this method overrides the default known method sets completely; it does + * not supplement it. + * + * @param knownMethods A set of recognized HTTP request methods. + * @see HttpClientAttributesExtractorBuilder#setKnownMethods(Set) + */ + @CanIgnoreReturnValue + public DefaultHttpClientInstrumenterBuilder setKnownMethods( + Set knownMethods) { + httpAttributesExtractorBuilder.setKnownMethods(knownMethods); + httpSpanNameExtractorBuilder.setKnownMethods(knownMethods); + return this; + } + + @CanIgnoreReturnValue + public DefaultHttpClientInstrumenterBuilder setHeaderSetter( + @Nullable TextMapSetter headerSetter) { + this.headerSetter = headerSetter; + return this; + } + + /** + * Configures the instrumentation to emit experimental HTTP client metrics. + * + * @param emitExperimentalHttpClientMetrics {@code true} if the experimental HTTP client metrics + * are to be emitted. + */ + @CanIgnoreReturnValue + public DefaultHttpClientInstrumenterBuilder + setEmitExperimentalHttpClientMetrics(boolean emitExperimentalHttpClientMetrics) { + this.emitExperimentalHttpClientMetrics = emitExperimentalHttpClientMetrics; + return this; + } + + /** Sets custom {@link SpanNameExtractor} via transform function. */ + @CanIgnoreReturnValue + public DefaultHttpClientInstrumenterBuilder setSpanNameExtractor( + Function, ? extends SpanNameExtractor> + spanNameExtractorTransformer) { + this.spanNameExtractorTransformer = spanNameExtractorTransformer; + return this; + } + + /** Sets custom {@link PeerServiceResolver}. */ + @CanIgnoreReturnValue + public DefaultHttpClientInstrumenterBuilder setPeerServiceResolver( + PeerServiceResolver peerServiceResolver) { + return addAttributeExtractor( + HttpClientPeerServiceAttributesExtractor.create(attributesGetter, peerServiceResolver)); + } + + /** Sets the {@code peer.service} attribute for http client spans. */ + @CanIgnoreReturnValue + public DefaultHttpClientInstrumenterBuilder setPeerService( + String peerService) { + return addAttributeExtractor(AttributesExtractor.constant(PEER_SERVICE, peerService)); + } + + @CanIgnoreReturnValue + public DefaultHttpClientInstrumenterBuilder setBuilderCustomizer( + Consumer> builderCustomizer) { + this.builderCustomizer = builderCustomizer; + return this; + } + + public Instrumenter build() { + + SpanNameExtractor spanNameExtractor = + spanNameExtractorTransformer.apply(httpSpanNameExtractorBuilder.build()); + + InstrumenterBuilder builder = + Instrumenter.builder( + openTelemetry, instrumentationName, spanNameExtractor) + .setSpanStatusExtractor( + statusExtractorTransformer.apply(HttpSpanStatusExtractor.create(attributesGetter))) + .addAttributesExtractor(httpAttributesExtractorBuilder.build()) + .addAttributesExtractors(additionalExtractors) + .addOperationMetrics(HttpClientMetrics.get()); + if (emitExperimentalHttpClientMetrics) { + builder + .addAttributesExtractor(HttpExperimentalAttributesExtractor.create(attributesGetter)) + .addOperationMetrics(HttpClientExperimentalMetrics.get()); + } + builderCustomizer.accept(builder); + + if (headerSetter != null) { + return builder.buildClientInstrumenter(headerSetter); + } + return builder.buildInstrumenter(SpanKindExtractor.alwaysClient()); + } + + public OpenTelemetry getOpenTelemetry() { + return openTelemetry; + } + + @CanIgnoreReturnValue + public DefaultHttpClientInstrumenterBuilder configure(CommonConfig config) { + set(config::getKnownHttpRequestMethods, this::setKnownMethods); + set(config::getClientRequestHeaders, this::setCapturedRequestHeaders); + set(config::getClientResponseHeaders, this::setCapturedResponseHeaders); + set(config::getPeerServiceResolver, this::setPeerServiceResolver); + set( + config::shouldEmitExperimentalHttpClientTelemetry, + this::setEmitExperimentalHttpClientMetrics); + return this; + } + + private static void set(Supplier supplier, Consumer consumer) { + T t = supplier.get(); + if (t != null) { + consumer.accept(t); + } + } +} diff --git a/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/builder/internal/DefaultHttpServerInstrumenterBuilder.java b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/builder/internal/DefaultHttpServerInstrumenterBuilder.java new file mode 100644 index 000000000000..b3c7976f6c8a --- /dev/null +++ b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/builder/internal/DefaultHttpServerInstrumenterBuilder.java @@ -0,0 +1,219 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.incubator.builder.internal; + +import com.google.errorprone.annotations.CanIgnoreReturnValue; +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.context.propagation.TextMapGetter; +import io.opentelemetry.instrumentation.api.incubator.config.internal.CommonConfig; +import io.opentelemetry.instrumentation.api.incubator.semconv.http.HttpExperimentalAttributesExtractor; +import io.opentelemetry.instrumentation.api.incubator.semconv.http.HttpServerExperimentalMetrics; +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 io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.SpanStatusExtractor; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerAttributesExtractor; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerAttributesExtractorBuilder; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerAttributesGetter; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerMetrics; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRoute; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRouteBuilder; +import io.opentelemetry.instrumentation.api.semconv.http.HttpSpanNameExtractor; +import io.opentelemetry.instrumentation.api.semconv.http.HttpSpanNameExtractorBuilder; +import io.opentelemetry.instrumentation.api.semconv.http.HttpSpanStatusExtractor; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; +import javax.annotation.Nullable; + +/** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time. + */ +public final class DefaultHttpServerInstrumenterBuilder { + + private final String instrumentationName; + private final OpenTelemetry openTelemetry; + + private final List> additionalExtractors = + new ArrayList<>(); + private Function< + SpanStatusExtractor, + ? extends SpanStatusExtractor> + statusExtractorTransformer = Function.identity(); + private final HttpServerAttributesExtractorBuilder + httpAttributesExtractorBuilder; + private final HttpSpanNameExtractorBuilder httpSpanNameExtractorBuilder; + + @Nullable private TextMapGetter headerGetter; + private Function, ? extends SpanNameExtractor> + spanNameExtractorTransformer = Function.identity(); + private final HttpServerRouteBuilder httpServerRouteBuilder; + private final HttpServerAttributesGetter attributesGetter; + private boolean emitExperimentalHttpServerMetrics = false; + + public DefaultHttpServerInstrumenterBuilder( + String instrumentationName, + OpenTelemetry openTelemetry, + HttpServerAttributesGetter attributesGetter) { + this.instrumentationName = instrumentationName; + this.openTelemetry = openTelemetry; + httpAttributesExtractorBuilder = HttpServerAttributesExtractor.builder(attributesGetter); + httpSpanNameExtractorBuilder = HttpSpanNameExtractor.builder(attributesGetter); + httpServerRouteBuilder = HttpServerRoute.builder(attributesGetter); + this.attributesGetter = attributesGetter; + } + + /** + * Adds an additional {@link AttributesExtractor} to invoke to set attributes to instrumented + * items. + */ + @CanIgnoreReturnValue + public DefaultHttpServerInstrumenterBuilder addAttributesExtractor( + AttributesExtractor attributesExtractor) { + additionalExtractors.add(attributesExtractor); + return this; + } + + @CanIgnoreReturnValue + public DefaultHttpServerInstrumenterBuilder setStatusExtractor( + Function< + SpanStatusExtractor, + ? extends SpanStatusExtractor> + statusExtractor) { + this.statusExtractorTransformer = statusExtractor; + return this; + } + + /** + * Configures the HTTP request headers that will be captured as span attributes. + * + * @param requestHeaders A list of HTTP header names. + */ + @CanIgnoreReturnValue + public DefaultHttpServerInstrumenterBuilder setCapturedRequestHeaders( + List requestHeaders) { + httpAttributesExtractorBuilder.setCapturedRequestHeaders(requestHeaders); + return this; + } + + /** + * Configures the HTTP response headers that will be captured as span attributes. + * + * @param responseHeaders A list of HTTP header names. + */ + @CanIgnoreReturnValue + public DefaultHttpServerInstrumenterBuilder setCapturedResponseHeaders( + List responseHeaders) { + httpAttributesExtractorBuilder.setCapturedResponseHeaders(responseHeaders); + return this; + } + + /** + * Configures the instrumentation to recognize an alternative set of HTTP request methods. + * + *

By default, this instrumentation defines "known" methods as the ones listed in RFC9110 and the PATCH + * method defined in RFC5789. + * + *

Note: calling this method overrides the default known method sets completely; it does + * not supplement it. + * + * @param knownMethods A set of recognized HTTP request methods. + * @see HttpServerAttributesExtractorBuilder#setKnownMethods(Set) + */ + @CanIgnoreReturnValue + public DefaultHttpServerInstrumenterBuilder setKnownMethods( + Set knownMethods) { + httpAttributesExtractorBuilder.setKnownMethods(knownMethods); + httpSpanNameExtractorBuilder.setKnownMethods(knownMethods); + httpServerRouteBuilder.setKnownMethods(knownMethods); + return this; + } + + @CanIgnoreReturnValue + public DefaultHttpServerInstrumenterBuilder setHeaderGetter( + @Nullable TextMapGetter headerGetter) { + this.headerGetter = headerGetter; + return this; + } + + /** + * Configures the instrumentation to emit experimental HTTP server metrics. + * + * @param emitExperimentalHttpServerMetrics {@code true} if the experimental HTTP server metrics + * are to be emitted. + */ + @CanIgnoreReturnValue + public DefaultHttpServerInstrumenterBuilder + setEmitExperimentalHttpServerMetrics(boolean emitExperimentalHttpServerMetrics) { + this.emitExperimentalHttpServerMetrics = emitExperimentalHttpServerMetrics; + return this; + } + + /** Sets custom {@link SpanNameExtractor} via transform function. */ + @CanIgnoreReturnValue + public DefaultHttpServerInstrumenterBuilder setSpanNameExtractor( + Function, ? extends SpanNameExtractor> + spanNameExtractorTransformer) { + this.spanNameExtractorTransformer = spanNameExtractorTransformer; + return this; + } + + public Instrumenter build() { + InstrumenterBuilder builder = builder(); + + if (headerGetter != null) { + return builder.buildServerInstrumenter(headerGetter); + } + return builder.buildInstrumenter(SpanKindExtractor.alwaysServer()); + } + + private InstrumenterBuilder builder() { + SpanNameExtractor spanNameExtractor = + spanNameExtractorTransformer.apply(httpSpanNameExtractorBuilder.build()); + + InstrumenterBuilder builder = + Instrumenter.builder( + openTelemetry, instrumentationName, spanNameExtractor) + .setSpanStatusExtractor( + statusExtractorTransformer.apply(HttpSpanStatusExtractor.create(attributesGetter))) + .addAttributesExtractor(httpAttributesExtractorBuilder.build()) + .addAttributesExtractors(additionalExtractors) + .addContextCustomizer(httpServerRouteBuilder.build()) + .addOperationMetrics(HttpServerMetrics.get()); + if (emitExperimentalHttpServerMetrics) { + builder + .addAttributesExtractor(HttpExperimentalAttributesExtractor.create(attributesGetter)) + .addOperationMetrics(HttpServerExperimentalMetrics.get()); + } + + return builder; + } + + @CanIgnoreReturnValue + public DefaultHttpServerInstrumenterBuilder configure(CommonConfig config) { + set(config::getKnownHttpRequestMethods, this::setKnownMethods); + set(config::getServerRequestHeaders, this::setCapturedRequestHeaders); + set(config::getServerResponseHeaders, this::setCapturedResponseHeaders); + set( + config::shouldEmitExperimentalHttpServerTelemetry, + this::setEmitExperimentalHttpServerMetrics); + return this; + } + + private static void set(Supplier supplier, Consumer consumer) { + T t = supplier.get(); + if (t != null) { + consumer.accept(t); + } + } +} diff --git a/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/config/internal/CommonConfig.java b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/config/internal/CommonConfig.java new file mode 100644 index 000000000000..23875d7e8b5f --- /dev/null +++ b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/config/internal/CommonConfig.java @@ -0,0 +1,125 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.incubator.config.internal; + +import static java.util.Collections.emptyMap; + +import io.opentelemetry.instrumentation.api.incubator.log.LoggingContextConstants; +import io.opentelemetry.instrumentation.api.incubator.semconv.net.PeerServiceResolver; +import io.opentelemetry.instrumentation.api.internal.HttpConstants; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time. + */ +public final class CommonConfig { + + private final PeerServiceResolver peerServiceResolver; + private final List clientRequestHeaders; + private final List clientResponseHeaders; + private final List serverRequestHeaders; + private final List serverResponseHeaders; + private final Set knownHttpRequestMethods; + private final EnduserConfig enduserConfig; + private final boolean statementSanitizationEnabled; + private final boolean emitExperimentalHttpClientTelemetry; + private final boolean emitExperimentalHttpServerTelemetry; + private final String loggingTraceIdKey; + private final String loggingSpanIdKey; + private final String loggingTraceFlagsKey; + + public CommonConfig(InstrumentationConfig config) { + peerServiceResolver = + PeerServiceResolver.create( + config.getMap("otel.instrumentation.common.peer-service-mapping", emptyMap())); + + clientRequestHeaders = + config.getList("otel.instrumentation.http.client.capture-request-headers"); + clientResponseHeaders = + config.getList("otel.instrumentation.http.client.capture-response-headers"); + serverRequestHeaders = + config.getList("otel.instrumentation.http.server.capture-request-headers"); + serverResponseHeaders = + config.getList("otel.instrumentation.http.server.capture-response-headers"); + knownHttpRequestMethods = + new HashSet<>( + config.getList( + "otel.instrumentation.http.known-methods", + new ArrayList<>(HttpConstants.KNOWN_METHODS))); + statementSanitizationEnabled = + config.getBoolean("otel.instrumentation.common.db-statement-sanitizer.enabled", true); + emitExperimentalHttpClientTelemetry = + config.getBoolean("otel.instrumentation.http.client.emit-experimental-telemetry", false); + emitExperimentalHttpServerTelemetry = + config.getBoolean("otel.instrumentation.http.server.emit-experimental-telemetry", false); + enduserConfig = new EnduserConfig(config); + loggingTraceIdKey = + config.getString( + "otel.instrumentation.common.logging.trace-id", LoggingContextConstants.TRACE_ID); + loggingSpanIdKey = + config.getString( + "otel.instrumentation.common.logging.span-id", LoggingContextConstants.SPAN_ID); + loggingTraceFlagsKey = + config.getString( + "otel.instrumentation.common.logging.trace-flags", LoggingContextConstants.TRACE_FLAGS); + } + + public PeerServiceResolver getPeerServiceResolver() { + return peerServiceResolver; + } + + public List getClientRequestHeaders() { + return clientRequestHeaders; + } + + public List getClientResponseHeaders() { + return clientResponseHeaders; + } + + public List getServerRequestHeaders() { + return serverRequestHeaders; + } + + public List getServerResponseHeaders() { + return serverResponseHeaders; + } + + public Set getKnownHttpRequestMethods() { + return knownHttpRequestMethods; + } + + public EnduserConfig getEnduserConfig() { + return enduserConfig; + } + + public boolean isStatementSanitizationEnabled() { + return statementSanitizationEnabled; + } + + public boolean shouldEmitExperimentalHttpClientTelemetry() { + return emitExperimentalHttpClientTelemetry; + } + + public boolean shouldEmitExperimentalHttpServerTelemetry() { + return emitExperimentalHttpServerTelemetry; + } + + public String getTraceIdKey() { + return loggingTraceIdKey; + } + + public String getSpanIdKey() { + return loggingSpanIdKey; + } + + public String getTraceFlagsKey() { + return loggingTraceFlagsKey; + } +} diff --git a/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/config/internal/EnduserConfig.java b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/config/internal/EnduserConfig.java new file mode 100644 index 000000000000..b44057c75f90 --- /dev/null +++ b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/config/internal/EnduserConfig.java @@ -0,0 +1,97 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.incubator.config.internal; + +import java.util.Objects; + +/** + * Configuration that controls capturing the {@code enduser.*} semantic attributes. + * + *

The {@code enduser.*} semantic attributes are not captured by default, due to this text in the + * specification: + * + *

+ * + * Given the sensitive nature of this information, SDKs and exporters SHOULD drop these attributes + * by default and then provide a configuration parameter to turn on retention for use cases where + * the information is required and would not violate any policies or regulations. + * + *
+ * + *

Capturing of the {@code enduser.*} semantic attributes can be individually enabled by + * configured the following properties: + * + *

+ * otel.instrumentation.common.enduser.id.enabled=true
+ * otel.instrumentation.common.enduser.role.enabled=true
+ * otel.instrumentation.common.enduser.scope.enabled=true
+ * 
+ * + *

This class is internal and is hence not for public use. Its APIs are unstable and can change + * at any time. + */ +public class EnduserConfig { + + private final boolean idEnabled; + private final boolean roleEnabled; + private final boolean scopeEnabled; + + EnduserConfig(InstrumentationConfig instrumentationConfig) { + Objects.requireNonNull(instrumentationConfig, "instrumentationConfig must not be null"); + + /* + * Capturing enduser.* attributes is disabled by default, because of this requirement in the specification: + * + * Given the sensitive nature of this information, SDKs and exporters SHOULD drop these attributes by default and then provide a configuration parameter to turn on retention for use cases where the information is required and would not violate any policies or regulations. + * + * https://github.com/open-telemetry/semantic-conventions/blob/main/docs/general/attributes.md#general-identity-attributes + */ + this.idEnabled = + instrumentationConfig.getBoolean("otel.instrumentation.common.enduser.id.enabled", false); + this.roleEnabled = + instrumentationConfig.getBoolean("otel.instrumentation.common.enduser.role.enabled", false); + this.scopeEnabled = + instrumentationConfig.getBoolean( + "otel.instrumentation.common.enduser.scope.enabled", false); + } + + /** + * Returns true if capturing of any {@code enduser.*} semantic attribute is enabled. + * + *

This flag can be used by capturing instrumentations to bypass all {@code enduser.*} + * attribute capturing. + */ + public boolean isAnyEnabled() { + return this.idEnabled || this.roleEnabled || this.scopeEnabled; + } + + /** + * Returns true if capturing the {@code enduser.id} semantic attribute is enabled. + * + * @return true if capturing the {@code enduser.id} semantic attribute is enabled. + */ + public boolean isIdEnabled() { + return this.idEnabled; + } + + /** + * Returns true if capturing the {@code enduser.role} semantic attribute is enabled. + * + * @return true if capturing the {@code enduser.role} semantic attribute is enabled. + */ + public boolean isRoleEnabled() { + return this.roleEnabled; + } + + /** + * Returns true if capturing the {@code enduser.scope} semantic attribute is enabled. + * + * @return true if capturing the {@code enduser.scope} semantic attribute is enabled. + */ + public boolean isScopeEnabled() { + return this.scopeEnabled; + } +} diff --git a/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/bootstrap/internal/InstrumentationConfig.java b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/config/internal/InstrumentationConfig.java similarity index 63% rename from javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/bootstrap/internal/InstrumentationConfig.java rename to instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/config/internal/InstrumentationConfig.java index f0601f423662..8ce777b270a7 100644 --- a/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/bootstrap/internal/InstrumentationConfig.java +++ b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/config/internal/InstrumentationConfig.java @@ -3,14 +3,13 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.javaagent.bootstrap.internal; +package io.opentelemetry.instrumentation.api.incubator.config.internal; -import static java.util.Objects.requireNonNull; +import static java.util.Collections.emptyList; import java.time.Duration; import java.util.List; import java.util.Map; -import java.util.logging.Logger; import javax.annotation.Nullable; /** @@ -27,72 +26,44 @@ *

This class is internal and is hence not for public use. Its APIs are unstable and can change * at any time. */ -public abstract class InstrumentationConfig { - - private static final Logger logger = Logger.getLogger(InstrumentationConfig.class.getName()); - - private static final InstrumentationConfig DEFAULT = new EmptyInstrumentationConfig(); - - // lazy initialized, so that javaagent can set it - private static volatile InstrumentationConfig instance = DEFAULT; - - /** - * Sets the instrumentation configuration singleton. This method is only supposed to be called - * once, during the agent initialization, just before {@link InstrumentationConfig#get()} is used - * for the first time. - * - *

This method is internal and is hence not for public use. Its API is unstable and can change - * at any time. - */ - public static void internalInitializeConfig(InstrumentationConfig config) { - if (instance != DEFAULT) { - logger.warning("InstrumentationConfig#instance was already set earlier"); - return; - } - instance = requireNonNull(config); - } - - /** Returns the global instrumentation configuration. */ - public static InstrumentationConfig get() { - return instance; - } +public interface InstrumentationConfig { /** * Returns a string-valued configuration property or {@code null} if a property with name {@code * name} has not been configured. */ @Nullable - public abstract String getString(String name); + String getString(String name); /** * Returns a string-valued configuration property or {@code defaultValue} if a property with name * {@code name} has not been configured. */ - public abstract String getString(String name, String defaultValue); + String getString(String name, String defaultValue); /** * Returns a boolean-valued configuration property or {@code defaultValue} if a property with name * {@code name} has not been configured. */ - public abstract boolean getBoolean(String name, boolean defaultValue); + boolean getBoolean(String name, boolean defaultValue); /** * Returns an integer-valued configuration property or {@code defaultValue} if a property with * name {@code name} has not been configured or when parsing has failed. */ - public abstract int getInt(String name, int defaultValue); + int getInt(String name, int defaultValue); /** * 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. */ - public abstract long getLong(String name, long defaultValue); + long getLong(String name, long defaultValue); /** * 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. */ - public abstract double getDouble(String name, double defaultValue); + double getDouble(String name, double defaultValue); /** * Returns a duration-valued configuration property or {@code defaultValue} if a property with @@ -112,14 +83,22 @@ public static InstrumentationConfig get() { * *

Examples: 10s, 20ms, 5000 */ - public abstract Duration getDuration(String name, Duration defaultValue); + Duration getDuration(String name, Duration defaultValue); + + /** + * This is the same as calling {@code getList(String, List)} with the defaultValue equal to the + * emptyList()/ + */ + default List getList(String name) { + return getList(name, emptyList()); + } /** * 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}. The returned list is unmodifiable. */ - public abstract List getList(String name, List defaultValue); + List getList(String name, List defaultValue); /** * Returns a map-valued configuration property or {@code defaultValue} if a property with name @@ -127,5 +106,5 @@ public static InstrumentationConfig get() { * value must be comma-separated for each key, with an '=' separating the key and value, e.g. * {@code key=value,anotherKey=anotherValue}. The returned map is unmodifiable. */ - public abstract Map getMap(String name, Map defaultValue); + Map getMap(String name, Map defaultValue); } diff --git a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/log/LoggingContextConstants.java b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/log/LoggingContextConstants.java similarity index 93% rename from instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/log/LoggingContextConstants.java rename to instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/log/LoggingContextConstants.java index 2e653a186091..a42e57655956 100644 --- a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/log/LoggingContextConstants.java +++ b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/log/LoggingContextConstants.java @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.instrumentation.api.log; +package io.opentelemetry.instrumentation.api.incubator.log; import io.opentelemetry.api.trace.SpanContext; diff --git a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/code/CodeAttributesExtractor.java b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/code/CodeAttributesExtractor.java similarity index 67% rename from instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/code/CodeAttributesExtractor.java rename to instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/code/CodeAttributesExtractor.java index 1a6b016d2ac3..a258c20aa516 100644 --- a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/code/CodeAttributesExtractor.java +++ b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/code/CodeAttributesExtractor.java @@ -3,24 +3,29 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.instrumentation.api.instrumenter.code; +package io.opentelemetry.instrumentation.api.incubator.semconv.code; import static io.opentelemetry.instrumentation.api.internal.AttributesExtractorUtil.internalSet; +import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.common.AttributesBuilder; import io.opentelemetry.context.Context; import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; import javax.annotation.Nullable; /** * Extractor of source + * href="https://github.com/open-telemetry/semantic-conventions/blob/main/docs/general/attributes.md#source-code-attributes">source * code attributes. */ public final class CodeAttributesExtractor implements AttributesExtractor { + // copied from CodeIncubatingAttributes + private static final AttributeKey CODE_FUNCTION = AttributeKey.stringKey("code.function"); + private static final AttributeKey CODE_NAMESPACE = + AttributeKey.stringKey("code.namespace"); + /** Creates the code attributes extractor. */ public static AttributesExtractor create( CodeAttributesGetter getter) { @@ -37,9 +42,9 @@ private CodeAttributesExtractor(CodeAttributesGetter getter) { public void onStart(AttributesBuilder attributes, Context parentContext, REQUEST request) { Class cls = getter.getCodeClass(request); if (cls != null) { - internalSet(attributes, SemanticAttributes.CODE_NAMESPACE, cls.getName()); + internalSet(attributes, CODE_NAMESPACE, cls.getName()); } - internalSet(attributes, SemanticAttributes.CODE_FUNCTION, getter.getMethodName(request)); + internalSet(attributes, CODE_FUNCTION, getter.getMethodName(request)); } @Override diff --git a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/code/CodeAttributesGetter.java b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/code/CodeAttributesGetter.java similarity index 89% rename from instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/code/CodeAttributesGetter.java rename to instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/code/CodeAttributesGetter.java index 5c6f62ddf611..92a3f537dfd6 100644 --- a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/code/CodeAttributesGetter.java +++ b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/code/CodeAttributesGetter.java @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.instrumentation.api.instrumenter.code; +package io.opentelemetry.instrumentation.api.incubator.semconv.code; import javax.annotation.Nullable; diff --git a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/code/CodeSpanNameExtractor.java b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/code/CodeSpanNameExtractor.java similarity index 87% rename from instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/code/CodeSpanNameExtractor.java rename to instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/code/CodeSpanNameExtractor.java index a64fae5cf724..fe191bff0988 100644 --- a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/code/CodeSpanNameExtractor.java +++ b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/code/CodeSpanNameExtractor.java @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.instrumentation.api.instrumenter.code; +package io.opentelemetry.instrumentation.api.incubator.semconv.code; import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor; import io.opentelemetry.instrumentation.api.internal.ClassNames; @@ -32,10 +32,10 @@ private CodeSpanNameExtractor(CodeAttributesGetter getter) { public String extract(REQUEST request) { Class cls = getter.getCodeClass(request); String className = cls != null ? ClassNames.simpleName(cls) : ""; - int lambdaIdx = className.indexOf("$$Lambda$"); + int lambdaIdx = className.indexOf("$$Lambda"); if (lambdaIdx > -1) { // need to produce low-cardinality name, since lambda class names change with each restart - className = className.substring(0, lambdaIdx + "$$Lambda$".length()); + className = className.substring(0, lambdaIdx + "$$Lambda".length()); } String methodName = getter.getMethodName(request); if (methodName == null) { diff --git a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/db/DbClientAttributesExtractor.java b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/db/DbClientAttributesExtractor.java similarity index 67% rename from instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/db/DbClientAttributesExtractor.java rename to instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/db/DbClientAttributesExtractor.java index 3b1174185909..09c8fbc72570 100644 --- a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/db/DbClientAttributesExtractor.java +++ b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/db/DbClientAttributesExtractor.java @@ -3,18 +3,18 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.instrumentation.api.instrumenter.db; +package io.opentelemetry.instrumentation.api.incubator.semconv.db; import static io.opentelemetry.instrumentation.api.internal.AttributesExtractorUtil.internalSet; +import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.common.AttributesBuilder; import io.opentelemetry.context.Context; import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; /** * Extractor of database + * href="https://github.com/open-telemetry/semantic-conventions/blob/main/docs/database/database-spans.md">database * client attributes. * *

This class delegates to a type-specific {@link DbClientAttributesGetter} for individual @@ -24,6 +24,10 @@ public final class DbClientAttributesExtractor extends DbClientCommonAttributesExtractor< REQUEST, RESPONSE, DbClientAttributesGetter> { + // copied from DbIncubatingAttributes + private static final AttributeKey DB_STATEMENT = AttributeKey.stringKey("db.statement"); + private static final AttributeKey DB_OPERATION = AttributeKey.stringKey("db.operation"); + /** Creates the database client attributes extractor with default configuration. */ public static AttributesExtractor create( DbClientAttributesGetter getter) { @@ -38,7 +42,7 @@ public static AttributesExtractor create( public void onStart(AttributesBuilder attributes, Context parentContext, REQUEST request) { super.onStart(attributes, parentContext, request); - internalSet(attributes, SemanticAttributes.DB_STATEMENT, getter.getStatement(request)); - internalSet(attributes, SemanticAttributes.DB_OPERATION, getter.getOperation(request)); + internalSet(attributes, DB_STATEMENT, getter.getStatement(request)); + internalSet(attributes, DB_OPERATION, getter.getOperation(request)); } } diff --git a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/db/DbClientAttributesGetter.java b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/db/DbClientAttributesGetter.java similarity index 92% rename from instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/db/DbClientAttributesGetter.java rename to instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/db/DbClientAttributesGetter.java index ffd57eab4f0f..0d924b3dcfbf 100644 --- a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/db/DbClientAttributesGetter.java +++ b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/db/DbClientAttributesGetter.java @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.instrumentation.api.instrumenter.db; +package io.opentelemetry.instrumentation.api.incubator.semconv.db; import javax.annotation.Nullable; diff --git a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/db/DbClientCommonAttributesExtractor.java b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/db/DbClientCommonAttributesExtractor.java similarity index 62% rename from instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/db/DbClientCommonAttributesExtractor.java rename to instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/db/DbClientCommonAttributesExtractor.java index a1488968a278..e3a6614c7660 100644 --- a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/db/DbClientCommonAttributesExtractor.java +++ b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/db/DbClientCommonAttributesExtractor.java @@ -3,22 +3,29 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.instrumentation.api.instrumenter.db; +package io.opentelemetry.instrumentation.api.incubator.semconv.db; import static io.opentelemetry.instrumentation.api.internal.AttributesExtractorUtil.internalSet; +import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.common.AttributesBuilder; import io.opentelemetry.context.Context; import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; import io.opentelemetry.instrumentation.api.internal.SpanKey; import io.opentelemetry.instrumentation.api.internal.SpanKeyProvider; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; import javax.annotation.Nullable; abstract class DbClientCommonAttributesExtractor< REQUEST, RESPONSE, GETTER extends DbClientCommonAttributesGetter> implements AttributesExtractor, SpanKeyProvider { + // copied from DbIncubatingAttributes + private static final AttributeKey DB_NAME = AttributeKey.stringKey("db.name"); + private static final AttributeKey DB_SYSTEM = AttributeKey.stringKey("db.system"); + private static final AttributeKey DB_USER = AttributeKey.stringKey("db.user"); + private static final AttributeKey DB_CONNECTION_STRING = + AttributeKey.stringKey("db.connection_string"); + final GETTER getter; DbClientCommonAttributesExtractor(GETTER getter) { @@ -27,11 +34,10 @@ abstract class DbClientCommonAttributesExtractor< @Override public void onStart(AttributesBuilder attributes, Context parentContext, REQUEST request) { - internalSet(attributes, SemanticAttributes.DB_SYSTEM, getter.getSystem(request)); - internalSet(attributes, SemanticAttributes.DB_USER, getter.getUser(request)); - internalSet(attributes, SemanticAttributes.DB_NAME, getter.getName(request)); - internalSet( - attributes, SemanticAttributes.DB_CONNECTION_STRING, getter.getConnectionString(request)); + internalSet(attributes, DB_SYSTEM, getter.getSystem(request)); + internalSet(attributes, DB_USER, getter.getUser(request)); + internalSet(attributes, DB_NAME, getter.getName(request)); + internalSet(attributes, DB_CONNECTION_STRING, getter.getConnectionString(request)); } @Override diff --git a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/db/DbClientCommonAttributesGetter.java b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/db/DbClientCommonAttributesGetter.java similarity index 87% rename from instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/db/DbClientCommonAttributesGetter.java rename to instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/db/DbClientCommonAttributesGetter.java index 7d11cdaa1954..430be029cb0c 100644 --- a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/db/DbClientCommonAttributesGetter.java +++ b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/db/DbClientCommonAttributesGetter.java @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.instrumentation.api.instrumenter.db; +package io.opentelemetry.instrumentation.api.incubator.semconv.db; import javax.annotation.Nullable; diff --git a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/db/DbClientSpanNameExtractor.java b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/db/DbClientSpanNameExtractor.java similarity index 94% rename from instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/db/DbClientSpanNameExtractor.java rename to instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/db/DbClientSpanNameExtractor.java index d358a3b339ca..534db4a2a49e 100644 --- a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/db/DbClientSpanNameExtractor.java +++ b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/db/DbClientSpanNameExtractor.java @@ -3,10 +3,8 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.instrumentation.api.instrumenter.db; +package io.opentelemetry.instrumentation.api.incubator.semconv.db; -import io.opentelemetry.instrumentation.api.db.SqlStatementInfo; -import io.opentelemetry.instrumentation.api.db.SqlStatementSanitizer; import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor; public abstract class DbClientSpanNameExtractor implements SpanNameExtractor { diff --git a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/metrics/db/DbConnectionPoolMetrics.java b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/db/DbConnectionPoolMetrics.java similarity index 95% rename from instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/metrics/db/DbConnectionPoolMetrics.java rename to instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/db/DbConnectionPoolMetrics.java index b70ac29a979c..be70b9562edc 100644 --- a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/metrics/db/DbConnectionPoolMetrics.java +++ b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/db/DbConnectionPoolMetrics.java @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.instrumentation.api.metrics.db; +package io.opentelemetry.instrumentation.api.incubator.semconv.db; import static io.opentelemetry.api.common.AttributeKey.stringKey; @@ -21,7 +21,7 @@ /** * A helper class that models the database + * href="https://github.com/open-telemetry/semantic-conventions/blob/main/docs/database/database-metrics.md#connection-pools">database * client connection pool metrics semantic conventions. */ public final class DbConnectionPoolMetrics { diff --git a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/db/RedisCommandSanitizer.java b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/db/RedisCommandSanitizer.java similarity index 99% rename from instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/db/RedisCommandSanitizer.java rename to instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/db/RedisCommandSanitizer.java index 98e94b3c580b..357b80e312be 100644 --- a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/db/RedisCommandSanitizer.java +++ b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/db/RedisCommandSanitizer.java @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.instrumentation.api.db; +package io.opentelemetry.instrumentation.api.incubator.semconv.db; import static java.util.Arrays.asList; import static java.util.Collections.unmodifiableMap; diff --git a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/db/SqlClientAttributesExtractor.java b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/db/SqlClientAttributesExtractor.java similarity index 61% rename from instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/db/SqlClientAttributesExtractor.java rename to instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/db/SqlClientAttributesExtractor.java index 00e7ba7533ed..eacf266333bc 100644 --- a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/db/SqlClientAttributesExtractor.java +++ b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/db/SqlClientAttributesExtractor.java @@ -3,32 +3,33 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.instrumentation.api.instrumenter.db; +package io.opentelemetry.instrumentation.api.incubator.semconv.db; import static io.opentelemetry.instrumentation.api.internal.AttributesExtractorUtil.internalSet; import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.common.AttributesBuilder; import io.opentelemetry.context.Context; -import io.opentelemetry.instrumentation.api.db.SqlStatementInfo; -import io.opentelemetry.instrumentation.api.db.SqlStatementSanitizer; import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; /** * Extractor of database + * href="https://github.com/open-telemetry/semantic-conventions/blob/main/docs/database/database-spans.md">database * attributes. This class is designed with SQL (or SQL-like) database clients in mind. * *

It sets the same set of attributes as {@link DbClientAttributesExtractor} plus an additional - * {@linkplain SemanticAttributes#DB_SQL_TABLE db.sql.table} attribute. The raw SQL - * statements returned by the {@link SqlClientAttributesGetter#getRawStatement(Object)} method are - * sanitized before use, all statement parameters are removed. + * db.sql.table attribute. The raw SQL statements returned by the {@link + * SqlClientAttributesGetter#getRawStatement(Object)} method are sanitized before use, all statement + * parameters are removed. */ public final class SqlClientAttributesExtractor extends DbClientCommonAttributesExtractor< REQUEST, RESPONSE, SqlClientAttributesGetter> { + // copied from DbIncubatingAttributes + private static final AttributeKey DB_OPERATION = AttributeKey.stringKey("db.operation"); + private static final AttributeKey DB_STATEMENT = AttributeKey.stringKey("db.statement"); + /** Creates the SQL client attributes extractor with default configuration. */ public static AttributesExtractor create( SqlClientAttributesGetter getter) { @@ -45,27 +46,33 @@ public static SqlClientAttributesExtractorBuilder dbTableAttribute; - private final SqlStatementSanitizer sanitizer; + private final boolean statementSanitizationEnabled; SqlClientAttributesExtractor( SqlClientAttributesGetter getter, AttributeKey dbTableAttribute, - SqlStatementSanitizer sanitizer) { + boolean statementSanitizationEnabled) { super(getter); this.dbTableAttribute = dbTableAttribute; - this.sanitizer = sanitizer; + this.statementSanitizationEnabled = statementSanitizationEnabled; } @Override public void onStart(AttributesBuilder attributes, Context parentContext, REQUEST request) { super.onStart(attributes, parentContext, request); - SqlStatementInfo sanitizedStatement = sanitizer.sanitize(getter.getRawStatement(request)); + String rawStatement = getter.getRawStatement(request); + SqlStatementInfo sanitizedStatement = sanitizer.sanitize(rawStatement); String operation = sanitizedStatement.getOperation(); - internalSet(attributes, SemanticAttributes.DB_STATEMENT, sanitizedStatement.getFullStatement()); - internalSet(attributes, SemanticAttributes.DB_OPERATION, operation); + internalSet( + attributes, + DB_STATEMENT, + statementSanitizationEnabled ? sanitizedStatement.getFullStatement() : rawStatement); + internalSet(attributes, DB_OPERATION, operation); if (!SQL_CALL.equals(operation)) { internalSet(attributes, dbTableAttribute, sanitizedStatement.getMainIdentifier()); } diff --git a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/db/SqlClientAttributesExtractorBuilder.java b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/db/SqlClientAttributesExtractorBuilder.java similarity index 81% rename from instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/db/SqlClientAttributesExtractorBuilder.java rename to instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/db/SqlClientAttributesExtractorBuilder.java index 641eb42aac7c..3e172e3d288e 100644 --- a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/db/SqlClientAttributesExtractorBuilder.java +++ b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/db/SqlClientAttributesExtractorBuilder.java @@ -3,21 +3,22 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.instrumentation.api.instrumenter.db; +package io.opentelemetry.instrumentation.api.incubator.semconv.db; import static java.util.Objects.requireNonNull; import com.google.errorprone.annotations.CanIgnoreReturnValue; import io.opentelemetry.api.common.AttributeKey; -import io.opentelemetry.instrumentation.api.db.SqlStatementSanitizer; import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; /** A builder of {@link SqlClientAttributesExtractor}. */ public final class SqlClientAttributesExtractorBuilder { + // copied from DbIncubatingAttributes + private static final AttributeKey DB_SQL_TABLE = AttributeKey.stringKey("db.sql.table"); + final SqlClientAttributesGetter getter; - AttributeKey dbTableAttribute = SemanticAttributes.DB_SQL_TABLE; + AttributeKey dbTableAttribute = DB_SQL_TABLE; boolean statementSanitizationEnabled = true; SqlClientAttributesExtractorBuilder(SqlClientAttributesGetter getter) { @@ -27,7 +28,7 @@ public final class SqlClientAttributesExtractorBuilder { /** * Configures the extractor to set the table value extracted by the {@link * SqlClientAttributesExtractor} under the {@code dbTableAttribute} key. By default, the - * {@linkplain SemanticAttributes#DB_SQL_TABLE db.sql.table} attribute is used. + * db.sql.table attribute is used. * * @param dbTableAttribute The {@link AttributeKey} under which the table extracted by the {@link * SqlClientAttributesExtractor} will be stored. @@ -57,6 +58,6 @@ public SqlClientAttributesExtractorBuilder setStatementSaniti */ public AttributesExtractor build() { return new SqlClientAttributesExtractor<>( - getter, dbTableAttribute, SqlStatementSanitizer.create(statementSanitizationEnabled)); + getter, dbTableAttribute, statementSanitizationEnabled); } } diff --git a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/db/SqlClientAttributesGetter.java b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/db/SqlClientAttributesGetter.java similarity index 93% rename from instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/db/SqlClientAttributesGetter.java rename to instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/db/SqlClientAttributesGetter.java index 38ada758a1f6..e0a19c43df2a 100644 --- a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/db/SqlClientAttributesGetter.java +++ b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/db/SqlClientAttributesGetter.java @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.instrumentation.api.instrumenter.db; +package io.opentelemetry.instrumentation.api.incubator.semconv.db; import javax.annotation.Nullable; diff --git a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/db/SqlDialect.java b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/db/SqlDialect.java similarity index 81% rename from instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/db/SqlDialect.java rename to instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/db/SqlDialect.java index 9fd640ded6d3..173e399379aa 100644 --- a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/db/SqlDialect.java +++ b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/db/SqlDialect.java @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.instrumentation.api.db; +package io.opentelemetry.instrumentation.api.incubator.semconv.db; /** Enumeration of sql dialects that are handled differently by {@link SqlStatementSanitizer}. */ public enum SqlDialect { diff --git a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/db/SqlStatementInfo.java b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/db/SqlStatementInfo.java similarity index 90% rename from instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/db/SqlStatementInfo.java rename to instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/db/SqlStatementInfo.java index e03f64f89c02..04320206b060 100644 --- a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/db/SqlStatementInfo.java +++ b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/db/SqlStatementInfo.java @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.instrumentation.api.db; +package io.opentelemetry.instrumentation.api.incubator.semconv.db; import com.google.auto.value.AutoValue; import javax.annotation.Nullable; diff --git a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/db/SqlStatementSanitizer.java b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/db/SqlStatementSanitizer.java similarity index 96% rename from instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/db/SqlStatementSanitizer.java rename to instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/db/SqlStatementSanitizer.java index a9175364a897..e0ea15415ef9 100644 --- a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/db/SqlStatementSanitizer.java +++ b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/db/SqlStatementSanitizer.java @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.instrumentation.api.db; +package io.opentelemetry.instrumentation.api.incubator.semconv.db; import static io.opentelemetry.instrumentation.api.internal.SupportabilityMetrics.CounterNames.SQL_STATEMENT_SANITIZER_CACHE_MISS; diff --git a/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/http/HttpClientExperimentalMetrics.java b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/http/HttpClientExperimentalMetrics.java new file mode 100644 index 000000000000..dd08cbe493ff --- /dev/null +++ b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/http/HttpClientExperimentalMetrics.java @@ -0,0 +1,100 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.incubator.semconv.http; + +import static io.opentelemetry.instrumentation.api.incubator.semconv.http.HttpMessageBodySizeUtil.getHttpRequestBodySize; +import static io.opentelemetry.instrumentation.api.incubator.semconv.http.HttpMessageBodySizeUtil.getHttpResponseBodySize; +import static java.util.logging.Level.FINE; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.metrics.LongHistogram; +import io.opentelemetry.api.metrics.LongHistogramBuilder; +import io.opentelemetry.api.metrics.Meter; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.ContextKey; +import io.opentelemetry.instrumentation.api.instrumenter.OperationListener; +import io.opentelemetry.instrumentation.api.instrumenter.OperationMetrics; +import io.opentelemetry.instrumentation.api.internal.OperationMetricsUtil; +import java.util.logging.Logger; + +/** + * {@link OperationListener} which keeps track of non-stable + * HTTP client metrics: the + * request size and + * the response size. + */ +public final class HttpClientExperimentalMetrics implements OperationListener { + + private static final ContextKey HTTP_CLIENT_REQUEST_METRICS_START_ATTRIBUTES = + ContextKey.named("http-client-experimental-metrics-start-attributes"); + + private static final Logger logger = + Logger.getLogger(HttpClientExperimentalMetrics.class.getName()); + + /** + * Returns a {@link OperationMetrics} which can be used to enable recording of {@link + * HttpClientExperimentalMetrics} on an {@link + * io.opentelemetry.instrumentation.api.instrumenter.InstrumenterBuilder}. + */ + public static OperationMetrics get() { + return OperationMetricsUtil.create( + "experimental http client", HttpClientExperimentalMetrics::new); + } + + private final LongHistogram requestSize; + private final LongHistogram responseSize; + + private HttpClientExperimentalMetrics(Meter meter) { + LongHistogramBuilder requestSizeBuilder = + meter + .histogramBuilder("http.client.request.body.size") + .setUnit("By") + .setDescription("Size of HTTP client request bodies.") + .ofLongs(); + HttpExperimentalMetricsAdvice.applyClientRequestSizeAdvice(requestSizeBuilder); + requestSize = requestSizeBuilder.build(); + LongHistogramBuilder responseSizeBuilder = + meter + .histogramBuilder("http.client.response.body.size") + .setUnit("By") + .setDescription("Size of HTTP client response bodies.") + .ofLongs(); + HttpExperimentalMetricsAdvice.applyClientRequestSizeAdvice(responseSizeBuilder); + responseSize = responseSizeBuilder.build(); + } + + @Override + public Context onStart(Context context, Attributes startAttributes, long startNanos) { + return context.with(HTTP_CLIENT_REQUEST_METRICS_START_ATTRIBUTES, startAttributes); + } + + @Override + public void onEnd(Context context, Attributes endAttributes, long endNanos) { + Attributes startAttributes = context.get(HTTP_CLIENT_REQUEST_METRICS_START_ATTRIBUTES); + if (startAttributes == null) { + logger.log( + FINE, + "No state present when ending context {0}. Cannot record HTTP request metrics.", + context); + return; + } + + Attributes sizeAttributes = startAttributes.toBuilder().putAll(endAttributes).build(); + + Long requestBodySize = getHttpRequestBodySize(endAttributes, startAttributes); + if (requestBodySize != null) { + requestSize.record(requestBodySize, sizeAttributes, context); + } + + Long responseBodySize = getHttpResponseBodySize(endAttributes, startAttributes); + if (responseBodySize != null) { + responseSize.record(responseBodySize, sizeAttributes, context); + } + } +} diff --git a/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/http/HttpClientPeerServiceAttributesExtractor.java b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/http/HttpClientPeerServiceAttributesExtractor.java new file mode 100644 index 000000000000..2f4066aeec7c --- /dev/null +++ b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/http/HttpClientPeerServiceAttributesExtractor.java @@ -0,0 +1,95 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.incubator.semconv.http; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.incubator.semconv.net.PeerServiceResolver; +import io.opentelemetry.instrumentation.api.incubator.semconv.net.internal.UrlParser; +import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; +import io.opentelemetry.instrumentation.api.semconv.http.HttpClientAttributesGetter; +import java.util.function.Supplier; +import javax.annotation.Nullable; + +/** + * Extractor of the {@code peer.service} span attribute, described in the + * specification. + */ +public final class HttpClientPeerServiceAttributesExtractor + implements AttributesExtractor { + + // copied from PeerIncubatingAttributes + private static final AttributeKey PEER_SERVICE = AttributeKey.stringKey("peer.service"); + + private final HttpClientAttributesGetter attributesGetter; + private final PeerServiceResolver peerServiceResolver; + + // visible for tests + HttpClientPeerServiceAttributesExtractor( + HttpClientAttributesGetter attributesGetter, + PeerServiceResolver peerServiceResolver) { + this.attributesGetter = attributesGetter; + this.peerServiceResolver = peerServiceResolver; + } + + /** + * Returns a new {@link HttpClientPeerServiceAttributesExtractor} that will use the passed {@code + * attributesGetter} instance to determine the value of the {@code peer.service} attribute. + */ + public static + HttpClientPeerServiceAttributesExtractor create( + HttpClientAttributesGetter attributesGetter, + PeerServiceResolver peerServiceResolver) { + return new HttpClientPeerServiceAttributesExtractor<>(attributesGetter, peerServiceResolver); + } + + @Override + public void onStart(AttributesBuilder attributes, Context parentContext, REQUEST request) {} + + @SuppressWarnings("deprecation") // old semconv + @Override + public void onEnd( + AttributesBuilder attributes, + Context context, + REQUEST request, + @Nullable RESPONSE response, + @Nullable Throwable error) { + + if (peerServiceResolver.isEmpty()) { + // optimization for common case + return; + } + + String serverAddress = attributesGetter.getServerAddress(request); + Integer serverPort = attributesGetter.getServerPort(request); + Supplier pathSupplier = () -> getUrlPath(attributesGetter, request); + String peerService = mapToPeerService(serverAddress, serverPort, pathSupplier); + if (peerService != null) { + attributes.put(PEER_SERVICE, peerService); + } + } + + @Nullable + private String mapToPeerService( + @Nullable String host, @Nullable Integer port, @Nullable Supplier pathSupplier) { + if (host == null) { + return null; + } + return peerServiceResolver.resolveService(host, port, pathSupplier); + } + + @Nullable + private String getUrlPath( + HttpClientAttributesGetter attributesGetter, REQUEST request) { + String urlFull = attributesGetter.getUrlFull(request); + if (urlFull == null) { + return null; + } + return UrlParser.getPath(urlFull); + } +} diff --git a/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/http/HttpExperimentalAttributesExtractor.java b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/http/HttpExperimentalAttributesExtractor.java new file mode 100644 index 000000000000..730a078882c1 --- /dev/null +++ b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/http/HttpExperimentalAttributesExtractor.java @@ -0,0 +1,94 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.incubator.semconv.http; + +import static io.opentelemetry.instrumentation.api.internal.AttributesExtractorUtil.internalSet; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; +import io.opentelemetry.instrumentation.api.semconv.http.HttpClientAttributesGetter; +import io.opentelemetry.instrumentation.api.semconv.http.HttpCommonAttributesGetter; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerAttributesGetter; +import java.util.List; +import javax.annotation.Nullable; + +public final class HttpExperimentalAttributesExtractor + implements AttributesExtractor { + + // copied from HttpIncubatingAttributes + static final AttributeKey HTTP_REQUEST_BODY_SIZE = + AttributeKey.longKey("http.request.body.size"); + static final AttributeKey HTTP_RESPONSE_BODY_SIZE = + AttributeKey.longKey("http.response.body.size"); + + public static AttributesExtractor create( + HttpClientAttributesGetter getter) { + return new HttpExperimentalAttributesExtractor<>(getter); + } + + public static AttributesExtractor create( + HttpServerAttributesGetter getter) { + return new HttpExperimentalAttributesExtractor<>(getter); + } + + private final HttpCommonAttributesGetter getter; + + private HttpExperimentalAttributesExtractor( + HttpCommonAttributesGetter getter) { + this.getter = getter; + } + + @Override + public void onStart(AttributesBuilder attributes, Context parentContext, REQUEST request) {} + + @Override + public void onEnd( + AttributesBuilder attributes, + Context context, + REQUEST request, + @Nullable RESPONSE response, + @Nullable Throwable error) { + + Long requestBodySize = requestBodySize(request); + internalSet(attributes, HTTP_REQUEST_BODY_SIZE, requestBodySize); + + if (response != null) { + Long responseBodySize = responseBodySize(request, response); + internalSet(attributes, HTTP_RESPONSE_BODY_SIZE, responseBodySize); + } + } + + @Nullable + private Long requestBodySize(REQUEST request) { + return parseNumber(firstHeaderValue(getter.getHttpRequestHeader(request, "content-length"))); + } + + @Nullable + private Long responseBodySize(REQUEST request, RESPONSE response) { + return parseNumber( + firstHeaderValue(getter.getHttpResponseHeader(request, response, "content-length"))); + } + + @Nullable + static String firstHeaderValue(List values) { + return values.isEmpty() ? null : values.get(0); + } + + @Nullable + private static Long parseNumber(@Nullable String number) { + if (number == null) { + return null; + } + try { + return Long.parseLong(number); + } catch (NumberFormatException e) { + // not a number + return null; + } + } +} diff --git a/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/http/HttpExperimentalMetricsAdvice.java b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/http/HttpExperimentalMetricsAdvice.java new file mode 100644 index 000000000000..fb14ee0f0153 --- /dev/null +++ b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/http/HttpExperimentalMetricsAdvice.java @@ -0,0 +1,67 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.incubator.semconv.http; + +import static java.util.Arrays.asList; + +import io.opentelemetry.api.incubator.metrics.ExtendedLongHistogramBuilder; +import io.opentelemetry.api.incubator.metrics.ExtendedLongUpDownCounterBuilder; +import io.opentelemetry.api.metrics.LongHistogramBuilder; +import io.opentelemetry.api.metrics.LongUpDownCounterBuilder; +import io.opentelemetry.semconv.ErrorAttributes; +import io.opentelemetry.semconv.HttpAttributes; +import io.opentelemetry.semconv.NetworkAttributes; +import io.opentelemetry.semconv.ServerAttributes; +import io.opentelemetry.semconv.UrlAttributes; + +final class HttpExperimentalMetricsAdvice { + + static void applyClientRequestSizeAdvice(LongHistogramBuilder builder) { + if (!(builder instanceof ExtendedLongHistogramBuilder)) { + return; + } + ((ExtendedLongHistogramBuilder) builder) + .setAttributesAdvice( + asList( + HttpAttributes.HTTP_REQUEST_METHOD, + HttpAttributes.HTTP_RESPONSE_STATUS_CODE, + ErrorAttributes.ERROR_TYPE, + NetworkAttributes.NETWORK_PROTOCOL_NAME, + NetworkAttributes.NETWORK_PROTOCOL_VERSION, + ServerAttributes.SERVER_ADDRESS, + ServerAttributes.SERVER_PORT)); + } + + static void applyServerRequestSizeAdvice(LongHistogramBuilder builder) { + if (!(builder instanceof ExtendedLongHistogramBuilder)) { + return; + } + ((ExtendedLongHistogramBuilder) builder) + .setAttributesAdvice( + asList( + // stable attributes + HttpAttributes.HTTP_ROUTE, + HttpAttributes.HTTP_REQUEST_METHOD, + HttpAttributes.HTTP_RESPONSE_STATUS_CODE, + ErrorAttributes.ERROR_TYPE, + NetworkAttributes.NETWORK_PROTOCOL_NAME, + NetworkAttributes.NETWORK_PROTOCOL_VERSION, + UrlAttributes.URL_SCHEME)); + } + + static void applyServerActiveRequestsAdvice(LongUpDownCounterBuilder builder) { + if (!(builder instanceof ExtendedLongUpDownCounterBuilder)) { + return; + } + ((ExtendedLongUpDownCounterBuilder) builder) + .setAttributesAdvice( + asList( + // https://github.com/open-telemetry/semantic-conventions/blob/v1.23.0/docs/http/http-metrics.md#metric-httpserveractive_requests + HttpAttributes.HTTP_REQUEST_METHOD, UrlAttributes.URL_SCHEME)); + } + + private HttpExperimentalMetricsAdvice() {} +} diff --git a/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/http/HttpMessageBodySizeUtil.java b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/http/HttpMessageBodySizeUtil.java new file mode 100644 index 000000000000..6b1847d2c40c --- /dev/null +++ b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/http/HttpMessageBodySizeUtil.java @@ -0,0 +1,37 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.incubator.semconv.http; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import javax.annotation.Nullable; + +final class HttpMessageBodySizeUtil { + + @Nullable + static Long getHttpRequestBodySize(Attributes... attributesList) { + return getAttribute(HttpExperimentalAttributesExtractor.HTTP_REQUEST_BODY_SIZE, attributesList); + } + + @Nullable + static Long getHttpResponseBodySize(Attributes... attributesList) { + return getAttribute( + HttpExperimentalAttributesExtractor.HTTP_RESPONSE_BODY_SIZE, attributesList); + } + + @Nullable + private static T getAttribute(AttributeKey key, Attributes... attributesList) { + for (Attributes attributes : attributesList) { + T value = attributes.get(key); + if (value != null) { + return value; + } + } + return null; + } + + private HttpMessageBodySizeUtil() {} +} diff --git a/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/http/HttpServerExperimentalMetrics.java b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/http/HttpServerExperimentalMetrics.java new file mode 100644 index 000000000000..7c91292f513e --- /dev/null +++ b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/http/HttpServerExperimentalMetrics.java @@ -0,0 +1,117 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.incubator.semconv.http; + +import static io.opentelemetry.instrumentation.api.incubator.semconv.http.HttpMessageBodySizeUtil.getHttpRequestBodySize; +import static io.opentelemetry.instrumentation.api.incubator.semconv.http.HttpMessageBodySizeUtil.getHttpResponseBodySize; +import static java.util.logging.Level.FINE; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.metrics.LongHistogram; +import io.opentelemetry.api.metrics.LongHistogramBuilder; +import io.opentelemetry.api.metrics.LongUpDownCounter; +import io.opentelemetry.api.metrics.LongUpDownCounterBuilder; +import io.opentelemetry.api.metrics.Meter; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.ContextKey; +import io.opentelemetry.instrumentation.api.instrumenter.OperationListener; +import io.opentelemetry.instrumentation.api.instrumenter.OperationMetrics; +import io.opentelemetry.instrumentation.api.internal.OperationMetricsUtil; +import java.util.logging.Logger; + +/** + * {@link OperationListener} which keeps track of non-stable + * HTTP server metrics: the + * number of in-flight request, the + * request size and the + * response size. + */ +public final class HttpServerExperimentalMetrics implements OperationListener { + + private static final ContextKey HTTP_SERVER_EXPERIMENTAL_METRICS_START_ATTRIBUTES = + ContextKey.named("http-server-experimental-metrics-start-attributes"); + + private static final Logger logger = + Logger.getLogger(HttpServerExperimentalMetrics.class.getName()); + + /** + * Returns a {@link OperationMetrics} which can be used to enable recording of {@link + * HttpServerExperimentalMetrics} on an {@link + * io.opentelemetry.instrumentation.api.instrumenter.InstrumenterBuilder}. + */ + public static OperationMetrics get() { + return OperationMetricsUtil.create( + "experimental http server", HttpServerExperimentalMetrics::new); + } + + private final LongUpDownCounter activeRequests; + private final LongHistogram requestSize; + private final LongHistogram responseSize; + + private HttpServerExperimentalMetrics(Meter meter) { + LongUpDownCounterBuilder activeRequestsBuilder = + meter + .upDownCounterBuilder("http.server.active_requests") + .setUnit("{requests}") + .setDescription("Number of active HTTP server requests."); + HttpExperimentalMetricsAdvice.applyServerActiveRequestsAdvice(activeRequestsBuilder); + activeRequests = activeRequestsBuilder.build(); + LongHistogramBuilder requestSizeBuilder = + meter + .histogramBuilder("http.server.request.body.size") + .setUnit("By") + .setDescription("Size of HTTP server request bodies.") + .ofLongs(); + HttpExperimentalMetricsAdvice.applyServerRequestSizeAdvice(requestSizeBuilder); + requestSize = requestSizeBuilder.build(); + LongHistogramBuilder responseSizeBuilder = + meter + .histogramBuilder("http.server.response.body.size") + .setUnit("By") + .setDescription("Size of HTTP server response bodies.") + .ofLongs(); + HttpExperimentalMetricsAdvice.applyServerRequestSizeAdvice(responseSizeBuilder); + responseSize = responseSizeBuilder.build(); + } + + @Override + public Context onStart(Context context, Attributes startAttributes, long startNanos) { + activeRequests.add(1, startAttributes, context); + + return context.with(HTTP_SERVER_EXPERIMENTAL_METRICS_START_ATTRIBUTES, startAttributes); + } + + @Override + public void onEnd(Context context, Attributes endAttributes, long endNanos) { + Attributes startAttributes = context.get(HTTP_SERVER_EXPERIMENTAL_METRICS_START_ATTRIBUTES); + if (startAttributes == null) { + logger.log( + FINE, + "No state present when ending context {0}. Cannot record HTTP request metrics.", + context); + return; + } + // it's important to use exactly the same attributes that were used when incrementing the active + // request count (otherwise it will split the timeseries) + activeRequests.add(-1, startAttributes, context); + + Attributes sizeAttributes = startAttributes.toBuilder().putAll(endAttributes).build(); + + Long requestBodySize = getHttpRequestBodySize(endAttributes, startAttributes); + if (requestBodySize != null) { + requestSize.record(requestBodySize, sizeAttributes, context); + } + + Long responseBodySize = getHttpResponseBodySize(endAttributes, startAttributes); + if (responseBodySize != null) { + responseSize.record(responseBodySize, sizeAttributes, context); + } + } +} diff --git a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/messaging/CapturedMessageHeadersUtil.java b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/messaging/CapturedMessageHeadersUtil.java similarity index 94% rename from instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/messaging/CapturedMessageHeadersUtil.java rename to instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/messaging/CapturedMessageHeadersUtil.java index e1ec7ae780eb..2a756f5797ab 100644 --- a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/messaging/CapturedMessageHeadersUtil.java +++ b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/messaging/CapturedMessageHeadersUtil.java @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.instrumentation.api.instrumenter.messaging; +package io.opentelemetry.instrumentation.api.incubator.semconv.messaging; import static java.util.Collections.unmodifiableList; diff --git a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/messaging/MessageOperation.java b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/messaging/MessageOperation.java similarity index 50% rename from instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/messaging/MessageOperation.java rename to instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/messaging/MessageOperation.java index 34f266bb7803..e7ef5c479ed0 100644 --- a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/messaging/MessageOperation.java +++ b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/messaging/MessageOperation.java @@ -3,23 +3,23 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.instrumentation.api.instrumenter.messaging; +package io.opentelemetry.instrumentation.api.incubator.semconv.messaging; import java.util.Locale; /** * Represents type of operations + * href="https://github.com/open-telemetry/semantic-conventions/blob/main/docs/messaging/messaging-spans.md#operation-names">operations * that may be used in a messaging system. */ public enum MessageOperation { - SEND, + PUBLISH, RECEIVE, PROCESS; /** * Returns the operation name as defined in the + * href="https://github.com/open-telemetry/semantic-conventions/blob/main/docs/messaging/messaging-spans.md#operation-names">the * specification. */ String operationName() { diff --git a/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/messaging/MessagingAttributesExtractor.java b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/messaging/MessagingAttributesExtractor.java new file mode 100644 index 000000000000..bdf5ab879786 --- /dev/null +++ b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/messaging/MessagingAttributesExtractor.java @@ -0,0 +1,158 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.incubator.semconv.messaging; + +import static io.opentelemetry.instrumentation.api.internal.AttributesExtractorUtil.internalSet; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; +import io.opentelemetry.instrumentation.api.internal.SpanKey; +import io.opentelemetry.instrumentation.api.internal.SpanKeyProvider; +import java.util.List; +import javax.annotation.Nullable; + +/** + * Extractor of messaging + * attributes. + * + *

This class delegates to a type-specific {@link MessagingAttributesGetter} for individual + * attribute extraction from request/response objects. + */ +public final class MessagingAttributesExtractor + implements AttributesExtractor, SpanKeyProvider { + + // copied from MessagingIncubatingAttributes + private static final AttributeKey MESSAGING_BATCH_MESSAGE_COUNT = + AttributeKey.longKey("messaging.batch.message_count"); + private static final AttributeKey MESSAGING_CLIENT_ID = + AttributeKey.stringKey("messaging.client_id"); + private static final AttributeKey MESSAGING_DESTINATION_ANONYMOUS = + AttributeKey.booleanKey("messaging.destination.anonymous"); + private static final AttributeKey MESSAGING_DESTINATION_NAME = + AttributeKey.stringKey("messaging.destination.name"); + private static final AttributeKey MESSAGING_DESTINATION_PARTITION_ID = + AttributeKey.stringKey("messaging.destination.partition.id"); + private static final AttributeKey MESSAGING_DESTINATION_TEMPLATE = + AttributeKey.stringKey("messaging.destination.template"); + private static final AttributeKey MESSAGING_DESTINATION_TEMPORARY = + AttributeKey.booleanKey("messaging.destination.temporary"); + private static final AttributeKey MESSAGING_MESSAGE_BODY_SIZE = + AttributeKey.longKey("messaging.message.body.size"); + private static final AttributeKey MESSAGING_MESSAGE_CONVERSATION_ID = + AttributeKey.stringKey("messaging.message.conversation_id"); + private static final AttributeKey MESSAGING_MESSAGE_ENVELOPE_SIZE = + AttributeKey.longKey("messaging.message.envelope.size"); + private static final AttributeKey MESSAGING_MESSAGE_ID = + AttributeKey.stringKey("messaging.message.id"); + private static final AttributeKey MESSAGING_OPERATION = + AttributeKey.stringKey("messaging.operation"); + private static final AttributeKey MESSAGING_SYSTEM = + AttributeKey.stringKey("messaging.system"); + + static final String TEMP_DESTINATION_NAME = "(temporary)"; + + /** + * Creates the messaging attributes extractor for the given {@link MessageOperation operation} + * with default configuration. + */ + public static AttributesExtractor create( + MessagingAttributesGetter getter, MessageOperation operation) { + return builder(getter, operation).build(); + } + + /** + * Returns a new {@link MessagingAttributesExtractorBuilder} for the given {@link MessageOperation + * operation} that can be used to configure the messaging attributes extractor. + */ + public static MessagingAttributesExtractorBuilder builder( + MessagingAttributesGetter getter, MessageOperation operation) { + return new MessagingAttributesExtractorBuilder<>(getter, operation); + } + + private final MessagingAttributesGetter getter; + private final MessageOperation operation; + private final List capturedHeaders; + + MessagingAttributesExtractor( + MessagingAttributesGetter getter, + MessageOperation operation, + List capturedHeaders) { + this.getter = getter; + this.operation = operation; + this.capturedHeaders = CapturedMessageHeadersUtil.lowercase(capturedHeaders); + } + + @Override + public void onStart(AttributesBuilder attributes, Context parentContext, REQUEST request) { + internalSet(attributes, MESSAGING_SYSTEM, getter.getSystem(request)); + boolean isTemporaryDestination = getter.isTemporaryDestination(request); + if (isTemporaryDestination) { + internalSet(attributes, MESSAGING_DESTINATION_TEMPORARY, true); + internalSet(attributes, MESSAGING_DESTINATION_NAME, TEMP_DESTINATION_NAME); + } else { + internalSet(attributes, MESSAGING_DESTINATION_NAME, getter.getDestination(request)); + internalSet( + attributes, MESSAGING_DESTINATION_TEMPLATE, getter.getDestinationTemplate(request)); + } + internalSet( + attributes, MESSAGING_DESTINATION_PARTITION_ID, getter.getDestinationPartitionId(request)); + boolean isAnonymousDestination = getter.isAnonymousDestination(request); + if (isAnonymousDestination) { + internalSet(attributes, MESSAGING_DESTINATION_ANONYMOUS, true); + } + internalSet(attributes, MESSAGING_MESSAGE_CONVERSATION_ID, getter.getConversationId(request)); + internalSet(attributes, MESSAGING_MESSAGE_BODY_SIZE, getter.getMessageBodySize(request)); + internalSet( + attributes, MESSAGING_MESSAGE_ENVELOPE_SIZE, getter.getMessageEnvelopeSize(request)); + internalSet(attributes, MESSAGING_CLIENT_ID, getter.getClientId(request)); + if (operation != null) { + internalSet(attributes, MESSAGING_OPERATION, operation.operationName()); + } + } + + @Override + public void onEnd( + AttributesBuilder attributes, + Context context, + REQUEST request, + @Nullable RESPONSE response, + @Nullable Throwable error) { + internalSet(attributes, MESSAGING_MESSAGE_ID, getter.getMessageId(request, response)); + internalSet( + attributes, MESSAGING_BATCH_MESSAGE_COUNT, getter.getBatchMessageCount(request, response)); + + for (String name : capturedHeaders) { + List values = getter.getMessageHeader(request, name); + if (!values.isEmpty()) { + internalSet(attributes, CapturedMessageHeadersUtil.attributeKey(name), values); + } + } + } + + /** + * This method is internal and is hence not for public use. Its API is unstable and can change at + * any time. + */ + @Override + public SpanKey internalGetSpanKey() { + if (operation == null) { + return null; + } + + switch (operation) { + case PUBLISH: + return SpanKey.PRODUCER; + case RECEIVE: + return SpanKey.CONSUMER_RECEIVE; + case PROCESS: + return SpanKey.CONSUMER_PROCESS; + } + throw new IllegalStateException("Can't possibly happen"); + } +} diff --git a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/messaging/MessagingAttributesExtractorBuilder.java b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/messaging/MessagingAttributesExtractorBuilder.java similarity index 95% rename from instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/messaging/MessagingAttributesExtractorBuilder.java rename to instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/messaging/MessagingAttributesExtractorBuilder.java index 25eee6ed869d..6cc82adb4ddc 100644 --- a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/messaging/MessagingAttributesExtractorBuilder.java +++ b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/messaging/MessagingAttributesExtractorBuilder.java @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.instrumentation.api.instrumenter.messaging; +package io.opentelemetry.instrumentation.api.incubator.semconv.messaging; import static java.util.Collections.emptyList; diff --git a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/messaging/MessagingAttributesGetter.java b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/messaging/MessagingAttributesGetter.java similarity index 62% rename from instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/messaging/MessagingAttributesGetter.java rename to instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/messaging/MessagingAttributesGetter.java index 18d1899a08ba..524e36b236c1 100644 --- a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/messaging/MessagingAttributesGetter.java +++ b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/messaging/MessagingAttributesGetter.java @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.instrumentation.api.instrumenter.messaging; +package io.opentelemetry.instrumentation.api.incubator.semconv.messaging; import static java.util.Collections.emptyList; @@ -25,20 +25,48 @@ public interface MessagingAttributesGetter { @Nullable String getDestination(REQUEST request); + @Nullable + String getDestinationTemplate(REQUEST request); + boolean isTemporaryDestination(REQUEST request); + boolean isAnonymousDestination(REQUEST request); + @Nullable String getConversationId(REQUEST request); @Nullable - Long getMessagePayloadSize(REQUEST request); + @Deprecated + default Long getMessagePayloadSize(REQUEST request) { + return null; + } + + @Nullable + @Deprecated + default Long getMessagePayloadCompressedSize(REQUEST request) { + return null; + } + + @Nullable + Long getMessageBodySize(REQUEST request); @Nullable - Long getMessagePayloadCompressedSize(REQUEST request); + Long getMessageEnvelopeSize(REQUEST request); @Nullable String getMessageId(REQUEST request, @Nullable RESPONSE response); + @Nullable + String getClientId(REQUEST request); + + @Nullable + Long getBatchMessageCount(REQUEST request, @Nullable RESPONSE response); + + @Nullable + default String getDestinationPartitionId(REQUEST request) { + return null; + } + /** * Extracts all values of header named {@code name} from the request, or an empty list if there * were none. diff --git a/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/messaging/MessagingConsumerMetrics.java b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/messaging/MessagingConsumerMetrics.java new file mode 100644 index 000000000000..fec179dc8da8 --- /dev/null +++ b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/messaging/MessagingConsumerMetrics.java @@ -0,0 +1,112 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.incubator.semconv.messaging; + +import static java.util.logging.Level.FINE; + +import com.google.auto.value.AutoValue; +import com.google.errorprone.annotations.CanIgnoreReturnValue; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.metrics.DoubleHistogram; +import io.opentelemetry.api.metrics.DoubleHistogramBuilder; +import io.opentelemetry.api.metrics.LongCounter; +import io.opentelemetry.api.metrics.LongCounterBuilder; +import io.opentelemetry.api.metrics.Meter; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.ContextKey; +import io.opentelemetry.instrumentation.api.instrumenter.OperationListener; +import io.opentelemetry.instrumentation.api.instrumenter.OperationMetrics; +import io.opentelemetry.instrumentation.api.internal.OperationMetricsUtil; +import java.util.concurrent.TimeUnit; +import java.util.logging.Logger; + +/** + * {@link OperationListener} which keeps track of Consumer + * metrics. + */ +public final class MessagingConsumerMetrics implements OperationListener { + private static final double NANOS_PER_S = TimeUnit.SECONDS.toNanos(1); + + // copied from MessagingIncubatingAttributes + private static final AttributeKey MESSAGING_BATCH_MESSAGE_COUNT = + AttributeKey.longKey("messaging.batch.message_count"); + private static final ContextKey MESSAGING_CONSUMER_METRICS_STATE = + ContextKey.named("messaging-consumer-metrics-state"); + private static final Logger logger = Logger.getLogger(MessagingConsumerMetrics.class.getName()); + + private final DoubleHistogram receiveDurationHistogram; + private final LongCounter receiveMessageCount; + + private MessagingConsumerMetrics(Meter meter) { + DoubleHistogramBuilder durationBuilder = + meter + .histogramBuilder("messaging.receive.duration") + .setDescription("Measures the duration of receive operation.") + .setExplicitBucketBoundariesAdvice(MessagingMetricsAdvice.DURATION_SECONDS_BUCKETS) + .setUnit("s"); + MessagingMetricsAdvice.applyReceiveDurationAdvice(durationBuilder); + receiveDurationHistogram = durationBuilder.build(); + + LongCounterBuilder longCounterBuilder = + meter + .counterBuilder("messaging.receive.messages") + .setDescription("Measures the number of received messages.") + .setUnit("{message}"); + MessagingMetricsAdvice.applyReceiveMessagesAdvice(longCounterBuilder); + receiveMessageCount = longCounterBuilder.build(); + } + + public static OperationMetrics get() { + return OperationMetricsUtil.create("messaging consumer", MessagingConsumerMetrics::new); + } + + @Override + @CanIgnoreReturnValue + public Context onStart(Context context, Attributes startAttributes, long startNanos) { + return context.with( + MESSAGING_CONSUMER_METRICS_STATE, + new AutoValue_MessagingConsumerMetrics_State(startAttributes, startNanos)); + } + + @Override + public void onEnd(Context context, Attributes endAttributes, long endNanos) { + MessagingConsumerMetrics.State state = context.get(MESSAGING_CONSUMER_METRICS_STATE); + if (state == null) { + logger.log( + FINE, + "No state present when ending context {0}. Cannot record consumer receive metrics.", + context); + return; + } + + Attributes attributes = state.startAttributes().toBuilder().putAll(endAttributes).build(); + receiveDurationHistogram.record( + (endNanos - state.startTimeNanos()) / NANOS_PER_S, attributes, context); + + long receiveMessagesCount = getReceiveMessagesCount(state.startAttributes(), endAttributes); + receiveMessageCount.add(receiveMessagesCount, attributes, context); + } + + private static long getReceiveMessagesCount(Attributes... attributesList) { + for (Attributes attributes : attributesList) { + Long value = attributes.get(MESSAGING_BATCH_MESSAGE_COUNT); + if (value != null) { + return value; + } + } + return 1; + } + + @AutoValue + abstract static class State { + + abstract Attributes startAttributes(); + + abstract long startTimeNanos(); + } +} diff --git a/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/messaging/MessagingMetricsAdvice.java b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/messaging/MessagingMetricsAdvice.java new file mode 100644 index 000000000000..f87c814d2a31 --- /dev/null +++ b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/messaging/MessagingMetricsAdvice.java @@ -0,0 +1,70 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.incubator.semconv.messaging; + +import static java.util.Arrays.asList; +import static java.util.Collections.unmodifiableList; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.incubator.metrics.ExtendedDoubleHistogramBuilder; +import io.opentelemetry.api.incubator.metrics.ExtendedLongCounterBuilder; +import io.opentelemetry.api.metrics.DoubleHistogramBuilder; +import io.opentelemetry.api.metrics.LongCounterBuilder; +import io.opentelemetry.semconv.ErrorAttributes; +import io.opentelemetry.semconv.ServerAttributes; +import java.util.List; + +final class MessagingMetricsAdvice { + static final List DURATION_SECONDS_BUCKETS = + unmodifiableList( + asList(0.005, 0.01, 0.025, 0.05, 0.075, 0.1, 0.25, 0.5, 0.75, 1.0, 2.5, 5.0, 7.5, 10.0)); + + // copied from MessagingIncubatingAttributes + private static final AttributeKey MESSAGING_SYSTEM = + AttributeKey.stringKey("messaging.system"); + private static final AttributeKey MESSAGING_DESTINATION_NAME = + AttributeKey.stringKey("messaging.destination.name"); + private static final AttributeKey MESSAGING_OPERATION = + AttributeKey.stringKey("messaging.operation"); + private static final AttributeKey MESSAGING_DESTINATION_PARTITION_ID = + AttributeKey.stringKey("messaging.destination.partition.id"); + private static final AttributeKey MESSAGING_DESTINATION_TEMPLATE = + AttributeKey.stringKey("messaging.destination.template"); + + private static final List> MESSAGING_ATTRIBUTES = + asList( + MESSAGING_SYSTEM, + MESSAGING_DESTINATION_NAME, + MESSAGING_OPERATION, + MESSAGING_DESTINATION_PARTITION_ID, + MESSAGING_DESTINATION_TEMPLATE, + ErrorAttributes.ERROR_TYPE, + ServerAttributes.SERVER_PORT, + ServerAttributes.SERVER_ADDRESS); + + static void applyPublishDurationAdvice(DoubleHistogramBuilder builder) { + if (!(builder instanceof ExtendedDoubleHistogramBuilder)) { + return; + } + ((ExtendedDoubleHistogramBuilder) builder).setAttributesAdvice(MESSAGING_ATTRIBUTES); + } + + static void applyReceiveDurationAdvice(DoubleHistogramBuilder builder) { + if (!(builder instanceof ExtendedDoubleHistogramBuilder)) { + return; + } + ((ExtendedDoubleHistogramBuilder) builder).setAttributesAdvice(MESSAGING_ATTRIBUTES); + } + + static void applyReceiveMessagesAdvice(LongCounterBuilder builder) { + if (!(builder instanceof ExtendedLongCounterBuilder)) { + return; + } + ((ExtendedLongCounterBuilder) builder).setAttributesAdvice(MESSAGING_ATTRIBUTES); + } + + private MessagingMetricsAdvice() {} +} diff --git a/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/messaging/MessagingProducerMetrics.java b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/messaging/MessagingProducerMetrics.java new file mode 100644 index 000000000000..44d5b243744a --- /dev/null +++ b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/messaging/MessagingProducerMetrics.java @@ -0,0 +1,85 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.incubator.semconv.messaging; + +import static java.util.logging.Level.FINE; + +import com.google.auto.value.AutoValue; +import com.google.errorprone.annotations.CanIgnoreReturnValue; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.metrics.DoubleHistogram; +import io.opentelemetry.api.metrics.DoubleHistogramBuilder; +import io.opentelemetry.api.metrics.Meter; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.ContextKey; +import io.opentelemetry.instrumentation.api.instrumenter.OperationListener; +import io.opentelemetry.instrumentation.api.instrumenter.OperationMetrics; +import io.opentelemetry.instrumentation.api.internal.OperationMetricsUtil; +import java.util.concurrent.TimeUnit; +import java.util.logging.Logger; + +/** + * {@link OperationListener} which keeps track of Producer + * metrics. + */ +public final class MessagingProducerMetrics implements OperationListener { + private static final double NANOS_PER_S = TimeUnit.SECONDS.toNanos(1); + + private static final ContextKey MESSAGING_PRODUCER_METRICS_STATE = + ContextKey.named("messaging-producer-metrics-state"); + private static final Logger logger = Logger.getLogger(MessagingProducerMetrics.class.getName()); + + private final DoubleHistogram publishDurationHistogram; + + private MessagingProducerMetrics(Meter meter) { + DoubleHistogramBuilder durationBuilder = + meter + .histogramBuilder("messaging.publish.duration") + .setDescription("Measures the duration of publish operation.") + .setExplicitBucketBoundariesAdvice(MessagingMetricsAdvice.DURATION_SECONDS_BUCKETS) + .setUnit("s"); + MessagingMetricsAdvice.applyPublishDurationAdvice(durationBuilder); + publishDurationHistogram = durationBuilder.build(); + } + + public static OperationMetrics get() { + return OperationMetricsUtil.create("messaging produce", MessagingProducerMetrics::new); + } + + @Override + @CanIgnoreReturnValue + public Context onStart(Context context, Attributes startAttributes, long startNanos) { + return context.with( + MESSAGING_PRODUCER_METRICS_STATE, + new AutoValue_MessagingProducerMetrics_State(startAttributes, startNanos)); + } + + @Override + public void onEnd(Context context, Attributes endAttributes, long endNanos) { + MessagingProducerMetrics.State state = context.get(MESSAGING_PRODUCER_METRICS_STATE); + if (state == null) { + logger.log( + FINE, + "No state present when ending context {0}. Cannot record produce publish metrics.", + context); + return; + } + + Attributes attributes = state.startAttributes().toBuilder().putAll(endAttributes).build(); + + publishDurationHistogram.record( + (endNanos - state.startTimeNanos()) / NANOS_PER_S, attributes, context); + } + + @AutoValue + abstract static class State { + + abstract Attributes startAttributes(); + + abstract long startTimeNanos(); + } +} diff --git a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/messaging/MessagingSpanNameExtractor.java b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/messaging/MessagingSpanNameExtractor.java similarity index 87% rename from instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/messaging/MessagingSpanNameExtractor.java rename to instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/messaging/MessagingSpanNameExtractor.java index f6162deb80ff..624d3b8e00bf 100644 --- a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/messaging/MessagingSpanNameExtractor.java +++ b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/messaging/MessagingSpanNameExtractor.java @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.instrumentation.api.instrumenter.messaging; +package io.opentelemetry.instrumentation.api.incubator.semconv.messaging; import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor; @@ -11,7 +11,7 @@ public final class MessagingSpanNameExtractor implements SpanNameExtrac /** * Returns a {@link SpanNameExtractor} that constructs the span name according to + * href="https://github.com/open-telemetry/semantic-conventions/blob/main/docs/messaging/messaging-spans.md#span-name"> * messaging semantic conventions: {@code }. * * @see MessagingAttributesGetter#getDestination(Object) used to extract {@code the + * specification. + */ +public final class PeerServiceAttributesExtractor + implements AttributesExtractor { + + // copied from PeerIncubatingAttributes + private static final AttributeKey PEER_SERVICE = AttributeKey.stringKey("peer.service"); + + private final ServerAttributesGetter attributesGetter; + private final PeerServiceResolver peerServiceResolver; + + // visible for tests + PeerServiceAttributesExtractor( + ServerAttributesGetter attributesGetter, PeerServiceResolver peerServiceResolver) { + this.attributesGetter = attributesGetter; + this.peerServiceResolver = peerServiceResolver; + } + + /** + * Returns a new {@link PeerServiceAttributesExtractor} that will use the passed {@code + * attributesGetter} instance to determine the value of the {@code peer.service} attribute. + */ + public static AttributesExtractor create( + ServerAttributesGetter attributesGetter, PeerServiceResolver peerServiceResolver) { + return new PeerServiceAttributesExtractor<>(attributesGetter, peerServiceResolver); + } + + @Override + public void onStart(AttributesBuilder attributes, Context parentContext, REQUEST request) {} + + @SuppressWarnings("deprecation") // old semconv + @Override + public void onEnd( + AttributesBuilder attributes, + Context context, + REQUEST request, + @Nullable RESPONSE response, + @Nullable Throwable error) { + + if (peerServiceResolver.isEmpty()) { + // optimization for common case + return; + } + + String serverAddress = attributesGetter.getServerAddress(request); + Integer serverPort = attributesGetter.getServerPort(request); + String peerService = mapToPeerService(serverAddress, serverPort); + if (peerService != null) { + attributes.put(PEER_SERVICE, peerService); + } + } + + @Nullable + private String mapToPeerService(@Nullable String host, @Nullable Integer port) { + if (host == null) { + return null; + } + return peerServiceResolver.resolveService(host, port, null); + } +} diff --git a/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/net/PeerServiceResolver.java b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/net/PeerServiceResolver.java new file mode 100644 index 000000000000..62223f714dcf --- /dev/null +++ b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/net/PeerServiceResolver.java @@ -0,0 +1,23 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.incubator.semconv.net; + +import java.util.Map; +import java.util.function.Supplier; +import javax.annotation.Nullable; + +public interface PeerServiceResolver { + + public boolean isEmpty(); + + @Nullable + public String resolveService( + String host, @Nullable Integer port, @Nullable Supplier pathSupplier); + + static PeerServiceResolver create(Map mapping) { + return new PeerServiceResolverImpl(mapping); + } +} diff --git a/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/net/PeerServiceResolverImpl.java b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/net/PeerServiceResolverImpl.java new file mode 100644 index 000000000000..cd896887f22f --- /dev/null +++ b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/net/PeerServiceResolverImpl.java @@ -0,0 +1,99 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.incubator.semconv.net; + +import static java.util.Comparator.comparing; +import static java.util.Comparator.naturalOrder; +import static java.util.Comparator.nullsFirst; + +import com.google.auto.value.AutoValue; +import io.opentelemetry.instrumentation.api.incubator.semconv.net.internal.UrlParser; +import java.util.Comparator; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Supplier; +import javax.annotation.Nullable; + +class PeerServiceResolverImpl implements PeerServiceResolver { + + private static final Comparator matcherComparator = + nullsFirst( + comparing(ServiceMatcher::getPort, nullsFirst(naturalOrder())) + .thenComparing(comparing(ServiceMatcher::getPath, nullsFirst(naturalOrder())))); + + private final Map> mapping = new HashMap<>(); + + PeerServiceResolverImpl(Map peerServiceMapping) { + peerServiceMapping.forEach( + (key, serviceName) -> { + String url = "https://" + key; + String host = UrlParser.getHost(url); + Integer port = UrlParser.getPort(url); + String path = UrlParser.getPath(url); + Map matchers = + mapping.computeIfAbsent(host, x -> new HashMap<>()); + matchers.putIfAbsent(ServiceMatcher.create(port, path), serviceName); + }); + } + + @Override + public boolean isEmpty() { + return mapping.isEmpty(); + } + + @Override + @Nullable + public String resolveService( + String host, @Nullable Integer port, @Nullable Supplier pathSupplier) { + Map matchers = mapping.get(host); + if (matchers == null) { + return null; + } + return matchers.entrySet().stream() + .filter(entry -> entry.getKey().matches(port, pathSupplier)) + .max((o1, o2) -> matcherComparator.compare(o1.getKey(), o2.getKey())) + .map(Map.Entry::getValue) + .orElse(null); + } + + @AutoValue + abstract static class ServiceMatcher { + + static ServiceMatcher create(Integer port, String path) { + return new AutoValue_PeerServiceResolverImpl_ServiceMatcher(port, path); + } + + @Nullable + abstract Integer getPort(); + + @Nullable + abstract String getPath(); + + public boolean matches(Integer port, Supplier pathSupplier) { + if (this.getPort() != null) { + if (!this.getPort().equals(port)) { + return false; + } + } + if (this.getPath() != null && this.getPath().length() > 0) { + if (pathSupplier == null) { + return false; + } + String path = pathSupplier.get(); + if (path == null) { + return false; + } + if (!path.startsWith(this.getPath())) { + return false; + } + if (port != null) { + return port.equals(this.getPort()); + } + } + return true; + } + } +} diff --git a/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/net/internal/UrlParser.java b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/net/internal/UrlParser.java new file mode 100644 index 000000000000..9cd354b0d76e --- /dev/null +++ b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/net/internal/UrlParser.java @@ -0,0 +1,148 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.incubator.semconv.net.internal; + +import java.util.function.Predicate; +import javax.annotation.Nullable; + +/** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time. + */ +public final class UrlParser { + + @Nullable + public static String getHost(String url) { + + int startIndex = getHostStartIndex(url); + if (startIndex == -1) { + return null; + } + + int endIndexExclusive = getHostEndIndexExclusive(url, startIndex); + if (endIndexExclusive == startIndex) { + return null; + } + + return url.substring(startIndex, endIndexExclusive); + } + + @Nullable + public static Integer getPort(String url) { + + int hostStartIndex = getHostStartIndex(url); + if (hostStartIndex == -1) { + return null; + } + + int hostEndIndexExclusive = getHostEndIndexExclusive(url, hostStartIndex); + if (hostEndIndexExclusive == hostStartIndex) { + return null; + } + + if (hostEndIndexExclusive < url.length() && url.charAt(hostEndIndexExclusive) != ':') { + return null; + } + + int portStartIndex = hostEndIndexExclusive + 1; + + int portEndIndexExclusive = getPortEndIndexExclusive(url, portStartIndex); + if (portEndIndexExclusive == portStartIndex) { + return null; + } + + return safeParse(url.substring(portStartIndex, portEndIndexExclusive)); + } + + @Nullable + public static String getPath(String url) { + + int hostStartIndex = getHostStartIndex(url); + if (hostStartIndex == -1) { + return null; + } + + int hostEndIndexExclusive = getHostEndIndexExclusive(url, hostStartIndex); + if (hostEndIndexExclusive == hostStartIndex) { + return null; + } + + int pathStartIndex = url.indexOf('/', hostEndIndexExclusive); + if (pathStartIndex == -1) { + return null; + } + + int pathEndIndexExclusive = getPathEndIndexExclusive(url, pathStartIndex); + if (pathEndIndexExclusive == pathStartIndex) { + return null; + } + + return url.substring(pathStartIndex, pathEndIndexExclusive); + } + + private static int getHostStartIndex(String url) { + + int schemeEndIndex = url.indexOf(':'); + if (schemeEndIndex == -1) { + // not a valid url + return -1; + } + + int len = url.length(); + if (len <= schemeEndIndex + 2 + || url.charAt(schemeEndIndex + 1) != '/' + || url.charAt(schemeEndIndex + 2) != '/') { + // has no authority component + return -1; + } + + return schemeEndIndex + 3; + } + + private static int getHostEndIndexExclusive(String url, int startIndex) { + // look for the end of the host: + // ':' ==> start of port, or + // '/', '?', '#' ==> start of path + return getEndIndexExclusive( + url, startIndex, c -> (c == ':' || c == '/' || c == '?' || c == '#')); + } + + private static int getPortEndIndexExclusive(String url, int startIndex) { + // look for the end of the port: + // '/', '?', '#' ==> start of path + return getEndIndexExclusive(url, startIndex, c -> (c == '/' || c == '?' || c == '#')); + } + + private static int getPathEndIndexExclusive(String url, int startIndex) { + // look for the end of the path: + // '?', '#' ==> end of path + return getEndIndexExclusive(url, startIndex, c -> (c == '?' || c == '#')); + } + + private static int getEndIndexExclusive( + String url, int startIndex, Predicate predicate) { + int index; + int len = url.length(); + for (index = startIndex; index < len; index++) { + char c = url.charAt(index); + if (predicate.test(c)) { + break; + } + } + return index; + } + + @Nullable + private static Integer safeParse(String port) { + try { + return Integer.valueOf(port); + } catch (NumberFormatException e) { + return null; + } + } + + private UrlParser() {} +} diff --git a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/rpc/RpcAttributesGetter.java b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/rpc/RpcAttributesGetter.java similarity index 90% rename from instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/rpc/RpcAttributesGetter.java rename to instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/rpc/RpcAttributesGetter.java index a22fe4cb5df8..2e7d0a6e539d 100644 --- a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/rpc/RpcAttributesGetter.java +++ b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/rpc/RpcAttributesGetter.java @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.instrumentation.api.instrumenter.rpc; +package io.opentelemetry.instrumentation.api.incubator.semconv.rpc; import javax.annotation.Nullable; diff --git a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/rpc/RpcClientAttributesExtractor.java b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/rpc/RpcClientAttributesExtractor.java similarity index 86% rename from instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/rpc/RpcClientAttributesExtractor.java rename to instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/rpc/RpcClientAttributesExtractor.java index 33a0f2d796bc..c0759f237438 100644 --- a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/rpc/RpcClientAttributesExtractor.java +++ b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/rpc/RpcClientAttributesExtractor.java @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.instrumentation.api.instrumenter.rpc; +package io.opentelemetry.instrumentation.api.incubator.semconv.rpc; import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; import io.opentelemetry.instrumentation.api.internal.SpanKey; @@ -11,7 +11,7 @@ /** * Extractor of RPC + * href="https://github.com/open-telemetry/semantic-conventions/blob/main/docs/rpc/rpc-spans.md">RPC * client attributes. * *

This class delegates to a type-specific {@link RpcAttributesGetter} for individual attribute diff --git a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/rpc/RpcClientMetrics.java b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/rpc/RpcClientMetrics.java similarity index 78% rename from instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/rpc/RpcClientMetrics.java rename to instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/rpc/RpcClientMetrics.java index eba0bfbd6e34..db001a3afc87 100644 --- a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/rpc/RpcClientMetrics.java +++ b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/rpc/RpcClientMetrics.java @@ -3,25 +3,26 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.instrumentation.api.instrumenter.rpc; +package io.opentelemetry.instrumentation.api.incubator.semconv.rpc; -import static io.opentelemetry.instrumentation.api.instrumenter.rpc.MetricsView.applyClientView; import static java.util.logging.Level.FINE; import com.google.auto.value.AutoValue; import io.opentelemetry.api.common.Attributes; import io.opentelemetry.api.metrics.DoubleHistogram; +import io.opentelemetry.api.metrics.DoubleHistogramBuilder; import io.opentelemetry.api.metrics.Meter; import io.opentelemetry.context.Context; import io.opentelemetry.context.ContextKey; import io.opentelemetry.instrumentation.api.instrumenter.OperationListener; import io.opentelemetry.instrumentation.api.instrumenter.OperationMetrics; +import io.opentelemetry.instrumentation.api.internal.OperationMetricsUtil; import java.util.concurrent.TimeUnit; import java.util.logging.Logger; /** * {@link OperationListener} which keeps track of RPC + * href="https://github.com/open-telemetry/semantic-conventions/blob/main/docs/rpc/rpc-metrics.md#rpc-client">RPC * client metrics. */ public final class RpcClientMetrics implements OperationListener { @@ -36,12 +37,13 @@ public final class RpcClientMetrics implements OperationListener { private final DoubleHistogram clientDurationHistogram; private RpcClientMetrics(Meter meter) { - clientDurationHistogram = + DoubleHistogramBuilder durationBuilder = meter .histogramBuilder("rpc.client.duration") .setDescription("The duration of an outbound RPC invocation") - .setUnit("ms") - .build(); + .setUnit("ms"); + RpcMetricsAdvice.applyClientDurationAdvice(durationBuilder); + clientDurationHistogram = durationBuilder.build(); } /** @@ -50,7 +52,7 @@ private RpcClientMetrics(Meter meter) { * io.opentelemetry.instrumentation.api.instrumenter.InstrumenterBuilder}. */ public static OperationMetrics get() { - return RpcClientMetrics::new; + return OperationMetricsUtil.create("rpc client", RpcClientMetrics::new); } @Override @@ -72,7 +74,7 @@ public void onEnd(Context context, Attributes endAttributes, long endNanos) { } clientDurationHistogram.record( (endNanos - state.startTimeNanos()) / NANOS_PER_MS, - applyClientView(state.startAttributes(), endAttributes), + state.startAttributes().toBuilder().putAll(endAttributes).build(), context); } diff --git a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/rpc/RpcCommonAttributesExtractor.java b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/rpc/RpcCommonAttributesExtractor.java similarity index 61% rename from instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/rpc/RpcCommonAttributesExtractor.java rename to instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/rpc/RpcCommonAttributesExtractor.java index 618a1f31aca1..f731e703586c 100644 --- a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/rpc/RpcCommonAttributesExtractor.java +++ b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/rpc/RpcCommonAttributesExtractor.java @@ -3,19 +3,24 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.instrumentation.api.instrumenter.rpc; +package io.opentelemetry.instrumentation.api.incubator.semconv.rpc; import static io.opentelemetry.instrumentation.api.internal.AttributesExtractorUtil.internalSet; +import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.common.AttributesBuilder; import io.opentelemetry.context.Context; import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; import javax.annotation.Nullable; abstract class RpcCommonAttributesExtractor implements AttributesExtractor { + // copied from RpcIncubatingAttributes + static final AttributeKey RPC_METHOD = AttributeKey.stringKey("rpc.method"); + static final AttributeKey RPC_SERVICE = AttributeKey.stringKey("rpc.service"); + static final AttributeKey RPC_SYSTEM = AttributeKey.stringKey("rpc.system"); + private final RpcAttributesGetter getter; RpcCommonAttributesExtractor(RpcAttributesGetter getter) { @@ -24,9 +29,9 @@ abstract class RpcCommonAttributesExtractor @Override public final void onStart(AttributesBuilder attributes, Context parentContext, REQUEST request) { - internalSet(attributes, SemanticAttributes.RPC_SYSTEM, getter.getSystem(request)); - internalSet(attributes, SemanticAttributes.RPC_SERVICE, getter.getService(request)); - internalSet(attributes, SemanticAttributes.RPC_METHOD, getter.getMethod(request)); + internalSet(attributes, RPC_SYSTEM, getter.getSystem(request)); + internalSet(attributes, RPC_SERVICE, getter.getService(request)); + internalSet(attributes, RPC_METHOD, getter.getMethod(request)); } @Override diff --git a/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/rpc/RpcMetricsAdvice.java b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/rpc/RpcMetricsAdvice.java new file mode 100644 index 000000000000..0a24cca3f39c --- /dev/null +++ b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/rpc/RpcMetricsAdvice.java @@ -0,0 +1,60 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.incubator.semconv.rpc; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.incubator.metrics.ExtendedDoubleHistogramBuilder; +import io.opentelemetry.api.metrics.DoubleHistogramBuilder; +import io.opentelemetry.semconv.NetworkAttributes; +import io.opentelemetry.semconv.ServerAttributes; +import java.util.Arrays; + +final class RpcMetricsAdvice { + + // copied from RpcIncubatingAttributes + private static final AttributeKey RPC_GRPC_STATUS_CODE = + AttributeKey.longKey("rpc.grpc.status_code"); + + static void applyClientDurationAdvice(DoubleHistogramBuilder builder) { + if (!(builder instanceof ExtendedDoubleHistogramBuilder)) { + return; + } + // the list of recommended metrics attributes is from + // https://github.com/open-telemetry/semantic-conventions/blob/main/docs/rpc/rpc-metrics.md + ((ExtendedDoubleHistogramBuilder) builder) + .setAttributesAdvice( + Arrays.asList( + RpcCommonAttributesExtractor.RPC_SYSTEM, + RpcCommonAttributesExtractor.RPC_SERVICE, + RpcCommonAttributesExtractor.RPC_METHOD, + RPC_GRPC_STATUS_CODE, + NetworkAttributes.NETWORK_TYPE, + NetworkAttributes.NETWORK_TRANSPORT, + ServerAttributes.SERVER_ADDRESS, + ServerAttributes.SERVER_PORT)); + } + + static void applyServerDurationAdvice(DoubleHistogramBuilder builder) { + if (!(builder instanceof ExtendedDoubleHistogramBuilder)) { + return; + } + // the list of recommended metrics attributes is from + // https://github.com/open-telemetry/semantic-conventions/blob/main/docs/rpc/rpc-metrics.md + ((ExtendedDoubleHistogramBuilder) builder) + .setAttributesAdvice( + Arrays.asList( + RpcCommonAttributesExtractor.RPC_SYSTEM, + RpcCommonAttributesExtractor.RPC_SERVICE, + RpcCommonAttributesExtractor.RPC_METHOD, + RPC_GRPC_STATUS_CODE, + NetworkAttributes.NETWORK_TYPE, + NetworkAttributes.NETWORK_TRANSPORT, + ServerAttributes.SERVER_ADDRESS, + ServerAttributes.SERVER_PORT)); + } + + private RpcMetricsAdvice() {} +} diff --git a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/rpc/RpcServerAttributesExtractor.java b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/rpc/RpcServerAttributesExtractor.java similarity index 86% rename from instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/rpc/RpcServerAttributesExtractor.java rename to instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/rpc/RpcServerAttributesExtractor.java index eb3f2b7e17ce..768af20dc7d6 100644 --- a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/rpc/RpcServerAttributesExtractor.java +++ b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/rpc/RpcServerAttributesExtractor.java @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.instrumentation.api.instrumenter.rpc; +package io.opentelemetry.instrumentation.api.incubator.semconv.rpc; import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; import io.opentelemetry.instrumentation.api.internal.SpanKey; @@ -11,7 +11,7 @@ /** * Extractor of RPC + * href="https://github.com/open-telemetry/semantic-conventions/blob/main/docs/rpc/rpc-spans.md">RPC * server attributes. * *

This class delegates to a type-specific {@link RpcAttributesGetter} for individual attribute diff --git a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/rpc/RpcServerMetrics.java b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/rpc/RpcServerMetrics.java similarity index 78% rename from instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/rpc/RpcServerMetrics.java rename to instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/rpc/RpcServerMetrics.java index 2c54283086f0..08f50d8b3483 100644 --- a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/rpc/RpcServerMetrics.java +++ b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/rpc/RpcServerMetrics.java @@ -3,25 +3,26 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.instrumentation.api.instrumenter.rpc; +package io.opentelemetry.instrumentation.api.incubator.semconv.rpc; -import static io.opentelemetry.instrumentation.api.instrumenter.rpc.MetricsView.applyServerView; import static java.util.logging.Level.FINE; import com.google.auto.value.AutoValue; import io.opentelemetry.api.common.Attributes; import io.opentelemetry.api.metrics.DoubleHistogram; +import io.opentelemetry.api.metrics.DoubleHistogramBuilder; import io.opentelemetry.api.metrics.Meter; import io.opentelemetry.context.Context; import io.opentelemetry.context.ContextKey; import io.opentelemetry.instrumentation.api.instrumenter.OperationListener; import io.opentelemetry.instrumentation.api.instrumenter.OperationMetrics; +import io.opentelemetry.instrumentation.api.internal.OperationMetricsUtil; import java.util.concurrent.TimeUnit; import java.util.logging.Logger; /** * {@link OperationListener} which keeps track of RPC + * href="https://github.com/open-telemetry/semantic-conventions/blob/main/docs/rpc/rpc-metrics.md#rpc-server">RPC * server metrics. */ public final class RpcServerMetrics implements OperationListener { @@ -36,12 +37,13 @@ public final class RpcServerMetrics implements OperationListener { private final DoubleHistogram serverDurationHistogram; private RpcServerMetrics(Meter meter) { - serverDurationHistogram = + DoubleHistogramBuilder durationBuilder = meter .histogramBuilder("rpc.server.duration") .setDescription("The duration of an inbound RPC invocation") - .setUnit("ms") - .build(); + .setUnit("ms"); + RpcMetricsAdvice.applyServerDurationAdvice(durationBuilder); + serverDurationHistogram = durationBuilder.build(); } /** @@ -50,7 +52,7 @@ private RpcServerMetrics(Meter meter) { * io.opentelemetry.instrumentation.api.instrumenter.InstrumenterBuilder}. */ public static OperationMetrics get() { - return RpcServerMetrics::new; + return OperationMetricsUtil.create("rpc server", RpcServerMetrics::new); } @Override @@ -72,7 +74,7 @@ public void onEnd(Context context, Attributes endAttributes, long endNanos) { } serverDurationHistogram.record( (endNanos - state.startTimeNanos()) / NANOS_PER_MS, - applyServerView(state.startAttributes(), endAttributes), + state.startAttributes().toBuilder().putAll(endAttributes).build(), context); } diff --git a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/rpc/RpcSpanNameExtractor.java b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/rpc/RpcSpanNameExtractor.java similarity index 94% rename from instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/rpc/RpcSpanNameExtractor.java rename to instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/rpc/RpcSpanNameExtractor.java index 1ecc8021068b..921839bd9234 100644 --- a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/rpc/RpcSpanNameExtractor.java +++ b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/rpc/RpcSpanNameExtractor.java @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.instrumentation.api.instrumenter.rpc; +package io.opentelemetry.instrumentation.api.incubator.semconv.rpc; import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor; diff --git a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/util/ClassAndMethod.java b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/util/ClassAndMethod.java similarity index 77% rename from instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/util/ClassAndMethod.java rename to instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/util/ClassAndMethod.java index ac9e55098417..9b192a5d29d8 100644 --- a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/util/ClassAndMethod.java +++ b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/util/ClassAndMethod.java @@ -3,10 +3,10 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.instrumentation.api.instrumenter.util; +package io.opentelemetry.instrumentation.api.incubator.semconv.util; import com.google.auto.value.AutoValue; -import io.opentelemetry.instrumentation.api.instrumenter.code.CodeAttributesGetter; +import io.opentelemetry.instrumentation.api.incubator.semconv.code.CodeAttributesGetter; @AutoValue public abstract class ClassAndMethod { diff --git a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/util/ClassAndMethodAttributesGetter.java b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/util/ClassAndMethodAttributesGetter.java similarity index 75% rename from instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/util/ClassAndMethodAttributesGetter.java rename to instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/util/ClassAndMethodAttributesGetter.java index db02cddad043..0bb664338f65 100644 --- a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/util/ClassAndMethodAttributesGetter.java +++ b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/util/ClassAndMethodAttributesGetter.java @@ -3,9 +3,9 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.instrumentation.api.instrumenter.util; +package io.opentelemetry.instrumentation.api.incubator.semconv.util; -import io.opentelemetry.instrumentation.api.instrumenter.code.CodeAttributesGetter; +import io.opentelemetry.instrumentation.api.incubator.semconv.code.CodeAttributesGetter; import javax.annotation.Nullable; enum ClassAndMethodAttributesGetter implements CodeAttributesGetter { diff --git a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/util/SpanNames.java b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/util/SpanNames.java similarity index 95% rename from instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/util/SpanNames.java rename to instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/util/SpanNames.java index 87e0ca076e2c..7962b0474333 100644 --- a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/util/SpanNames.java +++ b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/util/SpanNames.java @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.instrumentation.api.instrumenter.util; +package io.opentelemetry.instrumentation.api.incubator.semconv.util; import io.opentelemetry.instrumentation.api.internal.ClassNames; import io.opentelemetry.instrumentation.api.internal.cache.Cache; diff --git a/instrumentation-api-semconv/src/main/jflex/SqlSanitizer.jflex b/instrumentation-api-incubator/src/main/jflex/SqlSanitizer.jflex similarity index 74% rename from instrumentation-api-semconv/src/main/jflex/SqlSanitizer.jflex rename to instrumentation-api-incubator/src/main/jflex/SqlSanitizer.jflex index 8ce3e24a8bec..317a7c7ad8d6 100644 --- a/instrumentation-api-semconv/src/main/jflex/SqlSanitizer.jflex +++ b/instrumentation-api-incubator/src/main/jflex/SqlSanitizer.jflex @@ -3,7 +3,9 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.instrumentation.api.db; +package io.opentelemetry.instrumentation.api.incubator.semconv.db; + +import java.util.regex.Pattern; %% @@ -16,19 +18,20 @@ package io.opentelemetry.instrumentation.api.db; %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 = "$$" [^$]* "$$" -BACKTICK_QUOTED_STR = "`" [^`]* "`" -WHITESPACE = [ \t\r\n]+ +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 = "$$" [^$]* "$$" +BACKTICK_QUOTED_STR = "`" [^`]* "`" +POSTGRE_PARAM_MARKER = "$"[0-9]* +WHITESPACE = [ \t\r\n]+ %{ static SqlStatementInfo sanitize(String statement, SqlDialect dialect) { @@ -52,6 +55,10 @@ WHITESPACE = [ \t\r\n]+ // max length of the sanitized statement - SQLs longer than this will be trimmed static final int LIMIT = 32 * 1024; + // Match on strings like "IN(?, ?, ...)" + private static final Pattern IN_STATEMENT_PATTERN = Pattern.compile("(\\sIN\\s*)\\(\\s*\\?\\s*(?:,\\s*\\?\\s*)*+\\)", Pattern.CASE_INSENSITIVE); + private static final String IN_STATEMENT_NORMALIZED = "$1(?)"; + private final StringBuilder builder = new StringBuilder(); private void appendCurrentFragment() { @@ -122,13 +129,56 @@ WHITESPACE = [ \t\r\n]+ /** @return true if all statement info is gathered */ boolean handleNext() { return false; - } + } + + /** @return true if all statement info is gathered */ + boolean handleOperationTarget(String target) { + return false; + } + + boolean expectingOperationTarget() { + return false; + } SqlStatementInfo getResult(String fullStatement) { return SqlStatementInfo.create(fullStatement, getClass().getSimpleName().toUpperCase(java.util.Locale.ROOT), mainIdentifier); } } + private abstract class DdlOperation extends Operation { + private String operationTarget = ""; + private boolean expectingOperationTarget = true; + + boolean expectingOperationTarget() { + return expectingOperationTarget; + } + + boolean handleOperationTarget(String target) { + operationTarget = target; + expectingOperationTarget = false; + return false; + } + + boolean shouldHandleIdentifier() { + // Return true only if the provided value corresponds to a table, as it will be used to set the attribute `db.sql.table`. + return "TABLE".equals(operationTarget); + } + + boolean handleIdentifier() { + if (shouldHandleIdentifier()) { + mainIdentifier = readIdentifierName(); + } + return true; + } + + SqlStatementInfo getResult(String fullStatement) { + if (!"".equals(operationTarget)) { + return SqlStatementInfo.create(fullStatement, getClass().getSimpleName().toUpperCase(java.util.Locale.ROOT) + " " + operationTarget, mainIdentifier); + } + return super.getResult(fullStatement); + } + } + private static class NoOp extends Operation { static final Operation INSTANCE = new NoOp(); @@ -273,12 +323,25 @@ WHITESPACE = [ \t\r\n]+ } } + private class Create extends DdlOperation { + } + + private class Drop extends DdlOperation { + } + + private class Alter extends DdlOperation { + } + private SqlStatementInfo getResult() { if (builder.length() > LIMIT) { builder.delete(LIMIT, builder.length()); } String fullStatement = builder.toString(); - return operation.getResult(fullStatement); + + // Normalize all 'in (?, ?, ...)' statements to in (?) to reduce cardinality + String normalizedStatement = IN_STATEMENT_PATTERN.matcher(fullStatement).replaceAll(IN_STATEMENT_NORMALIZED); + + return operation.getResult(normalizedStatement); } %} @@ -329,6 +392,27 @@ WHITESPACE = [ \t\r\n]+ appendCurrentFragment(); if (isOverLimit()) return YYEOF; } + "CREATE" { + if (!insideComment) { + setOperation(new Create()); + } + appendCurrentFragment(); + if (isOverLimit()) return YYEOF; + } + "DROP" { + if (!insideComment) { + setOperation(new Drop()); + } + appendCurrentFragment(); + if (isOverLimit()) return YYEOF; + } + "ALTER" { + if (!insideComment) { + setOperation(new Alter()); + } + appendCurrentFragment(); + if (isOverLimit()) return YYEOF; + } "FROM" { if (!insideComment && !extractionDone) { if (operation == NoOp.INSTANCE) { @@ -357,11 +441,27 @@ WHITESPACE = [ \t\r\n]+ } "NEXT" { if (!insideComment && !extractionDone) { - extractionDone = operation.handleNext(); + extractionDone = operation.handleNext(); + } + appendCurrentFragment(); + if (isOverLimit()) return YYEOF; + } + "IF" | "NOT" | "EXISTS" { + appendCurrentFragment(); + if (isOverLimit()) return YYEOF; + } + "TABLE" | "INDEX" | "DATABASE" | "PROCEDURE" | "VIEW" { + if (!insideComment && !extractionDone) { + if (operation.expectingOperationTarget()) { + extractionDone = operation.handleOperationTarget(yytext()); + } else { + extractionDone = operation.handleIdentifier(); } + } appendCurrentFragment(); if (isOverLimit()) return YYEOF; } + {COMMA} { if (!insideComment && !extractionDone) { extractionDone = operation.handleComma(); @@ -421,7 +521,7 @@ WHITESPACE = [ \t\r\n]+ if (isOverLimit()) return YYEOF; } - {BACKTICK_QUOTED_STR} { + {BACKTICK_QUOTED_STR} | {POSTGRE_PARAM_MARKER} { if (!insideComment && !extractionDone) { extractionDone = operation.handleIdentifier(); } @@ -437,4 +537,4 @@ WHITESPACE = [ \t\r\n]+ appendCurrentFragment(); if (isOverLimit()) return YYEOF; } -} \ No newline at end of file +} diff --git a/instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/code/CodeAttributesExtractorTest.java b/instrumentation-api-incubator/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv/code/CodeAttributesExtractorTest.java similarity index 88% rename from instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/code/CodeAttributesExtractorTest.java rename to instrumentation-api-incubator/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv/code/CodeAttributesExtractorTest.java index ee913bebf183..1e399b0aa95a 100644 --- a/instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/code/CodeAttributesExtractorTest.java +++ b/instrumentation-api-incubator/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv/code/CodeAttributesExtractorTest.java @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.instrumentation.api.instrumenter.code; +package io.opentelemetry.instrumentation.api.incubator.semconv.code; import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; import static org.assertj.core.api.Assertions.entry; @@ -12,7 +12,7 @@ import io.opentelemetry.api.common.AttributesBuilder; import io.opentelemetry.context.Context; import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.opentelemetry.semconv.incubating.CodeIncubatingAttributes; import java.util.Collections; import java.util.HashMap; import java.util.Map; @@ -59,8 +59,8 @@ void shouldExtractAllAttributes() { // then assertThat(startAttributes.build()) .containsOnly( - entry(SemanticAttributes.CODE_NAMESPACE, TestClass.class.getName()), - entry(SemanticAttributes.CODE_FUNCTION, "doSomething")); + entry(CodeIncubatingAttributes.CODE_NAMESPACE, TestClass.class.getName()), + entry(CodeIncubatingAttributes.CODE_FUNCTION, "doSomething")); assertThat(endAttributes.build().isEmpty()).isTrue(); } diff --git a/instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/code/CodeSpanNameExtractorTest.java b/instrumentation-api-incubator/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv/code/CodeSpanNameExtractorTest.java similarity index 71% rename from instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/code/CodeSpanNameExtractorTest.java rename to instrumentation-api-incubator/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv/code/CodeSpanNameExtractorTest.java index 1c0cc3de110a..fba84a21d697 100644 --- a/instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/code/CodeSpanNameExtractorTest.java +++ b/instrumentation-api-incubator/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv/code/CodeSpanNameExtractorTest.java @@ -3,10 +3,10 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.instrumentation.api.instrumenter.code; +package io.opentelemetry.instrumentation.api.incubator.semconv.code; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.when; import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor; import org.junit.jupiter.api.Test; @@ -23,8 +23,8 @@ void shouldExtractFullSpanName() { // given Object request = new Object(); - doReturn(TestClass.class).when(getter).getCodeClass(request); - doReturn("doSomething").when(getter).getMethodName(request); + when(getter.getCodeClass(request)).thenAnswer(invocation -> TestClass.class); + when(getter.getMethodName(request)).thenReturn("doSomething"); SpanNameExtractor underTest = CodeSpanNameExtractor.create(getter); @@ -41,8 +41,8 @@ void shouldExtractFullSpanNameForAnonymousClass() { AnonymousBaseClass anon = new AnonymousBaseClass() {}; Object request = new Object(); - doReturn(anon.getClass()).when(getter).getCodeClass(request); - doReturn("doSomething").when(getter).getMethodName(request); + when(getter.getCodeClass(request)).thenAnswer(invocation -> anon.getClass()); + when(getter.getMethodName(request)).thenReturn("doSomething"); SpanNameExtractor underTest = CodeSpanNameExtractor.create(getter); @@ -59,8 +59,8 @@ void shouldExtractFullSpanNameForLambda() { Runnable lambda = () -> {}; Object request = new Object(); - doReturn(lambda.getClass()).when(getter).getCodeClass(request); - doReturn("doSomething").when(getter).getMethodName(request); + when(getter.getCodeClass(request)).thenAnswer(invocation -> lambda.getClass()); + when(getter.getMethodName(request)).thenReturn("doSomething"); SpanNameExtractor underTest = CodeSpanNameExtractor.create(getter); @@ -68,7 +68,7 @@ void shouldExtractFullSpanNameForLambda() { String spanName = underTest.extract(request); // then - assertEquals(getClass().getSimpleName() + "$$Lambda$.doSomething", spanName); + assertEquals(getClass().getSimpleName() + "$$Lambda.doSomething", spanName); } static class TestClass {} diff --git a/instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/db/DbClientAttributesExtractorTest.java b/instrumentation-api-incubator/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv/db/DbClientAttributesExtractorTest.java similarity index 81% rename from instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/db/DbClientAttributesExtractorTest.java rename to instrumentation-api-incubator/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv/db/DbClientAttributesExtractorTest.java index ea7462c36833..68991d1b813a 100644 --- a/instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/db/DbClientAttributesExtractorTest.java +++ b/instrumentation-api-incubator/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv/db/DbClientAttributesExtractorTest.java @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.instrumentation.api.instrumenter.db; +package io.opentelemetry.instrumentation.api.incubator.semconv.db; import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; import static org.assertj.core.api.Assertions.entry; @@ -12,7 +12,7 @@ import io.opentelemetry.api.common.AttributesBuilder; import io.opentelemetry.context.Context; import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.opentelemetry.semconv.incubating.DbIncubatingAttributes; import java.util.Collections; import java.util.HashMap; import java.util.Map; @@ -52,6 +52,7 @@ public String getOperation(Map map) { } } + @SuppressWarnings("deprecation") // TODO DbIncubatingAttributes.DB_CONNECTION_STRING deprecation @Test void shouldExtractAllAvailableAttributes() { // given @@ -78,12 +79,12 @@ void shouldExtractAllAvailableAttributes() { // 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")); + entry(DbIncubatingAttributes.DB_SYSTEM, "myDb"), + entry(DbIncubatingAttributes.DB_USER, "username"), + entry(DbIncubatingAttributes.DB_NAME, "potatoes"), + entry(DbIncubatingAttributes.DB_CONNECTION_STRING, "mydb:///potatoes"), + entry(DbIncubatingAttributes.DB_STATEMENT, "SELECT * FROM potato"), + entry(DbIncubatingAttributes.DB_OPERATION, "SELECT")); assertThat(endAttributes.build().isEmpty()).isTrue(); } diff --git a/instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/db/DbClientSpanNameExtractorTest.java b/instrumentation-api-incubator/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv/db/DbClientSpanNameExtractorTest.java similarity index 98% rename from instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/db/DbClientSpanNameExtractorTest.java rename to instrumentation-api-incubator/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv/db/DbClientSpanNameExtractorTest.java index d7b4e635095c..39698bdc1ca2 100644 --- a/instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/db/DbClientSpanNameExtractorTest.java +++ b/instrumentation-api-incubator/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv/db/DbClientSpanNameExtractorTest.java @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.instrumentation.api.instrumenter.db; +package io.opentelemetry.instrumentation.api.incubator.semconv.db; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.Mockito.when; diff --git a/instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/db/RedisCommandSanitizerTest.java b/instrumentation-api-incubator/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv/db/RedisCommandSanitizerTest.java similarity index 99% rename from instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/db/RedisCommandSanitizerTest.java rename to instrumentation-api-incubator/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv/db/RedisCommandSanitizerTest.java index 14cb95d22aa0..e20c412b15fa 100644 --- a/instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/db/RedisCommandSanitizerTest.java +++ b/instrumentation-api-incubator/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv/db/RedisCommandSanitizerTest.java @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.instrumentation.api.db; +package io.opentelemetry.instrumentation.api.incubator.semconv.db; import static org.assertj.core.api.AssertionsForClassTypes.assertThat; diff --git a/instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/db/SqlClientAttributesExtractorTest.java b/instrumentation-api-incubator/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv/db/SqlClientAttributesExtractorTest.java similarity index 76% rename from instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/db/SqlClientAttributesExtractorTest.java rename to instrumentation-api-incubator/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv/db/SqlClientAttributesExtractorTest.java index 066d19b6d6db..f782a20b16ff 100644 --- a/instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/db/SqlClientAttributesExtractorTest.java +++ b/instrumentation-api-incubator/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv/db/SqlClientAttributesExtractorTest.java @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.instrumentation.api.instrumenter.db; +package io.opentelemetry.instrumentation.api.incubator.semconv.db; import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; import static org.assertj.core.api.Assertions.entry; @@ -12,7 +12,7 @@ import io.opentelemetry.api.common.AttributesBuilder; import io.opentelemetry.context.Context; import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.opentelemetry.semconv.incubating.DbIncubatingAttributes; import java.util.Collections; import java.util.HashMap; import java.util.Map; @@ -49,6 +49,7 @@ public String getConnectionString(Map map) { } } + @SuppressWarnings("deprecation") // TODO DbIncubatingAttributes.DB_CONNECTION_STRING deprecation @Test void shouldExtractAllAttributes() { // given @@ -74,13 +75,13 @@ void shouldExtractAllAttributes() { // 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")); + entry(DbIncubatingAttributes.DB_SYSTEM, "myDb"), + entry(DbIncubatingAttributes.DB_USER, "username"), + entry(DbIncubatingAttributes.DB_NAME, "potatoes"), + entry(DbIncubatingAttributes.DB_CONNECTION_STRING, "mydb:///potatoes"), + entry(DbIncubatingAttributes.DB_STATEMENT, "SELECT * FROM potato WHERE id=?"), + entry(DbIncubatingAttributes.DB_OPERATION, "SELECT"), + entry(DbIncubatingAttributes.DB_SQL_TABLE, "potato")); assertThat(endAttributes.build().isEmpty()).isTrue(); } @@ -103,8 +104,8 @@ void shouldNotExtractTableIfAttributeIsNotSet() { // then assertThat(attributes.build()) .containsOnly( - entry(SemanticAttributes.DB_STATEMENT, "SELECT *"), - entry(SemanticAttributes.DB_OPERATION, "SELECT")); + entry(DbIncubatingAttributes.DB_STATEMENT, "SELECT *"), + entry(DbIncubatingAttributes.DB_OPERATION, "SELECT")); } @Test @@ -117,7 +118,7 @@ void shouldExtractTableToSpecifiedKey() { AttributesExtractor, Void> underTest = SqlClientAttributesExtractor., Void>builder(new TestAttributesGetter()) - .setTableAttribute(SemanticAttributes.DB_CASSANDRA_TABLE) + .setTableAttribute(DbIncubatingAttributes.DB_CASSANDRA_TABLE) .build(); // when @@ -127,9 +128,9 @@ void shouldExtractTableToSpecifiedKey() { // then assertThat(attributes.build()) .containsOnly( - entry(SemanticAttributes.DB_STATEMENT, "SELECT * FROM table"), - entry(SemanticAttributes.DB_OPERATION, "SELECT"), - entry(SemanticAttributes.DB_CASSANDRA_TABLE, "table")); + entry(DbIncubatingAttributes.DB_STATEMENT, "SELECT * FROM table"), + entry(DbIncubatingAttributes.DB_OPERATION, "SELECT"), + entry(DbIncubatingAttributes.DB_CASSANDRA_TABLE, "table")); } @Test diff --git a/instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/db/SqlStatementSanitizerTest.java b/instrumentation-api-incubator/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv/db/SqlStatementSanitizerTest.java similarity index 79% rename from instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/db/SqlStatementSanitizerTest.java rename to instrumentation-api-incubator/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv/db/SqlStatementSanitizerTest.java index fd25d13c4474..c52ffa966c3f 100644 --- a/instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/db/SqlStatementSanitizerTest.java +++ b/instrumentation-api-incubator/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv/db/SqlStatementSanitizerTest.java @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.instrumentation.api.db; +package io.opentelemetry.instrumentation.api.incubator.semconv.db; import static org.assertj.core.api.AssertionsForClassTypes.assertThat; @@ -36,10 +36,12 @@ void normalizeCouchbase(String original, String expected) { @ParameterizedTest @ArgumentsSource(SimplifyArgs.class) - void simplifySql(String original, Function expecter) { + void simplifySql(String original, Function expectedFunction) { SqlStatementInfo result = SqlStatementSanitizer.create(true).sanitize(original); - String expected = expecter.apply(original).getFullStatement(); - assertThat(result.getFullStatement()).isEqualTo(expected); + SqlStatementInfo expected = expectedFunction.apply(original); + assertThat(result.getFullStatement()).isEqualTo(expected.getFullStatement()); + assertThat(result.getOperation()).isEqualTo(expected.getOperation()); + assertThat(result.getMainIdentifier()).isEqualToIgnoringCase(expected.getMainIdentifier()); } @Test @@ -58,6 +60,17 @@ void veryLongSelectStatementsAreOk() { assertThat(result).isEqualTo(expected); } + @ParameterizedTest + @ArgumentsSource(DdlArgs.class) + void checkDdlOperationStatementsAreOk( + String actual, Function expectFunc) { + SqlStatementInfo result = SqlStatementSanitizer.create(true).sanitize(actual); + SqlStatementInfo expected = expectFunc.apply(actual); + assertThat(result.getFullStatement()).isEqualTo(expected.getFullStatement()); + assertThat(result.getOperation()).isEqualTo(expected.getOperation()); + assertThat(result.getMainIdentifier()).isEqualTo(expected.getMainIdentifier()); + } + @Test void lotsOfTicksDontCauseStackOverflowOrLongRuntimes() { String s = "'"; @@ -111,10 +124,23 @@ void randomBytesDontCauseExceptionsOrTimeouts() { } } + @Test + public void longInStatementDoesntCauseStackOverflow() { + StringBuilder s = new StringBuilder("select col from table where col in ("); + for (int i = 0; i < 10000; i++) { + s.append("?,"); + } + s.append("?)"); + + String sanitized = SqlStatementSanitizer.create(true).sanitize(s.toString()).getFullStatement(); + + assertThat(sanitized).isEqualTo("select col from table where col in (?)"); + } + static class SqlArgs implements ArgumentsProvider { @Override - public Stream provideArguments(ExtensionContext context) throws Exception { + public Stream provideArguments(ExtensionContext context) { return Stream.of( Arguments.of("SELECT * FROM TABLE WHERE FIELD=1234", "SELECT * FROM TABLE WHERE FIELD=?"), Arguments.of( @@ -188,6 +214,10 @@ public Stream provideArguments(ExtensionContext context) th Arguments.of( "SELECT * FROM TABLE WHERE FIELD = $$\\\\$$", "SELECT * FROM TABLE WHERE FIELD = ?"), + // PostgreSQL native parameter marker, we want to keep $1 instead of replacing it with ? + Arguments.of( + "SELECT * FROM TABLE WHERE FIELD = $1", "SELECT * FROM TABLE WHERE FIELD = $1"), + // Unicode, including a unicode identifier with a trailing number Arguments.of( "SELECT * FROM TABLEओ7 WHERE FIELD = 'ɣ'", "SELECT * FROM TABLEओ7 WHERE FIELD = ?"), @@ -205,7 +235,7 @@ public Stream provideArguments(ExtensionContext context) th static class CouchbaseArgs implements ArgumentsProvider { @Override - public Stream provideArguments(ExtensionContext context) throws Exception { + public Stream provideArguments(ExtensionContext context) { return Stream.of( // Some databases support/encourage " instead of ' with same escape rules Arguments.of( @@ -242,7 +272,7 @@ static Function expect( } @Override - public Stream provideArguments(ExtensionContext context) throws Exception { + public Stream provideArguments(ExtensionContext context) { return Stream.of( // Select Arguments.of("SELECT x, y, z FROM schema.table", expect("SELECT", "schema.table")), @@ -269,7 +299,11 @@ public Stream provideArguments(ExtensionContext context) th Arguments.of("select col from table1 as t1, table2 as t2", expect("SELECT", null)), Arguments.of( "select col from table where col in (1, 2, 3)", - expect("select col from table where col in (?, ?, ?)", "SELECT", "table")), + expect("select col from table where col in (?)", "SELECT", "table")), + Arguments.of( + "select 'a' IN(x, 'b') from table where col in (1) and z IN( '3', '4' )", + expect( + "select ? IN(x, ?) from table where col in (?) and z IN(?)", "SELECT", "table")), Arguments.of("select col from table order by col, col2", expect("SELECT", "table")), Arguments.of("select ąś∂ń© from źćļńĶ order by col, col2", expect("SELECT", "źćļńĶ")), Arguments.of("select 12345678", expect("select ?", "SELECT", null)), @@ -296,6 +330,9 @@ public Stream provideArguments(ExtensionContext context) th "delete from `my table` where something something", expect("DELETE", "my table")), Arguments.of( "delete from \"my table\" where something something", expect("DELETE", "my table")), + Arguments.of( + "delete from foo where x IN (1,2,3)", + expect("delete from foo where x IN (?)", "DELETE", "foo")), Arguments.of("delete from 12345678", expect("delete from ?", "DELETE", null)), Arguments.of("delete (((", expect("delete (((", "DELETE", null)), @@ -305,6 +342,12 @@ public Stream provideArguments(ExtensionContext context) th Arguments.of( "update `my table` set answer=42", expect("update `my table` set answer=?", "UPDATE", "my table")), + Arguments.of( + "update `my table` set answer=42 where x IN('a', 'b') AND y In ('a', 'b')", + expect( + "update `my table` set answer=? where x IN(?) AND y In (?)", + "UPDATE", + "my table")), Arguments.of( "update \"my table\" set answer=42", expect("update \"my table\" set answer=?", "UPDATE", "my table")), @@ -329,4 +372,34 @@ public Stream provideArguments(ExtensionContext context) th Arguments.of(null, expect(null, null))); } } + + static class DdlArgs implements ArgumentsProvider { + + static Function expect(String operation, String identifier) { + return sql -> SqlStatementInfo.create(sql, operation, identifier); + } + + static Function expect( + String sql, String operation, String identifier) { + return ignored -> SqlStatementInfo.create(sql, operation, identifier); + } + + @Override + public Stream provideArguments(ExtensionContext context) { + return Stream.of( + Arguments.of("CREATE TABLE `table`", expect("CREATE TABLE", "table")), + Arguments.of("CREATE TABLE IF NOT EXISTS table", expect("CREATE TABLE", "table")), + Arguments.of("DROP TABLE `if`", expect("DROP TABLE", "if")), + Arguments.of( + "ALTER TABLE table ADD CONSTRAINT c FOREIGN KEY (foreign_id) REFERENCES ref (id)", + expect("ALTER TABLE", "table")), + Arguments.of("CREATE INDEX types_name ON types (name)", expect("CREATE INDEX", null)), + Arguments.of("DROP INDEX types_name ON types (name)", expect("DROP INDEX", null)), + Arguments.of( + "CREATE VIEW tmp AS SELECT type FROM table WHERE id = ?", + expect("CREATE VIEW", null)), + Arguments.of( + "CREATE PROCEDURE p AS SELECT * FROM table GO", expect("CREATE PROCEDURE", null))); + } + } } diff --git a/instrumentation-api-semconv/src/testStableHttpSemconv/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpClientMetricsStableSemconvTest.java b/instrumentation-api-incubator/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv/http/HttpClientExperimentalMetricsTest.java similarity index 58% rename from instrumentation-api-semconv/src/testStableHttpSemconv/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpClientMetricsStableSemconvTest.java rename to instrumentation-api-incubator/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv/http/HttpClientExperimentalMetricsTest.java index 97e9f3a26af9..b15b58f71df7 100644 --- a/instrumentation-api-semconv/src/testStableHttpSemconv/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpClientMetricsStableSemconvTest.java +++ b/instrumentation-api-incubator/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv/http/HttpClientExperimentalMetricsTest.java @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.instrumentation.api.instrumenter.http; +package io.opentelemetry.instrumentation.api.incubator.semconv.http; import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; @@ -15,17 +15,18 @@ import io.opentelemetry.api.trace.TraceState; import io.opentelemetry.context.Context; import io.opentelemetry.instrumentation.api.instrumenter.OperationListener; -import io.opentelemetry.instrumentation.api.instrumenter.network.internal.NetworkAttributes; -import io.opentelemetry.instrumentation.api.instrumenter.url.internal.UrlAttributes; import io.opentelemetry.sdk.metrics.SdkMeterProvider; import io.opentelemetry.sdk.testing.exporter.InMemoryMetricReader; +import io.opentelemetry.semconv.ErrorAttributes; +import io.opentelemetry.semconv.HttpAttributes; +import io.opentelemetry.semconv.NetworkAttributes; +import io.opentelemetry.semconv.ServerAttributes; +import io.opentelemetry.semconv.UrlAttributes; +import io.opentelemetry.semconv.incubating.HttpIncubatingAttributes; import java.util.concurrent.TimeUnit; import org.junit.jupiter.api.Test; -class HttpClientMetricsStableSemconvTest { - - static final double[] DURATION_BUCKETS = - HttpMetricsUtil.DURATION_SECONDS_BUCKETS.stream().mapToDouble(d -> d).toArray(); +class HttpClientExperimentalMetricsTest { @Test void collectsMetrics() { @@ -33,29 +34,29 @@ void collectsMetrics() { SdkMeterProvider meterProvider = SdkMeterProvider.builder().registerMetricReader(metricReader).build(); - OperationListener listener = HttpClientMetrics.get().create(meterProvider.get("test")); + OperationListener listener = + HttpClientExperimentalMetrics.get().create(meterProvider.get("test")); Attributes requestAttributes = Attributes.builder() .put(HttpAttributes.HTTP_REQUEST_METHOD, "GET") .put(UrlAttributes.URL_FULL, "https://localhost:1234/") - .put(UrlAttributes.URL_SCHEME, "https") .put(UrlAttributes.URL_PATH, "/") .put(UrlAttributes.URL_QUERY, "q=a") - .put(NetworkAttributes.SERVER_ADDRESS, "localhost") - .put(NetworkAttributes.SERVER_PORT, 1234) + .put(ServerAttributes.SERVER_ADDRESS, "localhost") + .put(ServerAttributes.SERVER_PORT, 1234) .build(); Attributes responseAttributes = Attributes.builder() .put(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, 200) - .put(HttpAttributes.HTTP_REQUEST_BODY_SIZE, 100) - .put(HttpAttributes.HTTP_RESPONSE_BODY_SIZE, 200) + .put(ErrorAttributes.ERROR_TYPE, "400") + .put(HttpIncubatingAttributes.HTTP_REQUEST_BODY_SIZE, 100) + .put(HttpIncubatingAttributes.HTTP_RESPONSE_BODY_SIZE, 200) .put(NetworkAttributes.NETWORK_PROTOCOL_NAME, "http") .put(NetworkAttributes.NETWORK_PROTOCOL_VERSION, "2.0") - .put(NetworkAttributes.SERVER_SOCKET_ADDRESS, "1.2.3.4") - .put(NetworkAttributes.SERVER_SOCKET_DOMAIN, "somehost20") - .put(NetworkAttributes.SERVER_SOCKET_PORT, 8080) + .put(NetworkAttributes.NETWORK_PEER_ADDRESS, "1.2.3.4") + .put(NetworkAttributes.NETWORK_PEER_PORT, 8080) .build(); Context parent = @@ -82,35 +83,9 @@ void collectsMetrics() { .satisfiesExactlyInAnyOrder( metric -> assertThat(metric) - .hasName("http.client.duration") - .hasUnit("s") - .hasHistogramSatisfying( - histogram -> - histogram.hasPointsSatisfying( - point -> - point - .hasSum(0.15 /* seconds */) - .hasAttributesSatisfying( - equalTo(HttpAttributes.HTTP_REQUEST_METHOD, "GET"), - equalTo(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, 200), - equalTo( - NetworkAttributes.NETWORK_PROTOCOL_NAME, "http"), - equalTo( - NetworkAttributes.NETWORK_PROTOCOL_VERSION, "2.0"), - equalTo(NetworkAttributes.SERVER_ADDRESS, "localhost"), - equalTo(NetworkAttributes.SERVER_PORT, 1234), - equalTo( - NetworkAttributes.SERVER_SOCKET_ADDRESS, "1.2.3.4")) - .hasExemplarsSatisfying( - exemplar -> - exemplar - .hasTraceId("ff01020304050600ff0a0b0c0d0e0f00") - .hasSpanId("090a0b0c0d0e0f00")) - .hasBucketBoundaries(DURATION_BUCKETS))), - metric -> - assertThat(metric) - .hasName("http.client.request.size") + .hasName("http.client.request.body.size") .hasUnit("By") + .hasDescription("Size of HTTP client request bodies.") .hasHistogramSatisfying( histogram -> histogram.hasPointsSatisfying( @@ -120,14 +95,13 @@ void collectsMetrics() { .hasAttributesSatisfying( equalTo(HttpAttributes.HTTP_REQUEST_METHOD, "GET"), equalTo(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, 200), + equalTo(ErrorAttributes.ERROR_TYPE, "400"), equalTo( NetworkAttributes.NETWORK_PROTOCOL_NAME, "http"), equalTo( NetworkAttributes.NETWORK_PROTOCOL_VERSION, "2.0"), - equalTo(NetworkAttributes.SERVER_ADDRESS, "localhost"), - equalTo(NetworkAttributes.SERVER_PORT, 1234), - equalTo( - NetworkAttributes.SERVER_SOCKET_ADDRESS, "1.2.3.4")) + equalTo(ServerAttributes.SERVER_ADDRESS, "localhost"), + equalTo(ServerAttributes.SERVER_PORT, 1234)) .hasExemplarsSatisfying( exemplar -> exemplar @@ -135,8 +109,9 @@ void collectsMetrics() { .hasSpanId("090a0b0c0d0e0f00")))), metric -> assertThat(metric) - .hasName("http.client.response.size") + .hasName("http.client.response.body.size") .hasUnit("By") + .hasDescription("Size of HTTP client response bodies.") .hasHistogramSatisfying( histogram -> histogram.hasPointsSatisfying( @@ -146,14 +121,13 @@ void collectsMetrics() { .hasAttributesSatisfying( equalTo(HttpAttributes.HTTP_REQUEST_METHOD, "GET"), equalTo(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, 200), + equalTo(ErrorAttributes.ERROR_TYPE, "400"), equalTo( NetworkAttributes.NETWORK_PROTOCOL_NAME, "http"), equalTo( NetworkAttributes.NETWORK_PROTOCOL_VERSION, "2.0"), - equalTo(NetworkAttributes.SERVER_ADDRESS, "localhost"), - equalTo(NetworkAttributes.SERVER_PORT, 1234), - equalTo( - NetworkAttributes.SERVER_SOCKET_ADDRESS, "1.2.3.4")) + equalTo(ServerAttributes.SERVER_ADDRESS, "localhost"), + equalTo(ServerAttributes.SERVER_PORT, 1234)) .hasExemplarsSatisfying( exemplar -> exemplar @@ -166,20 +140,13 @@ void collectsMetrics() { .satisfiesExactlyInAnyOrder( metric -> assertThat(metric) - .hasName("http.client.duration") - .hasHistogramSatisfying( - histogram -> - histogram.hasPointsSatisfying( - point -> point.hasSum(0.3 /* seconds */))), - metric -> - assertThat(metric) - .hasName("http.client.request.size") + .hasName("http.client.request.body.size") .hasHistogramSatisfying( histogram -> histogram.hasPointsSatisfying(point -> point.hasSum(200 /* bytes */))), metric -> assertThat(metric) - .hasName("http.client.response.size") + .hasName("http.client.response.body.size") .hasHistogramSatisfying( histogram -> histogram.hasPointsSatisfying(point -> point.hasSum(400 /* bytes */)))); diff --git a/instrumentation-api-incubator/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv/http/HttpClientPeerServiceAttributesExtractorTest.java b/instrumentation-api-incubator/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv/http/HttpClientPeerServiceAttributesExtractorTest.java new file mode 100644 index 000000000000..c450a8add835 --- /dev/null +++ b/instrumentation-api-incubator/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv/http/HttpClientPeerServiceAttributesExtractorTest.java @@ -0,0 +1,106 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.incubator.semconv.http; + +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.Mockito.when; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.incubator.semconv.net.PeerServiceResolver; +import io.opentelemetry.instrumentation.api.semconv.http.HttpClientAttributesGetter; +import io.opentelemetry.semconv.incubating.PeerIncubatingAttributes; +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 HttpClientPeerServiceAttributesExtractorTest { + @Mock HttpClientAttributesGetter httpAttributesExtractor; + + @Test + void shouldNotSetAnyValueIfNetExtractorReturnsNulls() { + // given + PeerServiceResolver peerServiceResolver = + PeerServiceResolver.create(singletonMap("1.2.3.4", "myService")); + + HttpClientPeerServiceAttributesExtractor underTest = + new HttpClientPeerServiceAttributesExtractor<>( + httpAttributesExtractor, peerServiceResolver); + + Context context = Context.root(); + + // when + AttributesBuilder attributes = Attributes.builder(); + underTest.onStart(attributes, context, "request"); + underTest.onEnd(attributes, context, "request", "response", null); + + // then + assertTrue(attributes.build().isEmpty()); + } + + @Test + void shouldNotSetAnyValueIfPeerNameDoesNotMatch() { + // given + PeerServiceResolver peerServiceResolver = + PeerServiceResolver.create(singletonMap("example.com", "myService")); + + HttpClientPeerServiceAttributesExtractor underTest = + new HttpClientPeerServiceAttributesExtractor<>( + httpAttributesExtractor, peerServiceResolver); + + when(httpAttributesExtractor.getServerAddress(any())).thenReturn("example2.com"); + + Context context = Context.root(); + + // when + AttributesBuilder startAttributes = Attributes.builder(); + underTest.onStart(startAttributes, context, "request"); + AttributesBuilder endAttributes = Attributes.builder(); + underTest.onEnd(endAttributes, context, "request", "response", null); + + // 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"); + + PeerServiceResolver peerServiceResolver = PeerServiceResolver.create(peerServiceMapping); + + HttpClientPeerServiceAttributesExtractor underTest = + new HttpClientPeerServiceAttributesExtractor<>( + httpAttributesExtractor, peerServiceResolver); + + when(httpAttributesExtractor.getServerAddress(any())).thenReturn("example.com"); + + Context context = Context.root(); + + // when + AttributesBuilder startAttributes = Attributes.builder(); + underTest.onStart(startAttributes, context, "request"); + AttributesBuilder endAttributes = Attributes.builder(); + underTest.onEnd(endAttributes, context, "request", "response", null); + + // then + assertThat(startAttributes.build()).isEmpty(); + assertThat(endAttributes.build()) + .containsOnly(entry(PeerIncubatingAttributes.PEER_SERVICE, "myService")); + } +} diff --git a/instrumentation-api-incubator/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv/http/HttpExperimentalAttributesExtractorTest.java b/instrumentation-api-incubator/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv/http/HttpExperimentalAttributesExtractorTest.java new file mode 100644 index 000000000000..5a9fdf112ded --- /dev/null +++ b/instrumentation-api-incubator/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv/http/HttpExperimentalAttributesExtractorTest.java @@ -0,0 +1,60 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.incubator.semconv.http; + +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.entry; +import static org.mockito.Mockito.when; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; +import io.opentelemetry.instrumentation.api.semconv.http.HttpClientAttributesGetter; +import io.opentelemetry.instrumentation.api.semconv.http.HttpCommonAttributesGetter; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerAttributesGetter; +import io.opentelemetry.semconv.incubating.HttpIncubatingAttributes; +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 HttpExperimentalAttributesExtractorTest { + + @Mock HttpClientAttributesGetter clientGetter; + @Mock HttpServerAttributesGetter serverGetter; + + @Test + void shouldExtractRequestAndResponseSizes_client() { + runTest(clientGetter, HttpExperimentalAttributesExtractor.create(clientGetter)); + } + + @Test + void shouldExtractRequestAndResponseSizes_server() { + runTest(serverGetter, HttpExperimentalAttributesExtractor.create(serverGetter)); + } + + void runTest( + HttpCommonAttributesGetter getter, + AttributesExtractor extractor) { + + when(getter.getHttpRequestHeader("request", "content-length")).thenReturn(singletonList("123")); + when(getter.getHttpResponseHeader("request", "response", "content-length")) + .thenReturn(singletonList("42")); + + AttributesBuilder attributes = Attributes.builder(); + extractor.onStart(attributes, Context.root(), "request"); + assertThat(attributes.build()).isEmpty(); + + extractor.onEnd(attributes, Context.root(), "request", "response", null); + assertThat(attributes.build()) + .containsOnly( + entry(HttpIncubatingAttributes.HTTP_REQUEST_BODY_SIZE, 123L), + entry(HttpIncubatingAttributes.HTTP_RESPONSE_BODY_SIZE, 42L)); + } +} diff --git a/instrumentation-api-semconv/src/testStableHttpSemconv/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpServerMetricsStableSemconvTest.java b/instrumentation-api-incubator/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv/http/HttpServerExperimentalMetricsTest.java similarity index 61% rename from instrumentation-api-semconv/src/testStableHttpSemconv/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpServerMetricsStableSemconvTest.java rename to instrumentation-api-incubator/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv/http/HttpServerExperimentalMetricsTest.java index 4bdb7fe7e10f..13796304b236 100644 --- a/instrumentation-api-semconv/src/testStableHttpSemconv/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpServerMetricsStableSemconvTest.java +++ b/instrumentation-api-incubator/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv/http/HttpServerExperimentalMetricsTest.java @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.instrumentation.api.instrumenter.http; +package io.opentelemetry.instrumentation.api.incubator.semconv.http; import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; @@ -15,18 +15,18 @@ import io.opentelemetry.api.trace.TraceState; import io.opentelemetry.context.Context; import io.opentelemetry.instrumentation.api.instrumenter.OperationListener; -import io.opentelemetry.instrumentation.api.instrumenter.network.internal.NetworkAttributes; -import io.opentelemetry.instrumentation.api.instrumenter.url.internal.UrlAttributes; import io.opentelemetry.sdk.metrics.SdkMeterProvider; import io.opentelemetry.sdk.testing.exporter.InMemoryMetricReader; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.opentelemetry.semconv.ErrorAttributes; +import io.opentelemetry.semconv.HttpAttributes; +import io.opentelemetry.semconv.NetworkAttributes; +import io.opentelemetry.semconv.ServerAttributes; +import io.opentelemetry.semconv.UrlAttributes; +import io.opentelemetry.semconv.incubating.HttpIncubatingAttributes; import java.util.concurrent.TimeUnit; import org.junit.jupiter.api.Test; -class HttpServerMetricsStableSemconvTest { - - static final double[] DURATION_BUCKETS = - HttpMetricsUtil.DURATION_SECONDS_BUCKETS.stream().mapToDouble(d -> d).toArray(); +class HttpServerExperimentalMetricsTest { @Test void collectsMetrics() { @@ -34,7 +34,8 @@ void collectsMetrics() { SdkMeterProvider meterProvider = SdkMeterProvider.builder().registerMetricReader(metricReader).build(); - OperationListener listener = HttpServerMetrics.get().create(meterProvider.get("test")); + OperationListener listener = + HttpServerExperimentalMetrics.get().create(meterProvider.get("test")); Attributes requestAttributes = Attributes.builder() @@ -46,19 +47,20 @@ void collectsMetrics() { .put(NetworkAttributes.NETWORK_TYPE, "ipv4") .put(NetworkAttributes.NETWORK_PROTOCOL_NAME, "http") .put(NetworkAttributes.NETWORK_PROTOCOL_VERSION, "2.0") - .put(NetworkAttributes.SERVER_ADDRESS, "localhost") - .put(NetworkAttributes.SERVER_PORT, 1234) + .put(ServerAttributes.SERVER_ADDRESS, "localhost") + .put(ServerAttributes.SERVER_PORT, 1234) .build(); Attributes responseAttributes = Attributes.builder() .put(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, 200) - .put(HttpAttributes.HTTP_REQUEST_BODY_SIZE, 100) - .put(HttpAttributes.HTTP_RESPONSE_BODY_SIZE, 200) - .put(NetworkAttributes.CLIENT_SOCKET_ADDRESS, "1.2.3.4") - .put(NetworkAttributes.CLIENT_SOCKET_PORT, 8080) - .put(NetworkAttributes.SERVER_SOCKET_ADDRESS, "4.3.2.1") - .put(NetworkAttributes.SERVER_SOCKET_PORT, 9090) + .put(ErrorAttributes.ERROR_TYPE, "500") + .put(HttpIncubatingAttributes.HTTP_REQUEST_BODY_SIZE, 100) + .put(HttpIncubatingAttributes.HTTP_RESPONSE_BODY_SIZE, 200) + .put(NetworkAttributes.NETWORK_PEER_ADDRESS, "1.2.3.4") + .put(NetworkAttributes.NETWORK_PEER_PORT, 8080) + .put(NetworkAttributes.NETWORK_LOCAL_ADDRESS, "4.3.2.1") + .put(NetworkAttributes.NETWORK_LOCAL_PORT, 9090) .build(); SpanContext spanContext1 = @@ -82,9 +84,8 @@ void collectsMetrics() { metric -> assertThat(metric) .hasName("http.server.active_requests") - .hasDescription( - "The number of concurrent HTTP requests that are currently in-flight") .hasUnit("{requests}") + .hasDescription("Number of active HTTP server requests.") .hasLongSumSatisfying( sum -> sum.hasPointsSatisfying( @@ -93,9 +94,7 @@ void collectsMetrics() { .hasValue(1) .hasAttributesSatisfying( equalTo(HttpAttributes.HTTP_REQUEST_METHOD, "GET"), - equalTo(UrlAttributes.URL_SCHEME, "https"), - equalTo(NetworkAttributes.SERVER_ADDRESS, "localhost"), - equalTo(NetworkAttributes.SERVER_PORT, 1234L)) + equalTo(UrlAttributes.URL_SCHEME, "https")) .hasExemplarsSatisfying( exemplar -> exemplar @@ -118,9 +117,7 @@ void collectsMetrics() { .hasValue(2) .hasAttributesSatisfying( equalTo(HttpAttributes.HTTP_REQUEST_METHOD, "GET"), - equalTo(UrlAttributes.URL_SCHEME, "https"), - equalTo(NetworkAttributes.SERVER_ADDRESS, "localhost"), - equalTo(NetworkAttributes.SERVER_PORT, 1234L)) + equalTo(UrlAttributes.URL_SCHEME, "https")) .hasExemplarsSatisfying( exemplar -> exemplar @@ -142,9 +139,7 @@ void collectsMetrics() { .hasValue(1) .hasAttributesSatisfying( equalTo(HttpAttributes.HTTP_REQUEST_METHOD, "GET"), - equalTo(UrlAttributes.URL_SCHEME, "https"), - equalTo(NetworkAttributes.SERVER_ADDRESS, "localhost"), - equalTo(NetworkAttributes.SERVER_PORT, 1234L)) + equalTo(UrlAttributes.URL_SCHEME, "https")) .hasExemplarsSatisfying( exemplar -> exemplar @@ -152,34 +147,9 @@ void collectsMetrics() { .hasSpanId(spanContext1.getSpanId())))), metric -> assertThat(metric) - .hasName("http.server.duration") - .hasUnit("s") - .hasHistogramSatisfying( - histogram -> - histogram.hasPointsSatisfying( - point -> - point - .hasSum(0.15 /* seconds */) - .hasAttributesSatisfying( - equalTo(HttpAttributes.HTTP_REQUEST_METHOD, "GET"), - equalTo(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, 200), - equalTo( - NetworkAttributes.NETWORK_PROTOCOL_NAME, "http"), - equalTo( - NetworkAttributes.NETWORK_PROTOCOL_VERSION, "2.0"), - equalTo(UrlAttributes.URL_SCHEME, "https"), - equalTo(NetworkAttributes.SERVER_ADDRESS, "localhost"), - equalTo(NetworkAttributes.SERVER_PORT, 1234L)) - .hasExemplarsSatisfying( - exemplar -> - exemplar - .hasTraceId(spanContext1.getTraceId()) - .hasSpanId(spanContext1.getSpanId())) - .hasBucketBoundaries(DURATION_BUCKETS))), - metric -> - assertThat(metric) - .hasName("http.server.request.size") + .hasName("http.server.request.body.size") .hasUnit("By") + .hasDescription("Size of HTTP server request bodies.") .hasHistogramSatisfying( histogram -> histogram.hasPointsSatisfying( @@ -189,13 +159,12 @@ void collectsMetrics() { .hasAttributesSatisfying( equalTo(HttpAttributes.HTTP_REQUEST_METHOD, "GET"), equalTo(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, 200), + equalTo(ErrorAttributes.ERROR_TYPE, "500"), equalTo( NetworkAttributes.NETWORK_PROTOCOL_NAME, "http"), equalTo( NetworkAttributes.NETWORK_PROTOCOL_VERSION, "2.0"), - equalTo(UrlAttributes.URL_SCHEME, "https"), - equalTo(NetworkAttributes.SERVER_ADDRESS, "localhost"), - equalTo(NetworkAttributes.SERVER_PORT, 1234L)) + equalTo(UrlAttributes.URL_SCHEME, "https")) .hasExemplarsSatisfying( exemplar -> exemplar @@ -203,8 +172,9 @@ void collectsMetrics() { .hasSpanId(spanContext1.getSpanId())))), metric -> assertThat(metric) - .hasName("http.server.response.size") + .hasName("http.server.response.body.size") .hasUnit("By") + .hasDescription("Size of HTTP server response bodies.") .hasHistogramSatisfying( histogram -> histogram.hasPointsSatisfying( @@ -214,13 +184,12 @@ void collectsMetrics() { .hasAttributesSatisfying( equalTo(HttpAttributes.HTTP_REQUEST_METHOD, "GET"), equalTo(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, 200), + equalTo(ErrorAttributes.ERROR_TYPE, "500"), equalTo( NetworkAttributes.NETWORK_PROTOCOL_NAME, "http"), equalTo( NetworkAttributes.NETWORK_PROTOCOL_VERSION, "2.0"), - equalTo(UrlAttributes.URL_SCHEME, "https"), - equalTo(NetworkAttributes.SERVER_ADDRESS, "localhost"), - equalTo(NetworkAttributes.SERVER_PORT, 1234L)) + equalTo(UrlAttributes.URL_SCHEME, "https")) .hasExemplarsSatisfying( exemplar -> exemplar @@ -247,21 +216,7 @@ void collectsMetrics() { .hasSpanId(spanContext2.getSpanId())))), metric -> assertThat(metric) - .hasName("http.server.duration") - .hasHistogramSatisfying( - histogram -> - histogram.hasPointsSatisfying( - point -> - point - .hasSum(0.3 /* seconds */) - .hasExemplarsSatisfying( - exemplar -> - exemplar - .hasTraceId(spanContext2.getTraceId()) - .hasSpanId(spanContext2.getSpanId())))), - metric -> - assertThat(metric) - .hasName("http.server.request.size") + .hasName("http.server.request.body.size") .hasHistogramSatisfying( histogram -> histogram.hasPointsSatisfying( @@ -275,7 +230,7 @@ void collectsMetrics() { .hasSpanId(spanContext2.getSpanId())))), metric -> assertThat(metric) - .hasName("http.server.response.size") + .hasName("http.server.response.body.size") .hasHistogramSatisfying( histogram -> histogram.hasPointsSatisfying( @@ -289,50 +244,6 @@ void collectsMetrics() { .hasSpanId(spanContext2.getSpanId()))))); } - @Test - void collectsHttpRouteFromEndAttributes() { - // given - InMemoryMetricReader metricReader = InMemoryMetricReader.create(); - SdkMeterProvider meterProvider = - SdkMeterProvider.builder().registerMetricReader(metricReader).build(); - - OperationListener listener = HttpServerMetrics.get().create(meterProvider.get("test")); - - Attributes requestAttributes = - Attributes.builder() - .put(NetworkAttributes.SERVER_ADDRESS, "host") - .put(UrlAttributes.URL_SCHEME, "https") - .build(); - - Attributes responseAttributes = - Attributes.builder().put(SemanticAttributes.HTTP_ROUTE, "/test/{id}").build(); - - Context parentContext = Context.root(); - - // when - Context context = listener.onStart(parentContext, requestAttributes, nanos(100)); - listener.onEnd(context, responseAttributes, nanos(200)); - - // then - assertThat(metricReader.collectAllMetrics()) - .anySatisfy( - metric -> - assertThat(metric) - .hasName("http.server.duration") - .hasUnit("s") - .hasHistogramSatisfying( - histogram -> - histogram.hasPointsSatisfying( - point -> - point - .hasSum(0.100 /* seconds */) - .hasAttributesSatisfying( - equalTo(UrlAttributes.URL_SCHEME, "https"), - equalTo(NetworkAttributes.SERVER_ADDRESS, "host"), - equalTo( - SemanticAttributes.HTTP_ROUTE, "/test/{id}"))))); - } - private static long nanos(int millis) { return TimeUnit.MILLISECONDS.toNanos(millis); } diff --git a/instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/messaging/MessagingAttributesExtractorTest.java b/instrumentation-api-incubator/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv/messaging/MessagingAttributesExtractorTest.java similarity index 58% rename from instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/messaging/MessagingAttributesExtractorTest.java rename to instrumentation-api-incubator/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv/messaging/MessagingAttributesExtractorTest.java index d98fde90fb06..395f52fdfdb7 100644 --- a/instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/messaging/MessagingAttributesExtractorTest.java +++ b/instrumentation-api-incubator/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv/messaging/MessagingAttributesExtractorTest.java @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.instrumentation.api.instrumenter.messaging; +package io.opentelemetry.instrumentation.api.incubator.semconv.messaging; import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; import static org.assertj.core.api.Assertions.entry; @@ -13,13 +13,14 @@ import io.opentelemetry.api.common.AttributesBuilder; import io.opentelemetry.context.Context; import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes; 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 javax.annotation.Nullable; import org.assertj.core.data.MapEntry; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -32,6 +33,7 @@ class MessagingAttributesExtractorTest { @MethodSource("destinations") void shouldExtractAllAvailableAttributes( boolean temporary, + boolean anonymous, String destination, MessageOperation operation, String expectedDestination) { @@ -40,13 +42,19 @@ void shouldExtractAllAvailableAttributes( request.put("system", "myQueue"); request.put("destinationKind", "topic"); request.put("destination", destination); + request.put("destinationTemplate", destination); if (temporary) { request.put("temporaryDestination", "y"); } + if (anonymous) { + request.put("anonymousDestination", "y"); + } request.put("url", "http://broker/topic"); request.put("conversationId", "42"); - request.put("payloadSize", "100"); - request.put("payloadCompressedSize", "10"); + request.put("bodySize", "100"); + request.put("envelopeSize", "120"); + request.put("clientId", "43"); + request.put("batchMessageCount", "2"); AttributesExtractor, String> underTest = MessagingAttributesExtractor.create(TestGetter.INSTANCE, operation); @@ -62,16 +70,27 @@ void shouldExtractAllAvailableAttributes( // then List, Object>> expectedEntries = new ArrayList<>(); - expectedEntries.add(entry(SemanticAttributes.MESSAGING_SYSTEM, "myQueue")); - expectedEntries.add(entry(SemanticAttributes.MESSAGING_DESTINATION_NAME, expectedDestination)); + expectedEntries.add(entry(MessagingIncubatingAttributes.MESSAGING_SYSTEM, "myQueue")); + expectedEntries.add( + entry(MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME, expectedDestination)); if (temporary) { - expectedEntries.add(entry(SemanticAttributes.MESSAGING_DESTINATION_TEMPORARY, true)); + expectedEntries.add( + entry(MessagingIncubatingAttributes.MESSAGING_DESTINATION_TEMPORARY, true)); + } else { + expectedEntries.add( + entry(MessagingIncubatingAttributes.MESSAGING_DESTINATION_TEMPLATE, expectedDestination)); + } + if (anonymous) { + expectedEntries.add( + entry(MessagingIncubatingAttributes.MESSAGING_DESTINATION_ANONYMOUS, true)); } - expectedEntries.add(entry(SemanticAttributes.MESSAGING_MESSAGE_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())); + entry(MessagingIncubatingAttributes.MESSAGING_MESSAGE_CONVERSATION_ID, "42")); + expectedEntries.add(entry(MessagingIncubatingAttributes.MESSAGING_MESSAGE_BODY_SIZE, 100L)); + expectedEntries.add(entry(MessagingIncubatingAttributes.MESSAGING_MESSAGE_ENVELOPE_SIZE, 120L)); + expectedEntries.add(entry(MessagingIncubatingAttributes.MESSAGING_CLIENT_ID, "43")); + expectedEntries.add( + entry(MessagingIncubatingAttributes.MESSAGING_OPERATION, operation.operationName())); @SuppressWarnings({"unchecked", "rawtypes"}) MapEntry, ?>[] expectedEntriesArr = @@ -79,20 +98,22 @@ void shouldExtractAllAvailableAttributes( assertThat(startAttributes.build()).containsOnly(expectedEntriesArr); assertThat(endAttributes.build()) - .containsOnly(entry(SemanticAttributes.MESSAGING_MESSAGE_ID, "42")); + .containsOnly( + entry(MessagingIncubatingAttributes.MESSAGING_MESSAGE_ID, "42"), + entry(MessagingIncubatingAttributes.MESSAGING_BATCH_MESSAGE_COUNT, 2L)); } static Stream destinations() { return Stream.of( - Arguments.of(false, "destination", MessageOperation.RECEIVE, "destination"), - Arguments.of(true, null, MessageOperation.PROCESS, "(temporary)")); + Arguments.of(false, false, "destination", MessageOperation.RECEIVE, "destination"), + Arguments.of(true, true, null, MessageOperation.PROCESS, "(temporary)")); } @Test void shouldExtractNoAttributesIfNoneAreAvailable() { // given AttributesExtractor, String> underTest = - MessagingAttributesExtractor.create(TestGetter.INSTANCE, MessageOperation.SEND); + MessagingAttributesExtractor.create(TestGetter.INSTANCE, null); Context context = Context.root(); @@ -122,25 +143,38 @@ public String getDestination(Map request) { return request.get("destination"); } + @Nullable + @Override + public String getDestinationTemplate(Map request) { + return request.get("destinationTemplate"); + } + @Override public boolean isTemporaryDestination(Map request) { return request.containsKey("temporaryDestination"); } + @Override + public boolean isAnonymousDestination(Map request) { + return request.containsKey("anonymousDestination"); + } + @Override public String getConversationId(Map request) { return request.get("conversationId"); } + @Nullable @Override - public Long getMessagePayloadSize(Map request) { - String payloadSize = request.get("payloadSize"); + public Long getMessageBodySize(Map request) { + String payloadSize = request.get("bodySize"); return payloadSize == null ? null : Long.valueOf(payloadSize); } + @Nullable @Override - public Long getMessagePayloadCompressedSize(Map request) { - String payloadSize = request.get("payloadCompressedSize"); + public Long getMessageEnvelopeSize(Map request) { + String payloadSize = request.get("envelopeSize"); return payloadSize == null ? null : Long.valueOf(payloadSize); } @@ -148,5 +182,18 @@ public Long getMessagePayloadCompressedSize(Map request) { public String getMessageId(Map request, String response) { return response; } + + @Nullable + @Override + public String getClientId(Map request) { + return request.get("clientId"); + } + + @Nullable + @Override + public Long getBatchMessageCount(Map request, @Nullable String response) { + String payloadSize = request.get("batchMessageCount"); + return payloadSize == null ? null : Long.valueOf(payloadSize); + } } } diff --git a/instrumentation-api-incubator/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv/messaging/MessagingProducerMetricsTest.java b/instrumentation-api-incubator/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv/messaging/MessagingProducerMetricsTest.java new file mode 100644 index 000000000000..7bf3c3670d4c --- /dev/null +++ b/instrumentation-api-incubator/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv/messaging/MessagingProducerMetricsTest.java @@ -0,0 +1,125 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.incubator.semconv.messaging; + +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; + +import io.opentelemetry.api.common.Attributes; +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.instrumentation.api.instrumenter.OperationListener; +import io.opentelemetry.sdk.metrics.SdkMeterProvider; +import io.opentelemetry.sdk.testing.exporter.InMemoryMetricReader; +import io.opentelemetry.semconv.ServerAttributes; +import io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes; +import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.Test; + +class MessagingProducerMetricsTest { + + private static final double[] DURATION_BUCKETS = + MessagingMetricsAdvice.DURATION_SECONDS_BUCKETS.stream().mapToDouble(d -> d).toArray(); + + @Test + void collectsMetrics() { + InMemoryMetricReader metricReader = InMemoryMetricReader.create(); + SdkMeterProvider meterProvider = + SdkMeterProvider.builder().registerMetricReader(metricReader).build(); + + OperationListener listener = MessagingProducerMetrics.get().create(meterProvider.get("test")); + + Attributes requestAttributes = + Attributes.builder() + .put(MessagingIncubatingAttributes.MESSAGING_SYSTEM, "pulsar") + .put( + MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME, + "persistent://public/default/topic") + .put(MessagingIncubatingAttributes.MESSAGING_OPERATION, "publish") + .put(ServerAttributes.SERVER_PORT, 6650) + .put(ServerAttributes.SERVER_ADDRESS, "localhost") + .build(); + + Attributes responseAttributes = + Attributes.builder() + .put(MessagingIncubatingAttributes.MESSAGING_MESSAGE_ID, "1:1:0:0") + .put(MessagingIncubatingAttributes.MESSAGING_DESTINATION_PARTITION_ID, "1") + .build(); + + Context parent = + Context.root() + .with( + Span.wrap( + SpanContext.create( + "ff01020304050600ff0a0b0c0d0e0f00", + "090a0b0c0d0e0f00", + TraceFlags.getSampled(), + TraceState.getDefault()))); + + Context context1 = listener.onStart(parent, requestAttributes, nanos(100)); + + assertThat(metricReader.collectAllMetrics()).isEmpty(); + + Context context2 = listener.onStart(Context.root(), requestAttributes, nanos(150)); + + assertThat(metricReader.collectAllMetrics()).isEmpty(); + + listener.onEnd(context1, responseAttributes, nanos(250)); + + assertThat(metricReader.collectAllMetrics()) + .satisfiesExactlyInAnyOrder( + metric -> + assertThat(metric) + .hasName("messaging.publish.duration") + .hasUnit("s") + .hasDescription("Measures the duration of publish operation.") + .hasHistogramSatisfying( + histogram -> + histogram.hasPointsSatisfying( + point -> + point + .hasSum(0.15 /* seconds */) + .hasAttributesSatisfying( + equalTo( + MessagingIncubatingAttributes.MESSAGING_SYSTEM, + "pulsar"), + equalTo( + MessagingIncubatingAttributes + .MESSAGING_DESTINATION_PARTITION_ID, + "1"), + equalTo( + MessagingIncubatingAttributes + .MESSAGING_DESTINATION_NAME, + "persistent://public/default/topic"), + equalTo(ServerAttributes.SERVER_PORT, 6650), + equalTo(ServerAttributes.SERVER_ADDRESS, "localhost")) + .hasExemplarsSatisfying( + exemplar -> + exemplar + .hasTraceId("ff01020304050600ff0a0b0c0d0e0f00") + .hasSpanId("090a0b0c0d0e0f00")) + .hasBucketBoundaries(DURATION_BUCKETS)))); + + listener.onEnd(context2, responseAttributes, nanos(300)); + + assertThat(metricReader.collectAllMetrics()) + .satisfiesExactlyInAnyOrder( + metric -> + assertThat(metric) + .hasName("messaging.publish.duration") + .hasHistogramSatisfying( + histogram -> + histogram.hasPointsSatisfying( + point -> point.hasSum(0.3 /* seconds */)))); + } + + private static long nanos(int millis) { + return TimeUnit.MILLISECONDS.toNanos(millis); + } +} diff --git a/instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/messaging/MessagingSpanNameExtractorTest.java b/instrumentation-api-incubator/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv/messaging/MessagingSpanNameExtractorTest.java similarity index 90% rename from instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/messaging/MessagingSpanNameExtractorTest.java rename to instrumentation-api-incubator/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv/messaging/MessagingSpanNameExtractorTest.java index fb60066816b4..c5f8d6b70c30 100644 --- a/instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/messaging/MessagingSpanNameExtractorTest.java +++ b/instrumentation-api-incubator/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv/messaging/MessagingSpanNameExtractorTest.java @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.instrumentation.api.instrumenter.messaging; +package io.opentelemetry.instrumentation.api.incubator.semconv.messaging; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.Mockito.when; @@ -49,7 +49,7 @@ void shouldExtractSpanName( static Stream spanNameParams() { return Stream.of( - Arguments.of(false, "destination", MessageOperation.SEND, "destination send"), + Arguments.of(false, "destination", MessageOperation.PUBLISH, "destination publish"), Arguments.of(true, null, MessageOperation.PROCESS, "(temporary) process"), Arguments.of(false, null, MessageOperation.RECEIVE, "unknown receive")); } diff --git a/instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/net/PeerServiceAttributesExtractorTest.java b/instrumentation-api-incubator/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv/net/PeerServiceAttributesExtractorTest.java similarity index 62% rename from instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/net/PeerServiceAttributesExtractorTest.java rename to instrumentation-api-incubator/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv/net/PeerServiceAttributesExtractorTest.java index 9ed76b89b598..4c20771af4bd 100644 --- a/instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/net/PeerServiceAttributesExtractorTest.java +++ b/instrumentation-api-incubator/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv/net/PeerServiceAttributesExtractorTest.java @@ -3,22 +3,20 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.instrumentation.api.instrumenter.net; +package io.opentelemetry.instrumentation.api.incubator.semconv.net; 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.Mockito.never; -import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import io.opentelemetry.api.common.Attributes; import io.opentelemetry.api.common.AttributesBuilder; import io.opentelemetry.context.Context; -import io.opentelemetry.instrumentation.api.instrumenter.network.ServerAttributesGetter; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.opentelemetry.instrumentation.api.semconv.network.ServerAttributesGetter; +import io.opentelemetry.semconv.incubating.PeerIncubatingAttributes; import java.util.HashMap; import java.util.Map; import org.junit.jupiter.api.Test; @@ -28,15 +26,16 @@ @ExtendWith(MockitoExtension.class) class PeerServiceAttributesExtractorTest { - @Mock ServerAttributesGetter netAttributesExtractor; + @Mock ServerAttributesGetter netAttributesExtractor; @Test void shouldNotSetAnyValueIfNetExtractorReturnsNulls() { // given - Map peerServiceMapping = singletonMap("1.2.3.4", "myService"); + PeerServiceResolver peerServiceResolver = + PeerServiceResolver.create(singletonMap("1.2.3.4", "myService")); PeerServiceAttributesExtractor underTest = - new PeerServiceAttributesExtractor<>(netAttributesExtractor, peerServiceMapping); + new PeerServiceAttributesExtractor<>(netAttributesExtractor, peerServiceResolver); Context context = Context.root(); @@ -52,10 +51,11 @@ void shouldNotSetAnyValueIfNetExtractorReturnsNulls() { @Test void shouldNotSetAnyValueIfPeerNameDoesNotMatch() { // given - Map peerServiceMapping = singletonMap("example.com", "myService"); + PeerServiceResolver peerServiceResolver = + PeerServiceResolver.create(singletonMap("example.com", "myService")); PeerServiceAttributesExtractor underTest = - new PeerServiceAttributesExtractor<>(netAttributesExtractor, peerServiceMapping); + new PeerServiceAttributesExtractor<>(netAttributesExtractor, peerServiceResolver); when(netAttributesExtractor.getServerAddress(any())).thenReturn("example2.com"); @@ -79,37 +79,12 @@ void shouldSetPeerNameIfItMatches() { peerServiceMapping.put("example.com", "myService"); peerServiceMapping.put("1.2.3.4", "someOtherService"); - PeerServiceAttributesExtractor underTest = - new PeerServiceAttributesExtractor<>(netAttributesExtractor, peerServiceMapping); - - when(netAttributesExtractor.getServerAddress(any())).thenReturn("example.com"); - - Context context = Context.root(); - - // when - AttributesBuilder startAttributes = Attributes.builder(); - underTest.onStart(startAttributes, context, "request"); - AttributesBuilder endAttributes = Attributes.builder(); - underTest.onEnd(endAttributes, context, "request", "response", null); - - // then - assertThat(startAttributes.build()).isEmpty(); - assertThat(endAttributes.build()) - .containsOnly(entry(SemanticAttributes.PEER_SERVICE, "myService")); - verify(netAttributesExtractor, never()).getServerSocketDomain(any(), any()); - } - - @Test - void shouldSetSockPeerNameIfItMatchesAndNoPeerNameProvided() { - // given - Map peerServiceMapping = new HashMap<>(); - peerServiceMapping.put("example.com", "myService"); - peerServiceMapping.put("1.2.3.4", "someOtherService"); + PeerServiceResolver peerServiceResolver = PeerServiceResolver.create(peerServiceMapping); PeerServiceAttributesExtractor underTest = - new PeerServiceAttributesExtractor<>(netAttributesExtractor, peerServiceMapping); + new PeerServiceAttributesExtractor<>(netAttributesExtractor, peerServiceResolver); - when(netAttributesExtractor.getServerSocketDomain(any(), any())).thenReturn("example.com"); + when(netAttributesExtractor.getServerAddress(any())).thenReturn("example.com"); Context context = Context.root(); @@ -122,6 +97,6 @@ void shouldSetSockPeerNameIfItMatchesAndNoPeerNameProvided() { // then assertThat(startAttributes.build()).isEmpty(); assertThat(endAttributes.build()) - .containsOnly(entry(SemanticAttributes.PEER_SERVICE, "myService")); + .containsOnly(entry(PeerIncubatingAttributes.PEER_SERVICE, "myService")); } } diff --git a/instrumentation-api-incubator/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv/net/PeerServiceResolverTest.java b/instrumentation-api-incubator/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv/net/PeerServiceResolverTest.java new file mode 100644 index 000000000000..fd9ef69d1ef2 --- /dev/null +++ b/instrumentation-api-incubator/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv/net/PeerServiceResolverTest.java @@ -0,0 +1,38 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.incubator.semconv.net; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.HashMap; +import java.util.Map; +import org.junit.jupiter.api.Test; + +class PeerServiceResolverTest { + + @Test + void test() { + Map peerServiceMapping = new HashMap<>(); + peerServiceMapping.put("example.com:8080", "myService"); + peerServiceMapping.put("example.com", "myServiceBase"); + peerServiceMapping.put("1.2.3.4", "someOtherService"); + peerServiceMapping.put("1.2.3.4:8080/api", "someOtherService8080"); + peerServiceMapping.put("1.2.3.4/api", "someOtherServiceAPI"); + + PeerServiceResolver peerServiceResolver = PeerServiceResolver.create(peerServiceMapping); + + assertEquals("myServiceBase", peerServiceResolver.resolveService("example.com", null, null)); + assertEquals("myService", peerServiceResolver.resolveService("example.com", 8080, () -> "/")); + assertEquals( + "someOtherService8080", peerServiceResolver.resolveService("1.2.3.4", 8080, () -> "/api")); + assertEquals( + "someOtherService", peerServiceResolver.resolveService("1.2.3.4", 9000, () -> "/api")); + assertEquals( + "someOtherService", peerServiceResolver.resolveService("1.2.3.4", 8080, () -> null)); + assertEquals( + "someOtherServiceAPI", peerServiceResolver.resolveService("1.2.3.4", null, () -> "/api")); + } +} diff --git a/instrumentation-api-incubator/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv/net/internal/UrlParserTest.java b/instrumentation-api-incubator/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv/net/internal/UrlParserTest.java new file mode 100644 index 000000000000..03c00b88e2c9 --- /dev/null +++ b/instrumentation-api-incubator/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv/net/internal/UrlParserTest.java @@ -0,0 +1,249 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.incubator.semconv.net.internal; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +class UrlParserTest { + + @Test + void testGetHost() { + assertThat(UrlParser.getHost("https://localhost")).isEqualTo("localhost"); + assertThat(UrlParser.getHost("https://localhost/")).isEqualTo("localhost"); + + assertThat(UrlParser.getHost("https://localhost?")).isEqualTo("localhost"); + assertThat(UrlParser.getHost("https://localhost/?")).isEqualTo("localhost"); + + assertThat(UrlParser.getHost("https://localhost?query")).isEqualTo("localhost"); + assertThat(UrlParser.getHost("https://localhost/?query")).isEqualTo("localhost"); + + assertThat(UrlParser.getHost("https://localhost#")).isEqualTo("localhost"); + assertThat(UrlParser.getHost("https://localhost/#")).isEqualTo("localhost"); + + assertThat(UrlParser.getHost("https://localhost#fragment")).isEqualTo("localhost"); + assertThat(UrlParser.getHost("https://localhost/#fragment")).isEqualTo("localhost"); + } + + @Test + void testGetHostWithPort() { + assertThat(UrlParser.getHost("https://localhost:8080")).isEqualTo("localhost"); + assertThat(UrlParser.getHost("https://localhost:8080/")).isEqualTo("localhost"); + + assertThat(UrlParser.getHost("https://localhost:8080?")).isEqualTo("localhost"); + assertThat(UrlParser.getHost("https://localhost:8080/?")).isEqualTo("localhost"); + + assertThat(UrlParser.getHost("https://localhost:8080?query")).isEqualTo("localhost"); + assertThat(UrlParser.getHost("https://localhost:8080/?query")).isEqualTo("localhost"); + + assertThat(UrlParser.getHost("https://localhost:8080#")).isEqualTo("localhost"); + assertThat(UrlParser.getHost("https://localhost:8080/#")).isEqualTo("localhost"); + + assertThat(UrlParser.getHost("https://localhost:8080#fragment")).isEqualTo("localhost"); + assertThat(UrlParser.getHost("https://localhost:8080/#fragment")).isEqualTo("localhost"); + } + + @Test + void testGetHostWithNoAuthority() { + assertThat(UrlParser.getHost("https:")).isNull(); + assertThat(UrlParser.getHost("https:/")).isNull(); + + assertThat(UrlParser.getHost("https:?")).isNull(); + assertThat(UrlParser.getHost("https:/?")).isNull(); + + assertThat(UrlParser.getHost("https:?query")).isNull(); + assertThat(UrlParser.getHost("https:/?query")).isNull(); + + assertThat(UrlParser.getHost("https:#")).isNull(); + assertThat(UrlParser.getHost("https:/#")).isNull(); + + assertThat(UrlParser.getHost("https:#fragment")).isNull(); + assertThat(UrlParser.getHost("https:/#fragment")).isNull(); + } + + @Test + void testGetHostWithNoScheme() { + assertThat(UrlParser.getHost("")).isNull(); + assertThat(UrlParser.getHost("/")).isNull(); + + assertThat(UrlParser.getHost("?")).isNull(); + assertThat(UrlParser.getHost("/?")).isNull(); + + assertThat(UrlParser.getHost("?query")).isNull(); + assertThat(UrlParser.getHost("/?query")).isNull(); + + assertThat(UrlParser.getHost("#")).isNull(); + assertThat(UrlParser.getHost("/#")).isNull(); + + assertThat(UrlParser.getHost("#fragment")).isNull(); + assertThat(UrlParser.getHost("/#fragment")).isNull(); + } + + @Test + void testGetPort() { + assertThat(UrlParser.getPort("https://localhost")).isNull(); + assertThat(UrlParser.getPort("https://localhost/")).isNull(); + + assertThat(UrlParser.getPort("https://localhost?")).isNull(); + assertThat(UrlParser.getPort("https://localhost/?")).isNull(); + + assertThat(UrlParser.getPort("https://localhost?query")).isNull(); + assertThat(UrlParser.getPort("https://localhost/?query")).isNull(); + + assertThat(UrlParser.getPort("https://localhost#")).isNull(); + assertThat(UrlParser.getPort("https://localhost/#")).isNull(); + + assertThat(UrlParser.getPort("https://localhost#fragment")).isNull(); + assertThat(UrlParser.getPort("https://localhost/#fragment")).isNull(); + } + + @Test + void testGetPortWithPort() { + assertThat(UrlParser.getPort("https://localhost:8080")).isEqualTo(8080); + assertThat(UrlParser.getPort("https://localhost:8080/")).isEqualTo(8080); + + assertThat(UrlParser.getPort("https://localhost:8080?")).isEqualTo(8080); + assertThat(UrlParser.getPort("https://localhost:8080/?")).isEqualTo(8080); + + assertThat(UrlParser.getPort("https://localhost:8080?query")).isEqualTo(8080); + assertThat(UrlParser.getPort("https://localhost:8080/?query")).isEqualTo(8080); + + assertThat(UrlParser.getPort("https://localhost:8080#")).isEqualTo(8080); + assertThat(UrlParser.getPort("https://localhost:8080/#")).isEqualTo(8080); + + assertThat(UrlParser.getPort("https://localhost:8080#fragment")).isEqualTo(8080); + assertThat(UrlParser.getPort("https://localhost:8080/#fragment")).isEqualTo(8080); + } + + @Test + void testGetPortWithNoAuthority() { + assertThat(UrlParser.getPort("https:")).isNull(); + assertThat(UrlParser.getPort("https:/")).isNull(); + + assertThat(UrlParser.getPort("https:?")).isNull(); + assertThat(UrlParser.getPort("https:/?")).isNull(); + + assertThat(UrlParser.getPort("https:?query")).isNull(); + assertThat(UrlParser.getPort("https:/?query")).isNull(); + + assertThat(UrlParser.getPort("https:#")).isNull(); + assertThat(UrlParser.getPort("https:/#")).isNull(); + + assertThat(UrlParser.getPort("https:#fragment")).isNull(); + assertThat(UrlParser.getPort("https:/#fragment")).isNull(); + } + + @Test + void testGetPortWithNoScheme() { + assertThat(UrlParser.getPort("")).isNull(); + assertThat(UrlParser.getPort("/")).isNull(); + + assertThat(UrlParser.getPort("?")).isNull(); + assertThat(UrlParser.getPort("/?")).isNull(); + + assertThat(UrlParser.getPort("?query")).isNull(); + assertThat(UrlParser.getPort("/?query")).isNull(); + + assertThat(UrlParser.getPort("#")).isNull(); + assertThat(UrlParser.getPort("/#")).isNull(); + + assertThat(UrlParser.getPort("#fragment")).isNull(); + assertThat(UrlParser.getPort("/#fragment")).isNull(); + } + + @Test + void testGetPath() { + assertThat(UrlParser.getPath("https://localhost")).isNull(); + assertThat(UrlParser.getPath("https://localhost/")).isEqualTo("/"); + assertThat(UrlParser.getPath("https://localhost/api/v1")).isEqualTo("/api/v1"); + + assertThat(UrlParser.getPath("https://localhost?")).isNull(); + assertThat(UrlParser.getPath("https://localhost/?")).isEqualTo("/"); + assertThat(UrlParser.getPath("https://localhost/api/v1?")).isEqualTo("/api/v1"); + + assertThat(UrlParser.getPath("https://localhost?query")).isNull(); + assertThat(UrlParser.getPath("https://localhost/?query")).isEqualTo("/"); + assertThat(UrlParser.getPath("https://localhost/api/v1?query")).isEqualTo("/api/v1"); + + assertThat(UrlParser.getPath("https://localhost#")).isNull(); + assertThat(UrlParser.getPath("https://localhost/#")).isEqualTo("/"); + assertThat(UrlParser.getPath("https://localhost/api/v1#")).isEqualTo("/api/v1"); + + assertThat(UrlParser.getPath("https://localhost#fragment")).isNull(); + assertThat(UrlParser.getPath("https://localhost/#fragment")).isEqualTo("/"); + assertThat(UrlParser.getPath("https://localhost/api/v1#fragment")).isEqualTo("/api/v1"); + } + + @Test + void testGetPathWithPort() { + assertThat(UrlParser.getPath("https://localhost:8080")).isNull(); + assertThat(UrlParser.getPath("https://localhost:8080/")).isEqualTo("/"); + assertThat(UrlParser.getPath("https://localhost:8080/api/v1")).isEqualTo("/api/v1"); + + assertThat(UrlParser.getPath("https://localhost:8080?")).isNull(); + assertThat(UrlParser.getPath("https://localhost:8080/?")).isEqualTo("/"); + assertThat(UrlParser.getPath("https://localhost:8080/api/v1?")).isEqualTo("/api/v1"); + + assertThat(UrlParser.getPath("https://localhost:8080?query")).isNull(); + assertThat(UrlParser.getPath("https://localhost:8080/?query")).isEqualTo("/"); + assertThat(UrlParser.getPath("https://localhost:8080/api/v1?query")).isEqualTo("/api/v1"); + + assertThat(UrlParser.getPath("https://localhost:8080#")).isNull(); + assertThat(UrlParser.getPath("https://localhost:8080/#")).isEqualTo("/"); + assertThat(UrlParser.getPath("https://localhost:8080/api/v1#")).isEqualTo("/api/v1"); + + assertThat(UrlParser.getPath("https://localhost:8080#fragment")).isNull(); + assertThat(UrlParser.getPath("https://localhost:8080/#fragment")).isEqualTo("/"); + assertThat(UrlParser.getPath("https://localhost:8080/api/v1#fragment")).isEqualTo("/api/v1"); + } + + @Test + void testGetPathWithNoAuthority() { + assertThat(UrlParser.getPath("https:")).isNull(); + assertThat(UrlParser.getPath("https:/")).isNull(); + assertThat(UrlParser.getPath("https:/api/v1")).isNull(); + + assertThat(UrlParser.getPath("https:?")).isNull(); + assertThat(UrlParser.getPath("https:/?")).isNull(); + assertThat(UrlParser.getPath("https:/api/v1?")).isNull(); + + assertThat(UrlParser.getPath("https:?query")).isNull(); + assertThat(UrlParser.getPath("https:/?query")).isNull(); + assertThat(UrlParser.getPath("https:/api/v1?query")).isNull(); + + assertThat(UrlParser.getPath("https:#")).isNull(); + assertThat(UrlParser.getPath("https:/#")).isNull(); + assertThat(UrlParser.getPath("https:/api/v1#")).isNull(); + + assertThat(UrlParser.getPath("https:#fragment")).isNull(); + assertThat(UrlParser.getPath("https:/#fragment")).isNull(); + assertThat(UrlParser.getPath("https:/api/v1#fragment")).isNull(); + } + + @Test + void testGetPathtWithNoScheme() { + assertThat(UrlParser.getPath("")).isNull(); + assertThat(UrlParser.getPath("/")).isNull(); + assertThat(UrlParser.getPath("/api/v1")).isNull(); + + assertThat(UrlParser.getPath("?")).isNull(); + assertThat(UrlParser.getPath("/?")).isNull(); + assertThat(UrlParser.getPath("/api/v1?")).isNull(); + + assertThat(UrlParser.getPath("?query")).isNull(); + assertThat(UrlParser.getPath("/?query")).isNull(); + assertThat(UrlParser.getPath("/api/v1?query")).isNull(); + + assertThat(UrlParser.getPath("#")).isNull(); + assertThat(UrlParser.getPath("/#")).isNull(); + assertThat(UrlParser.getPath("/api/v1#")).isNull(); + + assertThat(UrlParser.getPath("#fragment")).isNull(); + assertThat(UrlParser.getPath("/#fragment")).isNull(); + assertThat(UrlParser.getPath("/api/v1#fragment")).isNull(); + } +} diff --git a/instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/rpc/RpcAttributesExtractorTest.java b/instrumentation-api-incubator/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv/rpc/RpcAttributesExtractorTest.java similarity index 76% rename from instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/rpc/RpcAttributesExtractorTest.java rename to instrumentation-api-incubator/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv/rpc/RpcAttributesExtractorTest.java index d14edbc8a0ee..dfca442b2573 100644 --- a/instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/rpc/RpcAttributesExtractorTest.java +++ b/instrumentation-api-incubator/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv/rpc/RpcAttributesExtractorTest.java @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.instrumentation.api.instrumenter.rpc; +package io.opentelemetry.instrumentation.api.incubator.semconv.rpc; import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; import static org.assertj.core.api.Assertions.entry; @@ -12,7 +12,7 @@ import io.opentelemetry.api.common.AttributesBuilder; import io.opentelemetry.context.Context; import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.opentelemetry.semconv.incubating.RpcIncubatingAttributes; import java.util.HashMap; import java.util.Map; import org.junit.jupiter.api.Test; @@ -59,14 +59,14 @@ private static void testExtractor(AttributesExtractor, Void> extractor.onStart(attributes, context, request); assertThat(attributes.build()) .containsOnly( - entry(SemanticAttributes.RPC_SYSTEM, "test"), - entry(SemanticAttributes.RPC_SERVICE, "my.Service"), - entry(SemanticAttributes.RPC_METHOD, "Method")); + entry(RpcIncubatingAttributes.RPC_SYSTEM, "test"), + entry(RpcIncubatingAttributes.RPC_SERVICE, "my.Service"), + entry(RpcIncubatingAttributes.RPC_METHOD, "Method")); extractor.onEnd(attributes, context, request, null, null); assertThat(attributes.build()) .containsOnly( - entry(SemanticAttributes.RPC_SYSTEM, "test"), - entry(SemanticAttributes.RPC_SERVICE, "my.Service"), - entry(SemanticAttributes.RPC_METHOD, "Method")); + entry(RpcIncubatingAttributes.RPC_SYSTEM, "test"), + entry(RpcIncubatingAttributes.RPC_SERVICE, "my.Service"), + entry(RpcIncubatingAttributes.RPC_METHOD, "Method")); } } diff --git a/instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/rpc/RpcClientMetricsTest.java b/instrumentation-api-incubator/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv/rpc/RpcClientMetricsTest.java similarity index 66% rename from instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/rpc/RpcClientMetricsTest.java rename to instrumentation-api-incubator/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv/rpc/RpcClientMetricsTest.java index 295851545797..768a67de0bb2 100644 --- a/instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/rpc/RpcClientMetricsTest.java +++ b/instrumentation-api-incubator/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv/rpc/RpcClientMetricsTest.java @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.instrumentation.api.instrumenter.rpc; +package io.opentelemetry.instrumentation.api.incubator.semconv.rpc; import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; @@ -17,7 +17,9 @@ import io.opentelemetry.instrumentation.api.instrumenter.OperationListener; import io.opentelemetry.sdk.metrics.SdkMeterProvider; import io.opentelemetry.sdk.testing.exporter.InMemoryMetricReader; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.opentelemetry.semconv.NetworkAttributes; +import io.opentelemetry.semconv.ServerAttributes; +import io.opentelemetry.semconv.incubating.RpcIncubatingAttributes; import java.util.concurrent.TimeUnit; import org.junit.jupiter.api.Test; @@ -33,22 +35,23 @@ void collectsMetrics() { Attributes requestAttributes = Attributes.builder() - .put(SemanticAttributes.RPC_SYSTEM, "grpc") - .put(SemanticAttributes.RPC_SERVICE, "myservice.EchoService") - .put(SemanticAttributes.RPC_METHOD, "exampleMethod") + .put(RpcIncubatingAttributes.RPC_SYSTEM, "grpc") + .put(RpcIncubatingAttributes.RPC_SERVICE, "myservice.EchoService") + .put(RpcIncubatingAttributes.RPC_METHOD, "exampleMethod") .build(); Attributes responseAttributes1 = Attributes.builder() - .put(SemanticAttributes.NET_PEER_NAME, "example.com") - .put(SemanticAttributes.NET_PEER_PORT, 8080) - .put(SemanticAttributes.NET_TRANSPORT, "ip_tcp") + .put(ServerAttributes.SERVER_ADDRESS, "example.com") + .put(ServerAttributes.SERVER_PORT, 8080) + .put(NetworkAttributes.NETWORK_TRANSPORT, "tcp") + .put(NetworkAttributes.NETWORK_TYPE, "ipv4") .build(); Attributes responseAttributes2 = Attributes.builder() - .put(SemanticAttributes.NET_PEER_PORT, 8080) - .put(SemanticAttributes.NET_TRANSPORT, "ip_tcp") + .put(ServerAttributes.SERVER_PORT, 8080) + .put(NetworkAttributes.NETWORK_TRANSPORT, "tcp") .build(); Context parent = @@ -84,15 +87,17 @@ void collectsMetrics() { point .hasSum(150 /* millis */) .hasAttributesSatisfying( - equalTo(SemanticAttributes.RPC_SYSTEM, "grpc"), + equalTo(RpcIncubatingAttributes.RPC_SYSTEM, "grpc"), equalTo( - SemanticAttributes.RPC_SERVICE, + RpcIncubatingAttributes.RPC_SERVICE, "myservice.EchoService"), - equalTo(SemanticAttributes.RPC_METHOD, "exampleMethod"), equalTo( - SemanticAttributes.NET_PEER_NAME, "example.com"), - equalTo(SemanticAttributes.NET_PEER_PORT, 8080), - equalTo(SemanticAttributes.NET_TRANSPORT, "ip_tcp")) + RpcIncubatingAttributes.RPC_METHOD, + "exampleMethod"), + equalTo(ServerAttributes.SERVER_ADDRESS, "example.com"), + equalTo(ServerAttributes.SERVER_PORT, 8080), + equalTo(NetworkAttributes.NETWORK_TRANSPORT, "tcp"), + equalTo(NetworkAttributes.NETWORK_TYPE, "ipv4")) .hasExemplarsSatisfying( exemplar -> exemplar @@ -114,13 +119,15 @@ void collectsMetrics() { point .hasSum(150 /* millis */) .hasAttributesSatisfying( - equalTo(SemanticAttributes.RPC_SYSTEM, "grpc"), + equalTo(RpcIncubatingAttributes.RPC_SYSTEM, "grpc"), equalTo( - SemanticAttributes.RPC_SERVICE, + RpcIncubatingAttributes.RPC_SERVICE, "myservice.EchoService"), - equalTo(SemanticAttributes.RPC_METHOD, "exampleMethod"), - equalTo(SemanticAttributes.NET_PEER_PORT, 8080), - equalTo(SemanticAttributes.NET_TRANSPORT, "ip_tcp"))))); + equalTo( + RpcIncubatingAttributes.RPC_METHOD, + "exampleMethod"), + equalTo(ServerAttributes.SERVER_PORT, 8080), + equalTo(NetworkAttributes.NETWORK_TRANSPORT, "tcp"))))); } private static long nanos(int millis) { diff --git a/instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/rpc/RpcServerMetricsTest.java b/instrumentation-api-incubator/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv/rpc/RpcServerMetricsTest.java similarity index 67% rename from instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/rpc/RpcServerMetricsTest.java rename to instrumentation-api-incubator/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv/rpc/RpcServerMetricsTest.java index dae6f1cc2e25..bb8b59082aa5 100644 --- a/instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/rpc/RpcServerMetricsTest.java +++ b/instrumentation-api-incubator/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv/rpc/RpcServerMetricsTest.java @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.instrumentation.api.instrumenter.rpc; +package io.opentelemetry.instrumentation.api.incubator.semconv.rpc; import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; @@ -17,7 +17,9 @@ import io.opentelemetry.instrumentation.api.instrumenter.OperationListener; import io.opentelemetry.sdk.metrics.SdkMeterProvider; import io.opentelemetry.sdk.testing.exporter.InMemoryMetricReader; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.opentelemetry.semconv.NetworkAttributes; +import io.opentelemetry.semconv.ServerAttributes; +import io.opentelemetry.semconv.incubating.RpcIncubatingAttributes; import java.util.concurrent.TimeUnit; import org.junit.jupiter.api.Test; @@ -33,24 +35,25 @@ void collectsMetrics() { Attributes requestAttributes = Attributes.builder() - .put(SemanticAttributes.RPC_SYSTEM, "grpc") - .put(SemanticAttributes.RPC_SERVICE, "myservice.EchoService") - .put(SemanticAttributes.RPC_METHOD, "exampleMethod") + .put(RpcIncubatingAttributes.RPC_SYSTEM, "grpc") + .put(RpcIncubatingAttributes.RPC_SERVICE, "myservice.EchoService") + .put(RpcIncubatingAttributes.RPC_METHOD, "exampleMethod") .build(); Attributes responseAttributes1 = Attributes.builder() - .put(SemanticAttributes.NET_HOST_NAME, "example.com") - .put(SemanticAttributes.NET_SOCK_HOST_ADDR, "127.0.0.1") - .put(SemanticAttributes.NET_HOST_PORT, 8080) - .put(SemanticAttributes.NET_TRANSPORT, "ip_tcp") + .put(ServerAttributes.SERVER_ADDRESS, "example.com") + .put(ServerAttributes.SERVER_PORT, 8080) + .put(NetworkAttributes.NETWORK_LOCAL_ADDRESS, "127.0.0.1") + .put(NetworkAttributes.NETWORK_TRANSPORT, "tcp") + .put(NetworkAttributes.NETWORK_TYPE, "ipv4") .build(); Attributes responseAttributes2 = Attributes.builder() - .put(SemanticAttributes.NET_SOCK_HOST_ADDR, "127.0.0.1") - .put(SemanticAttributes.NET_HOST_PORT, 8080) - .put(SemanticAttributes.NET_TRANSPORT, "ip_tcp") + .put(ServerAttributes.SERVER_PORT, 8080) + .put(NetworkAttributes.NETWORK_LOCAL_ADDRESS, "127.0.0.1") + .put(NetworkAttributes.NETWORK_TRANSPORT, "tcp") .build(); Context parent = @@ -86,14 +89,16 @@ void collectsMetrics() { point .hasSum(150 /* millis */) .hasAttributesSatisfying( - equalTo(SemanticAttributes.RPC_SYSTEM, "grpc"), + equalTo(RpcIncubatingAttributes.RPC_SYSTEM, "grpc"), equalTo( - SemanticAttributes.RPC_SERVICE, + RpcIncubatingAttributes.RPC_SERVICE, "myservice.EchoService"), - equalTo(SemanticAttributes.RPC_METHOD, "exampleMethod"), equalTo( - SemanticAttributes.NET_HOST_NAME, "example.com"), - equalTo(SemanticAttributes.NET_TRANSPORT, "ip_tcp")) + RpcIncubatingAttributes.RPC_METHOD, + "exampleMethod"), + equalTo(ServerAttributes.SERVER_ADDRESS, "example.com"), + equalTo(NetworkAttributes.NETWORK_TRANSPORT, "tcp"), + equalTo(NetworkAttributes.NETWORK_TYPE, "ipv4")) .hasExemplarsSatisfying( exemplar -> exemplar @@ -115,14 +120,14 @@ void collectsMetrics() { point .hasSum(150 /* millis */) .hasAttributesSatisfying( - equalTo(SemanticAttributes.RPC_SYSTEM, "grpc"), + equalTo(RpcIncubatingAttributes.RPC_SYSTEM, "grpc"), equalTo( - SemanticAttributes.RPC_SERVICE, + RpcIncubatingAttributes.RPC_SERVICE, "myservice.EchoService"), - equalTo(SemanticAttributes.RPC_METHOD, "exampleMethod"), equalTo( - SemanticAttributes.NET_SOCK_HOST_ADDR, "127.0.0.1"), - equalTo(SemanticAttributes.NET_TRANSPORT, "ip_tcp"))))); + RpcIncubatingAttributes.RPC_METHOD, + "exampleMethod"), + equalTo(NetworkAttributes.NETWORK_TRANSPORT, "tcp"))))); } private static long nanos(int millis) { diff --git a/instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/rpc/RpcSpanNameExtractorTest.java b/instrumentation-api-incubator/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv/rpc/RpcSpanNameExtractorTest.java similarity index 95% rename from instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/rpc/RpcSpanNameExtractorTest.java rename to instrumentation-api-incubator/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv/rpc/RpcSpanNameExtractorTest.java index eb908a4198db..e20af5c494a4 100644 --- a/instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/rpc/RpcSpanNameExtractorTest.java +++ b/instrumentation-api-incubator/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv/rpc/RpcSpanNameExtractorTest.java @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.instrumentation.api.instrumenter.rpc; +package io.opentelemetry.instrumentation.api.incubator.semconv.rpc; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.when; diff --git a/instrumentation-api-semconv/build.gradle.kts b/instrumentation-api-semconv/build.gradle.kts deleted file mode 100644 index 937bef23896b..000000000000 --- a/instrumentation-api-semconv/build.gradle.kts +++ /dev/null @@ -1,85 +0,0 @@ -plugins { - id("org.xbib.gradle.plugin.jflex") - - id("otel.java-conventions") - id("otel.animalsniffer-conventions") - id("otel.jacoco-conventions") - id("otel.japicmp-conventions") - id("otel.publish-conventions") -} - -group = "io.opentelemetry.instrumentation" - -dependencies { - api("io.opentelemetry:opentelemetry-semconv") - api(project(":instrumentation-api")) - implementation("io.opentelemetry:opentelemetry-extension-incubator") - - compileOnly("com.google.auto.value:auto-value-annotations") - annotationProcessor("com.google.auto.value:auto-value") - - testImplementation(project(":testing-common")) - testImplementation("io.opentelemetry:opentelemetry-sdk") - testImplementation("io.opentelemetry:opentelemetry-sdk-testing") -} - -testing { - suites { - val testStableHttpSemconv by registering(JvmTestSuite::class) { - dependencies { - implementation(project()) - implementation(project(":testing-common")) - implementation("io.opentelemetry:opentelemetry-sdk") - implementation("io.opentelemetry:opentelemetry-sdk-testing") - } - targets { - all { - testTask.configure { - jvmArgs("-Dotel.semconv-stability.opt-in=http") - } - } - } - } - val testBothHttpSemconv by registering(JvmTestSuite::class) { - dependencies { - implementation(project()) - implementation(project(":testing-common")) - implementation("io.opentelemetry:opentelemetry-sdk") - implementation("io.opentelemetry:opentelemetry-sdk-testing") - } - targets { - all { - testTask.configure { - jvmArgs("-Dotel.semconv-stability.opt-in=http/dup") - } - } - } - } - } -} - -tasks { - // exclude auto-generated code - named("checkstyleMain") { - exclude("**/AutoSqlSanitizer.java") - } - - // Work around https://github.com/jflex-de/jflex/issues/762 - compileJava { - with(options) { - compilerArgs.add("-Xlint:-fallthrough") - } - } - - sourcesJar { - dependsOn("generateJflex") - } - - test { - jvmArgs("-Dotel.instrumentation.http.prefer-forwarded-url-scheme=true") - } - - check { - dependsOn(testing.suites) - } -} diff --git a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/ForwardedHeaderParser.java b/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/ForwardedHeaderParser.java deleted file mode 100644 index e2e7f6df70bc..000000000000 --- a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/ForwardedHeaderParser.java +++ /dev/null @@ -1,112 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.api.instrumenter.http; - -import java.util.Locale; -import javax.annotation.Nullable; - -final class ForwardedHeaderParser { - - /** Extract proto (aka scheme) from "Forwarded" http header. */ - @Nullable - static String extractProtoFromForwardedHeader(String forwarded) { - int start = forwarded.toLowerCase(Locale.ROOT).indexOf("proto="); - if (start < 0) { - return null; - } - start += 6; // start is now the index after proto= - if (start >= forwarded.length() - 1) { // the value after for= must not be empty - return null; - } - return extractProto(forwarded, start); - } - - /** Extract proto (aka scheme) from "X-Forwarded-Proto" http header. */ - @Nullable - static String extractProtoFromForwardedProtoHeader(String forwardedProto) { - return extractProto(forwardedProto, 0); - } - - /** Extract client IP address from "Forwarded" http header. */ - @Nullable - static String extractClientIpFromForwardedHeader(String forwarded) { - int start = forwarded.toLowerCase(Locale.ROOT).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; - } - return extractIpAddress(forwarded, start); - } - - /** Extract client IP address from "X-Forwarded-For" http header. */ - @Nullable - static String extractClientIpFromForwardedForHeader(String forwardedFor) { - return extractIpAddress(forwardedFor, 0); - } - - @Nullable - private static String extractProto(String forwarded, int start) { - if (forwarded.length() == start) { - return null; - } - if (forwarded.charAt(start) == '"') { - return extractProto(forwarded, start + 1); - } - for (int i = start; i < forwarded.length(); i++) { - char c = forwarded.charAt(i); - if (c == ',' || c == ';' || c == '"') { - if (i == start) { // empty string - return null; - } - return forwarded.substring(start, i); - } - } - return forwarded.substring(start); - } - - // from https://www.rfc-editor.org/rfc/rfc7239 - // "Note that IPv6 addresses may not be quoted in - // X-Forwarded-For and may not be enclosed by square brackets, but they - // are quoted and enclosed in square brackets in Forwarded" - // and also (applying to Forwarded but not X-Forwarded-For) - // "It is important to note that an IPv6 address and any nodename with - // node-port specified MUST be quoted, since ':' is not an allowed - // character in 'token'." - @Nullable - private static String extractIpAddress(String forwarded, int start) { - if (forwarded.length() == start) { - return null; - } - if (forwarded.charAt(start) == '"') { - return extractIpAddress(forwarded, start + 1); - } - if (forwarded.charAt(start) == '[') { - int end = forwarded.indexOf(']', start + 1); - if (end == -1) { - return null; - } - return forwarded.substring(start + 1, end); - } - boolean inIpv4 = false; - for (int i = start; i < forwarded.length(); i++) { - char c = forwarded.charAt(i); - if (c == '.') { - inIpv4 = true; - } else if (c == ',' || c == ';' || c == '"' || (inIpv4 && c == ':')) { - if (i == start) { // empty string - return null; - } - return forwarded.substring(start, i); - } - } - return forwarded.substring(start); - } - - private ForwardedHeaderParser() {} -} diff --git a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpAttributes.java b/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpAttributes.java deleted file mode 100644 index 18000841137e..000000000000 --- a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpAttributes.java +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.api.instrumenter.http; - -import static io.opentelemetry.api.common.AttributeKey.longKey; -import static io.opentelemetry.api.common.AttributeKey.stringKey; - -import io.opentelemetry.api.common.AttributeKey; - -final class HttpAttributes { - - // FIXME: remove this class and replace its usages with SemanticAttributes once schema 1.21 is - // released - - static final AttributeKey HTTP_REQUEST_METHOD = stringKey("http.request.method"); - - static final AttributeKey HTTP_REQUEST_BODY_SIZE = longKey("http.request.body.size"); - - static final AttributeKey HTTP_RESPONSE_BODY_SIZE = longKey("http.response.body.size"); - - static final AttributeKey HTTP_RESPONSE_STATUS_CODE = longKey("http.response.status_code"); - - private HttpAttributes() {} -} diff --git a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpClientAttributesExtractor.java b/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpClientAttributesExtractor.java deleted file mode 100644 index 816cc66fcd99..000000000000 --- a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpClientAttributesExtractor.java +++ /dev/null @@ -1,202 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.api.instrumenter.http; - -import static io.opentelemetry.instrumentation.api.internal.AttributesExtractorUtil.internalSet; - -import io.opentelemetry.api.common.AttributesBuilder; -import io.opentelemetry.context.Context; -import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.net.NetClientAttributesGetter; -import io.opentelemetry.instrumentation.api.instrumenter.net.internal.InternalNetClientAttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.network.internal.InternalNetworkAttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.network.internal.InternalServerAttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.url.internal.UrlAttributes; -import io.opentelemetry.instrumentation.api.internal.SemconvStability; -import io.opentelemetry.instrumentation.api.internal.SpanKey; -import io.opentelemetry.instrumentation.api.internal.SpanKeyProvider; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; -import java.util.List; -import java.util.function.ToIntFunction; -import javax.annotation.Nullable; - -/** - * Extractor of HTTP - * client attributes. Instrumentation of HTTP 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 final class HttpClientAttributesExtractor - extends HttpCommonAttributesExtractor< - REQUEST, RESPONSE, HttpClientAttributesGetter> - implements SpanKeyProvider { - - /** Creates the HTTP client attributes extractor with default configuration. */ - public static AttributesExtractor create( - HttpClientAttributesGetter httpAttributesGetter, - NetClientAttributesGetter netAttributesGetter) { - return builder(httpAttributesGetter, netAttributesGetter).build(); - } - - /** - * Returns a new {@link HttpClientAttributesExtractorBuilder} that can be used to configure the - * HTTP client attributes extractor. - */ - public static HttpClientAttributesExtractorBuilder builder( - HttpClientAttributesGetter httpAttributesGetter, - NetClientAttributesGetter netAttributesGetter) { - return new HttpClientAttributesExtractorBuilder<>(httpAttributesGetter, netAttributesGetter); - } - - private final InternalNetClientAttributesExtractor internalNetExtractor; - private final InternalNetworkAttributesExtractor internalNetworkExtractor; - private final InternalServerAttributesExtractor internalServerExtractor; - private final ToIntFunction resendCountIncrementer; - - HttpClientAttributesExtractor( - HttpClientAttributesGetter httpAttributesGetter, - NetClientAttributesGetter netAttributesGetter, - List capturedRequestHeaders, - List capturedResponseHeaders) { - this( - httpAttributesGetter, - netAttributesGetter, - capturedRequestHeaders, - capturedResponseHeaders, - HttpClientResend::getAndIncrement); - } - - // visible for tests - HttpClientAttributesExtractor( - HttpClientAttributesGetter httpAttributesGetter, - NetClientAttributesGetter netAttributesGetter, - List capturedRequestHeaders, - List capturedResponseHeaders, - ToIntFunction resendCountIncrementer) { - super(httpAttributesGetter, capturedRequestHeaders, capturedResponseHeaders); - HttpNetNamePortGetter namePortGetter = - new HttpNetNamePortGetter<>(httpAttributesGetter); - internalNetExtractor = - new InternalNetClientAttributesExtractor<>( - netAttributesGetter, namePortGetter, SemconvStability.emitOldHttpSemconv()); - internalNetworkExtractor = - new InternalNetworkAttributesExtractor<>( - netAttributesGetter, - HttpNetworkTransportFilter.INSTANCE, - SemconvStability.emitStableHttpSemconv(), - SemconvStability.emitOldHttpSemconv()); - internalServerExtractor = - new InternalServerAttributesExtractor<>( - netAttributesGetter, - this::shouldCaptureServerPort, - namePortGetter, - SemconvStability.emitStableHttpSemconv(), - SemconvStability.emitOldHttpSemconv(), - InternalServerAttributesExtractor.Mode.PEER); - this.resendCountIncrementer = resendCountIncrementer; - } - - @Override - public void onStart(AttributesBuilder attributes, Context parentContext, REQUEST request) { - super.onStart(attributes, parentContext, request); - - internalServerExtractor.onStart(attributes, request); - - String fullUrl = stripSensitiveData(getter.getUrlFull(request)); - if (SemconvStability.emitStableHttpSemconv()) { - internalSet(attributes, UrlAttributes.URL_FULL, fullUrl); - } - if (SemconvStability.emitOldHttpSemconv()) { - internalSet(attributes, SemanticAttributes.HTTP_URL, fullUrl); - } - } - - private boolean shouldCaptureServerPort(int port, REQUEST request) { - String url = getter.getUrlFull(request); - if (url == null) { - return true; - } - // according to spec: extract if not default (80 for http scheme, 443 for https). - if ((url.startsWith("http://") && port == 80) || (url.startsWith("https://") && port == 443)) { - return false; - } - return true; - } - - @Override - public void onEnd( - AttributesBuilder attributes, - Context context, - REQUEST request, - @Nullable RESPONSE response, - @Nullable Throwable error) { - super.onEnd(attributes, context, request, response, error); - - internalNetExtractor.onEnd(attributes, request, response); - internalNetworkExtractor.onEnd(attributes, request, response); - internalServerExtractor.onEnd(attributes, request, response); - - int resendCount = resendCountIncrementer.applyAsInt(context); - if (resendCount > 0) { - attributes.put(SemanticAttributes.HTTP_RESEND_COUNT, resendCount); - } - } - - /** - * This method is internal and is hence not for public use. Its API is unstable and can change at - * any time. - */ - @Override - public SpanKey internalGetSpanKey() { - return SpanKey.HTTP_CLIENT; - } - - @Nullable - private static String stripSensitiveData(@Nullable String url) { - if (url == null || url.isEmpty()) { - return url; - } - - int schemeEndIndex = url.indexOf(':'); - - if (schemeEndIndex == -1) { - // not a valid url - return url; - } - - int len = url.length(); - if (len <= schemeEndIndex + 2 - || url.charAt(schemeEndIndex + 1) != '/' - || url.charAt(schemeEndIndex + 2) != '/') { - // has no authority component - return url; - } - - // look for the end of the authority component: - // '/', '?', '#' ==> start of path - int index; - int atIndex = -1; - for (index = schemeEndIndex + 3; index < len; index++) { - char c = url.charAt(index); - - if (c == '@') { - atIndex = index; - } - - if (c == '/' || c == '?' || c == '#') { - break; - } - } - - if (atIndex == -1 || atIndex == len - 1) { - return url; - } - return url.substring(0, schemeEndIndex + 3) + "REDACTED:REDACTED" + url.substring(atIndex); - } -} diff --git a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpClientAttributesExtractorBuilder.java b/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpClientAttributesExtractorBuilder.java deleted file mode 100644 index a1cddcfa0e14..000000000000 --- a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpClientAttributesExtractorBuilder.java +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.api.instrumenter.http; - -import static java.util.Collections.emptyList; - -import com.google.errorprone.annotations.CanIgnoreReturnValue; -import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.net.NetClientAttributesGetter; -import java.util.List; - -/** A builder of {@link HttpClientAttributesExtractor}. */ -public final class HttpClientAttributesExtractorBuilder { - - final HttpClientAttributesGetter httpAttributesGetter; - final NetClientAttributesGetter netAttributesGetter; - List capturedRequestHeaders = emptyList(); - List capturedResponseHeaders = emptyList(); - - HttpClientAttributesExtractorBuilder( - HttpClientAttributesGetter httpAttributesGetter, - NetClientAttributesGetter netAttributesGetter) { - this.httpAttributesGetter = httpAttributesGetter; - this.netAttributesGetter = netAttributesGetter; - } - - /** - * Configures the HTTP request headers that will be captured as span attributes as described in HTTP - * semantic conventions. - * - *

The HTTP request header values will be captured under the {@code http.request.header.} - * attribute key. The {@code } part in the attribute key is the normalized header name: - * lowercase, with dashes replaced by underscores. - * - * @param requestHeaders A list of HTTP header names. - */ - @CanIgnoreReturnValue - public HttpClientAttributesExtractorBuilder setCapturedRequestHeaders( - List requestHeaders) { - this.capturedRequestHeaders = requestHeaders; - return this; - } - - /** - * Configures the HTTP response headers that will be captured as span attributes as described in - * HTTP - * semantic conventions. - * - *

The HTTP response header values will be captured under the {@code - * http.response.header.} attribute key. The {@code } part in the attribute key is the - * normalized header name: lowercase, with dashes replaced by underscores. - * - * @param responseHeaders A list of HTTP header names. - */ - @CanIgnoreReturnValue - public HttpClientAttributesExtractorBuilder setCapturedResponseHeaders( - List responseHeaders) { - this.capturedResponseHeaders = responseHeaders; - return this; - } - - /** - * Returns a new {@link HttpClientAttributesExtractor} with the settings of this {@link - * HttpClientAttributesExtractorBuilder}. - */ - public AttributesExtractor build() { - return new HttpClientAttributesExtractor<>( - httpAttributesGetter, netAttributesGetter, capturedRequestHeaders, capturedResponseHeaders); - } -} diff --git a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpClientMetrics.java b/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpClientMetrics.java deleted file mode 100644 index e31eeb09ff0d..000000000000 --- a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpClientMetrics.java +++ /dev/null @@ -1,112 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.api.instrumenter.http; - -import static io.opentelemetry.instrumentation.api.instrumenter.http.HttpMessageBodySizeUtil.getHttpRequestBodySize; -import static io.opentelemetry.instrumentation.api.instrumenter.http.HttpMessageBodySizeUtil.getHttpResponseBodySize; -import static io.opentelemetry.instrumentation.api.instrumenter.http.HttpMetricsUtil.createDurationHistogram; -import static io.opentelemetry.instrumentation.api.instrumenter.http.HttpMetricsUtil.nanosToUnit; -import static io.opentelemetry.instrumentation.api.instrumenter.http.TemporaryMetricsView.applyClientDurationAndSizeView; -import static java.util.logging.Level.FINE; - -import com.google.auto.value.AutoValue; -import io.opentelemetry.api.common.Attributes; -import io.opentelemetry.api.metrics.DoubleHistogram; -import io.opentelemetry.api.metrics.LongHistogram; -import io.opentelemetry.api.metrics.Meter; -import io.opentelemetry.context.Context; -import io.opentelemetry.context.ContextKey; -import io.opentelemetry.instrumentation.api.instrumenter.OperationListener; -import io.opentelemetry.instrumentation.api.instrumenter.OperationMetrics; -import java.util.logging.Logger; - -/** - * {@link OperationListener} which keeps track of HTTP - * client metrics. - */ -public final class HttpClientMetrics implements OperationListener { - - private static final ContextKey HTTP_CLIENT_REQUEST_METRICS_STATE = - ContextKey.named("http-client-request-metrics-state"); - - private static final Logger logger = Logger.getLogger(HttpClientMetrics.class.getName()); - - /** - * Returns a {@link OperationMetrics} which can be used to enable recording of {@link - * HttpClientMetrics} on an {@link - * io.opentelemetry.instrumentation.api.instrumenter.InstrumenterBuilder}. - */ - public static OperationMetrics get() { - return HttpClientMetrics::new; - } - - private final DoubleHistogram duration; - private final LongHistogram requestSize; - private final LongHistogram responseSize; - - private HttpClientMetrics(Meter meter) { - duration = - createDurationHistogram( - meter, "http.client.duration", "The duration of the outbound HTTP request"); - requestSize = - meter - .histogramBuilder("http.client.request.size") - .setUnit("By") - .setDescription("The size of HTTP request messages") - .ofLongs() - .build(); - responseSize = - meter - .histogramBuilder("http.client.response.size") - .setUnit("By") - .setDescription("The size of HTTP response messages") - .ofLongs() - .build(); - } - - @Override - public Context onStart(Context context, Attributes startAttributes, long startNanos) { - return context.with( - HTTP_CLIENT_REQUEST_METRICS_STATE, - new AutoValue_HttpClientMetrics_State(startAttributes, startNanos)); - } - - @Override - public void onEnd(Context context, Attributes endAttributes, long endNanos) { - State state = context.get(HTTP_CLIENT_REQUEST_METRICS_STATE); - if (state == null) { - logger.log( - FINE, - "No state present when ending context {0}. Cannot record HTTP request metrics.", - context); - return; - } - - Attributes durationAndSizeAttributes = - applyClientDurationAndSizeView(state.startAttributes(), endAttributes); - duration.record( - nanosToUnit(endNanos - state.startTimeNanos()), durationAndSizeAttributes, context); - - Long requestBodySize = getHttpRequestBodySize(endAttributes, state.startAttributes()); - if (requestBodySize != null) { - requestSize.record(requestBodySize, durationAndSizeAttributes, context); - } - - Long responseBodySize = getHttpResponseBodySize(endAttributes, state.startAttributes()); - if (responseBodySize != null) { - responseSize.record(responseBodySize, durationAndSizeAttributes, context); - } - } - - @AutoValue - abstract static class State { - - abstract Attributes startAttributes(); - - abstract long startTimeNanos(); - } -} diff --git a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpCommonAttributesExtractor.java b/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpCommonAttributesExtractor.java deleted file mode 100644 index 543dd81f5747..000000000000 --- a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpCommonAttributesExtractor.java +++ /dev/null @@ -1,181 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.api.instrumenter.http; - -import static io.opentelemetry.instrumentation.api.instrumenter.http.CapturedHttpHeadersUtil.lowercase; -import static io.opentelemetry.instrumentation.api.instrumenter.http.CapturedHttpHeadersUtil.requestAttributeKey; -import static io.opentelemetry.instrumentation.api.instrumenter.http.CapturedHttpHeadersUtil.responseAttributeKey; -import static io.opentelemetry.instrumentation.api.internal.AttributesExtractorUtil.internalSet; -import static java.util.logging.Level.FINE; - -import io.opentelemetry.api.common.AttributesBuilder; -import io.opentelemetry.context.Context; -import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.net.internal.FallbackNamePortGetter; -import io.opentelemetry.instrumentation.api.internal.SemconvStability; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; -import java.util.List; -import java.util.logging.Logger; -import javax.annotation.Nullable; - -/** - * Extractor of HTTP - * attributes that are common to client and server instrumentations. - */ -abstract class HttpCommonAttributesExtractor< - REQUEST, RESPONSE, GETTER extends HttpCommonAttributesGetter> - implements AttributesExtractor { - - private static final Logger logger = Logger.getLogger(HttpCommonAttributesGetter.class.getName()); - - final GETTER getter; - private final List capturedRequestHeaders; - private final List capturedResponseHeaders; - - HttpCommonAttributesExtractor( - GETTER getter, List capturedRequestHeaders, List capturedResponseHeaders) { - this.getter = getter; - this.capturedRequestHeaders = lowercase(capturedRequestHeaders); - this.capturedResponseHeaders = lowercase(capturedResponseHeaders); - } - - @Override - public void onStart(AttributesBuilder attributes, Context parentContext, REQUEST request) { - String method = getter.getHttpRequestMethod(request); - if (SemconvStability.emitStableHttpSemconv()) { - internalSet(attributes, HttpAttributes.HTTP_REQUEST_METHOD, method); - } - if (SemconvStability.emitOldHttpSemconv()) { - internalSet(attributes, SemanticAttributes.HTTP_METHOD, method); - } - internalSet(attributes, SemanticAttributes.USER_AGENT_ORIGINAL, userAgent(request)); - - for (String name : capturedRequestHeaders) { - List values = getter.getHttpRequestHeader(request, name); - if (!values.isEmpty()) { - internalSet(attributes, requestAttributeKey(name), values); - } - } - } - - @Override - public void onEnd( - AttributesBuilder attributes, - Context context, - REQUEST request, - @Nullable RESPONSE response, - @Nullable Throwable error) { - - Long requestBodySize = requestBodySize(request); - if (SemconvStability.emitStableHttpSemconv()) { - internalSet(attributes, HttpAttributes.HTTP_REQUEST_BODY_SIZE, requestBodySize); - } - if (SemconvStability.emitOldHttpSemconv()) { - internalSet(attributes, SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH, requestBodySize); - } - - if (response != null) { - Integer statusCode = getter.getHttpResponseStatusCode(request, response, error); - if (statusCode != null && statusCode > 0) { - if (SemconvStability.emitStableHttpSemconv()) { - internalSet(attributes, HttpAttributes.HTTP_RESPONSE_STATUS_CODE, (long) statusCode); - } - if (SemconvStability.emitOldHttpSemconv()) { - internalSet(attributes, SemanticAttributes.HTTP_STATUS_CODE, (long) statusCode); - } - } - - Long responseBodySize = responseBodySize(request, response); - if (SemconvStability.emitStableHttpSemconv()) { - internalSet(attributes, HttpAttributes.HTTP_RESPONSE_BODY_SIZE, responseBodySize); - } - if (SemconvStability.emitOldHttpSemconv()) { - internalSet(attributes, SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH, responseBodySize); - } - - for (String name : capturedResponseHeaders) { - List values = getter.getHttpResponseHeader(request, response, name); - if (!values.isEmpty()) { - internalSet(attributes, responseAttributeKey(name), values); - } - } - } - } - - @Nullable - private String userAgent(REQUEST request) { - return firstHeaderValue(getter.getHttpRequestHeader(request, "user-agent")); - } - - @Nullable - private Long requestBodySize(REQUEST request) { - return parseNumber(firstHeaderValue(getter.getHttpRequestHeader(request, "content-length"))); - } - - @Nullable - private Long responseBodySize(REQUEST request, RESPONSE response) { - return parseNumber( - firstHeaderValue(getter.getHttpResponseHeader(request, response, "content-length"))); - } - - @Nullable - static String firstHeaderValue(List values) { - return values.isEmpty() ? null : values.get(0); - } - - @Nullable - private static Long parseNumber(@Nullable String number) { - if (number == null) { - return null; - } - try { - return Long.parseLong(number); - } catch (NumberFormatException e) { - // not a number - return null; - } - } - - static final class HttpNetNamePortGetter implements FallbackNamePortGetter { - - private final HttpCommonAttributesGetter getter; - - HttpNetNamePortGetter(HttpCommonAttributesGetter getter) { - this.getter = getter; - } - - @Nullable - @Override - public String name(REQUEST request) { - String host = firstHeaderValue(getter.getHttpRequestHeader(request, "host")); - if (host == null) { - return null; - } - int hostHeaderSeparator = host.indexOf(':'); - return hostHeaderSeparator == -1 ? host : host.substring(0, hostHeaderSeparator); - } - - @Nullable - @Override - public Integer port(REQUEST request) { - String host = firstHeaderValue(getter.getHttpRequestHeader(request, "host")); - if (host == null) { - return null; - } - int hostHeaderSeparator = host.indexOf(':'); - if (hostHeaderSeparator == -1) { - return null; - } - try { - return Integer.parseInt(host.substring(hostHeaderSeparator + 1)); - } catch (NumberFormatException e) { - logger.log(FINE, e.getMessage(), e); - } - return null; - } - } -} diff --git a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpMessageBodySizeUtil.java b/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpMessageBodySizeUtil.java deleted file mode 100644 index 73b9c0d7e354..000000000000 --- a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpMessageBodySizeUtil.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.api.instrumenter.http; - -import io.opentelemetry.api.common.AttributeKey; -import io.opentelemetry.api.common.Attributes; -import io.opentelemetry.instrumentation.api.internal.SemconvStability; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; -import javax.annotation.Nullable; - -final class HttpMessageBodySizeUtil { - - private static final AttributeKey HTTP_REQUEST_BODY_SIZE = - SemconvStability.emitOldHttpSemconv() - ? SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH - : HttpAttributes.HTTP_REQUEST_BODY_SIZE; - - private static final AttributeKey HTTP_RESPONSE_BODY_SIZE = - SemconvStability.emitOldHttpSemconv() - ? SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH - : HttpAttributes.HTTP_RESPONSE_BODY_SIZE; - - @Nullable - static Long getHttpRequestBodySize(Attributes... attributesList) { - return getAttribute(HTTP_REQUEST_BODY_SIZE, attributesList); - } - - @Nullable - static Long getHttpResponseBodySize(Attributes... attributesList) { - return getAttribute(HTTP_RESPONSE_BODY_SIZE, attributesList); - } - - @Nullable - private static T getAttribute(AttributeKey key, Attributes... attributesList) { - for (Attributes attributes : attributesList) { - T value = attributes.get(key); - if (value != null) { - return value; - } - } - return null; - } - - private HttpMessageBodySizeUtil() {} -} diff --git a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpMetricsUtil.java b/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpMetricsUtil.java deleted file mode 100644 index fcbbd8238977..000000000000 --- a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpMetricsUtil.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.api.instrumenter.http; - -import static java.util.Arrays.asList; -import static java.util.Collections.unmodifiableList; - -import io.opentelemetry.api.metrics.DoubleHistogram; -import io.opentelemetry.api.metrics.DoubleHistogramBuilder; -import io.opentelemetry.api.metrics.Meter; -import io.opentelemetry.extension.incubator.metrics.ExtendedDoubleHistogramBuilder; -import io.opentelemetry.instrumentation.api.internal.SemconvStability; -import java.util.List; -import java.util.concurrent.TimeUnit; - -final class HttpMetricsUtil { - - // we'll use the old unit if the old semconv is in use - private static final boolean useSeconds = - SemconvStability.emitStableHttpSemconv() && !SemconvStability.emitOldHttpSemconv(); - - static final List DURATION_SECONDS_BUCKETS = - unmodifiableList( - asList( - 0.0, 0.005, 0.01, 0.025, 0.05, 0.075, 0.1, 0.25, 0.5, 0.75, 1.0, 2.5, 5.0, 7.5, - 10.0)); - - private static final double NANOS_PER_MS = TimeUnit.MILLISECONDS.toNanos(1); - private static final double NANOS_PER_S = TimeUnit.SECONDS.toNanos(1); - - static DoubleHistogram createDurationHistogram(Meter meter, String name, String description) { - DoubleHistogramBuilder durationBuilder = - meter.histogramBuilder(name).setUnit(useSeconds ? "s" : "ms").setDescription(description); - // don't set custom buckets if milliseconds are still used - if (useSeconds && durationBuilder instanceof ExtendedDoubleHistogramBuilder) { - ((ExtendedDoubleHistogramBuilder) durationBuilder) - .setAdvice(advice -> advice.setExplicitBucketBoundaries(DURATION_SECONDS_BUCKETS)); - } - return durationBuilder.build(); - } - - static double nanosToUnit(long durationNanos) { - return durationNanos / (useSeconds ? NANOS_PER_S : NANOS_PER_MS); - } - - private HttpMetricsUtil() {} -} diff --git a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpNetworkTransportFilter.java b/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpNetworkTransportFilter.java deleted file mode 100644 index 8df6989fcdde..000000000000 --- a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpNetworkTransportFilter.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.api.instrumenter.http; - -import io.opentelemetry.instrumentation.api.instrumenter.network.internal.NetworkTransportFilter; -import javax.annotation.Nullable; - -enum HttpNetworkTransportFilter implements NetworkTransportFilter { - INSTANCE; - - @Override - public boolean shouldAddNetworkTransport( - @Nullable String protocolName, - @Nullable String protocolVersion, - @Nullable String proposedTransport) { - // tcp is the default transport for http/1* and http/2*, we're skipping it - if ("http".equals(protocolName) - && protocolVersion != null - && (protocolVersion.startsWith("1") || protocolVersion.startsWith("2")) - && "tcp".equals(proposedTransport)) { - return false; - } - // udp is the default transport for http/3*, we're skipping it - if ("http".equals(protocolName) - && protocolVersion != null - && protocolVersion.startsWith("3") - && "udp".equals(proposedTransport)) { - return false; - } - return true; - } -} diff --git a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpRouteSource.java b/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpRouteSource.java deleted file mode 100644 index e566d7ea80e3..000000000000 --- a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpRouteSource.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.api.instrumenter.http; - -/** Represents the source that provided the {@code http.route} attribute. */ -public enum HttpRouteSource { - // for servlet filters we try to find the best name which isn't necessarily from the first - // filter that is called - FILTER(1, /* useFirst= */ false), - SERVLET(2), - CONTROLLER(3), - // Some frameworks, e.g. JaxRS, allow for nested controller/paths and we want to select the - // longest one - NESTED_CONTROLLER(4, false); - - final int order; - final boolean useFirst; - - HttpRouteSource(int order) { - this(order, /* useFirst= */ true); - } - - HttpRouteSource(int order, boolean useFirst) { - this.order = order; - this.useFirst = useFirst; - } -} diff --git a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpServerAttributesExtractor.java b/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpServerAttributesExtractor.java deleted file mode 100644 index 154896c2fff0..000000000000 --- a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpServerAttributesExtractor.java +++ /dev/null @@ -1,244 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.api.instrumenter.http; - -import static io.opentelemetry.instrumentation.api.instrumenter.http.ForwardedHeaderParser.extractClientIpFromForwardedForHeader; -import static io.opentelemetry.instrumentation.api.instrumenter.http.ForwardedHeaderParser.extractClientIpFromForwardedHeader; -import static io.opentelemetry.instrumentation.api.instrumenter.http.ForwardedHeaderParser.extractProtoFromForwardedHeader; -import static io.opentelemetry.instrumentation.api.instrumenter.http.ForwardedHeaderParser.extractProtoFromForwardedProtoHeader; -import static io.opentelemetry.instrumentation.api.internal.AttributesExtractorUtil.internalSet; - -import io.opentelemetry.api.common.AttributesBuilder; -import io.opentelemetry.context.Context; -import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.net.NetServerAttributesGetter; -import io.opentelemetry.instrumentation.api.instrumenter.net.internal.FallbackNamePortGetter; -import io.opentelemetry.instrumentation.api.instrumenter.net.internal.InternalNetServerAttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.network.internal.InternalClientAttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.network.internal.InternalNetworkAttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.network.internal.InternalServerAttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.url.internal.InternalUrlAttributesExtractor; -import io.opentelemetry.instrumentation.api.internal.ConfigPropertiesUtil; -import io.opentelemetry.instrumentation.api.internal.SemconvStability; -import io.opentelemetry.instrumentation.api.internal.SpanKey; -import io.opentelemetry.instrumentation.api.internal.SpanKeyProvider; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; -import java.util.List; -import java.util.function.Function; -import javax.annotation.Nullable; - -/** - * Extractor of HTTP - * server attributes. Instrumentation of HTTP server 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 final class HttpServerAttributesExtractor - extends HttpCommonAttributesExtractor< - REQUEST, RESPONSE, HttpServerAttributesGetter> - implements SpanKeyProvider { - - /** Creates the HTTP server attributes extractor with default configuration. */ - public static AttributesExtractor create( - HttpServerAttributesGetter httpAttributesGetter, - NetServerAttributesGetter netAttributesGetter) { - return builder(httpAttributesGetter, netAttributesGetter).build(); - } - - /** - * Returns a new {@link HttpServerAttributesExtractorBuilder} that can be used to configure the - * HTTP client attributes extractor. - */ - public static HttpServerAttributesExtractorBuilder builder( - HttpServerAttributesGetter httpAttributesGetter, - NetServerAttributesGetter netAttributesGetter) { - return new HttpServerAttributesExtractorBuilder<>(httpAttributesGetter, netAttributesGetter); - } - - // if set to true, the instrumentation will prefer the scheme from Forwarded/X-Forwarded-Proto - // headers over the one extracted from the URL - private static final boolean PREFER_FORWARDED_URL_SCHEME = - ConfigPropertiesUtil.getBoolean( - "otel.instrumentation.http.prefer-forwarded-url-scheme", false); - - private final InternalUrlAttributesExtractor internalUrlExtractor; - private final InternalNetServerAttributesExtractor internalNetExtractor; - private final InternalNetworkAttributesExtractor internalNetworkExtractor; - private final InternalServerAttributesExtractor internalServerExtractor; - private final InternalClientAttributesExtractor internalClientExtractor; - private final Function httpRouteHolderGetter; - - HttpServerAttributesExtractor( - HttpServerAttributesGetter httpAttributesGetter, - NetServerAttributesGetter netAttributesGetter, - List capturedRequestHeaders, - List capturedResponseHeaders) { - this( - httpAttributesGetter, - netAttributesGetter, - capturedRequestHeaders, - capturedResponseHeaders, - HttpRouteHolder::getRoute); - } - - // visible for tests - HttpServerAttributesExtractor( - HttpServerAttributesGetter httpAttributesGetter, - NetServerAttributesGetter netAttributesGetter, - List capturedRequestHeaders, - List capturedResponseHeaders, - Function httpRouteHolderGetter) { - super(httpAttributesGetter, capturedRequestHeaders, capturedResponseHeaders); - HttpNetNamePortGetter namePortGetter = - new HttpNetNamePortGetter<>(httpAttributesGetter); - internalUrlExtractor = - new InternalUrlAttributesExtractor<>( - httpAttributesGetter, - /* alternateSchemeProvider= */ this::forwardedProto, - SemconvStability.emitStableHttpSemconv(), - SemconvStability.emitOldHttpSemconv()); - internalNetExtractor = - new InternalNetServerAttributesExtractor<>( - netAttributesGetter, namePortGetter, SemconvStability.emitOldHttpSemconv()); - internalNetworkExtractor = - new InternalNetworkAttributesExtractor<>( - netAttributesGetter, - HttpNetworkTransportFilter.INSTANCE, - SemconvStability.emitStableHttpSemconv(), - SemconvStability.emitOldHttpSemconv()); - internalServerExtractor = - new InternalServerAttributesExtractor<>( - netAttributesGetter, - this::shouldCaptureServerPort, - namePortGetter, - SemconvStability.emitStableHttpSemconv(), - SemconvStability.emitOldHttpSemconv(), - InternalServerAttributesExtractor.Mode.HOST); - internalClientExtractor = - new InternalClientAttributesExtractor<>( - netAttributesGetter, - new ClientAddressGetter<>(httpAttributesGetter), - SemconvStability.emitStableHttpSemconv(), - SemconvStability.emitOldHttpSemconv()); - this.httpRouteHolderGetter = httpRouteHolderGetter; - } - - @Override - public void onStart(AttributesBuilder attributes, Context parentContext, REQUEST request) { - super.onStart(attributes, parentContext, request); - - internalUrlExtractor.onStart(attributes, request); - internalNetExtractor.onStart(attributes, request); - internalServerExtractor.onStart(attributes, request); - internalClientExtractor.onStart(attributes, request); - - internalSet(attributes, SemanticAttributes.HTTP_ROUTE, getter.getHttpRoute(request)); - } - - private boolean shouldCaptureServerPort(int port, REQUEST request) { - String scheme = getter.getUrlScheme(request); - if (scheme == null) { - return true; - } - // according to spec: extract if not default (80 for http scheme, 443 for https). - if ((scheme.equals("http") && port == 80) || (scheme.equals("https") && port == 443)) { - return false; - } - return true; - } - - @Override - public void onEnd( - AttributesBuilder attributes, - Context context, - REQUEST request, - @Nullable RESPONSE response, - @Nullable Throwable error) { - - super.onEnd(attributes, context, request, response, error); - - internalNetworkExtractor.onEnd(attributes, request, response); - internalServerExtractor.onEnd(attributes, request, response); - internalClientExtractor.onEnd(attributes, request, response); - - internalSet(attributes, SemanticAttributes.HTTP_ROUTE, httpRouteHolderGetter.apply(context)); - } - - @Nullable - private String forwardedProto(REQUEST request) { - if (!PREFER_FORWARDED_URL_SCHEME) { - // don't parse headers, extract scheme from the URL - return null; - } - - // try Forwarded - String forwarded = firstHeaderValue(getter.getHttpRequestHeader(request, "forwarded")); - if (forwarded != null) { - forwarded = extractProtoFromForwardedHeader(forwarded); - if (forwarded != null) { - return forwarded; - } - } - - // try X-Forwarded-Proto - forwarded = firstHeaderValue(getter.getHttpRequestHeader(request, "x-forwarded-proto")); - if (forwarded != null) { - return extractProtoFromForwardedProtoHeader(forwarded); - } - - return null; - } - - /** - * This method is internal and is hence not for public use. Its API is unstable and can change at - * any time. - */ - @Override - public SpanKey internalGetSpanKey() { - return SpanKey.HTTP_SERVER; - } - - private static final class ClientAddressGetter - implements FallbackNamePortGetter { - - private final HttpServerAttributesGetter getter; - - private ClientAddressGetter(HttpServerAttributesGetter getter) { - this.getter = getter; - } - - @Nullable - @Override - public String name(REQUEST request) { - // try Forwarded - String forwarded = firstHeaderValue(getter.getHttpRequestHeader(request, "forwarded")); - if (forwarded != null) { - forwarded = extractClientIpFromForwardedHeader(forwarded); - if (forwarded != null) { - return forwarded; - } - } - - // try X-Forwarded-For - forwarded = firstHeaderValue(getter.getHttpRequestHeader(request, "x-forwarded-for")); - if (forwarded != null) { - return extractClientIpFromForwardedForHeader(forwarded); - } - - return null; - } - - @Nullable - @Override - public Integer port(REQUEST request) { - // TODO: client.port will be implemented in a future PR - return null; - } - } -} diff --git a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpServerAttributesExtractorBuilder.java b/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpServerAttributesExtractorBuilder.java deleted file mode 100644 index eccb8ba08175..000000000000 --- a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpServerAttributesExtractorBuilder.java +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.api.instrumenter.http; - -import static java.util.Collections.emptyList; - -import com.google.errorprone.annotations.CanIgnoreReturnValue; -import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.net.NetServerAttributesGetter; -import java.util.List; - -/** A builder of {@link HttpServerAttributesExtractor}. */ -public final class HttpServerAttributesExtractorBuilder { - - final HttpServerAttributesGetter httpAttributesGetter; - final NetServerAttributesGetter netAttributesGetter; - List capturedRequestHeaders = emptyList(); - List capturedResponseHeaders = emptyList(); - - HttpServerAttributesExtractorBuilder( - HttpServerAttributesGetter httpAttributesGetter, - NetServerAttributesGetter netAttributesGetter) { - this.httpAttributesGetter = httpAttributesGetter; - this.netAttributesGetter = netAttributesGetter; - } - - /** - * Configures the HTTP request headers that will be captured as span attributes as described in HTTP - * semantic conventions. - * - *

The HTTP request header values will be captured under the {@code http.request.header.} - * attribute key. The {@code } part in the attribute key is the normalized header name: - * lowercase, with dashes replaced by underscores. - * - * @param requestHeaders A list of HTTP header names. - */ - @CanIgnoreReturnValue - public HttpServerAttributesExtractorBuilder setCapturedRequestHeaders( - List requestHeaders) { - this.capturedRequestHeaders = requestHeaders; - return this; - } - - /** - * Configures the HTTP response headers that will be captured as span attributes as described in - * HTTP - * semantic conventions. - * - *

The HTTP response header values will be captured under the {@code - * http.response.header.} attribute key. The {@code } part in the attribute key is the - * normalized header name: lowercase, with dashes replaced by underscores. - * - * @param responseHeaders A list of HTTP header names. - */ - @CanIgnoreReturnValue - public HttpServerAttributesExtractorBuilder setCapturedResponseHeaders( - List responseHeaders) { - this.capturedResponseHeaders = responseHeaders; - return this; - } - - /** - * Returns a new {@link HttpServerAttributesExtractor} with the settings of this {@link - * HttpServerAttributesExtractorBuilder}. - */ - public AttributesExtractor build() { - return new HttpServerAttributesExtractor<>( - httpAttributesGetter, netAttributesGetter, capturedRequestHeaders, capturedResponseHeaders); - } -} diff --git a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpServerMetrics.java b/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpServerMetrics.java deleted file mode 100644 index 25fb7c1f0692..000000000000 --- a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpServerMetrics.java +++ /dev/null @@ -1,126 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.api.instrumenter.http; - -import static io.opentelemetry.instrumentation.api.instrumenter.http.HttpMessageBodySizeUtil.getHttpRequestBodySize; -import static io.opentelemetry.instrumentation.api.instrumenter.http.HttpMessageBodySizeUtil.getHttpResponseBodySize; -import static io.opentelemetry.instrumentation.api.instrumenter.http.HttpMetricsUtil.createDurationHistogram; -import static io.opentelemetry.instrumentation.api.instrumenter.http.HttpMetricsUtil.nanosToUnit; -import static io.opentelemetry.instrumentation.api.instrumenter.http.TemporaryMetricsView.applyActiveRequestsView; -import static io.opentelemetry.instrumentation.api.instrumenter.http.TemporaryMetricsView.applyServerDurationAndSizeView; -import static java.util.logging.Level.FINE; - -import com.google.auto.value.AutoValue; -import io.opentelemetry.api.common.Attributes; -import io.opentelemetry.api.metrics.DoubleHistogram; -import io.opentelemetry.api.metrics.LongHistogram; -import io.opentelemetry.api.metrics.LongUpDownCounter; -import io.opentelemetry.api.metrics.Meter; -import io.opentelemetry.context.Context; -import io.opentelemetry.context.ContextKey; -import io.opentelemetry.instrumentation.api.instrumenter.OperationListener; -import io.opentelemetry.instrumentation.api.instrumenter.OperationMetrics; -import java.util.logging.Logger; - -/** - * {@link OperationListener} which keeps track of HTTP - * server metrics. - */ -public final class HttpServerMetrics implements OperationListener { - - private static final ContextKey HTTP_SERVER_REQUEST_METRICS_STATE = - ContextKey.named("http-server-request-metrics-state"); - - private static final Logger logger = Logger.getLogger(HttpServerMetrics.class.getName()); - - /** - * Returns a {@link OperationMetrics} which can be used to enable recording of {@link - * HttpServerMetrics} on an {@link - * io.opentelemetry.instrumentation.api.instrumenter.InstrumenterBuilder}. - */ - public static OperationMetrics get() { - return HttpServerMetrics::new; - } - - private final LongUpDownCounter activeRequests; - private final DoubleHistogram duration; - private final LongHistogram requestSize; - private final LongHistogram responseSize; - - private HttpServerMetrics(Meter meter) { - activeRequests = - meter - .upDownCounterBuilder("http.server.active_requests") - .setUnit("{requests}") - .setDescription("The number of concurrent HTTP requests that are currently in-flight") - .build(); - duration = - createDurationHistogram( - meter, "http.server.duration", "The duration of the inbound HTTP request"); - requestSize = - meter - .histogramBuilder("http.server.request.size") - .setUnit("By") - .setDescription("The size of HTTP request messages") - .ofLongs() - .build(); - responseSize = - meter - .histogramBuilder("http.server.response.size") - .setUnit("By") - .setDescription("The size of HTTP response messages") - .ofLongs() - .build(); - } - - @Override - public Context onStart(Context context, Attributes startAttributes, long startNanos) { - activeRequests.add(1, applyActiveRequestsView(startAttributes), context); - - return context.with( - HTTP_SERVER_REQUEST_METRICS_STATE, - new AutoValue_HttpServerMetrics_State(startAttributes, startNanos)); - } - - @Override - public void onEnd(Context context, Attributes endAttributes, long endNanos) { - State state = context.get(HTTP_SERVER_REQUEST_METRICS_STATE); - if (state == null) { - logger.log( - FINE, - "No state present when ending context {0}. Cannot record HTTP request metrics.", - context); - return; - } - // it's important to use exactly the same attributes that were used when incrementing the active - // request count (otherwise it will split the timeseries) - activeRequests.add(-1, applyActiveRequestsView(state.startAttributes()), context); - - Attributes durationAndSizeAttributes = - applyServerDurationAndSizeView(state.startAttributes(), endAttributes); - duration.record( - nanosToUnit(endNanos - state.startTimeNanos()), durationAndSizeAttributes, context); - - Long requestBodySize = getHttpRequestBodySize(endAttributes, state.startAttributes()); - if (requestBodySize != null) { - requestSize.record(requestBodySize, durationAndSizeAttributes, context); - } - - Long responseBodySize = getHttpResponseBodySize(endAttributes, state.startAttributes()); - if (responseBodySize != null) { - responseSize.record(responseBodySize, durationAndSizeAttributes, context); - } - } - - @AutoValue - abstract static class State { - - abstract Attributes startAttributes(); - - abstract long startTimeNanos(); - } -} diff --git a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpSpanNameExtractor.java b/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpSpanNameExtractor.java deleted file mode 100644 index 65794011b27b..000000000000 --- a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpSpanNameExtractor.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.api.instrumenter.http; - -import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor; -import javax.annotation.Nullable; - -/** - * 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( - HttpCommonAttributesGetter getter) { - return new HttpSpanNameExtractor<>(getter); - } - - private final HttpCommonAttributesGetter getter; - - private HttpSpanNameExtractor(HttpCommonAttributesGetter getter) { - this.getter = getter; - } - - @Override - public String extract(REQUEST request) { - String method = getter.getHttpRequestMethod(request); - String route = extractRoute(request); - if (method != null) { - return route == null ? method : method + " " + route; - } - return "HTTP"; - } - - @Nullable - private String extractRoute(REQUEST request) { - if (getter instanceof HttpServerAttributesGetter) { - return ((HttpServerAttributesGetter) getter).getHttpRoute(request); - } - return null; - } -} diff --git a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpStatusConverter.java b/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpStatusConverter.java deleted file mode 100644 index d7e3becb91ea..000000000000 --- a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpStatusConverter.java +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.api.instrumenter.http; - -import io.opentelemetry.api.trace.StatusCode; - -// https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/trace/semantic_conventions/http.md#status -enum HttpStatusConverter { - SERVER { - @Override - StatusCode statusFromHttpStatus(int httpStatus) { - if (httpStatus >= 100 && httpStatus < 500) { - return StatusCode.UNSET; - } - - return StatusCode.ERROR; - } - }, - CLIENT { - @Override - StatusCode statusFromHttpStatus(int httpStatus) { - if (httpStatus >= 100 && httpStatus < 400) { - return StatusCode.UNSET; - } - - return StatusCode.ERROR; - } - }; - - abstract StatusCode statusFromHttpStatus(int httpStatus); -} diff --git a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/TemporaryMetricsView.java b/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/TemporaryMetricsView.java deleted file mode 100644 index b340e9871faf..000000000000 --- a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/TemporaryMetricsView.java +++ /dev/null @@ -1,127 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.api.instrumenter.http; - -import io.opentelemetry.api.common.AttributeKey; -import io.opentelemetry.api.common.Attributes; -import io.opentelemetry.api.common.AttributesBuilder; -import io.opentelemetry.instrumentation.api.instrumenter.net.internal.NetAttributes; -import io.opentelemetry.instrumentation.api.instrumenter.network.internal.NetworkAttributes; -import io.opentelemetry.instrumentation.api.instrumenter.url.internal.UrlAttributes; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; -import java.util.HashSet; -import java.util.Set; -import java.util.function.BiConsumer; - -// this is temporary, see -// https://github.com/open-telemetry/opentelemetry-java-instrumentation/issues/3962#issuecomment-906606325 -@SuppressWarnings("rawtypes") -final class TemporaryMetricsView { - - private static final Set durationAlwaysInclude = buildDurationAlwaysInclude(); - private static final Set durationClientView = buildDurationClientView(); - private static final Set durationServerView = buildDurationServerView(); - private static final Set activeRequestsView = buildActiveRequestsView(); - - private static Set buildDurationAlwaysInclude() { - // the list of included metrics is from - // https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/semantic_conventions/http-metrics.md#attributes - Set view = new HashSet<>(); - view.add(SemanticAttributes.HTTP_METHOD); - view.add(SemanticAttributes.HTTP_STATUS_CODE); // Optional - view.add(NetAttributes.NET_PROTOCOL_NAME); // Optional - view.add(NetAttributes.NET_PROTOCOL_VERSION); // Optional - // stable semconv - view.add(HttpAttributes.HTTP_REQUEST_METHOD); - view.add(HttpAttributes.HTTP_RESPONSE_STATUS_CODE); - view.add(NetworkAttributes.NETWORK_PROTOCOL_NAME); - view.add(NetworkAttributes.NETWORK_PROTOCOL_VERSION); - view.add(NetworkAttributes.SERVER_ADDRESS); - view.add(NetworkAttributes.SERVER_PORT); - return view; - } - - private static Set buildDurationClientView() { - // We pull identifying attributes according to: - // https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/semantic_conventions/http-metrics.md#attribute-alternatives - // We only pull net.peer.name and net.peer.port because http.url has too high cardinality - Set view = new HashSet<>(durationAlwaysInclude); - view.add(SemanticAttributes.NET_PEER_NAME); - view.add(SemanticAttributes.NET_PEER_PORT); - view.add(SemanticAttributes.NET_SOCK_PEER_ADDR); - // stable semconv - view.add(NetworkAttributes.SERVER_SOCKET_ADDRESS); - return view; - } - - private static Set buildDurationServerView() { - // We pull identifying attributes according to: - // https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/semantic_conventions/http-metrics.md#attribute-alternatives - // With the following caveat: - // - we always rely on http.route + http.host in this repository. - // - we prefer http.route (which is scrubbed) over http.target (which is not scrubbed). - Set view = new HashSet<>(durationAlwaysInclude); - view.add(SemanticAttributes.HTTP_SCHEME); - view.add(SemanticAttributes.NET_HOST_NAME); - view.add(SemanticAttributes.NET_HOST_PORT); - view.add(SemanticAttributes.HTTP_ROUTE); - // stable semconv - view.add(UrlAttributes.URL_SCHEME); - return view; - } - - private static Set buildActiveRequestsView() { - // the list of included metrics is from - // https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/semantic_conventions/http-metrics.md#attributes - Set view = new HashSet<>(); - view.add(SemanticAttributes.HTTP_METHOD); - view.add(SemanticAttributes.HTTP_SCHEME); - view.add(SemanticAttributes.NET_HOST_NAME); - view.add(SemanticAttributes.NET_HOST_PORT); - // stable semconv - view.add(HttpAttributes.HTTP_REQUEST_METHOD); - view.add(NetworkAttributes.SERVER_ADDRESS); - view.add(NetworkAttributes.SERVER_PORT); - view.add(UrlAttributes.URL_SCHEME); - return view; - } - - static Attributes applyClientDurationAndSizeView( - Attributes startAttributes, Attributes endAttributes) { - AttributesBuilder filtered = Attributes.builder(); - applyView(filtered, startAttributes, durationClientView); - applyView(filtered, endAttributes, durationClientView); - return filtered.build(); - } - - static Attributes applyServerDurationAndSizeView( - Attributes startAttributes, Attributes endAttributes) { - AttributesBuilder filtered = Attributes.builder(); - applyView(filtered, startAttributes, durationServerView); - applyView(filtered, endAttributes, durationServerView); - return filtered.build(); - } - - static Attributes applyActiveRequestsView(Attributes attributes) { - AttributesBuilder filtered = Attributes.builder(); - applyView(filtered, attributes, activeRequestsView); - return filtered.build(); - } - - @SuppressWarnings("unchecked") - private static void applyView( - AttributesBuilder filtered, Attributes attributes, Set view) { - attributes.forEach( - (BiConsumer) - (key, value) -> { - if (view.contains(key)) { - filtered.put(key, value); - } - }); - } - - private TemporaryMetricsView() {} -} diff --git a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/messaging/MessagingAttributesExtractor.java b/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/messaging/MessagingAttributesExtractor.java deleted file mode 100644 index 6cfe85d8465b..000000000000 --- a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/messaging/MessagingAttributesExtractor.java +++ /dev/null @@ -1,133 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.api.instrumenter.messaging; - -import static io.opentelemetry.instrumentation.api.instrumenter.messaging.CapturedMessageHeadersUtil.attributeKey; -import static io.opentelemetry.instrumentation.api.instrumenter.messaging.CapturedMessageHeadersUtil.lowercase; -import static io.opentelemetry.instrumentation.api.instrumenter.messaging.MessageOperation.PROCESS; -import static io.opentelemetry.instrumentation.api.instrumenter.messaging.MessageOperation.RECEIVE; -import static io.opentelemetry.instrumentation.api.internal.AttributesExtractorUtil.internalSet; - -import io.opentelemetry.api.common.AttributesBuilder; -import io.opentelemetry.context.Context; -import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; -import io.opentelemetry.instrumentation.api.internal.SpanKey; -import io.opentelemetry.instrumentation.api.internal.SpanKeyProvider; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; -import java.util.List; -import javax.annotation.Nullable; - -/** - * Extractor of messaging - * attributes. - * - *

This class delegates to a type-specific {@link MessagingAttributesGetter} for individual - * attribute extraction from request/response objects. - */ -public final class MessagingAttributesExtractor - implements AttributesExtractor, SpanKeyProvider { - - static final String TEMP_DESTINATION_NAME = "(temporary)"; - - /** - * Creates the messaging attributes extractor for the given {@link MessageOperation operation} - * with default configuration. - */ - public static AttributesExtractor create( - MessagingAttributesGetter getter, MessageOperation operation) { - return builder(getter, operation).build(); - } - - /** - * Returns a new {@link MessagingAttributesExtractorBuilder} for the given {@link MessageOperation - * operation} that can be used to configure the messaging attributes extractor. - */ - public static MessagingAttributesExtractorBuilder builder( - MessagingAttributesGetter getter, MessageOperation operation) { - return new MessagingAttributesExtractorBuilder<>(getter, operation); - } - - private final MessagingAttributesGetter getter; - private final MessageOperation operation; - private final List capturedHeaders; - - MessagingAttributesExtractor( - MessagingAttributesGetter getter, - MessageOperation operation, - List capturedHeaders) { - this.getter = getter; - this.operation = operation; - this.capturedHeaders = lowercase(capturedHeaders); - } - - @Override - public void onStart(AttributesBuilder attributes, Context parentContext, REQUEST request) { - internalSet(attributes, SemanticAttributes.MESSAGING_SYSTEM, getter.getSystem(request)); - boolean isTemporaryDestination = getter.isTemporaryDestination(request); - if (isTemporaryDestination) { - internalSet(attributes, SemanticAttributes.MESSAGING_DESTINATION_TEMPORARY, true); - internalSet(attributes, SemanticAttributes.MESSAGING_DESTINATION_NAME, TEMP_DESTINATION_NAME); - } else { - internalSet( - attributes, - SemanticAttributes.MESSAGING_DESTINATION_NAME, - getter.getDestination(request)); - } - internalSet( - attributes, - SemanticAttributes.MESSAGING_MESSAGE_CONVERSATION_ID, - getter.getConversationId(request)); - internalSet( - attributes, - SemanticAttributes.MESSAGING_MESSAGE_PAYLOAD_SIZE_BYTES, - getter.getMessagePayloadSize(request)); - internalSet( - attributes, - SemanticAttributes.MESSAGING_MESSAGE_PAYLOAD_COMPRESSED_SIZE_BYTES, - getter.getMessagePayloadCompressedSize(request)); - if (operation == RECEIVE || operation == PROCESS) { - internalSet(attributes, SemanticAttributes.MESSAGING_OPERATION, operation.operationName()); - } - } - - @Override - public void onEnd( - AttributesBuilder attributes, - Context context, - REQUEST request, - @Nullable RESPONSE response, - @Nullable Throwable error) { - internalSet( - attributes, - SemanticAttributes.MESSAGING_MESSAGE_ID, - getter.getMessageId(request, response)); - - for (String name : capturedHeaders) { - List values = getter.getMessageHeader(request, name); - if (!values.isEmpty()) { - internalSet(attributes, attributeKey(name), values); - } - } - } - - /** - * This method is internal and is hence not for public use. Its API is unstable and can change at - * any time. - */ - @Override - public SpanKey internalGetSpanKey() { - switch (operation) { - case SEND: - return SpanKey.PRODUCER; - case RECEIVE: - return SpanKey.CONSUMER_RECEIVE; - case PROCESS: - return SpanKey.CONSUMER_PROCESS; - } - throw new IllegalStateException("Can't possibly happen"); - } -} diff --git a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/net/NetClientAttributesExtractor.java b/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/net/NetClientAttributesExtractor.java deleted file mode 100644 index 016ca5e24d79..000000000000 --- a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/net/NetClientAttributesExtractor.java +++ /dev/null @@ -1,75 +0,0 @@ -/* - * 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.context.Context; -import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.net.internal.FallbackNamePortGetter; -import io.opentelemetry.instrumentation.api.instrumenter.net.internal.InternalNetClientAttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.network.internal.InternalNetworkAttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.network.internal.InternalServerAttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.network.internal.NetworkTransportFilter; -import io.opentelemetry.instrumentation.api.internal.SemconvStability; -import javax.annotation.Nullable; - -/** - * Extractor of Network - * attributes. - * - *

This class delegates to a type-specific {@link NetClientAttributesGetter} for individual - * attribute extraction from request/response objects. - */ -public final class NetClientAttributesExtractor - implements AttributesExtractor { - - private final InternalNetClientAttributesExtractor internalExtractor; - private final InternalNetworkAttributesExtractor internalNetworkExtractor; - private final InternalServerAttributesExtractor internalServerExtractor; - - public static AttributesExtractor create( - NetClientAttributesGetter getter) { - return new NetClientAttributesExtractor<>(getter); - } - - private NetClientAttributesExtractor(NetClientAttributesGetter getter) { - internalExtractor = - new InternalNetClientAttributesExtractor<>( - getter, FallbackNamePortGetter.noop(), SemconvStability.emitOldHttpSemconv()); - internalNetworkExtractor = - new InternalNetworkAttributesExtractor<>( - getter, - NetworkTransportFilter.alwaysTrue(), - SemconvStability.emitStableHttpSemconv(), - SemconvStability.emitOldHttpSemconv()); - internalServerExtractor = - new InternalServerAttributesExtractor<>( - getter, - (port, request) -> true, - FallbackNamePortGetter.noop(), - SemconvStability.emitStableHttpSemconv(), - SemconvStability.emitOldHttpSemconv(), - InternalServerAttributesExtractor.Mode.PEER); - } - - @Override - public void onStart(AttributesBuilder attributes, Context parentContext, REQUEST request) { - internalServerExtractor.onStart(attributes, request); - } - - @Override - public void onEnd( - AttributesBuilder attributes, - Context context, - REQUEST request, - @Nullable RESPONSE response, - @Nullable Throwable error) { - internalExtractor.onEnd(attributes, request, response); - internalNetworkExtractor.onEnd(attributes, request, response); - internalServerExtractor.onEnd(attributes, request, response); - } -} diff --git a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/net/NetClientAttributesGetter.java b/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/net/NetClientAttributesGetter.java deleted file mode 100644 index aa3ffbf8520b..000000000000 --- a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/net/NetClientAttributesGetter.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.api.instrumenter.net; - -import io.opentelemetry.instrumentation.api.instrumenter.net.internal.InetSocketAddressUtil; -import io.opentelemetry.instrumentation.api.instrumenter.network.NetworkAttributesGetter; -import io.opentelemetry.instrumentation.api.instrumenter.network.ServerAttributesGetter; -import java.net.InetSocketAddress; -import javax.annotation.Nullable; - -/** - * An interface for getting client-based network attributes. It adapts from a type-specific request - * and response into the 4 common network attribute values. - * - *

Instrumentation authors will create implementations of this interface for their specific - * library/framework. It will be used by the NetClientAttributesExtractor to obtain the various - * network attributes in a type-generic way. - */ -public interface NetClientAttributesGetter - extends NetworkAttributesGetter, ServerAttributesGetter { - - @Nullable - default String getTransport(REQUEST request, @Nullable RESPONSE response) { - return null; - } - - /** - * Returns the protocol address family which - * is used for communication. - * - *

Examples: {@code inet}, {@code inet6} - * - *

By default, this method attempts to retrieve the address family using the {@link - * #getServerInetSocketAddress(Object, Object)} method. If it is not implemented, it will simply - * return {@code null}. If the instrumented library does not expose {@link InetSocketAddress} in - * its API, you might want to implement this method instead of {@link - * #getServerSocketAddress(Object, Object)}. - */ - @Nullable - default String getSockFamily(REQUEST request, @Nullable RESPONSE response) { - return InetSocketAddressUtil.getSockFamily(getServerInetSocketAddress(request, response), null); - } - - /** {@inheritDoc} */ - @Nullable - @Override - default String getNetworkType(REQUEST request, @Nullable RESPONSE response) { - return InetSocketAddressUtil.getNetworkType( - getServerInetSocketAddress(request, response), null); - } -} diff --git a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/net/NetServerAttributesExtractor.java b/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/net/NetServerAttributesExtractor.java deleted file mode 100644 index ddbfc8d1f32d..000000000000 --- a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/net/NetServerAttributesExtractor.java +++ /dev/null @@ -1,82 +0,0 @@ -/* - * 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.context.Context; -import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.net.internal.FallbackNamePortGetter; -import io.opentelemetry.instrumentation.api.instrumenter.net.internal.InternalNetServerAttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.network.internal.InternalClientAttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.network.internal.InternalNetworkAttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.network.internal.InternalServerAttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.network.internal.NetworkTransportFilter; -import io.opentelemetry.instrumentation.api.internal.SemconvStability; -import javax.annotation.Nullable; - -/** - * Extractor of Network - * attributes. - */ -public final class NetServerAttributesExtractor - implements AttributesExtractor { - - public static AttributesExtractor create( - NetServerAttributesGetter getter) { - return new NetServerAttributesExtractor<>(getter); - } - - private final InternalNetServerAttributesExtractor internalExtractor; - private final InternalNetworkAttributesExtractor internalNetworkExtractor; - private final InternalServerAttributesExtractor internalServerExtractor; - private final InternalClientAttributesExtractor internalClientExtractor; - - private NetServerAttributesExtractor(NetServerAttributesGetter getter) { - internalExtractor = - new InternalNetServerAttributesExtractor<>( - getter, FallbackNamePortGetter.noop(), SemconvStability.emitOldHttpSemconv()); - internalNetworkExtractor = - new InternalNetworkAttributesExtractor<>( - getter, - NetworkTransportFilter.alwaysTrue(), - SemconvStability.emitStableHttpSemconv(), - SemconvStability.emitOldHttpSemconv()); - internalServerExtractor = - new InternalServerAttributesExtractor<>( - getter, - (port, request) -> true, - FallbackNamePortGetter.noop(), - SemconvStability.emitStableHttpSemconv(), - SemconvStability.emitOldHttpSemconv(), - InternalServerAttributesExtractor.Mode.HOST); - internalClientExtractor = - new InternalClientAttributesExtractor<>( - getter, - FallbackNamePortGetter.noop(), - SemconvStability.emitStableHttpSemconv(), - SemconvStability.emitOldHttpSemconv()); - } - - @Override - public void onStart(AttributesBuilder attributes, Context parentContext, REQUEST request) { - internalExtractor.onStart(attributes, request); - internalServerExtractor.onStart(attributes, request); - internalClientExtractor.onStart(attributes, request); - } - - @Override - public void onEnd( - AttributesBuilder attributes, - Context context, - REQUEST request, - @Nullable RESPONSE response, - @Nullable Throwable error) { - internalNetworkExtractor.onEnd(attributes, request, response); - internalServerExtractor.onEnd(attributes, request, response); - internalClientExtractor.onEnd(attributes, request, response); - } -} diff --git a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/net/NetServerAttributesGetter.java b/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/net/NetServerAttributesGetter.java deleted file mode 100644 index fe42893752b4..000000000000 --- a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/net/NetServerAttributesGetter.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.api.instrumenter.net; - -import io.opentelemetry.instrumentation.api.instrumenter.net.internal.InetSocketAddressUtil; -import io.opentelemetry.instrumentation.api.instrumenter.network.ClientAttributesGetter; -import io.opentelemetry.instrumentation.api.instrumenter.network.NetworkAttributesGetter; -import io.opentelemetry.instrumentation.api.instrumenter.network.ServerAttributesGetter; -import java.net.InetSocketAddress; -import javax.annotation.Nullable; - -/** - * An interface for getting server-based network attributes. It adapts an instrumentation-specific - * request type into the 3 common attributes (transport, sockPeerPort, sockPeerAddr). - * - *

Instrumentation authors will create implementations of this interface for their specific - * server library/framework. It will be used by the {@link NetServerAttributesExtractor} to obtain - * the various network attributes in a type-generic way. - */ -public interface NetServerAttributesGetter - extends NetworkAttributesGetter, - ServerAttributesGetter, - ClientAttributesGetter { - - @Nullable - default String getTransport(REQUEST request) { - return null; - } - - /** - * Returns the protocol address family which - * is used for communication. - * - *

Examples: `inet`, `inet6`. - * - *

By default, this method attempts to retrieve the address family using one of the {@link - * #getClientInetSocketAddress(Object, Object)} and {@link #getServerInetSocketAddress(Object, - * Object)} methods. If neither of these methods is implemented, it will simply return {@code - * null}. If the instrumented library does not expose {@link InetSocketAddress} in its API, you - * might want to implement this method instead of {@link #getClientInetSocketAddress(Object, - * Object)} and {@link #getServerInetSocketAddress(Object, Object)}. - */ - @Nullable - default String getSockFamily(REQUEST request) { - return InetSocketAddressUtil.getSockFamily( - getClientInetSocketAddress(request, null), getServerInetSocketAddress(request, null)); - } - - /** {@inheritDoc} */ - @Nullable - @Override - default String getNetworkType(REQUEST request, @Nullable RESPONSE response) { - return InetSocketAddressUtil.getNetworkType( - getClientInetSocketAddress(request, response), - getServerInetSocketAddress(request, response)); - } -} diff --git a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/net/PeerServiceAttributesExtractor.java b/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/net/PeerServiceAttributesExtractor.java deleted file mode 100644 index 8744b5f1845e..000000000000 --- a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/net/PeerServiceAttributesExtractor.java +++ /dev/null @@ -1,79 +0,0 @@ -/* - * 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.context.Context; -import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.network.ServerAttributesGetter; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; -import java.util.Map; -import javax.annotation.Nullable; - -/** - * Extractor of the {@code peer.service} span attribute, described in the - * specification. - */ -public final class PeerServiceAttributesExtractor - implements AttributesExtractor { - - private final ServerAttributesGetter attributesGetter; - private final Map peerServiceMapping; - - // visible for tests - PeerServiceAttributesExtractor( - ServerAttributesGetter attributesGetter, - Map peerServiceMapping) { - this.attributesGetter = attributesGetter; - this.peerServiceMapping = peerServiceMapping; - } - - /** - * 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 AttributesExtractor create( - ServerAttributesGetter attributesGetter, - Map peerServiceMapping) { - return new PeerServiceAttributesExtractor<>(attributesGetter, peerServiceMapping); - } - - @Override - public void onStart(AttributesBuilder attributes, Context parentContext, REQUEST request) {} - - @Override - public void onEnd( - AttributesBuilder attributes, - Context context, - REQUEST request, - @Nullable RESPONSE response, - @Nullable Throwable error) { - - if (peerServiceMapping.isEmpty()) { - // optimization for common case - return; - } - - String serverAddress = attributesGetter.getServerAddress(request); - String peerService = mapToPeerService(serverAddress); - if (peerService == null) { - String serverSocketDomain = attributesGetter.getServerSocketDomain(request, response); - peerService = mapToPeerService(serverSocketDomain); - } - if (peerService != null) { - attributes.put(SemanticAttributes.PEER_SERVICE, peerService); - } - } - - @Nullable - private String mapToPeerService(@Nullable String endpoint) { - if (endpoint == null) { - return null; - } - return peerServiceMapping.get(endpoint); - } -} diff --git a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/net/internal/FallbackNamePortGetter.java b/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/net/internal/FallbackNamePortGetter.java deleted file mode 100644 index 93fc1d468eab..000000000000 --- a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/net/internal/FallbackNamePortGetter.java +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.api.instrumenter.net.internal; - -import javax.annotation.Nullable; - -/** - * This class is internal and is hence not for public use. Its APIs are unstable and can change at - * any time. - */ -public interface FallbackNamePortGetter { - - @Nullable - String name(REQUEST request); - - @Nullable - Integer port(REQUEST request); - - @SuppressWarnings("unchecked") - static FallbackNamePortGetter noop() { - return (FallbackNamePortGetter) NoopNamePortGetter.INSTANCE; - } -} diff --git a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/net/internal/InternalNetClientAttributesExtractor.java b/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/net/internal/InternalNetClientAttributesExtractor.java deleted file mode 100644 index 50c4e3649f67..000000000000 --- a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/net/internal/InternalNetClientAttributesExtractor.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.api.instrumenter.net.internal; - -import static io.opentelemetry.instrumentation.api.internal.AttributesExtractorUtil.internalSet; - -import io.opentelemetry.api.common.AttributesBuilder; -import io.opentelemetry.instrumentation.api.instrumenter.net.NetClientAttributesGetter; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; -import javax.annotation.Nullable; - -/** - * This class is internal and is hence not for public use. Its APIs are unstable and can change at - * any time. - */ -public final class InternalNetClientAttributesExtractor { - - private final NetClientAttributesGetter getter; - private final FallbackNamePortGetter fallbackNamePortGetter; - private final boolean emitOldHttpAttributes; - - public InternalNetClientAttributesExtractor( - NetClientAttributesGetter getter, - FallbackNamePortGetter fallbackNamePortGetter, - boolean emitOldHttpAttributes) { - this.getter = getter; - this.fallbackNamePortGetter = fallbackNamePortGetter; - this.emitOldHttpAttributes = emitOldHttpAttributes; - } - - public void onEnd(AttributesBuilder attributes, REQUEST request, @Nullable RESPONSE response) { - - if (emitOldHttpAttributes) { - internalSet( - attributes, SemanticAttributes.NET_TRANSPORT, getter.getTransport(request, response)); - - String peerName = extractPeerName(request); - String sockPeerAddr = getter.getServerSocketAddress(request, response); - if (sockPeerAddr != null && !sockPeerAddr.equals(peerName)) { - String sockFamily = getter.getSockFamily(request, response); - if (sockFamily != null && !SemanticAttributes.NetSockFamilyValues.INET.equals(sockFamily)) { - internalSet(attributes, SemanticAttributes.NET_SOCK_FAMILY, sockFamily); - } - } - } - } - - private String extractPeerName(REQUEST request) { - String peerName = getter.getServerAddress(request); - if (peerName == null) { - peerName = fallbackNamePortGetter.name(request); - } - return peerName; - } -} diff --git a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/net/internal/InternalNetServerAttributesExtractor.java b/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/net/internal/InternalNetServerAttributesExtractor.java deleted file mode 100644 index abd751fd008e..000000000000 --- a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/net/internal/InternalNetServerAttributesExtractor.java +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.api.instrumenter.net.internal; - -import static io.opentelemetry.instrumentation.api.internal.AttributesExtractorUtil.internalSet; - -import io.opentelemetry.api.common.AttributesBuilder; -import io.opentelemetry.instrumentation.api.instrumenter.net.NetServerAttributesGetter; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; - -/** - * This class is internal and is hence not for public use. Its APIs are unstable and can change at - * any time. - */ -public final class InternalNetServerAttributesExtractor { - - private final NetServerAttributesGetter getter; - private final FallbackNamePortGetter fallbackNamePortGetter; - private final boolean emitOldHttpAttributes; - - public InternalNetServerAttributesExtractor( - NetServerAttributesGetter getter, - FallbackNamePortGetter fallbackNamePortGetter, - boolean emitOldHttpAttributes) { - this.getter = getter; - this.fallbackNamePortGetter = fallbackNamePortGetter; - this.emitOldHttpAttributes = emitOldHttpAttributes; - } - - public void onStart(AttributesBuilder attributes, REQUEST request) { - - if (emitOldHttpAttributes) { - internalSet(attributes, SemanticAttributes.NET_TRANSPORT, getter.getTransport(request)); - - boolean setSockFamily = false; - - String clientSocketAddress = getter.getClientSocketAddress(request, null); - if (clientSocketAddress != null) { - setSockFamily = true; - } - - String serverSocketAddress = getter.getServerSocketAddress(request, null); - if (serverSocketAddress != null - && !serverSocketAddress.equals(extractServerAddress(request))) { - setSockFamily = true; - } - - if (setSockFamily) { - String sockFamily = getter.getSockFamily(request); - if (sockFamily != null && !SemanticAttributes.NetSockFamilyValues.INET.equals(sockFamily)) { - internalSet(attributes, SemanticAttributes.NET_SOCK_FAMILY, sockFamily); - } - } - } - } - - private String extractServerAddress(REQUEST request) { - String serverAddress = getter.getServerAddress(request); - if (serverAddress == null) { - serverAddress = fallbackNamePortGetter.name(request); - } - return serverAddress; - } -} diff --git a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/net/internal/NetAttributes.java b/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/net/internal/NetAttributes.java deleted file mode 100644 index 1a23d126cbd6..000000000000 --- a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/net/internal/NetAttributes.java +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.api.instrumenter.net.internal; - -import static io.opentelemetry.api.common.AttributeKey.stringKey; - -import io.opentelemetry.api.common.AttributeKey; - -/** - * This class is internal and is hence not for public use. Its APIs are unstable and can change at - * any time. - */ -public final class NetAttributes { - - // FIXME: remove this class and replace its usages with SemanticAttributes once schema 1.20 is - // released - - public static final AttributeKey NET_PROTOCOL_NAME = stringKey("net.protocol.name"); - - public static final AttributeKey NET_PROTOCOL_VERSION = stringKey("net.protocol.version"); - - private NetAttributes() {} -} diff --git a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/net/internal/NoopNamePortGetter.java b/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/net/internal/NoopNamePortGetter.java deleted file mode 100644 index 27b97b57a54f..000000000000 --- a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/net/internal/NoopNamePortGetter.java +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.api.instrumenter.net.internal; - -import javax.annotation.Nullable; - -enum NoopNamePortGetter implements FallbackNamePortGetter { - INSTANCE; - - @Nullable - @Override - public String name(Object o) { - return null; - } - - @Nullable - @Override - public Integer port(Object o) { - return null; - } -} diff --git a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/network/ClientAttributesGetter.java b/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/network/ClientAttributesGetter.java deleted file mode 100644 index 788854d7ec2b..000000000000 --- a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/network/ClientAttributesGetter.java +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.api.instrumenter.network; - -import io.opentelemetry.instrumentation.api.instrumenter.net.internal.InetSocketAddressUtil; -import java.net.InetSocketAddress; -import javax.annotation.Nullable; - -/** - * An interface for getting attributes describing a network client. - * - *

Instrumentation authors will create implementations of this interface for their specific - * library/framework. It will be used by the {@link ClientAttributesExtractor} (or other convention - * specific extractors) to obtain the various server attributes in a type-generic way. - */ -public interface ClientAttributesGetter { - - /** - * Returns the client address - unix domain socket name, IPv4 or IPv6 address. - * - *

Examples: {@code /tmp/my.sock}, {@code 10.1.2.80} - */ - @Nullable - default String getClientAddress(REQUEST request) { - return null; - } - - /** - * Returns the client port number. - * - *

Examples: {@code 65123} - */ - @Nullable - default Integer getClientPort(REQUEST request) { - return null; - } - - /** - * Returns an {@link InetSocketAddress} object representing the immediate client socket address. - * - *

Implementing this method is equivalent to implementing all of {@link - * #getClientSocketAddress(Object, Object)} and {@link #getClientSocketPort(Object, Object)}. - */ - @Nullable - default InetSocketAddress getClientInetSocketAddress( - REQUEST request, @Nullable RESPONSE response) { - return null; - } - - /** - * Returns the immediate client peer address - unix domain socket name, IPv4 or IPv6 address. - * - *

Examples: {@code /tmp/my.sock}, {@code 127.0.0.1} - * - *

By default, this method attempts to retrieve the immediate client address using the {@link - * #getClientInetSocketAddress(Object, Object)} method. If this method is not implemented, it will - * simply return {@code null}. If the instrumented library does not expose {@link - * InetSocketAddress} in its API, you might want to implement this method instead of {@link - * #getClientInetSocketAddress(Object, Object)}. - */ - @Nullable - default String getClientSocketAddress(REQUEST request, @Nullable RESPONSE response) { - return InetSocketAddressUtil.getIpAddress(getClientInetSocketAddress(request, response)); - } - - /** - * Returns the immediate client peer port number. - * - *

Examples: {@code 35555} - * - *

By default, this method attempts to retrieve the immediate client port using the {@link - * #getClientInetSocketAddress(Object, Object)} method. If this method is not implemented, it will - * simply return {@code null}. If the instrumented library does not expose {@link - * InetSocketAddress} in its API, you might want to implement this method instead of {@link - * #getClientInetSocketAddress(Object, Object)}. - */ - @Nullable - default Integer getClientSocketPort(REQUEST request, @Nullable RESPONSE response) { - return InetSocketAddressUtil.getPort(getClientInetSocketAddress(request, response)); - } -} diff --git a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/network/NetworkAttributesGetter.java b/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/network/NetworkAttributesGetter.java deleted file mode 100644 index 0c172d253eec..000000000000 --- a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/network/NetworkAttributesGetter.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.api.instrumenter.network; - -import javax.annotation.Nullable; - -/** - * An interface for getting network attributes. - * - *

Instrumentation authors will create implementations of this interface for their specific - * library/framework. It will be used by the {@link NetworkAttributesExtractor} (or other convention - * specific extractors) to obtain the various network attributes in a type-generic way. - */ -public interface NetworkAttributesGetter { - - /** - * Returns the OSI Transport Layer or Inter-process Communication - * method. - * - *

Examples: {@code tcp}, {@code udp} - */ - @Nullable - default String getNetworkTransport(REQUEST request, @Nullable RESPONSE response) { - return null; - } - - /** - * Returns the OSI Network Layer or non-OSI - * equivalent. - * - *

Examples: {@code ipv4}, {@code ipv6} - */ - @Nullable - default String getNetworkType(REQUEST request, @Nullable RESPONSE response) { - return null; - } - - /** - * Returns the OSI Application Layer or - * non-OSI equivalent. - * - *

Examples: {@code ampq}, {@code http}, {@code mqtt} - */ - @Nullable - default String getNetworkProtocolName(REQUEST request, @Nullable RESPONSE response) { - return null; - } - - /** - * Returns the version of the application layer protocol used. - * - *

Examples: {@code 3.1.1} - */ - @Nullable - default String getNetworkProtocolVersion(REQUEST request, @Nullable RESPONSE response) { - return null; - } -} diff --git a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/network/ServerAttributesExtractor.java b/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/network/ServerAttributesExtractor.java deleted file mode 100644 index c622da9adb64..000000000000 --- a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/network/ServerAttributesExtractor.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.api.instrumenter.network; - -import io.opentelemetry.api.common.AttributesBuilder; -import io.opentelemetry.context.Context; -import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.net.internal.FallbackNamePortGetter; -import io.opentelemetry.instrumentation.api.instrumenter.network.internal.InternalServerAttributesExtractor; -import javax.annotation.Nullable; - -/** - * Extractor of server - * attributes. - */ -public final class ServerAttributesExtractor - implements AttributesExtractor { - - /** - * Returns a new {@link ServerAttributesExtractor} that will use the passed {@link - * ServerAttributesGetter}. - */ - public static ServerAttributesExtractor create( - ServerAttributesGetter getter) { - return new ServerAttributesExtractor<>(getter); - } - - private final InternalServerAttributesExtractor internalExtractor; - - ServerAttributesExtractor(ServerAttributesGetter getter) { - // the ServerAttributesExtractor will always emit new semconv - internalExtractor = - new InternalServerAttributesExtractor<>( - getter, - (port, request) -> true, - FallbackNamePortGetter.noop(), - /* emitStableUrlAttributes= */ true, - /* emitOldHttpAttributes= */ false, - // this param does not matter when old semconv is off - InternalServerAttributesExtractor.Mode.HOST); - } - - @Override - public void onStart(AttributesBuilder attributes, Context parentContext, REQUEST request) { - internalExtractor.onStart(attributes, request); - } - - @Override - public void onEnd( - AttributesBuilder attributes, - Context context, - REQUEST request, - @Nullable RESPONSE response, - @Nullable Throwable error) { - internalExtractor.onEnd(attributes, request, response); - } -} diff --git a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/network/ServerAttributesGetter.java b/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/network/ServerAttributesGetter.java deleted file mode 100644 index 672f79a1b897..000000000000 --- a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/network/ServerAttributesGetter.java +++ /dev/null @@ -1,102 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.api.instrumenter.network; - -import io.opentelemetry.instrumentation.api.instrumenter.net.internal.InetSocketAddressUtil; -import java.net.InetSocketAddress; -import javax.annotation.Nullable; - -/** - * An interface for getting attributes describing a network server. - * - *

Instrumentation authors will create implementations of this interface for their specific - * library/framework. It will be used by the {@link ServerAttributesExtractor} (or other convention - * specific extractors) to obtain the various server attributes in a type-generic way. - */ -public interface ServerAttributesGetter { - - /** - * Return the logical server hostname that matches server FQDN if available, and IP or socket - * address if FQDN is not known. - * - *

Examples: {@code example.com} - */ - @Nullable - default String getServerAddress(REQUEST request) { - return null; - } - - /** - * Return the logical server port number. - * - *

Examples: {@code 80}, {@code 8080}, {@code 443} - */ - @Nullable - default Integer getServerPort(REQUEST request) { - return null; - } - - /** - * Returns an {@link InetSocketAddress} object representing the server socket address. - * - *

Implementing this method is equivalent to implementing all of {@link - * #getServerSocketDomain(Object, Object)}, {@link #getServerSocketAddress(Object, Object)} and - * {@link #getServerSocketPort(Object, Object)}. - */ - @Nullable - default InetSocketAddress getServerInetSocketAddress( - REQUEST request, @Nullable RESPONSE response) { - return null; - } - - /** - * Return the domain name of an immediate peer. - * - *

Examples: {@code proxy.example.com} - * - *

By default, this method attempts to retrieve the server domain name using the {@link - * #getServerInetSocketAddress(Object, Object)} method. If this method is not implemented, it will - * simply return {@code null}. If the instrumented library does not expose {@link - * InetSocketAddress} in its API, you might want to implement this method instead of {@link - * #getServerInetSocketAddress(Object, Object)}. - */ - @Nullable - default String getServerSocketDomain(REQUEST request, @Nullable RESPONSE response) { - return InetSocketAddressUtil.getDomainName(getServerInetSocketAddress(request, response)); - } - - /** - * Return the physical server IP address or Unix socket address. - * - *

Examples: {@code 10.5.3.2} - * - *

By default, this method attempts to retrieve the server address using the {@link - * #getServerInetSocketAddress(Object, Object)} method. If this method is not implemented, it will - * simply return {@code null}. If the instrumented library does not expose {@link - * InetSocketAddress} in its API, you might want to implement this method instead of {@link - * #getServerInetSocketAddress(Object, Object)}. - */ - @Nullable - default String getServerSocketAddress(REQUEST request, @Nullable RESPONSE response) { - return InetSocketAddressUtil.getIpAddress(getServerInetSocketAddress(request, response)); - } - - /** - * Return the physical server port. - * - *

Examples: {@code 16456} - * - *

By default, this method attempts to retrieve the server port using the {@link - * #getServerInetSocketAddress(Object, Object)} method. If this method is not implemented, it will - * simply return {@code null}. If the instrumented library does not expose {@link - * InetSocketAddress} in its API, you might want to implement this method instead of {@link - * #getServerInetSocketAddress(Object, Object)}. - */ - @Nullable - default Integer getServerSocketPort(REQUEST request, @Nullable RESPONSE response) { - return InetSocketAddressUtil.getPort(getServerInetSocketAddress(request, response)); - } -} diff --git a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/network/internal/InternalClientAttributesExtractor.java b/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/network/internal/InternalClientAttributesExtractor.java deleted file mode 100644 index d71b18e205d6..000000000000 --- a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/network/internal/InternalClientAttributesExtractor.java +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.api.instrumenter.network.internal; - -import static io.opentelemetry.instrumentation.api.internal.AttributesExtractorUtil.internalSet; - -import io.opentelemetry.api.common.AttributesBuilder; -import io.opentelemetry.instrumentation.api.instrumenter.net.internal.FallbackNamePortGetter; -import io.opentelemetry.instrumentation.api.instrumenter.network.ClientAttributesGetter; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; -import javax.annotation.Nullable; - -/** - * This class is internal and is hence not for public use. Its APIs are unstable and can change at - * any time. - */ -public final class InternalClientAttributesExtractor { - - private final ClientAttributesGetter getter; - private final FallbackNamePortGetter fallbackNamePortGetter; - private final boolean emitStableUrlAttributes; - private final boolean emitOldHttpAttributes; - - public InternalClientAttributesExtractor( - ClientAttributesGetter getter, - FallbackNamePortGetter fallbackNamePortGetter, - boolean emitStableUrlAttributes, - boolean emitOldHttpAttributes) { - this.getter = getter; - this.fallbackNamePortGetter = fallbackNamePortGetter; - this.emitStableUrlAttributes = emitStableUrlAttributes; - this.emitOldHttpAttributes = emitOldHttpAttributes; - } - - public void onStart(AttributesBuilder attributes, REQUEST request) { - String clientAddress = extractClientAddress(request); - if (emitStableUrlAttributes) { - internalSet(attributes, NetworkAttributes.CLIENT_ADDRESS, clientAddress); - Integer clientPort = extractClientPort(request); - if (clientPort != null && clientPort > 0) { - internalSet(attributes, NetworkAttributes.CLIENT_PORT, (long) clientPort); - } - } - if (emitOldHttpAttributes) { - internalSet(attributes, SemanticAttributes.HTTP_CLIENT_IP, clientAddress); - } - } - - public void onEnd(AttributesBuilder attributes, REQUEST request, @Nullable RESPONSE response) { - String clientAddress = extractClientAddress(request); - String clientSocketAddress = getter.getClientSocketAddress(request, response); - Integer clientSocketPort = getter.getClientSocketPort(request, response); - - if (clientSocketAddress != null && !clientSocketAddress.equals(clientAddress)) { - if (emitStableUrlAttributes) { - internalSet(attributes, NetworkAttributes.CLIENT_SOCKET_ADDRESS, clientSocketAddress); - } - if (emitOldHttpAttributes) { - internalSet(attributes, SemanticAttributes.NET_SOCK_PEER_ADDR, clientSocketAddress); - } - } - if (clientSocketPort != null && clientSocketPort > 0) { - if (emitStableUrlAttributes) { - Integer clientPort = extractClientPort(request); - if (!clientSocketPort.equals(clientPort)) { - internalSet(attributes, NetworkAttributes.CLIENT_SOCKET_PORT, (long) clientSocketPort); - } - } - if (emitOldHttpAttributes) { - internalSet(attributes, SemanticAttributes.NET_SOCK_PEER_PORT, (long) clientSocketPort); - } - } - } - - private String extractClientAddress(REQUEST request) { - String clientAddress = getter.getClientAddress(request); - if (clientAddress == null) { - clientAddress = fallbackNamePortGetter.name(request); - } - return clientAddress; - } - - private Integer extractClientPort(REQUEST request) { - Integer clientPort = getter.getClientPort(request); - if (clientPort == null) { - clientPort = fallbackNamePortGetter.port(request); - } - return clientPort; - } -} diff --git a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/network/internal/InternalServerAttributesExtractor.java b/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/network/internal/InternalServerAttributesExtractor.java deleted file mode 100644 index 68e4ef1db9ce..000000000000 --- a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/network/internal/InternalServerAttributesExtractor.java +++ /dev/null @@ -1,163 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.api.instrumenter.network.internal; - -import static io.opentelemetry.instrumentation.api.internal.AttributesExtractorUtil.internalSet; - -import io.opentelemetry.api.common.AttributeKey; -import io.opentelemetry.api.common.AttributesBuilder; -import io.opentelemetry.instrumentation.api.instrumenter.net.internal.FallbackNamePortGetter; -import io.opentelemetry.instrumentation.api.instrumenter.network.ServerAttributesGetter; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; -import java.util.function.BiPredicate; -import javax.annotation.Nullable; - -/** - * This class is internal and is hence not for public use. Its APIs are unstable and can change at - * any time. - */ -public final class InternalServerAttributesExtractor { - - private final ServerAttributesGetter getter; - private final BiPredicate captureServerPortCondition; - private final FallbackNamePortGetter fallbackNamePortGetter; - private final boolean emitStableUrlAttributes; - private final boolean emitOldHttpAttributes; - private final Mode oldSemconvMode; - - public InternalServerAttributesExtractor( - ServerAttributesGetter getter, - BiPredicate captureServerPortCondition, - FallbackNamePortGetter fallbackNamePortGetter, - boolean emitStableUrlAttributes, - boolean emitOldHttpAttributes, - Mode oldSemconvMode) { - this.getter = getter; - this.captureServerPortCondition = captureServerPortCondition; - this.fallbackNamePortGetter = fallbackNamePortGetter; - this.emitStableUrlAttributes = emitStableUrlAttributes; - this.emitOldHttpAttributes = emitOldHttpAttributes; - this.oldSemconvMode = oldSemconvMode; - } - - public void onStart(AttributesBuilder attributes, REQUEST request) { - String serverAddress = extractServerAddress(request); - - if (serverAddress != null) { - if (emitStableUrlAttributes) { - internalSet(attributes, NetworkAttributes.SERVER_ADDRESS, serverAddress); - } - if (emitOldHttpAttributes) { - internalSet(attributes, oldSemconvMode.address, serverAddress); - } - - Integer serverPort = extractServerPort(request); - if (serverPort != null - && serverPort > 0 - && captureServerPortCondition.test(serverPort, request)) { - if (emitStableUrlAttributes) { - internalSet(attributes, NetworkAttributes.SERVER_PORT, (long) serverPort); - } - if (emitOldHttpAttributes) { - internalSet(attributes, oldSemconvMode.port, (long) serverPort); - } - } - } - } - - public void onEnd(AttributesBuilder attributes, REQUEST request, @Nullable RESPONSE response) { - String serverAddress = extractServerAddress(request); - - String serverSocketAddress = getter.getServerSocketAddress(request, response); - if (serverSocketAddress != null && !serverSocketAddress.equals(serverAddress)) { - if (emitStableUrlAttributes) { - internalSet(attributes, NetworkAttributes.SERVER_SOCKET_ADDRESS, serverSocketAddress); - } - if (emitOldHttpAttributes) { - internalSet(attributes, oldSemconvMode.socketAddress, serverSocketAddress); - } - - Integer serverPort = extractServerPort(request); - Integer serverSocketPort = getter.getServerSocketPort(request, response); - if (serverSocketPort != null - && serverSocketPort > 0 - && !serverSocketPort.equals(serverPort)) { - if (emitStableUrlAttributes) { - internalSet(attributes, NetworkAttributes.SERVER_SOCKET_PORT, (long) serverSocketPort); - } - if (emitOldHttpAttributes) { - internalSet(attributes, oldSemconvMode.socketPort, (long) serverSocketPort); - } - } - - String serverSocketDomain = getter.getServerSocketDomain(request, response); - if (serverSocketDomain != null && !serverSocketDomain.equals(serverAddress)) { - if (emitStableUrlAttributes) { - internalSet(attributes, NetworkAttributes.SERVER_SOCKET_DOMAIN, serverSocketDomain); - } - if (emitOldHttpAttributes && oldSemconvMode.socketDomain != null) { - internalSet(attributes, oldSemconvMode.socketDomain, serverSocketDomain); - } - } - } - } - - private String extractServerAddress(REQUEST request) { - String serverAddress = getter.getServerAddress(request); - if (serverAddress == null) { - serverAddress = fallbackNamePortGetter.name(request); - } - return serverAddress; - } - - private Integer extractServerPort(REQUEST request) { - Integer serverPort = getter.getServerPort(request); - if (serverPort == null) { - serverPort = fallbackNamePortGetter.port(request); - } - return serverPort; - } - - /** - * This class is internal and is hence not for public use. Its APIs are unstable and can change at - * any time. - */ - @SuppressWarnings("ImmutableEnumChecker") - public enum Mode { - PEER( - SemanticAttributes.NET_PEER_NAME, - SemanticAttributes.NET_PEER_PORT, - SemanticAttributes.NET_SOCK_PEER_NAME, - SemanticAttributes.NET_SOCK_PEER_ADDR, - SemanticAttributes.NET_SOCK_PEER_PORT), - HOST( - SemanticAttributes.NET_HOST_NAME, - SemanticAttributes.NET_HOST_PORT, - // the old semconv does not have an attribute for this - null, - SemanticAttributes.NET_SOCK_HOST_ADDR, - SemanticAttributes.NET_SOCK_HOST_PORT); - - final AttributeKey address; - final AttributeKey port; - @Nullable final AttributeKey socketDomain; - final AttributeKey socketAddress; - final AttributeKey socketPort; - - Mode( - AttributeKey address, - AttributeKey port, - AttributeKey socketDomain, - AttributeKey socketAddress, - AttributeKey socketPort) { - this.address = address; - this.port = port; - this.socketDomain = socketDomain; - this.socketAddress = socketAddress; - this.socketPort = socketPort; - } - } -} diff --git a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/network/internal/NetworkAttributes.java b/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/network/internal/NetworkAttributes.java deleted file mode 100644 index 1d8c57a30cfc..000000000000 --- a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/network/internal/NetworkAttributes.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.api.instrumenter.network.internal; - -import static io.opentelemetry.api.common.AttributeKey.longKey; -import static io.opentelemetry.api.common.AttributeKey.stringKey; - -import io.opentelemetry.api.common.AttributeKey; - -/** - * This class is internal and is hence not for public use. Its APIs are unstable and can change at - * any time. - */ -public final class NetworkAttributes { - - // FIXME: remove this class and replace its usages with SemanticAttributes once schema 1.20 is - // released - - public static final AttributeKey NETWORK_TRANSPORT = stringKey("network.transport"); - - public static final AttributeKey NETWORK_TYPE = stringKey("network.type"); - - public static final AttributeKey NETWORK_PROTOCOL_NAME = - stringKey("network.protocol.name"); - - public static final AttributeKey NETWORK_PROTOCOL_VERSION = - stringKey("network.protocol.version"); - - public static final AttributeKey SERVER_ADDRESS = stringKey("server.address"); - - public static final AttributeKey SERVER_PORT = longKey("server.port"); - - public static final AttributeKey SERVER_SOCKET_DOMAIN = stringKey("server.socket.domain"); - - public static final AttributeKey SERVER_SOCKET_ADDRESS = - stringKey("server.socket.address"); - - public static final AttributeKey SERVER_SOCKET_PORT = longKey("server.socket.port"); - - public static final AttributeKey CLIENT_ADDRESS = stringKey("client.address"); - - public static final AttributeKey CLIENT_PORT = longKey("client.port"); - - public static final AttributeKey CLIENT_SOCKET_ADDRESS = - stringKey("client.socket.address"); - - public static final AttributeKey CLIENT_SOCKET_PORT = longKey("client.socket.port"); - - private NetworkAttributes() {} -} diff --git a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/network/internal/NetworkTransportFilter.java b/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/network/internal/NetworkTransportFilter.java deleted file mode 100644 index 87b32c9dc1fa..000000000000 --- a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/network/internal/NetworkTransportFilter.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.api.instrumenter.network.internal; - -import javax.annotation.Nullable; - -/** - * This class is internal and is hence not for public use. Its APIs are unstable and can change at - * any time. - */ -@FunctionalInterface -public interface NetworkTransportFilter { - - boolean shouldAddNetworkTransport( - @Nullable String protocolName, - @Nullable String protocolVersion, - @Nullable String proposedTransport); - - static NetworkTransportFilter alwaysTrue() { - return (protocolName, protocolVersion, proposedTransport) -> true; - } -} diff --git a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/rpc/MetricsView.java b/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/rpc/MetricsView.java deleted file mode 100644 index 16d427c536b5..000000000000 --- a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/rpc/MetricsView.java +++ /dev/null @@ -1,103 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.api.instrumenter.rpc; - -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.HashSet; -import java.util.Set; -import java.util.function.BiConsumer; - -// this is temporary, see -// https://github.com/open-telemetry/opentelemetry-java-instrumentation/issues/3962#issuecomment-906606325 -@SuppressWarnings("rawtypes") -final class MetricsView { - - private static final Set alwaysInclude = buildAlwaysInclude(); - private static final Set clientView = buildClientView(); - private static final Set serverView = buildServerView(); - private static final Set serverFallbackView = buildServerFallbackView(); - - private static Set buildAlwaysInclude() { - // the list of recommended metrics attributes is from - // https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/semantic_conventions/rpc.md#attributes - Set view = new HashSet<>(); - view.add(SemanticAttributes.RPC_SYSTEM); - view.add(SemanticAttributes.RPC_SERVICE); - view.add(SemanticAttributes.RPC_METHOD); - view.add(SemanticAttributes.RPC_GRPC_STATUS_CODE); - return view; - } - - private static Set buildClientView() { - // the list of rpc client metrics attributes is from - // https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/semantic_conventions/rpc.md#attributes - Set view = new HashSet<>(alwaysInclude); - view.add(SemanticAttributes.NET_PEER_NAME); - view.add(SemanticAttributes.NET_PEER_PORT); - view.add(SemanticAttributes.NET_TRANSPORT); - return view; - } - - private static Set buildServerView() { - // the list of rpc server metrics attributes is from - // https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/semantic_conventions/rpc.md#attributes - Set view = new HashSet<>(alwaysInclude); - view.add(SemanticAttributes.NET_HOST_NAME); - view.add(SemanticAttributes.NET_TRANSPORT); - return view; - } - - private static Set buildServerFallbackView() { - // the list of rpc server metrics attributes is from - // https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/semantic_conventions/rpc.md#attributes - Set view = new HashSet<>(alwaysInclude); - view.add(SemanticAttributes.NET_SOCK_HOST_ADDR); - view.add(SemanticAttributes.NET_TRANSPORT); - return view; - } - - private static boolean containsAttribute( - AttributeKey key, Attributes startAttributes, Attributes endAttributes) { - return startAttributes.get(key) != null || endAttributes.get(key) != null; - } - - static Attributes applyClientView(Attributes startAttributes, Attributes endAttributes) { - return applyView(clientView, startAttributes, endAttributes); - } - - static Attributes applyServerView(Attributes startAttributes, Attributes endAttributes) { - Set fullSet = serverView; - if (!containsAttribute(SemanticAttributes.NET_HOST_NAME, startAttributes, endAttributes)) { - fullSet = serverFallbackView; - } - return applyView(fullSet, startAttributes, endAttributes); - } - - static Attributes applyView( - Set view, Attributes startAttributes, Attributes endAttributes) { - AttributesBuilder filtered = Attributes.builder(); - applyView(filtered, startAttributes, view); - applyView(filtered, endAttributes, view); - return filtered.build(); - } - - @SuppressWarnings("unchecked") - private static void applyView( - AttributesBuilder filtered, Attributes attributes, Set view) { - attributes.forEach( - (BiConsumer) - (key, value) -> { - if (view.contains(key)) { - filtered.put(key, value); - } - }); - } - - private MetricsView() {} -} diff --git a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/url/internal/InternalUrlAttributesExtractor.java b/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/url/internal/InternalUrlAttributesExtractor.java deleted file mode 100644 index 3c114300429e..000000000000 --- a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/url/internal/InternalUrlAttributesExtractor.java +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.api.instrumenter.url.internal; - -import static io.opentelemetry.instrumentation.api.internal.AttributesExtractorUtil.internalSet; - -import io.opentelemetry.api.common.AttributesBuilder; -import io.opentelemetry.instrumentation.api.instrumenter.url.UrlAttributesGetter; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; -import java.util.function.Function; -import javax.annotation.Nullable; - -/** - * This class is internal and is hence not for public use. Its APIs are unstable and can change at - * any time. - */ -public final class InternalUrlAttributesExtractor { - - private final UrlAttributesGetter getter; - private final Function alternateSchemeProvider; - private final boolean emitStableUrlAttributes; - private final boolean emitOldHttpAttributes; - - public InternalUrlAttributesExtractor( - UrlAttributesGetter getter, - Function alternateSchemeProvider, - boolean emitStableUrlAttributes, - boolean emitOldHttpAttributes) { - this.getter = getter; - this.alternateSchemeProvider = alternateSchemeProvider; - this.emitStableUrlAttributes = emitStableUrlAttributes; - this.emitOldHttpAttributes = emitOldHttpAttributes; - } - - public void onStart(AttributesBuilder attributes, REQUEST request) { - String urlScheme = getUrlScheme(request); - String urlPath = getter.getUrlPath(request); - String urlQuery = getter.getUrlQuery(request); - - if (emitStableUrlAttributes) { - internalSet(attributes, UrlAttributes.URL_SCHEME, urlScheme); - internalSet(attributes, UrlAttributes.URL_PATH, urlPath); - internalSet(attributes, UrlAttributes.URL_QUERY, urlQuery); - } - if (emitOldHttpAttributes) { - internalSet(attributes, SemanticAttributes.HTTP_SCHEME, urlScheme); - internalSet(attributes, SemanticAttributes.HTTP_TARGET, getTarget(urlPath, urlQuery)); - } - } - - private String getUrlScheme(REQUEST request) { - String urlScheme = alternateSchemeProvider.apply(request); - if (urlScheme == null) { - urlScheme = getter.getUrlScheme(request); - } - return urlScheme; - } - - @Nullable - private static String getTarget(@Nullable String path, @Nullable String query) { - if (path == null && query == null) { - return null; - } - return (path == null ? "" : path) + (query == null || query.isEmpty() ? "" : "?" + query); - } -} diff --git a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/url/internal/UrlAttributes.java b/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/url/internal/UrlAttributes.java deleted file mode 100644 index 01b8eb37c550..000000000000 --- a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/url/internal/UrlAttributes.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.api.instrumenter.url.internal; - -import static io.opentelemetry.api.common.AttributeKey.stringKey; - -import io.opentelemetry.api.common.AttributeKey; - -/** - * This class is internal and is hence not for public use. Its APIs are unstable and can change at - * any time. - */ -public final class UrlAttributes { - - // FIXME: remove this class and replace its usages with SemanticAttributes once schema 1.21 is - // released - - public static final AttributeKey URL_FULL = stringKey("url.full"); - - public static final AttributeKey URL_SCHEME = stringKey("url.scheme"); - - public static final AttributeKey URL_PATH = stringKey("url.path"); - - public static final AttributeKey URL_QUERY = stringKey("url.query"); - - private UrlAttributes() {} -} diff --git a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/server/ServerSpan.java b/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/server/ServerSpan.java deleted file mode 100644 index 67c541853bfa..000000000000 --- a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/server/ServerSpan.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.api.server; - -import io.opentelemetry.api.trace.Span; -import io.opentelemetry.api.trace.SpanKind; -import io.opentelemetry.context.Context; -import io.opentelemetry.instrumentation.api.instrumenter.LocalRootSpan; -import javax.annotation.Nullable; - -/** - * This class encapsulates the context key for storing the current {@link SpanKind#SERVER} span in - * the {@link Context}. - * - * @deprecated Use {@link LocalRootSpan} instead. - */ -@Deprecated -public final class ServerSpan { - - /** - * 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 LocalRootSpan.fromContextOrNull(context); - } - - private ServerSpan() {} -} diff --git a/instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/http/ForwardedHeaderParserTest.java b/instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/http/ForwardedHeaderParserTest.java deleted file mode 100644 index 2756aab924a8..000000000000 --- a/instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/http/ForwardedHeaderParserTest.java +++ /dev/null @@ -1,292 +0,0 @@ -/* - * 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 org.junit.jupiter.api.Test; - -class ForwardedHeaderParserTest { - - @Test - void extractProtoFromForwardedHeader() { - assertThat(ForwardedHeaderParser.extractProtoFromForwardedHeader("for=1.1.1.1;proto=xyz")) - .isEqualTo("xyz"); - } - - @Test - void extractProtoFromForwardedHeaderWithTrailingSemicolon() { - assertThat(ForwardedHeaderParser.extractProtoFromForwardedHeader("for=1.1.1.1;proto=xyz;")) - .isEqualTo("xyz"); - } - - @Test - void extractProtoFromForwardedHeaderWithTrailingComma() { - assertThat(ForwardedHeaderParser.extractProtoFromForwardedHeader("for=1.1.1.1;proto=xyz,")) - .isEqualTo("xyz"); - } - - @Test - void extractProtoFromForwardedHeaderWithQuotes() { - assertThat(ForwardedHeaderParser.extractProtoFromForwardedHeader("for=1.1.1.1;proto=\"xyz\"")) - .isEqualTo("xyz"); - } - - @Test - void extractProtoFromForwardedHeaderWithQuotesAndTrailingSemicolon() { - assertThat(ForwardedHeaderParser.extractProtoFromForwardedHeader("for=1.1.1.1;proto=\"xyz\";")) - .isEqualTo("xyz"); - } - - @Test - void extractProtoFromForwardedHeaderWithQuotesAndTrailingComma() { - assertThat(ForwardedHeaderParser.extractProtoFromForwardedHeader("for=1.1.1.1;proto=\"xyz\",")) - .isEqualTo("xyz"); - } - - @Test - void extractProtoFromForwardedProtoHeader() { - assertThat(ForwardedHeaderParser.extractProtoFromForwardedProtoHeader("xyz")).isEqualTo("xyz"); - } - - @Test - void extractProtoFromForwardedProtoHeaderWithQuotes() { - assertThat(ForwardedHeaderParser.extractProtoFromForwardedProtoHeader("\"xyz\"")) - .isEqualTo("xyz"); - } - - @Test - void extractClientIpFromForwardedHeader() { - assertThat(ForwardedHeaderParser.extractClientIpFromForwardedHeader("for=1.1.1.1")) - .isEqualTo("1.1.1.1"); - } - - @Test - void extractClientIpFromForwardedHeaderWithIpv6() { - assertThat( - ForwardedHeaderParser.extractClientIpFromForwardedHeader( - "for=\"[1111:1111:1111:1111:1111:1111:1111:1111]\"")) - .isEqualTo("1111:1111:1111:1111:1111:1111:1111:1111"); - } - - @Test - void extractClientIpFromForwardedHeaderWithPort() { - assertThat(ForwardedHeaderParser.extractClientIpFromForwardedHeader("for=\"1.1.1.1:2222\"")) - .isEqualTo("1.1.1.1"); - } - - @Test - void extractClientIpFromForwardedHeaderWithIpv6AndPort() { - assertThat( - ForwardedHeaderParser.extractClientIpFromForwardedHeader( - "for=\"[1111:1111:1111:1111:1111:1111:1111:1111]:2222\"")) - .isEqualTo("1111:1111:1111:1111:1111:1111:1111:1111"); - } - - @Test - void extractClientIpFromForwardedHeaderWithCaps() { - assertThat(ForwardedHeaderParser.extractClientIpFromForwardedHeader("For=1.1.1.1")) - .isEqualTo("1.1.1.1"); - } - - @Test - void extractClientIpFromMalformedForwardedHeader() { - assertThat(ForwardedHeaderParser.extractClientIpFromForwardedHeader("for=;for=1.1.1.1")) - .isNull(); - } - - @Test - void extractClientIpFromEmptyForwardedHeader() { - assertThat(ForwardedHeaderParser.extractClientIpFromForwardedHeader("")).isNull(); - } - - @Test - void extractClientIpFromForwardedHeaderWithEmptyValue() { - assertThat(ForwardedHeaderParser.extractClientIpFromForwardedHeader("for=")).isNull(); - } - - @Test - void extractClientIpFromForwardedHeaderWithValueAndSemicolon() { - assertThat(ForwardedHeaderParser.extractClientIpFromForwardedHeader("for=;")).isNull(); - } - - @Test - void extractClientIpFromForwardedHeaderWithNoFor() { - assertThat(ForwardedHeaderParser.extractClientIpFromForwardedHeader("by=1.1.1.1;test=1.1.1.1")) - .isNull(); - } - - @Test - void extractClientIpFromForwardedHeaderWithMultiple() { - assertThat(ForwardedHeaderParser.extractClientIpFromForwardedHeader("for=1.1.1.1;for=1.2.3.4")) - .isEqualTo("1.1.1.1"); - } - - @Test - void extractClientIpFromForwardedHeaderWithMultipleIpV6() { - assertThat( - ForwardedHeaderParser.extractClientIpFromForwardedHeader( - "for=\"[1111:1111:1111:1111:1111:1111:1111:1111]\";for=1.2.3.4")) - .isEqualTo("1111:1111:1111:1111:1111:1111:1111:1111"); - } - - @Test - void extractClientIpFromForwardedHeaderWithMultipleAndPort() { - assertThat( - ForwardedHeaderParser.extractClientIpFromForwardedHeader( - "for=\"1.1.1.1:2222\";for=1.2.3.4")) - .isEqualTo("1.1.1.1"); - } - - @Test - void extractClientIpFromForwardedHeaderWithMultipleIpV6AndPort() { - assertThat( - ForwardedHeaderParser.extractClientIpFromForwardedHeader( - "for=\"[1111:1111:1111:1111:1111:1111:1111:1111]:2222\";for=1.2.3.4")) - .isEqualTo("1111:1111:1111:1111:1111:1111:1111:1111"); - } - - @Test - void extractClientIpFromForwardedHeaderWithMixedSplitter() { - assertThat( - ForwardedHeaderParser.extractClientIpFromForwardedHeader( - "test=abcd; by=1.2.3.4, for=1.1.1.1;for=1.2.3.4")) - .isEqualTo("1.1.1.1"); - } - - @Test - void extractClientIpFromForwardedHeaderWithMixedSplitterIpv6() { - assertThat( - ForwardedHeaderParser.extractClientIpFromForwardedHeader( - "test=abcd; by=1.2.3.4, for=\"[1111:1111:1111:1111:1111:1111:1111:1111]\";for=1.2.3.4")) - .isEqualTo("1111:1111:1111:1111:1111:1111:1111:1111"); - } - - @Test - void extractClientIpFromForwardedHeaderWithMixedSplitterAndPort() { - assertThat( - ForwardedHeaderParser.extractClientIpFromForwardedHeader( - "test=abcd; by=1.2.3.4, for=\"1.1.1.1:2222\";for=1.2.3.4")) - .isEqualTo("1.1.1.1"); - } - - @Test - void extractClientIpFromForwardedHeaderWithMixedSplitterIpv6AndPort() { - assertThat( - ForwardedHeaderParser.extractClientIpFromForwardedHeader( - "test=abcd; by=1.2.3.4, for=\"[1111:1111:1111:1111:1111:1111:1111:1111]:2222\";for=1.2.3.4")) - .isEqualTo("1111:1111:1111:1111:1111:1111:1111:1111"); - } - - @Test - void extractClientIpFromForwardedForHeader() { - assertThat(ForwardedHeaderParser.extractClientIpFromForwardedForHeader("1.1.1.1")) - .isEqualTo("1.1.1.1"); - } - - @Test - void extractClientIpFromForwardedForHeaderWithIpv6() { - assertThat( - ForwardedHeaderParser.extractClientIpFromForwardedForHeader( - "\"[1111:1111:1111:1111:1111:1111:1111:1111]\"")) - .isEqualTo("1111:1111:1111:1111:1111:1111:1111:1111"); - } - - @Test - void extractClientIpFromForwardedForHeaderWithIpv6Unquoted() { - assertThat( - ForwardedHeaderParser.extractClientIpFromForwardedForHeader( - "[1111:1111:1111:1111:1111:1111:1111:1111]")) - .isEqualTo("1111:1111:1111:1111:1111:1111:1111:1111"); - } - - @Test - void extractClientIpFromForwardedForHeaderWithIpv6Unbracketed() { - assertThat( - ForwardedHeaderParser.extractClientIpFromForwardedForHeader( - "1111:1111:1111:1111:1111:1111:1111:1111")) - .isEqualTo("1111:1111:1111:1111:1111:1111:1111:1111"); - } - - @Test - void extractClientIpFromForwardedForHeaderWithPort() { - assertThat(ForwardedHeaderParser.extractClientIpFromForwardedForHeader("1.1.1.1:2222")) - .isEqualTo("1.1.1.1"); - } - - @Test - void extractClientIpFromForwardedForHeaderWithIpv6AndPort() { - assertThat( - ForwardedHeaderParser.extractClientIpFromForwardedForHeader( - "\"[1111:1111:1111:1111:1111:1111:1111:1111]:2222\"")) - .isEqualTo("1111:1111:1111:1111:1111:1111:1111:1111"); - } - - @Test - void extractClientIpFromForwardedForHeaderWithIpv6UnquotedAndPort() { - assertThat( - ForwardedHeaderParser.extractClientIpFromForwardedForHeader( - "[1111:1111:1111:1111:1111:1111:1111:1111]:2222")) - .isEqualTo("1111:1111:1111:1111:1111:1111:1111:1111"); - } - - @Test - void extractClientIpFromEmptyForwardedForHeader() { - assertThat(ForwardedHeaderParser.extractClientIpFromForwardedForHeader("")).isNull(); - } - - @Test - void extractClientIpFromForwardedForHeaderWithMultiple() { - assertThat(ForwardedHeaderParser.extractClientIpFromForwardedForHeader("1.1.1.1,1.2.3.4")) - .isEqualTo("1.1.1.1"); - } - - @Test - void extractClientIpFromForwardedForHeaderWithMultipleIpv6() { - assertThat( - ForwardedHeaderParser.extractClientIpFromForwardedForHeader( - "\"[1111:1111:1111:1111:1111:1111:1111:1111]\",1.2.3.4")) - .isEqualTo("1111:1111:1111:1111:1111:1111:1111:1111"); - } - - @Test - void extractClientIpFromForwardedForHeaderWithMultipleIpv6Unquoted() { - assertThat( - ForwardedHeaderParser.extractClientIpFromForwardedForHeader( - "[1111:1111:1111:1111:1111:1111:1111:1111],1.2.3.4")) - .isEqualTo("1111:1111:1111:1111:1111:1111:1111:1111"); - } - - @Test - void extractClientIpFromForwardedForHeaderWithMultipleIpv6Unbracketed() { - assertThat( - ForwardedHeaderParser.extractClientIpFromForwardedForHeader( - "1111:1111:1111:1111:1111:1111:1111:1111,1.2.3.4")) - .isEqualTo("1111:1111:1111:1111:1111:1111:1111:1111"); - } - - @Test - void extractClientIpFromForwardedForHeaderWithMultipleAndPort() { - assertThat(ForwardedHeaderParser.extractClientIpFromForwardedForHeader("1.1.1.1:2222,1.2.3.4")) - .isEqualTo("1.1.1.1"); - } - - @Test - void extractClientIpFromForwardedForHeaderWithMultipleIpv6AndPort() { - assertThat( - ForwardedHeaderParser.extractClientIpFromForwardedForHeader( - "\"[1111:1111:1111:1111:1111:1111:1111:1111]:2222\",1.2.3.4")) - .isEqualTo("1111:1111:1111:1111:1111:1111:1111:1111"); - } - - @Test - void extractClientIpFromForwardedForHeaderWithMultipleIpv6UnquotedAndPort() { - assertThat( - ForwardedHeaderParser.extractClientIpFromForwardedForHeader( - "[1111:1111:1111:1111:1111:1111:1111:1111]:2222,1.2.3.4")) - .isEqualTo("1111:1111:1111:1111:1111:1111:1111:1111"); - } -} diff --git a/instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpClientAttributesExtractorTest.java b/instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpClientAttributesExtractorTest.java deleted file mode 100644 index e1ceb0e48cfa..000000000000 --- a/instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpClientAttributesExtractorTest.java +++ /dev/null @@ -1,321 +0,0 @@ -/* - * 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 java.util.Arrays.asList; -import static java.util.Collections.emptyList; -import static java.util.Collections.singletonList; -import static org.assertj.core.api.Assertions.entry; -import static org.junit.jupiter.params.provider.Arguments.arguments; - -import io.opentelemetry.api.common.AttributeKey; -import io.opentelemetry.api.common.Attributes; -import io.opentelemetry.api.common.AttributesBuilder; -import io.opentelemetry.context.Context; -import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.net.NetClientAttributesGetter; -import io.opentelemetry.instrumentation.api.instrumenter.net.internal.NetAttributes; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.function.ToIntFunction; -import java.util.stream.Stream; -import javax.annotation.Nullable; -import org.junit.jupiter.api.Test; -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; - -class HttpClientAttributesExtractorTest { - - static class TestHttpClientAttributesGetter - implements HttpClientAttributesGetter, Map> { - - @Override - public String getUrlFull(Map request) { - return request.get("url"); - } - - @Override - public String getHttpRequestMethod(Map request) { - return request.get("method"); - } - - @Override - public List getHttpRequestHeader(Map request, String name) { - String value = request.get("header." + name); - return value == null ? emptyList() : asList(value.split(",")); - } - - @Override - public Integer getHttpResponseStatusCode( - Map request, Map response, @Nullable Throwable error) { - return Integer.parseInt(response.get("statusCode")); - } - - @Override - public List getHttpResponseHeader( - Map request, Map response, String name) { - String value = response.get("header." + name); - return value == null ? emptyList() : asList(value.split(",")); - } - } - - static class TestNetClientAttributesGetter - implements NetClientAttributesGetter, Map> { - - @Nullable - @Override - public String getNetworkTransport( - Map request, @Nullable Map response) { - return request.get("transport"); - } - - @Nullable - @Override - public String getNetworkType( - Map request, @Nullable Map response) { - return request.get("type"); - } - - @Nullable - @Override - public String getNetworkProtocolName( - Map request, @Nullable Map response) { - return request.get("protocolName"); - } - - @Nullable - @Override - public String getNetworkProtocolVersion( - Map request, @Nullable Map response) { - return request.get("protocolVersion"); - } - - @Nullable - @Override - public String getServerAddress(Map request) { - return request.get("peerName"); - } - - @Nullable - @Override - public Integer getServerPort(Map request) { - String statusCode = request.get("peerPort"); - return statusCode == null ? null : Integer.parseInt(statusCode); - } - } - - @Test - void normal() { - Map request = new HashMap<>(); - request.put("method", "POST"); - request.put("url", "http://github.com"); - request.put("header.content-length", "10"); - request.put("header.user-agent", "okhttp 3.x"); - request.put("header.custom-request-header", "123,456"); - request.put("transport", "tcp"); - request.put("type", "ipv4"); - request.put("protocolName", "http"); - request.put("protocolVersion", "1.1"); - request.put("peerName", "github.com"); - request.put("peerPort", "123"); - - Map response = new HashMap<>(); - response.put("statusCode", "202"); - response.put("header.content-length", "20"); - response.put("header.custom-response-header", "654,321"); - - ToIntFunction resendCountFromContext = context -> 2; - - AttributesExtractor, Map> extractor = - new HttpClientAttributesExtractor<>( - new TestHttpClientAttributesGetter(), - new TestNetClientAttributesGetter(), - singletonList("Custom-Request-Header"), - singletonList("Custom-Response-Header"), - resendCountFromContext); - - AttributesBuilder startAttributes = Attributes.builder(); - extractor.onStart(startAttributes, Context.root(), request); - assertThat(startAttributes.build()) - .containsOnly( - entry(SemanticAttributes.HTTP_METHOD, "POST"), - entry(SemanticAttributes.HTTP_URL, "http://github.com"), - entry(SemanticAttributes.USER_AGENT_ORIGINAL, "okhttp 3.x"), - entry( - AttributeKey.stringArrayKey("http.request.header.custom_request_header"), - asList("123", "456")), - entry(SemanticAttributes.NET_PEER_NAME, "github.com"), - entry(SemanticAttributes.NET_PEER_PORT, 123L)); - - AttributesBuilder endAttributes = Attributes.builder(); - extractor.onEnd(endAttributes, Context.root(), request, response, null); - assertThat(endAttributes.build()) - .containsOnly( - entry(SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH, 10L), - entry(SemanticAttributes.HTTP_STATUS_CODE, 202L), - entry(SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH, 20L), - entry(SemanticAttributes.HTTP_RESEND_COUNT, 2L), - entry( - AttributeKey.stringArrayKey("http.response.header.custom_response_header"), - asList("654", "321")), - entry(NetAttributes.NET_PROTOCOL_NAME, "http"), - entry(NetAttributes.NET_PROTOCOL_VERSION, "1.1")); - } - - @ParameterizedTest - @ArgumentsSource(StripUrlArgumentSource.class) - void stripBasicAuthTest(String url, String expectedResult) { - Map request = new HashMap<>(); - request.put("url", url); - - AttributesExtractor, Map> extractor = - HttpClientAttributesExtractor.builder( - new TestHttpClientAttributesGetter(), new TestNetClientAttributesGetter()) - .build(); - - AttributesBuilder attributes = Attributes.builder(); - extractor.onStart(attributes, Context.root(), request); - - assertThat(attributes.build()).containsOnly(entry(SemanticAttributes.HTTP_URL, expectedResult)); - } - - static final class StripUrlArgumentSource implements ArgumentsProvider { - - @Override - public Stream provideArguments(ExtensionContext context) { - return Stream.of( - arguments("https://user1:secret@github.com", "https://REDACTED:REDACTED@github.com"), - arguments( - "https://user1:secret@github.com/path/", - "https://REDACTED:REDACTED@github.com/path/"), - arguments( - "https://user1:secret@github.com#test.html", - "https://REDACTED:REDACTED@github.com#test.html"), - arguments( - "https://user1:secret@github.com?foo=b@r", - "https://REDACTED:REDACTED@github.com?foo=b@r"), - arguments( - "https://user1:secret@github.com/p@th?foo=b@r", - "https://REDACTED:REDACTED@github.com/p@th?foo=b@r"), - arguments("https://github.com/p@th?foo=b@r", "https://github.com/p@th?foo=b@r"), - arguments("https://github.com#t@st.html", "https://github.com#t@st.html"), - arguments("user1:secret@github.com", "user1:secret@github.com"), - arguments("https://github.com@", "https://github.com@")); - } - } - - @Test - void invalidStatusCode() { - Map request = new HashMap<>(); - - Map response = new HashMap<>(); - response.put("statusCode", "0"); - - AttributesExtractor, Map> extractor = - HttpClientAttributesExtractor.builder( - new TestHttpClientAttributesGetter(), new TestNetClientAttributesGetter()) - .setCapturedRequestHeaders(emptyList()) - .setCapturedResponseHeaders(emptyList()) - .build(); - - AttributesBuilder attributes = Attributes.builder(); - extractor.onStart(attributes, Context.root(), request); - assertThat(attributes.build()).isEmpty(); - - extractor.onEnd(attributes, Context.root(), request, response, null); - assertThat(attributes.build()).isEmpty(); - } - - @Test - void extractNetPeerNameAndPortFromHostHeader() { - Map request = new HashMap<>(); - request.put("header.host", "thehost:777"); - - AttributesExtractor, Map> extractor = - HttpClientAttributesExtractor.create( - new TestHttpClientAttributesGetter(), new TestNetClientAttributesGetter()); - - AttributesBuilder attributes = Attributes.builder(); - extractor.onStart(attributes, Context.root(), request); - - assertThat(attributes.build()) - .containsOnly( - entry(SemanticAttributes.NET_PEER_NAME, "thehost"), - entry(SemanticAttributes.NET_PEER_PORT, 777L)); - } - - @Test - void extractNetHostAndPortFromNetAttributesGetter() { - Map request = new HashMap<>(); - request.put("header.host", "notthehost:77777"); // this should have lower precedence - request.put("peerName", "thehost"); - request.put("peerPort", "777"); - - AttributesExtractor, Map> extractor = - HttpClientAttributesExtractor.create( - new TestHttpClientAttributesGetter(), new TestNetClientAttributesGetter()); - - AttributesBuilder attributes = Attributes.builder(); - extractor.onStart(attributes, Context.root(), request); - - assertThat(attributes.build()) - .containsOnly( - entry(SemanticAttributes.NET_PEER_NAME, "thehost"), - entry(SemanticAttributes.NET_PEER_PORT, 777L)); - } - - @ParameterizedTest - @ArgumentsSource(DefaultPeerPortArgumentSource.class) - void defaultPeerPort(int peerPort, String url) { - Map request = new HashMap<>(); - request.put("url", url); - request.put("peerPort", String.valueOf(peerPort)); - - AttributesExtractor, Map> extractor = - HttpClientAttributesExtractor.builder( - new TestHttpClientAttributesGetter(), new TestNetClientAttributesGetter()) - .build(); - - AttributesBuilder attributes = Attributes.builder(); - extractor.onStart(attributes, Context.root(), request); - - assertThat(attributes.build()).doesNotContainKey(SemanticAttributes.NET_PEER_PORT); - } - - static class DefaultPeerPortArgumentSource implements ArgumentsProvider { - - @Override - public Stream provideArguments(ExtensionContext context) { - return Stream.of(arguments(80, "http://github.com"), arguments(443, "https://github.com")); - } - } - - @Test - void zeroResends() { - Map request = new HashMap<>(); - - ToIntFunction resendCountFromContext = context -> 0; - - HttpClientAttributesExtractor, Map> extractor = - new HttpClientAttributesExtractor<>( - new TestHttpClientAttributesGetter(), - new TestNetClientAttributesGetter(), - emptyList(), - emptyList(), - resendCountFromContext); - - AttributesBuilder attributes = Attributes.builder(); - extractor.onStart(attributes, Context.root(), request); - extractor.onEnd(attributes, Context.root(), request, null, null); - assertThat(attributes.build()).isEmpty(); - } -} diff --git a/instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpClientMetricsTest.java b/instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpClientMetricsTest.java deleted file mode 100644 index 0dc22463892e..000000000000 --- a/instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpClientMetricsTest.java +++ /dev/null @@ -1,186 +0,0 @@ -/* - * 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 io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; - -import io.opentelemetry.api.common.Attributes; -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.instrumentation.api.instrumenter.OperationListener; -import io.opentelemetry.instrumentation.api.instrumenter.net.internal.NetAttributes; -import io.opentelemetry.sdk.metrics.SdkMeterProvider; -import io.opentelemetry.sdk.metrics.internal.aggregator.ExplicitBucketHistogramUtils; -import io.opentelemetry.sdk.testing.exporter.InMemoryMetricReader; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; -import java.util.concurrent.TimeUnit; -import org.junit.jupiter.api.Test; - -class HttpClientMetricsTest { - - static final double[] DEFAULT_BUCKETS = - ExplicitBucketHistogramUtils.DEFAULT_HISTOGRAM_BUCKET_BOUNDARIES.stream() - .mapToDouble(d -> d) - .toArray(); - - @Test - void collectsMetrics() { - InMemoryMetricReader metricReader = InMemoryMetricReader.create(); - SdkMeterProvider meterProvider = - SdkMeterProvider.builder().registerMetricReader(metricReader).build(); - - OperationListener listener = HttpClientMetrics.get().create(meterProvider.get("test")); - - Attributes requestAttributes = - Attributes.builder() - .put("http.method", "GET") - .put("http.url", "https://localhost:1234/") - .put("http.target", "/") - .put("http.scheme", "https") - .put("net.peer.name", "localhost") - .put("net.peer.port", 1234) - .put("http.request_content_length", 100) - .build(); - - Attributes responseAttributes = - Attributes.builder() - .put("http.status_code", 200) - .put("http.response_content_length", 200) - .put(NetAttributes.NET_PROTOCOL_NAME, "http") - .put(NetAttributes.NET_PROTOCOL_VERSION, "2.0") - .put("net.sock.peer.addr", "1.2.3.4") - .put("net.sock.peer.name", "somehost20") - .put("net.sock.peer.port", 8080) - .build(); - - Context parent = - Context.root() - .with( - Span.wrap( - SpanContext.create( - "ff01020304050600ff0a0b0c0d0e0f00", - "090a0b0c0d0e0f00", - TraceFlags.getSampled(), - TraceState.getDefault()))); - - Context context1 = listener.onStart(parent, requestAttributes, nanos(100)); - - assertThat(metricReader.collectAllMetrics()).isEmpty(); - - Context context2 = listener.onStart(Context.root(), requestAttributes, nanos(150)); - - assertThat(metricReader.collectAllMetrics()).isEmpty(); - - listener.onEnd(context1, responseAttributes, nanos(250)); - - assertThat(metricReader.collectAllMetrics()) - .satisfiesExactlyInAnyOrder( - metric -> - assertThat(metric) - .hasName("http.client.duration") - .hasUnit("ms") - .hasHistogramSatisfying( - histogram -> - histogram.hasPointsSatisfying( - point -> - point - .hasSum(150 /* millis */) - .hasAttributesSatisfying( - equalTo(SemanticAttributes.HTTP_METHOD, "GET"), - equalTo(SemanticAttributes.HTTP_STATUS_CODE, 200), - equalTo(NetAttributes.NET_PROTOCOL_NAME, "http"), - equalTo(NetAttributes.NET_PROTOCOL_VERSION, "2.0"), - equalTo(SemanticAttributes.NET_PEER_NAME, "localhost"), - equalTo(SemanticAttributes.NET_PEER_PORT, 1234), - equalTo( - SemanticAttributes.NET_SOCK_PEER_ADDR, "1.2.3.4")) - .hasExemplarsSatisfying( - exemplar -> - exemplar - .hasTraceId("ff01020304050600ff0a0b0c0d0e0f00") - .hasSpanId("090a0b0c0d0e0f00")) - .hasBucketBoundaries(DEFAULT_BUCKETS))), - metric -> - assertThat(metric) - .hasName("http.client.request.size") - .hasUnit("By") - .hasHistogramSatisfying( - histogram -> - histogram.hasPointsSatisfying( - point -> - point - .hasSum(100 /* bytes */) - .hasAttributesSatisfying( - equalTo(SemanticAttributes.HTTP_METHOD, "GET"), - equalTo(SemanticAttributes.HTTP_STATUS_CODE, 200), - equalTo(NetAttributes.NET_PROTOCOL_NAME, "http"), - equalTo(NetAttributes.NET_PROTOCOL_VERSION, "2.0"), - equalTo(SemanticAttributes.NET_PEER_NAME, "localhost"), - equalTo(SemanticAttributes.NET_PEER_PORT, 1234), - equalTo( - SemanticAttributes.NET_SOCK_PEER_ADDR, "1.2.3.4")) - .hasExemplarsSatisfying( - exemplar -> - exemplar - .hasTraceId("ff01020304050600ff0a0b0c0d0e0f00") - .hasSpanId("090a0b0c0d0e0f00")))), - metric -> - assertThat(metric) - .hasName("http.client.response.size") - .hasUnit("By") - .hasHistogramSatisfying( - histogram -> - histogram.hasPointsSatisfying( - point -> - point - .hasSum(200 /* bytes */) - .hasAttributesSatisfying( - equalTo(SemanticAttributes.HTTP_METHOD, "GET"), - equalTo(SemanticAttributes.HTTP_STATUS_CODE, 200), - equalTo(NetAttributes.NET_PROTOCOL_NAME, "http"), - equalTo(NetAttributes.NET_PROTOCOL_VERSION, "2.0"), - equalTo(SemanticAttributes.NET_PEER_NAME, "localhost"), - equalTo(SemanticAttributes.NET_PEER_PORT, 1234), - equalTo( - SemanticAttributes.NET_SOCK_PEER_ADDR, "1.2.3.4")) - .hasExemplarsSatisfying( - exemplar -> - exemplar - .hasTraceId("ff01020304050600ff0a0b0c0d0e0f00") - .hasSpanId("090a0b0c0d0e0f00"))))); - - listener.onEnd(context2, responseAttributes, nanos(300)); - - assertThat(metricReader.collectAllMetrics()) - .satisfiesExactlyInAnyOrder( - metric -> - assertThat(metric) - .hasName("http.client.duration") - .hasHistogramSatisfying( - histogram -> - histogram.hasPointsSatisfying(point -> point.hasSum(300 /* millis */))), - metric -> - assertThat(metric) - .hasName("http.client.request.size") - .hasHistogramSatisfying( - histogram -> - histogram.hasPointsSatisfying(point -> point.hasSum(200 /* bytes */))), - metric -> - assertThat(metric) - .hasName("http.client.response.size") - .hasHistogramSatisfying( - histogram -> - histogram.hasPointsSatisfying(point -> point.hasSum(400 /* bytes */)))); - } - - private static long nanos(int millis) { - return TimeUnit.MILLISECONDS.toNanos(millis); - } -} diff --git a/instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpClientResendTest.java b/instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpClientResendTest.java deleted file mode 100644 index 91f8b5aacb29..000000000000 --- a/instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpClientResendTest.java +++ /dev/null @@ -1,28 +0,0 @@ -/* - * 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 io.opentelemetry.context.Context; -import org.junit.jupiter.api.Test; - -class HttpClientResendTest { - - @Test - void resendCountShouldBeZeroWhenNotInitialized() { - assertThat(HttpClientResend.getAndIncrement(Context.root())).isEqualTo(0); - assertThat(HttpClientResend.getAndIncrement(Context.root())).isEqualTo(0); - } - - @Test - void incrementResendCount() { - Context context = HttpClientResend.initialize(Context.root()); - - assertThat(HttpClientResend.getAndIncrement(context)).isEqualTo(0); - assertThat(HttpClientResend.getAndIncrement(context)).isEqualTo(1); - } -} diff --git a/instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpClientStatusConverterTest.java b/instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpClientStatusConverterTest.java deleted file mode 100644 index 7840ad60ed1e..000000000000 --- a/instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpClientStatusConverterTest.java +++ /dev/null @@ -1,94 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.api.instrumenter.http; - -import static io.opentelemetry.instrumentation.api.instrumenter.http.HttpStatusConverter.CLIENT; -import static org.junit.jupiter.api.Assertions.assertEquals; - -import io.opentelemetry.api.trace.StatusCode; -import java.util.stream.Stream; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; - -public class HttpClientStatusConverterTest { - - @ParameterizedTest - @MethodSource("generateParams") - void httpStatusCodeToOtelStatus(int numeric, StatusCode code) { - assertEquals(code, CLIENT.statusFromHttpStatus(numeric)); - } - - static Stream generateParams() { - return Stream.of( - Arguments.of(100, StatusCode.UNSET), - Arguments.of(101, StatusCode.UNSET), - Arguments.of(102, StatusCode.UNSET), - Arguments.of(103, StatusCode.UNSET), - Arguments.of(200, StatusCode.UNSET), - Arguments.of(201, StatusCode.UNSET), - Arguments.of(202, StatusCode.UNSET), - Arguments.of(203, StatusCode.UNSET), - Arguments.of(204, StatusCode.UNSET), - Arguments.of(205, StatusCode.UNSET), - Arguments.of(206, StatusCode.UNSET), - Arguments.of(207, StatusCode.UNSET), - Arguments.of(208, StatusCode.UNSET), - Arguments.of(226, StatusCode.UNSET), - Arguments.of(300, StatusCode.UNSET), - Arguments.of(301, StatusCode.UNSET), - Arguments.of(302, StatusCode.UNSET), - Arguments.of(303, StatusCode.UNSET), - Arguments.of(304, StatusCode.UNSET), - Arguments.of(305, StatusCode.UNSET), - Arguments.of(306, StatusCode.UNSET), - Arguments.of(307, StatusCode.UNSET), - Arguments.of(308, StatusCode.UNSET), - Arguments.of(400, StatusCode.ERROR), - Arguments.of(401, StatusCode.ERROR), - Arguments.of(403, StatusCode.ERROR), - Arguments.of(404, StatusCode.ERROR), - Arguments.of(405, StatusCode.ERROR), - Arguments.of(406, StatusCode.ERROR), - Arguments.of(407, StatusCode.ERROR), - Arguments.of(408, StatusCode.ERROR), - Arguments.of(409, StatusCode.ERROR), - Arguments.of(410, StatusCode.ERROR), - Arguments.of(411, StatusCode.ERROR), - Arguments.of(412, StatusCode.ERROR), - Arguments.of(413, StatusCode.ERROR), - Arguments.of(414, StatusCode.ERROR), - Arguments.of(415, StatusCode.ERROR), - Arguments.of(416, StatusCode.ERROR), - Arguments.of(417, StatusCode.ERROR), - Arguments.of(418, StatusCode.ERROR), - Arguments.of(421, StatusCode.ERROR), - Arguments.of(422, StatusCode.ERROR), - Arguments.of(423, StatusCode.ERROR), - Arguments.of(424, StatusCode.ERROR), - Arguments.of(425, StatusCode.ERROR), - Arguments.of(426, StatusCode.ERROR), - Arguments.of(428, StatusCode.ERROR), - Arguments.of(429, StatusCode.ERROR), - Arguments.of(431, StatusCode.ERROR), - Arguments.of(451, StatusCode.ERROR), - Arguments.of(500, StatusCode.ERROR), - Arguments.of(501, StatusCode.ERROR), - Arguments.of(502, StatusCode.ERROR), - Arguments.of(503, StatusCode.ERROR), - Arguments.of(504, StatusCode.ERROR), - Arguments.of(505, StatusCode.ERROR), - Arguments.of(506, StatusCode.ERROR), - Arguments.of(507, StatusCode.ERROR), - Arguments.of(508, StatusCode.ERROR), - Arguments.of(510, StatusCode.ERROR), - Arguments.of(511, StatusCode.ERROR), - - // Don't exist - Arguments.of(99, StatusCode.ERROR), - Arguments.of(600, StatusCode.ERROR)); - } -} diff --git a/instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpServerAttributesExtractorTest.java b/instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpServerAttributesExtractorTest.java deleted file mode 100644 index ab2fcc33f3fd..000000000000 --- a/instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpServerAttributesExtractorTest.java +++ /dev/null @@ -1,342 +0,0 @@ -/* - * 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 java.util.Arrays.asList; -import static java.util.Collections.emptyList; -import static java.util.Collections.singletonList; -import static org.assertj.core.api.Assertions.entry; -import static org.junit.jupiter.params.provider.Arguments.arguments; - -import io.opentelemetry.api.common.AttributeKey; -import io.opentelemetry.api.common.Attributes; -import io.opentelemetry.api.common.AttributesBuilder; -import io.opentelemetry.context.Context; -import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.net.NetServerAttributesGetter; -import io.opentelemetry.instrumentation.api.instrumenter.net.internal.NetAttributes; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.function.Function; -import java.util.stream.Stream; -import javax.annotation.Nullable; -import org.junit.jupiter.api.Test; -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; - -class HttpServerAttributesExtractorTest { - - static class TestHttpServerAttributesGetter - implements HttpServerAttributesGetter, Map> { - - @Override - public String getHttpRequestMethod(Map request) { - return (String) request.get("method"); - } - - @Override - public String getUrlScheme(Map request) { - return (String) request.get("scheme"); - } - - @Nullable - @Override - public String getUrlPath(Map request) { - return (String) request.get("path"); - } - - @Nullable - @Override - public String getUrlQuery(Map request) { - return (String) request.get("query"); - } - - @Override - public String getHttpRoute(Map request) { - return (String) request.get("route"); - } - - @Override - public List getHttpRequestHeader(Map request, String name) { - String values = (String) request.get("header." + name); - return values == null ? emptyList() : asList(values.split(",")); - } - - @Override - public Integer getHttpResponseStatusCode( - Map request, Map response, @Nullable Throwable error) { - String value = (String) response.get("statusCode"); - return value == null ? null : Integer.parseInt(value); - } - - @Override - public List getHttpResponseHeader( - Map request, Map response, String name) { - String values = (String) response.get("header." + name); - return values == null ? emptyList() : asList(values.split(",")); - } - } - - static class TestNetServerAttributesGetter - implements NetServerAttributesGetter, Map> { - - @Nullable - @Override - public String getNetworkTransport( - Map request, @Nullable Map response) { - return (String) request.get("transport"); - } - - @Nullable - @Override - public String getNetworkType( - Map request, @Nullable Map response) { - return (String) request.get("type"); - } - - @Nullable - @Override - public String getNetworkProtocolName( - Map request, Map response) { - return (String) request.get("protocolName"); - } - - @Nullable - @Override - public String getNetworkProtocolVersion( - Map request, Map response) { - return (String) request.get("protocolVersion"); - } - - @Nullable - @Override - public String getServerAddress(Map request) { - return (String) request.get("hostName"); - } - - @Nullable - @Override - public Integer getServerPort(Map request) { - return (Integer) request.get("hostPort"); - } - } - - @Test - void normal() { - Map request = new HashMap<>(); - request.put("method", "POST"); - request.put("url", "http://github.com"); - request.put("path", "/repositories/1"); - request.put("query", "details=true"); - request.put("scheme", "http"); - request.put("header.content-length", "10"); - request.put("route", "/repositories/{id}"); - request.put("header.user-agent", "okhttp 3.x"); - request.put("header.host", "github.com"); - request.put("header.forwarded", "for=1.1.1.1;proto=https"); - request.put("header.custom-request-header", "123,456"); - request.put("transport", "tcp"); - request.put("type", "ipv4"); - request.put("protocolName", "http"); - request.put("protocolVersion", "2.0"); - - Map response = new HashMap<>(); - response.put("statusCode", "202"); - response.put("header.content-length", "20"); - response.put("header.custom-response-header", "654,321"); - - Function routeFromContext = ctx -> "/repositories/{repoId}"; - - HttpServerAttributesExtractor, Map> extractor = - new HttpServerAttributesExtractor<>( - new TestHttpServerAttributesGetter(), - new TestNetServerAttributesGetter(), - singletonList("Custom-Request-Header"), - singletonList("Custom-Response-Header"), - routeFromContext); - - AttributesBuilder startAttributes = Attributes.builder(); - extractor.onStart(startAttributes, Context.root(), request); - assertThat(startAttributes.build()) - .containsOnly( - entry(SemanticAttributes.NET_HOST_NAME, "github.com"), - entry(SemanticAttributes.HTTP_METHOD, "POST"), - entry(SemanticAttributes.HTTP_SCHEME, "https"), - entry(SemanticAttributes.HTTP_TARGET, "/repositories/1?details=true"), - entry(SemanticAttributes.USER_AGENT_ORIGINAL, "okhttp 3.x"), - entry(SemanticAttributes.HTTP_ROUTE, "/repositories/{id}"), - entry(SemanticAttributes.HTTP_CLIENT_IP, "1.1.1.1"), - entry( - AttributeKey.stringArrayKey("http.request.header.custom_request_header"), - asList("123", "456"))); - - AttributesBuilder endAttributes = Attributes.builder(); - extractor.onEnd(endAttributes, Context.root(), request, response, null); - assertThat(endAttributes.build()) - .containsOnly( - entry(NetAttributes.NET_PROTOCOL_NAME, "http"), - entry(NetAttributes.NET_PROTOCOL_VERSION, "2.0"), - entry(SemanticAttributes.HTTP_ROUTE, "/repositories/{repoId}"), - entry(SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH, 10L), - entry(SemanticAttributes.HTTP_STATUS_CODE, 202L), - entry(SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH, 20L), - entry( - AttributeKey.stringArrayKey("http.response.header.custom_response_header"), - asList("654", "321"))); - } - - @Test - void extractClientIpFromX_Forwarded_For() { - Map request = new HashMap<>(); - request.put("header.x-forwarded-for", "1.1.1.1"); - - AttributesExtractor, Map> extractor = - HttpServerAttributesExtractor.builder( - new TestHttpServerAttributesGetter(), new TestNetServerAttributesGetter()) - .setCapturedRequestHeaders(emptyList()) - .setCapturedResponseHeaders(emptyList()) - .build(); - - AttributesBuilder attributes = Attributes.builder(); - extractor.onStart(attributes, Context.root(), request); - assertThat(attributes.build()) - .containsOnly(entry(SemanticAttributes.HTTP_CLIENT_IP, "1.1.1.1")); - - extractor.onEnd(attributes, Context.root(), request, null, null); - assertThat(attributes.build()) - .containsOnly(entry(SemanticAttributes.HTTP_CLIENT_IP, "1.1.1.1")); - } - - @Test - void extractClientIpFromX_Forwarded_Proto() { - Map request = new HashMap<>(); - request.put("header.x-forwarded-proto", "https"); - - AttributesExtractor, Map> extractor = - HttpServerAttributesExtractor.builder( - new TestHttpServerAttributesGetter(), new TestNetServerAttributesGetter()) - .setCapturedRequestHeaders(emptyList()) - .setCapturedResponseHeaders(emptyList()) - .build(); - - AttributesBuilder attributes = Attributes.builder(); - extractor.onStart(attributes, Context.root(), request); - assertThat(attributes.build()).containsOnly(entry(SemanticAttributes.HTTP_SCHEME, "https")); - - extractor.onEnd(attributes, Context.root(), request, null, null); - assertThat(attributes.build()).containsOnly(entry(SemanticAttributes.HTTP_SCHEME, "https")); - } - - @Test - void extractNetHostAndPortFromHostHeader() { - Map request = new HashMap<>(); - request.put("header.host", "thehost:777"); - - AttributesExtractor, Map> extractor = - HttpServerAttributesExtractor.builder( - new TestHttpServerAttributesGetter(), new TestNetServerAttributesGetter()) - .setCapturedRequestHeaders(emptyList()) - .setCapturedResponseHeaders(emptyList()) - .build(); - - AttributesBuilder attributes = Attributes.builder(); - extractor.onStart(attributes, Context.root(), request); - assertThat(attributes.build()) - .containsOnly( - entry(SemanticAttributes.NET_HOST_NAME, "thehost"), - entry(SemanticAttributes.NET_HOST_PORT, 777L)); - } - - @Test - void extractNetHostAndPortFromNetAttributesGetter() { - Map request = new HashMap<>(); - request.put("header.host", "notthehost:77777"); // this should have lower precedence - request.put("hostName", "thehost"); - request.put("hostPort", 777); - - AttributesExtractor, Map> extractor = - HttpServerAttributesExtractor.builder( - new TestHttpServerAttributesGetter(), new TestNetServerAttributesGetter()) - .setCapturedRequestHeaders(emptyList()) - .setCapturedResponseHeaders(emptyList()) - .build(); - - AttributesBuilder attributes = Attributes.builder(); - extractor.onStart(attributes, Context.root(), request); - assertThat(attributes.build()) - .containsOnly( - entry(SemanticAttributes.NET_HOST_NAME, "thehost"), - entry(SemanticAttributes.NET_HOST_PORT, 777L)); - } - - @ParameterizedTest - @ArgumentsSource(DefaultHostPortArgumentSource.class) - void defaultHostPort(int hostPort, String scheme) { - Map request = new HashMap<>(); - request.put("scheme", scheme); - request.put("hostPort", hostPort); - - AttributesExtractor, Map> extractor = - HttpServerAttributesExtractor.builder( - new TestHttpServerAttributesGetter(), new TestNetServerAttributesGetter()) - .setCapturedRequestHeaders(emptyList()) - .setCapturedResponseHeaders(emptyList()) - .build(); - - AttributesBuilder attributes = Attributes.builder(); - extractor.onStart(attributes, Context.root(), request); - - assertThat(attributes.build()).doesNotContainKey(SemanticAttributes.NET_HOST_PORT); - } - - static class DefaultHostPortArgumentSource implements ArgumentsProvider { - - @Override - public Stream provideArguments(ExtensionContext context) { - return Stream.of(arguments(80, "http"), arguments(443, "https")); - } - } - - @ParameterizedTest - @ArgumentsSource(PathAndQueryArgumentSource.class) - void computeTargetFromPathAndQuery(String path, String query, String expectedTarget) { - Map request = new HashMap<>(); - request.put("path", path); - request.put("query", query); - - AttributesExtractor, Map> extractor = - HttpServerAttributesExtractor.create( - new TestHttpServerAttributesGetter(), new TestNetServerAttributesGetter()); - - AttributesBuilder attributes = Attributes.builder(); - extractor.onStart(attributes, Context.root(), request); - - if (expectedTarget == null) { - assertThat(attributes.build()).doesNotContainKey(SemanticAttributes.HTTP_TARGET); - } else { - assertThat(attributes.build()).containsEntry(SemanticAttributes.HTTP_TARGET, expectedTarget); - } - } - - static class PathAndQueryArgumentSource implements ArgumentsProvider { - - @Override - public Stream provideArguments(ExtensionContext context) { - return Stream.of( - arguments(null, null, null), - arguments("path", null, "path"), - arguments("path", "", "path"), - arguments(null, "query", "?query"), - arguments("path", "query", "path?query")); - } - } -} diff --git a/instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpServerMetricsTest.java b/instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpServerMetricsTest.java deleted file mode 100644 index 79546118d17d..000000000000 --- a/instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpServerMetricsTest.java +++ /dev/null @@ -1,331 +0,0 @@ -/* - * 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 io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.NetTransportValues.IP_TCP; - -import io.opentelemetry.api.common.Attributes; -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.instrumentation.api.instrumenter.OperationListener; -import io.opentelemetry.instrumentation.api.instrumenter.net.internal.NetAttributes; -import io.opentelemetry.sdk.metrics.SdkMeterProvider; -import io.opentelemetry.sdk.metrics.internal.aggregator.ExplicitBucketHistogramUtils; -import io.opentelemetry.sdk.testing.exporter.InMemoryMetricReader; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; -import java.util.concurrent.TimeUnit; -import org.junit.jupiter.api.Test; - -class HttpServerMetricsTest { - - static final double[] DEFAULT_BUCKETS = - ExplicitBucketHistogramUtils.DEFAULT_HISTOGRAM_BUCKET_BOUNDARIES.stream() - .mapToDouble(d -> d) - .toArray(); - - @Test - void collectsMetrics() { - InMemoryMetricReader metricReader = InMemoryMetricReader.create(); - SdkMeterProvider meterProvider = - SdkMeterProvider.builder().registerMetricReader(metricReader).build(); - - OperationListener listener = HttpServerMetrics.get().create(meterProvider.get("test")); - - Attributes requestAttributes = - Attributes.builder() - .put("http.method", "GET") - .put("http.target", "/") - .put("http.scheme", "https") - .put("net.transport", IP_TCP) - .put(NetAttributes.NET_PROTOCOL_NAME, "http") - .put(NetAttributes.NET_PROTOCOL_VERSION, "2.0") - .put("net.host.name", "localhost") - .put("net.host.port", 1234) - .put("net.sock.family", "inet") - .put("net.sock.peer.addr", "1.2.3.4") - .put("net.sock.peer.port", 8080) - .put("net.sock.host.addr", "4.3.2.1") - .put("net.sock.host.port", 9090) - .build(); - - Attributes responseAttributes = - Attributes.builder() - .put("http.status_code", 200) - .put("http.request_content_length", 100) - .put("http.response_content_length", 200) - .build(); - - SpanContext spanContext1 = - SpanContext.create( - "ff01020304050600ff0a0b0c0d0e0f00", - "090a0b0c0d0e0f00", - TraceFlags.getSampled(), - TraceState.getDefault()); - SpanContext spanContext2 = - SpanContext.create( - "123456789abcdef00000000000999999", - "abcde00000054321", - TraceFlags.getSampled(), - TraceState.getDefault()); - - Context parent1 = Context.root().with(Span.wrap(spanContext1)); - Context context1 = listener.onStart(parent1, requestAttributes, nanos(100)); - - assertThat(metricReader.collectAllMetrics()) - .satisfiesExactlyInAnyOrder( - metric -> - assertThat(metric) - .hasName("http.server.active_requests") - .hasDescription( - "The number of concurrent HTTP requests that are currently in-flight") - .hasUnit("{requests}") - .hasLongSumSatisfying( - sum -> - sum.hasPointsSatisfying( - point -> - point - .hasValue(1) - .hasAttributesSatisfying( - equalTo(SemanticAttributes.HTTP_METHOD, "GET"), - equalTo(SemanticAttributes.HTTP_SCHEME, "https"), - equalTo(SemanticAttributes.NET_HOST_NAME, "localhost"), - equalTo(SemanticAttributes.NET_HOST_PORT, 1234L)) - .hasExemplarsSatisfying( - exemplar -> - exemplar - .hasTraceId(spanContext1.getTraceId()) - .hasSpanId(spanContext1.getSpanId()))))); - - Context parent2 = Context.root().with(Span.wrap(spanContext2)); - Context context2 = listener.onStart(parent2, requestAttributes, nanos(150)); - - assertThat(metricReader.collectAllMetrics()) - .satisfiesExactlyInAnyOrder( - metric -> - assertThat(metric) - .hasName("http.server.active_requests") - .hasLongSumSatisfying( - sum -> - sum.hasPointsSatisfying( - point -> - point - .hasValue(2) - .hasAttributesSatisfying( - equalTo(SemanticAttributes.HTTP_METHOD, "GET"), - equalTo(SemanticAttributes.HTTP_SCHEME, "https"), - equalTo(SemanticAttributes.NET_HOST_NAME, "localhost"), - equalTo(SemanticAttributes.NET_HOST_PORT, 1234L)) - .hasExemplarsSatisfying( - exemplar -> - exemplar - .hasTraceId(spanContext2.getTraceId()) - .hasSpanId(spanContext2.getSpanId()))))); - - listener.onEnd(context1, responseAttributes, nanos(250)); - - assertThat(metricReader.collectAllMetrics()) - .satisfiesExactlyInAnyOrder( - metric -> - assertThat(metric) - .hasName("http.server.active_requests") - .hasLongSumSatisfying( - sum -> - sum.hasPointsSatisfying( - point -> - point - .hasValue(1) - .hasAttributesSatisfying( - equalTo(SemanticAttributes.HTTP_METHOD, "GET"), - equalTo(SemanticAttributes.HTTP_SCHEME, "https"), - equalTo(SemanticAttributes.NET_HOST_NAME, "localhost"), - equalTo(SemanticAttributes.NET_HOST_PORT, 1234L)) - .hasExemplarsSatisfying( - exemplar -> - exemplar - .hasTraceId(spanContext1.getTraceId()) - .hasSpanId(spanContext1.getSpanId())))), - metric -> - assertThat(metric) - .hasName("http.server.duration") - .hasUnit("ms") - .hasHistogramSatisfying( - histogram -> - histogram.hasPointsSatisfying( - point -> - point - .hasSum(150 /* millis */) - .hasAttributesSatisfying( - equalTo(SemanticAttributes.HTTP_METHOD, "GET"), - equalTo(SemanticAttributes.HTTP_STATUS_CODE, 200), - equalTo(NetAttributes.NET_PROTOCOL_NAME, "http"), - equalTo(NetAttributes.NET_PROTOCOL_VERSION, "2.0"), - equalTo(SemanticAttributes.HTTP_SCHEME, "https"), - equalTo(SemanticAttributes.NET_HOST_NAME, "localhost"), - equalTo(SemanticAttributes.NET_HOST_PORT, 1234)) - .hasExemplarsSatisfying( - exemplar -> - exemplar - .hasTraceId(spanContext1.getTraceId()) - .hasSpanId(spanContext1.getSpanId())) - .hasBucketBoundaries(DEFAULT_BUCKETS))), - metric -> - assertThat(metric) - .hasName("http.server.request.size") - .hasUnit("By") - .hasHistogramSatisfying( - histogram -> - histogram.hasPointsSatisfying( - point -> - point - .hasSum(100 /* bytes */) - .hasAttributesSatisfying( - equalTo(SemanticAttributes.HTTP_METHOD, "GET"), - equalTo(SemanticAttributes.HTTP_STATUS_CODE, 200), - equalTo(NetAttributes.NET_PROTOCOL_NAME, "http"), - equalTo(NetAttributes.NET_PROTOCOL_VERSION, "2.0"), - equalTo(SemanticAttributes.HTTP_SCHEME, "https"), - equalTo(SemanticAttributes.NET_HOST_NAME, "localhost"), - equalTo(SemanticAttributes.NET_HOST_PORT, 1234)) - .hasExemplarsSatisfying( - exemplar -> - exemplar - .hasTraceId(spanContext1.getTraceId()) - .hasSpanId(spanContext1.getSpanId())))), - metric -> - assertThat(metric) - .hasName("http.server.response.size") - .hasUnit("By") - .hasHistogramSatisfying( - histogram -> - histogram.hasPointsSatisfying( - point -> - point - .hasSum(200 /* bytes */) - .hasAttributesSatisfying( - equalTo(SemanticAttributes.HTTP_METHOD, "GET"), - equalTo(SemanticAttributes.HTTP_STATUS_CODE, 200), - equalTo(NetAttributes.NET_PROTOCOL_NAME, "http"), - equalTo(NetAttributes.NET_PROTOCOL_VERSION, "2.0"), - equalTo(SemanticAttributes.HTTP_SCHEME, "https"), - equalTo(SemanticAttributes.NET_HOST_NAME, "localhost"), - equalTo(SemanticAttributes.NET_HOST_PORT, 1234)) - .hasExemplarsSatisfying( - exemplar -> - exemplar - .hasTraceId(spanContext1.getTraceId()) - .hasSpanId(spanContext1.getSpanId()))))); - - listener.onEnd(context2, responseAttributes, nanos(300)); - - assertThat(metricReader.collectAllMetrics()) - .satisfiesExactlyInAnyOrder( - metric -> - assertThat(metric) - .hasName("http.server.active_requests") - .hasLongSumSatisfying( - sum -> - sum.hasPointsSatisfying( - point -> - point - .hasValue(0) - .hasExemplarsSatisfying( - exemplar -> - exemplar - .hasTraceId(spanContext2.getTraceId()) - .hasSpanId(spanContext2.getSpanId())))), - metric -> - assertThat(metric) - .hasName("http.server.duration") - .hasHistogramSatisfying( - histogram -> - histogram.hasPointsSatisfying( - point -> - point - .hasSum(300 /* millis */) - .hasExemplarsSatisfying( - exemplar -> - exemplar - .hasTraceId(spanContext2.getTraceId()) - .hasSpanId(spanContext2.getSpanId())))), - metric -> - assertThat(metric) - .hasName("http.server.request.size") - .hasHistogramSatisfying( - histogram -> - histogram.hasPointsSatisfying( - point -> - point - .hasSum(200 /* bytes */) - .hasExemplarsSatisfying( - exemplar -> - exemplar - .hasTraceId(spanContext2.getTraceId()) - .hasSpanId(spanContext2.getSpanId())))), - metric -> - assertThat(metric) - .hasName("http.server.response.size") - .hasHistogramSatisfying( - histogram -> - histogram.hasPointsSatisfying( - point -> - point - .hasSum(400 /* bytes */) - .hasExemplarsSatisfying( - exemplar -> - exemplar - .hasTraceId(spanContext2.getTraceId()) - .hasSpanId(spanContext2.getSpanId()))))); - } - - @Test - void collectsHttpRouteFromEndAttributes() { - // given - InMemoryMetricReader metricReader = InMemoryMetricReader.create(); - SdkMeterProvider meterProvider = - SdkMeterProvider.builder().registerMetricReader(metricReader).build(); - - OperationListener listener = HttpServerMetrics.get().create(meterProvider.get("test")); - - Attributes requestAttributes = - Attributes.builder().put("net.host.name", "host").put("http.scheme", "https").build(); - - Attributes responseAttributes = Attributes.builder().put("http.route", "/test/{id}").build(); - - Context parentContext = Context.root(); - - // when - Context context = listener.onStart(parentContext, requestAttributes, nanos(100)); - listener.onEnd(context, responseAttributes, nanos(200)); - - // then - assertThat(metricReader.collectAllMetrics()) - .anySatisfy( - metric -> - assertThat(metric) - .hasName("http.server.duration") - .hasUnit("ms") - .hasHistogramSatisfying( - histogram -> - histogram.hasPointsSatisfying( - point -> - point - .hasSum(100 /* millis */) - .hasAttributesSatisfying( - equalTo(SemanticAttributes.HTTP_SCHEME, "https"), - equalTo(SemanticAttributes.NET_HOST_NAME, "host"), - equalTo( - SemanticAttributes.HTTP_ROUTE, "/test/{id}"))))); - } - - private static long nanos(int millis) { - return TimeUnit.MILLISECONDS.toNanos(millis); - } -} diff --git a/instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpServerStatusConverterTest.java b/instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpServerStatusConverterTest.java deleted file mode 100644 index 532c5338095e..000000000000 --- a/instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpServerStatusConverterTest.java +++ /dev/null @@ -1,94 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.api.instrumenter.http; - -import static io.opentelemetry.instrumentation.api.instrumenter.http.HttpStatusConverter.SERVER; -import static org.junit.jupiter.api.Assertions.assertEquals; - -import io.opentelemetry.api.trace.StatusCode; -import java.util.stream.Stream; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; - -public class HttpServerStatusConverterTest { - - @ParameterizedTest - @MethodSource("generateParams") - void httpStatusCodeToOtelStatus(int numeric, StatusCode code) { - assertEquals(code, SERVER.statusFromHttpStatus(numeric)); - } - - static Stream generateParams() { - return Stream.of( - Arguments.of(100, StatusCode.UNSET), - Arguments.of(101, StatusCode.UNSET), - Arguments.of(102, StatusCode.UNSET), - Arguments.of(103, StatusCode.UNSET), - Arguments.of(200, StatusCode.UNSET), - Arguments.of(201, StatusCode.UNSET), - Arguments.of(202, StatusCode.UNSET), - Arguments.of(203, StatusCode.UNSET), - Arguments.of(204, StatusCode.UNSET), - Arguments.of(205, StatusCode.UNSET), - Arguments.of(206, StatusCode.UNSET), - Arguments.of(207, StatusCode.UNSET), - Arguments.of(208, StatusCode.UNSET), - Arguments.of(226, StatusCode.UNSET), - Arguments.of(300, StatusCode.UNSET), - Arguments.of(301, StatusCode.UNSET), - Arguments.of(302, StatusCode.UNSET), - Arguments.of(303, StatusCode.UNSET), - Arguments.of(304, StatusCode.UNSET), - Arguments.of(305, StatusCode.UNSET), - Arguments.of(306, StatusCode.UNSET), - Arguments.of(307, StatusCode.UNSET), - Arguments.of(308, StatusCode.UNSET), - Arguments.of(400, StatusCode.UNSET), - Arguments.of(401, StatusCode.UNSET), - Arguments.of(403, StatusCode.UNSET), - Arguments.of(404, StatusCode.UNSET), - Arguments.of(405, StatusCode.UNSET), - Arguments.of(406, StatusCode.UNSET), - Arguments.of(407, StatusCode.UNSET), - Arguments.of(408, StatusCode.UNSET), - Arguments.of(409, StatusCode.UNSET), - Arguments.of(410, StatusCode.UNSET), - Arguments.of(411, StatusCode.UNSET), - Arguments.of(412, StatusCode.UNSET), - Arguments.of(413, StatusCode.UNSET), - Arguments.of(414, StatusCode.UNSET), - Arguments.of(415, StatusCode.UNSET), - Arguments.of(416, StatusCode.UNSET), - Arguments.of(417, StatusCode.UNSET), - Arguments.of(418, StatusCode.UNSET), - Arguments.of(421, StatusCode.UNSET), - Arguments.of(422, StatusCode.UNSET), - Arguments.of(423, StatusCode.UNSET), - Arguments.of(424, StatusCode.UNSET), - Arguments.of(425, StatusCode.UNSET), - Arguments.of(426, StatusCode.UNSET), - Arguments.of(428, StatusCode.UNSET), - Arguments.of(429, StatusCode.UNSET), - Arguments.of(431, StatusCode.UNSET), - Arguments.of(451, StatusCode.UNSET), - Arguments.of(500, StatusCode.ERROR), - Arguments.of(501, StatusCode.ERROR), - Arguments.of(502, StatusCode.ERROR), - Arguments.of(503, StatusCode.ERROR), - Arguments.of(504, StatusCode.ERROR), - Arguments.of(505, StatusCode.ERROR), - Arguments.of(506, StatusCode.ERROR), - Arguments.of(507, StatusCode.ERROR), - Arguments.of(508, StatusCode.ERROR), - Arguments.of(510, StatusCode.ERROR), - Arguments.of(511, StatusCode.ERROR), - - // Don't exist - Arguments.of(99, StatusCode.ERROR), - Arguments.of(600, StatusCode.ERROR)); - } -} diff --git a/instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/http/TemporaryMetricsViewTest.java b/instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/http/TemporaryMetricsViewTest.java deleted file mode 100644 index a5968c913718..000000000000 --- a/instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/http/TemporaryMetricsViewTest.java +++ /dev/null @@ -1,241 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.api.instrumenter.http; - -import static io.opentelemetry.instrumentation.api.instrumenter.http.TemporaryMetricsView.applyActiveRequestsView; -import static io.opentelemetry.instrumentation.api.instrumenter.http.TemporaryMetricsView.applyClientDurationAndSizeView; -import static io.opentelemetry.instrumentation.api.instrumenter.http.TemporaryMetricsView.applyServerDurationAndSizeView; -import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.NetTransportValues.IP_TCP; -import static org.assertj.core.api.Assertions.entry; - -import io.opentelemetry.api.common.Attributes; -import io.opentelemetry.instrumentation.api.instrumenter.net.internal.NetAttributes; -import io.opentelemetry.instrumentation.api.instrumenter.network.internal.NetworkAttributes; -import io.opentelemetry.instrumentation.api.instrumenter.url.internal.UrlAttributes; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; -import org.junit.jupiter.api.Test; - -class TemporaryMetricsViewTest { - - @Test - void shouldApplyClientDurationAndSizeView() { - Attributes startAttributes = - Attributes.builder() - .put( - SemanticAttributes.HTTP_URL, - "https://somehost/high/cardinality/12345?jsessionId=121454") - .put(SemanticAttributes.HTTP_METHOD, "GET") - .put(SemanticAttributes.HTTP_SCHEME, "https") - .put(SemanticAttributes.HTTP_TARGET, "/high/cardinality/12345?jsessionId=121454") - .build(); - - Attributes endAttributes = - Attributes.builder() - .put(SemanticAttributes.HTTP_STATUS_CODE, 500) - .put(SemanticAttributes.NET_TRANSPORT, IP_TCP) - .put(NetAttributes.NET_PROTOCOL_NAME, "http") - .put(NetAttributes.NET_PROTOCOL_VERSION, "1.1") - .put(SemanticAttributes.NET_PEER_NAME, "somehost2") - .put(SemanticAttributes.NET_PEER_PORT, 443) - .put(SemanticAttributes.NET_SOCK_FAMILY, "inet") - .put(SemanticAttributes.NET_SOCK_PEER_ADDR, "1.2.3.4") - .put(SemanticAttributes.NET_SOCK_PEER_NAME, "somehost20") - .put(SemanticAttributes.NET_SOCK_PEER_PORT, 8080) - .build(); - - assertThat(applyClientDurationAndSizeView(startAttributes, endAttributes)) - .containsOnly( - entry(SemanticAttributes.HTTP_METHOD, "GET"), - entry(SemanticAttributes.HTTP_STATUS_CODE, 500L), - entry(NetAttributes.NET_PROTOCOL_NAME, "http"), - entry(NetAttributes.NET_PROTOCOL_VERSION, "1.1"), - entry(SemanticAttributes.NET_PEER_NAME, "somehost2"), - entry(SemanticAttributes.NET_PEER_PORT, 443L), - entry(SemanticAttributes.NET_SOCK_PEER_ADDR, "1.2.3.4")); - } - - @Test - void shouldApplyClientDurationAndSizeView_stableSemconv() { - Attributes startAttributes = - Attributes.builder() - .put( - UrlAttributes.URL_FULL, "https://somehost/high/cardinality/12345?jsessionId=121454") - .put(HttpAttributes.HTTP_REQUEST_METHOD, "GET") - .put(UrlAttributes.URL_SCHEME, "https") - .put(UrlAttributes.URL_PATH, "/high/cardinality/12345") - .put(UrlAttributes.URL_QUERY, "jsessionId=121454") - .put(NetworkAttributes.SERVER_ADDRESS, "somehost2") - .put(NetworkAttributes.SERVER_PORT, 443) - .build(); - - Attributes endAttributes = - Attributes.builder() - .put(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, 500) - .put(NetworkAttributes.NETWORK_TRANSPORT, "tcp") - .put(NetworkAttributes.NETWORK_TYPE, "ipv4") - .put(NetworkAttributes.NETWORK_PROTOCOL_NAME, "http") - .put(NetworkAttributes.NETWORK_PROTOCOL_VERSION, "1.1") - .put(NetworkAttributes.SERVER_SOCKET_ADDRESS, "1.2.3.4") - .put(NetworkAttributes.SERVER_SOCKET_DOMAIN, "somehost20") - .put(NetworkAttributes.SERVER_SOCKET_PORT, 8080) - .build(); - - assertThat(applyClientDurationAndSizeView(startAttributes, endAttributes)) - .containsOnly( - entry(HttpAttributes.HTTP_REQUEST_METHOD, "GET"), - entry(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, 500L), - entry(NetworkAttributes.NETWORK_PROTOCOL_NAME, "http"), - entry(NetworkAttributes.NETWORK_PROTOCOL_VERSION, "1.1"), - entry(NetworkAttributes.SERVER_ADDRESS, "somehost2"), - entry(NetworkAttributes.SERVER_PORT, 443L), - entry(NetworkAttributes.SERVER_SOCKET_ADDRESS, "1.2.3.4")); - } - - @Test - void shouldApplyServerDurationAndSizeView() { - Attributes startAttributes = - Attributes.builder() - .put(SemanticAttributes.HTTP_METHOD, "GET") - .put( - SemanticAttributes.HTTP_URL, - "https://somehost/high/cardinality/12345?jsessionId=121454") - .put(SemanticAttributes.HTTP_TARGET, "/high/cardinality/12345?jsessionId=121454") - .put(SemanticAttributes.HTTP_SCHEME, "https") - .put(SemanticAttributes.NET_TRANSPORT, IP_TCP) - .put(NetAttributes.NET_PROTOCOL_NAME, "http") - .put(NetAttributes.NET_PROTOCOL_VERSION, "1.1") - .put(SemanticAttributes.NET_HOST_NAME, "somehost") - .put(SemanticAttributes.NET_HOST_PORT, 443) - .put(SemanticAttributes.NET_SOCK_FAMILY, "inet") - .put(SemanticAttributes.NET_SOCK_PEER_ADDR, "1.2.3.4") - .put(SemanticAttributes.NET_SOCK_PEER_PORT, 8080) - .put(SemanticAttributes.NET_SOCK_HOST_ADDR, "4.3.2.1") - .put(SemanticAttributes.NET_SOCK_HOST_PORT, 9090) - .build(); - - Attributes endAttributes = - Attributes.builder() - .put(SemanticAttributes.HTTP_ROUTE, "/somehost/high/{name}/{id}") - .put(SemanticAttributes.HTTP_STATUS_CODE, 500) - .put(SemanticAttributes.NET_PEER_NAME, "somehost2") - .put(SemanticAttributes.NET_PEER_PORT, 443) - .build(); - - assertThat(applyServerDurationAndSizeView(startAttributes, endAttributes)) - .containsOnly( - entry(SemanticAttributes.HTTP_METHOD, "GET"), - entry(SemanticAttributes.HTTP_STATUS_CODE, 500L), - entry(SemanticAttributes.HTTP_SCHEME, "https"), - entry(NetAttributes.NET_PROTOCOL_NAME, "http"), - entry(NetAttributes.NET_PROTOCOL_VERSION, "1.1"), - entry(SemanticAttributes.NET_HOST_NAME, "somehost"), - entry(SemanticAttributes.NET_HOST_PORT, 443L), - entry(SemanticAttributes.HTTP_ROUTE, "/somehost/high/{name}/{id}")); - } - - @Test - void shouldApplyServerDurationAndSizeView_stableSemconv() { - Attributes startAttributes = - Attributes.builder() - .put(HttpAttributes.HTTP_REQUEST_METHOD, "GET") - .put( - UrlAttributes.URL_FULL, "https://somehost/high/cardinality/12345?jsessionId=121454") - .put(UrlAttributes.URL_SCHEME, "https") - .put(UrlAttributes.URL_PATH, "/high/cardinality/12345") - .put(UrlAttributes.URL_QUERY, "jsessionId=121454") - .put(NetworkAttributes.SERVER_ADDRESS, "somehost") - .put(NetworkAttributes.SERVER_PORT, 443) - .put(NetworkAttributes.CLIENT_ADDRESS, "somehost2") - .put(NetworkAttributes.CLIENT_PORT, 443) - .build(); - - Attributes endAttributes = - Attributes.builder() - .put(SemanticAttributes.HTTP_ROUTE, "/somehost/high/{name}/{id}") - .put(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, 500) - .put(NetworkAttributes.NETWORK_TRANSPORT, "tcp") - .put(NetworkAttributes.NETWORK_TYPE, "ipv4") - .put(NetworkAttributes.NETWORK_PROTOCOL_NAME, "http") - .put(NetworkAttributes.NETWORK_PROTOCOL_VERSION, "1.1") - .put(NetworkAttributes.SERVER_SOCKET_ADDRESS, "4.3.2.1") - .put(NetworkAttributes.SERVER_SOCKET_PORT, 9090) - .put(NetworkAttributes.CLIENT_SOCKET_ADDRESS, "1.2.3.4") - .put(NetworkAttributes.CLIENT_SOCKET_PORT, 8080) - .build(); - - assertThat(applyServerDurationAndSizeView(startAttributes, endAttributes)) - .containsOnly( - entry(HttpAttributes.HTTP_REQUEST_METHOD, "GET"), - entry(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, 500L), - entry(SemanticAttributes.HTTP_ROUTE, "/somehost/high/{name}/{id}"), - entry(UrlAttributes.URL_SCHEME, "https"), - entry(NetworkAttributes.NETWORK_PROTOCOL_NAME, "http"), - entry(NetworkAttributes.NETWORK_PROTOCOL_VERSION, "1.1"), - entry(NetworkAttributes.SERVER_ADDRESS, "somehost"), - entry(NetworkAttributes.SERVER_PORT, 443L)); - } - - @Test - void shouldApplyActiveRequestsView() { - Attributes attributes = - Attributes.builder() - .put(SemanticAttributes.HTTP_METHOD, "GET") - .put( - SemanticAttributes.HTTP_URL, - "https://somehost/high/cardinality/12345?jsessionId=121454") - .put(SemanticAttributes.HTTP_TARGET, "/high/cardinality/12345?jsessionId=121454") - .put(SemanticAttributes.HTTP_SCHEME, "https") - .put(SemanticAttributes.NET_TRANSPORT, IP_TCP) - .put(NetAttributes.NET_PROTOCOL_NAME, "http") - .put(NetAttributes.NET_PROTOCOL_VERSION, "1.1") - .put(SemanticAttributes.NET_HOST_NAME, "somehost") - .put(SemanticAttributes.NET_HOST_PORT, 443) - .put(SemanticAttributes.NET_SOCK_FAMILY, "inet") - .put(SemanticAttributes.NET_SOCK_PEER_ADDR, "1.2.3.4") - .put(SemanticAttributes.NET_SOCK_PEER_PORT, 8080) - .put(SemanticAttributes.NET_SOCK_HOST_ADDR, "4.3.2.1") - .put(SemanticAttributes.NET_SOCK_HOST_PORT, 9090) - .build(); - - assertThat(applyActiveRequestsView(attributes)) - .containsOnly( - entry(SemanticAttributes.HTTP_METHOD, "GET"), - entry(SemanticAttributes.HTTP_SCHEME, "https"), - entry(SemanticAttributes.NET_HOST_NAME, "somehost"), - entry(SemanticAttributes.NET_HOST_PORT, 443L)); - } - - @Test - void shouldApplyActiveRequestsView_stableSemconv() { - Attributes attributes = - Attributes.builder() - .put(HttpAttributes.HTTP_REQUEST_METHOD, "GET") - .put( - UrlAttributes.URL_FULL, "https://somehost/high/cardinality/12345?jsessionId=121454") - .put(UrlAttributes.URL_SCHEME, "https") - .put(UrlAttributes.URL_PATH, "/high/cardinality/12345") - .put(UrlAttributes.URL_QUERY, "jsessionId=121454") - .put(NetworkAttributes.NETWORK_TRANSPORT, "tcp") - .put(NetworkAttributes.NETWORK_TYPE, "ipv4") - .put(NetworkAttributes.NETWORK_PROTOCOL_NAME, "http") - .put(NetworkAttributes.NETWORK_PROTOCOL_VERSION, "1.1") - .put(NetworkAttributes.SERVER_ADDRESS, "somehost") - .put(NetworkAttributes.SERVER_PORT, 443) - .put(NetworkAttributes.SERVER_SOCKET_ADDRESS, "4.3.2.1") - .put(NetworkAttributes.SERVER_SOCKET_PORT, 9090) - .put(NetworkAttributes.CLIENT_SOCKET_ADDRESS, "1.2.3.4") - .put(NetworkAttributes.CLIENT_SOCKET_PORT, 8080) - .build(); - - assertThat(applyActiveRequestsView(attributes)) - .containsOnly( - entry(HttpAttributes.HTTP_REQUEST_METHOD, "GET"), - entry(UrlAttributes.URL_SCHEME, "https"), - entry(NetworkAttributes.SERVER_ADDRESS, "somehost"), - entry(NetworkAttributes.SERVER_PORT, 443L)); - } -} diff --git a/instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/net/InetSocketAddressNetClientAttributesGetterTest.java b/instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/net/InetSocketAddressNetClientAttributesGetterTest.java deleted file mode 100644 index 03fe8a25e4ff..000000000000 --- a/instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/net/InetSocketAddressNetClientAttributesGetterTest.java +++ /dev/null @@ -1,108 +0,0 @@ -/* - * 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 io.opentelemetry.api.common.Attributes; -import io.opentelemetry.api.common.AttributesBuilder; -import io.opentelemetry.context.Context; -import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; -import java.net.Inet4Address; -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 InetSocketAddressNetClientAttributesGetterTest { - - static class TestNetClientAttributesGetter - implements NetClientAttributesGetter { - - @Override - public String getServerAddress(InetSocketAddress request) { - // net.peer.name and net.peer.port are tested in NetClientAttributesExtractorTest - return null; - } - - @Override - public Integer getServerPort(InetSocketAddress request) { - // net.peer.name and net.peer.port are tested in NetClientAttributesExtractorTest - return null; - } - - @Override - public InetSocketAddress getServerInetSocketAddress( - InetSocketAddress request, InetSocketAddress response) { - return response; - } - } - - private final AttributesExtractor extractor = - NetClientAttributesExtractor.create(new TestNetClientAttributesGetter()); - - @Test - void noInetSocketAddress() { - - AttributesBuilder attributes = Attributes.builder(); - extractor.onEnd(attributes, Context.root(), null, null, null); - assertThat(attributes.build()).isEmpty(); - } - - @Test - void fullAddress() { - // given - InetSocketAddress address = new InetSocketAddress("api.github.com", 456); - assertThat(address.getAddress().getHostAddress()).isNotNull(); - - boolean ipv4 = address.getAddress() instanceof Inet4Address; - - Context context = Context.root(); - - // when - AttributesBuilder startAttributes = Attributes.builder(); - extractor.onStart(startAttributes, context, address); - - AttributesBuilder endAttributes = Attributes.builder(); - extractor.onEnd(endAttributes, context, address, address, null); - - // then - assertThat(startAttributes.build()).isEmpty(); - - AttributesBuilder builder = Attributes.builder(); - builder.put(SemanticAttributes.NET_SOCK_PEER_ADDR, address.getAddress().getHostAddress()); - if (!ipv4) { - builder.put(SemanticAttributes.NET_SOCK_FAMILY, "inet6"); - } - builder.put(SemanticAttributes.NET_SOCK_PEER_NAME, "api.github.com"); - builder.put(SemanticAttributes.NET_SOCK_PEER_PORT, 456L); - - assertThat(endAttributes.build()).isEqualTo(builder.build()); - } - - @Test - void unresolved() { - // given - InetSocketAddress address = InetSocketAddress.createUnresolved("api.github.com", 456); - assertThat(address.getAddress()).isNull(); - - Context context = Context.root(); - - // when - AttributesBuilder startAttributes = Attributes.builder(); - extractor.onStart(startAttributes, context, address); - - AttributesBuilder endAttributes = Attributes.builder(); - extractor.onEnd(endAttributes, context, address, address, null); - - // then - assertThat(startAttributes.build()).isEmpty(); - - assertThat(endAttributes.build()).isEmpty(); - } -} diff --git a/instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/net/InetSocketAddressNetServerAttributesGetterTest.java b/instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/net/InetSocketAddressNetServerAttributesGetterTest.java deleted file mode 100644 index 60943f038ebf..000000000000 --- a/instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/net/InetSocketAddressNetServerAttributesGetterTest.java +++ /dev/null @@ -1,137 +0,0 @@ -/* - * 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.context.Context; -import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; -import java.net.Inet4Address; -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 InetSocketAddressNetServerAttributesGetterTest { - - static class TestNetServerAttributesGetter - implements NetServerAttributesGetter { - - @Override - public String getServerAddress(Addresses request) { - // net.host.name and net.host.port are tested in NetClientAttributesExtractorTest - return null; - } - - @Override - public Integer getServerPort(Addresses request) { - // net.host.name and net.host.port are tested in NetClientAttributesExtractorTest - return null; - } - - @Override - public InetSocketAddress getClientInetSocketAddress(Addresses request, Addresses response) { - return request.peer; - } - - @Override - public InetSocketAddress getServerInetSocketAddress(Addresses request, Addresses response) { - return request.host; - } - } - - private final AttributesExtractor extractor = - NetServerAttributesExtractor.create(new TestNetServerAttributesGetter()); - - @Test - void noInetSocketAddress() { - AttributesBuilder attributes = Attributes.builder(); - extractor.onStart(attributes, Context.root(), new Addresses(null, null)); - assertThat(attributes.build()).isEmpty(); - } - - @Test - void fullAddress() { - // given - Addresses request = - new Addresses( - new InetSocketAddress("github.com", 123), new InetSocketAddress("api.github.com", 456)); - assertThat(request.peer.getAddress().getHostAddress()).isNotNull(); - assertThat(request.host.getAddress().getHostAddress()).isNotNull(); - - Context context = Context.root(); - - // when - AttributesBuilder startAttributes = Attributes.builder(); - extractor.onStart(startAttributes, context, request); - - AttributesBuilder endAttributes = Attributes.builder(); - extractor.onEnd(endAttributes, context, request, request, null); - - // then - if (!request.isIpv4()) { - assertThat(startAttributes.build()) - .isEqualTo(Attributes.of(SemanticAttributes.NET_SOCK_FAMILY, "inet6")); - } else { - assertThat(startAttributes.build()).isEmpty(); - } - - assertThat(endAttributes.build()) - .containsOnly( - entry( - SemanticAttributes.NET_SOCK_HOST_ADDR, request.host.getAddress().getHostAddress()), - entry(SemanticAttributes.NET_SOCK_HOST_PORT, 456L), - entry( - SemanticAttributes.NET_SOCK_PEER_ADDR, request.peer.getAddress().getHostAddress()), - entry(SemanticAttributes.NET_SOCK_PEER_PORT, 123L)); - } - - @Test - void unresolved() { - // given - Addresses request = - new Addresses( - InetSocketAddress.createUnresolved("github.com", 123), - InetSocketAddress.createUnresolved("api.github.com", 456)); - assertThat(request.peer.getAddress()).isNull(); - assertThat(request.host.getAddress()).isNull(); - - Context context = Context.root(); - - // when - AttributesBuilder startAttributes = Attributes.builder(); - extractor.onStart(startAttributes, context, request); - - AttributesBuilder endAttributes = Attributes.builder(); - extractor.onEnd(endAttributes, context, request, request, null); - - // then - assertThat(startAttributes.build()).isEmpty(); - - assertThat(endAttributes.build()) - .containsOnly(entry(SemanticAttributes.NET_SOCK_PEER_PORT, 123L)); - } - - static final class Addresses { - - private final InetSocketAddress peer; - private final InetSocketAddress host; - - Addresses(InetSocketAddress peer, InetSocketAddress host) { - this.peer = peer; - this.host = host; - } - - boolean isIpv4() { - return peer.getAddress() instanceof Inet4Address; - } - } -} diff --git a/instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/net/NetClientAttributesExtractorTest.java b/instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/net/NetClientAttributesExtractorTest.java deleted file mode 100644 index 84cc2e27cf10..000000000000 --- a/instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/net/NetClientAttributesExtractorTest.java +++ /dev/null @@ -1,276 +0,0 @@ -/* - * 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 io.opentelemetry.semconv.trace.attributes.SemanticAttributes.NetTransportValues.IP_TCP; -import static java.util.Collections.emptyMap; -import static org.assertj.core.api.Assertions.entry; - -import io.opentelemetry.api.common.Attributes; -import io.opentelemetry.api.common.AttributesBuilder; -import io.opentelemetry.context.Context; -import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.net.internal.NetAttributes; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; -import java.util.HashMap; -import java.util.Map; -import javax.annotation.Nullable; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; - -class NetClientAttributesExtractorTest { - - static class TestNetClientAttributesGetter - implements NetClientAttributesGetter, Map> { - - @Override - public String getTransport(Map request, Map response) { - return response.get("netTransport"); - } - - @Nullable - @Override - public String getNetworkTransport( - Map request, @Nullable Map response) { - return request.get("transport"); - } - - @Nullable - @Override - public String getNetworkType( - Map request, @Nullable Map response) { - return request.get("type"); - } - - @Nullable - @Override - public String getNetworkProtocolName( - Map request, @Nullable Map response) { - return request.get("protocolName"); - } - - @Nullable - @Override - public String getNetworkProtocolVersion( - Map request, @Nullable Map response) { - return request.get("protocolVersion"); - } - - @Override - public String getServerAddress(Map request) { - return request.get("peerName"); - } - - @Override - public Integer getServerPort(Map request) { - String peerPort = request.get("peerPort"); - return peerPort == null ? null : Integer.valueOf(peerPort); - } - - @Override - public String getSockFamily(Map request, Map response) { - return response.get("sockFamily"); - } - - @Override - public String getServerSocketDomain(Map request, Map response) { - return response.get("sockPeerName"); - } - - @Override - public String getServerSocketAddress( - Map request, Map response) { - return response.get("sockPeerAddr"); - } - - @Override - public Integer getServerSocketPort(Map request, Map response) { - String sockPeerPort = response.get("sockPeerPort"); - return sockPeerPort == null ? null : Integer.valueOf(sockPeerPort); - } - } - - private final AttributesExtractor, Map> extractor = - NetClientAttributesExtractor.create(new TestNetClientAttributesGetter()); - - @Test - void normal() { - // given - Map map = new HashMap<>(); - map.put("netTransport", IP_TCP); - map.put("transport", "tcp"); - map.put("type", "ipv6"); - map.put("protocolName", "http"); - map.put("protocolVersion", "1.1"); - map.put("peerName", "opentelemetry.io"); - map.put("peerPort", "42"); - map.put("sockFamily", "inet6"); - map.put("sockPeerAddr", "1:2:3:4::"); - map.put("sockPeerName", "proxy.opentelemetry.io"); - map.put("sockPeerPort", "123"); - - Context context = Context.root(); - - // when - AttributesBuilder startAttributes = Attributes.builder(); - extractor.onStart(startAttributes, context, map); - - AttributesBuilder endAttributes = Attributes.builder(); - extractor.onEnd(endAttributes, context, map, map, null); - - // then - assertThat(startAttributes.build()) - .containsOnly( - entry(SemanticAttributes.NET_PEER_NAME, "opentelemetry.io"), - entry(SemanticAttributes.NET_PEER_PORT, 42L)); - - assertThat(endAttributes.build()) - .containsOnly( - entry(SemanticAttributes.NET_TRANSPORT, IP_TCP), - entry(NetAttributes.NET_PROTOCOL_NAME, "http"), - entry(NetAttributes.NET_PROTOCOL_VERSION, "1.1"), - entry(SemanticAttributes.NET_SOCK_FAMILY, "inet6"), - entry(SemanticAttributes.NET_SOCK_PEER_ADDR, "1:2:3:4::"), - entry(SemanticAttributes.NET_SOCK_PEER_NAME, "proxy.opentelemetry.io"), - entry(SemanticAttributes.NET_SOCK_PEER_PORT, 123L)); - } - - @Test - void empty() { - // given - Context context = Context.root(); - - // when - AttributesBuilder startAttributes = Attributes.builder(); - extractor.onStart(startAttributes, context, emptyMap()); - - AttributesBuilder endAttributes = Attributes.builder(); - extractor.onEnd(endAttributes, context, emptyMap(), emptyMap(), null); - - // then - assertThat(startAttributes.build()).isEmpty(); - assertThat(endAttributes.build()).isEmpty(); - } - - @Test - @DisplayName("does not set any net.sock.* attributes when net.peer.name = net.sock.peer.addr") - void doesNotSetDuplicates1() { - // given - Map map = new HashMap<>(); - map.put("netTransport", IP_TCP); - map.put("peerName", "1:2:3:4::"); - map.put("peerPort", "42"); - map.put("sockFamily", "inet6"); - map.put("sockPeerAddr", "1:2:3:4::"); - map.put("sockPeerName", "proxy.opentelemetry.io"); - map.put("sockPeerPort", "123"); - - Context context = Context.root(); - - // when - AttributesBuilder startAttributes = Attributes.builder(); - extractor.onStart(startAttributes, context, map); - - AttributesBuilder endAttributes = Attributes.builder(); - extractor.onEnd(endAttributes, context, map, map, null); - - // then - assertThat(startAttributes.build()) - .containsOnly( - entry(SemanticAttributes.NET_PEER_NAME, "1:2:3:4::"), - entry(SemanticAttributes.NET_PEER_PORT, 42L)); - - assertThat(endAttributes.build()).containsOnly(entry(SemanticAttributes.NET_TRANSPORT, IP_TCP)); - } - - @Test - @DisplayName( - "does not set net.sock.* attributes when they duplicate related net.peer.* attributes") - void doesNotSetDuplicates2() { - // given - Map map = new HashMap<>(); - map.put("netTransport", IP_TCP); - map.put("peerName", "opentelemetry.io"); - map.put("peerPort", "42"); - map.put("sockFamily", "inet6"); - map.put("sockPeerAddr", "1:2:3:4::"); - map.put("sockPeerName", "opentelemetry.io"); - map.put("sockPeerPort", "42"); - - Context context = Context.root(); - - // when - AttributesBuilder startAttributes = Attributes.builder(); - extractor.onStart(startAttributes, context, map); - - AttributesBuilder endAttributes = Attributes.builder(); - extractor.onEnd(endAttributes, context, map, map, null); - - // then - assertThat(startAttributes.build()) - .containsOnly( - entry(SemanticAttributes.NET_PEER_NAME, "opentelemetry.io"), - entry(SemanticAttributes.NET_PEER_PORT, 42L)); - - assertThat(endAttributes.build()) - .containsOnly( - entry(SemanticAttributes.NET_TRANSPORT, IP_TCP), - entry(SemanticAttributes.NET_SOCK_FAMILY, "inet6"), - entry(SemanticAttributes.NET_SOCK_PEER_ADDR, "1:2:3:4::")); - } - - @Test - void doesNotSetNegativePortValues() { - // given - Map map = new HashMap<>(); - map.put("peerName", "opentelemetry.io"); - map.put("peerPort", "-12"); - map.put("sockPeerAddr", "1:2:3:4::"); - map.put("sockPeerPort", "-42"); - - Context context = Context.root(); - - // when - AttributesBuilder startAttributes = Attributes.builder(); - extractor.onStart(startAttributes, context, map); - - AttributesBuilder endAttributes = Attributes.builder(); - extractor.onEnd(endAttributes, context, map, map, null); - - // then - assertThat(startAttributes.build()) - .containsOnly(entry(SemanticAttributes.NET_PEER_NAME, "opentelemetry.io")); - - assertThat(endAttributes.build()) - .containsOnly(entry(SemanticAttributes.NET_SOCK_PEER_ADDR, "1:2:3:4::")); - } - - @Test - void doesNotSetSockFamilyInet() { - // given - Map map = new HashMap<>(); - map.put("peerName", "opentelemetry.io"); - map.put("sockPeerAddr", "1.2.3.4"); - map.put("sockFamily", SemanticAttributes.NetSockFamilyValues.INET); - - Context context = Context.root(); - - // when - AttributesBuilder startAttributes = Attributes.builder(); - extractor.onStart(startAttributes, context, map); - - AttributesBuilder endAttributes = Attributes.builder(); - extractor.onEnd(endAttributes, context, map, map, null); - - // then - assertThat(startAttributes.build()) - .containsOnly(entry(SemanticAttributes.NET_PEER_NAME, "opentelemetry.io")); - - assertThat(endAttributes.build()) - .containsOnly(entry(SemanticAttributes.NET_SOCK_PEER_ADDR, "1.2.3.4")); - } -} diff --git a/instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/net/NetServerAttributesExtractorTest.java b/instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/net/NetServerAttributesExtractorTest.java deleted file mode 100644 index 26e726619d74..000000000000 --- a/instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/net/NetServerAttributesExtractorTest.java +++ /dev/null @@ -1,287 +0,0 @@ -/* - * 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 io.opentelemetry.semconv.trace.attributes.SemanticAttributes.NetTransportValues.IP_TCP; -import static java.util.Collections.emptyMap; -import static org.assertj.core.api.Assertions.entry; - -import io.opentelemetry.api.common.Attributes; -import io.opentelemetry.api.common.AttributesBuilder; -import io.opentelemetry.context.Context; -import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.net.internal.NetAttributes; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; -import java.util.HashMap; -import java.util.Map; -import javax.annotation.Nullable; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; - -class NetServerAttributesExtractorTest { - - static class TestNetServerAttributesGetter - implements NetServerAttributesGetter, Void> { - - @Override - public String getTransport(Map request) { - return request.get("netTransport"); - } - - @Nullable - @Override - public String getNetworkTransport(Map request, @Nullable Void response) { - return request.get("transport"); - } - - @Nullable - @Override - public String getNetworkType(Map request, @Nullable Void response) { - return request.get("type"); - } - - @Nullable - @Override - public String getNetworkProtocolName(Map request, Void response) { - return request.get("protocolName"); - } - - @Nullable - @Override - public String getNetworkProtocolVersion(Map request, Void response) { - return request.get("protocolVersion"); - } - - @Nullable - @Override - public String getServerAddress(Map request) { - return request.get("hostName"); - } - - @Nullable - @Override - public Integer getServerPort(Map request) { - String hostPort = request.get("hostPort"); - return hostPort == null ? null : Integer.valueOf(hostPort); - } - - @Nullable - @Override - public String getSockFamily(Map request) { - return request.get("sockFamily"); - } - - @Override - public String getClientSocketAddress(Map request, Void response) { - return request.get("sockPeerAddr"); - } - - @Override - public Integer getClientSocketPort(Map request, Void response) { - String sockPeerPort = request.get("sockPeerPort"); - return sockPeerPort == null ? null : Integer.valueOf(sockPeerPort); - } - - @Nullable - @Override - public String getServerSocketAddress(Map request, Void response) { - return request.get("sockHostAddr"); - } - - @Nullable - @Override - public Integer getServerSocketPort(Map request, Void response) { - String sockHostPort = request.get("sockHostPort"); - return sockHostPort == null ? null : Integer.valueOf(sockHostPort); - } - } - - AttributesExtractor, Void> extractor = - NetServerAttributesExtractor.create(new TestNetServerAttributesGetter()); - - @Test - void normal() { - // given - Map map = new HashMap<>(); - map.put("netTransport", IP_TCP); - map.put("transport", "tcp"); - map.put("type", "ipv6"); - map.put("protocolName", "http"); - map.put("protocolVersion", "1.1"); - map.put("hostName", "opentelemetry.io"); - map.put("hostPort", "80"); - map.put("sockFamily", "inet6"); - map.put("sockPeerAddr", "1:2:3:4::"); - map.put("sockPeerPort", "42"); - map.put("sockHostAddr", "4:3:2:1::"); - map.put("sockHostPort", "8080"); - - Context context = Context.root(); - - // when - AttributesBuilder startAttributes = Attributes.builder(); - extractor.onStart(startAttributes, context, map); - - AttributesBuilder endAttributes = Attributes.builder(); - extractor.onEnd(endAttributes, context, map, null, null); - - // then - assertThat(startAttributes.build()) - .containsOnly( - entry(SemanticAttributes.NET_TRANSPORT, IP_TCP), - entry(SemanticAttributes.NET_HOST_NAME, "opentelemetry.io"), - entry(SemanticAttributes.NET_HOST_PORT, 80L), - entry(SemanticAttributes.NET_SOCK_FAMILY, "inet6")); - - assertThat(endAttributes.build()) - .containsOnly( - entry(NetAttributes.NET_PROTOCOL_NAME, "http"), - entry(NetAttributes.NET_PROTOCOL_VERSION, "1.1"), - entry(SemanticAttributes.NET_SOCK_HOST_ADDR, "4:3:2:1::"), - entry(SemanticAttributes.NET_SOCK_HOST_PORT, 8080L), - entry(SemanticAttributes.NET_SOCK_PEER_ADDR, "1:2:3:4::"), - entry(SemanticAttributes.NET_SOCK_PEER_PORT, 42L)); - } - - @Test - void empty() { - // given - Context context = Context.root(); - - // when - AttributesBuilder startAttributes = Attributes.builder(); - extractor.onStart(startAttributes, context, emptyMap()); - - AttributesBuilder endAttributes = Attributes.builder(); - extractor.onEnd(endAttributes, context, emptyMap(), null, null); - - // then - assertThat(startAttributes.build()).isEmpty(); - assertThat(endAttributes.build()).isEmpty(); - } - - @Test - @DisplayName( - "does not set any net.sock.host.* attributes when net.host.name = net.sock.host.addr") - void doesNotSetDuplicates1() { - // given - Map map = new HashMap<>(); - map.put("netTransport", IP_TCP); - map.put("hostName", "4:3:2:1::"); - map.put("hostPort", "80"); - map.put("sockFamily", "inet6"); - map.put("sockHostAddr", "4:3:2:1::"); - map.put("sockHostPort", "8080"); - - Context context = Context.root(); - - // when - AttributesBuilder startAttributes = Attributes.builder(); - extractor.onStart(startAttributes, context, map); - - AttributesBuilder endAttributes = Attributes.builder(); - extractor.onEnd(endAttributes, context, map, null, null); - - // then - assertThat(startAttributes.build()) - .containsOnly( - entry(SemanticAttributes.NET_TRANSPORT, IP_TCP), - entry(SemanticAttributes.NET_HOST_NAME, "4:3:2:1::"), - entry(SemanticAttributes.NET_HOST_PORT, 80L)); - - assertThat(endAttributes.build()).isEmpty(); - } - - @Test - @DisplayName( - "does not set net.sock.host.* attributes when they duplicate related net.host.* attributes") - void doesNotSetDuplicates2() { - // given - Map map = new HashMap<>(); - map.put("netTransport", IP_TCP); - map.put("hostName", "opentelemetry.io"); - map.put("hostPort", "80"); - map.put("sockFamily", "inet6"); - map.put("sockHostAddr", "4:3:2:1::"); - map.put("sockHostPort", "80"); - - Context context = Context.root(); - - // when - AttributesBuilder startAttributes = Attributes.builder(); - extractor.onStart(startAttributes, context, map); - - AttributesBuilder endAttributes = Attributes.builder(); - extractor.onEnd(endAttributes, context, map, null, null); - - // then - assertThat(startAttributes.build()) - .containsOnly( - entry(SemanticAttributes.NET_TRANSPORT, IP_TCP), - entry(SemanticAttributes.NET_HOST_NAME, "opentelemetry.io"), - entry(SemanticAttributes.NET_HOST_PORT, 80L), - entry(SemanticAttributes.NET_SOCK_FAMILY, "inet6")); - - assertThat(endAttributes.build()) - .containsOnly(entry(SemanticAttributes.NET_SOCK_HOST_ADDR, "4:3:2:1::")); - } - - @Test - void doesNotSetNegativePort() { - // given - Map map = new HashMap<>(); - map.put("hostName", "opentelemetry.io"); - map.put("hostPort", "-80"); - map.put("sockPeerAddr", "1:2:3:4::"); - map.put("sockPeerPort", "-42"); - map.put("sockHostAddr", "4:3:2:1::"); - map.put("sockHostPort", "-8080"); - - Context context = Context.root(); - - // when - AttributesBuilder startAttributes = Attributes.builder(); - extractor.onStart(startAttributes, context, map); - - AttributesBuilder endAttributes = Attributes.builder(); - extractor.onEnd(endAttributes, context, map, null, null); - - // then - assertThat(startAttributes.build()) - .containsOnly(entry(SemanticAttributes.NET_HOST_NAME, "opentelemetry.io")); - - assertThat(endAttributes.build()) - .containsOnly( - entry(SemanticAttributes.NET_SOCK_HOST_ADDR, "4:3:2:1::"), - entry(SemanticAttributes.NET_SOCK_PEER_ADDR, "1:2:3:4::")); - } - - @Test - void doesNotSetSockFamilyInet() { - // given - Map map = new HashMap<>(); - map.put("hostName", "opentelemetry.io"); - map.put("sockPeerAddr", "1.2.3.4"); - map.put("sockFamily", SemanticAttributes.NetSockFamilyValues.INET); - - Context context = Context.root(); - - // when - AttributesBuilder startAttributes = Attributes.builder(); - extractor.onStart(startAttributes, context, map); - - AttributesBuilder endAttributes = Attributes.builder(); - extractor.onEnd(endAttributes, context, map, null, null); - - // then - assertThat(startAttributes.build()) - .containsOnly(entry(SemanticAttributes.NET_HOST_NAME, "opentelemetry.io")); - - assertThat(endAttributes.build()) - .containsOnly(entry(SemanticAttributes.NET_SOCK_PEER_ADDR, "1.2.3.4")); - } -} diff --git a/instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/network/ServerAttributesExtractorInetSocketAddressTest.java b/instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/network/ServerAttributesExtractorInetSocketAddressTest.java deleted file mode 100644 index 4d3a40a0a1ec..000000000000 --- a/instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/network/ServerAttributesExtractorInetSocketAddressTest.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.api.instrumenter.network; - -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.context.Context; -import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.network.internal.NetworkAttributes; -import java.net.InetSocketAddress; -import javax.annotation.Nullable; -import org.junit.jupiter.api.Test; - -class ServerAttributesExtractorInetSocketAddressTest { - - static class TestServerAttributesGetter - implements ServerAttributesGetter { - - @Nullable - @Override - public String getServerAddress(InetSocketAddress request) { - // covered in ServerAttributesExtractorTest - return null; - } - - @Nullable - @Override - public Integer getServerPort(InetSocketAddress request) { - // covered in ServerAttributesExtractorTest - return null; - } - - @Nullable - @Override - public InetSocketAddress getServerInetSocketAddress( - InetSocketAddress request, @Nullable Void response) { - return request; - } - } - - @Test - void fullAddress() { - InetSocketAddress address = new InetSocketAddress("api.github.com", 456); - assertThat(address.getAddress().getHostAddress()).isNotNull(); - - AttributesExtractor extractor = - ServerAttributesExtractor.create(new TestServerAttributesGetter()); - - AttributesBuilder startAttributes = Attributes.builder(); - extractor.onStart(startAttributes, Context.root(), address); - assertThat(startAttributes.build()).isEmpty(); - - AttributesBuilder endAttributes = Attributes.builder(); - extractor.onEnd(endAttributes, Context.root(), address, null, null); - assertThat(endAttributes.build()) - .containsOnly( - entry(NetworkAttributes.SERVER_SOCKET_DOMAIN, "api.github.com"), - entry(NetworkAttributes.SERVER_SOCKET_ADDRESS, address.getAddress().getHostAddress()), - entry(NetworkAttributes.SERVER_SOCKET_PORT, 456L)); - } - - @Test - void noAttributes() { - AttributesExtractor extractor = - ServerAttributesExtractor.create(new TestServerAttributesGetter()); - - AttributesBuilder startAttributes = Attributes.builder(); - extractor.onStart(startAttributes, Context.root(), null); - assertThat(startAttributes.build()).isEmpty(); - - AttributesBuilder endAttributes = Attributes.builder(); - extractor.onEnd(endAttributes, Context.root(), null, null, null); - assertThat(endAttributes.build()).isEmpty(); - } -} diff --git a/instrumentation-api-semconv/src/testBothHttpSemconv/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpClientAttributesExtractorBothSemconvTest.java b/instrumentation-api-semconv/src/testBothHttpSemconv/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpClientAttributesExtractorBothSemconvTest.java deleted file mode 100644 index 0871425910dc..000000000000 --- a/instrumentation-api-semconv/src/testBothHttpSemconv/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpClientAttributesExtractorBothSemconvTest.java +++ /dev/null @@ -1,179 +0,0 @@ -/* - * 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 java.util.Arrays.asList; -import static java.util.Collections.emptyList; -import static java.util.Collections.singletonList; -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.context.Context; -import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.net.NetClientAttributesGetter; -import io.opentelemetry.instrumentation.api.instrumenter.net.internal.NetAttributes; -import io.opentelemetry.instrumentation.api.instrumenter.network.internal.NetworkAttributes; -import io.opentelemetry.instrumentation.api.instrumenter.url.internal.UrlAttributes; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.function.ToIntFunction; -import javax.annotation.Nullable; -import org.junit.jupiter.api.Test; - -class HttpClientAttributesExtractorBothSemconvTest { - - static class TestHttpClientAttributesGetter - implements HttpClientAttributesGetter, Map> { - - @Override - public String getUrlFull(Map request) { - return request.get("url"); - } - - @Override - public String getHttpRequestMethod(Map request) { - return request.get("method"); - } - - @Override - public List getHttpRequestHeader(Map request, String name) { - String value = request.get("header." + name); - return value == null ? emptyList() : asList(value.split(",")); - } - - @Override - public Integer getHttpResponseStatusCode( - Map request, Map response, @Nullable Throwable error) { - return Integer.parseInt(response.get("statusCode")); - } - - @Override - public List getHttpResponseHeader( - Map request, Map response, String name) { - String value = response.get("header." + name); - return value == null ? emptyList() : asList(value.split(",")); - } - } - - static class TestNetClientAttributesGetter - implements NetClientAttributesGetter, Map> { - - @Nullable - @Override - public String getNetworkTransport( - Map request, @Nullable Map response) { - return request.get("transport"); - } - - @Nullable - @Override - public String getNetworkType( - Map request, @Nullable Map response) { - return request.get("type"); - } - - @Nullable - @Override - public String getNetworkProtocolName( - Map request, @Nullable Map response) { - return request.get("protocolName"); - } - - @Nullable - @Override - public String getNetworkProtocolVersion( - Map request, @Nullable Map response) { - return request.get("protocolVersion"); - } - - @Nullable - @Override - public String getServerAddress(Map request) { - return request.get("peerName"); - } - - @Nullable - @Override - public Integer getServerPort(Map request) { - String statusCode = request.get("peerPort"); - return statusCode == null ? null : Integer.parseInt(statusCode); - } - } - - @Test - void normal() { - Map request = new HashMap<>(); - request.put("method", "POST"); - request.put("url", "http://github.com"); - request.put("header.content-length", "10"); - request.put("header.user-agent", "okhttp 3.x"); - request.put("header.custom-request-header", "123,456"); - request.put("transport", "udp"); - request.put("type", "ipv4"); - request.put("protocolName", "http"); - request.put("protocolVersion", "1.1"); - request.put("peerName", "github.com"); - request.put("peerPort", "123"); - - Map response = new HashMap<>(); - response.put("statusCode", "202"); - response.put("header.content-length", "20"); - response.put("header.custom-response-header", "654,321"); - - ToIntFunction resendCountFromContext = context -> 2; - - AttributesExtractor, Map> extractor = - new HttpClientAttributesExtractor<>( - new TestHttpClientAttributesGetter(), - new TestNetClientAttributesGetter(), - singletonList("Custom-Request-Header"), - singletonList("Custom-Response-Header"), - resendCountFromContext); - - AttributesBuilder startAttributes = Attributes.builder(); - extractor.onStart(startAttributes, Context.root(), request); - assertThat(startAttributes.build()) - .containsOnly( - entry(SemanticAttributes.HTTP_METHOD, "POST"), - entry(HttpAttributes.HTTP_REQUEST_METHOD, "POST"), - entry(SemanticAttributes.HTTP_URL, "http://github.com"), - entry(UrlAttributes.URL_FULL, "http://github.com"), - entry(SemanticAttributes.USER_AGENT_ORIGINAL, "okhttp 3.x"), - entry( - AttributeKey.stringArrayKey("http.request.header.custom_request_header"), - asList("123", "456")), - entry(SemanticAttributes.NET_PEER_NAME, "github.com"), - entry(SemanticAttributes.NET_PEER_PORT, 123L), - entry(NetworkAttributes.SERVER_ADDRESS, "github.com"), - entry(NetworkAttributes.SERVER_PORT, 123L)); - - AttributesBuilder endAttributes = Attributes.builder(); - extractor.onEnd(endAttributes, Context.root(), request, response, null); - assertThat(endAttributes.build()) - .containsOnly( - entry(SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH, 10L), - entry(HttpAttributes.HTTP_REQUEST_BODY_SIZE, 10L), - entry(SemanticAttributes.HTTP_STATUS_CODE, 202L), - entry(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, 202L), - entry(SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH, 20L), - entry(HttpAttributes.HTTP_RESPONSE_BODY_SIZE, 20L), - entry(SemanticAttributes.HTTP_RESEND_COUNT, 2L), - entry( - AttributeKey.stringArrayKey("http.response.header.custom_response_header"), - asList("654", "321")), - entry(NetAttributes.NET_PROTOCOL_NAME, "http"), - entry(NetAttributes.NET_PROTOCOL_VERSION, "1.1"), - entry(NetworkAttributes.NETWORK_TRANSPORT, "udp"), - entry(NetworkAttributes.NETWORK_TYPE, "ipv4"), - entry(NetworkAttributes.NETWORK_PROTOCOL_NAME, "http"), - entry(NetworkAttributes.NETWORK_PROTOCOL_VERSION, "1.1")); - } -} diff --git a/instrumentation-api-semconv/src/testBothHttpSemconv/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpServerAttributesExtractorBothSemconvTest.java b/instrumentation-api-semconv/src/testBothHttpSemconv/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpServerAttributesExtractorBothSemconvTest.java deleted file mode 100644 index 8f926a62da85..000000000000 --- a/instrumentation-api-semconv/src/testBothHttpSemconv/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpServerAttributesExtractorBothSemconvTest.java +++ /dev/null @@ -1,203 +0,0 @@ -/* - * 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 java.util.Arrays.asList; -import static java.util.Collections.emptyList; -import static java.util.Collections.singletonList; -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.context.Context; -import io.opentelemetry.instrumentation.api.instrumenter.net.NetServerAttributesGetter; -import io.opentelemetry.instrumentation.api.instrumenter.net.internal.NetAttributes; -import io.opentelemetry.instrumentation.api.instrumenter.network.internal.NetworkAttributes; -import io.opentelemetry.instrumentation.api.instrumenter.url.internal.UrlAttributes; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.function.Function; -import javax.annotation.Nullable; -import org.junit.jupiter.api.Test; - -class HttpServerAttributesExtractorBothSemconvTest { - - static class TestHttpServerAttributesGetter - implements HttpServerAttributesGetter, Map> { - - @Override - public String getHttpRequestMethod(Map request) { - return (String) request.get("method"); - } - - @Override - public String getUrlScheme(Map request) { - return (String) request.get("scheme"); - } - - @Nullable - @Override - public String getUrlPath(Map request) { - return (String) request.get("path"); - } - - @Nullable - @Override - public String getUrlQuery(Map request) { - return (String) request.get("query"); - } - - @Override - public String getHttpRoute(Map request) { - return (String) request.get("route"); - } - - @Override - public List getHttpRequestHeader(Map request, String name) { - String values = (String) request.get("header." + name); - return values == null ? emptyList() : asList(values.split(",")); - } - - @Override - public Integer getHttpResponseStatusCode( - Map request, Map response, @Nullable Throwable error) { - String value = (String) response.get("statusCode"); - return value == null ? null : Integer.parseInt(value); - } - - @Override - public List getHttpResponseHeader( - Map request, Map response, String name) { - String values = (String) response.get("header." + name); - return values == null ? emptyList() : asList(values.split(",")); - } - } - - static class TestNetServerAttributesGetter - implements NetServerAttributesGetter, Map> { - - @Nullable - @Override - public String getNetworkTransport( - Map request, @Nullable Map response) { - return (String) request.get("transport"); - } - - @Nullable - @Override - public String getNetworkType( - Map request, @Nullable Map response) { - return (String) request.get("type"); - } - - @Nullable - @Override - public String getNetworkProtocolName( - Map request, Map response) { - return (String) request.get("protocolName"); - } - - @Nullable - @Override - public String getNetworkProtocolVersion( - Map request, Map response) { - return (String) request.get("protocolVersion"); - } - - @Nullable - @Override - public String getServerAddress(Map request) { - return (String) request.get("hostName"); - } - - @Nullable - @Override - public Integer getServerPort(Map request) { - return (Integer) request.get("hostPort"); - } - } - - @Test - void normal() { - Map request = new HashMap<>(); - request.put("method", "POST"); - request.put("url", "http://github.com"); - request.put("path", "/repositories/1"); - request.put("query", "details=true"); - request.put("scheme", "http"); - request.put("header.content-length", "10"); - request.put("route", "/repositories/{id}"); - request.put("header.user-agent", "okhttp 3.x"); - request.put("header.host", "github.com"); - request.put("header.forwarded", "for=1.1.1.1;proto=https"); - request.put("header.custom-request-header", "123,456"); - request.put("transport", "udp"); - request.put("type", "ipv4"); - request.put("protocolName", "http"); - request.put("protocolVersion", "2.0"); - - Map response = new HashMap<>(); - response.put("statusCode", "202"); - response.put("header.content-length", "20"); - response.put("header.custom-response-header", "654,321"); - - Function routeFromContext = ctx -> "/repositories/{repoId}"; - - HttpServerAttributesExtractor, Map> extractor = - new HttpServerAttributesExtractor<>( - new TestHttpServerAttributesGetter(), - new TestNetServerAttributesGetter(), - singletonList("Custom-Request-Header"), - singletonList("Custom-Response-Header"), - routeFromContext); - - AttributesBuilder startAttributes = Attributes.builder(); - extractor.onStart(startAttributes, Context.root(), request); - assertThat(startAttributes.build()) - .containsOnly( - entry(SemanticAttributes.NET_HOST_NAME, "github.com"), - entry(NetworkAttributes.SERVER_ADDRESS, "github.com"), - entry(SemanticAttributes.HTTP_METHOD, "POST"), - entry(HttpAttributes.HTTP_REQUEST_METHOD, "POST"), - entry(SemanticAttributes.HTTP_SCHEME, "http"), - entry(SemanticAttributes.HTTP_TARGET, "/repositories/1?details=true"), - entry(UrlAttributes.URL_SCHEME, "http"), - entry(UrlAttributes.URL_PATH, "/repositories/1"), - entry(UrlAttributes.URL_QUERY, "details=true"), - entry(SemanticAttributes.USER_AGENT_ORIGINAL, "okhttp 3.x"), - entry(SemanticAttributes.HTTP_ROUTE, "/repositories/{id}"), - entry(SemanticAttributes.HTTP_CLIENT_IP, "1.1.1.1"), - entry(NetworkAttributes.CLIENT_ADDRESS, "1.1.1.1"), - entry( - AttributeKey.stringArrayKey("http.request.header.custom_request_header"), - asList("123", "456"))); - - AttributesBuilder endAttributes = Attributes.builder(); - extractor.onEnd(endAttributes, Context.root(), request, response, null); - assertThat(endAttributes.build()) - .containsOnly( - entry(NetAttributes.NET_PROTOCOL_NAME, "http"), - entry(NetAttributes.NET_PROTOCOL_VERSION, "2.0"), - entry(NetworkAttributes.NETWORK_TRANSPORT, "udp"), - entry(NetworkAttributes.NETWORK_TYPE, "ipv4"), - entry(NetworkAttributes.NETWORK_PROTOCOL_NAME, "http"), - entry(NetworkAttributes.NETWORK_PROTOCOL_VERSION, "2.0"), - entry(SemanticAttributes.HTTP_ROUTE, "/repositories/{repoId}"), - entry(SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH, 10L), - entry(HttpAttributes.HTTP_REQUEST_BODY_SIZE, 10L), - entry(SemanticAttributes.HTTP_STATUS_CODE, 202L), - entry(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, 202L), - entry(SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH, 20L), - entry(HttpAttributes.HTTP_RESPONSE_BODY_SIZE, 20L), - entry( - AttributeKey.stringArrayKey("http.response.header.custom_response_header"), - asList("654", "321"))); - } -} diff --git a/instrumentation-api-semconv/src/testBothHttpSemconv/java/io/opentelemetry/instrumentation/api/instrumenter/net/NetClientAttributesExtractorBothSemconvTest.java b/instrumentation-api-semconv/src/testBothHttpSemconv/java/io/opentelemetry/instrumentation/api/instrumenter/net/NetClientAttributesExtractorBothSemconvTest.java deleted file mode 100644 index 2b20c5dc41f4..000000000000 --- a/instrumentation-api-semconv/src/testBothHttpSemconv/java/io/opentelemetry/instrumentation/api/instrumenter/net/NetClientAttributesExtractorBothSemconvTest.java +++ /dev/null @@ -1,149 +0,0 @@ -/* - * 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 io.opentelemetry.semconv.trace.attributes.SemanticAttributes.NetTransportValues.IP_TCP; -import static org.assertj.core.api.Assertions.entry; - -import io.opentelemetry.api.common.Attributes; -import io.opentelemetry.api.common.AttributesBuilder; -import io.opentelemetry.context.Context; -import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.net.internal.NetAttributes; -import io.opentelemetry.instrumentation.api.instrumenter.network.internal.NetworkAttributes; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; -import java.util.HashMap; -import java.util.Map; -import javax.annotation.Nullable; -import org.junit.jupiter.api.Test; - -class NetClientAttributesExtractorBothSemconvTest { - - static class TestNetClientAttributesGetter - implements NetClientAttributesGetter, Map> { - - @Override - public String getTransport(Map request, Map response) { - return response.get("netTransport"); - } - - @Nullable - @Override - public String getNetworkTransport( - Map request, @Nullable Map response) { - return request.get("transport"); - } - - @Nullable - @Override - public String getNetworkType( - Map request, @Nullable Map response) { - return request.get("type"); - } - - @Nullable - @Override - public String getNetworkProtocolName( - Map request, @Nullable Map response) { - return request.get("protocolName"); - } - - @Nullable - @Override - public String getNetworkProtocolVersion( - Map request, @Nullable Map response) { - return request.get("protocolVersion"); - } - - @Override - public String getServerAddress(Map request) { - return request.get("peerName"); - } - - @Override - public Integer getServerPort(Map request) { - String peerPort = request.get("peerPort"); - return peerPort == null ? null : Integer.valueOf(peerPort); - } - - @Override - public String getSockFamily(Map request, Map response) { - return response.get("sockFamily"); - } - - @Override - public String getServerSocketDomain(Map request, Map response) { - return response.get("sockPeerName"); - } - - @Override - public String getServerSocketAddress( - Map request, Map response) { - return response.get("sockPeerAddr"); - } - - @Override - public Integer getServerSocketPort(Map request, Map response) { - String sockPeerPort = response.get("sockPeerPort"); - return sockPeerPort == null ? null : Integer.valueOf(sockPeerPort); - } - } - - private final AttributesExtractor, Map> extractor = - NetClientAttributesExtractor.create(new TestNetClientAttributesGetter()); - - @Test - void normal() { - // given - Map map = new HashMap<>(); - map.put("netTransport", IP_TCP); - map.put("transport", "tcp"); - map.put("type", "ipv6"); - map.put("protocolName", "http"); - map.put("protocolVersion", "1.1"); - map.put("peerName", "opentelemetry.io"); - map.put("peerPort", "42"); - map.put("sockFamily", "inet6"); - map.put("sockPeerAddr", "1:2:3:4::"); - map.put("sockPeerName", "proxy.opentelemetry.io"); - map.put("sockPeerPort", "123"); - - Context context = Context.root(); - - // when - AttributesBuilder startAttributes = Attributes.builder(); - extractor.onStart(startAttributes, context, map); - - AttributesBuilder endAttributes = Attributes.builder(); - extractor.onEnd(endAttributes, context, map, map, null); - - // then - assertThat(startAttributes.build()) - .containsOnly( - entry(SemanticAttributes.NET_PEER_NAME, "opentelemetry.io"), - entry(SemanticAttributes.NET_PEER_PORT, 42L), - entry(NetworkAttributes.SERVER_ADDRESS, "opentelemetry.io"), - entry(NetworkAttributes.SERVER_PORT, 42L)); - - assertThat(endAttributes.build()) - .containsOnly( - entry(SemanticAttributes.NET_TRANSPORT, IP_TCP), - entry(NetAttributes.NET_PROTOCOL_NAME, "http"), - entry(NetAttributes.NET_PROTOCOL_VERSION, "1.1"), - entry(NetworkAttributes.NETWORK_TRANSPORT, "tcp"), - entry(NetworkAttributes.NETWORK_TYPE, "ipv6"), - entry(NetworkAttributes.NETWORK_PROTOCOL_NAME, "http"), - entry(NetworkAttributes.NETWORK_PROTOCOL_VERSION, "1.1"), - entry(SemanticAttributes.NET_SOCK_FAMILY, "inet6"), - entry(SemanticAttributes.NET_SOCK_PEER_ADDR, "1:2:3:4::"), - entry(SemanticAttributes.NET_SOCK_PEER_NAME, "proxy.opentelemetry.io"), - entry(SemanticAttributes.NET_SOCK_PEER_PORT, 123L), - entry(NetworkAttributes.SERVER_SOCKET_DOMAIN, "proxy.opentelemetry.io"), - entry(NetworkAttributes.SERVER_SOCKET_ADDRESS, "1:2:3:4::"), - entry(NetworkAttributes.SERVER_SOCKET_PORT, 123L)); - } -} diff --git a/instrumentation-api-semconv/src/testBothHttpSemconv/java/io/opentelemetry/instrumentation/api/instrumenter/net/NetServerAttributesExtractorBothSemconvTest.java b/instrumentation-api-semconv/src/testBothHttpSemconv/java/io/opentelemetry/instrumentation/api/instrumenter/net/NetServerAttributesExtractorBothSemconvTest.java deleted file mode 100644 index 5b6ea38cf535..000000000000 --- a/instrumentation-api-semconv/src/testBothHttpSemconv/java/io/opentelemetry/instrumentation/api/instrumenter/net/NetServerAttributesExtractorBothSemconvTest.java +++ /dev/null @@ -1,158 +0,0 @@ -/* - * 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 io.opentelemetry.semconv.trace.attributes.SemanticAttributes.NetTransportValues.IP_TCP; -import static org.assertj.core.api.Assertions.entry; - -import io.opentelemetry.api.common.Attributes; -import io.opentelemetry.api.common.AttributesBuilder; -import io.opentelemetry.context.Context; -import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.net.internal.NetAttributes; -import io.opentelemetry.instrumentation.api.instrumenter.network.internal.NetworkAttributes; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; -import java.util.HashMap; -import java.util.Map; -import javax.annotation.Nullable; -import org.junit.jupiter.api.Test; - -class NetServerAttributesExtractorBothSemconvTest { - - static class TestNetServerAttributesGetter - implements NetServerAttributesGetter, Void> { - - @Override - public String getTransport(Map request) { - return request.get("netTransport"); - } - - @Nullable - @Override - public String getNetworkTransport(Map request, @Nullable Void response) { - return request.get("transport"); - } - - @Nullable - @Override - public String getNetworkType(Map request, @Nullable Void response) { - return request.get("type"); - } - - @Nullable - @Override - public String getNetworkProtocolName(Map request, Void response) { - return request.get("protocolName"); - } - - @Nullable - @Override - public String getNetworkProtocolVersion(Map request, Void response) { - return request.get("protocolVersion"); - } - - @Nullable - @Override - public String getServerAddress(Map request) { - return request.get("hostName"); - } - - @Nullable - @Override - public Integer getServerPort(Map request) { - String hostPort = request.get("hostPort"); - return hostPort == null ? null : Integer.valueOf(hostPort); - } - - @Nullable - @Override - public String getSockFamily(Map request) { - return request.get("sockFamily"); - } - - @Override - public String getClientSocketAddress(Map request, Void response) { - return request.get("sockPeerAddr"); - } - - @Override - public Integer getClientSocketPort(Map request, Void response) { - String sockPeerPort = request.get("sockPeerPort"); - return sockPeerPort == null ? null : Integer.valueOf(sockPeerPort); - } - - @Nullable - @Override - public String getServerSocketAddress(Map request, Void response) { - return request.get("sockHostAddr"); - } - - @Nullable - @Override - public Integer getServerSocketPort(Map request, Void response) { - String sockHostPort = request.get("sockHostPort"); - return sockHostPort == null ? null : Integer.valueOf(sockHostPort); - } - } - - AttributesExtractor, Void> extractor = - NetServerAttributesExtractor.create(new TestNetServerAttributesGetter()); - - @Test - void normal() { - // given - Map map = new HashMap<>(); - map.put("netTransport", IP_TCP); - map.put("transport", "tcp"); - map.put("type", "ipv6"); - map.put("protocolName", "http"); - map.put("protocolVersion", "1.1"); - map.put("hostName", "opentelemetry.io"); - map.put("hostPort", "80"); - map.put("sockFamily", "inet6"); - map.put("sockPeerAddr", "1:2:3:4::"); - map.put("sockPeerPort", "42"); - map.put("sockHostAddr", "4:3:2:1::"); - map.put("sockHostPort", "8080"); - - Context context = Context.root(); - - // when - AttributesBuilder startAttributes = Attributes.builder(); - extractor.onStart(startAttributes, context, map); - - AttributesBuilder endAttributes = Attributes.builder(); - extractor.onEnd(endAttributes, context, map, null, null); - - // then - assertThat(startAttributes.build()) - .containsOnly( - entry(SemanticAttributes.NET_TRANSPORT, IP_TCP), - entry(SemanticAttributes.NET_HOST_NAME, "opentelemetry.io"), - entry(SemanticAttributes.NET_HOST_PORT, 80L), - entry(NetworkAttributes.SERVER_ADDRESS, "opentelemetry.io"), - entry(NetworkAttributes.SERVER_PORT, 80L), - entry(SemanticAttributes.NET_SOCK_FAMILY, "inet6")); - - assertThat(endAttributes.build()) - .containsOnly( - entry(SemanticAttributes.NET_SOCK_HOST_ADDR, "4:3:2:1::"), - entry(SemanticAttributes.NET_SOCK_HOST_PORT, 8080L), - entry(NetworkAttributes.SERVER_SOCKET_ADDRESS, "4:3:2:1::"), - entry(NetworkAttributes.SERVER_SOCKET_PORT, 8080L), - entry(SemanticAttributes.NET_SOCK_PEER_ADDR, "1:2:3:4::"), - entry(SemanticAttributes.NET_SOCK_PEER_PORT, 42L), - entry(NetworkAttributes.CLIENT_SOCKET_ADDRESS, "1:2:3:4::"), - entry(NetworkAttributes.CLIENT_SOCKET_PORT, 42L), - entry(NetworkAttributes.NETWORK_TRANSPORT, "tcp"), - entry(NetworkAttributes.NETWORK_TYPE, "ipv6"), - entry(NetworkAttributes.NETWORK_PROTOCOL_NAME, "http"), - entry(NetworkAttributes.NETWORK_PROTOCOL_VERSION, "1.1"), - entry(NetAttributes.NET_PROTOCOL_NAME, "http"), - entry(NetAttributes.NET_PROTOCOL_VERSION, "1.1")); - } -} diff --git a/instrumentation-api-semconv/src/testStableHttpSemconv/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpClientAttributesExtractorStableSemconvTest.java b/instrumentation-api-semconv/src/testStableHttpSemconv/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpClientAttributesExtractorStableSemconvTest.java deleted file mode 100644 index 4a90c677b05f..000000000000 --- a/instrumentation-api-semconv/src/testStableHttpSemconv/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpClientAttributesExtractorStableSemconvTest.java +++ /dev/null @@ -1,222 +0,0 @@ -/* - * 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 java.util.Arrays.asList; -import static java.util.Collections.emptyList; -import static java.util.Collections.emptyMap; -import static java.util.Collections.singletonList; -import static org.assertj.core.api.Assertions.entry; -import static org.junit.jupiter.params.provider.Arguments.arguments; - -import io.opentelemetry.api.common.AttributeKey; -import io.opentelemetry.api.common.Attributes; -import io.opentelemetry.api.common.AttributesBuilder; -import io.opentelemetry.context.Context; -import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.net.NetClientAttributesGetter; -import io.opentelemetry.instrumentation.api.instrumenter.network.internal.NetworkAttributes; -import io.opentelemetry.instrumentation.api.instrumenter.url.internal.UrlAttributes; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.function.ToIntFunction; -import java.util.stream.Stream; -import javax.annotation.Nullable; -import org.junit.jupiter.api.Test; -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; - -class HttpClientAttributesExtractorStableSemconvTest { - - static class TestHttpClientAttributesGetter - implements HttpClientAttributesGetter, Map> { - - @Override - public String getUrlFull(Map request) { - return request.get("url"); - } - - @Override - public String getHttpRequestMethod(Map request) { - return request.get("method"); - } - - @Override - public List getHttpRequestHeader(Map request, String name) { - String value = request.get("header." + name); - return value == null ? emptyList() : asList(value.split(",")); - } - - @Override - public Integer getHttpResponseStatusCode( - Map request, Map response, @Nullable Throwable error) { - String value = response.get("statusCode"); - return value == null ? null : Integer.parseInt(value); - } - - @Override - public List getHttpResponseHeader( - Map request, Map response, String name) { - String value = response.get("header." + name); - return value == null ? emptyList() : asList(value.split(",")); - } - } - - static class TestNetClientAttributesGetter - implements NetClientAttributesGetter, Map> { - - @Nullable - @Override - public String getNetworkTransport( - Map request, @Nullable Map response) { - return request.get("transport"); - } - - @Nullable - @Override - public String getNetworkType( - Map request, @Nullable Map response) { - return request.get("type"); - } - - @Nullable - @Override - public String getNetworkProtocolName( - Map request, @Nullable Map response) { - return request.get("protocolName"); - } - - @Nullable - @Override - public String getNetworkProtocolVersion( - Map request, @Nullable Map response) { - return request.get("protocolVersion"); - } - - @Nullable - @Override - public String getServerAddress(Map request) { - return request.get("peerName"); - } - - @Nullable - @Override - public Integer getServerPort(Map request) { - String value = request.get("peerPort"); - return value == null ? null : Integer.parseInt(value); - } - } - - @Test - void normal() { - Map request = new HashMap<>(); - request.put("method", "POST"); - request.put("url", "http://github.com"); - request.put("header.content-length", "10"); - request.put("header.user-agent", "okhttp 3.x"); - request.put("header.custom-request-header", "123,456"); - request.put("transport", "udp"); - request.put("type", "ipv4"); - request.put("protocolName", "http"); - request.put("protocolVersion", "1.1"); - request.put("peerName", "github.com"); - request.put("peerPort", "123"); - - Map response = new HashMap<>(); - response.put("statusCode", "202"); - response.put("header.content-length", "20"); - response.put("header.custom-response-header", "654,321"); - - ToIntFunction resendCountFromContext = context -> 2; - - AttributesExtractor, Map> extractor = - new HttpClientAttributesExtractor<>( - new TestHttpClientAttributesGetter(), - new TestNetClientAttributesGetter(), - singletonList("Custom-Request-Header"), - singletonList("Custom-Response-Header"), - resendCountFromContext); - - AttributesBuilder startAttributes = Attributes.builder(); - extractor.onStart(startAttributes, Context.root(), request); - assertThat(startAttributes.build()) - .containsOnly( - entry(HttpAttributes.HTTP_REQUEST_METHOD, "POST"), - entry(UrlAttributes.URL_FULL, "http://github.com"), - entry(SemanticAttributes.USER_AGENT_ORIGINAL, "okhttp 3.x"), - entry( - AttributeKey.stringArrayKey("http.request.header.custom_request_header"), - asList("123", "456")), - entry(NetworkAttributes.SERVER_ADDRESS, "github.com"), - entry(NetworkAttributes.SERVER_PORT, 123L)); - - AttributesBuilder endAttributes = Attributes.builder(); - extractor.onEnd(endAttributes, Context.root(), request, response, null); - assertThat(endAttributes.build()) - .containsOnly( - entry(HttpAttributes.HTTP_REQUEST_BODY_SIZE, 10L), - entry(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, 202L), - entry(HttpAttributes.HTTP_RESPONSE_BODY_SIZE, 20L), - entry(SemanticAttributes.HTTP_RESEND_COUNT, 2L), - entry( - AttributeKey.stringArrayKey("http.response.header.custom_response_header"), - asList("654", "321")), - entry(NetworkAttributes.NETWORK_TRANSPORT, "udp"), - entry(NetworkAttributes.NETWORK_TYPE, "ipv4"), - entry(NetworkAttributes.NETWORK_PROTOCOL_NAME, "http"), - entry(NetworkAttributes.NETWORK_PROTOCOL_VERSION, "1.1")); - } - - @ParameterizedTest - @ArgumentsSource(NetworkTransportAndProtocolProvider.class) - void skipNetworkTransportIfDefaultForProtocol( - String observedProtocolName, - String observedProtocolVersion, - String observedTransport, - @Nullable String extractedTransport) { - Map request = new HashMap<>(); - request.put("protocolName", observedProtocolName); - request.put("protocolVersion", observedProtocolVersion); - request.put("transport", observedTransport); - - AttributesExtractor, Map> extractor = - HttpClientAttributesExtractor.create( - new TestHttpClientAttributesGetter(), new TestNetClientAttributesGetter()); - - AttributesBuilder attributes = Attributes.builder(); - extractor.onStart(attributes, Context.root(), request); - extractor.onEnd(attributes, Context.root(), request, emptyMap(), null); - - if (extractedTransport != null) { - assertThat(attributes.build()) - .containsEntry(NetworkAttributes.NETWORK_TRANSPORT, extractedTransport); - } else { - assertThat(attributes.build()).doesNotContainKey(NetworkAttributes.NETWORK_TRANSPORT); - } - } - - static final class NetworkTransportAndProtocolProvider implements ArgumentsProvider { - - @Override - public Stream provideArguments(ExtensionContext context) { - return Stream.of( - arguments("http", "1.0", "tcp", null), - arguments("http", "1.1", "tcp", null), - arguments("http", "2.0", "tcp", null), - arguments("http", "3.0", "udp", null), - arguments("http", "1.1", "udp", "udp"), - arguments("ftp", "2.0", "tcp", "tcp"), - arguments("http", "3.0", "tcp", "tcp"), - arguments("http", "42", "tcp", "tcp")); - } - } -} diff --git a/instrumentation-api-semconv/src/testStableHttpSemconv/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpServerAttributesExtractorStableSemconvTest.java b/instrumentation-api-semconv/src/testStableHttpSemconv/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpServerAttributesExtractorStableSemconvTest.java deleted file mode 100644 index ea26ec3591ad..000000000000 --- a/instrumentation-api-semconv/src/testStableHttpSemconv/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpServerAttributesExtractorStableSemconvTest.java +++ /dev/null @@ -1,246 +0,0 @@ -/* - * 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 java.util.Arrays.asList; -import static java.util.Collections.emptyList; -import static java.util.Collections.emptyMap; -import static java.util.Collections.singletonList; -import static org.assertj.core.api.Assertions.entry; -import static org.junit.jupiter.params.provider.Arguments.arguments; - -import io.opentelemetry.api.common.AttributeKey; -import io.opentelemetry.api.common.Attributes; -import io.opentelemetry.api.common.AttributesBuilder; -import io.opentelemetry.context.Context; -import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.net.NetServerAttributesGetter; -import io.opentelemetry.instrumentation.api.instrumenter.network.internal.NetworkAttributes; -import io.opentelemetry.instrumentation.api.instrumenter.url.internal.UrlAttributes; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.function.Function; -import java.util.stream.Stream; -import javax.annotation.Nullable; -import org.junit.jupiter.api.Test; -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; - -class HttpServerAttributesExtractorStableSemconvTest { - - static class TestHttpServerAttributesGetter - implements HttpServerAttributesGetter, Map> { - - @Override - public String getHttpRequestMethod(Map request) { - return (String) request.get("method"); - } - - @Override - public String getUrlScheme(Map request) { - return (String) request.get("scheme"); - } - - @Nullable - @Override - public String getUrlPath(Map request) { - return (String) request.get("path"); - } - - @Nullable - @Override - public String getUrlQuery(Map request) { - return (String) request.get("query"); - } - - @Override - public String getHttpRoute(Map request) { - return (String) request.get("route"); - } - - @Override - public List getHttpRequestHeader(Map request, String name) { - String values = (String) request.get("header." + name); - return values == null ? emptyList() : asList(values.split(",")); - } - - @Override - public Integer getHttpResponseStatusCode( - Map request, Map response, @Nullable Throwable error) { - String value = (String) response.get("statusCode"); - return value == null ? null : Integer.parseInt(value); - } - - @Override - public List getHttpResponseHeader( - Map request, Map response, String name) { - String values = (String) response.get("header." + name); - return values == null ? emptyList() : asList(values.split(",")); - } - } - - static class TestNetServerAttributesGetter - implements NetServerAttributesGetter, Map> { - - @Nullable - @Override - public String getNetworkTransport( - Map request, @Nullable Map response) { - return (String) request.get("transport"); - } - - @Nullable - @Override - public String getNetworkType( - Map request, @Nullable Map response) { - return (String) request.get("type"); - } - - @Nullable - @Override - public String getNetworkProtocolName( - Map request, Map response) { - return (String) request.get("protocolName"); - } - - @Nullable - @Override - public String getNetworkProtocolVersion( - Map request, Map response) { - return (String) request.get("protocolVersion"); - } - - @Nullable - @Override - public String getServerAddress(Map request) { - return (String) request.get("hostName"); - } - - @Nullable - @Override - public Integer getServerPort(Map request) { - return (Integer) request.get("hostPort"); - } - } - - @Test - void normal() { - Map request = new HashMap<>(); - request.put("method", "POST"); - request.put("url", "http://github.com"); - request.put("path", "/repositories/1"); - request.put("query", "details=true"); - request.put("scheme", "http"); - request.put("header.content-length", "10"); - request.put("route", "/repositories/{id}"); - request.put("header.user-agent", "okhttp 3.x"); - request.put("header.host", "github.com"); - request.put("header.forwarded", "for=1.1.1.1;proto=https"); - request.put("header.custom-request-header", "123,456"); - request.put("transport", "udp"); - request.put("type", "ipv4"); - request.put("protocolName", "http"); - request.put("protocolVersion", "2.0"); - - Map response = new HashMap<>(); - response.put("statusCode", "202"); - response.put("header.content-length", "20"); - response.put("header.custom-response-header", "654,321"); - - Function routeFromContext = ctx -> "/repositories/{repoId}"; - - HttpServerAttributesExtractor, Map> extractor = - new HttpServerAttributesExtractor<>( - new TestHttpServerAttributesGetter(), - new TestNetServerAttributesGetter(), - singletonList("Custom-Request-Header"), - singletonList("Custom-Response-Header"), - routeFromContext); - - AttributesBuilder startAttributes = Attributes.builder(); - extractor.onStart(startAttributes, Context.root(), request); - assertThat(startAttributes.build()) - .containsOnly( - entry(NetworkAttributes.SERVER_ADDRESS, "github.com"), - entry(HttpAttributes.HTTP_REQUEST_METHOD, "POST"), - entry(UrlAttributes.URL_SCHEME, "http"), - entry(UrlAttributes.URL_PATH, "/repositories/1"), - entry(UrlAttributes.URL_QUERY, "details=true"), - entry(SemanticAttributes.USER_AGENT_ORIGINAL, "okhttp 3.x"), - entry(SemanticAttributes.HTTP_ROUTE, "/repositories/{id}"), - entry(NetworkAttributes.CLIENT_ADDRESS, "1.1.1.1"), - entry( - AttributeKey.stringArrayKey("http.request.header.custom_request_header"), - asList("123", "456"))); - - AttributesBuilder endAttributes = Attributes.builder(); - extractor.onEnd(endAttributes, Context.root(), request, response, null); - assertThat(endAttributes.build()) - .containsOnly( - entry(NetworkAttributes.NETWORK_TRANSPORT, "udp"), - entry(NetworkAttributes.NETWORK_TYPE, "ipv4"), - entry(NetworkAttributes.NETWORK_PROTOCOL_NAME, "http"), - entry(NetworkAttributes.NETWORK_PROTOCOL_VERSION, "2.0"), - entry(SemanticAttributes.HTTP_ROUTE, "/repositories/{repoId}"), - entry(HttpAttributes.HTTP_REQUEST_BODY_SIZE, 10L), - entry(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, 202L), - entry(HttpAttributes.HTTP_RESPONSE_BODY_SIZE, 20L), - entry( - AttributeKey.stringArrayKey("http.response.header.custom_response_header"), - asList("654", "321"))); - } - - @ParameterizedTest - @ArgumentsSource(NetworkTransportAndProtocolProvider.class) - void skipNetworkTransportIfDefaultForProtocol( - String observedProtocolName, - String observedProtocolVersion, - String observedTransport, - @Nullable String extractedTransport) { - Map request = new HashMap<>(); - request.put("protocolName", observedProtocolName); - request.put("protocolVersion", observedProtocolVersion); - request.put("transport", observedTransport); - - AttributesExtractor, Map> extractor = - HttpClientAttributesExtractor.create( - new HttpClientAttributesExtractorStableSemconvTest.TestHttpClientAttributesGetter(), - new HttpClientAttributesExtractorStableSemconvTest.TestNetClientAttributesGetter()); - - AttributesBuilder attributes = Attributes.builder(); - extractor.onStart(attributes, Context.root(), request); - extractor.onEnd(attributes, Context.root(), request, emptyMap(), null); - - if (extractedTransport != null) { - assertThat(attributes.build()) - .containsEntry(NetworkAttributes.NETWORK_TRANSPORT, extractedTransport); - } else { - assertThat(attributes.build()).doesNotContainKey(NetworkAttributes.NETWORK_TRANSPORT); - } - } - - static final class NetworkTransportAndProtocolProvider implements ArgumentsProvider { - - @Override - public Stream provideArguments(ExtensionContext context) { - return Stream.of( - arguments("http", "1.0", "tcp", null), - arguments("http", "1.1", "tcp", null), - arguments("http", "2.0", "tcp", null), - arguments("http", "3.0", "udp", null), - arguments("http", "1.1", "udp", "udp"), - arguments("ftp", "2.0", "tcp", "tcp"), - arguments("http", "3.0", "tcp", "tcp"), - arguments("http", "42", "tcp", "tcp")); - } - } -} diff --git a/instrumentation-api-semconv/src/testStableHttpSemconv/java/io/opentelemetry/instrumentation/api/instrumenter/net/NetClientAttributesExtractorStableSemconvTest.java b/instrumentation-api-semconv/src/testStableHttpSemconv/java/io/opentelemetry/instrumentation/api/instrumenter/net/NetClientAttributesExtractorStableSemconvTest.java deleted file mode 100644 index c3637ecb24a1..000000000000 --- a/instrumentation-api-semconv/src/testStableHttpSemconv/java/io/opentelemetry/instrumentation/api/instrumenter/net/NetClientAttributesExtractorStableSemconvTest.java +++ /dev/null @@ -1,138 +0,0 @@ -/* - * 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 io.opentelemetry.semconv.trace.attributes.SemanticAttributes.NetTransportValues.IP_TCP; -import static org.assertj.core.api.Assertions.entry; - -import io.opentelemetry.api.common.Attributes; -import io.opentelemetry.api.common.AttributesBuilder; -import io.opentelemetry.context.Context; -import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.network.internal.NetworkAttributes; -import java.util.HashMap; -import java.util.Map; -import javax.annotation.Nullable; -import org.junit.jupiter.api.Test; - -class NetClientAttributesExtractorStableSemconvTest { - - static class TestNetClientAttributesGetter - implements NetClientAttributesGetter, Map> { - - @Override - public String getTransport(Map request, Map response) { - return response.get("netTransport"); - } - - @Nullable - @Override - public String getNetworkTransport( - Map request, @Nullable Map response) { - return request.get("transport"); - } - - @Nullable - @Override - public String getNetworkType( - Map request, @Nullable Map response) { - return request.get("type"); - } - - @Nullable - @Override - public String getNetworkProtocolName( - Map request, @Nullable Map response) { - return request.get("protocolName"); - } - - @Nullable - @Override - public String getNetworkProtocolVersion( - Map request, @Nullable Map response) { - return request.get("protocolVersion"); - } - - @Override - public String getServerAddress(Map request) { - return request.get("peerName"); - } - - @Override - public Integer getServerPort(Map request) { - String peerPort = request.get("peerPort"); - return peerPort == null ? null : Integer.valueOf(peerPort); - } - - @Override - public String getSockFamily(Map request, Map response) { - return response.get("sockFamily"); - } - - @Override - public String getServerSocketDomain(Map request, Map response) { - return response.get("sockPeerName"); - } - - @Override - public String getServerSocketAddress( - Map request, Map response) { - return response.get("sockPeerAddr"); - } - - @Override - public Integer getServerSocketPort(Map request, Map response) { - String sockPeerPort = response.get("sockPeerPort"); - return sockPeerPort == null ? null : Integer.valueOf(sockPeerPort); - } - } - - private final AttributesExtractor, Map> extractor = - NetClientAttributesExtractor.create(new TestNetClientAttributesGetter()); - - @Test - void normal() { - // given - Map map = new HashMap<>(); - map.put("netTransport", IP_TCP); - map.put("transport", "tcp"); - map.put("type", "ipv6"); - map.put("protocolName", "http"); - map.put("protocolVersion", "1.1"); - map.put("peerName", "opentelemetry.io"); - map.put("peerPort", "42"); - map.put("sockFamily", "inet6"); - map.put("sockPeerAddr", "1:2:3:4::"); - map.put("sockPeerName", "proxy.opentelemetry.io"); - map.put("sockPeerPort", "123"); - - Context context = Context.root(); - - // when - AttributesBuilder startAttributes = Attributes.builder(); - extractor.onStart(startAttributes, context, map); - - AttributesBuilder endAttributes = Attributes.builder(); - extractor.onEnd(endAttributes, context, map, map, null); - - // then - assertThat(startAttributes.build()) - .containsOnly( - entry(NetworkAttributes.SERVER_ADDRESS, "opentelemetry.io"), - entry(NetworkAttributes.SERVER_PORT, 42L)); - - assertThat(endAttributes.build()) - .containsOnly( - entry(NetworkAttributes.NETWORK_TRANSPORT, "tcp"), - entry(NetworkAttributes.NETWORK_TYPE, "ipv6"), - entry(NetworkAttributes.NETWORK_PROTOCOL_NAME, "http"), - entry(NetworkAttributes.NETWORK_PROTOCOL_VERSION, "1.1"), - entry(NetworkAttributes.SERVER_SOCKET_DOMAIN, "proxy.opentelemetry.io"), - entry(NetworkAttributes.SERVER_SOCKET_ADDRESS, "1:2:3:4::"), - entry(NetworkAttributes.SERVER_SOCKET_PORT, 123L)); - } -} diff --git a/instrumentation-api-semconv/src/testStableHttpSemconv/java/io/opentelemetry/instrumentation/api/instrumenter/net/NetServerAttributesExtractorStableSemconvTest.java b/instrumentation-api-semconv/src/testStableHttpSemconv/java/io/opentelemetry/instrumentation/api/instrumenter/net/NetServerAttributesExtractorStableSemconvTest.java deleted file mode 100644 index a16cdf23dd15..000000000000 --- a/instrumentation-api-semconv/src/testStableHttpSemconv/java/io/opentelemetry/instrumentation/api/instrumenter/net/NetServerAttributesExtractorStableSemconvTest.java +++ /dev/null @@ -1,146 +0,0 @@ -/* - * 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 io.opentelemetry.semconv.trace.attributes.SemanticAttributes.NetTransportValues.IP_TCP; -import static org.assertj.core.api.Assertions.entry; - -import io.opentelemetry.api.common.Attributes; -import io.opentelemetry.api.common.AttributesBuilder; -import io.opentelemetry.context.Context; -import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.network.internal.NetworkAttributes; -import java.util.HashMap; -import java.util.Map; -import javax.annotation.Nullable; -import org.junit.jupiter.api.Test; - -class NetServerAttributesExtractorStableSemconvTest { - - static class TestNetServerAttributesGetter - implements NetServerAttributesGetter, Void> { - - @Override - public String getTransport(Map request) { - return request.get("netTransport"); - } - - @Nullable - @Override - public String getNetworkTransport(Map request, @Nullable Void response) { - return request.get("transport"); - } - - @Nullable - @Override - public String getNetworkType(Map request, @Nullable Void response) { - return request.get("type"); - } - - @Nullable - @Override - public String getNetworkProtocolName(Map request, Void response) { - return request.get("protocolName"); - } - - @Nullable - @Override - public String getNetworkProtocolVersion(Map request, Void response) { - return request.get("protocolVersion"); - } - - @Nullable - @Override - public String getServerAddress(Map request) { - return request.get("hostName"); - } - - @Nullable - @Override - public Integer getServerPort(Map request) { - String hostPort = request.get("hostPort"); - return hostPort == null ? null : Integer.valueOf(hostPort); - } - - @Nullable - @Override - public String getSockFamily(Map request) { - return request.get("sockFamily"); - } - - @Override - public String getClientSocketAddress(Map request, Void response) { - return request.get("sockPeerAddr"); - } - - @Override - public Integer getClientSocketPort(Map request, Void response) { - String sockPeerPort = request.get("sockPeerPort"); - return sockPeerPort == null ? null : Integer.valueOf(sockPeerPort); - } - - @Nullable - @Override - public String getServerSocketAddress(Map request, Void response) { - return request.get("sockHostAddr"); - } - - @Nullable - @Override - public Integer getServerSocketPort(Map request, Void response) { - String sockHostPort = request.get("sockHostPort"); - return sockHostPort == null ? null : Integer.valueOf(sockHostPort); - } - } - - AttributesExtractor, Void> extractor = - NetServerAttributesExtractor.create(new TestNetServerAttributesGetter()); - - @Test - void normal() { - // given - Map map = new HashMap<>(); - map.put("netTransport", IP_TCP); - map.put("transport", "tcp"); - map.put("type", "ipv6"); - map.put("protocolName", "http"); - map.put("protocolVersion", "1.1"); - map.put("hostName", "opentelemetry.io"); - map.put("hostPort", "80"); - map.put("sockFamily", "inet6"); - map.put("sockPeerAddr", "1:2:3:4::"); - map.put("sockPeerPort", "42"); - map.put("sockHostAddr", "4:3:2:1::"); - map.put("sockHostPort", "8080"); - - Context context = Context.root(); - - // when - AttributesBuilder startAttributes = Attributes.builder(); - extractor.onStart(startAttributes, context, map); - - AttributesBuilder endAttributes = Attributes.builder(); - extractor.onEnd(endAttributes, context, map, null, null); - - // then - assertThat(startAttributes.build()) - .containsOnly( - entry(NetworkAttributes.SERVER_ADDRESS, "opentelemetry.io"), - entry(NetworkAttributes.SERVER_PORT, 80L)); - - assertThat(endAttributes.build()) - .containsOnly( - entry(NetworkAttributes.NETWORK_TRANSPORT, "tcp"), - entry(NetworkAttributes.NETWORK_TYPE, "ipv6"), - entry(NetworkAttributes.NETWORK_PROTOCOL_NAME, "http"), - entry(NetworkAttributes.NETWORK_PROTOCOL_VERSION, "1.1"), - entry(NetworkAttributes.SERVER_SOCKET_ADDRESS, "4:3:2:1::"), - entry(NetworkAttributes.SERVER_SOCKET_PORT, 8080L), - entry(NetworkAttributes.CLIENT_SOCKET_ADDRESS, "1:2:3:4::"), - entry(NetworkAttributes.CLIENT_SOCKET_PORT, 42L)); - } -} diff --git a/instrumentation-api/build.gradle.kts b/instrumentation-api/build.gradle.kts index ec73a8ed0c2f..07c476fd8b94 100644 --- a/instrumentation-api/build.gradle.kts +++ b/instrumentation-api/build.gradle.kts @@ -13,16 +13,18 @@ group = "io.opentelemetry.instrumentation" dependencies { api("io.opentelemetry:opentelemetry-api") - implementation("io.opentelemetry:opentelemetry-extension-incubator") + implementation("io.opentelemetry:opentelemetry-api-incubator") + implementation("io.opentelemetry.semconv:opentelemetry-semconv") compileOnly("com.google.auto.value:auto-value-annotations") annotationProcessor("com.google.auto.value:auto-value") testImplementation(project(":testing-common")) testImplementation("io.opentelemetry:opentelemetry-sdk-testing") + testImplementation("io.opentelemetry:opentelemetry-exporter-common") testImplementation("org.junit-pioneer:junit-pioneer") - jmhImplementation(project(":instrumentation-api-semconv")) + jmhImplementation(project(":instrumentation-api-incubator")) } tasks { @@ -39,6 +41,7 @@ tasks { withType().configureEach { // required on jdk17 + jvmArgs("--add-opens=java.base/java.lang=ALL-UNNAMED") jvmArgs("--add-opens=java.base/java.util=ALL-UNNAMED") jvmArgs("-XX:+IgnoreUnrecognizedVMOptions") } diff --git a/instrumentation-api/src/jmh/java/io/opentelemetry/instrumentation/api/instrumenter/InstrumenterBenchmark.java b/instrumentation-api/src/jmh/java/io/opentelemetry/instrumentation/api/instrumenter/InstrumenterBenchmark.java index 9ed697b2a300..ff69ca3ebca8 100644 --- a/instrumentation-api/src/jmh/java/io/opentelemetry/instrumentation/api/instrumenter/InstrumenterBenchmark.java +++ b/instrumentation-api/src/jmh/java/io/opentelemetry/instrumentation/api/instrumenter/InstrumenterBenchmark.java @@ -7,10 +7,9 @@ import io.opentelemetry.api.OpenTelemetry; import io.opentelemetry.context.Context; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpClientAttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpClientAttributesGetter; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpSpanNameExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.net.NetClientAttributesGetter; +import io.opentelemetry.instrumentation.api.semconv.http.HttpClientAttributesExtractor; +import io.opentelemetry.instrumentation.api.semconv.http.HttpClientAttributesGetter; +import io.opentelemetry.instrumentation.api.semconv.http.HttpSpanNameExtractor; import java.net.InetSocketAddress; import java.util.Collections; import java.util.List; @@ -40,8 +39,7 @@ public class InstrumenterBenchmark { "benchmark", HttpSpanNameExtractor.create(ConstantHttpAttributesGetter.INSTANCE)) .addAttributesExtractor( - HttpClientAttributesExtractor.create( - ConstantHttpAttributesGetter.INSTANCE, new ConstantNetAttributesGetter())) + HttpClientAttributesExtractor.create(ConstantHttpAttributesGetter.INSTANCE)) .buildInstrumenter(); @Benchmark @@ -59,6 +57,9 @@ public Context startEnd() { enum ConstantHttpAttributesGetter implements HttpClientAttributesGetter { INSTANCE; + private static final InetSocketAddress PEER_ADDRESS = + InetSocketAddress.createUnresolved("localhost", 8080); + @Override public String getUrlFull(Void unused) { return "https://opentelemetry.io/benchmark"; @@ -86,20 +87,12 @@ public Integer getHttpResponseStatusCode(Void unused, Void unused2, @Nullable Th public List getHttpResponseHeader(Void unused, Void unused2, String name) { return Collections.emptyList(); } - } - - static class ConstantNetAttributesGetter implements NetClientAttributesGetter { - private static final InetSocketAddress PEER_ADDRESS = - InetSocketAddress.createUnresolved("localhost", 8080); - - @Nullable @Override public String getNetworkProtocolName(Void unused, @Nullable Void unused2) { return "http"; } - @Nullable @Override public String getNetworkProtocolVersion(Void unused, @Nullable Void unused2) { return "2.0"; @@ -117,9 +110,9 @@ public Integer getServerPort(Void request) { return null; } - @Nullable @Override - public InetSocketAddress getServerInetSocketAddress(Void request, @Nullable Void response) { + public InetSocketAddress getNetworkPeerInetSocketAddress( + Void request, @Nullable Void response) { return PEER_ADDRESS; } } diff --git a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/Instrumenter.java b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/Instrumenter.java index b56874a2cd81..a51990317067 100644 --- a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/Instrumenter.java +++ b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/Instrumenter.java @@ -11,13 +11,12 @@ import io.opentelemetry.api.trace.SpanKind; import io.opentelemetry.api.trace.Tracer; import io.opentelemetry.context.Context; +import io.opentelemetry.context.ContextKey; +import io.opentelemetry.instrumentation.api.internal.HttpRouteState; import io.opentelemetry.instrumentation.api.internal.InstrumenterAccess; import io.opentelemetry.instrumentation.api.internal.InstrumenterUtil; import io.opentelemetry.instrumentation.api.internal.SupportabilityMetrics; import java.time.Instant; -import java.util.ArrayList; -import java.util.List; -import java.util.ListIterator; import java.util.concurrent.TimeUnit; import javax.annotation.Nullable; @@ -43,6 +42,9 @@ */ public class Instrumenter { + private static final ContextKey START_OPERATION_LISTENERS = + ContextKey.named("instrumenter-start-operation-listeners"); + /** * Returns a new {@link InstrumenterBuilder}. * @@ -71,26 +73,28 @@ public static InstrumenterBuilder builder private final SpanNameExtractor spanNameExtractor; private final SpanKindExtractor spanKindExtractor; private final SpanStatusExtractor spanStatusExtractor; - private final List> spanLinksExtractors; - private final List> - attributesExtractors; - private final List> contextCustomizers; - private final List operationListeners; + private final SpanLinksExtractor[] spanLinksExtractors; + private final AttributesExtractor[] attributesExtractors; + private final ContextCustomizer[] contextCustomizers; + private final OperationListener[] operationListeners; private final ErrorCauseExtractor errorCauseExtractor; + private final boolean propagateOperationListenersToOnEnd; private final boolean enabled; private final SpanSuppressor spanSuppressor; + @SuppressWarnings({"rawtypes", "unchecked"}) Instrumenter(InstrumenterBuilder builder) { this.instrumentationName = builder.instrumentationName; this.tracer = builder.buildTracer(); this.spanNameExtractor = builder.spanNameExtractor; this.spanKindExtractor = builder.spanKindExtractor; this.spanStatusExtractor = builder.spanStatusExtractor; - this.spanLinksExtractors = new ArrayList<>(builder.spanLinksExtractors); - this.attributesExtractors = new ArrayList<>(builder.attributesExtractors); - this.contextCustomizers = new ArrayList<>(builder.contextCustomizers); - this.operationListeners = builder.buildOperationListeners(); + this.spanLinksExtractors = builder.spanLinksExtractors.toArray(new SpanLinksExtractor[0]); + this.attributesExtractors = builder.attributesExtractors.toArray(new AttributesExtractor[0]); + this.contextCustomizers = builder.contextCustomizers.toArray(new ContextCustomizer[0]); + this.operationListeners = builder.buildOperationListeners().toArray(new OperationListener[0]); this.errorCauseExtractor = builder.errorCauseExtractor; + this.propagateOperationListenersToOnEnd = builder.propagateOperationListenersToOnEnd; this.enabled = builder.enabled; this.spanSuppressor = builder.buildSpanSuppressor(); } @@ -192,17 +196,29 @@ private Context doStart(Context parentContext, REQUEST request, @Nullable Instan Span span = spanBuilder.setParent(context).startSpan(); context = context.with(span); - if (!operationListeners.isEmpty()) { + if (operationListeners.length != 0) { // operation listeners run after span start, so that they have access to the current span // for capturing exemplars long startNanos = getNanos(startTime); - for (OperationListener operationListener : operationListeners) { - context = operationListener.onStart(context, attributes, startNanos); + for (int i = 0; i < operationListeners.length; i++) { + context = operationListeners[i].onStart(context, attributes, startNanos); } } + if (propagateOperationListenersToOnEnd || context.get(START_OPERATION_LISTENERS) != null) { + // when start and end are not called on the same instrumenter we need to use the operation + // listeners that were used during start in end to correctly handle metrics like + // http.server.active_requests that is recorded both in start and end + // + // need to also add when there is already START_OPERATION_LISTENERS, otherwise this + // instrumenter will call its parent's operation listeners in doEnd + context = context.with(START_OPERATION_LISTENERS, operationListeners); + } if (localRoot) { context = LocalRootSpan.store(context, span); + if (spanKind == SpanKind.SERVER) { + HttpRouteState.updateSpan(context, span); + } } return spanSuppressor.storeInContext(context, spanKind, span); @@ -227,12 +243,14 @@ private void doEnd( } span.setAllAttributes(attributes); - if (!operationListeners.isEmpty()) { + OperationListener[] operationListeners = context.get(START_OPERATION_LISTENERS); + if (operationListeners == null) { + operationListeners = this.operationListeners; + } + if (operationListeners.length != 0) { long endNanos = getNanos(endTime); - ListIterator i = - operationListeners.listIterator(operationListeners.size()); - while (i.hasPrevious()) { - i.previous().onEnd(context, attributes, endNanos); + for (int i = operationListeners.length - 1; i >= 0; i--) { + operationListeners[i].onEnd(context, attributes, endNanos); } } @@ -268,6 +286,17 @@ public Context startAndEnd( return instrumenter.startAndEnd( parentContext, request, response, error, startTime, endTime); } + + @Override + public Context suppressSpan( + Instrumenter instrumenter, + Context parentContext, + REQUEST request) { + SpanKind spanKind = instrumenter.spanKindExtractor.extract(request); + + return instrumenter.spanSuppressor.storeInContext( + parentContext, spanKind, Span.getInvalid()); + } }); } } diff --git a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/InstrumenterBuilder.java b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/InstrumenterBuilder.java index 25dbd6ebeb2a..6b41ae6c60ab 100644 --- a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/InstrumenterBuilder.java +++ b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/InstrumenterBuilder.java @@ -6,6 +6,7 @@ package io.opentelemetry.instrumentation.api.instrumenter; import static java.util.Objects.requireNonNull; +import static java.util.logging.Level.WARNING; import com.google.errorprone.annotations.CanIgnoreReturnValue; import io.opentelemetry.api.OpenTelemetry; @@ -22,11 +23,13 @@ import io.opentelemetry.instrumentation.api.internal.EmbeddedInstrumentationProperties; import io.opentelemetry.instrumentation.api.internal.InstrumenterBuilderAccess; import io.opentelemetry.instrumentation.api.internal.InstrumenterUtil; +import io.opentelemetry.instrumentation.api.internal.SchemaUrlProvider; import io.opentelemetry.instrumentation.api.internal.SpanKey; import io.opentelemetry.instrumentation.api.internal.SpanKeyProvider; import java.util.ArrayList; import java.util.List; import java.util.Set; +import java.util.logging.Logger; import java.util.stream.Collectors; import java.util.stream.Stream; import javax.annotation.Nullable; @@ -39,6 +42,8 @@ */ public final class InstrumenterBuilder { + private static final Logger logger = Logger.getLogger(InstrumenterBuilder.class.getName()); + private static final SpanSuppressionStrategy spanSuppressionStrategy = SpanSuppressionStrategy.fromConfig( ConfigPropertiesUtil.getString( @@ -61,6 +66,7 @@ public final class InstrumenterBuilder { SpanStatusExtractor spanStatusExtractor = SpanStatusExtractor.getDefault(); ErrorCauseExtractor errorCauseExtractor = ErrorCauseExtractor.getDefault(); + boolean propagateOperationListenersToOnEnd = false; boolean enabled = true; InstrumenterBuilder( @@ -285,6 +291,7 @@ Tracer buildTracer() { if (instrumentationVersion != null) { tracerBuilder.setInstrumentationVersion(instrumentationVersion); } + String schemaUrl = getSchemaUrl(); if (schemaUrl != null) { tracerBuilder.setSchemaUrl(schemaUrl); } @@ -305,6 +312,7 @@ List buildOperationListeners() { if (instrumentationVersion != null) { meterBuilder.setInstrumentationVersion(instrumentationVersion); } + String schemaUrl = getSchemaUrl(); if (schemaUrl != null) { meterBuilder.setSchemaUrl(schemaUrl); } @@ -316,8 +324,39 @@ List buildOperationListeners() { return listeners; } + @Nullable + private String getSchemaUrl() { + // url set explicitly overrides url computed using attributes extractors + if (schemaUrl != null) { + return schemaUrl; + } + Set computedSchemaUrls = + attributesExtractors.stream() + .filter(SchemaUrlProvider.class::isInstance) + .map(SchemaUrlProvider.class::cast) + .flatMap( + provider -> { + String url = provider.internalGetSchemaUrl(); + return url == null ? Stream.of() : Stream.of(url); + }) + .collect(Collectors.toSet()); + switch (computedSchemaUrls.size()) { + case 0: + return null; + case 1: + return computedSchemaUrls.iterator().next(); + default: + logger.log( + WARNING, + "Multiple schemaUrls were detected: {0}. The built Instrumenter will have no schemaUrl assigned.", + computedSchemaUrls); + return null; + } + } + SpanSuppressor buildSpanSuppressor() { - return spanSuppressionStrategy.create(getSpanKeysFromAttributesExtractors()); + return new SpanSuppressors.ByContextKey( + spanSuppressionStrategy.create(getSpanKeysFromAttributesExtractors())); } private Set getSpanKeysFromAttributesExtractors() { @@ -332,6 +371,10 @@ private Set getSpanKeysFromAttributesExtractors() { .collect(Collectors.toSet()); } + private void propagateOperationListenersToOnEnd() { + propagateOperationListenersToOnEnd = true; + } + private interface InstrumenterConstructor { Instrumenter create(InstrumenterBuilder builder); @@ -368,6 +411,12 @@ public Instrumenter buildDownstreamInstrumenter( SpanKindExtractor spanKindExtractor) { return builder.buildDownstreamInstrumenter(setter, spanKindExtractor); } + + @Override + public void propagateOperationListenersToOnEnd( + InstrumenterBuilder builder) { + builder.propagateOperationListenersToOnEnd(); + } }); } } diff --git a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/SpanSuppressors.java b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/SpanSuppressors.java index 0867f4b2febb..44c997f85f4a 100644 --- a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/SpanSuppressors.java +++ b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/SpanSuppressors.java @@ -6,6 +6,7 @@ package io.opentelemetry.instrumentation.api.instrumenter; import com.google.errorprone.annotations.CanIgnoreReturnValue; +import io.opentelemetry.api.internal.InstrumentationUtil; import io.opentelemetry.api.trace.Span; import io.opentelemetry.api.trace.SpanKind; import io.opentelemetry.context.Context; @@ -61,10 +62,10 @@ public boolean shouldSuppress(Context parentContext, SpanKind spanKind) { static final class BySpanKey implements SpanSuppressor { - private final Set spanKeys; + private final SpanKey[] spanKeys; BySpanKey(Set spanKeys) { - this.spanKeys = spanKeys; + this.spanKeys = spanKeys.toArray(new SpanKey[0]); } @Override @@ -85,4 +86,25 @@ public boolean shouldSuppress(Context parentContext, SpanKind spanKind) { return true; } } + + static class ByContextKey implements SpanSuppressor { + private final SpanSuppressor delegate; + + ByContextKey(SpanSuppressor delegate) { + this.delegate = delegate; + } + + @Override + public Context storeInContext(Context context, SpanKind spanKind, Span span) { + return delegate.storeInContext(context, spanKind, span); + } + + @Override + public boolean shouldSuppress(Context parentContext, SpanKind spanKind) { + if (InstrumentationUtil.shouldSuppressInstrumentation(parentContext)) { + return true; + } + return delegate.shouldSuppress(parentContext, spanKind); + } + } } diff --git a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/ClassNames.java b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/ClassNames.java index f611707d5fe5..2d40592f26c5 100644 --- a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/ClassNames.java +++ b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/ClassNames.java @@ -26,10 +26,21 @@ public static String simpleName(Class type) { } private static String computeSimpleName(Class type) { + String className = type.getName(); if (!type.isAnonymousClass()) { - return type.getSimpleName(); + String simpleName = type.getSimpleName(); + // on openj9 21 simple name for lambda classes is an empty string + if (!simpleName.isEmpty()) { + return simpleName; + } else { + // handle lambda names on openj9 21 + // only lambda class names contain / + int index = className.indexOf('/'); + if (index != -1) { + className = className.substring(0, index); + } + } } - String className = type.getName(); if (type.getPackage() != null) { String pkgName = type.getPackage().getName(); if (!pkgName.isEmpty()) { diff --git a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/ConfigPropertiesUtil.java b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/ConfigPropertiesUtil.java index 5c315810ff3c..862cd6b13588 100644 --- a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/ConfigPropertiesUtil.java +++ b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/ConfigPropertiesUtil.java @@ -5,7 +5,10 @@ package io.opentelemetry.instrumentation.api.internal; +import java.util.Arrays; +import java.util.List; import java.util.Locale; +import java.util.stream.Collectors; import javax.annotation.Nullable; /** @@ -40,6 +43,26 @@ public static String getString(String propertyName) { return System.getenv(toEnvVarName(propertyName)); } + public static String getString(String propertyName, String defaultValue) { + String strValue = getString(propertyName); + return strValue == null ? defaultValue : strValue; + } + + public static List getList(String propertyName, List defaultValue) { + String value = getString(propertyName); + if (value == null) { + return defaultValue; + } + return filterBlanksAndNulls(value.split(",")); + } + + private static List filterBlanksAndNulls(String[] values) { + return Arrays.stream(values) + .map(String::trim) + .filter(s -> !s.isEmpty()) + .collect(Collectors.toList()); + } + private static String toEnvVarName(String propertyName) { return propertyName.toUpperCase(Locale.ROOT).replace('-', '_').replace('.', '_'); } diff --git a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/ContextPropagationDebug.java b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/ContextPropagationDebug.java index de5a3147c131..fac5726af8d1 100644 --- a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/ContextPropagationDebug.java +++ b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/ContextPropagationDebug.java @@ -56,6 +56,15 @@ public static boolean isThreadPropagationDebuggerEnabled() { return THREAD_PROPAGATION_DEBUGGER; } + public static Context addDebugInfo(Context context, Object carrier) { + if (ContextPropagationDebug.isThreadPropagationDebuggerEnabled()) { + context = + ContextPropagationDebug.appendLocations( + context, new Exception().getStackTrace(), carrier); + } + return context; + } + public static Context appendLocations( Context context, StackTraceElement[] locations, Object carrier) { ContextPropagationDebug propagationDebug = ContextPropagationDebug.getPropagations(context); diff --git a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/HttpConstants.java b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/HttpConstants.java new file mode 100644 index 000000000000..76aeefe16b4a --- /dev/null +++ b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/HttpConstants.java @@ -0,0 +1,29 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.internal; + +import static java.util.Arrays.asList; +import static java.util.Collections.unmodifiableSet; + +import java.util.HashSet; +import java.util.Set; + +/** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time. + */ +public final class HttpConstants { + + public static final Set KNOWN_METHODS = + unmodifiableSet( + new HashSet<>( + asList( + "CONNECT", "DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT", "TRACE"))); + + public static final String _OTHER = "_OTHER"; + + private HttpConstants() {} +} diff --git a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/HttpProtocolUtil.java b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/HttpProtocolUtil.java new file mode 100644 index 000000000000..99668ff4f4bd --- /dev/null +++ b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/HttpProtocolUtil.java @@ -0,0 +1,39 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.internal; + +import javax.annotation.Nullable; + +/** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time. + */ +public final class HttpProtocolUtil { + + public static String getProtocol(@Nullable String protocol) { + if (protocol != null && protocol.startsWith("HTTP/")) { + return "http"; + } + return null; + } + + public static String getVersion(@Nullable String protocol) { + if (protocol != null && protocol.startsWith("HTTP/")) { + return normalizeHttpVersion(protocol.substring("HTTP/".length())); + } + return null; + } + + public static String normalizeHttpVersion(String version) { + if ("2.0".equals(version)) { + return "2"; + } + + return version; + } + + private HttpProtocolUtil() {} +} diff --git a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/HttpRouteState.java b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/HttpRouteState.java index 0eda2755da92..afd023467c16 100644 --- a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/HttpRouteState.java +++ b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/HttpRouteState.java @@ -5,6 +5,7 @@ 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.context.ImplicitContextKeyed; @@ -24,20 +25,36 @@ public static HttpRouteState fromContextOrNull(Context context) { return context.get(KEY); } + public static void updateSpan(Context context, Span span) { + HttpRouteState state = fromContextOrNull(context); + if (state != null) { + state.span = span; + } + } + + // this method is used reflectively from InstrumentationApiContextBridging public static HttpRouteState create( @Nullable String method, @Nullable String route, int updatedBySourceOrder) { - return new HttpRouteState(method, route, updatedBySourceOrder); + return create(method, route, updatedBySourceOrder, null); + } + + // this method is used reflectively from InstrumentationApiContextBridging + public static HttpRouteState create( + @Nullable String method, @Nullable String route, int updatedBySourceOrder, Span span) { + return new HttpRouteState(method, route, updatedBySourceOrder, span); } @Nullable private final String method; @Nullable private volatile String route; private volatile int updatedBySourceOrder; + @Nullable private volatile Span span; private HttpRouteState( - @Nullable String method, @Nullable String route, int updatedBySourceOrder) { + @Nullable String method, @Nullable String route, int updatedBySourceOrder, Span span) { this.method = method; this.updatedBySourceOrder = updatedBySourceOrder; this.route = route; + this.span = span; } @Override @@ -59,6 +76,11 @@ public String getRoute() { return route; } + @Nullable + public Span getSpan() { + return span; + } + public void update( @SuppressWarnings("unused") Context context, // context is used by the javaagent bridge instrumentation diff --git a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/InstrumenterAccess.java b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/InstrumenterAccess.java index 2589ea56cfb3..6b899b9f1637 100644 --- a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/InstrumenterAccess.java +++ b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/InstrumenterAccess.java @@ -24,4 +24,7 @@ Context startAndEnd( @Nullable Throwable error, Instant startTime, Instant endTime); + + Context suppressSpan( + Instrumenter instrumenter, Context parentContext, REQUEST request); } diff --git a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/InstrumenterBuilderAccess.java b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/InstrumenterBuilderAccess.java index d8c40af3e47a..2c660577b17c 100644 --- a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/InstrumenterBuilderAccess.java +++ b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/InstrumenterBuilderAccess.java @@ -26,4 +26,7 @@ Instrumenter buildDownstreamInstrumenter( InstrumenterBuilder builder, TextMapSetter setter, SpanKindExtractor spanKindExtractor); + + void propagateOperationListenersToOnEnd( + InstrumenterBuilder builder); } diff --git a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/InstrumenterUtil.java b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/InstrumenterUtil.java index 4962d69b9f0e..d61a6e8fe367 100644 --- a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/InstrumenterUtil.java +++ b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/InstrumenterUtil.java @@ -45,6 +45,11 @@ public static Context startAndEnd( instrumenter, parentContext, request, response, error, startTime, endTime); } + public static Context suppressSpan( + Instrumenter instrumenter, Context parentContext, REQUEST request) { + return instrumenterAccess.suppressSpan(instrumenter, parentContext, request); + } + public static Instrumenter buildUpstreamInstrumenter( InstrumenterBuilder builder, TextMapGetter getter, @@ -62,5 +67,11 @@ public static Instrumenter buildDownstrea builder, setter, spanKindExtractor); } + public static void propagateOperationListenersToOnEnd( + InstrumenterBuilder builder) { + // instrumenterBuilderAccess is guaranteed to be non-null here + instrumenterBuilderAccess.propagateOperationListenersToOnEnd(builder); + } + private InstrumenterUtil() {} } diff --git a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/OperationMetricsUtil.java b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/OperationMetricsUtil.java new file mode 100644 index 000000000000..b8da9946074a --- /dev/null +++ b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/OperationMetricsUtil.java @@ -0,0 +1,73 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.internal; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.incubator.metrics.ExtendedDoubleHistogramBuilder; +import io.opentelemetry.api.metrics.DoubleHistogramBuilder; +import io.opentelemetry.api.metrics.Meter; +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.instrumenter.OperationListener; +import io.opentelemetry.instrumentation.api.instrumenter.OperationMetrics; +import java.util.function.BiConsumer; +import java.util.function.Function; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time. + */ +public class OperationMetricsUtil { + private static final Logger logger = Logger.getLogger(OperationMetricsUtil.class.getName()); + private static final OperationListener NOOP_OPERATION_LISTENER = + new OperationListener() { + + @Override + public Context onStart(Context context, Attributes startAttributes, long startNanos) { + return context; + } + + @Override + public void onEnd(Context context, Attributes endAttributes, long endNanos) {} + }; + + public static OperationMetrics create( + String description, Function factory) { + return create( + description, + factory, + (s, histogramBuilder) -> + logger.log( + Level.WARNING, + "Disabling {0} metrics because {1} does not implement {2}. This prevents using " + + "metrics advice, which could result in {0} metrics having high cardinality " + + "attributes.", + new Object[] { + description, + histogramBuilder.getClass().getName(), + ExtendedDoubleHistogramBuilder.class.getName() + })); + } + + // visible for testing + static OperationMetrics create( + String description, + Function factory, + BiConsumer warningEmitter) { + return meter -> { + DoubleHistogramBuilder histogramBuilder = meter.histogramBuilder("compatibility-test"); + if (!(histogramBuilder instanceof ExtendedDoubleHistogramBuilder) + && !histogramBuilder.getClass().getName().contains("NoopDoubleHistogram")) { + warningEmitter.accept(description, histogramBuilder); + return NOOP_OPERATION_LISTENER; + } + return factory.apply(meter); + }; + } + + private OperationMetricsUtil() {} +} diff --git a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/SchemaUrlProvider.java b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/SchemaUrlProvider.java new file mode 100644 index 000000000000..d60878294abb --- /dev/null +++ b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/SchemaUrlProvider.java @@ -0,0 +1,22 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.internal; + +import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; +import javax.annotation.Nullable; + +/** + * Returns the OpenTelemetry schema URL associated with the {@link AttributesExtractor} that + * implements this interface. + * + *

This class is internal and is hence not for public use. Its APIs are unstable and can change + * at any time. + */ +public interface SchemaUrlProvider { + + @Nullable + String internalGetSchemaUrl(); +} diff --git a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/SemconvStability.java b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/SemconvStability.java deleted file mode 100644 index 12b5a7e063e8..000000000000 --- a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/SemconvStability.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.api.internal; - -import static java.util.Arrays.asList; - -import java.util.HashSet; -import java.util.Set; - -/** - * This class is internal and is hence not for public use. Its APIs are unstable and can change at - * any time. - */ -public final class SemconvStability { - - private static final boolean emitOldHttpSemconv; - private static final boolean emitStableHttpSemconv; - - static { - boolean old = true; - boolean stable = false; - - String value = ConfigPropertiesUtil.getString("otel.semconv-stability.opt-in"); - if (value != null) { - Set values = new HashSet<>(asList(value.split(","))); - if (values.contains("http")) { - old = false; - stable = true; - } - // no else -- technically it's possible to set "http,http/dup", in which case we should emit - // both sets of attributes - if (values.contains("http/dup")) { - old = true; - stable = true; - } - } - - emitOldHttpSemconv = old; - emitStableHttpSemconv = stable; - } - - public static boolean emitOldHttpSemconv() { - return emitOldHttpSemconv; - } - - public static boolean emitStableHttpSemconv() { - return emitStableHttpSemconv; - } - - private SemconvStability() {} -} diff --git a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/SupportabilityMetrics.java b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/SupportabilityMetrics.java index be34dcb8d182..cf91c4274c32 100644 --- a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/SupportabilityMetrics.java +++ b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/SupportabilityMetrics.java @@ -6,6 +6,7 @@ package io.opentelemetry.instrumentation.api.internal; import io.opentelemetry.api.trace.SpanKind; +import java.security.PrivilegedAction; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.Executors; @@ -82,18 +83,20 @@ void report() { } // this private method is designed for assignment of the return value - @SuppressWarnings("CanIgnoreReturnValueSuggester") + @SuppressWarnings("OtelCanIgnoreReturnValueSuggester") private SupportabilityMetrics start() { if (agentDebugEnabled) { ScheduledExecutorService executor = Executors.newScheduledThreadPool( 1, - runnable -> { - Thread result = new Thread(runnable, "supportability_metrics_reporter"); - result.setDaemon(true); - result.setContextClassLoader(null); - return result; - }); + runnable -> + doPrivileged( + () -> { + Thread result = new Thread(runnable, "supportability_metrics_reporter"); + result.setDaemon(true); + result.setContextClassLoader(null); + return result; + })); executor.scheduleAtFixedRate(this::report, 5, 5, TimeUnit.SECONDS); // the condition below will always be false, but by referencing the executor it ensures the // executor can't become unreachable in the middle of the scheduleAtFixedRate() method @@ -107,6 +110,13 @@ private SupportabilityMetrics start() { return this; } + private static T doPrivileged(PrivilegedAction action) { + if (System.getSecurityManager() == null) { + return action.run(); + } + return java.security.AccessController.doPrivileged(action); + } + /** * This class is internal and is hence not for public use. Its APIs are unstable and can change at * any time. diff --git a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/cache/weaklockfree/WeakConcurrentMap.java b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/cache/weaklockfree/WeakConcurrentMap.java index 4d2d55862c86..d7ca2dc941b7 100644 --- a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/cache/weaklockfree/WeakConcurrentMap.java +++ b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/cache/weaklockfree/WeakConcurrentMap.java @@ -145,7 +145,7 @@ static final class LookupKey { private K key; private int hashCode; - @SuppressWarnings("CanIgnoreReturnValueSuggester") + @SuppressWarnings("OtelCanIgnoreReturnValueSuggester") LookupKey withValue(K key) { this.key = key; hashCode = System.identityHashCode(key); diff --git a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/CapturedHttpHeadersUtil.java b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/http/CapturedHttpHeadersUtil.java similarity index 68% rename from instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/CapturedHttpHeadersUtil.java rename to instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/http/CapturedHttpHeadersUtil.java index 682cb3da09b0..b5dca93cec2c 100644 --- a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/CapturedHttpHeadersUtil.java +++ b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/http/CapturedHttpHeadersUtil.java @@ -3,9 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.instrumentation.api.instrumenter.http; - -import static java.util.Collections.unmodifiableList; +package io.opentelemetry.instrumentation.api.semconv.http; import io.opentelemetry.api.common.AttributeKey; import java.util.List; @@ -17,16 +15,18 @@ final class CapturedHttpHeadersUtil { // these are naturally bounded because they only store keys listed in - // otel.instrumentation.http.capture-headers.server.request and - // otel.instrumentation.http.capture-headers.server.response + // otel.instrumentation.http.server.capture-request-headers and + // otel.instrumentation.http.server.capture-response-headers private static final ConcurrentMap>> requestKeysCache = new ConcurrentHashMap<>(); private static final ConcurrentMap>> responseKeysCache = new ConcurrentHashMap<>(); - static List lowercase(List names) { - return unmodifiableList( - names.stream().map(s -> s.toLowerCase(Locale.ROOT)).collect(Collectors.toList())); + static String[] lowercase(List names) { + return names.stream() + .map(s -> s.toLowerCase(Locale.ROOT)) + .collect(Collectors.toList()) + .toArray(new String[0]); } static AttributeKey> requestAttributeKey(String headerName) { @@ -38,8 +38,8 @@ static AttributeKey> responseAttributeKey(String headerName) { } private static AttributeKey> createKey(String type, String headerName) { - // headerName is always lowercase, see CapturedHttpHeaders - String key = "http." + type + ".header." + headerName.replace('-', '_'); + // headerName is always lowercase, see CapturedHttpHeadersUtil#lowercase + String key = "http." + type + ".header." + headerName; return AttributeKey.stringArrayKey(key); } diff --git a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/http/ForwardedHostAddressAndPortExtractor.java b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/http/ForwardedHostAddressAndPortExtractor.java new file mode 100644 index 000000000000..f4c7b5926da7 --- /dev/null +++ b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/http/ForwardedHostAddressAndPortExtractor.java @@ -0,0 +1,97 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.semconv.http; + +import static io.opentelemetry.instrumentation.api.semconv.http.HeaderParsingHelper.notFound; +import static io.opentelemetry.instrumentation.api.semconv.http.HeaderParsingHelper.setPort; + +import io.opentelemetry.instrumentation.api.semconv.network.internal.AddressAndPortExtractor; +import java.util.Locale; + +final class ForwardedHostAddressAndPortExtractor + implements AddressAndPortExtractor { + + private final HttpCommonAttributesGetter getter; + + ForwardedHostAddressAndPortExtractor(HttpCommonAttributesGetter getter) { + this.getter = getter; + } + + @Override + public void extract(AddressPortSink sink, REQUEST request) { + // try Forwarded + for (String forwarded : getter.getHttpRequestHeader(request, "forwarded")) { + if (extractFromForwardedHeader(sink, forwarded)) { + return; + } + } + + // try X-Forwarded-Host + for (String forwardedHost : getter.getHttpRequestHeader(request, "x-forwarded-host")) { + if (extractHost(sink, forwardedHost, 0, forwardedHost.length())) { + return; + } + } + + // try :authority (HTTP 2.0 pseudo-header) + for (String host : getter.getHttpRequestHeader(request, ":authority")) { + if (extractHost(sink, host, 0, host.length())) { + return; + } + } + + // try Host + for (String host : getter.getHttpRequestHeader(request, "host")) { + if (extractHost(sink, host, 0, host.length())) { + return; + } + } + } + + private static boolean extractFromForwardedHeader(AddressPortSink sink, String forwarded) { + int start = forwarded.toLowerCase(Locale.ROOT).indexOf("host="); + if (start < 0) { + return false; + } + start += "host=".length(); // start is now the index after host= + if (start >= forwarded.length() - 1) { // the value after host= must not be empty + return false; + } + // find the end of the `host=

` section + int end = forwarded.indexOf(';', start); + if (end < 0) { + end = forwarded.length(); + } + return extractHost(sink, forwarded, start, end); + } + + private static boolean extractHost(AddressPortSink sink, String host, int start, int end) { + if (start >= end) { + return false; + } + + // skip quotes + if (host.charAt(start) == '"') { + // try to find the end of the quote + int quoteEnd = host.indexOf('"', start + 1); + if (notFound(quoteEnd, end)) { + // malformed header value + return false; + } + return extractHost(sink, host, start + 1, quoteEnd); + } + + int hostHeaderSeparator = host.indexOf(':', start); + if (notFound(hostHeaderSeparator, end)) { + sink.setAddress(host.substring(start, end)); + } else { + sink.setAddress(host.substring(start, hostHeaderSeparator)); + setPort(sink, host, hostHeaderSeparator + 1, end); + } + + return true; + } +} diff --git a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/http/ForwardedUrlSchemeProvider.java b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/http/ForwardedUrlSchemeProvider.java new file mode 100644 index 000000000000..ccfb71c537e1 --- /dev/null +++ b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/http/ForwardedUrlSchemeProvider.java @@ -0,0 +1,80 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.semconv.http; + +import java.util.Locale; +import java.util.function.Function; +import javax.annotation.Nullable; + +final class ForwardedUrlSchemeProvider implements Function { + + private final HttpServerAttributesGetter getter; + + ForwardedUrlSchemeProvider(HttpServerAttributesGetter getter) { + this.getter = getter; + } + + @Override + public String apply(REQUEST request) { + // try Forwarded + for (String forwarded : getter.getHttpRequestHeader(request, "forwarded")) { + String proto = extractProtoFromForwardedHeader(forwarded); + if (proto != null) { + return proto; + } + } + + // try X-Forwarded-Proto + for (String forwardedProto : getter.getHttpRequestHeader(request, "x-forwarded-proto")) { + String proto = extractProtoFromForwardedProtoHeader(forwardedProto); + if (proto != null) { + return proto; + } + } + + return null; + } + + /** Extract proto (aka scheme) from "Forwarded" http header. */ + @Nullable + private static String extractProtoFromForwardedHeader(String forwarded) { + int start = forwarded.toLowerCase(Locale.ROOT).indexOf("proto="); + if (start < 0) { + return null; + } + start += 6; // start is now the index after proto= + if (start >= forwarded.length() - 1) { // the value after for= must not be empty + return null; + } + return extractProto(forwarded, start); + } + + /** Extract proto (aka scheme) from "X-Forwarded-Proto" http header. */ + @Nullable + private static String extractProtoFromForwardedProtoHeader(String forwardedProto) { + return extractProto(forwardedProto, 0); + } + + @Nullable + private static String extractProto(String forwarded, int start) { + if (forwarded.length() == start) { + return null; + } + if (forwarded.charAt(start) == '"') { + return extractProto(forwarded, start + 1); + } + for (int i = start; i < forwarded.length(); i++) { + char c = forwarded.charAt(i); + if (c == ',' || c == ';' || c == '"') { + if (i == start) { // empty string + return null; + } + return forwarded.substring(start, i); + } + } + return forwarded.substring(start); + } +} diff --git a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/http/HeaderParsingHelper.java b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/http/HeaderParsingHelper.java new file mode 100644 index 000000000000..c6ee67da21a7 --- /dev/null +++ b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/http/HeaderParsingHelper.java @@ -0,0 +1,28 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.semconv.http; + +import io.opentelemetry.instrumentation.api.semconv.network.internal.AddressAndPortExtractor.AddressPortSink; + +final class HeaderParsingHelper { + + static boolean notFound(int pos, int end) { + return pos < 0 || pos >= end; + } + + static void setPort(AddressPortSink sink, String header, int start, int end) { + if (start == end) { + return; + } + try { + sink.setPort(Integer.parseInt(header.substring(start, end))); + } catch (NumberFormatException ignored) { + // malformed port, ignoring + } + } + + private HeaderParsingHelper() {} +} diff --git a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/http/HostAddressAndPortExtractor.java b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/http/HostAddressAndPortExtractor.java new file mode 100644 index 000000000000..a9637d12a835 --- /dev/null +++ b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/http/HostAddressAndPortExtractor.java @@ -0,0 +1,36 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.semconv.http; + +import static io.opentelemetry.instrumentation.api.semconv.http.HeaderParsingHelper.setPort; +import static io.opentelemetry.instrumentation.api.semconv.http.HttpCommonAttributesExtractor.firstHeaderValue; + +import io.opentelemetry.instrumentation.api.semconv.network.internal.AddressAndPortExtractor; + +final class HostAddressAndPortExtractor implements AddressAndPortExtractor { + + private final HttpCommonAttributesGetter getter; + + HostAddressAndPortExtractor(HttpCommonAttributesGetter getter) { + this.getter = getter; + } + + @Override + public void extract(AddressPortSink sink, REQUEST request) { + String host = firstHeaderValue(getter.getHttpRequestHeader(request, "host")); + if (host == null) { + return; + } + + int hostHeaderSeparator = host.indexOf(':'); + if (hostHeaderSeparator == -1) { + sink.setAddress(host); + } else { + sink.setAddress(host.substring(0, hostHeaderSeparator)); + setPort(sink, host, hostHeaderSeparator + 1, host.length()); + } + } +} diff --git a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/http/HttpClientAttributesExtractor.java b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/http/HttpClientAttributesExtractor.java new file mode 100644 index 000000000000..5769d1169c3f --- /dev/null +++ b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/http/HttpClientAttributesExtractor.java @@ -0,0 +1,148 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.semconv.http; + +import static io.opentelemetry.instrumentation.api.internal.AttributesExtractorUtil.internalSet; + +import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.InstrumenterBuilder; +import io.opentelemetry.instrumentation.api.internal.SpanKey; +import io.opentelemetry.instrumentation.api.internal.SpanKeyProvider; +import io.opentelemetry.instrumentation.api.semconv.network.internal.InternalNetworkAttributesExtractor; +import io.opentelemetry.instrumentation.api.semconv.network.internal.InternalServerAttributesExtractor; +import io.opentelemetry.semconv.HttpAttributes; +import io.opentelemetry.semconv.UrlAttributes; +import java.util.function.ToIntFunction; +import javax.annotation.Nullable; + +/** + * Extractor of HTTP + * client attributes. + * + * @since 2.0.0 + */ +public final class HttpClientAttributesExtractor + extends HttpCommonAttributesExtractor< + REQUEST, RESPONSE, HttpClientAttributesGetter> + implements SpanKeyProvider { + + /** + * Creates the HTTP client attributes extractor with default configuration. + * + * @see InstrumenterBuilder#addAttributesExtractor(AttributesExtractor) + */ + public static AttributesExtractor create( + HttpClientAttributesGetter httpAttributesGetter) { + return builder(httpAttributesGetter).build(); + } + + /** + * Returns a new {@link HttpClientAttributesExtractorBuilder} that can be used to configure the + * HTTP client attributes extractor. + */ + public static HttpClientAttributesExtractorBuilder builder( + HttpClientAttributesGetter httpAttributesGetter) { + return new HttpClientAttributesExtractorBuilder<>(httpAttributesGetter); + } + + private final InternalNetworkAttributesExtractor internalNetworkExtractor; + private final InternalServerAttributesExtractor internalServerExtractor; + private final ToIntFunction resendCountIncrementer; + + HttpClientAttributesExtractor(HttpClientAttributesExtractorBuilder builder) { + super( + builder.httpAttributesGetter, + HttpStatusCodeConverter.CLIENT, + builder.capturedRequestHeaders, + builder.capturedResponseHeaders, + builder.knownMethods); + internalNetworkExtractor = builder.buildNetworkExtractor(); + internalServerExtractor = builder.buildServerExtractor(); + resendCountIncrementer = builder.resendCountIncrementer; + } + + @Override + public void onStart(AttributesBuilder attributes, Context parentContext, REQUEST request) { + super.onStart(attributes, parentContext, request); + + internalServerExtractor.onStart(attributes, request); + + String fullUrl = stripSensitiveData(getter.getUrlFull(request)); + internalSet(attributes, UrlAttributes.URL_FULL, fullUrl); + + int resendCount = resendCountIncrementer.applyAsInt(parentContext); + if (resendCount > 0) { + attributes.put(HttpAttributes.HTTP_REQUEST_RESEND_COUNT, resendCount); + } + } + + @Override + public void onEnd( + AttributesBuilder attributes, + Context context, + REQUEST request, + @Nullable RESPONSE response, + @Nullable Throwable error) { + super.onEnd(attributes, context, request, response, error); + + internalNetworkExtractor.onEnd(attributes, request, response); + } + + /** + * This method is internal and is hence not for public use. Its API is unstable and can change at + * any time. + */ + @Override + public SpanKey internalGetSpanKey() { + return SpanKey.HTTP_CLIENT; + } + + @Nullable + private static String stripSensitiveData(@Nullable String url) { + if (url == null || url.isEmpty()) { + return url; + } + + int schemeEndIndex = url.indexOf(':'); + + if (schemeEndIndex == -1) { + // not a valid url + return url; + } + + int len = url.length(); + if (len <= schemeEndIndex + 2 + || url.charAt(schemeEndIndex + 1) != '/' + || url.charAt(schemeEndIndex + 2) != '/') { + // has no authority component + return url; + } + + // look for the end of the authority component: + // '/', '?', '#' ==> start of path + int index; + int atIndex = -1; + for (index = schemeEndIndex + 3; index < len; index++) { + char c = url.charAt(index); + + if (c == '@') { + atIndex = index; + } + + if (c == '/' || c == '?' || c == '#') { + break; + } + } + + if (atIndex == -1 || atIndex == len - 1) { + return url; + } + return url.substring(0, schemeEndIndex + 3) + "REDACTED:REDACTED" + url.substring(atIndex); + } +} diff --git a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/http/HttpClientAttributesExtractorBuilder.java b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/http/HttpClientAttributesExtractorBuilder.java new file mode 100644 index 000000000000..c1ba07a1cb12 --- /dev/null +++ b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/http/HttpClientAttributesExtractorBuilder.java @@ -0,0 +1,135 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.semconv.http; + +import static java.util.Collections.emptyList; + +import com.google.errorprone.annotations.CanIgnoreReturnValue; +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.InstrumenterBuilder; +import io.opentelemetry.instrumentation.api.internal.HttpConstants; +import io.opentelemetry.instrumentation.api.semconv.network.internal.AddressAndPortExtractor; +import io.opentelemetry.instrumentation.api.semconv.network.internal.InternalNetworkAttributesExtractor; +import io.opentelemetry.instrumentation.api.semconv.network.internal.InternalServerAttributesExtractor; +import io.opentelemetry.instrumentation.api.semconv.network.internal.ServerAddressAndPortExtractor; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.function.ToIntFunction; + +/** + * A builder of {@link HttpClientAttributesExtractor}. + * + * @since 2.0.0 + */ +public final class HttpClientAttributesExtractorBuilder { + + final HttpClientAttributesGetter httpAttributesGetter; + + final AddressAndPortExtractor serverAddressAndPortExtractor; + List capturedRequestHeaders = emptyList(); + List capturedResponseHeaders = emptyList(); + Set knownMethods = HttpConstants.KNOWN_METHODS; + ToIntFunction resendCountIncrementer = HttpClientRequestResendCount::getAndIncrement; + + HttpClientAttributesExtractorBuilder( + HttpClientAttributesGetter httpAttributesGetter) { + this.httpAttributesGetter = httpAttributesGetter; + serverAddressAndPortExtractor = + new ServerAddressAndPortExtractor<>( + httpAttributesGetter, new HostAddressAndPortExtractor<>(httpAttributesGetter)); + } + + /** + * Configures the HTTP request headers that will be captured as span attributes as described in HTTP + * semantic conventions. + * + *

The HTTP request header values will be captured under the {@code http.request.header.} + * attribute key. The {@code } part in the attribute key is the lowercase header name. + * + * @param requestHeaders A list of HTTP header names. + */ + @CanIgnoreReturnValue + public HttpClientAttributesExtractorBuilder setCapturedRequestHeaders( + List requestHeaders) { + this.capturedRequestHeaders = new ArrayList<>(requestHeaders); + return this; + } + + /** + * Configures the HTTP response headers that will be captured as span attributes as described in + * HTTP + * semantic conventions. + * + *

The HTTP response header values will be captured under the {@code + * http.response.header.} attribute key. The {@code } part in the attribute key is the + * lowercase header name. + * + * @param responseHeaders A list of HTTP header names. + */ + @CanIgnoreReturnValue + public HttpClientAttributesExtractorBuilder setCapturedResponseHeaders( + List responseHeaders) { + this.capturedResponseHeaders = new ArrayList<>(responseHeaders); + return this; + } + + /** + * Configures the extractor to recognize an alternative set of HTTP request methods. + * + *

By default, this extractor defines "known" methods as the ones listed in RFC9110 and the PATCH + * method defined in RFC5789. If an + * unknown method is encountered, the extractor will use the value {@value HttpConstants#_OTHER} + * instead of it and put the original value in an extra {@code http.request.method_original} + * attribute. + * + *

Note: calling this method overrides the default known method sets completely; it does + * not supplement it. + * + * @param knownMethods A set of recognized HTTP request methods. + */ + @CanIgnoreReturnValue + public HttpClientAttributesExtractorBuilder setKnownMethods( + Set knownMethods) { + this.knownMethods = new HashSet<>(knownMethods); + return this; + } + + // visible for tests + @CanIgnoreReturnValue + HttpClientAttributesExtractorBuilder setResendCountIncrementer( + ToIntFunction resendCountIncrementer) { + this.resendCountIncrementer = resendCountIncrementer; + return this; + } + + /** + * Returns a new {@link HttpClientAttributesExtractor} with the settings of this {@link + * HttpClientAttributesExtractorBuilder}. + * + * @see InstrumenterBuilder#addAttributesExtractor(AttributesExtractor) + */ + public AttributesExtractor build() { + return new HttpClientAttributesExtractor<>(this); + } + + InternalNetworkAttributesExtractor buildNetworkExtractor() { + return new InternalNetworkAttributesExtractor<>( + httpAttributesGetter, + // network.{transport,type} are opt-in, network.protocol.* have HTTP-specific logic + /* captureProtocolAttributes= */ false, + /* captureLocalSocketAttributes= */ false); + } + + InternalServerAttributesExtractor buildServerExtractor() { + return new InternalServerAttributesExtractor<>(serverAddressAndPortExtractor); + } +} diff --git a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpClientAttributesGetter.java b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/http/HttpClientAttributesGetter.java similarity index 58% rename from instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpClientAttributesGetter.java rename to instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/http/HttpClientAttributesGetter.java index 169a6181abc2..d81eb582f2a0 100644 --- a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpClientAttributesGetter.java +++ b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/http/HttpClientAttributesGetter.java @@ -3,8 +3,10 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.instrumentation.api.instrumenter.http; +package io.opentelemetry.instrumentation.api.semconv.http; +import io.opentelemetry.instrumentation.api.semconv.network.NetworkAttributesGetter; +import io.opentelemetry.instrumentation.api.semconv.network.ServerAttributesGetter; import javax.annotation.Nullable; /** @@ -13,9 +15,13 @@ *

Instrumentation authors will create implementations of this interface for their specific * library/framework. It will be used by the {@link HttpClientAttributesExtractor} to obtain the * various HTTP client attributes in a type-generic way. + * + * @since 2.0.0 */ public interface HttpClientAttributesGetter - extends HttpCommonAttributesGetter { + extends HttpCommonAttributesGetter, + NetworkAttributesGetter, + ServerAttributesGetter { /** * Returns the absolute URL describing a network resource according to */ @Nullable String getUrlFull(REQUEST request); + + /** {@inheritDoc} */ + @Nullable + @Override + String getServerAddress(REQUEST request); + + /** {@inheritDoc} */ + @Nullable + @Override + Integer getServerPort(REQUEST request); } diff --git a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/http/HttpClientMetrics.java b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/http/HttpClientMetrics.java new file mode 100644 index 000000000000..6f9974206e3d --- /dev/null +++ b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/http/HttpClientMetrics.java @@ -0,0 +1,93 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.semconv.http; + +import static java.util.logging.Level.FINE; + +import com.google.auto.value.AutoValue; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.metrics.DoubleHistogram; +import io.opentelemetry.api.metrics.DoubleHistogramBuilder; +import io.opentelemetry.api.metrics.Meter; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.ContextKey; +import io.opentelemetry.instrumentation.api.instrumenter.InstrumenterBuilder; +import io.opentelemetry.instrumentation.api.instrumenter.OperationListener; +import io.opentelemetry.instrumentation.api.instrumenter.OperationMetrics; +import io.opentelemetry.instrumentation.api.internal.OperationMetricsUtil; +import java.util.concurrent.TimeUnit; +import java.util.logging.Logger; + +/** + * {@link OperationListener} which keeps track of HTTP + * client metrics. + * + * @since 2.0.0 + */ +public final class HttpClientMetrics implements OperationListener { + + private static final double NANOS_PER_S = TimeUnit.SECONDS.toNanos(1); + + private static final ContextKey HTTP_CLIENT_REQUEST_METRICS_STATE = + ContextKey.named("http-client-metrics-state"); + + private static final Logger logger = Logger.getLogger(HttpClientMetrics.class.getName()); + + /** + * Returns an {@link OperationMetrics} instance which can be used to enable recording of {@link + * HttpClientMetrics}. + * + * @see InstrumenterBuilder#addOperationMetrics(OperationMetrics) + */ + public static OperationMetrics get() { + return OperationMetricsUtil.create("http client", HttpClientMetrics::new); + } + + private final DoubleHistogram duration; + + private HttpClientMetrics(Meter meter) { + DoubleHistogramBuilder stableDurationBuilder = + meter + .histogramBuilder("http.client.request.duration") + .setUnit("s") + .setDescription("Duration of HTTP client requests.") + .setExplicitBucketBoundariesAdvice(HttpMetricsAdvice.DURATION_SECONDS_BUCKETS); + HttpMetricsAdvice.applyClientDurationAdvice(stableDurationBuilder); + duration = stableDurationBuilder.build(); + } + + @Override + public Context onStart(Context context, Attributes startAttributes, long startNanos) { + return context.with( + HTTP_CLIENT_REQUEST_METRICS_STATE, + new AutoValue_HttpClientMetrics_State(startAttributes, startNanos)); + } + + @Override + public void onEnd(Context context, Attributes endAttributes, long endNanos) { + State state = context.get(HTTP_CLIENT_REQUEST_METRICS_STATE); + if (state == null) { + logger.log( + FINE, + "No state present when ending context {0}. Cannot record HTTP request metrics.", + context); + return; + } + + Attributes attributes = state.startAttributes().toBuilder().putAll(endAttributes).build(); + + duration.record((endNanos - state.startTimeNanos()) / NANOS_PER_S, attributes, context); + } + + @AutoValue + abstract static class State { + + abstract Attributes startAttributes(); + + abstract long startTimeNanos(); + } +} diff --git a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpClientResend.java b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/http/HttpClientRequestResendCount.java similarity index 58% rename from instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpClientResend.java rename to instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/http/HttpClientRequestResendCount.java index 3df222ec1e7f..cb8dda66aa4e 100644 --- a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpClientResend.java +++ b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/http/HttpClientRequestResendCount.java @@ -3,20 +3,24 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.instrumentation.api.instrumenter.http; +package io.opentelemetry.instrumentation.api.semconv.http; import io.opentelemetry.context.Context; import io.opentelemetry.context.ContextKey; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; -/** A helper that keeps track of the count of the HTTP request resend attempts. */ -public final class HttpClientResend { +/** + * A helper that keeps track of the count of the HTTP request resend attempts. + * + * @since 2.0.0 + */ +public final class HttpClientRequestResendCount { - private static final ContextKey KEY = + private static final ContextKey KEY = ContextKey.named("opentelemetry-http-client-resend-key"); - private static final AtomicIntegerFieldUpdater resendsUpdater = - AtomicIntegerFieldUpdater.newUpdater(HttpClientResend.class, "resends"); + private static final AtomicIntegerFieldUpdater resendsUpdater = + AtomicIntegerFieldUpdater.newUpdater(HttpClientRequestResendCount.class, "resends"); /** * Initializes the HTTP request resend counter. @@ -29,16 +33,20 @@ public static Context initialize(Context context) { if (context.get(KEY) != null) { return context; } - return context.with(KEY, new HttpClientResend()); + return context.with(KEY, new HttpClientRequestResendCount()); } + /** + * Returns the count of the already made attempts to send an HTTP request; 0 if this is the first + * send attempt. + */ public static int get(Context context) { - HttpClientResend resend = context.get(KEY); + HttpClientRequestResendCount resend = context.get(KEY); return resend == null ? 0 : resend.resends; } static int getAndIncrement(Context context) { - HttpClientResend resend = context.get(KEY); + HttpClientRequestResendCount resend = context.get(KEY); if (resend == null) { return 0; } @@ -48,5 +56,5 @@ static int getAndIncrement(Context context) { @SuppressWarnings("unused") // it actually is used by the resendsUpdater private volatile int resends = 0; - private HttpClientResend() {} + private HttpClientRequestResendCount() {} } diff --git a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/http/HttpCommonAttributesExtractor.java b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/http/HttpCommonAttributesExtractor.java new file mode 100644 index 000000000000..a3810af5ce13 --- /dev/null +++ b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/http/HttpCommonAttributesExtractor.java @@ -0,0 +1,137 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.semconv.http; + +import static io.opentelemetry.instrumentation.api.internal.AttributesExtractorUtil.internalSet; +import static io.opentelemetry.instrumentation.api.internal.HttpConstants._OTHER; +import static io.opentelemetry.instrumentation.api.semconv.http.CapturedHttpHeadersUtil.lowercase; +import static io.opentelemetry.instrumentation.api.semconv.http.CapturedHttpHeadersUtil.requestAttributeKey; +import static io.opentelemetry.instrumentation.api.semconv.http.CapturedHttpHeadersUtil.responseAttributeKey; + +import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; +import io.opentelemetry.instrumentation.api.semconv.network.NetworkAttributesGetter; +import io.opentelemetry.semconv.ErrorAttributes; +import io.opentelemetry.semconv.HttpAttributes; +import io.opentelemetry.semconv.NetworkAttributes; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Set; +import javax.annotation.Nullable; + +/** + * Extractor of HTTP + * attributes that are common to client and server instrumentations. + */ +abstract class HttpCommonAttributesExtractor< + REQUEST, + RESPONSE, + GETTER extends + HttpCommonAttributesGetter + & NetworkAttributesGetter> + implements AttributesExtractor { + + final GETTER getter; + private final HttpStatusCodeConverter statusCodeConverter; + private final String[] capturedRequestHeaders; + private final String[] capturedResponseHeaders; + private final Set knownMethods; + + HttpCommonAttributesExtractor( + GETTER getter, + HttpStatusCodeConverter statusCodeConverter, + List capturedRequestHeaders, + List capturedResponseHeaders, + Set knownMethods) { + this.getter = getter; + this.statusCodeConverter = statusCodeConverter; + this.capturedRequestHeaders = lowercase(capturedRequestHeaders); + this.capturedResponseHeaders = lowercase(capturedResponseHeaders); + this.knownMethods = new HashSet<>(knownMethods); + } + + @Override + public void onStart(AttributesBuilder attributes, Context parentContext, REQUEST request) { + String method = getter.getHttpRequestMethod(request); + if (method == null || knownMethods.contains(method)) { + internalSet(attributes, HttpAttributes.HTTP_REQUEST_METHOD, method); + } else { + internalSet(attributes, HttpAttributes.HTTP_REQUEST_METHOD, _OTHER); + internalSet(attributes, HttpAttributes.HTTP_REQUEST_METHOD_ORIGINAL, method); + } + + for (String name : capturedRequestHeaders) { + List values = getter.getHttpRequestHeader(request, name); + if (!values.isEmpty()) { + internalSet(attributes, requestAttributeKey(name), values); + } + } + } + + @Override + public void onEnd( + AttributesBuilder attributes, + Context context, + REQUEST request, + @Nullable RESPONSE response, + @Nullable Throwable error) { + + Integer statusCode = null; + if (response != null) { + statusCode = getter.getHttpResponseStatusCode(request, response, error); + if (statusCode != null && statusCode > 0) { + internalSet(attributes, HttpAttributes.HTTP_RESPONSE_STATUS_CODE, (long) statusCode); + } + + for (String name : capturedResponseHeaders) { + List values = getter.getHttpResponseHeader(request, response, name); + if (!values.isEmpty()) { + internalSet(attributes, responseAttributeKey(name), values); + } + } + } + + String errorType = null; + if (statusCode != null && statusCode > 0) { + if (statusCodeConverter.isError(statusCode)) { + errorType = statusCode.toString(); + } + } else { + errorType = getter.getErrorType(request, response, error); + // fall back to exception class name & _OTHER + if (errorType == null && error != null) { + errorType = error.getClass().getName(); + } + if (errorType == null) { + errorType = _OTHER; + } + } + internalSet(attributes, ErrorAttributes.ERROR_TYPE, errorType); + + String protocolName = lowercaseStr(getter.getNetworkProtocolName(request, response)); + String protocolVersion = lowercaseStr(getter.getNetworkProtocolVersion(request, response)); + + if (protocolVersion != null) { + if (!"http".equals(protocolName)) { + internalSet(attributes, NetworkAttributes.NETWORK_PROTOCOL_NAME, protocolName); + } + internalSet(attributes, NetworkAttributes.NETWORK_PROTOCOL_VERSION, protocolVersion); + } + } + + @Nullable + static String firstHeaderValue(List values) { + return values.isEmpty() ? null : values.get(0); + } + + @Nullable + private static String lowercaseStr(@Nullable String str) { + return str == null ? null : str.toLowerCase(Locale.ROOT); + } +} diff --git a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpCommonAttributesGetter.java b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/http/HttpCommonAttributesGetter.java similarity index 61% rename from instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpCommonAttributesGetter.java rename to instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/http/HttpCommonAttributesGetter.java index 320f1cfde5fc..e3342f3b86d9 100644 --- a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpCommonAttributesGetter.java +++ b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/http/HttpCommonAttributesGetter.java @@ -3,14 +3,19 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.instrumentation.api.instrumenter.http; +package io.opentelemetry.instrumentation.api.semconv.http; import io.opentelemetry.context.Context; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import io.opentelemetry.instrumentation.api.internal.HttpConstants; import java.util.List; import javax.annotation.Nullable; -/** An interface for getting HTTP attributes common to clients and servers. */ +/** + * An interface for getting HTTP attributes common to clients and servers. + * + * @since 2.0.0 + */ public interface HttpCommonAttributesGetter { /** @@ -53,4 +58,25 @@ public interface HttpCommonAttributesGetter { * returned instead. */ List getHttpResponseHeader(REQUEST request, RESPONSE response, String name); + + /** + * Returns a description of a class of error the operation ended with. + * + *

This method is only called if the request failed before response status code was sent or + * received. + * + *

If this method is not implemented, or if it returns {@code null}, the exception class name + * (if any was caught) or the value {@value HttpConstants#_OTHER} will be used as error type. + * + *

The cardinality of the error type should be low. The instrumentations implementing this + * method are recommended to document the custom values they support. + * + *

Examples: {@code timeout}, {@code java.net.UnknownHostException}, {@code + * server_certificate_invalid}, {@code 500}, {@code _OTHER}. + */ + @Nullable + default String getErrorType( + REQUEST request, @Nullable RESPONSE response, @Nullable Throwable error) { + return null; + } } diff --git a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/http/HttpMetricsAdvice.java b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/http/HttpMetricsAdvice.java new file mode 100644 index 000000000000..c852e9d07f91 --- /dev/null +++ b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/http/HttpMetricsAdvice.java @@ -0,0 +1,59 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.semconv.http; + +import static java.util.Arrays.asList; +import static java.util.Collections.unmodifiableList; + +import io.opentelemetry.api.incubator.metrics.ExtendedDoubleHistogramBuilder; +import io.opentelemetry.api.metrics.DoubleHistogramBuilder; +import io.opentelemetry.semconv.ErrorAttributes; +import io.opentelemetry.semconv.HttpAttributes; +import io.opentelemetry.semconv.NetworkAttributes; +import io.opentelemetry.semconv.ServerAttributes; +import io.opentelemetry.semconv.UrlAttributes; +import java.util.List; + +final class HttpMetricsAdvice { + + static final List DURATION_SECONDS_BUCKETS = + unmodifiableList( + asList(0.005, 0.01, 0.025, 0.05, 0.075, 0.1, 0.25, 0.5, 0.75, 1.0, 2.5, 5.0, 7.5, 10.0)); + + static void applyClientDurationAdvice(DoubleHistogramBuilder builder) { + if (!(builder instanceof ExtendedDoubleHistogramBuilder)) { + return; + } + ((ExtendedDoubleHistogramBuilder) builder) + .setAttributesAdvice( + asList( + HttpAttributes.HTTP_REQUEST_METHOD, + HttpAttributes.HTTP_RESPONSE_STATUS_CODE, + ErrorAttributes.ERROR_TYPE, + NetworkAttributes.NETWORK_PROTOCOL_NAME, + NetworkAttributes.NETWORK_PROTOCOL_VERSION, + ServerAttributes.SERVER_ADDRESS, + ServerAttributes.SERVER_PORT)); + } + + static void applyServerDurationAdvice(DoubleHistogramBuilder builder) { + if (!(builder instanceof ExtendedDoubleHistogramBuilder)) { + return; + } + ((ExtendedDoubleHistogramBuilder) builder) + .setAttributesAdvice( + asList( + HttpAttributes.HTTP_ROUTE, + HttpAttributes.HTTP_REQUEST_METHOD, + HttpAttributes.HTTP_RESPONSE_STATUS_CODE, + ErrorAttributes.ERROR_TYPE, + NetworkAttributes.NETWORK_PROTOCOL_NAME, + NetworkAttributes.NETWORK_PROTOCOL_VERSION, + UrlAttributes.URL_SCHEME)); + } + + private HttpMetricsAdvice() {} +} diff --git a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/http/HttpServerAddressAndPortExtractor.java b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/http/HttpServerAddressAndPortExtractor.java new file mode 100644 index 000000000000..a5d075e3d2c5 --- /dev/null +++ b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/http/HttpServerAddressAndPortExtractor.java @@ -0,0 +1,130 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.semconv.http; + +import static io.opentelemetry.instrumentation.api.semconv.http.HeaderParsingHelper.notFound; + +import io.opentelemetry.instrumentation.api.semconv.network.internal.AddressAndPortExtractor; +import java.util.Locale; + +final class HttpServerAddressAndPortExtractor implements AddressAndPortExtractor { + + private final HttpServerAttributesGetter getter; + + HttpServerAddressAndPortExtractor(HttpServerAttributesGetter getter) { + this.getter = getter; + } + + @Override + public void extract(AddressPortSink sink, REQUEST request) { + // try Forwarded + for (String forwarded : getter.getHttpRequestHeader(request, "forwarded")) { + if (extractFromForwardedHeader(sink, forwarded)) { + return; + } + } + + // try X-Forwarded-For + for (String forwardedFor : getter.getHttpRequestHeader(request, "x-forwarded-for")) { + if (extractFromForwardedForHeader(sink, forwardedFor)) { + return; + } + } + + // use network.peer.address and network.peer.port + sink.setAddress(getter.getNetworkPeerAddress(request, null)); + Integer port = getter.getNetworkPeerPort(request, null); + if (port != null && port > 0) { + sink.setPort(port); + } + } + + private static boolean extractFromForwardedHeader(AddressPortSink sink, String forwarded) { + int start = forwarded.toLowerCase(Locale.ROOT).indexOf("for="); + if (start < 0) { + return false; + } + start += "for=".length(); // start is now the index after for= + if (start >= forwarded.length() - 1) { // the value after for= must not be empty + return false; + } + // find the end of the `for=

` section + int end = forwarded.indexOf(';', start); + if (end < 0) { + end = forwarded.length(); + } + return extractClientInfo(sink, forwarded, start, end); + } + + private static boolean extractFromForwardedForHeader(AddressPortSink sink, String forwardedFor) { + return extractClientInfo(sink, forwardedFor, 0, forwardedFor.length()); + } + + // from https://www.rfc-editor.org/rfc/rfc7239 + // "Note that IPv6 addresses may not be quoted in + // X-Forwarded-For and may not be enclosed by square brackets, but they + // are quoted and enclosed in square brackets in Forwarded" + // and also (applying to Forwarded but not X-Forwarded-For) + // "It is important to note that an IPv6 address and any nodename with + // node-port specified MUST be quoted, since ':' is not an allowed + // character in 'token'." + private static boolean extractClientInfo( + AddressPortSink sink, String forwarded, int start, int end) { + if (start >= end) { + return false; + } + + // skip quotes + if (forwarded.charAt(start) == '"') { + // try to find the end of the quote + int quoteEnd = forwarded.indexOf('"', start + 1); + if (notFound(quoteEnd, end)) { + // malformed header value + return false; + } + return extractClientInfo(sink, forwarded, start + 1, quoteEnd); + } + + // ipv6 address enclosed in square brackets case + if (forwarded.charAt(start) == '[') { + int ipv6End = forwarded.indexOf(']', start + 1); + if (notFound(ipv6End, end)) { + // malformed header value + return false; + } + sink.setAddress(forwarded.substring(start + 1, ipv6End)); + + return true; + } + + // try to match either ipv4 or ipv6 without brackets + boolean inIpv4 = false; + for (int i = start; i < end; ++i) { + char c = forwarded.charAt(i); + + // dots only appear in ipv4 + if (c == '.') { + inIpv4 = true; + } + + // find the character terminating the address + boolean isIpv4PortSeparator = inIpv4 && c == ':'; + if (c == ',' || c == ';' || c == '"' || isIpv4PortSeparator) { + // empty string + if (i == start) { + return false; + } + + sink.setAddress(forwarded.substring(start, i)); + return true; + } + } + + // just an address without a port + sink.setAddress(forwarded.substring(start, end)); + return true; + } +} diff --git a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/http/HttpServerAttributesExtractor.java b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/http/HttpServerAttributesExtractor.java new file mode 100644 index 000000000000..ed6077eab787 --- /dev/null +++ b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/http/HttpServerAttributesExtractor.java @@ -0,0 +1,116 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.semconv.http; + +import static io.opentelemetry.instrumentation.api.internal.AttributesExtractorUtil.internalSet; + +import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.InstrumenterBuilder; +import io.opentelemetry.instrumentation.api.internal.SpanKey; +import io.opentelemetry.instrumentation.api.internal.SpanKeyProvider; +import io.opentelemetry.instrumentation.api.semconv.network.internal.InternalClientAttributesExtractor; +import io.opentelemetry.instrumentation.api.semconv.network.internal.InternalNetworkAttributesExtractor; +import io.opentelemetry.instrumentation.api.semconv.network.internal.InternalServerAttributesExtractor; +import io.opentelemetry.instrumentation.api.semconv.url.internal.InternalUrlAttributesExtractor; +import io.opentelemetry.semconv.HttpAttributes; +import io.opentelemetry.semconv.UserAgentAttributes; +import java.util.function.Function; +import javax.annotation.Nullable; + +/** + * Extractor of HTTP + * server attributes. + * + * @since 2.0.0 + */ +public final class HttpServerAttributesExtractor + extends HttpCommonAttributesExtractor< + REQUEST, RESPONSE, HttpServerAttributesGetter> + implements SpanKeyProvider { + + /** + * Creates the HTTP server attributes extractor with default configuration. + * + * @see InstrumenterBuilder#addAttributesExtractor(AttributesExtractor) + */ + public static AttributesExtractor create( + HttpServerAttributesGetter httpAttributesGetter) { + return builder(httpAttributesGetter).build(); + } + + /** + * Returns a new {@link HttpServerAttributesExtractorBuilder} that can be used to configure the + * HTTP client attributes extractor. + */ + public static HttpServerAttributesExtractorBuilder builder( + HttpServerAttributesGetter httpAttributesGetter) { + return new HttpServerAttributesExtractorBuilder<>(httpAttributesGetter); + } + + private final InternalUrlAttributesExtractor internalUrlExtractor; + private final InternalNetworkAttributesExtractor internalNetworkExtractor; + private final InternalServerAttributesExtractor internalServerExtractor; + private final InternalClientAttributesExtractor internalClientExtractor; + private final Function httpRouteGetter; + + HttpServerAttributesExtractor(HttpServerAttributesExtractorBuilder builder) { + super( + builder.httpAttributesGetter, + HttpStatusCodeConverter.SERVER, + builder.capturedRequestHeaders, + builder.capturedResponseHeaders, + builder.knownMethods); + internalUrlExtractor = builder.buildUrlExtractor(); + internalNetworkExtractor = builder.buildNetworkExtractor(); + internalServerExtractor = builder.buildServerExtractor(); + internalClientExtractor = builder.buildClientExtractor(); + httpRouteGetter = builder.httpRouteGetter; + } + + @Override + public void onStart(AttributesBuilder attributes, Context parentContext, REQUEST request) { + super.onStart(attributes, parentContext, request); + + internalUrlExtractor.onStart(attributes, request); + internalServerExtractor.onStart(attributes, request); + internalClientExtractor.onStart(attributes, request); + + internalSet(attributes, HttpAttributes.HTTP_ROUTE, getter.getHttpRoute(request)); + internalSet(attributes, UserAgentAttributes.USER_AGENT_ORIGINAL, userAgent(request)); + } + + @Override + public void onEnd( + AttributesBuilder attributes, + Context context, + REQUEST request, + @Nullable RESPONSE response, + @Nullable Throwable error) { + + super.onEnd(attributes, context, request, response, error); + + internalNetworkExtractor.onEnd(attributes, request, response); + + internalSet(attributes, HttpAttributes.HTTP_ROUTE, httpRouteGetter.apply(context)); + } + + /** + * This method is internal and is hence not for public use. Its API is unstable and can change at + * any time. + */ + @Override + public SpanKey internalGetSpanKey() { + return SpanKey.HTTP_SERVER; + } + + @Nullable + private String userAgent(REQUEST request) { + return firstHeaderValue(getter.getHttpRequestHeader(request, "user-agent")); + } +} diff --git a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/http/HttpServerAttributesExtractorBuilder.java b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/http/HttpServerAttributesExtractorBuilder.java new file mode 100644 index 000000000000..9eec4966d8d0 --- /dev/null +++ b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/http/HttpServerAttributesExtractorBuilder.java @@ -0,0 +1,154 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.semconv.http; + +import static java.util.Collections.emptyList; + +import com.google.errorprone.annotations.CanIgnoreReturnValue; +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.InstrumenterBuilder; +import io.opentelemetry.instrumentation.api.internal.HttpConstants; +import io.opentelemetry.instrumentation.api.semconv.network.internal.AddressAndPortExtractor; +import io.opentelemetry.instrumentation.api.semconv.network.internal.ClientAddressAndPortExtractor; +import io.opentelemetry.instrumentation.api.semconv.network.internal.InternalClientAttributesExtractor; +import io.opentelemetry.instrumentation.api.semconv.network.internal.InternalNetworkAttributesExtractor; +import io.opentelemetry.instrumentation.api.semconv.network.internal.InternalServerAttributesExtractor; +import io.opentelemetry.instrumentation.api.semconv.url.internal.InternalUrlAttributesExtractor; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.function.Function; + +/** + * A builder of {@link HttpServerAttributesExtractor}. + * + * @since 2.0.0 + */ +public final class HttpServerAttributesExtractorBuilder { + + final HttpServerAttributesGetter httpAttributesGetter; + + final AddressAndPortExtractor clientAddressPortExtractor; + final AddressAndPortExtractor serverAddressPortExtractor; + List capturedRequestHeaders = emptyList(); + List capturedResponseHeaders = emptyList(); + Set knownMethods = HttpConstants.KNOWN_METHODS; + Function httpRouteGetter = HttpServerRoute::get; + + HttpServerAttributesExtractorBuilder( + HttpServerAttributesGetter httpAttributesGetter) { + this.httpAttributesGetter = httpAttributesGetter; + + clientAddressPortExtractor = + new ClientAddressAndPortExtractor<>( + httpAttributesGetter, new HttpServerAddressAndPortExtractor<>(httpAttributesGetter)); + serverAddressPortExtractor = new ForwardedHostAddressAndPortExtractor<>(httpAttributesGetter); + } + + /** + * Configures the HTTP response headers that will be captured as span attributes as described in + * HTTP + * semantic conventions. + * + *

The HTTP request header values will be captured under the {@code http.request.header.} + * attribute key. The {@code } part in the attribute key is the lowercase header name. + * + * @param requestHeaders A list of HTTP header names. + */ + @CanIgnoreReturnValue + public HttpServerAttributesExtractorBuilder setCapturedRequestHeaders( + List requestHeaders) { + this.capturedRequestHeaders = new ArrayList<>(requestHeaders); + return this; + } + + /** + * Configures the HTTP response headers that will be captured as span attributes as described in + * HTTP + * semantic conventions. + * + *

The HTTP response header values will be captured under the {@code + * http.response.header.} attribute key. The {@code } part in the attribute key is the + * lowercase header name. + * + * @param responseHeaders A list of HTTP header names. + */ + @CanIgnoreReturnValue + public HttpServerAttributesExtractorBuilder setCapturedResponseHeaders( + List responseHeaders) { + this.capturedResponseHeaders = new ArrayList<>(responseHeaders); + return this; + } + + /** + * Configures the extractor to recognize an alternative set of HTTP request methods. + * + *

By default, this extractor defines "known" methods as the ones listed in RFC9110 and the PATCH + * method defined in RFC5789. If an + * unknown method is encountered, the extractor will use the value {@value HttpConstants#_OTHER} + * instead of it and put the original value in an extra {@code http.request.method_original} + * attribute. + * + *

Note: calling this method overrides the default known method sets completely; it does + * not supplement it. + * + * @param knownMethods A set of recognized HTTP request methods. + */ + @CanIgnoreReturnValue + public HttpServerAttributesExtractorBuilder setKnownMethods( + Set knownMethods) { + this.knownMethods = new HashSet<>(knownMethods); + return this; + } + + // visible for tests + @CanIgnoreReturnValue + HttpServerAttributesExtractorBuilder setHttpRouteGetter( + Function httpRouteGetter) { + this.httpRouteGetter = httpRouteGetter; + return this; + } + + /** + * Returns a new {@link HttpServerAttributesExtractor} with the settings of this {@link + * HttpServerAttributesExtractorBuilder}. + * + * @see InstrumenterBuilder#addAttributesExtractor(AttributesExtractor) + */ + public AttributesExtractor build() { + return new HttpServerAttributesExtractor<>(this); + } + + InternalUrlAttributesExtractor buildUrlExtractor() { + return new InternalUrlAttributesExtractor<>( + httpAttributesGetter, new ForwardedUrlSchemeProvider<>(httpAttributesGetter)); + } + + InternalNetworkAttributesExtractor buildNetworkExtractor() { + return new InternalNetworkAttributesExtractor<>( + httpAttributesGetter, + // network.{transport,type} are opt-in, network.protocol.* have HTTP-specific logic + /* captureProtocolAttributes= */ false, + // network.local.* are opt-in + /* captureLocalSocketAttributes= */ false); + } + + InternalServerAttributesExtractor buildServerExtractor() { + return new InternalServerAttributesExtractor<>(serverAddressPortExtractor); + } + + InternalClientAttributesExtractor buildClientExtractor() { + return new InternalClientAttributesExtractor<>( + clientAddressPortExtractor, + // client.port is opt-in + /* capturePort= */ false); + } +} diff --git a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpServerAttributesGetter.java b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/http/HttpServerAttributesGetter.java similarity index 59% rename from instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpServerAttributesGetter.java rename to instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/http/HttpServerAttributesGetter.java index b377f5b441bb..0c60d22b7d86 100644 --- a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpServerAttributesGetter.java +++ b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/http/HttpServerAttributesGetter.java @@ -3,9 +3,11 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.instrumentation.api.instrumenter.http; +package io.opentelemetry.instrumentation.api.semconv.http; -import io.opentelemetry.instrumentation.api.instrumenter.url.UrlAttributesGetter; +import io.opentelemetry.instrumentation.api.semconv.network.ClientAttributesGetter; +import io.opentelemetry.instrumentation.api.semconv.network.NetworkAttributesGetter; +import io.opentelemetry.instrumentation.api.semconv.url.UrlAttributesGetter; import javax.annotation.Nullable; /** @@ -14,9 +16,14 @@ *

Instrumentation authors will create implementations of this interface for their specific * library/framework. It will be used by the {@link HttpServerAttributesExtractor} to obtain the * various HTTP server attributes in a type-generic way. + * + * @since 2.0.0 */ public interface HttpServerAttributesGetter - extends HttpCommonAttributesGetter, UrlAttributesGetter { + extends HttpCommonAttributesGetter, + UrlAttributesGetter, + NetworkAttributesGetter, + ClientAttributesGetter { /** {@inheritDoc} */ @Nullable @@ -34,8 +41,8 @@ public interface HttpServerAttributesGetter String getUrlQuery(REQUEST request); /** - * Returns the matched route (path template in the format used by the respective server - * framework). + * Returns the matched route, that is, the path template in the format used by the respective + * server framework. * *

Examples: {@code /users/:userID?}, {@code {controller}/{action}/{id?}} */ diff --git a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/http/HttpServerMetrics.java b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/http/HttpServerMetrics.java new file mode 100644 index 000000000000..58cd3abf5caf --- /dev/null +++ b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/http/HttpServerMetrics.java @@ -0,0 +1,93 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.semconv.http; + +import static java.util.logging.Level.FINE; + +import com.google.auto.value.AutoValue; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.metrics.DoubleHistogram; +import io.opentelemetry.api.metrics.DoubleHistogramBuilder; +import io.opentelemetry.api.metrics.Meter; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.ContextKey; +import io.opentelemetry.instrumentation.api.instrumenter.InstrumenterBuilder; +import io.opentelemetry.instrumentation.api.instrumenter.OperationListener; +import io.opentelemetry.instrumentation.api.instrumenter.OperationMetrics; +import io.opentelemetry.instrumentation.api.internal.OperationMetricsUtil; +import java.util.concurrent.TimeUnit; +import java.util.logging.Logger; + +/** + * {@link OperationListener} which keeps track of HTTP + * server metrics. + * + * @since 2.0.0 + */ +public final class HttpServerMetrics implements OperationListener { + + private static final double NANOS_PER_S = TimeUnit.SECONDS.toNanos(1); + + private static final ContextKey HTTP_SERVER_METRICS_STATE = + ContextKey.named("http-server-metrics-state"); + + private static final Logger logger = Logger.getLogger(HttpServerMetrics.class.getName()); + + /** + * Returns an {@link OperationMetrics} instance which can be used to enable recording of {@link + * HttpServerMetrics}. + * + * @see InstrumenterBuilder#addOperationMetrics(OperationMetrics) + */ + public static OperationMetrics get() { + return OperationMetricsUtil.create("http server", HttpServerMetrics::new); + } + + private final DoubleHistogram duration; + + private HttpServerMetrics(Meter meter) { + DoubleHistogramBuilder stableDurationBuilder = + meter + .histogramBuilder("http.server.request.duration") + .setUnit("s") + .setDescription("Duration of HTTP server requests.") + .setExplicitBucketBoundariesAdvice(HttpMetricsAdvice.DURATION_SECONDS_BUCKETS); + HttpMetricsAdvice.applyServerDurationAdvice(stableDurationBuilder); + duration = stableDurationBuilder.build(); + } + + @Override + public Context onStart(Context context, Attributes startAttributes, long startNanos) { + return context.with( + HTTP_SERVER_METRICS_STATE, + new AutoValue_HttpServerMetrics_State(startAttributes, startNanos)); + } + + @Override + public void onEnd(Context context, Attributes endAttributes, long endNanos) { + State state = context.get(HTTP_SERVER_METRICS_STATE); + if (state == null) { + logger.log( + FINE, + "No state present when ending context {0}. Cannot record HTTP request metrics.", + context); + return; + } + + Attributes attributes = state.startAttributes().toBuilder().putAll(endAttributes).build(); + + duration.record((endNanos - state.startTimeNanos()) / NANOS_PER_S, attributes, context); + } + + @AutoValue + abstract static class State { + + abstract Attributes startAttributes(); + + abstract long startTimeNanos(); + } +} diff --git a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpRouteHolder.java b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/http/HttpServerRoute.java similarity index 53% rename from instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpRouteHolder.java rename to instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/http/HttpServerRoute.java index 6ed74e08e966..69172ca72fb9 100644 --- a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpRouteHolder.java +++ b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/http/HttpServerRoute.java @@ -3,15 +3,14 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.instrumentation.api.instrumenter.http; +package io.opentelemetry.instrumentation.api.semconv.http; import io.opentelemetry.api.trace.Span; import io.opentelemetry.context.Context; import io.opentelemetry.instrumentation.api.instrumenter.ContextCustomizer; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; -import io.opentelemetry.instrumentation.api.instrumenter.LocalRootSpan; +import io.opentelemetry.instrumentation.api.instrumenter.InstrumenterBuilder; import io.opentelemetry.instrumentation.api.internal.HttpRouteState; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; import javax.annotation.Nullable; /** @@ -22,97 +21,92 @@ * later, after the instrumented operation starts. This class provides several static methods that * allow the instrumentation author to provide the matching HTTP route to the instrumentation when * it is discovered. + * + * @since 2.0.0 */ -public final class HttpRouteHolder { +public final class HttpServerRoute { /** - * Returns a {@link ContextCustomizer} that initializes a {@link HttpRouteHolder} in the {@link + * Returns a {@link ContextCustomizer} that initializes an {@link HttpServerRoute} in the {@link * Context} returned from {@link Instrumenter#start(Context, Object)}. + * + * @see InstrumenterBuilder#addContextCustomizer(ContextCustomizer) */ public static ContextCustomizer create( HttpServerAttributesGetter getter) { - return (context, request, startAttributes) -> { - if (HttpRouteState.fromContextOrNull(context) != null) { - return context; - } - String method = getter.getHttpRequestMethod(request); - return context.with(HttpRouteState.create(method, null, 0)); - }; + return builder(getter).build(); } - private HttpRouteHolder() {} + /** + * Returns a new {@link HttpServerRouteBuilder} that can be used to configure the {@link + * HttpServerRoute}. + */ + public static HttpServerRouteBuilder builder( + HttpServerAttributesGetter getter) { + return new HttpServerRouteBuilder<>(getter); + } + + private HttpServerRoute() {} /** * Updates the {@code http.route} attribute in the received {@code context}. * *

If there is a server span in the context, and the context has been customized with a {@link - * HttpRouteHolder}, then this method will update the route using the provided {@code httpRoute} - * if and only if the last {@link HttpRouteSource} to update the route using this method has - * strictly lower priority than the provided {@link HttpRouteSource}, and the passed value is - * non-null. - * - *

If there is a server span in the context, and the context has NOT been customized with a - * {@link HttpRouteHolder}, then this method will update the route using the provided value if it + * HttpServerRoute}, then this method will update the route using the provided {@code httpRoute} + * if and only if the last {@link HttpServerRouteSource} to update the route using this method has + * strictly lower priority than the provided {@link HttpServerRouteSource}, and the passed value * is non-null. */ - public static void updateHttpRoute( - Context context, HttpRouteSource source, @Nullable String httpRoute) { - updateHttpRoute(context, source, ConstantAdapter.INSTANCE, httpRoute); + public static void update( + Context context, HttpServerRouteSource source, @Nullable String httpRoute) { + update(context, source, ConstantAdapter.INSTANCE, httpRoute); } /** * Updates the {@code http.route} attribute in the received {@code context}. * *

If there is a server span in the context, and the context has been customized with a {@link - * HttpRouteHolder}, then this method will update the route using the provided {@link - * HttpRouteGetter} if and only if the last {@link HttpRouteSource} to update the route using this - * method has strictly lower priority than the provided {@link HttpRouteSource}, and the value - * returned from the {@link HttpRouteGetter} is non-null. - * - *

If there is a server span in the context, and the context has NOT been customized with a - * {@link HttpRouteHolder}, then this method will update the route using the provided {@link - * HttpRouteGetter} if the value returned from it is non-null. + * HttpServerRoute}, then this method will update the route using the provided {@link + * HttpServerRouteGetter} if and only if the last {@link HttpServerRouteSource} to update the + * route using this method has strictly lower priority than the provided {@link + * HttpServerRouteSource}, and the value returned from the {@link HttpServerRouteGetter} is + * non-null. */ - public static void updateHttpRoute( - Context context, HttpRouteSource source, HttpRouteGetter httpRouteGetter, T arg1) { - updateHttpRoute(context, source, OneArgAdapter.getInstance(), arg1, httpRouteGetter); + public static void update( + Context context, + HttpServerRouteSource source, + HttpServerRouteGetter httpRouteGetter, + T arg1) { + update(context, source, OneArgAdapter.getInstance(), arg1, httpRouteGetter); } /** * Updates the {@code http.route} attribute in the received {@code context}. * *

If there is a server span in the context, and the context has been customized with a {@link - * HttpRouteHolder}, then this method will update the route using the provided {@link - * HttpRouteBiGetter} if and only if the last {@link HttpRouteSource} to update the route using - * this method has strictly lower priority than the provided {@link HttpRouteSource}, and the - * value returned from the {@link HttpRouteBiGetter} is non-null. - * - *

If there is a server span in the context, and the context has NOT been customized with a - * {@code ServerSpanName}, then this method will update the route using the provided {@link - * HttpRouteBiGetter} if the value returned from it is non-null. + * HttpServerRoute}, then this method will update the route using the provided {@link + * HttpServerRouteBiGetter} if and only if the last {@link HttpServerRouteSource} to update the + * route using this method has strictly lower priority than the provided {@link + * HttpServerRouteSource}, and the value returned from the {@link HttpServerRouteBiGetter} is + * non-null. */ - public static void updateHttpRoute( + public static void update( Context context, - HttpRouteSource source, - HttpRouteBiGetter httpRouteGetter, + HttpServerRouteSource source, + HttpServerRouteBiGetter httpRouteGetter, T arg1, U arg2) { - Span serverSpan = LocalRootSpan.fromContextOrNull(context); + HttpRouteState httpRouteState = HttpRouteState.fromContextOrNull(context); + if (httpRouteState == null) { + return; + } + Span serverSpan = httpRouteState.getSpan(); // even if the server span is not sampled, we have to continue - we need to compute the // http.route properly so that it can be captured by the server metrics if (serverSpan == null) { return; } - HttpRouteState httpRouteState = HttpRouteState.fromContextOrNull(context); - if (httpRouteState == null) { - // TODO: remove this branch? - String httpRoute = httpRouteGetter.get(context, arg1, arg2); - if (httpRoute != null && !httpRoute.isEmpty()) { - // update just the attribute - without http.method we can't compute a proper span name here - serverSpan.setAttribute(SemanticAttributes.HTTP_ROUTE, httpRoute); - } - return; - } + // special case for servlet filters, even when we have a route from previous filter see whether // the new route is better and if so use it instead boolean onlyIfBetterRoute = @@ -142,11 +136,8 @@ private static boolean isBetterRoute(HttpRouteState httpRouteState, String name) private static void updateSpanName(Span serverSpan, HttpRouteState httpRouteState, String route) { String method = httpRouteState.getMethod(); - // method should never really be null - but in case it for some reason is, we'll rely on the - // span name extractor behavior - if (method != null) { - serverSpan.updateName(method + " " + route); - } + // method should never really be null + serverSpan.updateName(method + " " + route); } /** @@ -154,12 +145,13 @@ private static void updateSpanName(Span serverSpan, HttpRouteState httpRouteStat * it was not set before. */ @Nullable - static String getRoute(Context context) { + static String get(Context context) { HttpRouteState httpRouteState = HttpRouteState.fromContextOrNull(context); return httpRouteState == null ? null : httpRouteState.getRoute(); } - private static final class OneArgAdapter implements HttpRouteBiGetter> { + private static final class OneArgAdapter + implements HttpServerRouteBiGetter> { private static final OneArgAdapter INSTANCE = new OneArgAdapter<>(); @@ -170,12 +162,12 @@ static OneArgAdapter getInstance() { @Override @Nullable - public String get(Context context, T arg, HttpRouteGetter httpRouteGetter) { + public String get(Context context, T arg, HttpServerRouteGetter httpRouteGetter) { return httpRouteGetter.get(context, arg); } } - private static final class ConstantAdapter implements HttpRouteGetter { + private static final class ConstantAdapter implements HttpServerRouteGetter { private static final ConstantAdapter INSTANCE = new ConstantAdapter(); diff --git a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpRouteBiGetter.java b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/http/HttpServerRouteBiGetter.java similarity index 67% rename from instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpRouteBiGetter.java rename to instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/http/HttpServerRouteBiGetter.java index 4a7008953bd8..6f7be0e547a9 100644 --- a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpRouteBiGetter.java +++ b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/http/HttpServerRouteBiGetter.java @@ -3,14 +3,18 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.instrumentation.api.instrumenter.http; +package io.opentelemetry.instrumentation.api.semconv.http; import io.opentelemetry.context.Context; import javax.annotation.Nullable; -/** An interface for getting the {@code http.route} attribute. */ +/** + * An interface for getting the {@code http.route} attribute. + * + * @since 2.0.0 + */ @FunctionalInterface -public interface HttpRouteBiGetter { +public interface HttpServerRouteBiGetter { /** * Returns the {@code http.route} attribute extracted from {@code context}, {@code arg1} and diff --git a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/http/HttpServerRouteBuilder.java b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/http/HttpServerRouteBuilder.java new file mode 100644 index 000000000000..e1a6e3dc21a7 --- /dev/null +++ b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/http/HttpServerRouteBuilder.java @@ -0,0 +1,72 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.semconv.http; + +import com.google.errorprone.annotations.CanIgnoreReturnValue; +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.instrumenter.ContextCustomizer; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import io.opentelemetry.instrumentation.api.instrumenter.InstrumenterBuilder; +import io.opentelemetry.instrumentation.api.internal.HttpConstants; +import io.opentelemetry.instrumentation.api.internal.HttpRouteState; +import java.util.HashSet; +import java.util.Set; + +/** + * A builder of {@link HttpServerRoute}. + * + * @since 2.0.0 + */ +public final class HttpServerRouteBuilder { + + final HttpServerAttributesGetter getter; + Set knownMethods = HttpConstants.KNOWN_METHODS; + + HttpServerRouteBuilder(HttpServerAttributesGetter getter) { + this.getter = getter; + } + + /** + * Configures the customizer to recognize an alternative set of HTTP request methods. + * + *

By default, this customizer defines "known" methods as the ones listed in RFC9110 and the PATCH + * method defined in RFC5789. If an + * unknown method is encountered, the customizer will use the value {@value HttpConstants#_OTHER} + * instead. + * + *

Note: calling this method overrides the default known method sets completely; it does + * not supplement it. + * + * @param knownMethods A set of recognized HTTP request methods. + */ + @CanIgnoreReturnValue + public HttpServerRouteBuilder setKnownMethods(Set knownMethods) { + this.knownMethods = new HashSet<>(knownMethods); + return this; + } + + /** + * Returns a {@link ContextCustomizer} that initializes an {@link HttpServerRoute} in the {@link + * Context} returned from {@link Instrumenter#start(Context, Object)}. The returned customizer is + * configured with the settings of this {@link HttpServerRouteBuilder}. + * + * @see InstrumenterBuilder#addContextCustomizer(ContextCustomizer) + */ + public ContextCustomizer build() { + Set knownMethods = new HashSet<>(this.knownMethods); + return (context, request, startAttributes) -> { + if (HttpRouteState.fromContextOrNull(context) != null) { + return context; + } + String method = getter.getHttpRequestMethod(request); + if (method == null || !knownMethods.contains(method)) { + method = "HTTP"; + } + return context.with(HttpRouteState.create(method, null, 0)); + }; + } +} diff --git a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpRouteGetter.java b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/http/HttpServerRouteGetter.java similarity index 66% rename from instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpRouteGetter.java rename to instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/http/HttpServerRouteGetter.java index a76cbf2ad8ff..ee2eb17433b9 100644 --- a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpRouteGetter.java +++ b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/http/HttpServerRouteGetter.java @@ -3,14 +3,18 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.instrumentation.api.instrumenter.http; +package io.opentelemetry.instrumentation.api.semconv.http; import io.opentelemetry.context.Context; import javax.annotation.Nullable; -/** An interface for getting the {@code http.route} attribute. */ +/** + * An interface for getting the {@code http.route} attribute. + * + * @since 2.0.0 + */ @FunctionalInterface -public interface HttpRouteGetter { +public interface HttpServerRouteGetter { /** * Returns the {@code http.route} attribute extracted from {@code context} and {@code arg}; or diff --git a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/http/HttpServerRouteSource.java b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/http/HttpServerRouteSource.java new file mode 100644 index 000000000000..e25aea737963 --- /dev/null +++ b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/http/HttpServerRouteSource.java @@ -0,0 +1,45 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.semconv.http; + +/** + * Represents the source that provided the {@code http.route} attribute. + * + * @since 2.0.0 + */ +public enum HttpServerRouteSource { + /** + * Represents a "filter" that may execute before the actual server handler. E.g. the Servlet API + * {@code Filter} interface. Since multiple filters may match the same request, the one with the + * longest (most detailed) route will be chosen. + */ + SERVER_FILTER(1, /* useFirst= */ false), + /** Represents the actual server handler. E.g. the Servlet API {@code Servlet} interface. */ + SERVER(2), + /** + * Represents the controller, usually defined as part of some MVC framework. E.g. a Spring Web MVC + * controller method, or a JAX-RS annotated resource method. + */ + CONTROLLER(3), + /** + * Represents a nested controller, usually defined as part of some MVC framework. E.g. a JAX-RS + * annotated sub-resource method. Since multiple nested controllers may match the same request, + * the one with the longest (most detailed) route will be chosen. + */ + NESTED_CONTROLLER(4, /* useFirst= */ false); + + final int order; + final boolean useFirst; + + HttpServerRouteSource(int order) { + this(order, /* useFirst= */ true); + } + + HttpServerRouteSource(int order, boolean useFirst) { + this.order = order; + this.useFirst = useFirst; + } +} diff --git a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/http/HttpSpanNameExtractor.java b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/http/HttpSpanNameExtractor.java new file mode 100644 index 000000000000..23a24579d875 --- /dev/null +++ b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/http/HttpSpanNameExtractor.java @@ -0,0 +1,105 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.semconv.http; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor; +import java.util.Set; + +/** + * Extractor of the HTTP + * span name. + * + * @since 2.0.0 + */ +public final class HttpSpanNameExtractor { + + /** + * Returns an HTTP client {@link SpanNameExtractor} with default configuration. + * + * @see Instrumenter#builder(OpenTelemetry, String, SpanNameExtractor) + */ + public static SpanNameExtractor create( + HttpClientAttributesGetter getter) { + return builder(getter).build(); + } + + /** + * Returns an HTTP server {@link SpanNameExtractor} with default configuration. + * + * @see Instrumenter#builder(OpenTelemetry, String, SpanNameExtractor) + */ + public static SpanNameExtractor create( + HttpServerAttributesGetter getter) { + return builder(getter).build(); + } + + /** + * Returns a new {@link HttpSpanNameExtractorBuilder} that can be used to configure the HTTP + * client span name extractor. + */ + public static HttpSpanNameExtractorBuilder builder( + HttpClientAttributesGetter getter) { + return new HttpSpanNameExtractorBuilder<>(getter, null); + } + + /** + * Returns a new {@link HttpSpanNameExtractorBuilder} that can be used to configure the HTTP + * server span name extractor. + */ + public static HttpSpanNameExtractorBuilder builder( + HttpServerAttributesGetter getter) { + return new HttpSpanNameExtractorBuilder<>(null, getter); + } + + static final class Client implements SpanNameExtractor { + + private final HttpClientAttributesGetter getter; + private final Set knownMethods; + + Client(HttpClientAttributesGetter getter, Set knownMethods) { + this.getter = getter; + this.knownMethods = knownMethods; + } + + @Override + public String extract(REQUEST request) { + String method = getter.getHttpRequestMethod(request); + if (method == null || !knownMethods.contains(method)) { + return "HTTP"; + } + return method; + } + } + + static final class Server implements SpanNameExtractor { + + private final HttpServerAttributesGetter getter; + private final Set knownMethods; + + Server(HttpServerAttributesGetter getter, Set knownMethods) { + this.getter = getter; + this.knownMethods = knownMethods; + } + + @Override + public String extract(REQUEST request) { + String method = getter.getHttpRequestMethod(request); + String route = getter.getHttpRoute(request); + if (method == null) { + return "HTTP"; + } + if (!knownMethods.contains(method)) { + method = "HTTP"; + } + return route == null ? method : method + " " + route; + } + } + + private HttpSpanNameExtractor() {} +} diff --git a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/http/HttpSpanNameExtractorBuilder.java b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/http/HttpSpanNameExtractorBuilder.java new file mode 100644 index 000000000000..f2e16ba10a79 --- /dev/null +++ b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/http/HttpSpanNameExtractorBuilder.java @@ -0,0 +1,70 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.semconv.http; + +import static java.util.Objects.requireNonNull; + +import com.google.errorprone.annotations.CanIgnoreReturnValue; +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor; +import io.opentelemetry.instrumentation.api.internal.HttpConstants; +import java.util.HashSet; +import java.util.Set; +import javax.annotation.Nullable; + +/** + * A builder of {@link HttpSpanNameExtractor}. + * + * @since 2.0.0 + */ +public final class HttpSpanNameExtractorBuilder { + + @Nullable final HttpClientAttributesGetter clientGetter; + @Nullable final HttpServerAttributesGetter serverGetter; + Set knownMethods = HttpConstants.KNOWN_METHODS; + + public HttpSpanNameExtractorBuilder( + @Nullable HttpClientAttributesGetter clientGetter, + @Nullable HttpServerAttributesGetter serverGetter) { + this.clientGetter = clientGetter; + this.serverGetter = serverGetter; + } + + /** + * Configures the extractor to recognize an alternative set of HTTP request methods. + * + *

By default, this extractor defines "known" methods as the ones listed in RFC9110 and the PATCH + * method defined in RFC5789. If an + * unknown method is encountered, the extractor will use the value {@value HttpConstants#_OTHER} + * instead of it and put the original value in an extra {@code http.request.method_original} + * attribute. + * + *

Note: calling this method overrides the default known method sets completely; it does + * not supplement it. + * + * @param knownMethods A set of recognized HTTP request methods. + */ + @CanIgnoreReturnValue + public HttpSpanNameExtractorBuilder setKnownMethods(Set knownMethods) { + this.knownMethods = new HashSet<>(knownMethods); + return this; + } + + /** + * Returns a new {@link HttpSpanNameExtractor} with the settings of this {@link + * HttpSpanNameExtractorBuilder}. + * + * @see Instrumenter#builder(OpenTelemetry, String, SpanNameExtractor) + */ + public SpanNameExtractor build() { + Set knownMethods = new HashSet<>(this.knownMethods); + return clientGetter != null + ? new HttpSpanNameExtractor.Client<>(clientGetter, knownMethods) + : new HttpSpanNameExtractor.Server<>(requireNonNull(serverGetter), knownMethods); + } +} diff --git a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpSpanStatusExtractor.java b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/http/HttpSpanStatusExtractor.java similarity index 55% rename from instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpSpanStatusExtractor.java rename to instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/http/HttpSpanStatusExtractor.java index c86f533b9dc4..f3349f66122f 100644 --- a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpSpanStatusExtractor.java +++ b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/http/HttpSpanStatusExtractor.java @@ -3,50 +3,56 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.instrumentation.api.instrumenter.http; +package io.opentelemetry.instrumentation.api.semconv.http; import io.opentelemetry.api.trace.StatusCode; +import io.opentelemetry.instrumentation.api.instrumenter.InstrumenterBuilder; import io.opentelemetry.instrumentation.api.instrumenter.SpanStatusBuilder; import io.opentelemetry.instrumentation.api.instrumenter.SpanStatusExtractor; import javax.annotation.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. + * href="https://github.com/open-telemetry/semantic-conventions/blob/v1.23.0/docs/http/http-spans.md#status">HTTP + * span status. + * + * @since 2.0.0 */ 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 + * Returns the HTTP client {@link SpanStatusExtractor}, which will use the HTTP status code to + * determine the {@link StatusCode} if available or fallback to {@linkplain #getDefault() the * default status} otherwise. + * + * @see InstrumenterBuilder#setSpanStatusExtractor(SpanStatusExtractor) */ public static SpanStatusExtractor create( HttpClientAttributesGetter getter) { - return new HttpSpanStatusExtractor<>(getter, HttpStatusConverter.CLIENT); + return new HttpSpanStatusExtractor<>(getter, HttpStatusCodeConverter.CLIENT); } /** - * 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 + * Returns the HTTP server {@link SpanStatusExtractor}, which will use the HTTP status code to + * determine the {@link StatusCode} if available or fallback to {@linkplain #getDefault() the * default status} otherwise. + * + * @see InstrumenterBuilder#setSpanStatusExtractor(SpanStatusExtractor) */ public static SpanStatusExtractor create( HttpServerAttributesGetter getter) { - return new HttpSpanStatusExtractor<>(getter, HttpStatusConverter.SERVER); + return new HttpSpanStatusExtractor<>(getter, HttpStatusCodeConverter.SERVER); } private final HttpCommonAttributesGetter getter; - private final HttpStatusConverter statusConverter; + private final HttpStatusCodeConverter statusCodeConverter; private HttpSpanStatusExtractor( HttpCommonAttributesGetter getter, - HttpStatusConverter statusConverter) { + HttpStatusCodeConverter statusCodeConverter) { this.getter = getter; - this.statusConverter = statusConverter; + this.statusCodeConverter = statusCodeConverter; } @Override @@ -59,9 +65,8 @@ public void extract( if (response != null) { Integer statusCode = getter.getHttpResponseStatusCode(request, response, error); if (statusCode != null) { - StatusCode statusCodeObj = statusConverter.statusFromHttpStatus(statusCode); - if (statusCodeObj == StatusCode.ERROR) { - spanStatusBuilder.setStatus(statusCodeObj); + if (statusCodeConverter.isError(statusCode)) { + spanStatusBuilder.setStatus(StatusCode.ERROR); return; } } diff --git a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/http/HttpStatusCodeConverter.java b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/http/HttpStatusCodeConverter.java new file mode 100644 index 000000000000..eb1724d69d35 --- /dev/null +++ b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/http/HttpStatusCodeConverter.java @@ -0,0 +1,30 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.semconv.http; + +// https://github.com/open-telemetry/semantic-conventions/blob/v1.21.0/docs/http/http-spans.md#status +enum HttpStatusCodeConverter { + SERVER { + @Override + boolean isError(int responseStatusCode) { + return responseStatusCode >= 500 + || + // invalid status code, does not exists + responseStatusCode < 100; + } + }, + CLIENT { + @Override + boolean isError(int responseStatusCode) { + return responseStatusCode >= 400 + || + // invalid status code, does not exists + responseStatusCode < 100; + } + }; + + abstract boolean isError(int responseStatusCode); +} diff --git a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/http/package-info.java b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/http/package-info.java new file mode 100644 index 000000000000..d9180f265503 --- /dev/null +++ b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/http/package-info.java @@ -0,0 +1,9 @@ +/** + * This module contains the implementation of the + * OpenTelemetry HTTP semantic conventions. + */ +@ParametersAreNonnullByDefault +package io.opentelemetry.instrumentation.api.semconv.http; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/network/ClientAttributesExtractor.java b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/network/ClientAttributesExtractor.java similarity index 51% rename from instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/network/ClientAttributesExtractor.java rename to instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/network/ClientAttributesExtractor.java index 8c3bb259e8e4..fd5143e5f1da 100644 --- a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/network/ClientAttributesExtractor.java +++ b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/network/ClientAttributesExtractor.java @@ -3,42 +3,44 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.instrumentation.api.instrumenter.network; +package io.opentelemetry.instrumentation.api.semconv.network; import io.opentelemetry.api.common.AttributesBuilder; import io.opentelemetry.context.Context; import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.net.internal.FallbackNamePortGetter; -import io.opentelemetry.instrumentation.api.instrumenter.network.internal.InternalClientAttributesExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.InstrumenterBuilder; +import io.opentelemetry.instrumentation.api.semconv.network.internal.AddressAndPortExtractor; +import io.opentelemetry.instrumentation.api.semconv.network.internal.ClientAddressAndPortExtractor; +import io.opentelemetry.instrumentation.api.semconv.network.internal.InternalClientAttributesExtractor; import javax.annotation.Nullable; /** * Extractor of client + * href="https://github.com/open-telemetry/semantic-conventions/blob/v1.23.0/docs/general/attributes.md#client-attributes">client * attributes. + * + * @since 2.0.0 */ public final class ClientAttributesExtractor implements AttributesExtractor { /** - * Returns a new {@link ClientAttributesExtractor} that will use the passed {@link - * ClientAttributesGetter}. + * Creates the client attributes extractor. + * + * @see InstrumenterBuilder#addAttributesExtractor(AttributesExtractor) */ public static ClientAttributesExtractor create( - ClientAttributesGetter getter) { + ClientAttributesGetter getter) { return new ClientAttributesExtractor<>(getter); } - private final InternalClientAttributesExtractor internalExtractor; + private final InternalClientAttributesExtractor internalExtractor; - ClientAttributesExtractor(ClientAttributesGetter getter) { - // the ClientAttributesExtractor will always emit new semconv + ClientAttributesExtractor(ClientAttributesGetter getter) { internalExtractor = new InternalClientAttributesExtractor<>( - getter, - FallbackNamePortGetter.noop(), - /* emitStableUrlAttributes= */ true, - /* emitOldHttpAttributes= */ false); + new ClientAddressAndPortExtractor<>(getter, AddressAndPortExtractor.noop()), + /* capturePort= */ true); } @Override @@ -52,7 +54,5 @@ public void onEnd( Context context, REQUEST request, @Nullable RESPONSE response, - @Nullable Throwable error) { - internalExtractor.onEnd(attributes, request, response); - } + @Nullable Throwable error) {} } diff --git a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/network/ClientAttributesGetter.java b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/network/ClientAttributesGetter.java new file mode 100644 index 000000000000..005b1a997f61 --- /dev/null +++ b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/network/ClientAttributesGetter.java @@ -0,0 +1,41 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.semconv.network; + +import javax.annotation.Nullable; + +/** + * An interface for getting attributes describing a network client. + * + *

Instrumentation authors will create implementations of this interface for their specific + * library/framework. It will be used by the {@link ClientAttributesExtractor} (or other convention + * specific extractors) to obtain the various server attributes in a type-generic way. + * + * @since 2.0.0 + */ +public interface ClientAttributesGetter { + + /** + * Returns the client address - domain name if available without reverse DNS lookup; otherwise, IP + * address or Unix domain socket name. + * + *

Examples: {@code client.example.com}, {@code 10.1.2.80}, {@code /tmp/my.sock} + */ + @Nullable + default String getClientAddress(REQUEST request) { + return null; + } + + /** + * Returns the client port number. + * + *

Examples: {@code 65123} + */ + @Nullable + default Integer getClientPort(REQUEST request) { + return null; + } +} diff --git a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/network/NetworkAttributesExtractor.java b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/network/NetworkAttributesExtractor.java similarity index 66% rename from instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/network/NetworkAttributesExtractor.java rename to instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/network/NetworkAttributesExtractor.java index ed3de6712d68..a4312791746d 100644 --- a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/network/NetworkAttributesExtractor.java +++ b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/network/NetworkAttributesExtractor.java @@ -3,26 +3,29 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.instrumentation.api.instrumenter.network; +package io.opentelemetry.instrumentation.api.semconv.network; import io.opentelemetry.api.common.AttributesBuilder; import io.opentelemetry.context.Context; import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.network.internal.InternalNetworkAttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.network.internal.NetworkTransportFilter; +import io.opentelemetry.instrumentation.api.instrumenter.InstrumenterBuilder; +import io.opentelemetry.instrumentation.api.semconv.network.internal.InternalNetworkAttributesExtractor; import javax.annotation.Nullable; /** * Extractor of network + * href="https://github.com/open-telemetry/semantic-conventions/blob/v1.23.0/docs/general/attributes.md#other-network-attributes">network * attributes. + * + * @since 2.0.0 */ public final class NetworkAttributesExtractor implements AttributesExtractor { /** - * Returns a new {@link NetworkAttributesExtractor} that will use the passed {@link - * NetworkAttributesGetter}. + * Creates the network attributes extractor. + * + * @see InstrumenterBuilder#addAttributesExtractor(AttributesExtractor) */ public static NetworkAttributesExtractor create( NetworkAttributesGetter getter) { @@ -32,13 +35,11 @@ public static NetworkAttributesExtractor private final InternalNetworkAttributesExtractor internalExtractor; NetworkAttributesExtractor(NetworkAttributesGetter getter) { - // the NetworkAttributesExtractor will always emit new semconv internalExtractor = new InternalNetworkAttributesExtractor<>( getter, - NetworkTransportFilter.alwaysTrue(), - /* emitStableUrlAttributes= */ true, - /* emitOldHttpAttributes= */ false); + /* captureProtocolAttributes= */ true, + /* captureLocalSocketAttributes= */ true); } @Override diff --git a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/network/NetworkAttributesGetter.java b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/network/NetworkAttributesGetter.java new file mode 100644 index 000000000000..9e00d82dfb6b --- /dev/null +++ b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/network/NetworkAttributesGetter.java @@ -0,0 +1,157 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.semconv.network; + +import io.opentelemetry.instrumentation.api.semconv.network.internal.InetSocketAddressUtil; +import java.net.InetSocketAddress; +import javax.annotation.Nullable; + +/** + * An interface for getting network attributes. + * + *

Instrumentation authors will create implementations of this interface for their specific + * library/framework. It will be used by the {@link NetworkAttributesExtractor} (or other convention + * specific extractors) to obtain the various network attributes in a type-generic way. + * + * @since 2.0.0 + */ +public interface NetworkAttributesGetter { + + /** + * Returns the OSI network layer or non-OSI + * equivalent. + * + *

Examples: {@code ipv4}, {@code ipv6} + */ + @Nullable + default String getNetworkType(REQUEST request, @Nullable RESPONSE response) { + return InetSocketAddressUtil.getNetworkType( + getNetworkLocalInetSocketAddress(request, response), + getNetworkPeerInetSocketAddress(request, response)); + } + + /** + * Returns the OSI transport layer or inter-process communication + * method. + * + *

Examples: {@code tcp}, {@code udp} + */ + @Nullable + default String getNetworkTransport(REQUEST request, @Nullable RESPONSE response) { + return null; + } + + /** + * Returns the OSI application layer or + * non-OSI equivalent. + * + *

Examples: {@code ampq}, {@code http}, {@code mqtt} + */ + @Nullable + default String getNetworkProtocolName(REQUEST request, @Nullable RESPONSE response) { + return null; + } + + /** + * Returns the version of the protocol returned by {@link #getNetworkProtocolName(Object, + * Object)}. + * + *

Examples: {@code 3.1.1} + */ + @Nullable + default String getNetworkProtocolVersion(REQUEST request, @Nullable RESPONSE response) { + return null; + } + + /** + * Returns an {@link InetSocketAddress} object representing the local socket address. + * + *

Implementing this method is equivalent to implementing both {@link + * #getNetworkLocalAddress(Object, Object)} and {@link #getNetworkLocalPort(Object, Object)}. + */ + @Nullable + default InetSocketAddress getNetworkLocalInetSocketAddress( + REQUEST request, @Nullable RESPONSE response) { + return null; + } + + /** + * Returns the local address of the network connection - IP address or Unix domain socket name. + * + *

Examples: {@code 10.1.2.80}, {@code /tmp/my.sock} + * + *

By default, this method attempts to retrieve the local address using the {@link + * #getNetworkLocalInetSocketAddress(Object, Object)} method. If that method is not implemented, + * it will simply return {@code null}. If the instrumented library does not expose {@link + * InetSocketAddress} in its API, you might want to implement this method instead of {@link + * #getNetworkLocalInetSocketAddress(Object, Object)}. + */ + @Nullable + default String getNetworkLocalAddress(REQUEST request, @Nullable RESPONSE response) { + return InetSocketAddressUtil.getIpAddress(getNetworkLocalInetSocketAddress(request, response)); + } + + /** + * Returns the local port number of the network connection. + * + *

Examples: {@code 65123} + * + *

By default, this method attempts to retrieve the local port using the {@link + * #getNetworkLocalInetSocketAddress(Object, Object)} method. If that method is not implemented, + * it will simply return {@code null}. If the instrumented library does not expose {@link + * InetSocketAddress} in its API, you might want to implement this method instead of {@link + * #getNetworkLocalInetSocketAddress(Object, Object)}. + */ + @Nullable + default Integer getNetworkLocalPort(REQUEST request, @Nullable RESPONSE response) { + return InetSocketAddressUtil.getPort(getNetworkLocalInetSocketAddress(request, response)); + } + + /** + * Returns an {@link InetSocketAddress} object representing the peer socket address. + * + *

Implementing this method is equivalent to implementing both {@link + * #getNetworkPeerAddress(Object, Object)} and {@link #getNetworkPeerPort(Object, Object)}. + */ + @Nullable + default InetSocketAddress getNetworkPeerInetSocketAddress( + REQUEST request, @Nullable RESPONSE response) { + return null; + } + + /** + * Returns the peer address of the network connection - IP address or Unix domain socket name. + * + *

Examples: {@code 10.1.2.80}, {@code /tmp/my.sock} + * + *

By default, this method attempts to retrieve the peer address using the {@link + * #getNetworkPeerInetSocketAddress(Object, Object)} method. If that method is not implemented, it + * will simply return {@code null}. If the instrumented library does not expose {@link + * InetSocketAddress} in its API, you might want to implement this method instead of {@link + * #getNetworkPeerInetSocketAddress(Object, Object)}. + */ + @Nullable + default String getNetworkPeerAddress(REQUEST request, @Nullable RESPONSE response) { + return InetSocketAddressUtil.getIpAddress(getNetworkPeerInetSocketAddress(request, response)); + } + + /** + * Returns the peer port number of the network connection. + * + *

Examples: {@code 65123} + * + *

By default, this method attempts to retrieve the peer port using the {@link + * #getNetworkPeerInetSocketAddress(Object, Object)} method. If that method is not implemented, it + * will simply return {@code null}. If the instrumented library does not expose {@link + * InetSocketAddress} in its API, you might want to implement this method instead of {@link + * #getNetworkPeerInetSocketAddress(Object, Object)}. + */ + @Nullable + default Integer getNetworkPeerPort(REQUEST request, @Nullable RESPONSE response) { + return InetSocketAddressUtil.getPort(getNetworkPeerInetSocketAddress(request, response)); + } +} diff --git a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/network/ServerAttributesExtractor.java b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/network/ServerAttributesExtractor.java new file mode 100644 index 000000000000..0f0c2ec4dc04 --- /dev/null +++ b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/network/ServerAttributesExtractor.java @@ -0,0 +1,57 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.semconv.network; + +import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.InstrumenterBuilder; +import io.opentelemetry.instrumentation.api.semconv.network.internal.AddressAndPortExtractor; +import io.opentelemetry.instrumentation.api.semconv.network.internal.InternalServerAttributesExtractor; +import io.opentelemetry.instrumentation.api.semconv.network.internal.ServerAddressAndPortExtractor; +import javax.annotation.Nullable; + +/** + * Extractor of server + * attributes. + * + * @since 2.0.0 + */ +public final class ServerAttributesExtractor + implements AttributesExtractor { + + /** + * Creates the server attributes extractor. + * + * @see InstrumenterBuilder#addAttributesExtractor(AttributesExtractor) + */ + public static ServerAttributesExtractor create( + ServerAttributesGetter getter) { + return new ServerAttributesExtractor<>(getter); + } + + private final InternalServerAttributesExtractor internalExtractor; + + ServerAttributesExtractor(ServerAttributesGetter getter) { + internalExtractor = + new InternalServerAttributesExtractor<>( + new ServerAddressAndPortExtractor<>(getter, AddressAndPortExtractor.noop())); + } + + @Override + public void onStart(AttributesBuilder attributes, Context parentContext, REQUEST request) { + internalExtractor.onStart(attributes, request); + } + + @Override + public void onEnd( + AttributesBuilder attributes, + Context context, + REQUEST request, + @Nullable RESPONSE response, + @Nullable Throwable error) {} +} diff --git a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/network/ServerAttributesGetter.java b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/network/ServerAttributesGetter.java new file mode 100644 index 000000000000..a864a51feab2 --- /dev/null +++ b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/network/ServerAttributesGetter.java @@ -0,0 +1,41 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.semconv.network; + +import javax.annotation.Nullable; + +/** + * An interface for getting attributes describing a network server. + * + *

Instrumentation authors will create implementations of this interface for their specific + * library/framework. It will be used by the {@link ServerAttributesExtractor} (or other convention + * specific extractors) to obtain the various server attributes in a type-generic way. + * + * @since 2.0.0 + */ +public interface ServerAttributesGetter { + + /** + * Returns the server domain name if available without reverse DNS lookup; otherwise, IP address + * or Unix domain socket name. + * + *

Examples: {@code client.example.com}, {@code 10.1.2.80}, {@code /tmp/my.sock} + */ + @Nullable + default String getServerAddress(REQUEST request) { + return null; + } + + /** + * Return the server port number. + * + *

Examples: {@code 80}, {@code 8080}, {@code 443} + */ + @Nullable + default Integer getServerPort(REQUEST request) { + return null; + } +} diff --git a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/network/internal/AddressAndPort.java b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/network/internal/AddressAndPort.java new file mode 100644 index 000000000000..ffd9b80774ab --- /dev/null +++ b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/network/internal/AddressAndPort.java @@ -0,0 +1,38 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.semconv.network.internal; + +import javax.annotation.Nullable; + +/** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time. + */ +public final class AddressAndPort implements AddressAndPortExtractor.AddressPortSink { + + @Nullable String address; + @Nullable Integer port; + + @Override + public void setAddress(String address) { + this.address = address; + } + + @Override + public void setPort(Integer port) { + this.port = port; + } + + @Nullable + public String getAddress() { + return address; + } + + @Nullable + public Integer getPort() { + return port; + } +} diff --git a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/network/internal/AddressAndPortExtractor.java b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/network/internal/AddressAndPortExtractor.java new file mode 100644 index 000000000000..ea6671dd483d --- /dev/null +++ b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/network/internal/AddressAndPortExtractor.java @@ -0,0 +1,36 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.semconv.network.internal; + +/** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time. + */ +public interface AddressAndPortExtractor { + + default AddressAndPort extract(REQUEST request) { + AddressAndPort addressAndPort = new AddressAndPort(); + extract(addressAndPort, request); + return addressAndPort; + } + + void extract(AddressPortSink sink, REQUEST request); + + static AddressAndPortExtractor noop() { + return (sink, request) -> {}; + } + + /** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time. + */ + interface AddressPortSink { + + void setAddress(String address); + + void setPort(Integer port); + } +} diff --git a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/network/internal/ClientAddressAndPortExtractor.java b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/network/internal/ClientAddressAndPortExtractor.java new file mode 100644 index 000000000000..7085dd7fc1a0 --- /dev/null +++ b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/network/internal/ClientAddressAndPortExtractor.java @@ -0,0 +1,38 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.semconv.network.internal; + +import io.opentelemetry.instrumentation.api.semconv.network.ClientAttributesGetter; + +/** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time. + */ +public final class ClientAddressAndPortExtractor + implements AddressAndPortExtractor { + + private final ClientAttributesGetter getter; + private final AddressAndPortExtractor fallbackAddressAndPortExtractor; + + public ClientAddressAndPortExtractor( + ClientAttributesGetter getter, + AddressAndPortExtractor fallbackAddressAndPortExtractor) { + this.getter = getter; + this.fallbackAddressAndPortExtractor = fallbackAddressAndPortExtractor; + } + + @Override + public void extract(AddressPortSink sink, REQUEST request) { + String address = getter.getClientAddress(request); + Integer port = getter.getClientPort(request); + if (address == null && port == null) { + fallbackAddressAndPortExtractor.extract(sink, request); + } else { + sink.setAddress(address); + sink.setPort(port); + } + } +} diff --git a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/net/internal/InetSocketAddressUtil.java b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/network/internal/InetSocketAddressUtil.java similarity index 63% rename from instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/net/internal/InetSocketAddressUtil.java rename to instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/network/internal/InetSocketAddressUtil.java index 575eed42ea93..1eb80fbf4fab 100644 --- a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/net/internal/InetSocketAddressUtil.java +++ b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/network/internal/InetSocketAddressUtil.java @@ -3,8 +3,9 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.instrumentation.api.instrumenter.net.internal; +package io.opentelemetry.instrumentation.api.semconv.network.internal; +import java.net.Inet4Address; import java.net.Inet6Address; import java.net.InetAddress; import java.net.InetSocketAddress; @@ -16,22 +17,6 @@ */ public final class InetSocketAddressUtil { - @Nullable - public static String getSockFamily( - @Nullable InetSocketAddress address, @Nullable InetSocketAddress otherAddress) { - if (address == null) { - address = otherAddress; - } - if (address == null) { - return null; - } - InetAddress remoteAddress = address.getAddress(); - if (remoteAddress instanceof Inet6Address) { - return "inet6"; - } - return null; - } - @Nullable public static String getNetworkType( @Nullable InetSocketAddress address, @Nullable InetSocketAddress otherAddress) { @@ -42,15 +27,12 @@ public static String getNetworkType( return null; } InetAddress remoteAddress = address.getAddress(); - return remoteAddress instanceof Inet6Address ? "ipv6" : "ipv4"; - } - - @Nullable - public static String getDomainName(@Nullable InetSocketAddress address) { - if (address == null) { - return null; + if (remoteAddress instanceof Inet4Address) { + return "ipv4"; + } else if (remoteAddress instanceof Inet6Address) { + return "ipv6"; } - return address.getHostString(); + return null; } @Nullable diff --git a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/network/internal/InternalClientAttributesExtractor.java b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/network/internal/InternalClientAttributesExtractor.java new file mode 100644 index 000000000000..cdb095e38344 --- /dev/null +++ b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/network/internal/InternalClientAttributesExtractor.java @@ -0,0 +1,38 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.semconv.network.internal; + +import static io.opentelemetry.instrumentation.api.internal.AttributesExtractorUtil.internalSet; + +import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.semconv.ClientAttributes; + +/** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time. + */ +public final class InternalClientAttributesExtractor { + + private final AddressAndPortExtractor addressAndPortExtractor; + private final boolean capturePort; + + public InternalClientAttributesExtractor( + AddressAndPortExtractor addressAndPortExtractor, boolean capturePort) { + this.addressAndPortExtractor = addressAndPortExtractor; + this.capturePort = capturePort; + } + + public void onStart(AttributesBuilder attributes, REQUEST request) { + AddressAndPort clientAddressAndPort = addressAndPortExtractor.extract(request); + + if (clientAddressAndPort.address != null) { + internalSet(attributes, ClientAttributes.CLIENT_ADDRESS, clientAddressAndPort.address); + if (capturePort && clientAddressAndPort.port != null && clientAddressAndPort.port > 0) { + internalSet(attributes, ClientAttributes.CLIENT_PORT, (long) clientAddressAndPort.port); + } + } + } +} diff --git a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/network/internal/InternalNetworkAttributesExtractor.java b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/network/internal/InternalNetworkAttributesExtractor.java similarity index 50% rename from instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/network/internal/InternalNetworkAttributesExtractor.java rename to instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/network/internal/InternalNetworkAttributesExtractor.java index 957f0b75ece2..7d0cb9eea012 100644 --- a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/network/internal/InternalNetworkAttributesExtractor.java +++ b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/network/internal/InternalNetworkAttributesExtractor.java @@ -3,13 +3,13 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.instrumentation.api.instrumenter.network.internal; +package io.opentelemetry.instrumentation.api.semconv.network.internal; import static io.opentelemetry.instrumentation.api.internal.AttributesExtractorUtil.internalSet; import io.opentelemetry.api.common.AttributesBuilder; -import io.opentelemetry.instrumentation.api.instrumenter.net.internal.NetAttributes; -import io.opentelemetry.instrumentation.api.instrumenter.network.NetworkAttributesGetter; +import io.opentelemetry.instrumentation.api.semconv.network.NetworkAttributesGetter; +import io.opentelemetry.semconv.NetworkAttributes; import java.util.Locale; import javax.annotation.Nullable; @@ -20,31 +20,25 @@ public final class InternalNetworkAttributesExtractor { private final NetworkAttributesGetter getter; - private final NetworkTransportFilter networkTransportFilter; - private final boolean emitStableUrlAttributes; - private final boolean emitOldHttpAttributes; + private final boolean captureProtocolAttributes; + private final boolean captureLocalSocketAttributes; public InternalNetworkAttributesExtractor( NetworkAttributesGetter getter, - NetworkTransportFilter networkTransportFilter, - boolean emitStableUrlAttributes, - boolean emitOldHttpAttributes) { + boolean captureProtocolAttributes, + boolean captureLocalSocketAttributes) { this.getter = getter; - this.networkTransportFilter = networkTransportFilter; - this.emitStableUrlAttributes = emitStableUrlAttributes; - this.emitOldHttpAttributes = emitOldHttpAttributes; + this.captureProtocolAttributes = captureProtocolAttributes; + this.captureLocalSocketAttributes = captureLocalSocketAttributes; } public void onEnd(AttributesBuilder attributes, REQUEST request, @Nullable RESPONSE response) { String protocolName = lowercase(getter.getNetworkProtocolName(request, response)); String protocolVersion = lowercase(getter.getNetworkProtocolVersion(request, response)); - if (emitStableUrlAttributes) { + if (captureProtocolAttributes) { String transport = lowercase(getter.getNetworkTransport(request, response)); - if (networkTransportFilter.shouldAddNetworkTransport( - protocolName, protocolVersion, transport)) { - internalSet(attributes, NetworkAttributes.NETWORK_TRANSPORT, transport); - } + internalSet(attributes, NetworkAttributes.NETWORK_TRANSPORT, transport); internalSet( attributes, NetworkAttributes.NETWORK_TYPE, @@ -52,11 +46,27 @@ public void onEnd(AttributesBuilder attributes, REQUEST request, @Nullable RESPO internalSet(attributes, NetworkAttributes.NETWORK_PROTOCOL_NAME, protocolName); internalSet(attributes, NetworkAttributes.NETWORK_PROTOCOL_VERSION, protocolVersion); } - if (emitOldHttpAttributes) { - // net.transport and net.sock.family are not 1:1 convertible with network.transport and - // network.type; they must be handled separately in the old net.* extractors - internalSet(attributes, NetAttributes.NET_PROTOCOL_NAME, protocolName); - internalSet(attributes, NetAttributes.NET_PROTOCOL_VERSION, protocolVersion); + + if (captureLocalSocketAttributes) { + String localAddress = getter.getNetworkLocalAddress(request, response); + if (localAddress != null) { + internalSet(attributes, NetworkAttributes.NETWORK_LOCAL_ADDRESS, localAddress); + + Integer localPort = getter.getNetworkLocalPort(request, response); + if (localPort != null && localPort > 0) { + internalSet(attributes, NetworkAttributes.NETWORK_LOCAL_PORT, (long) localPort); + } + } + } + + String peerAddress = getter.getNetworkPeerAddress(request, response); + if (peerAddress != null) { + internalSet(attributes, NetworkAttributes.NETWORK_PEER_ADDRESS, peerAddress); + + Integer peerPort = getter.getNetworkPeerPort(request, response); + if (peerPort != null && peerPort > 0) { + internalSet(attributes, NetworkAttributes.NETWORK_PEER_PORT, (long) peerPort); + } } } diff --git a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/network/internal/InternalServerAttributesExtractor.java b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/network/internal/InternalServerAttributesExtractor.java new file mode 100644 index 000000000000..1c23d093143e --- /dev/null +++ b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/network/internal/InternalServerAttributesExtractor.java @@ -0,0 +1,37 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.semconv.network.internal; + +import static io.opentelemetry.instrumentation.api.internal.AttributesExtractorUtil.internalSet; + +import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.semconv.ServerAttributes; + +/** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time. + */ +public final class InternalServerAttributesExtractor { + + private final AddressAndPortExtractor addressAndPortExtractor; + + public InternalServerAttributesExtractor( + AddressAndPortExtractor addressAndPortExtractor) { + this.addressAndPortExtractor = addressAndPortExtractor; + } + + public void onStart(AttributesBuilder attributes, REQUEST request) { + AddressAndPort serverAddressAndPort = addressAndPortExtractor.extract(request); + + if (serverAddressAndPort.address != null) { + internalSet(attributes, ServerAttributes.SERVER_ADDRESS, serverAddressAndPort.address); + + if (serverAddressAndPort.port != null && serverAddressAndPort.port > 0) { + internalSet(attributes, ServerAttributes.SERVER_PORT, (long) serverAddressAndPort.port); + } + } + } +} diff --git a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/network/internal/ServerAddressAndPortExtractor.java b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/network/internal/ServerAddressAndPortExtractor.java new file mode 100644 index 000000000000..3ce90ebe29c9 --- /dev/null +++ b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/network/internal/ServerAddressAndPortExtractor.java @@ -0,0 +1,38 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.semconv.network.internal; + +import io.opentelemetry.instrumentation.api.semconv.network.ServerAttributesGetter; + +/** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time. + */ +public final class ServerAddressAndPortExtractor + implements AddressAndPortExtractor { + + private final ServerAttributesGetter getter; + private final AddressAndPortExtractor fallbackAddressAndPortExtractor; + + public ServerAddressAndPortExtractor( + ServerAttributesGetter getter, + AddressAndPortExtractor fallbackAddressAndPortExtractor) { + this.getter = getter; + this.fallbackAddressAndPortExtractor = fallbackAddressAndPortExtractor; + } + + @Override + public void extract(AddressPortSink sink, REQUEST request) { + String address = getter.getServerAddress(request); + Integer port = getter.getServerPort(request); + if (address == null && port == null) { + fallbackAddressAndPortExtractor.extract(sink, request); + } else { + sink.setAddress(address); + sink.setPort(port); + } + } +} diff --git a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/network/package-info.java b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/network/package-info.java new file mode 100644 index 000000000000..e8e743b459e3 --- /dev/null +++ b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/network/package-info.java @@ -0,0 +1,9 @@ +/** + * This module contains the implementation of the + * OpenTelemetry server, client and shared network attributes semantic conventions. + */ +@ParametersAreNonnullByDefault +package io.opentelemetry.instrumentation.api.semconv.network; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/package-info.java b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/package-info.java new file mode 100644 index 000000000000..8552f58f48a5 --- /dev/null +++ b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/package-info.java @@ -0,0 +1,4 @@ +@ParametersAreNonnullByDefault +package io.opentelemetry.instrumentation.api.semconv; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/url/UrlAttributesExtractor.java b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/url/UrlAttributesExtractor.java similarity index 69% rename from instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/url/UrlAttributesExtractor.java rename to instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/url/UrlAttributesExtractor.java index e8d6d0249826..58b409c29906 100644 --- a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/url/UrlAttributesExtractor.java +++ b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/url/UrlAttributesExtractor.java @@ -3,25 +3,29 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.instrumentation.api.instrumenter.url; +package io.opentelemetry.instrumentation.api.semconv.url; import io.opentelemetry.api.common.AttributesBuilder; import io.opentelemetry.context.Context; import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.url.internal.InternalUrlAttributesExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.InstrumenterBuilder; +import io.opentelemetry.instrumentation.api.semconv.url.internal.InternalUrlAttributesExtractor; import javax.annotation.Nullable; /** * Extractor of URL + * href="https://github.com/open-telemetry/semantic-conventions/blob/v1.23.0/docs/url/url.md">URL * attributes. + * + * @since 2.0.0 */ public final class UrlAttributesExtractor implements AttributesExtractor { /** - * Returns a new {@link UrlAttributesExtractor} that will use the passed {@link - * UrlAttributesGetter}. + * Creates the URL attributes extractor. + * + * @see InstrumenterBuilder#addAttributesExtractor(AttributesExtractor) */ public static UrlAttributesExtractor create( UrlAttributesGetter getter) { @@ -34,10 +38,7 @@ public static UrlAttributesExtractor crea // the UrlAttributesExtractor will always emit new semconv internalExtractor = new InternalUrlAttributesExtractor<>( - getter, - /* alternateSchemeProvider= */ request -> null, - /* emitStableUrlAttributes= */ true, - /* emitOldHttpAttributes= */ false); + getter, /* alternateSchemeProvider= */ request -> null); } @Override diff --git a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/url/UrlAttributesGetter.java b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/url/UrlAttributesGetter.java similarity index 94% rename from instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/url/UrlAttributesGetter.java rename to instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/url/UrlAttributesGetter.java index 2053811e182d..3d04836bebd2 100644 --- a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/url/UrlAttributesGetter.java +++ b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/url/UrlAttributesGetter.java @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.instrumentation.api.instrumenter.url; +package io.opentelemetry.instrumentation.api.semconv.url; import javax.annotation.Nullable; @@ -13,6 +13,8 @@ *

Instrumentation authors will create implementations of this interface for their specific * library/framework. It will be used by the {@link UrlAttributesExtractor} (or other convention * specific extractors) to obtain the various URL attributes in a type-generic way. + * + * @since 2.0.0 */ public interface UrlAttributesGetter { diff --git a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/url/internal/InternalUrlAttributesExtractor.java b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/url/internal/InternalUrlAttributesExtractor.java new file mode 100644 index 000000000000..37103b0c6983 --- /dev/null +++ b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/url/internal/InternalUrlAttributesExtractor.java @@ -0,0 +1,47 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.semconv.url.internal; + +import static io.opentelemetry.instrumentation.api.internal.AttributesExtractorUtil.internalSet; + +import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.instrumentation.api.semconv.url.UrlAttributesGetter; +import io.opentelemetry.semconv.UrlAttributes; +import java.util.function.Function; + +/** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time. + */ +public final class InternalUrlAttributesExtractor { + + private final UrlAttributesGetter getter; + private final Function alternateSchemeProvider; + + public InternalUrlAttributesExtractor( + UrlAttributesGetter getter, Function alternateSchemeProvider) { + this.getter = getter; + this.alternateSchemeProvider = alternateSchemeProvider; + } + + public void onStart(AttributesBuilder attributes, REQUEST request) { + String urlScheme = getUrlScheme(request); + String urlPath = getter.getUrlPath(request); + String urlQuery = getter.getUrlQuery(request); + + internalSet(attributes, UrlAttributes.URL_SCHEME, urlScheme); + internalSet(attributes, UrlAttributes.URL_PATH, urlPath); + internalSet(attributes, UrlAttributes.URL_QUERY, urlQuery); + } + + private String getUrlScheme(REQUEST request) { + String urlScheme = alternateSchemeProvider.apply(request); + if (urlScheme == null) { + urlScheme = getter.getUrlScheme(request); + } + return urlScheme; + } +} diff --git a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/url/package-info.java b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/url/package-info.java new file mode 100644 index 000000000000..8239d34a4bfa --- /dev/null +++ b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/url/package-info.java @@ -0,0 +1,9 @@ +/** + * This module contains the implementation of the + * OpenTelemetry URL semantic conventions. + */ +@ParametersAreNonnullByDefault +package io.opentelemetry.instrumentation.api.semconv.url; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/InstrumenterTest.java b/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/InstrumenterTest.java index 3774c19a9fd2..fb9ae8622d8d 100644 --- a/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/InstrumenterTest.java +++ b/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/InstrumenterTest.java @@ -25,6 +25,7 @@ import io.opentelemetry.context.Context; import io.opentelemetry.context.ContextKey; import io.opentelemetry.context.propagation.TextMapGetter; +import io.opentelemetry.instrumentation.api.internal.SchemaUrlProvider; import io.opentelemetry.instrumentation.api.internal.SpanKey; import io.opentelemetry.instrumentation.api.internal.SpanKeyProvider; import io.opentelemetry.sdk.common.InstrumentationScopeInfo; @@ -113,6 +114,30 @@ public void onEnd( } } + static class AttributesExtractorWithSchemaUrl + implements AttributesExtractor, Map>, SchemaUrlProvider { + + @Override + public void onStart( + AttributesBuilder attributes, Context parentContext, Map request) { + attributes.put("key", "value"); + } + + @Override + public void onEnd( + AttributesBuilder attributes, + Context context, + Map request, + @Nullable Map response, + @Nullable Throwable error) {} + + @Nullable + @Override + public String internalGetSchemaUrl() { + return "schemaUrl from extractor"; + } + } + static class LinksExtractor implements SpanLinksExtractor> { @Override @@ -583,11 +608,60 @@ void instrumentationVersion_custom() { } @Test - void schemaUrl() { + void schemaUrl_setExplicitly() { + Instrumenter, Map> instrumenter = + Instrumenter., Map>builder( + otelTesting.getOpenTelemetry(), "test", name -> "span") + .setSchemaUrl("https://opentelemetry.io/schemas/1.0.0") + .buildInstrumenter(); + + Context context = instrumenter.start(Context.root(), emptyMap()); + assertThat(Span.fromContext(context)).isNotNull(); + + instrumenter.end(context, emptyMap(), emptyMap(), null); + + InstrumentationScopeInfo expectedLibraryInfo = + InstrumentationScopeInfo.builder("test") + .setSchemaUrl("https://opentelemetry.io/schemas/1.0.0") + .build(); + otelTesting + .assertTraces() + .hasTracesSatisfyingExactly( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("span").hasInstrumentationScopeInfo(expectedLibraryInfo))); + } + + @Test + void schemaUrl_computedFromExtractors() { + Instrumenter, Map> instrumenter = + Instrumenter., Map>builder( + otelTesting.getOpenTelemetry(), "test", name -> "span") + .addAttributesExtractor(new AttributesExtractorWithSchemaUrl()) + .buildInstrumenter(); + + Context context = instrumenter.start(Context.root(), emptyMap()); + assertThat(Span.fromContext(context)).isNotNull(); + + instrumenter.end(context, emptyMap(), emptyMap(), null); + + InstrumentationScopeInfo expectedLibraryInfo = + InstrumentationScopeInfo.builder("test").setSchemaUrl("schemaUrl from extractor").build(); + otelTesting + .assertTraces() + .hasTracesSatisfyingExactly( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("span").hasInstrumentationScopeInfo(expectedLibraryInfo))); + } + + @Test + void schemaUrl_schemaSetExplicitlyOverridesSchemaComputedFromExtractors() { Instrumenter, Map> instrumenter = Instrumenter., Map>builder( otelTesting.getOpenTelemetry(), "test", name -> "span") .setSchemaUrl("https://opentelemetry.io/schemas/1.0.0") + .addAttributesExtractor(new AttributesExtractorWithSchemaUrl()) .buildInstrumenter(); Context context = instrumenter.start(Context.root(), emptyMap()); diff --git a/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/SpanSuppressionStrategyTest.java b/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/SpanSuppressionStrategyTest.java index 1593c4efde82..82e29fdbdaa6 100644 --- a/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/SpanSuppressionStrategyTest.java +++ b/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/SpanSuppressionStrategyTest.java @@ -13,6 +13,7 @@ import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertTrue; +import io.opentelemetry.api.internal.InstrumentationUtil; import io.opentelemetry.api.trace.Span; import io.opentelemetry.api.trace.SpanKind; import io.opentelemetry.context.Context; @@ -157,4 +158,24 @@ void semconv_shouldNotSuppressContextWithPartiallyDifferentSpanKeys() { assertFalse(suppressor.shouldSuppress(context, SpanKind.SERVER)); } + + @Test + void context_shouldSuppressWhenKeyIsAvailableAndTrue() { + InstrumentationUtil.suppressInstrumentation( + () -> { + SpanSuppressor suppressor = + new SpanSuppressors.ByContextKey(SpanSuppressionStrategy.NONE.create(emptySet())); + + assertTrue(suppressor.shouldSuppress(Context.current(), SpanKind.CLIENT)); + }); + } + + @Test + void context_shouldNotSuppressWhenKeyIsNotAvailable() { + Context context = Context.current(); + SpanSuppressor suppressor = + new SpanSuppressors.ByContextKey(SpanSuppressionStrategy.NONE.create(emptySet())); + + assertFalse(suppressor.shouldSuppress(context, SpanKind.CLIENT)); + } } diff --git a/instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/internal/ClassNamesTest.java b/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/internal/ClassNamesTest.java similarity index 97% rename from instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/internal/ClassNamesTest.java rename to instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/internal/ClassNamesTest.java index 20f0ae8df487..93d1851d8bca 100644 --- a/instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/internal/ClassNamesTest.java +++ b/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/internal/ClassNamesTest.java @@ -30,7 +30,7 @@ public void run() {} @Test void testLambda() { Runnable x = () -> {}; - assertThat(ClassNames.simpleName(x.getClass())).startsWith("ClassNamesTest$$Lambda$"); + assertThat(ClassNames.simpleName(x.getClass())).startsWith("ClassNamesTest$$Lambda"); } static class Outer { diff --git a/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/internal/OperationMetricsUtilTest.java b/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/internal/OperationMetricsUtilTest.java new file mode 100644 index 000000000000..c7a8d0d54a2b --- /dev/null +++ b/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/internal/OperationMetricsUtilTest.java @@ -0,0 +1,79 @@ +/* + * 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.metrics.DoubleHistogramBuilder; +import io.opentelemetry.api.metrics.Meter; +import io.opentelemetry.api.metrics.MeterProvider; +import io.opentelemetry.instrumentation.api.instrumenter.OperationMetrics; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.LibraryInstrumentationExtension; +import java.lang.reflect.Proxy; +import java.util.concurrent.atomic.AtomicBoolean; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +class OperationMetricsUtilTest { + @RegisterExtension + static final InstrumentationExtension testing = LibraryInstrumentationExtension.create(); + + @Test + void noWarning() { + AtomicBoolean warning = new AtomicBoolean(false); + OperationMetrics operationMetrics = + OperationMetricsUtil.create( + "test metrics", meter -> null, (s, doubleHistogramBuilder) -> warning.set(true)); + operationMetrics.create(testing.getOpenTelemetry().getMeter("test")); + + assertThat(warning).isFalse(); + } + + @Test + void noWarningWithNoopMetrics() { + AtomicBoolean warning = new AtomicBoolean(false); + OperationMetrics operationMetrics = + OperationMetricsUtil.create( + "test metrics", meter -> null, (s, doubleHistogramBuilder) -> warning.set(true)); + operationMetrics.create(MeterProvider.noop().get("test")); + + assertThat(warning).isFalse(); + } + + @Test + void warning() { + AtomicBoolean warning = new AtomicBoolean(false); + OperationMetrics operationMetrics = + OperationMetricsUtil.create( + "test metrics", meter -> null, (s, doubleHistogramBuilder) -> warning.set(true)); + Meter defaultMeter = MeterProvider.noop().get("test"); + Meter meter = + (Meter) + Proxy.newProxyInstance( + Meter.class.getClassLoader(), + new Class[] {Meter.class}, + (proxy, method, args) -> { + if ("histogramBuilder".equals(method.getName())) { + // proxy the histogram builder so that the builder instance does not implement + // ExtendedDoubleHistogramBuilder which will trigger the warning + return proxyDoubleHistogramBuilder(defaultMeter); + } + return method.invoke(defaultMeter, args); + }); + operationMetrics.create(meter); + + assertThat(warning).isTrue(); + } + + private static DoubleHistogramBuilder proxyDoubleHistogramBuilder(Meter meter) { + return (DoubleHistogramBuilder) + Proxy.newProxyInstance( + DoubleHistogramBuilder.class.getClassLoader(), + new Class[] {DoubleHistogramBuilder.class}, + (proxy1, method1, args1) -> meter.histogramBuilder((String) args1[0])); + } +} diff --git a/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/semconv/http/ForwardedHostAddressAndPortExtractorTest.java b/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/semconv/http/ForwardedHostAddressAndPortExtractorTest.java new file mode 100644 index 000000000000..dfd8b20934b0 --- /dev/null +++ b/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/semconv/http/ForwardedHostAddressAndPortExtractorTest.java @@ -0,0 +1,152 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.semconv.http; + +import static java.util.Arrays.asList; +import static java.util.Collections.emptyList; +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.params.provider.Arguments.arguments; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.when; + +import io.opentelemetry.instrumentation.api.semconv.network.internal.AddressAndPort; +import java.util.List; +import java.util.stream.Stream; +import javax.annotation.Nullable; +import org.junit.jupiter.api.extension.ExtendWith; +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.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class ForwardedHostAddressAndPortExtractorTest { + + private static final String REQUEST = "request"; + + @Mock HttpCommonAttributesGetter getter; + + @InjectMocks ForwardedHostAddressAndPortExtractor underTest; + + @ParameterizedTest + @ArgumentsSource(ForwardedArgs.class) + void shouldParseForwarded( + List headers, @Nullable String expectedAddress, @Nullable Integer expectedPort) { + when(getter.getHttpRequestHeader(REQUEST, "forwarded")).thenReturn(headers); + + AddressAndPort sink = new AddressAndPort(); + underTest.extract(sink, REQUEST); + + assertThat(sink.getAddress()).isEqualTo(expectedAddress); + assertThat(sink.getPort()).isEqualTo(expectedPort); + } + + static final class ForwardedArgs implements ArgumentsProvider { + + @Override + public Stream provideArguments(ExtensionContext extensionContext) { + return Stream.of( + // empty/invalid headers + arguments(singletonList(""), null, null), + arguments(singletonList("host="), null, null), + arguments(singletonList("host=;"), null, null), + arguments(singletonList("host=\""), null, null), + arguments(singletonList("host=\"\""), null, null), + arguments(singletonList("host=\"example.com"), null, null), + arguments(singletonList("by=1.2.3.4, test=abc"), null, null), + arguments(singletonList("host=example.com"), "example.com", null), + arguments(singletonList("host=\"example.com\""), "example.com", null), + arguments(singletonList("host=example.com; test=abc:1234"), "example.com", null), + arguments(singletonList("host=\"example.com\"; test=abc:1234"), "example.com", null), + arguments(singletonList("host=example.com:port"), "example.com", null), + arguments(singletonList("host=\"example.com:port\""), "example.com", null), + arguments(singletonList("host=example.com:42"), "example.com", 42), + arguments(singletonList("host=\"example.com:42\""), "example.com", 42), + arguments(singletonList("host=example.com:42; test=abc:1234"), "example.com", 42), + arguments(singletonList("host=\"example.com:42\"; test=abc:1234"), "example.com", 42), + + // multiple headers + arguments( + asList("proto=https", "host=example.com", "host=github.com:1234"), + "example.com", + null)); + } + } + + @ParameterizedTest + @ArgumentsSource(HostArgs.class) + @SuppressWarnings("MockitoDoSetup") + void shouldParseForwardedHost( + List headers, @Nullable String expectedAddress, @Nullable Integer expectedPort) { + doReturn(emptyList()).when(getter).getHttpRequestHeader(REQUEST, "forwarded"); + doReturn(headers).when(getter).getHttpRequestHeader(REQUEST, "x-forwarded-host"); + + AddressAndPort sink = new AddressAndPort(); + underTest.extract(sink, REQUEST); + + assertThat(sink.getAddress()).isEqualTo(expectedAddress); + assertThat(sink.getPort()).isEqualTo(expectedPort); + } + + @ParameterizedTest + @ArgumentsSource(HostArgs.class) + @SuppressWarnings("MockitoDoSetup") + void shouldParsePseudoAuthority( + List headers, @Nullable String expectedAddress, @Nullable Integer expectedPort) { + doReturn(emptyList()).when(getter).getHttpRequestHeader(REQUEST, "forwarded"); + doReturn(emptyList()).when(getter).getHttpRequestHeader(REQUEST, "x-forwarded-host"); + doReturn(headers).when(getter).getHttpRequestHeader(REQUEST, ":authority"); + + AddressAndPort sink = new AddressAndPort(); + underTest.extract(sink, REQUEST); + + assertThat(sink.getAddress()).isEqualTo(expectedAddress); + assertThat(sink.getPort()).isEqualTo(expectedPort); + } + + @ParameterizedTest + @ArgumentsSource(HostArgs.class) + @SuppressWarnings("MockitoDoSetup") + void shouldParseHost( + List headers, @Nullable String expectedAddress, @Nullable Integer expectedPort) { + doReturn(emptyList()).when(getter).getHttpRequestHeader(REQUEST, "forwarded"); + doReturn(emptyList()).when(getter).getHttpRequestHeader(REQUEST, "x-forwarded-host"); + doReturn(emptyList()).when(getter).getHttpRequestHeader(REQUEST, ":authority"); + doReturn(headers).when(getter).getHttpRequestHeader(REQUEST, "host"); + + AddressAndPort sink = new AddressAndPort(); + underTest.extract(sink, REQUEST); + + assertThat(sink.getAddress()).isEqualTo(expectedAddress); + assertThat(sink.getPort()).isEqualTo(expectedPort); + } + + static final class HostArgs implements ArgumentsProvider { + + @Override + public Stream provideArguments(ExtensionContext extensionContext) { + return Stream.of( + // empty/invalid headers + arguments(singletonList(""), null, null), + arguments(singletonList("\""), null, null), + arguments(singletonList("\"\""), null, null), + arguments(singletonList("example.com"), "example.com", null), + arguments(singletonList("example.com:port"), "example.com", null), + arguments(singletonList("example.com:42"), "example.com", 42), + arguments(singletonList("\"example.com\""), "example.com", null), + arguments(singletonList("\"example.com:port\""), "example.com", null), + arguments(singletonList("\"example.com:42\""), "example.com", 42), + + // multiple headers + arguments(asList("example.com", "github.com:1234"), "example.com", null)); + } + } +} diff --git a/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/semconv/http/ForwardedUrlSchemeProviderTest.java b/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/semconv/http/ForwardedUrlSchemeProviderTest.java new file mode 100644 index 000000000000..98197a8f0114 --- /dev/null +++ b/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/semconv/http/ForwardedUrlSchemeProviderTest.java @@ -0,0 +1,95 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.semconv.http; + +import static java.util.Arrays.asList; +import static java.util.Collections.emptyList; +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.params.provider.Arguments.arguments; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.when; + +import java.util.List; +import java.util.stream.Stream; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +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.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class ForwardedUrlSchemeProviderTest { + + private static final String REQUEST = "request"; + + @Mock HttpServerAttributesGetter getter; + + @InjectMocks ForwardedUrlSchemeProvider underTest; + + @Test + void noHeaders() { + when(getter.getHttpRequestHeader(eq(REQUEST), any())).thenReturn(emptyList()); + assertThat(underTest.apply(REQUEST)).isNull(); + } + + @ParameterizedTest + @ArgumentsSource(ForwardedHeaderValues.class) + void parseForwardedHeader(List values, String expectedScheme) { + when(getter.getHttpRequestHeader(REQUEST, "forwarded")).thenReturn(values); + assertThat(underTest.apply(REQUEST)).isEqualTo(expectedScheme); + } + + static final class ForwardedHeaderValues implements ArgumentsProvider { + + @Override + public Stream provideArguments(ExtensionContext extensionContext) { + return Stream.of( + arguments(singletonList("for=1.1.1.1;proto=xyz"), "xyz"), + arguments(singletonList("for=1.1.1.1;proto=xyz;"), "xyz"), + arguments(singletonList("for=1.1.1.1;proto=xyz,"), "xyz"), + arguments(singletonList("for=1.1.1.1;proto="), null), + arguments(singletonList("for=1.1.1.1;proto=;"), null), + arguments(singletonList("for=1.1.1.1;proto=,"), null), + arguments(singletonList("for=1.1.1.1;proto=\"xyz\""), "xyz"), + arguments(singletonList("for=1.1.1.1;proto=\"xyz\";"), "xyz"), + arguments(singletonList("for=1.1.1.1;proto=\"xyz\","), "xyz"), + arguments(singletonList("for=1.1.1.1;proto=\""), null), + arguments(singletonList("for=1.1.1.1;proto=\"\""), null), + arguments(singletonList("for=1.1.1.1;proto=\"\";"), null), + arguments(singletonList("for=1.1.1.1;proto=\"\","), null), + arguments(asList("for=1.1.1.1", "proto=xyz", "proto=abc"), "xyz")); + } + } + + @ParameterizedTest + @ArgumentsSource(ForwardedProtoHeaderValues.class) + @SuppressWarnings("MockitoDoSetup") + void parseForwardedProtoHeader(List values, String expectedScheme) { + doReturn(emptyList()).when(getter).getHttpRequestHeader(REQUEST, "forwarded"); + doReturn(values).when(getter).getHttpRequestHeader(REQUEST, "x-forwarded-proto"); + assertThat(underTest.apply(REQUEST)).isEqualTo(expectedScheme); + } + + static final class ForwardedProtoHeaderValues implements ArgumentsProvider { + + @Override + public Stream provideArguments(ExtensionContext extensionContext) { + return Stream.of( + arguments(singletonList("xyz"), "xyz"), + arguments(singletonList("\"xyz\""), "xyz"), + arguments(singletonList("\""), null), + arguments(asList("xyz", "abc"), "xyz")); + } + } +} diff --git a/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/semconv/http/HostAddressAndPortExtractorTest.java b/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/semconv/http/HostAddressAndPortExtractorTest.java new file mode 100644 index 000000000000..57a3b45f6504 --- /dev/null +++ b/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/semconv/http/HostAddressAndPortExtractorTest.java @@ -0,0 +1,71 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.semconv.http; + +import static java.util.Collections.emptyList; +import static java.util.Collections.singletonList; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +import io.opentelemetry.instrumentation.api.semconv.network.internal.AddressAndPortExtractor; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class HostAddressAndPortExtractorTest { + + private static final String REQUEST = "request"; + + @Mock HttpCommonAttributesGetter getter; + @Mock AddressAndPortExtractor.AddressPortSink sink; + + @InjectMocks HostAddressAndPortExtractor underTest; + + @Test + void noHostHeader() { + when(getter.getHttpRequestHeader(REQUEST, "host")).thenReturn(emptyList()); + + underTest.extract(sink, REQUEST); + + verifyNoInteractions(sink); + } + + @Test + void justHost() { + when(getter.getHttpRequestHeader(REQUEST, "host")).thenReturn(singletonList("host")); + + underTest.extract(sink, REQUEST); + + verify(sink).setAddress("host"); + verifyNoMoreInteractions(sink); + } + + @Test + void portIsNotNumeric() { + when(getter.getHttpRequestHeader(REQUEST, "host")).thenReturn(singletonList("host:port")); + + underTest.extract(sink, REQUEST); + + verify(sink).setAddress("host"); + verifyNoMoreInteractions(sink); + } + + @Test + void hostAndPort() { + when(getter.getHttpRequestHeader(REQUEST, "host")).thenReturn(singletonList("host:42")); + + underTest.extract(sink, REQUEST); + + verify(sink).setAddress("host"); + verify(sink).setPort(42); + verifyNoMoreInteractions(sink); + } +} diff --git a/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/semconv/http/HttpClientAttributesExtractorTest.java b/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/semconv/http/HttpClientAttributesExtractorTest.java new file mode 100644 index 000000000000..280292bb088a --- /dev/null +++ b/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/semconv/http/HttpClientAttributesExtractorTest.java @@ -0,0 +1,426 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.semconv.http; + +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; +import static java.util.Arrays.asList; +import static java.util.Collections.emptyList; +import static java.util.Collections.emptyMap; +import static java.util.Collections.singletonList; +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.context.Context; +import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; +import io.opentelemetry.instrumentation.api.internal.HttpConstants; +import io.opentelemetry.semconv.ErrorAttributes; +import io.opentelemetry.semconv.HttpAttributes; +import io.opentelemetry.semconv.NetworkAttributes; +import io.opentelemetry.semconv.ServerAttributes; +import io.opentelemetry.semconv.UrlAttributes; +import java.net.ConnectException; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.function.ToIntFunction; +import javax.annotation.Nullable; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ArgumentsSource; +import org.junit.jupiter.params.provider.ValueSource; + +class HttpClientAttributesExtractorTest { + + static class TestHttpClientAttributesGetter + implements HttpClientAttributesGetter, Map> { + + @Override + public String getUrlFull(Map request) { + return request.get("urlFull"); + } + + @Override + public String getHttpRequestMethod(Map request) { + return request.get("method"); + } + + @Override + public List getHttpRequestHeader(Map request, String name) { + String value = request.get("header." + name); + return value == null ? emptyList() : asList(value.split(",")); + } + + @Override + public Integer getHttpResponseStatusCode( + Map request, Map response, @Nullable Throwable error) { + String value = response.get("statusCode"); + return value == null ? null : Integer.parseInt(value); + } + + @Override + public List getHttpResponseHeader( + Map request, Map response, String name) { + String value = response.get("header." + name); + return value == null ? emptyList() : asList(value.split(",")); + } + + @Nullable + @Override + public String getNetworkTransport( + Map request, @Nullable Map response) { + return request.get("networkTransport"); + } + + @Nullable + @Override + public String getNetworkType( + Map request, @Nullable Map response) { + return request.get("networkType"); + } + + @Nullable + @Override + public String getNetworkProtocolName( + Map request, @Nullable Map response) { + return request.get("networkProtocolName"); + } + + @Nullable + @Override + public String getNetworkProtocolVersion( + Map request, @Nullable Map response) { + return request.get("networkProtocolVersion"); + } + + @Nullable + @Override + public String getNetworkPeerAddress( + Map request, @Nullable Map response) { + return request.get("networkPeerAddress"); + } + + @Nullable + @Override + public Integer getNetworkPeerPort( + Map request, @Nullable Map response) { + String value = request.get("networkPeerPort"); + return value == null ? null : Integer.parseInt(value); + } + + @Nullable + @Override + public String getServerAddress(Map request) { + return request.get("serverAddress"); + } + + @Nullable + @Override + public Integer getServerPort(Map request) { + String value = request.get("serverPort"); + return value == null ? null : Integer.parseInt(value); + } + + @Nullable + @Override + public String getErrorType( + Map request, + @Nullable Map respobse, + @Nullable Throwable error) { + return request.get("errorType"); + } + } + + @Test + void normal() { + Map request = new HashMap<>(); + request.put("method", "POST"); + request.put("urlFull", "http://github.com"); + request.put("header.content-length", "10"); + request.put("header.user-agent", "okhttp 3.x"); + request.put("header.custom-request-header", "123,456"); + request.put("networkTransport", "udp"); + request.put("networkType", "ipv4"); + request.put("networkProtocolName", "http"); + request.put("networkProtocolVersion", "1.1"); + request.put("networkPeerAddress", "4.3.2.1"); + request.put("networkPeerPort", "456"); + request.put("serverAddress", "github.com"); + request.put("serverPort", "80"); + + Map response = new HashMap<>(); + response.put("statusCode", "202"); + response.put("header.content-length", "20"); + response.put("header.custom-response-header", "654,321"); + + ToIntFunction resendCountFromContext = context -> 2; + + AttributesExtractor, Map> extractor = + HttpClientAttributesExtractor.builder(new TestHttpClientAttributesGetter()) + .setCapturedRequestHeaders(singletonList("Custom-Request-Header")) + .setCapturedResponseHeaders(singletonList("Custom-Response-Header")) + .setResendCountIncrementer(resendCountFromContext) + .build(); + + AttributesBuilder startAttributes = Attributes.builder(); + extractor.onStart(startAttributes, Context.root(), request); + assertThat(startAttributes.build()) + .containsOnly( + entry(HttpAttributes.HTTP_REQUEST_METHOD, "POST"), + entry(UrlAttributes.URL_FULL, "http://github.com"), + entry( + AttributeKey.stringArrayKey("http.request.header.custom-request-header"), + asList("123", "456")), + entry(ServerAttributes.SERVER_ADDRESS, "github.com"), + entry(ServerAttributes.SERVER_PORT, 80L), + entry(HttpAttributes.HTTP_REQUEST_RESEND_COUNT, 2L)); + + AttributesBuilder endAttributes = Attributes.builder(); + extractor.onEnd(endAttributes, Context.root(), request, response, null); + assertThat(endAttributes.build()) + .containsOnly( + entry(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, 202L), + entry( + AttributeKey.stringArrayKey("http.response.header.custom-response-header"), + asList("654", "321")), + entry(NetworkAttributes.NETWORK_PROTOCOL_VERSION, "1.1"), + entry(NetworkAttributes.NETWORK_PEER_ADDRESS, "4.3.2.1"), + entry(NetworkAttributes.NETWORK_PEER_PORT, 456L)); + } + + @ParameterizedTest + @ArgumentsSource(ValidRequestMethodsProvider.class) + void shouldExtractKnownMethods(String requestMethod) { + Map request = new HashMap<>(); + request.put("method", requestMethod); + + AttributesExtractor, Map> extractor = + HttpClientAttributesExtractor.create(new TestHttpClientAttributesGetter()); + + AttributesBuilder attributes = Attributes.builder(); + extractor.onStart(attributes, Context.root(), request); + extractor.onEnd(attributes, Context.root(), request, emptyMap(), null); + + assertThat(attributes.build()) + .containsEntry(HttpAttributes.HTTP_REQUEST_METHOD, requestMethod) + .doesNotContainKey(HttpAttributes.HTTP_REQUEST_METHOD_ORIGINAL); + } + + @ParameterizedTest + @ValueSource(strings = {"get", "Get"}) + void shouldTreatMethodsAsCaseSensitive(String requestMethod) { + Map request = new HashMap<>(); + request.put("method", requestMethod); + + AttributesExtractor, Map> extractor = + HttpClientAttributesExtractor.create(new TestHttpClientAttributesGetter()); + + AttributesBuilder attributes = Attributes.builder(); + extractor.onStart(attributes, Context.root(), request); + extractor.onEnd(attributes, Context.root(), request, emptyMap(), null); + + assertThat(attributes.build()) + .containsEntry(HttpAttributes.HTTP_REQUEST_METHOD, HttpConstants._OTHER) + .containsEntry(HttpAttributes.HTTP_REQUEST_METHOD_ORIGINAL, requestMethod); + } + + @ParameterizedTest + @ValueSource(strings = {"PURGE", "not a method really"}) + void shouldUseOtherForUnknownMethods(String requestMethod) { + Map request = new HashMap<>(); + request.put("method", requestMethod); + + AttributesExtractor, Map> extractor = + HttpClientAttributesExtractor.create(new TestHttpClientAttributesGetter()); + + AttributesBuilder attributes = Attributes.builder(); + extractor.onStart(attributes, Context.root(), request); + extractor.onEnd(attributes, Context.root(), request, emptyMap(), null); + + assertThat(attributes.build()) + .containsEntry(HttpAttributes.HTTP_REQUEST_METHOD, HttpConstants._OTHER) + .containsEntry(HttpAttributes.HTTP_REQUEST_METHOD_ORIGINAL, requestMethod); + } + + @ParameterizedTest + @ValueSource(strings = {"only", "custom", "methods", "allowed"}) + void shouldExtractKnownMethods_override(String requestMethod) { + Map request = new HashMap<>(); + request.put("method", requestMethod); + + AttributesExtractor, Map> extractor = + HttpClientAttributesExtractor.builder(new TestHttpClientAttributesGetter()) + .setKnownMethods(new HashSet<>(asList("only", "custom", "methods", "allowed"))) + .build(); + + AttributesBuilder attributes = Attributes.builder(); + extractor.onStart(attributes, Context.root(), request); + extractor.onEnd(attributes, Context.root(), request, emptyMap(), null); + + assertThat(attributes.build()) + .containsEntry(HttpAttributes.HTTP_REQUEST_METHOD, requestMethod) + .doesNotContainKey(HttpAttributes.HTTP_REQUEST_METHOD_ORIGINAL); + } + + @ParameterizedTest + @ArgumentsSource(ValidRequestMethodsProvider.class) + void shouldUseOtherForUnknownMethods_override(String requestMethod) { + Map request = new HashMap<>(); + request.put("method", requestMethod); + + AttributesExtractor, Map> extractor = + HttpClientAttributesExtractor.builder(new TestHttpClientAttributesGetter()) + .setKnownMethods(new HashSet<>(asList("only", "custom", "methods", "allowed"))) + .build(); + + AttributesBuilder attributes = Attributes.builder(); + extractor.onStart(attributes, Context.root(), request); + extractor.onEnd(attributes, Context.root(), request, emptyMap(), null); + + assertThat(attributes.build()) + .containsEntry(HttpAttributes.HTTP_REQUEST_METHOD, HttpConstants._OTHER) + .containsEntry(HttpAttributes.HTTP_REQUEST_METHOD_ORIGINAL, requestMethod); + } + + @Test + void shouldExtractErrorType_httpStatusCode() { + Map response = new HashMap<>(); + response.put("statusCode", "400"); + + AttributesExtractor, Map> extractor = + HttpClientAttributesExtractor.create(new TestHttpClientAttributesGetter()); + + AttributesBuilder attributes = Attributes.builder(); + extractor.onStart(attributes, Context.root(), emptyMap()); + extractor.onEnd(attributes, Context.root(), emptyMap(), response, null); + + assertThat(attributes.build()) + .containsEntry(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, 400) + .containsEntry(ErrorAttributes.ERROR_TYPE, "400"); + } + + @Test + void shouldExtractErrorType_getter() { + Map request = new HashMap<>(); + request.put("statusCode", "0"); + request.put("errorType", "custom error type"); + + AttributesExtractor, Map> extractor = + HttpClientAttributesExtractor.create(new TestHttpClientAttributesGetter()); + + AttributesBuilder attributes = Attributes.builder(); + extractor.onStart(attributes, Context.root(), emptyMap()); + extractor.onEnd(attributes, Context.root(), request, emptyMap(), null); + + assertThat(attributes.build()).containsEntry(ErrorAttributes.ERROR_TYPE, "custom error type"); + } + + @Test + void shouldExtractErrorType_exceptionClassName() { + AttributesExtractor, Map> extractor = + HttpClientAttributesExtractor.create(new TestHttpClientAttributesGetter()); + + AttributesBuilder attributes = Attributes.builder(); + extractor.onStart(attributes, Context.root(), emptyMap()); + extractor.onEnd(attributes, Context.root(), emptyMap(), emptyMap(), new ConnectException()); + + assertThat(attributes.build()) + .containsEntry(ErrorAttributes.ERROR_TYPE, "java.net.ConnectException"); + } + + @Test + void shouldExtractErrorType_other() { + AttributesExtractor, Map> extractor = + HttpClientAttributesExtractor.create(new TestHttpClientAttributesGetter()); + + AttributesBuilder attributes = Attributes.builder(); + extractor.onStart(attributes, Context.root(), emptyMap()); + extractor.onEnd(attributes, Context.root(), emptyMap(), emptyMap(), null); + + assertThat(attributes.build()).containsEntry(ErrorAttributes.ERROR_TYPE, HttpConstants._OTHER); + } + + @Test + void shouldExtractServerAddressAndPortFromHostHeader() { + Map request = new HashMap<>(); + request.put("header.host", "github.com:123"); + + Map response = new HashMap<>(); + response.put("statusCode", "200"); + + AttributesExtractor, Map> extractor = + HttpClientAttributesExtractor.create(new TestHttpClientAttributesGetter()); + + AttributesBuilder startAttributes = Attributes.builder(); + extractor.onStart(startAttributes, Context.root(), request); + assertThat(startAttributes.build()) + .containsOnly( + entry(ServerAttributes.SERVER_ADDRESS, "github.com"), + entry(ServerAttributes.SERVER_PORT, 123L)); + + AttributesBuilder endAttributes = Attributes.builder(); + extractor.onEnd(endAttributes, Context.root(), request, response, null); + assertThat(endAttributes.build()) + .containsOnly(entry(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, 200L)); + } + + @Test + void shouldExtractPeerAddressEvenIfItDuplicatesServerAddress() { + Map request = new HashMap<>(); + request.put("networkPeerAddress", "1.2.3.4"); + request.put("networkPeerPort", "456"); + request.put("serverAddress", "1.2.3.4"); + request.put("serverPort", "123"); + + Map response = new HashMap<>(); + response.put("statusCode", "200"); + + AttributesExtractor, Map> extractor = + HttpClientAttributesExtractor.create(new TestHttpClientAttributesGetter()); + + AttributesBuilder startAttributes = Attributes.builder(); + extractor.onStart(startAttributes, Context.root(), request); + assertThat(startAttributes.build()) + .containsOnly( + entry(ServerAttributes.SERVER_ADDRESS, "1.2.3.4"), + entry(ServerAttributes.SERVER_PORT, 123L)); + + AttributesBuilder endAttributes = Attributes.builder(); + extractor.onEnd(endAttributes, Context.root(), request, response, null); + assertThat(endAttributes.build()) + .containsOnly( + entry(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, 200L), + entry(NetworkAttributes.NETWORK_PEER_ADDRESS, "1.2.3.4"), + entry(NetworkAttributes.NETWORK_PEER_PORT, 456L)); + } + + @Test + void shouldExtractProtocolNameDifferentFromHttp() { + Map request = new HashMap<>(); + request.put("networkProtocolName", "spdy"); + request.put("networkProtocolVersion", "3.1"); + + Map response = new HashMap<>(); + response.put("statusCode", "200"); + + AttributesExtractor, Map> extractor = + HttpClientAttributesExtractor.create(new TestHttpClientAttributesGetter()); + + AttributesBuilder startAttributes = Attributes.builder(); + extractor.onStart(startAttributes, Context.root(), request); + assertThat(startAttributes.build()).isEmpty(); + + AttributesBuilder endAttributes = Attributes.builder(); + extractor.onEnd(endAttributes, Context.root(), request, response, null); + assertThat(endAttributes.build()) + .containsOnly( + entry(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, 200L), + entry(NetworkAttributes.NETWORK_PROTOCOL_NAME, "spdy"), + entry(NetworkAttributes.NETWORK_PROTOCOL_VERSION, "3.1")); + } +} diff --git a/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/semconv/http/HttpClientMetricsTest.java b/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/semconv/http/HttpClientMetricsTest.java new file mode 100644 index 000000000000..0e8c497c6edc --- /dev/null +++ b/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/semconv/http/HttpClientMetricsTest.java @@ -0,0 +1,130 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.semconv.http; + +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; + +import io.opentelemetry.api.common.Attributes; +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.instrumentation.api.instrumenter.OperationListener; +import io.opentelemetry.sdk.metrics.SdkMeterProvider; +import io.opentelemetry.sdk.testing.exporter.InMemoryMetricReader; +import io.opentelemetry.semconv.ErrorAttributes; +import io.opentelemetry.semconv.HttpAttributes; +import io.opentelemetry.semconv.NetworkAttributes; +import io.opentelemetry.semconv.ServerAttributes; +import io.opentelemetry.semconv.UrlAttributes; +import io.opentelemetry.semconv.incubating.HttpIncubatingAttributes; +import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.Test; + +class HttpClientMetricsTest { + + static final double[] DURATION_BUCKETS = + HttpMetricsAdvice.DURATION_SECONDS_BUCKETS.stream().mapToDouble(d -> d).toArray(); + + @Test + void collectsMetrics() { + InMemoryMetricReader metricReader = InMemoryMetricReader.create(); + SdkMeterProvider meterProvider = + SdkMeterProvider.builder().registerMetricReader(metricReader).build(); + + OperationListener listener = HttpClientMetrics.get().create(meterProvider.get("test")); + + Attributes requestAttributes = + Attributes.builder() + .put(HttpAttributes.HTTP_REQUEST_METHOD, "GET") + .put(UrlAttributes.URL_FULL, "https://localhost:1234/") + .put(UrlAttributes.URL_PATH, "/") + .put(UrlAttributes.URL_QUERY, "q=a") + .put(ServerAttributes.SERVER_ADDRESS, "localhost") + .put(ServerAttributes.SERVER_PORT, 1234) + .build(); + + Attributes responseAttributes = + Attributes.builder() + .put(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, 200) + .put(ErrorAttributes.ERROR_TYPE, "400") + .put(HttpIncubatingAttributes.HTTP_REQUEST_BODY_SIZE, 100) + .put(HttpIncubatingAttributes.HTTP_RESPONSE_BODY_SIZE, 200) + .put(NetworkAttributes.NETWORK_PROTOCOL_NAME, "http") + .put(NetworkAttributes.NETWORK_PROTOCOL_VERSION, "2.0") + .put(NetworkAttributes.NETWORK_PEER_ADDRESS, "1.2.3.4") + .put(NetworkAttributes.NETWORK_PEER_PORT, 8080) + .build(); + + Context parent = + Context.root() + .with( + Span.wrap( + SpanContext.create( + "ff01020304050600ff0a0b0c0d0e0f00", + "090a0b0c0d0e0f00", + TraceFlags.getSampled(), + TraceState.getDefault()))); + + Context context1 = listener.onStart(parent, requestAttributes, nanos(100)); + + assertThat(metricReader.collectAllMetrics()).isEmpty(); + + Context context2 = listener.onStart(Context.root(), requestAttributes, nanos(150)); + + assertThat(metricReader.collectAllMetrics()).isEmpty(); + + listener.onEnd(context1, responseAttributes, nanos(250)); + + assertThat(metricReader.collectAllMetrics()) + .satisfiesExactlyInAnyOrder( + metric -> + assertThat(metric) + .hasName("http.client.request.duration") + .hasUnit("s") + .hasDescription("Duration of HTTP client requests.") + .hasHistogramSatisfying( + histogram -> + histogram.hasPointsSatisfying( + point -> + point + .hasSum(0.15 /* seconds */) + .hasAttributesSatisfying( + equalTo(HttpAttributes.HTTP_REQUEST_METHOD, "GET"), + equalTo(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, 200), + equalTo(ErrorAttributes.ERROR_TYPE, "400"), + equalTo( + NetworkAttributes.NETWORK_PROTOCOL_NAME, "http"), + equalTo( + NetworkAttributes.NETWORK_PROTOCOL_VERSION, "2.0"), + equalTo(ServerAttributes.SERVER_ADDRESS, "localhost"), + equalTo(ServerAttributes.SERVER_PORT, 1234)) + .hasExemplarsSatisfying( + exemplar -> + exemplar + .hasTraceId("ff01020304050600ff0a0b0c0d0e0f00") + .hasSpanId("090a0b0c0d0e0f00")) + .hasBucketBoundaries(DURATION_BUCKETS)))); + + listener.onEnd(context2, responseAttributes, nanos(300)); + + assertThat(metricReader.collectAllMetrics()) + .satisfiesExactlyInAnyOrder( + metric -> + assertThat(metric) + .hasName("http.client.request.duration") + .hasHistogramSatisfying( + histogram -> + histogram.hasPointsSatisfying( + point -> point.hasSum(0.3 /* seconds */)))); + } + + private static long nanos(int millis) { + return TimeUnit.MILLISECONDS.toNanos(millis); + } +} diff --git a/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/semconv/http/HttpClientRequestResendCountTest.java b/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/semconv/http/HttpClientRequestResendCountTest.java new file mode 100644 index 000000000000..347ee5fd5c16 --- /dev/null +++ b/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/semconv/http/HttpClientRequestResendCountTest.java @@ -0,0 +1,28 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.semconv.http; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.context.Context; +import org.junit.jupiter.api.Test; + +class HttpClientRequestResendCountTest { + + @Test + void resendCountShouldBeZeroWhenNotInitialized() { + assertThat(HttpClientRequestResendCount.getAndIncrement(Context.root())).isEqualTo(0); + assertThat(HttpClientRequestResendCount.getAndIncrement(Context.root())).isEqualTo(0); + } + + @Test + void incrementResendCount() { + Context context = HttpClientRequestResendCount.initialize(Context.root()); + + assertThat(HttpClientRequestResendCount.getAndIncrement(context)).isEqualTo(0); + assertThat(HttpClientRequestResendCount.getAndIncrement(context)).isEqualTo(1); + } +} diff --git a/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/semconv/http/HttpServerAddressAndPortExtractorTest.java b/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/semconv/http/HttpServerAddressAndPortExtractorTest.java new file mode 100644 index 000000000000..21b6dd6ba8ae --- /dev/null +++ b/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/semconv/http/HttpServerAddressAndPortExtractorTest.java @@ -0,0 +1,150 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.semconv.http; + +import static java.util.Arrays.asList; +import static java.util.Collections.emptyList; +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.params.provider.Arguments.arguments; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.when; + +import io.opentelemetry.instrumentation.api.semconv.network.internal.AddressAndPort; +import java.util.List; +import java.util.stream.Stream; +import javax.annotation.Nullable; +import org.junit.jupiter.api.extension.ExtendWith; +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.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class HttpServerAddressAndPortExtractorTest { + + @Mock HttpServerAttributesGetter getter; + + @InjectMocks HttpServerAddressAndPortExtractor underTest; + + @ParameterizedTest + @ArgumentsSource(ForwardedArgs.class) + void shouldParseForwarded(List headers, @Nullable String expectedAddress) { + when(getter.getHttpRequestHeader("request", "forwarded")).thenReturn(headers); + + AddressAndPort sink = new AddressAndPort(); + underTest.extract(sink, "request"); + + assertThat(sink.getAddress()).isEqualTo(expectedAddress); + assertThat(sink.getPort()).isNull(); + } + + static final class ForwardedArgs implements ArgumentsProvider { + + @Override + public Stream provideArguments(ExtensionContext extensionContext) { + return Stream.of( + // empty/invalid headers + arguments(singletonList(""), null), + arguments(singletonList("for="), null), + arguments(singletonList("for=;"), null), + arguments(singletonList("for=\""), null), + arguments(singletonList("for=\"\""), null), + arguments(singletonList("for=\"1.2.3.4"), null), + arguments(singletonList("for=\"[::1]"), null), + arguments(singletonList("for=[::1"), null), + arguments(singletonList("for=\"[::1\""), null), + arguments(singletonList("for=\"[::1\"]"), null), + arguments(singletonList("by=1.2.3.4, test=abc"), null), + + // ipv6 + arguments(singletonList("for=[::1]"), "::1"), + arguments(singletonList("For=[::1]"), "::1"), + arguments(singletonList("for=\"[::1]\":42"), "::1"), + arguments(singletonList("for=[::1]:42"), "::1"), + arguments(singletonList("for=\"[::1]:42\""), "::1"), + arguments(singletonList("for=[::1], for=1.2.3.4"), "::1"), + arguments(singletonList("for=[::1]; for=1.2.3.4:42"), "::1"), + arguments(singletonList("for=[::1]:42abc"), "::1"), + arguments(singletonList("for=[::1]:abc"), "::1"), + + // ipv4 + arguments(singletonList("for=1.2.3.4"), "1.2.3.4"), + arguments(singletonList("FOR=1.2.3.4"), "1.2.3.4"), + arguments(singletonList("for=1.2.3.4, :42"), "1.2.3.4"), + arguments(singletonList("for=1.2.3.4;proto=https;by=4.3.2.1"), "1.2.3.4"), + arguments(singletonList("for=1.2.3.4:42"), "1.2.3.4"), + arguments(singletonList("for=1.2.3.4:42abc"), "1.2.3.4"), + arguments(singletonList("for=1.2.3.4:abc"), "1.2.3.4"), + arguments(singletonList("for=1.2.3.4; for=4.3.2.1:42"), "1.2.3.4"), + + // multiple headers + arguments(asList("proto=https", "for=1.2.3.4", "for=[::1]:42"), "1.2.3.4")); + } + } + + @ParameterizedTest + @ArgumentsSource(ForwardedForArgs.class) + @SuppressWarnings("MockitoDoSetup") + void shouldParseForwardedFor(List headers, @Nullable String expectedAddress) { + doReturn(emptyList()).when(getter).getHttpRequestHeader("request", "forwarded"); + doReturn(headers).when(getter).getHttpRequestHeader("request", "x-forwarded-for"); + + AddressAndPort sink = new AddressAndPort(); + underTest.extract(sink, "request"); + + assertThat(sink.getAddress()).isEqualTo(expectedAddress); + assertThat(sink.getPort()).isNull(); + } + + static final class ForwardedForArgs implements ArgumentsProvider { + + @Override + public Stream provideArguments(ExtensionContext extensionContext) { + return Stream.of( + // empty/invalid headers + arguments(singletonList(""), null), + arguments(singletonList(";"), null), + arguments(singletonList("\""), null), + arguments(singletonList("\"\""), null), + arguments(singletonList("\"1.2.3.4"), null), + arguments(singletonList("\"[::1]"), null), + arguments(singletonList("[::1"), null), + arguments(singletonList("\"[::1\""), null), + arguments(singletonList("\"[::1\"]"), null), + + // ipv6 + arguments(singletonList("[::1]"), "::1"), + arguments(singletonList("\"[::1]\":42"), "::1"), + arguments(singletonList("[::1]:42"), "::1"), + arguments(singletonList("\"[::1]:42\""), "::1"), + arguments(singletonList("[::1],1.2.3.4"), "::1"), + arguments(singletonList("[::1];1.2.3.4:42"), "::1"), + arguments(singletonList("[::1]:42abc"), "::1"), + arguments(singletonList("[::1]:abc"), "::1"), + + // ipv4 + arguments(singletonList("1.2.3.4"), "1.2.3.4"), + arguments(singletonList("1.2.3.4, :42"), "1.2.3.4"), + arguments(singletonList("1.2.3.4,4.3.2.1"), "1.2.3.4"), + arguments(singletonList("1.2.3.4:42"), "1.2.3.4"), + arguments(singletonList("1.2.3.4:42abc"), "1.2.3.4"), + arguments(singletonList("1.2.3.4:abc"), "1.2.3.4"), + + // ipv6 without brackets + arguments(singletonList("::1"), "::1"), + arguments(singletonList("::1,::2,1.2.3.4"), "::1"), + arguments(singletonList("::1;::2;1.2.3.4"), "::1"), + + // multiple headers + arguments(asList("1.2.3.4", "::1"), "1.2.3.4")); + } + } +} diff --git a/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/semconv/http/HttpServerAttributesExtractorTest.java b/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/semconv/http/HttpServerAttributesExtractorTest.java new file mode 100644 index 000000000000..8526d9e04f29 --- /dev/null +++ b/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/semconv/http/HttpServerAttributesExtractorTest.java @@ -0,0 +1,559 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.semconv.http; + +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; +import static java.util.Arrays.asList; +import static java.util.Collections.emptyList; +import static java.util.Collections.emptyMap; +import static java.util.Collections.singletonList; +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.context.Context; +import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; +import io.opentelemetry.instrumentation.api.internal.HttpConstants; +import io.opentelemetry.semconv.ClientAttributes; +import io.opentelemetry.semconv.ErrorAttributes; +import io.opentelemetry.semconv.HttpAttributes; +import io.opentelemetry.semconv.NetworkAttributes; +import io.opentelemetry.semconv.ServerAttributes; +import io.opentelemetry.semconv.UrlAttributes; +import io.opentelemetry.semconv.UserAgentAttributes; +import java.net.ConnectException; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import javax.annotation.Nullable; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ArgumentsSource; +import org.junit.jupiter.params.provider.ValueSource; + +class HttpServerAttributesExtractorTest { + + static class TestHttpServerAttributesGetter + implements HttpServerAttributesGetter, Map> { + + @Override + public String getHttpRequestMethod(Map request) { + return request.get("method"); + } + + @Override + public String getUrlScheme(Map request) { + return request.get("urlScheme"); + } + + @Nullable + @Override + public String getUrlPath(Map request) { + return request.get("urlPath"); + } + + @Nullable + @Override + public String getUrlQuery(Map request) { + return request.get("urlQuery"); + } + + @Override + public String getHttpRoute(Map request) { + return request.get("route"); + } + + @Override + public List getHttpRequestHeader(Map request, String name) { + String values = request.get("header." + name); + return values == null ? emptyList() : asList(values.split(",")); + } + + @Override + public Integer getHttpResponseStatusCode( + Map request, Map response, @Nullable Throwable error) { + String value = response.get("statusCode"); + return value == null ? null : Integer.parseInt(value); + } + + @Override + public List getHttpResponseHeader( + Map request, Map response, String name) { + String values = response.get("header." + name); + return values == null ? emptyList() : asList(values.split(",")); + } + + @Nullable + @Override + public String getNetworkTransport( + Map request, @Nullable Map response) { + return request.get("networkTransport"); + } + + @Nullable + @Override + public String getNetworkType( + Map request, @Nullable Map response) { + return request.get("networkType"); + } + + @Nullable + @Override + public String getNetworkProtocolName( + Map request, Map response) { + return request.get("networkProtocolName"); + } + + @Nullable + @Override + public String getNetworkProtocolVersion( + Map request, Map response) { + return request.get("networkProtocolVersion"); + } + + @Nullable + @Override + public String getNetworkLocalAddress( + Map request, @Nullable Map response) { + return request.get("networkLocalAddress"); + } + + @Nullable + @Override + public Integer getNetworkLocalPort( + Map request, @Nullable Map response) { + String value = request.get("networkLocalPort"); + return value == null ? null : Integer.parseInt(value); + } + + @Nullable + @Override + public String getNetworkPeerAddress( + Map request, @Nullable Map response) { + return request.get("networkPeerAddress"); + } + + @Nullable + @Override + public Integer getNetworkPeerPort( + Map request, @Nullable Map response) { + String value = request.get("networkPeerPort"); + return value == null ? null : Integer.parseInt(value); + } + + @Nullable + @Override + public String getErrorType( + Map request, + @Nullable Map respobse, + @Nullable Throwable error) { + return request.get("errorType"); + } + } + + @Test + void normal() { + Map request = new HashMap<>(); + request.put("method", "POST"); + request.put("urlFull", "https://github.com"); + request.put("urlPath", "/repositories/1"); + request.put("urlQuery", "details=true"); + request.put("urlScheme", "https"); + request.put("header.content-length", "10"); + request.put("route", "/repositories/{id}"); + request.put("header.user-agent", "okhttp 3.x"); + request.put("header.host", "github.com:443"); + request.put("header.forwarded", "for=1.1.1.1;proto=https"); + request.put("header.custom-request-header", "123,456"); + request.put("networkTransport", "udp"); + request.put("networkType", "ipv4"); + request.put("networkProtocolName", "http"); + request.put("networkProtocolVersion", "2.0"); + request.put("networkLocalAddress", "1.2.3.4"); + request.put("networkLocalPort", "42"); + request.put("networkPeerAddress", "4.3.2.1"); + request.put("networkPeerPort", "456"); + + Map response = new HashMap<>(); + response.put("statusCode", "202"); + response.put("header.content-length", "20"); + response.put("header.custom-response-header", "654,321"); + + Function routeFromContext = ctx -> "/repositories/{repoId}"; + + AttributesExtractor, Map> extractor = + HttpServerAttributesExtractor.builder(new TestHttpServerAttributesGetter()) + .setCapturedRequestHeaders(singletonList("Custom-Request-Header")) + .setCapturedResponseHeaders(singletonList("Custom-Response-Header")) + .setHttpRouteGetter(routeFromContext) + .build(); + + AttributesBuilder startAttributes = Attributes.builder(); + extractor.onStart(startAttributes, Context.root(), request); + assertThat(startAttributes.build()) + .containsOnly( + entry(ServerAttributes.SERVER_ADDRESS, "github.com"), + entry(ServerAttributes.SERVER_PORT, 443L), + entry(HttpAttributes.HTTP_REQUEST_METHOD, "POST"), + entry(UrlAttributes.URL_SCHEME, "https"), + entry(UrlAttributes.URL_PATH, "/repositories/1"), + entry(UrlAttributes.URL_QUERY, "details=true"), + entry(UserAgentAttributes.USER_AGENT_ORIGINAL, "okhttp 3.x"), + entry(HttpAttributes.HTTP_ROUTE, "/repositories/{id}"), + entry(ClientAttributes.CLIENT_ADDRESS, "1.1.1.1"), + entry( + AttributeKey.stringArrayKey("http.request.header.custom-request-header"), + asList("123", "456"))); + + AttributesBuilder endAttributes = Attributes.builder(); + extractor.onEnd(endAttributes, Context.root(), request, response, null); + assertThat(endAttributes.build()) + .containsOnly( + entry(NetworkAttributes.NETWORK_PROTOCOL_VERSION, "2.0"), + entry(NetworkAttributes.NETWORK_PEER_ADDRESS, "4.3.2.1"), + entry(NetworkAttributes.NETWORK_PEER_PORT, 456L), + entry(HttpAttributes.HTTP_ROUTE, "/repositories/{repoId}"), + entry(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, 202L), + entry( + AttributeKey.stringArrayKey("http.response.header.custom-response-header"), + asList("654", "321"))); + } + + @ParameterizedTest + @ArgumentsSource(ValidRequestMethodsProvider.class) + void shouldExtractKnownMethods(String requestMethod) { + Map request = new HashMap<>(); + request.put("method", requestMethod); + + AttributesExtractor, Map> extractor = + HttpServerAttributesExtractor.create(new TestHttpServerAttributesGetter()); + + AttributesBuilder attributes = Attributes.builder(); + extractor.onStart(attributes, Context.root(), request); + extractor.onEnd(attributes, Context.root(), request, emptyMap(), null); + + assertThat(attributes.build()) + .containsEntry(HttpAttributes.HTTP_REQUEST_METHOD, requestMethod) + .doesNotContainKey(HttpAttributes.HTTP_REQUEST_METHOD_ORIGINAL); + } + + @ParameterizedTest + @ValueSource(strings = {"get", "Get"}) + void shouldTreatMethodsAsCaseSensitive(String requestMethod) { + Map request = new HashMap<>(); + request.put("method", requestMethod); + + AttributesExtractor, Map> extractor = + HttpServerAttributesExtractor.create(new TestHttpServerAttributesGetter()); + + AttributesBuilder attributes = Attributes.builder(); + extractor.onStart(attributes, Context.root(), request); + extractor.onEnd(attributes, Context.root(), request, emptyMap(), null); + + assertThat(attributes.build()) + .containsEntry(HttpAttributes.HTTP_REQUEST_METHOD, HttpConstants._OTHER) + .containsEntry(HttpAttributes.HTTP_REQUEST_METHOD_ORIGINAL, requestMethod); + } + + @ParameterizedTest + @ValueSource(strings = {"PURGE", "not a method really"}) + void shouldUseOtherForUnknownMethods(String requestMethod) { + Map request = new HashMap<>(); + request.put("method", requestMethod); + + AttributesExtractor, Map> extractor = + HttpServerAttributesExtractor.create(new TestHttpServerAttributesGetter()); + + AttributesBuilder attributes = Attributes.builder(); + extractor.onStart(attributes, Context.root(), request); + extractor.onEnd(attributes, Context.root(), request, emptyMap(), null); + + assertThat(attributes.build()) + .containsEntry(HttpAttributes.HTTP_REQUEST_METHOD, HttpConstants._OTHER) + .containsEntry(HttpAttributes.HTTP_REQUEST_METHOD_ORIGINAL, requestMethod); + } + + @ParameterizedTest + @ValueSource(strings = {"only", "custom", "methods", "allowed"}) + void shouldExtractKnownMethods_override(String requestMethod) { + Map request = new HashMap<>(); + request.put("method", requestMethod); + + AttributesExtractor, Map> extractor = + HttpServerAttributesExtractor.builder(new TestHttpServerAttributesGetter()) + .setKnownMethods(new HashSet<>(asList("only", "custom", "methods", "allowed"))) + .build(); + + AttributesBuilder attributes = Attributes.builder(); + extractor.onStart(attributes, Context.root(), request); + extractor.onEnd(attributes, Context.root(), request, emptyMap(), null); + + assertThat(attributes.build()) + .containsEntry(HttpAttributes.HTTP_REQUEST_METHOD, requestMethod) + .doesNotContainKey(HttpAttributes.HTTP_REQUEST_METHOD_ORIGINAL); + } + + @ParameterizedTest + @ArgumentsSource(ValidRequestMethodsProvider.class) + void shouldUseOtherForUnknownMethods_override(String requestMethod) { + Map request = new HashMap<>(); + request.put("method", requestMethod); + + AttributesExtractor, Map> extractor = + HttpServerAttributesExtractor.builder(new TestHttpServerAttributesGetter()) + .setKnownMethods(new HashSet<>(asList("only", "custom", "methods", "allowed"))) + .build(); + + AttributesBuilder attributes = Attributes.builder(); + extractor.onStart(attributes, Context.root(), request); + extractor.onEnd(attributes, Context.root(), request, emptyMap(), null); + + assertThat(attributes.build()) + .containsEntry(HttpAttributes.HTTP_REQUEST_METHOD, HttpConstants._OTHER) + .containsEntry(HttpAttributes.HTTP_REQUEST_METHOD_ORIGINAL, requestMethod); + } + + @Test + void shouldExtractErrorType_httpStatusCode() { + Map response = new HashMap<>(); + response.put("statusCode", "500"); + + AttributesExtractor, Map> extractor = + HttpServerAttributesExtractor.create(new TestHttpServerAttributesGetter()); + + AttributesBuilder attributes = Attributes.builder(); + extractor.onStart(attributes, Context.root(), emptyMap()); + extractor.onEnd(attributes, Context.root(), emptyMap(), response, null); + + assertThat(attributes.build()) + .containsEntry(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, 500) + .containsEntry(ErrorAttributes.ERROR_TYPE, "500"); + } + + @Test + void shouldExtractErrorType_getter() { + Map request = new HashMap<>(); + request.put("statusCode", "0"); + request.put("errorType", "custom error type"); + + AttributesExtractor, Map> extractor = + HttpServerAttributesExtractor.create(new TestHttpServerAttributesGetter()); + + AttributesBuilder attributes = Attributes.builder(); + extractor.onStart(attributes, Context.root(), emptyMap()); + extractor.onEnd(attributes, Context.root(), request, emptyMap(), null); + + assertThat(attributes.build()).containsEntry(ErrorAttributes.ERROR_TYPE, "custom error type"); + } + + @Test + void shouldExtractErrorType_exceptionClassName() { + AttributesExtractor, Map> extractor = + HttpServerAttributesExtractor.create(new TestHttpServerAttributesGetter()); + + AttributesBuilder attributes = Attributes.builder(); + extractor.onStart(attributes, Context.root(), emptyMap()); + extractor.onEnd(attributes, Context.root(), emptyMap(), emptyMap(), new ConnectException()); + + assertThat(attributes.build()) + .containsEntry(ErrorAttributes.ERROR_TYPE, "java.net.ConnectException"); + } + + @Test + void shouldExtractErrorType_other() { + AttributesExtractor, Map> extractor = + HttpServerAttributesExtractor.create(new TestHttpServerAttributesGetter()); + + AttributesBuilder attributes = Attributes.builder(); + extractor.onStart(attributes, Context.root(), emptyMap()); + extractor.onEnd(attributes, Context.root(), emptyMap(), emptyMap(), null); + + assertThat(attributes.build()).containsEntry(ErrorAttributes.ERROR_TYPE, HttpConstants._OTHER); + } + + @Test + void shouldPreferUrlSchemeFromForwardedHeader() { + Map request = new HashMap<>(); + request.put("urlScheme", "http"); + request.put("header.forwarded", "proto=https"); + + Map response = new HashMap<>(); + response.put("statusCode", "202"); + + AttributesExtractor, Map> extractor = + HttpServerAttributesExtractor.create(new TestHttpServerAttributesGetter()); + + AttributesBuilder startAttributes = Attributes.builder(); + extractor.onStart(startAttributes, Context.root(), request); + assertThat(startAttributes.build()).containsOnly(entry(UrlAttributes.URL_SCHEME, "https")); + + AttributesBuilder endAttributes = Attributes.builder(); + extractor.onEnd(endAttributes, Context.root(), request, response, null); + assertThat(endAttributes.build()) + .containsOnly(entry(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, 202L)); + } + + @Test + void shouldExtractServerAddressAndPortFromForwardedHeader() { + Map request = new HashMap<>(); + request.put("header.forwarded", "host=example.com:42"); + request.put("header.x-forwarded-host", "opentelemetry.io:987"); + request.put("header.host", "github.com:123"); + request.put("header.:authority", "opentelemetry.io:456"); + + Map response = new HashMap<>(); + response.put("statusCode", "200"); + + AttributesExtractor, Map> extractor = + HttpServerAttributesExtractor.create(new TestHttpServerAttributesGetter()); + + AttributesBuilder startAttributes = Attributes.builder(); + extractor.onStart(startAttributes, Context.root(), request); + + assertThat(startAttributes.build()) + .containsOnly( + entry(ServerAttributes.SERVER_ADDRESS, "example.com"), + entry(ServerAttributes.SERVER_PORT, 42L)); + + AttributesBuilder endAttributes = Attributes.builder(); + extractor.onEnd(endAttributes, Context.root(), request, response, null); + assertThat(endAttributes.build()) + .containsOnly(entry(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, 200L)); + } + + @Test + void shouldExtractServerAddressAndPortFromForwardedHostHeader() { + Map request = new HashMap<>(); + request.put("header.x-forwarded-host", "opentelemetry.io:987"); + request.put("header.host", "github.com:123"); + request.put("header.:authority", "opentelemetry.io:42"); + + Map response = new HashMap<>(); + response.put("statusCode", "200"); + + AttributesExtractor, Map> extractor = + HttpServerAttributesExtractor.create(new TestHttpServerAttributesGetter()); + + AttributesBuilder startAttributes = Attributes.builder(); + extractor.onStart(startAttributes, Context.root(), request); + + assertThat(startAttributes.build()) + .containsOnly( + entry(ServerAttributes.SERVER_ADDRESS, "opentelemetry.io"), + entry(ServerAttributes.SERVER_PORT, 987L)); + + AttributesBuilder endAttributes = Attributes.builder(); + extractor.onEnd(endAttributes, Context.root(), request, response, null); + assertThat(endAttributes.build()) + .containsOnly(entry(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, 200L)); + } + + @Test + void shouldExtractServerAddressAndPortFromAuthorityPseudoHeader() { + Map request = new HashMap<>(); + request.put("header.:authority", "opentelemetry.io:42"); + request.put("header.host", "github.com:123"); + + Map response = new HashMap<>(); + response.put("statusCode", "200"); + + AttributesExtractor, Map> extractor = + HttpServerAttributesExtractor.create(new TestHttpServerAttributesGetter()); + + AttributesBuilder startAttributes = Attributes.builder(); + extractor.onStart(startAttributes, Context.root(), request); + + assertThat(startAttributes.build()) + .containsOnly( + entry(ServerAttributes.SERVER_ADDRESS, "opentelemetry.io"), + entry(ServerAttributes.SERVER_PORT, 42L)); + + AttributesBuilder endAttributes = Attributes.builder(); + extractor.onEnd(endAttributes, Context.root(), request, response, null); + assertThat(endAttributes.build()) + .containsOnly(entry(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, 200L)); + } + + @Test + void shouldExtractServerAddressAndPortFromHostHeader() { + Map request = new HashMap<>(); + request.put("header.host", "github.com:123"); + + Map response = new HashMap<>(); + response.put("statusCode", "200"); + + AttributesExtractor, Map> extractor = + HttpServerAttributesExtractor.create(new TestHttpServerAttributesGetter()); + + AttributesBuilder startAttributes = Attributes.builder(); + extractor.onStart(startAttributes, Context.root(), request); + + assertThat(startAttributes.build()) + .containsOnly( + entry(ServerAttributes.SERVER_ADDRESS, "github.com"), + entry(ServerAttributes.SERVER_PORT, 123L)); + + AttributesBuilder endAttributes = Attributes.builder(); + extractor.onEnd(endAttributes, Context.root(), request, response, null); + assertThat(endAttributes.build()) + .containsOnly(entry(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, 200L)); + } + + @Test + void shouldExtractPeerAddressEvenIfItDuplicatesClientAddress() { + Map request = new HashMap<>(); + request.put("networkPeerAddress", "1.2.3.4"); + request.put("networkPeerPort", "456"); + request.put("header.forwarded", "for=1.2.3.4:123"); + + Map response = new HashMap<>(); + response.put("statusCode", "200"); + + AttributesExtractor, Map> extractor = + HttpServerAttributesExtractor.create(new TestHttpServerAttributesGetter()); + + AttributesBuilder startAttributes = Attributes.builder(); + extractor.onStart(startAttributes, Context.root(), request); + assertThat(startAttributes.build()) + .containsOnly(entry(ClientAttributes.CLIENT_ADDRESS, "1.2.3.4")); + + AttributesBuilder endAttributes = Attributes.builder(); + extractor.onEnd(endAttributes, Context.root(), request, response, null); + assertThat(endAttributes.build()) + .containsOnly( + entry(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, 200L), + entry(NetworkAttributes.NETWORK_PEER_ADDRESS, "1.2.3.4"), + entry(NetworkAttributes.NETWORK_PEER_PORT, 456L)); + } + + @Test + void shouldExtractProtocolNameDifferentFromHttp() { + Map request = new HashMap<>(); + request.put("networkProtocolName", "spdy"); + request.put("networkProtocolVersion", "3.1"); + + Map response = new HashMap<>(); + response.put("statusCode", "200"); + + AttributesExtractor, Map> extractor = + HttpServerAttributesExtractor.create(new TestHttpServerAttributesGetter()); + + AttributesBuilder startAttributes = Attributes.builder(); + extractor.onStart(startAttributes, Context.root(), request); + assertThat(startAttributes.build()).isEmpty(); + + AttributesBuilder endAttributes = Attributes.builder(); + extractor.onEnd(endAttributes, Context.root(), request, response, null); + assertThat(endAttributes.build()) + .containsOnly( + entry(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, 200L), + entry(NetworkAttributes.NETWORK_PROTOCOL_NAME, "spdy"), + entry(NetworkAttributes.NETWORK_PROTOCOL_VERSION, "3.1")); + } +} diff --git a/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/semconv/http/HttpServerMetricsTest.java b/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/semconv/http/HttpServerMetricsTest.java new file mode 100644 index 000000000000..9a9da78627cd --- /dev/null +++ b/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/semconv/http/HttpServerMetricsTest.java @@ -0,0 +1,183 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.semconv.http; + +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; + +import io.opentelemetry.api.common.Attributes; +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.instrumentation.api.instrumenter.OperationListener; +import io.opentelemetry.sdk.metrics.SdkMeterProvider; +import io.opentelemetry.sdk.testing.exporter.InMemoryMetricReader; +import io.opentelemetry.semconv.ErrorAttributes; +import io.opentelemetry.semconv.HttpAttributes; +import io.opentelemetry.semconv.NetworkAttributes; +import io.opentelemetry.semconv.ServerAttributes; +import io.opentelemetry.semconv.UrlAttributes; +import io.opentelemetry.semconv.incubating.HttpIncubatingAttributes; +import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.Test; + +class HttpServerMetricsTest { + + static final double[] DURATION_BUCKETS = + HttpMetricsAdvice.DURATION_SECONDS_BUCKETS.stream().mapToDouble(d -> d).toArray(); + + @Test + void collectsMetrics() { + InMemoryMetricReader metricReader = InMemoryMetricReader.create(); + SdkMeterProvider meterProvider = + SdkMeterProvider.builder().registerMetricReader(metricReader).build(); + + OperationListener listener = HttpServerMetrics.get().create(meterProvider.get("test")); + + Attributes requestAttributes = + Attributes.builder() + .put(HttpAttributes.HTTP_REQUEST_METHOD, "GET") + .put(UrlAttributes.URL_SCHEME, "https") + .put(UrlAttributes.URL_PATH, "/") + .put(UrlAttributes.URL_QUERY, "q=a") + .put(NetworkAttributes.NETWORK_TRANSPORT, "tcp") + .put(NetworkAttributes.NETWORK_TYPE, "ipv4") + .put(NetworkAttributes.NETWORK_PROTOCOL_NAME, "http") + .put(NetworkAttributes.NETWORK_PROTOCOL_VERSION, "2.0") + .put(ServerAttributes.SERVER_ADDRESS, "localhost") + .put(ServerAttributes.SERVER_PORT, 1234) + .build(); + + Attributes responseAttributes = + Attributes.builder() + .put(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, 200) + .put(ErrorAttributes.ERROR_TYPE, "500") + .put(HttpIncubatingAttributes.HTTP_REQUEST_BODY_SIZE, 100) + .put(HttpIncubatingAttributes.HTTP_RESPONSE_BODY_SIZE, 200) + .put(NetworkAttributes.NETWORK_PEER_ADDRESS, "1.2.3.4") + .put(NetworkAttributes.NETWORK_PEER_PORT, 8080) + .put(NetworkAttributes.NETWORK_LOCAL_ADDRESS, "4.3.2.1") + .put(NetworkAttributes.NETWORK_LOCAL_PORT, 9090) + .build(); + + SpanContext spanContext1 = + SpanContext.create( + "ff01020304050600ff0a0b0c0d0e0f00", + "090a0b0c0d0e0f00", + TraceFlags.getSampled(), + TraceState.getDefault()); + SpanContext spanContext2 = + SpanContext.create( + "123456789abcdef00000000000999999", + "abcde00000054321", + TraceFlags.getSampled(), + TraceState.getDefault()); + + Context parent1 = Context.root().with(Span.wrap(spanContext1)); + Context context1 = listener.onStart(parent1, requestAttributes, nanos(100)); + + Context parent2 = Context.root().with(Span.wrap(spanContext2)); + Context context2 = listener.onStart(parent2, requestAttributes, nanos(150)); + + listener.onEnd(context1, responseAttributes, nanos(250)); + + assertThat(metricReader.collectAllMetrics()) + .satisfiesExactlyInAnyOrder( + metric -> + assertThat(metric) + .hasName("http.server.request.duration") + .hasDescription("Duration of HTTP server requests.") + .hasUnit("s") + .hasHistogramSatisfying( + histogram -> + histogram.hasPointsSatisfying( + point -> + point + .hasSum(0.15 /* seconds */) + .hasAttributesSatisfying( + equalTo(HttpAttributes.HTTP_REQUEST_METHOD, "GET"), + equalTo(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, 200), + equalTo(ErrorAttributes.ERROR_TYPE, "500"), + equalTo( + NetworkAttributes.NETWORK_PROTOCOL_NAME, "http"), + equalTo( + NetworkAttributes.NETWORK_PROTOCOL_VERSION, "2.0"), + equalTo(UrlAttributes.URL_SCHEME, "https")) + .hasExemplarsSatisfying( + exemplar -> + exemplar + .hasTraceId(spanContext1.getTraceId()) + .hasSpanId(spanContext1.getSpanId())) + .hasBucketBoundaries(DURATION_BUCKETS)))); + + listener.onEnd(context2, responseAttributes, nanos(300)); + + assertThat(metricReader.collectAllMetrics()) + .satisfiesExactlyInAnyOrder( + metric -> + assertThat(metric) + .hasName("http.server.request.duration") + .hasHistogramSatisfying( + histogram -> + histogram.hasPointsSatisfying( + point -> + point + .hasSum(0.3 /* seconds */) + .hasExemplarsSatisfying( + exemplar -> + exemplar + .hasTraceId(spanContext2.getTraceId()) + .hasSpanId(spanContext2.getSpanId()))))); + } + + @Test + void collectsHttpRouteFromEndAttributes() { + // given + InMemoryMetricReader metricReader = InMemoryMetricReader.create(); + SdkMeterProvider meterProvider = + SdkMeterProvider.builder().registerMetricReader(metricReader).build(); + + OperationListener listener = HttpServerMetrics.get().create(meterProvider.get("test")); + + Attributes requestAttributes = + Attributes.builder() + .put(ServerAttributes.SERVER_ADDRESS, "host") + .put(UrlAttributes.URL_SCHEME, "https") + .build(); + + Attributes responseAttributes = + Attributes.builder().put(HttpAttributes.HTTP_ROUTE, "/test/{id}").build(); + + Context parentContext = Context.root(); + + // when + Context context = listener.onStart(parentContext, requestAttributes, nanos(100)); + listener.onEnd(context, responseAttributes, nanos(200)); + + // then + assertThat(metricReader.collectAllMetrics()) + .anySatisfy( + metric -> + assertThat(metric) + .hasName("http.server.request.duration") + .hasUnit("s") + .hasHistogramSatisfying( + histogram -> + histogram.hasPointsSatisfying( + point -> + point + .hasSum(0.100 /* seconds */) + .hasAttributesSatisfying( + equalTo(UrlAttributes.URL_SCHEME, "https"), + equalTo(HttpAttributes.HTTP_ROUTE, "/test/{id}"))))); + } + + private static long nanos(int millis) { + return TimeUnit.MILLISECONDS.toNanos(millis); + } +} diff --git a/instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpRouteHolderTest.java b/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/semconv/http/HttpServerRouteTest.java similarity index 52% rename from instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpRouteHolderTest.java rename to instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/semconv/http/HttpServerRouteTest.java index 46ab3d6892e9..c57261c83ab7 100644 --- a/instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpRouteHolderTest.java +++ b/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/semconv/http/HttpServerRouteTest.java @@ -3,17 +3,20 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.instrumentation.api.instrumenter.http; +package io.opentelemetry.instrumentation.api.semconv.http; import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; +import static java.util.Collections.singletonList; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; import static org.mockito.Mockito.when; import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanKind; import io.opentelemetry.context.Context; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; import io.opentelemetry.sdk.testing.junit5.OpenTelemetryExtension; +import java.util.HashSet; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -22,7 +25,7 @@ import org.mockito.junit.jupiter.MockitoExtension; @ExtendWith(MockitoExtension.class) -class HttpRouteHolderTest { +class HttpServerRouteTest { @RegisterExtension static final OpenTelemetryExtension testing = OpenTelemetryExtension.create(); @@ -33,8 +36,11 @@ class HttpRouteHolderTest { void setUp() { instrumenter = Instrumenter.builder(testing.getOpenTelemetry(), "test", s -> s) - .addContextCustomizer(HttpRouteHolder.create(getter)) - .buildInstrumenter(); + .addContextCustomizer( + HttpServerRoute.builder(getter) + .setKnownMethods(new HashSet<>(singletonList("GET"))) + .build()) + .buildInstrumenter(s -> SpanKind.SERVER); } @Test @@ -44,30 +50,51 @@ void noLocalRootSpan() { parentSpan.end(); Context context = instrumenter.start(Context.root().with(parentSpan), "test"); - assertNull(HttpRouteHolder.getRoute(context)); + assertNull(HttpServerRoute.get(context)); - HttpRouteHolder.updateHttpRoute(context, HttpRouteSource.SERVLET, "/get/:id"); + HttpServerRoute.update(context, HttpServerRouteSource.SERVER, "/get/:id"); instrumenter.end(context, "test", null, null); - assertNull(HttpRouteHolder.getRoute(context)); + assertNull(HttpServerRoute.get(context)); assertThat(testing.getSpans()) .satisfiesExactly( span -> assertThat(span).hasName("parent"), span -> assertThat(span).hasName("test")); } + @Test + void nonServerRootSpan() { + Instrumenter testInstrumenter = + Instrumenter.builder(testing.getOpenTelemetry(), "test", s -> s) + .addContextCustomizer( + HttpServerRoute.builder(getter) + .setKnownMethods(new HashSet<>(singletonList("GET"))) + .build()) + .buildInstrumenter(s -> SpanKind.INTERNAL); + + Context context = testInstrumenter.start(Context.root(), "test"); + assertNull(HttpServerRoute.get(context)); + + HttpServerRoute.update(context, HttpServerRouteSource.SERVER, "/get/:id"); + + testInstrumenter.end(context, "test", null, null); + + assertNull(HttpServerRoute.get(context)); + assertThat(testing.getSpans()).satisfiesExactly(span -> assertThat(span).hasName("test")); + } + @Test void shouldSetRoute() { when(getter.getHttpRequestMethod("test")).thenReturn("GET"); Context context = instrumenter.start(Context.root(), "test"); - assertNull(HttpRouteHolder.getRoute(context)); + assertNull(HttpServerRoute.get(context)); - HttpRouteHolder.updateHttpRoute(context, HttpRouteSource.SERVLET, "/get/:id"); + HttpServerRoute.update(context, HttpServerRouteSource.SERVER, "/get/:id"); instrumenter.end(context, "test", null, null); - assertEquals("/get/:id", HttpRouteHolder.getRoute(context)); + assertEquals("/get/:id", HttpServerRoute.get(context)); assertThat(testing.getSpans()) .satisfiesExactly(span -> assertThat(span).hasName("GET /get/:id")); } @@ -77,14 +104,14 @@ void shouldNotUpdateRoute_sameSource() { when(getter.getHttpRequestMethod("test")).thenReturn("GET"); Context context = instrumenter.start(Context.root(), "test"); - assertNull(HttpRouteHolder.getRoute(context)); + assertNull(HttpServerRoute.get(context)); - HttpRouteHolder.updateHttpRoute(context, HttpRouteSource.SERVLET, "/route1"); - HttpRouteHolder.updateHttpRoute(context, HttpRouteSource.SERVLET, "/route2"); + HttpServerRoute.update(context, HttpServerRouteSource.SERVER, "/route1"); + HttpServerRoute.update(context, HttpServerRouteSource.SERVER, "/route2"); instrumenter.end(context, "test", null, null); - assertEquals("/route1", HttpRouteHolder.getRoute(context)); + assertEquals("/route1", HttpServerRoute.get(context)); assertThat(testing.getSpans()) .satisfiesExactly(span -> assertThat(span).hasName("GET /route1")); } @@ -94,14 +121,14 @@ void shouldNotUpdateRoute_lowerOrderSource() { when(getter.getHttpRequestMethod("test")).thenReturn("GET"); Context context = instrumenter.start(Context.root(), "test"); - assertNull(HttpRouteHolder.getRoute(context)); + assertNull(HttpServerRoute.get(context)); - HttpRouteHolder.updateHttpRoute(context, HttpRouteSource.CONTROLLER, "/route1"); - HttpRouteHolder.updateHttpRoute(context, HttpRouteSource.SERVLET, "/route2"); + HttpServerRoute.update(context, HttpServerRouteSource.CONTROLLER, "/route1"); + HttpServerRoute.update(context, HttpServerRouteSource.SERVER, "/route2"); instrumenter.end(context, "test", null, null); - assertEquals("/route1", HttpRouteHolder.getRoute(context)); + assertEquals("/route1", HttpServerRoute.get(context)); assertThat(testing.getSpans()) .satisfiesExactly(span -> assertThat(span).hasName("GET /route1")); } @@ -111,14 +138,14 @@ void shouldUpdateRoute_higherOrderSource() { when(getter.getHttpRequestMethod("test")).thenReturn("GET"); Context context = instrumenter.start(Context.root(), "test"); - assertNull(HttpRouteHolder.getRoute(context)); + assertNull(HttpServerRoute.get(context)); - HttpRouteHolder.updateHttpRoute(context, HttpRouteSource.SERVLET, "/route1"); - HttpRouteHolder.updateHttpRoute(context, HttpRouteSource.CONTROLLER, "/route2"); + HttpServerRoute.update(context, HttpServerRouteSource.SERVER, "/route1"); + HttpServerRoute.update(context, HttpServerRouteSource.CONTROLLER, "/route2"); instrumenter.end(context, "test", null, null); - assertEquals("/route2", HttpRouteHolder.getRoute(context)); + assertEquals("/route2", HttpServerRoute.get(context)); assertThat(testing.getSpans()) .satisfiesExactly(span -> assertThat(span).hasName("GET /route2")); } @@ -128,14 +155,14 @@ void shouldUpdateRoute_betterMatch() { when(getter.getHttpRequestMethod("test")).thenReturn("GET"); Context context = instrumenter.start(Context.root(), "test"); - assertNull(HttpRouteHolder.getRoute(context)); + assertNull(HttpServerRoute.get(context)); - HttpRouteHolder.updateHttpRoute(context, HttpRouteSource.FILTER, "/a/route"); - HttpRouteHolder.updateHttpRoute(context, HttpRouteSource.FILTER, "/a/much/better/route"); + HttpServerRoute.update(context, HttpServerRouteSource.SERVER_FILTER, "/a/route"); + HttpServerRoute.update(context, HttpServerRouteSource.SERVER_FILTER, "/a/much/better/route"); instrumenter.end(context, "test", null, null); - assertEquals("/a/much/better/route", HttpRouteHolder.getRoute(context)); + assertEquals("/a/much/better/route", HttpServerRoute.get(context)); assertThat(testing.getSpans()) .satisfiesExactly(span -> assertThat(span).hasName("GET /a/much/better/route")); } @@ -145,30 +172,47 @@ void shouldNotUpdateRoute_worseMatch() { when(getter.getHttpRequestMethod("test")).thenReturn("GET"); Context context = instrumenter.start(Context.root(), "test"); - assertNull(HttpRouteHolder.getRoute(context)); + assertNull(HttpServerRoute.get(context)); - HttpRouteHolder.updateHttpRoute(context, HttpRouteSource.FILTER, "/a/pretty/good/route"); - HttpRouteHolder.updateHttpRoute(context, HttpRouteSource.FILTER, "/a"); + HttpServerRoute.update(context, HttpServerRouteSource.SERVER_FILTER, "/a/pretty/good/route"); + HttpServerRoute.update(context, HttpServerRouteSource.SERVER_FILTER, "/a"); instrumenter.end(context, "test", null, null); - assertEquals("/a/pretty/good/route", HttpRouteHolder.getRoute(context)); + assertEquals("/a/pretty/good/route", HttpServerRoute.get(context)); assertThat(testing.getSpans()) .satisfiesExactly(span -> assertThat(span).hasName("GET /a/pretty/good/route")); } @Test - void shouldNotUpdateSpanName_noMethod() { + void shouldUseHttp_noMethod() { when(getter.getHttpRequestMethod("test")).thenReturn(null); Context context = instrumenter.start(Context.root(), "test"); - assertNull(HttpRouteHolder.getRoute(context)); + assertNull(HttpServerRoute.get(context)); - HttpRouteHolder.updateHttpRoute(context, HttpRouteSource.SERVLET, "/get/:id"); + HttpServerRoute.update(context, HttpServerRouteSource.SERVER, "/get/:id"); instrumenter.end(context, "test", null, null); - assertEquals("/get/:id", HttpRouteHolder.getRoute(context)); - assertThat(testing.getSpans()).satisfiesExactly(span -> assertThat(span).hasName("test")); + assertEquals("/get/:id", HttpServerRoute.get(context)); + assertThat(testing.getSpans()) + .satisfiesExactly(span -> assertThat(span).hasName("HTTP /get/:id")); + } + + @Test + void shouldUseHttp_unknownMethod() { + when(getter.getHttpRequestMethod("test")).thenReturn("POST"); + + Context context = instrumenter.start(Context.root(), "test"); + assertNull(HttpServerRoute.get(context)); + + HttpServerRoute.update(context, HttpServerRouteSource.SERVER, "/get/:id"); + + instrumenter.end(context, "test", null, null); + + assertEquals("/get/:id", HttpServerRoute.get(context)); + assertThat(testing.getSpans()) + .satisfiesExactly(span -> assertThat(span).hasName("HTTP /get/:id")); } } diff --git a/instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpSpanNameExtractorTest.java b/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/semconv/http/HttpSpanNameExtractorTest.java similarity index 96% rename from instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpSpanNameExtractorTest.java rename to instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/semconv/http/HttpSpanNameExtractorTest.java index 5f159eadb1e0..0c59545984a2 100644 --- a/instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpSpanNameExtractorTest.java +++ b/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/semconv/http/HttpSpanNameExtractorTest.java @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.instrumentation.api.instrumenter.http; +package io.opentelemetry.instrumentation.api.semconv.http; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.anyMap; diff --git a/instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpSpanStatusExtractorTest.java b/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/semconv/http/HttpSpanStatusExtractorTest.java similarity index 90% rename from instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpSpanStatusExtractorTest.java rename to instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/semconv/http/HttpSpanStatusExtractorTest.java index e7ef16725111..63d11f421f6a 100644 --- a/instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpSpanStatusExtractorTest.java +++ b/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/semconv/http/HttpSpanStatusExtractorTest.java @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.instrumentation.api.instrumenter.http; +package io.opentelemetry.instrumentation.api.semconv.http; import static org.mockito.ArgumentMatchers.anyMap; import static org.mockito.ArgumentMatchers.isNull; @@ -34,15 +34,15 @@ class HttpSpanStatusExtractorTest { @ParameterizedTest @ValueSource(ints = {1, 100, 101, 200, 201, 300, 301, 500, 501, 600, 601}) void hasServerStatus(int statusCode) { - StatusCode expectedStatusCode = HttpStatusConverter.SERVER.statusFromHttpStatus(statusCode); + boolean isError = HttpStatusCodeConverter.SERVER.isError(statusCode); when(serverGetter.getHttpResponseStatusCode(anyMap(), anyMap(), isNull())) .thenReturn(statusCode); HttpSpanStatusExtractor.create(serverGetter) .extract(spanStatusBuilder, Collections.emptyMap(), Collections.emptyMap(), null); - if (expectedStatusCode != StatusCode.UNSET) { - verify(spanStatusBuilder).setStatus(expectedStatusCode); + if (isError) { + verify(spanStatusBuilder).setStatus(StatusCode.ERROR); } else { verifyNoInteractions(spanStatusBuilder); } @@ -51,15 +51,15 @@ void hasServerStatus(int statusCode) { @ParameterizedTest @ValueSource(ints = {1, 100, 101, 200, 201, 300, 301, 400, 401, 500, 501, 600, 601}) void hasClientStatus(int statusCode) { - StatusCode expectedStatusCode = HttpStatusConverter.CLIENT.statusFromHttpStatus(statusCode); + boolean isError = HttpStatusCodeConverter.CLIENT.isError(statusCode); when(clientGetter.getHttpResponseStatusCode(anyMap(), anyMap(), isNull())) .thenReturn(statusCode); HttpSpanStatusExtractor.create(clientGetter) .extract(spanStatusBuilder, Collections.emptyMap(), Collections.emptyMap(), null); - if (expectedStatusCode != StatusCode.UNSET) { - verify(spanStatusBuilder).setStatus(expectedStatusCode); + if (isError) { + verify(spanStatusBuilder).setStatus(StatusCode.ERROR); } else { verifyNoInteractions(spanStatusBuilder); } diff --git a/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/semconv/http/HttpStatusCodeConverterClientTest.java b/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/semconv/http/HttpStatusCodeConverterClientTest.java new file mode 100644 index 000000000000..1f6f3d18d139 --- /dev/null +++ b/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/semconv/http/HttpStatusCodeConverterClientTest.java @@ -0,0 +1,93 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.semconv.http; + +import static io.opentelemetry.instrumentation.api.semconv.http.HttpStatusCodeConverter.CLIENT; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.stream.Stream; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +class HttpStatusCodeConverterClientTest { + + @ParameterizedTest + @MethodSource("spanStatusCodes") + void httpStatusCodeToOtelStatus(int numeric, boolean isError) { + assertEquals(isError, CLIENT.isError(numeric)); + } + + static Stream spanStatusCodes() { + return Stream.of( + Arguments.of(100, false), + Arguments.of(101, false), + Arguments.of(102, false), + Arguments.of(103, false), + Arguments.of(200, false), + Arguments.of(201, false), + Arguments.of(202, false), + Arguments.of(203, false), + Arguments.of(204, false), + Arguments.of(205, false), + Arguments.of(206, false), + Arguments.of(207, false), + Arguments.of(208, false), + Arguments.of(226, false), + Arguments.of(300, false), + Arguments.of(301, false), + Arguments.of(302, false), + Arguments.of(303, false), + Arguments.of(304, false), + Arguments.of(305, false), + Arguments.of(306, false), + Arguments.of(307, false), + Arguments.of(308, false), + Arguments.of(400, true), + Arguments.of(401, true), + Arguments.of(403, true), + Arguments.of(404, true), + Arguments.of(405, true), + Arguments.of(406, true), + Arguments.of(407, true), + Arguments.of(408, true), + Arguments.of(409, true), + Arguments.of(410, true), + Arguments.of(411, true), + Arguments.of(412, true), + Arguments.of(413, true), + Arguments.of(414, true), + Arguments.of(415, true), + Arguments.of(416, true), + Arguments.of(417, true), + Arguments.of(418, true), + Arguments.of(421, true), + Arguments.of(422, true), + Arguments.of(423, true), + Arguments.of(424, true), + Arguments.of(425, true), + Arguments.of(426, true), + Arguments.of(428, true), + Arguments.of(429, true), + Arguments.of(431, true), + Arguments.of(451, true), + Arguments.of(500, true), + Arguments.of(501, true), + Arguments.of(502, true), + Arguments.of(503, true), + Arguments.of(504, true), + Arguments.of(505, true), + Arguments.of(506, true), + Arguments.of(507, true), + Arguments.of(508, true), + Arguments.of(510, true), + Arguments.of(511, true), + + // Don't exist + Arguments.of(99, true), + Arguments.of(600, true)); + } +} diff --git a/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/semconv/http/HttpStatusCodeConverterServerTest.java b/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/semconv/http/HttpStatusCodeConverterServerTest.java new file mode 100644 index 000000000000..f45a84880840 --- /dev/null +++ b/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/semconv/http/HttpStatusCodeConverterServerTest.java @@ -0,0 +1,93 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.semconv.http; + +import static io.opentelemetry.instrumentation.api.semconv.http.HttpStatusCodeConverter.SERVER; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.stream.Stream; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +class HttpStatusCodeConverterServerTest { + + @ParameterizedTest + @MethodSource("spanStatusCodes") + void httpStatusCodeToOtelStatus(int numeric, boolean isError) { + assertEquals(isError, SERVER.isError(numeric)); + } + + static Stream spanStatusCodes() { + return Stream.of( + Arguments.of(100, false), + Arguments.of(101, false), + Arguments.of(102, false), + Arguments.of(103, false), + Arguments.of(200, false), + Arguments.of(201, false), + Arguments.of(202, false), + Arguments.of(203, false), + Arguments.of(204, false), + Arguments.of(205, false), + Arguments.of(206, false), + Arguments.of(207, false), + Arguments.of(208, false), + Arguments.of(226, false), + Arguments.of(300, false), + Arguments.of(301, false), + Arguments.of(302, false), + Arguments.of(303, false), + Arguments.of(304, false), + Arguments.of(305, false), + Arguments.of(306, false), + Arguments.of(307, false), + Arguments.of(308, false), + Arguments.of(400, false), + Arguments.of(401, false), + Arguments.of(403, false), + Arguments.of(404, false), + Arguments.of(405, false), + Arguments.of(406, false), + Arguments.of(407, false), + Arguments.of(408, false), + Arguments.of(409, false), + Arguments.of(410, false), + Arguments.of(411, false), + Arguments.of(412, false), + Arguments.of(413, false), + Arguments.of(414, false), + Arguments.of(415, false), + Arguments.of(416, false), + Arguments.of(417, false), + Arguments.of(418, false), + Arguments.of(421, false), + Arguments.of(422, false), + Arguments.of(423, false), + Arguments.of(424, false), + Arguments.of(425, false), + Arguments.of(426, false), + Arguments.of(428, false), + Arguments.of(429, false), + Arguments.of(431, false), + Arguments.of(451, false), + Arguments.of(500, true), + Arguments.of(501, true), + Arguments.of(502, true), + Arguments.of(503, true), + Arguments.of(504, true), + Arguments.of(505, true), + Arguments.of(506, true), + Arguments.of(507, true), + Arguments.of(508, true), + Arguments.of(510, true), + Arguments.of(511, true), + + // Don't exist + Arguments.of(99, true), + Arguments.of(600, true)); + } +} diff --git a/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/semconv/http/ValidRequestMethodsProvider.java b/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/semconv/http/ValidRequestMethodsProvider.java new file mode 100644 index 000000000000..f7b5a56775fb --- /dev/null +++ b/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/semconv/http/ValidRequestMethodsProvider.java @@ -0,0 +1,20 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.semconv.http; + +import io.opentelemetry.instrumentation.api.internal.HttpConstants; +import java.util.stream.Stream; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; + +final class ValidRequestMethodsProvider implements ArgumentsProvider { + + @Override + public Stream provideArguments(ExtensionContext context) { + return HttpConstants.KNOWN_METHODS.stream().map(Arguments::of); + } +} diff --git a/instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/network/ClientAttributesExtractorTest.java b/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/semconv/network/ClientAttributesExtractorTest.java similarity index 68% rename from instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/network/ClientAttributesExtractorTest.java rename to instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/semconv/network/ClientAttributesExtractorTest.java index d42f97a31e0f..889568efc26c 100644 --- a/instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/network/ClientAttributesExtractorTest.java +++ b/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/semconv/network/ClientAttributesExtractorTest.java @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.instrumentation.api.instrumenter.network; +package io.opentelemetry.instrumentation.api.semconv.network; import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; import static java.util.Collections.emptyMap; @@ -13,7 +13,7 @@ import io.opentelemetry.api.common.AttributesBuilder; import io.opentelemetry.context.Context; import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.network.internal.NetworkAttributes; +import io.opentelemetry.semconv.ClientAttributes; import java.util.HashMap; import java.util.Map; import javax.annotation.Nullable; @@ -21,8 +21,7 @@ class ClientAttributesExtractorTest { - static class TestClientAttributesGetter - implements ClientAttributesGetter, Void> { + static class TestClientAttributesGetter implements ClientAttributesGetter> { @Nullable @Override @@ -36,19 +35,6 @@ public Integer getClientPort(Map request) { String value = request.get("port"); return value == null ? null : Integer.parseInt(value); } - - @Nullable - @Override - public String getClientSocketAddress(Map request, @Nullable Void response) { - return request.get("socketAddress"); - } - - @Nullable - @Override - public Integer getClientSocketPort(Map request, @Nullable Void response) { - String value = request.get("socketPort"); - return value == null ? null : Integer.parseInt(value); - } } @Test @@ -56,8 +42,6 @@ void allAttributes() { Map request = new HashMap<>(); request.put("address", "opentelemetry.io"); request.put("port", "80"); - request.put("socketAddress", "1.2.3.4"); - request.put("socketPort", "8080"); AttributesExtractor, Void> extractor = ClientAttributesExtractor.create(new TestClientAttributesGetter()); @@ -66,15 +50,12 @@ void allAttributes() { extractor.onStart(startAttributes, Context.root(), request); assertThat(startAttributes.build()) .containsOnly( - entry(NetworkAttributes.CLIENT_ADDRESS, "opentelemetry.io"), - entry(NetworkAttributes.CLIENT_PORT, 80L)); + entry(ClientAttributes.CLIENT_ADDRESS, "opentelemetry.io"), + entry(ClientAttributes.CLIENT_PORT, 80L)); AttributesBuilder endAttributes = Attributes.builder(); extractor.onEnd(endAttributes, Context.root(), request, null, null); - assertThat(endAttributes.build()) - .containsOnly( - entry(NetworkAttributes.CLIENT_SOCKET_ADDRESS, "1.2.3.4"), - entry(NetworkAttributes.CLIENT_SOCKET_PORT, 8080L)); + assertThat(endAttributes.build()).isEmpty(); } @Test @@ -92,10 +73,9 @@ void noAttributes() { } @Test - void doesNotSetNegativePortValues() { + void doesNotSetNegativePortValue() { Map request = new HashMap<>(); request.put("port", "-12"); - request.put("socketPort", "-42"); AttributesExtractor, Void> extractor = ClientAttributesExtractor.create(new TestClientAttributesGetter()); @@ -110,22 +90,16 @@ void doesNotSetNegativePortValues() { } @Test - void doesNotSetDuplicates() { + void portWithoutAddress() { Map request = new HashMap<>(); - request.put("address", "opentelemetry.io"); request.put("port", "80"); - request.put("socketAddress", "opentelemetry.io"); - request.put("socketPort", "80"); AttributesExtractor, Void> extractor = ClientAttributesExtractor.create(new TestClientAttributesGetter()); AttributesBuilder startAttributes = Attributes.builder(); extractor.onStart(startAttributes, Context.root(), request); - assertThat(startAttributes.build()) - .containsOnly( - entry(NetworkAttributes.CLIENT_ADDRESS, "opentelemetry.io"), - entry(NetworkAttributes.CLIENT_PORT, 80L)); + assertThat(startAttributes.build()).isEmpty(); AttributesBuilder endAttributes = Attributes.builder(); extractor.onEnd(endAttributes, Context.root(), request, null, null); diff --git a/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/semconv/network/NetworkAttributesExtractorInetSocketAddressTest.java b/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/semconv/network/NetworkAttributesExtractorInetSocketAddressTest.java new file mode 100644 index 000000000000..9a96690cbabd --- /dev/null +++ b/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/semconv/network/NetworkAttributesExtractorInetSocketAddressTest.java @@ -0,0 +1,76 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.semconv.network; + +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.context.Context; +import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; +import io.opentelemetry.semconv.NetworkAttributes; +import java.net.InetSocketAddress; +import javax.annotation.Nullable; +import org.junit.jupiter.api.Test; + +class NetworkAttributesExtractorInetSocketAddressTest { + + static class TestNetworkAttributesGetter + implements NetworkAttributesGetter { + + @Nullable + @Override + public InetSocketAddress getNetworkLocalInetSocketAddress( + InetSocketAddress request, @Nullable InetSocketAddress response) { + return request; + } + + @Nullable + @Override + public InetSocketAddress getNetworkPeerInetSocketAddress( + InetSocketAddress request, @Nullable InetSocketAddress response) { + return response; + } + } + + @Test + void fullAddress() { + InetSocketAddress local = new InetSocketAddress("1.2.3.4", 8080); + InetSocketAddress peer = new InetSocketAddress("4.3.2.1", 9090); + + AttributesExtractor extractor = + NetworkAttributesExtractor.create(new TestNetworkAttributesGetter()); + + AttributesBuilder startAttributes = Attributes.builder(); + extractor.onStart(startAttributes, Context.root(), local); + assertThat(startAttributes.build()).isEmpty(); + + AttributesBuilder endAttributes = Attributes.builder(); + extractor.onEnd(endAttributes, Context.root(), local, peer, null); + assertThat(endAttributes.build()) + .containsOnly( + entry(NetworkAttributes.NETWORK_TYPE, "ipv4"), + entry(NetworkAttributes.NETWORK_LOCAL_ADDRESS, "1.2.3.4"), + entry(NetworkAttributes.NETWORK_LOCAL_PORT, 8080L), + entry(NetworkAttributes.NETWORK_PEER_ADDRESS, "4.3.2.1"), + entry(NetworkAttributes.NETWORK_PEER_PORT, 9090L)); + } + + @Test + void noAttributes() { + AttributesExtractor extractor = + NetworkAttributesExtractor.create(new TestNetworkAttributesGetter()); + + AttributesBuilder startAttributes = Attributes.builder(); + extractor.onStart(startAttributes, Context.root(), null); + assertThat(startAttributes.build()).isEmpty(); + + AttributesBuilder endAttributes = Attributes.builder(); + extractor.onEnd(endAttributes, Context.root(), null, null, null); + assertThat(endAttributes.build()).isEmpty(); + } +} diff --git a/instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/network/NetworkAttributesExtractorTest.java b/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/semconv/network/NetworkAttributesExtractorTest.java similarity index 56% rename from instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/network/NetworkAttributesExtractorTest.java rename to instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/semconv/network/NetworkAttributesExtractorTest.java index 937031406651..5371b2aa8b20 100644 --- a/instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/network/NetworkAttributesExtractorTest.java +++ b/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/semconv/network/NetworkAttributesExtractorTest.java @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.instrumentation.api.instrumenter.network; +package io.opentelemetry.instrumentation.api.semconv.network; import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; import static java.util.Collections.emptyMap; @@ -13,7 +13,7 @@ import io.opentelemetry.api.common.AttributesBuilder; import io.opentelemetry.context.Context; import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.network.internal.NetworkAttributes; +import io.opentelemetry.semconv.NetworkAttributes; import java.util.HashMap; import java.util.Map; import javax.annotation.Nullable; @@ -47,6 +47,32 @@ public String getNetworkProtocolName(Map request, @Nullable Void public String getNetworkProtocolVersion(Map request, @Nullable Void response) { return request.get("protocolVersion"); } + + @Nullable + @Override + public String getNetworkLocalAddress(Map request, @Nullable Void response) { + return request.get("localAddress"); + } + + @Nullable + @Override + public Integer getNetworkLocalPort(Map request, @Nullable Void response) { + String value = request.get("localPort"); + return value == null ? null : Integer.parseInt(value); + } + + @Nullable + @Override + public String getNetworkPeerAddress(Map request, @Nullable Void response) { + return request.get("peerAddress"); + } + + @Nullable + @Override + public Integer getNetworkPeerPort(Map request, @Nullable Void response) { + String value = request.get("peerPort"); + return value == null ? null : Integer.parseInt(value); + } } @Test @@ -56,6 +82,10 @@ void allAttributes() { request.put("type", "IPv4"); request.put("protocolName", "Http"); request.put("protocolVersion", "1.1"); + request.put("localAddress", "1.2.3.4"); + request.put("localPort", "8080"); + request.put("peerAddress", "4.3.2.1"); + request.put("peerPort", "9090"); AttributesExtractor, Void> extractor = NetworkAttributesExtractor.create(new TestNetworkAttributesGetter()); @@ -71,7 +101,11 @@ void allAttributes() { entry(NetworkAttributes.NETWORK_TRANSPORT, "tcp"), entry(NetworkAttributes.NETWORK_TYPE, "ipv4"), entry(NetworkAttributes.NETWORK_PROTOCOL_NAME, "http"), - entry(NetworkAttributes.NETWORK_PROTOCOL_VERSION, "1.1")); + entry(NetworkAttributes.NETWORK_PROTOCOL_VERSION, "1.1"), + entry(NetworkAttributes.NETWORK_LOCAL_ADDRESS, "1.2.3.4"), + entry(NetworkAttributes.NETWORK_LOCAL_PORT, 8080L), + entry(NetworkAttributes.NETWORK_PEER_ADDRESS, "4.3.2.1"), + entry(NetworkAttributes.NETWORK_PEER_PORT, 9090L)); } @Test @@ -87,4 +121,27 @@ void noAttributes() { extractor.onEnd(endAttributes, Context.root(), emptyMap(), null, null); assertThat(endAttributes.build()).isEmpty(); } + + @Test + void doesNotSetNegativePortValues() { + Map request = new HashMap<>(); + request.put("localAddress", "1.2.3.4"); + request.put("localPort", "-12"); + request.put("peerAddress", "4.3.2.1"); + request.put("peerPort", "-42"); + + AttributesExtractor, Void> extractor = + NetworkAttributesExtractor.create(new TestNetworkAttributesGetter()); + + AttributesBuilder startAttributes = Attributes.builder(); + extractor.onStart(startAttributes, Context.root(), request); + assertThat(startAttributes.build()).isEmpty(); + + AttributesBuilder endAttributes = Attributes.builder(); + extractor.onEnd(endAttributes, Context.root(), request, null, null); + assertThat(endAttributes.build()) + .containsOnly( + entry(NetworkAttributes.NETWORK_LOCAL_ADDRESS, "1.2.3.4"), + entry(NetworkAttributes.NETWORK_PEER_ADDRESS, "4.3.2.1")); + } } diff --git a/instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/network/ServerAttributesExtractorTest.java b/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/semconv/network/ServerAttributesExtractorTest.java similarity index 59% rename from instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/network/ServerAttributesExtractorTest.java rename to instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/semconv/network/ServerAttributesExtractorTest.java index 0d0ed6f7e786..9cea5e06e361 100644 --- a/instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/network/ServerAttributesExtractorTest.java +++ b/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/semconv/network/ServerAttributesExtractorTest.java @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.instrumentation.api.instrumenter.network; +package io.opentelemetry.instrumentation.api.semconv.network; import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; import static java.util.Collections.emptyMap; @@ -13,7 +13,7 @@ import io.opentelemetry.api.common.AttributesBuilder; import io.opentelemetry.context.Context; import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.network.internal.NetworkAttributes; +import io.opentelemetry.semconv.ServerAttributes; import java.util.HashMap; import java.util.Map; import javax.annotation.Nullable; @@ -21,8 +21,7 @@ class ServerAttributesExtractorTest { - static class TestServerAttributesGetter - implements ServerAttributesGetter, Void> { + static class TestServerAttributesGetter implements ServerAttributesGetter> { @Nullable @Override @@ -36,25 +35,6 @@ public Integer getServerPort(Map request) { String port = request.get("port"); return port == null ? null : Integer.parseInt(port); } - - @Nullable - @Override - public String getServerSocketDomain(Map request, @Nullable Void response) { - return request.get("socketDomain"); - } - - @Nullable - @Override - public String getServerSocketAddress(Map request, @Nullable Void response) { - return request.get("socketAddress"); - } - - @Nullable - @Override - public Integer getServerSocketPort(Map request, @Nullable Void response) { - String port = request.get("socketPort"); - return port == null ? null : Integer.parseInt(port); - } } @Test @@ -62,9 +42,6 @@ void allAttributes() { Map request = new HashMap<>(); request.put("address", "opentelemetry.io"); request.put("port", "80"); - request.put("socketDomain", "proxy.opentelemetry.io"); - request.put("socketAddress", "1.2.3.4"); - request.put("socketPort", "8080"); AttributesExtractor, Void> extractor = ServerAttributesExtractor.create(new TestServerAttributesGetter()); @@ -73,16 +50,12 @@ void allAttributes() { extractor.onStart(startAttributes, Context.root(), request); assertThat(startAttributes.build()) .containsOnly( - entry(NetworkAttributes.SERVER_ADDRESS, "opentelemetry.io"), - entry(NetworkAttributes.SERVER_PORT, 80L)); + entry(ServerAttributes.SERVER_ADDRESS, "opentelemetry.io"), + entry(ServerAttributes.SERVER_PORT, 80L)); AttributesBuilder endAttributes = Attributes.builder(); extractor.onEnd(endAttributes, Context.root(), request, null, null); - assertThat(endAttributes.build()) - .containsOnly( - entry(NetworkAttributes.SERVER_SOCKET_DOMAIN, "proxy.opentelemetry.io"), - entry(NetworkAttributes.SERVER_SOCKET_ADDRESS, "1.2.3.4"), - entry(NetworkAttributes.SERVER_SOCKET_PORT, 8080L)); + assertThat(endAttributes.build()).isEmpty(); } @Test @@ -100,12 +73,10 @@ void noAttributes() { } @Test - void doesNotSetNegativePortValues() { + void doesNotSetNegativePortValue() { Map request = new HashMap<>(); request.put("address", "opentelemetry.io"); request.put("port", "-12"); - request.put("socketAddress", "1.2.3.4"); - request.put("socketPort", "-42"); AttributesExtractor, Void> extractor = ServerAttributesExtractor.create(new TestServerAttributesGetter()); @@ -113,14 +84,23 @@ void doesNotSetNegativePortValues() { AttributesBuilder startAttributes = Attributes.builder(); extractor.onStart(startAttributes, Context.root(), request); assertThat(startAttributes.build()) - .containsOnly(entry(NetworkAttributes.SERVER_ADDRESS, "opentelemetry.io")); + .containsOnly(entry(ServerAttributes.SERVER_ADDRESS, "opentelemetry.io")); + } + + @Test + void portWithoutAddress() { + Map request = new HashMap<>(); + request.put("port", "80"); + + AttributesExtractor, Void> extractor = + ServerAttributesExtractor.create(new TestServerAttributesGetter()); + + AttributesBuilder startAttributes = Attributes.builder(); + extractor.onStart(startAttributes, Context.root(), request); + assertThat(startAttributes.build()).isEmpty(); AttributesBuilder endAttributes = Attributes.builder(); extractor.onEnd(endAttributes, Context.root(), request, null, null); - assertThat(endAttributes.build()) - .containsOnly(entry(NetworkAttributes.SERVER_SOCKET_ADDRESS, "1.2.3.4")); + assertThat(endAttributes.build()).isEmpty(); } - - // TODO: add more test cases around duplicate data once - // https://github.com/open-telemetry/semantic-conventions/issues/85 clears up } diff --git a/instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/url/UrlAttributesExtractorTest.java b/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/semconv/url/UrlAttributesExtractorTest.java similarity index 94% rename from instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/url/UrlAttributesExtractorTest.java rename to instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/semconv/url/UrlAttributesExtractorTest.java index 3e0ab4446456..41b3d8c9371d 100644 --- a/instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/url/UrlAttributesExtractorTest.java +++ b/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/semconv/url/UrlAttributesExtractorTest.java @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.instrumentation.api.instrumenter.url; +package io.opentelemetry.instrumentation.api.semconv.url; import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; import static java.util.Collections.emptyMap; @@ -13,7 +13,7 @@ import io.opentelemetry.api.common.AttributesBuilder; import io.opentelemetry.context.Context; import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.url.internal.UrlAttributes; +import io.opentelemetry.semconv.UrlAttributes; import java.util.HashMap; import java.util.Map; import javax.annotation.Nullable; diff --git a/instrumentation/akka/akka-actor-fork-join-2.5/javaagent/build.gradle.kts b/instrumentation/akka/akka-actor-fork-join-2.5/javaagent/build.gradle.kts index 501e31ce83d5..77275f533569 100644 --- a/instrumentation/akka/akka-actor-fork-join-2.5/javaagent/build.gradle.kts +++ b/instrumentation/akka/akka-actor-fork-join-2.5/javaagent/build.gradle.kts @@ -4,10 +4,23 @@ plugins { } muzzle { + // Akka's fork-join was removed in 2.6, replaced with the normal java.concurrent version pass { group.set("com.typesafe.akka") module.set("akka-actor_2.11") - versions.set("[2.5,)") + versions.set("[2.5,)") // Scala 2.11 support was dropped after 2.5, so no 2.6 versions exist with this name + assertInverse.set(true) + } + pass { + group.set("com.typesafe.akka") + module.set("akka-actor_2.12") + versions.set("[2.5,2.6)") + assertInverse.set(true) + } + pass { + group.set("com.typesafe.akka") + module.set("akka-actor_2.13") + versions.set("[2.5.23,2.6)") // Scala 2.13 support was added in the middle of the 2.5 release assertInverse.set(true) } } diff --git a/instrumentation/akka/akka-actor-fork-join-2.5/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkaactor/AkkaForkJoinPoolInstrumentation.java b/instrumentation/akka/akka-actor-fork-join-2.5/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkaactor/AkkaForkJoinPoolInstrumentation.java index bb5658955e71..fd1b534c338d 100644 --- a/instrumentation/akka/akka-actor-fork-join-2.5/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkaactor/AkkaForkJoinPoolInstrumentation.java +++ b/instrumentation/akka/akka-actor-fork-join-2.5/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkaactor/AkkaForkJoinPoolInstrumentation.java @@ -48,8 +48,7 @@ public void transform(TypeTransformer transformer) { public static class SetAkkaForkJoinStateAdvice { @Advice.OnMethodEnter(suppress = Throwable.class) - public static PropagatedContext enterJobSubmit( - @Advice.Argument(value = 0, readOnly = false) ForkJoinTask task) { + public static PropagatedContext enterJobSubmit(@Advice.Argument(0) ForkJoinTask task) { Context context = Java8BytecodeBridge.currentContext(); if (ExecutorAdviceHelper.shouldPropagateContext(context, task)) { VirtualField, PropagatedContext> virtualField = diff --git a/instrumentation/akka/akka-http-10.0/javaagent/build.gradle.kts b/instrumentation/akka/akka-http-10.0/javaagent/build.gradle.kts index 7bfb2d7a48ac..ff1e6c9cccfc 100644 --- a/instrumentation/akka/akka-http-10.0/javaagent/build.gradle.kts +++ b/instrumentation/akka/akka-http-10.0/javaagent/build.gradle.kts @@ -42,14 +42,36 @@ dependencies { latestDepTestLibrary("com.typesafe.akka:akka-stream_2.13:+") } -tasks.withType().configureEach { - // required on jdk17 - jvmArgs("--add-exports=java.base/sun.security.util=ALL-UNNAMED") - jvmArgs("-XX:+IgnoreUnrecognizedVMOptions") +testing { + suites { + val javaRouteTest by registering(JvmTestSuite::class) { + dependencies { + if (findProperty("testLatestDeps") as Boolean) { + implementation("com.typesafe.akka:akka-http_2.13:+") + implementation("com.typesafe.akka:akka-stream_2.13:+") + } else { + implementation("com.typesafe.akka:akka-http_2.12:10.2.0") + implementation("com.typesafe.akka:akka-stream_2.12:2.6.21") + } + } + } + } +} + +tasks { + withType().configureEach { + // required on jdk17 + jvmArgs("--add-exports=java.base/sun.security.util=ALL-UNNAMED") + jvmArgs("-XX:+IgnoreUnrecognizedVMOptions") - jvmArgs("-Dio.opentelemetry.javaagent.shaded.io.opentelemetry.context.enableStrictContext=false") + jvmArgs("-Dio.opentelemetry.javaagent.shaded.io.opentelemetry.context.enableStrictContext=false") - systemProperty("testLatestDeps", findProperty("testLatestDeps") as Boolean) + systemProperty("testLatestDeps", findProperty("testLatestDeps") as Boolean) + } + + check { + dependsOn(testing.suites) + } } if (findProperty("testLatestDeps") as Boolean) { diff --git a/instrumentation/akka/akka-http-10.0/javaagent/src/javaRouteTest/java/io/opentelemetry/javaagent/instrumentation/akkahttp/AkkaHttpServerJavaRouteTest.java b/instrumentation/akka/akka-http-10.0/javaagent/src/javaRouteTest/java/io/opentelemetry/javaagent/instrumentation/akkahttp/AkkaHttpServerJavaRouteTest.java new file mode 100644 index 000000000000..be1357c8d9bf --- /dev/null +++ b/instrumentation/akka/akka-http-10.0/javaagent/src/javaRouteTest/java/io/opentelemetry/javaagent/instrumentation/akkahttp/AkkaHttpServerJavaRouteTest.java @@ -0,0 +1,64 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.akkahttp; + +import static akka.http.javadsl.server.PathMatchers.integerSegment; +import static org.assertj.core.api.Assertions.assertThat; + +import akka.actor.ActorSystem; +import akka.http.javadsl.Http; +import akka.http.javadsl.ServerBinding; +import akka.http.javadsl.server.AllDirectives; +import akka.http.javadsl.server.Route; +import io.opentelemetry.instrumentation.test.utils.PortUtils; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.testing.internal.armeria.client.WebClient; +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 java.util.concurrent.CompletionStage; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +class AkkaHttpServerJavaRouteTest extends AllDirectives { + @RegisterExtension + private static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + private final WebClient client = WebClient.of(); + + @Test + void testRoute() { + ActorSystem system = ActorSystem.create("my-system"); + int port = PortUtils.findOpenPort(); + Http http = Http.get(system); + + Route route = + concat( + pathEndOrSingleSlash(() -> complete("root")), + pathPrefix( + "test", + () -> + concat( + pathSingleSlash(() -> complete("test")), + path(integerSegment(), (i) -> complete("ok"))))); + + CompletionStage binding = http.newServerAt("localhost", port).bind(route); + try { + AggregatedHttpRequest request = + AggregatedHttpRequest.of(HttpMethod.GET, "h1c://localhost:" + port + "/test/1"); + AggregatedHttpResponse response = client.execute(request).aggregate().join(); + + assertThat(response.status().code()).isEqualTo(200); + assertThat(response.contentUtf8()).isEqualTo("ok"); + + testing.waitAndAssertTraces( + trace -> trace.hasSpansSatisfyingExactly(span -> span.hasName("GET /test/*"))); + } finally { + binding.thenCompose(ServerBinding::unbind).thenAccept(unbound -> system.terminate()); + } + } +} diff --git a/instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/client/AkkaHttpClientAttributesGetter.java b/instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/client/AkkaHttpClientAttributesGetter.java index b9bffcbe14bd..29ce769cca25 100644 --- a/instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/client/AkkaHttpClientAttributesGetter.java +++ b/instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/client/AkkaHttpClientAttributesGetter.java @@ -7,7 +7,7 @@ import akka.http.scaladsl.model.HttpRequest; import akka.http.scaladsl.model.HttpResponse; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpClientAttributesGetter; +import io.opentelemetry.instrumentation.api.semconv.http.HttpClientAttributesGetter; import io.opentelemetry.javaagent.instrumentation.akkahttp.AkkaHttpUtil; import java.util.List; import javax.annotation.Nullable; @@ -41,4 +41,28 @@ public List getHttpResponseHeader( HttpRequest httpRequest, HttpResponse httpResponse, String name) { return AkkaHttpUtil.responseHeader(httpResponse, name); } + + @Nullable + @Override + public String getNetworkProtocolName( + HttpRequest httpRequest, @Nullable HttpResponse httpResponse) { + return AkkaHttpUtil.protocolName(httpRequest); + } + + @Nullable + @Override + public String getNetworkProtocolVersion( + HttpRequest httpRequest, @Nullable HttpResponse httpResponse) { + return AkkaHttpUtil.protocolVersion(httpRequest); + } + + @Override + public String getServerAddress(HttpRequest httpRequest) { + return httpRequest.uri().authority().host().address(); + } + + @Override + public Integer getServerPort(HttpRequest httpRequest) { + return httpRequest.uri().authority().port(); + } } diff --git a/instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/client/AkkaHttpClientSingletons.java b/instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/client/AkkaHttpClientSingletons.java index e9d1b7611c52..aa20eeec9f93 100644 --- a/instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/client/AkkaHttpClientSingletons.java +++ b/instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/client/AkkaHttpClientSingletons.java @@ -9,13 +9,7 @@ import akka.http.scaladsl.model.HttpResponse; 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.http.HttpClientAttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpClientMetrics; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpSpanNameExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpSpanStatusExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.net.PeerServiceAttributesExtractor; -import io.opentelemetry.javaagent.bootstrap.internal.CommonConfig; +import io.opentelemetry.javaagent.bootstrap.internal.JavaagentHttpClientInstrumenters; import io.opentelemetry.javaagent.instrumentation.akkahttp.AkkaHttpUtil; public class AkkaHttpClientSingletons { @@ -25,24 +19,9 @@ public class AkkaHttpClientSingletons { static { SETTER = new HttpHeaderSetter(GlobalOpenTelemetry.getPropagators()); - AkkaHttpClientAttributesGetter httpAttributesGetter = new AkkaHttpClientAttributesGetter(); - AkkaHttpNetAttributesGetter netAttributesGetter = new AkkaHttpNetAttributesGetter(); INSTRUMENTER = - Instrumenter.builder( - GlobalOpenTelemetry.get(), - AkkaHttpUtil.instrumentationName(), - HttpSpanNameExtractor.create(httpAttributesGetter)) - .setSpanStatusExtractor(HttpSpanStatusExtractor.create(httpAttributesGetter)) - .addAttributesExtractor( - HttpClientAttributesExtractor.builder(httpAttributesGetter, netAttributesGetter) - .setCapturedRequestHeaders(CommonConfig.get().getClientRequestHeaders()) - .setCapturedResponseHeaders(CommonConfig.get().getClientResponseHeaders()) - .build()) - .addAttributesExtractor( - PeerServiceAttributesExtractor.create( - netAttributesGetter, CommonConfig.get().getPeerServiceMapping())) - .addOperationMetrics(HttpClientMetrics.get()) - .buildInstrumenter(SpanKindExtractor.alwaysClient()); + JavaagentHttpClientInstrumenters.create( + AkkaHttpUtil.instrumentationName(), new AkkaHttpClientAttributesGetter()); } public static Instrumenter instrumenter() { diff --git a/instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/client/AkkaHttpClientUtil.java b/instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/client/AkkaHttpClientUtil.java new file mode 100644 index 000000000000..c433a24a68c8 --- /dev/null +++ b/instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/client/AkkaHttpClientUtil.java @@ -0,0 +1,52 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.akkahttp.client; + +import akka.actor.ActorSystem; +import akka.actor.ExtendedActorSystem; +import akka.http.scaladsl.HttpExt; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import javax.annotation.Nullable; + +public final class AkkaHttpClientUtil { + @Nullable private static final MethodHandle actorSystemAccessor = findActorSystemAccessor(); + + @Nullable + private static MethodHandle findActorSystemAccessor() { + MethodHandle methodHandle = findActorSystemAccessor(ExtendedActorSystem.class); + if (methodHandle != null) { + return methodHandle; + } + return findActorSystemAccessor(ActorSystem.class); + } + + @Nullable + private static MethodHandle findActorSystemAccessor(Class type) { + try { + return MethodHandles.publicLookup() + .findVirtual(HttpExt.class, "system", MethodType.methodType(type)); + } catch (Throwable t) { + return null; + } + } + + @Nullable + public static ActorSystem getActorSystem(HttpExt httpExt) { + if (actorSystemAccessor == null) { + return null; + } + + try { + return (ActorSystem) actorSystemAccessor.invoke(httpExt); + } catch (Throwable e) { + return null; + } + } + + private AkkaHttpClientUtil() {} +} diff --git a/instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/client/AkkaHttpNetAttributesGetter.java b/instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/client/AkkaHttpNetAttributesGetter.java deleted file mode 100644 index f76ba014f9d0..000000000000 --- a/instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/client/AkkaHttpNetAttributesGetter.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.javaagent.instrumentation.akkahttp.client; - -import akka.http.scaladsl.model.HttpRequest; -import akka.http.scaladsl.model.HttpResponse; -import io.opentelemetry.instrumentation.api.instrumenter.net.NetClientAttributesGetter; -import io.opentelemetry.javaagent.instrumentation.akkahttp.AkkaHttpUtil; -import javax.annotation.Nullable; - -class AkkaHttpNetAttributesGetter implements NetClientAttributesGetter { - - @Nullable - @Override - public String getNetworkProtocolName( - HttpRequest httpRequest, @Nullable HttpResponse httpResponse) { - return AkkaHttpUtil.protocolName(httpRequest); - } - - @Nullable - @Override - public String getNetworkProtocolVersion( - HttpRequest httpRequest, @Nullable HttpResponse httpResponse) { - return AkkaHttpUtil.protocolVersion(httpRequest); - } - - @Override - public String getServerAddress(HttpRequest httpRequest) { - return httpRequest.uri().authority().host().address(); - } - - @Override - public Integer getServerPort(HttpRequest httpRequest) { - return httpRequest.uri().authority().port(); - } -} diff --git a/instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/client/HttpExtClientInstrumentation.java b/instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/client/HttpExtClientInstrumentation.java index f8eae50d8640..308575a04a29 100644 --- a/instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/client/HttpExtClientInstrumentation.java +++ b/instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/client/HttpExtClientInstrumentation.java @@ -9,8 +9,10 @@ import static io.opentelemetry.javaagent.instrumentation.akkahttp.client.AkkaHttpClientSingletons.instrumenter; import static io.opentelemetry.javaagent.instrumentation.akkahttp.client.AkkaHttpClientSingletons.setter; import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.namedOneOf; import static net.bytebuddy.matcher.ElementMatchers.takesArgument; +import akka.actor.ActorSystem; import akka.http.scaladsl.HttpExt; import akka.http.scaladsl.model.HttpRequest; import akka.http.scaladsl.model.HttpResponse; @@ -31,13 +33,9 @@ public ElementMatcher typeMatcher() { @Override public void transform(TypeTransformer transformer) { - // This is mainly for compatibility with 10.0 + // singleRequestImpl is only present in 10.1.x 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") + namedOneOf("singleRequest", "singleRequestImpl") .and(takesArgument(0, named("akka.http.scaladsl.model.HttpRequest"))), this.getClass().getName() + "$SingleRequestAdvice"); } @@ -80,15 +78,19 @@ public static void methodExit( } scope.close(); + ActorSystem actorSystem = AkkaHttpClientUtil.getActorSystem(thiz); + if (actorSystem == null) { + return; + } if (throwable == null) { responseFuture.onComplete( - new OnCompleteHandler(context, request), thiz.system().dispatcher()); + new OnCompleteHandler(context, request), actorSystem.dispatcher()); } else { instrumenter().end(context, request, null, throwable); } if (responseFuture != null) { responseFuture = - FutureWrapper.wrap(responseFuture, thiz.system().dispatcher(), currentContext()); + FutureWrapper.wrap(responseFuture, actorSystem.dispatcher(), currentContext()); } } } diff --git a/instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/client/PoolMasterActorInstrumentation.java b/instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/client/PoolMasterActorInstrumentation.java index b939d125cc7a..ac8600f7821e 100644 --- a/instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/client/PoolMasterActorInstrumentation.java +++ b/instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/client/PoolMasterActorInstrumentation.java @@ -6,6 +6,7 @@ package io.opentelemetry.javaagent.instrumentation.akkahttp.client; import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.namedOneOf; import io.opentelemetry.context.Scope; import io.opentelemetry.javaagent.bootstrap.Java8BytecodeBridge; @@ -25,8 +26,9 @@ public ElementMatcher typeMatcher() { 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")), + namedOneOf( + "akka$http$impl$engine$client$PoolMasterActor$$startPoolInterface", + "akka$http$impl$engine$client$PoolMasterActor$$startPoolInterfaceActor"), ClearContextAdvice.class.getName()); } diff --git a/instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/server/AkkaFlowWrapper.java b/instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/server/AkkaFlowWrapper.java index 51b31549ab5a..1880275ceade 100644 --- a/instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/server/AkkaFlowWrapper.java +++ b/instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/server/AkkaFlowWrapper.java @@ -24,6 +24,7 @@ import akka.stream.stage.OutHandler; import io.opentelemetry.context.Context; import io.opentelemetry.javaagent.bootstrap.http.HttpServerResponseCustomizerHolder; +import io.opentelemetry.javaagent.instrumentation.akkahttp.server.route.AkkaRouteHolder; import java.util.ArrayDeque; import java.util.Deque; import java.util.List; @@ -118,6 +119,7 @@ public void onPush() { Context parentContext = currentContext(); if (instrumenter().shouldStart(parentContext, request)) { Context context = instrumenter().start(parentContext, request); + context = AkkaRouteHolder.init(context); tracingRequest = new TracingRequest(context, request); } // event if span wasn't started we need to push TracingRequest to match response diff --git a/instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/server/AkkaHttpServerAttributesGetter.java b/instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/server/AkkaHttpServerAttributesGetter.java index 9360a82af6c6..ebe2062a92c7 100644 --- a/instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/server/AkkaHttpServerAttributesGetter.java +++ b/instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/server/AkkaHttpServerAttributesGetter.java @@ -7,7 +7,7 @@ import akka.http.scaladsl.model.HttpRequest; import akka.http.scaladsl.model.HttpResponse; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpServerAttributesGetter; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerAttributesGetter; import io.opentelemetry.javaagent.instrumentation.akkahttp.AkkaHttpUtil; import java.util.List; import javax.annotation.Nullable; @@ -54,4 +54,17 @@ public String getUrlQuery(HttpRequest request) { Option queryString = request.uri().rawQueryString(); return queryString.isDefined() ? queryString.get() : null; } + + @Nullable + @Override + public String getNetworkProtocolName(HttpRequest request, @Nullable HttpResponse httpResponse) { + return AkkaHttpUtil.protocolName(request); + } + + @Nullable + @Override + public String getNetworkProtocolVersion( + HttpRequest request, @Nullable HttpResponse httpResponse) { + return AkkaHttpUtil.protocolVersion(request); + } } diff --git a/instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/server/AkkaHttpServerInstrumentationModule.java b/instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/server/AkkaHttpServerInstrumentationModule.java index 12e5de05edff..b84f85f78816 100644 --- a/instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/server/AkkaHttpServerInstrumentationModule.java +++ b/instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/server/AkkaHttpServerInstrumentationModule.java @@ -27,8 +27,18 @@ public ElementMatcher.Junction classLoaderMatcher() { return hasClassesNamed("akka.http.scaladsl.HttpExt"); } + @Override + public boolean isIndyModule() { + // AkkaHttpServerInstrumentationModule and AkkaHttpServerRouteInstrumentationModule share + // AkkaRouteHolder class + return false; + } + @Override public List typeInstrumentations() { - return asList(new HttpExtServerInstrumentation(), new GraphInterpreterInstrumentation()); + return asList( + new HttpExtServerInstrumentation(), + new GraphInterpreterInstrumentation(), + new AkkaHttpServerSourceInstrumentation()); } } diff --git a/instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/server/AkkaHttpServerSingletons.java b/instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/server/AkkaHttpServerSingletons.java index f676fbe503c7..af0a3d61da82 100644 --- a/instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/server/AkkaHttpServerSingletons.java +++ b/instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/server/AkkaHttpServerSingletons.java @@ -8,13 +8,16 @@ import akka.http.scaladsl.model.HttpRequest; import akka.http.scaladsl.model.HttpResponse; import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.instrumentation.api.incubator.semconv.http.HttpExperimentalAttributesExtractor; +import io.opentelemetry.instrumentation.api.incubator.semconv.http.HttpServerExperimentalMetrics; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteHolder; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpServerAttributesExtractor; -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 io.opentelemetry.javaagent.bootstrap.internal.CommonConfig; +import io.opentelemetry.instrumentation.api.instrumenter.InstrumenterBuilder; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerAttributesExtractor; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerMetrics; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRoute; +import io.opentelemetry.instrumentation.api.semconv.http.HttpSpanNameExtractor; +import io.opentelemetry.instrumentation.api.semconv.http.HttpSpanStatusExtractor; +import io.opentelemetry.javaagent.bootstrap.internal.AgentCommonConfig; import io.opentelemetry.javaagent.instrumentation.akkahttp.AkkaHttpUtil; public final class AkkaHttpServerSingletons { @@ -23,21 +26,31 @@ public final class AkkaHttpServerSingletons { static { AkkaHttpServerAttributesGetter httpAttributesGetter = new AkkaHttpServerAttributesGetter(); - INSTRUMENTER = + InstrumenterBuilder builder = Instrumenter.builder( GlobalOpenTelemetry.get(), AkkaHttpUtil.instrumentationName(), - HttpSpanNameExtractor.create(httpAttributesGetter)) + HttpSpanNameExtractor.builder(httpAttributesGetter) + .setKnownMethods(AgentCommonConfig.get().getKnownHttpRequestMethods()) + .build()) .setSpanStatusExtractor(HttpSpanStatusExtractor.create(httpAttributesGetter)) .addAttributesExtractor( - HttpServerAttributesExtractor.builder( - httpAttributesGetter, new AkkaNetServerAttributesGetter()) - .setCapturedRequestHeaders(CommonConfig.get().getServerRequestHeaders()) - .setCapturedResponseHeaders(CommonConfig.get().getServerResponseHeaders()) + HttpServerAttributesExtractor.builder(httpAttributesGetter) + .setCapturedRequestHeaders(AgentCommonConfig.get().getServerRequestHeaders()) + .setCapturedResponseHeaders(AgentCommonConfig.get().getServerResponseHeaders()) + .setKnownMethods(AgentCommonConfig.get().getKnownHttpRequestMethods()) .build()) .addOperationMetrics(HttpServerMetrics.get()) - .addContextCustomizer(HttpRouteHolder.create(httpAttributesGetter)) - .buildServerInstrumenter(AkkaHttpServerHeaders.INSTANCE); + .addContextCustomizer( + HttpServerRoute.builder(httpAttributesGetter) + .setKnownMethods(AgentCommonConfig.get().getKnownHttpRequestMethods()) + .build()); + if (AgentCommonConfig.get().shouldEmitExperimentalHttpServerTelemetry()) { + builder + .addAttributesExtractor(HttpExperimentalAttributesExtractor.create(httpAttributesGetter)) + .addOperationMetrics(HttpServerExperimentalMetrics.get()); + } + INSTRUMENTER = builder.buildServerInstrumenter(AkkaHttpServerHeaders.INSTANCE); } public static Instrumenter instrumenter() { diff --git a/instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/server/AkkaHttpServerSourceInstrumentation.java b/instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/server/AkkaHttpServerSourceInstrumentation.java new file mode 100644 index 000000000000..2e14803f5cf0 --- /dev/null +++ b/instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/server/AkkaHttpServerSourceInstrumentation.java @@ -0,0 +1,42 @@ +/* + * 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.scaladsl.Flow; +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 AkkaHttpServerSourceInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher typeMatcher() { + return named("akka.http.scaladsl.Http$IncomingConnection"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + named("handleWith").and(takesArgument(0, named("akka.stream.scaladsl.Flow"))), + this.getClass().getName() + "$AkkaBindAndHandleAdvice"); + } + + @SuppressWarnings("unused") + public static class AkkaBindAndHandleAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void wrapHandler( + @Advice.Argument(value = 0, readOnly = false) Flow handler) { + handler = AkkaFlowWrapper.wrap(handler); + } + } +} diff --git a/instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/server/AkkaNetServerAttributesGetter.java b/instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/server/AkkaNetServerAttributesGetter.java deleted file mode 100644 index a85ac5a3492d..000000000000 --- a/instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/server/AkkaNetServerAttributesGetter.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.javaagent.instrumentation.akkahttp.server; - -import akka.http.scaladsl.model.HttpRequest; -import akka.http.scaladsl.model.HttpResponse; -import akka.http.scaladsl.model.Uri; -import io.opentelemetry.instrumentation.api.instrumenter.net.NetServerAttributesGetter; -import io.opentelemetry.javaagent.instrumentation.akkahttp.AkkaHttpUtil; -import javax.annotation.Nullable; - -class AkkaNetServerAttributesGetter - implements NetServerAttributesGetter { - - @Nullable - @Override - public String getNetworkProtocolName(HttpRequest request, @Nullable HttpResponse httpResponse) { - return AkkaHttpUtil.protocolName(request); - } - - @Nullable - @Override - public String getNetworkProtocolVersion( - HttpRequest request, @Nullable HttpResponse httpResponse) { - return AkkaHttpUtil.protocolVersion(request); - } - - @Nullable - @Override - public String getServerAddress(HttpRequest request) { - Uri.Host host = request.uri().authority().host(); - return host.isEmpty() ? null : host.address(); - } - - @Override - public Integer getServerPort(HttpRequest request) { - return request.uri().authority().port(); - } -} diff --git a/instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/server/route/AkkaHttpServerRouteInstrumentationModule.java b/instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/server/route/AkkaHttpServerRouteInstrumentationModule.java new file mode 100644 index 000000000000..6c484eb968ed --- /dev/null +++ b/instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/server/route/AkkaHttpServerRouteInstrumentationModule.java @@ -0,0 +1,40 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.akkahttp.server.route; + +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; + +/** + * This instrumentation applies to classes in akka-http.jar while + * AkkaHttpServerInstrumentationModule applies to classes in akka-http-core.jar + */ +@AutoService(InstrumentationModule.class) +public class AkkaHttpServerRouteInstrumentationModule extends InstrumentationModule { + public AkkaHttpServerRouteInstrumentationModule() { + super("akka-http", "akka-http-10.0", "akka-http-server", "akka-http-server-route"); + } + + @Override + public boolean isIndyModule() { + // AkkaHttpServerInstrumentationModule and AkkaHttpServerRouteInstrumentationModule share + // AkkaRouteHolder class + return false; + } + + @Override + public List typeInstrumentations() { + return asList( + new PathMatcherInstrumentation(), + new PathMatcherStaticInstrumentation(), + new RouteConcatenationInstrumentation(), + new PathConcatenationInstrumentation()); + } +} diff --git a/instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/server/route/AkkaRouteHolder.java b/instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/server/route/AkkaRouteHolder.java new file mode 100644 index 000000000000..3f192e5c7d2c --- /dev/null +++ b/instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/server/route/AkkaRouteHolder.java @@ -0,0 +1,88 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.akkahttp.server.route; + +import static io.opentelemetry.context.ContextKey.named; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.ContextKey; +import io.opentelemetry.context.ImplicitContextKeyed; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRoute; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRouteSource; +import java.util.ArrayDeque; +import java.util.Deque; + +public class AkkaRouteHolder implements ImplicitContextKeyed { + private static final ContextKey KEY = named("opentelemetry-akka-route"); + + private String route = ""; + private boolean newSegment = true; + private boolean endMatched; + private final Deque stack = new ArrayDeque<>(); + + public static Context init(Context context) { + if (context.get(KEY) != null) { + return context; + } + return context.with(new AkkaRouteHolder()); + } + + public static void push(String path) { + AkkaRouteHolder holder = Context.current().get(KEY); + if (holder != null && holder.newSegment && !holder.endMatched) { + holder.route += path; + holder.newSegment = false; + } + } + + public static void startSegment() { + AkkaRouteHolder holder = Context.current().get(KEY); + if (holder != null) { + holder.newSegment = true; + } + } + + public static void endMatched() { + Context context = Context.current(); + AkkaRouteHolder holder = context.get(KEY); + if (holder != null) { + holder.endMatched = true; + HttpServerRoute.update(context, HttpServerRouteSource.CONTROLLER, holder.route); + } + } + + public static void save() { + AkkaRouteHolder holder = Context.current().get(KEY); + if (holder != null) { + holder.stack.push(holder.route); + holder.newSegment = true; + } + } + + public static void restore() { + AkkaRouteHolder holder = Context.current().get(KEY); + if (holder != null) { + holder.route = holder.stack.pop(); + holder.newSegment = true; + } + } + + // reset the state to when save was called + public static void reset() { + AkkaRouteHolder holder = Context.current().get(KEY); + if (holder != null) { + holder.route = holder.stack.peek(); + holder.newSegment = true; + } + } + + @Override + public Context storeInContext(Context context) { + return context.with(KEY, this); + } + + private AkkaRouteHolder() {} +} diff --git a/instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/server/route/PathConcatenationInstrumentation.java b/instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/server/route/PathConcatenationInstrumentation.java new file mode 100644 index 000000000000..8897a9ebd3bf --- /dev/null +++ b/instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/server/route/PathConcatenationInstrumentation.java @@ -0,0 +1,43 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.akkahttp.server.route; + +import static net.bytebuddy.matcher.ElementMatchers.namedOneOf; + +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 PathConcatenationInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher typeMatcher() { + return namedOneOf( + "akka.http.scaladsl.server.PathMatcher$$anonfun$$tilde$1", + "akka.http.scaladsl.server.PathMatcher"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + namedOneOf("apply", "$anonfun$append$1"), this.getClass().getName() + "$ApplyAdvice"); + } + + @SuppressWarnings("unused") + public static class ApplyAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter() { + // https://github.com/akka/akka-http/blob/0fedb87671ecc450e7378713105ea1dc1d9d0c7d/akka-http/src/main/scala/akka/http/scaladsl/server/PathMatcher.scala#L43 + // https://github.com/akka/akka-http/blob/0fedb87671ecc450e7378713105ea1dc1d9d0c7d/akka-http/src/main/scala/akka/http/scaladsl/server/PathMatcher.scala#L47 + // when routing dsl uses path("path1" / "path2") we are concatenating 3 segments "path1" and / + // and "path2" we need to notify the matcher that a new segment has started, so it could be + // captured in the route + AkkaRouteHolder.startSegment(); + } + } +} diff --git a/instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/server/route/PathMatcherInstrumentation.java b/instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/server/route/PathMatcherInstrumentation.java new file mode 100644 index 000000000000..c0e3ef261c37 --- /dev/null +++ b/instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/server/route/PathMatcherInstrumentation.java @@ -0,0 +1,47 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.akkahttp.server.route; + +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.returns; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import akka.http.scaladsl.model.Uri; +import akka.http.scaladsl.server.PathMatcher; +import io.opentelemetry.instrumentation.api.util.VirtualField; +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 PathMatcherInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher typeMatcher() { + return named("akka.http.scaladsl.server.PathMatcher$"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + named("apply") + .and(takesArgument(0, named("akka.http.scaladsl.model.Uri$Path"))) + .and(returns(named("akka.http.scaladsl.server.PathMatcher"))), + this.getClass().getName() + "$ApplyAdvice"); + } + + @SuppressWarnings("unused") + public static class ApplyAdvice { + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void onEnter( + @Advice.Argument(0) Uri.Path prefix, @Advice.Return PathMatcher result) { + // store the path being matched inside a VirtualField on the given matcher, so it can be used + // for constructing the route + VirtualField.find(PathMatcher.class, String.class).set(result, prefix.toString()); + } + } +} diff --git a/instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/server/route/PathMatcherStaticInstrumentation.java b/instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/server/route/PathMatcherStaticInstrumentation.java new file mode 100644 index 000000000000..41d952ce177e --- /dev/null +++ b/instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/server/route/PathMatcherStaticInstrumentation.java @@ -0,0 +1,65 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.akkahttp.server.route; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.extendsClass; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import akka.http.scaladsl.model.Uri; +import akka.http.scaladsl.server.PathMatcher; +import akka.http.scaladsl.server.PathMatchers; +import io.opentelemetry.instrumentation.api.util.VirtualField; +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 PathMatcherStaticInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher typeMatcher() { + return extendsClass(named("akka.http.scaladsl.server.PathMatcher")); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + named("apply").and(takesArgument(0, named("akka.http.scaladsl.model.Uri$Path"))), + this.getClass().getName() + "$ApplyAdvice"); + } + + @SuppressWarnings("unused") + public static class ApplyAdvice { + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void onExit( + @Advice.This PathMatcher pathMatcher, + @Advice.Argument(0) Uri.Path path, + @Advice.Return PathMatcher.Matching result) { + // result is either matched or unmatched, we only care about the matches + if (result.getClass() == PathMatcher.Matched.class) { + if (PathMatchers.PathEnd$.class == pathMatcher.getClass()) { + AkkaRouteHolder.endMatched(); + return; + } + // if present use the matched path that was remembered in PathMatcherInstrumentation, + // otherwise just use a * + String prefix = VirtualField.find(PathMatcher.class, String.class).get(pathMatcher); + if (prefix == null) { + if (PathMatchers.Slash$.class == pathMatcher.getClass()) { + prefix = "/"; + } else { + prefix = "*"; + } + } + if (prefix != null) { + AkkaRouteHolder.push(prefix); + } + } + } + } +} diff --git a/instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/server/route/RouteConcatenationInstrumentation.java b/instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/server/route/RouteConcatenationInstrumentation.java new file mode 100644 index 000000000000..eb13f402bc1e --- /dev/null +++ b/instrumentation/akka/akka-http-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/akkahttp/server/route/RouteConcatenationInstrumentation.java @@ -0,0 +1,67 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.akkahttp.server.route; + +import static net.bytebuddy.matcher.ElementMatchers.namedOneOf; + +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 RouteConcatenationInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher typeMatcher() { + return namedOneOf( + // scala 2.11 + "akka.http.scaladsl.server.RouteConcatenation$RouteWithConcatenation$$anonfun$$tilde$1", + // scala 2.12 and later + "akka.http.scaladsl.server.RouteConcatenation$RouteWithConcatenation"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + namedOneOf( + // scala 2.11 + "apply", + // scala 2.12 and later + "$anonfun$$tilde$1"), + this.getClass().getName() + "$ApplyAdvice"); + + // This advice seems to be only needed when defining routes with java dsl. Since java dsl tests + // use scala 2.12 we are going to skip instrumenting this for scala 2.11. + transformer.applyAdviceToMethod( + namedOneOf("$anonfun$$tilde$2"), this.getClass().getName() + "$Apply2Advice"); + } + + @SuppressWarnings("unused") + public static class ApplyAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter() { + // when routing dsl uses concat(path(...) {...}, path(...) {...}) we'll restore the currently + // matched route after each matcher so that match attempts that failed wouldn't get recorded + // in the route + AkkaRouteHolder.save(); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void onExit() { + AkkaRouteHolder.restore(); + } + } + + @SuppressWarnings("unused") + public static class Apply2Advice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter() { + AkkaRouteHolder.reset(); + } + } +} diff --git a/instrumentation/akka/akka-http-10.0/javaagent/src/test/scala/io/opentelemetry/javaagent/instrumentation/akkahttp/AbstractHttpServerInstrumentationTest.scala b/instrumentation/akka/akka-http-10.0/javaagent/src/test/scala/io/opentelemetry/javaagent/instrumentation/akkahttp/AbstractHttpServerInstrumentationTest.scala index 58b98f31d16b..785ccf8f5f6b 100644 --- a/instrumentation/akka/akka-http-10.0/javaagent/src/test/scala/io/opentelemetry/javaagent/instrumentation/akkahttp/AbstractHttpServerInstrumentationTest.scala +++ b/instrumentation/akka/akka-http-10.0/javaagent/src/test/scala/io/opentelemetry/javaagent/instrumentation/akkahttp/AbstractHttpServerInstrumentationTest.scala @@ -11,9 +11,9 @@ import io.opentelemetry.instrumentation.testing.junit.http.{ HttpServerTestOptions, ServerEndpoint } +import io.opentelemetry.semconv.HttpAttributes import java.util -import java.util.Collections import java.util.function.{Function, Predicate} abstract class AbstractHttpServerInstrumentationTest @@ -25,8 +25,13 @@ abstract class AbstractHttpServerInstrumentationTest options.setTestCaptureHttpHeaders(false) options.setHttpAttributes( new Function[ServerEndpoint, util.Set[AttributeKey[_]]] { - override def apply(v1: ServerEndpoint): util.Set[AttributeKey[_]] = - Collections.emptySet() + override def apply(v1: ServerEndpoint): util.Set[AttributeKey[_]] = { + val set = new util.HashSet[AttributeKey[_]]( + HttpServerTestOptions.DEFAULT_HTTP_ATTRIBUTES + ) + set.remove(HttpAttributes.HTTP_ROUTE) + set + } } ) options.setHasResponseCustomizer( @@ -35,5 +40,7 @@ abstract class AbstractHttpServerInstrumentationTest t != ServerEndpoint.EXCEPTION } ) + // instrumentation does not create a span at all + options.disableTestNonStandardHttpMethod } } diff --git a/instrumentation/akka/akka-http-10.0/javaagent/src/test/scala/io/opentelemetry/javaagent/instrumentation/akkahttp/AkkaHttpClientInstrumentationTest.scala b/instrumentation/akka/akka-http-10.0/javaagent/src/test/scala/io/opentelemetry/javaagent/instrumentation/akkahttp/AkkaHttpClientInstrumentationTest.scala index 224b23b85e38..23e17e17c450 100644 --- a/instrumentation/akka/akka-http-10.0/javaagent/src/test/scala/io/opentelemetry/javaagent/instrumentation/akkahttp/AkkaHttpClientInstrumentationTest.scala +++ b/instrumentation/akka/akka-http-10.0/javaagent/src/test/scala/io/opentelemetry/javaagent/instrumentation/akkahttp/AkkaHttpClientInstrumentationTest.scala @@ -117,6 +117,7 @@ class AkkaHttpClientInstrumentationTest options: HttpClientTestOptions.Builder ): Unit = { options.disableTestRedirects() + options.disableTestNonStandardHttpMethod() // singleConnection test would require instrumentation to support requests made through pools // (newHostConnectionPool, superPool, etc), which is currently not supported. options.setSingleConnectionFactory( diff --git a/instrumentation/akka/akka-http-10.0/javaagent/src/test/scala/io/opentelemetry/javaagent/instrumentation/akkahttp/AkkaHttpServerInstrumentationTest.scala b/instrumentation/akka/akka-http-10.0/javaagent/src/test/scala/io/opentelemetry/javaagent/instrumentation/akkahttp/AkkaHttpServerInstrumentationTest.scala index 2001f3335f9f..8a340318475b 100644 --- a/instrumentation/akka/akka-http-10.0/javaagent/src/test/scala/io/opentelemetry/javaagent/instrumentation/akkahttp/AkkaHttpServerInstrumentationTest.scala +++ b/instrumentation/akka/akka-http-10.0/javaagent/src/test/scala/io/opentelemetry/javaagent/instrumentation/akkahttp/AkkaHttpServerInstrumentationTest.scala @@ -5,13 +5,26 @@ package io.opentelemetry.javaagent.instrumentation.akkahttp +import io.opentelemetry.api.common.AttributeKey import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension +import io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.SUCCESS import io.opentelemetry.instrumentation.testing.junit.http.{ HttpServerInstrumentationExtension, - HttpServerTestOptions + HttpServerTestOptions, + ServerEndpoint } +import io.opentelemetry.sdk.testing.assertj.{SpanDataAssert, TraceAssert} +import io.opentelemetry.testing.internal.armeria.common.{ + AggregatedHttpRequest, + HttpMethod +} +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.RegisterExtension +import java.util +import java.util.function.{BiFunction, Consumer, Function} + class AkkaHttpServerInstrumentationTest extends AbstractHttpServerInstrumentationTest { @RegisterExtension val extension: InstrumentationExtension = @@ -31,5 +44,66 @@ class AkkaHttpServerInstrumentationTest super.configure(options) // exception doesn't propagate options.setTestException(false) + options.setTestPathParam(true) + + options.setHttpAttributes( + new Function[ServerEndpoint, util.Set[AttributeKey[_]]] { + override def apply(v1: ServerEndpoint): util.Set[AttributeKey[_]] = { + HttpServerTestOptions.DEFAULT_HTTP_ATTRIBUTES + } + } + ) + + val expectedRoute = new BiFunction[ServerEndpoint, String, String] { + def apply(endpoint: ServerEndpoint, method: String): String = { + if (endpoint eq ServerEndpoint.PATH_PARAM) + return "/path/*/param" + expectedHttpRoute(endpoint, method) + } + } + options.setExpectedHttpRoute(expectedRoute) + } + + @Test def testPathMatchers(): Unit = { + // /test1 / IntNumber / HexIntNumber / LongNumber / HexLongNumber / DoubleNumber / JavaUUID / Remaining + val request = AggregatedHttpRequest.of( + HttpMethod.GET, + address + .resolve( + "/test1/1/a1/2/b2/3.0/e58ed763-928c-4155-bee9-fdbaaadc15f3/remaining" + ) + .toString + ) + val response = client.execute(request).aggregate.join + assertThat(response.status.code).isEqualTo(SUCCESS.getStatus) + assertThat(response.contentUtf8).isEqualTo(SUCCESS.getBody) + + testing.waitAndAssertTraces(new Consumer[TraceAssert] { + override def accept(trace: TraceAssert): Unit = + trace.hasSpansSatisfyingExactly(new Consumer[SpanDataAssert] { + override def accept(span: SpanDataAssert): Unit = { + span.hasName("GET /test1/*/*/*/*/*/*/*") + } + }) + }) + } + + @Test def testConcat(): Unit = { + val request = AggregatedHttpRequest.of( + HttpMethod.GET, + address.resolve("/test2/second").toString + ) + val response = client.execute(request).aggregate.join + assertThat(response.status.code).isEqualTo(SUCCESS.getStatus) + assertThat(response.contentUtf8).isEqualTo(SUCCESS.getBody) + + testing.waitAndAssertTraces(new Consumer[TraceAssert] { + override def accept(trace: TraceAssert): Unit = + trace.hasSpansSatisfyingExactly(new Consumer[SpanDataAssert] { + override def accept(span: SpanDataAssert): Unit = { + span.hasName("GET /test2/second") + } + }) + }) } } diff --git a/instrumentation/akka/akka-http-10.0/javaagent/src/test/scala/io/opentelemetry/javaagent/instrumentation/akkahttp/AkkaHttpServerRouteTest.scala b/instrumentation/akka/akka-http-10.0/javaagent/src/test/scala/io/opentelemetry/javaagent/instrumentation/akkahttp/AkkaHttpServerRouteTest.scala new file mode 100644 index 000000000000..3920c6aeb15e --- /dev/null +++ b/instrumentation/akka/akka-http-10.0/javaagent/src/test/scala/io/opentelemetry/javaagent/instrumentation/akkahttp/AkkaHttpServerRouteTest.scala @@ -0,0 +1,113 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.akkahttp + +import akka.actor.ActorSystem +import akka.http.scaladsl.Http +import akka.http.scaladsl.server.Directives.{ + IntNumber, + complete, + concat, + path, + pathEndOrSingleSlash, + pathPrefix, + pathSingleSlash +} +import akka.http.scaladsl.server.Route +import akka.stream.ActorMaterializer +import io.opentelemetry.instrumentation.test.utils.PortUtils +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension +import io.opentelemetry.sdk.testing.assertj.{SpanDataAssert, TraceAssert} +import io.opentelemetry.testing.internal.armeria.client.WebClient +import io.opentelemetry.testing.internal.armeria.common.{ + AggregatedHttpRequest, + HttpMethod +} +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.{AfterAll, Test, TestInstance} +import org.junit.jupiter.api.extension.RegisterExtension + +import java.net.{URI, URISyntaxException} +import java.util.function.Consumer +import scala.concurrent.duration.DurationInt +import scala.concurrent.Await + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class AkkaHttpServerRouteTest { + @RegisterExtension private val testing: AgentInstrumentationExtension = + AgentInstrumentationExtension.create + private val client: WebClient = WebClient.of() + + implicit val system: ActorSystem = ActorSystem("my-system") + implicit val materializer: ActorMaterializer = ActorMaterializer() + + private def buildAddress(port: Int): URI = try + new URI("http://localhost:" + port + "/") + catch { + case exception: URISyntaxException => + throw new IllegalStateException(exception) + } + + @Test def testSimple(): Unit = { + val route = path("test") { + complete("ok") + } + + test(route, "/test", "GET /test") + } + + @Test def testRoute(): Unit = { + val route = concat( + pathEndOrSingleSlash { + complete("root") + }, + pathPrefix("test") { + concat( + pathSingleSlash { + complete("test") + }, + path(IntNumber) { _ => + complete("ok") + } + ) + } + ) + + test(route, "/test/1", "GET /test/*") + } + + def test(route: Route, path: String, spanName: String): Unit = { + val port = PortUtils.findOpenPort + val address: URI = buildAddress(port) + val binding = + Await.result(Http().bindAndHandle(route, "localhost", port), 10.seconds) + try { + val request = AggregatedHttpRequest.of( + HttpMethod.GET, + address.resolve(path).toString + ) + val response = client.execute(request).aggregate.join + assertThat(response.status.code).isEqualTo(200) + assertThat(response.contentUtf8).isEqualTo("ok") + + testing.waitAndAssertTraces(new Consumer[TraceAssert] { + override def accept(trace: TraceAssert): Unit = + trace.hasSpansSatisfyingExactly(new Consumer[SpanDataAssert] { + override def accept(span: SpanDataAssert): Unit = { + span.hasName(spanName) + } + }) + }) + } finally { + binding.unbind() + } + } + + @AfterAll + def cleanUp(): Unit = { + system.terminate() + } +} diff --git a/instrumentation/akka/akka-http-10.0/javaagent/src/test/scala/io/opentelemetry/javaagent/instrumentation/akkahttp/AkkaHttpTestServerSourceWebServer.scala b/instrumentation/akka/akka-http-10.0/javaagent/src/test/scala/io/opentelemetry/javaagent/instrumentation/akkahttp/AkkaHttpTestServerSourceWebServer.scala new file mode 100644 index 000000000000..a9af27787d3b --- /dev/null +++ b/instrumentation/akka/akka-http-10.0/javaagent/src/test/scala/io/opentelemetry/javaagent/instrumentation/akkahttp/AkkaHttpTestServerSourceWebServer.scala @@ -0,0 +1,124 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.akkahttp + +import akka.actor.ActorSystem +import akka.http.scaladsl.Http +import akka.http.scaladsl.Http.ServerBinding +import akka.http.scaladsl.model.StatusCodes.Found +import akka.http.scaladsl.server.Directives._ +import akka.stream.ActorMaterializer +import akka.stream.scaladsl.Sink +import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpServerTest +import io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint._ + +import java.util.function.Supplier +import scala.concurrent.Await + +object AkkaHttpTestServerSourceWebServer { + implicit val system = ActorSystem("my-system") + implicit val materializer = ActorMaterializer() + // needed for the future flatMap/onComplete in the end + implicit val executionContext = system.dispatcher + + var route = get { + concat( + path(SUCCESS.rawPath()) { + complete( + AbstractHttpServerTest.controller(SUCCESS, supplier(SUCCESS.getBody)) + ) + }, + path(INDEXED_CHILD.rawPath()) { + parameterMap { map => + val supplier = new Supplier[String] { + def get(): String = { + INDEXED_CHILD.collectSpanAttributes(new UrlParameterProvider { + override def getParameter(name: String): String = + map.get(name).orNull + }) + "" + } + } + complete(AbstractHttpServerTest.controller(INDEXED_CHILD, supplier)) + } + }, + path(QUERY_PARAM.rawPath()) { + extractUri { uri => + complete( + AbstractHttpServerTest + .controller(INDEXED_CHILD, supplier(uri.queryString().orNull)) + ) + } + }, + path(REDIRECT.rawPath()) { + redirect( + AbstractHttpServerTest + .controller(REDIRECT, supplier(REDIRECT.getBody)), + Found + ) + }, + path(ERROR.rawPath()) { + complete( + 500 -> AbstractHttpServerTest + .controller(ERROR, supplier(ERROR.getBody)) + ) + }, + path("path" / LongNumber / "param") { id => + complete( + AbstractHttpServerTest.controller(PATH_PARAM, supplier(id.toString)) + ) + }, + path( + "test1" / IntNumber / HexIntNumber / LongNumber / HexLongNumber / + DoubleNumber / JavaUUID / Remaining + ) { (_, _, _, _, _, _, _) => + complete(SUCCESS.getBody) + }, + pathPrefix("test2") { + concat( + path("first") { + complete(SUCCESS.getBody) + }, + path("second") { + complete(SUCCESS.getBody) + } + ) + } + ) + } + + private var binding: ServerBinding = null + + def start(port: Int): Unit = synchronized { + if (null == binding) { + import scala.concurrent.duration._ + binding = Await.result( + Http() + .bind("localhost", port) + .map(_.handleWith(route)) + .to(Sink.ignore) + .run(), + 10.seconds + ) + } + } + + def stop(): Unit = synchronized { + if (null != binding) { + binding.unbind() + system.terminate() + binding = null + } + } + + def supplier(string: String): Supplier[String] = { + new Supplier[String] { + def get(): String = { + string + } + } + } +} diff --git a/instrumentation/akka/akka-http-10.0/javaagent/src/test/scala/io/opentelemetry/javaagent/instrumentation/akkahttp/AkkaHttpTestWebServer.scala b/instrumentation/akka/akka-http-10.0/javaagent/src/test/scala/io/opentelemetry/javaagent/instrumentation/akkahttp/AkkaHttpTestWebServer.scala index 69fffed25314..b17c3b65a934 100644 --- a/instrumentation/akka/akka-http-10.0/javaagent/src/test/scala/io/opentelemetry/javaagent/instrumentation/akkahttp/AkkaHttpTestWebServer.scala +++ b/instrumentation/akka/akka-http-10.0/javaagent/src/test/scala/io/opentelemetry/javaagent/instrumentation/akkahttp/AkkaHttpTestWebServer.scala @@ -8,14 +8,10 @@ package io.opentelemetry.javaagent.instrumentation.akkahttp import akka.actor.ActorSystem import akka.http.scaladsl.Http import akka.http.scaladsl.Http.ServerBinding -import akka.http.scaladsl.model._ +import akka.http.scaladsl.model.StatusCodes.Found import akka.http.scaladsl.server.Directives._ -import akka.http.scaladsl.server.ExceptionHandler import akka.stream.ActorMaterializer -import io.opentelemetry.instrumentation.testing.junit.http.{ - AbstractHttpServerTest, - ServerEndpoint -} +import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpServerTest import io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint._ import java.util.function.Supplier @@ -27,43 +23,70 @@ object AkkaHttpTestWebServer { // 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) { - extractUri { uri => - val endpoint = ServerEndpoint.forPath(uri.path.toString()) - complete { - AbstractHttpServerTest.controller( - endpoint, - new Supplier[HttpResponse] { - def get(): 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) - } + var route = get { + concat( + path(SUCCESS.rawPath()) { + complete( + AbstractHttpServerTest.controller(SUCCESS, supplier(SUCCESS.getBody)) + ) + }, + path(INDEXED_CHILD.rawPath()) { + parameterMap { map => + val supplier = new Supplier[String] { + def get(): String = { + INDEXED_CHILD.collectSpanAttributes(new UrlParameterProvider { + override def getParameter(name: String): String = + map.get(name).orNull + }) + "" } } + complete(AbstractHttpServerTest.controller(INDEXED_CHILD, supplier)) + } + }, + path(QUERY_PARAM.rawPath()) { + extractUri { uri => + complete( + AbstractHttpServerTest + .controller(INDEXED_CHILD, supplier(uri.queryString().orNull)) + ) + } + }, + path(REDIRECT.rawPath()) { + redirect( + AbstractHttpServerTest + .controller(REDIRECT, supplier(REDIRECT.getBody)), + Found + ) + }, + path(ERROR.rawPath()) { + complete( + 500 -> AbstractHttpServerTest + .controller(ERROR, supplier(ERROR.getBody)) + ) + }, + path("path" / LongNumber / "param") { id => + complete( + AbstractHttpServerTest.controller(PATH_PARAM, supplier(id.toString)) + ) + }, + path( + "test1" / IntNumber / HexIntNumber / LongNumber / HexLongNumber / + DoubleNumber / JavaUUID / Remaining + ) { (_, _, _, _, _, _, _) => + complete(SUCCESS.getBody) + }, + pathPrefix("test2") { + concat( + path("first") { + complete(SUCCESS.getBody) + }, + path("second") { + complete(SUCCESS.getBody) + } ) } - } + ) } private var binding: ServerBinding = null @@ -83,4 +106,12 @@ object AkkaHttpTestWebServer { binding = null } } + + def supplier(string: String): Supplier[String] = { + new Supplier[String] { + def get(): String = { + string + } + } + } } diff --git a/instrumentation/alibaba-druid-1.0/javaagent/build.gradle.kts b/instrumentation/alibaba-druid-1.0/javaagent/build.gradle.kts new file mode 100644 index 000000000000..b8a2b3eb8d7c --- /dev/null +++ b/instrumentation/alibaba-druid-1.0/javaagent/build.gradle.kts @@ -0,0 +1,20 @@ +plugins { + id("otel.javaagent-instrumentation") +} + +muzzle { + pass { + group.set("com.alibaba") + module.set("druid") + versions.set("(,)") + skip("1.0.30") + } +} + +dependencies { + library("com.alibaba:druid:1.0.0") + + implementation(project(":instrumentation:alibaba-druid-1.0:library")) + + testImplementation(project(":instrumentation:alibaba-druid-1.0:testing")) +} diff --git a/instrumentation/alibaba-druid-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/alibabadruid/v1_0/DruidDataSourceInstrumentation.java b/instrumentation/alibaba-druid-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/alibabadruid/v1_0/DruidDataSourceInstrumentation.java new file mode 100644 index 000000000000..054ebbefd11e --- /dev/null +++ b/instrumentation/alibaba-druid-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/alibabadruid/v1_0/DruidDataSourceInstrumentation.java @@ -0,0 +1,60 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.alibabadruid.v1_0; + +import static io.opentelemetry.javaagent.instrumentation.alibabadruid.v1_0.DruidSingletons.telemetry; +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 com.alibaba.druid.pool.DruidDataSourceMBean; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import javax.management.ObjectName; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class DruidDataSourceInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher typeMatcher() { + return named("com.alibaba.druid.stat.DruidDataSourceStatManager"); + } + + @Override + public void transform(TypeTransformer typeTransformer) { + typeTransformer.applyAdviceToMethod( + isMethod().and(isPublic()).and(isStatic()).and(named("addDataSource")), + this.getClass().getName() + "$AddDataSourceAdvice"); + + typeTransformer.applyAdviceToMethod( + isMethod().and(isPublic()).and(isStatic()).and(named("removeDataSource")), + this.getClass().getName() + "$RemoveDataSourceAdvice"); + } + + @SuppressWarnings("unused") + public static class AddDataSourceAdvice { + + @Advice.OnMethodExit(suppress = Throwable.class) + public static void onExit( + @Advice.Argument(0) Object dataSource, @Advice.Return ObjectName objectName) { + DruidDataSourceMBean druidDataSource = (DruidDataSourceMBean) dataSource; + String dataSourceName = + objectName.getKeyProperty("type") + "-" + objectName.getKeyProperty("id"); + telemetry().registerMetrics(druidDataSource, dataSourceName); + } + } + + @SuppressWarnings("unused") + public static class RemoveDataSourceAdvice { + @Advice.OnMethodExit(suppress = Throwable.class) + public static void onExit(@Advice.Argument(0) Object dataSource) { + DruidDataSourceMBean druidDataSource = (DruidDataSourceMBean) dataSource; + telemetry().unregisterMetrics(druidDataSource); + } + } +} diff --git a/instrumentation/jaxrs-client/jaxrs-client-1.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrsclient/v1_1/JaxRsClientInstrumentationModule.java b/instrumentation/alibaba-druid-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/alibabadruid/v1_0/DruidInstrumentationModule.java similarity index 62% rename from instrumentation/jaxrs-client/jaxrs-client-1.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrsclient/v1_1/JaxRsClientInstrumentationModule.java rename to instrumentation/alibaba-druid-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/alibabadruid/v1_0/DruidInstrumentationModule.java index 86c731c99a55..f2dbf33fe434 100644 --- a/instrumentation/jaxrs-client/jaxrs-client-1.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrsclient/v1_1/JaxRsClientInstrumentationModule.java +++ b/instrumentation/alibaba-druid-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/alibabadruid/v1_0/DruidInstrumentationModule.java @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.javaagent.instrumentation.jaxrsclient.v1_1; +package io.opentelemetry.javaagent.instrumentation.alibabadruid.v1_0; import static java.util.Collections.singletonList; @@ -13,14 +13,14 @@ import java.util.List; @AutoService(InstrumentationModule.class) -public class JaxRsClientInstrumentationModule extends InstrumentationModule { +public class DruidInstrumentationModule extends InstrumentationModule { - public JaxRsClientInstrumentationModule() { - super("jaxrs-client", "jaxrs-client-1.1"); + public DruidInstrumentationModule() { + super("alibaba-druid", "alibaba-druid-1.0"); } @Override public List typeInstrumentations() { - return singletonList(new ClientHandlerInstrumentation()); + return singletonList(new DruidDataSourceInstrumentation()); } } diff --git a/instrumentation/alibaba-druid-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/alibabadruid/v1_0/DruidSingletons.java b/instrumentation/alibaba-druid-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/alibabadruid/v1_0/DruidSingletons.java new file mode 100644 index 000000000000..05242c2b1abb --- /dev/null +++ b/instrumentation/alibaba-druid-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/alibabadruid/v1_0/DruidSingletons.java @@ -0,0 +1,21 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.alibabadruid.v1_0; + +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.instrumentation.alibabadruid.v1_0.DruidTelemetry; + +public final class DruidSingletons { + + private static final DruidTelemetry druidTelemetry = + DruidTelemetry.create(GlobalOpenTelemetry.get()); + + public static DruidTelemetry telemetry() { + return druidTelemetry; + } + + private DruidSingletons() {} +} diff --git a/instrumentation/alibaba-druid-1.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/alibabadruid/v1_0/DruidInstrumentationTest.java b/instrumentation/alibaba-druid-1.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/alibabadruid/v1_0/DruidInstrumentationTest.java new file mode 100644 index 000000000000..7514f5249710 --- /dev/null +++ b/instrumentation/alibaba-druid-1.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/alibabadruid/v1_0/DruidInstrumentationTest.java @@ -0,0 +1,34 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.alibabadruid.v1_0; + +import com.alibaba.druid.pool.DruidDataSource; +import com.alibaba.druid.stat.DruidDataSourceStatManager; +import io.opentelemetry.instrumentation.alibabadruid.AbstractDruidInstrumentationTest; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import org.junit.jupiter.api.extension.RegisterExtension; + +class DruidInstrumentationTest extends AbstractDruidInstrumentationTest { + + @RegisterExtension + static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + @Override + protected InstrumentationExtension testing() { + return testing; + } + + @Override + protected void configure(DruidDataSource dataSource, String name) throws Exception { + DruidDataSourceStatManager.addDataSource(dataSource, name); + } + + @Override + protected void shutdown(DruidDataSource dataSource) throws Exception { + DruidDataSourceStatManager.removeDataSource(dataSource); + } +} diff --git a/instrumentation/alibaba-druid-1.0/library/build.gradle.kts b/instrumentation/alibaba-druid-1.0/library/build.gradle.kts new file mode 100644 index 000000000000..0711495271de --- /dev/null +++ b/instrumentation/alibaba-druid-1.0/library/build.gradle.kts @@ -0,0 +1,10 @@ +plugins { + id("otel.library-instrumentation") + id("otel.nullaway-conventions") +} + +dependencies { + library("com.alibaba:druid:1.0.0") + + testImplementation(project(":instrumentation:alibaba-druid-1.0:testing")) +} diff --git a/instrumentation/alibaba-druid-1.0/library/src/main/java/io/opentelemetry/instrumentation/alibabadruid/v1_0/ConnectionPoolMetrics.java b/instrumentation/alibaba-druid-1.0/library/src/main/java/io/opentelemetry/instrumentation/alibabadruid/v1_0/ConnectionPoolMetrics.java new file mode 100644 index 000000000000..6b6bfad78f99 --- /dev/null +++ b/instrumentation/alibaba-druid-1.0/library/src/main/java/io/opentelemetry/instrumentation/alibabadruid/v1_0/ConnectionPoolMetrics.java @@ -0,0 +1,66 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.alibabadruid.v1_0; + +import com.alibaba.druid.pool.DruidDataSourceMBean; +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.metrics.BatchCallback; +import io.opentelemetry.api.metrics.ObservableLongMeasurement; +import io.opentelemetry.instrumentation.api.incubator.semconv.db.DbConnectionPoolMetrics; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +final class ConnectionPoolMetrics { + + private static final String INSTRUMENTATION_NAME = "io.opentelemetry.alibaba-druid-1.0"; + + private static final Map dataSourceMetrics = + new ConcurrentHashMap<>(); + + public static void registerMetrics( + OpenTelemetry openTelemetry, DruidDataSourceMBean dataSource, String dataSourceName) { + DbConnectionPoolMetrics metrics = + DbConnectionPoolMetrics.create(openTelemetry, INSTRUMENTATION_NAME, dataSourceName); + + ObservableLongMeasurement connections = metrics.connections(); + ObservableLongMeasurement minIdleConnections = metrics.minIdleConnections(); + ObservableLongMeasurement maxIdleConnections = metrics.maxIdleConnections(); + ObservableLongMeasurement maxConnections = metrics.maxConnections(); + ObservableLongMeasurement pendingRequestsForConnection = metrics.pendingRequestsForConnection(); + + Attributes attributes = metrics.getAttributes(); + Attributes usedConnectionsAttributes = metrics.getUsedConnectionsAttributes(); + Attributes idleConnectionsAttributes = metrics.getIdleConnectionsAttributes(); + + BatchCallback callback = + metrics.batchCallback( + () -> { + connections.record(dataSource.getActiveCount(), usedConnectionsAttributes); + connections.record(dataSource.getPoolingCount(), idleConnectionsAttributes); + pendingRequestsForConnection.record(dataSource.getWaitThreadCount(), attributes); + minIdleConnections.record(dataSource.getMinIdle(), attributes); + maxIdleConnections.record(dataSource.getMaxIdle(), attributes); + maxConnections.record(dataSource.getMaxActive(), attributes); + }, + connections, + pendingRequestsForConnection, + minIdleConnections, + maxIdleConnections, + maxConnections); + + dataSourceMetrics.put(dataSource, callback); + } + + public static void unregisterMetrics(DruidDataSourceMBean dataSource) { + BatchCallback callback = dataSourceMetrics.remove(dataSource); + if (callback != null) { + callback.close(); + } + } + + private ConnectionPoolMetrics() {} +} diff --git a/instrumentation/alibaba-druid-1.0/library/src/main/java/io/opentelemetry/instrumentation/alibabadruid/v1_0/DruidTelemetry.java b/instrumentation/alibaba-druid-1.0/library/src/main/java/io/opentelemetry/instrumentation/alibabadruid/v1_0/DruidTelemetry.java new file mode 100644 index 000000000000..02b1103627d9 --- /dev/null +++ b/instrumentation/alibaba-druid-1.0/library/src/main/java/io/opentelemetry/instrumentation/alibabadruid/v1_0/DruidTelemetry.java @@ -0,0 +1,30 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.alibabadruid.v1_0; + +import com.alibaba.druid.pool.DruidDataSourceMBean; +import io.opentelemetry.api.OpenTelemetry; + +public final class DruidTelemetry { + + public static DruidTelemetry create(OpenTelemetry openTelemetry) { + return new DruidTelemetry(openTelemetry); + } + + private final OpenTelemetry openTelemetry; + + private DruidTelemetry(OpenTelemetry openTelemetry) { + this.openTelemetry = openTelemetry; + } + + public void registerMetrics(DruidDataSourceMBean dataSource, String dataSourceName) { + ConnectionPoolMetrics.registerMetrics(openTelemetry, dataSource, dataSourceName); + } + + public void unregisterMetrics(DruidDataSourceMBean dataSource) { + ConnectionPoolMetrics.unregisterMetrics(dataSource); + } +} diff --git a/instrumentation/alibaba-druid-1.0/library/src/test/java/io/opentelemetry/instrumentation/alibabadruid/v1_0/DruidInstrumentationTest.java b/instrumentation/alibaba-druid-1.0/library/src/test/java/io/opentelemetry/instrumentation/alibabadruid/v1_0/DruidInstrumentationTest.java new file mode 100644 index 000000000000..6d182b968c5a --- /dev/null +++ b/instrumentation/alibaba-druid-1.0/library/src/test/java/io/opentelemetry/instrumentation/alibabadruid/v1_0/DruidInstrumentationTest.java @@ -0,0 +1,44 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.alibabadruid.v1_0; + +import com.alibaba.druid.pool.DruidDataSource; +import io.opentelemetry.instrumentation.alibabadruid.AbstractDruidInstrumentationTest; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.LibraryInstrumentationExtension; +import javax.management.ObjectName; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.extension.RegisterExtension; + +class DruidInstrumentationTest extends AbstractDruidInstrumentationTest { + + @RegisterExtension + static final InstrumentationExtension testing = LibraryInstrumentationExtension.create(); + + private static DruidTelemetry telemetry; + + @Override + protected InstrumentationExtension testing() { + return testing; + } + + @BeforeAll + static void setup() { + telemetry = DruidTelemetry.create(testing.getOpenTelemetry()); + } + + @Override + protected void configure(DruidDataSource dataSource, String name) throws Exception { + ObjectName objectName = new ObjectName("com.alibaba.druid:type=DruidDataSource,id=" + name); + telemetry.registerMetrics( + dataSource, objectName.getKeyProperty("type") + "-" + objectName.getKeyProperty("id")); + } + + @Override + protected void shutdown(DruidDataSource dataSource) throws Exception { + telemetry.unregisterMetrics(dataSource); + } +} diff --git a/instrumentation/alibaba-druid-1.0/testing/build.gradle.kts b/instrumentation/alibaba-druid-1.0/testing/build.gradle.kts new file mode 100644 index 000000000000..52c1bfd3adb1 --- /dev/null +++ b/instrumentation/alibaba-druid-1.0/testing/build.gradle.kts @@ -0,0 +1,11 @@ +plugins { + id("otel.java-conventions") +} + +dependencies { + api(project(":testing-common")) + api("org.mockito:mockito-core") + api("org.mockito:mockito-junit-jupiter") + + compileOnly("com.alibaba:druid:1.0.0") +} diff --git a/instrumentation/alibaba-druid-1.0/testing/src/main/java/io/opentelemetry/instrumentation/alibabadruid/AbstractDruidInstrumentationTest.java b/instrumentation/alibaba-druid-1.0/testing/src/main/java/io/opentelemetry/instrumentation/alibabadruid/AbstractDruidInstrumentationTest.java new file mode 100644 index 000000000000..947a7aa095f2 --- /dev/null +++ b/instrumentation/alibaba-druid-1.0/testing/src/main/java/io/opentelemetry/instrumentation/alibabadruid/AbstractDruidInstrumentationTest.java @@ -0,0 +1,88 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.alibabadruid; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.alibaba.druid.pool.DruidDataSource; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.db.DbConnectionPoolMetricsAssertions; +import io.opentelemetry.instrumentation.testing.junit.db.MockDriver; +import java.sql.SQLException; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; +import javax.management.ObjectName; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +public abstract class AbstractDruidInstrumentationTest { + + private static final String INSTRUMENTATION_NAME = "io.opentelemetry.alibaba-druid-1.0"; + + protected abstract InstrumentationExtension testing(); + + protected abstract void configure(DruidDataSource dataSource, String dataSourceName) + throws Exception; + + protected abstract void shutdown(DruidDataSource dataSource) throws Exception; + + @BeforeAll + static void setUpMocks() throws SQLException { + MockDriver.register(); + } + + @Test + void shouldReportMetrics() throws Exception { + String name = "dataSourceName"; + DruidDataSource dataSource = new DruidDataSource(); + dataSource.setDriverClassName(MockDriver.class.getName()); + dataSource.setUrl("db:///url"); + dataSource.setTestWhileIdle(false); + configure(dataSource, name); + + // then + ObjectName objectName = new ObjectName("com.alibaba.druid:type=DruidDataSource,id=" + name); + + DbConnectionPoolMetricsAssertions.create( + testing(), + INSTRUMENTATION_NAME, + objectName.getKeyProperty("type") + "-" + objectName.getKeyProperty("id")) + .disableConnectionTimeouts() + .disableCreateTime() + .disableWaitTime() + .disableUseTime() + .assertConnectionPoolEmitsMetrics(); + + // when + dataSource.close(); + shutdown(dataSource); + + // sleep exporter interval + Thread.sleep(100); + testing().clearData(); + Thread.sleep(100); + + // then + Set metricNames = + new HashSet<>( + Arrays.asList( + "db.client.connections.usage", + "db.client.connections.idle.min", + "db.client.connections.idle.max", + "db.client.connections.max", + "db.client.connections.pending_requests")); + assertThat(testing().metrics()) + .filteredOn( + metricData -> + metricData.getInstrumentationScopeInfo().getName().equals(INSTRUMENTATION_NAME) + && metricNames.contains(metricData.getName())) + .isEmpty(); + } +} diff --git a/instrumentation/apache-dbcp-2.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apachedbcp/v2_0/ApacheDbcpInstrumentationTest.java b/instrumentation/apache-dbcp-2.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apachedbcp/v2_0/ApacheDbcpInstrumentationTest.java index db961af943bd..6587741c1e0a 100644 --- a/instrumentation/apache-dbcp-2.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apachedbcp/v2_0/ApacheDbcpInstrumentationTest.java +++ b/instrumentation/apache-dbcp-2.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apachedbcp/v2_0/ApacheDbcpInstrumentationTest.java @@ -13,7 +13,7 @@ import org.apache.commons.dbcp2.BasicDataSource; import org.junit.jupiter.api.extension.RegisterExtension; -public class ApacheDbcpInstrumentationTest extends AbstractApacheDbcpInstrumentationTest { +class ApacheDbcpInstrumentationTest extends AbstractApacheDbcpInstrumentationTest { @RegisterExtension static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); diff --git a/instrumentation/apache-dbcp-2.0/library/src/main/java/io/opentelemetry/instrumentation/apachedbcp/v2_0/DataSourceMetrics.java b/instrumentation/apache-dbcp-2.0/library/src/main/java/io/opentelemetry/instrumentation/apachedbcp/v2_0/DataSourceMetrics.java index 16f75318cc24..81efbecab133 100644 --- a/instrumentation/apache-dbcp-2.0/library/src/main/java/io/opentelemetry/instrumentation/apachedbcp/v2_0/DataSourceMetrics.java +++ b/instrumentation/apache-dbcp-2.0/library/src/main/java/io/opentelemetry/instrumentation/apachedbcp/v2_0/DataSourceMetrics.java @@ -9,7 +9,7 @@ import io.opentelemetry.api.common.Attributes; import io.opentelemetry.api.metrics.BatchCallback; import io.opentelemetry.api.metrics.ObservableLongMeasurement; -import io.opentelemetry.instrumentation.api.metrics.db.DbConnectionPoolMetrics; +import io.opentelemetry.instrumentation.api.incubator.semconv.db.DbConnectionPoolMetrics; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import org.apache.commons.dbcp2.BasicDataSourceMXBean; diff --git a/instrumentation/apache-dbcp-2.0/library/src/test/java/io/opentelemetry/instrumentation/apachedbcp/v2_0/ApacheDbcpInstrumentationTest.java b/instrumentation/apache-dbcp-2.0/library/src/test/java/io/opentelemetry/instrumentation/apachedbcp/v2_0/ApacheDbcpInstrumentationTest.java index 9df3d159fff1..b94966f0b213 100644 --- a/instrumentation/apache-dbcp-2.0/library/src/test/java/io/opentelemetry/instrumentation/apachedbcp/v2_0/ApacheDbcpInstrumentationTest.java +++ b/instrumentation/apache-dbcp-2.0/library/src/test/java/io/opentelemetry/instrumentation/apachedbcp/v2_0/ApacheDbcpInstrumentationTest.java @@ -12,7 +12,7 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.extension.RegisterExtension; -public class ApacheDbcpInstrumentationTest extends AbstractApacheDbcpInstrumentationTest { +class ApacheDbcpInstrumentationTest extends AbstractApacheDbcpInstrumentationTest { @RegisterExtension static final InstrumentationExtension testing = LibraryInstrumentationExtension.create(); diff --git a/instrumentation/apache-dubbo-2.7/javaagent/build.gradle.kts b/instrumentation/apache-dubbo-2.7/javaagent/build.gradle.kts index 6c206efce039..6e1a666c9908 100644 --- a/instrumentation/apache-dubbo-2.7/javaagent/build.gradle.kts +++ b/instrumentation/apache-dubbo-2.7/javaagent/build.gradle.kts @@ -15,10 +15,28 @@ dependencies { implementation(project(":instrumentation:apache-dubbo-2.7:library-autoconfigure")) library("org.apache.dubbo:dubbo:2.7.0") +} - testImplementation(project(":instrumentation:apache-dubbo-2.7:testing")) +val latestDepTest = findProperty("testLatestDeps") as Boolean - testLibrary("org.apache.dubbo:dubbo-config-api:2.7.0") +testing { + suites { + // using a test suite to ensure that project(":instrumentation:apache-dubbo-2.7:library-autoconfigure") + // is not available on test runtime class path, otherwise instrumentation from library-autoconfigure + // module would be used instead of the javaagent instrumentation that we want to test + val testDubbo by registering(JvmTestSuite::class) { + dependencies { + implementation(project(":instrumentation:apache-dubbo-2.7:testing")) + if (latestDepTest) { + implementation("org.apache.dubbo:dubbo:+") + implementation("org.apache.dubbo:dubbo-config-api:+") + } else { + implementation("org.apache.dubbo:dubbo:2.7.0") + implementation("org.apache.dubbo:dubbo-config-api:2.7.0") + } + } + } + } } tasks.withType().configureEach { @@ -28,3 +46,9 @@ tasks.withType().configureEach { // required on jdk17 jvmArgs("--add-opens=java.base/java.lang=ALL-UNNAMED") } + +tasks { + check { + dependsOn(testing.suites) + } +} diff --git a/instrumentation/apache-dubbo-2.7/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachedubbo/v2_7/DubboInstrumentationModule.java b/instrumentation/apache-dubbo-2.7/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachedubbo/v2_7/DubboInstrumentationModule.java index be12bcfbee54..c5f0b5641e94 100644 --- a/instrumentation/apache-dubbo-2.7/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachedubbo/v2_7/DubboInstrumentationModule.java +++ b/instrumentation/apache-dubbo-2.7/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachedubbo/v2_7/DubboInstrumentationModule.java @@ -14,12 +14,16 @@ import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.extension.instrumentation.internal.ExperimentalInstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.internal.injection.ClassInjector; +import io.opentelemetry.javaagent.extension.instrumentation.internal.injection.InjectionMode; import java.util.List; import net.bytebuddy.description.type.TypeDescription; import net.bytebuddy.matcher.ElementMatcher; @AutoService(InstrumentationModule.class) -public class DubboInstrumentationModule extends InstrumentationModule { +public class DubboInstrumentationModule extends InstrumentationModule + implements ExperimentalInstrumentationModule { public DubboInstrumentationModule() { super("apache-dubbo", "apache-dubbo-2.7"); } @@ -28,7 +32,7 @@ public DubboInstrumentationModule() { public void registerHelperResources(HelperResourceBuilder helperResourceBuilder) { helperResourceBuilder.register( "META-INF/services/org.apache.dubbo.rpc.Filter", - "spi-to-inject/org.apache.dubbo.rpc.Filter"); + "apache-dubbo-2.7/META-INF/org.apache.dubbo.rpc.Filter"); } @Override @@ -36,6 +40,14 @@ public ElementMatcher.Junction classLoaderMatcher() { return hasClassesNamed("org.apache.dubbo.rpc.Filter"); } + @Override + public void injectClasses(ClassInjector injector) { + injector + .proxyBuilder( + "io.opentelemetry.javaagent.instrumentation.apachedubbo.v2_7.OpenTelemetryFilter") + .inject(InjectionMode.CLASS_ONLY); + } + @Override public List typeInstrumentations() { return singletonList(new ResourceInjectingTypeInstrumentation()); diff --git a/instrumentation/apache-dubbo-2.7/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachedubbo/v2_7/OpenTelemetryFilter.java b/instrumentation/apache-dubbo-2.7/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachedubbo/v2_7/OpenTelemetryFilter.java index a3010a6cfc8d..550f469ad9df 100644 --- a/instrumentation/apache-dubbo-2.7/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachedubbo/v2_7/OpenTelemetryFilter.java +++ b/instrumentation/apache-dubbo-2.7/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachedubbo/v2_7/OpenTelemetryFilter.java @@ -7,9 +7,9 @@ import io.opentelemetry.api.GlobalOpenTelemetry; import io.opentelemetry.instrumentation.apachedubbo.v2_7.DubboTelemetry; -import io.opentelemetry.instrumentation.apachedubbo.v2_7.internal.DubboNetClientAttributesGetter; -import io.opentelemetry.instrumentation.api.instrumenter.net.PeerServiceAttributesExtractor; -import io.opentelemetry.javaagent.bootstrap.internal.CommonConfig; +import io.opentelemetry.instrumentation.apachedubbo.v2_7.internal.DubboClientNetworkAttributesGetter; +import io.opentelemetry.instrumentation.api.incubator.semconv.net.PeerServiceAttributesExtractor; +import io.opentelemetry.javaagent.bootstrap.internal.AgentCommonConfig; import org.apache.dubbo.common.extension.Activate; import org.apache.dubbo.rpc.Filter; import org.apache.dubbo.rpc.Invocation; @@ -26,8 +26,8 @@ public OpenTelemetryFilter() { DubboTelemetry.builder(GlobalOpenTelemetry.get()) .addAttributesExtractor( PeerServiceAttributesExtractor.create( - new DubboNetClientAttributesGetter(), - CommonConfig.get().getPeerServiceMapping())) + new DubboClientNetworkAttributesGetter(), + AgentCommonConfig.get().getPeerServiceResolver())) .build() .newFilter(); } diff --git a/instrumentation/apache-dubbo-2.7/javaagent/src/main/resources/spi-to-inject/org.apache.dubbo.rpc.Filter b/instrumentation/apache-dubbo-2.7/javaagent/src/main/resources/apache-dubbo-2.7/META-INF/org.apache.dubbo.rpc.Filter similarity index 100% rename from instrumentation/apache-dubbo-2.7/javaagent/src/main/resources/spi-to-inject/org.apache.dubbo.rpc.Filter rename to instrumentation/apache-dubbo-2.7/javaagent/src/main/resources/apache-dubbo-2.7/META-INF/org.apache.dubbo.rpc.Filter diff --git a/instrumentation/apache-dubbo-2.7/javaagent/src/test/groovy/io/opentelemetry/instrumentation/apachedubbo/v2_7/DubboTest.groovy b/instrumentation/apache-dubbo-2.7/javaagent/src/test/groovy/io/opentelemetry/instrumentation/apachedubbo/v2_7/DubboTest.groovy deleted file mode 100644 index 36f81f58b828..000000000000 --- a/instrumentation/apache-dubbo-2.7/javaagent/src/test/groovy/io/opentelemetry/instrumentation/apachedubbo/v2_7/DubboTest.groovy +++ /dev/null @@ -1,11 +0,0 @@ -/* - * 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/instrumentation/apache-dubbo-2.7/javaagent/src/test/groovy/io/opentelemetry/instrumentation/apachedubbo/v2_7/DubboTraceChainTest.groovy b/instrumentation/apache-dubbo-2.7/javaagent/src/test/groovy/io/opentelemetry/instrumentation/apachedubbo/v2_7/DubboTraceChainTest.groovy deleted file mode 100644 index 432be0305633..000000000000 --- a/instrumentation/apache-dubbo-2.7/javaagent/src/test/groovy/io/opentelemetry/instrumentation/apachedubbo/v2_7/DubboTraceChainTest.groovy +++ /dev/null @@ -1,11 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.apachedubbo.v2_7 - -import io.opentelemetry.instrumentation.test.AgentTestTrait - -class DubboTraceChainTest extends AbstractDubboTraceChainTest implements AgentTestTrait { -} diff --git a/instrumentation/apache-dubbo-2.7/javaagent/src/testDubbo/java/io/opentelemetry/javaagent/instrumentation/apachedubbo/v2_7/DubboAgentTest.java b/instrumentation/apache-dubbo-2.7/javaagent/src/testDubbo/java/io/opentelemetry/javaagent/instrumentation/apachedubbo/v2_7/DubboAgentTest.java new file mode 100644 index 000000000000..1a7e2fb42405 --- /dev/null +++ b/instrumentation/apache-dubbo-2.7/javaagent/src/testDubbo/java/io/opentelemetry/javaagent/instrumentation/apachedubbo/v2_7/DubboAgentTest.java @@ -0,0 +1,22 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.apachedubbo.v2_7; + +import io.opentelemetry.instrumentation.apachedubbo.v2_7.AbstractDubboTest; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import org.junit.jupiter.api.extension.RegisterExtension; + +class DubboAgentTest extends AbstractDubboTest { + + @RegisterExtension + static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + @Override + protected InstrumentationExtension testing() { + return testing; + } +} diff --git a/instrumentation/apache-dubbo-2.7/javaagent/src/testDubbo/java/io/opentelemetry/javaagent/instrumentation/apachedubbo/v2_7/DubboAgentTraceChainTest.java b/instrumentation/apache-dubbo-2.7/javaagent/src/testDubbo/java/io/opentelemetry/javaagent/instrumentation/apachedubbo/v2_7/DubboAgentTraceChainTest.java new file mode 100644 index 000000000000..bf34ae8633fe --- /dev/null +++ b/instrumentation/apache-dubbo-2.7/javaagent/src/testDubbo/java/io/opentelemetry/javaagent/instrumentation/apachedubbo/v2_7/DubboAgentTraceChainTest.java @@ -0,0 +1,22 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.apachedubbo.v2_7; + +import io.opentelemetry.instrumentation.apachedubbo.v2_7.AbstractDubboTraceChainTest; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import org.junit.jupiter.api.extension.RegisterExtension; + +class DubboAgentTraceChainTest extends AbstractDubboTraceChainTest { + + @RegisterExtension + static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + @Override + protected InstrumentationExtension testing() { + return testing; + } +} diff --git a/instrumentation/apache-dubbo-2.7/library-autoconfigure/src/main/java/io/opentelemetry/instrumentation/apachedubbo/v2_7/DubboHeadersGetter.java b/instrumentation/apache-dubbo-2.7/library-autoconfigure/src/main/java/io/opentelemetry/instrumentation/apachedubbo/v2_7/DubboHeadersGetter.java index 2b8e262bf7b0..eef5238bb46d 100644 --- a/instrumentation/apache-dubbo-2.7/library-autoconfigure/src/main/java/io/opentelemetry/instrumentation/apachedubbo/v2_7/DubboHeadersGetter.java +++ b/instrumentation/apache-dubbo-2.7/library-autoconfigure/src/main/java/io/opentelemetry/instrumentation/apachedubbo/v2_7/DubboHeadersGetter.java @@ -11,11 +11,13 @@ enum DubboHeadersGetter implements TextMapGetter { INSTANCE; @Override + @SuppressWarnings("deprecation") // deprecation for dubbo 3.2.15 public Iterable keys(DubboRequest request) { return request.invocation().getAttachments().keySet(); } @Override + @SuppressWarnings("deprecation") // deprecation for dubbo 3.2.15 public String get(DubboRequest request, String key) { return request.invocation().getAttachment(key); } diff --git a/instrumentation/apache-dubbo-2.7/library-autoconfigure/src/main/java/io/opentelemetry/instrumentation/apachedubbo/v2_7/DubboNetworkServerAttributesGetter.java b/instrumentation/apache-dubbo-2.7/library-autoconfigure/src/main/java/io/opentelemetry/instrumentation/apachedubbo/v2_7/DubboNetworkServerAttributesGetter.java new file mode 100644 index 000000000000..d9bf6e245ff8 --- /dev/null +++ b/instrumentation/apache-dubbo-2.7/library-autoconfigure/src/main/java/io/opentelemetry/instrumentation/apachedubbo/v2_7/DubboNetworkServerAttributesGetter.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.instrumentation.api.semconv.network.NetworkAttributesGetter; +import java.net.InetSocketAddress; +import javax.annotation.Nullable; +import org.apache.dubbo.rpc.Result; + +final class DubboNetworkServerAttributesGetter + implements NetworkAttributesGetter { + + @Nullable + @Override + public InetSocketAddress getNetworkLocalInetSocketAddress( + DubboRequest request, @Nullable Result result) { + return request.localAddress(); + } + + @Override + @Nullable + public InetSocketAddress getNetworkPeerInetSocketAddress( + DubboRequest request, @Nullable Result result) { + return request.remoteAddress(); + } +} diff --git a/instrumentation/apache-dubbo-2.7/library-autoconfigure/src/main/java/io/opentelemetry/instrumentation/apachedubbo/v2_7/DubboRpcAttributesGetter.java b/instrumentation/apache-dubbo-2.7/library-autoconfigure/src/main/java/io/opentelemetry/instrumentation/apachedubbo/v2_7/DubboRpcAttributesGetter.java index 497a44ba664e..aac66a138c01 100644 --- a/instrumentation/apache-dubbo-2.7/library-autoconfigure/src/main/java/io/opentelemetry/instrumentation/apachedubbo/v2_7/DubboRpcAttributesGetter.java +++ b/instrumentation/apache-dubbo-2.7/library-autoconfigure/src/main/java/io/opentelemetry/instrumentation/apachedubbo/v2_7/DubboRpcAttributesGetter.java @@ -5,7 +5,7 @@ package io.opentelemetry.instrumentation.apachedubbo.v2_7; -import io.opentelemetry.instrumentation.api.instrumenter.rpc.RpcAttributesGetter; +import io.opentelemetry.instrumentation.api.incubator.semconv.rpc.RpcAttributesGetter; enum DubboRpcAttributesGetter implements RpcAttributesGetter { INSTANCE; diff --git a/instrumentation/apache-dubbo-2.7/library-autoconfigure/src/main/java/io/opentelemetry/instrumentation/apachedubbo/v2_7/DubboTelemetryBuilder.java b/instrumentation/apache-dubbo-2.7/library-autoconfigure/src/main/java/io/opentelemetry/instrumentation/apachedubbo/v2_7/DubboTelemetryBuilder.java index f20838fd74b5..bbd4f9ad1d01 100644 --- a/instrumentation/apache-dubbo-2.7/library-autoconfigure/src/main/java/io/opentelemetry/instrumentation/apachedubbo/v2_7/DubboTelemetryBuilder.java +++ b/instrumentation/apache-dubbo-2.7/library-autoconfigure/src/main/java/io/opentelemetry/instrumentation/apachedubbo/v2_7/DubboTelemetryBuilder.java @@ -7,20 +7,20 @@ import com.google.errorprone.annotations.CanIgnoreReturnValue; import io.opentelemetry.api.OpenTelemetry; -import io.opentelemetry.instrumentation.apachedubbo.v2_7.internal.DubboNetClientAttributesGetter; -import io.opentelemetry.instrumentation.apachedubbo.v2_7.internal.DubboNetServerAttributesGetter; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.instrumentation.apachedubbo.v2_7.internal.DubboClientNetworkAttributesGetter; +import io.opentelemetry.instrumentation.api.incubator.semconv.rpc.RpcClientAttributesExtractor; +import io.opentelemetry.instrumentation.api.incubator.semconv.rpc.RpcServerAttributesExtractor; +import io.opentelemetry.instrumentation.api.incubator.semconv.rpc.RpcSpanNameExtractor; 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.net.NetClientAttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.net.NetServerAttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.rpc.RpcClientAttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.rpc.RpcServerAttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.rpc.RpcSpanNameExtractor; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.opentelemetry.instrumentation.api.semconv.network.NetworkAttributesExtractor; +import io.opentelemetry.instrumentation.api.semconv.network.ServerAttributesExtractor; import java.util.ArrayList; import java.util.List; +import java.util.function.Function; import javax.annotation.Nullable; import org.apache.dubbo.rpc.Result; @@ -29,10 +29,19 @@ public final class DubboTelemetryBuilder { private static final String INSTRUMENTATION_NAME = "io.opentelemetry.apache-dubbo-2.7"; + // copied from PeerIncubatingAttributes + private static final AttributeKey PEER_SERVICE = AttributeKey.stringKey("peer.service"); + private final OpenTelemetry openTelemetry; @Nullable private String peerService; private final List> attributesExtractors = new ArrayList<>(); + private Function< + SpanNameExtractor, ? extends SpanNameExtractor> + clientSpanNameExtractorTransformer = Function.identity(); + private Function< + SpanNameExtractor, ? extends SpanNameExtractor> + serverSpanNameExtractorTransformer = Function.identity(); DubboTelemetryBuilder(OpenTelemetry openTelemetry) { this.openTelemetry = openTelemetry; @@ -54,6 +63,24 @@ public DubboTelemetryBuilder addAttributesExtractor( return this; } + /** Sets custom client {@link SpanNameExtractor} via transform function. */ + @CanIgnoreReturnValue + public DubboTelemetryBuilder setClientSpanNameExtractor( + Function, ? extends SpanNameExtractor> + clientSpanNameExtractor) { + this.clientSpanNameExtractorTransformer = clientSpanNameExtractor; + return this; + } + + /** Sets custom server {@link SpanNameExtractor} via transform function. */ + @CanIgnoreReturnValue + public DubboTelemetryBuilder setServerSpanNameExtractor( + Function, ? extends SpanNameExtractor> + serverSpanNameExtractor) { + this.serverSpanNameExtractorTransformer = serverSpanNameExtractor; + return this; + } + /** * Returns a new {@link DubboTelemetry} with the settings of this {@link DubboTelemetryBuilder}. */ @@ -61,26 +88,33 @@ public DubboTelemetry build() { DubboRpcAttributesGetter rpcAttributesGetter = DubboRpcAttributesGetter.INSTANCE; SpanNameExtractor spanNameExtractor = RpcSpanNameExtractor.create(rpcAttributesGetter); - DubboNetClientAttributesGetter netClientAttributesGetter = new DubboNetClientAttributesGetter(); + SpanNameExtractor clientSpanNameExtractor = + clientSpanNameExtractorTransformer.apply(spanNameExtractor); + SpanNameExtractor serverSpanNameExtractor = + serverSpanNameExtractorTransformer.apply(spanNameExtractor); + DubboClientNetworkAttributesGetter netClientAttributesGetter = + new DubboClientNetworkAttributesGetter(); + DubboNetworkServerAttributesGetter netServerAttributesGetter = + new DubboNetworkServerAttributesGetter(); InstrumenterBuilder serverInstrumenterBuilder = Instrumenter.builder( - openTelemetry, INSTRUMENTATION_NAME, spanNameExtractor) + openTelemetry, INSTRUMENTATION_NAME, serverSpanNameExtractor) .addAttributesExtractor(RpcServerAttributesExtractor.create(rpcAttributesGetter)) - .addAttributesExtractor( - NetServerAttributesExtractor.create(new DubboNetServerAttributesGetter())) + .addAttributesExtractor(NetworkAttributesExtractor.create(netServerAttributesGetter)) .addAttributesExtractors(attributesExtractors); InstrumenterBuilder clientInstrumenterBuilder = Instrumenter.builder( - openTelemetry, INSTRUMENTATION_NAME, spanNameExtractor) + openTelemetry, INSTRUMENTATION_NAME, clientSpanNameExtractor) .addAttributesExtractor(RpcClientAttributesExtractor.create(rpcAttributesGetter)) - .addAttributesExtractor(NetClientAttributesExtractor.create(netClientAttributesGetter)) + .addAttributesExtractor(ServerAttributesExtractor.create(netClientAttributesGetter)) + .addAttributesExtractor(NetworkAttributesExtractor.create(netClientAttributesGetter)) .addAttributesExtractors(attributesExtractors); if (peerService != null) { clientInstrumenterBuilder.addAttributesExtractor( - AttributesExtractor.constant(SemanticAttributes.PEER_SERVICE, peerService)); + AttributesExtractor.constant(PEER_SERVICE, peerService)); } return new DubboTelemetry( diff --git a/instrumentation/apache-dubbo-2.7/library-autoconfigure/src/main/java/io/opentelemetry/instrumentation/apachedubbo/v2_7/TracingFilter.java b/instrumentation/apache-dubbo-2.7/library-autoconfigure/src/main/java/io/opentelemetry/instrumentation/apachedubbo/v2_7/TracingFilter.java index a19e8ef6d5db..d3d239096bb1 100644 --- a/instrumentation/apache-dubbo-2.7/library-autoconfigure/src/main/java/io/opentelemetry/instrumentation/apachedubbo/v2_7/TracingFilter.java +++ b/instrumentation/apache-dubbo-2.7/library-autoconfigure/src/main/java/io/opentelemetry/instrumentation/apachedubbo/v2_7/TracingFilter.java @@ -29,6 +29,7 @@ final class TracingFilter implements Filter { } @Override + @SuppressWarnings("deprecation") // deprecation for RpcContext.getContext() public Result invoke(Invoker invoker, Invocation invocation) { if (!(invocation instanceof RpcInvocation)) { return invoker.invoke(invocation); diff --git a/instrumentation/apache-dubbo-2.7/library-autoconfigure/src/main/java/io/opentelemetry/instrumentation/apachedubbo/v2_7/internal/DubboNetClientAttributesGetter.java b/instrumentation/apache-dubbo-2.7/library-autoconfigure/src/main/java/io/opentelemetry/instrumentation/apachedubbo/v2_7/internal/DubboClientNetworkAttributesGetter.java similarity index 67% rename from instrumentation/apache-dubbo-2.7/library-autoconfigure/src/main/java/io/opentelemetry/instrumentation/apachedubbo/v2_7/internal/DubboNetClientAttributesGetter.java rename to instrumentation/apache-dubbo-2.7/library-autoconfigure/src/main/java/io/opentelemetry/instrumentation/apachedubbo/v2_7/internal/DubboClientNetworkAttributesGetter.java index a4d9470f728c..1077ee19c7ce 100644 --- a/instrumentation/apache-dubbo-2.7/library-autoconfigure/src/main/java/io/opentelemetry/instrumentation/apachedubbo/v2_7/internal/DubboNetClientAttributesGetter.java +++ b/instrumentation/apache-dubbo-2.7/library-autoconfigure/src/main/java/io/opentelemetry/instrumentation/apachedubbo/v2_7/internal/DubboClientNetworkAttributesGetter.java @@ -6,7 +6,8 @@ package io.opentelemetry.instrumentation.apachedubbo.v2_7.internal; import io.opentelemetry.instrumentation.apachedubbo.v2_7.DubboRequest; -import io.opentelemetry.instrumentation.api.instrumenter.net.NetClientAttributesGetter; +import io.opentelemetry.instrumentation.api.semconv.network.NetworkAttributesGetter; +import io.opentelemetry.instrumentation.api.semconv.network.ServerAttributesGetter; import java.net.InetSocketAddress; import javax.annotation.Nullable; import org.apache.dubbo.rpc.Result; @@ -15,8 +16,8 @@ * This class is internal and is hence not for public use. Its APIs are unstable and can change at * any time. */ -public final class DubboNetClientAttributesGetter - implements NetClientAttributesGetter { +public final class DubboClientNetworkAttributesGetter + implements ServerAttributesGetter, NetworkAttributesGetter { @Nullable @Override @@ -31,7 +32,7 @@ public Integer getServerPort(DubboRequest request) { @Override @Nullable - public InetSocketAddress getServerInetSocketAddress( + public InetSocketAddress getNetworkPeerInetSocketAddress( DubboRequest request, @Nullable Result response) { return request.remoteAddress(); } diff --git a/instrumentation/apache-dubbo-2.7/library-autoconfigure/src/main/java/io/opentelemetry/instrumentation/apachedubbo/v2_7/internal/DubboNetServerAttributesGetter.java b/instrumentation/apache-dubbo-2.7/library-autoconfigure/src/main/java/io/opentelemetry/instrumentation/apachedubbo/v2_7/internal/DubboNetServerAttributesGetter.java deleted file mode 100644 index 613375a7552a..000000000000 --- a/instrumentation/apache-dubbo-2.7/library-autoconfigure/src/main/java/io/opentelemetry/instrumentation/apachedubbo/v2_7/internal/DubboNetServerAttributesGetter.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.apachedubbo.v2_7.internal; - -import io.opentelemetry.instrumentation.apachedubbo.v2_7.DubboRequest; -import io.opentelemetry.instrumentation.api.instrumenter.net.NetServerAttributesGetter; -import java.net.InetSocketAddress; -import javax.annotation.Nullable; -import org.apache.dubbo.rpc.Result; - -/** - * This class is internal and is hence not for public use. Its APIs are unstable and can change at - * any time. - */ -public final class DubboNetServerAttributesGetter - implements NetServerAttributesGetter { - - @Nullable - @Override - public String getServerAddress(DubboRequest request) { - return null; - } - - @Nullable - @Override - public Integer getServerPort(DubboRequest request) { - return null; - } - - @Override - @Nullable - public InetSocketAddress getClientInetSocketAddress( - DubboRequest request, @Nullable Result result) { - return request.remoteAddress(); - } - - @Nullable - @Override - public InetSocketAddress getServerInetSocketAddress( - DubboRequest request, @Nullable Result result) { - return request.localAddress(); - } -} diff --git a/instrumentation/apache-dubbo-2.7/library-autoconfigure/src/test/groovy/io/opentelemetry/instrumentation/apachedubbo/v2_7/DubboTest.groovy b/instrumentation/apache-dubbo-2.7/library-autoconfigure/src/test/groovy/io/opentelemetry/instrumentation/apachedubbo/v2_7/DubboTest.groovy deleted file mode 100644 index daa71bc333c6..000000000000 --- a/instrumentation/apache-dubbo-2.7/library-autoconfigure/src/test/groovy/io/opentelemetry/instrumentation/apachedubbo/v2_7/DubboTest.groovy +++ /dev/null @@ -1,11 +0,0 @@ -/* - * 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/instrumentation/apache-dubbo-2.7/library-autoconfigure/src/test/groovy/io/opentelemetry/instrumentation/apachedubbo/v2_7/DubboTraceChainTest.groovy b/instrumentation/apache-dubbo-2.7/library-autoconfigure/src/test/groovy/io/opentelemetry/instrumentation/apachedubbo/v2_7/DubboTraceChainTest.groovy deleted file mode 100644 index ee0b1040dec5..000000000000 --- a/instrumentation/apache-dubbo-2.7/library-autoconfigure/src/test/groovy/io/opentelemetry/instrumentation/apachedubbo/v2_7/DubboTraceChainTest.groovy +++ /dev/null @@ -1,11 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.apachedubbo.v2_7 - -import io.opentelemetry.instrumentation.test.LibraryTestTrait - -class DubboTraceChainTest extends AbstractDubboTraceChainTest implements LibraryTestTrait { -} diff --git a/instrumentation/apache-dubbo-2.7/library-autoconfigure/src/test/java/io/opentelemetry/instrumentation/apachedubbo/v2_7/DubboTest.java b/instrumentation/apache-dubbo-2.7/library-autoconfigure/src/test/java/io/opentelemetry/instrumentation/apachedubbo/v2_7/DubboTest.java new file mode 100644 index 000000000000..84667a51e074 --- /dev/null +++ b/instrumentation/apache-dubbo-2.7/library-autoconfigure/src/test/java/io/opentelemetry/instrumentation/apachedubbo/v2_7/DubboTest.java @@ -0,0 +1,21 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.apachedubbo.v2_7; + +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.LibraryInstrumentationExtension; +import org.junit.jupiter.api.extension.RegisterExtension; + +class DubboTest extends AbstractDubboTest { + + @RegisterExtension + static final InstrumentationExtension testing = LibraryInstrumentationExtension.create(); + + @Override + protected InstrumentationExtension testing() { + return testing; + } +} diff --git a/instrumentation/apache-dubbo-2.7/library-autoconfigure/src/test/java/io/opentelemetry/instrumentation/apachedubbo/v2_7/DubboTraceChainTest.java b/instrumentation/apache-dubbo-2.7/library-autoconfigure/src/test/java/io/opentelemetry/instrumentation/apachedubbo/v2_7/DubboTraceChainTest.java new file mode 100644 index 000000000000..dab4e6f03150 --- /dev/null +++ b/instrumentation/apache-dubbo-2.7/library-autoconfigure/src/test/java/io/opentelemetry/instrumentation/apachedubbo/v2_7/DubboTraceChainTest.java @@ -0,0 +1,21 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.apachedubbo.v2_7; + +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.LibraryInstrumentationExtension; +import org.junit.jupiter.api.extension.RegisterExtension; + +class DubboTraceChainTest extends AbstractDubboTraceChainTest { + + @RegisterExtension + static final InstrumentationExtension testing = LibraryInstrumentationExtension.create(); + + @Override + protected InstrumentationExtension testing() { + return testing; + } +} diff --git a/instrumentation/apache-dubbo-2.7/testing/src/main/groovy/io/opentelemetry/instrumentation/apachedubbo/v2_7/AbstractDubboTest.groovy b/instrumentation/apache-dubbo-2.7/testing/src/main/groovy/io/opentelemetry/instrumentation/apachedubbo/v2_7/AbstractDubboTest.groovy deleted file mode 100644 index e3d7445b22ce..000000000000 --- a/instrumentation/apache-dubbo-2.7/testing/src/main/groovy/io/opentelemetry/instrumentation/apachedubbo/v2_7/AbstractDubboTest.groovy +++ /dev/null @@ -1,203 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.apachedubbo.v2_7 - -import io.opentelemetry.api.trace.SpanKind -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.rpc.service.GenericService -import spock.lang.Shared -import spock.lang.Unroll - -import static io.opentelemetry.api.trace.SpanKind.CLIENT -import static io.opentelemetry.api.trace.SpanKind.SERVER -import static io.opentelemetry.instrumentation.apachedubbo.v2_7.DubboTestUtil.newDubboBootstrap -import static io.opentelemetry.instrumentation.apachedubbo.v2_7.DubboTestUtil.newFrameworkModel - -@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) - - def frameworkModel = newFrameworkModel() - DubboBootstrap bootstrap = newDubboBootstrap(frameworkModel) - 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 = newDubboBootstrap(frameworkModel) - consumerBootstrap.application(new ApplicationConfig("dubbo-demo-api-consumer")) - .reference(reference) - .protocol(consumerProtocolConfig) - .start() - - when: - GenericService genericService = reference.get() - def o = new Object[1] - o[0] = "hello" - def response = runWithSpan("parent") { - genericService.$invoke("hello", [String.getName()] as String[], o) - } - - then: - response == "hello" - assertTraces(1) { - trace(0, 3) { - span(0) { - name "parent" - kind SpanKind.INTERNAL - hasNoParent() - } - span(1) { - name "org.apache.dubbo.rpc.service.GenericService/\$invoke" - kind CLIENT - childOf span(0) - attributes { - "$SemanticAttributes.RPC_SYSTEM" "apache_dubbo" - "$SemanticAttributes.RPC_SERVICE" "org.apache.dubbo.rpc.service.GenericService" - "$SemanticAttributes.RPC_METHOD" "\$invoke" - "$SemanticAttributes.NET_PEER_NAME" "localhost" - "$SemanticAttributes.NET_PEER_PORT" Long - } - } - span(2) { - name "io.opentelemetry.instrumentation.apachedubbo.v2_7.api.HelloService/hello" - kind SERVER - childOf span(1) - attributes { - "$SemanticAttributes.RPC_SYSTEM" "apache_dubbo" - "$SemanticAttributes.RPC_SERVICE" "io.opentelemetry.instrumentation.apachedubbo.v2_7.api.HelloService" - "$SemanticAttributes.RPC_METHOD" "hello" - "$SemanticAttributes.NET_SOCK_PEER_ADDR" String - "$SemanticAttributes.NET_SOCK_PEER_PORT" Long - "$SemanticAttributes.NET_SOCK_FAMILY" { it == SemanticAttributes.NetSockFamilyValues.INET6 || it == null } - } - } - } - } - - cleanup: - bootstrap.destroy() - consumerBootstrap.destroy() - frameworkModel?.destroy() - } - - def "test apache dubbo test #dubbo"() { - setup: - def port = PortUtils.findOpenPort() - protocolConfig.setPort(port) - - def frameworkModel = newFrameworkModel() - DubboBootstrap bootstrap = newDubboBootstrap(frameworkModel) - 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 = newDubboBootstrap(frameworkModel) - consumerBootstrap.application(new ApplicationConfig("dubbo-demo-async-api-consumer")) - .reference(reference) - .protocol(consumerProtocolConfig) - .start() - - when: - GenericService genericService = reference.get() - def o = new Object[1] - o[0] = "hello" - def responseAsync = runWithSpan("parent") { - genericService.$invokeAsync("hello", [String.getName()] as String[], o) - } - - then: - responseAsync.get() == "hello" - assertTraces(1) { - trace(0, 3) { - span(0) { - name "parent" - kind SpanKind.INTERNAL - hasNoParent() - } - span(1) { - name "org.apache.dubbo.rpc.service.GenericService/\$invokeAsync" - kind CLIENT - childOf span(0) - attributes { - "$SemanticAttributes.RPC_SYSTEM" "apache_dubbo" - "$SemanticAttributes.RPC_SERVICE" "org.apache.dubbo.rpc.service.GenericService" - "$SemanticAttributes.RPC_METHOD" "\$invokeAsync" - "$SemanticAttributes.NET_PEER_NAME" "localhost" - "$SemanticAttributes.NET_PEER_PORT" Long - } - } - span(2) { - name "io.opentelemetry.instrumentation.apachedubbo.v2_7.api.HelloService/hello" - kind SERVER - childOf span(1) - attributes { - "$SemanticAttributes.RPC_SYSTEM" "apache_dubbo" - "$SemanticAttributes.RPC_SERVICE" "io.opentelemetry.instrumentation.apachedubbo.v2_7.api.HelloService" - "$SemanticAttributes.RPC_METHOD" "hello" - "$SemanticAttributes.NET_SOCK_PEER_ADDR" String - "$SemanticAttributes.NET_SOCK_PEER_PORT" Long - "$SemanticAttributes.NET_SOCK_FAMILY" { it == SemanticAttributes.NetSockFamilyValues.INET6 || it == null } - } - } - } - } - - cleanup: - bootstrap.destroy() - consumerBootstrap.destroy() - frameworkModel?.destroy() - } -} diff --git a/instrumentation/apache-dubbo-2.7/testing/src/main/groovy/io/opentelemetry/instrumentation/apachedubbo/v2_7/AbstractDubboTraceChainTest.groovy b/instrumentation/apache-dubbo-2.7/testing/src/main/groovy/io/opentelemetry/instrumentation/apachedubbo/v2_7/AbstractDubboTraceChainTest.groovy deleted file mode 100644 index 8295373562df..000000000000 --- a/instrumentation/apache-dubbo-2.7/testing/src/main/groovy/io/opentelemetry/instrumentation/apachedubbo/v2_7/AbstractDubboTraceChainTest.groovy +++ /dev/null @@ -1,279 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.apachedubbo.v2_7 - -import io.opentelemetry.api.trace.SpanKind -import io.opentelemetry.instrumentation.apachedubbo.v2_7.api.HelloService -import io.opentelemetry.instrumentation.apachedubbo.v2_7.api.MiddleService -import io.opentelemetry.instrumentation.apachedubbo.v2_7.impl.HelloServiceImpl -import io.opentelemetry.instrumentation.apachedubbo.v2_7.impl.MiddleServiceImpl -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.rpc.service.GenericService -import spock.lang.Unroll - -import static io.opentelemetry.api.trace.SpanKind.CLIENT -import static io.opentelemetry.api.trace.SpanKind.SERVER -import static io.opentelemetry.instrumentation.apachedubbo.v2_7.DubboTestUtil.newDubboBootstrap -import static io.opentelemetry.instrumentation.apachedubbo.v2_7.DubboTestUtil.newFrameworkModel - -@Unroll -abstract class AbstractDubboTraceChainTest extends InstrumentationSpecification { - - 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 - } - - ReferenceConfig configureLocalClient(int port) { - ReferenceConfig reference = new ReferenceConfig<>() - reference.setInterface(HelloService) - reference.setGeneric("true") - reference.setUrl("injvm://localhost:" + port + "/?timeout=30000") - return reference - } - - ReferenceConfig configureMiddleClient(int port) { - ReferenceConfig reference = new ReferenceConfig<>() - reference.setInterface(MiddleService) - 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 - } - - ServiceConfig configureMiddleServer(ReferenceConfig referenceConfig) { - def registerConfig = new RegistryConfig() - registerConfig.setAddress("N/A") - ServiceConfig service = new ServiceConfig<>() - service.setInterface(MiddleService) - service.setRef(new MiddleServiceImpl(referenceConfig)) - service.setRegistry(registerConfig) - return service - } - - def "test that context is propagated correctly in chained dubbo calls"() { - setup: - def port = PortUtils.findOpenPorts(2) - def middlePort = port + 1 - def protocolConfig = new ProtocolConfig() - protocolConfig.setPort(port) - - def frameworkModel = newFrameworkModel() - DubboBootstrap bootstrap = newDubboBootstrap(frameworkModel) - bootstrap.application(new ApplicationConfig("dubbo-test-provider")) - .service(configureServer()) - .protocol(protocolConfig) - .start() - - def middleProtocolConfig = new ProtocolConfig() - middleProtocolConfig.setPort(middlePort) - - def reference = configureClient(port) - DubboBootstrap middleBootstrap = newDubboBootstrap(frameworkModel) - middleBootstrap.application(new ApplicationConfig("dubbo-demo-middle")) - .reference(reference) - .service(configureMiddleServer(reference)) - .protocol(middleProtocolConfig) - .start() - - - def consumerProtocolConfig = new ProtocolConfig() - consumerProtocolConfig.setRegister(false) - - def middleReference = configureMiddleClient(middlePort) - DubboBootstrap consumerBootstrap = newDubboBootstrap(frameworkModel) - consumerBootstrap.application(new ApplicationConfig("dubbo-demo-api-consumer")) - .reference(middleReference) - .protocol(consumerProtocolConfig) - .start() - - when: - GenericService genericService = middleReference.get() - def response = runWithSpan("parent") { - genericService.$invoke("hello", [String.getName()] as String[], ["hello"] as Object[]) - } - - then: - response == "hello" - assertTraces(1) { - trace(0, 5) { - span(0) { - name "parent" - kind SpanKind.INTERNAL - hasNoParent() - } - span(1) { - name "org.apache.dubbo.rpc.service.GenericService/\$invoke" - kind CLIENT - childOf span(0) - attributes { - "$SemanticAttributes.RPC_SYSTEM" "apache_dubbo" - "$SemanticAttributes.RPC_SERVICE" "org.apache.dubbo.rpc.service.GenericService" - "$SemanticAttributes.RPC_METHOD" "\$invoke" - "$SemanticAttributes.NET_PEER_NAME" "localhost" - "$SemanticAttributes.NET_PEER_PORT" Long - } - } - span(2) { - name "io.opentelemetry.instrumentation.apachedubbo.v2_7.api.MiddleService/hello" - kind SERVER - childOf span(1) - attributes { - "$SemanticAttributes.RPC_SYSTEM" "apache_dubbo" - "$SemanticAttributes.RPC_SERVICE" "io.opentelemetry.instrumentation.apachedubbo.v2_7.api.MiddleService" - "$SemanticAttributes.RPC_METHOD" "hello" - "$SemanticAttributes.NET_SOCK_PEER_ADDR" String - "$SemanticAttributes.NET_SOCK_PEER_PORT" Long - "$SemanticAttributes.NET_SOCK_FAMILY" { it == SemanticAttributes.NetSockFamilyValues.INET6 || it == null } - } - } - span(3) { - name "org.apache.dubbo.rpc.service.GenericService/\$invoke" - kind CLIENT - childOf span(2) - attributes { - "$SemanticAttributes.RPC_SYSTEM" "apache_dubbo" - "$SemanticAttributes.RPC_SERVICE" "org.apache.dubbo.rpc.service.GenericService" - "$SemanticAttributes.RPC_METHOD" "\$invoke" - "$SemanticAttributes.NET_PEER_NAME" "localhost" - "$SemanticAttributes.NET_PEER_PORT" Long - "$SemanticAttributes.NET_SOCK_PEER_ADDR" { it == null || String } - "$SemanticAttributes.NET_SOCK_PEER_PORT" { it == null || Long } - "$SemanticAttributes.NET_SOCK_PEER_NAME" { it == null || String } - } - } - span(4) { - name "io.opentelemetry.instrumentation.apachedubbo.v2_7.api.HelloService/hello" - kind SERVER - childOf span(3) - attributes { - "$SemanticAttributes.RPC_SYSTEM" "apache_dubbo" - "$SemanticAttributes.RPC_SERVICE" "io.opentelemetry.instrumentation.apachedubbo.v2_7.api.HelloService" - "$SemanticAttributes.RPC_METHOD" "hello" - "$SemanticAttributes.NET_SOCK_PEER_ADDR" String - "$SemanticAttributes.NET_SOCK_PEER_PORT" Long - "$SemanticAttributes.NET_SOCK_FAMILY" { it == SemanticAttributes.NetSockFamilyValues.INET6 || it == null } - } - } - } - } - - cleanup: - bootstrap.destroy() - middleBootstrap.destroy() - consumerBootstrap.destroy() - } - - def "test ignore injvm calls"() { - setup: - def port = PortUtils.findOpenPorts(2) - def middlePort = port + 1 - def protocolConfig = new ProtocolConfig() - protocolConfig.setPort(port) - - def frameworkModel = newFrameworkModel() - DubboBootstrap bootstrap = newDubboBootstrap(frameworkModel) - bootstrap.application(new ApplicationConfig("dubbo-test-provider")) - .service(configureServer()) - .protocol(protocolConfig) - .start() - - def middleProtocolConfig = new ProtocolConfig() - middleProtocolConfig.setPort(middlePort) - - def reference = configureLocalClient(port) - DubboBootstrap middleBootstrap = newDubboBootstrap(frameworkModel) - middleBootstrap.application(new ApplicationConfig("dubbo-demo-middle")) - .reference(reference) - .service(configureMiddleServer(reference)) - .protocol(middleProtocolConfig) - .start() - - - def consumerProtocolConfig = new ProtocolConfig() - consumerProtocolConfig.setRegister(false) - - def middleReference = configureMiddleClient(middlePort) - DubboBootstrap consumerBootstrap = newDubboBootstrap(frameworkModel) - consumerBootstrap.application(new ApplicationConfig("dubbo-demo-api-consumer")) - .reference(middleReference) - .protocol(consumerProtocolConfig) - .start() - - when: - GenericService genericService = middleReference.get() - def response = runWithSpan("parent") { - genericService.$invoke("hello", [String.getName()] as String[], ["hello"] as Object[]) - } - - then: - response == "hello" - assertTraces(1) { - trace(0, 3) { - span(0) { - name "parent" - kind SpanKind.INTERNAL - hasNoParent() - } - span(1) { - name "org.apache.dubbo.rpc.service.GenericService/\$invoke" - kind CLIENT - childOf span(0) - attributes { - "$SemanticAttributes.RPC_SYSTEM" "apache_dubbo" - "$SemanticAttributes.RPC_SERVICE" "org.apache.dubbo.rpc.service.GenericService" - "$SemanticAttributes.RPC_METHOD" "\$invoke" - "$SemanticAttributes.NET_PEER_NAME" "localhost" - "$SemanticAttributes.NET_PEER_PORT" Long - } - } - span(2) { - name "io.opentelemetry.instrumentation.apachedubbo.v2_7.api.MiddleService/hello" - kind SERVER - childOf span(1) - attributes { - "$SemanticAttributes.RPC_SYSTEM" "apache_dubbo" - "$SemanticAttributes.RPC_SERVICE" "io.opentelemetry.instrumentation.apachedubbo.v2_7.api.MiddleService" - "$SemanticAttributes.RPC_METHOD" "hello" - "$SemanticAttributes.NET_SOCK_PEER_ADDR" String - "$SemanticAttributes.NET_SOCK_PEER_PORT" Long - "$SemanticAttributes.NET_SOCK_FAMILY" { it == SemanticAttributes.NetSockFamilyValues.INET6 || it == null } - } - } - } - } - - cleanup: - bootstrap.destroy() - middleBootstrap.destroy() - consumerBootstrap.destroy() - } -} diff --git a/instrumentation/apache-dubbo-2.7/testing/src/main/groovy/io/opentelemetry/instrumentation/apachedubbo/v2_7/DubboTestUtil.groovy b/instrumentation/apache-dubbo-2.7/testing/src/main/groovy/io/opentelemetry/instrumentation/apachedubbo/v2_7/DubboTestUtil.groovy deleted file mode 100644 index 03ec85a3955d..000000000000 --- a/instrumentation/apache-dubbo-2.7/testing/src/main/groovy/io/opentelemetry/instrumentation/apachedubbo/v2_7/DubboTestUtil.groovy +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.apachedubbo.v2_7 - -import org.apache.dubbo.config.bootstrap.DubboBootstrap - -class DubboTestUtil { - static newFrameworkModel() { - try { - // only present in latest dep - return Class.forName("org.apache.dubbo.rpc.model.FrameworkModel").newInstance() - } catch (ClassNotFoundException exception) { - return null - } - } - - static DubboBootstrap newDubboBootstrap(Object frameworkModel) { - if (frameworkModel == null) { - return DubboBootstrap.newInstance() - } - return DubboBootstrap.newInstance(frameworkModel) - } -} diff --git a/instrumentation/apache-dubbo-2.7/testing/src/main/java/io/opentelemetry/instrumentation/apachedubbo/v2_7/AbstractDubboTest.java b/instrumentation/apache-dubbo-2.7/testing/src/main/java/io/opentelemetry/instrumentation/apachedubbo/v2_7/AbstractDubboTest.java new file mode 100644 index 000000000000..85ea385128cd --- /dev/null +++ b/instrumentation/apache-dubbo-2.7/testing/src/main/java/io/opentelemetry/instrumentation/apachedubbo/v2_7/AbstractDubboTest.java @@ -0,0 +1,287 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.apachedubbo.v2_7; + +import static io.opentelemetry.instrumentation.testing.GlobalTraceUtil.runWithSpan; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.satisfies; +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.apachedubbo.v2_7.api.HelloService; +import io.opentelemetry.instrumentation.apachedubbo.v2_7.impl.HelloServiceImpl; +import io.opentelemetry.instrumentation.test.utils.PortUtils; +import io.opentelemetry.instrumentation.testing.internal.AutoCleanupExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.semconv.NetworkAttributes; +import io.opentelemetry.semconv.ServerAttributes; +import io.opentelemetry.semconv.incubating.RpcIncubatingAttributes; +import java.lang.reflect.Field; +import java.net.InetAddress; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +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.rpc.service.GenericService; +import org.assertj.core.api.AbstractAssert; +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; + +public abstract class AbstractDubboTest { + + private final ProtocolConfig protocolConfig = new ProtocolConfig(); + + protected abstract InstrumentationExtension testing(); + + @RegisterExtension static final AutoCleanupExtension cleanup = AutoCleanupExtension.create(); + + @BeforeAll + static void setUp() throws Exception { + System.setProperty("dubbo.application.qos-enable", "false"); + Field field = NetUtils.class.getDeclaredField("LOCAL_ADDRESS"); + field.setAccessible(true); + field.set(null, InetAddress.getLoopbackAddress()); + } + + @AfterAll + static void tearDown() { + System.clearProperty("dubbo.application.qos-enable"); + } + + ReferenceConfig configureClient(int port) { + ReferenceConfig reference = new ReferenceConfig<>(); + reference.setInterface(HelloService.class); + reference.setGeneric("true"); + reference.setUrl("dubbo://localhost:" + port + "/?timeout=30000"); + return reference; + } + + ServiceConfig configureServer() { + RegistryConfig registerConfig = new RegistryConfig(); + registerConfig.setAddress("N/A"); + ServiceConfig service = new ServiceConfig<>(); + service.setInterface(HelloService.class); + service.setRef(new HelloServiceImpl()); + service.setRegistry(registerConfig); + return service; + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + ReferenceConfig convertReference(ReferenceConfig config) { + return (ReferenceConfig) config; + } + + @Test + void testApacheDubboBase() throws ReflectiveOperationException { + int port = PortUtils.findOpenPort(); + protocolConfig.setPort(port); + // provider boostrap + DubboBootstrap bootstrap = DubboTestUtil.newDubboBootstrap(); + cleanup.deferCleanup(bootstrap::destroy); + bootstrap + .application(new ApplicationConfig("dubbo-test-provider")) + .service(configureServer()) + .protocol(protocolConfig) + .start(); + + // consumer boostrap + DubboBootstrap consumerBootstrap = DubboTestUtil.newDubboBootstrap(); + cleanup.deferCleanup(consumerBootstrap::destroy); + ReferenceConfig referenceConfig = configureClient(port); + ProtocolConfig consumerProtocolConfig = new ProtocolConfig(); + consumerProtocolConfig.setRegister(false); + consumerBootstrap + .application(new ApplicationConfig("dubbo-demo-api-consumer")) + .reference(referenceConfig) + .protocol(consumerProtocolConfig) + .start(); + + // generic call + ReferenceConfig reference = convertReference(referenceConfig); + GenericService genericService = reference.get(); + + Object response = + runWithSpan( + "parent", + () -> + genericService.$invoke( + "hello", new String[] {String.class.getName()}, new Object[] {"hello"})); + + assertThat(response).isEqualTo("hello"); + testing() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(), + span -> + span.hasName("org.apache.dubbo.rpc.service.GenericService/$invoke") + .hasKind(SpanKind.CLIENT) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo( + RpcIncubatingAttributes.RPC_SYSTEM, + RpcIncubatingAttributes.RpcSystemValues.APACHE_DUBBO), + equalTo( + RpcIncubatingAttributes.RPC_SERVICE, + "org.apache.dubbo.rpc.service.GenericService"), + equalTo(RpcIncubatingAttributes.RPC_METHOD, "$invoke"), + equalTo(ServerAttributes.SERVER_ADDRESS, "localhost"), + satisfies( + ServerAttributes.SERVER_PORT, k -> k.isInstanceOf(Long.class)), + satisfies( + NetworkAttributes.NETWORK_PEER_ADDRESS, + k -> + k.satisfiesAnyOf( + val -> assertThat(val).isNull(), + val -> assertThat(val).isInstanceOf(String.class))), + satisfies( + NetworkAttributes.NETWORK_PEER_PORT, + k -> + k.satisfiesAnyOf( + val -> assertThat(val).isNull(), + val -> assertThat(val).isInstanceOf(Long.class))), + satisfies( + NetworkAttributes.NETWORK_TYPE, + k -> + k.satisfiesAnyOf( + val -> assertThat(val).isNull(), + val -> assertThat(val).isEqualTo("ipv4"), + val -> assertThat(val).isEqualTo("ipv6")))), + span -> + span.hasName( + "io.opentelemetry.instrumentation.apachedubbo.v2_7.api.HelloService/hello") + .hasKind(SpanKind.SERVER) + .hasParent(trace.getSpan(1)) + .hasAttributesSatisfying( + equalTo( + RpcIncubatingAttributes.RPC_SYSTEM, + RpcIncubatingAttributes.RpcSystemValues.APACHE_DUBBO), + equalTo( + RpcIncubatingAttributes.RPC_SERVICE, + "io.opentelemetry.instrumentation.apachedubbo.v2_7.api.HelloService"), + equalTo(RpcIncubatingAttributes.RPC_METHOD, "hello"), + satisfies( + NetworkAttributes.NETWORK_PEER_ADDRESS, + k -> k.isInstanceOf(String.class)), + satisfies( + NetworkAttributes.NETWORK_PEER_PORT, + k -> k.isInstanceOf(Long.class)), + satisfies( + NetworkAttributes.NETWORK_TYPE, AbstractAssert::isNull)))); + } + + @Test + void testApacheDubboTest() + throws ExecutionException, InterruptedException, ReflectiveOperationException { + int port = PortUtils.findOpenPort(); + protocolConfig.setPort(port); + + DubboBootstrap bootstrap = DubboTestUtil.newDubboBootstrap(); + cleanup.deferCleanup(bootstrap::destroy); + bootstrap + .application(new ApplicationConfig("dubbo-test-async-provider")) + .service(configureServer()) + .protocol(protocolConfig) + .start(); + + ProtocolConfig consumerProtocolConfig = new ProtocolConfig(); + consumerProtocolConfig.setRegister(false); + + ReferenceConfig referenceConfig = configureClient(port); + DubboBootstrap consumerBootstrap = DubboTestUtil.newDubboBootstrap(); + cleanup.deferCleanup(consumerBootstrap::destroy); + consumerBootstrap + .application(new ApplicationConfig("dubbo-demo-async-api-consumer")) + .reference(referenceConfig) + .protocol(consumerProtocolConfig) + .start(); + + // generic call + ReferenceConfig reference = convertReference(referenceConfig); + GenericService genericService = reference.get(); + CompletableFuture response = + runWithSpan( + "parent", + () -> + genericService.$invokeAsync( + "hello", new String[] {String.class.getName()}, new Object[] {"hello"})); + + assertThat(response.get()).isEqualTo("hello"); + + testing() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(), + span -> + span.hasName("org.apache.dubbo.rpc.service.GenericService/$invokeAsync") + .hasKind(SpanKind.CLIENT) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo( + RpcIncubatingAttributes.RPC_SYSTEM, + RpcIncubatingAttributes.RpcSystemValues.APACHE_DUBBO), + equalTo( + RpcIncubatingAttributes.RPC_SERVICE, + "org.apache.dubbo.rpc.service.GenericService"), + equalTo(RpcIncubatingAttributes.RPC_METHOD, "$invokeAsync"), + equalTo(ServerAttributes.SERVER_ADDRESS, "localhost"), + satisfies( + ServerAttributes.SERVER_PORT, k -> k.isInstanceOf(Long.class)), + satisfies( + NetworkAttributes.NETWORK_PEER_ADDRESS, + k -> + k.satisfiesAnyOf( + val -> assertThat(val).isNull(), + val -> assertThat(val).isInstanceOf(String.class))), + satisfies( + NetworkAttributes.NETWORK_PEER_PORT, + k -> + k.satisfiesAnyOf( + val -> assertThat(val).isNull(), + val -> assertThat(val).isInstanceOf(Long.class))), + satisfies( + NetworkAttributes.NETWORK_TYPE, + k -> + k.satisfiesAnyOf( + val -> assertThat(val).isNull(), + val -> assertThat(val).isEqualTo("ipv4"), + val -> assertThat(val).isEqualTo("ipv6")))), + span -> + span.hasName( + "io.opentelemetry.instrumentation.apachedubbo.v2_7.api.HelloService/hello") + .hasKind(SpanKind.SERVER) + .hasParent(trace.getSpan(1)) + .hasAttributesSatisfying( + equalTo( + RpcIncubatingAttributes.RPC_SYSTEM, + RpcIncubatingAttributes.RpcSystemValues.APACHE_DUBBO), + equalTo( + RpcIncubatingAttributes.RPC_SERVICE, + "io.opentelemetry.instrumentation.apachedubbo.v2_7.api.HelloService"), + equalTo(RpcIncubatingAttributes.RPC_METHOD, "hello"), + satisfies( + NetworkAttributes.NETWORK_PEER_ADDRESS, + k -> k.isInstanceOf(String.class)), + satisfies( + NetworkAttributes.NETWORK_PEER_PORT, + k -> k.isInstanceOf(Long.class)), + satisfies( + NetworkAttributes.NETWORK_TYPE, + k -> + k.satisfiesAnyOf( + val -> assertThat(val).isNull(), + val -> assertThat(val).isEqualTo("ipv4"), + val -> assertThat(val).isEqualTo("ipv6")))))); + } +} diff --git a/instrumentation/apache-dubbo-2.7/testing/src/main/java/io/opentelemetry/instrumentation/apachedubbo/v2_7/AbstractDubboTraceChainTest.java b/instrumentation/apache-dubbo-2.7/testing/src/main/java/io/opentelemetry/instrumentation/apachedubbo/v2_7/AbstractDubboTraceChainTest.java new file mode 100644 index 000000000000..4f90694b49f8 --- /dev/null +++ b/instrumentation/apache-dubbo-2.7/testing/src/main/java/io/opentelemetry/instrumentation/apachedubbo/v2_7/AbstractDubboTraceChainTest.java @@ -0,0 +1,411 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.apachedubbo.v2_7; + +import static io.opentelemetry.instrumentation.testing.GlobalTraceUtil.runWithSpan; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.satisfies; +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.apachedubbo.v2_7.api.HelloService; +import io.opentelemetry.instrumentation.apachedubbo.v2_7.api.MiddleService; +import io.opentelemetry.instrumentation.apachedubbo.v2_7.impl.HelloServiceImpl; +import io.opentelemetry.instrumentation.apachedubbo.v2_7.impl.MiddleServiceImpl; +import io.opentelemetry.instrumentation.test.utils.PortUtils; +import io.opentelemetry.instrumentation.testing.internal.AutoCleanupExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.semconv.NetworkAttributes; +import io.opentelemetry.semconv.ServerAttributes; +import io.opentelemetry.semconv.incubating.RpcIncubatingAttributes; +import java.lang.reflect.Field; +import java.net.InetAddress; +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.rpc.service.GenericService; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +public abstract class AbstractDubboTraceChainTest { + + @RegisterExtension static final AutoCleanupExtension cleanup = AutoCleanupExtension.create(); + + @BeforeAll + static void setUp() throws Exception { + System.setProperty("dubbo.application.qos-enable", "false"); + Field field = NetUtils.class.getDeclaredField("LOCAL_ADDRESS"); + field.setAccessible(true); + field.set(null, InetAddress.getLoopbackAddress()); + } + + @AfterAll + static void tearDown() { + System.clearProperty("dubbo.application.qos-enable"); + } + + protected abstract InstrumentationExtension testing(); + + ReferenceConfig configureClient(int port) { + ReferenceConfig reference = new ReferenceConfig<>(); + reference.setInterface(HelloService.class); + reference.setGeneric("true"); + reference.setUrl("dubbo://localhost:" + port + "/?timeout=30000"); + return reference; + } + + ReferenceConfig configureLocalClient(int port) { + ReferenceConfig reference = new ReferenceConfig<>(); + reference.setInterface(HelloService.class); + reference.setGeneric("true"); + reference.setUrl("injvm://localhost:" + port + "/?timeout=30000"); + return reference; + } + + ReferenceConfig configureMiddleClient(int port) { + ReferenceConfig reference = new ReferenceConfig<>(); + reference.setInterface(MiddleService.class); + reference.setGeneric("true"); + reference.setUrl("dubbo://localhost:" + port + "/?timeout=30000"); + return reference; + } + + ServiceConfig configureServer() { + RegistryConfig registerConfig = new RegistryConfig(); + registerConfig.setAddress("N/A"); + ServiceConfig service = new ServiceConfig<>(); + service.setInterface(HelloService.class); + service.setRef(new HelloServiceImpl()); + service.setRegistry(registerConfig); + return service; + } + + ServiceConfig configureMiddleServer( + ReferenceConfig referenceConfig) { + RegistryConfig registerConfig = new RegistryConfig(); + registerConfig.setAddress("N/A"); + ServiceConfig service = new ServiceConfig<>(); + service.setInterface(MiddleService.class); + service.setRef(new MiddleServiceImpl(referenceConfig)); + service.setRegistry(registerConfig); + return service; + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + ReferenceConfig convertReference(ReferenceConfig config) { + return (ReferenceConfig) config; + } + + @Test + @DisplayName("test that context is propagated correctly in chained dubbo calls") + void testDubboChain() throws ReflectiveOperationException { + int port = PortUtils.findOpenPorts(2); + int middlePort = port + 1; + + // setup hello service provider + ProtocolConfig protocolConfig = new ProtocolConfig(); + protocolConfig.setPort(port); + + DubboBootstrap bootstrap = DubboTestUtil.newDubboBootstrap(); + cleanup.deferCleanup(bootstrap::destroy); + bootstrap + .application(new ApplicationConfig("dubbo-test-provider")) + .service(configureServer()) + .protocol(protocolConfig) + .start(); + + // setup middle service provider, hello service consumer + ProtocolConfig middleProtocolConfig = new ProtocolConfig(); + middleProtocolConfig.setPort(middlePort); + + ReferenceConfig clientReference = configureClient(port); + DubboBootstrap middleBootstrap = DubboTestUtil.newDubboBootstrap(); + cleanup.deferCleanup(middleBootstrap::destroy); + middleBootstrap + .application(new ApplicationConfig("dubbo-demo-middle")) + .reference(clientReference) + .service(configureMiddleServer(clientReference)) + .protocol(middleProtocolConfig) + .start(); + + // setup middle service consumer + ProtocolConfig consumerProtocolConfig = new ProtocolConfig(); + consumerProtocolConfig.setRegister(false); + + ReferenceConfig middleReference = configureMiddleClient(middlePort); + DubboBootstrap consumerBootstrap = DubboTestUtil.newDubboBootstrap(); + cleanup.deferCleanup(consumerBootstrap::destroy); + consumerBootstrap + .application(new ApplicationConfig("dubbo-demo-api-consumer")) + .reference(middleReference) + .protocol(consumerProtocolConfig) + .start(); + + GenericService genericService = convertReference(middleReference).get(); + + Object response = + runWithSpan( + "parent", + () -> + genericService.$invoke( + "hello", new String[] {String.class.getName()}, new Object[] {"hello"})); + + assertThat(response).isEqualTo("hello"); + testing() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(), + span -> + span.hasName("org.apache.dubbo.rpc.service.GenericService/$invoke") + .hasKind(SpanKind.CLIENT) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo( + RpcIncubatingAttributes.RPC_SYSTEM, + RpcIncubatingAttributes.RpcSystemValues.APACHE_DUBBO), + equalTo( + RpcIncubatingAttributes.RPC_SERVICE, + "org.apache.dubbo.rpc.service.GenericService"), + equalTo(RpcIncubatingAttributes.RPC_METHOD, "$invoke"), + equalTo(ServerAttributes.SERVER_ADDRESS, "localhost"), + satisfies( + ServerAttributes.SERVER_PORT, k -> k.isInstanceOf(Long.class)), + satisfies( + NetworkAttributes.NETWORK_PEER_ADDRESS, + k -> + k.satisfiesAnyOf( + val -> assertThat(val).isNull(), + val -> assertThat(val).isInstanceOf(String.class))), + satisfies( + NetworkAttributes.NETWORK_PEER_PORT, + k -> + k.satisfiesAnyOf( + val -> assertThat(val).isNull(), + val -> assertThat(val).isInstanceOf(Long.class))), + satisfies( + NetworkAttributes.NETWORK_TYPE, + k -> + k.satisfiesAnyOf( + val -> assertThat(val).isNull(), + val -> assertThat(val).isEqualTo("ipv4"), + val -> assertThat(val).isEqualTo("ipv6")))), + span -> + span.hasName( + "io.opentelemetry.instrumentation.apachedubbo.v2_7.api.MiddleService/hello") + .hasKind(SpanKind.SERVER) + .hasParent(trace.getSpan(1)) + .hasAttributesSatisfying( + equalTo( + RpcIncubatingAttributes.RPC_SYSTEM, + RpcIncubatingAttributes.RpcSystemValues.APACHE_DUBBO), + equalTo( + RpcIncubatingAttributes.RPC_SERVICE, + "io.opentelemetry.instrumentation.apachedubbo.v2_7.api.MiddleService"), + equalTo(RpcIncubatingAttributes.RPC_METHOD, "hello"), + satisfies( + NetworkAttributes.NETWORK_PEER_ADDRESS, + k -> k.isInstanceOf(String.class)), + satisfies( + NetworkAttributes.NETWORK_PEER_PORT, + k -> k.isInstanceOf(Long.class)), + satisfies( + NetworkAttributes.NETWORK_TYPE, + k -> + k.satisfiesAnyOf( + val -> assertThat(val).isNull(), + val -> assertThat(val).isEqualTo("ipv4"), + val -> assertThat(val).isEqualTo("ipv6")))), + span -> + span.hasName("org.apache.dubbo.rpc.service.GenericService/$invoke") + .hasKind(SpanKind.CLIENT) + .hasParent(trace.getSpan(2)) + .hasAttributesSatisfyingExactly( + equalTo( + RpcIncubatingAttributes.RPC_SYSTEM, + RpcIncubatingAttributes.RpcSystemValues.APACHE_DUBBO), + equalTo( + RpcIncubatingAttributes.RPC_SERVICE, + "org.apache.dubbo.rpc.service.GenericService"), + equalTo(RpcIncubatingAttributes.RPC_METHOD, "$invoke"), + equalTo(ServerAttributes.SERVER_ADDRESS, "localhost"), + satisfies( + ServerAttributes.SERVER_PORT, k -> k.isInstanceOf(Long.class)), + satisfies( + NetworkAttributes.NETWORK_PEER_ADDRESS, + k -> + k.satisfiesAnyOf( + val -> assertThat(val).isNull(), + val -> assertThat(val).isInstanceOf(String.class))), + satisfies( + NetworkAttributes.NETWORK_PEER_PORT, + k -> + k.satisfiesAnyOf( + val -> assertThat(val).isNull(), + val -> assertThat(val).isInstanceOf(Long.class))), + satisfies( + NetworkAttributes.NETWORK_TYPE, + k -> + k.satisfiesAnyOf( + val -> assertThat(val).isNull(), + val -> assertThat(val).isEqualTo("ipv4"), + val -> assertThat(val).isEqualTo("ipv6")))), + span -> + span.hasName( + "io.opentelemetry.instrumentation.apachedubbo.v2_7.api.HelloService/hello") + .hasKind(SpanKind.SERVER) + .hasParent(trace.getSpan(3)) + .hasAttributesSatisfying( + equalTo( + RpcIncubatingAttributes.RPC_SYSTEM, + RpcIncubatingAttributes.RpcSystemValues.APACHE_DUBBO), + equalTo( + RpcIncubatingAttributes.RPC_SERVICE, + "io.opentelemetry.instrumentation.apachedubbo.v2_7.api.HelloService"), + equalTo(RpcIncubatingAttributes.RPC_METHOD, "hello"), + satisfies( + NetworkAttributes.NETWORK_PEER_ADDRESS, + k -> k.isInstanceOf(String.class)), + satisfies( + NetworkAttributes.NETWORK_PEER_PORT, + k -> k.isInstanceOf(Long.class)), + satisfies( + NetworkAttributes.NETWORK_TYPE, + k -> + k.satisfiesAnyOf( + val -> assertThat(val).isNull(), + val -> assertThat(val).isEqualTo("ipv4"), + val -> assertThat(val).isEqualTo("ipv6")))))); + } + + @Test + @DisplayName("test ignore injvm calls") + void testDubboChainInJvm() throws ReflectiveOperationException { + int port = PortUtils.findOpenPorts(2); + int middlePort = port + 1; + + // setup hello service provider + ProtocolConfig protocolConfig = new ProtocolConfig(); + protocolConfig.setPort(port); + + DubboBootstrap bootstrap = DubboTestUtil.newDubboBootstrap(); + cleanup.deferCleanup(bootstrap::destroy); + bootstrap + .application(new ApplicationConfig("dubbo-test-provider")) + .service(configureServer()) + .protocol(protocolConfig) + .start(); + + // setup middle service provider, hello service consumer + ProtocolConfig middleProtocolConfig = new ProtocolConfig(); + middleProtocolConfig.setPort(middlePort); + + ReferenceConfig clientReference = configureLocalClient(port); + DubboBootstrap middleBootstrap = DubboTestUtil.newDubboBootstrap(); + cleanup.deferCleanup(middleBootstrap::destroy); + middleBootstrap + .application(new ApplicationConfig("dubbo-demo-middle")) + .service(configureMiddleServer(clientReference)) + .protocol(middleProtocolConfig) + .start(); + + // setup middle service consumer + ProtocolConfig consumerProtocolConfig = new ProtocolConfig(); + consumerProtocolConfig.setRegister(false); + + ReferenceConfig middleReference = configureMiddleClient(middlePort); + DubboBootstrap consumerBootstrap = DubboTestUtil.newDubboBootstrap(); + cleanup.deferCleanup(consumerBootstrap::destroy); + consumerBootstrap + .application(new ApplicationConfig("dubbo-demo-api-consumer")) + .reference(middleReference) + .protocol(consumerProtocolConfig) + .start(); + + GenericService genericService = convertReference(middleReference).get(); + + Object response = + runWithSpan( + "parent", + () -> + genericService.$invoke( + "hello", new String[] {String.class.getName()}, new Object[] {"hello"})); + + assertThat(response).isEqualTo("hello"); + testing() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(), + span -> + span.hasName("org.apache.dubbo.rpc.service.GenericService/$invoke") + .hasKind(SpanKind.CLIENT) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo( + RpcIncubatingAttributes.RPC_SYSTEM, + RpcIncubatingAttributes.RpcSystemValues.APACHE_DUBBO), + equalTo( + RpcIncubatingAttributes.RPC_SERVICE, + "org.apache.dubbo.rpc.service.GenericService"), + equalTo(RpcIncubatingAttributes.RPC_METHOD, "$invoke"), + equalTo(ServerAttributes.SERVER_ADDRESS, "localhost"), + satisfies( + ServerAttributes.SERVER_PORT, k -> k.isInstanceOf(Long.class)), + satisfies( + NetworkAttributes.NETWORK_PEER_ADDRESS, + k -> + k.satisfiesAnyOf( + val -> assertThat(val).isNull(), + val -> assertThat(val).isInstanceOf(String.class))), + satisfies( + NetworkAttributes.NETWORK_PEER_PORT, + k -> + k.satisfiesAnyOf( + val -> assertThat(val).isNull(), + val -> assertThat(val).isInstanceOf(Long.class))), + satisfies( + NetworkAttributes.NETWORK_TYPE, + k -> + k.satisfiesAnyOf( + val -> assertThat(val).isNull(), + val -> assertThat(val).isEqualTo("ipv4"), + val -> assertThat(val).isEqualTo("ipv6")))), + span -> + span.hasName( + "io.opentelemetry.instrumentation.apachedubbo.v2_7.api.MiddleService/hello") + .hasKind(SpanKind.SERVER) + .hasParent(trace.getSpan(1)) + .hasAttributesSatisfying( + equalTo( + RpcIncubatingAttributes.RPC_SYSTEM, + RpcIncubatingAttributes.RpcSystemValues.APACHE_DUBBO), + equalTo( + RpcIncubatingAttributes.RPC_SERVICE, + "io.opentelemetry.instrumentation.apachedubbo.v2_7.api.MiddleService"), + equalTo(RpcIncubatingAttributes.RPC_METHOD, "hello"), + satisfies( + NetworkAttributes.NETWORK_PEER_ADDRESS, + k -> k.isInstanceOf(String.class)), + satisfies( + NetworkAttributes.NETWORK_PEER_PORT, + k -> k.isInstanceOf(Long.class)), + satisfies( + NetworkAttributes.NETWORK_TYPE, + k -> + k.satisfiesAnyOf( + val -> assertThat(val).isNull(), + val -> assertThat(val).isEqualTo("ipv4"), + val -> assertThat(val).isEqualTo("ipv6")))))); + } +} diff --git a/instrumentation/apache-dubbo-2.7/testing/src/main/java/io/opentelemetry/instrumentation/apachedubbo/v2_7/DubboTestUtil.java b/instrumentation/apache-dubbo-2.7/testing/src/main/java/io/opentelemetry/instrumentation/apachedubbo/v2_7/DubboTestUtil.java new file mode 100644 index 000000000000..1de3cef738ce --- /dev/null +++ b/instrumentation/apache-dubbo-2.7/testing/src/main/java/io/opentelemetry/instrumentation/apachedubbo/v2_7/DubboTestUtil.java @@ -0,0 +1,49 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.apachedubbo.v2_7; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import org.apache.dubbo.config.bootstrap.DubboBootstrap; + +/** compatible with dubbo3.x and dubbo 2.7 */ +class DubboTestUtil { + + private DubboTestUtil() {} + + static Object newFrameworkModel() { + try { + // only present in latest dep + return Class.forName("org.apache.dubbo.rpc.model.FrameworkModel") + .getDeclaredConstructor() + .newInstance(); + } catch (ReflectiveOperationException exception) { + return null; + } + } + + static DubboBootstrap newDubboBootstrap() throws ReflectiveOperationException { + Object newFrameworkModel = newFrameworkModel(); + if (newFrameworkModel == null) { + return newDubboBootstrapV27(); + } else { + return newDubboBootstrapV3(newFrameworkModel); + } + } + + private static DubboBootstrap newDubboBootstrapV3(Object newFrameworkModel) + throws ReflectiveOperationException { + Method getInstance = + DubboBootstrap.class.getDeclaredMethod("newInstance", newFrameworkModel.getClass()); + return (DubboBootstrap) getInstance.invoke(null, newFrameworkModel); + } + + private static DubboBootstrap newDubboBootstrapV27() throws ReflectiveOperationException { + Constructor constructor = DubboBootstrap.class.getDeclaredConstructor(); + constructor.setAccessible(true); + return constructor.newInstance(); + } +} diff --git a/instrumentation/apache-httpasyncclient-4.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpasyncclient/ApacheHttpAsyncClientHttpAttributesGetter.java b/instrumentation/apache-httpasyncclient-4.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpasyncclient/ApacheHttpAsyncClientHttpAttributesGetter.java index 5ba87caba4b6..2e754cf3548e 100644 --- a/instrumentation/apache-httpasyncclient-4.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpasyncclient/ApacheHttpAsyncClientHttpAttributesGetter.java +++ b/instrumentation/apache-httpasyncclient-4.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpasyncclient/ApacheHttpAsyncClientHttpAttributesGetter.java @@ -7,7 +7,8 @@ import static io.opentelemetry.javaagent.instrumentation.apachehttpasyncclient.ApacheHttpClientRequest.headersToList; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpClientAttributesGetter; +import io.opentelemetry.instrumentation.api.semconv.http.HttpClientAttributesGetter; +import java.net.InetSocketAddress; import java.util.List; import javax.annotation.Nullable; import org.apache.http.HttpResponse; @@ -44,4 +45,34 @@ public List getHttpResponseHeader( ApacheHttpClientRequest request, HttpResponse response, String name) { return headersToList(response.getHeaders(name)); } + + @Override + public String getNetworkProtocolName( + ApacheHttpClientRequest request, @Nullable HttpResponse response) { + return request.getProtocolName(); + } + + @Override + public String getNetworkProtocolVersion( + ApacheHttpClientRequest request, @Nullable HttpResponse response) { + return request.getProtocolVersion(); + } + + @Override + @Nullable + public String getServerAddress(ApacheHttpClientRequest request) { + return request.getServerAddress(); + } + + @Override + public Integer getServerPort(ApacheHttpClientRequest request) { + return request.getServerPort(); + } + + @Nullable + @Override + public InetSocketAddress getNetworkPeerInetSocketAddress( + ApacheHttpClientRequest request, @Nullable HttpResponse response) { + return request.getNetworkPeerAddress(); + } } diff --git a/instrumentation/apache-httpasyncclient-4.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpasyncclient/ApacheHttpAsyncClientNetAttributesGetter.java b/instrumentation/apache-httpasyncclient-4.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpasyncclient/ApacheHttpAsyncClientNetAttributesGetter.java deleted file mode 100644 index 20908bcf270e..000000000000 --- a/instrumentation/apache-httpasyncclient-4.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpasyncclient/ApacheHttpAsyncClientNetAttributesGetter.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.javaagent.instrumentation.apachehttpasyncclient; - -import io.opentelemetry.instrumentation.api.instrumenter.net.NetClientAttributesGetter; -import java.net.InetSocketAddress; -import javax.annotation.Nullable; -import org.apache.http.HttpResponse; - -final class ApacheHttpAsyncClientNetAttributesGetter - implements NetClientAttributesGetter { - - @Override - public String getNetworkProtocolName( - ApacheHttpClientRequest request, @Nullable HttpResponse response) { - return request.getProtocolName(); - } - - @Override - public String getNetworkProtocolVersion( - ApacheHttpClientRequest request, @Nullable HttpResponse response) { - return request.getProtocolVersion(); - } - - @Override - @Nullable - public String getServerAddress(ApacheHttpClientRequest request) { - return request.getServerAddress(); - } - - @Override - public Integer getServerPort(ApacheHttpClientRequest request) { - return request.getServerPort(); - } - - @Nullable - @Override - public InetSocketAddress getServerInetSocketAddress( - ApacheHttpClientRequest request, @Nullable HttpResponse response) { - return request.getServerSocketAddress(); - } -} diff --git a/instrumentation/apache-httpasyncclient-4.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpasyncclient/ApacheHttpAsyncClientSingletons.java b/instrumentation/apache-httpasyncclient-4.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpasyncclient/ApacheHttpAsyncClientSingletons.java index 152b9c7ca740..6a2af90f730e 100644 --- a/instrumentation/apache-httpasyncclient-4.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpasyncclient/ApacheHttpAsyncClientSingletons.java +++ b/instrumentation/apache-httpasyncclient-4.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpasyncclient/ApacheHttpAsyncClientSingletons.java @@ -5,14 +5,8 @@ package io.opentelemetry.javaagent.instrumentation.apachehttpasyncclient; -import io.opentelemetry.api.GlobalOpenTelemetry; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpClientAttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpClientMetrics; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpSpanNameExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpSpanStatusExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.net.PeerServiceAttributesExtractor; -import io.opentelemetry.javaagent.bootstrap.internal.CommonConfig; +import io.opentelemetry.javaagent.bootstrap.internal.JavaagentHttpClientInstrumenters; import org.apache.http.HttpResponse; public final class ApacheHttpAsyncClientSingletons { @@ -21,27 +15,11 @@ public final class ApacheHttpAsyncClientSingletons { private static final Instrumenter INSTRUMENTER; static { - ApacheHttpAsyncClientHttpAttributesGetter httpAttributesGetter = - new ApacheHttpAsyncClientHttpAttributesGetter(); - ApacheHttpAsyncClientNetAttributesGetter netAttributesGetter = - new ApacheHttpAsyncClientNetAttributesGetter(); - INSTRUMENTER = - Instrumenter.builder( - GlobalOpenTelemetry.get(), - INSTRUMENTATION_NAME, - HttpSpanNameExtractor.create(httpAttributesGetter)) - .setSpanStatusExtractor(HttpSpanStatusExtractor.create(httpAttributesGetter)) - .addAttributesExtractor( - HttpClientAttributesExtractor.builder(httpAttributesGetter, netAttributesGetter) - .setCapturedRequestHeaders(CommonConfig.get().getClientRequestHeaders()) - .setCapturedResponseHeaders(CommonConfig.get().getClientResponseHeaders()) - .build()) - .addAttributesExtractor( - PeerServiceAttributesExtractor.create( - netAttributesGetter, CommonConfig.get().getPeerServiceMapping())) - .addOperationMetrics(HttpClientMetrics.get()) - .buildClientInstrumenter(HttpHeaderSetter.INSTANCE); + JavaagentHttpClientInstrumenters.create( + INSTRUMENTATION_NAME, + new ApacheHttpAsyncClientHttpAttributesGetter(), + HttpHeaderSetter.INSTANCE); } public static Instrumenter instrumenter() { diff --git a/instrumentation/apache-httpasyncclient-4.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpasyncclient/ApacheHttpClientRequest.java b/instrumentation/apache-httpasyncclient-4.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpasyncclient/ApacheHttpClientRequest.java index 12421cce67cc..244e10c892a8 100644 --- a/instrumentation/apache-httpasyncclient-4.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpasyncclient/ApacheHttpClientRequest.java +++ b/instrumentation/apache-httpasyncclient-4.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpasyncclient/ApacheHttpClientRequest.java @@ -136,7 +136,7 @@ private static URI getCalculatedUri(HttpHost httpHost, URI uri) { } @Nullable - public InetSocketAddress getServerSocketAddress() { + public InetSocketAddress getNetworkPeerAddress() { if (target == null) { return null; } diff --git a/instrumentation/apache-httpasyncclient-4.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apachehttpasyncclient/ApacheHttpAsyncClientTest.java b/instrumentation/apache-httpasyncclient-4.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apachehttpasyncclient/ApacheHttpAsyncClientTest.java index ca28b7a38d83..c435fe49067c 100644 --- a/instrumentation/apache-httpasyncclient-4.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apachehttpasyncclient/ApacheHttpAsyncClientTest.java +++ b/instrumentation/apache-httpasyncclient-4.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apachehttpasyncclient/ApacheHttpAsyncClientTest.java @@ -5,19 +5,15 @@ package io.opentelemetry.javaagent.instrumentation.apachehttpasyncclient; -import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpClientTest; import io.opentelemetry.instrumentation.testing.junit.http.HttpClientInstrumentationExtension; import io.opentelemetry.instrumentation.testing.junit.http.HttpClientResult; import io.opentelemetry.instrumentation.testing.junit.http.HttpClientTestOptions; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; import java.io.IOException; import java.io.UncheckedIOException; import java.net.URI; -import java.util.HashSet; import java.util.Map; -import java.util.Set; import java.util.concurrent.CancellationException; import org.apache.http.HttpHost; import org.apache.http.HttpResponse; @@ -74,7 +70,7 @@ HttpAsyncClient getClient(URI uri) { } @Nested - class ApacheClientUriRequestTest extends AbstractHttpClientTest { + class ApacheClientUriRequestTest extends AbstractTest { @Override public HttpUriRequest buildRequest(String method, URI uri, Map headers) { @@ -97,15 +93,10 @@ public void sendRequestWithCallback( HttpClientResult httpClientResult) { getClient(uri).execute(request, responseCallback(httpClientResult)); } - - @Override - protected void configure(HttpClientTestOptions.Builder optionsBuilder) { - configureTest(optionsBuilder); - } } @Nested - class ApacheClientHostRequestTest extends AbstractHttpClientTest { + class ApacheClientHostRequestTest extends AbstractTest { @Override public HttpUriRequest buildRequest(String method, URI uri, Map headers) { @@ -136,15 +127,10 @@ public void sendRequestWithCallback( request, responseCallback(httpClientResult)); } - - @Override - protected void configure(HttpClientTestOptions.Builder optionsBuilder) { - configureTest(optionsBuilder); - } } @Nested - class ApacheClientHostAbsoluteUriRequestTest extends AbstractHttpClientTest { + class ApacheClientHostAbsoluteUriRequestTest extends AbstractTest { @Override public HttpUriRequest buildRequest(String method, URI uri, Map headers) { @@ -174,10 +160,14 @@ public void sendRequestWithCallback( request, responseCallback(httpClientResult)); } + } + + abstract static class AbstractTest extends AbstractHttpClientTest { @Override protected void configure(HttpClientTestOptions.Builder optionsBuilder) { - configureTest(optionsBuilder); + super.configure(optionsBuilder); + optionsBuilder.spanEndsAfterBody(); } } @@ -224,18 +214,6 @@ public void cancelled() { }; } - void configureTest(HttpClientTestOptions.Builder optionsBuilder) { - optionsBuilder.setUserAgent("httpasyncclient"); - optionsBuilder.setHttpAttributes( - endpoint -> { - Set> attributes = - new HashSet<>(HttpClientTestOptions.DEFAULT_HTTP_ATTRIBUTES); - attributes.add(SemanticAttributes.HTTP_SCHEME); - attributes.add(SemanticAttributes.HTTP_TARGET); - return attributes; - }); - } - static String fullPathFromUri(URI uri) { StringBuilder builder = new StringBuilder(); if (uri.getPath() != null) { diff --git a/instrumentation/apache-httpclient/apache-httpclient-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v2_0/ApacheHttpClientHttpAttributesGetter.java b/instrumentation/apache-httpclient/apache-httpclient-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v2_0/ApacheHttpClientHttpAttributesGetter.java index ab2bd6fe0011..4ba160af9177 100644 --- a/instrumentation/apache-httpclient/apache-httpclient-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v2_0/ApacheHttpClientHttpAttributesGetter.java +++ b/instrumentation/apache-httpclient/apache-httpclient-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v2_0/ApacheHttpClientHttpAttributesGetter.java @@ -8,12 +8,13 @@ import static java.util.Collections.emptyList; import static java.util.Collections.singletonList; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpClientAttributesGetter; +import io.opentelemetry.instrumentation.api.semconv.http.HttpClientAttributesGetter; import java.util.List; import javax.annotation.Nullable; 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; final class ApacheHttpClientHttpAttributesGetter @@ -74,4 +75,32 @@ public List getHttpResponseHeader(HttpMethod request, HttpMethod respons Header header = response.getResponseHeader(name); return header == null ? emptyList() : singletonList(header.getValue()); } + + @Override + public String getNetworkProtocolName(HttpMethod request, @Nullable HttpMethod response) { + return "http"; + } + + @Nullable + @Override + public String getNetworkProtocolVersion(HttpMethod request, @Nullable HttpMethod response) { + if (request instanceof HttpMethodBase) { + return ((HttpMethodBase) request).isHttp11() ? "1.1" : "1.0"; + } + return null; + } + + @Override + @Nullable + public String getServerAddress(HttpMethod request) { + HostConfiguration hostConfiguration = request.getHostConfiguration(); + return hostConfiguration != null ? hostConfiguration.getHost() : null; + } + + @Override + @Nullable + public Integer getServerPort(HttpMethod request) { + HostConfiguration hostConfiguration = request.getHostConfiguration(); + return hostConfiguration != null ? hostConfiguration.getPort() : null; + } } diff --git a/instrumentation/apache-httpclient/apache-httpclient-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v2_0/ApacheHttpClientNetAttributesGetter.java b/instrumentation/apache-httpclient/apache-httpclient-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v2_0/ApacheHttpClientNetAttributesGetter.java deleted file mode 100644 index a5a1d1dc7e28..000000000000 --- a/instrumentation/apache-httpclient/apache-httpclient-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v2_0/ApacheHttpClientNetAttributesGetter.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * 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.NetClientAttributesGetter; -import javax.annotation.Nullable; -import org.apache.commons.httpclient.HostConfiguration; -import org.apache.commons.httpclient.HttpMethod; -import org.apache.commons.httpclient.HttpMethodBase; - -final class ApacheHttpClientNetAttributesGetter - implements NetClientAttributesGetter { - - @Override - public String getNetworkProtocolName(HttpMethod request, @Nullable HttpMethod response) { - return "http"; - } - - @Nullable - @Override - public String getNetworkProtocolVersion(HttpMethod request, @Nullable HttpMethod response) { - if (request instanceof HttpMethodBase) { - return ((HttpMethodBase) request).isHttp11() ? "1.1" : "1.0"; - } - return null; - } - - @Override - @Nullable - public String getServerAddress(HttpMethod request) { - HostConfiguration hostConfiguration = request.getHostConfiguration(); - return hostConfiguration != null ? hostConfiguration.getHost() : null; - } - - @Override - @Nullable - public Integer getServerPort(HttpMethod request) { - HostConfiguration hostConfiguration = request.getHostConfiguration(); - return hostConfiguration != null ? hostConfiguration.getPort() : null; - } -} diff --git a/instrumentation/apache-httpclient/apache-httpclient-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v2_0/ApacheHttpClientSingletons.java b/instrumentation/apache-httpclient/apache-httpclient-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v2_0/ApacheHttpClientSingletons.java index e78afadb23e3..ef8e120c78c9 100644 --- a/instrumentation/apache-httpclient/apache-httpclient-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v2_0/ApacheHttpClientSingletons.java +++ b/instrumentation/apache-httpclient/apache-httpclient-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v2_0/ApacheHttpClientSingletons.java @@ -5,14 +5,8 @@ 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.http.HttpClientAttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpClientMetrics; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpSpanNameExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpSpanStatusExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.net.PeerServiceAttributesExtractor; -import io.opentelemetry.javaagent.bootstrap.internal.CommonConfig; +import io.opentelemetry.javaagent.bootstrap.internal.JavaagentHttpClientInstrumenters; import org.apache.commons.httpclient.HttpMethod; public final class ApacheHttpClientSingletons { @@ -21,27 +15,11 @@ public final class ApacheHttpClientSingletons { private static final Instrumenter INSTRUMENTER; static { - ApacheHttpClientHttpAttributesGetter httpAttributesGetter = - new ApacheHttpClientHttpAttributesGetter(); - ApacheHttpClientNetAttributesGetter netAttributesGetter = - new ApacheHttpClientNetAttributesGetter(); - INSTRUMENTER = - Instrumenter.builder( - GlobalOpenTelemetry.get(), - INSTRUMENTATION_NAME, - HttpSpanNameExtractor.create(httpAttributesGetter)) - .setSpanStatusExtractor(HttpSpanStatusExtractor.create(httpAttributesGetter)) - .addAttributesExtractor( - HttpClientAttributesExtractor.builder(httpAttributesGetter, netAttributesGetter) - .setCapturedRequestHeaders(CommonConfig.get().getClientRequestHeaders()) - .setCapturedResponseHeaders(CommonConfig.get().getClientResponseHeaders()) - .build()) - .addAttributesExtractor( - PeerServiceAttributesExtractor.create( - netAttributesGetter, CommonConfig.get().getPeerServiceMapping())) - .addOperationMetrics(HttpClientMetrics.get()) - .buildClientInstrumenter(HttpHeaderSetter.INSTANCE); + JavaagentHttpClientInstrumenters.create( + INSTRUMENTATION_NAME, + new ApacheHttpClientHttpAttributesGetter(), + HttpHeaderSetter.INSTANCE); } public static Instrumenter instrumenter() { diff --git a/instrumentation/apache-httpclient/apache-httpclient-2.0/javaagent/src/test/groovy/AbstractCommonsHttpClientTest.groovy b/instrumentation/apache-httpclient/apache-httpclient-2.0/javaagent/src/test/groovy/AbstractCommonsHttpClientTest.groovy deleted file mode 100644 index 4163c7c6f552..000000000000 --- a/instrumentation/apache-httpclient/apache-httpclient-2.0/javaagent/src/test/groovy/AbstractCommonsHttpClientTest.groovy +++ /dev/null @@ -1,104 +0,0 @@ -/* - * 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.HttpConnectionManager -import org.apache.commons.httpclient.HttpMethod -import org.apache.commons.httpclient.MultiThreadedHttpConnectionManager -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 - -abstract class AbstractCommonsHttpClientTest extends HttpClientTest implements AgentTestTrait { - @Shared - HttpConnectionManager connectionManager = new MultiThreadedHttpConnectionManager() - @Shared - HttpClient client = buildClient(false) - @Shared - HttpClient clientWithReadTimeout = buildClient(true) - - def buildClient(boolean readTimeout) { - HttpClient client = new HttpClient(connectionManager) - client.setConnectionTimeout(CONNECT_TIMEOUT_MS) - if (readTimeout) { - client.setTimeout(READ_TIMEOUT_MS) - } - return client - } - - HttpClient getClient(URI uri) { - if (uri.toString().contains("/read-timeout")) { - return clientWithReadTimeout - } - return client - } - - @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 - boolean testCircularRedirects() { - 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/instrumentation/apache-httpclient/apache-httpclient-2.0/javaagent/src/test/groovy/CommonsHttpClientLatestDepsTest.groovy b/instrumentation/apache-httpclient/apache-httpclient-2.0/javaagent/src/test/groovy/CommonsHttpClientLatestDepsTest.groovy deleted file mode 100644 index 0a68757a007b..000000000000 --- a/instrumentation/apache-httpclient/apache-httpclient-2.0/javaagent/src/test/groovy/CommonsHttpClientLatestDepsTest.groovy +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -import org.apache.commons.httpclient.HttpMethod -import spock.lang.IgnoreIf - -//this test will be ignored if not executed with -PtestLatestDeps=true -//because the latest dependency commons-httpclient v3.1 allows a call to the executeMethod -//with some null parameters like HttpClient.executeMethod(null, request, null) -//but this construct is not allowed in commons-httpclient v2 that is used for regular otel testing -@IgnoreIf({ !Boolean.getBoolean("testLatestDeps") }) -class CommonsHttpClientLatestDepsTest extends AbstractCommonsHttpClientTest { - - @Override - int sendRequest(HttpMethod request, String method, URI uri, Map headers) { - try { - getClient(uri).executeMethod(null, request, null) - return request.getStatusCode() - } finally { - request.releaseConnection() - } - } -} diff --git a/instrumentation/apache-httpclient/apache-httpclient-2.0/javaagent/src/test/groovy/CommonsHttpClientTest.groovy b/instrumentation/apache-httpclient/apache-httpclient-2.0/javaagent/src/test/groovy/CommonsHttpClientTest.groovy deleted file mode 100644 index 7502f15c1603..000000000000 --- a/instrumentation/apache-httpclient/apache-httpclient-2.0/javaagent/src/test/groovy/CommonsHttpClientTest.groovy +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -import org.apache.commons.httpclient.HttpMethod - -class CommonsHttpClientTest extends AbstractCommonsHttpClientTest { - - @Override - int sendRequest(HttpMethod request, String method, URI uri, Map headers) { - try { - getClient(uri).executeMethod(request) - return request.getStatusCode() - } finally { - request.releaseConnection() - } - } -} diff --git a/instrumentation/apache-httpclient/apache-httpclient-2.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v2_0/AbstractCommonsHttpClientTest.java b/instrumentation/apache-httpclient/apache-httpclient-2.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v2_0/AbstractCommonsHttpClientTest.java new file mode 100644 index 000000000000..3575b63f4d91 --- /dev/null +++ b/instrumentation/apache-httpclient/apache-httpclient-2.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v2_0/AbstractCommonsHttpClientTest.java @@ -0,0 +1,96 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.apachehttpclient.v2_0; + +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpClientTest; +import io.opentelemetry.instrumentation.testing.junit.http.HttpClientInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.http.HttpClientTestOptions; +import java.net.URI; +import java.util.Map; +import org.apache.commons.httpclient.HttpClient; +import org.apache.commons.httpclient.HttpConnectionManager; +import org.apache.commons.httpclient.HttpMethod; +import org.apache.commons.httpclient.MultiThreadedHttpConnectionManager; +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 org.junit.jupiter.api.extension.RegisterExtension; + +abstract class AbstractCommonsHttpClientTest extends AbstractHttpClientTest { + + @RegisterExtension + static final InstrumentationExtension testing = HttpClientInstrumentationExtension.forAgent(); + + private static final HttpConnectionManager connectionManager = + new MultiThreadedHttpConnectionManager(); + private static final HttpClient client = buildClient(false); + private static final HttpClient clientWithReadTimeout = buildClient(true); + + static HttpClient buildClient(boolean readTimeout) { + HttpClient client = new HttpClient(connectionManager); + client.setConnectionTimeout((int) CONNECTION_TIMEOUT.toMillis()); + if (readTimeout) { + client.setTimeout((int) READ_TIMEOUT.toMillis()); + } + return client; + } + + HttpClient getClient(URI uri) { + if (uri.toString().contains("/read-timeout")) { + return clientWithReadTimeout; + } + return client; + } + + @Override + public HttpMethod buildRequest(String method, URI uri, Map headers) { + HttpMethod 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); + } + + for (Map.Entry entry : headers.entrySet()) { + request.setRequestHeader(entry.getKey(), entry.getValue()); + } + return request; + } + + @Override + protected void configure(HttpClientTestOptions.Builder optionsBuilder) { + optionsBuilder + .disableTestCallback() + .disableTestReusedRequest() + .disableTestNonStandardHttpMethod() + .disableTestCircularRedirects(); + } +} diff --git a/instrumentation/apache-httpclient/apache-httpclient-2.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v2_0/CommonsHttpClientLatestDepsTest.java b/instrumentation/apache-httpclient/apache-httpclient-2.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v2_0/CommonsHttpClientLatestDepsTest.java new file mode 100644 index 000000000000..a482ddeff860 --- /dev/null +++ b/instrumentation/apache-httpclient/apache-httpclient-2.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v2_0/CommonsHttpClientLatestDepsTest.java @@ -0,0 +1,29 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.apachehttpclient.v2_0; + +import java.net.URI; +import java.util.Map; +import org.apache.commons.httpclient.HttpMethod; +import org.junit.jupiter.api.condition.EnabledIfSystemProperty; + +// this test will be ignored if not executed with -PtestLatestDeps=true +// because the latest dependency commons-httpclient v3.1 allows a call to the executeMethod +// with some null parameters like HttpClient.executeMethod(null, request, null) +// but this construct is not allowed in commons-httpclient v2 that is used for regular otel testing +@EnabledIfSystemProperty(named = "testLatestDeps", matches = "true") +public class CommonsHttpClientLatestDepsTest extends AbstractCommonsHttpClientTest { + @Override + public int sendRequest(HttpMethod request, String method, URI uri, Map headers) + throws Exception { + try { + getClient(uri).executeMethod(null, request, null); + return request.getStatusCode(); + } finally { + request.releaseConnection(); + } + } +} diff --git a/instrumentation/apache-httpclient/apache-httpclient-2.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v2_0/CommonsHttpClientTest.java b/instrumentation/apache-httpclient/apache-httpclient-2.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v2_0/CommonsHttpClientTest.java new file mode 100644 index 000000000000..ddcbe7d3af1f --- /dev/null +++ b/instrumentation/apache-httpclient/apache-httpclient-2.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v2_0/CommonsHttpClientTest.java @@ -0,0 +1,24 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.apachehttpclient.v2_0; + +import java.net.URI; +import java.util.Map; +import org.apache.commons.httpclient.HttpMethod; + +class CommonsHttpClientTest extends AbstractCommonsHttpClientTest { + + @Override + public int sendRequest(HttpMethod request, String method, URI uri, Map headers) + throws Exception { + try { + getClient(uri).executeMethod(request); + return request.getStatusCode(); + } finally { + request.releaseConnection(); + } + } +} diff --git a/instrumentation/apache-httpclient/apache-httpclient-4.0/javaagent/build.gradle.kts b/instrumentation/apache-httpclient/apache-httpclient-4.0/javaagent/build.gradle.kts index 0da7c576db72..efa347eeefe5 100644 --- a/instrumentation/apache-httpclient/apache-httpclient-4.0/javaagent/build.gradle.kts +++ b/instrumentation/apache-httpclient/apache-httpclient-4.0/javaagent/build.gradle.kts @@ -20,6 +20,8 @@ muzzle { module.set("dropwizard-client") versions.set("(,3.0.0)") assertInverse.set(true) + // Could not find com.google.code.findbugs:jsr305:. + skip("3.0.2", "4.0.2") } } diff --git a/instrumentation/apache-httpclient/apache-httpclient-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v4_0/ApacheHttpClientHttpAttributesGetter.java b/instrumentation/apache-httpclient/apache-httpclient-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v4_0/ApacheHttpClientHttpAttributesGetter.java index b536559c71a5..d5b715414c0e 100644 --- a/instrumentation/apache-httpclient/apache-httpclient-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v4_0/ApacheHttpClientHttpAttributesGetter.java +++ b/instrumentation/apache-httpclient/apache-httpclient-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v4_0/ApacheHttpClientHttpAttributesGetter.java @@ -7,7 +7,7 @@ import static io.opentelemetry.javaagent.instrumentation.apachehttpclient.v4_0.ApacheHttpClientRequest.headersToList; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpClientAttributesGetter; +import io.opentelemetry.instrumentation.api.semconv.http.HttpClientAttributesGetter; import java.util.List; import javax.annotation.Nullable; import org.apache.http.HttpResponse; @@ -41,4 +41,27 @@ public List getHttpResponseHeader( ApacheHttpClientRequest request, HttpResponse response, String name) { return headersToList(response.getHeaders(name)); } + + @Override + public String getNetworkProtocolName( + ApacheHttpClientRequest request, @Nullable HttpResponse response) { + return request.getProtocolName(); + } + + @Override + public String getNetworkProtocolVersion( + ApacheHttpClientRequest request, @Nullable HttpResponse response) { + return request.getProtocolVersion(); + } + + @Override + @Nullable + public String getServerAddress(ApacheHttpClientRequest request) { + return request.getServerAddress(); + } + + @Override + public Integer getServerPort(ApacheHttpClientRequest request) { + return request.getServerPort(); + } } diff --git a/instrumentation/apache-httpclient/apache-httpclient-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v4_0/ApacheHttpClientNetAttributesGetter.java b/instrumentation/apache-httpclient/apache-httpclient-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v4_0/ApacheHttpClientNetAttributesGetter.java deleted file mode 100644 index 3220cc31cb7e..000000000000 --- a/instrumentation/apache-httpclient/apache-httpclient-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v4_0/ApacheHttpClientNetAttributesGetter.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * 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.NetClientAttributesGetter; -import javax.annotation.Nullable; -import org.apache.http.HttpResponse; - -final class ApacheHttpClientNetAttributesGetter - implements NetClientAttributesGetter { - - @Override - public String getNetworkProtocolName( - ApacheHttpClientRequest request, @Nullable HttpResponse response) { - return request.getProtocolName(); - } - - @Override - public String getNetworkProtocolVersion( - ApacheHttpClientRequest request, @Nullable HttpResponse response) { - return request.getProtocolVersion(); - } - - @Override - @Nullable - public String getServerAddress(ApacheHttpClientRequest request) { - return request.getServerAddress(); - } - - @Override - public Integer getServerPort(ApacheHttpClientRequest request) { - return request.getServerPort(); - } -} diff --git a/instrumentation/apache-httpclient/apache-httpclient-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v4_0/ApacheHttpClientRequest.java b/instrumentation/apache-httpclient/apache-httpclient-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v4_0/ApacheHttpClientRequest.java index e6f2be2b7a4e..90ff34f6929e 100644 --- a/instrumentation/apache-httpclient/apache-httpclient-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v4_0/ApacheHttpClientRequest.java +++ b/instrumentation/apache-httpclient/apache-httpclient-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v4_0/ApacheHttpClientRequest.java @@ -77,6 +77,9 @@ String getProtocolName() { String getProtocolVersion() { ProtocolVersion protocolVersion = delegate.getProtocolVersion(); + if (protocolVersion.getMinor() == 0) { + return Integer.toString(protocolVersion.getMajor()); + } return protocolVersion.getMajor() + "." + protocolVersion.getMinor(); } diff --git a/instrumentation/apache-httpclient/apache-httpclient-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v4_0/ApacheHttpClientSingletons.java b/instrumentation/apache-httpclient/apache-httpclient-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v4_0/ApacheHttpClientSingletons.java index 095f132776d6..a93209c3f42a 100644 --- a/instrumentation/apache-httpclient/apache-httpclient-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v4_0/ApacheHttpClientSingletons.java +++ b/instrumentation/apache-httpclient/apache-httpclient-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v4_0/ApacheHttpClientSingletons.java @@ -5,14 +5,8 @@ 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.http.HttpClientAttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpClientMetrics; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpSpanNameExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpSpanStatusExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.net.PeerServiceAttributesExtractor; -import io.opentelemetry.javaagent.bootstrap.internal.CommonConfig; +import io.opentelemetry.javaagent.bootstrap.internal.JavaagentHttpClientInstrumenters; import org.apache.http.HttpResponse; public final class ApacheHttpClientSingletons { @@ -21,27 +15,11 @@ public final class ApacheHttpClientSingletons { private static final Instrumenter INSTRUMENTER; static { - ApacheHttpClientHttpAttributesGetter httpAttributesGetter = - new ApacheHttpClientHttpAttributesGetter(); - ApacheHttpClientNetAttributesGetter netAttributesGetter = - new ApacheHttpClientNetAttributesGetter(); - INSTRUMENTER = - Instrumenter.builder( - GlobalOpenTelemetry.get(), - INSTRUMENTATION_NAME, - HttpSpanNameExtractor.create(httpAttributesGetter)) - .setSpanStatusExtractor(HttpSpanStatusExtractor.create(httpAttributesGetter)) - .addAttributesExtractor( - HttpClientAttributesExtractor.builder(httpAttributesGetter, netAttributesGetter) - .setCapturedRequestHeaders(CommonConfig.get().getClientRequestHeaders()) - .setCapturedResponseHeaders(CommonConfig.get().getClientResponseHeaders()) - .build()) - .addAttributesExtractor( - PeerServiceAttributesExtractor.create( - netAttributesGetter, CommonConfig.get().getPeerServiceMapping())) - .addOperationMetrics(HttpClientMetrics.get()) - .buildClientInstrumenter(HttpHeaderSetter.INSTANCE); + JavaagentHttpClientInstrumenters.create( + INSTRUMENTATION_NAME, + new ApacheHttpClientHttpAttributesGetter(), + HttpHeaderSetter.INSTANCE); } public static Instrumenter instrumenter() { diff --git a/instrumentation/apache-httpclient/apache-httpclient-4.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v4_0/AbstractApacheHttpClientTest.java b/instrumentation/apache-httpclient/apache-httpclient-4.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v4_0/AbstractApacheHttpClientTest.java index 4b8fd661b8ab..ed90075de0ae 100644 --- a/instrumentation/apache-httpclient/apache-httpclient-4.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v4_0/AbstractApacheHttpClientTest.java +++ b/instrumentation/apache-httpclient/apache-httpclient-4.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v4_0/AbstractApacheHttpClientTest.java @@ -5,13 +5,10 @@ package io.opentelemetry.javaagent.instrumentation.apachehttpclient.v4_0; -import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpClientTest; import io.opentelemetry.instrumentation.testing.junit.http.HttpClientResult; -import io.opentelemetry.instrumentation.testing.junit.http.HttpClientTestOptions; import java.net.URI; import java.util.Map; -import java.util.Set; import org.apache.http.HttpHost; import org.apache.http.HttpRequest; import org.apache.http.HttpResponse; @@ -21,22 +18,10 @@ abstract class AbstractApacheHttpClientTest extends AbstractHttpClientTest { - private static final String USER_AGENT = "apachehttpclient"; - - @Override - protected void configure(HttpClientTestOptions.Builder optionsBuilder) { - optionsBuilder.setUserAgent(USER_AGENT); - optionsBuilder.setHttpAttributes(AbstractApacheHttpClientTest::getHttpAttributes); - } - - private static Set> getHttpAttributes(URI endpoint) { - return HttpClientTestOptions.DEFAULT_HTTP_ATTRIBUTES; - } - @Override public T buildRequest(String method, URI uri, Map headers) { T request = createRequest(method, uri); - request.addHeader("user-agent", USER_AGENT); + request.addHeader("user-agent", "apachehttpclient"); headers.forEach(request::setHeader); return request; } diff --git a/instrumentation/apache-httpclient/apache-httpclient-4.3/library/src/main/java/io/opentelemetry/instrumentation/apachehttpclient/v4_3/ApacheHttpClientHttpAttributesGetter.java b/instrumentation/apache-httpclient/apache-httpclient-4.3/library/src/main/java/io/opentelemetry/instrumentation/apachehttpclient/v4_3/ApacheHttpClientHttpAttributesGetter.java index f34dff2bf968..27f59ee2e903 100644 --- a/instrumentation/apache-httpclient/apache-httpclient-4.3/library/src/main/java/io/opentelemetry/instrumentation/apachehttpclient/v4_3/ApacheHttpClientHttpAttributesGetter.java +++ b/instrumentation/apache-httpclient/apache-httpclient-4.3/library/src/main/java/io/opentelemetry/instrumentation/apachehttpclient/v4_3/ApacheHttpClientHttpAttributesGetter.java @@ -7,7 +7,8 @@ import static io.opentelemetry.instrumentation.apachehttpclient.v4_3.ApacheHttpClientRequest.headersToList; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpClientAttributesGetter; +import io.opentelemetry.instrumentation.api.semconv.http.HttpClientAttributesGetter; +import java.net.InetSocketAddress; import java.util.List; import javax.annotation.Nullable; import org.apache.http.HttpResponse; @@ -43,4 +44,35 @@ public List getHttpResponseHeader( ApacheHttpClientRequest request, HttpResponse response, String name) { return headersToList(response.getHeaders(name)); } + + @Override + public String getNetworkProtocolName( + ApacheHttpClientRequest request, @Nullable HttpResponse response) { + return request.getProtocolName(); + } + + @Override + public String getNetworkProtocolVersion( + ApacheHttpClientRequest request, @Nullable HttpResponse response) { + return request.getProtocolVersion(); + } + + @Override + @Nullable + public String getServerAddress(ApacheHttpClientRequest request) { + return request.getServerAddress(); + } + + @Override + @Nullable + public Integer getServerPort(ApacheHttpClientRequest request) { + return request.getServerPort(); + } + + @Nullable + @Override + public InetSocketAddress getNetworkPeerInetSocketAddress( + ApacheHttpClientRequest request, @Nullable HttpResponse response) { + return request.getNetworkPeerAddress(); + } } diff --git a/instrumentation/apache-httpclient/apache-httpclient-4.3/library/src/main/java/io/opentelemetry/instrumentation/apachehttpclient/v4_3/ApacheHttpClientNetAttributesGetter.java b/instrumentation/apache-httpclient/apache-httpclient-4.3/library/src/main/java/io/opentelemetry/instrumentation/apachehttpclient/v4_3/ApacheHttpClientNetAttributesGetter.java deleted file mode 100644 index 6a3e2131ae4b..000000000000 --- a/instrumentation/apache-httpclient/apache-httpclient-4.3/library/src/main/java/io/opentelemetry/instrumentation/apachehttpclient/v4_3/ApacheHttpClientNetAttributesGetter.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.apachehttpclient.v4_3; - -import io.opentelemetry.instrumentation.api.instrumenter.net.NetClientAttributesGetter; -import java.net.InetSocketAddress; -import javax.annotation.Nullable; -import org.apache.http.HttpResponse; - -final class ApacheHttpClientNetAttributesGetter - implements NetClientAttributesGetter { - - @Override - public String getNetworkProtocolName( - ApacheHttpClientRequest request, @Nullable HttpResponse response) { - return request.getProtocolName(); - } - - @Override - public String getNetworkProtocolVersion( - ApacheHttpClientRequest request, @Nullable HttpResponse response) { - return request.getProtocolVersion(); - } - - @Override - @Nullable - public String getServerAddress(ApacheHttpClientRequest request) { - return request.getServerAddress(); - } - - @Override - @Nullable - public Integer getServerPort(ApacheHttpClientRequest request) { - return request.getServerPort(); - } - - @Nullable - @Override - public InetSocketAddress getServerInetSocketAddress( - ApacheHttpClientRequest request, @Nullable HttpResponse response) { - return request.getServerSocketAddress(); - } -} diff --git a/instrumentation/apache-httpclient/apache-httpclient-4.3/library/src/main/java/io/opentelemetry/instrumentation/apachehttpclient/v4_3/ApacheHttpClientRequest.java b/instrumentation/apache-httpclient/apache-httpclient-4.3/library/src/main/java/io/opentelemetry/instrumentation/apachehttpclient/v4_3/ApacheHttpClientRequest.java index 8fc9dd376f8d..4d5a26746333 100644 --- a/instrumentation/apache-httpclient/apache-httpclient-4.3/library/src/main/java/io/opentelemetry/instrumentation/apachehttpclient/v4_3/ApacheHttpClientRequest.java +++ b/instrumentation/apache-httpclient/apache-httpclient-4.3/library/src/main/java/io/opentelemetry/instrumentation/apachehttpclient/v4_3/ApacheHttpClientRequest.java @@ -81,6 +81,9 @@ String getProtocolName() { String getProtocolVersion() { ProtocolVersion protocolVersion = delegate.getProtocolVersion(); + if (protocolVersion.getMinor() == 0) { + return Integer.toString(protocolVersion.getMajor()); + } return protocolVersion.getMajor() + "." + protocolVersion.getMinor(); } @@ -123,7 +126,7 @@ private static URI getCalculatedUri(HttpHost httpHost, URI uri) { } @Nullable - public InetSocketAddress getServerSocketAddress() { + public InetSocketAddress getNetworkPeerAddress() { if (target == null) { return null; } diff --git a/instrumentation/apache-httpclient/apache-httpclient-4.3/library/src/main/java/io/opentelemetry/instrumentation/apachehttpclient/v4_3/ApacheHttpClientTelemetryBuilder.java b/instrumentation/apache-httpclient/apache-httpclient-4.3/library/src/main/java/io/opentelemetry/instrumentation/apachehttpclient/v4_3/ApacheHttpClientTelemetryBuilder.java index ad5c8ef030f2..7508931128b1 100644 --- a/instrumentation/apache-httpclient/apache-httpclient-4.3/library/src/main/java/io/opentelemetry/instrumentation/apachehttpclient/v4_3/ApacheHttpClientTelemetryBuilder.java +++ b/instrumentation/apache-httpclient/apache-httpclient-4.3/library/src/main/java/io/opentelemetry/instrumentation/apachehttpclient/v4_3/ApacheHttpClientTelemetryBuilder.java @@ -7,35 +7,25 @@ import com.google.errorprone.annotations.CanIgnoreReturnValue; import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.instrumentation.api.incubator.builder.internal.DefaultHttpClientInstrumenterBuilder; 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.http.HttpClientAttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpClientAttributesExtractorBuilder; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpClientMetrics; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpSpanNameExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpSpanStatusExtractor; -import java.util.ArrayList; +import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor; +import io.opentelemetry.instrumentation.api.semconv.http.HttpClientAttributesExtractorBuilder; import java.util.List; +import java.util.Set; +import java.util.function.Function; import org.apache.http.HttpResponse; /** A builder for {@link ApacheHttpClientTelemetry}. */ public final class ApacheHttpClientTelemetryBuilder { private static final String INSTRUMENTATION_NAME = "io.opentelemetry.apache-httpclient-4.3"; - - private final OpenTelemetry openTelemetry; - - private final List> - additionalExtractors = new ArrayList<>(); - private final HttpClientAttributesExtractorBuilder - httpAttributesExtractorBuilder = - HttpClientAttributesExtractor.builder( - ApacheHttpClientHttpAttributesGetter.INSTANCE, - new ApacheHttpClientNetAttributesGetter()); + private final DefaultHttpClientInstrumenterBuilder builder; ApacheHttpClientTelemetryBuilder(OpenTelemetry openTelemetry) { - this.openTelemetry = openTelemetry; + builder = + new DefaultHttpClientInstrumenterBuilder<>( + INSTRUMENTATION_NAME, openTelemetry, ApacheHttpClientHttpAttributesGetter.INSTANCE); } /** @@ -46,7 +36,7 @@ public final class ApacheHttpClientTelemetryBuilder { public ApacheHttpClientTelemetryBuilder addAttributeExtractor( AttributesExtractor attributesExtractor) { - additionalExtractors.add(attributesExtractor); + builder.addAttributeExtractor(attributesExtractor); return this; } @@ -57,7 +47,7 @@ public ApacheHttpClientTelemetryBuilder addAttributeExtractor( */ @CanIgnoreReturnValue public ApacheHttpClientTelemetryBuilder setCapturedRequestHeaders(List requestHeaders) { - httpAttributesExtractorBuilder.setCapturedRequestHeaders(requestHeaders); + builder.setCapturedRequestHeaders(requestHeaders); return this; } @@ -68,7 +58,50 @@ public ApacheHttpClientTelemetryBuilder setCapturedRequestHeaders(List r */ @CanIgnoreReturnValue public ApacheHttpClientTelemetryBuilder setCapturedResponseHeaders(List responseHeaders) { - httpAttributesExtractorBuilder.setCapturedResponseHeaders(responseHeaders); + builder.setCapturedResponseHeaders(responseHeaders); + return this; + } + + /** + * Configures the instrumentation to recognize an alternative set of HTTP request methods. + * + *

By default, this instrumentation defines "known" methods as the ones listed in RFC9110 and the PATCH + * method defined in RFC5789. + * + *

Note: calling this method overrides the default known method sets completely; it does + * not supplement it. + * + * @param knownMethods A set of recognized HTTP request methods. + * @see HttpClientAttributesExtractorBuilder#setKnownMethods(Set) + */ + @CanIgnoreReturnValue + public ApacheHttpClientTelemetryBuilder setKnownMethods(Set knownMethods) { + builder.setKnownMethods(knownMethods); + return this; + } + + /** + * Configures the instrumentation to emit experimental HTTP client metrics. + * + * @param emitExperimentalHttpClientMetrics {@code true} if the experimental HTTP client metrics + * are to be emitted. + */ + @CanIgnoreReturnValue + public ApacheHttpClientTelemetryBuilder setEmitExperimentalHttpClientMetrics( + boolean emitExperimentalHttpClientMetrics) { + builder.setEmitExperimentalHttpClientMetrics(emitExperimentalHttpClientMetrics); + return this; + } + + /** Sets custom {@link SpanNameExtractor} via transform function. */ + @CanIgnoreReturnValue + public ApacheHttpClientTelemetryBuilder setSpanNameExtractor( + Function< + SpanNameExtractor, + ? extends SpanNameExtractor> + spanNameExtractorTransformer) { + builder.setSpanNameExtractor(spanNameExtractorTransformer); return this; } @@ -77,21 +110,7 @@ public ApacheHttpClientTelemetryBuilder setCapturedResponseHeaders(List * ApacheHttpClientTelemetryBuilder}. */ public ApacheHttpClientTelemetry build() { - ApacheHttpClientHttpAttributesGetter httpAttributesGetter = - ApacheHttpClientHttpAttributesGetter.INSTANCE; - - Instrumenter instrumenter = - Instrumenter.builder( - openTelemetry, - INSTRUMENTATION_NAME, - HttpSpanNameExtractor.create(httpAttributesGetter)) - .setSpanStatusExtractor(HttpSpanStatusExtractor.create(httpAttributesGetter)) - .addAttributesExtractor(httpAttributesExtractorBuilder.build()) - .addAttributesExtractors(additionalExtractors) - .addOperationMetrics(HttpClientMetrics.get()) - // We manually inject because we need to inject internal requests for redirects. - .buildInstrumenter(SpanKindExtractor.alwaysClient()); - - return new ApacheHttpClientTelemetry(instrumenter, openTelemetry.getPropagators()); + return new ApacheHttpClientTelemetry( + builder.build(), builder.getOpenTelemetry().getPropagators()); } } diff --git a/instrumentation/apache-httpclient/apache-httpclient-4.3/library/src/main/java/io/opentelemetry/instrumentation/apachehttpclient/v4_3/TracingProtocolExec.java b/instrumentation/apache-httpclient/apache-httpclient-4.3/library/src/main/java/io/opentelemetry/instrumentation/apachehttpclient/v4_3/TracingProtocolExec.java index 3a59039150d5..1f29cf4fa758 100644 --- a/instrumentation/apache-httpclient/apache-httpclient-4.3/library/src/main/java/io/opentelemetry/instrumentation/apachehttpclient/v4_3/TracingProtocolExec.java +++ b/instrumentation/apache-httpclient/apache-httpclient-4.3/library/src/main/java/io/opentelemetry/instrumentation/apachehttpclient/v4_3/TracingProtocolExec.java @@ -9,30 +9,22 @@ import io.opentelemetry.context.Scope; import io.opentelemetry.context.propagation.ContextPropagators; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import io.opentelemetry.instrumentation.api.semconv.http.HttpClientRequestResendCount; import java.io.IOException; -import javax.annotation.Nullable; import org.apache.http.HttpException; import org.apache.http.HttpHost; import org.apache.http.HttpResponse; -import org.apache.http.ProtocolException; -import org.apache.http.client.ClientProtocolException; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpExecutionAware; import org.apache.http.client.methods.HttpRequestWrapper; import org.apache.http.client.protocol.HttpClientContext; import org.apache.http.conn.routing.HttpRoute; -import org.apache.http.impl.client.DefaultRedirectStrategy; -import org.apache.http.impl.client.RedirectLocations; import org.apache.http.impl.execchain.ClientExecChain; final class TracingProtocolExec implements ClientExecChain { - private static final String REQUEST_CONTEXT_ATTRIBUTE_ID = + private static final String REQUEST_PARENT_CONTEXT_ATTRIBUTE_ID = TracingProtocolExec.class.getName() + ".context"; - private static final String REQUEST_WRAPPER_ATTRIBUTE_ID = - TracingProtocolExec.class.getName() + ".requestWrapper"; - private static final String REDIRECT_COUNT_ATTRIBUTE_ID = - TracingProtocolExec.class.getName() + ".redirectCount"; private final Instrumenter instrumenter; private final ContextPropagators propagators; @@ -54,14 +46,12 @@ public CloseableHttpResponse execute( HttpClientContext httpContext, HttpExecutionAware httpExecutionAware) throws IOException, HttpException { - Context context = httpContext.getAttribute(REQUEST_CONTEXT_ATTRIBUTE_ID, Context.class); - if (context != null) { - ApacheHttpClientRequest instrumenterRequest = - httpContext.getAttribute(REQUEST_WRAPPER_ATTRIBUTE_ID, ApacheHttpClientRequest.class); - // Request already had a context so a redirect. Don't create a new span just inject and - // execute. - propagators.getTextMapPropagator().inject(context, request, HttpHeaderSetter.INSTANCE); - return execute(route, request, instrumenterRequest, httpContext, httpExecutionAware, context); + + Context parentContext = + httpContext.getAttribute(REQUEST_PARENT_CONTEXT_ATTRIBUTE_ID, Context.class); + if (parentContext == null) { + parentContext = HttpClientRequestResendCount.initialize(Context.current()); + httpContext.setAttribute(REQUEST_PARENT_CONTEXT_ATTRIBUTE_ID, parentContext); } HttpHost host = null; @@ -81,16 +71,11 @@ public CloseableHttpResponse execute( } ApacheHttpClientRequest instrumenterRequest = new ApacheHttpClientRequest(host, request); - Context parentContext = Context.current(); if (!instrumenter.shouldStart(parentContext, instrumenterRequest)) { return exec.execute(route, request, httpContext, httpExecutionAware); } - context = instrumenter.start(parentContext, instrumenterRequest); - httpContext.setAttribute(REQUEST_CONTEXT_ATTRIBUTE_ID, context); - httpContext.setAttribute(REQUEST_WRAPPER_ATTRIBUTE_ID, instrumenterRequest); - httpContext.setAttribute(REDIRECT_COUNT_ATTRIBUTE_ID, 0); - + Context context = instrumenter.start(parentContext, instrumenterRequest); propagators.getTextMapPropagator().inject(context, request, HttpHeaderSetter.INSTANCE); return execute(route, request, instrumenterRequest, httpContext, httpExecutionAware, context); @@ -113,63 +98,7 @@ private CloseableHttpResponse execute( error = e; throw e; } finally { - if (!pendingRedirect(context, httpContext, request, instrumenterRequest, response)) { - instrumenter.end(context, instrumenterRequest, response, error); - } - } - } - - private boolean pendingRedirect( - Context context, - HttpClientContext httpContext, - HttpRequestWrapper request, - ApacheHttpClientRequest instrumenterRequest, - @Nullable CloseableHttpResponse response) { - if (response == null) { - return false; - } - if (!httpContext.getRequestConfig().isRedirectsEnabled()) { - return false; + instrumenter.end(context, instrumenterRequest, response, error); } - - // TODO(anuraaga): Support redirect strategies other than the default. There is no way to get - // the user defined redirect strategy without some tricks, but it's very rare to override - // the strategy, usually it is either on or off as checked above. We can add support for this - // later if needed. - try { - if (!DefaultRedirectStrategy.INSTANCE.isRedirected(request, response, httpContext)) { - return false; - } - } catch (ProtocolException e) { - // DefaultRedirectStrategy.isRedirected cannot throw this so just return a default. - return false; - } - - // Very hacky and a bit slow, but the only way to determine whether the client will fail with - // a circular redirect, which happens before exec decorators run. - RedirectLocations redirectLocations = - (RedirectLocations) httpContext.getAttribute(HttpClientContext.REDIRECT_LOCATIONS); - if (redirectLocations != null) { - RedirectLocations copy = new RedirectLocations(); - copy.addAll(redirectLocations); - - try { - DefaultRedirectStrategy.INSTANCE.getLocationURI(request, response, httpContext); - } catch (ProtocolException e) { - // We will not be returning to the Exec, finish the span. - instrumenter.end(context, instrumenterRequest, response, new ClientProtocolException(e)); - return true; - } finally { - httpContext.setAttribute(HttpClientContext.REDIRECT_LOCATIONS, copy); - } - } - - int redirectCount = httpContext.getAttribute(REDIRECT_COUNT_ATTRIBUTE_ID, Integer.class); - if (++redirectCount > httpContext.getRequestConfig().getMaxRedirects()) { - return false; - } - - httpContext.setAttribute(REDIRECT_COUNT_ATTRIBUTE_ID, redirectCount); - return true; } } diff --git a/instrumentation/apache-httpclient/apache-httpclient-4.3/testing/build.gradle.kts b/instrumentation/apache-httpclient/apache-httpclient-4.3/testing/build.gradle.kts index ef1efff84e0f..9328255dc436 100644 --- a/instrumentation/apache-httpclient/apache-httpclient-4.3/testing/build.gradle.kts +++ b/instrumentation/apache-httpclient/apache-httpclient-4.3/testing/build.gradle.kts @@ -2,6 +2,16 @@ plugins { id("otel.java-conventions") } +tasks { + compileJava { + // when code is compiled with jdk 21 and executed with jdk 8 -parameters flag is needed to avoid + // java.lang.reflect.MalformedParametersException: Invalid parameter name "" + // when junit calls java.lang.reflect.Executable.getParameters() on the constructor of a + // non-static nested test class + options.compilerArgs.add("-parameters") + } +} + dependencies { api(project(":testing-common")) diff --git a/instrumentation/apache-httpclient/apache-httpclient-4.3/testing/src/main/java/io/opentelemetry/instrumentation/apachehttpclient/v4_3/AbstractApacheHttpClientTest.java b/instrumentation/apache-httpclient/apache-httpclient-4.3/testing/src/main/java/io/opentelemetry/instrumentation/apachehttpclient/v4_3/AbstractApacheHttpClientTest.java index 049874c88363..5d7d198d7193 100644 --- a/instrumentation/apache-httpclient/apache-httpclient-4.3/testing/src/main/java/io/opentelemetry/instrumentation/apachehttpclient/v4_3/AbstractApacheHttpClientTest.java +++ b/instrumentation/apache-httpclient/apache-httpclient-4.3/testing/src/main/java/io/opentelemetry/instrumentation/apachehttpclient/v4_3/AbstractApacheHttpClientTest.java @@ -5,18 +5,14 @@ package io.opentelemetry.instrumentation.apachehttpclient.v4_3; -import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpClientTest; import io.opentelemetry.instrumentation.testing.junit.http.HttpClientResult; import io.opentelemetry.instrumentation.testing.junit.http.HttpClientTestOptions; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; import java.io.IOException; import java.io.UncheckedIOException; import java.net.URI; -import java.util.HashSet; import java.util.Map; -import java.util.Set; import org.apache.http.HttpHost; import org.apache.http.HttpRequest; import org.apache.http.HttpResponse; @@ -59,8 +55,15 @@ CloseableHttpClient getClient(URI uri) { return client; } + abstract static class ApacheHttpClientTest extends AbstractHttpClientTest { + @Override + protected void configure(HttpClientTestOptions.Builder optionsBuilder) { + optionsBuilder.markAsLowLevelInstrumentation(); + } + } + @Nested - class ApacheClientHostRequestTest extends AbstractHttpClientTest { + class ApacheClientHostRequestTest extends ApacheHttpClientTest { @Override public BasicHttpRequest buildRequest(String method, URI uri, Map headers) { @@ -94,15 +97,10 @@ public void sendRequestWithCallback( httpClientResult.complete(t); } } - - @Override - protected void configure(HttpClientTestOptions.Builder optionsBuilder) { - configureTest(optionsBuilder); - } } @Nested - class ApacheClientHostRequestContextTest extends AbstractHttpClientTest { + class ApacheClientHostRequestContextTest extends ApacheHttpClientTest { @Override public BasicHttpRequest buildRequest(String method, URI uri, Map headers) { @@ -140,15 +138,10 @@ public void sendRequestWithCallback( httpClientResult.complete(t); } } - - @Override - protected void configure(HttpClientTestOptions.Builder optionsBuilder) { - configureTest(optionsBuilder); - } } @Nested - class ApacheClientHostAbsoluteUriRequestTest extends AbstractHttpClientTest { + class ApacheClientHostAbsoluteUriRequestTest extends ApacheHttpClientTest { @Override public BasicHttpRequest buildRequest(String method, URI uri, Map headers) { @@ -181,16 +174,11 @@ public void sendRequestWithCallback( httpClientResult.complete(t); } } - - @Override - protected void configure(HttpClientTestOptions.Builder optionsBuilder) { - configureTest(optionsBuilder); - } } @Nested class ApacheClientHostAbsoluteUriRequestContextTest - extends AbstractHttpClientTest { + extends ApacheHttpClientTest { @Override public BasicHttpRequest buildRequest(String method, URI uri, Map headers) { @@ -227,15 +215,10 @@ public void sendRequestWithCallback( httpClientResult.complete(t); } } - - @Override - protected void configure(HttpClientTestOptions.Builder optionsBuilder) { - configureTest(optionsBuilder); - } } @Nested - class ApacheClientUriRequestTest extends AbstractHttpClientTest { + class ApacheClientUriRequestTest extends ApacheHttpClientTest { @Override public HttpUriRequest buildRequest(String method, URI uri, Map headers) { @@ -263,15 +246,10 @@ public void sendRequestWithCallback( httpClientResult.complete(t); } } - - @Override - protected void configure(HttpClientTestOptions.Builder optionsBuilder) { - configureTest(optionsBuilder); - } } @Nested - class ApacheClientUriRequestContextTest extends AbstractHttpClientTest { + class ApacheClientUriRequestContextTest extends ApacheHttpClientTest { @Override public HttpUriRequest buildRequest(String method, URI uri, Map headers) { @@ -299,11 +277,6 @@ public void sendRequestWithCallback( httpClientResult.complete(t); } } - - @Override - protected void configure(HttpClientTestOptions.Builder optionsBuilder) { - configureTest(optionsBuilder); - } } static T configureRequest(T request, Map headers) { @@ -335,18 +308,6 @@ static ResponseHandler responseCallback(HttpClientResult httpClien }; } - static void configureTest(HttpClientTestOptions.Builder optionsBuilder) { - optionsBuilder.setUserAgent("apachehttpclient"); - optionsBuilder.setHttpAttributes( - endpoint -> { - Set> attributes = - new HashSet<>(HttpClientTestOptions.DEFAULT_HTTP_ATTRIBUTES); - attributes.add(SemanticAttributes.HTTP_SCHEME); - attributes.add(SemanticAttributes.HTTP_TARGET); - return attributes; - }); - } - static String fullPathFromUri(URI uri) { StringBuilder builder = new StringBuilder(); if (uri.getPath() != null) { diff --git a/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/build.gradle.kts b/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/build.gradle.kts index 185eba7481a3..49122b19ca14 100644 --- a/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/build.gradle.kts +++ b/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/build.gradle.kts @@ -13,3 +13,9 @@ muzzle { dependencies { library("org.apache.httpcomponents.client5:httpclient5:5.0") } + +tasks { + withType().configureEach { + systemProperty("testLatestDeps", findProperty("testLatestDeps") as Boolean) + } +} diff --git a/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v5_0/ApacheHttpClientHttpAttributesGetter.java b/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v5_0/ApacheHttpClientHttpAttributesGetter.java index 131a2d8c74a7..eef2489c969d 100644 --- a/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v5_0/ApacheHttpClientHttpAttributesGetter.java +++ b/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v5_0/ApacheHttpClientHttpAttributesGetter.java @@ -5,7 +5,7 @@ package io.opentelemetry.javaagent.instrumentation.apachehttpclient.v5_0; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpClientAttributesGetter; +import io.opentelemetry.instrumentation.api.semconv.http.HttpClientAttributesGetter; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -14,6 +14,7 @@ import org.apache.hc.core5.http.HttpRequest; import org.apache.hc.core5.http.HttpResponse; import org.apache.hc.core5.http.MessageHeaders; +import org.apache.hc.core5.http.ProtocolVersion; import org.apache.hc.core5.net.URIAuthority; final class ApacheHttpClientHttpAttributesGetter @@ -89,4 +90,46 @@ private static List headersToList(Header[] headers) { } return headersList; } + + @Nullable + @Override + public String getNetworkProtocolName(HttpRequest request, @Nullable HttpResponse response) { + ProtocolVersion protocolVersion = getVersion(request, response); + if (protocolVersion == null) { + return null; + } + return protocolVersion.getProtocol(); + } + + @Nullable + @Override + public String getNetworkProtocolVersion(HttpRequest request, @Nullable HttpResponse response) { + ProtocolVersion protocolVersion = getVersion(request, response); + if (protocolVersion == null) { + return null; + } + if (protocolVersion.getMinor() == 0) { + return Integer.toString(protocolVersion.getMajor()); + } + return protocolVersion.getMajor() + "." + protocolVersion.getMinor(); + } + + @Override + @Nullable + public String getServerAddress(HttpRequest request) { + return request.getAuthority().getHostName(); + } + + @Override + public Integer getServerPort(HttpRequest request) { + return request.getAuthority().getPort(); + } + + private static ProtocolVersion getVersion(HttpRequest request, @Nullable HttpResponse response) { + ProtocolVersion protocolVersion = request.getVersion(); + if (protocolVersion == null && response != null) { + protocolVersion = response.getVersion(); + } + return protocolVersion; + } } diff --git a/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v5_0/ApacheHttpClientNetAttributesGetter.java b/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v5_0/ApacheHttpClientNetAttributesGetter.java deleted file mode 100644 index 3a3993946421..000000000000 --- a/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v5_0/ApacheHttpClientNetAttributesGetter.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * 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.NetClientAttributesGetter; -import javax.annotation.Nullable; -import org.apache.hc.core5.http.HttpRequest; -import org.apache.hc.core5.http.HttpResponse; -import org.apache.hc.core5.http.ProtocolVersion; - -final class ApacheHttpClientNetAttributesGetter - implements NetClientAttributesGetter { - - @Nullable - @Override - public String getNetworkProtocolName(HttpRequest request, @Nullable HttpResponse response) { - ProtocolVersion protocolVersion = getVersion(request, response); - if (protocolVersion == null) { - return null; - } - return protocolVersion.getProtocol(); - } - - @Nullable - @Override - public String getNetworkProtocolVersion(HttpRequest request, @Nullable HttpResponse response) { - ProtocolVersion protocolVersion = getVersion(request, response); - if (protocolVersion == null) { - return null; - } - return protocolVersion.getMajor() + "." + protocolVersion.getMinor(); - } - - @Override - @Nullable - public String getServerAddress(HttpRequest request) { - return request.getAuthority().getHostName(); - } - - @Override - public Integer getServerPort(HttpRequest request) { - return request.getAuthority().getPort(); - } - - private static ProtocolVersion getVersion(HttpRequest request, @Nullable HttpResponse response) { - ProtocolVersion protocolVersion = request.getVersion(); - if (protocolVersion == null && response != null) { - protocolVersion = response.getVersion(); - } - return protocolVersion; - } -} diff --git a/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v5_0/ApacheHttpClientSingletons.java b/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v5_0/ApacheHttpClientSingletons.java index ac2405b69754..e1cc0a667cef 100644 --- a/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v5_0/ApacheHttpClientSingletons.java +++ b/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v5_0/ApacheHttpClientSingletons.java @@ -5,14 +5,8 @@ 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.http.HttpClientAttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpClientMetrics; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpSpanNameExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpSpanStatusExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.net.PeerServiceAttributesExtractor; -import io.opentelemetry.javaagent.bootstrap.internal.CommonConfig; +import io.opentelemetry.javaagent.bootstrap.internal.JavaagentHttpClientInstrumenters; import org.apache.hc.core5.http.HttpRequest; import org.apache.hc.core5.http.HttpResponse; @@ -22,27 +16,11 @@ public final class ApacheHttpClientSingletons { private static final Instrumenter INSTRUMENTER; static { - ApacheHttpClientHttpAttributesGetter httpAttributesGetter = - new ApacheHttpClientHttpAttributesGetter(); - ApacheHttpClientNetAttributesGetter netAttributesGetter = - new ApacheHttpClientNetAttributesGetter(); - INSTRUMENTER = - Instrumenter.builder( - GlobalOpenTelemetry.get(), - INSTRUMENTATION_NAME, - HttpSpanNameExtractor.create(httpAttributesGetter)) - .setSpanStatusExtractor(HttpSpanStatusExtractor.create(httpAttributesGetter)) - .addAttributesExtractor( - HttpClientAttributesExtractor.builder(httpAttributesGetter, netAttributesGetter) - .setCapturedRequestHeaders(CommonConfig.get().getClientRequestHeaders()) - .setCapturedResponseHeaders(CommonConfig.get().getClientResponseHeaders()) - .build()) - .addAttributesExtractor( - PeerServiceAttributesExtractor.create( - netAttributesGetter, CommonConfig.get().getPeerServiceMapping())) - .addOperationMetrics(HttpClientMetrics.get()) - .buildClientInstrumenter(HttpHeaderSetter.INSTANCE); + JavaagentHttpClientInstrumenters.create( + INSTRUMENTATION_NAME, + new ApacheHttpClientHttpAttributesGetter(), + HttpHeaderSetter.INSTANCE); } public static Instrumenter instrumenter() { diff --git a/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v5_0/RequestWithHost.java b/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v5_0/RequestWithHost.java index 6fd5767e83a8..e18de7e6d035 100644 --- a/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v5_0/RequestWithHost.java +++ b/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v5_0/RequestWithHost.java @@ -19,9 +19,13 @@ public class RequestWithHost extends HttpRequestWrapper implements ClassicHttpRe public RequestWithHost(HttpHost httpHost, ClassicHttpRequest httpRequest) { super(httpRequest); - - this.scheme = httpHost.getSchemeName(); - this.authority = new URIAuthority(httpHost.getHostName(), httpHost.getPort()); + if (httpHost != null) { + this.scheme = httpHost.getSchemeName(); + this.authority = new URIAuthority(httpHost.getHostName(), httpHost.getPort()); + } else { + this.scheme = httpRequest.getScheme(); + this.authority = httpRequest.getAuthority(); + } } @Override diff --git a/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v5_0/AbstractApacheHttpClientTest.java b/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v5_0/AbstractApacheHttpClientTest.java index 7a2a7941c5e8..cf804e182196 100644 --- a/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v5_0/AbstractApacheHttpClientTest.java +++ b/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v5_0/AbstractApacheHttpClientTest.java @@ -5,12 +5,11 @@ package io.opentelemetry.javaagent.instrumentation.apachehttpclient.v5_0; -import static io.opentelemetry.api.common.AttributeKey.stringKey; - import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpClientTest; import io.opentelemetry.instrumentation.testing.junit.http.HttpClientResult; import io.opentelemetry.instrumentation.testing.junit.http.HttpClientTestOptions; +import io.opentelemetry.semconv.NetworkAttributes; import java.net.URI; import java.time.Duration; import java.util.HashSet; @@ -27,11 +26,9 @@ abstract class AbstractApacheHttpClientTest extends AbstractHttpClientTest { - private static final String USER_AGENT = "apachehttpclient"; @Override protected void configure(HttpClientTestOptions.Builder optionsBuilder) { - optionsBuilder.setUserAgent(USER_AGENT); optionsBuilder.setHttpAttributes(this::getHttpAttributes); } @@ -43,8 +40,7 @@ protected Set> getHttpAttributes(URI uri) { || "https://192.0.2.1/".equals(uri.toString()) || uri.toString().contains("/read-timeout") || uri.toString().contains("/circular-redirect")) { - attributes.remove(stringKey("net.protocol.name")); - attributes.remove(stringKey("net.protocol.version")); + attributes.remove(NetworkAttributes.NETWORK_PROTOCOL_VERSION); } return attributes; } @@ -52,7 +48,7 @@ protected Set> getHttpAttributes(URI uri) { @Override public T buildRequest(String method, URI uri, Map headers) { T request = createRequest(method, uri); - request.addHeader("user-agent", USER_AGENT); + request.addHeader("user-agent", "apachehttpclient"); headers.forEach((key, value) -> request.setHeader(new BasicHeader(key, value))); return request; } diff --git a/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v5_0/ApacheHttpAsyncClientTest.java b/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v5_0/ApacheHttpAsyncClientTest.java index 401ffbd5694e..3cc44f22551d 100644 --- a/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v5_0/ApacheHttpAsyncClientTest.java +++ b/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v5_0/ApacheHttpAsyncClientTest.java @@ -8,6 +8,7 @@ import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; import io.opentelemetry.instrumentation.testing.junit.http.HttpClientInstrumentationExtension; import io.opentelemetry.instrumentation.testing.junit.http.HttpClientResult; +import io.opentelemetry.instrumentation.testing.junit.http.HttpClientTestOptions; import java.net.URI; import java.util.Map; import java.util.concurrent.CancellationException; @@ -86,6 +87,13 @@ protected HttpContext getContext() { } abstract class AbstractTest extends AbstractApacheHttpClientTest { + + @Override + protected void configure(HttpClientTestOptions.Builder optionsBuilder) { + super.configure(optionsBuilder); + optionsBuilder.spanEndsAfterBody(); + } + @Override public SimpleHttpRequest buildRequest(String method, URI uri, Map headers) { SimpleHttpRequest httpRequest = super.buildRequest(method, uri, headers); diff --git a/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v5_0/ApacheHttpClientTest.java b/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v5_0/ApacheHttpClientTest.java index cea57cc51900..e76b1ae75821 100644 --- a/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v5_0/ApacheHttpClientTest.java +++ b/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v5_0/ApacheHttpClientTest.java @@ -20,6 +20,7 @@ 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.HttpResponse; import org.apache.hc.core5.http.io.HttpClientResponseHandler; import org.apache.hc.core5.http.message.BasicClassicHttpRequest; @@ -90,6 +91,26 @@ void executeRequestWithCallback(ClassicHttpRequest request, URI uri, HttpClientR } } + @Nested + class ApacheClientNullHttpHostRequestTest extends AbstractTest { + @Override + ClassicHttpRequest createRequest(String method, URI uri) { + // also testing with an absolute path below + return new BasicClassicHttpRequest(method, HttpHost.create(uri), fullPathFromUri(uri)); + } + + @Override + ClassicHttpResponse doExecuteRequest(ClassicHttpRequest request, URI uri) throws Exception { + return getClient(uri).execute(null, request); + } + + @Override + void executeRequestWithCallback(ClassicHttpRequest request, URI uri, HttpClientResult result) + throws Exception { + getClient(uri).execute(null, request, new ResponseHandler(result)); + } + } + @Nested class ApacheClientHostAbsoluteUriRequestTest extends AbstractTest { @Override @@ -202,6 +223,11 @@ protected void configure(HttpClientTestOptions.Builder optionsBuilder) { super.configure(optionsBuilder); // apparently apache http client does not report the 302 status code? optionsBuilder.setResponseCodeOnRedirectError(null); + + if (Boolean.getBoolean("testLatestDeps")) { + optionsBuilder.disableTestHttps(); + optionsBuilder.disableTestRemoteConnection(); + } } } diff --git a/instrumentation/apache-httpclient/apache-httpclient-5.2/library/README.md b/instrumentation/apache-httpclient/apache-httpclient-5.2/library/README.md new file mode 100644 index 000000000000..22694b3076cd --- /dev/null +++ b/instrumentation/apache-httpclient/apache-httpclient-5.2/library/README.md @@ -0,0 +1,60 @@ +# Library Instrumentation for Apache Http client version 5.2 + +Provides OpenTelemetry instrumentation for [Apache Http Client 5.2](https://hc.apache.org/httpcomponents-client-5.2.x/). + +## Quickstart + +### Add these dependencies to your project + +Replace `OPENTELEMETRY_VERSION` with the [latest +release](https://search.maven.org/search?q=g:io.opentelemetry.instrumentation%20AND%20a:opentelemetry-apache-httpclient-5.2). + +For Maven, add to your `pom.xml` dependencies: + +```xml + + + io.opentelemetry.instrumentation + opentelemetry-apache-httpclient-5.2 + OPENTELEMETRY_VERSION + + +``` + +For Gradle, add to your dependencies: + +```groovy +implementation("io.opentelemetry.instrumentation:opentelemetry-apache-httpclient-5.2:OPENTELEMETRY_VERSION") +``` + +### Usage + +The instrumentation library provides the class `ApacheHttpClient5Telemetry` that has a builder +method and allows the creation of an instance of the `HttpClientBuilder` to provide +OpenTelemetry-based spans and context propagation: + +```java +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.instrumentation.apachehttpclient.v5_2.ApacheHttpClient5Telemetry; +import org.apache.hc.client5.http.classic.HttpClient; +import org.apache.hc.client5.http.impl.classic.HttpClientBuilder; + +public class ApacheHttpClient5Configuration { + + private OpenTelemetry openTelemetry; + + public ApacheHttpClient5Configuration(OpenTelemetry openTelemetry) { + this.openTelemetry = openTelemetry; + } + + // creates a new http client builder for constructing http clients with open telemetry instrumentation + public HttpClientBuilder createBuilder() { + return ApacheHttpClient5Telemetry.builder(openTelemetry).build().newHttpClientBuilder(); + } + + // creates a new http client with open telemetry instrumentation + public HttpClient newHttpClient() { + return ApacheHttpClient5Telemetry.builder(openTelemetry).build().newHttpClient(); + } +} +``` diff --git a/instrumentation/apache-httpclient/apache-httpclient-5.2/library/build.gradle.kts b/instrumentation/apache-httpclient/apache-httpclient-5.2/library/build.gradle.kts new file mode 100644 index 000000000000..f395a7dc5b4d --- /dev/null +++ b/instrumentation/apache-httpclient/apache-httpclient-5.2/library/build.gradle.kts @@ -0,0 +1,9 @@ +plugins { + id("otel.library-instrumentation") + id("otel.nullaway-conventions") + id("otel.animalsniffer-conventions") +} + +dependencies { + library("org.apache.httpcomponents.client5:httpclient5:5.2.1") +} diff --git a/instrumentation/apache-httpclient/apache-httpclient-5.2/library/src/main/java/io/opentelemetry/instrumentation/apachehttpclient/v5_2/ApacheHttpClient5HttpAttributesGetter.java b/instrumentation/apache-httpclient/apache-httpclient-5.2/library/src/main/java/io/opentelemetry/instrumentation/apachehttpclient/v5_2/ApacheHttpClient5HttpAttributesGetter.java new file mode 100644 index 000000000000..36d69791d57c --- /dev/null +++ b/instrumentation/apache-httpclient/apache-httpclient-5.2/library/src/main/java/io/opentelemetry/instrumentation/apachehttpclient/v5_2/ApacheHttpClient5HttpAttributesGetter.java @@ -0,0 +1,114 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.apachehttpclient.v5_2; + +import io.opentelemetry.instrumentation.api.semconv.http.HttpClientAttributesGetter; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import javax.annotation.Nullable; +import org.apache.hc.core5.http.Header; +import org.apache.hc.core5.http.HttpResponse; +import org.apache.hc.core5.http.MessageHeaders; +import org.apache.hc.core5.http.ProtocolVersion; + +enum ApacheHttpClient5HttpAttributesGetter + implements HttpClientAttributesGetter { + INSTANCE; + + @Override + public String getHttpRequestMethod(ApacheHttpClient5Request request) { + return request.getMethod(); + } + + @Override + @Nullable + public String getUrlFull(ApacheHttpClient5Request request) { + return request.getUrl(); + } + + @Override + public List getHttpRequestHeader(ApacheHttpClient5Request request, String name) { + return getHeader(request, name); + } + + @Override + public Integer getHttpResponseStatusCode( + ApacheHttpClient5Request request, HttpResponse response, @Nullable Throwable error) { + return response.getCode(); + } + + @Override + public List getHttpResponseHeader( + ApacheHttpClient5Request request, HttpResponse response, String name) { + return getHeader(response, name); + } + + private static List getHeader(MessageHeaders messageHeaders, String name) { + return headersToList(messageHeaders.getHeaders(name)); + } + + private static List getHeader(ApacheHttpClient5Request messageHeaders, String name) { + return headersToList(messageHeaders.getDelegate().getHeaders(name)); + } + + // minimize memory overhead by not using streams + private static List headersToList(Header[] headers) { + if (headers.length == 0) { + return Collections.emptyList(); + } + List headersList = new ArrayList<>(headers.length); + for (Header header : headers) { + headersList.add(header.getValue()); + } + return headersList; + } + + @Nullable + @Override + public String getNetworkProtocolName( + ApacheHttpClient5Request request, @Nullable HttpResponse response) { + ProtocolVersion protocolVersion = getVersion(request, response); + if (protocolVersion == null) { + return null; + } + return protocolVersion.getProtocol(); + } + + @Nullable + @Override + public String getNetworkProtocolVersion( + ApacheHttpClient5Request request, @Nullable HttpResponse response) { + ProtocolVersion protocolVersion = getVersion(request, response); + if (protocolVersion == null) { + return null; + } + if (protocolVersion.getMinor() == 0) { + return Integer.toString(protocolVersion.getMajor()); + } + return protocolVersion.getMajor() + "." + protocolVersion.getMinor(); + } + + @Override + @Nullable + public String getServerAddress(ApacheHttpClient5Request request) { + return request.getDelegate().getAuthority().getHostName(); + } + + @Override + public Integer getServerPort(ApacheHttpClient5Request request) { + return request.getDelegate().getAuthority().getPort(); + } + + private static ProtocolVersion getVersion( + ApacheHttpClient5Request request, @Nullable HttpResponse response) { + ProtocolVersion protocolVersion = request.getDelegate().getVersion(); + if (protocolVersion == null && response != null) { + protocolVersion = response.getVersion(); + } + return protocolVersion; + } +} diff --git a/instrumentation/apache-httpclient/apache-httpclient-5.2/library/src/main/java/io/opentelemetry/instrumentation/apachehttpclient/v5_2/ApacheHttpClient5Request.java b/instrumentation/apache-httpclient/apache-httpclient-5.2/library/src/main/java/io/opentelemetry/instrumentation/apachehttpclient/v5_2/ApacheHttpClient5Request.java new file mode 100644 index 000000000000..b8a0de70b7a1 --- /dev/null +++ b/instrumentation/apache-httpclient/apache-httpclient-5.2/library/src/main/java/io/opentelemetry/instrumentation/apachehttpclient/v5_2/ApacheHttpClient5Request.java @@ -0,0 +1,136 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.apachehttpclient.v5_2; + +import static java.util.logging.Level.FINE; + +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.logging.Logger; +import javax.annotation.Nullable; +import org.apache.hc.core5.http.Header; +import org.apache.hc.core5.http.HttpHost; +import org.apache.hc.core5.http.HttpRequest; +import org.apache.hc.core5.http.ProtocolVersion; + +final class ApacheHttpClient5Request { + + private static final Logger logger = Logger.getLogger(ApacheHttpClient5Request.class.getName()); + + @Nullable private final URI uri; + + private final HttpRequest delegate; + @Nullable private final HttpHost target; + + ApacheHttpClient5Request(@Nullable HttpHost httpHost, HttpRequest httpRequest) { + URI calculatedUri = getUri(httpRequest); + if (calculatedUri != null && httpHost != null) { + uri = getCalculatedUri(httpHost, calculatedUri); + } else { + uri = calculatedUri; + } + delegate = httpRequest; + target = httpHost; + } + + /** Returns the actual {@link HttpRequest} being executed by the client. */ + public HttpRequest getDelegate() { + return delegate; + } + + List getHeader(String name) { + return headersToList(delegate.getHeaders(name)); + } + + // minimize memory overhead by not using streams + static List headersToList(Header[] headers) { + if (headers.length == 0) { + return Collections.emptyList(); + } + List headersList = new ArrayList<>(headers.length); + for (Header header : headers) { + headersList.add(header.getValue()); + } + return headersList; + } + + void setHeader(String name, String value) { + delegate.setHeader(name, value); + } + + String getMethod() { + return delegate.getMethod(); + } + + @Nullable + String getUrl() { + return uri != null ? uri.toString() : null; + } + + String getProtocolName() { + return delegate.getVersion().getProtocol(); + } + + String getProtocolVersion() { + ProtocolVersion protocolVersion = delegate.getVersion(); + if (protocolVersion.getMinor() == 0) { + return Integer.toString(protocolVersion.getMajor()); + } + return protocolVersion.getMajor() + "." + protocolVersion.getMinor(); + } + + @Nullable + public String getServerAddress() { + return uri == null ? null : uri.getHost(); + } + + @Nullable + public Integer getServerPort() { + return uri == null ? null : uri.getPort(); + } + + @Nullable + private static URI getUri(HttpRequest httpRequest) { + try { + // this can be relative or absolute + return new URI(httpRequest.getUri().toString()); + } catch (URISyntaxException e) { + logger.log(FINE, e.getMessage(), e); + return null; + } + } + + @Nullable + private static URI getCalculatedUri(HttpHost httpHost, URI uri) { + try { + return new URI( + httpHost.getSchemeName(), + uri.getUserInfo(), + httpHost.getHostName(), + httpHost.getPort(), + uri.getPath(), + uri.getQuery(), + uri.getFragment()); + } catch (URISyntaxException e) { + logger.log(FINE, e.getMessage(), e); + return null; + } + } + + @Nullable + public InetSocketAddress getServerSocketAddress() { + if (target == null) { + return null; + } + InetAddress inetAddress = target.getAddress(); + return inetAddress == null ? null : new InetSocketAddress(inetAddress, target.getPort()); + } +} diff --git a/instrumentation/apache-httpclient/apache-httpclient-5.2/library/src/main/java/io/opentelemetry/instrumentation/apachehttpclient/v5_2/ApacheHttpClient5Telemetry.java b/instrumentation/apache-httpclient/apache-httpclient-5.2/library/src/main/java/io/opentelemetry/instrumentation/apachehttpclient/v5_2/ApacheHttpClient5Telemetry.java new file mode 100644 index 000000000000..dca1909a1b44 --- /dev/null +++ b/instrumentation/apache-httpclient/apache-httpclient-5.2/library/src/main/java/io/opentelemetry/instrumentation/apachehttpclient/v5_2/ApacheHttpClient5Telemetry.java @@ -0,0 +1,58 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.apachehttpclient.v5_2; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.context.propagation.ContextPropagators; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import org.apache.hc.client5.http.impl.ChainElement; +import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; +import org.apache.hc.client5.http.impl.classic.HttpClientBuilder; +import org.apache.hc.core5.http.HttpResponse; + +/** Entrypoint for instrumenting Apache HTTP Client. */ +public final class ApacheHttpClient5Telemetry { + + /** + * Returns a new {@link ApacheHttpClient5Telemetry} configured with the given {@link + * OpenTelemetry}. + */ + public static ApacheHttpClient5Telemetry create(OpenTelemetry openTelemetry) { + return builder(openTelemetry).build(); + } + + /** + * Returns a new {@link ApacheHttpClient5TelemetryBuilder} configured with the given {@link + * OpenTelemetry}. + */ + public static ApacheHttpClient5TelemetryBuilder builder(OpenTelemetry openTelemetry) { + return new ApacheHttpClient5TelemetryBuilder(openTelemetry); + } + + private final Instrumenter instrumenter; + private final ContextPropagators propagators; + + ApacheHttpClient5Telemetry( + Instrumenter instrumenter, + ContextPropagators propagators) { + this.instrumenter = instrumenter; + this.propagators = propagators; + } + + /** Returns a new {@link CloseableHttpClient} with tracing configured. */ + public CloseableHttpClient newHttpClient() { + return newHttpClientBuilder().build(); + } + + /** Returns a new {@link HttpClientBuilder} to create a client with tracing configured. */ + public HttpClientBuilder newHttpClientBuilder() { + return HttpClientBuilder.create() + .addExecInterceptorAfter( + ChainElement.PROTOCOL.name(), + "OtelExecChainHandler", + new OtelExecChainHandler(instrumenter, propagators)); + } +} diff --git a/instrumentation/apache-httpclient/apache-httpclient-5.2/library/src/main/java/io/opentelemetry/instrumentation/apachehttpclient/v5_2/ApacheHttpClient5TelemetryBuilder.java b/instrumentation/apache-httpclient/apache-httpclient-5.2/library/src/main/java/io/opentelemetry/instrumentation/apachehttpclient/v5_2/ApacheHttpClient5TelemetryBuilder.java new file mode 100644 index 000000000000..b9b98eb5d4a6 --- /dev/null +++ b/instrumentation/apache-httpclient/apache-httpclient-5.2/library/src/main/java/io/opentelemetry/instrumentation/apachehttpclient/v5_2/ApacheHttpClient5TelemetryBuilder.java @@ -0,0 +1,118 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.apachehttpclient.v5_2; + +import com.google.errorprone.annotations.CanIgnoreReturnValue; +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.instrumentation.api.incubator.builder.internal.DefaultHttpClientInstrumenterBuilder; +import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor; +import io.opentelemetry.instrumentation.api.semconv.http.HttpClientAttributesExtractorBuilder; +import java.util.List; +import java.util.Set; +import java.util.function.Function; +import org.apache.hc.core5.http.HttpResponse; + +/** A builder for {@link ApacheHttpClient5Telemetry}. */ +public final class ApacheHttpClient5TelemetryBuilder { + + private static final String INSTRUMENTATION_NAME = "io.opentelemetry.apache-httpclient-5.2"; + private final DefaultHttpClientInstrumenterBuilder + builder; + + ApacheHttpClient5TelemetryBuilder(OpenTelemetry openTelemetry) { + builder = + new DefaultHttpClientInstrumenterBuilder<>( + INSTRUMENTATION_NAME, openTelemetry, ApacheHttpClient5HttpAttributesGetter.INSTANCE); + } + + /** + * Adds an additional {@link AttributesExtractor} to invoke to set attributes to instrumented + * items. The {@link AttributesExtractor} will be executed after all default extractors. + */ + @CanIgnoreReturnValue + public ApacheHttpClient5TelemetryBuilder addAttributeExtractor( + AttributesExtractor + attributesExtractor) { + builder.addAttributeExtractor(attributesExtractor); + return this; + } + + /** + * Configures the HTTP request headers that will be captured as span attributes. + * + * @param requestHeaders A list of HTTP header names. + */ + @CanIgnoreReturnValue + public ApacheHttpClient5TelemetryBuilder setCapturedRequestHeaders(List requestHeaders) { + builder.setCapturedRequestHeaders(requestHeaders); + return this; + } + + /** + * Configures the HTTP response headers that will be captured as span attributes. + * + * @param responseHeaders A list of HTTP header names. + */ + @CanIgnoreReturnValue + public ApacheHttpClient5TelemetryBuilder setCapturedResponseHeaders( + List responseHeaders) { + builder.setCapturedResponseHeaders(responseHeaders); + return this; + } + + /** + * Configures the instrumentation to recognize an alternative set of HTTP request methods. + * + *

By default, this instrumentation defines "known" methods as the ones listed in RFC9110 and the PATCH + * method defined in RFC5789. + * + *

Note: calling this method overrides the default known method sets completely; it does + * not supplement it. + * + * @param knownMethods A set of recognized HTTP request methods. + * @see HttpClientAttributesExtractorBuilder#setKnownMethods(Set) + */ + @CanIgnoreReturnValue + public ApacheHttpClient5TelemetryBuilder setKnownMethods(Set knownMethods) { + builder.setKnownMethods(knownMethods); + return this; + } + + /** + * Configures the instrumentation to emit experimental HTTP client metrics. + * + * @param emitExperimentalHttpClientMetrics {@code true} if the experimental HTTP client metrics + * are to be emitted. + */ + @CanIgnoreReturnValue + public ApacheHttpClient5TelemetryBuilder setEmitExperimentalHttpClientMetrics( + boolean emitExperimentalHttpClientMetrics) { + builder.setEmitExperimentalHttpClientMetrics(emitExperimentalHttpClientMetrics); + return this; + } + + /** Sets custom {@link SpanNameExtractor} via transform function. */ + @CanIgnoreReturnValue + public ApacheHttpClient5TelemetryBuilder setSpanNameExtractor( + Function< + SpanNameExtractor, + ? extends SpanNameExtractor> + spanNameExtractorTransformer) { + builder.setSpanNameExtractor(spanNameExtractorTransformer); + return this; + } + + /** + * Returns a new {@link ApacheHttpClient5Telemetry} configured with this {@link + * ApacheHttpClient5TelemetryBuilder}. + */ + public ApacheHttpClient5Telemetry build() { + return new ApacheHttpClient5Telemetry( + builder.build(), builder.getOpenTelemetry().getPropagators()); + } +} diff --git a/instrumentation/apache-httpclient/apache-httpclient-5.2/library/src/main/java/io/opentelemetry/instrumentation/apachehttpclient/v5_2/HttpHeaderSetter.java b/instrumentation/apache-httpclient/apache-httpclient-5.2/library/src/main/java/io/opentelemetry/instrumentation/apachehttpclient/v5_2/HttpHeaderSetter.java new file mode 100644 index 000000000000..635d53147a80 --- /dev/null +++ b/instrumentation/apache-httpclient/apache-httpclient-5.2/library/src/main/java/io/opentelemetry/instrumentation/apachehttpclient/v5_2/HttpHeaderSetter.java @@ -0,0 +1,22 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.apachehttpclient.v5_2; + +import io.opentelemetry.context.propagation.TextMapSetter; +import javax.annotation.Nullable; +import org.apache.hc.core5.http.HttpRequest; + +enum HttpHeaderSetter implements TextMapSetter { + INSTANCE; + + @Override + public void set(@Nullable HttpRequest carrier, String key, String value) { + if (carrier == null) { + return; + } + carrier.setHeader(key, value); + } +} diff --git a/instrumentation/apache-httpclient/apache-httpclient-5.2/library/src/main/java/io/opentelemetry/instrumentation/apachehttpclient/v5_2/OtelExecChainHandler.java b/instrumentation/apache-httpclient/apache-httpclient-5.2/library/src/main/java/io/opentelemetry/instrumentation/apachehttpclient/v5_2/OtelExecChainHandler.java new file mode 100644 index 000000000000..bf361ea646d1 --- /dev/null +++ b/instrumentation/apache-httpclient/apache-httpclient-5.2/library/src/main/java/io/opentelemetry/instrumentation/apachehttpclient/v5_2/OtelExecChainHandler.java @@ -0,0 +1,99 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.apachehttpclient.v5_2; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.propagation.ContextPropagators; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import io.opentelemetry.instrumentation.api.semconv.http.HttpClientRequestResendCount; +import java.io.IOException; +import org.apache.hc.client5.http.classic.ExecChain; +import org.apache.hc.client5.http.classic.ExecChain.Scope; +import org.apache.hc.client5.http.classic.ExecChainHandler; +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.HttpHost; +import org.apache.hc.core5.http.HttpResponse; + +class OtelExecChainHandler implements ExecChainHandler { + + private static final String REQUEST_PARENT_CONTEXT_ATTRIBUTE_ID = + OtelExecChainHandler.class.getName() + ".context"; + + private final Instrumenter instrumenter; + private final ContextPropagators propagators; + + public OtelExecChainHandler( + Instrumenter instrumenter, + ContextPropagators propagators) { + this.instrumenter = instrumenter; + this.propagators = propagators; + } + + @Override + public ClassicHttpResponse execute(ClassicHttpRequest request, Scope scope, ExecChain chain) + throws IOException, HttpException { + Context parentContext = + scope.clientContext.getAttribute(REQUEST_PARENT_CONTEXT_ATTRIBUTE_ID, Context.class); + request.setVersion(scope.clientContext.getProtocolVersion()); + if (parentContext == null) { + parentContext = HttpClientRequestResendCount.initialize(Context.current()); + scope.clientContext.setAttribute(REQUEST_PARENT_CONTEXT_ATTRIBUTE_ID, parentContext); + } + + ApacheHttpClient5Request instrumenterRequest = getApacheHttpClient5Request(request, scope); + + if (!instrumenter.shouldStart(parentContext, instrumenterRequest)) { + return chain.proceed(request, scope); + } + + Context context = instrumenter.start(parentContext, instrumenterRequest); + propagators.getTextMapPropagator().inject(context, request, HttpHeaderSetter.INSTANCE); + + return execute(request, instrumenterRequest, chain, scope, context); + } + + private ClassicHttpResponse execute( + ClassicHttpRequest request, + ApacheHttpClient5Request instrumenterRequest, + ExecChain chain, + Scope scope, + Context context) + throws IOException, HttpException { + ClassicHttpResponse response = null; + Throwable error = null; + try (io.opentelemetry.context.Scope ignored = context.makeCurrent()) { + response = chain.proceed(request, scope); + return response; + } catch (Exception e) { + error = e; + throw e; + } finally { + instrumenter.end(context, instrumenterRequest, response, error); + } + } + + private static ApacheHttpClient5Request getApacheHttpClient5Request( + ClassicHttpRequest request, Scope scope) { + HttpHost host = null; + if (scope.route.getTargetHost() != null) { + host = scope.route.getTargetHost(); + } else if (scope.clientContext.getHttpRoute().getTargetHost() != null) { + host = scope.clientContext.getHttpRoute().getTargetHost(); + } + if (host != null + && ((host.getSchemeName().equals("https") && host.getPort() == 443) + || (host.getSchemeName().equals("http") && host.getPort() == 80))) { + // port seems to be added to the host by route planning for standard ports even if not + // specified in the URL. There doesn't seem to be a way to differentiate between explicit + // and implicit port, but ignore in both cases to match the more common case. + host = new HttpHost(host.getSchemeName(), host.getHostName(), -1); + } + + return new ApacheHttpClient5Request(host, request); + } +} diff --git a/instrumentation/apache-httpclient/apache-httpclient-5.2/library/src/test/java/io/opentelemetry/instrumentation/apachehttpclient/v5_2/AbstractApacheHttpClient5Test.java b/instrumentation/apache-httpclient/apache-httpclient-5.2/library/src/test/java/io/opentelemetry/instrumentation/apachehttpclient/v5_2/AbstractApacheHttpClient5Test.java new file mode 100644 index 000000000000..b23b5f32d247 --- /dev/null +++ b/instrumentation/apache-httpclient/apache-httpclient-5.2/library/src/test/java/io/opentelemetry/instrumentation/apachehttpclient/v5_2/AbstractApacheHttpClient5Test.java @@ -0,0 +1,350 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.apachehttpclient.v5_2; + +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpClientTest; +import io.opentelemetry.instrumentation.testing.junit.http.HttpClientResult; +import io.opentelemetry.instrumentation.testing.junit.http.HttpClientTestOptions; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.net.URI; +import java.util.Map; +import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; +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.io.HttpClientResponseHandler; +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 org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.TestInstance; + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +abstract class AbstractApacheHttpClient5Test { + + protected abstract InstrumentationExtension testing(); + + protected abstract CloseableHttpClient createClient(boolean readTimeout); + + private CloseableHttpClient client; + private CloseableHttpClient clientWithReadTimeout; + + @BeforeAll + void setUp() { + client = createClient(false); + clientWithReadTimeout = createClient(true); + } + + @AfterAll + void tearDown() throws Exception { + client.close(); + clientWithReadTimeout.close(); + } + + CloseableHttpClient getClient(URI uri) { + if (uri.toString().contains("/read-timeout")) { + return clientWithReadTimeout; + } + return client; + } + + abstract static class ApacheHttpClientTest extends AbstractHttpClientTest { + @Override + protected void configure(HttpClientTestOptions.Builder optionsBuilder) { + optionsBuilder.markAsLowLevelInstrumentation(); + } + } + + @Nested + class ApacheClientHostRequestTest extends ApacheHttpClientTest { + + @Override + public BasicClassicHttpRequest buildRequest( + String method, URI uri, Map headers) { + // also testing with an absolute path below + return configureRequest(new BasicClassicHttpRequest(method, fullPathFromUri(uri)), headers); + } + + @Override + public int sendRequest( + BasicClassicHttpRequest request, String method, URI uri, Map headers) + throws Exception { + return getClient(uri) + .execute( + new HttpHost(uri.getScheme(), uri.getHost(), uri.getPort()), + request, + responseHandler()); + } + + @Override + public void sendRequestWithCallback( + BasicClassicHttpRequest request, + String method, + URI uri, + Map headers, + HttpClientResult httpClientResult) { + try { + getClient(uri) + .execute( + new HttpHost(uri.getScheme(), uri.getHost(), uri.getPort()), + request, + responseCallback(httpClientResult)); + } catch (Throwable t) { + httpClientResult.complete(t); + } + } + } + + @Nested + class ApacheClientHostRequestContextTest extends ApacheHttpClientTest { + + @Override + public BasicClassicHttpRequest buildRequest( + String method, URI uri, Map headers) { + // also testing with an absolute path below + return configureRequest(new BasicClassicHttpRequest(method, fullPathFromUri(uri)), headers); + } + + @Override + public int sendRequest( + BasicClassicHttpRequest request, String method, URI uri, Map headers) + throws Exception { + return getClient(uri) + .execute( + new HttpHost(uri.getScheme(), uri.getHost(), uri.getPort()), + request, + new BasicHttpContext(), + responseHandler()); + } + + @Override + public void sendRequestWithCallback( + BasicClassicHttpRequest request, + String method, + URI uri, + Map headers, + HttpClientResult httpClientResult) { + try { + getClient(uri) + .execute( + new HttpHost(uri.getScheme(), uri.getHost(), uri.getPort()), + request, + new BasicHttpContext(), + responseCallback(httpClientResult)); + } catch (Throwable t) { + httpClientResult.complete(t); + } + } + } + + @Nested + class ApacheClientHostAbsoluteUriRequestTest + extends ApacheHttpClientTest { + + @Override + public BasicClassicHttpRequest buildRequest( + String method, URI uri, Map headers) { + return configureRequest(new BasicClassicHttpRequest(method, uri.toString()), headers); + } + + @Override + public int sendRequest( + BasicClassicHttpRequest request, String method, URI uri, Map headers) + throws Exception { + return getClient(uri) + .execute( + new HttpHost(uri.getScheme(), uri.getHost(), uri.getPort()), + request, + responseHandler()); + } + + @Override + public void sendRequestWithCallback( + BasicClassicHttpRequest request, + String method, + URI uri, + Map headers, + HttpClientResult httpClientResult) { + try { + getClient(uri) + .execute( + new HttpHost(uri.getScheme(), uri.getHost(), uri.getPort()), + request, + responseCallback(httpClientResult)); + } catch (Throwable t) { + httpClientResult.complete(t); + } + } + } + + @Nested + class ApacheClientHostAbsoluteUriRequestContextTest + extends ApacheHttpClientTest { + + @Override + public BasicClassicHttpRequest buildRequest( + String method, URI uri, Map headers) { + return configureRequest(new BasicClassicHttpRequest(method, uri.toString()), headers); + } + + @Override + public int sendRequest( + BasicClassicHttpRequest request, String method, URI uri, Map headers) + throws Exception { + return getClient(uri) + .execute( + new HttpHost(uri.getScheme(), uri.getHost(), uri.getPort()), + request, + new BasicHttpContext(), + responseHandler()); + } + + @Override + public void sendRequestWithCallback( + BasicClassicHttpRequest request, + String method, + URI uri, + Map headers, + HttpClientResult httpClientResult) { + try { + getClient(uri) + .execute( + new HttpHost(uri.getScheme(), uri.getHost(), uri.getPort()), + request, + new BasicHttpContext(), + responseCallback(httpClientResult)); + } catch (Throwable t) { + httpClientResult.complete(t); + } + } + } + + @Nested + class ApacheClientUriRequestTest extends ApacheHttpClientTest { + + @Override + public BasicClassicHttpRequest buildRequest( + String method, URI uri, Map headers) { + // also testing with an absolute path below + return configureRequest(new HttpUriRequest(method, uri), headers); + } + + @Override + public int sendRequest( + BasicClassicHttpRequest request, String method, URI uri, Map headers) + throws Exception { + return getClient(uri).execute(request, new BasicHttpContext(), responseHandler()); + } + + @Override + public void sendRequestWithCallback( + BasicClassicHttpRequest request, + String method, + URI uri, + Map headers, + HttpClientResult httpClientResult) { + try { + getClient(uri).execute(request, responseCallback(httpClientResult)); + } catch (Throwable t) { + httpClientResult.complete(t); + } + } + } + + @Nested + class ApacheClientUriRequestContextTest extends ApacheHttpClientTest { + + @Override + public HttpUriRequest buildRequest(String method, URI uri, Map headers) { + // also testing with an absolute path below + return configureRequest(new HttpUriRequest(method, uri), headers); + } + + @Override + public int sendRequest( + HttpUriRequest request, String method, URI uri, Map headers) + throws Exception { + return getClient(uri) + .execute( + new HttpHost(uri.getScheme(), uri.getHost(), uri.getPort()), + request, + new BasicHttpContext(), + responseHandler()); + } + + @Override + public void sendRequestWithCallback( + HttpUriRequest request, + String method, + URI uri, + Map headers, + HttpClientResult httpClientResult) { + try { + getClient(uri).execute(request, new BasicHttpContext(), responseCallback(httpClientResult)); + } catch (Throwable t) { + httpClientResult.complete(t); + } + } + } + + static T configureRequest(T request, Map headers) { + request.addHeader("user-agent", "apachehttpclient"); + headers.forEach((key, value) -> request.setHeader(new BasicHeader(key, value))); + return request; + } + + static int getResponseCode(ClassicHttpResponse response) { + try { + if (response.getEntity() != null && response.getEntity().getContent() != null) { + response.getEntity().getContent().close(); + } + response.close(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + return response.getCode(); + } + + static HttpClientResponseHandler responseCallback( + HttpClientResult httpClientResult) { + return response -> { + try { + response.close(); + httpClientResult.complete(getResponseCode(response)); + } catch (Throwable t) { + httpClientResult.complete(t); + return response; + } + return response; + }; + } + + static HttpClientResponseHandler responseHandler() { + return response -> getResponseCode(response); + } + + 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(); + } +} diff --git a/instrumentation/apache-httpclient/apache-httpclient-5.2/library/src/test/java/io/opentelemetry/instrumentation/apachehttpclient/v5_2/ApacheHttpClient5Test.java b/instrumentation/apache-httpclient/apache-httpclient-5.2/library/src/test/java/io/opentelemetry/instrumentation/apachehttpclient/v5_2/ApacheHttpClient5Test.java new file mode 100644 index 000000000000..8d8f5a4c289f --- /dev/null +++ b/instrumentation/apache-httpclient/apache-httpclient-5.2/library/src/test/java/io/opentelemetry/instrumentation/apachehttpclient/v5_2/ApacheHttpClient5Test.java @@ -0,0 +1,62 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.apachehttpclient.v5_2; + +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpClientTest; +import io.opentelemetry.instrumentation.testing.junit.http.HttpClientInstrumentationExtension; +import java.util.Collections; +import org.apache.hc.client5.http.config.ConnectionConfig; +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.io.PoolingHttpClientConnectionManager; +import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder; +import org.apache.hc.core5.util.Timeout; +import org.junit.jupiter.api.extension.RegisterExtension; + +class ApacheHttpClient5Test extends AbstractApacheHttpClient5Test { + + @RegisterExtension + static final InstrumentationExtension testing = HttpClientInstrumentationExtension.forLibrary(); + + @Override + protected InstrumentationExtension testing() { + return testing; + } + + @Override + protected CloseableHttpClient createClient(boolean readTimeout) { + HttpClientBuilder builder = + ApacheHttpClient5Telemetry.builder(testing.getOpenTelemetry()) + .setCapturedRequestHeaders( + Collections.singletonList(AbstractHttpClientTest.TEST_REQUEST_HEADER)) + .setCapturedResponseHeaders( + Collections.singletonList(AbstractHttpClientTest.TEST_RESPONSE_HEADER)) + .build() + .newHttpClientBuilder(); + builder.setDefaultRequestConfig(RequestConfig.custom().setMaxRedirects(2).build()); + + ConnectionConfig.Builder connectionConfigBuilder = + ConnectionConfig.custom() + .setConnectTimeout( + Timeout.ofMilliseconds(AbstractHttpClientTest.CONNECTION_TIMEOUT.toMillis())); + + if (readTimeout) { + connectionConfigBuilder.setSocketTimeout( + Timeout.ofMilliseconds(AbstractHttpClientTest.READ_TIMEOUT.toMillis())); + } + + PoolingHttpClientConnectionManager connManager = + PoolingHttpClientConnectionManagerBuilder.create() + .setDefaultConnectionConfig(connectionConfigBuilder.build()) + .build(); + + builder.setConnectionManager(connManager); + builder.setConnectionManagerShared(true); + return builder.build(); + } +} diff --git a/instrumentation/apache-httpclient/apache-httpclient-5.2/library/src/test/java/io/opentelemetry/instrumentation/apachehttpclient/v5_2/HttpUriRequest.java b/instrumentation/apache-httpclient/apache-httpclient-5.2/library/src/test/java/io/opentelemetry/instrumentation/apachehttpclient/v5_2/HttpUriRequest.java new file mode 100644 index 000000000000..e6ef9b2b1d9a --- /dev/null +++ b/instrumentation/apache-httpclient/apache-httpclient-5.2/library/src/test/java/io/opentelemetry/instrumentation/apachehttpclient/v5_2/HttpUriRequest.java @@ -0,0 +1,27 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.apachehttpclient.v5_2; + +import java.net.URI; +import org.apache.hc.client5.http.classic.methods.HttpUriRequestBase; + +final class HttpUriRequest extends HttpUriRequestBase { + + private static final long serialVersionUID = 1L; + + private final String methodName; + + HttpUriRequest(String methodName, URI uri) { + super(methodName, uri); + this.methodName = methodName; + setUri(uri); + } + + @Override + public String getMethod() { + return methodName; + } +} diff --git a/instrumentation/apache-shenyu-2.4/README.md b/instrumentation/apache-shenyu-2.4/README.md new file mode 100644 index 000000000000..d72e5cfccc25 --- /dev/null +++ b/instrumentation/apache-shenyu-2.4/README.md @@ -0,0 +1,5 @@ +# Settings for the Apache ShenYu instrumentation + +| System property | Type | Default | Description | +|---------------------------------------------------------------------| ------- | ------- |---------------------------------------------------------------------------------------------| +| `otel.instrumentation.apache-shenyu.experimental-span-attributes` | Boolean | `false` | Enable the capture of experimental span attributes. | diff --git a/instrumentation/apache-shenyu-2.4/javaagent/build.gradle.kts b/instrumentation/apache-shenyu-2.4/javaagent/build.gradle.kts new file mode 100644 index 000000000000..c80be7addd0a --- /dev/null +++ b/instrumentation/apache-shenyu-2.4/javaagent/build.gradle.kts @@ -0,0 +1,51 @@ +plugins { + id("otel.javaagent-instrumentation") +} + +muzzle { + pass { + group.set("org.apache.shenyu") + module.set("shenyu-web") + versions.set("[2.4.0,)") + assertInverse.set(true) + } +} + +dependencies { + compileOnly("org.apache.shenyu:shenyu-web:2.4.0") + compileOnly("com.google.auto.value:auto-value-annotations") + annotationProcessor("com.google.auto.value:auto-value") + + testLibrary("org.springframework.boot:spring-boot-starter-test:2.0.0.RELEASE") + + // based on apache shenyu 2.4.0 official example + testLibrary("org.apache.shenyu:shenyu-spring-boot-starter-gateway:2.4.0") { + exclude("org.codehaus.groovy", "groovy") + } + testImplementation("org.springframework.boot:spring-boot-starter-webflux:2.2.2.RELEASE") { + exclude("org.codehaus.groovy", "groovy") + } + + // the latest version of apache shenyu uses spring-boot 2.7 + latestDepTestLibrary("org.springframework.boot:spring-boot-starter-test:2.7.+") + + testInstrumentation(project(":instrumentation:netty:netty-4.1:javaagent")) +} + +tasks.withType().configureEach { + jvmArgs("-Dotel.instrumentation.apache-shenyu.experimental-span-attributes=true") + + // required on jdk17 + jvmArgs("--add-opens=java.base/java.lang=ALL-UNNAMED") + jvmArgs("-XX:+IgnoreUnrecognizedVMOptions") + + systemProperty("testLatestDeps", findProperty("testLatestDeps") as Boolean) +} + +configurations.testRuntimeClasspath { + resolutionStrategy { + // requires old logback (and therefore also old slf4j) + force("ch.qos.logback:logback-classic:1.2.11") + force("org.slf4j:slf4j-api:1.7.36") + } +} diff --git a/instrumentation/apache-shenyu-2.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apacheshenyu/v2_4/ApacheShenYuInstrumentationModule.java b/instrumentation/apache-shenyu-2.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apacheshenyu/v2_4/ApacheShenYuInstrumentationModule.java new file mode 100644 index 000000000000..75e4f2d9c40e --- /dev/null +++ b/instrumentation/apache-shenyu-2.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apacheshenyu/v2_4/ApacheShenYuInstrumentationModule.java @@ -0,0 +1,24 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.apacheshenyu.v2_4; + +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 ApacheShenYuInstrumentationModule extends InstrumentationModule { + public ApacheShenYuInstrumentationModule() { + super("apache-shenyu", "apache-shenyu-2.4"); + } + + @Override + public List typeInstrumentations() { + return Collections.singletonList(new ContextBuilderInstrumentation()); + } +} diff --git a/instrumentation/apache-shenyu-2.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apacheshenyu/v2_4/ApacheShenYuSingletons.java b/instrumentation/apache-shenyu-2.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apacheshenyu/v2_4/ApacheShenYuSingletons.java new file mode 100644 index 000000000000..a26a11623f3f --- /dev/null +++ b/instrumentation/apache-shenyu-2.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apacheshenyu/v2_4/ApacheShenYuSingletons.java @@ -0,0 +1,18 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.apacheshenyu.v2_4; + +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRouteGetter; +import org.apache.shenyu.common.dto.MetaData; + +public final class ApacheShenYuSingletons { + + private ApacheShenYuSingletons() {} + + public static HttpServerRouteGetter httpRouteGetter() { + return (context, metaData) -> metaData.getPath(); + } +} diff --git a/instrumentation/apache-shenyu-2.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apacheshenyu/v2_4/ContextBuilderInstrumentation.java b/instrumentation/apache-shenyu-2.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apacheshenyu/v2_4/ContextBuilderInstrumentation.java new file mode 100644 index 000000000000..c858eb1bad33 --- /dev/null +++ b/instrumentation/apache-shenyu-2.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apacheshenyu/v2_4/ContextBuilderInstrumentation.java @@ -0,0 +1,56 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.apacheshenyu.v2_4; + +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.instrumentation.api.semconv.http.HttpServerRoute; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRouteSource; +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.shenyu.common.constant.Constants; +import org.apache.shenyu.common.dto.MetaData; +import org.springframework.web.server.ServerWebExchange; + +public class ContextBuilderInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher typeMatcher() { + return named("org.apache.shenyu.plugin.global.DefaultShenyuContextBuilder"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + named("build") + .and(takesArgument(0, named("org.springframework.web.server.ServerWebExchange"))) + .and(isPublic()), + this.getClass().getName() + "$BuildAdvice"); + } + + @SuppressWarnings("unused") + public static class BuildAdvice { + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void onExit(@Advice.Argument(0) ServerWebExchange exchange) { + Context context = Context.current(); + MetaData metaData = exchange.getAttribute(Constants.META_DATA); + if (metaData == null) { + return; + } + HttpServerRoute.update( + context, + HttpServerRouteSource.NESTED_CONTROLLER, + ApacheShenYuSingletons.httpRouteGetter(), + metaData); + MetaDataHelper.extractAttributes(metaData, context); + } + } +} diff --git a/instrumentation/apache-shenyu-2.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apacheshenyu/v2_4/MetaDataHelper.java b/instrumentation/apache-shenyu-2.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apacheshenyu/v2_4/MetaDataHelper.java new file mode 100644 index 000000000000..4414e6eeb628 --- /dev/null +++ b/instrumentation/apache-shenyu-2.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apacheshenyu/v2_4/MetaDataHelper.java @@ -0,0 +1,85 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.apacheshenyu.v2_4; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.instrumenter.LocalRootSpan; +import io.opentelemetry.javaagent.bootstrap.internal.AgentInstrumentationConfig; +import org.apache.shenyu.common.dto.MetaData; + +public final class MetaDataHelper { + + /** ID for apache shenyu metadata * */ + private static final AttributeKey META_ID_ATTRIBUTE = + AttributeKey.stringKey("apache-shenyu.meta.id"); + + /** App name for apache shenyu metadata * */ + private static final AttributeKey APP_NAME_ATTRIBUTE = + AttributeKey.stringKey("apache-shenyu.meta.app-name"); + + /** Context path for apache shenyu metadata * */ + private static final AttributeKey CONTEXT_PATH_ATTRIBUTE = + AttributeKey.stringKey("apache-shenyu.meta.context-path"); + + /** Path for apache shenyu metadata * */ + private static final AttributeKey PATH_ATTRIBUTE = + AttributeKey.stringKey("apache-shenyu.meta.path"); + + /** Rpc type for apache shenyu metadata * */ + private static final AttributeKey RPC_TYPE_ATTRIBUTE = + AttributeKey.stringKey("apache-shenyu.meta.rpc-type"); + + /** Service name for apache shenyu metadata * */ + private static final AttributeKey SERVICE_NAME_ATTRIBUTE = + AttributeKey.stringKey("apache-shenyu.meta.service-name"); + + /** Method name for apache shenyu metadata * */ + private static final AttributeKey METHOD_NAME_ATTRIBUTE = + AttributeKey.stringKey("apache-shenyu.meta.method-name"); + + /** Parameter types for apache shenyu metadata * */ + private static final AttributeKey PARAMETER_TYPES_ATTRIBUTE = + AttributeKey.stringKey("apache-shenyu.meta.param-types"); + + /** Rpc extension for apache shenyu metadata * */ + private static final AttributeKey RPC_EXT_ATTRIBUTE = + AttributeKey.stringKey("apache-shenyu.meta.rpc-ext"); + + /** Rpc extension for apache shenyu metadata * */ + private static final AttributeKey META_ENABLED_ATTRIBUTE = + AttributeKey.booleanKey("apache-shenyu.meta.enabled"); + + private static final boolean CAPTURE_EXPERIMENTAL_SPAN_ATTRIBUTES; + + static { + CAPTURE_EXPERIMENTAL_SPAN_ATTRIBUTES = + AgentInstrumentationConfig.get() + .getBoolean("otel.instrumentation.apache-shenyu.experimental-span-attributes", false); + } + + private MetaDataHelper() {} + + public static void extractAttributes(MetaData metadata, Context context) { + if (metadata != null && CAPTURE_EXPERIMENTAL_SPAN_ATTRIBUTES) { + Span serverSpan = LocalRootSpan.fromContextOrNull(context); + if (serverSpan == null) { + return; + } + serverSpan.setAttribute(META_ID_ATTRIBUTE, metadata.getId()); + serverSpan.setAttribute(APP_NAME_ATTRIBUTE, metadata.getAppName()); + serverSpan.setAttribute(CONTEXT_PATH_ATTRIBUTE, metadata.getContextPath()); + serverSpan.setAttribute(PATH_ATTRIBUTE, metadata.getPath()); + serverSpan.setAttribute(RPC_TYPE_ATTRIBUTE, metadata.getRpcType()); + serverSpan.setAttribute(SERVICE_NAME_ATTRIBUTE, metadata.getServiceName()); + serverSpan.setAttribute(METHOD_NAME_ATTRIBUTE, metadata.getMethodName()); + serverSpan.setAttribute(PARAMETER_TYPES_ATTRIBUTE, metadata.getParameterTypes()); + serverSpan.setAttribute(RPC_EXT_ATTRIBUTE, metadata.getRpcExt()); + serverSpan.setAttribute(META_ENABLED_ATTRIBUTE, metadata.getEnabled()); + } + } +} diff --git a/instrumentation/apache-shenyu-2.4/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apacheshenyu/v2_4/ShenYuBootstrapApplication.java b/instrumentation/apache-shenyu-2.4/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apacheshenyu/v2_4/ShenYuBootstrapApplication.java new file mode 100644 index 000000000000..6048d8ca97d2 --- /dev/null +++ b/instrumentation/apache-shenyu-2.4/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apacheshenyu/v2_4/ShenYuBootstrapApplication.java @@ -0,0 +1,12 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.apacheshenyu.v2_4; + +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.autoconfigure.gson.GsonAutoConfiguration; + +@SpringBootApplication(exclude = {GsonAutoConfiguration.class}) +public class ShenYuBootstrapApplication {} diff --git a/instrumentation/apache-shenyu-2.4/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apacheshenyu/v2_4/ShenYuRouteTest.java b/instrumentation/apache-shenyu-2.4/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apacheshenyu/v2_4/ShenYuRouteTest.java new file mode 100644 index 000000000000..06cd346e8f03 --- /dev/null +++ b/instrumentation/apache-shenyu-2.4/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apacheshenyu/v2_4/ShenYuRouteTest.java @@ -0,0 +1,137 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.apacheshenyu.v2_4; + +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import org.apache.shenyu.common.dto.MetaData; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import reactor.netty.http.client.HttpClient; + +@ExtendWith(SpringExtension.class) +@SpringBootTest( + properties = {"shenyu.local.enabled=true", "spring.main.allow-bean-definition-overriding=true"}, + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + classes = {ShenYuBootstrapApplication.class}) +class ShenYuRouteTest { + + private static final AttributeKey META_ID_ATTRIBUTE = + AttributeKey.stringKey("apache-shenyu.meta.id"); + + private static final AttributeKey APP_NAME_ATTRIBUTE = + AttributeKey.stringKey("apache-shenyu.meta.app-name"); + + private static final AttributeKey CONTEXT_PATH_ATTRIBUTE = + AttributeKey.stringKey("apache-shenyu.meta.context-path"); + + private static final AttributeKey PATH_ATTRIBUTE = + AttributeKey.stringKey("apache-shenyu.meta.path"); + + private static final AttributeKey RPC_TYPE_ATTRIBUTE = + AttributeKey.stringKey("apache-shenyu.meta.rpc-type"); + + private static final AttributeKey SERVICE_NAME_ATTRIBUTE = + AttributeKey.stringKey("apache-shenyu.meta.service-name"); + + private static final AttributeKey METHOD_NAME_ATTRIBUTE = + AttributeKey.stringKey("apache-shenyu.meta.method-name"); + + private static final AttributeKey PARAMETER_TYPES_ATTRIBUTE = + AttributeKey.stringKey("apache-shenyu.meta.param-types"); + + private static final AttributeKey RPC_EXT_ATTRIBUTE = + AttributeKey.stringKey("apache-shenyu.meta.rpc-ext"); + + private static final AttributeKey META_ENABLED_ATTRIBUTE = + AttributeKey.booleanKey("apache-shenyu.meta.enabled"); + + @Value("${local.server.port}") + private int port; + + @RegisterExtension + private static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + @BeforeAll + static void beforeAll() + throws ClassNotFoundException, + NoSuchMethodException, + InvocationTargetException, + IllegalAccessException { + + Class metaDataCache = null; + try { + metaDataCache = Class.forName("org.apache.shenyu.plugin.global.cache.MetaDataCache"); + } catch (ClassNotFoundException e) { + // in 2.5.0, the MetaDataCache turned to be org.apache.shenyu.plugin.base.cache + metaDataCache = Class.forName("org.apache.shenyu.plugin.base.cache.MetaDataCache"); + } + + Object cacheInst = metaDataCache.getMethod("getInstance").invoke(null); + Method cacheMethod = metaDataCache.getMethod("cache", MetaData.class); + + cacheMethod.invoke( + cacheInst, + new MetaData( + "123", + "test-shenyu", + "/", + "/a/b/c", + "http", + "shenyu-service", + "hello", + "string", + "test-ext", + true)); + } + + @Test + void testUpdateRouter() { + HttpClient httpClient = HttpClient.create(); + httpClient.get().uri("http://localhost:" + port + "/a/b/c").response().block(); + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("GET").hasKind(SpanKind.CLIENT), + span -> + span.hasName("GET /a/b/c") + .hasKind(SpanKind.SERVER) + .hasAttributesSatisfying( + equalTo(AttributeKey.stringKey("http.route"), "/a/b/c"), + equalTo(META_ID_ATTRIBUTE, "123"), + equalTo(META_ENABLED_ATTRIBUTE, true), + equalTo(METHOD_NAME_ATTRIBUTE, "hello"), + equalTo(PARAMETER_TYPES_ATTRIBUTE, "string"), + equalTo(PATH_ATTRIBUTE, "/a/b/c"), + equalTo(RPC_EXT_ATTRIBUTE, "test-ext"), + equalTo(RPC_TYPE_ATTRIBUTE, "http"), + equalTo(SERVICE_NAME_ATTRIBUTE, "shenyu-service"), + equalTo(APP_NAME_ATTRIBUTE, "test-shenyu"), + equalTo(CONTEXT_PATH_ATTRIBUTE, "/")))); + } + + @Test + void testUnmatchedRouter() { + HttpClient httpClient = HttpClient.create(); + httpClient.get().uri("http://localhost:" + port + "/a/b/c/d").response().block(); + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("GET").hasKind(SpanKind.CLIENT), + span -> span.hasName("GET").hasKind(SpanKind.SERVER))); + } +} diff --git a/instrumentation/armeria-1.3/library/src/main/java/io/opentelemetry/instrumentation/armeria/v1_3/ArmeriaHttpClientAttributesGetter.java b/instrumentation/armeria-1.3/library/src/main/java/io/opentelemetry/instrumentation/armeria/v1_3/ArmeriaHttpClientAttributesGetter.java deleted file mode 100644 index 3ab7e9449cf5..000000000000 --- a/instrumentation/armeria-1.3/library/src/main/java/io/opentelemetry/instrumentation/armeria/v1_3/ArmeriaHttpClientAttributesGetter.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * 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.HttpStatus; -import com.linecorp.armeria.common.RequestContext; -import com.linecorp.armeria.common.logging.RequestLog; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpClientAttributesGetter; -import java.util.List; -import javax.annotation.Nullable; - -enum ArmeriaHttpClientAttributesGetter - implements HttpClientAttributesGetter { - INSTANCE; - - @Override - public String getHttpRequestMethod(RequestContext ctx) { - return ctx.method().name(); - } - - @Override - public String getUrlFull(RequestContext ctx) { - HttpRequest request = request(ctx); - StringBuilder uri = new StringBuilder(); - String scheme = request.scheme(); - if (scheme != null) { - uri.append(scheme).append("://"); - } - String authority = request.authority(); - if (authority != null) { - uri.append(authority); - } - uri.append(request.path()); - return uri.toString(); - } - - @Override - public List getHttpRequestHeader(RequestContext ctx, String name) { - return request(ctx).headers().getAll(name); - } - - @Override - @Nullable - public Integer getHttpResponseStatusCode( - RequestContext ctx, RequestLog requestLog, @Nullable Throwable error) { - HttpStatus status = requestLog.responseHeaders().status(); - if (!status.equals(HttpStatus.UNKNOWN)) { - return status.code(); - } - return null; - } - - @Override - public List getHttpResponseHeader( - RequestContext ctx, RequestLog requestLog, String name) { - return requestLog.responseHeaders().getAll(name); - } - - 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/instrumentation/armeria-1.3/library/src/main/java/io/opentelemetry/instrumentation/armeria/v1_3/ArmeriaHttpServerAttributesGetter.java b/instrumentation/armeria-1.3/library/src/main/java/io/opentelemetry/instrumentation/armeria/v1_3/ArmeriaHttpServerAttributesGetter.java deleted file mode 100644 index 25f183c8b88f..000000000000 --- a/instrumentation/armeria-1.3/library/src/main/java/io/opentelemetry/instrumentation/armeria/v1_3/ArmeriaHttpServerAttributesGetter.java +++ /dev/null @@ -1,86 +0,0 @@ -/* - * 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.HttpStatus; -import com.linecorp.armeria.common.RequestContext; -import com.linecorp.armeria.common.logging.RequestLog; -import com.linecorp.armeria.server.ServiceRequestContext; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpServerAttributesGetter; -import java.util.List; -import javax.annotation.Nullable; - -enum ArmeriaHttpServerAttributesGetter - implements HttpServerAttributesGetter { - INSTANCE; - - @Override - public String getHttpRequestMethod(RequestContext ctx) { - return ctx.method().name(); - } - - @Override - @Nullable - public String getUrlScheme(RequestContext ctx) { - return request(ctx).scheme(); - } - - @Override - public String getUrlPath(RequestContext ctx) { - String fullPath = request(ctx).path(); - int separatorPos = fullPath.indexOf('?'); - return separatorPos == -1 ? fullPath : fullPath.substring(0, separatorPos); - } - - @Nullable - @Override - public String getUrlQuery(RequestContext ctx) { - String fullPath = request(ctx).path(); - int separatorPos = fullPath.indexOf('?'); - return separatorPos == -1 ? null : fullPath.substring(separatorPos + 1); - } - - @Override - public List getHttpRequestHeader(RequestContext ctx, String name) { - return request(ctx).headers().getAll(name); - } - - @Override - @Nullable - public Integer getHttpResponseStatusCode( - RequestContext ctx, RequestLog requestLog, @Nullable Throwable error) { - HttpStatus status = requestLog.responseHeaders().status(); - if (!status.equals(HttpStatus.UNKNOWN)) { - return status.code(); - } - return null; - } - - @Override - public List getHttpResponseHeader( - RequestContext ctx, RequestLog requestLog, String name) { - return requestLog.responseHeaders().getAll(name); - } - - @Override - @Nullable - public String getHttpRoute(RequestContext ctx) { - if (ctx instanceof ServiceRequestContext) { - return ((ServiceRequestContext) ctx).config().route().patternString(); - } - 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/instrumentation/armeria-1.3/library/src/main/java/io/opentelemetry/instrumentation/armeria/v1_3/ArmeriaNetServerAttributesGetter.java b/instrumentation/armeria-1.3/library/src/main/java/io/opentelemetry/instrumentation/armeria/v1_3/ArmeriaNetServerAttributesGetter.java deleted file mode 100644 index 74206910ac93..000000000000 --- a/instrumentation/armeria-1.3/library/src/main/java/io/opentelemetry/instrumentation/armeria/v1_3/ArmeriaNetServerAttributesGetter.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * 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.SessionProtocol; -import com.linecorp.armeria.common.logging.RequestLog; -import io.opentelemetry.instrumentation.api.instrumenter.net.NetServerAttributesGetter; -import io.opentelemetry.instrumentation.armeria.v1_3.internal.RequestContextAccess; -import java.net.InetSocketAddress; -import javax.annotation.Nullable; - -final class ArmeriaNetServerAttributesGetter - implements NetServerAttributesGetter { - - @Override - public String getNetworkProtocolName(RequestContext ctx, @Nullable RequestLog requestLog) { - return "http"; - } - - @Override - public String getNetworkProtocolVersion(RequestContext ctx, @Nullable RequestLog requestLog) { - SessionProtocol protocol = ctx.sessionProtocol(); - return protocol.isMultiplex() ? "2.0" : "1.1"; - } - - @Nullable - @Override - public String getServerAddress(RequestContext ctx) { - return null; - } - - @Nullable - @Override - public Integer getServerPort(RequestContext ctx) { - return null; - } - - @Override - @Nullable - public InetSocketAddress getClientInetSocketAddress( - RequestContext ctx, @Nullable RequestLog requestLog) { - return RequestContextAccess.remoteAddress(ctx); - } - - @Nullable - @Override - public InetSocketAddress getServerInetSocketAddress( - RequestContext ctx, @Nullable RequestLog log) { - return RequestContextAccess.localAddress(ctx); - } -} diff --git a/instrumentation/armeria-1.3/library/src/main/java/io/opentelemetry/instrumentation/armeria/v1_3/ArmeriaTelemetryBuilder.java b/instrumentation/armeria-1.3/library/src/main/java/io/opentelemetry/instrumentation/armeria/v1_3/ArmeriaTelemetryBuilder.java deleted file mode 100644 index 9490044d65ed..000000000000 --- a/instrumentation/armeria-1.3/library/src/main/java/io/opentelemetry/instrumentation/armeria/v1_3/ArmeriaTelemetryBuilder.java +++ /dev/null @@ -1,193 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.armeria.v1_3; - -import com.google.errorprone.annotations.CanIgnoreReturnValue; -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.SpanStatusExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpClientAttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpClientAttributesExtractorBuilder; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpClientMetrics; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteHolder; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpServerAttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpServerAttributesExtractorBuilder; -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 io.opentelemetry.instrumentation.armeria.v1_3.internal.ArmeriaNetClientAttributesGetter; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; -import java.util.ArrayList; -import java.util.List; -import java.util.function.Function; -import java.util.stream.Stream; -import javax.annotation.Nullable; - -public final class ArmeriaTelemetryBuilder { - - private static final String INSTRUMENTATION_NAME = "io.opentelemetry.armeria-1.3"; - - private final OpenTelemetry openTelemetry; - @Nullable private String peerService; - - private final List> - additionalExtractors = new ArrayList<>(); - private final List> - additionalClientExtractors = new ArrayList<>(); - - private final HttpClientAttributesExtractorBuilder - httpClientAttributesExtractorBuilder = - HttpClientAttributesExtractor.builder( - ArmeriaHttpClientAttributesGetter.INSTANCE, new ArmeriaNetClientAttributesGetter()); - private final HttpServerAttributesExtractorBuilder - httpServerAttributesExtractorBuilder = - HttpServerAttributesExtractor.builder( - ArmeriaHttpServerAttributesGetter.INSTANCE, new ArmeriaNetServerAttributesGetter()); - - private Function< - SpanStatusExtractor, - ? extends SpanStatusExtractor> - statusExtractorTransformer = Function.identity(); - - ArmeriaTelemetryBuilder(OpenTelemetry openTelemetry) { - this.openTelemetry = openTelemetry; - } - - @CanIgnoreReturnValue - public ArmeriaTelemetryBuilder 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. - */ - @CanIgnoreReturnValue - public ArmeriaTelemetryBuilder addAttributeExtractor( - AttributesExtractor attributesExtractor) { - additionalExtractors.add(attributesExtractor); - return this; - } - - /** - * Adds an extra client-only {@link AttributesExtractor} to invoke to set attributes to - * instrumented items. The {@link AttributesExtractor} will be executed after all default - * extractors. - */ - @CanIgnoreReturnValue - public ArmeriaTelemetryBuilder addClientAttributeExtractor( - AttributesExtractor attributesExtractor) { - additionalClientExtractors.add(attributesExtractor); - return this; - } - - /** Sets the {@code peer.service} attribute for http client spans. */ - @CanIgnoreReturnValue - public ArmeriaTelemetryBuilder setPeerService(String peerService) { - this.peerService = peerService; - return this; - } - - /** - * Configures the HTTP client request headers that will be captured as span attributes. - * - * @param requestHeaders A list of HTTP header names. - */ - @CanIgnoreReturnValue - public ArmeriaTelemetryBuilder setCapturedClientRequestHeaders(List requestHeaders) { - httpClientAttributesExtractorBuilder.setCapturedRequestHeaders(requestHeaders); - return this; - } - - /** - * Configures the HTTP client response headers that will be captured as span attributes. - * - * @param responseHeaders A list of HTTP header names. - */ - @CanIgnoreReturnValue - public ArmeriaTelemetryBuilder setCapturedClientResponseHeaders(List responseHeaders) { - httpClientAttributesExtractorBuilder.setCapturedResponseHeaders(responseHeaders); - return this; - } - - /** - * Configures the HTTP server request headers that will be captured as span attributes. - * - * @param requestHeaders A list of HTTP header names. - */ - @CanIgnoreReturnValue - public ArmeriaTelemetryBuilder setCapturedServerRequestHeaders(List requestHeaders) { - httpServerAttributesExtractorBuilder.setCapturedRequestHeaders(requestHeaders); - return this; - } - - /** - * Configures the HTTP server response headers that will be captured as span attributes. - * - * @param responseHeaders A list of HTTP header names. - */ - @CanIgnoreReturnValue - public ArmeriaTelemetryBuilder setCapturedServerResponseHeaders(List responseHeaders) { - httpServerAttributesExtractorBuilder.setCapturedResponseHeaders(responseHeaders); - return this; - } - - public ArmeriaTelemetry build() { - ArmeriaHttpClientAttributesGetter clientAttributesGetter = - ArmeriaHttpClientAttributesGetter.INSTANCE; - ArmeriaHttpServerAttributesGetter serverAttributesGetter = - ArmeriaHttpServerAttributesGetter.INSTANCE; - - InstrumenterBuilder clientInstrumenterBuilder = - Instrumenter.builder( - openTelemetry, - INSTRUMENTATION_NAME, - HttpSpanNameExtractor.create(clientAttributesGetter)); - InstrumenterBuilder serverInstrumenterBuilder = - Instrumenter.builder( - openTelemetry, - INSTRUMENTATION_NAME, - HttpSpanNameExtractor.create(serverAttributesGetter)); - - Stream.of(clientInstrumenterBuilder, serverInstrumenterBuilder) - .forEach(instrumenter -> instrumenter.addAttributesExtractors(additionalExtractors)); - - clientInstrumenterBuilder - .setSpanStatusExtractor( - statusExtractorTransformer.apply( - HttpSpanStatusExtractor.create(clientAttributesGetter))) - .addAttributesExtractor(httpClientAttributesExtractorBuilder.build()) - .addAttributesExtractors(additionalClientExtractors) - .addOperationMetrics(HttpClientMetrics.get()); - serverInstrumenterBuilder - .setSpanStatusExtractor( - statusExtractorTransformer.apply( - HttpSpanStatusExtractor.create(serverAttributesGetter))) - .addAttributesExtractor(httpServerAttributesExtractorBuilder.build()) - .addOperationMetrics(HttpServerMetrics.get()) - .addContextCustomizer(HttpRouteHolder.create(serverAttributesGetter)); - - if (peerService != null) { - clientInstrumenterBuilder.addAttributesExtractor( - AttributesExtractor.constant(SemanticAttributes.PEER_SERVICE, peerService)); - } - - return new ArmeriaTelemetry( - clientInstrumenterBuilder.buildClientInstrumenter(ClientRequestContextSetter.INSTANCE), - serverInstrumenterBuilder.buildServerInstrumenter(RequestContextGetter.INSTANCE)); - } -} diff --git a/instrumentation/armeria-1.3/library/src/main/java/io/opentelemetry/instrumentation/armeria/v1_3/internal/ArmeriaNetClientAttributesGetter.java b/instrumentation/armeria-1.3/library/src/main/java/io/opentelemetry/instrumentation/armeria/v1_3/internal/ArmeriaNetClientAttributesGetter.java deleted file mode 100644 index 143151bcff35..000000000000 --- a/instrumentation/armeria-1.3/library/src/main/java/io/opentelemetry/instrumentation/armeria/v1_3/internal/ArmeriaNetClientAttributesGetter.java +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.armeria.v1_3.internal; - -import com.linecorp.armeria.common.HttpRequest; -import com.linecorp.armeria.common.RequestContext; -import com.linecorp.armeria.common.SessionProtocol; -import com.linecorp.armeria.common.logging.RequestLog; -import io.opentelemetry.instrumentation.api.instrumenter.net.NetClientAttributesGetter; -import java.net.InetSocketAddress; -import javax.annotation.Nullable; - -/** - * This class is internal and is hence not for public use. Its APIs are unstable and can change at - * any time. - */ -public final class ArmeriaNetClientAttributesGetter - implements NetClientAttributesGetter { - - @Override - public String getNetworkProtocolName(RequestContext ctx, @Nullable RequestLog requestLog) { - return "http"; - } - - @Override - public String getNetworkProtocolVersion(RequestContext ctx, @Nullable RequestLog requestLog) { - SessionProtocol protocol = ctx.sessionProtocol(); - return protocol.isMultiplex() ? "2.0" : "1.1"; - } - - @Nullable - @Override - public String getServerAddress(RequestContext ctx) { - HttpRequest request = request(ctx); - String authority = request.authority(); - if (authority == null) { - return null; - } - int separatorPos = authority.indexOf(':'); - return separatorPos == -1 ? authority : authority.substring(0, separatorPos); - } - - @Nullable - @Override - public Integer getServerPort(RequestContext ctx) { - HttpRequest request = request(ctx); - String authority = request.authority(); - if (authority == null) { - return null; - } - int separatorPos = authority.indexOf(':'); - if (separatorPos == -1) { - return null; - } - try { - return Integer.parseInt(authority.substring(separatorPos + 1)); - } catch (NumberFormatException e) { - return null; - } - } - - @Override - @Nullable - public InetSocketAddress getServerInetSocketAddress( - RequestContext ctx, @Nullable RequestLog requestLog) { - return RequestContextAccess.remoteAddress(ctx); - } - - 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/instrumentation/armeria-1.3/javaagent/build.gradle.kts b/instrumentation/armeria/armeria-1.3/javaagent/build.gradle.kts similarity index 63% rename from instrumentation/armeria-1.3/javaagent/build.gradle.kts rename to instrumentation/armeria/armeria-1.3/javaagent/build.gradle.kts index 2fbe62c943b6..64e7c0f61cb8 100644 --- a/instrumentation/armeria-1.3/javaagent/build.gradle.kts +++ b/instrumentation/armeria/armeria-1.3/javaagent/build.gradle.kts @@ -12,16 +12,17 @@ muzzle { } dependencies { - implementation(project(":instrumentation:armeria-1.3:library")) + implementation(project(":instrumentation:armeria:armeria-1.3:library")) testInstrumentation(project(":instrumentation:netty:netty-4.1:javaagent")) library("com.linecorp.armeria:armeria:1.3.0") + testLibrary("com.linecorp.armeria:armeria-junit5:1.3.0") - testImplementation(project(":instrumentation:armeria-1.3:testing")) + testImplementation(project(":instrumentation:armeria:armeria-1.3:testing")) } tasks { - test { + withType().configureEach { systemProperty("testLatestDeps", findProperty("testLatestDeps") as Boolean) } } diff --git a/instrumentation/armeria-1.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/armeria/v1_3/AbstractStreamMessageSubscriptionInstrumentation.java b/instrumentation/armeria/armeria-1.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/armeria/v1_3/AbstractStreamMessageSubscriptionInstrumentation.java similarity index 100% rename from instrumentation/armeria-1.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/armeria/v1_3/AbstractStreamMessageSubscriptionInstrumentation.java rename to instrumentation/armeria/armeria-1.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/armeria/v1_3/AbstractStreamMessageSubscriptionInstrumentation.java diff --git a/instrumentation/armeria/armeria-1.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/armeria/v1_3/ArmeriaHttpResponseMutator.java b/instrumentation/armeria/armeria-1.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/armeria/v1_3/ArmeriaHttpResponseMutator.java new file mode 100644 index 000000000000..a9380503da0a --- /dev/null +++ b/instrumentation/armeria/armeria-1.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/armeria/v1_3/ArmeriaHttpResponseMutator.java @@ -0,0 +1,18 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.armeria.v1_3; + +import com.linecorp.armeria.common.ResponseHeadersBuilder; +import io.opentelemetry.javaagent.bootstrap.http.HttpServerResponseMutator; + +enum ArmeriaHttpResponseMutator implements HttpServerResponseMutator { + INSTANCE; + + @Override + public void appendHeader(ResponseHeadersBuilder response, String name, String value) { + response.add(name, value); + } +} diff --git a/instrumentation/armeria-1.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/armeria/v1_3/ArmeriaInstrumentationModule.java b/instrumentation/armeria/armeria-1.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/armeria/v1_3/ArmeriaInstrumentationModule.java similarity index 100% rename from instrumentation/armeria-1.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/armeria/v1_3/ArmeriaInstrumentationModule.java rename to instrumentation/armeria/armeria-1.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/armeria/v1_3/ArmeriaInstrumentationModule.java diff --git a/instrumentation/armeria-1.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/armeria/v1_3/ArmeriaServerBuilderInstrumentation.java b/instrumentation/armeria/armeria-1.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/armeria/v1_3/ArmeriaServerBuilderInstrumentation.java similarity index 100% rename from instrumentation/armeria-1.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/armeria/v1_3/ArmeriaServerBuilderInstrumentation.java rename to instrumentation/armeria/armeria-1.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/armeria/v1_3/ArmeriaServerBuilderInstrumentation.java diff --git a/instrumentation/armeria-1.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/armeria/v1_3/ArmeriaSingletons.java b/instrumentation/armeria/armeria-1.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/armeria/v1_3/ArmeriaSingletons.java similarity index 51% rename from instrumentation/armeria-1.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/armeria/v1_3/ArmeriaSingletons.java rename to instrumentation/armeria/armeria-1.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/armeria/v1_3/ArmeriaSingletons.java index ee23124e8159..4845c528e7e1 100644 --- a/instrumentation/armeria-1.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/armeria/v1_3/ArmeriaSingletons.java +++ b/instrumentation/armeria/armeria-1.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/armeria/v1_3/ArmeriaSingletons.java @@ -8,10 +8,11 @@ import com.linecorp.armeria.client.HttpClient; import com.linecorp.armeria.server.HttpService; import io.opentelemetry.api.GlobalOpenTelemetry; -import io.opentelemetry.instrumentation.api.instrumenter.net.PeerServiceAttributesExtractor; +import io.opentelemetry.instrumentation.api.incubator.config.internal.CommonConfig; import io.opentelemetry.instrumentation.armeria.v1_3.ArmeriaTelemetry; -import io.opentelemetry.instrumentation.armeria.v1_3.internal.ArmeriaNetClientAttributesGetter; -import io.opentelemetry.javaagent.bootstrap.internal.CommonConfig; +import io.opentelemetry.instrumentation.armeria.v1_3.ArmeriaTelemetryBuilder; +import io.opentelemetry.instrumentation.armeria.v1_3.internal.ArmeriaInstrumenterBuilderUtil; +import io.opentelemetry.javaagent.bootstrap.internal.AgentCommonConfig; import java.util.function.Function; // Holds singleton references to decorators to match against during suppression. @@ -22,18 +23,16 @@ public final class ArmeriaSingletons { public static final Function SERVER_DECORATOR; static { - ArmeriaTelemetry telemetry = - ArmeriaTelemetry.builder(GlobalOpenTelemetry.get()) - .setCapturedClientRequestHeaders(CommonConfig.get().getClientRequestHeaders()) - .setCapturedClientResponseHeaders(CommonConfig.get().getClientResponseHeaders()) - .addClientAttributeExtractor( - PeerServiceAttributesExtractor.create( - new ArmeriaNetClientAttributesGetter(), - CommonConfig.get().getPeerServiceMapping())) - .build(); + ArmeriaTelemetryBuilder builder = ArmeriaTelemetry.builder(GlobalOpenTelemetry.get()); + CommonConfig config = AgentCommonConfig.get(); + ArmeriaInstrumenterBuilderUtil.getClientBuilderExtractor().apply(builder).configure(config); + ArmeriaInstrumenterBuilderUtil.getServerBuilderExtractor().apply(builder).configure(config); + ArmeriaTelemetry telemetry = builder.build(); CLIENT_DECORATOR = telemetry.newClientDecorator(); - SERVER_DECORATOR = service -> new ServerDecorator(service); + Function libraryDecorator = + telemetry.newServiceDecorator().compose(ResponseCustomizingDecorator::new); + SERVER_DECORATOR = service -> new ServerDecorator(service, libraryDecorator.apply(service)); } private ArmeriaSingletons() {} diff --git a/instrumentation/armeria-1.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/armeria/v1_3/ArmeriaWebClientBuilderInstrumentation.java b/instrumentation/armeria/armeria-1.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/armeria/v1_3/ArmeriaWebClientBuilderInstrumentation.java similarity index 100% rename from instrumentation/armeria-1.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/armeria/v1_3/ArmeriaWebClientBuilderInstrumentation.java rename to instrumentation/armeria/armeria-1.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/armeria/v1_3/ArmeriaWebClientBuilderInstrumentation.java diff --git a/instrumentation/armeria-1.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/armeria/v1_3/CompletableFutureWrapper.java b/instrumentation/armeria/armeria-1.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/armeria/v1_3/CompletableFutureWrapper.java similarity index 100% rename from instrumentation/armeria-1.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/armeria/v1_3/CompletableFutureWrapper.java rename to instrumentation/armeria/armeria-1.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/armeria/v1_3/CompletableFutureWrapper.java diff --git a/instrumentation/armeria/armeria-1.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/armeria/v1_3/ResponseCustomizingDecorator.java b/instrumentation/armeria/armeria-1.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/armeria/v1_3/ResponseCustomizingDecorator.java new file mode 100644 index 000000000000..35a60207e89e --- /dev/null +++ b/instrumentation/armeria/armeria-1.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/armeria/v1_3/ResponseCustomizingDecorator.java @@ -0,0 +1,47 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.armeria.v1_3; + +import com.linecorp.armeria.common.FilteredHttpResponse; +import com.linecorp.armeria.common.HttpObject; +import com.linecorp.armeria.common.HttpRequest; +import com.linecorp.armeria.common.HttpResponse; +import com.linecorp.armeria.common.ResponseHeaders; +import com.linecorp.armeria.common.ResponseHeadersBuilder; +import com.linecorp.armeria.server.HttpService; +import com.linecorp.armeria.server.ServiceRequestContext; +import com.linecorp.armeria.server.SimpleDecoratingHttpService; +import io.opentelemetry.context.Context; +import io.opentelemetry.javaagent.bootstrap.http.HttpServerResponseCustomizerHolder; + +class ResponseCustomizingDecorator extends SimpleDecoratingHttpService { + + ResponseCustomizingDecorator(HttpService delegate) { + super(delegate); + } + + @Override + public HttpResponse serve(ServiceRequestContext ctx, HttpRequest req) throws Exception { + HttpResponse response = unwrap().serve(ctx, req); + Context context = Context.current(); + return new FilteredHttpResponse(response) { + @Override + public HttpObject filter(HttpObject obj) { + // Ignore other objects like HttpData. + if (!(obj instanceof ResponseHeaders)) { + return obj; + } + + ResponseHeaders headers = (ResponseHeaders) obj; + ResponseHeadersBuilder headersBuilder = headers.toBuilder(); + HttpServerResponseCustomizerHolder.getCustomizer() + .customize(context, headersBuilder, ArmeriaHttpResponseMutator.INSTANCE); + + return headersBuilder.build(); + } + }; + } +} diff --git a/instrumentation/armeria-1.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/armeria/v1_3/ServerDecorator.java b/instrumentation/armeria/armeria-1.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/armeria/v1_3/ServerDecorator.java similarity index 61% rename from instrumentation/armeria-1.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/armeria/v1_3/ServerDecorator.java rename to instrumentation/armeria/armeria-1.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/armeria/v1_3/ServerDecorator.java index 102305b20d07..476d0b808006 100644 --- a/instrumentation/armeria-1.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/armeria/v1_3/ServerDecorator.java +++ b/instrumentation/armeria/armeria-1.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/armeria/v1_3/ServerDecorator.java @@ -14,17 +14,27 @@ import io.opentelemetry.api.trace.StatusCode; import io.opentelemetry.context.Context; import io.opentelemetry.instrumentation.api.instrumenter.ErrorCauseExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteHolder; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteSource; +import io.opentelemetry.instrumentation.api.instrumenter.LocalRootSpan; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRoute; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRouteSource; class ServerDecorator extends SimpleDecoratingHttpService { + private final HttpService libraryDelegate; - ServerDecorator(HttpService delegate) { + ServerDecorator(HttpService delegate, HttpService libraryDelegate) { super(delegate); + this.libraryDelegate = libraryDelegate; } @Override public HttpResponse serve(ServiceRequestContext ctx, HttpRequest req) throws Exception { + // If there is no server span fall back to armeria liberary instrumentation. Server span is + // usually created by netty instrumentation, it can be missing when netty instrumentation is + // disabled or when http2 is used (netty instrumentation does not support http2). + if (!LocalRootSpan.current().getSpanContext().isValid()) { + return libraryDelegate.serve(ctx, req); + } + String matchedRoute = ctx.config().route().patternString(); if (matchedRoute == null || matchedRoute.isEmpty()) { matchedRoute = "/"; @@ -34,8 +44,7 @@ public HttpResponse serve(ServiceRequestContext ctx, HttpRequest req) throws Exc Context otelContext = Context.current(); - HttpRouteHolder.updateHttpRoute( - otelContext, HttpRouteSource.SERVLET, (context, name) -> name, matchedRoute); + HttpServerRoute.update(otelContext, HttpServerRouteSource.SERVER, matchedRoute); try { return unwrap().serve(ctx, req); diff --git a/instrumentation/armeria-1.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/armeria/v1_3/SubscriberWrapper.java b/instrumentation/armeria/armeria-1.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/armeria/v1_3/SubscriberWrapper.java similarity index 100% rename from instrumentation/armeria-1.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/armeria/v1_3/SubscriberWrapper.java rename to instrumentation/armeria/armeria-1.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/armeria/v1_3/SubscriberWrapper.java diff --git a/instrumentation/armeria/armeria-1.3/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/armeria/v1_3/ArmeriaHttp2ServerTest.java b/instrumentation/armeria/armeria-1.3/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/armeria/v1_3/ArmeriaHttp2ServerTest.java new file mode 100644 index 000000000000..e54919bf49df --- /dev/null +++ b/instrumentation/armeria/armeria-1.3/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/armeria/v1_3/ArmeriaHttp2ServerTest.java @@ -0,0 +1,17 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.armeria.v1_3; + +import io.opentelemetry.instrumentation.testing.junit.http.HttpServerTestOptions; + +class ArmeriaHttp2ServerTest extends ArmeriaHttpServerTest { + + @Override + protected void configure(HttpServerTestOptions options) { + super.configure(options); + options.useHttp2(); + } +} diff --git a/instrumentation/armeria/armeria-1.3/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/armeria/v1_3/ArmeriaHttp2Test.java b/instrumentation/armeria/armeria-1.3/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/armeria/v1_3/ArmeriaHttp2Test.java new file mode 100644 index 000000000000..4351adc47c4c --- /dev/null +++ b/instrumentation/armeria/armeria-1.3/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/armeria/v1_3/ArmeriaHttp2Test.java @@ -0,0 +1,136 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.armeria.v1_3; + +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.satisfies; +import static org.assertj.core.api.Assertions.assertThat; + +import com.linecorp.armeria.client.WebClient; +import com.linecorp.armeria.common.AggregatedHttpResponse; +import com.linecorp.armeria.common.HttpResponse; +import com.linecorp.armeria.server.ServerBuilder; +import com.linecorp.armeria.testing.junit5.server.ServerExtension; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.semconv.ClientAttributes; +import io.opentelemetry.semconv.HttpAttributes; +import io.opentelemetry.semconv.NetworkAttributes; +import io.opentelemetry.semconv.ServerAttributes; +import io.opentelemetry.semconv.UrlAttributes; +import io.opentelemetry.semconv.UserAgentAttributes; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +class ArmeriaHttp2Test { + @RegisterExtension + static final AgentInstrumentationExtension testing = AgentInstrumentationExtension.create(); + + @RegisterExtension + static ServerExtension server1 = + new ServerExtension() { + @Override + protected void configure(ServerBuilder sb) { + sb.service("/", (ctx, req) -> HttpResponse.of("hello")); + } + }; + + @RegisterExtension + static ServerExtension server2 = + new ServerExtension() { + @Override + protected void configure(ServerBuilder sb) { + sb.service("/", (ctx, req) -> createWebClient(server1).get("/")); + } + }; + + private static WebClient createWebClient(ServerExtension server) { + return WebClient.builder(server.httpUri()).build(); + } + + @Test + void testHello() throws Exception { + // verify that spans are created and context is propagated + AggregatedHttpResponse result = createWebClient(server2).get("/").aggregate().get(); + assertThat(result.contentAscii()).isEqualTo("hello"); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("GET") + .hasKind(SpanKind.CLIENT) + .hasNoParent() + .hasAttributesSatisfyingExactly( + equalTo(UrlAttributes.URL_FULL, server2.httpUri() + "/"), + equalTo(HttpAttributes.HTTP_REQUEST_METHOD, "GET"), + equalTo(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, 200), + equalTo(NetworkAttributes.NETWORK_PROTOCOL_VERSION, "2"), + equalTo(ServerAttributes.SERVER_ADDRESS, "127.0.0.1"), + equalTo(ServerAttributes.SERVER_PORT, server2.httpPort()), + equalTo(NetworkAttributes.NETWORK_PEER_ADDRESS, "127.0.0.1"), + satisfies( + NetworkAttributes.NETWORK_PEER_PORT, + val -> val.isInstanceOf(Long.class))), + span -> + span.hasName("GET /") + .hasKind(SpanKind.SERVER) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo(UrlAttributes.URL_SCHEME, "http"), + equalTo(UrlAttributes.URL_PATH, "/"), + equalTo(HttpAttributes.HTTP_ROUTE, "/"), + equalTo(HttpAttributes.HTTP_REQUEST_METHOD, "GET"), + equalTo(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, 200), + equalTo(NetworkAttributes.NETWORK_PROTOCOL_VERSION, "2"), + equalTo(ClientAttributes.CLIENT_ADDRESS, "127.0.0.1"), + equalTo(ServerAttributes.SERVER_ADDRESS, "127.0.0.1"), + equalTo(ServerAttributes.SERVER_PORT, server2.httpPort()), + satisfies( + UserAgentAttributes.USER_AGENT_ORIGINAL, + val -> val.isInstanceOf(String.class)), + equalTo(NetworkAttributes.NETWORK_PEER_ADDRESS, "127.0.0.1"), + satisfies( + NetworkAttributes.NETWORK_PEER_PORT, + val -> val.isInstanceOf(Long.class))), + span -> + span.hasName("GET") + .hasKind(SpanKind.CLIENT) + .hasParent(trace.getSpan(1)) + .hasAttributesSatisfyingExactly( + equalTo(UrlAttributes.URL_FULL, server1.httpUri() + "/"), + equalTo(HttpAttributes.HTTP_REQUEST_METHOD, "GET"), + equalTo(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, 200), + equalTo(NetworkAttributes.NETWORK_PROTOCOL_VERSION, "2"), + equalTo(ServerAttributes.SERVER_ADDRESS, "127.0.0.1"), + equalTo(ServerAttributes.SERVER_PORT, server1.httpPort()), + equalTo(NetworkAttributes.NETWORK_PEER_ADDRESS, "127.0.0.1"), + satisfies( + NetworkAttributes.NETWORK_PEER_PORT, + val -> val.isInstanceOf(Long.class))), + span -> + span.hasName("GET /") + .hasKind(SpanKind.SERVER) + .hasParent(trace.getSpan(2)) + .hasAttributesSatisfyingExactly( + equalTo(UrlAttributes.URL_SCHEME, "http"), + equalTo(UrlAttributes.URL_PATH, "/"), + equalTo(HttpAttributes.HTTP_ROUTE, "/"), + equalTo(HttpAttributes.HTTP_REQUEST_METHOD, "GET"), + equalTo(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, 200), + equalTo(NetworkAttributes.NETWORK_PROTOCOL_VERSION, "2"), + equalTo(ClientAttributes.CLIENT_ADDRESS, "127.0.0.1"), + equalTo(ServerAttributes.SERVER_ADDRESS, "127.0.0.1"), + equalTo(ServerAttributes.SERVER_PORT, server1.httpPort()), + satisfies( + UserAgentAttributes.USER_AGENT_ORIGINAL, + val -> val.isInstanceOf(String.class)), + equalTo(NetworkAttributes.NETWORK_PEER_ADDRESS, "127.0.0.1"), + satisfies( + NetworkAttributes.NETWORK_PEER_PORT, + val -> val.isInstanceOf(Long.class))))); + } +} diff --git a/instrumentation/armeria-1.3/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/armeria/v1_3/ArmeriaHttpClientTest.java b/instrumentation/armeria/armeria-1.3/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/armeria/v1_3/ArmeriaHttpClientTest.java similarity index 100% rename from instrumentation/armeria-1.3/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/armeria/v1_3/ArmeriaHttpClientTest.java rename to instrumentation/armeria/armeria-1.3/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/armeria/v1_3/ArmeriaHttpClientTest.java diff --git a/instrumentation/armeria-1.3/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/armeria/v1_3/ArmeriaHttpServerTest.java b/instrumentation/armeria/armeria-1.3/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/armeria/v1_3/ArmeriaHttpServerTest.java similarity index 81% rename from instrumentation/armeria-1.3/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/armeria/v1_3/ArmeriaHttpServerTest.java rename to instrumentation/armeria/armeria-1.3/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/armeria/v1_3/ArmeriaHttpServerTest.java index 862bda592832..422fe0d0bf2e 100644 --- a/instrumentation/armeria-1.3/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/armeria/v1_3/ArmeriaHttpServerTest.java +++ b/instrumentation/armeria/armeria-1.3/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/armeria/v1_3/ArmeriaHttpServerTest.java @@ -29,5 +29,12 @@ protected void configure(HttpServerTestOptions options) { options.setHasResponseCustomizer( endpoint -> ServerEndpoint.NOT_FOUND != endpoint && ServerEndpoint.EXCEPTION != endpoint); options.setTestHttpPipelining(false); + // span for non-standard request is created by netty instrumentation when not running latest + // dep tests + if (Boolean.getBoolean("testLatestDeps")) { + options.disableTestNonStandardHttpMethod(); + } else { + options.setResponseCodeOnNonStandardHttpMethod(405); + } } } diff --git a/instrumentation/armeria-1.3/library/build.gradle.kts b/instrumentation/armeria/armeria-1.3/library/build.gradle.kts similarity index 68% rename from instrumentation/armeria-1.3/library/build.gradle.kts rename to instrumentation/armeria/armeria-1.3/library/build.gradle.kts index 2d38bf072d15..301d3b874baa 100644 --- a/instrumentation/armeria-1.3/library/build.gradle.kts +++ b/instrumentation/armeria/armeria-1.3/library/build.gradle.kts @@ -6,11 +6,11 @@ plugins { dependencies { library("com.linecorp.armeria:armeria:1.3.0") - testImplementation(project(":instrumentation:armeria-1.3:testing")) + testImplementation(project(":instrumentation:armeria:armeria-1.3:testing")) } tasks { - test { + withType().configureEach { systemProperty("testLatestDeps", findProperty("testLatestDeps") as Boolean) } } diff --git a/instrumentation/armeria-1.3/library/src/main/java/io/opentelemetry/instrumentation/armeria/v1_3/ArmeriaTelemetry.java b/instrumentation/armeria/armeria-1.3/library/src/main/java/io/opentelemetry/instrumentation/armeria/v1_3/ArmeriaTelemetry.java similarity index 100% rename from instrumentation/armeria-1.3/library/src/main/java/io/opentelemetry/instrumentation/armeria/v1_3/ArmeriaTelemetry.java rename to instrumentation/armeria/armeria-1.3/library/src/main/java/io/opentelemetry/instrumentation/armeria/v1_3/ArmeriaTelemetry.java diff --git a/instrumentation/armeria/armeria-1.3/library/src/main/java/io/opentelemetry/instrumentation/armeria/v1_3/ArmeriaTelemetryBuilder.java b/instrumentation/armeria/armeria-1.3/library/src/main/java/io/opentelemetry/instrumentation/armeria/v1_3/ArmeriaTelemetryBuilder.java new file mode 100644 index 000000000000..5d6a17eb6586 --- /dev/null +++ b/instrumentation/armeria/armeria-1.3/library/src/main/java/io/opentelemetry/instrumentation/armeria/v1_3/ArmeriaTelemetryBuilder.java @@ -0,0 +1,260 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.armeria.v1_3; + +import com.google.errorprone.annotations.CanIgnoreReturnValue; +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.incubator.builder.internal.DefaultHttpClientInstrumenterBuilder; +import io.opentelemetry.instrumentation.api.incubator.builder.internal.DefaultHttpServerInstrumenterBuilder; +import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.SpanStatusExtractor; +import io.opentelemetry.instrumentation.api.semconv.http.HttpClientAttributesExtractorBuilder; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerAttributesExtractorBuilder; +import io.opentelemetry.instrumentation.armeria.v1_3.internal.ArmeriaInstrumenterBuilderFactory; +import io.opentelemetry.instrumentation.armeria.v1_3.internal.ArmeriaInstrumenterBuilderUtil; +import java.util.List; +import java.util.Set; +import java.util.function.Function; + +public final class ArmeriaTelemetryBuilder { + + private final DefaultHttpClientInstrumenterBuilder + clientBuilder; + private final DefaultHttpServerInstrumenterBuilder + serverBuilder; + + static { + ArmeriaInstrumenterBuilderUtil.setClientBuilderExtractor( + ArmeriaTelemetryBuilder::getClientBuilder); + ArmeriaInstrumenterBuilderUtil.setServerBuilderExtractor( + ArmeriaTelemetryBuilder::getServerBuilder); + } + + ArmeriaTelemetryBuilder(OpenTelemetry openTelemetry) { + clientBuilder = ArmeriaInstrumenterBuilderFactory.getClientBuilder(openTelemetry); + serverBuilder = ArmeriaInstrumenterBuilderFactory.getServerBuilder(openTelemetry); + } + + /** + * Sets the status extractor for both client and server spans. + * + * @deprecated Use {@link #setClientStatusExtractor(Function)} or {@link + * #setServerStatusExtractor(Function)} instead. + */ + @Deprecated + @SuppressWarnings({"unchecked", "rawtypes"}) + @CanIgnoreReturnValue + public ArmeriaTelemetryBuilder setStatusExtractor( + Function< + SpanStatusExtractor, + ? extends SpanStatusExtractor> + statusExtractor) { + clientBuilder.setStatusExtractor((Function) statusExtractor); + serverBuilder.setStatusExtractor((Function) statusExtractor); + return this; + } + + /** Sets the status extractor for client spans. */ + @CanIgnoreReturnValue + public ArmeriaTelemetryBuilder setClientStatusExtractor( + Function< + SpanStatusExtractor, + ? extends SpanStatusExtractor> + statusExtractor) { + clientBuilder.setStatusExtractor(statusExtractor); + return this; + } + + /** Sets the status extractor for server spans. */ + @CanIgnoreReturnValue + public ArmeriaTelemetryBuilder setServerStatusExtractor( + Function< + SpanStatusExtractor, + ? extends SpanStatusExtractor> + statusExtractor) { + serverBuilder.setStatusExtractor(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. + * + * @deprecated Use {@link #addClientAttributeExtractor(AttributesExtractor)} or {@link + * #addServerAttributeExtractor(AttributesExtractor)} instead. + */ + @Deprecated + @CanIgnoreReturnValue + public ArmeriaTelemetryBuilder addAttributeExtractor( + AttributesExtractor attributesExtractor) { + clientBuilder.addAttributeExtractor(attributesExtractor); + serverBuilder.addAttributesExtractor(attributesExtractor); + return this; + } + + /** + * Adds an extra client-only {@link AttributesExtractor} to invoke to set attributes to + * instrumented items. The {@link AttributesExtractor} will be executed after all default + * extractors. + */ + @CanIgnoreReturnValue + public ArmeriaTelemetryBuilder addClientAttributeExtractor( + AttributesExtractor attributesExtractor) { + clientBuilder.addAttributeExtractor(attributesExtractor); + return this; + } + + /** + * Adds an extra server-only {@link AttributesExtractor} to invoke to set attributes to + * instrumented items. The {@link AttributesExtractor} will be executed after all default + * extractors. + */ + @CanIgnoreReturnValue + public ArmeriaTelemetryBuilder addServerAttributeExtractor( + AttributesExtractor attributesExtractor) { + serverBuilder.addAttributesExtractor(attributesExtractor); + return this; + } + + /** Sets the {@code peer.service} attribute for http client spans. */ + @CanIgnoreReturnValue + public ArmeriaTelemetryBuilder setPeerService(String peerService) { + clientBuilder.setPeerService(peerService); + return this; + } + + /** + * Configures the HTTP client request headers that will be captured as span attributes. + * + * @param requestHeaders A list of HTTP header names. + */ + @CanIgnoreReturnValue + public ArmeriaTelemetryBuilder setCapturedClientRequestHeaders(List requestHeaders) { + clientBuilder.setCapturedRequestHeaders(requestHeaders); + return this; + } + + /** + * Configures the HTTP client response headers that will be captured as span attributes. + * + * @param responseHeaders A list of HTTP header names. + */ + @CanIgnoreReturnValue + public ArmeriaTelemetryBuilder setCapturedClientResponseHeaders(List responseHeaders) { + clientBuilder.setCapturedResponseHeaders(responseHeaders); + return this; + } + + /** + * Configures the HTTP server request headers that will be captured as span attributes. + * + * @param requestHeaders A list of HTTP header names. + */ + @CanIgnoreReturnValue + public ArmeriaTelemetryBuilder setCapturedServerRequestHeaders(List requestHeaders) { + serverBuilder.setCapturedRequestHeaders(requestHeaders); + return this; + } + + /** + * Configures the HTTP server response headers that will be captured as span attributes. + * + * @param responseHeaders A list of HTTP header names. + */ + @CanIgnoreReturnValue + public ArmeriaTelemetryBuilder setCapturedServerResponseHeaders(List responseHeaders) { + serverBuilder.setCapturedResponseHeaders(responseHeaders); + return this; + } + + /** + * Configures the instrumentation to recognize an alternative set of HTTP request methods. + * + *

By default, this instrumentation defines "known" methods as the ones listed in RFC9110 and the PATCH + * method defined in RFC5789. + * + *

Note: calling this method overrides the default known method sets completely; it does + * not supplement it. + * + * @param knownMethods A set of recognized HTTP request methods. + * @see HttpClientAttributesExtractorBuilder#setKnownMethods(Set) + * @see HttpServerAttributesExtractorBuilder#setKnownMethods(Set) + */ + @CanIgnoreReturnValue + public ArmeriaTelemetryBuilder setKnownMethods(Set knownMethods) { + clientBuilder.setKnownMethods(knownMethods); + serverBuilder.setKnownMethods(knownMethods); + return this; + } + + /** + * Configures the instrumentation to emit experimental HTTP client metrics. + * + * @param emitExperimentalHttpClientMetrics {@code true} if the experimental HTTP client metrics + * are to be emitted. + */ + @CanIgnoreReturnValue + public ArmeriaTelemetryBuilder setEmitExperimentalHttpClientMetrics( + boolean emitExperimentalHttpClientMetrics) { + clientBuilder.setEmitExperimentalHttpClientMetrics(emitExperimentalHttpClientMetrics); + return this; + } + + /** + * Configures the instrumentation to emit experimental HTTP server metrics. + * + * @param emitExperimentalHttpServerMetrics {@code true} if the experimental HTTP server metrics + * are to be emitted. + */ + @CanIgnoreReturnValue + public ArmeriaTelemetryBuilder setEmitExperimentalHttpServerMetrics( + boolean emitExperimentalHttpServerMetrics) { + serverBuilder.setEmitExperimentalHttpServerMetrics(emitExperimentalHttpServerMetrics); + return this; + } + + /** Sets custom client {@link SpanNameExtractor} via transform function. */ + @CanIgnoreReturnValue + public ArmeriaTelemetryBuilder setClientSpanNameExtractor( + Function< + SpanNameExtractor, + ? extends SpanNameExtractor> + clientSpanNameExtractor) { + clientBuilder.setSpanNameExtractor(clientSpanNameExtractor); + return this; + } + + /** Sets custom server {@link SpanNameExtractor} via transform function. */ + @CanIgnoreReturnValue + public ArmeriaTelemetryBuilder setServerSpanNameExtractor( + Function< + SpanNameExtractor, + ? extends SpanNameExtractor> + serverSpanNameExtractor) { + serverBuilder.setSpanNameExtractor(serverSpanNameExtractor); + return this; + } + + public ArmeriaTelemetry build() { + return new ArmeriaTelemetry(clientBuilder.build(), serverBuilder.build()); + } + + private DefaultHttpClientInstrumenterBuilder + getClientBuilder() { + return clientBuilder; + } + + private DefaultHttpServerInstrumenterBuilder + getServerBuilder() { + return serverBuilder; + } +} diff --git a/instrumentation/armeria-1.3/library/src/main/java/io/opentelemetry/instrumentation/armeria/v1_3/OpenTelemetryClient.java b/instrumentation/armeria/armeria-1.3/library/src/main/java/io/opentelemetry/instrumentation/armeria/v1_3/OpenTelemetryClient.java similarity index 100% rename from instrumentation/armeria-1.3/library/src/main/java/io/opentelemetry/instrumentation/armeria/v1_3/OpenTelemetryClient.java rename to instrumentation/armeria/armeria-1.3/library/src/main/java/io/opentelemetry/instrumentation/armeria/v1_3/OpenTelemetryClient.java diff --git a/instrumentation/armeria-1.3/library/src/main/java/io/opentelemetry/instrumentation/armeria/v1_3/OpenTelemetryService.java b/instrumentation/armeria/armeria-1.3/library/src/main/java/io/opentelemetry/instrumentation/armeria/v1_3/OpenTelemetryService.java similarity index 100% rename from instrumentation/armeria-1.3/library/src/main/java/io/opentelemetry/instrumentation/armeria/v1_3/OpenTelemetryService.java rename to instrumentation/armeria/armeria-1.3/library/src/main/java/io/opentelemetry/instrumentation/armeria/v1_3/OpenTelemetryService.java diff --git a/instrumentation/armeria/armeria-1.3/library/src/main/java/io/opentelemetry/instrumentation/armeria/v1_3/internal/ArmeriaHttpClientAttributesGetter.java b/instrumentation/armeria/armeria-1.3/library/src/main/java/io/opentelemetry/instrumentation/armeria/v1_3/internal/ArmeriaHttpClientAttributesGetter.java new file mode 100644 index 000000000000..29dfa4aca00b --- /dev/null +++ b/instrumentation/armeria/armeria-1.3/library/src/main/java/io/opentelemetry/instrumentation/armeria/v1_3/internal/ArmeriaHttpClientAttributesGetter.java @@ -0,0 +1,161 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.armeria.v1_3.internal; + +import com.linecorp.armeria.client.ClientRequestContext; +import com.linecorp.armeria.common.HttpRequest; +import com.linecorp.armeria.common.HttpStatus; +import com.linecorp.armeria.common.SessionProtocol; +import com.linecorp.armeria.common.logging.RequestLog; +import io.opentelemetry.instrumentation.api.semconv.http.HttpClientAttributesGetter; +import java.lang.reflect.Method; +import java.net.InetSocketAddress; +import java.util.List; +import javax.annotation.Nullable; + +enum ArmeriaHttpClientAttributesGetter + implements HttpClientAttributesGetter { + INSTANCE; + + private static final ClassValue authorityMethodCache = + new ClassValue() { + @Nullable + @Override + protected Method computeValue(Class type) { + try { + return type.getMethod("authority"); + } catch (NoSuchMethodException e) { + return null; + } + } + }; + + @Override + public String getHttpRequestMethod(ClientRequestContext ctx) { + return ctx.method().name(); + } + + @Override + public String getUrlFull(ClientRequestContext ctx) { + HttpRequest request = request(ctx); + StringBuilder uri = new StringBuilder(); + String scheme = request.scheme(); + if (scheme == null) { + String name = ctx.sessionProtocol().uriText(); + if ("http".equals(name) || "https".equals(name)) { + scheme = name; + } + } + if (scheme != null) { + uri.append(scheme).append("://"); + } + String authority = authority(ctx); + if (authority != null) { + uri.append(authority); + } + uri.append(request.path()); + return uri.toString(); + } + + @Override + public List getHttpRequestHeader(ClientRequestContext ctx, String name) { + return request(ctx).headers().getAll(name); + } + + @Override + @Nullable + public Integer getHttpResponseStatusCode( + ClientRequestContext ctx, RequestLog requestLog, @Nullable Throwable error) { + HttpStatus status = requestLog.responseHeaders().status(); + if (!status.equals(HttpStatus.UNKNOWN)) { + return status.code(); + } + return null; + } + + @Override + public List getHttpResponseHeader( + ClientRequestContext ctx, RequestLog requestLog, String name) { + return requestLog.responseHeaders().getAll(name); + } + + @Override + public String getNetworkProtocolName(ClientRequestContext ctx, @Nullable RequestLog requestLog) { + return "http"; + } + + @Override + public String getNetworkProtocolVersion( + ClientRequestContext ctx, @Nullable RequestLog requestLog) { + SessionProtocol protocol = + requestLog != null ? requestLog.sessionProtocol() : ctx.sessionProtocol(); + return protocol.isMultiplex() ? "2" : "1.1"; + } + + @Nullable + @Override + public String getServerAddress(ClientRequestContext ctx) { + String authority = authority(ctx); + if (authority == null) { + return null; + } + int separatorPos = authority.indexOf(':'); + return separatorPos == -1 ? authority : authority.substring(0, separatorPos); + } + + @Nullable + @Override + public Integer getServerPort(ClientRequestContext ctx) { + String authority = authority(ctx); + if (authority == null) { + return null; + } + int separatorPos = authority.indexOf(':'); + if (separatorPos == -1) { + return null; + } + try { + return Integer.parseInt(authority.substring(separatorPos + 1)); + } catch (NumberFormatException e) { + return null; + } + } + + @Override + @Nullable + public InetSocketAddress getNetworkPeerInetSocketAddress( + ClientRequestContext ctx, @Nullable RequestLog requestLog) { + return RequestContextAccess.remoteAddress(ctx); + } + + @Nullable + private static String authority(ClientRequestContext ctx) { + // newer armeria versions expose authority through DefaultClientRequestContext#authority + // we are using this method as it provides default values based on endpoint + // in older versions armeria wraps the request, and we can get the same default values through + // the request + Method method = authorityMethodCache.get(ctx.getClass()); + if (method != null) { + try { + return (String) method.invoke(ctx); + } catch (Exception e) { + return null; + } + } + + HttpRequest request = request(ctx); + return request.authority(); + } + + private static HttpRequest request(ClientRequestContext 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/instrumentation/armeria/armeria-1.3/library/src/main/java/io/opentelemetry/instrumentation/armeria/v1_3/internal/ArmeriaHttpServerAttributesGetter.java b/instrumentation/armeria/armeria-1.3/library/src/main/java/io/opentelemetry/instrumentation/armeria/v1_3/internal/ArmeriaHttpServerAttributesGetter.java new file mode 100644 index 000000000000..5cef09ca7fd9 --- /dev/null +++ b/instrumentation/armeria/armeria-1.3/library/src/main/java/io/opentelemetry/instrumentation/armeria/v1_3/internal/ArmeriaHttpServerAttributesGetter.java @@ -0,0 +1,110 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.armeria.v1_3.internal; + +import com.linecorp.armeria.common.HttpRequest; +import com.linecorp.armeria.common.HttpStatus; +import com.linecorp.armeria.common.SessionProtocol; +import com.linecorp.armeria.common.logging.RequestLog; +import com.linecorp.armeria.server.ServiceRequestContext; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerAttributesGetter; +import java.net.InetSocketAddress; +import java.util.List; +import javax.annotation.Nullable; + +enum ArmeriaHttpServerAttributesGetter + implements HttpServerAttributesGetter { + INSTANCE; + + @Override + public String getHttpRequestMethod(ServiceRequestContext ctx) { + return ctx.method().name(); + } + + @Override + @Nullable + public String getUrlScheme(ServiceRequestContext ctx) { + return request(ctx).scheme(); + } + + @Override + public String getUrlPath(ServiceRequestContext ctx) { + String fullPath = request(ctx).path(); + int separatorPos = fullPath.indexOf('?'); + return separatorPos == -1 ? fullPath : fullPath.substring(0, separatorPos); + } + + @Nullable + @Override + public String getUrlQuery(ServiceRequestContext ctx) { + String fullPath = request(ctx).path(); + int separatorPos = fullPath.indexOf('?'); + return separatorPos == -1 ? null : fullPath.substring(separatorPos + 1); + } + + @Override + public List getHttpRequestHeader(ServiceRequestContext ctx, String name) { + return request(ctx).headers().getAll(name); + } + + @Override + @Nullable + public Integer getHttpResponseStatusCode( + ServiceRequestContext ctx, RequestLog requestLog, @Nullable Throwable error) { + HttpStatus status = requestLog.responseHeaders().status(); + if (!status.equals(HttpStatus.UNKNOWN)) { + return status.code(); + } + return null; + } + + @Override + public List getHttpResponseHeader( + ServiceRequestContext ctx, RequestLog requestLog, String name) { + return requestLog.responseHeaders().getAll(name); + } + + @Override + @Nullable + public String getHttpRoute(ServiceRequestContext ctx) { + return ctx.config().route().patternString(); + } + + @Override + public String getNetworkProtocolName(ServiceRequestContext ctx, @Nullable RequestLog requestLog) { + return "http"; + } + + @Override + public String getNetworkProtocolVersion( + ServiceRequestContext ctx, @Nullable RequestLog requestLog) { + SessionProtocol protocol = ctx.sessionProtocol(); + return protocol.isMultiplex() ? "2" : "1.1"; + } + + @Override + @Nullable + public InetSocketAddress getNetworkPeerInetSocketAddress( + ServiceRequestContext ctx, @Nullable RequestLog requestLog) { + return RequestContextAccess.remoteAddress(ctx); + } + + @Nullable + @Override + public InetSocketAddress getNetworkLocalInetSocketAddress( + ServiceRequestContext ctx, @Nullable RequestLog log) { + return RequestContextAccess.localAddress(ctx); + } + + private static HttpRequest request(ServiceRequestContext 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/instrumentation/armeria/armeria-1.3/library/src/main/java/io/opentelemetry/instrumentation/armeria/v1_3/internal/ArmeriaInstrumenterBuilderFactory.java b/instrumentation/armeria/armeria-1.3/library/src/main/java/io/opentelemetry/instrumentation/armeria/v1_3/internal/ArmeriaInstrumenterBuilderFactory.java new file mode 100644 index 000000000000..07a28da6da85 --- /dev/null +++ b/instrumentation/armeria/armeria-1.3/library/src/main/java/io/opentelemetry/instrumentation/armeria/v1_3/internal/ArmeriaInstrumenterBuilderFactory.java @@ -0,0 +1,37 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.armeria.v1_3.internal; + +import com.linecorp.armeria.client.ClientRequestContext; +import com.linecorp.armeria.common.logging.RequestLog; +import com.linecorp.armeria.server.ServiceRequestContext; +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.instrumentation.api.incubator.builder.internal.DefaultHttpClientInstrumenterBuilder; +import io.opentelemetry.instrumentation.api.incubator.builder.internal.DefaultHttpServerInstrumenterBuilder; + +/** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time. + */ +public class ArmeriaInstrumenterBuilderFactory { + private ArmeriaInstrumenterBuilderFactory() {} + + private static final String INSTRUMENTATION_NAME = "io.opentelemetry.armeria-1.3"; + + public static DefaultHttpServerInstrumenterBuilder + getServerBuilder(OpenTelemetry openTelemetry) { + return new DefaultHttpServerInstrumenterBuilder<>( + INSTRUMENTATION_NAME, openTelemetry, ArmeriaHttpServerAttributesGetter.INSTANCE) + .setHeaderGetter(RequestContextGetter.INSTANCE); + } + + public static DefaultHttpClientInstrumenterBuilder + getClientBuilder(OpenTelemetry openTelemetry) { + return new DefaultHttpClientInstrumenterBuilder<>( + INSTRUMENTATION_NAME, openTelemetry, ArmeriaHttpClientAttributesGetter.INSTANCE) + .setHeaderSetter(ClientRequestContextSetter.INSTANCE); + } +} diff --git a/instrumentation/armeria/armeria-1.3/library/src/main/java/io/opentelemetry/instrumentation/armeria/v1_3/internal/ArmeriaInstrumenterBuilderUtil.java b/instrumentation/armeria/armeria-1.3/library/src/main/java/io/opentelemetry/instrumentation/armeria/v1_3/internal/ArmeriaInstrumenterBuilderUtil.java new file mode 100644 index 000000000000..a9a7d8b43b09 --- /dev/null +++ b/instrumentation/armeria/armeria-1.3/library/src/main/java/io/opentelemetry/instrumentation/armeria/v1_3/internal/ArmeriaInstrumenterBuilderUtil.java @@ -0,0 +1,67 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.armeria.v1_3.internal; + +import com.linecorp.armeria.client.ClientRequestContext; +import com.linecorp.armeria.common.logging.RequestLog; +import com.linecorp.armeria.server.ServiceRequestContext; +import io.opentelemetry.instrumentation.api.incubator.builder.internal.DefaultHttpClientInstrumenterBuilder; +import io.opentelemetry.instrumentation.api.incubator.builder.internal.DefaultHttpServerInstrumenterBuilder; +import io.opentelemetry.instrumentation.armeria.v1_3.ArmeriaTelemetryBuilder; +import java.util.function.Function; +import javax.annotation.Nullable; + +/** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time. + */ +public class ArmeriaInstrumenterBuilderUtil { + private ArmeriaInstrumenterBuilderUtil() {} + + @Nullable + private static Function< + ArmeriaTelemetryBuilder, + DefaultHttpClientInstrumenterBuilder> + clientBuilderExtractor; + + @Nullable + private static Function< + ArmeriaTelemetryBuilder, + DefaultHttpServerInstrumenterBuilder> + serverBuilderExtractor; + + @Nullable + public static Function< + ArmeriaTelemetryBuilder, + DefaultHttpClientInstrumenterBuilder> + getClientBuilderExtractor() { + return clientBuilderExtractor; + } + + public static void setClientBuilderExtractor( + Function< + ArmeriaTelemetryBuilder, + DefaultHttpClientInstrumenterBuilder> + clientBuilderExtractor) { + ArmeriaInstrumenterBuilderUtil.clientBuilderExtractor = clientBuilderExtractor; + } + + @Nullable + public static Function< + ArmeriaTelemetryBuilder, + DefaultHttpServerInstrumenterBuilder> + getServerBuilderExtractor() { + return serverBuilderExtractor; + } + + public static void setServerBuilderExtractor( + Function< + ArmeriaTelemetryBuilder, + DefaultHttpServerInstrumenterBuilder> + serverBuilderExtractor) { + ArmeriaInstrumenterBuilderUtil.serverBuilderExtractor = serverBuilderExtractor; + } +} diff --git a/instrumentation/armeria-1.3/library/src/main/java/io/opentelemetry/instrumentation/armeria/v1_3/ClientRequestContextSetter.java b/instrumentation/armeria/armeria-1.3/library/src/main/java/io/opentelemetry/instrumentation/armeria/v1_3/internal/ClientRequestContextSetter.java similarity index 89% rename from instrumentation/armeria-1.3/library/src/main/java/io/opentelemetry/instrumentation/armeria/v1_3/ClientRequestContextSetter.java rename to instrumentation/armeria/armeria-1.3/library/src/main/java/io/opentelemetry/instrumentation/armeria/v1_3/internal/ClientRequestContextSetter.java index 91ce53e54805..1a225fd7108b 100644 --- a/instrumentation/armeria-1.3/library/src/main/java/io/opentelemetry/instrumentation/armeria/v1_3/ClientRequestContextSetter.java +++ b/instrumentation/armeria/armeria-1.3/library/src/main/java/io/opentelemetry/instrumentation/armeria/v1_3/internal/ClientRequestContextSetter.java @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.instrumentation.armeria.v1_3; +package io.opentelemetry.instrumentation.armeria.v1_3.internal; import com.linecorp.armeria.client.ClientRequestContext; import io.opentelemetry.context.propagation.TextMapSetter; diff --git a/instrumentation/armeria-1.3/library/src/main/java/io/opentelemetry/instrumentation/armeria/v1_3/internal/RequestContextAccess.java b/instrumentation/armeria/armeria-1.3/library/src/main/java/io/opentelemetry/instrumentation/armeria/v1_3/internal/RequestContextAccess.java similarity index 92% rename from instrumentation/armeria-1.3/library/src/main/java/io/opentelemetry/instrumentation/armeria/v1_3/internal/RequestContextAccess.java rename to instrumentation/armeria/armeria-1.3/library/src/main/java/io/opentelemetry/instrumentation/armeria/v1_3/internal/RequestContextAccess.java index acaf7fd9cfa0..1a7830758fd6 100644 --- a/instrumentation/armeria-1.3/library/src/main/java/io/opentelemetry/instrumentation/armeria/v1_3/internal/RequestContextAccess.java +++ b/instrumentation/armeria/armeria-1.3/library/src/main/java/io/opentelemetry/instrumentation/armeria/v1_3/internal/RequestContextAccess.java @@ -13,11 +13,7 @@ import java.net.SocketAddress; import javax.annotation.Nullable; -/** - * This class is internal and is hence not for public use. Its APIs are unstable and can change at - * any time. - */ -public final class RequestContextAccess { +final class RequestContextAccess { @Nullable private static final MethodHandle remoteAddress = findAccessorOrNull("remoteAddress"); @Nullable private static final MethodHandle localAddress = findAccessorOrNull("localAddress"); diff --git a/instrumentation/armeria-1.3/library/src/main/java/io/opentelemetry/instrumentation/armeria/v1_3/RequestContextGetter.java b/instrumentation/armeria/armeria-1.3/library/src/main/java/io/opentelemetry/instrumentation/armeria/v1_3/internal/RequestContextGetter.java similarity index 93% rename from instrumentation/armeria-1.3/library/src/main/java/io/opentelemetry/instrumentation/armeria/v1_3/RequestContextGetter.java rename to instrumentation/armeria/armeria-1.3/library/src/main/java/io/opentelemetry/instrumentation/armeria/v1_3/internal/RequestContextGetter.java index 5163333591a0..5cae10de6578 100644 --- a/instrumentation/armeria-1.3/library/src/main/java/io/opentelemetry/instrumentation/armeria/v1_3/RequestContextGetter.java +++ b/instrumentation/armeria/armeria-1.3/library/src/main/java/io/opentelemetry/instrumentation/armeria/v1_3/internal/RequestContextGetter.java @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.instrumentation.armeria.v1_3; +package io.opentelemetry.instrumentation.armeria.v1_3.internal; import com.linecorp.armeria.server.ServiceRequestContext; import io.netty.util.AsciiString; diff --git a/instrumentation/armeria/armeria-1.3/library/src/test/java/io/opentelemetry/instrumentation/armeria/v1_3/ArmeriaHttp2ServerTest.java b/instrumentation/armeria/armeria-1.3/library/src/test/java/io/opentelemetry/instrumentation/armeria/v1_3/ArmeriaHttp2ServerTest.java new file mode 100644 index 000000000000..e9a46b064ac2 --- /dev/null +++ b/instrumentation/armeria/armeria-1.3/library/src/test/java/io/opentelemetry/instrumentation/armeria/v1_3/ArmeriaHttp2ServerTest.java @@ -0,0 +1,17 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.armeria.v1_3; + +import io.opentelemetry.instrumentation.testing.junit.http.HttpServerTestOptions; + +class ArmeriaHttp2ServerTest extends ArmeriaHttpServerTest { + + @Override + protected void configure(HttpServerTestOptions options) { + super.configure(options); + options.useHttp2(); + } +} diff --git a/instrumentation/armeria-1.3/library/src/test/java/io/opentelemetry/instrumentation/armeria/v1_3/ArmeriaHttpClientTest.java b/instrumentation/armeria/armeria-1.3/library/src/test/java/io/opentelemetry/instrumentation/armeria/v1_3/ArmeriaHttpClientTest.java similarity index 100% rename from instrumentation/armeria-1.3/library/src/test/java/io/opentelemetry/instrumentation/armeria/v1_3/ArmeriaHttpClientTest.java rename to instrumentation/armeria/armeria-1.3/library/src/test/java/io/opentelemetry/instrumentation/armeria/v1_3/ArmeriaHttpClientTest.java diff --git a/instrumentation/armeria-1.3/library/src/test/java/io/opentelemetry/instrumentation/armeria/v1_3/ArmeriaHttpServerTest.java b/instrumentation/armeria/armeria-1.3/library/src/test/java/io/opentelemetry/instrumentation/armeria/v1_3/ArmeriaHttpServerTest.java similarity index 80% rename from instrumentation/armeria-1.3/library/src/test/java/io/opentelemetry/instrumentation/armeria/v1_3/ArmeriaHttpServerTest.java rename to instrumentation/armeria/armeria-1.3/library/src/test/java/io/opentelemetry/instrumentation/armeria/v1_3/ArmeriaHttpServerTest.java index 0faf2f0ec62f..283261595fa8 100644 --- a/instrumentation/armeria-1.3/library/src/test/java/io/opentelemetry/instrumentation/armeria/v1_3/ArmeriaHttpServerTest.java +++ b/instrumentation/armeria/armeria-1.3/library/src/test/java/io/opentelemetry/instrumentation/armeria/v1_3/ArmeriaHttpServerTest.java @@ -9,6 +9,7 @@ import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpServerTest; import io.opentelemetry.instrumentation.testing.junit.http.HttpServerInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.http.HttpServerTestOptions; import java.util.Collections; import org.junit.jupiter.api.extension.RegisterExtension; @@ -28,4 +29,11 @@ protected ServerBuilder configureServer(ServerBuilder sb) { .build() .newServiceDecorator()); } + + @Override + protected void configure(HttpServerTestOptions options) { + super.configure(options); + // library instrumentation does not create a span at all + options.disableTestNonStandardHttpMethod(); + } } diff --git a/instrumentation/armeria-1.3/testing/build.gradle.kts b/instrumentation/armeria/armeria-1.3/testing/build.gradle.kts similarity index 100% rename from instrumentation/armeria-1.3/testing/build.gradle.kts rename to instrumentation/armeria/armeria-1.3/testing/build.gradle.kts diff --git a/instrumentation/armeria-1.3/testing/src/main/java/io/opentelemetry/instrumentation/armeria/v1_3/AbstractArmeriaHttpClientTest.java b/instrumentation/armeria/armeria-1.3/testing/src/main/java/io/opentelemetry/instrumentation/armeria/v1_3/AbstractArmeriaHttpClientTest.java similarity index 89% rename from instrumentation/armeria-1.3/testing/src/main/java/io/opentelemetry/instrumentation/armeria/v1_3/AbstractArmeriaHttpClientTest.java rename to instrumentation/armeria/armeria-1.3/testing/src/main/java/io/opentelemetry/instrumentation/armeria/v1_3/AbstractArmeriaHttpClientTest.java index 74fca95c5d41..0e6cf264c3a1 100644 --- a/instrumentation/armeria-1.3/testing/src/main/java/io/opentelemetry/instrumentation/armeria/v1_3/AbstractArmeriaHttpClientTest.java +++ b/instrumentation/armeria/armeria-1.3/testing/src/main/java/io/opentelemetry/instrumentation/armeria/v1_3/AbstractArmeriaHttpClientTest.java @@ -109,6 +109,18 @@ protected void configure(HttpClientTestOptions.Builder optionsBuilder) { optionsBuilder.disableTestRedirects(); // armeria requests can't be reused optionsBuilder.disableTestReusedRequest(); + // can only use methods from HttpMethod enum + optionsBuilder.disableTestNonStandardHttpMethod(); + optionsBuilder.spanEndsAfterBody(); + // armeria uses http/2 + optionsBuilder.setHttpProtocolVersion( + uri -> { + String uriString = uri.toString(); + if (uriString.equals("http://localhost:61/") || uriString.equals("https://192.0.2.1/")) { + return "1.1"; + } + return "2"; + }); } @Test diff --git a/instrumentation/armeria-1.3/testing/src/main/java/io/opentelemetry/instrumentation/armeria/v1_3/AbstractArmeriaHttpServerTest.java b/instrumentation/armeria/armeria-1.3/testing/src/main/java/io/opentelemetry/instrumentation/armeria/v1_3/AbstractArmeriaHttpServerTest.java similarity index 98% rename from instrumentation/armeria-1.3/testing/src/main/java/io/opentelemetry/instrumentation/armeria/v1_3/AbstractArmeriaHttpServerTest.java rename to instrumentation/armeria/armeria-1.3/testing/src/main/java/io/opentelemetry/instrumentation/armeria/v1_3/AbstractArmeriaHttpServerTest.java index 7c684f4e8040..4f8e008ba6ef 100644 --- a/instrumentation/armeria-1.3/testing/src/main/java/io/opentelemetry/instrumentation/armeria/v1_3/AbstractArmeriaHttpServerTest.java +++ b/instrumentation/armeria/armeria-1.3/testing/src/main/java/io/opentelemetry/instrumentation/armeria/v1_3/AbstractArmeriaHttpServerTest.java @@ -178,13 +178,13 @@ protected void stopServer(Server server) { @Override protected void configure(HttpServerTestOptions options) { options.setExpectedHttpRoute( - endpoint -> { + (endpoint, method) -> { if (endpoint == ServerEndpoint.NOT_FOUND) { // TODO(anuraaga): Revisit this when applying instrumenters to more libraries, Armeria // currently reports '/*' which is a fallback route. return "/*"; } - return expectedHttpRoute(endpoint); + return expectedHttpRoute(endpoint, method); }); options.setTestPathParam(true); diff --git a/instrumentation/armeria/armeria-grpc-1.14/javaagent/build.gradle.kts b/instrumentation/armeria/armeria-grpc-1.14/javaagent/build.gradle.kts new file mode 100644 index 000000000000..65a2309b0f69 --- /dev/null +++ b/instrumentation/armeria/armeria-grpc-1.14/javaagent/build.gradle.kts @@ -0,0 +1,23 @@ +plugins { + id("otel.javaagent-instrumentation") +} + +muzzle { + pass { + group.set("com.linecorp.armeria") + module.set("armeria-grpc") + versions.set("[1.14.0,)") + assertInverse.set(true) + } +} + +dependencies { + library("com.linecorp.armeria:armeria-grpc:1.14.0") + implementation(project(":instrumentation:grpc-1.6:library")) + + testInstrumentation(project(":instrumentation:netty:netty-4.1:javaagent")) + testInstrumentation(project(":instrumentation:grpc-1.6:javaagent")) + + testImplementation(project(":instrumentation:grpc-1.6:testing")) + testLibrary("com.linecorp.armeria:armeria-junit5:1.14.0") +} diff --git a/instrumentation/armeria/armeria-grpc-1.14/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/armeria/grpc/v1_14/ArmeriaGrpcClientBuilderInstrumentation.java b/instrumentation/armeria/armeria-grpc-1.14/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/armeria/grpc/v1_14/ArmeriaGrpcClientBuilderInstrumentation.java new file mode 100644 index 000000000000..62364832dfa5 --- /dev/null +++ b/instrumentation/armeria/armeria-grpc-1.14/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/armeria/grpc/v1_14/ArmeriaGrpcClientBuilderInstrumentation.java @@ -0,0 +1,43 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.armeria.grpc.v1_14; + +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.client.grpc.GrpcClientBuilder; +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.instrumentation.grpc.v1_6.GrpcTelemetry; +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 ArmeriaGrpcClientBuilderInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("com.linecorp.armeria.client.grpc.GrpcClientBuilder"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod().and(isPublic()).and(named("build")), + ArmeriaGrpcClientBuilderInstrumentation.class.getName() + "$BuildAdvice"); + } + + @SuppressWarnings("unused") + public static class BuildAdvice { + + @Advice.OnMethodEnter + public static void onEnter(@Advice.This GrpcClientBuilder builder) { + builder.intercept(GrpcTelemetry.create(GlobalOpenTelemetry.get()).newClientInterceptor()); + } + } +} diff --git a/instrumentation/armeria/armeria-grpc-1.14/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/armeria/grpc/v1_14/ArmeriaGrpcInstrumentationModule.java b/instrumentation/armeria/armeria-grpc-1.14/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/armeria/grpc/v1_14/ArmeriaGrpcInstrumentationModule.java new file mode 100644 index 000000000000..5eb6733689cc --- /dev/null +++ b/instrumentation/armeria/armeria-grpc-1.14/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/armeria/grpc/v1_14/ArmeriaGrpcInstrumentationModule.java @@ -0,0 +1,27 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.armeria.grpc.v1_14; + +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 ArmeriaGrpcInstrumentationModule extends InstrumentationModule { + public ArmeriaGrpcInstrumentationModule() { + super("armeria", "armeria-1.14", "armeria-grpc-1.14"); + } + + @Override + public List typeInstrumentations() { + return asList( + new ArmeriaGrpcClientBuilderInstrumentation(), + new ArmeriaGrpcServiceBuilderInstrumentation()); + } +} diff --git a/instrumentation/armeria/armeria-grpc-1.14/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/armeria/grpc/v1_14/ArmeriaGrpcServiceBuilderInstrumentation.java b/instrumentation/armeria/armeria-grpc-1.14/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/armeria/grpc/v1_14/ArmeriaGrpcServiceBuilderInstrumentation.java new file mode 100644 index 000000000000..7d05757f5c4c --- /dev/null +++ b/instrumentation/armeria/armeria-grpc-1.14/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/armeria/grpc/v1_14/ArmeriaGrpcServiceBuilderInstrumentation.java @@ -0,0 +1,42 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.armeria.grpc.v1_14; + +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.grpc.GrpcServiceBuilder; +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.instrumentation.grpc.v1_6.GrpcTelemetry; +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 ArmeriaGrpcServiceBuilderInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher typeMatcher() { + return named("com.linecorp.armeria.server.grpc.GrpcServiceBuilder"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod().and(isPublic()).and(named("build")), + ArmeriaGrpcServiceBuilderInstrumentation.class.getName() + "$BuildAdvice"); + } + + @SuppressWarnings("unused") + public static class BuildAdvice { + + @Advice.OnMethodEnter + public static void onEnter(@Advice.This GrpcServiceBuilder builder) { + builder.intercept(GrpcTelemetry.create(GlobalOpenTelemetry.get()).newServerInterceptor()); + } + } +} diff --git a/instrumentation/armeria/armeria-grpc-1.14/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/armeria/grpc/v1_14/ArmeriaGrpcTest.java b/instrumentation/armeria/armeria-grpc-1.14/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/armeria/grpc/v1_14/ArmeriaGrpcTest.java new file mode 100644 index 000000000000..2402365b9efa --- /dev/null +++ b/instrumentation/armeria/armeria-grpc-1.14/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/armeria/grpc/v1_14/ArmeriaGrpcTest.java @@ -0,0 +1,127 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.armeria.grpc.v1_14; + +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; +import static org.assertj.core.api.Assertions.assertThat; + +import com.linecorp.armeria.client.grpc.GrpcClients; +import com.linecorp.armeria.server.ServerBuilder; +import com.linecorp.armeria.server.grpc.GrpcService; +import com.linecorp.armeria.testing.junit5.server.ServerExtension; +import example.GreeterGrpc; +import example.Helloworld; +import io.grpc.Status; +import io.grpc.stub.StreamObserver; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.semconv.ServerAttributes; +import io.opentelemetry.semconv.incubating.MessageIncubatingAttributes; +import io.opentelemetry.semconv.incubating.RpcIncubatingAttributes; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +class ArmeriaGrpcTest { + + @RegisterExtension + static final AgentInstrumentationExtension testing = AgentInstrumentationExtension.create(); + + @RegisterExtension + static final ServerExtension server = + new ServerExtension() { + @Override + protected void configure(ServerBuilder sb) throws Exception { + sb.service( + GrpcService.builder() + .addService( + new GreeterGrpc.GreeterImplBase() { + @Override + public void sayHello( + Helloworld.Request request, + StreamObserver responseObserver) { + responseObserver.onNext( + Helloworld.Response.newBuilder() + .setMessage("Hello " + request.getName()) + .build()); + responseObserver.onCompleted(); + } + }) + .build()); + } + }; + + @Test + void grpcInstrumentation() { + GreeterGrpc.GreeterBlockingStub client = + GrpcClients.builder(server.httpUri()).build(GreeterGrpc.GreeterBlockingStub.class); + + Helloworld.Response response = + testing.runWithSpan( + "parent", + () -> client.sayHello(Helloworld.Request.newBuilder().setName("test").build())); + + assertThat(response.getMessage()).isEqualTo("Hello test"); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(), + span -> + span.hasName("example.Greeter/SayHello") + .hasKind(SpanKind.CLIENT) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo(RpcIncubatingAttributes.RPC_SYSTEM, "grpc"), + equalTo(RpcIncubatingAttributes.RPC_SERVICE, "example.Greeter"), + equalTo(RpcIncubatingAttributes.RPC_METHOD, "SayHello"), + equalTo( + RpcIncubatingAttributes.RPC_GRPC_STATUS_CODE, + (long) Status.Code.OK.value()), + equalTo(ServerAttributes.SERVER_ADDRESS, "127.0.0.1"), + equalTo(ServerAttributes.SERVER_PORT, (long) server.httpPort())) + .hasEventsSatisfyingExactly( + event -> + event + .hasName("message") + .hasAttributesSatisfyingExactly( + equalTo(MessageIncubatingAttributes.MESSAGE_TYPE, "SENT"), + equalTo(MessageIncubatingAttributes.MESSAGE_ID, 1L)), + event -> + event + .hasName("message") + .hasAttributesSatisfyingExactly( + equalTo( + MessageIncubatingAttributes.MESSAGE_TYPE, "RECEIVED"), + equalTo(MessageIncubatingAttributes.MESSAGE_ID, 2L))), + span -> + span.hasName("example.Greeter/SayHello") + .hasKind(SpanKind.SERVER) + .hasParent(trace.getSpan(1)) + .hasAttributesSatisfyingExactly( + equalTo(RpcIncubatingAttributes.RPC_SYSTEM, "grpc"), + equalTo(RpcIncubatingAttributes.RPC_SERVICE, "example.Greeter"), + equalTo(RpcIncubatingAttributes.RPC_METHOD, "SayHello"), + equalTo( + RpcIncubatingAttributes.RPC_GRPC_STATUS_CODE, + (long) Status.Code.OK.value()), + equalTo(ServerAttributes.SERVER_ADDRESS, "127.0.0.1"), + equalTo(ServerAttributes.SERVER_PORT, server.httpPort())) + .hasEventsSatisfyingExactly( + event -> + event + .hasName("message") + .hasAttributesSatisfyingExactly( + equalTo( + MessageIncubatingAttributes.MESSAGE_TYPE, "RECEIVED"), + equalTo(MessageIncubatingAttributes.MESSAGE_ID, 1L)), + event -> + event + .hasName("message") + .hasAttributesSatisfyingExactly( + equalTo(MessageIncubatingAttributes.MESSAGE_TYPE, "SENT"), + equalTo(MessageIncubatingAttributes.MESSAGE_ID, 2L))))); + } +} diff --git a/instrumentation/async-http-client/async-http-client-1.9/javaagent/build.gradle.kts b/instrumentation/async-http-client/async-http-client-1.9/javaagent/build.gradle.kts index bf875f4f4671..47e8d3d6a87b 100644 --- a/instrumentation/async-http-client/async-http-client-1.9/javaagent/build.gradle.kts +++ b/instrumentation/async-http-client/async-http-client-1.9/javaagent/build.gradle.kts @@ -20,10 +20,12 @@ dependencies { testInstrumentation(project(":instrumentation:netty:netty-3.8:javaagent")) } -tasks.withType().configureEach { - // required on jdk17 - jvmArgs("--add-exports=java.base/sun.security.util=ALL-UNNAMED") - jvmArgs("-XX:+IgnoreUnrecognizedVMOptions") +tasks { + withType().configureEach { + // required on jdk17 + jvmArgs("--add-exports=java.base/sun.security.util=ALL-UNNAMED") + jvmArgs("-XX:+IgnoreUnrecognizedVMOptions") - systemProperty("testLatestDeps", findProperty("testLatestDeps") as Boolean) + systemProperty("testLatestDeps", findProperty("testLatestDeps") as Boolean) + } } diff --git a/instrumentation/async-http-client/async-http-client-1.9/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/asynchttpclient/v1_9/AsyncHttpClientHttpAttributesGetter.java b/instrumentation/async-http-client/async-http-client-1.9/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/asynchttpclient/v1_9/AsyncHttpClientHttpAttributesGetter.java index 1e1544fdb297..f30f6dc1af27 100644 --- a/instrumentation/async-http-client/async-http-client-1.9/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/asynchttpclient/v1_9/AsyncHttpClientHttpAttributesGetter.java +++ b/instrumentation/async-http-client/async-http-client-1.9/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/asynchttpclient/v1_9/AsyncHttpClientHttpAttributesGetter.java @@ -7,7 +7,7 @@ import com.ning.http.client.Request; import com.ning.http.client.Response; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpClientAttributesGetter; +import io.opentelemetry.instrumentation.api.semconv.http.HttpClientAttributesGetter; import java.util.Collections; import java.util.List; import javax.annotation.Nullable; @@ -40,4 +40,14 @@ public Integer getHttpResponseStatusCode( public List getHttpResponseHeader(Request request, Response response, String name) { return response.getHeaders().getOrDefault(name, Collections.emptyList()); } + + @Override + public String getServerAddress(Request request) { + return request.getUri().getHost(); + } + + @Override + public Integer getServerPort(Request request) { + return request.getUri().getPort(); + } } diff --git a/instrumentation/async-http-client/async-http-client-1.9/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/asynchttpclient/v1_9/AsyncHttpClientNetAttributesGetter.java b/instrumentation/async-http-client/async-http-client-1.9/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/asynchttpclient/v1_9/AsyncHttpClientNetAttributesGetter.java deleted file mode 100644 index 508d727b450b..000000000000 --- a/instrumentation/async-http-client/async-http-client-1.9/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/asynchttpclient/v1_9/AsyncHttpClientNetAttributesGetter.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * 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.instrumentation.api.instrumenter.net.NetClientAttributesGetter; -import javax.annotation.Nullable; - -final class AsyncHttpClientNetAttributesGetter - implements NetClientAttributesGetter { - - @Nullable - @Override - public String getNetworkProtocolName(Request request, @Nullable Response response) { - return null; - } - - @Nullable - @Override - public String getNetworkProtocolVersion(Request request, @Nullable Response response) { - return null; - } - - @Override - public String getServerAddress(Request request) { - return request.getUri().getHost(); - } - - @Override - public Integer getServerPort(Request request) { - return request.getUri().getPort(); - } -} diff --git a/instrumentation/async-http-client/async-http-client-1.9/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/asynchttpclient/v1_9/AsyncHttpClientSingletons.java b/instrumentation/async-http-client/async-http-client-1.9/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/asynchttpclient/v1_9/AsyncHttpClientSingletons.java index afed3993ce37..c886631a93cc 100644 --- a/instrumentation/async-http-client/async-http-client-1.9/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/asynchttpclient/v1_9/AsyncHttpClientSingletons.java +++ b/instrumentation/async-http-client/async-http-client-1.9/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/asynchttpclient/v1_9/AsyncHttpClientSingletons.java @@ -7,14 +7,8 @@ import com.ning.http.client.Request; import com.ning.http.client.Response; -import io.opentelemetry.api.GlobalOpenTelemetry; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpClientAttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpClientMetrics; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpSpanNameExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpSpanStatusExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.net.PeerServiceAttributesExtractor; -import io.opentelemetry.javaagent.bootstrap.internal.CommonConfig; +import io.opentelemetry.javaagent.bootstrap.internal.JavaagentHttpClientInstrumenters; public final class AsyncHttpClientSingletons { private static final String INSTRUMENTATION_NAME = "io.opentelemetry.async-http-client-1.9"; @@ -22,27 +16,11 @@ public final class AsyncHttpClientSingletons { private static final Instrumenter INSTRUMENTER; static { - AsyncHttpClientHttpAttributesGetter httpAttributesGetter = - new AsyncHttpClientHttpAttributesGetter(); - AsyncHttpClientNetAttributesGetter netAttributesGetter = - new AsyncHttpClientNetAttributesGetter(); - INSTRUMENTER = - Instrumenter.builder( - GlobalOpenTelemetry.get(), - INSTRUMENTATION_NAME, - HttpSpanNameExtractor.create(httpAttributesGetter)) - .setSpanStatusExtractor(HttpSpanStatusExtractor.create(httpAttributesGetter)) - .addAttributesExtractor( - HttpClientAttributesExtractor.builder(httpAttributesGetter, netAttributesGetter) - .setCapturedRequestHeaders(CommonConfig.get().getClientRequestHeaders()) - .setCapturedResponseHeaders(CommonConfig.get().getClientResponseHeaders()) - .build()) - .addAttributesExtractor( - PeerServiceAttributesExtractor.create( - netAttributesGetter, CommonConfig.get().getPeerServiceMapping())) - .addOperationMetrics(HttpClientMetrics.get()) - .buildClientInstrumenter(HttpHeaderSetter.INSTANCE); + JavaagentHttpClientInstrumenters.create( + INSTRUMENTATION_NAME, + new AsyncHttpClientHttpAttributesGetter(), + HttpHeaderSetter.INSTANCE); } public static Instrumenter instrumenter() { diff --git a/instrumentation/async-http-client/async-http-client-1.9/javaagent/src/test/groovy/AsyncHttpClientTest.groovy b/instrumentation/async-http-client/async-http-client-1.9/javaagent/src/test/groovy/AsyncHttpClientTest.groovy deleted file mode 100644 index 52847856128f..000000000000 --- a/instrumentation/async-http-client/async-http-client-1.9/javaagent/src/test/groovy/AsyncHttpClientTest.groovy +++ /dev/null @@ -1,90 +0,0 @@ -/* - * 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.api.common.AttributeKey -import io.opentelemetry.instrumentation.test.AgentTestTrait -import io.opentelemetry.instrumentation.test.base.HttpClientTest -import io.opentelemetry.instrumentation.testing.junit.http.HttpClientResult -import io.opentelemetry.instrumentation.testing.junit.http.SingleConnection -import spock.lang.AutoCleanup -import spock.lang.Shared - -import static io.opentelemetry.api.common.AttributeKey.stringKey - -class AsyncHttpClientTest extends HttpClientTest implements AgentTestTrait { - - @AutoCleanup - @Shared - def client = new AsyncHttpClient(new AsyncHttpClientConfig.Builder() - .setConnectTimeout(CONNECT_TIMEOUT_MS) - .setReadTimeout(READ_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, HttpClientResult 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 - boolean testReadTimeout() { - // disable read timeout test for non latest because it is flaky with 1.9.0 - Boolean.getBoolean("testLatestDeps") - } - - @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 - } - - Set> httpAttributes(URI uri) { - def attributes = super.httpAttributes(uri) - attributes.remove(stringKey("net.protocol.name")) - attributes.remove(stringKey("net.protocol.version")) - return attributes - } -} diff --git a/instrumentation/async-http-client/async-http-client-1.9/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/asynchttpclient/v1_9/AsyncHttpClientTest.java b/instrumentation/async-http-client/async-http-client-1.9/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/asynchttpclient/v1_9/AsyncHttpClientTest.java new file mode 100644 index 000000000000..b273548b9f2c --- /dev/null +++ b/instrumentation/async-http-client/async-http-client-1.9/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/asynchttpclient/v1_9/AsyncHttpClientTest.java @@ -0,0 +1,114 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.asynchttpclient.v1_9; + +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.api.common.AttributeKey; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpClientTest; +import io.opentelemetry.instrumentation.testing.junit.http.HttpClientInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.http.HttpClientResult; +import io.opentelemetry.instrumentation.testing.junit.http.HttpClientTestOptions; +import io.opentelemetry.semconv.NetworkAttributes; +import java.net.URI; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ExecutionException; +import org.junit.jupiter.api.extension.RegisterExtension; + +class AsyncHttpClientTest extends AbstractHttpClientTest { + + @RegisterExtension + public static final InstrumentationExtension testing = + HttpClientInstrumentationExtension.forAgent(); + + private static final int CONNECTION_TIMEOUT_MS = (int) CONNECTION_TIMEOUT.toMillis(); + private static final int READ_TIMEOUT_MS = (int) READ_TIMEOUT.toMillis(); + + private static final AsyncHttpClient client = buildClient(false); + private static final AsyncHttpClient clientWithReadTimeout = buildClient(true); + + private static AsyncHttpClient buildClient(boolean readTimeout) { + AsyncHttpClientConfig.Builder builder = + new AsyncHttpClientConfig.Builder().setConnectTimeout(CONNECTION_TIMEOUT_MS); + if (readTimeout) { + builder.setReadTimeout(READ_TIMEOUT_MS); + } + return new AsyncHttpClient(builder.build()); + } + + private static AsyncHttpClient getClient(URI uri) { + if (uri.toString().contains("/read-timeout")) { + return clientWithReadTimeout; + } + return client; + } + + @Override + public Request buildRequest(String method, URI uri, Map headers) { + RequestBuilder requestBuilder = new RequestBuilder(method).setUri(Uri.create(uri.toString())); + for (Map.Entry entry : headers.entrySet()) { + requestBuilder.setHeader(entry.getKey(), entry.getValue()); + } + return requestBuilder.build(); + } + + @Override + public int sendRequest(Request request, String method, URI uri, Map headers) + throws ExecutionException, InterruptedException { + return getClient(uri).executeRequest(request).get().getStatusCode(); + } + + @Override + public void sendRequestWithCallback( + Request request, + String method, + URI uri, + Map headers, + HttpClientResult requestResult) { + getClient(uri) + .executeRequest( + request, + new AsyncCompletionHandler() { + @Override + public Void onCompleted(Response response) { + requestResult.complete(response.getStatusCode()); + return null; + } + + @Override + public void onThrowable(Throwable throwable) { + requestResult.complete(throwable); + } + }); + } + + @Override + protected void configure(HttpClientTestOptions.Builder optionsBuilder) { + optionsBuilder.spanEndsAfterBody(); + optionsBuilder.disableTestRedirects(); + + // disable read timeout test for non latest because it is flaky with 1.9.0 + if (!Boolean.getBoolean("testLatestDeps")) { + optionsBuilder.disableTestReadTimeout(); + } + + optionsBuilder.setHttpAttributes( + endpoint -> { + Set> attributes = + new HashSet<>(HttpClientTestOptions.DEFAULT_HTTP_ATTRIBUTES); + attributes.remove(NetworkAttributes.NETWORK_PROTOCOL_VERSION); + return attributes; + }); + } +} diff --git a/instrumentation/async-http-client/async-http-client-2.0/javaagent/build.gradle.kts b/instrumentation/async-http-client/async-http-client-2.0/javaagent/build.gradle.kts index 30b722110c08..9c573a0fd4ec 100644 --- a/instrumentation/async-http-client/async-http-client-2.0/javaagent/build.gradle.kts +++ b/instrumentation/async-http-client/async-http-client-2.0/javaagent/build.gradle.kts @@ -18,14 +18,32 @@ dependencies { testInstrumentation(project(":instrumentation:netty:netty-4.1:javaagent")) } -otelJava { - // AHC uses Unsafe and so does not run on later java version - maxJavaVersionForTests.set(JavaVersion.VERSION_1_8) +val latestDepTest = findProperty("testLatestDeps") as Boolean +val testJavaVersion = + gradle.startParameter.projectProperties["testJavaVersion"]?.let(JavaVersion::toVersion) + ?: JavaVersion.current() + +if (!latestDepTest) { + otelJava { + // AHC uses Unsafe and so does not run on later java version + maxJavaVersionForTests.set(JavaVersion.VERSION_1_8) + } +} + +tasks.withType().configureEach { + systemProperty("testLatestDeps", latestDepTest) + // async-http-client 3.0 requires java 11 + // We are not using minJavaVersionSupported for latestDepTest because that way the instrumentation + // gets compiled with java 11 when running latestDepTest. This causes play-mvc-2.4 latest dep tests + // to fail because they require java 8 and instrumentation compiled with java 11 won't apply. + if (latestDepTest && testJavaVersion.isJava8) { + enabled = false + } } // async-http-client 2.0.0 does not work with Netty versions newer than this due to referencing an // internal file. -if (!(findProperty("testLatestDeps") as Boolean)) { +if (!latestDepTest) { configurations.configureEach { if (!name.contains("muzzle")) { resolutionStrategy { diff --git a/instrumentation/async-http-client/async-http-client-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/asynchttpclient/v2_0/AsyncHttpClientAdditionalAttributesExtractor.java b/instrumentation/async-http-client/async-http-client-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/asynchttpclient/v2_0/AsyncHttpClientAdditionalAttributesExtractor.java deleted file mode 100644 index 944a43fe6fb9..000000000000 --- a/instrumentation/async-http-client/async-http-client-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/asynchttpclient/v2_0/AsyncHttpClientAdditionalAttributesExtractor.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.javaagent.instrumentation.asynchttpclient.v2_0; - -import io.opentelemetry.api.common.AttributesBuilder; -import io.opentelemetry.context.Context; -import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; -import javax.annotation.Nullable; -import org.asynchttpclient.Response; -import org.asynchttpclient.netty.request.NettyRequest; - -public class AsyncHttpClientAdditionalAttributesExtractor - implements AttributesExtractor { - - @Override - public void onStart( - AttributesBuilder attributes, Context parentContext, RequestContext requestContext) {} - - @Override - public void onEnd( - AttributesBuilder attributes, - Context context, - RequestContext requestContext, - @Nullable Response response, - @Nullable Throwable error) { - - NettyRequest nettyRequest = requestContext.getNettyRequest(); - if (nettyRequest != null) { - String userAgent = nettyRequest.getHttpRequest().headers().get("User-Agent"); - if (userAgent != null) { - attributes.put(SemanticAttributes.USER_AGENT_ORIGINAL, userAgent); - } - } - } -} diff --git a/instrumentation/async-http-client/async-http-client-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/asynchttpclient/v2_0/AsyncHttpClientHttpAttributesGetter.java b/instrumentation/async-http-client/async-http-client-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/asynchttpclient/v2_0/AsyncHttpClientHttpAttributesGetter.java index 408220f0298c..ea7dce02bf8e 100644 --- a/instrumentation/async-http-client/async-http-client-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/asynchttpclient/v2_0/AsyncHttpClientHttpAttributesGetter.java +++ b/instrumentation/async-http-client/async-http-client-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/asynchttpclient/v2_0/AsyncHttpClientHttpAttributesGetter.java @@ -5,10 +5,14 @@ package io.opentelemetry.javaagent.instrumentation.asynchttpclient.v2_0; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpClientAttributesGetter; +import io.netty.handler.codec.http.HttpRequest; +import io.netty.handler.codec.http.HttpVersion; +import io.opentelemetry.instrumentation.api.semconv.http.HttpClientAttributesGetter; +import java.net.InetSocketAddress; import java.util.List; import javax.annotation.Nullable; import org.asynchttpclient.Response; +import org.asynchttpclient.netty.request.NettyRequest; final class AsyncHttpClientHttpAttributesGetter implements HttpClientAttributesGetter { @@ -39,4 +43,61 @@ public List getHttpResponseHeader( RequestContext requestContext, Response response, String name) { return response.getHeaders().getAll(name); } + + @Nullable + @Override + public String getNetworkProtocolName(RequestContext request, @Nullable Response response) { + HttpVersion httpVersion = getHttpVersion(request); + if (httpVersion == null) { + return null; + } + return httpVersion.protocolName(); + } + + @Nullable + @Override + public String getNetworkProtocolVersion(RequestContext request, @Nullable Response response) { + HttpVersion httpVersion = getHttpVersion(request); + if (httpVersion == null) { + return null; + } + if (httpVersion.minorVersion() == 0) { + return Integer.toString(httpVersion.majorVersion()); + } + return httpVersion.majorVersion() + "." + httpVersion.minorVersion(); + } + + @Nullable + private static HttpVersion getHttpVersion(RequestContext request) { + NettyRequest nettyRequest = request.getNettyRequest(); + if (nettyRequest == null) { + return null; + } + HttpRequest httpRequest = nettyRequest.getHttpRequest(); + if (httpRequest == null) { + return null; + } + return httpRequest.getProtocolVersion(); + } + + @Nullable + @Override + public String getServerAddress(RequestContext request) { + return request.getRequest().getUri().getHost(); + } + + @Override + public Integer getServerPort(RequestContext request) { + return request.getRequest().getUri().getPort(); + } + + @Override + @Nullable + public InetSocketAddress getNetworkPeerInetSocketAddress( + RequestContext request, @Nullable Response response) { + if (response != null && response.getRemoteAddress() instanceof InetSocketAddress) { + return (InetSocketAddress) response.getRemoteAddress(); + } + return null; + } } diff --git a/instrumentation/async-http-client/async-http-client-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/asynchttpclient/v2_0/AsyncHttpClientNetAttributesGetter.java b/instrumentation/async-http-client/async-http-client-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/asynchttpclient/v2_0/AsyncHttpClientNetAttributesGetter.java deleted file mode 100644 index 8295ef298143..000000000000 --- a/instrumentation/async-http-client/async-http-client-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/asynchttpclient/v2_0/AsyncHttpClientNetAttributesGetter.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.javaagent.instrumentation.asynchttpclient.v2_0; - -import io.netty.handler.codec.http.HttpRequest; -import io.netty.handler.codec.http.HttpVersion; -import io.opentelemetry.instrumentation.api.instrumenter.net.NetClientAttributesGetter; -import java.net.InetSocketAddress; -import javax.annotation.Nullable; -import org.asynchttpclient.Response; -import org.asynchttpclient.netty.request.NettyRequest; - -final class AsyncHttpClientNetAttributesGetter - implements NetClientAttributesGetter { - - @Nullable - @Override - public String getNetworkProtocolName(RequestContext request, @Nullable Response response) { - HttpVersion httpVersion = getHttpVersion(request); - if (httpVersion == null) { - return null; - } - return httpVersion.protocolName(); - } - - @Nullable - @Override - public String getNetworkProtocolVersion(RequestContext request, @Nullable Response response) { - HttpVersion httpVersion = getHttpVersion(request); - if (httpVersion == null) { - return null; - } - return httpVersion.majorVersion() + "." + httpVersion.minorVersion(); - } - - @Nullable - private static HttpVersion getHttpVersion(RequestContext request) { - NettyRequest nettyRequest = request.getNettyRequest(); - if (nettyRequest == null) { - return null; - } - HttpRequest httpRequest = nettyRequest.getHttpRequest(); - if (httpRequest == null) { - return null; - } - return httpRequest.getProtocolVersion(); - } - - @Nullable - @Override - public String getServerAddress(RequestContext request) { - return request.getRequest().getUri().getHost(); - } - - @Override - public Integer getServerPort(RequestContext request) { - return request.getRequest().getUri().getPort(); - } - - @Override - @Nullable - public InetSocketAddress getServerInetSocketAddress( - RequestContext request, @Nullable Response response) { - if (response != null && response.getRemoteAddress() instanceof InetSocketAddress) { - return (InetSocketAddress) response.getRemoteAddress(); - } - return null; - } -} diff --git a/instrumentation/async-http-client/async-http-client-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/asynchttpclient/v2_0/AsyncHttpClientSingletons.java b/instrumentation/async-http-client/async-http-client-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/asynchttpclient/v2_0/AsyncHttpClientSingletons.java index 3ec5f219511b..2e140594f0bd 100644 --- a/instrumentation/async-http-client/async-http-client-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/asynchttpclient/v2_0/AsyncHttpClientSingletons.java +++ b/instrumentation/async-http-client/async-http-client-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/asynchttpclient/v2_0/AsyncHttpClientSingletons.java @@ -5,14 +5,8 @@ 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.http.HttpClientAttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpClientMetrics; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpSpanNameExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpSpanStatusExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.net.PeerServiceAttributesExtractor; -import io.opentelemetry.javaagent.bootstrap.internal.CommonConfig; +import io.opentelemetry.javaagent.bootstrap.internal.JavaagentHttpClientInstrumenters; import org.asynchttpclient.Response; public final class AsyncHttpClientSingletons { @@ -21,28 +15,11 @@ public final class AsyncHttpClientSingletons { private static final Instrumenter INSTRUMENTER; static { - AsyncHttpClientHttpAttributesGetter httpAttributesGetter = - new AsyncHttpClientHttpAttributesGetter(); - AsyncHttpClientNetAttributesGetter netAttributeGetter = - new AsyncHttpClientNetAttributesGetter(); - INSTRUMENTER = - Instrumenter.builder( - GlobalOpenTelemetry.get(), - INSTRUMENTATION_NAME, - HttpSpanNameExtractor.create(httpAttributesGetter)) - .setSpanStatusExtractor(HttpSpanStatusExtractor.create(httpAttributesGetter)) - .addAttributesExtractor( - HttpClientAttributesExtractor.builder(httpAttributesGetter, netAttributeGetter) - .setCapturedRequestHeaders(CommonConfig.get().getClientRequestHeaders()) - .setCapturedResponseHeaders(CommonConfig.get().getClientResponseHeaders()) - .build()) - .addAttributesExtractor( - PeerServiceAttributesExtractor.create( - netAttributeGetter, CommonConfig.get().getPeerServiceMapping())) - .addAttributesExtractor(new AsyncHttpClientAdditionalAttributesExtractor()) - .addOperationMetrics(HttpClientMetrics.get()) - .buildClientInstrumenter(HttpHeaderSetter.INSTANCE); + JavaagentHttpClientInstrumenters.create( + INSTRUMENTATION_NAME, + new AsyncHttpClientHttpAttributesGetter(), + HttpHeaderSetter.INSTANCE); } public static Instrumenter instrumenter() { diff --git a/instrumentation/async-http-client/async-http-client-2.0/javaagent/src/test/groovy/AsyncHttpClientTest.groovy b/instrumentation/async-http-client/async-http-client-2.0/javaagent/src/test/groovy/AsyncHttpClientTest.groovy deleted file mode 100644 index c24fdc215d32..000000000000 --- a/instrumentation/async-http-client/async-http-client-2.0/javaagent/src/test/groovy/AsyncHttpClientTest.groovy +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -import io.opentelemetry.instrumentation.test.AgentTestTrait -import io.opentelemetry.instrumentation.test.base.HttpClientTest -import io.opentelemetry.instrumentation.testing.junit.http.HttpClientResult -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, HttpClientResult 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) - } - }) - } - - @Override - String userAgent() { - return "AHC" - } - - @Override - boolean testRedirects() { - false - } -} diff --git a/instrumentation/async-http-client/async-http-client-2.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/asynchttpclient/v2_0/AsyncHttpClientTest.java b/instrumentation/async-http-client/async-http-client-2.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/asynchttpclient/v2_0/AsyncHttpClientTest.java new file mode 100644 index 000000000000..d37febdba3d2 --- /dev/null +++ b/instrumentation/async-http-client/async-http-client-2.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/asynchttpclient/v2_0/AsyncHttpClientTest.java @@ -0,0 +1,101 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.asynchttpclient.v2_0; + +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpClientTest; +import io.opentelemetry.instrumentation.testing.junit.http.HttpClientInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.http.HttpClientResult; +import io.opentelemetry.instrumentation.testing.junit.http.HttpClientTestOptions; +import java.lang.reflect.Method; +import java.net.URI; +import java.time.Duration; +import java.util.Map; +import java.util.concurrent.ExecutionException; +import org.asynchttpclient.AsyncCompletionHandler; +import org.asynchttpclient.AsyncHttpClient; +import org.asynchttpclient.DefaultAsyncHttpClientConfig; +import org.asynchttpclient.Dsl; +import org.asynchttpclient.Request; +import org.asynchttpclient.RequestBuilder; +import org.asynchttpclient.Response; +import org.asynchttpclient.uri.Uri; +import org.junit.jupiter.api.extension.RegisterExtension; + +class AsyncHttpClientTest extends AbstractHttpClientTest { + + @RegisterExtension + public static final InstrumentationExtension testing = + HttpClientInstrumentationExtension.forAgent(); + + private static final int CONNECTION_TIMEOUT_MS = (int) CONNECTION_TIMEOUT.toMillis(); + + // request timeout is needed in addition to connect timeout on async-http-client versions 2.1.0+ + private static final AsyncHttpClient client = Dsl.asyncHttpClient(configureTimeout(Dsl.config())); + + private static DefaultAsyncHttpClientConfig.Builder configureTimeout( + DefaultAsyncHttpClientConfig.Builder builder) { + setTimeout(builder, "setConnectTimeout", CONNECTION_TIMEOUT_MS); + setTimeout(builder, "setRequestTimeout", CONNECTION_TIMEOUT_MS); + return builder; + } + + private static void setTimeout( + DefaultAsyncHttpClientConfig.Builder builder, String methodName, int timeout) { + boolean testLatestDeps = Boolean.getBoolean("testLatestDeps"); + try { + Method method = + builder.getClass().getMethod(methodName, testLatestDeps ? Duration.class : int.class); + method.invoke(builder, testLatestDeps ? Duration.ofMillis(timeout) : timeout); + } catch (Exception exception) { + throw new IllegalStateException("Failed to set timeout " + methodName, exception); + } + } + + @Override + public Request buildRequest(String method, URI uri, Map headers) { + RequestBuilder requestBuilder = new RequestBuilder(method).setUri(Uri.create(uri.toString())); + for (Map.Entry entry : headers.entrySet()) { + requestBuilder.setHeader(entry.getKey(), entry.getValue()); + } + return requestBuilder.build(); + } + + @Override + public int sendRequest(Request request, String method, URI uri, Map headers) + throws ExecutionException, InterruptedException { + return client.executeRequest(request).get().getStatusCode(); + } + + @Override + public void sendRequestWithCallback( + Request request, + String method, + URI uri, + Map headers, + HttpClientResult requestResult) { + client.executeRequest( + request, + new AsyncCompletionHandler() { + @Override + public Void onCompleted(Response response) { + requestResult.complete(response.getStatusCode()); + return null; + } + + @Override + public void onThrowable(Throwable throwable) { + requestResult.complete(throwable); + } + }); + } + + @Override + protected void configure(HttpClientTestOptions.Builder optionsBuilder) { + optionsBuilder.disableTestRedirects(); + optionsBuilder.spanEndsAfterBody(); + } +} diff --git a/instrumentation/aws-lambda/README.md b/instrumentation/aws-lambda/README.md index 72fba2abab51..9173db38f84b 100644 --- a/instrumentation/aws-lambda/README.md +++ b/instrumentation/aws-lambda/README.md @@ -3,9 +3,9 @@ We provide two packages for instrumenting AWS lambda functions. - [aws-lambda-core-1.0](./aws-lambda-core-1.0/library) provides lightweight instrumentation of the Lambda core library, supporting -all versions. Use this package if you only use `RequestStreamHandler` or know you don't use any event classes from -`aws-lambda-java-events`. This also includes when you are using `aws-serverless-java-container` to run e.g., a -Spring Boot application on Lambda. + all versions. Use this package if you only use `RequestStreamHandler` or know you don't use any event classes from + `aws-lambda-java-events`. This also includes when you are using `aws-serverless-java-container` to run e.g., a + Spring Boot application on Lambda. - [aws-lambda-events-2.2](./aws-lambda-events-2.2/library) provides full instrumentation of the Lambda library, including standard -and custom event types, from `aws-lambda-java-events` 2.2+. + and custom event types, from `aws-lambda-java-events` 2.2+. diff --git a/instrumentation/aws-lambda/aws-lambda-core-1.0/javaagent/build.gradle.kts b/instrumentation/aws-lambda/aws-lambda-core-1.0/javaagent/build.gradle.kts index 68cfa9a0b6e8..f82163152e21 100644 --- a/instrumentation/aws-lambda/aws-lambda-core-1.0/javaagent/build.gradle.kts +++ b/instrumentation/aws-lambda/aws-lambda-core-1.0/javaagent/build.gradle.kts @@ -19,7 +19,6 @@ dependencies { library("com.amazonaws:aws-lambda-java-core:1.0.0") testImplementation(project(":instrumentation:aws-lambda:aws-lambda-core-1.0:testing")) - testInstrumentation(project(":instrumentation:aws-lambda:aws-lambda-events-2.2:javaagent")) } tasks.withType().configureEach { diff --git a/instrumentation/aws-lambda/aws-lambda-core-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/awslambdacore/v1_0/AwsLambdaInstrumentationModule.java b/instrumentation/aws-lambda/aws-lambda-core-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/awslambdacore/v1_0/AwsLambdaInstrumentationModule.java index 9c45a8ae7449..35d6b70ed68b 100644 --- a/instrumentation/aws-lambda/aws-lambda-core-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/awslambdacore/v1_0/AwsLambdaInstrumentationModule.java +++ b/instrumentation/aws-lambda/aws-lambda-core-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/awslambdacore/v1_0/AwsLambdaInstrumentationModule.java @@ -5,12 +5,15 @@ package io.opentelemetry.javaagent.instrumentation.awslambdacore.v1_0; +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.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 AwsLambdaInstrumentationModule extends InstrumentationModule { @@ -18,6 +21,12 @@ public AwsLambdaInstrumentationModule() { super("aws-lambda-core", "aws-lambda-core-1.0", "aws-lambda"); } + @Override + public ElementMatcher.Junction classLoaderMatcher() { + // aws-lambda-events-2.2 is used when SQSEvent is present + return not(hasClassesNamed("com.amazonaws.services.lambda.runtime.events.SQSEvent")); + } + @Override public boolean isHelperClass(String className) { return className.startsWith("io.opentelemetry.contrib.awsxray."); diff --git a/instrumentation/aws-lambda/aws-lambda-core-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/awslambdacore/v1_0/AwsLambdaRequestHandlerInstrumentation.java b/instrumentation/aws-lambda/aws-lambda-core-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/awslambdacore/v1_0/AwsLambdaRequestHandlerInstrumentation.java index 2332f24a8fd8..dfc70b368fc4 100644 --- a/instrumentation/aws-lambda/aws-lambda-core-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/awslambdacore/v1_0/AwsLambdaRequestHandlerInstrumentation.java +++ b/instrumentation/aws-lambda/aws-lambda-core-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/awslambdacore/v1_0/AwsLambdaRequestHandlerInstrumentation.java @@ -10,7 +10,9 @@ import static io.opentelemetry.javaagent.instrumentation.awslambdacore.v1_0.AwsLambdaInstrumentationHelper.functionInstrumenter; 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 com.amazonaws.services.lambda.runtime.Context; @@ -35,7 +37,13 @@ public ElementMatcher classLoaderOptimization() { @Override public ElementMatcher typeMatcher() { - return implementsInterface(named("com.amazonaws.services.lambda.runtime.RequestHandler")); + return implementsInterface(named("com.amazonaws.services.lambda.runtime.RequestHandler")) + .and(not(nameStartsWith("com.amazonaws.services.lambda.runtime.api.client"))) + // In Java 8 and Java 11 runtimes, + // AWS Lambda runtime is packaged under `lambdainternal` package. + // But it is `com.amazonaws.services.lambda.runtime.api.client` + // for new runtime likes Java 17 and Java 21. + .and(not(nameStartsWith("lambdainternal"))); } @Override diff --git a/instrumentation/aws-lambda/aws-lambda-core-1.0/javaagent/src/test/java/com/amazonaws/services/lambda/runtime/api/client/AwsLambdaInternalRequestHandler.java b/instrumentation/aws-lambda/aws-lambda-core-1.0/javaagent/src/test/java/com/amazonaws/services/lambda/runtime/api/client/AwsLambdaInternalRequestHandler.java new file mode 100644 index 000000000000..de18a333df15 --- /dev/null +++ b/instrumentation/aws-lambda/aws-lambda-core-1.0/javaagent/src/test/java/com/amazonaws/services/lambda/runtime/api/client/AwsLambdaInternalRequestHandler.java @@ -0,0 +1,23 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.amazonaws.services.lambda.runtime.api.client; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestHandler; + +public class AwsLambdaInternalRequestHandler implements RequestHandler { + + private final RequestHandler requestHandler; + + public AwsLambdaInternalRequestHandler(RequestHandler requestHandler) { + this.requestHandler = requestHandler; + } + + @Override + public String handleRequest(String input, Context context) { + return requestHandler.handleRequest(input, context); + } +} diff --git a/instrumentation/aws-lambda/aws-lambda-core-1.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/awslambdacore/v1_0/AwsLambdaTest.java b/instrumentation/aws-lambda/aws-lambda-core-1.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/awslambdacore/v1_0/AwsLambdaTest.java index af939fcb6d8f..575a553fbfb0 100644 --- a/instrumentation/aws-lambda/aws-lambda-core-1.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/awslambdacore/v1_0/AwsLambdaTest.java +++ b/instrumentation/aws-lambda/aws-lambda-core-1.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/awslambdacore/v1_0/AwsLambdaTest.java @@ -5,14 +5,20 @@ package io.opentelemetry.javaagent.instrumentation.awslambdacore.v1_0; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; import static org.assertj.core.api.Assertions.assertThat; import com.amazonaws.services.lambda.runtime.Context; import com.amazonaws.services.lambda.runtime.RequestHandler; +import com.amazonaws.services.lambda.runtime.api.client.AwsLambdaInternalRequestHandler; +import io.opentelemetry.api.trace.SpanKind; import io.opentelemetry.instrumentation.awslambdacore.v1_0.AbstractAwsLambdaTest; import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.semconv.incubating.FaasIncubatingAttributes; +import lambdainternal.AwsLambdaLegacyInternalRequestHandler; import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; public class AwsLambdaTest extends AbstractAwsLambdaTest { @@ -35,6 +41,40 @@ void tearDown() { assertThat(testing.forceFlushCalled()).isTrue(); } + @Test + void awsLambdaInternalHandlerIgnoredAndUserHandlerTraced() { + RequestHandler handler = new AwsLambdaInternalRequestHandler(handler()); + String result = handler.handleRequest("hello", context()); + assertThat(result).isEqualTo("world"); + + testing() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("my_function") + .hasKind(SpanKind.SERVER) + .hasAttributesSatisfyingExactly( + equalTo(FaasIncubatingAttributes.FAAS_INVOCATION_ID, "1-22-333")))); + } + + @Test + void awsLambdaLegacyInternalHandlerIgnoredAndUserHandlerTraced() { + RequestHandler handler = new AwsLambdaLegacyInternalRequestHandler(handler()); + String result = handler.handleRequest("hello", context()); + assertThat(result).isEqualTo("world"); + + testing() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("my_function") + .hasKind(SpanKind.SERVER) + .hasAttributesSatisfyingExactly( + equalTo(FaasIncubatingAttributes.FAAS_INVOCATION_ID, "1-22-333")))); + } + private static final class TestRequestHandler implements RequestHandler { @Override diff --git a/instrumentation/aws-lambda/aws-lambda-core-1.0/javaagent/src/test/java/lambdainternal/AwsLambdaLegacyInternalRequestHandler.java b/instrumentation/aws-lambda/aws-lambda-core-1.0/javaagent/src/test/java/lambdainternal/AwsLambdaLegacyInternalRequestHandler.java new file mode 100644 index 000000000000..5d4625ef47c1 --- /dev/null +++ b/instrumentation/aws-lambda/aws-lambda-core-1.0/javaagent/src/test/java/lambdainternal/AwsLambdaLegacyInternalRequestHandler.java @@ -0,0 +1,23 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package lambdainternal; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestHandler; + +public class AwsLambdaLegacyInternalRequestHandler implements RequestHandler { + + private final RequestHandler requestHandler; + + public AwsLambdaLegacyInternalRequestHandler(RequestHandler requestHandler) { + this.requestHandler = requestHandler; + } + + @Override + public String handleRequest(String input, Context context) { + return requestHandler.handleRequest(input, context); + } +} diff --git a/instrumentation/aws-lambda/aws-lambda-core-1.0/library/README.md b/instrumentation/aws-lambda/aws-lambda-core-1.0/library/README.md index fa5cffc9d5c9..932bb41978ea 100644 --- a/instrumentation/aws-lambda/aws-lambda-core-1.0/library/README.md +++ b/instrumentation/aws-lambda/aws-lambda-core-1.0/library/README.md @@ -87,7 +87,7 @@ 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 ). + traces can be propagated with supported HTTP headers (see ). In order to enable requested propagation for a handler, configure it on the SDK you build. diff --git a/instrumentation/aws-lambda/aws-lambda-core-1.0/library/build.gradle.kts b/instrumentation/aws-lambda/aws-lambda-core-1.0/library/build.gradle.kts index f1bc5ba6cae9..4fcb6700fd01 100644 --- a/instrumentation/aws-lambda/aws-lambda-core-1.0/library/build.gradle.kts +++ b/instrumentation/aws-lambda/aws-lambda-core-1.0/library/build.gradle.kts @@ -11,8 +11,6 @@ dependencies { library("com.amazonaws:aws-lambda-java-core:1.0.0") - implementation("io.opentelemetry.contrib:opentelemetry-aws-xray-propagator") - // We do lightweight parsing of JSON to extract HTTP headers from requests for propagation. // This will be commonly needed even for users that don't use events, but luckily it's not too big. // Note that Lambda itself uses Jackson, but does not expose it to the function so we need to include @@ -22,6 +20,7 @@ dependencies { // allows to get the function ARN testLibrary("com.amazonaws:aws-lambda-java-core:1.2.1") + testImplementation("io.opentelemetry.contrib:opentelemetry-aws-xray-propagator") testImplementation("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure") testImplementation("io.opentelemetry:opentelemetry-extension-trace-propagators") testImplementation("com.google.guava:guava") @@ -32,6 +31,7 @@ dependencies { tasks.withType().configureEach { // required on jdk17 + jvmArgs("--add-opens=java.base/java.lang=ALL-UNNAMED") jvmArgs("--add-opens=java.base/java.util=ALL-UNNAMED") jvmArgs("-XX:+IgnoreUnrecognizedVMOptions") } diff --git a/instrumentation/aws-lambda/aws-lambda-core-1.0/library/src/main/java/io/opentelemetry/instrumentation/awslambdacore/v1_0/TracingRequestStreamHandler.java b/instrumentation/aws-lambda/aws-lambda-core-1.0/library/src/main/java/io/opentelemetry/instrumentation/awslambdacore/v1_0/TracingRequestStreamHandler.java index fced56818f52..ae6a42bd6de4 100644 --- a/instrumentation/aws-lambda/aws-lambda-core-1.0/library/src/main/java/io/opentelemetry/instrumentation/awslambdacore/v1_0/TracingRequestStreamHandler.java +++ b/instrumentation/aws-lambda/aws-lambda-core-1.0/library/src/main/java/io/opentelemetry/instrumentation/awslambdacore/v1_0/TracingRequestStreamHandler.java @@ -7,6 +7,7 @@ import com.amazonaws.services.lambda.runtime.Context; import com.amazonaws.services.lambda.runtime.RequestStreamHandler; +import io.opentelemetry.api.trace.Span; import io.opentelemetry.context.Scope; import io.opentelemetry.instrumentation.awslambdacore.v1_0.internal.ApiGatewayProxyRequest; import io.opentelemetry.instrumentation.awslambdacore.v1_0.internal.AwsLambdaFunctionInstrumenter; @@ -67,49 +68,55 @@ protected TracingRequestStreamHandler( @Override public void handleRequest(InputStream input, OutputStream output, Context context) throws IOException { - ApiGatewayProxyRequest proxyRequest = ApiGatewayProxyRequest.forStream(input); - AwsLambdaRequest request = - AwsLambdaRequest.create(context, proxyRequest, proxyRequest.getHeaders()); + AwsLambdaRequest request = createRequest(input, context, proxyRequest); io.opentelemetry.context.Context parentContext = instrumenter.extract(request); if (!instrumenter.shouldStart(parentContext, request)) { - doHandleRequest(proxyRequest.freshStream(), output, context); + doHandleRequest(proxyRequest.freshStream(), output, context, request); return; } io.opentelemetry.context.Context otelContext = instrumenter.start(parentContext, request); + Throwable error = null; try (Scope ignored = otelContext.makeCurrent()) { doHandleRequest( proxyRequest.freshStream(), - new OutputStreamWrapper(output, otelContext, request, openTelemetrySdk), - context); + new OutputStreamWrapper(output, otelContext), + context, + request); } catch (Throwable t) { - instrumenter.end(otelContext, request, null, t); - LambdaUtils.forceFlush(openTelemetrySdk, flushTimeoutNanos, TimeUnit.NANOSECONDS); + error = t; throw t; + } finally { + instrumenter.end(otelContext, request, null, error); + LambdaUtils.forceFlush(openTelemetrySdk, flushTimeoutNanos, TimeUnit.NANOSECONDS); } } + protected AwsLambdaRequest createRequest( + InputStream input, Context context, ApiGatewayProxyRequest proxyRequest) throws IOException { + return AwsLambdaRequest.create(context, proxyRequest, proxyRequest.getHeaders()); + } + + protected void doHandleRequest( + InputStream input, OutputStream output, Context context, AwsLambdaRequest request) + throws IOException { + doHandleRequest(input, output, context); + } + protected abstract void doHandleRequest(InputStream input, OutputStream output, Context context) throws IOException; - private class OutputStreamWrapper extends OutputStream { + private static class OutputStreamWrapper extends OutputStream { private final OutputStream delegate; private final io.opentelemetry.context.Context otelContext; - private final AwsLambdaRequest request; - private final OpenTelemetrySdk openTelemetrySdk; private OutputStreamWrapper( - OutputStream delegate, - io.opentelemetry.context.Context otelContext, - AwsLambdaRequest request, - OpenTelemetrySdk openTelemetrySdk) { + OutputStream delegate, io.opentelemetry.context.Context otelContext) { this.delegate = delegate; this.otelContext = otelContext; - this.request = request; - this.openTelemetrySdk = openTelemetrySdk; } @Override @@ -135,8 +142,8 @@ public void flush() throws IOException { @Override public void close() throws IOException { delegate.close(); - instrumenter.end(otelContext, request, null, null); - LambdaUtils.forceFlush(openTelemetrySdk, flushTimeoutNanos, TimeUnit.NANOSECONDS); + Span span = Span.fromContext(otelContext); + span.addEvent("Output stream closed"); } } } diff --git a/instrumentation/aws-lambda/aws-lambda-core-1.0/library/src/main/java/io/opentelemetry/instrumentation/awslambdacore/v1_0/TracingRequestStreamWrapper.java b/instrumentation/aws-lambda/aws-lambda-core-1.0/library/src/main/java/io/opentelemetry/instrumentation/awslambdacore/v1_0/TracingRequestStreamWrapper.java index 18ceb38cbf1b..d20564ffc805 100644 --- a/instrumentation/aws-lambda/aws-lambda-core-1.0/library/src/main/java/io/opentelemetry/instrumentation/awslambdacore/v1_0/TracingRequestStreamWrapper.java +++ b/instrumentation/aws-lambda/aws-lambda-core-1.0/library/src/main/java/io/opentelemetry/instrumentation/awslambdacore/v1_0/TracingRequestStreamWrapper.java @@ -23,7 +23,7 @@ */ public class TracingRequestStreamWrapper extends TracingRequestStreamHandler { - private final WrappedLambda wrappedLambda; + protected final WrappedLambda wrappedLambda; public TracingRequestStreamWrapper() { this( @@ -32,7 +32,8 @@ public TracingRequestStreamWrapper() { } // Visible for testing - TracingRequestStreamWrapper(OpenTelemetrySdk openTelemetrySdk, WrappedLambda wrappedLambda) { + protected TracingRequestStreamWrapper( + OpenTelemetrySdk openTelemetrySdk, WrappedLambda wrappedLambda) { super(openTelemetrySdk, WrapperConfiguration.flushTimeout()); this.wrappedLambda = wrappedLambda; } diff --git a/instrumentation/aws-lambda/aws-lambda-core-1.0/library/src/main/java/io/opentelemetry/instrumentation/awslambdacore/v1_0/internal/AwsLambdaFunctionAttributesExtractor.java b/instrumentation/aws-lambda/aws-lambda-core-1.0/library/src/main/java/io/opentelemetry/instrumentation/awslambdacore/v1_0/internal/AwsLambdaFunctionAttributesExtractor.java index c7a3c5745bfa..d2d4569563e6 100644 --- a/instrumentation/aws-lambda/aws-lambda-core-1.0/library/src/main/java/io/opentelemetry/instrumentation/awslambdacore/v1_0/internal/AwsLambdaFunctionAttributesExtractor.java +++ b/instrumentation/aws-lambda/aws-lambda-core-1.0/library/src/main/java/io/opentelemetry/instrumentation/awslambdacore/v1_0/internal/AwsLambdaFunctionAttributesExtractor.java @@ -5,11 +5,8 @@ package io.opentelemetry.instrumentation.awslambdacore.v1_0.internal; -import static io.opentelemetry.semconv.resource.attributes.ResourceAttributes.CLOUD_ACCOUNT_ID; -import static io.opentelemetry.semconv.resource.attributes.ResourceAttributes.CLOUD_RESOURCE_ID; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.FAAS_INVOCATION_ID; - import com.amazonaws.services.lambda.runtime.Context; +import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.common.AttributesBuilder; import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; import io.opentelemetry.instrumentation.awslambdacore.v1_0.AwsLambdaRequest; @@ -25,6 +22,15 @@ public final class AwsLambdaFunctionAttributesExtractor implements AttributesExtractor { + // copied from FaasIncubatingAttributes + private static final AttributeKey FAAS_INVOCATION_ID = + AttributeKey.stringKey("faas.invocation_id"); + // copied from CloudIncubatingAttributes + private static final AttributeKey CLOUD_ACCOUNT_ID = + AttributeKey.stringKey("cloud.account.id"); + private static final AttributeKey CLOUD_RESOURCE_ID = + AttributeKey.stringKey("cloud.resource_id"); + @Nullable private static final MethodHandle GET_FUNCTION_ARN; static { diff --git a/instrumentation/aws-lambda/aws-lambda-core-1.0/library/src/main/java/io/opentelemetry/instrumentation/awslambdacore/v1_0/internal/AwsLambdaFunctionInstrumenter.java b/instrumentation/aws-lambda/aws-lambda-core-1.0/library/src/main/java/io/opentelemetry/instrumentation/awslambdacore/v1_0/internal/AwsLambdaFunctionInstrumenter.java index 4136e7bed954..9341bf6f79dc 100644 --- a/instrumentation/aws-lambda/aws-lambda-core-1.0/library/src/main/java/io/opentelemetry/instrumentation/awslambdacore/v1_0/internal/AwsLambdaFunctionInstrumenter.java +++ b/instrumentation/aws-lambda/aws-lambda-core-1.0/library/src/main/java/io/opentelemetry/instrumentation/awslambdacore/v1_0/internal/AwsLambdaFunctionInstrumenter.java @@ -11,6 +11,7 @@ import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; import io.opentelemetry.instrumentation.api.internal.ContextPropagationDebug; import io.opentelemetry.instrumentation.awslambdacore.v1_0.AwsLambdaRequest; +import java.util.HashMap; import java.util.Locale; import java.util.Map; import javax.annotation.Nullable; @@ -48,11 +49,20 @@ public void end( public Context extract(AwsLambdaRequest input) { ContextPropagationDebug.debugContextLeakIfEnabled(); + // Look in both the http headers and the custom client context + Map headers = input.getHeaders(); + if (input.getAwsContext() != null && input.getAwsContext().getClientContext() != null) { + Map customContext = input.getAwsContext().getClientContext().getCustom(); + if (customContext != null) { + headers = new HashMap<>(headers); + headers.putAll(customContext); + } + } return openTelemetry .getPropagators() .getTextMapPropagator() - .extract(Context.root(), input.getHeaders(), MapGetter.INSTANCE); + .extract(Context.root(), headers, MapGetter.INSTANCE); } private enum MapGetter implements TextMapGetter> { diff --git a/instrumentation/aws-lambda/aws-lambda-core-1.0/library/src/main/java/io/opentelemetry/instrumentation/awslambdacore/v1_0/internal/AwsLambdaFunctionInstrumenterFactory.java b/instrumentation/aws-lambda/aws-lambda-core-1.0/library/src/main/java/io/opentelemetry/instrumentation/awslambdacore/v1_0/internal/AwsLambdaFunctionInstrumenterFactory.java index aeb828b8e743..277c358ca8e2 100644 --- a/instrumentation/aws-lambda/aws-lambda-core-1.0/library/src/main/java/io/opentelemetry/instrumentation/awslambdacore/v1_0/internal/AwsLambdaFunctionInstrumenterFactory.java +++ b/instrumentation/aws-lambda/aws-lambda-core-1.0/library/src/main/java/io/opentelemetry/instrumentation/awslambdacore/v1_0/internal/AwsLambdaFunctionInstrumenterFactory.java @@ -23,7 +23,6 @@ public static AwsLambdaFunctionInstrumenter createInstrumenter(OpenTelemetry ope openTelemetry, "io.opentelemetry.aws-lambda-core-1.0", AwsLambdaFunctionInstrumenterFactory::spanName) - .addSpanLinksExtractor(new AwsXrayEnvSpanLinksExtractor()) .addAttributesExtractor(new AwsLambdaFunctionAttributesExtractor()) .buildInstrumenter(SpanKindExtractor.alwaysServer())); } diff --git a/instrumentation/aws-lambda/aws-lambda-core-1.0/library/src/main/java/io/opentelemetry/instrumentation/awslambdacore/v1_0/internal/AwsXrayEnvSpanLinksExtractor.java b/instrumentation/aws-lambda/aws-lambda-core-1.0/library/src/main/java/io/opentelemetry/instrumentation/awslambdacore/v1_0/internal/AwsXrayEnvSpanLinksExtractor.java deleted file mode 100644 index c88cf20c917d..000000000000 --- a/instrumentation/aws-lambda/aws-lambda-core-1.0/library/src/main/java/io/opentelemetry/instrumentation/awslambdacore/v1_0/internal/AwsXrayEnvSpanLinksExtractor.java +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.awslambdacore.v1_0.internal; - -import io.opentelemetry.api.common.AttributeKey; -import io.opentelemetry.api.common.Attributes; -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.contrib.awsxray.propagator.AwsXrayPropagator; -import io.opentelemetry.instrumentation.api.instrumenter.SpanLinksBuilder; -import io.opentelemetry.instrumentation.api.instrumenter.SpanLinksExtractor; -import io.opentelemetry.instrumentation.awslambdacore.v1_0.AwsLambdaRequest; -import java.util.Collections; -import java.util.Locale; -import java.util.Map; - -/** - * This class is internal and is hence not for public use. Its APIs are unstable and can change at - * any time. - */ -final class AwsXrayEnvSpanLinksExtractor implements SpanLinksExtractor { - - private static final String AWS_TRACE_HEADER_ENV_KEY = "_X_AMZN_TRACE_ID"; - private static final String AWS_TRACE_HEADER_PROP = "com.amazonaws.xray.traceHeader"; - // lower-case map getter used for extraction - private static final String AWS_TRACE_HEADER_PROPAGATOR_KEY = "x-amzn-trace-id"; - - private static final Attributes LINK_ATTRIBUTES = - Attributes.of(AttributeKey.stringKey("source"), "x-ray-env"); - - @Override - public void extract( - SpanLinksBuilder spanLinks, - io.opentelemetry.context.Context parentContext, - AwsLambdaRequest awsLambdaRequest) { - extract(spanLinks); - } - - public static void extract(SpanLinksBuilder spanLinks) { - Map contextMap = getTraceHeaderMap(); - if (contextMap.isEmpty()) { - return; - } - Context xrayContext = - AwsXrayPropagator.getInstance().extract(Context.root(), contextMap, MapGetter.INSTANCE); - SpanContext envVarSpanCtx = Span.fromContext(xrayContext).getSpanContext(); - if (envVarSpanCtx.isValid()) { - spanLinks.addLink(envVarSpanCtx, LINK_ATTRIBUTES); - } - } - - private static Map getTraceHeaderMap() { - String traceHeader = System.getProperty(AWS_TRACE_HEADER_PROP); - if (isEmptyOrNull(traceHeader)) { - traceHeader = System.getenv(AWS_TRACE_HEADER_ENV_KEY); - } - return isEmptyOrNull(traceHeader) - ? Collections.emptyMap() - : Collections.singletonMap(AWS_TRACE_HEADER_PROPAGATOR_KEY, traceHeader); - } - - private static boolean isEmptyOrNull(String value) { - return value == null || value.isEmpty(); - } - - private enum MapGetter implements TextMapGetter> { - INSTANCE; - - @Override - public Iterable keys(Map map) { - return map.keySet(); - } - - @Override - public String get(Map map, String s) { - return map.get(s.toLowerCase(Locale.ROOT)); - } - } -} diff --git a/instrumentation/aws-lambda/aws-lambda-core-1.0/library/src/main/java/io/opentelemetry/instrumentation/awslambdacore/v1_0/internal/HeadersFactory.java b/instrumentation/aws-lambda/aws-lambda-core-1.0/library/src/main/java/io/opentelemetry/instrumentation/awslambdacore/v1_0/internal/HeadersFactory.java index 590b887683c3..75ba3c6f2518 100644 --- a/instrumentation/aws-lambda/aws-lambda-core-1.0/library/src/main/java/io/opentelemetry/instrumentation/awslambdacore/v1_0/internal/HeadersFactory.java +++ b/instrumentation/aws-lambda/aws-lambda-core-1.0/library/src/main/java/io/opentelemetry/instrumentation/awslambdacore/v1_0/internal/HeadersFactory.java @@ -32,7 +32,7 @@ static Map ofStream(InputStream inputStream) { } while (parser.nextToken() != JsonToken.END_OBJECT) { parser.nextToken(); - if (!parser.getCurrentName().equals("headers")) { + if (!parser.currentName().equals("headers")) { parser.skipChildren(); continue; } @@ -46,7 +46,7 @@ static Map ofStream(InputStream inputStream) { while (parser.nextToken() != JsonToken.END_OBJECT) { String value = parser.nextTextValue(); if (value != null) { - headers.put(parser.getCurrentName(), value); + headers.put(parser.currentName(), value); } } return headers; diff --git a/instrumentation/aws-lambda/aws-lambda-core-1.0/library/src/test/java/io/opentelemetry/instrumentation/awslambdacore/v1_0/AwsLambdaStreamWrapperHttpPropagationTest.java b/instrumentation/aws-lambda/aws-lambda-core-1.0/library/src/test/java/io/opentelemetry/instrumentation/awslambdacore/v1_0/AwsLambdaStreamWrapperHttpPropagationTest.java index 13180d58ca68..45818d860558 100644 --- a/instrumentation/aws-lambda/aws-lambda-core-1.0/library/src/test/java/io/opentelemetry/instrumentation/awslambdacore/v1_0/AwsLambdaStreamWrapperHttpPropagationTest.java +++ b/instrumentation/aws-lambda/aws-lambda-core-1.0/library/src/test/java/io/opentelemetry/instrumentation/awslambdacore/v1_0/AwsLambdaStreamWrapperHttpPropagationTest.java @@ -20,8 +20,8 @@ import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; import io.opentelemetry.instrumentation.testing.junit.LibraryInstrumentationExtension; import io.opentelemetry.sdk.trace.data.StatusData; -import io.opentelemetry.semconv.resource.attributes.ResourceAttributes; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.opentelemetry.semconv.incubating.CloudIncubatingAttributes; +import io.opentelemetry.semconv.incubating.FaasIncubatingAttributes; import java.io.BufferedWriter; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; @@ -91,10 +91,10 @@ void handlerTraced() throws Exception { .hasParentSpanId("0000000000000456") .hasAttributesSatisfyingExactly( equalTo( - ResourceAttributes.CLOUD_RESOURCE_ID, + CloudIncubatingAttributes.CLOUD_RESOURCE_ID, "arn:aws:lambda:us-east-1:123456789:function:test"), - equalTo(ResourceAttributes.CLOUD_ACCOUNT_ID, "123456789"), - equalTo(SemanticAttributes.FAAS_INVOCATION_ID, "1-22-333")))); + equalTo(CloudIncubatingAttributes.CLOUD_ACCOUNT_ID, "123456789"), + equalTo(FaasIncubatingAttributes.FAAS_INVOCATION_ID, "1-22-333")))); } @Test @@ -128,10 +128,10 @@ void handlerTracedWithException() { .hasException(thrown) .hasAttributesSatisfyingExactly( equalTo( - ResourceAttributes.CLOUD_RESOURCE_ID, + CloudIncubatingAttributes.CLOUD_RESOURCE_ID, "arn:aws:lambda:us-east-1:123456789:function:test"), - equalTo(ResourceAttributes.CLOUD_ACCOUNT_ID, "123456789"), - equalTo(SemanticAttributes.FAAS_INVOCATION_ID, "1-22-333")))); + equalTo(CloudIncubatingAttributes.CLOUD_ACCOUNT_ID, "123456789"), + equalTo(FaasIncubatingAttributes.FAAS_INVOCATION_ID, "1-22-333")))); } public static final class TestRequestHandler implements RequestStreamHandler { @@ -146,7 +146,7 @@ public void handleRequest(InputStream input, OutputStream output, Context contex parser.nextToken(); while (parser.nextToken() != JsonToken.END_OBJECT) { parser.nextToken(); - if (!parser.getCurrentName().equals("body")) { + if (!parser.currentName().equals("body")) { parser.skipChildren(); continue; } diff --git a/instrumentation/aws-lambda/aws-lambda-core-1.0/library/src/test/java/io/opentelemetry/instrumentation/awslambdacore/v1_0/AwsLambdaStreamWrapperTest.java b/instrumentation/aws-lambda/aws-lambda-core-1.0/library/src/test/java/io/opentelemetry/instrumentation/awslambdacore/v1_0/AwsLambdaStreamWrapperTest.java index bdb005c740a2..71ac8db1ed8c 100644 --- a/instrumentation/aws-lambda/aws-lambda-core-1.0/library/src/test/java/io/opentelemetry/instrumentation/awslambdacore/v1_0/AwsLambdaStreamWrapperTest.java +++ b/instrumentation/aws-lambda/aws-lambda-core-1.0/library/src/test/java/io/opentelemetry/instrumentation/awslambdacore/v1_0/AwsLambdaStreamWrapperTest.java @@ -17,8 +17,8 @@ import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; import io.opentelemetry.instrumentation.testing.junit.LibraryInstrumentationExtension; import io.opentelemetry.sdk.trace.data.StatusData; -import io.opentelemetry.semconv.resource.attributes.ResourceAttributes; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.opentelemetry.semconv.incubating.CloudIncubatingAttributes; +import io.opentelemetry.semconv.incubating.FaasIncubatingAttributes; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.ByteArrayInputStream; @@ -81,10 +81,10 @@ void handlerTraced() throws Exception { .hasKind(SpanKind.SERVER) .hasAttributesSatisfyingExactly( equalTo( - ResourceAttributes.CLOUD_RESOURCE_ID, + CloudIncubatingAttributes.CLOUD_RESOURCE_ID, "arn:aws:lambda:us-east-1:123456789:function:test"), - equalTo(ResourceAttributes.CLOUD_ACCOUNT_ID, "123456789"), - equalTo(SemanticAttributes.FAAS_INVOCATION_ID, "1-22-333")))); + equalTo(CloudIncubatingAttributes.CLOUD_ACCOUNT_ID, "123456789"), + equalTo(FaasIncubatingAttributes.FAAS_INVOCATION_ID, "1-22-333")))); } @Test @@ -109,10 +109,10 @@ void handlerTracedWithException() { .hasException(thrown) .hasAttributesSatisfyingExactly( equalTo( - ResourceAttributes.CLOUD_RESOURCE_ID, + CloudIncubatingAttributes.CLOUD_RESOURCE_ID, "arn:aws:lambda:us-east-1:123456789:function:test"), - equalTo(ResourceAttributes.CLOUD_ACCOUNT_ID, "123456789"), - equalTo(SemanticAttributes.FAAS_INVOCATION_ID, "1-22-333")))); + equalTo(CloudIncubatingAttributes.CLOUD_ACCOUNT_ID, "123456789"), + equalTo(FaasIncubatingAttributes.FAAS_INVOCATION_ID, "1-22-333")))); } public static final class TestRequestHandler implements RequestStreamHandler { diff --git a/instrumentation/aws-lambda/aws-lambda-core-1.0/library/src/test/java/io/opentelemetry/instrumentation/awslambdacore/v1_0/internal/AwsXrayEnvSpanLinksExtractorTest.java b/instrumentation/aws-lambda/aws-lambda-core-1.0/library/src/test/java/io/opentelemetry/instrumentation/awslambdacore/v1_0/internal/AwsXrayEnvSpanLinksExtractorTest.java deleted file mode 100644 index 509bfbd05eaf..000000000000 --- a/instrumentation/aws-lambda/aws-lambda-core-1.0/library/src/test/java/io/opentelemetry/instrumentation/awslambdacore/v1_0/internal/AwsXrayEnvSpanLinksExtractorTest.java +++ /dev/null @@ -1,128 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.awslambdacore.v1_0.internal; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoInteractions; - -import io.opentelemetry.api.common.AttributeKey; -import io.opentelemetry.api.common.Attributes; -import io.opentelemetry.api.trace.SpanContext; -import io.opentelemetry.instrumentation.api.instrumenter.SpanLinksBuilder; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.ArgumentCaptor; -import uk.org.webcompere.systemstubs.environment.EnvironmentVariables; -import uk.org.webcompere.systemstubs.jupiter.SystemStub; -import uk.org.webcompere.systemstubs.jupiter.SystemStubsExtension; -import uk.org.webcompere.systemstubs.properties.SystemProperties; - -/** - * This class is internal and is hence not for public use. Its APIs are unstable and can change at - * any time. - */ -@ExtendWith(SystemStubsExtension.class) -class AwsXrayEnvSpanLinksExtractorTest { - private static final Attributes EXPECTED_LINK_ATTRIBUTES = - Attributes.of(AttributeKey.stringKey("source"), "x-ray-env"); - - @SystemStub final EnvironmentVariables environmentVariables = new EnvironmentVariables(); - @SystemStub final SystemProperties systemProperties = new SystemProperties(); - - @Test - void shouldIgnoreIfEnvVarAndSystemPropertyEmpty() { - // given - SpanLinksBuilder spanLinksBuilder = mock(SpanLinksBuilder.class); - environmentVariables.set("_X_AMZN_TRACE_ID", ""); - systemProperties.set("com.amazonaws.xray.traceHeader", ""); - // when - AwsXrayEnvSpanLinksExtractor.extract(spanLinksBuilder); - // then - verifyNoInteractions(spanLinksBuilder); - } - - @Test - void shouldLinkAwsParentHeaderAndChooseSystemPropertyIfValidAndNotSampled() { - // given - SpanLinksBuilder spanLinksBuilder = mock(SpanLinksBuilder.class); - environmentVariables.set( - "_X_AMZN_TRACE_ID", - "Root=1-8a3c60f7-d188f8fa79d48a391a778fa6;Parent=0000000000000456;Sampled=0"); - systemProperties.set( - "com.amazonaws.xray.traceHeader", - "Root=1-8a3c60f7-d188f8fa79d48a391a778fa7;Parent=0000000000000789;Sampled=0"); - // when - AwsXrayEnvSpanLinksExtractor.extract(spanLinksBuilder); - // then - ArgumentCaptor captor = ArgumentCaptor.forClass(SpanContext.class); - verify(spanLinksBuilder).addLink(captor.capture(), eq(EXPECTED_LINK_ATTRIBUTES)); - SpanContext spanContext = captor.getValue(); - assertThat(spanContext.isValid()).isTrue(); - assertThat(spanContext.isSampled()).isFalse(); - assertThat(spanContext.getSpanId()).isEqualTo("0000000000000789"); - assertThat(spanContext.getTraceId()).isEqualTo("8a3c60f7d188f8fa79d48a391a778fa7"); - } - - @Test - void shouldLinkAwsParentHeaderIfValidAndNotSampled() { - // given - SpanLinksBuilder spanLinksBuilder = mock(SpanLinksBuilder.class); - environmentVariables.set( - "_X_AMZN_TRACE_ID", - "Root=1-8a3c60f7-d188f8fa79d48a391a778fa6;Parent=0000000000000456;Sampled=0"); - // when - AwsXrayEnvSpanLinksExtractor.extract(spanLinksBuilder); - // then - ArgumentCaptor captor = ArgumentCaptor.forClass(SpanContext.class); - verify(spanLinksBuilder).addLink(captor.capture(), eq(EXPECTED_LINK_ATTRIBUTES)); - SpanContext spanContext = captor.getValue(); - assertThat(spanContext.isValid()).isTrue(); - assertThat(spanContext.isSampled()).isFalse(); - assertThat(spanContext.getSpanId()).isEqualTo("0000000000000456"); - assertThat(spanContext.getTraceId()).isEqualTo("8a3c60f7d188f8fa79d48a391a778fa6"); - } - - @Test - void shouldLinkAwsParentHeaderIfValidAndNotSampledSystemProperty() { - // given - SpanLinksBuilder spanLinksBuilder = mock(SpanLinksBuilder.class); - systemProperties.set( - "com.amazonaws.xray.traceHeader", - "Root=1-8a3c60f7-d188f8fa79d48a391a778fa6;Parent=0000000000000456;Sampled=0"); - // when - AwsXrayEnvSpanLinksExtractor.extract(spanLinksBuilder); - // then - ArgumentCaptor captor = ArgumentCaptor.forClass(SpanContext.class); - verify(spanLinksBuilder).addLink(captor.capture(), eq(EXPECTED_LINK_ATTRIBUTES)); - SpanContext spanContext = captor.getValue(); - assertThat(spanContext.isValid()).isTrue(); - assertThat(spanContext.isSampled()).isFalse(); - assertThat(spanContext.getSpanId()).isEqualTo("0000000000000456"); - assertThat(spanContext.getTraceId()).isEqualTo("8a3c60f7d188f8fa79d48a391a778fa6"); - } - - @Test - void shouldLinkAwsParentHeaderIfValidAndSampledSystemProperty() { - // given - SpanLinksBuilder spanLinksBuilder = mock(SpanLinksBuilder.class); - systemProperties.set( - "com.amazonaws.xray.traceHeader", - "Root=1-8a3c60f7-d188f8fa79d48a391a778fa6;Parent=0000000000000456;Sampled=1"); - // when - AwsXrayEnvSpanLinksExtractor.extract(spanLinksBuilder); - // then - ArgumentCaptor captor = ArgumentCaptor.forClass(SpanContext.class); - verify(spanLinksBuilder).addLink(captor.capture(), eq(EXPECTED_LINK_ATTRIBUTES)); - SpanContext spanContext = captor.getValue(); - assertThat(spanContext.isValid()).isTrue(); - assertThat(spanContext.isSampled()).isTrue(); - assertThat(spanContext.getSpanId()).isEqualTo("0000000000000456"); - assertThat(spanContext.getTraceId()).isEqualTo("8a3c60f7d188f8fa79d48a391a778fa6"); - } -} diff --git a/instrumentation/aws-lambda/aws-lambda-core-1.0/library/src/test/java/io/opentelemetry/instrumentation/awslambdacore/v1_0/internal/InstrumenterExtractionTest.java b/instrumentation/aws-lambda/aws-lambda-core-1.0/library/src/test/java/io/opentelemetry/instrumentation/awslambdacore/v1_0/internal/InstrumenterExtractionTest.java new file mode 100644 index 000000000000..cb19d1e56879 --- /dev/null +++ b/instrumentation/aws-lambda/aws-lambda-core-1.0/library/src/test/java/io/opentelemetry/instrumentation/awslambdacore/v1_0/internal/InstrumenterExtractionTest.java @@ -0,0 +1,45 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.awslambdacore.v1_0.internal; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.amazonaws.services.lambda.runtime.ClientContext; +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.propagation.ContextPropagators; +import io.opentelemetry.instrumentation.awslambdacore.v1_0.AwsLambdaRequest; +import java.util.HashMap; +import org.junit.jupiter.api.Test; + +class InstrumenterExtractionTest { + @Test + public void useCustomContext() { + AwsLambdaFunctionInstrumenter instr = + AwsLambdaFunctionInstrumenterFactory.createInstrumenter( + OpenTelemetry.propagating( + ContextPropagators.create(W3CTraceContextPropagator.getInstance()))); + com.amazonaws.services.lambda.runtime.Context awsContext = + mock(com.amazonaws.services.lambda.runtime.Context.class); + ClientContext clientContext = mock(ClientContext.class); + when(awsContext.getClientContext()).thenReturn(clientContext); + HashMap customMap = new HashMap<>(); + customMap.put("traceparent", "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"); + when(clientContext.getCustom()).thenReturn(customMap); + + AwsLambdaRequest input = AwsLambdaRequest.create(awsContext, new HashMap<>(), new HashMap<>()); + + Context extracted = instr.extract(input); + SpanContext spanContext = Span.fromContext(extracted).getSpanContext(); + assertThat(spanContext.getTraceId()).isEqualTo("4bf92f3577b34da6a3ce929d0e0e4736"); + assertThat(spanContext.getSpanId()).isEqualTo("00f067aa0ba902b7"); + } +} diff --git a/instrumentation/aws-lambda/aws-lambda-core-1.0/testing/build.gradle.kts b/instrumentation/aws-lambda/aws-lambda-core-1.0/testing/build.gradle.kts index d035a0ffc7d9..b414e5a45350 100644 --- a/instrumentation/aws-lambda/aws-lambda-core-1.0/testing/build.gradle.kts +++ b/instrumentation/aws-lambda/aws-lambda-core-1.0/testing/build.gradle.kts @@ -12,8 +12,6 @@ dependencies { implementation("com.google.guava:guava") - implementation("org.apache.groovy:groovy") implementation("io.opentelemetry:opentelemetry-api") - implementation("org.spockframework:spock-core") implementation("com.github.stefanbirkner:system-lambda") } diff --git a/instrumentation/aws-lambda/aws-lambda-core-1.0/testing/src/main/java/io/opentelemetry/instrumentation/awslambdacore/v1_0/AbstractAwsLambdaTest.java b/instrumentation/aws-lambda/aws-lambda-core-1.0/testing/src/main/java/io/opentelemetry/instrumentation/awslambdacore/v1_0/AbstractAwsLambdaTest.java index e088efa9062b..fc47b394969c 100644 --- a/instrumentation/aws-lambda/aws-lambda-core-1.0/testing/src/main/java/io/opentelemetry/instrumentation/awslambdacore/v1_0/AbstractAwsLambdaTest.java +++ b/instrumentation/aws-lambda/aws-lambda-core-1.0/testing/src/main/java/io/opentelemetry/instrumentation/awslambdacore/v1_0/AbstractAwsLambdaTest.java @@ -12,12 +12,10 @@ import com.amazonaws.services.lambda.runtime.Context; import com.amazonaws.services.lambda.runtime.RequestHandler; -import io.opentelemetry.api.common.AttributeKey; -import io.opentelemetry.api.common.Attributes; import io.opentelemetry.api.trace.SpanKind; import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; import io.opentelemetry.sdk.trace.data.StatusData; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.opentelemetry.semconv.incubating.FaasIncubatingAttributes; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -53,6 +51,10 @@ void tearDown() { assertThat(testing().forceFlushCalled()).isTrue(); } + protected Context context() { + return context; + } + @Test void handlerTraced() { String result = handler().handleRequest("hello", context); @@ -66,7 +68,7 @@ void handlerTraced() { span.hasName("my_function") .hasKind(SpanKind.SERVER) .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.FAAS_INVOCATION_ID, "1-22-333")))); + equalTo(FaasIncubatingAttributes.FAAS_INVOCATION_ID, "1-22-333")))); } @Test @@ -84,14 +86,18 @@ void handlerTracedWithException() { .hasStatus(StatusData.error()) .hasException(thrown) .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.FAAS_INVOCATION_ID, "1-22-333")))); + equalTo(FaasIncubatingAttributes.FAAS_INVOCATION_ID, "1-22-333")))); } + /** + * For more details about active tracing see + * https://docs.aws.amazon.com/lambda/latest/dg/services-xray.html + */ @Test @SetEnvironmentVariable( key = "_X_AMZN_TRACE_ID", value = "Root=1-8a3c60f7-d188f8fa79d48a391a778fa6;Parent=0000000000000456;Sampled=1") - void handlerLinksToInfrastructureTrace() { + void handlerDoesNotLinkToActiveTracingSpan() { String result = handler().handleRequest("hello", context); assertThat(result).isEqualTo("world"); @@ -102,23 +108,9 @@ void handlerLinksToInfrastructureTrace() { span -> span.hasName("my_function") .hasKind(SpanKind.SERVER) - .hasLinksSatisfying( - links -> - assertThat(links) - .singleElement() - .satisfies( - link -> { - assertThat(link.getSpanContext().getTraceId()) - .isEqualTo("8a3c60f7d188f8fa79d48a391a778fa6"); - assertThat(link.getSpanContext().getSpanId()) - .isEqualTo("0000000000000456"); - assertThat(link.getAttributes()) - .isEqualTo( - Attributes.of( - AttributeKey.stringKey("source"), - "x-ray-env")); - })) + .hasNoParent() + .hasLinks() .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.FAAS_INVOCATION_ID, "1-22-333")))); + equalTo(FaasIncubatingAttributes.FAAS_INVOCATION_ID, "1-22-333")))); } } diff --git a/instrumentation/aws-lambda/aws-lambda-events-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/awslambdaevents/v2_2/AwsLambdaInstrumentationHelper.java b/instrumentation/aws-lambda/aws-lambda-events-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/awslambdaevents/v2_2/AwsLambdaInstrumentationHelper.java index c02754661aef..d55898e75053 100644 --- a/instrumentation/aws-lambda/aws-lambda-events-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/awslambdaevents/v2_2/AwsLambdaInstrumentationHelper.java +++ b/instrumentation/aws-lambda/aws-lambda-events-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/awslambdaevents/v2_2/AwsLambdaInstrumentationHelper.java @@ -10,13 +10,15 @@ import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; import io.opentelemetry.instrumentation.awslambdaevents.v2_2.internal.AwsLambdaEventsInstrumenterFactory; import io.opentelemetry.instrumentation.awslambdaevents.v2_2.internal.AwsLambdaSqsInstrumenterFactory; +import io.opentelemetry.javaagent.bootstrap.internal.AgentCommonConfig; public final class AwsLambdaInstrumentationHelper { private static final io.opentelemetry.instrumentation.awslambdacore.v1_0.internal .AwsLambdaFunctionInstrumenter FUNCTION_INSTRUMENTER = - AwsLambdaEventsInstrumenterFactory.createInstrumenter(GlobalOpenTelemetry.get()); + AwsLambdaEventsInstrumenterFactory.createInstrumenter( + GlobalOpenTelemetry.get(), AgentCommonConfig.get().getKnownHttpRequestMethods()); public static io.opentelemetry.instrumentation.awslambdacore.v1_0.internal .AwsLambdaFunctionInstrumenter diff --git a/instrumentation/aws-lambda/aws-lambda-events-2.2/library/README.md b/instrumentation/aws-lambda/aws-lambda-events-2.2/library/README.md index c200496bbdf0..10beb9fe7299 100644 --- a/instrumentation/aws-lambda/aws-lambda-events-2.2/library/README.md +++ b/instrumentation/aws-lambda/aws-lambda-events-2.2/library/README.md @@ -118,7 +118,7 @@ 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 ). + traces can be propagated with supported HTTP headers (see ). In order to enable requested propagation for a handler, configure it on the SDK you build. diff --git a/instrumentation/aws-lambda/aws-lambda-events-2.2/library/build.gradle.kts b/instrumentation/aws-lambda/aws-lambda-events-2.2/library/build.gradle.kts index 02899f461ec1..452d42189590 100644 --- a/instrumentation/aws-lambda/aws-lambda-events-2.2/library/build.gradle.kts +++ b/instrumentation/aws-lambda/aws-lambda-events-2.2/library/build.gradle.kts @@ -18,6 +18,13 @@ dependencies { // in public API. library("com.amazonaws:aws-lambda-java-events:2.2.1") + // By default, "aws-lambda-java-serialization" library is enabled in the classpath + // at the AWS Lambda environment except "java8" runtime which is deprecated. + // But it is available at "java8.al2" runtime, so it is still can be used + // by Java 8 based Lambda functions. + // So that is the reason that why we add it as compile only dependency. + compileOnly("com.amazonaws:aws-lambda-java-serialization:1.1.5") + // We need Jackson for wrappers to reproduce the serialization does when Lambda invokes a RequestHandler with event // since Lambda will only be able to invoke the wrapper itself with a generic Object. // Note that Lambda itself uses Jackson, but does not expose it to the function so we need to include it here. @@ -33,6 +40,7 @@ dependencies { testImplementation("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure") testImplementation("io.opentelemetry:opentelemetry-extension-trace-propagators") testImplementation("com.google.guava:guava") + testImplementation("com.amazonaws:aws-lambda-java-serialization:1.1.5") testImplementation(project(":instrumentation:aws-lambda:aws-lambda-events-2.2:testing")) testImplementation("uk.org.webcompere:system-stubs-jupiter") @@ -40,6 +48,7 @@ dependencies { tasks.withType().configureEach { // required on jdk17 + jvmArgs("--add-opens=java.base/java.lang=ALL-UNNAMED") jvmArgs("--add-opens=java.base/java.util=ALL-UNNAMED") jvmArgs("-XX:+IgnoreUnrecognizedVMOptions") } diff --git a/instrumentation/aws-lambda/aws-lambda-events-2.2/library/src/main/java/io/opentelemetry/instrumentation/awslambdaevents/v2_2/LambdaParameters.java b/instrumentation/aws-lambda/aws-lambda-events-2.2/library/src/main/java/io/opentelemetry/instrumentation/awslambdaevents/v2_2/LambdaParameters.java index ecc3a33d7003..53e68280bade 100644 --- a/instrumentation/aws-lambda/aws-lambda-events-2.2/library/src/main/java/io/opentelemetry/instrumentation/awslambdaevents/v2_2/LambdaParameters.java +++ b/instrumentation/aws-lambda/aws-lambda-events-2.2/library/src/main/java/io/opentelemetry/instrumentation/awslambdaevents/v2_2/LambdaParameters.java @@ -6,6 +6,7 @@ package io.opentelemetry.instrumentation.awslambdaevents.v2_2; import com.amazonaws.services.lambda.runtime.Context; +import java.io.InputStream; import java.lang.reflect.Method; import java.util.function.BiFunction; @@ -27,5 +28,35 @@ static Object[] toArray( return parameters; } + static Object[] toParameters(Method targetMethod, T input, Context context) { + Class[] parameterTypes = targetMethod.getParameterTypes(); + Object[] parameters = new Object[parameterTypes.length]; + for (int i = 0; i < parameterTypes.length; i++) { + Class clazz = parameterTypes[i]; + boolean isContext = clazz.equals(Context.class); + if (isContext) { + parameters[i] = context; + } else if (i == 0) { + parameters[0] = input; + } + } + return parameters; + } + + static Object toInput( + Method targetMethod, + InputStream inputStream, + BiFunction, Object> mapper) { + Class[] parameterTypes = targetMethod.getParameterTypes(); + for (int i = 0; i < parameterTypes.length; i++) { + Class clazz = parameterTypes[i]; + boolean isContext = clazz.equals(Context.class); + if (i == 0 && !isContext) { + return mapper.apply(inputStream, clazz); + } + } + return null; + } + private LambdaParameters() {} } diff --git a/instrumentation/aws-lambda/aws-lambda-events-2.2/library/src/main/java/io/opentelemetry/instrumentation/awslambdaevents/v2_2/TracingRequestApiGatewayWrapper.java b/instrumentation/aws-lambda/aws-lambda-events-2.2/library/src/main/java/io/opentelemetry/instrumentation/awslambdaevents/v2_2/TracingRequestApiGatewayWrapper.java index 00eb6e88e53b..711b7e3079a0 100644 --- a/instrumentation/aws-lambda/aws-lambda-events-2.2/library/src/main/java/io/opentelemetry/instrumentation/awslambdaevents/v2_2/TracingRequestApiGatewayWrapper.java +++ b/instrumentation/aws-lambda/aws-lambda-events-2.2/library/src/main/java/io/opentelemetry/instrumentation/awslambdaevents/v2_2/TracingRequestApiGatewayWrapper.java @@ -8,8 +8,8 @@ import com.amazonaws.services.lambda.runtime.Context; import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent; import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent; -import com.fasterxml.jackson.core.JsonProcessingException; import io.opentelemetry.instrumentation.awslambdacore.v1_0.internal.WrappedLambda; +import io.opentelemetry.instrumentation.awslambdaevents.v2_2.internal.SerializationUtil; import io.opentelemetry.sdk.OpenTelemetrySdk; import java.util.function.BiFunction; @@ -35,12 +35,7 @@ public TracingRequestApiGatewayWrapper() { // Visible for testing static T map(APIGatewayProxyRequestEvent event, Class clazz) { - try { - return OBJECT_MAPPER.readValue(event.getBody(), clazz); - } catch (JsonProcessingException e) { - throw new IllegalStateException( - "Could not map API Gateway event body to requested parameter type: " + clazz, e); - } + return SerializationUtil.fromJson(event.getBody(), clazz); } @Override @@ -52,12 +47,8 @@ protected APIGatewayProxyResponseEvent doHandleRequest( if (result instanceof APIGatewayProxyResponseEvent) { event = (APIGatewayProxyResponseEvent) result; } else { - try { - event = new APIGatewayProxyResponseEvent(); - event.setBody(OBJECT_MAPPER.writeValueAsString(result)); - } catch (JsonProcessingException e) { - throw new IllegalStateException("Could not serialize return value.", e); - } + event = new APIGatewayProxyResponseEvent(); + event.setBody(SerializationUtil.toJson(result)); } return event; } diff --git a/instrumentation/aws-lambda/aws-lambda-events-2.2/library/src/main/java/io/opentelemetry/instrumentation/awslambdaevents/v2_2/TracingRequestWrapper.java b/instrumentation/aws-lambda/aws-lambda-events-2.2/library/src/main/java/io/opentelemetry/instrumentation/awslambdaevents/v2_2/TracingRequestWrapper.java index 5cbeec41cfee..484ee169d623 100644 --- a/instrumentation/aws-lambda/aws-lambda-events-2.2/library/src/main/java/io/opentelemetry/instrumentation/awslambdaevents/v2_2/TracingRequestWrapper.java +++ b/instrumentation/aws-lambda/aws-lambda-events-2.2/library/src/main/java/io/opentelemetry/instrumentation/awslambdaevents/v2_2/TracingRequestWrapper.java @@ -5,32 +5,90 @@ package io.opentelemetry.instrumentation.awslambdaevents.v2_2; +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent; +import io.opentelemetry.instrumentation.awslambdacore.v1_0.AwsLambdaRequest; +import io.opentelemetry.instrumentation.awslambdacore.v1_0.TracingRequestStreamWrapper; +import io.opentelemetry.instrumentation.awslambdacore.v1_0.internal.ApiGatewayProxyRequest; +import io.opentelemetry.instrumentation.awslambdacore.v1_0.internal.MapUtils; import io.opentelemetry.instrumentation.awslambdacore.v1_0.internal.WrappedLambda; +import io.opentelemetry.instrumentation.awslambdaevents.v2_2.internal.SerializationUtil; import io.opentelemetry.sdk.OpenTelemetrySdk; -import java.util.function.BiFunction; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Collections; +import java.util.Map; /** - * Wrapper for {@link io.opentelemetry.instrumentation.awslambdacore.v1_0.TracingRequestHandler}. - * Allows for wrapping a regular lambda, not proxied through API Gateway. Therefore, HTTP headers - * propagation is not supported. + * Wrapper for {@link com.amazonaws.services.lambda.runtime.RequestHandler} based Lambda handlers. */ -public class TracingRequestWrapper extends TracingRequestWrapperBase { +public class TracingRequestWrapper extends TracingRequestStreamWrapper { public TracingRequestWrapper() { - super(TracingRequestWrapper::map); + super(); } // Visible for testing - TracingRequestWrapper( - OpenTelemetrySdk openTelemetrySdk, - WrappedLambda wrappedLambda, - BiFunction, Object> mapper) { - super(openTelemetrySdk, wrappedLambda, mapper); + TracingRequestWrapper(OpenTelemetrySdk openTelemetrySdk, WrappedLambda wrappedLambda) { + super(openTelemetrySdk, wrappedLambda); + } + + @Override + protected final AwsLambdaRequest createRequest( + InputStream inputStream, Context context, ApiGatewayProxyRequest proxyRequest) { + Method targetMethod = wrappedLambda.getRequestTargetMethod(); + Object input = LambdaParameters.toInput(targetMethod, inputStream, TracingRequestWrapper::map); + return AwsLambdaRequest.create(context, input, extractHeaders(input)); + } + + protected Map extractHeaders(Object input) { + if (input instanceof APIGatewayProxyRequestEvent) { + return MapUtils.emptyIfNull(((APIGatewayProxyRequestEvent) input).getHeaders()); + } + return Collections.emptyMap(); + } + + @Override + protected final void doHandleRequest( + InputStream input, OutputStream output, Context context, AwsLambdaRequest request) { + Method targetMethod = wrappedLambda.getRequestTargetMethod(); + Object[] parameters = LambdaParameters.toParameters(targetMethod, request.getInput(), context); + try { + Object result = targetMethod.invoke(wrappedLambda.getTargetObject(), parameters); + SerializationUtil.toJson(output, result); + } 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())); + } + } + + @SuppressWarnings({"unchecked", "TypeParameterUnusedInFormals"}) + // Used for testing + OUTPUT handleRequest(INPUT input, Context context) throws IOException { + byte[] inputJsonData = SerializationUtil.toJsonData(input); + ByteArrayInputStream inputStream = new ByteArrayInputStream(inputJsonData); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + + super.handleRequest(inputStream, outputStream, context); + + byte[] outputJsonData = outputStream.toByteArray(); + return (OUTPUT) + SerializationUtil.fromJson( + new ByteArrayInputStream(outputJsonData), + wrappedLambda.getRequestTargetMethod().getReturnType()); } // Visible for testing - static T map(Object jsonMap, Class clazz) { + static T map(InputStream inputStream, Class clazz) { try { - return OBJECT_MAPPER.convertValue(jsonMap, clazz); + return SerializationUtil.fromJson(inputStream, clazz); } catch (IllegalArgumentException e) { throw new IllegalStateException( "Could not map input to requested parameter type: " + clazz, e); diff --git a/instrumentation/aws-lambda/aws-lambda-events-2.2/library/src/main/java/io/opentelemetry/instrumentation/awslambdaevents/v2_2/TracingRequestWrapperBase.java b/instrumentation/aws-lambda/aws-lambda-events-2.2/library/src/main/java/io/opentelemetry/instrumentation/awslambdaevents/v2_2/TracingRequestWrapperBase.java index 058432f50f84..5169c8f121ee 100644 --- a/instrumentation/aws-lambda/aws-lambda-events-2.2/library/src/main/java/io/opentelemetry/instrumentation/awslambdaevents/v2_2/TracingRequestWrapperBase.java +++ b/instrumentation/aws-lambda/aws-lambda-events-2.2/library/src/main/java/io/opentelemetry/instrumentation/awslambdaevents/v2_2/TracingRequestWrapperBase.java @@ -7,8 +7,7 @@ import com.amazonaws.services.lambda.runtime.Context; import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent; -import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.ObjectMapper; +import io.opentelemetry.instrumentation.api.internal.HttpConstants; import io.opentelemetry.instrumentation.awslambdacore.v1_0.TracingRequestHandler; import io.opentelemetry.instrumentation.awslambdacore.v1_0.internal.MapUtils; import io.opentelemetry.instrumentation.awslambdacore.v1_0.internal.WrappedLambda; @@ -28,10 +27,6 @@ */ abstract class TracingRequestWrapperBase extends TracingRequestHandler { - protected static final ObjectMapper OBJECT_MAPPER = - new ObjectMapper() - .registerModule(new CustomJodaModule()) - .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); private final WrappedLambda wrappedLambda; private final Method targetMethod; private final BiFunction, Object> parameterMapper; @@ -51,7 +46,8 @@ protected TracingRequestWrapperBase(BiFunction, Object> parameterMap super( openTelemetrySdk, WrapperConfiguration.flushTimeout(), - AwsLambdaEventsInstrumenterFactory.createInstrumenter(openTelemetrySdk)); + AwsLambdaEventsInstrumenterFactory.createInstrumenter( + openTelemetrySdk, HttpConstants.KNOWN_METHODS)); this.wrappedLambda = wrappedLambda; this.targetMethod = wrappedLambda.getRequestTargetMethod(); this.parameterMapper = parameterMapper; diff --git a/instrumentation/aws-lambda/aws-lambda-events-2.2/library/src/main/java/io/opentelemetry/instrumentation/awslambdaevents/v2_2/internal/ApiGatewayProxyAttributesExtractor.java b/instrumentation/aws-lambda/aws-lambda-events-2.2/library/src/main/java/io/opentelemetry/instrumentation/awslambdaevents/v2_2/internal/ApiGatewayProxyAttributesExtractor.java index 2ebc14d16c6b..36f8dcd0b53e 100644 --- a/instrumentation/aws-lambda/aws-lambda-events-2.2/library/src/main/java/io/opentelemetry/instrumentation/awslambdaevents/v2_2/internal/ApiGatewayProxyAttributesExtractor.java +++ b/instrumentation/aws-lambda/aws-lambda-events-2.2/library/src/main/java/io/opentelemetry/instrumentation/awslambdaevents/v2_2/internal/ApiGatewayProxyAttributesExtractor.java @@ -5,50 +5,68 @@ package io.opentelemetry.instrumentation.awslambdaevents.v2_2.internal; +import static io.opentelemetry.instrumentation.api.internal.AttributesExtractorUtil.internalSet; +import static io.opentelemetry.instrumentation.api.internal.HttpConstants._OTHER; import static io.opentelemetry.instrumentation.awslambdacore.v1_0.internal.MapUtils.emptyIfNull; import static io.opentelemetry.instrumentation.awslambdacore.v1_0.internal.MapUtils.lowercaseMap; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.FAAS_TRIGGER; -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.USER_AGENT_ORIGINAL; +import static io.opentelemetry.semconv.HttpAttributes.HTTP_RESPONSE_STATUS_CODE; +import static io.opentelemetry.semconv.UserAgentAttributes.USER_AGENT_ORIGINAL; import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent; import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent; +import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.common.AttributesBuilder; import io.opentelemetry.context.Context; import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; import io.opentelemetry.instrumentation.awslambdacore.v1_0.AwsLambdaRequest; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.opentelemetry.semconv.HttpAttributes; +import io.opentelemetry.semconv.UrlAttributes; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.util.Map; +import java.util.Set; import javax.annotation.Nullable; final class ApiGatewayProxyAttributesExtractor implements AttributesExtractor { + + // copied from FaasIncubatingAttributes + private static final AttributeKey FAAS_TRIGGER = AttributeKey.stringKey("faas.trigger"); + // copied from FaasIncubatingAttributes.FaasTriggerValues + private static final String HTTP = "http"; + + private final Set knownMethods; + + ApiGatewayProxyAttributesExtractor(Set knownMethods) { + this.knownMethods = knownMethods; + } + @Override public void onStart( AttributesBuilder attributes, Context parentContext, AwsLambdaRequest request) { if (request.getInput() instanceof APIGatewayProxyRequestEvent) { - attributes.put(FAAS_TRIGGER, SemanticAttributes.FaasTriggerValues.HTTP); + attributes.put(FAAS_TRIGGER, HTTP); onRequest(attributes, (APIGatewayProxyRequestEvent) request.getInput()); } } void onRequest(AttributesBuilder attributes, APIGatewayProxyRequestEvent request) { - attributes.put(HTTP_METHOD, request.getHttpMethod()); + String method = request.getHttpMethod(); + if (method == null || knownMethods.contains(method)) { + internalSet(attributes, HttpAttributes.HTTP_REQUEST_METHOD, method); + } else { + internalSet(attributes, HttpAttributes.HTTP_REQUEST_METHOD, _OTHER); + internalSet(attributes, HttpAttributes.HTTP_REQUEST_METHOD_ORIGINAL, method); + } Map headers = lowercaseMap(request.getHeaders()); String userAgent = headers.get("user-agent"); if (userAgent != null) { attributes.put(USER_AGENT_ORIGINAL, userAgent); } - String httpUrl = getHttpUrl(request, headers); - if (httpUrl != null) { - attributes.put(HTTP_URL, httpUrl); - } + + internalSet(attributes, UrlAttributes.URL_FULL, getHttpUrl(request, headers)); } private static String getHttpUrl( @@ -93,10 +111,8 @@ public void onEnd( if (response instanceof APIGatewayProxyResponseEvent) { Integer statusCode = ((APIGatewayProxyResponseEvent) response).getStatusCode(); if (statusCode != null) { - attributes.put(HTTP_STATUS_CODE, statusCode); + attributes.put(HTTP_RESPONSE_STATUS_CODE, statusCode); } } } - - ApiGatewayProxyAttributesExtractor() {} } diff --git a/instrumentation/aws-lambda/aws-lambda-events-2.2/library/src/main/java/io/opentelemetry/instrumentation/awslambdaevents/v2_2/internal/AwsLambdaEventsInstrumenterFactory.java b/instrumentation/aws-lambda/aws-lambda-events-2.2/library/src/main/java/io/opentelemetry/instrumentation/awslambdaevents/v2_2/internal/AwsLambdaEventsInstrumenterFactory.java index 6c25f71ed401..502a8e016da8 100644 --- a/instrumentation/aws-lambda/aws-lambda-events-2.2/library/src/main/java/io/opentelemetry/instrumentation/awslambdaevents/v2_2/internal/AwsLambdaEventsInstrumenterFactory.java +++ b/instrumentation/aws-lambda/aws-lambda-events-2.2/library/src/main/java/io/opentelemetry/instrumentation/awslambdaevents/v2_2/internal/AwsLambdaEventsInstrumenterFactory.java @@ -12,6 +12,7 @@ import io.opentelemetry.instrumentation.awslambdacore.v1_0.AwsLambdaRequest; import io.opentelemetry.instrumentation.awslambdacore.v1_0.internal.AwsLambdaFunctionAttributesExtractor; import io.opentelemetry.instrumentation.awslambdacore.v1_0.internal.AwsLambdaFunctionInstrumenter; +import java.util.Set; /** * This class is internal and is hence not for public use. Its APIs are unstable and can change at @@ -19,7 +20,8 @@ */ public final class AwsLambdaEventsInstrumenterFactory { - public static AwsLambdaFunctionInstrumenter createInstrumenter(OpenTelemetry openTelemetry) { + public static AwsLambdaFunctionInstrumenter createInstrumenter( + OpenTelemetry openTelemetry, Set knownMethods) { return new AwsLambdaFunctionInstrumenter( openTelemetry, Instrumenter.builder( @@ -27,7 +29,7 @@ public static AwsLambdaFunctionInstrumenter createInstrumenter(OpenTelemetry ope "io.opentelemetry.aws-lambda-events-2.2", AwsLambdaEventsInstrumenterFactory::spanName) .addAttributesExtractor(new AwsLambdaFunctionAttributesExtractor()) - .addAttributesExtractor(new ApiGatewayProxyAttributesExtractor()) + .addAttributesExtractor(new ApiGatewayProxyAttributesExtractor(knownMethods)) .buildInstrumenter(SpanKindExtractor.alwaysServer())); } diff --git a/instrumentation/aws-lambda/aws-lambda-events-2.2/library/src/main/java/io/opentelemetry/instrumentation/awslambdaevents/v2_2/internal/AwsLambdaSqsInstrumenterFactory.java b/instrumentation/aws-lambda/aws-lambda-events-2.2/library/src/main/java/io/opentelemetry/instrumentation/awslambdaevents/v2_2/internal/AwsLambdaSqsInstrumenterFactory.java index 3433b667e0f1..4cd11fc0c4f1 100644 --- a/instrumentation/aws-lambda/aws-lambda-events-2.2/library/src/main/java/io/opentelemetry/instrumentation/awslambdaevents/v2_2/internal/AwsLambdaSqsInstrumenterFactory.java +++ b/instrumentation/aws-lambda/aws-lambda-events-2.2/library/src/main/java/io/opentelemetry/instrumentation/awslambdaevents/v2_2/internal/AwsLambdaSqsInstrumenterFactory.java @@ -10,6 +10,7 @@ import io.opentelemetry.api.OpenTelemetry; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; import io.opentelemetry.instrumentation.api.instrumenter.SpanKindExtractor; +import java.util.List; /** * This class is internal and is hence not for public use. Its APIs are unstable and can change at @@ -39,10 +40,11 @@ public static Instrumenter forMessage(OpenTelemetry openTeleme private static String spanName(SQSEvent event) { 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); + List records = event.getRecords(); + if (records != null && !records.isEmpty()) { + String messageSource = records.get(0).getEventSource(); + for (int i = 1; i < records.size(); i++) { + SQSMessage message = records.get(i); if (!message.getEventSource().equals(messageSource)) { messageSource = null; break; diff --git a/instrumentation/aws-lambda/aws-lambda-events-2.2/library/src/main/java/io/opentelemetry/instrumentation/awslambdaevents/v2_2/CustomJodaModule.java b/instrumentation/aws-lambda/aws-lambda-events-2.2/library/src/main/java/io/opentelemetry/instrumentation/awslambdaevents/v2_2/internal/CustomJodaModule.java similarity index 96% rename from instrumentation/aws-lambda/aws-lambda-events-2.2/library/src/main/java/io/opentelemetry/instrumentation/awslambdaevents/v2_2/CustomJodaModule.java rename to instrumentation/aws-lambda/aws-lambda-events-2.2/library/src/main/java/io/opentelemetry/instrumentation/awslambdaevents/v2_2/internal/CustomJodaModule.java index a1991b468145..1be598ae8ffb 100644 --- a/instrumentation/aws-lambda/aws-lambda-events-2.2/library/src/main/java/io/opentelemetry/instrumentation/awslambdaevents/v2_2/CustomJodaModule.java +++ b/instrumentation/aws-lambda/aws-lambda-events-2.2/library/src/main/java/io/opentelemetry/instrumentation/awslambdaevents/v2_2/internal/CustomJodaModule.java @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.instrumentation.awslambdaevents.v2_2; +package io.opentelemetry.instrumentation.awslambdaevents.v2_2.internal; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.databind.DeserializationContext; diff --git a/instrumentation/aws-lambda/aws-lambda-events-2.2/library/src/main/java/io/opentelemetry/instrumentation/awslambdaevents/v2_2/internal/SerializationUtil.java b/instrumentation/aws-lambda/aws-lambda-events-2.2/library/src/main/java/io/opentelemetry/instrumentation/awslambdaevents/v2_2/internal/SerializationUtil.java new file mode 100644 index 000000000000..81649340e649 --- /dev/null +++ b/instrumentation/aws-lambda/aws-lambda-events-2.2/library/src/main/java/io/opentelemetry/instrumentation/awslambdaevents/v2_2/internal/SerializationUtil.java @@ -0,0 +1,133 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.awslambdaevents.v2_2.internal; + +import com.amazonaws.services.lambda.runtime.serialization.PojoSerializer; +import com.amazonaws.services.lambda.runtime.serialization.events.LambdaEventSerializers; +import com.amazonaws.services.lambda.runtime.serialization.factories.JacksonFactory; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; + +/** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time. + */ +public final class SerializationUtil { + + private static final int DEFAULT_BUFFER_SIZE = 1024; + + private static final ClassValue> serializerCache = + new ClassValue>() { + @Override + protected PojoSerializer computeValue(Class type) { + return createSerializer(type); + } + }; + + private static PojoSerializer createSerializer(Class clazz) { + try { + if (LambdaEventSerializers.isLambdaSupportedEvent(clazz.getName())) { + return LambdaEventSerializers.serializerFor(clazz, clazz.getClassLoader()); + } + return JacksonFactory.getInstance().getSerializer(clazz); + } catch (NoClassDefFoundError e) { + // For "java8" runtime, "aws-lambda-java-serialization" library + // is not available in the classpath by default. + // So fall back to object mapper based legacy serialization. + return new ObjectMapperPojoSerializer(clazz); + } + } + + @SuppressWarnings("unchecked") + public static PojoSerializer getSerializer(Class clazz) { + return (PojoSerializer) serializerCache.get(clazz); + } + + public static T fromJson(String json, Class clazz) { + PojoSerializer serializer = getSerializer(clazz); + return serializer.fromJson(json); + } + + public static T fromJson(InputStream inputStream, Class clazz) { + PojoSerializer serializer = getSerializer(clazz); + return serializer.fromJson(inputStream); + } + + @SuppressWarnings("unchecked") + public static void toJson(OutputStream outputStream, T obj) { + if (obj != null) { + PojoSerializer serializer = getSerializer((Class) obj.getClass()); + serializer.toJson(obj, outputStream); + } + } + + @SuppressWarnings("unchecked") + public static String toJson(T obj) { + if (obj == null) { + return null; + } + PojoSerializer serializer = getSerializer((Class) obj.getClass()); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(DEFAULT_BUFFER_SIZE); + serializer.toJson(obj, outputStream); + return new String(outputStream.toByteArray(), StandardCharsets.UTF_8); + } + + public static byte[] toJsonData(T obj) { + if (obj == null) { + return new byte[] {}; + } + ByteArrayOutputStream os = new ByteArrayOutputStream(DEFAULT_BUFFER_SIZE); + SerializationUtil.toJson(os, obj); + return os.toByteArray(); + } + + private static class ObjectMapperPojoSerializer implements PojoSerializer { + + private final ObjectMapper objectMapper = + new ObjectMapper() + .registerModule(new CustomJodaModule()) + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + private final Class clazz; + + ObjectMapperPojoSerializer(Class clazz) { + this.clazz = clazz; + } + + @Override + public T fromJson(InputStream input) { + try { + return objectMapper.readValue(input, clazz); + } catch (IOException e) { + throw new IllegalStateException("Could not deserialize from JSON input stream.", e); + } + } + + @Override + public T fromJson(String input) { + try { + return objectMapper.readValue(input, clazz); + } catch (IOException e) { + throw new IllegalStateException("Could not deserialize from JSON string.", e); + } + } + + @Override + public void toJson(T value, OutputStream output) { + try { + objectMapper.writeValue(output, value); + } catch (IOException e) { + throw new IllegalStateException("Could not serialize to JSON.", e); + } + } + } + + private SerializationUtil() {} +} diff --git a/instrumentation/aws-lambda/aws-lambda-events-2.2/library/src/main/java/io/opentelemetry/instrumentation/awslambdaevents/v2_2/internal/SqsEventAttributesExtractor.java b/instrumentation/aws-lambda/aws-lambda-events-2.2/library/src/main/java/io/opentelemetry/instrumentation/awslambdaevents/v2_2/internal/SqsEventAttributesExtractor.java index fdd29c1aec91..f8aa3c80e137 100644 --- a/instrumentation/aws-lambda/aws-lambda-events-2.2/library/src/main/java/io/opentelemetry/instrumentation/awslambdaevents/v2_2/internal/SqsEventAttributesExtractor.java +++ b/instrumentation/aws-lambda/aws-lambda-events-2.2/library/src/main/java/io/opentelemetry/instrumentation/awslambdaevents/v2_2/internal/SqsEventAttributesExtractor.java @@ -6,17 +6,26 @@ package io.opentelemetry.instrumentation.awslambdaevents.v2_2.internal; import com.amazonaws.services.lambda.runtime.events.SQSEvent; +import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.common.AttributesBuilder; import io.opentelemetry.context.Context; import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; import javax.annotation.Nullable; class SqsEventAttributesExtractor implements AttributesExtractor { + + // copied from MessagingIncubatingAttributes + private static final AttributeKey MESSAGING_OPERATION = + AttributeKey.stringKey("messaging.operation"); + private static final AttributeKey MESSAGING_SYSTEM = + AttributeKey.stringKey("messaging.system"); + // copied from MessagingIncubatingAttributes.MessagingSystemValues + private static final String AWS_SQS = "aws_sqs"; + @Override public void onStart(AttributesBuilder attributes, Context parentContext, SQSEvent event) { - attributes.put(SemanticAttributes.MESSAGING_SYSTEM, "AmazonSQS"); - attributes.put(SemanticAttributes.MESSAGING_OPERATION, "process"); + attributes.put(MESSAGING_SYSTEM, AWS_SQS); + attributes.put(MESSAGING_OPERATION, "process"); } @Override diff --git a/instrumentation/aws-lambda/aws-lambda-events-2.2/library/src/main/java/io/opentelemetry/instrumentation/awslambdaevents/v2_2/internal/SqsMessageAttributesExtractor.java b/instrumentation/aws-lambda/aws-lambda-events-2.2/library/src/main/java/io/opentelemetry/instrumentation/awslambdaevents/v2_2/internal/SqsMessageAttributesExtractor.java index e0ba91413e05..ad6358c34f50 100644 --- a/instrumentation/aws-lambda/aws-lambda-events-2.2/library/src/main/java/io/opentelemetry/instrumentation/awslambdaevents/v2_2/internal/SqsMessageAttributesExtractor.java +++ b/instrumentation/aws-lambda/aws-lambda-events-2.2/library/src/main/java/io/opentelemetry/instrumentation/awslambdaevents/v2_2/internal/SqsMessageAttributesExtractor.java @@ -6,19 +6,32 @@ package io.opentelemetry.instrumentation.awslambdaevents.v2_2.internal; import com.amazonaws.services.lambda.runtime.events.SQSEvent.SQSMessage; +import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.common.AttributesBuilder; import io.opentelemetry.context.Context; import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; import javax.annotation.Nullable; class SqsMessageAttributesExtractor implements AttributesExtractor { + + // copied from MessagingIncubatingAttributes + private static final AttributeKey MESSAGING_DESTINATION_NAME = + AttributeKey.stringKey("messaging.destination.name"); + private static final AttributeKey MESSAGING_MESSAGE_ID = + AttributeKey.stringKey("messaging.message.id"); + private static final AttributeKey MESSAGING_OPERATION = + AttributeKey.stringKey("messaging.operation"); + private static final AttributeKey MESSAGING_SYSTEM = + AttributeKey.stringKey("messaging.system"); + // copied from MessagingIncubatingAttributes.MessagingSystemValues + private static final String AWS_SQS = "aws_sqs"; + @Override public void onStart(AttributesBuilder attributes, Context parentContext, SQSMessage message) { - attributes.put(SemanticAttributes.MESSAGING_SYSTEM, "AmazonSQS"); - attributes.put(SemanticAttributes.MESSAGING_OPERATION, "process"); - attributes.put(SemanticAttributes.MESSAGING_MESSAGE_ID, message.getMessageId()); - attributes.put(SemanticAttributes.MESSAGING_DESTINATION_NAME, message.getEventSource()); + attributes.put(MESSAGING_SYSTEM, AWS_SQS); + attributes.put(MESSAGING_OPERATION, "process"); + attributes.put(MESSAGING_MESSAGE_ID, message.getMessageId()); + attributes.put(MESSAGING_DESTINATION_NAME, message.getEventSource()); } @Override diff --git a/instrumentation/aws-lambda/aws-lambda-events-2.2/library/src/test/java/io/opentelemetry/instrumentation/awslambdaevents/v2_2/AwsLambdaApiGatewayWrapperTest.java b/instrumentation/aws-lambda/aws-lambda-events-2.2/library/src/test/java/io/opentelemetry/instrumentation/awslambdaevents/v2_2/AwsLambdaApiGatewayWrapperTest.java index 58a37d145d11..0e70a0d6db46 100644 --- a/instrumentation/aws-lambda/aws-lambda-events-2.2/library/src/test/java/io/opentelemetry/instrumentation/awslambdaevents/v2_2/AwsLambdaApiGatewayWrapperTest.java +++ b/instrumentation/aws-lambda/aws-lambda-events-2.2/library/src/test/java/io/opentelemetry/instrumentation/awslambdaevents/v2_2/AwsLambdaApiGatewayWrapperTest.java @@ -17,8 +17,11 @@ import io.opentelemetry.instrumentation.awslambdacore.v1_0.internal.WrappedLambda; import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; import io.opentelemetry.instrumentation.testing.junit.LibraryInstrumentationExtension; -import io.opentelemetry.semconv.resource.attributes.ResourceAttributes; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.opentelemetry.semconv.HttpAttributes; +import io.opentelemetry.semconv.UrlAttributes; +import io.opentelemetry.semconv.UserAgentAttributes; +import io.opentelemetry.semconv.incubating.CloudIncubatingAttributes; +import io.opentelemetry.semconv.incubating.FaasIncubatingAttributes; import java.util.HashMap; import java.util.Map; import org.junit.jupiter.api.AfterEach; @@ -97,17 +100,16 @@ void tracedWithHttpPropagation() { .hasParentSpanId("0000000000000456") .hasAttributesSatisfyingExactly( equalTo( - ResourceAttributes.CLOUD_RESOURCE_ID, + CloudIncubatingAttributes.CLOUD_RESOURCE_ID, "arn:aws:lambda:us-east-1:123456789:function:test"), - equalTo(ResourceAttributes.CLOUD_ACCOUNT_ID, "123456789"), - equalTo(SemanticAttributes.FAAS_INVOCATION_ID, "1-22-333"), - equalTo(SemanticAttributes.FAAS_TRIGGER, "http"), - equalTo(SemanticAttributes.HTTP_METHOD, "GET"), - equalTo(SemanticAttributes.USER_AGENT_ORIGINAL, "Test Client"), + equalTo(CloudIncubatingAttributes.CLOUD_ACCOUNT_ID, "123456789"), + equalTo(FaasIncubatingAttributes.FAAS_INVOCATION_ID, "1-22-333"), + equalTo(FaasIncubatingAttributes.FAAS_TRIGGER, "http"), + equalTo(HttpAttributes.HTTP_REQUEST_METHOD, "GET"), + equalTo(UserAgentAttributes.USER_AGENT_ORIGINAL, "Test Client"), equalTo( - SemanticAttributes.HTTP_URL, - "http://localhost:123/hello/world?a=b&c=d"), - equalTo(SemanticAttributes.HTTP_STATUS_CODE, 200L)))); + UrlAttributes.URL_FULL, "http://localhost:123/hello/world?a=b&c=d"), + equalTo(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, 200L)))); } @Test @@ -134,11 +136,11 @@ void handlerTraced_empty() { .hasKind(SpanKind.SERVER) .hasAttributesSatisfyingExactly( equalTo( - ResourceAttributes.CLOUD_RESOURCE_ID, + CloudIncubatingAttributes.CLOUD_RESOURCE_ID, "arn:aws:lambda:us-east-1:123456789:function:test"), - equalTo(ResourceAttributes.CLOUD_ACCOUNT_ID, "123456789"), - equalTo(SemanticAttributes.FAAS_INVOCATION_ID, "1-22-333"), - equalTo(SemanticAttributes.FAAS_TRIGGER, "http")))); + equalTo(CloudIncubatingAttributes.CLOUD_ACCOUNT_ID, "123456789"), + equalTo(FaasIncubatingAttributes.FAAS_INVOCATION_ID, "1-22-333"), + equalTo(FaasIncubatingAttributes.FAAS_TRIGGER, "http")))); } @Test @@ -165,11 +167,11 @@ void handlerTraced_string() { .hasKind(SpanKind.SERVER) .hasAttributesSatisfyingExactly( equalTo( - ResourceAttributes.CLOUD_RESOURCE_ID, + CloudIncubatingAttributes.CLOUD_RESOURCE_ID, "arn:aws:lambda:us-east-1:123456789:function:test"), - equalTo(ResourceAttributes.CLOUD_ACCOUNT_ID, "123456789"), - equalTo(SemanticAttributes.FAAS_INVOCATION_ID, "1-22-333"), - equalTo(SemanticAttributes.FAAS_TRIGGER, "http")))); + equalTo(CloudIncubatingAttributes.CLOUD_ACCOUNT_ID, "123456789"), + equalTo(FaasIncubatingAttributes.FAAS_INVOCATION_ID, "1-22-333"), + equalTo(FaasIncubatingAttributes.FAAS_TRIGGER, "http")))); } @Test @@ -196,11 +198,11 @@ void handlerTraced_integer() { .hasKind(SpanKind.SERVER) .hasAttributesSatisfyingExactly( equalTo( - ResourceAttributes.CLOUD_RESOURCE_ID, + CloudIncubatingAttributes.CLOUD_RESOURCE_ID, "arn:aws:lambda:us-east-1:123456789:function:test"), - equalTo(ResourceAttributes.CLOUD_ACCOUNT_ID, "123456789"), - equalTo(SemanticAttributes.FAAS_INVOCATION_ID, "1-22-333"), - equalTo(SemanticAttributes.FAAS_TRIGGER, "http")))); + equalTo(CloudIncubatingAttributes.CLOUD_ACCOUNT_ID, "123456789"), + equalTo(FaasIncubatingAttributes.FAAS_INVOCATION_ID, "1-22-333"), + equalTo(FaasIncubatingAttributes.FAAS_TRIGGER, "http")))); } @Test @@ -230,11 +232,11 @@ void handlerTraced_customType() { .hasKind(SpanKind.SERVER) .hasAttributesSatisfyingExactly( equalTo( - ResourceAttributes.CLOUD_RESOURCE_ID, + CloudIncubatingAttributes.CLOUD_RESOURCE_ID, "arn:aws:lambda:us-east-1:123456789:function:test"), - equalTo(ResourceAttributes.CLOUD_ACCOUNT_ID, "123456789"), - equalTo(SemanticAttributes.FAAS_INVOCATION_ID, "1-22-333"), - equalTo(SemanticAttributes.FAAS_TRIGGER, "http")))); + equalTo(CloudIncubatingAttributes.CLOUD_ACCOUNT_ID, "123456789"), + equalTo(FaasIncubatingAttributes.FAAS_INVOCATION_ID, "1-22-333"), + equalTo(FaasIncubatingAttributes.FAAS_TRIGGER, "http")))); } public static class TestRequestHandlerApiGateway diff --git a/instrumentation/aws-lambda/aws-lambda-events-2.2/library/src/test/java/io/opentelemetry/instrumentation/awslambdaevents/v2_2/AwsLambdaSqsEventWrapperTest.java b/instrumentation/aws-lambda/aws-lambda-events-2.2/library/src/test/java/io/opentelemetry/instrumentation/awslambdaevents/v2_2/AwsLambdaSqsEventWrapperTest.java index 20205c7fd226..de6bd8b85f5b 100644 --- a/instrumentation/aws-lambda/aws-lambda-events-2.2/library/src/test/java/io/opentelemetry/instrumentation/awslambdaevents/v2_2/AwsLambdaSqsEventWrapperTest.java +++ b/instrumentation/aws-lambda/aws-lambda-events-2.2/library/src/test/java/io/opentelemetry/instrumentation/awslambdaevents/v2_2/AwsLambdaSqsEventWrapperTest.java @@ -16,8 +16,9 @@ import io.opentelemetry.instrumentation.awslambdacore.v1_0.internal.WrappedLambda; import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; import io.opentelemetry.instrumentation.testing.junit.LibraryInstrumentationExtension; -import io.opentelemetry.semconv.resource.attributes.ResourceAttributes; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.opentelemetry.semconv.incubating.CloudIncubatingAttributes; +import io.opentelemetry.semconv.incubating.FaasIncubatingAttributes; +import io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes; import java.lang.reflect.Constructor; import java.util.Collections; import org.junit.jupiter.api.AfterEach; @@ -75,16 +76,19 @@ void eventTraced() { .hasKind(SpanKind.SERVER) .hasAttributesSatisfyingExactly( equalTo( - ResourceAttributes.CLOUD_RESOURCE_ID, + CloudIncubatingAttributes.CLOUD_RESOURCE_ID, "arn:aws:lambda:us-east-1:123456789:function:test"), - equalTo(ResourceAttributes.CLOUD_ACCOUNT_ID, "123456789"), - equalTo(SemanticAttributes.FAAS_INVOCATION_ID, "1-22-333")), + equalTo(CloudIncubatingAttributes.CLOUD_ACCOUNT_ID, "123456789"), + equalTo(FaasIncubatingAttributes.FAAS_INVOCATION_ID, "1-22-333")), span -> span.hasName("otel process") .hasKind(SpanKind.CONSUMER) .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.MESSAGING_SYSTEM, "AmazonSQS"), - equalTo(SemanticAttributes.MESSAGING_OPERATION, "process")))); + equalTo( + MessagingIncubatingAttributes.MESSAGING_SYSTEM, + MessagingIncubatingAttributes.MessagingSystemValues.AWS_SQS), + equalTo( + MessagingIncubatingAttributes.MESSAGING_OPERATION, "process")))); } public static final class TestRequestHandler implements RequestHandler { diff --git a/instrumentation/aws-lambda/aws-lambda-events-2.2/library/src/test/java/io/opentelemetry/instrumentation/awslambdaevents/v2_2/AwsLambdaSqsMessageHandlerTest.java b/instrumentation/aws-lambda/aws-lambda-events-2.2/library/src/test/java/io/opentelemetry/instrumentation/awslambdaevents/v2_2/AwsLambdaSqsMessageHandlerTest.java index 05b73caec0a8..49bcbe97e241 100644 --- a/instrumentation/aws-lambda/aws-lambda-events-2.2/library/src/test/java/io/opentelemetry/instrumentation/awslambdaevents/v2_2/AwsLambdaSqsMessageHandlerTest.java +++ b/instrumentation/aws-lambda/aws-lambda-events-2.2/library/src/test/java/io/opentelemetry/instrumentation/awslambdaevents/v2_2/AwsLambdaSqsMessageHandlerTest.java @@ -19,7 +19,8 @@ import io.opentelemetry.instrumentation.testing.junit.LibraryInstrumentationExtension; import io.opentelemetry.sdk.OpenTelemetrySdk; import io.opentelemetry.sdk.trace.data.LinkData; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.opentelemetry.semconv.incubating.FaasIncubatingAttributes; +import io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes; import java.lang.reflect.Constructor; import java.util.Arrays; import java.util.Collections; @@ -79,14 +80,16 @@ void processSpans() { span.hasName("my_function") .hasKind(SpanKind.SERVER) .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.FAAS_INVOCATION_ID, "1-22-333")), + equalTo(FaasIncubatingAttributes.FAAS_INVOCATION_ID, "1-22-333")), span -> span.hasName("queue1 process") .hasKind(SpanKind.CONSUMER) .hasParentSpanId(trace.getSpan(0).getSpanId()) .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.MESSAGING_SYSTEM, "AmazonSQS"), - equalTo(SemanticAttributes.MESSAGING_OPERATION, "process")) + equalTo( + MessagingIncubatingAttributes.MESSAGING_SYSTEM, + MessagingIncubatingAttributes.MessagingSystemValues.AWS_SQS), + equalTo(MessagingIncubatingAttributes.MESSAGING_OPERATION, "process")) .hasLinks( LinkData.create( SpanContext.createFromRemoteParent( @@ -105,10 +108,13 @@ void processSpans() { .hasKind(SpanKind.CONSUMER) .hasParentSpanId(trace.getSpan(1).getSpanId()) .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.MESSAGING_SYSTEM, "AmazonSQS"), - equalTo(SemanticAttributes.MESSAGING_OPERATION, "process"), - equalTo(SemanticAttributes.MESSAGING_MESSAGE_ID, "message1"), - equalTo(SemanticAttributes.MESSAGING_DESTINATION_NAME, "queue1")) + equalTo( + MessagingIncubatingAttributes.MESSAGING_SYSTEM, + MessagingIncubatingAttributes.MessagingSystemValues.AWS_SQS), + equalTo(MessagingIncubatingAttributes.MESSAGING_OPERATION, "process"), + equalTo(MessagingIncubatingAttributes.MESSAGING_MESSAGE_ID, "message1"), + equalTo( + MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME, "queue1")) .hasLinks( LinkData.create( SpanContext.createFromRemoteParent( @@ -121,10 +127,13 @@ void processSpans() { .hasKind(SpanKind.CONSUMER) .hasParentSpanId(trace.getSpan(1).getSpanId()) .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.MESSAGING_SYSTEM, "AmazonSQS"), - equalTo(SemanticAttributes.MESSAGING_OPERATION, "process"), - equalTo(SemanticAttributes.MESSAGING_MESSAGE_ID, "message2"), - equalTo(SemanticAttributes.MESSAGING_DESTINATION_NAME, "queue1")) + equalTo( + MessagingIncubatingAttributes.MESSAGING_SYSTEM, + MessagingIncubatingAttributes.MessagingSystemValues.AWS_SQS), + equalTo(MessagingIncubatingAttributes.MESSAGING_OPERATION, "process"), + equalTo(MessagingIncubatingAttributes.MESSAGING_MESSAGE_ID, "message2"), + equalTo( + MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME, "queue1")) .hasLinks( LinkData.create( SpanContext.createFromRemoteParent( diff --git a/instrumentation/aws-lambda/aws-lambda-events-2.2/library/src/test/java/io/opentelemetry/instrumentation/awslambdaevents/v2_2/AwsLambdaWrapperTest.java b/instrumentation/aws-lambda/aws-lambda-events-2.2/library/src/test/java/io/opentelemetry/instrumentation/awslambdaevents/v2_2/AwsLambdaWrapperTest.java index b06de40de4d0..12bef1d95082 100644 --- a/instrumentation/aws-lambda/aws-lambda-events-2.2/library/src/test/java/io/opentelemetry/instrumentation/awslambdaevents/v2_2/AwsLambdaWrapperTest.java +++ b/instrumentation/aws-lambda/aws-lambda-events-2.2/library/src/test/java/io/opentelemetry/instrumentation/awslambdaevents/v2_2/AwsLambdaWrapperTest.java @@ -17,8 +17,9 @@ import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; import io.opentelemetry.instrumentation.testing.junit.LibraryInstrumentationExtension; import io.opentelemetry.sdk.trace.data.StatusData; -import io.opentelemetry.semconv.resource.attributes.ResourceAttributes; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.opentelemetry.semconv.incubating.CloudIncubatingAttributes; +import io.opentelemetry.semconv.incubating.FaasIncubatingAttributes; +import java.io.IOException; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -54,12 +55,9 @@ void tearDown() { key = WrappedLambda.OTEL_LAMBDA_HANDLER_ENV_KEY, value = "io.opentelemetry.instrumentation.awslambdaevents.v2_2.AwsLambdaWrapperTest$TestRequestHandlerString::handleRequest") - void handlerTraced() { + void handlerTraced() throws IOException { TracingRequestWrapper wrapper = - new TracingRequestWrapper( - testing.getOpenTelemetrySdk(), - WrappedLambda.fromConfiguration(), - TracingRequestWrapper::map); + new TracingRequestWrapper(testing.getOpenTelemetrySdk(), WrappedLambda.fromConfiguration()); Object result = wrapper.handleRequest("hello", context); assertThat(result).isEqualTo("world"); @@ -71,10 +69,10 @@ void handlerTraced() { .hasKind(SpanKind.SERVER) .hasAttributesSatisfyingExactly( equalTo( - ResourceAttributes.CLOUD_RESOURCE_ID, + CloudIncubatingAttributes.CLOUD_RESOURCE_ID, "arn:aws:lambda:us-east-1:123456789:function:test"), - equalTo(ResourceAttributes.CLOUD_ACCOUNT_ID, "123456789"), - equalTo(SemanticAttributes.FAAS_INVOCATION_ID, "1-22-333")))); + equalTo(CloudIncubatingAttributes.CLOUD_ACCOUNT_ID, "123456789"), + equalTo(FaasIncubatingAttributes.FAAS_INVOCATION_ID, "1-22-333")))); } @Test @@ -84,10 +82,7 @@ void handlerTraced() { "io.opentelemetry.instrumentation.awslambdaevents.v2_2.AwsLambdaWrapperTest$TestRequestHandlerString::handleRequest") void handlerTracedWithException() { TracingRequestWrapper wrapper = - new TracingRequestWrapper( - testing.getOpenTelemetrySdk(), - WrappedLambda.fromConfiguration(), - TracingRequestWrapper::map); + new TracingRequestWrapper(testing.getOpenTelemetrySdk(), WrappedLambda.fromConfiguration()); Throwable thrown = catchThrowable(() -> wrapper.handleRequest("goodbye", context)); assertThat(thrown).isInstanceOf(IllegalArgumentException.class); @@ -101,10 +96,10 @@ void handlerTracedWithException() { .hasException(thrown) .hasAttributesSatisfyingExactly( equalTo( - ResourceAttributes.CLOUD_RESOURCE_ID, + CloudIncubatingAttributes.CLOUD_RESOURCE_ID, "arn:aws:lambda:us-east-1:123456789:function:test"), - equalTo(ResourceAttributes.CLOUD_ACCOUNT_ID, "123456789"), - equalTo(SemanticAttributes.FAAS_INVOCATION_ID, "1-22-333")))); + equalTo(CloudIncubatingAttributes.CLOUD_ACCOUNT_ID, "123456789"), + equalTo(FaasIncubatingAttributes.FAAS_INVOCATION_ID, "1-22-333")))); } @Test @@ -112,12 +107,9 @@ void handlerTracedWithException() { key = WrappedLambda.OTEL_LAMBDA_HANDLER_ENV_KEY, value = "io.opentelemetry.instrumentation.awslambdaevents.v2_2.AwsLambdaWrapperTest$TestRequestHandlerInteger::handleRequest") - void handlerTraced_integer() { + void handlerTraced_integer() throws IOException { TracingRequestWrapper wrapper = - new TracingRequestWrapper( - testing.getOpenTelemetrySdk(), - WrappedLambda.fromConfiguration(), - TracingRequestWrapper::map); + new TracingRequestWrapper(testing.getOpenTelemetrySdk(), WrappedLambda.fromConfiguration()); Object result = wrapper.handleRequest(1, context); assertThat(result).isEqualTo("world"); @@ -129,10 +121,10 @@ void handlerTraced_integer() { .hasKind(SpanKind.SERVER) .hasAttributesSatisfyingExactly( equalTo( - ResourceAttributes.CLOUD_RESOURCE_ID, + CloudIncubatingAttributes.CLOUD_RESOURCE_ID, "arn:aws:lambda:us-east-1:123456789:function:test"), - equalTo(ResourceAttributes.CLOUD_ACCOUNT_ID, "123456789"), - equalTo(SemanticAttributes.FAAS_INVOCATION_ID, "1-22-333")))); + equalTo(CloudIncubatingAttributes.CLOUD_ACCOUNT_ID, "123456789"), + equalTo(FaasIncubatingAttributes.FAAS_INVOCATION_ID, "1-22-333")))); } @Test @@ -140,12 +132,9 @@ void handlerTraced_integer() { key = WrappedLambda.OTEL_LAMBDA_HANDLER_ENV_KEY, value = "io.opentelemetry.instrumentation.awslambdaevents.v2_2.AwsLambdaWrapperTest$TestRequestHandlerCustomType::handleRequest") - void handlerTraced_custom() { + void handlerTraced_custom() throws IOException { TracingRequestWrapper wrapper = - new TracingRequestWrapper( - testing.getOpenTelemetrySdk(), - WrappedLambda.fromConfiguration(), - TracingRequestWrapper::map); + new TracingRequestWrapper(testing.getOpenTelemetrySdk(), WrappedLambda.fromConfiguration()); CustomType ct = new CustomType(); ct.key = "hello there"; ct.value = "General Kenobi"; @@ -160,10 +149,10 @@ void handlerTraced_custom() { .hasKind(SpanKind.SERVER) .hasAttributesSatisfyingExactly( equalTo( - ResourceAttributes.CLOUD_RESOURCE_ID, + CloudIncubatingAttributes.CLOUD_RESOURCE_ID, "arn:aws:lambda:us-east-1:123456789:function:test"), - equalTo(ResourceAttributes.CLOUD_ACCOUNT_ID, "123456789"), - equalTo(SemanticAttributes.FAAS_INVOCATION_ID, "1-22-333")))); + equalTo(CloudIncubatingAttributes.CLOUD_ACCOUNT_ID, "123456789"), + equalTo(FaasIncubatingAttributes.FAAS_INVOCATION_ID, "1-22-333")))); } public static final class TestRequestHandlerString implements RequestHandler { @@ -188,9 +177,28 @@ public String handleRequest(Integer input, Context context) { } } + @SuppressWarnings("UnusedMethod") private static class CustomType { String key; String value; + + // Need getter/setter of all the attributes for serialization/deserialization + + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } } public static final class TestRequestHandlerCustomType diff --git a/instrumentation/aws-lambda/aws-lambda-events-2.2/library/src/test/java/io/opentelemetry/instrumentation/awslambdaevents/v2_2/LambdaParametersTest.java b/instrumentation/aws-lambda/aws-lambda-events-2.2/library/src/test/java/io/opentelemetry/instrumentation/awslambdaevents/v2_2/LambdaParametersTest.java index 2bf8357f4334..f738b5a34932 100644 --- a/instrumentation/aws-lambda/aws-lambda-events-2.2/library/src/test/java/io/opentelemetry/instrumentation/awslambdaevents/v2_2/LambdaParametersTest.java +++ b/instrumentation/aws-lambda/aws-lambda-events-2.2/library/src/test/java/io/opentelemetry/instrumentation/awslambdaevents/v2_2/LambdaParametersTest.java @@ -9,6 +9,8 @@ import static org.mockito.Mockito.mock; import com.amazonaws.services.lambda.runtime.Context; +import io.opentelemetry.instrumentation.awslambdaevents.v2_2.internal.SerializationUtil; +import java.io.ByteArrayInputStream; import java.lang.reflect.Method; import org.junit.jupiter.api.Test; @@ -16,6 +18,10 @@ class LambdaParametersTest { public void onlyContext(Context context) {} + public void noContext(String one) {} + + public void contextOnSecond(String one, Context context) {} + public void contextOnThird(String one, String two, Context context) {} @Test @@ -30,6 +36,31 @@ void shouldSetContextOnFirstPosition() throws NoSuchMethodException { assertThat(params[0]).isEqualTo(context); } + @Test + void shouldOnlySetInputWhenNoContext() throws NoSuchMethodException { + // given + Context context = mock(Context.class); + Method method = getClass().getMethod("noContext", String.class); + // when + Object[] params = LambdaParameters.toArray(method, "", context, (o, c) -> o); + // then + assertThat(params).hasSize(1); + assertThat(params[0]).isEqualTo(""); + } + + @Test + void shouldSetContextOnTheSecondPosition() throws NoSuchMethodException { + // given + Context context = mock(Context.class); + Method method = getClass().getMethod("contextOnSecond", String.class, Context.class); + // when + Object[] params = LambdaParameters.toArray(method, "", context, (o, c) -> o); + // then + assertThat(params).hasSize(2); + assertThat(params[0]).isEqualTo(""); + assertThat(params[1]).isEqualTo(context); + } + @Test void shouldSetContextOnTheLastPosition() throws NoSuchMethodException { // given @@ -44,4 +75,103 @@ void shouldSetContextOnTheLastPosition() throws NoSuchMethodException { assertThat(params[1]).isNull(); assertThat(params[2]).isEqualTo(context); } + + @Test + void shouldNotResolveInputWhenNoInput() throws NoSuchMethodException { + // given + Method method = getClass().getMethod("onlyContext", Context.class); + String giveInput = "testInput"; + // when + Object resolvedInput = + LambdaParameters.toInput( + method, + new ByteArrayInputStream(SerializationUtil.toJsonData(giveInput)), + (i, c) -> SerializationUtil.fromJson(i, c)); + // then + assertThat(resolvedInput).isNull(); + } + + @Test + void shouldResolveInputWithContext() throws NoSuchMethodException { + // given + Method method = getClass().getMethod("contextOnSecond", String.class, Context.class); + String givenInput = "testInput"; + // when + Object resolvedInput = + LambdaParameters.toInput( + method, + new ByteArrayInputStream(SerializationUtil.toJsonData(givenInput)), + (i, c) -> SerializationUtil.fromJson(i, c)); + // then + assertThat(resolvedInput).isNotNull(); + assertThat(resolvedInput).isEqualTo(givenInput); + } + + @Test + void shouldResolveInputWithoutContext() throws NoSuchMethodException { + // given + Method method = getClass().getMethod("noContext", String.class); + String givenInput = "testInput"; + // when + Object resolvedInput = + LambdaParameters.toInput( + method, + new ByteArrayInputStream(SerializationUtil.toJsonData(givenInput)), + (i, c) -> SerializationUtil.fromJson(i, c)); + // then + assertThat(resolvedInput).isNotNull(); + assertThat(resolvedInput).isEqualTo(givenInput); + } + + @Test + void shouldResolveParametersWhenOnlyContext() throws NoSuchMethodException { + // given + Context context = mock(Context.class); + Method method = getClass().getMethod("onlyContext", Context.class); + // when + Object[] params = LambdaParameters.toParameters(method, "", context); + // then + assertThat(params).hasSize(1); + assertThat(params[0]).isEqualTo(context); + } + + @Test + void shouldResolveParametersWhenNoContext() throws NoSuchMethodException { + // given + Context context = mock(Context.class); + Method method = getClass().getMethod("noContext", String.class); + // when + Object[] params = LambdaParameters.toParameters(method, "", context); + // then + assertThat(params).hasSize(1); + assertThat(params[0]).isEqualTo(""); + } + + @Test + void shouldResolveParametersWhenContextOnTheSecondPosition() throws NoSuchMethodException { + // given + Context context = mock(Context.class); + Method method = getClass().getMethod("contextOnSecond", String.class, Context.class); + // when + Object[] params = LambdaParameters.toParameters(method, "", context); + // then + assertThat(params).hasSize(2); + assertThat(params[0]).isEqualTo(""); + assertThat(params[1]).isEqualTo(context); + } + + @Test + void shouldResolveParametersWhenContextOnTheLastPosition() throws NoSuchMethodException { + // given + Context context = mock(Context.class); + Method method = + getClass().getMethod("contextOnThird", String.class, String.class, Context.class); + // when + Object[] params = LambdaParameters.toParameters(method, "", context); + // then + assertThat(params).hasSize(3); + assertThat(params[0]).isEqualTo(""); + assertThat(params[1]).isNull(); + assertThat(params[2]).isEqualTo(context); + } } diff --git a/instrumentation/aws-lambda/aws-lambda-events-2.2/library/src/test/java/io/opentelemetry/instrumentation/awslambdaevents/v2_2/TracingRequestWrapperStandardEventsTest.java b/instrumentation/aws-lambda/aws-lambda-events-2.2/library/src/test/java/io/opentelemetry/instrumentation/awslambdaevents/v2_2/TracingRequestWrapperStandardEventsTest.java index 620d084a1b04..c25d911ed1cb 100644 --- a/instrumentation/aws-lambda/aws-lambda-events-2.2/library/src/test/java/io/opentelemetry/instrumentation/awslambdaevents/v2_2/TracingRequestWrapperStandardEventsTest.java +++ b/instrumentation/aws-lambda/aws-lambda-events-2.2/library/src/test/java/io/opentelemetry/instrumentation/awslambdaevents/v2_2/TracingRequestWrapperStandardEventsTest.java @@ -5,7 +5,7 @@ package io.opentelemetry.instrumentation.awslambdaevents.v2_2; -import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; import com.amazonaws.services.lambda.runtime.Context; @@ -15,198 +15,215 @@ import com.amazonaws.services.lambda.runtime.events.SNSEvent; import com.amazonaws.services.lambda.runtime.events.SQSEvent; import com.amazonaws.services.lambda.runtime.events.ScheduledEvent; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; import io.opentelemetry.instrumentation.awslambdacore.v1_0.internal.WrappedLambda; +import io.opentelemetry.instrumentation.awslambdaevents.v2_2.internal.SerializationUtil; import io.opentelemetry.sdk.OpenTelemetrySdk; +import java.io.IOException; import java.util.HashMap; import java.util.Map; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; class TracingRequestWrapperStandardEventsTest { - private static final String SUCCESS = "success"; - private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); - private static final Map, String> EVENTS_JSON = buildEventExamples(); + private static final Map, EventInfo> EVENTS_JSON = buildEventExamples(); private final OpenTelemetrySdk sdk = OpenTelemetrySdk.builder().build(); private final Context context = mock(Context.class); private TracingRequestWrapper wrapper; - private static Map, String> buildEventExamples() { - Map, String> events = new HashMap<>(); + static final class EventInfo { + final Class eventType; + final String eventBody; + + EventInfo(Class eventType, String eventBody) { + this.eventType = eventType; + this.eventBody = eventBody; + } + } + + private static Map, EventInfo> buildEventExamples() { + Map, EventInfo> events = new HashMap<>(); events.put( ScheduledEventRequestHandler.class, - "{\n" - + " \"version\": \"0\",\n" - + " \"id\": \"53dc4d37-cffa-4f76-80c9-8b7d4a4d2eaa\",\n" - + " \"detail-type\": \"Scheduled Event\",\n" - + " \"source\": \"aws.events\",\n" - + " \"account\": \"123456789012\",\n" - + " \"time\": \"2015-10-08T16:53:06Z\",\n" - + " \"region\": \"us-east-1\",\n" - + " \"resources\": [\n" - + " \"arn:aws:events:us-east-1:123456789012:rule/my-scheduled-rule\"\n" - + " ],\n" - + " \"detail\": {}\n" - + "}"); + new EventInfo( + ScheduledEvent.class, + "{\n" + + " \"version\": \"0\",\n" + + " \"id\": \"53dc4d37-cffa-4f76-80c9-8b7d4a4d2eaa\",\n" + + " \"detail-type\": \"Scheduled Event\",\n" + + " \"source\": \"aws.events\",\n" + + " \"account\": \"123456789012\",\n" + + " \"time\": \"2015-10-08T16:53:06Z\",\n" + + " \"region\": \"us-east-1\",\n" + + " \"resources\": [\n" + + " \"arn:aws:events:us-east-1:123456789012:rule/my-scheduled-rule\"\n" + + " ],\n" + + " \"detail\": {}\n" + + "}")); events.put( KinesisEventRequestHandler.class, - "{\n" - + " \"Records\": [\n" - + " {\n" - + " \"kinesis\": {\n" - + " \"kinesisSchemaVersion\": \"1.0\",\n" - + " \"partitionKey\": \"1\",\n" - + " \"sequenceNumber\": \"49590338271490256608559692538361571095921575989136588898\",\n" - + " \"data\": \"SGVsbG8sIHRoaXMgaXMgYSB0ZXN0Lg==\",\n" - + " \"approximateArrivalTimestamp\": 1545084650.987\n" - + " },\n" - + " \"eventSource\": \"aws:kinesis\",\n" - + " \"eventVersion\": \"1.0\",\n" - + " \"eventID\": \"shardId-000000000006:49590338271490256608559692538361571095921575989136588898\",\n" - + " \"eventName\": \"aws:kinesis:record\",\n" - + " \"invokeIdentityArn\": \"arn:aws:iam::123456789012:role/lambda-role\",\n" - + " \"awsRegion\": \"us-east-2\",\n" - + " \"eventSourceARN\": \"arn:aws:kinesis:us-east-2:123456789012:stream/lambda-stream\"\n" - + " },\n" - + " {\n" - + " \"kinesis\": {\n" - + " \"kinesisSchemaVersion\": \"1.0\",\n" - + " \"partitionKey\": \"1\",\n" - + " \"sequenceNumber\": \"49590338271490256608559692540925702759324208523137515618\",\n" - + " \"data\": \"VGhpcyBpcyBvbmx5IGEgdGVzdC4=\",\n" - + " \"approximateArrivalTimestamp\": 1545084711.166\n" - + " },\n" - + " \"eventSource\": \"aws:kinesis\",\n" - + " \"eventVersion\": \"1.0\",\n" - + " \"eventID\": \"shardId-000000000006:49590338271490256608559692540925702759324208523137515618\",\n" - + " \"eventName\": \"aws:kinesis:record\",\n" - + " \"invokeIdentityArn\": \"arn:aws:iam::123456789012:role/lambda-role\",\n" - + " \"awsRegion\": \"us-east-2\",\n" - + " \"eventSourceARN\": \"arn:aws:kinesis:us-east-2:123456789012:stream/lambda-stream\"\n" - + " }\n" - + " ]\n" - + "}"); + new EventInfo( + KinesisEvent.class, + "{\n" + + " \"Records\": [\n" + + " {\n" + + " \"kinesis\": {\n" + + " \"kinesisSchemaVersion\": \"1.0\",\n" + + " \"partitionKey\": \"1\",\n" + + " \"sequenceNumber\": \"49590338271490256608559692538361571095921575989136588898\",\n" + + " \"data\": \"SGVsbG8sIHRoaXMgaXMgYSB0ZXN0Lg==\",\n" + + " \"approximateArrivalTimestamp\": 1545084650.987\n" + + " },\n" + + " \"eventSource\": \"aws:kinesis\",\n" + + " \"eventVersion\": \"1.0\",\n" + + " \"eventID\": \"shardId-000000000006:49590338271490256608559692538361571095921575989136588898\",\n" + + " \"eventName\": \"aws:kinesis:record\",\n" + + " \"invokeIdentityArn\": \"arn:aws:iam::123456789012:role/lambda-role\",\n" + + " \"awsRegion\": \"us-east-2\",\n" + + " \"eventSourceARN\": \"arn:aws:kinesis:us-east-2:123456789012:stream/lambda-stream\"\n" + + " },\n" + + " {\n" + + " \"kinesis\": {\n" + + " \"kinesisSchemaVersion\": \"1.0\",\n" + + " \"partitionKey\": \"1\",\n" + + " \"sequenceNumber\": \"49590338271490256608559692540925702759324208523137515618\",\n" + + " \"data\": \"VGhpcyBpcyBvbmx5IGEgdGVzdC4=\",\n" + + " \"approximateArrivalTimestamp\": 1545084711.166\n" + + " },\n" + + " \"eventSource\": \"aws:kinesis\",\n" + + " \"eventVersion\": \"1.0\",\n" + + " \"eventID\": \"shardId-000000000006:49590338271490256608559692540925702759324208523137515618\",\n" + + " \"eventName\": \"aws:kinesis:record\",\n" + + " \"invokeIdentityArn\": \"arn:aws:iam::123456789012:role/lambda-role\",\n" + + " \"awsRegion\": \"us-east-2\",\n" + + " \"eventSourceARN\": \"arn:aws:kinesis:us-east-2:123456789012:stream/lambda-stream\"\n" + + " }\n" + + " ]\n" + + "}")); events.put( SqsEventRequestHandler.class, - "{\n" - + " \"Records\": [\n" - + " {\n" - + " \"messageId\": \"059f36b4-87a3-44ab-83d2-661975830a7d\",\n" - + " \"receiptHandle\": \"AQEBwJnKyrHigUMZj6rYigCgxlaS3SLy0a...\",\n" - + " \"body\": \"Test message.\",\n" - + " \"attributes\": {\n" - + " \"ApproximateReceiveCount\": \"1\",\n" - + " \"SentTimestamp\": \"1545082649183\",\n" - + " \"SenderId\": \"AIDAIENQZJOLO23YVJ4VO\",\n" - + " \"ApproximateFirstReceiveTimestamp\": \"1545082649185\"\n" - + " },\n" - + " \"messageAttributes\": {},\n" - + " \"md5OfBody\": \"e4e68fb7bd0e697a0ae8f1bb342846b3\",\n" - + " \"eventSource\": \"aws:sqs\",\n" - + " \"eventSourceARN\": \"arn:aws:sqs:us-east-2:123456789012:my-queue\",\n" - + " \"awsRegion\": \"us-east-2\"\n" - + " },\n" - + " {\n" - + " \"messageId\": \"2e1424d4-f796-459a-8184-9c92662be6da\",\n" - + " \"receiptHandle\": \"AQEBzWwaftRI0KuVm4tP+/7q1rGgNqicHq...\",\n" - + " \"body\": \"Test message.\",\n" - + " \"attributes\": {\n" - + " \"ApproximateReceiveCount\": \"1\",\n" - + " \"SentTimestamp\": \"1545082650636\",\n" - + " \"SenderId\": \"AIDAIENQZJOLO23YVJ4VO\",\n" - + " \"ApproximateFirstReceiveTimestamp\": \"1545082650649\"\n" - + " },\n" - + " \"messageAttributes\": {},\n" - + " \"md5OfBody\": \"e4e68fb7bd0e697a0ae8f1bb342846b3\",\n" - + " \"eventSource\": \"aws:sqs\",\n" - + " \"eventSourceARN\": \"arn:aws:sqs:us-east-2:123456789012:my-queue\",\n" - + " \"awsRegion\": \"us-east-2\"\n" - + " }\n" - + " ]\n" - + "}"); + new EventInfo( + SQSEvent.class, + "{\n" + + " \"Records\": [\n" + + " {\n" + + " \"messageId\": \"059f36b4-87a3-44ab-83d2-661975830a7d\",\n" + + " \"receiptHandle\": \"AQEBwJnKyrHigUMZj6rYigCgxlaS3SLy0a...\",\n" + + " \"body\": \"Test message.\",\n" + + " \"attributes\": {\n" + + " \"ApproximateReceiveCount\": \"1\",\n" + + " \"SentTimestamp\": \"1545082649183\",\n" + + " \"SenderId\": \"AIDAIENQZJOLO23YVJ4VO\",\n" + + " \"ApproximateFirstReceiveTimestamp\": \"1545082649185\"\n" + + " },\n" + + " \"messageAttributes\": {},\n" + + " \"md5OfBody\": \"e4e68fb7bd0e697a0ae8f1bb342846b3\",\n" + + " \"eventSource\": \"aws:sqs\",\n" + + " \"eventSourceARN\": \"arn:aws:sqs:us-east-2:123456789012:my-queue\",\n" + + " \"awsRegion\": \"us-east-2\"\n" + + " },\n" + + " {\n" + + " \"messageId\": \"2e1424d4-f796-459a-8184-9c92662be6da\",\n" + + " \"receiptHandle\": \"AQEBzWwaftRI0KuVm4tP+/7q1rGgNqicHq...\",\n" + + " \"body\": \"Test message.\",\n" + + " \"attributes\": {\n" + + " \"ApproximateReceiveCount\": \"1\",\n" + + " \"SentTimestamp\": \"1545082650636\",\n" + + " \"SenderId\": \"AIDAIENQZJOLO23YVJ4VO\",\n" + + " \"ApproximateFirstReceiveTimestamp\": \"1545082650649\"\n" + + " },\n" + + " \"messageAttributes\": {},\n" + + " \"md5OfBody\": \"e4e68fb7bd0e697a0ae8f1bb342846b3\",\n" + + " \"eventSource\": \"aws:sqs\",\n" + + " \"eventSourceARN\": \"arn:aws:sqs:us-east-2:123456789012:my-queue\",\n" + + " \"awsRegion\": \"us-east-2\"\n" + + " }\n" + + " ]\n" + + "}")); events.put( S3EventRequestHandler.class, - "{\n" - + " \"Records\": [\n" - + " {\n" - + " \"eventVersion\": \"2.1\",\n" - + " \"eventSource\": \"aws:s3\",\n" - + " \"awsRegion\": \"us-east-2\",\n" - + " \"eventTime\": \"2019-09-03T19:37:27.192Z\",\n" - + " \"eventName\": \"ObjectCreated:Put\",\n" - + " \"userIdentity\": {\n" - + " \"principalId\": \"AWS:AIDAINPONIXQXHT3IKHL2\"\n" - + " },\n" - + " \"requestParameters\": {\n" - + " \"sourceIPAddress\": \"205.255.255.255\"\n" - + " },\n" - + " \"responseElements\": {\n" - + " \"x-amz-request-id\": \"D82B88E5F771F645\",\n" - + " \"x-amz-id-2\": \"vlR7PnpV2Ce81l0PRw6jlUpck7Jo5ZsQjryTjKlc5aLWGVHPZLj5NeC6qMa0emYBDXOo6QBU0Wo=\"\n" - + " },\n" - + " \"s3\": {\n" - + " \"s3SchemaVersion\": \"1.0\",\n" - + " \"configurationId\": \"828aa6fc-f7b5-4305-8584-487c791949c1\",\n" - + " \"bucket\": {\n" - + " \"name\": \"DOC-EXAMPLE-BUCKET\",\n" - + " \"ownerIdentity\": {\n" - + " \"principalId\": \"A3I5XTEXAMAI3E\"\n" - + " },\n" - + " \"arn\": \"arn:aws:s3:::lambda-artifacts-deafc19498e3f2df\"\n" - + " },\n" - + " \"object\": {\n" - + " \"key\": \"b21b84d653bb07b05b1e6b33684dc11b\",\n" - + " \"size\": 1305107,\n" - + " \"eTag\": \"b21b84d653bb07b05b1e6b33684dc11b\",\n" - + " \"sequencer\": \"0C0F6F405D6ED209E1\"\n" - + " }\n" - + " }\n" - + " }\n" - + " ]\n" - + "}"); + new EventInfo( + S3Event.class, + "{\n" + + " \"Records\": [\n" + + " {\n" + + " \"eventVersion\": \"2.1\",\n" + + " \"eventSource\": \"aws:s3\",\n" + + " \"awsRegion\": \"us-east-2\",\n" + + " \"eventTime\": \"2019-09-03T19:37:27.192Z\",\n" + + " \"eventName\": \"ObjectCreated:Put\",\n" + + " \"userIdentity\": {\n" + + " \"principalId\": \"AWS:AIDAINPONIXQXHT3IKHL2\"\n" + + " },\n" + + " \"requestParameters\": {\n" + + " \"sourceIPAddress\": \"205.255.255.255\"\n" + + " },\n" + + " \"responseElements\": {\n" + + " \"x-amz-request-id\": \"D82B88E5F771F645\",\n" + + " \"x-amz-id-2\": \"vlR7PnpV2Ce81l0PRw6jlUpck7Jo5ZsQjryTjKlc5aLWGVHPZLj5NeC6qMa0emYBDXOo6QBU0Wo=\"\n" + + " },\n" + + " \"s3\": {\n" + + " \"s3SchemaVersion\": \"1.0\",\n" + + " \"configurationId\": \"828aa6fc-f7b5-4305-8584-487c791949c1\",\n" + + " \"bucket\": {\n" + + " \"name\": \"DOC-EXAMPLE-BUCKET\",\n" + + " \"ownerIdentity\": {\n" + + " \"principalId\": \"A3I5XTEXAMAI3E\"\n" + + " },\n" + + " \"arn\": \"arn:aws:s3:::lambda-artifacts-deafc19498e3f2df\"\n" + + " },\n" + + " \"object\": {\n" + + " \"key\": \"b21b84d653bb07b05b1e6b33684dc11b\",\n" + + " \"size\": 1305107,\n" + + " \"eTag\": \"b21b84d653bb07b05b1e6b33684dc11b\",\n" + + " \"sequencer\": \"0C0F6F405D6ED209E1\"\n" + + " }\n" + + " }\n" + + " }\n" + + " ]\n" + + "}")); events.put( SnsEventRequestHandler.class, - "{\n" - + " \"Records\": [\n" - + " {\n" - + " \"EventVersion\": \"1.0\",\n" - + " \"EventSubscriptionArn\": \"arn:aws:sns:us-east-2:123456789012:sns-lambda:21be56ed-a058-49f5-8c98-aedd2564c486\",\n" - + " \"EventSource\": \"aws:sns\",\n" - + " \"Sns\": {\n" - + " \"SignatureVersion\": \"1\",\n" - + " \"Timestamp\": \"2019-01-02T12:45:07.000Z\",\n" - + " \"Signature\": \"tcc6faL2yUC6dgZdmrwh1Y4cGa/ebXEkAi6RibDsvpi+tE/1+82j...65r==\",\n" - + " \"SigningCertUrl\": \"https://sns.us-east-2.amazonaws.com/SimpleNotificationService-ac565b8b1a6c5d002d285f9598aa1d9b.pem\",\n" - + " \"MessageId\": \"95df01b4-ee98-5cb9-9903-4c221d41eb5e\",\n" - + " \"Message\": \"Hello from SNS!\",\n" - + " \"MessageAttributes\": {\n" - + " \"Test\": {\n" - + " \"Type\": \"String\",\n" - + " \"Value\": \"TestString\"\n" - + " },\n" - + " \"TestBinary\": {\n" - + " \"Type\": \"Binary\",\n" - + " \"Value\": \"TestBinary\"\n" - + " }\n" - + " },\n" - + " \"Type\": \"Notification\",\n" - + " \"UnsubscribeUrl\": \"https://sns.us-east-2.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:us-east-2:123456789012:test-lambda:21be56ed-a058-49f5-8c98-aedd2564c486\",\n" - + " \"TopicArn\":\"arn:aws:sns:us-east-2:123456789012:sns-lambda\",\n" - + " \"Subject\": \"TestInvoke\"\n" - + " }\n" - + " }\n" - + " ]\n" - + "}"); - + new EventInfo( + SNSEvent.class, + "{\n" + + " \"Records\": [\n" + + " {\n" + + " \"EventVersion\": \"1.0\",\n" + + " \"EventSubscriptionArn\": \"arn:aws:sns:us-east-2:123456789012:sns-lambda:21be56ed-a058-49f5-8c98-aedd2564c486\",\n" + + " \"EventSource\": \"aws:sns\",\n" + + " \"Sns\": {\n" + + " \"SignatureVersion\": \"1\",\n" + + " \"Timestamp\": \"2019-01-02T12:45:07.000Z\",\n" + + " \"Signature\": \"tcc6faL2yUC6dgZdmrwh1Y4cGa/ebXEkAi6RibDsvpi+tE/1+82j...65r==\",\n" + + " \"SigningCertUrl\": \"https://sns.us-east-2.amazonaws.com/SimpleNotificationService-ac565b8b1a6c5d002d285f9598aa1d9b.pem\",\n" + + " \"MessageId\": \"95df01b4-ee98-5cb9-9903-4c221d41eb5e\",\n" + + " \"Message\": \"Hello from SNS!\",\n" + + " \"MessageAttributes\": {\n" + + " \"Test\": {\n" + + " \"Type\": \"String\",\n" + + " \"Value\": \"TestString\"\n" + + " },\n" + + " \"TestBinary\": {\n" + + " \"Type\": \"Binary\",\n" + + " \"Value\": \"TestBinary\"\n" + + " }\n" + + " },\n" + + " \"Type\": \"Notification\",\n" + + " \"UnsubscribeUrl\": \"https://sns.us-east-2.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:us-east-2:123456789012:test-lambda:21be56ed-a058-49f5-8c98-aedd2564c486\",\n" + + " \"TopicArn\":\"arn:aws:sns:us-east-2:123456789012:sns-lambda\",\n" + + " \"Subject\": \"TestInvoke\"\n" + + " }\n" + + " }\n" + + " ]\n" + + "}")); return events; } private TracingRequestWrapper buildWrapper(Class targetClass) { WrappedLambda wrappedLambda = new WrappedLambda(targetClass, "handleRequest"); - return new TracingRequestWrapper(sdk, wrappedLambda, TracingRequestWrapper::map); + return new TracingRequestWrapper(sdk, wrappedLambda); } @ParameterizedTest @@ -218,46 +235,61 @@ private TracingRequestWrapper buildWrapper(Class targetClass) { S3EventRequestHandler.class, SnsEventRequestHandler.class }) - void handleScheduledEvent(Class targetClass) throws JsonProcessingException { + void handleLambdaEvent(Class targetClass) throws IOException { wrapper = buildWrapper(targetClass); - Object parsedScheduledEvent = - OBJECT_MAPPER.readValue(EVENTS_JSON.get(targetClass), Object.class); - assertEquals(SUCCESS, wrapper.doHandleRequest(parsedScheduledEvent, context)); + EventInfo eventInfo = EVENTS_JSON.get(targetClass); + + Object input = SerializationUtil.fromJson(eventInfo.eventBody, eventInfo.eventType); + + // Call to object based "O handleRequest(I input, Context context)" method + // delegates to the stream based + // "handleRequest(InputStream input, OutputStream output, Context context)" method. + // So serialization/deserialization of both input and outputs are triggered to be verified here. + Object output = wrapper.handleRequest(input, context); + + assertThat(input.getClass()).isEqualTo(output.getClass()); + + // "equals" methods are not properly implemented of Lambda events, + // so we are comparing them over their serialized json data + String inputJson = SerializationUtil.toJson(input); + String outputJson = SerializationUtil.toJson(output); + assertThat(inputJson).isEqualTo(outputJson); } public static class ScheduledEventRequestHandler - implements RequestHandler { + implements RequestHandler { @Override - public String handleRequest(ScheduledEvent i, Context cntxt) { - return SUCCESS; + public ScheduledEvent handleRequest(ScheduledEvent i, Context cntxt) { + return i; } } - public static class KinesisEventRequestHandler implements RequestHandler { + public static class KinesisEventRequestHandler + implements RequestHandler { @Override - public String handleRequest(KinesisEvent i, Context cntxt) { - return SUCCESS; + public KinesisEvent handleRequest(KinesisEvent i, Context cntxt) { + return i; } } - public static class SqsEventRequestHandler implements RequestHandler { + public static class SqsEventRequestHandler implements RequestHandler { @Override - public String handleRequest(SQSEvent i, Context cntxt) { - return SUCCESS; + public SQSEvent handleRequest(SQSEvent i, Context cntxt) { + return i; } } - public static class S3EventRequestHandler implements RequestHandler { + public static class S3EventRequestHandler implements RequestHandler { @Override - public String handleRequest(S3Event i, Context cntxt) { - return SUCCESS; + public S3Event handleRequest(S3Event i, Context cntxt) { + return i; } } - public static class SnsEventRequestHandler implements RequestHandler { + public static class SnsEventRequestHandler implements RequestHandler { @Override - public String handleRequest(SNSEvent i, Context cntxt) { - return SUCCESS; + public SNSEvent handleRequest(SNSEvent i, Context cntxt) { + return i; } } } diff --git a/instrumentation/aws-lambda/aws-lambda-events-2.2/library/src/test/java/io/opentelemetry/instrumentation/awslambdaevents/v2_2/internal/SerializationUtilTest.java b/instrumentation/aws-lambda/aws-lambda-events-2.2/library/src/test/java/io/opentelemetry/instrumentation/awslambdaevents/v2_2/internal/SerializationUtilTest.java new file mode 100644 index 000000000000..7711d15779c3 --- /dev/null +++ b/instrumentation/aws-lambda/aws-lambda-events-2.2/library/src/test/java/io/opentelemetry/instrumentation/awslambdaevents/v2_2/internal/SerializationUtilTest.java @@ -0,0 +1,359 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.awslambdaevents.v2_2.internal; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.amazonaws.services.lambda.runtime.events.KinesisEvent; +import com.amazonaws.services.lambda.runtime.events.S3Event; +import com.amazonaws.services.lambda.runtime.events.SNSEvent; +import com.amazonaws.services.lambda.runtime.events.SQSEvent; +import com.amazonaws.services.lambda.runtime.events.ScheduledEvent; +import com.amazonaws.services.lambda.runtime.events.models.s3.S3EventNotification; +import java.io.ByteArrayInputStream; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; +import org.joda.time.DateTime; +import org.junit.jupiter.api.Test; + +public class SerializationUtilTest { + + private static final Map, String> events = buildEventExamples(); + + private static Map, String> buildEventExamples() { + Map, String> events = new HashMap<>(); + events.put( + ScheduledEvent.class, + "{\n" + + " \"version\": \"0\",\n" + + " \"id\": \"53dc4d37-cffa-4f76-80c9-8b7d4a4d2eaa\",\n" + + " \"detail-type\": \"Scheduled Event\",\n" + + " \"source\": \"aws.events\",\n" + + " \"account\": \"123456789012\",\n" + + " \"time\": \"2015-10-08T16:53:06Z\",\n" + + " \"region\": \"us-east-1\",\n" + + " \"resources\": [\n" + + " \"arn:aws:events:us-east-1:123456789012:rule/my-scheduled-rule\"\n" + + " ],\n" + + " \"detail\": {}\n" + + "}"); + events.put( + KinesisEvent.class, + "{\n" + + " \"Records\": [\n" + + " {\n" + + " \"kinesis\": {\n" + + " \"kinesisSchemaVersion\": \"1.0\",\n" + + " \"partitionKey\": \"1\",\n" + + " \"sequenceNumber\": \"49590338271490256608559692538361571095921575989136588898\",\n" + + " \"data\": \"SGVsbG8sIHRoaXMgaXMgYSB0ZXN0Lg==\",\n" + + " \"approximateArrivalTimestamp\": 1545084650.987\n" + + " },\n" + + " \"eventSource\": \"aws:kinesis\",\n" + + " \"eventVersion\": \"1.0\",\n" + + " \"eventID\": \"shardId-000000000006:49590338271490256608559692538361571095921575989136588898\",\n" + + " \"eventName\": \"aws:kinesis:record\",\n" + + " \"invokeIdentityArn\": \"arn:aws:iam::123456789012:role/lambda-role\",\n" + + " \"awsRegion\": \"us-east-2\",\n" + + " \"eventSourceARN\": \"arn:aws:kinesis:us-east-2:123456789012:stream/lambda-stream\"\n" + + " },\n" + + " {\n" + + " \"kinesis\": {\n" + + " \"kinesisSchemaVersion\": \"1.0\",\n" + + " \"partitionKey\": \"1\",\n" + + " \"sequenceNumber\": \"49590338271490256608559692540925702759324208523137515618\",\n" + + " \"data\": \"VGhpcyBpcyBvbmx5IGEgdGVzdC4=\",\n" + + " \"approximateArrivalTimestamp\": 1545084711.166\n" + + " },\n" + + " \"eventSource\": \"aws:kinesis\",\n" + + " \"eventVersion\": \"1.0\",\n" + + " \"eventID\": \"shardId-000000000006:49590338271490256608559692540925702759324208523137515618\",\n" + + " \"eventName\": \"aws:kinesis:record\",\n" + + " \"invokeIdentityArn\": \"arn:aws:iam::123456789012:role/lambda-role\",\n" + + " \"awsRegion\": \"us-east-2\",\n" + + " \"eventSourceARN\": \"arn:aws:kinesis:us-east-2:123456789012:stream/lambda-stream\"\n" + + " }\n" + + " ]\n" + + "}"); + events.put( + SQSEvent.class, + "{\n" + + " \"Records\": [\n" + + " {\n" + + " \"messageId\": \"059f36b4-87a3-44ab-83d2-661975830a7d\",\n" + + " \"receiptHandle\": \"AQEBwJnKyrHigUMZj6rYigCgxlaS3SLy0a...\",\n" + + " \"body\": \"Test message.\",\n" + + " \"attributes\": {\n" + + " \"ApproximateReceiveCount\": \"1\",\n" + + " \"SentTimestamp\": \"1545082649183\",\n" + + " \"SenderId\": \"AIDAIENQZJOLO23YVJ4VO\",\n" + + " \"ApproximateFirstReceiveTimestamp\": \"1545082649185\"\n" + + " },\n" + + " \"messageAttributes\": {},\n" + + " \"md5OfBody\": \"e4e68fb7bd0e697a0ae8f1bb342846b3\",\n" + + " \"eventSource\": \"aws:sqs\",\n" + + " \"eventSourceARN\": \"arn:aws:sqs:us-east-2:123456789012:my-queue\",\n" + + " \"awsRegion\": \"us-east-2\"\n" + + " },\n" + + " {\n" + + " \"messageId\": \"2e1424d4-f796-459a-8184-9c92662be6da\",\n" + + " \"receiptHandle\": \"AQEBzWwaftRI0KuVm4tP+/7q1rGgNqicHq...\",\n" + + " \"body\": \"Test message.\",\n" + + " \"attributes\": {\n" + + " \"ApproximateReceiveCount\": \"1\",\n" + + " \"SentTimestamp\": \"1545082650636\",\n" + + " \"SenderId\": \"AIDAIENQZJOLO23YVJ4VO\",\n" + + " \"ApproximateFirstReceiveTimestamp\": \"1545082650649\"\n" + + " },\n" + + " \"messageAttributes\": {},\n" + + " \"md5OfBody\": \"e4e68fb7bd0e697a0ae8f1bb342846b3\",\n" + + " \"eventSource\": \"aws:sqs\",\n" + + " \"eventSourceARN\": \"arn:aws:sqs:us-east-2:123456789012:my-queue\",\n" + + " \"awsRegion\": \"us-east-2\"\n" + + " }\n" + + " ]\n" + + "}"); + events.put( + S3Event.class, + "{\n" + + " \"Records\": [\n" + + " {\n" + + " \"eventVersion\": \"2.1\",\n" + + " \"eventSource\": \"aws:s3\",\n" + + " \"awsRegion\": \"us-east-2\",\n" + + " \"eventTime\": \"2019-09-03T19:37:27.192Z\",\n" + + " \"eventName\": \"ObjectCreated:Put\",\n" + + " \"userIdentity\": {\n" + + " \"principalId\": \"AWS:AIDAINPONIXQXHT3IKHL2\"\n" + + " },\n" + + " \"requestParameters\": {\n" + + " \"sourceIPAddress\": \"205.255.255.255\"\n" + + " },\n" + + " \"responseElements\": {\n" + + " \"x-amz-request-id\": \"D82B88E5F771F645\",\n" + + " \"x-amz-id-2\": \"vlR7PnpV2Ce81l0PRw6jlUpck7Jo5ZsQjryTjKlc5aLWGVHPZLj5NeC6qMa0emYBDXOo6QBU0Wo=\"\n" + + " },\n" + + " \"s3\": {\n" + + " \"s3SchemaVersion\": \"1.0\",\n" + + " \"configurationId\": \"828aa6fc-f7b5-4305-8584-487c791949c1\",\n" + + " \"bucket\": {\n" + + " \"name\": \"DOC-EXAMPLE-BUCKET\",\n" + + " \"ownerIdentity\": {\n" + + " \"principalId\": \"A3I5XTEXAMAI3E\"\n" + + " },\n" + + " \"arn\": \"arn:aws:s3:::lambda-artifacts-deafc19498e3f2df\"\n" + + " },\n" + + " \"object\": {\n" + + " \"key\": \"b21b84d653bb07b05b1e6b33684dc11b\",\n" + + " \"size\": 1305107,\n" + + " \"eTag\": \"b21b84d653bb07b05b1e6b33684dc11b\",\n" + + " \"sequencer\": \"0C0F6F405D6ED209E1\"\n" + + " }\n" + + " }\n" + + " }\n" + + " ]\n" + + "}"); + events.put( + SNSEvent.class, + "{\n" + + " \"Records\": [\n" + + " {\n" + + " \"EventVersion\": \"1.0\",\n" + + " \"EventSubscriptionArn\": \"arn:aws:sns:us-east-2:123456789012:sns-lambda:21be56ed-a058-49f5-8c98-aedd2564c486\",\n" + + " \"EventSource\": \"aws:sns\",\n" + + " \"Sns\": {\n" + + " \"SignatureVersion\": \"1\",\n" + + " \"Timestamp\": \"2019-01-02T12:45:07.000Z\",\n" + + " \"Signature\": \"tcc6faL2yUC6dgZdmrwh1Y4cGa/ebXEkAi6RibDsvpi+tE/1+82j...65r==\",\n" + + " \"SigningCertUrl\": \"https://sns.us-east-2.amazonaws.com/SimpleNotificationService-ac565b8b1a6c5d002d285f9598aa1d9b.pem\",\n" + + " \"MessageId\": \"95df01b4-ee98-5cb9-9903-4c221d41eb5e\",\n" + + " \"Message\": \"Hello from SNS!\",\n" + + " \"MessageAttributes\": {\n" + + " \"Test\": {\n" + + " \"Type\": \"String\",\n" + + " \"Value\": \"TestString\"\n" + + " },\n" + + " \"TestBinary\": {\n" + + " \"Type\": \"Binary\",\n" + + " \"Value\": \"TestBinary\"\n" + + " }\n" + + " },\n" + + " \"Type\": \"Notification\",\n" + + " \"UnsubscribeUrl\": \"https://sns.us-east-2.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:us-east-2:123456789012:test-lambda:21be56ed-a058-49f5-8c98-aedd2564c486\",\n" + + " \"TopicArn\":\"arn:aws:sns:us-east-2:123456789012:sns-lambda\",\n" + + " \"Subject\": \"TestInvoke\"\n" + + " }\n" + + " }\n" + + " ]\n" + + "}"); + return events; + } + + private static void assertScheduledEvent(ScheduledEvent event) { + assertThat(event).isNotNull(); + assertThat(event.getSource()).isEqualTo("aws.events"); + assertThat(event.getDetailType()).isEqualTo("Scheduled Event"); + assertThat(event.getAccount()).isEqualTo("123456789012"); + assertThat(event.getId()).isEqualTo("53dc4d37-cffa-4f76-80c9-8b7d4a4d2eaa"); + assertThat(event.getRegion()).isEqualTo("us-east-1"); + assertThat(event.getTime().getMillis()) + .isEqualTo(new DateTime("2015-10-08T16:53:06Z").getMillis()); + } + + private static void assertKinesisEvent(KinesisEvent event) { + assertThat(event).isNotNull(); + assertThat(event.getRecords()).isNotNull(); + assertThat(event.getRecords().size()).isEqualTo(2); + KinesisEvent.KinesisEventRecord record = event.getRecords().get(0); + assertThat(record.getEventSource()).isEqualTo("aws:kinesis"); + assertThat(record.getEventSourceARN()) + .isEqualTo("arn:aws:kinesis:us-east-2:123456789012:stream/lambda-stream"); + assertThat(record.getEventName()).isEqualTo("aws:kinesis:record"); + assertThat(record.getEventID()) + .isEqualTo("shardId-000000000006:49590338271490256608559692538361571095921575989136588898"); + assertThat(record.getKinesis().getPartitionKey()).isEqualTo("1"); + assertThat(record.getKinesis().getSequenceNumber()) + .isEqualTo("49590338271490256608559692538361571095921575989136588898"); + assertThat(new String(record.getKinesis().getData().array(), StandardCharsets.UTF_8)) + .isEqualTo("Hello, this is a test."); + } + + private static void assertSqsEvent(SQSEvent event) { + assertThat(event).isNotNull(); + assertThat(event.getRecords()).isNotNull(); + assertThat(event.getRecords().size()).isEqualTo(2); + SQSEvent.SQSMessage record = event.getRecords().get(0); + assertThat(record.getEventSource()).isEqualTo("aws:sqs"); + assertThat(record.getEventSourceArn()).isEqualTo("arn:aws:sqs:us-east-2:123456789012:my-queue"); + assertThat(record.getMessageId()).isEqualTo("059f36b4-87a3-44ab-83d2-661975830a7d"); + assertThat(record.getBody()).isEqualTo("Test message."); + assertThat(record.getMd5OfBody()).isEqualTo("e4e68fb7bd0e697a0ae8f1bb342846b3"); + assertThat(record.getReceiptHandle()).isEqualTo("AQEBwJnKyrHigUMZj6rYigCgxlaS3SLy0a..."); + } + + private static void assertS3Event(S3Event event) { + assertThat(event).isNotNull(); + assertThat(event.getRecords()).isNotNull(); + assertThat(event.getRecords().size()).isEqualTo(1); + S3EventNotification.S3EventNotificationRecord record = event.getRecords().get(0); + assertThat(record.getEventSource()).isEqualTo("aws:s3"); + assertThat(record.getEventName()).isEqualTo("ObjectCreated:Put"); + assertThat(record.getS3().getBucket().getName()).isEqualTo("DOC-EXAMPLE-BUCKET"); + assertThat(record.getS3().getBucket().getArn()) + .isEqualTo("arn:aws:s3:::lambda-artifacts-deafc19498e3f2df"); + assertThat(record.getS3().getObject().getKey()).isEqualTo("b21b84d653bb07b05b1e6b33684dc11b"); + } + + private static void assertSnsEvent(SNSEvent event) { + assertThat(event).isNotNull(); + assertThat(event.getRecords()).isNotNull(); + assertThat(event.getRecords().size()).isEqualTo(1); + SNSEvent.SNSRecord record = event.getRecords().get(0); + assertThat(record.getEventSource()).isEqualTo("aws:sns"); + assertThat(record.getEventSubscriptionArn()) + .isEqualTo( + "arn:aws:sns:us-east-2:123456789012:sns-lambda:21be56ed-a058-49f5-8c98-aedd2564c486"); + assertThat(record.getSNS().getMessageId()).isEqualTo("95df01b4-ee98-5cb9-9903-4c221d41eb5e"); + assertThat(record.getSNS().getMessage()).isEqualTo("Hello from SNS!"); + assertThat(record.getSNS().getType()).isEqualTo("Notification"); + assertThat(record.getSNS().getTopicArn()) + .isEqualTo("arn:aws:sns:us-east-2:123456789012:sns-lambda"); + assertThat(record.getSNS().getSubject()).isEqualTo("TestInvoke"); + } + + @Test + void scheduledEventDeserializedFromStringJson() { + String eventBody = events.get(ScheduledEvent.class); + ScheduledEvent event = SerializationUtil.fromJson(eventBody, ScheduledEvent.class); + + assertScheduledEvent(event); + } + + @Test + void scheduledEventDeserializedFromInputStreamJson() { + String eventBody = events.get(ScheduledEvent.class); + ScheduledEvent event = + SerializationUtil.fromJson( + new ByteArrayInputStream(eventBody.getBytes(StandardCharsets.UTF_8)), + ScheduledEvent.class); + + assertScheduledEvent(event); + } + + @Test + void kinesisEventDeserializedFromStringJson() { + String eventBody = events.get(KinesisEvent.class); + KinesisEvent event = SerializationUtil.fromJson(eventBody, KinesisEvent.class); + + assertKinesisEvent(event); + } + + @Test + void kinesisEventDeserializedFromInputStreamJson() { + String eventBody = events.get(KinesisEvent.class); + KinesisEvent event = + SerializationUtil.fromJson( + new ByteArrayInputStream(eventBody.getBytes(StandardCharsets.UTF_8)), + KinesisEvent.class); + + assertKinesisEvent(event); + } + + @Test + void sqsEventDeserializedFromStringJson() { + String eventBody = events.get(SQSEvent.class); + SQSEvent event = SerializationUtil.fromJson(eventBody, SQSEvent.class); + + assertSqsEvent(event); + } + + @Test + void sqsEventDeserializedFromInputStreamJson() { + String eventBody = events.get(SQSEvent.class); + SQSEvent event = + SerializationUtil.fromJson( + new ByteArrayInputStream(eventBody.getBytes(StandardCharsets.UTF_8)), SQSEvent.class); + + assertSqsEvent(event); + } + + @Test + void s3EventDeserializedFromStringJson() { + String eventBody = events.get(S3Event.class); + S3Event event = SerializationUtil.fromJson(eventBody, S3Event.class); + + assertS3Event(event); + } + + @Test + void s3EventDeserializedFromInputStreamJson() { + String eventBody = events.get(S3Event.class); + S3Event event = + SerializationUtil.fromJson( + new ByteArrayInputStream(eventBody.getBytes(StandardCharsets.UTF_8)), S3Event.class); + + assertS3Event(event); + } + + @Test + void snsEventDeserializedFromStringJson() { + String eventBody = events.get(SNSEvent.class); + SNSEvent event = SerializationUtil.fromJson(eventBody, SNSEvent.class); + + assertSnsEvent(event); + } + + @Test + void snsEventDeserializedFromInputStreamJson() { + String eventBody = events.get(SNSEvent.class); + SNSEvent event = + SerializationUtil.fromJson( + new ByteArrayInputStream(eventBody.getBytes(StandardCharsets.UTF_8)), SNSEvent.class); + + assertSnsEvent(event); + } +} diff --git a/instrumentation/aws-lambda/aws-lambda-events-2.2/testing/build.gradle.kts b/instrumentation/aws-lambda/aws-lambda-events-2.2/testing/build.gradle.kts index 8d7e6c135dce..9eb92feeae0f 100644 --- a/instrumentation/aws-lambda/aws-lambda-events-2.2/testing/build.gradle.kts +++ b/instrumentation/aws-lambda/aws-lambda-events-2.2/testing/build.gradle.kts @@ -13,8 +13,6 @@ dependencies { implementation("com.google.guava:guava") - implementation("org.apache.groovy:groovy") implementation("io.opentelemetry:opentelemetry-api") - implementation("org.spockframework:spock-core") implementation("com.github.stefanbirkner:system-lambda") } diff --git a/instrumentation/aws-lambda/aws-lambda-events-2.2/testing/src/main/java/io/opentelemetry/instrumentation/awslambdaevents/v2_2/AbstractAwsLambdaSqsEventHandlerTest.java b/instrumentation/aws-lambda/aws-lambda-events-2.2/testing/src/main/java/io/opentelemetry/instrumentation/awslambdaevents/v2_2/AbstractAwsLambdaSqsEventHandlerTest.java index f1418b73dbc8..c7003983787c 100644 --- a/instrumentation/aws-lambda/aws-lambda-events-2.2/testing/src/main/java/io/opentelemetry/instrumentation/awslambdaevents/v2_2/AbstractAwsLambdaSqsEventHandlerTest.java +++ b/instrumentation/aws-lambda/aws-lambda-events-2.2/testing/src/main/java/io/opentelemetry/instrumentation/awslambdaevents/v2_2/AbstractAwsLambdaSqsEventHandlerTest.java @@ -14,7 +14,8 @@ import com.amazonaws.services.lambda.runtime.events.SQSEvent; import io.opentelemetry.api.trace.SpanKind; import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.opentelemetry.semconv.incubating.FaasIncubatingAttributes; +import io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes; import java.lang.reflect.Constructor; import java.util.Arrays; import java.util.Collections; @@ -73,14 +74,17 @@ void sameSource() { span.hasName("my_function") .hasKind(SpanKind.SERVER) .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.FAAS_INVOCATION_ID, "1-22-333")), + equalTo(FaasIncubatingAttributes.FAAS_INVOCATION_ID, "1-22-333")), span -> span.hasName("queue1 process") .hasKind(SpanKind.CONSUMER) .hasParentSpanId(trace.getSpan(0).getSpanId()) .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.MESSAGING_SYSTEM, "AmazonSQS"), - equalTo(SemanticAttributes.MESSAGING_OPERATION, "process")) + equalTo( + MessagingIncubatingAttributes.MESSAGING_SYSTEM, + MessagingIncubatingAttributes.MessagingSystemValues.AWS_SQS), + equalTo( + MessagingIncubatingAttributes.MESSAGING_OPERATION, "process")) .hasLinksSatisfying( links -> assertThat(links) @@ -119,14 +123,17 @@ void differentSource() { span.hasName("my_function") .hasKind(SpanKind.SERVER) .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.FAAS_INVOCATION_ID, "1-22-333")), + equalTo(FaasIncubatingAttributes.FAAS_INVOCATION_ID, "1-22-333")), span -> span.hasName("multiple_sources process") .hasKind(SpanKind.CONSUMER) .hasParentSpanId(trace.getSpan(0).getSpanId()) .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.MESSAGING_SYSTEM, "AmazonSQS"), - equalTo(SemanticAttributes.MESSAGING_OPERATION, "process")) + equalTo( + MessagingIncubatingAttributes.MESSAGING_SYSTEM, + MessagingIncubatingAttributes.MessagingSystemValues.AWS_SQS), + equalTo( + MessagingIncubatingAttributes.MESSAGING_OPERATION, "process")) .hasLinksSatisfying( links -> assertThat(links) diff --git a/instrumentation/aws-sdk/README.md b/instrumentation/aws-sdk/README.md index afc2b69874a7..dd332b7cfe36 100644 --- a/instrumentation/aws-sdk/README.md +++ b/instrumentation/aws-sdk/README.md @@ -2,10 +2,11 @@ For more information, see the respective public setters in the `AwsSdkTelemetryBuilder` classes: -* [SDK v1](./aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AwsSdkTelemetryBuilder.java) -* [SDK v2](./aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AwsSdkTelemetryBuilder.java) +- [SDK v1](./aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AwsSdkTelemetryBuilder.java) +- [SDK v2](./aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AwsSdkTelemetryBuilder.java) -| System property | Type | Default | Description | -|---|---|---|------------------------------------------------------------------------------------------------------------------------------------------------| -| `otel.instrumentation.aws-sdk.experimental-span-attributes` | Boolean | `false` | Enable the capture of experimental span attributes. | -| `otel.instrumentation.aws-sdk.experimental-use-propagator-for-messaging` | Boolean | `false` | Enable propagation via message attributes using configured propagator (in addition to X-Ray). At the moment, Supports only SQS and the v2 SDK. | +| System property | Type | Default | Description | +|--------------------------------------------------------------------------| ------- | ------- |---------------------------------------------------------------------------------------------------------------------------------------| +| `otel.instrumentation.aws-sdk.experimental-span-attributes` | Boolean | `false` | Enable the capture of experimental span attributes. | +| `otel.instrumentation.aws-sdk.experimental-use-propagator-for-messaging` | Boolean | `false` | v2 only, inject into SNS/SQS attributes with configured propagator: See [v2 README](aws-sdk-2.2/library/README.md#trace-propagation). | +| `otel.instrumentation.aws-sdk.experimental-record-individual-http-error` | Boolean | `false` | v2 only, record errors returned by each individual HTTP request as events for the SDK span. | diff --git a/instrumentation/aws-sdk/aws-sdk-1.11/javaagent/build.gradle.kts b/instrumentation/aws-sdk/aws-sdk-1.11/javaagent/build.gradle.kts index c226dad0fc1e..f357a19f885d 100644 --- a/instrumentation/aws-sdk/aws-sdk-1.11/javaagent/build.gradle.kts +++ b/instrumentation/aws-sdk/aws-sdk-1.11/javaagent/build.gradle.kts @@ -15,6 +15,22 @@ muzzle { module.set("aws-java-sdk-core") versions.set("[1.10.33,)") assertInverse.set(true) + + excludeInstrumentationName("aws-sdk-1.11-sqs") + } + + fail { + group.set("com.amazonaws") + module.set("aws-java-sdk-core") + versions.set("[1.10.33,)") + + excludeInstrumentationName("aws-sdk-1.11-core") + } + + pass { + group.set("com.amazonaws") + module.set("aws-java-sdk-sqs") + versions.set("[1.10.33,)") } } @@ -45,6 +61,9 @@ dependencies { // needed by S3 testImplementation("javax.xml.bind:jaxb-api:2.3.1") + + // last version that does not use json protocol + latestDepTestLibrary("com.amazonaws:aws-java-sdk-sqs:1.12.583") } testing { @@ -65,6 +84,9 @@ testing { implementation("com.amazonaws:aws-java-sdk-kinesis:1.11.0") implementation("com.amazonaws:aws-java-sdk-dynamodb:1.11.0") implementation("com.amazonaws:aws-java-sdk-sns:1.11.0") + + // needed by S3 + implementation("javax.xml.bind:jaxb-api:2.3.1") } } @@ -75,7 +97,33 @@ testing { dependencies { implementation(project(":instrumentation:aws-sdk:aws-sdk-1.11:testing")) - implementation("com.amazonaws:aws-java-sdk-sqs:1.11.106") + if (findProperty("testLatestDeps") as Boolean) { + // last version that does not use json protocol + implementation("com.amazonaws:aws-java-sdk-sqs:1.12.583") + } else { + implementation("com.amazonaws:aws-java-sdk-sqs:1.11.106") + } + } + + targets { + all { + testTask.configure { + jvmArgs("-Dotel.instrumentation.messaging.experimental.receive-telemetry.enabled=true") + } + } + } + } + + val testSqsNoReceiveTelemetry by registering(JvmTestSuite::class) { + dependencies { + implementation(project(":instrumentation:aws-sdk:aws-sdk-1.11:testing")) + + if (findProperty("testLatestDeps") as Boolean) { + // last version that does not use json protocol + implementation("com.amazonaws:aws-java-sdk-sqs:1.12.583") + } else { + implementation("com.amazonaws:aws-java-sdk-sqs:1.11.106") + } } } } @@ -86,14 +134,32 @@ tasks { check { dependsOn(testing.suites) } + } else { + check { + dependsOn(testing.suites.named("testSqs"), testing.suites.named("testSqsNoReceiveTelemetry")) + } } test { - systemProperty("testLatestDeps", findProperty("testLatestDeps") as Boolean) + usesService(gradle.sharedServices.registrations["testcontainersBuildService"].service) } withType().configureEach { // TODO run tests both with and without experimental span attributes jvmArgs("-Dotel.instrumentation.aws-sdk.experimental-span-attributes=true") + systemProperty("testLatestDeps", findProperty("testLatestDeps") as Boolean) + } +} + +if (!(findProperty("testLatestDeps") as Boolean)) { + configurations.testRuntimeClasspath { + resolutionStrategy { + eachDependency { + // early versions of aws sdk are not compatible with jackson 2.16.0 + if (requested.group.startsWith("com.fasterxml.jackson")) { + useVersion("2.15.3") + } + } + } } } diff --git a/instrumentation/aws-sdk/aws-sdk-1.11/javaagent/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/SqsAdviceBridge.java b/instrumentation/aws-sdk/aws-sdk-1.11/javaagent/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/SqsAdviceBridge.java new file mode 100644 index 000000000000..efa6fe9a3428 --- /dev/null +++ b/instrumentation/aws-sdk/aws-sdk-1.11/javaagent/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/SqsAdviceBridge.java @@ -0,0 +1,15 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.awssdk.v1_11; + +public final class SqsAdviceBridge { + private SqsAdviceBridge() {} + + public static void referenceForMuzzleOnly() { + throw new UnsupportedOperationException( + SqsImpl.class.getName() + " referencing for muzzle, should never be actually called"); + } +} diff --git a/instrumentation/aws-sdk/aws-sdk-1.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/awssdk/v1_11/AbstractAwsSdkInstrumentationModule.java b/instrumentation/aws-sdk/aws-sdk-1.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/awssdk/v1_11/AbstractAwsSdkInstrumentationModule.java new file mode 100644 index 000000000000..ff02429b910c --- /dev/null +++ b/instrumentation/aws-sdk/aws-sdk-1.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/awssdk/v1_11/AbstractAwsSdkInstrumentationModule.java @@ -0,0 +1,67 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.awssdk.v1_11; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.hasClassesNamed; +import static java.util.Collections.singletonList; +import static net.bytebuddy.matcher.ElementMatchers.named; + +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.extension.instrumentation.internal.ExperimentalInstrumentationModule; +import java.util.List; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +// TODO: Copy & paste with only trivial adaptions from v2 +abstract class AbstractAwsSdkInstrumentationModule extends InstrumentationModule + implements ExperimentalInstrumentationModule { + + protected AbstractAwsSdkInstrumentationModule(String additionalInstrumentationName) { + super("aws-sdk", "aws-sdk-1.11", additionalInstrumentationName); + } + + @Override + public boolean isHelperClass(String className) { + return className.startsWith("io.opentelemetry.contrib.awsxray."); + } + + @Override + public String getModuleGroup() { + return "aws-sdk"; + } + + @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("com.amazonaws.AmazonWebServiceClient"); + } + + @Override + public List typeInstrumentations() { + return singletonList(new ResourceInjectingTypeInstrumentation()); + } + + abstract void doTransform(TypeTransformer transformer); + + // A type instrumentation is needed to trigger resource injection. + public 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("com.amazonaws.AmazonWebServiceClient"); + } + + @Override + public void transform(TypeTransformer transformer) { + doTransform(transformer); + } + } +} diff --git a/instrumentation/aws-sdk/aws-sdk-1.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/awssdk/v1_11/AwsSdkInstrumentationModule.java b/instrumentation/aws-sdk/aws-sdk-1.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/awssdk/v1_11/AwsSdkInstrumentationModule.java index 99d72487993e..89a4a6c8f673 100644 --- a/instrumentation/aws-sdk/aws-sdk-1.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/awssdk/v1_11/AwsSdkInstrumentationModule.java +++ b/instrumentation/aws-sdk/aws-sdk-1.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/awssdk/v1_11/AwsSdkInstrumentationModule.java @@ -10,12 +10,14 @@ 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.internal.ExperimentalInstrumentationModule; import java.util.List; @AutoService(InstrumentationModule.class) -public class AwsSdkInstrumentationModule extends InstrumentationModule { +public class AwsSdkInstrumentationModule extends InstrumentationModule + implements ExperimentalInstrumentationModule { public AwsSdkInstrumentationModule() { - super("aws-sdk", "aws-sdk-1.11"); + super("aws-sdk", "aws-sdk-1.11", "aws-sdk-1.11-core"); } @Override @@ -23,6 +25,11 @@ public boolean isHelperClass(String className) { return className.startsWith("io.opentelemetry.contrib.awsxray."); } + @Override + public String getModuleGroup() { + return "aws-sdk"; + } + @Override public List typeInstrumentations() { return asList( diff --git a/instrumentation/aws-sdk/aws-sdk-1.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/awssdk/v1_11/SqsInstrumentationModule.java b/instrumentation/aws-sdk/aws-sdk-1.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/awssdk/v1_11/SqsInstrumentationModule.java new file mode 100644 index 000000000000..cc61cf6d9cc0 --- /dev/null +++ b/instrumentation/aws-sdk/aws-sdk-1.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/awssdk/v1_11/SqsInstrumentationModule.java @@ -0,0 +1,38 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.awssdk.v1_11; + +import static net.bytebuddy.matcher.ElementMatchers.none; + +import com.google.auto.service.AutoService; +import io.opentelemetry.instrumentation.awssdk.v1_11.SqsAdviceBridge; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import net.bytebuddy.asm.Advice; + +@AutoService(InstrumentationModule.class) +public class SqsInstrumentationModule extends AbstractAwsSdkInstrumentationModule { + + public SqsInstrumentationModule() { + super("aws-sdk-1.11-sqs"); + } + + @Override + public void doTransform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + none(), SqsInstrumentationModule.class.getName() + "$RegisterAdvice"); + } + + @SuppressWarnings("unused") + public static class RegisterAdvice { + @Advice.OnMethodExit(suppress = Throwable.class) + public static void onExit() { + // (indirectly) using SqsImpl class here to make sure it is available from SqsAccess + // (injected into app classloader) and checked by Muzzle + SqsAdviceBridge.referenceForMuzzleOnly(); + } + } +} diff --git a/instrumentation/aws-sdk/aws-sdk-1.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/awssdk/v1_11/TracingRequestHandler.java b/instrumentation/aws-sdk/aws-sdk-1.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/awssdk/v1_11/TracingRequestHandler.java index 0e31a1f29c16..872c7303507a 100644 --- a/instrumentation/aws-sdk/aws-sdk-1.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/awssdk/v1_11/TracingRequestHandler.java +++ b/instrumentation/aws-sdk/aws-sdk-1.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/awssdk/v1_11/TracingRequestHandler.java @@ -14,7 +14,8 @@ import io.opentelemetry.context.Context; import io.opentelemetry.context.Scope; import io.opentelemetry.instrumentation.awssdk.v1_11.AwsSdkTelemetry; -import io.opentelemetry.javaagent.bootstrap.internal.InstrumentationConfig; +import io.opentelemetry.javaagent.bootstrap.internal.AgentInstrumentationConfig; +import io.opentelemetry.javaagent.bootstrap.internal.ExperimentalConfig; /** * A {@link RequestHandler2} for use in the agent. Unlike library instrumentation, the agent will @@ -35,8 +36,11 @@ public class TracingRequestHandler extends RequestHandler2 { public static final RequestHandler2 tracingHandler = AwsSdkTelemetry.builder(GlobalOpenTelemetry.get()) .setCaptureExperimentalSpanAttributes( - InstrumentationConfig.get() + AgentInstrumentationConfig.get() .getBoolean("otel.instrumentation.aws-sdk.experimental-span-attributes", false)) + .setMessagingReceiveInstrumentationEnabled( + ExperimentalConfig.get().messagingReceiveInstrumentationEnabled()) + .setCapturedHeaders(ExperimentalConfig.get().getMessagingHeaders()) .build() .newRequestHandler(); diff --git a/instrumentation/aws-sdk/aws-sdk-1.11/javaagent/src/test/groovy/S3TracingTest.groovy b/instrumentation/aws-sdk/aws-sdk-1.11/javaagent/src/test/groovy/S3TracingTest.groovy index 39e09dde97e8..1dea81b84719 100644 --- a/instrumentation/aws-sdk/aws-sdk-1.11/javaagent/src/test/groovy/S3TracingTest.groovy +++ b/instrumentation/aws-sdk/aws-sdk-1.11/javaagent/src/test/groovy/S3TracingTest.groovy @@ -4,7 +4,11 @@ */ import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes +import io.opentelemetry.semconv.ServerAttributes +import io.opentelemetry.semconv.HttpAttributes +import io.opentelemetry.semconv.NetworkAttributes +import io.opentelemetry.semconv.UrlAttributes import spock.lang.Shared import static io.opentelemetry.api.trace.SpanKind.CLIENT @@ -36,13 +40,17 @@ class S3TracingTest extends AgentInstrumentationSpecification { awsConnector.receiveMessage(queueUrl) awsConnector.putSampleData(bucketName) // traced message - awsConnector.receiveMessage(queueUrl) + def receiveMessageResult = awsConnector.receiveMessage(queueUrl) + receiveMessageResult.messages.each {message -> + runWithSpan("process child") {} + } + // cleanup awsConnector.deleteBucket(bucketName) awsConnector.purgeQueue(queueUrl) then: - assertTraces(12) { + assertTraces(10) { trace(0, 1) { span(0) { @@ -56,15 +64,12 @@ class S3TracingTest extends AgentInstrumentationSpecification { "aws.queue.name" queueName "rpc.system" "aws-api" "rpc.service" "AmazonSQS" - "http.method" "POST" - "http.status_code" 200 - "http.url" String - "net.peer.name" String - "net.protocol.name" "http" - "net.protocol.version" "1.1" - "net.peer.port" { it == null || Number } - "$SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH" { it == null || it instanceof Long } - "$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" { it == null || it instanceof Long } + "$HttpAttributes.HTTP_REQUEST_METHOD" "POST" + "$HttpAttributes.HTTP_RESPONSE_STATUS_CODE" 200 + "$UrlAttributes.URL_FULL" { it.startsWith("http://") } + "$ServerAttributes.SERVER_ADDRESS" String + "$NetworkAttributes.NETWORK_PROTOCOL_VERSION" "1.1" + "$ServerAttributes.SERVER_PORT" { it == null || Number } } } } @@ -81,15 +86,12 @@ class S3TracingTest extends AgentInstrumentationSpecification { "rpc.system" "aws-api" "rpc.service" "Amazon S3" "aws.bucket.name" bucketName - "http.method" "PUT" - "http.status_code" 200 - "http.url" String - "net.peer.name" String - "net.protocol.name" "http" - "net.protocol.version" "1.1" - "net.peer.port" { it == null || Number } - "$SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH" { it == null || it instanceof Long } - "$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" { it == null || it instanceof Long } + "$HttpAttributes.HTTP_REQUEST_METHOD" "PUT" + "$HttpAttributes.HTTP_RESPONSE_STATUS_CODE" 200 + "$UrlAttributes.URL_FULL" { it.startsWith("http://") } + "$ServerAttributes.SERVER_ADDRESS" String + "$NetworkAttributes.NETWORK_PROTOCOL_VERSION" "1.1" + "$ServerAttributes.SERVER_PORT" { it == null || Number } } } } @@ -106,15 +108,12 @@ class S3TracingTest extends AgentInstrumentationSpecification { "aws.queue.url" queueUrl "rpc.system" "aws-api" "rpc.service" "AmazonSQS" - "http.method" "POST" - "http.status_code" 200 - "http.url" String - "net.peer.name" String - "net.protocol.name" "http" - "net.protocol.version" "1.1" - "net.peer.port" { it == null || Number } - "$SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH" { it == null || it instanceof Long } - "$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" { it == null || it instanceof Long } + "$HttpAttributes.HTTP_REQUEST_METHOD" "POST" + "$HttpAttributes.HTTP_RESPONSE_STATUS_CODE" 200 + "$UrlAttributes.URL_FULL" { it.startsWith("http://") } + "$ServerAttributes.SERVER_ADDRESS" String + "$NetworkAttributes.NETWORK_PROTOCOL_VERSION" "1.1" + "$ServerAttributes.SERVER_PORT" { it == null || Number } } } } @@ -131,15 +130,12 @@ class S3TracingTest extends AgentInstrumentationSpecification { "aws.queue.url" queueUrl "rpc.system" "aws-api" "rpc.service" "AmazonSQS" - "http.method" "POST" - "http.status_code" 200 - "http.url" String - "net.peer.name" String - "net.protocol.name" "http" - "net.protocol.version" "1.1" - "net.peer.port" { it == null || Number } - "$SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH" { it == null || it instanceof Long } - "$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" { it == null || it instanceof Long } + "$HttpAttributes.HTTP_REQUEST_METHOD" "POST" + "$HttpAttributes.HTTP_RESPONSE_STATUS_CODE" 200 + "$UrlAttributes.URL_FULL" { it.startsWith("http://") } + "$ServerAttributes.SERVER_ADDRESS" String + "$NetworkAttributes.NETWORK_PROTOCOL_VERSION" "1.1" + "$ServerAttributes.SERVER_PORT" { it == null || Number } } } } @@ -156,43 +152,16 @@ class S3TracingTest extends AgentInstrumentationSpecification { "rpc.system" "aws-api" "rpc.service" "Amazon S3" "aws.bucket.name" bucketName - "http.method" "PUT" - "http.status_code" 200 - "http.url" String - "net.peer.name" String - "net.protocol.name" "http" - "net.protocol.version" "1.1" - "net.peer.port" { it == null || Number } - "$SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH" { it == null || it instanceof Long } - "$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" { it == null || it instanceof Long } + "$HttpAttributes.HTTP_REQUEST_METHOD" "PUT" + "$HttpAttributes.HTTP_RESPONSE_STATUS_CODE" 200 + "$UrlAttributes.URL_FULL" { it.startsWith("http://") } + "$ServerAttributes.SERVER_ADDRESS" String + "$NetworkAttributes.NETWORK_PROTOCOL_VERSION" "1.1" + "$ServerAttributes.SERVER_PORT" { it == null || Number } } } } - trace(5, 1) { - span(0) { - name "SQS.ReceiveMessage" - kind CLIENT - hasNoParent() - attributes { - "aws.agent" "java-aws-sdk" - "aws.endpoint" String - "rpc.method" "ReceiveMessage" - "aws.queue.url" queueUrl - "rpc.system" "aws-api" - "rpc.service" "AmazonSQS" - "http.method" "POST" - "http.status_code" 200 - "http.url" String - "net.peer.name" String - "net.protocol.name" "http" - "net.protocol.version" "1.1" - "net.peer.port" { it == null || Number } - "$SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH" { it == null || it instanceof Long } - "$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" { it == null || it instanceof Long } - } - } - } - trace(6, 2) { + trace(5, 3) { span(0) { name "S3.PutObject" kind CLIENT @@ -204,19 +173,16 @@ class S3TracingTest extends AgentInstrumentationSpecification { "rpc.system" "aws-api" "rpc.service" "Amazon S3" "aws.bucket.name" bucketName - "http.method" "PUT" - "http.status_code" 200 - "http.url" String - "net.peer.name" String - "net.protocol.name" "http" - "net.protocol.version" "1.1" - "net.peer.port" { it == null || Number } - "$SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH" { it == null || it instanceof Long } - "$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" { it == null || it instanceof Long } + "$HttpAttributes.HTTP_REQUEST_METHOD" "PUT" + "$HttpAttributes.HTTP_RESPONSE_STATUS_CODE" 200 + "$UrlAttributes.URL_FULL" { it.startsWith("http://") } + "$ServerAttributes.SERVER_ADDRESS" String + "$NetworkAttributes.NETWORK_PROTOCOL_VERSION" "1.1" + "$ServerAttributes.SERVER_PORT" { it == null || Number } } } span(1) { - name "SQS.ReceiveMessage" + name "s3ToSqsTestQueue process" kind CONSUMER childOf span(0) attributes { @@ -226,49 +192,26 @@ class S3TracingTest extends AgentInstrumentationSpecification { "aws.queue.url" queueUrl "rpc.system" "aws-api" "rpc.service" "AmazonSQS" - "http.method" "POST" - "http.status_code" 200 - "http.url" String - "$SemanticAttributes.USER_AGENT_ORIGINAL" String - "net.peer.name" String - "net.protocol.name" "http" - "net.protocol.version" "1.1" - "net.peer.port" { it == null || Number } - "$SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH" { it == null || it instanceof Long } - "$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" { it == null || it instanceof Long } + "$HttpAttributes.HTTP_REQUEST_METHOD" "POST" + "$HttpAttributes.HTTP_RESPONSE_STATUS_CODE" 200 + "$UrlAttributes.URL_FULL" { it.startsWith("http://") } + "$ServerAttributes.SERVER_ADDRESS" String + "$ServerAttributes.SERVER_PORT" { it == null || Number } + "$MessagingIncubatingAttributes.MESSAGING_SYSTEM" MessagingIncubatingAttributes.MessagingSystemValues.AWS_SQS + "$MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME" "s3ToSqsTestQueue" + "$MessagingIncubatingAttributes.MESSAGING_OPERATION" "process" + "$MessagingIncubatingAttributes.MESSAGING_MESSAGE_ID" String + "$NetworkAttributes.NETWORK_PROTOCOL_VERSION" "1.1" } } - } - - /** - * 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(7, 1) { - span(0) { - name "SQS.ReceiveMessage" - kind CLIENT - hasNoParent() + span(2) { + name "process child" + childOf span(1) attributes { - "aws.agent" "java-aws-sdk" - "aws.endpoint" String - "rpc.method" "ReceiveMessage" - "aws.queue.url" queueUrl - "rpc.system" "aws-api" - "rpc.service" "AmazonSQS" - "http.method" "POST" - "http.status_code" 200 - "http.url" String - "net.peer.name" String - "net.protocol.name" "http" - "net.protocol.version" "1.1" - "net.peer.port" { it == null || Number } - "$SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH" { it == null || it instanceof Long } - "$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" { it == null || it instanceof Long } } } } - trace(8, 1) { + trace(6, 1) { span(0) { name "S3.ListObjects" kind CLIENT @@ -280,19 +223,16 @@ class S3TracingTest extends AgentInstrumentationSpecification { "rpc.system" "aws-api" "rpc.service" "Amazon S3" "aws.bucket.name" bucketName - "http.method" "GET" - "http.status_code" 200 - "http.url" String - "net.peer.name" String - "net.protocol.name" "http" - "net.protocol.version" "1.1" - "net.peer.port" { it == null || Number } - "$SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH" { it == null || it instanceof Long } - "$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" { it == null || it instanceof Long } + "$HttpAttributes.HTTP_REQUEST_METHOD" "GET" + "$HttpAttributes.HTTP_RESPONSE_STATUS_CODE" 200 + "$UrlAttributes.URL_FULL" { it.startsWith("http://") } + "$ServerAttributes.SERVER_ADDRESS" String + "$NetworkAttributes.NETWORK_PROTOCOL_VERSION" "1.1" + "$ServerAttributes.SERVER_PORT" { it == null || Number } } } } - trace(9, 1) { + trace(7, 1) { span(0) { name "S3.DeleteObject" kind CLIENT @@ -304,19 +244,16 @@ class S3TracingTest extends AgentInstrumentationSpecification { "rpc.system" "aws-api" "rpc.service" "Amazon S3" "aws.bucket.name" bucketName - "http.method" "DELETE" - "http.status_code" 204 - "http.url" String - "net.peer.name" String - "net.protocol.name" "http" - "net.protocol.version" "1.1" - "net.peer.port" { it == null || Number } - "$SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH" { it == null || it instanceof Long } - "$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" { it == null || it instanceof Long } + "$HttpAttributes.HTTP_REQUEST_METHOD" "DELETE" + "$HttpAttributes.HTTP_RESPONSE_STATUS_CODE" 204 + "$UrlAttributes.URL_FULL" { it.startsWith("http://") } + "$ServerAttributes.SERVER_ADDRESS" String + "$NetworkAttributes.NETWORK_PROTOCOL_VERSION" "1.1" + "$ServerAttributes.SERVER_PORT" { it == null || Number } } } } - trace(10, 1) { + trace(8, 1) { span(0) { name "S3.DeleteBucket" kind CLIENT @@ -328,19 +265,16 @@ class S3TracingTest extends AgentInstrumentationSpecification { "rpc.system" "aws-api" "rpc.service" "Amazon S3" "aws.bucket.name" bucketName - "http.method" "DELETE" - "http.status_code" 204 - "http.url" String - "net.peer.name" String - "net.protocol.name" "http" - "net.protocol.version" "1.1" - "net.peer.port" { it == null || Number } - "$SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH" { it == null || it instanceof Long } - "$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" { it == null || it instanceof Long } + "$HttpAttributes.HTTP_REQUEST_METHOD" "DELETE" + "$HttpAttributes.HTTP_RESPONSE_STATUS_CODE" 204 + "$UrlAttributes.URL_FULL" { it.startsWith("http://") } + "$ServerAttributes.SERVER_ADDRESS" String + "$NetworkAttributes.NETWORK_PROTOCOL_VERSION" "1.1" + "$ServerAttributes.SERVER_PORT" { it == null || Number } } } } - trace(11, 1) { + trace(9, 1) { span(0) { name "SQS.PurgeQueue" kind CLIENT @@ -352,15 +286,12 @@ class S3TracingTest extends AgentInstrumentationSpecification { "aws.queue.url" queueUrl "rpc.system" "aws-api" "rpc.service" "AmazonSQS" - "http.method" "POST" - "http.status_code" 200 - "http.url" String - "net.peer.name" String - "net.protocol.name" "http" - "net.protocol.version" "1.1" - "net.peer.port" { it == null || Number } - "$SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH" { it == null || it instanceof Long } - "$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" { it == null || it instanceof Long } + "$HttpAttributes.HTTP_REQUEST_METHOD" "POST" + "$HttpAttributes.HTTP_RESPONSE_STATUS_CODE" 200 + "$UrlAttributes.URL_FULL" { it.startsWith("http://") } + "$ServerAttributes.SERVER_ADDRESS" String + "$NetworkAttributes.NETWORK_PROTOCOL_VERSION" "1.1" + "$ServerAttributes.SERVER_PORT" { it == null || Number } } } } @@ -387,13 +318,16 @@ class S3TracingTest extends AgentInstrumentationSpecification { awsConnector.receiveMessage(queueUrl) awsConnector.putSampleData(bucketName) // traced message - awsConnector.receiveMessage(queueUrl) + def receiveMessageResult = awsConnector.receiveMessage(queueUrl) + receiveMessageResult.messages.each {message -> + runWithSpan("process child") {} + } // cleanup awsConnector.deleteBucket(bucketName) awsConnector.purgeQueue(queueUrl) then: - assertTraces(16) { + assertTraces(14) { trace(0, 1) { span(0) { name "SQS.CreateQueue" @@ -406,15 +340,12 @@ class S3TracingTest extends AgentInstrumentationSpecification { "aws.queue.name" queueName "rpc.system" "aws-api" "rpc.service" "AmazonSQS" - "http.method" "POST" - "http.status_code" 200 - "http.url" String - "net.peer.name" String - "net.protocol.name" "http" - "net.protocol.version" "1.1" - "net.peer.port" { it == null || Number } - "$SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH" { it == null || it instanceof Long } - "$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" { it == null || it instanceof Long } + "$HttpAttributes.HTTP_REQUEST_METHOD" "POST" + "$HttpAttributes.HTTP_RESPONSE_STATUS_CODE" 200 + "$UrlAttributes.URL_FULL" { it.startsWith("http://") } + "$ServerAttributes.SERVER_ADDRESS" String + "$NetworkAttributes.NETWORK_PROTOCOL_VERSION" "1.1" + "$ServerAttributes.SERVER_PORT" { it == null || Number } } } } @@ -430,15 +361,12 @@ class S3TracingTest extends AgentInstrumentationSpecification { "aws.queue.url" queueUrl "rpc.system" "aws-api" "rpc.service" "AmazonSQS" - "http.method" "POST" - "http.status_code" 200 - "http.url" String - "net.peer.name" String - "net.protocol.name" "http" - "net.protocol.version" "1.1" - "net.peer.port" { it == null || Number } - "$SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH" { it == null || it instanceof Long } - "$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" { it == null || it instanceof Long } + "$HttpAttributes.HTTP_REQUEST_METHOD" "POST" + "$HttpAttributes.HTTP_RESPONSE_STATUS_CODE" 200 + "$UrlAttributes.URL_FULL" { it.startsWith("http://") } + "$ServerAttributes.SERVER_ADDRESS" String + "$NetworkAttributes.NETWORK_PROTOCOL_VERSION" "1.1" + "$ServerAttributes.SERVER_PORT" { it == null || Number } } } } @@ -454,15 +382,12 @@ class S3TracingTest extends AgentInstrumentationSpecification { "rpc.system" "aws-api" "rpc.service" "Amazon S3" "aws.bucket.name" bucketName - "http.method" "PUT" - "http.status_code" 200 - "http.url" String - "net.peer.name" String - "net.protocol.name" "http" - "net.protocol.version" "1.1" - "net.peer.port" { it == null || Number } - "$SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH" { it == null || it instanceof Long } - "$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" { it == null || it instanceof Long } + "$HttpAttributes.HTTP_REQUEST_METHOD" "PUT" + "$HttpAttributes.HTTP_RESPONSE_STATUS_CODE" 200 + "$UrlAttributes.URL_FULL" { it.startsWith("http://") } + "$ServerAttributes.SERVER_ADDRESS" String + "$NetworkAttributes.NETWORK_PROTOCOL_VERSION" "1.1" + "$ServerAttributes.SERVER_PORT" { it == null || Number } } } } @@ -477,15 +402,12 @@ class S3TracingTest extends AgentInstrumentationSpecification { "rpc.method" "CreateTopic" "rpc.system" "aws-api" "rpc.service" "AmazonSNS" - "http.method" "POST" - "http.status_code" 200 - "http.url" String - "net.peer.name" String - "net.protocol.name" "http" - "net.protocol.version" "1.1" - "net.peer.port" { it == null || Number } - "$SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH" { it == null || it instanceof Long } - "$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" { it == null || it instanceof Long } + "$HttpAttributes.HTTP_REQUEST_METHOD" "POST" + "$HttpAttributes.HTTP_RESPONSE_STATUS_CODE" 200 + "$UrlAttributes.URL_FULL" { it.startsWith("http://") } + "$ServerAttributes.SERVER_ADDRESS" String + "$NetworkAttributes.NETWORK_PROTOCOL_VERSION" "1.1" + "$ServerAttributes.SERVER_PORT" { it == null || Number } } } } @@ -500,15 +422,13 @@ class S3TracingTest extends AgentInstrumentationSpecification { "rpc.method" "Subscribe" "rpc.system" "aws-api" "rpc.service" "AmazonSNS" - "http.method" "POST" - "http.status_code" 200 - "http.url" String - "net.peer.name" String - "net.protocol.name" "http" - "net.protocol.version" "1.1" - "net.peer.port" { it == null || Number } - "$SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH" { it == null || it instanceof Long } - "$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" { it == null || it instanceof Long } + "$HttpAttributes.HTTP_REQUEST_METHOD" "POST" + "$HttpAttributes.HTTP_RESPONSE_STATUS_CODE" 200 + "$UrlAttributes.URL_FULL" { it.startsWith("http://") } + "$ServerAttributes.SERVER_ADDRESS" String + "$NetworkAttributes.NETWORK_PROTOCOL_VERSION" "1.1" + "$ServerAttributes.SERVER_PORT" { it == null || Number } + "$MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME" topicArn } } } @@ -524,15 +444,12 @@ class S3TracingTest extends AgentInstrumentationSpecification { "aws.queue.url" queueUrl "rpc.system" "aws-api" "rpc.service" "AmazonSQS" - "http.method" "POST" - "http.status_code" 200 - "http.url" String - "net.peer.name" String - "net.protocol.name" "http" - "net.protocol.version" "1.1" - "net.peer.port" { it == null || Number } - "$SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH" { it == null || it instanceof Long } - "$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" { it == null || it instanceof Long } + "$HttpAttributes.HTTP_REQUEST_METHOD" "POST" + "$HttpAttributes.HTTP_RESPONSE_STATUS_CODE" 200 + "$UrlAttributes.URL_FULL" { it.startsWith("http://") } + "$ServerAttributes.SERVER_ADDRESS" String + "$NetworkAttributes.NETWORK_PROTOCOL_VERSION" "1.1" + "$ServerAttributes.SERVER_PORT" { it == null || Number } } } } @@ -547,15 +464,13 @@ class S3TracingTest extends AgentInstrumentationSpecification { "rpc.method" "SetTopicAttributes" "rpc.system" "aws-api" "rpc.service" "AmazonSNS" - "http.method" "POST" - "http.status_code" 200 - "http.url" String - "net.peer.name" String - "net.protocol.name" "http" - "net.protocol.version" "1.1" - "net.peer.port" { it == null || Number } - "$SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH" { it == null || it instanceof Long } - "$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" { it == null || it instanceof Long } + "$HttpAttributes.HTTP_REQUEST_METHOD" "POST" + "$HttpAttributes.HTTP_RESPONSE_STATUS_CODE" 200 + "$UrlAttributes.URL_FULL" { it.startsWith("http://") } + "$ServerAttributes.SERVER_ADDRESS" String + "$NetworkAttributes.NETWORK_PROTOCOL_VERSION" "1.1" + "$ServerAttributes.SERVER_PORT" { it == null || Number } + "$MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME" topicArn } } } @@ -571,44 +486,16 @@ class S3TracingTest extends AgentInstrumentationSpecification { "rpc.system" "aws-api" "rpc.service" "Amazon S3" "aws.bucket.name" bucketName - "http.method" "PUT" - "http.status_code" 200 - "http.url" String - "net.peer.name" String - "net.protocol.name" "http" - "net.protocol.version" "1.1" - "net.peer.port" { it == null || Number } - "$SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH" { it == null || it instanceof Long } - "$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" { it == null || it instanceof Long } + "$HttpAttributes.HTTP_REQUEST_METHOD" "PUT" + "$HttpAttributes.HTTP_RESPONSE_STATUS_CODE" 200 + "$UrlAttributes.URL_FULL" { it.startsWith("http://") } + "$ServerAttributes.SERVER_ADDRESS" String + "$NetworkAttributes.NETWORK_PROTOCOL_VERSION" "1.1" + "$ServerAttributes.SERVER_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 - "rpc.method" "ReceiveMessage" - "aws.queue.url" queueUrl - "rpc.system" "aws-api" - "rpc.service" "AmazonSQS" - "http.method" "POST" - "http.status_code" 200 - "http.url" String - "net.peer.name" String - "net.protocol.name" "http" - "net.protocol.version" "1.1" - "net.peer.port" { it == null || Number } - "$SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH" { it == null || it instanceof Long } - "$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" { it == null || it instanceof Long } - } - } - } - trace(9, 1) { span(0) { name "S3.PutObject" kind CLIENT @@ -620,26 +507,19 @@ class S3TracingTest extends AgentInstrumentationSpecification { "rpc.system" "aws-api" "rpc.service" "Amazon S3" "aws.bucket.name" bucketName - "http.method" "PUT" - "http.status_code" 200 - "http.url" String - "net.peer.name" String - "net.protocol.name" "http" - "net.protocol.version" "1.1" - "net.peer.port" { it == null || Number } - "$SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH" { it == null || it instanceof Long } - "$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" { it == null || it instanceof Long } + "$HttpAttributes.HTTP_REQUEST_METHOD" "PUT" + "$HttpAttributes.HTTP_RESPONSE_STATUS_CODE" 200 + "$UrlAttributes.URL_FULL" { it.startsWith("http://") } + "$ServerAttributes.SERVER_ADDRESS" String + "$NetworkAttributes.NETWORK_PROTOCOL_VERSION" "1.1" + "$ServerAttributes.SERVER_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(10, 1) { + trace(9, 2) { span(0) { - name "SQS.ReceiveMessage" - kind CLIENT + name "s3ToSnsToSqsTestQueue process" + kind CONSUMER hasNoParent() attributes { "aws.agent" "java-aws-sdk" @@ -648,44 +528,26 @@ class S3TracingTest extends AgentInstrumentationSpecification { "aws.queue.url" queueUrl "rpc.system" "aws-api" "rpc.service" "AmazonSQS" - "http.method" "POST" - "http.status_code" 200 - "http.url" String - "net.peer.name" String - "net.protocol.name" "http" - "net.protocol.version" "1.1" - "net.peer.port" { it == null || Number } - "$SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH" { it == null || it instanceof Long } - "$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" { it == null || it instanceof Long } + "$HttpAttributes.HTTP_REQUEST_METHOD" "POST" + "$HttpAttributes.HTTP_RESPONSE_STATUS_CODE" 200 + "$UrlAttributes.URL_FULL" { it.startsWith("http://") } + "$ServerAttributes.SERVER_ADDRESS" String + "$ServerAttributes.SERVER_PORT" { it == null || Number } + "$MessagingIncubatingAttributes.MESSAGING_SYSTEM" MessagingIncubatingAttributes.MessagingSystemValues.AWS_SQS + "$MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME" "s3ToSnsToSqsTestQueue" + "$MessagingIncubatingAttributes.MESSAGING_OPERATION" "process" + "$MessagingIncubatingAttributes.MESSAGING_MESSAGE_ID" String + "$NetworkAttributes.NETWORK_PROTOCOL_VERSION" "1.1" } } - } - trace(11, 1) { - span(0) { - name "SQS.ReceiveMessage" - kind CONSUMER - hasNoParent() + span(1) { + name "process child" + childOf span(0) attributes { - "aws.agent" "java-aws-sdk" - "aws.endpoint" String - "rpc.method" "ReceiveMessage" - "aws.queue.url" queueUrl - "rpc.system" "aws-api" - "rpc.service" "AmazonSQS" - "http.method" "POST" - "http.status_code" 200 - "http.url" String - "$SemanticAttributes.USER_AGENT_ORIGINAL" String - "net.peer.name" String - "net.protocol.name" "http" - "net.protocol.version" "1.1" - "net.peer.port" { it == null || Number } - "$SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH" { it == null || it instanceof Long } - "$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" { it == null || it instanceof Long } } } } - trace(12, 1) { + trace(10, 1) { span(0) { name "S3.ListObjects" kind CLIENT @@ -697,19 +559,16 @@ class S3TracingTest extends AgentInstrumentationSpecification { "rpc.system" "aws-api" "rpc.service" "Amazon S3" "aws.bucket.name" bucketName - "http.method" "GET" - "http.status_code" 200 - "http.url" String - "net.peer.name" String - "net.protocol.name" "http" - "net.protocol.version" "1.1" - "net.peer.port" { it == null || Number } - "$SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH" { it == null || it instanceof Long } - "$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" { it == null || it instanceof Long } + "$HttpAttributes.HTTP_REQUEST_METHOD" "GET" + "$HttpAttributes.HTTP_RESPONSE_STATUS_CODE" 200 + "$UrlAttributes.URL_FULL" { it.startsWith("http://") } + "$ServerAttributes.SERVER_ADDRESS" String + "$NetworkAttributes.NETWORK_PROTOCOL_VERSION" "1.1" + "$ServerAttributes.SERVER_PORT" { it == null || Number } } } } - trace(13, 1) { + trace(11, 1) { span(0) { name "S3.DeleteObject" kind CLIENT @@ -721,19 +580,16 @@ class S3TracingTest extends AgentInstrumentationSpecification { "rpc.system" "aws-api" "rpc.service" "Amazon S3" "aws.bucket.name" bucketName - "http.method" "DELETE" - "http.status_code" 204 - "http.url" String - "net.peer.name" String - "net.protocol.name" "http" - "net.protocol.version" "1.1" - "net.peer.port" { it == null || Number } - "$SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH" { it == null || it instanceof Long } - "$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" { it == null || it instanceof Long } + "$HttpAttributes.HTTP_REQUEST_METHOD" "DELETE" + "$HttpAttributes.HTTP_RESPONSE_STATUS_CODE" 204 + "$UrlAttributes.URL_FULL" { it.startsWith("http://") } + "$ServerAttributes.SERVER_ADDRESS" String + "$NetworkAttributes.NETWORK_PROTOCOL_VERSION" "1.1" + "$ServerAttributes.SERVER_PORT" { it == null || Number } } } } - trace(14, 1) { + trace(12, 1) { span(0) { name "S3.DeleteBucket" kind CLIENT @@ -745,19 +601,16 @@ class S3TracingTest extends AgentInstrumentationSpecification { "rpc.system" "aws-api" "rpc.service" "Amazon S3" "aws.bucket.name" bucketName - "http.method" "DELETE" - "http.status_code" 204 - "http.url" String - "net.peer.name" String - "net.protocol.name" "http" - "net.protocol.version" "1.1" - "net.peer.port" { it == null || Number } - "$SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH" { it == null || it instanceof Long } - "$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" { it == null || it instanceof Long } + "$HttpAttributes.HTTP_REQUEST_METHOD" "DELETE" + "$HttpAttributes.HTTP_RESPONSE_STATUS_CODE" 204 + "$UrlAttributes.URL_FULL" { it.startsWith("http://") } + "$ServerAttributes.SERVER_ADDRESS" String + "$NetworkAttributes.NETWORK_PROTOCOL_VERSION" "1.1" + "$ServerAttributes.SERVER_PORT" { it == null || Number } } } } - trace(15, 1) { + trace(13, 1) { span(0) { name "SQS.PurgeQueue" kind CLIENT @@ -769,15 +622,12 @@ class S3TracingTest extends AgentInstrumentationSpecification { "aws.queue.url" queueUrl "rpc.system" "aws-api" "rpc.service" "AmazonSQS" - "http.method" "POST" - "http.status_code" 200 - "http.url" String - "net.peer.name" String - "net.protocol.name" "http" - "net.protocol.version" "1.1" - "net.peer.port" { it == null || Number } - "$SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH" { it == null || it instanceof Long } - "$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" { it == null || it instanceof Long } + "$HttpAttributes.HTTP_REQUEST_METHOD" "POST" + "$HttpAttributes.HTTP_RESPONSE_STATUS_CODE" 200 + "$UrlAttributes.URL_FULL" { it.startsWith("http://") } + "$ServerAttributes.SERVER_ADDRESS" String + "$NetworkAttributes.NETWORK_PROTOCOL_VERSION" "1.1" + "$ServerAttributes.SERVER_PORT" { it == null || Number } } } } diff --git a/instrumentation/aws-sdk/aws-sdk-1.11/javaagent/src/test/groovy/SnsTracingTest.groovy b/instrumentation/aws-sdk/aws-sdk-1.11/javaagent/src/test/groovy/SnsTracingTest.groovy index be0845bed214..ba6b28fafb59 100644 --- a/instrumentation/aws-sdk/aws-sdk-1.11/javaagent/src/test/groovy/SnsTracingTest.groovy +++ b/instrumentation/aws-sdk/aws-sdk-1.11/javaagent/src/test/groovy/SnsTracingTest.groovy @@ -4,7 +4,11 @@ */ import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes +import io.opentelemetry.semconv.ServerAttributes +import io.opentelemetry.semconv.HttpAttributes +import io.opentelemetry.semconv.NetworkAttributes +import io.opentelemetry.semconv.UrlAttributes import spock.lang.Shared import static io.opentelemetry.api.trace.SpanKind.CLIENT @@ -32,10 +36,13 @@ class SnsTracingTest extends AgentInstrumentationSpecification { when: awsConnector.publishSampleNotification(topicArn) - awsConnector.receiveMessage(queueUrl) + def receiveMessageResult = awsConnector.receiveMessage(queueUrl) + receiveMessageResult.messages.each {message -> + runWithSpan("process child") {} + } then: - assertTraces(7) { + assertTraces(6) { trace(0, 1) { span(0) { @@ -49,14 +56,12 @@ class SnsTracingTest extends AgentInstrumentationSpecification { "aws.queue.name" queueName "rpc.system" "aws-api" "rpc.service" "AmazonSQS" - "http.method" "POST" - "http.status_code" 200 - "http.url" String - "net.peer.name" String - "net.protocol.name" "http" - "net.protocol.version" "1.1" - "net.peer.port" { it == null || Number } - "$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" Long + "$HttpAttributes.HTTP_REQUEST_METHOD" "POST" + "$HttpAttributes.HTTP_RESPONSE_STATUS_CODE" 200 + "$UrlAttributes.URL_FULL" String + "$ServerAttributes.SERVER_ADDRESS" String + "$NetworkAttributes.NETWORK_PROTOCOL_VERSION" "1.1" + "$ServerAttributes.SERVER_PORT" { it == null || Number } } } } @@ -73,14 +78,12 @@ class SnsTracingTest extends AgentInstrumentationSpecification { "aws.queue.url" queueUrl "rpc.system" "aws-api" "rpc.service" "AmazonSQS" - "http.method" "POST" - "http.status_code" 200 - "http.url" String - "net.peer.name" String - "net.protocol.name" "http" - "net.protocol.version" "1.1" - "net.peer.port" { it == null || Number } - "$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" Long + "$HttpAttributes.HTTP_REQUEST_METHOD" "POST" + "$HttpAttributes.HTTP_RESPONSE_STATUS_CODE" 200 + "$UrlAttributes.URL_FULL" String + "$ServerAttributes.SERVER_ADDRESS" String + "$NetworkAttributes.NETWORK_PROTOCOL_VERSION" "1.1" + "$ServerAttributes.SERVER_PORT" { it == null || Number } } } } @@ -97,14 +100,12 @@ class SnsTracingTest extends AgentInstrumentationSpecification { "aws.queue.url" queueUrl "rpc.system" "aws-api" "rpc.service" "AmazonSQS" - "http.method" "POST" - "http.status_code" 200 - "http.url" String - "net.peer.name" String - "net.protocol.name" "http" - "net.protocol.version" "1.1" - "net.peer.port" { it == null || Number } - "$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" Long + "$HttpAttributes.HTTP_REQUEST_METHOD" "POST" + "$HttpAttributes.HTTP_RESPONSE_STATUS_CODE" 200 + "$UrlAttributes.URL_FULL" String + "$ServerAttributes.SERVER_ADDRESS" String + "$NetworkAttributes.NETWORK_PROTOCOL_VERSION" "1.1" + "$ServerAttributes.SERVER_PORT" { it == null || Number } } } } @@ -120,14 +121,12 @@ class SnsTracingTest extends AgentInstrumentationSpecification { "rpc.method" "CreateTopic" "rpc.system" "aws-api" "rpc.service" "AmazonSNS" - "http.method" "POST" - "http.status_code" 200 - "http.url" String - "net.peer.name" String - "net.protocol.name" "http" - "net.protocol.version" "1.1" - "net.peer.port" { it == null || Number } - "$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" Long + "$HttpAttributes.HTTP_REQUEST_METHOD" "POST" + "$HttpAttributes.HTTP_RESPONSE_STATUS_CODE" 200 + "$UrlAttributes.URL_FULL" String + "$ServerAttributes.SERVER_ADDRESS" String + "$NetworkAttributes.NETWORK_PROTOCOL_VERSION" "1.1" + "$ServerAttributes.SERVER_PORT" { it == null || Number } } } } @@ -143,18 +142,17 @@ class SnsTracingTest extends AgentInstrumentationSpecification { "rpc.method" "Subscribe" "rpc.system" "aws-api" "rpc.service" "AmazonSNS" - "http.method" "POST" - "http.status_code" 200 - "http.url" String - "net.peer.name" String - "net.protocol.name" "http" - "net.protocol.version" "1.1" - "net.peer.port" { it == null || Number } - "$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" Long + "$HttpAttributes.HTTP_REQUEST_METHOD" "POST" + "$HttpAttributes.HTTP_RESPONSE_STATUS_CODE" 200 + "$UrlAttributes.URL_FULL" String + "$ServerAttributes.SERVER_ADDRESS" String + "$NetworkAttributes.NETWORK_PROTOCOL_VERSION" "1.1" + "$ServerAttributes.SERVER_PORT" { it == null || Number } + "$MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME" topicArn } } } - trace(5, 2) { + trace(5, 3) { span(0) { name "SNS.Publish" kind CLIENT @@ -165,18 +163,17 @@ class SnsTracingTest extends AgentInstrumentationSpecification { "rpc.method" "Publish" "rpc.system" "aws-api" "rpc.service" "AmazonSNS" - "http.method" "POST" - "http.status_code" 200 - "http.url" String - "net.peer.name" String - "net.protocol.name" "http" - "net.protocol.version" "1.1" - "net.peer.port" { it == null || Number } - "$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" Long + "$HttpAttributes.HTTP_REQUEST_METHOD" "POST" + "$HttpAttributes.HTTP_RESPONSE_STATUS_CODE" 200 + "$UrlAttributes.URL_FULL" String + "$ServerAttributes.SERVER_ADDRESS" String + "$NetworkAttributes.NETWORK_PROTOCOL_VERSION" "1.1" + "$ServerAttributes.SERVER_PORT" { it == null || Number } + "$MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME" topicArn } } span(1) { - name "SQS.ReceiveMessage" + name "snsToSqsTestQueue process" kind CONSUMER childOf span(0) attributes { @@ -186,42 +183,22 @@ class SnsTracingTest extends AgentInstrumentationSpecification { "rpc.system" "aws-api" "rpc.service" "AmazonSQS" "rpc.method" "ReceiveMessage" - "http.method" "POST" - "http.status_code" 200 - "http.url" String - "$SemanticAttributes.USER_AGENT_ORIGINAL" String - "net.peer.name" String - "net.protocol.name" "http" - "net.protocol.version" "1.1" - "net.peer.port" { it == null || Number } - "$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" Long + "$HttpAttributes.HTTP_REQUEST_METHOD" "POST" + "$HttpAttributes.HTTP_RESPONSE_STATUS_CODE" 200 + "$UrlAttributes.URL_FULL" String + "$ServerAttributes.SERVER_ADDRESS" String + "$ServerAttributes.SERVER_PORT" { it == null || Number } + "$MessagingIncubatingAttributes.MESSAGING_SYSTEM" MessagingIncubatingAttributes.MessagingSystemValues.AWS_SQS + "$MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME" "snsToSqsTestQueue" + "$MessagingIncubatingAttributes.MESSAGING_OPERATION" "process" + "$MessagingIncubatingAttributes.MESSAGING_MESSAGE_ID" String + "$NetworkAttributes.NETWORK_PROTOCOL_VERSION" "1.1" } } - } - /** - * 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() + span(2) { + name "process child" + childOf span(1) attributes { - "aws.agent" "java-aws-sdk" - "aws.endpoint" String - "aws.queue.url" queueUrl - "rpc.system" "aws-api" - "rpc.service" "AmazonSQS" - "rpc.method" "ReceiveMessage" - "http.method" "POST" - "http.status_code" 200 - "http.url" String - "net.peer.name" String - "net.protocol.name" "http" - "net.protocol.version" "1.1" - "net.peer.port" { it == null || Number } - "$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" Long } } } diff --git a/instrumentation/aws-sdk/aws-sdk-1.11/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/awssdk/v1_11/Aws1ClientTest.groovy b/instrumentation/aws-sdk/aws-sdk-1.11/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/awssdk/v1_11/Aws1ClientTest.groovy index c4a4593ef0c1..8aabe4ffdff3 100644 --- a/instrumentation/aws-sdk/aws-sdk-1.11/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/awssdk/v1_11/Aws1ClientTest.groovy +++ b/instrumentation/aws-sdk/aws-sdk-1.11/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/awssdk/v1_11/Aws1ClientTest.groovy @@ -6,8 +6,11 @@ package io.opentelemetry.javaagent.instrumentation.awssdk.v1_11 import com.amazonaws.AmazonWebServiceClient +import com.amazonaws.ClientConfiguration import com.amazonaws.Request import com.amazonaws.auth.BasicAWSCredentials +import com.amazonaws.auth.NoOpSigner +import com.amazonaws.auth.SignerFactory import com.amazonaws.handlers.RequestHandler2 import com.amazonaws.regions.Regions import com.amazonaws.services.s3.AmazonS3Client @@ -16,7 +19,11 @@ 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 +import io.opentelemetry.semconv.incubating.RpcIncubatingAttributes +import io.opentelemetry.semconv.ServerAttributes +import io.opentelemetry.semconv.ErrorAttributes +import io.opentelemetry.semconv.HttpAttributes +import io.opentelemetry.semconv.UrlAttributes import static io.opentelemetry.api.trace.StatusCode.ERROR @@ -96,18 +103,35 @@ class Aws1ClientTest extends AbstractAws1ClientTest implements AgentTestTrait { errorEvent IllegalStateException, "bad handler" hasNoParent() attributes { - "$SemanticAttributes.HTTP_URL" "https://s3.amazonaws.com" - "$SemanticAttributes.HTTP_METHOD" "HEAD" - "$SemanticAttributes.NET_PEER_NAME" "s3.amazonaws.com" - "$SemanticAttributes.RPC_SYSTEM" "aws-api" - "$SemanticAttributes.RPC_SERVICE" "Amazon S3" - "$SemanticAttributes.RPC_METHOD" "HeadBucket" + "$UrlAttributes.URL_FULL" "https://s3.amazonaws.com" + "$HttpAttributes.HTTP_REQUEST_METHOD" "HEAD" + "$ServerAttributes.SERVER_ADDRESS" "s3.amazonaws.com" + "$RpcIncubatingAttributes.RPC_SYSTEM" "aws-api" + "$RpcIncubatingAttributes.RPC_SERVICE" "Amazon S3" + "$RpcIncubatingAttributes.RPC_METHOD" "HeadBucket" "aws.endpoint" "https://s3.amazonaws.com" "aws.agent" "java-aws-sdk" "aws.bucket.name" "someBucket" + "$ErrorAttributes.ERROR_TYPE" IllegalStateException.name } } } } } + + def "calling generatePresignedUrl does not leak context"() { + setup: + SignerFactory.registerSigner("noop", NoOpSigner) + def client = AmazonS3ClientBuilder.standard() + .withRegion(Regions.US_EAST_1) + .withClientConfiguration(new ClientConfiguration().withSignerOverride("noop")) + .build() + + when: + client.generatePresignedUrl("someBucket", "someKey", new Date()) + + then: + // expecting no active span after call to generatePresignedUrl + !Span.current().getSpanContext().isValid() + } } diff --git a/instrumentation/aws-sdk/aws-sdk-1.11/javaagent/src/testSqs/groovy/io/opentelemetry/javaagent/instrumentation/awssdk/v1_11/SqsTracingTest.groovy b/instrumentation/aws-sdk/aws-sdk-1.11/javaagent/src/testSqs/groovy/io/opentelemetry/javaagent/instrumentation/awssdk/v1_11/SqsTracingTest.groovy deleted file mode 100644 index 80b19b8f0f67..000000000000 --- a/instrumentation/aws-sdk/aws-sdk-1.11/javaagent/src/testSqs/groovy/io/opentelemetry/javaagent/instrumentation/awssdk/v1_11/SqsTracingTest.groovy +++ /dev/null @@ -1,17 +0,0 @@ -/* - * 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/instrumentation/aws-sdk/aws-sdk-1.11/javaagent/src/testSqs/java/io/opentelemetry/javaagent/instrumentation/awssdk/v1_11/SqsTracingTest.java b/instrumentation/aws-sdk/aws-sdk-1.11/javaagent/src/testSqs/java/io/opentelemetry/javaagent/instrumentation/awssdk/v1_11/SqsTracingTest.java new file mode 100644 index 000000000000..7d2175145eb5 --- /dev/null +++ b/instrumentation/aws-sdk/aws-sdk-1.11/javaagent/src/testSqs/java/io/opentelemetry/javaagent/instrumentation/awssdk/v1_11/SqsTracingTest.java @@ -0,0 +1,28 @@ +/* + * 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.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import org.junit.jupiter.api.extension.RegisterExtension; + +class SqsTracingTest extends AbstractSqsTracingTest { + + @RegisterExtension + private static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + @Override + protected InstrumentationExtension testing() { + return testing; + } + + @Override + public AmazonSQSAsyncClientBuilder configureClient(AmazonSQSAsyncClientBuilder client) { + return client; + } +} diff --git a/instrumentation/aws-sdk/aws-sdk-1.11/javaagent/src/testSqsNoReceiveTelemetry/java/io/opentelemetry/javaagent/instrumentation/awssdk/v1_11/SqsSuppressReceiveSpansTest.java b/instrumentation/aws-sdk/aws-sdk-1.11/javaagent/src/testSqsNoReceiveTelemetry/java/io/opentelemetry/javaagent/instrumentation/awssdk/v1_11/SqsSuppressReceiveSpansTest.java new file mode 100644 index 000000000000..16fce32ac1e3 --- /dev/null +++ b/instrumentation/aws-sdk/aws-sdk-1.11/javaagent/src/testSqsNoReceiveTelemetry/java/io/opentelemetry/javaagent/instrumentation/awssdk/v1_11/SqsSuppressReceiveSpansTest.java @@ -0,0 +1,28 @@ +/* + * 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.AbstractSqsSuppressReceiveSpansTest; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import org.junit.jupiter.api.extension.RegisterExtension; + +class SqsSuppressReceiveSpansTest extends AbstractSqsSuppressReceiveSpansTest { + + @RegisterExtension + private static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + @Override + protected InstrumentationExtension testing() { + return testing; + } + + @Override + public AmazonSQSAsyncClientBuilder configureClient(AmazonSQSAsyncClientBuilder client) { + return client; + } +} diff --git a/instrumentation/aws-sdk/aws-sdk-1.11/javaagent/src/test_before_1_11_106/groovy/Aws0ClientTest.groovy b/instrumentation/aws-sdk/aws-sdk-1.11/javaagent/src/test_before_1_11_106/groovy/Aws0ClientTest.groovy index f21ab34c2073..78baace09f5c 100644 --- a/instrumentation/aws-sdk/aws-sdk-1.11/javaagent/src/test_before_1_11_106/groovy/Aws0ClientTest.groovy +++ b/instrumentation/aws-sdk/aws-sdk-1.11/javaagent/src/test_before_1_11_106/groovy/Aws0ClientTest.groovy @@ -11,6 +11,8 @@ import com.amazonaws.auth.AWSCredentialsProviderChain import com.amazonaws.auth.BasicAWSCredentials import com.amazonaws.auth.EnvironmentVariableCredentialsProvider import com.amazonaws.auth.InstanceProfileCredentialsProvider +import com.amazonaws.auth.NoOpSigner +import com.amazonaws.auth.SignerFactory import com.amazonaws.auth.SystemPropertiesCredentialsProvider import com.amazonaws.auth.profile.ProfileCredentialsProvider import com.amazonaws.handlers.RequestHandler2 @@ -22,7 +24,12 @@ 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.semconv.incubating.RpcIncubatingAttributes +import io.opentelemetry.semconv.ServerAttributes +import io.opentelemetry.semconv.ErrorAttributes +import io.opentelemetry.semconv.HttpAttributes +import io.opentelemetry.semconv.NetworkAttributes +import io.opentelemetry.semconv.UrlAttributes import io.opentelemetry.testing.internal.armeria.common.HttpResponse import io.opentelemetry.testing.internal.armeria.common.HttpStatus import io.opentelemetry.testing.internal.armeria.common.MediaType @@ -104,17 +111,15 @@ class Aws0ClientTest extends AgentInstrumentationSpecification { kind CLIENT hasNoParent() attributes { - "$SemanticAttributes.HTTP_URL" "${server.httpUri()}" - "$SemanticAttributes.HTTP_METHOD" "$method" - "$SemanticAttributes.HTTP_STATUS_CODE" 200 - "$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" Long - "net.protocol.name" "http" - "net.protocol.version" "1.1" - "$SemanticAttributes.NET_PEER_PORT" server.httpPort() - "$SemanticAttributes.NET_PEER_NAME" "127.0.0.1" - "$SemanticAttributes.RPC_SYSTEM" "aws-api" - "$SemanticAttributes.RPC_SERVICE" { it.contains(service) } - "$SemanticAttributes.RPC_METHOD" "${operation}" + "$UrlAttributes.URL_FULL" "${server.httpUri()}" + "$HttpAttributes.HTTP_REQUEST_METHOD" "$method" + "$HttpAttributes.HTTP_RESPONSE_STATUS_CODE" 200 + "$NetworkAttributes.NETWORK_PROTOCOL_VERSION" "1.1" + "$ServerAttributes.SERVER_PORT" server.httpPort() + "$ServerAttributes.SERVER_ADDRESS" "127.0.0.1" + "$RpcIncubatingAttributes.RPC_SYSTEM" "aws-api" + "$RpcIncubatingAttributes.RPC_SERVICE" { it.contains(service) } + "$RpcIncubatingAttributes.RPC_METHOD" "${operation}" "aws.endpoint" "${server.httpUri()}" "aws.agent" "java-aws-sdk" for (def addedTag : additionalAttributes) { @@ -168,18 +173,19 @@ class Aws0ClientTest extends AgentInstrumentationSpecification { errorEvent AmazonClientException, ~/Unable to execute HTTP request/ hasNoParent() attributes { - "$SemanticAttributes.HTTP_URL" "http://localhost:${UNUSABLE_PORT}" - "$SemanticAttributes.HTTP_METHOD" "$method" - "$SemanticAttributes.NET_PEER_PORT" 61 - "$SemanticAttributes.NET_PEER_NAME" "localhost" - "$SemanticAttributes.RPC_SYSTEM" "aws-api" - "$SemanticAttributes.RPC_SERVICE" { it.contains(service) } - "$SemanticAttributes.RPC_METHOD" "${operation}" + "$UrlAttributes.URL_FULL" "http://localhost:${UNUSABLE_PORT}" + "$HttpAttributes.HTTP_REQUEST_METHOD" "$method" + "$ServerAttributes.SERVER_PORT" 61 + "$ServerAttributes.SERVER_ADDRESS" "localhost" + "$RpcIncubatingAttributes.RPC_SYSTEM" "aws-api" + "$RpcIncubatingAttributes.RPC_SERVICE" { it.contains(service) } + "$RpcIncubatingAttributes.RPC_METHOD" "${operation}" "aws.endpoint" "http://localhost:${UNUSABLE_PORT}" "aws.agent" "java-aws-sdk" for (def addedTag : additionalAttributes) { "$addedTag.key" "$addedTag.value" } + "$ErrorAttributes.ERROR_TYPE" AmazonClientException.name } } } @@ -215,15 +221,16 @@ class Aws0ClientTest extends AgentInstrumentationSpecification { errorEvent IllegalStateException, "bad handler" hasNoParent() attributes { - "$SemanticAttributes.HTTP_URL" "https://s3.amazonaws.com" - "$SemanticAttributes.HTTP_METHOD" "GET" - "$SemanticAttributes.NET_PEER_NAME" "s3.amazonaws.com" - "$SemanticAttributes.RPC_SYSTEM" "aws-api" - "$SemanticAttributes.RPC_SERVICE" "Amazon S3" - "$SemanticAttributes.RPC_METHOD" "GetObject" + "$UrlAttributes.URL_FULL" "https://s3.amazonaws.com" + "$HttpAttributes.HTTP_REQUEST_METHOD" "GET" + "$ServerAttributes.SERVER_ADDRESS" "s3.amazonaws.com" + "$RpcIncubatingAttributes.RPC_SYSTEM" "aws-api" + "$RpcIncubatingAttributes.RPC_SERVICE" "Amazon S3" + "$RpcIncubatingAttributes.RPC_METHOD" "GetObject" "aws.endpoint" "https://s3.amazonaws.com" "aws.agent" "java-aws-sdk" "aws.bucket.name" "someBucket" + "$ErrorAttributes.ERROR_TYPE" IllegalStateException.name } } } @@ -257,19 +264,34 @@ class Aws0ClientTest extends AgentInstrumentationSpecification { errorEvent AmazonClientException, ~/Unable to execute HTTP request/ hasNoParent() attributes { - "$SemanticAttributes.HTTP_URL" "${server.httpUri()}" - "$SemanticAttributes.HTTP_METHOD" "GET" - "$SemanticAttributes.NET_PEER_PORT" server.httpPort() - "$SemanticAttributes.NET_PEER_NAME" "127.0.0.1" - "$SemanticAttributes.RPC_SYSTEM" "aws-api" - "$SemanticAttributes.RPC_SERVICE" "Amazon S3" - "$SemanticAttributes.RPC_METHOD" "GetObject" + "$UrlAttributes.URL_FULL" "${server.httpUri()}" + "$HttpAttributes.HTTP_REQUEST_METHOD" "GET" + "$ServerAttributes.SERVER_PORT" server.httpPort() + "$ServerAttributes.SERVER_ADDRESS" "127.0.0.1" + "$RpcIncubatingAttributes.RPC_SYSTEM" "aws-api" + "$RpcIncubatingAttributes.RPC_SERVICE" "Amazon S3" + "$RpcIncubatingAttributes.RPC_METHOD" "GetObject" "aws.endpoint" "${server.httpUri()}" "aws.agent" "java-aws-sdk" "aws.bucket.name" "someBucket" + "$ErrorAttributes.ERROR_TYPE" AmazonClientException.name } } } } } + + def "calling generatePresignedUrl does not leak context"() { + setup: + SignerFactory.registerSigner("noop", NoOpSigner) + def client = new AmazonS3Client(new ClientConfiguration().withSignerOverride("noop")) + .withEndpoint("${server.httpUri()}") + + when: + client.generatePresignedUrl("someBucket", "someKey", new Date()) + + then: + // expecting no active span after call to generatePresignedUrl + !Span.current().getSpanContext().isValid() + } } diff --git a/instrumentation/aws-sdk/aws-sdk-1.11/library-autoconfigure/build.gradle.kts b/instrumentation/aws-sdk/aws-sdk-1.11/library-autoconfigure/build.gradle.kts index 8ca4a2fd6226..6cf49a21c49a 100644 --- a/instrumentation/aws-sdk/aws-sdk-1.11/library-autoconfigure/build.gradle.kts +++ b/instrumentation/aws-sdk/aws-sdk-1.11/library-autoconfigure/build.gradle.kts @@ -18,8 +18,45 @@ dependencies { 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") + + // last version that does not use json protocol + latestDepTestLibrary("com.amazonaws:aws-java-sdk-sqs:1.12.583") +} + +tasks { + withType().configureEach { + systemProperty("otel.instrumentation.aws-sdk.experimental-span-attributes", "true") + systemProperty("otel.instrumentation.messaging.experimental.capture-headers", "test-message-header") + } + + val testReceiveSpansDisabled by registering(Test::class) { + filter { + includeTestsMatching("SqsSuppressReceiveSpansTest") + } + include("**/SqsSuppressReceiveSpansTest.*") + } + + test { + filter { + excludeTestsMatching("SqsSuppressReceiveSpansTest") + } + jvmArgs("-Dotel.instrumentation.messaging.experimental.receive-telemetry.enabled=true") + } + + check { + dependsOn(testReceiveSpansDisabled) + } } -tasks.test { - systemProperty("otel.instrumentation.aws-sdk.experimental-span-attributes", "true") +if (!(findProperty("testLatestDeps") as Boolean)) { + configurations.testRuntimeClasspath { + resolutionStrategy { + eachDependency { + // early versions of aws sdk are not compatible with jackson 2.16.0 + if (requested.group.startsWith("com.fasterxml.jackson")) { + useVersion("2.15.3") + } + } + } + } } diff --git a/instrumentation/aws-sdk/aws-sdk-1.11/library-autoconfigure/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/autoconfigure/TracingRequestHandler.java b/instrumentation/aws-sdk/aws-sdk-1.11/library-autoconfigure/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/autoconfigure/TracingRequestHandler.java index fa0fbe8d699d..2d92c041c0be 100644 --- a/instrumentation/aws-sdk/aws-sdk-1.11/library-autoconfigure/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/autoconfigure/TracingRequestHandler.java +++ b/instrumentation/aws-sdk/aws-sdk-1.11/library-autoconfigure/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/autoconfigure/TracingRequestHandler.java @@ -5,6 +5,8 @@ package io.opentelemetry.instrumentation.awssdk.v1_11.autoconfigure; +import static java.util.Collections.emptyList; + import com.amazonaws.AmazonWebServiceRequest; import com.amazonaws.Request; import com.amazonaws.Response; @@ -23,6 +25,12 @@ public class TracingRequestHandler extends RequestHandler2 { .setCaptureExperimentalSpanAttributes( ConfigPropertiesUtil.getBoolean( "otel.instrumentation.aws-sdk.experimental-span-attributes", false)) + .setMessagingReceiveInstrumentationEnabled( + ConfigPropertiesUtil.getBoolean( + "otel.instrumentation.messaging.experimental.receive-telemetry.enabled", false)) + .setCapturedHeaders( + ConfigPropertiesUtil.getList( + "otel.instrumentation.messaging.experimental.capture-headers", emptyList())) .build() .newRequestHandler(); diff --git a/instrumentation/aws-sdk/aws-sdk-1.11/library-autoconfigure/src/test/groovy/io/opentelemetry/instrumentation/awssdk/v1_11/instrumentor/SqsTracingTest.groovy b/instrumentation/aws-sdk/aws-sdk-1.11/library-autoconfigure/src/test/groovy/io/opentelemetry/instrumentation/awssdk/v1_11/instrumentor/SqsTracingTest.groovy deleted file mode 100644 index 61b55395dd22..000000000000 --- a/instrumentation/aws-sdk/aws-sdk-1.11/library-autoconfigure/src/test/groovy/io/opentelemetry/instrumentation/awssdk/v1_11/instrumentor/SqsTracingTest.groovy +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.awssdk.v1_11.instrumentor - -import com.amazonaws.services.sqs.AmazonSQSAsyncClientBuilder -import io.opentelemetry.instrumentation.awssdk.v1_11.AbstractSqsTracingTest -import io.opentelemetry.instrumentation.test.LibraryTestTrait - -class SqsTracingTest extends AbstractSqsTracingTest implements LibraryTestTrait { - @Override - AmazonSQSAsyncClientBuilder configureClient(AmazonSQSAsyncClientBuilder client) { - return client - } -} diff --git a/instrumentation/aws-sdk/aws-sdk-1.11/library-autoconfigure/src/test/java/io/opentelemetry/instrumentation/awssdk/v1_11/instrumentor/SqsSuppressReceiveSpansTest.java b/instrumentation/aws-sdk/aws-sdk-1.11/library-autoconfigure/src/test/java/io/opentelemetry/instrumentation/awssdk/v1_11/instrumentor/SqsSuppressReceiveSpansTest.java new file mode 100644 index 000000000000..f676cf2c2703 --- /dev/null +++ b/instrumentation/aws-sdk/aws-sdk-1.11/library-autoconfigure/src/test/java/io/opentelemetry/instrumentation/awssdk/v1_11/instrumentor/SqsSuppressReceiveSpansTest.java @@ -0,0 +1,28 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.awssdk.v1_11.instrumentor; + +import com.amazonaws.services.sqs.AmazonSQSAsyncClientBuilder; +import io.opentelemetry.instrumentation.awssdk.v1_11.AbstractSqsSuppressReceiveSpansTest; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.LibraryInstrumentationExtension; +import org.junit.jupiter.api.extension.RegisterExtension; + +public class SqsSuppressReceiveSpansTest extends AbstractSqsSuppressReceiveSpansTest { + + @RegisterExtension + private static final InstrumentationExtension testing = LibraryInstrumentationExtension.create(); + + @Override + protected InstrumentationExtension testing() { + return testing; + } + + @Override + public AmazonSQSAsyncClientBuilder configureClient(AmazonSQSAsyncClientBuilder client) { + return client; + } +} diff --git a/instrumentation/aws-sdk/aws-sdk-1.11/library-autoconfigure/src/test/java/io/opentelemetry/javaagent/instrumentation/awssdk/v1_11/SqsTracingTest.java b/instrumentation/aws-sdk/aws-sdk-1.11/library-autoconfigure/src/test/java/io/opentelemetry/javaagent/instrumentation/awssdk/v1_11/SqsTracingTest.java new file mode 100644 index 000000000000..eba6b45fb0d5 --- /dev/null +++ b/instrumentation/aws-sdk/aws-sdk-1.11/library-autoconfigure/src/test/java/io/opentelemetry/javaagent/instrumentation/awssdk/v1_11/SqsTracingTest.java @@ -0,0 +1,28 @@ +/* + * 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.testing.junit.InstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.LibraryInstrumentationExtension; +import org.junit.jupiter.api.extension.RegisterExtension; + +class SqsTracingTest extends AbstractSqsTracingTest { + + @RegisterExtension + private static final InstrumentationExtension testing = LibraryInstrumentationExtension.create(); + + @Override + protected InstrumentationExtension testing() { + return testing; + } + + @Override + public AmazonSQSAsyncClientBuilder configureClient(AmazonSQSAsyncClientBuilder client) { + return client; + } +} diff --git a/instrumentation/aws-sdk/aws-sdk-1.11/library/build.gradle.kts b/instrumentation/aws-sdk/aws-sdk-1.11/library/build.gradle.kts index 99277d1ea885..bfe844e413c4 100644 --- a/instrumentation/aws-sdk/aws-sdk-1.11/library/build.gradle.kts +++ b/instrumentation/aws-sdk/aws-sdk-1.11/library/build.gradle.kts @@ -6,6 +6,8 @@ dependencies { implementation("io.opentelemetry.contrib:opentelemetry-aws-xray-propagator") library("com.amazonaws:aws-java-sdk-core:1.11.0") + library("com.amazonaws:aws-java-sdk-sqs:1.11.106") + compileOnly(project(":muzzle")) testImplementation(project(":instrumentation:aws-sdk:aws-sdk-1.11:testing")) @@ -15,5 +17,20 @@ dependencies { 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") + + // last version that does not use json protocol + latestDepTestLibrary("com.amazonaws:aws-java-sdk-sqs:1.12.583") +} + +if (!(findProperty("testLatestDeps") as Boolean)) { + configurations.testRuntimeClasspath { + resolutionStrategy { + eachDependency { + // early versions of aws sdk are not compatible with jackson 2.16.0 + if (requested.group.startsWith("com.fasterxml.jackson")) { + useVersion("2.15.3") + } + } + } + } } diff --git a/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AbstractSqsRequest.java b/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AbstractSqsRequest.java new file mode 100644 index 000000000000..b96720f66bc4 --- /dev/null +++ b/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AbstractSqsRequest.java @@ -0,0 +1,13 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.awssdk.v1_11; + +import com.amazonaws.Request; + +abstract class AbstractSqsRequest { + + public abstract Request getRequest(); +} diff --git a/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AwsSdkHttpAttributesGetter.java b/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AwsSdkHttpAttributesGetter.java index e945ef58aa36..e0ee24861c91 100644 --- a/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AwsSdkHttpAttributesGetter.java +++ b/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AwsSdkHttpAttributesGetter.java @@ -10,9 +10,12 @@ import com.amazonaws.Request; import com.amazonaws.Response; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpClientAttributesGetter; +import com.amazonaws.http.HttpResponse; +import io.opentelemetry.instrumentation.api.semconv.http.HttpClientAttributesGetter; import java.util.List; import javax.annotation.Nullable; +import org.apache.http.ProtocolVersion; +import org.apache.http.client.methods.HttpRequestBase; class AwsSdkHttpAttributesGetter implements HttpClientAttributesGetter, Response> { @@ -43,4 +46,54 @@ public List getHttpResponseHeader(Request request, Response respon String value = response.getHttpResponse().getHeaders().get(name); return value == null ? emptyList() : singletonList(value); } + + @Nullable + @Override + public String getNetworkProtocolName(Request request, @Nullable Response response) { + ProtocolVersion protocolVersion = getProtocolVersion(response); + if (protocolVersion == null) { + return null; + } + return protocolVersion.getProtocol(); + } + + @Nullable + @Override + public String getNetworkProtocolVersion(Request request, @Nullable Response response) { + ProtocolVersion protocolVersion = getProtocolVersion(response); + if (protocolVersion == null) { + return null; + } + if (protocolVersion.getMinor() == 0) { + return Integer.toString(protocolVersion.getMajor()); + } + return protocolVersion.getMajor() + "." + protocolVersion.getMinor(); + } + + @Nullable + private static ProtocolVersion getProtocolVersion(@Nullable Response response) { + if (response == null) { + return null; + } + HttpResponse httpResponse = response.getHttpResponse(); + if (httpResponse == null) { + return null; + } + HttpRequestBase httpRequest = httpResponse.getHttpRequest(); + if (httpRequest == null) { + return null; + } + return httpRequest.getProtocolVersion(); + } + + @Override + @Nullable + public String getServerAddress(Request request) { + return request.getEndpoint().getHost(); + } + + @Override + public Integer getServerPort(Request request) { + return request.getEndpoint().getPort(); + } } diff --git a/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AwsSdkInstrumenterFactory.java b/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AwsSdkInstrumenterFactory.java index 49628e2ee372..16ef43d40961 100644 --- a/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AwsSdkInstrumenterFactory.java +++ b/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AwsSdkInstrumenterFactory.java @@ -5,65 +5,188 @@ package io.opentelemetry.instrumentation.awssdk.v1_11; +import static java.util.Collections.emptyList; +import static java.util.Collections.singletonList; + import com.amazonaws.Request; import com.amazonaws.Response; import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.incubator.semconv.messaging.MessageOperation; +import io.opentelemetry.instrumentation.api.incubator.semconv.messaging.MessagingAttributesExtractor; +import io.opentelemetry.instrumentation.api.incubator.semconv.messaging.MessagingAttributesGetter; +import io.opentelemetry.instrumentation.api.incubator.semconv.messaging.MessagingSpanNameExtractor; +import io.opentelemetry.instrumentation.api.incubator.semconv.rpc.RpcClientAttributesExtractor; 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 io.opentelemetry.instrumentation.api.instrumenter.http.HttpClientAttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.rpc.RpcClientAttributesExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor; +import io.opentelemetry.instrumentation.api.semconv.http.HttpClientAttributesExtractor; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import javax.annotation.Nullable; final class AwsSdkInstrumenterFactory { private static final String INSTRUMENTATION_NAME = "io.opentelemetry.aws-sdk-1.11"; private static final AttributesExtractor, Response> httpAttributesExtractor = - HttpClientAttributesExtractor.create( - new AwsSdkHttpAttributesGetter(), new AwsSdkNetAttributesGetter()); + HttpClientAttributesExtractor.create(new AwsSdkHttpAttributesGetter()); private static final AttributesExtractor, Response> rpcAttributesExtractor = RpcClientAttributesExtractor.create(AwsSdkRpcAttributesGetter.INSTANCE); private static final AwsSdkExperimentalAttributesExtractor experimentalAttributesExtractor = new AwsSdkExperimentalAttributesExtractor(); - private static final AwsSdkSpanKindExtractor spanKindExtractor = new AwsSdkSpanKindExtractor(); + private static final SnsAttributesExtractor snsAttributesExtractor = new SnsAttributesExtractor(); private static final List, Response>> - defaultAttributesExtractors = Arrays.asList(httpAttributesExtractor, rpcAttributesExtractor); + defaultAttributesExtractors = + Arrays.asList(httpAttributesExtractor, rpcAttributesExtractor, snsAttributesExtractor); private static final List, Response>> extendedAttributesExtractors = Arrays.asList( - httpAttributesExtractor, rpcAttributesExtractor, experimentalAttributesExtractor); + httpAttributesExtractor, + rpcAttributesExtractor, + snsAttributesExtractor, + experimentalAttributesExtractor); private static final AwsSdkSpanNameExtractor spanName = new AwsSdkSpanNameExtractor(); - static Instrumenter, Response> requestInstrumenter( - OpenTelemetry openTelemetry, boolean captureExperimentalSpanAttributes) { + private final OpenTelemetry openTelemetry; + private final List capturedHeaders; + private final boolean captureExperimentalSpanAttributes; + private final boolean messagingReceiveInstrumentationEnabled; + + AwsSdkInstrumenterFactory( + OpenTelemetry openTelemetry, + List capturedHeaders, + boolean captureExperimentalSpanAttributes, + boolean messagingReceiveInstrumentationEnabled) { + this.openTelemetry = openTelemetry; + this.capturedHeaders = capturedHeaders; + this.captureExperimentalSpanAttributes = captureExperimentalSpanAttributes; + this.messagingReceiveInstrumentationEnabled = messagingReceiveInstrumentationEnabled; + } + Instrumenter, Response> requestInstrumenter() { return createInstrumenter( openTelemetry, - captureExperimentalSpanAttributes, - AwsSdkInstrumenterFactory.spanKindExtractor); + spanName, + SpanKindExtractor.alwaysClient(), + attributesExtractors(), + emptyList(), + true); + } + + private List, Response>> attributesExtractors() { + return captureExperimentalSpanAttributes + ? extendedAttributesExtractors + : defaultAttributesExtractors; + } + + private AttributesExtractor messagingAttributesExtractor( + MessagingAttributesGetter getter, MessageOperation operation) { + return MessagingAttributesExtractor.builder(getter, operation) + .setCapturedHeaders(capturedHeaders) + .build(); } - static Instrumenter, Response> consumerInstrumenter( - OpenTelemetry openTelemetry, boolean captureExperimentalSpanAttributes) { + Instrumenter> consumerReceiveInstrumenter() { + MessageOperation operation = MessageOperation.RECEIVE; + SqsReceiveRequestAttributesGetter getter = SqsReceiveRequestAttributesGetter.INSTANCE; + AttributesExtractor> messagingAttributeExtractor = + messagingAttributesExtractor(getter, operation); return createInstrumenter( - openTelemetry, captureExperimentalSpanAttributes, SpanKindExtractor.alwaysConsumer()); + openTelemetry, + MessagingSpanNameExtractor.create(getter, operation), + SpanKindExtractor.alwaysConsumer(), + toSqsRequestExtractors(attributesExtractors()), + singletonList(messagingAttributeExtractor), + messagingReceiveInstrumentationEnabled); } - private static Instrumenter, Response> createInstrumenter( - OpenTelemetry openTelemetry, - boolean captureExperimentalSpanAttributes, - SpanKindExtractor> kindExtractor) { - return Instrumenter., Response>builder( - openTelemetry, INSTRUMENTATION_NAME, spanName) - .addAttributesExtractors( - captureExperimentalSpanAttributes - ? extendedAttributesExtractors - : defaultAttributesExtractors) - .buildInstrumenter(kindExtractor); + Instrumenter> consumerProcessInstrumenter() { + MessageOperation operation = MessageOperation.PROCESS; + SqsProcessRequestAttributesGetter getter = SqsProcessRequestAttributesGetter.INSTANCE; + AttributesExtractor> messagingAttributeExtractor = + messagingAttributesExtractor(getter, operation); + + InstrumenterBuilder> builder = + Instrumenter.>builder( + openTelemetry, + INSTRUMENTATION_NAME, + MessagingSpanNameExtractor.create(getter, operation)) + .addAttributesExtractors(toSqsRequestExtractors(attributesExtractors())) + .addAttributesExtractor(messagingAttributeExtractor); + + if (messagingReceiveInstrumentationEnabled) { + builder.addSpanLinksExtractor( + (spanLinks, parentContext, request) -> { + Context extracted = + SqsParentContext.ofSystemAttributes(request.getMessage().getAttributes()); + spanLinks.addLink(Span.fromContext(extracted).getSpanContext()); + }); + } + return builder.buildInstrumenter(SpanKindExtractor.alwaysConsumer()); + } + + private static List>> toSqsRequestExtractors( + List, Response>> extractors) { + List>> result = new ArrayList<>(); + for (AttributesExtractor, Response> extractor : extractors) { + result.add( + new AttributesExtractor>() { + @Override + public void onStart( + AttributesBuilder attributes, + Context parentContext, + AbstractSqsRequest sqsRequest) { + extractor.onStart(attributes, parentContext, sqsRequest.getRequest()); + } + + @Override + public void onEnd( + AttributesBuilder attributes, + Context context, + AbstractSqsRequest sqsRequest, + @Nullable Response response, + @Nullable Throwable error) { + extractor.onEnd(attributes, context, sqsRequest.getRequest(), response, error); + } + }); + } + return result; + } + + Instrumenter, Response> producerInstrumenter() { + MessageOperation operation = MessageOperation.PUBLISH; + SqsAttributesGetter getter = SqsAttributesGetter.INSTANCE; + AttributesExtractor, Response> messagingAttributeExtractor = + messagingAttributesExtractor(getter, operation); + + return createInstrumenter( + openTelemetry, + MessagingSpanNameExtractor.create(getter, operation), + SpanKindExtractor.alwaysProducer(), + attributesExtractors(), + singletonList(messagingAttributeExtractor), + true); } - private AwsSdkInstrumenterFactory() {} + private static Instrumenter createInstrumenter( + OpenTelemetry openTelemetry, + SpanNameExtractor spanNameExtractor, + SpanKindExtractor spanKindExtractor, + List> attributeExtractors, + List> additionalAttributeExtractors, + boolean enabled) { + return Instrumenter.builder( + openTelemetry, INSTRUMENTATION_NAME, spanNameExtractor) + .addAttributesExtractors(attributeExtractors) + .addAttributesExtractors(additionalAttributeExtractors) + .setEnabled(enabled) + .buildInstrumenter(spanKindExtractor); + } } diff --git a/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AwsSdkNetAttributesGetter.java b/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AwsSdkNetAttributesGetter.java deleted file mode 100644 index 4364c264497c..000000000000 --- a/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AwsSdkNetAttributesGetter.java +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.awssdk.v1_11; - -import com.amazonaws.Request; -import com.amazonaws.Response; -import com.amazonaws.http.HttpResponse; -import io.opentelemetry.instrumentation.api.instrumenter.net.NetClientAttributesGetter; -import javax.annotation.Nullable; -import org.apache.http.ProtocolVersion; -import org.apache.http.client.methods.HttpRequestBase; - -class AwsSdkNetAttributesGetter implements NetClientAttributesGetter, Response> { - - @Nullable - @Override - public String getNetworkProtocolName(Request request, @Nullable Response response) { - ProtocolVersion protocolVersion = getProtocolVersion(response); - if (protocolVersion == null) { - return null; - } - return protocolVersion.getProtocol(); - } - - @Nullable - @Override - public String getNetworkProtocolVersion(Request request, @Nullable Response response) { - ProtocolVersion protocolVersion = getProtocolVersion(response); - if (protocolVersion == null) { - return null; - } - return protocolVersion.getMajor() + "." + protocolVersion.getMinor(); - } - - @Nullable - private static ProtocolVersion getProtocolVersion(@Nullable Response response) { - if (response == null) { - return null; - } - HttpResponse httpResponse = response.getHttpResponse(); - if (httpResponse == null) { - return null; - } - HttpRequestBase httpRequest = httpResponse.getHttpRequest(); - if (httpRequest == null) { - return null; - } - return httpRequest.getProtocolVersion(); - } - - @Override - @Nullable - public String getServerAddress(Request request) { - return request.getEndpoint().getHost(); - } - - @Override - public Integer getServerPort(Request request) { - return request.getEndpoint().getPort(); - } -} diff --git a/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AwsSdkRpcAttributesGetter.java b/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AwsSdkRpcAttributesGetter.java index aa0042725867..a3704e88855b 100644 --- a/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AwsSdkRpcAttributesGetter.java +++ b/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AwsSdkRpcAttributesGetter.java @@ -6,7 +6,7 @@ package io.opentelemetry.instrumentation.awssdk.v1_11; import com.amazonaws.Request; -import io.opentelemetry.instrumentation.api.instrumenter.rpc.RpcAttributesGetter; +import io.opentelemetry.instrumentation.api.incubator.semconv.rpc.RpcAttributesGetter; enum AwsSdkRpcAttributesGetter implements RpcAttributesGetter> { INSTANCE; diff --git a/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AwsSdkSpanKindExtractor.java b/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AwsSdkSpanKindExtractor.java deleted file mode 100644 index ca10913a66dc..000000000000 --- a/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AwsSdkSpanKindExtractor.java +++ /dev/null @@ -1,26 +0,0 @@ -/* - * 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 io.opentelemetry.api.trace.SpanKind; -import io.opentelemetry.instrumentation.api.instrumenter.SpanKindExtractor; - -class AwsSdkSpanKindExtractor implements SpanKindExtractor> { - @Override - public SpanKind extract(Request request) { - AmazonWebServiceRequest originalRequest = request.getOriginalRequest(); - return (isSqsProducer(originalRequest) ? SpanKind.PRODUCER : SpanKind.CLIENT); - } - - private static boolean isSqsProducer(AmazonWebServiceRequest request) { - return request - .getClass() - .getName() - .equals("com.amazonaws.services.sqs.model.SendMessageRequest"); - } -} diff --git a/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AwsSdkTelemetry.java b/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AwsSdkTelemetry.java index 37e5108be85f..a5b7df063be3 100644 --- a/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AwsSdkTelemetry.java +++ b/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AwsSdkTelemetry.java @@ -11,6 +11,7 @@ import io.opentelemetry.api.OpenTelemetry; import io.opentelemetry.context.Context; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import java.util.List; /** * Entrypoint for instrumenting AWS SDK v1 clients. @@ -45,15 +46,25 @@ public static AwsSdkTelemetryBuilder builder(OpenTelemetry openTelemetry) { } private final Instrumenter, Response> requestInstrumenter; - private final Instrumenter, Response> consumerInstrumenter; + private final Instrumenter> consumerReceiveInstrumenter; + private final Instrumenter> consumerProcessInstrumenter; + private final Instrumenter, Response> producerInstrumenter; - AwsSdkTelemetry(OpenTelemetry openTelemetry, boolean captureExperimentalSpanAttributes) { - requestInstrumenter = - AwsSdkInstrumenterFactory.requestInstrumenter( - openTelemetry, captureExperimentalSpanAttributes); - consumerInstrumenter = - AwsSdkInstrumenterFactory.consumerInstrumenter( - openTelemetry, captureExperimentalSpanAttributes); + AwsSdkTelemetry( + OpenTelemetry openTelemetry, + List capturedHeaders, + boolean captureExperimentalSpanAttributes, + boolean messagingReceiveInstrumentationEnabled) { + AwsSdkInstrumenterFactory instrumenterFactory = + new AwsSdkInstrumenterFactory( + openTelemetry, + capturedHeaders, + captureExperimentalSpanAttributes, + messagingReceiveInstrumentationEnabled); + requestInstrumenter = instrumenterFactory.requestInstrumenter(); + consumerReceiveInstrumenter = instrumenterFactory.consumerReceiveInstrumenter(); + consumerProcessInstrumenter = instrumenterFactory.consumerProcessInstrumenter(); + producerInstrumenter = instrumenterFactory.producerInstrumenter(); } /** @@ -61,6 +72,10 @@ public static AwsSdkTelemetryBuilder builder(OpenTelemetry openTelemetry) { * withRequestHandlers}. */ public RequestHandler2 newRequestHandler() { - return new TracingRequestHandler(requestInstrumenter, consumerInstrumenter); + return new TracingRequestHandler( + requestInstrumenter, + consumerReceiveInstrumenter, + consumerProcessInstrumenter, + producerInstrumenter); } } diff --git a/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AwsSdkTelemetryBuilder.java b/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AwsSdkTelemetryBuilder.java index 7ecb5341c8d2..b36104f531de 100644 --- a/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AwsSdkTelemetryBuilder.java +++ b/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AwsSdkTelemetryBuilder.java @@ -5,20 +5,36 @@ package io.opentelemetry.instrumentation.awssdk.v1_11; +import static java.util.Collections.emptyList; + import com.google.errorprone.annotations.CanIgnoreReturnValue; import io.opentelemetry.api.OpenTelemetry; +import java.util.List; /** A builder of {@link AwsSdkTelemetry}. */ public class AwsSdkTelemetryBuilder { private final OpenTelemetry openTelemetry; + private List capturedHeaders = emptyList(); private boolean captureExperimentalSpanAttributes; + private boolean messagingReceiveInstrumentationEnabled; AwsSdkTelemetryBuilder(OpenTelemetry openTelemetry) { this.openTelemetry = openTelemetry; } + /** + * Configures the messaging headers that will be captured as span attributes. + * + * @param capturedHeaders A list of messaging header names. + */ + @CanIgnoreReturnValue + public AwsSdkTelemetryBuilder setCapturedHeaders(List capturedHeaders) { + this.capturedHeaders = capturedHeaders; + 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 @@ -31,10 +47,27 @@ public AwsSdkTelemetryBuilder setCaptureExperimentalSpanAttributes( return this; } + /** + * Set whether to capture the consumer message receive telemetry in messaging instrumentation. + * + *

Note that this will cause the consumer side to start a new trace, with only a span link + * connecting it to the producer trace. + */ + @CanIgnoreReturnValue + public AwsSdkTelemetryBuilder setMessagingReceiveInstrumentationEnabled( + boolean messagingReceiveInstrumentationEnabled) { + this.messagingReceiveInstrumentationEnabled = messagingReceiveInstrumentationEnabled; + return this; + } + /** * Returns a new {@link AwsSdkTelemetry} with the settings of this {@link AwsSdkTelemetryBuilder}. */ public AwsSdkTelemetry build() { - return new AwsSdkTelemetry(openTelemetry, captureExperimentalSpanAttributes); + return new AwsSdkTelemetry( + openTelemetry, + capturedHeaders, + captureExperimentalSpanAttributes, + messagingReceiveInstrumentationEnabled); } } diff --git a/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/PluginImplUtil.java b/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/PluginImplUtil.java new file mode 100644 index 000000000000..2292fbdb3c1e --- /dev/null +++ b/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/PluginImplUtil.java @@ -0,0 +1,58 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.awssdk.v1_11; + +import java.util.logging.Level; +import java.util.logging.Logger; + +final class PluginImplUtil { // TODO: Copy & paste from v2 + private PluginImplUtil() {} + + private static final Logger logger = Logger.getLogger(PluginImplUtil.class.getName()); + + /** + * Check if the given {@code moduleNameImpl} is present. + * + *

For library instrumentations, the Impls will always be available but might fail to + * load/link/initialize if the corresponding SDK classes are not on the class path. For javaagent, + * the Impl is available only when the corresponding InstrumentationModule was successfully + * applied (muzzle passed). + * + *

Note that an present-but-incompatible library can only be reliably detected by Muzzle. In + * library-mode, users need to ensure they are using a compatible SDK (component) versions + * themselves. + * + * @param implSimpleClassName The simple name of the impl class, e.g. {@code "SqsImpl"}. * + */ + static boolean isImplPresent(String implSimpleClassName) { + // Computing the full name dynamically name here because library instrumentation classes are + // relocated when embedded in the agent. + // We use getName().replace() instead of getPackage() because the latter is not guaranteed to + // work in all cases (e.g., we might be loaded into a custom classloader that doesn't handle it) + String implFullClassName = + PluginImplUtil.class.getName().replace(".PluginImplUtil", "." + implSimpleClassName); + try { + Class.forName(implFullClassName); + return true; + } catch (ClassNotFoundException | LinkageError e) { + // ClassNotFoundException will happen when muzzle disabled us in javaagent mode; LinkageError + // (most likely a NoClassDefFoundError, potentially wrapped in an ExceptionInInitializerError) + // should be thrown when the class is loaded in library mode (where the Impl class itself can + // always be found) but a dependency failed to load (most likely because the corresponding SDK + // dependency is not on the class path). + logger.log( + Level.FINE, + () -> + "Failed to load " + + implFullClassName + + " (" + + e.getClass().getName() + + "). " + + "Most likely, corresponding SDK component is either not on classpath or incompatible."); + return false; + } + } +} diff --git a/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/RequestAccess.java b/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/RequestAccess.java index bb2ae9266c5c..c212a696781e 100644 --- a/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/RequestAccess.java +++ b/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/RequestAccess.java @@ -50,6 +50,18 @@ static String getTableName(Object request) { return invokeOrNull(access.getTableName, request); } + @Nullable + static String getTopicArn(Object request) { + RequestAccess access = REQUEST_ACCESSORS.get(request.getClass()); + return invokeOrNull(access.getTopicArn, request); + } + + @Nullable + static String getTargetArn(Object request) { + RequestAccess access = REQUEST_ACCESSORS.get(request.getClass()); + return invokeOrNull(access.getTargetArn, request); + } + @Nullable private static String invokeOrNull(@Nullable MethodHandle method, Object obj) { if (method == null) { @@ -67,6 +79,8 @@ private static String invokeOrNull(@Nullable MethodHandle method, Object obj) { @Nullable private final MethodHandle getQueueName; @Nullable private final MethodHandle getStreamName; @Nullable private final MethodHandle getTableName; + @Nullable private final MethodHandle getTopicArn; + @Nullable private final MethodHandle getTargetArn; private RequestAccess(Class clz) { getBucketName = findAccessorOrNull(clz, "getBucketName"); @@ -74,6 +88,8 @@ private RequestAccess(Class clz) { getQueueName = findAccessorOrNull(clz, "getQueueName"); getStreamName = findAccessorOrNull(clz, "getStreamName"); getTableName = findAccessorOrNull(clz, "getTableName"); + getTopicArn = findAccessorOrNull(clz, "getTopicArn"); + getTargetArn = findAccessorOrNull(clz, "getTargetArn"); } @Nullable diff --git a/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/SnsAttributesExtractor.java b/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/SnsAttributesExtractor.java new file mode 100644 index 000000000000..541db9291363 --- /dev/null +++ b/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/SnsAttributesExtractor.java @@ -0,0 +1,49 @@ +/* + * 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 io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; +import io.opentelemetry.instrumentation.api.internal.AttributesExtractorUtil; +import javax.annotation.Nullable; + +public class SnsAttributesExtractor implements AttributesExtractor, Response> { + + // copied from MessagingIncubatingAttributes + private static final AttributeKey MESSAGING_DESTINATION_NAME = + AttributeKey.stringKey("messaging.destination.name"); + + @Override + public void onStart(AttributesBuilder attributes, Context parentContext, Request request) { + String destination = findMessageDestination(request.getOriginalRequest()); + AttributesExtractorUtil.internalSet(attributes, MESSAGING_DESTINATION_NAME, destination); + } + + /* + * Attempt to discover the destination of the SNS message by first checking for a topic ARN and + * falling back to the target ARN. If neither is found null is returned. + */ + private static String findMessageDestination(AmazonWebServiceRequest request) { + String destination = RequestAccess.getTopicArn(request); + if (destination != null) { + return destination; + } + return RequestAccess.getTargetArn(request); + } + + @Override + public void onEnd( + AttributesBuilder attributes, + Context context, + Request request, + @Nullable Response response, + @Nullable Throwable error) {} +} diff --git a/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/SqsAccess.java b/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/SqsAccess.java new file mode 100644 index 000000000000..1b1104f6697a --- /dev/null +++ b/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/SqsAccess.java @@ -0,0 +1,45 @@ +/* + * 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 io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.internal.Timer; +import io.opentelemetry.javaagent.tooling.muzzle.NoMuzzle; + +final class SqsAccess { + private SqsAccess() {} + + private static final boolean enabled = PluginImplUtil.isImplPresent("SqsImpl"); + + @NoMuzzle + static boolean afterResponse( + Request request, + Response response, + Timer timer, + Context parentContext, + TracingRequestHandler requestHandler) { + return enabled + && SqsImpl.afterResponse(request, response, timer, parentContext, requestHandler); + } + + @NoMuzzle + static boolean beforeMarshalling(AmazonWebServiceRequest request) { + return enabled && SqsImpl.beforeMarshalling(request); + } + + @NoMuzzle + static String getMessageAttribute(Request request, String name) { + return enabled ? SqsImpl.getMessageAttribute(request, name) : null; + } + + @NoMuzzle + static String getMessageId(Response response) { + return enabled ? SqsImpl.getMessageId(response) : null; + } +} diff --git a/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/SqsAttributesGetter.java b/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/SqsAttributesGetter.java new file mode 100644 index 000000000000..cf24390446e1 --- /dev/null +++ b/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/SqsAttributesGetter.java @@ -0,0 +1,91 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.awssdk.v1_11; + +import com.amazonaws.Request; +import com.amazonaws.Response; +import io.opentelemetry.instrumentation.api.incubator.semconv.messaging.MessagingAttributesGetter; +import java.util.Collections; +import java.util.List; +import javax.annotation.Nullable; + +enum SqsAttributesGetter implements MessagingAttributesGetter, Response> { + INSTANCE; + + // copied from MessagingIncubatingAttributes.MessagingSystemValues + private static final String AWS_SQS = "aws_sqs"; + + @Override + public String getSystem(Request request) { + return AWS_SQS; + } + + @Override + public String getDestination(Request request) { + Object originalRequest = request.getOriginalRequest(); + String queueUrl = RequestAccess.getQueueUrl(originalRequest); + int i = queueUrl.lastIndexOf('/'); + return i > 0 ? queueUrl.substring(i + 1) : null; + } + + @Nullable + @Override + public String getDestinationTemplate(Request request) { + return null; + } + + @Override + public boolean isTemporaryDestination(Request request) { + return false; + } + + @Override + public boolean isAnonymousDestination(Request request) { + return false; + } + + @Override + @Nullable + public String getConversationId(Request request) { + return null; + } + + @Nullable + @Override + public Long getMessageBodySize(Request request) { + return null; + } + + @Nullable + @Override + public Long getMessageEnvelopeSize(Request request) { + return null; + } + + @Override + @Nullable + public String getMessageId(Request request, @Nullable Response response) { + return SqsAccess.getMessageId(response); + } + + @Nullable + @Override + public String getClientId(Request request) { + return null; + } + + @Nullable + @Override + public Long getBatchMessageCount(Request request, @Nullable Response response) { + return null; + } + + @Override + public List getMessageHeader(Request request, String name) { + String value = SqsAccess.getMessageAttribute(request, name); + return value != null ? Collections.singletonList(value) : Collections.emptyList(); + } +} diff --git a/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/SqsImpl.java b/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/SqsImpl.java new file mode 100644 index 000000000000..65fabf76b1a8 --- /dev/null +++ b/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/SqsImpl.java @@ -0,0 +1,148 @@ +/* + * 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.services.sqs.AmazonSQS; +import com.amazonaws.services.sqs.model.MessageAttributeValue; +import com.amazonaws.services.sqs.model.ReceiveMessageRequest; +import com.amazonaws.services.sqs.model.ReceiveMessageResult; +import com.amazonaws.services.sqs.model.SendMessageRequest; +import com.amazonaws.services.sqs.model.SendMessageResult; +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import io.opentelemetry.instrumentation.api.internal.InstrumenterUtil; +import io.opentelemetry.instrumentation.api.internal.Timer; +import java.lang.reflect.Field; +import java.util.Map; + +final class SqsImpl { + static { + // Force loading of SQS class; this ensures that an exception is thrown at this point when the + // SQS library is not present, which will cause SqsAccess to have enabled=false in library mode. + @SuppressWarnings("unused") + String ensureLoadedDummy = AmazonSQS.class.getName(); + } + + private SqsImpl() {} + + static boolean afterResponse( + Request request, + Response response, + Timer timer, + Context parentContext, + TracingRequestHandler requestHandler) { + if (response.getAwsResponse() instanceof ReceiveMessageResult) { + afterConsumerResponse(request, response, timer, parentContext, requestHandler); + return true; + } + return false; + } + + private static void afterConsumerResponse( + Request request, + Response response, + Timer timer, + Context parentContext, + TracingRequestHandler requestHandler) { + ReceiveMessageResult receiveMessageResult = (ReceiveMessageResult) response.getAwsResponse(); + if (receiveMessageResult.getMessages().isEmpty()) { + return; + } + + Instrumenter> consumerReceiveInstrumenter = + requestHandler.getConsumerReceiveInstrumenter(); + Instrumenter> consumerProcessInstrumenter = + requestHandler.getConsumerProcessInstrumenter(); + + Context receiveContext = null; + SqsReceiveRequest receiveRequest = + SqsReceiveRequest.create(request, SqsMessageImpl.wrap(receiveMessageResult.getMessages())); + if (timer != null && consumerReceiveInstrumenter.shouldStart(parentContext, receiveRequest)) { + receiveContext = + InstrumenterUtil.startAndEnd( + consumerReceiveInstrumenter, + parentContext, + receiveRequest, + response, + null, + timer.startTime(), + timer.now()); + } + + addTracing( + receiveMessageResult, request, response, consumerProcessInstrumenter, receiveContext); + } + + private static final Field messagesField = getMessagesField(); + + private static Field getMessagesField() { + try { + Field field = ReceiveMessageResult.class.getDeclaredField("messages"); + field.setAccessible(true); + return field; + } catch (Exception e) { + return null; + } + } + + private static void addTracing( + ReceiveMessageResult receiveMessageResult, + Request request, + Response response, + Instrumenter> consumerProcessInstrumenter, + Context receiveContext) { + if (messagesField == null) { + return; + } + // replace Messages list inside ReceiveMessageResult with a tracing list that creates process + // spans as the list is iterated + try { + messagesField.set( + receiveMessageResult, + TracingList.wrap( + receiveMessageResult.getMessages(), + consumerProcessInstrumenter, + request, + response, + receiveContext)); + } catch (IllegalAccessException ignored) { + // should not happen, we call setAccessible on the field + } + } + + static boolean beforeMarshalling(AmazonWebServiceRequest rawRequest) { + if (rawRequest instanceof ReceiveMessageRequest) { + ReceiveMessageRequest request = (ReceiveMessageRequest) rawRequest; + if (!request.getAttributeNames().contains(SqsParentContext.AWS_TRACE_SYSTEM_ATTRIBUTE)) { + request.withAttributeNames(SqsParentContext.AWS_TRACE_SYSTEM_ATTRIBUTE); + } + return true; + } + return false; + } + + static String getMessageAttribute(Request request, String name) { + if (request.getOriginalRequest() instanceof SendMessageRequest) { + Map map = + ((SendMessageRequest) request.getOriginalRequest()).getMessageAttributes(); + MessageAttributeValue value = map.get(name); + if (value != null) { + return value.getStringValue(); + } + } + return null; + } + + static String getMessageId(Response response) { + if (response != null && response.getAwsResponse() instanceof SendMessageResult) { + return ((SendMessageResult) response.getAwsResponse()).getMessageId(); + } + return null; + } +} diff --git a/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/SqsMessage.java b/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/SqsMessage.java new file mode 100644 index 000000000000..637c68eadbf6 --- /dev/null +++ b/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/SqsMessage.java @@ -0,0 +1,21 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.awssdk.v1_11; + +import java.util.Map; + +/** + * A wrapper interface for {@link com.amazonaws.services.sqs.model.Message}. Using this wrapper + * avoids muzzle failure when sqs classes are not present. + */ +interface SqsMessage { + + Map getAttributes(); + + String getMessageAttribute(String name); + + String getMessageId(); +} diff --git a/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/SqsMessageAccess.java b/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/SqsMessageAccess.java deleted file mode 100644 index 84726bab934f..000000000000 --- a/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/SqsMessageAccess.java +++ /dev/null @@ -1,63 +0,0 @@ -/* - * 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 javax.annotation.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/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/SqsMessageImpl.java b/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/SqsMessageImpl.java new file mode 100644 index 000000000000..548e108a7613 --- /dev/null +++ b/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/SqsMessageImpl.java @@ -0,0 +1,49 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.awssdk.v1_11; + +import com.amazonaws.services.sqs.model.Message; +import com.amazonaws.services.sqs.model.MessageAttributeValue; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +final class SqsMessageImpl implements SqsMessage { + + private final Message message; + + private SqsMessageImpl(Message message) { + this.message = message; + } + + static SqsMessage wrap(Message message) { + return new SqsMessageImpl(message); + } + + static List wrap(List messages) { + List result = new ArrayList<>(); + for (Message message : messages) { + result.add(wrap(message)); + } + return result; + } + + @Override + public Map getAttributes() { + return message.getAttributes(); + } + + @Override + public String getMessageAttribute(String name) { + MessageAttributeValue value = message.getMessageAttributes().get(name); + return value != null ? value.getStringValue() : null; + } + + @Override + public String getMessageId() { + return message.getMessageId(); + } +} diff --git a/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/SqsProcessRequest.java b/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/SqsProcessRequest.java new file mode 100644 index 000000000000..9a13347f9870 --- /dev/null +++ b/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/SqsProcessRequest.java @@ -0,0 +1,31 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.awssdk.v1_11; + +import com.amazonaws.Request; + +final class SqsProcessRequest extends AbstractSqsRequest { + private final Request request; + private final SqsMessage message; + + private SqsProcessRequest(Request request, SqsMessage message) { + this.request = request; + this.message = message; + } + + public static SqsProcessRequest create(Request request, SqsMessage message) { + return new SqsProcessRequest(request, message); + } + + @Override + public Request getRequest() { + return request; + } + + public SqsMessage getMessage() { + return message; + } +} diff --git a/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/SqsProcessRequestAttributesGetter.java b/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/SqsProcessRequestAttributesGetter.java new file mode 100644 index 000000000000..c7786c8dab2f --- /dev/null +++ b/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/SqsProcessRequestAttributesGetter.java @@ -0,0 +1,91 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.awssdk.v1_11; + +import com.amazonaws.Response; +import io.opentelemetry.instrumentation.api.incubator.semconv.messaging.MessagingAttributesGetter; +import java.util.Collections; +import java.util.List; +import javax.annotation.Nullable; + +enum SqsProcessRequestAttributesGetter + implements MessagingAttributesGetter> { + INSTANCE; + + // copied from MessagingIncubatingAttributes.MessagingSystemValues + private static final String AWS_SQS = "aws_sqs"; + + @Override + public String getSystem(SqsProcessRequest request) { + return AWS_SQS; + } + + @Override + public String getDestination(SqsProcessRequest request) { + Object originalRequest = request.getRequest().getOriginalRequest(); + String queueUrl = RequestAccess.getQueueUrl(originalRequest); + int i = queueUrl.lastIndexOf('/'); + return i > 0 ? queueUrl.substring(i + 1) : null; + } + + @Nullable + @Override + public String getDestinationTemplate(SqsProcessRequest request) { + return null; + } + + @Override + public boolean isTemporaryDestination(SqsProcessRequest request) { + return false; + } + + @Override + public boolean isAnonymousDestination(SqsProcessRequest request) { + return false; + } + + @Override + @Nullable + public String getConversationId(SqsProcessRequest request) { + return null; + } + + @Nullable + @Override + public Long getMessageBodySize(SqsProcessRequest request) { + return null; + } + + @Nullable + @Override + public Long getMessageEnvelopeSize(SqsProcessRequest request) { + return null; + } + + @Override + @Nullable + public String getMessageId(SqsProcessRequest request, @Nullable Response response) { + return request.getMessage().getMessageId(); + } + + @Nullable + @Override + public String getClientId(SqsProcessRequest request) { + return null; + } + + @Nullable + @Override + public Long getBatchMessageCount(SqsProcessRequest request, @Nullable Response response) { + return null; + } + + @Override + public List getMessageHeader(SqsProcessRequest request, String name) { + String value = request.getMessage().getMessageAttribute(name); + return value != null ? Collections.singletonList(value) : Collections.emptyList(); + } +} diff --git a/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/SqsReceiveMessageRequestAccess.java b/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/SqsReceiveMessageRequestAccess.java deleted file mode 100644 index 122e66f8c0d3..000000000000 --- a/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/SqsReceiveMessageRequestAccess.java +++ /dev/null @@ -1,99 +0,0 @@ -/* - * 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 javax.annotation.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 - } - } - - @SuppressWarnings("unchecked") - 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/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/SqsReceiveMessageResultAccess.java b/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/SqsReceiveMessageResultAccess.java deleted file mode 100644 index 27fb6e255f92..000000000000 --- a/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/SqsReceiveMessageResultAccess.java +++ /dev/null @@ -1,64 +0,0 @@ -/* - * 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 javax.annotation.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/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/SqsReceiveRequest.java b/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/SqsReceiveRequest.java new file mode 100644 index 000000000000..947f7562433d --- /dev/null +++ b/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/SqsReceiveRequest.java @@ -0,0 +1,32 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.awssdk.v1_11; + +import com.amazonaws.Request; +import java.util.List; + +final class SqsReceiveRequest extends AbstractSqsRequest { + private final Request request; + private final List messages; + + private SqsReceiveRequest(Request request, List messages) { + this.request = request; + this.messages = messages; + } + + public static SqsReceiveRequest create(Request request, List messages) { + return new SqsReceiveRequest(request, messages); + } + + @Override + public Request getRequest() { + return request; + } + + public List getMessages() { + return messages; + } +} diff --git a/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/SqsReceiveRequestAttributesGetter.java b/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/SqsReceiveRequestAttributesGetter.java new file mode 100644 index 000000000000..107adb34f648 --- /dev/null +++ b/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/SqsReceiveRequestAttributesGetter.java @@ -0,0 +1,94 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.awssdk.v1_11; + +import com.amazonaws.Response; +import io.opentelemetry.instrumentation.api.incubator.semconv.messaging.MessagingAttributesGetter; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; +import javax.annotation.Nullable; + +enum SqsReceiveRequestAttributesGetter + implements MessagingAttributesGetter> { + INSTANCE; + + // copied from MessagingIncubatingAttributes.MessagingSystemValues + private static final String AWS_SQS = "aws_sqs"; + + @Override + public String getSystem(SqsReceiveRequest request) { + return AWS_SQS; + } + + @Override + public String getDestination(SqsReceiveRequest request) { + Object originalRequest = request.getRequest().getOriginalRequest(); + String queueUrl = RequestAccess.getQueueUrl(originalRequest); + int i = queueUrl.lastIndexOf('/'); + return i > 0 ? queueUrl.substring(i + 1) : null; + } + + @Nullable + @Override + public String getDestinationTemplate(SqsReceiveRequest request) { + return null; + } + + @Override + public boolean isTemporaryDestination(SqsReceiveRequest request) { + return false; + } + + @Override + public boolean isAnonymousDestination(SqsReceiveRequest request) { + return false; + } + + @Override + @Nullable + public String getConversationId(SqsReceiveRequest request) { + return null; + } + + @Nullable + @Override + public Long getMessageBodySize(SqsReceiveRequest request) { + return null; + } + + @Nullable + @Override + public Long getMessageEnvelopeSize(SqsReceiveRequest request) { + return null; + } + + @Override + @Nullable + public String getMessageId(SqsReceiveRequest request, @Nullable Response response) { + return null; + } + + @Nullable + @Override + public String getClientId(SqsReceiveRequest request) { + return null; + } + + @Nullable + @Override + public Long getBatchMessageCount(SqsReceiveRequest request, @Nullable Response response) { + return (long) request.getMessages().size(); + } + + @Override + public List getMessageHeader(SqsReceiveRequest request, String name) { + return StreamSupport.stream(request.getMessages().spliterator(), false) + .map(message -> message.getMessageAttribute(name)) + .filter(value -> value != null) + .collect(Collectors.toList()); + } +} diff --git a/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/TracingIterator.java b/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/TracingIterator.java new file mode 100644 index 000000000000..8984c38202e1 --- /dev/null +++ b/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/TracingIterator.java @@ -0,0 +1,99 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.awssdk.v1_11; + +import com.amazonaws.Request; +import com.amazonaws.Response; +import com.amazonaws.services.sqs.model.Message; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import java.util.Iterator; +import javax.annotation.Nullable; + +class TracingIterator implements Iterator { + + private final Iterator delegateIterator; + private final Instrumenter> instrumenter; + private final Request request; + private final Response response; + private final Context receiveContext; + + /* + * 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 SqsProcessRequest currentRequest; + @Nullable private Context currentContext; + @Nullable private Scope currentScope; + + private TracingIterator( + Iterator delegateIterator, + Instrumenter> instrumenter, + Request request, + Response response, + Context receiveContext) { + this.delegateIterator = delegateIterator; + this.instrumenter = instrumenter; + this.request = request; + this.response = response; + this.receiveContext = receiveContext; + } + + public static Iterator wrap( + Iterator delegateIterator, + Instrumenter> instrumenter, + Request request, + Response response, + Context receiveContext) { + return new TracingIterator(delegateIterator, instrumenter, request, response, receiveContext); + } + + @Override + public boolean hasNext() { + closeScopeAndEndSpan(); + return delegateIterator.hasNext(); + } + + @Override + public Message next() { + // in case they didn't call hasNext()... + closeScopeAndEndSpan(); + + // it's important not to suppress consumer span creation here using Instrumenter.shouldStart() + // because this instrumentation can leak the context and so there may be a leaked consumer span + // in the context, in which case it's important to overwrite the leaked span instead of + // suppressing the correct span + // (https://github.com/open-telemetry/opentelemetry-java-instrumentation/issues/1947) + Message next = delegateIterator.next(); + if (next != null) { + Context parentContext = receiveContext; + if (parentContext == null) { + parentContext = SqsParentContext.ofSystemAttributes(next.getAttributes()); + } + + currentRequest = SqsProcessRequest.create(request, SqsMessageImpl.wrap(next)); + currentContext = instrumenter.start(parentContext, currentRequest); + currentScope = currentContext.makeCurrent(); + } + return next; + } + + private void closeScopeAndEndSpan() { + if (currentScope != null) { + currentScope.close(); + instrumenter.end(currentContext, currentRequest, response, null); + currentScope = null; + currentRequest = null; + currentContext = null; + } + } + + @Override + public void remove() { + delegateIterator.remove(); + } +} diff --git a/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/TracingList.java b/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/TracingList.java new file mode 100644 index 000000000000..f003d23a2929 --- /dev/null +++ b/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/TracingList.java @@ -0,0 +1,95 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.awssdk.v1_11; + +import com.amazonaws.Request; +import com.amazonaws.Response; +import com.amazonaws.internal.SdkInternalList; +import com.amazonaws.services.sqs.AmazonSQSClient; +import com.amazonaws.services.sqs.model.Message; +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import java.util.Iterator; +import java.util.List; +import java.util.function.Consumer; + +class TracingList extends SdkInternalList { + private static final long serialVersionUID = 1L; + + private final transient Instrumenter> instrumenter; + private final transient Request request; + private final transient Response response; + private final transient Context receiveContext; + private boolean firstIterator = true; + + private TracingList( + List list, + Instrumenter> instrumenter, + Request request, + Response response, + Context receiveContext) { + super(list); + this.instrumenter = instrumenter; + this.request = request; + this.response = response; + this.receiveContext = receiveContext; + } + + public static SdkInternalList wrap( + List list, + Instrumenter> instrumenter, + Request request, + Response response, + Context receiveContext) { + return new TracingList(list, instrumenter, request, response, receiveContext); + } + + @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 + // List is performed in the same thread that called receiveMessage() + if (firstIterator && !inAwsClient()) { + it = TracingIterator.wrap(super.iterator(), instrumenter, request, response, receiveContext); + firstIterator = false; + } else { + it = super.iterator(); + } + + return it; + } + + @Override + public void forEach(Consumer action) { + for (Message message : this) { + action.accept(message); + } + } + + private static boolean inAwsClient() { + for (Class caller : CallerClass.INSTANCE.getClassContext()) { + if (AmazonSQSClient.class == caller) { + return true; + } + } + return false; + } + + private Object writeReplace() { + // serialize this object to SdkInternalList + return new SdkInternalList<>(this); + } + + private static class CallerClass extends SecurityManager { + public static final CallerClass INSTANCE = new CallerClass(); + + @Override + public Class[] getClassContext() { + return super.getClassContext(); + } + } +} diff --git a/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/TracingRequestHandler.java b/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/TracingRequestHandler.java index c427c9f729c5..1cf74e6147c5 100644 --- a/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/TracingRequestHandler.java +++ b/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/TracingRequestHandler.java @@ -11,10 +11,13 @@ import com.amazonaws.handlers.HandlerContextKey; import com.amazonaws.handlers.RequestHandler2; import com.google.errorprone.annotations.CanIgnoreReturnValue; +import io.opentelemetry.api.trace.Span; import io.opentelemetry.context.Context; +import io.opentelemetry.context.ContextKey; import io.opentelemetry.contrib.awsxray.propagator.AwsXrayPropagator; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; -import java.util.List; +import io.opentelemetry.instrumentation.api.internal.InstrumenterUtil; +import io.opentelemetry.instrumentation.api.internal.Timer; import javax.annotation.Nullable; /** Tracing Request Handler. */ @@ -22,25 +25,64 @@ final class TracingRequestHandler extends RequestHandler2 { static final HandlerContextKey CONTEXT = new HandlerContextKey<>(Context.class.getName()); + private static final ContextKey PARENT_CONTEXT_KEY = + ContextKey.named(TracingRequestHandler.class.getName() + ".ParentContext"); + private static final ContextKey REQUEST_TIMER_KEY = + ContextKey.named(TracingRequestHandler.class.getName() + ".Timer"); + private static final ContextKey REQUEST_SPAN_SUPPRESSED_KEY = + ContextKey.named(TracingRequestHandler.class.getName() + ".RequestSpanSuppressed"); private final Instrumenter, Response> requestInstrumenter; - private final Instrumenter, Response> consumerInstrumenter; + private final Instrumenter> consumerReceiveInstrumenter; + private final Instrumenter> consumerProcessInstrumenter; + private final Instrumenter, Response> producerInstrumenter; TracingRequestHandler( Instrumenter, Response> requestInstrumenter, - Instrumenter, Response> consumerInstrumenter) { + Instrumenter> consumerReceiveInstrumenter, + Instrumenter> consumerProcessInstrumenter, + Instrumenter, Response> producerInstrumenter) { this.requestInstrumenter = requestInstrumenter; - this.consumerInstrumenter = consumerInstrumenter; + this.consumerReceiveInstrumenter = consumerReceiveInstrumenter; + this.consumerProcessInstrumenter = consumerProcessInstrumenter; + this.producerInstrumenter = producerInstrumenter; } @Override - @SuppressWarnings("deprecation") // deprecated class to be updated once published in new location public void beforeRequest(Request request) { + // GeneratePresignedUrlRequest doesn't result in actual request, beforeRequest is the only + // method called for it. Span created here would never be ended and scope would be leaked when + // running with java agent. + if ("com.amazonaws.services.s3.model.GeneratePresignedUrlRequest" + .equals(request.getOriginalRequest().getClass().getName())) { + return; + } + + Instrumenter, Response> instrumenter = getInstrumenter(request); + Context parentContext = Context.current(); - if (!requestInstrumenter.shouldStart(parentContext, request)) { + if (!instrumenter.shouldStart(parentContext, request)) { return; } - Context context = requestInstrumenter.start(parentContext, request); + + // Skip creating request span for AmazonSQSClient.receiveMessage if there is no parent span and + // also suppress the span from the underlying http client. Request/http client span appears in a + // separate trace from message producer/consumer spans if there is no parent span just having + // a trace with only the request/http client span isn't useful. + if (Span.fromContextOrNull(parentContext) == null + && "com.amazonaws.services.sqs.model.ReceiveMessageRequest" + .equals(request.getOriginalRequest().getClass().getName())) { + Context context = InstrumenterUtil.suppressSpan(instrumenter, parentContext, request); + context = context.with(REQUEST_TIMER_KEY, Timer.start()); + context = context.with(PARENT_CONTEXT_KEY, parentContext); + context = context.with(REQUEST_SPAN_SUPPRESSED_KEY, Boolean.TRUE); + request.addHandlerContext(CONTEXT, context); + return; + } + + Context context = instrumenter.start(parentContext, request); + context = context.with(REQUEST_TIMER_KEY, Timer.start()); + context = context.with(PARENT_CONTEXT_KEY, parentContext); AwsXrayPropagator.getInstance().inject(context, request, HeaderSetter.INSTANCE); @@ -50,38 +92,33 @@ public void beforeRequest(Request request) { @Override @CanIgnoreReturnValue 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); - } - } + // TODO: We are modifying the request in-place instead of using clone() as recommended + // by the Javadoc in the interface. + SqsAccess.beforeMarshalling(request); return request; } - @Override - public void afterResponse(Request request, Response response) { - if (SqsReceiveMessageRequestAccess.isInstance(request.getOriginalRequest())) { - afterConsumerResponse(request, response); - } - finish(request, response, null); + Instrumenter> getConsumerReceiveInstrumenter() { + return consumerReceiveInstrumenter; } - /** 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); - } + Instrumenter> getConsumerProcessInstrumenter() { + return consumerProcessInstrumenter; } - private void createConsumerSpan(Object message, Request request, Response response) { - Context parentContext = - SqsParentContext.ofSystemAttributes(SqsMessageAccess.getAttributes(message)); - Context context = consumerInstrumenter.start(parentContext, request); - consumerInstrumenter.end(context, request, response, null); + @Override + public void afterResponse(Request request, Response response) { + Context context = request.getHandlerContext(CONTEXT); + if (context == null) { + return; + } + Timer timer = context.get(REQUEST_TIMER_KEY); + // javaagent instrumentation activates scope for the request span, we need to use the context + // we stored before creating the request span to avoid making request span the parent of the + // sqs receive span + Context parentContext = context.get(PARENT_CONTEXT_KEY); + SqsAccess.afterResponse(request, response, timer, parentContext, this); + finish(request, response, null); } @Override @@ -96,6 +133,32 @@ private void finish(Request request, Response response, @Nullable Throwabl return; } request.addHandlerContext(CONTEXT, null); - requestInstrumenter.end(context, request, response, error); + + Instrumenter, Response> instrumenter = getInstrumenter(request); + + // see beforeRequest, request suppressed is only set when we skip creating request span for sqs + // AmazonSQSClient.receiveMessage calls + if (Boolean.TRUE.equals(context.get(REQUEST_SPAN_SUPPRESSED_KEY))) { + Context parentContext = context.get(PARENT_CONTEXT_KEY); + Timer timer = context.get(REQUEST_TIMER_KEY); + // create request span if there was an error + if (error != null + && parentContext != null + && timer != null + && requestInstrumenter.shouldStart(parentContext, request)) { + InstrumenterUtil.startAndEnd( + instrumenter, parentContext, request, response, error, timer.startTime(), timer.now()); + } + return; + } + + instrumenter.end(context, request, response, error); + } + + private Instrumenter, Response> getInstrumenter(Request request) { + boolean isSqsProducer = + "com.amazonaws.services.sqs.model.SendMessageRequest" + .equals(request.getOriginalRequest().getClass().getName()); + return isSqsProducer ? producerInstrumenter : requestInstrumenter; } } diff --git a/instrumentation/aws-sdk/aws-sdk-1.11/library/src/test/groovy/io/opentelemetry/instrumentation/awssdk/v1_11/SqsTracingTest.groovy b/instrumentation/aws-sdk/aws-sdk-1.11/library/src/test/groovy/io/opentelemetry/instrumentation/awssdk/v1_11/SqsTracingTest.groovy deleted file mode 100644 index 8f1083b796e7..000000000000 --- a/instrumentation/aws-sdk/aws-sdk-1.11/library/src/test/groovy/io/opentelemetry/instrumentation/awssdk/v1_11/SqsTracingTest.groovy +++ /dev/null @@ -1,20 +0,0 @@ -/* - * 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( - AwsSdkTelemetry.builder(getOpenTelemetry()) - .setCaptureExperimentalSpanAttributes(true) - .build() - .newRequestHandler()) - } -} diff --git a/instrumentation/aws-sdk/aws-sdk-1.11/library/src/test/java/io/opentelemetry/instrumentation/awssdk/v1_11/SqsSuppressReceiveSpansTest.java b/instrumentation/aws-sdk/aws-sdk-1.11/library/src/test/java/io/opentelemetry/instrumentation/awssdk/v1_11/SqsSuppressReceiveSpansTest.java new file mode 100644 index 000000000000..d87d8f0af1a1 --- /dev/null +++ b/instrumentation/aws-sdk/aws-sdk-1.11/library/src/test/java/io/opentelemetry/instrumentation/awssdk/v1_11/SqsSuppressReceiveSpansTest.java @@ -0,0 +1,31 @@ +/* + * 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.testing.junit.InstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.LibraryInstrumentationExtension; +import org.junit.jupiter.api.extension.RegisterExtension; + +public class SqsSuppressReceiveSpansTest extends AbstractSqsSuppressReceiveSpansTest { + + @RegisterExtension + private static final InstrumentationExtension testing = LibraryInstrumentationExtension.create(); + + @Override + protected InstrumentationExtension testing() { + return testing; + } + + @Override + public AmazonSQSAsyncClientBuilder configureClient(AmazonSQSAsyncClientBuilder client) { + return client.withRequestHandlers( + AwsSdkTelemetry.builder(testing().getOpenTelemetry()) + .setCaptureExperimentalSpanAttributes(true) + .build() + .newRequestHandler()); + } +} diff --git a/instrumentation/aws-sdk/aws-sdk-1.11/library/src/test/java/io/opentelemetry/instrumentation/awssdk/v1_11/SqsTracingTest.java b/instrumentation/aws-sdk/aws-sdk-1.11/library/src/test/java/io/opentelemetry/instrumentation/awssdk/v1_11/SqsTracingTest.java new file mode 100644 index 000000000000..6384fa1aaa72 --- /dev/null +++ b/instrumentation/aws-sdk/aws-sdk-1.11/library/src/test/java/io/opentelemetry/instrumentation/awssdk/v1_11/SqsTracingTest.java @@ -0,0 +1,35 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.awssdk.v1_11; + +import static java.util.Collections.singletonList; + +import com.amazonaws.services.sqs.AmazonSQSAsyncClientBuilder; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.LibraryInstrumentationExtension; +import org.junit.jupiter.api.extension.RegisterExtension; + +class SqsTracingTest extends AbstractSqsTracingTest { + + @RegisterExtension + private static final InstrumentationExtension testing = LibraryInstrumentationExtension.create(); + + @Override + protected InstrumentationExtension testing() { + return testing; + } + + @Override + public AmazonSQSAsyncClientBuilder configureClient(AmazonSQSAsyncClientBuilder client) { + return client.withRequestHandlers( + AwsSdkTelemetry.builder(testing().getOpenTelemetry()) + .setCaptureExperimentalSpanAttributes(true) + .setMessagingReceiveInstrumentationEnabled(true) + .setCapturedHeaders(singletonList("test-message-header")) + .build() + .newRequestHandler()); + } +} diff --git a/instrumentation/aws-sdk/aws-sdk-1.11/testing/src/main/groovy/io/opentelemetry/instrumentation/awssdk/v1_11/AbstractAws1ClientTest.groovy b/instrumentation/aws-sdk/aws-sdk-1.11/testing/src/main/groovy/io/opentelemetry/instrumentation/awssdk/v1_11/AbstractAws1ClientTest.groovy index fe27ec4d6ece..81a3c4243b2f 100644 --- a/instrumentation/aws-sdk/aws-sdk-1.11/testing/src/main/groovy/io/opentelemetry/instrumentation/awssdk/v1_11/AbstractAws1ClientTest.groovy +++ b/instrumentation/aws-sdk/aws-sdk-1.11/testing/src/main/groovy/io/opentelemetry/instrumentation/awssdk/v1_11/AbstractAws1ClientTest.groovy @@ -27,9 +27,17 @@ 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 com.amazonaws.services.sns.AmazonSNSClientBuilder +import com.amazonaws.services.sns.model.PublishRequest import io.opentelemetry.api.trace.Span import io.opentelemetry.instrumentation.test.InstrumentationSpecification -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes +import io.opentelemetry.semconv.incubating.RpcIncubatingAttributes +import io.opentelemetry.semconv.ServerAttributes +import io.opentelemetry.semconv.ErrorAttributes +import io.opentelemetry.semconv.HttpAttributes +import io.opentelemetry.semconv.NetworkAttributes +import io.opentelemetry.semconv.UrlAttributes import io.opentelemetry.testing.internal.armeria.common.HttpResponse import io.opentelemetry.testing.internal.armeria.common.HttpStatus import io.opentelemetry.testing.internal.armeria.common.MediaType @@ -102,18 +110,15 @@ abstract class AbstractAws1ClientTest extends InstrumentationSpecification { kind operation == "SendMessage" ? PRODUCER : CLIENT hasNoParent() attributes { - "$SemanticAttributes.HTTP_URL" "${server.httpUri()}" - "$SemanticAttributes.HTTP_METHOD" "$method" - "$SemanticAttributes.HTTP_STATUS_CODE" 200 - "$SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH" { it == null || it instanceof Long } - "$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" Long - "net.protocol.name" "http" - "net.protocol.version" "1.1" - "$SemanticAttributes.NET_PEER_PORT" server.httpPort() - "$SemanticAttributes.NET_PEER_NAME" "127.0.0.1" - "$SemanticAttributes.RPC_SYSTEM" "aws-api" - "$SemanticAttributes.RPC_SERVICE" { it.contains(service) } - "$SemanticAttributes.RPC_METHOD" "${operation}" + "$UrlAttributes.URL_FULL" "${server.httpUri()}" + "$HttpAttributes.HTTP_REQUEST_METHOD" "$method" + "$HttpAttributes.HTTP_RESPONSE_STATUS_CODE" 200 + "$NetworkAttributes.NETWORK_PROTOCOL_VERSION" "1.1" + "$ServerAttributes.SERVER_PORT" server.httpPort() + "$ServerAttributes.SERVER_ADDRESS" "127.0.0.1" + "$RpcIncubatingAttributes.RPC_SYSTEM" "aws-api" + "$RpcIncubatingAttributes.RPC_SERVICE" { it.contains(service) } + "$RpcIncubatingAttributes.RPC_METHOD" "${operation}" "aws.endpoint" "${server.httpUri()}" "aws.agent" "java-aws-sdk" for (def addedTag : additionalAttributes) { @@ -156,6 +161,26 @@ abstract class AbstractAws1ClientTest extends InstrumentationSpecification { """ + "SNS" | "Publish" | "POST" | "d74b8436-ae13-5ab4-a9ff-ce54dfea72a0" | AmazonSNSClientBuilder.standard() | { c -> c.publish(new PublishRequest().withMessage("somemessage").withTopicArn("somearn")) } | ["$MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME": "somearn"] | """ + + + 567910cd-659e-55d4-8ccb-5aaf14679dc0 + + + d74b8436-ae13-5ab4-a9ff-ce54dfea72a0 + + + """ + "SNS" | "Publish" | "POST" | "d74b8436-ae13-5ab4-a9ff-ce54dfea72a0" | AmazonSNSClientBuilder.standard() | { c -> c.publish(new PublishRequest().withMessage("somemessage").withTargetArn("somearn")) } | ["$MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME": "somearn"] | """ + + + 567910cd-659e-55d4-8ccb-5aaf14679dc0 + + + d74b8436-ae13-5ab4-a9ff-ce54dfea72a0 + + + """ } def "send #operation request to closed port"() { @@ -182,18 +207,19 @@ abstract class AbstractAws1ClientTest extends InstrumentationSpecification { errorEvent SdkClientException, ~/Unable to execute HTTP request/ hasNoParent() attributes { - "$SemanticAttributes.HTTP_URL" "http://127.0.0.1:${UNUSABLE_PORT}" - "$SemanticAttributes.HTTP_METHOD" "$method" - "$SemanticAttributes.NET_PEER_NAME" "127.0.0.1" - "$SemanticAttributes.NET_PEER_PORT" 61 - "$SemanticAttributes.RPC_SYSTEM" "aws-api" - "$SemanticAttributes.RPC_SERVICE" { it.contains(service) } - "$SemanticAttributes.RPC_METHOD" "${operation}" + "$UrlAttributes.URL_FULL" "http://127.0.0.1:${UNUSABLE_PORT}" + "$HttpAttributes.HTTP_REQUEST_METHOD" "$method" + "$ServerAttributes.SERVER_ADDRESS" "127.0.0.1" + "$ServerAttributes.SERVER_PORT" 61 + "$RpcIncubatingAttributes.RPC_SYSTEM" "aws-api" + "$RpcIncubatingAttributes.RPC_SERVICE" { it.contains(service) } + "$RpcIncubatingAttributes.RPC_METHOD" "${operation}" "aws.endpoint" "http://127.0.0.1:${UNUSABLE_PORT}" "aws.agent" "java-aws-sdk" for (def addedTag : additionalAttributes) { "$addedTag.key" "$addedTag.value" } + "$ErrorAttributes.ERROR_TYPE" SdkClientException.name } } } @@ -237,16 +263,17 @@ abstract class AbstractAws1ClientTest extends InstrumentationSpecification { } hasNoParent() attributes { - "$SemanticAttributes.HTTP_URL" "${server.httpUri()}" - "$SemanticAttributes.HTTP_METHOD" "GET" - "$SemanticAttributes.NET_PEER_PORT" server.httpPort() - "$SemanticAttributes.NET_PEER_NAME" "127.0.0.1" - "$SemanticAttributes.RPC_SYSTEM" "aws-api" - "$SemanticAttributes.RPC_SERVICE" "Amazon S3" - "$SemanticAttributes.RPC_METHOD" "GetObject" + "$UrlAttributes.URL_FULL" "${server.httpUri()}" + "$HttpAttributes.HTTP_REQUEST_METHOD" "GET" + "$ServerAttributes.SERVER_PORT" server.httpPort() + "$ServerAttributes.SERVER_ADDRESS" "127.0.0.1" + "$RpcIncubatingAttributes.RPC_SYSTEM" "aws-api" + "$RpcIncubatingAttributes.RPC_SERVICE" "Amazon S3" + "$RpcIncubatingAttributes.RPC_METHOD" "GetObject" "aws.endpoint" "${server.httpUri()}" "aws.agent" "java-aws-sdk" "aws.bucket.name" "someBucket" + "$ErrorAttributes.ERROR_TYPE" {it == SdkClientException.name || it == AmazonClientException.name } } } } diff --git a/instrumentation/aws-sdk/aws-sdk-1.11/testing/src/main/groovy/io/opentelemetry/instrumentation/awssdk/v1_11/AbstractSqsTracingTest.groovy b/instrumentation/aws-sdk/aws-sdk-1.11/testing/src/main/groovy/io/opentelemetry/instrumentation/awssdk/v1_11/AbstractSqsTracingTest.groovy deleted file mode 100644 index 9d1b03492464..000000000000 --- a/instrumentation/aws-sdk/aws-sdk-1.11/testing/src/main/groovy/io/opentelemetry/instrumentation/awssdk/v1_11/AbstractSqsTracingTest.groovy +++ /dev/null @@ -1,178 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.awssdk.v1_11 - -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 io.opentelemetry.semconv.trace.attributes.SemanticAttributes -import org.elasticmq.rest.sqs.SQSRestServerBuilder -import spock.lang.Shared - -import static io.opentelemetry.api.trace.SpanKind.CLIENT -import static io.opentelemetry.api.trace.SpanKind.CONSUMER -import static io.opentelemetry.api.trace.SpanKind.PRODUCER - -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.queue.name" "testSdkSqs" - "rpc.system" "aws-api" - "rpc.service" "AmazonSQS" - "rpc.method" "CreateQueue" - "http.method" "POST" - "http.status_code" 200 - "http.url" "http://localhost:$sqsPort" - "net.peer.name" "localhost" - "net.peer.port" sqsPort - "net.protocol.name" "http" - "net.protocol.version" "1.1" - "$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" Long - } - } - } - trace(1, 2) { - span(0) { - name "SQS.SendMessage" - kind PRODUCER - hasNoParent() - attributes { - "aws.agent" "java-aws-sdk" - "aws.endpoint" "http://localhost:$sqsPort" - "aws.queue.url" "http://localhost:$sqsPort/000000000000/testSdkSqs" - "rpc.system" "aws-api" - "rpc.method" "SendMessage" - "rpc.service" "AmazonSQS" - "http.method" "POST" - "http.status_code" 200 - "http.url" "http://localhost:$sqsPort" - "net.peer.name" "localhost" - "net.peer.port" sqsPort - "net.protocol.name" "http" - "net.protocol.version" "1.1" - "$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" Long - } - } - span(1) { - name "SQS.ReceiveMessage" - kind CONSUMER - childOf span(0) - attributes { - "aws.agent" "java-aws-sdk" - "aws.endpoint" "http://localhost:$sqsPort" - "rpc.method" "ReceiveMessage" - "aws.queue.url" "http://localhost:$sqsPort/000000000000/testSdkSqs" - "rpc.system" "aws-api" - "rpc.service" "AmazonSQS" - "http.method" "POST" - "http.status_code" 200 - "http.url" "http://localhost:$sqsPort" - "$SemanticAttributes.USER_AGENT_ORIGINAL" String - "net.peer.name" "localhost" - "net.peer.port" sqsPort - "net.protocol.name" "http" - "net.protocol.version" "1.1" - "$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" Long - } - } - } - /** - * 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" - "rpc.method" "ReceiveMessage" - "aws.queue.url" "http://localhost:$sqsPort/000000000000/testSdkSqs" - "rpc.system" "aws-api" - "rpc.service" "AmazonSQS" - "http.method" "POST" - "http.status_code" 200 - "http.url" "http://localhost:$sqsPort" - "net.peer.name" "localhost" - "net.peer.port" sqsPort - "net.protocol.name" "http" - "net.protocol.version" "1.1" - "$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" Long - } - } - } - } - } - - 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"] - } -} diff --git a/instrumentation/aws-sdk/aws-sdk-1.11/testing/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AbstractSqsSuppressReceiveSpansTest.java b/instrumentation/aws-sdk/aws-sdk-1.11/testing/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AbstractSqsSuppressReceiveSpansTest.java new file mode 100644 index 000000000000..51dc8b4d37e9 --- /dev/null +++ b/instrumentation/aws-sdk/aws-sdk-1.11/testing/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AbstractSqsSuppressReceiveSpansTest.java @@ -0,0 +1,326 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.awssdk.v1_11; + +import static io.opentelemetry.api.common.AttributeKey.stringKey; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.satisfies; +import static org.assertj.core.api.Assertions.assertThat; + +import com.amazonaws.auth.AWSStaticCredentialsProvider; +import com.amazonaws.auth.BasicAWSCredentials; +import com.amazonaws.client.builder.AwsClientBuilder; +import com.amazonaws.services.sqs.AmazonSQSAsync; +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.ReceiveMessageResult; +import com.amazonaws.services.sqs.model.SendMessageRequest; +import com.google.common.collect.ImmutableList; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.test.utils.PortUtils; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.semconv.HttpAttributes; +import io.opentelemetry.semconv.NetworkAttributes; +import io.opentelemetry.semconv.ServerAttributes; +import io.opentelemetry.semconv.UrlAttributes; +import io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes; +import io.opentelemetry.semconv.incubating.RpcIncubatingAttributes; +import org.elasticmq.rest.sqs.SQSRestServer; +import org.elasticmq.rest.sqs.SQSRestServerBuilder; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +public abstract class AbstractSqsSuppressReceiveSpansTest { + + protected abstract InstrumentationExtension testing(); + + protected abstract AmazonSQSAsyncClientBuilder configureClient( + AmazonSQSAsyncClientBuilder client); + + private static int sqsPort; + private static SQSRestServer sqsRestServer; + private static AmazonSQSAsync sqsClient; + + @BeforeEach + void setUp() { + sqsPort = PortUtils.findOpenPort(); + sqsRestServer = SQSRestServerBuilder.withPort(sqsPort).withInterface("localhost").start(); + + AWSStaticCredentialsProvider credentials = + new AWSStaticCredentialsProvider(new BasicAWSCredentials("x", "x")); + AwsClientBuilder.EndpointConfiguration endpointConfiguration = + new AwsClientBuilder.EndpointConfiguration("http://localhost:" + sqsPort, "elasticmq"); + + sqsClient = + configureClient(AmazonSQSAsyncClient.asyncBuilder()) + .withCredentials(credentials) + .withEndpointConfiguration(endpointConfiguration) + .build(); + } + + @AfterEach + void cleanUp() { + if (sqsRestServer != null) { + sqsRestServer.stopAndWait(); + } + } + + @Test + void testSimpleSqsProducerConsumerServices() { + sqsClient.createQueue("testSdkSqs"); + + SendMessageRequest send = + new SendMessageRequest( + "http://localhost:" + sqsPort + "/000000000000/testSdkSqs", "{\"type\": \"hello\"}"); + sqsClient.sendMessage(send); + ReceiveMessageResult receiveMessageResult = + sqsClient.receiveMessage("http://localhost:" + sqsPort + "/000000000000/testSdkSqs"); + receiveMessageResult + .getMessages() + .forEach(message -> testing().runWithSpan("process child", () -> {})); + + testing() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("SQS.CreateQueue") + .hasKind(SpanKind.CLIENT) + .hasNoParent() + .hasAttributesSatisfyingExactly( + equalTo(stringKey("aws.agent"), "java-aws-sdk"), + equalTo(stringKey("aws.endpoint"), "http://localhost:" + sqsPort), + equalTo(stringKey("aws.queue.name"), "testSdkSqs"), + equalTo(RpcIncubatingAttributes.RPC_SYSTEM, "aws-api"), + equalTo(RpcIncubatingAttributes.RPC_SERVICE, "AmazonSQS"), + equalTo(RpcIncubatingAttributes.RPC_METHOD, "CreateQueue"), + equalTo(HttpAttributes.HTTP_REQUEST_METHOD, "POST"), + equalTo(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, 200), + equalTo(UrlAttributes.URL_FULL, "http://localhost:" + sqsPort), + equalTo(ServerAttributes.SERVER_ADDRESS, "localhost"), + equalTo(ServerAttributes.SERVER_PORT, sqsPort), + equalTo(NetworkAttributes.NETWORK_PROTOCOL_VERSION, "1.1"))), + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("testSdkSqs publish") + .hasKind(SpanKind.PRODUCER) + .hasNoParent() + .hasAttributesSatisfyingExactly( + equalTo(stringKey("aws.agent"), "java-aws-sdk"), + equalTo(stringKey("aws.endpoint"), "http://localhost:" + sqsPort), + equalTo( + stringKey("aws.queue.url"), + "http://localhost:" + sqsPort + "/000000000000/testSdkSqs"), + equalTo(RpcIncubatingAttributes.RPC_SYSTEM, "aws-api"), + equalTo(RpcIncubatingAttributes.RPC_SERVICE, "AmazonSQS"), + equalTo(RpcIncubatingAttributes.RPC_METHOD, "SendMessage"), + equalTo(HttpAttributes.HTTP_REQUEST_METHOD, "POST"), + equalTo(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, 200), + equalTo(UrlAttributes.URL_FULL, "http://localhost:" + sqsPort), + equalTo(ServerAttributes.SERVER_ADDRESS, "localhost"), + equalTo(ServerAttributes.SERVER_PORT, sqsPort), + equalTo( + MessagingIncubatingAttributes.MESSAGING_SYSTEM, + MessagingIncubatingAttributes.MessagingSystemValues.AWS_SQS), + equalTo( + MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME, + "testSdkSqs"), + equalTo( + MessagingIncubatingAttributes.MESSAGING_OPERATION, "publish"), + satisfies( + MessagingIncubatingAttributes.MESSAGING_MESSAGE_ID, + val -> val.isInstanceOf(String.class)), + equalTo(NetworkAttributes.NETWORK_PROTOCOL_VERSION, "1.1")), + span -> + span.hasName("testSdkSqs process") + .hasKind(SpanKind.CONSUMER) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo(stringKey("aws.agent"), "java-aws-sdk"), + equalTo(stringKey("aws.endpoint"), "http://localhost:" + sqsPort), + equalTo( + stringKey("aws.queue.url"), + "http://localhost:" + sqsPort + "/000000000000/testSdkSqs"), + equalTo(RpcIncubatingAttributes.RPC_SYSTEM, "aws-api"), + equalTo(RpcIncubatingAttributes.RPC_SERVICE, "AmazonSQS"), + equalTo(RpcIncubatingAttributes.RPC_METHOD, "ReceiveMessage"), + equalTo(HttpAttributes.HTTP_REQUEST_METHOD, "POST"), + equalTo(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, 200), + equalTo(UrlAttributes.URL_FULL, "http://localhost:" + sqsPort), + equalTo(ServerAttributes.SERVER_ADDRESS, "localhost"), + equalTo(ServerAttributes.SERVER_PORT, sqsPort), + equalTo( + MessagingIncubatingAttributes.MESSAGING_SYSTEM, + MessagingIncubatingAttributes.MessagingSystemValues.AWS_SQS), + equalTo( + MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME, + "testSdkSqs"), + equalTo( + MessagingIncubatingAttributes.MESSAGING_OPERATION, "process"), + satisfies( + MessagingIncubatingAttributes.MESSAGING_MESSAGE_ID, + val -> val.isInstanceOf(String.class)), + equalTo(NetworkAttributes.NETWORK_PROTOCOL_VERSION, "1.1")), + span -> + span.hasName("process child") + .hasParent(trace.getSpan(1)) + .hasAttributes(Attributes.empty()))); + } + + @Test + void testSimpleSqsProducerConsumerServicesWithParentSpan() { + sqsClient.createQueue("testSdkSqs"); + SendMessageRequest sendMessageRequest = + new SendMessageRequest( + "http://localhost:" + sqsPort + "/000000000000/testSdkSqs", "{\"type\": \"hello\"}"); + sqsClient.sendMessage(sendMessageRequest); + + testing() + .runWithSpan( + "parent", + () -> { + ReceiveMessageResult receiveMessageResult = + sqsClient.receiveMessage( + "http://localhost:" + sqsPort + "/000000000000/testSdkSqs"); + receiveMessageResult + .getMessages() + .forEach(message -> testing().runWithSpan("process child", () -> {})); + }); + + testing() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("SQS.CreateQueue") + .hasKind(SpanKind.CLIENT) + .hasNoParent() + .hasAttributesSatisfyingExactly( + equalTo(stringKey("aws.agent"), "java-aws-sdk"), + equalTo(stringKey("aws.endpoint"), "http://localhost:" + sqsPort), + equalTo(stringKey("aws.queue.name"), "testSdkSqs"), + equalTo(RpcIncubatingAttributes.RPC_SYSTEM, "aws-api"), + equalTo(RpcIncubatingAttributes.RPC_SERVICE, "AmazonSQS"), + equalTo(RpcIncubatingAttributes.RPC_METHOD, "CreateQueue"), + equalTo(HttpAttributes.HTTP_REQUEST_METHOD, "POST"), + equalTo(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, 200), + equalTo(UrlAttributes.URL_FULL, "http://localhost:" + sqsPort), + equalTo(ServerAttributes.SERVER_ADDRESS, "localhost"), + equalTo(ServerAttributes.SERVER_PORT, sqsPort), + equalTo(NetworkAttributes.NETWORK_PROTOCOL_VERSION, "1.1"))), + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("testSdkSqs publish") + .hasKind(SpanKind.PRODUCER) + .hasNoParent() + .hasAttributesSatisfyingExactly( + equalTo(stringKey("aws.agent"), "java-aws-sdk"), + equalTo(stringKey("aws.endpoint"), "http://localhost:" + sqsPort), + equalTo( + stringKey("aws.queue.url"), + "http://localhost:" + sqsPort + "/000000000000/testSdkSqs"), + equalTo(RpcIncubatingAttributes.RPC_SYSTEM, "aws-api"), + equalTo(RpcIncubatingAttributes.RPC_SERVICE, "AmazonSQS"), + equalTo(RpcIncubatingAttributes.RPC_METHOD, "SendMessage"), + equalTo(HttpAttributes.HTTP_REQUEST_METHOD, "POST"), + equalTo(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, 200), + equalTo(UrlAttributes.URL_FULL, "http://localhost:" + sqsPort), + equalTo(ServerAttributes.SERVER_ADDRESS, "localhost"), + equalTo(ServerAttributes.SERVER_PORT, sqsPort), + equalTo( + MessagingIncubatingAttributes.MESSAGING_SYSTEM, + MessagingIncubatingAttributes.MessagingSystemValues.AWS_SQS), + equalTo( + MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME, + "testSdkSqs"), + equalTo( + MessagingIncubatingAttributes.MESSAGING_OPERATION, "publish"), + satisfies( + MessagingIncubatingAttributes.MESSAGING_MESSAGE_ID, + val -> val.isInstanceOf(String.class)), + equalTo(NetworkAttributes.NETWORK_PROTOCOL_VERSION, "1.1")), + span -> + span.hasName("testSdkSqs process") + .hasKind(SpanKind.CONSUMER) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo(stringKey("aws.agent"), "java-aws-sdk"), + equalTo(stringKey("aws.endpoint"), "http://localhost:" + sqsPort), + equalTo( + stringKey("aws.queue.url"), + "http://localhost:" + sqsPort + "/000000000000/testSdkSqs"), + equalTo(RpcIncubatingAttributes.RPC_SYSTEM, "aws-api"), + equalTo(RpcIncubatingAttributes.RPC_SERVICE, "AmazonSQS"), + equalTo(RpcIncubatingAttributes.RPC_METHOD, "ReceiveMessage"), + equalTo(HttpAttributes.HTTP_REQUEST_METHOD, "POST"), + equalTo(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, 200), + equalTo(UrlAttributes.URL_FULL, "http://localhost:" + sqsPort), + equalTo(ServerAttributes.SERVER_ADDRESS, "localhost"), + equalTo(ServerAttributes.SERVER_PORT, sqsPort), + equalTo( + MessagingIncubatingAttributes.MESSAGING_SYSTEM, + MessagingIncubatingAttributes.MessagingSystemValues.AWS_SQS), + equalTo( + MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME, + "testSdkSqs"), + equalTo( + MessagingIncubatingAttributes.MESSAGING_OPERATION, "process"), + satisfies( + MessagingIncubatingAttributes.MESSAGING_MESSAGE_ID, + val -> val.isInstanceOf(String.class)), + equalTo(NetworkAttributes.NETWORK_PROTOCOL_VERSION, "1.1")), + span -> + span.hasName("process child") + .hasParent(trace.getSpan(1)) + .hasAttributes(Attributes.empty())), + /* + * 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 -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasNoParent(), + span -> + span.hasName("SQS.ReceiveMessage") + .hasKind(SpanKind.CLIENT) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo(stringKey("aws.agent"), "java-aws-sdk"), + equalTo(stringKey("aws.endpoint"), "http://localhost:" + sqsPort), + equalTo( + stringKey("aws.queue.url"), + "http://localhost:" + sqsPort + "/000000000000/testSdkSqs"), + equalTo(RpcIncubatingAttributes.RPC_SYSTEM, "aws-api"), + equalTo(RpcIncubatingAttributes.RPC_SERVICE, "AmazonSQS"), + equalTo(RpcIncubatingAttributes.RPC_METHOD, "ReceiveMessage"), + equalTo(HttpAttributes.HTTP_REQUEST_METHOD, "POST"), + equalTo(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, 200), + equalTo(UrlAttributes.URL_FULL, "http://localhost:" + sqsPort), + equalTo(ServerAttributes.SERVER_ADDRESS, "localhost"), + equalTo(ServerAttributes.SERVER_PORT, sqsPort), + equalTo(NetworkAttributes.NETWORK_PROTOCOL_VERSION, "1.1")))); + } + + @Test + void testOnlyAddsAttributeNameOnceWhenRequestReused() { + sqsClient.createQueue("testSdkSqs2"); + SendMessageRequest send = + new SendMessageRequest( + "http://localhost:" + sqsPort + "/000000000000/testSdkSqs2", "{\"type\": \"hello\"}"); + sqsClient.sendMessage(send); + ReceiveMessageRequest receive = + new ReceiveMessageRequest("http://localhost:" + sqsPort + "/000000000000/testSdkSqs2"); + sqsClient.receiveMessage(receive); + sqsClient.sendMessage(send); + sqsClient.receiveMessage(receive); + assertThat(receive.getAttributeNames()).isEqualTo(ImmutableList.of("AWSTraceHeader")); + } +} diff --git a/instrumentation/aws-sdk/aws-sdk-1.11/testing/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AbstractSqsTracingTest.java b/instrumentation/aws-sdk/aws-sdk-1.11/testing/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AbstractSqsTracingTest.java new file mode 100644 index 000000000000..3dd3fe259a3b --- /dev/null +++ b/instrumentation/aws-sdk/aws-sdk-1.11/testing/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AbstractSqsTracingTest.java @@ -0,0 +1,493 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.awssdk.v1_11; + +import static io.opentelemetry.api.common.AttributeKey.stringArrayKey; +import static io.opentelemetry.api.common.AttributeKey.stringKey; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.satisfies; +import static org.assertj.core.api.Assertions.assertThat; + +import com.amazonaws.auth.AWSStaticCredentialsProvider; +import com.amazonaws.auth.BasicAWSCredentials; +import com.amazonaws.client.builder.AwsClientBuilder; +import com.amazonaws.services.sqs.AmazonSQSAsync; +import com.amazonaws.services.sqs.AmazonSQSAsyncClient; +import com.amazonaws.services.sqs.AmazonSQSAsyncClientBuilder; +import com.amazonaws.services.sqs.model.Message; +import com.amazonaws.services.sqs.model.MessageAttributeValue; +import com.amazonaws.services.sqs.model.ReceiveMessageRequest; +import com.amazonaws.services.sqs.model.ReceiveMessageResult; +import com.amazonaws.services.sqs.model.SendMessageRequest; +import com.google.common.collect.ImmutableList; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.test.utils.PortUtils; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.sdk.testing.assertj.AttributeAssertion; +import io.opentelemetry.sdk.testing.assertj.SpanDataAssert; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.semconv.HttpAttributes; +import io.opentelemetry.semconv.NetworkAttributes; +import io.opentelemetry.semconv.ServerAttributes; +import io.opentelemetry.semconv.UrlAttributes; +import io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes; +import io.opentelemetry.semconv.incubating.RpcIncubatingAttributes; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; +import org.elasticmq.rest.sqs.SQSRestServer; +import org.elasticmq.rest.sqs.SQSRestServerBuilder; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +public abstract class AbstractSqsTracingTest { + + protected abstract InstrumentationExtension testing(); + + protected abstract AmazonSQSAsyncClientBuilder configureClient( + AmazonSQSAsyncClientBuilder client); + + private static int sqsPort; + private static SQSRestServer sqsRestServer; + private static AmazonSQSAsync sqsClient; + + @BeforeEach + void setUp() { + sqsPort = PortUtils.findOpenPort(); + sqsRestServer = SQSRestServerBuilder.withPort(sqsPort).withInterface("localhost").start(); + + AWSStaticCredentialsProvider credentials = + new AWSStaticCredentialsProvider(new BasicAWSCredentials("x", "x")); + AwsClientBuilder.EndpointConfiguration endpointConfiguration = + new AwsClientBuilder.EndpointConfiguration("http://localhost:" + sqsPort, "elasticmq"); + + sqsClient = + configureClient(AmazonSQSAsyncClient.asyncBuilder()) + .withCredentials(credentials) + .withEndpointConfiguration(endpointConfiguration) + .build(); + } + + @AfterEach + void cleanUp() { + if (sqsRestServer != null) { + sqsRestServer.stopAndWait(); + } + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void testSimpleSqsProducerConsumerServicesCaptureHeaders(boolean testCaptureHeaders) { + sqsClient.createQueue("testSdkSqs"); + + SendMessageRequest sendMessageRequest = + new SendMessageRequest( + "http://localhost:" + sqsPort + "/000000000000/testSdkSqs", "{\"type\": \"hello\"}"); + + if (testCaptureHeaders) { + sendMessageRequest.addMessageAttributesEntry( + "test-message-header", + new MessageAttributeValue().withDataType("String").withStringValue("test")); + } + sqsClient.sendMessage(sendMessageRequest); + + ReceiveMessageRequest receiveMessageRequest = + new ReceiveMessageRequest("http://localhost:" + sqsPort + "/000000000000/testSdkSqs"); + if (testCaptureHeaders) { + receiveMessageRequest.withMessageAttributeNames("test-message-header"); + } + ReceiveMessageResult receiveMessageResult = sqsClient.receiveMessage(receiveMessageRequest); + + // test different ways of iterating the messages list + if (testCaptureHeaders) { + for (Message unused : receiveMessageResult.getMessages()) { + testing().runWithSpan("process child", () -> {}); + } + } else { + receiveMessageResult + .getMessages() + .forEach(message -> testing().runWithSpan("process child", () -> {})); + } + + testing() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("SQS.CreateQueue") + .hasKind(SpanKind.CLIENT) + .hasNoParent() + .hasAttributesSatisfyingExactly( + equalTo(stringKey("aws.agent"), "java-aws-sdk"), + equalTo(stringKey("aws.endpoint"), "http://localhost:" + sqsPort), + equalTo(stringKey("aws.queue.name"), "testSdkSqs"), + equalTo(RpcIncubatingAttributes.RPC_SYSTEM, "aws-api"), + equalTo(RpcIncubatingAttributes.RPC_SERVICE, "AmazonSQS"), + equalTo(RpcIncubatingAttributes.RPC_METHOD, "CreateQueue"), + equalTo(HttpAttributes.HTTP_REQUEST_METHOD, "POST"), + equalTo(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, 200), + equalTo(UrlAttributes.URL_FULL, "http://localhost:" + sqsPort), + equalTo(ServerAttributes.SERVER_ADDRESS, "localhost"), + equalTo(ServerAttributes.SERVER_PORT, sqsPort), + equalTo(NetworkAttributes.NETWORK_PROTOCOL_VERSION, "1.1"))), + trace -> + trace.hasSpansSatisfyingExactly( + span -> { + List attributes = + new ArrayList<>( + Arrays.asList( + equalTo(stringKey("aws.agent"), "java-aws-sdk"), + equalTo(stringKey("aws.endpoint"), "http://localhost:" + sqsPort), + equalTo( + stringKey("aws.queue.url"), + "http://localhost:" + sqsPort + "/000000000000/testSdkSqs"), + equalTo(RpcIncubatingAttributes.RPC_SYSTEM, "aws-api"), + equalTo(RpcIncubatingAttributes.RPC_SERVICE, "AmazonSQS"), + equalTo(RpcIncubatingAttributes.RPC_METHOD, "SendMessage"), + equalTo(HttpAttributes.HTTP_REQUEST_METHOD, "POST"), + equalTo(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, 200), + equalTo(UrlAttributes.URL_FULL, "http://localhost:" + sqsPort), + equalTo(ServerAttributes.SERVER_ADDRESS, "localhost"), + equalTo(ServerAttributes.SERVER_PORT, sqsPort), + equalTo( + MessagingIncubatingAttributes.MESSAGING_SYSTEM, + MessagingIncubatingAttributes.MessagingSystemValues.AWS_SQS), + equalTo( + MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME, + "testSdkSqs"), + equalTo( + MessagingIncubatingAttributes.MESSAGING_OPERATION, "publish"), + satisfies( + MessagingIncubatingAttributes.MESSAGING_MESSAGE_ID, + val -> val.isInstanceOf(String.class)), + equalTo(NetworkAttributes.NETWORK_PROTOCOL_VERSION, "1.1"))); + + if (testCaptureHeaders) { + attributes.add( + satisfies( + stringArrayKey("messaging.header.test_message_header"), + val -> val.isEqualTo(ImmutableList.of("test")))); + } + + span.hasName("testSdkSqs publish") + .hasKind(SpanKind.PRODUCER) + .hasNoParent() + .hasAttributesSatisfyingExactly(attributes); + }), + trace -> + trace.hasSpansSatisfyingExactly( + span -> { + List attributes = + new ArrayList<>( + Arrays.asList( + equalTo(stringKey("aws.agent"), "java-aws-sdk"), + equalTo(stringKey("aws.endpoint"), "http://localhost:" + sqsPort), + equalTo( + stringKey("aws.queue.url"), + "http://localhost:" + sqsPort + "/000000000000/testSdkSqs"), + equalTo(RpcIncubatingAttributes.RPC_SYSTEM, "aws-api"), + equalTo(RpcIncubatingAttributes.RPC_SERVICE, "AmazonSQS"), + equalTo(RpcIncubatingAttributes.RPC_METHOD, "ReceiveMessage"), + equalTo(HttpAttributes.HTTP_REQUEST_METHOD, "POST"), + equalTo(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, 200), + equalTo(UrlAttributes.URL_FULL, "http://localhost:" + sqsPort), + equalTo(ServerAttributes.SERVER_ADDRESS, "localhost"), + equalTo(ServerAttributes.SERVER_PORT, sqsPort), + equalTo( + MessagingIncubatingAttributes.MESSAGING_SYSTEM, + MessagingIncubatingAttributes.MessagingSystemValues.AWS_SQS), + equalTo( + MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME, + "testSdkSqs"), + equalTo( + MessagingIncubatingAttributes.MESSAGING_OPERATION, "receive"), + equalTo( + MessagingIncubatingAttributes.MESSAGING_BATCH_MESSAGE_COUNT, + 1), + equalTo(NetworkAttributes.NETWORK_PROTOCOL_VERSION, "1.1"))); + + if (testCaptureHeaders) { + attributes.add( + satisfies( + stringArrayKey("messaging.header.test_message_header"), + val -> val.isEqualTo(ImmutableList.of("test")))); + } + + span.hasName("testSdkSqs receive") + .hasKind(SpanKind.CONSUMER) + .hasNoParent() + .hasAttributesSatisfyingExactly(attributes); + }, + span -> { + List attributes = + new ArrayList<>( + Arrays.asList( + equalTo(stringKey("aws.agent"), "java-aws-sdk"), + equalTo(stringKey("aws.endpoint"), "http://localhost:" + sqsPort), + equalTo( + stringKey("aws.queue.url"), + "http://localhost:" + sqsPort + "/000000000000/testSdkSqs"), + equalTo(RpcIncubatingAttributes.RPC_SYSTEM, "aws-api"), + equalTo(RpcIncubatingAttributes.RPC_SERVICE, "AmazonSQS"), + equalTo(RpcIncubatingAttributes.RPC_METHOD, "ReceiveMessage"), + equalTo(HttpAttributes.HTTP_REQUEST_METHOD, "POST"), + equalTo(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, 200), + equalTo(UrlAttributes.URL_FULL, "http://localhost:" + sqsPort), + equalTo(ServerAttributes.SERVER_ADDRESS, "localhost"), + equalTo(ServerAttributes.SERVER_PORT, sqsPort), + equalTo( + MessagingIncubatingAttributes.MESSAGING_SYSTEM, + MessagingIncubatingAttributes.MessagingSystemValues.AWS_SQS), + equalTo( + MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME, + "testSdkSqs"), + equalTo( + MessagingIncubatingAttributes.MESSAGING_OPERATION, "process"), + satisfies( + MessagingIncubatingAttributes.MESSAGING_MESSAGE_ID, + val -> val.isInstanceOf(String.class)), + equalTo(NetworkAttributes.NETWORK_PROTOCOL_VERSION, "1.1"))); + + if (testCaptureHeaders) { + attributes.add( + satisfies( + stringArrayKey("messaging.header.test_message_header"), + val -> val.isEqualTo(ImmutableList.of("test")))); + } + span.hasName("testSdkSqs process") + .hasKind(SpanKind.CONSUMER) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly(attributes); + }, + span -> + span.hasName("process child") + .hasParent(trace.getSpan(1)) + .hasAttributes(Attributes.empty()))); + } + + @Test + void testSimpleSqsProducerConsumerServicesWithParentSpan() { + sqsClient.createQueue("testSdkSqs"); + SendMessageRequest sendMessageRequest = + new SendMessageRequest( + "http://localhost:" + sqsPort + "/000000000000/testSdkSqs", "{\"type\": \"hello\"}"); + sqsClient.sendMessage(sendMessageRequest); + + testing() + .runWithSpan( + "parent", + () -> { + ReceiveMessageResult receiveMessageResult = + sqsClient.receiveMessage( + "http://localhost:" + sqsPort + "/000000000000/testSdkSqs"); + receiveMessageResult + .getMessages() + .forEach(message -> testing().runWithSpan("process child", () -> {})); + }); + + testing() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("SQS.CreateQueue") + .hasKind(SpanKind.CLIENT) + .hasNoParent() + .hasAttributesSatisfyingExactly( + equalTo(stringKey("aws.agent"), "java-aws-sdk"), + equalTo(stringKey("aws.endpoint"), "http://localhost:" + sqsPort), + equalTo(stringKey("aws.queue.name"), "testSdkSqs"), + equalTo(RpcIncubatingAttributes.RPC_SYSTEM, "aws-api"), + equalTo(RpcIncubatingAttributes.RPC_SERVICE, "AmazonSQS"), + equalTo(RpcIncubatingAttributes.RPC_METHOD, "CreateQueue"), + equalTo(HttpAttributes.HTTP_REQUEST_METHOD, "POST"), + equalTo(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, 200), + equalTo(UrlAttributes.URL_FULL, "http://localhost:" + sqsPort), + equalTo(ServerAttributes.SERVER_ADDRESS, "localhost"), + equalTo(ServerAttributes.SERVER_PORT, sqsPort), + equalTo(NetworkAttributes.NETWORK_PROTOCOL_VERSION, "1.1"))), + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("testSdkSqs publish") + .hasKind(SpanKind.PRODUCER) + .hasNoParent() + .hasAttributesSatisfyingExactly( + equalTo(stringKey("aws.agent"), "java-aws-sdk"), + equalTo(stringKey("aws.endpoint"), "http://localhost:" + sqsPort), + equalTo( + stringKey("aws.queue.url"), + "http://localhost:" + sqsPort + "/000000000000/testSdkSqs"), + equalTo(RpcIncubatingAttributes.RPC_SYSTEM, "aws-api"), + equalTo(RpcIncubatingAttributes.RPC_SERVICE, "AmazonSQS"), + equalTo(RpcIncubatingAttributes.RPC_METHOD, "SendMessage"), + equalTo(HttpAttributes.HTTP_REQUEST_METHOD, "POST"), + equalTo(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, 200), + equalTo(UrlAttributes.URL_FULL, "http://localhost:" + sqsPort), + equalTo(ServerAttributes.SERVER_ADDRESS, "localhost"), + equalTo(ServerAttributes.SERVER_PORT, sqsPort), + equalTo( + MessagingIncubatingAttributes.MESSAGING_SYSTEM, + MessagingIncubatingAttributes.MessagingSystemValues.AWS_SQS), + equalTo( + MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME, + "testSdkSqs"), + equalTo( + MessagingIncubatingAttributes.MESSAGING_OPERATION, "publish"), + satisfies( + MessagingIncubatingAttributes.MESSAGING_MESSAGE_ID, + val -> val.isInstanceOf(String.class)), + equalTo(NetworkAttributes.NETWORK_PROTOCOL_VERSION, "1.1"))), + trace -> { + AtomicReference receiveSpan = new AtomicReference<>(); + AtomicReference processSpan = new AtomicReference<>(); + + List> assertions = + new ArrayList<>( + Arrays.asList( + span -> + span.hasName("parent") + .hasNoParent() + .hasAttributes(Attributes.empty()), + span -> + span.hasName("SQS.ReceiveMessage") + .hasKind(SpanKind.CLIENT) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo(stringKey("aws.agent"), "java-aws-sdk"), + equalTo( + stringKey("aws.endpoint"), "http://localhost:" + sqsPort), + equalTo( + stringKey("aws.queue.url"), + "http://localhost:" + + sqsPort + + "/000000000000/testSdkSqs"), + equalTo(RpcIncubatingAttributes.RPC_SYSTEM, "aws-api"), + equalTo(RpcIncubatingAttributes.RPC_SERVICE, "AmazonSQS"), + equalTo(RpcIncubatingAttributes.RPC_METHOD, "ReceiveMessage"), + equalTo(HttpAttributes.HTTP_REQUEST_METHOD, "POST"), + equalTo(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, 200), + equalTo( + UrlAttributes.URL_FULL, "http://localhost:" + sqsPort), + equalTo(ServerAttributes.SERVER_ADDRESS, "localhost"), + equalTo(ServerAttributes.SERVER_PORT, sqsPort), + equalTo(NetworkAttributes.NETWORK_PROTOCOL_VERSION, "1.1")), + span -> + span.hasName("testSdkSqs receive") + .hasKind(SpanKind.CONSUMER) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo(stringKey("aws.agent"), "java-aws-sdk"), + equalTo( + stringKey("aws.endpoint"), "http://localhost:" + sqsPort), + equalTo( + stringKey("aws.queue.url"), + "http://localhost:" + + sqsPort + + "/000000000000/testSdkSqs"), + equalTo(RpcIncubatingAttributes.RPC_SYSTEM, "aws-api"), + equalTo(RpcIncubatingAttributes.RPC_SERVICE, "AmazonSQS"), + equalTo(RpcIncubatingAttributes.RPC_METHOD, "ReceiveMessage"), + equalTo(HttpAttributes.HTTP_REQUEST_METHOD, "POST"), + equalTo(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, 200), + equalTo( + UrlAttributes.URL_FULL, "http://localhost:" + sqsPort), + equalTo(ServerAttributes.SERVER_ADDRESS, "localhost"), + equalTo(ServerAttributes.SERVER_PORT, sqsPort), + equalTo( + MessagingIncubatingAttributes.MESSAGING_SYSTEM, + MessagingIncubatingAttributes.MessagingSystemValues + .AWS_SQS), + equalTo( + MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME, + "testSdkSqs"), + equalTo( + MessagingIncubatingAttributes.MESSAGING_OPERATION, + "receive"), + equalTo( + MessagingIncubatingAttributes + .MESSAGING_BATCH_MESSAGE_COUNT, + 1), + equalTo(NetworkAttributes.NETWORK_PROTOCOL_VERSION, "1.1")), + span -> + span.hasName("testSdkSqs process") + .hasKind(SpanKind.CONSUMER) + .hasParent(receiveSpan.get()) + .hasAttributesSatisfyingExactly( + equalTo(stringKey("aws.agent"), "java-aws-sdk"), + equalTo( + stringKey("aws.endpoint"), "http://localhost:" + sqsPort), + equalTo( + stringKey("aws.queue.url"), + "http://localhost:" + + sqsPort + + "/000000000000/testSdkSqs"), + equalTo(RpcIncubatingAttributes.RPC_SYSTEM, "aws-api"), + equalTo(RpcIncubatingAttributes.RPC_SERVICE, "AmazonSQS"), + equalTo(RpcIncubatingAttributes.RPC_METHOD, "ReceiveMessage"), + equalTo(HttpAttributes.HTTP_REQUEST_METHOD, "POST"), + equalTo(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, 200), + equalTo( + UrlAttributes.URL_FULL, "http://localhost:" + sqsPort), + equalTo(ServerAttributes.SERVER_ADDRESS, "localhost"), + equalTo(ServerAttributes.SERVER_PORT, sqsPort), + equalTo( + MessagingIncubatingAttributes.MESSAGING_SYSTEM, + MessagingIncubatingAttributes.MessagingSystemValues + .AWS_SQS), + equalTo( + MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME, + "testSdkSqs"), + equalTo( + MessagingIncubatingAttributes.MESSAGING_OPERATION, + "process"), + satisfies( + MessagingIncubatingAttributes.MESSAGING_MESSAGE_ID, + val -> val.isInstanceOf(String.class)), + equalTo(NetworkAttributes.NETWORK_PROTOCOL_VERSION, "1.1")), + span -> + span.hasName("process child") + .hasParent(processSpan.get()) + .hasAttributes(Attributes.empty()))); + + // on jdk8 the order of the "SQS.ReceiveMessage" and "testSdkSqs receive" + // spans can vary + if ("SQS.ReceiveMessage".equals(trace.getSpan(1).getName())) { + receiveSpan.set(trace.getSpan(2)); + processSpan.set(trace.getSpan(3)); + } else { + receiveSpan.set(trace.getSpan(1)); + processSpan.set(trace.getSpan(2)); + + // move "SQS.ReceiveMessage" assertions to the last position + assertions.add(assertions.remove(1)); + } + + trace.hasSpansSatisfyingExactly(assertions); + }); + } + + @Test + void testOnlyAddsAttributeNameOnceWhenRequestReused() { + sqsClient.createQueue("testSdkSqs2"); + SendMessageRequest send = + new SendMessageRequest( + "http://localhost:$sqsPort/000000000000/testSdkSqs2", "{\"type\": \"hello\"}"); + sqsClient.sendMessage(send); + ReceiveMessageRequest receive = + new ReceiveMessageRequest("http://localhost:$sqsPort/000000000000/testSdkSqs2"); + sqsClient.receiveMessage(receive); + sqsClient.sendMessage(send); + sqsClient.receiveMessage(receive); + assertThat(receive.getAttributeNames()).isEqualTo(ImmutableList.of("AWSTraceHeader")); + } +} diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/javaagent/build.gradle.kts b/instrumentation/aws-sdk/aws-sdk-2.2/javaagent/build.gradle.kts index 4def26b6629d..9f83480f5511 100644 --- a/instrumentation/aws-sdk/aws-sdk-2.2/javaagent/build.gradle.kts +++ b/instrumentation/aws-sdk/aws-sdk-2.2/javaagent/build.gradle.kts @@ -10,14 +10,31 @@ muzzle { // 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") + excludeInstrumentationName("aws-sdk-2.2-sqs") + excludeInstrumentationName("aws-sdk-2.2-sns") + excludeInstrumentationName("aws-sdk-2.2-lambda") + + // several software.amazon.awssdk artifacts are missing for this version + skip("2.17.200") + } + + fail { + group.set("software.amazon.awssdk") + module.set("aws-core") + versions.set("[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") + + // "fail" asserts that *all* the instrumentation modules fail to load, but the core one is + // actually expected to succeed, so exclude it from checks. + excludeInstrumentationName("aws-sdk-2.2-core") // several software.amazon.awssdk artifacts are missing for this version skip("2.17.200") } -} -muzzle { pass { group.set("software.amazon.awssdk") module.set("sqs") @@ -26,6 +43,38 @@ muzzle { // client, which is not target of instrumentation anyways. extraDependency("software.amazon.awssdk:protocol-core") + excludeInstrumentationName("aws-sdk-2.2-sns") + excludeInstrumentationName("aws-sdk-2.2-lambda") + + // several software.amazon.awssdk artifacts are missing for this version + skip("2.17.200") + } + + pass { + group.set("software.amazon.awssdk") + module.set("sns") + versions.set("[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") + + excludeInstrumentationName("aws-sdk-2.2-sqs") + excludeInstrumentationName("aws-sdk-2.2-lambda") + + // several software.amazon.awssdk artifacts are missing for this version + skip("2.17.200") + } + pass { + group.set("software.amazon.awssdk") + module.set("lambda") + versions.set("[2.17.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") + + excludeInstrumentationName("aws-sdk-2.2-sqs") + excludeInstrumentationName("aws-sdk-2.2-sns") + // several software.amazon.awssdk artifacts are missing for this version skip("2.17.200") } @@ -39,31 +88,80 @@ dependencies { library("software.amazon.awssdk:sqs:2.2.0") testImplementation(project(":instrumentation:aws-sdk:aws-sdk-2.2:testing")) + testImplementation("io.opentelemetry.contrib:opentelemetry-aws-xray-propagator") + // 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")) - - latestDepTestLibrary("software.amazon.awssdk:aws-json-protocol:+") - latestDepTestLibrary("software.amazon.awssdk:aws-core:+") - latestDepTestLibrary("software.amazon.awssdk:dynamodb:+") - latestDepTestLibrary("software.amazon.awssdk:ec2:+") - latestDepTestLibrary("software.amazon.awssdk:kinesis:+") - latestDepTestLibrary("software.amazon.awssdk:rds:+") - latestDepTestLibrary("software.amazon.awssdk:s3:+") - latestDepTestLibrary("software.amazon.awssdk:sqs:+") + testInstrumentation(project(":instrumentation:apache-httpclient:apache-httpclient-4.0:javaagent")) + testInstrumentation(project(":instrumentation:apache-httpclient:apache-httpclient-5.0:javaagent")) + testInstrumentation(project(":instrumentation:netty:netty-4.1:javaagent")) + + testLibrary("software.amazon.awssdk:dynamodb:2.2.0") + testLibrary("software.amazon.awssdk:ec2:2.2.0") + testLibrary("software.amazon.awssdk:kinesis:2.2.0") + testLibrary("software.amazon.awssdk:lambda:2.2.0") + testLibrary("software.amazon.awssdk:rds:2.2.0") + testLibrary("software.amazon.awssdk:s3:2.2.0") + testLibrary("software.amazon.awssdk:sqs:2.2.0") + testLibrary("software.amazon.awssdk:sns:2.2.0") + testLibrary("software.amazon.awssdk:ses:2.2.0") } -tasks.withType().configureEach { - systemProperty("testLatestDeps", findProperty("testLatestDeps") as Boolean) - // TODO run tests both with and without experimental span attributes, with & without extra propagation - systemProperties(mapOf( - "otel.instrumentation.aws-sdk.experimental-span-attributes" to "true", - "otel.instrumentation.aws-sdk.experimental-use-propagator-for-messaging" to "true", - )) +val latestDepTest = findProperty("testLatestDeps") as Boolean + +testing { + suites { + val s3PresignerTest by registering(JvmTestSuite::class) { + dependencies { + if (latestDepTest) { + implementation("software.amazon.awssdk:s3:+") + } else { + implementation("software.amazon.awssdk:s3:2.10.12") + } + implementation(project(":instrumentation:aws-sdk:aws-sdk-2.2:library")) + } + } + } } -tasks.withType().configureEach { - mergeServiceFiles { - include("software/amazon/awssdk/global/handlers/execution.interceptors") +tasks { + val testExperimentalSqs by registering(Test::class) { + filter { + excludeTestsMatching("Aws2SqsSuppressReceiveSpansTest") + } + systemProperty("otel.instrumentation.aws-sdk.experimental-use-propagator-for-messaging", "true") + systemProperty("otel.instrumentation.messaging.experimental.receive-telemetry.enabled", "true") + } + + val testReceiveSpansDisabled by registering(Test::class) { + filter { + includeTestsMatching("Aws2SqsSuppressReceiveSpansTest") + } + include("**/Aws2SqsSuppressReceiveSpansTest.*") + } + + test { + filter { + excludeTestsMatching("Aws2SqsSuppressReceiveSpansTest") + } + systemProperty("otel.instrumentation.messaging.experimental.receive-telemetry.enabled", "true") + } + + check { + dependsOn(testExperimentalSqs) + dependsOn(testReceiveSpansDisabled) + dependsOn(testing.suites) + } + + withType().configureEach { + // TODO run tests both with and without experimental span attributes + systemProperty("otel.instrumentation.aws-sdk.experimental-span-attributes", "true") + systemProperty("otel.instrumentation.aws-sdk.experimental-record-individual-http-error", "true") + systemProperty("testLatestDeps", findProperty("testLatestDeps") as Boolean) + } + + withType().configureEach { + mergeServiceFiles { + include("software/amazon/awssdk/global/handlers/execution.interceptors") + } } } diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/javaagent/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/LambdaAdviceBridge.java b/instrumentation/aws-sdk/aws-sdk-2.2/javaagent/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/LambdaAdviceBridge.java new file mode 100644 index 000000000000..3aaa17525137 --- /dev/null +++ b/instrumentation/aws-sdk/aws-sdk-2.2/javaagent/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/LambdaAdviceBridge.java @@ -0,0 +1,15 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.awssdk.v2_2; + +public final class LambdaAdviceBridge { + private LambdaAdviceBridge() {} + + public static void referenceForMuzzleOnly() { + throw new UnsupportedOperationException( + LambdaImpl.class.getName() + " referencing for muzzle, should never be actually called"); + } +} diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/javaagent/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/SnsAdviceBridge.java b/instrumentation/aws-sdk/aws-sdk-2.2/javaagent/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/SnsAdviceBridge.java new file mode 100644 index 000000000000..9077412ec258 --- /dev/null +++ b/instrumentation/aws-sdk/aws-sdk-2.2/javaagent/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/SnsAdviceBridge.java @@ -0,0 +1,15 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.awssdk.v2_2; + +public final class SnsAdviceBridge { + private SnsAdviceBridge() {} + + public static void referenceForMuzzleOnly() { + throw new UnsupportedOperationException( + SnsImpl.class.getName() + " referencing for muzzle, should never be actually called"); + } +} diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/SqsAdviceBridge.java b/instrumentation/aws-sdk/aws-sdk-2.2/javaagent/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/SqsAdviceBridge.java similarity index 53% rename from instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/SqsAdviceBridge.java rename to instrumentation/aws-sdk/aws-sdk-2.2/javaagent/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/SqsAdviceBridge.java index 20eebbeac8e2..ddb3c05c5cbb 100644 --- a/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/SqsAdviceBridge.java +++ b/instrumentation/aws-sdk/aws-sdk-2.2/javaagent/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/SqsAdviceBridge.java @@ -8,8 +8,8 @@ public final class SqsAdviceBridge { private SqsAdviceBridge() {} - public static void init() { - // called from advice - SqsImpl.init(); // Reference the actual, package-private, implementation class for Muzzle + public static void referenceForMuzzleOnly() { + throw new UnsupportedOperationException( + SqsImpl.class.getName() + " referencing for muzzle, should never be actually called"); } } diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/awssdk/v2_2/AbstractAwsSdkInstrumentationModule.java b/instrumentation/aws-sdk/aws-sdk-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/awssdk/v2_2/AbstractAwsSdkInstrumentationModule.java index e3cc3579cd3c..3138429d4fc9 100644 --- a/instrumentation/aws-sdk/aws-sdk-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/awssdk/v2_2/AbstractAwsSdkInstrumentationModule.java +++ b/instrumentation/aws-sdk/aws-sdk-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/awssdk/v2_2/AbstractAwsSdkInstrumentationModule.java @@ -5,16 +5,62 @@ package io.opentelemetry.javaagent.instrumentation.awssdk.v2_2; +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.hasClassesNamed; +import static java.util.Collections.singletonList; +import static net.bytebuddy.matcher.ElementMatchers.named; + import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.extension.instrumentation.internal.ExperimentalInstrumentationModule; +import java.util.List; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; -abstract class AbstractAwsSdkInstrumentationModule extends InstrumentationModule { +abstract class AbstractAwsSdkInstrumentationModule extends InstrumentationModule + implements ExperimentalInstrumentationModule { protected AbstractAwsSdkInstrumentationModule(String additionalInstrumentationName) { super("aws-sdk", "aws-sdk-2.2", additionalInstrumentationName); } + @Override + public String getModuleGroup() { + return "aws-sdk-v2"; + } + @Override public boolean isHelperClass(String className) { return className.startsWith("io.opentelemetry.contrib.awsxray."); } + + @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()); + } + + abstract void doTransform(TypeTransformer transformer); + + // A type instrumentation is needed to trigger resource injection. + public 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) { + doTransform(transformer); + } + } } diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/awssdk/v2_2/AwsSdkInstrumentationModule.java b/instrumentation/aws-sdk/aws-sdk-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/awssdk/v2_2/AwsSdkInstrumentationModule.java index 8ee1719a7c2e..18d9ca5bc0cb 100644 --- a/instrumentation/aws-sdk/aws-sdk-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/awssdk/v2_2/AwsSdkInstrumentationModule.java +++ b/instrumentation/aws-sdk/aws-sdk-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/awssdk/v2_2/AwsSdkInstrumentationModule.java @@ -5,19 +5,13 @@ package io.opentelemetry.javaagent.instrumentation.awssdk.v2_2; -import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.hasClassesNamed; -import static java.util.Collections.singletonList; -import static net.bytebuddy.matcher.ElementMatchers.named; - import com.google.auto.service.AutoService; import io.opentelemetry.instrumentation.awssdk.v2_2.autoconfigure.TracingExecutionInterceptor; import io.opentelemetry.javaagent.extension.instrumentation.HelperResourceBuilder; 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; +import io.opentelemetry.javaagent.extension.instrumentation.internal.injection.ClassInjector; +import io.opentelemetry.javaagent.extension.instrumentation.internal.injection.InjectionMode; @AutoService(InstrumentationModule.class) public class AwsSdkInstrumentationModule extends AbstractAwsSdkInstrumentationModule { @@ -35,30 +29,15 @@ public void registerHelperResources(HelperResourceBuilder helperResourceBuilder) } @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"); + public void injectClasses(ClassInjector injector) { + injector + .proxyBuilder( + "io.opentelemetry.instrumentation.awssdk.v2_2.autoconfigure.TracingExecutionInterceptor") + .inject(InjectionMode.CLASS_ONLY); } @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. - } + void doTransform(TypeTransformer transformer) { + // Nothing to transform, this type instrumentation is only used for injecting resources. } } diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/awssdk/v2_2/DefaultSqsAsyncClientBuilderInstrumentation.java b/instrumentation/aws-sdk/aws-sdk-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/awssdk/v2_2/DefaultSqsAsyncClientBuilderInstrumentation.java new file mode 100644 index 000000000000..8ecf30b62313 --- /dev/null +++ b/instrumentation/aws-sdk/aws-sdk-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/awssdk/v2_2/DefaultSqsAsyncClientBuilderInstrumentation.java @@ -0,0 +1,39 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.awssdk.v2_2; + +import static net.bytebuddy.matcher.ElementMatchers.named; + +import io.opentelemetry.instrumentation.awssdk.v2_2.autoconfigure.AwsSdkSingletons; +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 software.amazon.awssdk.services.sqs.SqsAsyncClient; + +public class DefaultSqsAsyncClientBuilderInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("software.amazon.awssdk.services.sqs.DefaultSqsAsyncClientBuilder"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + named("buildClient"), this.getClass().getName() + "$BuildClientAdvice"); + } + + @SuppressWarnings("unused") + public static class BuildClientAdvice { + + @Advice.OnMethodExit(suppress = Throwable.class) + public static void methodExit(@Advice.Return(readOnly = false) SqsAsyncClient sqsClient) { + sqsClient = AwsSdkSingletons.telemetry().wrap(sqsClient); + } + } +} diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/awssdk/v2_2/DefaultSqsClientBuilderInstrumentation.java b/instrumentation/aws-sdk/aws-sdk-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/awssdk/v2_2/DefaultSqsClientBuilderInstrumentation.java new file mode 100644 index 000000000000..7e360066e29f --- /dev/null +++ b/instrumentation/aws-sdk/aws-sdk-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/awssdk/v2_2/DefaultSqsClientBuilderInstrumentation.java @@ -0,0 +1,39 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.awssdk.v2_2; + +import static net.bytebuddy.matcher.ElementMatchers.named; + +import io.opentelemetry.instrumentation.awssdk.v2_2.autoconfigure.AwsSdkSingletons; +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 software.amazon.awssdk.services.sqs.SqsClient; + +public class DefaultSqsClientBuilderInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("software.amazon.awssdk.services.sqs.DefaultSqsClientBuilder"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + named("buildClient"), this.getClass().getName() + "$BuildClientAdvice"); + } + + @SuppressWarnings("unused") + public static class BuildClientAdvice { + + @Advice.OnMethodExit(suppress = Throwable.class) + public static void methodExit(@Advice.Return(readOnly = false) SqsClient sqsClient) { + sqsClient = AwsSdkSingletons.telemetry().wrap(sqsClient); + } + } +} diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/awssdk/v2_2/LambdaInstrumentationModule.java b/instrumentation/aws-sdk/aws-sdk-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/awssdk/v2_2/LambdaInstrumentationModule.java new file mode 100644 index 000000000000..575bb5ec7fbc --- /dev/null +++ b/instrumentation/aws-sdk/aws-sdk-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/awssdk/v2_2/LambdaInstrumentationModule.java @@ -0,0 +1,47 @@ +/* + * 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.AgentElementMatchers.hasClassesNamed; +import static net.bytebuddy.matcher.ElementMatchers.none; + +import com.google.auto.service.AutoService; +import io.opentelemetry.instrumentation.awssdk.v2_2.LambdaAdviceBridge; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.matcher.ElementMatcher; + +@AutoService(InstrumentationModule.class) +public class LambdaInstrumentationModule extends AbstractAwsSdkInstrumentationModule { + + public LambdaInstrumentationModule() { + super("aws-sdk-2.2-lambda"); + } + + @Override + public ElementMatcher.Junction classLoaderMatcher() { + return hasClassesNamed( + "software.amazon.awssdk.services.lambda.model.InvokeRequest", + "software.amazon.awssdk.protocols.jsoncore.JsonNode"); + } + + @Override + public void doTransform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + none(), LambdaInstrumentationModule.class.getName() + "$RegisterAdvice"); + } + + @SuppressWarnings("unused") + public static class RegisterAdvice { + @Advice.OnMethodExit(suppress = Throwable.class) + public static void onExit() { + // (indirectly) using LambdaImpl class here to make sure it is available from LambdaAccess + // (injected into app classloader) and checked by Muzzle + LambdaAdviceBridge.referenceForMuzzleOnly(); + } + } +} diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/awssdk/v2_2/SnsInstrumentationModule.java b/instrumentation/aws-sdk/aws-sdk-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/awssdk/v2_2/SnsInstrumentationModule.java new file mode 100644 index 000000000000..6086fb4431e3 --- /dev/null +++ b/instrumentation/aws-sdk/aws-sdk-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/awssdk/v2_2/SnsInstrumentationModule.java @@ -0,0 +1,45 @@ +/* + * 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.AgentElementMatchers.hasClassesNamed; +import static net.bytebuddy.matcher.ElementMatchers.none; + +import com.google.auto.service.AutoService; +import io.opentelemetry.instrumentation.awssdk.v2_2.SnsAdviceBridge; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.matcher.ElementMatcher; + +@AutoService(InstrumentationModule.class) +public class SnsInstrumentationModule extends AbstractAwsSdkInstrumentationModule { + + public SnsInstrumentationModule() { + super("aws-sdk-2.2-sns"); + } + + @Override + public ElementMatcher.Junction classLoaderMatcher() { + return hasClassesNamed("software.amazon.awssdk.services.sns.SnsClient"); + } + + @Override + public void doTransform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + none(), SnsInstrumentationModule.class.getName() + "$RegisterAdvice"); + } + + @SuppressWarnings("unused") + public static class RegisterAdvice { + @Advice.OnMethodExit(suppress = Throwable.class) + public static void onExit() { + // (indirectly) using SnsImpl class here to make sure it is available from SnsAccess + // (injected into app classloader) and checked by Muzzle + SnsAdviceBridge.referenceForMuzzleOnly(); + } + } +} diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/awssdk/v2_2/SqsInstrumentationModule.java b/instrumentation/aws-sdk/aws-sdk-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/awssdk/v2_2/SqsInstrumentationModule.java index 380443ab96a1..bc1f0c351713 100644 --- a/instrumentation/aws-sdk/aws-sdk-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/awssdk/v2_2/SqsInstrumentationModule.java +++ b/instrumentation/aws-sdk/aws-sdk-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/awssdk/v2_2/SqsInstrumentationModule.java @@ -5,18 +5,17 @@ package io.opentelemetry.javaagent.instrumentation.awssdk.v2_2; -import static java.util.Collections.singletonList; -import static net.bytebuddy.matcher.ElementMatchers.isConstructor; -import static net.bytebuddy.matcher.ElementMatchers.named; +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.hasClassesNamed; +import static net.bytebuddy.matcher.ElementMatchers.none; import com.google.auto.service.AutoService; import io.opentelemetry.instrumentation.awssdk.v2_2.SqsAdviceBridge; import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import java.util.ArrayList; import java.util.List; import net.bytebuddy.asm.Advice; -import net.bytebuddy.description.type.TypeDescription; import net.bytebuddy.matcher.ElementMatcher; @AutoService(InstrumentationModule.class) @@ -27,21 +26,22 @@ public SqsInstrumentationModule() { } @Override - public List typeInstrumentations() { - return singletonList(new DefaultSqsClientTypeInstrumentation()); + public ElementMatcher.Junction classLoaderMatcher() { + return hasClassesNamed("software.amazon.awssdk.services.sqs.SqsClient"); } - public static class DefaultSqsClientTypeInstrumentation implements TypeInstrumentation { - @Override - public ElementMatcher typeMatcher() { - return named("software.amazon.awssdk.services.sqs.DefaultSqsClient"); - } + @Override + public List typeInstrumentations() { + List instrumentations = new ArrayList<>(super.typeInstrumentations()); + instrumentations.add(new DefaultSqsClientBuilderInstrumentation()); + instrumentations.add(new DefaultSqsAsyncClientBuilderInstrumentation()); + return instrumentations; + } - @Override - public void transform(TypeTransformer transformer) { - transformer.applyAdviceToMethod( - isConstructor(), SqsInstrumentationModule.class.getName() + "$RegisterAdvice"); - } + @Override + public void doTransform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + none(), SqsInstrumentationModule.class.getName() + "$RegisterAdvice"); } @SuppressWarnings("unused") @@ -50,7 +50,7 @@ public static class RegisterAdvice { public static void onExit() { // (indirectly) using SqsImpl class here to make sure it is available from SqsAccess // (injected into app classloader) and checked by Muzzle - SqsAdviceBridge.init(); + SqsAdviceBridge.referenceForMuzzleOnly(); } } } diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/javaagent/src/s3PresignerTest/java/io/opentelemetry/javaagent/instrumentation/awssdk/v2_2/S3PresignerTest.java b/instrumentation/aws-sdk/aws-sdk-2.2/javaagent/src/s3PresignerTest/java/io/opentelemetry/javaagent/instrumentation/awssdk/v2_2/S3PresignerTest.java new file mode 100644 index 000000000000..1cbe4e4696e8 --- /dev/null +++ b/instrumentation/aws-sdk/aws-sdk-2.2/javaagent/src/s3PresignerTest/java/io/opentelemetry/javaagent/instrumentation/awssdk/v2_2/S3PresignerTest.java @@ -0,0 +1,71 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.awssdk.v2_2; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import java.time.Duration; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.presigner.S3Presigner; +import software.amazon.awssdk.services.s3.presigner.model.GetObjectPresignRequest; +import software.amazon.awssdk.services.s3.presigner.model.PutObjectPresignRequest; + +class S3PresignerTest { + @RegisterExtension + static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + private static final StaticCredentialsProvider CREDENTIALS_PROVIDER = + StaticCredentialsProvider.create( + AwsBasicCredentials.create("my-access-key", "my-secret-key")); + + private static S3Presigner s3Presigner; + + @BeforeAll + static void setUp() { + // trigger adding tracing interceptor + S3Client.builder() + .region(Region.AP_NORTHEAST_1) + .credentialsProvider(CREDENTIALS_PROVIDER) + .build(); + + s3Presigner = + S3Presigner.builder() + .region(Region.AP_NORTHEAST_1) + .credentialsProvider(CREDENTIALS_PROVIDER) + .build(); + } + + @Test + void testPresignGetObject() { + s3Presigner.presignGetObject( + GetObjectPresignRequest.builder() + .getObjectRequest(builder -> builder.bucket("test").key("test")) + .signatureDuration(Duration.ofDays(1)) + .build()); + + assertThat(Span.current().getSpanContext().isValid()).isFalse(); + } + + @Test + void testPresignPutObject() { + s3Presigner.presignPutObject( + PutObjectPresignRequest.builder() + .putObjectRequest(builder -> builder.bucket("test").key("test")) + .signatureDuration(Duration.ofDays(1)) + .build()); + + assertThat(Span.current().getSpanContext().isValid()).isFalse(); + } +} diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/javaagent/src/test/groovy/Aws2SqsSuppressReceiveSpansTest.groovy b/instrumentation/aws-sdk/aws-sdk-2.2/javaagent/src/test/groovy/Aws2SqsSuppressReceiveSpansTest.groovy new file mode 100644 index 000000000000..e7f2128682d6 --- /dev/null +++ b/instrumentation/aws-sdk/aws-sdk-2.2/javaagent/src/test/groovy/Aws2SqsSuppressReceiveSpansTest.groovy @@ -0,0 +1,27 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import io.opentelemetry.instrumentation.awssdk.v2_2.AbstractAws2SqsSuppressReceiveSpansTest +import io.opentelemetry.instrumentation.test.AgentTestTrait +import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration +import software.amazon.awssdk.services.sqs.SqsAsyncClient +import software.amazon.awssdk.services.sqs.SqsClient + +class Aws2SqsSuppressReceiveSpansTest extends AbstractAws2SqsSuppressReceiveSpansTest implements AgentTestTrait { + @Override + ClientOverrideConfiguration.Builder createOverrideConfigurationBuilder() { + return ClientOverrideConfiguration.builder() + } + + @Override + SqsClient configureSqsClient(SqsClient sqsClient) { + return sqsClient + } + + @Override + SqsAsyncClient configureSqsClient(SqsAsyncClient sqsClient) { + return sqsClient + } +} diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/javaagent/src/test/groovy/Aws2SqsTracingTest.groovy b/instrumentation/aws-sdk/aws-sdk-2.2/javaagent/src/test/groovy/Aws2SqsTracingTest.groovy deleted file mode 100644 index 4b73e00cb366..000000000000 --- a/instrumentation/aws-sdk/aws-sdk-2.2/javaagent/src/test/groovy/Aws2SqsTracingTest.groovy +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -import io.opentelemetry.instrumentation.awssdk.v2_2.AbstractAws2SqsTracingTest -import io.opentelemetry.instrumentation.test.AgentTestTrait -import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration - -class Aws2SqsTracingTest extends AbstractAws2SqsTracingTest implements AgentTestTrait { - @Override - ClientOverrideConfiguration.Builder createOverrideConfigurationBuilder() { - return ClientOverrideConfiguration.builder() - } -} diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/awssdk/v2_2/Aws2ClientRecordHttpErrorTest.java b/instrumentation/aws-sdk/aws-sdk-2.2/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/awssdk/v2_2/Aws2ClientRecordHttpErrorTest.java new file mode 100644 index 000000000000..1d7f36ed378d --- /dev/null +++ b/instrumentation/aws-sdk/aws-sdk-2.2/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/awssdk/v2_2/Aws2ClientRecordHttpErrorTest.java @@ -0,0 +1,27 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.awssdk.v2_2; + +import io.opentelemetry.instrumentation.awssdk.v2_2.AbstractAws2ClientRecordHttpErrorTest; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import org.junit.jupiter.api.extension.RegisterExtension; +import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration; + +public class Aws2ClientRecordHttpErrorTest extends AbstractAws2ClientRecordHttpErrorTest { + @RegisterExtension + private final AgentInstrumentationExtension testing = AgentInstrumentationExtension.create(); + + @Override + public ClientOverrideConfiguration.Builder createOverrideConfigurationBuilder() { + return ClientOverrideConfiguration.builder(); + } + + @Override + protected InstrumentationExtension getTesting() { + return testing; + } +} diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/awssdk/v2_2/Aws2LambdaTest.java b/instrumentation/aws-sdk/aws-sdk-2.2/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/awssdk/v2_2/Aws2LambdaTest.java new file mode 100644 index 000000000000..0991e804956e --- /dev/null +++ b/instrumentation/aws-sdk/aws-sdk-2.2/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/awssdk/v2_2/Aws2LambdaTest.java @@ -0,0 +1,35 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.awssdk.v2_2; + +import io.opentelemetry.instrumentation.awssdk.v2_2.AbstractAws2LambdaTest; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import org.junit.jupiter.api.extension.RegisterExtension; +import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration; + +class Aws2LambdaTest extends AbstractAws2LambdaTest { + + @RegisterExtension + private static final AgentInstrumentationExtension testing = + AgentInstrumentationExtension.create(); + + @Override + protected InstrumentationExtension getTesting() { + return testing; + } + + @Override + protected boolean canTestLambdaInvoke() { + // only supported since 2.17.0 + return Boolean.getBoolean("testLatestDeps"); + } + + @Override + protected ClientOverrideConfiguration.Builder createOverrideConfigurationBuilder() { + return ClientOverrideConfiguration.builder(); + } +} diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/awssdk/v2_2/Aws2SqsTracingTest.java b/instrumentation/aws-sdk/aws-sdk-2.2/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/awssdk/v2_2/Aws2SqsTracingTest.java new file mode 100644 index 000000000000..0cc770651693 --- /dev/null +++ b/instrumentation/aws-sdk/aws-sdk-2.2/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/awssdk/v2_2/Aws2SqsTracingTest.java @@ -0,0 +1,40 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.awssdk.v2_2; + +import io.opentelemetry.instrumentation.awssdk.v2_2.AbstractAws2SqsTracingTest; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import org.junit.jupiter.api.extension.RegisterExtension; +import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration; +import software.amazon.awssdk.services.sqs.SqsAsyncClient; +import software.amazon.awssdk.services.sqs.SqsClient; + +class Aws2SqsTracingTest extends AbstractAws2SqsTracingTest { + + @RegisterExtension + private static final AgentInstrumentationExtension testing = + AgentInstrumentationExtension.create(); + + @Override + protected final AgentInstrumentationExtension getTesting() { + return testing; + } + + @Override + protected ClientOverrideConfiguration.Builder createOverrideConfigurationBuilder() { + return ClientOverrideConfiguration.builder(); + } + + @Override + protected SqsClient configureSqsClient(SqsClient sqsClient) { + return sqsClient; + } + + @Override + protected SqsAsyncClient configureSqsClient(SqsAsyncClient sqsClient) { + return sqsClient; + } +} diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/awssdk/v2_2/AwsXrayPropagatorTest.java b/instrumentation/aws-sdk/aws-sdk-2.2/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/awssdk/v2_2/AwsXrayPropagatorTest.java new file mode 100644 index 000000000000..1659f4996893 --- /dev/null +++ b/instrumentation/aws-sdk/aws-sdk-2.2/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/awssdk/v2_2/AwsXrayPropagatorTest.java @@ -0,0 +1,55 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.awssdk.v2_2; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.propagation.TextMapGetter; +import io.opentelemetry.contrib.awsxray.propagator.AwsXrayPropagator; +import java.util.Collections; +import java.util.Map; +import org.junit.jupiter.api.Test; +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.sqs.SqsClient; + +class AwsXrayPropagatorTest { + private static final StaticCredentialsProvider CREDENTIALS_PROVIDER = + StaticCredentialsProvider.create( + AwsBasicCredentials.create("my-access-key", "my-secret-key")); + + @Test + void testAgentShadesAwsXrayPropagator() { + // first build a SqsClient to trigger instrumentation + SqsClient.builder() + .region(Region.AP_NORTHEAST_1) + .credentialsProvider(CREDENTIALS_PROVIDER) + .build(); + // verify that AwsXrayPropagator is usable, if agent has not shaded AwsXrayPropagator this will + // fail with NoSuchMethodError + AwsXrayPropagator.getInstance() + .extract( + Context.root(), + Collections.singletonMap( + "X-Amzn-Trace-Id", + "Root=1-35a77be2-beae321878f706079d392ac3;Parent=df79f9d51134dc0b;Sampled=1"), + StringMapGetter.INSTANCE); + } + + private enum StringMapGetter implements TextMapGetter> { + INSTANCE; + + @Override + public Iterable keys(Map map) { + return map.keySet(); + } + + @Override + public String get(Map map, String s) { + return map.get(s); + } + } +} diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/awssdk/v2_2/QueryProtocolModelTest.java b/instrumentation/aws-sdk/aws-sdk-2.2/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/awssdk/v2_2/QueryProtocolModelTest.java new file mode 100644 index 000000000000..96fcaf20cb39 --- /dev/null +++ b/instrumentation/aws-sdk/aws-sdk-2.2/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/awssdk/v2_2/QueryProtocolModelTest.java @@ -0,0 +1,27 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.awssdk.v2_2; + +import io.opentelemetry.instrumentation.awssdk.v2_2.AbstractQueryProtocolModelTest; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import org.junit.jupiter.api.extension.RegisterExtension; +import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration; + +class QueryProtocolModelTest extends AbstractQueryProtocolModelTest { + @RegisterExtension + private final AgentInstrumentationExtension testing = AgentInstrumentationExtension.create(); + + @Override + protected ClientOverrideConfiguration.Builder createClientOverrideConfigurationBuilder() { + return ClientOverrideConfiguration.builder(); + } + + @Override + protected InstrumentationExtension getTesting() { + return testing; + } +} diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/library-autoconfigure/build.gradle.kts b/instrumentation/aws-sdk/aws-sdk-2.2/library-autoconfigure/build.gradle.kts index 6f3d3936ae3d..7f621f4977d2 100644 --- a/instrumentation/aws-sdk/aws-sdk-2.2/library-autoconfigure/build.gradle.kts +++ b/instrumentation/aws-sdk/aws-sdk-2.2/library-autoconfigure/build.gradle.kts @@ -5,6 +5,8 @@ plugins { base.archivesName.set("${base.archivesName.get()}-autoconfigure") dependencies { + compileOnly(project(":javaagent-extension-api")) + implementation(project(":instrumentation:aws-sdk:aws-sdk-2.2:library")) library("software.amazon.awssdk:aws-core:2.2.0") @@ -12,20 +14,21 @@ dependencies { testImplementation(project(":instrumentation:aws-sdk:aws-sdk-2.2:testing")) - latestDepTestLibrary("software.amazon.awssdk:aws-core:+") - latestDepTestLibrary("software.amazon.awssdk:aws-json-protocol:+") - latestDepTestLibrary("software.amazon.awssdk:dynamodb:+") - latestDepTestLibrary("software.amazon.awssdk:ec2:+") - latestDepTestLibrary("software.amazon.awssdk:kinesis:+") - latestDepTestLibrary("software.amazon.awssdk:rds:+") - latestDepTestLibrary("software.amazon.awssdk:s3:+") - latestDepTestLibrary("software.amazon.awssdk:sqs:+") + testLibrary("software.amazon.awssdk:dynamodb:2.2.0") + testLibrary("software.amazon.awssdk:ec2:2.2.0") + testLibrary("software.amazon.awssdk:kinesis:2.2.0") + testLibrary("software.amazon.awssdk:lambda:2.2.0") + testLibrary("software.amazon.awssdk:rds:2.2.0") + testLibrary("software.amazon.awssdk:s3:2.2.0") + testLibrary("software.amazon.awssdk:sqs:2.2.0") + testLibrary("software.amazon.awssdk:sns:2.2.0") } tasks { test { - systemProperty("testLatestDeps", findProperty("testLatestDeps") as Boolean) systemProperty("otel.instrumentation.aws-sdk.experimental-span-attributes", true) - systemProperty("otel.instrumentation.aws-sdk.experimental-use-propagator-for-messaging", true) + systemProperty("otel.instrumentation.aws-sdk.experimental-record-individual-http-error", true) + systemProperty("otel.instrumentation.messaging.experimental.capture-headers", "test-message-header") + systemProperty("testLatestDeps", findProperty("testLatestDeps") as Boolean) } } diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/library-autoconfigure/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/autoconfigure/AwsSdkSingletons.java b/instrumentation/aws-sdk/aws-sdk-2.2/library-autoconfigure/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/autoconfigure/AwsSdkSingletons.java new file mode 100644 index 000000000000..dddb124eb938 --- /dev/null +++ b/instrumentation/aws-sdk/aws-sdk-2.2/library-autoconfigure/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/autoconfigure/AwsSdkSingletons.java @@ -0,0 +1,83 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.awssdk.v2_2.autoconfigure; + +import static java.util.Collections.emptyList; + +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.instrumentation.api.internal.ConfigPropertiesUtil; +import io.opentelemetry.instrumentation.awssdk.v2_2.AwsSdkTelemetry; +import io.opentelemetry.javaagent.bootstrap.internal.AgentInstrumentationConfig; +import io.opentelemetry.javaagent.bootstrap.internal.ExperimentalConfig; +import java.util.List; + +public final class AwsSdkSingletons { + + private static final boolean HAS_INSTRUMENTATION_CONFIG = hasAgentConfiguration(); + private static final AwsSdkTelemetry TELEMETRY = + AwsSdkTelemetry.builder(GlobalOpenTelemetry.get()) + .setCapturedHeaders(getCapturedHeaders()) + .setCaptureExperimentalSpanAttributes(captureExperimentalSpanAttributes()) + .setMessagingReceiveInstrumentationEnabled(messagingReceiveInstrumentationEnabled()) + .setUseConfiguredPropagatorForMessaging(useMessagingPropagator()) + .setRecordIndividualHttpError(recordIndividualHttpError()) + .build(); + + private static boolean hasAgentConfiguration() { + try { + Class.forName("io.opentelemetry.javaagent.bootstrap.internal.AgentInstrumentationConfig"); + return true; + } catch (ClassNotFoundException e) { + return false; + } + } + + private static List getCapturedHeaders() { + if (HAS_INSTRUMENTATION_CONFIG) { + return ExperimentalConfig.get().getMessagingHeaders(); + } else { + return ConfigPropertiesUtil.getList( + "otel.instrumentation.messaging.experimental.capture-headers", emptyList()); + } + } + + private static boolean captureExperimentalSpanAttributes() { + return getBoolean("otel.instrumentation.aws-sdk.experimental-span-attributes", false); + } + + private static boolean messagingReceiveInstrumentationEnabled() { + if (HAS_INSTRUMENTATION_CONFIG) { + return ExperimentalConfig.get().messagingReceiveInstrumentationEnabled(); + } else { + return ConfigPropertiesUtil.getBoolean( + "otel.instrumentation.messaging.experimental.receive-telemetry.enabled", false); + } + } + + private static boolean useMessagingPropagator() { + return getBoolean( + "otel.instrumentation.aws-sdk.experimental-use-propagator-for-messaging", false); + } + + private static boolean recordIndividualHttpError() { + return getBoolean( + "otel.instrumentation.aws-sdk.experimental-record-individual-http-error", false); + } + + private static boolean getBoolean(String name, boolean defaultValue) { + if (HAS_INSTRUMENTATION_CONFIG) { + return AgentInstrumentationConfig.get().getBoolean(name, defaultValue); + } else { + return ConfigPropertiesUtil.getBoolean(name, defaultValue); + } + } + + public static AwsSdkTelemetry telemetry() { + return TELEMETRY; + } + + private AwsSdkSingletons() {} +} diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/library-autoconfigure/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/autoconfigure/TracingExecutionInterceptor.java b/instrumentation/aws-sdk/aws-sdk-2.2/library-autoconfigure/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/autoconfigure/TracingExecutionInterceptor.java index 657a51b6b7db..4393aee50027 100644 --- a/instrumentation/aws-sdk/aws-sdk-2.2/library-autoconfigure/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/autoconfigure/TracingExecutionInterceptor.java +++ b/instrumentation/aws-sdk/aws-sdk-2.2/library-autoconfigure/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/autoconfigure/TracingExecutionInterceptor.java @@ -5,9 +5,6 @@ package io.opentelemetry.instrumentation.awssdk.v2_2.autoconfigure; -import io.opentelemetry.api.GlobalOpenTelemetry; -import io.opentelemetry.instrumentation.api.internal.ConfigPropertiesUtil; -import io.opentelemetry.instrumentation.awssdk.v2_2.AwsSdkTelemetry; import java.io.InputStream; import java.nio.ByteBuffer; import java.util.Optional; @@ -28,20 +25,8 @@ */ public class TracingExecutionInterceptor implements ExecutionInterceptor { - private static final boolean CAPTURE_EXPERIMENTAL_SPAN_ATTRIBUTES = - ConfigPropertiesUtil.getBoolean( - "otel.instrumentation.aws-sdk.experimental-span-attributes", false); - - private static final boolean USE_MESSAGING_PROPAGATOR = - ConfigPropertiesUtil.getBoolean( - "otel.instrumentation.aws-sdk.experimental-use-propagator-for-messaging", false); - private final ExecutionInterceptor delegate = - AwsSdkTelemetry.builder(GlobalOpenTelemetry.get()) - .setCaptureExperimentalSpanAttributes(CAPTURE_EXPERIMENTAL_SPAN_ATTRIBUTES) - .setUseConfiguredPropagatorForMessaging(USE_MESSAGING_PROPAGATOR) - .build() - .newExecutionInterceptor(); + AwsSdkSingletons.telemetry().newExecutionInterceptor(); @Override public void beforeExecution( diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/library/README.md b/instrumentation/aws-sdk/aws-sdk-2.2/library/README.md index 7e473fee406b..7504c3aaa952 100644 --- a/instrumentation/aws-sdk/aws-sdk-2.2/library/README.md +++ b/instrumentation/aws-sdk/aws-sdk-2.2/library/README.md @@ -9,18 +9,45 @@ To instrument all AWS SDK clients include the `opentelemetry-aws-sdk-2.2-autocon To register instrumentation only on a specific SDK client, register the interceptor when creating it. ```java +AwsSdkTelemetry telemetrty = AwsSdkTelemetry.create(openTelemetry).build(); DynamoDbClient client = DynamoDbClient.builder() .overrideConfiguration(ClientOverrideConfiguration.builder() - .addExecutionInterceptor(AwsSdk.newInterceptor())) + .addExecutionInterceptor(telemetrty.newExecutionInterceptor())) .build()) .build(); ``` +For SQS an additional step is needed +```java +SqsClientBuilder sqsClientBuilder = SqsClient.builder(); +... +SqsClient sqsClient = telemetry.wrap(sqsClientBuilder.build()); +``` +```java +SqsAsyncClientBuilder sqsAsyncClientBuilder = SqsAsyncClient.builder(); +... +SqsAsyncClient sqsAsyncClient = telemetry.wrap(sqsAsyncClientBuilder.build()); +``` + ## Trace propagation -The AWS SDK instrumentation currently only supports injecting the trace header into the request +The AWS SDK instrumentation always injects 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 +propagating the trace through them. + +Additionally, you can enable an experimental option to use the configured propagator to inject into +message attributes (see [parent README](../../README.md)). This currently supports the following AWS APIs: + +- SQS.SendMessage +- SQS.SendMessageBatch +- SNS.Publish + (SNS.PublishBatch is not supported at the moment because it is not available in the minimum SDK + version targeted by the instrumentation) + +Note that injection will only happen if, after injection, a maximum of 10 attributes is used to not +run over API limitations set by AWS. + +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/instrumentation/aws-sdk/aws-sdk-2.2/library/build.gradle.kts b/instrumentation/aws-sdk/aws-sdk-2.2/library/build.gradle.kts index 56f43c7e1668..e4be5bdfe30a 100644 --- a/instrumentation/aws-sdk/aws-sdk-2.2/library/build.gradle.kts +++ b/instrumentation/aws-sdk/aws-sdk-2.2/library/build.gradle.kts @@ -7,24 +7,69 @@ dependencies { library("software.amazon.awssdk:aws-core:2.2.0") library("software.amazon.awssdk:sqs:2.2.0") + library("software.amazon.awssdk:lambda:2.2.0") + library("software.amazon.awssdk:sns:2.2.0") library("software.amazon.awssdk:aws-json-protocol:2.2.0") + // json-utils was added in 2.17.0 + compileOnly("software.amazon.awssdk:json-utils:2.17.0") compileOnly(project(":muzzle")) // For @NoMuzzle testImplementation(project(":instrumentation:aws-sdk:aws-sdk-2.2:testing")) - latestDepTestLibrary("software.amazon.awssdk:aws-core:+") - latestDepTestLibrary("software.amazon.awssdk:aws-json-protocol:+") - latestDepTestLibrary("software.amazon.awssdk:dynamodb:+") - latestDepTestLibrary("software.amazon.awssdk:ec2:+") - latestDepTestLibrary("software.amazon.awssdk:kinesis:+") - latestDepTestLibrary("software.amazon.awssdk:rds:+") - latestDepTestLibrary("software.amazon.awssdk:s3:+") - latestDepTestLibrary("software.amazon.awssdk:sqs:+") + testLibrary("software.amazon.awssdk:dynamodb:2.2.0") + testLibrary("software.amazon.awssdk:ec2:2.2.0") + testLibrary("software.amazon.awssdk:kinesis:2.2.0") + testLibrary("software.amazon.awssdk:rds:2.2.0") + testLibrary("software.amazon.awssdk:s3:2.2.0") + testLibrary("software.amazon.awssdk:ses:2.2.0") +} + +testing { + suites { + val testCoreOnly by registering(JvmTestSuite::class) { + sources { + groovy { + setSrcDirs(listOf("src/testCoreOnly/groovy")) + } + } + + dependencies { + implementation(project()) + implementation(project(":instrumentation:aws-sdk:aws-sdk-2.2:testing")) + compileOnly("software.amazon.awssdk:sqs:2.2.0") + if (findProperty("testLatestDeps") as Boolean) { + implementation("software.amazon.awssdk:aws-core:+") + implementation("software.amazon.awssdk:aws-json-protocol:+") + implementation("software.amazon.awssdk:dynamodb:+") + implementation("software.amazon.awssdk:lambda:+") + } else { + implementation("software.amazon.awssdk:aws-core:2.2.0") + implementation("software.amazon.awssdk:aws-json-protocol:2.2.0") + implementation("software.amazon.awssdk:dynamodb:2.2.0") + implementation("software.amazon.awssdk:lambda:2.2.0") + } + } + } + + val testLambda by registering(JvmTestSuite::class) { + dependencies { + implementation(project()) + implementation(project(":instrumentation:aws-sdk:aws-sdk-2.2:testing")) + if (findProperty("testLatestDeps") as Boolean) { + implementation("software.amazon.awssdk:lambda:+") + } else { + implementation("software.amazon.awssdk:lambda:2.17.0") + } + } + } + } } tasks { - test { - systemProperty("testLatestDeps", findProperty("testLatestDeps") as Boolean) + withType { + // NB: If you'd like to change these, there is some cleanup work to be done, as most tests ignore this and + // set the value directly (the "library" does not normally query it, only library-autoconfigure) systemProperty("otel.instrumentation.aws-sdk.experimental-span-attributes", true) + systemProperty("testLatestDeps", findProperty("testLatestDeps") as Boolean) } } diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AbstractSqsRequest.java b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AbstractSqsRequest.java new file mode 100644 index 000000000000..8413ce772df4 --- /dev/null +++ b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AbstractSqsRequest.java @@ -0,0 +1,13 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.awssdk.v2_2; + +import software.amazon.awssdk.core.interceptor.ExecutionAttributes; + +abstract class AbstractSqsRequest { + + public abstract ExecutionAttributes getRequest(); +} diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AwsSdkExperimentalAttributesExtractor.java b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AwsSdkExperimentalAttributesExtractor.java index 7a365317cfce..646234ce0e03 100644 --- a/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AwsSdkExperimentalAttributesExtractor.java +++ b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AwsSdkExperimentalAttributesExtractor.java @@ -11,10 +11,9 @@ import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; import javax.annotation.Nullable; import software.amazon.awssdk.core.interceptor.ExecutionAttributes; -import software.amazon.awssdk.http.SdkHttpResponse; class AwsSdkExperimentalAttributesExtractor - implements AttributesExtractor { + implements AttributesExtractor { private static final String COMPONENT_NAME = "java-aws-sdk"; private static final AttributeKey AWS_AGENT = AttributeKey.stringKey("aws.agent"); @@ -32,6 +31,6 @@ public void onEnd( AttributesBuilder attributes, Context context, ExecutionAttributes executionAttributes, - @Nullable SdkHttpResponse sdkHttpResponse, + @Nullable Response response, @Nullable Throwable error) {} } diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AwsSdkHttpAttributesGetter.java b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AwsSdkHttpAttributesGetter.java index 67c4ff5a2c45..63284523eb14 100644 --- a/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AwsSdkHttpAttributesGetter.java +++ b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AwsSdkHttpAttributesGetter.java @@ -7,15 +7,14 @@ import static java.util.Collections.emptyList; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpClientAttributesGetter; +import io.opentelemetry.instrumentation.api.semconv.http.HttpClientAttributesGetter; import java.util.List; import javax.annotation.Nullable; import software.amazon.awssdk.core.interceptor.ExecutionAttributes; import software.amazon.awssdk.http.SdkHttpRequest; -import software.amazon.awssdk.http.SdkHttpResponse; class AwsSdkHttpAttributesGetter - implements HttpClientAttributesGetter { + implements HttpClientAttributesGetter { @Override public String getUrlFull(ExecutionAttributes request) { @@ -41,14 +40,29 @@ public List getHttpRequestHeader(ExecutionAttributes request, String nam @Override public Integer getHttpResponseStatusCode( - ExecutionAttributes request, SdkHttpResponse response, @Nullable Throwable error) { - return response.statusCode(); + ExecutionAttributes request, Response response, @Nullable Throwable error) { + return response.getSdkHttpResponse().statusCode(); } @Override public List getHttpResponseHeader( - ExecutionAttributes request, SdkHttpResponse response, String name) { - List value = response.headers().get(name); + ExecutionAttributes request, Response response, String name) { + List value = response.getSdkHttpResponse().headers().get(name); return value == null ? emptyList() : value; } + + @Override + @Nullable + public String getServerAddress(ExecutionAttributes request) { + SdkHttpRequest httpRequest = + request.getAttribute(TracingExecutionInterceptor.SDK_HTTP_REQUEST_ATTRIBUTE); + return httpRequest.host(); + } + + @Override + public Integer getServerPort(ExecutionAttributes request) { + SdkHttpRequest httpRequest = + request.getAttribute(TracingExecutionInterceptor.SDK_HTTP_REQUEST_ATTRIBUTE); + return httpRequest.port(); + } } diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AwsSdkHttpClientSuppressionAttributesExtractor.java b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AwsSdkHttpClientSuppressionAttributesExtractor.java new file mode 100644 index 000000000000..2530ad754ea7 --- /dev/null +++ b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AwsSdkHttpClientSuppressionAttributesExtractor.java @@ -0,0 +1,44 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.awssdk.v2_2; + +import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; +import io.opentelemetry.instrumentation.api.internal.SpanKey; +import io.opentelemetry.instrumentation.api.internal.SpanKeyProvider; +import io.opentelemetry.instrumentation.api.semconv.http.HttpClientAttributesExtractor; +import javax.annotation.Nullable; +import software.amazon.awssdk.core.interceptor.ExecutionAttributes; + +/** + * An attribute extractor that reports implementing HTTP client semantic conventions. Adding this + * extractor suppresses nested HTTP client instrumentations similarly to how using {@link + * HttpClientAttributesExtractor} would. + */ +class AwsSdkHttpClientSuppressionAttributesExtractor + implements AttributesExtractor, SpanKeyProvider { + + @Override + public void onStart( + AttributesBuilder attributes, + Context parentContext, + ExecutionAttributes executionAttributes) {} + + @Override + public void onEnd( + AttributesBuilder attributes, + Context context, + ExecutionAttributes executionAttributes, + @Nullable Response response, + @Nullable Throwable error) {} + + @Nullable + @Override + public SpanKey internalGetSpanKey() { + return SpanKey.HTTP_CLIENT; + } +} diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AwsSdkInstrumenterFactory.java b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AwsSdkInstrumenterFactory.java index 584588983057..0b04496debc6 100644 --- a/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AwsSdkInstrumenterFactory.java +++ b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AwsSdkInstrumenterFactory.java @@ -5,80 +5,214 @@ package io.opentelemetry.instrumentation.awssdk.v2_2; +import static java.util.Collections.emptyList; +import static java.util.Collections.singletonList; + import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.propagation.TextMapPropagator; +import io.opentelemetry.instrumentation.api.incubator.semconv.messaging.MessageOperation; +import io.opentelemetry.instrumentation.api.incubator.semconv.messaging.MessagingAttributesExtractor; +import io.opentelemetry.instrumentation.api.incubator.semconv.messaging.MessagingAttributesGetter; +import io.opentelemetry.instrumentation.api.incubator.semconv.messaging.MessagingSpanNameExtractor; +import io.opentelemetry.instrumentation.api.incubator.semconv.rpc.RpcClientAttributesExtractor; 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 io.opentelemetry.instrumentation.api.instrumenter.http.HttpClientAttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.rpc.RpcClientAttributesExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor; +import io.opentelemetry.instrumentation.api.semconv.http.HttpClientAttributesExtractor; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import javax.annotation.Nullable; import software.amazon.awssdk.core.interceptor.ExecutionAttributes; import software.amazon.awssdk.core.interceptor.SdkExecutionAttribute; -import software.amazon.awssdk.http.SdkHttpResponse; final class AwsSdkInstrumenterFactory { private static final String INSTRUMENTATION_NAME = "io.opentelemetry.aws-sdk-2.2"; - static final AttributesExtractor rpcAttributesExtractor = + private static final AttributesExtractor rpcAttributesExtractor = RpcClientAttributesExtractor.create(AwsSdkRpcAttributesGetter.INSTANCE); private static final AwsSdkExperimentalAttributesExtractor experimentalAttributesExtractor = new AwsSdkExperimentalAttributesExtractor(); static final AwsSdkHttpAttributesGetter httpAttributesGetter = new AwsSdkHttpAttributesGetter(); - private static final AwsSdkNetAttributesGetter netAttributesGetter = - new AwsSdkNetAttributesGetter(); - static final AttributesExtractor httpAttributesExtractor = - HttpClientAttributesExtractor.create(httpAttributesGetter, netAttributesGetter); + static final AttributesExtractor httpAttributesExtractor = + HttpClientAttributesExtractor.create(httpAttributesGetter); - private static final AwsSdkSpanKindExtractor spanKindExtractor = new AwsSdkSpanKindExtractor(); + private static final AttributesExtractor + httpClientSuppressionAttributesExtractor = + new AwsSdkHttpClientSuppressionAttributesExtractor(); - private static final List> - defaultAttributesExtractors = Arrays.asList(rpcAttributesExtractor); + private static final List> + defaultAttributesExtractors = + Arrays.asList(rpcAttributesExtractor, httpClientSuppressionAttributesExtractor); - private static final List> + private static final List> extendedAttributesExtractors = - Arrays.asList(rpcAttributesExtractor, experimentalAttributesExtractor); + Arrays.asList( + rpcAttributesExtractor, + experimentalAttributesExtractor, + httpClientSuppressionAttributesExtractor); - private static final List> + private static final List> defaultConsumerAttributesExtractors = Arrays.asList(rpcAttributesExtractor, httpAttributesExtractor); - private static final List> + private static final List> extendedConsumerAttributesExtractors = Arrays.asList( rpcAttributesExtractor, httpAttributesExtractor, experimentalAttributesExtractor); - static Instrumenter requestInstrumenter( - OpenTelemetry openTelemetry, boolean captureExperimentalSpanAttributes) { + private final OpenTelemetry openTelemetry; + @Nullable private final TextMapPropagator messagingPropagator; + private final List capturedHeaders; + private final boolean captureExperimentalSpanAttributes; + private final boolean messagingReceiveInstrumentationEnabled; + private final boolean useXrayPropagator; + + AwsSdkInstrumenterFactory( + OpenTelemetry openTelemetry, + @Nullable TextMapPropagator messagingPropagator, + List capturedHeaders, + boolean captureExperimentalSpanAttributes, + boolean messagingReceiveInstrumentationEnabled, + boolean useXrayPropagator) { + this.openTelemetry = openTelemetry; + this.messagingPropagator = messagingPropagator; + this.capturedHeaders = capturedHeaders; + this.captureExperimentalSpanAttributes = captureExperimentalSpanAttributes; + this.messagingReceiveInstrumentationEnabled = messagingReceiveInstrumentationEnabled; + this.useXrayPropagator = useXrayPropagator; + } + + Instrumenter requestInstrumenter() { + return createInstrumenter( + openTelemetry, + AwsSdkInstrumenterFactory::spanName, + SpanKindExtractor.alwaysClient(), + attributesExtractors(), + emptyList(), + true); + } + + private List> attributesExtractors() { + return captureExperimentalSpanAttributes + ? extendedAttributesExtractors + : defaultAttributesExtractors; + } + + private List> consumerAttributesExtractors() { + return captureExperimentalSpanAttributes + ? extendedConsumerAttributesExtractors + : defaultConsumerAttributesExtractors; + } + + private AttributesExtractor messagingAttributesExtractor( + MessagingAttributesGetter getter, MessageOperation operation) { + return MessagingAttributesExtractor.builder(getter, operation) + .setCapturedHeaders(capturedHeaders) + .build(); + } + + Instrumenter consumerReceiveInstrumenter() { + MessageOperation operation = MessageOperation.RECEIVE; + SqsReceiveRequestAttributesGetter getter = SqsReceiveRequestAttributesGetter.INSTANCE; + AttributesExtractor messagingAttributeExtractor = + messagingAttributesExtractor(getter, operation); return createInstrumenter( openTelemetry, - captureExperimentalSpanAttributes - ? extendedAttributesExtractors - : defaultAttributesExtractors, - AwsSdkInstrumenterFactory.spanKindExtractor); + MessagingSpanNameExtractor.create(getter, operation), + SpanKindExtractor.alwaysConsumer(), + toSqsRequestExtractors(consumerAttributesExtractors()), + singletonList(messagingAttributeExtractor), + messagingReceiveInstrumentationEnabled); } - static Instrumenter consumerInstrumenter( - OpenTelemetry openTelemetry, boolean captureExperimentalSpanAttributes) { + Instrumenter consumerProcessInstrumenter() { + MessageOperation operation = MessageOperation.PROCESS; + SqsProcessRequestAttributesGetter getter = SqsProcessRequestAttributesGetter.INSTANCE; + + InstrumenterBuilder builder = + Instrumenter.builder( + openTelemetry, + INSTRUMENTATION_NAME, + MessagingSpanNameExtractor.create(getter, operation)) + .addAttributesExtractors(toSqsRequestExtractors(consumerAttributesExtractors())) + .addAttributesExtractor(messagingAttributesExtractor(getter, operation)); + + if (messagingReceiveInstrumentationEnabled) { + builder.addSpanLinksExtractor( + (spanLinks, parentContext, request) -> { + Context extracted = + SqsParentContext.ofMessage( + request.getMessage(), messagingPropagator, useXrayPropagator); + spanLinks.addLink(Span.fromContext(extracted).getSpanContext()); + }); + } + return builder.buildInstrumenter(SpanKindExtractor.alwaysConsumer()); + } + + private static List> toSqsRequestExtractors( + List> extractors) { + List> result = new ArrayList<>(); + for (AttributesExtractor extractor : extractors) { + result.add( + new AttributesExtractor() { + @Override + public void onStart( + AttributesBuilder attributes, + Context parentContext, + AbstractSqsRequest sqsRequest) { + extractor.onStart(attributes, parentContext, sqsRequest.getRequest()); + } + + @Override + public void onEnd( + AttributesBuilder attributes, + Context context, + AbstractSqsRequest sqsRequest, + @Nullable Response response, + @Nullable Throwable error) { + extractor.onEnd(attributes, context, sqsRequest.getRequest(), response, error); + } + }); + } + return result; + } + + Instrumenter producerInstrumenter() { + MessageOperation operation = MessageOperation.PUBLISH; + SqsAttributesGetter getter = SqsAttributesGetter.INSTANCE; + AttributesExtractor messagingAttributeExtractor = + messagingAttributesExtractor(getter, operation); return createInstrumenter( openTelemetry, - captureExperimentalSpanAttributes - ? extendedConsumerAttributesExtractors - : defaultConsumerAttributesExtractors, - SpanKindExtractor.alwaysConsumer()); + MessagingSpanNameExtractor.create(getter, operation), + SpanKindExtractor.alwaysProducer(), + attributesExtractors(), + singletonList(messagingAttributeExtractor), + true); } - private static Instrumenter createInstrumenter( + private static Instrumenter createInstrumenter( OpenTelemetry openTelemetry, - List> extractors, - SpanKindExtractor spanKindExtractor) { - - return Instrumenter.builder( - openTelemetry, INSTRUMENTATION_NAME, AwsSdkInstrumenterFactory::spanName) - .addAttributesExtractors(extractors) + SpanNameExtractor spanNameExtractor, + SpanKindExtractor spanKindExtractor, + List> attributeExtractors, + List> additionalAttributeExtractors, + boolean enabled) { + + return Instrumenter.builder( + openTelemetry, INSTRUMENTATION_NAME, spanNameExtractor) + .addAttributesExtractors(attributeExtractors) + .addAttributesExtractors(additionalAttributeExtractors) + .setEnabled(enabled) .buildInstrumenter(spanKindExtractor); } @@ -87,6 +221,4 @@ private static String spanName(ExecutionAttributes attributes) { String awsOperation = attributes.getAttribute(SdkExecutionAttribute.OPERATION_NAME); return awsServiceName + "." + awsOperation; } - - private AwsSdkInstrumenterFactory() {} } diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AwsSdkNetAttributesGetter.java b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AwsSdkNetAttributesGetter.java deleted file mode 100644 index f3e0263a6eb6..000000000000 --- a/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AwsSdkNetAttributesGetter.java +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.awssdk.v2_2; - -import io.opentelemetry.instrumentation.api.instrumenter.net.NetClientAttributesGetter; -import javax.annotation.Nullable; -import software.amazon.awssdk.core.interceptor.ExecutionAttributes; -import software.amazon.awssdk.http.SdkHttpRequest; -import software.amazon.awssdk.http.SdkHttpResponse; - -class AwsSdkNetAttributesGetter - implements NetClientAttributesGetter { - - @Override - @Nullable - public String getServerAddress(ExecutionAttributes request) { - SdkHttpRequest httpRequest = - request.getAttribute(TracingExecutionInterceptor.SDK_HTTP_REQUEST_ATTRIBUTE); - return httpRequest.host(); - } - - @Override - public Integer getServerPort(ExecutionAttributes request) { - SdkHttpRequest httpRequest = - request.getAttribute(TracingExecutionInterceptor.SDK_HTTP_REQUEST_ATTRIBUTE); - return httpRequest.port(); - } -} diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AwsSdkRequest.java b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AwsSdkRequest.java index 54253d0f7bcb..163488c06ac6 100644 --- a/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AwsSdkRequest.java +++ b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AwsSdkRequest.java @@ -8,6 +8,7 @@ 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.SNS; 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; @@ -30,6 +31,7 @@ enum AwsSdkRequest { // generic requests DynamoDbRequest(DYNAMODB, "DynamoDbRequest"), S3Request(S3, "S3Request"), + SnsRequest(SNS, "SnsRequest"), SqsRequest(SQS, "SqsRequest"), KinesisRequest(KINESIS, "KinesisRequest"), // specific requests diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AwsSdkRequestType.java b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AwsSdkRequestType.java index 9062f2aa1738..8c6f4d4f311a 100644 --- a/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AwsSdkRequestType.java +++ b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AwsSdkRequestType.java @@ -7,6 +7,7 @@ import static io.opentelemetry.instrumentation.awssdk.v2_2.FieldMapping.request; +import io.opentelemetry.api.common.AttributeKey; import java.util.Collections; import java.util.List; import java.util.Map; @@ -15,7 +16,13 @@ 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")); + DYNAMODB(request("aws.table.name", "TableName")), + SNS( + /* + * Only one of TopicArn and TargetArn are permitted on an SNS request. + */ + request(AttributeKeys.MESSAGING_DESTINATION_NAME.getKey(), "TargetArn"), + request(AttributeKeys.MESSAGING_DESTINATION_NAME.getKey(), "TopicArn")); // Wrapping in unmodifiableMap @SuppressWarnings("ImmutableEnumChecker") @@ -28,4 +35,10 @@ enum AwsSdkRequestType { List fields(FieldMapping.Type type) { return fields.get(type); } + + private static class AttributeKeys { + // copied from MessagingIncubatingAttributes + static final AttributeKey MESSAGING_DESTINATION_NAME = + AttributeKey.stringKey("messaging.destination.name"); + } } diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AwsSdkRpcAttributesGetter.java b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AwsSdkRpcAttributesGetter.java index 930a607d7bc9..af355b3a9752 100644 --- a/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AwsSdkRpcAttributesGetter.java +++ b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AwsSdkRpcAttributesGetter.java @@ -5,7 +5,7 @@ package io.opentelemetry.instrumentation.awssdk.v2_2; -import io.opentelemetry.instrumentation.api.instrumenter.rpc.RpcAttributesGetter; +import io.opentelemetry.instrumentation.api.incubator.semconv.rpc.RpcAttributesGetter; import software.amazon.awssdk.core.interceptor.ExecutionAttributes; import software.amazon.awssdk.core.interceptor.SdkExecutionAttribute; diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AwsSdkSpanKindExtractor.java b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AwsSdkSpanKindExtractor.java deleted file mode 100644 index d2ec9c12339b..000000000000 --- a/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AwsSdkSpanKindExtractor.java +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.awssdk.v2_2; - -import io.opentelemetry.api.trace.SpanKind; -import io.opentelemetry.instrumentation.api.instrumenter.SpanKindExtractor; -import software.amazon.awssdk.core.SdkRequest; -import software.amazon.awssdk.core.interceptor.ExecutionAttributes; - -class AwsSdkSpanKindExtractor implements SpanKindExtractor { - @Override - public SpanKind extract(ExecutionAttributes request) { - return isSqsProducer(request) ? SpanKind.PRODUCER : SpanKind.CLIENT; - } - - private static boolean isSqsProducer(ExecutionAttributes executionAttributes) { - SdkRequest request = - executionAttributes.getAttribute(TracingExecutionInterceptor.SDK_REQUEST_ATTRIBUTE); - return request - .getClass() - .getName() - .equals("software.amazon.awssdk.services.sqs.model.SendMessageRequest"); - } -} diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AwsSdkTelemetry.java b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AwsSdkTelemetry.java index 166c8ca36bee..0a938e45967b 100644 --- a/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AwsSdkTelemetry.java +++ b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AwsSdkTelemetry.java @@ -8,11 +8,14 @@ import io.opentelemetry.api.OpenTelemetry; import io.opentelemetry.context.propagation.TextMapPropagator; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import io.opentelemetry.javaagent.tooling.muzzle.NoMuzzle; +import java.util.List; import javax.annotation.Nullable; import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration; import software.amazon.awssdk.core.interceptor.ExecutionAttributes; import software.amazon.awssdk.core.interceptor.ExecutionInterceptor; -import software.amazon.awssdk.http.SdkHttpResponse; +import software.amazon.awssdk.services.sqs.SqsAsyncClient; +import software.amazon.awssdk.services.sqs.SqsClient; /** * Entrypoint to OpenTelemetry instrumentation of the AWS SDK. Register the {@link @@ -41,27 +44,42 @@ public static AwsSdkTelemetryBuilder builder(OpenTelemetry openTelemetry) { return new AwsSdkTelemetryBuilder(openTelemetry); } - private final Instrumenter requestInstrumenter; - private final Instrumenter consumerInstrumenter; + private final Instrumenter requestInstrumenter; + private final Instrumenter consumerReceiveInstrumenter; + private final Instrumenter consumerProcessInstrumenter; + private final Instrumenter producerInstrumenter; private final boolean captureExperimentalSpanAttributes; @Nullable private final TextMapPropagator messagingPropagator; private final boolean useXrayPropagator; + private final boolean recordIndividualHttpError; AwsSdkTelemetry( OpenTelemetry openTelemetry, + List capturedHeaders, boolean captureExperimentalSpanAttributes, boolean useMessagingPropagator, - boolean useXrayPropagator) { + boolean useXrayPropagator, + boolean recordIndividualHttpError, + boolean messagingReceiveInstrumentationEnabled) { this.useXrayPropagator = useXrayPropagator; - this.requestInstrumenter = - AwsSdkInstrumenterFactory.requestInstrumenter( - openTelemetry, captureExperimentalSpanAttributes); - this.consumerInstrumenter = - AwsSdkInstrumenterFactory.consumerInstrumenter( - openTelemetry, captureExperimentalSpanAttributes); - this.captureExperimentalSpanAttributes = captureExperimentalSpanAttributes; this.messagingPropagator = useMessagingPropagator ? openTelemetry.getPropagators().getTextMapPropagator() : null; + + AwsSdkInstrumenterFactory instrumenterFactory = + new AwsSdkInstrumenterFactory( + openTelemetry, + messagingPropagator, + capturedHeaders, + captureExperimentalSpanAttributes, + messagingReceiveInstrumentationEnabled, + useXrayPropagator); + + this.requestInstrumenter = instrumenterFactory.requestInstrumenter(); + this.consumerReceiveInstrumenter = instrumenterFactory.consumerReceiveInstrumenter(); + this.consumerProcessInstrumenter = instrumenterFactory.consumerProcessInstrumenter(); + this.producerInstrumenter = instrumenterFactory.producerInstrumenter(); + this.captureExperimentalSpanAttributes = captureExperimentalSpanAttributes; + this.recordIndividualHttpError = recordIndividualHttpError; } /** @@ -71,9 +89,29 @@ public static AwsSdkTelemetryBuilder builder(OpenTelemetry openTelemetry) { public ExecutionInterceptor newExecutionInterceptor() { return new TracingExecutionInterceptor( requestInstrumenter, - consumerInstrumenter, + consumerReceiveInstrumenter, + consumerProcessInstrumenter, + producerInstrumenter, captureExperimentalSpanAttributes, messagingPropagator, - useXrayPropagator); + useXrayPropagator, + recordIndividualHttpError); + } + + /** + * Construct a new tracing-enable {@link SqsClient} using the provided {@link SqsClient} instance. + */ + @NoMuzzle + public SqsClient wrap(SqsClient sqsClient) { + return SqsImpl.wrap(sqsClient); + } + + /** + * Construct a new tracing-enable {@link SqsAsyncClient} using the provided {@link SqsAsyncClient} + * instance. + */ + @NoMuzzle + public SqsAsyncClient wrap(SqsAsyncClient sqsClient) { + return SqsImpl.wrap(sqsClient); } } diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AwsSdkTelemetryBuilder.java b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AwsSdkTelemetryBuilder.java index 0418d950223a..11ddca0ea559 100644 --- a/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AwsSdkTelemetryBuilder.java +++ b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AwsSdkTelemetryBuilder.java @@ -5,24 +5,39 @@ package io.opentelemetry.instrumentation.awssdk.v2_2; +import static java.util.Collections.emptyList; + import com.google.errorprone.annotations.CanIgnoreReturnValue; import io.opentelemetry.api.OpenTelemetry; +import java.util.List; /** A builder of {@link AwsSdkTelemetry}. */ public final class AwsSdkTelemetryBuilder { private final OpenTelemetry openTelemetry; + private List capturedHeaders = emptyList(); private boolean captureExperimentalSpanAttributes; - private boolean useMessagingPropagator; - + private boolean recordIndividualHttpError; private boolean useXrayPropagator = true; + private boolean messagingReceiveInstrumentationEnabled; AwsSdkTelemetryBuilder(OpenTelemetry openTelemetry) { this.openTelemetry = openTelemetry; } + /** + * Configures the messaging headers that will be captured as span attributes. + * + * @param capturedHeaders A list of messaging header names. + */ + @CanIgnoreReturnValue + public AwsSdkTelemetryBuilder setCapturedHeaders(List capturedHeaders) { + this.capturedHeaders = capturedHeaders; + 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 @@ -57,6 +72,20 @@ public AwsSdkTelemetryBuilder setUseConfiguredPropagatorForMessaging( return this; } + /** + * Sets whether errors returned by each individual HTTP request should be recorded as events for + * the SDK span. + * + *

This option is off by default. If enabled, the HTTP error code and the error message will be + * captured and associated with the span. This provides detailed insights into errors on a + * per-request basis. + */ + @CanIgnoreReturnValue + public AwsSdkTelemetryBuilder setRecordIndividualHttpError(boolean recordIndividualHttpError) { + this.recordIndividualHttpError = recordIndividualHttpError; + return this; + } + /** * This setter implemented package-private for testing the messaging propagator, it does not seem * too useful in general. The option is on by default. @@ -71,14 +100,30 @@ AwsSdkTelemetryBuilder setUseXrayPropagator(boolean useMessagingPropagator) { return this; } + /** + * Set whether to capture the consumer message receive telemetry in messaging instrumentation. + * + *

Note that this will cause the consumer side to start a new trace, with only a span link + * connecting it to the producer trace. + */ + @CanIgnoreReturnValue + public AwsSdkTelemetryBuilder setMessagingReceiveInstrumentationEnabled( + boolean messagingReceiveInstrumentationEnabled) { + this.messagingReceiveInstrumentationEnabled = messagingReceiveInstrumentationEnabled; + return this; + } + /** * Returns a new {@link AwsSdkTelemetry} with the settings of this {@link AwsSdkTelemetryBuilder}. */ public AwsSdkTelemetry build() { return new AwsSdkTelemetry( openTelemetry, + capturedHeaders, captureExperimentalSpanAttributes, useMessagingPropagator, - useXrayPropagator); + useXrayPropagator, + recordIndividualHttpError, + messagingReceiveInstrumentationEnabled); } } diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/LambdaAccess.java b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/LambdaAccess.java new file mode 100644 index 000000000000..33d792c0c826 --- /dev/null +++ b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/LambdaAccess.java @@ -0,0 +1,21 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.awssdk.v2_2; + +import io.opentelemetry.context.Context; +import io.opentelemetry.javaagent.tooling.muzzle.NoMuzzle; +import software.amazon.awssdk.core.SdkRequest; + +final class LambdaAccess { + private LambdaAccess() {} + + private static final boolean enabled = PluginImplUtil.isImplPresent("LambdaImpl"); + + @NoMuzzle + public static SdkRequest modifyRequest(SdkRequest request, Context otelContext) { + return enabled ? LambdaImpl.modifyRequest(request, otelContext) : null; + } +} diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/LambdaImpl.java b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/LambdaImpl.java new file mode 100644 index 000000000000..fae5edf5e12a --- /dev/null +++ b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/LambdaImpl.java @@ -0,0 +1,102 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.awssdk.v2_2; + +import io.opentelemetry.api.GlobalOpenTelemetry; +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.LinkedHashMap; +import java.util.Map; +import javax.annotation.Nullable; +import software.amazon.awssdk.core.SdkRequest; +import software.amazon.awssdk.protocols.jsoncore.JsonNode; +import software.amazon.awssdk.protocols.jsoncore.internal.ObjectJsonNode; +import software.amazon.awssdk.protocols.jsoncore.internal.StringJsonNode; +import software.amazon.awssdk.services.lambda.model.InvokeRequest; + +// this class is only used from LambdaAccess from method with @NoMuzzle annotation + +// Direct lambda invocations (e.g., not through an api gateway) currently strip +// away the otel propagation headers (but leave x-ray ones intact). Use the +// custom client context header as an additional propagation mechanism for this +// very specific scenario. For reference, the header is named "X-Amz-Client-Context" but the api to +// manipulate it abstracts that away. The client context field is documented in +// https://docs.aws.amazon.com/lambda/latest/api/API_Invoke.html#API_Invoke_RequestParameters + +final class LambdaImpl { + static { + // Force loading of InvokeRequest; this ensures that an exception is thrown at this point when + // the Lambda library is not present, which will cause DirectLambdaAccess to have + // enabled=false in library mode. + @SuppressWarnings("unused") + String invokeRequestName = InvokeRequest.class.getName(); + // was added in 2.17.0 + @SuppressWarnings("unused") + String jsonNodeName = JsonNode.class.getName(); + } + + private static final String CLIENT_CONTEXT_CUSTOM_FIELDS_KEY = "custom"; + static final int MAX_CLIENT_CONTEXT_LENGTH = 3583; // visible for testing + + private LambdaImpl() {} + + @Nullable + static SdkRequest modifyRequest( + SdkRequest request, io.opentelemetry.context.Context otelContext) { + if (isDirectLambdaInvocation(request)) { + return modifyOrAddCustomContextHeader((InvokeRequest) request, otelContext); + } + return null; + } + + static boolean isDirectLambdaInvocation(SdkRequest request) { + return request instanceof InvokeRequest; + } + + static SdkRequest modifyOrAddCustomContextHeader( + InvokeRequest request, io.opentelemetry.context.Context otelContext) { + InvokeRequest.Builder builder = request.toBuilder(); + // Unfortunately the value of this thing is a base64-encoded json with a character limit; also + // therefore not comma-composable like many http headers + String clientContextString = request.clientContext(); + String clientContextJsonString = "{}"; + if (clientContextString != null && !clientContextString.isEmpty()) { + clientContextJsonString = + new String(Base64.getDecoder().decode(clientContextString), StandardCharsets.UTF_8); + } + JsonNode jsonNode = JsonNode.parser().parse(clientContextJsonString); + if (!jsonNode.isObject()) { + return null; + } + JsonNode customNode = + jsonNode + .asObject() + .computeIfAbsent( + CLIENT_CONTEXT_CUSTOM_FIELDS_KEY, (k) -> new ObjectJsonNode(new LinkedHashMap<>())); + if (!customNode.isObject()) { + return null; + } + Map map = customNode.asObject(); + GlobalOpenTelemetry.getPropagators() + .getTextMapPropagator() + .inject(otelContext, map, (nodes, key, value) -> nodes.put(key, new StringJsonNode(value))); + if (map.isEmpty()) { + return null; + } + + // turn it back into a string (json encode) + String newJson = jsonNode.toString(); + + // turn it back into a base64 string + String newJson64 = Base64.getEncoder().encodeToString(newJson.getBytes(StandardCharsets.UTF_8)); + // check it for length (err on the safe side with >=) + if (newJson64.length() >= MAX_CLIENT_CONTEXT_LENGTH) { + return null; + } + builder.clientContext(newJson64); + return builder.build(); + } +} diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/PluginImplUtil.java b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/PluginImplUtil.java new file mode 100644 index 000000000000..e77e52129082 --- /dev/null +++ b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/PluginImplUtil.java @@ -0,0 +1,58 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.awssdk.v2_2; + +import java.util.logging.Level; +import java.util.logging.Logger; + +final class PluginImplUtil { // TODO: Copy & pasted to v1 + private PluginImplUtil() {} + + private static final Logger logger = Logger.getLogger(PluginImplUtil.class.getName()); + + /** + * Check if the given {@code moduleNameImpl} is present. + * + *

For library instrumentations, the Impls will always be available but might fail to + * load/link/initialize if the corresponding SDK classes are not on the class path. For javaagent, + * the Impl is available only when the corresponding InstrumentationModule was successfully + * applied (muzzle passed). + * + *

Note that an present-but-incompatible library can only be reliably detected by Muzzle. In + * library-mode, users need to ensure they are using a compatible SDK (component) versions + * themselves. + * + * @param implSimpleClassName The simple name of the impl class, e.g. {@code "SqsImpl"}. * + */ + static boolean isImplPresent(String implSimpleClassName) { + // Computing the full name dynamically name here because library instrumentation classes are + // relocated when embedded in the agent. + // We use getName().replace() instead of getPackage() because the latter is not guaranteed to + // work in all cases (e.g., we might be loaded into a custom classloader that doesn't handle it) + String implFullClassName = + PluginImplUtil.class.getName().replace(".PluginImplUtil", "." + implSimpleClassName); + try { + Class.forName(implFullClassName); + return true; + } catch (ClassNotFoundException | LinkageError e) { + // ClassNotFoundException will happen when muzzle disabled us in javaagent mode; LinkageError + // (most likely a NoClassDefFoundError, potentially wrapped in an ExceptionInInitializerError) + // should be thrown when the class is loaded in library mode (where the Impl class itself can + // always be found) but a dependency failed to load (most likely because the corresponding SDK + // dependency is not on the class path). + logger.log( + Level.FINE, + () -> + "Failed to load " + + implFullClassName + + " (" + + e.getClass().getName() + + "). " + + "Most likely, corresponding SDK component is either not on classpath or incompatible."); + return false; + } + } +} diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/RequestHeaderSetter.java b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/RequestHeaderSetter.java index f61b7f14820e..aab3b4d3f45d 100644 --- a/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/RequestHeaderSetter.java +++ b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/RequestHeaderSetter.java @@ -13,6 +13,6 @@ enum RequestHeaderSetter implements TextMapSetter { @Override public void set(SdkHttpRequest.Builder builder, String name, String value) { - builder.appendHeader(name, value); + builder.putHeader(name, value); } } diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/Response.java b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/Response.java new file mode 100644 index 000000000000..ca6c37c6247c --- /dev/null +++ b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/Response.java @@ -0,0 +1,31 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.awssdk.v2_2; + +import software.amazon.awssdk.core.SdkResponse; +import software.amazon.awssdk.http.SdkHttpResponse; + +final class Response { + private final SdkHttpResponse sdkHttpResponse; + private final SdkResponse sdkResponse; + + Response(SdkHttpResponse sdkHttpResponse) { + this(sdkHttpResponse, null); + } + + Response(SdkHttpResponse sdkHttpResponse, SdkResponse sdkResponse) { + this.sdkHttpResponse = sdkHttpResponse; + this.sdkResponse = sdkResponse; + } + + public SdkHttpResponse getSdkHttpResponse() { + return sdkHttpResponse; + } + + public SdkResponse getSdkResponse() { + return sdkResponse; + } +} diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/SnsAccess.java b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/SnsAccess.java new file mode 100644 index 000000000000..7c9cd1bc596d --- /dev/null +++ b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/SnsAccess.java @@ -0,0 +1,23 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.awssdk.v2_2; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.propagation.TextMapPropagator; +import io.opentelemetry.javaagent.tooling.muzzle.NoMuzzle; +import software.amazon.awssdk.core.SdkRequest; + +final class SnsAccess { + private SnsAccess() {} + + private static final boolean enabled = PluginImplUtil.isImplPresent("SnsImpl"); + + @NoMuzzle + public static SdkRequest modifyRequest( + SdkRequest request, Context otelContext, TextMapPropagator messagingPropagator) { + return enabled ? SnsImpl.modifyRequest(request, otelContext, messagingPropagator) : null; + } +} diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/SnsImpl.java b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/SnsImpl.java new file mode 100644 index 000000000000..464b50922c5c --- /dev/null +++ b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/SnsImpl.java @@ -0,0 +1,75 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.awssdk.v2_2; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.propagation.TextMapPropagator; +import java.util.HashMap; +import java.util.Map; +import software.amazon.awssdk.core.SdkRequest; +import software.amazon.awssdk.services.sns.SnsClient; +import software.amazon.awssdk.services.sns.model.MessageAttributeValue; +import software.amazon.awssdk.services.sns.model.PublishRequest; + +// this class is only used from SnsAccess from method with @NoMuzzle annotation +class SnsImpl { + static { + // Force loading of SnsClient; this ensures that an exception is thrown at this point when the + // SNS library is not present, which will cause SnsAccess to have enabled=false in library mode. + @SuppressWarnings("unused") + String ensureLoadedDummy = SnsClient.class.getName(); + } + + private SnsImpl() {} + + static SdkRequest modifyRequest( + SdkRequest request, Context otelContext, TextMapPropagator messagingPropagator) { + if (messagingPropagator == null) { + return null; + } else if (request instanceof PublishRequest) { + return injectIntoPublishRequest((PublishRequest) request, otelContext, messagingPropagator); + } else { + // NB: We do not support PublishBatchRequest which was only introduced in 2.17.84. + // To add support, some targeted use of @NoMuzzle + checks that the needed class + // is available should work. See + // https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/8830#discussion_r1247570985 + return null; + } + } + + private static SdkRequest injectIntoPublishRequest( + PublishRequest request, Context otelContext, TextMapPropagator messagingPropagator) { + // Note: Code is 1:1 copy & paste from SQS, but due to different types (packages) cannot be + // reused. + Map messageAttributes = + new HashMap<>(request.messageAttributes()); + if (!injectIntoMessageAttributes(messageAttributes, otelContext, messagingPropagator)) { + return request; + } + return request.toBuilder().messageAttributes(messageAttributes).build(); + } + + private static boolean injectIntoMessageAttributes( + Map messageAttributes, + io.opentelemetry.context.Context otelContext, + TextMapPropagator messagingPropagator) { + // Note: Code is 1:1 copy & paste from SQS, but due to different types (packages) cannot be + // reused. + messagingPropagator.inject( + otelContext, + messageAttributes, + (carrier, k, v) -> { + carrier.put(k, MessageAttributeValue.builder().stringValue(v).dataType("String").build()); + }); + + // Return whether the injection resulted in an attribute count that is still supported. + // See https://docs.aws.amazon.com/sns/latest/dg/sns-message-attributes.html + // While non-raw delivery would support an arbitrary number, that is something configured in + // the subscription, and adding more attributes might result in odd behavior (e.g. we might + // push out other attributes) + return messageAttributes.size() <= 10; + } +} diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/SqsAccess.java b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/SqsAccess.java index 6efff826aa29..29baec5c6bf1 100644 --- a/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/SqsAccess.java +++ b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/SqsAccess.java @@ -6,51 +6,64 @@ package io.opentelemetry.instrumentation.awssdk.v2_2; import io.opentelemetry.context.propagation.TextMapPropagator; +import io.opentelemetry.instrumentation.api.internal.Timer; import io.opentelemetry.javaagent.tooling.muzzle.NoMuzzle; +import javax.annotation.Nullable; 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.ExecutionAttributes; // helper class for calling methods that use sqs types in SqsImpl // if SqsImpl is not present these methods are no op final class SqsAccess { - private static final boolean enabled = isSqsImplPresent(); - - private static boolean isSqsImplPresent() { - try { - // for library instrumentation SqsImpl is always available - // for javaagent instrumentation SqsImpl is available only when SqsInstrumentationModule was - // successfully applied (muzzle passed) - // using package name here because library instrumentation classes are relocated when embedded - // in the agent - Class.forName(SqsAccess.class.getName().replace(".SqsAccess", ".SqsImpl")); - return true; - } catch (ClassNotFoundException e) { - return false; - } + private SqsAccess() {} + + private static final boolean enabled = PluginImplUtil.isImplPresent("SqsImpl"); + + @NoMuzzle + static boolean afterReceiveMessageExecution( + Context.AfterExecution context, + ExecutionAttributes executionAttributes, + TracingExecutionInterceptor config, + Timer timer) { + return enabled + && SqsImpl.afterReceiveMessageExecution(context, executionAttributes, config, timer); } + /** + * Returns {@code null} (not the unmodified {@code request}!) if nothing matched, so that other + * handling can be tried. + */ + @Nullable @NoMuzzle - static SdkRequest injectIntoSqsSendMessageRequest( - TextMapPropagator messagingPropagator, - SdkRequest rawRequest, - io.opentelemetry.context.Context otelContext) { - if (!enabled) { - return rawRequest; - } - return SqsImpl.injectIntoSqsSendMessageRequest(messagingPropagator, rawRequest, otelContext); + static SdkRequest modifyRequest( + SdkRequest request, + io.opentelemetry.context.Context otelContext, + boolean useXrayPropagator, + TextMapPropagator messagingPropagator) { + return enabled + ? SqsImpl.modifyRequest(request, otelContext, useXrayPropagator, messagingPropagator) + : null; } @NoMuzzle - static void afterReceiveMessageExecution( - TracingExecutionInterceptor config, - Context.AfterExecution context, - ExecutionAttributes executionAttributes) { - if (!enabled) { - return; - } - SqsImpl.afterConsumerResponse(config, executionAttributes, context); + static boolean isSqsProducerRequest(SdkRequest request) { + return enabled && SqsImpl.isSqsProducerRequest(request); } - private SqsAccess() {} + @NoMuzzle + static String getQueueUrl(SdkRequest request) { + return enabled ? SqsImpl.getQueueUrl(request) : null; + } + + @NoMuzzle + static String getMessageAttribute(SdkRequest request, String name) { + return enabled ? SqsImpl.getMessageAttribute(request, name) : null; + } + + @NoMuzzle + static String getMessageId(SdkResponse response) { + return enabled ? SqsImpl.getMessageId(response) : null; + } } diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/SqsAttributesGetter.java b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/SqsAttributesGetter.java new file mode 100644 index 000000000000..1d899029d90e --- /dev/null +++ b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/SqsAttributesGetter.java @@ -0,0 +1,102 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.awssdk.v2_2; + +import io.opentelemetry.instrumentation.api.incubator.semconv.messaging.MessagingAttributesGetter; +import java.util.Collections; +import java.util.List; +import javax.annotation.Nullable; +import software.amazon.awssdk.core.SdkRequest; +import software.amazon.awssdk.core.SdkResponse; +import software.amazon.awssdk.core.interceptor.ExecutionAttributes; + +enum SqsAttributesGetter implements MessagingAttributesGetter { + INSTANCE; + + // copied from MessagingIncubatingAttributes.MessagingSystemValues + private static final String AWS_SQS = "aws_sqs"; + + @Override + public String getSystem(ExecutionAttributes request) { + return AWS_SQS; + } + + @Override + public String getDestination(ExecutionAttributes request) { + SdkRequest sdkRequest = request.getAttribute(TracingExecutionInterceptor.SDK_REQUEST_ATTRIBUTE); + String queueUrl = SqsAccess.getQueueUrl(sdkRequest); + if (queueUrl != null) { + int i = queueUrl.lastIndexOf('/'); + if (i > 0) { + return queueUrl.substring(i + 1); + } + } + return null; + } + + @Nullable + @Override + public String getDestinationTemplate(ExecutionAttributes request) { + return null; + } + + @Override + public boolean isTemporaryDestination(ExecutionAttributes request) { + return false; + } + + @Override + public boolean isAnonymousDestination(ExecutionAttributes request) { + return false; + } + + @Override + @Nullable + public String getConversationId(ExecutionAttributes request) { + return null; + } + + @Nullable + @Override + public Long getMessageBodySize(ExecutionAttributes request) { + return null; + } + + @Nullable + @Override + public Long getMessageEnvelopeSize(ExecutionAttributes request) { + return null; + } + + @Override + @Nullable + public String getMessageId(ExecutionAttributes request, @Nullable Response response) { + if (response != null && response.getSdkResponse() != null) { + SdkResponse sdkResponse = response.getSdkResponse(); + return SqsAccess.getMessageId(sdkResponse); + } + return null; + } + + @Nullable + @Override + public String getClientId(ExecutionAttributes request) { + return null; + } + + @Nullable + @Override + public Long getBatchMessageCount(ExecutionAttributes request, @Nullable Response response) { + return null; + } + + @Override + public List getMessageHeader(ExecutionAttributes request, String name) { + SdkRequest sdkRequest = request.getAttribute(TracingExecutionInterceptor.SDK_REQUEST_ATTRIBUTE); + String value = SqsAccess.getMessageAttribute(sdkRequest, name); + return value != null ? Collections.singletonList(value) : Collections.emptyList(); + } +} diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/SqsImpl.java b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/SqsImpl.java index 99c1f155450e..bca3a6c49c69 100644 --- a/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/SqsImpl.java +++ b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/SqsImpl.java @@ -5,35 +5,192 @@ package io.opentelemetry.instrumentation.awssdk.v2_2; +import static io.opentelemetry.instrumentation.awssdk.v2_2.TracingExecutionInterceptor.SDK_HTTP_REQUEST_ATTRIBUTE; +import static io.opentelemetry.instrumentation.awssdk.v2_2.TracingExecutionInterceptor.SDK_REQUEST_ATTRIBUTE; + +import io.opentelemetry.context.Scope; import io.opentelemetry.context.propagation.TextMapPropagator; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import io.opentelemetry.instrumentation.api.internal.InstrumenterUtil; +import io.opentelemetry.instrumentation.api.internal.Timer; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Map; +import java.util.concurrent.CompletableFuture; +import javax.annotation.Nullable; 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.ExecutionAttributes; -import software.amazon.awssdk.http.SdkHttpResponse; +import software.amazon.awssdk.core.interceptor.SdkExecutionAttribute; +import software.amazon.awssdk.services.sqs.SqsAsyncClient; +import software.amazon.awssdk.services.sqs.SqsClient; import software.amazon.awssdk.services.sqs.model.Message; import software.amazon.awssdk.services.sqs.model.MessageAttributeValue; +import software.amazon.awssdk.services.sqs.model.ReceiveMessageRequest; import software.amazon.awssdk.services.sqs.model.ReceiveMessageResponse; +import software.amazon.awssdk.services.sqs.model.SendMessageBatchRequest; +import software.amazon.awssdk.services.sqs.model.SendMessageBatchRequestEntry; import software.amazon.awssdk.services.sqs.model.SendMessageRequest; +import software.amazon.awssdk.services.sqs.model.SendMessageResponse; // this class is only used from SqsAccess from method with @NoMuzzle annotation final class SqsImpl { + static { + // Force loading of SqsClient; this ensures that an exception is thrown at this point when the + // SQS library is not present, which will cause SqsAccess to have enabled=false in library mode. + @SuppressWarnings("unused") + String ensureLoadedDummy = SqsClient.class.getName(); + } + private SqsImpl() {} - public static void init() { - // called from advice + static boolean afterReceiveMessageExecution( + Context.AfterExecution context, + ExecutionAttributes executionAttributes, + TracingExecutionInterceptor config, + Timer timer) { + + SdkResponse rawResponse = context.response(); + if (!(rawResponse instanceof ReceiveMessageResponse)) { + return false; + } + + ReceiveMessageResponse response = (ReceiveMessageResponse) rawResponse; + if (response.messages().isEmpty()) { + return false; + } + + io.opentelemetry.context.Context parentContext = + TracingExecutionInterceptor.getParentContext(executionAttributes); + Instrumenter consumerReceiveInstrumenter = + config.getConsumerReceiveInstrumenter(); + io.opentelemetry.context.Context receiveContext = null; + SqsReceiveRequest receiveRequest = + SqsReceiveRequest.create(executionAttributes, SqsMessageImpl.wrap(response.messages())); + if (timer != null && consumerReceiveInstrumenter.shouldStart(parentContext, receiveRequest)) { + receiveContext = + InstrumenterUtil.startAndEnd( + consumerReceiveInstrumenter, + parentContext, + receiveRequest, + new Response(context.httpResponse(), response), + null, + timer.startTime(), + timer.now()); + } + // copy ExecutionAttributes as these will get cleared before the process spans are created + ExecutionAttributes copy = new ExecutionAttributes(); + copy.putAttribute( + SDK_HTTP_REQUEST_ATTRIBUTE, executionAttributes.getAttribute(SDK_HTTP_REQUEST_ATTRIBUTE)); + copy.putAttribute( + SDK_REQUEST_ATTRIBUTE, executionAttributes.getAttribute(SDK_REQUEST_ATTRIBUTE)); + copy.putAttribute( + SdkExecutionAttribute.SERVICE_NAME, + executionAttributes.getAttribute(SdkExecutionAttribute.SERVICE_NAME)); + copy.putAttribute( + SdkExecutionAttribute.OPERATION_NAME, + executionAttributes.getAttribute(SdkExecutionAttribute.OPERATION_NAME)); + + TracingList tracingList = + TracingList.wrap( + response.messages(), + config.getConsumerProcessInstrumenter(), + copy, + new Response(context.httpResponse(), response), + config, + receiveContext); + + // store tracing list in context so that our proxied SqsClient/SqsAsyncClient could pick it up + SqsTracingContext.set(parentContext, tracingList); + + return true; } - static SdkRequest injectIntoSqsSendMessageRequest( - TextMapPropagator messagingPropagator, - SdkRequest rawRequest, - io.opentelemetry.context.Context otelContext) { - SendMessageRequest request = (SendMessageRequest) rawRequest; + private static final Field messagesField = getMessagesField(); + + private static Field getMessagesField() { + try { + Field field = ReceiveMessageResponse.class.getDeclaredField("messages"); + field.setAccessible(true); + return field; + } catch (Exception e) { + return null; + } + } + + public static void setMessages( + ReceiveMessageResponse receiveMessageResponse, List messages) { + try { + messagesField.set(receiveMessageResponse, messages); + } catch (IllegalAccessException ignored) { + // should not happen, we call setAccessible on the field + } + } + + @Nullable + static SdkRequest modifyRequest( + SdkRequest request, + io.opentelemetry.context.Context otelContext, + boolean useXrayPropagator, + TextMapPropagator messagingPropagator) { + if (request instanceof ReceiveMessageRequest) { + return modifyReceiveMessageRequest( + (ReceiveMessageRequest) request, useXrayPropagator, messagingPropagator); + } else if (messagingPropagator != null) { + if (request instanceof SendMessageRequest) { + return injectIntoSendMessageRequest( + (SendMessageRequest) request, otelContext, messagingPropagator); + } else if (request instanceof SendMessageBatchRequest) { + return injectIntoSendMessageBatchRequest( + (SendMessageBatchRequest) request, otelContext, messagingPropagator); + } + } + return null; + } + + private static SdkRequest injectIntoSendMessageBatchRequest( + SendMessageBatchRequest request, + io.opentelemetry.context.Context otelContext, + TextMapPropagator messagingPropagator) { + ArrayList entries = new ArrayList<>(request.entries()); + for (int i = 0; i < entries.size(); ++i) { + SendMessageBatchRequestEntry entry = entries.get(i); + Map messageAttributes = + new HashMap<>(entry.messageAttributes()); + + // TODO: Per https://github.com/open-telemetry/oteps/pull/220, each message should get + // a separate context. We don't support this yet, also because it would be inconsistent + // with the header-based X-Ray propagation + // (probably could override it here by setting the X-Ray message system attribute) + if (injectIntoMessageAttributes(messageAttributes, otelContext, messagingPropagator)) { + entries.set(i, entry.toBuilder().messageAttributes(messageAttributes).build()); + } + } + return request.toBuilder().entries(entries).build(); + } + + private static SdkRequest injectIntoSendMessageRequest( + SendMessageRequest request, + io.opentelemetry.context.Context otelContext, + TextMapPropagator messagingPropagator) { Map messageAttributes = new HashMap<>(request.messageAttributes()); + if (!injectIntoMessageAttributes(messageAttributes, otelContext, messagingPropagator)) { + return request; + } + return request.toBuilder().messageAttributes(messageAttributes).build(); + } + private static boolean injectIntoMessageAttributes( + Map messageAttributes, + io.opentelemetry.context.Context otelContext, + TextMapPropagator messagingPropagator) { messagingPropagator.inject( otelContext, messageAttributes, @@ -41,54 +198,161 @@ static SdkRequest injectIntoSqsSendMessageRequest( carrier.put(k, MessageAttributeValue.builder().stringValue(v).dataType("String").build()); }); - if (messageAttributes.size() > 10) { // Too many attributes, we don't want to break the call. + // Return whether the injection resulted in an attribute count that is still supported. + // See + // https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-message-metadata.html#sqs-message-attributes + return messageAttributes.size() <= 10; + } + + private static SdkRequest modifyReceiveMessageRequest( + ReceiveMessageRequest request, + boolean useXrayPropagator, + TextMapPropagator messagingPropagator) { + boolean hasXrayAttribute = true; + List existingAttributeNames = null; + if (useXrayPropagator) { + existingAttributeNames = request.attributeNamesAsStrings(); + hasXrayAttribute = + existingAttributeNames.contains(SqsParentContext.AWS_TRACE_SYSTEM_ATTRIBUTE); + } + + boolean hasMessageAttribute = true; + List existingMessageAttributeNames = null; + if (messagingPropagator != null) { + existingMessageAttributeNames = request.messageAttributeNames(); + hasMessageAttribute = existingMessageAttributeNames.containsAll(messagingPropagator.fields()); + } + + if (hasMessageAttribute && hasXrayAttribute) { return request; } - return request.toBuilder().messageAttributes(messageAttributes).build(); - } - /** Create and close CONSUMER span for each message consumed. */ - static void afterConsumerResponse( - TracingExecutionInterceptor config, - ExecutionAttributes executionAttributes, - Context.AfterExecution context) { - ReceiveMessageResponse response = (ReceiveMessageResponse) context.response(); - SdkHttpResponse httpResponse = context.httpResponse(); - for (Message message : response.messages()) { - createConsumerSpan(config, message, executionAttributes, httpResponse); + ReceiveMessageRequest.Builder builder = request.toBuilder(); + if (!hasXrayAttribute) { + List attributeNames = new ArrayList<>(existingAttributeNames); + attributeNames.add(SqsParentContext.AWS_TRACE_SYSTEM_ATTRIBUTE); + builder.attributeNamesWithStrings(attributeNames); } + if (messagingPropagator != null) { + List messageAttributeNames = new ArrayList<>(existingMessageAttributeNames); + for (String field : messagingPropagator.fields()) { + if (!existingMessageAttributeNames.contains(field)) { + messageAttributeNames.add(field); + } + } + builder.messageAttributeNames(messageAttributeNames); + } + return builder.build(); } - private static void createConsumerSpan( - TracingExecutionInterceptor config, - Message message, - ExecutionAttributes executionAttributes, - SdkHttpResponse httpResponse) { + static boolean isSqsProducerRequest(SdkRequest request) { + return request instanceof SendMessageRequest || request instanceof SendMessageBatchRequest; + } - io.opentelemetry.context.Context parentContext = io.opentelemetry.context.Context.root(); + static String getQueueUrl(SdkRequest request) { + if (request instanceof SendMessageRequest) { + return ((SendMessageRequest) request).queueUrl(); + } else if (request instanceof SendMessageBatchRequest) { + return ((SendMessageBatchRequest) request).queueUrl(); + } else if (request instanceof ReceiveMessageRequest) { + return ((ReceiveMessageRequest) request).queueUrl(); + } + return null; + } - TextMapPropagator messagingPropagator = config.getMessagingPropagator(); - if (messagingPropagator != null) { - parentContext = - SqsParentContext.ofMessageAttributes(message.messageAttributes(), messagingPropagator); + static String getMessageAttribute(SdkRequest request, String name) { + if (request instanceof SendMessageRequest) { + MessageAttributeValue value = ((SendMessageRequest) request).messageAttributes().get(name); + return value != null ? value.stringValue() : null; } + return null; + } - if (config.shouldUseXrayPropagator() - && parentContext == io.opentelemetry.context.Context.root()) { - parentContext = SqsParentContext.ofSystemAttributes(message.attributesAsStrings()); + static String getMessageId(SdkResponse response) { + if (response instanceof SendMessageResponse) { + return ((SendMessageResponse) response).messageId(); } + return null; + } - Instrumenter consumerInstrumenter = - config.getConsumerInstrumenter(); - if (consumerInstrumenter.shouldStart(parentContext, executionAttributes)) { - io.opentelemetry.context.Context context = - consumerInstrumenter.start(parentContext, executionAttributes); + static SqsClient wrap(SqsClient sqsClient) { + // proxy SqsClient so we could replace the messages list in ReceiveMessageResponse returned from + // receiveMessage call + return (SqsClient) + Proxy.newProxyInstance( + sqsClient.getClass().getClassLoader(), + new Class[] {SqsClient.class}, + (proxy, method, args) -> { + if ("receiveMessage".equals(method.getName())) { + SqsTracingContext sqsTracingContext = new SqsTracingContext(); + try (Scope ignored = + io.opentelemetry.context.Context.current() + .with(sqsTracingContext) + .makeCurrent()) { + Object result = invokeProxyMethod(method, sqsClient, args); + TracingList tracingList = sqsTracingContext.get(); + if (tracingList != null) { + ReceiveMessageResponse response = (ReceiveMessageResponse) result; + SqsImpl.setMessages(response, tracingList); + return response; + } + return result; + } + } else { + return invokeProxyMethod(method, sqsClient, args); + } + }); + } + + @SuppressWarnings("unchecked") + static SqsAsyncClient wrap(SqsAsyncClient sqsClient) { + // proxy SqsAsyncClient so we could replace the messages list in ReceiveMessageResponse returned + // from receiveMessage call + return (SqsAsyncClient) + Proxy.newProxyInstance( + sqsClient.getClass().getClassLoader(), + new Class[] {SqsAsyncClient.class}, + (proxy, method, args) -> { + if ("receiveMessage".equals(method.getName())) { + SqsTracingContext sqsTracingContext = new SqsTracingContext(); + try (Scope ignored = + io.opentelemetry.context.Context.current() + .with(sqsTracingContext) + .makeCurrent()) { + Object result = invokeProxyMethod(method, sqsClient, args); + CompletableFuture originalFuture = + (CompletableFuture) result; + CompletableFuture resultFuture = + new CompletableFuture<>(); + originalFuture.whenComplete( + (response, throwable) -> { + if (throwable != null) { + resultFuture.completeExceptionally(throwable); + } else { + TracingList tracingList = sqsTracingContext.get(); + if (tracingList != null) { + SqsImpl.setMessages(response, tracingList); + } + resultFuture.complete(response); + } + }); + + return resultFuture; + } catch (InvocationTargetException exception) { + throw exception.getCause(); + } + } else { + return invokeProxyMethod(method, sqsClient, args); + } + }); + } - // TODO: Even if we keep HTTP attributes (see afterMarshalling), does it make sense here - // per-message? - // TODO: Should we really create root spans if we can't extract anything, or should we attach - // to the current context? - consumerInstrumenter.end(context, executionAttributes, httpResponse, null); + private static Object invokeProxyMethod(Method method, Object target, Object[] args) + throws Throwable { + try { + return method.invoke(target, args); + } catch (InvocationTargetException exception) { + throw exception.getCause(); } } } diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/SqsMessage.java b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/SqsMessage.java new file mode 100644 index 000000000000..966f76ec7732 --- /dev/null +++ b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/SqsMessage.java @@ -0,0 +1,24 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.awssdk.v2_2; + +import java.util.Map; +import software.amazon.awssdk.services.sqs.model.MessageAttributeValue; + +/** + * A wrapper interface for {@link software.amazon.awssdk.services.sqs.model.Message}. Using this + * wrapper avoids muzzle failure when sqs classes are not present. + */ +interface SqsMessage { + + Map messageAttributes(); + + Map attributesAsStrings(); + + String getMessageAttribute(String name); + + String getMessageId(); +} diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/SqsMessageImpl.java b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/SqsMessageImpl.java new file mode 100644 index 000000000000..63f7009d250b --- /dev/null +++ b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/SqsMessageImpl.java @@ -0,0 +1,54 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.awssdk.v2_2; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import software.amazon.awssdk.services.sqs.model.Message; +import software.amazon.awssdk.services.sqs.model.MessageAttributeValue; + +final class SqsMessageImpl implements SqsMessage { + + private final Message message; + + private SqsMessageImpl(Message message) { + this.message = message; + } + + static SqsMessage wrap(Message message) { + return new SqsMessageImpl(message); + } + + static List wrap(List messages) { + List result = new ArrayList<>(); + for (Message message : messages) { + result.add(wrap(message)); + } + return result; + } + + @Override + public Map messageAttributes() { + return message.messageAttributes(); + } + + @Override + public Map attributesAsStrings() { + return message.attributesAsStrings(); + } + + @Override + public String getMessageAttribute(String name) { + MessageAttributeValue value = message.messageAttributes().get(name); + return value != null ? value.stringValue() : null; + } + + @Override + public String getMessageId() { + return message.messageId(); + } +} diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/SqsParentContext.java b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/SqsParentContext.java index d8e44490136a..1540215cf561 100644 --- a/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/SqsParentContext.java +++ b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/SqsParentContext.java @@ -69,5 +69,24 @@ static Context ofSystemAttributes(Map systemAttributes) { StringMapGetter.INSTANCE); } + static Context ofMessage(SqsMessage message, TracingExecutionInterceptor config) { + return ofMessage(message, config.getMessagingPropagator(), config.shouldUseXrayPropagator()); + } + + static Context ofMessage( + SqsMessage message, TextMapPropagator messagingPropagator, boolean shouldUseXrayPropagator) { + io.opentelemetry.context.Context parentContext = io.opentelemetry.context.Context.root(); + + if (messagingPropagator != null) { + parentContext = ofMessageAttributes(message.messageAttributes(), messagingPropagator); + } + + if (shouldUseXrayPropagator && parentContext == io.opentelemetry.context.Context.root()) { + parentContext = ofSystemAttributes(message.attributesAsStrings()); + } + + return parentContext; + } + private SqsParentContext() {} } diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/SqsProcessRequest.java b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/SqsProcessRequest.java new file mode 100644 index 000000000000..a2bc70295a55 --- /dev/null +++ b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/SqsProcessRequest.java @@ -0,0 +1,31 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.awssdk.v2_2; + +import software.amazon.awssdk.core.interceptor.ExecutionAttributes; + +final class SqsProcessRequest extends AbstractSqsRequest { + private final ExecutionAttributes request; + private final SqsMessage message; + + private SqsProcessRequest(ExecutionAttributes request, SqsMessage message) { + this.request = request; + this.message = message; + } + + public static SqsProcessRequest create(ExecutionAttributes request, SqsMessage message) { + return new SqsProcessRequest(request, message); + } + + @Override + public ExecutionAttributes getRequest() { + return request; + } + + public SqsMessage getMessage() { + return message; + } +} diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/SqsProcessRequestAttributesGetter.java b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/SqsProcessRequestAttributesGetter.java new file mode 100644 index 000000000000..d8ecfefbb113 --- /dev/null +++ b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/SqsProcessRequestAttributesGetter.java @@ -0,0 +1,97 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.awssdk.v2_2; + +import io.opentelemetry.instrumentation.api.incubator.semconv.messaging.MessagingAttributesGetter; +import java.util.Collections; +import java.util.List; +import javax.annotation.Nullable; +import software.amazon.awssdk.core.SdkRequest; + +enum SqsProcessRequestAttributesGetter + implements MessagingAttributesGetter { + INSTANCE; + + // copied from MessagingIncubatingAttributes.MessagingSystemValues + private static final String AWS_SQS = "aws_sqs"; + + @Override + public String getSystem(SqsProcessRequest request) { + return AWS_SQS; + } + + @Override + public String getDestination(SqsProcessRequest request) { + SdkRequest sdkRequest = + request.getRequest().getAttribute(TracingExecutionInterceptor.SDK_REQUEST_ATTRIBUTE); + String queueUrl = SqsAccess.getQueueUrl(sdkRequest); + if (queueUrl != null) { + int i = queueUrl.lastIndexOf('/'); + if (i > 0) { + return queueUrl.substring(i + 1); + } + } + return null; + } + + @Nullable + @Override + public String getDestinationTemplate(SqsProcessRequest request) { + return null; + } + + @Override + public boolean isTemporaryDestination(SqsProcessRequest request) { + return false; + } + + @Override + public boolean isAnonymousDestination(SqsProcessRequest request) { + return false; + } + + @Override + @Nullable + public String getConversationId(SqsProcessRequest request) { + return null; + } + + @Nullable + @Override + public Long getMessageBodySize(SqsProcessRequest request) { + return null; + } + + @Nullable + @Override + public Long getMessageEnvelopeSize(SqsProcessRequest request) { + return null; + } + + @Override + @Nullable + public String getMessageId(SqsProcessRequest request, @Nullable Response response) { + return request.getMessage().getMessageId(); + } + + @Nullable + @Override + public String getClientId(SqsProcessRequest request) { + return null; + } + + @Nullable + @Override + public Long getBatchMessageCount(SqsProcessRequest request, @Nullable Response response) { + return null; + } + + @Override + public List getMessageHeader(SqsProcessRequest request, String name) { + String value = request.getMessage().getMessageAttribute(name); + return value != null ? Collections.singletonList(value) : Collections.emptyList(); + } +} diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/SqsReceiveMessageRequestAccess.java b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/SqsReceiveMessageRequestAccess.java deleted file mode 100644 index b6ac23ded0fe..000000000000 --- a/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/SqsReceiveMessageRequestAccess.java +++ /dev/null @@ -1,122 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.awssdk.v2_2; - -import static java.lang.invoke.MethodType.methodType; - -import java.lang.invoke.MethodHandle; -import java.lang.invoke.MethodHandles; -import java.util.Collection; -import java.util.Collections; -import java.util.List; -import java.util.Optional; -import javax.annotation.Nullable; -import software.amazon.awssdk.core.SdkRequest; - -/** - * 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. - * - * @see SDK - * Javadoc - * @see Definition - * JSON - */ -final class SqsReceiveMessageRequestAccess { - - @Nullable private static final MethodHandle ATTRIBUTE_NAMES_WITH_STRINGS; - @Nullable private static final MethodHandle MESSAGE_ATTRIBUTE_NAMES; - - static { - Class receiveMessageRequestClass = null; - try { - receiveMessageRequestClass = - Class.forName("software.amazon.awssdk.services.sqs.model.ReceiveMessageRequest$Builder"); - } catch (Throwable t) { - // Ignore. - } - if (receiveMessageRequestClass != null) { - MethodHandles.Lookup lookup = MethodHandles.publicLookup(); - MethodHandle withAttributeNames = null; - try { - withAttributeNames = - lookup.findVirtual( - receiveMessageRequestClass, - "attributeNamesWithStrings", - methodType(receiveMessageRequestClass, Collection.class)); - } catch (NoSuchMethodException | IllegalAccessException e) { - // Ignore - } - ATTRIBUTE_NAMES_WITH_STRINGS = withAttributeNames; - - MethodHandle messageAttributeNames = null; - try { - messageAttributeNames = - lookup.findVirtual( - receiveMessageRequestClass, - "messageAttributeNames", - methodType(receiveMessageRequestClass, Collection.class)); - } catch (NoSuchMethodException | IllegalAccessException e) { - // Ignore - } - MESSAGE_ATTRIBUTE_NAMES = messageAttributeNames; - } else { - ATTRIBUTE_NAMES_WITH_STRINGS = null; - MESSAGE_ATTRIBUTE_NAMES = null; - } - } - - static boolean isInstance(SdkRequest request) { - return request - .getClass() - .getName() - .equals("software.amazon.awssdk.services.sqs.model.ReceiveMessageRequest"); - } - - static void attributeNamesWithStrings(SdkRequest.Builder builder, List attributeNames) { - if (ATTRIBUTE_NAMES_WITH_STRINGS == null) { - return; - } - try { - ATTRIBUTE_NAMES_WITH_STRINGS.invoke(builder, attributeNames); - } catch (Throwable throwable) { - // Ignore - } - } - - static void messageAttributeNames( - SdkRequest.Builder builder, List messageAttributeNames) { - if (MESSAGE_ATTRIBUTE_NAMES == null) { - return; - } - try { - MESSAGE_ATTRIBUTE_NAMES.invoke(builder, messageAttributeNames); - } catch (Throwable throwable) { - // Ignore - } - } - - private SqsReceiveMessageRequestAccess() {} - - @SuppressWarnings({"rawtypes", "unchecked"}) - static List getAttributeNames(SdkRequest request) { - Optional optional = request.getValueForField("AttributeNames", List.class); - return optional.isPresent() ? (List) optional.get() : Collections.emptyList(); - } - - @SuppressWarnings({"rawtypes", "unchecked"}) - static List getMessageAttributeNames(SdkRequest request) { - Optional optional = request.getValueForField("MessageAttributeNames", List.class); - return optional.isPresent() ? (List) optional.get() : Collections.emptyList(); - } -} diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/SqsReceiveRequest.java b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/SqsReceiveRequest.java new file mode 100644 index 000000000000..4d1ef63accb2 --- /dev/null +++ b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/SqsReceiveRequest.java @@ -0,0 +1,32 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.awssdk.v2_2; + +import java.util.List; +import software.amazon.awssdk.core.interceptor.ExecutionAttributes; + +final class SqsReceiveRequest extends AbstractSqsRequest { + private final ExecutionAttributes request; + private final List messages; + + private SqsReceiveRequest(ExecutionAttributes request, List messages) { + this.request = request; + this.messages = messages; + } + + public static SqsReceiveRequest create(ExecutionAttributes request, List messages) { + return new SqsReceiveRequest(request, messages); + } + + @Override + public ExecutionAttributes getRequest() { + return request; + } + + public List getMessages() { + return messages; + } +} diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/SqsReceiveRequestAttributesGetter.java b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/SqsReceiveRequestAttributesGetter.java new file mode 100644 index 000000000000..cab84a37205b --- /dev/null +++ b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/SqsReceiveRequestAttributesGetter.java @@ -0,0 +1,99 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.awssdk.v2_2; + +import io.opentelemetry.instrumentation.api.incubator.semconv.messaging.MessagingAttributesGetter; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; +import javax.annotation.Nullable; +import software.amazon.awssdk.core.SdkRequest; + +enum SqsReceiveRequestAttributesGetter + implements MessagingAttributesGetter { + INSTANCE; + + // copied from MessagingIncubatingAttributes.MessagingSystemValues + private static final String AWS_SQS = "aws_sqs"; + + @Override + public String getSystem(SqsReceiveRequest request) { + return AWS_SQS; + } + + @Override + public String getDestination(SqsReceiveRequest request) { + SdkRequest sdkRequest = + request.getRequest().getAttribute(TracingExecutionInterceptor.SDK_REQUEST_ATTRIBUTE); + String queueUrl = SqsAccess.getQueueUrl(sdkRequest); + if (queueUrl != null) { + int i = queueUrl.lastIndexOf('/'); + if (i > 0) { + return queueUrl.substring(i + 1); + } + } + return null; + } + + @Nullable + @Override + public String getDestinationTemplate(SqsReceiveRequest request) { + return null; + } + + @Override + public boolean isTemporaryDestination(SqsReceiveRequest request) { + return false; + } + + @Override + public boolean isAnonymousDestination(SqsReceiveRequest request) { + return false; + } + + @Override + @Nullable + public String getConversationId(SqsReceiveRequest request) { + return null; + } + + @Nullable + @Override + public Long getMessageBodySize(SqsReceiveRequest request) { + return null; + } + + @Nullable + @Override + public Long getMessageEnvelopeSize(SqsReceiveRequest request) { + return null; + } + + @Override + @Nullable + public String getMessageId(SqsReceiveRequest request, @Nullable Response response) { + return null; + } + + @Nullable + @Override + public String getClientId(SqsReceiveRequest request) { + return null; + } + + @Override + public Long getBatchMessageCount(SqsReceiveRequest request, @Nullable Response response) { + return (long) request.getMessages().size(); + } + + @Override + public List getMessageHeader(SqsReceiveRequest request, String name) { + return StreamSupport.stream(request.getMessages().spliterator(), false) + .map(message -> message.getMessageAttribute(name)) + .filter(value -> value != null) + .collect(Collectors.toList()); + } +} diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/SqsSendMessageRequestAccess.java b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/SqsSendMessageRequestAccess.java deleted file mode 100644 index b863ef3ceaf2..000000000000 --- a/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/SqsSendMessageRequestAccess.java +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.awssdk.v2_2; - -import software.amazon.awssdk.core.SdkRequest; - -/** - * Reflective access to aws-sdk-java-sqs class ReceiveMessageRequest for points where we are not - * sure whether SQS is on the classpath. - */ -final class SqsSendMessageRequestAccess { - static boolean isInstance(SdkRequest request) { - return request - .getClass() - .getName() - .equals("software.amazon.awssdk.services.sqs.model.SendMessageRequest"); - } - - private SqsSendMessageRequestAccess() {} -} diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/SqsTracingContext.java b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/SqsTracingContext.java new file mode 100644 index 000000000000..d23838b94e4b --- /dev/null +++ b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/SqsTracingContext.java @@ -0,0 +1,37 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.awssdk.v2_2; + +import static io.opentelemetry.context.ContextKey.named; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.ContextKey; +import io.opentelemetry.context.ImplicitContextKeyed; +import javax.annotation.Nullable; + +final class SqsTracingContext implements ImplicitContextKeyed { + + private static final ContextKey KEY = named("sqs-tracing-context"); + + private TracingList tracingList; + + public static void set(Context context, TracingList tracingList) { + SqsTracingContext holder = context.get(KEY); + if (holder != null) { + holder.tracingList = tracingList; + } + } + + @Nullable + public TracingList get() { + return tracingList; + } + + @Override + public Context storeInContext(Context context) { + return context.with(KEY, this); + } +} diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/TracingExecutionInterceptor.java b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/TracingExecutionInterceptor.java index 95d81cd3034d..ada21457d45b 100644 --- a/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/TracingExecutionInterceptor.java +++ b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/TracingExecutionInterceptor.java @@ -7,6 +7,7 @@ import static io.opentelemetry.instrumentation.awssdk.v2_2.AwsSdkRequestType.DYNAMODB; +import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.common.Attributes; import io.opentelemetry.api.common.AttributesBuilder; import io.opentelemetry.api.trace.Span; @@ -14,10 +15,19 @@ import io.opentelemetry.context.propagation.TextMapPropagator; import io.opentelemetry.contrib.awsxray.propagator.AwsXrayPropagator; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; -import java.util.ArrayList; -import java.util.List; +import io.opentelemetry.instrumentation.api.internal.InstrumenterUtil; +import io.opentelemetry.instrumentation.api.internal.Timer; +import io.opentelemetry.semconv.HttpAttributes; +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.Charset; +import java.time.Instant; +import java.util.Optional; +import java.util.stream.Collectors; import javax.annotation.Nullable; +import software.amazon.awssdk.auth.signer.AwsSignerExecutionAttribute; import software.amazon.awssdk.awscore.AwsResponse; import software.amazon.awssdk.core.ClientType; import software.amazon.awssdk.core.SdkRequest; @@ -33,25 +43,48 @@ /** AWS request execution interceptor. */ final class TracingExecutionInterceptor implements ExecutionInterceptor { + // copied from DbIncubatingAttributes + private static final AttributeKey DB_OPERATION = AttributeKey.stringKey("db.operation"); + private static final AttributeKey DB_SYSTEM = AttributeKey.stringKey("db.system"); + // copied from DbIncubatingAttributes.DbSystemValues + private static final String DB_SYSTEM_DYNAMODB = "dynamodb"; + // 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 = + private static final ExecutionAttribute CONTEXT_ATTRIBUTE = new ExecutionAttribute<>(TracingExecutionInterceptor.class.getName() + ".Context"); - static final ExecutionAttribute SCOPE_ATTRIBUTE = + private static final ExecutionAttribute + PARENT_CONTEXT_ATTRIBUTE = + new ExecutionAttribute<>(TracingExecutionInterceptor.class.getName() + ".ParentContext"); + private static final ExecutionAttribute SCOPE_ATTRIBUTE = new ExecutionAttribute<>(TracingExecutionInterceptor.class.getName() + ".Scope"); - static final ExecutionAttribute AWS_SDK_REQUEST_ATTRIBUTE = + private static final ExecutionAttribute AWS_SDK_REQUEST_ATTRIBUTE = new ExecutionAttribute<>(TracingExecutionInterceptor.class.getName() + ".AwsSdkRequest"); static final ExecutionAttribute SDK_HTTP_REQUEST_ATTRIBUTE = new ExecutionAttribute<>(TracingExecutionInterceptor.class.getName() + ".SdkHttpRequest"); static final ExecutionAttribute SDK_REQUEST_ATTRIBUTE = new ExecutionAttribute<>(TracingExecutionInterceptor.class.getName() + ".SdkRequest"); - - private final Instrumenter requestInstrumenter; - private final Instrumenter consumerInstrumenter; + private static final ExecutionAttribute REQUEST_FINISHER_ATTRIBUTE = + new ExecutionAttribute<>(TracingExecutionInterceptor.class.getName() + ".RequestFinisher"); + static final ExecutionAttribute TRACING_MESSAGES_ATTRIBUTE = + new ExecutionAttribute<>(TracingExecutionInterceptor.class.getName() + ".TracingMessages"); + + private final Instrumenter requestInstrumenter; + private final Instrumenter consumerReceiveInstrumenter; + private final Instrumenter consumerProcessInstrumenter; + private final Instrumenter producerInstrumenter; private final boolean captureExperimentalSpanAttributes; - Instrumenter getConsumerInstrumenter() { - return consumerInstrumenter; + static final AttributeKey HTTP_ERROR_MSG = + AttributeKey.stringKey("aws.http.error_message"); + static final String HTTP_FAILURE_EVENT = "HTTP request failure"; + + Instrumenter getConsumerReceiveInstrumenter() { + return consumerReceiveInstrumenter; + } + + Instrumenter getConsumerProcessInstrumenter() { + return consumerProcessInstrumenter; } @Nullable @@ -65,19 +98,26 @@ boolean shouldUseXrayPropagator() { @Nullable private final TextMapPropagator messagingPropagator; private final boolean useXrayPropagator; + private final boolean recordIndividualHttpError; private final FieldMapper fieldMapper; TracingExecutionInterceptor( - Instrumenter requestInstrumenter, - Instrumenter consumerInstrumenter, + Instrumenter requestInstrumenter, + Instrumenter consumerReceiveInstrumenter, + Instrumenter consumerProcessInstrumenter, + Instrumenter producerInstrumenter, boolean captureExperimentalSpanAttributes, TextMapPropagator messagingPropagator, - boolean useXrayPropagator) { + boolean useXrayPropagator, + boolean recordIndividualHttpError) { this.requestInstrumenter = requestInstrumenter; - this.consumerInstrumenter = consumerInstrumenter; + this.consumerReceiveInstrumenter = consumerReceiveInstrumenter; + this.consumerProcessInstrumenter = consumerProcessInstrumenter; + this.producerInstrumenter = producerInstrumenter; this.captureExperimentalSpanAttributes = captureExperimentalSpanAttributes; this.messagingPropagator = messagingPropagator; this.useXrayPropagator = useXrayPropagator; + this.recordIndividualHttpError = recordIndividualHttpError; this.fieldMapper = new FieldMapper(); } @@ -90,16 +130,57 @@ public SdkRequest modifyRequest( io.opentelemetry.context.Context parentOtelContext = io.opentelemetry.context.Context.current(); SdkRequest request = context.request(); + + // Ignore presign request. These requests don't run all interceptor methods and the span created + // here would never be ended and scope closed. + if (executionAttributes.getAttribute(AwsSignerExecutionAttribute.PRESIGNER_EXPIRATION) + != null) { + return request; + } + executionAttributes.putAttribute(SDK_REQUEST_ATTRIBUTE, request); + Instrumenter instrumenter = getInstrumenter(request); - if (!requestInstrumenter.shouldStart(parentOtelContext, executionAttributes)) { + if (!instrumenter.shouldStart(parentOtelContext, executionAttributes)) { // NB: We also skip injection in case we don't start. return request; } - io.opentelemetry.context.Context otelContext = - requestInstrumenter.start(parentOtelContext, executionAttributes); + RequestSpanFinisher requestFinisher; + io.opentelemetry.context.Context otelContext; + Instant requestStart = Instant.now(); + // Skip creating request span for SqsClient.receiveMessage if there is no parent span and also + // suppress the span from the underlying http client. Request/http client span appears in a + // separate trace from message producer/consumer spans if there is no parent span just having + // a trace with only the request/http client span isn't useful. + if (Span.fromContextOrNull(parentOtelContext) == null + && "software.amazon.awssdk.services.sqs.model.ReceiveMessageRequest" + .equals(request.getClass().getName())) { + otelContext = + InstrumenterUtil.suppressSpan(instrumenter, parentOtelContext, executionAttributes); + requestFinisher = + (finisherOtelContext, finisherExecutionAttributes, response, exception) -> { + // generate request span when there was an error + if (exception != null + && instrumenter.shouldStart(finisherOtelContext, finisherExecutionAttributes)) { + InstrumenterUtil.startAndEnd( + instrumenter, + finisherOtelContext, + finisherExecutionAttributes, + response, + exception, + requestStart, + Instant.now()); + } + }; + } else { + otelContext = instrumenter.start(parentOtelContext, executionAttributes); + requestFinisher = instrumenter::end; + } + + executionAttributes.putAttribute(PARENT_CONTEXT_ATTRIBUTE, parentOtelContext); executionAttributes.putAttribute(CONTEXT_ATTRIBUTE, otelContext); + executionAttributes.putAttribute(REQUEST_FINISHER_ATTRIBUTE, requestFinisher); if (executionAttributes .getAttribute(SdkExecutionAttribute.CLIENT_TYPE) .equals(ClientType.SYNC)) { @@ -117,65 +198,38 @@ public SdkRequest modifyRequest( populateRequestAttributes(span, awsSdkRequest, context.request(), executionAttributes); } } catch (Throwable throwable) { - requestInstrumenter.end(otelContext, executionAttributes, null, throwable); + requestFinisher.finish(otelContext, executionAttributes, null, throwable); clearAttributes(executionAttributes); throw throwable; } - if (SqsReceiveMessageRequestAccess.isInstance(request)) { - return modifySqsReceiveMessageRequest(request); - } else if (messagingPropagator != null) { - if (SqsSendMessageRequestAccess.isInstance(request)) { - return SqsAccess.injectIntoSqsSendMessageRequest(messagingPropagator, request, otelContext); - } - // TODO: Support SendMessageBatchRequest (and thus SendMessageBatchRequestEntry) + SdkRequest modifiedRequest = + SqsAccess.modifyRequest(request, otelContext, useXrayPropagator, messagingPropagator); + if (modifiedRequest != null) { + return modifiedRequest; } - return request; - } - - private SdkRequest modifySqsReceiveMessageRequest(SdkRequest request) { - boolean hasXrayAttribute = true; - List existingAttributeNames = null; - if (useXrayPropagator) { - existingAttributeNames = SqsReceiveMessageRequestAccess.getAttributeNames(request); - hasXrayAttribute = - existingAttributeNames.contains(SqsParentContext.AWS_TRACE_SYSTEM_ATTRIBUTE); + modifiedRequest = SnsAccess.modifyRequest(request, otelContext, messagingPropagator); + if (modifiedRequest != null) { + return modifiedRequest; } - - boolean hasMessageAttribute = true; - List existingMessageAttributeNames = null; - if (messagingPropagator != null) { - existingMessageAttributeNames = - SqsReceiveMessageRequestAccess.getMessageAttributeNames(request); - hasMessageAttribute = existingMessageAttributeNames.containsAll(messagingPropagator.fields()); + modifiedRequest = LambdaAccess.modifyRequest(request, otelContext); + if (modifiedRequest != null) { + return modifiedRequest; } - if (hasMessageAttribute && hasXrayAttribute) { - return request; - } + // Insert other special handling here, following the same pattern as SQS and SNS. - SdkRequest.Builder builder = request.toBuilder(); - if (!hasXrayAttribute) { - List attributeNames = new ArrayList<>(existingAttributeNames); - attributeNames.add(SqsParentContext.AWS_TRACE_SYSTEM_ATTRIBUTE); - SqsReceiveMessageRequestAccess.attributeNamesWithStrings(builder, attributeNames); - } - if (messagingPropagator != null) { - List messageAttributeNames = new ArrayList<>(existingMessageAttributeNames); - for (String field : messagingPropagator.fields()) { - if (!existingMessageAttributeNames.contains(field)) { - messageAttributeNames.add(field); - } - } - SqsReceiveMessageRequestAccess.messageAttributeNames(builder, messageAttributeNames); - } - return builder.build(); + return request; } @Override - public void afterMarshalling( - Context.AfterMarshalling context, ExecutionAttributes executionAttributes) { - + public void beforeTransmission( + Context.BeforeTransmission context, ExecutionAttributes executionAttributes) { + // In beforeTransmission we get access to the finalized http request, including modifications + // performed by other interceptors and the message signature. + // It is unlikely that further modifications are performed by the http client performing the + // request given that this would require the signature to be regenerated. + // // Since we merge the HTTP attributes into an already started span instead of creating a // full child span, we have to do some dirty work here. // @@ -210,7 +264,7 @@ private static void onHttpResponseAvailable( // For the httpAttributesExtractor dance, see afterMarshalling AttributesBuilder builder = Attributes.builder(); // NB: UnsafeAttributes are package-private AwsSdkInstrumenterFactory.httpAttributesExtractor.onEnd( - builder, otelContext, executionAttributes, httpResponse, null); + builder, otelContext, executionAttributes, new Response(httpResponse), null); span.setAllAttributes(builder.build()); } @@ -225,7 +279,6 @@ private static void onHttpRequestAvailable( } @Override - @SuppressWarnings("deprecation") // deprecated class to be updated once published in new location public SdkHttpRequest modifyHttpRequest( Context.ModifyHttpRequest context, ExecutionAttributes executionAttributes) { @@ -245,6 +298,19 @@ public SdkHttpRequest modifyHttpRequest( return builder.build(); } + @Override + public Optional modifyHttpResponseContent( + Context.ModifyHttpResponse context, ExecutionAttributes executionAttributes) { + Optional responseBody = context.responseBody(); + if (recordIndividualHttpError) { + String errorMsg = extractHttpErrorAsEvent(context, executionAttributes); + if (errorMsg != null) { + return Optional.of(new ByteArrayInputStream(errorMsg.getBytes(Charset.defaultCharset()))); + } + } + return responseBody; + } + private void populateRequestAttributes( Span span, AwsSdkRequest awsSdkRequest, @@ -254,10 +320,10 @@ private void populateRequestAttributes( fieldMapper.mapToAttributes(sdkRequest, awsSdkRequest, span); if (awsSdkRequest.type() == DYNAMODB) { - span.setAttribute(SemanticAttributes.DB_SYSTEM, "dynamodb"); + span.setAttribute(DB_SYSTEM, DB_SYSTEM_DYNAMODB); String operation = attributes.getAttribute(SdkExecutionAttribute.OPERATION_NAME); if (operation != null) { - span.setAttribute(SemanticAttributes.DB_OPERATION, operation); + span.setAttribute(DB_OPERATION, operation); } } } @@ -265,8 +331,12 @@ private void populateRequestAttributes( @Override public void afterExecution( Context.AfterExecution context, ExecutionAttributes executionAttributes) { - if (SqsReceiveMessageRequestAccess.isInstance(context.request())) { - SqsAccess.afterReceiveMessageExecution(this, context, executionAttributes); + + if (executionAttributes.getAttribute(SDK_HTTP_REQUEST_ATTRIBUTE) != null) { + // Other special handling could be shortcut-&&ed after this (false is returned if not + // handled). + Timer timer = Timer.start(); + SqsAccess.afterReceiveMessageExecution(context, executionAttributes, this, timer); } io.opentelemetry.context.Context otelContext = getContext(executionAttributes); @@ -275,27 +345,19 @@ public void afterExecution( executionAttributes.putAttribute(SDK_HTTP_REQUEST_ATTRIBUTE, context.httpRequest()); Span span = Span.fromContext(otelContext); - onUserAgentHeaderAvailable(span, executionAttributes); onSdkResponse(span, context.response(), executionAttributes); SdkHttpResponse httpResponse = context.httpResponse(); onHttpResponseAvailable( executionAttributes, otelContext, Span.fromContext(otelContext), httpResponse); - requestInstrumenter.end(otelContext, executionAttributes, httpResponse, null); + RequestSpanFinisher finisher = executionAttributes.getAttribute(REQUEST_FINISHER_ATTRIBUTE); + finisher.finish( + otelContext, executionAttributes, new Response(httpResponse, context.response()), null); } clearAttributes(executionAttributes); } - // Certain headers in the request like User-Agent are only available after execution. - private static void onUserAgentHeaderAvailable(Span span, ExecutionAttributes request) { - List userAgent = - AwsSdkInstrumenterFactory.httpAttributesGetter.getHttpRequestHeader(request, "User-Agent"); - if (!userAgent.isEmpty()) { - span.setAttribute(SemanticAttributes.USER_AGENT_ORIGINAL, userAgent.get(0)); - } - } - private void onSdkResponse( Span span, SdkResponse response, ExecutionAttributes executionAttributes) { if (captureExperimentalSpanAttributes) { @@ -309,12 +371,44 @@ private void onSdkResponse( } } + private static String extractHttpErrorAsEvent( + Context.AfterTransmission context, ExecutionAttributes executionAttributes) { + io.opentelemetry.context.Context otelContext = getContext(executionAttributes); + if (otelContext != null) { + Span span = Span.fromContext(otelContext); + SdkHttpResponse response = context.httpResponse(); + + if (response != null && !response.isSuccessful()) { + int errorCode = response.statusCode(); + // we want to record the error message from http response + Optional responseBody = context.responseBody(); + if (responseBody.isPresent()) { + String errorMsg = + new BufferedReader( + new InputStreamReader(responseBody.get(), Charset.defaultCharset())) + .lines() + .collect(Collectors.joining("\n")); + Attributes attributes = + Attributes.of( + HttpAttributes.HTTP_RESPONSE_STATUS_CODE, + Long.valueOf(errorCode), + HTTP_ERROR_MSG, + errorMsg); + span.addEvent(HTTP_FAILURE_EVENT, attributes); + return errorMsg; + } + } + } + return null; + } + @Override public void onExecutionFailure( Context.FailedExecution context, ExecutionAttributes executionAttributes) { io.opentelemetry.context.Context otelContext = getContext(executionAttributes); if (otelContext != null) { - requestInstrumenter.end(otelContext, executionAttributes, null, context.exception()); + RequestSpanFinisher finisher = executionAttributes.getAttribute(REQUEST_FINISHER_ATTRIBUTE); + finisher.finish(otelContext, executionAttributes, null, context.exception()); } clearAttributes(executionAttributes); } @@ -325,8 +419,11 @@ private static void clearAttributes(ExecutionAttributes executionAttributes) { scope.close(); } executionAttributes.putAttribute(CONTEXT_ATTRIBUTE, null); + executionAttributes.putAttribute(PARENT_CONTEXT_ATTRIBUTE, null); executionAttributes.putAttribute(AWS_SDK_REQUEST_ATTRIBUTE, null); executionAttributes.putAttribute(SDK_HTTP_REQUEST_ATTRIBUTE, null); + executionAttributes.putAttribute(REQUEST_FINISHER_ATTRIBUTE, null); + executionAttributes.putAttribute(TRACING_MESSAGES_ATTRIBUTE, null); } /** @@ -336,4 +433,20 @@ private static void clearAttributes(ExecutionAttributes executionAttributes) { static io.opentelemetry.context.Context getContext(ExecutionAttributes attributes) { return attributes.getAttribute(CONTEXT_ATTRIBUTE); } + + static io.opentelemetry.context.Context getParentContext(ExecutionAttributes attributes) { + return attributes.getAttribute(PARENT_CONTEXT_ATTRIBUTE); + } + + private Instrumenter getInstrumenter(SdkRequest request) { + return SqsAccess.isSqsProducerRequest(request) ? producerInstrumenter : requestInstrumenter; + } + + private interface RequestSpanFinisher { + void finish( + io.opentelemetry.context.Context otelContext, + ExecutionAttributes executionAttributes, + Response response, + Throwable exception); + } } diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/TracingIterator.java b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/TracingIterator.java new file mode 100644 index 000000000000..8d2e1e4d99c4 --- /dev/null +++ b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/TracingIterator.java @@ -0,0 +1,104 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.awssdk.v2_2; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import java.util.Iterator; +import javax.annotation.Nullable; +import software.amazon.awssdk.core.interceptor.ExecutionAttributes; +import software.amazon.awssdk.services.sqs.model.Message; + +class TracingIterator implements Iterator { + + private final Iterator delegateIterator; + private final Instrumenter instrumenter; + private final ExecutionAttributes request; + private final Response response; + private final TracingExecutionInterceptor config; + private final Context receiveContext; + + /* + * 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 SqsProcessRequest currentRequest; + @Nullable private Context currentContext; + @Nullable private Scope currentScope; + + private TracingIterator( + Iterator delegateIterator, + Instrumenter instrumenter, + ExecutionAttributes request, + Response response, + TracingExecutionInterceptor config, + Context receiveContext) { + this.delegateIterator = delegateIterator; + this.instrumenter = instrumenter; + this.request = request; + this.response = response; + this.config = config; + this.receiveContext = receiveContext; + } + + public static Iterator wrap( + Iterator delegateIterator, + Instrumenter instrumenter, + ExecutionAttributes request, + Response response, + TracingExecutionInterceptor config, + Context receiveContext) { + return new TracingIterator( + delegateIterator, instrumenter, request, response, config, receiveContext); + } + + @Override + public boolean hasNext() { + closeScopeAndEndSpan(); + return delegateIterator.hasNext(); + } + + @Override + public Message next() { + // in case they didn't call hasNext()... + closeScopeAndEndSpan(); + + // it's important not to suppress consumer span creation here using Instrumenter.shouldStart() + // because this instrumentation can leak the context and so there may be a leaked consumer span + // in the context, in which case it's important to overwrite the leaked span instead of + // suppressing the correct span + // (https://github.com/open-telemetry/opentelemetry-java-instrumentation/issues/1947) + Message next = delegateIterator.next(); + if (next != null) { + SqsMessage sqsMessage = SqsMessageImpl.wrap(next); + Context parentContext = receiveContext; + if (parentContext == null) { + parentContext = SqsParentContext.ofMessage(sqsMessage, config); + } + + currentRequest = SqsProcessRequest.create(request, sqsMessage); + currentContext = instrumenter.start(parentContext, currentRequest); + currentScope = currentContext.makeCurrent(); + } + return next; + } + + private void closeScopeAndEndSpan() { + if (currentScope != null) { + currentScope.close(); + instrumenter.end(currentContext, currentRequest, response, null); + currentScope = null; + currentRequest = null; + currentContext = null; + } + } + + @Override + public void remove() { + delegateIterator.remove(); + } +} diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/TracingList.java b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/TracingList.java new file mode 100644 index 000000000000..b2b262b3c3a0 --- /dev/null +++ b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/TracingList.java @@ -0,0 +1,76 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.awssdk.v2_2; + +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.function.Consumer; +import software.amazon.awssdk.core.interceptor.ExecutionAttributes; +import software.amazon.awssdk.services.sqs.model.Message; + +class TracingList extends ArrayList { + private static final long serialVersionUID = 1L; + + private final Instrumenter instrumenter; + private final ExecutionAttributes request; + private final Response response; + private final TracingExecutionInterceptor config; + private final Context receiveContext; + private boolean firstIterator = true; + + private TracingList( + List list, + Instrumenter instrumenter, + ExecutionAttributes request, + Response response, + TracingExecutionInterceptor config, + Context receiveContext) { + super(list); + this.instrumenter = instrumenter; + this.request = request; + this.response = response; + this.config = config; + this.receiveContext = receiveContext; + } + + public static TracingList wrap( + List list, + Instrumenter instrumenter, + ExecutionAttributes request, + Response response, + TracingExecutionInterceptor config, + Context receiveContext) { + return new TracingList(list, instrumenter, request, response, config, receiveContext); + } + + @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 + // List is performed in the same thread that called receiveMessage() + if (firstIterator) { + it = + TracingIterator.wrap( + super.iterator(), instrumenter, request, response, config, receiveContext); + firstIterator = false; + } else { + it = super.iterator(); + } + + return it; + } + + @Override + public void forEach(Consumer action) { + for (Message message : this) { + action.accept(message); + } + } +} diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/library/src/test/groovy/io/opentelemetry/instrumentation/awssdk/v2_2/Aws2ClientTest.groovy b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/test/groovy/io/opentelemetry/instrumentation/awssdk/v2_2/Aws2ClientTest.groovy index 211773859068..40a88e4c5863 100644 --- a/instrumentation/aws-sdk/aws-sdk-2.2/library/src/test/groovy/io/opentelemetry/instrumentation/awssdk/v2_2/Aws2ClientTest.groovy +++ b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/test/groovy/io/opentelemetry/instrumentation/awssdk/v2_2/Aws2ClientTest.groovy @@ -15,7 +15,7 @@ class Aws2ClientTest extends AbstractAws2ClientTest implements LibraryTestTrait .addExecutionInterceptor( AwsSdkTelemetry.builder(getOpenTelemetry()) .setCaptureExperimentalSpanAttributes(true) - .setUseConfiguredPropagatorForMessaging(true) // Default on in tests to cover more code + .setUseConfiguredPropagatorForMessaging(isSqsAttributeInjectionEnabled()) .build() .newExecutionInterceptor()) } diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/library/src/test/groovy/io/opentelemetry/instrumentation/awssdk/v2_2/Aws2SqsSuppressReceiveSpansTest.groovy b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/test/groovy/io/opentelemetry/instrumentation/awssdk/v2_2/Aws2SqsSuppressReceiveSpansTest.groovy new file mode 100644 index 000000000000..af05d2ee6d8c --- /dev/null +++ b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/test/groovy/io/opentelemetry/instrumentation/awssdk/v2_2/Aws2SqsSuppressReceiveSpansTest.groovy @@ -0,0 +1,109 @@ +/* + * 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.config.ClientOverrideConfiguration +import software.amazon.awssdk.services.sqs.SqsAsyncClient +import software.amazon.awssdk.services.sqs.SqsClient + +abstract class Aws2SqsSuppressReceiveSpansTest extends AbstractAws2SqsSuppressReceiveSpansTest implements LibraryTestTrait { + static AwsSdkTelemetry telemetry + + def setupSpec() { + def telemetryBuilder = AwsSdkTelemetry.builder(getOpenTelemetry()) + .setCaptureExperimentalSpanAttributes(true) + configure(telemetryBuilder) + telemetry = telemetryBuilder.build() + } + + abstract void configure(AwsSdkTelemetryBuilder telemetryBuilder) + + @Override + ClientOverrideConfiguration.Builder createOverrideConfigurationBuilder() { + return ClientOverrideConfiguration.builder() + .addExecutionInterceptor( + telemetry.newExecutionInterceptor()) + } + + @Override + SqsClient configureSqsClient(SqsClient sqsClient) { + return telemetry.wrap(sqsClient) + } + + @Override + SqsAsyncClient configureSqsClient(SqsAsyncClient sqsClient) { + return telemetry.wrap(sqsClient) + } +} + +class Aws2SqsSuppressReceiveSpansDefaultPropagatorTest extends Aws2SqsSuppressReceiveSpansTest { + + @Override + void configure(AwsSdkTelemetryBuilder telemetryBuilder) {} + + @Override + boolean isSqsAttributeInjectionEnabled() { + false + } + + def "duplicate tracing interceptor"() { + setup: + def builder = SqsClient.builder() + configureSdkClient(builder) + def overrideConfiguration = ClientOverrideConfiguration.builder() + .addExecutionInterceptor(telemetry.newExecutionInterceptor()) + .addExecutionInterceptor(telemetry.newExecutionInterceptor()) + .build() + builder.overrideConfiguration(overrideConfiguration) + def client = configureSqsClient(builder.build()) + + client.createQueue(createQueueRequest) + + when: + client.sendMessage(sendMessageRequest) + + def resp = client.receiveMessage(receiveMessageRequest) + + then: + resp.messages().size() == 1 + resp.messages.each {message -> runWithSpan("process child") {}} + assertSqsTraces() + } +} + +class Aws2SqsSuppressReceiveSpansW3CPropagatorTest extends Aws2SqsSuppressReceiveSpansTest { + + @Override + void configure(AwsSdkTelemetryBuilder telemetryBuilder) { + telemetryBuilder.setUseConfiguredPropagatorForMessaging(isSqsAttributeInjectionEnabled()) // Difference to main test + .setUseXrayPropagator(isXrayInjectionEnabled()) // Disable to confirm messaging propagator actually works + } + + @Override + boolean isSqsAttributeInjectionEnabled() { + true + } + + @Override + boolean isXrayInjectionEnabled() { + false + } +} + +/** We want to test the combination of W3C + Xray, as that's what you'll get in prod if you enable W3C. */ +class Aws2SqsSuppressReceiveSpansW3CPropagatorAndXrayPropagatorTest extends Aws2SqsSuppressReceiveSpansTest { + + @Override + void configure(AwsSdkTelemetryBuilder telemetryBuilder) { + telemetryBuilder.setUseConfiguredPropagatorForMessaging(isSqsAttributeInjectionEnabled()) // Difference to main test + } + + @Override + boolean isSqsAttributeInjectionEnabled() { + true + } +} diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/library/src/test/groovy/io/opentelemetry/instrumentation/awssdk/v2_2/Aws2SqsTracingTest.groovy b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/test/groovy/io/opentelemetry/instrumentation/awssdk/v2_2/Aws2SqsTracingTest.groovy deleted file mode 100644 index 3aadab3cbc49..000000000000 --- a/instrumentation/aws-sdk/aws-sdk-2.2/library/src/test/groovy/io/opentelemetry/instrumentation/awssdk/v2_2/Aws2SqsTracingTest.groovy +++ /dev/null @@ -1,21 +0,0 @@ -/* - * 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.config.ClientOverrideConfiguration - -class Aws2SqsTracingTest extends AbstractAws2SqsTracingTest implements LibraryTestTrait { - @Override - ClientOverrideConfiguration.Builder createOverrideConfigurationBuilder() { - return ClientOverrideConfiguration.builder() - .addExecutionInterceptor( - AwsSdkTelemetry.builder(getOpenTelemetry()) - .setCaptureExperimentalSpanAttributes(true) - .build() - .newExecutionInterceptor()) - } -} diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/library/src/test/groovy/io/opentelemetry/instrumentation/awssdk/v2_2/Aws2SqsTracingTestWithW3CPropagator.groovy b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/test/groovy/io/opentelemetry/instrumentation/awssdk/v2_2/Aws2SqsTracingTestWithW3CPropagator.groovy deleted file mode 100644 index 4099b0ae91a6..000000000000 --- a/instrumentation/aws-sdk/aws-sdk-2.2/library/src/test/groovy/io/opentelemetry/instrumentation/awssdk/v2_2/Aws2SqsTracingTestWithW3CPropagator.groovy +++ /dev/null @@ -1,23 +0,0 @@ -/* - * 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.config.ClientOverrideConfiguration - -class Aws2SqsTracingTestWithW3CPropagator extends AbstractAws2SqsTracingTest implements LibraryTestTrait { - @Override - ClientOverrideConfiguration.Builder createOverrideConfigurationBuilder() { - return ClientOverrideConfiguration.builder() - .addExecutionInterceptor( - AwsSdkTelemetry.builder(getOpenTelemetry()) - .setCaptureExperimentalSpanAttributes(true) - .setUseConfiguredPropagatorForMessaging(true) // Difference to main test - .setUseXrayPropagator(false) // Disable to confirm messaging propagator actually works - .build() - .newExecutionInterceptor()) - } -} diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/library/src/test/groovy/io/opentelemetry/instrumentation/awssdk/v2_2/Aws2SqsTracingTestWithW3CPropagatorAndXrayPropagator.groovy b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/test/groovy/io/opentelemetry/instrumentation/awssdk/v2_2/Aws2SqsTracingTestWithW3CPropagatorAndXrayPropagator.groovy deleted file mode 100644 index a4af1e1b56d9..000000000000 --- a/instrumentation/aws-sdk/aws-sdk-2.2/library/src/test/groovy/io/opentelemetry/instrumentation/awssdk/v2_2/Aws2SqsTracingTestWithW3CPropagatorAndXrayPropagator.groovy +++ /dev/null @@ -1,23 +0,0 @@ -/* - * 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.config.ClientOverrideConfiguration - -/** We want to test the combination of W3C + Xray, as that's what you'll get in prod if you enable W3C. */ -class Aws2SqsTracingTestWithW3CPropagatorAndXrayPropagator extends AbstractAws2SqsTracingTest implements LibraryTestTrait { - @Override - ClientOverrideConfiguration.Builder createOverrideConfigurationBuilder() { - return ClientOverrideConfiguration.builder() - .addExecutionInterceptor( - AwsSdkTelemetry.builder(getOpenTelemetry()) - .setCaptureExperimentalSpanAttributes(true) - .setUseConfiguredPropagatorForMessaging(true) // Difference to main test - .build() - .newExecutionInterceptor()) - } -} diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/library/src/test/java/io/opentelemetry/instrumentation/awssdk/v2_2/Aws2ClientNotRecordHttpErrorTest.java b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/test/java/io/opentelemetry/instrumentation/awssdk/v2_2/Aws2ClientNotRecordHttpErrorTest.java new file mode 100644 index 000000000000..c40d83d97e6d --- /dev/null +++ b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/test/java/io/opentelemetry/instrumentation/awssdk/v2_2/Aws2ClientNotRecordHttpErrorTest.java @@ -0,0 +1,38 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.awssdk.v2_2; + +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.LibraryInstrumentationExtension; +import org.junit.jupiter.api.extension.RegisterExtension; +import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration; + +public class Aws2ClientNotRecordHttpErrorTest extends AbstractAws2ClientRecordHttpErrorTest { + @RegisterExtension + public static final LibraryInstrumentationExtension testing = + LibraryInstrumentationExtension.create(); + + @Override + public ClientOverrideConfiguration.Builder createOverrideConfigurationBuilder() { + return ClientOverrideConfiguration.builder() + .addExecutionInterceptor( + AwsSdkTelemetry.builder(testing.getOpenTelemetry()) + .setCaptureExperimentalSpanAttributes(true) + .setRecordIndividualHttpError(isRecordIndividualHttpErrorEnabled()) + .build() + .newExecutionInterceptor()); + } + + @Override + public boolean isRecordIndividualHttpErrorEnabled() { + return false; + } + + @Override + protected InstrumentationExtension getTesting() { + return testing; + } +} diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/library/src/test/java/io/opentelemetry/instrumentation/awssdk/v2_2/Aws2SqsDefaultPropagatorTest.java b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/test/java/io/opentelemetry/instrumentation/awssdk/v2_2/Aws2SqsDefaultPropagatorTest.java new file mode 100644 index 000000000000..9006104b2f7b --- /dev/null +++ b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/test/java/io/opentelemetry/instrumentation/awssdk/v2_2/Aws2SqsDefaultPropagatorTest.java @@ -0,0 +1,48 @@ +/* + * 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.net.URISyntaxException; +import org.junit.jupiter.api.Test; +import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration; +import software.amazon.awssdk.services.sqs.SqsClient; +import software.amazon.awssdk.services.sqs.SqsClientBuilder; +import software.amazon.awssdk.services.sqs.model.ReceiveMessageResponse; + +class Aws2SqsDefaultPropagatorTest extends Aws2SqsTracingTest { + + @Override + void configure(AwsSdkTelemetryBuilder telemetryBuilder) {} + + @Override + boolean isSqsAttributeInjectionEnabled() { + return false; + } + + @Test + void testDuplicateTracingInterceptor() throws URISyntaxException { + SqsClientBuilder builder = SqsClient.builder(); + configureSdkClient(builder); + ClientOverrideConfiguration overrideConfiguration = + ClientOverrideConfiguration.builder() + .addExecutionInterceptor(telemetry.newExecutionInterceptor()) + .addExecutionInterceptor(telemetry.newExecutionInterceptor()) + .build(); + + builder.overrideConfiguration(overrideConfiguration); + SqsClient client = configureSqsClient(builder.build()); + + client.createQueue(createQueueRequest); + client.sendMessage(sendMessageRequest); + ReceiveMessageResponse response = client.receiveMessage(receiveMessageRequest); + + assertThat(response.messages().size()).isEqualTo(1); + response.messages().forEach(message -> getTesting().runWithSpan("process child", () -> {})); + assertSqsTraces(false, false); + } +} diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/library/src/test/java/io/opentelemetry/instrumentation/awssdk/v2_2/Aws2SqsTracingTest.java b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/test/java/io/opentelemetry/instrumentation/awssdk/v2_2/Aws2SqsTracingTest.java new file mode 100644 index 000000000000..6720cdbbc6ea --- /dev/null +++ b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/test/java/io/opentelemetry/instrumentation/awssdk/v2_2/Aws2SqsTracingTest.java @@ -0,0 +1,59 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.awssdk.v2_2; + +import static java.util.Collections.singletonList; + +import io.opentelemetry.instrumentation.testing.junit.LibraryInstrumentationExtension; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.RegisterExtension; +import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration; +import software.amazon.awssdk.services.sqs.SqsAsyncClient; +import software.amazon.awssdk.services.sqs.SqsClient; + +abstract class Aws2SqsTracingTest extends AbstractAws2SqsTracingTest { + + @RegisterExtension + private static final LibraryInstrumentationExtension testing = + LibraryInstrumentationExtension.create(); + + static AwsSdkTelemetry telemetry; + + @Override + protected final LibraryInstrumentationExtension getTesting() { + return testing; + } + + @BeforeEach + void setup() { + AwsSdkTelemetryBuilder telemetryBuilder = + AwsSdkTelemetry.builder(getTesting().getOpenTelemetry()) + .setCaptureExperimentalSpanAttributes(true) + .setMessagingReceiveInstrumentationEnabled(true) + .setCapturedHeaders(singletonList("test-message-header")); + + configure(telemetryBuilder); + telemetry = telemetryBuilder.build(); + } + + abstract void configure(AwsSdkTelemetryBuilder telemetryBuilder); + + @Override + protected ClientOverrideConfiguration.Builder createOverrideConfigurationBuilder() { + return ClientOverrideConfiguration.builder() + .addExecutionInterceptor(telemetry.newExecutionInterceptor()); + } + + @Override + protected SqsClient configureSqsClient(SqsClient sqsClient) { + return telemetry.wrap(sqsClient); + } + + @Override + protected SqsAsyncClient configureSqsClient(SqsAsyncClient sqsClient) { + return telemetry.wrap(sqsClient); + } +} diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/library/src/test/java/io/opentelemetry/instrumentation/awssdk/v2_2/Aws2SqsW3cPropagatorAndXrayPropagatorTest.java b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/test/java/io/opentelemetry/instrumentation/awssdk/v2_2/Aws2SqsW3cPropagatorAndXrayPropagatorTest.java new file mode 100644 index 000000000000..a0d2da870136 --- /dev/null +++ b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/test/java/io/opentelemetry/instrumentation/awssdk/v2_2/Aws2SqsW3cPropagatorAndXrayPropagatorTest.java @@ -0,0 +1,19 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.awssdk.v2_2; + +class Aws2SqsW3cPropagatorAndXrayPropagatorTest extends Aws2SqsTracingTest { + @Override + void configure(AwsSdkTelemetryBuilder telemetryBuilder) { + telemetryBuilder.setUseConfiguredPropagatorForMessaging( + isSqsAttributeInjectionEnabled()); // Difference to main test + } + + @Override + boolean isSqsAttributeInjectionEnabled() { + return true; + } +} diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/library/src/test/java/io/opentelemetry/instrumentation/awssdk/v2_2/Aws2SqsW3cPropagatorTest.java b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/test/java/io/opentelemetry/instrumentation/awssdk/v2_2/Aws2SqsW3cPropagatorTest.java new file mode 100644 index 000000000000..9e8076e5f3a6 --- /dev/null +++ b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/test/java/io/opentelemetry/instrumentation/awssdk/v2_2/Aws2SqsW3cPropagatorTest.java @@ -0,0 +1,28 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.awssdk.v2_2; + +class Aws2SqsW3cPropagatorTest extends Aws2SqsTracingTest { + + @Override + void configure(AwsSdkTelemetryBuilder telemetryBuilder) { + telemetryBuilder + .setUseConfiguredPropagatorForMessaging( + isSqsAttributeInjectionEnabled()) // Difference to main test + .setUseXrayPropagator( + isXrayInjectionEnabled()); // Disable to confirm messaging propagator actually works + } + + @Override + boolean isSqsAttributeInjectionEnabled() { + return true; + } + + @Override + boolean isXrayInjectionEnabled() { + return false; + } +} diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/library/src/test/java/io/opentelemetry/instrumentation/awssdk/v2_2/FieldMapperTest.java b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/test/java/io/opentelemetry/instrumentation/awssdk/v2_2/FieldMapperTest.java index 756d09ef61d6..0e8309d6e059 100644 --- a/instrumentation/aws-sdk/aws-sdk-2.2/library/src/test/java/io/opentelemetry/instrumentation/awssdk/v2_2/FieldMapperTest.java +++ b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/test/java/io/opentelemetry/instrumentation/awssdk/v2_2/FieldMapperTest.java @@ -16,7 +16,7 @@ import java.util.Collection; import java.util.HashMap; import java.util.Map; -import org.junit.Test; +import org.junit.jupiter.api.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; @@ -25,11 +25,10 @@ import software.amazon.awssdk.services.dynamodb.model.UpdateTableRequest; import software.amazon.awssdk.services.dynamodb.model.WriteRequest; -public class FieldMapperTest { +class FieldMapperTest { @Test - public void shouldMapNestedField() { - + void shouldMapNestedField() { // given AwsSdkRequest awsSdkRequest = UpdateTable; MethodHandleFactory methodHandleFactory = new MethodHandleFactory(); @@ -56,8 +55,7 @@ public void shouldMapNestedField() { } @Test - public void shouldMapRequestFieldsOnly() { - + void shouldMapRequestFieldsOnly() { // given AwsSdkRequest awsSdkRequest = BatchWriteItem; MethodHandleFactory methodHandleFactory = new MethodHandleFactory(); @@ -76,8 +74,7 @@ public void shouldMapRequestFieldsOnly() { } @Test - public void shouldMapResponseFieldsOnly() { - + void shouldMapResponseFieldsOnly() { // given AwsSdkRequest awsSdkRequest = BatchWriteItem; MethodHandleFactory methodHandleFactory = new MethodHandleFactory(); diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/library/src/test/java/io/opentelemetry/instrumentation/awssdk/v2_2/QueryProtocolModelTest.java b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/test/java/io/opentelemetry/instrumentation/awssdk/v2_2/QueryProtocolModelTest.java new file mode 100644 index 000000000000..f18f75f5948f --- /dev/null +++ b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/test/java/io/opentelemetry/instrumentation/awssdk/v2_2/QueryProtocolModelTest.java @@ -0,0 +1,29 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.awssdk.v2_2; + +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.LibraryInstrumentationExtension; +import org.junit.jupiter.api.extension.RegisterExtension; +import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration; + +class QueryProtocolModelTest extends AbstractQueryProtocolModelTest { + @RegisterExtension + private static final LibraryInstrumentationExtension testing = + LibraryInstrumentationExtension.create(); + + @Override + protected ClientOverrideConfiguration.Builder createClientOverrideConfigurationBuilder() { + return ClientOverrideConfiguration.builder() + .addExecutionInterceptor( + AwsSdkTelemetry.builder(testing.getOpenTelemetry()).build().newExecutionInterceptor()); + } + + @Override + protected InstrumentationExtension getTesting() { + return testing; + } +} diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/library/src/test/java/io/opentelemetry/instrumentation/awssdk/v2_2/SerializerTest.java b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/test/java/io/opentelemetry/instrumentation/awssdk/v2_2/SerializerTest.java index aa267e524041..b2616dcaddc2 100644 --- a/instrumentation/aws-sdk/aws-sdk-2.2/library/src/test/java/io/opentelemetry/instrumentation/awssdk/v2_2/SerializerTest.java +++ b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/test/java/io/opentelemetry/instrumentation/awssdk/v2_2/SerializerTest.java @@ -13,14 +13,14 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import org.junit.Test; +import org.junit.jupiter.api.Test; import software.amazon.awssdk.core.SdkPojo; import software.amazon.awssdk.services.dynamodb.model.ProvisionedThroughput; -public class SerializerTest { +class SerializerTest { @Test - public void shouldSerializeSimpleString() { + void shouldSerializeSimpleString() { // given // when String serialized = new Serializer().serialize("simpleString"); @@ -29,7 +29,7 @@ public void shouldSerializeSimpleString() { } @Test - public void shouldSerializeSdkPojo() { + void shouldSerializeSdkPojo() { // given SdkPojo sdkPojo = ProvisionedThroughput.builder().readCapacityUnits(1L).writeCapacityUnits(2L).build(); @@ -40,7 +40,7 @@ public void shouldSerializeSdkPojo() { } @Test - public void shouldSerializeCollection() { + void shouldSerializeCollection() { // given List collection = Arrays.asList("one", "two", "three"); // when @@ -50,7 +50,7 @@ public void shouldSerializeCollection() { } @Test - public void shouldSerializeEmptyCollectionAsNull() { + void shouldSerializeEmptyCollectionAsNull() { // given List collection = Collections.emptyList(); // when @@ -60,7 +60,7 @@ public void shouldSerializeEmptyCollectionAsNull() { } @Test - public void shouldSerializeMapAsKeyCollection() { + void shouldSerializeMapAsKeyCollection() { // given Map map = new HashMap<>(); map.put("uno", 1L); diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/library/src/testCoreOnly/groovy/io/opentelemetry/instrumentation/awssdk/v2_2/Aws2ClientDynamodbTest.groovy b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/testCoreOnly/groovy/io/opentelemetry/instrumentation/awssdk/v2_2/Aws2ClientDynamodbTest.groovy new file mode 100644 index 000000000000..a8c5a4aab1bf --- /dev/null +++ b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/testCoreOnly/groovy/io/opentelemetry/instrumentation/awssdk/v2_2/Aws2ClientDynamodbTest.groovy @@ -0,0 +1,24 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.awssdk.v2_2 + +import groovy.transform.CompileStatic +import io.opentelemetry.instrumentation.test.LibraryTestTrait +import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration + +@CompileStatic +class Aws2ClientDynamodbTest extends AbstractAws2ClientCoreTest implements LibraryTestTrait { + @Override + ClientOverrideConfiguration.Builder createOverrideConfigurationBuilder() { + return ClientOverrideConfiguration.builder() + .addExecutionInterceptor( + AwsSdkTelemetry.builder(getOpenTelemetry()) + .setCaptureExperimentalSpanAttributes(true) + .setUseConfiguredPropagatorForMessaging(isSqsAttributeInjectionEnabled()) + .build() + .newExecutionInterceptor()) + } +} diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/library/src/testLambda/java/io/opentelemetry/instrumentation/awssdk/v2_2/Aws2LambdaTest.java b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/testLambda/java/io/opentelemetry/instrumentation/awssdk/v2_2/Aws2LambdaTest.java new file mode 100644 index 000000000000..3bfe7788e066 --- /dev/null +++ b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/testLambda/java/io/opentelemetry/instrumentation/awssdk/v2_2/Aws2LambdaTest.java @@ -0,0 +1,107 @@ +/* + * 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 io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.LibraryInstrumentationExtension; +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration; +import software.amazon.awssdk.services.lambda.model.InvokeRequest; + +class Aws2LambdaTest extends AbstractAws2LambdaTest { + + @RegisterExtension + private static final LibraryInstrumentationExtension testing = + LibraryInstrumentationExtension.create(); + + private static Context context; + private static AwsSdkTelemetry telemetry; + + @BeforeAll + static void setup() { + testing.runWithHttpServerSpan( + () -> { + context = Context.current(); + }); + + telemetry = AwsSdkTelemetry.create(testing.getOpenTelemetry()); + } + + @Override + protected InstrumentationExtension getTesting() { + return testing; + } + + @Override + protected ClientOverrideConfiguration.Builder createOverrideConfigurationBuilder() { + return ClientOverrideConfiguration.builder() + .addExecutionInterceptor(telemetry.newExecutionInterceptor()); + } + + private static String base64ify(String json) { + return Base64.getEncoder().encodeToString(json.getBytes(StandardCharsets.UTF_8)); + } + + @Test + void noExistingClientContext() { + InvokeRequest request = InvokeRequest.builder().build(); + + InvokeRequest newRequest = + (InvokeRequest) LambdaImpl.modifyOrAddCustomContextHeader(request, context); + + String newClientContext = newRequest.clientContext(); + newClientContext = + new String(Base64.getDecoder().decode(newClientContext), StandardCharsets.UTF_8); + assertThat(newClientContext.contains("traceparent")).isTrue(); + } + + @Test + void withExistingClientContext() { + String clientContext = + base64ify( + "{\"otherStuff\": \"otherValue\", \"custom\": {\"preExisting\": \"somevalue\"} }"); + InvokeRequest request = InvokeRequest.builder().clientContext(clientContext).build(); + + InvokeRequest newRequest = + (InvokeRequest) LambdaImpl.modifyOrAddCustomContextHeader(request, context); + + String newClientContext = newRequest.clientContext(); + newClientContext = + new String(Base64.getDecoder().decode(newClientContext), StandardCharsets.UTF_8); + assertThat(newClientContext.contains("traceparent")).isTrue(); + assertThat(newClientContext.contains("preExisting")).isTrue(); + assertThat(newClientContext.contains("otherStuff")).isTrue(); + } + + @Test + void exceedingMaximumLengthDoesNotModify() { + // awkward way to build a valid json that is almost but not quite too long + StringBuilder buffer = new StringBuilder("x"); + String long64edClientContext = ""; + while (true) { + buffer.append("x"); + String newClientContext = base64ify("{\"" + buffer + "\": \"" + buffer + "\"}"); + if (newClientContext.length() >= LambdaImpl.MAX_CLIENT_CONTEXT_LENGTH) { + break; + } + long64edClientContext = newClientContext; + } + + InvokeRequest request = InvokeRequest.builder().clientContext(long64edClientContext).build(); + assertThat(request.clientContext().equals(long64edClientContext)).isTrue(); + + InvokeRequest newRequest = + (InvokeRequest) LambdaImpl.modifyOrAddCustomContextHeader(request, context); + assertThat(newRequest).isNull(); // null return means no modification performed + } +} diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/testing/build.gradle.kts b/instrumentation/aws-sdk/aws-sdk-2.2/testing/build.gradle.kts index 6feb89ed47a4..f9c0791bd205 100644 --- a/instrumentation/aws-sdk/aws-sdk-2.2/testing/build.gradle.kts +++ b/instrumentation/aws-sdk/aws-sdk-2.2/testing/build.gradle.kts @@ -8,17 +8,21 @@ dependencies { api("software.amazon.awssdk:apache-client:2.2.0") // older versions don't play nice with armeria http server api("software.amazon.awssdk:netty-nio-client:2.11.0") - // When adding libraries, make sure to also add to the library/javaagent build files - // to ensure they are bumped to the latest version under testLatestDeps - api("software.amazon.awssdk:dynamodb:2.2.0") - api("software.amazon.awssdk:ec2:2.2.0") - api("software.amazon.awssdk:kinesis:2.2.0") - api("software.amazon.awssdk:rds:2.2.0") - api("software.amazon.awssdk:s3:2.2.0") - api("software.amazon.awssdk:sqs:2.2.0") + + // compileOnly because we never want to pin the low version implicitly; need to add dependencies + // explicitly in user projects, e.g. using testLatestDeps. + compileOnly("software.amazon.awssdk:dynamodb:2.2.0") + compileOnly("software.amazon.awssdk:ec2:2.2.0") + compileOnly("software.amazon.awssdk:kinesis:2.2.0") + compileOnly("software.amazon.awssdk:lambda:2.2.0") + compileOnly("software.amazon.awssdk:rds:2.2.0") + compileOnly("software.amazon.awssdk:s3:2.2.0") + compileOnly("software.amazon.awssdk:sqs:2.2.0") + compileOnly("software.amazon.awssdk:sns:2.2.0") + compileOnly("software.amazon.awssdk:ses:2.2.0") // 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("org.elasticmq:elasticmq-rest-sqs_2.13:1.5.1") implementation("com.google.guava:guava") diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/testing/src/main/groovy/io/opentelemetry/instrumentation/awssdk/v2_2/AbstractAws2ClientCoreTest.groovy b/instrumentation/aws-sdk/aws-sdk-2.2/testing/src/main/groovy/io/opentelemetry/instrumentation/awssdk/v2_2/AbstractAws2ClientCoreTest.groovy new file mode 100644 index 000000000000..a73122bc1af9 --- /dev/null +++ b/instrumentation/aws-sdk/aws-sdk-2.2/testing/src/main/groovy/io/opentelemetry/instrumentation/awssdk/v2_2/AbstractAws2ClientCoreTest.groovy @@ -0,0 +1,288 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.awssdk.v2_2 + +import io.opentelemetry.instrumentation.api.internal.ConfigPropertiesUtil +import io.opentelemetry.instrumentation.test.InstrumentationSpecification +import io.opentelemetry.semconv.incubating.RpcIncubatingAttributes +import io.opentelemetry.semconv.incubating.DbIncubatingAttributes +import io.opentelemetry.semconv.ServerAttributes +import io.opentelemetry.semconv.HttpAttributes +import io.opentelemetry.semconv.UrlAttributes +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 software.amazon.awssdk.auth.credentials.AwsBasicCredentials +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider +import software.amazon.awssdk.core.client.builder.SdkClientBuilder +import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration +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.* +import io.opentelemetry.testing.internal.armeria.testing.junit5.server.mock.MockWebServerExtension +import spock.lang.Shared +import spock.lang.Unroll + +import java.util.concurrent.Future + +import static com.google.common.collect.ImmutableMap.of +import static io.opentelemetry.api.trace.SpanKind.CLIENT + + +@Unroll +abstract class AbstractAws2ClientCoreTest extends InstrumentationSpecification { + static boolean isSqsAttributeInjectionEnabled() { + // See io.opentelemetry.instrumentation.awssdk.v2_2.autoconfigure.TracingExecutionInterceptor + return ConfigPropertiesUtil.getBoolean("otel.instrumentation.aws-sdk.experimental-use-propagator-for-messaging", false) + } + + 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) + } + + void configureSdkClient(SdkClientBuilder builder) { + builder.overrideConfiguration(createOverrideConfigurationBuilder().build()) + } + + abstract ClientOverrideConfiguration.Builder createOverrideConfigurationBuilder(); + + 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 { + "$ServerAttributes.SERVER_ADDRESS" "127.0.0.1" + "$ServerAttributes.SERVER_PORT" server.httpPort() + "$UrlAttributes.URL_FULL" { it.startsWith("${server.httpUri()}${path}") } + "$HttpAttributes.HTTP_REQUEST_METHOD" "$method" + "$HttpAttributes.HTTP_RESPONSE_STATUS_CODE" 200 + "$RpcIncubatingAttributes.RPC_SYSTEM" "aws-api" + "$RpcIncubatingAttributes.RPC_SERVICE" "DynamoDb" + "$RpcIncubatingAttributes.RPC_METHOD" "CreateTable" + "aws.agent" "java-aws-sdk" + "aws.requestId" "$requestId" + "aws.table.name" "sometable" + "$DbIncubatingAttributes.DB_SYSTEM" "dynamodb" + "$DbIncubatingAttributes.DB_OPERATION" "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 { + "$ServerAttributes.SERVER_ADDRESS" "127.0.0.1" + "$ServerAttributes.SERVER_PORT" server.httpPort() + "$UrlAttributes.URL_FULL" { it.startsWith("${server.httpUri()}${path}") } + "$HttpAttributes.HTTP_REQUEST_METHOD" "$method" + "$HttpAttributes.HTTP_RESPONSE_STATUS_CODE" 200 + "$RpcIncubatingAttributes.RPC_SYSTEM" "aws-api" + "$RpcIncubatingAttributes.RPC_SERVICE" "DynamoDb" + "$RpcIncubatingAttributes.RPC_METHOD" "Query" + "aws.agent" "java-aws-sdk" + "aws.requestId" "$requestId" + "aws.table.name" "sometable" + "$DbIncubatingAttributes.DB_SYSTEM" "dynamodb" + "$DbIncubatingAttributes.DB_OPERATION" "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 { + "$ServerAttributes.SERVER_ADDRESS" "127.0.0.1" + "$ServerAttributes.SERVER_PORT" server.httpPort() + "$UrlAttributes.URL_FULL" { it.startsWith("${server.httpUri()}${path}") } + "$HttpAttributes.HTTP_REQUEST_METHOD" "$method" + "$HttpAttributes.HTTP_RESPONSE_STATUS_CODE" 200 + "$RpcIncubatingAttributes.RPC_SYSTEM" "aws-api" + "$RpcIncubatingAttributes.RPC_SERVICE" "$service" + "$RpcIncubatingAttributes.RPC_METHOD" "${operation}" + "aws.agent" "java-aws-sdk" + "aws.requestId" "$requestId" + "aws.table.name" "sometable" + "$DbIncubatingAttributes.DB_SYSTEM" "dynamodb" + "$DbIncubatingAttributes.DB_OPERATION" "${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() + } +} diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/testing/src/main/groovy/io/opentelemetry/instrumentation/awssdk/v2_2/AbstractAws2ClientTest.groovy b/instrumentation/aws-sdk/aws-sdk-2.2/testing/src/main/groovy/io/opentelemetry/instrumentation/awssdk/v2_2/AbstractAws2ClientTest.groovy index a3fcdca522da..3241c48640bb 100644 --- a/instrumentation/aws-sdk/aws-sdk-2.2/testing/src/main/groovy/io/opentelemetry/instrumentation/awssdk/v2_2/AbstractAws2ClientTest.groovy +++ b/instrumentation/aws-sdk/aws-sdk-2.2/testing/src/main/groovy/io/opentelemetry/instrumentation/awssdk/v2_2/AbstractAws2ClientTest.groovy @@ -5,35 +5,24 @@ package io.opentelemetry.instrumentation.awssdk.v2_2 -import io.opentelemetry.instrumentation.test.InstrumentationSpecification -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes + +import io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes +import io.opentelemetry.semconv.incubating.RpcIncubatingAttributes +import io.opentelemetry.semconv.ServerAttributes +import io.opentelemetry.semconv.HttpAttributes +import io.opentelemetry.semconv.UrlAttributes +import io.opentelemetry.testing.internal.armeria.common.HttpData 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 software.amazon.awssdk.auth.credentials.AwsBasicCredentials -import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider +import io.opentelemetry.testing.internal.armeria.common.ResponseHeaders +import org.junit.jupiter.api.Assumptions 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.ClientOverrideConfiguration 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 @@ -45,289 +34,74 @@ 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.sns.SnsAsyncClient +import software.amazon.awssdk.services.sns.SnsClient +import software.amazon.awssdk.services.sns.model.PublishRequest import software.amazon.awssdk.services.sqs.SqsAsyncClient import software.amazon.awssdk.services.sqs.SqsClient import software.amazon.awssdk.services.sqs.model.CreateQueueRequest -import spock.lang.Shared +import software.amazon.awssdk.services.sqs.model.SendMessageRequest import spock.lang.Unroll +import java.nio.charset.StandardCharsets import java.time.Duration import java.util.concurrent.Future -import static com.google.common.collect.ImmutableMap.of import static io.opentelemetry.api.trace.SpanKind.CLIENT import static io.opentelemetry.api.trace.SpanKind.PRODUCER import static io.opentelemetry.api.trace.StatusCode.ERROR @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) - } - - void configureSdkClient(SdkClientBuilder builder) { - builder.overrideConfiguration(createOverrideConfigurationBuilder().build()) - } - - abstract ClientOverrideConfiguration.Builder createOverrideConfigurationBuilder(); - - 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()) +abstract class AbstractAws2ClientTest extends AbstractAws2ClientCoreTest { + static final String QUEUE_URL = "http://xxx/somequeue" + + void assumeSupportedConfig(service, operation) { + Assumptions.assumeFalse( + service == "Sqs" + && operation == "SendMessage" + && isSqsAttributeInjectionEnabled(), + "Cannot check Sqs.SendMessage here due to hard-coded MD5.") } - def assertCreateTableRequest(path, method, requestId) { - assertTraces(1) { - trace(0, 1) { - span(0) { - name "DynamoDb.CreateTable" - kind CLIENT - hasNoParent() - attributes { - "$SemanticAttributes.NET_PEER_NAME" "127.0.0.1" - "$SemanticAttributes.NET_PEER_PORT" server.httpPort() - "$SemanticAttributes.HTTP_URL" { it.startsWith("${server.httpUri()}${path}") } - "$SemanticAttributes.HTTP_METHOD" "$method" - "$SemanticAttributes.HTTP_STATUS_CODE" 200 - "$SemanticAttributes.USER_AGENT_ORIGINAL" { it.startsWith("aws-sdk-java/") } - "$SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH" { it == null || it instanceof Long } - "$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" { it == null || it instanceof Long } - "$SemanticAttributes.RPC_SYSTEM" "aws-api" - "$SemanticAttributes.RPC_SERVICE" "DynamoDb" - "$SemanticAttributes.RPC_METHOD" "CreateTable" - "aws.agent" "java-aws-sdk" - "aws.requestId" "$requestId" - "aws.table.name" "sometable" - "$SemanticAttributes.DB_SYSTEM" "dynamodb" - "$SemanticAttributes.DB_OPERATION" "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 - } + // Force localhost instead of relying on mock server because using ip is yet another corner case of the virtual + // bucket changes introduced by aws sdk v2.18.0. When using IP, there is no way to prefix the hostname with the + // bucket name as label. + def clientUri = URI.create("http://localhost:${server.httpPort()}") - def assertQueryRequest(path, method, requestId) { - assertTraces(1) { - trace(0, 1) { - span(0) { - name "DynamoDb.Query" - kind CLIENT - hasNoParent() - attributes { - "$SemanticAttributes.NET_PEER_NAME" "127.0.0.1" - "$SemanticAttributes.NET_PEER_PORT" server.httpPort() - "$SemanticAttributes.HTTP_URL" { it.startsWith("${server.httpUri()}${path}") } - "$SemanticAttributes.HTTP_METHOD" "$method" - "$SemanticAttributes.HTTP_STATUS_CODE" 200 - "$SemanticAttributes.USER_AGENT_ORIGINAL" { it.startsWith("aws-sdk-java/") } - "$SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH" { it == null || it instanceof Long } - "$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" { it == null || it instanceof Long } - "$SemanticAttributes.RPC_SYSTEM" "aws-api" - "$SemanticAttributes.RPC_SERVICE" "DynamoDb" - "$SemanticAttributes.RPC_METHOD" "Query" - "aws.agent" "java-aws-sdk" - "aws.requestId" "$requestId" - "aws.table.name" "sometable" - "$SemanticAttributes.DB_SYSTEM" "dynamodb" - "$SemanticAttributes.DB_OPERATION" "Query" - "aws.dynamodb.limit" "10" - "aws.dynamodb.select" "ALL_ATTRIBUTES" - } - } - } + def s3ClientBuilder() { + def builder = S3Client.builder() + if (Boolean.getBoolean("testLatestDeps")) { + builder.forcePathStyle(true) } - def request = server.takeRequest() - request.request().headers().get("X-Amzn-Trace-Id") != null - request.request().headers().get("traceparent") == null + return builder } - def assertDynamoDbRequest(service, operation, path, method, requestId) { - assertTraces(1) { - trace(0, 1) { - span(0) { - name "$service.$operation" - kind CLIENT - hasNoParent() - attributes { - "$SemanticAttributes.NET_PEER_NAME" "127.0.0.1" - "$SemanticAttributes.NET_PEER_PORT" server.httpPort() - "$SemanticAttributes.HTTP_URL" { it.startsWith("${server.httpUri()}${path}") } - "$SemanticAttributes.HTTP_METHOD" "$method" - "$SemanticAttributes.HTTP_STATUS_CODE" 200 - "$SemanticAttributes.USER_AGENT_ORIGINAL" { it.startsWith("aws-sdk-java/") } - "$SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH" { it == null || it instanceof Long } - "$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" { it == null || it instanceof Long } - "$SemanticAttributes.RPC_SYSTEM" "aws-api" - "$SemanticAttributes.RPC_SERVICE" "$service" - "$SemanticAttributes.RPC_METHOD" "${operation}" - "aws.agent" "java-aws-sdk" - "aws.requestId" "$requestId" - "aws.table.name" "sometable" - "$SemanticAttributes.DB_SYSTEM" "dynamodb" - "$SemanticAttributes.DB_OPERATION" "${operation}" - } - } - } + def s3AsyncClientBuilder() { + def builder = S3AsyncClient.builder() + if (Boolean.getBoolean("testLatestDeps")) { + builder.forcePathStyle(true) } - 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() + return builder } def "send #operation request with builder #builder.class.getName() mocked response"() { + assumeSupportedConfig(service, operation) + 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) + .endpointOverride(clientUri) + .region(Region.AP_NORTHEAST_1) + .credentialsProvider(CREDENTIALS_PROVIDER) + .build() + + if (body instanceof Closure) { + server.enqueue(body.call()) + } else { + server.enqueue(HttpResponse.of(HttpStatus.OK, MediaType.PLAIN_TEXT_UTF_8, body)) + } + def response = call.call(client) if (response instanceof Future) { response = response.get() } @@ -339,21 +113,27 @@ abstract class AbstractAws2ClientTest extends InstrumentationSpecification { assertTraces(1) { trace(0, 1) { span(0) { - name "$service.$operation" + name operation != "SendMessage" ? "$service.$operation" : "somequeue publish" kind operation != "SendMessage" ? CLIENT : PRODUCER hasNoParent() attributes { - "$SemanticAttributes.NET_PEER_NAME" "127.0.0.1" - "$SemanticAttributes.NET_PEER_PORT" server.httpPort() - "$SemanticAttributes.HTTP_URL" { it.startsWith("${server.httpUri()}${path}") } - "$SemanticAttributes.HTTP_METHOD" "$method" - "$SemanticAttributes.HTTP_STATUS_CODE" 200 - "$SemanticAttributes.USER_AGENT_ORIGINAL" { it.startsWith("aws-sdk-java/") } - "$SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH" { it == null || it instanceof Long } - "$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" { it == null || it instanceof Long } - "$SemanticAttributes.RPC_SYSTEM" "aws-api" - "$SemanticAttributes.RPC_SERVICE" "$service" - "$SemanticAttributes.RPC_METHOD" "${operation}" + if (service == "S3") { + // Starting with AWS SDK V2 2.18.0, the s3 sdk will prefix the hostname with the bucket name in case + // the bucket name is a valid DNS label, even in the case that we are using an endpoint override. + // Previously the sdk was only doing that if endpoint had "s3" as label in the FQDN. + // Our test assert both cases so that we don't need to know what version is being tested. + "$ServerAttributes.SERVER_ADDRESS" { it == "somebucket.localhost" || it == "localhost" } + "$UrlAttributes.URL_FULL" { it.startsWith("http://somebucket.localhost:${server.httpPort()}") || it.startsWith("http://localhost:${server.httpPort()}/somebucket") } + } else { + "$ServerAttributes.SERVER_ADDRESS" "localhost" + "$UrlAttributes.URL_FULL" { it.startsWith("http://localhost:${server.httpPort()}") } + } + "$ServerAttributes.SERVER_PORT" server.httpPort() + "$HttpAttributes.HTTP_REQUEST_METHOD" "$method" + "$HttpAttributes.HTTP_RESPONSE_STATUS_CODE" 200 + "$RpcIncubatingAttributes.RPC_SYSTEM" "aws-api" + "$RpcIncubatingAttributes.RPC_SERVICE" "$service" + "$RpcIncubatingAttributes.RPC_METHOD" "${operation}" "aws.agent" "java-aws-sdk" "aws.requestId" "$requestId" if (service == "S3") { @@ -361,9 +141,15 @@ abstract class AbstractAws2ClientTest extends InstrumentationSpecification { } else if (service == "Sqs" && operation == "CreateQueue") { "aws.queue.name" "somequeue" } else if (service == "Sqs" && operation == "SendMessage") { - "aws.queue.url" "someurl" + "aws.queue.url" QUEUE_URL + "$MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME" "somequeue" + "$MessagingIncubatingAttributes.MESSAGING_OPERATION" "publish" + "$MessagingIncubatingAttributes.MESSAGING_MESSAGE_ID" String + "$MessagingIncubatingAttributes.MESSAGING_SYSTEM" MessagingIncubatingAttributes.MessagingSystemValues.AWS_SQS } else if (service == "Kinesis") { "aws.stream.name" "somestream" + } else if (service == "Sns") { + "$MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME" "somearn" } } } @@ -374,24 +160,86 @@ abstract class AbstractAws2ClientTest extends InstrumentationSpecification { request.request().headers().get("traceparent") == null where: - service | operation | method | path | requestId | builder | call | body - "S3" | "CreateBucket" | "PUT" | path("somebucket") | "UNKNOWN" | S3Client.builder() | { c -> c.createBucket(CreateBucketRequest.builder().bucket("somebucket").build()) } | "" - "S3" | "GetObject" | "GET" | path("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 - - """ - "Ec2" | "AllocateAddress" | "POST" | "" | "59dbff89-35bd-4eac-99ed-be587EXAMPLE" | Ec2Client.builder() | { c -> c.allocateAddress() } | """ + service | operation | method | requestId | builder | call | body + "S3" | "CreateBucket" | "PUT" | "UNKNOWN" | s3ClientBuilder() | { c -> c.createBucket(CreateBucketRequest.builder().bucket("somebucket").build()) } | "" + "S3" | "GetObject" | "GET" | "UNKNOWN" | s3ClientBuilder() | { c -> c.getObject(GetObjectRequest.builder().bucket("somebucket").key("somekey").build()) } | "" + "Kinesis" | "DeleteStream" | "POST" | "UNKNOWN" | KinesisClient.builder() | { c -> c.deleteStream(DeleteStreamRequest.builder().streamName("somestream").build()) } | "" + "Sns" | "Publish" | "POST" | "d74b8436-ae13-5ab4-a9ff-ce54dfea72a0" | SnsClient.builder() | { c -> c.publish(PublishRequest.builder().message("somemessage").topicArn("somearn").build()) } | """ + + + 567910cd-659e-55d4-8ccb-5aaf14679dc0 + + + d74b8436-ae13-5ab4-a9ff-ce54dfea72a0 + + + """ + "Sns" | "Publish" | "POST" | "d74b8436-ae13-5ab4-a9ff-ce54dfea72a0" | SnsClient.builder() | { c -> c.publish(PublishRequest.builder().message("somemessage").targetArn("somearn").build()) } | """ + + + 567910cd-659e-55d4-8ccb-5aaf14679dc0 + + + d74b8436-ae13-5ab4-a9ff-ce54dfea72a0 + + + """ + "Sqs" | "CreateQueue" | "POST" | "7a62c49f-347e-4fc4-9331-6e8e7a96aa73" | SqsClient.builder() | { c -> c.createQueue(CreateQueueRequest.builder().queueName("somequeue").build()) } | { + if (!Boolean.getBoolean("testLatestDeps")) { + def content = """ + + https://queue.amazonaws.com/123456789012/MyQueue + 7a62c49f-347e-4fc4-9331-6e8e7a96aa73 + + """ + return HttpResponse.of(HttpStatus.OK, MediaType.PLAIN_TEXT_UTF_8, content) + } + def content = """ + { + "QueueUrl":"https://queue.amazonaws.com/123456789012/MyQueue" + } + """ + ResponseHeaders headers = ResponseHeaders.builder(HttpStatus.OK) + .contentType(MediaType.PLAIN_TEXT_UTF_8) + .add("x-amzn-RequestId", "7a62c49f-347e-4fc4-9331-6e8e7a96aa73") + .build() + return HttpResponse.of(headers, HttpData.of(StandardCharsets.UTF_8, content)) + } + "Sqs" | "SendMessage" | "POST" | "27daac76-34dd-47df-bd01-1f6e873584a0" | SqsClient.builder() | { c -> c.sendMessage(SendMessageRequest.builder().queueUrl(QUEUE_URL).messageBody("").build()) } | { + if (!Boolean.getBoolean("testLatestDeps")) { + def content = """ + + + d41d8cd98f00b204e9800998ecf8427e + 3ae8f24a165a8cedc005670c81a27295 + 5fea7756-0ea4-451a-a703-a558b933e274 + + 27daac76-34dd-47df-bd01-1f6e873584a0 + + """ + return HttpResponse.of(HttpStatus.OK, MediaType.PLAIN_TEXT_UTF_8, content) + } + def content = """ + { + "MD5OfMessageBody":"d41d8cd98f00b204e9800998ecf8427e", + "MD5OfMessageAttributes":"3ae8f24a165a8cedc005670c81a27295", + "MessageId":"5fea7756-0ea4-451a-a703-a558b933e274" + } + """ + ResponseHeaders headers = ResponseHeaders.builder(HttpStatus.OK) + .contentType(MediaType.PLAIN_TEXT_UTF_8) + .add("x-amzn-RequestId", "27daac76-34dd-47df-bd01-1f6e873584a0") + .build() + return HttpResponse.of(headers, HttpData.of(StandardCharsets.UTF_8, content)) + } + "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()) } | """ + "Rds" | "DeleteOptionGroup" | "POST" | "0ac9cda2-bbf4-11d3-f92b-31fa5e8dbc99" | RdsClient.builder() | { c -> c.deleteOptionGroup(DeleteOptionGroupRequest.builder().build()) } | """ 0ac9cda2-bbf4-11d3-f92b-31fa5e8dbc99 @@ -399,16 +247,22 @@ abstract class AbstractAws2ClientTest extends InstrumentationSpecification { } def "send #operation async request with builder #builder.class.getName() mocked response"() { + assumeSupportedConfig(service, operation) 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) + .endpointOverride(clientUri) + .region(Region.AP_NORTHEAST_1) + .credentialsProvider(CREDENTIALS_PROVIDER) + .build() + + if (body instanceof Closure) { + server.enqueue(body.call()) + } else { + server.enqueue(HttpResponse.of(HttpStatus.OK, MediaType.PLAIN_TEXT_UTF_8, body)) + } + def response = call.call(client) if (response instanceof Future) { response = response.get() } @@ -419,21 +273,27 @@ abstract class AbstractAws2ClientTest extends InstrumentationSpecification { assertTraces(1) { trace(0, 1) { span(0) { - name "$service.$operation" + name operation != "SendMessage" ? "$service.$operation" : "somequeue publish" kind operation != "SendMessage" ? CLIENT : PRODUCER hasNoParent() attributes { - "$SemanticAttributes.NET_PEER_NAME" "127.0.0.1" - "$SemanticAttributes.NET_PEER_PORT" server.httpPort() - "$SemanticAttributes.HTTP_URL" { it.startsWith("${server.httpUri()}${path}") } - "$SemanticAttributes.HTTP_METHOD" "$method" - "$SemanticAttributes.HTTP_STATUS_CODE" 200 - "$SemanticAttributes.USER_AGENT_ORIGINAL" { it.startsWith("aws-sdk-java/") } - "$SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH" { it == null || it instanceof Long } - "$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" { it == null || it instanceof Long } - "$SemanticAttributes.RPC_SYSTEM" "aws-api" - "$SemanticAttributes.RPC_SERVICE" "$service" - "$SemanticAttributes.RPC_METHOD" "${operation}" + if (service == "S3") { + // Starting with AWS SDK V2 2.18.0, the s3 sdk will prefix the hostname with the bucket name in case + // the bucket name is a valid DNS label, even in the case that we are using an endpoint override. + // Previously the sdk was only doing that if endpoint had "s3" as label in the FQDN. + // Our test assert both cases so that we don't need to know what version is being tested. + "$ServerAttributes.SERVER_ADDRESS" { it == "somebucket.localhost" || it == "localhost" } + "$UrlAttributes.URL_FULL" { it.startsWith("http://somebucket.localhost:${server.httpPort()}") || it.startsWith("http://localhost:${server.httpPort()}") } + } else { + "$ServerAttributes.SERVER_ADDRESS" "localhost" + "$UrlAttributes.URL_FULL" { it == "http://localhost:${server.httpPort()}" || it == "http://localhost:${server.httpPort()}/" } + } + "$ServerAttributes.SERVER_PORT" server.httpPort() + "$HttpAttributes.HTTP_REQUEST_METHOD" "$method" + "$HttpAttributes.HTTP_RESPONSE_STATUS_CODE" 200 + "$RpcIncubatingAttributes.RPC_SYSTEM" "aws-api" + "$RpcIncubatingAttributes.RPC_SERVICE" "$service" + "$RpcIncubatingAttributes.RPC_METHOD" "${operation}" "aws.agent" "java-aws-sdk" "aws.requestId" "$requestId" if (service == "S3") { @@ -441,9 +301,15 @@ abstract class AbstractAws2ClientTest extends InstrumentationSpecification { } else if (service == "Sqs" && operation == "CreateQueue") { "aws.queue.name" "somequeue" } else if (service == "Sqs" && operation == "SendMessage") { - "aws.queue.url" "someurl" + "aws.queue.url" QUEUE_URL + "$MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME" "somequeue" + "$MessagingIncubatingAttributes.MESSAGING_OPERATION" "publish" + "$MessagingIncubatingAttributes.MESSAGING_MESSAGE_ID" String + "$MessagingIncubatingAttributes.MESSAGING_SYSTEM" MessagingIncubatingAttributes.MessagingSystemValues.AWS_SQS } else if (service == "Kinesis") { "aws.stream.name" "somestream" + } else if (service == "Sns") { + "$MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME" "somearn" } } } @@ -453,30 +319,93 @@ abstract class AbstractAws2ClientTest extends InstrumentationSpecification { request.request().headers().get("X-Amzn-Trace-Id") != null request.request().headers().get("traceparent") == null + if (service == "Sns" && operation == "Publish") { + def content = request.request().content().toStringUtf8() + def containsId = content.contains("${traces[0][0].traceId}-${traces[0][0].spanId}") + def containsTp = content.contains("=traceparent") + if (isSqsAttributeInjectionEnabled()) { + assert containsId && containsTp + } else { + assert !containsId && !containsTp + } + } + where: - service | operation | method | path | requestId | builder | call | body - "S3" | "CreateBucket" | "PUT" | path("somebucket") | "UNKNOWN" | S3AsyncClient.builder() | { c -> c.createBucket(CreateBucketRequest.builder().bucket("somebucket").build()) } | "" - "S3" | "GetObject" | "GET" | path("somebucket", "somekey") | "UNKNOWN" | S3AsyncClient.builder() | { c -> c.getObject(GetObjectRequest.builder().bucket("somebucket").key("somekey").build(), AsyncResponseTransformer.toBytes()) } | "1234567890" + service | operation | method | requestId | builder | call | body + "S3" | "CreateBucket" | "PUT" | "UNKNOWN" | s3AsyncClientBuilder() | { c -> c.createBucket(CreateBucketRequest.builder().bucket("somebucket").build()) } | "" + "S3" | "GetObject" | "GET" | "UNKNOWN" | s3AsyncClientBuilder() | { 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 - - """ - "Ec2" | "AllocateAddress" | "POST" | "" | "59dbff89-35bd-4eac-99ed-be587EXAMPLE" | Ec2AsyncClient.builder() | { c -> c.allocateAddress() } | """ + "Sqs" | "CreateQueue" | "POST" | "7a62c49f-347e-4fc4-9331-6e8e7a96aa73" | SqsAsyncClient.builder() | { c -> c.createQueue(CreateQueueRequest.builder().queueName("somequeue").build()) } | { + if (!Boolean.getBoolean("testLatestDeps")) { + def content = """ + + https://queue.amazonaws.com/123456789012/MyQueue + 7a62c49f-347e-4fc4-9331-6e8e7a96aa73 + + """ + return HttpResponse.of(HttpStatus.OK, MediaType.PLAIN_TEXT_UTF_8, content) + } + def content = """ + { + "QueueUrl":"https://queue.amazonaws.com/123456789012/MyQueue" + } + """ + ResponseHeaders headers = ResponseHeaders.builder(HttpStatus.OK) + .contentType(MediaType.PLAIN_TEXT_UTF_8) + .add("x-amzn-RequestId", "7a62c49f-347e-4fc4-9331-6e8e7a96aa73") + .build() + return HttpResponse.of(headers, HttpData.of(StandardCharsets.UTF_8, content)) + } + "Sqs" | "SendMessage" | "POST" | "27daac76-34dd-47df-bd01-1f6e873584a0" | SqsAsyncClient.builder() | { c -> c.sendMessage(SendMessageRequest.builder().queueUrl(QUEUE_URL).messageBody("").build()) } | { + if (!Boolean.getBoolean("testLatestDeps")) { + def content = """ + + + d41d8cd98f00b204e9800998ecf8427e + 3ae8f24a165a8cedc005670c81a27295 + 5fea7756-0ea4-451a-a703-a558b933e274 + + 27daac76-34dd-47df-bd01-1f6e873584a0 + + """ + return HttpResponse.of(HttpStatus.OK, MediaType.PLAIN_TEXT_UTF_8, content) + } + def content = """ + { + "MD5OfMessageBody":"d41d8cd98f00b204e9800998ecf8427e", + "MD5OfMessageAttributes":"3ae8f24a165a8cedc005670c81a27295", + "MessageId":"5fea7756-0ea4-451a-a703-a558b933e274" + } + """ + ResponseHeaders headers = ResponseHeaders.builder(HttpStatus.OK) + .contentType(MediaType.PLAIN_TEXT_UTF_8) + .add("x-amzn-RequestId", "27daac76-34dd-47df-bd01-1f6e873584a0") + .build() + return HttpResponse.of(headers, HttpData.of(StandardCharsets.UTF_8, content)) + } + "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()) } | """ + "Rds" | "DeleteOptionGroup" | "POST" | "0ac9cda2-bbf4-11d3-f92b-31fa5e8dbc99" | RdsAsyncClient.builder() | { c -> c.deleteOptionGroup(DeleteOptionGroupRequest.builder().build()) } | """ 0ac9cda2-bbf4-11d3-f92b-31fa5e8dbc99 """ + "Sns" | "Publish" | "POST" | "f187a3c1-376f-11df-8963-01868b7c937a" | SnsAsyncClient.builder() | { SnsAsyncClient c -> c.publish(r -> r.message("hello").topicArn("somearn")) } | """ + + + 94f20ce6-13c5-43a0-9a9e-ca52d816e90b + + + f187a3c1-376f-11df-8963-01868b7c937a + + + """ } // TODO(anuraaga): Without AOP instrumentation of the HTTP client, we cannot model retries as @@ -487,15 +416,20 @@ abstract class AbstractAws2ClientTest extends InstrumentationSpecification { // One retry so two requests. server.enqueue(HttpResponse.delayed(HttpResponse.of(HttpStatus.OK), Duration.ofMillis(5000))) server.enqueue(HttpResponse.delayed(HttpResponse.of(HttpStatus.OK), Duration.ofMillis(5000))) - def client = S3Client.builder() - .overrideConfiguration(createOverrideConfigurationBuilder() - .retryPolicy(RetryPolicy.builder().numRetries(1).build()) - .build()) - .endpointOverride(server.httpUri()) - .region(Region.AP_NORTHEAST_1) - .credentialsProvider(CREDENTIALS_PROVIDER) - .httpClientBuilder(ApacheHttpClient.builder().socketTimeout(Duration.ofMillis(50))) - .build() + def builder = S3Client.builder() + .overrideConfiguration(createOverrideConfigurationBuilder() + .retryPolicy(RetryPolicy.builder().numRetries(1).build()) + .build()) + .endpointOverride(clientUri) + .region(Region.AP_NORTHEAST_1) + .credentialsProvider(CREDENTIALS_PROVIDER) + .httpClientBuilder(ApacheHttpClient.builder().socketTimeout(Duration.ofMillis(50))) + + if (Boolean.getBoolean("testLatestDeps")) { + builder.forcePathStyle(true) + } + + def client = builder.build() when: client.getObject(GetObjectRequest.builder().bucket("somebucket").key("somekey").build()) @@ -503,7 +437,6 @@ abstract class AbstractAws2ClientTest extends InstrumentationSpecification { then: thrown SdkClientException - def path = path("somebucket", "somekey") assertTraces(1) { trace(0, 1) { span(0) { @@ -513,13 +446,17 @@ abstract class AbstractAws2ClientTest extends InstrumentationSpecification { errorEvent SdkClientException, "Unable to execute HTTP request: Read timed out" hasNoParent() attributes { - "$SemanticAttributes.NET_PEER_NAME" "127.0.0.1" - "$SemanticAttributes.NET_PEER_PORT" server.httpPort() - "$SemanticAttributes.HTTP_URL" "${server.httpUri()}${path}" - "$SemanticAttributes.HTTP_METHOD" "GET" - "$SemanticAttributes.RPC_SYSTEM" "aws-api" - "$SemanticAttributes.RPC_SERVICE" "S3" - "$SemanticAttributes.RPC_METHOD" "GetObject" + // Starting with AWS SDK V2 2.18.0, the s3 sdk will prefix the hostname with the bucket name in case + // the bucket name is a valid DNS label, even in the case that we are using an endpoint override. + // Previously the sdk was only doing that if endpoint had "s3" as label in the FQDN. + // Our test assert both cases so that we don't need to know what version is being tested. + "$ServerAttributes.SERVER_ADDRESS" { it == "somebucket.localhost" || it == "localhost" } + "$UrlAttributes.URL_FULL" { it == "http://somebucket.localhost:${server.httpPort()}/somekey" || it == "http://localhost:${server.httpPort()}/somebucket/somekey" } + "$ServerAttributes.SERVER_PORT" server.httpPort() + "$HttpAttributes.HTTP_REQUEST_METHOD" "GET" + "$RpcIncubatingAttributes.RPC_SYSTEM" "aws-api" + "$RpcIncubatingAttributes.RPC_SERVICE" "S3" + "$RpcIncubatingAttributes.RPC_METHOD" "GetObject" "aws.agent" "java-aws-sdk" "aws.bucket.name" "somebucket" } @@ -527,16 +464,4 @@ abstract class AbstractAws2ClientTest extends InstrumentationSpecification { } } } - - static String path(String bucket, String path = null) { - def result = "" - // since 2.18.0 bucket name is not present in request path - if (!Boolean.getBoolean("testLatestDeps") && !bucket.isEmpty()) { - result = "/" + bucket - } - if (path != null && !path.isEmpty()) { - result += "/" + path - } - return result - } } diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/testing/src/main/groovy/io/opentelemetry/instrumentation/awssdk/v2_2/AbstractAws2SqsSuppressReceiveSpansTest.groovy b/instrumentation/aws-sdk/aws-sdk-2.2/testing/src/main/groovy/io/opentelemetry/instrumentation/awssdk/v2_2/AbstractAws2SqsSuppressReceiveSpansTest.groovy new file mode 100644 index 000000000000..6f44c92573d6 --- /dev/null +++ b/instrumentation/aws-sdk/aws-sdk-2.2/testing/src/main/groovy/io/opentelemetry/instrumentation/awssdk/v2_2/AbstractAws2SqsSuppressReceiveSpansTest.groovy @@ -0,0 +1,392 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.awssdk.v2_2 + +import io.opentelemetry.instrumentation.test.InstrumentationSpecification +import io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes +import io.opentelemetry.semconv.ServerAttributes +import io.opentelemetry.semconv.HttpAttributes +import io.opentelemetry.semconv.UrlAttributes +import org.elasticmq.rest.sqs.SQSRestServerBuilder +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider +import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration +import software.amazon.awssdk.regions.Region +import software.amazon.awssdk.services.sqs.SqsAsyncClient +import software.amazon.awssdk.services.sqs.SqsBaseClientBuilder +import software.amazon.awssdk.services.sqs.SqsClient +import software.amazon.awssdk.services.sqs.model.CreateQueueRequest +import software.amazon.awssdk.services.sqs.model.MessageAttributeValue +import software.amazon.awssdk.services.sqs.model.ReceiveMessageRequest +import software.amazon.awssdk.services.sqs.model.SendMessageBatchRequest +import software.amazon.awssdk.services.sqs.model.SendMessageRequest +import spock.lang.Shared + +import static io.opentelemetry.api.trace.SpanKind.CLIENT +import static io.opentelemetry.api.trace.SpanKind.CONSUMER +import static io.opentelemetry.api.trace.SpanKind.PRODUCER + +abstract class AbstractAws2SqsSuppressReceiveSpansTest extends InstrumentationSpecification { + + private static final StaticCredentialsProvider CREDENTIALS_PROVIDER = StaticCredentialsProvider + .create(AwsBasicCredentials.create("my-access-key", "my-secret-key")) + + @Shared + def sqs + + @Shared + int sqsPort + + static Map dummyMessageAttributes(count) { + (0.. e.messageBody("e1").id("i1"), + // 8 attributes, injection always possible + e -> e.messageBody("e2").id("i2") + .messageAttributes(dummyMessageAttributes(8)), + // 10 attributes, injection with custom propagator never possible + e -> e.messageBody("e3").id("i3").messageAttributes(dummyMessageAttributes(10))) + .build() + + boolean isSqsAttributeInjectionEnabled() { + AbstractAws2ClientCoreTest.isSqsAttributeInjectionEnabled() + } + + boolean isXrayInjectionEnabled() { + true + } + + void configureSdkClient(SqsBaseClientBuilder builder) { + builder + .overrideConfiguration(createOverrideConfigurationBuilder().build()) + .endpointOverride(new URI("http://localhost:" + sqsPort)) + builder + .region(Region.AP_NORTHEAST_1) + .credentialsProvider(CREDENTIALS_PROVIDER) + } + + abstract SqsClient configureSqsClient(SqsClient sqsClient) + + abstract SqsAsyncClient configureSqsClient(SqsAsyncClient sqsClient) + + abstract ClientOverrideConfiguration.Builder createOverrideConfigurationBuilder() + + def setupSpec() { + sqs = SQSRestServerBuilder.withPort(0).withInterface("localhost").start() + def server = sqs.waitUntilStarted() + sqsPort = server.localAddress().port + println getClass().name + " SQS server started at: localhost:$sqsPort/" + } + + def cleanupSpec() { + if (sqs != null) { + sqs.stopAndWait() + } + } + + void assertSqsTraces(withParent = false) { + assertTraces(2 + (withParent ? 1 : 0)) { + trace(0, 1) { + + span(0) { + name "Sqs.CreateQueue" + kind CLIENT + hasNoParent() + attributes { + "aws.agent" "java-aws-sdk" + "aws.queue.name" "testSdkSqs" + "aws.requestId" { it == "00000000-0000-0000-0000-000000000000" || it == "UNKNOWN" } + "rpc.system" "aws-api" + "rpc.service" "Sqs" + "rpc.method" "CreateQueue" + "$HttpAttributes.HTTP_REQUEST_METHOD" "POST" + "$HttpAttributes.HTTP_RESPONSE_STATUS_CODE" 200 + "$UrlAttributes.URL_FULL" { it.startsWith("http://localhost:$sqsPort") } + "$ServerAttributes.SERVER_ADDRESS" "localhost" + "$ServerAttributes.SERVER_PORT" sqsPort + } + } + } + trace(1, 3) { + span(0) { + name "testSdkSqs publish" + kind PRODUCER + hasNoParent() + attributes { + "aws.agent" "java-aws-sdk" + "aws.queue.url" "http://localhost:$sqsPort/000000000000/testSdkSqs" + "aws.requestId" { it == "00000000-0000-0000-0000-000000000000" || it == "UNKNOWN" } + "rpc.system" "aws-api" + "rpc.method" "SendMessage" + "rpc.service" "Sqs" + "$HttpAttributes.HTTP_REQUEST_METHOD" "POST" + "$HttpAttributes.HTTP_RESPONSE_STATUS_CODE" 200 + "$UrlAttributes.URL_FULL" { it.startsWith("http://localhost:$sqsPort") } + "$ServerAttributes.SERVER_ADDRESS" "localhost" + "$ServerAttributes.SERVER_PORT" sqsPort + "$MessagingIncubatingAttributes.MESSAGING_SYSTEM" MessagingIncubatingAttributes.MessagingSystemValues.AWS_SQS + "$MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME" "testSdkSqs" + "$MessagingIncubatingAttributes.MESSAGING_OPERATION" "publish" + "$MessagingIncubatingAttributes.MESSAGING_MESSAGE_ID" String + } + } + span(1) { + name "testSdkSqs process" + kind CONSUMER + childOf span(0) + hasNoLinks() + attributes { + "aws.agent" "java-aws-sdk" + "rpc.method" "ReceiveMessage" + "rpc.system" "aws-api" + "rpc.service" "Sqs" + "$HttpAttributes.HTTP_REQUEST_METHOD" "POST" + "$HttpAttributes.HTTP_RESPONSE_STATUS_CODE" 200 + "$UrlAttributes.URL_FULL" { it.startsWith("http://localhost:$sqsPort") } + "$ServerAttributes.SERVER_ADDRESS" "localhost" + "$ServerAttributes.SERVER_PORT" sqsPort + "$MessagingIncubatingAttributes.MESSAGING_SYSTEM" MessagingIncubatingAttributes.MessagingSystemValues.AWS_SQS + "$MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME" "testSdkSqs" + "$MessagingIncubatingAttributes.MESSAGING_OPERATION" "process" + "$MessagingIncubatingAttributes.MESSAGING_MESSAGE_ID" String + } + } + span(2) { + name "process child" + childOf span(1) + attributes { + } + } + } + if (withParent) { + /** + * 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, 2) { + span(0) { + name "parent" + hasNoParent() + } + span(1) { + name "Sqs.ReceiveMessage" + kind CLIENT + childOf span(0) + hasNoLinks() + attributes { + "aws.agent" "java-aws-sdk" + "aws.requestId" { it == "00000000-0000-0000-0000-000000000000" || it == "UNKNOWN" } + "rpc.method" "ReceiveMessage" + "aws.queue.url" "http://localhost:$sqsPort/000000000000/testSdkSqs" + "rpc.system" "aws-api" + "rpc.service" "Sqs" + "$HttpAttributes.HTTP_REQUEST_METHOD" "POST" + "$HttpAttributes.HTTP_RESPONSE_STATUS_CODE" 200 + "$UrlAttributes.URL_FULL" { it.startsWith("http://localhost:$sqsPort") } + "$ServerAttributes.SERVER_ADDRESS" "localhost" + "$ServerAttributes.SERVER_PORT" sqsPort + } + } + } + } + } + } + + def "simple sqs producer-consumer services: sync"() { + setup: + def builder = SqsClient.builder() + configureSdkClient(builder) + def client = configureSqsClient(builder.build()) + + client.createQueue(createQueueRequest) + + when: + client.sendMessage(sendMessageRequest) + + def resp = client.receiveMessage(receiveMessageRequest) + + then: + resp.messages.size() == 1 + resp.messages.each {message -> runWithSpan("process child") {}} + assertSqsTraces() + } + + def "simple sqs producer-consumer services with parent: sync"() { + setup: + def builder = SqsClient.builder() + configureSdkClient(builder) + def client = configureSqsClient(builder.build()) + + client.createQueue(createQueueRequest) + + when: + client.sendMessage(sendMessageRequest) + + def resp = runWithSpan("parent") { + client.receiveMessage(receiveMessageRequest) + } + + then: + resp.messages.size() == 1 + resp.messages.each {message -> runWithSpan("process child") {}} + assertSqsTraces(true) + } + + def "simple sqs producer-consumer services: async"() { + setup: + def builder = SqsAsyncClient.builder() + configureSdkClient(builder) + def client = configureSqsClient(builder.build()) + + client.createQueue(createQueueRequest).get() + + when: + client.sendMessage(sendMessageRequest).get() + + def resp = client.receiveMessage(receiveMessageRequest).get() + + then: + resp.messages.size() == 1 + resp.messages.each {message -> runWithSpan("process child") {}} + assertSqsTraces() + } + + def "batch sqs producer-consumer services: sync"() { + setup: + def builder = SqsClient.builder() + configureSdkClient(builder) + def client = configureSqsClient(builder.build()) + + client.createQueue(createQueueRequest) + + when: + client.sendMessageBatch(sendMessageBatchRequest) + + def resp = client.receiveMessage(receiveMessageBatchRequest) + def totalAttrs = resp.messages().sum {it.messageAttributes().size() } + + then: + resp.messages().size() == 3 + + // +2: 3 messages, 2x traceparent, 1x not injected due to too many attrs + totalAttrs == 18 + (sqsAttributeInjectionEnabled ? 2 : 0) + + assertTraces(xrayInjectionEnabled ? 2 : 3) { + trace(0, 1) { + + span(0) { + name "Sqs.CreateQueue" + kind CLIENT + } + } + trace(1, xrayInjectionEnabled ? 4 : 3) { + span(0) { + name "testSdkSqs publish" + kind PRODUCER + hasNoParent() + attributes { + "aws.agent" "java-aws-sdk" + "aws.queue.url" "http://localhost:$sqsPort/000000000000/testSdkSqs" + "aws.requestId" { it.trim() == "00000000-0000-0000-0000-000000000000" || it == "UNKNOWN" } + "rpc.system" "aws-api" + "rpc.method" "SendMessageBatch" + "rpc.service" "Sqs" + "$HttpAttributes.HTTP_REQUEST_METHOD" "POST" + "$HttpAttributes.HTTP_RESPONSE_STATUS_CODE" 200 + "$UrlAttributes.URL_FULL" { it.startsWith("http://localhost:$sqsPort") } + "$ServerAttributes.SERVER_ADDRESS" "localhost" + "$ServerAttributes.SERVER_PORT" sqsPort + "$MessagingIncubatingAttributes.MESSAGING_SYSTEM" MessagingIncubatingAttributes.MessagingSystemValues.AWS_SQS + "$MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME" "testSdkSqs" + "$MessagingIncubatingAttributes.MESSAGING_OPERATION" "publish" + } + } + for (int i: 1..(xrayInjectionEnabled ? 3 : 2)) { + span(i) { + name "testSdkSqs process" + kind CONSUMER + childOf span(0) + hasNoLinks() + + attributes { + "aws.agent" "java-aws-sdk" + "rpc.method" "ReceiveMessage" + "rpc.system" "aws-api" + "rpc.service" "Sqs" + "$HttpAttributes.HTTP_REQUEST_METHOD" "POST" + "$HttpAttributes.HTTP_RESPONSE_STATUS_CODE" 200 + "$UrlAttributes.URL_FULL" { it.startsWith("http://localhost:$sqsPort") } + "$ServerAttributes.SERVER_ADDRESS" "localhost" + "$ServerAttributes.SERVER_PORT" sqsPort + "$MessagingIncubatingAttributes.MESSAGING_SYSTEM" MessagingIncubatingAttributes.MessagingSystemValues.AWS_SQS + "$MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME" "testSdkSqs" + "$MessagingIncubatingAttributes.MESSAGING_OPERATION" "process" + "$MessagingIncubatingAttributes.MESSAGING_MESSAGE_ID" String + } + } + } + } + if (!xrayInjectionEnabled) { + trace(2, 1) { + span(0) { + name "testSdkSqs process" + kind CONSUMER + + // TODO This is not nice at all, and can also happen if producer is not instrumented + hasNoParent() + hasNoLinks() + + attributes { + "aws.agent" "java-aws-sdk" + "rpc.method" "ReceiveMessage" + "rpc.system" "aws-api" + "rpc.service" "Sqs" + "$HttpAttributes.HTTP_REQUEST_METHOD" "POST" + "$HttpAttributes.HTTP_RESPONSE_STATUS_CODE" 200 + "$UrlAttributes.URL_FULL" { it.startsWith("http://localhost:$sqsPort") } + "$ServerAttributes.SERVER_ADDRESS" "localhost" + "$ServerAttributes.SERVER_PORT" sqsPort + "$MessagingIncubatingAttributes.MESSAGING_SYSTEM" MessagingIncubatingAttributes.MessagingSystemValues.AWS_SQS + "$MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME" "testSdkSqs" + "$MessagingIncubatingAttributes.MESSAGING_OPERATION" "process" + "$MessagingIncubatingAttributes.MESSAGING_MESSAGE_ID" String + } + } + } + } + } + } +} diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/testing/src/main/groovy/io/opentelemetry/instrumentation/awssdk/v2_2/AbstractAws2SqsTracingTest.groovy b/instrumentation/aws-sdk/aws-sdk-2.2/testing/src/main/groovy/io/opentelemetry/instrumentation/awssdk/v2_2/AbstractAws2SqsTracingTest.groovy deleted file mode 100644 index 7a7a96efefb0..000000000000 --- a/instrumentation/aws-sdk/aws-sdk-2.2/testing/src/main/groovy/io/opentelemetry/instrumentation/awssdk/v2_2/AbstractAws2SqsTracingTest.groovy +++ /dev/null @@ -1,206 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.awssdk.v2_2 - -import io.opentelemetry.instrumentation.test.InstrumentationSpecification -import io.opentelemetry.instrumentation.test.utils.PortUtils -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes -import org.elasticmq.rest.sqs.SQSRestServerBuilder -import software.amazon.awssdk.auth.credentials.AwsBasicCredentials -import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider -import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration -import software.amazon.awssdk.regions.Region -import software.amazon.awssdk.services.sqs.SqsAsyncClient -import software.amazon.awssdk.services.sqs.SqsBaseClientBuilder -import software.amazon.awssdk.services.sqs.SqsClient -import software.amazon.awssdk.services.sqs.model.CreateQueueRequest -import software.amazon.awssdk.services.sqs.model.ReceiveMessageRequest -import software.amazon.awssdk.services.sqs.model.SendMessageRequest -import spock.lang.Shared - -import static io.opentelemetry.api.trace.SpanKind.CLIENT -import static io.opentelemetry.api.trace.SpanKind.CONSUMER -import static io.opentelemetry.api.trace.SpanKind.PRODUCER - -abstract class AbstractAws2SqsTracingTest extends InstrumentationSpecification { - - private static final StaticCredentialsProvider CREDENTIALS_PROVIDER = StaticCredentialsProvider - .create(AwsBasicCredentials.create("my-access-key", "my-secret-key")) - - @Shared - def sqs - - @Shared - int sqsPort - - ReceiveMessageRequest receiveMessageRequest = ReceiveMessageRequest.builder() - .queueUrl("http://localhost:$sqsPort/000000000000/testSdkSqs") - .build() - - CreateQueueRequest createQueueRequest = CreateQueueRequest.builder() - .queueName("testSdkSqs") - .build() - - SendMessageRequest sendMessageRequest = SendMessageRequest.builder() - .queueUrl("http://localhost:$sqsPort/000000000000/testSdkSqs") - .messageBody("{\"type\": \"hello\"}") - .build() - - void configureSdkClient(SqsBaseClientBuilder builder) { - builder - .overrideConfiguration(createOverrideConfigurationBuilder().build()) - .endpointOverride(new URI("http://localhost:" + sqsPort)) - builder - .region(Region.AP_NORTHEAST_1) - .credentialsProvider(CREDENTIALS_PROVIDER) - } - - abstract ClientOverrideConfiguration.Builder createOverrideConfigurationBuilder() - - def setupSpec() { - sqsPort = PortUtils.findOpenPort() - sqs = SQSRestServerBuilder.withPort(sqsPort).withInterface("localhost").start() - println getClass().name + " SQS server started at: localhost:$sqsPort/" - - } - - def cleanupSpec() { - if (sqs != null) { - sqs.stopAndWait() - } - } - - void assertSqsTraces() { - assertTraces(3) { - trace(0, 1) { - - span(0) { - name "Sqs.CreateQueue" - kind CLIENT - hasNoParent() - attributes { - "aws.agent" "java-aws-sdk" - "aws.queue.name" "testSdkSqs" - "aws.requestId" "00000000-0000-0000-0000-000000000000" - "rpc.system" "aws-api" - "rpc.service" "Sqs" - "rpc.method" "CreateQueue" - "http.method" "POST" - "http.status_code" 200 - "http.url" { it.startsWith("http://localhost:$sqsPort") } - "$SemanticAttributes.USER_AGENT_ORIGINAL" String - "net.peer.name" "localhost" - "net.peer.port" sqsPort - "$SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH" { it == null || it instanceof Long } - "$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" { it == null || it instanceof Long } - } - } - } - trace(1, 2) { - span(0) { - name "Sqs.SendMessage" - kind PRODUCER - hasNoParent() - attributes { - "aws.agent" "java-aws-sdk" - "aws.queue.url" "http://localhost:$sqsPort/000000000000/testSdkSqs" - "aws.requestId" "00000000-0000-0000-0000-000000000000" - "rpc.system" "aws-api" - "rpc.method" "SendMessage" - "rpc.service" "Sqs" - "http.method" "POST" - "http.status_code" 200 - "http.url" { it.startsWith("http://localhost:$sqsPort") } - "$SemanticAttributes.USER_AGENT_ORIGINAL" String - "net.peer.name" "localhost" - "net.peer.port" sqsPort - "$SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH" { it == null || it instanceof Long } - "$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" { it == null || it instanceof Long } - } - } - span(1) { - name "Sqs.ReceiveMessage" - kind CONSUMER - childOf span(0) - attributes { - "aws.agent" "java-aws-sdk" - "rpc.method" "ReceiveMessage" - "rpc.system" "aws-api" - "rpc.service" "Sqs" - "http.method" "POST" - "http.status_code" 200 - "http.url" { it.startsWith("http://localhost:$sqsPort") } - "net.peer.name" "localhost" - "net.peer.port" sqsPort - "$SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH" { it == null || it instanceof Long } - "$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" { it == null || it instanceof Long } - } - } - } - /** - * 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.requestId" "00000000-0000-0000-0000-000000000000" - "rpc.method" "ReceiveMessage" - "aws.queue.url" "http://localhost:$sqsPort/000000000000/testSdkSqs" - "rpc.system" "aws-api" - "rpc.service" "Sqs" - "http.method" "POST" - "http.status_code" 200 - "http.url" { it.startsWith("http://localhost:$sqsPort") } - "$SemanticAttributes.USER_AGENT_ORIGINAL" String - "net.peer.name" "localhost" - "net.peer.port" sqsPort - "$SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH" { it == null || it instanceof Long } - "$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" { it == null || it instanceof Long } - } - } - } - } - } - - def "simple sqs producer-consumer services: sync"() { - setup: - def builder = SqsClient.builder() - configureSdkClient(builder) - def client = builder.build() - - client.createQueue(createQueueRequest) - - when: - client.sendMessage(sendMessageRequest) - - client.receiveMessage(receiveMessageRequest) - - then: - assertSqsTraces() - } - - def "simple sqs producer-consumer services: async"() { - setup: - def builder = SqsAsyncClient.builder() - configureSdkClient(builder) - def client = builder.build() - - client.createQueue(createQueueRequest).get() - - when: - client.sendMessage(sendMessageRequest).get() - - client.receiveMessage(receiveMessageRequest).get() - - then: - assertSqsTraces() - } -} diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/testing/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AbstractAws2ClientRecordHttpErrorTest.java b/instrumentation/aws-sdk/aws-sdk-2.2/testing/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AbstractAws2ClientRecordHttpErrorTest.java new file mode 100644 index 000000000000..8c68be58729b --- /dev/null +++ b/instrumentation/aws-sdk/aws-sdk-2.2/testing/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AbstractAws2ClientRecordHttpErrorTest.java @@ -0,0 +1,210 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.awssdk.v2_2; + +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.api.internal.ConfigPropertiesUtil; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.semconv.HttpAttributes; +import io.opentelemetry.semconv.ServerAttributes; +import io.opentelemetry.semconv.incubating.DbIncubatingAttributes; +import io.opentelemetry.semconv.incubating.RpcIncubatingAttributes; +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.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; +import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration; +import software.amazon.awssdk.core.interceptor.Context; +import software.amazon.awssdk.core.interceptor.ExecutionAttributes; +import software.amazon.awssdk.core.interceptor.ExecutionInterceptor; +import software.amazon.awssdk.http.SdkHttpResponse; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.dynamodb.DynamoDbClient; +import software.amazon.awssdk.services.dynamodb.DynamoDbClientBuilder; +import software.amazon.awssdk.services.dynamodb.model.PutItemRequest; + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +public abstract class AbstractAws2ClientRecordHttpErrorTest { + private static final StaticCredentialsProvider CREDENTIALS_PROVIDER = + StaticCredentialsProvider.create( + AwsBasicCredentials.create("my-access-key", "my-secret-key")); + + private static final MockWebServerExtension server = new MockWebServerExtension(); + protected static List httpErrorMessages = new ArrayList<>(); + + @BeforeAll + public static void setup() { + server.start(); + } + + public static void cleanup() { + server.stop(); + } + + public abstract ClientOverrideConfiguration.Builder createOverrideConfigurationBuilder(); + + protected abstract InstrumentationExtension getTesting(); + + // Introducing a new ExecutionInterceptor that's registered with the AWS SDK. + // It's positioned to execute after the TracingExecutionInterceptor used for SDK instrumentation. + // The purpose of this interceptor is to inspect the response body of failed HTTP requests. + // We aim to ensure that the HTTP error message remains accessible in the response body's + // InputStream + // even after the TracingExecutionInterceptor has processed it. + static class ResponseCheckInterceptor implements ExecutionInterceptor { + @Override + public Optional modifyHttpResponseContent( + Context.ModifyHttpResponse context, ExecutionAttributes executionAttributes) { + Optional responseBody = context.responseBody(); + String errorMsg = extractHttpErrorMessage(context, executionAttributes); + if (errorMsg != null) { + return Optional.of(new ByteArrayInputStream(errorMsg.getBytes(Charset.defaultCharset()))); + } + return responseBody; + } + + private static String extractHttpErrorMessage( + Context.AfterTransmission context, ExecutionAttributes executionAttributes) { + SdkHttpResponse response = context.httpResponse(); + if (executionAttributes == null) { + return ""; + } + + if (response != null && !response.isSuccessful()) { + Optional responseBody = context.responseBody(); + if (responseBody.isPresent()) { + String errorMsg = + new BufferedReader( + new InputStreamReader(responseBody.get(), Charset.defaultCharset())) + .lines() + .collect(Collectors.joining("\n")); + httpErrorMessages.add(errorMsg); + return errorMsg; + } + } + return null; + } + } + + private static void cleanResponses() { + httpErrorMessages.clear(); + } + + public boolean isRecordIndividualHttpErrorEnabled() { + // See io.opentelemetry.instrumentation.awssdk.v2_2.autoconfigure.TracingExecutionInterceptor + return ConfigPropertiesUtil.getBoolean( + "otel.instrumentation.aws-sdk.experimental-record-individual-http-error", false); + } + + @Test + public void testSendDynamoDbRequestWithRetries() { + cleanResponses(); + // Setup and configuration + String service = "DynamoDb"; + String operation = "PutItem"; + String method = "POST"; + String requestId = "UNKNOWN"; + DynamoDbClientBuilder builder = DynamoDbClient.builder(); + ClientOverrideConfiguration.Builder overrideConfigBuilder = + createOverrideConfigurationBuilder() + .addExecutionInterceptor(new ResponseCheckInterceptor()); + builder.overrideConfiguration(overrideConfigBuilder.build()); + + DynamoDbClient client = + builder + .endpointOverride(server.httpUri()) + .region(Region.AP_NORTHEAST_1) + .credentialsProvider(CREDENTIALS_PROVIDER) + .build(); + + // Mocking server responses + server.enqueue( + HttpResponse.of( + HttpStatus.INTERNAL_SERVER_ERROR, + MediaType.PLAIN_TEXT_UTF_8, + "DynamoDB could not process your request")); + server.enqueue( + HttpResponse.of( + HttpStatus.SERVICE_UNAVAILABLE, + MediaType.PLAIN_TEXT_UTF_8, + "DynamoDB is currently unavailable")); + server.enqueue(HttpResponse.of(HttpStatus.OK, MediaType.PLAIN_TEXT_UTF_8, "")); + + // Making the call + client.putItem(PutItemRequest.builder().tableName("sometable").build()); + + getTesting() + .waitAndAssertTraces( + trace -> { + trace.hasSpansSatisfyingExactly( + span -> { + span.hasKind(SpanKind.CLIENT); + span.hasNoParent(); + span.hasAttributesSatisfying( + attributes -> { + assertThat(attributes) + .containsEntry(ServerAttributes.SERVER_ADDRESS, "127.0.0.1") + .containsEntry(ServerAttributes.SERVER_PORT, server.httpPort()) + .containsEntry(HttpAttributes.HTTP_REQUEST_METHOD, method) + .containsEntry(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, 200) + .containsEntry(RpcIncubatingAttributes.RPC_SYSTEM, "aws-api") + .containsEntry(RpcIncubatingAttributes.RPC_SERVICE, service) + .containsEntry(RpcIncubatingAttributes.RPC_METHOD, operation) + .containsEntry("aws.agent", "java-aws-sdk") + .containsEntry("aws.requestId", requestId) + .containsEntry("aws.table.name", "sometable") + .containsEntry(DbIncubatingAttributes.DB_SYSTEM, "dynamodb") + .containsEntry(DbIncubatingAttributes.DB_OPERATION, operation); + }); + if (isRecordIndividualHttpErrorEnabled()) { + span.hasEventsSatisfyingExactly( + event -> + event + .hasName("HTTP request failure") + .hasAttributesSatisfyingExactly( + equalTo(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, 500), + equalTo( + AttributeKey.stringKey("aws.http.error_message"), + "DynamoDB could not process your request")), + event -> + event + .hasName("HTTP request failure") + .hasAttributesSatisfyingExactly( + equalTo(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, 503), + equalTo( + AttributeKey.stringKey("aws.http.error_message"), + "DynamoDB is currently unavailable"))); + } else { + span.hasEventsSatisfying(events -> assertThat(events.size()).isEqualTo(0)); + } + }); + }); + + // make sure the response body input stream is still available and check its content to be + // expected + assertThat(httpErrorMessages.size()).isEqualTo(2); + assertThat(httpErrorMessages.get(0)).isEqualTo("DynamoDB could not process your request"); + assertThat(httpErrorMessages.get(1)).isEqualTo("DynamoDB is currently unavailable"); + } +} diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/testing/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AbstractAws2LambdaTest.java b/instrumentation/aws-sdk/aws-sdk-2.2/testing/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AbstractAws2LambdaTest.java new file mode 100644 index 000000000000..64c8a48a6c77 --- /dev/null +++ b/instrumentation/aws-sdk/aws-sdk-2.2/testing/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AbstractAws2LambdaTest.java @@ -0,0 +1,78 @@ +/* + * 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 io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +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.nio.charset.StandardCharsets; +import java.util.Base64; +import org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; +import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.lambda.LambdaClient; +import software.amazon.awssdk.services.lambda.LambdaClientBuilder; +import software.amazon.awssdk.services.lambda.model.InvokeRequest; +import software.amazon.awssdk.services.lambda.model.InvokeResponse; + +public abstract class AbstractAws2LambdaTest { + + @RegisterExtension + private static final MockWebServerExtension server = new MockWebServerExtension(); + + private static final StaticCredentialsProvider CREDENTIALS_PROVIDER = + StaticCredentialsProvider.create( + AwsBasicCredentials.create("my-access-key", "my-secret-key")); + + protected abstract InstrumentationExtension getTesting(); + + protected abstract ClientOverrideConfiguration.Builder createOverrideConfigurationBuilder(); + + protected boolean canTestLambdaInvoke() { + return true; + } + + @Test + void testInvokeLambda() { + Assumptions.assumeTrue(canTestLambdaInvoke()); + + LambdaClientBuilder builder = LambdaClient.builder(); + builder + .overrideConfiguration(createOverrideConfigurationBuilder().build()) + .endpointOverride(server.httpUri()); + builder.region(Region.AP_NORTHEAST_1).credentialsProvider(CREDENTIALS_PROVIDER); + LambdaClient client = builder.build(); + + server.enqueue(HttpResponse.of(HttpStatus.OK, MediaType.PLAIN_TEXT_UTF_8, "ok")); + + InvokeRequest request = InvokeRequest.builder().functionName("test").build(); + InvokeResponse response = client.invoke(request); + assertThat(response.statusCode()).isEqualTo(200); + assertThat(response.payload().asUtf8String()).isEqualTo("ok"); + + String clientContextHeader = + server.takeRequest().request().headers().get("x-amz-client-context"); + assertThat(clientContextHeader).isNotEmpty(); + String clientContextJson = + new String(Base64.getDecoder().decode(clientContextHeader), StandardCharsets.UTF_8); + assertThat(clientContextJson).contains("traceparent"); + + getTesting() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("Lambda.Invoke").hasKind(SpanKind.CLIENT).hasNoParent())); + } +} diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/testing/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AbstractAws2SqsTracingTest.java b/instrumentation/aws-sdk/aws-sdk-2.2/testing/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AbstractAws2SqsTracingTest.java new file mode 100644 index 000000000000..39a267222ba0 --- /dev/null +++ b/instrumentation/aws-sdk/aws-sdk-2.2/testing/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AbstractAws2SqsTracingTest.java @@ -0,0 +1,627 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.awssdk.v2_2; + +import static io.opentelemetry.api.common.AttributeKey.stringArrayKey; +import static io.opentelemetry.api.common.AttributeKey.stringKey; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.satisfies; +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.api.internal.ConfigPropertiesUtil; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.sdk.testing.assertj.AttributeAssertion; +import io.opentelemetry.sdk.testing.assertj.SpanDataAssert; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.semconv.HttpAttributes; +import io.opentelemetry.semconv.ServerAttributes; +import io.opentelemetry.semconv.UrlAttributes; +import io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes; +import io.opentelemetry.semconv.incubating.RpcIncubatingAttributes; +import io.opentelemetry.testing.internal.armeria.internal.shaded.guava.collect.ImmutableList; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; +import org.apache.pekko.http.scaladsl.Http; +import org.elasticmq.rest.sqs.SQSRestServer; +import org.elasticmq.rest.sqs.SQSRestServerBuilder; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; +import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.sqs.SqsAsyncClient; +import software.amazon.awssdk.services.sqs.SqsAsyncClientBuilder; +import software.amazon.awssdk.services.sqs.SqsClient; +import software.amazon.awssdk.services.sqs.SqsClientBuilder; +import software.amazon.awssdk.services.sqs.model.CreateQueueRequest; +import software.amazon.awssdk.services.sqs.model.MessageAttributeValue; +import software.amazon.awssdk.services.sqs.model.ReceiveMessageRequest; +import software.amazon.awssdk.services.sqs.model.ReceiveMessageResponse; +import software.amazon.awssdk.services.sqs.model.SendMessageBatchRequest; +import software.amazon.awssdk.services.sqs.model.SendMessageRequest; + +public abstract class AbstractAws2SqsTracingTest { + + protected abstract InstrumentationExtension getTesting(); + + protected abstract SqsClient configureSqsClient(SqsClient sqsClient); + + protected abstract SqsAsyncClient configureSqsClient(SqsAsyncClient sqsClient); + + protected abstract ClientOverrideConfiguration.Builder createOverrideConfigurationBuilder(); + + private static final StaticCredentialsProvider CREDENTIALS_PROVIDER = + StaticCredentialsProvider.create( + AwsBasicCredentials.create("my-access-key", "my-secret-key")); + + private static int sqsPort; + private static SQSRestServer sqs; + + static Map dummyMessageAttributes(int count) { + Map map = new HashMap<>(); + for (int i = 0; i < count; i++) { + map.put( + "a" + i, MessageAttributeValue.builder().stringValue("v" + i).dataType("String").build()); + } + return map; + } + + private final String queueUrl = "http://localhost:" + sqsPort + "/000000000000/testSdkSqs"; + + ReceiveMessageRequest receiveMessageRequest = + ReceiveMessageRequest.builder().queueUrl(queueUrl).build(); + + ReceiveMessageRequest receiveMessageBatchRequest = + ReceiveMessageRequest.builder() + .queueUrl(queueUrl) + .maxNumberOfMessages(3) + .messageAttributeNames("All") + .waitTimeSeconds(5) + .build(); + + CreateQueueRequest createQueueRequest = + CreateQueueRequest.builder().queueName("testSdkSqs").build(); + + SendMessageRequest sendMessageRequest = + SendMessageRequest.builder().queueUrl(queueUrl).messageBody("{\"type\": \"hello\"}").build(); + + @SuppressWarnings("unchecked") + SendMessageBatchRequest sendMessageBatchRequest = + SendMessageBatchRequest.builder() + .queueUrl(queueUrl) + .entries( + e -> e.messageBody("e1").id("i1"), + // 8 attributes, injection always possible + e -> e.messageBody("e2").id("i2").messageAttributes(dummyMessageAttributes(8)), + // 10 attributes, injection with custom propagator never possible + e -> e.messageBody("e3").id("i3").messageAttributes(dummyMessageAttributes(10))) + .build(); + + boolean isSqsAttributeInjectionEnabled() { + // See io.opentelemetry.instrumentation.awssdk.v2_2.autoconfigure.TracingExecutionInterceptor + return ConfigPropertiesUtil.getBoolean( + "otel.instrumentation.aws-sdk.experimental-use-propagator-for-messaging", false); + } + + boolean isXrayInjectionEnabled() { + return true; + } + + void configureSdkClient(SqsClientBuilder builder) throws URISyntaxException { + builder + .overrideConfiguration(createOverrideConfigurationBuilder().build()) + .endpointOverride(new URI("http://localhost:" + sqsPort)); + builder.region(Region.AP_NORTHEAST_1).credentialsProvider(CREDENTIALS_PROVIDER); + } + + void configureSdkClient(SqsAsyncClientBuilder builder) throws URISyntaxException { + builder + .overrideConfiguration(createOverrideConfigurationBuilder().build()) + .endpointOverride(new URI("http://localhost:" + sqsPort)); + builder.region(Region.AP_NORTHEAST_1).credentialsProvider(CREDENTIALS_PROVIDER); + } + + @BeforeAll + static void setUp() { + sqs = SQSRestServerBuilder.withPort(0).withInterface("localhost").start(); + Http.ServerBinding server = sqs.waitUntilStarted(); + sqsPort = server.localAddress().getPort(); + } + + @AfterAll + static void cleanUp() { + if (sqs != null) { + sqs.stopAndWait(); + } + } + + void assertSqsTraces(Boolean withParent, Boolean captureHeaders) { + int offset = withParent ? 2 : 0; + AtomicReference publishSpan = new AtomicReference<>(); + + getTesting() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("Sqs.CreateQueue") + .hasKind(SpanKind.CLIENT) + .hasNoParent() + .hasAttributesSatisfyingExactly( + equalTo(stringKey("aws.agent"), "java-aws-sdk"), + equalTo(stringKey("aws.queue.name"), "testSdkSqs"), + satisfies( + stringKey("aws.requestId"), + val -> + val.satisfiesAnyOf( + v -> + assertThat(v) + .isEqualTo( + "00000000-0000-0000-0000-000000000000"), + v -> assertThat(v).isEqualTo("UNKNOWN"))), + equalTo(RpcIncubatingAttributes.RPC_SYSTEM, "aws-api"), + equalTo(RpcIncubatingAttributes.RPC_SERVICE, "Sqs"), + equalTo(RpcIncubatingAttributes.RPC_METHOD, "CreateQueue"), + equalTo(HttpAttributes.HTTP_REQUEST_METHOD, "POST"), + equalTo(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, 200), + satisfies( + UrlAttributes.URL_FULL, + v -> v.startsWith("http://localhost:" + sqsPort)), + equalTo(ServerAttributes.SERVER_ADDRESS, "localhost"), + equalTo(ServerAttributes.SERVER_PORT, sqsPort))), + trace -> + trace.hasSpansSatisfyingExactly( + span -> { + publishSpan.set(trace.getSpan(0)); + List attributes = + new ArrayList<>( + Arrays.asList( + equalTo(stringKey("aws.agent"), "java-aws-sdk"), + equalTo( + stringKey("aws.queue.url"), + "http://localhost:" + sqsPort + "/000000000000/testSdkSqs"), + satisfies( + stringKey("aws.requestId"), + val -> + val.satisfiesAnyOf( + v -> + assertThat(v) + .isEqualTo( + "00000000-0000-0000-0000-000000000000"), + v -> assertThat(v).isEqualTo("UNKNOWN"))), + equalTo(RpcIncubatingAttributes.RPC_SYSTEM, "aws-api"), + equalTo(RpcIncubatingAttributes.RPC_SERVICE, "Sqs"), + equalTo(RpcIncubatingAttributes.RPC_METHOD, "SendMessage"), + equalTo(HttpAttributes.HTTP_REQUEST_METHOD, "POST"), + equalTo(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, 200), + satisfies( + UrlAttributes.URL_FULL, + v -> v.startsWith("http://localhost:" + sqsPort)), + equalTo(ServerAttributes.SERVER_ADDRESS, "localhost"), + equalTo(ServerAttributes.SERVER_PORT, sqsPort), + equalTo( + MessagingIncubatingAttributes.MESSAGING_SYSTEM, + MessagingIncubatingAttributes.MessagingSystemValues.AWS_SQS), + equalTo( + MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME, + "testSdkSqs"), + equalTo( + MessagingIncubatingAttributes.MESSAGING_OPERATION, "publish"), + satisfies( + MessagingIncubatingAttributes.MESSAGING_MESSAGE_ID, + v -> v.isInstanceOf(String.class)))); + + if (captureHeaders) { + attributes.add( + satisfies( + stringArrayKey("messaging.header.test_message_header"), + v -> v.isEqualTo(ImmutableList.of("test")))); + } + span.hasName("testSdkSqs publish") + .hasKind(SpanKind.PRODUCER) + .hasNoParent() + .hasAttributesSatisfyingExactly(attributes); + }), + trace -> { + List> spanAsserts = new ArrayList<>(); + if (withParent) { + spanAsserts.addAll( + Arrays.asList( + span -> span.hasName("parent").hasNoParent(), + /* + * 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) + */ + span -> + span.hasName("Sqs.ReceiveMessage") + .hasKind(SpanKind.CLIENT) + .hasParent(trace.getSpan(0)) + .hasTotalRecordedLinks(0) + .hasAttributesSatisfyingExactly( + equalTo(stringKey("aws.agent"), "java-aws-sdk"), + equalTo( + stringKey("aws.queue.url"), + "http://localhost:" + sqsPort + "/000000000000/testSdkSqs"), + satisfies( + stringKey("aws.requestId"), + val -> + val.satisfiesAnyOf( + v -> + assertThat(v) + .isEqualTo( + "00000000-0000-0000-0000-000000000000"), + v -> assertThat(v).isEqualTo("UNKNOWN"))), + equalTo(RpcIncubatingAttributes.RPC_SYSTEM, "aws-api"), + equalTo(RpcIncubatingAttributes.RPC_SERVICE, "Sqs"), + equalTo(RpcIncubatingAttributes.RPC_METHOD, "ReceiveMessage"), + equalTo(HttpAttributes.HTTP_REQUEST_METHOD, "POST"), + equalTo(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, 200), + satisfies( + UrlAttributes.URL_FULL, + v -> v.startsWith("http://localhost:" + sqsPort)), + equalTo(ServerAttributes.SERVER_ADDRESS, "localhost"), + equalTo(ServerAttributes.SERVER_PORT, sqsPort)))); + } + + spanAsserts.addAll( + Arrays.asList( + span -> { + List attributes = + new ArrayList<>( + Arrays.asList( + equalTo(stringKey("aws.agent"), "java-aws-sdk"), + equalTo(RpcIncubatingAttributes.RPC_SYSTEM, "aws-api"), + equalTo(RpcIncubatingAttributes.RPC_SERVICE, "Sqs"), + equalTo(RpcIncubatingAttributes.RPC_METHOD, "ReceiveMessage"), + equalTo(HttpAttributes.HTTP_REQUEST_METHOD, "POST"), + equalTo(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, 200), + satisfies( + UrlAttributes.URL_FULL, + v -> v.startsWith("http://localhost:" + sqsPort)), + equalTo(ServerAttributes.SERVER_ADDRESS, "localhost"), + equalTo(ServerAttributes.SERVER_PORT, sqsPort), + equalTo( + MessagingIncubatingAttributes.MESSAGING_SYSTEM, + MessagingIncubatingAttributes.MessagingSystemValues + .AWS_SQS), + equalTo( + MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME, + "testSdkSqs"), + equalTo( + MessagingIncubatingAttributes.MESSAGING_OPERATION, + "receive"), + equalTo( + MessagingIncubatingAttributes.MESSAGING_BATCH_MESSAGE_COUNT, + 1))); + + if (captureHeaders) { + attributes.add( + satisfies( + stringArrayKey("messaging.header.test_message_header"), + v -> v.isEqualTo(ImmutableList.of("test")))); + } + + if (withParent) { + span.hasParent(trace.getSpan(0)); + } else { + span.hasNoParent(); + } + + span.hasName("testSdkSqs receive") + .hasKind(SpanKind.CONSUMER) + .hasTotalRecordedLinks(0) + .hasAttributesSatisfyingExactly(attributes); + }, + span -> { + List attributes = + new ArrayList<>( + Arrays.asList( + equalTo(stringKey("aws.agent"), "java-aws-sdk"), + equalTo(RpcIncubatingAttributes.RPC_SYSTEM, "aws-api"), + equalTo(RpcIncubatingAttributes.RPC_SERVICE, "Sqs"), + equalTo(RpcIncubatingAttributes.RPC_METHOD, "ReceiveMessage"), + equalTo(HttpAttributes.HTTP_REQUEST_METHOD, "POST"), + equalTo(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, 200), + satisfies( + UrlAttributes.URL_FULL, + v -> v.startsWith("http://localhost:" + sqsPort)), + equalTo(ServerAttributes.SERVER_ADDRESS, "localhost"), + equalTo(ServerAttributes.SERVER_PORT, sqsPort), + equalTo( + MessagingIncubatingAttributes.MESSAGING_SYSTEM, + MessagingIncubatingAttributes.MessagingSystemValues + .AWS_SQS), + equalTo( + MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME, + "testSdkSqs"), + equalTo( + MessagingIncubatingAttributes.MESSAGING_OPERATION, + "process"), + satisfies( + MessagingIncubatingAttributes.MESSAGING_MESSAGE_ID, + v -> v.isInstanceOf(String.class)))); + + if (captureHeaders) { + attributes.add( + satisfies( + stringArrayKey("messaging.header.test_message_header"), + v -> v.isEqualTo(ImmutableList.of("test")))); + } + + span.hasName("testSdkSqs process") + .hasParent(trace.getSpan(0 + offset)) + .hasKind(SpanKind.CONSUMER) + .hasLinksSatisfying( + links -> + assertThat(links) + .singleElement() + .satisfies( + link -> + assertThat(link.getSpanContext().getSpanId()) + .isEqualTo(publishSpan.get().getSpanId()))) + .hasAttributesSatisfyingExactly(attributes); + }, + span -> + span.hasName("process child") + .hasParent(trace.getSpan(1 + offset)) + .hasAttributes(Attributes.empty()))); + trace.hasSpansSatisfyingExactly(spanAsserts); + }); + } + + @Test + void testSimpleSqsProducerConsumerServicesSync() throws URISyntaxException { + SqsClientBuilder builder = SqsClient.builder(); + configureSdkClient(builder); + SqsClient client = configureSqsClient(builder.build()); + + client.createQueue(createQueueRequest); + + client.sendMessage(sendMessageRequest); + + ReceiveMessageResponse response = client.receiveMessage(receiveMessageRequest); + + assertThat(response.messages().size()).isEqualTo(1); + + response.messages().forEach(message -> getTesting().runWithSpan("process child", () -> {})); + assertSqsTraces(false, false); + } + + @Test + void testCaptureMessageHeaderAsAttributeSpan() throws URISyntaxException { + SqsClientBuilder builder = SqsClient.builder(); + configureSdkClient(builder); + SqsClient client = configureSqsClient(builder.build()); + + client.createQueue(createQueueRequest); + + SendMessageRequest newSendMessageRequest = + sendMessageRequest.toBuilder() + .messageAttributes( + Collections.singletonMap( + "test-message-header", + MessageAttributeValue.builder().dataType("String").stringValue("test").build())) + .build(); + client.sendMessage(newSendMessageRequest); + + ReceiveMessageRequest newReceiveMessageRequest = + receiveMessageRequest.toBuilder().messageAttributeNames("test-message-header").build(); + ReceiveMessageResponse response = client.receiveMessage(newReceiveMessageRequest); + + assertThat(response.messages().size()).isEqualTo(1); + + response.messages().forEach(message -> getTesting().runWithSpan("process child", () -> {})); + assertSqsTraces(false, true); + } + + @Test + void testSimpleSqsProducerConsumerServicesWithParentSync() throws URISyntaxException { + SqsClientBuilder builder = SqsClient.builder(); + configureSdkClient(builder); + SqsClient client = configureSqsClient(builder.build()); + + client.createQueue(createQueueRequest); + client.sendMessage(sendMessageRequest); + + ReceiveMessageResponse response = + getTesting().runWithSpan("parent", () -> client.receiveMessage(receiveMessageRequest)); + + assertThat(response.messages().size()).isEqualTo(1); + + response.messages().forEach(message -> getTesting().runWithSpan("process child", () -> {})); + assertSqsTraces(true, false); + } + + @SuppressWarnings("InterruptedExceptionSwallowed") + @Test + void testSimpleSqsProducerConsumerServicesAsync() throws Exception { + SqsAsyncClientBuilder builder = SqsAsyncClient.builder(); + configureSdkClient(builder); + SqsAsyncClient client = configureSqsClient(builder.build()); + + client.createQueue(createQueueRequest).get(); + client.sendMessage(sendMessageRequest).get(); + + ReceiveMessageResponse response = client.receiveMessage(receiveMessageRequest).get(); + + assertThat(response.messages().size()).isEqualTo(1); + + response.messages().forEach(message -> getTesting().runWithSpan("process child", () -> {})); + assertSqsTraces(false, false); + } + + @Test + void testBatchSqsProducerConsumerServicesSync() throws URISyntaxException { + SqsClientBuilder builder = SqsClient.builder(); + configureSdkClient(builder); + SqsClient client = configureSqsClient(builder.build()); + + client.createQueue(createQueueRequest); + client.sendMessageBatch(sendMessageBatchRequest); + + ReceiveMessageResponse response = client.receiveMessage(receiveMessageBatchRequest); + response.messages().forEach(message -> getTesting().runWithSpan("process child", () -> {})); + + int totalAttrs = + response.messages().stream().mapToInt(message -> message.messageAttributes().size()).sum(); + + assertThat(response.messages().size()).isEqualTo(3); + + // +2: 3 messages, 2x traceparent, 1x not injected due to too many attrs + assertThat(totalAttrs).isEqualTo(18 + (isSqsAttributeInjectionEnabled() ? 2 : 0)); + + AtomicReference publishSpan = new AtomicReference<>(); + + getTesting() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("Sqs.CreateQueue").hasKind(SpanKind.CLIENT)), + trace -> { + publishSpan.set(trace.getSpan(0)); + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("testSdkSqs publish") + .hasKind(SpanKind.PRODUCER) + .hasNoParent() + .hasAttributesSatisfyingExactly( + equalTo(stringKey("aws.agent"), "java-aws-sdk"), + equalTo( + stringKey("aws.queue.url"), + "http://localhost:" + sqsPort + "/000000000000/testSdkSqs"), + satisfies( + stringKey("aws.requestId"), + val -> + val.satisfiesAnyOf( + v -> + assertThat(v.trim()) + .isEqualTo( + "00000000-0000-0000-0000-000000000000"), + v -> assertThat(v.trim()).isEqualTo("UNKNOWN"))), + equalTo(RpcIncubatingAttributes.RPC_SYSTEM, "aws-api"), + equalTo(RpcIncubatingAttributes.RPC_SERVICE, "Sqs"), + equalTo(RpcIncubatingAttributes.RPC_METHOD, "SendMessageBatch"), + equalTo(HttpAttributes.HTTP_REQUEST_METHOD, "POST"), + equalTo(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, 200), + satisfies( + UrlAttributes.URL_FULL, + v -> v.startsWith("http://localhost:" + sqsPort)), + equalTo(ServerAttributes.SERVER_ADDRESS, "localhost"), + equalTo(ServerAttributes.SERVER_PORT, sqsPort), + equalTo( + MessagingIncubatingAttributes.MESSAGING_SYSTEM, + MessagingIncubatingAttributes.MessagingSystemValues.AWS_SQS), + equalTo( + MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME, + "testSdkSqs"), + equalTo( + MessagingIncubatingAttributes.MESSAGING_OPERATION, "publish"))); + }, + trace -> { + List> spanAsserts = new ArrayList<>(); + spanAsserts.add( + span -> + span.hasName("testSdkSqs receive") + .hasKind(SpanKind.CONSUMER) + .hasNoParent() + .hasTotalRecordedLinks(0) + .hasAttributesSatisfyingExactly( + equalTo(stringKey("aws.agent"), "java-aws-sdk"), + equalTo(RpcIncubatingAttributes.RPC_SYSTEM, "aws-api"), + equalTo(RpcIncubatingAttributes.RPC_SERVICE, "Sqs"), + equalTo(RpcIncubatingAttributes.RPC_METHOD, "ReceiveMessage"), + equalTo(HttpAttributes.HTTP_REQUEST_METHOD, "POST"), + equalTo(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, 200), + satisfies( + UrlAttributes.URL_FULL, + v -> v.startsWith("http://localhost:" + sqsPort)), + equalTo(ServerAttributes.SERVER_ADDRESS, "localhost"), + equalTo(ServerAttributes.SERVER_PORT, sqsPort), + equalTo( + MessagingIncubatingAttributes.MESSAGING_SYSTEM, + MessagingIncubatingAttributes.MessagingSystemValues.AWS_SQS), + equalTo( + MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME, + "testSdkSqs"), + equalTo(MessagingIncubatingAttributes.MESSAGING_OPERATION, "receive"), + equalTo( + MessagingIncubatingAttributes.MESSAGING_BATCH_MESSAGE_COUNT, 3))); + + // one of the 3 process spans is expected to not have a span link + for (int i = 0; i <= 2; i++) { + int finalI = i; + spanAsserts.addAll( + new ArrayList<>( + Arrays.asList( + span -> { + if (!isXrayInjectionEnabled() && finalI == 2) { + // last message in batch has too many attributes so injecting + // tracing header is not possible + span.hasTotalRecordedLinks(0); + } else { + span.hasLinksSatisfying( + links -> + assertThat(links) + .singleElement() + .satisfies( + link -> + assertThat(link.getSpanContext().getSpanId()) + .isEqualTo(publishSpan.get().getSpanId()))); + } + + span.hasName("testSdkSqs process") + .hasKind(SpanKind.CONSUMER) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo(stringKey("aws.agent"), "java-aws-sdk"), + equalTo(RpcIncubatingAttributes.RPC_SYSTEM, "aws-api"), + equalTo(RpcIncubatingAttributes.RPC_SERVICE, "Sqs"), + equalTo(RpcIncubatingAttributes.RPC_METHOD, "ReceiveMessage"), + equalTo(HttpAttributes.HTTP_REQUEST_METHOD, "POST"), + equalTo(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, 200), + satisfies( + UrlAttributes.URL_FULL, + v -> v.startsWith("http://localhost:" + sqsPort)), + equalTo(ServerAttributes.SERVER_ADDRESS, "localhost"), + equalTo(ServerAttributes.SERVER_PORT, sqsPort), + equalTo( + MessagingIncubatingAttributes.MESSAGING_SYSTEM, + MessagingIncubatingAttributes.MessagingSystemValues + .AWS_SQS), + equalTo( + MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME, + "testSdkSqs"), + equalTo( + MessagingIncubatingAttributes.MESSAGING_OPERATION, + "process"), + satisfies( + MessagingIncubatingAttributes.MESSAGING_MESSAGE_ID, + v -> v.isInstanceOf(String.class))); + }, + span -> + span.hasName("process child") + .hasParent(trace.getSpan(1 + 2 * finalI)) + .hasAttributes(Attributes.empty())))); + } + + trace.hasSpansSatisfyingExactlyInAnyOrder(spanAsserts); + }); + } +} diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/testing/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AbstractQueryProtocolModelTest.java b/instrumentation/aws-sdk/aws-sdk-2.2/testing/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AbstractQueryProtocolModelTest.java new file mode 100644 index 000000000000..098e1daf7d7b --- /dev/null +++ b/instrumentation/aws-sdk/aws-sdk-2.2/testing/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AbstractQueryProtocolModelTest.java @@ -0,0 +1,110 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.awssdk.v2_2; + +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; +import static io.opentelemetry.semconv.UrlAttributes.URL_FULL; + +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +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.net.URI; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; +import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.ses.SesClient; +import software.amazon.awssdk.services.ses.model.Body; +import software.amazon.awssdk.services.ses.model.Content; +import software.amazon.awssdk.services.ses.model.Destination; +import software.amazon.awssdk.services.ses.model.Message; +import software.amazon.awssdk.services.ses.model.SendEmailRequest; + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +public abstract class AbstractQueryProtocolModelTest { + private final MockWebServerExtension server = new MockWebServerExtension(); + + @BeforeAll + public void setup() { + server.start(); + } + + @AfterAll + public void end() { + server.stop(); + } + + @BeforeEach + public void setupEach() { + server.beforeTestExecution(null); + } + + protected abstract ClientOverrideConfiguration.Builder createClientOverrideConfigurationBuilder(); + + protected abstract InstrumentationExtension getTesting(); + + @Test + void testClientWithQueryProtocolModel() { + server.enqueue( + HttpResponse.of( + HttpStatus.OK, + MediaType.PLAIN_TEXT_UTF_8, + "12345")); + SesClient ses = + SesClient.builder() + .endpointOverride(server.httpUri()) + .credentialsProvider( + StaticCredentialsProvider.create(AwsBasicCredentials.create("foo", "bar"))) + .overrideConfiguration(createClientOverrideConfigurationBuilder().build()) + .region(Region.US_WEST_2) + .build(); + + Destination destination = Destination.builder().toAddresses("dest@test.com").build(); + Content content = Content.builder().data("content").build(); + Content sub = Content.builder().data("subject").build(); + Body body = Body.builder().html(content).build(); + Message msg = Message.builder().subject(sub).body(body).build(); + SendEmailRequest emailRequest = + SendEmailRequest.builder() + .destination(destination) + .message(msg) + .source("source@test.com") + .build(); + + ses.sendEmail(emailRequest); + + getTesting() + .waitAndAssertTraces( + trace -> { + trace.hasSpansSatisfyingExactly( + span -> { + span.hasKind(SpanKind.CLIENT); + span.hasAttributesSatisfying( + attributes -> { + assertThat(attributes) + .hasEntrySatisfying( + URL_FULL, + entry -> { + assertThat(entry) + .satisfies( + value -> { + URI uri = URI.create(value); + assertThat(uri.getQuery()).isNull(); + }); + }); + }); + }); + }); + } +} diff --git a/instrumentation/azure-core/azure-core-1.14/javaagent/build.gradle.kts b/instrumentation/azure-core/azure-core-1.14/javaagent/build.gradle.kts index 64459d1582d1..0833f4e53432 100644 --- a/instrumentation/azure-core/azure-core-1.14/javaagent/build.gradle.kts +++ b/instrumentation/azure-core/azure-core-1.14/javaagent/build.gradle.kts @@ -29,6 +29,28 @@ dependencies { // Ensure no cross interference testInstrumentation(project(":instrumentation:azure-core:azure-core-1.19:javaagent")) testInstrumentation(project(":instrumentation:azure-core:azure-core-1.36:javaagent")) +} + +val latestDepTest = findProperty("testLatestDeps") as Boolean + +testing { + suites { + // using a test suite to ensure that classes from library-instrumentation-shaded that were + // extracted to the output directory are not available during tests + val testAzure by registering(JvmTestSuite::class) { + dependencies { + if (latestDepTest) { + implementation("com.azure:azure-core:1.18.0") // see azure-core-1.19 module + } else { + implementation("com.azure:azure-core:1.14.0") + } + } + } + } +} - latestDepTestLibrary("com.azure:azure-core:1.18.+") // see azure-core-1.19 module +tasks { + check { + dependsOn(testing.suites) + } } diff --git a/instrumentation/azure-core/azure-core-1.14/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/azurecore/v1_14/AzureSdkInstrumentationModule.java b/instrumentation/azure-core/azure-core-1.14/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/azurecore/v1_14/AzureSdkInstrumentationModule.java index d9560a8b127d..7eeb61f87bbd 100644 --- a/instrumentation/azure-core/azure-core-1.14/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/azurecore/v1_14/AzureSdkInstrumentationModule.java +++ b/instrumentation/azure-core/azure-core-1.14/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/azurecore/v1_14/AzureSdkInstrumentationModule.java @@ -15,12 +15,16 @@ import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.extension.instrumentation.internal.ExperimentalInstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.internal.injection.ClassInjector; +import io.opentelemetry.javaagent.extension.instrumentation.internal.injection.InjectionMode; import java.util.List; import net.bytebuddy.description.type.TypeDescription; import net.bytebuddy.matcher.ElementMatcher; @AutoService(InstrumentationModule.class) -public class AzureSdkInstrumentationModule extends InstrumentationModule { +public class AzureSdkInstrumentationModule extends InstrumentationModule + implements ExperimentalInstrumentationModule { public AzureSdkInstrumentationModule() { super("azure-core", "azure-core-1.14"); } @@ -35,6 +39,18 @@ public void registerHelperResources(HelperResourceBuilder helperResourceBuilder) "azure-core-1.14/META-INF/services/com.azure.core.util.tracing.Tracer"); } + @Override + public void injectClasses(ClassInjector injector) { + injector + .proxyBuilder( + "io.opentelemetry.javaagent.instrumentation.azurecore.v1_14.shaded.com.azure.core.tracing.opentelemetry.OpenTelemetryHttpPolicy") + .inject(InjectionMode.CLASS_ONLY); + injector + .proxyBuilder( + "io.opentelemetry.javaagent.instrumentation.azurecore.v1_14.shaded.com.azure.core.tracing.opentelemetry.OpenTelemetryTracer") + .inject(InjectionMode.CLASS_ONLY); + } + @Override public ElementMatcher.Junction classLoaderMatcher() { return hasClassesNamed("com.azure.core.util.tracing.Tracer") diff --git a/instrumentation/azure-core/azure-core-1.14/javaagent/src/test/java/AzureSdkTest.java b/instrumentation/azure-core/azure-core-1.14/javaagent/src/testAzure/java/io/opentelemetry/javaagent/instrumentation/azurecore/v1_14/AzureSdkTest.java similarity index 96% rename from instrumentation/azure-core/azure-core-1.14/javaagent/src/test/java/AzureSdkTest.java rename to instrumentation/azure-core/azure-core-1.14/javaagent/src/testAzure/java/io/opentelemetry/javaagent/instrumentation/azurecore/v1_14/AzureSdkTest.java index 84fc4a9a644a..bceda620a0c2 100644 --- a/instrumentation/azure-core/azure-core-1.14/javaagent/src/test/java/AzureSdkTest.java +++ b/instrumentation/azure-core/azure-core-1.14/javaagent/src/testAzure/java/io/opentelemetry/javaagent/instrumentation/azurecore/v1_14/AzureSdkTest.java @@ -3,6 +3,8 @@ * SPDX-License-Identifier: Apache-2.0 */ +package io.opentelemetry.javaagent.instrumentation.azurecore.v1_14; + import static org.assertj.core.api.Assertions.assertThat; import com.azure.core.http.policy.HttpPipelinePolicy; diff --git a/instrumentation/azure-core/azure-core-1.14/library-instrumentation-shaded/build.gradle.kts b/instrumentation/azure-core/azure-core-1.14/library-instrumentation-shaded/build.gradle.kts index 25dbbc455e5e..67250df40347 100644 --- a/instrumentation/azure-core/azure-core-1.14/library-instrumentation-shaded/build.gradle.kts +++ b/instrumentation/azure-core/azure-core-1.14/library-instrumentation-shaded/build.gradle.kts @@ -1,6 +1,5 @@ plugins { - id("com.github.johnrengelman.shadow") - + id("com.gradleup.shadow") id("otel.java-conventions") } diff --git a/instrumentation/azure-core/azure-core-1.19/javaagent/build.gradle.kts b/instrumentation/azure-core/azure-core-1.19/javaagent/build.gradle.kts index 57e4692ec199..0a32d0161335 100644 --- a/instrumentation/azure-core/azure-core-1.19/javaagent/build.gradle.kts +++ b/instrumentation/azure-core/azure-core-1.19/javaagent/build.gradle.kts @@ -29,6 +29,28 @@ dependencies { // Ensure no cross interference testInstrumentation(project(":instrumentation:azure-core:azure-core-1.14:javaagent")) testInstrumentation(project(":instrumentation:azure-core:azure-core-1.36:javaagent")) +} + +val latestDepTest = findProperty("testLatestDeps") as Boolean + +testing { + suites { + // using a test suite to ensure that classes from library-instrumentation-shaded that were + // extracted to the output directory are not available during tests + val testAzure by registering(JvmTestSuite::class) { + dependencies { + if (latestDepTest) { + implementation("com.azure:azure-core:1.35.0") + } else { + implementation("com.azure:azure-core:1.19.0") + } + } + } + } +} - latestDepTestLibrary("com.azure:azure-core:1.35.0") +tasks { + check { + dependsOn(testing.suites) + } } diff --git a/instrumentation/azure-core/azure-core-1.19/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/azurecore/v1_19/AzureSdkInstrumentationModule.java b/instrumentation/azure-core/azure-core-1.19/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/azurecore/v1_19/AzureSdkInstrumentationModule.java index 6bf1f14a686f..3fb14c6001cc 100644 --- a/instrumentation/azure-core/azure-core-1.19/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/azurecore/v1_19/AzureSdkInstrumentationModule.java +++ b/instrumentation/azure-core/azure-core-1.19/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/azurecore/v1_19/AzureSdkInstrumentationModule.java @@ -15,12 +15,16 @@ import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.extension.instrumentation.internal.ExperimentalInstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.internal.injection.ClassInjector; +import io.opentelemetry.javaagent.extension.instrumentation.internal.injection.InjectionMode; import java.util.List; import net.bytebuddy.description.type.TypeDescription; import net.bytebuddy.matcher.ElementMatcher; @AutoService(InstrumentationModule.class) -public class AzureSdkInstrumentationModule extends InstrumentationModule { +public class AzureSdkInstrumentationModule extends InstrumentationModule + implements ExperimentalInstrumentationModule { public AzureSdkInstrumentationModule() { super("azure-core", "azure-core-1.19"); } @@ -35,6 +39,18 @@ public void registerHelperResources(HelperResourceBuilder helperResourceBuilder) "azure-core-1.19/META-INF/services/com.azure.core.util.tracing.Tracer"); } + @Override + public void injectClasses(ClassInjector injector) { + injector + .proxyBuilder( + "io.opentelemetry.javaagent.instrumentation.azurecore.v1_19.shaded.com.azure.core.tracing.opentelemetry.OpenTelemetryHttpPolicy") + .inject(InjectionMode.CLASS_ONLY); + injector + .proxyBuilder( + "io.opentelemetry.javaagent.instrumentation.azurecore.v1_19.shaded.com.azure.core.tracing.opentelemetry.OpenTelemetryTracer") + .inject(InjectionMode.CLASS_ONLY); + } + @Override public ElementMatcher.Junction classLoaderMatcher() { // this class was introduced in azure-core 1.19 diff --git a/instrumentation/azure-core/azure-core-1.19/javaagent/src/test/java/AzureSdkTest.java b/instrumentation/azure-core/azure-core-1.19/javaagent/src/testAzure/java/io/opentelemetry/javaagent/instrumentation/azurecore/v1_19/AzureSdkTest.java similarity index 96% rename from instrumentation/azure-core/azure-core-1.19/javaagent/src/test/java/AzureSdkTest.java rename to instrumentation/azure-core/azure-core-1.19/javaagent/src/testAzure/java/io/opentelemetry/javaagent/instrumentation/azurecore/v1_19/AzureSdkTest.java index 455ca2d8eaf2..ae3cea3b7ed4 100644 --- a/instrumentation/azure-core/azure-core-1.19/javaagent/src/test/java/AzureSdkTest.java +++ b/instrumentation/azure-core/azure-core-1.19/javaagent/src/testAzure/java/io/opentelemetry/javaagent/instrumentation/azurecore/v1_19/AzureSdkTest.java @@ -3,6 +3,8 @@ * SPDX-License-Identifier: Apache-2.0 */ +package io.opentelemetry.javaagent.instrumentation.azurecore.v1_19; + import static org.assertj.core.api.Assertions.assertThat; import com.azure.core.http.policy.HttpPipelinePolicy; diff --git a/instrumentation/azure-core/azure-core-1.19/library-instrumentation-shaded/build.gradle.kts b/instrumentation/azure-core/azure-core-1.19/library-instrumentation-shaded/build.gradle.kts index 590937930dee..ebcd721e7c19 100644 --- a/instrumentation/azure-core/azure-core-1.19/library-instrumentation-shaded/build.gradle.kts +++ b/instrumentation/azure-core/azure-core-1.19/library-instrumentation-shaded/build.gradle.kts @@ -1,6 +1,5 @@ plugins { - id("com.github.johnrengelman.shadow") - + id("com.gradleup.shadow") id("otel.java-conventions") } diff --git a/instrumentation/azure-core/azure-core-1.36/javaagent/build.gradle.kts b/instrumentation/azure-core/azure-core-1.36/javaagent/build.gradle.kts index e1403f63b8c5..3a8fd040b87c 100644 --- a/instrumentation/azure-core/azure-core-1.36/javaagent/build.gradle.kts +++ b/instrumentation/azure-core/azure-core-1.36/javaagent/build.gradle.kts @@ -29,6 +29,28 @@ dependencies { // Ensure no cross interference testInstrumentation(project(":instrumentation:azure-core:azure-core-1.14:javaagent")) testInstrumentation(project(":instrumentation:azure-core:azure-core-1.19:javaagent")) +} + +val latestDepTest = findProperty("testLatestDeps") as Boolean + +testing { + suites { + // using a test suite to ensure that classes from library-instrumentation-shaded that were + // extracted to the output directory are not available during tests + val testAzure by registering(JvmTestSuite::class) { + dependencies { + if (latestDepTest) { + implementation("com.azure:azure-core:+") + } else { + implementation("com.azure:azure-core:1.36.0") + } + } + } + } +} - latestDepTestLibrary("com.azure:azure-core:1.36.0") +tasks { + check { + dependsOn(testing.suites) + } } diff --git a/instrumentation/azure-core/azure-core-1.36/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/azurecore/v1_36/AzureHttpClientInstrumentation.java b/instrumentation/azure-core/azure-core-1.36/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/azurecore/v1_36/AzureHttpClientInstrumentation.java index 5a27c5ba13e3..2e1a68f2affe 100644 --- a/instrumentation/azure-core/azure-core-1.36/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/azurecore/v1_36/AzureHttpClientInstrumentation.java +++ b/instrumentation/azure-core/azure-core-1.36/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/azurecore/v1_36/AzureHttpClientInstrumentation.java @@ -6,12 +6,15 @@ package io.opentelemetry.javaagent.instrumentation.azurecore.v1_36; import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.implementsInterface; +import static io.opentelemetry.javaagent.instrumentation.azurecore.v1_36.SuppressNestedClientHelper.disallowNestedClientSpanMono; +import static io.opentelemetry.javaagent.instrumentation.azurecore.v1_36.SuppressNestedClientHelper.disallowNestedClientSpanSync; 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 com.azure.core.http.HttpResponse; +import io.opentelemetry.context.Scope; import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; import net.bytebuddy.asm.Advice; @@ -33,15 +36,37 @@ public void transform(TypeTransformer transformer) { .and(isPublic()) .and(named("send")) .and(returns(named("reactor.core.publisher.Mono"))), - this.getClass().getName() + "$SuppressNestedClientAdvice"); + this.getClass().getName() + "$SuppressNestedClientMonoAdvice"); + transformer.applyAdviceToMethod( + isMethod() + .and(isPublic()) + .and(named("sendSync")) + .and(returns(named("com.azure.core.http.HttpResponse"))), + this.getClass().getName() + "$SuppressNestedClientSyncAdvice"); + } + + @SuppressWarnings("unused") + public static class SuppressNestedClientMonoAdvice { + @Advice.OnMethodExit(suppress = Throwable.class) + public static void asyncSendExit(@Advice.Return(readOnly = false) Mono mono) { + mono = disallowNestedClientSpanMono(mono); + } } @SuppressWarnings("unused") - public static class SuppressNestedClientAdvice { + public static class SuppressNestedClientSyncAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void syncSendEnter(@Advice.Local("otelScope") Scope scope) { + scope = disallowNestedClientSpanSync(); + } @Advice.OnMethodExit(suppress = Throwable.class) - public static void methodExit(@Advice.Return(readOnly = false) Mono mono) { - mono = new SuppressNestedClientMono<>(mono); + public static void syncSendExit( + @Advice.Return HttpResponse response, @Advice.Local("otelScope") Scope scope) { + if (scope != null) { + scope.close(); + } } } } diff --git a/instrumentation/azure-core/azure-core-1.36/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/azurecore/v1_36/AzureSdkInstrumentationModule.java b/instrumentation/azure-core/azure-core-1.36/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/azurecore/v1_36/AzureSdkInstrumentationModule.java index 2f3f4b44729a..665ace5d14f7 100644 --- a/instrumentation/azure-core/azure-core-1.36/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/azurecore/v1_36/AzureSdkInstrumentationModule.java +++ b/instrumentation/azure-core/azure-core-1.36/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/azurecore/v1_36/AzureSdkInstrumentationModule.java @@ -7,7 +7,7 @@ import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.hasClassesNamed; import static java.util.Arrays.asList; -import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.namedOneOf; import static net.bytebuddy.matcher.ElementMatchers.not; import com.google.auto.service.AutoService; @@ -15,12 +15,16 @@ import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.extension.instrumentation.internal.ExperimentalInstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.internal.injection.ClassInjector; +import io.opentelemetry.javaagent.extension.instrumentation.internal.injection.InjectionMode; import java.util.List; import net.bytebuddy.description.type.TypeDescription; import net.bytebuddy.matcher.ElementMatcher; @AutoService(InstrumentationModule.class) -public class AzureSdkInstrumentationModule extends InstrumentationModule { +public class AzureSdkInstrumentationModule extends InstrumentationModule + implements ExperimentalInstrumentationModule { public AzureSdkInstrumentationModule() { super("azure-core", "azure-core-1.36"); } @@ -30,6 +34,23 @@ public void registerHelperResources(HelperResourceBuilder helperResourceBuilder) helperResourceBuilder.register( "META-INF/services/com.azure.core.util.tracing.TracerProvider", "azure-core-1.36/META-INF/services/com.azure.core.util.tracing.TracerProvider"); + // some azure sdks (e.g. EventHubs) are still looking up Tracer via service loader + // and not yet using the new TracerProvider + helperResourceBuilder.register( + "META-INF/services/com.azure.core.util.tracing.Tracer", + "azure-core-1.36/META-INF/services/com.azure.core.util.tracing.Tracer"); + } + + @Override + public void injectClasses(ClassInjector injector) { + injector + .proxyBuilder( + "io.opentelemetry.javaagent.instrumentation.azurecore.v1_36.shaded.com.azure.core.tracing.opentelemetry.OpenTelemetryTracer") + .inject(InjectionMode.CLASS_ONLY); + injector + .proxyBuilder( + "io.opentelemetry.javaagent.instrumentation.azurecore.v1_36.shaded.com.azure.core.tracing.opentelemetry.OpenTelemetryTracerProvider") + .inject(InjectionMode.CLASS_ONLY); } @Override @@ -47,7 +68,8 @@ public List typeInstrumentations() { public static class EmptyTypeInstrumentation implements TypeInstrumentation { @Override public ElementMatcher typeMatcher() { - return named("com.azure.core.util.tracing.TracerProvider"); + return namedOneOf( + "com.azure.core.util.tracing.TracerProvider", "com.azure.core.util.tracing.Tracer"); } @Override diff --git a/instrumentation/azure-core/azure-core-1.36/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/azurecore/v1_36/SuppressNestedClientMono.java b/instrumentation/azure-core/azure-core-1.36/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/azurecore/v1_36/SuppressNestedClientHelper.java similarity index 59% rename from instrumentation/azure-core/azure-core-1.36/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/azurecore/v1_36/SuppressNestedClientMono.java rename to instrumentation/azure-core/azure-core-1.36/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/azurecore/v1_36/SuppressNestedClientHelper.java index f899ba48b5bf..ce3bb12e4211 100644 --- a/instrumentation/azure-core/azure-core-1.36/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/azurecore/v1_36/SuppressNestedClientMono.java +++ b/instrumentation/azure-core/azure-core-1.36/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/azurecore/v1_36/SuppressNestedClientHelper.java @@ -14,24 +14,30 @@ import reactor.core.CoreSubscriber; import reactor.core.publisher.Mono; -public class SuppressNestedClientMono extends Mono { +public class SuppressNestedClientHelper { - private final Mono delegate; - - public SuppressNestedClientMono(Mono delegate) { - this.delegate = delegate; - } - - @Override - public void subscribe(CoreSubscriber actual) { + public static Scope disallowNestedClientSpanSync() { Context parentContext = currentContext(); if (doesNotHaveClientSpan(parentContext)) { - try (Scope ignored = disallowNestedClientSpan(parentContext).makeCurrent()) { - delegate.subscribe(actual); - } - } else { - delegate.subscribe(actual); + return disallowNestedClientSpan(parentContext).makeCurrent(); } + return null; + } + + public static Mono disallowNestedClientSpanMono(Mono delegate) { + return new Mono() { + @Override + public void subscribe(CoreSubscriber coreSubscriber) { + Context parentContext = currentContext(); + if (doesNotHaveClientSpan(parentContext)) { + try (Scope ignored = disallowNestedClientSpan(parentContext).makeCurrent()) { + delegate.subscribe(coreSubscriber); + } + } else { + delegate.subscribe(coreSubscriber); + } + } + }; } private static boolean doesNotHaveClientSpan(Context parentContext) { @@ -44,4 +50,6 @@ private static Context disallowNestedClientSpan(Context parentContext) { return SpanKey.HTTP_CLIENT.storeInContext( SpanKey.KIND_CLIENT.storeInContext(parentContext, span), span); } + + private SuppressNestedClientHelper() {} } diff --git a/instrumentation/azure-core/azure-core-1.36/javaagent/src/main/resources/azure-core-1.36/META-INF/services/com.azure.core.util.tracing.Tracer b/instrumentation/azure-core/azure-core-1.36/javaagent/src/main/resources/azure-core-1.36/META-INF/services/com.azure.core.util.tracing.Tracer new file mode 100644 index 000000000000..00cafebf9a66 --- /dev/null +++ b/instrumentation/azure-core/azure-core-1.36/javaagent/src/main/resources/azure-core-1.36/META-INF/services/com.azure.core.util.tracing.Tracer @@ -0,0 +1 @@ +io.opentelemetry.javaagent.instrumentation.azurecore.v1_36.shaded.com.azure.core.tracing.opentelemetry.OpenTelemetryTracer diff --git a/instrumentation/azure-core/azure-core-1.36/javaagent/src/test/java/AzureSdkTest.java b/instrumentation/azure-core/azure-core-1.36/javaagent/src/testAzure/java/io/opentelemetry/javaagent/instrumentation/azurecore/v1_36/AzureSdkTest.java similarity index 96% rename from instrumentation/azure-core/azure-core-1.36/javaagent/src/test/java/AzureSdkTest.java rename to instrumentation/azure-core/azure-core-1.36/javaagent/src/testAzure/java/io/opentelemetry/javaagent/instrumentation/azurecore/v1_36/AzureSdkTest.java index a450e0c9f51b..4661a9cf7ea0 100644 --- a/instrumentation/azure-core/azure-core-1.36/javaagent/src/test/java/AzureSdkTest.java +++ b/instrumentation/azure-core/azure-core-1.36/javaagent/src/testAzure/java/io/opentelemetry/javaagent/instrumentation/azurecore/v1_36/AzureSdkTest.java @@ -3,6 +3,8 @@ * SPDX-License-Identifier: Apache-2.0 */ +package io.opentelemetry.javaagent.instrumentation.azurecore.v1_36; + import static org.assertj.core.api.Assertions.assertThat; import com.azure.core.util.Context; diff --git a/instrumentation/azure-core/azure-core-1.36/javaagent/src/testAzure/java/io/opentelemetry/javaagent/instrumentation/azurecore/v1_36/AzureSdkTestOld.java b/instrumentation/azure-core/azure-core-1.36/javaagent/src/testAzure/java/io/opentelemetry/javaagent/instrumentation/azurecore/v1_36/AzureSdkTestOld.java new file mode 100644 index 000000000000..650f84ebeecb --- /dev/null +++ b/instrumentation/azure-core/azure-core-1.36/javaagent/src/testAzure/java/io/opentelemetry/javaagent/instrumentation/azurecore/v1_36/AzureSdkTestOld.java @@ -0,0 +1,62 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.azurecore.v1_36; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.azure.core.util.Context; +import com.azure.core.util.tracing.Tracer; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.sdk.trace.data.StatusData; +import java.util.Iterator; +import java.util.ServiceLoader; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +// some azure sdks (e.g. EventHubs) are still looking up Tracer via service loader +// and not yet using the new TracerProvider +class AzureSdkTestOld { + + @RegisterExtension + public static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + @Test + void testHelperClassesInjected() { + com.azure.core.util.tracing.Tracer azTracer = createAzTracer(); + assertThat(azTracer.isEnabled()).isTrue(); + + assertThat(azTracer.getClass().getName()) + .isEqualTo( + "io.opentelemetry.javaagent.instrumentation.azurecore.v1_36.shaded" + + ".com.azure.core.tracing.opentelemetry.OpenTelemetryTracer"); + } + + @Test + void testSpan() { + com.azure.core.util.tracing.Tracer azTracer = createAzTracer(); + Context context = azTracer.start("hello", Context.NONE); + azTracer.end(null, null, context); + + testing.waitAndAssertTracesWithoutScopeVersionVerification( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("hello") + .hasKind(SpanKind.INTERNAL) + .hasStatus(StatusData.unset()) + .hasAttributesSatisfying(Attributes::isEmpty))); + } + + private static com.azure.core.util.tracing.Tracer createAzTracer() { + Iterable tracers = + ServiceLoader.load(com.azure.core.util.tracing.Tracer.class); + Iterator it = tracers.iterator(); + return it.hasNext() ? it.next() : null; + } +} diff --git a/instrumentation/azure-core/azure-core-1.36/library-instrumentation-shaded/build.gradle.kts b/instrumentation/azure-core/azure-core-1.36/library-instrumentation-shaded/build.gradle.kts index ed5e44af39b2..423f391f7e29 100644 --- a/instrumentation/azure-core/azure-core-1.36/library-instrumentation-shaded/build.gradle.kts +++ b/instrumentation/azure-core/azure-core-1.36/library-instrumentation-shaded/build.gradle.kts @@ -1,13 +1,12 @@ plugins { - id("com.github.johnrengelman.shadow") - + id("com.gradleup.shadow") id("otel.java-conventions") } group = "io.opentelemetry.javaagent.instrumentation" dependencies { - implementation("com.azure:azure-core-tracing-opentelemetry:1.0.0-beta.32") + implementation("com.azure:azure-core-tracing-opentelemetry:1.0.0-beta.42") } tasks { diff --git a/instrumentation/azure-core/azure-core-1.36/library-instrumentation-shaded/src/main/java/com/azure/core/tracing/opentelemetry/OpenTelemetryTracingOptions.java b/instrumentation/azure-core/azure-core-1.36/library-instrumentation-shaded/src/main/java/com/azure/core/tracing/opentelemetry/OpenTelemetryTracingOptions.java new file mode 100644 index 000000000000..99338d6958ea --- /dev/null +++ b/instrumentation/azure-core/azure-core-1.36/library-instrumentation-shaded/src/main/java/com/azure/core/tracing/opentelemetry/OpenTelemetryTracingOptions.java @@ -0,0 +1,16 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.azure.core.tracing.opentelemetry; + +import com.azure.core.util.TracingOptions; + +/** + * Replace {@link OpenTelemetryTracingOptions} from com.azure:azure-core-tracing-opentelemetry with + * a stub. Auto instrumentation does not use {@link OpenTelemetryTracingOptions}. This is needed + * because {@link OpenTelemetryTracingOptions} calls super constructor in {@link TracingOptions} + * that does exist in com.azure:azure-core:1.36.0 which triggers muzzle failure. + */ +public class OpenTelemetryTracingOptions extends TracingOptions {} diff --git a/instrumentation/c3p0-0.9/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/c3p0/v0_9/C3p0InstrumentationTest.java b/instrumentation/c3p0-0.9/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/c3p0/v0_9/C3p0InstrumentationTest.java index dfe5d3b80411..6d1525c751eb 100644 --- a/instrumentation/c3p0-0.9/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/c3p0/v0_9/C3p0InstrumentationTest.java +++ b/instrumentation/c3p0-0.9/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/c3p0/v0_9/C3p0InstrumentationTest.java @@ -11,7 +11,7 @@ import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; import org.junit.jupiter.api.extension.RegisterExtension; -public class C3p0InstrumentationTest extends AbstractC3p0InstrumentationTest { +class C3p0InstrumentationTest extends AbstractC3p0InstrumentationTest { @RegisterExtension static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); diff --git a/instrumentation/c3p0-0.9/library/src/main/java/io/opentelemetry/instrumentation/c3p0/v0_9/ConnectionPoolMetrics.java b/instrumentation/c3p0-0.9/library/src/main/java/io/opentelemetry/instrumentation/c3p0/v0_9/ConnectionPoolMetrics.java index a4ca9d731e87..0f693a53243b 100644 --- a/instrumentation/c3p0-0.9/library/src/main/java/io/opentelemetry/instrumentation/c3p0/v0_9/ConnectionPoolMetrics.java +++ b/instrumentation/c3p0-0.9/library/src/main/java/io/opentelemetry/instrumentation/c3p0/v0_9/ConnectionPoolMetrics.java @@ -10,7 +10,7 @@ import io.opentelemetry.api.common.Attributes; import io.opentelemetry.api.metrics.BatchCallback; import io.opentelemetry.api.metrics.ObservableLongMeasurement; -import io.opentelemetry.instrumentation.api.metrics.db.DbConnectionPoolMetrics; +import io.opentelemetry.instrumentation.api.incubator.semconv.db.DbConnectionPoolMetrics; import java.sql.SQLException; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; diff --git a/instrumentation/c3p0-0.9/library/src/test/java/io/opentelemetry/instrumentation/c3p0/v0_9/C3p0InstrumentationTest.java b/instrumentation/c3p0-0.9/library/src/test/java/io/opentelemetry/instrumentation/c3p0/v0_9/C3p0InstrumentationTest.java index 6fcacdcd93fb..fde9449ad7de 100644 --- a/instrumentation/c3p0-0.9/library/src/test/java/io/opentelemetry/instrumentation/c3p0/v0_9/C3p0InstrumentationTest.java +++ b/instrumentation/c3p0-0.9/library/src/test/java/io/opentelemetry/instrumentation/c3p0/v0_9/C3p0InstrumentationTest.java @@ -12,7 +12,7 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.extension.RegisterExtension; -public class C3p0InstrumentationTest extends AbstractC3p0InstrumentationTest { +class C3p0InstrumentationTest extends AbstractC3p0InstrumentationTest { @RegisterExtension static final InstrumentationExtension testing = LibraryInstrumentationExtension.create(); diff --git a/instrumentation/camel-2.20/README.md b/instrumentation/camel-2.20/README.md index 0d3dd09472a3..6b115d4de799 100644 --- a/instrumentation/camel-2.20/README.md +++ b/instrumentation/camel-2.20/README.md @@ -1,5 +1,5 @@ # Settings for the Apache Camel instrumentation -| System property | Type | Default | Description | -|---|---|---|---| -| `otel.instrumentation.camel.experimental-span-attributes` | Boolean | `false` | Enable the capture of experimental span attributes. | +| System property | Type | Default | Description | +| --------------------------------------------------------- | ------- | ------- | --------------------------------------------------- | +| `otel.instrumentation.camel.experimental-span-attributes` | Boolean | `false` | Enable the capture of experimental span attributes. | diff --git a/instrumentation/camel-2.20/javaagent-unit-tests/build.gradle.kts b/instrumentation/camel-2.20/javaagent-unit-tests/build.gradle.kts index fe571317f1fa..17b83b78922e 100644 --- a/instrumentation/camel-2.20/javaagent-unit-tests/build.gradle.kts +++ b/instrumentation/camel-2.20/javaagent-unit-tests/build.gradle.kts @@ -4,7 +4,7 @@ plugins { dependencies { testImplementation(project(":instrumentation:camel-2.20:javaagent")) - testImplementation(project(":instrumentation-api-semconv")) + testImplementation(project(":instrumentation-api-incubator")) testImplementation(project(":javaagent-extension-api")) testImplementation("org.apache.camel:camel-core:2.20.1") diff --git a/instrumentation/camel-2.20/javaagent-unit-tests/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/decorators/SanitizationTest.groovy b/instrumentation/camel-2.20/javaagent-unit-tests/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/decorators/SanitizationTest.groovy deleted file mode 100644 index 2d4e848e9ce5..000000000000 --- a/instrumentation/camel-2.20/javaagent-unit-tests/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/decorators/SanitizationTest.groovy +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.javaagent.instrumentation.apachecamel.decorators - -import org.apache.camel.Exchange -import org.apache.camel.Message -import spock.lang.Specification -import spock.lang.Unroll - -class SanitizationTest extends Specification { - - @Unroll - def "sanitize cql #originalCql"() { - - setup: - def decorator = new DbSpanDecorator("cql", "") - def exchange = Mock(Exchange) { - getIn() >> Mock(Message) { - getHeader("CamelCqlQuery") >> originalCql - } - } - def actualSanitized = decorator.getStatement(exchange, null) - - expect: - actualSanitized == sanitizedCql - - where: - originalCql | sanitizedCql - "FROM TABLE WHERE FIELD>=-1234" | "FROM TABLE WHERE FIELD>=?" - "SELECT Name, Phone.Number FROM Contact WHERE Address.State = 'NY'" | "SELECT Name, Phone.Number FROM Contact WHERE Address.State = ?" - "FROM col WHERE @Tag='Something'" | "FROM col WHERE @Tag=?" - } - - @Unroll - def "sanitize jdbc #originalSql"() { - - setup: - def decorator = new DbSpanDecorator("jdbc", "") - def exchange = Mock(Exchange) { - getIn() >> Mock(Message) { - getBody() >> originalSql - } - } - def actualSanitized = decorator.getStatement(exchange, null) - - expect: - actualSanitized == sanitizedSql - - where: - originalSql | sanitizedSql - "SELECT 3" | "SELECT ?" - "SELECT * FROM TABLE WHERE FIELD = 1234" | "SELECT * FROM TABLE WHERE FIELD = ?" - "SELECT * FROM TABLE WHERE FIELD<-1234" | "SELECT * FROM TABLE WHERE FIELD> Mock(Message) { - getHeader("CamelSqlQuery") >> originalSql - } - } - def actualSanitized = decorator.getStatement(exchange, null) - - expect: - actualSanitized == sanitizedSql - - where: - originalSql | sanitizedSql - "SELECT * FROM table WHERE col1=1234 AND col2>3" | "SELECT * FROM table WHERE col1=? AND col2>?" - "UPDATE table SET col=12" | "UPDATE table SET col=?" - 'insert into table where col=321' | 'insert into table where col=?' - } - - -} diff --git a/instrumentation/camel-2.20/javaagent-unit-tests/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/CamelPropagationUtilTest.java b/instrumentation/camel-2.20/javaagent-unit-tests/src/test/java/io/opentelemetry/javaagent/instrumentation/apachecamel/CamelPropagationUtilTest.java similarity index 100% rename from instrumentation/camel-2.20/javaagent-unit-tests/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/CamelPropagationUtilTest.java rename to instrumentation/camel-2.20/javaagent-unit-tests/src/test/java/io/opentelemetry/javaagent/instrumentation/apachecamel/CamelPropagationUtilTest.java diff --git a/instrumentation/camel-2.20/javaagent-unit-tests/src/test/java/io/opentelemetry/javaagent/instrumentation/apachecamel/decorators/SanitizationTest.java b/instrumentation/camel-2.20/javaagent-unit-tests/src/test/java/io/opentelemetry/javaagent/instrumentation/apachecamel/decorators/SanitizationTest.java new file mode 100644 index 000000000000..a59f82f86897 --- /dev/null +++ b/instrumentation/camel-2.20/javaagent-unit-tests/src/test/java/io/opentelemetry/javaagent/instrumentation/apachecamel/decorators/SanitizationTest.java @@ -0,0 +1,104 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.apachecamel.decorators; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.stream.Stream; +import org.apache.camel.Exchange; +import org.apache.camel.Message; +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; + +class SanitizationTest { + + @ParameterizedTest + @ArgumentsSource(CqlArgs.class) + void sanitizeCql(String original, String expected) { + DbSpanDecorator decorator = new DbSpanDecorator("cql", ""); + + Exchange exchange = mock(Exchange.class); + Message message = mock(Message.class); + when(message.getHeader("CamelCqlQuery")).thenReturn(original); + when(exchange.getIn()).thenReturn(message); + + String actualSanitized = decorator.getStatement(exchange, null); + assertEquals(expected, actualSanitized); + } + + @ParameterizedTest + @ArgumentsSource(JdbcArgs.class) + void sanitizeJdbc(String original, String expected) { + DbSpanDecorator decorator = new DbSpanDecorator("jdbc", ""); + + Exchange exchange = mock(Exchange.class); + Message message = mock(Message.class); + when(message.getBody()).thenReturn(original); + when(exchange.getIn()).thenReturn(message); + + String actualSanitized = decorator.getStatement(exchange, null); + assertEquals(expected, actualSanitized); + } + + @ParameterizedTest + @ArgumentsSource(SqlArgs.class) + void sanitizeSql(String original, String expected) { + + DbSpanDecorator decorator = new DbSpanDecorator("sql", ""); + + Exchange exchange = mock(Exchange.class); + Message message = mock(Message.class); + when(message.getHeader("CamelSqlQuery")).thenReturn(original); + when(exchange.getIn()).thenReturn(message); + + String actualSanitized = decorator.getStatement(exchange, null); + assertEquals(expected, actualSanitized); + } + + static class SqlArgs implements ArgumentsProvider { + @Override + public Stream provideArguments(ExtensionContext context) { + return Stream.of( + Arguments.of( + "SELECT * FROM table WHERE col1=1234 AND col2>3", + "SELECT * FROM table WHERE col1=? AND col2>?"), + Arguments.of("UPDATE table SET col=12", "UPDATE table SET col=?"), + Arguments.of("insert into table where col=321", "insert into table where col=?")); + } + } + + static class CqlArgs implements ArgumentsProvider { + @Override + public Stream provideArguments(ExtensionContext context) { + return Stream.of( + Arguments.of("FROM TABLE WHERE FIELD>=-1234", "FROM TABLE WHERE FIELD>=?"), + Arguments.of( + "SELECT Name, Phone.Number FROM Contact WHERE Address.State = 'NY'", + "SELECT Name, Phone.Number FROM Contact WHERE Address.State = ?"), + Arguments.of("FROM col WHERE @Tag='Something'", "FROM col WHERE @Tag=?")); + } + } + + static class JdbcArgs implements ArgumentsProvider { + @Override + public Stream provideArguments(ExtensionContext context) { + return Stream.of( + Arguments.of("SELECT 3", "SELECT ?"), + Arguments.of( + "SELECT * FROM TABLE WHERE FIELD = 1234", "SELECT * FROM TABLE WHERE FIELD = ?"), + Arguments.of( + "SELECT * FROM TABLE WHERE FIELD<-1234", "SELECT * FROM TABLE WHERE FIELD loadDecorators() { 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("cql", new DbSpanDecorator("cql", DbIncubatingAttributes.DbSystemValues.CASSANDRA)); result.put("direct", new InternalSpanDecorator()); result.put("direct-vm", new InternalSpanDecorator()); result.put("disruptor", new InternalSpanDecorator()); @@ -36,12 +36,14 @@ private static Map loadDecorators() { result.put("https4", new Https4SpanDecorator()); result.put("http", new HttpSpanDecorator()); result.put("ironmq", new MessagingSpanDecorator("ironmq")); - result.put("jdbc", new DbSpanDecorator("jdbc", DbSystemValues.OTHER_SQL)); + result.put( + "jdbc", new DbSpanDecorator("jdbc", DbIncubatingAttributes.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( + "mongodb", new DbSpanDecorator("mongodb", DbIncubatingAttributes.DbSystemValues.MONGODB)); result.put("mqtt", new MessagingSpanDecorator("mqtt")); result.put("netty-http4", new HttpSpanDecorator()); result.put("netty-http", new HttpSpanDecorator()); @@ -52,7 +54,7 @@ private static Map loadDecorators() { 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("sql", new DbSpanDecorator("sql", DbIncubatingAttributes.DbSystemValues.OTHER_SQL)); result.put("stomp", new MessagingSpanDecorator("stomp")); result.put("timer", new TimerSpanDecorator()); result.put("undertow", new HttpSpanDecorator()); diff --git a/instrumentation/camel-2.20/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecamel/decorators/HttpSpanDecorator.java b/instrumentation/camel-2.20/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecamel/decorators/HttpSpanDecorator.java index b2c461977e37..8e1c8b49f3fb 100644 --- a/instrumentation/camel-2.20/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecamel/decorators/HttpSpanDecorator.java +++ b/instrumentation/camel-2.20/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecamel/decorators/HttpSpanDecorator.java @@ -23,14 +23,20 @@ package io.opentelemetry.javaagent.instrumentation.apachecamel.decorators; +import static io.opentelemetry.instrumentation.api.internal.AttributesExtractorUtil.internalSet; +import static io.opentelemetry.instrumentation.api.internal.HttpConstants._OTHER; + import io.opentelemetry.api.common.AttributesBuilder; import io.opentelemetry.context.Context; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteHolder; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteSource; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRoute; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRouteSource; +import io.opentelemetry.javaagent.bootstrap.internal.AgentCommonConfig; import io.opentelemetry.javaagent.instrumentation.apachecamel.CamelDirection; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.opentelemetry.semconv.HttpAttributes; +import io.opentelemetry.semconv.UrlAttributes; import java.net.MalformedURLException; import java.net.URL; +import java.util.Set; import javax.annotation.Nullable; import org.apache.camel.Endpoint; import org.apache.camel.Exchange; @@ -39,6 +45,8 @@ class HttpSpanDecorator extends BaseSpanDecorator { private static final String POST_METHOD = "POST"; private static final String GET_METHOD = "GET"; + private static final Set knownMethods = + AgentCommonConfig.get().getKnownHttpRequestMethods(); protected String getProtocol() { return "http"; @@ -89,12 +97,15 @@ public void pre( CamelDirection camelDirection) { super.pre(attributes, exchange, endpoint, camelDirection); - String httpUrl = getHttpUrl(exchange, endpoint); - if (httpUrl != null) { - attributes.put(SemanticAttributes.HTTP_URL, httpUrl); - } + internalSet(attributes, UrlAttributes.URL_FULL, getHttpUrl(exchange, endpoint)); - attributes.put(SemanticAttributes.HTTP_METHOD, getHttpMethod(exchange, endpoint)); + String method = getHttpMethod(exchange, endpoint); + if (method == null || knownMethods.contains(method)) { + internalSet(attributes, HttpAttributes.HTTP_REQUEST_METHOD, method); + } else { + internalSet(attributes, HttpAttributes.HTTP_REQUEST_METHOD, _OTHER); + internalSet(attributes, HttpAttributes.HTTP_REQUEST_METHOD_ORIGINAL, method); + } } private static boolean shouldAppendHttpRoute(CamelDirection camelDirection) { @@ -122,9 +133,9 @@ public void updateServerSpanName( if (!shouldAppendHttpRoute(camelDirection)) { return; } - HttpRouteHolder.updateHttpRoute( + HttpServerRoute.update( context, - HttpRouteSource.CONTROLLER, + HttpServerRouteSource.CONTROLLER, (c, exchange, endpoint) -> getPath(exchange, endpoint), camelExchange, camelEndpoint); @@ -156,7 +167,7 @@ public void post(AttributesBuilder attributes, Exchange exchange, Endpoint endpo if (exchange.hasOut()) { Object responseCode = exchange.getOut().getHeader(Exchange.HTTP_RESPONSE_CODE); if (responseCode instanceof Integer) { - attributes.put(SemanticAttributes.HTTP_STATUS_CODE, (Integer) responseCode); + attributes.put(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, (Integer) responseCode); } } } diff --git a/instrumentation/camel-2.20/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecamel/decorators/KafkaSpanDecorator.java b/instrumentation/camel-2.20/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecamel/decorators/KafkaSpanDecorator.java index 9efd4f9e7c2e..fc6e1fc87113 100644 --- a/instrumentation/camel-2.20/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecamel/decorators/KafkaSpanDecorator.java +++ b/instrumentation/camel-2.20/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecamel/decorators/KafkaSpanDecorator.java @@ -25,7 +25,7 @@ import io.opentelemetry.api.common.AttributesBuilder; import io.opentelemetry.javaagent.instrumentation.apachecamel.CamelDirection; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes; import java.util.Map; import org.apache.camel.Endpoint; import org.apache.camel.Exchange; @@ -60,15 +60,12 @@ public void pre( CamelDirection camelDirection) { super.pre(attributes, exchange, endpoint, camelDirection); - attributes.put(SemanticAttributes.MESSAGING_OPERATION, "process"); + attributes.put(MessagingIncubatingAttributes.MESSAGING_OPERATION, "process"); Integer partition = exchange.getIn().getHeader(PARTITION, Integer.class); if (partition != null) { - if (camelDirection == CamelDirection.OUTBOUND) { - attributes.put(SemanticAttributes.MESSAGING_KAFKA_DESTINATION_PARTITION, partition); - } else { - attributes.put(SemanticAttributes.MESSAGING_KAFKA_SOURCE_PARTITION, partition); - } + attributes.put( + MessagingIncubatingAttributes.MESSAGING_DESTINATION_PARTITION_ID, partition.toString()); } if (CAPTURE_EXPERIMENTAL_SPAN_ATTRIBUTES) { diff --git a/instrumentation/camel-2.20/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecamel/decorators/MessagingSpanDecorator.java b/instrumentation/camel-2.20/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecamel/decorators/MessagingSpanDecorator.java index 455e2ada58cb..478f3e07d087 100644 --- a/instrumentation/camel-2.20/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecamel/decorators/MessagingSpanDecorator.java +++ b/instrumentation/camel-2.20/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecamel/decorators/MessagingSpanDecorator.java @@ -26,7 +26,7 @@ import io.opentelemetry.api.common.AttributesBuilder; import io.opentelemetry.api.trace.SpanKind; import io.opentelemetry.javaagent.instrumentation.apachecamel.CamelDirection; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes; import java.net.URI; import java.util.Map; import org.apache.camel.Endpoint; @@ -59,11 +59,12 @@ public void pre( super.pre(attributes, exchange, endpoint, camelDirection); attributes.put( - SemanticAttributes.MESSAGING_DESTINATION_NAME, getDestination(exchange, endpoint)); + MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME, + getDestination(exchange, endpoint)); String messageId = getMessageId(exchange); if (messageId != null) { - attributes.put(SemanticAttributes.MESSAGING_MESSAGE_ID, messageId); + attributes.put(MessagingIncubatingAttributes.MESSAGING_MESSAGE_ID, messageId); } } diff --git a/instrumentation/camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/DirectCamelTest.groovy b/instrumentation/camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/DirectCamelTest.groovy deleted file mode 100644 index 06079281939a..000000000000 --- a/instrumentation/camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/DirectCamelTest.groovy +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.javaagent.instrumentation.apachecamel - -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 - -import static io.opentelemetry.api.trace.SpanKind.INTERNAL - -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 { - "camel.uri" "direct://input" - } - } - it.span(1) { - name "receiver" - kind INTERNAL - parentSpanId parent.span(0).spanId - attributes { - "camel.uri" "direct://receiver" - } - } - } - } - } -} diff --git a/instrumentation/camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/DirectConfig.groovy b/instrumentation/camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/DirectConfig.groovy deleted file mode 100644 index 9ec2ee387d64..000000000000 --- a/instrumentation/camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/DirectConfig.groovy +++ /dev/null @@ -1,45 +0,0 @@ -/* - * 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/instrumentation/camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/MulticastConfig.groovy b/instrumentation/camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/MulticastConfig.groovy deleted file mode 100644 index e1f5bd587777..000000000000 --- a/instrumentation/camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/MulticastConfig.groovy +++ /dev/null @@ -1,62 +0,0 @@ -/* - * 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/instrumentation/camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/MulticastDirectCamelTest.groovy b/instrumentation/camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/MulticastDirectCamelTest.groovy deleted file mode 100644 index 69aad50c30c8..000000000000 --- a/instrumentation/camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/MulticastDirectCamelTest.groovy +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.javaagent.instrumentation.apachecamel - -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 - -import static io.opentelemetry.api.trace.SpanKind.INTERNAL - -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 { - "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 { - "camel.uri" "direct://first" - } - } - it.span(indexOfSecond) { - name "second" - kind INTERNAL - parentSpanId parent.span(0).spanId - attributes { - "camel.uri" "direct://second" - } - } - } - } - } -} diff --git a/instrumentation/camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/RestCamelTest.groovy b/instrumentation/camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/RestCamelTest.groovy deleted file mode 100644 index 9c18ec4832d8..000000000000 --- a/instrumentation/camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/RestCamelTest.groovy +++ /dev/null @@ -1,126 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.javaagent.instrumentation.apachecamel - -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 - -import static io.opentelemetry.api.trace.SpanKind.CLIENT -import static io.opentelemetry.api.trace.SpanKind.INTERNAL -import static io.opentelemetry.api.trace.SpanKind.SERVER - -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 { - "camel.uri" "direct://start" - } - } - it.span(1) { - name "GET" - kind CLIENT - parentSpanId(span(0).spanId) - attributes { - "$SemanticAttributes.HTTP_METHOD" "GET" - "$SemanticAttributes.HTTP_STATUS_CODE" 200 - "camel.uri" "rest://get:api/%7Bmodule%7D/unit/%7BunitId%7D" - } - } - it.span(2) { - name "GET /api/{module}/unit/{unitId}" - kind SERVER - parentSpanId(span(1).spanId) - attributes { - "$SemanticAttributes.HTTP_SCHEME" "http" - "$SemanticAttributes.HTTP_TARGET" "/api/firstModule/unit/unitOne" - "$SemanticAttributes.HTTP_STATUS_CODE" 200 - "$SemanticAttributes.USER_AGENT_ORIGINAL" String - "$SemanticAttributes.HTTP_METHOD" "GET" - "$SemanticAttributes.HTTP_ROUTE" "/api/{module}/unit/{unitId}" - "net.protocol.name" "http" - "net.protocol.version" "1.1" - "$SemanticAttributes.NET_HOST_NAME" "localhost" - "$SemanticAttributes.NET_HOST_PORT" port - "$SemanticAttributes.NET_SOCK_PEER_ADDR" "127.0.0.1" - "$SemanticAttributes.NET_SOCK_PEER_PORT" Long - "$SemanticAttributes.NET_SOCK_HOST_ADDR" "127.0.0.1" - } - } - it.span(3) { - name "GET /api/{module}/unit/{unitId}" - kind INTERNAL - parentSpanId(span(2).spanId) - attributes { - "$SemanticAttributes.HTTP_METHOD" "GET" - "$SemanticAttributes.HTTP_URL" "http://localhost:$port/api/firstModule/unit/unitOne" - "camel.uri" String - } - } - it.span(4) { - name "moduleUnit" - kind INTERNAL - parentSpanId(span(3).spanId) - attributes { - "camel.uri" "direct://moduleUnit" - } - } - } - } - } -} diff --git a/instrumentation/camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/RestConfig.groovy b/instrumentation/camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/RestConfig.groovy deleted file mode 100644 index 1345de8616e0..000000000000 --- a/instrumentation/camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/RestConfig.groovy +++ /dev/null @@ -1,47 +0,0 @@ -/* - * 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/instrumentation/camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/SingleServiceCamelTest.groovy b/instrumentation/camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/SingleServiceCamelTest.groovy deleted file mode 100644 index 16d672899005..000000000000 --- a/instrumentation/camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/SingleServiceCamelTest.groovy +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.javaagent.instrumentation.apachecamel - -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 - -import static io.opentelemetry.api.trace.SpanKind.SERVER - -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 "POST /camelService" - attributes { - "$SemanticAttributes.HTTP_METHOD" "POST" - "$SemanticAttributes.HTTP_URL" "${address.resolve("/camelService")}" - "camel.uri" "${address.resolve("/camelService")}".replace("localhost", "0.0.0.0") - } - } - } - } - } -} diff --git a/instrumentation/camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/SingleServiceConfig.groovy b/instrumentation/camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/SingleServiceConfig.groovy deleted file mode 100644 index 603a4e713ff8..000000000000 --- a/instrumentation/camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/SingleServiceConfig.groovy +++ /dev/null @@ -1,35 +0,0 @@ -/* - * 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/instrumentation/camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/TwoServicesConfig.groovy b/instrumentation/camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/TwoServicesConfig.groovy deleted file mode 100644 index 8a979ec9c27f..000000000000 --- a/instrumentation/camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/TwoServicesConfig.groovy +++ /dev/null @@ -1,54 +0,0 @@ -/* - * 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/instrumentation/camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/TwoServicesWithDirectClientCamelTest.groovy b/instrumentation/camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/TwoServicesWithDirectClientCamelTest.groovy deleted file mode 100644 index 0231b4109d8a..000000000000 --- a/instrumentation/camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/TwoServicesWithDirectClientCamelTest.groovy +++ /dev/null @@ -1,153 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.javaagent.instrumentation.apachecamel - -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 - -import static io.opentelemetry.api.trace.SpanKind.CLIENT -import static io.opentelemetry.api.trace.SpanKind.INTERNAL -import static io.opentelemetry.api.trace.SpanKind.SERVER - -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 { - "camel.uri" "direct://input" - } - } - it.span(1) { - name "POST" - kind CLIENT - parentSpanId(span(0).spanId) - attributes { - "$SemanticAttributes.HTTP_METHOD" "POST" - "$SemanticAttributes.HTTP_URL" "http://localhost:$portOne/serviceOne" - "$SemanticAttributes.HTTP_STATUS_CODE" 200 - "camel.uri" "http://localhost:$portOne/serviceOne" - } - } - it.span(2) { - name "POST /serviceOne" - kind SERVER - parentSpanId(span(1).spanId) - attributes { - "$SemanticAttributes.HTTP_METHOD" "POST" - "$SemanticAttributes.HTTP_URL" "http://localhost:$portOne/serviceOne" - "$SemanticAttributes.HTTP_STATUS_CODE" 200 - "camel.uri" "http://0.0.0.0:$portOne/serviceOne" - } - } - it.span(3) { - name "POST" - kind CLIENT - parentSpanId(span(2).spanId) - attributes { - "$SemanticAttributes.HTTP_METHOD" "POST" - "$SemanticAttributes.HTTP_URL" "http://127.0.0.1:$portTwo/serviceTwo" - "$SemanticAttributes.HTTP_STATUS_CODE" 200 - "camel.uri" "http://127.0.0.1:$portTwo/serviceTwo" - } - } - it.span(4) { - name "POST /serviceTwo" - kind SERVER - parentSpanId(span(3).spanId) - attributes { - "$SemanticAttributes.HTTP_METHOD" "POST" - "$SemanticAttributes.HTTP_STATUS_CODE" 200 - "$SemanticAttributes.HTTP_SCHEME" "http" - "$SemanticAttributes.HTTP_TARGET" "/serviceTwo" - "$SemanticAttributes.USER_AGENT_ORIGINAL" "Jakarta Commons-HttpClient/3.1" - "$SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH" Long - "$SemanticAttributes.HTTP_ROUTE" "/serviceTwo" - "net.protocol.name" "http" - "net.protocol.version" "1.1" - "$SemanticAttributes.NET_HOST_NAME" "127.0.0.1" - "$SemanticAttributes.NET_HOST_PORT" portTwo - "$SemanticAttributes.NET_SOCK_PEER_ADDR" "127.0.0.1" - "$SemanticAttributes.NET_SOCK_PEER_PORT" Long - } - } - it.span(5) { - name "POST /serviceTwo" - kind INTERNAL - parentSpanId(span(4).spanId) - attributes { - "$SemanticAttributes.HTTP_METHOD" "POST" - "$SemanticAttributes.HTTP_URL" "http://127.0.0.1:$portTwo/serviceTwo" - "camel.uri" "jetty:http://0.0.0.0:$portTwo/serviceTwo?arg=value" - } - } - } - } - } -} diff --git a/instrumentation/camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/aws/AwsConnector.groovy b/instrumentation/camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/aws/AwsConnector.groovy deleted file mode 100644 index 043a8f99b5e4..000000000000 --- a/instrumentation/camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/aws/AwsConnector.groovy +++ /dev/null @@ -1,173 +0,0 @@ -/* - * 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/instrumentation/camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/aws/AwsSpan.groovy b/instrumentation/camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/aws/AwsSpan.groovy deleted file mode 100644 index 03f68912db45..000000000000 --- a/instrumentation/camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/aws/AwsSpan.groovy +++ /dev/null @@ -1,99 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.javaagent.instrumentation.apachecamel.aws - -import io.opentelemetry.instrumentation.test.asserts.TraceAssert -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes - -import static io.opentelemetry.api.trace.SpanKind.CLIENT - -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 - "rpc.system" "aws-api" - "rpc.method" spanName.substring(3) - "rpc.service" "Amazon S3" - "aws.bucket.name" bucketName - "http.method" method - "http.status_code" 200 - "http.url" String - "net.protocol.name" "http" - "net.protocol.version" "1.1" - "net.peer.name" String - "net.peer.port" { it == null || it instanceof 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 - "rpc.system" "aws-api" - "rpc.method" spanName.substring(4) - "rpc.service" "AmazonSQS" - "aws.queue.name" { it == null || it == queueName } - "aws.queue.url" { it == null || it == queueUrl } - "http.method" "POST" - "http.status_code" 200 - "http.url" String - "$SemanticAttributes.USER_AGENT_ORIGINAL" { it == null || it instanceof String } - "http.request_content_length" { it == null || it instanceof Long } - "http.response_content_length" { it == null || it instanceof Long } - "net.protocol.name" "http" - "net.protocol.version" "1.1" - "net.peer.name" String - "net.peer.port" { it == null || it instanceof Number } - } - } - } - - 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 - "rpc.system" "aws-api" - "rpc.method" spanName.substring(4) - "rpc.service" "AmazonSNS" - "http.method" "POST" - "http.status_code" 200 - "http.url" String - "net.protocol.name" "http" - "net.protocol.version" "1.1" - "net.peer.name" String - "net.peer.port" { it == null || it instanceof Number } - } - } - } - -} diff --git a/instrumentation/camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/aws/CamelSpan.groovy b/instrumentation/camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/aws/CamelSpan.groovy deleted file mode 100644 index b084f45f31dc..000000000000 --- a/instrumentation/camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/aws/CamelSpan.groovy +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.javaagent.instrumentation.apachecamel.aws - -import io.opentelemetry.instrumentation.test.asserts.TraceAssert -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes - -import static io.opentelemetry.api.trace.SpanKind.INTERNAL - -class CamelSpan { - - static direct(TraceAssert traceAssert, int index, spanName) { - return traceAssert.span(index) { - name spanName - kind INTERNAL - hasNoParent() - attributes { - "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 { - "camel.uri" "aws-sqs://${queueName}?amazonSQSClient=%23sqsClient&delay=1000" - "$SemanticAttributes.MESSAGING_DESTINATION_NAME" 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 { - "camel.uri" "aws-sqs://${queueName}?amazonSQSClient=%23sqsClient&delay=1000" - "$SemanticAttributes.MESSAGING_DESTINATION_NAME" queueName - "$SemanticAttributes.MESSAGING_MESSAGE_ID" String - } - } - } - - static snsPublish(TraceAssert traceAssert, int index, topicName, parentSpan = null) { - return traceAssert.span(index) { - name topicName - kind INTERNAL - childOf parentSpan - attributes { - "camel.uri" "aws-sns://${topicName}?amazonSNSClient=%23snsClient" - "$SemanticAttributes.MESSAGING_DESTINATION_NAME" topicName - } - } - } - - static s3(TraceAssert traceAssert, int index, parentSpan = null) { - return traceAssert.span(index) { - name "aws-s3" - kind INTERNAL - childOf parentSpan - attributes { - "camel.uri" "aws-s3://${bucketName}?amazonS3Client=%23s3Client" - } - } - } -} diff --git a/instrumentation/camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/aws/CamelSpringApp.groovy b/instrumentation/camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/aws/CamelSpringApp.groovy deleted file mode 100644 index c58735be738d..000000000000 --- a/instrumentation/camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/aws/CamelSpringApp.groovy +++ /dev/null @@ -1,57 +0,0 @@ -/* - * 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/instrumentation/camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/aws/S3CamelTest.groovy b/instrumentation/camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/aws/S3CamelTest.groovy deleted file mode 100644 index 7c08bbfbeab6..000000000000 --- a/instrumentation/camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/aws/S3CamelTest.groovy +++ /dev/null @@ -1,109 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.javaagent.instrumentation.apachecamel.aws - -import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification -import spock.lang.Ignore -import spock.lang.Shared - -import static io.opentelemetry.api.trace.SpanKind.CONSUMER - -@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, [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() - } -} diff --git a/instrumentation/camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/aws/S3Config.groovy b/instrumentation/camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/aws/S3Config.groovy deleted file mode 100644 index f477afb7314d..000000000000 --- a/instrumentation/camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/aws/S3Config.groovy +++ /dev/null @@ -1,48 +0,0 @@ -/* - * 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/instrumentation/camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/aws/SnsCamelTest.groovy b/instrumentation/camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/aws/SnsCamelTest.groovy deleted file mode 100644 index 71af5016c245..000000000000 --- a/instrumentation/camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/aws/SnsCamelTest.groovy +++ /dev/null @@ -1,133 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.javaagent.instrumentation.apachecamel.aws - -import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification -import spock.lang.Ignore -import spock.lang.Shared - -import static io.opentelemetry.api.trace.SpanKind.CONSUMER - -@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, [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, [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/instrumentation/camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/aws/SnsConfig.groovy b/instrumentation/camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/aws/SnsConfig.groovy deleted file mode 100644 index 640da33bfee6..000000000000 --- a/instrumentation/camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/aws/SnsConfig.groovy +++ /dev/null @@ -1,45 +0,0 @@ -/* - * 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/instrumentation/camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/aws/SqsCamelTest.groovy b/instrumentation/camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/aws/SqsCamelTest.groovy deleted file mode 100644 index 7f028eeb6504..000000000000 --- a/instrumentation/camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/aws/SqsCamelTest.groovy +++ /dev/null @@ -1,138 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.javaagent.instrumentation.apachecamel.aws - -import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification -import spock.lang.Shared - -import static io.opentelemetry.api.trace.SpanKind.CONSUMER -import static io.opentelemetry.api.trace.SpanKind.PRODUCER - -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, ["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, ["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, [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/instrumentation/camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/aws/SqsConfig.groovy b/instrumentation/camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/aws/SqsConfig.groovy deleted file mode 100644 index 370c98a9f6b7..000000000000 --- a/instrumentation/camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/aws/SqsConfig.groovy +++ /dev/null @@ -1,64 +0,0 @@ -/* - * 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/instrumentation/camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/decorators/CassandraTest.groovy b/instrumentation/camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/decorators/CassandraTest.groovy deleted file mode 100644 index 519ce2732750..000000000000 --- a/instrumentation/camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/decorators/CassandraTest.groovy +++ /dev/null @@ -1,114 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.javaagent.instrumentation.apachecamel.decorators - -import com.datastax.driver.core.Cluster -import com.datastax.driver.core.Session -import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification -import io.opentelemetry.instrumentation.test.RetryOnAddressAlreadyInUseTrait -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 org.testcontainers.containers.CassandraContainer -import org.testcontainers.containers.GenericContainer -import spock.lang.Shared - -import static io.opentelemetry.api.trace.SpanKind.CLIENT -import static io.opentelemetry.api.trace.SpanKind.INTERNAL - -class CassandraTest extends AgentInstrumentationSpecification implements RetryOnAddressAlreadyInUseTrait { - - @Shared - ConfigurableApplicationContext server - @Shared - GenericContainer cassandra - @Shared - Cluster cluster - @Shared - String host - @Shared - int port - - Session session - - def setupSpec() { - withRetryOnAddressAlreadyInUse({ - setupSpecUnderRetry() - }) - } - - def setupSpecUnderRetry() { - cassandra = new CassandraContainer() - cassandra.withExposedPorts(9042) - cassandra.start() - - port = cassandra.getFirstMappedPort() - host = cassandra.getHost() - - cluster = cassandra.getCluster() - - def app = new SpringApplication(CassandraConfig) - app.setDefaultProperties(["cassandra.host": host, "cassandra.port": port]) - server = app.run() - } - - def cleanupSpec() { - server?.close() - cluster?.close() - cassandra.stop() - } - - def setup() { - session = cluster.connect() - - session.execute("CREATE KEYSPACE IF NOT EXISTS test WITH REPLICATION = {'class':'SimpleStrategy', 'replication_factor':1};") - session.execute("CREATE TABLE IF NOT EXISTS test.users ( id int primary key, name text );") - session.execute("INSERT INTO test.users (id,name) VALUES (1, 'user1') IF NOT EXISTS;") - session.execute("INSERT INTO test.users (id,name) VALUES (2, 'user2') IF NOT EXISTS;") - } - - def cleanup() { - session?.close() - } - - def "test cassandra "() { - - setup: - def camelContext = server.getBean(CamelContext) - ProducerTemplate template = camelContext.createProducerTemplate() - - when: - def response = template.requestBody("direct:input", null) - - then: - response.first().getString("name") == "user1" - - assertTraces(1) { - trace(0, 2) { - span(0) { - kind INTERNAL - hasNoParent() - attributes { - "camel.uri" "direct://input" - } - } - span(1) { - kind CLIENT - attributes { - "camel.uri" "cql://$host:$port/test" - "$SemanticAttributes.DB_NAME" "test" - "$SemanticAttributes.DB_STATEMENT" "select * from test.users where id=? ALLOW FILTERING" - "$SemanticAttributes.DB_SYSTEM" "cassandra" - } - } - } - } - - } - -} diff --git a/instrumentation/camel-2.20/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apachecamel/DirectCamelTest.java b/instrumentation/camel-2.20/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apachecamel/DirectCamelTest.java new file mode 100644 index 000000000000..758a49963c65 --- /dev/null +++ b/instrumentation/camel-2.20/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apachecamel/DirectCamelTest.java @@ -0,0 +1,82 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.apachecamel; + +import static io.opentelemetry.api.common.AttributeKey.stringKey; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; + +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpServerUsingTest; +import io.opentelemetry.instrumentation.testing.junit.http.HttpServerInstrumentationExtension; +import org.apache.camel.CamelContext; +import org.apache.camel.ProducerTemplate; +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; +import org.springframework.boot.SpringApplication; +import org.springframework.context.ConfigurableApplicationContext; + +class DirectCamelTest extends AbstractHttpServerUsingTest { + + @RegisterExtension + public static final InstrumentationExtension testing = + HttpServerInstrumentationExtension.forAgent(); + + private ConfigurableApplicationContext appContext; + + @Override + protected ConfigurableApplicationContext setupServer() { + SpringApplication app = new SpringApplication(DirectConfig.class); + appContext = app.run(); + return appContext; + } + + @Override + protected void stopServer(ConfigurableApplicationContext ctx) { + ctx.close(); + } + + @Override + protected String getContextPath() { + return ""; + } + + @BeforeAll + protected void setUp() { + startServer(); + } + + @AfterAll + protected void cleanUp() { + cleanupServer(); + } + + @Test + void simpleDirectToSingleService() { + CamelContext camelContext = appContext.getBean(CamelContext.class); + ProducerTemplate template = camelContext.createProducerTemplate(); + + template.sendBody("direct:input", "Example request"); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("input") + .hasKind(SpanKind.INTERNAL) + .hasNoParent() + .hasAttributesSatisfyingExactly( + equalTo(stringKey("camel.uri"), "direct://input")), + span -> + span.hasName("receiver") + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo(stringKey("camel.uri"), "direct://receiver")))); + } +} diff --git a/instrumentation/camel-2.20/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apachecamel/DirectConfig.java b/instrumentation/camel-2.20/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apachecamel/DirectConfig.java new file mode 100644 index 000000000000..1fe96ac8c1ba --- /dev/null +++ b/instrumentation/camel-2.20/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apachecamel/DirectConfig.java @@ -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 + public 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 + public 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/instrumentation/camel-2.20/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apachecamel/MulticastConfig.java b/instrumentation/camel-2.20/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apachecamel/MulticastConfig.java new file mode 100644 index 000000000000..46c9a1bd7e30 --- /dev/null +++ b/instrumentation/camel-2.20/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apachecamel/MulticastConfig.java @@ -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 + public 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 + public 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 + public 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/instrumentation/camel-2.20/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apachecamel/MulticastDirectCamelTest.java b/instrumentation/camel-2.20/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apachecamel/MulticastDirectCamelTest.java new file mode 100644 index 000000000000..4584a54eac4b --- /dev/null +++ b/instrumentation/camel-2.20/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apachecamel/MulticastDirectCamelTest.java @@ -0,0 +1,88 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.apachecamel; + +import static io.opentelemetry.api.common.AttributeKey.stringKey; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; + +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpServerUsingTest; +import io.opentelemetry.instrumentation.testing.junit.http.HttpServerInstrumentationExtension; +import org.apache.camel.CamelContext; +import org.apache.camel.ProducerTemplate; +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; +import org.springframework.boot.SpringApplication; +import org.springframework.context.ConfigurableApplicationContext; + +class MulticastDirectCamelTest extends AbstractHttpServerUsingTest { + + @RegisterExtension + public static final InstrumentationExtension testing = + HttpServerInstrumentationExtension.forAgent(); + + private ConfigurableApplicationContext appContext; + + @Override + protected ConfigurableApplicationContext setupServer() { + SpringApplication app = new SpringApplication(MulticastConfig.class); + appContext = app.run(); + return appContext; + } + + @Override + protected void stopServer(ConfigurableApplicationContext ctx) { + ctx.close(); + } + + @Override + protected String getContextPath() { + return ""; + } + + @BeforeAll + protected void setUp() { + startServer(); + } + + @AfterAll + protected void cleanUp() { + cleanupServer(); + } + + @Test + void parallelMulticastToTwoChildServices() { + CamelContext camelContext = appContext.getBean(CamelContext.class); + ProducerTemplate template = camelContext.createProducerTemplate(); + + template.sendBody("direct:input", "Example request"); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactlyInAnyOrder( + span -> + span.hasName("input") + .hasKind(SpanKind.INTERNAL) + .hasNoParent() + .hasAttributesSatisfyingExactly( + equalTo(stringKey("camel.uri"), "direct://input")), + span -> + span.hasName("first") + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo(stringKey("camel.uri"), "direct://first")), + span -> + span.hasName("second") + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo(stringKey("camel.uri"), "direct://second")))); + } +} diff --git a/instrumentation/camel-2.20/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apachecamel/RestCamelTest.java b/instrumentation/camel-2.20/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apachecamel/RestCamelTest.java new file mode 100644 index 000000000000..feed0b0f1a99 --- /dev/null +++ b/instrumentation/camel-2.20/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apachecamel/RestCamelTest.java @@ -0,0 +1,139 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.apachecamel; + +import static io.opentelemetry.api.common.AttributeKey.stringKey; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.satisfies; + +import com.google.common.collect.ImmutableMap; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpServerUsingTest; +import io.opentelemetry.instrumentation.testing.junit.http.HttpServerInstrumentationExtension; +import io.opentelemetry.semconv.ClientAttributes; +import io.opentelemetry.semconv.HttpAttributes; +import io.opentelemetry.semconv.NetworkAttributes; +import io.opentelemetry.semconv.ServerAttributes; +import io.opentelemetry.semconv.UrlAttributes; +import io.opentelemetry.semconv.UserAgentAttributes; +import org.apache.camel.CamelContext; +import org.apache.camel.ProducerTemplate; +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; +import org.springframework.boot.SpringApplication; +import org.springframework.context.ConfigurableApplicationContext; + +class RestCamelTest extends AbstractHttpServerUsingTest { + + @RegisterExtension + public static final InstrumentationExtension testing = + HttpServerInstrumentationExtension.forAgent(); + + private ConfigurableApplicationContext appContext; + + @Override + protected ConfigurableApplicationContext setupServer() { + SpringApplication app = new SpringApplication(RestConfig.class); + app.setDefaultProperties(ImmutableMap.of("restServer.port", port)); + appContext = app.run(); + return appContext; + } + + @Override + protected void stopServer(ConfigurableApplicationContext ctx) { + ctx.close(); + } + + @Override + protected String getContextPath() { + return ""; + } + + @BeforeAll + protected void setUp() { + startServer(); + } + + @AfterAll + protected void cleanUp() { + cleanupServer(); + } + + @Test + void restComponentServerAndClientCallWithJettyBackend() { + CamelContext camelContext = appContext.getBean(CamelContext.class); + ProducerTemplate template = camelContext.createProducerTemplate(); + + // run client and server in separate threads to simulate "real" rest client/server call + new Thread( + () -> + template.sendBodyAndHeaders( + "direct:start", + null, + ImmutableMap.of("module", "firstModule", "unitId", "unitOne"))) + .start(); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("start") + .hasKind(SpanKind.INTERNAL) + .hasAttributesSatisfyingExactly( + equalTo(stringKey("camel.uri"), "direct://start")), + span -> + span.hasName("GET") + .hasKind(SpanKind.CLIENT) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo( + stringKey("camel.uri"), + "rest://get:api/%7Bmodule%7D/unit/%7BunitId%7D"), + equalTo(HttpAttributes.HTTP_REQUEST_METHOD, "GET"), + equalTo(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, 200L)), + span -> + span.hasName("GET /api/{module}/unit/{unitId}") + .hasKind(SpanKind.SERVER) + .hasParent(trace.getSpan(1)) + .hasAttributesSatisfyingExactly( + equalTo(UrlAttributes.URL_SCHEME, "http"), + equalTo(UrlAttributes.URL_PATH, "/api/firstModule/unit/unitOne"), + equalTo(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, 200L), + equalTo(HttpAttributes.HTTP_REQUEST_METHOD, "GET"), + equalTo(HttpAttributes.HTTP_ROUTE, "/api/{module}/unit/{unitId}"), + equalTo(NetworkAttributes.NETWORK_PROTOCOL_VERSION, "1.1"), + equalTo(ServerAttributes.SERVER_ADDRESS, "localhost"), + equalTo(ServerAttributes.SERVER_PORT, Long.valueOf(port)), + equalTo(ClientAttributes.CLIENT_ADDRESS, "127.0.0.1"), + equalTo(NetworkAttributes.NETWORK_PEER_ADDRESS, "127.0.0.1"), + satisfies( + UserAgentAttributes.USER_AGENT_ORIGINAL, + val -> val.isInstanceOf(String.class)), + satisfies( + NetworkAttributes.NETWORK_PEER_PORT, + val -> val.isInstanceOf(Long.class))), + span -> + span.hasName("GET /api/{module}/unit/{unitId}") + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(2)) + .hasAttributesSatisfyingExactly( + equalTo(HttpAttributes.HTTP_REQUEST_METHOD, "GET"), + equalTo( + UrlAttributes.URL_FULL, + "http://localhost:" + port + "/api/firstModule/unit/unitOne"), + satisfies( + stringKey("camel.uri"), val -> val.isInstanceOf(String.class))), + span -> + span.hasName("moduleUnit") + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(3)) + .hasAttributesSatisfyingExactly( + equalTo(stringKey("camel.uri"), "direct://moduleUnit")))); + } +} diff --git a/instrumentation/camel-2.20/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apachecamel/RestConfig.java b/instrumentation/camel-2.20/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apachecamel/RestConfig.java new file mode 100644 index 000000000000..869f8120611d --- /dev/null +++ b/instrumentation/camel-2.20/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apachecamel/RestConfig.java @@ -0,0 +1,44 @@ +/* + * 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 + public 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/instrumentation/camel-2.20/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apachecamel/SingleServiceCamelTest.java b/instrumentation/camel-2.20/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apachecamel/SingleServiceCamelTest.java new file mode 100644 index 000000000000..4bcbfc8c1631 --- /dev/null +++ b/instrumentation/camel-2.20/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apachecamel/SingleServiceCamelTest.java @@ -0,0 +1,78 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.apachecamel; + +import static io.opentelemetry.api.common.AttributeKey.stringKey; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; + +import com.google.common.collect.ImmutableMap; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpServerUsingTest; +import io.opentelemetry.instrumentation.testing.junit.http.HttpServerInstrumentationExtension; +import io.opentelemetry.semconv.HttpAttributes; +import io.opentelemetry.semconv.UrlAttributes; +import java.net.URI; +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; +import org.springframework.boot.SpringApplication; +import org.springframework.context.ConfigurableApplicationContext; + +class SingleServiceCamelTest extends AbstractHttpServerUsingTest { + + @RegisterExtension + public static final InstrumentationExtension testing = + HttpServerInstrumentationExtension.forAgent(); + + @Override + protected ConfigurableApplicationContext setupServer() { + SpringApplication app = new SpringApplication(SingleServiceConfig.class); + app.setDefaultProperties(ImmutableMap.of("camelService.port", port)); + return app.run(); + } + + @Override + protected void stopServer(ConfigurableApplicationContext ctx) { + ctx.close(); + } + + @Override + protected String getContextPath() { + return ""; + } + + @BeforeAll + protected void setUp() { + startServer(); + } + + @AfterAll + protected void cleanUp() { + cleanupServer(); + } + + @Test + public void singleCamelServiceSpan() { + URI requestUrl = address.resolve("/camelService"); + + client.post(requestUrl.toString(), "testContent").aggregate().join(); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("POST /camelService") + .hasKind(SpanKind.SERVER) + .hasAttributesSatisfyingExactly( + equalTo(HttpAttributes.HTTP_REQUEST_METHOD, "POST"), + equalTo(UrlAttributes.URL_FULL, requestUrl.toString()), + equalTo( + stringKey("camel.uri"), + requestUrl.toString().replace("localhost", "0.0.0.0"))))); + } +} diff --git a/instrumentation/camel-2.20/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apachecamel/SingleServiceConfig.java b/instrumentation/camel-2.20/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apachecamel/SingleServiceConfig.java new file mode 100644 index 000000000000..b862e23a42e4 --- /dev/null +++ b/instrumentation/camel-2.20/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apachecamel/SingleServiceConfig.java @@ -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 + public 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/instrumentation/camel-2.20/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apachecamel/TwoServicesConfig.java b/instrumentation/camel-2.20/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apachecamel/TwoServicesConfig.java new file mode 100644 index 000000000000..f0fe24b4e3bc --- /dev/null +++ b/instrumentation/camel-2.20/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apachecamel/TwoServicesConfig.java @@ -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 + public void configure() { + + 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 + public void configure() { + + 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/instrumentation/camel-2.20/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apachecamel/TwoServicesWithDirectClientCamelTest.java b/instrumentation/camel-2.20/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apachecamel/TwoServicesWithDirectClientCamelTest.java new file mode 100644 index 000000000000..e1ecebc6a11c --- /dev/null +++ b/instrumentation/camel-2.20/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apachecamel/TwoServicesWithDirectClientCamelTest.java @@ -0,0 +1,182 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.apachecamel; + +import static io.opentelemetry.api.common.AttributeKey.stringKey; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.satisfies; + +import com.google.common.collect.ImmutableMap; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.test.utils.PortUtils; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpServerUsingTest; +import io.opentelemetry.instrumentation.testing.junit.http.HttpServerInstrumentationExtension; +import io.opentelemetry.semconv.ClientAttributes; +import io.opentelemetry.semconv.HttpAttributes; +import io.opentelemetry.semconv.NetworkAttributes; +import io.opentelemetry.semconv.ServerAttributes; +import io.opentelemetry.semconv.UrlAttributes; +import io.opentelemetry.semconv.UserAgentAttributes; +import org.apache.camel.CamelContext; +import org.apache.camel.ProducerTemplate; +import org.apache.camel.builder.RouteBuilder; +import org.apache.camel.impl.DefaultCamelContext; +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; +import org.springframework.boot.SpringApplication; +import org.springframework.context.ConfigurableApplicationContext; + +class TwoServicesWithDirectClientCamelTest + extends AbstractHttpServerUsingTest { + @RegisterExtension + public static final InstrumentationExtension testing = + HttpServerInstrumentationExtension.forAgent(); + + private static CamelContext clientContext; + + private static Integer portOne; + + private static Integer portTwo; + + @Override + protected ConfigurableApplicationContext setupServer() { + portOne = port; + portTwo = PortUtils.findOpenPort(); + SpringApplication app = new SpringApplication(TwoServicesConfig.class); + app.setDefaultProperties( + ImmutableMap.of("service.one.port", portOne, "service.two.port", portTwo)); + return app.run(); + } + + @Override + protected void stopServer(ConfigurableApplicationContext ctx) { + ctx.close(); + } + + @Override + protected String getContextPath() { + return ""; + } + + @BeforeAll + protected void setUp() { + startServer(); + } + + @AfterAll + protected void cleanUp() { + cleanupServer(); + } + + void createAndStartClient() throws Exception { + clientContext = new DefaultCamelContext(); + clientContext.addRoutes( + new RouteBuilder() { + @Override + public void configure() { + from("direct:input") + .log("SENT Client request") + .to("http://localhost:" + portOne + "/serviceOne") + .log("RECEIVED Client response"); + } + }); + clientContext.start(); + } + + @Test + void twoCamelServiceSpans() throws Exception { + createAndStartClient(); + + ProducerTemplate template = clientContext.createProducerTemplate(); + + template.sendBody("direct:input", "Example request"); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("input") + .hasKind(SpanKind.INTERNAL) + .hasNoParent() + .hasAttributesSatisfyingExactly( + equalTo(stringKey("camel.uri"), "direct://input")), + span -> + span.hasName("POST") + .hasKind(SpanKind.CLIENT) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo(HttpAttributes.HTTP_REQUEST_METHOD, "POST"), + equalTo( + UrlAttributes.URL_FULL, + "http://localhost:" + portOne + "/serviceOne"), + equalTo(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, 200L), + equalTo( + stringKey("camel.uri"), + "http://localhost:" + portOne + "/serviceOne")), + span -> + span.hasName("POST /serviceOne") + .hasKind(SpanKind.SERVER) + .hasParent(trace.getSpan(1)) + .hasAttributesSatisfyingExactly( + equalTo(HttpAttributes.HTTP_REQUEST_METHOD, "POST"), + equalTo( + UrlAttributes.URL_FULL, + "http://localhost:" + portOne + "/serviceOne"), + equalTo(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, 200L), + equalTo( + stringKey("camel.uri"), + "http://0.0.0.0:" + portOne + "/serviceOne")), + span -> + span.hasName("POST") + .hasKind(SpanKind.CLIENT) + .hasParent(trace.getSpan(2)) + .hasAttributesSatisfyingExactly( + equalTo(HttpAttributes.HTTP_REQUEST_METHOD, "POST"), + equalTo( + UrlAttributes.URL_FULL, + "http://127.0.0.1:" + portTwo + "/serviceTwo"), + equalTo(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, 200L), + equalTo( + stringKey("camel.uri"), + "http://127.0.0.1:" + portTwo + "/serviceTwo")), + span -> + span.hasName("POST /serviceTwo") + .hasKind(SpanKind.SERVER) + .hasParent(trace.getSpan(3)) + .hasAttributesSatisfyingExactly( + equalTo(HttpAttributes.HTTP_REQUEST_METHOD, "POST"), + equalTo(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, 200L), + equalTo(UrlAttributes.URL_SCHEME, "http"), + equalTo(UrlAttributes.URL_PATH, "/serviceTwo"), + equalTo( + UserAgentAttributes.USER_AGENT_ORIGINAL, + "Jakarta Commons-HttpClient/3.1"), + equalTo(HttpAttributes.HTTP_ROUTE, "/serviceTwo"), + equalTo(NetworkAttributes.NETWORK_PROTOCOL_VERSION, "1.1"), + equalTo(ServerAttributes.SERVER_ADDRESS, "127.0.0.1"), + equalTo(ServerAttributes.SERVER_PORT, portTwo), + equalTo(ClientAttributes.CLIENT_ADDRESS, "127.0.0.1"), + equalTo(NetworkAttributes.NETWORK_PEER_ADDRESS, "127.0.0.1"), + satisfies( + NetworkAttributes.NETWORK_PEER_PORT, + val -> val.isInstanceOf(Long.class))), + span -> + span.hasName("POST /serviceTwo") + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(4)) + .hasAttributesSatisfyingExactly( + equalTo(HttpAttributes.HTTP_REQUEST_METHOD, "POST"), + equalTo( + UrlAttributes.URL_FULL, + "http://127.0.0.1:" + portTwo + "/serviceTwo"), + equalTo( + stringKey("camel.uri"), + "jetty:http://0.0.0.0:" + portTwo + "/serviceTwo?arg=value")))); + } +} diff --git a/instrumentation/camel-2.20/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apachecamel/aws/AwsConnector.java b/instrumentation/camel-2.20/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apachecamel/aws/AwsConnector.java new file mode 100644 index 000000000000..62808d398385 --- /dev/null +++ b/instrumentation/camel-2.20/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apachecamel/aws/AwsConnector.java @@ -0,0 +1,185 @@ +/* + * 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.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.sns.AmazonSNSAsyncClient; +import com.amazonaws.services.sns.model.CreateTopicResult; +import com.amazonaws.services.sns.model.PublishRequest; +import com.amazonaws.services.sqs.AmazonSQSAsyncClient; +import com.amazonaws.services.sqs.model.GetQueueAttributesRequest; +import com.amazonaws.services.sqs.model.Message; +import com.amazonaws.services.sqs.model.PurgeQueueRequest; +import com.amazonaws.services.sqs.model.ReceiveMessageRequest; +import com.amazonaws.services.sqs.model.ReceiveMessageResult; +import com.amazonaws.services.sqs.model.SendMessageRequest; +import io.opentelemetry.instrumentation.test.utils.PortUtils; +import java.util.Collections; +import java.util.EnumSet; +import org.elasticmq.rest.sqs.SQSRestServer; +import org.elasticmq.rest.sqs.SQSRestServerBuilder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +class AwsConnector { + private static final Logger logger = LoggerFactory.getLogger(AwsConnector.class); + private final AmazonSQSAsyncClient sqsClient; + private final AmazonS3Client s3Client; + private final AmazonSNSAsyncClient snsClient; + private final SQSRestServer sqsRestServer; + + AwsConnector( + AmazonSQSAsyncClient sqsClient, + AmazonS3Client s3Client, + AmazonSNSAsyncClient snsClient, + SQSRestServer sqsRestServer) { + this.sqsRestServer = sqsRestServer; + this.sqsClient = sqsClient; + this.s3Client = s3Client; + this.snsClient = snsClient; + } + + static AwsConnector elasticMq() { + int sqsPort = PortUtils.findOpenPort(); + SQSRestServer sqsRestServer = + SQSRestServerBuilder.withPort(sqsPort).withInterface("localhost").start(); + + AWSStaticCredentialsProvider credentials = + new AWSStaticCredentialsProvider(new BasicAWSCredentials("x", "x")); + AwsClientBuilder.EndpointConfiguration endpointConfiguration = + new AwsClientBuilder.EndpointConfiguration("http://localhost:" + sqsPort, "elasticmq"); + AmazonSQSAsyncClient sqsClient = + (AmazonSQSAsyncClient) + AmazonSQSAsyncClient.asyncBuilder() + .withCredentials(credentials) + .withEndpointConfiguration(endpointConfiguration) + .build(); + + return new AwsConnector(sqsClient, null, null, sqsRestServer); + } + + static AwsConnector liveAws() { + + AmazonSQSAsyncClient sqsClient = + (AmazonSQSAsyncClient) + AmazonSQSAsyncClient.asyncBuilder().withRegion(Regions.US_EAST_1).build(); + + AmazonS3Client s3Client = + (AmazonS3Client) AmazonS3Client.builder().withRegion(Regions.US_EAST_1).build(); + + AmazonSNSAsyncClient snsClient = + (AmazonSNSAsyncClient) + AmazonSNSAsyncClient.asyncBuilder().withRegion(Regions.US_EAST_1).build(); + + return new AwsConnector(sqsClient, s3Client, snsClient, null); + } + + void createBucket(String bucketName) { + logger.info("Create bucket {}", bucketName); + s3Client.createBucket(bucketName); + } + + void deleteBucket(String bucketName) { + logger.info("Delete bucket {}", bucketName); + ObjectListing objectListing = s3Client.listObjects(bucketName); + for (S3ObjectSummary s3ObjectSummary : objectListing.getObjectSummaries()) { + s3Client.deleteObject(bucketName, s3ObjectSummary.getKey()); + } + s3Client.deleteBucket(bucketName); + } + + void enableS3ToSqsNotifications(String bucketName, String sqsQueueArn) { + logger.info("Enable notification for bucket {} to queue {}", bucketName, sqsQueueArn); + BucketNotificationConfiguration notificationConfiguration = + new BucketNotificationConfiguration(); + notificationConfiguration.addConfiguration( + "sqsQueueConfig", + new QueueConfiguration(sqsQueueArn, EnumSet.of(S3Event.ObjectCreatedByPut))); + s3Client.setBucketNotificationConfiguration( + new SetBucketNotificationConfigurationRequest(bucketName, notificationConfiguration)); + } + + String getQueueArn(String queueUrl) { + logger.info("Get ARN for queue " + queueUrl); + return sqsClient + .getQueueAttributes(new GetQueueAttributesRequest(queueUrl).withAttributeNames("QueueArn")) + .getAttributes() + .get("QueueArn"); + } + + private static String getSqsPolicy(String resource) { + return String.format( + "{\"Statement\": [{\"Effect\": \"Allow\", \"Principal\": \"*\", \"Action\": \"sqs:SendMessage\", \"Resource\": \"%s\"}]}", + resource); + } + + void purgeQueue(String queueUrl) { + logger.info("Purge queue {}", queueUrl); + sqsClient.purgeQueue(new PurgeQueueRequest(queueUrl)); + } + + void setQueuePublishingPolicy(String queueUrl, String queueArn) { + logger.info("Set policy for queue {}", queueArn); + sqsClient.setQueueAttributes( + queueUrl, Collections.singletonMap("Policy", getSqsPolicy(queueArn))); + } + + String createQueue(String queueName) { + logger.info("Create queue {}", queueName); + return sqsClient.createQueue(queueName).getQueueUrl(); + } + + void sendSampleMessage(String queueUrl) { + SendMessageRequest send = new SendMessageRequest(queueUrl, "{\"type\": \"hello\"}"); + sqsClient.sendMessage(send); + } + + void receiveMessage(String queueUrl) { + logger.info("Receive message from queue {}", queueUrl); + ReceiveMessageResult receiveMessageResult = + sqsClient.receiveMessage(new ReceiveMessageRequest(queueUrl).withWaitTimeSeconds(20)); + for (Message ignored : receiveMessageResult.getMessages()) {} + } + + void disconnect() { + if (sqsRestServer != null) { + sqsRestServer.stopAndWait(); + } + } + + void publishSampleNotification(String topicArn) { + snsClient.publish(new PublishRequest().withMessage("Hello There").withTopicArn(topicArn)); + } + + String createTopicAndSubscribeQueue(String topicName, String queueArn) { + logger.info("Create topic {} and subscribe to queue {}", topicName, queueArn); + CreateTopicResult ctr = snsClient.createTopic(topicName); + snsClient.subscribe(ctr.getTopicArn(), "sqs", queueArn); + return ctr.getTopicArn(); + } + + AmazonSQSAsyncClient getSqsClient() { + return sqsClient; + } + + AmazonS3Client getS3Client() { + return s3Client; + } + + AmazonSNSAsyncClient getSnsClient() { + return snsClient; + } +} diff --git a/instrumentation/camel-2.20/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apachecamel/aws/AwsSpanAssertions.java b/instrumentation/camel-2.20/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apachecamel/aws/AwsSpanAssertions.java new file mode 100644 index 000000000000..b004b676d209 --- /dev/null +++ b/instrumentation/camel-2.20/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apachecamel/aws/AwsSpanAssertions.java @@ -0,0 +1,168 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.apachecamel.aws; + +import static io.opentelemetry.api.common.AttributeKey.stringKey; +import static io.opentelemetry.api.trace.SpanKind.CLIENT; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.satisfies; +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.sdk.testing.assertj.AttributeAssertion; +import io.opentelemetry.sdk.testing.assertj.SpanDataAssert; +import io.opentelemetry.semconv.HttpAttributes; +import io.opentelemetry.semconv.NetworkAttributes; +import io.opentelemetry.semconv.ServerAttributes; +import io.opentelemetry.semconv.UrlAttributes; +import io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes; +import io.opentelemetry.semconv.incubating.RpcIncubatingAttributes; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +class AwsSpanAssertions { + + private AwsSpanAssertions() {} + + static SpanDataAssert sqs(SpanDataAssert span, String spanName) { + return sqs(span, spanName, null, null, CLIENT); + } + + static SpanDataAssert sqs(SpanDataAssert span, String spanName, String queueUrl) { + return sqs(span, spanName, queueUrl, null, CLIENT); + } + + static SpanDataAssert sqs( + SpanDataAssert span, String spanName, String queueUrl, String queueName) { + return sqs(span, spanName, queueUrl, queueName, CLIENT); + } + + static SpanDataAssert sqs( + SpanDataAssert span, String spanName, String queueUrl, String queueName, SpanKind spanKind) { + + String rpcMethod; + if (spanName.startsWith("SQS.")) { + rpcMethod = spanName.substring(4); + } else if (spanName.endsWith("process")) { + rpcMethod = "ReceiveMessage"; + } else if (spanName.endsWith("publish")) { + rpcMethod = "SendMessage"; + } else { + throw new IllegalStateException("can't get rpc method from span name " + spanName); + } + + List attributeAssertions = new ArrayList<>(); + attributeAssertions.addAll( + Arrays.asList( + equalTo(stringKey("aws.agent"), "java-aws-sdk"), + satisfies(stringKey("aws.endpoint"), val -> val.isInstanceOf(String.class)), + satisfies( + stringKey("aws.queue.name"), + val -> + val.satisfiesAnyOf( + v -> assertThat(v).isEqualTo(queueName), v -> assertThat(v).isNull())), + satisfies( + stringKey("aws.queue.url"), + val -> + val.satisfiesAnyOf( + v -> assertThat(v).isEqualTo(queueUrl), v -> assertThat(v).isNull())), + equalTo(HttpAttributes.HTTP_REQUEST_METHOD, "POST"), + equalTo(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, 200), + satisfies(UrlAttributes.URL_FULL, val -> val.isInstanceOf(String.class)), + satisfies( + ServerAttributes.SERVER_ADDRESS, + stringAssert -> stringAssert.isInstanceOf(String.class)), + satisfies( + ServerAttributes.SERVER_PORT, + val -> + val.satisfiesAnyOf( + v -> assertThat(v).isNull(), + v -> assertThat(v).isInstanceOf(Number.class))), + equalTo(NetworkAttributes.NETWORK_PROTOCOL_VERSION, "1.1"), + equalTo(RpcIncubatingAttributes.RPC_SYSTEM, "aws-api"), + satisfies( + RpcIncubatingAttributes.RPC_METHOD, + stringAssert -> stringAssert.isEqualTo(rpcMethod)), + equalTo(RpcIncubatingAttributes.RPC_SERVICE, "AmazonSQS"))); + + if (spanName.endsWith("receive") + || spanName.endsWith("process") + || spanName.endsWith("publish")) { + attributeAssertions.addAll( + Arrays.asList( + equalTo(MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME, queueName), + equalTo( + MessagingIncubatingAttributes.MESSAGING_SYSTEM, + MessagingIncubatingAttributes.MessagingSystemValues.AWS_SQS))); + if (spanName.endsWith("receive")) { + attributeAssertions.add( + equalTo(MessagingIncubatingAttributes.MESSAGING_OPERATION, "receive")); + } else if (spanName.endsWith("process")) { + attributeAssertions.add( + equalTo(MessagingIncubatingAttributes.MESSAGING_OPERATION, "process")); + attributeAssertions.add( + satisfies( + MessagingIncubatingAttributes.MESSAGING_MESSAGE_ID, + val -> assertThat(val).isNotNull())); + } else if (spanName.endsWith("publish")) { + attributeAssertions.add( + equalTo(MessagingIncubatingAttributes.MESSAGING_OPERATION, "publish")); + attributeAssertions.add( + satisfies( + MessagingIncubatingAttributes.MESSAGING_MESSAGE_ID, + val -> assertThat(val).isNotNull())); + } + } + + return span.hasName(spanName) + .hasKind(spanKind) + .hasAttributesSatisfyingExactly(attributeAssertions); + } + + static SpanDataAssert s3(SpanDataAssert span, String spanName, String bucketName, String method) { + return span.hasName(spanName) + .hasAttributesSatisfyingExactly( + equalTo(stringKey("aws.agent"), "java-aws-sdk"), + satisfies(stringKey("aws.endpoint"), val -> val.isInstanceOf(String.class)), + equalTo(stringKey("aws.bucket.name"), bucketName), + equalTo(RpcIncubatingAttributes.RPC_SYSTEM, "aws-api"), + equalTo(RpcIncubatingAttributes.RPC_METHOD, spanName.substring(3)), + equalTo(RpcIncubatingAttributes.RPC_SERVICE, "Amazon S3"), + equalTo(HttpAttributes.HTTP_REQUEST_METHOD, method), + equalTo(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, 200), + satisfies(UrlAttributes.URL_FULL, val -> val.isInstanceOf(String.class)), + equalTo(NetworkAttributes.NETWORK_PROTOCOL_VERSION, "1.1"), + satisfies(ServerAttributes.SERVER_ADDRESS, val -> val.isInstanceOf(String.class)), + satisfies( + ServerAttributes.SERVER_PORT, + val -> + val.satisfiesAnyOf( + v -> val.isInstanceOf(Number.class), v -> assertThat(v).isNull()))); + } + + static SpanDataAssert sns(SpanDataAssert span, String spanName, String topicArn) { + return span.hasName(spanName) + .hasKind(CLIENT) + .hasAttributesSatisfyingExactly( + equalTo(stringKey("aws.agent"), "java-aws-sdk"), + satisfies(stringKey("aws.endpoint"), val -> val.isInstanceOf(String.class)), + equalTo(RpcIncubatingAttributes.RPC_SYSTEM, "aws-api"), + equalTo(RpcIncubatingAttributes.RPC_METHOD, spanName.substring(4)), + equalTo(RpcIncubatingAttributes.RPC_SERVICE, "AmazonSNS"), + equalTo(MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME, topicArn), + equalTo(HttpAttributes.HTTP_REQUEST_METHOD, "POST"), + equalTo(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, 200), + satisfies(UrlAttributes.URL_FULL, val -> val.isInstanceOf(String.class)), + equalTo(NetworkAttributes.NETWORK_PROTOCOL_VERSION, "1.1"), + satisfies(ServerAttributes.SERVER_ADDRESS, val -> val.isInstanceOf(String.class)), + satisfies( + ServerAttributes.SERVER_PORT, + val -> + val.satisfiesAnyOf( + v -> val.isInstanceOf(Number.class), v -> assertThat(v).isNull()))); + } +} diff --git a/instrumentation/camel-2.20/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apachecamel/aws/CamelSpanAssertions.java b/instrumentation/camel-2.20/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apachecamel/aws/CamelSpanAssertions.java new file mode 100644 index 000000000000..a90fe8fbaf92 --- /dev/null +++ b/instrumentation/camel-2.20/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apachecamel/aws/CamelSpanAssertions.java @@ -0,0 +1,70 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.apachecamel.aws; + +import static io.opentelemetry.api.common.AttributeKey.stringKey; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.satisfies; + +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.sdk.testing.assertj.SpanDataAssert; +import io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes; + +class CamelSpanAssertions { + + private CamelSpanAssertions() {} + + static void direct(SpanDataAssert span, String spanName) { + span.hasName(spanName) + .hasKind(SpanKind.INTERNAL) + .hasNoParent() + .hasAttribute(stringKey("camel.uri"), "direct://" + spanName); + } + + static SpanDataAssert sqsProduce(SpanDataAssert span, String queueName) { + return span.hasName(queueName) + .hasKind(SpanKind.INTERNAL) + .hasAttributesSatisfyingExactly( + equalTo( + stringKey("camel.uri"), + "aws-sqs://" + queueName + "?amazonSQSClient=%23sqsClient&delay=1000"), + equalTo(MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME, queueName)); + } + + static SpanDataAssert sqsConsume(SpanDataAssert span, String queueName) { + return sqsConsume(span, queueName, 1000); + } + + static SpanDataAssert sqsConsume(SpanDataAssert span, String queueName, int delay) { + return span.hasName(queueName) + .hasKind(SpanKind.INTERNAL) + .hasAttributesSatisfying( + equalTo( + stringKey("camel.uri"), + "aws-sqs://" + queueName + "?amazonSQSClient=%23sqsClient&delay=" + delay), + equalTo(MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME, queueName), + satisfies( + MessagingIncubatingAttributes.MESSAGING_MESSAGE_ID, + stringAssert -> stringAssert.isInstanceOf(String.class))); + } + + static SpanDataAssert snsPublish(SpanDataAssert span, String topicName) { + return span.hasName(topicName) + .hasKind(SpanKind.INTERNAL) + .hasAttributesSatisfying( + equalTo( + stringKey("camel.uri"), "aws-sns://" + topicName + "?amazonSNSClient=%23snsClient"), + equalTo(MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME, topicName)); + } + + static SpanDataAssert s3(SpanDataAssert span, String bucketName) { + return span.hasName("aws-s3") + .hasKind(SpanKind.INTERNAL) + .hasAttributesSatisfying( + equalTo( + stringKey("camel.uri"), "aws-s3://" + bucketName + "?amazonS3Client=%23s3Client")); + } +} diff --git a/instrumentation/camel-2.20/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apachecamel/aws/CamelSpringApplication.java b/instrumentation/camel-2.20/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apachecamel/aws/CamelSpringApplication.java new file mode 100644 index 000000000000..c9f7b7976678 --- /dev/null +++ b/instrumentation/camel-2.20/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apachecamel/aws/CamelSpringApplication.java @@ -0,0 +1,64 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.apachecamel.aws; + +import java.util.Map; +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 CamelSpringApplication { + + private final SpringApplication springApplication; + private ConfigurableApplicationContext context; + + CamelSpringApplication( + AwsConnector awsConnector, Class config, Map properties) { + springApplication = new SpringApplication(config); + springApplication.setDefaultProperties(properties); + injectClients(awsConnector); + } + + private void injectClients(AwsConnector awsConnector) { + springApplication.addInitializers( + (ApplicationContextInitializer) + applicationContext -> { + if (awsConnector.getSqsClient() != null) { + applicationContext + .getBeanFactory() + .registerSingleton("sqsClient", awsConnector.getSqsClient()); + } + if (awsConnector.getS3Client() != null) { + applicationContext + .getBeanFactory() + .registerSingleton("s3Client", awsConnector.getS3Client()); + } + if (awsConnector.getSnsClient() != null) { + applicationContext + .getBeanFactory() + .registerSingleton("snsClient", awsConnector.getSnsClient()); + } + }); + } + + void start() { + context = springApplication.run(); + } + + ProducerTemplate producerTemplate() { + CamelContext camelContext = context.getBean(CamelContext.class); + return camelContext.createProducerTemplate(); + } + + void stop() { + if (context != null) { + SpringApplication.exit(context); + } + } +} diff --git a/instrumentation/camel-2.20/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apachecamel/aws/S3CamelTest.java b/instrumentation/camel-2.20/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apachecamel/aws/S3CamelTest.java new file mode 100644 index 000000000000..e02b631b728c --- /dev/null +++ b/instrumentation/camel-2.20/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apachecamel/aws/S3CamelTest.java @@ -0,0 +1,140 @@ +/* + * 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 com.amazonaws.services.sqs.model.PurgeQueueInProgressException; +import com.google.common.collect.ImmutableMap; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@Disabled("Does not work with localstack - X-Ray features needed") +class S3CamelTest { + + @RegisterExtension + public static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + private static final Logger logger = LoggerFactory.getLogger(S3CamelTest.class); + + // To account for any delay interacting with real AWS environment + private static final int sqsDelay = 10000; + + private static AwsConnector awsConnector; + + @BeforeAll + protected static void setUp() { + awsConnector = AwsConnector.liveAws(); + } + + @Test + public void camelS3ProducerToCamelSqsConsumer() { + String queueName = "s3SqsCamelTest"; + String bucketName = "bucket-test-s3-sqs-camel"; + + CamelSpringApplication camelApp = + new CamelSpringApplication( + awsConnector, + S3Config.class, + ImmutableMap.of("bucketName", bucketName, "queueName", queueName)); + + String queueUrl = setupTestInfrastructure(queueName, bucketName); + waitAndClearSetupTraces(queueUrl, queueName, bucketName); + + camelApp.start(); + camelApp.producerTemplate().sendBody("direct:input", "{\"type\": \"hello\"}"); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> AwsSpanAssertions.sqs(span, "SQS.ListQueues").hasNoParent()), + trace -> + trace.hasSpansSatisfyingExactly( + span -> + AwsSpanAssertions.s3(span, "S3.ListObjects", bucketName, "GET").hasNoParent()), + trace -> + trace.hasSpansSatisfyingExactly( + span -> CamelSpanAssertions.direct(span, "input"), + span -> CamelSpanAssertions.s3(span, bucketName).hasParent(trace.getSpan(0)), + span -> + AwsSpanAssertions.s3(span, "S3.PutObject", bucketName, "PUT") + .hasParent(trace.getSpan(1)), + span -> + AwsSpanAssertions.sqs( + span, "s3SqsCamelTest process", queueUrl, queueName, CONSUMER) + .hasParent(trace.getSpan(2)), + span -> + CamelSpanAssertions.sqsConsume(span, queueName, sqsDelay) + .hasParent(trace.getSpan(2))), + // camel cleaning received msg + trace -> + trace.hasSpansSatisfyingExactly( + span -> AwsSpanAssertions.sqs(span, "SQS.DeleteMessage", queueUrl).hasNoParent())); + + camelApp.stop(); + awsConnector.deleteBucket(bucketName); + try { + awsConnector.purgeQueue(queueUrl); + } catch (PurgeQueueInProgressException e) { + logger.warn("Throttled by AWS trying to purge queue, try doing it manually."); + } + } + + String setupTestInfrastructure(String queueName, String 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; + } + + private static void waitAndClearSetupTraces( + String queueUrl, String queueName, String bucketName) { + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + AwsSpanAssertions.sqs(span, "SQS.CreateQueue", queueUrl, queueName) + .hasNoParent()), + trace -> + trace.hasSpansSatisfyingExactly( + span -> + AwsSpanAssertions.s3(span, "S3.CreateBucket", bucketName, "PUT").hasNoParent()), + trace -> + trace.hasSpansSatisfyingExactly( + span -> + AwsSpanAssertions.sqs(span, "SQS.GetQueueAttributes", queueUrl).hasNoParent()), + trace -> + trace.hasSpansSatisfyingExactly( + span -> + AwsSpanAssertions.sqs(span, "SQS.SetQueueAttributes", queueUrl).hasNoParent()), + trace -> + trace.hasSpansSatisfyingExactly( + span -> + AwsSpanAssertions.s3( + span, "S3.SetBucketNotificationConfiguration", bucketName, "PUT") + .hasNoParent()), + trace -> + trace.hasSpansSatisfyingExactly( + span -> + AwsSpanAssertions.sqs( + span, "s3SqsCamelTest process", queueUrl, queueName, CONSUMER) + .hasNoParent())); + testing.clearData(); + } +} diff --git a/instrumentation/camel-2.20/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apachecamel/aws/S3Config.java b/instrumentation/camel-2.20/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apachecamel/aws/S3Config.java new file mode 100644 index 000000000000..fb396ba0b301 --- /dev/null +++ b/instrumentation/camel-2.20/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apachecamel/aws/S3Config.java @@ -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 + public void configure() throws Exception { + from("aws-sqs://" + queueName + "?amazonSQSClient=#sqsClient&delay=10000") + .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 + public 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/instrumentation/camel-2.20/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apachecamel/aws/SnsCamelTest.java b/instrumentation/camel-2.20/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apachecamel/aws/SnsCamelTest.java new file mode 100644 index 000000000000..21bc6dee94e1 --- /dev/null +++ b/instrumentation/camel-2.20/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apachecamel/aws/SnsCamelTest.java @@ -0,0 +1,182 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.apachecamel.aws; + +import com.amazonaws.services.sqs.model.PurgeQueueInProgressException; +import com.google.common.collect.ImmutableMap; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@Disabled("Does not work with localstack - X-Ray features needed") +class SnsCamelTest { + + @RegisterExtension + public static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + private static final Logger logger = LoggerFactory.getLogger(SnsCamelTest.class); + + private static AwsConnector awsConnector; + + @BeforeAll + protected static void setUp() { + awsConnector = AwsConnector.liveAws(); + } + + @Test + void awsSdkSnsProducerToCamelSqsConsumer() { + String topicName = "snsCamelTest"; + String queueName = "snsCamelTest"; + + CamelSpringApplication camelApp = + new CamelSpringApplication( + awsConnector, + SnsConfig.class, + ImmutableMap.of("topicName", topicName, "queueName", queueName)); + + SnsMetadata metaData = setupTestInfrastructure(queueName, topicName); + waitAndClearSetupTraces(metaData.queueUrl, queueName, metaData.topicArn); + + camelApp.start(); + awsConnector.publishSampleNotification(metaData.topicArn); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> AwsSpanAssertions.sqs(span, "SQS.ListQueues").hasNoParent()), + trace -> + trace.hasSpansSatisfyingExactly( + span -> AwsSpanAssertions.sns(span, "SNS.ListTopics", null).hasNoParent()), + trace -> + trace.hasSpansSatisfyingExactly( + span -> AwsSpanAssertions.sns(span, "SNS.Publish", metaData.topicArn).hasNoParent(), + span -> + AwsSpanAssertions.sqs( + span, + "snsCamelTest process", + metaData.queueUrl, + queueName, + SpanKind.CONSUMER) + .hasParent(trace.getSpan(0)), + span -> + CamelSpanAssertions.sqsConsume(span, queueName).hasParent(trace.getSpan(0))), + trace -> + trace.hasSpansSatisfyingExactly( + span -> + AwsSpanAssertions.sqs(span, "SQS.DeleteMessage", metaData.queueUrl) + .hasNoParent())); + + try { + awsConnector.purgeQueue(metaData.queueUrl); + } catch (PurgeQueueInProgressException e) { + logger.warn("Throttled by AWS trying to purge queue, try doing it manually."); + } + camelApp.stop(); + } + + @Test + void camelSnsProducerToCamelSqsConsumer() { + String topicName = "snsCamelTest"; + String queueName = "snsCamelTest"; + + CamelSpringApplication camelApp = + new CamelSpringApplication( + awsConnector, + SnsConfig.class, + ImmutableMap.of("topicName", topicName, "queueName", queueName)); + + SnsMetadata metaData = setupTestInfrastructure(queueName, topicName); + String queueUrl = metaData.queueUrl; + waitAndClearSetupTraces(queueUrl, queueName, metaData.topicArn); + + camelApp.start(); + camelApp.producerTemplate().sendBody("direct:input", "{\"type\": \"hello\"}"); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> AwsSpanAssertions.sqs(span, "SQS.ListQueues").hasNoParent()), + trace -> + trace.hasSpansSatisfyingExactly( + span -> AwsSpanAssertions.sns(span, "SNS.ListTopics", null).hasNoParent()), + trace -> + trace.hasSpansSatisfyingExactly( + span -> CamelSpanAssertions.direct(span, "input"), + span -> CamelSpanAssertions.snsPublish(span, topicName).hasParent(trace.getSpan(0)), + span -> + AwsSpanAssertions.sns(span, "SNS.Publish", metaData.topicArn) + .hasParent(trace.getSpan(1)), + span -> + AwsSpanAssertions.sqs( + span, "snsCamelTest process", queueUrl, queueName, SpanKind.CONSUMER) + .hasParent(trace.getSpan(2)), + span -> + CamelSpanAssertions.sqsConsume(span, queueName).hasParent(trace.getSpan(2))), + trace -> + trace.hasSpansSatisfyingExactly( + span -> AwsSpanAssertions.sqs(span, "SQS.DeleteMessage", queueUrl).hasNoParent())); + + try { + awsConnector.purgeQueue(queueUrl); + } catch (PurgeQueueInProgressException e) { + logger.warn("Throttled by AWS trying to purge queue, try doing it manually."); + } + camelApp.stop(); + } + + private static void waitAndClearSetupTraces(String queueUrl, String queueName, String topicArn) { + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + AwsSpanAssertions.sqs(span, "SQS.CreateQueue", queueUrl, queueName) + .hasNoParent()), + trace -> + trace.hasSpansSatisfyingExactly( + span -> + AwsSpanAssertions.sqs(span, "SQS.GetQueueAttributes", queueUrl).hasNoParent()), + trace -> + trace.hasSpansSatisfyingExactly( + span -> + AwsSpanAssertions.sqs(span, "SQS.SetQueueAttributes", queueUrl).hasNoParent()), + trace -> + trace.hasSpansSatisfyingExactly( + span -> AwsSpanAssertions.sns(span, "SNS.CreateTopic", null).hasNoParent()), + trace -> + trace.hasSpansSatisfyingExactly( + span -> AwsSpanAssertions.sns(span, "SNS.Subscribe", topicArn).hasNoParent())); + testing.clearData(); + } + + SnsMetadata setupTestInfrastructure(String queueName, String topicName) { + // setup infra + String queueUrl = awsConnector.createQueue(queueName); + String queueArn = awsConnector.getQueueArn(queueUrl); + awsConnector.setQueuePublishingPolicy(queueUrl, queueArn); + String topicArn = awsConnector.createTopicAndSubscribeQueue(topicName, queueArn); + + // consume test message from AWS + awsConnector.receiveMessage(queueUrl); + + return new SnsMetadata(queueUrl, topicArn); + } + + private static final class SnsMetadata { + private final String queueUrl; + private final String topicArn; + + public SnsMetadata(String queueUrl, String topicArn) { + this.queueUrl = queueUrl; + this.topicArn = topicArn; + } + } +} diff --git a/instrumentation/camel-2.20/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apachecamel/aws/SnsConfig.java b/instrumentation/camel-2.20/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apachecamel/aws/SnsConfig.java new file mode 100644 index 000000000000..a1e0e7b46dbf --- /dev/null +++ b/instrumentation/camel-2.20/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apachecamel/aws/SnsConfig.java @@ -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 + public void configure() throws Exception { + from("aws-sqs://" + queueName + "?amazonSQSClient=#sqsClient&delay=1000") + .log(LoggingLevel.INFO, "test-sqs", "RECEIVER got body : ${body}") + .log(LoggingLevel.INFO, "test-sqs", "RECEIVER got headers : ${headers}"); + } + }; + } + + @Bean + RouteBuilder snsProducerRoute(@Value("${topicName}") String topicName) { + return new RouteBuilder() { + + @Override + public void configure() throws Exception { + from("direct:input") + .log(LoggingLevel.INFO, "test-sns", "SENDING body: ${body}") + .log(LoggingLevel.INFO, "test-sns", "SENDING headers: ${headers}") + .to("aws-sns://" + topicName + "?amazonSNSClient=#snsClient"); + } + }; + } +} diff --git a/instrumentation/camel-2.20/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apachecamel/aws/SqsCamelTest.java b/instrumentation/camel-2.20/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apachecamel/aws/SqsCamelTest.java new file mode 100644 index 000000000000..b47bcae7f807 --- /dev/null +++ b/instrumentation/camel-2.20/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apachecamel/aws/SqsCamelTest.java @@ -0,0 +1,147 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.apachecamel.aws; + +import com.google.common.collect.ImmutableMap; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +class SqsCamelTest { + + @RegisterExtension + public static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + private static final AwsConnector awsConnector = AwsConnector.elasticMq(); + + @AfterAll + static void cleanUp() { + awsConnector.disconnect(); + } + + private static void waitAndClearSetupTraces(String queueUrl, String queueName) { + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> AwsSpanAssertions.sqs(span, "SQS.CreateQueue", queueUrl, queueName))); + testing.clearData(); + } + + @Test + void camelSqsProducerToCamelSqsConsumer() { + String queueName = "sqsCamelTest"; + String queueUrl = awsConnector.createQueue(queueName); + waitAndClearSetupTraces(queueUrl, queueName); + + CamelSpringApplication camelApp = + new CamelSpringApplication( + awsConnector, SqsConfig.class, ImmutableMap.of("queueName", queueName)); + + camelApp.start(); + camelApp.producerTemplate().sendBody("direct:input", "{\"type\": \"hello\"}"); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> AwsSpanAssertions.sqs(span, "SQS.ListQueues").hasNoParent()), + trace -> + trace.hasSpansSatisfyingExactly( + span -> CamelSpanAssertions.direct(span, "input"), + span -> CamelSpanAssertions.sqsProduce(span, queueName).hasParent(trace.getSpan(0)), + span -> + AwsSpanAssertions.sqs( + span, "sqsCamelTest publish", queueUrl, queueName, SpanKind.PRODUCER) + .hasParent(trace.getSpan(1)), + span -> + AwsSpanAssertions.sqs( + span, "sqsCamelTest process", queueUrl, queueName, SpanKind.CONSUMER) + .hasParent(trace.getSpan(2)), + span -> + CamelSpanAssertions.sqsConsume(span, queueName).hasParent(trace.getSpan(2))), + trace -> + trace.hasSpansSatisfyingExactly( + span -> AwsSpanAssertions.sqs(span, "SQS.DeleteMessage", queueUrl).hasNoParent())); + camelApp.stop(); + } + + @Test + void awsSdkSqsProducerToCamelSqsConsumer() { + String queueName = "sqsCamelTest"; + String queueUrl = awsConnector.createQueue(queueName); + waitAndClearSetupTraces(queueUrl, queueName); + + CamelSpringApplication camelApp = + new CamelSpringApplication( + awsConnector, SqsConfig.class, ImmutableMap.of("queueName", queueName)); + + camelApp.start(); + awsConnector.sendSampleMessage(queueUrl); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> AwsSpanAssertions.sqs(span, "SQS.ListQueues").hasNoParent()), + trace -> + trace.hasSpansSatisfyingExactly( + span -> + AwsSpanAssertions.sqs( + span, "sqsCamelTest publish", queueUrl, queueName, SpanKind.PRODUCER) + .hasNoParent(), + span -> + AwsSpanAssertions.sqs( + span, "sqsCamelTest process", queueUrl, queueName, SpanKind.CONSUMER) + .hasParent(trace.getSpan(0)), + span -> + CamelSpanAssertions.sqsConsume(span, queueName).hasParent(trace.getSpan(0))), + trace -> + trace.hasSpansSatisfyingExactly( + span -> AwsSpanAssertions.sqs(span, "SQS.DeleteMessage", queueUrl).hasNoParent())); + camelApp.stop(); + } + + @Test + void camelSqsProducerToAwsSdkSqsConsumer() { + String queueName = "sqsCamelTestSdkConsumer"; + String queueUrl = awsConnector.createQueue(queueName); + waitAndClearSetupTraces(queueUrl, queueName); + + CamelSpringApplication camelApp = + new CamelSpringApplication( + awsConnector, SqsConfig.class, ImmutableMap.of("queueSdkConsumerName", queueName)); + + camelApp.start(); + camelApp.producerTemplate().sendBody("direct:inputSdkConsumer", "{\"type\": \"hello\"}"); + awsConnector.receiveMessage(queueUrl); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly(span -> AwsSpanAssertions.sqs(span, "SQS.ListQueues")), + trace -> + trace.hasSpansSatisfyingExactly( + span -> CamelSpanAssertions.direct(span, "inputSdkConsumer"), + span -> CamelSpanAssertions.sqsProduce(span, queueName).hasParent(trace.getSpan(0)), + span -> + AwsSpanAssertions.sqs( + span, + "sqsCamelTestSdkConsumer publish", + queueUrl, + queueName, + SpanKind.PRODUCER) + .hasParent(trace.getSpan(1)), + span -> + AwsSpanAssertions.sqs( + span, + "sqsCamelTestSdkConsumer process", + queueUrl, + queueName, + SpanKind.CONSUMER) + .hasParent(trace.getSpan(2)))); + camelApp.stop(); + } +} diff --git a/instrumentation/camel-2.20/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apachecamel/aws/SqsConfig.java b/instrumentation/camel-2.20/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apachecamel/aws/SqsConfig.java new file mode 100644 index 000000000000..2af2372d5c22 --- /dev/null +++ b/instrumentation/camel-2.20/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apachecamel/aws/SqsConfig.java @@ -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 + public void configure() throws Exception { + from("aws-sqs://" + queueName + "?amazonSQSClient=#sqsClient&delay=1000") + .log(LoggingLevel.INFO, "test-consumer", "RECEIVER got body : ${body}") + .log(LoggingLevel.INFO, "test-consumer", "RECEIVER got headers : ${headers}"); + } + }; + } + + @Bean + @ConditionalOnProperty("queueName") + RouteBuilder producerRoute(@Value("${queueName}") String queueName) { + return new RouteBuilder() { + + @Override + public void configure() throws Exception { + from("direct:input") + .log(LoggingLevel.INFO, "test-producer", "SENDING body: ${body}") + .log(LoggingLevel.INFO, "test-producer", "SENDING headers: ${headers}") + .to("aws-sqs://" + queueName + "?amazonSQSClient=#sqsClient&delay=1000"); + } + }; + } + + @Bean + @ConditionalOnProperty("queueSdkConsumerName") + RouteBuilder producerRouteForSdkConsumer( + @Value("${queueSdkConsumerName}") String queueSdkConsumerName) { + return new RouteBuilder() { + + @Override + public 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/instrumentation/camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/decorators/CassandraConfig.groovy b/instrumentation/camel-2.20/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apachecamel/decorators/CassandraConfig.java similarity index 51% rename from instrumentation/camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/decorators/CassandraConfig.groovy rename to instrumentation/camel-2.20/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apachecamel/decorators/CassandraConfig.java index c88e17a3478b..9b79a0bc7225 100644 --- a/instrumentation/camel-2.20/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/apachecamel/decorators/CassandraConfig.groovy +++ b/instrumentation/camel-2.20/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apachecamel/decorators/CassandraConfig.java @@ -3,12 +3,12 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.javaagent.instrumentation.apachecamel.decorators +package io.opentelemetry.javaagent.instrumentation.apachecamel.decorators; -import org.apache.camel.builder.RouteBuilder -import org.springframework.boot.SpringBootConfiguration -import org.springframework.boot.autoconfigure.EnableAutoConfiguration -import org.springframework.context.annotation.Bean +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 @@ -17,13 +17,13 @@ class CassandraConfig { @Bean RouteBuilder serviceRoute() { return new RouteBuilder() { - @Override - void configure() throws Exception { + public void configure() { from("direct:input") - .setHeader("CamelCqlQuery", simple("select * from test.users where id=1 ALLOW FILTERING")) - .toD("cql://{{cassandra.host}}:{{cassandra.port}}/test") + .setHeader( + "CamelCqlQuery", simple("select * from test.users where id=1 ALLOW FILTERING")) + .toD("cql://{{cassandra.host}}:{{cassandra.port}}/test"); } - } + }; } } diff --git a/instrumentation/camel-2.20/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apachecamel/decorators/CassandraTest.java b/instrumentation/camel-2.20/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apachecamel/decorators/CassandraTest.java new file mode 100644 index 000000000000..4e21f1bff32d --- /dev/null +++ b/instrumentation/camel-2.20/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apachecamel/decorators/CassandraTest.java @@ -0,0 +1,125 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.apachecamel.decorators; + +import static io.opentelemetry.api.common.AttributeKey.stringKey; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; + +import com.datastax.oss.driver.api.core.CqlSession; +import com.google.common.collect.ImmutableMap; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpServerUsingTest; +import io.opentelemetry.instrumentation.testing.junit.http.HttpServerInstrumentationExtension; +import io.opentelemetry.semconv.incubating.DbIncubatingAttributes; +import org.apache.camel.CamelContext; +import org.apache.camel.ProducerTemplate; +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; +import org.springframework.boot.SpringApplication; +import org.springframework.context.ConfigurableApplicationContext; +import org.testcontainers.containers.CassandraContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +@Testcontainers +class CassandraTest extends AbstractHttpServerUsingTest { + + @RegisterExtension + public static final InstrumentationExtension testing = + HttpServerInstrumentationExtension.forAgent(); + + private ConfigurableApplicationContext appContext; + + @Container + private static final CassandraContainer cassandra = + new CassandraContainer<>("cassandra:3.11.2").withExposedPorts(9042); + + private static String host; + + private static Integer cassandraPort; + + private static CqlSession cqlSession; + + @Override + protected ConfigurableApplicationContext setupServer() { + cassandra.start(); + cassandraSetup(); + + cassandraPort = cassandra.getFirstMappedPort(); + host = cassandra.getHost(); + + SpringApplication app = new SpringApplication(CassandraConfig.class); + app.setDefaultProperties( + ImmutableMap.of("cassandra.host", host, "cassandra.port", cassandraPort)); + appContext = app.run(); + return appContext; + } + + @Override + protected void stopServer(ConfigurableApplicationContext ctx) { + ctx.close(); + } + + @Override + protected String getContextPath() { + return ""; + } + + @BeforeAll + protected void setUp() { + startServer(); + } + + @AfterAll + protected void cleanUp() { + cleanupServer(); + cqlSession.close(); + cassandra.stop(); + } + + static void cassandraSetup() { + cqlSession = + CqlSession.builder() + .addContactPoint(cassandra.getContactPoint()) + .withLocalDatacenter(cassandra.getLocalDatacenter()) + .build(); + + cqlSession.execute( + "CREATE KEYSPACE IF NOT EXISTS test WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 1};"); + cqlSession.execute("CREATE TABLE IF NOT EXISTS test.users (id int PRIMARY KEY, name TEXT);"); + } + + @Test + void testCassandra() { + CamelContext camelContext = appContext.getBean(CamelContext.class); + ProducerTemplate template = camelContext.createProducerTemplate(); + + template.requestBody("direct:input", (Object) null); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasKind(SpanKind.INTERNAL) + .hasNoParent() + .hasAttributesSatisfyingExactly( + equalTo(stringKey("camel.uri"), "direct://input")), + span -> + span.hasKind(SpanKind.CLIENT) + .hasAttributesSatisfyingExactly( + equalTo( + stringKey("camel.uri"), + "cql://" + host + ":" + cassandraPort + "/test"), + equalTo(DbIncubatingAttributes.DB_NAME, "test"), + equalTo( + DbIncubatingAttributes.DB_STATEMENT, + "select * from test.users where id=? ALLOW FILTERING"), + equalTo(DbIncubatingAttributes.DB_SYSTEM, "cassandra")))); + } +} diff --git a/instrumentation/cassandra/cassandra-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cassandra/v3_0/CassandraAttributesExtractor.java b/instrumentation/cassandra/cassandra-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cassandra/v3_0/CassandraAttributesExtractor.java new file mode 100644 index 000000000000..6e56a5a58997 --- /dev/null +++ b/instrumentation/cassandra/cassandra-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cassandra/v3_0/CassandraAttributesExtractor.java @@ -0,0 +1,36 @@ +/* + * 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.context.Context; +import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; +import io.opentelemetry.semconv.ServerAttributes; +import javax.annotation.Nullable; + +public class CassandraAttributesExtractor + implements AttributesExtractor { + @Override + public void onStart(AttributesBuilder attributes, Context context, CassandraRequest request) {} + + @Override + public void onEnd( + AttributesBuilder attributes, + Context context, + CassandraRequest request, + @Nullable ExecutionInfo executionInfo, + @Nullable Throwable error) { + if (executionInfo == null) { + return; + } + attributes.put( + ServerAttributes.SERVER_ADDRESS, + executionInfo.getQueriedHost().getSocketAddress().getHostString()); + attributes.put( + ServerAttributes.SERVER_PORT, executionInfo.getQueriedHost().getSocketAddress().getPort()); + } +} diff --git a/instrumentation/cassandra/cassandra-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cassandra/v3_0/CassandraNetAttributesGetter.java b/instrumentation/cassandra/cassandra-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cassandra/v3_0/CassandraNetAttributesGetter.java deleted file mode 100644 index bc5ace7cb0a5..000000000000 --- a/instrumentation/cassandra/cassandra-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cassandra/v3_0/CassandraNetAttributesGetter.java +++ /dev/null @@ -1,34 +0,0 @@ -/* - * 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.NetClientAttributesGetter; -import java.net.InetSocketAddress; -import javax.annotation.Nullable; - -final class CassandraNetAttributesGetter - implements NetClientAttributesGetter { - - @Nullable - @Override - public String getServerAddress(CassandraRequest request) { - return null; - } - - @Nullable - @Override - public Integer getServerPort(CassandraRequest request) { - return null; - } - - @Override - @Nullable - public InetSocketAddress getServerInetSocketAddress( - CassandraRequest request, @Nullable ExecutionInfo executionInfo) { - return executionInfo == null ? null : executionInfo.getQueriedHost().getSocketAddress(); - } -} diff --git a/instrumentation/cassandra/cassandra-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cassandra/v3_0/CassandraNetworkAttributesGetter.java b/instrumentation/cassandra/cassandra-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cassandra/v3_0/CassandraNetworkAttributesGetter.java new file mode 100644 index 000000000000..5e6f41fecdee --- /dev/null +++ b/instrumentation/cassandra/cassandra-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cassandra/v3_0/CassandraNetworkAttributesGetter.java @@ -0,0 +1,22 @@ +/* + * 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.semconv.network.NetworkAttributesGetter; +import java.net.InetSocketAddress; +import javax.annotation.Nullable; + +final class CassandraNetworkAttributesGetter + implements NetworkAttributesGetter { + + @Override + @Nullable + public InetSocketAddress getNetworkPeerInetSocketAddress( + CassandraRequest request, @Nullable ExecutionInfo executionInfo) { + return executionInfo == null ? null : executionInfo.getQueriedHost().getSocketAddress(); + } +} diff --git a/instrumentation/cassandra/cassandra-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cassandra/v3_0/CassandraSingletons.java b/instrumentation/cassandra/cassandra-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cassandra/v3_0/CassandraSingletons.java index 9b61b45e186a..841f48fdbc6e 100644 --- a/instrumentation/cassandra/cassandra-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cassandra/v3_0/CassandraSingletons.java +++ b/instrumentation/cassandra/cassandra-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cassandra/v3_0/CassandraSingletons.java @@ -7,13 +7,13 @@ import com.datastax.driver.core.ExecutionInfo; import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.instrumentation.api.incubator.semconv.db.DbClientSpanNameExtractor; +import io.opentelemetry.instrumentation.api.incubator.semconv.db.SqlClientAttributesExtractor; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; import io.opentelemetry.instrumentation.api.instrumenter.SpanKindExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.db.DbClientSpanNameExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.db.SqlClientAttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.net.NetClientAttributesExtractor; -import io.opentelemetry.javaagent.bootstrap.internal.CommonConfig; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.opentelemetry.instrumentation.api.semconv.network.NetworkAttributesExtractor; +import io.opentelemetry.javaagent.bootstrap.internal.AgentCommonConfig; +import io.opentelemetry.semconv.incubating.DbIncubatingAttributes; public final class CassandraSingletons { private static final String INSTRUMENTATION_NAME = "io.opentelemetry.cassandra-3.0"; @@ -32,12 +32,13 @@ public final class CassandraSingletons { DbClientSpanNameExtractor.create(attributesGetter)) .addAttributesExtractor( SqlClientAttributesExtractor.builder(attributesGetter) - .setTableAttribute(SemanticAttributes.DB_CASSANDRA_TABLE) + .setTableAttribute(DbIncubatingAttributes.DB_CASSANDRA_TABLE) .setStatementSanitizationEnabled( - CommonConfig.get().isStatementSanitizationEnabled()) + AgentCommonConfig.get().isStatementSanitizationEnabled()) .build()) .addAttributesExtractor( - NetClientAttributesExtractor.create(new CassandraNetAttributesGetter())) + NetworkAttributesExtractor.create(new CassandraNetworkAttributesGetter())) + .addAttributesExtractor(new CassandraAttributesExtractor()) .buildInstrumenter(SpanKindExtractor.alwaysClient()); } diff --git a/instrumentation/cassandra/cassandra-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cassandra/v3_0/CassandraSqlAttributesGetter.java b/instrumentation/cassandra/cassandra-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cassandra/v3_0/CassandraSqlAttributesGetter.java index ab439fe00f52..5b973f4e9922 100644 --- a/instrumentation/cassandra/cassandra-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cassandra/v3_0/CassandraSqlAttributesGetter.java +++ b/instrumentation/cassandra/cassandra-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cassandra/v3_0/CassandraSqlAttributesGetter.java @@ -5,15 +5,15 @@ package io.opentelemetry.javaagent.instrumentation.cassandra.v3_0; -import io.opentelemetry.instrumentation.api.instrumenter.db.SqlClientAttributesGetter; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.opentelemetry.instrumentation.api.incubator.semconv.db.SqlClientAttributesGetter; +import io.opentelemetry.semconv.incubating.DbIncubatingAttributes; import javax.annotation.Nullable; final class CassandraSqlAttributesGetter implements SqlClientAttributesGetter { @Override public String getSystem(CassandraRequest request) { - return SemanticAttributes.DbSystemValues.CASSANDRA; + return DbIncubatingAttributes.DbSystemValues.CASSANDRA; } @Override diff --git a/instrumentation/cassandra/cassandra-3.0/javaagent/src/test/java/CassandraClientTest.java b/instrumentation/cassandra/cassandra-3.0/javaagent/src/test/java/CassandraClientTest.java index 0ffcb03d8a8b..7d581375d537 100644 --- a/instrumentation/cassandra/cassandra-3.0/javaagent/src/test/java/CassandraClientTest.java +++ b/instrumentation/cassandra/cassandra-3.0/javaagent/src/test/java/CassandraClientTest.java @@ -4,14 +4,6 @@ */ import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.DB_CASSANDRA_TABLE; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.DB_NAME; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.DB_OPERATION; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.DB_STATEMENT; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.DB_SYSTEM; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.NET_SOCK_PEER_ADDR; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.NET_SOCK_PEER_NAME; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.NET_SOCK_PEER_PORT; import static org.junit.jupiter.api.Named.named; import com.datastax.driver.core.Cluster; @@ -20,7 +12,12 @@ import io.opentelemetry.api.trace.SpanKind; import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.semconv.NetworkAttributes; +import io.opentelemetry.semconv.ServerAttributes; +import io.opentelemetry.semconv.incubating.DbIncubatingAttributes; +import java.net.InetAddress; import java.net.InetSocketAddress; +import java.net.UnknownHostException; import java.time.Duration; import java.util.concurrent.Executor; import java.util.concurrent.Executors; @@ -49,11 +46,14 @@ public class CassandraClientTest { @SuppressWarnings("rawtypes") private static GenericContainer cassandra; + protected static String cassandraHost; + + protected static String cassandraIp; private static int cassandraPort; private static Cluster cluster; @BeforeAll - static void beforeAll() { + static void beforeAll() throws UnknownHostException { cassandra = new GenericContainer<>("cassandra:3") .withEnv("JVM_OPTS", "-Xmx128m -Xms128m") @@ -62,10 +62,12 @@ static void beforeAll() { .withStartupTimeout(Duration.ofMinutes(2)); cassandra.start(); + cassandraHost = cassandra.getHost(); + cassandraIp = InetAddress.getByName(cassandra.getHost()).getHostAddress(); cassandraPort = cassandra.getMappedPort(9042); cluster = Cluster.builder() - .addContactPointsWithPorts(new InetSocketAddress("localhost", cassandraPort)) + .addContactPointsWithPorts(new InetSocketAddress(cassandra.getHost(), cassandraPort)) .build(); } @@ -91,11 +93,15 @@ void syncTest(Parameter parameter) { .hasKind(SpanKind.CLIENT) .hasNoParent() .hasAttributesSatisfyingExactly( - equalTo(NET_SOCK_PEER_ADDR, "127.0.0.1"), - equalTo(NET_SOCK_PEER_NAME, "localhost"), - equalTo(NET_SOCK_PEER_PORT, cassandraPort), - equalTo(DB_SYSTEM, "cassandra"), - equalTo(DB_STATEMENT, "USE " + parameter.keyspace))), + equalTo(NetworkAttributes.NETWORK_TYPE, "ipv4"), + equalTo(ServerAttributes.SERVER_ADDRESS, cassandraHost), + equalTo(ServerAttributes.SERVER_PORT, cassandraPort), + equalTo(NetworkAttributes.NETWORK_PEER_ADDRESS, cassandraIp), + equalTo(NetworkAttributes.NETWORK_PEER_PORT, cassandraPort), + equalTo(DbIncubatingAttributes.DB_SYSTEM, "cassandra"), + equalTo( + DbIncubatingAttributes.DB_STATEMENT, + "USE " + parameter.keyspace))), trace -> trace.hasSpansSatisfyingExactly( span -> @@ -103,14 +109,18 @@ void syncTest(Parameter parameter) { .hasKind(SpanKind.CLIENT) .hasNoParent() .hasAttributesSatisfyingExactly( - equalTo(NET_SOCK_PEER_ADDR, "127.0.0.1"), - equalTo(NET_SOCK_PEER_NAME, "localhost"), - equalTo(NET_SOCK_PEER_PORT, cassandraPort), - equalTo(DB_SYSTEM, "cassandra"), - equalTo(DB_NAME, parameter.keyspace), - equalTo(DB_STATEMENT, parameter.expectedStatement), - equalTo(DB_OPERATION, parameter.operation), - equalTo(DB_CASSANDRA_TABLE, parameter.table)))); + equalTo(NetworkAttributes.NETWORK_TYPE, "ipv4"), + equalTo(ServerAttributes.SERVER_ADDRESS, cassandraHost), + equalTo(ServerAttributes.SERVER_PORT, cassandraPort), + equalTo(NetworkAttributes.NETWORK_PEER_ADDRESS, cassandraIp), + equalTo(NetworkAttributes.NETWORK_PEER_PORT, cassandraPort), + equalTo(DbIncubatingAttributes.DB_SYSTEM, "cassandra"), + equalTo(DbIncubatingAttributes.DB_NAME, parameter.keyspace), + equalTo( + DbIncubatingAttributes.DB_STATEMENT, parameter.expectedStatement), + equalTo(DbIncubatingAttributes.DB_OPERATION, parameter.operation), + equalTo( + DbIncubatingAttributes.DB_CASSANDRA_TABLE, parameter.table)))); } else { testing.waitAndAssertTraces( trace -> @@ -120,13 +130,17 @@ void syncTest(Parameter parameter) { .hasKind(SpanKind.CLIENT) .hasNoParent() .hasAttributesSatisfyingExactly( - equalTo(NET_SOCK_PEER_ADDR, "127.0.0.1"), - equalTo(NET_SOCK_PEER_NAME, "localhost"), - equalTo(NET_SOCK_PEER_PORT, cassandraPort), - equalTo(DB_SYSTEM, "cassandra"), - equalTo(DB_STATEMENT, parameter.expectedStatement), - equalTo(DB_OPERATION, parameter.operation), - equalTo(DB_CASSANDRA_TABLE, parameter.table)))); + equalTo(NetworkAttributes.NETWORK_TYPE, "ipv4"), + equalTo(ServerAttributes.SERVER_ADDRESS, cassandraHost), + equalTo(ServerAttributes.SERVER_PORT, cassandraPort), + equalTo(NetworkAttributes.NETWORK_PEER_ADDRESS, cassandraIp), + equalTo(NetworkAttributes.NETWORK_PEER_PORT, cassandraPort), + equalTo(DbIncubatingAttributes.DB_SYSTEM, "cassandra"), + equalTo( + DbIncubatingAttributes.DB_STATEMENT, parameter.expectedStatement), + equalTo(DbIncubatingAttributes.DB_OPERATION, parameter.operation), + equalTo( + DbIncubatingAttributes.DB_CASSANDRA_TABLE, parameter.table)))); } session.close(); @@ -157,11 +171,15 @@ void asyncTest(Parameter parameter) { .hasKind(SpanKind.CLIENT) .hasNoParent() .hasAttributesSatisfyingExactly( - equalTo(NET_SOCK_PEER_ADDR, "127.0.0.1"), - equalTo(NET_SOCK_PEER_NAME, "localhost"), - equalTo(NET_SOCK_PEER_PORT, cassandraPort), - equalTo(DB_SYSTEM, "cassandra"), - equalTo(DB_STATEMENT, "USE " + parameter.keyspace))), + equalTo(NetworkAttributes.NETWORK_TYPE, "ipv4"), + equalTo(ServerAttributes.SERVER_ADDRESS, cassandraHost), + equalTo(ServerAttributes.SERVER_PORT, cassandraPort), + equalTo(NetworkAttributes.NETWORK_PEER_ADDRESS, cassandraIp), + equalTo(NetworkAttributes.NETWORK_PEER_PORT, cassandraPort), + equalTo(DbIncubatingAttributes.DB_SYSTEM, "cassandra"), + equalTo( + DbIncubatingAttributes.DB_STATEMENT, + "USE " + parameter.keyspace))), trace -> trace.hasSpansSatisfyingExactly( span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(), @@ -170,14 +188,17 @@ void asyncTest(Parameter parameter) { .hasKind(SpanKind.CLIENT) .hasParent(trace.getSpan(0)) .hasAttributesSatisfyingExactly( - equalTo(NET_SOCK_PEER_ADDR, "127.0.0.1"), - equalTo(NET_SOCK_PEER_NAME, "localhost"), - equalTo(NET_SOCK_PEER_PORT, cassandraPort), - equalTo(DB_SYSTEM, "cassandra"), - equalTo(DB_NAME, parameter.keyspace), - equalTo(DB_STATEMENT, parameter.expectedStatement), - equalTo(DB_OPERATION, parameter.operation), - equalTo(DB_CASSANDRA_TABLE, parameter.table)), + equalTo(NetworkAttributes.NETWORK_TYPE, "ipv4"), + equalTo(ServerAttributes.SERVER_ADDRESS, cassandraHost), + equalTo(ServerAttributes.SERVER_PORT, cassandraPort), + equalTo(NetworkAttributes.NETWORK_PEER_ADDRESS, cassandraIp), + equalTo(NetworkAttributes.NETWORK_PEER_PORT, cassandraPort), + equalTo(DbIncubatingAttributes.DB_SYSTEM, "cassandra"), + equalTo(DbIncubatingAttributes.DB_NAME, parameter.keyspace), + equalTo( + DbIncubatingAttributes.DB_STATEMENT, parameter.expectedStatement), + equalTo(DbIncubatingAttributes.DB_OPERATION, parameter.operation), + equalTo(DbIncubatingAttributes.DB_CASSANDRA_TABLE, parameter.table)), span -> span.hasName("callbackListener") .hasKind(SpanKind.INTERNAL) @@ -192,13 +213,16 @@ void asyncTest(Parameter parameter) { .hasKind(SpanKind.CLIENT) .hasParent(trace.getSpan(0)) .hasAttributesSatisfyingExactly( - equalTo(NET_SOCK_PEER_ADDR, "127.0.0.1"), - equalTo(NET_SOCK_PEER_NAME, "localhost"), - equalTo(NET_SOCK_PEER_PORT, cassandraPort), - equalTo(DB_SYSTEM, "cassandra"), - equalTo(DB_STATEMENT, parameter.expectedStatement), - equalTo(DB_OPERATION, parameter.operation), - equalTo(DB_CASSANDRA_TABLE, parameter.table)), + equalTo(NetworkAttributes.NETWORK_TYPE, "ipv4"), + equalTo(ServerAttributes.SERVER_ADDRESS, cassandraHost), + equalTo(ServerAttributes.SERVER_PORT, cassandraPort), + equalTo(NetworkAttributes.NETWORK_PEER_ADDRESS, cassandraIp), + equalTo(NetworkAttributes.NETWORK_PEER_PORT, cassandraPort), + equalTo(DbIncubatingAttributes.DB_SYSTEM, "cassandra"), + equalTo( + DbIncubatingAttributes.DB_STATEMENT, parameter.expectedStatement), + equalTo(DbIncubatingAttributes.DB_OPERATION, parameter.operation), + equalTo(DbIncubatingAttributes.DB_CASSANDRA_TABLE, parameter.table)), span -> span.hasName("callbackListener") .hasKind(SpanKind.INTERNAL) @@ -217,8 +241,8 @@ private static Stream provideSyncParameters() { null, "DROP KEYSPACE IF EXISTS sync_test", "DROP KEYSPACE IF EXISTS sync_test", - "DB Query", - null, + "DROP", + "DROP", null))), Arguments.of( named( @@ -227,8 +251,8 @@ private static Stream provideSyncParameters() { null, "CREATE KEYSPACE sync_test WITH REPLICATION = {'class':'SimpleStrategy', 'replication_factor':3}", "CREATE KEYSPACE sync_test WITH REPLICATION = {?:?, ?:?}", - "DB Query", - null, + "CREATE", + "CREATE", null))), Arguments.of( named( @@ -237,9 +261,9 @@ private static Stream provideSyncParameters() { "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))), + "CREATE TABLE sync_test.users", + "CREATE TABLE", + "sync_test.users"))), Arguments.of( named( "Insert data", @@ -271,8 +295,8 @@ private static Stream provideAsyncParameters() { null, "DROP KEYSPACE IF EXISTS async_test", "DROP KEYSPACE IF EXISTS async_test", - "DB Query", - null, + "DROP", + "DROP", null))), Arguments.of( named( @@ -281,8 +305,8 @@ private static Stream provideAsyncParameters() { null, "CREATE KEYSPACE async_test WITH REPLICATION = {'class':'SimpleStrategy', 'replication_factor':3}", "CREATE KEYSPACE async_test WITH REPLICATION = {?:?, ?:?}", - "DB Query", - null, + "CREATE", + "CREATE", null))), Arguments.of( named( @@ -291,9 +315,9 @@ private static Stream provideAsyncParameters() { "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))), + "CREATE TABLE async_test.users", + "CREATE TABLE", + "async_test.users"))), Arguments.of( named( "Insert data", diff --git a/instrumentation/cassandra/cassandra-4-common/testing/build.gradle.kts b/instrumentation/cassandra/cassandra-4-common/testing/build.gradle.kts index 3d12c28f94e9..8147a0167158 100644 --- a/instrumentation/cassandra/cassandra-4-common/testing/build.gradle.kts +++ b/instrumentation/cassandra/cassandra-4-common/testing/build.gradle.kts @@ -5,6 +5,6 @@ plugins { dependencies { api(project(":testing-common")) - implementation("org.testcontainers:testcontainers:1.17.5") + implementation("org.testcontainers:testcontainers") implementation("com.datastax.oss:java-driver-core:4.0.0") } diff --git a/instrumentation/cassandra/cassandra-4-common/testing/src/main/java/io/opentelemetry/cassandra/v4/common/AbstractCassandraTest.java b/instrumentation/cassandra/cassandra-4-common/testing/src/main/java/io/opentelemetry/cassandra/v4/common/AbstractCassandraTest.java index 9b3b06a5770f..93bdbda788e4 100644 --- a/instrumentation/cassandra/cassandra-4-common/testing/src/main/java/io/opentelemetry/cassandra/v4/common/AbstractCassandraTest.java +++ b/instrumentation/cassandra/cassandra-4-common/testing/src/main/java/io/opentelemetry/cassandra/v4/common/AbstractCassandraTest.java @@ -7,29 +7,34 @@ import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.satisfies; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.DB_CASSANDRA_CONSISTENCY_LEVEL; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.DB_CASSANDRA_COORDINATOR_DC; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.DB_CASSANDRA_COORDINATOR_ID; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.DB_CASSANDRA_IDEMPOTENCE; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.DB_CASSANDRA_PAGE_SIZE; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.DB_CASSANDRA_SPECULATIVE_EXECUTION_COUNT; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.DB_CASSANDRA_TABLE; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.DB_NAME; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.DB_OPERATION; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.DB_STATEMENT; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.DB_SYSTEM; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.NET_SOCK_PEER_ADDR; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.NET_SOCK_PEER_NAME; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.NET_SOCK_PEER_PORT; +import static io.opentelemetry.semconv.NetworkAttributes.NETWORK_TYPE; +import static io.opentelemetry.semconv.ServerAttributes.SERVER_ADDRESS; +import static io.opentelemetry.semconv.ServerAttributes.SERVER_PORT; +import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DB_CASSANDRA_CONSISTENCY_LEVEL; +import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DB_CASSANDRA_COORDINATOR_DC; +import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DB_CASSANDRA_COORDINATOR_ID; +import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DB_CASSANDRA_IDEMPOTENCE; +import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DB_CASSANDRA_PAGE_SIZE; +import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DB_CASSANDRA_SPECULATIVE_EXECUTION_COUNT; +import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DB_CASSANDRA_TABLE; +import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DB_NAME; +import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DB_OPERATION; +import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DB_STATEMENT; +import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DB_SYSTEM; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Named.named; import com.datastax.oss.driver.api.core.CqlSession; +import com.datastax.oss.driver.api.core.CqlSessionBuilder; 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.api.trace.SpanKind; import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.semconv.NetworkAttributes; +import java.net.InetAddress; import java.net.InetSocketAddress; +import java.net.UnknownHostException; import java.time.Duration; import java.util.stream.Stream; import org.junit.jupiter.api.AfterAll; @@ -49,6 +54,10 @@ public abstract class AbstractCassandraTest { @SuppressWarnings("rawtypes") private static GenericContainer cassandra; + protected static String cassandraHost; + + protected static String cassandraIp; + protected static int cassandraPort; protected abstract InstrumentationExtension testing(); @@ -58,7 +67,7 @@ protected CqlSession wrap(CqlSession session) { } @BeforeAll - static void beforeAll() { + static void beforeAll() throws UnknownHostException { cassandra = new GenericContainer<>("cassandra:4.0") .withEnv("JVM_OPTS", "-Xmx128m -Xms128m") @@ -67,6 +76,8 @@ static void beforeAll() { .withStartupTimeout(Duration.ofMinutes(2)); cassandra.start(); + cassandraHost = cassandra.getHost(); + cassandraIp = InetAddress.getByName(cassandra.getHost()).getHostAddress(); cassandraPort = cassandra.getMappedPort(9042); } @@ -91,9 +102,16 @@ void syncTest(Parameter parameter) { .hasKind(SpanKind.CLIENT) .hasNoParent() .hasAttributesSatisfyingExactly( - equalTo(NET_SOCK_PEER_ADDR, "127.0.0.1"), - equalTo(NET_SOCK_PEER_NAME, "localhost"), - equalTo(NET_SOCK_PEER_PORT, cassandraPort), + satisfies( + NETWORK_TYPE, + val -> + val.satisfiesAnyOf( + v -> assertThat(v).isEqualTo("ipv4"), + v -> assertThat(v).isEqualTo("ipv6"))), + equalTo(SERVER_ADDRESS, cassandraHost), + equalTo(SERVER_PORT, cassandraPort), + equalTo(NetworkAttributes.NETWORK_PEER_ADDRESS, cassandraIp), + equalTo(NetworkAttributes.NETWORK_PEER_PORT, cassandraPort), equalTo(DB_SYSTEM, "cassandra"), equalTo(DB_NAME, parameter.keyspace), equalTo(DB_STATEMENT, parameter.expectedStatement), @@ -138,9 +156,16 @@ void asyncTest(Parameter parameter) throws Exception { .hasKind(SpanKind.CLIENT) .hasParent(trace.getSpan(0)) .hasAttributesSatisfyingExactly( - equalTo(NET_SOCK_PEER_ADDR, "127.0.0.1"), - equalTo(NET_SOCK_PEER_NAME, "localhost"), - equalTo(NET_SOCK_PEER_PORT, cassandraPort), + satisfies( + NETWORK_TYPE, + val -> + val.satisfiesAnyOf( + v -> assertThat(v).isEqualTo("ipv4"), + v -> assertThat(v).isEqualTo("ipv6"))), + equalTo(SERVER_ADDRESS, cassandraHost), + equalTo(SERVER_PORT, cassandraPort), + equalTo(NetworkAttributes.NETWORK_PEER_ADDRESS, cassandraIp), + equalTo(NetworkAttributes.NETWORK_PEER_PORT, cassandraPort), equalTo(DB_SYSTEM, "cassandra"), equalTo(DB_NAME, parameter.keyspace), equalTo(DB_STATEMENT, parameter.expectedStatement), @@ -173,8 +198,8 @@ private static Stream provideSyncParameters() { null, "DROP KEYSPACE IF EXISTS sync_test", "DROP KEYSPACE IF EXISTS sync_test", - "DB Query", - null, + "DROP", + "DROP", null))), Arguments.of( named( @@ -183,8 +208,8 @@ private static Stream provideSyncParameters() { null, "CREATE KEYSPACE sync_test WITH REPLICATION = {'class':'SimpleStrategy', 'replication_factor':3}", "CREATE KEYSPACE sync_test WITH REPLICATION = {?:?, ?:?}", - "DB Query", - null, + "CREATE", + "CREATE", null))), Arguments.of( named( @@ -193,9 +218,9 @@ private static Stream provideSyncParameters() { "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))), + "CREATE TABLE sync_test.users", + "CREATE TABLE", + "sync_test.users"))), Arguments.of( named( "Insert data", @@ -227,8 +252,8 @@ private static Stream provideAsyncParameters() { null, "DROP KEYSPACE IF EXISTS async_test", "DROP KEYSPACE IF EXISTS async_test", - "DB Query", - null, + "DROP", + "DROP", null))), Arguments.of( named( @@ -237,8 +262,8 @@ private static Stream provideAsyncParameters() { null, "CREATE KEYSPACE async_test WITH REPLICATION = {'class':'SimpleStrategy', 'replication_factor':3}", "CREATE KEYSPACE async_test WITH REPLICATION = {?:?, ?:?}", - "DB Query", - null, + "CREATE", + "CREATE", null))), Arguments.of( named( @@ -247,9 +272,9 @@ private static Stream provideAsyncParameters() { "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))), + "CREATE TABLE async_test.users", + "CREATE TABLE", + "async_test.users"))), Arguments.of( named( "Insert data", @@ -300,13 +325,18 @@ protected CqlSession getSession(String keyspace) { DriverConfigLoader configLoader = DefaultDriverConfigLoader.builder() .withDuration(DefaultDriverOption.REQUEST_TIMEOUT, Duration.ofSeconds(0)) + .withDuration(DefaultDriverOption.CONNECTION_INIT_QUERY_TIMEOUT, Duration.ofSeconds(10)) .build(); return wrap( - CqlSession.builder() - .addContactPoint(new InetSocketAddress("localhost", cassandraPort)) + addContactPoint(CqlSession.builder()) .withConfigLoader(configLoader) .withLocalDatacenter("datacenter1") .withKeyspace(keyspace) .build()); } + + protected CqlSessionBuilder addContactPoint(CqlSessionBuilder sessionBuilder) { + sessionBuilder.addContactPoint(new InetSocketAddress(cassandra.getHost(), cassandraPort)); + return sessionBuilder; + } } diff --git a/instrumentation/cassandra/cassandra-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cassandra/v4_0/CassandraAttributesExtractor.java b/instrumentation/cassandra/cassandra-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cassandra/v4_0/CassandraAttributesExtractor.java index be41c016b491..3904af69ec95 100644 --- a/instrumentation/cassandra/cassandra-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cassandra/v4_0/CassandraAttributesExtractor.java +++ b/instrumentation/cassandra/cassandra-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cassandra/v4_0/CassandraAttributesExtractor.java @@ -13,7 +13,10 @@ import io.opentelemetry.api.common.AttributesBuilder; import io.opentelemetry.context.Context; import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.opentelemetry.semconv.ServerAttributes; +import io.opentelemetry.semconv.incubating.DbIncubatingAttributes; +import java.net.InetSocketAddress; +import java.net.SocketAddress; import javax.annotation.Nullable; final class CassandraAttributesExtractor @@ -36,16 +39,23 @@ public void onEnd( Node coordinator = executionInfo.getCoordinator(); if (coordinator != null) { + SocketAddress address = coordinator.getEndPoint().resolve(); + if (address instanceof InetSocketAddress) { + attributes.put( + ServerAttributes.SERVER_ADDRESS, ((InetSocketAddress) address).getHostString()); + attributes.put(ServerAttributes.SERVER_PORT, ((InetSocketAddress) address).getPort()); + } if (coordinator.getDatacenter() != null) { - attributes.put(SemanticAttributes.DB_CASSANDRA_COORDINATOR_DC, coordinator.getDatacenter()); + attributes.put( + DbIncubatingAttributes.DB_CASSANDRA_COORDINATOR_DC, coordinator.getDatacenter()); } if (coordinator.getHostId() != null) { attributes.put( - SemanticAttributes.DB_CASSANDRA_COORDINATOR_ID, coordinator.getHostId().toString()); + DbIncubatingAttributes.DB_CASSANDRA_COORDINATOR_ID, coordinator.getHostId().toString()); } } attributes.put( - SemanticAttributes.DB_CASSANDRA_SPECULATIVE_EXECUTION_COUNT, + DbIncubatingAttributes.DB_CASSANDRA_SPECULATIVE_EXECUTION_COUNT, executionInfo.getSpeculativeExecutionCount()); Statement statement = executionInfo.getStatement(); @@ -57,14 +67,14 @@ public void onEnd( } else { consistencyLevel = config.getString(DefaultDriverOption.REQUEST_CONSISTENCY); } - attributes.put(SemanticAttributes.DB_CASSANDRA_CONSISTENCY_LEVEL, consistencyLevel); + attributes.put(DbIncubatingAttributes.DB_CASSANDRA_CONSISTENCY_LEVEL, consistencyLevel); if (statement.getPageSize() > 0) { - attributes.put(SemanticAttributes.DB_CASSANDRA_PAGE_SIZE, statement.getPageSize()); + attributes.put(DbIncubatingAttributes.DB_CASSANDRA_PAGE_SIZE, statement.getPageSize()); } else { int pageSize = config.getInt(DefaultDriverOption.REQUEST_PAGE_SIZE); if (pageSize > 0) { - attributes.put(SemanticAttributes.DB_CASSANDRA_PAGE_SIZE, pageSize); + attributes.put(DbIncubatingAttributes.DB_CASSANDRA_PAGE_SIZE, pageSize); } } @@ -72,6 +82,6 @@ public void onEnd( if (idempotent == null) { idempotent = config.getBoolean(DefaultDriverOption.REQUEST_DEFAULT_IDEMPOTENCE); } - attributes.put(SemanticAttributes.DB_CASSANDRA_IDEMPOTENCE, idempotent); + attributes.put(DbIncubatingAttributes.DB_CASSANDRA_IDEMPOTENCE, idempotent); } } diff --git a/instrumentation/cassandra/cassandra-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cassandra/v4_0/CassandraNetAttributesGetter.java b/instrumentation/cassandra/cassandra-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cassandra/v4_0/CassandraNetworkAttributesGetter.java similarity index 66% rename from instrumentation/cassandra/cassandra-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cassandra/v4_0/CassandraNetAttributesGetter.java rename to instrumentation/cassandra/cassandra-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cassandra/v4_0/CassandraNetworkAttributesGetter.java index b68a52ea7a51..44ed4e1e851d 100644 --- a/instrumentation/cassandra/cassandra-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cassandra/v4_0/CassandraNetAttributesGetter.java +++ b/instrumentation/cassandra/cassandra-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cassandra/v4_0/CassandraNetworkAttributesGetter.java @@ -7,29 +7,17 @@ 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.NetClientAttributesGetter; +import io.opentelemetry.instrumentation.api.semconv.network.NetworkAttributesGetter; import java.net.InetSocketAddress; import java.net.SocketAddress; import javax.annotation.Nullable; -final class CassandraNetAttributesGetter - implements NetClientAttributesGetter { - - @Nullable - @Override - public String getServerAddress(CassandraRequest request) { - return null; - } - - @Nullable - @Override - public Integer getServerPort(CassandraRequest request) { - return null; - } +final class CassandraNetworkAttributesGetter + implements NetworkAttributesGetter { @Override @Nullable - public InetSocketAddress getServerInetSocketAddress( + public InetSocketAddress getNetworkPeerInetSocketAddress( CassandraRequest request, @Nullable ExecutionInfo executionInfo) { if (executionInfo == null) { return null; diff --git a/instrumentation/cassandra/cassandra-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cassandra/v4_0/CassandraSingletons.java b/instrumentation/cassandra/cassandra-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cassandra/v4_0/CassandraSingletons.java index b84df6935485..4da5b599d0a6 100644 --- a/instrumentation/cassandra/cassandra-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cassandra/v4_0/CassandraSingletons.java +++ b/instrumentation/cassandra/cassandra-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cassandra/v4_0/CassandraSingletons.java @@ -7,13 +7,13 @@ import com.datastax.oss.driver.api.core.cql.ExecutionInfo; import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.instrumentation.api.incubator.semconv.db.DbClientSpanNameExtractor; +import io.opentelemetry.instrumentation.api.incubator.semconv.db.SqlClientAttributesExtractor; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; import io.opentelemetry.instrumentation.api.instrumenter.SpanKindExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.db.DbClientSpanNameExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.db.SqlClientAttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.net.NetClientAttributesExtractor; -import io.opentelemetry.javaagent.bootstrap.internal.CommonConfig; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.opentelemetry.instrumentation.api.semconv.network.NetworkAttributesExtractor; +import io.opentelemetry.javaagent.bootstrap.internal.AgentCommonConfig; +import io.opentelemetry.semconv.incubating.DbIncubatingAttributes; public final class CassandraSingletons { private static final String INSTRUMENTATION_NAME = "io.opentelemetry.cassandra-4.0"; @@ -31,12 +31,12 @@ public final class CassandraSingletons { DbClientSpanNameExtractor.create(attributesGetter)) .addAttributesExtractor( SqlClientAttributesExtractor.builder(attributesGetter) - .setTableAttribute(SemanticAttributes.DB_CASSANDRA_TABLE) + .setTableAttribute(DbIncubatingAttributes.DB_CASSANDRA_TABLE) .setStatementSanitizationEnabled( - CommonConfig.get().isStatementSanitizationEnabled()) + AgentCommonConfig.get().isStatementSanitizationEnabled()) .build()) .addAttributesExtractor( - NetClientAttributesExtractor.create(new CassandraNetAttributesGetter())) + NetworkAttributesExtractor.create(new CassandraNetworkAttributesGetter())) .addAttributesExtractor(new CassandraAttributesExtractor()) .buildInstrumenter(SpanKindExtractor.alwaysClient()); } diff --git a/instrumentation/cassandra/cassandra-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cassandra/v4_0/CassandraSqlAttributesGetter.java b/instrumentation/cassandra/cassandra-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cassandra/v4_0/CassandraSqlAttributesGetter.java index b56af5fc716c..e08cf36a940f 100644 --- a/instrumentation/cassandra/cassandra-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cassandra/v4_0/CassandraSqlAttributesGetter.java +++ b/instrumentation/cassandra/cassandra-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cassandra/v4_0/CassandraSqlAttributesGetter.java @@ -6,15 +6,15 @@ package io.opentelemetry.javaagent.instrumentation.cassandra.v4_0; import com.datastax.oss.driver.api.core.CqlIdentifier; -import io.opentelemetry.instrumentation.api.instrumenter.db.SqlClientAttributesGetter; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.opentelemetry.instrumentation.api.incubator.semconv.db.SqlClientAttributesGetter; +import io.opentelemetry.semconv.incubating.DbIncubatingAttributes; import javax.annotation.Nullable; final class CassandraSqlAttributesGetter implements SqlClientAttributesGetter { @Override public String getSystem(CassandraRequest request) { - return SemanticAttributes.DbSystemValues.CASSANDRA; + return DbIncubatingAttributes.DbSystemValues.CASSANDRA; } @Override diff --git a/instrumentation/cassandra/cassandra-4.0/javaagent/src/test/java/CassandraTest.java b/instrumentation/cassandra/cassandra-4.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/cassandra/v4_0/CassandraTest.java similarity index 82% rename from instrumentation/cassandra/cassandra-4.0/javaagent/src/test/java/CassandraTest.java rename to instrumentation/cassandra/cassandra-4.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/cassandra/v4_0/CassandraTest.java index 4db6a86efbdb..1df7618a85c9 100644 --- a/instrumentation/cassandra/cassandra-4.0/javaagent/src/test/java/CassandraTest.java +++ b/instrumentation/cassandra/cassandra-4.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/cassandra/v4_0/CassandraTest.java @@ -3,12 +3,14 @@ * SPDX-License-Identifier: Apache-2.0 */ +package io.opentelemetry.javaagent.instrumentation.cassandra.v4_0; + import io.opentelemetry.cassandra.v4.common.AbstractCassandraTest; import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; import org.junit.jupiter.api.extension.RegisterExtension; -public class CassandraTest extends AbstractCassandraTest { +class CassandraTest extends AbstractCassandraTest { @RegisterExtension static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); diff --git a/instrumentation/cassandra/cassandra-4.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cassandra/v4_4/CassandraSingletons.java b/instrumentation/cassandra/cassandra-4.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cassandra/v4_4/CassandraSingletons.java index 7803fbd26174..d7151d1b5722 100644 --- a/instrumentation/cassandra/cassandra-4.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cassandra/v4_4/CassandraSingletons.java +++ b/instrumentation/cassandra/cassandra-4.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cassandra/v4_4/CassandraSingletons.java @@ -7,13 +7,13 @@ import io.opentelemetry.api.GlobalOpenTelemetry; import io.opentelemetry.instrumentation.cassandra.v4_4.CassandraTelemetry; -import io.opentelemetry.javaagent.bootstrap.internal.CommonConfig; +import io.opentelemetry.javaagent.bootstrap.internal.AgentCommonConfig; final class CassandraSingletons { static final CassandraTelemetry telemetry = CassandraTelemetry.builder(GlobalOpenTelemetry.get()) - .setStatementSanitizationEnabled(CommonConfig.get().isStatementSanitizationEnabled()) + .setStatementSanitizationEnabled(AgentCommonConfig.get().isStatementSanitizationEnabled()) .build(); private CassandraSingletons() {} diff --git a/instrumentation/cassandra/cassandra-4.4/library/src/main/java/io/opentelemetry/instrumentation/cassandra/v4_4/CassandraAttributesExtractor.java b/instrumentation/cassandra/cassandra-4.4/library/src/main/java/io/opentelemetry/instrumentation/cassandra/v4_4/CassandraAttributesExtractor.java index 08c93b9eb33f..a4219a59881a 100644 --- a/instrumentation/cassandra/cassandra-4.4/library/src/main/java/io/opentelemetry/instrumentation/cassandra/v4_4/CassandraAttributesExtractor.java +++ b/instrumentation/cassandra/cassandra-4.4/library/src/main/java/io/opentelemetry/instrumentation/cassandra/v4_4/CassandraAttributesExtractor.java @@ -9,16 +9,43 @@ 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.EndPoint; import com.datastax.oss.driver.api.core.metadata.Node; +import com.datastax.oss.driver.internal.core.metadata.DefaultEndPoint; +import com.datastax.oss.driver.internal.core.metadata.SniEndPoint; +import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.common.AttributesBuilder; import io.opentelemetry.context.Context; import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.opentelemetry.semconv.ServerAttributes; +import java.lang.reflect.Field; +import java.net.InetSocketAddress; +import java.util.logging.Level; +import java.util.logging.Logger; import javax.annotation.Nullable; final class CassandraAttributesExtractor implements AttributesExtractor { + private static final Logger logger = + Logger.getLogger(CassandraAttributesExtractor.class.getName()); + + // copied from DbIncubatingAttributes + private static final AttributeKey DB_CASSANDRA_CONSISTENCY_LEVEL = + AttributeKey.stringKey("db.cassandra.consistency_level"); + private static final AttributeKey DB_CASSANDRA_COORDINATOR_DC = + AttributeKey.stringKey("db.cassandra.coordinator.dc"); + private static final AttributeKey DB_CASSANDRA_COORDINATOR_ID = + AttributeKey.stringKey("db.cassandra.coordinator.id"); + private static final AttributeKey DB_CASSANDRA_IDEMPOTENCE = + AttributeKey.booleanKey("db.cassandra.idempotence"); + private static final AttributeKey DB_CASSANDRA_PAGE_SIZE = + AttributeKey.longKey("db.cassandra.page_size"); + private static final AttributeKey DB_CASSANDRA_SPECULATIVE_EXECUTION_COUNT = + AttributeKey.longKey("db.cassandra.speculative_execution_count"); + + private static final Field proxyAddressField = getProxyAddressField(); + @Override public void onStart( AttributesBuilder attributes, Context parentContext, CassandraRequest request) {} @@ -36,17 +63,17 @@ public void onEnd( Node coordinator = executionInfo.getCoordinator(); if (coordinator != null) { + updateServerAddressAndPort(attributes, coordinator); + if (coordinator.getDatacenter() != null) { - attributes.put(SemanticAttributes.DB_CASSANDRA_COORDINATOR_DC, coordinator.getDatacenter()); + attributes.put(DB_CASSANDRA_COORDINATOR_DC, coordinator.getDatacenter()); } if (coordinator.getHostId() != null) { - attributes.put( - SemanticAttributes.DB_CASSANDRA_COORDINATOR_ID, coordinator.getHostId().toString()); + attributes.put(DB_CASSANDRA_COORDINATOR_ID, coordinator.getHostId().toString()); } } attributes.put( - SemanticAttributes.DB_CASSANDRA_SPECULATIVE_EXECUTION_COUNT, - executionInfo.getSpeculativeExecutionCount()); + DB_CASSANDRA_SPECULATIVE_EXECUTION_COUNT, executionInfo.getSpeculativeExecutionCount()); Statement statement = (Statement) executionInfo.getRequest(); String consistencyLevel; @@ -57,14 +84,14 @@ public void onEnd( } else { consistencyLevel = config.getString(DefaultDriverOption.REQUEST_CONSISTENCY); } - attributes.put(SemanticAttributes.DB_CASSANDRA_CONSISTENCY_LEVEL, consistencyLevel); + attributes.put(DB_CASSANDRA_CONSISTENCY_LEVEL, consistencyLevel); if (statement.getPageSize() > 0) { - attributes.put(SemanticAttributes.DB_CASSANDRA_PAGE_SIZE, statement.getPageSize()); + attributes.put(DB_CASSANDRA_PAGE_SIZE, statement.getPageSize()); } else { int pageSize = config.getInt(DefaultDriverOption.REQUEST_PAGE_SIZE); if (pageSize > 0) { - attributes.put(SemanticAttributes.DB_CASSANDRA_PAGE_SIZE, pageSize); + attributes.put(DB_CASSANDRA_PAGE_SIZE, pageSize); } } @@ -72,6 +99,42 @@ public void onEnd( if (idempotent == null) { idempotent = config.getBoolean(DefaultDriverOption.REQUEST_DEFAULT_IDEMPOTENCE); } - attributes.put(SemanticAttributes.DB_CASSANDRA_IDEMPOTENCE, idempotent); + attributes.put(DB_CASSANDRA_IDEMPOTENCE, idempotent); + } + + private static void updateServerAddressAndPort(AttributesBuilder attributes, Node coordinator) { + EndPoint endPoint = coordinator.getEndPoint(); + if (endPoint instanceof DefaultEndPoint) { + InetSocketAddress address = ((DefaultEndPoint) endPoint).resolve(); + attributes.put(ServerAttributes.SERVER_ADDRESS, address.getHostString()); + attributes.put(ServerAttributes.SERVER_PORT, address.getPort()); + } else if (endPoint instanceof SniEndPoint && proxyAddressField != null) { + SniEndPoint sniEndPoint = (SniEndPoint) endPoint; + Object object = null; + try { + object = proxyAddressField.get(sniEndPoint); + } catch (Exception e) { + logger.log( + Level.FINE, + "Error when accessing the private field proxyAddress of SniEndPoint using reflection.", + e); + } + if (object instanceof InetSocketAddress) { + InetSocketAddress address = (InetSocketAddress) object; + attributes.put(ServerAttributes.SERVER_ADDRESS, address.getHostString()); + attributes.put(ServerAttributes.SERVER_PORT, address.getPort()); + } + } + } + + @Nullable + private static Field getProxyAddressField() { + try { + Field field = SniEndPoint.class.getDeclaredField("proxyAddress"); + field.setAccessible(true); + return field; + } catch (Exception e) { + return null; + } } } diff --git a/instrumentation/cassandra/cassandra-4.4/library/src/main/java/io/opentelemetry/instrumentation/cassandra/v4_4/CassandraNetAttributesGetter.java b/instrumentation/cassandra/cassandra-4.4/library/src/main/java/io/opentelemetry/instrumentation/cassandra/v4_4/CassandraNetAttributesGetter.java deleted file mode 100644 index d1e26aa71489..000000000000 --- a/instrumentation/cassandra/cassandra-4.4/library/src/main/java/io/opentelemetry/instrumentation/cassandra/v4_4/CassandraNetAttributesGetter.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.cassandra.v4_4; - -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.NetClientAttributesGetter; -import java.net.InetSocketAddress; -import java.net.SocketAddress; -import javax.annotation.Nullable; - -final class CassandraNetAttributesGetter - implements NetClientAttributesGetter { - - @Nullable - @Override - public String getServerAddress(CassandraRequest request) { - return null; - } - - @Nullable - @Override - public Integer getServerPort(CassandraRequest request) { - return null; - } - - @Override - @Nullable - public InetSocketAddress getServerInetSocketAddress( - 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/instrumentation/cassandra/cassandra-4.4/library/src/main/java/io/opentelemetry/instrumentation/cassandra/v4_4/CassandraNetworkAttributesGetter.java b/instrumentation/cassandra/cassandra-4.4/library/src/main/java/io/opentelemetry/instrumentation/cassandra/v4_4/CassandraNetworkAttributesGetter.java new file mode 100644 index 000000000000..532dcb0646f6 --- /dev/null +++ b/instrumentation/cassandra/cassandra-4.4/library/src/main/java/io/opentelemetry/instrumentation/cassandra/v4_4/CassandraNetworkAttributesGetter.java @@ -0,0 +1,37 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.cassandra.v4_4; + +import com.datastax.oss.driver.api.core.cql.ExecutionInfo; +import com.datastax.oss.driver.api.core.metadata.EndPoint; +import com.datastax.oss.driver.api.core.metadata.Node; +import com.datastax.oss.driver.internal.core.metadata.DefaultEndPoint; +import io.opentelemetry.instrumentation.api.semconv.network.NetworkAttributesGetter; +import java.net.InetSocketAddress; +import javax.annotation.Nullable; + +final class CassandraNetworkAttributesGetter + implements NetworkAttributesGetter { + + @Override + @Nullable + public InetSocketAddress getNetworkPeerInetSocketAddress( + CassandraRequest request, @Nullable ExecutionInfo executionInfo) { + if (executionInfo == null) { + return null; + } + Node coordinator = executionInfo.getCoordinator(); + if (coordinator == null) { + return null; + } + EndPoint endPoint = coordinator.getEndPoint(); + if (endPoint instanceof DefaultEndPoint) { + // resolve() returns an existing InetSocketAddress, it does not do a dns resolve, + return (InetSocketAddress) endPoint.resolve(); + } + return null; + } +} diff --git a/instrumentation/cassandra/cassandra-4.4/library/src/main/java/io/opentelemetry/instrumentation/cassandra/v4_4/CassandraSqlAttributesGetter.java b/instrumentation/cassandra/cassandra-4.4/library/src/main/java/io/opentelemetry/instrumentation/cassandra/v4_4/CassandraSqlAttributesGetter.java index 80ceadf8798d..f78a1fab53be 100644 --- a/instrumentation/cassandra/cassandra-4.4/library/src/main/java/io/opentelemetry/instrumentation/cassandra/v4_4/CassandraSqlAttributesGetter.java +++ b/instrumentation/cassandra/cassandra-4.4/library/src/main/java/io/opentelemetry/instrumentation/cassandra/v4_4/CassandraSqlAttributesGetter.java @@ -6,15 +6,16 @@ package io.opentelemetry.instrumentation.cassandra.v4_4; import com.datastax.oss.driver.api.core.CqlIdentifier; -import io.opentelemetry.instrumentation.api.instrumenter.db.SqlClientAttributesGetter; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.opentelemetry.instrumentation.api.incubator.semconv.db.SqlClientAttributesGetter; import javax.annotation.Nullable; final class CassandraSqlAttributesGetter implements SqlClientAttributesGetter { + // copied from DbIncubatingAttributes.DbSystemValues + private static final String CASSANDRA = "cassandra"; @Override public String getSystem(CassandraRequest request) { - return SemanticAttributes.DbSystemValues.CASSANDRA; + return CASSANDRA; } @Override diff --git a/instrumentation/cassandra/cassandra-4.4/library/src/main/java/io/opentelemetry/instrumentation/cassandra/v4_4/CassandraTelemetryBuilder.java b/instrumentation/cassandra/cassandra-4.4/library/src/main/java/io/opentelemetry/instrumentation/cassandra/v4_4/CassandraTelemetryBuilder.java index bf8657a09550..6d39ac2d6f0a 100644 --- a/instrumentation/cassandra/cassandra-4.4/library/src/main/java/io/opentelemetry/instrumentation/cassandra/v4_4/CassandraTelemetryBuilder.java +++ b/instrumentation/cassandra/cassandra-4.4/library/src/main/java/io/opentelemetry/instrumentation/cassandra/v4_4/CassandraTelemetryBuilder.java @@ -8,17 +8,20 @@ import com.datastax.oss.driver.api.core.cql.ExecutionInfo; import com.google.errorprone.annotations.CanIgnoreReturnValue; import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.instrumentation.api.incubator.semconv.db.DbClientSpanNameExtractor; +import io.opentelemetry.instrumentation.api.incubator.semconv.db.SqlClientAttributesExtractor; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; import io.opentelemetry.instrumentation.api.instrumenter.SpanKindExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.db.DbClientSpanNameExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.db.SqlClientAttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.net.NetClientAttributesExtractor; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.opentelemetry.instrumentation.api.semconv.network.NetworkAttributesExtractor; /** A builder of {@link CassandraTelemetry}. */ public class CassandraTelemetryBuilder { private static final String INSTRUMENTATION_NAME = "io.opentelemetry.cassandra-4.4"; + // copied from DbIncubatingAttributes + private static final AttributeKey DB_CASSANDRA_TABLE = + AttributeKey.stringKey("db.cassandra.table"); private final OpenTelemetry openTelemetry; @@ -55,11 +58,11 @@ protected Instrumenter createInstrumenter( openTelemetry, INSTRUMENTATION_NAME, DbClientSpanNameExtractor.create(attributesGetter)) .addAttributesExtractor( SqlClientAttributesExtractor.builder(attributesGetter) - .setTableAttribute(SemanticAttributes.DB_CASSANDRA_TABLE) + .setTableAttribute(DB_CASSANDRA_TABLE) .setStatementSanitizationEnabled(statementSanitizationEnabled) .build()) .addAttributesExtractor( - NetClientAttributesExtractor.create(new CassandraNetAttributesGetter())) + NetworkAttributesExtractor.create(new CassandraNetworkAttributesGetter())) .addAttributesExtractor(new CassandraAttributesExtractor()) .buildInstrumenter(SpanKindExtractor.alwaysClient()); } diff --git a/instrumentation/cassandra/cassandra-4.4/testing/src/main/java/io/opentelemetry/testing/cassandra/v4_4/AbstractCassandra44Test.java b/instrumentation/cassandra/cassandra-4.4/testing/src/main/java/io/opentelemetry/testing/cassandra/v4_4/AbstractCassandra44Test.java index 33f00b1790df..bd3357978d36 100644 --- a/instrumentation/cassandra/cassandra-4.4/testing/src/main/java/io/opentelemetry/testing/cassandra/v4_4/AbstractCassandra44Test.java +++ b/instrumentation/cassandra/cassandra-4.4/testing/src/main/java/io/opentelemetry/testing/cassandra/v4_4/AbstractCassandra44Test.java @@ -7,25 +7,27 @@ import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.satisfies; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.DB_CASSANDRA_CONSISTENCY_LEVEL; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.DB_CASSANDRA_COORDINATOR_DC; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.DB_CASSANDRA_COORDINATOR_ID; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.DB_CASSANDRA_IDEMPOTENCE; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.DB_CASSANDRA_PAGE_SIZE; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.DB_CASSANDRA_SPECULATIVE_EXECUTION_COUNT; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.DB_CASSANDRA_TABLE; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.DB_NAME; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.DB_OPERATION; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.DB_STATEMENT; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.DB_SYSTEM; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.NET_SOCK_PEER_ADDR; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.NET_SOCK_PEER_NAME; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.NET_SOCK_PEER_PORT; +import static io.opentelemetry.semconv.NetworkAttributes.NETWORK_TYPE; +import static io.opentelemetry.semconv.ServerAttributes.SERVER_ADDRESS; +import static io.opentelemetry.semconv.ServerAttributes.SERVER_PORT; +import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DB_CASSANDRA_CONSISTENCY_LEVEL; +import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DB_CASSANDRA_COORDINATOR_DC; +import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DB_CASSANDRA_COORDINATOR_ID; +import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DB_CASSANDRA_IDEMPOTENCE; +import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DB_CASSANDRA_PAGE_SIZE; +import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DB_CASSANDRA_SPECULATIVE_EXECUTION_COUNT; +import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DB_CASSANDRA_TABLE; +import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DB_NAME; +import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DB_OPERATION; +import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DB_STATEMENT; +import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DB_SYSTEM; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Named.named; import com.datastax.oss.driver.api.core.CqlSession; import io.opentelemetry.api.trace.SpanKind; import io.opentelemetry.cassandra.v4.common.AbstractCassandraTest; +import io.opentelemetry.semconv.NetworkAttributes; import java.util.stream.Stream; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; @@ -57,9 +59,16 @@ void reactiveTest(Parameter parameter) { .hasKind(SpanKind.CLIENT) .hasParent(trace.getSpan(0)) .hasAttributesSatisfyingExactly( - equalTo(NET_SOCK_PEER_ADDR, "127.0.0.1"), - equalTo(NET_SOCK_PEER_NAME, "localhost"), - equalTo(NET_SOCK_PEER_PORT, cassandraPort), + satisfies( + NETWORK_TYPE, + val -> + val.satisfiesAnyOf( + v -> assertThat(v).isEqualTo("ipv4"), + v -> assertThat(v).isEqualTo("ipv6"))), + equalTo(SERVER_ADDRESS, cassandraHost), + equalTo(SERVER_PORT, cassandraPort), + equalTo(NetworkAttributes.NETWORK_PEER_ADDRESS, cassandraIp), + equalTo(NetworkAttributes.NETWORK_PEER_PORT, cassandraPort), equalTo(DB_SYSTEM, "cassandra"), equalTo(DB_NAME, parameter.keyspace), equalTo(DB_STATEMENT, parameter.expectedStatement), @@ -92,8 +101,8 @@ private static Stream provideReactiveParameters() { null, "DROP KEYSPACE IF EXISTS reactive_test", "DROP KEYSPACE IF EXISTS reactive_test", - "DB Query", - null, + "DROP", + "DROP", null))), Arguments.of( named( @@ -102,8 +111,8 @@ private static Stream provideReactiveParameters() { null, "CREATE KEYSPACE reactive_test WITH REPLICATION = {'class':'SimpleStrategy', 'replication_factor':3}", "CREATE KEYSPACE reactive_test WITH REPLICATION = {?:?, ?:?}", - "DB Query", - null, + "CREATE", + "CREATE", null))), Arguments.of( named( @@ -112,9 +121,9 @@ private static Stream provideReactiveParameters() { "reactive_test", "CREATE TABLE reactive_test.users ( id UUID PRIMARY KEY, name text )", "CREATE TABLE reactive_test.users ( id UUID PRIMARY KEY, name text )", - "reactive_test", - null, - null))), + "CREATE TABLE reactive_test.users", + "CREATE TABLE", + "reactive_test.users"))), Arguments.of( named( "Insert data", @@ -136,4 +145,12 @@ private static Stream provideReactiveParameters() { "SELECT", "users")))); } + + // TODO (trask) this is causing sporadic test failures + // @Override + // protected CqlSessionBuilder addContactPoint(CqlSessionBuilder sessionBuilder) { + // InetSocketAddress address = new InetSocketAddress("localhost", cassandraPort); + // sessionBuilder.addContactEndPoint(new SniEndPoint(address, "localhost")); + // return sessionBuilder; + // } } diff --git a/instrumentation/cdi-testing/src/test/groovy/CDIContainerTest.groovy b/instrumentation/cdi-testing/src/test/groovy/CDIContainerTest.groovy deleted file mode 100644 index 3a34b98a9d26..000000000000 --- a/instrumentation/cdi-testing/src/test/groovy/CDIContainerTest.groovy +++ /dev/null @@ -1,29 +0,0 @@ -/* - * 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/instrumentation/cdi-testing/src/test/java/io/opentelemetry/test/cdi/CdiContainerTest.java b/instrumentation/cdi-testing/src/test/java/io/opentelemetry/test/cdi/CdiContainerTest.java new file mode 100644 index 000000000000..6d6596745021 --- /dev/null +++ b/instrumentation/cdi-testing/src/test/java/io/opentelemetry/test/cdi/CdiContainerTest.java @@ -0,0 +1,36 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.test.cdi; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import org.jboss.weld.environment.se.Weld; +import org.jboss.weld.environment.se.WeldContainer; +import org.jboss.weld.environment.se.threading.RunnableDecorator; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +class CdiContainerTest { + @RegisterExtension + static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + @Test + public void cdiContainerStartsWithAgent() { + Weld builder = + new Weld() + .disableDiscovery() + .addDecorator(RunnableDecorator.class) + .addBeanClass(TestBean.class); + WeldContainer container = builder.initialize(); + + assertThat(container.isRunning()).isTrue(); + if (container != null) { + container.shutdown(); + } + } +} diff --git a/instrumentation/cdi-testing/src/test/java/TestBean.java b/instrumentation/cdi-testing/src/test/java/io/opentelemetry/test/cdi/TestBean.java similarity index 88% rename from instrumentation/cdi-testing/src/test/java/TestBean.java rename to instrumentation/cdi-testing/src/test/java/io/opentelemetry/test/cdi/TestBean.java index d7fba0156045..31ef6736f14b 100644 --- a/instrumentation/cdi-testing/src/test/java/TestBean.java +++ b/instrumentation/cdi-testing/src/test/java/io/opentelemetry/test/cdi/TestBean.java @@ -3,6 +3,8 @@ * SPDX-License-Identifier: Apache-2.0 */ +package io.opentelemetry.test.cdi; + public class TestBean { private String someField; diff --git a/instrumentation/clickhouse-client-0.5/javaagent/build.gradle.kts b/instrumentation/clickhouse-client-0.5/javaagent/build.gradle.kts new file mode 100644 index 000000000000..2322b3b8b3a8 --- /dev/null +++ b/instrumentation/clickhouse-client-0.5/javaagent/build.gradle.kts @@ -0,0 +1,28 @@ +plugins { + id("otel.javaagent-instrumentation") +} + +muzzle { + pass { + group.set("com.clickhouse.client") + module.set("clickhouse-client") + versions.set("[0.5.0,)") + assertInverse.set(true) + } +} + +dependencies { + compileOnly("com.clickhouse:clickhouse-client:0.5.0") + compileOnly("com.google.auto.value:auto-value-annotations") + annotationProcessor("com.google.auto.value:auto-value") + + testLibrary("com.clickhouse:clickhouse-client:0.5.0") + testLibrary("com.clickhouse:clickhouse-http-client:0.5.0") + testLibrary("org.apache.httpcomponents.client5:httpclient5:5.2.3") +} + +tasks { + test { + usesService(gradle.sharedServices.registrations["testcontainersBuildService"].service) + } +} diff --git a/instrumentation/clickhouse-client-0.5/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/clickhouse/ClickHouseAttributesGetter.java b/instrumentation/clickhouse-client-0.5/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/clickhouse/ClickHouseAttributesGetter.java new file mode 100644 index 000000000000..2e729cdd2416 --- /dev/null +++ b/instrumentation/clickhouse-client-0.5/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/clickhouse/ClickHouseAttributesGetter.java @@ -0,0 +1,58 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.clickhouse; + +import io.opentelemetry.instrumentation.api.incubator.semconv.db.DbClientAttributesGetter; +import io.opentelemetry.semconv.incubating.DbIncubatingAttributes; +import javax.annotation.Nullable; + +final class ClickHouseAttributesGetter implements DbClientAttributesGetter { + @Nullable + @Override + public String getStatement(ClickHouseDbRequest request) { + if (request.getSqlStatementInfo() == null) { + return null; + } + return request.getSqlStatementInfo().getFullStatement(); + } + + @Nullable + @Override + public String getOperation(ClickHouseDbRequest request) { + if (request.getSqlStatementInfo() == null) { + return null; + } + return request.getSqlStatementInfo().getOperation(); + } + + @Nullable + @Override + public String getSystem(ClickHouseDbRequest request) { + return DbIncubatingAttributes.DbSystemValues.CLICKHOUSE; + } + + @Nullable + @Override + public String getUser(ClickHouseDbRequest request) { + return null; + } + + @Nullable + @Override + public String getName(ClickHouseDbRequest request) { + String dbName = request.getDbName(); + if (dbName == null || dbName.isEmpty()) { + return null; + } + return dbName; + } + + @Nullable + @Override + public String getConnectionString(ClickHouseDbRequest request) { + return null; + } +} diff --git a/instrumentation/clickhouse-client-0.5/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/clickhouse/ClickHouseClientInstrumentation.java b/instrumentation/clickhouse-client-0.5/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/clickhouse/ClickHouseClientInstrumentation.java new file mode 100644 index 000000000000..78055968a872 --- /dev/null +++ b/instrumentation/clickhouse-client-0.5/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/clickhouse/ClickHouseClientInstrumentation.java @@ -0,0 +1,101 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.clickhouse; + +import static io.opentelemetry.javaagent.bootstrap.Java8BytecodeBridge.currentContext; +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.implementsInterface; +import static io.opentelemetry.javaagent.instrumentation.clickhouse.ClickHouseSingletons.instrumenter; +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 com.clickhouse.client.ClickHouseClient; +import com.clickhouse.client.ClickHouseRequest; +import com.clickhouse.client.config.ClickHouseDefaults; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.bootstrap.CallDepth; +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 ClickHouseClientInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher typeMatcher() { + return implementsInterface(named("com.clickhouse.client.ClickHouseClient")); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod() + .and(namedOneOf("executeAndWait", "execute")) + .and(takesArgument(0, named("com.clickhouse.client.ClickHouseRequest"))), + this.getClass().getName() + "$ClickHouseExecuteAndWaitAdvice"); + } + + @SuppressWarnings("unused") + public static class ClickHouseExecuteAndWaitAdvice { + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter( + @Advice.Argument(0) ClickHouseRequest clickHouseRequest, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope, + @Advice.Local("otelCallDepth") CallDepth callDepth) { + + callDepth = CallDepth.forClass(ClickHouseClient.class); + if (callDepth.getAndIncrement() > 0) { + return; + } + + if (clickHouseRequest == null) { + return; + } + + Context parentContext = currentContext(); + + ClickHouseDbRequest request = + ClickHouseDbRequest.create( + clickHouseRequest.getServer().getHost(), + clickHouseRequest.getServer().getPort(), + clickHouseRequest + .getServer() + .getDatabase() + .orElse(ClickHouseDefaults.DATABASE.getDefaultValue().toString()), + clickHouseRequest.getPreparedQuery().getOriginalQuery()); + + 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 onExit( + @Advice.Thrown Throwable throwable, + @Advice.Local("otelRequest") ClickHouseDbRequest clickHouseRequest, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope, + @Advice.Local("otelCallDepth") CallDepth callDepth) { + + if (callDepth.decrementAndGet() > 0) { + return; + } + + if (scope == null) { + return; + } + + scope.close(); + instrumenter().end(context, clickHouseRequest, null, throwable); + } + } +} diff --git a/instrumentation/clickhouse-client-0.5/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/clickhouse/ClickHouseDbRequest.java b/instrumentation/clickhouse-client-0.5/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/clickhouse/ClickHouseDbRequest.java new file mode 100644 index 000000000000..1afe12768aa2 --- /dev/null +++ b/instrumentation/clickhouse-client-0.5/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/clickhouse/ClickHouseDbRequest.java @@ -0,0 +1,30 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.clickhouse; + +import com.google.auto.value.AutoValue; +import io.opentelemetry.instrumentation.api.incubator.semconv.db.SqlStatementInfo; +import io.opentelemetry.instrumentation.api.incubator.semconv.db.SqlStatementSanitizer; +import io.opentelemetry.javaagent.bootstrap.internal.AgentCommonConfig; + +@AutoValue +public abstract class ClickHouseDbRequest { + + private static final SqlStatementSanitizer sanitizer = + SqlStatementSanitizer.create(AgentCommonConfig.get().isStatementSanitizationEnabled()); + + public static ClickHouseDbRequest create(String host, int port, String dbName, String sql) { + return new AutoValue_ClickHouseDbRequest(host, port, dbName, sanitizer.sanitize(sql)); + } + + public abstract String getHost(); + + public abstract int getPort(); + + public abstract String getDbName(); + + public abstract SqlStatementInfo getSqlStatementInfo(); +} diff --git a/instrumentation/clickhouse-client-0.5/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/clickhouse/ClickHouseInstrumentationModule.java b/instrumentation/clickhouse-client-0.5/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/clickhouse/ClickHouseInstrumentationModule.java new file mode 100644 index 000000000000..4477710668e8 --- /dev/null +++ b/instrumentation/clickhouse-client-0.5/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/clickhouse/ClickHouseInstrumentationModule.java @@ -0,0 +1,26 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.clickhouse; + +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 ClickHouseInstrumentationModule extends InstrumentationModule { + + public ClickHouseInstrumentationModule() { + super("clickhouse", "clickhouse-client-0.5"); + } + + @Override + public List typeInstrumentations() { + return singletonList(new ClickHouseClientInstrumentation()); + } +} diff --git a/instrumentation/clickhouse-client-0.5/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/clickhouse/ClickHouseNetworkAttributesGetter.java b/instrumentation/clickhouse-client-0.5/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/clickhouse/ClickHouseNetworkAttributesGetter.java new file mode 100644 index 000000000000..21a57f7f9911 --- /dev/null +++ b/instrumentation/clickhouse-client-0.5/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/clickhouse/ClickHouseNetworkAttributesGetter.java @@ -0,0 +1,22 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.clickhouse; + +import io.opentelemetry.instrumentation.api.semconv.network.ServerAttributesGetter; + +final class ClickHouseNetworkAttributesGetter + implements ServerAttributesGetter { + + @Override + public String getServerAddress(ClickHouseDbRequest request) { + return request.getHost(); + } + + @Override + public Integer getServerPort(ClickHouseDbRequest request) { + return request.getPort(); + } +} diff --git a/instrumentation/clickhouse-client-0.5/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/clickhouse/ClickHouseSingletons.java b/instrumentation/clickhouse-client-0.5/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/clickhouse/ClickHouseSingletons.java new file mode 100644 index 000000000000..1710cd98a6e8 --- /dev/null +++ b/instrumentation/clickhouse-client-0.5/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/clickhouse/ClickHouseSingletons.java @@ -0,0 +1,38 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.clickhouse; + +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.instrumentation.api.incubator.semconv.db.DbClientAttributesExtractor; +import io.opentelemetry.instrumentation.api.incubator.semconv.db.DbClientSpanNameExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import io.opentelemetry.instrumentation.api.instrumenter.SpanKindExtractor; +import io.opentelemetry.instrumentation.api.semconv.network.ServerAttributesExtractor; + +public final class ClickHouseSingletons { + + private static final Instrumenter INSTRUMENTER; + + static { + ClickHouseAttributesGetter dbAttributesGetter = new ClickHouseAttributesGetter(); + + INSTRUMENTER = + Instrumenter.builder( + GlobalOpenTelemetry.get(), + "io.opentelemetry.clickhouse-client-0.5", + DbClientSpanNameExtractor.create(dbAttributesGetter)) + .addAttributesExtractor(DbClientAttributesExtractor.create(dbAttributesGetter)) + .addAttributesExtractor( + ServerAttributesExtractor.create(new ClickHouseNetworkAttributesGetter())) + .buildInstrumenter(SpanKindExtractor.alwaysClient()); + } + + public static Instrumenter instrumenter() { + return INSTRUMENTER; + } + + private ClickHouseSingletons() {} +} diff --git a/instrumentation/clickhouse-client-0.5/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/clickhouse/ClickHouseClientTest.java b/instrumentation/clickhouse-client-0.5/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/clickhouse/ClickHouseClientTest.java new file mode 100644 index 000000000000..745bed68392e --- /dev/null +++ b/instrumentation/clickhouse-client-0.5/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/clickhouse/ClickHouseClientTest.java @@ -0,0 +1,344 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.clickhouse; + +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; +import static java.util.Arrays.asList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.catchThrowable; + +import com.clickhouse.client.ClickHouseClient; +import com.clickhouse.client.ClickHouseException; +import com.clickhouse.client.ClickHouseNode; +import com.clickhouse.client.ClickHouseParameterizedQuery; +import com.clickhouse.client.ClickHouseRequest; +import com.clickhouse.client.ClickHouseResponse; +import com.clickhouse.client.ClickHouseResponseSummary; +import com.clickhouse.data.ClickHouseFormat; +import com.google.common.collect.ImmutableMap; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.sdk.testing.assertj.AttributeAssertion; +import io.opentelemetry.sdk.trace.data.StatusData; +import io.opentelemetry.semconv.ServerAttributes; +import io.opentelemetry.semconv.incubating.DbIncubatingAttributes; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +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.RegisterExtension; +import org.testcontainers.containers.GenericContainer; + +@TestInstance(Lifecycle.PER_CLASS) +class ClickHouseClientTest { + + @RegisterExtension + private static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + private static final GenericContainer clickhouseServer = + new GenericContainer<>("clickhouse/clickhouse-server:24.4.2").withExposedPorts(8123); + + private static final String dbName = "default"; + private static final String tableName = "test_table"; + private static int port; + private static String host; + private static ClickHouseNode server; + private static ClickHouseClient client; + + @BeforeAll + void setup() throws ClickHouseException { + clickhouseServer.start(); + port = clickhouseServer.getMappedPort(8123); + host = clickhouseServer.getHost(); + server = ClickHouseNode.of("http://" + host + ":" + port + "/" + dbName + "?compress=0"); + client = ClickHouseClient.builder().build(); + + ClickHouseResponse response = + client + .read(server) + .query("create table if not exists " + tableName + "(s String) engine=Memory") + .executeAndWait(); + response.close(); + + // wait for CREATE operation and clear + testing.waitForTraces(1); + testing.clearData(); + } + + @AfterAll + void cleanup() { + if (client != null) { + client.close(); + } + clickhouseServer.stop(); + } + + @Test + void testConnectionStringWithoutDatabaseSpecifiedStillGeneratesSpans() + throws ClickHouseException { + ClickHouseNode server = ClickHouseNode.of("http://" + host + ":" + port + "?compress=0"); + ClickHouseClient client = ClickHouseClient.builder().build(); + + ClickHouseResponse response = + client + .read(server) + .format(ClickHouseFormat.RowBinaryWithNamesAndTypes) + .query("select * from " + tableName) + .executeAndWait(); + response.close(); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("SELECT " + dbName) + .hasKind(SpanKind.CLIENT) + .hasNoParent() + .hasAttributesSatisfyingExactly( + attributeAssertions("select * from " + tableName, "SELECT")))); + } + + @Test + void testExecuteAndWaitWithStringQuery() throws ClickHouseException { + testing.runWithSpan( + "parent", + () -> { + ClickHouseResponse response; + response = + client + .write(server) + .query("insert into " + tableName + " values('1')('2')('3')") + .executeAndWait(); + response.close(); + + response = + client + .read(server) + .format(ClickHouseFormat.RowBinaryWithNamesAndTypes) + .query("select * from " + tableName) + .executeAndWait(); + response.close(); + }); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasNoParent().hasAttributes(Attributes.empty()), + span -> + span.hasName("INSERT " + dbName) + .hasKind(SpanKind.CLIENT) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + attributeAssertions( + "insert into " + tableName + " values(?)(?)(?)", "INSERT")), + span -> + span.hasName("SELECT " + dbName) + .hasKind(SpanKind.CLIENT) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + attributeAssertions("select * from " + tableName, "SELECT")))); + } + + @Test + void testExecuteAndWaitWithStringQueryAndId() throws ClickHouseException { + testing.runWithSpan( + "parent", + () -> { + ClickHouseResponse response = + client + .read(server) + .format(ClickHouseFormat.RowBinaryWithNamesAndTypes) + .query("select * from " + tableName, "test_query_id") + .executeAndWait(); + response.close(); + }); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasNoParent().hasAttributes(Attributes.empty()), + span -> + span.hasName("SELECT " + dbName) + .hasKind(SpanKind.CLIENT) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + attributeAssertions("select * from " + tableName, "SELECT")))); + } + + @Test + void testExecuteAndWaitThrowsException() { + Throwable thrown = + catchThrowable( + () -> { + ClickHouseResponse response = + client + .read(server) + .format(ClickHouseFormat.RowBinaryWithNamesAndTypes) + .query("select * from non_existent_table") + .executeAndWait(); + response.close(); + }); + + assertThat(thrown).isInstanceOf(ClickHouseException.class); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("SELECT " + dbName) + .hasKind(SpanKind.CLIENT) + .hasStatus(StatusData.error()) + .hasException(thrown) + .hasAttributesSatisfyingExactly( + attributeAssertions("select * from non_existent_table", "SELECT")))); + } + + @Test + void testAsyncExecuteQuery() throws Exception { + CompletableFuture response = + client + .read(server) + .format(ClickHouseFormat.RowBinaryWithNamesAndTypes) + .query("select * from " + tableName) + .execute(); + + ClickHouseResponse result = response.get(); + assertThat(result).isNotNull(); + result.close(); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("SELECT " + dbName) + .hasKind(SpanKind.CLIENT) + .hasAttributesSatisfyingExactly( + attributeAssertions("select * from " + tableName, "SELECT")))); + } + + @Test + void testSendQuery() throws Exception { + testing.runWithSpan( + "parent", + () -> { + CompletableFuture> future = + ClickHouseClient.send(server, "select * from " + tableName + " limit 1"); + List results = future.get(); + assertThat(results).hasSize(1); + }); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasNoParent().hasAttributes(Attributes.empty()), + span -> + span.hasName("SELECT " + dbName) + .hasKind(SpanKind.CLIENT) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + attributeAssertions( + "select * from " + tableName + " limit ?", "SELECT")))); + } + + @Test + void testSendMultipleQueries() throws Exception { + testing.runWithSpan( + "parent", + () -> { + CompletableFuture> future = + ClickHouseClient.send( + server, + "insert into " + tableName + " values('1')('2')('3')", + "select * from " + tableName + " limit 1"); + List results = future.get(); + assertThat(results).hasSize(2); + }); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasNoParent().hasAttributes(Attributes.empty()), + span -> + span.hasName("INSERT " + dbName) + .hasKind(SpanKind.CLIENT) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + attributeAssertions( + "insert into " + tableName + " values(?)(?)(?)", "INSERT")), + span -> + span.hasName("SELECT " + dbName) + .hasKind(SpanKind.CLIENT) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + attributeAssertions( + "select * from " + tableName + " limit ?", "SELECT")))); + } + + @Test + void testParameterizedQueryInput() throws ClickHouseException { + ClickHouseRequest request = + client.read(server).format(ClickHouseFormat.RowBinaryWithNamesAndTypes); + + testing.runWithSpan( + "parent", + () -> { + ClickHouseResponse response = + client + .write(server) + .query( + ClickHouseParameterizedQuery.of( + request.getConfig(), + "insert into " + tableName + " values(:val1)(:val2)(:val3)")) + .params(ImmutableMap.of("val1", "1", "val2", "2", "val3", "3")) + .executeAndWait(); + response.close(); + + response = + request + .query( + ClickHouseParameterizedQuery.of( + request.getConfig(), "select * from " + tableName + " where s=:val")) + .params(ImmutableMap.of("val", "'2'")) + .executeAndWait(); + response.close(); + }); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasNoParent().hasAttributes(Attributes.empty()), + span -> + span.hasName("INSERT " + dbName) + .hasKind(SpanKind.CLIENT) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + attributeAssertions( + "insert into " + tableName + " values(:val1)(:val2)(:val3)", + "INSERT")), + span -> + span.hasName("SELECT " + dbName) + .hasKind(SpanKind.CLIENT) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + attributeAssertions( + "select * from " + tableName + " where s=:val", "SELECT")))); + } + + private static List attributeAssertions(String statement, String operation) { + return asList( + equalTo(DbIncubatingAttributes.DB_SYSTEM, DbIncubatingAttributes.DbSystemValues.CLICKHOUSE), + equalTo(DbIncubatingAttributes.DB_NAME, dbName), + equalTo(ServerAttributes.SERVER_ADDRESS, host), + equalTo(ServerAttributes.SERVER_PORT, port), + equalTo(DbIncubatingAttributes.DB_STATEMENT, statement), + equalTo(DbIncubatingAttributes.DB_OPERATION, operation)); + } +} diff --git a/instrumentation/couchbase/README.md b/instrumentation/couchbase/README.md index c27d0b28611e..2258abb0a49f 100644 --- a/instrumentation/couchbase/README.md +++ b/instrumentation/couchbase/README.md @@ -1,5 +1,5 @@ # Settings for the Couchbase instrumentation -| System property | Type | Default | Description | -|---|---|---|---| +| System property | Type | Default | Description | +| ------------------------------------------------------------- | ------- | ------- | --------------------------------------------------------------------------------------------------------- | | `otel.instrumentation.couchbase.experimental-span-attributes` | Boolean | `false` | Enables the capture of experimental span attributes (for version 2.6 and higher of this instrumentation). | diff --git a/instrumentation/couchbase/couchbase-2-common/javaagent-unit-tests/build.gradle.kts b/instrumentation/couchbase/couchbase-2-common/javaagent-unit-tests/build.gradle.kts index 08e327b8b0b6..69287a4bb7f6 100644 --- a/instrumentation/couchbase/couchbase-2-common/javaagent-unit-tests/build.gradle.kts +++ b/instrumentation/couchbase/couchbase-2-common/javaagent-unit-tests/build.gradle.kts @@ -3,10 +3,7 @@ plugins { } dependencies { - testImplementation("org.apache.groovy:groovy") - testImplementation("org.spockframework:spock-core") - - testImplementation(project(":instrumentation-api-semconv")) + testImplementation(project(":instrumentation-api-incubator")) testImplementation(project(":javaagent-extension-api")) testImplementation(project(":instrumentation:couchbase:couchbase-2-common:javaagent")) testImplementation("com.couchbase.client:java-client:2.5.0") diff --git a/instrumentation/couchbase/couchbase-2-common/javaagent-unit-tests/src/test/groovy/io/opentelemetry/javaagent/instrumentation/couchbase/v2_0/CouchbaseQuerySanitizerTest.groovy b/instrumentation/couchbase/couchbase-2-common/javaagent-unit-tests/src/test/groovy/io/opentelemetry/javaagent/instrumentation/couchbase/v2_0/CouchbaseQuerySanitizerTest.groovy deleted file mode 100644 index ec3bcd9ab0b8..000000000000 --- a/instrumentation/couchbase/couchbase-2-common/javaagent-unit-tests/src/test/groovy/io/opentelemetry/javaagent/instrumentation/couchbase/v2_0/CouchbaseQuerySanitizerTest.groovy +++ /dev/null @@ -1,36 +0,0 @@ -/* - * 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/instrumentation/couchbase/couchbase-2-common/javaagent-unit-tests/src/test/java/io/opentelemetry/javaagent/instrumentation/couchbase/v2_0/CouchbaseQuerySanitizerTest.java b/instrumentation/couchbase/couchbase-2-common/javaagent-unit-tests/src/test/java/io/opentelemetry/javaagent/instrumentation/couchbase/v2_0/CouchbaseQuerySanitizerTest.java new file mode 100644 index 000000000000..b67f857a2e17 --- /dev/null +++ b/instrumentation/couchbase/couchbase-2-common/javaagent-unit-tests/src/test/java/io/opentelemetry/javaagent/instrumentation/couchbase/v2_0/CouchbaseQuerySanitizerTest.java @@ -0,0 +1,83 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.couchbase.v2_0; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Named.named; + +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 java.util.stream.Stream; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +class CouchbaseQuerySanitizerTest { + + @ParameterizedTest + @MethodSource("providesArguments") + void testShouldNormalizeStringQuery(Parameter parameter) { + String normalized = CouchbaseQuerySanitizer.sanitize(parameter.query).getFullStatement(); + assertThat(normalized).isNotNull(); + // the analytics query ends up with trailing ';' in earlier couchbase version, but no trailing + // ';' in later couchbase version + assertThat(normalized.replaceFirst(";$", "")).isEqualTo(parameter.expected); + } + + private static Stream providesArguments() { + return Stream.of( + Arguments.of( + named( + "plain string", + new Parameter( + "SELECT field1 FROM `test` WHERE field2 = 'asdf'", + "SELECT field1 FROM `test` WHERE field2 = ?"))), + Arguments.of( + named( + "Statement", + new Parameter( + Select.select("field1") + .from("test") + .where(Expression.path("field2").eq(Expression.s("asdf"))), + "SELECT field1 FROM test WHERE field2 = ?"))), + Arguments.of( + named( + "N1QL", + new Parameter( + N1qlQuery.simple("SELECT field1 FROM `test` WHERE field2 = 'asdf'"), + "SELECT field1 FROM `test` WHERE field2 = ?"))), + Arguments.of( + named( + "Analytics", + new Parameter( + AnalyticsQuery.simple("SELECT field1 FROM `test` WHERE field2 = 'asdf'"), + "SELECT field1 FROM `test` WHERE field2 = ?"))), + Arguments.of( + named( + "View", + new Parameter( + ViewQuery.from("design", "view").skip(10), + "ViewQuery(design/view){params=\"skip=10\"}"))), + Arguments.of( + named( + "SpatialView", + new Parameter(SpatialViewQuery.from("design", "view").skip(10), "skip=10")))); + } + + private static class Parameter { + public final Object query; + public final String expected; + + public Parameter(Object query, String expected) { + this.query = query; + this.expected = expected; + } + } +} diff --git a/instrumentation/couchbase/couchbase-2-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/couchbase/v2_0/CouchbaseQuerySanitizer.java b/instrumentation/couchbase/couchbase-2-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/couchbase/v2_0/CouchbaseQuerySanitizer.java index 222d1b2e2f19..2fa42b1ca6bb 100644 --- a/instrumentation/couchbase/couchbase-2-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/couchbase/v2_0/CouchbaseQuerySanitizer.java +++ b/instrumentation/couchbase/couchbase-2-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/couchbase/v2_0/CouchbaseQuerySanitizer.java @@ -5,10 +5,10 @@ package io.opentelemetry.javaagent.instrumentation.couchbase.v2_0; -import io.opentelemetry.instrumentation.api.db.SqlDialect; -import io.opentelemetry.instrumentation.api.db.SqlStatementInfo; -import io.opentelemetry.instrumentation.api.db.SqlStatementSanitizer; -import io.opentelemetry.javaagent.bootstrap.internal.CommonConfig; +import io.opentelemetry.instrumentation.api.incubator.semconv.db.SqlDialect; +import io.opentelemetry.instrumentation.api.incubator.semconv.db.SqlStatementInfo; +import io.opentelemetry.instrumentation.api.incubator.semconv.db.SqlStatementSanitizer; +import io.opentelemetry.javaagent.bootstrap.internal.AgentCommonConfig; import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodType; @@ -17,7 +17,7 @@ public final class CouchbaseQuerySanitizer { private static final SqlStatementSanitizer sanitizer = - SqlStatementSanitizer.create(CommonConfig.get().isStatementSanitizationEnabled()); + SqlStatementSanitizer.create(AgentCommonConfig.get().isStatementSanitizationEnabled()); @Nullable private static final Class QUERY_CLASS; @Nullable private static final Class STATEMENT_CLASS; diff --git a/instrumentation/couchbase/couchbase-2-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/couchbase/v2_0/CouchbaseRequestInfo.java b/instrumentation/couchbase/couchbase-2-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/couchbase/v2_0/CouchbaseRequestInfo.java index 992a8512f855..b2f336b2b3fd 100644 --- a/instrumentation/couchbase/couchbase-2-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/couchbase/v2_0/CouchbaseRequestInfo.java +++ b/instrumentation/couchbase/couchbase-2-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/couchbase/v2_0/CouchbaseRequestInfo.java @@ -10,7 +10,7 @@ import com.google.auto.value.AutoValue; import io.opentelemetry.context.Context; import io.opentelemetry.context.ContextKey; -import io.opentelemetry.instrumentation.api.db.SqlStatementInfo; +import io.opentelemetry.instrumentation.api.incubator.semconv.db.SqlStatementInfo; import java.net.SocketAddress; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; diff --git a/instrumentation/couchbase/couchbase-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/couchbase/v2_0/CouchbaseAttributesGetter.java b/instrumentation/couchbase/couchbase-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/couchbase/v2_0/CouchbaseAttributesGetter.java index d65cd3fcc2da..05edc5679f75 100644 --- a/instrumentation/couchbase/couchbase-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/couchbase/v2_0/CouchbaseAttributesGetter.java +++ b/instrumentation/couchbase/couchbase-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/couchbase/v2_0/CouchbaseAttributesGetter.java @@ -5,15 +5,15 @@ package io.opentelemetry.javaagent.instrumentation.couchbase.v2_0; -import io.opentelemetry.instrumentation.api.instrumenter.db.DbClientAttributesGetter; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.opentelemetry.instrumentation.api.incubator.semconv.db.DbClientAttributesGetter; +import io.opentelemetry.semconv.incubating.DbIncubatingAttributes; import javax.annotation.Nullable; final class CouchbaseAttributesGetter implements DbClientAttributesGetter { @Override public String getSystem(CouchbaseRequestInfo couchbaseRequest) { - return SemanticAttributes.DbSystemValues.COUCHBASE; + return DbIncubatingAttributes.DbSystemValues.COUCHBASE; } @Override diff --git a/instrumentation/couchbase/couchbase-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/couchbase/v2_0/CouchbaseInstrumentationModule.java b/instrumentation/couchbase/couchbase-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/couchbase/v2_0/CouchbaseInstrumentationModule.java index 6cff8c4f1406..8c2889870cea 100644 --- a/instrumentation/couchbase/couchbase-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/couchbase/v2_0/CouchbaseInstrumentationModule.java +++ b/instrumentation/couchbase/couchbase-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/couchbase/v2_0/CouchbaseInstrumentationModule.java @@ -6,14 +6,18 @@ package io.opentelemetry.javaagent.instrumentation.couchbase.v2_0; import static java.util.Arrays.asList; +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.instrumentation.internal.ExperimentalInstrumentationModule; import java.util.List; @AutoService(InstrumentationModule.class) -public class CouchbaseInstrumentationModule extends InstrumentationModule { +public class CouchbaseInstrumentationModule extends InstrumentationModule + implements ExperimentalInstrumentationModule { + public CouchbaseInstrumentationModule() { super("couchbase", "couchbase-2.0"); } @@ -27,4 +31,9 @@ public boolean isHelperClass(String className) { public List typeInstrumentations() { return asList(new CouchbaseBucketInstrumentation(), new CouchbaseClusterInstrumentation()); } + + @Override + public List injectedClassNames() { + return singletonList("rx.__OpenTelemetryTracingUtil"); + } } diff --git a/instrumentation/couchbase/couchbase-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/couchbase/v2_0/CouchbaseNetAttributesGetter.java b/instrumentation/couchbase/couchbase-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/couchbase/v2_0/CouchbaseNetworkAttributesGetter.java similarity index 52% rename from instrumentation/couchbase/couchbase-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/couchbase/v2_0/CouchbaseNetAttributesGetter.java rename to instrumentation/couchbase/couchbase-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/couchbase/v2_0/CouchbaseNetworkAttributesGetter.java index b0557b9ccde2..a551acdb322b 100644 --- a/instrumentation/couchbase/couchbase-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/couchbase/v2_0/CouchbaseNetAttributesGetter.java +++ b/instrumentation/couchbase/couchbase-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/couchbase/v2_0/CouchbaseNetworkAttributesGetter.java @@ -5,29 +5,17 @@ package io.opentelemetry.javaagent.instrumentation.couchbase.v2_0; -import io.opentelemetry.instrumentation.api.instrumenter.net.NetClientAttributesGetter; +import io.opentelemetry.instrumentation.api.semconv.network.NetworkAttributesGetter; import java.net.InetSocketAddress; import java.net.SocketAddress; import javax.annotation.Nullable; -public class CouchbaseNetAttributesGetter - implements NetClientAttributesGetter { +public class CouchbaseNetworkAttributesGetter + implements NetworkAttributesGetter { @Nullable @Override - public String getServerAddress(CouchbaseRequestInfo couchbaseRequest) { - return null; - } - - @Nullable - @Override - public Integer getServerPort(CouchbaseRequestInfo couchbaseRequest) { - return null; - } - - @Nullable - @Override - public InetSocketAddress getServerInetSocketAddress( + public InetSocketAddress getNetworkPeerInetSocketAddress( CouchbaseRequestInfo couchbaseRequest, @Nullable Void unused) { SocketAddress peerAddress = couchbaseRequest.getPeerAddress(); if (peerAddress instanceof InetSocketAddress) { diff --git a/instrumentation/couchbase/couchbase-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/couchbase/v2_0/CouchbaseSingletons.java b/instrumentation/couchbase/couchbase-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/couchbase/v2_0/CouchbaseSingletons.java index 15b9ebddf3ad..0e6c3eee550a 100644 --- a/instrumentation/couchbase/couchbase-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/couchbase/v2_0/CouchbaseSingletons.java +++ b/instrumentation/couchbase/couchbase-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/couchbase/v2_0/CouchbaseSingletons.java @@ -6,16 +6,14 @@ package io.opentelemetry.javaagent.instrumentation.couchbase.v2_0; import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.instrumentation.api.incubator.semconv.db.DbClientAttributesExtractor; +import io.opentelemetry.instrumentation.api.incubator.semconv.db.DbClientSpanNameExtractor; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; import io.opentelemetry.instrumentation.api.instrumenter.InstrumenterBuilder; import io.opentelemetry.instrumentation.api.instrumenter.SpanKindExtractor; import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.db.DbClientAttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.db.DbClientSpanNameExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.net.NetClientAttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.net.PeerServiceAttributesExtractor; -import io.opentelemetry.javaagent.bootstrap.internal.CommonConfig; -import io.opentelemetry.javaagent.bootstrap.internal.InstrumentationConfig; +import io.opentelemetry.instrumentation.api.semconv.network.NetworkAttributesExtractor; +import io.opentelemetry.javaagent.bootstrap.internal.AgentInstrumentationConfig; public final class CouchbaseSingletons { @@ -27,21 +25,18 @@ public final class CouchbaseSingletons { CouchbaseAttributesGetter couchbaseAttributesGetter = new CouchbaseAttributesGetter(); SpanNameExtractor spanNameExtractor = new CouchbaseSpanNameExtractor(DbClientSpanNameExtractor.create(couchbaseAttributesGetter)); - CouchbaseNetAttributesGetter netAttributesGetter = new CouchbaseNetAttributesGetter(); + CouchbaseNetworkAttributesGetter netAttributesGetter = new CouchbaseNetworkAttributesGetter(); InstrumenterBuilder builder = Instrumenter.builder( GlobalOpenTelemetry.get(), INSTRUMENTATION_NAME, spanNameExtractor) .addAttributesExtractor(DbClientAttributesExtractor.create(couchbaseAttributesGetter)) - .addAttributesExtractor(NetClientAttributesExtractor.create(netAttributesGetter)) - .addAttributesExtractor( - PeerServiceAttributesExtractor.create( - netAttributesGetter, CommonConfig.get().getPeerServiceMapping())) + .addAttributesExtractor(NetworkAttributesExtractor.create(netAttributesGetter)) .addContextCustomizer( (context, couchbaseRequest, startAttributes) -> CouchbaseRequestInfo.init(context, couchbaseRequest)); - if (InstrumentationConfig.get() + if (AgentInstrumentationConfig.get() .getBoolean("otel.instrumentation.couchbase.experimental-span-attributes", false)) { builder.addAttributesExtractor(new ExperimentalAttributesExtractor()); } diff --git a/instrumentation/couchbase/couchbase-2.6/javaagent/src/test/groovy/CouchbaseSpanUtil.groovy b/instrumentation/couchbase/couchbase-2.6/javaagent/src/test/groovy/CouchbaseSpanUtil.groovy index 9dcec7802c66..0686c76da4b4 100644 --- a/instrumentation/couchbase/couchbase-2.6/javaagent/src/test/groovy/CouchbaseSpanUtil.groovy +++ b/instrumentation/couchbase/couchbase-2.6/javaagent/src/test/groovy/CouchbaseSpanUtil.groovy @@ -5,7 +5,8 @@ import io.opentelemetry.instrumentation.test.asserts.TraceAssert import io.opentelemetry.sdk.trace.data.SpanData -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import io.opentelemetry.semconv.incubating.DbIncubatingAttributes +import io.opentelemetry.semconv.NetworkAttributes import static io.opentelemetry.api.trace.SpanKind.CLIENT @@ -28,15 +29,15 @@ class CouchbaseSpanUtil { childOf((SpanData) parentSpan) } attributes { - "$SemanticAttributes.DB_SYSTEM" "couchbase" - "$SemanticAttributes.DB_NAME" bucketName - "$SemanticAttributes.DB_STATEMENT" statement - "$SemanticAttributes.DB_OPERATION"(operation ?: spanName) + "$DbIncubatingAttributes.DB_SYSTEM" "couchbase" + "$DbIncubatingAttributes.DB_NAME" bucketName + "$DbIncubatingAttributes.DB_STATEMENT" statement + "$DbIncubatingAttributes.DB_OPERATION"(operation ?: spanName) // Because of caching, not all requests hit the server so these attributes may be absent - "$SemanticAttributes.NET_SOCK_PEER_ADDR" { it == "127.0.0.1" || it == null } - "$SemanticAttributes.NET_SOCK_PEER_NAME" { it == "localhost" || it == "127.0.0.1" || it == null } - "$SemanticAttributes.NET_SOCK_PEER_PORT" { it == null || it instanceof Number } + "$NetworkAttributes.NETWORK_TYPE" { it == "ipv4" || it == null } + "$NetworkAttributes.NETWORK_PEER_ADDRESS" { it == "127.0.0.1" || it == null } + "$NetworkAttributes.NETWORK_PEER_PORT" { it instanceof Number || it == null } // Because of caching, not all requests hit the server so this tag may be absent "couchbase.local.address" { it == null || it instanceof String } diff --git a/instrumentation/couchbase/couchbase-3.1.6/tracing-opentelemetry-shaded/build.gradle.kts b/instrumentation/couchbase/couchbase-3.1.6/tracing-opentelemetry-shaded/build.gradle.kts index 366ba2c0aef4..128e45cc51f1 100644 --- a/instrumentation/couchbase/couchbase-3.1.6/tracing-opentelemetry-shaded/build.gradle.kts +++ b/instrumentation/couchbase/couchbase-3.1.6/tracing-opentelemetry-shaded/build.gradle.kts @@ -1,6 +1,5 @@ plugins { - id("com.github.johnrengelman.shadow") - + id("com.gradleup.shadow") id("otel.java-conventions") } diff --git a/instrumentation/couchbase/couchbase-3.1/javaagent/build.gradle.kts b/instrumentation/couchbase/couchbase-3.1/javaagent/build.gradle.kts index faef00237dcf..0d6c7ddc5394 100644 --- a/instrumentation/couchbase/couchbase-3.1/javaagent/build.gradle.kts +++ b/instrumentation/couchbase/couchbase-3.1/javaagent/build.gradle.kts @@ -31,7 +31,8 @@ dependencies { ), ) - library("com.couchbase.client:java-client:3.1.0") + // 3.1.4 (instead of 3.1.0) needed for test stability and for compatibility with server versions that run on M1 processors + library("com.couchbase.client:java-client:3.1.4") testImplementation("org.testcontainers:couchbase") diff --git a/instrumentation/couchbase/couchbase-3.1/javaagent/src/test/groovy/CouchbaseClient31Test.groovy b/instrumentation/couchbase/couchbase-3.1/javaagent/src/test/groovy/CouchbaseClient31Test.groovy deleted file mode 100644 index a358c9ed445a..000000000000 --- a/instrumentation/couchbase/couchbase-3.1/javaagent/src/test/groovy/CouchbaseClient31Test.groovy +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -import com.couchbase.client.core.env.TimeoutConfig -import com.couchbase.client.core.error.DocumentNotFoundException -import com.couchbase.client.java.Cluster -import com.couchbase.client.java.ClusterOptions -import com.couchbase.client.java.Collection -import com.couchbase.client.java.env.ClusterEnvironment -import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification -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 - -import java.time.Duration - -// 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 logger = 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(logger)) - .withStartupTimeout(Duration.ofSeconds(120)) - couchbase.start() - - ClusterEnvironment environment = ClusterEnvironment.builder() - .timeoutConfig(TimeoutConfig.kvTimeout(Duration.ofSeconds(10))) - .build() - - cluster = Cluster.connect(couchbase.connectionString, ClusterOptions - .clusterOptions(couchbase.username, couchbase.password) - .environment(environment)) - def bucket = cluster.bucket("test") - collection = bucket.defaultCollection() - bucket.waitUntilReady(Duration.ofSeconds(30)) - } - - def cleanupSpec() { - couchbase.stop() - } - - def "emits spans"() { - when: - try { - collection.get("id") - } catch (DocumentNotFoundException e) { - // Expected - } - - then: - assertTracesWithoutScopeVersionVerification(1) { - trace(0, 2) { - span(0) { - name(~/.*get/) - } - span(1) { - name(~/.*dispatch_to_server/) - } - } - } - - cleanup: - cluster.disconnect() - } -} diff --git a/instrumentation/couchbase/couchbase-3.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/couchbase/v3_1/CouchbaseClient31Test.java b/instrumentation/couchbase/couchbase-3.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/couchbase/v3_1/CouchbaseClient31Test.java new file mode 100644 index 000000000000..b74fb2927ca9 --- /dev/null +++ b/instrumentation/couchbase/couchbase-3.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/couchbase/v3_1/CouchbaseClient31Test.java @@ -0,0 +1,90 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.couchbase.v3_1; + +import com.couchbase.client.core.env.TimeoutConfig; +import com.couchbase.client.core.error.DocumentNotFoundException; +import com.couchbase.client.java.Bucket; +import com.couchbase.client.java.Cluster; +import com.couchbase.client.java.ClusterOptions; +import com.couchbase.client.java.Collection; +import com.couchbase.client.java.env.ClusterEnvironment; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import java.time.Duration; +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; +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; + +// 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 { + @RegisterExtension + private static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + private static final Logger logger = LoggerFactory.getLogger("couchbase-container"); + + private static CouchbaseContainer couchbase; + private static Cluster cluster; + private static Collection collection; + + @BeforeAll + static void setup() { + couchbase = + new CouchbaseContainer("couchbase/server:7.6.0") + .withExposedPorts(8091) + .withEnabledServices(CouchbaseService.KV) + .withBucket(new BucketDefinition("test")) + .withLogConsumer(new Slf4jLogConsumer(logger)) + .withStartupTimeout(Duration.ofMinutes(2)); + couchbase.start(); + + ClusterEnvironment environment = + ClusterEnvironment.builder() + .timeoutConfig(TimeoutConfig.kvTimeout(Duration.ofSeconds(30))) + .build(); + + cluster = + Cluster.connect( + couchbase.getConnectionString(), + ClusterOptions.clusterOptions(couchbase.getUsername(), couchbase.getPassword()) + .environment(environment)); + + Bucket bucket = cluster.bucket("test"); + collection = bucket.defaultCollection(); + + // Wait 1 minute due to slow startup contributing to flakiness + bucket.waitUntilReady(Duration.ofMinutes(1)); + } + + @AfterAll + static void cleanup() { + cluster.disconnect(); + couchbase.stop(); + } + + @Test + void testEmitsSpans() { + try { + collection.get("id"); + } catch (DocumentNotFoundException e) { + // Expected + } + + testing.waitAndAssertTracesWithoutScopeVersionVerification( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("get"), span -> span.hasName("dispatch_to_server"))); + } +} diff --git a/instrumentation/couchbase/couchbase-3.1/tracing-opentelemetry-shaded/build.gradle.kts b/instrumentation/couchbase/couchbase-3.1/tracing-opentelemetry-shaded/build.gradle.kts index 53e83b2a78dd..e1bff72f5885 100644 --- a/instrumentation/couchbase/couchbase-3.1/tracing-opentelemetry-shaded/build.gradle.kts +++ b/instrumentation/couchbase/couchbase-3.1/tracing-opentelemetry-shaded/build.gradle.kts @@ -1,5 +1,5 @@ plugins { - id("com.github.johnrengelman.shadow") + id("com.gradleup.shadow") id("otel.java-conventions") } diff --git a/instrumentation/couchbase/couchbase-3.2/javaagent/src/test/groovy/CouchbaseClient32Test.groovy b/instrumentation/couchbase/couchbase-3.2/javaagent/src/test/groovy/CouchbaseClient32Test.groovy deleted file mode 100644 index 541cb8a2caf6..000000000000 --- a/instrumentation/couchbase/couchbase-3.2/javaagent/src/test/groovy/CouchbaseClient32Test.groovy +++ /dev/null @@ -1,80 +0,0 @@ -/* - * 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 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 - -import java.time.Duration - -import static io.opentelemetry.api.trace.StatusCode.ERROR - -// 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 CouchbaseClient32Test extends AgentInstrumentationSpecification { - private static final Logger logger = 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(logger)) - .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(30)) - } - - def cleanupSpec() { - couchbase.stop() - } - - def "emits spans"() { - when: - try { - collection.get("id") - } catch (DocumentNotFoundException e) { - // Expected - } - - then: - assertTracesWithoutScopeVersionVerification(1) { - trace(0, 2) { - span(0) { - name(~/.*get/) - if (Boolean.getBoolean("testLatestDeps")) { - // this is the correct behavior - status ERROR - } - } - span(1) { - name(~/.*dispatch_to_server/) - } - } - } - - cleanup: - cluster.disconnect() - } -} diff --git a/instrumentation/couchbase/couchbase-3.2/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/couchbase/v3_2/CouchbaseClient32Test.java b/instrumentation/couchbase/couchbase-3.2/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/couchbase/v3_2/CouchbaseClient32Test.java new file mode 100644 index 000000000000..ef7a320ccabe --- /dev/null +++ b/instrumentation/couchbase/couchbase-3.2/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/couchbase/v3_2/CouchbaseClient32Test.java @@ -0,0 +1,83 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.couchbase.v3_2; + +import com.couchbase.client.core.error.DocumentNotFoundException; +import com.couchbase.client.java.Bucket; +import com.couchbase.client.java.Cluster; +import com.couchbase.client.java.Collection; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.sdk.trace.data.StatusData; +import java.time.Duration; +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; +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; + +// 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 CouchbaseClient32Test { + @RegisterExtension + private static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + private static final Logger logger = LoggerFactory.getLogger("couchbase-container"); + + static CouchbaseContainer couchbase; + static Cluster cluster; + static Collection collection; + + @BeforeAll + static void setup() { + couchbase = + new CouchbaseContainer("couchbase/server:6.5.1") + .withExposedPorts(8091) + .withEnabledServices(CouchbaseService.KV) + .withBucket(new BucketDefinition("test")) + .withLogConsumer(new Slf4jLogConsumer(logger)) + .withStartupTimeout(Duration.ofMinutes(2)); + couchbase.start(); + + cluster = + Cluster.connect( + couchbase.getConnectionString(), couchbase.getUsername(), couchbase.getPassword()); + Bucket bucket = cluster.bucket("test"); + collection = bucket.defaultCollection(); + bucket.waitUntilReady(Duration.ofSeconds(30)); + } + + @AfterAll + static void cleanup() { + cluster.disconnect(); + couchbase.stop(); + } + + @Test + void testEmitsSpans() { + try { + collection.get("id"); + } catch (DocumentNotFoundException e) { + // Expected + } + + testing.waitAndAssertTracesWithoutScopeVersionVerification( + trace -> + trace.hasSpansSatisfyingExactly( + span -> { + span.hasName("get"); + if (Boolean.getBoolean("testLatestDeps")) { + span.hasStatus(StatusData.error()); + } + }, + span -> span.hasName("dispatch_to_server"))); + } +} diff --git a/instrumentation/couchbase/couchbase-3.2/tracing-opentelemetry-shaded/build.gradle.kts b/instrumentation/couchbase/couchbase-3.2/tracing-opentelemetry-shaded/build.gradle.kts index 9844a74e964e..c97e14e07e7a 100644 --- a/instrumentation/couchbase/couchbase-3.2/tracing-opentelemetry-shaded/build.gradle.kts +++ b/instrumentation/couchbase/couchbase-3.2/tracing-opentelemetry-shaded/build.gradle.kts @@ -1,6 +1,5 @@ plugins { - id("com.github.johnrengelman.shadow") - + id("com.gradleup.shadow") id("otel.java-conventions") } diff --git a/instrumentation/couchbase/couchbase-common/testing/src/main/groovy/util/AbstractCouchbaseTest.groovy b/instrumentation/couchbase/couchbase-common/testing/src/main/groovy/util/AbstractCouchbaseTest.groovy index e16bb1f6e3f0..e669562f1917 100644 --- a/instrumentation/couchbase/couchbase-common/testing/src/main/groovy/util/AbstractCouchbaseTest.groovy +++ b/instrumentation/couchbase/couchbase-common/testing/src/main/groovy/util/AbstractCouchbaseTest.groovy @@ -19,7 +19,7 @@ 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 io.opentelemetry.semconv.incubating.DbIncubatingAttributes import spock.lang.Shared import java.util.concurrent.TimeUnit @@ -120,10 +120,10 @@ abstract class AbstractCouchbaseTest extends AgentInstrumentationSpecification { childOf((SpanData) parentSpan) } attributes { - "$SemanticAttributes.DB_SYSTEM" "couchbase" - "$SemanticAttributes.DB_NAME" bucketName - "$SemanticAttributes.DB_STATEMENT" statement - "$SemanticAttributes.DB_OPERATION"(operation ?: spanName) + "$DbIncubatingAttributes.DB_SYSTEM" "couchbase" + "$DbIncubatingAttributes.DB_NAME" bucketName + "$DbIncubatingAttributes.DB_STATEMENT" statement + "$DbIncubatingAttributes.DB_OPERATION"(operation ?: spanName) } } } diff --git a/instrumentation/dropwizard/dropwizard-testing/build.gradle.kts b/instrumentation/dropwizard/dropwizard-testing/build.gradle.kts index aef551a103c2..c955214ed6be 100644 --- a/instrumentation/dropwizard/dropwizard-testing/build.gradle.kts +++ b/instrumentation/dropwizard/dropwizard-testing/build.gradle.kts @@ -21,5 +21,13 @@ configurations.testRuntimeClasspath { // requires old logback (and therefore also old slf4j) force("ch.qos.logback:logback-classic:1.2.11") force("org.slf4j:slf4j-api:1.7.36") + + // dropwizard testing is not compatible with jackson 2.16.0 + force("com.fasterxml.jackson.core:jackson-databind:2.15.3") + force("com.fasterxml.jackson.module:jackson-module-afterburner:2.15.3") } } + +tasks.withType().configureEach { + jvmArgs("-Dotel.instrumentation.common.experimental.controller-telemetry.enabled=true") +} diff --git a/instrumentation/dropwizard/dropwizard-testing/src/test/groovy/DropwizardTest.groovy b/instrumentation/dropwizard/dropwizard-testing/src/test/groovy/DropwizardTest.groovy index a121252656d3..197d6181b2cb 100644 --- a/instrumentation/dropwizard/dropwizard-testing/src/test/groovy/DropwizardTest.groovy +++ b/instrumentation/dropwizard/dropwizard-testing/src/test/groovy/DropwizardTest.groovy @@ -10,6 +10,7 @@ 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.api.internal.HttpConstants import io.opentelemetry.instrumentation.test.AgentTestTrait import io.opentelemetry.instrumentation.test.asserts.TraceAssert import io.opentelemetry.instrumentation.test.base.HttpServerTest @@ -83,17 +84,25 @@ class DropwizardTest extends HttpServerTest implements Ag } @Override - String expectedHttpRoute(ServerEndpoint endpoint) { + String expectedHttpRoute(ServerEndpoint endpoint, String method) { + if (method == HttpConstants._OTHER) { + return getContextPath() + "/*" + } switch (endpoint) { case NOT_FOUND: return getContextPath() + "/*" case PATH_PARAM: return getContextPath() + "/path/{id}/param" default: - return super.expectedHttpRoute(endpoint) + return super.expectedHttpRoute(endpoint, method) } } + @Override + int getResponseCodeOnNonStandardHttpMethod() { + 405 + } + @Override void handlerSpan(TraceAssert trace, int index, Object parent, String method = "GET", ServerEndpoint endpoint = SUCCESS) { trace.span(index) { diff --git a/instrumentation/dropwizard/dropwizard-views-0.7/javaagent/build.gradle.kts b/instrumentation/dropwizard/dropwizard-views-0.7/javaagent/build.gradle.kts index 300423192aff..e7f0e014775f 100644 --- a/instrumentation/dropwizard/dropwizard-views-0.7/javaagent/build.gradle.kts +++ b/instrumentation/dropwizard/dropwizard-views-0.7/javaagent/build.gradle.kts @@ -16,3 +16,8 @@ dependencies { testImplementation("io.dropwizard:dropwizard-views-freemarker:0.7.0") testImplementation("io.dropwizard:dropwizard-views-mustache:0.7.0") } + +tasks.withType().configureEach { + jvmArgs("-Dotel.instrumentation.common.experimental.view-telemetry.enabled=true") + jvmArgs("-Dotel.instrumentation.common.experimental.controller-telemetry.enabled=true") +} diff --git a/instrumentation/dropwizard/dropwizard-views-0.7/javaagent/src/test/groovy/ViewRenderTest.groovy b/instrumentation/dropwizard/dropwizard-views-0.7/javaagent/src/test/groovy/ViewRenderTest.groovy deleted file mode 100644 index 414926d5658f..000000000000 --- a/instrumentation/dropwizard/dropwizard-views-0.7/javaagent/src/test/groovy/ViewRenderTest.groovy +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -import io.dropwizard.views.View -import io.dropwizard.views.freemarker.FreemarkerViewRenderer -import io.dropwizard.views.mustache.MustacheViewRenderer -import io.opentelemetry.api.trace.SpanKind -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: - runWithSpan("parent") { - renderer.render(view, Locale.ENGLISH, outputStream) - } - - then: - outputStream.toString().contains("This is an example of a view") - assertTraces(1) { - trace(0, 2) { - span(0) { - name "parent" - kind SpanKind.INTERNAL - hasNoParent() - } - 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) {} - } - - def "do not create span when there's no parent"() { - setup: - def outputStream = new ByteArrayOutputStream() - def view = new View("/views/ftl/utf8.ftl", StandardCharsets.UTF_8) {} - - when: - new FreemarkerViewRenderer().render(view, Locale.ENGLISH, outputStream) - - then: - Thread.sleep(500) - assert traces.isEmpty() - } -} diff --git a/instrumentation/dropwizard/dropwizard-views-0.7/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/dropwizardviews/ViewRenderTest.java b/instrumentation/dropwizard/dropwizard-views-0.7/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/dropwizardviews/ViewRenderTest.java new file mode 100644 index 000000000000..dbba452d344e --- /dev/null +++ b/instrumentation/dropwizard/dropwizard-views-0.7/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/dropwizardviews/ViewRenderTest.java @@ -0,0 +1,67 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.dropwizardviews; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.dropwizard.views.View; +import io.dropwizard.views.ViewRenderer; +import io.dropwizard.views.freemarker.FreemarkerViewRenderer; +import io.dropwizard.views.mustache.MustacheViewRenderer; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Locale; +import java.util.stream.Stream; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +class ViewRenderTest { + + @RegisterExtension + private static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + private static Stream provideParameters() { + return Stream.of( + Arguments.of(new FreemarkerViewRenderer(), "/views/ftl/utf8.ftl"), + Arguments.of(new MustacheViewRenderer(), "/views/mustache/utf8.mustache"), + Arguments.of(new FreemarkerViewRenderer(), "/views/ftl/utf8.ftl"), + Arguments.of(new MustacheViewRenderer(), "/views/mustache/utf8.mustache")); + } + + @ParameterizedTest + @MethodSource("provideParameters") + void testSpan(ViewRenderer renderer, String template) throws IOException { + View view = new View(template, StandardCharsets.UTF_8) {}; + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + testing.runWithSpan( + "parent", + () -> { + renderer.render(view, Locale.ENGLISH, outputStream); + }); + assertThat(outputStream.toString("UTF-8")).contains("This is an example of a view"); + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(), + span -> span.hasName("Render " + template).hasParent(trace.getSpan(0)))); + } + + @Test + void testDoesNotCreateSpanWithoutParent() throws InterruptedException, IOException { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + View view = new View("/views/ftl/utf8.ftl", StandardCharsets.UTF_8) {}; + new FreemarkerViewRenderer().render(view, Locale.ENGLISH, outputStream); + Thread.sleep(500); + assertThat(testing.spans().size()).isEqualTo(0); + } +} diff --git a/instrumentation/elasticsearch/README.md b/instrumentation/elasticsearch/README.md index 0b78df613e25..a63ceb46c197 100644 --- a/instrumentation/elasticsearch/README.md +++ b/instrumentation/elasticsearch/README.md @@ -1,5 +1,13 @@ # Settings for the elasticsearch instrumentation -| System property | Type | Default | Description | -|---|---|---|---| -| `otel.instrumentation.elasticsearch.experimental-span-attributes` | `Boolean | `false` | Enable the capture of experimental span attributes. | +## Settings for the [Elasticsearch Java API Client](https://www.elastic.co/guide/en/elasticsearch/client/java-api-client/current/index.html) instrumentation + +| System property | Type | Default | Description | +| --------------------------------------------------------- | ------- | ------- | -------------------------------------------------------------------------------------------------------------------------- | +| `otel.instrumentation.elasticsearch.capture-search-query` | Boolean | `false` | Enable the capture of search query bodies. Attention: Elasticsearch queries may contain personal or sensitive information. | + +## Settings for the [Elasticsearch Transport Client](https://www.elastic.co/guide/en/elasticsearch/client/java-api/current/index.html) instrumentation + +| System property | Type | Default | Description | +| ----------------------------------------------------------------- | ------- | ------- | --------------------------------------------------- | +| `otel.instrumentation.elasticsearch.experimental-span-attributes` | Boolean | `false` | Enable the capture of experimental span attributes. | diff --git a/instrumentation/elasticsearch/elasticsearch-api-client-7.16/javaagent-unit-tests/build.gradle.kts b/instrumentation/elasticsearch/elasticsearch-api-client-7.16/javaagent-unit-tests/build.gradle.kts new file mode 100644 index 000000000000..2eb2dd8c5a71 --- /dev/null +++ b/instrumentation/elasticsearch/elasticsearch-api-client-7.16/javaagent-unit-tests/build.gradle.kts @@ -0,0 +1,8 @@ +plugins { + id("otel.java-conventions") +} + +dependencies { + testImplementation(project(":instrumentation:elasticsearch:elasticsearch-rest-common:javaagent")) + testImplementation(project(":instrumentation:elasticsearch:elasticsearch-api-client-7.16:javaagent")) +} diff --git a/instrumentation/elasticsearch/elasticsearch-api-client-7.16/javaagent-unit-tests/src/test/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/rest/ElasticsearchEndpointMapTest.java b/instrumentation/elasticsearch/elasticsearch-api-client-7.16/javaagent-unit-tests/src/test/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/rest/ElasticsearchEndpointMapTest.java new file mode 100644 index 000000000000..dc75d567e235 --- /dev/null +++ b/instrumentation/elasticsearch/elasticsearch-api-client-7.16/javaagent-unit-tests/src/test/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/rest/ElasticsearchEndpointMapTest.java @@ -0,0 +1,124 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.elasticsearch.rest; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import io.opentelemetry.instrumentation.elasticsearch.rest.internal.ElasticsearchEndpointDefinition; +import io.opentelemetry.javaagent.instrumentation.elasticsearch.apiclient.ElasticsearchEndpointMap; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import org.junit.jupiter.api.Test; + +public class ElasticsearchEndpointMapTest { + + private static final Set SEARCH_ENDPOINTS = + new HashSet<>( + Arrays.asList( + "search", + "async_search.submit", + "msearch", + "eql.search", + "terms_enum", + "search_template", + "msearch_template", + "render_search_template")); + + private static List getPathParts(String route) { + List pathParts = new ArrayList<>(); + String routeFragment = route; + int paramStartIndex = routeFragment.indexOf('{'); + while (paramStartIndex >= 0) { + int paramEndIndex = routeFragment.indexOf('}'); + if (paramEndIndex < 0 || paramEndIndex <= paramStartIndex + 1) { + throw new IllegalStateException("Invalid route syntax!"); + } + pathParts.add(routeFragment.substring(paramStartIndex + 1, paramEndIndex)); + + int nextIdx = paramEndIndex + 1; + if (nextIdx >= routeFragment.length()) { + break; + } + + routeFragment = routeFragment.substring(nextIdx); + paramStartIndex = routeFragment.indexOf('{'); + } + return pathParts; + } + + @Test + public void testIsSearchEndpoint() { + for (ElasticsearchEndpointDefinition esEndpointDefinition : + ElasticsearchEndpointMap.getAllEndpoints()) { + String endpointId = esEndpointDefinition.getEndpointName(); + assertEquals(SEARCH_ENDPOINTS.contains(endpointId), esEndpointDefinition.isSearchEndpoint()); + } + } + + @Test + public void testProcessPathParts() { + for (ElasticsearchEndpointDefinition esEndpointDefinition : + ElasticsearchEndpointMap.getAllEndpoints()) { + for (String route : + esEndpointDefinition.getRoutes().stream() + .map(ElasticsearchEndpointDefinition.Route::getName) + .collect(Collectors.toList())) { + List pathParts = getPathParts(route); + String resolvedRoute = route.replace("{", "").replace("}", ""); + Map observedParams = new HashMap<>(); + esEndpointDefinition.processPathParts(resolvedRoute, (k, v) -> observedParams.put(k, v)); + + Map expectedMap = new HashMap<>(); + pathParts.forEach(part -> expectedMap.put(part, part)); + + assertEquals(expectedMap, observedParams); + } + } + } + + @Test + public void testSearchEndpoint() { + ElasticsearchEndpointDefinition esEndpoint = ElasticsearchEndpointMap.get("search"); + Map observedParams = new HashMap<>(); + esEndpoint.processPathParts( + "/test-index-1,test-index-2/_search", (k, v) -> observedParams.put(k, v)); + + assertEquals("test-index-1,test-index-2", observedParams.get("index")); + } + + @Test + public void testBuildRegexPattern() { + Pattern pattern = + ElasticsearchEndpointDefinition.EndpointPattern.buildRegexPattern( + "/_nodes/{node_id}/shutdown"); + assertEquals("^/_nodes/(?[^/]+)/shutdown$", pattern.pattern()); + + pattern = + ElasticsearchEndpointDefinition.EndpointPattern.buildRegexPattern( + "/_snapshot/{repository}/{snapshot}/_mount"); + assertEquals("^/_snapshot/(?[^/]+)/(?[^/]+)/_mount$", pattern.pattern()); + + pattern = + ElasticsearchEndpointDefinition.EndpointPattern.buildRegexPattern( + "/_security/profile/_suggest"); + assertEquals("^/_security/profile/_suggest$", pattern.pattern()); + + pattern = + ElasticsearchEndpointDefinition.EndpointPattern.buildRegexPattern( + "/_application/search_application/{name}"); + assertEquals("^/_application/search_application/(?[^/]+)$", pattern.pattern()); + + pattern = ElasticsearchEndpointDefinition.EndpointPattern.buildRegexPattern("/"); + assertEquals("^/$", pattern.pattern()); + } +} diff --git a/instrumentation/elasticsearch/elasticsearch-api-client-7.16/javaagent/build.gradle.kts b/instrumentation/elasticsearch/elasticsearch-api-client-7.16/javaagent/build.gradle.kts new file mode 100644 index 000000000000..676975efd718 --- /dev/null +++ b/instrumentation/elasticsearch/elasticsearch-api-client-7.16/javaagent/build.gradle.kts @@ -0,0 +1,87 @@ +plugins { + id("otel.javaagent-instrumentation") +} + +muzzle { + pass { + group.set("co.elastic.clients") + module.set("elasticsearch-java") + versions.set("[7.16,7.17.20)") // 7.17.20+ has native, on-by-default opentelemetry instrumentation + } + pass { + group.set("co.elastic.clients") + module.set("elasticsearch-java") + versions.set("[8.0.0,8.10)") // 8.10+ has native, on-by-default opentelemetry instrumentation + } + fail { + group.set("co.elastic.clients") + module.set("elasticsearch-java") + versions.set("(,7.16)") + } + fail { + group.set("co.elastic.clients") + module.set("elasticsearch-java") + versions.set("[7.17.20,8.0.0)") + } + fail { + group.set("co.elastic.clients") + module.set("elasticsearch-java") + versions.set("[8.10,)") + } +} + +dependencies { + library("co.elastic.clients:elasticsearch-java:7.16.0") + + implementation(project(":instrumentation:elasticsearch:elasticsearch-rest-common:javaagent")) + + testInstrumentation(project(":instrumentation:elasticsearch:elasticsearch-rest-7.0:javaagent")) + testInstrumentation(project(":instrumentation:apache-httpclient:apache-httpclient-4.0:javaagent")) + testInstrumentation(project(":instrumentation:apache-httpasyncclient-4.1:javaagent")) + + testImplementation("com.fasterxml.jackson.core:jackson-databind:2.14.2") + testImplementation("org.testcontainers:elasticsearch") + + latestDepTestLibrary("co.elastic.clients:elasticsearch-java:7.17.19") +} + +val latestDepTest = findProperty("testLatestDeps") as Boolean +testing { + suites { + val version8Test by registering(JvmTestSuite::class) { + dependencies { + sources { + java { + setSrcDirs(listOf("src/test/java")) + } + resources { + setSrcDirs(listOf("src/test/resources")) + } + } + + implementation("com.fasterxml.jackson.core:jackson-databind:2.14.2") + implementation("org.testcontainers:elasticsearch") + + if (latestDepTest) { + // 8.10+ has native, on-by-default opentelemetry instrumentation + implementation("co.elastic.clients:elasticsearch-java:8.9.+") + } else { + implementation("co.elastic.clients:elasticsearch-java:8.0.0") + } + } + } + } +} + +tasks { + test { + usesService(gradle.sharedServices.registrations["testcontainersBuildService"].service) + } + check { + dependsOn(testing.suites) + } +} + +tasks.withType().configureEach { + jvmArgs("-Dotel.instrumentation.common.experimental.controller-telemetry.enabled=true") +} diff --git a/instrumentation/elasticsearch/elasticsearch-api-client-7.16/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/apiclient/ElasticsearchApiClientInstrumentationModule.java b/instrumentation/elasticsearch/elasticsearch-api-client-7.16/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/apiclient/ElasticsearchApiClientInstrumentationModule.java new file mode 100644 index 000000000000..75cad4dee6fd --- /dev/null +++ b/instrumentation/elasticsearch/elasticsearch-api-client-7.16/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/apiclient/ElasticsearchApiClientInstrumentationModule.java @@ -0,0 +1,45 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.elasticsearch.apiclient; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.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.extension.instrumentation.internal.ExperimentalInstrumentationModule; +import java.util.List; +import net.bytebuddy.matcher.ElementMatcher; + +@AutoService(InstrumentationModule.class) +public class ElasticsearchApiClientInstrumentationModule extends InstrumentationModule + implements ExperimentalInstrumentationModule { + public ElasticsearchApiClientInstrumentationModule() { + super("elasticsearch-api-client", "elasticsearch-api-client-7.16", "elasticsearch"); + } + + @Override + public ElementMatcher.Junction classLoaderMatcher() { + // Since Elasticsearch client version 8.10, the ES client comes with a native OTel + // instrumentation + // that introduced the class `co.elastic.clients.transport.instrumentation.Instrumentation`. + // Disabling agent instrumentation for those cases. + return not(hasClassesNamed("co.elastic.clients.transport.instrumentation.Instrumentation")); + } + + @Override + public String getModuleGroup() { + return "elasticsearch"; + } + + @Override + public List typeInstrumentations() { + return asList( + new RestClientTransportInstrumentation(), new RestClientHttpClientInstrumentation()); + } +} diff --git a/instrumentation/elasticsearch/elasticsearch-api-client-7.16/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/apiclient/ElasticsearchEndpointMap.java b/instrumentation/elasticsearch/elasticsearch-api-client-7.16/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/apiclient/ElasticsearchEndpointMap.java new file mode 100644 index 000000000000..1e8401c7c6a9 --- /dev/null +++ b/instrumentation/elasticsearch/elasticsearch-api-client-7.16/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/apiclient/ElasticsearchEndpointMap.java @@ -0,0 +1,883 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.elasticsearch.apiclient; + +import io.opentelemetry.instrumentation.elasticsearch.rest.internal.ElasticsearchEndpointDefinition; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import javax.annotation.Nullable; + +public final class ElasticsearchEndpointMap { + + private static final Map routesMap; + + static { + Map routes = new HashMap<>(415); + initEndpoint(routes, "async_search.status", false, "/_async_search/status/{id}"); + initEndpoint(routes, "indices.analyze", false, "/_analyze", "/{index}/_analyze"); + initEndpoint(routes, "sql.clear_cursor", false, "/_sql/close"); + initEndpoint(routes, "ml.delete_datafeed", false, "/_ml/datafeeds/{datafeed_id}"); + initEndpoint(routes, "explain", false, "/{index}/_explain/{id}"); + initEndpoint( + routes, + "cat.thread_pool", + false, + "/_cat/thread_pool", + "/_cat/thread_pool/{thread_pool_patterns}"); + initEndpoint(routes, "ml.delete_calendar", false, "/_ml/calendars/{calendar_id}"); + initEndpoint(routes, "indices.create_data_stream", false, "/_data_stream/{name}"); + initEndpoint(routes, "cat.fielddata", false, "/_cat/fielddata", "/_cat/fielddata/{fields}"); + initEndpoint(routes, "security.enroll_node", false, "/_security/enroll/node"); + initEndpoint(routes, "slm.get_status", false, "/_slm/status"); + initEndpoint(routes, "ml.put_calendar", false, "/_ml/calendars/{calendar_id}"); + initEndpoint(routes, "create", false, "/{index}/_create/{id}"); + initEndpoint( + routes, + "ml.preview_datafeed", + false, + "/_ml/datafeeds/{datafeed_id}/_preview", + "/_ml/datafeeds/_preview"); + initEndpoint(routes, "indices.put_template", false, "/_template/{name}"); + initEndpoint( + routes, + "nodes.reload_secure_settings", + false, + "/_nodes/reload_secure_settings", + "/_nodes/{node_id}/reload_secure_settings"); + initEndpoint(routes, "indices.delete_data_stream", false, "/_data_stream/{name}"); + initEndpoint( + routes, + "transform.schedule_now_transform", + false, + "/_transform/{transform_id}/_schedule_now"); + initEndpoint(routes, "slm.stop", false, "/_slm/stop"); + initEndpoint(routes, "rollup.delete_job", false, "/_rollup/job/{id}"); + initEndpoint(routes, "cluster.put_component_template", false, "/_component_template/{name}"); + initEndpoint(routes, "delete_script", false, "/_scripts/{id}"); + initEndpoint(routes, "ml.delete_trained_model", false, "/_ml/trained_models/{model_id}"); + initEndpoint( + routes, + "indices.simulate_template", + false, + "/_index_template/_simulate", + "/_index_template/_simulate/{name}"); + initEndpoint(routes, "slm.get_lifecycle", false, "/_slm/policy/{policy_id}", "/_slm/policy"); + initEndpoint(routes, "security.enroll_kibana", false, "/_security/enroll/kibana"); + initEndpoint(routes, "fleet.search", false, "/{index}/_fleet/_fleet_search"); + initEndpoint(routes, "reindex_rethrottle", false, "/_reindex/{task_id}/_rethrottle"); + initEndpoint(routes, "ml.update_filter", false, "/_ml/filters/{filter_id}/_update"); + initEndpoint(routes, "rollup.get_rollup_caps", false, "/_rollup/data/{id}", "/_rollup/data"); + initEndpoint( + routes, "ccr.resume_auto_follow_pattern", false, "/_ccr/auto_follow/{name}/resume"); + initEndpoint(routes, "features.get_features", false, "/_features"); + initEndpoint(routes, "slm.get_stats", false, "/_slm/stats"); + initEndpoint(routes, "indices.clear_cache", false, "/_cache/clear", "/{index}/_cache/clear"); + initEndpoint( + routes, + "cluster.post_voting_config_exclusions", + false, + "/_cluster/voting_config_exclusions"); + initEndpoint(routes, "index", false, "/{index}/_doc/{id}", "/{index}/_doc"); + initEndpoint(routes, "cat.pending_tasks", false, "/_cat/pending_tasks"); + initEndpoint(routes, "indices.promote_data_stream", false, "/_data_stream/_promote/{name}"); + initEndpoint(routes, "ml.delete_filter", false, "/_ml/filters/{filter_id}"); + initEndpoint(routes, "sql.query", false, "/_sql"); + initEndpoint(routes, "ccr.follow_stats", false, "/{index}/_ccr/stats"); + initEndpoint(routes, "transform.stop_transform", false, "/_transform/{transform_id}/_stop"); + initEndpoint( + routes, + "security.has_privileges_user_profile", + false, + "/_security/profile/_has_privileges"); + initEndpoint( + routes, "autoscaling.delete_autoscaling_policy", false, "/_autoscaling/policy/{name}"); + initEndpoint(routes, "scripts_painless_execute", false, "/_scripts/painless/_execute"); + initEndpoint(routes, "indices.delete", false, "/{index}"); + initEndpoint( + routes, "security.clear_cached_roles", false, "/_security/role/{name}/_clear_cache"); + initEndpoint(routes, "eql.delete", false, "/_eql/search/{id}"); + initEndpoint(routes, "update", false, "/{index}/_update/{id}"); + initEndpoint( + routes, + "snapshot.clone", + false, + "/_snapshot/{repository}/{snapshot}/_clone/{target_snapshot}"); + initEndpoint(routes, "license.get_basic_status", false, "/_license/basic_status"); + initEndpoint(routes, "indices.close", false, "/{index}/_close"); + initEndpoint(routes, "security.saml_authenticate", false, "/_security/saml/authenticate"); + initEndpoint( + routes, "search_application.put", false, "/_application/search_application/{name}"); + initEndpoint(routes, "count", false, "/_count", "/{index}/_count"); + initEndpoint( + routes, + "migration.deprecations", + false, + "/_migration/deprecations", + "/{index}/_migration/deprecations"); + initEndpoint(routes, "indices.segments", false, "/_segments", "/{index}/_segments"); + initEndpoint(routes, "security.suggest_user_profiles", false, "/_security/profile/_suggest"); + initEndpoint(routes, "security.get_user_privileges", false, "/_security/user/_privileges"); + initEndpoint( + routes, + "indices.delete_alias", + false, + "/{index}/_alias/{name}", + "/{index}/_aliases/{name}"); + initEndpoint(routes, "indices.get_mapping", false, "/_mapping", "/{index}/_mapping"); + initEndpoint(routes, "indices.put_index_template", false, "/_index_template/{name}"); + initEndpoint( + routes, + "searchable_snapshots.stats", + false, + "/_searchable_snapshots/stats", + "/{index}/_searchable_snapshots/stats"); + initEndpoint(routes, "security.disable_user", false, "/_security/user/{username}/_disable"); + initEndpoint( + routes, + "ml.upgrade_job_snapshot", + false, + "/_ml/anomaly_detectors/{job_id}/model_snapshots/{snapshot_id}/_upgrade"); + initEndpoint(routes, "delete", false, "/{index}/_doc/{id}"); + initEndpoint(routes, "async_search.delete", false, "/_async_search/{id}"); + initEndpoint( + routes, "cat.transforms", false, "/_cat/transforms", "/_cat/transforms/{transform_id}"); + initEndpoint(routes, "ping", false, "/"); + initEndpoint(routes, "ccr.pause_auto_follow_pattern", false, "/_ccr/auto_follow/{name}/pause"); + initEndpoint(routes, "indices.shard_stores", false, "/_shard_stores", "/{index}/_shard_stores"); + initEndpoint( + routes, "ml.update_data_frame_analytics", false, "/_ml/data_frame/analytics/{id}/_update"); + initEndpoint(routes, "logstash.delete_pipeline", false, "/_logstash/pipeline/{id}"); + initEndpoint(routes, "sql.translate", false, "/_sql/translate"); + initEndpoint(routes, "exists", false, "/{index}/_doc/{id}"); + initEndpoint(routes, "snapshot.get_repository", false, "/_snapshot", "/_snapshot/{repository}"); + initEndpoint(routes, "snapshot.verify_repository", false, "/_snapshot/{repository}/_verify"); + initEndpoint(routes, "indices.put_data_lifecycle", false, "/_data_stream/{name}/_lifecycle"); + initEndpoint(routes, "ml.open_job", false, "/_ml/anomaly_detectors/{job_id}/_open"); + initEndpoint( + routes, "security.update_user_profile_data", false, "/_security/profile/{uid}/_data"); + initEndpoint(routes, "enrich.put_policy", false, "/_enrich/policy/{name}"); + initEndpoint( + routes, + "ml.get_datafeed_stats", + false, + "/_ml/datafeeds/{datafeed_id}/_stats", + "/_ml/datafeeds/_stats"); + initEndpoint(routes, "open_point_in_time", false, "/{index}/_pit"); + initEndpoint(routes, "get_source", false, "/{index}/_source/{id}"); + initEndpoint(routes, "delete_by_query", false, "/{index}/_delete_by_query"); + initEndpoint(routes, "security.create_api_key", false, "/_security/api_key"); + initEndpoint(routes, "cat.tasks", false, "/_cat/tasks"); + initEndpoint(routes, "watcher.delete_watch", false, "/_watcher/watch/{id}"); + initEndpoint(routes, "ingest.processor_grok", false, "/_ingest/processor/grok"); + initEndpoint(routes, "ingest.put_pipeline", false, "/_ingest/pipeline/{id}"); + initEndpoint( + routes, + "ml.get_data_frame_analytics_stats", + false, + "/_ml/data_frame/analytics/_stats", + "/_ml/data_frame/analytics/{id}/_stats"); + initEndpoint( + routes, + "indices.data_streams_stats", + false, + "/_data_stream/_stats", + "/_data_stream/{name}/_stats"); + initEndpoint( + routes, "security.clear_cached_realms", false, "/_security/realm/{realms}/_clear_cache"); + initEndpoint(routes, "field_caps", false, "/_field_caps", "/{index}/_field_caps"); + initEndpoint(routes, "ml.evaluate_data_frame", false, "/_ml/data_frame/_evaluate"); + initEndpoint( + routes, + "ml.delete_forecast", + false, + "/_ml/anomaly_detectors/{job_id}/_forecast", + "/_ml/anomaly_detectors/{job_id}/_forecast/{forecast_id}"); + initEndpoint(routes, "enrich.get_policy", false, "/_enrich/policy/{name}", "/_enrich/policy"); + initEndpoint(routes, "rollup.start_job", false, "/_rollup/job/{id}/_start"); + initEndpoint(routes, "tasks.cancel", false, "/_tasks/_cancel", "/_tasks/{task_id}/_cancel"); + initEndpoint(routes, "security.saml_logout", false, "/_security/saml/logout"); + initEndpoint( + routes, "render_search_template", true, "/_render/template", "/_render/template/{id}"); + initEndpoint(routes, "ml.get_calendar_events", false, "/_ml/calendars/{calendar_id}/events"); + initEndpoint(routes, "security.enable_user_profile", false, "/_security/profile/{uid}/_enable"); + initEndpoint( + routes, "logstash.get_pipeline", false, "/_logstash/pipeline", "/_logstash/pipeline/{id}"); + initEndpoint(routes, "cat.snapshots", false, "/_cat/snapshots", "/_cat/snapshots/{repository}"); + initEndpoint(routes, "indices.add_block", false, "/{index}/_block/{block}"); + initEndpoint(routes, "terms_enum", true, "/{index}/_terms_enum"); + initEndpoint(routes, "ml.forecast", false, "/_ml/anomaly_detectors/{job_id}/_forecast"); + initEndpoint( + routes, "cluster.stats", false, "/_cluster/stats", "/_cluster/stats/nodes/{node_id}"); + initEndpoint(routes, "search_application.list", false, "/_application/search_application"); + initEndpoint(routes, "cat.count", false, "/_cat/count", "/_cat/count/{index}"); + initEndpoint(routes, "cat.segments", false, "/_cat/segments", "/_cat/segments/{index}"); + initEndpoint(routes, "ccr.resume_follow", false, "/{index}/_ccr/resume_follow"); + initEndpoint( + routes, "search_application.get", false, "/_application/search_application/{name}"); + initEndpoint( + routes, + "security.saml_service_provider_metadata", + false, + "/_security/saml/metadata/{realm_name}"); + initEndpoint(routes, "update_by_query", false, "/{index}/_update_by_query"); + initEndpoint(routes, "ml.stop_datafeed", false, "/_ml/datafeeds/{datafeed_id}/_stop"); + initEndpoint(routes, "ilm.explain_lifecycle", false, "/{index}/_ilm/explain"); + initEndpoint( + routes, + "ml.put_trained_model_vocabulary", + false, + "/_ml/trained_models/{model_id}/vocabulary"); + initEndpoint(routes, "indices.exists", false, "/{index}"); + initEndpoint(routes, "ml.set_upgrade_mode", false, "/_ml/set_upgrade_mode"); + initEndpoint(routes, "security.saml_invalidate", false, "/_security/saml/invalidate"); + initEndpoint( + routes, + "ml.get_job_stats", + false, + "/_ml/anomaly_detectors/_stats", + "/_ml/anomaly_detectors/{job_id}/_stats"); + initEndpoint(routes, "cluster.allocation_explain", false, "/_cluster/allocation/explain"); + initEndpoint(routes, "watcher.activate_watch", false, "/_watcher/watch/{watch_id}/_activate"); + initEndpoint( + routes, + "searchable_snapshots.clear_cache", + false, + "/_searchable_snapshots/cache/clear", + "/{index}/_searchable_snapshots/cache/clear"); + initEndpoint( + routes, "msearch_template", true, "/_msearch/template", "/{index}/_msearch/template"); + initEndpoint(routes, "bulk", false, "/_bulk", "/{index}/_bulk"); + initEndpoint(routes, "cat.nodeattrs", false, "/_cat/nodeattrs"); + initEndpoint( + routes, "indices.get_index_template", false, "/_index_template", "/_index_template/{name}"); + initEndpoint(routes, "license.get", false, "/_license"); + initEndpoint(routes, "ccr.forget_follower", false, "/{index}/_ccr/forget_follower"); + initEndpoint(routes, "security.delete_role", false, "/_security/role/{name}"); + initEndpoint( + routes, "indices.validate_query", false, "/_validate/query", "/{index}/_validate/query"); + initEndpoint(routes, "tasks.get", false, "/_tasks/{task_id}"); + initEndpoint( + routes, "ml.start_data_frame_analytics", false, "/_ml/data_frame/analytics/{id}/_start"); + initEndpoint(routes, "indices.create", false, "/{index}"); + initEndpoint( + routes, + "cluster.delete_voting_config_exclusions", + false, + "/_cluster/voting_config_exclusions"); + initEndpoint(routes, "info", false, "/"); + initEndpoint(routes, "watcher.stop", false, "/_watcher/_stop"); + initEndpoint(routes, "enrich.delete_policy", false, "/_enrich/policy/{name}"); + initEndpoint( + routes, + "cat.ml_data_frame_analytics", + false, + "/_cat/ml/data_frame/analytics", + "/_cat/ml/data_frame/analytics/{id}"); + initEndpoint( + routes, + "security.change_password", + false, + "/_security/user/{username}/_password", + "/_security/user/_password"); + initEndpoint(routes, "put_script", false, "/_scripts/{id}", "/_scripts/{id}/{context}"); + initEndpoint(routes, "ml.put_datafeed", false, "/_ml/datafeeds/{datafeed_id}"); + initEndpoint(routes, "cat.master", false, "/_cat/master"); + initEndpoint(routes, "features.reset_features", false, "/_features/_reset"); + initEndpoint(routes, "indices.get_data_lifecycle", false, "/_data_stream/{name}/_lifecycle"); + initEndpoint( + routes, + "ml.get_data_frame_analytics", + false, + "/_ml/data_frame/analytics/{id}", + "/_ml/data_frame/analytics"); + initEndpoint( + routes, + "security.delete_service_token", + false, + "/_security/service/{namespace}/{service}/credential/token/{name}"); + initEndpoint(routes, "indices.recovery", false, "/_recovery", "/{index}/_recovery"); + initEndpoint(routes, "cat.recovery", false, "/_cat/recovery", "/_cat/recovery/{index}"); + initEndpoint(routes, "indices.downsample", false, "/{index}/_downsample/{target_index}"); + initEndpoint(routes, "ingest.delete_pipeline", false, "/_ingest/pipeline/{id}"); + initEndpoint(routes, "async_search.get", false, "/_async_search/{id}"); + initEndpoint(routes, "eql.get", false, "/_eql/search/{id}"); + initEndpoint(routes, "cat.aliases", false, "/_cat/aliases", "/_cat/aliases/{name}"); + initEndpoint( + routes, + "security.get_service_credentials", + false, + "/_security/service/{namespace}/{service}/credential"); + initEndpoint(routes, "cat.allocation", false, "/_cat/allocation", "/_cat/allocation/{node_id}"); + initEndpoint( + routes, "ml.stop_data_frame_analytics", false, "/_ml/data_frame/analytics/{id}/_stop"); + initEndpoint(routes, "indices.open", false, "/{index}/_open"); + initEndpoint(routes, "ilm.get_lifecycle", false, "/_ilm/policy/{policy}", "/_ilm/policy"); + initEndpoint(routes, "ilm.remove_policy", false, "/{index}/_ilm/remove"); + initEndpoint( + routes, + "security.get_role_mapping", + false, + "/_security/role_mapping/{name}", + "/_security/role_mapping"); + initEndpoint(routes, "snapshot.create", false, "/_snapshot/{repository}/{snapshot}"); + initEndpoint(routes, "watcher.get_watch", false, "/_watcher/watch/{id}"); + initEndpoint(routes, "license.post_start_trial", false, "/_license/start_trial"); + initEndpoint(routes, "snapshot.restore", false, "/_snapshot/{repository}/{snapshot}/_restore"); + initEndpoint(routes, "indices.put_mapping", false, "/{index}/_mapping"); + initEndpoint( + routes, "ml.delete_calendar_job", false, "/_ml/calendars/{calendar_id}/jobs/{job_id}"); + initEndpoint( + routes, "security.clear_api_key_cache", false, "/_security/api_key/{ids}/_clear_cache"); + initEndpoint(routes, "slm.start", false, "/_slm/start"); + initEndpoint( + routes, + "cat.component_templates", + false, + "/_cat/component_templates", + "/_cat/component_templates/{name}"); + initEndpoint(routes, "security.enable_user", false, "/_security/user/{username}/_enable"); + initEndpoint(routes, "cluster.delete_component_template", false, "/_component_template/{name}"); + initEndpoint(routes, "security.get_role", false, "/_security/role/{name}", "/_security/role"); + initEndpoint( + routes, "ingest.get_pipeline", false, "/_ingest/pipeline", "/_ingest/pipeline/{id}"); + initEndpoint( + routes, + "ml.delete_expired_data", + false, + "/_ml/_delete_expired_data/{job_id}", + "/_ml/_delete_expired_data"); + initEndpoint( + routes, + "indices.get_settings", + false, + "/_settings", + "/{index}/_settings", + "/{index}/_settings/{name}", + "/_settings/{name}"); + initEndpoint(routes, "ccr.follow", false, "/{index}/_ccr/follow"); + initEndpoint( + routes, "termvectors", false, "/{index}/_termvectors/{id}", "/{index}/_termvectors"); + initEndpoint(routes, "ml.post_data", false, "/_ml/anomaly_detectors/{job_id}/_data"); + initEndpoint(routes, "eql.search", true, "/{index}/_eql/search"); + initEndpoint( + routes, + "ml.get_trained_models", + false, + "/_ml/trained_models/{model_id}", + "/_ml/trained_models"); + initEndpoint( + routes, "security.disable_user_profile", false, "/_security/profile/{uid}/_disable"); + initEndpoint(routes, "security.put_privileges", false, "/_security/privilege"); + initEndpoint(routes, "cat.nodes", false, "/_cat/nodes"); + initEndpoint( + routes, "nodes.info", false, "/_nodes", "/_nodes/{node_id}", "/_nodes/{node_id}/{metric}"); + initEndpoint(routes, "graph.explore", false, "/{index}/_graph/explore"); + initEndpoint( + routes, "autoscaling.put_autoscaling_policy", false, "/_autoscaling/policy/{name}"); + initEndpoint(routes, "cat.templates", false, "/_cat/templates", "/_cat/templates/{name}"); + initEndpoint(routes, "cluster.remote_info", false, "/_remote/info"); + initEndpoint(routes, "rank_eval", false, "/_rank_eval", "/{index}/_rank_eval"); + initEndpoint( + routes, "security.delete_privileges", false, "/_security/privilege/{application}/{name}"); + initEndpoint( + routes, + "security.get_privileges", + false, + "/_security/privilege", + "/_security/privilege/{application}", + "/_security/privilege/{application}/{name}"); + initEndpoint(routes, "scroll", false, "/_search/scroll"); + initEndpoint(routes, "license.delete", false, "/_license"); + initEndpoint(routes, "indices.disk_usage", false, "/{index}/_disk_usage"); + initEndpoint(routes, "msearch", true, "/_msearch", "/{index}/_msearch"); + initEndpoint(routes, "indices.field_usage_stats", false, "/{index}/_field_usage_stats"); + initEndpoint( + routes, "indices.rollover", false, "/{alias}/_rollover", "/{alias}/_rollover/{new_index}"); + initEndpoint( + routes, + "cat.ml_trained_models", + false, + "/_cat/ml/trained_models", + "/_cat/ml/trained_models/{model_id}"); + initEndpoint( + routes, + "ml.delete_trained_model_alias", + false, + "/_ml/trained_models/{model_id}/model_aliases/{model_alias}"); + initEndpoint(routes, "indices.get", false, "/{index}"); + initEndpoint(routes, "sql.get_async_status", false, "/_sql/async/status/{id}"); + initEndpoint(routes, "ilm.stop", false, "/_ilm/stop"); + initEndpoint(routes, "security.put_user", false, "/_security/user/{username}"); + initEndpoint( + routes, + "cluster.state", + false, + "/_cluster/state", + "/_cluster/state/{metric}", + "/_cluster/state/{metric}/{index}"); + initEndpoint(routes, "indices.put_settings", false, "/_settings", "/{index}/_settings"); + initEndpoint(routes, "knn_search", false, "/{index}/_knn_search"); + initEndpoint(routes, "get", false, "/{index}/_doc/{id}"); + initEndpoint(routes, "eql.get_status", false, "/_eql/search/status/{id}"); + initEndpoint(routes, "ssl.certificates", false, "/_ssl/certificates"); + initEndpoint( + routes, + "ml.get_model_snapshots", + false, + "/_ml/anomaly_detectors/{job_id}/model_snapshots/{snapshot_id}", + "/_ml/anomaly_detectors/{job_id}/model_snapshots"); + initEndpoint( + routes, + "nodes.clear_repositories_metering_archive", + false, + "/_nodes/{node_id}/_repositories_metering/{max_archive_version}"); + initEndpoint(routes, "security.put_role", false, "/_security/role/{name}"); + initEndpoint( + routes, "ml.get_influencers", false, "/_ml/anomaly_detectors/{job_id}/results/influencers"); + initEndpoint(routes, "transform.upgrade_transforms", false, "/_transform/_upgrade"); + initEndpoint( + routes, + "ml.delete_calendar_event", + false, + "/_ml/calendars/{calendar_id}/events/{event_id}"); + initEndpoint( + routes, + "indices.get_field_mapping", + false, + "/_mapping/field/{fields}", + "/{index}/_mapping/field/{fields}"); + initEndpoint( + routes, + "transform.preview_transform", + false, + "/_transform/{transform_id}/_preview", + "/_transform/_preview"); + initEndpoint(routes, "tasks.list", false, "/_tasks"); + initEndpoint( + routes, + "ml.clear_trained_model_deployment_cache", + false, + "/_ml/trained_models/{model_id}/deployment/cache/_clear"); + initEndpoint(routes, "cluster.reroute", false, "/_cluster/reroute"); + initEndpoint(routes, "security.saml_complete_logout", false, "/_security/saml/complete_logout"); + initEndpoint( + routes, + "indices.simulate_index_template", + false, + "/_index_template/_simulate_index/{name}"); + initEndpoint(routes, "snapshot.get", false, "/_snapshot/{repository}/{snapshot}"); + initEndpoint(routes, "ccr.put_auto_follow_pattern", false, "/_ccr/auto_follow/{name}"); + initEndpoint( + routes, "nodes.hot_threads", false, "/_nodes/hot_threads", "/_nodes/{node_id}/hot_threads"); + initEndpoint( + routes, + "ml.preview_data_frame_analytics", + false, + "/_ml/data_frame/analytics/_preview", + "/_ml/data_frame/analytics/{id}/_preview"); + initEndpoint(routes, "indices.flush", false, "/_flush", "/{index}/_flush"); + initEndpoint(routes, "cluster.exists_component_template", false, "/_component_template/{name}"); + initEndpoint( + routes, + "snapshot.status", + false, + "/_snapshot/_status", + "/_snapshot/{repository}/_status", + "/_snapshot/{repository}/{snapshot}/_status"); + initEndpoint(routes, "ml.update_datafeed", false, "/_ml/datafeeds/{datafeed_id}/_update"); + initEndpoint(routes, "indices.update_aliases", false, "/_aliases"); + initEndpoint(routes, "autoscaling.get_autoscaling_capacity", false, "/_autoscaling/capacity"); + initEndpoint(routes, "migration.post_feature_upgrade", false, "/_migration/system_features"); + initEndpoint( + routes, "ml.get_records", false, "/_ml/anomaly_detectors/{job_id}/results/records"); + initEndpoint( + routes, + "indices.get_alias", + false, + "/_alias", + "/_alias/{name}", + "/{index}/_alias/{name}", + "/{index}/_alias"); + initEndpoint(routes, "logstash.put_pipeline", false, "/_logstash/pipeline/{id}"); + initEndpoint(routes, "snapshot.delete_repository", false, "/_snapshot/{repository}"); + initEndpoint( + routes, + "security.has_privileges", + false, + "/_security/user/_has_privileges", + "/_security/user/{user}/_has_privileges"); + initEndpoint(routes, "cat.indices", false, "/_cat/indices", "/_cat/indices/{index}"); + initEndpoint( + routes, + "ccr.get_auto_follow_pattern", + false, + "/_ccr/auto_follow", + "/_ccr/auto_follow/{name}"); + initEndpoint(routes, "ml.start_datafeed", false, "/_ml/datafeeds/{datafeed_id}/_start"); + initEndpoint(routes, "indices.clone", false, "/{index}/_clone/{target}"); + initEndpoint( + routes, "search_application.delete", false, "/_application/search_application/{name}"); + initEndpoint(routes, "security.query_api_keys", false, "/_security/_query/api_key"); + initEndpoint(routes, "ml.flush_job", false, "/_ml/anomaly_detectors/{job_id}/_flush"); + initEndpoint( + routes, + "security.clear_cached_privileges", + false, + "/_security/privilege/{application}/_clear_cache"); + initEndpoint(routes, "indices.exists_index_template", false, "/_index_template/{name}"); + initEndpoint(routes, "indices.explain_data_lifecycle", false, "/{index}/_lifecycle/explain"); + initEndpoint( + routes, "indices.put_alias", false, "/{index}/_alias/{name}", "/{index}/_aliases/{name}"); + initEndpoint( + routes, + "ml.get_buckets", + false, + "/_ml/anomaly_detectors/{job_id}/results/buckets/{timestamp}", + "/_ml/anomaly_detectors/{job_id}/results/buckets"); + initEndpoint( + routes, + "ml.put_trained_model_definition_part", + false, + "/_ml/trained_models/{model_id}/definition/{part}"); + initEndpoint(routes, "get_script", false, "/_scripts/{id}"); + initEndpoint( + routes, + "ingest.simulate", + false, + "/_ingest/pipeline/_simulate", + "/_ingest/pipeline/{id}/_simulate"); + initEndpoint(routes, "indices.migrate_to_data_stream", false, "/_data_stream/_migrate/{name}"); + initEndpoint(routes, "enrich.execute_policy", false, "/_enrich/policy/{name}/_execute"); + initEndpoint(routes, "indices.split", false, "/{index}/_split/{target}"); + initEndpoint( + routes, + "ml.delete_model_snapshot", + false, + "/_ml/anomaly_detectors/{job_id}/model_snapshots/{snapshot_id}"); + initEndpoint( + routes, + "nodes.usage", + false, + "/_nodes/usage", + "/_nodes/{node_id}/usage", + "/_nodes/usage/{metric}", + "/_nodes/{node_id}/usage/{metric}"); + initEndpoint(routes, "cat.help", false, "/_cat"); + initEndpoint( + routes, "ml.estimate_model_memory", false, "/_ml/anomaly_detectors/_estimate_model_memory"); + initEndpoint(routes, "exists_source", false, "/{index}/_source/{id}"); + initEndpoint(routes, "ml.put_data_frame_analytics", false, "/_ml/data_frame/analytics/{id}"); + initEndpoint(routes, "security.put_role_mapping", false, "/_security/role_mapping/{name}"); + initEndpoint(routes, "rollup.get_rollup_index_caps", false, "/{index}/_rollup/data"); + initEndpoint(routes, "transform.reset_transform", false, "/_transform/{transform_id}/_reset"); + initEndpoint( + routes, + "ml.infer_trained_model", + false, + "/_ml/trained_models/{model_id}/_infer", + "/_ml/trained_models/{model_id}/deployment/_infer"); + initEndpoint(routes, "reindex", false, "/_reindex"); + initEndpoint(routes, "ml.put_trained_model", false, "/_ml/trained_models/{model_id}"); + initEndpoint( + routes, + "cat.ml_jobs", + false, + "/_cat/ml/anomaly_detectors", + "/_cat/ml/anomaly_detectors/{job_id}"); + initEndpoint( + routes, + "search_application.search", + false, + "/_application/search_application/{name}/_search"); + initEndpoint(routes, "ilm.put_lifecycle", false, "/_ilm/policy/{policy}"); + initEndpoint(routes, "security.get_token", false, "/_security/oauth2/token"); + initEndpoint(routes, "ilm.move_to_step", false, "/_ilm/move/{index}"); + initEndpoint(routes, "search_template", true, "/_search/template", "/{index}/_search/template"); + initEndpoint(routes, "indices.delete_data_lifecycle", false, "/_data_stream/{name}/_lifecycle"); + initEndpoint(routes, "indices.get_data_stream", false, "/_data_stream", "/_data_stream/{name}"); + initEndpoint(routes, "ml.get_filters", false, "/_ml/filters", "/_ml/filters/{filter_id}"); + initEndpoint( + routes, + "cat.ml_datafeeds", + false, + "/_cat/ml/datafeeds", + "/_cat/ml/datafeeds/{datafeed_id}"); + initEndpoint(routes, "rollup.rollup_search", false, "/{index}/_rollup_search"); + initEndpoint(routes, "ml.put_job", false, "/_ml/anomaly_detectors/{job_id}"); + initEndpoint( + routes, "update_by_query_rethrottle", false, "/_update_by_query/{task_id}/_rethrottle"); + initEndpoint(routes, "indices.delete_index_template", false, "/_index_template/{name}"); + initEndpoint( + routes, "indices.reload_search_analyzers", false, "/{index}/_reload_search_analyzers"); + initEndpoint(routes, "cluster.get_settings", false, "/_cluster/settings"); + initEndpoint(routes, "cluster.put_settings", false, "/_cluster/settings"); + initEndpoint(routes, "transform.put_transform", false, "/_transform/{transform_id}"); + initEndpoint(routes, "watcher.stats", false, "/_watcher/stats", "/_watcher/stats/{metric}"); + initEndpoint(routes, "ccr.delete_auto_follow_pattern", false, "/_ccr/auto_follow/{name}"); + initEndpoint(routes, "mtermvectors", false, "/_mtermvectors", "/{index}/_mtermvectors"); + initEndpoint(routes, "license.post", false, "/_license"); + initEndpoint(routes, "xpack.info", false, "/_xpack"); + initEndpoint( + routes, "dangling_indices.import_dangling_index", false, "/_dangling/{index_uuid}"); + initEndpoint( + routes, + "nodes.get_repositories_metering_info", + false, + "/_nodes/{node_id}/_repositories_metering"); + initEndpoint( + routes, "transform.get_transform_stats", false, "/_transform/{transform_id}/_stats"); + initEndpoint(routes, "mget", false, "/_mget", "/{index}/_mget"); + initEndpoint(routes, "security.get_builtin_privileges", false, "/_security/privilege/_builtin"); + initEndpoint( + routes, + "ml.update_model_snapshot", + false, + "/_ml/anomaly_detectors/{job_id}/model_snapshots/{snapshot_id}/_update"); + initEndpoint(routes, "ml.info", false, "/_ml/info"); + initEndpoint(routes, "indices.exists_template", false, "/_template/{name}"); + initEndpoint( + routes, + "watcher.ack_watch", + false, + "/_watcher/watch/{watch_id}/_ack", + "/_watcher/watch/{watch_id}/_ack/{action_id}"); + initEndpoint( + routes, "security.get_user", false, "/_security/user/{username}", "/_security/user"); + initEndpoint( + routes, "shutdown.get_node", false, "/_nodes/shutdown", "/_nodes/{node_id}/shutdown"); + initEndpoint(routes, "watcher.start", false, "/_watcher/_start"); + initEndpoint(routes, "indices.shrink", false, "/{index}/_shrink/{target}"); + initEndpoint(routes, "license.post_start_basic", false, "/_license/start_basic"); + initEndpoint(routes, "xpack.usage", false, "/_xpack/usage"); + initEndpoint(routes, "ilm.delete_lifecycle", false, "/_ilm/policy/{policy}"); + initEndpoint(routes, "ccr.follow_info", false, "/{index}/_ccr/info"); + initEndpoint( + routes, "ml.put_calendar_job", false, "/_ml/calendars/{calendar_id}/jobs/{job_id}"); + initEndpoint(routes, "rollup.put_job", false, "/_rollup/job/{id}"); + initEndpoint(routes, "clear_scroll", false, "/_search/scroll"); + initEndpoint(routes, "ml.delete_data_frame_analytics", false, "/_ml/data_frame/analytics/{id}"); + initEndpoint(routes, "security.get_api_key", false, "/_security/api_key"); + initEndpoint(routes, "cat.health", false, "/_cat/health"); + initEndpoint(routes, "security.invalidate_token", false, "/_security/oauth2/token"); + initEndpoint(routes, "slm.delete_lifecycle", false, "/_slm/policy/{policy_id}"); + initEndpoint( + routes, + "ml.stop_trained_model_deployment", + false, + "/_ml/trained_models/{model_id}/deployment/_stop"); + initEndpoint(routes, "monitoring.bulk", false, "/_monitoring/bulk", "/_monitoring/{type}/bulk"); + initEndpoint( + routes, + "indices.stats", + false, + "/_stats", + "/_stats/{metric}", + "/{index}/_stats", + "/{index}/_stats/{metric}"); + initEndpoint( + routes, + "searchable_snapshots.cache_stats", + false, + "/_searchable_snapshots/cache/stats", + "/_searchable_snapshots/{node_id}/cache/stats"); + initEndpoint(routes, "async_search.submit", true, "/_async_search", "/{index}/_async_search"); + initEndpoint(routes, "rollup.get_jobs", false, "/_rollup/job/{id}", "/_rollup/job"); + initEndpoint( + routes, + "ml.revert_model_snapshot", + false, + "/_ml/anomaly_detectors/{job_id}/model_snapshots/{snapshot_id}/_revert"); + initEndpoint(routes, "transform.delete_transform", false, "/_transform/{transform_id}"); + initEndpoint(routes, "cluster.pending_tasks", false, "/_cluster/pending_tasks"); + initEndpoint( + routes, + "ml.get_model_snapshot_upgrade_stats", + false, + "/_ml/anomaly_detectors/{job_id}/model_snapshots/{snapshot_id}/_upgrade/_stats"); + initEndpoint( + routes, + "ml.get_categories", + false, + "/_ml/anomaly_detectors/{job_id}/results/categories/{category_id}", + "/_ml/anomaly_detectors/{job_id}/results/categories"); + initEndpoint(routes, "ccr.pause_follow", false, "/{index}/_ccr/pause_follow"); + initEndpoint(routes, "security.authenticate", false, "/_security/_authenticate"); + initEndpoint(routes, "enrich.stats", false, "/_enrich/_stats"); + initEndpoint( + routes, + "ml.put_trained_model_alias", + false, + "/_ml/trained_models/{model_id}/model_aliases/{model_alias}"); + initEndpoint( + routes, + "ml.get_overall_buckets", + false, + "/_ml/anomaly_detectors/{job_id}/results/overall_buckets"); + initEndpoint(routes, "indices.get_template", false, "/_template", "/_template/{name}"); + initEndpoint(routes, "security.delete_role_mapping", false, "/_security/role_mapping/{name}"); + initEndpoint( + routes, "ml.get_datafeeds", false, "/_ml/datafeeds/{datafeed_id}", "/_ml/datafeeds"); + initEndpoint(routes, "slm.execute_lifecycle", false, "/_slm/policy/{policy_id}/_execute"); + initEndpoint(routes, "close_point_in_time", false, "/_pit"); + initEndpoint(routes, "snapshot.cleanup_repository", false, "/_snapshot/{repository}/_cleanup"); + initEndpoint( + routes, "autoscaling.get_autoscaling_policy", false, "/_autoscaling/policy/{name}"); + initEndpoint(routes, "slm.put_lifecycle", false, "/_slm/policy/{policy_id}"); + initEndpoint( + routes, "ml.get_jobs", false, "/_ml/anomaly_detectors/{job_id}", "/_ml/anomaly_detectors"); + initEndpoint( + routes, + "ml.get_trained_models_stats", + false, + "/_ml/trained_models/{model_id}/_stats", + "/_ml/trained_models/_stats"); + initEndpoint( + routes, "ml.validate_detector", false, "/_ml/anomaly_detectors/_validate/detector"); + initEndpoint(routes, "watcher.put_watch", false, "/_watcher/watch/{id}"); + initEndpoint(routes, "transform.update_transform", false, "/_transform/{transform_id}/_update"); + initEndpoint(routes, "ml.post_calendar_events", false, "/_ml/calendars/{calendar_id}/events"); + initEndpoint( + routes, "migration.get_feature_upgrade_status", false, "/_migration/system_features"); + initEndpoint(routes, "get_script_context", false, "/_script_context"); + initEndpoint(routes, "ml.put_filter", false, "/_ml/filters/{filter_id}"); + initEndpoint(routes, "ml.update_job", false, "/_ml/anomaly_detectors/{job_id}/_update"); + initEndpoint(routes, "ingest.geo_ip_stats", false, "/_ingest/geoip/stats"); + initEndpoint(routes, "security.delete_user", false, "/_security/user/{username}"); + initEndpoint(routes, "indices.unfreeze", false, "/{index}/_unfreeze"); + initEndpoint(routes, "snapshot.create_repository", false, "/_snapshot/{repository}"); + initEndpoint( + routes, + "cluster.get_component_template", + false, + "/_component_template", + "/_component_template/{name}"); + initEndpoint(routes, "ilm.migrate_to_data_tiers", false, "/_ilm/migrate_to_data_tiers"); + initEndpoint(routes, "indices.refresh", false, "/_refresh", "/{index}/_refresh"); + initEndpoint( + routes, "ml.get_calendars", false, "/_ml/calendars", "/_ml/calendars/{calendar_id}"); + initEndpoint( + routes, "watcher.deactivate_watch", false, "/_watcher/watch/{watch_id}/_deactivate"); + initEndpoint(routes, "cluster.health", false, "/_cluster/health", "/_cluster/health/{index}"); + initEndpoint( + routes, "dangling_indices.delete_dangling_index", false, "/_dangling/{index_uuid}"); + initEndpoint(routes, "health_report", false, "/_health_report", "/_health_report/{feature}"); + initEndpoint(routes, "watcher.query_watches", false, "/_watcher/_query/watches"); + initEndpoint(routes, "ccr.unfollow", false, "/{index}/_ccr/unfollow"); + initEndpoint(routes, "ml.validate", false, "/_ml/anomaly_detectors/_validate"); + initEndpoint(routes, "cat.plugins", false, "/_cat/plugins"); + initEndpoint( + routes, + "watcher.execute_watch", + false, + "/_watcher/watch/{id}/_execute", + "/_watcher/watch/_execute"); + initEndpoint(routes, "search_shards", false, "/_search_shards", "/{index}/_search_shards"); + initEndpoint(routes, "cat.shards", false, "/_cat/shards", "/_cat/shards/{index}"); + initEndpoint(routes, "ml.delete_job", false, "/_ml/anomaly_detectors/{job_id}"); + initEndpoint(routes, "ilm.start", false, "/_ilm/start"); + initEndpoint(routes, "security.get_user_profile", false, "/_security/profile/{uid}"); + initEndpoint(routes, "indices.modify_data_stream", false, "/_data_stream/_modify"); + initEndpoint(routes, "indices.exists_alias", false, "/_alias/{name}", "/{index}/_alias/{name}"); + initEndpoint(routes, "rollup.stop_job", false, "/_rollup/job/{id}/_stop"); + initEndpoint(routes, "dangling_indices.list_dangling_indices", false, "/_dangling"); + initEndpoint(routes, "snapshot.delete", false, "/_snapshot/{repository}/{snapshot}"); + initEndpoint(routes, "security.activate_user_profile", false, "/_security/profile/_activate"); + initEndpoint( + routes, + "ml.start_trained_model_deployment", + false, + "/_ml/trained_models/{model_id}/deployment/_start"); + initEndpoint(routes, "transform.start_transform", false, "/_transform/{transform_id}/_start"); + initEndpoint(routes, "cat.repositories", false, "/_cat/repositories"); + initEndpoint(routes, "ilm.get_status", false, "/_ilm/status"); + initEndpoint(routes, "shutdown.delete_node", false, "/_nodes/{node_id}/shutdown"); + initEndpoint( + routes, + "nodes.stats", + false, + "/_nodes/stats", + "/_nodes/{node_id}/stats", + "/_nodes/stats/{metric}", + "/_nodes/{node_id}/stats/{metric}", + "/_nodes/stats/{metric}/{index_metric}", + "/_nodes/{node_id}/stats/{metric}/{index_metric}"); + initEndpoint(routes, "get_script_languages", false, "/_script_language"); + initEndpoint(routes, "slm.execute_retention", false, "/_slm/_execute_retention"); + initEndpoint( + routes, + "security.get_service_accounts", + false, + "/_security/service/{namespace}/{service}", + "/_security/service/{namespace}", + "/_security/service"); + initEndpoint(routes, "shutdown.put_node", false, "/_nodes/{node_id}/shutdown"); + initEndpoint(routes, "indices.resolve_index", false, "/_resolve/index/{name}"); + initEndpoint(routes, "search", true, "/_search", "/{index}/_search"); + initEndpoint(routes, "sql.get_async", false, "/_sql/async/{id}"); + initEndpoint( + routes, "delete_by_query_rethrottle", false, "/_delete_by_query/{task_id}/_rethrottle"); + initEndpoint( + routes, "transform.get_transform", false, "/_transform/{transform_id}", "/_transform"); + initEndpoint(routes, "security.invalidate_api_key", false, "/_security/api_key"); + initEndpoint(routes, "security.saml_prepare_authentication", false, "/_security/saml/prepare"); + initEndpoint( + routes, "ml.get_memory_stats", false, "/_ml/memory/_stats", "/_ml/memory/{node_id}/_stats"); + initEndpoint(routes, "ccr.stats", false, "/_ccr/stats"); + initEndpoint(routes, "indices.forcemerge", false, "/_forcemerge", "/{index}/_forcemerge"); + initEndpoint(routes, "indices.delete_template", false, "/_template/{name}"); + initEndpoint(routes, "sql.delete_async", false, "/_sql/async/delete/{id}"); + initEndpoint(routes, "security.update_api_key", false, "/_security/api_key/{id}"); + initEndpoint( + routes, + "security.create_service_token", + false, + "/_security/service/{namespace}/{service}/credential/token/{name}", + "/_security/service/{namespace}/{service}/credential/token"); + initEndpoint(routes, "license.get_trial_status", false, "/_license/trial_status"); + initEndpoint( + routes, "searchable_snapshots.mount", false, "/_snapshot/{repository}/{snapshot}/_mount"); + initEndpoint(routes, "security.grant_api_key", false, "/_security/api_key/grant"); + initEndpoint(routes, "ilm.retry", false, "/{index}/_ilm/retry"); + initEndpoint(routes, "ml.reset_job", false, "/_ml/anomaly_detectors/{job_id}/_reset"); + initEndpoint(routes, "ml.close_job", false, "/_ml/anomaly_detectors/{job_id}/_close"); + initEndpoint( + routes, + "ml.explain_data_frame_analytics", + false, + "/_ml/data_frame/analytics/_explain", + "/_ml/data_frame/analytics/{id}/_explain"); + initEndpoint( + routes, + "security.clear_cached_service_tokens", + false, + "/_security/service/{namespace}/{service}/credential/token/{name}/_clear_cache"); + initEndpoint(routes, "search_mvt", false, "/{index}/_mvt/{field}/{zoom}/{x}/{y}"); + routesMap = Collections.unmodifiableMap(routes); + } + + private ElasticsearchEndpointMap() {} + + private static void initEndpoint( + Map map, + String endpointId, + boolean isSearchEndpoint, + String... routes) { + ElasticsearchEndpointDefinition endpointDef = + new ElasticsearchEndpointDefinition(endpointId, routes, isSearchEndpoint); + map.put(endpointId, endpointDef); + } + + @Nullable + public static ElasticsearchEndpointDefinition get(String endpointId) { + return routesMap.get(endpointId); + } + + public static Collection getAllEndpoints() { + return routesMap.values(); + } +} diff --git a/instrumentation/elasticsearch/elasticsearch-api-client-7.16/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/apiclient/EndpointId.java b/instrumentation/elasticsearch/elasticsearch-api-client-7.16/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/apiclient/EndpointId.java new file mode 100644 index 000000000000..96f1468067cf --- /dev/null +++ b/instrumentation/elasticsearch/elasticsearch-api-client-7.16/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/apiclient/EndpointId.java @@ -0,0 +1,28 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.elasticsearch.apiclient; + +import static io.opentelemetry.context.ContextKey.named; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.ContextKey; +import javax.annotation.Nullable; + +public final class EndpointId { + + private static final ContextKey KEY = named("elasticsearch-api-client-endpoint-id"); + + public static Context storeInContext(Context context, String endpointId) { + return context.with(KEY, endpointId); + } + + @Nullable + public static String get(Context context) { + return context.get(KEY); + } + + private EndpointId() {} +} diff --git a/instrumentation/elasticsearch/elasticsearch-api-client-7.16/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/apiclient/RestClientHttpClientInstrumentation.java b/instrumentation/elasticsearch/elasticsearch-api-client-7.16/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/apiclient/RestClientHttpClientInstrumentation.java new file mode 100644 index 000000000000..f299553e4426 --- /dev/null +++ b/instrumentation/elasticsearch/elasticsearch-api-client-7.16/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/apiclient/RestClientHttpClientInstrumentation.java @@ -0,0 +1,79 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.elasticsearch.apiclient; + +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 io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.instrumentation.api.util.VirtualField; +import io.opentelemetry.instrumentation.elasticsearch.rest.internal.ElasticsearchEndpointDefinition; +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.client.Request; + +// starting from 8.9 +public class RestClientHttpClientInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("co.elastic.clients.transport.rest_client.RestClientHttpClient"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod() + .and(namedOneOf("performRequest", "performRequestAsync")) + .and(takesArgument(0, String.class)), + this.getClass().getName() + "$PerformRequestAdvice"); + transformer.applyAdviceToMethod( + isMethod() + .and(named("createRestRequest")) + .and(returns(named("org.elasticsearch.client.Request"))), + this.getClass().getName() + "$CreateRestRequestAdvice"); + } + + @SuppressWarnings("unused") + public static class PerformRequestAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static Scope onEnter(@Advice.Argument(0) String endpointId) { + return EndpointId.storeInContext(Context.current(), endpointId).makeCurrent(); + } + + @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class) + public static void onExit(@Advice.Enter Scope scope) { + if (scope != null) { + scope.close(); + } + } + } + + @SuppressWarnings("unused") + public static class CreateRestRequestAdvice { + + @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class) + public static void onExit(@Advice.Return Request request) { + String endpointId = EndpointId.get(Context.current()); + if (endpointId == null) { + return; + } + if (endpointId.startsWith("es/") && endpointId.length() > 3) { + endpointId = endpointId.substring(3); + } + VirtualField.find(Request.class, ElasticsearchEndpointDefinition.class) + .set(request, ElasticsearchEndpointMap.get(endpointId)); + } + } +} diff --git a/instrumentation/elasticsearch/elasticsearch-api-client-7.16/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/apiclient/RestClientTransportInstrumentation.java b/instrumentation/elasticsearch/elasticsearch-api-client-7.16/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/apiclient/RestClientTransportInstrumentation.java new file mode 100644 index 000000000000..15d449d33364 --- /dev/null +++ b/instrumentation/elasticsearch/elasticsearch-api-client-7.16/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/apiclient/RestClientTransportInstrumentation.java @@ -0,0 +1,56 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.elasticsearch.apiclient; + +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.returns; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import co.elastic.clients.transport.Endpoint; +import io.opentelemetry.instrumentation.api.util.VirtualField; +import io.opentelemetry.instrumentation.elasticsearch.rest.internal.ElasticsearchEndpointDefinition; +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.client.Request; + +// up to 8.8 (included) +public class RestClientTransportInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("co.elastic.clients.transport.rest_client.RestClientTransport"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod() + .and(named("prepareLowLevelRequest")) + .and(takesArgument(1, named("co.elastic.clients.transport.Endpoint"))) + .and(returns(named("org.elasticsearch.client.Request"))), + this.getClass().getName() + "$RestClientTransportAdvice"); + } + + @SuppressWarnings("unused") + public static class RestClientTransportAdvice { + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void onPrepareLowLevelRequest( + @Advice.Argument(1) Endpoint endpoint, @Advice.Return Request request) { + VirtualField virtualField = + VirtualField.find(Request.class, ElasticsearchEndpointDefinition.class); + String endpointId = endpoint.id(); + if (endpointId.startsWith("es/") && endpointId.length() > 3) { + endpointId = endpointId.substring(3); + } + virtualField.set(request, ElasticsearchEndpointMap.get(endpointId)); + } + } +} diff --git a/instrumentation/elasticsearch/elasticsearch-api-client-7.16/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/apiclient/ElasticsearchClientTest.java b/instrumentation/elasticsearch/elasticsearch-api-client-7.16/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/apiclient/ElasticsearchClientTest.java new file mode 100644 index 000000000000..698320035221 --- /dev/null +++ b/instrumentation/elasticsearch/elasticsearch-api-client-7.16/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/apiclient/ElasticsearchClientTest.java @@ -0,0 +1,236 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.elasticsearch.apiclient; + +import static io.opentelemetry.instrumentation.testing.GlobalTraceUtil.runWithSpan; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; + +import co.elastic.clients.elasticsearch.ElasticsearchAsyncClient; +import co.elastic.clients.elasticsearch.ElasticsearchClient; +import co.elastic.clients.elasticsearch.core.InfoResponse; +import co.elastic.clients.json.jackson.JacksonJsonpMapper; +import co.elastic.clients.transport.ElasticsearchTransport; +import co.elastic.clients.transport.rest_client.RestClientTransport; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.semconv.HttpAttributes; +import io.opentelemetry.semconv.NetworkAttributes; +import io.opentelemetry.semconv.ServerAttributes; +import io.opentelemetry.semconv.UrlAttributes; +import io.opentelemetry.semconv.incubating.DbIncubatingAttributes; +import java.io.IOException; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import org.apache.http.HttpHost; +import org.elasticsearch.client.RestClient; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.testcontainers.elasticsearch.ElasticsearchContainer; + +class ElasticsearchClientTest { + @RegisterExtension + static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + static ElasticsearchContainer elasticsearch; + + static HttpHost httpHost; + + static ElasticsearchClient client; + static ElasticsearchAsyncClient asyncClient; + + @BeforeAll + static void setUp() { + elasticsearch = + new ElasticsearchContainer("docker.elastic.co/elasticsearch/elasticsearch:7.17.2"); + // limit memory usage + elasticsearch.withEnv("ES_JAVA_OPTS", "-Xmx256m -Xms256m"); + elasticsearch.start(); + + httpHost = HttpHost.create(elasticsearch.getHttpHostAddress()); + + RestClient restClient = + RestClient.builder(httpHost) + .setRequestConfigCallback( + builder -> + builder + .setConnectTimeout(Integer.MAX_VALUE) + .setSocketTimeout(Integer.MAX_VALUE)) + .build(); + + ElasticsearchTransport transport = + new RestClientTransport(restClient, new JacksonJsonpMapper()); + client = new ElasticsearchClient(transport); + asyncClient = new ElasticsearchAsyncClient(transport); + } + + @AfterAll + static void cleanUp() { + elasticsearch.stop(); + } + + @Test + public void elasticsearchStatus() throws IOException { + InfoResponse response = client.info(); + Assertions.assertEquals(response.version().number(), "7.17.2"); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("info") + .hasKind(SpanKind.CLIENT) + .hasNoParent() + .hasAttributesSatisfyingExactly( + equalTo(DbIncubatingAttributes.DB_SYSTEM, "elasticsearch"), + equalTo(DbIncubatingAttributes.DB_OPERATION, "info"), + equalTo(HttpAttributes.HTTP_REQUEST_METHOD, "GET"), + equalTo(UrlAttributes.URL_FULL, httpHost.toURI() + "/"), + equalTo(ServerAttributes.SERVER_ADDRESS, httpHost.getHostName()), + equalTo(ServerAttributes.SERVER_PORT, httpHost.getPort())), + span -> + span.hasName("GET") + .hasKind(SpanKind.CLIENT) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo(ServerAttributes.SERVER_ADDRESS, httpHost.getHostName()), + equalTo(ServerAttributes.SERVER_PORT, httpHost.getPort()), + equalTo(HttpAttributes.HTTP_REQUEST_METHOD, "GET"), + equalTo(NetworkAttributes.NETWORK_PROTOCOL_VERSION, "1.1"), + equalTo(UrlAttributes.URL_FULL, httpHost.toURI() + "/"), + equalTo(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, 200L)))); + } + + @Test + public void elasticsearchIndex() throws IOException { + client.index( + r -> + r.id("test-id") + .index("test-index") + .document(new Person("person-name")) + .timeout(t -> t.time("10s"))); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("index") + .hasKind(SpanKind.CLIENT) + .hasNoParent() + .hasAttributesSatisfyingExactly( + equalTo(DbIncubatingAttributes.DB_SYSTEM, "elasticsearch"), + equalTo(DbIncubatingAttributes.DB_OPERATION, "index"), + equalTo(ServerAttributes.SERVER_ADDRESS, httpHost.getHostName()), + equalTo(ServerAttributes.SERVER_PORT, httpHost.getPort()), + equalTo(HttpAttributes.HTTP_REQUEST_METHOD, "PUT"), + equalTo( + UrlAttributes.URL_FULL, + httpHost.toURI() + "/test-index/_doc/test-id?timeout=10s"), + equalTo( + AttributeKey.stringKey("db.elasticsearch.path_parts.index"), + "test-index"), + equalTo( + AttributeKey.stringKey("db.elasticsearch.path_parts.id"), + "test-id")), + span -> + span.hasName("PUT") + .hasKind(SpanKind.CLIENT) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo(ServerAttributes.SERVER_ADDRESS, httpHost.getHostName()), + equalTo(ServerAttributes.SERVER_PORT, httpHost.getPort()), + equalTo(HttpAttributes.HTTP_REQUEST_METHOD, "PUT"), + equalTo(NetworkAttributes.NETWORK_PROTOCOL_VERSION, "1.1"), + equalTo( + UrlAttributes.URL_FULL, + httpHost.toURI() + "/test-index/_doc/test-id?timeout=10s"), + equalTo(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, 201L)))); + } + + @Test + public void elasticsearchStatusAsync() throws Exception { + CountDownLatch countDownLatch = new CountDownLatch(1); + AsyncRequest request = new AsyncRequest(); + + runWithSpan( + "parent", + () -> + asyncClient + .info() + .thenAccept( + infoResponse -> + runWithSpan( + "callback", + () -> { + request.setResponse(infoResponse); + countDownLatch.countDown(); + }))); + //noinspection ResultOfMethodCallIgnored + countDownLatch.await(10, TimeUnit.SECONDS); + + Assertions.assertEquals(request.getResponse().version().number(), "7.17.2"); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(), + span -> + span.hasName("info") + .hasKind(SpanKind.CLIENT) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo(DbIncubatingAttributes.DB_SYSTEM, "elasticsearch"), + equalTo(DbIncubatingAttributes.DB_OPERATION, "info"), + equalTo(ServerAttributes.SERVER_ADDRESS, httpHost.getHostName()), + equalTo(ServerAttributes.SERVER_PORT, httpHost.getPort()), + equalTo(HttpAttributes.HTTP_REQUEST_METHOD, "GET"), + equalTo(UrlAttributes.URL_FULL, httpHost.toURI() + "/")), + span -> + span.hasName("GET") + .hasKind(SpanKind.CLIENT) + .hasParent(trace.getSpan(1)) + .hasAttributesSatisfyingExactly( + equalTo(ServerAttributes.SERVER_ADDRESS, httpHost.getHostName()), + equalTo(ServerAttributes.SERVER_PORT, httpHost.getPort()), + equalTo(HttpAttributes.HTTP_REQUEST_METHOD, "GET"), + equalTo(NetworkAttributes.NETWORK_PROTOCOL_VERSION, "1.1"), + equalTo(UrlAttributes.URL_FULL, httpHost.toURI() + "/"), + equalTo(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, 200L)), + span -> + span.hasName("callback") + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(0)))); + } + + private static class AsyncRequest { + volatile InfoResponse response = null; + + public InfoResponse getResponse() { + return response; + } + + public void setResponse(InfoResponse response) { + this.response = response; + } + } + + private static class Person { + public final String name; + + Person(String name) { + this.name = name; + } + + @SuppressWarnings("unused") + public String getName() { + return name; + } + } +} diff --git a/instrumentation/elasticsearch/elasticsearch-rest-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/rest/v5_0/ElasticsearchRest5Singletons.java b/instrumentation/elasticsearch/elasticsearch-rest-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/rest/v5_0/ElasticsearchRest5Singletons.java index d5012c5426ba..c5dcb16a5844 100644 --- a/instrumentation/elasticsearch/elasticsearch-rest-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/rest/v5_0/ElasticsearchRest5Singletons.java +++ b/instrumentation/elasticsearch/elasticsearch-rest-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/rest/v5_0/ElasticsearchRest5Singletons.java @@ -6,14 +6,15 @@ package io.opentelemetry.javaagent.instrumentation.elasticsearch.rest.v5_0; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; -import io.opentelemetry.javaagent.instrumentation.elasticsearch.rest.ElasticsearchRestInstrumenterFactory; -import io.opentelemetry.javaagent.instrumentation.elasticsearch.rest.ElasticsearchRestRequest; +import io.opentelemetry.instrumentation.elasticsearch.rest.internal.ElasticsearchRestRequest; +import io.opentelemetry.javaagent.instrumentation.elasticsearch.rest.ElasticsearchRestJavaagentInstrumenterFactory; import org.elasticsearch.client.Response; public final class ElasticsearchRest5Singletons { private static final Instrumenter INSTRUMENTER = - ElasticsearchRestInstrumenterFactory.create("io.opentelemetry.elasticsearch-rest-5.0"); + ElasticsearchRestJavaagentInstrumenterFactory.create( + "io.opentelemetry.elasticsearch-rest-5.0"); public static Instrumenter instrumenter() { return INSTRUMENTER; diff --git a/instrumentation/elasticsearch/elasticsearch-rest-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/rest/v5_0/RestClientInstrumentation.java b/instrumentation/elasticsearch/elasticsearch-rest-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/rest/v5_0/RestClientInstrumentation.java index 3a88ea57cce4..fa361b726142 100644 --- a/instrumentation/elasticsearch/elasticsearch-rest-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/rest/v5_0/RestClientInstrumentation.java +++ b/instrumentation/elasticsearch/elasticsearch-rest-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/rest/v5_0/RestClientInstrumentation.java @@ -15,10 +15,10 @@ import io.opentelemetry.context.Context; import io.opentelemetry.context.Scope; +import io.opentelemetry.instrumentation.elasticsearch.rest.internal.ElasticsearchRestRequest; +import io.opentelemetry.instrumentation.elasticsearch.rest.internal.RestResponseListener; import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; -import io.opentelemetry.javaagent.instrumentation.elasticsearch.rest.ElasticsearchRestRequest; -import io.opentelemetry.javaagent.instrumentation.elasticsearch.rest.RestResponseListener; import net.bytebuddy.asm.Advice; import net.bytebuddy.description.type.TypeDescription; import net.bytebuddy.matcher.ElementMatcher; diff --git a/instrumentation/elasticsearch/elasticsearch-rest-5.0/javaagent/src/test/java/ElasticsearchRest5Test.java b/instrumentation/elasticsearch/elasticsearch-rest-5.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/rest/v5_0/ElasticsearchRest5Test.java similarity index 71% rename from instrumentation/elasticsearch/elasticsearch-rest-5.0/javaagent/src/test/java/ElasticsearchRest5Test.java rename to instrumentation/elasticsearch/elasticsearch-rest-5.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/rest/v5_0/ElasticsearchRest5Test.java index 138ed7260b94..15b3a23ec8c7 100644 --- a/instrumentation/elasticsearch/elasticsearch-rest-5.0/javaagent/src/test/java/ElasticsearchRest5Test.java +++ b/instrumentation/elasticsearch/elasticsearch-rest-5.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/rest/v5_0/ElasticsearchRest5Test.java @@ -3,14 +3,19 @@ * SPDX-License-Identifier: Apache-2.0 */ +package io.opentelemetry.javaagent.instrumentation.elasticsearch.rest.v5_0; + import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; import com.fasterxml.jackson.databind.ObjectMapper; -import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.trace.SpanKind; import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.opentelemetry.semconv.HttpAttributes; +import io.opentelemetry.semconv.NetworkAttributes; +import io.opentelemetry.semconv.ServerAttributes; +import io.opentelemetry.semconv.UrlAttributes; +import io.opentelemetry.semconv.incubating.DbIncubatingAttributes; import java.io.IOException; import java.util.Map; import java.util.concurrent.CountDownLatch; @@ -25,7 +30,7 @@ import org.junit.jupiter.api.extension.RegisterExtension; import org.testcontainers.elasticsearch.ElasticsearchContainer; -public class ElasticsearchRest5Test { +class ElasticsearchRest5Test { @RegisterExtension static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); @@ -91,25 +96,23 @@ void elasticsearchStatus() throws IOException { .hasKind(SpanKind.CLIENT) .hasNoParent() .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.DB_SYSTEM, "elasticsearch"), - equalTo(SemanticAttributes.DB_OPERATION, "GET"), - equalTo(SemanticAttributes.DB_STATEMENT, "GET _cluster/health")); + equalTo(DbIncubatingAttributes.DB_SYSTEM, "elasticsearch"), + equalTo(HttpAttributes.HTTP_REQUEST_METHOD, "GET"), + equalTo(ServerAttributes.SERVER_ADDRESS, httpHost.getHostName()), + equalTo(ServerAttributes.SERVER_PORT, httpHost.getPort()), + equalTo(UrlAttributes.URL_FULL, httpHost.toURI() + "/_cluster/health")); }, span -> { span.hasName("GET") .hasKind(SpanKind.CLIENT) .hasParent(trace.getSpan(0)) .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.NET_PEER_NAME, httpHost.getHostName()), - equalTo(SemanticAttributes.NET_PEER_PORT, httpHost.getPort()), - equalTo(SemanticAttributes.HTTP_METHOD, "GET"), - equalTo(AttributeKey.stringKey("net.protocol.name"), "http"), - equalTo(AttributeKey.stringKey("net.protocol.version"), "1.1"), - equalTo(SemanticAttributes.HTTP_URL, httpHost.toURI() + "/_cluster/health"), - equalTo(SemanticAttributes.HTTP_STATUS_CODE, 200), - equalTo( - SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH, - response.getEntity().getContentLength())); + equalTo(ServerAttributes.SERVER_ADDRESS, httpHost.getHostName()), + equalTo(ServerAttributes.SERVER_PORT, httpHost.getPort()), + equalTo(HttpAttributes.HTTP_REQUEST_METHOD, "GET"), + equalTo(NetworkAttributes.NETWORK_PROTOCOL_VERSION, "1.1"), + equalTo(UrlAttributes.URL_FULL, httpHost.toURI() + "/_cluster/health"), + equalTo(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, 200)); }); }); } @@ -169,25 +172,23 @@ public void onFailure(Exception e) { .hasKind(SpanKind.CLIENT) .hasParent(trace.getSpan(0)) .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.DB_SYSTEM, "elasticsearch"), - equalTo(SemanticAttributes.DB_OPERATION, "GET"), - equalTo(SemanticAttributes.DB_STATEMENT, "GET _cluster/health")); + equalTo(DbIncubatingAttributes.DB_SYSTEM, "elasticsearch"), + equalTo(HttpAttributes.HTTP_REQUEST_METHOD, "GET"), + equalTo(ServerAttributes.SERVER_ADDRESS, httpHost.getHostName()), + equalTo(ServerAttributes.SERVER_PORT, httpHost.getPort()), + equalTo(UrlAttributes.URL_FULL, httpHost.toURI() + "/_cluster/health")); }, span -> { span.hasName("GET") .hasKind(SpanKind.CLIENT) .hasParent(trace.getSpan(1)) .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.NET_PEER_NAME, httpHost.getHostName()), - equalTo(SemanticAttributes.NET_PEER_PORT, httpHost.getPort()), - equalTo(SemanticAttributes.HTTP_METHOD, "GET"), - equalTo(AttributeKey.stringKey("net.protocol.name"), "http"), - equalTo(AttributeKey.stringKey("net.protocol.version"), "1.1"), - equalTo(SemanticAttributes.HTTP_URL, httpHost.toURI() + "/_cluster/health"), - equalTo(SemanticAttributes.HTTP_STATUS_CODE, 200), - equalTo( - SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH, - requestResponse[0].getEntity().getContentLength())); + equalTo(ServerAttributes.SERVER_ADDRESS, httpHost.getHostName()), + equalTo(ServerAttributes.SERVER_PORT, httpHost.getPort()), + equalTo(HttpAttributes.HTTP_REQUEST_METHOD, "GET"), + equalTo(NetworkAttributes.NETWORK_PROTOCOL_VERSION, "1.1"), + equalTo(UrlAttributes.URL_FULL, httpHost.toURI() + "/_cluster/health"), + equalTo(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, 200)); }, span -> { span.hasName("callback").hasKind(SpanKind.INTERNAL).hasParent(trace.getSpan(0)); diff --git a/instrumentation/elasticsearch/elasticsearch-rest-6.4/javaagent/build.gradle.kts b/instrumentation/elasticsearch/elasticsearch-rest-6.4/javaagent/build.gradle.kts index 79e573af55b9..32c09caa798a 100644 --- a/instrumentation/elasticsearch/elasticsearch-rest-6.4/javaagent/build.gradle.kts +++ b/instrumentation/elasticsearch/elasticsearch-rest-6.4/javaagent/build.gradle.kts @@ -24,9 +24,6 @@ dependencies { 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") diff --git a/instrumentation/elasticsearch/elasticsearch-rest-6.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/rest/v6_4/ElasticsearchRest6Singletons.java b/instrumentation/elasticsearch/elasticsearch-rest-6.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/rest/v6_4/ElasticsearchRest6Singletons.java index 2b82e991f941..3ba62a42a970 100644 --- a/instrumentation/elasticsearch/elasticsearch-rest-6.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/rest/v6_4/ElasticsearchRest6Singletons.java +++ b/instrumentation/elasticsearch/elasticsearch-rest-6.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/rest/v6_4/ElasticsearchRest6Singletons.java @@ -6,14 +6,15 @@ package io.opentelemetry.javaagent.instrumentation.elasticsearch.rest.v6_4; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; -import io.opentelemetry.javaagent.instrumentation.elasticsearch.rest.ElasticsearchRestInstrumenterFactory; -import io.opentelemetry.javaagent.instrumentation.elasticsearch.rest.ElasticsearchRestRequest; +import io.opentelemetry.instrumentation.elasticsearch.rest.internal.ElasticsearchRestRequest; +import io.opentelemetry.javaagent.instrumentation.elasticsearch.rest.ElasticsearchRestJavaagentInstrumenterFactory; import org.elasticsearch.client.Response; public final class ElasticsearchRest6Singletons { private static final Instrumenter INSTRUMENTER = - ElasticsearchRestInstrumenterFactory.create("io.opentelemetry.elasticsearch-rest-6.4"); + ElasticsearchRestJavaagentInstrumenterFactory.create( + "io.opentelemetry.elasticsearch-rest-6.4"); public static Instrumenter instrumenter() { return INSTRUMENTER; diff --git a/instrumentation/elasticsearch/elasticsearch-rest-6.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/rest/v6_4/RestClientInstrumentation.java b/instrumentation/elasticsearch/elasticsearch-rest-6.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/rest/v6_4/RestClientInstrumentation.java index 563863c10ee9..46072eb9f0d2 100644 --- a/instrumentation/elasticsearch/elasticsearch-rest-6.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/rest/v6_4/RestClientInstrumentation.java +++ b/instrumentation/elasticsearch/elasticsearch-rest-6.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/rest/v6_4/RestClientInstrumentation.java @@ -14,10 +14,10 @@ import io.opentelemetry.context.Context; import io.opentelemetry.context.Scope; +import io.opentelemetry.instrumentation.elasticsearch.rest.internal.ElasticsearchRestRequest; +import io.opentelemetry.instrumentation.elasticsearch.rest.internal.RestResponseListener; import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; -import io.opentelemetry.javaagent.instrumentation.elasticsearch.rest.ElasticsearchRestRequest; -import io.opentelemetry.javaagent.instrumentation.elasticsearch.rest.RestResponseListener; import net.bytebuddy.asm.Advice; import net.bytebuddy.description.type.TypeDescription; import net.bytebuddy.matcher.ElementMatcher; diff --git a/instrumentation/elasticsearch/elasticsearch-rest-6.4/javaagent/src/test/java/ElasticsearchRest6Test.java b/instrumentation/elasticsearch/elasticsearch-rest-6.4/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/rest/v6_4/ElasticsearchRest6Test.java similarity index 69% rename from instrumentation/elasticsearch/elasticsearch-rest-6.4/javaagent/src/test/java/ElasticsearchRest6Test.java rename to instrumentation/elasticsearch/elasticsearch-rest-6.4/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/rest/v6_4/ElasticsearchRest6Test.java index 178a2351235f..ff3ccc15d302 100644 --- a/instrumentation/elasticsearch/elasticsearch-rest-6.4/javaagent/src/test/java/ElasticsearchRest6Test.java +++ b/instrumentation/elasticsearch/elasticsearch-rest-6.4/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/rest/v6_4/ElasticsearchRest6Test.java @@ -3,14 +3,19 @@ * SPDX-License-Identifier: Apache-2.0 */ +package io.opentelemetry.javaagent.instrumentation.elasticsearch.rest.v6_4; + import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; import com.fasterxml.jackson.databind.ObjectMapper; -import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.trace.SpanKind; import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.opentelemetry.semconv.HttpAttributes; +import io.opentelemetry.semconv.NetworkAttributes; +import io.opentelemetry.semconv.ServerAttributes; +import io.opentelemetry.semconv.UrlAttributes; +import io.opentelemetry.semconv.incubating.DbIncubatingAttributes; import java.io.IOException; import java.util.Map; import java.util.concurrent.CountDownLatch; @@ -25,7 +30,7 @@ import org.junit.jupiter.api.extension.RegisterExtension; import org.testcontainers.elasticsearch.ElasticsearchContainer; -public class ElasticsearchRest6Test { +class ElasticsearchRest6Test { @RegisterExtension static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); @@ -82,25 +87,23 @@ public void elasticsearchStatus() throws IOException { .hasKind(SpanKind.CLIENT) .hasNoParent() .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.DB_SYSTEM, "elasticsearch"), - equalTo(SemanticAttributes.DB_OPERATION, "GET"), - equalTo(SemanticAttributes.DB_STATEMENT, "GET _cluster/health")); + equalTo(DbIncubatingAttributes.DB_SYSTEM, "elasticsearch"), + equalTo(HttpAttributes.HTTP_REQUEST_METHOD, "GET"), + equalTo(ServerAttributes.SERVER_ADDRESS, httpHost.getHostName()), + equalTo(ServerAttributes.SERVER_PORT, httpHost.getPort()), + equalTo(UrlAttributes.URL_FULL, httpHost.toURI() + "/_cluster/health")); }, span -> { span.hasName("GET") .hasKind(SpanKind.CLIENT) .hasParent(trace.getSpan(0)) .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.NET_PEER_NAME, httpHost.getHostName()), - equalTo(SemanticAttributes.NET_PEER_PORT, httpHost.getPort()), - equalTo(SemanticAttributes.HTTP_METHOD, "GET"), - equalTo(AttributeKey.stringKey("net.protocol.name"), "http"), - equalTo(AttributeKey.stringKey("net.protocol.version"), "1.1"), - equalTo(SemanticAttributes.HTTP_URL, httpHost.toURI() + "/_cluster/health"), - equalTo(SemanticAttributes.HTTP_STATUS_CODE, 200L), - equalTo( - SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH, - response.getEntity().getContentLength())); + equalTo(ServerAttributes.SERVER_ADDRESS, httpHost.getHostName()), + equalTo(ServerAttributes.SERVER_PORT, httpHost.getPort()), + equalTo(HttpAttributes.HTTP_REQUEST_METHOD, "GET"), + equalTo(NetworkAttributes.NETWORK_PROTOCOL_VERSION, "1.1"), + equalTo(UrlAttributes.URL_FULL, httpHost.toURI() + "/_cluster/health"), + equalTo(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, 200L)); }); }); } @@ -159,25 +162,23 @@ public void onFailure(Exception e) { .hasKind(SpanKind.CLIENT) .hasParent(trace.getSpan(0)) .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.DB_SYSTEM, "elasticsearch"), - equalTo(SemanticAttributes.DB_OPERATION, "GET"), - equalTo(SemanticAttributes.DB_STATEMENT, "GET _cluster/health")); + equalTo(DbIncubatingAttributes.DB_SYSTEM, "elasticsearch"), + equalTo(HttpAttributes.HTTP_REQUEST_METHOD, "GET"), + equalTo(ServerAttributes.SERVER_ADDRESS, httpHost.getHostName()), + equalTo(ServerAttributes.SERVER_PORT, httpHost.getPort()), + equalTo(UrlAttributes.URL_FULL, httpHost.toURI() + "/_cluster/health")); }, span -> { span.hasName("GET") .hasKind(SpanKind.CLIENT) .hasParent(trace.getSpan(1)) .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.NET_PEER_NAME, httpHost.getHostName()), - equalTo(SemanticAttributes.NET_PEER_PORT, httpHost.getPort()), - equalTo(SemanticAttributes.HTTP_METHOD, "GET"), - equalTo(AttributeKey.stringKey("net.protocol.name"), "http"), - equalTo(AttributeKey.stringKey("net.protocol.version"), "1.1"), - equalTo(SemanticAttributes.HTTP_URL, httpHost.toURI() + "/_cluster/health"), - equalTo(SemanticAttributes.HTTP_STATUS_CODE, 200), - equalTo( - SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH, - requestResponse[0].getEntity().getContentLength())); + equalTo(ServerAttributes.SERVER_ADDRESS, httpHost.getHostName()), + equalTo(ServerAttributes.SERVER_PORT, httpHost.getPort()), + equalTo(HttpAttributes.HTTP_REQUEST_METHOD, "GET"), + equalTo(NetworkAttributes.NETWORK_PROTOCOL_VERSION, "1.1"), + equalTo(UrlAttributes.URL_FULL, httpHost.toURI() + "/_cluster/health"), + equalTo(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, 200)); }, span -> { span.hasName("callback").hasKind(SpanKind.INTERNAL).hasParent(trace.getSpan(0)); diff --git a/instrumentation/elasticsearch/elasticsearch-rest-7.0/javaagent/build.gradle.kts b/instrumentation/elasticsearch/elasticsearch-rest-7.0/javaagent/build.gradle.kts index 6e04bbed3905..782d9e83fb2e 100644 --- a/instrumentation/elasticsearch/elasticsearch-rest-7.0/javaagent/build.gradle.kts +++ b/instrumentation/elasticsearch/elasticsearch-rest-7.0/javaagent/build.gradle.kts @@ -10,6 +10,15 @@ muzzle { assertInverse.set(true) } + fail { + group.set("org.elasticsearch.client") + module.set("elasticsearch-rest-client") + versions.set("[8.10,)") + // elasticsearch-java 8.10+ has native, on-by-default opentelemetry instrumentation + // we disable our elasticsearch-rest-client instrumentation when elasticsearch-java is present + extraDependency("co.elastic.clients:elasticsearch-java:8.10.0") + } + fail { group.set("org.elasticsearch.client") module.set("rest") @@ -24,13 +33,8 @@ dependencies { 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("com.fasterxml.jackson.core:jackson-databind") testImplementation("org.testcontainers:elasticsearch") } diff --git a/instrumentation/elasticsearch/elasticsearch-rest-7.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/rest/v7_0/ElasticsearchRest7InstrumentationModule.java b/instrumentation/elasticsearch/elasticsearch-rest-7.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/rest/v7_0/ElasticsearchRest7InstrumentationModule.java index 81001ea209e0..22d00be64dbd 100644 --- a/instrumentation/elasticsearch/elasticsearch-rest-7.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/rest/v7_0/ElasticsearchRest7InstrumentationModule.java +++ b/instrumentation/elasticsearch/elasticsearch-rest-7.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/rest/v7_0/ElasticsearchRest7InstrumentationModule.java @@ -7,23 +7,36 @@ import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.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 io.opentelemetry.javaagent.extension.instrumentation.internal.ExperimentalInstrumentationModule; import java.util.List; import net.bytebuddy.matcher.ElementMatcher; @AutoService(InstrumentationModule.class) -public class ElasticsearchRest7InstrumentationModule extends InstrumentationModule { +public class ElasticsearchRest7InstrumentationModule extends InstrumentationModule + implements ExperimentalInstrumentationModule { public ElasticsearchRest7InstrumentationModule() { 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"); + // Class `org.elasticsearch.client.RestClient$InternalRequest` introduced in 7.0.0. + // Since Elasticsearch client version 8.10, the ES client comes with a native OTel + // instrumentation that introduced the class + // `co.elastic.clients.transport.instrumentation.Instrumentation`. + // Disabling agent instrumentation for those cases. + return hasClassesNamed("org.elasticsearch.client.RestClient$InternalRequest") + .and(not(hasClassesNamed("co.elastic.clients.transport.instrumentation.Instrumentation"))); + } + + @Override + public String getModuleGroup() { + return "elasticsearch"; } @Override diff --git a/instrumentation/elasticsearch/elasticsearch-rest-7.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/rest/v7_0/ElasticsearchRest7Singletons.java b/instrumentation/elasticsearch/elasticsearch-rest-7.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/rest/v7_0/ElasticsearchRest7Singletons.java index 5e9a37e51bd6..6d7a1de77710 100644 --- a/instrumentation/elasticsearch/elasticsearch-rest-7.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/rest/v7_0/ElasticsearchRest7Singletons.java +++ b/instrumentation/elasticsearch/elasticsearch-rest-7.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/rest/v7_0/ElasticsearchRest7Singletons.java @@ -6,14 +6,15 @@ package io.opentelemetry.javaagent.instrumentation.elasticsearch.rest.v7_0; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; -import io.opentelemetry.javaagent.instrumentation.elasticsearch.rest.ElasticsearchRestInstrumenterFactory; -import io.opentelemetry.javaagent.instrumentation.elasticsearch.rest.ElasticsearchRestRequest; +import io.opentelemetry.instrumentation.elasticsearch.rest.internal.ElasticsearchRestRequest; +import io.opentelemetry.javaagent.instrumentation.elasticsearch.rest.ElasticsearchRestJavaagentInstrumenterFactory; import org.elasticsearch.client.Response; public final class ElasticsearchRest7Singletons { private static final Instrumenter INSTRUMENTER = - ElasticsearchRestInstrumenterFactory.create("io.opentelemetry.elasticsearch-rest-7.0"); + ElasticsearchRestJavaagentInstrumenterFactory.create( + "io.opentelemetry.elasticsearch-rest-7.0"); public static Instrumenter instrumenter() { return INSTRUMENTER; diff --git a/instrumentation/elasticsearch/elasticsearch-rest-7.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/rest/v7_0/RestClientInstrumentation.java b/instrumentation/elasticsearch/elasticsearch-rest-7.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/rest/v7_0/RestClientInstrumentation.java index bc51e5757636..7026a70f9872 100644 --- a/instrumentation/elasticsearch/elasticsearch-rest-7.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/rest/v7_0/RestClientInstrumentation.java +++ b/instrumentation/elasticsearch/elasticsearch-rest-7.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/rest/v7_0/RestClientInstrumentation.java @@ -14,10 +14,12 @@ import io.opentelemetry.context.Context; import io.opentelemetry.context.Scope; +import io.opentelemetry.instrumentation.api.util.VirtualField; +import io.opentelemetry.instrumentation.elasticsearch.rest.internal.ElasticsearchEndpointDefinition; +import io.opentelemetry.instrumentation.elasticsearch.rest.internal.ElasticsearchRestRequest; +import io.opentelemetry.instrumentation.elasticsearch.rest.internal.RestResponseListener; import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; -import io.opentelemetry.javaagent.instrumentation.elasticsearch.rest.ElasticsearchRestRequest; -import io.opentelemetry.javaagent.instrumentation.elasticsearch.rest.RestResponseListener; import net.bytebuddy.asm.Advice; import net.bytebuddy.description.type.TypeDescription; import net.bytebuddy.matcher.ElementMatcher; @@ -59,7 +61,15 @@ public static void onEnter( @Advice.Local("otelScope") Scope scope) { Context parentContext = currentContext(); - otelRequest = ElasticsearchRestRequest.create(request.getMethod(), request.getEndpoint()); + VirtualField virtualField = + VirtualField.find(Request.class, ElasticsearchEndpointDefinition.class); + otelRequest = + ElasticsearchRestRequest.create( + request.getMethod(), + request.getEndpoint(), + // set by elasticsearch-api-client instrumentation + virtualField.get(request), + request.getEntity()); if (!instrumenter().shouldStart(parentContext, otelRequest)) { return; } @@ -70,7 +80,7 @@ public static void onEnter( @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) public static void stopSpan( - @Advice.Return(readOnly = false) Response response, + @Advice.Return Response response, @Advice.Thrown Throwable throwable, @Advice.Local("otelRequest") ElasticsearchRestRequest otelRequest, @Advice.Local("otelContext") Context context, @@ -97,7 +107,16 @@ public static void onEnter( @Advice.Local("otelScope") Scope scope) { Context parentContext = currentContext(); - otelRequest = ElasticsearchRestRequest.create(request.getMethod(), request.getEndpoint()); + VirtualField virtualField = + VirtualField.find(Request.class, ElasticsearchEndpointDefinition.class); + + otelRequest = + ElasticsearchRestRequest.create( + request.getMethod(), + request.getEndpoint(), + // set by elasticsearch-api-client instrumentation + virtualField.get(request), + request.getEntity()); if (!instrumenter().shouldStart(parentContext, otelRequest)) { return; } diff --git a/instrumentation/elasticsearch/elasticsearch-rest-7.0/javaagent/src/test/groovy/ElasticsearchRest7Test.groovy b/instrumentation/elasticsearch/elasticsearch-rest-7.0/javaagent/src/test/groovy/ElasticsearchRest7Test.groovy deleted file mode 100644 index 299795ddf1cb..000000000000 --- a/instrumentation/elasticsearch/elasticsearch-rest-7.0/javaagent/src/test/groovy/ElasticsearchRest7Test.groovy +++ /dev/null @@ -1,171 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -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.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 - -import java.util.concurrent.CountDownLatch - -import static io.opentelemetry.api.trace.SpanKind.CLIENT -import static io.opentelemetry.api.trace.SpanKind.INTERNAL - -class ElasticsearchRest7Test 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") - // limit memory usage - elasticsearch.withEnv("ES_JAVA_OPTS", "-Xmx256m -Xms256m") - 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, 2) { - span(0) { - name "GET" - kind CLIENT - hasNoParent() - attributes { - "$SemanticAttributes.DB_SYSTEM" "elasticsearch" - "$SemanticAttributes.DB_OPERATION" "GET" - "$SemanticAttributes.DB_STATEMENT" "GET _cluster/health" - } - } - span(1) { - name "GET" - kind CLIENT - childOf span(0) - attributes { - "$SemanticAttributes.NET_PEER_NAME" httpHost.hostName - "$SemanticAttributes.NET_PEER_PORT" httpHost.port - "$SemanticAttributes.HTTP_METHOD" "GET" - "net.protocol.name" "http" - "net.protocol.version" "1.1" - "$SemanticAttributes.HTTP_URL" "${httpHost.toURI()}/_cluster/health" - "$SemanticAttributes.HTTP_STATUS_CODE" 200 - "$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" Long - } - } - } - } - } - - 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) { - runWithSpan("callback") { - requestResponse = response - countDownLatch.countDown() - } - } - - @Override - void onFailure(Exception e) { - runWithSpan("callback") { - exception = e - countDownLatch.countDown() - } - } - } - runWithSpan("parent") { - 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, 4) { - span(0) { - name "parent" - kind INTERNAL - hasNoParent() - } - span(1) { - name "GET" - kind CLIENT - childOf(span(0)) - attributes { - "$SemanticAttributes.DB_SYSTEM" "elasticsearch" - "$SemanticAttributes.DB_OPERATION" "GET" - "$SemanticAttributes.DB_STATEMENT" "GET _cluster/health" - } - } - span(2) { - name "GET" - kind CLIENT - childOf span(1) - attributes { - "$SemanticAttributes.NET_PEER_NAME" httpHost.hostName - "$SemanticAttributes.NET_PEER_PORT" httpHost.port - "$SemanticAttributes.HTTP_METHOD" "GET" - "net.protocol.name" "http" - "net.protocol.version" "1.1" - "$SemanticAttributes.HTTP_URL" "${httpHost.toURI()}/_cluster/health" - "$SemanticAttributes.HTTP_STATUS_CODE" 200 - "$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" Long - } - } - span(3) { - name "callback" - kind INTERNAL - childOf(span(0)) - } - } - } - } -} diff --git a/instrumentation/elasticsearch/elasticsearch-rest-7.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/rest/v7_0/ElasticsearchRest7Test.java b/instrumentation/elasticsearch/elasticsearch-rest-7.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/rest/v7_0/ElasticsearchRest7Test.java new file mode 100644 index 000000000000..517a7a821f48 --- /dev/null +++ b/instrumentation/elasticsearch/elasticsearch-rest-7.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/rest/v7_0/ElasticsearchRest7Test.java @@ -0,0 +1,200 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.elasticsearch.rest.v7_0; + +import static io.opentelemetry.instrumentation.testing.GlobalTraceUtil.runWithSpan; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; + +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.semconv.HttpAttributes; +import io.opentelemetry.semconv.NetworkAttributes; +import io.opentelemetry.semconv.ServerAttributes; +import io.opentelemetry.semconv.UrlAttributes; +import io.opentelemetry.semconv.incubating.DbIncubatingAttributes; +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import org.apache.http.HttpHost; +import org.elasticsearch.client.Request; +import org.elasticsearch.client.Response; +import org.elasticsearch.client.ResponseListener; +import org.elasticsearch.client.RestClient; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.testcontainers.elasticsearch.ElasticsearchContainer; +import org.testcontainers.shaded.com.fasterxml.jackson.databind.ObjectMapper; + +class ElasticsearchRest7Test { + @RegisterExtension + static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + static ElasticsearchContainer elasticsearch; + + static HttpHost httpHost; + + static RestClient client; + + static ObjectMapper objectMapper; + + @BeforeAll + static void setUp() { + elasticsearch = + new ElasticsearchContainer("docker.elastic.co/elasticsearch/elasticsearch-oss:7.10.2"); + // limit memory usage + elasticsearch.withEnv("ES_JAVA_OPTS", "-Xmx256m -Xms256m"); + elasticsearch.start(); + + httpHost = HttpHost.create(elasticsearch.getHttpHostAddress()); + + client = + RestClient.builder(httpHost) + .setRequestConfigCallback( + builder -> + builder + .setConnectTimeout(Integer.MAX_VALUE) + .setSocketTimeout(Integer.MAX_VALUE)) + .build(); + + objectMapper = new ObjectMapper(); + } + + @AfterAll + static void cleanUp() { + elasticsearch.stop(); + } + + @Test + public void elasticsearchStatus() throws Exception { + Response response = client.performRequest(new Request("GET", "_cluster/health")); + Map result = objectMapper.readValue(response.getEntity().getContent(), Map.class); + Assertions.assertEquals(result.get("status"), "green"); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("GET") + .hasKind(SpanKind.CLIENT) + .hasNoParent() + .hasAttributesSatisfyingExactly( + equalTo(DbIncubatingAttributes.DB_SYSTEM, "elasticsearch"), + equalTo(HttpAttributes.HTTP_REQUEST_METHOD, "GET"), + equalTo(ServerAttributes.SERVER_ADDRESS, httpHost.getHostName()), + equalTo(ServerAttributes.SERVER_PORT, httpHost.getPort()), + equalTo(UrlAttributes.URL_FULL, httpHost.toURI() + "/_cluster/health")), + span -> + span.hasName("GET") + .hasKind(SpanKind.CLIENT) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo(ServerAttributes.SERVER_ADDRESS, httpHost.getHostName()), + equalTo(ServerAttributes.SERVER_PORT, httpHost.getPort()), + equalTo(HttpAttributes.HTTP_REQUEST_METHOD, "GET"), + equalTo(NetworkAttributes.NETWORK_PROTOCOL_VERSION, "1.1"), + equalTo(UrlAttributes.URL_FULL, httpHost.toURI() + "/_cluster/health"), + equalTo(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, 200L)))); + } + + @Test + public void elasticsearchStatusAsync() throws Exception { + AsyncRequest asyncRequest = new AsyncRequest(); + CountDownLatch countDownLatch = new CountDownLatch(1); + ResponseListener responseListener = + new ResponseListener() { + @Override + public void onSuccess(Response response) { + + runWithSpan( + "callback", + () -> { + asyncRequest.setRequestResponse(response); + countDownLatch.countDown(); + }); + } + + @Override + public void onFailure(Exception e) { + runWithSpan( + "callback", + () -> { + asyncRequest.setException(e); + countDownLatch.countDown(); + }); + } + }; + + runWithSpan( + "parent", + () -> client.performRequestAsync(new Request("GET", "_cluster/health"), responseListener)); + //noinspection ResultOfMethodCallIgnored + countDownLatch.await(10, TimeUnit.SECONDS); + + if (asyncRequest.getException() != null) { + throw asyncRequest.getException(); + } + + Map result = + objectMapper.readValue( + asyncRequest.getRequestResponse().getEntity().getContent(), Map.class); + Assertions.assertEquals(result.get("status"), "green"); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(), + span -> + span.hasName("GET") + .hasKind(SpanKind.CLIENT) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo(DbIncubatingAttributes.DB_SYSTEM, "elasticsearch"), + equalTo(HttpAttributes.HTTP_REQUEST_METHOD, "GET"), + equalTo(ServerAttributes.SERVER_ADDRESS, httpHost.getHostName()), + equalTo(ServerAttributes.SERVER_PORT, httpHost.getPort()), + equalTo(UrlAttributes.URL_FULL, httpHost.toURI() + "/_cluster/health")), + span -> + span.hasName("GET") + .hasKind(SpanKind.CLIENT) + .hasParent(trace.getSpan(1)) + .hasAttributesSatisfyingExactly( + equalTo(ServerAttributes.SERVER_ADDRESS, httpHost.getHostName()), + equalTo(ServerAttributes.SERVER_PORT, httpHost.getPort()), + equalTo(HttpAttributes.HTTP_REQUEST_METHOD, "GET"), + equalTo(NetworkAttributes.NETWORK_PROTOCOL_VERSION, "1.1"), + equalTo(UrlAttributes.URL_FULL, httpHost.toURI() + "/_cluster/health"), + equalTo(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, 200L)), + span -> + span.hasName("callback") + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(0)))); + } + + private static class AsyncRequest { + volatile Response requestResponse = null; + volatile Exception exception = null; + + public Response getRequestResponse() { + return requestResponse; + } + + public void setRequestResponse(Response requestResponse) { + this.requestResponse = requestResponse; + } + + public Exception getException() { + return exception; + } + + public void setException(Exception exception) { + this.exception = exception; + } + } +} diff --git a/instrumentation/elasticsearch/elasticsearch-rest-7.0/library/build.gradle.kts b/instrumentation/elasticsearch/elasticsearch-rest-7.0/library/build.gradle.kts new file mode 100644 index 000000000000..2af7dc297a23 --- /dev/null +++ b/instrumentation/elasticsearch/elasticsearch-rest-7.0/library/build.gradle.kts @@ -0,0 +1,18 @@ +plugins { + id("otel.library-instrumentation") +} + +dependencies { + library("org.elasticsearch.client:elasticsearch-rest-client:7.0.0") + implementation("net.bytebuddy:byte-buddy") + implementation(project(":instrumentation:elasticsearch:elasticsearch-rest-common:library")) + + testImplementation("com.fasterxml.jackson.core:jackson-databind") + testImplementation("org.testcontainers:elasticsearch") +} + +tasks { + test { + usesService(gradle.sharedServices.registrations["testcontainersBuildService"].service) + } +} diff --git a/instrumentation/elasticsearch/elasticsearch-rest-7.0/library/src/main/java/io/opentelemetry/instrumentation/elasticsearch/rest/v7_0/ElasticsearchRest7Telemetry.java b/instrumentation/elasticsearch/elasticsearch-rest-7.0/library/src/main/java/io/opentelemetry/instrumentation/elasticsearch/rest/v7_0/ElasticsearchRest7Telemetry.java new file mode 100644 index 000000000000..7193ba383fdd --- /dev/null +++ b/instrumentation/elasticsearch/elasticsearch-rest-7.0/library/src/main/java/io/opentelemetry/instrumentation/elasticsearch/rest/v7_0/ElasticsearchRest7Telemetry.java @@ -0,0 +1,46 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.elasticsearch.rest.v7_0; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import io.opentelemetry.instrumentation.elasticsearch.rest.internal.ElasticsearchRestRequest; +import org.elasticsearch.client.Response; +import org.elasticsearch.client.RestClient; + +/** Entrypoint for instrumenting Apache Elasticsearch Rest clients. */ +public final class ElasticsearchRest7Telemetry { + + /** + * Returns a new {@link ElasticsearchRest7Telemetry} configured with the given {@link + * OpenTelemetry}. + */ + public static ElasticsearchRest7Telemetry create(OpenTelemetry openTelemetry) { + return builder(openTelemetry).build(); + } + + /** + * Returns a new {@link ElasticsearchRest7TelemetryBuilder} configured with the given {@link + * OpenTelemetry}. + */ + public static ElasticsearchRest7TelemetryBuilder builder(OpenTelemetry openTelemetry) { + return new ElasticsearchRest7TelemetryBuilder(openTelemetry); + } + + private final Instrumenter instrumenter; + + ElasticsearchRest7Telemetry(Instrumenter instrumenter) { + this.instrumenter = instrumenter; + } + + /** + * Construct a new tracing-enable {@link RestClient} using the provided {@link RestClient} + * instance. + */ + public RestClient wrap(RestClient restClient) { + return RestClientWrapper.wrap(restClient, instrumenter); + } +} diff --git a/instrumentation/elasticsearch/elasticsearch-rest-7.0/library/src/main/java/io/opentelemetry/instrumentation/elasticsearch/rest/v7_0/ElasticsearchRest7TelemetryBuilder.java b/instrumentation/elasticsearch/elasticsearch-rest-7.0/library/src/main/java/io/opentelemetry/instrumentation/elasticsearch/rest/v7_0/ElasticsearchRest7TelemetryBuilder.java new file mode 100644 index 000000000000..8145d8e4fc91 --- /dev/null +++ b/instrumentation/elasticsearch/elasticsearch-rest-7.0/library/src/main/java/io/opentelemetry/instrumentation/elasticsearch/rest/v7_0/ElasticsearchRest7TelemetryBuilder.java @@ -0,0 +1,98 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.elasticsearch.rest.v7_0; + +import com.google.errorprone.annotations.CanIgnoreReturnValue; +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.SpanNameExtractor; +import io.opentelemetry.instrumentation.api.internal.HttpConstants; +import io.opentelemetry.instrumentation.api.semconv.http.HttpClientAttributesExtractorBuilder; +import io.opentelemetry.instrumentation.elasticsearch.rest.internal.ElasticsearchRestInstrumenterFactory; +import io.opentelemetry.instrumentation.elasticsearch.rest.internal.ElasticsearchRestRequest; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.function.Function; +import org.elasticsearch.client.Response; + +public final class ElasticsearchRest7TelemetryBuilder { + + private static final String INSTRUMENTATION_NAME = "io.opentelemetry.elasticsearch-rest-7.0"; + + private final OpenTelemetry openTelemetry; + private final List> attributesExtractors = + new ArrayList<>(); + private Set knownMethods = HttpConstants.KNOWN_METHODS; + private Function< + SpanNameExtractor, + ? extends SpanNameExtractor> + spanNameExtractorTransformer = Function.identity(); + + ElasticsearchRest7TelemetryBuilder(OpenTelemetry openTelemetry) { + this.openTelemetry = openTelemetry; + } + + /** + * Adds an additional {@link AttributesExtractor} to invoke to set attributes to instrumented + * items. + */ + @CanIgnoreReturnValue + public ElasticsearchRest7TelemetryBuilder addAttributesExtractor( + AttributesExtractor attributesExtractor) { + attributesExtractors.add(attributesExtractor); + return this; + } + + /** + * Configures the instrumentation to recognize an alternative set of HTTP request methods. + * + *

By default, this instrumentation defines "known" methods as the ones listed in RFC9110 and the PATCH + * method defined in RFC5789. + * + *

Note: calling this method overrides the default known method sets completely; it does + * not supplement it. + * + * @param knownMethods A set of recognized HTTP request methods. + * @see HttpClientAttributesExtractorBuilder#setKnownMethods(Set) + */ + @CanIgnoreReturnValue + public ElasticsearchRest7TelemetryBuilder setKnownMethods(Set knownMethods) { + this.knownMethods = new HashSet<>(knownMethods); + return this; + } + + /** Sets custom {@link SpanNameExtractor} via transform function. */ + @CanIgnoreReturnValue + public ElasticsearchRest7TelemetryBuilder setSpanNameExtractor( + Function< + SpanNameExtractor, + ? extends SpanNameExtractor> + spanNameExtractorTransformer) { + this.spanNameExtractorTransformer = spanNameExtractorTransformer; + return this; + } + + /** + * Returns a new {@link ElasticsearchRest7Telemetry} with the settings of this {@link + * ElasticsearchRest7TelemetryBuilder}. + */ + public ElasticsearchRest7Telemetry build() { + Instrumenter instrumenter = + ElasticsearchRestInstrumenterFactory.create( + openTelemetry, + INSTRUMENTATION_NAME, + attributesExtractors, + spanNameExtractorTransformer, + knownMethods, + false); + + return new ElasticsearchRest7Telemetry(instrumenter); + } +} diff --git a/instrumentation/elasticsearch/elasticsearch-rest-7.0/library/src/main/java/io/opentelemetry/instrumentation/elasticsearch/rest/v7_0/RestClientWrapper.java b/instrumentation/elasticsearch/elasticsearch-rest-7.0/library/src/main/java/io/opentelemetry/instrumentation/elasticsearch/rest/v7_0/RestClientWrapper.java new file mode 100644 index 000000000000..9c5d39be5ea5 --- /dev/null +++ b/instrumentation/elasticsearch/elasticsearch-rest-7.0/library/src/main/java/io/opentelemetry/instrumentation/elasticsearch/rest/v7_0/RestClientWrapper.java @@ -0,0 +1,200 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.elasticsearch.rest.v7_0; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import io.opentelemetry.instrumentation.elasticsearch.rest.internal.ElasticsearchRestRequest; +import io.opentelemetry.instrumentation.elasticsearch.rest.internal.RestResponseListener; +import java.lang.reflect.Array; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.util.List; +import java.util.function.Function; +import java.util.function.Supplier; +import net.bytebuddy.ByteBuddy; +import net.bytebuddy.description.modifier.Visibility; +import net.bytebuddy.dynamic.loading.ClassLoadingStrategy; +import net.bytebuddy.implementation.InvocationHandlerAdapter; +import net.bytebuddy.matcher.ElementMatchers; +import org.apache.http.Header; +import org.elasticsearch.client.Node; +import org.elasticsearch.client.Request; +import org.elasticsearch.client.Response; +import org.elasticsearch.client.ResponseListener; +import org.elasticsearch.client.RestClient; + +class RestClientWrapper { + private static final Class proxyClass = createProxyClass(); + private static final Field targetField = getTargetField(proxyClass); + private static final Field instrumenterSupplierField = getInstrumenterSupplierField(proxyClass); + private static final Function proxyFactory = getProxyFactory(proxyClass); + + private static Class createProxyClass() { + return new ByteBuddy() + .subclass(RestClient.class) + .defineField("target", RestClient.class, Visibility.PUBLIC) + // using Supplier instead of Instrumenter in case RestClientWrapper and opentelemetry apis + // are in a child class loader of RestClient's class loader and Instrumenter is not visible + // for RestClient + .defineField("instrumenterSupplier", Supplier.class, Visibility.PUBLIC) + .method(ElementMatchers.any()) + .intercept( + InvocationHandlerAdapter.of( + (proxy, method, args) -> { + RestClient target = (RestClient) targetField.get(proxy); + Instrumenter instrumenter = + getInstrumenter(proxy); + // target is null when running proxy constructor + if (target == null || instrumenter == null) { + return null; + } + + // instrument performRequest and performRequestAsync methods + if ("performRequest".equals(method.getName()) + && args.length == 1 + && args[0] instanceof Request + && Response.class == method.getReturnType()) { + Request request = (Request) args[0]; + Context parentContext = Context.current(); + ElasticsearchRestRequest otelRequest = + ElasticsearchRestRequest.create(request.getMethod(), request.getEndpoint()); + if (!instrumenter.shouldStart(parentContext, otelRequest)) { + return method.invoke(target, args); + } + + Response response = null; + Throwable throwable = null; + Context context = instrumenter.start(parentContext, otelRequest); + try (Scope scope = context.makeCurrent()) { + response = (Response) method.invoke(target, args); + } catch (Throwable exception) { + throwable = exception; + throw throwable; + } finally { + instrumenter.end(context, otelRequest, response, throwable); + } + + return response; + } else if ("performRequestAsync".equals(method.getName()) + && args.length == 2 + && args[0] instanceof Request + && args[1] instanceof ResponseListener) { + + Request request = (Request) args[0]; + ResponseListener responseListener = (ResponseListener) args[1]; + Context parentContext = Context.current(); + ElasticsearchRestRequest otelRequest = + ElasticsearchRestRequest.create(request.getMethod(), request.getEndpoint()); + if (!instrumenter.shouldStart(parentContext, otelRequest)) { + return method.invoke(target, args); + } + + Throwable throwable = null; + Context context = instrumenter.start(parentContext, otelRequest); + args[1] = + new RestResponseListener( + responseListener, parentContext, instrumenter, context, otelRequest); + try (Scope scope = context.makeCurrent()) { + return method.invoke(target, args); + } catch (Throwable exception) { + throwable = exception; + throw throwable; + } finally { + if (throwable != null) { + instrumenter.end(context, otelRequest, null, throwable); + } + // span ended in RestResponseListener + } + } + + // delegate to wrapped RestClient + return method.invoke(target, args); + })) + .make() + .load(RestClient.class.getClassLoader(), ClassLoadingStrategy.Default.INJECTION) + .getLoaded(); + } + + private static Field getTargetField(Class clazz) { + return getProxyField(clazz, "target"); + } + + private static Field getInstrumenterSupplierField(Class clazz) { + return getProxyField(clazz, "instrumenterSupplier"); + } + + private static Field getProxyField(Class clazz, String fieldName) { + try { + return clazz.getDeclaredField(fieldName); + } catch (NoSuchFieldException exception) { + throw new IllegalStateException("Could not find proxy field", exception); + } + } + + @SuppressWarnings("unchecked") + private static Instrumenter getInstrumenter(Object proxy) + throws IllegalAccessException { + Supplier> supplier = + (Supplier>) + instrumenterSupplierField.get(proxy); + return supplier != null ? supplier.get() : null; + } + + private static Function getProxyFactory(Class clazz) { + for (Constructor constructor : clazz.getDeclaredConstructors()) { + Class[] parameterTypes = constructor.getParameterTypes(); + if (parameterTypes.length >= 3 + && !parameterTypes[0].isPrimitive() + && parameterTypes[1] == Header[].class + && parameterTypes[2] == List.class) { + return restClient -> { + List nodes = restClient.getNodes(); + // all the proxy methods will delegate to the wrapped RestClient, we need to fill only the + // arguments that are required by the constructor + Object[] arguments = new Object[parameterTypes.length]; + arguments[1] = new Header[0]; + arguments[2] = nodes; + for (int i = 3; i < parameterTypes.length; i++) { + if (parameterTypes[i].isPrimitive()) { + arguments[i] = getDefaultValue(parameterTypes[i]); + } + } + try { + return (RestClient) constructor.newInstance(arguments); + } catch (Exception exception) { + throw new IllegalStateException("Failed to construct proxy instance", exception); + } + }; + } + } + throw new IllegalStateException("Failed to find suitable constructor"); + } + + // create a single element array of given type, this method is used to get the default value of + // a primitive type + @SuppressWarnings("unchecked") + private static T getDefaultValue(Class clazz) { + return (T) Array.get(Array.newInstance(clazz, 1), 0); + } + + static RestClient wrap( + RestClient restClient, Instrumenter instrumenter) { + RestClient wrapped = proxyFactory.apply(restClient); + try { + // set wrapped RestClient instance and the instrumenter on the proxy + targetField.set(wrapped, restClient); + instrumenterSupplierField.set( + wrapped, (Supplier>) () -> instrumenter); + return wrapped; + } catch (Exception exception) { + throw new IllegalStateException("Failed to construct proxy instance", exception); + } + } + + private RestClientWrapper() {} +} diff --git a/instrumentation/elasticsearch/elasticsearch-rest-7.0/library/src/test/java/io/opentelemetry/instrumentation/elasticsearch/rest/v7_0/ElasticsearchRest7Test.java b/instrumentation/elasticsearch/elasticsearch-rest-7.0/library/src/test/java/io/opentelemetry/instrumentation/elasticsearch/rest/v7_0/ElasticsearchRest7Test.java new file mode 100644 index 000000000000..09e74606b735 --- /dev/null +++ b/instrumentation/elasticsearch/elasticsearch-rest-7.0/library/src/test/java/io/opentelemetry/instrumentation/elasticsearch/rest/v7_0/ElasticsearchRest7Test.java @@ -0,0 +1,179 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.elasticsearch.rest.v7_0; + +import static io.opentelemetry.instrumentation.testing.GlobalTraceUtil.runWithSpan; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; + +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.LibraryInstrumentationExtension; +import io.opentelemetry.semconv.HttpAttributes; +import io.opentelemetry.semconv.ServerAttributes; +import io.opentelemetry.semconv.UrlAttributes; +import io.opentelemetry.semconv.incubating.DbIncubatingAttributes; +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import org.apache.http.HttpHost; +import org.elasticsearch.client.Request; +import org.elasticsearch.client.Response; +import org.elasticsearch.client.ResponseListener; +import org.elasticsearch.client.RestClient; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.testcontainers.elasticsearch.ElasticsearchContainer; +import org.testcontainers.shaded.com.fasterxml.jackson.databind.ObjectMapper; + +class ElasticsearchRest7Test { + @RegisterExtension + static final InstrumentationExtension testing = LibraryInstrumentationExtension.create(); + + static ElasticsearchContainer elasticsearch; + + static HttpHost httpHost; + + static RestClient client; + + static ObjectMapper objectMapper; + + @BeforeAll + static void setUp() { + elasticsearch = + new ElasticsearchContainer("docker.elastic.co/elasticsearch/elasticsearch-oss:7.10.2"); + // limit memory usage + elasticsearch.withEnv("ES_JAVA_OPTS", "-Xmx256m -Xms256m"); + elasticsearch.start(); + + httpHost = HttpHost.create(elasticsearch.getHttpHostAddress()); + + client = + RestClient.builder(httpHost) + .setRequestConfigCallback( + builder -> + builder + .setConnectTimeout(Integer.MAX_VALUE) + .setSocketTimeout(Integer.MAX_VALUE)) + .build(); + client = ElasticsearchRest7Telemetry.create(testing.getOpenTelemetry()).wrap(client); + + objectMapper = new ObjectMapper(); + } + + @AfterAll + static void cleanUp() { + elasticsearch.stop(); + } + + @Test + public void elasticsearchStatus() throws Exception { + Response response = client.performRequest(new Request("GET", "_cluster/health")); + Map result = objectMapper.readValue(response.getEntity().getContent(), Map.class); + Assertions.assertEquals(result.get("status"), "green"); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("GET") + .hasKind(SpanKind.CLIENT) + .hasNoParent() + .hasAttributesSatisfyingExactly( + equalTo(DbIncubatingAttributes.DB_SYSTEM, "elasticsearch"), + equalTo(HttpAttributes.HTTP_REQUEST_METHOD, "GET"), + equalTo(ServerAttributes.SERVER_ADDRESS, httpHost.getHostName()), + equalTo(ServerAttributes.SERVER_PORT, httpHost.getPort()), + equalTo( + UrlAttributes.URL_FULL, httpHost.toURI() + "/_cluster/health")))); + } + + @Test + public void elasticsearchStatusAsync() throws Exception { + AsyncRequest asyncRequest = new AsyncRequest(); + CountDownLatch countDownLatch = new CountDownLatch(1); + ResponseListener responseListener = + new ResponseListener() { + @Override + public void onSuccess(Response response) { + + runWithSpan( + "callback", + () -> { + asyncRequest.setRequestResponse(response); + countDownLatch.countDown(); + }); + } + + @Override + public void onFailure(Exception e) { + runWithSpan( + "callback", + () -> { + asyncRequest.setException(e); + countDownLatch.countDown(); + }); + } + }; + + runWithSpan( + "parent", + () -> client.performRequestAsync(new Request("GET", "_cluster/health"), responseListener)); + //noinspection ResultOfMethodCallIgnored + countDownLatch.await(10, TimeUnit.SECONDS); + + if (asyncRequest.getException() != null) { + throw asyncRequest.getException(); + } + + Map result = + objectMapper.readValue( + asyncRequest.getRequestResponse().getEntity().getContent(), Map.class); + Assertions.assertEquals(result.get("status"), "green"); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(), + span -> + span.hasName("GET") + .hasKind(SpanKind.CLIENT) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo(DbIncubatingAttributes.DB_SYSTEM, "elasticsearch"), + equalTo(HttpAttributes.HTTP_REQUEST_METHOD, "GET"), + equalTo(ServerAttributes.SERVER_ADDRESS, httpHost.getHostName()), + equalTo(ServerAttributes.SERVER_PORT, httpHost.getPort()), + equalTo(UrlAttributes.URL_FULL, httpHost.toURI() + "/_cluster/health")), + span -> + span.hasName("callback") + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(0)))); + } + + private static class AsyncRequest { + volatile Response requestResponse = null; + volatile Exception exception = null; + + public Response getRequestResponse() { + return requestResponse; + } + + public void setRequestResponse(Response requestResponse) { + this.requestResponse = requestResponse; + } + + public Exception getException() { + return exception; + } + + public void setException(Exception exception) { + this.exception = exception; + } + } +} diff --git a/instrumentation/elasticsearch/elasticsearch-rest-common/javaagent/build.gradle.kts b/instrumentation/elasticsearch/elasticsearch-rest-common/javaagent/build.gradle.kts index 427b317c98ba..47bfbd6c00ee 100644 --- a/instrumentation/elasticsearch/elasticsearch-rest-common/javaagent/build.gradle.kts +++ b/instrumentation/elasticsearch/elasticsearch-rest-common/javaagent/build.gradle.kts @@ -4,7 +4,6 @@ plugins { dependencies { compileOnly("org.elasticsearch.client:rest:5.0.0") - compileOnly("com.google.auto.value:auto-value-annotations") - annotationProcessor("com.google.auto.value:auto-value") + api(project(":instrumentation:elasticsearch:elasticsearch-rest-common:library")) } diff --git a/instrumentation/elasticsearch/elasticsearch-rest-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/rest/ElasticsearchRestAttributesGetter.java b/instrumentation/elasticsearch/elasticsearch-rest-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/rest/ElasticsearchRestAttributesGetter.java deleted file mode 100644 index e6ae243108c8..000000000000 --- a/instrumentation/elasticsearch/elasticsearch-rest-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/rest/ElasticsearchRestAttributesGetter.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.javaagent.instrumentation.elasticsearch.rest; - -import io.opentelemetry.instrumentation.api.instrumenter.db.DbClientAttributesGetter; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; -import javax.annotation.Nullable; - -final class ElasticsearchRestAttributesGetter - implements DbClientAttributesGetter { - - @Override - public String getSystem(ElasticsearchRestRequest request) { - return SemanticAttributes.DbSystemValues.ELASTICSEARCH; - } - - @Override - @Nullable - public String getUser(ElasticsearchRestRequest request) { - return null; - } - - @Override - @Nullable - public String getName(ElasticsearchRestRequest request) { - return null; - } - - @Override - @Nullable - public String getConnectionString(ElasticsearchRestRequest request) { - return null; - } - - @Override - @Nullable - public String getStatement(ElasticsearchRestRequest request) { - return request.getMethod() + " " + request.getOperation(); - } - - @Override - @Nullable - public String getOperation(ElasticsearchRestRequest request) { - return request.getMethod(); - } -} diff --git a/instrumentation/elasticsearch/elasticsearch-rest-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/rest/ElasticsearchRestInstrumenterFactory.java b/instrumentation/elasticsearch/elasticsearch-rest-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/rest/ElasticsearchRestInstrumenterFactory.java deleted file mode 100644 index e624a3a2d388..000000000000 --- a/instrumentation/elasticsearch/elasticsearch-rest-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/rest/ElasticsearchRestInstrumenterFactory.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.javaagent.instrumentation.elasticsearch.rest; - -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.DbClientAttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.db.DbClientSpanNameExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.net.NetClientAttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.net.PeerServiceAttributesExtractor; -import io.opentelemetry.javaagent.bootstrap.internal.CommonConfig; -import org.elasticsearch.client.Response; - -public final class ElasticsearchRestInstrumenterFactory { - - public static Instrumenter create( - String instrumentationName) { - ElasticsearchRestAttributesGetter dbClientAttributesGetter = - new ElasticsearchRestAttributesGetter(); - ElasticsearchRestNetResponseAttributesGetter netAttributesGetter = - new ElasticsearchRestNetResponseAttributesGetter(); - - return Instrumenter.builder( - GlobalOpenTelemetry.get(), - instrumentationName, - DbClientSpanNameExtractor.create(dbClientAttributesGetter)) - .addAttributesExtractor(DbClientAttributesExtractor.create(dbClientAttributesGetter)) - .addAttributesExtractor(NetClientAttributesExtractor.create(netAttributesGetter)) - .addAttributesExtractor( - PeerServiceAttributesExtractor.create( - netAttributesGetter, CommonConfig.get().getPeerServiceMapping())) - .buildInstrumenter(SpanKindExtractor.alwaysClient()); - } - - private ElasticsearchRestInstrumenterFactory() {} -} diff --git a/instrumentation/elasticsearch/elasticsearch-rest-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/rest/ElasticsearchRestJavaagentInstrumenterFactory.java b/instrumentation/elasticsearch/elasticsearch-rest-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/rest/ElasticsearchRestJavaagentInstrumenterFactory.java new file mode 100644 index 000000000000..39682ce28a95 --- /dev/null +++ b/instrumentation/elasticsearch/elasticsearch-rest-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/rest/ElasticsearchRestJavaagentInstrumenterFactory.java @@ -0,0 +1,36 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.elasticsearch.rest; + +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import io.opentelemetry.instrumentation.elasticsearch.rest.internal.ElasticsearchRestInstrumenterFactory; +import io.opentelemetry.instrumentation.elasticsearch.rest.internal.ElasticsearchRestRequest; +import io.opentelemetry.javaagent.bootstrap.internal.AgentCommonConfig; +import io.opentelemetry.javaagent.bootstrap.internal.AgentInstrumentationConfig; +import java.util.Collections; +import java.util.function.Function; +import org.elasticsearch.client.Response; + +public final class ElasticsearchRestJavaagentInstrumenterFactory { + + private static final boolean CAPTURE_SEARCH_QUERY = + AgentInstrumentationConfig.get() + .getBoolean("otel.instrumentation.elasticsearch.capture-search-query", false); + + private ElasticsearchRestJavaagentInstrumenterFactory() {} + + public static Instrumenter create( + String instrumentationName) { + return ElasticsearchRestInstrumenterFactory.create( + GlobalOpenTelemetry.get(), + instrumentationName, + Collections.emptyList(), + Function.identity(), + AgentCommonConfig.get().getKnownHttpRequestMethods(), + CAPTURE_SEARCH_QUERY); + } +} diff --git a/instrumentation/elasticsearch/elasticsearch-rest-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/rest/ElasticsearchRestNetResponseAttributesGetter.java b/instrumentation/elasticsearch/elasticsearch-rest-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/rest/ElasticsearchRestNetResponseAttributesGetter.java deleted file mode 100644 index 7716a2b8562c..000000000000 --- a/instrumentation/elasticsearch/elasticsearch-rest-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/rest/ElasticsearchRestNetResponseAttributesGetter.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.javaagent.instrumentation.elasticsearch.rest; - -import io.opentelemetry.instrumentation.api.instrumenter.net.NetClientAttributesGetter; -import java.net.Inet6Address; -import javax.annotation.Nullable; -import org.elasticsearch.client.Response; - -final class ElasticsearchRestNetResponseAttributesGetter - implements NetClientAttributesGetter { - - @Override - @Nullable - public String getServerAddress(ElasticsearchRestRequest request) { - return null; - } - - @Override - @Nullable - public Integer getServerPort(ElasticsearchRestRequest request) { - return null; - } - - @Nullable - @Override - public String getSockFamily( - ElasticsearchRestRequest elasticsearchRestRequest, @Nullable Response response) { - if (response != null && response.getHost().getAddress() instanceof Inet6Address) { - return "inet6"; - } - return null; - } - - @Override - @Nullable - public String getServerSocketAddress( - ElasticsearchRestRequest request, @Nullable Response response) { - if (response != null && response.getHost().getAddress() != null) { - return response.getHost().getAddress().getHostAddress(); - } - return null; - } -} diff --git a/instrumentation/elasticsearch/elasticsearch-rest-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/rest/ElasticsearchRestRequest.java b/instrumentation/elasticsearch/elasticsearch-rest-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/rest/ElasticsearchRestRequest.java deleted file mode 100644 index 3298c1a40dc7..000000000000 --- a/instrumentation/elasticsearch/elasticsearch-rest-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/rest/ElasticsearchRestRequest.java +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.javaagent.instrumentation.elasticsearch.rest; - -import com.google.auto.value.AutoValue; - -@AutoValue -public abstract class ElasticsearchRestRequest { - - public static ElasticsearchRestRequest create(String method, String endpoint) { - return new AutoValue_ElasticsearchRestRequest(method, endpoint); - } - - public abstract String getMethod(); - - public abstract String getOperation(); -} diff --git a/instrumentation/elasticsearch/elasticsearch-rest-common/library/build.gradle.kts b/instrumentation/elasticsearch/elasticsearch-rest-common/library/build.gradle.kts new file mode 100644 index 000000000000..667d2ae3ae34 --- /dev/null +++ b/instrumentation/elasticsearch/elasticsearch-rest-common/library/build.gradle.kts @@ -0,0 +1,10 @@ +plugins { + id("otel.library-instrumentation") +} + +dependencies { + compileOnly("org.elasticsearch.client:rest:5.0.0") + compileOnly("com.google.auto.value:auto-value-annotations") + + annotationProcessor("com.google.auto.value:auto-value") +} diff --git a/instrumentation/elasticsearch/elasticsearch-rest-common/library/src/main/java/io/opentelemetry/instrumentation/elasticsearch/rest/internal/ElasticsearchClientAttributeExtractor.java b/instrumentation/elasticsearch/elasticsearch-rest-common/library/src/main/java/io/opentelemetry/instrumentation/elasticsearch/rest/internal/ElasticsearchClientAttributeExtractor.java new file mode 100644 index 000000000000..4218609df6cc --- /dev/null +++ b/instrumentation/elasticsearch/elasticsearch-rest-common/library/src/main/java/io/opentelemetry/instrumentation/elasticsearch/rest/internal/ElasticsearchClientAttributeExtractor.java @@ -0,0 +1,101 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.elasticsearch.rest.internal; + +import static io.opentelemetry.instrumentation.api.internal.AttributesExtractorUtil.internalSet; +import static io.opentelemetry.instrumentation.api.internal.HttpConstants._OTHER; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; +import io.opentelemetry.instrumentation.api.internal.cache.Cache; +import io.opentelemetry.semconv.HttpAttributes; +import io.opentelemetry.semconv.ServerAttributes; +import io.opentelemetry.semconv.UrlAttributes; +import java.util.HashSet; +import java.util.Set; +import javax.annotation.Nullable; +import org.apache.http.HttpHost; +import org.elasticsearch.client.Response; + +/** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time. + */ +public class ElasticsearchClientAttributeExtractor + implements AttributesExtractor { + + private static final String PATH_PARTS_ATTRIBUTE_PREFIX = "db.elasticsearch.path_parts."; + + private static final Cache> pathPartKeysCache = Cache.bounded(64); + + private final Set knownMethods; + + ElasticsearchClientAttributeExtractor(Set knownMethods) { + this.knownMethods = new HashSet<>(knownMethods); + } + + private static void setServerAttributes(AttributesBuilder attributes, Response response) { + HttpHost host = response.getHost(); + if (host != null) { + internalSet(attributes, ServerAttributes.SERVER_ADDRESS, host.getHostName()); + internalSet(attributes, ServerAttributes.SERVER_PORT, (long) host.getPort()); + } + } + + private static void setUrlAttribute(AttributesBuilder attributes, Response response) { + String uri = response.getRequestLine().getUri(); + uri = uri.startsWith("/") ? uri : "/" + uri; + String fullUrl = response.getHost().toURI() + uri; + + internalSet(attributes, UrlAttributes.URL_FULL, fullUrl); + } + + private static void setPathPartsAttributes( + AttributesBuilder attributes, ElasticsearchRestRequest request) { + ElasticsearchEndpointDefinition endpointDef = request.getEndpointDefinition(); + if (endpointDef == null) { + return; + } + + endpointDef.processPathParts( + request.getEndpoint(), + (key, value) -> { + AttributeKey attributeKey = + pathPartKeysCache.computeIfAbsent( + key, k -> AttributeKey.stringKey(PATH_PARTS_ATTRIBUTE_PREFIX + k)); + internalSet(attributes, attributeKey, value); + }); + } + + @Override + public void onStart( + AttributesBuilder attributes, Context parentContext, ElasticsearchRestRequest request) { + String method = request.getMethod(); + if (method == null || knownMethods.contains(method)) { + internalSet(attributes, HttpAttributes.HTTP_REQUEST_METHOD, method); + } else { + internalSet(attributes, HttpAttributes.HTTP_REQUEST_METHOD, _OTHER); + internalSet(attributes, HttpAttributes.HTTP_REQUEST_METHOD_ORIGINAL, method); + } + setPathPartsAttributes(attributes, request); + } + + @Override + public void onEnd( + AttributesBuilder attributes, + Context context, + ElasticsearchRestRequest request, + @Nullable Response response, + @Nullable Throwable error) { + + if (response != null) { + setUrlAttribute(attributes, response); + setServerAttributes(attributes, response); + } + } +} diff --git a/instrumentation/elasticsearch/elasticsearch-rest-common/library/src/main/java/io/opentelemetry/instrumentation/elasticsearch/rest/internal/ElasticsearchDbAttributesGetter.java b/instrumentation/elasticsearch/elasticsearch-rest-common/library/src/main/java/io/opentelemetry/instrumentation/elasticsearch/rest/internal/ElasticsearchDbAttributesGetter.java new file mode 100644 index 000000000000..22bc0374d444 --- /dev/null +++ b/instrumentation/elasticsearch/elasticsearch-rest-common/library/src/main/java/io/opentelemetry/instrumentation/elasticsearch/rest/internal/ElasticsearchDbAttributesGetter.java @@ -0,0 +1,92 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.elasticsearch.rest.internal; + +import static java.util.logging.Level.FINE; + +import io.opentelemetry.instrumentation.api.incubator.semconv.db.DbClientAttributesGetter; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.logging.Logger; +import java.util.stream.Collectors; +import javax.annotation.Nullable; +import org.apache.http.HttpEntity; + +/** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time. + */ +final class ElasticsearchDbAttributesGetter + implements DbClientAttributesGetter { + + private static final Logger logger = + Logger.getLogger(ElasticsearchDbAttributesGetter.class.getName()); + + // copied from DbIncubatingAttributes.DbSystemValues + private static final String ELASTICSEARCH = "elasticsearch"; + + private final boolean captureSearchQuery; + + ElasticsearchDbAttributesGetter(boolean captureSearchQuery) { + this.captureSearchQuery = captureSearchQuery; + } + + @Override + public String getSystem(ElasticsearchRestRequest request) { + return ELASTICSEARCH; + } + + @Override + @Nullable + public String getUser(ElasticsearchRestRequest request) { + return null; + } + + @Override + @Nullable + public String getName(ElasticsearchRestRequest request) { + return null; + } + + @Override + @Nullable + public String getConnectionString(ElasticsearchRestRequest request) { + return null; + } + + @Override + @Nullable + public String getStatement(ElasticsearchRestRequest request) { + ElasticsearchEndpointDefinition epDefinition = request.getEndpointDefinition(); + HttpEntity httpEntity = request.getHttpEntity(); + if (captureSearchQuery + && epDefinition != null + && epDefinition.isSearchEndpoint() + && httpEntity != null + && httpEntity.isRepeatable()) { + // Retrieve HTTP body for search-type Elasticsearch requests when CAPTURE_SEARCH_QUERY is + // enabled. + try { + return new BufferedReader( + new InputStreamReader(httpEntity.getContent(), StandardCharsets.UTF_8)) + .lines() + .collect(Collectors.joining()); + } catch (IOException e) { + logger.log(FINE, "Failed reading HTTP body content.", e); + } + } + return null; + } + + @Override + @Nullable + public String getOperation(ElasticsearchRestRequest request) { + ElasticsearchEndpointDefinition endpointDefinition = request.getEndpointDefinition(); + return endpointDefinition != null ? endpointDefinition.getEndpointName() : null; + } +} diff --git a/instrumentation/elasticsearch/elasticsearch-rest-common/library/src/main/java/io/opentelemetry/instrumentation/elasticsearch/rest/internal/ElasticsearchEndpointDefinition.java b/instrumentation/elasticsearch/elasticsearch-rest-common/library/src/main/java/io/opentelemetry/instrumentation/elasticsearch/rest/internal/ElasticsearchEndpointDefinition.java new file mode 100644 index 000000000000..a3ac1c94288b --- /dev/null +++ b/instrumentation/elasticsearch/elasticsearch-rest-common/library/src/main/java/io/opentelemetry/instrumentation/elasticsearch/rest/internal/ElasticsearchEndpointDefinition.java @@ -0,0 +1,191 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.elasticsearch.rest.internal; + +import static java.util.Collections.unmodifiableList; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.function.BiConsumer; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import javax.annotation.Nullable; + +/** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time. + */ +public final class ElasticsearchEndpointDefinition { + + private static final String UNDERSCORE_REPLACEMENT = "0"; + + private final String endpointName; + private final List routes; + + private final boolean isSearchEndpoint; + + public ElasticsearchEndpointDefinition( + String endpointName, String[] routes, boolean isSearchEndpoint) { + this.endpointName = endpointName; + this.routes = + unmodifiableList(Arrays.stream(routes).map(Route::new).collect(Collectors.toList())); + this.isSearchEndpoint = isSearchEndpoint; + } + + @Nullable + public String getEndpointName() { + return endpointName; + } + + public boolean isSearchEndpoint() { + return isSearchEndpoint; + } + + public void processPathParts(String urlPath, BiConsumer consumer) { + for (Route route : routes) { + if (route.hasParameters()) { + Matcher matcher = route.createMatcher(urlPath); + if (matcher.find()) { + for (String key : route.getPathPartNames()) { + String value = matcher.group(key); + if (key.contains(UNDERSCORE_REPLACEMENT)) { + // replace underscore back + key = key.replace(UNDERSCORE_REPLACEMENT, "_"); + } + consumer.accept(key, value); + } + return; + } + } + } + } + + public List getRoutes() { + return routes; + } + + /** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time. + */ + // Visible for testing + public static final class Route { + private final String name; + private final boolean hasParameters; + + private volatile EndpointPattern epPattern; + + public Route(String name) { + this.name = name; + this.hasParameters = name.contains("{") && name.contains("}"); + } + + public String getName() { + return name; + } + + boolean hasParameters() { + return hasParameters; + } + + List getPathPartNames() { + return getEndpointPattern().getPathPartNames(); + } + + Matcher createMatcher(String urlPath) { + return getEndpointPattern().getPattern().matcher(urlPath); + } + + private EndpointPattern getEndpointPattern() { + // Intentionally NOT synchronizing here to avoid synchronization overhead. + // Main purpose here is to cache the pattern without the need for strict thread-safety. + if (epPattern == null) { + epPattern = new EndpointPattern(this); + } + + return epPattern; + } + } + + /** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time. + */ + // Visible for testing + public static final class EndpointPattern { + private static final Pattern PATH_PART_NAMES_PATTERN = Pattern.compile("\\{([^}]+)}"); + private final Pattern pattern; + private final List pathPartNames; + + /** + * Creates, compiles and caches a regular expression pattern and retrieves a set of + * pathPartNames (names of the URL path parameters) for this route. + * + *

The regex pattern is later being used to match against a URL path to retrieve the URL path + * parameters for that route pattern using named regex capture groups. + */ + private EndpointPattern(Route route) { + pattern = buildRegexPattern(route.getName()); + + if (route.hasParameters()) { + pathPartNames = new ArrayList<>(); + Matcher matcher = PATH_PART_NAMES_PATTERN.matcher(route.getName()); + while (matcher.find()) { + String groupName = matcher.group(1); + + if (groupName != null) { + groupName = groupName.replace("_", UNDERSCORE_REPLACEMENT); + pathPartNames.add(groupName); + } + } + } else { + pathPartNames = Collections.emptyList(); + } + } + + /** Builds a regex pattern from the parameterized route pattern. */ + public static Pattern buildRegexPattern(String routeStr) { + StringBuilder regexStr = new StringBuilder(); + regexStr.append('^'); + int startIdx = routeStr.indexOf("{"); + while (startIdx >= 0) { + regexStr.append(routeStr.substring(0, startIdx)); + + int endIndex = routeStr.indexOf("}"); + if (endIndex <= startIdx + 1) { + break; + } + + // Append named capture group. + // If group name contains an underscore `_` it is being replaced with `0`, + // because `_` is not allowed in capture group names. + regexStr.append("(?<"); + regexStr.append( + routeStr.substring(startIdx + 1, endIndex).replace("_", UNDERSCORE_REPLACEMENT)); + regexStr.append(">[^/]+)"); + + routeStr = routeStr.substring(endIndex + 1); + startIdx = routeStr.indexOf("{"); + } + + regexStr.append(routeStr); + regexStr.append('$'); + + return Pattern.compile(regexStr.toString()); + } + + Pattern getPattern() { + return pattern; + } + + List getPathPartNames() { + return pathPartNames; + } + } +} diff --git a/instrumentation/elasticsearch/elasticsearch-rest-common/library/src/main/java/io/opentelemetry/instrumentation/elasticsearch/rest/internal/ElasticsearchRestInstrumenterFactory.java b/instrumentation/elasticsearch/elasticsearch-rest-common/library/src/main/java/io/opentelemetry/instrumentation/elasticsearch/rest/internal/ElasticsearchRestInstrumenterFactory.java new file mode 100644 index 000000000000..75feafca224c --- /dev/null +++ b/instrumentation/elasticsearch/elasticsearch-rest-common/library/src/main/java/io/opentelemetry/instrumentation/elasticsearch/rest/internal/ElasticsearchRestInstrumenterFactory.java @@ -0,0 +1,52 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.elasticsearch.rest.internal; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.instrumentation.api.incubator.semconv.db.DbClientAttributesExtractor; +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; +import java.util.List; +import java.util.Set; +import java.util.function.Function; +import org.elasticsearch.client.Response; + +/** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time. + */ +public final class ElasticsearchRestInstrumenterFactory { + + private ElasticsearchRestInstrumenterFactory() {} + + public static Instrumenter create( + OpenTelemetry openTelemetry, + String instrumentationName, + List> attributesExtractors, + Function< + SpanNameExtractor, + ? extends SpanNameExtractor> + spanNameExtractorTransformer, + Set knownMethods, + boolean captureSearchQuery) { + ElasticsearchDbAttributesGetter dbClientAttributesGetter = + new ElasticsearchDbAttributesGetter(captureSearchQuery); + ElasticsearchClientAttributeExtractor esClientAtrributesExtractor = + new ElasticsearchClientAttributeExtractor(knownMethods); + SpanNameExtractor spanNameExtractor = + spanNameExtractorTransformer.apply( + new ElasticsearchSpanNameExtractor(dbClientAttributesGetter)); + + return Instrumenter.builder( + openTelemetry, instrumentationName, spanNameExtractor) + .addAttributesExtractor(DbClientAttributesExtractor.create(dbClientAttributesGetter)) + .addAttributesExtractor(esClientAtrributesExtractor) + .addAttributesExtractors(attributesExtractors) + .buildInstrumenter(SpanKindExtractor.alwaysClient()); + } +} diff --git a/instrumentation/elasticsearch/elasticsearch-rest-common/library/src/main/java/io/opentelemetry/instrumentation/elasticsearch/rest/internal/ElasticsearchRestRequest.java b/instrumentation/elasticsearch/elasticsearch-rest-common/library/src/main/java/io/opentelemetry/instrumentation/elasticsearch/rest/internal/ElasticsearchRestRequest.java new file mode 100644 index 000000000000..4630cc56a4b5 --- /dev/null +++ b/instrumentation/elasticsearch/elasticsearch-rest-common/library/src/main/java/io/opentelemetry/instrumentation/elasticsearch/rest/internal/ElasticsearchRestRequest.java @@ -0,0 +1,40 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.elasticsearch.rest.internal; + +import com.google.auto.value.AutoValue; +import javax.annotation.Nullable; +import org.apache.http.HttpEntity; + +/** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time. + */ +@AutoValue +public abstract class ElasticsearchRestRequest { + + public static ElasticsearchRestRequest create(String method, String endpoint) { + return create(method, endpoint, null, null); + } + + public static ElasticsearchRestRequest create( + String method, + String endpoint, + @Nullable ElasticsearchEndpointDefinition endpointDefinition, + @Nullable HttpEntity httpEntity) { + return new AutoValue_ElasticsearchRestRequest(method, endpoint, endpointDefinition, httpEntity); + } + + public abstract String getMethod(); + + public abstract String getEndpoint(); + + @Nullable + public abstract ElasticsearchEndpointDefinition getEndpointDefinition(); + + @Nullable + public abstract HttpEntity getHttpEntity(); +} diff --git a/instrumentation/elasticsearch/elasticsearch-rest-common/library/src/main/java/io/opentelemetry/instrumentation/elasticsearch/rest/internal/ElasticsearchSpanNameExtractor.java b/instrumentation/elasticsearch/elasticsearch-rest-common/library/src/main/java/io/opentelemetry/instrumentation/elasticsearch/rest/internal/ElasticsearchSpanNameExtractor.java new file mode 100644 index 000000000000..1faf90ee33e2 --- /dev/null +++ b/instrumentation/elasticsearch/elasticsearch-rest-common/library/src/main/java/io/opentelemetry/instrumentation/elasticsearch/rest/internal/ElasticsearchSpanNameExtractor.java @@ -0,0 +1,27 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.elasticsearch.rest.internal; + +import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor; + +/** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time. + */ +public class ElasticsearchSpanNameExtractor implements SpanNameExtractor { + + private final ElasticsearchDbAttributesGetter dbAttributesGetter; + + public ElasticsearchSpanNameExtractor(ElasticsearchDbAttributesGetter dbAttributesGetter) { + this.dbAttributesGetter = dbAttributesGetter; + } + + @Override + public String extract(ElasticsearchRestRequest elasticsearchRestRequest) { + String name = dbAttributesGetter.getOperation(elasticsearchRestRequest); + return name != null ? name : elasticsearchRestRequest.getMethod(); + } +} diff --git a/instrumentation/elasticsearch/elasticsearch-rest-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/rest/RestResponseListener.java b/instrumentation/elasticsearch/elasticsearch-rest-common/library/src/main/java/io/opentelemetry/instrumentation/elasticsearch/rest/internal/RestResponseListener.java similarity index 88% rename from instrumentation/elasticsearch/elasticsearch-rest-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/rest/RestResponseListener.java rename to instrumentation/elasticsearch/elasticsearch-rest-common/library/src/main/java/io/opentelemetry/instrumentation/elasticsearch/rest/internal/RestResponseListener.java index ba29b49d57f1..710ab7cc63f6 100644 --- a/instrumentation/elasticsearch/elasticsearch-rest-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/rest/RestResponseListener.java +++ b/instrumentation/elasticsearch/elasticsearch-rest-common/library/src/main/java/io/opentelemetry/instrumentation/elasticsearch/rest/internal/RestResponseListener.java @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.javaagent.instrumentation.elasticsearch.rest; +package io.opentelemetry.instrumentation.elasticsearch.rest.internal; import io.opentelemetry.context.Context; import io.opentelemetry.context.Scope; @@ -11,6 +11,10 @@ import org.elasticsearch.client.Response; import org.elasticsearch.client.ResponseListener; +/** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time. + */ public final class RestResponseListener implements ResponseListener { private final ResponseListener listener; diff --git a/instrumentation/elasticsearch/elasticsearch-transport-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/transport/v5_0/Elasticsearch5TransportSingletons.java b/instrumentation/elasticsearch/elasticsearch-transport-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/transport/v5_0/Elasticsearch5TransportSingletons.java index 7980678389ef..2b5d953d5b0e 100644 --- a/instrumentation/elasticsearch/elasticsearch-transport-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/transport/v5_0/Elasticsearch5TransportSingletons.java +++ b/instrumentation/elasticsearch/elasticsearch-transport-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/transport/v5_0/Elasticsearch5TransportSingletons.java @@ -6,8 +6,8 @@ package io.opentelemetry.javaagent.instrumentation.elasticsearch.transport.v5_0; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; -import io.opentelemetry.instrumentation.api.instrumenter.net.NetClientAttributesExtractor; -import io.opentelemetry.javaagent.instrumentation.elasticsearch.transport.ElasticTransportNetResponseAttributesGetter; +import io.opentelemetry.instrumentation.api.semconv.network.NetworkAttributesExtractor; +import io.opentelemetry.javaagent.instrumentation.elasticsearch.transport.ElasticTransportNetworkAttributesGetter; import io.opentelemetry.javaagent.instrumentation.elasticsearch.transport.ElasticTransportRequest; import io.opentelemetry.javaagent.instrumentation.elasticsearch.transport.ElasticsearchTransportInstrumenterFactory; import org.elasticsearch.action.ActionResponse; @@ -18,7 +18,7 @@ public final class Elasticsearch5TransportSingletons { ElasticsearchTransportInstrumenterFactory.create( "io.opentelemetry.elasticsearch-transport-5.0", new Elasticsearch5TransportExperimentalAttributesExtractor(), - NetClientAttributesExtractor.create(new ElasticTransportNetResponseAttributesGetter())); + NetworkAttributesExtractor.create(new ElasticTransportNetworkAttributesGetter())); public static Instrumenter instrumenter() { return INSTRUMENTER; diff --git a/instrumentation/elasticsearch/elasticsearch-transport-5.0/javaagent/src/test/groovy/Elasticsearch5NodeClientTest.groovy b/instrumentation/elasticsearch/elasticsearch-transport-5.0/javaagent/src/test/groovy/Elasticsearch5NodeClientTest.groovy index 14822a873c8e..b18a0c09654a 100644 --- a/instrumentation/elasticsearch/elasticsearch-transport-5.0/javaagent/src/test/groovy/Elasticsearch5NodeClientTest.groovy +++ b/instrumentation/elasticsearch/elasticsearch-transport-5.0/javaagent/src/test/groovy/Elasticsearch5NodeClientTest.groovy @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import io.opentelemetry.semconv.incubating.DbIncubatingAttributes import org.elasticsearch.client.Client import org.elasticsearch.common.io.FileSystemUtils import org.elasticsearch.common.settings.Settings @@ -113,8 +113,8 @@ class Elasticsearch5NodeClientTest extends AbstractElasticsearchNodeClientTest { kind CLIENT childOf(span(0)) attributes { - "$SemanticAttributes.DB_SYSTEM" "elasticsearch" - "$SemanticAttributes.DB_OPERATION" "ClusterHealthAction" + "$DbIncubatingAttributes.DB_SYSTEM" "elasticsearch" + "$DbIncubatingAttributes.DB_OPERATION" "ClusterHealthAction" "elasticsearch.action" "ClusterHealthAction" "elasticsearch.request" "ClusterHealthRequest" } @@ -160,8 +160,8 @@ class Elasticsearch5NodeClientTest extends AbstractElasticsearchNodeClientTest { kind CLIENT childOf(span(0)) attributes { - "$SemanticAttributes.DB_SYSTEM" "elasticsearch" - "$SemanticAttributes.DB_OPERATION" "GetAction" + "$DbIncubatingAttributes.DB_SYSTEM" "elasticsearch" + "$DbIncubatingAttributes.DB_OPERATION" "GetAction" "elasticsearch.action" "GetAction" "elasticsearch.request" "GetRequest" "elasticsearch.request.indices" indexName @@ -226,8 +226,8 @@ class Elasticsearch5NodeClientTest extends AbstractElasticsearchNodeClientTest { name "CreateIndexAction" kind CLIENT attributes { - "$SemanticAttributes.DB_SYSTEM" "elasticsearch" - "$SemanticAttributes.DB_OPERATION" "CreateIndexAction" + "$DbIncubatingAttributes.DB_SYSTEM" "elasticsearch" + "$DbIncubatingAttributes.DB_OPERATION" "CreateIndexAction" "elasticsearch.action" "CreateIndexAction" "elasticsearch.request" "CreateIndexRequest" "elasticsearch.request.indices" indexName @@ -239,8 +239,8 @@ class Elasticsearch5NodeClientTest extends AbstractElasticsearchNodeClientTest { name "ClusterHealthAction" kind CLIENT attributes { - "$SemanticAttributes.DB_SYSTEM" "elasticsearch" - "$SemanticAttributes.DB_OPERATION" "ClusterHealthAction" + "$DbIncubatingAttributes.DB_SYSTEM" "elasticsearch" + "$DbIncubatingAttributes.DB_OPERATION" "ClusterHealthAction" "elasticsearch.action" "ClusterHealthAction" "elasticsearch.request" "ClusterHealthRequest" } @@ -251,8 +251,8 @@ class Elasticsearch5NodeClientTest extends AbstractElasticsearchNodeClientTest { name "GetAction" kind CLIENT attributes { - "$SemanticAttributes.DB_SYSTEM" "elasticsearch" - "$SemanticAttributes.DB_OPERATION" "GetAction" + "$DbIncubatingAttributes.DB_SYSTEM" "elasticsearch" + "$DbIncubatingAttributes.DB_OPERATION" "GetAction" "elasticsearch.action" "GetAction" "elasticsearch.request" "GetRequest" "elasticsearch.request.indices" indexName @@ -267,8 +267,8 @@ class Elasticsearch5NodeClientTest extends AbstractElasticsearchNodeClientTest { name "IndexAction" kind CLIENT attributes { - "$SemanticAttributes.DB_SYSTEM" "elasticsearch" - "$SemanticAttributes.DB_OPERATION" "IndexAction" + "$DbIncubatingAttributes.DB_SYSTEM" "elasticsearch" + "$DbIncubatingAttributes.DB_OPERATION" "IndexAction" "elasticsearch.action" "IndexAction" "elasticsearch.request" "IndexRequest" "elasticsearch.request.indices" indexName @@ -285,8 +285,8 @@ class Elasticsearch5NodeClientTest extends AbstractElasticsearchNodeClientTest { name "GetAction" kind CLIENT attributes { - "$SemanticAttributes.DB_SYSTEM" "elasticsearch" - "$SemanticAttributes.DB_OPERATION" "GetAction" + "$DbIncubatingAttributes.DB_SYSTEM" "elasticsearch" + "$DbIncubatingAttributes.DB_OPERATION" "GetAction" "elasticsearch.action" "GetAction" "elasticsearch.request" "GetRequest" "elasticsearch.request.indices" indexName diff --git a/instrumentation/elasticsearch/elasticsearch-transport-5.0/javaagent/src/test/groovy/Elasticsearch5TransportClientTest.groovy b/instrumentation/elasticsearch/elasticsearch-transport-5.0/javaagent/src/test/groovy/Elasticsearch5TransportClientTest.groovy index 2ed7137beb75..11728f2c3e45 100644 --- a/instrumentation/elasticsearch/elasticsearch-transport-5.0/javaagent/src/test/groovy/Elasticsearch5TransportClientTest.groovy +++ b/instrumentation/elasticsearch/elasticsearch-transport-5.0/javaagent/src/test/groovy/Elasticsearch5TransportClientTest.groovy @@ -3,7 +3,8 @@ * SPDX-License-Identifier: Apache-2.0 */ -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import io.opentelemetry.semconv.NetworkAttributes +import io.opentelemetry.semconv.incubating.DbIncubatingAttributes import org.elasticsearch.client.transport.TransportClient import org.elasticsearch.common.io.FileSystemUtils import org.elasticsearch.common.settings.Settings @@ -125,12 +126,10 @@ class Elasticsearch5TransportClientTest extends AbstractElasticsearchTransportCl name "ClusterHealthAction" kind CLIENT attributes { - "$SemanticAttributes.NET_SOCK_FAMILY" { it == SemanticAttributes.NetSockFamilyValues.INET6 || it == null } - "$SemanticAttributes.NET_SOCK_PEER_ADDR" tcpPublishAddress.address - "$SemanticAttributes.NET_SOCK_PEER_NAME" tcpPublishAddress.host - "$SemanticAttributes.NET_SOCK_PEER_PORT" tcpPublishAddress.port - "$SemanticAttributes.DB_SYSTEM" "elasticsearch" - "$SemanticAttributes.DB_OPERATION" "ClusterHealthAction" + "$NetworkAttributes.NETWORK_PEER_ADDRESS" tcpPublishAddress.address + "$NetworkAttributes.NETWORK_PEER_PORT" tcpPublishAddress.port + "$DbIncubatingAttributes.DB_SYSTEM" "elasticsearch" + "$DbIncubatingAttributes.DB_OPERATION" "ClusterHealthAction" "elasticsearch.action" "ClusterHealthAction" "elasticsearch.request" "ClusterHealthRequest" } @@ -175,8 +174,8 @@ class Elasticsearch5TransportClientTest extends AbstractElasticsearchTransportCl childOf(span(0)) errorEvent RemoteTransportException, String attributes { - "$SemanticAttributes.DB_SYSTEM" "elasticsearch" - "$SemanticAttributes.DB_OPERATION" "GetAction" + "$DbIncubatingAttributes.DB_SYSTEM" "elasticsearch" + "$DbIncubatingAttributes.DB_OPERATION" "GetAction" "elasticsearch.action" "GetAction" "elasticsearch.request" "GetRequest" "elasticsearch.request.indices" indexName @@ -243,12 +242,10 @@ class Elasticsearch5TransportClientTest extends AbstractElasticsearchTransportCl name "CreateIndexAction" kind CLIENT attributes { - "$SemanticAttributes.NET_SOCK_FAMILY" { it == SemanticAttributes.NetSockFamilyValues.INET6 || it == null } - "$SemanticAttributes.NET_SOCK_PEER_ADDR" tcpPublishAddress.address - "$SemanticAttributes.NET_SOCK_PEER_NAME" tcpPublishAddress.host - "$SemanticAttributes.NET_SOCK_PEER_PORT" tcpPublishAddress.port - "$SemanticAttributes.DB_SYSTEM" "elasticsearch" - "$SemanticAttributes.DB_OPERATION" "CreateIndexAction" + "$NetworkAttributes.NETWORK_PEER_ADDRESS" tcpPublishAddress.address + "$NetworkAttributes.NETWORK_PEER_PORT" tcpPublishAddress.port + "$DbIncubatingAttributes.DB_SYSTEM" "elasticsearch" + "$DbIncubatingAttributes.DB_OPERATION" "CreateIndexAction" "elasticsearch.action" "CreateIndexAction" "elasticsearch.request" "CreateIndexRequest" "elasticsearch.request.indices" indexName @@ -260,12 +257,10 @@ class Elasticsearch5TransportClientTest extends AbstractElasticsearchTransportCl name "GetAction" kind CLIENT attributes { - "$SemanticAttributes.NET_SOCK_FAMILY" { it == SemanticAttributes.NetSockFamilyValues.INET6 || it == null } - "$SemanticAttributes.NET_SOCK_PEER_ADDR" tcpPublishAddress.address - "$SemanticAttributes.NET_SOCK_PEER_NAME" tcpPublishAddress.host - "$SemanticAttributes.NET_SOCK_PEER_PORT" tcpPublishAddress.port - "$SemanticAttributes.DB_SYSTEM" "elasticsearch" - "$SemanticAttributes.DB_OPERATION" "GetAction" + "$NetworkAttributes.NETWORK_PEER_ADDRESS" tcpPublishAddress.address + "$NetworkAttributes.NETWORK_PEER_PORT" tcpPublishAddress.port + "$DbIncubatingAttributes.DB_SYSTEM" "elasticsearch" + "$DbIncubatingAttributes.DB_OPERATION" "GetAction" "elasticsearch.action" "GetAction" "elasticsearch.request" "GetRequest" "elasticsearch.request.indices" indexName @@ -280,8 +275,8 @@ class Elasticsearch5TransportClientTest extends AbstractElasticsearchTransportCl name "PutMappingAction" kind CLIENT attributes { - "$SemanticAttributes.DB_SYSTEM" "elasticsearch" - "$SemanticAttributes.DB_OPERATION" "PutMappingAction" + "$DbIncubatingAttributes.DB_SYSTEM" "elasticsearch" + "$DbIncubatingAttributes.DB_OPERATION" "PutMappingAction" "elasticsearch.action" "PutMappingAction" "elasticsearch.request" "PutMappingRequest" } @@ -292,12 +287,10 @@ class Elasticsearch5TransportClientTest extends AbstractElasticsearchTransportCl name "IndexAction" kind CLIENT attributes { - "$SemanticAttributes.NET_SOCK_FAMILY" { it == SemanticAttributes.NetSockFamilyValues.INET6 || it == null } - "$SemanticAttributes.NET_SOCK_PEER_ADDR" tcpPublishAddress.address - "$SemanticAttributes.NET_SOCK_PEER_NAME" tcpPublishAddress.host - "$SemanticAttributes.NET_SOCK_PEER_PORT" tcpPublishAddress.port - "$SemanticAttributes.DB_SYSTEM" "elasticsearch" - "$SemanticAttributes.DB_OPERATION" "IndexAction" + "$NetworkAttributes.NETWORK_PEER_ADDRESS" tcpPublishAddress.address + "$NetworkAttributes.NETWORK_PEER_PORT" tcpPublishAddress.port + "$DbIncubatingAttributes.DB_SYSTEM" "elasticsearch" + "$DbIncubatingAttributes.DB_OPERATION" "IndexAction" "elasticsearch.action" "IndexAction" "elasticsearch.request" "IndexRequest" "elasticsearch.request.indices" indexName @@ -314,12 +307,10 @@ class Elasticsearch5TransportClientTest extends AbstractElasticsearchTransportCl name "GetAction" kind CLIENT attributes { - "$SemanticAttributes.NET_SOCK_FAMILY" { it == SemanticAttributes.NetSockFamilyValues.INET6 || it == null } - "$SemanticAttributes.NET_SOCK_PEER_ADDR" tcpPublishAddress.address - "$SemanticAttributes.NET_SOCK_PEER_NAME" tcpPublishAddress.host - "$SemanticAttributes.NET_SOCK_PEER_PORT" tcpPublishAddress.port - "$SemanticAttributes.DB_SYSTEM" "elasticsearch" - "$SemanticAttributes.DB_OPERATION" "GetAction" + "$NetworkAttributes.NETWORK_PEER_ADDRESS" tcpPublishAddress.address + "$NetworkAttributes.NETWORK_PEER_PORT" tcpPublishAddress.port + "$DbIncubatingAttributes.DB_SYSTEM" "elasticsearch" + "$DbIncubatingAttributes.DB_OPERATION" "GetAction" "elasticsearch.action" "GetAction" "elasticsearch.request" "GetRequest" "elasticsearch.request.indices" indexName diff --git a/instrumentation/elasticsearch/elasticsearch-transport-5.3/javaagent/build.gradle.kts b/instrumentation/elasticsearch/elasticsearch-transport-5.3/javaagent/build.gradle.kts index 934d51f6805c..eb8ff28ad473 100644 --- a/instrumentation/elasticsearch/elasticsearch-transport-5.3/javaagent/build.gradle.kts +++ b/instrumentation/elasticsearch/elasticsearch-transport-5.3/javaagent/build.gradle.kts @@ -28,6 +28,13 @@ muzzle { } } +if (findProperty("testLatestDeps") as Boolean) { + // when running on jdk 21 Elasticsearch53SpringRepositoryTest occasionally fails with timeout + otelJava { + maxJavaVersionSupported.set(JavaVersion.VERSION_17) + } +} + dependencies { compileOnly("org.elasticsearch.client:transport:5.3.0") { isTransitive = false @@ -59,6 +66,8 @@ dependencies { } tasks.withType().configureEach { + systemProperty("testLatestDeps", findProperty("testLatestDeps") as Boolean) + // TODO run tests both with and without experimental span attributes jvmArgs("-Dotel.instrumentation.elasticsearch.experimental-span-attributes=true") diff --git a/instrumentation/elasticsearch/elasticsearch-transport-5.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/transport/v5_3/Elasticsearch53TransportSingletons.java b/instrumentation/elasticsearch/elasticsearch-transport-5.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/transport/v5_3/Elasticsearch53TransportSingletons.java index 215ef95e029f..4af55f35ae55 100644 --- a/instrumentation/elasticsearch/elasticsearch-transport-5.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/transport/v5_3/Elasticsearch53TransportSingletons.java +++ b/instrumentation/elasticsearch/elasticsearch-transport-5.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/transport/v5_3/Elasticsearch53TransportSingletons.java @@ -6,8 +6,8 @@ package io.opentelemetry.javaagent.instrumentation.elasticsearch.transport.v5_3; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; -import io.opentelemetry.instrumentation.api.instrumenter.net.NetClientAttributesExtractor; -import io.opentelemetry.javaagent.instrumentation.elasticsearch.transport.ElasticTransportNetResponseAttributesGetter; +import io.opentelemetry.instrumentation.api.semconv.network.NetworkAttributesExtractor; +import io.opentelemetry.javaagent.instrumentation.elasticsearch.transport.ElasticTransportNetworkAttributesGetter; import io.opentelemetry.javaagent.instrumentation.elasticsearch.transport.ElasticTransportRequest; import io.opentelemetry.javaagent.instrumentation.elasticsearch.transport.ElasticsearchTransportInstrumenterFactory; import org.elasticsearch.action.ActionResponse; @@ -18,7 +18,7 @@ public final class Elasticsearch53TransportSingletons { ElasticsearchTransportInstrumenterFactory.create( "io.opentelemetry.elasticsearch-transport-5.3", new Elasticsearch53TransportExperimentalAttributesExtractor(), - NetClientAttributesExtractor.create(new ElasticTransportNetResponseAttributesGetter())); + NetworkAttributesExtractor.create(new ElasticTransportNetworkAttributesGetter())); public static Instrumenter instrumenter() { return INSTRUMENTER; diff --git a/instrumentation/elasticsearch/elasticsearch-transport-5.3/javaagent/src/test/groovy/Elasticsearch53NodeClientTest.groovy b/instrumentation/elasticsearch/elasticsearch-transport-5.3/javaagent/src/test/groovy/Elasticsearch53NodeClientTest.groovy index f26356d8df18..81cdeb4376eb 100644 --- a/instrumentation/elasticsearch/elasticsearch-transport-5.3/javaagent/src/test/groovy/Elasticsearch53NodeClientTest.groovy +++ b/instrumentation/elasticsearch/elasticsearch-transport-5.3/javaagent/src/test/groovy/Elasticsearch53NodeClientTest.groovy @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import io.opentelemetry.semconv.incubating.DbIncubatingAttributes import org.elasticsearch.action.admin.cluster.settings.ClusterUpdateSettingsRequest import org.elasticsearch.client.Client import org.elasticsearch.common.io.FileSystemUtils @@ -116,8 +116,8 @@ class Elasticsearch53NodeClientTest extends AbstractElasticsearchNodeClientTest kind CLIENT childOf(span(0)) attributes { - "$SemanticAttributes.DB_SYSTEM" "elasticsearch" - "$SemanticAttributes.DB_OPERATION" "ClusterHealthAction" + "$DbIncubatingAttributes.DB_SYSTEM" "elasticsearch" + "$DbIncubatingAttributes.DB_OPERATION" "ClusterHealthAction" "elasticsearch.action" "ClusterHealthAction" "elasticsearch.request" "ClusterHealthRequest" } @@ -163,8 +163,8 @@ class Elasticsearch53NodeClientTest extends AbstractElasticsearchNodeClientTest childOf(span(0)) errorEvent IndexNotFoundException, "no such index" attributes { - "$SemanticAttributes.DB_SYSTEM" "elasticsearch" - "$SemanticAttributes.DB_OPERATION" "GetAction" + "$DbIncubatingAttributes.DB_SYSTEM" "elasticsearch" + "$DbIncubatingAttributes.DB_OPERATION" "GetAction" "elasticsearch.action" "GetAction" "elasticsearch.request" "GetRequest" "elasticsearch.request.indices" indexName @@ -229,8 +229,8 @@ class Elasticsearch53NodeClientTest extends AbstractElasticsearchNodeClientTest name "CreateIndexAction" kind CLIENT attributes { - "$SemanticAttributes.DB_SYSTEM" "elasticsearch" - "$SemanticAttributes.DB_OPERATION" "CreateIndexAction" + "$DbIncubatingAttributes.DB_SYSTEM" "elasticsearch" + "$DbIncubatingAttributes.DB_OPERATION" "CreateIndexAction" "elasticsearch.action" "CreateIndexAction" "elasticsearch.request" "CreateIndexRequest" "elasticsearch.request.indices" indexName @@ -242,8 +242,8 @@ class Elasticsearch53NodeClientTest extends AbstractElasticsearchNodeClientTest name "ClusterHealthAction" kind CLIENT attributes { - "$SemanticAttributes.DB_SYSTEM" "elasticsearch" - "$SemanticAttributes.DB_OPERATION" "ClusterHealthAction" + "$DbIncubatingAttributes.DB_SYSTEM" "elasticsearch" + "$DbIncubatingAttributes.DB_OPERATION" "ClusterHealthAction" "elasticsearch.action" "ClusterHealthAction" "elasticsearch.request" "ClusterHealthRequest" } @@ -254,8 +254,8 @@ class Elasticsearch53NodeClientTest extends AbstractElasticsearchNodeClientTest name "GetAction" kind CLIENT attributes { - "$SemanticAttributes.DB_SYSTEM" "elasticsearch" - "$SemanticAttributes.DB_OPERATION" "GetAction" + "$DbIncubatingAttributes.DB_SYSTEM" "elasticsearch" + "$DbIncubatingAttributes.DB_OPERATION" "GetAction" "elasticsearch.action" "GetAction" "elasticsearch.request" "GetRequest" "elasticsearch.request.indices" indexName @@ -270,8 +270,8 @@ class Elasticsearch53NodeClientTest extends AbstractElasticsearchNodeClientTest name "IndexAction" kind CLIENT attributes { - "$SemanticAttributes.DB_SYSTEM" "elasticsearch" - "$SemanticAttributes.DB_OPERATION" "IndexAction" + "$DbIncubatingAttributes.DB_SYSTEM" "elasticsearch" + "$DbIncubatingAttributes.DB_OPERATION" "IndexAction" "elasticsearch.action" "IndexAction" "elasticsearch.request" "IndexRequest" "elasticsearch.request.indices" indexName @@ -289,8 +289,8 @@ class Elasticsearch53NodeClientTest extends AbstractElasticsearchNodeClientTest name "GetAction" kind CLIENT attributes { - "$SemanticAttributes.DB_SYSTEM" "elasticsearch" - "$SemanticAttributes.DB_OPERATION" "GetAction" + "$DbIncubatingAttributes.DB_SYSTEM" "elasticsearch" + "$DbIncubatingAttributes.DB_OPERATION" "GetAction" "elasticsearch.action" "GetAction" "elasticsearch.request" "GetRequest" "elasticsearch.request.indices" indexName diff --git a/instrumentation/elasticsearch/elasticsearch-transport-5.3/javaagent/src/test/groovy/Elasticsearch53TransportClientTest.groovy b/instrumentation/elasticsearch/elasticsearch-transport-5.3/javaagent/src/test/groovy/Elasticsearch53TransportClientTest.groovy index e876438e3f24..ad97aa15b5a5 100644 --- a/instrumentation/elasticsearch/elasticsearch-transport-5.3/javaagent/src/test/groovy/Elasticsearch53TransportClientTest.groovy +++ b/instrumentation/elasticsearch/elasticsearch-transport-5.3/javaagent/src/test/groovy/Elasticsearch53TransportClientTest.groovy @@ -3,7 +3,8 @@ * SPDX-License-Identifier: Apache-2.0 */ -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import io.opentelemetry.semconv.NetworkAttributes +import io.opentelemetry.semconv.incubating.DbIncubatingAttributes import org.elasticsearch.action.admin.cluster.settings.ClusterUpdateSettingsRequest import org.elasticsearch.client.transport.TransportClient import org.elasticsearch.common.io.FileSystemUtils @@ -131,12 +132,10 @@ class Elasticsearch53TransportClientTest extends AbstractElasticsearchTransportC kind CLIENT childOf(span(0)) attributes { - "$SemanticAttributes.NET_SOCK_FAMILY" { it == SemanticAttributes.NetSockFamilyValues.INET6 || it == null } - "$SemanticAttributes.NET_SOCK_PEER_ADDR" tcpPublishAddress.address - "$SemanticAttributes.NET_SOCK_PEER_NAME" tcpPublishAddress.host - "$SemanticAttributes.NET_SOCK_PEER_PORT" tcpPublishAddress.port - "$SemanticAttributes.DB_SYSTEM" "elasticsearch" - "$SemanticAttributes.DB_OPERATION" "ClusterHealthAction" + "$NetworkAttributes.NETWORK_PEER_ADDRESS" tcpPublishAddress.address + "$NetworkAttributes.NETWORK_PEER_PORT" tcpPublishAddress.port + "$DbIncubatingAttributes.DB_SYSTEM" "elasticsearch" + "$DbIncubatingAttributes.DB_OPERATION" "ClusterHealthAction" "elasticsearch.action" "ClusterHealthAction" "elasticsearch.request" "ClusterHealthRequest" } @@ -180,8 +179,8 @@ class Elasticsearch53TransportClientTest extends AbstractElasticsearchTransportC status ERROR errorEvent RemoteTransportException, String attributes { - "$SemanticAttributes.DB_SYSTEM" "elasticsearch" - "$SemanticAttributes.DB_OPERATION" "GetAction" + "$DbIncubatingAttributes.DB_SYSTEM" "elasticsearch" + "$DbIncubatingAttributes.DB_OPERATION" "GetAction" "elasticsearch.action" "GetAction" "elasticsearch.request" "GetRequest" "elasticsearch.request.indices" indexName @@ -248,12 +247,10 @@ class Elasticsearch53TransportClientTest extends AbstractElasticsearchTransportC name "CreateIndexAction" kind CLIENT attributes { - "$SemanticAttributes.NET_SOCK_FAMILY" { it == SemanticAttributes.NetSockFamilyValues.INET6 || it == null } - "$SemanticAttributes.NET_SOCK_PEER_ADDR" tcpPublishAddress.address - "$SemanticAttributes.NET_SOCK_PEER_NAME" tcpPublishAddress.host - "$SemanticAttributes.NET_SOCK_PEER_PORT" tcpPublishAddress.port - "$SemanticAttributes.DB_SYSTEM" "elasticsearch" - "$SemanticAttributes.DB_OPERATION" "CreateIndexAction" + "$NetworkAttributes.NETWORK_PEER_ADDRESS" tcpPublishAddress.address + "$NetworkAttributes.NETWORK_PEER_PORT" tcpPublishAddress.port + "$DbIncubatingAttributes.DB_SYSTEM" "elasticsearch" + "$DbIncubatingAttributes.DB_OPERATION" "CreateIndexAction" "elasticsearch.action" "CreateIndexAction" "elasticsearch.request" "CreateIndexRequest" "elasticsearch.request.indices" indexName @@ -265,12 +262,10 @@ class Elasticsearch53TransportClientTest extends AbstractElasticsearchTransportC name "GetAction" kind CLIENT attributes { - "$SemanticAttributes.NET_SOCK_FAMILY" { it == SemanticAttributes.NetSockFamilyValues.INET6 || it == null } - "$SemanticAttributes.NET_SOCK_PEER_ADDR" tcpPublishAddress.address - "$SemanticAttributes.NET_SOCK_PEER_NAME" tcpPublishAddress.host - "$SemanticAttributes.NET_SOCK_PEER_PORT" tcpPublishAddress.port - "$SemanticAttributes.DB_SYSTEM" "elasticsearch" - "$SemanticAttributes.DB_OPERATION" "GetAction" + "$NetworkAttributes.NETWORK_PEER_ADDRESS" tcpPublishAddress.address + "$NetworkAttributes.NETWORK_PEER_PORT" tcpPublishAddress.port + "$DbIncubatingAttributes.DB_SYSTEM" "elasticsearch" + "$DbIncubatingAttributes.DB_OPERATION" "GetAction" "elasticsearch.action" "GetAction" "elasticsearch.request" "GetRequest" "elasticsearch.request.indices" indexName @@ -285,8 +280,8 @@ class Elasticsearch53TransportClientTest extends AbstractElasticsearchTransportC name "PutMappingAction" kind CLIENT attributes { - "$SemanticAttributes.DB_SYSTEM" "elasticsearch" - "$SemanticAttributes.DB_OPERATION" "PutMappingAction" + "$DbIncubatingAttributes.DB_SYSTEM" "elasticsearch" + "$DbIncubatingAttributes.DB_OPERATION" "PutMappingAction" "elasticsearch.action" "PutMappingAction" "elasticsearch.request" "PutMappingRequest" } @@ -297,12 +292,10 @@ class Elasticsearch53TransportClientTest extends AbstractElasticsearchTransportC name "IndexAction" kind CLIENT attributes { - "$SemanticAttributes.NET_SOCK_FAMILY" { it == SemanticAttributes.NetSockFamilyValues.INET6 || it == null } - "$SemanticAttributes.NET_SOCK_PEER_ADDR" tcpPublishAddress.address - "$SemanticAttributes.NET_SOCK_PEER_NAME" tcpPublishAddress.host - "$SemanticAttributes.NET_SOCK_PEER_PORT" tcpPublishAddress.port - "$SemanticAttributes.DB_SYSTEM" "elasticsearch" - "$SemanticAttributes.DB_OPERATION" "IndexAction" + "$NetworkAttributes.NETWORK_PEER_ADDRESS" tcpPublishAddress.address + "$NetworkAttributes.NETWORK_PEER_PORT" tcpPublishAddress.port + "$DbIncubatingAttributes.DB_SYSTEM" "elasticsearch" + "$DbIncubatingAttributes.DB_OPERATION" "IndexAction" "elasticsearch.action" "IndexAction" "elasticsearch.request" "IndexRequest" "elasticsearch.request.indices" indexName @@ -320,12 +313,10 @@ class Elasticsearch53TransportClientTest extends AbstractElasticsearchTransportC name "GetAction" kind CLIENT attributes { - "$SemanticAttributes.NET_SOCK_FAMILY" { it == SemanticAttributes.NetSockFamilyValues.INET6 || it == null } - "$SemanticAttributes.NET_SOCK_PEER_ADDR" tcpPublishAddress.address - "$SemanticAttributes.NET_SOCK_PEER_NAME" tcpPublishAddress.host - "$SemanticAttributes.NET_SOCK_PEER_PORT" tcpPublishAddress.port - "$SemanticAttributes.DB_SYSTEM" "elasticsearch" - "$SemanticAttributes.DB_OPERATION" "GetAction" + "$NetworkAttributes.NETWORK_PEER_ADDRESS" tcpPublishAddress.address + "$NetworkAttributes.NETWORK_PEER_PORT" tcpPublishAddress.port + "$DbIncubatingAttributes.DB_SYSTEM" "elasticsearch" + "$DbIncubatingAttributes.DB_OPERATION" "GetAction" "elasticsearch.action" "GetAction" "elasticsearch.request" "GetRequest" "elasticsearch.request.indices" indexName diff --git a/instrumentation/elasticsearch/elasticsearch-transport-5.3/javaagent/src/test/groovy/springdata/Elasticsearch53SpringRepositoryTest.groovy b/instrumentation/elasticsearch/elasticsearch-transport-5.3/javaagent/src/test/groovy/springdata/Elasticsearch53SpringRepositoryTest.groovy index 5fef17105943..d75b13be9bd3 100644 --- a/instrumentation/elasticsearch/elasticsearch-transport-5.3/javaagent/src/test/groovy/springdata/Elasticsearch53SpringRepositoryTest.groovy +++ b/instrumentation/elasticsearch/elasticsearch-transport-5.3/javaagent/src/test/groovy/springdata/Elasticsearch53SpringRepositoryTest.groovy @@ -6,9 +6,12 @@ package springdata import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import io.opentelemetry.semconv.incubating.CodeIncubatingAttributes +import io.opentelemetry.semconv.incubating.DbIncubatingAttributes +import org.junit.jupiter.api.Assumptions import org.springframework.context.annotation.AnnotationConfigApplicationContext import spock.lang.Shared +import spock.util.environment.Jvm import java.lang.reflect.InvocationHandler import java.lang.reflect.Method @@ -48,7 +51,7 @@ class Elasticsearch53SpringRepositoryTest extends AgentInstrumentationSpecificat } void close() { - applicationContext.close() + applicationContext?.close() } @Override @@ -58,6 +61,9 @@ class Elasticsearch53SpringRepositoryTest extends AgentInstrumentationSpecificat } def setup() { + // when running on jdk 21 this test occasionally fails with timeout + Assumptions.assumeTrue(Boolean.getBoolean("testLatestDeps") || !Jvm.getCurrent().isJava21Compatible()) + repo.refresh() clearExportedData() runWithSpan("delete") { @@ -84,8 +90,8 @@ class Elasticsearch53SpringRepositoryTest extends AgentInstrumentationSpecificat name "DocRepository.findAll" kind INTERNAL attributes { - "$SemanticAttributes.CODE_NAMESPACE" DocRepository.name - "$SemanticAttributes.CODE_FUNCTION" "findAll" + "$CodeIncubatingAttributes.CODE_NAMESPACE" DocRepository.name + "$CodeIncubatingAttributes.CODE_FUNCTION" "findAll" } } span(1) { @@ -93,8 +99,8 @@ class Elasticsearch53SpringRepositoryTest extends AgentInstrumentationSpecificat kind CLIENT childOf span(0) attributes { - "$SemanticAttributes.DB_SYSTEM" "elasticsearch" - "$SemanticAttributes.DB_OPERATION" "SearchAction" + "$DbIncubatingAttributes.DB_SYSTEM" "elasticsearch" + "$DbIncubatingAttributes.DB_OPERATION" "SearchAction" "elasticsearch.action" "SearchAction" "elasticsearch.request" "SearchRequest" "elasticsearch.request.indices" indexName @@ -122,8 +128,8 @@ class Elasticsearch53SpringRepositoryTest extends AgentInstrumentationSpecificat name "DocRepository.index" kind INTERNAL attributes { - "$SemanticAttributes.CODE_NAMESPACE" DocRepository.name - "$SemanticAttributes.CODE_FUNCTION" "index" + "$CodeIncubatingAttributes.CODE_NAMESPACE" DocRepository.name + "$CodeIncubatingAttributes.CODE_FUNCTION" "index" } } span(1) { @@ -131,8 +137,8 @@ class Elasticsearch53SpringRepositoryTest extends AgentInstrumentationSpecificat kind CLIENT childOf span(0) attributes { - "$SemanticAttributes.DB_SYSTEM" "elasticsearch" - "$SemanticAttributes.DB_OPERATION" "IndexAction" + "$DbIncubatingAttributes.DB_SYSTEM" "elasticsearch" + "$DbIncubatingAttributes.DB_OPERATION" "IndexAction" "elasticsearch.action" "IndexAction" "elasticsearch.request" "IndexRequest" "elasticsearch.request.indices" indexName @@ -149,8 +155,8 @@ class Elasticsearch53SpringRepositoryTest extends AgentInstrumentationSpecificat kind CLIENT childOf span(0) attributes { - "$SemanticAttributes.DB_SYSTEM" "elasticsearch" - "$SemanticAttributes.DB_OPERATION" "RefreshAction" + "$DbIncubatingAttributes.DB_SYSTEM" "elasticsearch" + "$DbIncubatingAttributes.DB_OPERATION" "RefreshAction" "elasticsearch.action" "RefreshAction" "elasticsearch.request" "RefreshRequest" "elasticsearch.request.indices" indexName @@ -173,8 +179,8 @@ class Elasticsearch53SpringRepositoryTest extends AgentInstrumentationSpecificat name "DocRepository.findById" kind INTERNAL attributes { - "$SemanticAttributes.CODE_NAMESPACE" DocRepository.name - "$SemanticAttributes.CODE_FUNCTION" "findById" + "$CodeIncubatingAttributes.CODE_NAMESPACE" DocRepository.name + "$CodeIncubatingAttributes.CODE_FUNCTION" "findById" } } span(1) { @@ -182,8 +188,8 @@ class Elasticsearch53SpringRepositoryTest extends AgentInstrumentationSpecificat kind CLIENT childOf span(0) attributes { - "$SemanticAttributes.DB_SYSTEM" "elasticsearch" - "$SemanticAttributes.DB_OPERATION" "GetAction" + "$DbIncubatingAttributes.DB_SYSTEM" "elasticsearch" + "$DbIncubatingAttributes.DB_OPERATION" "GetAction" "elasticsearch.action" "GetAction" "elasticsearch.request" "GetRequest" "elasticsearch.request.indices" indexName @@ -210,8 +216,8 @@ class Elasticsearch53SpringRepositoryTest extends AgentInstrumentationSpecificat name "DocRepository.index" kind INTERNAL attributes { - "$SemanticAttributes.CODE_NAMESPACE" DocRepository.name - "$SemanticAttributes.CODE_FUNCTION" "index" + "$CodeIncubatingAttributes.CODE_NAMESPACE" DocRepository.name + "$CodeIncubatingAttributes.CODE_FUNCTION" "index" } } span(1) { @@ -219,8 +225,8 @@ class Elasticsearch53SpringRepositoryTest extends AgentInstrumentationSpecificat kind CLIENT childOf span(0) attributes { - "$SemanticAttributes.DB_SYSTEM" "elasticsearch" - "$SemanticAttributes.DB_OPERATION" "IndexAction" + "$DbIncubatingAttributes.DB_SYSTEM" "elasticsearch" + "$DbIncubatingAttributes.DB_OPERATION" "IndexAction" "elasticsearch.action" "IndexAction" "elasticsearch.request" "IndexRequest" "elasticsearch.request.indices" indexName @@ -237,8 +243,8 @@ class Elasticsearch53SpringRepositoryTest extends AgentInstrumentationSpecificat kind CLIENT childOf span(0) attributes { - "$SemanticAttributes.DB_SYSTEM" "elasticsearch" - "$SemanticAttributes.DB_OPERATION" "RefreshAction" + "$DbIncubatingAttributes.DB_SYSTEM" "elasticsearch" + "$DbIncubatingAttributes.DB_OPERATION" "RefreshAction" "elasticsearch.action" "RefreshAction" "elasticsearch.request" "RefreshRequest" "elasticsearch.request.indices" indexName @@ -253,8 +259,8 @@ class Elasticsearch53SpringRepositoryTest extends AgentInstrumentationSpecificat name "DocRepository.findById" kind INTERNAL attributes { - "$SemanticAttributes.CODE_NAMESPACE" DocRepository.name - "$SemanticAttributes.CODE_FUNCTION" "findById" + "$CodeIncubatingAttributes.CODE_NAMESPACE" DocRepository.name + "$CodeIncubatingAttributes.CODE_FUNCTION" "findById" } } span(1) { @@ -262,8 +268,8 @@ class Elasticsearch53SpringRepositoryTest extends AgentInstrumentationSpecificat kind CLIENT childOf span(0) attributes { - "$SemanticAttributes.DB_SYSTEM" "elasticsearch" - "$SemanticAttributes.DB_OPERATION" "GetAction" + "$DbIncubatingAttributes.DB_SYSTEM" "elasticsearch" + "$DbIncubatingAttributes.DB_OPERATION" "GetAction" "elasticsearch.action" "GetAction" "elasticsearch.request" "GetRequest" "elasticsearch.request.indices" indexName @@ -289,8 +295,8 @@ class Elasticsearch53SpringRepositoryTest extends AgentInstrumentationSpecificat name "DocRepository.deleteById" kind INTERNAL attributes { - "$SemanticAttributes.CODE_NAMESPACE" DocRepository.name - "$SemanticAttributes.CODE_FUNCTION" "deleteById" + "$CodeIncubatingAttributes.CODE_NAMESPACE" DocRepository.name + "$CodeIncubatingAttributes.CODE_FUNCTION" "deleteById" } } span(1) { @@ -298,8 +304,8 @@ class Elasticsearch53SpringRepositoryTest extends AgentInstrumentationSpecificat kind CLIENT childOf span(0) attributes { - "$SemanticAttributes.DB_SYSTEM" "elasticsearch" - "$SemanticAttributes.DB_OPERATION" "DeleteAction" + "$DbIncubatingAttributes.DB_SYSTEM" "elasticsearch" + "$DbIncubatingAttributes.DB_OPERATION" "DeleteAction" "elasticsearch.action" "DeleteAction" "elasticsearch.request" "DeleteRequest" "elasticsearch.request.indices" indexName @@ -315,8 +321,8 @@ class Elasticsearch53SpringRepositoryTest extends AgentInstrumentationSpecificat kind CLIENT childOf span(0) attributes { - "$SemanticAttributes.DB_SYSTEM" "elasticsearch" - "$SemanticAttributes.DB_OPERATION" "RefreshAction" + "$DbIncubatingAttributes.DB_SYSTEM" "elasticsearch" + "$DbIncubatingAttributes.DB_OPERATION" "RefreshAction" "elasticsearch.action" "RefreshAction" "elasticsearch.request" "RefreshRequest" "elasticsearch.request.indices" indexName @@ -332,8 +338,8 @@ class Elasticsearch53SpringRepositoryTest extends AgentInstrumentationSpecificat name "DocRepository.findAll" kind INTERNAL attributes { - "$SemanticAttributes.CODE_NAMESPACE" DocRepository.name - "$SemanticAttributes.CODE_FUNCTION" "findAll" + "$CodeIncubatingAttributes.CODE_NAMESPACE" DocRepository.name + "$CodeIncubatingAttributes.CODE_FUNCTION" "findAll" } } span(1) { @@ -341,8 +347,8 @@ class Elasticsearch53SpringRepositoryTest extends AgentInstrumentationSpecificat kind CLIENT childOf span(0) attributes { - "$SemanticAttributes.DB_SYSTEM" "elasticsearch" - "$SemanticAttributes.DB_OPERATION" "SearchAction" + "$DbIncubatingAttributes.DB_SYSTEM" "elasticsearch" + "$DbIncubatingAttributes.DB_OPERATION" "SearchAction" "elasticsearch.action" "SearchAction" "elasticsearch.request" "SearchRequest" "elasticsearch.request.indices" indexName diff --git a/instrumentation/elasticsearch/elasticsearch-transport-5.3/javaagent/src/test/groovy/springdata/Elasticsearch53SpringTemplateTest.groovy b/instrumentation/elasticsearch/elasticsearch-transport-5.3/javaagent/src/test/groovy/springdata/Elasticsearch53SpringTemplateTest.groovy index 79b6cd0d04dd..009f5e7067ab 100644 --- a/instrumentation/elasticsearch/elasticsearch-transport-5.3/javaagent/src/test/groovy/springdata/Elasticsearch53SpringTemplateTest.groovy +++ b/instrumentation/elasticsearch/elasticsearch-transport-5.3/javaagent/src/test/groovy/springdata/Elasticsearch53SpringTemplateTest.groovy @@ -6,7 +6,7 @@ package springdata import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import io.opentelemetry.semconv.incubating.DbIncubatingAttributes import org.elasticsearch.action.admin.cluster.settings.ClusterUpdateSettingsRequest import org.elasticsearch.action.search.SearchResponse import org.elasticsearch.common.io.FileSystemUtils @@ -18,12 +18,14 @@ 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.junit.jupiter.api.Assumptions 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 +import spock.util.environment.Jvm import java.util.concurrent.atomic.AtomicLong @@ -45,7 +47,6 @@ class Elasticsearch53SpringTemplateTest extends AgentInstrumentationSpecificatio ElasticsearchTemplate template def setupSpec() { - esWorkingDir = File.createTempDir("test-es-working-dir-", "") esWorkingDir.deleteOnExit() println "ES work dir: $esWorkingDir" @@ -81,6 +82,11 @@ class Elasticsearch53SpringTemplateTest extends AgentInstrumentationSpecificatio } } + def setup() { + // when running on jdk 21 this test occasionally fails with timeout + Assumptions.assumeTrue(Boolean.getBoolean("testLatestDeps") || !Jvm.getCurrent().isJava21Compatible()) + } + def "test elasticsearch error"() { when: template.refresh(indexName) @@ -97,8 +103,8 @@ class Elasticsearch53SpringTemplateTest extends AgentInstrumentationSpecificatio status ERROR errorEvent IndexNotFoundException, "no such index" attributes { - "$SemanticAttributes.DB_SYSTEM" "elasticsearch" - "$SemanticAttributes.DB_OPERATION" "RefreshAction" + "$DbIncubatingAttributes.DB_SYSTEM" "elasticsearch" + "$DbIncubatingAttributes.DB_OPERATION" "RefreshAction" "elasticsearch.action" "RefreshAction" "elasticsearch.request" "RefreshRequest" "elasticsearch.request.indices" indexName @@ -146,8 +152,8 @@ class Elasticsearch53SpringTemplateTest extends AgentInstrumentationSpecificatio name "CreateIndexAction" kind CLIENT attributes { - "$SemanticAttributes.DB_SYSTEM" "elasticsearch" - "$SemanticAttributes.DB_OPERATION" "CreateIndexAction" + "$DbIncubatingAttributes.DB_SYSTEM" "elasticsearch" + "$DbIncubatingAttributes.DB_OPERATION" "CreateIndexAction" "elasticsearch.action" "CreateIndexAction" "elasticsearch.request" "CreateIndexRequest" "elasticsearch.request.indices" indexName @@ -159,8 +165,8 @@ class Elasticsearch53SpringTemplateTest extends AgentInstrumentationSpecificatio name "ClusterHealthAction" kind CLIENT attributes { - "$SemanticAttributes.DB_SYSTEM" "elasticsearch" - "$SemanticAttributes.DB_OPERATION" "ClusterHealthAction" + "$DbIncubatingAttributes.DB_SYSTEM" "elasticsearch" + "$DbIncubatingAttributes.DB_OPERATION" "ClusterHealthAction" "elasticsearch.action" "ClusterHealthAction" "elasticsearch.request" "ClusterHealthRequest" } @@ -171,8 +177,8 @@ class Elasticsearch53SpringTemplateTest extends AgentInstrumentationSpecificatio name "SearchAction" kind CLIENT attributes { - "$SemanticAttributes.DB_SYSTEM" "elasticsearch" - "$SemanticAttributes.DB_OPERATION" "SearchAction" + "$DbIncubatingAttributes.DB_SYSTEM" "elasticsearch" + "$DbIncubatingAttributes.DB_OPERATION" "SearchAction" "elasticsearch.action" "SearchAction" "elasticsearch.request" "SearchRequest" "elasticsearch.request.indices" indexName @@ -185,8 +191,8 @@ class Elasticsearch53SpringTemplateTest extends AgentInstrumentationSpecificatio name "IndexAction" kind CLIENT attributes { - "$SemanticAttributes.DB_SYSTEM" "elasticsearch" - "$SemanticAttributes.DB_OPERATION" "IndexAction" + "$DbIncubatingAttributes.DB_SYSTEM" "elasticsearch" + "$DbIncubatingAttributes.DB_OPERATION" "IndexAction" "elasticsearch.action" "IndexAction" "elasticsearch.request" "IndexRequest" "elasticsearch.request.indices" indexName @@ -204,8 +210,8 @@ class Elasticsearch53SpringTemplateTest extends AgentInstrumentationSpecificatio name "RefreshAction" kind CLIENT attributes { - "$SemanticAttributes.DB_SYSTEM" "elasticsearch" - "$SemanticAttributes.DB_OPERATION" "RefreshAction" + "$DbIncubatingAttributes.DB_SYSTEM" "elasticsearch" + "$DbIncubatingAttributes.DB_OPERATION" "RefreshAction" "elasticsearch.action" "RefreshAction" "elasticsearch.request" "RefreshRequest" "elasticsearch.request.indices" indexName @@ -220,8 +226,8 @@ class Elasticsearch53SpringTemplateTest extends AgentInstrumentationSpecificatio name "SearchAction" kind CLIENT attributes { - "$SemanticAttributes.DB_SYSTEM" "elasticsearch" - "$SemanticAttributes.DB_OPERATION" "SearchAction" + "$DbIncubatingAttributes.DB_SYSTEM" "elasticsearch" + "$DbIncubatingAttributes.DB_OPERATION" "SearchAction" "elasticsearch.action" "SearchAction" "elasticsearch.request" "SearchRequest" "elasticsearch.request.indices" indexName @@ -297,8 +303,8 @@ class Elasticsearch53SpringTemplateTest extends AgentInstrumentationSpecificatio name "SearchAction" kind CLIENT attributes { - "$SemanticAttributes.DB_SYSTEM" "elasticsearch" - "$SemanticAttributes.DB_OPERATION" "SearchAction" + "$DbIncubatingAttributes.DB_SYSTEM" "elasticsearch" + "$DbIncubatingAttributes.DB_OPERATION" "SearchAction" "elasticsearch.action" "SearchAction" "elasticsearch.request" "SearchRequest" "elasticsearch.request.indices" indexName diff --git a/instrumentation/elasticsearch/elasticsearch-transport-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/transport/v6_0/Elasticsearch6TransportNetAttributesGetter.java b/instrumentation/elasticsearch/elasticsearch-transport-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/transport/v6_0/Elasticsearch6TransportNetworkAttributesGetter.java similarity index 55% rename from instrumentation/elasticsearch/elasticsearch-transport-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/transport/v6_0/Elasticsearch6TransportNetAttributesGetter.java rename to instrumentation/elasticsearch/elasticsearch-transport-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/transport/v6_0/Elasticsearch6TransportNetworkAttributesGetter.java index da846f89803e..752093bdd6ec 100644 --- a/instrumentation/elasticsearch/elasticsearch-transport-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/transport/v6_0/Elasticsearch6TransportNetAttributesGetter.java +++ b/instrumentation/elasticsearch/elasticsearch-transport-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/transport/v6_0/Elasticsearch6TransportNetworkAttributesGetter.java @@ -5,30 +5,18 @@ package io.opentelemetry.javaagent.instrumentation.elasticsearch.transport.v6_0; -import io.opentelemetry.instrumentation.api.instrumenter.net.NetClientAttributesGetter; +import io.opentelemetry.instrumentation.api.semconv.network.NetworkAttributesGetter; import io.opentelemetry.javaagent.instrumentation.elasticsearch.transport.ElasticTransportRequest; import java.net.InetSocketAddress; import javax.annotation.Nullable; import org.elasticsearch.action.ActionResponse; -public class Elasticsearch6TransportNetAttributesGetter - implements NetClientAttributesGetter { - - @Nullable - @Override - public String getServerAddress(ElasticTransportRequest request) { - return null; - } - - @Nullable - @Override - public Integer getServerPort(ElasticTransportRequest request) { - return null; - } +public class Elasticsearch6TransportNetworkAttributesGetter + implements NetworkAttributesGetter { @Override @Nullable - public InetSocketAddress getServerInetSocketAddress( + public InetSocketAddress getNetworkPeerInetSocketAddress( ElasticTransportRequest request, @Nullable ActionResponse response) { if (response != null && response.remoteAddress() != null) { return response.remoteAddress().address(); diff --git a/instrumentation/elasticsearch/elasticsearch-transport-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/transport/v6_0/Elasticsearch6TransportSingletons.java b/instrumentation/elasticsearch/elasticsearch-transport-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/transport/v6_0/Elasticsearch6TransportSingletons.java index 5e72056bb4f8..79ed2dbe12ae 100644 --- a/instrumentation/elasticsearch/elasticsearch-transport-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/transport/v6_0/Elasticsearch6TransportSingletons.java +++ b/instrumentation/elasticsearch/elasticsearch-transport-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/transport/v6_0/Elasticsearch6TransportSingletons.java @@ -6,7 +6,7 @@ package io.opentelemetry.javaagent.instrumentation.elasticsearch.transport.v6_0; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; -import io.opentelemetry.instrumentation.api.instrumenter.net.NetClientAttributesExtractor; +import io.opentelemetry.instrumentation.api.semconv.network.NetworkAttributesExtractor; import io.opentelemetry.javaagent.instrumentation.elasticsearch.transport.ElasticTransportRequest; import io.opentelemetry.javaagent.instrumentation.elasticsearch.transport.ElasticsearchTransportInstrumenterFactory; import org.elasticsearch.action.ActionResponse; @@ -17,7 +17,7 @@ public final class Elasticsearch6TransportSingletons { ElasticsearchTransportInstrumenterFactory.create( "io.opentelemetry.elasticsearch-transport-6.0", new Elasticsearch6TransportExperimentalAttributesExtractor(), - NetClientAttributesExtractor.create(new Elasticsearch6TransportNetAttributesGetter())); + NetworkAttributesExtractor.create(new Elasticsearch6TransportNetworkAttributesGetter())); public static Instrumenter instrumenter() { return INSTRUMENTER; diff --git a/instrumentation/elasticsearch/elasticsearch-transport-6.0/javaagent/src/test/groovy/Elasticsearch6NodeClientTest.groovy b/instrumentation/elasticsearch/elasticsearch-transport-6.0/javaagent/src/test/groovy/Elasticsearch6NodeClientTest.groovy index 64b5f70a8877..457f4ac68928 100644 --- a/instrumentation/elasticsearch/elasticsearch-transport-6.0/javaagent/src/test/groovy/Elasticsearch6NodeClientTest.groovy +++ b/instrumentation/elasticsearch/elasticsearch-transport-6.0/javaagent/src/test/groovy/Elasticsearch6NodeClientTest.groovy @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import io.opentelemetry.semconv.incubating.DbIncubatingAttributes import org.elasticsearch.action.admin.cluster.settings.ClusterUpdateSettingsRequest import org.elasticsearch.client.Client import org.elasticsearch.common.io.FileSystemUtils @@ -92,8 +92,8 @@ class Elasticsearch6NodeClientTest extends AbstractElasticsearchNodeClientTest { kind CLIENT childOf(span(0)) attributes { - "$SemanticAttributes.DB_SYSTEM" "elasticsearch" - "$SemanticAttributes.DB_OPERATION" "ClusterHealthAction" + "$DbIncubatingAttributes.DB_SYSTEM" "elasticsearch" + "$DbIncubatingAttributes.DB_OPERATION" "ClusterHealthAction" "elasticsearch.action" "ClusterHealthAction" "elasticsearch.request" "ClusterHealthRequest" } @@ -138,8 +138,8 @@ class Elasticsearch6NodeClientTest extends AbstractElasticsearchNodeClientTest { childOf(span(0)) errorEvent IndexNotFoundException, ~/no such index( \[invalid-index])?/ attributes { - "$SemanticAttributes.DB_SYSTEM" "elasticsearch" - "$SemanticAttributes.DB_OPERATION" "GetAction" + "$DbIncubatingAttributes.DB_SYSTEM" "elasticsearch" + "$DbIncubatingAttributes.DB_OPERATION" "GetAction" "elasticsearch.action" "GetAction" "elasticsearch.request" "GetRequest" "elasticsearch.request.indices" indexName @@ -203,8 +203,8 @@ class Elasticsearch6NodeClientTest extends AbstractElasticsearchNodeClientTest { name "CreateIndexAction" kind CLIENT attributes { - "$SemanticAttributes.DB_SYSTEM" "elasticsearch" - "$SemanticAttributes.DB_OPERATION" "CreateIndexAction" + "$DbIncubatingAttributes.DB_SYSTEM" "elasticsearch" + "$DbIncubatingAttributes.DB_OPERATION" "CreateIndexAction" "elasticsearch.action" "CreateIndexAction" "elasticsearch.request" "CreateIndexRequest" "elasticsearch.request.indices" indexName @@ -216,8 +216,8 @@ class Elasticsearch6NodeClientTest extends AbstractElasticsearchNodeClientTest { name "GetAction" kind CLIENT attributes { - "$SemanticAttributes.DB_SYSTEM" "elasticsearch" - "$SemanticAttributes.DB_OPERATION" "GetAction" + "$DbIncubatingAttributes.DB_SYSTEM" "elasticsearch" + "$DbIncubatingAttributes.DB_OPERATION" "GetAction" "elasticsearch.action" "GetAction" "elasticsearch.request" "GetRequest" "elasticsearch.request.indices" indexName @@ -232,8 +232,8 @@ class Elasticsearch6NodeClientTest extends AbstractElasticsearchNodeClientTest { name "IndexAction" kind CLIENT attributes { - "$SemanticAttributes.DB_SYSTEM" "elasticsearch" - "$SemanticAttributes.DB_OPERATION" "IndexAction" + "$DbIncubatingAttributes.DB_SYSTEM" "elasticsearch" + "$DbIncubatingAttributes.DB_OPERATION" "IndexAction" "elasticsearch.action" "IndexAction" "elasticsearch.request" "IndexRequest" "elasticsearch.request.indices" indexName @@ -251,8 +251,8 @@ class Elasticsearch6NodeClientTest extends AbstractElasticsearchNodeClientTest { name "GetAction" kind CLIENT attributes { - "$SemanticAttributes.DB_SYSTEM" "elasticsearch" - "$SemanticAttributes.DB_OPERATION" "GetAction" + "$DbIncubatingAttributes.DB_SYSTEM" "elasticsearch" + "$DbIncubatingAttributes.DB_OPERATION" "GetAction" "elasticsearch.action" "GetAction" "elasticsearch.request" "GetRequest" "elasticsearch.request.indices" indexName diff --git a/instrumentation/elasticsearch/elasticsearch-transport-6.0/javaagent/src/test/groovy/Elasticsearch6TransportClientTest.groovy b/instrumentation/elasticsearch/elasticsearch-transport-6.0/javaagent/src/test/groovy/Elasticsearch6TransportClientTest.groovy index 55b46edd969c..0fafe46112e4 100644 --- a/instrumentation/elasticsearch/elasticsearch-transport-6.0/javaagent/src/test/groovy/Elasticsearch6TransportClientTest.groovy +++ b/instrumentation/elasticsearch/elasticsearch-transport-6.0/javaagent/src/test/groovy/Elasticsearch6TransportClientTest.groovy @@ -3,7 +3,8 @@ * SPDX-License-Identifier: Apache-2.0 */ -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import io.opentelemetry.semconv.incubating.DbIncubatingAttributes +import io.opentelemetry.semconv.NetworkAttributes import org.elasticsearch.action.admin.cluster.settings.ClusterUpdateSettingsRequest import org.elasticsearch.client.transport.TransportClient import org.elasticsearch.common.io.FileSystemUtils @@ -105,12 +106,11 @@ class Elasticsearch6TransportClientTest extends AbstractElasticsearchTransportCl kind CLIENT childOf(span(0)) attributes { - "$SemanticAttributes.NET_SOCK_FAMILY" { it == SemanticAttributes.NetSockFamilyValues.INET6 || it == null } - "$SemanticAttributes.NET_SOCK_PEER_ADDR" tcpPublishAddress.address - "$SemanticAttributes.NET_SOCK_PEER_NAME" tcpPublishAddress.address().hostString - "$SemanticAttributes.NET_SOCK_PEER_PORT" tcpPublishAddress.port - "$SemanticAttributes.DB_SYSTEM" "elasticsearch" - "$SemanticAttributes.DB_OPERATION" "ClusterHealthAction" + "$NetworkAttributes.NETWORK_TYPE" { it == "ipv4" || it == "ipv6" } + "$NetworkAttributes.NETWORK_PEER_ADDRESS" tcpPublishAddress.address + "$NetworkAttributes.NETWORK_PEER_PORT" tcpPublishAddress.port + "$DbIncubatingAttributes.DB_SYSTEM" "elasticsearch" + "$DbIncubatingAttributes.DB_OPERATION" "ClusterHealthAction" "elasticsearch.action" "ClusterHealthAction" "elasticsearch.request" "ClusterHealthRequest" } @@ -154,8 +154,8 @@ class Elasticsearch6TransportClientTest extends AbstractElasticsearchTransportCl status ERROR errorEvent RemoteTransportException, String attributes { - "$SemanticAttributes.DB_SYSTEM" "elasticsearch" - "$SemanticAttributes.DB_OPERATION" "GetAction" + "$DbIncubatingAttributes.DB_SYSTEM" "elasticsearch" + "$DbIncubatingAttributes.DB_OPERATION" "GetAction" "elasticsearch.action" "GetAction" "elasticsearch.request" "GetRequest" "elasticsearch.request.indices" indexName @@ -225,12 +225,11 @@ class Elasticsearch6TransportClientTest extends AbstractElasticsearchTransportCl name "CreateIndexAction" kind CLIENT attributes { - "$SemanticAttributes.NET_SOCK_FAMILY" { it == SemanticAttributes.NetSockFamilyValues.INET6 || it == null } - "$SemanticAttributes.NET_SOCK_PEER_ADDR" tcpPublishAddress.address - "$SemanticAttributes.NET_SOCK_PEER_NAME" tcpPublishAddress.address().hostString - "$SemanticAttributes.NET_SOCK_PEER_PORT" tcpPublishAddress.port - "$SemanticAttributes.DB_SYSTEM" "elasticsearch" - "$SemanticAttributes.DB_OPERATION" "CreateIndexAction" + "$NetworkAttributes.NETWORK_TYPE" { it == "ipv4" || it == "ipv6" } + "$NetworkAttributes.NETWORK_PEER_ADDRESS" tcpPublishAddress.address + "$NetworkAttributes.NETWORK_PEER_PORT" tcpPublishAddress.port + "$DbIncubatingAttributes.DB_SYSTEM" "elasticsearch" + "$DbIncubatingAttributes.DB_OPERATION" "CreateIndexAction" "elasticsearch.action" "CreateIndexAction" "elasticsearch.request" "CreateIndexRequest" "elasticsearch.request.indices" indexName @@ -242,12 +241,11 @@ class Elasticsearch6TransportClientTest extends AbstractElasticsearchTransportCl name "GetAction" kind CLIENT attributes { - "$SemanticAttributes.NET_SOCK_FAMILY" { it == SemanticAttributes.NetSockFamilyValues.INET6 || it == null } - "$SemanticAttributes.NET_SOCK_PEER_ADDR" tcpPublishAddress.address - "$SemanticAttributes.NET_SOCK_PEER_NAME" tcpPublishAddress.address().hostString - "$SemanticAttributes.NET_SOCK_PEER_PORT" tcpPublishAddress.port - "$SemanticAttributes.DB_SYSTEM" "elasticsearch" - "$SemanticAttributes.DB_OPERATION" "GetAction" + "$NetworkAttributes.NETWORK_TYPE" { it == "ipv4" || it == "ipv6" } + "$NetworkAttributes.NETWORK_PEER_ADDRESS" tcpPublishAddress.address + "$NetworkAttributes.NETWORK_PEER_PORT" tcpPublishAddress.port + "$DbIncubatingAttributes.DB_SYSTEM" "elasticsearch" + "$DbIncubatingAttributes.DB_OPERATION" "GetAction" "elasticsearch.action" "GetAction" "elasticsearch.request" "GetRequest" "elasticsearch.request.indices" indexName @@ -262,8 +260,8 @@ class Elasticsearch6TransportClientTest extends AbstractElasticsearchTransportCl name ~/(Auto)?PutMappingAction/ kind CLIENT attributes { - "$SemanticAttributes.DB_SYSTEM" "elasticsearch" - "$SemanticAttributes.DB_OPERATION" ~/(Auto)?PutMappingAction/ + "$DbIncubatingAttributes.DB_SYSTEM" "elasticsearch" + "$DbIncubatingAttributes.DB_OPERATION" ~/(Auto)?PutMappingAction/ "elasticsearch.action" ~/(Auto)?PutMappingAction/ "elasticsearch.request" "PutMappingRequest" } @@ -274,12 +272,11 @@ class Elasticsearch6TransportClientTest extends AbstractElasticsearchTransportCl name "IndexAction" kind CLIENT attributes { - "$SemanticAttributes.NET_SOCK_FAMILY" { it == SemanticAttributes.NetSockFamilyValues.INET6 || it == null } - "$SemanticAttributes.NET_SOCK_PEER_ADDR" tcpPublishAddress.address - "$SemanticAttributes.NET_SOCK_PEER_NAME" tcpPublishAddress.address().hostString - "$SemanticAttributes.NET_SOCK_PEER_PORT" tcpPublishAddress.port - "$SemanticAttributes.DB_SYSTEM" "elasticsearch" - "$SemanticAttributes.DB_OPERATION" "IndexAction" + "$NetworkAttributes.NETWORK_TYPE" { it == "ipv4" || it == "ipv6" } + "$NetworkAttributes.NETWORK_PEER_ADDRESS" tcpPublishAddress.address + "$NetworkAttributes.NETWORK_PEER_PORT" tcpPublishAddress.port + "$DbIncubatingAttributes.DB_SYSTEM" "elasticsearch" + "$DbIncubatingAttributes.DB_OPERATION" "IndexAction" "elasticsearch.action" "IndexAction" "elasticsearch.request" "IndexRequest" "elasticsearch.request.indices" indexName @@ -297,12 +294,11 @@ class Elasticsearch6TransportClientTest extends AbstractElasticsearchTransportCl name "GetAction" kind CLIENT attributes { - "$SemanticAttributes.NET_SOCK_FAMILY" { it == SemanticAttributes.NetSockFamilyValues.INET6 || it == null } - "$SemanticAttributes.NET_SOCK_PEER_ADDR" tcpPublishAddress.address - "$SemanticAttributes.NET_SOCK_PEER_NAME" tcpPublishAddress.address().hostString - "$SemanticAttributes.NET_SOCK_PEER_PORT" tcpPublishAddress.port - "$SemanticAttributes.DB_SYSTEM" "elasticsearch" - "$SemanticAttributes.DB_OPERATION" "GetAction" + "$NetworkAttributes.NETWORK_TYPE" { it == "ipv4" || it == "ipv6" } + "$NetworkAttributes.NETWORK_PEER_ADDRESS" tcpPublishAddress.address + "$NetworkAttributes.NETWORK_PEER_PORT" tcpPublishAddress.port + "$DbIncubatingAttributes.DB_SYSTEM" "elasticsearch" + "$DbIncubatingAttributes.DB_OPERATION" "GetAction" "elasticsearch.action" "GetAction" "elasticsearch.request" "GetRequest" "elasticsearch.request.indices" indexName diff --git a/instrumentation/elasticsearch/elasticsearch-transport-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/transport/ElasticTransportNetResponseAttributesGetter.java b/instrumentation/elasticsearch/elasticsearch-transport-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/transport/ElasticTransportNetResponseAttributesGetter.java deleted file mode 100644 index 05a9c7b2bfe6..000000000000 --- a/instrumentation/elasticsearch/elasticsearch-transport-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/transport/ElasticTransportNetResponseAttributesGetter.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.javaagent.instrumentation.elasticsearch.transport; - -import io.opentelemetry.instrumentation.api.instrumenter.net.NetClientAttributesGetter; -import javax.annotation.Nullable; -import org.elasticsearch.action.ActionResponse; - -public class ElasticTransportNetResponseAttributesGetter - implements NetClientAttributesGetter { - - @Override - @Nullable - public String getServerAddress(ElasticTransportRequest request) { - return null; - } - - @Override - @Nullable - public Integer getServerPort(ElasticTransportRequest request) { - return null; - } - - @Override - @Nullable - public String getServerSocketAddress( - ElasticTransportRequest request, @Nullable ActionResponse response) { - if (response != null && response.remoteAddress() != null) { - return response.remoteAddress().getAddress(); - } - return null; - } - - @Nullable - @Override - public String getServerSocketDomain( - ElasticTransportRequest request, @Nullable ActionResponse response) { - if (response != null && response.remoteAddress() != null) { - return response.remoteAddress().getHost(); - } - return null; - } - - @Nullable - @Override - public Integer getServerSocketPort( - ElasticTransportRequest request, @Nullable ActionResponse response) { - if (response != null && response.remoteAddress() != null) { - return response.remoteAddress().getPort(); - } - return null; - } -} diff --git a/instrumentation/elasticsearch/elasticsearch-transport-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/transport/ElasticTransportNetworkAttributesGetter.java b/instrumentation/elasticsearch/elasticsearch-transport-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/transport/ElasticTransportNetworkAttributesGetter.java new file mode 100644 index 000000000000..5ade6b5f17c2 --- /dev/null +++ b/instrumentation/elasticsearch/elasticsearch-transport-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/transport/ElasticTransportNetworkAttributesGetter.java @@ -0,0 +1,34 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.elasticsearch.transport; + +import io.opentelemetry.instrumentation.api.semconv.network.NetworkAttributesGetter; +import javax.annotation.Nullable; +import org.elasticsearch.action.ActionResponse; + +public class ElasticTransportNetworkAttributesGetter + implements NetworkAttributesGetter { + + @Override + @Nullable + public String getNetworkPeerAddress( + ElasticTransportRequest request, @Nullable ActionResponse response) { + if (response != null && response.remoteAddress() != null) { + return response.remoteAddress().getAddress(); + } + return null; + } + + @Nullable + @Override + public Integer getNetworkPeerPort( + ElasticTransportRequest request, @Nullable ActionResponse response) { + if (response != null && response.remoteAddress() != null) { + return response.remoteAddress().getPort(); + } + return null; + } +} diff --git a/instrumentation/elasticsearch/elasticsearch-transport-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/transport/ElasticsearchTransportAttributesGetter.java b/instrumentation/elasticsearch/elasticsearch-transport-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/transport/ElasticsearchTransportAttributesGetter.java index 43adedb9784b..537dd06bed20 100644 --- a/instrumentation/elasticsearch/elasticsearch-transport-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/transport/ElasticsearchTransportAttributesGetter.java +++ b/instrumentation/elasticsearch/elasticsearch-transport-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/transport/ElasticsearchTransportAttributesGetter.java @@ -5,8 +5,8 @@ package io.opentelemetry.javaagent.instrumentation.elasticsearch.transport; -import io.opentelemetry.instrumentation.api.instrumenter.db.DbClientAttributesGetter; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.opentelemetry.instrumentation.api.incubator.semconv.db.DbClientAttributesGetter; +import io.opentelemetry.semconv.incubating.DbIncubatingAttributes; import javax.annotation.Nullable; final class ElasticsearchTransportAttributesGetter @@ -14,7 +14,7 @@ final class ElasticsearchTransportAttributesGetter @Override public String getSystem(ElasticTransportRequest s) { - return SemanticAttributes.DbSystemValues.ELASTICSEARCH; + return DbIncubatingAttributes.DbSystemValues.ELASTICSEARCH; } @Override diff --git a/instrumentation/elasticsearch/elasticsearch-transport-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/transport/ElasticsearchTransportInstrumenterFactory.java b/instrumentation/elasticsearch/elasticsearch-transport-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/transport/ElasticsearchTransportInstrumenterFactory.java index c552e4d4b35a..33702561b4c8 100644 --- a/instrumentation/elasticsearch/elasticsearch-transport-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/transport/ElasticsearchTransportInstrumenterFactory.java +++ b/instrumentation/elasticsearch/elasticsearch-transport-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/transport/ElasticsearchTransportInstrumenterFactory.java @@ -6,18 +6,18 @@ package io.opentelemetry.javaagent.instrumentation.elasticsearch.transport; import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.instrumentation.api.incubator.semconv.db.DbClientAttributesExtractor; +import io.opentelemetry.instrumentation.api.incubator.semconv.db.DbClientSpanNameExtractor; 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 io.opentelemetry.instrumentation.api.instrumenter.db.DbClientAttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.db.DbClientSpanNameExtractor; -import io.opentelemetry.javaagent.bootstrap.internal.InstrumentationConfig; +import io.opentelemetry.javaagent.bootstrap.internal.AgentInstrumentationConfig; import org.elasticsearch.action.ActionResponse; public final class ElasticsearchTransportInstrumenterFactory { private static final boolean CAPTURE_EXPERIMENTAL_SPAN_ATTRIBUTES = - InstrumentationConfig.get() + AgentInstrumentationConfig.get() .getBoolean("otel.instrumentation.elasticsearch.experimental-span-attributes", false); public static Instrumenter create( diff --git a/instrumentation/executors/README.md b/instrumentation/executors/README.md index 675b90d64fca..deb0e9ed46cc 100644 --- a/instrumentation/executors/README.md +++ b/instrumentation/executors/README.md @@ -1,6 +1,6 @@ # Settings for the executors instrumentation -| System property | Type | Default | Description | -|---|---|---|---| -| `otel.instrumentation.executors.include` | List | Empty | List of `Executor` subclasses to be instrumented. | +| System property | Type | Default | Description | +| -------------------------------------------- | ------- | ------- | -------------------------------------------------------------------------- | +| `otel.instrumentation.executors.include` | List | Empty | List of `Executor` subclasses to be instrumented. | | `otel.instrumentation.executors.include-all` | Boolean | `false` | Whether to instrument all classes that implement the `Executor` interface. | diff --git a/instrumentation/executors/bootstrap/src/main/java/io/opentelemetry/javaagent/bootstrap/executors/ContextPropagatingRunnable.java b/instrumentation/executors/bootstrap/src/main/java/io/opentelemetry/javaagent/bootstrap/executors/ContextPropagatingRunnable.java new file mode 100644 index 000000000000..99d3bd54ddf2 --- /dev/null +++ b/instrumentation/executors/bootstrap/src/main/java/io/opentelemetry/javaagent/bootstrap/executors/ContextPropagatingRunnable.java @@ -0,0 +1,44 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.bootstrap.executors; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.instrumentation.api.internal.ContextPropagationDebug; + +public final class ContextPropagatingRunnable implements Runnable { + + public static boolean shouldDecorateRunnable(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. + // note: it is always safe to decorate lambdas since downstream code cannot be expecting a + // specific runnable implementation anyways + return task.getClass().getName().contains("/") && !(task instanceof ContextPropagatingRunnable); + } + + public static Runnable propagateContext(Runnable task, Context context) { + return new ContextPropagatingRunnable(task, context); + } + + private final Runnable delegate; + private final Context context; + + private ContextPropagatingRunnable(Runnable delegate, Context context) { + this.delegate = delegate; + this.context = ContextPropagationDebug.addDebugInfo(context, delegate); + } + + @Override + public void run() { + try (Scope ignored = context.makeCurrent()) { + delegate.run(); + } + } + + public Runnable unwrap() { + return delegate; + } +} diff --git a/instrumentation/executors/bootstrap/src/main/java/io/opentelemetry/javaagent/bootstrap/executors/ExecutorAdviceHelper.java b/instrumentation/executors/bootstrap/src/main/java/io/opentelemetry/javaagent/bootstrap/executors/ExecutorAdviceHelper.java index 08f8aa4cb230..5471179df34d 100644 --- a/instrumentation/executors/bootstrap/src/main/java/io/opentelemetry/javaagent/bootstrap/executors/ExecutorAdviceHelper.java +++ b/instrumentation/executors/bootstrap/src/main/java/io/opentelemetry/javaagent/bootstrap/executors/ExecutorAdviceHelper.java @@ -17,13 +17,36 @@ */ public final class ExecutorAdviceHelper { + private static final ThreadLocal propagationDisabled = new ThreadLocal<>(); + + /** + * Temporarily disable context propagation for current thread. Call {@link #enablePropagation()} + * to re-enable the propagation. + */ + public static void disablePropagation() { + propagationDisabled.set(Boolean.TRUE); + } + + /** + * Enable context propagation for current thread after it was disabled by calling {@link + * #disablePropagation()}. + */ + public static void enablePropagation() { + propagationDisabled.remove(); + } + + // visible for testing + public static boolean isPropagationDisabled() { + return propagationDisabled.get() != null; + } + /** * Check if {@code context} should be propagated to the passed {@code task}. This method must be * called before each {@link #attachContextToTask(Context, VirtualField, Object)} call to ensure * that unwanted tasks are not instrumented. */ public static boolean shouldPropagateContext(Context context, @Nullable Object task) { - if (task == null) { + if (task == null || isPropagationDisabled()) { return false; } @@ -62,10 +85,7 @@ public static PropagatedContext attachContextToTask( } } - if (ContextPropagationDebug.isThreadPropagationDebuggerEnabled()) { - context = - ContextPropagationDebug.appendLocations(context, new Exception().getStackTrace(), task); - } + context = ContextPropagationDebug.addDebugInfo(context, task); propagatedContext.setContext(context); return propagatedContext; } @@ -92,6 +112,10 @@ public static void cleanUpAfterSubmit( /** Clean context attached to the given task. */ public static void cleanPropagatedContext( VirtualField virtualField, T task) { + if (isPropagationDisabled()) { + return; + } + PropagatedContext propagatedContext = virtualField.get(task); if (propagatedContext != null) { propagatedContext.clear(); diff --git a/instrumentation/executors/javaagent/build.gradle.kts b/instrumentation/executors/javaagent/build.gradle.kts index 0ae6b9a79e3f..65fd4332209a 100644 --- a/instrumentation/executors/javaagent/build.gradle.kts +++ b/instrumentation/executors/javaagent/build.gradle.kts @@ -13,8 +13,8 @@ dependencies { bootstrap(project(":instrumentation:executors:bootstrap")) testImplementation(project(":instrumentation:executors:testing")) - testImplementation("org.scala-lang:scala-library:2.11.12") + testCompileOnly(project(":instrumentation:executors:bootstrap")) } testing { @@ -29,6 +29,7 @@ testing { dependencies { implementation(project(":instrumentation:executors:testing")) + compileOnly(project(":instrumentation:executors:bootstrap")) } targets { @@ -44,10 +45,15 @@ testing { tasks { withType().configureEach { + // needed for VirtualThreadTest on jdk21 + jvmArgs("--add-opens=java.base/java.lang=ALL-UNNAMED") + jvmArgs("-XX:+IgnoreUnrecognizedVMOptions") + jvmArgs( - "-Dotel.instrumentation.executors.include=io.opentelemetry.javaagent.instrumentation.executors.ExecutorInstrumentationTest\$CustomThreadPoolExecutor" + "-Dotel.instrumentation.executors.include=io.opentelemetry.javaagent.instrumentation.executors.ExecutorInstrumentationTest\$CustomThreadPoolExecutor,io.opentelemetry.javaagent.instrumentation.executors.ThreadPoolExecutorTest\$RunnableCheckingThreadPoolExecutor" ) jvmArgs("-Djava.awt.headless=true") + jvmArgs("-Dotel.instrumentation.common.experimental.controller-telemetry.enabled=true") } check { diff --git a/instrumentation/executors/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/executors/AbstractExecutorInstrumentation.java b/instrumentation/executors/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/executors/AbstractExecutorInstrumentation.java deleted file mode 100644 index ee53135054c2..000000000000 --- a/instrumentation/executors/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/executors/AbstractExecutorInstrumentation.java +++ /dev/null @@ -1,144 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.javaagent.instrumentation.executors; - -import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.implementsInterface; -import static java.util.Collections.emptyList; -import static java.util.logging.Level.FINE; -import static net.bytebuddy.matcher.ElementMatchers.any; -import static net.bytebuddy.matcher.ElementMatchers.named; - -import io.opentelemetry.javaagent.bootstrap.internal.InstrumentationConfig; -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 java.util.logging.Logger; -import net.bytebuddy.description.type.TypeDescription; -import net.bytebuddy.matcher.ElementMatcher; - -public abstract class AbstractExecutorInstrumentation implements TypeInstrumentation { - private static final Logger logger = - Logger.getLogger(AbstractExecutorInstrumentation.class.getName()); - - 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 = - InstrumentationConfig.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.apache.tomcat.util.threads.ThreadPoolExecutor", - "org.eclipse.jetty.util.thread.QueuedThreadPool", // dispatch() covered in the jetty module - "org.eclipse.jetty.util.thread.ReservedThreadExecutor", - "org.glassfish.grizzly.threadpool.GrizzlyExecutorService", - "org.jboss.threads.EnhancedQueueExecutor", - "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( - InstrumentationConfig.get().getList(EXECUTORS_INCLUDE_PROPERTY_NAME, emptyList())); - 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 && hasExecutorInterfaceMatcher.matches(target)) { - logger.log(FINE, "Skipping executor instrumentation for {0}", target.getName()); - } - return allowed; - } - }); - } - return matcher.and(hasExecutorInterfaceMatcher); // Apply expensive matcher last. - } -} diff --git a/instrumentation/executors/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/executors/ExecutorMatchers.java b/instrumentation/executors/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/executors/ExecutorMatchers.java new file mode 100644 index 000000000000..717f088e4fcd --- /dev/null +++ b/instrumentation/executors/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/executors/ExecutorMatchers.java @@ -0,0 +1,149 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.executors; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.extendsClass; +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.implementsInterface; +import static java.util.Collections.emptyList; +import static java.util.logging.Level.FINE; +import static net.bytebuddy.matcher.ElementMatchers.any; +import static net.bytebuddy.matcher.ElementMatchers.named; + +import io.opentelemetry.javaagent.bootstrap.internal.AgentInstrumentationConfig; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.Executor; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.logging.Logger; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +final class ExecutorMatchers { + + private static final Logger logger = Logger.getLogger(ExecutorMatchers.class.getName()); + + /** + * Only apply executor instrumentation to allowed executors. To apply to all executors, use + * override setting above. + */ + private static final Set INSTRUMENTED_EXECUTOR_NAMES; + + /** + * 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 static final List INSTRUMENTED_EXECUTOR_PREFIXES; + + static { + Set combined = + new HashSet<>( + Arrays.asList( + "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", + "java.util.concurrent.ThreadPerTaskExecutor", + "org.apache.tomcat.util.threads.ThreadPoolExecutor", + "org.eclipse.jetty.util.thread.QueuedThreadPool", // dispatch() covered in the jetty + // module + "org.eclipse.jetty.util.thread.ReservedThreadExecutor", + "org.glassfish.grizzly.threadpool.GrizzlyExecutorService", + "org.jboss.threads.EnhancedQueueExecutor", + "org.apache.pekko.dispatch.BalancingDispatcher", + "org.apache.pekko.dispatch.Dispatcher", + "org.apache.pekko.dispatch.Dispatcher$LazyExecutorServiceDelegate", + "org.apache.pekko.dispatch.ExecutionContexts$sameThreadExecutionContext$", + "org.apache.pekko.dispatch.ForkJoinExecutorConfigurator$PekkoForkJoinPool", + "org.apache.pekko.dispatch.MessageDispatcher", + "org.apache.pekko.dispatch.PinnedDispatcher", + "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")); + combined.addAll( + AgentInstrumentationConfig.get() + .getList("otel.instrumentation.executors.include", emptyList())); + INSTRUMENTED_EXECUTOR_NAMES = Collections.unmodifiableSet(combined); + + INSTRUMENTED_EXECUTOR_PREFIXES = Collections.singletonList("slick.util.AsyncExecutor$"); + } + + static ElementMatcher.Junction executorNameMatcher() { + if (AgentInstrumentationConfig.get() + .getBoolean("otel.instrumentation.executors.include-all", false)) { + return any(); + } + + return new ElementMatcher.Junction.AbstractBase() { + @Override + public boolean matches(TypeDescription target) { + boolean allowed = INSTRUMENTED_EXECUTOR_NAMES.contains(target.getName()); + + // Check for possible prefixes match only if not allowed already + if (!allowed) { + for (String name : INSTRUMENTED_EXECUTOR_PREFIXES) { + if (target.getName().startsWith(name)) { + allowed = true; + break; + } + } + } + + // only log the statement if we log that level of detail + if (logger.isLoggable(FINE)) { + if (!allowed && isExecutor().matches(target)) { + logger.log(FINE, "Skipping executor instrumentation for {0}", target.getName()); + } + } + return allowed; + } + }; + } + + static ElementMatcher isExecutor() { + return implementsInterface(named(Executor.class.getName())); + } + + static ElementMatcher isThreadPoolExecutor() { + return extendsClass(named(ThreadPoolExecutor.class.getName())); + } + + private ExecutorMatchers() {} +} diff --git a/instrumentation/executors/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/executors/ExecutorsInstrumentationModule.java b/instrumentation/executors/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/executors/ExecutorsInstrumentationModule.java index 0f84ac69fe8d..60de54633025 100644 --- a/instrumentation/executors/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/executors/ExecutorsInstrumentationModule.java +++ b/instrumentation/executors/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/executors/ExecutorsInstrumentationModule.java @@ -16,10 +16,7 @@ public class ExecutorsInstrumentationModule extends InstrumentationModule { public ExecutorsInstrumentationModule() { - super( - "executors", - // TODO: remove that after release 1.26.0 - "executor"); + super("executors"); } @Override @@ -29,6 +26,9 @@ public List typeInstrumentations() { new FutureInstrumentation(), new JavaExecutorInstrumentation(), new JavaForkJoinTaskInstrumentation(), - new RunnableInstrumentation()); + new RunnableInstrumentation(), + new ThreadPoolExtendingExecutorInstrumentation(), + new VirtualThreadInstrumentation(), + new StructuredTaskScopeInstrumentation()); } } diff --git a/instrumentation/executors/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/executors/FutureInstrumentation.java b/instrumentation/executors/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/executors/FutureInstrumentation.java index c2dce950faa3..4562c2e80f99 100644 --- a/instrumentation/executors/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/executors/FutureInstrumentation.java +++ b/instrumentation/executors/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/executors/FutureInstrumentation.java @@ -61,6 +61,8 @@ public class FutureInstrumentation implements TypeInstrumentation { "java.util.concurrent.FutureTask", "java.util.concurrent.RecursiveAction", "java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask", + "org.apache.pekko.dispatch.ForkJoinExecutorConfigurator$PekkoForkJoinTask", + "org.apache.pekko.dispatch.Mailbox", "scala.collection.parallel.AdaptiveWorkStealingForkJoinTasks$WrappedTask", "scala.concurrent.forkjoin.ForkJoinTask", "scala.concurrent.forkjoin.ForkJoinTask$AdaptedCallable", diff --git a/instrumentation/executors/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/executors/JavaExecutorInstrumentation.java b/instrumentation/executors/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/executors/JavaExecutorInstrumentation.java index 07949cbe16b1..979fe92f48f6 100644 --- a/instrumentation/executors/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/executors/JavaExecutorInstrumentation.java +++ b/instrumentation/executors/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/executors/JavaExecutorInstrumentation.java @@ -6,6 +6,8 @@ package io.opentelemetry.javaagent.instrumentation.executors; import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.hasSuperType; +import static io.opentelemetry.javaagent.instrumentation.executors.ExecutorMatchers.executorNameMatcher; +import static io.opentelemetry.javaagent.instrumentation.executors.ExecutorMatchers.isExecutor; import static net.bytebuddy.matcher.ElementMatchers.is; import static net.bytebuddy.matcher.ElementMatchers.named; import static net.bytebuddy.matcher.ElementMatchers.namedOneOf; @@ -16,8 +18,10 @@ import io.opentelemetry.context.Context; import io.opentelemetry.instrumentation.api.util.VirtualField; import io.opentelemetry.javaagent.bootstrap.Java8BytecodeBridge; +import io.opentelemetry.javaagent.bootstrap.executors.ContextPropagatingRunnable; import io.opentelemetry.javaagent.bootstrap.executors.ExecutorAdviceHelper; import io.opentelemetry.javaagent.bootstrap.executors.PropagatedContext; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; import java.util.Collection; import java.util.Collections; @@ -25,8 +29,15 @@ import java.util.concurrent.ForkJoinTask; import java.util.concurrent.Future; import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; -public class JavaExecutorInstrumentation extends AbstractExecutorInstrumentation { +public class JavaExecutorInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return executorNameMatcher().and(isExecutor()); // Apply expensive matcher last. + } @Override public void transform(TypeTransformer transformer) { @@ -80,12 +91,16 @@ public static class SetExecuteRunnableStateAdvice { public static PropagatedContext enterJobSubmit( @Advice.Argument(value = 0, readOnly = false) Runnable task) { Context context = Java8BytecodeBridge.currentContext(); - if (ExecutorAdviceHelper.shouldPropagateContext(context, task)) { - VirtualField virtualField = - VirtualField.find(Runnable.class, PropagatedContext.class); - return ExecutorAdviceHelper.attachContextToTask(context, virtualField, task); + if (!ExecutorAdviceHelper.shouldPropagateContext(context, task)) { + return null; } - return null; + if (ContextPropagatingRunnable.shouldDecorateRunnable(task)) { + task = ContextPropagatingRunnable.propagateContext(task, context); + return null; + } + VirtualField virtualField = + VirtualField.find(Runnable.class, PropagatedContext.class); + return ExecutorAdviceHelper.attachContextToTask(context, virtualField, task); } @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) @@ -99,8 +114,7 @@ public static void exitJobSubmit( public static class SetJavaForkJoinStateAdvice { @Advice.OnMethodEnter(suppress = Throwable.class) - public static PropagatedContext enterJobSubmit( - @Advice.Argument(value = 0, readOnly = false) ForkJoinTask task) { + public static PropagatedContext enterJobSubmit(@Advice.Argument(0) ForkJoinTask task) { Context context = Java8BytecodeBridge.currentContext(); if (ExecutorAdviceHelper.shouldPropagateContext(context, task)) { VirtualField, PropagatedContext> virtualField = @@ -150,8 +164,7 @@ public static void exitJobSubmit( public static class SetCallableStateAdvice { @Advice.OnMethodEnter(suppress = Throwable.class) - public static PropagatedContext enterJobSubmit( - @Advice.Argument(value = 0, readOnly = false) Callable task) { + public static PropagatedContext enterJobSubmit(@Advice.Argument(0) Callable task) { Context context = Java8BytecodeBridge.currentContext(); if (ExecutorAdviceHelper.shouldPropagateContext(context, task)) { VirtualField, PropagatedContext> virtualField = @@ -180,7 +193,7 @@ public static class SetCallableStateForCallableCollectionAdvice { @Advice.OnMethodEnter(suppress = Throwable.class) public static Collection submitEnter( - @Advice.Argument(value = 0, readOnly = false) Collection> tasks) { + @Advice.Argument(0) Collection> tasks) { if (tasks == null) { return Collections.emptyList(); } diff --git a/instrumentation/executors/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/executors/StructuredTaskScopeInstrumentation.java b/instrumentation/executors/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/executors/StructuredTaskScopeInstrumentation.java new file mode 100644 index 000000000000..16678b268517 --- /dev/null +++ b/instrumentation/executors/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/executors/StructuredTaskScopeInstrumentation.java @@ -0,0 +1,57 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.executors; + +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.util.VirtualField; +import io.opentelemetry.javaagent.bootstrap.Java8BytecodeBridge; +import io.opentelemetry.javaagent.bootstrap.executors.ExecutorAdviceHelper; +import io.opentelemetry.javaagent.bootstrap.executors.PropagatedContext; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import java.util.concurrent.Callable; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class StructuredTaskScopeInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("java.util.concurrent.StructuredTaskScope"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + named("fork").and(takesArgument(0, Callable.class)), + this.getClass().getName() + "$ForkCallableAdvice"); + } + + @SuppressWarnings("unused") + public static class ForkCallableAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static PropagatedContext enterCallableFork(@Advice.Argument(0) Callable task) { + Context context = Java8BytecodeBridge.currentContext(); + if (ExecutorAdviceHelper.shouldPropagateContext(context, task)) { + VirtualField, PropagatedContext> virtualField = + VirtualField.find(Callable.class, PropagatedContext.class); + return ExecutorAdviceHelper.attachContextToTask(context, virtualField, task); + } + return null; + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void exitCallableFork( + @Advice.Enter PropagatedContext propagatedContext, @Advice.Thrown Throwable throwable) { + ExecutorAdviceHelper.cleanUpAfterSubmit(propagatedContext, throwable); + } + } +} diff --git a/instrumentation/executors/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/executors/ThreadPoolExtendingExecutorInstrumentation.java b/instrumentation/executors/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/executors/ThreadPoolExtendingExecutorInstrumentation.java new file mode 100644 index 000000000000..febd9fdfbcb8 --- /dev/null +++ b/instrumentation/executors/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/executors/ThreadPoolExtendingExecutorInstrumentation.java @@ -0,0 +1,58 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.executors; + +import static io.opentelemetry.javaagent.instrumentation.executors.ExecutorMatchers.executorNameMatcher; +import static io.opentelemetry.javaagent.instrumentation.executors.ExecutorMatchers.isThreadPoolExecutor; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import io.opentelemetry.javaagent.bootstrap.executors.ContextPropagatingRunnable; +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 ThreadPoolExtendingExecutorInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return executorNameMatcher().and(isThreadPoolExecutor()); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + named("beforeExecute").and(takesArgument(1, Runnable.class)), + this.getClass().getName() + "$BeforeExecuteAdvice"); + transformer.applyAdviceToMethod( + named("afterExecute").and(takesArgument(0, Runnable.class)), + this.getClass().getName() + "$AfterExecuteAdvice"); + } + + @SuppressWarnings("unused") + public static class BeforeExecuteAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter(@Advice.Argument(value = 1, readOnly = false) Runnable runnable) { + if (runnable instanceof ContextPropagatingRunnable) { + runnable = ((ContextPropagatingRunnable) runnable).unwrap(); + } + } + } + + @SuppressWarnings("unused") + public static class AfterExecuteAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter(@Advice.Argument(value = 0, readOnly = false) Runnable runnable) { + if (runnable instanceof ContextPropagatingRunnable) { + runnable = ((ContextPropagatingRunnable) runnable).unwrap(); + } + } + } +} diff --git a/instrumentation/executors/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/executors/VirtualThreadInstrumentation.java b/instrumentation/executors/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/executors/VirtualThreadInstrumentation.java new file mode 100644 index 000000000000..a348e0539e5e --- /dev/null +++ b/instrumentation/executors/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/executors/VirtualThreadInstrumentation.java @@ -0,0 +1,57 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.executors; + +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import io.opentelemetry.javaagent.bootstrap.executors.ExecutorAdviceHelper; +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 VirtualThreadInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("java.lang.VirtualThread"); + } + + @Override + public void transform(TypeTransformer transformer) { + // Disable context propagation when virtual thread is switched to the carrier thread. We should + // not propagate context on the carrier thread. Also, context propagation code can cause the + // carrier thread to park when it normally does not park, which may be unexpected for the jvm. + // https://github.com/open-telemetry/opentelemetry-java-instrumentation/issues/10747 + transformer.applyAdviceToMethod( + named("switchToCarrierThread").and(takesArguments(0)), + this.getClass().getName() + "$SwitchToCarrierAdvice"); + transformer.applyAdviceToMethod( + // takes an extra argument in jdk 21 ea versions + named("switchToVirtualThread").and(takesArguments(1).or(takesArguments(2))), + this.getClass().getName() + "$SwitchToVirtualAdvice"); + } + + @SuppressWarnings("unused") + public static class SwitchToCarrierAdvice { + + @Advice.OnMethodExit(suppress = Throwable.class) + public static void exit() { + ExecutorAdviceHelper.disablePropagation(); + } + } + + @SuppressWarnings("unused") + public static class SwitchToVirtualAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void enter() { + ExecutorAdviceHelper.enablePropagation(); + } + } +} diff --git a/instrumentation/executors/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/executors/LambdaContextPropagationTest.java b/instrumentation/executors/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/executors/LambdaContextPropagationTest.java new file mode 100644 index 000000000000..ad46803d30ab --- /dev/null +++ b/instrumentation/executors/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/executors/LambdaContextPropagationTest.java @@ -0,0 +1,44 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.executors; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.baggage.Baggage; +import io.opentelemetry.context.Scope; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicInteger; +import org.junit.jupiter.api.Test; + +// regression test for #9175 +class LambdaContextPropagationTest { + + // must be static! the lambda that uses that must be non-capturing + private static final AtomicInteger failureCounter = new AtomicInteger(); + + @Test + void shouldCorrectlyPropagateContextToRunnables() { + ExecutorService executor = Executors.newSingleThreadExecutor(); + + Baggage baggage = Baggage.builder().put("test", "test").build(); + try (Scope ignored = baggage.makeCurrent()) { + for (int i = 0; i < 20; i++) { + // must text execute() -- other methods like submit() decorate the Runnable with a + // FutureTask + executor.execute(LambdaContextPropagationTest::assertBaggage); + } + } + + assertThat(failureCounter).hasValue(0); + } + + private static void assertBaggage() { + if (Baggage.current().getEntryValue("test") == null) { + failureCounter.incrementAndGet(); + } + } +} diff --git a/instrumentation/executors/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/executors/ThreadPoolExecutorTest.java b/instrumentation/executors/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/executors/ThreadPoolExecutorTest.java new file mode 100644 index 000000000000..e31d0d2d4fa0 --- /dev/null +++ b/instrumentation/executors/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/executors/ThreadPoolExecutorTest.java @@ -0,0 +1,62 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.executors; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +import io.opentelemetry.api.baggage.Baggage; +import io.opentelemetry.context.Scope; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import org.junit.jupiter.api.Test; + +class ThreadPoolExecutorTest { + + @Test + void shouldPassOriginalRunnableToBeforeAfterMethods() throws InterruptedException { + CountDownLatch latch = new CountDownLatch(1); + Runnable task = latch::countDown; + + RunnableCheckingThreadPoolExecutor executor = new RunnableCheckingThreadPoolExecutor(task); + + Baggage baggage = Baggage.builder().put("test", "test").build(); + try (Scope ignored = baggage.makeCurrent()) { + executor.execute(task); + } + latch.await(10, TimeUnit.SECONDS); + + assertThat(executor.sameTaskBefore).isTrue(); + await().untilAsserted(() -> assertThat(executor.sameTaskAfter).isTrue()); + } + + // class is configured to be instrumented via otel.instrumentation.executors.include + static class RunnableCheckingThreadPoolExecutor extends ThreadPoolExecutor { + + final Runnable expectedTask; + + final AtomicBoolean sameTaskBefore = new AtomicBoolean(); + final AtomicBoolean sameTaskAfter = new AtomicBoolean(); + + RunnableCheckingThreadPoolExecutor(Runnable expectedTask) { + super(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>()); + this.expectedTask = expectedTask; + } + + @Override + protected void beforeExecute(Thread t, Runnable r) { + sameTaskBefore.set(r == expectedTask); + } + + @Override + protected void afterExecute(Runnable r, Throwable t) { + sameTaskAfter.set(r == expectedTask); + } + } +} diff --git a/instrumentation/executors/jdk21-testing/build.gradle.kts b/instrumentation/executors/jdk21-testing/build.gradle.kts new file mode 100644 index 000000000000..6f07c501dfd6 --- /dev/null +++ b/instrumentation/executors/jdk21-testing/build.gradle.kts @@ -0,0 +1,39 @@ +import kotlin.math.max + +plugins { + id("otel.javaagent-testing") +} + +dependencies { + testInstrumentation(project(":instrumentation:executors:javaagent")) + + testCompileOnly(project(":instrumentation:executors:bootstrap")) + testImplementation(project(":instrumentation:executors:testing")) +} + +otelJava { + // StructuredTaskScopeTest that uses preview feature, requires that the test is compiled for the + // same vm version that is going to execute the test. Choose whichever is greater 21 or the + // version of the vm that is going to run test + val testJavaVersion = + gradle.startParameter.projectProperties["testJavaVersion"]?.let(JavaVersion::toVersion) + ?: JavaVersion.current() + minJavaVersionSupported.set(JavaVersion.toVersion(max( + testJavaVersion.majorVersion.toInt(), + JavaVersion.VERSION_21.majorVersion.toInt() + ))) +} + +tasks.withType().configureEach { + with(options) { + compilerArgs.add("--enable-preview") + } +} + +tasks.withType().configureEach { + // needed for VirtualThreadTest + jvmArgs("--add-opens=java.base/java.lang=ALL-UNNAMED") + jvmArgs("-XX:+IgnoreUnrecognizedVMOptions") + // needed for structured concurrency test + jvmArgs("--enable-preview") +} diff --git a/instrumentation/executors/jdk21-testing/src/test/java/io/opentelemetry/javaagent/instrumentation/executors/StructuredTaskScopeTest.java b/instrumentation/executors/jdk21-testing/src/test/java/io/opentelemetry/javaagent/instrumentation/executors/StructuredTaskScopeTest.java new file mode 100644 index 000000000000..595f1c8c40ff --- /dev/null +++ b/instrumentation/executors/jdk21-testing/src/test/java/io/opentelemetry/javaagent/instrumentation/executors/StructuredTaskScopeTest.java @@ -0,0 +1,63 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.executors; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import java.util.concurrent.Callable; +import java.util.concurrent.StructuredTaskScope; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +@SuppressWarnings("preview") +class StructuredTaskScopeTest { + + @RegisterExtension + static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + @Test + void multipleForkJoin() throws Exception { + StructuredTaskScope taskScope = new StructuredTaskScope.ShutdownOnFailure(); + + Callable callable1 = + () -> { + testing.runWithSpan("task1", () -> {}); + return "a"; + }; + Callable callable2 = + () -> { + testing.runWithSpan("task2", () -> {}); + return "b"; + }; + + String result = + testing.runWithSpan( + "parent", + () -> { + StructuredTaskScope.Subtask fork1 = taskScope.fork(callable1); + StructuredTaskScope.Subtask fork2 = taskScope.fork(callable2); + taskScope.join(); + + return "" + fork1.get() + fork2.get(); + }); + + assertThat(result).isEqualTo("ab"); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactlyInAnyOrder( + span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(), + span -> + span.hasName("task1").hasKind(SpanKind.INTERNAL).hasParent(trace.getSpan(0)), + span -> + span.hasName("task2").hasKind(SpanKind.INTERNAL).hasParent(trace.getSpan(0)))); + + taskScope.close(); + } +} diff --git a/instrumentation/executors/jdk21-testing/src/test/java/io/opentelemetry/javaagent/instrumentation/executors/VirtualThreadExecutorTest.java b/instrumentation/executors/jdk21-testing/src/test/java/io/opentelemetry/javaagent/instrumentation/executors/VirtualThreadExecutorTest.java new file mode 100644 index 000000000000..e9a6e1a1fcdb --- /dev/null +++ b/instrumentation/executors/jdk21-testing/src/test/java/io/opentelemetry/javaagent/instrumentation/executors/VirtualThreadExecutorTest.java @@ -0,0 +1,28 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.executors; + +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import org.junit.jupiter.api.extension.RegisterExtension; + +class VirtualThreadExecutorTest + extends AbstractExecutorServiceTest { + + @RegisterExtension + static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + VirtualThreadExecutorTest() { + super(Executors.newVirtualThreadPerTaskExecutor(), testing); + } + + @Override + protected JavaAsyncChild newTask(boolean doTraceableWork, boolean blockThread) { + return new JavaAsyncChild(doTraceableWork, blockThread); + } +} diff --git a/instrumentation/executors/jdk21-testing/src/test/java/io/opentelemetry/javaagent/instrumentation/executors/VirtualThreadTest.java b/instrumentation/executors/jdk21-testing/src/test/java/io/opentelemetry/javaagent/instrumentation/executors/VirtualThreadTest.java new file mode 100644 index 000000000000..924856c6dfca --- /dev/null +++ b/instrumentation/executors/jdk21-testing/src/test/java/io/opentelemetry/javaagent/instrumentation/executors/VirtualThreadTest.java @@ -0,0 +1,55 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.executors; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.javaagent.bootstrap.executors.ExecutorAdviceHelper; +import java.lang.reflect.Method; +import java.util.concurrent.Callable; +import java.util.concurrent.atomic.AtomicBoolean; +import org.junit.jupiter.api.Test; + +class VirtualThreadTest { + + @Test + void testDisableContextPropagation() throws Exception { + TestRunnable testRunnable = new TestRunnable(); + Thread thread = Thread.ofVirtual().start(testRunnable); + thread.join(); + + assertThat(testRunnable.error).isNull(); + assertThat(testRunnable.isPropagationDisabled.get()).isTrue(); + } + + private static void executeOnCarrierThread(Callable callable) throws Exception { + // call VirtualThread.executeOnCarrierThread, VirtualThreadInstrumentation disables context + // propagation inside that method + Method executeOnCarrierThreadMethod = + Class.forName("java.lang.VirtualThread") + .getDeclaredMethod("executeOnCarrierThread", Callable.class); + executeOnCarrierThreadMethod.setAccessible(true); + executeOnCarrierThreadMethod.invoke(Thread.currentThread(), callable); + } + + static class TestRunnable implements Runnable { + AtomicBoolean isPropagationDisabled = new AtomicBoolean(); + Exception error; + + @Override + public void run() { + try { + executeOnCarrierThread( + () -> { + isPropagationDisabled.set(ExecutorAdviceHelper.isPropagationDisabled()); + return null; + }); + } catch (Exception exception) { + error = exception; + } + } + } +} diff --git a/instrumentation/executors/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/executors/JavaAsyncChild.java b/instrumentation/executors/testing/src/main/java/io/opentelemetry/javaagent/instrumentation/executors/JavaAsyncChild.java similarity index 98% rename from instrumentation/executors/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/executors/JavaAsyncChild.java rename to instrumentation/executors/testing/src/main/java/io/opentelemetry/javaagent/instrumentation/executors/JavaAsyncChild.java index 68c45abbf04a..fa136cc83e02 100644 --- a/instrumentation/executors/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/executors/JavaAsyncChild.java +++ b/instrumentation/executors/testing/src/main/java/io/opentelemetry/javaagent/instrumentation/executors/JavaAsyncChild.java @@ -11,6 +11,7 @@ import java.util.concurrent.ForkJoinTask; import java.util.concurrent.atomic.AtomicBoolean; +@SuppressWarnings("serial") final class JavaAsyncChild extends ForkJoinTask implements TestTask { private static final Tracer tracer = GlobalOpenTelemetry.getTracer("test"); diff --git a/instrumentation/external-annotations/README.md b/instrumentation/external-annotations/README.md index 143dbcbdd572..5dae19d769b5 100644 --- a/instrumentation/external-annotations/README.md +++ b/instrumentation/external-annotations/README.md @@ -1,6 +1,6 @@ # Settings for the external annotations instrumentation -| System property | Type | Default | Description | -|----------------- |------ |--------- |------------- | -| `otel.instrumentation.external-annotations.include` | String | Default annotations | Configuration for trace annotations, in the form of a pattern that matches `'package.Annotation$Name;*'`. -| `otel.instrumentation.external-annotations.exclude-methods` | String | | All methods to be excluded from auto-instrumentation by annotation-based advices. | +| System property | Type | Default | Description | +| ----------------------------------------------------------- | ------ | ------------------- | --------------------------------------------------------------------------------------------------------- | +| `otel.instrumentation.external-annotations.include` | String | Default annotations | Configuration for trace annotations, in the form of a pattern that matches `'package.Annotation$Name;*'`. | +| `otel.instrumentation.external-annotations.exclude-methods` | String | | All methods to be excluded from auto-instrumentation by annotation-based advices. | diff --git a/instrumentation/external-annotations/javaagent-unit-tests/build.gradle.kts b/instrumentation/external-annotations/javaagent-unit-tests/build.gradle.kts index 364911c6d0df..7ce37d7de0b3 100644 --- a/instrumentation/external-annotations/javaagent-unit-tests/build.gradle.kts +++ b/instrumentation/external-annotations/javaagent-unit-tests/build.gradle.kts @@ -4,6 +4,7 @@ plugins { dependencies { testImplementation(project(":instrumentation-api")) + testImplementation(project(":instrumentation-api-incubator")) testImplementation(project(":javaagent-extension-api")) testImplementation(project(":javaagent-tooling")) testImplementation(project(":instrumentation:external-annotations:javaagent")) diff --git a/instrumentation/external-annotations/javaagent-unit-tests/src/test/groovy/IncludeTest.groovy b/instrumentation/external-annotations/javaagent-unit-tests/src/test/groovy/IncludeTest.groovy deleted file mode 100644 index e284fe47309c..000000000000 --- a/instrumentation/external-annotations/javaagent-unit-tests/src/test/groovy/IncludeTest.groovy +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -import io.opentelemetry.javaagent.bootstrap.internal.InstrumentationConfig -import io.opentelemetry.javaagent.instrumentation.extannotations.ExternalAnnotationInstrumentation -import spock.lang.Specification -import spock.lang.Unroll - -import static io.opentelemetry.javaagent.instrumentation.extannotations.ExternalAnnotationInstrumentation.DEFAULT_ANNOTATIONS - -class IncludeTest extends Specification { - - @Unroll - def "test configuration #value"() { - setup: - InstrumentationConfig config = Mock() - config.getString("otel.instrumentation.external-annotations.include") >> value - - expect: - ExternalAnnotationInstrumentation.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/instrumentation/external-annotations/javaagent-unit-tests/src/test/java/io/opentelemetry/javaagent/instrumentation/extannotations/IncludeTest.java b/instrumentation/external-annotations/javaagent-unit-tests/src/test/java/io/opentelemetry/javaagent/instrumentation/extannotations/IncludeTest.java new file mode 100644 index 000000000000..9f717b107392 --- /dev/null +++ b/instrumentation/external-annotations/javaagent-unit-tests/src/test/java/io/opentelemetry/javaagent/instrumentation/extannotations/IncludeTest.java @@ -0,0 +1,53 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.extannotations; + +import static io.opentelemetry.javaagent.instrumentation.extannotations.ExternalAnnotationInstrumentation.DEFAULT_ANNOTATIONS; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +import io.opentelemetry.instrumentation.api.incubator.config.internal.InstrumentationConfig; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +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 IncludeTest { + + @Mock InstrumentationConfig config; + + @ParameterizedTest + @MethodSource("provideArguments") + void testConfiguration(String value, List expected) { + when(config.getString("otel.instrumentation.external-annotations.include")).thenReturn(value); + + assertThat(ExternalAnnotationInstrumentation.configureAdditionalTraceAnnotations(config)) + .isEqualTo(new HashSet<>(expected)); + } + + private static Stream provideArguments() { + return Stream.of( + Arguments.of(null, DEFAULT_ANNOTATIONS), + Arguments.of(" ", Collections.emptyList()), + Arguments.of("some.Invalid[]", Collections.emptyList()), + Arguments.of( + "some.package.ClassName ", Collections.singletonList("some.package.ClassName")), + Arguments.of( + " some.package.Class$Name", Collections.singletonList("some.package.Class$Name")), + Arguments.of(" ClassName ", Collections.singletonList("ClassName")), + Arguments.of("ClassName", Collections.singletonList("ClassName")), + Arguments.of("Class$1;Class$2", Arrays.asList("Class$1", "Class$2")), + Arguments.of("Duplicate ;Duplicate ;Duplicate; ", Collections.singletonList("Duplicate"))); + } +} diff --git a/instrumentation/external-annotations/javaagent/build.gradle.kts b/instrumentation/external-annotations/javaagent/build.gradle.kts index 73da6e3aff76..c42a62ba0df4 100644 --- a/instrumentation/external-annotations/javaagent/build.gradle.kts +++ b/instrumentation/external-annotations/javaagent/build.gradle.kts @@ -39,7 +39,7 @@ tasks { includeTestsMatching("ConfiguredTraceAnnotationsTest") } include("**/ConfiguredTraceAnnotationsTest.*") - jvmArgs("-Dotel.instrumentation.external-annotations.include=package.Class\$Name;OuterClass\$InterestingMethod") + jvmArgs("-Dotel.instrumentation.external-annotations.include=io.opentelemetry.javaagent.instrumentation.extannotations.OuterClass\$InterestingMethod") } val testExcludeMethodsProperty by registering(Test::class) { @@ -47,7 +47,9 @@ tasks { includeTestsMatching("TracedMethodsExclusionTest") } include("**/TracedMethodsExclusionTest.*") - jvmArgs("-Dotel.instrumentation.external-annotations.exclude-methods=TracedMethodsExclusionTest\$TestClass[excluded,annotatedButExcluded]") + jvmArgs( + "-Dotel.instrumentation.external-annotations.exclude-methods=io.opentelemetry.javaagent.instrumentation.extannotations.TracedMethodsExclusionTest\$TestClass[excluded,annotatedButExcluded]" + ) } test { diff --git a/instrumentation/external-annotations/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/extannotations/ExternalAnnotationInstrumentation.java b/instrumentation/external-annotations/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/extannotations/ExternalAnnotationInstrumentation.java index ddceefb9af34..ee6baffd60bd 100644 --- a/instrumentation/external-annotations/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/extannotations/ExternalAnnotationInstrumentation.java +++ b/instrumentation/external-annotations/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/extannotations/ExternalAnnotationInstrumentation.java @@ -17,9 +17,10 @@ import io.opentelemetry.context.Context; import io.opentelemetry.context.Scope; -import io.opentelemetry.instrumentation.api.instrumenter.util.ClassAndMethod; +import io.opentelemetry.instrumentation.api.incubator.config.internal.InstrumentationConfig; +import io.opentelemetry.instrumentation.api.incubator.semconv.util.ClassAndMethod; import io.opentelemetry.javaagent.bootstrap.Java8BytecodeBridge; -import io.opentelemetry.javaagent.bootstrap.internal.InstrumentationConfig; +import io.opentelemetry.javaagent.bootstrap.internal.AgentInstrumentationConfig; import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; import io.opentelemetry.javaagent.tooling.config.MethodsConfigurationParser; @@ -52,7 +53,8 @@ public class ExternalAnnotationInstrumentation implements TypeInstrumentation { + PACKAGE_CLASS_NAME_REGEX + "\\s*;?\\s*"; - private static final List DEFAULT_ANNOTATIONS = + // visible for testing + static final List DEFAULT_ANNOTATIONS = Arrays.asList( "com.appoptics.api.ext.LogMethod", "com.newrelic.api.agent.Trace", @@ -77,7 +79,7 @@ public class ExternalAnnotationInstrumentation implements TypeInstrumentation { public ExternalAnnotationInstrumentation() { Set additionalTraceAnnotations = - configureAdditionalTraceAnnotations(InstrumentationConfig.get()); + configureAdditionalTraceAnnotations(AgentInstrumentationConfig.get()); if (additionalTraceAnnotations.isEmpty()) { classLoaderOptimization = none(); @@ -111,7 +113,8 @@ public void transform(TypeTransformer transformer) { ExternalAnnotationInstrumentation.class.getName() + "$ExternalAnnotationAdvice"); } - private static Set configureAdditionalTraceAnnotations(InstrumentationConfig config) { + // visible for testing + static Set configureAdditionalTraceAnnotations(InstrumentationConfig config) { String configString = config.getString(TRACE_ANNOTATIONS_CONFIG); if (configString == null) { return Collections.unmodifiableSet(new HashSet<>(DEFAULT_ANNOTATIONS)); @@ -144,7 +147,7 @@ private static ElementMatcher.Junction configureExcludedMetho Map> excludedMethods = MethodsConfigurationParser.parse( - InstrumentationConfig.get().getString(TRACE_ANNOTATED_METHODS_EXCLUDE_CONFIG)); + AgentInstrumentationConfig.get().getString(TRACE_ANNOTATED_METHODS_EXCLUDE_CONFIG)); for (Map.Entry> entry : excludedMethods.entrySet()) { String className = entry.getKey(); ElementMatcher.Junction classMather = diff --git a/instrumentation/external-annotations/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/extannotations/ExternalAnnotationSingletons.java b/instrumentation/external-annotations/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/extannotations/ExternalAnnotationSingletons.java index eefa5d3f6dbf..b98967d79d32 100644 --- a/instrumentation/external-annotations/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/extannotations/ExternalAnnotationSingletons.java +++ b/instrumentation/external-annotations/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/extannotations/ExternalAnnotationSingletons.java @@ -6,11 +6,11 @@ package io.opentelemetry.javaagent.instrumentation.extannotations; import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.instrumentation.api.incubator.semconv.code.CodeAttributesExtractor; +import io.opentelemetry.instrumentation.api.incubator.semconv.code.CodeAttributesGetter; +import io.opentelemetry.instrumentation.api.incubator.semconv.code.CodeSpanNameExtractor; +import io.opentelemetry.instrumentation.api.incubator.semconv.util.ClassAndMethod; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; -import io.opentelemetry.instrumentation.api.instrumenter.code.CodeAttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.code.CodeAttributesGetter; -import io.opentelemetry.instrumentation.api.instrumenter.code.CodeSpanNameExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.util.ClassAndMethod; public final class ExternalAnnotationSingletons { diff --git a/instrumentation/external-annotations/javaagent/src/test/groovy/ConfiguredTraceAnnotationsTest.groovy b/instrumentation/external-annotations/javaagent/src/test/groovy/ConfiguredTraceAnnotationsTest.groovy deleted file mode 100644 index 39ba2dec10ad..000000000000 --- a/instrumentation/external-annotations/javaagent/src/test/groovy/ConfiguredTraceAnnotationsTest.groovy +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes -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 { - "$SemanticAttributes.CODE_NAMESPACE" AnnotationTracedCallable.name - "$SemanticAttributes.CODE_FUNCTION" "call" - } - } - } - } - } - - static class AnnotationTracedCallable implements Callable { - @OuterClass.InterestingMethod - @Override - String call() throws Exception { - return "Hello!" - } - } -} diff --git a/instrumentation/external-annotations/javaagent/src/test/groovy/TraceAnnotationsTest.groovy b/instrumentation/external-annotations/javaagent/src/test/groovy/TraceAnnotationsTest.groovy deleted file mode 100644 index 18c44f8a35f3..000000000000 --- a/instrumentation/external-annotations/javaagent/src/test/groovy/TraceAnnotationsTest.groovy +++ /dev/null @@ -1,152 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes -import io.opentelemetry.test.annotation.SayTracedHello -import io.opentracing.contrib.dropwizard.Trace - -import java.util.concurrent.Callable - -import static io.opentelemetry.api.trace.StatusCode.ERROR - -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 { - "$SemanticAttributes.CODE_NAMESPACE" SayTracedHello.name - "$SemanticAttributes.CODE_FUNCTION" "sayHello" - "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 { - "$SemanticAttributes.CODE_NAMESPACE" SayTracedHello.name - "$SemanticAttributes.CODE_FUNCTION" "sayHelloSayHa" - "myattr" "test2" - } - } - span(1) { - name "SayTracedHello.sayHello" - childOf span(0) - attributes { - "$SemanticAttributes.CODE_NAMESPACE" SayTracedHello.name - "$SemanticAttributes.CODE_FUNCTION" "sayHello" - "myattr" "test" - } - } - span(2) { - name "SayTracedHello.sayHello" - childOf span(0) - attributes { - "$SemanticAttributes.CODE_NAMESPACE" SayTracedHello.name - "$SemanticAttributes.CODE_FUNCTION" "sayHello" - "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) - attributes { - "$SemanticAttributes.CODE_NAMESPACE" SayTracedHello.name - "$SemanticAttributes.CODE_FUNCTION" "sayError" - } - } - } - } - } - - 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 { - "$SemanticAttributes.CODE_NAMESPACE" SayTracedHello.name + '$1' - "$SemanticAttributes.CODE_FUNCTION" "call" - } - } - } - } - - 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 { - "$SemanticAttributes.CODE_NAMESPACE" SayTracedHello.name + '$1' - "$SemanticAttributes.CODE_FUNCTION" "call" - } - } - trace(1, 1) { - span(0) { - name "TraceAnnotationsTest\$1.call" - attributes { - "$SemanticAttributes.CODE_NAMESPACE" TraceAnnotationsTest.name + '$1' - "$SemanticAttributes.CODE_FUNCTION" "call" - } - } - } - } - } - } -} diff --git a/instrumentation/external-annotations/javaagent/src/test/groovy/TraceProvidersTest.groovy b/instrumentation/external-annotations/javaagent/src/test/groovy/TraceProvidersTest.groovy deleted file mode 100644 index ee348dae997a..000000000000 --- a/instrumentation/external-annotations/javaagent/src/test/groovy/TraceProvidersTest.groovy +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes -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 { - "$SemanticAttributes.CODE_NAMESPACE" SayTracedHello.name - "$SemanticAttributes.CODE_FUNCTION" "${provider.toLowerCase()}" - "providerAttr" provider - } - } - } - } - - where: - provider << ["AppOptics", "Datadog", "Dropwizard", "KamonOld", "KamonNew", "NewRelic", "SignalFx", "Sleuth", "Tracelytics"] - } - -} diff --git a/instrumentation/external-annotations/javaagent/src/test/groovy/TracedMethodsExclusionTest.groovy b/instrumentation/external-annotations/javaagent/src/test/groovy/TracedMethodsExclusionTest.groovy deleted file mode 100644 index 66653187fae8..000000000000 --- a/instrumentation/external-annotations/javaagent/src/test/groovy/TracedMethodsExclusionTest.groovy +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes -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 { - "$SemanticAttributes.CODE_NAMESPACE" TestClass.name - "$SemanticAttributes.CODE_FUNCTION" "annotated" - } - } - } - } - } - - 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/instrumentation/external-annotations/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/extannotations/ConfiguredTraceAnnotationsTest.java b/instrumentation/external-annotations/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/extannotations/ConfiguredTraceAnnotationsTest.java new file mode 100644 index 000000000000..220a050b007f --- /dev/null +++ b/instrumentation/external-annotations/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/extannotations/ConfiguredTraceAnnotationsTest.java @@ -0,0 +1,51 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.extannotations; + +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.semconv.incubating.CodeIncubatingAttributes; +import java.util.concurrent.Callable; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +class ConfiguredTraceAnnotationsTest { + + @RegisterExtension + static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + @Test + void testMethodWithDisabledNewRelicAnnotationShouldBeIgnored() { + SayTracedHello.fromCallableWhenDisabled(); + assertThat(testing.spans()).isEmpty(); + } + + @Test + void testMethodWithAnnotationShouldBeTraced() { + assertThat(new AnnotationTracedCallable().call()).isEqualTo("Hello!"); + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("AnnotationTracedCallable.call") + .hasAttributesSatisfyingExactly( + equalTo( + CodeIncubatingAttributes.CODE_NAMESPACE, + AnnotationTracedCallable.class.getName()), + equalTo(CodeIncubatingAttributes.CODE_FUNCTION, "call")))); + } + + static class AnnotationTracedCallable implements Callable { + @OuterClass.InterestingMethod + @Override + public String call() { + return "Hello!"; + } + } +} diff --git a/instrumentation/external-annotations/javaagent/src/test/java/OuterClass.java b/instrumentation/external-annotations/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/extannotations/OuterClass.java similarity index 85% rename from instrumentation/external-annotations/javaagent/src/test/java/OuterClass.java rename to instrumentation/external-annotations/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/extannotations/OuterClass.java index 951d8d95c242..8d47c7165de4 100644 --- a/instrumentation/external-annotations/javaagent/src/test/java/OuterClass.java +++ b/instrumentation/external-annotations/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/extannotations/OuterClass.java @@ -3,6 +3,8 @@ * SPDX-License-Identifier: Apache-2.0 */ +package io.opentelemetry.javaagent.instrumentation.extannotations; + import static java.lang.annotation.ElementType.METHOD; import static java.lang.annotation.RetentionPolicy.RUNTIME; diff --git a/instrumentation/external-annotations/javaagent/src/test/java/io/opentelemetry/test/annotation/SayTracedHello.java b/instrumentation/external-annotations/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/extannotations/SayTracedHello.java similarity index 97% rename from instrumentation/external-annotations/javaagent/src/test/java/io/opentelemetry/test/annotation/SayTracedHello.java rename to instrumentation/external-annotations/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/extannotations/SayTracedHello.java index e3a196bbb355..7dae3fb47395 100644 --- a/instrumentation/external-annotations/javaagent/src/test/java/io/opentelemetry/test/annotation/SayTracedHello.java +++ b/instrumentation/external-annotations/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/extannotations/SayTracedHello.java @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.test.annotation; +package io.opentelemetry.javaagent.instrumentation.extannotations; import io.opentelemetry.api.trace.Span; import java.util.concurrent.Callable; diff --git a/instrumentation/external-annotations/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/extannotations/TraceAnnotationsTest.java b/instrumentation/external-annotations/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/extannotations/TraceAnnotationsTest.java new file mode 100644 index 000000000000..4bdc9aa6e089 --- /dev/null +++ b/instrumentation/external-annotations/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/extannotations/TraceAnnotationsTest.java @@ -0,0 +1,145 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.extannotations; + +import static io.opentelemetry.api.common.AttributeKey.stringKey; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; +import static org.assertj.core.api.Assertions.catchThrowable; + +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.sdk.trace.data.StatusData; +import io.opentelemetry.semconv.incubating.CodeIncubatingAttributes; +import io.opentracing.contrib.dropwizard.Trace; +import java.util.concurrent.Callable; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +class TraceAnnotationsTest { + + @RegisterExtension + static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + @Test + void testSimpleCaseAnnotations() { + // Test single span in new trace + SayTracedHello.sayHello(); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("SayTracedHello.sayHello") + .hasNoParent() + .hasAttributesSatisfyingExactly( + equalTo( + CodeIncubatingAttributes.CODE_NAMESPACE, + SayTracedHello.class.getName()), + equalTo(CodeIncubatingAttributes.CODE_FUNCTION, "sayHello"), + equalTo(stringKey("myattr"), "test")))); + } + + @Test + void testComplexCaseAnnotations() { + // Test new trace with 2 children spans + SayTracedHello.sayHelloSayHa(); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("SayTracedHello.sayHelloSayHa") + .hasNoParent() + .hasAttributesSatisfyingExactly( + equalTo( + CodeIncubatingAttributes.CODE_NAMESPACE, + SayTracedHello.class.getName()), + equalTo(CodeIncubatingAttributes.CODE_FUNCTION, "sayHelloSayHa"), + equalTo(stringKey("myattr"), "test2")), + span -> + span.hasName("SayTracedHello.sayHello") + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo( + CodeIncubatingAttributes.CODE_NAMESPACE, + SayTracedHello.class.getName()), + equalTo(CodeIncubatingAttributes.CODE_FUNCTION, "sayHello"), + equalTo(stringKey("myattr"), "test")), + span -> + span.hasName("SayTracedHello.sayHello") + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo( + CodeIncubatingAttributes.CODE_NAMESPACE, + SayTracedHello.class.getName()), + equalTo(CodeIncubatingAttributes.CODE_FUNCTION, "sayHello"), + equalTo(stringKey("myattr"), "test")))); + } + + @Test + void testExceptionExit() { + Throwable thrown = catchThrowable(SayTracedHello::sayError); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("SayTracedHello.sayError") + .hasStatus(StatusData.error()) + .hasException(thrown) + .hasAttributesSatisfyingExactly( + equalTo( + CodeIncubatingAttributes.CODE_NAMESPACE, + SayTracedHello.class.getName()), + equalTo(CodeIncubatingAttributes.CODE_FUNCTION, "sayError")))); + } + + @Test + void testAnonymousClassAnnotations() { + // Test anonymous classes with package + SayTracedHello.fromCallable(); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("SayTracedHello$1.call") + .hasAttributesSatisfyingExactly( + equalTo( + CodeIncubatingAttributes.CODE_NAMESPACE, + SayTracedHello.class.getName() + "$1"), + equalTo(CodeIncubatingAttributes.CODE_FUNCTION, "call")))); + + // Test anonymous classes with no package + new Callable() { + @Trace + @Override + public String call() { + return "Howdy!"; + } + }.call(); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("SayTracedHello$1.call") + .hasAttributesSatisfyingExactly( + equalTo( + CodeIncubatingAttributes.CODE_NAMESPACE, + SayTracedHello.class.getName() + "$1"), + equalTo(CodeIncubatingAttributes.CODE_FUNCTION, "call"))), + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("TraceAnnotationsTest$1.call") + .hasAttributesSatisfyingExactly( + equalTo( + CodeIncubatingAttributes.CODE_NAMESPACE, + TraceAnnotationsTest.class.getName() + "$1"), + equalTo(CodeIncubatingAttributes.CODE_FUNCTION, "call")))); + } +} diff --git a/instrumentation/external-annotations/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/extannotations/TraceProvidersTest.java b/instrumentation/external-annotations/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/extannotations/TraceProvidersTest.java new file mode 100644 index 000000000000..6fba3ee5ff65 --- /dev/null +++ b/instrumentation/external-annotations/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/extannotations/TraceProvidersTest.java @@ -0,0 +1,70 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.extannotations; + +import static io.opentelemetry.api.common.AttributeKey.stringKey; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; + +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.semconv.incubating.CodeIncubatingAttributes; +import java.util.Locale; +import java.util.function.Consumer; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; + +/** This test verifies that Otel supports various 3rd-party trace annotations */ +class TraceProvidersTest { + @RegisterExtension + static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + @ParameterizedTest + @EnumSource(TraceProvider.class) + void testShouldSupportProvider(TraceProvider provider) { + provider.test(); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("SayTracedHello." + provider.testMethodName()) + .hasAttributesSatisfyingExactly( + equalTo( + CodeIncubatingAttributes.CODE_NAMESPACE, + SayTracedHello.class.getName()), + equalTo( + CodeIncubatingAttributes.CODE_FUNCTION, provider.testMethodName()), + equalTo(stringKey("providerAttr"), provider.name())))); + } + + @SuppressWarnings("ImmutableEnumChecker") + private enum TraceProvider { + AppOptics((sayTracedHello) -> sayTracedHello.appoptics()), + Datadog((sayTracedHello) -> sayTracedHello.datadog()), + Dropwizard((sayTracedHello) -> sayTracedHello.dropwizard()), + KamonOld((sayTracedHello) -> sayTracedHello.kamonold()), + KamonNew((sayTracedHello) -> sayTracedHello.kamonnew()), + NewRelic((sayTracedHello) -> sayTracedHello.newrelic()), + SignalFx((sayTracedHello) -> sayTracedHello.signalfx()), + Sleuth((sayTracedHello) -> sayTracedHello.sleuth()), + Tracelytics((sayTracedHello) -> sayTracedHello.tracelytics()); + + final Consumer test; + + TraceProvider(Consumer test) { + this.test = test; + } + + void test() { + test.accept(new SayTracedHello()); + } + + String testMethodName() { + return name().toLowerCase(Locale.ROOT); + } + } +} diff --git a/instrumentation/external-annotations/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/extannotations/TracedMethodsExclusionTest.java b/instrumentation/external-annotations/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/extannotations/TracedMethodsExclusionTest.java new file mode 100644 index 000000000000..d6aab0be7006 --- /dev/null +++ b/instrumentation/external-annotations/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/extannotations/TracedMethodsExclusionTest.java @@ -0,0 +1,89 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.extannotations; + +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.semconv.incubating.CodeIncubatingAttributes; +import io.opentracing.contrib.dropwizard.Trace; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +class TracedMethodsExclusionTest { + + @RegisterExtension + static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + @Test + void testCallingTheseMethodsShouldBeTraced() { + // Baseline and assumption validation + assertThat(new TestClass().annotated()).isEqualTo("Hello!"); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("TestClass.annotated") + .hasAttributesSatisfyingExactly( + equalTo( + CodeIncubatingAttributes.CODE_NAMESPACE, TestClass.class.getName()), + equalTo(CodeIncubatingAttributes.CODE_FUNCTION, "annotated")))); + } + + @Test + void testNotExplicitlyConfiguredMethodShouldNotBeTraced() throws InterruptedException { + assertThat(new TestClass().notMentioned()).isEqualTo("Hello!"); + + Thread.sleep(500); // sleep a bit just to make sure no span is captured + assertThat(testing.spans()).isEmpty(); + } + + @Test + void testMethodWhichBothAnnotatedAndExcludedForTracingShouldNotBeTraced() + throws InterruptedException { + + assertThat(new TestClass().excluded()).isEqualTo("Hello!"); + Thread.sleep(500); // sleep a bit just to make sure no span is captured + assertThat(testing.spans()).isEmpty(); + } + + @Test + void testMethodExclusionShouldOverrideTracingAnnotations() throws InterruptedException { + + assertThat(new TestClass().annotatedButExcluded()).isEqualTo("Hello!"); + Thread.sleep(500); // sleep a bit just to make sure no span is captured + assertThat(testing.spans()).isEmpty(); + } + + 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!"; + } + } +} diff --git a/instrumentation/finagle-http-23.11/javaagent/build.gradle.kts b/instrumentation/finagle-http-23.11/javaagent/build.gradle.kts new file mode 100644 index 000000000000..292ca2ac477a --- /dev/null +++ b/instrumentation/finagle-http-23.11/javaagent/build.gradle.kts @@ -0,0 +1,50 @@ +plugins { + id("otel.javaagent-instrumentation") + id("otel.scala-conventions") +} + +muzzle { + pass { + group.set("com.twitter") + module.set("finagle-http_2.12") + versions.set("[23.11.0,]") + } + + pass { + group.set("com.twitter") + module.set("finagle-http_2.13") + versions.set("[23.11.0,]") + } +} + +val finagleVersion = "23.11.0" +val scalaVersion = "2.13.10" + +val scalaMinor = Regex("""^([0-9]+\.[0-9]+)\.?.*$""").find(scalaVersion)!!.run { + val (minorVersion) = this.destructured + minorVersion +} + +val scalified = fun(pack: String): String { + return "${pack}_$scalaMinor" +} + +dependencies { + bootstrap(project(":instrumentation:executors:bootstrap")) + + library("${scalified("com.twitter:finagle-http")}:$finagleVersion") + + // should wire netty contexts + testInstrumentation(project(":instrumentation:netty:netty-4.1:javaagent")) + + implementation(project(":instrumentation:netty:netty-4.1:javaagent")) + implementation(project(":instrumentation:netty:netty-4.1:library")) + implementation(project(":instrumentation:netty:netty-4-common:library")) +} + +tasks { + test { + jvmArgs("-Dotel.instrumentation.http.client.emit-experimental-telemetry=true") + jvmArgs("-Dotel.instrumentation.http.server.emit-experimental-telemetry=true") + } +} diff --git a/instrumentation/finagle-http-23.11/javaagent/src/main/java/com/twitter/finagle/ChannelTransportHelpers.java b/instrumentation/finagle-http-23.11/javaagent/src/main/java/com/twitter/finagle/ChannelTransportHelpers.java new file mode 100644 index 000000000000..b3c4a363d264 --- /dev/null +++ b/instrumentation/finagle-http-23.11/javaagent/src/main/java/com/twitter/finagle/ChannelTransportHelpers.java @@ -0,0 +1,17 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.twitter.finagle; + +import com.twitter.finagle.netty4.transport.ChannelTransport; + +/** Exposes the finagle-internal {@link ChannelTransport#HandlerName()}. */ +public final class ChannelTransportHelpers { + private ChannelTransportHelpers() {} + + public static String getHandlerName() { + return ChannelTransport.HandlerName(); + } +} diff --git a/instrumentation/finagle-http-23.11/javaagent/src/main/java/io/netty/channel/OpenTelemetryChannelInitializerDelegate.java b/instrumentation/finagle-http-23.11/javaagent/src/main/java/io/netty/channel/OpenTelemetryChannelInitializerDelegate.java new file mode 100644 index 000000000000..84688c987fe9 --- /dev/null +++ b/instrumentation/finagle-http-23.11/javaagent/src/main/java/io/netty/channel/OpenTelemetryChannelInitializerDelegate.java @@ -0,0 +1,22 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.netty.channel; + +/** Exists to correctly expose and propagate the {@link #initChannel(Channel)} calls. */ +public abstract class OpenTelemetryChannelInitializerDelegate + extends ChannelInitializer { + + private final ChannelInitializer initializer; + + public OpenTelemetryChannelInitializerDelegate(ChannelInitializer initializer) { + this.initializer = initializer; + } + + @Override + protected void initChannel(T t) throws Exception { + initializer.initChannel(t); + } +} diff --git a/instrumentation/finagle-http-23.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/finaglehttp/v23_11/ChannelTransportInstrumentation.java b/instrumentation/finagle-http-23.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/finaglehttp/v23_11/ChannelTransportInstrumentation.java new file mode 100644 index 000000000000..f7490431ff92 --- /dev/null +++ b/instrumentation/finagle-http-23.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/finaglehttp/v23_11/ChannelTransportInstrumentation.java @@ -0,0 +1,52 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.finaglehttp.v23_11; + +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 net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import scala.Option; + +public class ChannelTransportInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher typeMatcher() { + return named("com.twitter.finagle.netty4.transport.ChannelTransport"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod().and(named("write")), + ChannelTransportInstrumentation.class.getName() + "$WriteAdvice"); + } + + @SuppressWarnings("unused") + public static class WriteAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static Scope methodEnter() { + Option ref = Helpers.CONTEXT_LOCAL.apply(); + if (ref.isDefined()) { + return ref.get().makeCurrent(); + } + return null; + } + + @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class) + public static void methodExit(@Advice.Enter Scope scope, @Advice.Thrown Throwable thrown) { + if (scope != null) { + scope.close(); + } + } + } +} diff --git a/instrumentation/finagle-http-23.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/finaglehttp/v23_11/FinagleHttpInstrumentationModule.java b/instrumentation/finagle-http-23.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/finaglehttp/v23_11/FinagleHttpInstrumentationModule.java new file mode 100644 index 000000000000..3938e762bfa4 --- /dev/null +++ b/instrumentation/finagle-http-23.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/finaglehttp/v23_11/FinagleHttpInstrumentationModule.java @@ -0,0 +1,50 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.finaglehttp.v23_11; + +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.internal.ExperimentalInstrumentationModule; +import java.util.Arrays; +import java.util.List; + +@AutoService(InstrumentationModule.class) +public class FinagleHttpInstrumentationModule extends InstrumentationModule + implements ExperimentalInstrumentationModule { + + public FinagleHttpInstrumentationModule() { + super("finagle-http", "finagle-http-23.11"); + } + + @Override + public List typeInstrumentations() { + return Arrays.asList( + new GenStreamingServerDispatcherInstrumentation(), + new ChannelTransportInstrumentation(), + new H2StreamChannelInitInstrumentation()); + } + + @Override + public String getModuleGroup() { + // relies on netty and needs access to common netty instrumentation classes + return "netty"; + } + + @Override + public List injectedClassNames() { + // these are injected so that they can access package-private members + return Arrays.asList( + "com.twitter.finagle.ChannelTransportHelpers", + "io.netty.channel.OpenTelemetryChannelInitializerDelegate"); + } + + @Override + public boolean isHelperClass(String className) { + return className.equals("com.twitter.finagle.ChannelTransportHelpers") + || className.equals("io.netty.channel.OpenTelemetryChannelInitializerDelegate"); + } +} diff --git a/instrumentation/finagle-http-23.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/finaglehttp/v23_11/Function1Wrapper.java b/instrumentation/finagle-http-23.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/finaglehttp/v23_11/Function1Wrapper.java new file mode 100644 index 000000000000..586be1efc0fe --- /dev/null +++ b/instrumentation/finagle-http-23.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/finaglehttp/v23_11/Function1Wrapper.java @@ -0,0 +1,24 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.finaglehttp.v23_11; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import scala.Function1; + +public final class Function1Wrapper { + + public static Function1 wrap(Function1 function1) { + Context context = Context.current(); + return value -> { + try (Scope ignored = context.makeCurrent()) { + return function1.apply(value); + } + }; + } + + private Function1Wrapper() {} +} diff --git a/instrumentation/finagle-http-23.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/finaglehttp/v23_11/GenStreamingServerDispatcherInstrumentation.java b/instrumentation/finagle-http-23.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/finaglehttp/v23_11/GenStreamingServerDispatcherInstrumentation.java new file mode 100644 index 000000000000..bd4f265974a5 --- /dev/null +++ b/instrumentation/finagle-http-23.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/finaglehttp/v23_11/GenStreamingServerDispatcherInstrumentation.java @@ -0,0 +1,59 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.finaglehttp.v23_11; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.hasClassesNamed; +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.hasSuperType; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +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 net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class GenStreamingServerDispatcherInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher typeMatcher() { + return hasSuperType(named("com.twitter.finagle.http.GenStreamingSerialServerDispatcher")); + } + + @Override + public ElementMatcher classLoaderOptimization() { + return hasClassesNamed("com.twitter.finagle.http.GenStreamingSerialServerDispatcher"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod().and(named("loop")), + GenStreamingServerDispatcherInstrumentation.class.getName() + "$LoopAdvice"); + } + + @SuppressWarnings("unused") + public static class LoopAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void methodEnter() { + // this works bc at this point in the server evaluation, the netty + // instrumentation has already gone to work and assigned the context to the + // local thread; + // + // this works specifically in finagle's netty stack bc at this point the loop() + // method is running on a netty thread with the necessary access to the + // java-native ThreadLocal where the Context is stored + Helpers.CONTEXT_LOCAL.update(Context.current()); + } + + @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class) + public static void methodExit(@Advice.Thrown Throwable thrown) { + // always clear this + Helpers.CONTEXT_LOCAL.clear(); + } + } +} diff --git a/instrumentation/finagle-http-23.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/finaglehttp/v23_11/H2StreamChannelInitInstrumentation.java b/instrumentation/finagle-http-23.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/finaglehttp/v23_11/H2StreamChannelInitInstrumentation.java new file mode 100644 index 000000000000..cfa7176077c7 --- /dev/null +++ b/instrumentation/finagle-http-23.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/finaglehttp/v23_11/H2StreamChannelInitInstrumentation.java @@ -0,0 +1,62 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.finaglehttp.v23_11; + +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.returns; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelInitializer; +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 H2StreamChannelInitInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher typeMatcher() { + // scala object instance -- append $ to name + return named("com.twitter.finagle.http2.transport.common.H2StreamChannelInit$"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod() + .and(named("initServer")) + .and(returns(named("io.netty.channel.ChannelInitializer"))), + H2StreamChannelInitInstrumentation.class.getName() + "$InitServerAdvice"); + transformer.applyAdviceToMethod( + isMethod() + .and(named("initClient")) + .and(returns(named("io.netty.channel.ChannelInitializer"))), + H2StreamChannelInitInstrumentation.class.getName() + "$InitClientAdvice"); + } + + @SuppressWarnings("unused") + public static class InitServerAdvice { + + @Advice.OnMethodExit + @Advice.AssignReturned.ToReturned + public static ChannelInitializer handleExit( + @Advice.Return ChannelInitializer initializer) { + return Helpers.wrapServer(initializer); + } + } + + @SuppressWarnings("unused") + public static class InitClientAdvice { + + @Advice.OnMethodExit + @Advice.AssignReturned.ToReturned + public static ChannelInitializer handleExit( + @Advice.Return ChannelInitializer initializer) { + return Helpers.wrapClient(initializer); + } + } +} diff --git a/instrumentation/finagle-http-23.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/finaglehttp/v23_11/Helpers.java b/instrumentation/finagle-http-23.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/finaglehttp/v23_11/Helpers.java new file mode 100644 index 000000000000..6f00d4cd69d9 --- /dev/null +++ b/instrumentation/finagle-http-23.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/finaglehttp/v23_11/Helpers.java @@ -0,0 +1,110 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.finaglehttp.v23_11; + +import static io.opentelemetry.instrumentation.netty.v4_1.internal.client.HttpClientRequestTracingHandler.HTTP_CLIENT_REQUEST; +import static io.opentelemetry.javaagent.instrumentation.netty.v4_1.NettyClientSingletons.clientHandlerFactory; + +import com.twitter.util.Local; +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.OpenTelemetryChannelInitializerDelegate; +import io.netty.handler.codec.http2.Http2StreamFrameToHttpObjectCodec; +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.util.VirtualField; +import io.opentelemetry.instrumentation.netty.v4_1.internal.AttributeKeys; +import io.opentelemetry.instrumentation.netty.v4_1.internal.ServerContexts; +import io.opentelemetry.instrumentation.netty.v4_1.internal.client.HttpClientTracingHandler; +import io.opentelemetry.instrumentation.netty.v4_1.internal.server.HttpServerTracingHandler; +import io.opentelemetry.javaagent.instrumentation.netty.v4_1.NettyHttpServerResponseBeforeCommitHandler; +import io.opentelemetry.javaagent.instrumentation.netty.v4_1.NettyServerSingletons; + +public final class Helpers { + + private Helpers() {} + + public static final Local CONTEXT_LOCAL = new Local<>(); + + public static ChannelInitializer wrapServer(ChannelInitializer inner) { + return new OpenTelemetryChannelInitializerDelegate(inner) { + + @Override + protected void initChannel(C channel) throws Exception { + // do all the needful up front, as this may add necessary handlers -- see below + super.initChannel(channel); + + // the parent channel is the original http/1.1 channel and has the contexts stored in it; + // we assign to this new channel as the old one will not be evaluated in the upgraded h2c + // chain + ServerContexts serverContexts = ServerContexts.get(channel.parent()); + channel.attr(AttributeKeys.SERVER_CONTEXTS).set(serverContexts); + + // todo add way to propagate the protocol version override up to the netty instrumentation; + // why: the netty instrumentation extracts the http protocol version from the HttpRequest + // object which in this case is _always_ http/1.1 due to the use of this adapter codec, + // Http2StreamFrameToHttpObjectCodec + ChannelHandlerContext codecCtx = + channel.pipeline().context(Http2StreamFrameToHttpObjectCodec.class); + if (codecCtx != null) { + if (channel.pipeline().get(HttpServerTracingHandler.class) == null) { + VirtualField virtualField = + VirtualField.find(ChannelHandler.class, ChannelHandler.class); + ChannelHandler ourHandler = + NettyServerSingletons.serverTelemetry() + .createCombinedHandler(NettyHttpServerResponseBeforeCommitHandler.INSTANCE); + + channel + .pipeline() + .addAfter(codecCtx.name(), ourHandler.getClass().getName(), ourHandler); + // attach this in this way to match up with how netty instrumentation expects things + virtualField.set(codecCtx.handler(), ourHandler); + } + } + } + }; + } + + public static ChannelInitializer wrapClient(ChannelInitializer inner) { + return new OpenTelemetryChannelInitializerDelegate(inner) { + + // wraps everything for roughly the same reasons as in wrapServer(), above + @Override + protected void initChannel(C channel) throws Exception { + super.initChannel(channel); + + channel + .attr(AttributeKeys.CLIENT_PARENT_CONTEXT) + .set(channel.parent().attr(AttributeKeys.CLIENT_PARENT_CONTEXT).get()); + channel + .attr(AttributeKeys.CLIENT_CONTEXT) + .set(channel.parent().attr(AttributeKeys.CLIENT_CONTEXT).get()); + channel.attr(HTTP_CLIENT_REQUEST).set(channel.parent().attr(HTTP_CLIENT_REQUEST).get()); + + // todo add way to propagate the protocol version override up to the netty instrumentation; + // why: the netty instrumentation extracts the http protocol version from the HttpRequest + // object which in this case is _always_ http/1.1 due to the use of this adapter codec, + // Http2StreamFrameToHttpObjectCodec + ChannelHandlerContext codecCtx = + channel.pipeline().context(Http2StreamFrameToHttpObjectCodec.class); + if (codecCtx != null) { + if (channel.pipeline().get(HttpClientTracingHandler.class) == null) { + VirtualField virtualField = + VirtualField.find(ChannelHandler.class, ChannelHandler.class); + ChannelHandler ourHandler = clientHandlerFactory().createCombinedHandler(); + + channel + .pipeline() + .addAfter(codecCtx.name(), ourHandler.getClass().getName(), ourHandler); + // attach this in this way to match up with how netty instrumentation expects things + virtualField.set(codecCtx.handler(), ourHandler); + } + } + } + }; + } +} diff --git a/instrumentation/finagle-http-23.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/finaglehttp/v23_11/LocalSchedulerActivationInstrumentation.java b/instrumentation/finagle-http-23.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/finaglehttp/v23_11/LocalSchedulerActivationInstrumentation.java new file mode 100644 index 000000000000..1cbec8c9c0fd --- /dev/null +++ b/instrumentation/finagle-http-23.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/finaglehttp/v23_11/LocalSchedulerActivationInstrumentation.java @@ -0,0 +1,49 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.finaglehttp.v23_11; + +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import io.opentelemetry.context.Context; +import io.opentelemetry.javaagent.bootstrap.Java8BytecodeBridge; +import io.opentelemetry.javaagent.bootstrap.executors.ContextPropagatingRunnable; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.asm.Advice.AssignReturned.ToArguments.ToArgument; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class LocalSchedulerActivationInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("com.twitter.concurrent.LocalScheduler$Activation"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + named("submit").and(takesArguments(Runnable.class)), + this.getClass().getName() + "$WrapRunnableAdvice"); + } + + @SuppressWarnings("unused") + public static class WrapRunnableAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + @Advice.AssignReturned.ToArguments(@ToArgument(0)) + public static Runnable wrap(@Advice.Argument(0) Runnable task) { + if (task == null) { + return null; + } + + Context context = Java8BytecodeBridge.currentContext(); + return ContextPropagatingRunnable.propagateContext(task, context); + } + } +} diff --git a/instrumentation/finagle-http-23.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/finaglehttp/v23_11/PromiseMonitoredInstrumentation.java b/instrumentation/finagle-http-23.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/finaglehttp/v23_11/PromiseMonitoredInstrumentation.java new file mode 100644 index 000000000000..bef50738b3ec --- /dev/null +++ b/instrumentation/finagle-http-23.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/finaglehttp/v23_11/PromiseMonitoredInstrumentation.java @@ -0,0 +1,47 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.finaglehttp.v23_11; + +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.asm.Advice.AssignReturned.ToArguments.ToArgument; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import scala.Function1; + +public class PromiseMonitoredInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("com.twitter.util.Promise$Monitored"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isConstructor().and(takesArgument(1, named("scala.Function1"))), + this.getClass().getName() + "$WrapFunctionAdvice"); + } + + @SuppressWarnings("unused") + public static class WrapFunctionAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + @Advice.AssignReturned.ToArguments(@ToArgument(1)) + public static Function1 wrap(@Advice.Argument(1) Function1 function1) { + if (function1 == null) { + return null; + } + + return Function1Wrapper.wrap(function1); + } + } +} diff --git a/instrumentation/finagle-http-23.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/finaglehttp/v23_11/TwitterUtilCoreInstrumentationModule.java b/instrumentation/finagle-http-23.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/finaglehttp/v23_11/TwitterUtilCoreInstrumentationModule.java new file mode 100644 index 000000000000..1f72e97f352c --- /dev/null +++ b/instrumentation/finagle-http-23.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/finaglehttp/v23_11/TwitterUtilCoreInstrumentationModule.java @@ -0,0 +1,34 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.finaglehttp.v23_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 io.opentelemetry.javaagent.extension.instrumentation.internal.ExperimentalInstrumentationModule; +import java.util.List; + +@AutoService(InstrumentationModule.class) +public class TwitterUtilCoreInstrumentationModule extends InstrumentationModule + implements ExperimentalInstrumentationModule { + + public TwitterUtilCoreInstrumentationModule() { + super("twitter-util-core"); + } + + @Override + public String getModuleGroup() { + return "netty"; + } + + @Override + public List typeInstrumentations() { + return asList( + new LocalSchedulerActivationInstrumentation(), new PromiseMonitoredInstrumentation()); + } +} diff --git a/instrumentation/finagle-http-23.11/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/finaglehttp/v23_11/AbstractServerTest.java b/instrumentation/finagle-http-23.11/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/finaglehttp/v23_11/AbstractServerTest.java new file mode 100644 index 000000000000..3b4730a25777 --- /dev/null +++ b/instrumentation/finagle-http-23.11/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/finaglehttp/v23_11/AbstractServerTest.java @@ -0,0 +1,100 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.finaglehttp.v23_11; + +import static io.opentelemetry.instrumentation.testing.junit.http.HttpServerTestOptions.DEFAULT_HTTP_ATTRIBUTES; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.CAPTURE_HEADERS; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.ERROR; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.EXCEPTION; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.INDEXED_CHILD; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.NOT_FOUND; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.QUERY_PARAM; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.REDIRECT; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.SUCCESS; + +import com.google.common.collect.Sets; +import com.twitter.finagle.ListeningServer; +import com.twitter.finagle.Service; +import com.twitter.finagle.http.Request; +import com.twitter.finagle.http.Response; +import com.twitter.finagle.http.Status; +import com.twitter.io.Buf; +import com.twitter.util.Await; +import com.twitter.util.Duration; +import com.twitter.util.Future; +import com.twitter.util.logging.Logging; +import io.netty.handler.codec.http.HttpHeaderNames; +import io.netty.handler.codec.http.QueryStringDecoder; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpServerTest; +import io.opentelemetry.instrumentation.testing.junit.http.HttpServerInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.http.HttpServerTestOptions; +import io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint; +import io.opentelemetry.semconv.HttpAttributes; +import java.net.URI; +import java.util.Collections; +import org.junit.jupiter.api.extension.RegisterExtension; + +abstract class AbstractServerTest extends AbstractHttpServerTest { + + @RegisterExtension + static final InstrumentationExtension testing = HttpServerInstrumentationExtension.forAgent(); + + @Override + protected void configure(HttpServerTestOptions options) { + super.configure(options); + options.setTestException(false); + options.setHttpAttributes( + unused -> + Sets.difference( + DEFAULT_HTTP_ATTRIBUTES, Collections.singleton(HttpAttributes.HTTP_ROUTE))); + + options.setTestCaptureHttpHeaders(true); + } + + @Override + protected void stopServer(ListeningServer server) throws Exception { + Await.ready(server.close(), Duration.fromSeconds(2)); + } + + static class TestService extends Service implements Logging { + @Override + public Future apply(Request request) { + URI uri = URI.create(request.uri()); + ServerEndpoint endpoint = ServerEndpoint.forPath(uri.getPath()); + return controller( + endpoint, + () -> { + Response response = Response.apply().status(Status.apply(endpoint.getStatus())); + if (SUCCESS.equals(endpoint) || ERROR.equals(endpoint)) { + response.content(Buf.Utf8$.MODULE$.apply(endpoint.getBody())); + } else if (INDEXED_CHILD.equals(endpoint)) { + endpoint.collectSpanAttributes( + name -> + new QueryStringDecoder(uri) + .parameters().get(name).stream().findFirst().orElse("")); + response.content(Buf.Empty()); + } else if (QUERY_PARAM.equals(endpoint)) { + response.content(Buf.Utf8$.MODULE$.apply(uri.getQuery())); + } else if (REDIRECT.equals(endpoint)) { + response.content(Buf.Empty()); + response.headerMap().put(HttpHeaderNames.LOCATION.toString(), endpoint.getBody()); + } else if (CAPTURE_HEADERS.equals(endpoint)) { + response.content(Buf.Utf8$.MODULE$.apply(endpoint.getBody())); + response + .headerMap() + .set("X-Test-Response", request.headerMap().get("X-Test-Request").get()); + } else if (EXCEPTION.equals(endpoint)) { + throw new IllegalStateException(endpoint.getBody()); + } else { + response.content(Buf.Utf8$.MODULE$.apply(NOT_FOUND.getBody())); + response = Response.apply().status(Status.apply(NOT_FOUND.getStatus())); + } + return Future.value(response); + }); + } + } +} diff --git a/instrumentation/finagle-http-23.11/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/finaglehttp/v23_11/ClientTest.java b/instrumentation/finagle-http-23.11/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/finaglehttp/v23_11/ClientTest.java new file mode 100644 index 000000000000..e58eda6d84c6 --- /dev/null +++ b/instrumentation/finagle-http-23.11/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/finaglehttp/v23_11/ClientTest.java @@ -0,0 +1,213 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.finaglehttp.v23_11; + +import static io.opentelemetry.javaagent.instrumentation.finaglehttp.v23_11.Utils.createClient; +import static org.assertj.core.api.Assertions.assertThat; + +import com.twitter.finagle.ConnectionFailedException; +import com.twitter.finagle.Failure; +import com.twitter.finagle.ReadTimedOutException; +import com.twitter.finagle.Service; +import com.twitter.finagle.http.Request; +import com.twitter.finagle.http.Response; +import com.twitter.util.Await; +import com.twitter.util.Duration; +import com.twitter.util.Future; +import com.twitter.util.FuturePool; +import com.twitter.util.Time; +import io.netty.handler.codec.http2.Http2StreamFrameToHttpObjectCodec; +import io.netty.handler.timeout.ReadTimeoutException; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpClientTest; +import io.opentelemetry.instrumentation.testing.junit.http.HttpClientInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.http.HttpClientResult; +import io.opentelemetry.instrumentation.testing.junit.http.HttpClientTestOptions; +import io.opentelemetry.javaagent.instrumentation.finaglehttp.v23_11.Utils.ClientType; +import io.opentelemetry.semconv.ServerAttributes; +import java.net.ConnectException; +import java.net.URI; +import java.util.Collections; +import java.util.HashSet; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import org.assertj.core.api.AbstractThrowableAssert; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.extension.RegisterExtension; + +/** + * Tests relevant client functionality. + * + * @implNote Why no http/2 tests: finagle maps everything down to http/1.1 via netty's own {@link + * Http2StreamFrameToHttpObjectCodec} which results in the same code path execution through + * finagle's netty stack. While testing would undoubtedly be beneficial, it's at this time + * untested due to lack of concrete support from the otel instrumentation test framework and + * upstream netty instrumentation, both. + */ +// todo implement http/2-specific tests; +// otel test framework doesn't support an http/2 server out of the box +class ClientTest extends AbstractHttpClientTest { + @RegisterExtension + static final InstrumentationExtension testing = HttpClientInstrumentationExtension.forAgent(); + + private final Map> clients = new ConcurrentHashMap<>(); + + // finagle Services are closeable, but are bound to a host + port; + // as these are only known during the invocation of the test, each test must create and then + // tear down their respective Services. + // + // however, the underlying netty bits are reused between Services by default, so "close" + // works out to a more "virtual" operation than with other client libraries. + @AfterEach + void tearDown() throws Exception { + for (Service client : clients.values()) { + Await.ready(client.close(Time.fromSeconds(10))); + } + clients.clear(); + } + + private Service getClient(URI uri) { + return getClient(uri, uri.getScheme().equals("https") ? ClientType.TLS : ClientType.DEFAULT); + } + + private Service getClient(URI uri, ClientType clientType) { + return clients.computeIfAbsent( + clientType, + (type) -> createClient(type).newService(uri.getHost() + ":" + Utils.safePort(uri))); + } + + private Future doSendRequest(Request request, URI uri) { + // push this onto a FuturePool for 2 reasons: + // 1) forces the request handling onto a different thread, ensuring test accuracy + // 2) using the default thread can mess with high concurrency scenarios + Context context = Context.current(); + return FuturePool.unboundedPool() + .apply( + () -> { + try (Scope ignored = context.makeCurrent()) { + return Await.result(getClient(uri).apply(request)); + } catch (Exception e) { + throw new RuntimeException(e); + } + }); + } + + @Override + protected void configure(HttpClientTestOptions.Builder optionsBuilder) { + optionsBuilder.setSingleConnectionFactory( + (host, port) -> { + URI uri = URI.create(String.format(Locale.ROOT, "http://%s:%d", host, port)); + Service svc = getClient(uri, ClientType.SINGLE_CONN); + return (path, headers) -> { + // this is synchronized bc so is the Netty one; + // seems like the use of a "single" (presumably-queueing) connection would do this + // automatically, but apparently not + synchronized (svc) { + Request get = buildRequest("GET", URI.create(uri + path), headers); + return Await.result(svc.apply(get), Duration.fromSeconds(20)).statusCode(); + } + }; + }); + + optionsBuilder.setHttpAttributes(ClientTest::getHttpAttributes); + optionsBuilder.setExpectedClientSpanNameMapper(ClientTest::getExpectedClientSpanName); + optionsBuilder.disableTestRedirects(); + optionsBuilder.spanEndsAfterBody(); + optionsBuilder.setClientSpanErrorMapper( + (uri, error) -> { + // all errors should be wrapped in RuntimeExceptions due to how we run things in + // doSendRequest() + AbstractThrowableAssert clientWrapAssert = + assertThat(error).isInstanceOf(RuntimeException.class); + if ("http://localhost:61/".equals(uri.toString()) + || "https://192.0.2.1/".equals(uri.toString())) { + // finagle handles all these in com.twitter.finagle.netty4.ConnectionBuilder.build(); + // all errors emitted by the netty Bootstrap.connect() call are mapped to + // twitter/finagle exceptions and handled accordingly; + // namely, this means wrapping the root exception in a finagle + // ConnectionFailedException + // and then with a twitter Failure.rejected() call, resulting in the multiple nestings + // of the root exception + clientWrapAssert + .cause() + .isInstanceOf(Failure.class) + .cause() + .isInstanceOf(ConnectionFailedException.class) + .cause() + .isInstanceOf(ConnectException.class); + error = error.getCause().getCause().getCause(); + } else if (uri.getPath().endsWith("/read-timeout")) { + // not a connect() exception like the above, so is not wrapped as above; + clientWrapAssert.cause().isInstanceOf(ReadTimedOutException.class); + // however, this specific case results in a mapping from netty's ReadTimeoutException + // to finagle's ReadTimedOutException in the finagle client code, losing all trace of + // the original exception; so we must construct it manually here + error = new ReadTimeoutException(); + } + return error; + }); + } + + @Override + public Request buildRequest(String method, URI uri, Map headers) { + return Utils.buildRequest(method, uri, headers); + } + + @Override + public int sendRequest(Request request, String method, URI uri, Map headers) + throws Exception { + return Await.result(doSendRequest(request, uri), Duration.fromSeconds(30)).statusCode(); + } + + @Override + public void sendRequestWithCallback( + Request request, + String method, + URI uri, + Map headers, + HttpClientResult httpClientResult) { + doSendRequest(request, uri) + .onSuccess( + r -> { + httpClientResult.complete(r.statusCode()); + return null; + }) + .onFailure( + t -> { + httpClientResult.complete(t); + return null; + }); + } + + private static Set> getHttpAttributes(URI uri) { + String uriString = uri.toString(); + // http://localhost:61/ => unopened port, https://192.0.2.1/ => non routable address + if ("http://localhost:61/".equals(uriString) || "https://192.0.2.1/".equals(uriString)) { + return Collections.emptySet(); + } + Set> attributes = new HashSet<>(HttpClientTestOptions.DEFAULT_HTTP_ATTRIBUTES); + attributes.remove(ServerAttributes.SERVER_ADDRESS); + attributes.remove(ServerAttributes.SERVER_PORT); + return attributes; + } + + // borrowed from AbstractNetty41ClientTest as finagle's underlying framework under test here is + // netty + private static String getExpectedClientSpanName(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 HttpClientTestOptions.DEFAULT_EXPECTED_CLIENT_SPAN_NAME_MAPPER.apply(uri, method); + } + } +} diff --git a/instrumentation/finagle-http-23.11/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/finaglehttp/v23_11/ServerH1Test.java b/instrumentation/finagle-http-23.11/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/finaglehttp/v23_11/ServerH1Test.java new file mode 100644 index 000000000000..2cc5e4d53cd0 --- /dev/null +++ b/instrumentation/finagle-http-23.11/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/finaglehttp/v23_11/ServerH1Test.java @@ -0,0 +1,18 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.finaglehttp.v23_11; + +import com.twitter.finagle.Http; +import com.twitter.finagle.ListeningServer; + +class ServerH1Test extends AbstractServerTest { + @Override + protected ListeningServer setupServer() { + return Http.server() + .withNoHttp2() + .serve(address.getHost() + ":" + port, new AbstractServerTest.TestService()); + } +} diff --git a/instrumentation/finagle-http-23.11/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/finaglehttp/v23_11/ServerH2Test.java b/instrumentation/finagle-http-23.11/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/finaglehttp/v23_11/ServerH2Test.java new file mode 100644 index 000000000000..f896523017d5 --- /dev/null +++ b/instrumentation/finagle-http-23.11/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/finaglehttp/v23_11/ServerH2Test.java @@ -0,0 +1,110 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.finaglehttp.v23_11; + +import static io.opentelemetry.instrumentation.netty.v4_1.internal.ProtocolSpecificEvent.SWITCHING_PROTOCOLS; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.SUCCESS; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +import com.twitter.finagle.Http; +import com.twitter.finagle.ListeningServer; +import com.twitter.finagle.Service; +import com.twitter.finagle.http.Request; +import com.twitter.finagle.http.Response; +import com.twitter.finagle.http2.param.PriorKnowledge; +import com.twitter.util.Await; +import com.twitter.util.Duration; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.instrumentation.netty.v4_1.internal.ProtocolSpecificEvent; +import io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint; +import io.opentelemetry.sdk.testing.assertj.EventDataAssert; +import io.opentelemetry.sdk.testing.assertj.SpanDataAssert; +import io.opentelemetry.testing.internal.armeria.common.HttpHeaderNames; +import io.opentelemetry.testing.internal.armeria.internal.shaded.guava.collect.ImmutableMap; +import java.net.URI; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.function.Consumer; +import org.junit.jupiter.api.Test; + +class ServerH2Test extends AbstractServerTest { + + @Override + protected ListeningServer setupServer() { + return Http.server() + // when enabled, supports protocol h1 & h2, the latter with upgrade + .withHttp2() + // todo implement http/2-specific tests + // the armeria configuration used at the heart of AbstractHttpServerTest isn't configurable + // to http/2 + .configured(PriorKnowledge.apply(true).mk()) + .serve(address.getHost() + ":" + port, new AbstractServerTest.TestService()); + } + + private static void assertSwitchingProtocolsEvent(EventDataAssert eventDataAssert) { + eventDataAssert + .hasName(SWITCHING_PROTOCOLS.eventName()) + .hasAttributes( + Attributes.of( + ProtocolSpecificEvent.SWITCHING_PROTOCOLS_FROM_KEY, + "HTTP/1.1", + ProtocolSpecificEvent.SWITCHING_PROTOCOLS_TO_KEY, + Collections.singletonList("h2c"))); + } + + @Test + void h2ProtocolUpgrade() throws Exception { + URI uri = URI.create("http://localhost:" + port + SUCCESS.getPath()); + Service client = + Utils.createClient(Utils.ClientType.DEFAULT) + // must use http2 here + .withHttp2() + .newService(uri.getHost() + ":" + uri.getPort()); + + Response response = + Await.result( + client.apply( + Utils.buildRequest( + "GET", + uri, + ImmutableMap.of( + HttpHeaderNames.USER_AGENT.toString(), + TEST_USER_AGENT, + HttpHeaderNames.X_FORWARDED_FOR.toString(), + TEST_CLIENT_IP))), + com.twitter.util.Duration.fromSeconds(20)); + + Await.result(client.close(), Duration.fromSeconds(5)); + + assertThat(response.status().code()).isEqualTo(SUCCESS.getStatus()); + assertThat(response.contentString()).isEqualTo(SUCCESS.getBody()); + + String method = "GET"; + ServerEndpoint endpoint = SUCCESS; + + testing.waitAndAssertTraces( + trace -> { + List> spanAssertions = new ArrayList<>(); + spanAssertions.add( + s -> s.hasEventsSatisfyingExactly(ServerH2Test::assertSwitchingProtocolsEvent)); + spanAssertions.add( + span -> { + assertServerSpan(span, method, endpoint, endpoint.getStatus()); + span.hasEventsSatisfyingExactly(ServerH2Test::assertSwitchingProtocolsEvent); + }); + + int parentIndex = 1; + spanAssertions.add( + span -> { + assertControllerSpan(span, null); + span.hasParent(trace.getSpan(parentIndex)); + }); + + trace.hasSpansSatisfyingExactly(spanAssertions); + }); + } +} diff --git a/instrumentation/finagle-http-23.11/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/finaglehttp/v23_11/Utils.java b/instrumentation/finagle-http-23.11/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/finaglehttp/v23_11/Utils.java new file mode 100644 index 000000000000..94eb9685ab17 --- /dev/null +++ b/instrumentation/finagle-http-23.11/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/finaglehttp/v23_11/Utils.java @@ -0,0 +1,73 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.finaglehttp.v23_11; + +import static io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpClientTest.CONNECTION_TIMEOUT; +import static io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpClientTest.READ_TIMEOUT; + +import com.twitter.finagle.Http; +import com.twitter.finagle.http.Method; +import com.twitter.finagle.http.Request; +import com.twitter.finagle.service.RetryBudget; +import com.twitter.util.Duration; +import java.net.URI; +import java.util.Locale; +import java.util.Map; + +final class Utils { + + private Utils() {} + + static Http.Client createClient(ClientType clientType) { + Http.Client client = + Http.client() + .withNoHttp2() + .withTransport() + .readTimeout(Duration.fromMilliseconds(READ_TIMEOUT.toMillis())) + .withTransport() + .connectTimeout(Duration.fromMilliseconds(CONNECTION_TIMEOUT.toMillis())) + // disable automatic retries -- retries will result in under-counting traces in the + // tests + .withRetryBudget(RetryBudget.Empty()); + + switch (clientType) { + case TLS: + client = client.withTransport().tlsWithoutValidation(); + break; + case SINGLE_CONN: + client = client.withSessionPool().maxSize(1); + break; + case DEFAULT: + break; + } + + return client; + } + + enum ClientType { + TLS, + SINGLE_CONN, + DEFAULT; + } + + static int safePort(URI uri) { + int port = uri.getPort(); + if (port == -1) { + port = uri.getScheme().equals("https") ? 443 : 80; + } + return port; + } + + static Request buildRequest(String method, URI uri, Map headers) { + Request request = + Request.apply( + Method.apply(method.toUpperCase(Locale.ENGLISH)), + uri.getPath() + (uri.getQuery() == null ? "" : "?" + uri.getRawQuery())); + request.host(uri.getHost() + ":" + safePort(uri)); + headers.forEach((key, value) -> request.headerMap().put(key, value)); + return request; + } +} diff --git a/instrumentation/finatra-2.9/javaagent/build.gradle.kts b/instrumentation/finatra-2.9/javaagent/build.gradle.kts index bf3a2b55354a..4cb1cbaab917 100644 --- a/instrumentation/finatra-2.9/javaagent/build.gradle.kts +++ b/instrumentation/finatra-2.9/javaagent/build.gradle.kts @@ -81,22 +81,11 @@ tasks { dependsOn(testing.suites) } } -} - -tasks.withType().configureEach { - // required on jdk17 - jvmArgs("--add-opens=java.base/java.lang=ALL-UNNAMED") - jvmArgs("-XX:+IgnoreUnrecognizedVMOptions") -} -// com.fasterxml.jackson.module:jackson-module-scala_2.11:2.15.2 is missing force using jackson 2.15.1 -// remove this when a new version of jackson is released -configurations.configureEach { - resolutionStrategy { - eachDependency { - if (requested.group == "com.fasterxml.jackson" && requested.name == "jackson-bom" && requested.version == "2.15.2") { - useVersion("2.15.1") - } - } + withType().configureEach { + // required on jdk17 + jvmArgs("--add-opens=java.base/java.lang=ALL-UNNAMED") + jvmArgs("-XX:+IgnoreUnrecognizedVMOptions") + jvmArgs("-Dotel.instrumentation.common.experimental.controller-telemetry.enabled=true") } } diff --git a/instrumentation/finatra-2.9/javaagent/src/latestDepTest/scala/io/opentelemetry/javaagent/instrumentation/finatra/FinatraServerLatestTest.scala b/instrumentation/finatra-2.9/javaagent/src/latestDepTest/scala/io/opentelemetry/javaagent/instrumentation/finatra/FinatraServerLatestTest.scala index dcc771532373..95719e122f96 100644 --- a/instrumentation/finatra-2.9/javaagent/src/latestDepTest/scala/io/opentelemetry/javaagent/instrumentation/finatra/FinatraServerLatestTest.scala +++ b/instrumentation/finatra-2.9/javaagent/src/latestDepTest/scala/io/opentelemetry/javaagent/instrumentation/finatra/FinatraServerLatestTest.scala @@ -17,7 +17,7 @@ import io.opentelemetry.instrumentation.testing.junit.http.{ import io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo import io.opentelemetry.sdk.testing.assertj.SpanDataAssert import io.opentelemetry.sdk.trace.data.StatusData -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import io.opentelemetry.semconv.incubating.CodeIncubatingAttributes import org.junit.jupiter.api.extension.RegisterExtension import java.util.concurrent.Executors @@ -39,7 +39,7 @@ class FinatraServerLatestTest extends AbstractHttpServerTest[HttpServer] { implicit val ec: ExecutionContext = ExecutionContext.fromExecutor(startupThread) Future { - testServer.main(Array("-admin.port=:0", "-http.port=:" + port)) + testServer.nonExitingMain(Array("-admin.port=:0", "-http.port=:" + port)) } testServer.awaitReady() } @@ -55,6 +55,7 @@ class FinatraServerLatestTest extends AbstractHttpServerTest[HttpServer] { override def test(endpoint: ServerEndpoint): Boolean = endpoint != ServerEndpoint.NOT_FOUND }) + options.setResponseCodeOnNonStandardHttpMethod(400) } override protected def assertHandlerSpan( @@ -69,7 +70,7 @@ class FinatraServerLatestTest extends AbstractHttpServerTest[HttpServer] { .hasKind(SpanKind.INTERNAL) .hasAttributesSatisfyingExactly( equalTo( - SemanticAttributes.CODE_NAMESPACE, + CodeIncubatingAttributes.CODE_NAMESPACE, "io.opentelemetry.javaagent.instrumentation.finatra.FinatraController" ) ) diff --git a/instrumentation/finatra-2.9/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/finatra/FinatraCodeAttributesGetter.java b/instrumentation/finatra-2.9/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/finatra/FinatraCodeAttributesGetter.java index e0289f353256..69a5e90d4863 100644 --- a/instrumentation/finatra-2.9/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/finatra/FinatraCodeAttributesGetter.java +++ b/instrumentation/finatra-2.9/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/finatra/FinatraCodeAttributesGetter.java @@ -5,7 +5,7 @@ package io.opentelemetry.javaagent.instrumentation.finatra; -import io.opentelemetry.instrumentation.api.instrumenter.code.CodeAttributesGetter; +import io.opentelemetry.instrumentation.api.incubator.semconv.code.CodeAttributesGetter; import javax.annotation.Nullable; public class FinatraCodeAttributesGetter implements CodeAttributesGetter> { diff --git a/instrumentation/finatra-2.9/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/finatra/FinatraSingletons.java b/instrumentation/finatra-2.9/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/finatra/FinatraSingletons.java index b9215f21db4a..1007aa3101ee 100644 --- a/instrumentation/finatra-2.9/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/finatra/FinatraSingletons.java +++ b/instrumentation/finatra-2.9/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/finatra/FinatraSingletons.java @@ -8,11 +8,11 @@ import com.twitter.finatra.http.contexts.RouteInfo; import io.opentelemetry.api.GlobalOpenTelemetry; import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.incubator.semconv.code.CodeAttributesExtractor; +import io.opentelemetry.instrumentation.api.incubator.semconv.code.CodeSpanNameExtractor; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; -import io.opentelemetry.instrumentation.api.instrumenter.code.CodeAttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.code.CodeSpanNameExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteHolder; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteSource; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRoute; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRouteSource; public final class FinatraSingletons { @@ -34,8 +34,7 @@ public static Instrumenter, Void> instrumenter() { } public static void updateServerSpanName(Context context, RouteInfo routeInfo) { - HttpRouteHolder.updateHttpRoute( - context, HttpRouteSource.CONTROLLER, (c, route) -> route.path(), routeInfo); + HttpServerRoute.update(context, HttpServerRouteSource.CONTROLLER, routeInfo.path()); } private FinatraSingletons() {} diff --git a/instrumentation/finatra-2.9/javaagent/src/test/scala/io/opentelemetry/javaagent/instrumentation/finatra/FinatraServerTest.scala b/instrumentation/finatra-2.9/javaagent/src/test/scala/io/opentelemetry/javaagent/instrumentation/finatra/FinatraServerTest.scala index 2679d67b0829..73e6ff556888 100644 --- a/instrumentation/finatra-2.9/javaagent/src/test/scala/io/opentelemetry/javaagent/instrumentation/finatra/FinatraServerTest.scala +++ b/instrumentation/finatra-2.9/javaagent/src/test/scala/io/opentelemetry/javaagent/instrumentation/finatra/FinatraServerTest.scala @@ -17,7 +17,7 @@ import io.opentelemetry.instrumentation.testing.junit.http.{ import io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo import io.opentelemetry.sdk.testing.assertj.SpanDataAssert import io.opentelemetry.sdk.trace.data.StatusData -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import io.opentelemetry.semconv.incubating.CodeIncubatingAttributes import org.junit.jupiter.api.extension.RegisterExtension import java.util.concurrent.Executors @@ -39,7 +39,7 @@ class FinatraServerTest extends AbstractHttpServerTest[HttpServer] { implicit val ec: ExecutionContext = ExecutionContext.fromExecutor(startupThread) Future { - testServer.main(Array("-admin.port=:0", "-http.port=:" + port)) + testServer.nonExitingMain(Array("-admin.port=:0", "-http.port=:" + port)) } testServer.awaitReady() } @@ -56,6 +56,7 @@ class FinatraServerTest extends AbstractHttpServerTest[HttpServer] { override def test(endpoint: ServerEndpoint): Boolean = endpoint != ServerEndpoint.NOT_FOUND }) + options.setResponseCodeOnNonStandardHttpMethod(400) } override protected def assertHandlerSpan( @@ -70,7 +71,7 @@ class FinatraServerTest extends AbstractHttpServerTest[HttpServer] { .hasKind(SpanKind.INTERNAL) .hasAttributesSatisfyingExactly( equalTo( - SemanticAttributes.CODE_NAMESPACE, + CodeIncubatingAttributes.CODE_NAMESPACE, "io.opentelemetry.javaagent.instrumentation.finatra.FinatraController" ) ) diff --git a/instrumentation/geode-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/geode/GeodeDbAttributesGetter.java b/instrumentation/geode-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/geode/GeodeDbAttributesGetter.java index 423a6c059e66..f0f15e7f6d2b 100644 --- a/instrumentation/geode-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/geode/GeodeDbAttributesGetter.java +++ b/instrumentation/geode-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/geode/GeodeDbAttributesGetter.java @@ -5,20 +5,20 @@ package io.opentelemetry.javaagent.instrumentation.geode; -import io.opentelemetry.instrumentation.api.db.SqlStatementSanitizer; -import io.opentelemetry.instrumentation.api.instrumenter.db.DbClientAttributesGetter; -import io.opentelemetry.javaagent.bootstrap.internal.CommonConfig; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.opentelemetry.instrumentation.api.incubator.semconv.db.DbClientAttributesGetter; +import io.opentelemetry.instrumentation.api.incubator.semconv.db.SqlStatementSanitizer; +import io.opentelemetry.javaagent.bootstrap.internal.AgentCommonConfig; +import io.opentelemetry.semconv.incubating.DbIncubatingAttributes; import javax.annotation.Nullable; final class GeodeDbAttributesGetter implements DbClientAttributesGetter { private static final SqlStatementSanitizer sanitizer = - SqlStatementSanitizer.create(CommonConfig.get().isStatementSanitizationEnabled()); + SqlStatementSanitizer.create(AgentCommonConfig.get().isStatementSanitizationEnabled()); @Override public String getSystem(GeodeRequest request) { - return SemanticAttributes.DbSystemValues.GEODE; + return DbIncubatingAttributes.DbSystemValues.GEODE; } @Override diff --git a/instrumentation/geode-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/geode/GeodeSingletons.java b/instrumentation/geode-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/geode/GeodeSingletons.java index 15d67bcb9740..bca045c73674 100644 --- a/instrumentation/geode-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/geode/GeodeSingletons.java +++ b/instrumentation/geode-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/geode/GeodeSingletons.java @@ -6,10 +6,10 @@ package io.opentelemetry.javaagent.instrumentation.geode; import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.instrumentation.api.incubator.semconv.db.DbClientAttributesExtractor; +import io.opentelemetry.instrumentation.api.incubator.semconv.db.DbClientSpanNameExtractor; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; import io.opentelemetry.instrumentation.api.instrumenter.SpanKindExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.db.DbClientAttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.db.DbClientSpanNameExtractor; public final class GeodeSingletons { private static final String INSTRUMENTATION_NAME = "io.opentelemetry.geode-1.4"; diff --git a/instrumentation/geode-1.4/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/geode/PutGetTest.java b/instrumentation/geode-1.4/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/geode/PutGetTest.java index e9a2db2d300c..954ae031785d 100644 --- a/instrumentation/geode-1.4/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/geode/PutGetTest.java +++ b/instrumentation/geode-1.4/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/geode/PutGetTest.java @@ -12,7 +12,7 @@ import io.opentelemetry.api.trace.SpanKind; import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.opentelemetry.semconv.incubating.DbIncubatingAttributes; import java.io.DataInput; import java.io.DataOutput; import java.io.IOException; @@ -126,32 +126,30 @@ void shouldSanitizeGeodeQuery() throws QueryException { void assertGeodeTrace(String verb, String query) { testing.waitAndAssertTraces( trace -> - trace - .hasSize(4) - .hasSpansSatisfyingExactly( - span -> span.hasName("someTrace").hasKind(SpanKind.INTERNAL), - span -> - span.hasName("clear test-region") - .hasKind(SpanKind.CLIENT) - .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.DB_SYSTEM, "geode"), - equalTo(SemanticAttributes.DB_NAME, "test-region"), - equalTo(SemanticAttributes.DB_OPERATION, "clear")), - span -> - span.hasName("put test-region") - .hasKind(SpanKind.CLIENT) - .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.DB_SYSTEM, "geode"), - equalTo(SemanticAttributes.DB_NAME, "test-region"), - equalTo(SemanticAttributes.DB_OPERATION, "put")), - span -> - span.hasName(verb.concat(" test-region")) - .hasKind(SpanKind.CLIENT) - .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.DB_SYSTEM, "geode"), - equalTo(SemanticAttributes.DB_NAME, "test-region"), - equalTo(SemanticAttributes.DB_OPERATION, verb), - equalTo(SemanticAttributes.DB_STATEMENT, query)))); + trace.hasSpansSatisfyingExactly( + span -> span.hasName("someTrace").hasKind(SpanKind.INTERNAL), + span -> + span.hasName("clear test-region") + .hasKind(SpanKind.CLIENT) + .hasAttributesSatisfyingExactly( + equalTo(DbIncubatingAttributes.DB_SYSTEM, "geode"), + equalTo(DbIncubatingAttributes.DB_NAME, "test-region"), + equalTo(DbIncubatingAttributes.DB_OPERATION, "clear")), + span -> + span.hasName("put test-region") + .hasKind(SpanKind.CLIENT) + .hasAttributesSatisfyingExactly( + equalTo(DbIncubatingAttributes.DB_SYSTEM, "geode"), + equalTo(DbIncubatingAttributes.DB_NAME, "test-region"), + equalTo(DbIncubatingAttributes.DB_OPERATION, "put")), + span -> + span.hasName(verb.concat(" test-region")) + .hasKind(SpanKind.CLIENT) + .hasAttributesSatisfyingExactly( + equalTo(DbIncubatingAttributes.DB_SYSTEM, "geode"), + equalTo(DbIncubatingAttributes.DB_NAME, "test-region"), + equalTo(DbIncubatingAttributes.DB_OPERATION, verb), + equalTo(DbIncubatingAttributes.DB_STATEMENT, query)))); } static class Card implements DataSerializable { diff --git a/instrumentation/google-http-client-1.19/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/googlehttpclient/GoogleHttpClientHttpAttributesGetter.java b/instrumentation/google-http-client-1.19/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/googlehttpclient/GoogleHttpClientHttpAttributesGetter.java index 3555cecf0450..220875088880 100644 --- a/instrumentation/google-http-client-1.19/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/googlehttpclient/GoogleHttpClientHttpAttributesGetter.java +++ b/instrumentation/google-http-client-1.19/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/googlehttpclient/GoogleHttpClientHttpAttributesGetter.java @@ -7,7 +7,7 @@ import com.google.api.client.http.HttpRequest; import com.google.api.client.http.HttpResponse; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpClientAttributesGetter; +import io.opentelemetry.instrumentation.api.semconv.http.HttpClientAttributesGetter; import java.util.List; import javax.annotation.Nullable; @@ -41,4 +41,15 @@ public List getHttpResponseHeader( HttpRequest httpRequest, HttpResponse httpResponse, String name) { return httpResponse.getHeaders().getHeaderStringValues(name); } + + @Override + @Nullable + public String getServerAddress(HttpRequest request) { + return request.getUrl().getHost(); + } + + @Override + public Integer getServerPort(HttpRequest request) { + return request.getUrl().getPort(); + } } diff --git a/instrumentation/google-http-client-1.19/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/googlehttpclient/GoogleHttpClientNetAttributesGetter.java b/instrumentation/google-http-client-1.19/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/googlehttpclient/GoogleHttpClientNetAttributesGetter.java deleted file mode 100644 index 8fabc7d6d738..000000000000 --- a/instrumentation/google-http-client-1.19/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/googlehttpclient/GoogleHttpClientNetAttributesGetter.java +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.javaagent.instrumentation.googlehttpclient; - -import com.google.api.client.http.HttpRequest; -import com.google.api.client.http.HttpResponse; -import io.opentelemetry.instrumentation.api.instrumenter.net.NetClientAttributesGetter; -import javax.annotation.Nullable; - -final class GoogleHttpClientNetAttributesGetter - implements NetClientAttributesGetter { - - @Override - @Nullable - public String getServerAddress(HttpRequest request) { - return request.getUrl().getHost(); - } - - @Override - public Integer getServerPort(HttpRequest request) { - return request.getUrl().getPort(); - } -} diff --git a/instrumentation/google-http-client-1.19/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/googlehttpclient/GoogleHttpClientSingletons.java b/instrumentation/google-http-client-1.19/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/googlehttpclient/GoogleHttpClientSingletons.java index 514e22c03c60..68039f69b3ac 100644 --- a/instrumentation/google-http-client-1.19/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/googlehttpclient/GoogleHttpClientSingletons.java +++ b/instrumentation/google-http-client-1.19/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/googlehttpclient/GoogleHttpClientSingletons.java @@ -7,14 +7,8 @@ import com.google.api.client.http.HttpRequest; import com.google.api.client.http.HttpResponse; -import io.opentelemetry.api.GlobalOpenTelemetry; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpClientAttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpClientMetrics; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpSpanNameExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpSpanStatusExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.net.PeerServiceAttributesExtractor; -import io.opentelemetry.javaagent.bootstrap.internal.CommonConfig; +import io.opentelemetry.javaagent.bootstrap.internal.JavaagentHttpClientInstrumenters; public class GoogleHttpClientSingletons { private static final String INSTRUMENTATION_NAME = "io.opentelemetry.google-http-client-1.19"; @@ -22,27 +16,11 @@ public class GoogleHttpClientSingletons { private static final Instrumenter INSTRUMENTER; static { - GoogleHttpClientHttpAttributesGetter httpAttributesGetter = - new GoogleHttpClientHttpAttributesGetter(); - GoogleHttpClientNetAttributesGetter netAttributesGetter = - new GoogleHttpClientNetAttributesGetter(); - INSTRUMENTER = - Instrumenter.builder( - GlobalOpenTelemetry.get(), - INSTRUMENTATION_NAME, - HttpSpanNameExtractor.create(httpAttributesGetter)) - .setSpanStatusExtractor(HttpSpanStatusExtractor.create(httpAttributesGetter)) - .addAttributesExtractor( - HttpClientAttributesExtractor.builder(httpAttributesGetter, netAttributesGetter) - .setCapturedRequestHeaders(CommonConfig.get().getClientRequestHeaders()) - .setCapturedResponseHeaders(CommonConfig.get().getClientResponseHeaders()) - .build()) - .addAttributesExtractor( - PeerServiceAttributesExtractor.create( - netAttributesGetter, CommonConfig.get().getPeerServiceMapping())) - .addOperationMetrics(HttpClientMetrics.get()) - .buildClientInstrumenter(HttpHeaderSetter.INSTANCE); + JavaagentHttpClientInstrumenters.create( + INSTRUMENTATION_NAME, + new GoogleHttpClientHttpAttributesGetter(), + HttpHeaderSetter.INSTANCE); } public static Instrumenter instrumenter() { diff --git a/instrumentation/google-http-client-1.19/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/googlehttpclient/AbstractGoogleHttpClientTest.java b/instrumentation/google-http-client-1.19/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/googlehttpclient/AbstractGoogleHttpClientTest.java index 440d23aaaddb..5782383e4eb4 100644 --- a/instrumentation/google-http-client-1.19/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/googlehttpclient/AbstractGoogleHttpClientTest.java +++ b/instrumentation/google-http-client-1.19/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/googlehttpclient/AbstractGoogleHttpClientTest.java @@ -5,7 +5,6 @@ package io.opentelemetry.javaagent.instrumentation.googlehttpclient; -import static io.opentelemetry.api.common.AttributeKey.stringKey; import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.satisfies; @@ -22,14 +21,23 @@ import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpClientTest; import io.opentelemetry.instrumentation.testing.junit.http.HttpClientInstrumentationExtension; import io.opentelemetry.instrumentation.testing.junit.http.HttpClientTestOptions; +import io.opentelemetry.sdk.testing.assertj.AttributeAssertion; import io.opentelemetry.sdk.trace.data.StatusData; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.opentelemetry.semconv.ErrorAttributes; +import io.opentelemetry.semconv.HttpAttributes; +import io.opentelemetry.semconv.NetworkAttributes; +import io.opentelemetry.semconv.ServerAttributes; +import io.opentelemetry.semconv.UrlAttributes; import java.net.URI; +import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.HashSet; +import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; +import org.assertj.core.api.AbstractLongAssert; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; @@ -75,7 +83,10 @@ public int sendRequest(HttpRequest request, String method, URI uri, Map attributes = + new ArrayList<>( + Arrays.asList( + equalTo(ServerAttributes.SERVER_ADDRESS, "localhost"), + satisfies(ServerAttributes.SERVER_PORT, AbstractLongAssert::isPositive), + equalTo(UrlAttributes.URL_FULL, uri.toString()), + equalTo(HttpAttributes.HTTP_REQUEST_METHOD, "GET"), + equalTo(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, 500), + equalTo(ErrorAttributes.ERROR_TYPE, "500"))); + testing.waitAndAssertTraces( trace -> trace.hasSpansSatisfyingExactly( span -> span.hasKind(SpanKind.CLIENT) .hasStatus(StatusData.error()) - .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.NET_PEER_NAME, "localhost"), - satisfies(SemanticAttributes.NET_PEER_PORT, port -> port.isPositive()), - equalTo(SemanticAttributes.HTTP_URL, uri.toString()), - equalTo(SemanticAttributes.HTTP_METHOD, "GET"), - equalTo(SemanticAttributes.HTTP_STATUS_CODE, 500), - satisfies( - SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH, - length -> length.isPositive())), + .hasAttributesSatisfyingExactly(attributes), span -> span.hasKind(SpanKind.SERVER).hasParent(trace.getSpan(0)))); } @@ -118,12 +132,14 @@ protected void configure(HttpClientTestOptions.Builder optionsBuilder) { // Circular redirects don't throw an exception with Google Http Client optionsBuilder.disableTestCircularRedirects(); + // can only use supported method + optionsBuilder.disableTestNonStandardHttpMethod(); + optionsBuilder.setHttpAttributes( uri -> { Set> attributes = new HashSet<>(HttpClientTestOptions.DEFAULT_HTTP_ATTRIBUTES); - attributes.remove(stringKey("net.protocol.name")); - attributes.remove(stringKey("net.protocol.version")); + attributes.remove(NetworkAttributes.NETWORK_PROTOCOL_VERSION); return attributes; }); } diff --git a/instrumentation/grails-3.0/javaagent/build.gradle.kts b/instrumentation/grails-3.0/javaagent/build.gradle.kts index e79493957066..5e8fbd2e824f 100644 --- a/instrumentation/grails-3.0/javaagent/build.gradle.kts +++ b/instrumentation/grails-3.0/javaagent/build.gradle.kts @@ -13,11 +13,15 @@ muzzle { // which (also obviously) does not exist skip("3.1.15", "3.3.6") // these versions pass if you add the grails maven repository (https://repo.grails.org/artifactory/core) - skip("3.2.0", "3.3.0", "3.3.1", "3.3.2", "3.3.3", "3.3.10", "3.3.13", "3.3.14", "3.3.15", "3.3.16", "4.0.0", "4.0.1", "4.0.5", "4.0.6", "4.0.7", "4.0.8", "4.0.9", "4.0.10", "4.0.11", "4.0.12", "4.0.13") + skip("3.2.0", "3.3.0", "3.3.1", "3.3.2", "3.3.3", "3.3.10", "3.3.13", "3.3.14", "3.3.15", "3.3.16", "3.3.17", "3.3.18", "4.0.0", "4.0.1", "4.0.5", "4.0.6", "4.0.7", "4.0.8", "4.0.9", "4.0.10", "4.0.11", "4.0.12", "4.0.13") assertInverse.set(true) } } +otelJava { + maxJavaVersionSupported.set(JavaVersion.VERSION_17) +} + val grailsVersion = "3.0.6" // first version that the tests pass on val springBootVersion = "1.2.5.RELEASE" @@ -45,12 +49,16 @@ configurations.configureEach { exclude("org.spockframework", "spock-core") } -configurations.configureEach { - if (!name.contains("muzzle")) { - resolutionStrategy { - eachDependency { - if (requested.group == "org.codehaus.groovy") { - useVersion("3.0.9") +val latestDepTest = findProperty("testLatestDeps") as Boolean + +if (!latestDepTest) { + configurations.configureEach { + if (!name.contains("muzzle")) { + resolutionStrategy { + eachDependency { + if (requested.group == "org.codehaus.groovy") { + useVersion("3.0.9") + } } } } @@ -65,8 +73,13 @@ configurations.testRuntimeClasspath { } } -tasks.withType().configureEach { - // required on jdk17 - jvmArgs("--add-opens=java.base/java.lang=ALL-UNNAMED") - jvmArgs("-XX:+IgnoreUnrecognizedVMOptions") +tasks { + withType().configureEach { + systemProperty("testLatestDeps", latestDepTest) + + // required on jdk17 + jvmArgs("--add-opens=java.base/java.lang=ALL-UNNAMED") + jvmArgs("-XX:+IgnoreUnrecognizedVMOptions") + jvmArgs("-Dotel.instrumentation.common.experimental.controller-telemetry.enabled=true") + } } diff --git a/instrumentation/grails-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/grails/GrailsServerSpanNaming.java b/instrumentation/grails-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/grails/GrailsServerSpanNaming.java index 620b455205dc..81ecd77e2878 100644 --- a/instrumentation/grails-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/grails/GrailsServerSpanNaming.java +++ b/instrumentation/grails-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/grails/GrailsServerSpanNaming.java @@ -5,13 +5,13 @@ package io.opentelemetry.javaagent.instrumentation.grails; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteGetter; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRouteGetter; import io.opentelemetry.javaagent.bootstrap.servlet.ServletContextPath; import org.grails.web.mapping.mvc.GrailsControllerUrlMappingInfo; public class GrailsServerSpanNaming { - public static final HttpRouteGetter SERVER_SPAN_NAME = + public static final HttpServerRouteGetter SERVER_SPAN_NAME = (context, info) -> { String action = info.getActionName() != null diff --git a/instrumentation/grails-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/grails/HandlerData.java b/instrumentation/grails-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/grails/HandlerData.java index ac03f8b097ab..cc09d92aa9cd 100644 --- a/instrumentation/grails-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/grails/HandlerData.java +++ b/instrumentation/grails-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/grails/HandlerData.java @@ -5,7 +5,7 @@ package io.opentelemetry.javaagent.instrumentation.grails; -import io.opentelemetry.instrumentation.api.instrumenter.util.SpanNames; +import io.opentelemetry.instrumentation.api.incubator.semconv.util.SpanNames; public class HandlerData { diff --git a/instrumentation/grails-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/grails/UrlMappingsInfoHandlerAdapterInstrumentation.java b/instrumentation/grails-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/grails/UrlMappingsInfoHandlerAdapterInstrumentation.java index 91c09f4b22e4..be051ac625a2 100644 --- a/instrumentation/grails-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/grails/UrlMappingsInfoHandlerAdapterInstrumentation.java +++ b/instrumentation/grails-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/grails/UrlMappingsInfoHandlerAdapterInstrumentation.java @@ -5,7 +5,6 @@ package io.opentelemetry.javaagent.instrumentation.grails; -import static io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteSource.CONTROLLER; import static net.bytebuddy.matcher.ElementMatchers.isMethod; import static net.bytebuddy.matcher.ElementMatchers.isPublic; import static net.bytebuddy.matcher.ElementMatchers.named; @@ -13,7 +12,8 @@ import static net.bytebuddy.matcher.ElementMatchers.takesArguments; import io.opentelemetry.context.Context; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteHolder; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRoute; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRouteSource; import io.opentelemetry.javaagent.bootstrap.Java8BytecodeBridge; import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; @@ -48,9 +48,9 @@ public static void nameSpan(@Advice.Argument(2) Object handler) { if (handler instanceof GrailsControllerUrlMappingInfo) { Context parentContext = Java8BytecodeBridge.currentContext(); - HttpRouteHolder.updateHttpRoute( + HttpServerRoute.update( parentContext, - CONTROLLER, + HttpServerRouteSource.CONTROLLER, GrailsServerSpanNaming.SERVER_SPAN_NAME, (GrailsControllerUrlMappingInfo) handler); } diff --git a/instrumentation/grails-3.0/javaagent/src/test/java/test/GrailsTest.java b/instrumentation/grails-3.0/javaagent/src/test/java/test/GrailsTest.java index 94941605f9a7..11abd923af40 100644 --- a/instrumentation/grails-3.0/javaagent/src/test/java/test/GrailsTest.java +++ b/instrumentation/grails-3.0/javaagent/src/test/java/test/GrailsTest.java @@ -19,6 +19,7 @@ import grails.boot.config.GrailsAutoConfiguration; import io.opentelemetry.api.common.Attributes; import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.api.internal.HttpConstants; import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpServerTest; import io.opentelemetry.instrumentation.testing.junit.http.HttpServerInstrumentationExtension; @@ -34,7 +35,6 @@ import java.util.Locale; import java.util.Map; import java.util.function.Consumer; -import org.assertj.core.api.Assertions; import org.junit.jupiter.api.extension.RegisterExtension; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.web.ServerProperties; @@ -42,6 +42,8 @@ public class GrailsTest extends AbstractHttpServerTest { + static final boolean testLatestDeps = Boolean.getBoolean("testLatestDeps"); + @RegisterExtension static final InstrumentationExtension testing = HttpServerInstrumentationExtension.forAgent(); @@ -64,6 +66,7 @@ protected void configure(HttpServerTestOptions options) { options.setHasErrorPageSpans( endpoint -> endpoint == ERROR || endpoint == EXCEPTION || endpoint == NOT_FOUND); options.setTestPathParam(true); + options.setResponseCodeOnNonStandardHttpMethod(testLatestDeps ? 200 : 501); } @SpringBootApplication @@ -106,7 +109,12 @@ private static Class load(String name) { } @Override - public String expectedHttpRoute(ServerEndpoint endpoint) { + public String expectedHttpRoute(ServerEndpoint endpoint, String method) { + if (HttpConstants._OTHER.equals(method)) { + return testLatestDeps + ? getContextPath() + "/test" + endpoint.getPath() + : getContextPath() + "/*"; + } if (PATH_PARAM.equals(endpoint)) { return getContextPath() + "/test/path"; } else if (QUERY_PARAM.equals(endpoint)) { @@ -174,8 +182,7 @@ public List> errorPageSpanAssertions( if (endpoint == NOT_FOUND) { spanAssertions.add( span -> - span.satisfies( - spanData -> Assertions.assertThat(spanData.getName()).endsWith(".sendError")) + span.satisfies(spanData -> assertThat(spanData.getName()).endsWith(".sendError")) .hasKind(SpanKind.INTERNAL) .hasAttributesSatisfying(Attributes::isEmpty)); } diff --git a/instrumentation/grails-3.0/javaagent/src/test/resources/application.yml b/instrumentation/grails-3.0/javaagent/src/test/resources/application.yml index 730dce4bbd12..9e4744a2680f 100644 --- a/instrumentation/grails-3.0/javaagent/src/test/resources/application.yml +++ b/instrumentation/grails-3.0/javaagent/src/test/resources/application.yml @@ -5,4 +5,4 @@ grails: spring: groovy: template: - check-template-location: false \ No newline at end of file + check-template-location: false diff --git a/instrumentation/graphql-java-12.0/javaagent/README.md b/instrumentation/graphql-java-12.0/javaagent/README.md deleted file mode 100644 index 7bf9addd2579..000000000000 --- a/instrumentation/graphql-java-12.0/javaagent/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# Settings for the GraphQL instrumentation - -| System property | Type | Default | Description | -|---|---|---------|--------------------------------------------------------------------------------------------| -| `otel.instrumentation.graphql.query-sanitizer.enabled` | Boolean | `true` | Whether to remove sensitive information from query source that is added as span attribute. | diff --git a/instrumentation/graphql-java-12.0/javaagent/build.gradle.kts b/instrumentation/graphql-java-12.0/javaagent/build.gradle.kts deleted file mode 100644 index 699820cf67a6..000000000000 --- a/instrumentation/graphql-java-12.0/javaagent/build.gradle.kts +++ /dev/null @@ -1,21 +0,0 @@ -plugins { - id("otel.javaagent-instrumentation") -} - -muzzle { - pass { - group.set("com.graphql-java") - module.set("graphql-java") - versions.set("[12,)") - skip("230521-nf-execution") - assertInverse.set(true) - } -} - -dependencies { - implementation(project(":instrumentation:graphql-java-12.0:library")) - - library("com.graphql-java:graphql-java:12.0") - - testImplementation(project(":instrumentation:graphql-java-12.0:testing")) -} diff --git a/instrumentation/graphql-java-12.0/library/src/main/java/io/opentelemetry/instrumentation/graphql/v12_0/GraphQLTelemetry.java b/instrumentation/graphql-java-12.0/library/src/main/java/io/opentelemetry/instrumentation/graphql/v12_0/GraphQLTelemetry.java deleted file mode 100644 index 1f9b0eb47034..000000000000 --- a/instrumentation/graphql-java-12.0/library/src/main/java/io/opentelemetry/instrumentation/graphql/v12_0/GraphQLTelemetry.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.graphql.v12_0; - -import graphql.ExecutionResult; -import graphql.execution.instrumentation.Instrumentation; -import graphql.execution.instrumentation.parameters.InstrumentationExecutionParameters; -import io.opentelemetry.api.OpenTelemetry; -import io.opentelemetry.api.trace.StatusCode; -import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; -import io.opentelemetry.instrumentation.api.instrumenter.InstrumenterBuilder; -import io.opentelemetry.instrumentation.api.instrumenter.SpanStatusExtractor; - -@SuppressWarnings("AbbreviationAsWordInName") -public final class GraphQLTelemetry { - private static final String INSTRUMENTATION_NAME = "io.opentelemetry.graphql-java-12.0"; - - /** Returns a new {@link GraphQLTelemetry} configured with the given {@link OpenTelemetry}. */ - public static GraphQLTelemetry create(OpenTelemetry openTelemetry) { - return builder(openTelemetry).build(); - } - - /** - * Returns a new {@link GraphQLTelemetryBuilder} configured with the given {@link OpenTelemetry}. - */ - public static GraphQLTelemetryBuilder builder(OpenTelemetry openTelemetry) { - return new GraphQLTelemetryBuilder(openTelemetry); - } - - private final Instrumenter instrumenter; - private final boolean sanitizeQuery; - - GraphQLTelemetry(OpenTelemetry openTelemetry, boolean sanitizeQuery) { - InstrumenterBuilder builder = - Instrumenter.builder( - openTelemetry, INSTRUMENTATION_NAME, ignored -> "GraphQL Operation") - .setSpanStatusExtractor( - (spanStatusBuilder, instrumentationExecutionParameters, executionResult, error) -> { - if (!executionResult.getErrors().isEmpty()) { - spanStatusBuilder.setStatus(StatusCode.ERROR); - } else { - SpanStatusExtractor.getDefault() - .extract( - spanStatusBuilder, - instrumentationExecutionParameters, - executionResult, - error); - } - }); - builder.addAttributesExtractor(new GraphqlAttributesExtractor()); - - this.instrumenter = builder.buildInstrumenter(); - this.sanitizeQuery = sanitizeQuery; - } - - /** - * Returns a new {@link Instrumentation} that generates telemetry for received GraphQL requests. - */ - public Instrumentation newInstrumentation() { - return new OpenTelemetryInstrumentation(instrumenter, sanitizeQuery); - } -} diff --git a/instrumentation/graphql-java/README.md b/instrumentation/graphql-java/README.md new file mode 100644 index 000000000000..0509214588d1 --- /dev/null +++ b/instrumentation/graphql-java/README.md @@ -0,0 +1,12 @@ +# Settings for the GraphQL instrumentation + +| System property | Type | Default | Description | +|--------------------------------------------------------|---------|---------|--------------------------------------------------------------------------------------------| +| `otel.instrumentation.graphql.query-sanitizer.enabled` | Boolean | `true` | Whether to remove sensitive information from query source that is added as span attribute. | + +# Settings for the GraphQL 20 instrumentation + +| System property | Type | Default | Description | +|-------------------------------------------------------------|---------|---------|-----------------------------------------------------------------------------------------------------------------------------------| +| `otel.instrumentation.graphql.data-fetcher.enabled` | Boolean | `false` | Whether to create spans for data fetchers. | +| `otel.instrumentation.graphql.trivial-data-fetcher.enabled` | Boolean | `false` | Whether to create spans for trivial data fetchers. A trivial data fetcher is one that simply maps data from an object to a field. | diff --git a/instrumentation/graphql-java/graphql-java-12.0/javaagent/build.gradle.kts b/instrumentation/graphql-java/graphql-java-12.0/javaagent/build.gradle.kts new file mode 100644 index 000000000000..c7e0cff0920a --- /dev/null +++ b/instrumentation/graphql-java/graphql-java-12.0/javaagent/build.gradle.kts @@ -0,0 +1,26 @@ +plugins { + id("otel.javaagent-instrumentation") +} + +muzzle { + pass { + group.set("com.graphql-java") + module.set("graphql-java") + versions.set("[12,20)") + skip("230521-nf-execution") + assertInverse.set(true) + } +} + +dependencies { + implementation(project(":instrumentation:graphql-java:graphql-java-12.0:library")) + implementation(project(":instrumentation:graphql-java:graphql-java-common:library")) + + library("com.graphql-java:graphql-java:12.0") + + testInstrumentation(project(":instrumentation:graphql-java:graphql-java-20.0:javaagent")) + + testImplementation(project(":instrumentation:graphql-java:graphql-java-common:testing")) + + latestDepTestLibrary("com.graphql-java:graphql-java:19.+") +} diff --git a/instrumentation/graphql-java-12.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/graphql/v12_0/GraphqlInstrumentation.java b/instrumentation/graphql-java/graphql-java-12.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/graphql/v12_0/GraphqlInstrumentation.java similarity index 95% rename from instrumentation/graphql-java-12.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/graphql/v12_0/GraphqlInstrumentation.java rename to instrumentation/graphql-java/graphql-java-12.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/graphql/v12_0/GraphqlInstrumentation.java index 70f13a49f656..543efefaab08 100644 --- a/instrumentation/graphql-java-12.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/graphql/v12_0/GraphqlInstrumentation.java +++ b/instrumentation/graphql-java/graphql-java-12.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/graphql/v12_0/GraphqlInstrumentation.java @@ -17,7 +17,7 @@ import net.bytebuddy.description.type.TypeDescription; import net.bytebuddy.matcher.ElementMatcher; -public class GraphqlInstrumentation implements TypeInstrumentation { +class GraphqlInstrumentation implements TypeInstrumentation { @Override public ElementMatcher typeMatcher() { diff --git a/instrumentation/graphql-java-12.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/graphql/v12_0/GraphqlInstrumentationModule.java b/instrumentation/graphql-java/graphql-java-12.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/graphql/v12_0/GraphqlInstrumentationModule.java similarity index 65% rename from instrumentation/graphql-java-12.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/graphql/v12_0/GraphqlInstrumentationModule.java rename to instrumentation/graphql-java/graphql-java-12.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/graphql/v12_0/GraphqlInstrumentationModule.java index 2839845a500b..1df81f84334d 100644 --- a/instrumentation/graphql-java-12.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/graphql/v12_0/GraphqlInstrumentationModule.java +++ b/instrumentation/graphql-java/graphql-java-12.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/graphql/v12_0/GraphqlInstrumentationModule.java @@ -5,11 +5,15 @@ package io.opentelemetry.javaagent.instrumentation.graphql.v12_0; +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.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.Collections; import java.util.List; +import net.bytebuddy.matcher.ElementMatcher; @SuppressWarnings("unused") @AutoService(InstrumentationModule.class) @@ -19,6 +23,13 @@ public GraphqlInstrumentationModule() { super("graphql-java", "graphql-java-12.0"); } + @Override + public ElementMatcher.Junction classLoaderMatcher() { + // added in 20.0 + return not( + hasClassesNamed("graphql.execution.instrumentation.SimplePerformantInstrumentation")); + } + @Override public List typeInstrumentations() { return Collections.singletonList(new GraphqlInstrumentation()); diff --git a/instrumentation/graphql-java/graphql-java-12.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/graphql/v12_0/GraphqlSingletons.java b/instrumentation/graphql-java/graphql-java-12.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/graphql/v12_0/GraphqlSingletons.java new file mode 100644 index 000000000000..162a54a62728 --- /dev/null +++ b/instrumentation/graphql-java/graphql-java-12.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/graphql/v12_0/GraphqlSingletons.java @@ -0,0 +1,31 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.graphql.v12_0; + +import graphql.execution.instrumentation.Instrumentation; +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.instrumentation.graphql.internal.InstrumentationUtil; +import io.opentelemetry.instrumentation.graphql.v12_0.GraphQLTelemetry; +import io.opentelemetry.javaagent.bootstrap.internal.AgentInstrumentationConfig; + +public final class GraphqlSingletons { + + private static final boolean QUERY_SANITIZATION_ENABLED = + AgentInstrumentationConfig.get() + .getBoolean("otel.instrumentation.graphql.query-sanitizer.enabled", true); + + private static final GraphQLTelemetry TELEMETRY = + GraphQLTelemetry.builder(GlobalOpenTelemetry.get()) + .setSanitizeQuery(QUERY_SANITIZATION_ENABLED) + .build(); + + private GraphqlSingletons() {} + + public static Instrumentation addInstrumentation(Instrumentation instrumentation) { + Instrumentation ourInstrumentation = TELEMETRY.newInstrumentation(); + return InstrumentationUtil.addInstrumentation(instrumentation, ourInstrumentation); + } +} diff --git a/instrumentation/graphql-java-12.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/graphql/v12_0/GraphqlTest.java b/instrumentation/graphql-java/graphql-java-12.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/graphql/v12_0/GraphqlTest.java similarity index 100% rename from instrumentation/graphql-java-12.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/graphql/v12_0/GraphqlTest.java rename to instrumentation/graphql-java/graphql-java-12.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/graphql/v12_0/GraphqlTest.java diff --git a/instrumentation/graphql-java-12.0/library/README.md b/instrumentation/graphql-java/graphql-java-12.0/library/README.md similarity index 94% rename from instrumentation/graphql-java-12.0/library/README.md rename to instrumentation/graphql-java/graphql-java-12.0/library/README.md index 40ef49479d3c..447473d1df20 100644 --- a/instrumentation/graphql-java-12.0/library/README.md +++ b/instrumentation/graphql-java/graphql-java-12.0/library/README.md @@ -1,4 +1,4 @@ -# Library Instrumentation for GraphQL Java version 12.0 and higher +# Library Instrumentation for GraphQL Java version 12.0 to 20.0 Provides OpenTelemetry instrumentation for [GraphQL Java](https://www.graphql-java.com/). diff --git a/instrumentation/graphql-java/graphql-java-12.0/library/build.gradle.kts b/instrumentation/graphql-java/graphql-java-12.0/library/build.gradle.kts new file mode 100644 index 000000000000..052455bdda7a --- /dev/null +++ b/instrumentation/graphql-java/graphql-java-12.0/library/build.gradle.kts @@ -0,0 +1,12 @@ +plugins { + id("otel.library-instrumentation") +} + +dependencies { + library("com.graphql-java:graphql-java:12.0") + implementation(project(":instrumentation:graphql-java:graphql-java-common:library")) + + testImplementation(project(":instrumentation:graphql-java:graphql-java-common:testing")) + + latestDepTestLibrary("com.graphql-java:graphql-java:19.+") +} diff --git a/instrumentation/graphql-java/graphql-java-12.0/library/src/main/java/io/opentelemetry/instrumentation/graphql/v12_0/GraphQLTelemetry.java b/instrumentation/graphql-java/graphql-java-12.0/library/src/main/java/io/opentelemetry/instrumentation/graphql/v12_0/GraphQLTelemetry.java new file mode 100644 index 000000000000..4671cce91cfb --- /dev/null +++ b/instrumentation/graphql-java/graphql-java-12.0/library/src/main/java/io/opentelemetry/instrumentation/graphql/v12_0/GraphQLTelemetry.java @@ -0,0 +1,42 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.graphql.v12_0; + +import graphql.execution.instrumentation.Instrumentation; +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.instrumentation.graphql.internal.OpenTelemetryInstrumentationHelper; + +@SuppressWarnings("AbbreviationAsWordInName") +public final class GraphQLTelemetry { + private static final String INSTRUMENTATION_NAME = "io.opentelemetry.graphql-java-12.0"; + + /** Returns a new {@link GraphQLTelemetry} configured with the given {@link OpenTelemetry}. */ + public static GraphQLTelemetry create(OpenTelemetry openTelemetry) { + return builder(openTelemetry).build(); + } + + /** + * Returns a new {@link GraphQLTelemetryBuilder} configured with the given {@link OpenTelemetry}. + */ + public static GraphQLTelemetryBuilder builder(OpenTelemetry openTelemetry) { + return new GraphQLTelemetryBuilder(openTelemetry); + } + + private final OpenTelemetryInstrumentationHelper helper; + + GraphQLTelemetry(OpenTelemetry openTelemetry, boolean sanitizeQuery) { + helper = + OpenTelemetryInstrumentationHelper.create( + openTelemetry, INSTRUMENTATION_NAME, sanitizeQuery); + } + + /** + * Returns a new {@link Instrumentation} that generates telemetry for received GraphQL requests. + */ + public Instrumentation newInstrumentation() { + return new OpenTelemetryInstrumentation(helper); + } +} diff --git a/instrumentation/graphql-java-12.0/library/src/main/java/io/opentelemetry/instrumentation/graphql/v12_0/GraphQLTelemetryBuilder.java b/instrumentation/graphql-java/graphql-java-12.0/library/src/main/java/io/opentelemetry/instrumentation/graphql/v12_0/GraphQLTelemetryBuilder.java similarity index 100% rename from instrumentation/graphql-java-12.0/library/src/main/java/io/opentelemetry/instrumentation/graphql/v12_0/GraphQLTelemetryBuilder.java rename to instrumentation/graphql-java/graphql-java-12.0/library/src/main/java/io/opentelemetry/instrumentation/graphql/v12_0/GraphQLTelemetryBuilder.java diff --git a/instrumentation/graphql-java/graphql-java-12.0/library/src/main/java/io/opentelemetry/instrumentation/graphql/v12_0/OpenTelemetryInstrumentation.java b/instrumentation/graphql-java/graphql-java-12.0/library/src/main/java/io/opentelemetry/instrumentation/graphql/v12_0/OpenTelemetryInstrumentation.java new file mode 100644 index 000000000000..01a784a96c17 --- /dev/null +++ b/instrumentation/graphql-java/graphql-java-12.0/library/src/main/java/io/opentelemetry/instrumentation/graphql/v12_0/OpenTelemetryInstrumentation.java @@ -0,0 +1,51 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.graphql.v12_0; + +import graphql.ExecutionResult; +import graphql.execution.instrumentation.InstrumentationContext; +import graphql.execution.instrumentation.InstrumentationState; +import graphql.execution.instrumentation.SimpleInstrumentation; +import graphql.execution.instrumentation.parameters.InstrumentationExecuteOperationParameters; +import graphql.execution.instrumentation.parameters.InstrumentationExecutionParameters; +import graphql.execution.instrumentation.parameters.InstrumentationFieldFetchParameters; +import graphql.schema.DataFetcher; +import io.opentelemetry.instrumentation.graphql.internal.OpenTelemetryInstrumentationHelper; +import io.opentelemetry.instrumentation.graphql.internal.OpenTelemetryInstrumentationState; + +final class OpenTelemetryInstrumentation extends SimpleInstrumentation { + private final OpenTelemetryInstrumentationHelper helper; + + OpenTelemetryInstrumentation(OpenTelemetryInstrumentationHelper helper) { + this.helper = helper; + } + + @Override + public InstrumentationState createState() { + return new OpenTelemetryInstrumentationState(); + } + + @Override + public InstrumentationContext beginExecution( + InstrumentationExecutionParameters parameters) { + OpenTelemetryInstrumentationState state = parameters.getInstrumentationState(); + return helper.beginExecution(state); + } + + @Override + public InstrumentationContext beginExecuteOperation( + InstrumentationExecuteOperationParameters parameters) { + OpenTelemetryInstrumentationState state = parameters.getInstrumentationState(); + return helper.beginExecuteOperation(parameters, state); + } + + @Override + public DataFetcher instrumentDataFetcher( + DataFetcher dataFetcher, InstrumentationFieldFetchParameters parameters) { + OpenTelemetryInstrumentationState state = parameters.getInstrumentationState(); + return helper.instrumentDataFetcher(dataFetcher, state); + } +} diff --git a/instrumentation/graphql-java-12.0/library/src/test/java/io/opentelemetry/instrumentation/graphql/v12_0/GraphqlTest.java b/instrumentation/graphql-java/graphql-java-12.0/library/src/test/java/io/opentelemetry/instrumentation/graphql/v12_0/GraphqlTest.java similarity index 100% rename from instrumentation/graphql-java-12.0/library/src/test/java/io/opentelemetry/instrumentation/graphql/v12_0/GraphqlTest.java rename to instrumentation/graphql-java/graphql-java-12.0/library/src/test/java/io/opentelemetry/instrumentation/graphql/v12_0/GraphqlTest.java diff --git a/instrumentation/graphql-java/graphql-java-20.0/javaagent/build.gradle.kts b/instrumentation/graphql-java/graphql-java-20.0/javaagent/build.gradle.kts new file mode 100644 index 000000000000..6f6df45ef99d --- /dev/null +++ b/instrumentation/graphql-java/graphql-java-20.0/javaagent/build.gradle.kts @@ -0,0 +1,34 @@ +plugins { + id("otel.javaagent-instrumentation") +} + +muzzle { + pass { + group.set("com.graphql-java") + module.set("graphql-java") + versions.set("[20,)") + skip("230521-nf-execution") + assertInverse.set(true) + } +} + +dependencies { + implementation(project(":instrumentation:graphql-java:graphql-java-20.0:library")) + implementation(project(":instrumentation:graphql-java:graphql-java-common:library")) + + library("com.graphql-java:graphql-java:20.0") + + testInstrumentation(project(":instrumentation:graphql-java:graphql-java-12.0:javaagent")) + + testImplementation(project(":instrumentation:graphql-java:graphql-java-common:testing")) +} + +tasks.withType().configureEach { + jvmArgs("-Dotel.instrumentation.graphql.data-fetcher.enabled=true") +} + +if (findProperty("testLatestDeps") as Boolean) { + otelJava { + minJavaVersionSupported.set(JavaVersion.VERSION_11) + } +} diff --git a/instrumentation/graphql-java/graphql-java-20.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/graphql/v20_0/GraphqlInstrumentation.java b/instrumentation/graphql-java/graphql-java-20.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/graphql/v20_0/GraphqlInstrumentation.java new file mode 100644 index 000000000000..07b891bd52f2 --- /dev/null +++ b/instrumentation/graphql-java/graphql-java-20.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/graphql/v20_0/GraphqlInstrumentation.java @@ -0,0 +1,115 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.graphql.v20_0; + +import static io.opentelemetry.javaagent.instrumentation.graphql.v20_0.GraphqlSingletons.addInstrumentation; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.none; + +import com.google.errorprone.annotations.CanIgnoreReturnValue; +import graphql.execution.instrumentation.Instrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import net.bytebuddy.asm.Advice; +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.matcher.ElementMatcher; +import net.bytebuddy.pool.TypePool; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; + +class GraphqlInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("graphql.GraphQL"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + none(), this.getClass().getName() + "$AddInstrumentationAdvice"); + + transformer.applyTransformer( + (builder, typeDescription, classLoader, javaModule, protectionDomain) -> + builder.visit( + new AsmVisitorWrapper() { + @Override + public int mergeWriter(int flags) { + return flags; + } + + @Override + @CanIgnoreReturnValue + 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.ASM9, classVisitor) { + @Override + public MethodVisitor visitMethod( + int access, + String name, + String descriptor, + String signature, + String[] exceptions) { + MethodVisitor mv = + super.visitMethod(access, name, descriptor, signature, exceptions); + if ("".equals(name) + && "(Lgraphql/GraphQL$Builder;)V".equals(descriptor)) { + return new MethodVisitor(api, mv) { + @Override + public void visitFieldInsn( + int opcode, String owner, String name, String descriptor) { + // Call GraphqlSingletons.addInstrumentation on the value before it is + // written to the instrumentation field + if (opcode == Opcodes.PUTFIELD + && "instrumentation".equals(name) + && "Lgraphql/execution/instrumentation/Instrumentation;" + .equals(descriptor)) { + mv.visitMethodInsn( + Opcodes.INVOKESTATIC, + Type.getInternalName(GraphqlSingletons.class), + "addInstrumentation", + "(Lgraphql/execution/instrumentation/Instrumentation;)Lgraphql/execution/instrumentation/Instrumentation;", + false); + } + super.visitFieldInsn(opcode, owner, name, descriptor); + } + }; + } + return mv; + } + }; + } + })); + } + + @SuppressWarnings("unused") + public static class AddInstrumentationAdvice { + @Advice.OnMethodExit(suppress = Throwable.class) + public static void onExit(@Advice.Return(readOnly = false) Instrumentation instrumentation) { + // this advice is here only to get GraphqlSingletons injected and checked by muzzle + instrumentation = addInstrumentation(instrumentation); + } + } +} diff --git a/instrumentation/graphql-java/graphql-java-20.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/graphql/v20_0/GraphqlInstrumentationModule.java b/instrumentation/graphql-java/graphql-java-20.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/graphql/v20_0/GraphqlInstrumentationModule.java new file mode 100644 index 000000000000..63d41ac3c5c9 --- /dev/null +++ b/instrumentation/graphql-java/graphql-java-20.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/graphql/v20_0/GraphqlInstrumentationModule.java @@ -0,0 +1,49 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.graphql.v20_0; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.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.extension.instrumentation.internal.ExperimentalInstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.internal.injection.ClassInjector; +import io.opentelemetry.javaagent.extension.instrumentation.internal.injection.InjectionMode; +import java.util.Collections; +import java.util.List; +import net.bytebuddy.matcher.ElementMatcher; + +@SuppressWarnings("unused") +@AutoService(InstrumentationModule.class) +public class GraphqlInstrumentationModule extends InstrumentationModule + implements ExperimentalInstrumentationModule { + + public GraphqlInstrumentationModule() { + super("graphql-java", "graphql-java-20.0"); + } + + @Override + public ElementMatcher.Junction classLoaderMatcher() { + // added in 20.0 + return hasClassesNamed("graphql.execution.instrumentation.SimplePerformantInstrumentation"); + } + + @Override + public List typeInstrumentations() { + return Collections.singletonList(new GraphqlInstrumentation()); + } + + @Override + public void injectClasses(ClassInjector injector) { + // we do not use ByteBuddy Advice dispatching in this instrumentation + // Instead, we manually call GraphqlSingletons via ASM + // Easiest solution to work with indy is to inject an indy-proxy to be invoked + injector + .proxyBuilder("io.opentelemetry.javaagent.instrumentation.graphql.v20_0.GraphqlSingletons") + .inject(InjectionMode.CLASS_ONLY); + } +} diff --git a/instrumentation/graphql-java/graphql-java-20.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/graphql/v20_0/GraphqlSingletons.java b/instrumentation/graphql-java/graphql-java-20.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/graphql/v20_0/GraphqlSingletons.java new file mode 100644 index 000000000000..11f08b6c841a --- /dev/null +++ b/instrumentation/graphql-java/graphql-java-20.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/graphql/v20_0/GraphqlSingletons.java @@ -0,0 +1,39 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.graphql.v20_0; + +import graphql.execution.instrumentation.Instrumentation; +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.instrumentation.graphql.internal.InstrumentationUtil; +import io.opentelemetry.instrumentation.graphql.v20_0.GraphQLTelemetry; +import io.opentelemetry.javaagent.bootstrap.internal.AgentInstrumentationConfig; + +public final class GraphqlSingletons { + + private static final boolean QUERY_SANITIZATION_ENABLED = + AgentInstrumentationConfig.get() + .getBoolean("otel.instrumentation.graphql.query-sanitizer.enabled", true); + private static final boolean DATA_FETCHER_ENABLED = + AgentInstrumentationConfig.get() + .getBoolean("otel.instrumentation.graphql.data-fetcher.enabled", false); + private static final boolean TRIVIAL_DATA_FETCHER_ENABLED = + AgentInstrumentationConfig.get() + .getBoolean("otel.instrumentation.graphql.trivial-data-fetcher.enabled", false); + + private static final GraphQLTelemetry TELEMETRY = + GraphQLTelemetry.builder(GlobalOpenTelemetry.get()) + .setSanitizeQuery(QUERY_SANITIZATION_ENABLED) + .setDataFetcherInstrumentationEnabled(DATA_FETCHER_ENABLED) + .setTrivialDataFetcherInstrumentationEnabled(TRIVIAL_DATA_FETCHER_ENABLED) + .build(); + + private GraphqlSingletons() {} + + public static Instrumentation addInstrumentation(Instrumentation instrumentation) { + Instrumentation ourInstrumentation = TELEMETRY.newInstrumentation(); + return InstrumentationUtil.addInstrumentation(instrumentation, ourInstrumentation); + } +} diff --git a/instrumentation/graphql-java/graphql-java-20.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/graphql/v20_0/GraphqlTest.java b/instrumentation/graphql-java/graphql-java-20.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/graphql/v20_0/GraphqlTest.java new file mode 100644 index 000000000000..9de908c0105c --- /dev/null +++ b/instrumentation/graphql-java/graphql-java-20.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/graphql/v20_0/GraphqlTest.java @@ -0,0 +1,31 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.graphql.v20_0; + +import graphql.GraphQL; +import io.opentelemetry.instrumentation.graphql.AbstractGraphqlTest; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import org.junit.jupiter.api.extension.RegisterExtension; + +public class GraphqlTest extends AbstractGraphqlTest { + + @RegisterExtension + private static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + @Override + protected InstrumentationExtension getTesting() { + return testing; + } + + @Override + protected void configure(GraphQL.Builder builder) {} + + @Override + protected boolean hasDataFetcherSpans() { + return true; + } +} diff --git a/instrumentation/graphql-java/graphql-java-20.0/library/README.md b/instrumentation/graphql-java/graphql-java-20.0/library/README.md new file mode 100644 index 000000000000..2482e76ee8d9 --- /dev/null +++ b/instrumentation/graphql-java/graphql-java-20.0/library/README.md @@ -0,0 +1,40 @@ +# Library Instrumentation for GraphQL Java version 20.0 and higher + +Provides OpenTelemetry instrumentation for [GraphQL Java](https://www.graphql-java.com/). + +## Quickstart + +### Add these dependencies to your project + +Replace `OPENTELEMETRY_VERSION` with the [latest +release](https://search.maven.org/search?q=g:io.opentelemetry.instrumentation%20AND%20a:opentelemetry-graphql-java-12.0). + +For Maven, add to your `pom.xml` dependencies: + +```xml + + + io.opentelemetry.instrumentation + opentelemetry-graphql-java-20.0 + OPENTELEMETRY_VERSION + + +``` + +For Gradle, add to your dependencies: + +```groovy +implementation("io.opentelemetry.instrumentation:opentelemetry-graphql-java-20.0:OPENTELEMETRY_VERSION") +``` + +### Usage + +The instrumentation library provides a GraphQL Java `Instrumentation` implementation that can be +added to an instance of the `GraphQL` to provide OpenTelemetry-based spans. + +```java +void configure(OpenTelemetry openTelemetry, GraphQL.Builder builder) { + GraphQLTelemetry telemetry = GraphQLTelemetry.builder(openTelemetry).build(); + builder.instrumentation(telemetry.newInstrumentation()); +} +``` diff --git a/instrumentation/graphql-java/graphql-java-20.0/library/build.gradle.kts b/instrumentation/graphql-java/graphql-java-20.0/library/build.gradle.kts new file mode 100644 index 000000000000..79f49cf21985 --- /dev/null +++ b/instrumentation/graphql-java/graphql-java-20.0/library/build.gradle.kts @@ -0,0 +1,16 @@ +plugins { + id("otel.library-instrumentation") +} + +dependencies { + library("com.graphql-java:graphql-java:20.0") + implementation(project(":instrumentation:graphql-java:graphql-java-common:library")) + + testImplementation(project(":instrumentation:graphql-java:graphql-java-common:testing")) +} + +if (findProperty("testLatestDeps") as Boolean) { + otelJava { + minJavaVersionSupported.set(JavaVersion.VERSION_11) + } +} diff --git a/instrumentation/graphql-java/graphql-java-20.0/library/src/main/java/io/opentelemetry/instrumentation/graphql/v20_0/GraphQLTelemetry.java b/instrumentation/graphql-java/graphql-java-20.0/library/src/main/java/io/opentelemetry/instrumentation/graphql/v20_0/GraphQLTelemetry.java new file mode 100644 index 000000000000..7b7f598f0f46 --- /dev/null +++ b/instrumentation/graphql-java/graphql-java-20.0/library/src/main/java/io/opentelemetry/instrumentation/graphql/v20_0/GraphQLTelemetry.java @@ -0,0 +1,50 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.graphql.v20_0; + +import graphql.execution.instrumentation.Instrumentation; +import graphql.schema.DataFetchingEnvironment; +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import io.opentelemetry.instrumentation.graphql.internal.OpenTelemetryInstrumentationHelper; + +@SuppressWarnings("AbbreviationAsWordInName") +public final class GraphQLTelemetry { + + /** Returns a new {@link GraphQLTelemetry} configured with the given {@link OpenTelemetry}. */ + public static GraphQLTelemetry create(OpenTelemetry openTelemetry) { + return builder(openTelemetry).build(); + } + + /** + * Returns a new {@link GraphQLTelemetryBuilder} configured with the given {@link OpenTelemetry}. + */ + public static GraphQLTelemetryBuilder builder(OpenTelemetry openTelemetry) { + return new GraphQLTelemetryBuilder(openTelemetry); + } + + private final OpenTelemetryInstrumentationHelper helper; + private final Instrumenter dataFetcherInstrumenter; + private final boolean createSpansForTrivialDataFetcher; + + GraphQLTelemetry( + OpenTelemetry openTelemetry, + boolean sanitizeQuery, + Instrumenter dataFetcherInstrumenter, + boolean createSpansForTrivialDataFetcher) { + helper = GraphqlInstrumenterFactory.createInstrumentationHelper(openTelemetry, sanitizeQuery); + this.dataFetcherInstrumenter = dataFetcherInstrumenter; + this.createSpansForTrivialDataFetcher = createSpansForTrivialDataFetcher; + } + + /** + * Returns a new {@link Instrumentation} that generates telemetry for received GraphQL requests. + */ + public Instrumentation newInstrumentation() { + return new OpenTelemetryInstrumentation( + helper, dataFetcherInstrumenter, createSpansForTrivialDataFetcher); + } +} diff --git a/instrumentation/graphql-java/graphql-java-20.0/library/src/main/java/io/opentelemetry/instrumentation/graphql/v20_0/GraphQLTelemetryBuilder.java b/instrumentation/graphql-java/graphql-java-20.0/library/src/main/java/io/opentelemetry/instrumentation/graphql/v20_0/GraphQLTelemetryBuilder.java new file mode 100644 index 000000000000..a7c285e9dc82 --- /dev/null +++ b/instrumentation/graphql-java/graphql-java-20.0/library/src/main/java/io/opentelemetry/instrumentation/graphql/v20_0/GraphQLTelemetryBuilder.java @@ -0,0 +1,65 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.graphql.v20_0; + +import com.google.errorprone.annotations.CanIgnoreReturnValue; +import io.opentelemetry.api.OpenTelemetry; + +/** A builder of {@link GraphQLTelemetry}. */ +@SuppressWarnings("AbbreviationAsWordInName") +public final class GraphQLTelemetryBuilder { + + private final OpenTelemetry openTelemetry; + + private boolean sanitizeQuery = true; + + private boolean dataFetcherInstrumentationEnabled = false; + + private boolean trivialDataFetcherInstrumentationEnabled = false; + + GraphQLTelemetryBuilder(OpenTelemetry openTelemetry) { + this.openTelemetry = openTelemetry; + } + + /** Sets whether sensitive information should be removed from queries. Default is {@code true}. */ + @CanIgnoreReturnValue + public GraphQLTelemetryBuilder setSanitizeQuery(boolean sanitizeQuery) { + this.sanitizeQuery = sanitizeQuery; + return this; + } + + /** Sets whether spans are created for GraphQL Data Fetchers. Default is {@code false}. */ + @CanIgnoreReturnValue + public GraphQLTelemetryBuilder setDataFetcherInstrumentationEnabled( + boolean dataFetcherInstrumentationEnabled) { + this.dataFetcherInstrumentationEnabled = dataFetcherInstrumentationEnabled; + return this; + } + + /** + * Sets whether spans are created for trivial GraphQL Data Fetchers. A trivial DataFetcher is one + * that simply maps data from an object to a field. Default is {@code false}. + */ + @CanIgnoreReturnValue + public GraphQLTelemetryBuilder setTrivialDataFetcherInstrumentationEnabled( + boolean trivialDataFetcherInstrumentationEnabled) { + this.trivialDataFetcherInstrumentationEnabled = trivialDataFetcherInstrumentationEnabled; + return this; + } + + /** + * Returns a new {@link GraphQLTelemetry} with the settings of this {@link + * GraphQLTelemetryBuilder}. + */ + public GraphQLTelemetry build() { + return new GraphQLTelemetry( + openTelemetry, + sanitizeQuery, + GraphqlInstrumenterFactory.createDataFetcherInstrumenter( + openTelemetry, dataFetcherInstrumentationEnabled), + trivialDataFetcherInstrumentationEnabled); + } +} diff --git a/instrumentation/graphql-java/graphql-java-20.0/library/src/main/java/io/opentelemetry/instrumentation/graphql/v20_0/Graphql20OpenTelemetryInstrumentationState.java b/instrumentation/graphql-java/graphql-java-20.0/library/src/main/java/io/opentelemetry/instrumentation/graphql/v20_0/Graphql20OpenTelemetryInstrumentationState.java new file mode 100644 index 000000000000..ccc2665128a6 --- /dev/null +++ b/instrumentation/graphql-java/graphql-java-20.0/library/src/main/java/io/opentelemetry/instrumentation/graphql/v20_0/Graphql20OpenTelemetryInstrumentationState.java @@ -0,0 +1,50 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.graphql.v20_0; + +import graphql.execution.ResultPath; +import io.opentelemetry.context.Context; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +final class Graphql20OpenTelemetryInstrumentationState + extends io.opentelemetry.instrumentation.graphql.internal.OpenTelemetryInstrumentationState { + private static final String ROOT_PATH = ResultPath.rootPath().toString(); + + private final ConcurrentMap contextStorage = new ConcurrentHashMap<>(); + + @Override + public Context getContext() { + return contextStorage.getOrDefault(ROOT_PATH, Context.current()); + } + + @Override + public void setContext(Context context) { + this.contextStorage.put(ROOT_PATH, context); + } + + public Context setContextForPath(ResultPath resultPath, Context context) { + return contextStorage.putIfAbsent(resultPath.toString(), context); + } + + public Context getParentContextForPath(ResultPath resultPath) { + + // Navigate up the path until we find the closest parent context + for (ResultPath currentPath = resultPath.getParent(); + currentPath != null; + currentPath = currentPath.getParent()) { + + Context parentContext = contextStorage.getOrDefault(currentPath.toString(), null); + + if (parentContext != null) { + return parentContext; + } + } + + // Fallback to returning the context for ROOT_PATH + return getContext(); + } +} diff --git a/instrumentation/graphql-java/graphql-java-20.0/library/src/main/java/io/opentelemetry/instrumentation/graphql/v20_0/GraphqlDataFetcherAttributesExtractor.java b/instrumentation/graphql-java/graphql-java-20.0/library/src/main/java/io/opentelemetry/instrumentation/graphql/v20_0/GraphqlDataFetcherAttributesExtractor.java new file mode 100644 index 000000000000..59cf72aa7c13 --- /dev/null +++ b/instrumentation/graphql-java/graphql-java-20.0/library/src/main/java/io/opentelemetry/instrumentation/graphql/v20_0/GraphqlDataFetcherAttributesExtractor.java @@ -0,0 +1,39 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.graphql.v20_0; + +import graphql.schema.DataFetchingEnvironment; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; +import javax.annotation.Nullable; + +final class GraphqlDataFetcherAttributesExtractor + implements AttributesExtractor { + + // NOTE: These are not part of the Semantic Convention and are subject to change + private static final AttributeKey GRAPHQL_FIELD_NAME = + AttributeKey.stringKey("graphql.field.name"); + private static final AttributeKey GRAPHQL_FIELD_PATH = + AttributeKey.stringKey("graphql.field.path"); + + @Override + public void onStart( + AttributesBuilder attributes, Context parentContext, DataFetchingEnvironment environment) { + attributes + .put(GRAPHQL_FIELD_NAME, environment.getExecutionStepInfo().getField().getName()) + .put(GRAPHQL_FIELD_PATH, environment.getExecutionStepInfo().getPath().toString()); + } + + @Override + public void onEnd( + AttributesBuilder attributes, + Context context, + DataFetchingEnvironment environment, + @Nullable Void unused, + @Nullable Throwable error) {} +} diff --git a/instrumentation/graphql-java/graphql-java-20.0/library/src/main/java/io/opentelemetry/instrumentation/graphql/v20_0/GraphqlInstrumenterFactory.java b/instrumentation/graphql-java/graphql-java-20.0/library/src/main/java/io/opentelemetry/instrumentation/graphql/v20_0/GraphqlInstrumenterFactory.java new file mode 100644 index 000000000000..d1391ecde059 --- /dev/null +++ b/instrumentation/graphql-java/graphql-java-20.0/library/src/main/java/io/opentelemetry/instrumentation/graphql/v20_0/GraphqlInstrumenterFactory.java @@ -0,0 +1,35 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.graphql.v20_0; + +import graphql.schema.DataFetchingEnvironment; +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import io.opentelemetry.instrumentation.graphql.internal.OpenTelemetryInstrumentationHelper; + +final class GraphqlInstrumenterFactory { + + private static final String INSTRUMENTATION_NAME = "io.opentelemetry.graphql-java-20.0"; + + static OpenTelemetryInstrumentationHelper createInstrumentationHelper( + OpenTelemetry openTelemetry, boolean sanitizeQuery) { + return OpenTelemetryInstrumentationHelper.create( + openTelemetry, INSTRUMENTATION_NAME, sanitizeQuery); + } + + static Instrumenter createDataFetcherInstrumenter( + OpenTelemetry openTelemetry, boolean enabled) { + return Instrumenter.builder( + openTelemetry, + INSTRUMENTATION_NAME, + environment -> environment.getExecutionStepInfo().getField().getName()) + .addAttributesExtractor(new GraphqlDataFetcherAttributesExtractor()) + .setEnabled(enabled) + .buildInstrumenter(); + } + + private GraphqlInstrumenterFactory() {} +} diff --git a/instrumentation/graphql-java/graphql-java-20.0/library/src/main/java/io/opentelemetry/instrumentation/graphql/v20_0/OpenTelemetryInstrumentation.java b/instrumentation/graphql-java/graphql-java-20.0/library/src/main/java/io/opentelemetry/instrumentation/graphql/v20_0/OpenTelemetryInstrumentation.java new file mode 100644 index 000000000000..03dc358f1f0c --- /dev/null +++ b/instrumentation/graphql-java/graphql-java-20.0/library/src/main/java/io/opentelemetry/instrumentation/graphql/v20_0/OpenTelemetryInstrumentation.java @@ -0,0 +1,111 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.graphql.v20_0; + +import static graphql.execution.instrumentation.InstrumentationState.ofState; + +import graphql.ExecutionResult; +import graphql.execution.ResultPath; +import graphql.execution.instrumentation.InstrumentationContext; +import graphql.execution.instrumentation.InstrumentationState; +import graphql.execution.instrumentation.SimplePerformantInstrumentation; +import graphql.execution.instrumentation.parameters.InstrumentationCreateStateParameters; +import graphql.execution.instrumentation.parameters.InstrumentationExecuteOperationParameters; +import graphql.execution.instrumentation.parameters.InstrumentationExecutionParameters; +import graphql.execution.instrumentation.parameters.InstrumentationFieldFetchParameters; +import graphql.schema.DataFetcher; +import graphql.schema.DataFetchingEnvironment; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import io.opentelemetry.instrumentation.graphql.internal.OpenTelemetryInstrumentationHelper; +import io.opentelemetry.instrumentation.graphql.internal.OpenTelemetryInstrumentationState; +import java.util.concurrent.CompletionStage; + +final class OpenTelemetryInstrumentation extends SimplePerformantInstrumentation { + private final OpenTelemetryInstrumentationHelper helper; + private final Instrumenter dataFetcherInstrumenter; + private final boolean createSpansForTrivialDataFetcher; + + OpenTelemetryInstrumentation( + OpenTelemetryInstrumentationHelper helper, + Instrumenter dataFetcherInstrumenter, + boolean createSpansForTrivialDataFetcher) { + this.helper = helper; + this.dataFetcherInstrumenter = dataFetcherInstrumenter; + this.createSpansForTrivialDataFetcher = createSpansForTrivialDataFetcher; + } + + @Override + public InstrumentationState createState(InstrumentationCreateStateParameters parameters) { + return new Graphql20OpenTelemetryInstrumentationState(); + } + + @Override + public InstrumentationContext beginExecution( + InstrumentationExecutionParameters parameters, InstrumentationState rawState) { + OpenTelemetryInstrumentationState state = ofState(rawState); + return helper.beginExecution(state); + } + + @Override + public InstrumentationContext beginExecuteOperation( + InstrumentationExecuteOperationParameters parameters, InstrumentationState rawState) { + OpenTelemetryInstrumentationState state = ofState(rawState); + return helper.beginExecuteOperation(parameters, state); + } + + @Override + public DataFetcher instrumentDataFetcher( + DataFetcher dataFetcher, + InstrumentationFieldFetchParameters parameters, + InstrumentationState rawState) { + + Graphql20OpenTelemetryInstrumentationState state = ofState(rawState); + + return environment -> { + ResultPath path = environment.getExecutionStepInfo().getPath(); + Context parentContext = state.getParentContextForPath(path); + + if (!dataFetcherInstrumenter.shouldStart(parentContext, environment) + || (parameters.isTrivialDataFetcher() && !createSpansForTrivialDataFetcher)) { + // Propagate context only, do not create span + try (Scope ignored = parentContext.makeCurrent()) { + return dataFetcher.get(environment); + } + } + + // Start span + Context childContext = dataFetcherInstrumenter.start(parentContext, environment); + state.setContextForPath(path, childContext); + + boolean isCompletionStage = false; + + try (Scope ignored = childContext.makeCurrent()) { + Object fieldValue = dataFetcher.get(environment); + + isCompletionStage = fieldValue instanceof CompletionStage; + + if (isCompletionStage) { + return ((CompletionStage) fieldValue) + .whenComplete( + (result, throwable) -> + dataFetcherInstrumenter.end(childContext, environment, null, throwable)); + } + + return fieldValue; + + } catch (Throwable throwable) { + dataFetcherInstrumenter.end(childContext, environment, null, throwable); + throw throwable; + } finally { + if (!isCompletionStage) { + dataFetcherInstrumenter.end(childContext, environment, null, null); + } + } + }; + } +} diff --git a/instrumentation/graphql-java/graphql-java-20.0/library/src/test/java/io/opentelemetry/instrumentation/graphql/v20_0/GraphqlTest.java b/instrumentation/graphql-java/graphql-java-20.0/library/src/test/java/io/opentelemetry/instrumentation/graphql/v20_0/GraphqlTest.java new file mode 100644 index 000000000000..0bfda9ef6688 --- /dev/null +++ b/instrumentation/graphql-java/graphql-java-20.0/library/src/test/java/io/opentelemetry/instrumentation/graphql/v20_0/GraphqlTest.java @@ -0,0 +1,244 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.graphql.v20_0; + +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; + +import graphql.ExecutionResult; +import graphql.GraphQL; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.graphql.AbstractGraphqlTest; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.LibraryInstrumentationExtension; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.semconv.incubating.GraphqlIncubatingAttributes; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +public class GraphqlTest extends AbstractGraphqlTest { + + @RegisterExtension + private static final InstrumentationExtension testing = LibraryInstrumentationExtension.create(); + + private static final AttributeKey GRAPHQL_FIELD_NAME = + AttributeKey.stringKey("graphql.field.name"); + + private static final AttributeKey GRAPHQL_FIELD_PATH = + AttributeKey.stringKey("graphql.field.path"); + + @Override + protected InstrumentationExtension getTesting() { + return testing; + } + + @Override + protected void configure(GraphQL.Builder builder) { + GraphQLTelemetry telemetry = + GraphQLTelemetry.builder(testing.getOpenTelemetry()) + .setDataFetcherInstrumentationEnabled(true) + .build(); + builder.instrumentation(telemetry.newInstrumentation()); + } + + @Override + protected boolean hasDataFetcherSpans() { + return true; + } + + @Test + void createSpansForDataFetchers() { + // Arrange + GraphQLTelemetry telemetry = + GraphQLTelemetry.builder(testing.getOpenTelemetry()) + .setDataFetcherInstrumentationEnabled(true) + .build(); + + GraphQL graphql = + GraphQL.newGraphQL(graphqlSchema).instrumentation(telemetry.newInstrumentation()).build(); + + // Act + ExecutionResult result = + graphql.execute( + "" + + " query findBookById {\n" + + " bookById(id: \"book-1\") {\n" + + " name\n" + + " author {\n" + + " name\n" + + " }\n" + + " }\n" + + " }"); + + // Assert + assertThat(result.getErrors()).isEmpty(); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("query findBookById") + .hasKind(SpanKind.INTERNAL) + .hasNoParent() + .hasAttributesSatisfyingExactly( + equalTo( + GraphqlIncubatingAttributes.GRAPHQL_OPERATION_NAME, "findBookById"), + equalTo(GraphqlIncubatingAttributes.GRAPHQL_OPERATION_TYPE, "query"), + normalizedQueryEqualsTo( + GraphqlIncubatingAttributes.GRAPHQL_DOCUMENT, + "query findBookById { bookById(id: ?) { name author { name } } }")), + span -> + span.hasName("bookById") + .hasKind(SpanKind.INTERNAL) + .hasParent(spanWithName("query findBookById")) + .hasAttributesSatisfyingExactly( + equalTo(GRAPHQL_FIELD_NAME, "bookById"), + equalTo(GRAPHQL_FIELD_PATH, "/bookById")), + span -> + span.hasName("fetchBookById") + .hasKind(SpanKind.INTERNAL) + .hasParent(spanWithName("bookById")), + span -> + span.hasName("author") + .hasKind(SpanKind.INTERNAL) + .hasParent(spanWithName("bookById")) + .hasAttributesSatisfyingExactly( + equalTo(GRAPHQL_FIELD_NAME, "author"), + equalTo(GRAPHQL_FIELD_PATH, "/bookById/author")))); + } + + @Test + void createSpanForTrivialDataFetchers() { + // Arrange + GraphQLTelemetry telemetry = + GraphQLTelemetry.builder(testing.getOpenTelemetry()) + .setDataFetcherInstrumentationEnabled(true) + .setTrivialDataFetcherInstrumentationEnabled(true) + .build(); + + GraphQL graphql = + GraphQL.newGraphQL(graphqlSchema).instrumentation(telemetry.newInstrumentation()).build(); + + // Act + ExecutionResult result = + graphql.execute( + "" + + " query findBookById {\n" + + " bookById(id: \"book-1\") {\n" + + " name\n" + + " author {\n" + + " name\n" + + " }\n" + + " }\n" + + " }"); + + // Assert + assertThat(result.getErrors()).isEmpty(); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("query findBookById") + .hasKind(SpanKind.INTERNAL) + .hasNoParent() + .hasAttributesSatisfyingExactly( + equalTo( + GraphqlIncubatingAttributes.GRAPHQL_OPERATION_NAME, "findBookById"), + equalTo(GraphqlIncubatingAttributes.GRAPHQL_OPERATION_TYPE, "query"), + normalizedQueryEqualsTo( + GraphqlIncubatingAttributes.GRAPHQL_DOCUMENT, + "query findBookById { bookById(id: ?) { name author { name } } }")), + span -> + span.hasName("bookById") + .hasKind(SpanKind.INTERNAL) + .hasParent(spanWithName("query findBookById")) + .hasAttributesSatisfyingExactly( + equalTo(GRAPHQL_FIELD_NAME, "bookById"), + equalTo(GRAPHQL_FIELD_PATH, "/bookById")), + span -> + span.hasName("fetchBookById") + .hasKind(SpanKind.INTERNAL) + .hasParent(spanWithName("bookById")), + span -> + span.hasName("name") + .hasKind(SpanKind.INTERNAL) + .hasParent(spanWithName("bookById")) + .hasAttributesSatisfyingExactly( + equalTo(GRAPHQL_FIELD_NAME, "name"), + equalTo(GRAPHQL_FIELD_PATH, "/bookById/name")), + span -> + span.hasName("author") + .hasKind(SpanKind.INTERNAL) + .hasParent(spanWithName("bookById")) + .hasAttributesSatisfyingExactly( + equalTo(GRAPHQL_FIELD_NAME, "author"), + equalTo(GRAPHQL_FIELD_PATH, "/bookById/author")), + span -> + span.hasName("name") + .hasKind(SpanKind.INTERNAL) + .hasParent(spanWithName("author")) + .hasAttributesSatisfyingExactly( + equalTo(GRAPHQL_FIELD_NAME, "name"), + equalTo(GRAPHQL_FIELD_PATH, "/bookById/author/name")))); + } + + @Test + void noDataFetcherSpansCreated() { + // Arrange + GraphQLTelemetry telemetry = + GraphQLTelemetry.builder(testing.getOpenTelemetry()) + .setDataFetcherInstrumentationEnabled(false) + .setTrivialDataFetcherInstrumentationEnabled(true) + .build(); + + GraphQL graphql = + GraphQL.newGraphQL(graphqlSchema).instrumentation(telemetry.newInstrumentation()).build(); + + // Act + ExecutionResult result = + graphql.execute( + "" + + " query findBookById {\n" + + " bookById(id: \"book-1\") {\n" + + " name\n" + + " author {\n" + + " name\n" + + " }\n" + + " }\n" + + " }"); + + // Assert + assertThat(result.getErrors()).isEmpty(); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("query findBookById") + .hasKind(SpanKind.INTERNAL) + .hasNoParent() + .hasAttributesSatisfyingExactly( + equalTo( + GraphqlIncubatingAttributes.GRAPHQL_OPERATION_NAME, "findBookById"), + equalTo(GraphqlIncubatingAttributes.GRAPHQL_OPERATION_TYPE, "query"), + normalizedQueryEqualsTo( + GraphqlIncubatingAttributes.GRAPHQL_DOCUMENT, + "query findBookById { bookById(id: ?) { name author { name } } }")), + span -> + span.hasName("fetchBookById") + .hasKind(SpanKind.INTERNAL) + .hasParent(spanWithName("query findBookById")))); + } + + private static SpanData spanWithName(String name) { + return testing.spans().stream() + .filter(span -> span.getName().equals(name)) + .findFirst() + .orElse(null); + } +} diff --git a/instrumentation/graphql-java/graphql-java-20.0/library/src/test/java/io/opentelemetry/instrumentation/graphql/v20_0/OpenTelemetryInstrumentationStateTest.java b/instrumentation/graphql-java/graphql-java-20.0/library/src/test/java/io/opentelemetry/instrumentation/graphql/v20_0/OpenTelemetryInstrumentationStateTest.java new file mode 100644 index 000000000000..5de821a94bae --- /dev/null +++ b/instrumentation/graphql-java/graphql-java-20.0/library/src/test/java/io/opentelemetry/instrumentation/graphql/v20_0/OpenTelemetryInstrumentationStateTest.java @@ -0,0 +1,155 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.graphql.v20_0; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import graphql.execution.ResultPath; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.ContextKey; +import io.opentelemetry.context.Scope; +import org.junit.jupiter.api.Test; + +class OpenTelemetryInstrumentationStateTest { + + @Test + void setContextSetsContextForRootPath() { + // Arrange + Graphql20OpenTelemetryInstrumentationState state = + new Graphql20OpenTelemetryInstrumentationState(); + Context context = Context.root().with(ContextKey.named("New"), "Context"); + + // Act + state.setContext(context); + + // Assert + assertEquals(context, state.getParentContextForPath(ResultPath.rootPath())); + } + + @Test + void getContextGetsContextForRootPath() { + // Arrange + Graphql20OpenTelemetryInstrumentationState state = + new Graphql20OpenTelemetryInstrumentationState(); + Context context = Context.root().with(ContextKey.named("New"), "Context"); + + // Act + state.setContextForPath(ResultPath.rootPath(), context); + + // Assert + assertEquals(context, state.getContext()); + } + + @Test + void getContextReturnsCurrentContext() { + // Arrange + Graphql20OpenTelemetryInstrumentationState state = + new Graphql20OpenTelemetryInstrumentationState(); + Context context = Context.root().with(ContextKey.named("New"), "Context"); + + Context result; + + // Act + try (Scope ignored = context.makeCurrent()) { + result = state.getContext(); + } + + // Assert + assertEquals(context, result); + } + + @Test + void getParentContextForPathReturnsCurrentContextForRootPath() { + // Arrange + Graphql20OpenTelemetryInstrumentationState state = + new Graphql20OpenTelemetryInstrumentationState(); + Context context = Context.root().with(ContextKey.named("New"), "Context"); + + Context result; + + // Act and Assert + try (Scope ignored = context.makeCurrent()) { + result = state.getParentContextForPath(ResultPath.rootPath()); + } + + // Assert + assertEquals(context, result); + } + + @Test + void getParentContextForPathReturnsRootPathContextForRootPath() { + // Arrange + Graphql20OpenTelemetryInstrumentationState state = + new Graphql20OpenTelemetryInstrumentationState(); + Context context = Context.root().with(ContextKey.named("New"), "Context"); + + state.setContextForPath(ResultPath.rootPath(), context); + + // Act + Context result = state.getParentContextForPath(ResultPath.rootPath()); + + // Assert + assertEquals(context, result); + } + + @Test + void getParentContextForPathReturnsCurrentContextForDeepPath() { + // Arrange + Graphql20OpenTelemetryInstrumentationState state = + new Graphql20OpenTelemetryInstrumentationState(); + Context context = Context.root().with(ContextKey.named("New"), "Context"); + + ResultPath resultPath = ResultPath.parse("/segment1/segment2/segment3"); + + Context result; + + // Act and Assert + try (Scope ignored = context.makeCurrent()) { + result = state.getParentContextForPath(resultPath); + } + + // Assert + assertEquals(context, result); + } + + @Test + void getParentContextForPathReturnsRootPathContextForDeepPath() { + // Arrange + Graphql20OpenTelemetryInstrumentationState state = + new Graphql20OpenTelemetryInstrumentationState(); + Context context = Context.root().with(ContextKey.named("New"), "Context"); + + state.setContextForPath(ResultPath.rootPath(), context); + + ResultPath resultPath = ResultPath.parse("/segment1/segment2/segment3"); + + // Act + Context result = state.getParentContextForPath(resultPath); + + // Assert + assertEquals(context, result); + } + + @Test + void getParentContextForPathReturnsParentContextForDeepPath() { + // Arrange + Graphql20OpenTelemetryInstrumentationState state = + new Graphql20OpenTelemetryInstrumentationState(); + Context rootPathContext = Context.root().with(ContextKey.named("Name"), "RootPath"); + Context childContext = Context.root().with(ContextKey.named("Name"), "Child"); + + state.setContextForPath(ResultPath.rootPath(), rootPathContext); + state.setContextForPath(ResultPath.parse("/segment1/segment2"), childContext); + + // Act + Context result = + state.getParentContextForPath( + ResultPath.parse("/segment1/segment2/segment3/segment4/segment5")); + + // Assert + assertEquals(childContext, result); + } +} diff --git a/instrumentation/graphql-java-12.0/library/build.gradle.kts b/instrumentation/graphql-java/graphql-java-common/library/build.gradle.kts similarity index 59% rename from instrumentation/graphql-java-12.0/library/build.gradle.kts rename to instrumentation/graphql-java/graphql-java-common/library/build.gradle.kts index 5674a1bdb25b..9e5a30f72f68 100644 --- a/instrumentation/graphql-java-12.0/library/build.gradle.kts +++ b/instrumentation/graphql-java/graphql-java-common/library/build.gradle.kts @@ -4,6 +4,4 @@ plugins { dependencies { library("com.graphql-java:graphql-java:12.0") - - testImplementation(project(":instrumentation:graphql-java-12.0:testing")) } diff --git a/instrumentation/graphql-java-12.0/library/src/main/java/io/opentelemetry/instrumentation/graphql/v12_0/GraphqlAttributesExtractor.java b/instrumentation/graphql-java/graphql-java-common/library/src/main/java/io/opentelemetry/instrumentation/graphql/internal/GraphqlAttributesExtractor.java similarity index 50% rename from instrumentation/graphql-java-12.0/library/src/main/java/io/opentelemetry/instrumentation/graphql/v12_0/GraphqlAttributesExtractor.java rename to instrumentation/graphql-java/graphql-java-common/library/src/main/java/io/opentelemetry/instrumentation/graphql/internal/GraphqlAttributesExtractor.java index 283850d62bdc..b8cd9737c422 100644 --- a/instrumentation/graphql-java-12.0/library/src/main/java/io/opentelemetry/instrumentation/graphql/v12_0/GraphqlAttributesExtractor.java +++ b/instrumentation/graphql-java/graphql-java-common/library/src/main/java/io/opentelemetry/instrumentation/graphql/internal/GraphqlAttributesExtractor.java @@ -3,10 +3,9 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.instrumentation.graphql.v12_0; +package io.opentelemetry.instrumentation.graphql.internal; import graphql.ExecutionResult; -import graphql.execution.instrumentation.parameters.InstrumentationExecutionParameters; import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.common.AttributesBuilder; import io.opentelemetry.context.Context; @@ -14,34 +13,38 @@ import java.util.Locale; import javax.annotation.Nullable; +/** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time. + */ final class GraphqlAttributesExtractor - implements AttributesExtractor { - // https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/instrumentation/graphql.md - private static final AttributeKey OPERATION_NAME = - AttributeKey.stringKey("graphql.operation.name"); - private static final AttributeKey OPERATION_TYPE = - AttributeKey.stringKey("graphql.operation.type"); + implements AttributesExtractor { + // copied from GraphqlIncubatingAttributes private static final AttributeKey GRAPHQL_DOCUMENT = AttributeKey.stringKey("graphql.document"); + private static final AttributeKey GRAPHQL_OPERATION_NAME = + AttributeKey.stringKey("graphql.operation.name"); + private static final AttributeKey GRAPHQL_OPERATION_TYPE = + AttributeKey.stringKey("graphql.operation.type"); @Override public void onStart( AttributesBuilder attributes, Context parentContext, - InstrumentationExecutionParameters request) {} + OpenTelemetryInstrumentationState request) {} @Override public void onEnd( AttributesBuilder attributes, Context context, - InstrumentationExecutionParameters request, + OpenTelemetryInstrumentationState request, @Nullable ExecutionResult response, @Nullable Throwable error) { - OpenTelemetryInstrumentationState state = request.getInstrumentationState(); - attributes.put(OPERATION_NAME, state.getOperationName()); - if (state.getOperation() != null) { - attributes.put(OPERATION_TYPE, state.getOperation().name().toLowerCase(Locale.ROOT)); + attributes.put(GRAPHQL_OPERATION_NAME, request.getOperationName()); + if (request.getOperation() != null) { + attributes.put( + GRAPHQL_OPERATION_TYPE, request.getOperation().name().toLowerCase(Locale.ROOT)); } - attributes.put(GRAPHQL_DOCUMENT, state.getQuery()); + attributes.put(GRAPHQL_DOCUMENT, request.getQuery()); } } diff --git a/instrumentation/graphql-java-12.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/graphql/v12_0/GraphqlSingletons.java b/instrumentation/graphql-java/graphql-java-common/library/src/main/java/io/opentelemetry/instrumentation/graphql/internal/InstrumentationUtil.java similarity index 54% rename from instrumentation/graphql-java-12.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/graphql/v12_0/GraphqlSingletons.java rename to instrumentation/graphql-java/graphql-java-common/library/src/main/java/io/opentelemetry/instrumentation/graphql/internal/InstrumentationUtil.java index 74beb880b454..572b166d2969 100644 --- a/instrumentation/graphql-java-12.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/graphql/v12_0/GraphqlSingletons.java +++ b/instrumentation/graphql-java/graphql-java-common/library/src/main/java/io/opentelemetry/instrumentation/graphql/internal/InstrumentationUtil.java @@ -3,31 +3,21 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.javaagent.instrumentation.graphql.v12_0; +package io.opentelemetry.instrumentation.graphql.internal; import graphql.execution.instrumentation.ChainedInstrumentation; import graphql.execution.instrumentation.Instrumentation; -import io.opentelemetry.api.GlobalOpenTelemetry; -import io.opentelemetry.instrumentation.graphql.v12_0.GraphQLTelemetry; -import io.opentelemetry.javaagent.bootstrap.internal.InstrumentationConfig; import java.util.ArrayList; import java.util.List; -public final class GraphqlSingletons { - - private static final boolean QUERY_SANITIZATION_ENABLED = - InstrumentationConfig.get() - .getBoolean("otel.instrumentation.graphql.query-sanitizer.enabled", true); - - private static final GraphQLTelemetry TELEMETRY = - GraphQLTelemetry.builder(GlobalOpenTelemetry.get()) - .setSanitizeQuery(QUERY_SANITIZATION_ENABLED) - .build(); - - private GraphqlSingletons() {} +/** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time. + */ +public final class InstrumentationUtil { - public static Instrumentation addInstrumentation(Instrumentation instrumentation) { - Instrumentation ourInstrumentation = TELEMETRY.newInstrumentation(); + public static Instrumentation addInstrumentation( + Instrumentation instrumentation, Instrumentation ourInstrumentation) { if (instrumentation == null) { return ourInstrumentation; } @@ -47,4 +37,6 @@ public static Instrumentation addInstrumentation(Instrumentation instrumentation } return new ChainedInstrumentation(instrumentationList); } + + private InstrumentationUtil() {} } diff --git a/instrumentation/graphql-java-12.0/library/src/main/java/io/opentelemetry/instrumentation/graphql/v12_0/OpenTelemetryInstrumentation.java b/instrumentation/graphql-java/graphql-java-common/library/src/main/java/io/opentelemetry/instrumentation/graphql/internal/OpenTelemetryInstrumentationHelper.java similarity index 61% rename from instrumentation/graphql-java-12.0/library/src/main/java/io/opentelemetry/instrumentation/graphql/v12_0/OpenTelemetryInstrumentation.java rename to instrumentation/graphql-java/graphql-java-common/library/src/main/java/io/opentelemetry/instrumentation/graphql/internal/OpenTelemetryInstrumentationHelper.java index f8d6fd3941d5..ab8538450c2e 100644 --- a/instrumentation/graphql-java-12.0/library/src/main/java/io/opentelemetry/instrumentation/graphql/v12_0/OpenTelemetryInstrumentation.java +++ b/instrumentation/graphql-java/graphql-java-common/library/src/main/java/io/opentelemetry/instrumentation/graphql/internal/OpenTelemetryInstrumentationHelper.java @@ -3,17 +3,13 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.instrumentation.graphql.v12_0; +package io.opentelemetry.instrumentation.graphql.internal; import graphql.ExecutionResult; import graphql.GraphQLError; import graphql.execution.instrumentation.InstrumentationContext; -import graphql.execution.instrumentation.InstrumentationState; -import graphql.execution.instrumentation.SimpleInstrumentation; import graphql.execution.instrumentation.SimpleInstrumentationContext; import graphql.execution.instrumentation.parameters.InstrumentationExecuteOperationParameters; -import graphql.execution.instrumentation.parameters.InstrumentationExecutionParameters; -import graphql.execution.instrumentation.parameters.InstrumentationFieldFetchParameters; import graphql.language.AstPrinter; import graphql.language.AstTransformer; import graphql.language.BooleanValue; @@ -23,52 +19,75 @@ import graphql.language.NodeVisitorStub; import graphql.language.NullValue; import graphql.language.OperationDefinition; -import graphql.language.OperationDefinition.Operation; import graphql.language.Value; import graphql.language.VariableReference; import graphql.schema.DataFetcher; import graphql.util.TraversalControl; import graphql.util.TraverserContext; import graphql.util.TreeTransformerUtil; +import io.opentelemetry.api.OpenTelemetry; 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.instrumentation.api.instrumenter.Instrumenter; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.opentelemetry.instrumentation.api.instrumenter.InstrumenterBuilder; +import io.opentelemetry.instrumentation.api.instrumenter.SpanStatusExtractor; +import io.opentelemetry.semconv.ExceptionAttributes; import java.util.Locale; -final class OpenTelemetryInstrumentation extends SimpleInstrumentation { +/** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time. + */ +public final class OpenTelemetryInstrumentationHelper { private static final NodeVisitor sanitizingVisitor = new SanitizingVisitor(); private static final AstTransformer astTransformer = new AstTransformer(); - private final Instrumenter instrumenter; + private final Instrumenter instrumenter; private final boolean sanitizeQuery; - OpenTelemetryInstrumentation( - Instrumenter instrumenter, + private OpenTelemetryInstrumentationHelper( + Instrumenter instrumenter, boolean sanitizeQuery) { this.instrumenter = instrumenter; this.sanitizeQuery = sanitizeQuery; } - @Override - public InstrumentationState createState() { - return new OpenTelemetryInstrumentationState(); + public static OpenTelemetryInstrumentationHelper create( + OpenTelemetry openTelemetry, String instrumentationName, boolean sanitizeQuery) { + InstrumenterBuilder builder = + Instrumenter.builder( + openTelemetry, instrumentationName, ignored -> "GraphQL Operation") + .setSpanStatusExtractor( + (spanStatusBuilder, instrumentationExecutionParameters, executionResult, error) -> { + if (!executionResult.getErrors().isEmpty()) { + spanStatusBuilder.setStatus(StatusCode.ERROR); + } else { + SpanStatusExtractor.getDefault() + .extract( + spanStatusBuilder, + instrumentationExecutionParameters, + executionResult, + error); + } + }); + builder.addAttributesExtractor(new GraphqlAttributesExtractor()); + + return new OpenTelemetryInstrumentationHelper(builder.buildInstrumenter(), sanitizeQuery); } - @Override public InstrumentationContext beginExecution( - InstrumentationExecutionParameters parameters) { + OpenTelemetryInstrumentationState state) { Context parentContext = Context.current(); - if (!instrumenter.shouldStart(parentContext, parameters)) { + if (!instrumenter.shouldStart(parentContext, state)) { return SimpleInstrumentationContext.noOp(); } - Context context = instrumenter.start(parentContext, parameters); - OpenTelemetryInstrumentationState state = parameters.getInstrumentationState(); + Context context = instrumenter.start(parentContext, state); state.setContext(context); return SimpleInstrumentationContext.whenCompleted( @@ -76,26 +95,25 @@ public InstrumentationContext beginExecution( Span span = Span.fromContext(context); for (GraphQLError error : result.getErrors()) { AttributesBuilder attributes = Attributes.builder(); - attributes.put(SemanticAttributes.EXCEPTION_TYPE, String.valueOf(error.getErrorType())); - attributes.put(SemanticAttributes.EXCEPTION_MESSAGE, error.getMessage()); + attributes.put( + ExceptionAttributes.EXCEPTION_TYPE, String.valueOf(error.getErrorType())); + attributes.put(ExceptionAttributes.EXCEPTION_MESSAGE, error.getMessage()); - span.addEvent(SemanticAttributes.EXCEPTION_EVENT_NAME, attributes.build()); + span.addEvent("exception", attributes.build()); } - instrumenter.end(context, parameters, result, throwable); + instrumenter.end(context, state, result, throwable); }); } - @Override public InstrumentationContext beginExecuteOperation( - InstrumentationExecuteOperationParameters parameters) { - - OpenTelemetryInstrumentationState state = parameters.getInstrumentationState(); + InstrumentationExecuteOperationParameters parameters, + OpenTelemetryInstrumentationState state) { Span span = Span.fromContext(state.getContext()); OperationDefinition operationDefinition = parameters.getExecutionContext().getOperationDefinition(); - Operation operation = operationDefinition.getOperation(); + OperationDefinition.Operation operation = operationDefinition.getOperation(); String operationType = operation.name().toLowerCase(Locale.ROOT); String operationName = operationDefinition.getName(); @@ -117,10 +135,8 @@ public InstrumentationContext beginExecuteOperation( return SimpleInstrumentationContext.noOp(); } - @Override public DataFetcher instrumentDataFetcher( - DataFetcher dataFetcher, InstrumentationFieldFetchParameters parameters) { - OpenTelemetryInstrumentationState state = parameters.getInstrumentationState(); + DataFetcher dataFetcher, OpenTelemetryInstrumentationState state) { Context context = state.getContext(); return (DataFetcher) diff --git a/instrumentation/graphql-java-12.0/library/src/main/java/io/opentelemetry/instrumentation/graphql/v12_0/OpenTelemetryInstrumentationState.java b/instrumentation/graphql-java/graphql-java-common/library/src/main/java/io/opentelemetry/instrumentation/graphql/internal/OpenTelemetryInstrumentationState.java similarity index 51% rename from instrumentation/graphql-java-12.0/library/src/main/java/io/opentelemetry/instrumentation/graphql/v12_0/OpenTelemetryInstrumentationState.java rename to instrumentation/graphql-java/graphql-java-common/library/src/main/java/io/opentelemetry/instrumentation/graphql/internal/OpenTelemetryInstrumentationState.java index 10f453e7ba79..4467969cb575 100644 --- a/instrumentation/graphql-java-12.0/library/src/main/java/io/opentelemetry/instrumentation/graphql/v12_0/OpenTelemetryInstrumentationState.java +++ b/instrumentation/graphql-java/graphql-java-common/library/src/main/java/io/opentelemetry/instrumentation/graphql/internal/OpenTelemetryInstrumentationState.java @@ -3,47 +3,51 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.instrumentation.graphql.v12_0; +package io.opentelemetry.instrumentation.graphql.internal; import graphql.execution.instrumentation.InstrumentationState; import graphql.language.OperationDefinition.Operation; import io.opentelemetry.context.Context; -final class OpenTelemetryInstrumentationState implements InstrumentationState { +/** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time. + */ +public class OpenTelemetryInstrumentationState implements InstrumentationState { private Context context; private Operation operation; private String operationName; private String query; - Context getContext() { + public Context getContext() { return context; } - void setContext(Context context) { + public void setContext(Context context) { this.context = context; } - Operation getOperation() { + public Operation getOperation() { return operation; } - void setOperation(Operation operation) { + public void setOperation(Operation operation) { this.operation = operation; } - String getOperationName() { + public String getOperationName() { return operationName; } - void setOperationName(String operationName) { + public void setOperationName(String operationName) { this.operationName = operationName; } - String getQuery() { + public String getQuery() { return query; } - void setQuery(String query) { + public void setQuery(String query) { this.query = query; } } diff --git a/instrumentation/graphql-java-12.0/testing/build.gradle.kts b/instrumentation/graphql-java/graphql-java-common/testing/build.gradle.kts similarity index 100% rename from instrumentation/graphql-java-12.0/testing/build.gradle.kts rename to instrumentation/graphql-java/graphql-java-common/testing/build.gradle.kts diff --git a/instrumentation/graphql-java-12.0/testing/src/main/java/io/opentelemetry/instrumentation/graphql/AbstractGraphqlTest.java b/instrumentation/graphql-java/graphql-java-common/testing/src/main/java/io/opentelemetry/instrumentation/graphql/AbstractGraphqlTest.java similarity index 68% rename from instrumentation/graphql-java-12.0/testing/src/main/java/io/opentelemetry/instrumentation/graphql/AbstractGraphqlTest.java rename to instrumentation/graphql-java/graphql-java-common/testing/src/main/java/io/opentelemetry/instrumentation/graphql/AbstractGraphqlTest.java index ee6933e5c25b..fc40a585170b 100644 --- a/instrumentation/graphql-java-12.0/testing/src/main/java/io/opentelemetry/instrumentation/graphql/AbstractGraphqlTest.java +++ b/instrumentation/graphql-java/graphql-java-common/testing/src/main/java/io/opentelemetry/instrumentation/graphql/AbstractGraphqlTest.java @@ -23,8 +23,10 @@ import io.opentelemetry.api.trace.SpanKind; import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; import io.opentelemetry.sdk.testing.assertj.AttributeAssertion; +import io.opentelemetry.sdk.testing.assertj.SpanDataAssert; import io.opentelemetry.sdk.trace.data.StatusData; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.opentelemetry.semconv.ExceptionAttributes; +import io.opentelemetry.semconv.incubating.GraphqlIncubatingAttributes; import java.io.IOException; import java.io.InputStreamReader; import java.io.Reader; @@ -33,6 +35,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.function.Consumer; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; @@ -43,12 +46,17 @@ public abstract class AbstractGraphqlTest { private final List> books = new ArrayList<>(); private final List> authors = new ArrayList<>(); - private GraphQL graphql; + protected GraphQL graphql; + protected GraphQLSchema graphqlSchema; protected abstract InstrumentationExtension getTesting(); protected abstract void configure(GraphQL.Builder builder); + protected boolean hasDataFetcherSpans() { + return false; + } + @BeforeAll void setup() throws IOException { addAuthor("author-1", "John"); @@ -62,7 +70,7 @@ void setup() throws IOException { new InputStreamReader( this.getClass().getClassLoader().getResourceAsStream("schema.graphqls"), StandardCharsets.UTF_8)) { - GraphQLSchema graphqlSchema = buildSchema(reader); + graphqlSchema = buildSchema(reader); GraphQL.Builder graphqlBuilder = GraphQL.newGraphQL(graphqlSchema); configure(graphqlBuilder); this.graphql = graphqlBuilder.build(); @@ -138,21 +146,37 @@ void successfulQuery() { getTesting() .waitAndAssertTraces( - trace -> - trace.hasSpansSatisfyingExactly( + trace -> { + List> assertions = new ArrayList<>(); + assertions.add( + span -> + span.hasName("query findBookById") + .hasKind(SpanKind.INTERNAL) + .hasNoParent() + .hasAttributesSatisfyingExactly( + equalTo( + GraphqlIncubatingAttributes.GRAPHQL_OPERATION_NAME, + "findBookById"), + equalTo(GraphqlIncubatingAttributes.GRAPHQL_OPERATION_TYPE, "query"), + normalizedQueryEqualsTo( + GraphqlIncubatingAttributes.GRAPHQL_DOCUMENT, + "query findBookById { bookById(id: ?) { name } }"))); + if (hasDataFetcherSpans()) { + assertions.add( span -> - span.hasName("query findBookById") - .hasKind(SpanKind.INTERNAL) - .hasNoParent() + span.hasName("bookById") + .hasParent(trace.getSpan(0)) .hasAttributesSatisfyingExactly( - equalTo( - AttributeKey.stringKey("graphql.operation.name"), - "findBookById"), - equalTo(AttributeKey.stringKey("graphql.operation.type"), "query"), - normalizedQueryEqualsTo( - AttributeKey.stringKey("graphql.document"), - "query findBookById { bookById(id: ?) { name } }")), - span -> span.hasName("fetchBookById").hasParent(trace.getSpan(0)))); + equalTo(AttributeKey.stringKey("graphql.field.path"), "/bookById"), + equalTo(AttributeKey.stringKey("graphql.field.name"), "bookById"))); + } + assertions.add( + span -> + span.hasName("fetchBookById") + .hasParent(trace.getSpan(hasDataFetcherSpans() ? 1 : 0))); + + trace.hasSpansSatisfyingExactly(assertions); + }); } @Test @@ -170,18 +194,33 @@ void successfulQueryWithoutName() { getTesting() .waitAndAssertTraces( - trace -> - trace.hasSpansSatisfyingExactly( + trace -> { + List> assertions = new ArrayList<>(); + assertions.add( + span -> + span.hasName("query") + .hasKind(SpanKind.INTERNAL) + .hasNoParent() + .hasAttributesSatisfyingExactly( + equalTo(AttributeKey.stringKey("graphql.operation.type"), "query"), + normalizedQueryEqualsTo( + AttributeKey.stringKey("graphql.document"), + "{ bookById(id: ?) { name } }"))); + if (hasDataFetcherSpans()) { + assertions.add( span -> - span.hasName("query") - .hasKind(SpanKind.INTERNAL) - .hasNoParent() + span.hasName("bookById") + .hasParent(trace.getSpan(0)) .hasAttributesSatisfyingExactly( - equalTo(AttributeKey.stringKey("graphql.operation.type"), "query"), - normalizedQueryEqualsTo( - AttributeKey.stringKey("graphql.document"), - "query { bookById(id: ?) { name } }")), - span -> span.hasName("fetchBookById").hasParent(trace.getSpan(0)))); + equalTo(AttributeKey.stringKey("graphql.field.path"), "/bookById"), + equalTo(AttributeKey.stringKey("graphql.field.name"), "bookById"))); + } + assertions.add( + span -> + span.hasName("fetchBookById") + .hasParent(trace.getSpan(hasDataFetcherSpans() ? 1 : 0))); + trace.hasSpansSatisfyingExactly(assertions); + }); } @Test @@ -204,14 +243,16 @@ void parseError() { .hasEventsSatisfyingExactly( event -> event - .hasName(SemanticAttributes.EXCEPTION_EVENT_NAME) + .hasName("exception") .hasAttributesSatisfyingExactly( equalTo( - SemanticAttributes.EXCEPTION_TYPE, "InvalidSyntax"), + ExceptionAttributes.EXCEPTION_TYPE, + "InvalidSyntax"), satisfies( - SemanticAttributes.EXCEPTION_MESSAGE, + ExceptionAttributes.EXCEPTION_MESSAGE, message -> - message.startsWith("Invalid Syntax")))))); + message.startsWithIgnoringCase( + "Invalid Syntax")))))); } @Test @@ -244,16 +285,15 @@ void validationError() { .hasEventsSatisfyingExactly( event -> event - .hasName(SemanticAttributes.EXCEPTION_EVENT_NAME) + .hasName("exception") .hasAttributesSatisfyingExactly( equalTo( - SemanticAttributes.EXCEPTION_TYPE, + ExceptionAttributes.EXCEPTION_TYPE, "ValidationError"), satisfies( - SemanticAttributes.EXCEPTION_MESSAGE, + ExceptionAttributes.EXCEPTION_MESSAGE, message -> - message.startsWith( - "Validation error of type FieldUndefined")))))); + message.startsWith("Validation error")))))); } @Test @@ -279,15 +319,16 @@ void successfulMutation() { .hasNoParent() .hasAttributesSatisfyingExactly( equalTo( - AttributeKey.stringKey("graphql.operation.name"), "addNewBook"), + GraphqlIncubatingAttributes.GRAPHQL_OPERATION_NAME, + "addNewBook"), equalTo( - AttributeKey.stringKey("graphql.operation.type"), "mutation"), + GraphqlIncubatingAttributes.GRAPHQL_OPERATION_TYPE, "mutation"), normalizedQueryEqualsTo( - AttributeKey.stringKey("graphql.document"), + GraphqlIncubatingAttributes.GRAPHQL_DOCUMENT, "mutation addNewBook { addBook(id: ?, name: ?, author: ?) { id } }")))); } - private static AttributeAssertion normalizedQueryEqualsTo( + protected static AttributeAssertion normalizedQueryEqualsTo( AttributeKey key, String value) { return satisfies( key, @@ -295,6 +336,9 @@ private static AttributeAssertion normalizedQueryEqualsTo( stringAssert.satisfies( querySource -> { String normalized = querySource.replaceAll("(?s)\\s+", " "); + if (normalized.startsWith("query {")) { + normalized = normalized.substring("query ".length()); + } assertThat(normalized).isEqualTo(value); })); } diff --git a/instrumentation/graphql-java-12.0/testing/src/main/resources/schema.graphqls b/instrumentation/graphql-java/graphql-java-common/testing/src/main/resources/schema.graphqls similarity index 100% rename from instrumentation/graphql-java-12.0/testing/src/main/resources/schema.graphqls rename to instrumentation/graphql-java/graphql-java-common/testing/src/main/resources/schema.graphqls diff --git a/instrumentation/grizzly-2.3/javaagent/build.gradle.kts b/instrumentation/grizzly-2.3/javaagent/build.gradle.kts index b7fa010ebf6b..a025c78e44fe 100644 --- a/instrumentation/grizzly-2.3/javaagent/build.gradle.kts +++ b/instrumentation/grizzly-2.3/javaagent/build.gradle.kts @@ -19,12 +19,12 @@ dependencies { testLibrary("org.glassfish.grizzly:grizzly-http-server:2.3") } -tasks.withType().configureEach { - jvmArgs("-Dotel.instrumentation.grizzly.enabled=true") - - // required on jdk17 - jvmArgs("--add-opens=java.base/java.lang=ALL-UNNAMED") - jvmArgs("-XX:+IgnoreUnrecognizedVMOptions") +tasks { + withType().configureEach { + // required on jdk17 + jvmArgs("--add-opens=java.base/java.lang=ALL-UNNAMED") + jvmArgs("-XX:+IgnoreUnrecognizedVMOptions") + } } // Requires old Guava. Can't use enforcedPlatform since predates BOM diff --git a/instrumentation/grizzly-2.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/grizzly/GrizzlyHttpAttributesGetter.java b/instrumentation/grizzly-2.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/grizzly/GrizzlyHttpAttributesGetter.java index 67e47b6c7f64..44b4e5d21c8d 100644 --- a/instrumentation/grizzly-2.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/grizzly/GrizzlyHttpAttributesGetter.java +++ b/instrumentation/grizzly-2.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/grizzly/GrizzlyHttpAttributesGetter.java @@ -7,12 +7,15 @@ import static java.util.Collections.emptyList; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpServerAttributesGetter; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerAttributesGetter; import java.util.ArrayList; import java.util.List; import javax.annotation.Nullable; +import org.glassfish.grizzly.Transport; import org.glassfish.grizzly.http.HttpRequestPacket; import org.glassfish.grizzly.http.HttpResponsePacket; +import org.glassfish.grizzly.nio.transport.TCPNIOTransport; +import org.glassfish.grizzly.nio.transport.UDPNIOTransport; final class GrizzlyHttpAttributesGetter implements HttpServerAttributesGetter { @@ -64,4 +67,65 @@ public String getUrlPath(HttpRequestPacket request) { public String getUrlQuery(HttpRequestPacket request) { return request.getQueryString(); } + + @Nullable + @Override + public String getNetworkTransport( + HttpRequestPacket request, @Nullable HttpResponsePacket response) { + Transport transport = request.getConnection().getTransport(); + if (transport instanceof TCPNIOTransport) { + return "tcp"; + } else if (transport instanceof UDPNIOTransport) { + return "udp"; + } + return null; + } + + @Nullable + @Override + public String getNetworkProtocolName( + HttpRequestPacket request, @Nullable HttpResponsePacket response) { + String protocol = request.getProtocolString(); + if (protocol.startsWith("HTTP/")) { + return "http"; + } + return null; + } + + @Nullable + @Override + public String getNetworkProtocolVersion( + HttpRequestPacket request, @Nullable HttpResponsePacket response) { + String protocol = request.getProtocolString(); + if (protocol.startsWith("HTTP/")) { + return protocol.substring("HTTP/".length()); + } + return null; + } + + @Nullable + @Override + public String getNetworkPeerAddress( + HttpRequestPacket request, @Nullable HttpResponsePacket response) { + return request.getRemoteAddress(); + } + + @Override + public Integer getNetworkPeerPort( + HttpRequestPacket request, @Nullable HttpResponsePacket response) { + return request.getRemotePort(); + } + + @Nullable + @Override + public String getNetworkLocalAddress( + HttpRequestPacket request, @Nullable HttpResponsePacket response) { + return request.getLocalAddress(); + } + + @Override + public Integer getNetworkLocalPort( + HttpRequestPacket request, @Nullable HttpResponsePacket response) { + return request.getLocalPort(); + } } diff --git a/instrumentation/grizzly-2.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/grizzly/GrizzlyNetAttributesGetter.java b/instrumentation/grizzly-2.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/grizzly/GrizzlyNetAttributesGetter.java deleted file mode 100644 index 5190a8e0aa23..000000000000 --- a/instrumentation/grizzly-2.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/grizzly/GrizzlyNetAttributesGetter.java +++ /dev/null @@ -1,105 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.javaagent.instrumentation.grizzly; - -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.NetTransportValues.IP_TCP; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.NetTransportValues.IP_UDP; - -import io.opentelemetry.instrumentation.api.instrumenter.net.NetServerAttributesGetter; -import javax.annotation.Nullable; -import org.glassfish.grizzly.Transport; -import org.glassfish.grizzly.http.HttpRequestPacket; -import org.glassfish.grizzly.http.HttpResponsePacket; -import org.glassfish.grizzly.nio.transport.TCPNIOTransport; -import org.glassfish.grizzly.nio.transport.UDPNIOTransport; - -final class GrizzlyNetAttributesGetter - implements NetServerAttributesGetter { - - @Override - public String getTransport(HttpRequestPacket request) { - Transport transport = request.getConnection().getTransport(); - if (transport instanceof TCPNIOTransport) { - return IP_TCP; - } - if (transport instanceof UDPNIOTransport) { - return IP_UDP; - } - return null; - } - - @Nullable - @Override - public String getNetworkTransport( - HttpRequestPacket request, @Nullable HttpResponsePacket response) { - Transport transport = request.getConnection().getTransport(); - if (transport instanceof TCPNIOTransport) { - return "tcp"; - } else if (transport instanceof UDPNIOTransport) { - return "udp"; - } - return null; - } - - @Nullable - @Override - public String getNetworkProtocolName( - HttpRequestPacket request, @Nullable HttpResponsePacket response) { - String protocol = request.getProtocolString(); - if (protocol.startsWith("HTTP/")) { - return "http"; - } - return null; - } - - @Nullable - @Override - public String getNetworkProtocolVersion( - HttpRequestPacket request, @Nullable HttpResponsePacket response) { - String protocol = request.getProtocolString(); - if (protocol.startsWith("HTTP/")) { - return protocol.substring("HTTP/".length()); - } - return null; - } - - @Nullable - @Override - public String getServerAddress(HttpRequestPacket request) { - return request.getLocalHost(); - } - - @Override - public Integer getServerPort(HttpRequestPacket request) { - return request.getServerPort(); - } - - @Nullable - @Override - public String getClientSocketAddress( - HttpRequestPacket request, @Nullable HttpResponsePacket response) { - return request.getRemoteAddress(); - } - - @Override - public Integer getClientSocketPort( - HttpRequestPacket request, @Nullable HttpResponsePacket response) { - return request.getRemotePort(); - } - - @Nullable - @Override - public String getServerSocketAddress( - HttpRequestPacket request, @Nullable HttpResponsePacket response) { - return request.getLocalAddress(); - } - - @Override - public Integer getServerSocketPort( - HttpRequestPacket request, @Nullable HttpResponsePacket response) { - return request.getLocalPort(); - } -} diff --git a/instrumentation/grizzly-2.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/grizzly/GrizzlySingletons.java b/instrumentation/grizzly-2.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/grizzly/GrizzlySingletons.java index 1064701270b8..7633c3bf7d56 100644 --- a/instrumentation/grizzly-2.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/grizzly/GrizzlySingletons.java +++ b/instrumentation/grizzly-2.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/grizzly/GrizzlySingletons.java @@ -6,13 +6,16 @@ package io.opentelemetry.javaagent.instrumentation.grizzly; import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.instrumentation.api.incubator.semconv.http.HttpExperimentalAttributesExtractor; +import io.opentelemetry.instrumentation.api.incubator.semconv.http.HttpServerExperimentalMetrics; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteHolder; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpServerAttributesExtractor; -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 io.opentelemetry.javaagent.bootstrap.internal.CommonConfig; +import io.opentelemetry.instrumentation.api.instrumenter.InstrumenterBuilder; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerAttributesExtractor; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerMetrics; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRoute; +import io.opentelemetry.instrumentation.api.semconv.http.HttpSpanNameExtractor; +import io.opentelemetry.instrumentation.api.semconv.http.HttpSpanStatusExtractor; +import io.opentelemetry.javaagent.bootstrap.internal.AgentCommonConfig; import io.opentelemetry.javaagent.bootstrap.servlet.AppServerBridge; import org.glassfish.grizzly.http.HttpRequestPacket; import org.glassfish.grizzly.http.HttpResponsePacket; @@ -23,20 +26,29 @@ public final class GrizzlySingletons { static { GrizzlyHttpAttributesGetter httpAttributesGetter = new GrizzlyHttpAttributesGetter(); - GrizzlyNetAttributesGetter netAttributesGetter = new GrizzlyNetAttributesGetter(); - INSTRUMENTER = + InstrumenterBuilder builder = Instrumenter.builder( GlobalOpenTelemetry.get(), "io.opentelemetry.grizzly-2.3", - HttpSpanNameExtractor.create(httpAttributesGetter)) + HttpSpanNameExtractor.builder(httpAttributesGetter) + .setKnownMethods(AgentCommonConfig.get().getKnownHttpRequestMethods()) + .build()) .setSpanStatusExtractor(HttpSpanStatusExtractor.create(httpAttributesGetter)) .addAttributesExtractor( - HttpServerAttributesExtractor.builder(httpAttributesGetter, netAttributesGetter) - .setCapturedRequestHeaders(CommonConfig.get().getServerRequestHeaders()) - .setCapturedResponseHeaders(CommonConfig.get().getServerResponseHeaders()) + HttpServerAttributesExtractor.builder(httpAttributesGetter) + .setCapturedRequestHeaders(AgentCommonConfig.get().getServerRequestHeaders()) + .setCapturedResponseHeaders(AgentCommonConfig.get().getServerResponseHeaders()) + .setKnownMethods(AgentCommonConfig.get().getKnownHttpRequestMethods()) .build()) - .addOperationMetrics(HttpServerMetrics.get()) + .addOperationMetrics(HttpServerMetrics.get()); + if (AgentCommonConfig.get().shouldEmitExperimentalHttpServerTelemetry()) { + builder + .addAttributesExtractor(HttpExperimentalAttributesExtractor.create(httpAttributesGetter)) + .addOperationMetrics(HttpServerExperimentalMetrics.get()); + } + INSTRUMENTER = + builder .addContextCustomizer( (context, request, attributes) -> new AppServerBridge.Builder() @@ -45,7 +57,10 @@ public final class GrizzlySingletons { .init(context)) .addContextCustomizer( (context, httpRequestPacket, startAttributes) -> GrizzlyErrorHolder.init(context)) - .addContextCustomizer(HttpRouteHolder.create(httpAttributesGetter)) + .addContextCustomizer( + HttpServerRoute.builder(httpAttributesGetter) + .setKnownMethods(AgentCommonConfig.get().getKnownHttpRequestMethods()) + .build()) .buildServerInstrumenter(HttpRequestHeadersGetter.INSTANCE); } diff --git a/instrumentation/grizzly-2.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/grizzly/HttpCodecFilterInstrumentation.java b/instrumentation/grizzly-2.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/grizzly/HttpCodecFilterInstrumentation.java index 2668b0dff1d8..d55e0cbbef83 100644 --- a/instrumentation/grizzly-2.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/grizzly/HttpCodecFilterInstrumentation.java +++ b/instrumentation/grizzly-2.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/grizzly/HttpCodecFilterInstrumentation.java @@ -8,6 +8,7 @@ import static io.opentelemetry.javaagent.instrumentation.grizzly.GrizzlySingletons.instrumenter; 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.context.Context; @@ -36,9 +37,10 @@ public void transform(TypeTransformer transformer) { takesArgument( 1, // this is for 2.3.20+ - named("org.glassfish.grizzly.http.HttpHeader") + namedOneOf( + "org.glassfish.grizzly.http.HttpHeader", // this is for 2.3 through 2.3.19 - .or(named("org.glassfish.grizzly.http.HttpPacketParsing")))) + "org.glassfish.grizzly.http.HttpPacketParsing"))) .and(isPublic()), HttpCodecFilterInstrumentation.class.getName() + "$HandleReadAdvice"); } diff --git a/instrumentation/grizzly-2.3/javaagent/src/test/groovy/GrizzlyFilterchainServerTest.groovy b/instrumentation/grizzly-2.3/javaagent/src/test/groovy/GrizzlyFilterchainServerTest.groovy index df603aef684f..9924891803ac 100644 --- a/instrumentation/grizzly-2.3/javaagent/src/test/groovy/GrizzlyFilterchainServerTest.groovy +++ b/instrumentation/grizzly-2.3/javaagent/src/test/groovy/GrizzlyFilterchainServerTest.groovy @@ -7,7 +7,7 @@ import io.opentelemetry.api.common.AttributeKey import io.opentelemetry.instrumentation.test.AgentTestTrait import io.opentelemetry.instrumentation.test.base.HttpServerTest import io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import io.opentelemetry.semconv.HttpAttributes import org.glassfish.grizzly.filterchain.BaseFilter import org.glassfish.grizzly.filterchain.FilterChain import org.glassfish.grizzly.filterchain.FilterChainBuilder @@ -67,8 +67,7 @@ class GrizzlyFilterchainServerTest extends HttpServerTest implements @Override Set> httpAttributes(ServerEndpoint endpoint) { def attributes = super.httpAttributes(endpoint) - attributes.remove(SemanticAttributes.HTTP_ROUTE) - attributes.remove(SemanticAttributes.NET_TRANSPORT) + attributes.remove(HttpAttributes.HTTP_ROUTE) attributes } diff --git a/instrumentation/grizzly-2.3/javaagent/src/test/groovy/GrizzlyTest.groovy b/instrumentation/grizzly-2.3/javaagent/src/test/groovy/GrizzlyTest.groovy index 0505799c3e28..eeddf36f0ebf 100644 --- a/instrumentation/grizzly-2.3/javaagent/src/test/groovy/GrizzlyTest.groovy +++ b/instrumentation/grizzly-2.3/javaagent/src/test/groovy/GrizzlyTest.groovy @@ -7,7 +7,7 @@ import io.opentelemetry.api.common.AttributeKey import io.opentelemetry.instrumentation.test.AgentTestTrait import io.opentelemetry.instrumentation.test.base.HttpServerTest import io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import io.opentelemetry.semconv.HttpAttributes import org.glassfish.grizzly.http.server.HttpHandler import org.glassfish.grizzly.http.server.HttpServer import org.glassfish.grizzly.http.server.NetworkListener @@ -79,8 +79,7 @@ class GrizzlyTest extends HttpServerTest implements AgentTestTrait { @Override Set> httpAttributes(ServerEndpoint endpoint) { def attributes = super.httpAttributes(endpoint) - attributes.remove(SemanticAttributes.HTTP_ROUTE) - attributes.remove(SemanticAttributes.NET_TRANSPORT) + attributes.remove(HttpAttributes.HTTP_ROUTE) attributes } diff --git a/instrumentation/grpc-1.6/README.md b/instrumentation/grpc-1.6/README.md index 1ae85d827314..6d590ac3d0d0 100644 --- a/instrumentation/grpc-1.6/README.md +++ b/instrumentation/grpc-1.6/README.md @@ -1,5 +1,7 @@ # Settings for the gRPC instrumentation -| System property | Type | Default | Description | -|---|---|---|---| -| `otel.instrumentation.grpc.experimental-span-attributes` | Boolean | `false` | Enable the capture of experimental span attributes. | +| System property | Type | Default | Description | +|-------------------------------------------------------------|---------|---------|----------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `otel.instrumentation.grpc.experimental-span-attributes` | Boolean | `false` | Enable the capture of experimental span attributes. | +| `otel.instrumentation.grpc.capture-metadata.client.request` | String | | A comma-separated list of request metadata keys. gRPC client instrumentation will capture metadata values corresponding to configured keys as span attributes. | +| `otel.instrumentation.grpc.capture-metadata.server.request` | String | | A comma-separated list of request metadata keys. gRPC server instrumentation will capture metadata values corresponding to configured keys as span attributes. | diff --git a/instrumentation/grpc-1.6/javaagent/build.gradle.kts b/instrumentation/grpc-1.6/javaagent/build.gradle.kts index a7e5a2d54ea4..671fdb315e90 100644 --- a/instrumentation/grpc-1.6/javaagent/build.gradle.kts +++ b/instrumentation/grpc-1.6/javaagent/build.gradle.kts @@ -36,5 +36,28 @@ tasks { jvmArgs("-Dotel.javaagent.experimental.thread-propagation-debugger.enabled=false") jvmArgs("-Dotel.instrumentation.grpc.capture-metadata.client.request=some-client-key") jvmArgs("-Dotel.instrumentation.grpc.capture-metadata.server.request=some-server-key") + jvmArgs("-Dotel.instrumentation.common.experimental.controller-telemetry.enabled=true") + // latest dep test occasionally fails because network type is ipv6 instead of the expected ipv4 + // and peer address is 0:0:0:0:0:0:0:1 instead of 127.0.0.1 + jvmArgs("-Djava.net.preferIPv4Stack=true") + + // exclude our grpc library instrumentation, the ContextStorageOverride contained within it + // breaks the tests + classpath = classpath.filter { + !it.absolutePath.contains("opentelemetry-grpc-1.6") + } + } +} + +if (!(findProperty("testLatestDeps") as Boolean)) { + configurations.testRuntimeClasspath { + resolutionStrategy { + eachDependency { + // early versions of grpc are not compatible with netty 4.1.101.Final + if (requested.group == "io.netty") { + useVersion("4.1.100.Final") + } + } + } } } diff --git a/instrumentation/grpc-1.6/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/grpc/v1_6/GrpcContextInstrumentation.java b/instrumentation/grpc-1.6/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/grpc/v1_6/GrpcContextInstrumentation.java index aeca7598e39f..78fbef21688e 100644 --- a/instrumentation/grpc-1.6/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/grpc/v1_6/GrpcContextInstrumentation.java +++ b/instrumentation/grpc-1.6/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/grpc/v1_6/GrpcContextInstrumentation.java @@ -36,14 +36,20 @@ public void transform(TypeTransformer transformer) { @SuppressWarnings("unused") public static class ContextBridgeAdvice { - @Advice.OnMethodEnter(skipOn = Advice.OnDefaultValue.class) - public static Object onEnter() { - return null; + @Advice.OnMethodEnter(skipOn = Advice.OnNonDefaultValue.class) + public static Context.Storage onEnter() { + return GrpcSingletons.getStorage(); } @Advice.OnMethodExit - public static void onExit(@Advice.Return(readOnly = false) Context.Storage storage) { - storage = GrpcSingletons.STORAGE; + public static void onExit( + @Advice.Return(readOnly = false) Context.Storage storage, + @Advice.Enter Context.Storage ourStorage) { + if (ourStorage != null) { + storage = ourStorage; + } else { + storage = GrpcSingletons.setStorage(storage); + } } } } diff --git a/instrumentation/grpc-1.6/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/grpc/v1_6/GrpcServerBuilderInstrumentation.java b/instrumentation/grpc-1.6/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/grpc/v1_6/GrpcServerBuilderInstrumentation.java index ecb266be21df..335aff6c27a8 100644 --- a/instrumentation/grpc-1.6/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/grpc/v1_6/GrpcServerBuilderInstrumentation.java +++ b/instrumentation/grpc-1.6/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/grpc/v1_6/GrpcServerBuilderInstrumentation.java @@ -59,9 +59,7 @@ public static void onEnter( } @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) - public static void onExit( - @Advice.This ServerBuilder serverBuilder, - @Advice.Local("otelCallDepth") CallDepth callDepth) { + public static void onExit(@Advice.Local("otelCallDepth") CallDepth callDepth) { callDepth.decrementAndGet(); } } diff --git a/instrumentation/grpc-1.6/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/grpc/v1_6/GrpcSingletons.java b/instrumentation/grpc-1.6/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/grpc/v1_6/GrpcSingletons.java index 0f4e4edb5cc1..f42fb98aab00 100644 --- a/instrumentation/grpc-1.6/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/grpc/v1_6/GrpcSingletons.java +++ b/instrumentation/grpc-1.6/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/grpc/v1_6/GrpcSingletons.java @@ -13,8 +13,9 @@ import io.opentelemetry.api.GlobalOpenTelemetry; import io.opentelemetry.instrumentation.grpc.v1_6.GrpcTelemetry; import io.opentelemetry.instrumentation.grpc.v1_6.internal.ContextStorageBridge; -import io.opentelemetry.javaagent.bootstrap.internal.InstrumentationConfig; +import io.opentelemetry.javaagent.bootstrap.internal.AgentInstrumentationConfig; import java.util.List; +import java.util.concurrent.atomic.AtomicReference; // Holds singleton references. public final class GrpcSingletons { @@ -23,18 +24,18 @@ public final class GrpcSingletons { public static final ServerInterceptor SERVER_INTERCEPTOR; - public static final Context.Storage STORAGE = new ContextStorageBridge(false); + private static final AtomicReference STORAGE_REFERENCE = new AtomicReference<>(); static { boolean experimentalSpanAttributes = - InstrumentationConfig.get() + AgentInstrumentationConfig.get() .getBoolean("otel.instrumentation.grpc.experimental-span-attributes", false); List clientRequestMetadata = - InstrumentationConfig.get() + AgentInstrumentationConfig.get() .getList("otel.instrumentation.grpc.capture-metadata.client.request", emptyList()); List serverRequestMetadata = - InstrumentationConfig.get() + AgentInstrumentationConfig.get() .getList("otel.instrumentation.grpc.capture-metadata.server.request", emptyList()); GrpcTelemetry telemetry = @@ -48,5 +49,14 @@ public final class GrpcSingletons { SERVER_INTERCEPTOR = telemetry.newServerInterceptor(); } + public static Context.Storage getStorage() { + return STORAGE_REFERENCE.get(); + } + + public static Context.Storage setStorage(Context.Storage storage) { + STORAGE_REFERENCE.compareAndSet(null, new ContextStorageBridge(storage)); + return getStorage(); + } + private GrpcSingletons() {} } diff --git a/instrumentation/grpc-1.6/library/README.md b/instrumentation/grpc-1.6/library/README.md index ab574d251496..061570517957 100644 --- a/instrumentation/grpc-1.6/library/README.md +++ b/instrumentation/grpc-1.6/library/README.md @@ -32,14 +32,14 @@ The instrumentation library provides the implementation of `ClientInterceptor` a ```java // For client-side, attach the interceptor to your channel builder. -void configureClientInterceptor(Opentelemetry opentelemetry, NettyChannelBuilder nettyChannelBuilder) { - GrpcTelemetry grpcTelemetry = GrpcTelemetry.create(opentelemetry); +void configureClientInterceptor(OpenTelemetry openTelemetry, NettyChannelBuilder nettyChannelBuilder) { + GrpcTelemetry grpcTelemetry = GrpcTelemetry.create(openTelemetry); nettyChannelBuilder.intercept(grpcTelemetry.newClientInterceptor()); } // For server-side, attatch the interceptor to your service. -ServerServiceDefinition configureServerInterceptor(Opentelemetry opentelemetry, ServerServiceDefinition serviceDefinition) { - GrpcTelemetry grpcTelemetry = GrpcTelemetry.create(opentelemetry); +ServerServiceDefinition configureServerInterceptor(OpenTelemetry openTelemetry, ServerServiceDefinition serviceDefinition) { + GrpcTelemetry grpcTelemetry = GrpcTelemetry.create(openTelemetry); return ServiceInterceptors.intercept(serviceDefinition, grpcTelemetry.newServerInterceptor()); } ``` diff --git a/instrumentation/grpc-1.6/library/build.gradle.kts b/instrumentation/grpc-1.6/library/build.gradle.kts index 807f6950a246..00ace4566efb 100644 --- a/instrumentation/grpc-1.6/library/build.gradle.kts +++ b/instrumentation/grpc-1.6/library/build.gradle.kts @@ -19,5 +19,22 @@ dependencies { tasks { test { systemProperty("testLatestDeps", findProperty("testLatestDeps") as Boolean) + jvmArgs("-Dotel.instrumentation.common.experimental.controller-telemetry.enabled=true") + // latest dep test occasionally fails because network type is ipv6 instead of the expected ipv4 + // and peer address is 0:0:0:0:0:0:0:1 instead of 127.0.0.1 + jvmArgs("-Djava.net.preferIPv4Stack=true") + } +} + +if (!(findProperty("testLatestDeps") as Boolean)) { + configurations.testRuntimeClasspath { + resolutionStrategy { + eachDependency { + // early versions of grpc are not compatible with netty 4.1.101.Final + if (requested.group == "io.netty") { + useVersion("4.1.100.Final") + } + } + } } } diff --git a/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/GrpcAttributesExtractor.java b/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/GrpcAttributesExtractor.java index a8c5f292992d..1e4f6ee406ea 100644 --- a/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/GrpcAttributesExtractor.java +++ b/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/GrpcAttributesExtractor.java @@ -9,14 +9,19 @@ import static io.opentelemetry.instrumentation.grpc.v1_6.CapturedGrpcMetadataUtil.requestAttributeKey; import io.grpc.Status; +import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.common.AttributesBuilder; import io.opentelemetry.context.Context; import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; import java.util.List; import javax.annotation.Nullable; final class GrpcAttributesExtractor implements AttributesExtractor { + + // copied from RpcIncubatingAttributes + private static final AttributeKey RPC_GRPC_STATUS_CODE = + AttributeKey.longKey("rpc.grpc.status_code"); + private final GrpcRpcAttributesGetter getter; private final List capturedRequestMetadata; @@ -39,7 +44,7 @@ public void onEnd( @Nullable Status status, @Nullable Throwable error) { if (status != null) { - attributes.put(SemanticAttributes.RPC_GRPC_STATUS_CODE, status.getCode().value()); + attributes.put(RPC_GRPC_STATUS_CODE, status.getCode().value()); } for (String key : capturedRequestMetadata) { List value = getter.metadataValue(request, key); diff --git a/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/GrpcHelper.java b/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/GrpcHelper.java deleted file mode 100644 index 789d9f1bc342..000000000000 --- a/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/GrpcHelper.java +++ /dev/null @@ -1,16 +0,0 @@ -/* - * 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/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/internal/GrpcNetServerAttributesGetter.java b/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/GrpcNetworkServerAttributesGetter.java similarity index 62% rename from instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/internal/GrpcNetServerAttributesGetter.java rename to instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/GrpcNetworkServerAttributesGetter.java index be7875e6b8ef..a7f3701c9c60 100644 --- a/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/internal/GrpcNetServerAttributesGetter.java +++ b/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/GrpcNetworkServerAttributesGetter.java @@ -3,21 +3,17 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.instrumentation.grpc.v1_6.internal; +package io.opentelemetry.instrumentation.grpc.v1_6; import io.grpc.Status; -import io.opentelemetry.instrumentation.api.instrumenter.net.NetServerAttributesGetter; -import io.opentelemetry.instrumentation.grpc.v1_6.GrpcRequest; +import io.opentelemetry.instrumentation.api.semconv.network.NetworkAttributesGetter; +import io.opentelemetry.instrumentation.api.semconv.network.ServerAttributesGetter; import java.net.InetSocketAddress; import java.net.SocketAddress; import javax.annotation.Nullable; -/** - * This class is internal and is hence not for public use. Its APIs are unstable and can change at - * any time. - */ -public final class GrpcNetServerAttributesGetter - implements NetServerAttributesGetter { +final class GrpcNetworkServerAttributesGetter + implements ServerAttributesGetter, NetworkAttributesGetter { @Nullable @Override @@ -30,9 +26,17 @@ public Integer getServerPort(GrpcRequest grpcRequest) { return grpcRequest.getLogicalPort(); } + @Nullable + @Override + public InetSocketAddress getNetworkLocalInetSocketAddress( + GrpcRequest grpcRequest, @Nullable Status status) { + // TODO: later version introduces TRANSPORT_ATTR_LOCAL_ADDR, might be a good idea to use it + return null; + } + @Override @Nullable - public InetSocketAddress getClientInetSocketAddress( + public InetSocketAddress getNetworkPeerInetSocketAddress( GrpcRequest request, @Nullable Status status) { SocketAddress address = request.getPeerSocketAddress(); if (address instanceof InetSocketAddress) { @@ -40,12 +44,4 @@ public InetSocketAddress getClientInetSocketAddress( } return null; } - - @Nullable - @Override - public InetSocketAddress getServerInetSocketAddress( - GrpcRequest grpcRequest, @Nullable Status status) { - // TODO: later version introduces TRANSPORT_ATTR_LOCAL_ADDR, might be a good idea to use it - return null; - } } diff --git a/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/GrpcRpcAttributesGetter.java b/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/GrpcRpcAttributesGetter.java index e681704c4ba8..ee7214bb1a87 100644 --- a/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/GrpcRpcAttributesGetter.java +++ b/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/GrpcRpcAttributesGetter.java @@ -6,7 +6,7 @@ package io.opentelemetry.instrumentation.grpc.v1_6; import io.grpc.Metadata; -import io.opentelemetry.instrumentation.api.instrumenter.rpc.RpcAttributesGetter; +import io.opentelemetry.instrumentation.api.incubator.semconv.rpc.RpcAttributesGetter; import java.util.Collections; import java.util.List; import java.util.stream.Collectors; diff --git a/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/GrpcSpanStatusExtractor.java b/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/GrpcSpanStatusExtractor.java index a79b1c3b5a08..5e9d84dd2966 100644 --- a/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/GrpcSpanStatusExtractor.java +++ b/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/GrpcSpanStatusExtractor.java @@ -5,15 +5,29 @@ package io.opentelemetry.instrumentation.grpc.v1_6; +import com.google.errorprone.annotations.Immutable; import io.grpc.Status; import io.grpc.StatusException; import io.grpc.StatusRuntimeException; import io.opentelemetry.api.trace.StatusCode; import io.opentelemetry.instrumentation.api.instrumenter.SpanStatusBuilder; import io.opentelemetry.instrumentation.api.instrumenter.SpanStatusExtractor; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; +import java.util.function.Predicate; import javax.annotation.Nullable; -final class GrpcSpanStatusExtractor implements SpanStatusExtractor { +enum GrpcSpanStatusExtractor implements SpanStatusExtractor { + CLIENT(GrpcSpanStatusExtractor::isClientError), + SERVER(GrpcSpanStatusExtractor::isServerError); + + private final ErrorStatusPredicate isError; + + GrpcSpanStatusExtractor(ErrorStatusPredicate isError) { + this.isError = isError; + } + @Override public void extract( SpanStatusBuilder spanStatusBuilder, @@ -28,11 +42,35 @@ public void extract( } } if (status != null) { - if (!status.isOk()) { + if (isError.test(status)) { spanStatusBuilder.setStatus(StatusCode.ERROR); } } else { SpanStatusExtractor.getDefault().extract(spanStatusBuilder, request, status, error); } } + + private static final Set serverErrorStatuses = new HashSet<>(); + + static { + serverErrorStatuses.addAll( + Arrays.asList( + Status.Code.UNKNOWN, + Status.Code.DEADLINE_EXCEEDED, + Status.Code.UNIMPLEMENTED, + Status.Code.INTERNAL, + Status.Code.UNAVAILABLE, + Status.Code.DATA_LOSS)); + } + + private static boolean isServerError(Status status) { + return serverErrorStatuses.contains(status.getCode()); + } + + private static boolean isClientError(Status status) { + return !status.isOk(); + } + + @Immutable + private interface ErrorStatusPredicate extends Predicate {} } diff --git a/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/GrpcTelemetryBuilder.java b/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/GrpcTelemetryBuilder.java index 5376cff084c6..98a9ae52ed94 100644 --- a/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/GrpcTelemetryBuilder.java +++ b/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/GrpcTelemetryBuilder.java @@ -8,43 +8,39 @@ import com.google.errorprone.annotations.CanIgnoreReturnValue; import io.grpc.Status; import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.instrumentation.api.incubator.semconv.rpc.RpcClientAttributesExtractor; +import io.opentelemetry.instrumentation.api.incubator.semconv.rpc.RpcClientMetrics; +import io.opentelemetry.instrumentation.api.incubator.semconv.rpc.RpcServerAttributesExtractor; +import io.opentelemetry.instrumentation.api.incubator.semconv.rpc.RpcServerMetrics; 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 io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.net.NetClientAttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.net.NetServerAttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.rpc.RpcClientAttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.rpc.RpcClientMetrics; -import io.opentelemetry.instrumentation.api.instrumenter.rpc.RpcServerAttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.rpc.RpcServerMetrics; -import io.opentelemetry.instrumentation.grpc.v1_6.internal.GrpcNetClientAttributesGetter; -import io.opentelemetry.instrumentation.grpc.v1_6.internal.GrpcNetServerAttributesGetter; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.opentelemetry.instrumentation.api.semconv.network.NetworkAttributesExtractor; +import io.opentelemetry.instrumentation.api.semconv.network.ServerAttributesExtractor; +import io.opentelemetry.instrumentation.grpc.v1_6.internal.GrpcClientNetworkAttributesGetter; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.function.Function; -import java.util.stream.Stream; import javax.annotation.Nullable; /** A builder of {@link GrpcTelemetry}. */ public final class GrpcTelemetryBuilder { private static final String INSTRUMENTATION_NAME = "io.opentelemetry.grpc-1.6"; + // copied from PeerIncubatingAttributes + private static final AttributeKey PEER_SERVICE = AttributeKey.stringKey("peer.service"); private final OpenTelemetry openTelemetry; @Nullable private String peerService; - @Nullable private Function, ? extends SpanNameExtractor> - clientSpanNameExtractorTransformer; - - @Nullable + clientSpanNameExtractorTransformer = Function.identity(); private Function, ? extends SpanNameExtractor> - serverSpanNameExtractorTransformer; - + serverSpanNameExtractorTransformer = Function.identity(); private final List> additionalExtractors = new ArrayList<>(); private final List> @@ -151,44 +147,39 @@ public GrpcTelemetryBuilder setCapturedServerRequestMetadata( /** Returns a new {@link GrpcTelemetry} with the settings of this {@link GrpcTelemetryBuilder}. */ public GrpcTelemetry build() { SpanNameExtractor originalSpanNameExtractor = new GrpcSpanNameExtractor(); - - SpanNameExtractor clientSpanNameExtractor = originalSpanNameExtractor; - if (clientSpanNameExtractorTransformer != null) { - clientSpanNameExtractor = clientSpanNameExtractorTransformer.apply(originalSpanNameExtractor); - } - - SpanNameExtractor serverSpanNameExtractor = originalSpanNameExtractor; - if (serverSpanNameExtractorTransformer != null) { - serverSpanNameExtractor = serverSpanNameExtractorTransformer.apply(originalSpanNameExtractor); - } + SpanNameExtractor clientSpanNameExtractor = + clientSpanNameExtractorTransformer.apply(originalSpanNameExtractor); + SpanNameExtractor serverSpanNameExtractor = + serverSpanNameExtractorTransformer.apply(originalSpanNameExtractor); InstrumenterBuilder clientInstrumenterBuilder = Instrumenter.builder(openTelemetry, INSTRUMENTATION_NAME, clientSpanNameExtractor); InstrumenterBuilder serverInstrumenterBuilder = Instrumenter.builder(openTelemetry, INSTRUMENTATION_NAME, serverSpanNameExtractor); - Stream.of(clientInstrumenterBuilder, serverInstrumenterBuilder) - .forEach( - instrumenter -> - instrumenter - .setSpanStatusExtractor(new GrpcSpanStatusExtractor()) - .addAttributesExtractors(additionalExtractors)); - - GrpcNetClientAttributesGetter netClientAttributesGetter = new GrpcNetClientAttributesGetter(); + GrpcClientNetworkAttributesGetter netClientAttributesGetter = + new GrpcClientNetworkAttributesGetter(); + GrpcNetworkServerAttributesGetter netServerAttributesGetter = + new GrpcNetworkServerAttributesGetter(); GrpcRpcAttributesGetter rpcAttributesGetter = GrpcRpcAttributesGetter.INSTANCE; clientInstrumenterBuilder + .setSpanStatusExtractor(GrpcSpanStatusExtractor.CLIENT) + .addAttributesExtractors(additionalExtractors) .addAttributesExtractor(RpcClientAttributesExtractor.create(rpcAttributesGetter)) - .addAttributesExtractor(NetClientAttributesExtractor.create(netClientAttributesGetter)) + .addAttributesExtractor(ServerAttributesExtractor.create(netClientAttributesGetter)) + .addAttributesExtractor(NetworkAttributesExtractor.create(netClientAttributesGetter)) .addAttributesExtractors(additionalClientExtractors) .addAttributesExtractor( new GrpcAttributesExtractor( GrpcRpcAttributesGetter.INSTANCE, capturedClientRequestMetadata)) .addOperationMetrics(RpcClientMetrics.get()); serverInstrumenterBuilder + .setSpanStatusExtractor(GrpcSpanStatusExtractor.SERVER) + .addAttributesExtractors(additionalExtractors) .addAttributesExtractor(RpcServerAttributesExtractor.create(rpcAttributesGetter)) - .addAttributesExtractor( - NetServerAttributesExtractor.create(new GrpcNetServerAttributesGetter())) + .addAttributesExtractor(ServerAttributesExtractor.create(netServerAttributesGetter)) + .addAttributesExtractor(NetworkAttributesExtractor.create(netServerAttributesGetter)) .addAttributesExtractor( new GrpcAttributesExtractor( GrpcRpcAttributesGetter.INSTANCE, capturedServerRequestMetadata)) @@ -197,7 +188,7 @@ public GrpcTelemetry build() { if (peerService != null) { clientInstrumenterBuilder.addAttributesExtractor( - AttributesExtractor.constant(SemanticAttributes.PEER_SERVICE, peerService)); + AttributesExtractor.constant(PEER_SERVICE, peerService)); } return new GrpcTelemetry( diff --git a/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/MetadataSetter.java b/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/MetadataSetter.java index 99a50d9a151c..e79c4bb86907 100644 --- a/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/MetadataSetter.java +++ b/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/MetadataSetter.java @@ -13,6 +13,8 @@ enum MetadataSetter implements TextMapSetter { @Override public void set(Metadata carrier, String key, String value) { - carrier.put(Metadata.Key.of(key, Metadata.ASCII_STRING_MARSHALLER), value); + Metadata.Key metadataKey = Metadata.Key.of(key, Metadata.ASCII_STRING_MARSHALLER); + carrier.removeAll(metadataKey); + carrier.put(metadataKey, value); } } diff --git a/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/TracingClientInterceptor.java b/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/TracingClientInterceptor.java index 08642ffa7b79..8790dad4c493 100644 --- a/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/TracingClientInterceptor.java +++ b/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/TracingClientInterceptor.java @@ -15,6 +15,7 @@ import io.grpc.Metadata; import io.grpc.MethodDescriptor; import io.grpc.Status; +import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.common.Attributes; import io.opentelemetry.api.trace.Span; import io.opentelemetry.context.Context; @@ -25,6 +26,13 @@ final class TracingClientInterceptor implements ClientInterceptor { + // copied from MessageIncubatingAttributes + private static final AttributeKey MESSAGE_ID = AttributeKey.longKey("message.id"); + private static final AttributeKey MESSAGE_TYPE = AttributeKey.stringKey("message.type"); + // copied from MessageIncubatingAttributes.MessageTypeValues + private static final String SENT = "SENT"; + private static final String RECEIVED = "RECEIVED"; + @SuppressWarnings("rawtypes") private static final AtomicLongFieldUpdater MESSAGE_ID_UPDATER = AtomicLongFieldUpdater.newUpdater(TracingClientCall.class, "messageId"); @@ -109,11 +117,7 @@ public void sendMessage(REQUEST message) { } Span span = Span.fromContext(context); Attributes attributes = - Attributes.of( - GrpcHelper.MESSAGE_TYPE, - "SENT", - GrpcHelper.MESSAGE_ID, - MESSAGE_ID_UPDATER.incrementAndGet(this)); + Attributes.of(MESSAGE_TYPE, SENT, MESSAGE_ID, MESSAGE_ID_UPDATER.incrementAndGet(this)); span.addEvent("message", attributes); } @@ -140,9 +144,9 @@ public void onMessage(RESPONSE message) { Span span = Span.fromContext(context); Attributes attributes = Attributes.of( - GrpcHelper.MESSAGE_TYPE, - "RECEIVED", - GrpcHelper.MESSAGE_ID, + MESSAGE_TYPE, + RECEIVED, + MESSAGE_ID, MESSAGE_ID_UPDATER.incrementAndGet(TracingClientCall.this)); span.addEvent("message", attributes); try (Scope ignored = context.makeCurrent()) { diff --git a/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/TracingServerInterceptor.java b/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/TracingServerInterceptor.java index bd4175d1f62c..241f94188b4d 100644 --- a/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/TracingServerInterceptor.java +++ b/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/TracingServerInterceptor.java @@ -9,11 +9,13 @@ import io.grpc.ForwardingServerCall; import io.grpc.ForwardingServerCallListener; import io.grpc.Grpc; +import io.grpc.InternalMetadata; import io.grpc.Metadata; import io.grpc.ServerCall; import io.grpc.ServerCallHandler; import io.grpc.ServerInterceptor; import io.grpc.Status; +import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.common.Attributes; import io.opentelemetry.api.trace.Span; import io.opentelemetry.context.Context; @@ -23,10 +25,20 @@ final class TracingServerInterceptor implements ServerInterceptor { + // copied from MessageIncubatingAttributes + private static final AttributeKey MESSAGE_ID = AttributeKey.longKey("message.id"); + private static final AttributeKey MESSAGE_TYPE = AttributeKey.stringKey("message.type"); + // copied from MessageIncubatingAttributes.MessageTypeValues + private static final String SENT = "SENT"; + private static final String RECEIVED = "RECEIVED"; + @SuppressWarnings("rawtypes") private static final AtomicLongFieldUpdater MESSAGE_ID_UPDATER = AtomicLongFieldUpdater.newUpdater(TracingServerCall.class, "messageId"); + private static final Metadata.Key AUTHORITY_KEY = + InternalMetadata.keyOf(":authority", Metadata.ASCII_STRING_MARSHALLER); + private final Instrumenter instrumenter; private final boolean captureExperimentalSpanAttributes; @@ -41,12 +53,17 @@ public ServerCall.Listener interceptCall( ServerCall call, Metadata headers, ServerCallHandler next) { + String authority = call.getAuthority(); + if (authority == null && headers != null) { + // armeria grpc client exposes authority in a header + authority = headers.get(AUTHORITY_KEY); + } GrpcRequest request = new GrpcRequest( call.getMethodDescriptor(), headers, call.getAttributes().get(Grpc.TRANSPORT_ATTR_REMOTE_ADDR), - call.getAuthority()); + authority); Context parentContext = Context.current(); if (!instrumenter.shouldStart(parentContext, request)) { return next.startCall(call, headers); @@ -66,6 +83,7 @@ final class TracingServerCall extends ForwardingServerCall.SimpleForwardingServerCall { private final Context context; private final GrpcRequest request; + private Status status; // Used by MESSAGE_ID_UPDATER @SuppressWarnings("UnusedVariable") @@ -90,23 +108,19 @@ public void sendMessage(RESPONSE message) { } Span span = Span.fromContext(context); Attributes attributes = - Attributes.of( - GrpcHelper.MESSAGE_TYPE, - "SENT", - GrpcHelper.MESSAGE_ID, - MESSAGE_ID_UPDATER.incrementAndGet(this)); + Attributes.of(MESSAGE_TYPE, SENT, MESSAGE_ID, MESSAGE_ID_UPDATER.incrementAndGet(this)); span.addEvent("message", attributes); } @Override public void close(Status status, Metadata trailers) { + this.status = status; 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 @@ -122,12 +136,11 @@ final class TracingServerCallListener @Override public void onMessage(REQUEST message) { - // TODO(anuraaga): Restore Attributes attributes = Attributes.of( - GrpcHelper.MESSAGE_TYPE, - "RECEIVED", - GrpcHelper.MESSAGE_ID, + MESSAGE_TYPE, + RECEIVED, + MESSAGE_ID, MESSAGE_ID_UPDATER.incrementAndGet(TracingServerCall.this)); Span.fromContext(context).addEvent("message", attributes); delegate().onMessage(message); @@ -165,6 +178,10 @@ public void onComplete() { instrumenter.end(context, request, Status.UNKNOWN, e); throw e; } + if (status == null) { + status = Status.UNKNOWN; + } + instrumenter.end(context, request, status, status.getCause()); } @Override diff --git a/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/internal/ContextStorageBridge.java b/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/internal/ContextStorageBridge.java index 6dbef26e17ca..5e82e7474518 100644 --- a/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/internal/ContextStorageBridge.java +++ b/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/internal/ContextStorageBridge.java @@ -28,11 +28,24 @@ public final class ContextStorageBridge extends Context.Storage { private static final Context.Key OTEL_CONTEXT = Context.key("otel-context"); private static final Context.Key OTEL_SCOPE = Context.key("otel-scope"); + // context attached to original context store + private static final Context.Key ORIGINAL_CONTEXT = Context.key("original-context"); + // context that should be restored in original context store on detach + private static final Context.Key ORIGINAL_TO_RESTORE = + Context.key("original-to-restore"); private final boolean propagateGrpcDeadline; + // original context storage that would have been used when running without agent + private final Context.Storage originalStorage; public ContextStorageBridge(boolean propagateGrpcDeadline) { this.propagateGrpcDeadline = propagateGrpcDeadline; + this.originalStorage = null; + } + + public ContextStorageBridge(Context.Storage originalStorage) { + propagateGrpcDeadline = false; + this.originalStorage = originalStorage; } @Override @@ -45,7 +58,9 @@ public Context doAttach(Context toAttach) { } if (current == toAttach) { - return current.withValue(OTEL_SCOPE, Scope.noop()); + Context result = current.withValue(OTEL_SCOPE, Scope.noop()); + result = attachOriginalContextStorage(result); + return result; } io.opentelemetry.context.Context base = OTEL_CONTEXT.get(toAttach); @@ -64,11 +79,28 @@ public Context doAttach(Context toAttach) { } Scope scope = newOtelContext.makeCurrent(); - return current.withValue(OTEL_SCOPE, scope); + Context result = current.withValue(OTEL_SCOPE, scope); + result = attachOriginalContextStorage(result); + return result; + } + + private Context attachOriginalContextStorage(Context context) { + Context result = context; + if (originalStorage != null) { + Context originalToRestore = originalStorage.doAttach(result); + result = result.withValues(ORIGINAL_CONTEXT, result, ORIGINAL_TO_RESTORE, originalToRestore); + } + return result; } @Override public void detach(Context toDetach, Context toRestore) { + if (originalStorage != null) { + Context originalContext = ORIGINAL_CONTEXT.get(toRestore); + Context originalToRestore = ORIGINAL_TO_RESTORE.get(toRestore); + originalStorage.detach(originalContext, originalToRestore); + } + Scope scope = OTEL_SCOPE.get(toRestore); if (scope == null) { logger.log( @@ -93,17 +125,18 @@ public Context current() { // 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. - if (!propagateGrpcDeadline) { - // Because we are propagating gRPC context via OpenTelemetry here, we may also propagate a - // deadline where it - // wasn't present before. Notably, this could happen with no user intention when using the - // javaagent which will - // add OpenTelemetry propagation automatically, and cause that code to fail with a deadline - // cancellation. While - // ideally we could propagate deadline as well as gRPC intended, we cannot have existing - // code fail because it - // added the javaagent and choose to fork here. - current = current.fork(); + if (!propagateGrpcDeadline && originalStorage != null) { + Context originalCurrent = originalStorage.current(); + // check whether grpc context would have propagated without otel context + if (originalCurrent == null || originalCurrent == Context.ROOT) { + // Because we are propagating gRPC context via OpenTelemetry here, we may also propagate a + // deadline where it wasn't present before. Notably, this could happen with no user + // intention when using the javaagent which will add OpenTelemetry propagation + // automatically, and cause that code to fail with a deadline cancellation. While ideally + // we could propagate deadline as well as gRPC intended, we cannot have existing code fail + // because it added the javaagent and choose to fork here. + current = current.fork(); + } } return current.withValue(OTEL_CONTEXT, otelContext); diff --git a/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/internal/GrpcNetClientAttributesGetter.java b/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/internal/GrpcClientNetworkAttributesGetter.java similarity index 71% rename from instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/internal/GrpcNetClientAttributesGetter.java rename to instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/internal/GrpcClientNetworkAttributesGetter.java index a7a72cc8db35..1a8ce22d8eff 100644 --- a/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/internal/GrpcNetClientAttributesGetter.java +++ b/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/internal/GrpcClientNetworkAttributesGetter.java @@ -6,7 +6,8 @@ package io.opentelemetry.instrumentation.grpc.v1_6.internal; import io.grpc.Status; -import io.opentelemetry.instrumentation.api.instrumenter.net.NetClientAttributesGetter; +import io.opentelemetry.instrumentation.api.semconv.network.NetworkAttributesGetter; +import io.opentelemetry.instrumentation.api.semconv.network.ServerAttributesGetter; import io.opentelemetry.instrumentation.grpc.v1_6.GrpcRequest; import java.net.InetSocketAddress; import java.net.SocketAddress; @@ -16,8 +17,8 @@ * This class is internal and is hence not for public use. Its APIs are unstable and can change at * any time. */ -public final class GrpcNetClientAttributesGetter - implements NetClientAttributesGetter { +public final class GrpcClientNetworkAttributesGetter + implements ServerAttributesGetter, NetworkAttributesGetter { @Nullable @Override @@ -32,7 +33,7 @@ public Integer getServerPort(GrpcRequest grpcRequest) { @Override @Nullable - public InetSocketAddress getServerInetSocketAddress( + public InetSocketAddress getNetworkPeerInetSocketAddress( GrpcRequest request, @Nullable Status response) { SocketAddress address = request.getPeerSocketAddress(); if (address instanceof InetSocketAddress) { diff --git a/instrumentation/grpc-1.6/library/src/test/java/io/opentelemetry/instrumentation/grpc/v1_6/GrpcTest.java b/instrumentation/grpc-1.6/library/src/test/java/io/opentelemetry/instrumentation/grpc/v1_6/GrpcTest.java index da2180df7b4f..5aa6f1aa9a41 100644 --- a/instrumentation/grpc-1.6/library/src/test/java/io/opentelemetry/instrumentation/grpc/v1_6/GrpcTest.java +++ b/instrumentation/grpc-1.6/library/src/test/java/io/opentelemetry/instrumentation/grpc/v1_6/GrpcTest.java @@ -5,6 +5,8 @@ package io.opentelemetry.instrumentation.grpc.v1_6; +import static org.assertj.core.api.Assertions.assertThat; + import example.GreeterGrpc; import example.Helloworld; import io.grpc.BindableService; @@ -23,7 +25,6 @@ import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; import io.opentelemetry.instrumentation.testing.junit.LibraryInstrumentationExtension; -import io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions; import java.util.Collections; import java.util.concurrent.TimeUnit; import javax.annotation.Nullable; @@ -124,7 +125,7 @@ public void sayHello( "parent", () -> client.sayHello(Helloworld.Request.newBuilder().setName("test").build())); - OpenTelemetryAssertions.assertThat(response.getMessage()).isEqualTo("Hello test"); + assertThat(response.getMessage()).isEqualTo("Hello test"); testing() .waitAndAssertTraces( diff --git a/instrumentation/grpc-1.6/library/src/test/java/io/opentelemetry/instrumentation/grpc/v1_6/MetadataSetterTest.java b/instrumentation/grpc-1.6/library/src/test/java/io/opentelemetry/instrumentation/grpc/v1_6/MetadataSetterTest.java new file mode 100644 index 000000000000..f138f4db0a4e --- /dev/null +++ b/instrumentation/grpc-1.6/library/src/test/java/io/opentelemetry/instrumentation/grpc/v1_6/MetadataSetterTest.java @@ -0,0 +1,67 @@ +/* + * 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.grpc.Metadata; +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.propagation.W3CTraceContextPropagator; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.propagation.TextMapGetter; +import io.opentelemetry.context.propagation.TextMapPropagator; +import javax.annotation.Nullable; +import org.junit.jupiter.api.Test; + +class MetadataSetterTest { + + @Test + void overwriteTracingHeader() { + Metadata metadata = new Metadata(); + TextMapPropagator propagator = W3CTraceContextPropagator.getInstance(); + for (int i = 1; i <= 2; i++) { + Context context = + Context.root() + .with( + Span.wrap( + SpanContext.create( + TraceId.fromLongs(0, i), + SpanId.fromLong(i), + TraceFlags.getDefault(), + TraceState.getDefault()))); + propagator.inject(context, metadata, MetadataSetter.INSTANCE); + } + + assertThat(metadata.getAll(Metadata.Key.of("traceparent", Metadata.ASCII_STRING_MARSHALLER))) + .hasSize(1); + + Context context = + propagator.extract( + Context.root(), + metadata, + new TextMapGetter() { + @Override + public Iterable keys(Metadata metadata) { + return metadata.keys(); + } + + @Nullable + @Override + public String get(@Nullable Metadata metadata, String key) { + return metadata.get(Metadata.Key.of(key, Metadata.ASCII_STRING_MARSHALLER)); + } + }); + + SpanContext spanContext = Span.fromContext(context).getSpanContext(); + assertThat(spanContext.getTraceId()).isEqualTo(TraceId.fromLongs(0, 2)); + assertThat(spanContext.getSpanId()).isEqualTo(SpanId.fromLong(2)); + } +} diff --git a/instrumentation/grpc-1.6/testing/build.gradle.kts b/instrumentation/grpc-1.6/testing/build.gradle.kts index 53e988c1679a..259d70dbb53e 100644 --- a/instrumentation/grpc-1.6/testing/build.gradle.kts +++ b/instrumentation/grpc-1.6/testing/build.gradle.kts @@ -1,6 +1,8 @@ +import com.google.protobuf.gradle.* + plugins { id("otel.java-conventions") - id("otel.protobuf-conventions") + id("com.google.protobuf") } val grpcVersion = "1.6.0" @@ -30,3 +32,39 @@ tasks { } } } + +protobuf { + protoc { + // The artifact spec for the Protobuf Compiler + 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 { + id("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().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"))) + } +} diff --git a/instrumentation/grpc-1.6/testing/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/AbstractGrpcStreamingTest.java b/instrumentation/grpc-1.6/testing/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/AbstractGrpcStreamingTest.java index 8c9546f99436..9153436ba922 100644 --- a/instrumentation/grpc-1.6/testing/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/AbstractGrpcStreamingTest.java +++ b/instrumentation/grpc-1.6/testing/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/AbstractGrpcStreamingTest.java @@ -19,22 +19,29 @@ import io.grpc.ServerBuilder; import io.grpc.Status; import io.grpc.stub.StreamObserver; +import io.opentelemetry.api.trace.Span; import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.api.trace.Tracer; import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; import io.opentelemetry.instrumentation.testing.util.ThrowingRunnable; -import io.opentelemetry.sdk.testing.assertj.EventDataAssert; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.opentelemetry.sdk.trace.data.EventData; +import io.opentelemetry.semconv.NetworkAttributes; +import io.opentelemetry.semconv.ServerAttributes; +import io.opentelemetry.semconv.incubating.MessageIncubatingAttributes; +import io.opentelemetry.semconv.incubating.RpcIncubatingAttributes; import java.util.ArrayList; import java.util.List; import java.util.Queue; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; import java.util.stream.Collectors; import java.util.stream.IntStream; import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; import org.junitpioneer.jupiter.cartesian.CartesianTest; public abstract class AbstractGrpcStreamingTest { @@ -146,25 +153,25 @@ public void onCompleted() { .sorted() .collect(Collectors.toList())); - List> events = new ArrayList<>(); + List> events = new ArrayList<>(); for (int i = 1; i <= clientMessageCount * serverMessageCount + clientMessageCount; i++) { long messageId = i; events.add( event -> - event + assertThat(event) .hasName("message") .hasAttributesSatisfying( attrs -> assertThat(attrs) .hasSize(2) .hasEntrySatisfying( - SemanticAttributes.MESSAGE_TYPE, + MessageIncubatingAttributes.MESSAGE_TYPE, val -> assertThat(val) .satisfiesAnyOf( v -> assertThat(v).isEqualTo("RECEIVED"), v -> assertThat(v).isEqualTo("SENT"))) - .containsEntry(SemanticAttributes.MESSAGE_ID, messageId))); + .containsEntry(MessageIncubatingAttributes.MESSAGE_ID, messageId))); } testing() @@ -177,34 +184,42 @@ public void onCompleted() { .hasNoParent() .hasAttributesSatisfyingExactly( addExtraClientAttributes( - equalTo(SemanticAttributes.RPC_SYSTEM, "grpc"), - equalTo(SemanticAttributes.RPC_SERVICE, "example.Greeter"), - equalTo(SemanticAttributes.RPC_METHOD, "Conversation"), + equalTo(RpcIncubatingAttributes.RPC_SYSTEM, "grpc"), + equalTo(RpcIncubatingAttributes.RPC_SERVICE, "example.Greeter"), + equalTo(RpcIncubatingAttributes.RPC_METHOD, "Conversation"), equalTo( - SemanticAttributes.RPC_GRPC_STATUS_CODE, + RpcIncubatingAttributes.RPC_GRPC_STATUS_CODE, (long) Status.Code.OK.value()), - equalTo(SemanticAttributes.NET_PEER_NAME, "localhost"), - equalTo( - SemanticAttributes.NET_PEER_PORT, (long) server.getPort()))) - .hasEventsSatisfyingExactly(events.toArray(new Consumer[0])), + equalTo(ServerAttributes.SERVER_ADDRESS, "localhost"), + equalTo(ServerAttributes.SERVER_PORT, (long) server.getPort()))) + .satisfies( + spanData -> + assertThat(spanData.getEvents()) + .satisfiesExactlyInAnyOrder( + events.toArray(new Consumer[0]))), span -> span.hasName("example.Greeter/Conversation") .hasKind(SpanKind.SERVER) .hasParent(trace.getSpan(0)) .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.RPC_SYSTEM, "grpc"), - equalTo(SemanticAttributes.RPC_SERVICE, "example.Greeter"), - equalTo(SemanticAttributes.RPC_METHOD, "Conversation"), + equalTo(RpcIncubatingAttributes.RPC_SYSTEM, "grpc"), + equalTo(RpcIncubatingAttributes.RPC_SERVICE, "example.Greeter"), + equalTo(RpcIncubatingAttributes.RPC_METHOD, "Conversation"), equalTo( - SemanticAttributes.RPC_GRPC_STATUS_CODE, + RpcIncubatingAttributes.RPC_GRPC_STATUS_CODE, (long) Status.Code.OK.value()), - equalTo(SemanticAttributes.NET_HOST_NAME, "localhost"), - equalTo(SemanticAttributes.NET_HOST_PORT, server.getPort()), - equalTo(SemanticAttributes.NET_SOCK_PEER_ADDR, "127.0.0.1"), + equalTo(ServerAttributes.SERVER_ADDRESS, "localhost"), + equalTo(ServerAttributes.SERVER_PORT, server.getPort()), + equalTo(NetworkAttributes.NETWORK_TYPE, "ipv4"), + equalTo(NetworkAttributes.NETWORK_PEER_ADDRESS, "127.0.0.1"), satisfies( - SemanticAttributes.NET_SOCK_PEER_PORT, + NetworkAttributes.NETWORK_PEER_PORT, val -> assertThat(val).isNotNull())) - .hasEventsSatisfyingExactly(events.toArray(new Consumer[0])))); + .satisfies( + spanData -> + assertThat(spanData.getEvents()) + .satisfiesExactlyInAnyOrder( + events.toArray(new Consumer[0]))))); testing() .waitAndAssertMetrics( "io.opentelemetry.grpc-1.6", @@ -220,15 +235,16 @@ public void onCompleted() { point -> point.hasAttributesSatisfying( equalTo( - SemanticAttributes.NET_HOST_NAME, "localhost"), + ServerAttributes.SERVER_ADDRESS, "localhost"), equalTo( - SemanticAttributes.RPC_METHOD, "Conversation"), + RpcIncubatingAttributes.RPC_METHOD, + "Conversation"), equalTo( - SemanticAttributes.RPC_SERVICE, + RpcIncubatingAttributes.RPC_SERVICE, "example.Greeter"), - equalTo(SemanticAttributes.RPC_SYSTEM, "grpc"), + equalTo(RpcIncubatingAttributes.RPC_SYSTEM, "grpc"), equalTo( - SemanticAttributes.RPC_GRPC_STATUS_CODE, + RpcIncubatingAttributes.RPC_GRPC_STATUS_CODE, (long) Status.Code.OK.value())))))); testing() .waitAndAssertMetrics( @@ -245,21 +261,107 @@ public void onCompleted() { point -> point.hasAttributesSatisfying( equalTo( - SemanticAttributes.NET_PEER_NAME, "localhost"), + ServerAttributes.SERVER_ADDRESS, "localhost"), equalTo( - SemanticAttributes.NET_PEER_PORT, - server.getPort()), + ServerAttributes.SERVER_PORT, server.getPort()), equalTo( - SemanticAttributes.RPC_METHOD, "Conversation"), + RpcIncubatingAttributes.RPC_METHOD, + "Conversation"), equalTo( - SemanticAttributes.RPC_SERVICE, + RpcIncubatingAttributes.RPC_SERVICE, "example.Greeter"), - equalTo(SemanticAttributes.RPC_SYSTEM, "grpc"), + equalTo(RpcIncubatingAttributes.RPC_SYSTEM, "grpc"), equalTo( - SemanticAttributes.RPC_GRPC_STATUS_CODE, + RpcIncubatingAttributes.RPC_GRPC_STATUS_CODE, (long) Status.Code.OK.value())))))); } + @Test + void grpcServerSpanEndsAfterChildSpan() throws Exception { + Tracer tracer = testing().getOpenTelemetry().getTracer("test"); + AtomicBoolean serverSpanRecording = new AtomicBoolean(); + CountDownLatch latch = new CountDownLatch(2); + + BindableService greeter = + new GreeterGrpc.GreeterImplBase() { + @Override + public StreamObserver conversation( + StreamObserver observer) { + return new StreamObserver() { + Span span; + + @Override + public void onNext(Helloworld.Response value) { + span = tracer.spanBuilder("child").startSpan(); + observer.onNext(value); + } + + @Override + public void onError(Throwable t) { + observer.onError(t); + span.end(); + } + + @Override + public void onCompleted() { + observer.onCompleted(); + serverSpanRecording.set(Span.current().isRecording()); + span.end(); + latch.countDown(); + } + }; + } + }; + + Server server = configureServer(ServerBuilder.forPort(0).addService(greeter)).build().start(); + ManagedChannel channel = createChannel(server); + closer.add(() -> channel.shutdownNow().awaitTermination(10, TimeUnit.SECONDS)); + closer.add(() -> server.shutdownNow().awaitTermination()); + + GreeterGrpc.GreeterStub client = GreeterGrpc.newStub(channel).withWaitForReady(); + + StreamObserver observer2 = + client.conversation( + new StreamObserver() { + @Override + public void onNext(Helloworld.Response value) {} + + @Override + public void onError(Throwable t) {} + + @Override + public void onCompleted() { + latch.countDown(); + } + }); + + Helloworld.Response message = Helloworld.Response.newBuilder().setMessage("message").build(); + observer2.onNext(message); + observer2.onCompleted(); + + latch.await(10, TimeUnit.SECONDS); + + // server span should end after child span + assertThat(serverSpanRecording).isTrue(); + + testing() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("example.Greeter/Conversation") + .hasKind(SpanKind.CLIENT) + .hasNoParent(), + span -> + span.hasName("example.Greeter/Conversation") + .hasKind(SpanKind.SERVER) + .hasParent(trace.getSpan(0)), + span -> + span.hasName("child") + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(1)))); + } + private ManagedChannel createChannel(Server server) throws Exception { ManagedChannelBuilder channelBuilder = configureClient(ManagedChannelBuilder.forAddress("localhost", server.getPort())); diff --git a/instrumentation/grpc-1.6/testing/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/AbstractGrpcTest.java b/instrumentation/grpc-1.6/testing/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/AbstractGrpcTest.java index 7891d0263d6a..bc812d26babd 100644 --- a/instrumentation/grpc-1.6/testing/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/AbstractGrpcTest.java +++ b/instrumentation/grpc-1.6/testing/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/AbstractGrpcTest.java @@ -46,9 +46,11 @@ import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; import io.opentelemetry.instrumentation.testing.util.ThrowingRunnable; import io.opentelemetry.sdk.testing.assertj.AttributeAssertion; -import io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions; import io.opentelemetry.sdk.trace.data.StatusData; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.opentelemetry.semconv.NetworkAttributes; +import io.opentelemetry.semconv.ServerAttributes; +import io.opentelemetry.semconv.incubating.MessageIncubatingAttributes; +import io.opentelemetry.semconv.incubating.RpcIncubatingAttributes; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -59,6 +61,7 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Stream; import org.junit.jupiter.api.AfterEach; @@ -132,58 +135,64 @@ public void sayHello( .hasParent(trace.getSpan(0)) .hasAttributesSatisfyingExactly( addExtraClientAttributes( - equalTo(SemanticAttributes.RPC_SYSTEM, "grpc"), - equalTo(SemanticAttributes.RPC_SERVICE, "example.Greeter"), - equalTo(SemanticAttributes.RPC_METHOD, "SayHello"), + equalTo(RpcIncubatingAttributes.RPC_SYSTEM, "grpc"), + equalTo(RpcIncubatingAttributes.RPC_SERVICE, "example.Greeter"), + equalTo(RpcIncubatingAttributes.RPC_METHOD, "SayHello"), equalTo( - SemanticAttributes.RPC_GRPC_STATUS_CODE, + RpcIncubatingAttributes.RPC_GRPC_STATUS_CODE, (long) Status.Code.OK.value()), - equalTo(SemanticAttributes.NET_PEER_NAME, "localhost"), - equalTo( - SemanticAttributes.NET_PEER_PORT, (long) server.getPort()))) + equalTo(ServerAttributes.SERVER_ADDRESS, "localhost"), + equalTo(ServerAttributes.SERVER_PORT, (long) server.getPort()))) .hasEventsSatisfyingExactly( event -> event .hasName("message") .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.MESSAGE_TYPE, "SENT"), - equalTo(SemanticAttributes.MESSAGE_ID, 1L)), + equalTo( + MessageIncubatingAttributes.MESSAGE_TYPE, "SENT"), + equalTo(MessageIncubatingAttributes.MESSAGE_ID, 1L)), event -> event .hasName("message") .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.MESSAGE_TYPE, "RECEIVED"), - equalTo(SemanticAttributes.MESSAGE_ID, 2L))), + equalTo( + MessageIncubatingAttributes.MESSAGE_TYPE, + "RECEIVED"), + equalTo(MessageIncubatingAttributes.MESSAGE_ID, 2L))), span -> span.hasName("example.Greeter/SayHello") .hasKind(SpanKind.SERVER) .hasParent(trace.getSpan(1)) .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.RPC_SYSTEM, "grpc"), - equalTo(SemanticAttributes.RPC_SERVICE, "example.Greeter"), - equalTo(SemanticAttributes.RPC_METHOD, "SayHello"), + equalTo(RpcIncubatingAttributes.RPC_SYSTEM, "grpc"), + equalTo(RpcIncubatingAttributes.RPC_SERVICE, "example.Greeter"), + equalTo(RpcIncubatingAttributes.RPC_METHOD, "SayHello"), equalTo( - SemanticAttributes.RPC_GRPC_STATUS_CODE, + RpcIncubatingAttributes.RPC_GRPC_STATUS_CODE, (long) Status.Code.OK.value()), - equalTo(SemanticAttributes.NET_HOST_NAME, "localhost"), - equalTo(SemanticAttributes.NET_HOST_PORT, server.getPort()), - equalTo(SemanticAttributes.NET_SOCK_PEER_ADDR, "127.0.0.1"), + equalTo(ServerAttributes.SERVER_ADDRESS, "localhost"), + equalTo(ServerAttributes.SERVER_PORT, server.getPort()), + equalTo(NetworkAttributes.NETWORK_TYPE, "ipv4"), + equalTo(NetworkAttributes.NETWORK_PEER_ADDRESS, "127.0.0.1"), satisfies( - SemanticAttributes.NET_SOCK_PEER_PORT, + NetworkAttributes.NETWORK_PEER_PORT, val -> assertThat(val).isNotNull())) .hasEventsSatisfyingExactly( event -> event .hasName("message") .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.MESSAGE_TYPE, "RECEIVED"), - equalTo(SemanticAttributes.MESSAGE_ID, 1L)), + equalTo( + MessageIncubatingAttributes.MESSAGE_TYPE, + "RECEIVED"), + equalTo(MessageIncubatingAttributes.MESSAGE_ID, 1L)), event -> event .hasName("message") .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.MESSAGE_TYPE, "SENT"), - equalTo(SemanticAttributes.MESSAGE_ID, 2L))))); + equalTo( + MessageIncubatingAttributes.MESSAGE_TYPE, "SENT"), + equalTo(MessageIncubatingAttributes.MESSAGE_ID, 2L))))); testing() .waitAndAssertMetrics( "io.opentelemetry.grpc-1.6", @@ -199,14 +208,15 @@ public void sayHello( point -> point.hasAttributesSatisfying( equalTo( - SemanticAttributes.NET_HOST_NAME, "localhost"), - equalTo(SemanticAttributes.RPC_METHOD, "SayHello"), + ServerAttributes.SERVER_ADDRESS, "localhost"), + equalTo( + RpcIncubatingAttributes.RPC_METHOD, "SayHello"), equalTo( - SemanticAttributes.RPC_SERVICE, + RpcIncubatingAttributes.RPC_SERVICE, "example.Greeter"), - equalTo(SemanticAttributes.RPC_SYSTEM, "grpc"), + equalTo(RpcIncubatingAttributes.RPC_SYSTEM, "grpc"), equalTo( - SemanticAttributes.RPC_GRPC_STATUS_CODE, + RpcIncubatingAttributes.RPC_GRPC_STATUS_CODE, (long) Status.Code.OK.value())))))); testing() .waitAndAssertMetrics( @@ -223,17 +233,17 @@ public void sayHello( point -> point.hasAttributesSatisfying( equalTo( - SemanticAttributes.NET_PEER_NAME, "localhost"), + ServerAttributes.SERVER_ADDRESS, "localhost"), + equalTo( + ServerAttributes.SERVER_PORT, server.getPort()), equalTo( - SemanticAttributes.NET_PEER_PORT, - server.getPort()), - equalTo(SemanticAttributes.RPC_METHOD, "SayHello"), + RpcIncubatingAttributes.RPC_METHOD, "SayHello"), equalTo( - SemanticAttributes.RPC_SERVICE, + RpcIncubatingAttributes.RPC_SERVICE, "example.Greeter"), - equalTo(SemanticAttributes.RPC_SYSTEM, "grpc"), + equalTo(RpcIncubatingAttributes.RPC_SYSTEM, "grpc"), equalTo( - SemanticAttributes.RPC_GRPC_STATUS_CODE, + RpcIncubatingAttributes.RPC_GRPC_STATUS_CODE, (long) Status.Code.OK.value())))))); } @@ -293,58 +303,64 @@ public void sayHello( .hasParent(trace.getSpan(0)) .hasAttributesSatisfyingExactly( addExtraClientAttributes( - equalTo(SemanticAttributes.RPC_SYSTEM, "grpc"), - equalTo(SemanticAttributes.RPC_SERVICE, "example.Greeter"), - equalTo(SemanticAttributes.RPC_METHOD, "SayHello"), + equalTo(RpcIncubatingAttributes.RPC_SYSTEM, "grpc"), + equalTo(RpcIncubatingAttributes.RPC_SERVICE, "example.Greeter"), + equalTo(RpcIncubatingAttributes.RPC_METHOD, "SayHello"), equalTo( - SemanticAttributes.RPC_GRPC_STATUS_CODE, + RpcIncubatingAttributes.RPC_GRPC_STATUS_CODE, (long) Status.Code.OK.value()), - equalTo(SemanticAttributes.NET_PEER_NAME, "localhost"), - equalTo( - SemanticAttributes.NET_PEER_PORT, (long) server.getPort()))) + equalTo(ServerAttributes.SERVER_ADDRESS, "localhost"), + equalTo(ServerAttributes.SERVER_PORT, (long) server.getPort()))) .hasEventsSatisfyingExactly( event -> event .hasName("message") .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.MESSAGE_TYPE, "SENT"), - equalTo(SemanticAttributes.MESSAGE_ID, 1L)), + equalTo( + MessageIncubatingAttributes.MESSAGE_TYPE, "SENT"), + equalTo(MessageIncubatingAttributes.MESSAGE_ID, 1L)), event -> event .hasName("message") .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.MESSAGE_TYPE, "RECEIVED"), - equalTo(SemanticAttributes.MESSAGE_ID, 2L))), + equalTo( + MessageIncubatingAttributes.MESSAGE_TYPE, + "RECEIVED"), + equalTo(MessageIncubatingAttributes.MESSAGE_ID, 2L))), span -> span.hasName("example.Greeter/SayHello") .hasKind(SpanKind.SERVER) .hasParent(trace.getSpan(1)) .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.RPC_SYSTEM, "grpc"), - equalTo(SemanticAttributes.RPC_SERVICE, "example.Greeter"), - equalTo(SemanticAttributes.RPC_METHOD, "SayHello"), + equalTo(RpcIncubatingAttributes.RPC_SYSTEM, "grpc"), + equalTo(RpcIncubatingAttributes.RPC_SERVICE, "example.Greeter"), + equalTo(RpcIncubatingAttributes.RPC_METHOD, "SayHello"), equalTo( - SemanticAttributes.RPC_GRPC_STATUS_CODE, + RpcIncubatingAttributes.RPC_GRPC_STATUS_CODE, (long) Status.Code.OK.value()), - equalTo(SemanticAttributes.NET_HOST_NAME, "localhost"), - equalTo(SemanticAttributes.NET_HOST_PORT, server.getPort()), - equalTo(SemanticAttributes.NET_SOCK_PEER_ADDR, "127.0.0.1"), + equalTo(ServerAttributes.SERVER_ADDRESS, "localhost"), + equalTo(ServerAttributes.SERVER_PORT, server.getPort()), + equalTo(NetworkAttributes.NETWORK_TYPE, "ipv4"), + equalTo(NetworkAttributes.NETWORK_PEER_ADDRESS, "127.0.0.1"), satisfies( - SemanticAttributes.NET_SOCK_PEER_PORT, + NetworkAttributes.NETWORK_PEER_PORT, val -> assertThat(val).isNotNull())) .hasEventsSatisfyingExactly( event -> event .hasName("message") .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.MESSAGE_TYPE, "RECEIVED"), - equalTo(SemanticAttributes.MESSAGE_ID, 1L)), + equalTo( + MessageIncubatingAttributes.MESSAGE_TYPE, + "RECEIVED"), + equalTo(MessageIncubatingAttributes.MESSAGE_ID, 1L)), event -> event .hasName("message") .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.MESSAGE_TYPE, "SENT"), - equalTo(SemanticAttributes.MESSAGE_ID, 2L))), + equalTo( + MessageIncubatingAttributes.MESSAGE_TYPE, "SENT"), + equalTo(MessageIncubatingAttributes.MESSAGE_ID, 2L))), span -> span.hasName("child") .hasKind(SpanKind.INTERNAL) @@ -364,14 +380,15 @@ public void sayHello( point -> point.hasAttributesSatisfying( equalTo( - SemanticAttributes.NET_HOST_NAME, "localhost"), - equalTo(SemanticAttributes.RPC_METHOD, "SayHello"), + ServerAttributes.SERVER_ADDRESS, "localhost"), + equalTo( + RpcIncubatingAttributes.RPC_METHOD, "SayHello"), equalTo( - SemanticAttributes.RPC_SERVICE, + RpcIncubatingAttributes.RPC_SERVICE, "example.Greeter"), - equalTo(SemanticAttributes.RPC_SYSTEM, "grpc"), + equalTo(RpcIncubatingAttributes.RPC_SYSTEM, "grpc"), equalTo( - SemanticAttributes.RPC_GRPC_STATUS_CODE, + RpcIncubatingAttributes.RPC_GRPC_STATUS_CODE, (long) Status.Code.OK.value())))))); testing() .waitAndAssertMetrics( @@ -388,17 +405,17 @@ public void sayHello( point -> point.hasAttributesSatisfying( equalTo( - SemanticAttributes.NET_PEER_NAME, "localhost"), + ServerAttributes.SERVER_ADDRESS, "localhost"), equalTo( - SemanticAttributes.NET_PEER_PORT, - server.getPort()), - equalTo(SemanticAttributes.RPC_METHOD, "SayHello"), + ServerAttributes.SERVER_PORT, server.getPort()), equalTo( - SemanticAttributes.RPC_SERVICE, + RpcIncubatingAttributes.RPC_METHOD, "SayHello"), + equalTo( + RpcIncubatingAttributes.RPC_SERVICE, "example.Greeter"), - equalTo(SemanticAttributes.RPC_SYSTEM, "grpc"), + equalTo(RpcIncubatingAttributes.RPC_SYSTEM, "grpc"), equalTo( - SemanticAttributes.RPC_GRPC_STATUS_CODE, + RpcIncubatingAttributes.RPC_GRPC_STATUS_CODE, (long) Status.Code.OK.value())))))); } @@ -466,58 +483,64 @@ public void onCompleted() { .hasParent(trace.getSpan(0)) .hasAttributesSatisfyingExactly( addExtraClientAttributes( - equalTo(SemanticAttributes.RPC_SYSTEM, "grpc"), - equalTo(SemanticAttributes.RPC_SERVICE, "example.Greeter"), - equalTo(SemanticAttributes.RPC_METHOD, "SayHello"), + equalTo(RpcIncubatingAttributes.RPC_SYSTEM, "grpc"), + equalTo(RpcIncubatingAttributes.RPC_SERVICE, "example.Greeter"), + equalTo(RpcIncubatingAttributes.RPC_METHOD, "SayHello"), equalTo( - SemanticAttributes.RPC_GRPC_STATUS_CODE, + RpcIncubatingAttributes.RPC_GRPC_STATUS_CODE, (long) Status.Code.OK.value()), - equalTo(SemanticAttributes.NET_PEER_NAME, "localhost"), - equalTo( - SemanticAttributes.NET_PEER_PORT, (long) server.getPort()))) + equalTo(ServerAttributes.SERVER_ADDRESS, "localhost"), + equalTo(ServerAttributes.SERVER_PORT, (long) server.getPort()))) .hasEventsSatisfyingExactly( event -> event .hasName("message") .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.MESSAGE_TYPE, "SENT"), - equalTo(SemanticAttributes.MESSAGE_ID, 1L)), + equalTo( + MessageIncubatingAttributes.MESSAGE_TYPE, "SENT"), + equalTo(MessageIncubatingAttributes.MESSAGE_ID, 1L)), event -> event .hasName("message") .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.MESSAGE_TYPE, "RECEIVED"), - equalTo(SemanticAttributes.MESSAGE_ID, 2L))), + equalTo( + MessageIncubatingAttributes.MESSAGE_TYPE, + "RECEIVED"), + equalTo(MessageIncubatingAttributes.MESSAGE_ID, 2L))), span -> span.hasName("example.Greeter/SayHello") .hasKind(SpanKind.SERVER) .hasParent(trace.getSpan(1)) .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.RPC_SYSTEM, "grpc"), - equalTo(SemanticAttributes.RPC_SERVICE, "example.Greeter"), - equalTo(SemanticAttributes.RPC_METHOD, "SayHello"), + equalTo(RpcIncubatingAttributes.RPC_SYSTEM, "grpc"), + equalTo(RpcIncubatingAttributes.RPC_SERVICE, "example.Greeter"), + equalTo(RpcIncubatingAttributes.RPC_METHOD, "SayHello"), equalTo( - SemanticAttributes.RPC_GRPC_STATUS_CODE, + RpcIncubatingAttributes.RPC_GRPC_STATUS_CODE, (long) Status.Code.OK.value()), - equalTo(SemanticAttributes.NET_HOST_NAME, "localhost"), - equalTo(SemanticAttributes.NET_HOST_PORT, server.getPort()), - equalTo(SemanticAttributes.NET_SOCK_PEER_ADDR, "127.0.0.1"), + equalTo(ServerAttributes.SERVER_ADDRESS, "localhost"), + equalTo(ServerAttributes.SERVER_PORT, server.getPort()), + equalTo(NetworkAttributes.NETWORK_TYPE, "ipv4"), + equalTo(NetworkAttributes.NETWORK_PEER_ADDRESS, "127.0.0.1"), satisfies( - SemanticAttributes.NET_SOCK_PEER_PORT, + NetworkAttributes.NETWORK_PEER_PORT, val -> assertThat(val).isNotNull())) .hasEventsSatisfyingExactly( event -> event .hasName("message") .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.MESSAGE_TYPE, "RECEIVED"), - equalTo(SemanticAttributes.MESSAGE_ID, 1L)), + equalTo( + MessageIncubatingAttributes.MESSAGE_TYPE, + "RECEIVED"), + equalTo(MessageIncubatingAttributes.MESSAGE_ID, 1L)), event -> event .hasName("message") .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.MESSAGE_TYPE, "SENT"), - equalTo(SemanticAttributes.MESSAGE_ID, 2L))), + equalTo( + MessageIncubatingAttributes.MESSAGE_TYPE, "SENT"), + equalTo(MessageIncubatingAttributes.MESSAGE_ID, 2L))), span -> span.hasName("child") .hasKind(SpanKind.INTERNAL) @@ -537,14 +560,15 @@ public void onCompleted() { point -> point.hasAttributesSatisfying( equalTo( - SemanticAttributes.NET_HOST_NAME, "localhost"), - equalTo(SemanticAttributes.RPC_METHOD, "SayHello"), + ServerAttributes.SERVER_ADDRESS, "localhost"), equalTo( - SemanticAttributes.RPC_SERVICE, + RpcIncubatingAttributes.RPC_METHOD, "SayHello"), + equalTo( + RpcIncubatingAttributes.RPC_SERVICE, "example.Greeter"), - equalTo(SemanticAttributes.RPC_SYSTEM, "grpc"), + equalTo(RpcIncubatingAttributes.RPC_SYSTEM, "grpc"), equalTo( - SemanticAttributes.RPC_GRPC_STATUS_CODE, + RpcIncubatingAttributes.RPC_GRPC_STATUS_CODE, (long) Status.Code.OK.value())))))); testing() .waitAndAssertMetrics( @@ -561,17 +585,17 @@ public void onCompleted() { point -> point.hasAttributesSatisfying( equalTo( - SemanticAttributes.NET_PEER_NAME, "localhost"), + ServerAttributes.SERVER_ADDRESS, "localhost"), + equalTo( + ServerAttributes.SERVER_PORT, server.getPort()), equalTo( - SemanticAttributes.NET_PEER_PORT, - server.getPort()), - equalTo(SemanticAttributes.RPC_METHOD, "SayHello"), + RpcIncubatingAttributes.RPC_METHOD, "SayHello"), equalTo( - SemanticAttributes.RPC_SERVICE, + RpcIncubatingAttributes.RPC_SERVICE, "example.Greeter"), - equalTo(SemanticAttributes.RPC_SYSTEM, "grpc"), + equalTo(RpcIncubatingAttributes.RPC_SYSTEM, "grpc"), equalTo( - SemanticAttributes.RPC_GRPC_STATUS_CODE, + RpcIncubatingAttributes.RPC_GRPC_STATUS_CODE, (long) Status.Code.OK.value())))))); } @@ -602,6 +626,7 @@ public void sayHello( assertThat(t.getStatus().getDescription()).isEqualTo(status.getDescription()); }); + boolean isServerError = status.getCode() != Status.Code.NOT_FOUND; testing() .waitAndAssertTraces( trace -> @@ -613,39 +638,40 @@ public void sayHello( .hasStatus(StatusData.error()) .hasAttributesSatisfyingExactly( addExtraClientAttributes( - equalTo(SemanticAttributes.RPC_SYSTEM, "grpc"), - equalTo(SemanticAttributes.RPC_SERVICE, "example.Greeter"), - equalTo(SemanticAttributes.RPC_METHOD, "SayHello"), + equalTo(RpcIncubatingAttributes.RPC_SYSTEM, "grpc"), + equalTo(RpcIncubatingAttributes.RPC_SERVICE, "example.Greeter"), + equalTo(RpcIncubatingAttributes.RPC_METHOD, "SayHello"), equalTo( - SemanticAttributes.RPC_GRPC_STATUS_CODE, + RpcIncubatingAttributes.RPC_GRPC_STATUS_CODE, (long) status.getCode().value()), - equalTo(SemanticAttributes.NET_PEER_NAME, "localhost"), - equalTo( - SemanticAttributes.NET_PEER_PORT, (long) server.getPort()))) + equalTo(ServerAttributes.SERVER_ADDRESS, "localhost"), + equalTo(ServerAttributes.SERVER_PORT, (long) server.getPort()))) .hasEventsSatisfyingExactly( event -> event .hasName("message") .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.MESSAGE_TYPE, "SENT"), - equalTo(SemanticAttributes.MESSAGE_ID, 1L))), + equalTo( + MessageIncubatingAttributes.MESSAGE_TYPE, "SENT"), + equalTo(MessageIncubatingAttributes.MESSAGE_ID, 1L))), span -> span.hasName("example.Greeter/SayHello") .hasKind(SpanKind.SERVER) .hasParent(trace.getSpan(0)) - .hasStatus(StatusData.error()) + .hasStatus(isServerError ? StatusData.error() : StatusData.unset()) .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.RPC_SYSTEM, "grpc"), - equalTo(SemanticAttributes.RPC_SERVICE, "example.Greeter"), - equalTo(SemanticAttributes.RPC_METHOD, "SayHello"), + equalTo(RpcIncubatingAttributes.RPC_SYSTEM, "grpc"), + equalTo(RpcIncubatingAttributes.RPC_SERVICE, "example.Greeter"), + equalTo(RpcIncubatingAttributes.RPC_METHOD, "SayHello"), equalTo( - SemanticAttributes.RPC_GRPC_STATUS_CODE, + RpcIncubatingAttributes.RPC_GRPC_STATUS_CODE, (long) status.getCode().value()), - equalTo(SemanticAttributes.NET_HOST_NAME, "localhost"), - equalTo(SemanticAttributes.NET_HOST_PORT, server.getPort()), - equalTo(SemanticAttributes.NET_SOCK_PEER_ADDR, "127.0.0.1"), + equalTo(ServerAttributes.SERVER_ADDRESS, "localhost"), + equalTo(ServerAttributes.SERVER_PORT, server.getPort()), + equalTo(NetworkAttributes.NETWORK_TYPE, "ipv4"), + equalTo(NetworkAttributes.NETWORK_PEER_ADDRESS, "127.0.0.1"), satisfies( - SemanticAttributes.NET_SOCK_PEER_PORT, + NetworkAttributes.NETWORK_PEER_PORT, val -> assertThat(val).isNotNull())) .hasEventsSatisfying( events -> { @@ -653,8 +679,9 @@ public void sayHello( assertThat(events.get(0)) .hasName("message") .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.MESSAGE_TYPE, "RECEIVED"), - equalTo(SemanticAttributes.MESSAGE_ID, 1L)); + equalTo( + MessageIncubatingAttributes.MESSAGE_TYPE, "RECEIVED"), + equalTo(MessageIncubatingAttributes.MESSAGE_ID, 1L)); if (status.getCause() == null) { assertThat(events).hasSize(1); } else { @@ -677,14 +704,15 @@ public void sayHello( point -> point.hasAttributesSatisfying( equalTo( - SemanticAttributes.NET_HOST_NAME, "localhost"), - equalTo(SemanticAttributes.RPC_METHOD, "SayHello"), + ServerAttributes.SERVER_ADDRESS, "localhost"), equalTo( - SemanticAttributes.RPC_SERVICE, + RpcIncubatingAttributes.RPC_METHOD, "SayHello"), + equalTo( + RpcIncubatingAttributes.RPC_SERVICE, "example.Greeter"), - equalTo(SemanticAttributes.RPC_SYSTEM, "grpc"), + equalTo(RpcIncubatingAttributes.RPC_SYSTEM, "grpc"), equalTo( - SemanticAttributes.RPC_GRPC_STATUS_CODE, + RpcIncubatingAttributes.RPC_GRPC_STATUS_CODE, (long) status.getCode().value())))))); testing() .waitAndAssertMetrics( @@ -701,17 +729,17 @@ public void sayHello( point -> point.hasAttributesSatisfying( equalTo( - SemanticAttributes.NET_PEER_NAME, "localhost"), + ServerAttributes.SERVER_ADDRESS, "localhost"), + equalTo( + ServerAttributes.SERVER_PORT, server.getPort()), equalTo( - SemanticAttributes.NET_PEER_PORT, - server.getPort()), - equalTo(SemanticAttributes.RPC_METHOD, "SayHello"), + RpcIncubatingAttributes.RPC_METHOD, "SayHello"), equalTo( - SemanticAttributes.RPC_SERVICE, + RpcIncubatingAttributes.RPC_SERVICE, "example.Greeter"), - equalTo(SemanticAttributes.RPC_SYSTEM, "grpc"), + equalTo(RpcIncubatingAttributes.RPC_SYSTEM, "grpc"), equalTo( - SemanticAttributes.RPC_GRPC_STATUS_CODE, + RpcIncubatingAttributes.RPC_GRPC_STATUS_CODE, (long) status.getCode().value())))))); } @@ -740,7 +768,10 @@ public void sayHello( t -> { // gRPC doesn't appear to propagate server exceptions that are thrown, not onError. assertThat(t.getStatus().getCode()).isEqualTo(Status.UNKNOWN.getCode()); - assertThat(t.getStatus().getDescription()).isNull(); + assertThat(t.getStatus().getDescription()) + .satisfiesAnyOf( + a -> assertThat(a).isNull(), + a -> assertThat(a).isEqualTo("Application error processing RPC")); }); testing() @@ -759,39 +790,40 @@ public void sayHello( .hasStatus(StatusData.error()) .hasAttributesSatisfyingExactly( addExtraClientAttributes( - equalTo(SemanticAttributes.RPC_SYSTEM, "grpc"), - equalTo(SemanticAttributes.RPC_SERVICE, "example.Greeter"), - equalTo(SemanticAttributes.RPC_METHOD, "SayHello"), + equalTo(RpcIncubatingAttributes.RPC_SYSTEM, "grpc"), + equalTo(RpcIncubatingAttributes.RPC_SERVICE, "example.Greeter"), + equalTo(RpcIncubatingAttributes.RPC_METHOD, "SayHello"), equalTo( - SemanticAttributes.RPC_GRPC_STATUS_CODE, + RpcIncubatingAttributes.RPC_GRPC_STATUS_CODE, (long) Status.UNKNOWN.getCode().value()), - equalTo(SemanticAttributes.NET_PEER_NAME, "localhost"), - equalTo( - SemanticAttributes.NET_PEER_PORT, (long) server.getPort()))) + equalTo(ServerAttributes.SERVER_ADDRESS, "localhost"), + equalTo(ServerAttributes.SERVER_PORT, (long) server.getPort()))) .hasEventsSatisfyingExactly( event -> event .hasName("message") .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.MESSAGE_TYPE, "SENT"), - equalTo(SemanticAttributes.MESSAGE_ID, 1L))), + equalTo( + MessageIncubatingAttributes.MESSAGE_TYPE, "SENT"), + equalTo(MessageIncubatingAttributes.MESSAGE_ID, 1L))), span -> span.hasName("example.Greeter/SayHello") .hasKind(SpanKind.SERVER) .hasParent(trace.getSpan(0)) .hasStatus(StatusData.error()) .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.RPC_SYSTEM, "grpc"), - equalTo(SemanticAttributes.RPC_SERVICE, "example.Greeter"), - equalTo(SemanticAttributes.RPC_METHOD, "SayHello"), + equalTo(RpcIncubatingAttributes.RPC_SYSTEM, "grpc"), + equalTo(RpcIncubatingAttributes.RPC_SERVICE, "example.Greeter"), + equalTo(RpcIncubatingAttributes.RPC_METHOD, "SayHello"), equalTo( - SemanticAttributes.RPC_GRPC_STATUS_CODE, + RpcIncubatingAttributes.RPC_GRPC_STATUS_CODE, (long) Status.Code.UNKNOWN.value()), - equalTo(SemanticAttributes.NET_HOST_NAME, "localhost"), - equalTo(SemanticAttributes.NET_HOST_PORT, server.getPort()), - equalTo(SemanticAttributes.NET_SOCK_PEER_ADDR, "127.0.0.1"), + equalTo(ServerAttributes.SERVER_ADDRESS, "localhost"), + equalTo(ServerAttributes.SERVER_PORT, server.getPort()), + equalTo(NetworkAttributes.NETWORK_TYPE, "ipv4"), + equalTo(NetworkAttributes.NETWORK_PEER_ADDRESS, "127.0.0.1"), satisfies( - SemanticAttributes.NET_SOCK_PEER_PORT, + NetworkAttributes.NETWORK_PEER_PORT, val -> assertThat(val).isNotNull())) .hasEventsSatisfying( events -> { @@ -799,8 +831,9 @@ public void sayHello( assertThat(events.get(0)) .hasName("message") .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.MESSAGE_TYPE, "RECEIVED"), - equalTo(SemanticAttributes.MESSAGE_ID, 1L)); + equalTo( + MessageIncubatingAttributes.MESSAGE_TYPE, "RECEIVED"), + equalTo(MessageIncubatingAttributes.MESSAGE_ID, 1L)); span.hasException(status.asRuntimeException()); }))); testing() @@ -818,14 +851,15 @@ public void sayHello( point -> point.hasAttributesSatisfying( equalTo( - SemanticAttributes.NET_HOST_NAME, "localhost"), - equalTo(SemanticAttributes.RPC_METHOD, "SayHello"), + ServerAttributes.SERVER_ADDRESS, "localhost"), + equalTo( + RpcIncubatingAttributes.RPC_METHOD, "SayHello"), equalTo( - SemanticAttributes.RPC_SERVICE, + RpcIncubatingAttributes.RPC_SERVICE, "example.Greeter"), - equalTo(SemanticAttributes.RPC_SYSTEM, "grpc"), + equalTo(RpcIncubatingAttributes.RPC_SYSTEM, "grpc"), equalTo( - SemanticAttributes.RPC_GRPC_STATUS_CODE, + RpcIncubatingAttributes.RPC_GRPC_STATUS_CODE, (long) Status.Code.UNKNOWN.value())))))); testing() .waitAndAssertMetrics( @@ -842,17 +876,17 @@ public void sayHello( point -> point.hasAttributesSatisfying( equalTo( - SemanticAttributes.NET_PEER_NAME, "localhost"), + ServerAttributes.SERVER_ADDRESS, "localhost"), equalTo( - SemanticAttributes.NET_PEER_PORT, - server.getPort()), - equalTo(SemanticAttributes.RPC_METHOD, "SayHello"), + ServerAttributes.SERVER_PORT, server.getPort()), equalTo( - SemanticAttributes.RPC_SERVICE, + RpcIncubatingAttributes.RPC_METHOD, "SayHello"), + equalTo( + RpcIncubatingAttributes.RPC_SERVICE, "example.Greeter"), - equalTo(SemanticAttributes.RPC_SYSTEM, "grpc"), + equalTo(RpcIncubatingAttributes.RPC_SYSTEM, "grpc"), equalTo( - SemanticAttributes.RPC_GRPC_STATUS_CODE, + RpcIncubatingAttributes.RPC_GRPC_STATUS_CODE, (long) Status.Code.UNKNOWN.value())))))); } @@ -861,11 +895,19 @@ static class ErrorProvider implements ArgumentsProvider { public Stream provideArguments(ExtensionContext context) { return Stream.of( arguments(Status.UNKNOWN.withCause(new RuntimeException("some error"))), - arguments(Status.PERMISSION_DENIED.withCause(new RuntimeException("some error"))), + arguments(Status.DEADLINE_EXCEEDED.withCause(new RuntimeException("some error"))), arguments(Status.UNIMPLEMENTED.withCause(new RuntimeException("some error"))), + arguments(Status.INTERNAL.withCause(new RuntimeException("some error"))), + arguments(Status.UNAVAILABLE.withCause(new RuntimeException("some error"))), + arguments(Status.DATA_LOSS.withCause(new RuntimeException("some error"))), + arguments(Status.NOT_FOUND.withCause(new RuntimeException("some error"))), arguments(Status.UNKNOWN.withDescription("some description")), - arguments(Status.PERMISSION_DENIED.withDescription("some description")), - arguments(Status.UNIMPLEMENTED.withDescription("some description"))); + arguments(Status.DEADLINE_EXCEEDED.withDescription("some description")), + arguments(Status.UNIMPLEMENTED.withDescription("some description")), + arguments(Status.INTERNAL.withDescription("some description")), + arguments(Status.UNAVAILABLE.withDescription("some description")), + arguments(Status.DATA_LOSS.withDescription("some description")), + arguments(Status.NOT_FOUND.withDescription("some description"))); } } @@ -996,58 +1038,64 @@ public void onCompleted() { .hasParent(trace.getSpan(0)) .hasAttributesSatisfyingExactly( addExtraClientAttributes( - equalTo(SemanticAttributes.RPC_SYSTEM, "grpc"), - equalTo(SemanticAttributes.RPC_SERVICE, "example.Greeter"), - equalTo(SemanticAttributes.RPC_METHOD, "SayHello"), + equalTo(RpcIncubatingAttributes.RPC_SYSTEM, "grpc"), + equalTo(RpcIncubatingAttributes.RPC_SERVICE, "example.Greeter"), + equalTo(RpcIncubatingAttributes.RPC_METHOD, "SayHello"), equalTo( - SemanticAttributes.RPC_GRPC_STATUS_CODE, + RpcIncubatingAttributes.RPC_GRPC_STATUS_CODE, (long) Status.Code.OK.value()), - equalTo(SemanticAttributes.NET_PEER_NAME, "localhost"), - equalTo( - SemanticAttributes.NET_PEER_PORT, (long) server.getPort()))) + equalTo(ServerAttributes.SERVER_ADDRESS, "localhost"), + equalTo(ServerAttributes.SERVER_PORT, (long) server.getPort()))) .hasEventsSatisfyingExactly( event -> event .hasName("message") .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.MESSAGE_TYPE, "SENT"), - equalTo(SemanticAttributes.MESSAGE_ID, 1L)), + equalTo( + MessageIncubatingAttributes.MESSAGE_TYPE, "SENT"), + equalTo(MessageIncubatingAttributes.MESSAGE_ID, 1L)), event -> event .hasName("message") .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.MESSAGE_TYPE, "RECEIVED"), - equalTo(SemanticAttributes.MESSAGE_ID, 2L))), + equalTo( + MessageIncubatingAttributes.MESSAGE_TYPE, + "RECEIVED"), + equalTo(MessageIncubatingAttributes.MESSAGE_ID, 2L))), span -> span.hasName("example.Greeter/SayHello") .hasKind(SpanKind.SERVER) .hasParent(trace.getSpan(1)) .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.RPC_SYSTEM, "grpc"), - equalTo(SemanticAttributes.RPC_SERVICE, "example.Greeter"), - equalTo(SemanticAttributes.RPC_METHOD, "SayHello"), + equalTo(RpcIncubatingAttributes.RPC_SYSTEM, "grpc"), + equalTo(RpcIncubatingAttributes.RPC_SERVICE, "example.Greeter"), + equalTo(RpcIncubatingAttributes.RPC_METHOD, "SayHello"), equalTo( - SemanticAttributes.RPC_GRPC_STATUS_CODE, + RpcIncubatingAttributes.RPC_GRPC_STATUS_CODE, (long) Status.Code.OK.value()), - equalTo(SemanticAttributes.NET_HOST_NAME, "localhost"), - equalTo(SemanticAttributes.NET_HOST_PORT, server.getPort()), - equalTo(SemanticAttributes.NET_SOCK_PEER_ADDR, "127.0.0.1"), + equalTo(ServerAttributes.SERVER_ADDRESS, "localhost"), + equalTo(ServerAttributes.SERVER_PORT, server.getPort()), + equalTo(NetworkAttributes.NETWORK_TYPE, "ipv4"), + equalTo(NetworkAttributes.NETWORK_PEER_ADDRESS, "127.0.0.1"), satisfies( - SemanticAttributes.NET_SOCK_PEER_PORT, + NetworkAttributes.NETWORK_PEER_PORT, val -> assertThat(val).isNotNull())) .hasEventsSatisfyingExactly( event -> event .hasName("message") .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.MESSAGE_TYPE, "RECEIVED"), - equalTo(SemanticAttributes.MESSAGE_ID, 1L)), + equalTo( + MessageIncubatingAttributes.MESSAGE_TYPE, + "RECEIVED"), + equalTo(MessageIncubatingAttributes.MESSAGE_ID, 1L)), event -> event .hasName("message") .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.MESSAGE_TYPE, "SENT"), - equalTo(SemanticAttributes.MESSAGE_ID, 2L))))); + equalTo( + MessageIncubatingAttributes.MESSAGE_TYPE, "SENT"), + equalTo(MessageIncubatingAttributes.MESSAGE_ID, 2L))))); } @Test @@ -1113,28 +1161,28 @@ public void onCompleted() { .hasStatus(StatusData.error()) .hasAttributesSatisfyingExactly( addExtraClientAttributes( - equalTo(SemanticAttributes.RPC_SYSTEM, "grpc"), - equalTo(SemanticAttributes.RPC_SERVICE, "example.Greeter"), - equalTo(SemanticAttributes.RPC_METHOD, "SayMultipleHello"), + equalTo(RpcIncubatingAttributes.RPC_SYSTEM, "grpc"), + equalTo(RpcIncubatingAttributes.RPC_SERVICE, "example.Greeter"), + equalTo(RpcIncubatingAttributes.RPC_METHOD, "SayMultipleHello"), equalTo( - SemanticAttributes.RPC_GRPC_STATUS_CODE, + RpcIncubatingAttributes.RPC_GRPC_STATUS_CODE, (long) Status.Code.CANCELLED.value()), - equalTo(SemanticAttributes.NET_PEER_NAME, "localhost"), - equalTo( - SemanticAttributes.NET_PEER_PORT, (long) server.getPort()))) + equalTo(ServerAttributes.SERVER_ADDRESS, "localhost"), + equalTo(ServerAttributes.SERVER_PORT, (long) server.getPort()))) .hasEventsSatisfying( events -> { assertThat(events).hasSize(3); assertThat(events.get(0)) .hasName("message") .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.MESSAGE_TYPE, "SENT"), - equalTo(SemanticAttributes.MESSAGE_ID, 1L)); + equalTo(MessageIncubatingAttributes.MESSAGE_TYPE, "SENT"), + equalTo(MessageIncubatingAttributes.MESSAGE_ID, 1L)); assertThat(events.get(1)) .hasName("message") .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.MESSAGE_TYPE, "RECEIVED"), - equalTo(SemanticAttributes.MESSAGE_ID, 2L)); + equalTo( + MessageIncubatingAttributes.MESSAGE_TYPE, "RECEIVED"), + equalTo(MessageIncubatingAttributes.MESSAGE_ID, 2L)); span.hasException(thrown); }), span -> @@ -1142,31 +1190,35 @@ public void onCompleted() { .hasKind(SpanKind.SERVER) .hasParent(trace.getSpan(1)) .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.RPC_SYSTEM, "grpc"), - equalTo(SemanticAttributes.RPC_SERVICE, "example.Greeter"), - equalTo(SemanticAttributes.RPC_METHOD, "SayMultipleHello"), + equalTo(RpcIncubatingAttributes.RPC_SYSTEM, "grpc"), + equalTo(RpcIncubatingAttributes.RPC_SERVICE, "example.Greeter"), + equalTo(RpcIncubatingAttributes.RPC_METHOD, "SayMultipleHello"), equalTo( - SemanticAttributes.RPC_GRPC_STATUS_CODE, + RpcIncubatingAttributes.RPC_GRPC_STATUS_CODE, (long) Status.Code.CANCELLED.value()), - equalTo(SemanticAttributes.NET_HOST_NAME, "localhost"), - equalTo(SemanticAttributes.NET_HOST_PORT, server.getPort()), - equalTo(SemanticAttributes.NET_SOCK_PEER_ADDR, "127.0.0.1"), + equalTo(ServerAttributes.SERVER_ADDRESS, "localhost"), + equalTo(ServerAttributes.SERVER_PORT, server.getPort()), + equalTo(NetworkAttributes.NETWORK_TYPE, "ipv4"), + equalTo(NetworkAttributes.NETWORK_PEER_ADDRESS, "127.0.0.1"), satisfies( - SemanticAttributes.NET_SOCK_PEER_PORT, + NetworkAttributes.NETWORK_PEER_PORT, val -> assertThat(val).isNotNull())) .hasEventsSatisfyingExactly( event -> event .hasName("message") .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.MESSAGE_TYPE, "RECEIVED"), - equalTo(SemanticAttributes.MESSAGE_ID, 1L)), + equalTo( + MessageIncubatingAttributes.MESSAGE_TYPE, + "RECEIVED"), + equalTo(MessageIncubatingAttributes.MESSAGE_ID, 1L)), event -> event .hasName("message") .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.MESSAGE_TYPE, "SENT"), - equalTo(SemanticAttributes.MESSAGE_ID, 2L))))); + equalTo( + MessageIncubatingAttributes.MESSAGE_TYPE, "SENT"), + equalTo(MessageIncubatingAttributes.MESSAGE_ID, 2L))))); } @Test @@ -1228,63 +1280,70 @@ public void onCompleted() { .hasNoParent() .hasAttributesSatisfyingExactly( addExtraClientAttributes( - equalTo(SemanticAttributes.RPC_SYSTEM, "grpc"), + equalTo(RpcIncubatingAttributes.RPC_SYSTEM, "grpc"), equalTo( - SemanticAttributes.RPC_SERVICE, + RpcIncubatingAttributes.RPC_SERVICE, "grpc.reflection.v1alpha.ServerReflection"), - equalTo(SemanticAttributes.RPC_METHOD, "ServerReflectionInfo"), equalTo( - SemanticAttributes.RPC_GRPC_STATUS_CODE, - (long) Status.Code.OK.value()), - equalTo(SemanticAttributes.NET_PEER_NAME, "localhost"), + RpcIncubatingAttributes.RPC_METHOD, "ServerReflectionInfo"), equalTo( - SemanticAttributes.NET_PEER_PORT, (long) server.getPort()))) + RpcIncubatingAttributes.RPC_GRPC_STATUS_CODE, + (long) Status.Code.OK.value()), + equalTo(ServerAttributes.SERVER_ADDRESS, "localhost"), + equalTo(ServerAttributes.SERVER_PORT, (long) server.getPort()))) .hasEventsSatisfyingExactly( event -> event .hasName("message") .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.MESSAGE_TYPE, "SENT"), - equalTo(SemanticAttributes.MESSAGE_ID, 1L)), + equalTo( + MessageIncubatingAttributes.MESSAGE_TYPE, "SENT"), + equalTo(MessageIncubatingAttributes.MESSAGE_ID, 1L)), event -> event .hasName("message") .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.MESSAGE_TYPE, "RECEIVED"), - equalTo(SemanticAttributes.MESSAGE_ID, 2L))), + equalTo( + MessageIncubatingAttributes.MESSAGE_TYPE, + "RECEIVED"), + equalTo(MessageIncubatingAttributes.MESSAGE_ID, 2L))), span -> span.hasName( "grpc.reflection.v1alpha.ServerReflection/ServerReflectionInfo") .hasKind(SpanKind.SERVER) .hasParent(trace.getSpan(0)) .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.RPC_SYSTEM, "grpc"), + equalTo(RpcIncubatingAttributes.RPC_SYSTEM, "grpc"), equalTo( - SemanticAttributes.RPC_SERVICE, + RpcIncubatingAttributes.RPC_SERVICE, "grpc.reflection.v1alpha.ServerReflection"), - equalTo(SemanticAttributes.RPC_METHOD, "ServerReflectionInfo"), + equalTo(RpcIncubatingAttributes.RPC_METHOD, "ServerReflectionInfo"), equalTo( - SemanticAttributes.RPC_GRPC_STATUS_CODE, + RpcIncubatingAttributes.RPC_GRPC_STATUS_CODE, (long) Status.Code.OK.value()), - equalTo(SemanticAttributes.NET_HOST_NAME, "localhost"), - equalTo(SemanticAttributes.NET_HOST_PORT, server.getPort()), - equalTo(SemanticAttributes.NET_SOCK_PEER_ADDR, "127.0.0.1"), + equalTo(ServerAttributes.SERVER_ADDRESS, "localhost"), + equalTo(ServerAttributes.SERVER_PORT, server.getPort()), + equalTo(NetworkAttributes.NETWORK_TYPE, "ipv4"), + equalTo(NetworkAttributes.NETWORK_PEER_ADDRESS, "127.0.0.1"), satisfies( - SemanticAttributes.NET_SOCK_PEER_PORT, + NetworkAttributes.NETWORK_PEER_PORT, val -> assertThat(val).isNotNull())) .hasEventsSatisfyingExactly( event -> event .hasName("message") .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.MESSAGE_TYPE, "RECEIVED"), - equalTo(SemanticAttributes.MESSAGE_ID, 1L)), + equalTo( + MessageIncubatingAttributes.MESSAGE_TYPE, + "RECEIVED"), + equalTo(MessageIncubatingAttributes.MESSAGE_ID, 1L)), event -> event .hasName("message") .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.MESSAGE_TYPE, "SENT"), - equalTo(SemanticAttributes.MESSAGE_ID, 2L))))); + equalTo( + MessageIncubatingAttributes.MESSAGE_TYPE, "SENT"), + equalTo(MessageIncubatingAttributes.MESSAGE_ID, 2L))))); } @Test @@ -1334,58 +1393,64 @@ public void sayHello( .hasParent(trace.getSpan(0)) .hasAttributesSatisfyingExactly( addExtraClientAttributes( - equalTo(SemanticAttributes.RPC_SYSTEM, "grpc"), - equalTo(SemanticAttributes.RPC_SERVICE, "example.Greeter"), - equalTo(SemanticAttributes.RPC_METHOD, "SayHello"), + equalTo(RpcIncubatingAttributes.RPC_SYSTEM, "grpc"), + equalTo(RpcIncubatingAttributes.RPC_SERVICE, "example.Greeter"), + equalTo(RpcIncubatingAttributes.RPC_METHOD, "SayHello"), equalTo( - SemanticAttributes.RPC_GRPC_STATUS_CODE, + RpcIncubatingAttributes.RPC_GRPC_STATUS_CODE, (long) Status.Code.OK.value()), - equalTo(SemanticAttributes.NET_PEER_NAME, "localhost"), - equalTo( - SemanticAttributes.NET_PEER_PORT, (long) server.getPort()))) + equalTo(ServerAttributes.SERVER_ADDRESS, "localhost"), + equalTo(ServerAttributes.SERVER_PORT, (long) server.getPort()))) .hasEventsSatisfyingExactly( event -> event .hasName("message") .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.MESSAGE_TYPE, "SENT"), - equalTo(SemanticAttributes.MESSAGE_ID, 1L)), + equalTo( + MessageIncubatingAttributes.MESSAGE_TYPE, "SENT"), + equalTo(MessageIncubatingAttributes.MESSAGE_ID, 1L)), event -> event .hasName("message") .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.MESSAGE_TYPE, "RECEIVED"), - equalTo(SemanticAttributes.MESSAGE_ID, 2L))), + equalTo( + MessageIncubatingAttributes.MESSAGE_TYPE, + "RECEIVED"), + equalTo(MessageIncubatingAttributes.MESSAGE_ID, 2L))), span -> span.hasName("example.Greeter/SayHello") .hasKind(SpanKind.SERVER) .hasParent(trace.getSpan(1)) .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.RPC_SYSTEM, "grpc"), - equalTo(SemanticAttributes.RPC_SERVICE, "example.Greeter"), - equalTo(SemanticAttributes.RPC_METHOD, "SayHello"), + equalTo(RpcIncubatingAttributes.RPC_SYSTEM, "grpc"), + equalTo(RpcIncubatingAttributes.RPC_SERVICE, "example.Greeter"), + equalTo(RpcIncubatingAttributes.RPC_METHOD, "SayHello"), equalTo( - SemanticAttributes.RPC_GRPC_STATUS_CODE, + RpcIncubatingAttributes.RPC_GRPC_STATUS_CODE, (long) Status.Code.OK.value()), - equalTo(SemanticAttributes.NET_HOST_NAME, "localhost"), - equalTo(SemanticAttributes.NET_HOST_PORT, server.getPort()), - equalTo(SemanticAttributes.NET_SOCK_PEER_ADDR, "127.0.0.1"), + equalTo(ServerAttributes.SERVER_ADDRESS, "localhost"), + equalTo(ServerAttributes.SERVER_PORT, server.getPort()), + equalTo(NetworkAttributes.NETWORK_TYPE, "ipv4"), + equalTo(NetworkAttributes.NETWORK_PEER_ADDRESS, "127.0.0.1"), satisfies( - SemanticAttributes.NET_SOCK_PEER_PORT, + NetworkAttributes.NETWORK_PEER_PORT, val -> assertThat(val).isNotNull())) .hasEventsSatisfyingExactly( event -> event .hasName("message") .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.MESSAGE_TYPE, "RECEIVED"), - equalTo(SemanticAttributes.MESSAGE_ID, 1L)), + equalTo( + MessageIncubatingAttributes.MESSAGE_TYPE, + "RECEIVED"), + equalTo(MessageIncubatingAttributes.MESSAGE_ID, 1L)), event -> event .hasName("message") .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.MESSAGE_TYPE, "SENT"), - equalTo(SemanticAttributes.MESSAGE_ID, 2L))))); + equalTo( + MessageIncubatingAttributes.MESSAGE_TYPE, "SENT"), + equalTo(MessageIncubatingAttributes.MESSAGE_ID, 2L))))); } // Regression test for @@ -1469,6 +1534,70 @@ public void sayHello( assertThat(error).hasValue(null); } + // Regression test for + // https://github.com/open-telemetry/opentelemetry-java-instrumentation/issues/8923 + @Test + void cancelListenerCalled() throws Exception { + CountDownLatch startLatch = new CountDownLatch(1); + CountDownLatch cancelLatch = new CountDownLatch(1); + AtomicBoolean cancelCalled = new AtomicBoolean(); + + Server server = + configureServer( + ServerBuilder.forPort(0) + .addService( + new GreeterGrpc.GreeterImplBase() { + @Override + public void sayHello( + Helloworld.Request request, + StreamObserver responseObserver) { + startLatch.countDown(); + + io.grpc.Context context = io.grpc.Context.current(); + context.addListener( + context1 -> cancelCalled.set(true), MoreExecutors.directExecutor()); + try { + cancelLatch.await(10, TimeUnit.SECONDS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + responseObserver.onNext( + Helloworld.Response.newBuilder() + .setMessage(request.getName()) + .build()); + responseObserver.onCompleted(); + } + })) + .build() + .start(); + ManagedChannel channel = createChannel(server); + closer.add(() -> channel.shutdownNow().awaitTermination(10, TimeUnit.SECONDS)); + closer.add(() -> server.shutdownNow().awaitTermination()); + + GreeterGrpc.GreeterFutureStub client = GreeterGrpc.newFutureStub(channel); + ListenableFuture future = + client.sayHello(Helloworld.Request.newBuilder().setName("test").build()); + + startLatch.await(10, TimeUnit.SECONDS); + future.cancel(false); + cancelLatch.countDown(); + + testing() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("example.Greeter/SayHello") + .hasKind(SpanKind.CLIENT) + .hasNoParent(), + span -> + span.hasName("example.Greeter/SayHello") + .hasKind(SpanKind.SERVER) + .hasParent(trace.getSpan(0)))); + + assertThat(cancelCalled.get()).isEqualTo(true); + } + @Test void setCapturedRequestMetadata() throws Exception { String metadataAttributePrefix = "rpc.grpc.request.metadata."; @@ -1513,7 +1642,7 @@ public void sayHello( "parent", () -> client.sayHello(Helloworld.Request.newBuilder().setName("test").build())); - OpenTelemetryAssertions.assertThat(response.getMessage()).isEqualTo("Hello test"); + assertThat(response.getMessage()).isEqualTo("Hello test"); testing() .waitAndAssertTraces( @@ -1562,7 +1691,10 @@ static List addExtraClientAttributes(AttributeAssertion... a List result = new ArrayList<>(); result.addAll(Arrays.asList(assertions)); if (Boolean.getBoolean("testLatestDeps")) { - result.add(equalTo(SemanticAttributes.NET_SOCK_PEER_ADDR, "127.0.0.1")); + result.add(equalTo(NetworkAttributes.NETWORK_TYPE, "ipv4")); + result.add(equalTo(NetworkAttributes.NETWORK_PEER_ADDRESS, "127.0.0.1")); + result.add( + satisfies(NetworkAttributes.NETWORK_PEER_PORT, val -> assertThat(val).isNotNull())); } return result; } diff --git a/instrumentation/guava-10.0/README.md b/instrumentation/guava-10.0/README.md index 9a6e8afbb5bc..6292d56a53e3 100644 --- a/instrumentation/guava-10.0/README.md +++ b/instrumentation/guava-10.0/README.md @@ -1,5 +1,5 @@ # Settings for the Guava instrumentation -| System property | Type | Default | Description | -|---|---|---|---| +| System property | Type | Default | Description | +| --------------------------------------------------------- | ------- | ------- | --------------------------------------------------- | | `otel.instrumentation.guava.experimental-span-attributes` | Boolean | `false` | Enable the capture of experimental span attributes. | diff --git a/instrumentation/guava-10.0/javaagent/build.gradle.kts b/instrumentation/guava-10.0/javaagent/build.gradle.kts index e4957cd26c75..5898b7e846a7 100644 --- a/instrumentation/guava-10.0/javaagent/build.gradle.kts +++ b/instrumentation/guava-10.0/javaagent/build.gradle.kts @@ -7,6 +7,7 @@ muzzle { group.set("com.google.guava") module.set("guava") versions.set("[10.0,]") + skip("32.1.0-android") assertInverse.set(true) } } diff --git a/instrumentation/guava-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/guava/v10_0/GuavaListenableFutureInstrumentation.java b/instrumentation/guava-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/guava/v10_0/GuavaListenableFutureInstrumentation.java index 6418a348aff9..77e6b136a22f 100644 --- a/instrumentation/guava-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/guava/v10_0/GuavaListenableFutureInstrumentation.java +++ b/instrumentation/guava-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/guava/v10_0/GuavaListenableFutureInstrumentation.java @@ -49,8 +49,7 @@ public static void onConstruction() { public static class AddListenerAdvice { @Advice.OnMethodEnter(suppress = Throwable.class) - public static PropagatedContext addListenerEnter( - @Advice.Argument(value = 0, readOnly = false) Runnable task) { + public static PropagatedContext addListenerEnter(@Advice.Argument(0) Runnable task) { Context context = Java8BytecodeBridge.currentContext(); if (ExecutorAdviceHelper.shouldPropagateContext(context, task)) { VirtualField virtualField = diff --git a/instrumentation/guava-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/guava/v10_0/InstrumentationHelper.java b/instrumentation/guava-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/guava/v10_0/InstrumentationHelper.java index 9d15d8311d28..d1d8bbc637f0 100644 --- a/instrumentation/guava-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/guava/v10_0/InstrumentationHelper.java +++ b/instrumentation/guava-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/guava/v10_0/InstrumentationHelper.java @@ -7,14 +7,14 @@ import io.opentelemetry.instrumentation.api.annotation.support.async.AsyncOperationEndStrategies; import io.opentelemetry.instrumentation.guava.v10_0.GuavaAsyncOperationEndStrategy; -import io.opentelemetry.javaagent.bootstrap.internal.InstrumentationConfig; +import io.opentelemetry.javaagent.bootstrap.internal.AgentInstrumentationConfig; public final class InstrumentationHelper { static { asyncOperationEndStrategy = GuavaAsyncOperationEndStrategy.builder() .setCaptureExperimentalSpanAttributes( - InstrumentationConfig.get() + AgentInstrumentationConfig.get() .getBoolean("otel.instrumentation.guava.experimental-span-attributes", false)) .build(); diff --git a/instrumentation/gwt-2.0/javaagent/build.gradle.kts b/instrumentation/gwt-2.0/javaagent/build.gradle.kts index c1f58efd2724..7a94b9010330 100644 --- a/instrumentation/gwt-2.0/javaagent/build.gradle.kts +++ b/instrumentation/gwt-2.0/javaagent/build.gradle.kts @@ -21,7 +21,7 @@ muzzle { sourceSets { create("testapp") { java { - destinationDirectory.set(file("$buildDir/testapp/classes")) + destinationDirectory.set(layout.buildDirectory.dir("testapp/classes")) } resources { srcDirs("src/webapp") @@ -54,7 +54,7 @@ dependencies { testImplementation("org.eclipse.jetty:jetty-webapp:9.4.35.v20201120") } -val warDir = file("$buildDir/testapp/war") +val warDir = layout.buildDirectory.dir("testapp/war") val launcher = javaToolchains.launcherFor { languageVersion.set(JavaLanguageVersion.of(8)) @@ -62,13 +62,15 @@ val launcher = javaToolchains.launcherFor { class CompilerArgumentsProvider : CommandLineArgumentProvider { override fun asArguments(): Iterable = listOf( - "test.gwt.Greeting", // gwt module - "-war", "$buildDir/testapp/war", + // gwt module + "test.gwt.Greeting", + "-war", layout.buildDirectory.dir("testapp/war").get().asFile.absolutePath, "-logLevel", "INFO", "-localWorkers", "2", "-compileReport", - "-extra", "$buildDir/testapp/extra", - "-draftCompile", // makes compile a bit faster + "-extra", layout.buildDirectory.dir("testapp/extra").get().asFile.absolutePath, + // makes compile a bit faster + "-draftCompile", ) } @@ -94,7 +96,7 @@ tasks { from(file("src/testapp/webapp")) from(warDir) - into(file("$buildDir/testapp/web")) + into(file(layout.buildDirectory.dir("testapp/web"))) } test { @@ -102,7 +104,7 @@ tasks { dependsOn(copyTestWebapp) // add test app classes to classpath - classpath = sourceSets.test.get().runtimeClasspath.plus(files("$buildDir/testapp/classes")) + classpath = sourceSets.test.get().runtimeClasspath.plus(files(layout.buildDirectory.dir("testapp/classes"))) usesService(gradle.sharedServices.registrations["testcontainersBuildService"].service) } diff --git a/instrumentation/gwt-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/gwt/GwtRpcAttributesGetter.java b/instrumentation/gwt-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/gwt/GwtRpcAttributesGetter.java index c9d0685773ab..3ebef129f191 100644 --- a/instrumentation/gwt-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/gwt/GwtRpcAttributesGetter.java +++ b/instrumentation/gwt-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/gwt/GwtRpcAttributesGetter.java @@ -5,7 +5,7 @@ package io.opentelemetry.javaagent.instrumentation.gwt; -import io.opentelemetry.instrumentation.api.instrumenter.rpc.RpcAttributesGetter; +import io.opentelemetry.instrumentation.api.incubator.semconv.rpc.RpcAttributesGetter; import java.lang.reflect.Method; enum GwtRpcAttributesGetter implements RpcAttributesGetter { diff --git a/instrumentation/gwt-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/gwt/GwtSingletons.java b/instrumentation/gwt-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/gwt/GwtSingletons.java index b13de9f52ce0..cbf9b83bd5fd 100644 --- a/instrumentation/gwt-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/gwt/GwtSingletons.java +++ b/instrumentation/gwt-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/gwt/GwtSingletons.java @@ -7,10 +7,10 @@ import io.opentelemetry.api.GlobalOpenTelemetry; import io.opentelemetry.context.ContextKey; +import io.opentelemetry.instrumentation.api.incubator.semconv.rpc.RpcServerAttributesExtractor; +import io.opentelemetry.instrumentation.api.incubator.semconv.rpc.RpcSpanNameExtractor; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; import io.opentelemetry.instrumentation.api.instrumenter.SpanKindExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.rpc.RpcServerAttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.rpc.RpcSpanNameExtractor; import java.lang.reflect.Method; public final class GwtSingletons { diff --git a/instrumentation/gwt-2.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/gwt/GwtTest.java b/instrumentation/gwt-2.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/gwt/GwtTest.java index d8066d47b4e1..711d627c3ddb 100644 --- a/instrumentation/gwt-2.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/gwt/GwtTest.java +++ b/instrumentation/gwt-2.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/gwt/GwtTest.java @@ -13,7 +13,7 @@ import io.opentelemetry.instrumentation.test.utils.PortUtils; import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.opentelemetry.semconv.incubating.RpcIncubatingAttributes; import java.io.File; import java.io.IOException; import java.net.URI; @@ -147,23 +147,21 @@ void testGwt() { testing.waitAndAssertTraces( trace -> - trace - .hasSize(2) - .hasSpansSatisfyingExactly( - span -> - span.hasName("POST " + getContextPath() + "/greeting/greet") - .hasKind(SpanKind.SERVER) - .hasNoParent(), - span -> - span.hasName("test.gwt.shared.MessageService/sendMessage") - .hasKind(SpanKind.SERVER) - .hasParent(trace.getSpan(0)) - .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.RPC_SYSTEM, "gwt"), - equalTo( - SemanticAttributes.RPC_SERVICE, - "test.gwt.shared.MessageService"), - equalTo(SemanticAttributes.RPC_METHOD, "sendMessage")))); + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("POST " + getContextPath() + "/greeting/greet") + .hasKind(SpanKind.SERVER) + .hasNoParent(), + span -> + span.hasName("test.gwt.shared.MessageService/sendMessage") + .hasKind(SpanKind.SERVER) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo(RpcIncubatingAttributes.RPC_SYSTEM, "gwt"), + equalTo( + RpcIncubatingAttributes.RPC_SERVICE, + "test.gwt.shared.MessageService"), + equalTo(RpcIncubatingAttributes.RPC_METHOD, "sendMessage")))); testing.clearData(); @@ -173,24 +171,22 @@ void testGwt() { testing.waitAndAssertTraces( trace -> - trace - .hasSize(2) - .hasSpansSatisfyingExactly( - span -> - span.hasName("POST " + getContextPath() + "/greeting/greet") - .hasKind(SpanKind.SERVER) - .hasNoParent(), - span -> - span.hasName("test.gwt.shared.MessageService/sendMessage") - .hasKind(SpanKind.SERVER) - .hasParent(trace.getSpan(0)) - .hasException(new IOException()) - .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.RPC_SYSTEM, "gwt"), - equalTo( - SemanticAttributes.RPC_SERVICE, - "test.gwt.shared.MessageService"), - equalTo(SemanticAttributes.RPC_METHOD, "sendMessage")))); + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("POST " + getContextPath() + "/greeting/greet") + .hasKind(SpanKind.SERVER) + .hasNoParent(), + span -> + span.hasName("test.gwt.shared.MessageService/sendMessage") + .hasKind(SpanKind.SERVER) + .hasParent(trace.getSpan(0)) + .hasException(new IOException()) + .hasAttributesSatisfyingExactly( + equalTo(RpcIncubatingAttributes.RPC_SYSTEM, "gwt"), + equalTo( + RpcIncubatingAttributes.RPC_SERVICE, + "test.gwt.shared.MessageService"), + equalTo(RpcIncubatingAttributes.RPC_METHOD, "sendMessage")))); driver.close(); } diff --git a/instrumentation/gwt-2.0/javaagent/src/testapp/webapp/greeting.html b/instrumentation/gwt-2.0/javaagent/src/testapp/webapp/greeting.html index acd563c8cafd..26132ef45f65 100644 --- a/instrumentation/gwt-2.0/javaagent/src/testapp/webapp/greeting.html +++ b/instrumentation/gwt-2.0/javaagent/src/testapp/webapp/greeting.html @@ -1,13 +1,17 @@ - - -Example + + + Example - - - -

-

- - \ No newline at end of file + + + +

+

+ + diff --git a/instrumentation/hibernate/README.md b/instrumentation/hibernate/README.md index 3a7428682976..726c930431e6 100644 --- a/instrumentation/hibernate/README.md +++ b/instrumentation/hibernate/README.md @@ -1,5 +1,5 @@ # Settings for the Hibernate instrumentation -| System property | Type | Default | Description | -|---|---|---|---| +| System property | Type | Default | Description | +| ------------------------------------------------------------- | ------- | ------- | --------------------------------------------------- | | `otel.instrumentation.hibernate.experimental-span-attributes` | Boolean | `false` | Enable the capture of experimental span attributes. | diff --git a/instrumentation/hibernate/hibernate-3.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/v3_3/CriteriaInstrumentation.java b/instrumentation/hibernate/hibernate-3.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/v3_3/CriteriaInstrumentation.java index e995738cd723..962d82fea4b3 100644 --- a/instrumentation/hibernate/hibernate-3.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/v3_3/CriteriaInstrumentation.java +++ b/instrumentation/hibernate/hibernate-3.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/v3_3/CriteriaInstrumentation.java @@ -13,13 +13,12 @@ import static net.bytebuddy.matcher.ElementMatchers.namedOneOf; import io.opentelemetry.context.Context; -import io.opentelemetry.context.Scope; import io.opentelemetry.instrumentation.api.util.VirtualField; -import io.opentelemetry.javaagent.bootstrap.CallDepth; import io.opentelemetry.javaagent.bootstrap.Java8BytecodeBridge; import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; import io.opentelemetry.javaagent.instrumentation.hibernate.HibernateOperation; +import io.opentelemetry.javaagent.instrumentation.hibernate.HibernateOperationScope; import io.opentelemetry.javaagent.instrumentation.hibernate.SessionInfo; import net.bytebuddy.asm.Advice; import net.bytebuddy.description.type.TypeDescription; @@ -50,17 +49,11 @@ public void transform(TypeTransformer transformer) { public static class CriteriaMethodAdvice { @Advice.OnMethodEnter(suppress = Throwable.class) - public static void startMethod( - @Advice.This Criteria criteria, - @Advice.Origin("#m") String name, - @Advice.Local("otelCallDepth") CallDepth callDepth, - @Advice.Local("otelHibernateOperation") HibernateOperation hibernateOperation, - @Advice.Local("otelContext") Context context, - @Advice.Local("otelScope") Scope scope) { - - callDepth = CallDepth.forClass(HibernateOperation.class); - if (callDepth.getAndIncrement() > 0) { - return; + public static HibernateOperationScope startMethod( + @Advice.This Criteria criteria, @Advice.Origin("#m") String name) { + + if (HibernateOperationScope.enterDepthSkipCheck()) { + return null; } String entityName = null; @@ -73,31 +66,17 @@ public static void startMethod( SessionInfo sessionInfo = criteriaVirtualField.get(criteria); Context parentContext = Java8BytecodeBridge.currentContext(); - hibernateOperation = new HibernateOperation("Criteria." + name, entityName, sessionInfo); - if (!instrumenter().shouldStart(parentContext, hibernateOperation)) { - return; - } + HibernateOperation hibernateOperation = + new HibernateOperation("Criteria." + name, entityName, sessionInfo); - context = instrumenter().start(parentContext, hibernateOperation); - scope = context.makeCurrent(); + return HibernateOperationScope.start(hibernateOperation, parentContext, instrumenter()); } @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) public static void endMethod( - @Advice.Thrown Throwable throwable, - @Advice.Local("otelCallDepth") CallDepth callDepth, - @Advice.Local("otelHibernateOperation") HibernateOperation hibernateOperation, - @Advice.Local("otelContext") Context context, - @Advice.Local("otelScope") Scope scope) { - - if (callDepth.decrementAndGet() > 0) { - return; - } + @Advice.Thrown Throwable throwable, @Advice.Enter HibernateOperationScope scope) { - if (scope != null) { - scope.close(); - instrumenter().end(context, hibernateOperation, null, throwable); - } + HibernateOperationScope.end(scope, throwable); } } } diff --git a/instrumentation/hibernate/hibernate-3.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/v3_3/HibernateInstrumentationModule.java b/instrumentation/hibernate/hibernate-3.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/v3_3/HibernateInstrumentationModule.java index d9f6d5a919ac..8a9dd3a82040 100644 --- a/instrumentation/hibernate/hibernate-3.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/v3_3/HibernateInstrumentationModule.java +++ b/instrumentation/hibernate/hibernate-3.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/v3_3/HibernateInstrumentationModule.java @@ -11,11 +11,13 @@ 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.internal.ExperimentalInstrumentationModule; import java.util.List; import net.bytebuddy.matcher.ElementMatcher; @AutoService(InstrumentationModule.class) -public class HibernateInstrumentationModule extends InstrumentationModule { +public class HibernateInstrumentationModule extends InstrumentationModule + implements ExperimentalInstrumentationModule { public HibernateInstrumentationModule() { super("hibernate", "hibernate-3.3"); @@ -30,6 +32,11 @@ public ElementMatcher.Junction classLoaderMatcher() { "org.hibernate.transaction.JBossTransactionManagerLookup"); } + @Override + public String getModuleGroup() { + return "hibernate"; + } + @Override public List typeInstrumentations() { return asList( diff --git a/instrumentation/hibernate/hibernate-3.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/v3_3/QueryInstrumentation.java b/instrumentation/hibernate/hibernate-3.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/v3_3/QueryInstrumentation.java index 5a9742dcbcd0..e978128b4b22 100644 --- a/instrumentation/hibernate/hibernate-3.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/v3_3/QueryInstrumentation.java +++ b/instrumentation/hibernate/hibernate-3.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/v3_3/QueryInstrumentation.java @@ -14,13 +14,12 @@ import static net.bytebuddy.matcher.ElementMatchers.namedOneOf; import io.opentelemetry.context.Context; -import io.opentelemetry.context.Scope; import io.opentelemetry.instrumentation.api.util.VirtualField; -import io.opentelemetry.javaagent.bootstrap.CallDepth; import io.opentelemetry.javaagent.bootstrap.Java8BytecodeBridge; import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; import io.opentelemetry.javaagent.instrumentation.hibernate.HibernateOperation; +import io.opentelemetry.javaagent.instrumentation.hibernate.HibernateOperationScope; import io.opentelemetry.javaagent.instrumentation.hibernate.SessionInfo; import net.bytebuddy.asm.Advice; import net.bytebuddy.description.type.TypeDescription; @@ -50,16 +49,10 @@ public void transform(TypeTransformer transformer) { public static class QueryMethodAdvice { @Advice.OnMethodEnter(suppress = Throwable.class) - public static void startMethod( - @Advice.This Query query, - @Advice.Local("otelCallDepth") CallDepth callDepth, - @Advice.Local("otelHibernateOperation") HibernateOperation hibernateOperation, - @Advice.Local("otelContext") Context context, - @Advice.Local("otelScope") Scope scope) { + public static HibernateOperationScope startMethod(@Advice.This Query query) { - callDepth = CallDepth.forClass(HibernateOperation.class); - if (callDepth.getAndIncrement() > 0) { - return; + if (HibernateOperationScope.enterDepthSkipCheck()) { + return null; } VirtualField queryVirtualField = @@ -67,32 +60,17 @@ public static void startMethod( SessionInfo sessionInfo = queryVirtualField.get(query); Context parentContext = Java8BytecodeBridge.currentContext(); - hibernateOperation = + HibernateOperation hibernateOperation = new HibernateOperation(getOperationNameForQuery(query.getQueryString()), sessionInfo); - if (!instrumenter().shouldStart(parentContext, hibernateOperation)) { - return; - } - context = instrumenter().start(parentContext, hibernateOperation); - scope = context.makeCurrent(); + return HibernateOperationScope.start(hibernateOperation, parentContext, instrumenter()); } @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) public static void endMethod( - @Advice.Thrown Throwable throwable, - @Advice.Local("otelCallDepth") CallDepth callDepth, - @Advice.Local("otelHibernateOperation") HibernateOperation hibernateOperation, - @Advice.Local("otelContext") Context context, - @Advice.Local("otelScope") Scope scope) { - - if (callDepth.decrementAndGet() > 0) { - return; - } + @Advice.Thrown Throwable throwable, @Advice.Enter HibernateOperationScope scope) { - if (scope != null) { - scope.close(); - instrumenter().end(context, hibernateOperation, null, throwable); - } + HibernateOperationScope.end(scope, throwable); } } } diff --git a/instrumentation/hibernate/hibernate-3.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/v3_3/SessionInstrumentation.java b/instrumentation/hibernate/hibernate-3.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/v3_3/SessionInstrumentation.java index 8cdefda3d53d..b01cd3b5bc7a 100644 --- a/instrumentation/hibernate/hibernate-3.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/v3_3/SessionInstrumentation.java +++ b/instrumentation/hibernate/hibernate-3.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/v3_3/SessionInstrumentation.java @@ -18,13 +18,12 @@ import static net.bytebuddy.matcher.ElementMatchers.takesArgument; import io.opentelemetry.context.Context; -import io.opentelemetry.context.Scope; import io.opentelemetry.instrumentation.api.util.VirtualField; -import io.opentelemetry.javaagent.bootstrap.CallDepth; import io.opentelemetry.javaagent.bootstrap.Java8BytecodeBridge; import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; import io.opentelemetry.javaagent.instrumentation.hibernate.HibernateOperation; +import io.opentelemetry.javaagent.instrumentation.hibernate.HibernateOperationScope; import io.opentelemetry.javaagent.instrumentation.hibernate.SessionInfo; import net.bytebuddy.asm.Advice; import net.bytebuddy.description.type.TypeDescription; @@ -92,52 +91,32 @@ public void transform(TypeTransformer transformer) { public static class SessionMethodAdvice { @Advice.OnMethodEnter(suppress = Throwable.class) - public static void startMethod( + public static HibernateOperationScope startMethod( @Advice.This Object session, @Advice.Origin("#m") String name, @Advice.Origin("#d") String descriptor, @Advice.Argument(0) Object arg0, - @Advice.Argument(value = 1, optional = true) Object arg1, - @Advice.Local("otelCallDepth") CallDepth callDepth, - @Advice.Local("otelHibernateOperation") HibernateOperation hibernateOperation, - @Advice.Local("otelContext") Context context, - @Advice.Local("otelScope") Scope scope) { - - callDepth = CallDepth.forClass(HibernateOperation.class); - if (callDepth.getAndIncrement() > 0) { - return; + @Advice.Argument(value = 1, optional = true) Object arg1) { + + if (HibernateOperationScope.enterDepthSkipCheck()) { + return null; } Context parentContext = Java8BytecodeBridge.currentContext(); SessionInfo sessionInfo = SessionUtil.getSessionInfo(session); String entityName = getEntityName(descriptor, arg0, arg1, EntityNameUtil.bestGuessEntityName(session)); - hibernateOperation = + HibernateOperation hibernateOperation = new HibernateOperation(getSessionMethodOperationName(name), entityName, sessionInfo); - if (!instrumenter().shouldStart(parentContext, hibernateOperation)) { - return; - } - context = instrumenter().start(parentContext, hibernateOperation); - scope = context.makeCurrent(); + return HibernateOperationScope.start(hibernateOperation, parentContext, instrumenter()); } @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) public static void endMethod( - @Advice.Thrown Throwable throwable, - @Advice.Local("otelCallDepth") CallDepth callDepth, - @Advice.Local("otelHibernateOperation") HibernateOperation hibernateOperation, - @Advice.Local("otelContext") Context context, - @Advice.Local("otelScope") Scope scope) { - - if (callDepth.decrementAndGet() > 0) { - return; - } + @Advice.Thrown Throwable throwable, @Advice.Enter HibernateOperationScope enterScope) { - if (scope != null) { - scope.close(); - instrumenter().end(context, hibernateOperation, null, throwable); - } + HibernateOperationScope.end(enterScope, throwable); } } diff --git a/instrumentation/hibernate/hibernate-3.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/v3_3/TransactionInstrumentation.java b/instrumentation/hibernate/hibernate-3.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/v3_3/TransactionInstrumentation.java index c9f8631f54b8..4004151e1cd2 100644 --- a/instrumentation/hibernate/hibernate-3.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/v3_3/TransactionInstrumentation.java +++ b/instrumentation/hibernate/hibernate-3.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/v3_3/TransactionInstrumentation.java @@ -13,13 +13,12 @@ import static net.bytebuddy.matcher.ElementMatchers.takesArguments; import io.opentelemetry.context.Context; -import io.opentelemetry.context.Scope; import io.opentelemetry.instrumentation.api.util.VirtualField; -import io.opentelemetry.javaagent.bootstrap.CallDepth; import io.opentelemetry.javaagent.bootstrap.Java8BytecodeBridge; import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; import io.opentelemetry.javaagent.instrumentation.hibernate.HibernateOperation; +import io.opentelemetry.javaagent.instrumentation.hibernate.HibernateOperationScope; import io.opentelemetry.javaagent.instrumentation.hibernate.SessionInfo; import net.bytebuddy.asm.Advice; import net.bytebuddy.description.type.TypeDescription; @@ -49,16 +48,10 @@ public void transform(TypeTransformer transformer) { public static class TransactionCommitAdvice { @Advice.OnMethodEnter(suppress = Throwable.class) - public static void startCommit( - @Advice.This Transaction transaction, - @Advice.Local("otelCallDepth") CallDepth callDepth, - @Advice.Local("otelHibernateOperation") HibernateOperation hibernateOperation, - @Advice.Local("otelContext") Context context, - @Advice.Local("otelScope") Scope scope) { + public static HibernateOperationScope startCommit(@Advice.This Transaction transaction) { - callDepth = CallDepth.forClass(HibernateOperation.class); - if (callDepth.getAndIncrement() > 0) { - return; + if (HibernateOperationScope.enterDepthSkipCheck()) { + return null; } VirtualField transactionVirtualField = @@ -66,31 +59,17 @@ public static void startCommit( SessionInfo sessionInfo = transactionVirtualField.get(transaction); Context parentContext = Java8BytecodeBridge.currentContext(); - hibernateOperation = new HibernateOperation("Transaction.commit", sessionInfo); - if (!instrumenter().shouldStart(parentContext, hibernateOperation)) { - return; - } + HibernateOperation hibernateOperation = + new HibernateOperation("Transaction.commit", sessionInfo); - context = instrumenter().start(parentContext, hibernateOperation); - scope = context.makeCurrent(); + return HibernateOperationScope.start(hibernateOperation, parentContext, instrumenter()); } @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) public static void endCommit( - @Advice.Thrown Throwable throwable, - @Advice.Local("otelCallDepth") CallDepth callDepth, - @Advice.Local("otelHibernateOperation") HibernateOperation hibernateOperation, - @Advice.Local("otelContext") Context context, - @Advice.Local("otelScope") Scope scope) { - - if (callDepth.decrementAndGet() > 0) { - return; - } + @Advice.Thrown Throwable throwable, @Advice.Enter HibernateOperationScope scope) { - if (scope != null) { - scope.close(); - instrumenter().end(context, hibernateOperation, null, throwable); - } + HibernateOperationScope.end(scope, throwable); } } } diff --git a/instrumentation/hibernate/hibernate-3.3/javaagent/src/test/groovy/AbstractHibernateTest.groovy b/instrumentation/hibernate/hibernate-3.3/javaagent/src/test/groovy/AbstractHibernateTest.groovy deleted file mode 100644 index 10bd08ba5339..000000000000 --- a/instrumentation/hibernate/hibernate-3.3/javaagent/src/test/groovy/AbstractHibernateTest.groovy +++ /dev/null @@ -1,40 +0,0 @@ -/* - * 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/instrumentation/hibernate/hibernate-3.3/javaagent/src/test/groovy/CriteriaTest.groovy b/instrumentation/hibernate/hibernate-3.3/javaagent/src/test/groovy/CriteriaTest.groovy deleted file mode 100644 index 39289c20fa5c..000000000000 --- a/instrumentation/hibernate/hibernate-3.3/javaagent/src/test/groovy/CriteriaTest.groovy +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -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 - -import static io.opentelemetry.api.trace.SpanKind.CLIENT -import static io.opentelemetry.api.trace.SpanKind.INTERNAL - -class CriteriaTest extends AbstractHibernateTest { - - def "test criteria.#methodName"() { - setup: - runWithSpan("parent") { - 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: - def sessionId - assertTraces(1) { - trace(0, 4) { - span(0) { - name "parent" - kind INTERNAL - hasNoParent() - attributes { - } - } - span(1) { - name "Criteria.$methodName Value" - kind INTERNAL - childOf span(0) - attributes { - "hibernate.session_id" { - sessionId = it - it instanceof String - } - } - } - span(2) { - name "SELECT db1.Value" - kind CLIENT - childOf span(1) - attributes { - "$SemanticAttributes.DB_SYSTEM" "h2" - "$SemanticAttributes.DB_NAME" "db1" - "$SemanticAttributes.DB_USER" "sa" - "$SemanticAttributes.DB_CONNECTION_STRING" "h2:mem:" - "$SemanticAttributes.DB_STATEMENT" ~/^select / - "$SemanticAttributes.DB_OPERATION" "SELECT" - "$SemanticAttributes.DB_SQL_TABLE" "Value" - } - } - span(3) { - name "Transaction.commit" - kind INTERNAL - childOf span(0) - attributes { - "hibernate.session_id" sessionId - } - } - } - } - - where: - methodName | interaction - "list" | { c -> c.list() } - "uniqueResult" | { c -> c.uniqueResult() } - "scroll" | { c -> c.scroll() } - } -} diff --git a/instrumentation/hibernate/hibernate-3.3/javaagent/src/test/groovy/QueryTest.groovy b/instrumentation/hibernate/hibernate-3.3/javaagent/src/test/groovy/QueryTest.groovy deleted file mode 100644 index 1602c4cd4916..000000000000 --- a/instrumentation/hibernate/hibernate-3.3/javaagent/src/test/groovy/QueryTest.groovy +++ /dev/null @@ -1,206 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes -import org.hibernate.Query -import org.hibernate.Session - -import static io.opentelemetry.api.trace.SpanKind.CLIENT -import static io.opentelemetry.api.trace.SpanKind.INTERNAL - -class QueryTest extends AbstractHibernateTest { - - def "test hibernate query.#queryMethodName single call"() { - setup: - - // With Transaction - runWithSpan("parent") { - Session session = sessionFactory.openSession() - session.beginTransaction() - queryInteraction(session) - session.getTransaction().commit() - session.close() - } - - // Without Transaction - if (!requiresTransaction) { - runWithSpan("parent2") { - Session session = sessionFactory.openSession() - queryInteraction(session) - session.close() - } - } - - expect: - def sessionId - assertTraces(requiresTransaction ? 1 : 2) { - // With Transaction - trace(0, 4) { - span(0) { - name "parent" - kind INTERNAL - hasNoParent() - attributes { - } - } - span(1) { - name expectedSpanName - kind INTERNAL - childOf span(0) - attributes { - "hibernate.session_id" { - sessionId = it - it instanceof String - } - } - } - span(2) { - kind CLIENT - childOf span(1) - attributes { - "$SemanticAttributes.DB_SYSTEM" "h2" - "$SemanticAttributes.DB_NAME" "db1" - "$SemanticAttributes.DB_USER" "sa" - "$SemanticAttributes.DB_CONNECTION_STRING" "h2:mem:" - "$SemanticAttributes.DB_STATEMENT" String - "$SemanticAttributes.DB_OPERATION" String - "$SemanticAttributes.DB_SQL_TABLE" "Value" - } - } - span(3) { - name "Transaction.commit" - kind INTERNAL - childOf span(0) - attributes { - "hibernate.session_id" sessionId - } - } - } - if (!requiresTransaction) { - // Without Transaction - trace(1, 3) { - span(0) { - name "parent2" - kind INTERNAL - hasNoParent() - attributes { - } - } - span(1) { - name expectedSpanName - kind INTERNAL - childOf span(0) - attributes { - "hibernate.session_id" String - } - } - span(2) { - name "SELECT db1.Value" - kind CLIENT - childOf span(1) - attributes { - "$SemanticAttributes.DB_SYSTEM" "h2" - "$SemanticAttributes.DB_NAME" "db1" - "$SemanticAttributes.DB_USER" "sa" - "$SemanticAttributes.DB_CONNECTION_STRING" "h2:mem:" - "$SemanticAttributes.DB_STATEMENT" ~/^select / - "$SemanticAttributes.DB_OPERATION" "SELECT" - "$SemanticAttributes.DB_SQL_TABLE" "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: - - runWithSpan("parent") { - Session session = sessionFactory.openSession() - session.beginTransaction() - Query q = session.createQuery("from Value") - Iterator iterator = q.iterate() - while (iterator.hasNext()) { - iterator.next() - } - session.getTransaction().commit() - session.close() - } - - expect: - def sessionId - assertTraces(1) { - trace(0, 4) { - span(0) { - name "parent" - kind INTERNAL - hasNoParent() - attributes { - } - } - span(1) { - name "SELECT Value" - kind INTERNAL - childOf span(0) - attributes { - "hibernate.session_id" { - sessionId = it - it instanceof String - } - } - } - span(2) { - name "SELECT db1.Value" - kind CLIENT - childOf span(1) - attributes { - "$SemanticAttributes.DB_SYSTEM" "h2" - "$SemanticAttributes.DB_NAME" "db1" - "$SemanticAttributes.DB_USER" "sa" - "$SemanticAttributes.DB_CONNECTION_STRING" "h2:mem:" - "$SemanticAttributes.DB_STATEMENT" ~/^select / - "$SemanticAttributes.DB_OPERATION" "SELECT" - "$SemanticAttributes.DB_SQL_TABLE" "Value" - } - } - span(3) { - name "Transaction.commit" - kind INTERNAL - childOf span(0) - attributes { - "hibernate.session_id" sessionId - } - } - } - } - } - -} diff --git a/instrumentation/hibernate/hibernate-3.3/javaagent/src/test/groovy/SessionTest.groovy b/instrumentation/hibernate/hibernate-3.3/javaagent/src/test/groovy/SessionTest.groovy deleted file mode 100644 index ee9418b0c10e..000000000000 --- a/instrumentation/hibernate/hibernate-3.3/javaagent/src/test/groovy/SessionTest.groovy +++ /dev/null @@ -1,612 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -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 - -import static io.opentelemetry.api.trace.SpanKind.CLIENT -import static io.opentelemetry.api.trace.SpanKind.INTERNAL -import static io.opentelemetry.api.trace.StatusCode.ERROR - -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) { - runWithSpan("parent") { - 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: - def sessionId - assertTraces(sessionImplementations.size()) { - for (int i = 0; i < sessionImplementations.size(); i++) { - trace(i, 4) { - span(0) { - name "parent" - kind INTERNAL - hasNoParent() - attributes { - } - } - span(1) { - name "Session.$methodName $resource" - kind INTERNAL - childOf span(0) - attributes { - "hibernate.session_id" { - sessionId = it - it instanceof String - } - } - } - span(2) { - name "SELECT db1.Value" - kind CLIENT - childOf span(1) - attributes { - "$SemanticAttributes.DB_SYSTEM" "h2" - "$SemanticAttributes.DB_NAME" "db1" - "$SemanticAttributes.DB_USER" "sa" - "$SemanticAttributes.DB_CONNECTION_STRING" "h2:mem:" - "$SemanticAttributes.DB_STATEMENT" ~/^select / - "$SemanticAttributes.DB_OPERATION" "SELECT" - "$SemanticAttributes.DB_SQL_TABLE" "Value" - } - } - span(3) { - name "Transaction.commit" - kind INTERNAL - childOf span(0) - attributes { - "hibernate.session_id" sessionId - } - } - } - } - } - - 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: - - runWithSpan("parent") { - // 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: - def sessionId - assertTraces(1) { - trace(0, 4) { - span(0) { - name "parent" - kind INTERNAL - hasNoParent() - attributes { - } - } - span(1) { - name "Session.$methodName $resource" - kind INTERNAL - childOf span(0) - attributes { - "hibernate.session_id" { - sessionId = it - it instanceof String - } - } - } - span(2) { - name "Transaction.commit" - kind INTERNAL - childOf span(0) - attributes { - "hibernate.session_id" sessionId - } - } - span(3) { - kind CLIENT - childOf span(2) - attributes { - "$SemanticAttributes.DB_SYSTEM" "h2" - "$SemanticAttributes.DB_NAME" "db1" - "$SemanticAttributes.DB_USER" "sa" - "$SemanticAttributes.DB_CONNECTION_STRING" "h2:mem:" - "$SemanticAttributes.DB_STATEMENT" String - "$SemanticAttributes.DB_OPERATION" String - "$SemanticAttributes.DB_SQL_TABLE" "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: - - runWithSpan("parent") { - // 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: - def sessionId - assertTraces(1) { - trace(0, 5) { - span(0) { - name "parent" - kind INTERNAL - hasNoParent() - attributes { - } - } - span(1) { - name "Session.$methodName $resource" - kind INTERNAL - childOf span(0) - attributes { - "hibernate.session_id" { - sessionId = it - it instanceof String - } - } - } - span(2) { - name "SELECT db1.Value" - kind CLIENT - childOf span(1) - attributes { - "$SemanticAttributes.DB_SYSTEM" "h2" - "$SemanticAttributes.DB_NAME" "db1" - "$SemanticAttributes.DB_USER" "sa" - "$SemanticAttributes.DB_CONNECTION_STRING" "h2:mem:" - "$SemanticAttributes.DB_STATEMENT" ~/^select / - "$SemanticAttributes.DB_OPERATION" "SELECT" - "$SemanticAttributes.DB_SQL_TABLE" "Value" - } - } - span(3) { - name "Transaction.commit" - kind INTERNAL - childOf span(0) - attributes { - "hibernate.session_id" sessionId - } - } - span(4) { - kind CLIENT - childOf span(3) - attributes { - "$SemanticAttributes.DB_SYSTEM" "h2" - "$SemanticAttributes.DB_NAME" "db1" - "$SemanticAttributes.DB_USER" "sa" - "$SemanticAttributes.DB_CONNECTION_STRING" "h2:mem:" - "$SemanticAttributes.DB_STATEMENT" String - "$SemanticAttributes.DB_OPERATION" String - "$SemanticAttributes.DB_SQL_TABLE" "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: - - runWithSpan("parent") { - // 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: - def sessionId - assertTraces(1) { - trace(0, 3) { - span(0) { - name "parent" - kind INTERNAL - hasNoParent() - attributes { - } - } - span(1) { - name "Session.replicate java.lang.Long" - kind INTERNAL - childOf span(0) - status ERROR - errorEvent(MappingException, "Unknown entity: java.lang.Long") - attributes { - "hibernate.session_id" { - sessionId = it - it instanceof String - } - } - } - span(2) { - name "Transaction.commit" - kind INTERNAL - childOf span(0) - attributes { - "hibernate.session_id" sessionId - } - } - } - - } - } - - - def "test hibernate commit action #testName"() { - setup: - - runWithSpan("parent") { - 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: - def sessionId - assertTraces(1) { - trace(0, 4) { - span(0) { - name "parent" - kind INTERNAL - hasNoParent() - attributes { - } - } - span(1) { - name "Session.$methodName $resource" - kind INTERNAL - childOf span(0) - attributes { - "hibernate.session_id" { - sessionId = it - it instanceof String - } - } - } - span(2) { - name "Transaction.commit" - kind INTERNAL - childOf span(0) - attributes { - "hibernate.session_id" sessionId - } - } - span(3) { - kind CLIENT - childOf span(2) - attributes { - "$SemanticAttributes.DB_SYSTEM" "h2" - "$SemanticAttributes.DB_NAME" "db1" - "$SemanticAttributes.DB_USER" "sa" - "$SemanticAttributes.DB_CONNECTION_STRING" "h2:mem:" - "$SemanticAttributes.DB_STATEMENT" String - "$SemanticAttributes.DB_OPERATION" String - "$SemanticAttributes.DB_SQL_TABLE" "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: - runWithSpan("parent") { - Session session = sessionFactory.openSession() - session.beginTransaction() - Query query = queryBuildMethod(session) - query.list() - session.getTransaction().commit() - session.close() - } - - expect: - def sessionId - assertTraces(1) { - trace(0, 4) { - span(0) { - name "parent" - kind INTERNAL - hasNoParent() - attributes { - } - } - span(1) { - name expectedSpanName - kind INTERNAL - childOf span(0) - attributes { - "hibernate.session_id" { - sessionId = it - it instanceof String - } - } - } - span(2) { - kind CLIENT - childOf span(1) - attributes { - "$SemanticAttributes.DB_SYSTEM" "h2" - "$SemanticAttributes.DB_NAME" "db1" - "$SemanticAttributes.DB_USER" "sa" - "$SemanticAttributes.DB_CONNECTION_STRING" "h2:mem:" - "$SemanticAttributes.DB_STATEMENT" String - "$SemanticAttributes.DB_OPERATION" "SELECT" - "$SemanticAttributes.DB_SQL_TABLE" "Value" - } - } - span(3) { - name "Transaction.commit" - kind INTERNAL - childOf span(0) - attributes { - "hibernate.session_id" sessionId - } - } - } - } - - 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: - - runWithSpan("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: - def sessionId1 - def sessionId2 - def sessionId3 - assertTraces(1) { - trace(0, 8) { - span(0) { - name "overlapping Sessions" - attributes { - } - } - span(1) { - name "Session.save Value" - kind INTERNAL - childOf span(0) - attributes { - "hibernate.session_id" { - sessionId1 = it - it instanceof String - } - } - } - span(2) { - name "Session.insert Value" - kind INTERNAL - childOf span(0) - attributes { - "hibernate.session_id" { - sessionId2 = it - it instanceof String - } - } - } - span(3) { - name "Session.save Value" - kind INTERNAL - childOf span(0) - attributes { - "hibernate.session_id" { - sessionId3 = it - it instanceof String - } - } - } - span(4) { - name "Session.delete Value" - kind INTERNAL - childOf span(0) - attributes { - "hibernate.session_id" sessionId1 - } - } - span(5) { - name "Transaction.commit" - kind INTERNAL - childOf span(0) - attributes { - "hibernate.session_id" sessionId1 - } - } - span(6) { - name "INSERT db1.Value" - kind CLIENT - childOf span(5) - attributes { - "$SemanticAttributes.DB_SYSTEM" "h2" - "$SemanticAttributes.DB_NAME" "db1" - "$SemanticAttributes.DB_USER" "sa" - "$SemanticAttributes.DB_CONNECTION_STRING" "h2:mem:" - "$SemanticAttributes.DB_STATEMENT" ~/^insert / - "$SemanticAttributes.DB_OPERATION" "INSERT" - "$SemanticAttributes.DB_SQL_TABLE" "Value" - } - } - span(7) { - name "DELETE db1.Value" - kind CLIENT - childOf span(5) - attributes { - "$SemanticAttributes.DB_SYSTEM" "h2" - "$SemanticAttributes.DB_NAME" "db1" - "$SemanticAttributes.DB_USER" "sa" - "$SemanticAttributes.DB_CONNECTION_STRING" "h2:mem:" - "$SemanticAttributes.DB_STATEMENT" ~/^delete / - "$SemanticAttributes.DB_OPERATION" "DELETE" - "$SemanticAttributes.DB_SQL_TABLE" "Value" - } - } - } - } - sessionId1 != sessionId2 != sessionId3 - } -} diff --git a/instrumentation/hibernate/hibernate-3.3/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/hibernate/v3_3/AbstractHibernateTest.java b/instrumentation/hibernate/hibernate-3.3/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/hibernate/v3_3/AbstractHibernateTest.java new file mode 100644 index 000000000000..7042194917d5 --- /dev/null +++ b/instrumentation/hibernate/hibernate-3.3/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/hibernate/v3_3/AbstractHibernateTest.java @@ -0,0 +1,109 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.hibernate.v3_3; + +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.satisfies; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.sdk.testing.assertj.SpanDataAssert; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.semconv.incubating.DbIncubatingAttributes; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import org.hibernate.Session; +import org.hibernate.SessionFactory; +import org.hibernate.cfg.AnnotationConfiguration; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.extension.RegisterExtension; + +abstract class AbstractHibernateTest extends AgentInstrumentationSpecification { + + @RegisterExtension + protected static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + protected static SessionFactory sessionFactory; + protected static List prepopulated; + + @BeforeAll + static void setUp() { + 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(); + } + + @AfterAll + static void cleanUp() { + if (sessionFactory != null) { + sessionFactory.close(); + } + } + + @SuppressWarnings("deprecation") // TODO DbIncubatingAttributes.DB_CONNECTION_STRING deprecation + static SpanDataAssert assertClientSpan(SpanDataAssert span, SpanData parent) { + return span.hasKind(SpanKind.CLIENT) + .hasParent(parent) + .hasAttributesSatisfyingExactly( + equalTo(DbIncubatingAttributes.DB_SYSTEM, "h2"), + equalTo(DbIncubatingAttributes.DB_NAME, "db1"), + equalTo(DbIncubatingAttributes.DB_USER, "sa"), + equalTo(DbIncubatingAttributes.DB_CONNECTION_STRING, "h2:mem:"), + satisfies(DbIncubatingAttributes.DB_STATEMENT, val -> val.isInstanceOf(String.class)), + satisfies(DbIncubatingAttributes.DB_OPERATION, val -> val.isInstanceOf(String.class)), + equalTo(DbIncubatingAttributes.DB_SQL_TABLE, "Value")); + } + + @SuppressWarnings("deprecation") // TODO DbIncubatingAttributes.DB_CONNECTION_STRING deprecation + static SpanDataAssert assertClientSpan(SpanDataAssert span, SpanData parent, String verb) { + return span.hasName(verb.concat(" db1.Value")) + .hasKind(SpanKind.CLIENT) + .hasParent(parent) + .hasAttributesSatisfyingExactly( + equalTo(DbIncubatingAttributes.DB_SYSTEM, "h2"), + equalTo(DbIncubatingAttributes.DB_NAME, "db1"), + equalTo(DbIncubatingAttributes.DB_USER, "sa"), + equalTo(DbIncubatingAttributes.DB_CONNECTION_STRING, "h2:mem:"), + satisfies( + DbIncubatingAttributes.DB_STATEMENT, + stringAssert -> stringAssert.startsWith(verb.toLowerCase(Locale.ROOT))), + equalTo(DbIncubatingAttributes.DB_OPERATION, verb), + equalTo(DbIncubatingAttributes.DB_SQL_TABLE, "Value")); + } + + static SpanDataAssert assertSessionSpan(SpanDataAssert span, SpanData parent, String spanName) { + return span.hasName(spanName) + .hasKind(SpanKind.INTERNAL) + .hasParent(parent) + .hasAttributesSatisfyingExactly( + satisfies( + AttributeKey.stringKey("hibernate.session_id"), + val -> val.isInstanceOf(String.class))); + } + + static SpanDataAssert assertSpanWithSessionId( + SpanDataAssert span, SpanData parent, String spanName, String sessionId) { + return span.hasName(spanName) + .hasKind(SpanKind.INTERNAL) + .hasParent(parent) + .hasAttributesSatisfyingExactly( + equalTo(AttributeKey.stringKey("hibernate.session_id"), sessionId)); + } +} diff --git a/instrumentation/hibernate/hibernate-3.3/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/hibernate/v3_3/CriteriaTest.java b/instrumentation/hibernate/hibernate-3.3/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/hibernate/v3_3/CriteriaTest.java new file mode 100644 index 000000000000..b3d20591e5c6 --- /dev/null +++ b/instrumentation/hibernate/hibernate-3.3/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/hibernate/v3_3/CriteriaTest.java @@ -0,0 +1,70 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.hibernate.v3_3; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.trace.SpanKind; +import java.util.function.Consumer; +import java.util.stream.Stream; +import org.hibernate.Criteria; +import org.hibernate.Session; +import org.hibernate.criterion.Order; +import org.hibernate.criterion.Restrictions; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +class CriteriaTest extends AbstractHibernateTest { + + @ParameterizedTest + @MethodSource("provideArguments") + void testCriteria(String methodName, Consumer interaction) { + + testing.runWithSpan( + "parent", + () -> { + Session session = sessionFactory.openSession(); + session.beginTransaction(); + Criteria criteria = + session + .createCriteria(Value.class) + .add(Restrictions.like("name", "Hello")) + .addOrder(Order.desc("name")); + interaction.accept(criteria); + session.getTransaction().commit(); + session.close(); + }); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(), + span -> + assertSessionSpan( + span, + trace.getSpan(0), + "Criteria." + + methodName + + " io.opentelemetry.javaagent.instrumentation.hibernate.v3_3.Value"), + span -> assertClientSpan(span, trace.getSpan(1), "SELECT"), + span -> + assertSpanWithSessionId( + span, + trace.getSpan(0), + "Transaction.commit", + trace + .getSpan(1) + .getAttributes() + .get(AttributeKey.stringKey("hibernate.session_id"))))); + } + + private static Stream provideArguments() { + return Stream.of( + Arguments.of("list", (Consumer) Criteria::list), + Arguments.of("uniqueResult", (Consumer) Criteria::uniqueResult), + Arguments.of("scroll", (Consumer) Criteria::scroll)); + } +} diff --git a/instrumentation/hibernate/hibernate-3.3/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/hibernate/v3_3/QueryTest.java b/instrumentation/hibernate/hibernate-3.3/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/hibernate/v3_3/QueryTest.java new file mode 100644 index 000000000000..e8da2d58f341 --- /dev/null +++ b/instrumentation/hibernate/hibernate-3.3/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/hibernate/v3_3/QueryTest.java @@ -0,0 +1,154 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.hibernate.v3_3; + +import static org.junit.jupiter.api.Named.named; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.trace.SpanKind; +import java.util.function.Consumer; +import java.util.stream.Stream; +import org.hibernate.Query; +import org.hibernate.Session; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +class QueryTest extends AbstractHibernateTest { + + @ParameterizedTest + @MethodSource("provideArguments") + void testHibernateQuery(Parameter parameters) { + + // With Transaction + if (parameters.requiresTransaction) { + testing.runWithSpan( + "parent", + () -> { + Session session = sessionFactory.openSession(); + session.beginTransaction(); + parameters.queryInteraction.accept(session); + session.getTransaction().commit(); + session.close(); + }); + } else { + // Without Transaction + testing.runWithSpan( + "parent2", + () -> { + Session session = sessionFactory.openSession(); + parameters.queryInteraction.accept(session); + session.close(); + }); + } + + testing.waitAndAssertTraces( + trace -> { + if (parameters.requiresTransaction) { + // With Transaction + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(), + span -> assertSessionSpan(span, trace.getSpan(0), parameters.expectedSpanName), + span -> assertClientSpan(span, trace.getSpan(1)), + span -> + assertSpanWithSessionId( + span, + trace.getSpan(0), + "Transaction.commit", + trace + .getSpan(1) + .getAttributes() + .get(AttributeKey.stringKey("hibernate.session_id")))); + } else { + // Without Transaction + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent2").hasKind(SpanKind.INTERNAL).hasNoParent(), + span -> assertSessionSpan(span, trace.getSpan(0), parameters.expectedSpanName), + span -> assertClientSpan(span, trace.getSpan(1), "SELECT")); + } + }); + } + + private static Stream provideArguments() { + return Stream.of( + Arguments.of( + named( + "Query.list", + new Parameter( + "SELECT io.opentelemetry.javaagent.instrumentation.hibernate.v3_3.Value", + false, + sess -> { + Query q = + sess.createQuery( + "from io.opentelemetry.javaagent.instrumentation.hibernate.v3_3.Value where id = ?"); + q.setParameter(0, 1L); + q.uniqueResult(); + }))), + Arguments.of( + named( + "Query.executeUpdate", + new Parameter( + "UPDATE io.opentelemetry.javaagent.instrumentation.hibernate.v3_3.Value", + true, + sess -> { + Query q = + sess.createQuery( + "update io.opentelemetry.javaagent.instrumentation.hibernate.v3_3.Value set name = ?"); + q.setParameter(0, "alyx"); + q.executeUpdate(); + }))), + Arguments.of( + named( + "Query.uniqueResult", + new Parameter( + "SELECT io.opentelemetry.javaagent.instrumentation.hibernate.v3_3.Value", + false, + sess -> { + Query q = + sess.createQuery( + "from io.opentelemetry.javaagent.instrumentation.hibernate.v3_3.Value where id = ?"); + q.setParameter(0, 1L); + q.uniqueResult(); + }))), + Arguments.of( + named( + "Query.iterate", + new Parameter( + "SELECT io.opentelemetry.javaagent.instrumentation.hibernate.v3_3.Value", + false, + sess -> { + Query q = + sess.createQuery( + "from io.opentelemetry.javaagent.instrumentation.hibernate.v3_3.Value"); + q.iterate(); + }))), + Arguments.of( + named( + "Query.scroll", + new Parameter( + "SELECT io.opentelemetry.javaagent.instrumentation.hibernate.v3_3.Value", + false, + sess -> { + Query q = + sess.createQuery( + "from io.opentelemetry.javaagent.instrumentation.hibernate.v3_3.Value"); + q.scroll(); + })))); + } + + private static class Parameter { + public final String expectedSpanName; + public final boolean requiresTransaction; + public final Consumer queryInteraction; + + public Parameter( + String expectedSpanName, boolean requiresTransaction, Consumer queryInteraction) { + this.expectedSpanName = expectedSpanName; + this.requiresTransaction = requiresTransaction; + this.queryInteraction = queryInteraction; + } + } +} diff --git a/instrumentation/hibernate/hibernate-3.3/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/hibernate/v3_3/SessionTest.java b/instrumentation/hibernate/hibernate-3.3/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/hibernate/v3_3/SessionTest.java new file mode 100644 index 000000000000..9a246eb10754 --- /dev/null +++ b/instrumentation/hibernate/hibernate-3.3/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/hibernate/v3_3/SessionTest.java @@ -0,0 +1,614 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.hibernate.v3_3; + +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.satisfies; +import static io.opentelemetry.semconv.ExceptionAttributes.EXCEPTION_MESSAGE; +import static io.opentelemetry.semconv.ExceptionAttributes.EXCEPTION_STACKTRACE; +import static io.opentelemetry.semconv.ExceptionAttributes.EXCEPTION_TYPE; +import static org.junit.jupiter.api.Named.named; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.sdk.testing.assertj.TraceAssert; +import java.util.function.BiConsumer; +import java.util.function.Function; +import java.util.stream.Stream; +import org.hibernate.LockMode; +import org.hibernate.Query; +import org.hibernate.ReplicationMode; +import org.hibernate.Session; +import org.hibernate.StatelessSession; +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 SessionTest extends AbstractHibernateTest { + @ParameterizedTest + @MethodSource("provideArguments") + void testHibernateAction(Parameter parameter) { + + testing.runWithSpan( + "parent", + () -> { + Session session = sessionFactory.openSession(); + session.beginTransaction(); + parameter.sessionMethodTest.accept(session, prepopulated.get(0)); + session.getTransaction().commit(); + session.close(); + }); + + testing.waitAndAssertTraces( + trace -> sessionAssertion(trace, parameter.methodName, parameter.resource)); + } + + @ParameterizedTest + @MethodSource("provideArgumentsStateless") + void testHibernateActionWithStatelessSession(Parameter parameter) { + + testing.runWithSpan( + "parent", + () -> { + StatelessSession session = sessionFactory.openStatelessSession(); + session.beginTransaction(); + parameter.statelessSessionMethodTest.accept(session, prepopulated.get(0)); + session.getTransaction().commit(); + session.close(); + }); + + testing.waitAndAssertTraces( + trace -> sessionAssertion(trace, parameter.methodName, parameter.resource)); + } + + @ParameterizedTest + @MethodSource("provideArgumentsStatelessAction") + void testHibernateStatelessAction(Parameter parameter) { + testing.runWithSpan( + "parent", + () -> { + StatelessSession session = sessionFactory.openStatelessSession(); + session.beginTransaction(); + parameter.statelessSessionMethodTest.accept(session, prepopulated.get(0)); + session.getTransaction().commit(); + session.close(); + }); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(), + span -> + assertSessionSpan( + span, + trace.getSpan(0), + "Session." + parameter.methodName + " " + parameter.resource), + span -> + assertSpanWithSessionId( + span, + trace.getSpan(0), + "Transaction.commit", + trace + .getSpan(1) + .getAttributes() + .get(AttributeKey.stringKey("hibernate.session_id"))), + span -> assertClientSpan(span, trace.getSpan(2)))); + } + + @ParameterizedTest + @MethodSource("provideArgumentsReplicate") + void testHibernateReplicate(Parameter parameter) { + testing.runWithSpan( + "parent", + () -> { + Session session = sessionFactory.openSession(); + session.beginTransaction(); + parameter.sessionMethodTest.accept(session, prepopulated.get(0)); + session.getTransaction().commit(); + session.close(); + }); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(), + span -> + assertSessionSpan( + span, + trace.getSpan(0), + "Session." + parameter.methodName + " " + parameter.resource), + span -> assertClientSpan(span, trace.getSpan(1), "SELECT"), + span -> + assertSpanWithSessionId( + span, + trace.getSpan(0), + "Transaction.commit", + trace + .getSpan(1) + .getAttributes() + .get(AttributeKey.stringKey("hibernate.session_id"))), + span -> assertClientSpan(span, trace.getSpan(3)))); + } + + @Test + void testHibernateFailedReplicate() { + testing.runWithSpan( + "parent", + () -> { + Session session = sessionFactory.openSession(); + session.beginTransaction(); + try { + session.replicate(123L /* Not a valid entity */, ReplicationMode.OVERWRITE); + } catch (RuntimeException e) { + // We expected this, we should see the error field set on the span. + } + session.getTransaction().commit(); + session.close(); + }); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(), + span -> + assertSessionSpan(span, trace.getSpan(0), "Session.replicate java.lang.Long") + .hasEventsSatisfyingExactly( + event -> + event + .hasName("exception") + .hasAttributesSatisfyingExactly( + equalTo(EXCEPTION_TYPE, "org.hibernate.MappingException"), + equalTo( + EXCEPTION_MESSAGE, "Unknown entity: java.lang.Long"), + satisfies( + EXCEPTION_STACKTRACE, + val -> val.isInstanceOf(String.class)))), + span -> + assertSpanWithSessionId( + span, + trace.getSpan(0), + "Transaction.commit", + trace + .getSpan(1) + .getAttributes() + .get(AttributeKey.stringKey("hibernate.session_id"))))); + } + + @ParameterizedTest + @MethodSource("provideArgumentsCommitAction") + void testHibernateCommitAction(Parameter parameter) { + testing.runWithSpan( + "parent", + () -> { + Session session = sessionFactory.openSession(); + session.beginTransaction(); + parameter.sessionMethodTest.accept(session, prepopulated.get(0)); + session.getTransaction().commit(); + session.close(); + }); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(), + span -> + assertSessionSpan( + span, + trace.getSpan(0), + "Session." + parameter.methodName + " " + parameter.resource), + span -> + assertSpanWithSessionId( + span, + trace.getSpan(0), + "Transaction.commit", + trace + .getSpan(1) + .getAttributes() + .get(AttributeKey.stringKey("hibernate.session_id"))), + span -> assertClientSpan(span, trace.getSpan(2)))); + } + + @ParameterizedTest + @MethodSource("provideArgumentsForQueryMethods") + void testAttachesStateToQueryCreatedViaQueryMethods(Parameter parameter) { + testing.runWithSpan( + "parent", + () -> { + Session session = sessionFactory.openSession(); + session.beginTransaction(); + Query query = parameter.queryBuildMethod.apply(session); + query.list(); + session.getTransaction().commit(); + session.close(); + }); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(), + span -> assertSessionSpan(span, trace.getSpan(0), parameter.resource), + span -> assertClientSpan(span, trace.getSpan(1)), + span -> + assertSpanWithSessionId( + span, + trace.getSpan(0), + "Transaction.commit", + trace + .getSpan(1) + .getAttributes() + .get(AttributeKey.stringKey("hibernate.session_id"))))); + } + + @Test + void testHibernateOverlappingSessions() { + + testing.runWithSpan( + "overlapping Sessions", + () -> { + Session session1 = sessionFactory.openSession(); + session1.beginTransaction(); + + StatelessSession session2 = sessionFactory.openStatelessSession(); + Session session3 = sessionFactory.openSession(); + + Value value1 = + new Value("io.opentelemetry.javaagent.instrumentation.hibernate.v3_3.Value 1"); + session1.save(value1); + session2.insert( + new Value("io.opentelemetry.javaagent.instrumentation.hibernate.v3_3.Value 2")); + session3.save( + new Value("io.opentelemetry.javaagent.instrumentation.hibernate.v3_3.Value 3")); + session1.delete(value1); + + session2.close(); + session1.getTransaction().commit(); + session1.close(); + session3.close(); + }); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("overlapping Sessions"), + span -> + assertSessionSpan( + span, + trace.getSpan(0), + "Session.save io.opentelemetry.javaagent.instrumentation.hibernate.v3_3.Value"), + span -> + assertSessionSpan( + span, + trace.getSpan(0), + "Session.insert io.opentelemetry.javaagent.instrumentation.hibernate.v3_3.Value"), + span -> + assertSessionSpan( + span, + trace.getSpan(0), + "Session.save io.opentelemetry.javaagent.instrumentation.hibernate.v3_3.Value"), + span -> + assertSpanWithSessionId( + span, + trace.getSpan(0), + "Session.delete io.opentelemetry.javaagent.instrumentation.hibernate.v3_3.Value", + trace + .getSpan(1) + .getAttributes() + .get(AttributeKey.stringKey("hibernate.session_id"))), + span -> + assertSpanWithSessionId( + span, + trace.getSpan(0), + "Transaction.commit", + trace + .getSpan(1) + .getAttributes() + .get(AttributeKey.stringKey("hibernate.session_id"))), + span -> assertClientSpan(span, trace.getSpan(5), "INSERT"), + span -> assertClientSpan(span, trace.getSpan(5), "DELETE"))); + } + + private static Stream provideArguments() { + return Stream.of( + Arguments.of( + named( + "lock", + new Parameter( + "lock", + "io.opentelemetry.javaagent.instrumentation.hibernate.v3_3.Value", + (Session session, Value val) -> session.lock(val, LockMode.READ), + null, + null))), + Arguments.of( + named( + "refresh", + new Parameter( + "refresh", + "io.opentelemetry.javaagent.instrumentation.hibernate.v3_3.Value", + Session::refresh, + null, + null))), + Arguments.of( + named( + "get", + new Parameter( + "get", + "io.opentelemetry.javaagent.instrumentation.hibernate.v3_3.Value", + (Session session, Value val) -> + session.get( + "io.opentelemetry.javaagent.instrumentation.hibernate.v3_3.Value", + val.getId()), + null, + null)))); + } + + private static Stream provideArgumentsStateless() { + return Stream.of( + Arguments.of( + named( + "refresh", + new Parameter( + "refresh", + "io.opentelemetry.javaagent.instrumentation.hibernate.v3_3.Value", + null, + StatelessSession::refresh, + null))), + Arguments.of( + named( + "get", + new Parameter( + "get", + "io.opentelemetry.javaagent.instrumentation.hibernate.v3_3.Value", + null, + (StatelessSession session, Value val) -> + session.get( + "io.opentelemetry.javaagent.instrumentation.hibernate.v3_3.Value", + val.getId()), + null)))); + } + + private static Stream provideArgumentsStatelessAction() { + return Stream.of( + Arguments.of( + named( + "insert", + new Parameter( + "insert", + "io.opentelemetry.javaagent.instrumentation.hibernate.v3_3.Value", + null, + (StatelessSession session, Value val) -> + session.insert( + "io.opentelemetry.javaagent.instrumentation.hibernate.v3_3.Value", + new Value("insert me")), + null))), + Arguments.of( + named( + "get", + new Parameter( + "update", + "io.opentelemetry.javaagent.instrumentation.hibernate.v3_3.Value", + null, + (StatelessSession session, Value val) -> { + val.setName("New name"); + session.update(val); + }, + null))), + Arguments.of( + named( + "update by entityName", + new Parameter( + "update", + "io.opentelemetry.javaagent.instrumentation.hibernate.v3_3.Value", + null, + (StatelessSession session, Value val) -> { + val.setName("New name"); + session.update( + "io.opentelemetry.javaagent.instrumentation.hibernate.v3_3.Value", val); + }, + null))), + Arguments.of( + named( + "delete", + new Parameter( + "delete", + "io.opentelemetry.javaagent.instrumentation.hibernate.v3_3.Value", + null, + StatelessSession::delete, + null)))); + } + + private static Stream provideArgumentsReplicate() { + return Stream.of( + Arguments.of( + named( + "replicate", + new Parameter( + "replicate", + "io.opentelemetry.javaagent.instrumentation.hibernate.v3_3.Value", + (Session session, Value val) -> { + Value replicated = new Value(val.getName() + " replicated"); + replicated.setId(val.getId()); + session.replicate(replicated, ReplicationMode.OVERWRITE); + }, + null, + null))), + Arguments.of( + named( + "replicate by entityName", + new Parameter( + "replicate", + "io.opentelemetry.javaagent.instrumentation.hibernate.v3_3.Value", + (Session session, Value val) -> { + Value replicated = new Value(val.getName() + " replicated"); + replicated.setId(val.getId()); + session.replicate( + "io.opentelemetry.javaagent.instrumentation.hibernate.v3_3.Value", + replicated, + ReplicationMode.OVERWRITE); + }, + null, + null)))); + } + + private static Stream provideArgumentsCommitAction() { + return Stream.of( + Arguments.of( + named( + "save", + new Parameter( + "save", + "io.opentelemetry.javaagent.instrumentation.hibernate.v3_3.Value", + (Session session, Value val) -> session.save(new Value("Another value")), + null, + null))), + Arguments.of( + named( + "saveOrUpdate save", + new Parameter( + "saveOrUpdate", + "io.opentelemetry.javaagent.instrumentation.hibernate.v3_3.Value", + (Session session, Value val) -> + session.saveOrUpdate( + new Value( + "io.opentelemetry.javaagent.instrumentation.hibernate.v3_3.Value")), + null, + null))), + Arguments.of( + named( + "saveOrUpdate update", + new Parameter( + "saveOrUpdate", + "io.opentelemetry.javaagent.instrumentation.hibernate.v3_3.Value", + (Session session, Value val) -> { + val.setName("New name"); + session.saveOrUpdate(val); + }, + null, + null))), + Arguments.of( + named( + "merge", + new Parameter( + "merge", + "io.opentelemetry.javaagent.instrumentation.hibernate.v3_3.Value", + (Session session, Value val) -> session.merge(new Value("merge me in")), + null, + null))), + Arguments.of( + named( + "persist", + new Parameter( + "persist", + "io.opentelemetry.javaagent.instrumentation.hibernate.v3_3.Value", + (Session session, Value val) -> session.persist(new Value("merge me in")), + null, + null))), + Arguments.of( + named( + "update (Session)", + new Parameter( + "update", + "io.opentelemetry.javaagent.instrumentation.hibernate.v3_3.Value", + (Session session, Value val) -> { + val.setName("New name"); + session.update(val); + }, + null, + null))), + Arguments.of( + named( + "update by entityName (Session)", + new Parameter( + "update", + "io.opentelemetry.javaagent.instrumentation.hibernate.v3_3.Value", + (Session session, Value val) -> { + val.setName("New name"); + session.update( + "io.opentelemetry.javaagent.instrumentation.hibernate.v3_3.Value", val); + }, + null, + null))), + Arguments.of( + named( + "delete (Session)", + new Parameter( + "delete", + "io.opentelemetry.javaagent.instrumentation.hibernate.v3_3.Value", + Session::delete, + null, + null)))); + } + + private static Stream provideArgumentsForQueryMethods() { + return Stream.of( + Arguments.of( + named( + "createQuery", + new Parameter( + "createQuery", + "SELECT io.opentelemetry.javaagent.instrumentation.hibernate.v3_3.Value", + null, + null, + (Session session) -> + session.createQuery( + "from io.opentelemetry.javaagent.instrumentation.hibernate.v3_3.Value")))), + Arguments.of( + named( + "getNamedQuery", + new Parameter( + "getNamedQuery", + "SELECT Value", + null, + null, + (Session session) -> session.getNamedQuery("TestNamedQuery")))), + Arguments.of( + named( + "createSQLQuery", + new Parameter( + "createSQLQuery", + "SELECT Value", + null, + null, + (Session session) -> session.createSQLQuery("SELECT * FROM Value"))))); + } + + private static class Parameter { + public final String methodName; + public final String resource; + public final BiConsumer sessionMethodTest; + public final BiConsumer statelessSessionMethodTest; + public final Function queryBuildMethod; + + public Parameter( + String methodName, + String resource, + BiConsumer sessionMethodTest, + BiConsumer statelessSessionMethodTest, + Function queryBuildMethod) { + this.methodName = methodName; + this.resource = resource; + this.sessionMethodTest = sessionMethodTest; + this.statelessSessionMethodTest = statelessSessionMethodTest; + this.queryBuildMethod = queryBuildMethod; + } + } + + static void sessionAssertion(TraceAssert trace, String methodName, String resource) { + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(), + span -> assertSessionSpan(span, trace.getSpan(0), "Session." + methodName + " " + resource), + span -> assertClientSpan(span, trace.getSpan(1), "SELECT"), + span -> + assertSpanWithSessionId( + span, + trace.getSpan(0), + "Transaction.commit", + trace + .getSpan(1) + .getAttributes() + .get(AttributeKey.stringKey("hibernate.session_id")))); + } +} diff --git a/instrumentation/hibernate/hibernate-3.3/javaagent/src/test/java/Value.java b/instrumentation/hibernate/hibernate-3.3/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/hibernate/v3_3/Value.java similarity index 92% rename from instrumentation/hibernate/hibernate-3.3/javaagent/src/test/java/Value.java rename to instrumentation/hibernate/hibernate-3.3/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/hibernate/v3_3/Value.java index a9de87173ddd..da96916f77fb 100644 --- a/instrumentation/hibernate/hibernate-3.3/javaagent/src/test/java/Value.java +++ b/instrumentation/hibernate/hibernate-3.3/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/hibernate/v3_3/Value.java @@ -3,6 +3,8 @@ * SPDX-License-Identifier: Apache-2.0 */ +package io.opentelemetry.javaagent.instrumentation.hibernate.v3_3; + import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.Id; diff --git a/instrumentation/hibernate/hibernate-3.3/javaagent/src/test/resources/hibernate.cfg.xml b/instrumentation/hibernate/hibernate-3.3/javaagent/src/test/resources/hibernate.cfg.xml index 8cf25d386fc7..344f70046aa7 100644 --- a/instrumentation/hibernate/hibernate-3.3/javaagent/src/test/resources/hibernate.cfg.xml +++ b/instrumentation/hibernate/hibernate-3.3/javaagent/src/test/resources/hibernate.cfg.xml @@ -24,7 +24,7 @@ create - + diff --git a/instrumentation/hibernate/hibernate-4.0/javaagent/build.gradle.kts b/instrumentation/hibernate/hibernate-4.0/javaagent/build.gradle.kts index 5f85f7c3cecd..c2b3ac2d615b 100644 --- a/instrumentation/hibernate/hibernate-4.0/javaagent/build.gradle.kts +++ b/instrumentation/hibernate/hibernate-4.0/javaagent/build.gradle.kts @@ -32,6 +32,8 @@ dependencies { testImplementation("org.hibernate:hibernate-core:4.0.0.Final") testImplementation("org.hibernate:hibernate-entitymanager:4.0.0.Final") + + testImplementation("org.javassist:javassist:3.28.0-GA") } val latestDepTest = findProperty("testLatestDeps") as Boolean @@ -40,9 +42,6 @@ testing { val version5Test by registering(JvmTestSuite::class) { dependencies { sources { - groovy { - setSrcDirs(listOf("src/test/groovy")) - } java { setSrcDirs(listOf("src/test/java")) } @@ -80,6 +79,8 @@ tasks.withType().configureEach { // TODO run tests both with and without experimental span attributes jvmArgs("-Dotel.instrumentation.hibernate.experimental-span-attributes=true") + + jvmArgs("-Dotel.instrumentation.common.experimental.controller-telemetry.enabled=true") } tasks { diff --git a/instrumentation/hibernate/hibernate-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/v4_0/CriteriaInstrumentation.java b/instrumentation/hibernate/hibernate-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/v4_0/CriteriaInstrumentation.java index aec640bc6b07..495d6f9c595b 100644 --- a/instrumentation/hibernate/hibernate-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/v4_0/CriteriaInstrumentation.java +++ b/instrumentation/hibernate/hibernate-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/v4_0/CriteriaInstrumentation.java @@ -13,13 +13,12 @@ import static net.bytebuddy.matcher.ElementMatchers.namedOneOf; import io.opentelemetry.context.Context; -import io.opentelemetry.context.Scope; import io.opentelemetry.instrumentation.api.util.VirtualField; -import io.opentelemetry.javaagent.bootstrap.CallDepth; import io.opentelemetry.javaagent.bootstrap.Java8BytecodeBridge; import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; import io.opentelemetry.javaagent.instrumentation.hibernate.HibernateOperation; +import io.opentelemetry.javaagent.instrumentation.hibernate.HibernateOperationScope; import io.opentelemetry.javaagent.instrumentation.hibernate.SessionInfo; import net.bytebuddy.asm.Advice; import net.bytebuddy.description.type.TypeDescription; @@ -50,17 +49,11 @@ public void transform(TypeTransformer transformer) { public static class CriteriaMethodAdvice { @Advice.OnMethodEnter(suppress = Throwable.class) - public static void startMethod( - @Advice.This Criteria criteria, - @Advice.Origin("#m") String name, - @Advice.Local("otelCallDepth") CallDepth callDepth, - @Advice.Local("otelHibernateOperation") HibernateOperation hibernateOperation, - @Advice.Local("otelContext") Context context, - @Advice.Local("otelScope") Scope scope) { - - callDepth = CallDepth.forClass(HibernateOperation.class); - if (callDepth.getAndIncrement() > 0) { - return; + public static HibernateOperationScope startMethod( + @Advice.This Criteria criteria, @Advice.Origin("#m") String name) { + + if (HibernateOperationScope.enterDepthSkipCheck()) { + return null; } String entityName = null; @@ -73,31 +66,17 @@ public static void startMethod( SessionInfo sessionInfo = criteriaVirtualField.get(criteria); Context parentContext = Java8BytecodeBridge.currentContext(); - hibernateOperation = new HibernateOperation("Criteria." + name, entityName, sessionInfo); - if (!instrumenter().shouldStart(parentContext, hibernateOperation)) { - return; - } + HibernateOperation hibernateOperation = + new HibernateOperation("Criteria." + name, entityName, sessionInfo); - context = instrumenter().start(parentContext, hibernateOperation); - scope = context.makeCurrent(); + return HibernateOperationScope.start(hibernateOperation, parentContext, instrumenter()); } @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) public static void endMethod( - @Advice.Thrown Throwable throwable, - @Advice.Local("otelCallDepth") CallDepth callDepth, - @Advice.Local("otelHibernateOperation") HibernateOperation hibernateOperation, - @Advice.Local("otelContext") Context context, - @Advice.Local("otelScope") Scope scope) { - - if (callDepth.decrementAndGet() > 0) { - return; - } + @Advice.Thrown Throwable throwable, @Advice.Enter HibernateOperationScope scope) { - if (scope != null) { - scope.close(); - instrumenter().end(context, hibernateOperation, null, throwable); - } + HibernateOperationScope.end(scope, throwable); } } } diff --git a/instrumentation/hibernate/hibernate-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/v4_0/HibernateInstrumentationModule.java b/instrumentation/hibernate/hibernate-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/v4_0/HibernateInstrumentationModule.java index d165a28596c3..e8a919d13c37 100644 --- a/instrumentation/hibernate/hibernate-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/v4_0/HibernateInstrumentationModule.java +++ b/instrumentation/hibernate/hibernate-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/v4_0/HibernateInstrumentationModule.java @@ -11,11 +11,13 @@ 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.internal.ExperimentalInstrumentationModule; import java.util.List; import net.bytebuddy.matcher.ElementMatcher; @AutoService(InstrumentationModule.class) -public class HibernateInstrumentationModule extends InstrumentationModule { +public class HibernateInstrumentationModule extends InstrumentationModule + implements ExperimentalInstrumentationModule { public HibernateInstrumentationModule() { super("hibernate", "hibernate-4.0"); @@ -30,6 +32,11 @@ public ElementMatcher.Junction classLoaderMatcher() { "org.hibernate.Criteria"); } + @Override + public String getModuleGroup() { + return "hibernate"; + } + @Override public List typeInstrumentations() { return asList( diff --git a/instrumentation/hibernate/hibernate-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/v4_0/QueryInstrumentation.java b/instrumentation/hibernate/hibernate-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/v4_0/QueryInstrumentation.java index af0618e6c773..635634ea73fc 100644 --- a/instrumentation/hibernate/hibernate-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/v4_0/QueryInstrumentation.java +++ b/instrumentation/hibernate/hibernate-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/v4_0/QueryInstrumentation.java @@ -14,13 +14,12 @@ import static net.bytebuddy.matcher.ElementMatchers.namedOneOf; import io.opentelemetry.context.Context; -import io.opentelemetry.context.Scope; import io.opentelemetry.instrumentation.api.util.VirtualField; -import io.opentelemetry.javaagent.bootstrap.CallDepth; import io.opentelemetry.javaagent.bootstrap.Java8BytecodeBridge; import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; import io.opentelemetry.javaagent.instrumentation.hibernate.HibernateOperation; +import io.opentelemetry.javaagent.instrumentation.hibernate.HibernateOperationScope; import io.opentelemetry.javaagent.instrumentation.hibernate.SessionInfo; import net.bytebuddy.asm.Advice; import net.bytebuddy.description.type.TypeDescription; @@ -50,16 +49,10 @@ public void transform(TypeTransformer transformer) { public static class QueryMethodAdvice { @Advice.OnMethodEnter(suppress = Throwable.class) - public static void startMethod( - @Advice.This Query query, - @Advice.Local("otelCallDepth") CallDepth callDepth, - @Advice.Local("otelHibernateOperation") HibernateOperation hibernateOperation, - @Advice.Local("otelContext") Context context, - @Advice.Local("otelScope") Scope scope) { + public static HibernateOperationScope startMethod(@Advice.This Query query) { - callDepth = CallDepth.forClass(HibernateOperation.class); - if (callDepth.getAndIncrement() > 0) { - return; + if (HibernateOperationScope.enterDepthSkipCheck()) { + return null; } VirtualField queryVirtualField = @@ -67,32 +60,17 @@ public static void startMethod( SessionInfo sessionInfo = queryVirtualField.get(query); Context parentContext = Java8BytecodeBridge.currentContext(); - hibernateOperation = + HibernateOperation hibernateOperation = new HibernateOperation(getOperationNameForQuery(query.getQueryString()), sessionInfo); - if (!instrumenter().shouldStart(parentContext, hibernateOperation)) { - return; - } - context = instrumenter().start(parentContext, hibernateOperation); - scope = context.makeCurrent(); + return HibernateOperationScope.start(hibernateOperation, parentContext, instrumenter()); } @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) public static void endMethod( - @Advice.Thrown Throwable throwable, - @Advice.Local("otelCallDepth") CallDepth callDepth, - @Advice.Local("otelHibernateOperation") HibernateOperation hibernateOperation, - @Advice.Local("otelContext") Context context, - @Advice.Local("otelScope") Scope scope) { - - if (callDepth.decrementAndGet() > 0) { - return; - } + @Advice.Thrown Throwable throwable, @Advice.Enter HibernateOperationScope scope) { - if (scope != null) { - scope.close(); - instrumenter().end(context, hibernateOperation, null, throwable); - } + HibernateOperationScope.end(scope, throwable); } } } diff --git a/instrumentation/hibernate/hibernate-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/v4_0/SessionInstrumentation.java b/instrumentation/hibernate/hibernate-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/v4_0/SessionInstrumentation.java index e5e9fbc5116e..9457fbfbee51 100644 --- a/instrumentation/hibernate/hibernate-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/v4_0/SessionInstrumentation.java +++ b/instrumentation/hibernate/hibernate-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/v4_0/SessionInstrumentation.java @@ -18,13 +18,12 @@ import static net.bytebuddy.matcher.ElementMatchers.takesArgument; import io.opentelemetry.context.Context; -import io.opentelemetry.context.Scope; import io.opentelemetry.instrumentation.api.util.VirtualField; -import io.opentelemetry.javaagent.bootstrap.CallDepth; import io.opentelemetry.javaagent.bootstrap.Java8BytecodeBridge; import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; import io.opentelemetry.javaagent.instrumentation.hibernate.HibernateOperation; +import io.opentelemetry.javaagent.instrumentation.hibernate.HibernateOperationScope; import io.opentelemetry.javaagent.instrumentation.hibernate.SessionInfo; import net.bytebuddy.asm.Advice; import net.bytebuddy.description.type.TypeDescription; @@ -96,20 +95,15 @@ public void transform(TypeTransformer transformer) { public static class SessionMethodAdvice { @Advice.OnMethodEnter(suppress = Throwable.class) - public static void startMethod( + public static HibernateOperationScope startMethod( @Advice.This SharedSessionContract session, @Advice.Origin("#m") String name, @Advice.Origin("#d") String descriptor, @Advice.Argument(0) Object arg0, - @Advice.Argument(value = 1, optional = true) Object arg1, - @Advice.Local("otelCallDepth") CallDepth callDepth, - @Advice.Local("otelHibernateOperation") HibernateOperation hibernateOperation, - @Advice.Local("otelContext") Context context, - @Advice.Local("otelScope") Scope scope) { - - callDepth = CallDepth.forClass(HibernateOperation.class); - if (callDepth.getAndIncrement() > 0) { - return; + @Advice.Argument(value = 1, optional = true) Object arg1) { + + if (HibernateOperationScope.enterDepthSkipCheck()) { + return null; } VirtualField virtualField = @@ -119,32 +113,17 @@ public static void startMethod( Context parentContext = Java8BytecodeBridge.currentContext(); String entityName = getEntityName(descriptor, arg0, arg1, EntityNameUtil.bestGuessEntityName(session)); - hibernateOperation = + HibernateOperation hibernateOperation = new HibernateOperation(getSessionMethodOperationName(name), entityName, sessionInfo); - if (!instrumenter().shouldStart(parentContext, hibernateOperation)) { - return; - } - context = instrumenter().start(parentContext, hibernateOperation); - scope = context.makeCurrent(); + return HibernateOperationScope.start(hibernateOperation, parentContext, instrumenter()); } @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) public static void endMethod( - @Advice.Thrown Throwable throwable, - @Advice.Local("otelCallDepth") CallDepth callDepth, - @Advice.Local("otelHibernateOperation") HibernateOperation hibernateOperation, - @Advice.Local("otelContext") Context context, - @Advice.Local("otelScope") Scope scope) { - - if (callDepth.decrementAndGet() > 0) { - return; - } + @Advice.Thrown Throwable throwable, @Advice.Enter HibernateOperationScope scope) { - if (scope != null) { - scope.close(); - instrumenter().end(context, hibernateOperation, null, throwable); - } + HibernateOperationScope.end(scope, throwable); } } diff --git a/instrumentation/hibernate/hibernate-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/v4_0/TransactionInstrumentation.java b/instrumentation/hibernate/hibernate-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/v4_0/TransactionInstrumentation.java index 16be5e006ad9..738cf85db6b1 100644 --- a/instrumentation/hibernate/hibernate-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/v4_0/TransactionInstrumentation.java +++ b/instrumentation/hibernate/hibernate-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/v4_0/TransactionInstrumentation.java @@ -13,13 +13,12 @@ import static net.bytebuddy.matcher.ElementMatchers.takesArguments; import io.opentelemetry.context.Context; -import io.opentelemetry.context.Scope; import io.opentelemetry.instrumentation.api.util.VirtualField; -import io.opentelemetry.javaagent.bootstrap.CallDepth; import io.opentelemetry.javaagent.bootstrap.Java8BytecodeBridge; import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; import io.opentelemetry.javaagent.instrumentation.hibernate.HibernateOperation; +import io.opentelemetry.javaagent.instrumentation.hibernate.HibernateOperationScope; import io.opentelemetry.javaagent.instrumentation.hibernate.SessionInfo; import net.bytebuddy.asm.Advice; import net.bytebuddy.description.type.TypeDescription; @@ -49,16 +48,10 @@ public void transform(TypeTransformer transformer) { public static class TransactionCommitAdvice { @Advice.OnMethodEnter(suppress = Throwable.class) - public static void startCommit( - @Advice.This Transaction transaction, - @Advice.Local("otelCallDepth") CallDepth callDepth, - @Advice.Local("otelHibernateOperation") HibernateOperation hibernateOperation, - @Advice.Local("otelContext") Context context, - @Advice.Local("otelScope") Scope scope) { + public static HibernateOperationScope startCommit(@Advice.This Transaction transaction) { - callDepth = CallDepth.forClass(HibernateOperation.class); - if (callDepth.getAndIncrement() > 0) { - return; + if (HibernateOperationScope.enterDepthSkipCheck()) { + return null; } VirtualField transactionVirtualField = @@ -66,31 +59,17 @@ public static void startCommit( SessionInfo sessionInfo = transactionVirtualField.get(transaction); Context parentContext = Java8BytecodeBridge.currentContext(); - hibernateOperation = new HibernateOperation("Transaction.commit", sessionInfo); - if (!instrumenter().shouldStart(parentContext, hibernateOperation)) { - return; - } + HibernateOperation hibernateOperation = + new HibernateOperation("Transaction.commit", sessionInfo); - context = instrumenter().start(parentContext, hibernateOperation); - scope = context.makeCurrent(); + return HibernateOperationScope.start(hibernateOperation, parentContext, instrumenter()); } @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) public static void endCommit( - @Advice.Thrown Throwable throwable, - @Advice.Local("otelCallDepth") CallDepth callDepth, - @Advice.Local("otelHibernateOperation") HibernateOperation hibernateOperation, - @Advice.Local("otelContext") Context context, - @Advice.Local("otelScope") Scope scope) { - - if (callDepth.decrementAndGet() > 0) { - return; - } + @Advice.Thrown Throwable throwable, @Advice.Enter HibernateOperationScope scope) { - if (scope != null) { - scope.close(); - instrumenter().end(context, hibernateOperation, null, throwable); - } + HibernateOperationScope.end(scope, throwable); } } } diff --git a/instrumentation/hibernate/hibernate-4.0/javaagent/src/test/groovy/AbstractHibernateTest.groovy b/instrumentation/hibernate/hibernate-4.0/javaagent/src/test/groovy/AbstractHibernateTest.groovy deleted file mode 100644 index 79c3b8ddf288..000000000000 --- a/instrumentation/hibernate/hibernate-4.0/javaagent/src/test/groovy/AbstractHibernateTest.groovy +++ /dev/null @@ -1,39 +0,0 @@ -/* - * 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 < 5; 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/instrumentation/hibernate/hibernate-4.0/javaagent/src/test/groovy/CriteriaTest.groovy b/instrumentation/hibernate/hibernate-4.0/javaagent/src/test/groovy/CriteriaTest.groovy deleted file mode 100644 index 39289c20fa5c..000000000000 --- a/instrumentation/hibernate/hibernate-4.0/javaagent/src/test/groovy/CriteriaTest.groovy +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -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 - -import static io.opentelemetry.api.trace.SpanKind.CLIENT -import static io.opentelemetry.api.trace.SpanKind.INTERNAL - -class CriteriaTest extends AbstractHibernateTest { - - def "test criteria.#methodName"() { - setup: - runWithSpan("parent") { - 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: - def sessionId - assertTraces(1) { - trace(0, 4) { - span(0) { - name "parent" - kind INTERNAL - hasNoParent() - attributes { - } - } - span(1) { - name "Criteria.$methodName Value" - kind INTERNAL - childOf span(0) - attributes { - "hibernate.session_id" { - sessionId = it - it instanceof String - } - } - } - span(2) { - name "SELECT db1.Value" - kind CLIENT - childOf span(1) - attributes { - "$SemanticAttributes.DB_SYSTEM" "h2" - "$SemanticAttributes.DB_NAME" "db1" - "$SemanticAttributes.DB_USER" "sa" - "$SemanticAttributes.DB_CONNECTION_STRING" "h2:mem:" - "$SemanticAttributes.DB_STATEMENT" ~/^select / - "$SemanticAttributes.DB_OPERATION" "SELECT" - "$SemanticAttributes.DB_SQL_TABLE" "Value" - } - } - span(3) { - name "Transaction.commit" - kind INTERNAL - childOf span(0) - attributes { - "hibernate.session_id" sessionId - } - } - } - } - - where: - methodName | interaction - "list" | { c -> c.list() } - "uniqueResult" | { c -> c.uniqueResult() } - "scroll" | { c -> c.scroll() } - } -} diff --git a/instrumentation/hibernate/hibernate-4.0/javaagent/src/test/groovy/EntityManagerTest.groovy b/instrumentation/hibernate/hibernate-4.0/javaagent/src/test/groovy/EntityManagerTest.groovy deleted file mode 100644 index a37b82924604..000000000000 --- a/instrumentation/hibernate/hibernate-4.0/javaagent/src/test/groovy/EntityManagerTest.groovy +++ /dev/null @@ -1,234 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes -import spock.lang.Shared -import spock.lang.Unroll - -import javax.persistence.EntityManager -import javax.persistence.EntityManagerFactory -import javax.persistence.EntityTransaction -import javax.persistence.LockModeType -import javax.persistence.Persistence -import javax.persistence.Query - -import static io.opentelemetry.api.trace.SpanKind.CLIENT -import static io.opentelemetry.api.trace.SpanKind.INTERNAL - -class EntityManagerTest extends AbstractHibernateTest { - - @Shared - EntityManagerFactory entityManagerFactory = Persistence.createEntityManagerFactory("test-pu") - - @Unroll - def "test hibernate action #testName"() { - setup: - EntityManager entityManager = entityManagerFactory.createEntityManager() - EntityTransaction entityTransaction = entityManager.getTransaction() - entityTransaction.begin() - - def entity = prepopulated.get(0) - if (attach) { - entity = runWithSpan("setup") { - entityManager.merge(prepopulated.get(0)) - } - ignoreTracesAndClear(1) - } - - when: - runWithSpan("parent") { - try { - sessionMethodTest.call(entityManager, entity) - } catch (Exception e) { - // We expected this, we should see the error field set on the span. - } - - entityTransaction.commit() - entityManager.close() - } - - then: - boolean isPersistTest = "persist" == testName - def sessionId - assertTraces(1) { - trace(0, 4 + (isPersistTest ? 1 : 0)) { - span(0) { - name "parent" - kind INTERNAL - hasNoParent() - attributes { - } - } - span(1) { - name ~/Session.$methodName $resource/ - kind INTERNAL - childOf span(0) - attributes { - "hibernate.session_id" { - sessionId = it - it instanceof String - } - } - } - - def offset = 0 - if (isPersistTest) { - // persist test has an extra query for getting id of inserted element - offset = 1 - span(2) { - name "SELECT db1.Value" - childOf span(1) - kind CLIENT - attributes { - "$SemanticAttributes.DB_SYSTEM" "h2" - "$SemanticAttributes.DB_NAME" "db1" - "$SemanticAttributes.DB_USER" "sa" - "$SemanticAttributes.DB_CONNECTION_STRING" "h2:mem:" - "$SemanticAttributes.DB_STATEMENT" String - "$SemanticAttributes.DB_OPERATION" String - "$SemanticAttributes.DB_SQL_TABLE" "Value" - } - } - } - - if (!flushOnCommit) { - span(2 + offset) { - childOf span(1) - kind CLIENT - attributes { - "$SemanticAttributes.DB_SYSTEM" "h2" - "$SemanticAttributes.DB_NAME" "db1" - "$SemanticAttributes.DB_USER" "sa" - "$SemanticAttributes.DB_CONNECTION_STRING" "h2:mem:" - "$SemanticAttributes.DB_STATEMENT" String - "$SemanticAttributes.DB_OPERATION" String - "$SemanticAttributes.DB_SQL_TABLE" "Value" - } - } - span(3 + offset) { - name "Transaction.commit" - kind INTERNAL - childOf span(0) - attributes { - "hibernate.session_id" sessionId - } - } - } else { - span(2 + offset) { - name "Transaction.commit" - kind INTERNAL - childOf span(0) - attributes { - "hibernate.session_id" sessionId - } - } - span(3 + offset) { - childOf span(2 + offset) - kind CLIENT - attributes { - "$SemanticAttributes.DB_SYSTEM" "h2" - "$SemanticAttributes.DB_NAME" "db1" - "$SemanticAttributes.DB_USER" "sa" - "$SemanticAttributes.DB_CONNECTION_STRING" "h2:mem:" - "$SemanticAttributes.DB_STATEMENT" String - "$SemanticAttributes.DB_OPERATION" String - "$SemanticAttributes.DB_SQL_TABLE" "Value" - } - } - } - } - } - - where: - testName | methodName | resource | attach | flushOnCommit | sessionMethodTest - "lock" | "lock" | "Value" | true | false | { em, val -> - em.lock(val, LockModeType.PESSIMISTIC_READ) - } - "refresh" | "refresh" | "Value" | true | false | { em, val -> - em.refresh(val) - } - "find" | "(get|find)" | "Value" | false | false | { em, val -> - em.find(Value, val.getId()) - } - "persist" | "persist" | "Value" | false | true | { em, val -> - em.persist(new Value("insert me")) - } - "merge" | "merge" | "Value" | true | true | { em, val -> - val.setName("New name") - em.merge(val) - } - "remove" | "delete" | "Value" | true | true | { em, val -> - em.remove(val) - } - } - - @Unroll - def "test attaches State to query created via #queryMethodName"() { - setup: - runWithSpan("parent") { - EntityManager entityManager = entityManagerFactory.createEntityManager() - EntityTransaction entityTransaction = entityManager.getTransaction() - entityTransaction.begin() - Query query = queryBuildMethod(entityManager) - query.getResultList() - entityTransaction.commit() - entityManager.close() - } - - expect: - def sessionId - assertTraces(1) { - trace(0, 4) { - span(0) { - name "parent" - kind INTERNAL - hasNoParent() - attributes { - } - } - span(1) { - name resource - kind INTERNAL - childOf span(0) - attributes { - "hibernate.session_id" { - sessionId = it - it instanceof String - } - } - } - span(2) { - name "SELECT db1.Value" - kind CLIENT - childOf span(1) - attributes { - "$SemanticAttributes.DB_SYSTEM" "h2" - "$SemanticAttributes.DB_NAME" "db1" - "$SemanticAttributes.DB_USER" "sa" - "$SemanticAttributes.DB_CONNECTION_STRING" "h2:mem:" - "$SemanticAttributes.DB_STATEMENT" String - "$SemanticAttributes.DB_OPERATION" "SELECT" - "$SemanticAttributes.DB_SQL_TABLE" "Value" - } - } - span(3) { - name "Transaction.commit" - kind INTERNAL - childOf span(0) - attributes { - "hibernate.session_id" sessionId - } - } - } - } - - where: - queryMethodName | resource | queryBuildMethod - "createQuery" | "SELECT Value" | { em -> em.createQuery("from Value") } - "getNamedQuery" | "SELECT Value" | { em -> em.createNamedQuery("TestNamedQuery") } - "createSQLQuery" | "SELECT Value" | { em -> em.createNativeQuery("SELECT * FROM Value") } - } - -} diff --git a/instrumentation/hibernate/hibernate-4.0/javaagent/src/test/groovy/QueryTest.groovy b/instrumentation/hibernate/hibernate-4.0/javaagent/src/test/groovy/QueryTest.groovy deleted file mode 100644 index 55bef913494d..000000000000 --- a/instrumentation/hibernate/hibernate-4.0/javaagent/src/test/groovy/QueryTest.groovy +++ /dev/null @@ -1,206 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes -import org.hibernate.Query -import org.hibernate.Session - -import static io.opentelemetry.api.trace.SpanKind.CLIENT -import static io.opentelemetry.api.trace.SpanKind.INTERNAL - -class QueryTest extends AbstractHibernateTest { - - def "test hibernate query.#queryMethodName single call"() { - setup: - - // With Transaction - runWithSpan("parent") { - Session session = sessionFactory.openSession() - session.beginTransaction() - queryInteraction(session) - session.getTransaction().commit() - session.close() - } - - // Without Transaction - if (!requiresTransaction) { - runWithSpan("parent2") { - Session session = sessionFactory.openSession() - queryInteraction(session) - session.close() - } - } - - expect: - def sessionId - assertTraces(requiresTransaction ? 1 : 2) { - // With Transaction - trace(0, 4) { - span(0) { - name "parent" - kind INTERNAL - hasNoParent() - attributes { - } - } - span(1) { - name expectedSpanName - kind INTERNAL - childOf span(0) - attributes { - "hibernate.session_id" { - sessionId = it - it instanceof String - } - } - } - span(2) { - kind CLIENT - childOf span(1) - attributes { - "$SemanticAttributes.DB_SYSTEM" "h2" - "$SemanticAttributes.DB_NAME" "db1" - "$SemanticAttributes.DB_USER" "sa" - "$SemanticAttributes.DB_CONNECTION_STRING" "h2:mem:" - "$SemanticAttributes.DB_STATEMENT" String - "$SemanticAttributes.DB_OPERATION" String - "$SemanticAttributes.DB_SQL_TABLE" "Value" - } - } - span(3) { - name "Transaction.commit" - kind INTERNAL - childOf span(0) - attributes { - "hibernate.session_id" sessionId - } - } - } - if (!requiresTransaction) { - // Without Transaction - trace(1, 3) { - span(0) { - name "parent2" - kind INTERNAL - hasNoParent() - attributes { - } - } - span(1) { - name expectedSpanName - kind INTERNAL - childOf span(0) - attributes { - "hibernate.session_id" String - } - } - span(2) { - name "SELECT db1.Value" - kind CLIENT - childOf span(1) - attributes { - "$SemanticAttributes.DB_SYSTEM" "h2" - "$SemanticAttributes.DB_NAME" "db1" - "$SemanticAttributes.DB_USER" "sa" - "$SemanticAttributes.DB_CONNECTION_STRING" "h2:mem:" - "$SemanticAttributes.DB_STATEMENT" ~/^select / - "$SemanticAttributes.DB_OPERATION" "SELECT" - "$SemanticAttributes.DB_SQL_TABLE" "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: - - runWithSpan("parent") { - Session session = sessionFactory.openSession() - session.beginTransaction() - Query q = session.createQuery("from Value") - Iterator iterator = q.iterate() - while (iterator.hasNext()) { - iterator.next() - } - session.getTransaction().commit() - session.close() - } - - expect: - def sessionId - assertTraces(1) { - trace(0, 4) { - span(0) { - name "parent" - kind INTERNAL - hasNoParent() - attributes { - } - } - span(1) { - name "SELECT Value" - kind INTERNAL - childOf span(0) - attributes { - "hibernate.session_id" { - sessionId = it - it instanceof String - } - } - } - span(2) { - name "SELECT db1.Value" - kind CLIENT - childOf span(1) - attributes { - "$SemanticAttributes.DB_SYSTEM" "h2" - "$SemanticAttributes.DB_NAME" "db1" - "$SemanticAttributes.DB_USER" "sa" - "$SemanticAttributes.DB_CONNECTION_STRING" "h2:mem:" - "$SemanticAttributes.DB_STATEMENT" ~/^select / - "$SemanticAttributes.DB_OPERATION" "SELECT" - "$SemanticAttributes.DB_SQL_TABLE" "Value" - } - } - span(3) { - name "Transaction.commit" - kind INTERNAL - childOf span(0) - attributes { - "hibernate.session_id" sessionId - } - } - } - } - } - -} diff --git a/instrumentation/hibernate/hibernate-4.0/javaagent/src/test/groovy/SessionTest.groovy b/instrumentation/hibernate/hibernate-4.0/javaagent/src/test/groovy/SessionTest.groovy deleted file mode 100644 index be37fe77337a..000000000000 --- a/instrumentation/hibernate/hibernate-4.0/javaagent/src/test/groovy/SessionTest.groovy +++ /dev/null @@ -1,605 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes -import org.hibernate.LockMode -import org.hibernate.LockOptions -import org.hibernate.MappingException -import org.hibernate.Query -import org.hibernate.ReplicationMode -import org.hibernate.Session -import spock.lang.Shared - -import static io.opentelemetry.api.trace.SpanKind.CLIENT -import static io.opentelemetry.api.trace.SpanKind.INTERNAL -import static io.opentelemetry.api.trace.StatusCode.ERROR - -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) { - runWithSpan("parent") { - 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: - def sessionId - assertTraces(sessionImplementations.size()) { - for (int i = 0; i < sessionImplementations.size(); i++) { - trace(i, 4) { - span(0) { - name "parent" - kind INTERNAL - hasNoParent() - attributes { - } - } - span(1) { - name "Session.$methodName $resource" - kind INTERNAL - childOf span(0) - attributes { - "hibernate.session_id" { - sessionId = it - it instanceof String - } - } - } - span(2) { - childOf span(1) - kind CLIENT - attributes { - "$SemanticAttributes.DB_SYSTEM" "h2" - "$SemanticAttributes.DB_NAME" "db1" - "$SemanticAttributes.DB_USER" "sa" - "$SemanticAttributes.DB_CONNECTION_STRING" "h2:mem:" - "$SemanticAttributes.DB_STATEMENT" String - "$SemanticAttributes.DB_OPERATION" String - "$SemanticAttributes.DB_SQL_TABLE" "Value" - } - } - span(3) { - name "Transaction.commit" - kind INTERNAL - childOf span(0) - attributes { - "hibernate.session_id" sessionId - } - } - } - } - } - - where: - testName | methodName | resource | sessionImplementations | sessionMethodTest - "lock" | "lock" | "Value" | [sessionBuilder] | { sesh, val -> - sesh.lock(val, LockMode.READ) - } - "lock with entity name" | "lock" | "Value" | [sessionBuilder] | { sesh, val -> - sesh.lock("Value", val, LockMode.READ) - } - "lock with null name" | "lock" | "Value" | [sessionBuilder] | { sesh, val -> - sesh.lock(null, val, LockMode.READ) - } - "buildLockRequest" | "lock" | "Value" | [sessionBuilder] | { sesh, val -> - sesh.buildLockRequest(LockOptions.READ) - .lock(val) - } - "refresh" | "refresh" | "Value" | [sessionBuilder, statelessSessionBuilder] | { sesh, val -> - sesh.refresh(val) - } - "refresh with entity name" | "refresh" | "Value" | [sessionBuilder, statelessSessionBuilder] | { sesh, val -> - sesh.refresh("Value", val) - } - "get with entity name" | "get" | "Value" | [sessionBuilder, statelessSessionBuilder] | { sesh, val -> - sesh.get("Value", val.getId()) - } - "get with entity class" | "get" | "Value" | [sessionBuilder, statelessSessionBuilder] | { sesh, val -> - sesh.get(Value, val.getId()) - } - "insert" | "insert" | "Value" | [statelessSessionBuilder] | { sesh, val -> - sesh.insert(new Value("insert me")) - } - "insert with entity name" | "insert" | "Value" | [statelessSessionBuilder] | { sesh, val -> - sesh.insert("Value", new Value("insert me")) - } - "insert with null entity name" | "insert" | "Value" | [statelessSessionBuilder] | { sesh, val -> - sesh.insert(null, new Value("insert me")) - } - "update (StatelessSession)" | "update" | "Value" | [statelessSessionBuilder] | { sesh, val -> - val.setName("New name") - sesh.update(val) - } - "update with entity name (StatelessSession)" | "update" | "Value" | [statelessSessionBuilder] | { sesh, val -> - val.setName("New name") - sesh.update("Value", val) - } - "delete (Session)" | "delete" | "Value" | [statelessSessionBuilder] | { sesh, val -> - sesh.delete(val) - prepopulated.remove(val) - } - "delete with entity name (Session)" | "delete" | "Value" | [statelessSessionBuilder] | { sesh, val -> - sesh.delete("Value", val) - prepopulated.remove(val) - } - } - - def "test hibernate replicate: #testName"() { - setup: - - // Test for each implementation of Session. - runWithSpan("parent") { - 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: - def sessionId - assertTraces(1) { - trace(0, 5) { - span(0) { - name "parent" - kind INTERNAL - hasNoParent() - attributes { - } - } - span(1) { - name "Session.$methodName $resource" - kind INTERNAL - childOf span(0) - attributes { - "hibernate.session_id" { - sessionId = it - it instanceof String - } - } - } - span(2) { - name "SELECT db1.Value" - kind CLIENT - childOf span(1) - attributes { - "$SemanticAttributes.DB_SYSTEM" "h2" - "$SemanticAttributes.DB_NAME" "db1" - "$SemanticAttributes.DB_USER" "sa" - "$SemanticAttributes.DB_CONNECTION_STRING" "h2:mem:" - "$SemanticAttributes.DB_STATEMENT" ~/^select / - "$SemanticAttributes.DB_OPERATION" "SELECT" - "$SemanticAttributes.DB_SQL_TABLE" "Value" - } - } - span(3) { - name "Transaction.commit" - kind INTERNAL - childOf span(0) - attributes { - "hibernate.session_id" sessionId - } - } - span(4) { - kind CLIENT - childOf span(3) - attributes { - "$SemanticAttributes.DB_SYSTEM" "h2" - "$SemanticAttributes.DB_NAME" "db1" - "$SemanticAttributes.DB_USER" "sa" - "$SemanticAttributes.DB_CONNECTION_STRING" "h2:mem:" - "$SemanticAttributes.DB_STATEMENT" String - "$SemanticAttributes.DB_OPERATION" String - "$SemanticAttributes.DB_SQL_TABLE" "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. - runWithSpan("parent") { - 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: - def sessionId - assertTraces(1) { - trace(0, 3) { - span(0) { - name "parent" - kind INTERNAL - hasNoParent() - attributes { - } - } - span(1) { - name "Session.replicate java.lang.Long" - kind INTERNAL - childOf span(0) - status ERROR - errorEvent(MappingException, "Unknown entity: java.lang.Long") - attributes { - "hibernate.session_id" { - sessionId = it - it instanceof String - } - } - } - span(2) { - name "Transaction.commit" - kind INTERNAL - childOf span(0) - attributes { - "hibernate.session_id" sessionId - } - } - } - - } - } - - - def "test hibernate commit action #testName"() { - setup: - - runWithSpan("parent") { - 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: - def sessionId - assertTraces(1) { - trace(0, 4) { - span(0) { - name "parent" - kind INTERNAL - hasNoParent() - attributes { - } - } - span(1) { - name "Session.$methodName $resource" - kind INTERNAL - childOf span(0) - attributes { - "hibernate.session_id" { - sessionId = it - it instanceof String - } - } - } - span(2) { - name "Transaction.commit" - kind INTERNAL - childOf span(0) - attributes { - "hibernate.session_id" sessionId - } - } - span(3) { - kind CLIENT - childOf span(2) - attributes { - "$SemanticAttributes.DB_SYSTEM" "h2" - "$SemanticAttributes.DB_NAME" "db1" - "$SemanticAttributes.DB_USER" "sa" - "$SemanticAttributes.DB_CONNECTION_STRING" "h2:mem:" - "$SemanticAttributes.DB_STATEMENT" String - "$SemanticAttributes.DB_OPERATION" String - "$SemanticAttributes.DB_SQL_TABLE" "Value" - } - } - } - } - - where: - testName | methodName | resource | sessionMethodTest - "save" | "save" | "Value" | { sesh, val -> - sesh.save(new Value("Another value")) - } - "save with entity name" | "save" | "Value" | { sesh, val -> - sesh.save("Value", new Value("Another value")) - } - "saveOrUpdate save" | "saveOrUpdate" | "Value" | { sesh, val -> - sesh.saveOrUpdate(new Value("Value")) - } - "saveOrUpdate save with entity name" | "saveOrUpdate" | "Value" | { sesh, val -> - sesh.saveOrUpdate("Value", new Value("Value")) - } - "saveOrUpdate update with entity name" | "saveOrUpdate" | "Value" | { sesh, val -> - val.setName("New name") - sesh.saveOrUpdate("Value", val) - } - "merge" | "merge" | "Value" | { sesh, val -> - sesh.merge(new Value("merge me in")) - } - "merge with entity name" | "merge" | "Value" | { sesh, val -> - sesh.merge("Value", new Value("merge me in")) - } - "persist" | "persist" | "Value" | { sesh, val -> - sesh.persist(new Value("merge me in")) - } - "persist with entity name" | "persist" | "Value" | { sesh, val -> - sesh.persist("Value", new Value("merge me in")) - } - "persist with null entity name" | "persist" | "Value" | { sesh, val -> - sesh.persist(null, 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) - prepopulated.remove(val) - } - "delete by entityName (Session)" | "delete" | "Value" | { sesh, val -> - sesh.delete("Value", val) - prepopulated.remove(val) - } - } - - - def "test attaches State to query created via #queryMethodName"() { - setup: - runWithSpan("parent") { - Session session = sessionFactory.openSession() - session.beginTransaction() - Query query = queryBuildMethod(session) - query.list() - session.getTransaction().commit() - session.close() - } - - expect: - def sessionId - assertTraces(1) { - trace(0, 4) { - span(0) { - name "parent" - kind INTERNAL - hasNoParent() - attributes { - } - } - span(1) { - name resource - kind INTERNAL - childOf span(0) - attributes { - "hibernate.session_id" { - sessionId = it - it instanceof String - } - } - } - span(2) { - kind CLIENT - childOf span(1) - attributes { - "$SemanticAttributes.DB_SYSTEM" "h2" - "$SemanticAttributes.DB_NAME" "db1" - "$SemanticAttributes.DB_USER" "sa" - "$SemanticAttributes.DB_CONNECTION_STRING" "h2:mem:" - "$SemanticAttributes.DB_STATEMENT" String - "$SemanticAttributes.DB_OPERATION" "SELECT" - "$SemanticAttributes.DB_SQL_TABLE" "Value" - } - } - span(3) { - name "Transaction.commit" - kind INTERNAL - childOf span(0) - attributes { - "hibernate.session_id" sessionId - } - } - } - } - - 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: - - runWithSpan("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: - def sessionId1 - def sessionId2 - def sessionId3 - assertTraces(1) { - trace(0, 9) { - span(0) { - name "overlapping Sessions" - attributes { - } - } - span(1) { - name "Session.save Value" - kind INTERNAL - childOf span(0) - attributes { - "hibernate.session_id" { - sessionId1 = it - it instanceof String - } - } - } - span(2) { - name "Session.insert Value" - kind INTERNAL - childOf span(0) - attributes { - "hibernate.session_id" { - sessionId2 = it - it instanceof String - } - } - } - span(3) { - name "INSERT db1.Value" - kind CLIENT - childOf span(2) - attributes { - "$SemanticAttributes.DB_SYSTEM" "h2" - "$SemanticAttributes.DB_NAME" "db1" - "$SemanticAttributes.DB_USER" "sa" - "$SemanticAttributes.DB_CONNECTION_STRING" "h2:mem:" - "$SemanticAttributes.DB_STATEMENT" ~/^insert / - "$SemanticAttributes.DB_OPERATION" "INSERT" - "$SemanticAttributes.DB_SQL_TABLE" "Value" - } - } - span(4) { - name "Session.save Value" - kind INTERNAL - childOf span(0) - attributes { - "hibernate.session_id" { - sessionId3 = it - it instanceof String - } - } - } - span(5) { - name "Session.delete Value" - kind INTERNAL - childOf span(0) - attributes { - "hibernate.session_id" sessionId1 - } - } - span(6) { - name "Transaction.commit" - kind INTERNAL - childOf span(0) - attributes { - "hibernate.session_id" sessionId1 - } - } - span(7) { - name "INSERT db1.Value" - kind CLIENT - childOf span(6) - attributes { - "$SemanticAttributes.DB_SYSTEM" "h2" - "$SemanticAttributes.DB_NAME" "db1" - "$SemanticAttributes.DB_USER" "sa" - "$SemanticAttributes.DB_CONNECTION_STRING" "h2:mem:" - "$SemanticAttributes.DB_STATEMENT" ~/^insert / - "$SemanticAttributes.DB_OPERATION" "INSERT" - "$SemanticAttributes.DB_SQL_TABLE" "Value" - } - } - span(8) { - name "DELETE db1.Value" - kind CLIENT - childOf span(6) - attributes { - "$SemanticAttributes.DB_SYSTEM" "h2" - "$SemanticAttributes.DB_NAME" "db1" - "$SemanticAttributes.DB_USER" "sa" - "$SemanticAttributes.DB_CONNECTION_STRING" "h2:mem:" - "$SemanticAttributes.DB_STATEMENT" ~/^delete / - "$SemanticAttributes.DB_OPERATION" "DELETE" - "$SemanticAttributes.DB_SQL_TABLE" "Value" - } - } - } - } - sessionId1 != sessionId2 != sessionId3 - } -} diff --git a/instrumentation/hibernate/hibernate-4.0/javaagent/src/test/groovy/SpringJpaTest.groovy b/instrumentation/hibernate/hibernate-4.0/javaagent/src/test/groovy/SpringJpaTest.groovy deleted file mode 100644 index ee29738d527c..000000000000 --- a/instrumentation/hibernate/hibernate-4.0/javaagent/src/test/groovy/SpringJpaTest.groovy +++ /dev/null @@ -1,385 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes -import org.hibernate.Version -import org.springframework.context.annotation.AnnotationConfigApplicationContext -import spock.lang.Shared -import spring.jpa.Customer -import spring.jpa.CustomerRepository -import spring.jpa.PersistenceConfig - -import static io.opentelemetry.api.trace.SpanKind.CLIENT -import static io.opentelemetry.api.trace.SpanKind.INTERNAL - -class SpringJpaTest extends AgentInstrumentationSpecification { - - @Shared - def context = new AnnotationConfigApplicationContext(PersistenceConfig) - - @Shared - def repo = context.getBean(CustomerRepository) - - def "test CRUD"() { - setup: - def isHibernate4 = Version.getVersionString().startsWith("4.") - def customer = new Customer("Bob", "Anonymous") - - expect: - customer.id == null - !runWithSpan("parent") { - repo.findAll().iterator().hasNext() - } - - def sessionId - assertTraces(1) { - trace(0, 4) { - span(0) { - name "parent" - kind INTERNAL - hasNoParent() - attributes { - } - } - span(1) { - name "SELECT Customer" - kind INTERNAL - childOf span(0) - attributes { - "hibernate.session_id" { - sessionId = it - it instanceof String - } - } - } - span(2) { - name "SELECT test.Customer" - kind CLIENT - childOf span(1) - attributes { - "$SemanticAttributes.DB_SYSTEM" "hsqldb" - "$SemanticAttributes.DB_NAME" "test" - "$SemanticAttributes.DB_USER" "sa" - "$SemanticAttributes.DB_CONNECTION_STRING" "hsqldb:mem:" - "$SemanticAttributes.DB_STATEMENT" ~/select ([^.]+)\.id([^,]*), ([^.]+)\.firstName([^,]*), ([^.]+)\.lastName(.*)from Customer(.*)/ - "$SemanticAttributes.DB_OPERATION" "SELECT" - "$SemanticAttributes.DB_SQL_TABLE" "Customer" - } - } - span(3) { - name "Transaction.commit" - kind INTERNAL - childOf span(0) - attributes { - "hibernate.session_id" sessionId - } - } - } - } - clearExportedData() - - when: - runWithSpan("parent") { - repo.save(customer) - } - def savedId = customer.id - - then: - customer.id != null - def sessionId2 - assertTraces(1) { - trace(0, 4 + (isHibernate4 ? 0 : 1)) { - span(0) { - name "parent" - kind INTERNAL - hasNoParent() - attributes { - } - } - span(1) { - name "Session.persist spring.jpa.Customer" - kind INTERNAL - childOf span(0) - attributes { - "hibernate.session_id" { - sessionId2 = it - it instanceof String - } - } - } - if (!isHibernate4) { - span(2) { - name "CALL test" - kind CLIENT - childOf span(1) - attributes { - "$SemanticAttributes.DB_SYSTEM" "hsqldb" - "$SemanticAttributes.DB_NAME" "test" - "$SemanticAttributes.DB_USER" "sa" - "$SemanticAttributes.DB_STATEMENT" "call next value for hibernate_sequence" - "$SemanticAttributes.DB_CONNECTION_STRING" "hsqldb:mem:" - "$SemanticAttributes.DB_OPERATION" "CALL" - } - } - span(3) { - name "Transaction.commit" - kind INTERNAL - childOf span(0) - attributes { - "hibernate.session_id" sessionId2 - } - } - span(4) { - name "INSERT test.Customer" - kind CLIENT - childOf span(3) - attributes { - "$SemanticAttributes.DB_SYSTEM" "hsqldb" - "$SemanticAttributes.DB_NAME" "test" - "$SemanticAttributes.DB_USER" "sa" - "$SemanticAttributes.DB_CONNECTION_STRING" "hsqldb:mem:" - "$SemanticAttributes.DB_STATEMENT" ~/insert into Customer \(.*\) values \(.*, \?, \?\)/ - "$SemanticAttributes.DB_OPERATION" "INSERT" - "$SemanticAttributes.DB_SQL_TABLE" "Customer" - } - } - } else { - span(2) { - name "INSERT test.Customer" - kind CLIENT - childOf span(1) - attributes { - "$SemanticAttributes.DB_SYSTEM" "hsqldb" - "$SemanticAttributes.DB_NAME" "test" - "$SemanticAttributes.DB_USER" "sa" - "$SemanticAttributes.DB_CONNECTION_STRING" "hsqldb:mem:" - "$SemanticAttributes.DB_STATEMENT" ~/insert into Customer \(.*\) values \(.*, \?, \?\)/ - "$SemanticAttributes.DB_OPERATION" "INSERT" - "$SemanticAttributes.DB_SQL_TABLE" "Customer" - } - } - span(3) { - name "Transaction.commit" - kind INTERNAL - childOf span(0) - attributes { - "hibernate.session_id" sessionId2 - } - } - } - } - } - clearExportedData() - - when: - customer.firstName = "Bill" - runWithSpan("parent") { - repo.save(customer) - } - - then: - customer.id == savedId - def sessionId3 - assertTraces(1) { - trace(0, 5) { - span(0) { - name "parent" - kind INTERNAL - hasNoParent() - attributes { - } - } - span(1) { - name "Session.merge spring.jpa.Customer" - kind INTERNAL - childOf span(0) - attributes { - "hibernate.session_id" { - sessionId3 = it - it instanceof String - } - } - } - span(2) { - name "SELECT test.Customer" - kind CLIENT - attributes { - "$SemanticAttributes.DB_SYSTEM" "hsqldb" - "$SemanticAttributes.DB_NAME" "test" - "$SemanticAttributes.DB_USER" "sa" - "$SemanticAttributes.DB_CONNECTION_STRING" "hsqldb:mem:" - "$SemanticAttributes.DB_STATEMENT" ~/select ([^.]+)\.id([^,]*), ([^.]+)\.firstName([^,]*), ([^.]+)\.lastName (.*)from Customer (.*)where ([^.]+)\.id( ?)=( ?)\?/ - "$SemanticAttributes.DB_OPERATION" "SELECT" - "$SemanticAttributes.DB_SQL_TABLE" "Customer" - } - } - span(3) { - name "Transaction.commit" - kind INTERNAL - childOf span(0) - attributes { - "hibernate.session_id" sessionId3 - } - } - span(4) { - name "UPDATE test.Customer" - kind CLIENT - attributes { - "$SemanticAttributes.DB_SYSTEM" "hsqldb" - "$SemanticAttributes.DB_NAME" "test" - "$SemanticAttributes.DB_USER" "sa" - "$SemanticAttributes.DB_CONNECTION_STRING" "hsqldb:mem:" - "$SemanticAttributes.DB_STATEMENT" "update Customer set firstName=?, lastName=? where id=?" - "$SemanticAttributes.DB_OPERATION" "UPDATE" - "$SemanticAttributes.DB_SQL_TABLE" "Customer" - } - } - } - } - clearExportedData() - - when: - customer = runWithSpan("parent") { - repo.findByLastName("Anonymous")[0] - } - - then: - customer.id == savedId - customer.firstName == "Bill" - assertTraces(1) { - trace(0, 3) { - span(0) { - name "parent" - kind INTERNAL - hasNoParent() - attributes { - } - } - span(1) { - name "SELECT Customer" - kind INTERNAL - childOf span(0) - attributes { - "hibernate.session_id" String - } - } - span(2) { - name "SELECT test.Customer" - kind CLIENT - childOf span(1) - attributes { - "$SemanticAttributes.DB_SYSTEM" "hsqldb" - "$SemanticAttributes.DB_NAME" "test" - "$SemanticAttributes.DB_USER" "sa" - "$SemanticAttributes.DB_CONNECTION_STRING" "hsqldb:mem:" - "$SemanticAttributes.DB_STATEMENT" ~/select ([^.]+)\.id([^,]*), ([^.]+)\.firstName([^,]*), ([^.]+)\.lastName (.*)from Customer (.*)(where ([^.]+)\.lastName( ?)=( ?)\?|)/ - "$SemanticAttributes.DB_OPERATION" "SELECT" - "$SemanticAttributes.DB_SQL_TABLE" "Customer" - } - } - } - } - clearExportedData() - - when: - runWithSpan("parent") { - repo.delete(customer) - } - - then: - assertTraces(1) { - trace(0, 6 + (isHibernate4 ? 0 : 1)) { - span(0) { - name "parent" - kind INTERNAL - hasNoParent() - attributes { - } - } - def offset = 0 - if (!isHibernate4) { - offset = 2 - span(1) { - name ~/Session.(get|find) spring.jpa.Customer/ - kind INTERNAL - childOf span(0) - attributes { - "hibernate.session_id" String - } - } - span(2) { - name "SELECT test.Customer" - kind CLIENT - childOf span(1) - attributes { - "$SemanticAttributes.DB_SYSTEM" "hsqldb" - "$SemanticAttributes.DB_NAME" "test" - "$SemanticAttributes.DB_USER" "sa" - "$SemanticAttributes.DB_CONNECTION_STRING" "hsqldb:mem:" - "$SemanticAttributes.DB_STATEMENT" ~/select ([^.]+)\.id([^,]*), ([^.]+)\.firstName([^,]*), ([^.]+)\.lastName (.*)from Customer (.*)where ([^.]+)\.id( ?)=( ?)\?/ - "$SemanticAttributes.DB_OPERATION" "SELECT" - "$SemanticAttributes.DB_SQL_TABLE" "Customer" - } - } - } - span(1 + offset) { - name "Session.merge spring.jpa.Customer" - kind INTERNAL - childOf span(0) - attributes { - "hibernate.session_id" String - } - } - if (isHibernate4) { - offset = 1 - span(2) { - name "SELECT test.Customer" - kind CLIENT - childOf span(1) - attributes { - "$SemanticAttributes.DB_SYSTEM" "hsqldb" - "$SemanticAttributes.DB_NAME" "test" - "$SemanticAttributes.DB_USER" "sa" - "$SemanticAttributes.DB_CONNECTION_STRING" "hsqldb:mem:" - "$SemanticAttributes.DB_STATEMENT" ~/select ([^.]+)\.id([^,]*), ([^.]+)\.firstName([^,]*), ([^.]+)\.lastName (.*)from Customer (.*)where ([^.]+)\.id( ?)=( ?)\?/ - "$SemanticAttributes.DB_OPERATION" "SELECT" - "$SemanticAttributes.DB_SQL_TABLE" "Customer" - } - } - } - span(2 + offset) { - name "Session.delete spring.jpa.Customer" - kind INTERNAL - childOf span(0) - attributes { - "hibernate.session_id" String - } - } - span(3 + offset) { - name "Transaction.commit" - kind INTERNAL - childOf span(0) - attributes { - "hibernate.session_id" String - } - } - span(4 + offset) { - name "DELETE test.Customer" - kind CLIENT - attributes { - "$SemanticAttributes.DB_SYSTEM" "hsqldb" - "$SemanticAttributes.DB_NAME" "test" - "$SemanticAttributes.DB_USER" "sa" - "$SemanticAttributes.DB_CONNECTION_STRING" "hsqldb:mem:" - "$SemanticAttributes.DB_STATEMENT" "delete from Customer where id=?" - "$SemanticAttributes.DB_OPERATION" "DELETE" - "$SemanticAttributes.DB_SQL_TABLE" "Customer" - } - } - } - } - } -} diff --git a/instrumentation/hibernate/hibernate-4.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/hibernate/v4_0/AbstractHibernateTest.java b/instrumentation/hibernate/hibernate-4.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/hibernate/v4_0/AbstractHibernateTest.java new file mode 100644 index 000000000000..7290e4f85ab0 --- /dev/null +++ b/instrumentation/hibernate/hibernate-4.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/hibernate/v4_0/AbstractHibernateTest.java @@ -0,0 +1,50 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.hibernate.v4_0; + +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import java.util.ArrayList; +import java.util.List; +import org.hibernate.Session; +import org.hibernate.SessionFactory; +import org.hibernate.cfg.Configuration; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.extension.RegisterExtension; + +abstract class AbstractHibernateTest { + + @RegisterExtension + protected static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + protected static SessionFactory sessionFactory; + protected static List prepopulated; + + @BeforeAll + @SuppressWarnings("deprecation") // buildSessionFactory + static void setUp() { + 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 < 5; i++) { + prepopulated.add(new Value("Hello :) " + i)); + writer.save(prepopulated.get(i)); + } + writer.getTransaction().commit(); + writer.close(); + } + + @AfterAll + static void cleanUp() { + if (sessionFactory != null) { + sessionFactory.close(); + } + } +} diff --git a/instrumentation/hibernate/hibernate-4.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/hibernate/v4_0/CriteriaTest.java b/instrumentation/hibernate/hibernate-4.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/hibernate/v4_0/CriteriaTest.java new file mode 100644 index 000000000000..509173d79e80 --- /dev/null +++ b/instrumentation/hibernate/hibernate-4.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/hibernate/v4_0/CriteriaTest.java @@ -0,0 +1,97 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.hibernate.v4_0; + +import static io.opentelemetry.api.common.AttributeKey.stringKey; +import static io.opentelemetry.api.trace.SpanKind.CLIENT; +import static io.opentelemetry.api.trace.SpanKind.INTERNAL; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.satisfies; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.semconv.incubating.DbIncubatingAttributes; +import java.util.function.Consumer; +import java.util.stream.Stream; +import org.hibernate.Criteria; +import org.hibernate.Session; +import org.hibernate.criterion.Order; +import org.hibernate.criterion.Restrictions; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +class CriteriaTest extends AbstractHibernateTest { + + @ParameterizedTest + @MethodSource("provideArguments") + @SuppressWarnings("deprecation") // createCriteria(Class) has been deprecated in v5 + void testCriteria(String methodName, Consumer interaction) { + + testing.runWithSpan( + "parent", + () -> { + Session session = sessionFactory.openSession(); + session.beginTransaction(); + Criteria criteria = + session + .createCriteria(Value.class) + .add(Restrictions.like("name", "Hello")) + .addOrder(Order.desc("name")); + interaction.accept(criteria); + session.getTransaction().commit(); + session.close(); + }); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("parent") + .hasKind(INTERNAL) + .hasNoParent() + .hasAttributes(Attributes.empty()), + span -> + span.hasName("Criteria." + methodName + " " + Value.class.getName()) + .hasKind(INTERNAL) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + satisfies( + stringKey("hibernate.session_id"), + val -> val.isInstanceOf(String.class))), + span -> + span.hasName("SELECT db1.Value") + .hasKind(CLIENT) + .hasParent(trace.getSpan(1)) + .hasAttributesSatisfyingExactly( + equalTo(DbIncubatingAttributes.DB_SYSTEM, "h2"), + equalTo(DbIncubatingAttributes.DB_NAME, "db1"), + equalTo(DbIncubatingAttributes.DB_USER, "sa"), + equalTo(DbIncubatingAttributes.DB_CONNECTION_STRING, "h2:mem:"), + satisfies( + DbIncubatingAttributes.DB_STATEMENT, + stringAssert -> stringAssert.startsWith("select")), + equalTo(DbIncubatingAttributes.DB_OPERATION, "SELECT"), + equalTo(DbIncubatingAttributes.DB_SQL_TABLE, "Value")), + span -> + span.hasName("Transaction.commit") + .hasKind(INTERNAL) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo( + stringKey("hibernate.session_id"), + trace + .getSpan(1) + .getAttributes() + .get(stringKey("hibernate.session_id")))))); + } + + private static Stream provideArguments() { + return Stream.of( + Arguments.of("list", (Consumer) Criteria::list), + Arguments.of("uniqueResult", (Consumer) Criteria::uniqueResult), + Arguments.of("scroll", (Consumer) Criteria::scroll)); + } +} diff --git a/instrumentation/hibernate/hibernate-4.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/hibernate/v4_0/EntityManagerTest.java b/instrumentation/hibernate/hibernate-4.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/hibernate/v4_0/EntityManagerTest.java new file mode 100644 index 000000000000..d9402fc439ca --- /dev/null +++ b/instrumentation/hibernate/hibernate-4.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/hibernate/v4_0/EntityManagerTest.java @@ -0,0 +1,353 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.hibernate.v4_0; + +import static io.opentelemetry.api.common.AttributeKey.stringKey; +import static io.opentelemetry.api.trace.SpanKind.CLIENT; +import static io.opentelemetry.api.trace.SpanKind.INTERNAL; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.satisfies; +import static org.junit.jupiter.api.Named.named; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.semconv.incubating.DbIncubatingAttributes; +import java.util.function.BiConsumer; +import java.util.function.Function; +import java.util.stream.Stream; +import javax.persistence.EntityManager; +import javax.persistence.EntityManagerFactory; +import javax.persistence.EntityTransaction; +import javax.persistence.LockModeType; +import javax.persistence.Persistence; +import javax.persistence.Query; +import org.hibernate.Version; +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 EntityManagerTest extends AbstractHibernateTest { + + static final EntityManagerFactory entityManagerFactory = + Persistence.createEntityManagerFactory("test-pu"); + + @SuppressWarnings("deprecation") // TODO DbIncubatingAttributes.DB_CONNECTION_STRING deprecation + @ParameterizedTest + @MethodSource("provideArgumentsHibernateActionParameters") + void testHibernateActions(Parameter parameter) { + EntityManager entityManager = entityManagerFactory.createEntityManager(); + EntityTransaction entityTransaction = entityManager.getTransaction(); + entityTransaction.begin(); + + Value entity; + if (parameter.attach) { + entity = testing.runWithSpan("setup", () -> entityManager.merge(prepopulated.get(0))); + testing.clearData(); + } else { + entity = prepopulated.get(0); + } + + String version = Version.getVersionString(); + boolean isHibernate4 = version.startsWith("4."); + boolean isLatestDep = version.startsWith("5.0"); + String action; + if ((isHibernate4 || isLatestDep) && "find".equals(parameter.methodName)) { + action = "get"; + } else { + action = parameter.methodName; + } + + testing.runWithSpan( + "parent", + () -> { + parameter.sessionMethodTest.accept(entityManager, entity); + entityTransaction.commit(); + entityManager.close(); + }); + + testing.waitAndAssertTraces( + trace -> { + if (parameter.flushOnCommit) { + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("parent") + .hasKind(INTERNAL) + .hasNoParent() + .hasAttributes(Attributes.empty()), + span -> + span.hasName("Session." + action + " " + Value.class.getName()) + .hasKind(INTERNAL) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + satisfies( + stringKey("hibernate.session_id"), + val -> val.isInstanceOf(String.class))), + span -> + span.hasName("Transaction.commit") + .hasKind(INTERNAL) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo( + stringKey("hibernate.session_id"), + trace + .getSpan(1) + .getAttributes() + .get(stringKey("hibernate.session_id")))), + span -> + span.hasKind(CLIENT) + .hasParent(trace.getSpan(2)) + .hasAttributesSatisfyingExactly( + equalTo(DbIncubatingAttributes.DB_SYSTEM, "h2"), + equalTo(DbIncubatingAttributes.DB_NAME, "db1"), + equalTo(DbIncubatingAttributes.DB_USER, "sa"), + equalTo(DbIncubatingAttributes.DB_CONNECTION_STRING, "h2:mem:"), + satisfies( + DbIncubatingAttributes.DB_STATEMENT, + val -> val.isInstanceOf(String.class)), + satisfies( + DbIncubatingAttributes.DB_OPERATION, + val -> val.isInstanceOf(String.class)), + equalTo(DbIncubatingAttributes.DB_SQL_TABLE, "Value"))); + + } else { + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("parent") + .hasKind(INTERNAL) + .hasNoParent() + .hasAttributes(Attributes.empty()), + span -> + span.hasName("Session." + action + " " + Value.class.getName()) + .hasKind(INTERNAL) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + satisfies( + stringKey("hibernate.session_id"), + val -> val.isInstanceOf(String.class))), + span -> + span.hasKind(CLIENT) + .hasParent(trace.getSpan(1)) + .hasAttributesSatisfyingExactly( + equalTo(DbIncubatingAttributes.DB_SYSTEM, "h2"), + equalTo(DbIncubatingAttributes.DB_NAME, "db1"), + equalTo(DbIncubatingAttributes.DB_USER, "sa"), + equalTo(DbIncubatingAttributes.DB_CONNECTION_STRING, "h2:mem:"), + satisfies( + DbIncubatingAttributes.DB_STATEMENT, + val -> val.isInstanceOf(String.class)), + satisfies( + DbIncubatingAttributes.DB_OPERATION, + val -> val.isInstanceOf(String.class)), + equalTo(DbIncubatingAttributes.DB_SQL_TABLE, "Value")), + span -> + span.hasName("Transaction.commit") + .hasKind(INTERNAL) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo( + stringKey("hibernate.session_id"), + trace + .getSpan(1) + .getAttributes() + .get(stringKey("hibernate.session_id"))))); + } + }); + } + + private static Stream provideArgumentsHibernateActionParameters() { + return Stream.of( + Arguments.of( + named( + "lock", + new Parameter( + "lock", true, false, (em, v) -> em.lock(v, LockModeType.PESSIMISTIC_READ)))), + Arguments.of( + named("refresh", new Parameter("refresh", true, false, EntityManager::refresh))), + Arguments.of( + named( + "find", + new Parameter("find", false, false, (em, v) -> em.find(Value.class, v.getId())))), + Arguments.of( + named( + "merge", + new Parameter( + "merge", + true, + true, + (em, v) -> { + v.setName("New name"); + em.merge(v); + }))), + Arguments.of(named("remove", new Parameter("delete", true, true, EntityManager::remove)))); + } + + @SuppressWarnings("deprecation") // TODO DbIncubatingAttributes.DB_CONNECTION_STRING deprecation + @Test + void testHibernatePersist() { + EntityManager entityManager = entityManagerFactory.createEntityManager(); + EntityTransaction entityTransaction = entityManager.getTransaction(); + entityTransaction.begin(); + + testing.runWithSpan( + "parent", + () -> { + entityManager.persist(new Value("insert me")); + entityTransaction.commit(); + entityManager.close(); + }); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("parent") + .hasKind(INTERNAL) + .hasNoParent() + .hasAttributes(Attributes.empty()), + span -> + span.hasName("Session.persist " + Value.class.getName()) + .hasKind(INTERNAL) + .hasParent(trace.getSpan(0)), + // persist test has an extra query for getting id of inserted element + span -> + span.hasName("SELECT db1.Value") + .hasKind(CLIENT) + .hasParent(trace.getSpan(1)) + .hasAttributesSatisfyingExactly( + equalTo(DbIncubatingAttributes.DB_SYSTEM, "h2"), + equalTo(DbIncubatingAttributes.DB_NAME, "db1"), + equalTo(DbIncubatingAttributes.DB_USER, "sa"), + equalTo(DbIncubatingAttributes.DB_CONNECTION_STRING, "h2:mem:"), + satisfies( + DbIncubatingAttributes.DB_STATEMENT, + val -> val.isInstanceOf(String.class)), + satisfies( + DbIncubatingAttributes.DB_OPERATION, + val -> val.isInstanceOf(String.class)), + equalTo(DbIncubatingAttributes.DB_SQL_TABLE, "Value")), + span -> + span.hasName("Transaction.commit") + .hasKind(INTERNAL) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo( + stringKey("hibernate.session_id"), + trace + .getSpan(1) + .getAttributes() + .get(stringKey("hibernate.session_id")))), + span -> + span.hasKind(CLIENT) + .hasParent(trace.getSpan(3)) + .hasAttributesSatisfyingExactly( + equalTo(DbIncubatingAttributes.DB_SYSTEM, "h2"), + equalTo(DbIncubatingAttributes.DB_NAME, "db1"), + equalTo(DbIncubatingAttributes.DB_USER, "sa"), + equalTo(DbIncubatingAttributes.DB_CONNECTION_STRING, "h2:mem:"), + satisfies( + DbIncubatingAttributes.DB_STATEMENT, + val -> val.isInstanceOf(String.class)), + satisfies( + DbIncubatingAttributes.DB_OPERATION, + val -> val.isInstanceOf(String.class)), + equalTo(DbIncubatingAttributes.DB_SQL_TABLE, "Value")))); + } + + @SuppressWarnings("deprecation") // TODO DbIncubatingAttributes.DB_CONNECTION_STRING deprecation + @ParameterizedTest + @MethodSource("provideArgumentsAttachesState") + void testAttachesStateToQuery(Function queryBuildMethod) { + testing.runWithSpan( + "parent", + () -> { + EntityManager entityManager = entityManagerFactory.createEntityManager(); + EntityTransaction entityTransaction = entityManager.getTransaction(); + entityTransaction.begin(); + queryBuildMethod.apply(entityManager).getResultList(); + entityTransaction.commit(); + entityManager.close(); + }); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("parent") + .hasKind(INTERNAL) + .hasNoParent() + .hasAttributes(Attributes.empty()), + span -> + span.hasName("SELECT Value") + .hasKind(INTERNAL) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + satisfies( + stringKey("hibernate.session_id"), + val -> val.isInstanceOf(String.class))), + span -> + span.hasName("SELECT db1.Value") + .hasKind(CLIENT) + .hasParent(trace.getSpan(1)) + .hasAttributesSatisfyingExactly( + equalTo(DbIncubatingAttributes.DB_SYSTEM, "h2"), + equalTo(DbIncubatingAttributes.DB_NAME, "db1"), + equalTo(DbIncubatingAttributes.DB_USER, "sa"), + equalTo(DbIncubatingAttributes.DB_CONNECTION_STRING, "h2:mem:"), + satisfies( + DbIncubatingAttributes.DB_STATEMENT, + val -> val.isInstanceOf(String.class)), + satisfies( + DbIncubatingAttributes.DB_OPERATION, + val -> val.isInstanceOf(String.class)), + equalTo(DbIncubatingAttributes.DB_SQL_TABLE, "Value")), + span -> + span.hasName("Transaction.commit") + .hasKind(INTERNAL) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo( + stringKey("hibernate.session_id"), + trace + .getSpan(1) + .getAttributes() + .get(stringKey("hibernate.session_id")))))); + } + + private static Stream provideArgumentsAttachesState() { + return Stream.of( + Arguments.of( + named( + "createQuery", + (Function) em -> em.createQuery("from Value"))), + Arguments.of( + named( + "getNamedQuery", + (Function) em -> em.createNamedQuery("TestNamedQuery"))), + Arguments.of( + named( + "createSQLQuery", + (Function) + em -> em.createNativeQuery("SELECT * FROM Value")))); + } + + private static class Parameter { + public final String methodName; + public final boolean attach; + public final boolean flushOnCommit; + public final BiConsumer sessionMethodTest; + + public Parameter( + String methodName, + boolean attach, + boolean flushOnCommit, + BiConsumer sessionMethodTest) { + this.methodName = methodName; + this.attach = attach; + this.flushOnCommit = flushOnCommit; + this.sessionMethodTest = sessionMethodTest; + } + } +} diff --git a/instrumentation/hibernate/hibernate-4.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/hibernate/v4_0/QueryTest.java b/instrumentation/hibernate/hibernate-4.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/hibernate/v4_0/QueryTest.java new file mode 100644 index 000000000000..f68fe97e9ba5 --- /dev/null +++ b/instrumentation/hibernate/hibernate-4.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/hibernate/v4_0/QueryTest.java @@ -0,0 +1,227 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.hibernate.v4_0; + +import static io.opentelemetry.api.common.AttributeKey.stringKey; +import static io.opentelemetry.api.trace.SpanKind.CLIENT; +import static io.opentelemetry.api.trace.SpanKind.INTERNAL; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.satisfies; +import static org.junit.jupiter.api.Named.named; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.semconv.incubating.DbIncubatingAttributes; +import java.util.Iterator; +import java.util.function.Consumer; +import java.util.stream.Stream; +import org.hibernate.Session; +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 QueryTest extends AbstractHibernateTest { + + @SuppressWarnings("deprecation") // TODO DbIncubatingAttributes.DB_CONNECTION_STRING deprecation + @Test + void testHibernateQueryExecuteUpdateWithTransaction() { + testing.runWithSpan( + "parent", + () -> { + Session session = sessionFactory.openSession(); + session.beginTransaction(); + session + .createQuery("update Value set name = :name") + .setParameter("name", "alyx") + .executeUpdate(); + session.getTransaction().commit(); + session.close(); + }); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("parent") + .hasKind(INTERNAL) + .hasNoParent() + .hasAttributes(Attributes.empty()), + span -> + span.hasName("UPDATE Value") + .hasKind(INTERNAL) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + satisfies( + stringKey("hibernate.session_id"), + val -> val.isInstanceOf(String.class))), + span -> + span.hasKind(CLIENT) + .hasParent(trace.getSpan(1)) + .hasAttributesSatisfyingExactly( + equalTo(DbIncubatingAttributes.DB_SYSTEM, "h2"), + equalTo(DbIncubatingAttributes.DB_NAME, "db1"), + equalTo(DbIncubatingAttributes.DB_USER, "sa"), + equalTo(DbIncubatingAttributes.DB_CONNECTION_STRING, "h2:mem:"), + satisfies( + DbIncubatingAttributes.DB_STATEMENT, + val -> val.isInstanceOf(String.class)), + satisfies( + DbIncubatingAttributes.DB_OPERATION, + val -> val.isInstanceOf(String.class)), + equalTo(DbIncubatingAttributes.DB_SQL_TABLE, "Value")), + span -> + span.hasName("Transaction.commit") + .hasKind(INTERNAL) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo( + stringKey("hibernate.session_id"), + trace + .getSpan(1) + .getAttributes() + .get(stringKey("hibernate.session_id")))))); + } + + @SuppressWarnings("deprecation") // TODO DbIncubatingAttributes.DB_CONNECTION_STRING deprecation + @ParameterizedTest + @MethodSource("providesArgumentsSingleCall") + void testHibernateQuerySingleCall(Parameter parameter) { + + testing.runWithSpan( + "parent", + () -> { + Session session = sessionFactory.openSession(); + parameter.queryInteraction.accept(session); + session.close(); + }); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("parent") + .hasKind(INTERNAL) + .hasNoParent() + .hasAttributes(Attributes.empty()), + span -> + span.hasName(parameter.expectedSpanName) + .hasKind(INTERNAL) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + satisfies( + stringKey("hibernate.session_id"), + val -> val.isInstanceOf(String.class))), + span -> + span.hasName("SELECT db1.Value") + .hasKind(CLIENT) + .hasParent(trace.getSpan(1)) + .hasAttributesSatisfyingExactly( + equalTo(DbIncubatingAttributes.DB_SYSTEM, "h2"), + equalTo(DbIncubatingAttributes.DB_NAME, "db1"), + equalTo(DbIncubatingAttributes.DB_USER, "sa"), + equalTo(DbIncubatingAttributes.DB_CONNECTION_STRING, "h2:mem:"), + satisfies( + DbIncubatingAttributes.DB_STATEMENT, + val -> val.startsWith("select ")), + equalTo(DbIncubatingAttributes.DB_OPERATION, "SELECT"), + equalTo(DbIncubatingAttributes.DB_SQL_TABLE, "Value")))); + } + + private static Stream providesArgumentsSingleCall() { + return Stream.of( + Arguments.of( + named( + "query/list", + new Parameter("SELECT Value", sess -> sess.createQuery("from Value").list()))), + Arguments.of( + named( + "query/uniqueResult", + new Parameter( + "SELECT Value", + sess -> + sess.createQuery("from Value where id = :id") + .setParameter("id", 1L) + .uniqueResult()))), + Arguments.of( + named( + "iterate", + new Parameter("SELECT Value", sess -> sess.createQuery("from Value").iterate()))), + Arguments.of( + named( + "query/scroll", + new Parameter("SELECT Value", sess -> sess.createQuery("from Value").scroll())))); + } + + @SuppressWarnings("deprecation") // TODO DbIncubatingAttributes.DB_CONNECTION_STRING deprecation + @Test + void testHibernateQueryIterate() { + testing.runWithSpan( + "parent", + () -> { + Session session = sessionFactory.openSession(); + session.beginTransaction(); + @SuppressWarnings("unchecked") + Iterator iterator = session.createQuery("from Value").iterate(); + while (iterator.hasNext()) { + iterator.next(); + } + session.getTransaction().commit(); + session.close(); + }); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("parent") + .hasKind(INTERNAL) + .hasNoParent() + .hasAttributes(Attributes.empty()), + span -> + span.hasName("SELECT Value") + .hasKind(INTERNAL) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + satisfies( + stringKey("hibernate.session_id"), + val -> val.isInstanceOf(String.class))), + span -> + span.hasName("SELECT db1.Value") + .hasKind(CLIENT) + .hasParent(trace.getSpan(1)) + .hasAttributesSatisfyingExactly( + equalTo(DbIncubatingAttributes.DB_SYSTEM, "h2"), + equalTo(DbIncubatingAttributes.DB_NAME, "db1"), + equalTo(DbIncubatingAttributes.DB_USER, "sa"), + equalTo(DbIncubatingAttributes.DB_CONNECTION_STRING, "h2:mem:"), + satisfies( + DbIncubatingAttributes.DB_STATEMENT, + val -> val.startsWith("select ")), + equalTo(DbIncubatingAttributes.DB_OPERATION, "SELECT"), + equalTo(DbIncubatingAttributes.DB_SQL_TABLE, "Value")), + span -> + span.hasName("Transaction.commit") + .hasKind(INTERNAL) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo( + stringKey("hibernate.session_id"), + trace + .getSpan(1) + .getAttributes() + .get(stringKey("hibernate.session_id")))))); + } + + private static class Parameter { + public final String expectedSpanName; + public final Consumer queryInteraction; + + public Parameter(String expectedSpanName, Consumer queryInteraction) { + this.expectedSpanName = expectedSpanName; + this.queryInteraction = queryInteraction; + } + } +} diff --git a/instrumentation/hibernate/hibernate-4.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/hibernate/v4_0/SessionTest.java b/instrumentation/hibernate/hibernate-4.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/hibernate/v4_0/SessionTest.java new file mode 100644 index 000000000000..b40068f4827f --- /dev/null +++ b/instrumentation/hibernate/hibernate-4.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/hibernate/v4_0/SessionTest.java @@ -0,0 +1,852 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.hibernate.v4_0; + +import static io.opentelemetry.api.common.AttributeKey.stringKey; +import static io.opentelemetry.api.trace.SpanKind.CLIENT; +import static io.opentelemetry.api.trace.SpanKind.INTERNAL; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.satisfies; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.ThrowableAssert.catchThrowable; +import static org.junit.jupiter.api.Named.named; + +import io.opentelemetry.sdk.trace.data.StatusData; +import io.opentelemetry.semconv.incubating.DbIncubatingAttributes; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.stream.Stream; +import org.hibernate.LockMode; +import org.hibernate.LockOptions; +import org.hibernate.MappingException; +import org.hibernate.ReplicationMode; +import org.hibernate.Session; +import org.hibernate.StatelessSession; +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; + +@SuppressWarnings("deprecation") // 'lock' is a deprecated method in the Session class +class SessionTest extends AbstractHibernateTest { + + @SuppressWarnings("deprecation") // TODO DbIncubatingAttributes.DB_CONNECTION_STRING deprecation + @ParameterizedTest + @MethodSource("provideArgumentsHibernateAction") + void testHibernateAction(Parameter parameter) { + testing.runWithSpan( + "parent", + () -> { + Session session = sessionFactory.openSession(); + session.beginTransaction(); + parameter.sessionMethodTest.accept(session, prepopulated.get(0)); + session.getTransaction().commit(); + session.close(); + }); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasKind(INTERNAL).hasNoParent(), + span -> + span.hasName("Session." + parameter.methodName + " " + Value.class.getName()) + .hasKind(INTERNAL) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + satisfies( + stringKey("hibernate.session_id"), + val -> val.isInstanceOf(String.class))), + span -> + span.hasKind(CLIENT) + .hasParent(trace.getSpan(1)) + .hasAttributesSatisfyingExactly( + equalTo(DbIncubatingAttributes.DB_SYSTEM, "h2"), + equalTo(DbIncubatingAttributes.DB_NAME, "db1"), + equalTo(DbIncubatingAttributes.DB_USER, "sa"), + equalTo(DbIncubatingAttributes.DB_CONNECTION_STRING, "h2:mem:"), + satisfies( + DbIncubatingAttributes.DB_STATEMENT, + val -> val.isInstanceOf(String.class)), + satisfies( + DbIncubatingAttributes.DB_OPERATION, + val -> val.isInstanceOf(String.class)), + equalTo(DbIncubatingAttributes.DB_SQL_TABLE, "Value")), + span -> + span.hasName("Transaction.commit") + .hasKind(INTERNAL) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo( + stringKey("hibernate.session_id"), + trace + .getSpan(1) + .getAttributes() + .get(stringKey("hibernate.session_id")))))); + } + + private static Stream provideArgumentsHibernateAction() { + return Stream.of( + Arguments.of( + named( + "lock", + new Parameter( + "lock", + (Session session, Value val) -> session.lock(val, LockMode.READ), + null))), + Arguments.of( + named( + "lock with entity name", + new Parameter( + "lock", + (Session session, Value val) -> + session.lock(Value.class.getName(), val, LockMode.READ), + null))), + Arguments.of( + named( + "lock with null name", + new Parameter( + "lock", + (Session session, Value val) -> session.lock(null, val, LockMode.READ), + null))), + Arguments.of( + named( + "buildLockRequest", + new Parameter( + "lock", + (Session session, Value val) -> + session.buildLockRequest(LockOptions.READ).lock(val), + null))), + Arguments.of(named("refresh", new Parameter("refresh", Session::refresh, null))), + Arguments.of( + named( + "refresh with entity name", + new Parameter( + "refresh", + (Session session, Value val) -> session.refresh(Value.class.getName(), val), + null))), + Arguments.of( + named( + "get with entity name", + new Parameter( + "get", + (Session session, Value val) -> session.get(Value.class.getName(), val.getId()), + null))), + Arguments.of( + named( + "get with entity class", + new Parameter( + "get", + (Session session, Value val) -> session.get(Value.class, val.getId()), + null)))); + } + + @SuppressWarnings("deprecation") // TODO DbIncubatingAttributes.DB_CONNECTION_STRING deprecation + @ParameterizedTest + @MethodSource("provideArgumentsHibernateActionStateless") + void testHibernateActionStateless(Parameter parameter) { + + testing.runWithSpan( + "parent", + () -> { + StatelessSession session = sessionFactory.openStatelessSession(); + session.beginTransaction(); + parameter.statelessSessionMethodTest.accept(session, prepopulated.get(0)); + session.getTransaction().commit(); + session.close(); + }); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasKind(INTERNAL).hasNoParent(), + span -> + span.hasName("Session." + parameter.methodName + " " + Value.class.getName()) + .hasKind(INTERNAL) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + satisfies( + stringKey("hibernate.session_id"), + val -> val.isInstanceOf(String.class))), + span -> + span.hasKind(CLIENT) + .hasParent(trace.getSpan(1)) + .hasAttributesSatisfyingExactly( + equalTo(DbIncubatingAttributes.DB_SYSTEM, "h2"), + equalTo(DbIncubatingAttributes.DB_NAME, "db1"), + equalTo(DbIncubatingAttributes.DB_USER, "sa"), + equalTo(DbIncubatingAttributes.DB_CONNECTION_STRING, "h2:mem:"), + satisfies( + DbIncubatingAttributes.DB_STATEMENT, + val -> val.isInstanceOf(String.class)), + satisfies( + DbIncubatingAttributes.DB_OPERATION, + val -> val.isInstanceOf(String.class)), + equalTo(DbIncubatingAttributes.DB_SQL_TABLE, "Value")), + span -> + span.hasName("Transaction.commit") + .hasKind(INTERNAL) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo( + stringKey("hibernate.session_id"), + trace + .getSpan(1) + .getAttributes() + .get(stringKey("hibernate.session_id")))))); + } + + private static Stream provideArgumentsHibernateActionStateless() { + return Stream.of( + Arguments.of(named("refresh", new Parameter("refresh", null, StatelessSession::refresh))), + Arguments.of( + named( + "refresh with entity name", + new Parameter( + "refresh", + null, + (StatelessSession session, Value val) -> + session.refresh(Value.class.getName(), val)))), + Arguments.of( + named( + "get with entity name", + new Parameter( + "get", + null, + (StatelessSession session, Value val) -> + session.get(Value.class.getName(), val.getId())))), + Arguments.of( + named( + "get with entity class", + new Parameter( + "get", + null, + (StatelessSession session, Value val) -> + session.get(Value.class, val.getId())))), + Arguments.of( + named( + "insert", + new Parameter( + "insert", + null, + (StatelessSession session, Value val) -> + session.insert(new Value("insert me"))))), + Arguments.of( + named( + "insert with entity name", + new Parameter( + "insert", + null, + (StatelessSession session, Value val) -> + session.insert(Value.class.getName(), new Value("insert me"))))), + Arguments.of( + named( + "insert with null entity name", + new Parameter( + "insert", + null, + (StatelessSession session, Value val) -> + session.insert(null, new Value("insert me"))))), + Arguments.of( + named( + "update (StatelessSession)", + new Parameter( + "update", + null, + (StatelessSession session, Value val) -> { + val.setName("New name"); + session.update(val); + }))), + Arguments.of( + named( + "update with entity name (StatelessSession)", + new Parameter( + "update", + null, + (StatelessSession session, Value val) -> { + val.setName("New name"); + session.update(Value.class.getName(), val); + }))), + Arguments.of( + named( + "delete (Session)", + new Parameter( + "delete", + null, + (StatelessSession session, Value val) -> { + session.delete(val); + prepopulated.remove(val); + }))), + Arguments.of( + named( + "delete with entity name (Session)", + new Parameter( + "delete", + null, + (StatelessSession session, Value val) -> { + session.delete(Value.class.getName(), val); + prepopulated.remove(val); + })))); + } + + @SuppressWarnings("deprecation") // TODO DbIncubatingAttributes.DB_CONNECTION_STRING deprecation + @ParameterizedTest + @MethodSource("provideArgumentsHibernateReplicate") + void testHibernateReplicate(Parameter parameter) { + testing.runWithSpan( + "parent", + () -> { + Session session = sessionFactory.openSession(); + session.beginTransaction(); + parameter.sessionMethodTest.accept(session, prepopulated.get(0)); + session.getTransaction().commit(); + session.close(); + }); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasKind(INTERNAL).hasNoParent(), + span -> + span.hasName("Session." + parameter.methodName + " " + Value.class.getName()) + .hasKind(INTERNAL) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + satisfies( + stringKey("hibernate.session_id"), + val -> val.isInstanceOf(String.class))), + span -> + span.hasKind(CLIENT) + .hasParent(trace.getSpan(1)) + .hasAttributesSatisfyingExactly( + equalTo(DbIncubatingAttributes.DB_SYSTEM, "h2"), + equalTo(DbIncubatingAttributes.DB_NAME, "db1"), + equalTo(DbIncubatingAttributes.DB_USER, "sa"), + equalTo(DbIncubatingAttributes.DB_CONNECTION_STRING, "h2:mem:"), + satisfies( + DbIncubatingAttributes.DB_STATEMENT, + val -> val.isInstanceOf(String.class)), + satisfies( + DbIncubatingAttributes.DB_OPERATION, + val -> val.isInstanceOf(String.class)), + equalTo(DbIncubatingAttributes.DB_SQL_TABLE, "Value")), + span -> + span.hasName("Transaction.commit") + .hasKind(INTERNAL) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo( + stringKey("hibernate.session_id"), + trace + .getSpan(1) + .getAttributes() + .get(stringKey("hibernate.session_id")))), + span -> + span.hasKind(CLIENT) + .hasParent(trace.getSpan(3)) + .hasAttributesSatisfyingExactly( + equalTo(DbIncubatingAttributes.DB_SYSTEM, "h2"), + equalTo(DbIncubatingAttributes.DB_NAME, "db1"), + equalTo(DbIncubatingAttributes.DB_USER, "sa"), + equalTo(DbIncubatingAttributes.DB_CONNECTION_STRING, "h2:mem:"), + satisfies( + DbIncubatingAttributes.DB_STATEMENT, + val -> val.isInstanceOf(String.class)), + satisfies( + DbIncubatingAttributes.DB_OPERATION, + val -> val.isInstanceOf(String.class)), + equalTo(DbIncubatingAttributes.DB_SQL_TABLE, "Value")))); + } + + private static Stream provideArgumentsHibernateReplicate() { + return Stream.of( + Arguments.of( + named( + "replicate", + new Parameter( + "replicate", + (Session session, Value val) -> { + Value replicated = new Value(val.getName() + " replicated"); + replicated.setId(val.getId()); + session.replicate(replicated, ReplicationMode.OVERWRITE); + }, + null))), + Arguments.of( + named( + "replicate by entity name", + new Parameter( + "replicate", + (Session session, Value val) -> { + Value replicated = new Value(val.getName() + " replicated"); + replicated.setId(val.getId()); + session.replicate( + Value.class.getName(), replicated, ReplicationMode.OVERWRITE); + }, + null)))); + } + + @Test + void testHibernateFailedReplicate() { + Throwable mappingException = + testing.runWithSpan( + "parent", + () -> { + Session session = sessionFactory.openSession(); + session.beginTransaction(); + + Throwable exception = + catchThrowable( + () -> { + session.replicate(123L /* Not a valid entity */, ReplicationMode.OVERWRITE); + }); + + session.getTransaction().commit(); + session.close(); + return exception; + }); + + assertThat(mappingException.getClass()).isEqualTo(MappingException.class); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasKind(INTERNAL).hasNoParent(), + span -> + span.hasName("Session.replicate java.lang.Long") + .hasKind(INTERNAL) + .hasParent(trace.getSpan(0)) + .hasStatus(StatusData.error()) + .hasAttributesSatisfyingExactly( + equalTo( + stringKey("hibernate.session_id"), + trace + .getSpan(1) + .getAttributes() + .get(stringKey("hibernate.session_id")))) + .hasException(mappingException), + span -> + span.hasName("Transaction.commit") + .hasKind(INTERNAL) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo( + stringKey("hibernate.session_id"), + trace + .getSpan(1) + .getAttributes() + .get(stringKey("hibernate.session_id")))))); + } + + @SuppressWarnings("deprecation") // TODO DbIncubatingAttributes.DB_CONNECTION_STRING deprecation + @ParameterizedTest + @MethodSource("provideArgumentsHibernateCommitAction") + void testHibernateCommitAction(Parameter parameter) { + + testing.runWithSpan( + "parent", + () -> { + Session session = sessionFactory.openSession(); + session.beginTransaction(); + parameter.sessionMethodTest.accept(session, prepopulated.get(0)); + session.getTransaction().commit(); + session.close(); + }); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasKind(INTERNAL).hasNoParent(), + span -> + span.hasName("Session." + parameter.methodName + " " + Value.class.getName()) + .hasKind(INTERNAL) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + satisfies( + stringKey("hibernate.session_id"), + val -> val.isInstanceOf(String.class))), + span -> + span.hasName("Transaction.commit") + .hasKind(INTERNAL) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo( + stringKey("hibernate.session_id"), + trace + .getSpan(1) + .getAttributes() + .get(stringKey("hibernate.session_id")))), + span -> + span.hasKind(CLIENT) + .hasParent(trace.getSpan(2)) + .hasAttributesSatisfyingExactly( + equalTo(DbIncubatingAttributes.DB_SYSTEM, "h2"), + equalTo(DbIncubatingAttributes.DB_NAME, "db1"), + equalTo(DbIncubatingAttributes.DB_USER, "sa"), + equalTo(DbIncubatingAttributes.DB_CONNECTION_STRING, "h2:mem:"), + satisfies( + DbIncubatingAttributes.DB_STATEMENT, + val -> val.isInstanceOf(String.class)), + satisfies( + DbIncubatingAttributes.DB_OPERATION, + val -> val.isInstanceOf(String.class)), + equalTo(DbIncubatingAttributes.DB_SQL_TABLE, "Value")))); + } + + private static Stream provideArgumentsHibernateCommitAction() { + return Stream.of( + Arguments.of( + named( + "save", + new Parameter( + "save", + (Session session, Value val) -> { + session.save(new Value("Another value")); + }, + null))), + Arguments.of( + named( + "save with entity name", + new Parameter( + "save", + (Session session, Value val) -> { + session.save(Value.class.getName(), new Value("Another value")); + }, + null))), + Arguments.of( + named( + "saveOrUpdate save", + new Parameter( + "saveOrUpdate", + (Session session, Value val) -> { + session.saveOrUpdate(new Value("Value")); + }, + null))), + Arguments.of( + named( + "saveOrUpdate save with entity name", + new Parameter( + "saveOrUpdate", + (Session session, Value val) -> { + session.saveOrUpdate(Value.class.getName(), new Value("Value")); + }, + null))), + Arguments.of( + named( + "saveOrUpdate update with entity name", + new Parameter( + "saveOrUpdate", + (Session session, Value val) -> { + val.setName("New name"); + session.saveOrUpdate(Value.class.getName(), val); + }, + null))), + Arguments.of( + named( + "merge", + new Parameter( + "merge", + (Session session, Value val) -> { + session.merge(new Value("merge me in")); + }, + null))), + Arguments.of( + named( + "merge with entity name", + new Parameter( + "merge", + (Session session, Value val) -> { + session.merge(Value.class.getName(), new Value("merge me in")); + }, + null))), + Arguments.of( + named( + "persist", + new Parameter( + "persist", + (Session session, Value val) -> { + session.persist(new Value("merge me in")); + }, + null))), + Arguments.of( + named( + "persist with entity name", + new Parameter( + "persist", + (Session session, Value val) -> { + session.persist(Value.class.getName(), new Value("merge me in")); + }, + null))), + Arguments.of( + named( + "persist with null entity name", + new Parameter( + "persist", + (Session session, Value val) -> { + session.persist(null, new Value("merge me in")); + }, + null))), + Arguments.of( + named( + "update (Session)", + new Parameter( + "update", + (Session session, Value val) -> { + val.setName("New name"); + session.update(val); + }, + null))), + Arguments.of( + named( + "update by entityName (Session)", + new Parameter( + "update", + (Session session, Value val) -> { + val.setName("New name"); + session.update(Value.class.getName(), val); + }, + null))), + Arguments.of( + named( + "delete (Session)", + new Parameter( + "delete", + (Session session, Value val) -> { + session.delete(val); + prepopulated.remove(val); + }, + null))), + Arguments.of( + named( + "delete by entityName (Session)", + new Parameter( + "delete", + (Session session, Value val) -> { + session.delete(Value.class.getName(), val); + prepopulated.remove(val); + }, + null)))); + } + + @SuppressWarnings("deprecation") // TODO DbIncubatingAttributes.DB_CONNECTION_STRING deprecation + @ParameterizedTest + @MethodSource("provideArgumentsStateQuery") + void testAttachesStateToQueryCreated(Consumer queryBuilder) { + + testing.runWithSpan( + "parent", + () -> { + Session session = sessionFactory.openSession(); + session.beginTransaction(); + queryBuilder.accept(session); + session.getTransaction().commit(); + session.close(); + }); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasKind(INTERNAL).hasNoParent(), + span -> + span.hasName("SELECT Value") + .hasKind(INTERNAL) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + satisfies( + stringKey("hibernate.session_id"), + val -> val.isInstanceOf(String.class))), + span -> + span.hasKind(CLIENT) + .hasParent(trace.getSpan(1)) + .hasAttributesSatisfyingExactly( + equalTo(DbIncubatingAttributes.DB_SYSTEM, "h2"), + equalTo(DbIncubatingAttributes.DB_NAME, "db1"), + equalTo(DbIncubatingAttributes.DB_USER, "sa"), + equalTo(DbIncubatingAttributes.DB_CONNECTION_STRING, "h2:mem:"), + satisfies( + DbIncubatingAttributes.DB_STATEMENT, + val -> val.isInstanceOf(String.class)), + satisfies( + DbIncubatingAttributes.DB_OPERATION, + val -> val.isInstanceOf(String.class)), + equalTo(DbIncubatingAttributes.DB_SQL_TABLE, "Value")), + span -> + span.hasName("Transaction.commit") + .hasKind(INTERNAL) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo( + stringKey("hibernate.session_id"), + trace + .getSpan(1) + .getAttributes() + .get(stringKey("hibernate.session_id")))))); + } + + private static Stream provideArgumentsStateQuery() { + return Stream.of( + Arguments.of( + named( + "createQuery", + ((Consumer) session -> session.createQuery("from Value").list())), + Arguments.of( + named( + "getNamedQuery", + ((Consumer) + session -> session.getNamedQuery("TestNamedQuery").list())), + Arguments.of( + named( + "createSQLQuery", + (Consumer) + session -> session.createSQLQuery("SELECT * FROM Value").list()))))); + } + + @SuppressWarnings("deprecation") // TODO DbIncubatingAttributes.DB_CONNECTION_STRING deprecation + @Test + void testHibernateOverlappingSessions() { + testing.runWithSpan( + "overlapping Sessions", + () -> { + Session session1 = sessionFactory.openSession(); + session1.beginTransaction(); + + StatelessSession session2 = sessionFactory.openStatelessSession(); + Session session3 = sessionFactory.openSession(); + + Value 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(); + }); + + AtomicReference sessionId1 = new AtomicReference<>(); + AtomicReference sessionId2 = new AtomicReference<>(); + AtomicReference sessionId3 = new AtomicReference<>(); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("overlapping Sessions"), + span -> { + span.hasName("Session.save " + Value.class.getName()) + .hasKind(INTERNAL) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + satisfies( + stringKey("hibernate.session_id"), + val -> val.isInstanceOf(String.class))); + sessionId1.set( + trace.getSpan(1).getAttributes().get(stringKey("hibernate.session_id"))); + }, + span -> { + span.hasName("Session.insert " + Value.class.getName()) + .hasKind(INTERNAL) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + satisfies( + stringKey("hibernate.session_id"), + val -> val.isInstanceOf(String.class))); + sessionId2.set( + trace.getSpan(2).getAttributes().get(stringKey("hibernate.session_id"))); + }, + span -> + span.hasName("INSERT db1.Value") + .hasKind(CLIENT) + .hasParent(trace.getSpan(2)) + .hasAttributesSatisfyingExactly( + equalTo(DbIncubatingAttributes.DB_SYSTEM, "h2"), + equalTo(DbIncubatingAttributes.DB_NAME, "db1"), + equalTo(DbIncubatingAttributes.DB_USER, "sa"), + equalTo(DbIncubatingAttributes.DB_CONNECTION_STRING, "h2:mem:"), + satisfies( + DbIncubatingAttributes.DB_STATEMENT, + stringAssert -> stringAssert.startsWith("insert")), + equalTo(DbIncubatingAttributes.DB_OPERATION, "INSERT"), + equalTo(DbIncubatingAttributes.DB_SQL_TABLE, "Value")), + span -> { + span.hasName("Session.save " + Value.class.getName()) + .hasKind(INTERNAL) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + satisfies( + stringKey("hibernate.session_id"), + val -> val.isInstanceOf(String.class))); + sessionId3.set( + trace.getSpan(4).getAttributes().get(stringKey("hibernate.session_id"))); + }, + span -> + span.hasName("Session.delete " + Value.class.getName()) + .hasKind(INTERNAL) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + satisfies( + stringKey("hibernate.session_id"), + val -> val.isInstanceOf(String.class))), + span -> + span.hasName("Transaction.commit") + .hasKind(INTERNAL) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo( + stringKey("hibernate.session_id"), + trace + .getSpan(1) + .getAttributes() + .get(stringKey("hibernate.session_id")))), + span -> + span.hasName("INSERT db1.Value") + .hasKind(CLIENT) + .hasParent(trace.getSpan(6)) + .hasAttributesSatisfyingExactly( + equalTo(DbIncubatingAttributes.DB_SYSTEM, "h2"), + equalTo(DbIncubatingAttributes.DB_NAME, "db1"), + equalTo(DbIncubatingAttributes.DB_USER, "sa"), + equalTo(DbIncubatingAttributes.DB_CONNECTION_STRING, "h2:mem:"), + satisfies( + DbIncubatingAttributes.DB_STATEMENT, + stringAssert -> stringAssert.startsWith("insert")), + equalTo(DbIncubatingAttributes.DB_OPERATION, "INSERT"), + equalTo(DbIncubatingAttributes.DB_SQL_TABLE, "Value")), + span -> + span.hasName("DELETE db1.Value") + .hasKind(CLIENT) + .hasParent(trace.getSpan(6)) + .hasAttributesSatisfyingExactly( + equalTo(DbIncubatingAttributes.DB_SYSTEM, "h2"), + equalTo(DbIncubatingAttributes.DB_NAME, "db1"), + equalTo(DbIncubatingAttributes.DB_USER, "sa"), + equalTo(DbIncubatingAttributes.DB_CONNECTION_STRING, "h2:mem:"), + satisfies( + DbIncubatingAttributes.DB_STATEMENT, + stringAssert -> stringAssert.startsWith("delete")), + equalTo(DbIncubatingAttributes.DB_OPERATION, "DELETE"), + equalTo(DbIncubatingAttributes.DB_SQL_TABLE, "Value")))); + + assertThat(sessionId1.get()).isNotEqualTo(sessionId2.get()); + assertThat(sessionId1.get()).isNotEqualTo(sessionId3.get()); + assertThat(sessionId2.get()).isNotEqualTo(sessionId3.get()); + } + + private static class Parameter { + public final String methodName; + public final BiConsumer sessionMethodTest; + public final BiConsumer statelessSessionMethodTest; + + public Parameter( + String methodName, + BiConsumer sessionMethodTest, + BiConsumer statelessSessionMethodTest) { + this.methodName = methodName; + this.sessionMethodTest = sessionMethodTest; + this.statelessSessionMethodTest = statelessSessionMethodTest; + } + } +} diff --git a/instrumentation/hibernate/hibernate-4.0/javaagent/src/test/java/Value.java b/instrumentation/hibernate/hibernate-4.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/hibernate/v4_0/Value.java similarity index 92% rename from instrumentation/hibernate/hibernate-4.0/javaagent/src/test/java/Value.java rename to instrumentation/hibernate/hibernate-4.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/hibernate/v4_0/Value.java index a9de87173ddd..12a94b8a688c 100644 --- a/instrumentation/hibernate/hibernate-4.0/javaagent/src/test/java/Value.java +++ b/instrumentation/hibernate/hibernate-4.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/hibernate/v4_0/Value.java @@ -3,6 +3,8 @@ * SPDX-License-Identifier: Apache-2.0 */ +package io.opentelemetry.javaagent.instrumentation.hibernate.v4_0; + import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.Id; diff --git a/instrumentation/hibernate/hibernate-4.0/javaagent/src/test/java/spring/jpa/Customer.java b/instrumentation/hibernate/hibernate-4.0/javaagent/src/test/java/spring/jpa/Customer.java index cac701b9e950..27d780b22951 100644 --- a/instrumentation/hibernate/hibernate-4.0/javaagent/src/test/java/spring/jpa/Customer.java +++ b/instrumentation/hibernate/hibernate-4.0/javaagent/src/test/java/spring/jpa/Customer.java @@ -5,6 +5,7 @@ package spring.jpa; +import java.util.Locale; import java.util.Objects; import javax.annotation.Nullable; import javax.persistence.Entity; @@ -55,7 +56,8 @@ public void setLastName(String lastName) { @Override public String toString() { - return String.format("Customer[id=%d, firstName='%s', lastName='%s']", id, firstName, lastName); + return String.format( + Locale.ROOT, "Customer[id=%d, firstName='%s', lastName='%s']", id, firstName, lastName); } @Override diff --git a/instrumentation/hibernate/hibernate-4.0/javaagent/src/test/java/spring/jpa/SpringJpaTest.java b/instrumentation/hibernate/hibernate-4.0/javaagent/src/test/java/spring/jpa/SpringJpaTest.java new file mode 100644 index 000000000000..17ebc73ba798 --- /dev/null +++ b/instrumentation/hibernate/hibernate-4.0/javaagent/src/test/java/spring/jpa/SpringJpaTest.java @@ -0,0 +1,459 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package spring.jpa; + +import static io.opentelemetry.api.common.AttributeKey.stringKey; +import static io.opentelemetry.api.trace.SpanKind.CLIENT; +import static io.opentelemetry.api.trace.SpanKind.INTERNAL; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.satisfies; +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.semconv.incubating.DbIncubatingAttributes; +import java.util.regex.Pattern; +import org.hibernate.Version; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; + +class SpringJpaTest { + + @RegisterExtension + protected static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + AnnotationConfigApplicationContext context = + new AnnotationConfigApplicationContext(PersistenceConfig.class); + CustomerRepository repo = context.getBean(CustomerRepository.class); + + @SuppressWarnings("deprecation") // TODO DbIncubatingAttributes.DB_CONNECTION_STRING deprecation + @Test + void testCrud() { + String version = Version.getVersionString(); + boolean isHibernate4 = version.startsWith("4."); + boolean isLatestDep = version.startsWith("5.0"); + + Customer customer = new Customer("Bob", "Anonymous"); + customer.setId(null); + + boolean result = testing.runWithSpan("parent", () -> repo.findAll().iterator().hasNext()); + + assertThat(result).isEqualTo(false); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("parent") + .hasKind(INTERNAL) + .hasNoParent() + .hasAttributes(Attributes.empty()), + span -> + span.hasName("SELECT Customer") + .hasKind(INTERNAL) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + satisfies( + stringKey("hibernate.session_id"), + val -> val.isInstanceOf(String.class))), + span -> + span.hasName("SELECT test.Customer") + .hasKind(CLIENT) + .hasParent(trace.getSpan(1)) + .hasAttributesSatisfyingExactly( + equalTo(DbIncubatingAttributes.DB_SYSTEM, "hsqldb"), + equalTo(DbIncubatingAttributes.DB_NAME, "test"), + equalTo(DbIncubatingAttributes.DB_USER, "sa"), + equalTo(DbIncubatingAttributes.DB_CONNECTION_STRING, "hsqldb:mem:"), + satisfies( + DbIncubatingAttributes.DB_STATEMENT, + val -> + val.matches( + Pattern.compile( + "select ([^.]+).id([^,]*), ([^.]+).firstName([^,]*), ([^.]+).lastName(.*)from Customer(.*)"))), + equalTo(DbIncubatingAttributes.DB_OPERATION, "SELECT"), + equalTo(DbIncubatingAttributes.DB_SQL_TABLE, "Customer")), + span -> + span.hasName("Transaction.commit") + .hasKind(INTERNAL) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo( + stringKey("hibernate.session_id"), + trace + .getSpan(1) + .getAttributes() + .get(stringKey("hibernate.session_id")))))); + testing.clearData(); + + testing.runWithSpan( + "parent", + () -> { + repo.save(customer); + }); + + assertThat(customer.getId()).isNotNull(); + + testing.waitAndAssertTraces( + trace -> { + if (isHibernate4) { + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("parent") + .hasKind(INTERNAL) + .hasNoParent() + .hasAttributes(Attributes.empty()), + span -> + span.hasName("Session.persist spring.jpa.Customer") + .hasKind(INTERNAL) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + satisfies( + stringKey("hibernate.session_id"), + val -> val.isInstanceOf(String.class))), + span -> + span.hasName("INSERT test.Customer") + .hasKind(CLIENT) + .hasParent(trace.getSpan(1)) + .hasAttributesSatisfyingExactly( + equalTo(DbIncubatingAttributes.DB_SYSTEM, "hsqldb"), + equalTo(DbIncubatingAttributes.DB_NAME, "test"), + equalTo(DbIncubatingAttributes.DB_USER, "sa"), + equalTo(DbIncubatingAttributes.DB_CONNECTION_STRING, "hsqldb:mem:"), + satisfies( + DbIncubatingAttributes.DB_STATEMENT, + val -> + val.matches( + Pattern.compile( + "insert into Customer (.*) values \\(.*, \\?, \\?\\)"))), + equalTo(DbIncubatingAttributes.DB_OPERATION, "INSERT"), + equalTo(DbIncubatingAttributes.DB_SQL_TABLE, "Customer")), + span -> + span.hasName("Transaction.commit") + .hasKind(INTERNAL) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo( + stringKey("hibernate.session_id"), + trace + .getSpan(1) + .getAttributes() + .get(stringKey("hibernate.session_id"))))); + } else { + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("parent") + .hasKind(INTERNAL) + .hasNoParent() + .hasAttributes(Attributes.empty()), + span -> + span.hasName("Session.persist spring.jpa.Customer") + .hasKind(INTERNAL) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + satisfies( + stringKey("hibernate.session_id"), + val -> val.isInstanceOf(String.class))), + span -> + span.hasName("CALL test") + .hasKind(CLIENT) + .hasParent(trace.getSpan(1)) + .hasAttributesSatisfyingExactly( + equalTo(DbIncubatingAttributes.DB_SYSTEM, "hsqldb"), + equalTo(DbIncubatingAttributes.DB_NAME, "test"), + equalTo(DbIncubatingAttributes.DB_USER, "sa"), + equalTo( + DbIncubatingAttributes.DB_STATEMENT, + "call next value for hibernate_sequence"), + equalTo(DbIncubatingAttributes.DB_CONNECTION_STRING, "hsqldb:mem:"), + equalTo(DbIncubatingAttributes.DB_OPERATION, "CALL")), + span -> + span.hasName("Transaction.commit") + .hasKind(INTERNAL) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo( + stringKey("hibernate.session_id"), + trace + .getSpan(1) + .getAttributes() + .get(stringKey("hibernate.session_id")))), + span -> + span.hasName("INSERT test.Customer") + .hasKind(CLIENT) + .hasParent(trace.getSpan(3)) + .hasAttributesSatisfyingExactly( + equalTo(DbIncubatingAttributes.DB_SYSTEM, "hsqldb"), + equalTo(DbIncubatingAttributes.DB_NAME, "test"), + equalTo(DbIncubatingAttributes.DB_USER, "sa"), + equalTo(DbIncubatingAttributes.DB_CONNECTION_STRING, "hsqldb:mem:"), + satisfies( + DbIncubatingAttributes.DB_STATEMENT, + val -> + val.matches( + Pattern.compile( + "insert into Customer (.*) values \\(.* \\?, \\?\\)"))), + equalTo(DbIncubatingAttributes.DB_OPERATION, "INSERT"), + equalTo(DbIncubatingAttributes.DB_SQL_TABLE, "Customer"))); + } + }); + testing.clearData(); + + customer.setFirstName("Bill"); + + testing.runWithSpan( + "parent", + () -> { + repo.save(customer); + }); + + Long savedId = customer.getId(); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("parent") + .hasKind(INTERNAL) + .hasNoParent() + .hasAttributes(Attributes.empty()), + span -> + span.hasName("Session.merge spring.jpa.Customer") + .hasKind(INTERNAL) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + satisfies( + stringKey("hibernate.session_id"), + val -> val.isInstanceOf(String.class))), + span -> + span.hasName("SELECT test.Customer") + .hasKind(CLIENT) + .hasParent(trace.getSpan(1)) + .hasAttributesSatisfyingExactly( + equalTo(DbIncubatingAttributes.DB_SYSTEM, "hsqldb"), + equalTo(DbIncubatingAttributes.DB_NAME, "test"), + equalTo(DbIncubatingAttributes.DB_USER, "sa"), + equalTo(DbIncubatingAttributes.DB_CONNECTION_STRING, "hsqldb:mem:"), + satisfies( + DbIncubatingAttributes.DB_STATEMENT, + val -> + val.matches( + Pattern.compile( + "select ([^.]+).id([^,]*), ([^.]+).firstName([^,]*), ([^.]+).lastName (.*)from Customer (.*)where ([^.]+).id=\\?"))), + equalTo(DbIncubatingAttributes.DB_OPERATION, "SELECT"), + equalTo(DbIncubatingAttributes.DB_SQL_TABLE, "Customer")), + span -> + span.hasName("Transaction.commit") + .hasKind(INTERNAL) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo( + stringKey("hibernate.session_id"), + trace + .getSpan(1) + .getAttributes() + .get(stringKey("hibernate.session_id")))), + span -> + span.hasName("UPDATE test.Customer") + .hasKind(CLIENT) + .hasParent(trace.getSpan(3)) + .hasAttributesSatisfyingExactly( + equalTo(DbIncubatingAttributes.DB_SYSTEM, "hsqldb"), + equalTo(DbIncubatingAttributes.DB_NAME, "test"), + equalTo(DbIncubatingAttributes.DB_USER, "sa"), + equalTo(DbIncubatingAttributes.DB_CONNECTION_STRING, "hsqldb:mem:"), + equalTo( + DbIncubatingAttributes.DB_STATEMENT, + "update Customer set firstName=?, lastName=? where id=?"), + equalTo(DbIncubatingAttributes.DB_OPERATION, "UPDATE"), + equalTo(DbIncubatingAttributes.DB_SQL_TABLE, "Customer")))); + testing.clearData(); + + Customer foundCustomer = + testing.runWithSpan("parent", () -> repo.findByLastName("Anonymous").get(0)); + + assertThat(foundCustomer.getId()).isEqualTo(savedId); + assertThat(foundCustomer.getFirstName()).isEqualTo("Bill"); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("parent") + .hasKind(INTERNAL) + .hasNoParent() + .hasAttributes(Attributes.empty()), + span -> + span.hasName("SELECT Customer") + .hasKind(INTERNAL) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + satisfies( + stringKey("hibernate.session_id"), + val -> val.isInstanceOf(String.class))), + span -> + span.hasName("SELECT test.Customer") + .hasKind(CLIENT) + .hasParent(trace.getSpan(1)) + .hasAttributesSatisfyingExactly( + equalTo(DbIncubatingAttributes.DB_SYSTEM, "hsqldb"), + equalTo(DbIncubatingAttributes.DB_NAME, "test"), + equalTo(DbIncubatingAttributes.DB_USER, "sa"), + equalTo(DbIncubatingAttributes.DB_CONNECTION_STRING, "hsqldb:mem:"), + satisfies( + DbIncubatingAttributes.DB_STATEMENT, + val -> + val.matches( + Pattern.compile( + "select ([^.]+).id([^,]*), ([^.]+).firstName([^,]*), ([^.]+).lastName (.*)from Customer (.*)(where ([^.]+).lastName=\\?)"))), + equalTo(DbIncubatingAttributes.DB_OPERATION, "SELECT"), + equalTo(DbIncubatingAttributes.DB_SQL_TABLE, "Customer")))); + testing.clearData(); + + testing.runWithSpan("parent", () -> repo.delete(foundCustomer)); + + testing.waitAndAssertTraces( + trace -> { + if (isHibernate4) { + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("parent") + .hasKind(INTERNAL) + .hasNoParent() + .hasAttributes(Attributes.empty()), + span -> + span.hasName("Session.merge spring.jpa.Customer") + .hasKind(INTERNAL) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + satisfies( + stringKey("hibernate.session_id"), + val -> val.isInstanceOf(String.class))), + span -> + span.hasName("SELECT test.Customer") + .hasKind(CLIENT) + .hasParent(trace.getSpan(1)) + .hasAttributesSatisfyingExactly( + equalTo(DbIncubatingAttributes.DB_SYSTEM, "hsqldb"), + equalTo(DbIncubatingAttributes.DB_NAME, "test"), + equalTo(DbIncubatingAttributes.DB_USER, "sa"), + equalTo(DbIncubatingAttributes.DB_CONNECTION_STRING, "hsqldb:mem:"), + satisfies( + DbIncubatingAttributes.DB_STATEMENT, + val -> + val.matches( + Pattern.compile( + "select ([^.]+).id([^,]*), ([^.]+).firstName([^,]*), ([^.]+).lastName (.*)from Customer (.*)where ([^.]+).id=\\?"))), + equalTo(DbIncubatingAttributes.DB_OPERATION, "SELECT"), + equalTo(DbIncubatingAttributes.DB_SQL_TABLE, "Customer")), + span -> + span.hasName("Session.delete spring.jpa.Customer") + .hasKind(INTERNAL) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + satisfies( + stringKey("hibernate.session_id"), + val -> val.isInstanceOf(String.class))), + span -> + span.hasName("Transaction.commit") + .hasKind(INTERNAL) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + satisfies( + stringKey("hibernate.session_id"), + val -> val.isInstanceOf(String.class))), + span -> + span.hasName("DELETE test.Customer") + .hasKind(CLIENT) + .hasAttributesSatisfyingExactly( + equalTo(DbIncubatingAttributes.DB_SYSTEM, "hsqldb"), + equalTo(DbIncubatingAttributes.DB_NAME, "test"), + equalTo(DbIncubatingAttributes.DB_USER, "sa"), + equalTo(DbIncubatingAttributes.DB_CONNECTION_STRING, "hsqldb:mem:"), + equalTo( + DbIncubatingAttributes.DB_STATEMENT, + "delete from Customer where id=?"), + equalTo(DbIncubatingAttributes.DB_OPERATION, "DELETE"), + equalTo(DbIncubatingAttributes.DB_SQL_TABLE, "Customer"))); + + } else { + String findAction; + if (isLatestDep) { + findAction = "get"; + } else { + findAction = "find"; + } + + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasKind(INTERNAL).hasNoParent(), + span -> + span.hasName("Session." + findAction + " spring.jpa.Customer") + .hasKind(INTERNAL) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + satisfies( + stringKey("hibernate.session_id"), + val -> val.isInstanceOf(String.class))), + span -> + span.hasName("SELECT test.Customer") + .hasKind(CLIENT) + .hasParent(trace.getSpan(1)) + .hasAttributesSatisfyingExactly( + equalTo(DbIncubatingAttributes.DB_SYSTEM, "hsqldb"), + equalTo(DbIncubatingAttributes.DB_NAME, "test"), + equalTo(DbIncubatingAttributes.DB_USER, "sa"), + equalTo(DbIncubatingAttributes.DB_CONNECTION_STRING, "hsqldb:mem:"), + satisfies( + DbIncubatingAttributes.DB_STATEMENT, + val -> + val.matches( + Pattern.compile( + "select ([^.]+).id([^,]*), ([^.]+).firstName([^,]*), ([^.]+).lastName (.*)from Customer (.*)where ([^.]+).id=\\?"))), + equalTo(DbIncubatingAttributes.DB_OPERATION, "SELECT"), + equalTo(DbIncubatingAttributes.DB_SQL_TABLE, "Customer")), + span -> + span.hasName("Session.merge spring.jpa.Customer") + .hasKind(INTERNAL) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + satisfies( + stringKey("hibernate.session_id"), + val -> val.isInstanceOf(String.class))), + span -> + span.hasName("Session.delete spring.jpa.Customer") + .hasKind(INTERNAL) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + satisfies( + stringKey("hibernate.session_id"), + val -> val.isInstanceOf(String.class))), + span -> + span.hasName("Transaction.commit") + .hasKind(INTERNAL) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + satisfies( + stringKey("hibernate.session_id"), + val -> val.isInstanceOf(String.class))), + span -> + span.hasName("DELETE test.Customer") + .hasKind(CLIENT) + .hasAttributesSatisfyingExactly( + equalTo(DbIncubatingAttributes.DB_SYSTEM, "hsqldb"), + equalTo(DbIncubatingAttributes.DB_NAME, "test"), + equalTo(DbIncubatingAttributes.DB_USER, "sa"), + equalTo(DbIncubatingAttributes.DB_CONNECTION_STRING, "hsqldb:mem:"), + equalTo( + DbIncubatingAttributes.DB_STATEMENT, + "delete from Customer where id=?"), + equalTo(DbIncubatingAttributes.DB_OPERATION, "DELETE"), + equalTo(DbIncubatingAttributes.DB_SQL_TABLE, "Customer"))); + } + }); + } +} diff --git a/instrumentation/hibernate/hibernate-4.0/javaagent/src/test/resources/META-INF/persistence.xml b/instrumentation/hibernate/hibernate-4.0/javaagent/src/test/resources/META-INF/persistence.xml index 3cb3eed07e2c..531dd6af67ad 100644 --- a/instrumentation/hibernate/hibernate-4.0/javaagent/src/test/resources/META-INF/persistence.xml +++ b/instrumentation/hibernate/hibernate-4.0/javaagent/src/test/resources/META-INF/persistence.xml @@ -4,7 +4,7 @@ version="2.0"> - Value + io.opentelemetry.javaagent.instrumentation.hibernate.v4_0.Value true @@ -15,4 +15,4 @@ - \ No newline at end of file + diff --git a/instrumentation/hibernate/hibernate-4.0/javaagent/src/test/resources/hibernate.cfg.xml b/instrumentation/hibernate/hibernate-4.0/javaagent/src/test/resources/hibernate.cfg.xml index fe5e5de6b155..a3f9f7fef115 100644 --- a/instrumentation/hibernate/hibernate-4.0/javaagent/src/test/resources/hibernate.cfg.xml +++ b/instrumentation/hibernate/hibernate-4.0/javaagent/src/test/resources/hibernate.cfg.xml @@ -22,7 +22,7 @@ create - + diff --git a/instrumentation/hibernate/hibernate-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/v6_0/HibernateInstrumentationModule.java b/instrumentation/hibernate/hibernate-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/v6_0/HibernateInstrumentationModule.java index 3758ac451c01..83af9e7b0f90 100644 --- a/instrumentation/hibernate/hibernate-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/v6_0/HibernateInstrumentationModule.java +++ b/instrumentation/hibernate/hibernate-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/v6_0/HibernateInstrumentationModule.java @@ -11,11 +11,13 @@ 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.internal.ExperimentalInstrumentationModule; import java.util.List; import net.bytebuddy.matcher.ElementMatcher; @AutoService(InstrumentationModule.class) -public class HibernateInstrumentationModule extends InstrumentationModule { +public class HibernateInstrumentationModule extends InstrumentationModule + implements ExperimentalInstrumentationModule { public HibernateInstrumentationModule() { super("hibernate", "hibernate-6.0"); @@ -25,7 +27,12 @@ public HibernateInstrumentationModule() { public ElementMatcher.Junction classLoaderMatcher() { return hasClassesNamed( // not present before 6.0 - "org.hibernate.query.Query"); + "org.hibernate.query.spi.SqmQuery"); + } + + @Override + public String getModuleGroup() { + return "hibernate"; } @Override diff --git a/instrumentation/hibernate/hibernate-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/v6_0/QueryInstrumentation.java b/instrumentation/hibernate/hibernate-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/v6_0/QueryInstrumentation.java index 141175c30269..56ebecb8a2af 100644 --- a/instrumentation/hibernate/hibernate-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/v6_0/QueryInstrumentation.java +++ b/instrumentation/hibernate/hibernate-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/v6_0/QueryInstrumentation.java @@ -14,13 +14,12 @@ import static net.bytebuddy.matcher.ElementMatchers.namedOneOf; import io.opentelemetry.context.Context; -import io.opentelemetry.context.Scope; import io.opentelemetry.instrumentation.api.util.VirtualField; -import io.opentelemetry.javaagent.bootstrap.CallDepth; import io.opentelemetry.javaagent.bootstrap.Java8BytecodeBridge; import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; import io.opentelemetry.javaagent.instrumentation.hibernate.HibernateOperation; +import io.opentelemetry.javaagent.instrumentation.hibernate.HibernateOperationScope; import io.opentelemetry.javaagent.instrumentation.hibernate.SessionInfo; import net.bytebuddy.asm.Advice; import net.bytebuddy.description.type.TypeDescription; @@ -64,16 +63,10 @@ public void transform(TypeTransformer transformer) { public static class QueryMethodAdvice { @Advice.OnMethodEnter(suppress = Throwable.class) - public static void startMethod( - @Advice.This CommonQueryContract query, - @Advice.Local("otelCallDepth") CallDepth callDepth, - @Advice.Local("otelHibernateOperation") HibernateOperation hibernateOperation, - @Advice.Local("otelContext") Context context, - @Advice.Local("otelScope") Scope scope) { - - callDepth = CallDepth.forClass(HibernateOperation.class); - if (callDepth.getAndIncrement() > 0) { - return; + public static HibernateOperationScope startMethod(@Advice.This CommonQueryContract query) { + + if (HibernateOperationScope.enterDepthSkipCheck()) { + return null; } String queryString = null; @@ -93,32 +86,17 @@ public static void startMethod( SessionInfo sessionInfo = queryVirtualField.get(query); Context parentContext = Java8BytecodeBridge.currentContext(); - hibernateOperation = + HibernateOperation hibernateOperation = new HibernateOperation(getOperationNameForQuery(queryString), sessionInfo); - if (!instrumenter().shouldStart(parentContext, hibernateOperation)) { - return; - } - context = instrumenter().start(parentContext, hibernateOperation); - scope = context.makeCurrent(); + return HibernateOperationScope.start(hibernateOperation, parentContext, instrumenter()); } @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) public static void endMethod( - @Advice.Thrown Throwable throwable, - @Advice.Local("otelCallDepth") CallDepth callDepth, - @Advice.Local("otelHibernateOperation") HibernateOperation hibernateOperation, - @Advice.Local("otelContext") Context context, - @Advice.Local("otelScope") Scope scope) { - - if (callDepth.decrementAndGet() > 0) { - return; - } + @Advice.Thrown Throwable throwable, @Advice.Enter HibernateOperationScope scope) { - if (scope != null) { - scope.close(); - instrumenter().end(context, hibernateOperation, null, throwable); - } + HibernateOperationScope.end(scope, throwable); } } } diff --git a/instrumentation/hibernate/hibernate-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/v6_0/SessionInstrumentation.java b/instrumentation/hibernate/hibernate-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/v6_0/SessionInstrumentation.java index fde18f963d42..6853916ca629 100644 --- a/instrumentation/hibernate/hibernate-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/v6_0/SessionInstrumentation.java +++ b/instrumentation/hibernate/hibernate-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/v6_0/SessionInstrumentation.java @@ -18,13 +18,12 @@ import static net.bytebuddy.matcher.ElementMatchers.takesArgument; import io.opentelemetry.context.Context; -import io.opentelemetry.context.Scope; import io.opentelemetry.instrumentation.api.util.VirtualField; -import io.opentelemetry.javaagent.bootstrap.CallDepth; import io.opentelemetry.javaagent.bootstrap.Java8BytecodeBridge; import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; import io.opentelemetry.javaagent.instrumentation.hibernate.HibernateOperation; +import io.opentelemetry.javaagent.instrumentation.hibernate.HibernateOperationScope; import io.opentelemetry.javaagent.instrumentation.hibernate.SessionInfo; import jakarta.persistence.criteria.CriteriaQuery; import net.bytebuddy.asm.Advice; @@ -95,20 +94,15 @@ public void transform(TypeTransformer transformer) { public static class SessionMethodAdvice { @Advice.OnMethodEnter(suppress = Throwable.class) - public static void startMethod( + public static HibernateOperationScope startMethod( @Advice.This SharedSessionContract session, @Advice.Origin("#m") String name, @Advice.Origin("#d") String descriptor, @Advice.Argument(0) Object arg0, - @Advice.Argument(value = 1, optional = true) Object arg1, - @Advice.Local("otelCallDepth") CallDepth callDepth, - @Advice.Local("otelHibernateOperation") HibernateOperation hibernateOperation, - @Advice.Local("otelContext") Context context, - @Advice.Local("otelScope") Scope scope) { - - callDepth = CallDepth.forClass(HibernateOperation.class); - if (callDepth.getAndIncrement() > 0) { - return; + @Advice.Argument(value = 1, optional = true) Object arg1) { + + if (HibernateOperationScope.enterDepthSkipCheck()) { + return null; } VirtualField virtualField = @@ -118,32 +112,17 @@ public static void startMethod( Context parentContext = Java8BytecodeBridge.currentContext(); String entityName = getEntityName(descriptor, arg0, arg1, EntityNameUtil.bestGuessEntityName(session)); - hibernateOperation = + HibernateOperation hibernateOperation = new HibernateOperation(getSessionMethodOperationName(name), entityName, sessionInfo); - if (!instrumenter().shouldStart(parentContext, hibernateOperation)) { - return; - } - context = instrumenter().start(parentContext, hibernateOperation); - scope = context.makeCurrent(); + return HibernateOperationScope.start(hibernateOperation, parentContext, instrumenter()); } @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) public static void endMethod( - @Advice.Thrown Throwable throwable, - @Advice.Local("otelCallDepth") CallDepth callDepth, - @Advice.Local("otelHibernateOperation") HibernateOperation hibernateOperation, - @Advice.Local("otelContext") Context context, - @Advice.Local("otelScope") Scope scope) { + @Advice.Thrown Throwable throwable, @Advice.Enter HibernateOperationScope scope) { - if (callDepth.decrementAndGet() > 0) { - return; - } - - if (scope != null) { - scope.close(); - instrumenter().end(context, hibernateOperation, null, throwable); - } + HibernateOperationScope.end(scope, throwable); } } diff --git a/instrumentation/hibernate/hibernate-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/v6_0/TransactionInstrumentation.java b/instrumentation/hibernate/hibernate-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/v6_0/TransactionInstrumentation.java index 711773e4e5b4..cea7315139c2 100644 --- a/instrumentation/hibernate/hibernate-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/v6_0/TransactionInstrumentation.java +++ b/instrumentation/hibernate/hibernate-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/v6_0/TransactionInstrumentation.java @@ -13,13 +13,12 @@ import static net.bytebuddy.matcher.ElementMatchers.takesArguments; import io.opentelemetry.context.Context; -import io.opentelemetry.context.Scope; import io.opentelemetry.instrumentation.api.util.VirtualField; -import io.opentelemetry.javaagent.bootstrap.CallDepth; import io.opentelemetry.javaagent.bootstrap.Java8BytecodeBridge; import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; import io.opentelemetry.javaagent.instrumentation.hibernate.HibernateOperation; +import io.opentelemetry.javaagent.instrumentation.hibernate.HibernateOperationScope; import io.opentelemetry.javaagent.instrumentation.hibernate.SessionInfo; import net.bytebuddy.asm.Advice; import net.bytebuddy.description.type.TypeDescription; @@ -49,16 +48,10 @@ public void transform(TypeTransformer transformer) { public static class TransactionCommitAdvice { @Advice.OnMethodEnter(suppress = Throwable.class) - public static void startCommit( - @Advice.This Transaction transaction, - @Advice.Local("otelCallDepth") CallDepth callDepth, - @Advice.Local("otelHibernateOperation") HibernateOperation hibernateOperation, - @Advice.Local("otelContext") Context context, - @Advice.Local("otelScope") Scope scope) { + public static HibernateOperationScope startCommit(@Advice.This Transaction transaction) { - callDepth = CallDepth.forClass(HibernateOperation.class); - if (callDepth.getAndIncrement() > 0) { - return; + if (HibernateOperationScope.enterDepthSkipCheck()) { + return null; } VirtualField transactionVirtualField = @@ -66,31 +59,17 @@ public static void startCommit( SessionInfo sessionInfo = transactionVirtualField.get(transaction); Context parentContext = Java8BytecodeBridge.currentContext(); - hibernateOperation = new HibernateOperation("Transaction.commit", sessionInfo); - if (!instrumenter().shouldStart(parentContext, hibernateOperation)) { - return; - } + HibernateOperation hibernateOperation = + new HibernateOperation("Transaction.commit", sessionInfo); - context = instrumenter().start(parentContext, hibernateOperation); - scope = context.makeCurrent(); + return HibernateOperationScope.start(hibernateOperation, parentContext, instrumenter()); } @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) public static void endCommit( - @Advice.Thrown Throwable throwable, - @Advice.Local("otelCallDepth") CallDepth callDepth, - @Advice.Local("otelHibernateOperation") HibernateOperation hibernateOperation, - @Advice.Local("otelContext") Context context, - @Advice.Local("otelScope") Scope scope) { - - if (callDepth.decrementAndGet() > 0) { - return; - } + @Advice.Thrown Throwable throwable, @Advice.Enter HibernateOperationScope scope) { - if (scope != null) { - scope.close(); - instrumenter().end(context, hibernateOperation, null, throwable); - } + HibernateOperationScope.end(scope, throwable); } } } diff --git a/instrumentation/hibernate/hibernate-6.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/hibernate/v6_0/CriteriaTest.java b/instrumentation/hibernate/hibernate-6.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/hibernate/v6_0/CriteriaTest.java index ef52b232b0c8..d2ab1450ed09 100644 --- a/instrumentation/hibernate/hibernate-6.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/hibernate/v6_0/CriteriaTest.java +++ b/instrumentation/hibernate/hibernate-6.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/hibernate/v6_0/CriteriaTest.java @@ -11,7 +11,7 @@ import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.trace.SpanKind; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.opentelemetry.semconv.incubating.DbIncubatingAttributes; import jakarta.persistence.criteria.CriteriaBuilder; import jakarta.persistence.criteria.CriteriaQuery; import jakarta.persistence.criteria.Root; @@ -36,6 +36,7 @@ private static Stream provideParameters() { Arguments.of(named("getSingleResultOrNull", interactions.get(2)))); } + @SuppressWarnings("deprecation") // TODO DbIncubatingAttributes.DB_CONNECTION_STRING deprecation @ParameterizedTest(name = "{index}: {0}") @MethodSource("provideParameters") void testCriteriaQuery(Consumer> interaction) { @@ -59,43 +60,41 @@ void testCriteriaQuery(Consumer> interaction) { testing.waitAndAssertTraces( trace -> - trace - .hasSize(4) - .hasSpansSatisfyingExactly( - span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(), - span -> - span.hasName( - "SELECT io.opentelemetry.javaagent.instrumentation.hibernate.v6_0.Value") - .hasKind(SpanKind.INTERNAL) - .hasParent(trace.getSpan(0)) - .hasAttributesSatisfyingExactly( - satisfies( - AttributeKey.stringKey("hibernate.session_id"), - val -> val.isInstanceOf(String.class))), - span -> - span.hasName("SELECT db1.Value") - .hasKind(SpanKind.CLIENT) - .hasParent(trace.getSpan(1)) - .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.DB_SYSTEM, "h2"), - equalTo(SemanticAttributes.DB_NAME, "db1"), - equalTo(SemanticAttributes.DB_USER, "sa"), - equalTo(SemanticAttributes.DB_CONNECTION_STRING, "h2:mem:"), - satisfies( - SemanticAttributes.DB_STATEMENT, - stringAssert -> stringAssert.startsWith("select")), - equalTo(SemanticAttributes.DB_OPERATION, "SELECT"), - equalTo(SemanticAttributes.DB_SQL_TABLE, "Value")), - span -> - span.hasName("Transaction.commit") - .hasKind(SpanKind.INTERNAL) - .hasParent(trace.getSpan(0)) - .hasAttributesSatisfyingExactly( - equalTo( - AttributeKey.stringKey("hibernate.session_id"), - trace - .getSpan(1) - .getAttributes() - .get(AttributeKey.stringKey("hibernate.session_id")))))); + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(), + span -> + span.hasName( + "SELECT io.opentelemetry.javaagent.instrumentation.hibernate.v6_0.Value") + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + satisfies( + AttributeKey.stringKey("hibernate.session_id"), + val -> val.isInstanceOf(String.class))), + span -> + span.hasName("SELECT db1.Value") + .hasKind(SpanKind.CLIENT) + .hasParent(trace.getSpan(1)) + .hasAttributesSatisfyingExactly( + equalTo(DbIncubatingAttributes.DB_SYSTEM, "h2"), + equalTo(DbIncubatingAttributes.DB_NAME, "db1"), + equalTo(DbIncubatingAttributes.DB_USER, "sa"), + equalTo(DbIncubatingAttributes.DB_CONNECTION_STRING, "h2:mem:"), + satisfies( + DbIncubatingAttributes.DB_STATEMENT, + stringAssert -> stringAssert.startsWith("select")), + equalTo(DbIncubatingAttributes.DB_OPERATION, "SELECT"), + equalTo(DbIncubatingAttributes.DB_SQL_TABLE, "Value")), + span -> + span.hasName("Transaction.commit") + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo( + AttributeKey.stringKey("hibernate.session_id"), + trace + .getSpan(1) + .getAttributes() + .get(AttributeKey.stringKey("hibernate.session_id")))))); } } diff --git a/instrumentation/hibernate/hibernate-6.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/hibernate/v6_0/EntityManagerTest.java b/instrumentation/hibernate/hibernate-6.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/hibernate/v6_0/EntityManagerTest.java index 30e07a07fac1..5bc237dc24b2 100644 --- a/instrumentation/hibernate/hibernate-6.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/hibernate/v6_0/EntityManagerTest.java +++ b/instrumentation/hibernate/hibernate-6.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/hibernate/v6_0/EntityManagerTest.java @@ -13,7 +13,7 @@ import io.opentelemetry.api.trace.SpanKind; import io.opentelemetry.sdk.testing.assertj.SpanDataAssert; import io.opentelemetry.sdk.trace.data.SpanData; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.opentelemetry.semconv.incubating.DbIncubatingAttributes; import jakarta.persistence.EntityManager; import jakarta.persistence.EntityManagerFactory; import jakarta.persistence.EntityTransaction; @@ -112,6 +112,7 @@ void testHibernateAction(Parameter parameter) { }); } + @SuppressWarnings("deprecation") // TODO DbIncubatingAttributes.DB_CONNECTION_STRING deprecation @ParameterizedTest(name = "{index}: {0}") @MethodSource("provideAttachesStateParameters") void testAttachesStateToQuery(Parameter parameter) { @@ -129,33 +130,31 @@ void testAttachesStateToQuery(Parameter parameter) { testing.waitAndAssertTraces( trace -> - trace - .hasSize(4) - .hasSpansSatisfyingExactly( - span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(), - span -> assertSessionSpan(span, trace.getSpan(0), parameter.resource), - span -> - span.hasName("SELECT db1.Value") - .hasKind(SpanKind.CLIENT) - .hasParent(trace.getSpan(1)) - .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.DB_SYSTEM, "h2"), - equalTo(SemanticAttributes.DB_NAME, "db1"), - equalTo(SemanticAttributes.DB_USER, "sa"), - equalTo(SemanticAttributes.DB_CONNECTION_STRING, "h2:mem:"), - satisfies( - SemanticAttributes.DB_STATEMENT, - val -> val.isInstanceOf(String.class)), - equalTo(SemanticAttributes.DB_OPERATION, "SELECT"), - equalTo(SemanticAttributes.DB_SQL_TABLE, "Value")), - span -> - assertTransactionCommitSpan( - span, - trace.getSpan(0), - trace - .getSpan(1) - .getAttributes() - .get(AttributeKey.stringKey("hibernate.session_id"))))); + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(), + span -> assertSessionSpan(span, trace.getSpan(0), parameter.resource), + span -> + span.hasName("SELECT db1.Value") + .hasKind(SpanKind.CLIENT) + .hasParent(trace.getSpan(1)) + .hasAttributesSatisfyingExactly( + equalTo(DbIncubatingAttributes.DB_SYSTEM, "h2"), + equalTo(DbIncubatingAttributes.DB_NAME, "db1"), + equalTo(DbIncubatingAttributes.DB_USER, "sa"), + equalTo(DbIncubatingAttributes.DB_CONNECTION_STRING, "h2:mem:"), + satisfies( + DbIncubatingAttributes.DB_STATEMENT, + val -> val.isInstanceOf(String.class)), + equalTo(DbIncubatingAttributes.DB_OPERATION, "SELECT"), + equalTo(DbIncubatingAttributes.DB_SQL_TABLE, "Value")), + span -> + assertTransactionCommitSpan( + span, + trace.getSpan(0), + trace + .getSpan(1) + .getAttributes() + .get(AttributeKey.stringKey("hibernate.session_id"))))); } private static Stream provideHibernateActionParameters() { @@ -289,32 +288,34 @@ private static class Parameter { public final Function queryBuildMethod; } + @SuppressWarnings("deprecation") // TODO DbIncubatingAttributes.DB_CONNECTION_STRING deprecation private static SpanDataAssert assertClientSpan(SpanDataAssert span, SpanData parent) { return span.hasKind(SpanKind.CLIENT) .hasParent(parent) .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.DB_SYSTEM, "h2"), - equalTo(SemanticAttributes.DB_NAME, "db1"), - equalTo(SemanticAttributes.DB_USER, "sa"), - equalTo(SemanticAttributes.DB_CONNECTION_STRING, "h2:mem:"), - satisfies(SemanticAttributes.DB_STATEMENT, val -> val.isInstanceOf(String.class)), - satisfies(SemanticAttributes.DB_OPERATION, val -> val.isInstanceOf(String.class)), - equalTo(SemanticAttributes.DB_SQL_TABLE, "Value")); + equalTo(DbIncubatingAttributes.DB_SYSTEM, "h2"), + equalTo(DbIncubatingAttributes.DB_NAME, "db1"), + equalTo(DbIncubatingAttributes.DB_USER, "sa"), + equalTo(DbIncubatingAttributes.DB_CONNECTION_STRING, "h2:mem:"), + satisfies(DbIncubatingAttributes.DB_STATEMENT, val -> val.isInstanceOf(String.class)), + satisfies(DbIncubatingAttributes.DB_OPERATION, val -> val.isInstanceOf(String.class)), + equalTo(DbIncubatingAttributes.DB_SQL_TABLE, "Value")); } + @SuppressWarnings("deprecation") // TODO DbIncubatingAttributes.DB_CONNECTION_STRING deprecation private static SpanDataAssert assertClientSpan( SpanDataAssert span, SpanData parent, String spanName) { return span.hasName(spanName) .hasKind(SpanKind.CLIENT) .hasParent(parent) .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.DB_SYSTEM, "h2"), - equalTo(SemanticAttributes.DB_NAME, "db1"), - equalTo(SemanticAttributes.DB_USER, "sa"), - equalTo(SemanticAttributes.DB_CONNECTION_STRING, "h2:mem:"), - satisfies(SemanticAttributes.DB_STATEMENT, val -> val.isInstanceOf(String.class)), - satisfies(SemanticAttributes.DB_OPERATION, val -> val.isInstanceOf(String.class)), - equalTo(SemanticAttributes.DB_SQL_TABLE, "Value")); + equalTo(DbIncubatingAttributes.DB_SYSTEM, "h2"), + equalTo(DbIncubatingAttributes.DB_NAME, "db1"), + equalTo(DbIncubatingAttributes.DB_USER, "sa"), + equalTo(DbIncubatingAttributes.DB_CONNECTION_STRING, "h2:mem:"), + satisfies(DbIncubatingAttributes.DB_STATEMENT, val -> val.isInstanceOf(String.class)), + satisfies(DbIncubatingAttributes.DB_OPERATION, val -> val.isInstanceOf(String.class)), + equalTo(DbIncubatingAttributes.DB_SQL_TABLE, "Value")); } private static SpanDataAssert assertSessionSpan( diff --git a/instrumentation/hibernate/hibernate-6.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/hibernate/v6_0/ProcedureCallTest.java b/instrumentation/hibernate/hibernate-6.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/hibernate/v6_0/ProcedureCallTest.java index 7ea9a34683e5..1a1e36cf9a61 100644 --- a/instrumentation/hibernate/hibernate-6.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/hibernate/v6_0/ProcedureCallTest.java +++ b/instrumentation/hibernate/hibernate-6.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/hibernate/v6_0/ProcedureCallTest.java @@ -13,7 +13,7 @@ import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; import io.opentelemetry.sdk.trace.data.StatusData; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.opentelemetry.semconv.incubating.DbIncubatingAttributes; import jakarta.persistence.ParameterMode; import java.sql.Connection; import java.sql.DriverManager; @@ -69,6 +69,7 @@ static void cleanup() { } } + @SuppressWarnings("deprecation") // TODO DbIncubatingAttributes.DB_CONNECTION_STRING deprecation @Test void testProcedureCall() { testing.runWithSpan( @@ -86,40 +87,38 @@ void testProcedureCall() { testing.waitAndAssertTraces( trace -> - trace - .hasSize(4) - .hasSpansSatisfyingExactly( - span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(), - span -> - span.hasName("ProcedureCall.getOutputs TEST_PROC") - .hasKind(SpanKind.INTERNAL) - .hasParent(trace.getSpan(0)) - .hasAttributesSatisfyingExactly( - satisfies( - AttributeKey.stringKey("hibernate.session_id"), - val -> val.isInstanceOf(String.class))), - span -> - span.hasName("CALL test.TEST_PROC") - .hasKind(SpanKind.CLIENT) - .hasParent(trace.getSpan(1)) - .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.DB_SYSTEM, "hsqldb"), - equalTo(SemanticAttributes.DB_NAME, "test"), - equalTo(SemanticAttributes.DB_USER, "sa"), - equalTo(SemanticAttributes.DB_CONNECTION_STRING, "hsqldb:mem:"), - equalTo(SemanticAttributes.DB_STATEMENT, "{call TEST_PROC()}"), - equalTo(SemanticAttributes.DB_OPERATION, "CALL")), - span -> - span.hasName("Transaction.commit") - .hasKind(SpanKind.INTERNAL) - .hasParent(trace.getSpan(0)) - .hasAttributesSatisfyingExactly( - equalTo( - AttributeKey.stringKey("hibernate.session_id"), - trace - .getSpan(1) - .getAttributes() - .get(AttributeKey.stringKey("hibernate.session_id")))))); + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(), + span -> + span.hasName("ProcedureCall.getOutputs TEST_PROC") + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + satisfies( + AttributeKey.stringKey("hibernate.session_id"), + val -> val.isInstanceOf(String.class))), + span -> + span.hasName("CALL test.TEST_PROC") + .hasKind(SpanKind.CLIENT) + .hasParent(trace.getSpan(1)) + .hasAttributesSatisfyingExactly( + equalTo(DbIncubatingAttributes.DB_SYSTEM, "hsqldb"), + equalTo(DbIncubatingAttributes.DB_NAME, "test"), + equalTo(DbIncubatingAttributes.DB_USER, "sa"), + equalTo(DbIncubatingAttributes.DB_CONNECTION_STRING, "hsqldb:mem:"), + equalTo(DbIncubatingAttributes.DB_STATEMENT, "{call TEST_PROC()}"), + equalTo(DbIncubatingAttributes.DB_OPERATION, "CALL")), + span -> + span.hasName("Transaction.commit") + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo( + AttributeKey.stringKey("hibernate.session_id"), + trace + .getSpan(1) + .getAttributes() + .get(AttributeKey.stringKey("hibernate.session_id")))))); } @Test @@ -145,44 +144,41 @@ void testFailingProcedureCall() { testing.waitAndAssertTraces( trace -> - trace - .hasSize(3) - .hasSpansSatisfyingExactly( - span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(), - span -> - span.hasName("ProcedureCall.getOutputs TEST_PROC") - .hasKind(SpanKind.INTERNAL) - .hasParent(trace.getSpan(0)) - .hasStatus(StatusData.error()) - .hasEventsSatisfyingExactly( - event -> - event - .hasName("exception") - .hasAttributesSatisfyingExactly( - equalTo( - AttributeKey.stringKey("exception.type"), - "org.hibernate.exception.SQLGrammarException"), - satisfies( - AttributeKey.stringKey("exception.message"), - val -> - val.startsWith("could not prepare statement")), - satisfies( - AttributeKey.stringKey("exception.stacktrace"), - val -> val.isNotNull()))) - .hasAttributesSatisfyingExactly( - satisfies( - AttributeKey.stringKey("hibernate.session_id"), - val -> val.isInstanceOf(String.class))), - span -> - span.hasName("Transaction.commit") - .hasKind(SpanKind.INTERNAL) - .hasParent(trace.getSpan(0)) - .hasAttributesSatisfyingExactly( - equalTo( - AttributeKey.stringKey("hibernate.session_id"), - trace - .getSpan(1) - .getAttributes() - .get(AttributeKey.stringKey("hibernate.session_id")))))); + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(), + span -> + span.hasName("ProcedureCall.getOutputs TEST_PROC") + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(0)) + .hasStatus(StatusData.error()) + .hasEventsSatisfyingExactly( + event -> + event + .hasName("exception") + .hasAttributesSatisfyingExactly( + equalTo( + AttributeKey.stringKey("exception.type"), + "org.hibernate.exception.SQLGrammarException"), + satisfies( + AttributeKey.stringKey("exception.message"), + val -> val.startsWith("could not prepare statement")), + satisfies( + AttributeKey.stringKey("exception.stacktrace"), + val -> val.isNotNull()))) + .hasAttributesSatisfyingExactly( + satisfies( + AttributeKey.stringKey("hibernate.session_id"), + val -> val.isInstanceOf(String.class))), + span -> + span.hasName("Transaction.commit") + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo( + AttributeKey.stringKey("hibernate.session_id"), + trace + .getSpan(1) + .getAttributes() + .get(AttributeKey.stringKey("hibernate.session_id")))))); } } diff --git a/instrumentation/hibernate/hibernate-6.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/hibernate/v6_0/SessionTest.java b/instrumentation/hibernate/hibernate-6.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/hibernate/v6_0/SessionTest.java index b776bd35fe9e..d95c4fe7805d 100644 --- a/instrumentation/hibernate/hibernate-6.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/hibernate/v6_0/SessionTest.java +++ b/instrumentation/hibernate/hibernate-6.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/hibernate/v6_0/SessionTest.java @@ -15,7 +15,7 @@ import io.opentelemetry.sdk.testing.assertj.SpanDataAssert; import io.opentelemetry.sdk.trace.data.SpanData; import io.opentelemetry.sdk.trace.data.StatusData; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.opentelemetry.semconv.incubating.DbIncubatingAttributes; import java.util.Arrays; import java.util.List; import java.util.Locale; @@ -79,25 +79,23 @@ void testHibernateActionStatelessSession(Parameter parameter) { private static void assertTraces(Parameter parameter) { testing.waitAndAssertTraces( trace -> - trace - .hasSize(4) - .hasSpansSatisfyingExactly( - span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(), - span -> - assertSessionSpan( - span, - trace.getSpan(0), - "Session." + parameter.methodName + " " + parameter.resource), - span -> assertClientSpan(span, trace.getSpan(1)), - span -> - assertSpanWithSessionId( - span, - trace.getSpan(0), - "Transaction.commit", - trace - .getSpan(1) - .getAttributes() - .get(AttributeKey.stringKey("hibernate.session_id"))))); + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(), + span -> + assertSessionSpan( + span, + trace.getSpan(0), + "Session." + parameter.methodName + " " + parameter.resource), + span -> assertClientSpan(span, trace.getSpan(1)), + span -> + assertSpanWithSessionId( + span, + trace.getSpan(0), + "Transaction.commit", + trace + .getSpan(1) + .getAttributes() + .get(AttributeKey.stringKey("hibernate.session_id"))))); } @ParameterizedTest(name = "{index}: {0}") @@ -119,26 +117,24 @@ void testHibernateReplicate(Parameter parameter) { testing.waitAndAssertTraces( trace -> - trace - .hasSize(5) - .hasSpansSatisfyingExactly( - span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(), - span -> - assertSessionSpan( - span, - trace.getSpan(0), - "Session." + parameter.methodName + " " + parameter.resource), - span -> assertClientSpan(span, trace.getSpan(1), "SELECT"), - span -> - assertSpanWithSessionId( - span, - trace.getSpan(0), - "Transaction.commit", - trace - .getSpan(1) - .getAttributes() - .get(AttributeKey.stringKey("hibernate.session_id"))), - span -> assertClientSpan(span, trace.getSpan(3)))); + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(), + span -> + assertSessionSpan( + span, + trace.getSpan(0), + "Session." + parameter.methodName + " " + parameter.resource), + span -> assertClientSpan(span, trace.getSpan(1), "SELECT"), + span -> + assertSpanWithSessionId( + span, + trace.getSpan(0), + "Transaction.commit", + trace + .getSpan(1) + .getAttributes() + .get(AttributeKey.stringKey("hibernate.session_id"))), + span -> assertClientSpan(span, trace.getSpan(3)))); } @Test @@ -160,31 +156,29 @@ void testHibernateFailedReplicate() { testing.waitAndAssertTraces( trace -> - trace - .hasSize(3) - .hasSpansSatisfyingExactly( - span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(), - span -> - span.hasName("Session.replicate java.lang.Long") - .hasKind(SpanKind.INTERNAL) - .hasParent(trace.getSpan(0)) - .hasStatus(StatusData.error()) - .hasException( - new UnknownEntityTypeException( - "Unable to locate persister: java.lang.Long")) - .hasAttributesSatisfyingExactly( - satisfies( - AttributeKey.stringKey("hibernate.session_id"), - val -> val.isInstanceOf(String.class))), - span -> - assertSpanWithSessionId( - span, - trace.getSpan(0), - "Transaction.commit", - trace - .getSpan(1) - .getAttributes() - .get(AttributeKey.stringKey("hibernate.session_id"))))); + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(), + span -> + span.hasName("Session.replicate java.lang.Long") + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(0)) + .hasStatus(StatusData.error()) + .hasException( + new UnknownEntityTypeException( + "Unable to locate persister: java.lang.Long")) + .hasAttributesSatisfyingExactly( + satisfies( + AttributeKey.stringKey("hibernate.session_id"), + val -> val.isInstanceOf(String.class))), + span -> + assertSpanWithSessionId( + span, + trace.getSpan(0), + "Transaction.commit", + trace + .getSpan(1) + .getAttributes() + .get(AttributeKey.stringKey("hibernate.session_id"))))); } @ParameterizedTest(name = "{index}: {0}") @@ -206,27 +200,26 @@ void testHibernateCommitAction(Parameter parameter) { testing.waitAndAssertTraces( trace -> - trace - .hasSize(4) - .hasSpansSatisfyingExactly( - span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(), - span -> - assertSessionSpan( - span, - trace.getSpan(0), - "Session." + parameter.methodName + " " + parameter.resource), - span -> - assertSpanWithSessionId( - span, - trace.getSpan(0), - "Transaction.commit", - trace - .getSpan(1) - .getAttributes() - .get(AttributeKey.stringKey("hibernate.session_id"))), - span -> assertClientSpan(span, trace.getSpan(2)))); + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(), + span -> + assertSessionSpan( + span, + trace.getSpan(0), + "Session." + parameter.methodName + " " + parameter.resource), + span -> + assertSpanWithSessionId( + span, + trace.getSpan(0), + "Transaction.commit", + trace + .getSpan(1) + .getAttributes() + .get(AttributeKey.stringKey("hibernate.session_id"))), + span -> assertClientSpan(span, trace.getSpan(2)))); } + @SuppressWarnings("deprecation") // TODO DbIncubatingAttributes.DB_CONNECTION_STRING deprecation @ParameterizedTest(name = "{index}: {0}") @MethodSource("provideAttachesStateToQueryParameters") void testAttachesStateToQuery(Parameter parameter) { @@ -243,33 +236,31 @@ void testAttachesStateToQuery(Parameter parameter) { testing.waitAndAssertTraces( trace -> - trace - .hasSize(4) - .hasSpansSatisfyingExactly( - span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(), - span -> assertSessionSpan(span, trace.getSpan(0), parameter.resource), - span -> - span.hasKind(SpanKind.CLIENT) - .hasParent(trace.getSpan(1)) - .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.DB_SYSTEM, "h2"), - equalTo(SemanticAttributes.DB_NAME, "db1"), - equalTo(SemanticAttributes.DB_USER, "sa"), - equalTo(SemanticAttributes.DB_CONNECTION_STRING, "h2:mem:"), - satisfies( - SemanticAttributes.DB_STATEMENT, - val -> val.isInstanceOf(String.class)), - equalTo(SemanticAttributes.DB_OPERATION, "SELECT"), - equalTo(SemanticAttributes.DB_SQL_TABLE, "Value")), - span -> - assertSpanWithSessionId( - span, - trace.getSpan(0), - "Transaction.commit", - trace - .getSpan(1) - .getAttributes() - .get(AttributeKey.stringKey("hibernate.session_id"))))); + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(), + span -> assertSessionSpan(span, trace.getSpan(0), parameter.resource), + span -> + span.hasKind(SpanKind.CLIENT) + .hasParent(trace.getSpan(1)) + .hasAttributesSatisfyingExactly( + equalTo(DbIncubatingAttributes.DB_SYSTEM, "h2"), + equalTo(DbIncubatingAttributes.DB_NAME, "db1"), + equalTo(DbIncubatingAttributes.DB_USER, "sa"), + equalTo(DbIncubatingAttributes.DB_CONNECTION_STRING, "h2:mem:"), + satisfies( + DbIncubatingAttributes.DB_STATEMENT, + val -> val.isInstanceOf(String.class)), + equalTo(DbIncubatingAttributes.DB_OPERATION, "SELECT"), + equalTo(DbIncubatingAttributes.DB_SQL_TABLE, "Value")), + span -> + assertSpanWithSessionId( + span, + trace.getSpan(0), + "Transaction.commit", + trace + .getSpan(1) + .getAttributes() + .get(AttributeKey.stringKey("hibernate.session_id"))))); } @Test @@ -300,55 +291,53 @@ void testHibernateOverlappingSessions() { testing.waitAndAssertTraces( trace -> - trace - .hasSize(9) - .hasSpansSatisfyingExactly( - span -> span.hasName("overlapping Sessions"), - span -> { - assertSessionSpan( - span, - trace.getSpan(0), - "Session.save io.opentelemetry.javaagent.instrumentation.hibernate.v6_0.Value"); - sessionId1.set( - trace - .getSpan(1) - .getAttributes() - .get(AttributeKey.stringKey("hibernate.session_id"))); - }, - span -> { - assertSessionSpan( - span, - trace.getSpan(0), - "Session.insert io.opentelemetry.javaagent.instrumentation.hibernate.v6_0.Value"); - sessionId2.set( - trace - .getSpan(2) - .getAttributes() - .get(AttributeKey.stringKey("hibernate.session_id"))); - }, - span -> assertClientSpan(span, trace.getSpan(2), "INSERT"), - span -> { - assertSessionSpan( - span, - trace.getSpan(0), - "Session.save io.opentelemetry.javaagent.instrumentation.hibernate.v6_0.Value"); - sessionId3.set( - trace - .getSpan(4) - .getAttributes() - .get(AttributeKey.stringKey("hibernate.session_id"))); - }, - span -> - assertSpanWithSessionId( - span, - trace.getSpan(0), - "Session.delete io.opentelemetry.javaagent.instrumentation.hibernate.v6_0.Value", - sessionId1.get()), - span -> - assertSpanWithSessionId( - span, trace.getSpan(0), "Transaction.commit", sessionId1.get()), - span -> assertClientSpan(span, trace.getSpan(6), "INSERT"), - span -> assertClientSpan(span, trace.getSpan(6), "DELETE"))); + trace.hasSpansSatisfyingExactly( + span -> span.hasName("overlapping Sessions"), + span -> { + assertSessionSpan( + span, + trace.getSpan(0), + "Session.save io.opentelemetry.javaagent.instrumentation.hibernate.v6_0.Value"); + sessionId1.set( + trace + .getSpan(1) + .getAttributes() + .get(AttributeKey.stringKey("hibernate.session_id"))); + }, + span -> { + assertSessionSpan( + span, + trace.getSpan(0), + "Session.insert io.opentelemetry.javaagent.instrumentation.hibernate.v6_0.Value"); + sessionId2.set( + trace + .getSpan(2) + .getAttributes() + .get(AttributeKey.stringKey("hibernate.session_id"))); + }, + span -> assertClientSpan(span, trace.getSpan(2), "INSERT"), + span -> { + assertSessionSpan( + span, + trace.getSpan(0), + "Session.save io.opentelemetry.javaagent.instrumentation.hibernate.v6_0.Value"); + sessionId3.set( + trace + .getSpan(4) + .getAttributes() + .get(AttributeKey.stringKey("hibernate.session_id"))); + }, + span -> + assertSpanWithSessionId( + span, + trace.getSpan(0), + "Session.delete io.opentelemetry.javaagent.instrumentation.hibernate.v6_0.Value", + sessionId1.get()), + span -> + assertSpanWithSessionId( + span, trace.getSpan(0), "Transaction.commit", sessionId1.get()), + span -> assertClientSpan(span, trace.getSpan(6), "INSERT"), + span -> assertClientSpan(span, trace.getSpan(6), "DELETE"))); assertNotEquals(sessionId1.get(), sessionId2.get()); assertNotEquals(sessionId2.get(), sessionId3.get()); @@ -814,33 +803,35 @@ private static SpanDataAssert assertSpanWithSessionId( equalTo(AttributeKey.stringKey("hibernate.session_id"), sessionId)); } + @SuppressWarnings("deprecation") // TODO DbIncubatingAttributes.DB_CONNECTION_STRING deprecation private static SpanDataAssert assertClientSpan(SpanDataAssert span, SpanData parent) { return span.hasKind(SpanKind.CLIENT) .hasParent(parent) .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.DB_SYSTEM, "h2"), - equalTo(SemanticAttributes.DB_NAME, "db1"), - equalTo(SemanticAttributes.DB_USER, "sa"), - equalTo(SemanticAttributes.DB_CONNECTION_STRING, "h2:mem:"), - satisfies(SemanticAttributes.DB_STATEMENT, val -> val.isInstanceOf(String.class)), - satisfies(SemanticAttributes.DB_OPERATION, val -> val.isInstanceOf(String.class)), - equalTo(SemanticAttributes.DB_SQL_TABLE, "Value")); + equalTo(DbIncubatingAttributes.DB_SYSTEM, "h2"), + equalTo(DbIncubatingAttributes.DB_NAME, "db1"), + equalTo(DbIncubatingAttributes.DB_USER, "sa"), + equalTo(DbIncubatingAttributes.DB_CONNECTION_STRING, "h2:mem:"), + satisfies(DbIncubatingAttributes.DB_STATEMENT, val -> val.isInstanceOf(String.class)), + satisfies(DbIncubatingAttributes.DB_OPERATION, val -> val.isInstanceOf(String.class)), + equalTo(DbIncubatingAttributes.DB_SQL_TABLE, "Value")); } + @SuppressWarnings("deprecation") // TODO DbIncubatingAttributes.DB_CONNECTION_STRING deprecation private static SpanDataAssert assertClientSpan( SpanDataAssert span, SpanData parent, String verb) { return span.hasName(verb.concat(" db1.Value")) .hasKind(SpanKind.CLIENT) .hasParent(parent) .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.DB_SYSTEM, "h2"), - equalTo(SemanticAttributes.DB_NAME, "db1"), - equalTo(SemanticAttributes.DB_USER, "sa"), - equalTo(SemanticAttributes.DB_CONNECTION_STRING, "h2:mem:"), + equalTo(DbIncubatingAttributes.DB_SYSTEM, "h2"), + equalTo(DbIncubatingAttributes.DB_NAME, "db1"), + equalTo(DbIncubatingAttributes.DB_USER, "sa"), + equalTo(DbIncubatingAttributes.DB_CONNECTION_STRING, "h2:mem:"), satisfies( - SemanticAttributes.DB_STATEMENT, + DbIncubatingAttributes.DB_STATEMENT, stringAssert -> stringAssert.startsWith(verb.toLowerCase(Locale.ROOT))), - equalTo(SemanticAttributes.DB_OPERATION, verb), - equalTo(SemanticAttributes.DB_SQL_TABLE, "Value")); + equalTo(DbIncubatingAttributes.DB_OPERATION, verb), + equalTo(DbIncubatingAttributes.DB_SQL_TABLE, "Value")); } } diff --git a/instrumentation/hibernate/hibernate-6.0/spring-testing/build.gradle.kts b/instrumentation/hibernate/hibernate-6.0/spring-testing/build.gradle.kts index 7b0c7f9cd272..b55825df9f03 100644 --- a/instrumentation/hibernate/hibernate-6.0/spring-testing/build.gradle.kts +++ b/instrumentation/hibernate/hibernate-6.0/spring-testing/build.gradle.kts @@ -18,6 +18,8 @@ dependencies { testImplementation("org.springframework.data:spring-data-jpa:3.0.0") springAgent("org.springframework:spring-instrument:6.0.7") + + latestDepTestLibrary("org.hibernate:hibernate-core:6.2.+") } otelJava { @@ -29,4 +31,6 @@ tasks.withType().configureEach { // TODO run tests both with and without experimental span attributes jvmArgs("-Dotel.instrumentation.hibernate.experimental-span-attributes=true") + + jvmArgs("-Dotel.instrumentation.common.experimental.controller-telemetry.enabled=true") } diff --git a/instrumentation/hibernate/hibernate-6.0/spring-testing/src/test/groovy/SpringJpaTest.groovy b/instrumentation/hibernate/hibernate-6.0/spring-testing/src/test/groovy/SpringJpaTest.groovy index 5797d279a716..6be2e3e5cdb7 100644 --- a/instrumentation/hibernate/hibernate-6.0/spring-testing/src/test/groovy/SpringJpaTest.groovy +++ b/instrumentation/hibernate/hibernate-6.0/spring-testing/src/test/groovy/SpringJpaTest.groovy @@ -4,7 +4,7 @@ */ import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import io.opentelemetry.semconv.incubating.DbIncubatingAttributes import org.hibernate.Version import org.springframework.context.annotation.AnnotationConfigApplicationContext import spock.lang.Shared @@ -23,6 +23,7 @@ class SpringJpaTest extends AgentInstrumentationSpecification { @Shared def repo = context.getBean(CustomerRepository) + @SuppressWarnings("deprecation") // TODO DbIncubatingAttributes.DB_CONNECTION_STRING deprecation def "test CRUD"() { setup: def isHibernate4 = Version.getVersionString().startsWith("4.") @@ -60,13 +61,13 @@ class SpringJpaTest extends AgentInstrumentationSpecification { kind CLIENT childOf span(1) attributes { - "$SemanticAttributes.DB_SYSTEM" "hsqldb" - "$SemanticAttributes.DB_NAME" "test" - "$SemanticAttributes.DB_USER" "sa" - "$SemanticAttributes.DB_CONNECTION_STRING" "hsqldb:mem:" - "$SemanticAttributes.DB_STATEMENT" ~/select ([^.]+)\.id([^,]*),([^.]+)\.firstName([^,]*),([^.]+)\.lastName(.*)from Customer(.*)/ - "$SemanticAttributes.DB_OPERATION" "SELECT" - "$SemanticAttributes.DB_SQL_TABLE" "Customer" + "$DbIncubatingAttributes.DB_SYSTEM" "hsqldb" + "$DbIncubatingAttributes.DB_NAME" "test" + "$DbIncubatingAttributes.DB_USER" "sa" + "$DbIncubatingAttributes.DB_CONNECTION_STRING" "hsqldb:mem:" + "$DbIncubatingAttributes.DB_STATEMENT" ~/select ([^.]+)\.id([^,]*),([^.]+)\.firstName([^,]*),([^.]+)\.lastName(.*)from Customer(.*)/ + "$DbIncubatingAttributes.DB_OPERATION" "SELECT" + "$DbIncubatingAttributes.DB_SQL_TABLE" "Customer" } } span(3) { @@ -116,12 +117,12 @@ class SpringJpaTest extends AgentInstrumentationSpecification { kind CLIENT childOf span(1) attributes { - "$SemanticAttributes.DB_SYSTEM" "hsqldb" - "$SemanticAttributes.DB_NAME" "test" - "$SemanticAttributes.DB_USER" "sa" - "$SemanticAttributes.DB_STATEMENT" "call next value for Customer_SEQ" - "$SemanticAttributes.DB_CONNECTION_STRING" "hsqldb:mem:" - "$SemanticAttributes.DB_OPERATION" "CALL" + "$DbIncubatingAttributes.DB_SYSTEM" "hsqldb" + "$DbIncubatingAttributes.DB_NAME" "test" + "$DbIncubatingAttributes.DB_USER" "sa" + "$DbIncubatingAttributes.DB_STATEMENT" "call next value for Customer_SEQ" + "$DbIncubatingAttributes.DB_CONNECTION_STRING" "hsqldb:mem:" + "$DbIncubatingAttributes.DB_OPERATION" "CALL" } } span(3) { @@ -137,13 +138,13 @@ class SpringJpaTest extends AgentInstrumentationSpecification { kind CLIENT childOf span(3) attributes { - "$SemanticAttributes.DB_SYSTEM" "hsqldb" - "$SemanticAttributes.DB_NAME" "test" - "$SemanticAttributes.DB_USER" "sa" - "$SemanticAttributes.DB_CONNECTION_STRING" "hsqldb:mem:" - "$SemanticAttributes.DB_STATEMENT" ~/insert into Customer \(.*\) values \(.*\)/ - "$SemanticAttributes.DB_OPERATION" "INSERT" - "$SemanticAttributes.DB_SQL_TABLE" "Customer" + "$DbIncubatingAttributes.DB_SYSTEM" "hsqldb" + "$DbIncubatingAttributes.DB_NAME" "test" + "$DbIncubatingAttributes.DB_USER" "sa" + "$DbIncubatingAttributes.DB_CONNECTION_STRING" "hsqldb:mem:" + "$DbIncubatingAttributes.DB_STATEMENT" ~/insert into Customer \(.*\) values \(.*\)/ + "$DbIncubatingAttributes.DB_OPERATION" "INSERT" + "$DbIncubatingAttributes.DB_SQL_TABLE" "Customer" } } } else { @@ -152,13 +153,13 @@ class SpringJpaTest extends AgentInstrumentationSpecification { kind CLIENT childOf span(1) attributes { - "$SemanticAttributes.DB_SYSTEM" "hsqldb" - "$SemanticAttributes.DB_NAME" "test" - "$SemanticAttributes.DB_USER" "sa" - "$SemanticAttributes.DB_CONNECTION_STRING" "hsqldb:mem:" - "$SemanticAttributes.DB_STATEMENT" ~/insert into Customer \(.*\) values \(.*\)/ - "$SemanticAttributes.DB_OPERATION" "INSERT" - "$SemanticAttributes.DB_SQL_TABLE" "Customer" + "$DbIncubatingAttributes.DB_SYSTEM" "hsqldb" + "$DbIncubatingAttributes.DB_NAME" "test" + "$DbIncubatingAttributes.DB_USER" "sa" + "$DbIncubatingAttributes.DB_CONNECTION_STRING" "hsqldb:mem:" + "$DbIncubatingAttributes.DB_STATEMENT" ~/insert into Customer \(.*\) values \(.*\)/ + "$DbIncubatingAttributes.DB_OPERATION" "INSERT" + "$DbIncubatingAttributes.DB_SQL_TABLE" "Customer" } } span(3) { @@ -207,13 +208,13 @@ class SpringJpaTest extends AgentInstrumentationSpecification { name "SELECT test.Customer" kind CLIENT attributes { - "$SemanticAttributes.DB_SYSTEM" "hsqldb" - "$SemanticAttributes.DB_NAME" "test" - "$SemanticAttributes.DB_USER" "sa" - "$SemanticAttributes.DB_CONNECTION_STRING" "hsqldb:mem:" - "$SemanticAttributes.DB_STATEMENT" ~/select ([^.]+)\.id([^,]*),([^.]+)\.firstName([^,]*),([^.]+)\.lastName (.*)from Customer (.*)where ([^.]+)\.id( ?)=( ?)\?/ - "$SemanticAttributes.DB_OPERATION" "SELECT" - "$SemanticAttributes.DB_SQL_TABLE" "Customer" + "$DbIncubatingAttributes.DB_SYSTEM" "hsqldb" + "$DbIncubatingAttributes.DB_NAME" "test" + "$DbIncubatingAttributes.DB_USER" "sa" + "$DbIncubatingAttributes.DB_CONNECTION_STRING" "hsqldb:mem:" + "$DbIncubatingAttributes.DB_STATEMENT" ~/select ([^.]+)\.id([^,]*),([^.]+)\.firstName([^,]*),([^.]+)\.lastName (.*)from Customer (.*)where ([^.]+)\.id( ?)=( ?)\?/ + "$DbIncubatingAttributes.DB_OPERATION" "SELECT" + "$DbIncubatingAttributes.DB_SQL_TABLE" "Customer" } } span(3) { @@ -228,13 +229,13 @@ class SpringJpaTest extends AgentInstrumentationSpecification { name "UPDATE test.Customer" kind CLIENT attributes { - "$SemanticAttributes.DB_SYSTEM" "hsqldb" - "$SemanticAttributes.DB_NAME" "test" - "$SemanticAttributes.DB_USER" "sa" - "$SemanticAttributes.DB_CONNECTION_STRING" "hsqldb:mem:" - "$SemanticAttributes.DB_STATEMENT" ~/update Customer set firstName=\?,(.*)lastName=\? where id=\?/ - "$SemanticAttributes.DB_OPERATION" "UPDATE" - "$SemanticAttributes.DB_SQL_TABLE" "Customer" + "$DbIncubatingAttributes.DB_SYSTEM" "hsqldb" + "$DbIncubatingAttributes.DB_NAME" "test" + "$DbIncubatingAttributes.DB_USER" "sa" + "$DbIncubatingAttributes.DB_CONNECTION_STRING" "hsqldb:mem:" + "$DbIncubatingAttributes.DB_STATEMENT" ~/update Customer set firstName=\?,(.*)lastName=\? where id=\?/ + "$DbIncubatingAttributes.DB_OPERATION" "UPDATE" + "$DbIncubatingAttributes.DB_SQL_TABLE" "Customer" } } } @@ -271,13 +272,13 @@ class SpringJpaTest extends AgentInstrumentationSpecification { kind CLIENT childOf span(1) attributes { - "$SemanticAttributes.DB_SYSTEM" "hsqldb" - "$SemanticAttributes.DB_NAME" "test" - "$SemanticAttributes.DB_USER" "sa" - "$SemanticAttributes.DB_CONNECTION_STRING" "hsqldb:mem:" - "$SemanticAttributes.DB_STATEMENT" ~/select ([^.]+)\.id([^,]*),([^.]+)\.firstName([^,]*),([^.]+)\.lastName (.*)from Customer (.*)(where ([^.]+)\.lastName( ?)=( ?)\?|)/ - "$SemanticAttributes.DB_OPERATION" "SELECT" - "$SemanticAttributes.DB_SQL_TABLE" "Customer" + "$DbIncubatingAttributes.DB_SYSTEM" "hsqldb" + "$DbIncubatingAttributes.DB_NAME" "test" + "$DbIncubatingAttributes.DB_USER" "sa" + "$DbIncubatingAttributes.DB_CONNECTION_STRING" "hsqldb:mem:" + "$DbIncubatingAttributes.DB_STATEMENT" ~/select ([^.]+)\.id([^,]*),([^.]+)\.firstName([^,]*),([^.]+)\.lastName (.*)from Customer (.*)(where ([^.]+)\.lastName( ?)=( ?)\?|)/ + "$DbIncubatingAttributes.DB_OPERATION" "SELECT" + "$DbIncubatingAttributes.DB_SQL_TABLE" "Customer" } } } @@ -315,13 +316,13 @@ class SpringJpaTest extends AgentInstrumentationSpecification { kind CLIENT childOf span(1) attributes { - "$SemanticAttributes.DB_SYSTEM" "hsqldb" - "$SemanticAttributes.DB_NAME" "test" - "$SemanticAttributes.DB_USER" "sa" - "$SemanticAttributes.DB_CONNECTION_STRING" "hsqldb:mem:" - "$SemanticAttributes.DB_STATEMENT" ~/select ([^.]+)\.id([^,]*),([^.]+)\.firstName([^,]*),([^.]+)\.lastName (.*)from Customer (.*)where ([^.]+)\.id( ?)=( ?)\?/ - "$SemanticAttributes.DB_OPERATION" "SELECT" - "$SemanticAttributes.DB_SQL_TABLE" "Customer" + "$DbIncubatingAttributes.DB_SYSTEM" "hsqldb" + "$DbIncubatingAttributes.DB_NAME" "test" + "$DbIncubatingAttributes.DB_USER" "sa" + "$DbIncubatingAttributes.DB_CONNECTION_STRING" "hsqldb:mem:" + "$DbIncubatingAttributes.DB_STATEMENT" ~/select ([^.]+)\.id([^,]*),([^.]+)\.firstName([^,]*),([^.]+)\.lastName (.*)from Customer (.*)where ([^.]+)\.id( ?)=( ?)\?/ + "$DbIncubatingAttributes.DB_OPERATION" "SELECT" + "$DbIncubatingAttributes.DB_SQL_TABLE" "Customer" } } } @@ -340,13 +341,13 @@ class SpringJpaTest extends AgentInstrumentationSpecification { kind CLIENT childOf span(1) attributes { - "$SemanticAttributes.DB_SYSTEM" "hsqldb" - "$SemanticAttributes.DB_NAME" "test" - "$SemanticAttributes.DB_USER" "sa" - "$SemanticAttributes.DB_CONNECTION_STRING" "hsqldb:mem:" - "$SemanticAttributes.DB_STATEMENT" ~/select ([^.]+)\.id([^,]*),([^.]+)\.firstName([^,]*),([^.]+)\.lastName (.*)from Customer (.*)where ([^.]+)\.id( ?)=( ?)\?/ - "$SemanticAttributes.DB_OPERATION" "SELECT" - "$SemanticAttributes.DB_SQL_TABLE" "Customer" + "$DbIncubatingAttributes.DB_SYSTEM" "hsqldb" + "$DbIncubatingAttributes.DB_NAME" "test" + "$DbIncubatingAttributes.DB_USER" "sa" + "$DbIncubatingAttributes.DB_CONNECTION_STRING" "hsqldb:mem:" + "$DbIncubatingAttributes.DB_STATEMENT" ~/select ([^.]+)\.id([^,]*),([^.]+)\.firstName([^,]*),([^.]+)\.lastName (.*)from Customer (.*)where ([^.]+)\.id( ?)=( ?)\?/ + "$DbIncubatingAttributes.DB_OPERATION" "SELECT" + "$DbIncubatingAttributes.DB_SQL_TABLE" "Customer" } } } @@ -370,13 +371,13 @@ class SpringJpaTest extends AgentInstrumentationSpecification { name "DELETE test.Customer" kind CLIENT attributes { - "$SemanticAttributes.DB_SYSTEM" "hsqldb" - "$SemanticAttributes.DB_NAME" "test" - "$SemanticAttributes.DB_USER" "sa" - "$SemanticAttributes.DB_CONNECTION_STRING" "hsqldb:mem:" - "$SemanticAttributes.DB_STATEMENT" "delete from Customer where id=?" - "$SemanticAttributes.DB_OPERATION" "DELETE" - "$SemanticAttributes.DB_SQL_TABLE" "Customer" + "$DbIncubatingAttributes.DB_SYSTEM" "hsqldb" + "$DbIncubatingAttributes.DB_NAME" "test" + "$DbIncubatingAttributes.DB_USER" "sa" + "$DbIncubatingAttributes.DB_CONNECTION_STRING" "hsqldb:mem:" + "$DbIncubatingAttributes.DB_STATEMENT" "delete from Customer where id=?" + "$DbIncubatingAttributes.DB_OPERATION" "DELETE" + "$DbIncubatingAttributes.DB_SQL_TABLE" "Customer" } } } diff --git a/instrumentation/hibernate/hibernate-6.0/spring-testing/src/test/java/spring/jpa/Customer.java b/instrumentation/hibernate/hibernate-6.0/spring-testing/src/test/java/spring/jpa/Customer.java index c98d8046d583..966098c7d03e 100644 --- a/instrumentation/hibernate/hibernate-6.0/spring-testing/src/test/java/spring/jpa/Customer.java +++ b/instrumentation/hibernate/hibernate-6.0/spring-testing/src/test/java/spring/jpa/Customer.java @@ -9,6 +9,7 @@ import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; +import java.util.Locale; import java.util.Objects; import javax.annotation.Nullable; @@ -55,7 +56,8 @@ public void setLastName(String lastName) { @Override public String toString() { - return String.format("Customer[id=%d, firstName='%s', lastName='%s']", id, firstName, lastName); + return String.format( + Locale.ROOT, "Customer[id=%d, firstName='%s', lastName='%s']", id, firstName, lastName); } @Override diff --git a/instrumentation/hibernate/hibernate-common/javaagent/build.gradle.kts b/instrumentation/hibernate/hibernate-common/javaagent/build.gradle.kts index e7a0b6c719f2..4acd90922314 100644 --- a/instrumentation/hibernate/hibernate-common/javaagent/build.gradle.kts +++ b/instrumentation/hibernate/hibernate-common/javaagent/build.gradle.kts @@ -4,7 +4,3 @@ plugins { id("otel.javaagent-instrumentation") } - -dependencies { - compileOnly(project(":javaagent-extension-api")) -} diff --git a/instrumentation/hibernate/hibernate-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/HibernateInstrumenterFactory.java b/instrumentation/hibernate/hibernate-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/HibernateInstrumenterFactory.java index b9aa1c4c4d2d..217454ef1c8f 100644 --- a/instrumentation/hibernate/hibernate-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/HibernateInstrumenterFactory.java +++ b/instrumentation/hibernate/hibernate-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/HibernateInstrumenterFactory.java @@ -8,11 +8,11 @@ import io.opentelemetry.api.GlobalOpenTelemetry; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; import io.opentelemetry.instrumentation.api.instrumenter.InstrumenterBuilder; -import io.opentelemetry.javaagent.bootstrap.internal.InstrumentationConfig; +import io.opentelemetry.javaagent.bootstrap.internal.AgentInstrumentationConfig; public final class HibernateInstrumenterFactory { static final boolean CAPTURE_EXPERIMENTAL_SPAN_ATTRIBUTES = - InstrumentationConfig.get() + AgentInstrumentationConfig.get() .getBoolean("otel.instrumentation.hibernate.experimental-span-attributes", false); public static Instrumenter createInstrumenter( diff --git a/instrumentation/hibernate/hibernate-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/HibernateOperationScope.java b/instrumentation/hibernate/hibernate-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/HibernateOperationScope.java new file mode 100644 index 000000000000..6c8831718da1 --- /dev/null +++ b/instrumentation/hibernate/hibernate-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/HibernateOperationScope.java @@ -0,0 +1,88 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.hibernate; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import io.opentelemetry.javaagent.bootstrap.CallDepth; + +public class HibernateOperationScope { + + private final HibernateOperation hibernateOperation; + private final Context context; + private final Scope scope; + private final Instrumenter instrumenter; + + private HibernateOperationScope( + HibernateOperation hibernateOperation, + Context context, + Scope scope, + Instrumenter instrumenter) { + this.hibernateOperation = hibernateOperation; + this.context = context; + this.scope = scope; + this.instrumenter = instrumenter; + } + + /** + * Starts operation scope + * + * @param hibernateOperation hibernate operation + * @param parentContext parent context + * @param instrumenter instrumenter + * @return operation scope, to be ended with {@link #end(HibernateOperationScope, Throwable)} on + * exit advice. Might return {@literal null} when operation should not be captured. + */ + public static HibernateOperationScope start( + HibernateOperation hibernateOperation, + Context parentContext, + Instrumenter instrumenter) { + + if (!instrumenter.shouldStart(parentContext, hibernateOperation)) { + return null; + } + + Context context = instrumenter.start(parentContext, hibernateOperation); + + return new HibernateOperationScope( + hibernateOperation, context, context.makeCurrent(), instrumenter); + } + + /** + * Performs call depth increase and returns {@literal true} when depth is > 0, which indicates a + * nested hibernate operation is in progress. Must be called in the enter advice with an + * unconditional corresponding call to {@link #end(HibernateOperationScope, Throwable)} to + * decrement call depth. + */ + public static boolean enterDepthSkipCheck() { + CallDepth callDepth = CallDepth.forClass(HibernateOperation.class); + return callDepth.getAndIncrement() > 0; + } + + /** + * Ends operation scope. + * + * @param scope hibernate operation scope or {@literal null} when there is none + * @param throwable thrown exception + */ + public static void end(HibernateOperationScope scope, Throwable throwable) { + + CallDepth callDepth = CallDepth.forClass(HibernateOperation.class); + if (callDepth.decrementAndGet() > 0) { + return; + } + + if (scope != null) { + scope.end(throwable); + } + } + + private void end(Throwable throwable) { + scope.close(); + instrumenter.end(context, hibernateOperation, null, throwable); + } +} diff --git a/instrumentation/hibernate/hibernate-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/OperationNameUtil.java b/instrumentation/hibernate/hibernate-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/OperationNameUtil.java index bb7134fcf5cd..60783d0822d3 100644 --- a/instrumentation/hibernate/hibernate-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/OperationNameUtil.java +++ b/instrumentation/hibernate/hibernate-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/OperationNameUtil.java @@ -5,15 +5,15 @@ package io.opentelemetry.javaagent.instrumentation.hibernate; -import io.opentelemetry.instrumentation.api.db.SqlStatementInfo; -import io.opentelemetry.instrumentation.api.db.SqlStatementSanitizer; -import io.opentelemetry.javaagent.bootstrap.internal.CommonConfig; +import io.opentelemetry.instrumentation.api.incubator.semconv.db.SqlStatementInfo; +import io.opentelemetry.instrumentation.api.incubator.semconv.db.SqlStatementSanitizer; +import io.opentelemetry.javaagent.bootstrap.internal.AgentCommonConfig; import java.util.function.Function; public final class OperationNameUtil { private static final SqlStatementSanitizer sanitizer = - SqlStatementSanitizer.create(CommonConfig.get().isStatementSanitizationEnabled()); + SqlStatementSanitizer.create(AgentCommonConfig.get().isStatementSanitizationEnabled()); public static String getOperationNameForQuery(String query) { // set operation to default value that is used when sql sanitizer fails to extract diff --git a/instrumentation/hibernate/hibernate-procedure-call-4.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/v4_3/HibernateInstrumentationModule.java b/instrumentation/hibernate/hibernate-procedure-call-4.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/v4_3/HibernateInstrumentationModule.java index 456a701aed66..3e914808d004 100644 --- a/instrumentation/hibernate/hibernate-procedure-call-4.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/v4_3/HibernateInstrumentationModule.java +++ b/instrumentation/hibernate/hibernate-procedure-call-4.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/v4_3/HibernateInstrumentationModule.java @@ -11,11 +11,13 @@ 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.internal.ExperimentalInstrumentationModule; import java.util.List; import net.bytebuddy.matcher.ElementMatcher; @AutoService(InstrumentationModule.class) -public class HibernateInstrumentationModule extends InstrumentationModule { +public class HibernateInstrumentationModule extends InstrumentationModule + implements ExperimentalInstrumentationModule { public HibernateInstrumentationModule() { super("hibernate-procedure-call", "hibernate-procedure-call-4.3", "hibernate"); } @@ -25,6 +27,11 @@ public ElementMatcher.Junction classLoaderMatcher() { return hasClassesNamed("org.hibernate.procedure.ProcedureCall"); } + @Override + public String getModuleGroup() { + return "hibernate"; + } + @Override public List typeInstrumentations() { return asList(new ProcedureCallInstrumentation(), new SessionInstrumentation()); diff --git a/instrumentation/hibernate/hibernate-procedure-call-4.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/v4_3/ProcedureCallInstrumentation.java b/instrumentation/hibernate/hibernate-procedure-call-4.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/v4_3/ProcedureCallInstrumentation.java index ed9671808c2f..db6092567598 100644 --- a/instrumentation/hibernate/hibernate-procedure-call-4.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/v4_3/ProcedureCallInstrumentation.java +++ b/instrumentation/hibernate/hibernate-procedure-call-4.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/v4_3/ProcedureCallInstrumentation.java @@ -12,13 +12,12 @@ import static net.bytebuddy.matcher.ElementMatchers.named; import io.opentelemetry.context.Context; -import io.opentelemetry.context.Scope; import io.opentelemetry.instrumentation.api.util.VirtualField; -import io.opentelemetry.javaagent.bootstrap.CallDepth; import io.opentelemetry.javaagent.bootstrap.Java8BytecodeBridge; import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; import io.opentelemetry.javaagent.instrumentation.hibernate.HibernateOperation; +import io.opentelemetry.javaagent.instrumentation.hibernate.HibernateOperationScope; import io.opentelemetry.javaagent.instrumentation.hibernate.SessionInfo; import net.bytebuddy.asm.Advice; import net.bytebuddy.description.type.TypeDescription; @@ -48,17 +47,11 @@ public void transform(TypeTransformer transformer) { public static class ProcedureCallMethodAdvice { @Advice.OnMethodEnter(suppress = Throwable.class) - public static void startMethod( - @Advice.This ProcedureCall call, - @Advice.Origin("#m") String name, - @Advice.Local("otelCallDepth") CallDepth callDepth, - @Advice.Local("otelHibernateOperation") HibernateOperation hibernateOperation, - @Advice.Local("otelContext") Context context, - @Advice.Local("otelScope") Scope scope) { + public static HibernateOperationScope startMethod( + @Advice.This ProcedureCall call, @Advice.Origin("#m") String name) { - callDepth = CallDepth.forClass(HibernateOperation.class); - if (callDepth.getAndIncrement() > 0) { - return; + if (HibernateOperationScope.enterDepthSkipCheck()) { + return null; } VirtualField criteriaVirtualField = @@ -66,32 +59,17 @@ public static void startMethod( SessionInfo sessionInfo = criteriaVirtualField.get(call); Context parentContext = Java8BytecodeBridge.currentContext(); - hibernateOperation = + HibernateOperation hibernateOperation = new HibernateOperation("ProcedureCall." + name, call.getProcedureName(), sessionInfo); - if (!instrumenter().shouldStart(parentContext, hibernateOperation)) { - return; - } - context = instrumenter().start(parentContext, hibernateOperation); - scope = context.makeCurrent(); + return HibernateOperationScope.start(hibernateOperation, parentContext, instrumenter()); } @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) public static void endMethod( - @Advice.Thrown Throwable throwable, - @Advice.Local("otelCallDepth") CallDepth callDepth, - @Advice.Local("otelHibernateOperation") HibernateOperation hibernateOperation, - @Advice.Local("otelContext") Context context, - @Advice.Local("otelScope") Scope scope) { - - if (callDepth.decrementAndGet() > 0) { - return; - } + @Advice.Thrown Throwable throwable, @Advice.Enter HibernateOperationScope scope) { - if (scope != null) { - scope.close(); - instrumenter().end(context, hibernateOperation, null, throwable); - } + HibernateOperationScope.end(scope, throwable); } } } diff --git a/instrumentation/hibernate/hibernate-procedure-call-4.3/javaagent/src/test/groovy/ProcedureCallTest.groovy b/instrumentation/hibernate/hibernate-procedure-call-4.3/javaagent/src/test/groovy/ProcedureCallTest.groovy deleted file mode 100644 index 86345ba2f8b3..000000000000 --- a/instrumentation/hibernate/hibernate-procedure-call-4.3/javaagent/src/test/groovy/ProcedureCallTest.groovy +++ /dev/null @@ -1,175 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes -import org.hibernate.Session -import org.hibernate.SessionFactory -import org.hibernate.cfg.Configuration -import org.hibernate.exception.SQLGrammarException -import org.hibernate.procedure.ProcedureCall -import spock.lang.Shared - -import javax.persistence.ParameterMode -import java.sql.Connection -import java.sql.DriverManager -import java.sql.Statement - -import static io.opentelemetry.api.trace.SpanKind.CLIENT -import static io.opentelemetry.api.trace.SpanKind.INTERNAL -import static io.opentelemetry.api.trace.StatusCode.ERROR - -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 "test ProcedureCall"() { - setup: - - runWithSpan("parent") { - Session session = sessionFactory.openSession() - session.beginTransaction() - - ProcedureCall call = session.createStoredProcedureCall("TEST_PROC") - call.getOutputs() - - session.getTransaction().commit() - session.close() - } - - expect: - def sessionId - assertTraces(1) { - trace(0, 4) { - span(0) { - name "parent" - kind INTERNAL - hasNoParent() - attributes { - } - } - span(1) { - name "ProcedureCall.getOutputs TEST_PROC" - kind INTERNAL - childOf span(0) - attributes { - "hibernate.session_id" { - sessionId = it - it instanceof String - } - } - } - span(2) { - name "CALL test.TEST_PROC" - kind CLIENT - childOf span(1) - attributes { - "$SemanticAttributes.DB_SYSTEM" "hsqldb" - "$SemanticAttributes.DB_NAME" "test" - "$SemanticAttributes.DB_USER" "sa" - "$SemanticAttributes.DB_STATEMENT" "{call TEST_PROC()}" - "$SemanticAttributes.DB_CONNECTION_STRING" "hsqldb:mem:" - "$SemanticAttributes.DB_OPERATION" "CALL" - } - } - span(3) { - kind INTERNAL - name "Transaction.commit" - childOf span(0) - attributes { - "hibernate.session_id" sessionId - } - } - } - } - } - - def "test failing ProcedureCall"() { - setup: - - runWithSpan("parent") { - Session session = sessionFactory.openSession() - session.beginTransaction() - - ProcedureCall call = session.createStoredProcedureCall("TEST_PROC") - def parameterRegistration = call.registerParameter("nonexistent", Long, ParameterMode.IN) - parameterRegistration.bindValue(420L) - try { - call.getOutputs() - } catch (Exception e) { - // We expected this. - } - - session.getTransaction().commit() - session.close() - } - - expect: - def sessionId - assertTraces(1) { - trace(0, 3) { - span(0) { - name "parent" - kind INTERNAL - hasNoParent() - attributes { - } - } - span(1) { - name "ProcedureCall.getOutputs TEST_PROC" - kind INTERNAL - childOf span(0) - status ERROR - errorEvent(SQLGrammarException, "could not prepare statement") - attributes { - "hibernate.session_id" { - sessionId = it - it instanceof String - } - } - } - span(2) { - name "Transaction.commit" - kind INTERNAL - childOf span(0) - attributes { - "hibernate.session_id" sessionId - } - } - } - } - } -} diff --git a/instrumentation/hibernate/hibernate-procedure-call-4.3/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/hibernate/v4_3/ProcedureCallTest.java b/instrumentation/hibernate/hibernate-procedure-call-4.3/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/hibernate/v4_3/ProcedureCallTest.java new file mode 100644 index 000000000000..3e36101c7bef --- /dev/null +++ b/instrumentation/hibernate/hibernate-procedure-call-4.3/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/hibernate/v4_3/ProcedureCallTest.java @@ -0,0 +1,190 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.hibernate.v4_3; + +import static io.opentelemetry.api.common.AttributeKey.stringKey; +import static io.opentelemetry.api.trace.SpanKind.CLIENT; +import static io.opentelemetry.api.trace.SpanKind.INTERNAL; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.satisfies; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.catchThrowable; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.sdk.trace.data.StatusData; +import io.opentelemetry.semconv.incubating.DbIncubatingAttributes; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.ArrayList; +import java.util.List; +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.ParameterRegistration; +import org.hibernate.procedure.ProcedureCall; +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; + +class ProcedureCallTest { + + @RegisterExtension + protected static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + protected static SessionFactory sessionFactory; + protected static List prepopulated; + + @BeforeAll + @SuppressWarnings("deprecation") // buildSessionFactory + static void setUp() throws SQLException { + 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(); + } + + @AfterAll + static void cleanUp() { + if (sessionFactory != null) { + sessionFactory.close(); + } + } + + @SuppressWarnings("deprecation") // TODO DbIncubatingAttributes.DB_CONNECTION_STRING deprecation + @Test + void testProcedureCall() { + + testing.runWithSpan( + "parent", + () -> { + Session session = sessionFactory.openSession(); + session.beginTransaction(); + + ProcedureCall call = session.createStoredProcedureCall("TEST_PROC"); + call.getOutputs(); + + session.getTransaction().commit(); + session.close(); + }); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("parent") + .hasKind(INTERNAL) + .hasNoParent() + .hasAttributes(Attributes.empty()), + span -> + span.hasName("ProcedureCall.getOutputs TEST_PROC") + .hasKind(INTERNAL) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + satisfies( + stringKey("hibernate.session_id"), + val -> val.isInstanceOf(String.class))), + span -> + span.hasName("CALL test.TEST_PROC") + .hasKind(CLIENT) + .hasParent(trace.getSpan(1)) + .hasAttributesSatisfyingExactly( + equalTo(DbIncubatingAttributes.DB_SYSTEM, "hsqldb"), + equalTo(DbIncubatingAttributes.DB_NAME, "test"), + equalTo(DbIncubatingAttributes.DB_USER, "sa"), + equalTo(DbIncubatingAttributes.DB_STATEMENT, "{call TEST_PROC()}"), + equalTo(DbIncubatingAttributes.DB_CONNECTION_STRING, "hsqldb:mem:"), + equalTo(DbIncubatingAttributes.DB_OPERATION, "CALL")), + span -> + span.hasName("Transaction.commit") + .hasKind(INTERNAL) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo( + stringKey("hibernate.session_id"), + trace + .getSpan(1) + .getAttributes() + .get(stringKey("hibernate.session_id")))))); + } + + @Test + void testFailingProcedureCall() { + + Throwable exception = + testing.runWithSpan( + "parent", + () -> { + Session session = sessionFactory.openSession(); + session.beginTransaction(); + + ProcedureCall call = session.createStoredProcedureCall("TEST_PROC"); + ParameterRegistration parameterRegistration = + call.registerParameter("nonexistent", Long.class, ParameterMode.IN); + parameterRegistration.bindValue(420L); + + Throwable thrown = catchThrowable(call::getOutputs); + + assertThat(thrown).isInstanceOf(SQLGrammarException.class); + + session.getTransaction().commit(); + session.close(); + return thrown; + }); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("parent") + .hasKind(INTERNAL) + .hasNoParent() + .hasAttributes(Attributes.empty()), + span -> + span.hasName("ProcedureCall.getOutputs TEST_PROC") + .hasKind(INTERNAL) + .hasParent(trace.getSpan(0)) + .hasException(exception) + .hasStatus(StatusData.error()) + .hasAttributesSatisfyingExactly( + satisfies( + stringKey("hibernate.session_id"), + val -> val.isInstanceOf(String.class))), + span -> + span.hasName("Transaction.commit") + .hasKind(INTERNAL) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo( + stringKey("hibernate.session_id"), + trace + .getSpan(1) + .getAttributes() + .get(stringKey("hibernate.session_id")))))); + } +} diff --git a/instrumentation/hibernate/hibernate-procedure-call-4.3/javaagent/src/test/java/Value.java b/instrumentation/hibernate/hibernate-procedure-call-4.3/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/hibernate/v4_3/Value.java similarity index 92% rename from instrumentation/hibernate/hibernate-procedure-call-4.3/javaagent/src/test/java/Value.java rename to instrumentation/hibernate/hibernate-procedure-call-4.3/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/hibernate/v4_3/Value.java index a9de87173ddd..1293b1344804 100644 --- a/instrumentation/hibernate/hibernate-procedure-call-4.3/javaagent/src/test/java/Value.java +++ b/instrumentation/hibernate/hibernate-procedure-call-4.3/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/hibernate/v4_3/Value.java @@ -3,6 +3,8 @@ * SPDX-License-Identifier: Apache-2.0 */ +package io.opentelemetry.javaagent.instrumentation.hibernate.v4_3; + import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.Id; diff --git a/instrumentation/hibernate/hibernate-procedure-call-4.3/javaagent/src/test/resources/hibernate.cfg.xml b/instrumentation/hibernate/hibernate-procedure-call-4.3/javaagent/src/test/resources/hibernate.cfg.xml index bbc3827dda68..f7df4c3b00c1 100644 --- a/instrumentation/hibernate/hibernate-procedure-call-4.3/javaagent/src/test/resources/hibernate.cfg.xml +++ b/instrumentation/hibernate/hibernate-procedure-call-4.3/javaagent/src/test/resources/hibernate.cfg.xml @@ -17,7 +17,7 @@ create - + diff --git a/instrumentation/hibernate/hibernate-reactive-1.0/javaagent/build.gradle.kts b/instrumentation/hibernate/hibernate-reactive-1.0/javaagent/build.gradle.kts new file mode 100644 index 000000000000..fbe70f17463c --- /dev/null +++ b/instrumentation/hibernate/hibernate-reactive-1.0/javaagent/build.gradle.kts @@ -0,0 +1,92 @@ +plugins { + id("otel.javaagent-instrumentation") +} + +muzzle { + pass { + group.set("org.hibernate.reactive") + module.set("hibernate-reactive-core") + versions.set("(,)") + assertInverse.set(true) + } +} + +dependencies { + compileOnly("org.hibernate.reactive:hibernate-reactive-core:1.0.0.Final") + + testInstrumentation(project(":instrumentation:netty:netty-4.1:javaagent")) + testInstrumentation(project(":instrumentation:vertx:vertx-sql-client-4.0:javaagent")) + + library("io.vertx:vertx-sql-client:4.4.2") + compileOnly("io.vertx:vertx-codegen:4.4.2") + + testLibrary("io.vertx:vertx-pg-client:4.4.2") + testLibrary("io.vertx:vertx-codegen:4.4.2") +} + +val latestDepTest = findProperty("testLatestDeps") as Boolean + +testing { + suites { + val hibernateReactive1Test by registering(JvmTestSuite::class) { + dependencies { + implementation("org.testcontainers:testcontainers") + if (latestDepTest) { + implementation("org.hibernate.reactive:hibernate-reactive-core:1.+") + implementation("io.vertx:vertx-pg-client:+") + } else { + implementation("org.hibernate.reactive:hibernate-reactive-core:1.0.0.Final") + implementation("io.vertx:vertx-pg-client:4.1.5") + } + compileOnly("io.vertx:vertx-codegen:4.1.5") + } + } + + val hibernateReactive2Test by registering(JvmTestSuite::class) { + dependencies { + implementation("org.testcontainers:testcontainers") + if (latestDepTest) { + implementation("org.hibernate.reactive:hibernate-reactive-core:2.+") + implementation("io.vertx:vertx-pg-client:+") + } else { + implementation("org.hibernate.reactive:hibernate-reactive-core:2.0.0.Final") + implementation("io.vertx:vertx-pg-client:4.4.2") + } + compileOnly("io.vertx:vertx-codegen:4.4.2") + } + } + } +} + +tasks { + withType().configureEach { + usesService(gradle.sharedServices.registrations["testcontainersBuildService"].service) + } + named("compileHibernateReactive2TestJava", JavaCompile::class).configure { + options.release.set(11) + } + val testJavaVersion = + gradle.startParameter.projectProperties.get("testJavaVersion")?.let(JavaVersion::toVersion) + ?: JavaVersion.current() + if (testJavaVersion.isJava8) { + named("hibernateReactive2Test", Test::class).configure { + enabled = false + } + if (latestDepTest) { + named("hibernateReactive1Test", Test::class).configure { + enabled = false + } + } + } + + check { + dependsOn(testing.suites) + } +} + +if (!latestDepTest) { + // https://bugs.openjdk.org/browse/JDK-8320431 + otelJava { + maxJavaVersionForTests.set(JavaVersion.VERSION_21) + } +} diff --git a/instrumentation/hibernate/hibernate-reactive-1.0/javaagent/src/hibernateReactive1Test/java/io/opentelemetry/javaagent/instrumentation/hibernate/reactive/v1_0/HibernateReactiveTest.java b/instrumentation/hibernate/hibernate-reactive-1.0/javaagent/src/hibernateReactive1Test/java/io/opentelemetry/javaagent/instrumentation/hibernate/reactive/v1_0/HibernateReactiveTest.java new file mode 100644 index 000000000000..c2152b4c8dc0 --- /dev/null +++ b/instrumentation/hibernate/hibernate-reactive-1.0/javaagent/src/hibernateReactive1Test/java/io/opentelemetry/javaagent/instrumentation/hibernate/reactive/v1_0/HibernateReactiveTest.java @@ -0,0 +1,321 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.hibernate.reactive.v1_0; + +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; +import static io.opentelemetry.semconv.ServerAttributes.SERVER_ADDRESS; +import static io.opentelemetry.semconv.ServerAttributes.SERVER_PORT; +import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DB_NAME; +import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DB_OPERATION; +import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DB_SQL_TABLE; +import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DB_STATEMENT; +import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DB_USER; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.vertx.core.Vertx; +import java.time.Duration; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import javax.persistence.EntityManagerFactory; +import javax.persistence.Persistence; +import org.hibernate.reactive.mutiny.Mutiny; +import org.hibernate.reactive.stage.Stage; +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; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.output.Slf4jLogConsumer; + +class HibernateReactiveTest { + private static final Logger logger = LoggerFactory.getLogger(HibernateReactiveTest.class); + + private static final String USER_DB = "SA"; + private static final String PW_DB = "password123"; + private static final String DB = "tempdb"; + + @RegisterExtension + protected static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + private static final Vertx vertx = Vertx.vertx(); + private static GenericContainer container; + private static String host; + private static int port; + private static EntityManagerFactory entityManagerFactory; + private static Mutiny.SessionFactory mutinySessionFactory; + private static Stage.SessionFactory stageSessionFactory; + + @BeforeAll + static void setUp() throws Exception { + container = + new GenericContainer<>("postgres:9.6.8") + .withEnv("POSTGRES_USER", USER_DB) + .withEnv("POSTGRES_PASSWORD", PW_DB) + .withEnv("POSTGRES_DB", DB) + .withExposedPorts(5432) + .withLogConsumer(new Slf4jLogConsumer(logger)) + .withStartupTimeout(Duration.ofMinutes(2)); + container.start(); + + host = container.getHost(); + port = container.getMappedPort(5432); + System.setProperty("db.host", host); + System.setProperty("db.port", String.valueOf(port)); + + entityManagerFactory = + vertx + .getOrCreateContext() + .executeBlocking( + promise -> promise.complete(Persistence.createEntityManagerFactory("test-pu"))) + .toCompletionStage() + .toCompletableFuture() + .get(30, TimeUnit.SECONDS); + + Value value = new Value("name"); + value.setId(1L); + + mutinySessionFactory = entityManagerFactory.unwrap(Mutiny.SessionFactory.class); + stageSessionFactory = entityManagerFactory.unwrap(Stage.SessionFactory.class); + + mutinySessionFactory + .withTransaction((session, tx) -> session.merge(value)) + .await() + .atMost(Duration.ofSeconds(30)); + } + + @AfterAll + static void cleanUp() { + if (entityManagerFactory != null) { + entityManagerFactory.close(); + } + if (mutinySessionFactory != null) { + mutinySessionFactory.close(); + } + if (stageSessionFactory != null) { + stageSessionFactory.close(); + } + vertx.close(); + container.stop(); + } + + @Test + void testMutiny() { + testing.runWithSpan( + "parent", + () -> { + mutinySessionFactory + .withSession( + session -> { + if (!Span.current().getSpanContext().isValid()) { + throw new IllegalStateException("missing parent span"); + } + + return session + .find(Value.class, 1L) + .invoke(value -> testing.runWithSpan("callback", () -> {})); + }) + .await() + .atMost(Duration.ofSeconds(30)); + }); + + assertTrace(); + } + + @Test + void testStage() throws Exception { + CompletableFuture result = new CompletableFuture<>(); + testing.runWithSpan( + "parent", + () -> + runWithVertx( + () -> + stageSessionFactory + .withSession( + session -> { + if (!Span.current().getSpanContext().isValid()) { + throw new IllegalStateException("missing parent span"); + } + + return session + .find(Value.class, 1L) + .thenAccept(value -> testing.runWithSpan("callback", () -> {})); + }) + .whenComplete((value, throwable) -> complete(result, null, throwable)))); + result.get(30, TimeUnit.SECONDS); + + assertTrace(); + } + + @Test + void testStageWithStatelessSession() throws Exception { + CompletableFuture result = new CompletableFuture<>(); + testing.runWithSpan( + "parent", + () -> + runWithVertx( + () -> + stageSessionFactory + .withStatelessSession( + session -> { + if (!Span.current().getSpanContext().isValid()) { + throw new IllegalStateException("missing parent span"); + } + + return session + .get(Value.class, 1L) + .thenAccept(value -> testing.runWithSpan("callback", () -> {})); + }) + .whenComplete((value, throwable) -> complete(result, null, throwable)))); + result.get(30, TimeUnit.SECONDS); + + assertTrace(); + } + + @Test + void testStageSessionWithTransaction() throws Exception { + CompletableFuture result = new CompletableFuture<>(); + testing.runWithSpan( + "parent", + () -> + runWithVertx( + () -> + stageSessionFactory + .withSession( + session -> { + if (!Span.current().getSpanContext().isValid()) { + throw new IllegalStateException("missing parent span"); + } + + return session + .withTransaction(transaction -> session.find(Value.class, 1L)) + .thenAccept(value -> testing.runWithSpan("callback", () -> {})); + }) + .whenComplete((value, throwable) -> complete(result, null, throwable)))); + result.get(30, TimeUnit.SECONDS); + + assertTrace(); + } + + @Test + void testStageStatelessSessionWithTransaction() throws Exception { + CompletableFuture result = new CompletableFuture<>(); + testing.runWithSpan( + "parent", + () -> + runWithVertx( + () -> + stageSessionFactory + .withStatelessSession( + session -> { + if (!Span.current().getSpanContext().isValid()) { + throw new IllegalStateException("missing parent span"); + } + + return session + .withTransaction(transaction -> session.get(Value.class, 1L)) + .thenAccept(value -> testing.runWithSpan("callback", () -> {})); + }) + .whenComplete((value, throwable) -> complete(result, null, throwable)))); + result.get(30, TimeUnit.SECONDS); + + assertTrace(); + } + + @Test + void testStageOpenSession() throws Exception { + CompletableFuture result = new CompletableFuture<>(); + testing.runWithSpan( + "parent", + () -> + runWithVertx( + () -> + stageSessionFactory + .openSession() + .thenApply( + session -> { + if (!Span.current().getSpanContext().isValid()) { + throw new IllegalStateException("missing parent span"); + } + + return session + .find(Value.class, 1L) + .thenAccept(value -> testing.runWithSpan("callback", () -> {})); + }) + .whenComplete((value, throwable) -> complete(result, value, throwable)))); + result.get(30, TimeUnit.SECONDS); + + assertTrace(); + } + + @Test + void testStageOpenStatelessSession() throws Exception { + CompletableFuture result = new CompletableFuture<>(); + testing.runWithSpan( + "parent", + () -> + runWithVertx( + () -> + stageSessionFactory + .openStatelessSession() + .thenApply( + session -> { + if (!Span.current().getSpanContext().isValid()) { + throw new IllegalStateException("missing parent span"); + } + + return session + .get(Value.class, 1L) + .thenAccept(value -> testing.runWithSpan("callback", () -> {})); + }) + .whenComplete((value, throwable) -> complete(result, value, throwable)))); + result.get(30, TimeUnit.SECONDS); + + assertTrace(); + } + + private static void runWithVertx(Runnable runnable) { + vertx.getOrCreateContext().runOnContext(event -> runnable.run()); + } + + private static void complete( + CompletableFuture completableFuture, Object result, Throwable throwable) { + if (throwable != null) { + completableFuture.completeExceptionally(throwable); + } else { + completableFuture.complete(result); + } + } + + private static void assertTrace() { + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasKind(SpanKind.INTERNAL), + span -> + span.hasName("SELECT tempdb.Value") + .hasKind(SpanKind.CLIENT) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo(DB_NAME, DB), + equalTo(DB_USER, USER_DB), + equalTo( + DB_STATEMENT, + "select value0_.id as id1_0_0_, value0_.name as name2_0_0_ from Value value0_ where value0_.id=$1"), + equalTo(DB_OPERATION, "SELECT"), + equalTo(DB_SQL_TABLE, "Value"), + equalTo(SERVER_ADDRESS, host), + equalTo(SERVER_PORT, port)), + span -> + span.hasName("callback") + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(0)))); + } +} diff --git a/instrumentation/hibernate/hibernate-reactive-1.0/javaagent/src/hibernateReactive1Test/java/io/opentelemetry/javaagent/instrumentation/hibernate/reactive/v1_0/Value.java b/instrumentation/hibernate/hibernate-reactive-1.0/javaagent/src/hibernateReactive1Test/java/io/opentelemetry/javaagent/instrumentation/hibernate/reactive/v1_0/Value.java new file mode 100644 index 000000000000..50b0d1bdc1f8 --- /dev/null +++ b/instrumentation/hibernate/hibernate-reactive-1.0/javaagent/src/hibernateReactive1Test/java/io/opentelemetry/javaagent/instrumentation/hibernate/reactive/v1_0/Value.java @@ -0,0 +1,43 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.hibernate.reactive.v1_0; + +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.Id; +import javax.persistence.Table; + +@Entity +@Table +public class Value { + + private Long id; + private String name; + + public Value() {} + + public Value(String name) { + this.name = name; + } + + @Id + @GeneratedValue + 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/instrumentation/hibernate/hibernate-reactive-1.0/javaagent/src/hibernateReactive1Test/resources/META-INF/persistence.xml b/instrumentation/hibernate/hibernate-reactive-1.0/javaagent/src/hibernateReactive1Test/resources/META-INF/persistence.xml new file mode 100644 index 000000000000..b3c690663ea8 --- /dev/null +++ b/instrumentation/hibernate/hibernate-reactive-1.0/javaagent/src/hibernateReactive1Test/resources/META-INF/persistence.xml @@ -0,0 +1,20 @@ + + + + org.hibernate.reactive.provider.ReactivePersistenceProvider + io.opentelemetry.javaagent.instrumentation.hibernate.reactive.v1_0.Value + true + + + + + + + + + + + diff --git a/instrumentation/hibernate/hibernate-reactive-1.0/javaagent/src/hibernateReactive2Test/java/io/opentelemetry/javaagent/instrumentation/hibernate/reactive/v2_0/HibernateReactiveTest.java b/instrumentation/hibernate/hibernate-reactive-1.0/javaagent/src/hibernateReactive2Test/java/io/opentelemetry/javaagent/instrumentation/hibernate/reactive/v2_0/HibernateReactiveTest.java new file mode 100644 index 000000000000..a7ec8dc7248d --- /dev/null +++ b/instrumentation/hibernate/hibernate-reactive-1.0/javaagent/src/hibernateReactive2Test/java/io/opentelemetry/javaagent/instrumentation/hibernate/reactive/v2_0/HibernateReactiveTest.java @@ -0,0 +1,313 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.hibernate.reactive.v2_0; + +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; +import static io.opentelemetry.semconv.ServerAttributes.SERVER_ADDRESS; +import static io.opentelemetry.semconv.ServerAttributes.SERVER_PORT; +import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DB_NAME; +import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DB_OPERATION; +import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DB_SQL_TABLE; +import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DB_STATEMENT; +import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DB_USER; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.vertx.core.Vertx; +import jakarta.persistence.EntityManagerFactory; +import jakarta.persistence.Persistence; +import java.time.Duration; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import org.hibernate.reactive.mutiny.Mutiny; +import org.hibernate.reactive.stage.Stage; +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; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.output.Slf4jLogConsumer; + +class HibernateReactiveTest { + private static final Logger logger = LoggerFactory.getLogger(HibernateReactiveTest.class); + + private static final String USER_DB = "SA"; + private static final String PW_DB = "password123"; + private static final String DB = "tempdb"; + + @RegisterExtension + protected static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + private static final Vertx vertx = Vertx.vertx(); + private static GenericContainer container; + private static String host; + private static int port; + private static EntityManagerFactory entityManagerFactory; + private static Mutiny.SessionFactory mutinySessionFactory; + private static Stage.SessionFactory stageSessionFactory; + + @BeforeAll + static void setUp() throws Exception { + container = + new GenericContainer<>("postgres:9.6.8") + .withEnv("POSTGRES_USER", USER_DB) + .withEnv("POSTGRES_PASSWORD", PW_DB) + .withEnv("POSTGRES_DB", DB) + .withExposedPorts(5432) + .withLogConsumer(new Slf4jLogConsumer(logger)) + .withStartupTimeout(Duration.ofMinutes(2)); + container.start(); + + host = container.getHost(); + port = container.getMappedPort(5432); + System.setProperty("db.host", host); + System.setProperty("db.port", String.valueOf(port)); + + entityManagerFactory = + vertx + .getOrCreateContext() + .executeBlocking( + promise -> promise.complete(Persistence.createEntityManagerFactory("test-pu"))) + .toCompletionStage() + .toCompletableFuture() + .get(30, TimeUnit.SECONDS); + + Value value = new Value("name"); + value.setId(1L); + + mutinySessionFactory = entityManagerFactory.unwrap(Mutiny.SessionFactory.class); + stageSessionFactory = entityManagerFactory.unwrap(Stage.SessionFactory.class); + + mutinySessionFactory + .withTransaction((session, tx) -> session.merge(value)) + .await() + .atMost(Duration.ofSeconds(30)); + } + + @AfterAll + static void cleanUp() { + if (entityManagerFactory != null) { + entityManagerFactory.close(); + } + if (mutinySessionFactory != null) { + mutinySessionFactory.close(); + } + if (stageSessionFactory != null) { + stageSessionFactory.close(); + } + vertx.close(); + container.stop(); + } + + @Test + void testMutiny() { + testing.runWithSpan( + "parent", + () -> { + mutinySessionFactory + .withSession( + session -> { + if (!Span.current().getSpanContext().isValid()) { + throw new IllegalStateException("missing parent span"); + } + + return session + .find(Value.class, 1L) + .invoke(value -> testing.runWithSpan("callback", () -> {})); + }) + .await() + .atMost(Duration.ofSeconds(30)); + }); + + assertTrace(); + } + + @Test + void testStage() throws Exception { + testing + .runWithSpan( + "parent", + () -> + stageSessionFactory + .withSession( + session -> { + if (!Span.current().getSpanContext().isValid()) { + throw new IllegalStateException("missing parent span"); + } + + return session + .find(Value.class, 1L) + .thenAccept(value -> testing.runWithSpan("callback", () -> {})); + }) + .toCompletableFuture()) + .get(30, TimeUnit.SECONDS); + + assertTrace(); + } + + @Test + void testStageWithStatelessSession() throws Exception { + testing + .runWithSpan( + "parent", + () -> + stageSessionFactory + .withStatelessSession( + session -> { + if (!Span.current().getSpanContext().isValid()) { + throw new IllegalStateException("missing parent span"); + } + + return session + .get(Value.class, 1L) + .thenAccept(value -> testing.runWithSpan("callback", () -> {})); + }) + .toCompletableFuture()) + .get(30, TimeUnit.SECONDS); + + assertTrace(); + } + + @Test + void testStageSessionWithTransaction() throws Exception { + testing + .runWithSpan( + "parent", + () -> + stageSessionFactory + .withSession( + session -> { + if (!Span.current().getSpanContext().isValid()) { + throw new IllegalStateException("missing parent span"); + } + + return session + .withTransaction(transaction -> session.find(Value.class, 1L)) + .thenAccept(value -> testing.runWithSpan("callback", () -> {})); + }) + .toCompletableFuture()) + .get(30, TimeUnit.SECONDS); + + assertTrace(); + } + + @Test + void testStageStatelessSessionWithTransaction() throws Exception { + testing + .runWithSpan( + "parent", + () -> + stageSessionFactory + .withStatelessSession( + session -> { + if (!Span.current().getSpanContext().isValid()) { + throw new IllegalStateException("missing parent span"); + } + + return session + .withTransaction(transaction -> session.get(Value.class, 1L)) + .thenAccept(value -> testing.runWithSpan("callback", () -> {})); + }) + .toCompletableFuture()) + .get(30, TimeUnit.SECONDS); + + assertTrace(); + } + + @Test + void testStageOpenSession() throws Exception { + CompletableFuture result = new CompletableFuture<>(); + testing.runWithSpan( + "parent", + () -> + runWithVertx( + () -> + stageSessionFactory + .openSession() + .thenApply( + session -> { + if (!Span.current().getSpanContext().isValid()) { + throw new IllegalStateException("missing parent span"); + } + + return session + .find(Value.class, 1L) + .thenAccept(value -> testing.runWithSpan("callback", () -> {})); + }) + .whenComplete((value, throwable) -> complete(result, value, throwable)))); + result.get(30, TimeUnit.SECONDS); + + assertTrace(); + } + + @Test + void testStageOpenStatelessSession() throws Exception { + CompletableFuture result = new CompletableFuture<>(); + testing.runWithSpan( + "parent", + () -> + runWithVertx( + () -> + stageSessionFactory + .openStatelessSession() + .thenApply( + session -> { + if (!Span.current().getSpanContext().isValid()) { + throw new IllegalStateException("missing parent span"); + } + + return session + .get(Value.class, 1L) + .thenAccept(value -> testing.runWithSpan("callback", () -> {})); + }) + .whenComplete((value, throwable) -> complete(result, value, throwable)))); + result.get(30, TimeUnit.SECONDS); + + assertTrace(); + } + + private static void runWithVertx(Runnable runnable) { + vertx.getOrCreateContext().runOnContext(event -> runnable.run()); + } + + private static void complete( + CompletableFuture completableFuture, Object result, Throwable throwable) { + if (throwable != null) { + completableFuture.completeExceptionally(throwable); + } else { + completableFuture.complete(result); + } + } + + private static void assertTrace() { + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasKind(SpanKind.INTERNAL), + span -> + span.hasName("SELECT tempdb.Value") + .hasKind(SpanKind.CLIENT) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo(DB_NAME, DB), + equalTo(DB_USER, USER_DB), + equalTo( + DB_STATEMENT, + "select v1_0.id,v1_0.name from Value v1_0 where v1_0.id=$1"), + equalTo(DB_OPERATION, "SELECT"), + equalTo(DB_SQL_TABLE, "Value"), + equalTo(SERVER_ADDRESS, host), + equalTo(SERVER_PORT, port)), + span -> + span.hasName("callback") + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(0)))); + } +} diff --git a/instrumentation/hibernate/hibernate-reactive-1.0/javaagent/src/hibernateReactive2Test/java/io/opentelemetry/javaagent/instrumentation/hibernate/reactive/v2_0/Value.java b/instrumentation/hibernate/hibernate-reactive-1.0/javaagent/src/hibernateReactive2Test/java/io/opentelemetry/javaagent/instrumentation/hibernate/reactive/v2_0/Value.java new file mode 100644 index 000000000000..df243fdda487 --- /dev/null +++ b/instrumentation/hibernate/hibernate-reactive-1.0/javaagent/src/hibernateReactive2Test/java/io/opentelemetry/javaagent/instrumentation/hibernate/reactive/v2_0/Value.java @@ -0,0 +1,43 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.hibernate.reactive.v2_0; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.Table; + +@Entity +@Table +public class Value { + + private Long id; + private String name; + + public Value() {} + + public Value(String name) { + this.name = name; + } + + @Id + @GeneratedValue + 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/instrumentation/hibernate/hibernate-reactive-1.0/javaagent/src/hibernateReactive2Test/resources/META-INF/persistence.xml b/instrumentation/hibernate/hibernate-reactive-1.0/javaagent/src/hibernateReactive2Test/resources/META-INF/persistence.xml new file mode 100644 index 000000000000..0972582f6416 --- /dev/null +++ b/instrumentation/hibernate/hibernate-reactive-1.0/javaagent/src/hibernateReactive2Test/resources/META-INF/persistence.xml @@ -0,0 +1,20 @@ + + + + org.hibernate.reactive.provider.ReactivePersistenceProvider + io.opentelemetry.javaagent.instrumentation.hibernate.reactive.v2_0.Value + true + + + + + + + + + + + diff --git a/instrumentation/hibernate/hibernate-reactive-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/reactive/v1_0/mutiny/ContextOperator.java b/instrumentation/hibernate/hibernate-reactive-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/reactive/v1_0/mutiny/ContextOperator.java new file mode 100644 index 000000000000..3b6b6f685556 --- /dev/null +++ b/instrumentation/hibernate/hibernate-reactive-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/reactive/v1_0/mutiny/ContextOperator.java @@ -0,0 +1,72 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.hibernate.reactive.v1_0.mutiny; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.smallrye.mutiny.Uni; +import io.smallrye.mutiny.operators.UniOperator; +import io.smallrye.mutiny.subscription.UniSubscriber; +import io.smallrye.mutiny.subscription.UniSubscription; + +public final class ContextOperator extends UniOperator { + private final Context context; + + public ContextOperator(Uni upstream, Context context) { + super(upstream); + this.context = context; + } + + public static Uni plug(Uni uni) { + if (uni instanceof ContextOperator) { + return uni; + } + Context parentContext = Context.current(); + if (parentContext == Context.root()) { + return uni; + } + + return uni.plug(u -> new ContextOperator<>(u, parentContext)); + } + + @Override + public void subscribe(UniSubscriber downstream) { + try (Scope ignore = context.makeCurrent()) { + upstream().subscribe().withSubscriber(new ContextSubscriber<>(downstream, context)); + } + } + + private static class ContextSubscriber implements UniSubscriber { + private final UniSubscriber downstream; + private final Context context; + + private ContextSubscriber(UniSubscriber downstream, Context context) { + this.downstream = downstream; + this.context = context; + } + + @Override + public void onSubscribe(UniSubscription uniSubscription) { + try (Scope ignore = context.makeCurrent()) { + downstream.onSubscribe(uniSubscription); + } + } + + @Override + public void onItem(T t) { + try (Scope ignore = context.makeCurrent()) { + downstream.onItem(t); + } + } + + @Override + public void onFailure(Throwable throwable) { + try (Scope ignore = context.makeCurrent()) { + downstream.onFailure(throwable); + } + } + } +} diff --git a/instrumentation/hibernate/hibernate-reactive-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/reactive/v1_0/mutiny/HibernateReactiveMutinyInstrumentationModule.java b/instrumentation/hibernate/hibernate-reactive-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/reactive/v1_0/mutiny/HibernateReactiveMutinyInstrumentationModule.java new file mode 100644 index 000000000000..5aca6a790076 --- /dev/null +++ b/instrumentation/hibernate/hibernate-reactive-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/reactive/v1_0/mutiny/HibernateReactiveMutinyInstrumentationModule.java @@ -0,0 +1,26 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.hibernate.reactive.v1_0.mutiny; + +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 HibernateReactiveMutinyInstrumentationModule extends InstrumentationModule { + + public HibernateReactiveMutinyInstrumentationModule() { + super("hibernate-reactive", "hibernate-reactive-1.0", "hibernate-reactive-mutiny"); + } + + @Override + public List typeInstrumentations() { + return singletonList(new MutinySessionFactoryInstrumentation()); + } +} diff --git a/instrumentation/hibernate/hibernate-reactive-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/reactive/v1_0/mutiny/MutinySessionFactoryInstrumentation.java b/instrumentation/hibernate/hibernate-reactive-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/reactive/v1_0/mutiny/MutinySessionFactoryInstrumentation.java new file mode 100644 index 000000000000..c8bcea81fe41 --- /dev/null +++ b/instrumentation/hibernate/hibernate-reactive-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/reactive/v1_0/mutiny/MutinySessionFactoryInstrumentation.java @@ -0,0 +1,42 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.hibernate.reactive.v1_0.mutiny; + +import static net.bytebuddy.matcher.ElementMatchers.nameStartsWith; +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.smallrye.mutiny.Uni; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class MutinySessionFactoryInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("org.hibernate.reactive.mutiny.impl.MutinySessionFactoryImpl"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + nameStartsWith("open") + .or(nameStartsWith("with")) + .and(returns(named("io.smallrye.mutiny.Uni"))), + this.getClass().getName() + "$ContextAdvice"); + } + + @SuppressWarnings("unused") + public static class ContextAdvice { + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void onExit(@Advice.Return(readOnly = false) Uni uni) { + uni = ContextOperator.plug(uni); + } + } +} diff --git a/instrumentation/hibernate/hibernate-reactive-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/reactive/v1_0/stage/CompletionStageWrapper.java b/instrumentation/hibernate/hibernate-reactive-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/reactive/v1_0/stage/CompletionStageWrapper.java new file mode 100644 index 000000000000..ea6ca053a251 --- /dev/null +++ b/instrumentation/hibernate/hibernate-reactive-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/reactive/v1_0/stage/CompletionStageWrapper.java @@ -0,0 +1,40 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.hibernate.reactive.v1_0.stage; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; + +public final class CompletionStageWrapper { + + private CompletionStageWrapper() {} + + public static CompletionStage wrap(CompletionStage future) { + Context context = Context.current(); + if (context != Context.root()) { + return wrap(future, context); + } + return future; + } + + private static CompletionStage wrap(CompletionStage completionStage, Context context) { + CompletableFuture result = new CompletableFuture<>(); + completionStage.whenComplete( + (T value, Throwable throwable) -> { + try (Scope ignored = context.makeCurrent()) { + if (throwable != null) { + result.completeExceptionally(throwable); + } else { + result.complete(value); + } + } + }); + + return result; + } +} diff --git a/instrumentation/hibernate/hibernate-reactive-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/reactive/v1_0/stage/FunctionWrapper.java b/instrumentation/hibernate/hibernate-reactive-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/reactive/v1_0/stage/FunctionWrapper.java new file mode 100644 index 000000000000..b7983c6a093c --- /dev/null +++ b/instrumentation/hibernate/hibernate-reactive-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/reactive/v1_0/stage/FunctionWrapper.java @@ -0,0 +1,39 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.hibernate.reactive.v1_0.stage; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import java.util.function.Function; + +public final class FunctionWrapper implements Function { + private final Function delegate; + private final Context context; + + private FunctionWrapper(Function delegate, Context context) { + this.delegate = delegate; + this.context = context; + } + + public static Function wrap(Function function) { + if (function instanceof FunctionWrapper) { + return function; + } + Context context = Context.current(); + if (context == Context.root()) { + return function; + } + + return new FunctionWrapper<>(function, context); + } + + @Override + public R apply(T t) { + try (Scope ignore = context.makeCurrent()) { + return delegate.apply(t); + } + } +} diff --git a/instrumentation/hibernate/hibernate-reactive-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/reactive/v1_0/stage/HibernateReactiveStageInstrumentationModule.java b/instrumentation/hibernate/hibernate-reactive-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/reactive/v1_0/stage/HibernateReactiveStageInstrumentationModule.java new file mode 100644 index 000000000000..4fa2366b906a --- /dev/null +++ b/instrumentation/hibernate/hibernate-reactive-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/reactive/v1_0/stage/HibernateReactiveStageInstrumentationModule.java @@ -0,0 +1,26 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.hibernate.reactive.v1_0.stage; + +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 HibernateReactiveStageInstrumentationModule extends InstrumentationModule { + + public HibernateReactiveStageInstrumentationModule() { + super("hibernate-reactive", "hibernate-reactive-1.0", "hibernate-reactive-stage"); + } + + @Override + public List typeInstrumentations() { + return asList(new StageSessionFactoryInstrumentation(), new StageSessionImplInstrumentation()); + } +} diff --git a/instrumentation/hibernate/hibernate-reactive-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/reactive/v1_0/stage/StageSessionFactoryInstrumentation.java b/instrumentation/hibernate/hibernate-reactive-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/reactive/v1_0/stage/StageSessionFactoryInstrumentation.java new file mode 100644 index 000000000000..47154f9d1798 --- /dev/null +++ b/instrumentation/hibernate/hibernate-reactive-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/reactive/v1_0/stage/StageSessionFactoryInstrumentation.java @@ -0,0 +1,65 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.hibernate.reactive.v1_0.stage; + +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.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import java.util.concurrent.CompletionStage; +import java.util.function.Function; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class StageSessionFactoryInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher typeMatcher() { + return named("org.hibernate.reactive.stage.impl.StageSessionFactoryImpl"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + namedOneOf("withSession", "withStatelessSession").and(takesArgument(0, Function.class)), + this.getClass().getName() + "$Function0Advice"); + transformer.applyAdviceToMethod( + namedOneOf("withSession", "withStatelessSession").and(takesArgument(1, Function.class)), + this.getClass().getName() + "$Function1Advice"); + transformer.applyAdviceToMethod( + namedOneOf("openSession", "openStatelessSession").and(returns(CompletionStage.class)), + this.getClass().getName() + "$OpenSessionAdvice"); + } + + @SuppressWarnings("unused") + public static class Function0Advice { + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter( + @Advice.Argument(value = 0, readOnly = false) Function function) { + function = FunctionWrapper.wrap(function); + } + } + + @SuppressWarnings("unused") + public static class Function1Advice { + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter( + @Advice.Argument(value = 1, readOnly = false) Function function) { + function = FunctionWrapper.wrap(function); + } + } + + @SuppressWarnings("unused") + public static class OpenSessionAdvice { + @Advice.OnMethodExit(suppress = Throwable.class) + public static void onExit(@Advice.Return(readOnly = false) CompletionStage completionStage) { + completionStage = CompletionStageWrapper.wrap(completionStage); + } + } +} diff --git a/instrumentation/hibernate/hibernate-reactive-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/reactive/v1_0/stage/StageSessionImplInstrumentation.java b/instrumentation/hibernate/hibernate-reactive-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/reactive/v1_0/stage/StageSessionImplInstrumentation.java new file mode 100644 index 000000000000..fa92dd794780 --- /dev/null +++ b/instrumentation/hibernate/hibernate-reactive-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/reactive/v1_0/stage/StageSessionImplInstrumentation.java @@ -0,0 +1,50 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.hibernate.reactive.v1_0.stage; + +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.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import java.util.concurrent.CompletionStage; +import java.util.function.Function; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class StageSessionImplInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher typeMatcher() { + return namedOneOf( + "org.hibernate.reactive.stage.impl.StageSessionImpl", + "org.hibernate.reactive.stage.impl.StageStatelessSessionImpl"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + named("withTransaction") + .and(takesArgument(0, Function.class).and(returns(CompletionStage.class))), + this.getClass().getName() + "$WithTransactionAdvice"); + } + + @SuppressWarnings("unused") + public static class WithTransactionAdvice { + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter( + @Advice.Argument(value = 0, readOnly = false) Function function) { + function = FunctionWrapper.wrap(function); + } + + @Advice.OnMethodExit(suppress = Throwable.class) + public static void onExit(@Advice.Return(readOnly = false) CompletionStage completionStage) { + completionStage = CompletionStageWrapper.wrap(completionStage); + } + } +} diff --git a/instrumentation/hikaricp-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hikaricp/v3_0/HikariPoolInstrumentation.java b/instrumentation/hikaricp-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hikaricp/v3_0/HikariPoolInstrumentation.java index bad93404d20e..8e77f691b60c 100644 --- a/instrumentation/hikaricp-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hikaricp/v3_0/HikariPoolInstrumentation.java +++ b/instrumentation/hikaricp-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hikaricp/v3_0/HikariPoolInstrumentation.java @@ -40,7 +40,7 @@ public static class SetMetricsTrackerFactoryAdvice { @Advice.OnMethodEnter(suppress = Throwable.class) public static void onEnter( @Advice.Argument(value = 0, readOnly = false) MetricsTrackerFactory userMetricsTracker, - @Advice.FieldValue(value = "metricsTracker") AutoCloseable existingMetricsTracker) + @Advice.FieldValue("metricsTracker") AutoCloseable existingMetricsTracker) throws Exception { if (existingMetricsTracker != null) { diff --git a/instrumentation/hikaricp-3.0/library/src/main/java/io/opentelemetry/instrumentation/hikaricp/v3_0/OpenTelemetryMetricsTrackerFactory.java b/instrumentation/hikaricp-3.0/library/src/main/java/io/opentelemetry/instrumentation/hikaricp/v3_0/OpenTelemetryMetricsTrackerFactory.java index 703b6c4014b8..67234e7b1d96 100644 --- a/instrumentation/hikaricp-3.0/library/src/main/java/io/opentelemetry/instrumentation/hikaricp/v3_0/OpenTelemetryMetricsTrackerFactory.java +++ b/instrumentation/hikaricp-3.0/library/src/main/java/io/opentelemetry/instrumentation/hikaricp/v3_0/OpenTelemetryMetricsTrackerFactory.java @@ -12,7 +12,7 @@ import io.opentelemetry.api.common.Attributes; import io.opentelemetry.api.metrics.BatchCallback; import io.opentelemetry.api.metrics.ObservableLongMeasurement; -import io.opentelemetry.instrumentation.api.metrics.db.DbConnectionPoolMetrics; +import io.opentelemetry.instrumentation.api.incubator.semconv.db.DbConnectionPoolMetrics; import javax.annotation.Nullable; final class OpenTelemetryMetricsTrackerFactory implements MetricsTrackerFactory { diff --git a/instrumentation/http-url-connection/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/httpurlconnection/HttpMethodAttributeExtractor.java b/instrumentation/http-url-connection/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/httpurlconnection/HttpMethodAttributeExtractor.java index 7c0574a769f2..83bfe1e46f1f 100644 --- a/instrumentation/http-url-connection/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/httpurlconnection/HttpMethodAttributeExtractor.java +++ b/instrumentation/http-url-connection/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/httpurlconnection/HttpMethodAttributeExtractor.java @@ -5,22 +5,31 @@ package io.opentelemetry.javaagent.instrumentation.httpurlconnection; +import static io.opentelemetry.instrumentation.api.internal.AttributesExtractorUtil.internalSet; +import static io.opentelemetry.instrumentation.api.internal.HttpConstants._OTHER; + import io.opentelemetry.api.common.AttributesBuilder; import io.opentelemetry.api.trace.Span; import io.opentelemetry.context.Context; import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.opentelemetry.semconv.HttpAttributes; import java.net.HttpURLConnection; +import java.util.Set; import javax.annotation.Nullable; public class HttpMethodAttributeExtractor< REQUEST extends HttpURLConnection, RESPONSE extends Integer> implements AttributesExtractor { - private HttpMethodAttributeExtractor() {} + private final Set knownMethods; + + private HttpMethodAttributeExtractor(Set knownMethods) { + this.knownMethods = knownMethods; + } - public static AttributesExtractor create() { - return new HttpMethodAttributeExtractor<>(); + public static AttributesExtractor create( + Set knownMethods) { + return new HttpMethodAttributeExtractor<>(knownMethods); } @Override @@ -38,11 +47,17 @@ public void onEnd( GetOutputStreamContext getOutputStreamContext = GetOutputStreamContext.get(context); if (getOutputStreamContext.isOutputStreamMethodOfSunConnectionCalled()) { - String requestMethod = connection.getRequestMethod(); + String method = connection.getRequestMethod(); // The getOutputStream() has transformed "GET" into "POST" - attributes.put(SemanticAttributes.HTTP_METHOD, requestMethod); + if (knownMethods.contains(method)) { + internalSet(attributes, HttpAttributes.HTTP_REQUEST_METHOD, method); + attributes.remove(HttpAttributes.HTTP_REQUEST_METHOD_ORIGINAL); + } else { + internalSet(attributes, HttpAttributes.HTTP_REQUEST_METHOD, _OTHER); + internalSet(attributes, HttpAttributes.HTTP_REQUEST_METHOD_ORIGINAL, method); + } Span span = Span.fromContext(context); - span.updateName(requestMethod); + span.updateName(method); } } } diff --git a/instrumentation/http-url-connection/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/httpurlconnection/HttpUrlConnectionSingletons.java b/instrumentation/http-url-connection/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/httpurlconnection/HttpUrlConnectionSingletons.java index 8032ee912018..9f8b3006dc35 100644 --- a/instrumentation/http-url-connection/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/httpurlconnection/HttpUrlConnectionSingletons.java +++ b/instrumentation/http-url-connection/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/httpurlconnection/HttpUrlConnectionSingletons.java @@ -5,14 +5,9 @@ package io.opentelemetry.javaagent.instrumentation.httpurlconnection; -import io.opentelemetry.api.GlobalOpenTelemetry; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpClientAttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpClientMetrics; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpSpanNameExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpSpanStatusExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.net.PeerServiceAttributesExtractor; -import io.opentelemetry.javaagent.bootstrap.internal.CommonConfig; +import io.opentelemetry.javaagent.bootstrap.internal.AgentCommonConfig; +import io.opentelemetry.javaagent.bootstrap.internal.JavaagentHttpClientInstrumenters; import java.net.HttpURLConnection; public final class HttpUrlConnectionSingletons { @@ -20,29 +15,19 @@ public final class HttpUrlConnectionSingletons { private static final Instrumenter INSTRUMENTER; static { - HttpUrlHttpAttributesGetter httpAttributesGetter = new HttpUrlHttpAttributesGetter(); - HttpUrlNetAttributesGetter netAttributesGetter = new HttpUrlNetAttributesGetter(); - INSTRUMENTER = - Instrumenter.builder( - GlobalOpenTelemetry.get(), - "io.opentelemetry.http-url-connection", - HttpSpanNameExtractor.create(httpAttributesGetter)) - .setSpanStatusExtractor(HttpSpanStatusExtractor.create(httpAttributesGetter)) - .addAttributesExtractor( - HttpClientAttributesExtractor.builder(httpAttributesGetter, netAttributesGetter) - .setCapturedRequestHeaders(CommonConfig.get().getClientRequestHeaders()) - .setCapturedResponseHeaders(CommonConfig.get().getClientResponseHeaders()) - .build()) - .addAttributesExtractor( - PeerServiceAttributesExtractor.create( - netAttributesGetter, CommonConfig.get().getPeerServiceMapping())) - .addAttributesExtractor(HttpMethodAttributeExtractor.create()) - .addContextCustomizer( - (context, httpRequestPacket, startAttributes) -> - GetOutputStreamContext.init(context)) - .addOperationMetrics(HttpClientMetrics.get()) - .buildClientInstrumenter(RequestPropertySetter.INSTANCE); + JavaagentHttpClientInstrumenters.create( + "io.opentelemetry.http-url-connection", + new HttpUrlHttpAttributesGetter(), + RequestPropertySetter.INSTANCE, + builder -> + builder + .addAttributesExtractor( + HttpMethodAttributeExtractor.create( + AgentCommonConfig.get().getKnownHttpRequestMethods())) + .addContextCustomizer( + (context, httpRequestPacket, startAttributes) -> + GetOutputStreamContext.init(context))); } public static Instrumenter instrumenter() { diff --git a/instrumentation/http-url-connection/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/httpurlconnection/HttpUrlHttpAttributesGetter.java b/instrumentation/http-url-connection/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/httpurlconnection/HttpUrlHttpAttributesGetter.java index 050b24de6c47..fdb8545d517a 100644 --- a/instrumentation/http-url-connection/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/httpurlconnection/HttpUrlHttpAttributesGetter.java +++ b/instrumentation/http-url-connection/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/httpurlconnection/HttpUrlHttpAttributesGetter.java @@ -8,7 +8,7 @@ import static java.util.Collections.emptyList; import static java.util.Collections.singletonList; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpClientAttributesGetter; +import io.opentelemetry.instrumentation.api.semconv.http.HttpClientAttributesGetter; import java.net.HttpURLConnection; import java.util.List; import javax.annotation.Nullable; @@ -44,4 +44,28 @@ public List getHttpResponseHeader( String value = connection.getHeaderField(name); return value == null ? emptyList() : singletonList(value); } + + @Nullable + @Override + public String getNetworkProtocolName(HttpURLConnection connection, @Nullable Integer integer) { + // HttpURLConnection hardcodes the protocol name&version + return "http"; + } + + @Nullable + @Override + public String getNetworkProtocolVersion(HttpURLConnection connection, @Nullable Integer integer) { + // HttpURLConnection hardcodes the protocol name&version + return "1.1"; + } + + @Override + public String getServerAddress(HttpURLConnection connection) { + return connection.getURL().getHost(); + } + + @Override + public Integer getServerPort(HttpURLConnection connection) { + return connection.getURL().getPort(); + } } diff --git a/instrumentation/http-url-connection/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/httpurlconnection/HttpUrlNetAttributesGetter.java b/instrumentation/http-url-connection/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/httpurlconnection/HttpUrlNetAttributesGetter.java deleted file mode 100644 index 6d41b060e321..000000000000 --- a/instrumentation/http-url-connection/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/httpurlconnection/HttpUrlNetAttributesGetter.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.javaagent.instrumentation.httpurlconnection; - -import io.opentelemetry.instrumentation.api.instrumenter.net.NetClientAttributesGetter; -import java.net.HttpURLConnection; -import javax.annotation.Nullable; - -class HttpUrlNetAttributesGetter implements NetClientAttributesGetter { - - @Nullable - @Override - public String getNetworkProtocolName(HttpURLConnection connection, @Nullable Integer integer) { - // HttpURLConnection hardcodes the protocol name&version - return "http"; - } - - @Nullable - @Override - public String getNetworkProtocolVersion(HttpURLConnection connection, @Nullable Integer integer) { - // HttpURLConnection hardcodes the protocol name&version - return "1.1"; - } - - @Override - public String getServerAddress(HttpURLConnection connection) { - return connection.getURL().getHost(); - } - - @Override - public Integer getServerPort(HttpURLConnection connection) { - return connection.getURL().getPort(); - } -} diff --git a/instrumentation/http-url-connection/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/httpurlconnection/HttpUrlConnectionResponseCodeOnlyTest.java b/instrumentation/http-url-connection/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/httpurlconnection/HttpUrlConnectionResponseCodeOnlyTest.java index f4a49a9c5c89..9ffa646d5fd0 100644 --- a/instrumentation/http-url-connection/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/httpurlconnection/HttpUrlConnectionResponseCodeOnlyTest.java +++ b/instrumentation/http-url-connection/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/httpurlconnection/HttpUrlConnectionResponseCodeOnlyTest.java @@ -50,5 +50,6 @@ protected void configure(HttpClientTestOptions.Builder optionsBuilder) { // HttpURLConnection can't be reused optionsBuilder.disableTestReusedRequest(); optionsBuilder.disableTestCallback(); + optionsBuilder.disableTestNonStandardHttpMethod(); } } diff --git a/instrumentation/http-url-connection/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/httpurlconnection/HttpUrlConnectionTest.java b/instrumentation/http-url-connection/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/httpurlconnection/HttpUrlConnectionTest.java index 61c76c432405..1fcdc8384c55 100644 --- a/instrumentation/http-url-connection/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/httpurlconnection/HttpUrlConnectionTest.java +++ b/instrumentation/http-url-connection/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/httpurlconnection/HttpUrlConnectionTest.java @@ -5,30 +5,39 @@ package io.opentelemetry.javaagent.instrumentation.httpurlconnection; -import static io.opentelemetry.api.common.AttributeKey.stringKey; 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.javaagent.instrumentation.httpurlconnection.StreamUtils.readLines; import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; -import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.satisfies; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.catchThrowable; import io.opentelemetry.api.trace.Span; +import io.opentelemetry.instrumentation.test.utils.PortUtils; import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpClientTest; import io.opentelemetry.instrumentation.testing.junit.http.HttpClientInstrumentationExtension; import io.opentelemetry.instrumentation.testing.junit.http.HttpClientTestOptions; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.opentelemetry.sdk.testing.assertj.AttributeAssertion; +import io.opentelemetry.sdk.trace.data.StatusData; +import io.opentelemetry.semconv.ErrorAttributes; +import io.opentelemetry.semconv.HttpAttributes; +import io.opentelemetry.semconv.NetworkAttributes; +import io.opentelemetry.semconv.ServerAttributes; +import io.opentelemetry.semconv.UrlAttributes; import java.io.DataOutputStream; import java.io.IOException; import java.io.InputStream; import java.net.HttpURLConnection; import java.net.URI; import java.net.URL; +import java.net.URLConnection; +import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Map; -import org.assertj.core.api.AbstractLongAssert; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.junit.jupiter.params.ParameterizedTest; @@ -78,6 +87,7 @@ protected void configure(HttpClientTestOptions.Builder optionsBuilder) { // HttpURLConnection can't be reused optionsBuilder.disableTestReusedRequest(); optionsBuilder.disableTestCallback(); + optionsBuilder.disableTestNonStandardHttpMethod(); } @ParameterizedTest @@ -110,6 +120,16 @@ public void traceRequest(boolean useCache) throws IOException { assertThat(lines).isEqualTo(RESPONSE); }); + List attributes = + new ArrayList<>( + Arrays.asList( + equalTo(NetworkAttributes.NETWORK_PROTOCOL_VERSION, "1.1"), + equalTo(ServerAttributes.SERVER_ADDRESS, "localhost"), + equalTo(ServerAttributes.SERVER_PORT, url.getPort()), + equalTo(UrlAttributes.URL_FULL, url.toString()), + equalTo(HttpAttributes.HTTP_REQUEST_METHOD, "GET"), + equalTo(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, STATUS))); + testing.waitAndAssertTraces( trace -> trace.hasSpansSatisfyingExactly( @@ -118,34 +138,14 @@ public void traceRequest(boolean useCache) throws IOException { span.hasName("GET") .hasKind(CLIENT) .hasParent(trace.getSpan(0)) - .hasAttributesSatisfyingExactly( - equalTo(stringKey("net.protocol.name"), "http"), - equalTo(stringKey("net.protocol.version"), "1.1"), - equalTo(SemanticAttributes.NET_PEER_NAME, "localhost"), - equalTo(SemanticAttributes.NET_PEER_PORT, url.getPort()), - equalTo(SemanticAttributes.HTTP_URL, url.toString()), - equalTo(SemanticAttributes.HTTP_METHOD, "GET"), - equalTo(SemanticAttributes.HTTP_STATUS_CODE, STATUS), - satisfies( - SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH, - AbstractLongAssert::isNotNegative)), + .hasAttributesSatisfyingExactly(attributes), span -> span.hasName("test-http-server").hasKind(SERVER).hasParent(trace.getSpan(1)), span -> span.hasName("GET") .hasKind(CLIENT) .hasParent(trace.getSpan(0)) - .hasAttributesSatisfyingExactly( - equalTo(stringKey("net.protocol.name"), "http"), - equalTo(stringKey("net.protocol.version"), "1.1"), - equalTo(SemanticAttributes.NET_PEER_NAME, "localhost"), - equalTo(SemanticAttributes.NET_PEER_PORT, url.getPort()), - equalTo(SemanticAttributes.HTTP_URL, url.toString()), - equalTo(SemanticAttributes.HTTP_METHOD, "GET"), - equalTo(SemanticAttributes.HTTP_STATUS_CODE, STATUS), - satisfies( - SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH, - AbstractLongAssert::isNotNegative)), + .hasAttributesSatisfyingExactly(attributes), span -> span.hasName("test-http-server").hasKind(SERVER).hasParent(trace.getSpan(3)))); } @@ -165,6 +165,16 @@ public void testBrokenApiUsage() throws IOException { return con; }); + List attributes = + new ArrayList<>( + Arrays.asList( + equalTo(NetworkAttributes.NETWORK_PROTOCOL_VERSION, "1.1"), + equalTo(ServerAttributes.SERVER_ADDRESS, "localhost"), + equalTo(ServerAttributes.SERVER_PORT, url.getPort()), + equalTo(UrlAttributes.URL_FULL, url.toString()), + equalTo(HttpAttributes.HTTP_REQUEST_METHOD, "GET"), + equalTo(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, STATUS))); + testing.waitAndAssertTraces( trace -> trace.hasSpansSatisfyingExactly( @@ -173,17 +183,7 @@ public void testBrokenApiUsage() throws IOException { span.hasName("GET") .hasKind(CLIENT) .hasParent(trace.getSpan(0)) - .hasAttributesSatisfyingExactly( - equalTo(stringKey("net.protocol.name"), "http"), - equalTo(stringKey("net.protocol.version"), "1.1"), - equalTo(SemanticAttributes.NET_PEER_NAME, "localhost"), - equalTo(SemanticAttributes.NET_PEER_PORT, url.getPort()), - equalTo(SemanticAttributes.HTTP_URL, url.toString()), - equalTo(SemanticAttributes.HTTP_METHOD, "GET"), - equalTo(SemanticAttributes.HTTP_STATUS_CODE, STATUS), - satisfies( - SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH, - AbstractLongAssert::isNotNegative)), + .hasAttributesSatisfyingExactly(attributes), span -> span.hasName("test-http-server").hasKind(SERVER).hasParent(trace.getSpan(1)))); @@ -216,6 +216,16 @@ public void testPostRequest() throws IOException { assertThat(lines).isEqualTo(RESPONSE); }); + List attributes = + new ArrayList<>( + Arrays.asList( + equalTo(NetworkAttributes.NETWORK_PROTOCOL_VERSION, "1.1"), + equalTo(ServerAttributes.SERVER_ADDRESS, "localhost"), + equalTo(ServerAttributes.SERVER_PORT, url.getPort()), + equalTo(UrlAttributes.URL_FULL, url.toString()), + equalTo(HttpAttributes.HTTP_REQUEST_METHOD, "POST"), + equalTo(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, STATUS))); + testing.waitAndAssertTraces( trace -> trace.hasSpansSatisfyingExactly( @@ -224,20 +234,7 @@ public void testPostRequest() throws IOException { span.hasName("POST") .hasKind(CLIENT) .hasParent(trace.getSpan(0)) - .hasAttributesSatisfyingExactly( - equalTo(stringKey("net.protocol.name"), "http"), - equalTo(stringKey("net.protocol.version"), "1.1"), - equalTo(SemanticAttributes.NET_PEER_NAME, "localhost"), - equalTo(SemanticAttributes.NET_PEER_PORT, url.getPort()), - equalTo(SemanticAttributes.HTTP_URL, url.toString()), - equalTo(SemanticAttributes.HTTP_METHOD, "POST"), - equalTo(SemanticAttributes.HTTP_STATUS_CODE, STATUS), - satisfies( - SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH, - AbstractLongAssert::isNotNegative), - satisfies( - SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH, - AbstractLongAssert::isNotNegative)), + .hasAttributesSatisfyingExactly(attributes), span -> span.hasName("test-http-server").hasKind(SERVER).hasParent(trace.getSpan(1)))); } @@ -272,6 +269,16 @@ public void getOutputStreamShouldTransformGetIntoPost() throws IOException { assertThat(lines).isEqualTo(RESPONSE); }); + List attributes = + new ArrayList<>( + Arrays.asList( + equalTo(NetworkAttributes.NETWORK_PROTOCOL_VERSION, "1.1"), + equalTo(ServerAttributes.SERVER_ADDRESS, "localhost"), + equalTo(ServerAttributes.SERVER_PORT, url.getPort()), + equalTo(UrlAttributes.URL_FULL, url.toString()), + equalTo(HttpAttributes.HTTP_REQUEST_METHOD, "POST"), + equalTo(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, STATUS))); + testing.waitAndAssertTraces( trace -> trace.hasSpansSatisfyingExactly( @@ -280,21 +287,55 @@ public void getOutputStreamShouldTransformGetIntoPost() throws IOException { span.hasName("POST") .hasKind(CLIENT) .hasParent(trace.getSpan(0)) - .hasAttributesSatisfyingExactly( - equalTo(stringKey("net.protocol.name"), "http"), - equalTo(stringKey("net.protocol.version"), "1.1"), - equalTo(SemanticAttributes.NET_PEER_NAME, "localhost"), - equalTo(SemanticAttributes.NET_PEER_PORT, url.getPort()), - equalTo(SemanticAttributes.HTTP_URL, url.toString()), - equalTo(SemanticAttributes.HTTP_METHOD, "POST"), - equalTo(SemanticAttributes.HTTP_STATUS_CODE, STATUS), - satisfies( - SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH, - AbstractLongAssert::isNotNegative), - satisfies( - SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH, - AbstractLongAssert::isNotNegative)), + .hasAttributesSatisfyingExactly(attributes), span -> span.hasName("test-http-server").hasKind(SERVER).hasParent(trace.getSpan(1)))); } + + @ParameterizedTest + @ValueSource(strings = {"http", "https"}) + public void traceRequestWithConnectionFailure(String scheme) { + String uri = scheme + "://localhost:" + PortUtils.UNUSABLE_PORT; + + Throwable thrown = + catchThrowable( + () -> + testing.runWithSpan( + "someTrace", + () -> { + URL url = new URI(uri).toURL(); + URLConnection connection = url.openConnection(); + connection.setConnectTimeout(10000); + connection.setReadTimeout(10000); + assertThat(Span.current().getSpanContext().isValid()).isTrue(); + connection.getInputStream(); + })); + + List attributes = + new ArrayList<>( + Arrays.asList( + equalTo(NetworkAttributes.NETWORK_PROTOCOL_VERSION, "1.1"), + equalTo(ServerAttributes.SERVER_ADDRESS, "localhost"), + equalTo(ServerAttributes.SERVER_PORT, PortUtils.UNUSABLE_PORT), + equalTo(UrlAttributes.URL_FULL, uri), + equalTo(HttpAttributes.HTTP_REQUEST_METHOD, "GET"), + equalTo(ErrorAttributes.ERROR_TYPE, "java.net.ConnectException"))); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("someTrace") + .hasKind(INTERNAL) + .hasNoParent() + .hasStatus(StatusData.error()) + .hasException(thrown), + span -> + span.hasName("GET") + .hasKind(CLIENT) + .hasParent(trace.getSpan(0)) + .hasStatus(StatusData.error()) + .hasException(thrown) + .hasAttributesSatisfyingExactly(attributes))); + } } diff --git a/instrumentation/http-url-connection/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/httpurlconnection/HttpUrlConnectionUseCachesFalseTest.java b/instrumentation/http-url-connection/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/httpurlconnection/HttpUrlConnectionUseCachesFalseTest.java index 63abc43fdf49..313121e8198a 100644 --- a/instrumentation/http-url-connection/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/httpurlconnection/HttpUrlConnectionUseCachesFalseTest.java +++ b/instrumentation/http-url-connection/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/httpurlconnection/HttpUrlConnectionUseCachesFalseTest.java @@ -45,7 +45,10 @@ public int sendRequest( Span parentSpan = Span.current(); InputStream stream = connection.getInputStream(); assertThat(Span.current()).isEqualTo(parentSpan); - readLines(stream); + // skip reading body of long-request to make the test a bit faster + if (!uri.toString().contains("/long-request")) { + readLines(stream); + } stream.close(); return connection.getResponseCode(); } finally { @@ -60,5 +63,6 @@ protected void configure(HttpClientTestOptions.Builder optionsBuilder) { // HttpURLConnection can't be reused optionsBuilder.disableTestReusedRequest(); optionsBuilder.disableTestCallback(); + optionsBuilder.disableTestNonStandardHttpMethod(); } } diff --git a/instrumentation/http-url-connection/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/httpurlconnection/UrlConnectionTest.java b/instrumentation/http-url-connection/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/httpurlconnection/UrlConnectionTest.java deleted file mode 100644 index 7ec7d967be1f..000000000000 --- a/instrumentation/http-url-connection/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/httpurlconnection/UrlConnectionTest.java +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.javaagent.instrumentation.httpurlconnection; - -import static io.opentelemetry.api.common.AttributeKey.stringKey; -import static io.opentelemetry.api.trace.SpanKind.CLIENT; -import static io.opentelemetry.api.trace.SpanKind.INTERNAL; -import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.catchThrowable; - -import io.opentelemetry.api.trace.Span; -import io.opentelemetry.instrumentation.test.utils.PortUtils; -import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; -import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; -import io.opentelemetry.sdk.trace.data.StatusData; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; -import java.net.URI; -import java.net.URL; -import java.net.URLConnection; -import org.junit.jupiter.api.extension.RegisterExtension; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; - -class UrlConnectionTest { - @RegisterExtension - static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); - - @ParameterizedTest - @ValueSource(strings = {"http", "https"}) - public void traceRequestWithConnectionFailure(String scheme) { - String uri = scheme + "://localhost:" + PortUtils.UNUSABLE_PORT; - - Throwable thrown = - catchThrowable( - () -> - testing.runWithSpan( - "someTrace", - () -> { - URL url = new URI(uri).toURL(); - URLConnection connection = url.openConnection(); - connection.setConnectTimeout(10000); - connection.setReadTimeout(10000); - assertThat(Span.current().getSpanContext().isValid()).isTrue(); - connection.getInputStream(); - })); - - testing.waitAndAssertTraces( - trace -> - trace.hasSpansSatisfyingExactly( - span -> - span.hasName("someTrace") - .hasKind(INTERNAL) - .hasNoParent() - .hasStatus(StatusData.error()) - .hasException(thrown), - span -> - span.hasName("GET") - .hasKind(CLIENT) - .hasParent(trace.getSpan(0)) - .hasStatus(StatusData.error()) - .hasException(thrown) - .hasAttributesSatisfyingExactly( - equalTo(stringKey("net.protocol.name"), "http"), - equalTo(stringKey("net.protocol.version"), "1.1"), - equalTo(SemanticAttributes.NET_PEER_NAME, "localhost"), - equalTo(SemanticAttributes.NET_PEER_PORT, PortUtils.UNUSABLE_PORT), - equalTo(SemanticAttributes.HTTP_URL, uri), - equalTo(SemanticAttributes.HTTP_METHOD, "GET")))); - } -} diff --git a/instrumentation/hystrix-1.4/javaagent/README.md b/instrumentation/hystrix-1.4/javaagent/README.md index 1aca02952634..4ae238ad48e1 100644 --- a/instrumentation/hystrix-1.4/javaagent/README.md +++ b/instrumentation/hystrix-1.4/javaagent/README.md @@ -1,5 +1,5 @@ # Settings for the Hystrix instrumentation -| System property | Type | Default | Description | -|---|---|---|---| +| System property | Type | Default | Description | +| ----------------------------------------------------------- | ------- | ------- | --------------------------------------------------- | | `otel.instrumentation.hystrix.experimental-span-attributes` | Boolean | `false` | Enable the capture of experimental span attributes. | diff --git a/instrumentation/hystrix-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hystrix/HystrixInstrumentationModule.java b/instrumentation/hystrix-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hystrix/HystrixInstrumentationModule.java index e60c6cc0e80b..c58f5c48d8aa 100644 --- a/instrumentation/hystrix-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hystrix/HystrixInstrumentationModule.java +++ b/instrumentation/hystrix-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hystrix/HystrixInstrumentationModule.java @@ -10,10 +10,12 @@ 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.internal.ExperimentalInstrumentationModule; import java.util.List; @AutoService(InstrumentationModule.class) -public class HystrixInstrumentationModule extends InstrumentationModule { +public class HystrixInstrumentationModule extends InstrumentationModule + implements ExperimentalInstrumentationModule { public HystrixInstrumentationModule() { super("hystrix", "hystrix-1.4"); @@ -28,4 +30,9 @@ public boolean isHelperClass(String className) { public List typeInstrumentations() { return singletonList(new HystrixCommandInstrumentation()); } + + @Override + public List injectedClassNames() { + return singletonList("rx.__OpenTelemetryTracingUtil"); + } } diff --git a/instrumentation/hystrix-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hystrix/HystrixSingletons.java b/instrumentation/hystrix-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hystrix/HystrixSingletons.java index 19848e54d7cc..8a5ea9b501c4 100644 --- a/instrumentation/hystrix-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hystrix/HystrixSingletons.java +++ b/instrumentation/hystrix-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hystrix/HystrixSingletons.java @@ -8,7 +8,7 @@ import io.opentelemetry.api.GlobalOpenTelemetry; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; import io.opentelemetry.instrumentation.api.instrumenter.InstrumenterBuilder; -import io.opentelemetry.javaagent.bootstrap.internal.InstrumentationConfig; +import io.opentelemetry.javaagent.bootstrap.internal.AgentInstrumentationConfig; public final class HystrixSingletons { @@ -21,7 +21,7 @@ public final class HystrixSingletons { Instrumenter.builder( GlobalOpenTelemetry.get(), INSTRUMENTATION_NAME, HystrixRequest::spanName); - if (InstrumentationConfig.get() + if (AgentInstrumentationConfig.get() .getBoolean("otel.instrumentation.hystrix.experimental-span-attributes", false)) { builder.addAttributesExtractor(new ExperimentalAttributesExtractor()); } diff --git a/instrumentation/hystrix-1.4/javaagent/src/test/groovy/HystrixObservableChainTest.groovy b/instrumentation/hystrix-1.4/javaagent/src/test/groovy/HystrixObservableChainTest.groovy deleted file mode 100644 index 02a2957c2f3c..000000000000 --- a/instrumentation/hystrix-1.4/javaagent/src/test/groovy/HystrixObservableChainTest.groovy +++ /dev/null @@ -1,108 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -import com.netflix.hystrix.HystrixCommandProperties -import com.netflix.hystrix.HystrixObservableCommand -import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification -import rx.Observable -import rx.schedulers.Schedulers - -import static com.netflix.hystrix.HystrixCommandGroupKey.Factory.asKey - -class HystrixObservableChainTest extends AgentInstrumentationSpecification { - - def "test command #action"() { - setup: - - def result = runWithSpan("parent") { - def val = new HystrixObservableCommand(setter("ExampleGroup")) { - private String tracedMethod() { - runWithSpan("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(setter("OtherGroup")) { - private String anotherTracedMethod() { - runWithSpan("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 { - } - } - } - } - } - - def setter(String key) { - def setter = new HystrixObservableCommand.Setter(asKey(key)) - setter.andCommandPropertiesDefaults(new HystrixCommandProperties.Setter() - .withExecutionTimeoutInMilliseconds(10_000)) - return setter - } -} diff --git a/instrumentation/hystrix-1.4/javaagent/src/test/groovy/HystrixObservableTest.groovy b/instrumentation/hystrix-1.4/javaagent/src/test/groovy/HystrixObservableTest.groovy deleted file mode 100644 index aa6506ce7235..000000000000 --- a/instrumentation/hystrix-1.4/javaagent/src/test/groovy/HystrixObservableTest.groovy +++ /dev/null @@ -1,315 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -import com.netflix.hystrix.HystrixCommandProperties -import com.netflix.hystrix.HystrixObservable -import com.netflix.hystrix.HystrixObservableCommand -import com.netflix.hystrix.exception.HystrixRuntimeException -import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification -import rx.Observable -import rx.schedulers.Schedulers - -import java.util.concurrent.BlockingQueue -import java.util.concurrent.LinkedBlockingQueue - -import static com.netflix.hystrix.HystrixCommandGroupKey.Factory.asKey -import static io.opentelemetry.api.trace.StatusCode.ERROR - -class HystrixObservableTest extends AgentInstrumentationSpecification { - - def "test command #action"() { - setup: - def observeOnFn = observeOn - def subscribeOnFn = subscribeOn - def result = runWithSpan("parent") { - def val = operation new HystrixObservableCommand(setter("ExampleGroup")) { - private String tracedMethod() { - runWithSpan("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 = runWithSpan("parent") { - def val = operation new HystrixObservableCommand(setter("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: - runWithSpan("parent") { - operation new HystrixObservableCommand(setter("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 - } - } - - def setter(String key) { - def setter = new HystrixObservableCommand.Setter(asKey(key)) - setter.andCommandPropertiesDefaults(new HystrixCommandProperties.Setter() - .withExecutionTimeoutInMilliseconds(10_000)) - return setter - } -} diff --git a/instrumentation/hystrix-1.4/javaagent/src/test/groovy/HystrixTest.groovy b/instrumentation/hystrix-1.4/javaagent/src/test/groovy/HystrixTest.groovy deleted file mode 100644 index f4451da2672e..000000000000 --- a/instrumentation/hystrix-1.4/javaagent/src/test/groovy/HystrixTest.groovy +++ /dev/null @@ -1,148 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -import com.netflix.hystrix.HystrixCommand -import com.netflix.hystrix.HystrixCommandProperties -import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification - -import java.util.concurrent.BlockingQueue -import java.util.concurrent.LinkedBlockingQueue - -import static com.netflix.hystrix.HystrixCommandGroupKey.Factory.asKey -import static io.opentelemetry.api.trace.StatusCode.ERROR - -class HystrixTest extends AgentInstrumentationSpecification { - - def "test command #action"() { - setup: - def command = new HystrixCommand(setter("ExampleGroup")) { - @Override - protected String run() throws Exception { - return tracedMethod() - } - - private String tracedMethod() { - runWithSpan("tracedMethod") {} - return "Hello!" - } - } - def result = runWithSpan("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(setter("ExampleGroup")) { - @Override - protected String run() throws Exception { - throw new IllegalArgumentException() - } - - protected String getFallback() { - return "Fallback!" - } - } - def result = runWithSpan("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() - } - } - - def setter(String key) { - def setter = new HystrixCommand.Setter(asKey(key)) - setter.andCommandPropertiesDefaults(new HystrixCommandProperties.Setter() - .withExecutionTimeoutInMilliseconds(10_000)) - return setter - } -} diff --git a/instrumentation/hystrix-1.4/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/hystrix/HystrixObservableChainTest.java b/instrumentation/hystrix-1.4/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/hystrix/HystrixObservableChainTest.java new file mode 100644 index 000000000000..89eef213fb96 --- /dev/null +++ b/instrumentation/hystrix-1.4/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/hystrix/HystrixObservableChainTest.java @@ -0,0 +1,123 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.hystrix; + +import static io.opentelemetry.api.common.AttributeKey.booleanKey; +import static io.opentelemetry.api.common.AttributeKey.stringKey; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; +import static org.assertj.core.api.Assertions.assertThat; + +import com.netflix.hystrix.HystrixCommandGroupKey; +import com.netflix.hystrix.HystrixCommandProperties; +import com.netflix.hystrix.HystrixObservableCommand; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import rx.Observable; +import rx.schedulers.Schedulers; + +class HystrixObservableChainTest { + + @RegisterExtension + protected static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + @Test + @SuppressWarnings("RxReturnValueIgnored") + void testCommand() { + + class TestCommand extends HystrixObservableCommand { + protected TestCommand(Setter setter) { + super(setter); + } + + private String tracedMethod() { + testing.runWithSpan("tracedMethod", () -> {}); + return "Hello"; + } + + @Override + protected Observable construct() { + return Observable.defer(() -> Observable.just(tracedMethod())) + .subscribeOn(Schedulers.immediate()); + } + } + + class AnotherTestCommand extends HystrixObservableCommand { + private final String str; + + protected AnotherTestCommand(Setter setter, String str) { + super(setter); + this.str = str; + } + + private String anotherTracedMethod() { + testing.runWithSpan("anotherTracedMethod", () -> {}); + return str + "!"; + } + + @Override + protected Observable construct() { + return Observable.defer(() -> Observable.just(anotherTracedMethod())) + .subscribeOn(Schedulers.computation()); + } + } + + String result = + testing.runWithSpan( + "parent", + () -> + new TestCommand(setter("ExampleGroup")) + .toObservable() + .subscribeOn(Schedulers.io()) + .map(String::toUpperCase) + .flatMap( + str -> + new AnotherTestCommand(setter("OtherGroup"), str) + .toObservable() + .subscribeOn(Schedulers.trampoline())) + .toBlocking() + .first()); + + assertThat(result).isEqualTo("HELLO!"); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasNoParent().hasAttributes(Attributes.empty()), + span -> + span.hasName("ExampleGroup.TestCommand.execute") + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo(stringKey("hystrix.command"), "TestCommand"), + equalTo(stringKey("hystrix.group"), "ExampleGroup"), + equalTo(booleanKey("hystrix.circuit_open"), false)), + span -> + span.hasName("tracedMethod") + .hasParent(trace.getSpan(1)) + .hasAttributes(Attributes.empty()), + span -> + span.hasName("OtherGroup.AnotherTestCommand.execute") + .hasParent(trace.getSpan(1)) + .hasAttributesSatisfyingExactly( + equalTo(stringKey("hystrix.command"), "AnotherTestCommand"), + equalTo(stringKey("hystrix.group"), "OtherGroup"), + equalTo(booleanKey("hystrix.circuit_open"), false)), + span -> + span.hasName("anotherTracedMethod") + .hasParent(trace.getSpan(3)) + .hasAttributes(Attributes.empty()))); + } + + private static HystrixObservableCommand.Setter setter(String key) { + HystrixObservableCommand.Setter setter = + HystrixObservableCommand.Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey(key)); + setter.andCommandPropertiesDefaults( + HystrixCommandProperties.Setter().withExecutionTimeoutInMilliseconds(10_000)); + return setter; + } +} diff --git a/instrumentation/hystrix-1.4/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/hystrix/HystrixObservableTest.java b/instrumentation/hystrix-1.4/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/hystrix/HystrixObservableTest.java new file mode 100644 index 000000000000..fdac6e9b09b0 --- /dev/null +++ b/instrumentation/hystrix-1.4/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/hystrix/HystrixObservableTest.java @@ -0,0 +1,466 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.hystrix; + +import static io.opentelemetry.api.common.AttributeKey.booleanKey; +import static io.opentelemetry.api.common.AttributeKey.stringKey; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.catchException; +import static org.junit.jupiter.api.Named.named; + +import com.netflix.hystrix.HystrixCommandGroupKey; +import com.netflix.hystrix.HystrixCommandProperties; +import com.netflix.hystrix.HystrixObservableCommand; +import com.netflix.hystrix.exception.HystrixRuntimeException; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.sdk.trace.data.StatusData; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.function.Function; +import java.util.stream.Stream; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import rx.Observable; +import rx.Scheduler; +import rx.Subscription; +import rx.schedulers.Schedulers; + +class HystrixObservableTest { + + @RegisterExtension + protected static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + @ParameterizedTest + @MethodSource("provideCommandActionArguments") + void testCommands(Parameter parameter) { + + class TestCommand extends HystrixObservableCommand { + protected TestCommand(Setter setter) { + super(setter); + } + + private String tracedMethod() { + testing.runWithSpan("tracedMethod", () -> {}); + return "Hello!"; + } + + @Override + protected Observable construct() { + Observable obs = Observable.defer(() -> Observable.just(tracedMethod()).repeat(1)); + if (parameter.observeOn != null) { + obs = obs.observeOn(parameter.observeOn); + } + if (parameter.subscribeOn != null) { + obs = obs.subscribeOn(parameter.subscribeOn); + } + return obs; + } + } + + String result = + testing.runWithSpan( + "parent", + () -> { + HystrixObservableCommand val = new TestCommand(setter("ExampleGroup")); + return parameter.operation.apply(val); + }); + + assertThat(result).isEqualTo("Hello!"); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasNoParent().hasAttributes(Attributes.empty()), + span -> + span.hasName("ExampleGroup.TestCommand.execute") + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo(stringKey("hystrix.command"), "TestCommand"), + equalTo(stringKey("hystrix.group"), "ExampleGroup"), + equalTo(booleanKey("hystrix.circuit_open"), false)), + span -> + span.hasName("tracedMethod") + .hasParent(trace.getSpan(1)) + .hasAttributes(Attributes.empty()))); + } + + private static Stream baseArguments() { + return Stream.of( + Arguments.of( + named( + "toObservable", + new Parameter(null, null, cmd -> cmd.toObservable().toBlocking().first()))), + Arguments.of( + named( + "toObservable-I", + new Parameter( + Schedulers.immediate(), null, cmd -> cmd.toObservable().toBlocking().first()))), + Arguments.of( + named( + "toObservable-T", + new Parameter( + Schedulers.trampoline(), + null, + cmd -> cmd.toObservable().toBlocking().first()))), + Arguments.of( + named( + "toObservable-C", + new Parameter( + Schedulers.computation(), + null, + cmd -> cmd.toObservable().toBlocking().first()))), + Arguments.of( + named( + "toObservable-IO", + new Parameter( + Schedulers.io(), null, cmd -> cmd.toObservable().toBlocking().first()))), + Arguments.of( + named( + "toObservable-NT", + new Parameter( + Schedulers.newThread(), null, cmd -> cmd.toObservable().toBlocking().first()))), + Arguments.of( + named( + "toObservable+I", + new Parameter( + null, Schedulers.immediate(), cmd -> cmd.toObservable().toBlocking().first()))), + Arguments.of( + named( + "toObservable+T", + new Parameter( + null, + Schedulers.trampoline(), + cmd -> cmd.toObservable().toBlocking().first()))), + Arguments.of( + named( + "toObservable+C", + new Parameter( + null, + Schedulers.computation(), + cmd -> cmd.toObservable().toBlocking().first()))), + Arguments.of( + named( + "toObservable+IO", + new Parameter( + null, Schedulers.io(), cmd -> cmd.toObservable().toBlocking().first()))), + Arguments.of( + named( + "toObservable+NT", + new Parameter( + null, Schedulers.newThread(), cmd -> cmd.toObservable().toBlocking().first()))), + Arguments.of( + named("observe", new Parameter(null, null, cmd -> cmd.observe().toBlocking().first()))), + Arguments.of( + named( + "observe-I", + new Parameter( + Schedulers.immediate(), null, cmd -> cmd.observe().toBlocking().first()))), + Arguments.of( + named( + "observe-T", + new Parameter( + Schedulers.trampoline(), null, cmd -> cmd.observe().toBlocking().first()))), + Arguments.of( + named( + "observe-C", + new Parameter( + Schedulers.computation(), null, cmd -> cmd.observe().toBlocking().first()))), + Arguments.of( + named( + "observe-IO", + new Parameter(Schedulers.io(), null, cmd -> cmd.observe().toBlocking().first()))), + Arguments.of( + named( + "observe-NT", + new Parameter( + Schedulers.newThread(), null, cmd -> cmd.observe().toBlocking().first()))), + Arguments.of( + named( + "observe+I", + new Parameter( + null, Schedulers.immediate(), cmd -> cmd.observe().toBlocking().first()))), + Arguments.of( + named( + "observe+T", + new Parameter( + null, Schedulers.trampoline(), cmd -> cmd.observe().toBlocking().first()))), + Arguments.of( + named( + "observe+C", + new Parameter( + null, Schedulers.computation(), cmd -> cmd.observe().toBlocking().first()))), + Arguments.of( + named( + "observe+IO", + new Parameter(null, Schedulers.io(), cmd -> cmd.observe().toBlocking().first()))), + Arguments.of( + named( + "observe+NT", + new Parameter( + null, Schedulers.newThread(), cmd -> cmd.observe().toBlocking().first())))); + } + + private static Stream provideCommandActionArguments() { + return Stream.concat( + baseArguments(), + Stream.of( + Arguments.of( + named( + "toObservable block", + new Parameter( + Schedulers.computation(), + Schedulers.newThread(), + cmd -> { + BlockingQueue queue = new LinkedBlockingQueue<>(); + Subscription subscription = + cmd.toObservable() + .subscribe( + next -> { + try { + queue.put(next); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + }); + String returnValue; + try { + returnValue = queue.take(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + subscription.unsubscribe(); + return returnValue; + }))))); + } + + @ParameterizedTest + @MethodSource("provideCommandFallbackArguments") + void testCommandFallbacks(Parameter parameter) { + + class TestCommand extends HystrixObservableCommand { + protected TestCommand(Setter setter) { + super(setter); + } + + @Override + protected Observable construct() { + Observable err = + Observable.defer(() -> Observable.error(new IllegalArgumentException())); + if (parameter.observeOn != null) { + err = err.observeOn(parameter.observeOn); + } + if (parameter.subscribeOn != null) { + err = err.subscribeOn(parameter.subscribeOn); + } + return err; + } + + @Override + protected Observable resumeWithFallback() { + return Observable.just("Fallback!").repeat(1); + } + } + + String result = + testing.runWithSpan( + "parent", + () -> { + HystrixObservableCommand val = new TestCommand(setter("ExampleGroup")); + return parameter.operation.apply(val); + }); + + assertThat(result).isEqualTo("Fallback!"); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasNoParent().hasAttributes(Attributes.empty()), + span -> + span.hasName("ExampleGroup.TestCommand.execute") + .hasParent(trace.getSpan(0)) + .hasStatus(StatusData.error()) + .hasException(new IllegalArgumentException()), + span -> + span.hasName("ExampleGroup.TestCommand.fallback") + .hasParent(trace.getSpan(1)) + .hasAttributesSatisfyingExactly( + equalTo(stringKey("hystrix.command"), "TestCommand"), + equalTo(stringKey("hystrix.group"), "ExampleGroup"), + equalTo(booleanKey("hystrix.circuit_open"), false)))); + } + + private static Stream provideCommandFallbackArguments() { + return Stream.concat( + baseArguments(), + Stream.of( + Arguments.of( + named( + "toObservable block", + new Parameter( + null, + null, + cmd -> { + BlockingQueue queue = new LinkedBlockingQueue<>(); + Subscription subscription = + cmd.toObservable() + .subscribe( + next -> { + try { + queue.put(next); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + }); + String returnValue; + try { + returnValue = queue.take(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + subscription.unsubscribe(); + return returnValue; + }))))); + } + + @ParameterizedTest + @MethodSource("provideCommandNoFallbackResultsInErrorArguments") + void testNoFallbackResultsInErrorForAction(Parameter parameter) { + + class TestCommand extends HystrixObservableCommand { + protected TestCommand(Setter setter) { + super(setter); + } + + @Override + protected Observable construct() { + Observable err = + Observable.defer(() -> Observable.error(new IllegalArgumentException())); + if (parameter.observeOn != null) { + err = err.observeOn(parameter.observeOn); + } + if (parameter.subscribeOn != null) { + err = err.subscribeOn(parameter.subscribeOn); + } + return err; + } + } + + Throwable exception = + catchException( + () -> + testing.runWithSpan( + "parent", + () -> { + HystrixObservableCommand val = + new TestCommand(setter("FailingGroup")); + return parameter.operation.apply(val); + })); + + assertThat(exception) + .isInstanceOf(HystrixRuntimeException.class) + .hasCauseInstanceOf(IllegalArgumentException.class); + HystrixRuntimeException hystrixRuntimeException = (HystrixRuntimeException) exception; + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("parent") + .hasNoParent() + .hasStatus(StatusData.error()) + .hasException(exception), + span -> + span.hasName("FailingGroup.TestCommand.execute") + .hasParent(trace.getSpan(0)) + .hasStatus(StatusData.error()) + .hasException(exception.getCause()) + .hasAttributesSatisfyingExactly( + equalTo(stringKey("hystrix.command"), "TestCommand"), + equalTo(stringKey("hystrix.group"), "FailingGroup"), + equalTo(booleanKey("hystrix.circuit_open"), false)), + span -> + span.hasName("FailingGroup.TestCommand.fallback") + .hasParent(trace.getSpan(1)) + .hasException(hystrixRuntimeException.getFallbackException()) + .hasAttributesSatisfyingExactly( + equalTo(stringKey("hystrix.command"), "TestCommand"), + equalTo(stringKey("hystrix.group"), "FailingGroup"), + equalTo(booleanKey("hystrix.circuit_open"), false)))); + } + + private static Stream provideCommandNoFallbackResultsInErrorArguments() { + return Stream.concat( + baseArguments(), + Stream.of( + Arguments.of( + named( + "toObservable block", + new Parameter( + Schedulers.computation(), + Schedulers.newThread(), + cmd -> { + BlockingQueue queue = new LinkedBlockingQueue<>(); + Subscription subscription = + cmd.toObservable() + .subscribe( + next -> { + try { + queue.put(new Exception("Unexpectedly got a next")); + } catch (InterruptedException e) { + throw new IllegalArgumentException(e); + } + }, + next -> { + try { + queue.put(next); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + }); + Throwable returnValue; + try { + returnValue = queue.take(); + + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + subscription.unsubscribe(); + try { + throw returnValue; + } catch (Throwable e) { + throw (HystrixRuntimeException) e; + } + }))))); + } + + private static HystrixObservableCommand.Setter setter(String key) { + HystrixObservableCommand.Setter setter = + HystrixObservableCommand.Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey(key)); + setter.andCommandPropertiesDefaults( + HystrixCommandProperties.Setter().withExecutionTimeoutInMilliseconds(10_000)); + return setter; + } + + private static class Parameter { + public final Scheduler observeOn; + public final Scheduler subscribeOn; + public final Function, String> operation; + + public Parameter( + Scheduler observeOn, + Scheduler subscribeOn, + Function, String> operation) { + this.observeOn = observeOn; + this.subscribeOn = subscribeOn; + this.operation = operation; + } + } +} diff --git a/instrumentation/hystrix-1.4/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/hystrix/HystrixTest.java b/instrumentation/hystrix-1.4/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/hystrix/HystrixTest.java new file mode 100644 index 000000000000..bd8adba9f017 --- /dev/null +++ b/instrumentation/hystrix-1.4/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/hystrix/HystrixTest.java @@ -0,0 +1,180 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.hystrix; + +import static io.opentelemetry.api.common.AttributeKey.booleanKey; +import static io.opentelemetry.api.common.AttributeKey.stringKey; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Named.named; + +import com.netflix.hystrix.HystrixCommand; +import com.netflix.hystrix.HystrixCommandGroupKey; +import com.netflix.hystrix.HystrixCommandProperties; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.sdk.trace.data.StatusData; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.function.Function; +import java.util.stream.Stream; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +class HystrixTest { + + @RegisterExtension + protected static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + @ParameterizedTest + @MethodSource("provideCommandActionArguments") + void testCommands(Function, String> operation) { + class TestCommand extends HystrixCommand { + protected TestCommand(Setter setter) { + super(setter); + } + + @Override + protected String run() throws Exception { + return tracedMethod(); + } + + private String tracedMethod() { + testing.runWithSpan("tracedMethod", () -> {}); + return "Hello!"; + } + } + + HystrixCommand command = new TestCommand(setter()); + + String result = testing.runWithSpan("parent", () -> operation.apply(command)); + assertThat(result).isEqualTo("Hello!"); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasNoParent().hasAttributes(Attributes.empty()), + span -> + span.hasName("ExampleGroup.TestCommand.execute") + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo(stringKey("hystrix.command"), "TestCommand"), + equalTo(stringKey("hystrix.group"), "ExampleGroup"), + equalTo(booleanKey("hystrix.circuit_open"), false)), + span -> + span.hasName("tracedMethod") + .hasParent(trace.getSpan(1)) + .hasAttributes(Attributes.empty()))); + } + + @ParameterizedTest + @MethodSource("provideCommandActionArguments") + void testCommandFallbacks(Function, String> operation) { + class TestCommand extends HystrixCommand { + protected TestCommand(Setter setter) { + super(setter); + } + + @Override + protected String run() throws Exception { + throw new IllegalArgumentException(); + } + + @Override + protected String getFallback() { + return "Fallback!"; + } + } + + HystrixCommand command = new TestCommand(setter()); + + String result = testing.runWithSpan("parent", () -> operation.apply(command)); + assertThat(result).isEqualTo("Fallback!"); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasNoParent().hasAttributes(Attributes.empty()), + span -> + span.hasName("ExampleGroup.TestCommand.execute") + .hasParent(trace.getSpan(0)) + .hasStatus(StatusData.error()) + .hasException(new IllegalArgumentException()) + .hasAttributesSatisfyingExactly( + equalTo(stringKey("hystrix.command"), "TestCommand"), + equalTo(stringKey("hystrix.group"), "ExampleGroup"), + equalTo(booleanKey("hystrix.circuit_open"), false)), + span -> + span.hasName("ExampleGroup.TestCommand.fallback") + .hasParent(trace.getSpan(1)) + .hasAttributesSatisfyingExactly( + equalTo(stringKey("hystrix.command"), "TestCommand"), + equalTo(stringKey("hystrix.group"), "ExampleGroup"), + equalTo(booleanKey("hystrix.circuit_open"), false)))); + } + + private static Stream provideCommandActionArguments() { + return Stream.of( + Arguments.of( + named("execute", (Function, String>) HystrixCommand::execute)), + Arguments.of( + named( + "queue", + (Function, String>) + cmd -> { + try { + return cmd.queue().get(); + } catch (InterruptedException | ExecutionException e) { + throw new RuntimeException(e); + } + })), + Arguments.of( + named( + "toObservable", + (Function, String>) + cmd -> cmd.toObservable().toBlocking().first())), + Arguments.of( + named( + "observe", + (Function, String>) + cmd -> cmd.observe().toBlocking().first())), + Arguments.of( + named( + "observe block", + (Function, String>) + cmd -> { + BlockingQueue queue = new LinkedBlockingQueue<>(); + cmd.observe() + .subscribe( + next -> { + try { + queue.put(next); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + }); + String returnValue; + try { + returnValue = queue.take(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + return returnValue; + }))); + } + + private static HystrixCommand.Setter setter() { + HystrixCommand.Setter setter = + HystrixCommand.Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("ExampleGroup")); + setter.andCommandPropertiesDefaults( + HystrixCommandProperties.Setter().withExecutionTimeoutInMilliseconds(10_000)); + return setter; + } +} diff --git a/instrumentation/influxdb-2.4/javaagent/build.gradle.kts b/instrumentation/influxdb-2.4/javaagent/build.gradle.kts new file mode 100644 index 000000000000..ab93583d3e03 --- /dev/null +++ b/instrumentation/influxdb-2.4/javaagent/build.gradle.kts @@ -0,0 +1,54 @@ +plugins { + id("otel.javaagent-instrumentation") +} + +muzzle { + pass { + group.set("org.influxdb") + module.set("influxdb-java") + versions.set("[2.4,)") + assertInverse.set(true) + } +} + +dependencies { + compileOnly("org.influxdb:influxdb-java:2.4") + + compileOnly("com.google.auto.value:auto-value-annotations") + annotationProcessor("com.google.auto.value:auto-value") + + testInstrumentation(project(":instrumentation:okhttp:okhttp-3.0:javaagent")) + + // we use methods that weren't present before 2.14 in tests + testLibrary("org.influxdb:influxdb-java:2.14") +} + +testing { + suites { + val test24 by registering(JvmTestSuite::class) { + dependencies { + implementation(project()) + implementation("org.influxdb:influxdb-java:2.4") + implementation("org.testcontainers:testcontainers") + } + } + } +} + +tasks { + test { + usesService(gradle.sharedServices.registrations["testcontainersBuildService"].service) + } + + if (!(findProperty("testLatestDeps") as Boolean)) { + check { + dependsOn(testing.suites) + } + } +} + +tasks.withType().configureEach { + // we disable the okhttp instrumentation, so we don't need to assert on the okhttp spans + // from the okhttp instrumentation we need OkHttp3IgnoredTypesConfigurer to fix context leaks + jvmArgs("-Dotel.instrumentation.okhttp.enabled=false") +} diff --git a/instrumentation/influxdb-2.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/influxdb/v2_4/InfluxDbAttributesGetter.java b/instrumentation/influxdb-2.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/influxdb/v2_4/InfluxDbAttributesGetter.java new file mode 100644 index 000000000000..c73bf348583b --- /dev/null +++ b/instrumentation/influxdb-2.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/influxdb/v2_4/InfluxDbAttributesGetter.java @@ -0,0 +1,51 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.influxdb.v2_4; + +import io.opentelemetry.instrumentation.api.incubator.semconv.db.DbClientAttributesGetter; +import javax.annotation.Nullable; + +final class InfluxDbAttributesGetter implements DbClientAttributesGetter { + + @Nullable + @Override + public String getStatement(InfluxDbRequest request) { + return request.getSqlStatementInfo().getFullStatement(); + } + + @Nullable + @Override + public String getOperation(InfluxDbRequest request) { + if (request.getOperation() != null) { + return request.getOperation(); + } + return request.getSqlStatementInfo().getOperation(); + } + + @Override + public String getSystem(InfluxDbRequest request) { + return "influxdb"; + } + + @Nullable + @Override + public String getUser(InfluxDbRequest request) { + return null; + } + + @Nullable + @Override + public String getName(InfluxDbRequest request) { + String dbName = request.getDbName(); + return "".equals(dbName) ? null : dbName; + } + + @Nullable + @Override + public String getConnectionString(InfluxDbRequest influxDbRequest) { + return null; + } +} diff --git a/instrumentation/influxdb-2.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/influxdb/v2_4/InfluxDbImplInstrumentation.java b/instrumentation/influxdb-2.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/influxdb/v2_4/InfluxDbImplInstrumentation.java new file mode 100644 index 000000000000..72e5ee14bb5f --- /dev/null +++ b/instrumentation/influxdb-2.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/influxdb/v2_4/InfluxDbImplInstrumentation.java @@ -0,0 +1,173 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.influxdb.v2_4; + +import static io.opentelemetry.javaagent.bootstrap.Java8BytecodeBridge.currentContext; +import static io.opentelemetry.javaagent.instrumentation.influxdb.v2_4.InfluxDbSingletons.instrumenter; +import static net.bytebuddy.matcher.ElementMatchers.isEnum; +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.javaagent.bootstrap.CallDepth; +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 okhttp3.HttpUrl; +import org.influxdb.dto.BatchPoints; +import org.influxdb.dto.Query; +import org.influxdb.impl.InfluxDBImpl; +import retrofit2.Retrofit; + +public class InfluxDbImplInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("org.influxdb.impl.InfluxDBImpl"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod().and(named("query")).and(takesArgument(0, named("org.influxdb.dto.Query"))), + this.getClass().getName() + "$InfluxDbQueryAdvice"); + + transformer.applyAdviceToMethod( + isMethod() + .and(named("write")) + .and( + takesArguments(1) + .and(takesArgument(0, named("org.influxdb.dto.BatchPoints"))) + .or(takesArguments(2).and(takesArgument(0, int.class))) + .or( + takesArguments(4) + .and(takesArgument(0, String.class)) + .and(takesArgument(1, String.class)) + .and(takesArgument(2, isEnum()))) + .or( + takesArguments(5) + .and(takesArgument(0, String.class)) + .and(takesArgument(1, String.class)) + .and(takesArgument(2, isEnum())) + .and(takesArgument(3, named("java.util.concurrent.TimeUnit"))))), + this.getClass().getName() + "$InfluxDbModifyAdvice"); + transformer.applyAdviceToMethod( + isMethod().and(namedOneOf("createDatabase", "deleteDatabase")), + this.getClass().getName() + "$InfluxDbModifyAdvice"); + } + + @SuppressWarnings("unused") + public static class InfluxDbQueryAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + @Advice.AssignReturned.ToAllArguments(index = 0, typing = Assigner.Typing.DYNAMIC) + public static Object[] onEnter( + @Advice.AllArguments(typing = Assigner.Typing.DYNAMIC) Object[] arguments, + @Advice.FieldValue(value = "retrofit") Retrofit retrofit) { + CallDepth callDepth = CallDepth.forClass(InfluxDBImpl.class); + if (callDepth.getAndIncrement() > 0) { + return null; + } + + Query query = arguments[0] instanceof Query ? (Query) arguments[0] : null; + if (query == null) { + return null; + } + Context parentContext = currentContext(); + + HttpUrl httpUrl = retrofit.baseUrl(); + InfluxDbRequest influxDbRequest = + InfluxDbRequest.create( + httpUrl.host(), httpUrl.port(), query.getDatabase(), null, query.getCommand()); + + if (!instrumenter().shouldStart(parentContext, influxDbRequest)) { + return null; + } + + // wrap callbacks so they'd run in the context of the parent span + Object[] newArguments = new Object[arguments.length]; + for (int i = 0; i < arguments.length; i++) { + newArguments[i] = InfluxDbObjetWrapper.wrap(arguments[i], parentContext); + } + + return new Object[] {newArguments, InfluxDbScope.start(parentContext, influxDbRequest)}; + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void onExit( + @Advice.Thrown Throwable throwable, @Advice.Enter Object[] enterArgs) { + CallDepth callDepth = CallDepth.forClass(InfluxDBImpl.class); + if (callDepth.decrementAndGet() > 0 || enterArgs == null) { + return; + } + + ((InfluxDbScope) enterArgs[1]).end(throwable); + } + } + + @SuppressWarnings("unused") + public static class InfluxDbModifyAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static InfluxDbScope onEnter( + @Advice.Origin("#m") String methodName, + @Advice.Argument(0) Object arg0, + @Advice.FieldValue(value = "retrofit") Retrofit retrofit) { + CallDepth callDepth = CallDepth.forClass(InfluxDBImpl.class); + if (callDepth.getAndIncrement() > 0) { + return null; + } + + if (arg0 == null) { + return null; + } + + Context parentContext = currentContext(); + + HttpUrl httpUrl = retrofit.baseUrl(); + String database = + (arg0 instanceof BatchPoints) + ? ((BatchPoints) arg0).getDatabase() + // write data by UDP protocol, in this way, can't get database name. + : arg0 instanceof Integer ? "" : String.valueOf(arg0); + + String operation; + if ("createDatabase".equals(methodName)) { + operation = "CREATE DATABASE"; + } else if ("deleteDatabase".equals(methodName)) { + operation = "DROP DATABASE"; + } else { + operation = "WRITE"; + } + + InfluxDbRequest influxDbRequest = + InfluxDbRequest.create(httpUrl.host(), httpUrl.port(), database, operation, null); + + if (!instrumenter().shouldStart(parentContext, influxDbRequest)) { + return null; + } + + return InfluxDbScope.start(parentContext, influxDbRequest); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void onExit( + @Advice.Thrown Throwable throwable, @Advice.Enter InfluxDbScope scope) { + CallDepth callDepth = CallDepth.forClass(InfluxDBImpl.class); + if (callDepth.decrementAndGet() > 0 || scope == null) { + return; + } + + scope.end(throwable); + } + } +} diff --git a/instrumentation/influxdb-2.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/influxdb/v2_4/InfluxDbInstrumentationModule.java b/instrumentation/influxdb-2.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/influxdb/v2_4/InfluxDbInstrumentationModule.java new file mode 100644 index 000000000000..3aba153fc408 --- /dev/null +++ b/instrumentation/influxdb-2.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/influxdb/v2_4/InfluxDbInstrumentationModule.java @@ -0,0 +1,26 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.influxdb.v2_4; + +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 InfluxDbInstrumentationModule extends InstrumentationModule { + + public InfluxDbInstrumentationModule() { + super("influxdb", "influxdb-2.4"); + } + + @Override + public List typeInstrumentations() { + return singletonList(new InfluxDbImplInstrumentation()); + } +} diff --git a/instrumentation/influxdb-2.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/influxdb/v2_4/InfluxDbNetworkAttributesGetter.java b/instrumentation/influxdb-2.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/influxdb/v2_4/InfluxDbNetworkAttributesGetter.java new file mode 100644 index 000000000000..0338653cd1f9 --- /dev/null +++ b/instrumentation/influxdb-2.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/influxdb/v2_4/InfluxDbNetworkAttributesGetter.java @@ -0,0 +1,21 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.influxdb.v2_4; + +import io.opentelemetry.instrumentation.api.semconv.network.ServerAttributesGetter; + +final class InfluxDbNetworkAttributesGetter implements ServerAttributesGetter { + + @Override + public String getServerAddress(InfluxDbRequest request) { + return request.getHost(); + } + + @Override + public Integer getServerPort(InfluxDbRequest request) { + return request.getPort(); + } +} diff --git a/instrumentation/influxdb-2.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/influxdb/v2_4/InfluxDbObjetWrapper.java b/instrumentation/influxdb-2.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/influxdb/v2_4/InfluxDbObjetWrapper.java new file mode 100644 index 000000000000..7be6efe42aed --- /dev/null +++ b/instrumentation/influxdb-2.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/influxdb/v2_4/InfluxDbObjetWrapper.java @@ -0,0 +1,44 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.influxdb.v2_4; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import java.util.function.BiConsumer; +import java.util.function.Consumer; + +public final class InfluxDbObjetWrapper { + + @SuppressWarnings("unchecked") + public static Object wrap(Object object, Context parentContext) { + if (object instanceof Consumer) { + return (Consumer) + o -> { + try (Scope ignore = parentContext.makeCurrent()) { + ((Consumer) object).accept(o); + } + }; + } else if (object instanceof BiConsumer) { + return (BiConsumer) + (o1, o2) -> { + try (Scope ignore = parentContext.makeCurrent()) { + ((BiConsumer) object).accept(o1, o2); + } + }; + } else if (object instanceof Runnable) { + return (Runnable) + () -> { + try (Scope ignore = parentContext.makeCurrent()) { + ((Runnable) object).run(); + } + }; + } + + return object; + } + + private InfluxDbObjetWrapper() {} +} diff --git a/instrumentation/influxdb-2.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/influxdb/v2_4/InfluxDbRequest.java b/instrumentation/influxdb-2.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/influxdb/v2_4/InfluxDbRequest.java new file mode 100644 index 000000000000..560993c266d4 --- /dev/null +++ b/instrumentation/influxdb-2.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/influxdb/v2_4/InfluxDbRequest.java @@ -0,0 +1,35 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.influxdb.v2_4; + +import com.google.auto.value.AutoValue; +import io.opentelemetry.instrumentation.api.incubator.semconv.db.SqlStatementInfo; +import io.opentelemetry.instrumentation.api.incubator.semconv.db.SqlStatementSanitizer; +import io.opentelemetry.javaagent.bootstrap.internal.AgentCommonConfig; +import javax.annotation.Nullable; + +@AutoValue +public abstract class InfluxDbRequest { + + private static final SqlStatementSanitizer sanitizer = + SqlStatementSanitizer.create(AgentCommonConfig.get().isStatementSanitizationEnabled()); + + public static InfluxDbRequest create( + String host, int port, String dbName, String operation, String sql) { + return new AutoValue_InfluxDbRequest(host, port, dbName, operation, sanitizer.sanitize(sql)); + } + + public abstract String getHost(); + + public abstract int getPort(); + + public abstract String getDbName(); + + @Nullable + public abstract String getOperation(); + + public abstract SqlStatementInfo getSqlStatementInfo(); +} diff --git a/instrumentation/influxdb-2.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/influxdb/v2_4/InfluxDbScope.java b/instrumentation/influxdb-2.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/influxdb/v2_4/InfluxDbScope.java new file mode 100644 index 000000000000..6daaea80f727 --- /dev/null +++ b/instrumentation/influxdb-2.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/influxdb/v2_4/InfluxDbScope.java @@ -0,0 +1,39 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.influxdb.v2_4; + +import static io.opentelemetry.javaagent.instrumentation.influxdb.v2_4.InfluxDbSingletons.instrumenter; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; + +/** Container used to carry state between enter and exit advices */ +public final class InfluxDbScope { + private final InfluxDbRequest influxDbRequest; + private final Context context; + private final Scope scope; + + private InfluxDbScope(InfluxDbRequest influxDbRequest, Context context, Scope scope) { + this.influxDbRequest = influxDbRequest; + this.context = context; + this.scope = scope; + } + + public static InfluxDbScope start(Context parentContext, InfluxDbRequest influxDbRequest) { + Context context = instrumenter().start(parentContext, influxDbRequest); + return new InfluxDbScope(influxDbRequest, context, context.makeCurrent()); + } + + public void end(Throwable throwable) { + if (scope == null) { + return; + } + + scope.close(); + + instrumenter().end(context, influxDbRequest, null, throwable); + } +} diff --git a/instrumentation/influxdb-2.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/influxdb/v2_4/InfluxDbSingletons.java b/instrumentation/influxdb-2.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/influxdb/v2_4/InfluxDbSingletons.java new file mode 100644 index 000000000000..3f928f2f125e --- /dev/null +++ b/instrumentation/influxdb-2.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/influxdb/v2_4/InfluxDbSingletons.java @@ -0,0 +1,38 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.influxdb.v2_4; + +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.instrumentation.api.incubator.semconv.db.DbClientAttributesExtractor; +import io.opentelemetry.instrumentation.api.incubator.semconv.db.DbClientSpanNameExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import io.opentelemetry.instrumentation.api.instrumenter.SpanKindExtractor; +import io.opentelemetry.instrumentation.api.semconv.network.ServerAttributesExtractor; + +public final class InfluxDbSingletons { + + private static final Instrumenter INSTRUMENTER; + + static { + InfluxDbAttributesGetter dbAttributesGetter = new InfluxDbAttributesGetter(); + + INSTRUMENTER = + Instrumenter.builder( + GlobalOpenTelemetry.get(), + "io.opentelemetry.influxdb-2.4", + DbClientSpanNameExtractor.create(dbAttributesGetter)) + .addAttributesExtractor(DbClientAttributesExtractor.create(dbAttributesGetter)) + .addAttributesExtractor( + ServerAttributesExtractor.create(new InfluxDbNetworkAttributesGetter())) + .buildInstrumenter(SpanKindExtractor.alwaysClient()); + } + + public static Instrumenter instrumenter() { + return INSTRUMENTER; + } + + private InfluxDbSingletons() {} +} diff --git a/instrumentation/influxdb-2.4/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/influxdb/v2_4/InfluxDbClientTest.java b/instrumentation/influxdb-2.4/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/influxdb/v2_4/InfluxDbClientTest.java new file mode 100644 index 000000000000..40d5300fe5d8 --- /dev/null +++ b/instrumentation/influxdb-2.4/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/influxdb/v2_4/InfluxDbClientTest.java @@ -0,0 +1,331 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.influxdb.v2_4; + +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; +import static java.util.Arrays.asList; +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.sdk.testing.assertj.AttributeAssertion; +import io.opentelemetry.semconv.ServerAttributes; +import io.opentelemetry.semconv.incubating.DbIncubatingAttributes; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; +import org.influxdb.InfluxDB; +import org.influxdb.InfluxDBFactory; +import org.influxdb.dto.BatchPoints; +import org.influxdb.dto.Point; +import org.influxdb.dto.Query; +import org.influxdb.dto.QueryResult; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +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.RegisterExtension; +import org.testcontainers.containers.GenericContainer; + +// ignore using deprecated createDatabase and deleteDatabase methods warning. +@SuppressWarnings("deprecation") +@TestInstance(Lifecycle.PER_CLASS) +class InfluxDbClientTest { + + @RegisterExtension + private static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + private static final GenericContainer influxDbServer = + new GenericContainer<>("influxdb:1.8.10-alpine").withExposedPorts(8086); + + private static InfluxDB influxDb; + + private static final String databaseName = "mydb"; + + private static String host; + + private static int port; + + @BeforeAll + void setup() { + influxDbServer.start(); + port = influxDbServer.getMappedPort(8086); + host = influxDbServer.getHost(); + String serverUrl = "http://" + host + ":" + port + "/"; + String username = "root"; + String password = "root"; + influxDb = InfluxDBFactory.connect(serverUrl, username, password); + influxDb.createDatabase(databaseName); + } + + @AfterAll + void cleanup() { + influxDb.deleteDatabase(databaseName); + influxDb.close(); + influxDbServer.stop(); + } + + @Test + void testQueryAndModifyWithOneArgument() { + String dbName = databaseName + "2"; + influxDb.createDatabase(dbName); + BatchPoints batchPoints = + BatchPoints.database(dbName).tag("async", "true").retentionPolicy("autogen").build(); + Point point1 = + Point.measurement("cpu") + .tag("atag", "test") + .addField("idle", 90L) + .addField("usertime", 9L) + .addField("system", 1L) + .build(); + Point point2 = + Point.measurement("disk") + .tag("atag", "test") + .addField("used", 80L) + .addField("free", 1L) + .build(); + batchPoints.point(point1); + batchPoints.point(point2); + influxDb.write(batchPoints); + Query query = new Query("SELECT * FROM cpu GROUP BY *", dbName); + QueryResult result = influxDb.query(query); + assertThat(result.getResults().get(0).getSeries().get(0).getTags()).isNotEmpty(); + influxDb.deleteDatabase(dbName); + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("CREATE DATABASE " + dbName) + .hasKind(SpanKind.CLIENT) + .hasAttributesSatisfying( + attributeAssertions(null, "CREATE DATABASE", dbName))), + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("WRITE " + dbName) + .hasKind(SpanKind.CLIENT) + .hasAttributesSatisfying(attributeAssertions(null, "WRITE", dbName))), + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("SELECT " + dbName) + .hasKind(SpanKind.CLIENT) + .hasAttributesSatisfying( + attributeAssertions("SELECT * FROM cpu GROUP BY *", "SELECT", dbName))), + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("DROP DATABASE " + dbName) + .hasKind(SpanKind.CLIENT) + .hasAttributesSatisfying( + attributeAssertions(null, "DROP DATABASE", dbName)))); + } + + @Test + void testQueryWithTwoArguments() { + Query query = new Query("SELECT * FROM cpu_load where test1 = 'influxDb'", databaseName); + influxDb.query(query, TimeUnit.MILLISECONDS); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("SELECT " + databaseName) + .hasKind(SpanKind.CLIENT) + .hasAttributesSatisfying( + attributeAssertions( + "SELECT * FROM cpu_load where test1 = ?", + "SELECT", + databaseName)))); + } + + @Test + void testQueryWithThreeArguments() throws InterruptedException { + Query query = + new Query( + "SELECT * FROM cpu_load where time >= '2022-01-01T08:00:00Z' AND time <= '2022-01-01T20:00:00Z'", + databaseName); + BlockingQueue queue = new LinkedBlockingQueue<>(); + + influxDb.query(query, 2, result -> queue.add(result)); + queue.poll(20, TimeUnit.SECONDS); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("SELECT " + databaseName) + .hasKind(SpanKind.CLIENT) + .hasAttributesSatisfying( + attributeAssertions( + "SELECT * FROM cpu_load where time >= ? AND time <= ?", + "SELECT", + databaseName)))); + } + + @Test + void testQueryWithThreeArgumentsCallback() throws InterruptedException { + Query query = new Query("SELECT * FROM cpu_load", databaseName); + BlockingQueue queue = new LinkedBlockingQueue<>(); + + influxDb.query(query, 2, result -> queue.add(result)); + queue.poll(20, TimeUnit.SECONDS); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("SELECT " + databaseName) + .hasKind(SpanKind.CLIENT) + .hasAttributesSatisfying( + attributeAssertions( + "SELECT * FROM cpu_load", "SELECT", databaseName)))); + } + + @Test + void testQueryWithFiveArguments() throws InterruptedException { + CountDownLatch countDownLatch = new CountDownLatch(1); + Query query = + new Query( + "SELECT MEAN(water_level) FROM h2o_feet where time = '2022-01-01T08:00:00Z'; SELECT water_level FROM h2o_feet LIMIT 2", + databaseName); + testing.runWithSpan( + "parent", + () -> { + influxDb.query( + query, + 10, + (cancellable, queryResult) -> countDownLatch.countDown(), + () -> testing.runWithSpan("child", () -> {}), + throwable -> {}); + }); + assertThat(countDownLatch.await(10, TimeUnit.SECONDS)).isTrue(); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(), + span -> + span.hasName("SELECT " + databaseName) + .hasKind(SpanKind.CLIENT) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfying( + attributeAssertions( + "SELECT MEAN(water_level) FROM h2o_feet where time = ?; SELECT water_level FROM h2o_feet LIMIT ?", + "SELECT", + databaseName)), + span -> + span.hasName("child").hasKind(SpanKind.INTERNAL).hasParent(trace.getSpan(0)))); + } + + @Test + void testQueryFailedWithFiveArguments() throws InterruptedException { + CountDownLatch countDownLatchFailure = new CountDownLatch(1); + Query query = new Query("SELECT MEAN(water_level) FROM;", databaseName); + testing.runWithSpan( + "parent", + () -> { + influxDb.query( + query, + 10, + (cancellable, queryResult) -> {}, + () -> {}, + throwable -> { + testing.runWithSpan("child", () -> {}); + countDownLatchFailure.countDown(); + }); + }); + assertThat(countDownLatchFailure.await(10, TimeUnit.SECONDS)).isTrue(); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(), + span -> + span.hasName("SELECT " + databaseName) + .hasKind(SpanKind.CLIENT) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfying( + attributeAssertions( + "SELECT MEAN(water_level) FROM;", "SELECT", databaseName)), + span -> + span.hasName("child").hasKind(SpanKind.INTERNAL).hasParent(trace.getSpan(0)))); + } + + @Test + void testWriteWithFourArguments() { + String measurement = "cpu_load"; + List records = new ArrayList<>(); + records.add(measurement + ",atag=test1 idle=100,usertime=10,system=1 1485273600"); + influxDb.write(databaseName, "autogen", InfluxDB.ConsistencyLevel.ONE, records); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("WRITE " + databaseName) + .hasKind(SpanKind.CLIENT) + .hasAttributesSatisfying( + attributeAssertions(null, "WRITE", databaseName)))); + } + + @Test + void testWriteWithFiveArguments() { + String measurement = "cpu_load"; + List records = new ArrayList<>(); + records.add(measurement + ",atag=test1 idle=100,usertime=10,system=1 1485273600"); + influxDb.write( + databaseName, "autogen", InfluxDB.ConsistencyLevel.ONE, TimeUnit.SECONDS, records); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("WRITE " + databaseName) + .hasKind(SpanKind.CLIENT) + .hasAttributesSatisfying( + attributeAssertions(null, "WRITE", databaseName)))); + } + + @Test + void testWriteWithUdp() { + List lineProtocols = new ArrayList<>(); + for (int i = 0; i < 2000; i++) { + Point point = Point.measurement("udp_single_poit").addField("v", i).build(); + lineProtocols.add(point.lineProtocol()); + } + influxDb.write(port, lineProtocols); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("WRITE") + .hasKind(SpanKind.CLIENT) + .hasAttributesSatisfying(attributeAssertions(null, "WRITE", null)))); + } + + private static List attributeAssertions( + String statement, String operation, String databaseName) { + List result = new ArrayList<>(); + result.addAll( + asList( + equalTo(DbIncubatingAttributes.DB_SYSTEM, "influxdb"), + equalTo(DbIncubatingAttributes.DB_NAME, databaseName), + equalTo(ServerAttributes.SERVER_ADDRESS, host), + equalTo(ServerAttributes.SERVER_PORT, port), + equalTo(DbIncubatingAttributes.DB_OPERATION, operation))); + if (statement != null) { + result.add(equalTo(DbIncubatingAttributes.DB_STATEMENT, statement)); + } + return result; + } +} diff --git a/instrumentation/influxdb-2.4/javaagent/src/test24/java/io/opentelemetry/javaagent/instrumentation/influxdb/v2_4/InfluxDbClient24Test.java b/instrumentation/influxdb-2.4/javaagent/src/test24/java/io/opentelemetry/javaagent/instrumentation/influxdb/v2_4/InfluxDbClient24Test.java new file mode 100644 index 000000000000..3cd33152551c --- /dev/null +++ b/instrumentation/influxdb-2.4/javaagent/src/test24/java/io/opentelemetry/javaagent/instrumentation/influxdb/v2_4/InfluxDbClient24Test.java @@ -0,0 +1,159 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.influxdb.v2_4; + +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; +import static java.util.Arrays.asList; +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.sdk.testing.assertj.AttributeAssertion; +import io.opentelemetry.semconv.ServerAttributes; +import io.opentelemetry.semconv.incubating.DbIncubatingAttributes; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; +import org.influxdb.InfluxDB; +import org.influxdb.InfluxDBFactory; +import org.influxdb.dto.BatchPoints; +import org.influxdb.dto.Point; +import org.influxdb.dto.Query; +import org.influxdb.dto.QueryResult; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +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.RegisterExtension; +import org.testcontainers.containers.GenericContainer; + +@TestInstance(Lifecycle.PER_CLASS) +class InfluxDbClient24Test { + + @RegisterExtension + private static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + private static final GenericContainer influxDbServer = + new GenericContainer<>("influxdb:1.8.10-alpine").withExposedPorts(8086); + + private static InfluxDB influxDb; + + private static final String databaseName = "mydb"; + + private static String host; + + private static int port; + + @BeforeAll + void setup() { + influxDbServer.start(); + port = influxDbServer.getMappedPort(8086); + host = influxDbServer.getHost(); + String serverUrl = "http://" + host + ":" + port + "/"; + String username = "root"; + String password = "root"; + influxDb = InfluxDBFactory.connect(serverUrl, username, password); + influxDb.createDatabase(databaseName); + } + + @AfterAll + void cleanup() { + influxDb.deleteDatabase(databaseName); + influxDbServer.stop(); + } + + @Test + void testQueryAndModifyWithOneArgument() { + String dbName = databaseName + "2"; + influxDb.createDatabase(dbName); + BatchPoints batchPoints = + BatchPoints.database(dbName).tag("async", "true").retentionPolicy("autogen").build(); + Point point1 = + Point.measurement("cpu") + .tag("atag", "test") + .addField("idle", 90L) + .addField("usertime", 9L) + .addField("system", 1L) + .build(); + Point point2 = + Point.measurement("disk") + .tag("atag", "test") + .addField("used", 80L) + .addField("free", 1L) + .build(); + batchPoints.point(point1); + batchPoints.point(point2); + influxDb.write(batchPoints); + Query query = new Query("SELECT * FROM cpu GROUP BY *", dbName); + QueryResult result = influxDb.query(query); + assertThat(result.getResults().get(0).getSeries().get(0).getTags()).isNotEmpty(); + influxDb.deleteDatabase(dbName); + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("CREATE DATABASE " + dbName) + .hasKind(SpanKind.CLIENT) + .hasAttributesSatisfying( + attributeAssertions(null, "CREATE DATABASE", dbName))), + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("WRITE " + dbName) + .hasKind(SpanKind.CLIENT) + .hasAttributesSatisfying(attributeAssertions(null, "WRITE", dbName))), + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("SELECT " + dbName) + .hasKind(SpanKind.CLIENT) + .hasAttributesSatisfying( + attributeAssertions("SELECT * FROM cpu GROUP BY *", "SELECT", dbName))), + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("DROP DATABASE " + dbName) + .hasKind(SpanKind.CLIENT) + .hasAttributesSatisfying( + attributeAssertions(null, "DROP DATABASE", dbName)))); + } + + @Test + void testQueryWithTwoArguments() { + Query query = new Query("SELECT * FROM cpu_load where test1 = 'influxDb'", databaseName); + influxDb.query(query, TimeUnit.MILLISECONDS); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("SELECT " + databaseName) + .hasKind(SpanKind.CLIENT) + .hasAttributesSatisfying( + attributeAssertions( + "SELECT * FROM cpu_load where test1 = ?", + "SELECT", + databaseName)))); + } + + private static List attributeAssertions( + String statement, String operation, String databaseName) { + List result = new ArrayList<>(); + result.addAll( + asList( + equalTo(DbIncubatingAttributes.DB_SYSTEM, "influxdb"), + equalTo(DbIncubatingAttributes.DB_NAME, databaseName), + equalTo(ServerAttributes.SERVER_ADDRESS, host), + equalTo(ServerAttributes.SERVER_PORT, port), + equalTo(DbIncubatingAttributes.DB_OPERATION, operation))); + if (statement != null) { + result.add(equalTo(DbIncubatingAttributes.DB_STATEMENT, statement)); + } + return result; + } +} diff --git a/instrumentation/internal/internal-application-logger/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/internal/logging/ApplicationLoggingInstrumentationModule.java b/instrumentation/internal/internal-application-logger/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/internal/logging/ApplicationLoggingInstrumentationModule.java index 5926ae691cdb..d490e2fbf9eb 100644 --- a/instrumentation/internal/internal-application-logger/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/internal/logging/ApplicationLoggingInstrumentationModule.java +++ b/instrumentation/internal/internal-application-logger/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/internal/logging/ApplicationLoggingInstrumentationModule.java @@ -23,7 +23,8 @@ public ApplicationLoggingInstrumentationModule() { @Override public boolean defaultEnabled(ConfigProperties config) { // only enable this instrumentation if the application logger is enabled - return "application".equals(config.getString("otel.javaagent.logging")); + return super.defaultEnabled(config) + && "application".equals(config.getString("otel.javaagent.logging")); } @Override diff --git a/instrumentation/internal/internal-class-loader/javaagent-integration-tests/src/main/java/instrumentation/TestInstrumentationModule.java b/instrumentation/internal/internal-class-loader/javaagent-integration-tests/src/main/java/instrumentation/TestInstrumentationModule.java index e4044ababaa7..c8126cf78bb5 100644 --- a/instrumentation/internal/internal-class-loader/javaagent-integration-tests/src/main/java/instrumentation/TestInstrumentationModule.java +++ b/instrumentation/internal/internal-class-loader/javaagent-integration-tests/src/main/java/instrumentation/TestInstrumentationModule.java @@ -36,6 +36,11 @@ public void registerHelperResources(HelperResourceBuilder helperResourceBuilder) "test-resources/test-resource.txt", "test-resources/test-resource-2.txt"); } + @Override + public boolean isIndyModule() { + return false; + } + public static class TestTypeInstrumentation implements TypeInstrumentation { @Override public ElementMatcher classLoaderOptimization() { diff --git a/instrumentation/internal/internal-class-loader/javaagent-integration-tests/src/test/groovy/ResourceInjectionTest.groovy b/instrumentation/internal/internal-class-loader/javaagent-integration-tests/src/test/groovy/ResourceInjectionTest.groovy index df88737e9910..b5b3d799c496 100644 --- a/instrumentation/internal/internal-class-loader/javaagent-integration-tests/src/test/groovy/ResourceInjectionTest.groovy +++ b/instrumentation/internal/internal-class-loader/javaagent-integration-tests/src/test/groovy/ResourceInjectionTest.groovy @@ -7,6 +7,7 @@ import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification import org.apache.commons.lang3.SystemUtils import java.lang.ref.WeakReference +import java.time.Duration import java.util.concurrent.atomic.AtomicReference import static io.opentelemetry.instrumentation.test.utils.GcUtils.awaitGc @@ -44,7 +45,7 @@ class ResourceInjectionTest extends AgentInstrumentationSpecification { def ref = new WeakReference(emptyLoader.get()) emptyLoader.set(null) - awaitGc(ref) + awaitGc(ref, Duration.ofSeconds(10)) then: "HelperInjector doesn't prevent it from being collected" null == ref.get() diff --git a/instrumentation/internal/internal-class-loader/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/internal/classloader/ClassLoaderInstrumentationModule.java b/instrumentation/internal/internal-class-loader/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/internal/classloader/ClassLoaderInstrumentationModule.java index 9fff9300895e..91fbb3b4baf9 100644 --- a/instrumentation/internal/internal-class-loader/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/internal/classloader/ClassLoaderInstrumentationModule.java +++ b/instrumentation/internal/internal-class-loader/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/internal/classloader/ClassLoaderInstrumentationModule.java @@ -25,6 +25,11 @@ public boolean defaultEnabled(ConfigProperties config) { return true; } + @Override + public boolean isIndyModule() { + return false; + } + @Override public boolean isHelperClass(String className) { return className.equals("io.opentelemetry.javaagent.tooling.Constants"); diff --git a/instrumentation/internal/internal-class-loader/javaagent/src/test/groovy/ClassLoadingTest.groovy b/instrumentation/internal/internal-class-loader/javaagent/src/test/groovy/ClassLoadingTest.groovy deleted file mode 100644 index 1c9958206589..000000000000 --- a/instrumentation/internal/internal-class-loader/javaagent/src/test/groovy/ClassLoadingTest.groovy +++ /dev/null @@ -1,44 +0,0 @@ -/* - * 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.bootstrap.Java8BytecodeBridge", 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/instrumentation/internal/internal-class-loader/javaagent/src/test/groovy/JBossClassloadingTest.groovy b/instrumentation/internal/internal-class-loader/javaagent/src/test/groovy/JBossClassloadingTest.groovy deleted file mode 100644 index e1c8e9fd245a..000000000000 --- a/instrumentation/internal/internal-class-loader/javaagent/src/test/groovy/JBossClassloadingTest.groovy +++ /dev/null @@ -1,39 +0,0 @@ -/* - * 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.bootstrap.Java8BytecodeBridge", false, classLoader) - } catch (ClassNotFoundException e) { - } - - then: - assert clazz != null - assert clazz.getClassLoader() == null - } -} diff --git a/instrumentation/internal/internal-class-loader/javaagent/src/test/groovy/OSGIClassloadingTest.groovy b/instrumentation/internal/internal-class-loader/javaagent/src/test/groovy/OSGIClassloadingTest.groovy deleted file mode 100644 index 42bd8986b3a6..000000000000 --- a/instrumentation/internal/internal-class-loader/javaagent/src/test/groovy/OSGIClassloadingTest.groovy +++ /dev/null @@ -1,73 +0,0 @@ -/* - * 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.bootstrap.Java8BytecodeBridge") - } else { - clazz = loader.loadClass("io.opentelemetry.javaagent.bootstrap.Java8BytecodeBridge", 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/instrumentation/internal/internal-class-loader/javaagent/src/test/groovy/TomcatClassloadingTest.groovy b/instrumentation/internal/internal-class-loader/javaagent/src/test/groovy/TomcatClassloadingTest.groovy deleted file mode 100644 index 3d9741a2472d..000000000000 --- a/instrumentation/internal/internal-class-loader/javaagent/src/test/groovy/TomcatClassloadingTest.groovy +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification -import io.opentelemetry.javaagent.bootstrap.HelperResources -import java.nio.charset.StandardCharsets -import java.nio.file.Files -import java.util.stream.Collectors -import org.apache.catalina.WebResource -import org.apache.catalina.WebResourceRoot -import org.apache.catalina.loader.ParallelWebappClassLoader -import spock.lang.Shared - -class TomcatClassloadingTest extends AgentInstrumentationSpecification { - - @Shared - ParallelWebappClassLoader classloader - - def setupSpec() { - WebResourceRoot resources = Mock(WebResourceRoot) { - getResource(_) >> Mock(WebResource) - getClassLoaderResource(_) >> Mock(WebResource) - listResources(_) >> [] - // Looks like 9.x.x needs this one: - getResources(_) >> [] - getClassLoaderResources(_) >> [] - } - def parentClassLoader = new ClassLoader(null) { - } - classloader = new ParallelWebappClassLoader(parentClassLoader) - classloader.setResources(resources) - classloader.init() - classloader.start() - } - - def "tomcat class loading delegates to parent for agent classes"() { - expect: - // If instrumentation didn't work this would blow up with NPE due to incomplete resources mocking - classloader.loadClass("io.opentelemetry.javaagent.bootstrap.Java8BytecodeBridge") - } - - def "test resource injection"() { - setup: - def tmpFile = Files.createTempFile("hello", "tmp") - Files.write(tmpFile, "hello".getBytes(StandardCharsets.UTF_8)) - def url = tmpFile.toUri().toURL() - HelperResources.register(classloader, "hello.txt", Arrays.asList(url)) - - expect: - classloader.getResource("hello.txt") != null - - and: - def resources = classloader.getResources("hello.txt") - resources != null - resources.hasMoreElements() - - and: - def inputStream = classloader.getResourceAsStream("hello.txt") - inputStream != null - String text = new BufferedReader( - new InputStreamReader(inputStream, StandardCharsets.UTF_8)) - .lines() - .collect(Collectors.joining("\n")) - text == "hello" - - cleanup: - inputStream?.close() - } -} diff --git a/instrumentation/internal/internal-class-loader/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/internal/classloader/ClassLoadingTest.java b/instrumentation/internal/internal-class-loader/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/internal/classloader/ClassLoadingTest.java new file mode 100644 index 000000000000..6d1bd56efc4f --- /dev/null +++ b/instrumentation/internal/internal-class-loader/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/internal/classloader/ClassLoadingTest.java @@ -0,0 +1,57 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.internal.classloader; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.net.URL; +import java.net.URLClassLoader; +import org.junit.jupiter.api.Test; + +class ClassLoadingTest { + + @Test + void testDelegatesToBootstrapClassLoaderForAgentClasses() throws ClassNotFoundException { + NonDelegatingUrlClassLoader classLoader = new NonDelegatingUrlClassLoader(); + Class clazz = + Class.forName( + "io.opentelemetry.javaagent.bootstrap.Java8BytecodeBridge", false, classLoader); + + assertThat(clazz).isNotNull(); + assertThat(clazz.getClassLoader()).isNull(); + } + + @Test + void testDelegatesToBootstrapClassLoaderForAgentClassesTwoArguments() + throws ClassNotFoundException { + NonDelegatingUrlClassLoader classLoader = new NonDelegatingUrlClassLoader(); + Class clazz = + classLoader.loadClass("io.opentelemetry.javaagent.bootstrap.Java8BytecodeBridge", false); + assertThat(clazz).isNotNull(); + assertThat(clazz.getClassLoader()).isNull(); + } + + static class NonDelegatingUrlClassLoader extends URLClassLoader { + + NonDelegatingUrlClassLoader() { + super(new URL[0]); + } + + @Override + 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/instrumentation/internal/internal-class-loader/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/internal/classloader/JbossClassloadingTest.java b/instrumentation/internal/internal-class-loader/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/internal/classloader/JbossClassloadingTest.java new file mode 100644 index 000000000000..49076c25dcb1 --- /dev/null +++ b/instrumentation/internal/internal-class-loader/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/internal/classloader/JbossClassloadingTest.java @@ -0,0 +1,45 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.internal.classloader; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import org.jboss.modules.Module; +import org.jboss.modules.ModuleClassLoader; +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; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +class JbossClassloadingTest { + + @RegisterExtension + static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + @Test + void testDelegatesToBootstrapClassLoaderForAgentClasses() + throws ModuleLoadException, ClassNotFoundException { + ModuleFinder[] moduleFinders = new ModuleFinder[1]; + moduleFinders[0] = (identifier, delegateLoader) -> ModuleSpec.build(identifier).create(); + + ModuleLoader moduleLoader = new ModuleLoader(moduleFinders); + ModuleIdentifier moduleId = ModuleIdentifier.fromString("test"); + Module testModule = moduleLoader.loadModule(moduleId); + ModuleClassLoader classLoader = testModule.getClassLoader(); + + Class clazz = + Class.forName( + "io.opentelemetry.javaagent.bootstrap.Java8BytecodeBridge", false, classLoader); + + assertThat(clazz).isNotNull(); + assertThat(clazz.getClassLoader()).isNull(); + } +} diff --git a/instrumentation/internal/internal-class-loader/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/internal/classloader/OsgiClassloadingTest.java b/instrumentation/internal/internal-class-loader/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/internal/classloader/OsgiClassloadingTest.java new file mode 100644 index 000000000000..daf80f0e1572 --- /dev/null +++ b/instrumentation/internal/internal-class-loader/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/internal/classloader/OsgiClassloadingTest.java @@ -0,0 +1,80 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.internal.classloader; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import java.util.stream.Stream; +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; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +class OsgiClassloadingTest { + + @RegisterExtension + static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + @ParameterizedTest + @MethodSource("provideArguments") + void testOsgiDelegatesToBootstrapClassloaderForAgentClasses(ClassLoader loader) + throws ClassNotFoundException { + Class clazz = loader.loadClass("io.opentelemetry.javaagent.bootstrap.Java8BytecodeBridge"); + assertThat(clazz).isNotNull(); + assertThat(clazz.getClassLoader()).isNull(); + } + + private static Stream provideArguments() { + return Stream.of( + Arguments.of(new TestClassLoader()), + Arguments.of(new BundleWiringImpl.BundleClassLoader(null, null, null))); + } + + static class TestClassLoader extends ModuleClassLoader { + TestClassLoader() { + super(null); + } + + @Override + protected BundleInfo.Generation getGeneration() { + return null; + } + + @Override + protected Debug getDebug() { + return null; + } + + @Override + public ClasspathManager getClasspathManager() { + return null; + } + + @Override + protected EquinoxConfiguration getConfiguration() { + return null; + } + + @Override + public BundleLoader getBundleLoader() { + return null; + } + + @Override + public boolean isRegisteredAsParallel() { + return false; + } + } +} diff --git a/instrumentation/internal/internal-class-loader/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/internal/classloader/TomcatClassloadingTest.java b/instrumentation/internal/internal-class-loader/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/internal/classloader/TomcatClassloadingTest.java new file mode 100644 index 000000000000..2d4ca38adf83 --- /dev/null +++ b/instrumentation/internal/internal-class-loader/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/internal/classloader/TomcatClassloadingTest.java @@ -0,0 +1,99 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.internal.classloader; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import io.opentelemetry.instrumentation.testing.internal.AutoCleanupExtension; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.javaagent.bootstrap.HelperResources; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collections; +import java.util.Enumeration; +import java.util.stream.Collectors; +import org.apache.catalina.LifecycleException; +import org.apache.catalina.WebResource; +import org.apache.catalina.WebResourceRoot; +import org.apache.catalina.loader.ParallelWebappClassLoader; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +class TomcatClassloadingTest { + + @RegisterExtension + static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + @RegisterExtension static final AutoCleanupExtension cleanup = AutoCleanupExtension.create(); + + private static ParallelWebappClassLoader classloader; + + @BeforeAll + static void setup() throws LifecycleException { + WebResourceRoot resources = mock(WebResourceRoot.class); + WebResource resource = mock(WebResource.class); + WebResource[] webResources = new WebResource[0]; + + when(resources.getResource(any())).thenReturn(resource); + when(resources.getClassLoaderResource(any())).thenReturn(resource); + when(resources.listResources(any())).thenReturn(webResources); + when(resources.getResources(any())).thenReturn(webResources); + when(resources.getClassLoaderResources(any())).thenReturn(webResources); + + ClassLoader parentClassloader = new ClassLoader(null) {}; + classloader = new ParallelWebappClassLoader(parentClassloader); + classloader.setResources(resources); + classloader.init(); + classloader.start(); + } + + @Test + void testTomcatClassLoadingDelegatesToParentForAgentClasses() throws ClassNotFoundException { + // If instrumentation didn't work this would blow up with NPE due to incomplete resources + // mocking + classloader.loadClass("io.opentelemetry.javaagent.bootstrap.Java8BytecodeBridge"); + } + + @Test + void testResourceInjection() throws IOException { + Path tmpFile = Files.createTempFile("hello", "tmp"); + tmpFile.toFile().deleteOnExit(); + + Files.write(tmpFile, "hello".getBytes(StandardCharsets.UTF_8)); + URL url = tmpFile.toUri().toURL(); + HelperResources.register(classloader, "hello.txt", Collections.singletonList(url)); + + assertThat(classloader.getResource("hello.txt")).isNotNull(); + + Enumeration resources = classloader.getResources("hello.txt"); + + assertThat(resources).isNotNull(); + assertThat(resources.hasMoreElements()).isTrue(); + + InputStream inputStream = classloader.getResourceAsStream("hello.txt"); + cleanup.deferCleanup(inputStream); + + assertThat(inputStream).isNotNull(); + + String text = + new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8)) + .lines() + .collect(Collectors.joining("\n")); + + assertThat(text).isEqualTo("hello"); + } +} diff --git a/instrumentation/internal/internal-eclipse-osgi-3.6/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/internal/osgi/EclipseOsgiInstrumentation.java b/instrumentation/internal/internal-eclipse-osgi-3.6/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/internal/osgi/EclipseOsgiInstrumentation.java index 8f3b4d81a162..e4a8bda14df1 100644 --- a/instrumentation/internal/internal-eclipse-osgi-3.6/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/internal/osgi/EclipseOsgiInstrumentation.java +++ b/instrumentation/internal/internal-eclipse-osgi-3.6/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/internal/osgi/EclipseOsgiInstrumentation.java @@ -23,7 +23,8 @@ * *

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.. + * of the missing package/bundle when the call is originating from ClassLoaderMatcher, unless the + * request is for a package for which we explicitly allow the dynamic imports. */ class EclipseOsgiInstrumentation implements TypeInstrumentation { @@ -45,8 +46,10 @@ 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(); + public static boolean onEnter(@Advice.Argument(0) String packageName) { + // disable dynamic imports for everything except io.opentelemetry classes to allow dynamic + // import of @WithSpan etc. + return InClassLoaderMatcher.get() && !packageName.startsWith("io.opentelemetry."); } @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) diff --git a/instrumentation/internal/internal-lambda/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/internal/lambda/InnerClassLambdaMetafactoryInstrumentation.java b/instrumentation/internal/internal-lambda/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/internal/lambda/InnerClassLambdaMetafactoryInstrumentation.java index a45dd155bd69..07d444913afa 100644 --- a/instrumentation/internal/internal-lambda/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/internal/lambda/InnerClassLambdaMetafactoryInstrumentation.java +++ b/instrumentation/internal/internal-lambda/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/internal/lambda/InnerClassLambdaMetafactoryInstrumentation.java @@ -12,6 +12,7 @@ import io.opentelemetry.javaagent.bootstrap.DefineClassHelper.Handler.DefineClassContext; import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.extension.instrumentation.internal.AsmApi; import net.bytebuddy.asm.Advice; import net.bytebuddy.asm.AsmVisitorWrapper; import net.bytebuddy.description.field.FieldDescription; @@ -89,7 +90,7 @@ private static class MetaFactoryClassVisitor extends ClassVisitor { private final String slashClassName; MetaFactoryClassVisitor(ClassVisitor cv, String slashClassName) { - super(Opcodes.ASM7, cv); + super(AsmApi.VERSION, cv); this.slashClassName = slashClassName; } diff --git a/instrumentation/internal/internal-lambda/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/internal/lambda/LambdaInstrumentationModule.java b/instrumentation/internal/internal-lambda/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/internal/lambda/LambdaInstrumentationModule.java index edb209473b4d..0b0c12b82ae3 100644 --- a/instrumentation/internal/internal-lambda/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/internal/lambda/LambdaInstrumentationModule.java +++ b/instrumentation/internal/internal-lambda/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/internal/lambda/LambdaInstrumentationModule.java @@ -27,6 +27,11 @@ public boolean defaultEnabled(ConfigProperties config) { return true; } + @Override + public boolean isIndyModule() { + return false; + } + @Override public List getAdditionalHelperClassNames() { // this instrumentation uses ASM not ByteBuddy so muzzle doesn't automatically add helper diff --git a/instrumentation/internal/internal-lambda/javaagent/src/test/groovy/LambdaInstrumentationTest.groovy b/instrumentation/internal/internal-lambda/javaagent/src/test/groovy/LambdaInstrumentationTest.groovy deleted file mode 100644 index aa3e5e027fc4..000000000000 --- a/instrumentation/internal/internal-lambda/javaagent/src/test/groovy/LambdaInstrumentationTest.groovy +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification -import io.opentelemetry.javaagent.bootstrap.VirtualFieldInstalledMarker - -class LambdaInstrumentationTest extends AgentInstrumentationSpecification { - - def "test transform Runnable lambda"() { - setup: - Runnable runnable = TestLambda.makeRunnable() - - expect: - // RunnableInstrumentation adds a VirtualField to all implementors of Runnable. If lambda class - // is transformed then it must have context store marker interface. - runnable instanceof VirtualFieldInstalledMarker - !VirtualFieldInstalledMarker.isAssignableFrom(Runnable) - } -} diff --git a/instrumentation/internal/internal-lambda/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/internal/lambda/LambdaInstrumentationTest.java b/instrumentation/internal/internal-lambda/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/internal/lambda/LambdaInstrumentationTest.java new file mode 100644 index 000000000000..01bbb39b6921 --- /dev/null +++ b/instrumentation/internal/internal-lambda/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/internal/lambda/LambdaInstrumentationTest.java @@ -0,0 +1,24 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.internal.lambda; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.javaagent.bootstrap.VirtualFieldInstalledMarker; +import org.junit.jupiter.api.Test; + +class LambdaInstrumentationTest { + + @Test + void testTransformRunnableLambda() { + Runnable runnable = TestLambda.makeRunnable(); + + // RunnableInstrumentation adds a VirtualField to all implementors of Runnable. If lambda class + // is transformed then it must have context store marker interface. + assertThat(runnable).isInstanceOf(VirtualFieldInstalledMarker.class); + assertThat(VirtualFieldInstalledMarker.class.isAssignableFrom(Runnable.class)).isFalse(); + } +} diff --git a/instrumentation/internal/internal-lambda/javaagent/src/test/java/TestLambda.java b/instrumentation/internal/internal-lambda/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/internal/lambda/TestLambda.java similarity index 74% rename from instrumentation/internal/internal-lambda/javaagent/src/test/java/TestLambda.java rename to instrumentation/internal/internal-lambda/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/internal/lambda/TestLambda.java index 8ea194b6a12f..f1a5cea094cb 100644 --- a/instrumentation/internal/internal-lambda/javaagent/src/test/java/TestLambda.java +++ b/instrumentation/internal/internal-lambda/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/internal/lambda/TestLambda.java @@ -3,6 +3,8 @@ * SPDX-License-Identifier: Apache-2.0 */ +package io.opentelemetry.javaagent.instrumentation.internal.lambda; + public class TestLambda { static Runnable makeRunnable() { diff --git a/instrumentation/internal/internal-reflection/javaagent-integration-tests/src/test/groovy/ReflectionTest.groovy b/instrumentation/internal/internal-reflection/javaagent-integration-tests/src/test/groovy/ReflectionTest.groovy deleted file mode 100644 index 84420367d478..000000000000 --- a/instrumentation/internal/internal-reflection/javaagent-integration-tests/src/test/groovy/ReflectionTest.groovy +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification -import io.opentelemetry.javaagent.bootstrap.VirtualFieldAccessorMarker -import io.opentelemetry.javaagent.bootstrap.VirtualFieldInstalledMarker - -import java.lang.reflect.Field -import java.lang.reflect.Method - -class ReflectionTest extends AgentInstrumentationSpecification { - - def "test our fields and methods are not visible with reflection"() { - when: - TestClass test = new TestClass() - - then: - test.testMethod() == "instrumented" - test.testMethod2() == "instrumented" - - and: - def fieldFound = false - for (Field field : TestClass.getDeclaredFields()) { - if (field.getName().startsWith("__opentelemetry")) { - fieldFound = true - } - } - fieldFound == false - - and: - def methodFound = false - for (Method method : TestClass.getDeclaredMethods()) { - if (method.getName().contains("__opentelemetry")) { - methodFound = true - } - } - methodFound == false - - and: - // although marker interfaces are removed from getInterfaces() result class is still assignable - // to them - VirtualFieldInstalledMarker.isAssignableFrom(TestClass) - VirtualFieldAccessorMarker.isAssignableFrom(TestClass) - TestClass.getInterfaces().length == 2 - TestClass.getInterfaces() == [Runnable, Serializable] - } - - def "test generated serialVersionUID"() { - // expected value is computed with serialver utility that comes with jdk - expect: - ObjectStreamClass.lookup(TestClass).getSerialVersionUID() == -1508684692096503670L - - and: - TestClass.getDeclaredFields().length == 0 - } -} diff --git a/instrumentation/internal/internal-reflection/javaagent-integration-tests/src/test/java/ReflectionTest.java b/instrumentation/internal/internal-reflection/javaagent-integration-tests/src/test/java/ReflectionTest.java new file mode 100644 index 000000000000..5d142fbd4d0c --- /dev/null +++ b/instrumentation/internal/internal-reflection/javaagent-integration-tests/src/test/java/ReflectionTest.java @@ -0,0 +1,48 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.javaagent.bootstrap.VirtualFieldAccessorMarker; +import io.opentelemetry.javaagent.bootstrap.VirtualFieldInstalledMarker; +import java.io.ObjectStreamClass; +import java.io.Serializable; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import org.junit.jupiter.api.Test; + +public class ReflectionTest { + + @Test + void testOurFieldsAndMethodsAreNotVisibleWithReflection() { + TestClass test = new TestClass(); + + assertThat(test.testMethod()).isEqualTo("instrumented"); + assertThat(test.testMethod2()).isEqualTo("instrumented"); + + for (Field field : TestClass.class.getDeclaredFields()) { + assertThat(field.getName()).doesNotStartWith("__opentelemetry"); + } + + for (Method method : TestClass.class.getDeclaredMethods()) { + assertThat(method.getName()).doesNotStartWith("__opentelemetry"); + } + + // although marker interfaces are removed from getInterfaces() result class is still assignable + // to them + assertThat(VirtualFieldInstalledMarker.class.isAssignableFrom(TestClass.class)).isTrue(); + assertThat(VirtualFieldAccessorMarker.class.isAssignableFrom(TestClass.class)).isTrue(); + assertThat(TestClass.class.getInterfaces().length).isEqualTo(2); + assertThat(TestClass.class.getInterfaces()).isInstanceOfAny(Runnable.class, Serializable.class); + } + + @Test + void testGeneratedSerialVersionUid() { + // expected value is computed with serialver utility that comes with jdk + assertThat(ObjectStreamClass.lookup(TestClass.class).getSerialVersionUID()) + .isEqualTo(-1508684692096503670L); + assertThat(TestClass.class.getDeclaredFields().length).isEqualTo(0); + } +} diff --git a/instrumentation/internal/internal-reflection/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/internal/reflection/ClassInstrumentation.java b/instrumentation/internal/internal-reflection/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/internal/reflection/ClassInstrumentation.java index 3d6758370fcd..9dda5d174640 100644 --- a/instrumentation/internal/internal-reflection/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/internal/reflection/ClassInstrumentation.java +++ b/instrumentation/internal/internal-reflection/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/internal/reflection/ClassInstrumentation.java @@ -10,6 +10,7 @@ import com.google.errorprone.annotations.CanIgnoreReturnValue; import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.extension.instrumentation.internal.AsmApi; import net.bytebuddy.asm.AsmVisitorWrapper; import net.bytebuddy.description.field.FieldDescription; import net.bytebuddy.description.field.FieldList; @@ -65,7 +66,7 @@ public ClassVisitor wrap( private static class ClassClassVisitor extends ClassVisitor { ClassClassVisitor(ClassVisitor cv) { - super(Opcodes.ASM7, cv); + super(AsmApi.VERSION, cv); } @Override diff --git a/instrumentation/internal/internal-reflection/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/internal/reflection/ReflectionInstrumentation.java b/instrumentation/internal/internal-reflection/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/internal/reflection/ReflectionInstrumentation.java index 472b61d5244d..d2610b20481d 100644 --- a/instrumentation/internal/internal-reflection/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/internal/reflection/ReflectionInstrumentation.java +++ b/instrumentation/internal/internal-reflection/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/internal/reflection/ReflectionInstrumentation.java @@ -53,21 +53,27 @@ public void transform(TypeTransformer transformer) { @SuppressWarnings("unused") public static class FilterFieldsAdvice { + + // using AsScalar is needed to return the array itself instead of "advice Object[] return" + @Advice.AssignReturned.ToReturned + @Advice.AssignReturned.AsScalar @Advice.OnMethodExit(suppress = Throwable.class) - public static void filter( - @Advice.Argument(0) Class containingClass, - @Advice.Return(readOnly = false) Field[] fields) { - fields = ReflectionHelper.filterFields(containingClass, fields); + public static Field[] filter( + @Advice.Argument(0) Class containingClass, @Advice.Return Field[] fields) { + return ReflectionHelper.filterFields(containingClass, fields); } } @SuppressWarnings("unused") public static class FilterMethodsAdvice { + + // using AsScalar is needed to return the array itself instead of "advice Object[] return" + @Advice.AssignReturned.ToReturned + @Advice.AssignReturned.AsScalar @Advice.OnMethodExit(suppress = Throwable.class) - public static void filter( - @Advice.Argument(0) Class containingClass, - @Advice.Return(readOnly = false) Method[] methods) { - methods = ReflectionHelper.filterMethods(containingClass, methods); + public static Method[] filter( + @Advice.Argument(0) Class containingClass, @Advice.Return Method[] methods) { + return ReflectionHelper.filterMethods(containingClass, methods); } } } diff --git a/instrumentation/internal/internal-reflection/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/internal/reflection/ReflectionInstrumentationModule.java b/instrumentation/internal/internal-reflection/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/internal/reflection/ReflectionInstrumentationModule.java index 9f6fcd4dec9e..b22464b81215 100644 --- a/instrumentation/internal/internal-reflection/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/internal/reflection/ReflectionInstrumentationModule.java +++ b/instrumentation/internal/internal-reflection/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/internal/reflection/ReflectionInstrumentationModule.java @@ -10,11 +10,15 @@ 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.internal.ExperimentalInstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.internal.injection.ClassInjector; +import io.opentelemetry.javaagent.extension.instrumentation.internal.injection.InjectionMode; import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; import java.util.List; @AutoService(InstrumentationModule.class) -public class ReflectionInstrumentationModule extends InstrumentationModule { +public class ReflectionInstrumentationModule extends InstrumentationModule + implements ExperimentalInstrumentationModule { public ReflectionInstrumentationModule() { super("internal-reflection"); } @@ -29,4 +33,15 @@ public boolean defaultEnabled(ConfigProperties config) { public List typeInstrumentations() { return asList(new ClassInstrumentation(), new ReflectionInstrumentation()); } + + @Override + public void injectClasses(ClassInjector injector) { + // we do not use ByteBuddy Advice dispatching in this instrumentation + // Instead, we manually call ReflectionHelper via ASM + // Easiest solution to work with indy is to inject an indy-proxy to be invoked + injector + .proxyBuilder( + "io.opentelemetry.javaagent.instrumentation.internal.reflection.ReflectionHelper") + .inject(InjectionMode.CLASS_ONLY); + } } diff --git a/instrumentation/internal/internal-url-class-loader/javaagent-integration-tests/src/test/groovy/AddUrlTest.groovy b/instrumentation/internal/internal-url-class-loader/javaagent-integration-tests/src/test/groovy/AddUrlTest.groovy deleted file mode 100644 index 16a4efdb58dd..000000000000 --- a/instrumentation/internal/internal-url-class-loader/javaagent-integration-tests/src/test/groovy/AddUrlTest.groovy +++ /dev/null @@ -1,51 +0,0 @@ -/* - * 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) - } - - // silence CodeNarc. URLClassLoader#addURL is protected, this method is public - @SuppressWarnings("UnnecessaryOverridingMethod") - @Override - void addURL(URL url) { - super.addURL(url) - } - } -} diff --git a/instrumentation/internal/internal-url-class-loader/javaagent-integration-tests/src/test/java/AddUrlTest.java b/instrumentation/internal/internal-url-class-loader/javaagent-integration-tests/src/test/java/AddUrlTest.java new file mode 100644 index 000000000000..779eb072da5f --- /dev/null +++ b/instrumentation/internal/internal-url-class-loader/javaagent-integration-tests/src/test/java/AddUrlTest.java @@ -0,0 +1,56 @@ +/* + * 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.catchThrowable; + +import java.net.URL; +import java.net.URLClassLoader; +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.SystemUtils; +import org.junit.jupiter.api.Test; + +class AddUrlTest { + + @Test + void testShouldInstrumentClassAfterItIsLoadedViaAddUrl() throws Exception { + TestUrlClassLoader loader = new TestUrlClassLoader(); + + // 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) + Throwable thrown = + catchThrowable( + () -> { + loader.loadClass(SystemUtils.class.getName()); + }); + + assertThat(thrown).isInstanceOf(ClassNotFoundException.class); + + // loading a class in the URLClassLoader in order to trigger + // a negative cache hit on org.apache.commons.lang3.SystemUtils + loader.addURL(IOUtils.class.getProtectionDomain().getCodeSource().getLocation()); + loader.loadClass(IOUtils.class.getName()); + + loader.addURL(SystemUtils.class.getProtectionDomain().getCodeSource().getLocation()); + Class clazz = loader.loadClass(SystemUtils.class.getName()); + + assertThat(clazz.getClassLoader()).isEqualTo(loader); + assertThat(clazz.getMethod("getHostName").invoke(null)).isEqualTo("not-the-host-name"); + } + + static class TestUrlClassLoader extends URLClassLoader { + + TestUrlClassLoader() { + super(new URL[0], null); + } + + // silence CodeNarc. URLClassLoader#addURL is protected, this method is public + @SuppressWarnings("UnnecessaryOverridingMethod") + @Override + public void addURL(URL url) { + super.addURL(url); + } + } +} diff --git a/instrumentation/java-http-client/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/httpclient/JavaHttpClientSingletons.java b/instrumentation/java-http-client/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/httpclient/JavaHttpClientSingletons.java index 6d97f8618d2e..9adaf4847367 100644 --- a/instrumentation/java-http-client/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/httpclient/JavaHttpClientSingletons.java +++ b/instrumentation/java-http-client/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/httpclient/JavaHttpClientSingletons.java @@ -7,14 +7,11 @@ import io.opentelemetry.api.GlobalOpenTelemetry; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; -import io.opentelemetry.instrumentation.api.instrumenter.net.PeerServiceAttributesExtractor; import io.opentelemetry.instrumentation.httpclient.internal.HttpHeadersSetter; -import io.opentelemetry.instrumentation.httpclient.internal.JavaHttpClientInstrumenterFactory; -import io.opentelemetry.instrumentation.httpclient.internal.JavaHttpClientNetAttributesGetter; -import io.opentelemetry.javaagent.bootstrap.internal.CommonConfig; +import io.opentelemetry.instrumentation.httpclient.internal.JavaHttpClientInstrumenterBuilderFactory; +import io.opentelemetry.javaagent.bootstrap.internal.JavaagentHttpClientInstrumenters; import java.net.http.HttpRequest; import java.net.http.HttpResponse; -import java.util.Arrays; public class JavaHttpClientSingletons { @@ -24,16 +21,9 @@ public class JavaHttpClientSingletons { static { SETTER = new HttpHeadersSetter(GlobalOpenTelemetry.getPropagators()); - JavaHttpClientNetAttributesGetter netAttributesGetter = new JavaHttpClientNetAttributesGetter(); - INSTRUMENTER = - JavaHttpClientInstrumenterFactory.createInstrumenter( - GlobalOpenTelemetry.get(), - CommonConfig.get().getClientRequestHeaders(), - CommonConfig.get().getClientResponseHeaders(), - Arrays.asList( - PeerServiceAttributesExtractor.create( - netAttributesGetter, CommonConfig.get().getPeerServiceMapping()))); + JavaagentHttpClientInstrumenters.create( + JavaHttpClientInstrumenterBuilderFactory.create(GlobalOpenTelemetry.get())); } public static Instrumenter> instrumenter() { diff --git a/instrumentation/java-http-client/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/httpclient/JavaHttpClientTest.java b/instrumentation/java-http-client/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/httpclient/JavaHttpClientTest.java index 2d9906b1d46d..b3e0e74c6397 100644 --- a/instrumentation/java-http-client/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/httpclient/JavaHttpClientTest.java +++ b/instrumentation/java-http-client/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/httpclient/JavaHttpClientTest.java @@ -8,10 +8,12 @@ import io.opentelemetry.instrumentation.httpclient.AbstractJavaHttpClientTest; import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; import io.opentelemetry.instrumentation.testing.junit.http.HttpClientInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.http.HttpClientTestOptions; import java.net.http.HttpClient; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.extension.RegisterExtension; -public class JavaHttpClientTest extends AbstractJavaHttpClientTest { +public abstract class JavaHttpClientTest extends AbstractJavaHttpClientTest { @RegisterExtension static final InstrumentationExtension testing = HttpClientInstrumentationExtension.forAgent(); @@ -20,4 +22,37 @@ public class JavaHttpClientTest extends AbstractJavaHttpClientTest { protected HttpClient configureHttpClient(HttpClient httpClient) { return httpClient; } + + @Nested + static class Http1ClientTest extends JavaHttpClientTest { + + @Override + protected void configureHttpClientBuilder(HttpClient.Builder httpClientBuilder) { + httpClientBuilder.version(HttpClient.Version.HTTP_1_1); + } + } + + @Nested + static class Http2ClientTest extends JavaHttpClientTest { + + @Override + protected void configureHttpClientBuilder(HttpClient.Builder httpClientBuilder) { + httpClientBuilder.version(HttpClient.Version.HTTP_2); + } + + @Override + protected void configure(HttpClientTestOptions.Builder optionsBuilder) { + super.configure(optionsBuilder); + + optionsBuilder.setHttpProtocolVersion( + uri -> { + String uriString = uri.toString(); + if (uriString.equals("http://localhost:61/") + || uriString.equals("https://192.0.2.1/")) { + return "1.1"; + } + return "2"; + }); + } + } } diff --git a/instrumentation/java-http-client/library/src/main/java/io/opentelemetry/instrumentation/httpclient/JavaHttpClientTelemetryBuilder.java b/instrumentation/java-http-client/library/src/main/java/io/opentelemetry/instrumentation/httpclient/JavaHttpClientTelemetryBuilder.java index 64abb38b821f..66d8f753c300 100644 --- a/instrumentation/java-http-client/library/src/main/java/io/opentelemetry/instrumentation/httpclient/JavaHttpClientTelemetryBuilder.java +++ b/instrumentation/java-http-client/library/src/main/java/io/opentelemetry/instrumentation/httpclient/JavaHttpClientTelemetryBuilder.java @@ -5,31 +5,26 @@ package io.opentelemetry.instrumentation.httpclient; -import static java.util.Collections.emptyList; - import com.google.errorprone.annotations.CanIgnoreReturnValue; import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.instrumentation.api.incubator.builder.internal.DefaultHttpClientInstrumenterBuilder; import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor; +import io.opentelemetry.instrumentation.api.semconv.http.HttpClientAttributesExtractorBuilder; import io.opentelemetry.instrumentation.httpclient.internal.HttpHeadersSetter; -import io.opentelemetry.instrumentation.httpclient.internal.JavaHttpClientInstrumenterFactory; +import io.opentelemetry.instrumentation.httpclient.internal.JavaHttpClientInstrumenterBuilderFactory; import java.net.http.HttpRequest; import java.net.http.HttpResponse; -import java.util.ArrayList; import java.util.List; +import java.util.Set; +import java.util.function.Function; public final class JavaHttpClientTelemetryBuilder { - private final OpenTelemetry openTelemetry; - - private final List>> - additionalExtractors = new ArrayList<>(); - - private List capturedRequestHeaders = emptyList(); - private List capturedResponseHeaders = emptyList(); + private final DefaultHttpClientInstrumenterBuilder> builder; JavaHttpClientTelemetryBuilder(OpenTelemetry openTelemetry) { - this.openTelemetry = openTelemetry; + builder = JavaHttpClientInstrumenterBuilderFactory.create(openTelemetry); } /** @@ -39,38 +34,77 @@ public final class JavaHttpClientTelemetryBuilder { @CanIgnoreReturnValue public JavaHttpClientTelemetryBuilder addAttributeExtractor( AttributesExtractor> attributesExtractor) { - additionalExtractors.add(attributesExtractor); + builder.addAttributeExtractor(attributesExtractor); return this; } /** - * Configures the HTTP client request headers that will be captured as span attributes. + * Configures the HTTP request headers that will be captured as span attributes. * * @param requestHeaders A list of HTTP header names. */ @CanIgnoreReturnValue public JavaHttpClientTelemetryBuilder setCapturedRequestHeaders(List requestHeaders) { - capturedRequestHeaders = requestHeaders; + builder.setCapturedRequestHeaders(requestHeaders); return this; } /** - * Configures the HTTP client response headers that will be captured as span attributes. + * Configures the HTTP response headers that will be captured as span attributes. * * @param responseHeaders A list of HTTP header names. */ @CanIgnoreReturnValue public JavaHttpClientTelemetryBuilder setCapturedResponseHeaders(List responseHeaders) { - capturedResponseHeaders = responseHeaders; + builder.setCapturedResponseHeaders(responseHeaders); return this; } - public JavaHttpClientTelemetry build() { - Instrumenter> instrumenter = - JavaHttpClientInstrumenterFactory.createInstrumenter( - openTelemetry, capturedRequestHeaders, capturedResponseHeaders, additionalExtractors); + /** + * Configures the instrumentation to recognize an alternative set of HTTP request methods. + * + *

By default, this instrumentation defines "known" methods as the ones listed in RFC9110 and the PATCH + * method defined in RFC5789. + * + *

Note: calling this method overrides the default known method sets completely; it does + * not supplement it. + * + * @param knownMethods A set of recognized HTTP request methods. + * @see HttpClientAttributesExtractorBuilder#setKnownMethods(Set) + */ + @CanIgnoreReturnValue + public JavaHttpClientTelemetryBuilder setKnownMethods(Set knownMethods) { + builder.setKnownMethods(knownMethods); + return this; + } + /** + * Configures the instrumentation to emit experimental HTTP client metrics. + * + * @param emitExperimentalHttpClientMetrics {@code true} if the experimental HTTP client metrics + * are to be emitted. + */ + @CanIgnoreReturnValue + public JavaHttpClientTelemetryBuilder setEmitExperimentalHttpClientMetrics( + boolean emitExperimentalHttpClientMetrics) { + builder.setEmitExperimentalHttpClientMetrics(emitExperimentalHttpClientMetrics); + return this; + } + + /** Sets custom {@link SpanNameExtractor} via transform function. */ + @CanIgnoreReturnValue + public JavaHttpClientTelemetryBuilder setSpanNameExtractor( + Function< + SpanNameExtractor, + ? extends SpanNameExtractor> + spanNameExtractorTransformer) { + builder.setSpanNameExtractor(spanNameExtractorTransformer); + return this; + } + + public JavaHttpClientTelemetry build() { return new JavaHttpClientTelemetry( - instrumenter, new HttpHeadersSetter(openTelemetry.getPropagators())); + builder.build(), new HttpHeadersSetter(builder.getOpenTelemetry().getPropagators())); } } diff --git a/instrumentation/java-http-client/library/src/main/java/io/opentelemetry/instrumentation/httpclient/internal/JavaHttpClientAttributesGetter.java b/instrumentation/java-http-client/library/src/main/java/io/opentelemetry/instrumentation/httpclient/internal/JavaHttpClientAttributesGetter.java index 577b4fe2285d..e60a23b8c704 100644 --- a/instrumentation/java-http-client/library/src/main/java/io/opentelemetry/instrumentation/httpclient/internal/JavaHttpClientAttributesGetter.java +++ b/instrumentation/java-http-client/library/src/main/java/io/opentelemetry/instrumentation/httpclient/internal/JavaHttpClientAttributesGetter.java @@ -5,16 +5,13 @@ package io.opentelemetry.instrumentation.httpclient.internal; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpClientAttributesGetter; +import io.opentelemetry.instrumentation.api.semconv.http.HttpClientAttributesGetter; +import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.util.List; import javax.annotation.Nullable; -/** - * This class is internal and is hence not for public use. Its APIs are unstable and can change at - * any time. - */ enum JavaHttpClientAttributesGetter implements HttpClientAttributesGetter> { INSTANCE; @@ -45,4 +42,42 @@ public List getHttpResponseHeader( HttpRequest httpRequest, HttpResponse httpResponse, String name) { return httpResponse.headers().allValues(name); } + + @Nullable + @Override + public String getNetworkProtocolName(HttpRequest request, @Nullable HttpResponse response) { + return "http"; + } + + @Nullable + @Override + public String getNetworkProtocolVersion(HttpRequest request, @Nullable HttpResponse response) { + HttpClient.Version version; + if (response != null) { + version = response.version(); + } else { + version = request.version().orElse(null); + } + if (version == null) { + return null; + } + switch (version) { + case HTTP_1_1: + return "1.1"; + case HTTP_2: + return "2"; + } + return null; + } + + @Override + @Nullable + public String getServerAddress(HttpRequest request) { + return request.uri().getHost(); + } + + @Override + public Integer getServerPort(HttpRequest request) { + return request.uri().getPort(); + } } diff --git a/instrumentation/java-http-client/library/src/main/java/io/opentelemetry/instrumentation/httpclient/internal/JavaHttpClientInstrumenterBuilderFactory.java b/instrumentation/java-http-client/library/src/main/java/io/opentelemetry/instrumentation/httpclient/internal/JavaHttpClientInstrumenterBuilderFactory.java new file mode 100644 index 000000000000..9f9bb506f643 --- /dev/null +++ b/instrumentation/java-http-client/library/src/main/java/io/opentelemetry/instrumentation/httpclient/internal/JavaHttpClientInstrumenterBuilderFactory.java @@ -0,0 +1,27 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.httpclient.internal; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.instrumentation.api.incubator.builder.internal.DefaultHttpClientInstrumenterBuilder; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; + +/** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time. + */ +public class JavaHttpClientInstrumenterBuilderFactory { + private JavaHttpClientInstrumenterBuilderFactory() {} + + private static final String INSTRUMENTATION_NAME = "io.opentelemetry.java-http-client"; + + public static DefaultHttpClientInstrumenterBuilder> create( + OpenTelemetry openTelemetry) { + return new DefaultHttpClientInstrumenterBuilder<>( + INSTRUMENTATION_NAME, openTelemetry, JavaHttpClientAttributesGetter.INSTANCE); + } +} diff --git a/instrumentation/java-http-client/library/src/main/java/io/opentelemetry/instrumentation/httpclient/internal/JavaHttpClientInstrumenterFactory.java b/instrumentation/java-http-client/library/src/main/java/io/opentelemetry/instrumentation/httpclient/internal/JavaHttpClientInstrumenterFactory.java deleted file mode 100644 index c74113e677ef..000000000000 --- a/instrumentation/java-http-client/library/src/main/java/io/opentelemetry/instrumentation/httpclient/internal/JavaHttpClientInstrumenterFactory.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.httpclient.internal; - -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.SpanKindExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpClientAttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpClientAttributesExtractorBuilder; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpClientMetrics; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpSpanNameExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpSpanStatusExtractor; -import java.net.http.HttpRequest; -import java.net.http.HttpResponse; -import java.util.List; - -/** - * This class is internal and is hence not for public use. Its APIs are unstable and can change at - * any time. - */ -public final class JavaHttpClientInstrumenterFactory { - private static final String INSTRUMENTATION_NAME = "io.opentelemetry.java-http-client"; - - public static Instrumenter> createInstrumenter( - OpenTelemetry openTelemetry, - List capturedRequestHeaders, - List capturedResponseHeaders, - List>> - additionalExtractors) { - JavaHttpClientAttributesGetter httpAttributesGetter = JavaHttpClientAttributesGetter.INSTANCE; - - HttpClientAttributesExtractorBuilder> - httpAttributesExtractorBuilder = - HttpClientAttributesExtractor.builder( - httpAttributesGetter, new JavaHttpClientNetAttributesGetter()); - httpAttributesExtractorBuilder.setCapturedRequestHeaders(capturedRequestHeaders); - httpAttributesExtractorBuilder.setCapturedResponseHeaders(capturedResponseHeaders); - - return Instrumenter.>builder( - openTelemetry, INSTRUMENTATION_NAME, HttpSpanNameExtractor.create(httpAttributesGetter)) - .setSpanStatusExtractor(HttpSpanStatusExtractor.create(httpAttributesGetter)) - .addAttributesExtractor(httpAttributesExtractorBuilder.build()) - .addAttributesExtractors(additionalExtractors) - .addOperationMetrics(HttpClientMetrics.get()) - .buildInstrumenter(SpanKindExtractor.alwaysClient()); - } - - private JavaHttpClientInstrumenterFactory() {} -} diff --git a/instrumentation/java-http-client/library/src/main/java/io/opentelemetry/instrumentation/httpclient/internal/JavaHttpClientNetAttributesGetter.java b/instrumentation/java-http-client/library/src/main/java/io/opentelemetry/instrumentation/httpclient/internal/JavaHttpClientNetAttributesGetter.java deleted file mode 100644 index ac37372beea4..000000000000 --- a/instrumentation/java-http-client/library/src/main/java/io/opentelemetry/instrumentation/httpclient/internal/JavaHttpClientNetAttributesGetter.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.httpclient.internal; - -import io.opentelemetry.instrumentation.api.instrumenter.net.NetClientAttributesGetter; -import java.net.http.HttpClient; -import java.net.http.HttpRequest; -import java.net.http.HttpResponse; -import javax.annotation.Nullable; - -/** - * This class is internal and is hence not for public use. Its APIs are unstable and can change at - * any time. - */ -public class JavaHttpClientNetAttributesGetter - implements NetClientAttributesGetter> { - - @Nullable - @Override - public String getNetworkProtocolName(HttpRequest request, @Nullable HttpResponse response) { - return "http"; - } - - @Nullable - @Override - public String getNetworkProtocolVersion(HttpRequest request, @Nullable HttpResponse response) { - HttpClient.Version version; - if (response != null) { - version = response.version(); - } else { - version = request.version().orElse(null); - } - if (version == null) { - return null; - } - switch (version) { - case HTTP_1_1: - return "1.1"; - case HTTP_2: - return "2.0"; - } - return null; - } - - @Override - @Nullable - public String getServerAddress(HttpRequest request) { - return request.uri().getHost(); - } - - @Override - @Nullable - public Integer getServerPort(HttpRequest request) { - return request.uri().getPort(); - } -} diff --git a/instrumentation/java-http-client/library/src/test/java/io/opentelemetry/instrumentation/httpclient/JavaHttpClientTest.java b/instrumentation/java-http-client/library/src/test/java/io/opentelemetry/instrumentation/httpclient/JavaHttpClientTest.java index 5bd0396c96a9..900d916aeb24 100644 --- a/instrumentation/java-http-client/library/src/test/java/io/opentelemetry/instrumentation/httpclient/JavaHttpClientTest.java +++ b/instrumentation/java-http-client/library/src/test/java/io/opentelemetry/instrumentation/httpclient/JavaHttpClientTest.java @@ -8,11 +8,13 @@ import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpClientTest; import io.opentelemetry.instrumentation.testing.junit.http.HttpClientInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.http.HttpClientTestOptions; import java.net.http.HttpClient; import java.util.Collections; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.extension.RegisterExtension; -public class JavaHttpClientTest extends AbstractJavaHttpClientTest { +public abstract class JavaHttpClientTest extends AbstractJavaHttpClientTest { @RegisterExtension static final InstrumentationExtension testing = HttpClientInstrumentationExtension.forLibrary(); @@ -27,4 +29,37 @@ protected HttpClient configureHttpClient(HttpClient httpClient) { .build() .newHttpClient(httpClient); } + + @Nested + static class Http1ClientTest extends JavaHttpClientTest { + + @Override + protected void configureHttpClientBuilder(HttpClient.Builder httpClientBuilder) { + httpClientBuilder.version(HttpClient.Version.HTTP_1_1); + } + } + + @Nested + static class Http2ClientTest extends JavaHttpClientTest { + + @Override + protected void configureHttpClientBuilder(HttpClient.Builder httpClientBuilder) { + httpClientBuilder.version(HttpClient.Version.HTTP_2); + } + + @Override + protected void configure(HttpClientTestOptions.Builder optionsBuilder) { + super.configure(optionsBuilder); + + optionsBuilder.setHttpProtocolVersion( + uri -> { + String uriString = uri.toString(); + if (uriString.equals("http://localhost:61/") + || uriString.equals("https://192.0.2.1/")) { + return "1.1"; + } + return "2"; + }); + } + } } diff --git a/instrumentation/java-http-client/testing/src/main/java/io/opentelemetry/instrumentation/httpclient/AbstractJavaHttpClientTest.java b/instrumentation/java-http-client/testing/src/main/java/io/opentelemetry/instrumentation/httpclient/AbstractJavaHttpClientTest.java index 8c286509ef5e..02e9c3818688 100644 --- a/instrumentation/java-http-client/testing/src/main/java/io/opentelemetry/instrumentation/httpclient/AbstractJavaHttpClientTest.java +++ b/instrumentation/java-http-client/testing/src/main/java/io/opentelemetry/instrumentation/httpclient/AbstractJavaHttpClientTest.java @@ -5,12 +5,11 @@ package io.opentelemetry.instrumentation.httpclient; -import static io.opentelemetry.api.common.AttributeKey.stringKey; - import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpClientTest; import io.opentelemetry.instrumentation.testing.junit.http.HttpClientResult; import io.opentelemetry.instrumentation.testing.junit.http.HttpClientTestOptions; +import io.opentelemetry.semconv.NetworkAttributes; import java.io.IOException; import java.net.URI; import java.net.http.HttpClient; @@ -27,15 +26,17 @@ public abstract class AbstractJavaHttpClientTest extends AbstractHttpClientTest< @BeforeAll void setUp() { - HttpClient httpClient = + HttpClient.Builder httpClientBuilder = HttpClient.newBuilder() - .version(HttpClient.Version.HTTP_1_1) .connectTimeout(CONNECTION_TIMEOUT) - .followRedirects(HttpClient.Redirect.NORMAL) - .build(); + .followRedirects(HttpClient.Redirect.NORMAL); + configureHttpClientBuilder(httpClientBuilder); + HttpClient httpClient = httpClientBuilder.build(); client = configureHttpClient(httpClient); } + protected abstract void configureHttpClientBuilder(HttpClient.Builder httpClientBuilder); + protected abstract HttpClient configureHttpClient(HttpClient httpClient); @Override @@ -83,6 +84,7 @@ protected void configure(HttpClientTestOptions.Builder optionsBuilder) { // TODO nested client span is not created, but context is still injected // which is not what the test expects optionsBuilder.disableTestWithClientParent(); + optionsBuilder.spanEndsAfterBody(); optionsBuilder.setHttpAttributes( uri -> { @@ -92,8 +94,7 @@ protected void configure(HttpClientTestOptions.Builder optionsBuilder) { if ("http://localhost:61/".equals(uri.toString()) || "https://192.0.2.1/".equals(uri.toString()) || uri.toString().contains("/read-timeout")) { - attributes.remove(stringKey("net.protocol.name")); - attributes.remove(stringKey("net.protocol.version")); + attributes.remove(NetworkAttributes.NETWORK_PROTOCOL_VERSION); } return attributes; }); diff --git a/instrumentation/java-util-logging/javaagent/README.md b/instrumentation/java-util-logging/javaagent/README.md index 0d29c36cb338..69df0d1943db 100644 --- a/instrumentation/java-util-logging/javaagent/README.md +++ b/instrumentation/java-util-logging/javaagent/README.md @@ -1,5 +1,5 @@ # Settings for the Java Util Logging instrumentation -| System property | Type | Default | Description | -|---|---------|--|------------------------------------------------------| -| `otel.instrumentation.java-util-logging.experimental-log-attributes` | Boolean | `false` | Enable the capture of experimental span attributes `thread.name` and `thread.id`. | +| System property | Type | Default | Description | +| -------------------------------------------------------------------- | ------- | ------- |----------------------------------------------------------------------------------| +| `otel.instrumentation.java-util-logging.experimental-log-attributes` | Boolean | `false` | Enable the capture of experimental log attributes `thread.name` and `thread.id`. | diff --git a/instrumentation/java-util-logging/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jul/JavaUtilLoggingHelper.java b/instrumentation/java-util-logging/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jul/JavaUtilLoggingHelper.java index bc9867c2160e..780e7307c845 100644 --- a/instrumentation/java-util-logging/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jul/JavaUtilLoggingHelper.java +++ b/instrumentation/java-util-logging/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jul/JavaUtilLoggingHelper.java @@ -12,8 +12,9 @@ import io.opentelemetry.api.logs.LogRecordBuilder; import io.opentelemetry.api.logs.Severity; import io.opentelemetry.context.Context; -import io.opentelemetry.javaagent.bootstrap.internal.InstrumentationConfig; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.opentelemetry.javaagent.bootstrap.internal.AgentInstrumentationConfig; +import io.opentelemetry.semconv.ExceptionAttributes; +import io.opentelemetry.semconv.incubating.ThreadIncubatingAttributes; import java.io.PrintWriter; import java.io.StringWriter; import java.util.concurrent.TimeUnit; @@ -26,7 +27,7 @@ public final class JavaUtilLoggingHelper { private static final Formatter FORMATTER = new AccessibleFormatter(); private static final boolean captureExperimentalAttributes = - InstrumentationConfig.get() + AgentInstrumentationConfig.get() .getBoolean("otel.instrumentation.java-util-logging.experimental-log-attributes", false); public static void capture(Logger logger, LogRecord logRecord) { @@ -86,17 +87,17 @@ private static void mapLogRecord(LogRecordBuilder builder, LogRecord logRecord) if (throwable != null) { // TODO (trask) extract method for recording exception into // io.opentelemetry:opentelemetry-api - attributes.put(SemanticAttributes.EXCEPTION_TYPE, throwable.getClass().getName()); - attributes.put(SemanticAttributes.EXCEPTION_MESSAGE, throwable.getMessage()); + attributes.put(ExceptionAttributes.EXCEPTION_TYPE, throwable.getClass().getName()); + attributes.put(ExceptionAttributes.EXCEPTION_MESSAGE, throwable.getMessage()); StringWriter writer = new StringWriter(); throwable.printStackTrace(new PrintWriter(writer)); - attributes.put(SemanticAttributes.EXCEPTION_STACKTRACE, writer.toString()); + attributes.put(ExceptionAttributes.EXCEPTION_STACKTRACE, writer.toString()); } if (captureExperimentalAttributes) { Thread currentThread = Thread.currentThread(); - attributes.put(SemanticAttributes.THREAD_NAME, currentThread.getName()); - attributes.put(SemanticAttributes.THREAD_ID, currentThread.getId()); + attributes.put(ThreadIncubatingAttributes.THREAD_NAME, currentThread.getName()); + attributes.put(ThreadIncubatingAttributes.THREAD_ID, currentThread.getId()); } builder.setAllAttributes(attributes.build()); diff --git a/instrumentation/java-util-logging/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/jul/JavaUtilLoggingTest.java b/instrumentation/java-util-logging/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/jul/JavaUtilLoggingTest.java index aec5d7a342d3..a18bfd8c871b 100644 --- a/instrumentation/java-util-logging/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/jul/JavaUtilLoggingTest.java +++ b/instrumentation/java-util-logging/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/jul/JavaUtilLoggingTest.java @@ -14,7 +14,8 @@ import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; import io.opentelemetry.sdk.common.InstrumentationScopeInfo; import io.opentelemetry.sdk.logs.data.LogRecordData; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.opentelemetry.semconv.ExceptionAttributes; +import io.opentelemetry.semconv.incubating.ThreadIncubatingAttributes; import java.util.logging.Level; import java.util.logging.Logger; import java.util.stream.Stream; @@ -114,18 +115,18 @@ private static void test( if (logException) { assertThat(log) .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.THREAD_NAME, Thread.currentThread().getName()), - equalTo(SemanticAttributes.THREAD_ID, Thread.currentThread().getId()), - equalTo(SemanticAttributes.EXCEPTION_TYPE, IllegalStateException.class.getName()), - equalTo(SemanticAttributes.EXCEPTION_MESSAGE, "hello"), + equalTo(ThreadIncubatingAttributes.THREAD_NAME, Thread.currentThread().getName()), + equalTo(ThreadIncubatingAttributes.THREAD_ID, Thread.currentThread().getId()), + equalTo(ExceptionAttributes.EXCEPTION_TYPE, IllegalStateException.class.getName()), + equalTo(ExceptionAttributes.EXCEPTION_MESSAGE, "hello"), satisfies( - SemanticAttributes.EXCEPTION_STACKTRACE, + ExceptionAttributes.EXCEPTION_STACKTRACE, v -> v.contains(JavaUtilLoggingTest.class.getName()))); } else { assertThat(log) .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.THREAD_NAME, Thread.currentThread().getName()), - equalTo(SemanticAttributes.THREAD_ID, Thread.currentThread().getId())); + equalTo(ThreadIncubatingAttributes.THREAD_NAME, Thread.currentThread().getName()), + equalTo(ThreadIncubatingAttributes.THREAD_ID, Thread.currentThread().getId())); } if (withParent) { diff --git a/instrumentation/javalin-5.0/javaagent/build.gradle.kts b/instrumentation/javalin-5.0/javaagent/build.gradle.kts new file mode 100644 index 000000000000..e231ce371748 --- /dev/null +++ b/instrumentation/javalin-5.0/javaagent/build.gradle.kts @@ -0,0 +1,24 @@ +plugins { + id("otel.javaagent-instrumentation") +} + +muzzle { + pass { + group.set("io.javalin") + module.set("javalin") + versions.set("[5.0.0,)") + // 3.2.0 depends on org.meteogroup.jbrotli:jbrotli:0.5.0 that is not available in central + skip("3.2.0") + assertInverse.set(true) + } +} + +otelJava { + minJavaVersionSupported.set(JavaVersion.VERSION_11) +} + +dependencies { + library("io.javalin:javalin:5.0.0") + + testInstrumentation(project(":instrumentation:jetty:jetty-11.0:javaagent")) +} diff --git a/instrumentation/javalin-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/javalin/v5_0/JavalinInstrumentation.java b/instrumentation/javalin-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/javalin/v5_0/JavalinInstrumentation.java new file mode 100644 index 000000000000..b7ae2a4a5d0e --- /dev/null +++ b/instrumentation/javalin-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/javalin/v5_0/JavalinInstrumentation.java @@ -0,0 +1,48 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.javalin.v5_0; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.hasSuperType; +import static net.bytebuddy.matcher.ElementMatchers.isInterface; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.not; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import io.javalin.http.Context; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRoute; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRouteSource; +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 JavalinInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return hasSuperType(named("io.javalin.http.Handler")).and(not(isInterface())); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + named("handle").and(takesArgument(0, named("io.javalin.http.Context"))), + this.getClass().getName() + "$HandlerAdapterAdvice"); + } + + @SuppressWarnings("unused") + public static class HandlerAdapterAdvice { + + @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class) + public static void onAfterExecute(@Advice.Argument(0) Context ctx, @Advice.Thrown Throwable t) { + HttpServerRoute.update( + io.opentelemetry.context.Context.current(), + HttpServerRouteSource.CONTROLLER, + ctx.endpointHandlerPath()); + } + } +} diff --git a/instrumentation/javalin-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/javalin/v5_0/JavalinInstrumentationModule.java b/instrumentation/javalin-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/javalin/v5_0/JavalinInstrumentationModule.java new file mode 100644 index 000000000000..09d81be0b083 --- /dev/null +++ b/instrumentation/javalin-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/javalin/v5_0/JavalinInstrumentationModule.java @@ -0,0 +1,34 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.javalin.v5_0; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.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; + +@SuppressWarnings("unused") +@AutoService(InstrumentationModule.class) +public class JavalinInstrumentationModule extends InstrumentationModule { + + public JavalinInstrumentationModule() { + super("javalin", "javalin-5"); + } + + @Override + public List typeInstrumentations() { + return singletonList(new JavalinInstrumentation()); + } + + @Override + public ElementMatcher.Junction classLoaderMatcher() { + return hasClassesNamed("io.javalin.http.Handler"); + } +} diff --git a/instrumentation/javalin-5.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/javalin/v5_0/JavalinTest.java b/instrumentation/javalin-5.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/javalin/v5_0/JavalinTest.java new file mode 100644 index 000000000000..b9c16a4b941c --- /dev/null +++ b/instrumentation/javalin-5.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/javalin/v5_0/JavalinTest.java @@ -0,0 +1,170 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.javalin.v5_0; + +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.satisfies; +import static io.opentelemetry.semconv.HttpAttributes.HTTP_ROUTE; + +import io.javalin.Javalin; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.test.utils.PortUtils; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.semconv.ClientAttributes; +import io.opentelemetry.semconv.ErrorAttributes; +import io.opentelemetry.semconv.HttpAttributes; +import io.opentelemetry.semconv.NetworkAttributes; +import io.opentelemetry.semconv.ServerAttributes; +import io.opentelemetry.semconv.UrlAttributes; +import io.opentelemetry.semconv.UserAgentAttributes; +import io.opentelemetry.testing.internal.armeria.client.WebClient; +import io.opentelemetry.testing.internal.armeria.common.AggregatedHttpResponse; +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; + +class JavalinTest { + + @RegisterExtension + private static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + private static Javalin app; + private static int port; + private static WebClient client; + + @BeforeAll + static void setup() { + port = PortUtils.findOpenPort(); + app = TestJavalinJavaApplication.initJavalin(port); + client = WebClient.of("http://localhost:" + port); + } + + @AfterAll + static void cleanup() { + app.stop(); + } + + @Test + void testSpanNameAndHttpRouteSpanWithPathParamResponseSuccessful() { + String id = "123"; + AggregatedHttpResponse response = client.get("/param/" + id).aggregate().join(); + String content = response.contentUtf8(); + + assertThat(content).isEqualTo(id); + assertThat(response.status().code()).isEqualTo(200); + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("GET /param/{id}") + .hasKind(SpanKind.SERVER) + .hasNoParent() + .hasAttributesSatisfyingExactly( + equalTo(UrlAttributes.URL_SCHEME, "http"), + equalTo(UrlAttributes.URL_PATH, "/param/" + id), + equalTo(HttpAttributes.HTTP_REQUEST_METHOD, "GET"), + equalTo(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, 200), + satisfies( + UserAgentAttributes.USER_AGENT_ORIGINAL, + val -> val.isInstanceOf(String.class)), + equalTo(HTTP_ROUTE, "/param/{id}"), + equalTo(NetworkAttributes.NETWORK_PROTOCOL_VERSION, "1.1"), + equalTo(ServerAttributes.SERVER_ADDRESS, "localhost"), + equalTo(ServerAttributes.SERVER_PORT, port), + equalTo(ClientAttributes.CLIENT_ADDRESS, "127.0.0.1"), + equalTo(NetworkAttributes.NETWORK_PEER_ADDRESS, "127.0.0.1"), + satisfies( + NetworkAttributes.NETWORK_PEER_PORT, + val -> val.isInstanceOf(Long.class))))); + } + + @Test + void testSpanNameAndHttpRouteSpanResponseError() { + client.get("/error").aggregate().join(); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("GET /error") + .hasKind(SpanKind.SERVER) + .hasNoParent() + .hasAttributesSatisfyingExactly( + equalTo(UrlAttributes.URL_SCHEME, "http"), + equalTo(UrlAttributes.URL_PATH, "/error"), + equalTo(HttpAttributes.HTTP_REQUEST_METHOD, "GET"), + equalTo(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, 500), + satisfies( + UserAgentAttributes.USER_AGENT_ORIGINAL, + val -> val.isInstanceOf(String.class)), + equalTo(HTTP_ROUTE, "/error"), + equalTo(NetworkAttributes.NETWORK_PROTOCOL_VERSION, "1.1"), + equalTo(ServerAttributes.SERVER_ADDRESS, "localhost"), + equalTo(ServerAttributes.SERVER_PORT, port), + equalTo(ErrorAttributes.ERROR_TYPE, "500"), + equalTo(ClientAttributes.CLIENT_ADDRESS, "127.0.0.1"), + equalTo(NetworkAttributes.NETWORK_PEER_ADDRESS, "127.0.0.1"), + satisfies( + NetworkAttributes.NETWORK_PEER_PORT, + val -> val.isInstanceOf(Long.class))))); + } + + @Test + public void testSpanNameAndHttpRouteSpanAsyncRouteResponseSuccessful() { + AggregatedHttpResponse response = client.get("/async").aggregate().join(); + + assertThat(response.status().code()).isEqualTo(200); + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("GET /async") + .hasKind(SpanKind.SERVER) + .hasNoParent() + .hasAttributesSatisfyingExactly( + equalTo(UrlAttributes.URL_SCHEME, "http"), + equalTo(UrlAttributes.URL_PATH, "/async"), + equalTo(HttpAttributes.HTTP_REQUEST_METHOD, "GET"), + equalTo(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, 200), + satisfies( + UserAgentAttributes.USER_AGENT_ORIGINAL, + val -> val.isInstanceOf(String.class)), + equalTo(HTTP_ROUTE, "/async"), + equalTo(NetworkAttributes.NETWORK_PROTOCOL_VERSION, "1.1"), + equalTo(ServerAttributes.SERVER_ADDRESS, "localhost"), + equalTo(ServerAttributes.SERVER_PORT, port), + equalTo(ClientAttributes.CLIENT_ADDRESS, "127.0.0.1"), + equalTo(NetworkAttributes.NETWORK_PEER_ADDRESS, "127.0.0.1"), + satisfies( + NetworkAttributes.NETWORK_PEER_PORT, + val -> val.isInstanceOf(Long.class))))); + } + + @Test + void testHttpRouteMetricWithPathParamResponseSuccessful() { + String id = "123"; + AggregatedHttpResponse response = client.get("/param/" + id).aggregate().join(); + String content = response.contentUtf8(); + String instrumentation = "io.opentelemetry.jetty-11.0"; + + assertThat(content).isEqualTo(id); + assertThat(response.status().code()).isEqualTo(200); + testing.waitAndAssertMetrics( + instrumentation, + "http.server.request.duration", + metrics -> + metrics.anySatisfy( + metric -> + assertThat(metric) + .hasHistogramSatisfying( + histogram -> + histogram.hasPointsSatisfying( + point -> point.hasAttribute(HTTP_ROUTE, "/param/{id}"))))); + } +} diff --git a/instrumentation/javalin-5.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/javalin/v5_0/TestJavalinJavaApplication.java b/instrumentation/javalin-5.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/javalin/v5_0/TestJavalinJavaApplication.java new file mode 100644 index 000000000000..14f7ce45987d --- /dev/null +++ b/instrumentation/javalin-5.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/javalin/v5_0/TestJavalinJavaApplication.java @@ -0,0 +1,30 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.javalin.v5_0; + +import io.javalin.Javalin; + +public class TestJavalinJavaApplication { + + private TestJavalinJavaApplication() {} + + public static Javalin initJavalin(int port) { + Javalin app = Javalin.create().start(port); + app.get( + "/param/{id}", + ctx -> { + String paramId = ctx.pathParam("id"); + ctx.result(paramId); + }); + app.get( + "/error", + ctx -> { + throw new RuntimeException("boom"); + }); + app.get("/async", ctx -> ctx.async(() -> ctx.result("ok"))); + return app; + } +} diff --git a/instrumentation/jaxrs-client/jaxrs-client-1.1-testing/build.gradle.kts b/instrumentation/jaxrs-client/jaxrs-client-1.1-testing/build.gradle.kts new file mode 100644 index 000000000000..d78a27b57a60 --- /dev/null +++ b/instrumentation/jaxrs-client/jaxrs-client-1.1-testing/build.gradle.kts @@ -0,0 +1,9 @@ +plugins { + id("otel.javaagent-instrumentation") +} + +dependencies { + testLibrary("com.sun.jersey:jersey-client:1.1.4") + + testInstrumentation(project(":instrumentation:http-url-connection:javaagent")) +} diff --git a/instrumentation/jaxrs-client/jaxrs-client-1.1/javaagent/src/test/groovy/JaxRsClientV1Test.groovy b/instrumentation/jaxrs-client/jaxrs-client-1.1-testing/src/test/groovy/JaxRsClientV1Test.groovy similarity index 60% rename from instrumentation/jaxrs-client/jaxrs-client-1.1/javaagent/src/test/groovy/JaxRsClientV1Test.groovy rename to instrumentation/jaxrs-client/jaxrs-client-1.1-testing/src/test/groovy/JaxRsClientV1Test.groovy index fb21b26a7ff5..58478731e1ae 100644 --- a/instrumentation/jaxrs-client/jaxrs-client-1.1/javaagent/src/test/groovy/JaxRsClientV1Test.groovy +++ b/instrumentation/jaxrs-client/jaxrs-client-1.1-testing/src/test/groovy/JaxRsClientV1Test.groovy @@ -4,6 +4,7 @@ */ import com.sun.jersey.api.client.Client +import com.sun.jersey.api.client.ClientHandlerException import com.sun.jersey.api.client.ClientResponse import com.sun.jersey.api.client.WebResource import com.sun.jersey.api.client.filter.GZIPContentEncodingFilter @@ -11,26 +12,46 @@ import com.sun.jersey.api.client.filter.LoggingFilter import io.opentelemetry.api.common.AttributeKey import io.opentelemetry.instrumentation.test.AgentTestTrait import io.opentelemetry.instrumentation.test.base.HttpClientTest +import io.opentelemetry.semconv.NetworkAttributes import spock.lang.Shared -import static io.opentelemetry.api.common.AttributeKey.stringKey - class JaxRsClientV1Test extends HttpClientTest implements AgentTestTrait { @Shared - Client client = Client.create() + Client client = buildClient(false) + + @Shared + Client clientWithReadTimeout = buildClient(true) - def setupSpec() { + def buildClient(boolean readTimeout) { + def client = Client.create() client.setConnectTimeout(CONNECT_TIMEOUT_MS) - client.setReadTimeout(READ_TIMEOUT_MS) + if (readTimeout) { + client.setReadTimeout(READ_TIMEOUT_MS) + } // Add filters to ensure spans aren't duplicated. client.addFilter(new LoggingFilter()) client.addFilter(new GZIPContentEncodingFilter()) + + return client + } + + Client getClient(URI uri) { + if (uri.toString().contains("/read-timeout")) { + return clientWithReadTimeout + } + return client + } + + @Override + def cleanupSpec() { + client?.destroy() + clientWithReadTimeout?.destroy() } @Override WebResource.Builder buildRequest(String method, URI uri, Map headers) { - def resource = client.resource(uri).requestBuilder + def resource = getClient(uri).resource(uri).requestBuilder headers.each { resource.header(it.key, it.value) } return resource } @@ -38,7 +59,11 @@ class JaxRsClientV1Test extends HttpClientTest implements A @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 + try { + return resource.method(method, ClientResponse, body).status + } catch (ClientHandlerException exception) { + throw exception.getCause() + } } @Override @@ -54,8 +79,12 @@ class JaxRsClientV1Test extends HttpClientTest implements A @Override Set> httpAttributes(URI uri) { def attributes = super.httpAttributes(uri) - attributes.remove(stringKey("net.protocol.name")) - attributes.remove(stringKey("net.protocol.version")) + attributes.remove(NetworkAttributes.NETWORK_PROTOCOL_VERSION) return attributes } + + @Override + boolean testNonStandardHttpMethod() { + false + } } diff --git a/instrumentation/jaxrs-client/jaxrs-client-1.1/javaagent/build.gradle.kts b/instrumentation/jaxrs-client/jaxrs-client-1.1/javaagent/build.gradle.kts deleted file mode 100644 index a7cd23d68147..000000000000 --- a/instrumentation/jaxrs-client/jaxrs-client-1.1/javaagent/build.gradle.kts +++ /dev/null @@ -1,18 +0,0 @@ -plugins { - id("otel.javaagent-instrumentation") -} - -muzzle { - pass { - group.set("com.sun.jersey") - module.set("jersey-client") - versions.set("[1.1,]") - assertInverse.set(true) - } -} - -dependencies { - library("com.sun.jersey:jersey-client:1.1.4") - - testInstrumentation(project(":instrumentation:http-url-connection:javaagent")) -} diff --git a/instrumentation/jaxrs-client/jaxrs-client-1.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrsclient/v1_1/ClientHandlerInstrumentation.java b/instrumentation/jaxrs-client/jaxrs-client-1.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrsclient/v1_1/ClientHandlerInstrumentation.java deleted file mode 100644 index 85f0293f0db1..000000000000 --- a/instrumentation/jaxrs-client/jaxrs-client-1.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrsclient/v1_1/ClientHandlerInstrumentation.java +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.javaagent.instrumentation.jaxrsclient.v1_1; - -import static io.opentelemetry.javaagent.bootstrap.Java8BytecodeBridge.currentContext; -import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.hasClassesNamed; -import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.implementsInterface; -import static io.opentelemetry.javaagent.instrumentation.jaxrsclient.v1_1.JaxRsClientSingletons.instrumenter; -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, named("com.sun.jersey.api.client.ClientRequest"))) - .and(returns(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) { - Context parentContext = currentContext(); - if (instrumenter().shouldStart(parentContext, request)) { - context = instrumenter().start(parentContext, request); - scope = context.makeCurrent(); - } - } - - @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) - public static void onExit( - @Advice.Argument(0) ClientRequest request, - @Advice.Return ClientResponse response, - @Advice.Thrown Throwable throwable, - @Advice.Local("otelContext") Context context, - @Advice.Local("otelScope") Scope scope) { - if (scope == null) { - return; - } - - scope.close(); - instrumenter().end(context, request, response, throwable); - } - } -} diff --git a/instrumentation/jaxrs-client/jaxrs-client-1.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrsclient/v1_1/ClientRequestHeaderSetter.java b/instrumentation/jaxrs-client/jaxrs-client-1.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrsclient/v1_1/ClientRequestHeaderSetter.java deleted file mode 100644 index 53a1c59e2fcf..000000000000 --- a/instrumentation/jaxrs-client/jaxrs-client-1.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrsclient/v1_1/ClientRequestHeaderSetter.java +++ /dev/null @@ -1,18 +0,0 @@ -/* - * 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; - -enum ClientRequestHeaderSetter implements TextMapSetter { - INSTANCE; - - @Override - public void set(ClientRequest carrier, String key, String value) { - carrier.getHeaders().putSingle(key, value); - } -} diff --git a/instrumentation/jaxrs-client/jaxrs-client-1.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrsclient/v1_1/JaxRsClientHttpAttributesGetter.java b/instrumentation/jaxrs-client/jaxrs-client-1.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrsclient/v1_1/JaxRsClientHttpAttributesGetter.java deleted file mode 100644 index 39f50c7a5da4..000000000000 --- a/instrumentation/jaxrs-client/jaxrs-client-1.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrsclient/v1_1/JaxRsClientHttpAttributesGetter.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.javaagent.instrumentation.jaxrsclient.v1_1; - -import static java.util.Collections.emptyList; - -import com.sun.jersey.api.client.ClientRequest; -import com.sun.jersey.api.client.ClientResponse; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpClientAttributesGetter; -import java.util.ArrayList; -import java.util.List; -import javax.annotation.Nullable; - -final class JaxRsClientHttpAttributesGetter - implements HttpClientAttributesGetter { - - @Override - @Nullable - public String getHttpRequestMethod(ClientRequest httpRequest) { - return httpRequest.getMethod(); - } - - @Override - public String getUrlFull(ClientRequest httpRequest) { - return httpRequest.getURI().toString(); - } - - @Override - public List getHttpRequestHeader(ClientRequest httpRequest, String name) { - List rawHeaders = httpRequest.getHeaders().getOrDefault(name, emptyList()); - if (rawHeaders.isEmpty()) { - return emptyList(); - } - List stringHeaders = new ArrayList<>(rawHeaders.size()); - for (Object headerValue : rawHeaders) { - stringHeaders.add(String.valueOf(headerValue)); - } - return stringHeaders; - } - - @Override - public Integer getHttpResponseStatusCode( - ClientRequest httpRequest, ClientResponse httpResponse, @Nullable Throwable error) { - return httpResponse.getStatus(); - } - - @Override - public List getHttpResponseHeader( - ClientRequest httpRequest, ClientResponse httpResponse, String name) { - return httpResponse.getHeaders().getOrDefault(name, emptyList()); - } -} diff --git a/instrumentation/jaxrs-client/jaxrs-client-1.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrsclient/v1_1/JaxRsClientNetAttributesGetter.java b/instrumentation/jaxrs-client/jaxrs-client-1.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrsclient/v1_1/JaxRsClientNetAttributesGetter.java deleted file mode 100644 index 9d610a36b252..000000000000 --- a/instrumentation/jaxrs-client/jaxrs-client-1.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrsclient/v1_1/JaxRsClientNetAttributesGetter.java +++ /dev/null @@ -1,26 +0,0 @@ -/* - * 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 com.sun.jersey.api.client.ClientResponse; -import io.opentelemetry.instrumentation.api.instrumenter.net.NetClientAttributesGetter; -import javax.annotation.Nullable; - -final class JaxRsClientNetAttributesGetter - implements NetClientAttributesGetter { - - @Override - @Nullable - public String getServerAddress(ClientRequest request) { - return request.getURI().getHost(); - } - - @Override - public Integer getServerPort(ClientRequest request) { - return request.getURI().getPort(); - } -} diff --git a/instrumentation/jaxrs-client/jaxrs-client-1.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrsclient/v1_1/JaxRsClientSingletons.java b/instrumentation/jaxrs-client/jaxrs-client-1.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrsclient/v1_1/JaxRsClientSingletons.java deleted file mode 100644 index 0e2e7518d3b9..000000000000 --- a/instrumentation/jaxrs-client/jaxrs-client-1.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrsclient/v1_1/JaxRsClientSingletons.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * 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 com.sun.jersey.api.client.ClientResponse; -import io.opentelemetry.api.GlobalOpenTelemetry; -import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpClientAttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpClientMetrics; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpSpanNameExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpSpanStatusExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.net.PeerServiceAttributesExtractor; -import io.opentelemetry.javaagent.bootstrap.internal.CommonConfig; - -public class JaxRsClientSingletons { - private static final String INSTRUMENTATION_NAME = "io.opentelemetry.jaxrs-client-1.1"; - - private static final Instrumenter INSTRUMENTER; - - static { - JaxRsClientHttpAttributesGetter httpAttributesGetter = new JaxRsClientHttpAttributesGetter(); - JaxRsClientNetAttributesGetter netAttributesGetter = new JaxRsClientNetAttributesGetter(); - - INSTRUMENTER = - Instrumenter.builder( - GlobalOpenTelemetry.get(), - INSTRUMENTATION_NAME, - HttpSpanNameExtractor.create(httpAttributesGetter)) - .setSpanStatusExtractor(HttpSpanStatusExtractor.create(httpAttributesGetter)) - .addAttributesExtractor( - HttpClientAttributesExtractor.builder(httpAttributesGetter, netAttributesGetter) - .setCapturedRequestHeaders(CommonConfig.get().getClientRequestHeaders()) - .setCapturedResponseHeaders(CommonConfig.get().getClientResponseHeaders()) - .build()) - .addAttributesExtractor( - PeerServiceAttributesExtractor.create( - netAttributesGetter, CommonConfig.get().getPeerServiceMapping())) - .addOperationMetrics(HttpClientMetrics.get()) - .buildClientInstrumenter(ClientRequestHeaderSetter.INSTANCE); - } - - public static Instrumenter instrumenter() { - return INSTRUMENTER; - } - - private JaxRsClientSingletons() {} -} diff --git a/instrumentation/jaxrs-client/jaxrs-client-2.0-testing/src/test/groovy/JaxRsClientTest.groovy b/instrumentation/jaxrs-client/jaxrs-client-2.0-testing/src/test/groovy/JaxRsClientTest.groovy index 0c6816b9a432..b41a8efeea62 100644 --- a/instrumentation/jaxrs-client/jaxrs-client-2.0-testing/src/test/groovy/JaxRsClientTest.groovy +++ b/instrumentation/jaxrs-client/jaxrs-client-2.0-testing/src/test/groovy/JaxRsClientTest.groovy @@ -7,7 +7,11 @@ import io.opentelemetry.instrumentation.test.AgentTestTrait import io.opentelemetry.instrumentation.test.base.HttpClientTest import io.opentelemetry.instrumentation.testing.junit.http.HttpClientResult import io.opentelemetry.instrumentation.testing.junit.http.SingleConnection -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import io.opentelemetry.semconv.ServerAttributes +import io.opentelemetry.semconv.ErrorAttributes +import io.opentelemetry.semconv.HttpAttributes +import io.opentelemetry.semconv.NetworkAttributes +import io.opentelemetry.semconv.UrlAttributes import org.apache.cxf.jaxrs.client.spec.ClientBuilderImpl import org.glassfish.jersey.client.ClientConfig import org.glassfish.jersey.client.ClientProperties @@ -33,6 +37,11 @@ abstract class JaxRsClientTest extends HttpClientTest implem false } + @Override + boolean testNonStandardHttpMethod() { + false + } + @Override Invocation.Builder buildRequest(String method, URI uri, Map headers) { return internalBuildRequest(uri, headers) @@ -107,16 +116,14 @@ abstract class JaxRsClientTest extends HttpClientTest implem kind CLIENT status ERROR attributes { - "net.protocol.name" "http" - "net.protocol.version" "1.1" - "$SemanticAttributes.NET_PEER_NAME" uri.host - "$SemanticAttributes.NET_PEER_PORT" uri.port > 0 ? uri.port : { it == null || it == 443 } - "$SemanticAttributes.NET_SOCK_PEER_ADDR" { it == "127.0.0.1" || it == null } - "$SemanticAttributes.HTTP_URL" "${uri}" - "$SemanticAttributes.HTTP_METHOD" method - "$SemanticAttributes.HTTP_STATUS_CODE" statusCode - "$SemanticAttributes.USER_AGENT_ORIGINAL" { it == null || it instanceof String } - "$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" { it == null || it instanceof Long } + "$NetworkAttributes.NETWORK_PROTOCOL_VERSION" "1.1" + "$ServerAttributes.SERVER_ADDRESS" uri.host + "$ServerAttributes.SERVER_PORT" uri.port > 0 ? uri.port : { it == null || it == 443 } + "$NetworkAttributes.NETWORK_PEER_ADDRESS" { it == "127.0.0.1" || it == null } + "$UrlAttributes.URL_FULL" "${uri}" + "$HttpAttributes.HTTP_REQUEST_METHOD" method + "$HttpAttributes.HTTP_RESPONSE_STATUS_CODE" statusCode + "$ErrorAttributes.ERROR_TYPE" "$statusCode" } } serverSpan(it, 1, span(0)) @@ -140,11 +147,6 @@ class JerseyClientTest extends JaxRsClientTest { return new JerseyClientBuilder().withConfig(config) } - @Override - String userAgent() { - "Jersey" - } - @Override SingleConnection createSingleConnection(String host, int port) { // Jersey JAX-RS client uses HttpURLConnection internally, which does not support pipelining nor @@ -198,11 +200,6 @@ class CxfClientTest extends JaxRsClientTest { return false } - @Override - String userAgent() { - "Apache" - } - @Override ClientBuilder builder() { return new ClientBuilderImpl() diff --git a/instrumentation/jaxrs-client/jaxrs-client-2.0-testing/src/test/groovy/ResteasyProxyClientTest.groovy b/instrumentation/jaxrs-client/jaxrs-client-2.0-testing/src/test/groovy/ResteasyProxyClientTest.groovy index d106a7f94efc..d4a5e4556a2b 100644 --- a/instrumentation/jaxrs-client/jaxrs-client-2.0-testing/src/test/groovy/ResteasyProxyClientTest.groovy +++ b/instrumentation/jaxrs-client/jaxrs-client-2.0-testing/src/test/groovy/ResteasyProxyClientTest.groovy @@ -84,6 +84,11 @@ class ResteasyProxyClientTest extends HttpClientTest impl boolean testReadTimeout() { return false } + + @Override + boolean testNonStandardHttpMethod() { + false + } } @Path("") diff --git a/instrumentation/jaxrs/README.md b/instrumentation/jaxrs/README.md index 4deba69a25d8..78b410900746 100644 --- a/instrumentation/jaxrs/README.md +++ b/instrumentation/jaxrs/README.md @@ -1,5 +1,5 @@ # Settings for the Jaxrs instrumentation -| System property | Type | Default | Description | -|---|---|---|---| +| System property | Type | Default | Description | +| --------------------------------------------------------- | ------- | ------- | --------------------------------------------------- | | `otel.instrumentation.jaxrs.experimental-span-attributes` | Boolean | `false` | Enable the capture of experimental span attributes. | diff --git a/instrumentation/jaxrs/jaxrs-1.0/javaagent/build.gradle.kts b/instrumentation/jaxrs/jaxrs-1.0/javaagent/build.gradle.kts index a71653ffc643..1b0f3e6ecceb 100644 --- a/instrumentation/jaxrs/jaxrs-1.0/javaagent/build.gradle.kts +++ b/instrumentation/jaxrs/jaxrs-1.0/javaagent/build.gradle.kts @@ -25,3 +25,7 @@ dependencies { testImplementation("io.dropwizard:dropwizard-testing:0.7.1") testImplementation("javax.xml.bind:jaxb-api:2.2.3") } + +tasks.withType().configureEach { + jvmArgs("-Dotel.instrumentation.common.experimental.controller-telemetry.enabled=true") +} diff --git a/instrumentation/jaxrs/jaxrs-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v1_0/JaxrsAnnotationsInstrumentation.java b/instrumentation/jaxrs/jaxrs-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v1_0/JaxrsAnnotationsInstrumentation.java index 00e75dd3f00a..15c6a6998de3 100644 --- a/instrumentation/jaxrs/jaxrs-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v1_0/JaxrsAnnotationsInstrumentation.java +++ b/instrumentation/jaxrs/jaxrs-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v1_0/JaxrsAnnotationsInstrumentation.java @@ -19,8 +19,8 @@ import io.opentelemetry.context.Context; import io.opentelemetry.context.Scope; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteHolder; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteSource; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRoute; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRouteSource; import io.opentelemetry.javaagent.bootstrap.CallDepth; import io.opentelemetry.javaagent.bootstrap.Java8BytecodeBridge; import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; @@ -83,9 +83,9 @@ public static void nameSpan( Context parentContext = Java8BytecodeBridge.currentContext(); handlerData = new HandlerData(target.getClass(), method); - HttpRouteHolder.updateHttpRoute( + HttpServerRoute.update( parentContext, - HttpRouteSource.CONTROLLER, + HttpServerRouteSource.CONTROLLER, JaxrsServerSpanNaming.SERVER_SPAN_NAME, handlerData); diff --git a/instrumentation/jaxrs/jaxrs-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v1_0/JaxrsCodeAttributesGetter.java b/instrumentation/jaxrs/jaxrs-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v1_0/JaxrsCodeAttributesGetter.java index 7cadc024671b..ca5aa98ac262 100644 --- a/instrumentation/jaxrs/jaxrs-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v1_0/JaxrsCodeAttributesGetter.java +++ b/instrumentation/jaxrs/jaxrs-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v1_0/JaxrsCodeAttributesGetter.java @@ -5,7 +5,7 @@ package io.opentelemetry.javaagent.instrumentation.jaxrs.v1_0; -import io.opentelemetry.instrumentation.api.instrumenter.code.CodeAttributesGetter; +import io.opentelemetry.instrumentation.api.incubator.semconv.code.CodeAttributesGetter; public class JaxrsCodeAttributesGetter implements CodeAttributesGetter { diff --git a/instrumentation/jaxrs/jaxrs-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v1_0/JaxrsInstrumentationModule.java b/instrumentation/jaxrs/jaxrs-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v1_0/JaxrsInstrumentationModule.java index 2cdfe46bbe0f..27b50ae025d1 100644 --- a/instrumentation/jaxrs/jaxrs-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v1_0/JaxrsInstrumentationModule.java +++ b/instrumentation/jaxrs/jaxrs-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v1_0/JaxrsInstrumentationModule.java @@ -10,8 +10,10 @@ import static net.bytebuddy.matcher.ElementMatchers.not; import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.bootstrap.internal.ExperimentalConfig; import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; import java.util.List; import net.bytebuddy.matcher.ElementMatcher; @@ -31,4 +33,12 @@ public ElementMatcher.Junction classLoaderMatcher() { public List typeInstrumentations() { return singletonList(new JaxrsAnnotationsInstrumentation()); } + + @Override + public boolean defaultEnabled(ConfigProperties config) { + // This instrumentation produces controller telemetry and sets http route. Http route is set by + // this instrumentation only when it was not already set by a jax-rs framework instrumentation. + // This instrumentation uses complex type matcher, disabling it can improve startup performance. + return super.defaultEnabled(config) && ExperimentalConfig.get().controllerTelemetryEnabled(); + } } diff --git a/instrumentation/jaxrs/jaxrs-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v1_0/JaxrsServerSpanNaming.java b/instrumentation/jaxrs/jaxrs-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v1_0/JaxrsServerSpanNaming.java index 24d81c86af0b..df0e2dc73eb2 100644 --- a/instrumentation/jaxrs/jaxrs-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v1_0/JaxrsServerSpanNaming.java +++ b/instrumentation/jaxrs/jaxrs-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v1_0/JaxrsServerSpanNaming.java @@ -5,13 +5,13 @@ package io.opentelemetry.javaagent.instrumentation.jaxrs.v1_0; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteGetter; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRouteGetter; import io.opentelemetry.javaagent.bootstrap.jaxrs.JaxrsContextPath; import io.opentelemetry.javaagent.bootstrap.servlet.ServletContextPath; public class JaxrsServerSpanNaming { - public static final HttpRouteGetter SERVER_SPAN_NAME = + public static final HttpServerRouteGetter SERVER_SPAN_NAME = (context, handlerData) -> { String pathBasedSpanName = handlerData.getServerSpanName(); // If path based name is empty skip prepending context path so that path based name would diff --git a/instrumentation/jaxrs/jaxrs-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v1_0/JaxrsSingletons.java b/instrumentation/jaxrs/jaxrs-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v1_0/JaxrsSingletons.java index 47461937e44d..ea59c1902366 100644 --- a/instrumentation/jaxrs/jaxrs-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v1_0/JaxrsSingletons.java +++ b/instrumentation/jaxrs/jaxrs-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v1_0/JaxrsSingletons.java @@ -6,9 +6,9 @@ package io.opentelemetry.javaagent.instrumentation.jaxrs.v1_0; import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.instrumentation.api.incubator.semconv.code.CodeAttributesExtractor; +import io.opentelemetry.instrumentation.api.incubator.semconv.code.CodeSpanNameExtractor; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; -import io.opentelemetry.instrumentation.api.instrumenter.code.CodeAttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.code.CodeSpanNameExtractor; import io.opentelemetry.javaagent.bootstrap.internal.ExperimentalConfig; public final class JaxrsSingletons { diff --git a/instrumentation/jaxrs/jaxrs-1.0/javaagent/src/test/groovy/JaxRsAnnotations1InstrumentationTest.groovy b/instrumentation/jaxrs/jaxrs-1.0/javaagent/src/test/groovy/JaxRsAnnotations1InstrumentationTest.groovy index 145a7ba5e59c..375cb13cdfdf 100644 --- a/instrumentation/jaxrs/jaxrs-1.0/javaagent/src/test/groovy/JaxRsAnnotations1InstrumentationTest.groovy +++ b/instrumentation/jaxrs/jaxrs-1.0/javaagent/src/test/groovy/JaxRsAnnotations1InstrumentationTest.groovy @@ -4,7 +4,9 @@ */ import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import io.opentelemetry.semconv.incubating.CodeIncubatingAttributes +import io.opentelemetry.semconv.ErrorAttributes +import io.opentelemetry.semconv.HttpAttributes import spock.lang.Unroll import javax.ws.rs.DELETE @@ -35,16 +37,17 @@ class JaxRsAnnotations1InstrumentationTest extends AgentInstrumentationSpecifica kind SERVER hasNoParent() attributes { - "$SemanticAttributes.HTTP_METHOD" "GET" - "$SemanticAttributes.HTTP_ROUTE" paramName + "$HttpAttributes.HTTP_REQUEST_METHOD" "GET" + "$HttpAttributes.HTTP_ROUTE" paramName + "$ErrorAttributes.ERROR_TYPE" "_OTHER" } } span(1) { name "${className}.call" childOf span(0) attributes { - "$SemanticAttributes.CODE_NAMESPACE" obj.getClass().getName() - "$SemanticAttributes.CODE_FUNCTION" "call" + "$CodeIncubatingAttributes.CODE_NAMESPACE" obj.getClass().getName() + "$CodeIncubatingAttributes.CODE_FUNCTION" "call" } } } @@ -124,7 +127,8 @@ class JaxRsAnnotations1InstrumentationTest extends AgentInstrumentationSpecifica name "GET" kind SERVER attributes { - "$SemanticAttributes.HTTP_METHOD" "GET" + "$HttpAttributes.HTTP_REQUEST_METHOD" "GET" + "$ErrorAttributes.ERROR_TYPE" "_OTHER" } } } diff --git a/instrumentation/jaxrs/jaxrs-1.0/javaagent/src/test/groovy/JerseyTest.groovy b/instrumentation/jaxrs/jaxrs-1.0/javaagent/src/test/groovy/JerseyTest.groovy index 55c3621f5a67..57305fd90486 100644 --- a/instrumentation/jaxrs/jaxrs-1.0/javaagent/src/test/groovy/JerseyTest.groovy +++ b/instrumentation/jaxrs/jaxrs-1.0/javaagent/src/test/groovy/JerseyTest.groovy @@ -5,7 +5,9 @@ import io.dropwizard.testing.junit.ResourceTestRule import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import io.opentelemetry.semconv.incubating.CodeIncubatingAttributes +import io.opentelemetry.semconv.ErrorAttributes +import io.opentelemetry.semconv.HttpAttributes import org.junit.ClassRule import spock.lang.Shared import spock.lang.Unroll @@ -40,8 +42,9 @@ class JerseyTest extends AgentInstrumentationSpecification { name "GET " + expectedRoute kind SERVER attributes { - "$SemanticAttributes.HTTP_METHOD" "GET" - "$SemanticAttributes.HTTP_ROUTE" expectedRoute + "$HttpAttributes.HTTP_REQUEST_METHOD" "GET" + "$HttpAttributes.HTTP_ROUTE" expectedRoute + "$ErrorAttributes.ERROR_TYPE" "_OTHER" } } @@ -49,8 +52,8 @@ class JerseyTest extends AgentInstrumentationSpecification { childOf span(0) name controllerName attributes { - "$SemanticAttributes.CODE_NAMESPACE" ~/Resource[$]Test*/ - "$SemanticAttributes.CODE_FUNCTION" "hello" + "$CodeIncubatingAttributes.CODE_NAMESPACE" ~/Resource[$]Test*/ + "$CodeIncubatingAttributes.CODE_FUNCTION" "hello" } } } @@ -80,8 +83,9 @@ class JerseyTest extends AgentInstrumentationSpecification { name "GET " + expectedRoute kind SERVER attributes { - "$SemanticAttributes.HTTP_METHOD" "GET" - "$SemanticAttributes.HTTP_ROUTE" expectedRoute + "$HttpAttributes.HTTP_REQUEST_METHOD" "GET" + "$HttpAttributes.HTTP_ROUTE" expectedRoute + "$ErrorAttributes.ERROR_TYPE" "_OTHER" } } span(1) { @@ -89,8 +93,8 @@ class JerseyTest extends AgentInstrumentationSpecification { name controller1Name kind INTERNAL attributes { - "$SemanticAttributes.CODE_NAMESPACE" ~/Resource[$]Test*/ - "$SemanticAttributes.CODE_FUNCTION" "nested" + "$CodeIncubatingAttributes.CODE_NAMESPACE" ~/Resource[$]Test*/ + "$CodeIncubatingAttributes.CODE_FUNCTION" "nested" } } } diff --git a/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-annotations/javaagent/build.gradle.kts b/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-annotations/javaagent/build.gradle.kts index 4a86dcdd4f69..4cd18bf4d3e5 100644 --- a/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-annotations/javaagent/build.gradle.kts +++ b/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-annotations/javaagent/build.gradle.kts @@ -24,3 +24,9 @@ dependencies { testImplementation("javax.ws.rs:javax.ws.rs-api:2.0") } + +tasks { + withType().configureEach { + jvmArgs("-Dotel.instrumentation.common.experimental.controller-telemetry.enabled=true") + } +} diff --git a/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-annotations/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/DefaultRequestContextInstrumentation.java b/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-annotations/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/DefaultRequestContextInstrumentation.java index 7c903bbd8435..e29ce0cea55c 100644 --- a/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-annotations/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/DefaultRequestContextInstrumentation.java +++ b/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-annotations/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/DefaultRequestContextInstrumentation.java @@ -9,8 +9,8 @@ import io.opentelemetry.context.Context; import io.opentelemetry.context.Scope; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteHolder; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteSource; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRoute; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRouteSource; import io.opentelemetry.javaagent.bootstrap.Java8BytecodeBridge; import io.opentelemetry.javaagent.instrumentation.jaxrs.JaxrsConstants; import io.opentelemetry.javaagent.instrumentation.jaxrs.JaxrsServerSpanNaming; @@ -64,9 +64,9 @@ public static void createGenericSpan( Context parentContext = Java8BytecodeBridge.currentContext(); handlerData = new Jaxrs2HandlerData(filterClass, method); - HttpRouteHolder.updateHttpRoute( + HttpServerRoute.update( parentContext, - HttpRouteSource.CONTROLLER, + HttpServerRouteSource.CONTROLLER, JaxrsServerSpanNaming.SERVER_SPAN_NAME, handlerData); diff --git a/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-annotations/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/JaxrsAnnotationsInstrumentation.java b/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-annotations/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/JaxrsAnnotationsInstrumentation.java index 87f957abed6a..776127b07e2d 100644 --- a/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-annotations/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/JaxrsAnnotationsInstrumentation.java +++ b/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-annotations/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/JaxrsAnnotationsInstrumentation.java @@ -18,8 +18,8 @@ import io.opentelemetry.context.Context; import io.opentelemetry.context.Scope; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteHolder; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteSource; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRoute; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRouteSource; import io.opentelemetry.instrumentation.api.util.VirtualField; import io.opentelemetry.javaagent.bootstrap.CallDepth; import io.opentelemetry.javaagent.bootstrap.Java8BytecodeBridge; @@ -105,9 +105,9 @@ public static void nameSpan( Context parentContext = Java8BytecodeBridge.currentContext(); handlerData = new Jaxrs2HandlerData(target.getClass(), method); - HttpRouteHolder.updateHttpRoute( + HttpServerRoute.update( parentContext, - HttpRouteSource.CONTROLLER, + HttpServerRouteSource.CONTROLLER, JaxrsServerSpanNaming.SERVER_SPAN_NAME, handlerData); diff --git a/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-annotations/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/JaxrsAnnotationsInstrumentationModule.java b/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-annotations/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/JaxrsAnnotationsInstrumentationModule.java index 3639a19b473d..ee93dd45e3fc 100644 --- a/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-annotations/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/JaxrsAnnotationsInstrumentationModule.java +++ b/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-annotations/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/JaxrsAnnotationsInstrumentationModule.java @@ -9,8 +9,10 @@ import static java.util.Arrays.asList; import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.bootstrap.internal.ExperimentalConfig; import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; import java.util.List; import net.bytebuddy.matcher.ElementMatcher; @@ -34,4 +36,12 @@ public List typeInstrumentations() { new JaxrsAnnotationsInstrumentation(), new JaxrsAsyncResponseInstrumentation()); } + + @Override + public boolean defaultEnabled(ConfigProperties config) { + // This instrumentation produces controller telemetry and sets http route. Http route is set by + // this instrumentation only when it was not already set by a jax-rs framework instrumentation. + // This instrumentation uses complex type matcher, disabling it can improve startup performance. + return super.defaultEnabled(config) && ExperimentalConfig.get().controllerTelemetryEnabled(); + } } diff --git a/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-annotations/javaagent/src/test/groovy/JaxrsAnnotationsInstrumentationTest.groovy b/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-annotations/javaagent/src/test/groovy/JaxrsAnnotationsInstrumentationTest.groovy index d5c4b4d415f1..a5e10807fec1 100644 --- a/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-annotations/javaagent/src/test/groovy/JaxrsAnnotationsInstrumentationTest.groovy +++ b/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-annotations/javaagent/src/test/groovy/JaxrsAnnotationsInstrumentationTest.groovy @@ -4,7 +4,9 @@ */ import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import io.opentelemetry.semconv.incubating.CodeIncubatingAttributes +import io.opentelemetry.semconv.ErrorAttributes +import io.opentelemetry.semconv.HttpAttributes import spock.lang.Unroll import javax.ws.rs.DELETE @@ -35,16 +37,17 @@ class JaxrsAnnotationsInstrumentationTest extends AgentInstrumentationSpecificat kind SERVER hasNoParent() attributes { - "$SemanticAttributes.HTTP_METHOD" "GET" - "$SemanticAttributes.HTTP_ROUTE" paramName + "$HttpAttributes.HTTP_REQUEST_METHOD" "GET" + "$HttpAttributes.HTTP_ROUTE" paramName + "$ErrorAttributes.ERROR_TYPE" "_OTHER" } } span(1) { name "${className}.call" childOf span(0) attributes { - "$SemanticAttributes.CODE_NAMESPACE" obj.getClass().getName() - "$SemanticAttributes.CODE_FUNCTION" "call" + "$CodeIncubatingAttributes.CODE_NAMESPACE" obj.getClass().getName() + "$CodeIncubatingAttributes.CODE_FUNCTION" "call" } } } @@ -124,7 +127,8 @@ class JaxrsAnnotationsInstrumentationTest extends AgentInstrumentationSpecificat name "GET" kind SERVER attributes { - "$SemanticAttributes.HTTP_METHOD" "GET" + "$HttpAttributes.HTTP_REQUEST_METHOD" "GET" + "$ErrorAttributes.ERROR_TYPE" "_OTHER" } } } diff --git a/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-common/testing/src/main/groovy/JaxRsHttpServerTest.groovy b/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-common/testing/src/main/groovy/JaxRsHttpServerTest.groovy index e01ef29f638f..97391da68a1e 100644 --- a/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-common/testing/src/main/groovy/JaxRsHttpServerTest.groovy +++ b/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-common/testing/src/main/groovy/JaxRsHttpServerTest.groovy @@ -8,6 +8,11 @@ import test.JaxRsTestResource abstract class JaxRsHttpServerTest extends AbstractJaxRsHttpServerTest { + def setup() { + // reset the barrier to avoid a failing test breaking subsequent tests + JaxRsTestResource.BARRIER.reset() + } + @Override void awaitBarrier(int amount, TimeUnit timeUnit) { JaxRsTestResource.BARRIER.await(amount, timeUnit) diff --git a/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-common/testing/src/main/groovy/JaxRsJettyHttpServerTest.groovy b/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-common/testing/src/main/groovy/JaxRsJettyHttpServerTest.groovy index 4e310fcaa266..e817cb56e050 100644 --- a/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-common/testing/src/main/groovy/JaxRsJettyHttpServerTest.groovy +++ b/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-common/testing/src/main/groovy/JaxRsJettyHttpServerTest.groovy @@ -3,6 +3,8 @@ * SPDX-License-Identifier: Apache-2.0 */ +import io.opentelemetry.instrumentation.api.internal.HttpConstants +import io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint import org.eclipse.jetty.server.Server import org.eclipse.jetty.webapp.WebAppContext @@ -38,4 +40,12 @@ abstract class JaxRsJettyHttpServerTest extends JaxRsHttpServerTest { String getContextPath() { "/rest-app" } + + @Override + String expectedHttpRoute(ServerEndpoint endpoint, String method) { + if (method == HttpConstants._OTHER) { + return "${getContextPath()}/*" + } + return super.expectedHttpRoute(endpoint, method) + } } diff --git a/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-common/testing/src/main/groovy/test/JaxRsTestResource.groovy b/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-common/testing/src/main/groovy/test/JaxRsTestResource.groovy index f72f132e1208..92b3ae286d63 100644 --- a/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-common/testing/src/main/groovy/test/JaxRsTestResource.groovy +++ b/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-common/testing/src/main/groovy/test/JaxRsTestResource.groovy @@ -119,7 +119,7 @@ class JaxRsTestResource { 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) + BARRIER.await(10, SECONDS) switch (action) { case "succeed": @@ -144,7 +144,7 @@ class JaxRsTestResource { def result = new CompletableFuture() CompletableFuture.runAsync({ // await for the test method to verify that there are no spans yet - BARRIER.await(1, SECONDS) + BARRIER.await(10, SECONDS) switch (action) { case "succeed": diff --git a/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-cxf-3.2/javaagent/build.gradle.kts b/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-cxf-3.2/javaagent/build.gradle.kts index c234a6e62e70..ae7efabc72df 100644 --- a/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-cxf-3.2/javaagent/build.gradle.kts +++ b/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-cxf-3.2/javaagent/build.gradle.kts @@ -53,6 +53,9 @@ dependencies { } tasks.withType().configureEach { + systemProperty("testLatestDeps", findProperty("testLatestDeps") as Boolean) + // TODO run tests both with and without experimental span attributes jvmArgs("-Dotel.instrumentation.jaxrs.experimental-span-attributes=true") + jvmArgs("-Dotel.instrumentation.common.experimental.controller-telemetry.enabled=true") } diff --git a/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-cxf-3.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/CxfSpanName.java b/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-cxf-3.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/CxfSpanName.java index c156c16190e2..0c37a8fae70f 100644 --- a/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-cxf-3.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/CxfSpanName.java +++ b/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-cxf-3.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/CxfSpanName.java @@ -8,9 +8,9 @@ import static io.opentelemetry.javaagent.instrumentation.jaxrs.JaxrsPathUtil.normalizePath; import io.opentelemetry.context.Context; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteGetter; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteHolder; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteSource; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRoute; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRouteGetter; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRouteSource; import io.opentelemetry.javaagent.bootstrap.jaxrs.JaxrsContextPath; import io.opentelemetry.javaagent.bootstrap.servlet.ServletContextPath; import org.apache.cxf.jaxrs.model.ClassResourceInfo; @@ -18,7 +18,7 @@ import org.apache.cxf.jaxrs.model.URITemplate; import org.apache.cxf.message.Exchange; -public final class CxfSpanName implements HttpRouteGetter { +public final class CxfSpanName implements HttpServerRouteGetter { public static final CxfSpanName INSTANCE = new CxfSpanName(); @@ -26,7 +26,7 @@ public Context updateServerSpanName(Exchange exchange) { Context context = Context.current(); String jaxrsName = calculateJaxrsName(context, exchange); - HttpRouteHolder.updateHttpRoute(context, HttpRouteSource.NESTED_CONTROLLER, this, jaxrsName); + HttpServerRoute.update(context, HttpServerRouteSource.NESTED_CONTROLLER, this, jaxrsName); return JaxrsContextPath.init(context, jaxrsName); } diff --git a/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-cxf-3.2/javaagent/src/test/groovy/CxfHttpServerTest.groovy b/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-cxf-3.2/javaagent/src/test/groovy/CxfHttpServerTest.groovy index fe0307b581be..534816ffecdd 100644 --- a/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-cxf-3.2/javaagent/src/test/groovy/CxfHttpServerTest.groovy +++ b/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-cxf-3.2/javaagent/src/test/groovy/CxfHttpServerTest.groovy @@ -40,4 +40,9 @@ class CxfHttpServerTest extends JaxRsHttpServerTest { void stopServer(Server httpServer) { httpServer.stop() } + + @Override + int getResponseCodeOnNonStandardHttpMethod() { + Boolean.getBoolean("testLatestDeps") ? 500 : 405 + } } diff --git a/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-cxf-3.2/javaagent/src/test/groovy/CxfJettyHttpServerTest.groovy b/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-cxf-3.2/javaagent/src/test/groovy/CxfJettyHttpServerTest.groovy index cc2487a4dce3..7bf37d74f381 100644 --- a/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-cxf-3.2/javaagent/src/test/groovy/CxfJettyHttpServerTest.groovy +++ b/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-cxf-3.2/javaagent/src/test/groovy/CxfJettyHttpServerTest.groovy @@ -4,4 +4,9 @@ */ class CxfJettyHttpServerTest extends JaxRsJettyHttpServerTest { + + @Override + int getResponseCodeOnNonStandardHttpMethod() { + Boolean.getBoolean("testLatestDeps") ? 500 : 405 + } } diff --git a/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-jersey-2.0/javaagent/build.gradle.kts b/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-jersey-2.0/javaagent/build.gradle.kts index 5430bc8e3128..0e88dcd78a0d 100644 --- a/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-jersey-2.0/javaagent/build.gradle.kts +++ b/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-jersey-2.0/javaagent/build.gradle.kts @@ -72,5 +72,6 @@ tasks { // required on jdk17 jvmArgs("--add-opens=java.base/java.lang=ALL-UNNAMED") jvmArgs("-XX:+IgnoreUnrecognizedVMOptions") + jvmArgs("-Dotel.instrumentation.common.experimental.controller-telemetry.enabled=true") } } diff --git a/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/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-jersey-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/JerseyInstrumentationModule.java index a085abaaefa7..1e2a1e296bc0 100644 --- a/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/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-jersey-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/JerseyInstrumentationModule.java @@ -25,6 +25,14 @@ public ElementMatcher.Junction classLoaderMatcher() { return hasClassesNamed("javax.ws.rs.Path", "org.glassfish.jersey.server.ContainerRequest"); } + @Override + public boolean isIndyModule() { + // net.bytebuddy.pool.TypePool$Resolution$NoSuchTypeException: Cannot resolve type description + // for + // io.opentelemetry.javaagent.instrumentation.servlet.v3_0.snippet.Servlet3SnippetInjectingResponseWrapper + return false; + } + @Override public List typeInstrumentations() { return asList( diff --git a/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-jersey-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/JerseySpanName.java b/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-jersey-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/JerseySpanName.java index bcb0d83523a3..e7855f28dc83 100644 --- a/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-jersey-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/JerseySpanName.java +++ b/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-jersey-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/JerseySpanName.java @@ -8,9 +8,9 @@ import static io.opentelemetry.javaagent.instrumentation.jaxrs.JaxrsPathUtil.normalizePath; import io.opentelemetry.context.Context; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteGetter; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteHolder; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteSource; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRoute; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRouteGetter; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRouteSource; import io.opentelemetry.javaagent.bootstrap.jaxrs.JaxrsContextPath; import io.opentelemetry.javaagent.bootstrap.servlet.ServletContextPath; import javax.annotation.Nullable; @@ -19,13 +19,13 @@ import org.glassfish.jersey.server.ContainerRequest; import org.glassfish.jersey.server.ExtendedUriInfo; -public class JerseySpanName implements HttpRouteGetter { +public class JerseySpanName implements HttpServerRouteGetter { public static final JerseySpanName INSTANCE = new JerseySpanName(); public void updateServerSpanName(Request request) { Context context = Context.current(); - HttpRouteHolder.updateHttpRoute(context, HttpRouteSource.NESTED_CONTROLLER, this, request); + HttpServerRoute.update(context, HttpServerRouteSource.NESTED_CONTROLLER, this, request); } @Override diff --git a/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-jersey-2.0/javaagent/src/test/groovy/JerseyHttpServerTest.groovy b/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-jersey-2.0/javaagent/src/test/groovy/JerseyHttpServerTest.groovy index 58eea5ff8f45..05d37cd00ef2 100644 --- a/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-jersey-2.0/javaagent/src/test/groovy/JerseyHttpServerTest.groovy +++ b/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-jersey-2.0/javaagent/src/test/groovy/JerseyHttpServerTest.groovy @@ -3,6 +3,8 @@ * SPDX-License-Identifier: Apache-2.0 */ +import io.opentelemetry.instrumentation.api.internal.HttpConstants +import io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint import org.eclipse.jetty.server.Server import org.eclipse.jetty.servlet.ServletContextHandler import org.eclipse.jetty.servlet.ServletHolder @@ -42,4 +44,17 @@ class JerseyHttpServerTest extends JaxRsHttpServerTest { // disables a test that jersey deems invalid false } + + @Override + String expectedHttpRoute(ServerEndpoint endpoint, String method) { + if (method == HttpConstants._OTHER) { + return "${getContextPath()}/*" + } + return super.expectedHttpRoute(endpoint, method) + } + + @Override + int getResponseCodeOnNonStandardHttpMethod() { + 500 + } } diff --git a/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-jersey-2.0/javaagent/src/test/groovy/JerseyJettyHttpServerTest.groovy b/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-jersey-2.0/javaagent/src/test/groovy/JerseyJettyHttpServerTest.groovy index f1af4c75b0a3..97c4c1c28642 100644 --- a/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-jersey-2.0/javaagent/src/test/groovy/JerseyJettyHttpServerTest.groovy +++ b/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-jersey-2.0/javaagent/src/test/groovy/JerseyJettyHttpServerTest.groovy @@ -15,4 +15,9 @@ class JerseyJettyHttpServerTest extends JaxRsJettyHttpServerTest { // disables a test that jersey deems invalid false } + + @Override + int getResponseCodeOnNonStandardHttpMethod() { + 500 + } } diff --git a/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-payara-testing/build.gradle.kts b/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-payara-testing/build.gradle.kts index bc9a245063b3..547641ce41d3 100644 --- a/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-payara-testing/build.gradle.kts +++ b/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-payara-testing/build.gradle.kts @@ -16,3 +16,9 @@ dependencies { testInstrumentation(project(":instrumentation:jaxrs:jaxrs-2.0:jaxrs-2.0-annotations:javaagent")) testInstrumentation(project(":instrumentation:jaxrs:jaxrs-2.0:jaxrs-2.0-jersey-2.0:javaagent")) } + +tasks { + withType().configureEach { + jvmArgs("-Dotel.instrumentation.common.experimental.controller-telemetry.enabled=true") + } +} diff --git a/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-resteasy-3.0/javaagent/build.gradle.kts b/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-resteasy-3.0/javaagent/build.gradle.kts index 9d87ce6bd674..bc024e6d2b40 100644 --- a/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-resteasy-3.0/javaagent/build.gradle.kts +++ b/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-resteasy-3.0/javaagent/build.gradle.kts @@ -78,5 +78,6 @@ tasks { withType().configureEach { // TODO run tests both with and without experimental span attributes jvmArgs("-Dotel.instrumentation.jaxrs.experimental-span-attributes=true") + jvmArgs("-Dotel.instrumentation.common.experimental.controller-telemetry.enabled=true") } } diff --git a/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-resteasy-3.0/javaagent/src/test/groovy/ResteasyHttpServerTest.groovy b/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-resteasy-3.0/javaagent/src/test/groovy/ResteasyHttpServerTest.groovy index e026acb012a4..94835308b6bb 100644 --- a/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-resteasy-3.0/javaagent/src/test/groovy/ResteasyHttpServerTest.groovy +++ b/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-resteasy-3.0/javaagent/src/test/groovy/ResteasyHttpServerTest.groovy @@ -3,6 +3,8 @@ * SPDX-License-Identifier: Apache-2.0 */ +import io.opentelemetry.instrumentation.api.internal.HttpConstants +import io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint import io.undertow.Undertow import org.jboss.resteasy.plugins.server.undertow.UndertowJaxrsServer import test.JaxRsTestApplication @@ -33,4 +35,17 @@ class ResteasyHttpServerTest extends JaxRsHttpServerTest { boolean shouldTestCompletableStageAsync() { false } + + @Override + String expectedHttpRoute(ServerEndpoint endpoint, String method) { + if (method == HttpConstants._OTHER) { + return "${getContextPath()}/*" + } + return super.expectedHttpRoute(endpoint, method) + } + + @Override + int getResponseCodeOnNonStandardHttpMethod() { + 500 + } } diff --git a/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-resteasy-3.0/javaagent/src/test/groovy/ResteasyJettyHttpServerTest.groovy b/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-resteasy-3.0/javaagent/src/test/groovy/ResteasyJettyHttpServerTest.groovy index 55e32477f463..4691531a7292 100644 --- a/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-resteasy-3.0/javaagent/src/test/groovy/ResteasyJettyHttpServerTest.groovy +++ b/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-resteasy-3.0/javaagent/src/test/groovy/ResteasyJettyHttpServerTest.groovy @@ -9,4 +9,9 @@ class ResteasyJettyHttpServerTest extends JaxRsJettyHttpServerTest { boolean shouldTestCompletableStageAsync() { false } + + @Override + int getResponseCodeOnNonStandardHttpMethod() { + 500 + } } diff --git a/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-resteasy-3.1/javaagent/build.gradle.kts b/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-resteasy-3.1/javaagent/build.gradle.kts index 89720f1b1683..569ff6dd661b 100644 --- a/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-resteasy-3.1/javaagent/build.gradle.kts +++ b/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-resteasy-3.1/javaagent/build.gradle.kts @@ -75,6 +75,7 @@ tasks { withType().configureEach { // TODO run tests both with and without experimental span attributes jvmArgs("-Dotel.instrumentation.jaxrs.experimental-span-attributes=true") + jvmArgs("-Dotel.instrumentation.common.experimental.controller-telemetry.enabled=true") } } diff --git a/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-resteasy-3.1/javaagent/src/test/groovy/ResteasyHttpServerTest.groovy b/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-resteasy-3.1/javaagent/src/test/groovy/ResteasyHttpServerTest.groovy index 7922087750f7..113b2baab4cf 100644 --- a/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-resteasy-3.1/javaagent/src/test/groovy/ResteasyHttpServerTest.groovy +++ b/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-resteasy-3.1/javaagent/src/test/groovy/ResteasyHttpServerTest.groovy @@ -3,6 +3,8 @@ * SPDX-License-Identifier: Apache-2.0 */ +import io.opentelemetry.instrumentation.api.internal.HttpConstants +import io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint import io.undertow.Undertow import org.jboss.resteasy.plugins.server.undertow.UndertowJaxrsServer import test.JaxRsTestApplication @@ -27,4 +29,17 @@ class ResteasyHttpServerTest extends JaxRsHttpServerTest { void stopServer(UndertowJaxrsServer server) { server.stop() } + + @Override + String expectedHttpRoute(ServerEndpoint endpoint, String method) { + if (method == HttpConstants._OTHER) { + return "${getContextPath()}/*" + } + return super.expectedHttpRoute(endpoint, method) + } + + @Override + int getResponseCodeOnNonStandardHttpMethod() { + 500 + } } diff --git a/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-resteasy-3.1/javaagent/src/test/groovy/ResteasyJettyHttpServerTest.groovy b/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-resteasy-3.1/javaagent/src/test/groovy/ResteasyJettyHttpServerTest.groovy index 753f96cd0707..8d92d379711a 100644 --- a/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-resteasy-3.1/javaagent/src/test/groovy/ResteasyJettyHttpServerTest.groovy +++ b/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-resteasy-3.1/javaagent/src/test/groovy/ResteasyJettyHttpServerTest.groovy @@ -5,4 +5,8 @@ class ResteasyJettyHttpServerTest extends JaxRsJettyHttpServerTest { + @Override + int getResponseCodeOnNonStandardHttpMethod() { + 500 + } } diff --git a/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-resteasy-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/ResteasySpanName.java b/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-resteasy-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/ResteasySpanName.java index bf7d2ddbe77b..484cdd74b770 100644 --- a/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-resteasy-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/ResteasySpanName.java +++ b/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-resteasy-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/ResteasySpanName.java @@ -5,22 +5,21 @@ package io.opentelemetry.javaagent.instrumentation.jaxrs.v2_0; -import static io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteSource.NESTED_CONTROLLER; - import io.opentelemetry.context.Context; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteGetter; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteHolder; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRoute; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRouteGetter; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRouteSource; import io.opentelemetry.javaagent.bootstrap.jaxrs.JaxrsContextPath; import io.opentelemetry.javaagent.bootstrap.servlet.ServletContextPath; import javax.annotation.Nullable; -public final class ResteasySpanName implements HttpRouteGetter { +public final class ResteasySpanName implements HttpServerRouteGetter { public static final ResteasySpanName INSTANCE = new ResteasySpanName(); public void updateServerSpanName(Context context, String name) { if (name != null) { - HttpRouteHolder.updateHttpRoute(context, NESTED_CONTROLLER, this, name); + HttpServerRoute.update(context, HttpServerRouteSource.NESTED_CONTROLLER, this, name); } } diff --git a/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-tomee-testing/build.gradle.kts b/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-tomee-testing/build.gradle.kts index 317ee786df37..3ae8f9175d7b 100644 --- a/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-tomee-testing/build.gradle.kts +++ b/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-tomee-testing/build.gradle.kts @@ -19,4 +19,5 @@ tasks.withType().configureEach { jvmArgs("--add-opens=java.base/java.lang=ALL-UNNAMED") jvmArgs("--add-exports=java.base/sun.misc=ALL-UNNAMED") jvmArgs("-XX:+IgnoreUnrecognizedVMOptions") + jvmArgs("-Dotel.instrumentation.common.experimental.controller-telemetry.enabled=true") } diff --git a/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-wildfly-testing/build.gradle.kts b/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-wildfly-testing/build.gradle.kts index 8c12fdcf5ebe..7bab04b0f922 100644 --- a/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-wildfly-testing/build.gradle.kts +++ b/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-wildfly-testing/build.gradle.kts @@ -32,14 +32,16 @@ tasks { // that breaks deploy on embedded wildfly // create a copy of logback-classic jar that does not have this file val modifyLogbackJar by registering(Jar::class) { - destinationDirectory.set(file("$buildDir/tmp")) + destinationDirectory.set(layout.buildDirectory.dir("tmp")) archiveFileName.set("logback-classic-modified.jar") exclude("/META-INF/services/javax.servlet.ServletContainerInitializer") doFirst { configurations.configureEach { if (name.lowercase().endsWith("testruntimeclasspath")) { val logbackJar = find { it.name.contains("logback-classic") } - from(zipTree(logbackJar)) + logbackJar?.let { + from(zipTree(logbackJar)) + } } } } @@ -62,7 +64,7 @@ tasks { !it.absolutePath.contains("logback-classic") } // add modified copy of logback-classic to classpath - classpath = classpath.plus(files("$buildDir/tmp/logback-classic-modified.jar")) + classpath = classpath.plus(files(layout.buildDirectory.file("tmp/logback-classic-modified.jar"))) } } } @@ -71,4 +73,5 @@ tasks.withType().configureEach { // required on jdk17 jvmArgs("--add-opens=java.base/java.lang=ALL-UNNAMED") jvmArgs("-XX:+IgnoreUnrecognizedVMOptions") + jvmArgs("-Dotel.instrumentation.common.experimental.controller-telemetry.enabled=true") } diff --git a/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-annotations/javaagent/build.gradle.kts b/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-annotations/javaagent/build.gradle.kts index d21429e52eaf..754031ecc78f 100644 --- a/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-annotations/javaagent/build.gradle.kts +++ b/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-annotations/javaagent/build.gradle.kts @@ -20,3 +20,9 @@ dependencies { testImplementation("jakarta.ws.rs:jakarta.ws.rs-api:3.0.0") } + +tasks { + withType().configureEach { + jvmArgs("-Dotel.instrumentation.common.experimental.controller-telemetry.enabled=true") + } +} diff --git a/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-annotations/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v3_0/DefaultRequestContextInstrumentation.java b/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-annotations/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v3_0/DefaultRequestContextInstrumentation.java index 8960484733d4..9dc55ae2f62f 100644 --- a/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-annotations/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v3_0/DefaultRequestContextInstrumentation.java +++ b/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-annotations/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v3_0/DefaultRequestContextInstrumentation.java @@ -9,8 +9,8 @@ import io.opentelemetry.context.Context; import io.opentelemetry.context.Scope; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteHolder; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteSource; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRoute; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRouteSource; import io.opentelemetry.javaagent.bootstrap.Java8BytecodeBridge; import io.opentelemetry.javaagent.instrumentation.jaxrs.JaxrsConstants; import io.opentelemetry.javaagent.instrumentation.jaxrs.JaxrsServerSpanNaming; @@ -64,9 +64,9 @@ public static void createGenericSpan( Context parentContext = Java8BytecodeBridge.currentContext(); handlerData = new Jaxrs3HandlerData(filterClass, method); - HttpRouteHolder.updateHttpRoute( + HttpServerRoute.update( parentContext, - HttpRouteSource.CONTROLLER, + HttpServerRouteSource.CONTROLLER, JaxrsServerSpanNaming.SERVER_SPAN_NAME, handlerData); diff --git a/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-annotations/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v3_0/JaxrsAnnotationsInstrumentation.java b/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-annotations/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v3_0/JaxrsAnnotationsInstrumentation.java index e27e9d616890..7f426eca228d 100644 --- a/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-annotations/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v3_0/JaxrsAnnotationsInstrumentation.java +++ b/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-annotations/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v3_0/JaxrsAnnotationsInstrumentation.java @@ -18,8 +18,8 @@ import io.opentelemetry.context.Context; import io.opentelemetry.context.Scope; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteHolder; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteSource; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRoute; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRouteSource; import io.opentelemetry.instrumentation.api.util.VirtualField; import io.opentelemetry.javaagent.bootstrap.CallDepth; import io.opentelemetry.javaagent.bootstrap.Java8BytecodeBridge; @@ -105,9 +105,9 @@ public static void nameSpan( Context parentContext = Java8BytecodeBridge.currentContext(); handlerData = new Jaxrs3HandlerData(target.getClass(), method); - HttpRouteHolder.updateHttpRoute( + HttpServerRoute.update( parentContext, - HttpRouteSource.CONTROLLER, + HttpServerRouteSource.CONTROLLER, JaxrsServerSpanNaming.SERVER_SPAN_NAME, handlerData); diff --git a/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-annotations/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v3_0/JaxrsAnnotationsInstrumentationModule.java b/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-annotations/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v3_0/JaxrsAnnotationsInstrumentationModule.java index 891e9e8a3733..4ff3ea43e0bb 100644 --- a/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-annotations/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v3_0/JaxrsAnnotationsInstrumentationModule.java +++ b/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-annotations/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v3_0/JaxrsAnnotationsInstrumentationModule.java @@ -9,8 +9,10 @@ import static java.util.Arrays.asList; import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.bootstrap.internal.ExperimentalConfig; import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; import java.util.List; import net.bytebuddy.matcher.ElementMatcher; @@ -34,4 +36,12 @@ public List typeInstrumentations() { new JaxrsAnnotationsInstrumentation(), new JaxrsAsyncResponseInstrumentation()); } + + @Override + public boolean defaultEnabled(ConfigProperties config) { + // This instrumentation produces controller telemetry and sets http route. Http route is set by + // this instrumentation only when it was not already set by a jax-rs framework instrumentation. + // This instrumentation uses complex type matcher, disabling it can improve startup performance. + return super.defaultEnabled(config) && ExperimentalConfig.get().controllerTelemetryEnabled(); + } } diff --git a/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-annotations/javaagent/src/test/groovy/JaxrsAnnotationsInstrumentationTest.groovy b/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-annotations/javaagent/src/test/groovy/JaxrsAnnotationsInstrumentationTest.groovy index 7fbe0e5508ee..66519712839c 100644 --- a/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-annotations/javaagent/src/test/groovy/JaxrsAnnotationsInstrumentationTest.groovy +++ b/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-annotations/javaagent/src/test/groovy/JaxrsAnnotationsInstrumentationTest.groovy @@ -4,7 +4,9 @@ */ import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import io.opentelemetry.semconv.incubating.CodeIncubatingAttributes +import io.opentelemetry.semconv.ErrorAttributes +import io.opentelemetry.semconv.HttpAttributes import spock.lang.Unroll import jakarta.ws.rs.DELETE @@ -35,16 +37,17 @@ class JaxrsAnnotationsInstrumentationTest extends AgentInstrumentationSpecificat kind SERVER hasNoParent() attributes { - "$SemanticAttributes.HTTP_METHOD" "GET" - "$SemanticAttributes.HTTP_ROUTE" paramName + "$HttpAttributes.HTTP_REQUEST_METHOD" "GET" + "$HttpAttributes.HTTP_ROUTE" paramName + "$ErrorAttributes.ERROR_TYPE" "_OTHER" } } span(1) { name "${className}.call" childOf span(0) attributes { - "$SemanticAttributes.CODE_NAMESPACE" obj.getClass().getName() - "$SemanticAttributes.CODE_FUNCTION" "call" + "$CodeIncubatingAttributes.CODE_NAMESPACE" obj.getClass().getName() + "$CodeIncubatingAttributes.CODE_FUNCTION" "call" } } } @@ -124,7 +127,8 @@ class JaxrsAnnotationsInstrumentationTest extends AgentInstrumentationSpecificat name "GET" kind SERVER attributes { - "$SemanticAttributes.HTTP_METHOD" "GET" + "$HttpAttributes.HTTP_REQUEST_METHOD" "GET" + "$ErrorAttributes.ERROR_TYPE" "_OTHER" } } } diff --git a/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-common/testing/src/main/groovy/JaxRsHttpServerTest.groovy b/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-common/testing/src/main/groovy/JaxRsHttpServerTest.groovy index e01ef29f638f..97391da68a1e 100644 --- a/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-common/testing/src/main/groovy/JaxRsHttpServerTest.groovy +++ b/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-common/testing/src/main/groovy/JaxRsHttpServerTest.groovy @@ -8,6 +8,11 @@ import test.JaxRsTestResource abstract class JaxRsHttpServerTest extends AbstractJaxRsHttpServerTest { + def setup() { + // reset the barrier to avoid a failing test breaking subsequent tests + JaxRsTestResource.BARRIER.reset() + } + @Override void awaitBarrier(int amount, TimeUnit timeUnit) { JaxRsTestResource.BARRIER.await(amount, timeUnit) diff --git a/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-common/testing/src/main/groovy/JaxRsJettyHttpServerTest.groovy b/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-common/testing/src/main/groovy/JaxRsJettyHttpServerTest.groovy index 4e310fcaa266..e817cb56e050 100644 --- a/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-common/testing/src/main/groovy/JaxRsJettyHttpServerTest.groovy +++ b/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-common/testing/src/main/groovy/JaxRsJettyHttpServerTest.groovy @@ -3,6 +3,8 @@ * SPDX-License-Identifier: Apache-2.0 */ +import io.opentelemetry.instrumentation.api.internal.HttpConstants +import io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint import org.eclipse.jetty.server.Server import org.eclipse.jetty.webapp.WebAppContext @@ -38,4 +40,12 @@ abstract class JaxRsJettyHttpServerTest extends JaxRsHttpServerTest { String getContextPath() { "/rest-app" } + + @Override + String expectedHttpRoute(ServerEndpoint endpoint, String method) { + if (method == HttpConstants._OTHER) { + return "${getContextPath()}/*" + } + return super.expectedHttpRoute(endpoint, method) + } } diff --git a/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-common/testing/src/main/groovy/test/JaxRsTestResource.groovy b/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-common/testing/src/main/groovy/test/JaxRsTestResource.groovy index e01f4802e65a..b069ad507756 100644 --- a/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-common/testing/src/main/groovy/test/JaxRsTestResource.groovy +++ b/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-common/testing/src/main/groovy/test/JaxRsTestResource.groovy @@ -118,7 +118,7 @@ class JaxRsTestResource { 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) + BARRIER.await(10, SECONDS) switch (action) { case "succeed": @@ -143,7 +143,7 @@ class JaxRsTestResource { def result = new CompletableFuture() CompletableFuture.runAsync({ // await for the test method to verify that there are no spans yet - BARRIER.await(1, SECONDS) + BARRIER.await(10, SECONDS) switch (action) { case "succeed": diff --git a/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-jersey-3.0/javaagent/build.gradle.kts b/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-jersey-3.0/javaagent/build.gradle.kts index 2723d09bca00..a84bb422c608 100644 --- a/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-jersey-3.0/javaagent/build.gradle.kts +++ b/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-jersey-3.0/javaagent/build.gradle.kts @@ -42,5 +42,6 @@ tasks { withType().configureEach { // TODO run tests both with and without experimental span attributes jvmArgs("-Dotel.instrumentation.jaxrs.experimental-span-attributes=true") + jvmArgs("-Dotel.instrumentation.common.experimental.controller-telemetry.enabled=true") } } diff --git a/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-jersey-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v3_0/JerseySpanName.java b/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-jersey-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v3_0/JerseySpanName.java index 6089334fd555..f122afaf0d3a 100644 --- a/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-jersey-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v3_0/JerseySpanName.java +++ b/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-jersey-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v3_0/JerseySpanName.java @@ -8,9 +8,9 @@ import static io.opentelemetry.javaagent.instrumentation.jaxrs.JaxrsPathUtil.normalizePath; import io.opentelemetry.context.Context; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteGetter; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteHolder; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteSource; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRoute; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRouteGetter; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRouteSource; import io.opentelemetry.javaagent.bootstrap.jaxrs.JaxrsContextPath; import io.opentelemetry.javaagent.bootstrap.servlet.ServletContextPath; import jakarta.ws.rs.core.Request; @@ -19,13 +19,13 @@ import org.glassfish.jersey.server.ContainerRequest; import org.glassfish.jersey.server.ExtendedUriInfo; -public class JerseySpanName implements HttpRouteGetter { +public class JerseySpanName implements HttpServerRouteGetter { public static final JerseySpanName INSTANCE = new JerseySpanName(); public void updateServerSpanName(Request request) { Context context = Context.current(); - HttpRouteHolder.updateHttpRoute(context, HttpRouteSource.NESTED_CONTROLLER, this, request); + HttpServerRoute.update(context, HttpServerRouteSource.NESTED_CONTROLLER, this, request); } @Override diff --git a/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-jersey-3.0/javaagent/src/test/groovy/JerseyHttpServerTest.groovy b/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-jersey-3.0/javaagent/src/test/groovy/JerseyHttpServerTest.groovy index 58eea5ff8f45..05d37cd00ef2 100644 --- a/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-jersey-3.0/javaagent/src/test/groovy/JerseyHttpServerTest.groovy +++ b/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-jersey-3.0/javaagent/src/test/groovy/JerseyHttpServerTest.groovy @@ -3,6 +3,8 @@ * SPDX-License-Identifier: Apache-2.0 */ +import io.opentelemetry.instrumentation.api.internal.HttpConstants +import io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint import org.eclipse.jetty.server.Server import org.eclipse.jetty.servlet.ServletContextHandler import org.eclipse.jetty.servlet.ServletHolder @@ -42,4 +44,17 @@ class JerseyHttpServerTest extends JaxRsHttpServerTest { // disables a test that jersey deems invalid false } + + @Override + String expectedHttpRoute(ServerEndpoint endpoint, String method) { + if (method == HttpConstants._OTHER) { + return "${getContextPath()}/*" + } + return super.expectedHttpRoute(endpoint, method) + } + + @Override + int getResponseCodeOnNonStandardHttpMethod() { + 500 + } } diff --git a/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-jersey-3.0/javaagent/src/test/groovy/JerseyJettyHttpServerTest.groovy b/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-jersey-3.0/javaagent/src/test/groovy/JerseyJettyHttpServerTest.groovy index f1af4c75b0a3..97c4c1c28642 100644 --- a/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-jersey-3.0/javaagent/src/test/groovy/JerseyJettyHttpServerTest.groovy +++ b/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-jersey-3.0/javaagent/src/test/groovy/JerseyJettyHttpServerTest.groovy @@ -15,4 +15,9 @@ class JerseyJettyHttpServerTest extends JaxRsJettyHttpServerTest { // disables a test that jersey deems invalid false } + + @Override + int getResponseCodeOnNonStandardHttpMethod() { + 500 + } } diff --git a/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-resteasy-6.0/javaagent/build.gradle.kts b/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-resteasy-6.0/javaagent/build.gradle.kts index b352d018d243..9720803a9bd1 100644 --- a/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-resteasy-6.0/javaagent/build.gradle.kts +++ b/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-resteasy-6.0/javaagent/build.gradle.kts @@ -45,5 +45,6 @@ tasks { withType().configureEach { // TODO run tests both with and without experimental span attributes jvmArgs("-Dotel.instrumentation.jaxrs.experimental-span-attributes=true") + jvmArgs("-Dotel.instrumentation.common.experimental.controller-telemetry.enabled=true") } } diff --git a/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-resteasy-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v3_0/ResteasySpanName.java b/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-resteasy-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v3_0/ResteasySpanName.java index 9aff95c67e5d..81c1d2988903 100644 --- a/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-resteasy-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v3_0/ResteasySpanName.java +++ b/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-resteasy-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v3_0/ResteasySpanName.java @@ -5,22 +5,21 @@ package io.opentelemetry.javaagent.instrumentation.jaxrs.v3_0; -import static io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteSource.NESTED_CONTROLLER; - import io.opentelemetry.context.Context; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteGetter; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteHolder; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRoute; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRouteGetter; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRouteSource; import io.opentelemetry.javaagent.bootstrap.jaxrs.JaxrsContextPath; import io.opentelemetry.javaagent.bootstrap.servlet.ServletContextPath; import javax.annotation.Nullable; -public final class ResteasySpanName implements HttpRouteGetter { +public final class ResteasySpanName implements HttpServerRouteGetter { public static final ResteasySpanName INSTANCE = new ResteasySpanName(); public void updateServerSpanName(Context context, String name) { if (name != null) { - HttpRouteHolder.updateHttpRoute(context, NESTED_CONTROLLER, this, name); + HttpServerRoute.update(context, HttpServerRouteSource.NESTED_CONTROLLER, this, name); } } diff --git a/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-resteasy-6.0/javaagent/src/test/groovy/ResteasyHttpServerTest.groovy b/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-resteasy-6.0/javaagent/src/test/groovy/ResteasyHttpServerTest.groovy index df3d5a11c620..1ea11b42295c 100644 --- a/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-resteasy-6.0/javaagent/src/test/groovy/ResteasyHttpServerTest.groovy +++ b/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-resteasy-6.0/javaagent/src/test/groovy/ResteasyHttpServerTest.groovy @@ -3,6 +3,8 @@ * SPDX-License-Identifier: Apache-2.0 */ +import io.opentelemetry.instrumentation.api.internal.HttpConstants +import io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint import io.undertow.Undertow import org.jboss.resteasy.plugins.server.undertow.UndertowJaxrsServer import test.JaxRsTestApplication @@ -32,4 +34,17 @@ class ResteasyHttpServerTest extends JaxRsHttpServerTest { boolean shouldTestCompletableStageAsync() { false } + + @Override + String expectedHttpRoute(ServerEndpoint endpoint, String method) { + if (method == HttpConstants._OTHER) { + return "${getContextPath()}/*" + } + return super.expectedHttpRoute(endpoint, method) + } + + @Override + int getResponseCodeOnNonStandardHttpMethod() { + 500 + } } diff --git a/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-resteasy-6.0/javaagent/src/test/groovy/ResteasyJettyHttpServerTest.groovy b/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-resteasy-6.0/javaagent/src/test/groovy/ResteasyJettyHttpServerTest.groovy index 6b1683c9dfd4..8d92d379711a 100644 --- a/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-resteasy-6.0/javaagent/src/test/groovy/ResteasyJettyHttpServerTest.groovy +++ b/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-resteasy-6.0/javaagent/src/test/groovy/ResteasyJettyHttpServerTest.groovy @@ -4,4 +4,9 @@ */ class ResteasyJettyHttpServerTest extends JaxRsJettyHttpServerTest { + + @Override + int getResponseCodeOnNonStandardHttpMethod() { + 500 + } } diff --git a/instrumentation/jaxrs/jaxrs-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/JaxrsCodeAttributesGetter.java b/instrumentation/jaxrs/jaxrs-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/JaxrsCodeAttributesGetter.java index 9eba374574a5..4b04d1cecb49 100644 --- a/instrumentation/jaxrs/jaxrs-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/JaxrsCodeAttributesGetter.java +++ b/instrumentation/jaxrs/jaxrs-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/JaxrsCodeAttributesGetter.java @@ -5,7 +5,7 @@ package io.opentelemetry.javaagent.instrumentation.jaxrs; -import io.opentelemetry.instrumentation.api.instrumenter.code.CodeAttributesGetter; +import io.opentelemetry.instrumentation.api.incubator.semconv.code.CodeAttributesGetter; public class JaxrsCodeAttributesGetter implements CodeAttributesGetter { diff --git a/instrumentation/jaxrs/jaxrs-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/JaxrsConfig.java b/instrumentation/jaxrs/jaxrs-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/JaxrsConfig.java index 5327611e44f7..3457cfba7f24 100644 --- a/instrumentation/jaxrs/jaxrs-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/JaxrsConfig.java +++ b/instrumentation/jaxrs/jaxrs-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/JaxrsConfig.java @@ -5,12 +5,12 @@ package io.opentelemetry.javaagent.instrumentation.jaxrs; -import io.opentelemetry.javaagent.bootstrap.internal.InstrumentationConfig; +import io.opentelemetry.javaagent.bootstrap.internal.AgentInstrumentationConfig; public final class JaxrsConfig { public static final boolean CAPTURE_EXPERIMENTAL_SPAN_ATTRIBUTES = - InstrumentationConfig.get() + AgentInstrumentationConfig.get() .getBoolean("otel.instrumentation.jaxrs.experimental-span-attributes", false); private JaxrsConfig() {} diff --git a/instrumentation/jaxrs/jaxrs-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/JaxrsInstrumenterFactory.java b/instrumentation/jaxrs/jaxrs-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/JaxrsInstrumenterFactory.java index 9e184160891c..ae2096cbdaab 100644 --- a/instrumentation/jaxrs/jaxrs-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/JaxrsInstrumenterFactory.java +++ b/instrumentation/jaxrs/jaxrs-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/JaxrsInstrumenterFactory.java @@ -6,9 +6,9 @@ package io.opentelemetry.javaagent.instrumentation.jaxrs; import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.instrumentation.api.incubator.semconv.code.CodeAttributesExtractor; +import io.opentelemetry.instrumentation.api.incubator.semconv.code.CodeSpanNameExtractor; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; -import io.opentelemetry.instrumentation.api.instrumenter.code.CodeAttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.code.CodeSpanNameExtractor; import io.opentelemetry.javaagent.bootstrap.internal.ExperimentalConfig; public final class JaxrsInstrumenterFactory { diff --git a/instrumentation/jaxrs/jaxrs-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/JaxrsServerSpanNaming.java b/instrumentation/jaxrs/jaxrs-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/JaxrsServerSpanNaming.java index f65bd2f99b4a..34aeda62a3c7 100644 --- a/instrumentation/jaxrs/jaxrs-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/JaxrsServerSpanNaming.java +++ b/instrumentation/jaxrs/jaxrs-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/JaxrsServerSpanNaming.java @@ -5,13 +5,13 @@ package io.opentelemetry.javaagent.instrumentation.jaxrs; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteGetter; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRouteGetter; import io.opentelemetry.javaagent.bootstrap.jaxrs.JaxrsContextPath; import io.opentelemetry.javaagent.bootstrap.servlet.ServletContextPath; public class JaxrsServerSpanNaming { - public static final HttpRouteGetter SERVER_SPAN_NAME = + public static final HttpServerRouteGetter SERVER_SPAN_NAME = (context, handlerData) -> { String pathBasedSpanName = handlerData.getServerSpanName(); // If path based name is empty skip prepending context path so that path based name would diff --git a/instrumentation/jaxrs/jaxrs-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/RequestContextHelper.java b/instrumentation/jaxrs/jaxrs-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/RequestContextHelper.java index 5adf1e0580ba..d2f416096c16 100644 --- a/instrumentation/jaxrs/jaxrs-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/RequestContextHelper.java +++ b/instrumentation/jaxrs/jaxrs-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/RequestContextHelper.java @@ -9,8 +9,8 @@ import io.opentelemetry.context.Context; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; import io.opentelemetry.instrumentation.api.instrumenter.LocalRootSpan; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteHolder; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteSource; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRoute; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRouteSource; import io.opentelemetry.javaagent.bootstrap.Java8BytecodeBridge; public final class RequestContextHelper { @@ -21,9 +21,9 @@ public static Context createOrUpdateAbortSpan( Span serverSpan = LocalRootSpan.fromContextOrNull(parentContext); Span currentSpan = Java8BytecodeBridge.spanFromContext(parentContext); - HttpRouteHolder.updateHttpRoute( + HttpServerRoute.update( parentContext, - HttpRouteSource.CONTROLLER, + HttpServerRouteSource.CONTROLLER, JaxrsServerSpanNaming.SERVER_SPAN_NAME, handlerData); diff --git a/instrumentation/jaxrs/jaxrs-common/testing/src/main/groovy/AbstractJaxRsFilterTest.groovy b/instrumentation/jaxrs/jaxrs-common/testing/src/main/groovy/AbstractJaxRsFilterTest.groovy index 1efb6fff6649..8b880f6c02e7 100644 --- a/instrumentation/jaxrs/jaxrs-common/testing/src/main/groovy/AbstractJaxRsFilterTest.groovy +++ b/instrumentation/jaxrs/jaxrs-common/testing/src/main/groovy/AbstractJaxRsFilterTest.groovy @@ -4,7 +4,9 @@ */ import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import io.opentelemetry.semconv.incubating.CodeIncubatingAttributes +import io.opentelemetry.semconv.ErrorAttributes +import io.opentelemetry.semconv.HttpAttributes import org.junit.jupiter.api.Assumptions import spock.lang.Unroll @@ -79,13 +81,13 @@ abstract class AbstractJaxRsFilterTest extends AgentInstrumentationSpecification name controllerName if (abortPrematch) { attributes { - "$SemanticAttributes.CODE_NAMESPACE" "JaxRsFilterTest\$PrematchRequestFilter" - "$SemanticAttributes.CODE_FUNCTION" "filter" + "$CodeIncubatingAttributes.CODE_NAMESPACE" "JaxRsFilterTest\$PrematchRequestFilter" + "$CodeIncubatingAttributes.CODE_FUNCTION" "filter" } } else { attributes { - "$SemanticAttributes.CODE_NAMESPACE" ~/Resource[$]Test*/ - "$SemanticAttributes.CODE_FUNCTION" "hello" + "$CodeIncubatingAttributes.CODE_NAMESPACE" ~/Resource[$]Test*/ + "$CodeIncubatingAttributes.CODE_FUNCTION" "hello" } } } @@ -130,8 +132,9 @@ abstract class AbstractJaxRsFilterTest extends AgentInstrumentationSpecification kind SERVER if (!runsOnServer()) { attributes { - "$SemanticAttributes.HTTP_METHOD" method - "$SemanticAttributes.HTTP_ROUTE" route + "$HttpAttributes.HTTP_REQUEST_METHOD" method + "$HttpAttributes.HTTP_ROUTE" route + "$ErrorAttributes.ERROR_TYPE" "_OTHER" } } } @@ -140,8 +143,8 @@ abstract class AbstractJaxRsFilterTest extends AgentInstrumentationSpecification name controller1Name kind INTERNAL attributes { - "$SemanticAttributes.CODE_NAMESPACE" ~/Resource[$]Test*/ - "$SemanticAttributes.CODE_FUNCTION" "nested" + "$CodeIncubatingAttributes.CODE_NAMESPACE" ~/Resource[$]Test*/ + "$CodeIncubatingAttributes.CODE_FUNCTION" "nested" } } } diff --git a/instrumentation/jaxrs/jaxrs-common/testing/src/main/groovy/AbstractJaxRsHttpServerTest.groovy b/instrumentation/jaxrs/jaxrs-common/testing/src/main/groovy/AbstractJaxRsHttpServerTest.groovy index 7c3b89cef6fa..8625ff0d300f 100644 --- a/instrumentation/jaxrs/jaxrs-common/testing/src/main/groovy/AbstractJaxRsHttpServerTest.groovy +++ b/instrumentation/jaxrs/jaxrs-common/testing/src/main/groovy/AbstractJaxRsHttpServerTest.groovy @@ -8,7 +8,14 @@ import io.opentelemetry.instrumentation.test.asserts.TraceAssert import io.opentelemetry.instrumentation.test.base.HttpServerTest import io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint import io.opentelemetry.sdk.trace.data.SpanData -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import io.opentelemetry.semconv.incubating.CodeIncubatingAttributes +import io.opentelemetry.semconv.ServerAttributes +import io.opentelemetry.semconv.ClientAttributes +import io.opentelemetry.semconv.UserAgentAttributes +import io.opentelemetry.semconv.ErrorAttributes +import io.opentelemetry.semconv.HttpAttributes +import io.opentelemetry.semconv.NetworkAttributes +import io.opentelemetry.semconv.UrlAttributes import spock.lang.Unroll import java.util.concurrent.TimeUnit @@ -128,7 +135,7 @@ abstract class AbstractJaxRsHttpServerTest extends HttpServerTest implemen } when: "barrier is released and resource class sends response" - awaitBarrier(1, SECONDS) + awaitBarrier(10, SECONDS) def response = futureResponse.join() then: @@ -171,7 +178,7 @@ abstract class AbstractJaxRsHttpServerTest extends HttpServerTest implemen } when: "barrier is released and resource class sends response" - awaitBarrier(1, SECONDS) + awaitBarrier(10, SECONDS) def response = futureResponse.join() then: @@ -229,7 +236,6 @@ abstract class AbstractJaxRsHttpServerTest extends HttpServerTest implemen String traceID = null, String parentID = null, String method = "GET", - Long responseContentLength = null, ServerEndpoint endpoint = SUCCESS, String spanID = null) { serverSpan(trace, index, traceID, parentID, spanID, method, @@ -279,25 +285,26 @@ abstract class AbstractJaxRsHttpServerTest extends HttpServerTest implemen spanId spanID } attributes { - "net.protocol.name" "http" - "net.protocol.version" "1.1" - "$SemanticAttributes.NET_HOST_NAME" fullUrl.host - "$SemanticAttributes.NET_HOST_PORT" fullUrl.port - "$SemanticAttributes.NET_SOCK_PEER_ADDR" "127.0.0.1" - "$SemanticAttributes.NET_SOCK_PEER_PORT" Long - "$SemanticAttributes.NET_SOCK_HOST_ADDR" "127.0.0.1" - "$SemanticAttributes.HTTP_SCHEME" fullUrl.getScheme() - "$SemanticAttributes.HTTP_TARGET" fullUrl.getPath() + (fullUrl.getQuery() != null ? "?" + fullUrl.getQuery() : "") - "$SemanticAttributes.HTTP_METHOD" method - "$SemanticAttributes.HTTP_STATUS_CODE" statusCode - "$SemanticAttributes.USER_AGENT_ORIGINAL" TEST_USER_AGENT - "$SemanticAttributes.HTTP_CLIENT_IP" TEST_CLIENT_IP + "$NetworkAttributes.NETWORK_PROTOCOL_VERSION" "1.1" + "$ServerAttributes.SERVER_ADDRESS" fullUrl.host + "$ServerAttributes.SERVER_PORT" fullUrl.port + "$NetworkAttributes.NETWORK_PEER_ADDRESS" "127.0.0.1" + "$NetworkAttributes.NETWORK_PEER_PORT" Long + "$UrlAttributes.URL_SCHEME" fullUrl.getScheme() + "$UrlAttributes.URL_PATH" fullUrl.getPath() + "$UrlAttributes.URL_QUERY" fullUrl.getQuery() + "$HttpAttributes.HTTP_REQUEST_METHOD" method + "$HttpAttributes.HTTP_RESPONSE_STATUS_CODE" statusCode + "$UserAgentAttributes.USER_AGENT_ORIGINAL" TEST_USER_AGENT + "$ClientAttributes.CLIENT_ADDRESS" TEST_CLIENT_IP // Optional - "$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" { it == null || it instanceof Long } - "$SemanticAttributes.HTTP_ROUTE" path + "$HttpAttributes.HTTP_ROUTE" path if (fullUrl.getPath().endsWith(ServerEndpoint.CAPTURE_HEADERS.getPath())) { - "http.request.header.x_test_request" { it == ["test"] } - "http.response.header.x_test_response" { it == ["test"] } + "http.request.header.x-test-request" { it == ["test"] } + "http.response.header.x-test-response" { it == ["test"] } + } + if (statusCode >= 500) { + "$ErrorAttributes.ERROR_TYPE" "$statusCode" } } } @@ -332,8 +339,8 @@ abstract class AbstractJaxRsHttpServerTest extends HttpServerTest implemen } childOf((SpanData) parent) attributes { - "$SemanticAttributes.CODE_NAMESPACE" "test.JaxRsTestResource" - "$SemanticAttributes.CODE_FUNCTION" methodName + "$CodeIncubatingAttributes.CODE_NAMESPACE" "test.JaxRsTestResource" + "$CodeIncubatingAttributes.CODE_FUNCTION" methodName if (isCancelled) { "jaxrs.canceled" true } diff --git a/instrumentation/jaxws/jaxws-2.0-arquillian-testing/src/main/java/AbstractArquillianJaxWsTest.java b/instrumentation/jaxws/jaxws-2.0-arquillian-testing/src/main/java/AbstractArquillianJaxWsTest.java index 8a74e11c8a4c..b93e4b31e99e 100644 --- a/instrumentation/jaxws/jaxws-2.0-arquillian-testing/src/main/java/AbstractArquillianJaxWsTest.java +++ b/instrumentation/jaxws/jaxws-2.0-arquillian-testing/src/main/java/AbstractArquillianJaxWsTest.java @@ -10,7 +10,7 @@ import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; import io.opentelemetry.sdk.testing.assertj.SpanDataAssert; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.opentelemetry.semconv.incubating.CodeIncubatingAttributes; import io.opentelemetry.testing.internal.armeria.client.WebClient; import io.opentelemetry.testing.internal.armeria.common.AggregatedHttpResponse; import java.net.URI; @@ -123,7 +123,7 @@ private static SpanDataAssert assertAnnotationHandlerSpan( return span.hasName(service + "Impl." + methodName) .hasKind(SpanKind.INTERNAL) .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.CODE_NAMESPACE, "test." + service + "Impl"), - equalTo(SemanticAttributes.CODE_FUNCTION, methodName)); + equalTo(CodeIncubatingAttributes.CODE_NAMESPACE, "test." + service + "Impl"), + equalTo(CodeIncubatingAttributes.CODE_FUNCTION, methodName)); } } diff --git a/instrumentation/jaxws/jaxws-2.0-axis2-1.6/javaagent/build.gradle.kts b/instrumentation/jaxws/jaxws-2.0-axis2-1.6/javaagent/build.gradle.kts index 78860612d087..00269d60d5ee 100644 --- a/instrumentation/jaxws/jaxws-2.0-axis2-1.6/javaagent/build.gradle.kts +++ b/instrumentation/jaxws/jaxws-2.0-axis2-1.6/javaagent/build.gradle.kts @@ -52,4 +52,5 @@ tasks.withType().configureEach { // required on jdk17 jvmArgs("--add-opens=java.base/java.lang=ALL-UNNAMED") jvmArgs("-XX:+IgnoreUnrecognizedVMOptions") + jvmArgs("-Dotel.instrumentation.common.experimental.controller-telemetry.enabled=true") } diff --git a/instrumentation/jaxws/jaxws-2.0-axis2-1.6/javaagent/src/test/java/test/CustomJaxWsDeployer.java b/instrumentation/jaxws/jaxws-2.0-axis2-1.6/javaagent/src/test/java/test/CustomJaxWsDeployer.java index 0efa09ae0a97..d2bfc2617746 100644 --- a/instrumentation/jaxws/jaxws-2.0-axis2-1.6/javaagent/src/test/java/test/CustomJaxWsDeployer.java +++ b/instrumentation/jaxws/jaxws-2.0-axis2-1.6/javaagent/src/test/java/test/CustomJaxWsDeployer.java @@ -14,6 +14,7 @@ public class CustomJaxWsDeployer extends JAXWSDeployer { @Override + @SuppressWarnings("NonApiType") // errorprone bug that it doesn't recognize this is an override protected ArrayList getClassesInWebInfDirectory(File file) { // help axis find our webservice classes return new ArrayList<>(Arrays.asList("hello.HelloService", "hello.HelloServiceImpl")); diff --git a/instrumentation/jaxws/jaxws-2.0-common-testing/build.gradle.kts b/instrumentation/jaxws/jaxws-2.0-common-testing/build.gradle.kts index b3d89f72b792..607effb0c6c1 100644 --- a/instrumentation/jaxws/jaxws-2.0-common-testing/build.gradle.kts +++ b/instrumentation/jaxws/jaxws-2.0-common-testing/build.gradle.kts @@ -14,10 +14,6 @@ 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") diff --git a/instrumentation/jaxws/jaxws-2.0-common-testing/src/main/groovy/AbstractJaxWsTest.groovy b/instrumentation/jaxws/jaxws-2.0-common-testing/src/main/groovy/AbstractJaxWsTest.groovy index e6360d91f931..5f8040ae13d8 100644 --- a/instrumentation/jaxws/jaxws-2.0-common-testing/src/main/groovy/AbstractJaxWsTest.groovy +++ b/instrumentation/jaxws/jaxws-2.0-common-testing/src/main/groovy/AbstractJaxWsTest.groovy @@ -7,7 +7,7 @@ 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.semconv.incubating.CodeIncubatingAttributes import io.opentelemetry.test.hello_web_service.Hello2Request import io.opentelemetry.test.hello_web_service.HelloRequest import org.eclipse.jetty.server.Server @@ -199,8 +199,8 @@ abstract class AbstractJaxWsTest extends AgentInstrumentationSpecification imple errorEvent(exception.class, exception.message) } attributes { - "$SemanticAttributes.CODE_NAMESPACE" "hello.HelloServiceImpl" - "$SemanticAttributes.CODE_FUNCTION" methodName + "$CodeIncubatingAttributes.CODE_NAMESPACE" "hello.HelloServiceImpl" + "$CodeIncubatingAttributes.CODE_FUNCTION" methodName } } } diff --git a/instrumentation/jaxws/jaxws-2.0-cxf-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cxf/CxfServerSpanNaming.java b/instrumentation/jaxws/jaxws-2.0-cxf-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cxf/CxfServerSpanNaming.java deleted file mode 100644 index 96c5a74f16d2..000000000000 --- a/instrumentation/jaxws/jaxws-2.0-cxf-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cxf/CxfServerSpanNaming.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.javaagent.instrumentation.cxf; - -import io.opentelemetry.api.trace.Span; -import io.opentelemetry.context.Context; -import io.opentelemetry.instrumentation.api.instrumenter.LocalRootSpan; -import io.opentelemetry.javaagent.bootstrap.servlet.ServletContextPath; -import javax.servlet.http.HttpServletRequest; - -public final class CxfServerSpanNaming { - - public static void updateServerSpanName(Context context, CxfRequest cxfRequest) { - String spanName = cxfRequest.spanName(); - if (spanName == null) { - return; - } - - Span serverSpan = LocalRootSpan.fromContextOrNull(context); - if (serverSpan == null) { - return; - } - - HttpServletRequest request = (HttpServletRequest) cxfRequest.message().get("HTTP.REQUEST"); - if (request != null) { - String servletPath = request.getServletPath(); - if (!servletPath.isEmpty()) { - spanName = servletPath + "/" + spanName; - } - } - - serverSpan.updateName(ServletContextPath.prepend(context, spanName)); - } - - private CxfServerSpanNaming() {} -} diff --git a/instrumentation/jaxws/jaxws-2.0-metro-2.2/javaagent/build.gradle.kts b/instrumentation/jaxws/jaxws-2.0-metro-2.2-testing/build.gradle.kts similarity index 63% rename from instrumentation/jaxws/jaxws-2.0-metro-2.2/javaagent/build.gradle.kts rename to instrumentation/jaxws/jaxws-2.0-metro-2.2-testing/build.gradle.kts index 59be6661ab3e..f1673472b00e 100644 --- a/instrumentation/jaxws/jaxws-2.0-metro-2.2/javaagent/build.gradle.kts +++ b/instrumentation/jaxws/jaxws-2.0-metro-2.2-testing/build.gradle.kts @@ -1,31 +1,17 @@ plugins { - id("otel.javaagent-instrumentation") -} - -muzzle { - pass { - group.set("com.sun.xml.ws") - module.set("jaxws-rt") - versions.set("[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.set(true) - extraDependency("javax.servlet:javax.servlet-api:3.0.1") - } + id("otel.javaagent-testing") } dependencies { - bootstrap(project(":instrumentation:servlet:servlet-common:bootstrap")) - - library("com.sun.xml.ws:jaxws-rt:2.2.0.1") + testLibrary("com.sun.xml.ws:jaxws-rt:2.2.0.1") // early versions of streambuffer depend on latest release of org.jvnet.staxex:stax-ex // which doesn't work with java 8 - library("com.sun.xml.stream.buffer:streambuffer:1.4") - - compileOnly("javax.servlet:javax.servlet-api:3.0.1") + testLibrary("com.sun.xml.stream.buffer:streambuffer:1.4") + testImplementation("javax.servlet:javax.servlet-api:3.0.1") testImplementation(project(":instrumentation:jaxws:jaxws-2.0-common-testing")) + testInstrumentation(project(":instrumentation:jaxws:jaxws-metro-2.2:javaagent")) testInstrumentation(project(":instrumentation:jaxws:jaxws-2.0:javaagent")) testInstrumentation(project(":instrumentation:jaxws:jaxws-jws-api-1.1:javaagent")) @@ -42,4 +28,5 @@ tasks.withType().configureEach { jvmArgs("--add-exports=java.xml/com.sun.org.apache.xerces.internal.jaxp=ALL-UNNAMED") jvmArgs("--add-opens=java.base/java.lang=ALL-UNNAMED") jvmArgs("-XX:+IgnoreUnrecognizedVMOptions") + jvmArgs("-Dotel.instrumentation.common.experimental.controller-telemetry.enabled=true") } diff --git a/instrumentation/jaxws/jaxws-2.0-metro-2.2/javaagent/src/test/groovy/MetroJaxWsTest.groovy b/instrumentation/jaxws/jaxws-2.0-metro-2.2-testing/src/test/groovy/MetroJaxWsTest.groovy similarity index 100% rename from instrumentation/jaxws/jaxws-2.0-metro-2.2/javaagent/src/test/groovy/MetroJaxWsTest.groovy rename to instrumentation/jaxws/jaxws-2.0-metro-2.2-testing/src/test/groovy/MetroJaxWsTest.groovy diff --git a/instrumentation/jaxws/jaxws-2.0-metro-2.2/javaagent/src/test/resources/test-app/WEB-INF/sun-jaxws.xml b/instrumentation/jaxws/jaxws-2.0-metro-2.2-testing/src/test/resources/test-app/WEB-INF/sun-jaxws.xml similarity index 100% rename from instrumentation/jaxws/jaxws-2.0-metro-2.2/javaagent/src/test/resources/test-app/WEB-INF/sun-jaxws.xml rename to instrumentation/jaxws/jaxws-2.0-metro-2.2-testing/src/test/resources/test-app/WEB-INF/sun-jaxws.xml diff --git a/instrumentation/jaxws/jaxws-2.0-metro-2.2/javaagent/src/test/resources/test-app/WEB-INF/web.xml b/instrumentation/jaxws/jaxws-2.0-metro-2.2-testing/src/test/resources/test-app/WEB-INF/web.xml similarity index 100% rename from instrumentation/jaxws/jaxws-2.0-metro-2.2/javaagent/src/test/resources/test-app/WEB-INF/web.xml rename to instrumentation/jaxws/jaxws-2.0-metro-2.2-testing/src/test/resources/test-app/WEB-INF/web.xml diff --git a/instrumentation/jaxws/jaxws-2.0-metro-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/metro/MetroServerSpanNaming.java b/instrumentation/jaxws/jaxws-2.0-metro-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/metro/MetroServerSpanNaming.java deleted file mode 100644 index b0967efc71c5..000000000000 --- a/instrumentation/jaxws/jaxws-2.0-metro-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/metro/MetroServerSpanNaming.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.javaagent.instrumentation.metro; - -import com.sun.xml.ws.api.message.Packet; -import io.opentelemetry.api.trace.Span; -import io.opentelemetry.context.Context; -import io.opentelemetry.instrumentation.api.instrumenter.LocalRootSpan; -import io.opentelemetry.javaagent.bootstrap.servlet.ServletContextPath; -import javax.servlet.http.HttpServletRequest; -import javax.xml.ws.handler.MessageContext; - -public final class MetroServerSpanNaming { - - public static void updateServerSpanName(Context context, MetroRequest metroRequest) { - String spanName = metroRequest.spanName(); - if (spanName == null) { - return; - } - - Span serverSpan = LocalRootSpan.fromContextOrNull(context); - if (serverSpan == null) { - return; - } - - Packet packet = metroRequest.packet(); - 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) { - spanName = 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 - String operationName = packet.getWSDLOperation().getLocalPart(); - spanName = servletPath + "/" + operationName; - } - } - } - - serverSpan.updateName(ServletContextPath.prepend(context, spanName)); - } - - private MetroServerSpanNaming() {} -} diff --git a/instrumentation/jaxws/jaxws-2.0-tomee-testing/build.gradle.kts b/instrumentation/jaxws/jaxws-2.0-tomee-testing/build.gradle.kts index c24dea2e87e7..779f42848838 100644 --- a/instrumentation/jaxws/jaxws-2.0-tomee-testing/build.gradle.kts +++ b/instrumentation/jaxws/jaxws-2.0-tomee-testing/build.gradle.kts @@ -11,7 +11,7 @@ dependencies { testInstrumentation(project(":instrumentation:servlet:servlet-3.0:javaagent")) testInstrumentation(project(":instrumentation:jaxws:jaxws-2.0:javaagent")) - testInstrumentation(project(":instrumentation:jaxws:jaxws-2.0-cxf-3.0:javaagent")) + testInstrumentation(project(":instrumentation:jaxws:jaxws-cxf-3.0:javaagent")) testInstrumentation(project(":instrumentation:jaxws:jaxws-jws-api-1.1:javaagent")) } @@ -20,4 +20,5 @@ tasks.withType().configureEach { jvmArgs("--add-opens=java.base/java.lang=ALL-UNNAMED") jvmArgs("--add-exports=java.base/sun.misc=ALL-UNNAMED") jvmArgs("-XX:+IgnoreUnrecognizedVMOptions") + jvmArgs("-Dotel.instrumentation.common.experimental.controller-telemetry.enabled=true") } diff --git a/instrumentation/jaxws/jaxws-2.0-wildfly-testing/build.gradle.kts b/instrumentation/jaxws/jaxws-2.0-wildfly-testing/build.gradle.kts index 30475a48f3f0..62beaa709a54 100644 --- a/instrumentation/jaxws/jaxws-2.0-wildfly-testing/build.gradle.kts +++ b/instrumentation/jaxws/jaxws-2.0-wildfly-testing/build.gradle.kts @@ -12,7 +12,7 @@ dependencies { testInstrumentation(project(":instrumentation:servlet:servlet-3.0:javaagent")) testInstrumentation(project(":instrumentation:jaxws:jaxws-2.0:javaagent")) - testInstrumentation(project(":instrumentation:jaxws:jaxws-2.0-cxf-3.0:javaagent")) + testInstrumentation(project(":instrumentation:jaxws:jaxws-cxf-3.0:javaagent")) testInstrumentation(project(":instrumentation:jaxws:jaxws-jws-api-1.1:javaagent")) // wildfly version used to run tests @@ -33,14 +33,16 @@ tasks { // that breaks deploy on embedded wildfly // create a copy of logback-classic jar that does not have this file val modifyLogbackJar by registering(Jar::class) { - destinationDirectory.set(file("$buildDir/tmp")) + destinationDirectory.set(layout.buildDirectory.dir("tmp")) archiveFileName.set("logback-classic-modified.jar") exclude("/META-INF/services/javax.servlet.ServletContainerInitializer") doFirst { configurations.configureEach { if (name.lowercase().endsWith("testruntimeclasspath")) { val logbackJar = find { it.name.contains("logback-classic") } - from(zipTree(logbackJar)) + logbackJar?.let { + from(zipTree(logbackJar)) + } } } } @@ -63,7 +65,7 @@ tasks { !it.absolutePath.contains("logback-classic") } // add modified copy of logback-classic to classpath - classpath = classpath.plus(files("$buildDir/tmp/logback-classic-modified.jar")) + classpath = classpath.plus(files(layout.buildDirectory.file("tmp/logback-classic-modified.jar"))) } } } @@ -72,4 +74,5 @@ tasks.withType().configureEach { // required on jdk17 jvmArgs("--add-opens=java.base/java.lang=ALL-UNNAMED") jvmArgs("-XX:+IgnoreUnrecognizedVMOptions") + jvmArgs("-Dotel.instrumentation.common.experimental.controller-telemetry.enabled=true") } diff --git a/instrumentation/jaxws/jaxws-2.0/javaagent/build.gradle.kts b/instrumentation/jaxws/jaxws-2.0/javaagent/build.gradle.kts index 2a96778ed606..b0feeaeadfbd 100644 --- a/instrumentation/jaxws/jaxws-2.0/javaagent/build.gradle.kts +++ b/instrumentation/jaxws/jaxws-2.0/javaagent/build.gradle.kts @@ -14,3 +14,7 @@ dependencies { library("javax.xml.ws:jaxws-api:2.0") implementation(project(":instrumentation:jaxws:jaxws-common:javaagent")) } + +tasks.withType().configureEach { + jvmArgs("-Dotel.instrumentation.common.experimental.controller-telemetry.enabled=true") +} diff --git a/instrumentation/jaxws/jaxws-2.0/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/jaxws/v2_0/JaxWsAnnotationsTest.groovy b/instrumentation/jaxws/jaxws-2.0/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/jaxws/v2_0/JaxWsAnnotationsTest.groovy index a4d2eef71264..2bbf561d9a80 100644 --- a/instrumentation/jaxws/jaxws-2.0/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/jaxws/v2_0/JaxWsAnnotationsTest.groovy +++ b/instrumentation/jaxws/jaxws-2.0/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/jaxws/v2_0/JaxWsAnnotationsTest.groovy @@ -6,7 +6,7 @@ package io.opentelemetry.javaagent.instrumentation.jaxws.v2_0 import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import io.opentelemetry.semconv.incubating.CodeIncubatingAttributes class JaxWsAnnotationsTest extends AgentInstrumentationSpecification { @@ -20,8 +20,8 @@ class JaxWsAnnotationsTest extends AgentInstrumentationSpecification { span(0) { name "SoapProvider.invoke" attributes { - "$SemanticAttributes.CODE_NAMESPACE" "io.opentelemetry.javaagent.instrumentation.jaxws.v2_0.SoapProvider" - "$SemanticAttributes.CODE_FUNCTION" "invoke" + "$CodeIncubatingAttributes.CODE_NAMESPACE" "io.opentelemetry.javaagent.instrumentation.jaxws.v2_0.SoapProvider" + "$CodeIncubatingAttributes.CODE_FUNCTION" "invoke" } } } diff --git a/instrumentation/jaxws/jaxws-3.0-common-testing/build.gradle.kts b/instrumentation/jaxws/jaxws-3.0-common-testing/build.gradle.kts new file mode 100644 index 000000000000..0827852d5c94 --- /dev/null +++ b/instrumentation/jaxws/jaxws-3.0-common-testing/build.gradle.kts @@ -0,0 +1,24 @@ +plugins { + id("org.unbroken-dome.xjc") + id("otel.java-conventions") +} + +tasks { + named("checkstyleMain") { + // exclude generated web service classes + exclude("**/hello_web_service/**") + } +} + +dependencies { + api("jakarta.xml.ws:jakarta.xml.ws-api:3.0.0") + api("jakarta.jws:jakarta.jws-api:3.0.0") + + api("org.eclipse.jetty:jetty-webapp:11.0.17") + api("org.springframework.ws:spring-ws-core:4.0.0") + + implementation(project(":testing-common")) + + xjcTool("com.sun.xml.bind:jaxb-xjc:3.0.2") + xjcTool("com.sun.xml.bind:jaxb-impl:3.0.2") +} diff --git a/instrumentation/jaxws/jaxws-3.0-common-testing/src/main/groovy/AbstractJaxWsTest.groovy b/instrumentation/jaxws/jaxws-3.0-common-testing/src/main/groovy/AbstractJaxWsTest.groovy new file mode 100644 index 000000000000..ff0dac6a25e5 --- /dev/null +++ b/instrumentation/jaxws/jaxws-3.0-common-testing/src/main/groovy/AbstractJaxWsTest.groovy @@ -0,0 +1,187 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +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.incubating.CodeIncubatingAttributes +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 + +import static io.opentelemetry.api.trace.SpanKind.INTERNAL +import static io.opentelemetry.api.trace.SpanKind.SERVER +import static io.opentelemetry.api.trace.StatusCode.ERROR + +abstract class AbstractJaxWsTest extends AgentInstrumentationSpecification implements HttpServerTestTrait { + + @Shared + private Jaxb2Marshaller marshaller = new Jaxb2Marshaller() + + @Shared + protected WebServiceTemplate webServiceTemplate = new WebServiceTemplate(marshaller) + + def setupSpec() { + setupServer() + + marshaller.setPackagesToScan(ClassUtils.getPackageName(HelloRequest)) + marshaller.afterPropertiesSet() + } + + def cleanupSpec() { + cleanupServer() + } + + @Override + Server startServer(int port) { + WebAppContext webAppContext = new WebAppContext() + webAppContext.setContextPath(getContextPath()) + // set up test application + webAppContext.setBaseResource(Resource.newSystemResource("test-app")) + webAppContext.getMetaData().addWebInfResource(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 + assertTraces(1) { + trace(0, spanCount) { + serverSpan(it, 0, serverSpanName(methodName)) + handlerSpan(it, 1, methodName, span(0)) + } + } + + 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 + def expectedException = new Exception("hello exception") + assertTraces(1) { + trace(0, spanCount) { + serverSpan(it, 0, serverSpanName(methodName), expectedException) + handlerSpan(it, 1, methodName, span(0), expectedException) + } + } + + where: + methodName << ["hello", "hello2"] + } + + def serverSpanName(String 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 { + "$CodeIncubatingAttributes.CODE_NAMESPACE" "hello.HelloServiceImpl" + "$CodeIncubatingAttributes.CODE_FUNCTION" methodName + } + } + } +} diff --git a/instrumentation/jaxws/jaxws-3.0-common-testing/src/main/groovy/hello/BaseHelloService.groovy b/instrumentation/jaxws/jaxws-3.0-common-testing/src/main/groovy/hello/BaseHelloService.groovy new file mode 100644 index 000000000000..a2d8da1e8a11 --- /dev/null +++ b/instrumentation/jaxws/jaxws-3.0-common-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/instrumentation/jaxws/jaxws-3.0-common-testing/src/main/groovy/hello/HelloService.groovy b/instrumentation/jaxws/jaxws-3.0-common-testing/src/main/groovy/hello/HelloService.groovy new file mode 100644 index 000000000000..6e79845e2063 --- /dev/null +++ b/instrumentation/jaxws/jaxws-3.0-common-testing/src/main/groovy/hello/HelloService.groovy @@ -0,0 +1,24 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package hello + +import jakarta.jws.WebParam +import jakarta.jws.WebResult +import jakarta.jws.WebService +import jakarta.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) + +} diff --git a/instrumentation/jaxws/jaxws-3.0-common-testing/src/main/groovy/hello/HelloServiceImpl.groovy b/instrumentation/jaxws/jaxws-3.0-common-testing/src/main/groovy/hello/HelloServiceImpl.groovy new file mode 100644 index 000000000000..5a54f9e42021 --- /dev/null +++ b/instrumentation/jaxws/jaxws-3.0-common-testing/src/main/groovy/hello/HelloServiceImpl.groovy @@ -0,0 +1,19 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package hello + +import jakarta.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/instrumentation/jaxws/jaxws-3.0-common-testing/src/main/schema/hello.xsd b/instrumentation/jaxws/jaxws-3.0-common-testing/src/main/schema/hello.xsd new file mode 100644 index 000000000000..f46d7d715238 --- /dev/null +++ b/instrumentation/jaxws/jaxws-3.0-common-testing/src/main/schema/hello.xsd @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/instrumentation/jaxws/jaxws-3.0-cxf-4.0-testing/build.gradle.kts b/instrumentation/jaxws/jaxws-3.0-cxf-4.0-testing/build.gradle.kts new file mode 100644 index 000000000000..c6d53b53b53e --- /dev/null +++ b/instrumentation/jaxws/jaxws-3.0-cxf-4.0-testing/build.gradle.kts @@ -0,0 +1,27 @@ +plugins { + id("otel.javaagent-testing") +} + +dependencies { + testLibrary("org.apache.cxf:cxf-rt-frontend-jaxws:4.0.0") + testLibrary("org.apache.cxf:cxf-rt-transports-http:4.0.0") + + testImplementation("jakarta.servlet:jakarta.servlet-api:5.0.0") + testImplementation(project(":instrumentation:jaxws:jaxws-3.0-common-testing")) + + testInstrumentation(project(":instrumentation:jaxws:jaxws-cxf-3.0:javaagent")) + + testInstrumentation(project(":instrumentation:servlet:servlet-5.0:javaagent")) + testInstrumentation(project(":instrumentation:jetty:jetty-11.0:javaagent")) +} + +otelJava { + minJavaVersionSupported.set(JavaVersion.VERSION_17) +} + +tasks.withType().configureEach { + // required on jdk17 + jvmArgs("--add-opens=java.base/java.lang=ALL-UNNAMED") + jvmArgs("-XX:+IgnoreUnrecognizedVMOptions") + jvmArgs("-Dotel.instrumentation.common.experimental.controller-telemetry.enabled=true") +} diff --git a/instrumentation/jaxws/jaxws-2.0-cxf-3.0/javaagent/src/test/groovy/CxfJaxWsTest.groovy b/instrumentation/jaxws/jaxws-3.0-cxf-4.0-testing/src/test/groovy/CxfJaxWsTest.groovy similarity index 100% rename from instrumentation/jaxws/jaxws-2.0-cxf-3.0/javaagent/src/test/groovy/CxfJaxWsTest.groovy rename to instrumentation/jaxws/jaxws-3.0-cxf-4.0-testing/src/test/groovy/CxfJaxWsTest.groovy diff --git a/instrumentation/jaxws/jaxws-3.0-cxf-4.0-testing/src/test/groovy/TestWsServlet.groovy b/instrumentation/jaxws/jaxws-3.0-cxf-4.0-testing/src/test/groovy/TestWsServlet.groovy new file mode 100644 index 000000000000..e68b2838d07c --- /dev/null +++ b/instrumentation/jaxws/jaxws-3.0-cxf-4.0-testing/src/test/groovy/TestWsServlet.groovy @@ -0,0 +1,37 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import hello.HelloServiceImpl +import io.opentelemetry.api.trace.Span +import io.opentelemetry.context.Context +import io.opentelemetry.instrumentation.api.instrumenter.LocalRootSpan +import org.apache.cxf.jaxws.EndpointImpl +import org.apache.cxf.message.Message +import org.apache.cxf.phase.AbstractPhaseInterceptor +import org.apache.cxf.phase.Phase +import org.apache.cxf.transport.servlet.CXFNonSpringServlet + +import jakarta.servlet.ServletConfig + +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") + endpoint.getOutInterceptors().add(new AbstractPhaseInterceptor(Phase.SETUP) { + @Override + void handleMessage(Message message) { + Context context = Context.current() + if (LocalRootSpan.fromContext(context) != Span.fromContext(context)) { + throw new IllegalStateException("handler span should be ended before outgoing interceptors") + } + } + }) + } +} diff --git a/instrumentation/jaxws/jaxws-2.0-cxf-3.0/javaagent/src/test/resources/test-app/WEB-INF/web.xml b/instrumentation/jaxws/jaxws-3.0-cxf-4.0-testing/src/test/resources/test-app/WEB-INF/web.xml similarity index 100% rename from instrumentation/jaxws/jaxws-2.0-cxf-3.0/javaagent/src/test/resources/test-app/WEB-INF/web.xml rename to instrumentation/jaxws/jaxws-3.0-cxf-4.0-testing/src/test/resources/test-app/WEB-INF/web.xml diff --git a/instrumentation/jaxws/jaxws-3.0-metro-2.2-testing/build.gradle.kts b/instrumentation/jaxws/jaxws-3.0-metro-2.2-testing/build.gradle.kts new file mode 100644 index 000000000000..6e882766eb42 --- /dev/null +++ b/instrumentation/jaxws/jaxws-3.0-metro-2.2-testing/build.gradle.kts @@ -0,0 +1,28 @@ +plugins { + id("otel.javaagent-testing") +} + +dependencies { + testLibrary("com.sun.xml.ws:jaxws-rt:3.0.0") + + testImplementation("jakarta.servlet:jakarta.servlet-api:5.0.0") + testImplementation(project(":instrumentation:jaxws:jaxws-3.0-common-testing")) + + testInstrumentation(project(":instrumentation:jaxws:jaxws-metro-2.2:javaagent")) + + testInstrumentation(project(":instrumentation:servlet:servlet-5.0:javaagent")) + testInstrumentation(project(":instrumentation:jetty:jetty-11.0:javaagent")) +} + +otelJava { + minJavaVersionSupported.set(JavaVersion.VERSION_17) +} + +tasks.withType().configureEach { + // required on jdk17 + jvmArgs("--add-exports=java.xml/com.sun.org.apache.xerces.internal.dom=ALL-UNNAMED") + jvmArgs("--add-exports=java.xml/com.sun.org.apache.xerces.internal.jaxp=ALL-UNNAMED") + jvmArgs("--add-opens=java.base/java.lang=ALL-UNNAMED") + jvmArgs("-XX:+IgnoreUnrecognizedVMOptions") + jvmArgs("-Dotel.instrumentation.common.experimental.controller-telemetry.enabled=true") +} diff --git a/instrumentation/jsf/jsf-myfaces-3.0/javaagent/src/test/groovy/Myfaces3Test.groovy b/instrumentation/jaxws/jaxws-3.0-metro-2.2-testing/src/test/groovy/MetroJaxWsTest.groovy similarity index 64% rename from instrumentation/jsf/jsf-myfaces-3.0/javaagent/src/test/groovy/Myfaces3Test.groovy rename to instrumentation/jaxws/jaxws-3.0-metro-2.2-testing/src/test/groovy/MetroJaxWsTest.groovy index 9a98485cafbb..c20abd4456f3 100644 --- a/instrumentation/jsf/jsf-myfaces-3.0/javaagent/src/test/groovy/Myfaces3Test.groovy +++ b/instrumentation/jaxws/jaxws-3.0-metro-2.2-testing/src/test/groovy/MetroJaxWsTest.groovy @@ -3,5 +3,5 @@ * SPDX-License-Identifier: Apache-2.0 */ -class Myfaces3Test extends BaseJsfTest { +class MetroJaxWsTest extends AbstractJaxWsTest { } diff --git a/instrumentation/jaxws/jaxws-3.0-metro-2.2-testing/src/test/resources/test-app/WEB-INF/sun-jaxws.xml b/instrumentation/jaxws/jaxws-3.0-metro-2.2-testing/src/test/resources/test-app/WEB-INF/sun-jaxws.xml new file mode 100644 index 000000000000..6bf3e05acba2 --- /dev/null +++ b/instrumentation/jaxws/jaxws-3.0-metro-2.2-testing/src/test/resources/test-app/WEB-INF/sun-jaxws.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/instrumentation/jaxws/jaxws-3.0-metro-2.2-testing/src/test/resources/test-app/WEB-INF/web.xml b/instrumentation/jaxws/jaxws-3.0-metro-2.2-testing/src/test/resources/test-app/WEB-INF/web.xml new file mode 100644 index 000000000000..037ef81d3bd2 --- /dev/null +++ b/instrumentation/jaxws/jaxws-3.0-metro-2.2-testing/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/instrumentation/jaxws/jaxws-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxws/common/JaxWsCodeAttributesGetter.java b/instrumentation/jaxws/jaxws-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxws/common/JaxWsCodeAttributesGetter.java index cbdd229e65e7..f5971abeb04c 100644 --- a/instrumentation/jaxws/jaxws-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxws/common/JaxWsCodeAttributesGetter.java +++ b/instrumentation/jaxws/jaxws-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxws/common/JaxWsCodeAttributesGetter.java @@ -5,7 +5,7 @@ package io.opentelemetry.javaagent.instrumentation.jaxws.common; -import io.opentelemetry.instrumentation.api.instrumenter.code.CodeAttributesGetter; +import io.opentelemetry.instrumentation.api.incubator.semconv.code.CodeAttributesGetter; public class JaxWsCodeAttributesGetter implements CodeAttributesGetter { diff --git a/instrumentation/jaxws/jaxws-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxws/common/JaxWsInstrumenterFactory.java b/instrumentation/jaxws/jaxws-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxws/common/JaxWsInstrumenterFactory.java index b67f6d32d619..0a3502758ec0 100644 --- a/instrumentation/jaxws/jaxws-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxws/common/JaxWsInstrumenterFactory.java +++ b/instrumentation/jaxws/jaxws-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxws/common/JaxWsInstrumenterFactory.java @@ -6,9 +6,9 @@ package io.opentelemetry.javaagent.instrumentation.jaxws.common; import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.instrumentation.api.incubator.semconv.code.CodeAttributesExtractor; +import io.opentelemetry.instrumentation.api.incubator.semconv.code.CodeSpanNameExtractor; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; -import io.opentelemetry.instrumentation.api.instrumenter.code.CodeAttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.code.CodeSpanNameExtractor; import io.opentelemetry.javaagent.bootstrap.internal.ExperimentalConfig; public final class JaxWsInstrumenterFactory { diff --git a/instrumentation/jaxws/jaxws-2.0-cxf-3.0/javaagent-unit-tests/build.gradle.kts b/instrumentation/jaxws/jaxws-cxf-3.0/javaagent-unit-tests/build.gradle.kts similarity index 78% rename from instrumentation/jaxws/jaxws-2.0-cxf-3.0/javaagent-unit-tests/build.gradle.kts rename to instrumentation/jaxws/jaxws-cxf-3.0/javaagent-unit-tests/build.gradle.kts index d8fa5cc7c0cb..d41fa8e62010 100644 --- a/instrumentation/jaxws/jaxws-2.0-cxf-3.0/javaagent-unit-tests/build.gradle.kts +++ b/instrumentation/jaxws/jaxws-cxf-3.0/javaagent-unit-tests/build.gradle.kts @@ -6,7 +6,7 @@ dependencies { compileOnly("javax.servlet:javax.servlet-api:3.0.1") compileOnly("org.apache.cxf:cxf-rt-frontend-jaxws:3.0.0") - implementation(project(":instrumentation:jaxws:jaxws-2.0-cxf-3.0:javaagent")) + implementation(project(":instrumentation:jaxws:jaxws-cxf-3.0:javaagent")) testImplementation(project(":instrumentation-api")) testImplementation("org.apache.cxf:cxf-rt-frontend-jaxws:3.0.0") diff --git a/instrumentation/jaxws/jaxws-2.0-cxf-3.0/javaagent-unit-tests/src/test/java/io/opentelemetry/javaagent/instrumentation/cxf/TracingStartInInterceptorTest.java b/instrumentation/jaxws/jaxws-cxf-3.0/javaagent-unit-tests/src/test/java/io/opentelemetry/javaagent/instrumentation/cxf/TracingStartInInterceptorTest.java similarity index 100% rename from instrumentation/jaxws/jaxws-2.0-cxf-3.0/javaagent-unit-tests/src/test/java/io/opentelemetry/javaagent/instrumentation/cxf/TracingStartInInterceptorTest.java rename to instrumentation/jaxws/jaxws-cxf-3.0/javaagent-unit-tests/src/test/java/io/opentelemetry/javaagent/instrumentation/cxf/TracingStartInInterceptorTest.java diff --git a/instrumentation/jaxws/jaxws-2.0-cxf-3.0/javaagent/build.gradle.kts b/instrumentation/jaxws/jaxws-cxf-3.0/javaagent/build.gradle.kts similarity index 86% rename from instrumentation/jaxws/jaxws-2.0-cxf-3.0/javaagent/build.gradle.kts rename to instrumentation/jaxws/jaxws-cxf-3.0/javaagent/build.gradle.kts index dc3a9290c260..5ed3ec89bbcf 100644 --- a/instrumentation/jaxws/jaxws-2.0-cxf-3.0/javaagent/build.gradle.kts +++ b/instrumentation/jaxws/jaxws-cxf-3.0/javaagent/build.gradle.kts @@ -8,8 +8,9 @@ muzzle { module.set("cxf-rt-frontend-jaxws") // all earlier versions in maven central also pass muzzle check, // but 3.0.0 is already 8 years old and testing earlier versions adds complexity - versions.set("[3.0.0,4)") + versions.set("[3.0.0,)") extraDependency("javax.servlet:javax.servlet-api:3.0.1") + extraDependency("jakarta.servlet:jakarta.servlet-api:5.0.0") } } @@ -18,6 +19,8 @@ dependencies { library("org.apache.cxf:cxf-rt-frontend-jaxws:3.0.0") compileOnly("javax.servlet:javax.servlet-api:3.0.1") + compileOnly("jakarta.servlet:jakarta.servlet-api:5.0.0") + compileOnly(project(":muzzle")) testLibrary("org.apache.cxf:cxf-rt-transports-http:3.0.0") @@ -45,4 +48,5 @@ tasks.withType().configureEach { // required on jdk17 jvmArgs("--add-opens=java.base/java.lang=ALL-UNNAMED") jvmArgs("-XX:+IgnoreUnrecognizedVMOptions") + jvmArgs("-Dotel.instrumentation.common.experimental.controller-telemetry.enabled=true") } diff --git a/instrumentation/jaxws/jaxws-2.0-cxf-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cxf/CxfHelper.java b/instrumentation/jaxws/jaxws-cxf-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cxf/CxfHelper.java similarity index 100% rename from instrumentation/jaxws/jaxws-2.0-cxf-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cxf/CxfHelper.java rename to instrumentation/jaxws/jaxws-cxf-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cxf/CxfHelper.java diff --git a/instrumentation/jaxws/jaxws-2.0-cxf-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cxf/CxfInstrumentationModule.java b/instrumentation/jaxws/jaxws-cxf-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cxf/CxfInstrumentationModule.java similarity index 100% rename from instrumentation/jaxws/jaxws-2.0-cxf-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cxf/CxfInstrumentationModule.java rename to instrumentation/jaxws/jaxws-cxf-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cxf/CxfInstrumentationModule.java diff --git a/instrumentation/jaxws/jaxws-2.0-cxf-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cxf/CxfRequest.java b/instrumentation/jaxws/jaxws-cxf-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cxf/CxfRequest.java similarity index 100% rename from instrumentation/jaxws/jaxws-2.0-cxf-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cxf/CxfRequest.java rename to instrumentation/jaxws/jaxws-cxf-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cxf/CxfRequest.java diff --git a/instrumentation/jaxws/jaxws-cxf-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cxf/CxfServerSpanNaming.java b/instrumentation/jaxws/jaxws-cxf-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cxf/CxfServerSpanNaming.java new file mode 100644 index 000000000000..4a5400f6e838 --- /dev/null +++ b/instrumentation/jaxws/jaxws-cxf-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cxf/CxfServerSpanNaming.java @@ -0,0 +1,66 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.cxf; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.instrumenter.LocalRootSpan; +import io.opentelemetry.javaagent.bootstrap.servlet.ServletContextPath; +import io.opentelemetry.javaagent.tooling.muzzle.NoMuzzle; + +public final class CxfServerSpanNaming { + private static final Class JAVAX_SERVLET_REQUEST = + loadClass("javax.servlet.http.HttpServletRequest"); + private static final Class JAKARTA_SERVLET_REQUEST = + loadClass("jakarta.servlet.http.HttpServletRequest"); + + private static Class loadClass(String className) { + try { + return Class.forName(className); + } catch (ClassNotFoundException exception) { + return null; + } + } + + public static void updateServerSpanName(Context context, CxfRequest cxfRequest) { + String spanName = cxfRequest.spanName(); + if (spanName == null) { + return; + } + + Span serverSpan = LocalRootSpan.fromContextOrNull(context); + if (serverSpan == null) { + return; + } + + Object request = cxfRequest.message().get("HTTP.REQUEST"); + if (request != null) { + String servletPath = null; + if (JAVAX_SERVLET_REQUEST != null && JAVAX_SERVLET_REQUEST.isInstance(request)) { + servletPath = getJavaxServletPath(request); + } else if (JAKARTA_SERVLET_REQUEST != null && JAKARTA_SERVLET_REQUEST.isInstance(request)) { + servletPath = getJakartaServletPath(request); + } + if (servletPath != null && !servletPath.isEmpty()) { + spanName = servletPath + "/" + spanName; + } + } + + serverSpan.updateName(ServletContextPath.prepend(context, spanName)); + } + + @NoMuzzle + private static String getJavaxServletPath(Object request) { + return ((javax.servlet.http.HttpServletRequest) request).getServletPath(); + } + + @NoMuzzle + private static String getJakartaServletPath(Object request) { + return ((jakarta.servlet.http.HttpServletRequest) request).getServletPath(); + } + + private CxfServerSpanNaming() {} +} diff --git a/instrumentation/jaxws/jaxws-2.0-cxf-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cxf/CxfSingletons.java b/instrumentation/jaxws/jaxws-cxf-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cxf/CxfSingletons.java similarity index 97% rename from instrumentation/jaxws/jaxws-2.0-cxf-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cxf/CxfSingletons.java rename to instrumentation/jaxws/jaxws-cxf-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cxf/CxfSingletons.java index 9da76ff3b41c..b048d97985b8 100644 --- a/instrumentation/jaxws/jaxws-2.0-cxf-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cxf/CxfSingletons.java +++ b/instrumentation/jaxws/jaxws-cxf-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cxf/CxfSingletons.java @@ -10,7 +10,7 @@ import io.opentelemetry.javaagent.bootstrap.internal.ExperimentalConfig; public class CxfSingletons { - private static final String INSTRUMENTATION_NAME = "io.opentelemetry.jaxws-2.0-cxf-3.0"; + private static final String INSTRUMENTATION_NAME = "io.opentelemetry.jaxws-cxf-3.0"; private static final Instrumenter INSTRUMENTER; diff --git a/instrumentation/jaxws/jaxws-2.0-cxf-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cxf/JaxWsServerFactoryBeanInstrumentation.java b/instrumentation/jaxws/jaxws-cxf-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cxf/JaxWsServerFactoryBeanInstrumentation.java similarity index 100% rename from instrumentation/jaxws/jaxws-2.0-cxf-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cxf/JaxWsServerFactoryBeanInstrumentation.java rename to instrumentation/jaxws/jaxws-cxf-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cxf/JaxWsServerFactoryBeanInstrumentation.java diff --git a/instrumentation/jaxws/jaxws-2.0-cxf-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cxf/TracingEndInInterceptor.java b/instrumentation/jaxws/jaxws-cxf-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cxf/TracingEndInInterceptor.java similarity index 100% rename from instrumentation/jaxws/jaxws-2.0-cxf-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cxf/TracingEndInInterceptor.java rename to instrumentation/jaxws/jaxws-cxf-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cxf/TracingEndInInterceptor.java diff --git a/instrumentation/jaxws/jaxws-2.0-cxf-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cxf/TracingOutFaultInterceptor.java b/instrumentation/jaxws/jaxws-cxf-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cxf/TracingOutFaultInterceptor.java similarity index 100% rename from instrumentation/jaxws/jaxws-2.0-cxf-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cxf/TracingOutFaultInterceptor.java rename to instrumentation/jaxws/jaxws-cxf-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cxf/TracingOutFaultInterceptor.java diff --git a/instrumentation/jaxws/jaxws-2.0-cxf-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cxf/TracingStartInInterceptor.java b/instrumentation/jaxws/jaxws-cxf-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cxf/TracingStartInInterceptor.java similarity index 100% rename from instrumentation/jaxws/jaxws-2.0-cxf-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cxf/TracingStartInInterceptor.java rename to instrumentation/jaxws/jaxws-cxf-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/cxf/TracingStartInInterceptor.java diff --git a/instrumentation/jsf/jsf-mojarra-3.0/javaagent/src/test/groovy/Mojarra3Test.groovy b/instrumentation/jaxws/jaxws-cxf-3.0/javaagent/src/test/groovy/CxfJaxWsTest.groovy similarity index 65% rename from instrumentation/jsf/jsf-mojarra-3.0/javaagent/src/test/groovy/Mojarra3Test.groovy rename to instrumentation/jaxws/jaxws-cxf-3.0/javaagent/src/test/groovy/CxfJaxWsTest.groovy index 477e318d7000..b81fa377c198 100644 --- a/instrumentation/jsf/jsf-mojarra-3.0/javaagent/src/test/groovy/Mojarra3Test.groovy +++ b/instrumentation/jaxws/jaxws-cxf-3.0/javaagent/src/test/groovy/CxfJaxWsTest.groovy @@ -3,5 +3,5 @@ * SPDX-License-Identifier: Apache-2.0 */ -class Mojarra3Test extends BaseJsfTest { +class CxfJaxWsTest extends AbstractJaxWsTest { } diff --git a/instrumentation/jaxws/jaxws-2.0-cxf-3.0/javaagent/src/test/groovy/TestWsServlet.groovy b/instrumentation/jaxws/jaxws-cxf-3.0/javaagent/src/test/groovy/TestWsServlet.groovy similarity index 100% rename from instrumentation/jaxws/jaxws-2.0-cxf-3.0/javaagent/src/test/groovy/TestWsServlet.groovy rename to instrumentation/jaxws/jaxws-cxf-3.0/javaagent/src/test/groovy/TestWsServlet.groovy diff --git a/instrumentation/jaxws/jaxws-cxf-3.0/javaagent/src/test/resources/test-app/WEB-INF/web.xml b/instrumentation/jaxws/jaxws-cxf-3.0/javaagent/src/test/resources/test-app/WEB-INF/web.xml new file mode 100644 index 000000000000..9c6ebc21da06 --- /dev/null +++ b/instrumentation/jaxws/jaxws-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/instrumentation/jaxws/jaxws-jws-api-1.1/javaagent/build.gradle.kts b/instrumentation/jaxws/jaxws-jws-api-1.1/javaagent/build.gradle.kts index 027c32c288d5..8aa0c204f3e9 100644 --- a/instrumentation/jaxws/jaxws-jws-api-1.1/javaagent/build.gradle.kts +++ b/instrumentation/jaxws/jaxws-jws-api-1.1/javaagent/build.gradle.kts @@ -14,3 +14,7 @@ dependencies { library("javax.jws:javax.jws-api:1.1") implementation(project(":instrumentation:jaxws:jaxws-common:javaagent")) } + +tasks.withType().configureEach { + jvmArgs("-Dotel.instrumentation.common.experimental.controller-telemetry.enabled=true") +} diff --git a/instrumentation/jaxws/jaxws-jws-api-1.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxws/jws/v1_1/JwsInstrumentationModule.java b/instrumentation/jaxws/jaxws-jws-api-1.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxws/jws/v1_1/JwsInstrumentationModule.java index 1df371d02b6a..227db7466d24 100644 --- a/instrumentation/jaxws/jaxws-jws-api-1.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxws/jws/v1_1/JwsInstrumentationModule.java +++ b/instrumentation/jaxws/jaxws-jws-api-1.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxws/jws/v1_1/JwsInstrumentationModule.java @@ -6,8 +6,10 @@ package io.opentelemetry.javaagent.instrumentation.jaxws.jws.v1_1; import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.bootstrap.internal.ExperimentalConfig; import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; import java.util.Collections; import java.util.List; @@ -22,4 +24,10 @@ public JwsInstrumentationModule() { public List typeInstrumentations() { return Collections.singletonList(new JwsAnnotationsInstrumentation()); } + + @Override + public boolean defaultEnabled(ConfigProperties config) { + // this instrumentation only produces controller telemetry + return super.defaultEnabled(config) && ExperimentalConfig.get().controllerTelemetryEnabled(); + } } diff --git a/instrumentation/jaxws/jaxws-jws-api-1.1/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/jaxws/jws/v1_1/JwsAnnotationsTest.groovy b/instrumentation/jaxws/jaxws-jws-api-1.1/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/jaxws/jws/v1_1/JwsAnnotationsTest.groovy index 150a8d7378ca..9498fb9b6bc1 100644 --- a/instrumentation/jaxws/jaxws-jws-api-1.1/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/jaxws/jws/v1_1/JwsAnnotationsTest.groovy +++ b/instrumentation/jaxws/jaxws-jws-api-1.1/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/jaxws/jws/v1_1/JwsAnnotationsTest.groovy @@ -6,7 +6,7 @@ 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.semconv.incubating.CodeIncubatingAttributes import java.lang.reflect.Proxy @@ -24,8 +24,8 @@ class JwsAnnotationsTest extends AgentInstrumentationSpecification { span(0) { name "WebServiceClass.doSomethingPublic" attributes { - "$SemanticAttributes.CODE_NAMESPACE" "io.opentelemetry.javaagent.instrumentation.jaxws.jws.v1_1.WebServiceClass" - "$SemanticAttributes.CODE_FUNCTION" "doSomethingPublic" + "$CodeIncubatingAttributes.CODE_NAMESPACE" "io.opentelemetry.javaagent.instrumentation.jaxws.jws.v1_1.WebServiceClass" + "$CodeIncubatingAttributes.CODE_FUNCTION" "doSomethingPublic" } } } @@ -44,8 +44,8 @@ class JwsAnnotationsTest extends AgentInstrumentationSpecification { span(0) { name "WebServiceFromInterface.partOfPublicInterface" attributes { - "$SemanticAttributes.CODE_NAMESPACE" "io.opentelemetry.javaagent.instrumentation.jaxws.jws.v1_1.WebServiceFromInterface" - "$SemanticAttributes.CODE_FUNCTION" "partOfPublicInterface" + "$CodeIncubatingAttributes.CODE_NAMESPACE" "io.opentelemetry.javaagent.instrumentation.jaxws.jws.v1_1.WebServiceFromInterface" + "$CodeIncubatingAttributes.CODE_FUNCTION" "partOfPublicInterface" } } } @@ -68,8 +68,8 @@ class JwsAnnotationsTest extends AgentInstrumentationSpecification { span(0) { name "WebServiceFromInterface.partOfPublicInterface" attributes { - "$SemanticAttributes.CODE_NAMESPACE" "io.opentelemetry.javaagent.instrumentation.jaxws.jws.v1_1.WebServiceFromInterface" - "$SemanticAttributes.CODE_FUNCTION" "partOfPublicInterface" + "$CodeIncubatingAttributes.CODE_NAMESPACE" "io.opentelemetry.javaagent.instrumentation.jaxws.jws.v1_1.WebServiceFromInterface" + "$CodeIncubatingAttributes.CODE_FUNCTION" "partOfPublicInterface" } } } diff --git a/instrumentation/jaxws/jaxws-metro-2.2/javaagent/build.gradle.kts b/instrumentation/jaxws/jaxws-metro-2.2/javaagent/build.gradle.kts new file mode 100644 index 000000000000..a5c19447373d --- /dev/null +++ b/instrumentation/jaxws/jaxws-metro-2.2/javaagent/build.gradle.kts @@ -0,0 +1,26 @@ +plugins { + id("otel.javaagent-instrumentation") +} + +muzzle { + pass { + group.set("com.sun.xml.ws") + module.set("jaxws-rt") + versions.set("[2.2.0.1,)") + // version 2.3.4 depends on org.glassfish.gmbal:gmbal-api-only:4.0.3 which does not exist + skip("2.3.4") + assertInverse.set(true) + } +} + +dependencies { + bootstrap(project(":instrumentation:servlet:servlet-common:bootstrap")) + + library("com.sun.xml.ws:jaxws-rt:2.2.0.1") + // early versions of streambuffer depend on latest release of org.jvnet.staxex:stax-ex + // which doesn't work with java 8 + library("com.sun.xml.stream.buffer:streambuffer:1.4") + + compileOnly("javax.xml.ws:jaxws-api:2.0") + compileOnly("jakarta.xml.ws:jakarta.xml.ws-api:3.0.0") +} diff --git a/instrumentation/jaxws/jaxws-2.0-metro-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/metro/MetroHelper.java b/instrumentation/jaxws/jaxws-metro-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/metro/MetroHelper.java similarity index 92% rename from instrumentation/jaxws/jaxws-2.0-metro-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/metro/MetroHelper.java rename to instrumentation/jaxws/jaxws-metro-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/metro/MetroHelper.java index b2122c29e2b0..8c8ad4996b93 100644 --- a/instrumentation/jaxws/jaxws-2.0-metro-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/metro/MetroHelper.java +++ b/instrumentation/jaxws/jaxws-metro-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/metro/MetroHelper.java @@ -18,13 +18,16 @@ public final class MetroHelper { private static final String SCOPE_KEY = MetroHelper.class.getName() + ".Scope"; private static final String THROWABLE_KEY = MetroHelper.class.getName() + ".Throwable"; + private static final MetroServerSpanNameUpdater SPAN_NAME_UPDATER = + new MetroServerSpanNameUpdater(); + private MetroHelper() {} public static void start(WSEndpoint endpoint, Packet packet) { Context parentContext = Context.current(); MetroRequest request = new MetroRequest(endpoint, packet); - MetroServerSpanNaming.updateServerSpanName(parentContext, request); + SPAN_NAME_UPDATER.updateServerSpanName(parentContext, request); if (!instrumenter().shouldStart(parentContext, request)) { return; diff --git a/instrumentation/jaxws/jaxws-2.0-metro-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/metro/MetroInstrumentationModule.java b/instrumentation/jaxws/jaxws-metro-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/metro/MetroInstrumentationModule.java similarity index 92% rename from instrumentation/jaxws/jaxws-2.0-metro-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/metro/MetroInstrumentationModule.java rename to instrumentation/jaxws/jaxws-metro-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/metro/MetroInstrumentationModule.java index 957dbf04537a..2a39fe143e3e 100644 --- a/instrumentation/jaxws/jaxws-2.0-metro-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/metro/MetroInstrumentationModule.java +++ b/instrumentation/jaxws/jaxws-metro-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/metro/MetroInstrumentationModule.java @@ -22,7 +22,7 @@ public MetroInstrumentationModule() { @Override public ElementMatcher.Junction classLoaderMatcher() { - return hasClassesNamed("javax.jws.WebService"); + return hasClassesNamed("com.sun.xml.ws.api.pipe.ServerTubeAssemblerContext"); } @Override diff --git a/instrumentation/jaxws/jaxws-2.0-metro-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/metro/MetroRequest.java b/instrumentation/jaxws/jaxws-metro-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/metro/MetroRequest.java similarity index 100% rename from instrumentation/jaxws/jaxws-2.0-metro-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/metro/MetroRequest.java rename to instrumentation/jaxws/jaxws-metro-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/metro/MetroRequest.java diff --git a/instrumentation/jaxws/jaxws-metro-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/metro/MetroServerSpanNameUpdater.java b/instrumentation/jaxws/jaxws-metro-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/metro/MetroServerSpanNameUpdater.java new file mode 100644 index 000000000000..2d9ee053dafa --- /dev/null +++ b/instrumentation/jaxws/jaxws-metro-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/metro/MetroServerSpanNameUpdater.java @@ -0,0 +1,166 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.metro; + +import com.sun.xml.ws.api.message.Packet; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.instrumenter.LocalRootSpan; +import io.opentelemetry.javaagent.bootstrap.servlet.ServletContextPath; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Objects; +import java.util.logging.Logger; + +final class MetroServerSpanNameUpdater { + + private static final Logger logger = Logger.getLogger(MetroServerSpanNameUpdater.class.getName()); + + /** + * Map of message context key names to the {@link HttpServletRequestAdapter} to handle the {@code + * HttpServletRequest} found at that message context key. + * + *

This map will contain at most two entries: + * + *

    + *
  • {@value javax.xml.ws.handler.MessageContext#SERVLET_REQUEST} to an {@link + * HttpServletRequestAdapter} that handles {@code javax.servlet.http.HttpServletRequest} + *
  • {@value jakarta.xml.ws.handler.MessageContext#SERVLET_REQUEST} to an {@link + * HttpServletRequestAdapter} that handles {@code jakarta.servlet.http.HttpServletRequest} + *
+ */ + private final Map servletRequestAdapters; + + public MetroServerSpanNameUpdater() { + this.servletRequestAdapters = new LinkedHashMap<>(); + + registerHttpServletRequestAdapter( + "Jakarta EE", + // Same as jakarta.xml.ws.handler.MessageContext.SERVLET_REQUEST + "jakarta.xml.ws.servlet.request", + "jakarta.servlet.http.HttpServletRequest"); + + registerHttpServletRequestAdapter( + "Java EE", + // Same as javax.xml.ws.handler.MessageContext.SERVLET_REQUEST + "javax.xml.ws.servlet.request", + "javax.servlet.http.HttpServletRequest"); + } + + /** + * Registers a {@link HttpServletRequestAdapter} in the {@link #servletRequestAdapters} with the + * given {@code key} if the given {@code httpServletRequestClassName} is on the classpath. + */ + private void registerHttpServletRequestAdapter( + String name, String key, String httpServletRequestClassName) { + HttpServletRequestAdapter adapter; + try { + adapter = new HttpServletRequestAdapter(Class.forName(httpServletRequestClassName)); + } catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException e) { + // Ignore. Don't register + return; + } + servletRequestAdapters.put(key, adapter); + logger.finest(() -> "Enabled " + name + " jaxws metro server span naming"); + } + + public void updateServerSpanName(Context context, MetroRequest metroRequest) { + String spanName = metroRequest.spanName(); + if (spanName == null) { + return; + } + + Span serverSpan = LocalRootSpan.fromContextOrNull(context); + if (serverSpan == null) { + return; + } + + for (Map.Entry httpServletRequestAdapterEntry : + servletRequestAdapters.entrySet()) { + Packet packet = metroRequest.packet(); + String key = httpServletRequestAdapterEntry.getKey(); + if (packet.supports(key)) { + Object request = packet.get(key); + HttpServletRequestAdapter httpServletRequestAdapter = + httpServletRequestAdapterEntry.getValue(); + if (httpServletRequestAdapter.canHandle(request)) { + String servletPath = httpServletRequestAdapter.getServletPath(request); + if (servletPath != null && !servletPath.isEmpty()) { + String pathInfo = httpServletRequestAdapter.getPathInfo(request); + if (pathInfo != null) { + spanName = 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 + String operationName = packet.getWSDLOperation().getLocalPart(); + spanName = servletPath + "/" + operationName; + } + break; + } + } + } + } + + serverSpan.updateName(ServletContextPath.prepend(context, spanName)); + } + + /** + * Adapter class for accessing the methods needed from either {@code + * jakarta.servlet.http.HttpServletRequest} or {@code javax.servlet.http.HttpServletRequest}. + */ + private static class HttpServletRequestAdapter { + + private final Class httpServletRequestClass; + private final MethodHandle getServletPathMethodHandle; + private final MethodHandle getPathInfoMethodHandle; + + private HttpServletRequestAdapter(Class httpServletRequestClass) + throws NoSuchMethodException, IllegalAccessException { + this.httpServletRequestClass = + Objects.requireNonNull( + httpServletRequestClass, "httpServletRequestClass must not be null"); + + MethodHandles.Lookup lookup = MethodHandles.lookup(); + this.getServletPathMethodHandle = + lookup.unreflect(httpServletRequestClass.getMethod("getServletPath")); + this.getPathInfoMethodHandle = + lookup.unreflect(httpServletRequestClass.getMethod("getPathInfo")); + } + + public boolean canHandle(Object httpServletRequest) { + return httpServletRequestClass.isInstance(httpServletRequest); + } + + public String getServletPath(Object httpServletRequest) { + return invokeSafely(getServletPathMethodHandle, httpServletRequest); + } + + public String getPathInfo(Object httpServletRequest) { + return invokeSafely(getPathInfoMethodHandle, httpServletRequest); + } + + private static String invokeSafely(MethodHandle methodHandle, Object httpServletRequest) { + try { + return (String) methodHandle.invoke(httpServletRequest); + } catch (UnsupportedOperationException e) { + // when request wrapper throws UnsupportedOperationException return null to skip adding + // servlet path to the span name + // https://github.com/open-telemetry/opentelemetry-java-instrumentation/issues/10986 + return null; + } catch (RuntimeException | Error e) { + throw e; + } catch (Throwable t) { + /* + * This is impossible, because the methods being invoked do not throw checked exceptions, + * and unchecked exceptions and errors are handled above + */ + throw new AssertionError(t); + } + } + } +} diff --git a/instrumentation/jaxws/jaxws-2.0-metro-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/metro/MetroSingletons.java b/instrumentation/jaxws/jaxws-metro-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/metro/MetroSingletons.java similarity index 96% rename from instrumentation/jaxws/jaxws-2.0-metro-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/metro/MetroSingletons.java rename to instrumentation/jaxws/jaxws-metro-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/metro/MetroSingletons.java index 470bccf76cc9..aa6ef3858d56 100644 --- a/instrumentation/jaxws/jaxws-2.0-metro-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/metro/MetroSingletons.java +++ b/instrumentation/jaxws/jaxws-metro-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/metro/MetroSingletons.java @@ -10,7 +10,7 @@ import io.opentelemetry.javaagent.bootstrap.internal.ExperimentalConfig; public class MetroSingletons { - private static final String INSTRUMENTATION_NAME = "io.opentelemetry.jaxws-2.0-metro-2.2"; + private static final String INSTRUMENTATION_NAME = "io.opentelemetry.jaxws-metro-2.2"; private static final Instrumenter INSTRUMENTER; diff --git a/instrumentation/jaxws/jaxws-2.0-metro-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/metro/ServerTubeAssemblerContextInstrumentation.java b/instrumentation/jaxws/jaxws-metro-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/metro/ServerTubeAssemblerContextInstrumentation.java similarity index 100% rename from instrumentation/jaxws/jaxws-2.0-metro-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/metro/ServerTubeAssemblerContextInstrumentation.java rename to instrumentation/jaxws/jaxws-metro-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/metro/ServerTubeAssemblerContextInstrumentation.java diff --git a/instrumentation/jaxws/jaxws-2.0-metro-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/metro/SoapFaultBuilderInstrumentation.java b/instrumentation/jaxws/jaxws-metro-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/metro/SoapFaultBuilderInstrumentation.java similarity index 100% rename from instrumentation/jaxws/jaxws-2.0-metro-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/metro/SoapFaultBuilderInstrumentation.java rename to instrumentation/jaxws/jaxws-metro-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/metro/SoapFaultBuilderInstrumentation.java diff --git a/instrumentation/jaxws/jaxws-2.0-metro-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/metro/TracingTube.java b/instrumentation/jaxws/jaxws-metro-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/metro/TracingTube.java similarity index 100% rename from instrumentation/jaxws/jaxws-2.0-metro-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/metro/TracingTube.java rename to instrumentation/jaxws/jaxws-metro-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/metro/TracingTube.java diff --git a/instrumentation/jboss-logmanager/README.md b/instrumentation/jboss-logmanager/README.md new file mode 100644 index 000000000000..1cedd4d8aac7 --- /dev/null +++ b/instrumentation/jboss-logmanager/README.md @@ -0,0 +1,6 @@ +# Settings for the JBoss Log Manager instrumentation + +| System property | Type | Default | Description | +|-----------------------------------------------------------------------------|---------|---------|--------------------------------------------------------------------------------------------------------------| +| `otel.instrumentation.jboss-logmanager.experimental-log-attributes` | Boolean | `false` | Enable the capture of experimental log attributes `thread.name` and `thread.id`. | +| `otel.instrumentation.jboss-logmanager.experimental.capture-mdc-attributes` | String | | Comma separated list of MDC attributes to capture. Use the wildcard character `*` to capture all attributes. | diff --git a/instrumentation/jboss-logmanager/jboss-logmanager-appender-1.1/javaagent/build.gradle.kts b/instrumentation/jboss-logmanager/jboss-logmanager-appender-1.1/javaagent/build.gradle.kts index 387c0000e008..99b421c73921 100644 --- a/instrumentation/jboss-logmanager/jboss-logmanager-appender-1.1/javaagent/build.gradle.kts +++ b/instrumentation/jboss-logmanager/jboss-logmanager-appender-1.1/javaagent/build.gradle.kts @@ -22,6 +22,14 @@ dependencies { testImplementation("org.awaitility:awaitility") } +val latestDepTest = findProperty("testLatestDeps") as Boolean + +if (latestDepTest) { + otelJava { + minJavaVersionSupported.set(JavaVersion.VERSION_11) + } +} + tasks.withType().configureEach { // TODO run tests both with and without experimental log attributes jvmArgs("-Dotel.instrumentation.jboss-logmanager.experimental.capture-mdc-attributes=*") diff --git a/instrumentation/jboss-logmanager/jboss-logmanager-appender-1.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jbosslogmanager/appender/v1_1/LoggingEventMapper.java b/instrumentation/jboss-logmanager/jboss-logmanager-appender-1.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jbosslogmanager/appender/v1_1/LoggingEventMapper.java index 5bbe19c51ce9..b8e13d2f3e19 100644 --- a/instrumentation/jboss-logmanager/jboss-logmanager-appender-1.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jbosslogmanager/appender/v1_1/LoggingEventMapper.java +++ b/instrumentation/jboss-logmanager/jboss-logmanager-appender-1.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jbosslogmanager/appender/v1_1/LoggingEventMapper.java @@ -6,6 +6,7 @@ package io.opentelemetry.javaagent.instrumentation.jbosslogmanager.appender.v1_1; import static java.util.Collections.emptyList; +import static java.util.concurrent.TimeUnit.MILLISECONDS; import io.opentelemetry.api.GlobalOpenTelemetry; import io.opentelemetry.api.common.AttributeKey; @@ -15,8 +16,9 @@ import io.opentelemetry.api.logs.Severity; import io.opentelemetry.context.Context; import io.opentelemetry.instrumentation.api.internal.cache.Cache; -import io.opentelemetry.javaagent.bootstrap.internal.InstrumentationConfig; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.opentelemetry.javaagent.bootstrap.internal.AgentInstrumentationConfig; +import io.opentelemetry.semconv.ExceptionAttributes; +import io.opentelemetry.semconv.incubating.ThreadIncubatingAttributes; import java.io.PrintWriter; import java.io.StringWriter; import java.util.List; @@ -35,7 +37,7 @@ public final class LoggingEventMapper { private final List captureMdcAttributes; private static final boolean captureExperimentalAttributes = - InstrumentationConfig.get() + AgentInstrumentationConfig.get() .getBoolean("otel.instrumentation.jboss-logmanager.experimental-log-attributes", false); // cached as an optimization @@ -43,7 +45,7 @@ public final class LoggingEventMapper { private LoggingEventMapper() { this.captureMdcAttributes = - InstrumentationConfig.get() + AgentInstrumentationConfig.get() .getList( "otel.instrumentation.jboss-logmanager.experimental.capture-mdc-attributes", emptyList()); @@ -81,24 +83,25 @@ public void capture(Logger logger, ExtLogRecord record) { if (throwable != null) { // TODO (trask) extract method for recording exception into // io.opentelemetry:opentelemetry-api - attributes.put(SemanticAttributes.EXCEPTION_TYPE, throwable.getClass().getName()); - attributes.put(SemanticAttributes.EXCEPTION_MESSAGE, throwable.getMessage()); + attributes.put(ExceptionAttributes.EXCEPTION_TYPE, throwable.getClass().getName()); + attributes.put(ExceptionAttributes.EXCEPTION_MESSAGE, throwable.getMessage()); StringWriter writer = new StringWriter(); throwable.printStackTrace(new PrintWriter(writer)); - attributes.put(SemanticAttributes.EXCEPTION_STACKTRACE, writer.toString()); + attributes.put(ExceptionAttributes.EXCEPTION_STACKTRACE, writer.toString()); } captureMdcAttributes(attributes); if (captureExperimentalAttributes) { Thread currentThread = Thread.currentThread(); - attributes.put(SemanticAttributes.THREAD_NAME, currentThread.getName()); - attributes.put(SemanticAttributes.THREAD_ID, currentThread.getId()); + attributes.put(ThreadIncubatingAttributes.THREAD_NAME, currentThread.getName()); + attributes.put(ThreadIncubatingAttributes.THREAD_ID, currentThread.getId()); } builder.setAllAttributes(attributes.build()); builder.setContext(Context.current()); + builder.setTimestamp(record.getMillis(), MILLISECONDS); builder.emit(); } @@ -125,8 +128,7 @@ private void captureMdcAttributes(AttributesBuilder attributes) { } public static AttributeKey getMdcAttributeKey(String key) { - return mdcAttributeKeys.computeIfAbsent( - key, k -> AttributeKey.stringKey("jboss-logmanager.mdc." + k)); + return mdcAttributeKeys.computeIfAbsent(key, AttributeKey::stringKey); } private static Severity levelToSeverity(java.util.logging.Level level) { diff --git a/instrumentation/jboss-logmanager/jboss-logmanager-appender-1.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/jbosslogmanager/appender/v1_1/JbossLogmanagerTest.java b/instrumentation/jboss-logmanager/jboss-logmanager-appender-1.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/jbosslogmanager/appender/v1_1/JbossLogmanagerTest.java index cd3268443659..acef12077658 100644 --- a/instrumentation/jboss-logmanager/jboss-logmanager-appender-1.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/jbosslogmanager/appender/v1_1/JbossLogmanagerTest.java +++ b/instrumentation/jboss-logmanager/jboss-logmanager-appender-1.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/jbosslogmanager/appender/v1_1/JbossLogmanagerTest.java @@ -8,16 +8,24 @@ import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.satisfies; -import static org.assertj.core.api.Assertions.assertThat; +import static java.util.concurrent.TimeUnit.MILLISECONDS; import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.logs.Severity; +import io.opentelemetry.api.trace.SpanContext; import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; import io.opentelemetry.sdk.common.InstrumentationScopeInfo; import io.opentelemetry.sdk.logs.data.LogRecordData; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.opentelemetry.sdk.testing.assertj.AttributeAssertion; +import io.opentelemetry.semconv.ExceptionAttributes; +import io.opentelemetry.semconv.incubating.ThreadIncubatingAttributes; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; import java.util.stream.Stream; +import org.assertj.core.api.AssertAccess; import org.jboss.logmanager.Level; import org.jboss.logmanager.LogContext; import org.jboss.logmanager.Logger; @@ -108,6 +116,8 @@ private static void test( String expectedSeverityText) throws InterruptedException { + Instant start = Instant.now(); + // when if (withParent) { testing.runWithSpan( @@ -122,35 +132,45 @@ private static void test( } if (expectedSeverity != null) { - LogRecordData log = testing.waitForLogRecords(1).get(0); - assertThat(log) - .hasBody(withParam ? "xyz: 123" : "xyz") - .hasInstrumentationScope(InstrumentationScopeInfo.builder(expectedLoggerName).build()) - .hasSeverity(expectedSeverity) - .hasSeverityText(expectedSeverityText); - if (logException) { - assertThat(log) - .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.THREAD_NAME, Thread.currentThread().getName()), - equalTo(SemanticAttributes.THREAD_ID, Thread.currentThread().getId()), - equalTo(SemanticAttributes.EXCEPTION_TYPE, IllegalStateException.class.getName()), - equalTo(SemanticAttributes.EXCEPTION_MESSAGE, "hello"), - satisfies( - SemanticAttributes.EXCEPTION_STACKTRACE, - v -> v.contains(JbossLogmanagerTest.class.getName()))); - } else { - assertThat(log) - .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.THREAD_NAME, Thread.currentThread().getName()), - equalTo(SemanticAttributes.THREAD_ID, Thread.currentThread().getId())); - } - - if (withParent) { - assertThat(log).hasSpanContext(testing.spans().get(0).getSpanContext()); - } else { - assertThat(log.getSpanContext().isValid()).isFalse(); - } - + testing.waitAndAssertLogRecords( + logRecord -> { + logRecord + .hasBody(withParam ? "xyz: 123" : "xyz") + .hasInstrumentationScope( + InstrumentationScopeInfo.builder(expectedLoggerName).build()) + .hasSeverity(expectedSeverity) + .hasSeverityText(expectedSeverityText) + .hasSpanContext( + withParent + ? testing.spans().get(0).getSpanContext() + : SpanContext.getInvalid()); + + List attributeAsserts = + new ArrayList<>( + Arrays.asList( + equalTo( + ThreadIncubatingAttributes.THREAD_NAME, + Thread.currentThread().getName()), + equalTo( + ThreadIncubatingAttributes.THREAD_ID, Thread.currentThread().getId()))); + if (logException) { + attributeAsserts.addAll( + Arrays.asList( + equalTo( + ExceptionAttributes.EXCEPTION_TYPE, + IllegalStateException.class.getName()), + equalTo(ExceptionAttributes.EXCEPTION_MESSAGE, "hello"), + satisfies( + ExceptionAttributes.EXCEPTION_STACKTRACE, + v -> v.contains(JbossLogmanagerTest.class.getName())))); + } + logRecord.hasAttributesSatisfyingExactly(attributeAsserts); + + LogRecordData logRecordData = AssertAccess.getActual(logRecord); + assertThat(logRecordData.getTimestampEpochNanos()) + .isGreaterThanOrEqualTo(MILLISECONDS.toNanos(start.toEpochMilli())) + .isLessThanOrEqualTo(MILLISECONDS.toNanos(Instant.now().toEpochMilli())); + }); } else { Thread.sleep(500); // sleep a bit just to make sure no log is captured assertThat(testing.logRecords()).isEmpty(); @@ -189,17 +209,19 @@ void testMdc() { MDC.remove("key2"); } - LogRecordData log = testing.waitForLogRecords(1).get(0); - assertThat(log) - .hasBody("xyz") - .hasInstrumentationScope(InstrumentationScopeInfo.builder("abc").build()) - .hasSeverity(Severity.INFO) - .hasSeverityText("INFO") - .hasAttributesSatisfyingExactly( - equalTo(AttributeKey.stringKey("jboss-logmanager.mdc.key1"), "val1"), - equalTo(AttributeKey.stringKey("jboss-logmanager.mdc.key2"), "val2"), - equalTo(SemanticAttributes.THREAD_NAME, Thread.currentThread().getName()), - equalTo(SemanticAttributes.THREAD_ID, Thread.currentThread().getId())); + testing.waitAndAssertLogRecords( + logRecord -> + logRecord + .hasBody("xyz") + .hasInstrumentationScope(InstrumentationScopeInfo.builder("abc").build()) + .hasSeverity(Severity.INFO) + .hasSeverityText("INFO") + .hasAttributesSatisfyingExactly( + equalTo(AttributeKey.stringKey("key1"), "val1"), + equalTo(AttributeKey.stringKey("key2"), "val2"), + equalTo( + ThreadIncubatingAttributes.THREAD_NAME, Thread.currentThread().getName()), + equalTo(ThreadIncubatingAttributes.THREAD_ID, Thread.currentThread().getId()))); } @FunctionalInterface diff --git a/instrumentation/jboss-logmanager/jboss-logmanager-mdc-1.1/javaagent/build.gradle.kts b/instrumentation/jboss-logmanager/jboss-logmanager-mdc-1.1/javaagent/build.gradle.kts index 41716f6e7dae..b32c1c497a70 100644 --- a/instrumentation/jboss-logmanager/jboss-logmanager-mdc-1.1/javaagent/build.gradle.kts +++ b/instrumentation/jboss-logmanager/jboss-logmanager-mdc-1.1/javaagent/build.gradle.kts @@ -14,3 +14,11 @@ muzzle { dependencies { library("org.jboss.logmanager:jboss-logmanager:1.1.0.GA") } + +val latestDepTest = findProperty("testLatestDeps") as Boolean + +if (latestDepTest) { + otelJava { + minJavaVersionSupported.set(JavaVersion.VERSION_11) + } +} diff --git a/instrumentation/jboss-logmanager/jboss-logmanager-mdc-1.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jbosslogmanager/mdc/v1_1/JbossExtLogRecordInstrumentation.java b/instrumentation/jboss-logmanager/jboss-logmanager-mdc-1.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jbosslogmanager/mdc/v1_1/JbossExtLogRecordInstrumentation.java index 61a9709ab9df..5fb6ce47fc0b 100644 --- a/instrumentation/jboss-logmanager/jboss-logmanager-mdc-1.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jbosslogmanager/mdc/v1_1/JbossExtLogRecordInstrumentation.java +++ b/instrumentation/jboss-logmanager/jboss-logmanager-mdc-1.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jbosslogmanager/mdc/v1_1/JbossExtLogRecordInstrumentation.java @@ -5,9 +5,6 @@ package io.opentelemetry.javaagent.instrumentation.jbosslogmanager.mdc.v1_1; -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; @@ -18,6 +15,7 @@ import io.opentelemetry.context.Context; import io.opentelemetry.instrumentation.api.util.VirtualField; import io.opentelemetry.javaagent.bootstrap.Java8BytecodeBridge; +import io.opentelemetry.javaagent.bootstrap.internal.AgentCommonConfig; import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; import java.util.Map; @@ -57,7 +55,9 @@ public static void onExit( @Advice.This ExtLogRecord record, @Advice.Argument(0) String key, @Advice.Return(readOnly = false) String value) { - if (TRACE_ID.equals(key) || SPAN_ID.equals(key) || TRACE_FLAGS.equals(key)) { + if (AgentCommonConfig.get().getTraceIdKey().equals(key) + || AgentCommonConfig.get().getSpanIdKey().equals(key) + || AgentCommonConfig.get().getTraceFlagsKey().equals(key)) { if (value != null) { // Assume already instrumented event if traceId/spanId/sampled is present. return; @@ -72,18 +72,14 @@ public static void onExit( return; } - 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 + if (AgentCommonConfig.get().getTraceIdKey().equals(key)) { + value = spanContext.getTraceId(); + } + if (AgentCommonConfig.get().getSpanIdKey().equals(key)) { + value = spanContext.getSpanId(); + } + if (AgentCommonConfig.get().getTraceFlagsKey().equals(key)) { + value = spanContext.getTraceFlags().asHex(); } } } @@ -97,9 +93,9 @@ public static void onExit( @Advice.This ExtLogRecord record, @Advice.Return(readOnly = false) Map value) { - if (value.containsKey(TRACE_ID) - && value.containsKey(SPAN_ID) - && value.containsKey(TRACE_FLAGS)) { + if (value.containsKey(AgentCommonConfig.get().getTraceIdKey()) + && value.containsKey(AgentCommonConfig.get().getSpanIdKey()) + && value.containsKey(AgentCommonConfig.get().getTraceFlagsKey())) { return; } @@ -113,16 +109,16 @@ public static void onExit( return; } - if (!value.containsKey(TRACE_ID)) { - value.put(TRACE_ID, spanContext.getTraceId()); + if (!value.containsKey(AgentCommonConfig.get().getTraceIdKey())) { + value.put(AgentCommonConfig.get().getTraceIdKey(), spanContext.getTraceId()); } - if (!value.containsKey(SPAN_ID)) { - value.put(SPAN_ID, spanContext.getSpanId()); + if (!value.containsKey(AgentCommonConfig.get().getSpanIdKey())) { + value.put(AgentCommonConfig.get().getSpanIdKey(), spanContext.getSpanId()); } - if (!value.containsKey(TRACE_FLAGS)) { - value.put(TRACE_FLAGS, spanContext.getTraceFlags().asHex()); + if (!value.containsKey(AgentCommonConfig.get().getTraceFlagsKey())) { + value.put(AgentCommonConfig.get().getTraceFlagsKey(), spanContext.getTraceFlags().asHex()); } } } diff --git a/instrumentation/jboss-logmanager/jboss-logmanager-mdc-1.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/jbosslogmanager/mdc/v1_1/JbossLogmanagerMdcTest.java b/instrumentation/jboss-logmanager/jboss-logmanager-mdc-1.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/jbosslogmanager/mdc/v1_1/JbossLogmanagerMdcTest.java index a6064ef96468..518ca25aa289 100644 --- a/instrumentation/jboss-logmanager/jboss-logmanager-mdc-1.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/jbosslogmanager/mdc/v1_1/JbossLogmanagerMdcTest.java +++ b/instrumentation/jboss-logmanager/jboss-logmanager-mdc-1.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/jbosslogmanager/mdc/v1_1/JbossLogmanagerMdcTest.java @@ -33,7 +33,7 @@ public class JbossLogmanagerMdcTest extends AgentInstrumentationSpecification { static class LogHandler extends Handler { public List logRecords; - LogHandler(LinkedList logRecords) { + LogHandler(List logRecords) { this.logRecords = logRecords; } diff --git a/instrumentation/jdbc/README.md b/instrumentation/jdbc/README.md new file mode 100644 index 000000000000..7e3440fd8c01 --- /dev/null +++ b/instrumentation/jdbc/README.md @@ -0,0 +1,5 @@ +# Settings for the JDBC instrumentation + +| System property | Type | Default | Description | +|---------------------------------------------------------|---------|---------|----------------------------------------| +| `otel.instrumentation.jdbc.statement-sanitizer.enabled` | Boolean | `true` | Enables the DB statement sanitization. | diff --git a/instrumentation/jdbc/bootstrap/build.gradle.kts b/instrumentation/jdbc/bootstrap/build.gradle.kts index 6fa135bb7283..eac175c2d0a7 100644 --- a/instrumentation/jdbc/bootstrap/build.gradle.kts +++ b/instrumentation/jdbc/bootstrap/build.gradle.kts @@ -3,7 +3,7 @@ plugins { } /* -JDDC instrumentation uses VirtualField. Add DbInfo, that is used as the value of +JDBC instrumentation uses VirtualField. Add DbInfo, that is used as the value of VirtualField, to boot loader. We do this because when JDBC instrumentation is started in multiple class loaders in the same hierarchy, each would define their own version of DbInfo. It is possible that the value read from virtual field would be from the wrong class loader and could produce a diff --git a/instrumentation/jdbc/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jdbc/JdbcIgnoredTypesConfigurer.java b/instrumentation/jdbc/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jdbc/JdbcIgnoredTypesConfigurer.java index f36ed7c24977..4301a3352588 100644 --- a/instrumentation/jdbc/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jdbc/JdbcIgnoredTypesConfigurer.java +++ b/instrumentation/jdbc/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jdbc/JdbcIgnoredTypesConfigurer.java @@ -19,5 +19,7 @@ public void configure(IgnoredTypesBuilder builder, ConfigProperties config) { builder.ignoreClass("org.jboss.jca.adapters.jdbc."); // see https://github.com/open-telemetry/opentelemetry-java-instrumentation/issues/8109 builder.ignoreClass("org.apache.shardingsphere.shardingjdbc.jdbc.core.statement."); + // see https://github.com/open-telemetry/opentelemetry-java-instrumentation/issues/12065 + builder.ignoreClass("org.apache.shardingsphere.driver.jdbc.core.statement."); } } diff --git a/instrumentation/jdbc/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jdbc/JdbcSingletons.java b/instrumentation/jdbc/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jdbc/JdbcSingletons.java index f86ead0fa9a5..afc92f2fcad9 100644 --- a/instrumentation/jdbc/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jdbc/JdbcSingletons.java +++ b/instrumentation/jdbc/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jdbc/JdbcSingletons.java @@ -5,32 +5,33 @@ package io.opentelemetry.javaagent.instrumentation.jdbc; -import static io.opentelemetry.instrumentation.jdbc.internal.DataSourceInstrumenterFactory.createDataSourceInstrumenter; +import static io.opentelemetry.instrumentation.jdbc.internal.JdbcInstrumenterFactory.createDataSourceInstrumenter; import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.instrumentation.api.incubator.semconv.db.DbClientSpanNameExtractor; +import io.opentelemetry.instrumentation.api.incubator.semconv.db.SqlClientAttributesExtractor; +import io.opentelemetry.instrumentation.api.incubator.semconv.net.PeerServiceAttributesExtractor; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; import io.opentelemetry.instrumentation.api.instrumenter.SpanKindExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.db.DbClientSpanNameExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.db.SqlClientAttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.net.NetClientAttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.net.PeerServiceAttributesExtractor; +import io.opentelemetry.instrumentation.api.semconv.network.ServerAttributesExtractor; import io.opentelemetry.instrumentation.jdbc.internal.DbRequest; import io.opentelemetry.instrumentation.jdbc.internal.JdbcAttributesGetter; -import io.opentelemetry.instrumentation.jdbc.internal.JdbcNetAttributesGetter; -import io.opentelemetry.javaagent.bootstrap.internal.CommonConfig; -import io.opentelemetry.javaagent.bootstrap.internal.InstrumentationConfig; +import io.opentelemetry.instrumentation.jdbc.internal.JdbcNetworkAttributesGetter; +import io.opentelemetry.javaagent.bootstrap.internal.AgentCommonConfig; +import io.opentelemetry.javaagent.bootstrap.internal.AgentInstrumentationConfig; +import io.opentelemetry.javaagent.bootstrap.jdbc.DbInfo; import javax.sql.DataSource; public final class JdbcSingletons { private static final String INSTRUMENTATION_NAME = "io.opentelemetry.jdbc"; private static final Instrumenter STATEMENT_INSTRUMENTER; - public static final Instrumenter DATASOURCE_INSTRUMENTER = - createDataSourceInstrumenter(GlobalOpenTelemetry.get()); + public static final Instrumenter DATASOURCE_INSTRUMENTER = + createDataSourceInstrumenter(GlobalOpenTelemetry.get(), true); static { JdbcAttributesGetter dbAttributesGetter = new JdbcAttributesGetter(); - JdbcNetAttributesGetter netAttributesGetter = new JdbcNetAttributesGetter(); + JdbcNetworkAttributesGetter netAttributesGetter = new JdbcNetworkAttributesGetter(); STATEMENT_INSTRUMENTER = Instrumenter.builder( @@ -40,15 +41,15 @@ public final class JdbcSingletons { .addAttributesExtractor( SqlClientAttributesExtractor.builder(dbAttributesGetter) .setStatementSanitizationEnabled( - InstrumentationConfig.get() + AgentInstrumentationConfig.get() .getBoolean( "otel.instrumentation.jdbc.statement-sanitizer.enabled", - CommonConfig.get().isStatementSanitizationEnabled())) + AgentCommonConfig.get().isStatementSanitizationEnabled())) .build()) - .addAttributesExtractor(NetClientAttributesExtractor.create(netAttributesGetter)) + .addAttributesExtractor(ServerAttributesExtractor.create(netAttributesGetter)) .addAttributesExtractor( PeerServiceAttributesExtractor.create( - netAttributesGetter, CommonConfig.get().getPeerServiceMapping())) + netAttributesGetter, AgentCommonConfig.get().getPeerServiceResolver())) .buildInstrumenter(SpanKindExtractor.alwaysClient()); } @@ -56,7 +57,7 @@ public static Instrumenter statementInstrumenter() { return STATEMENT_INSTRUMENTER; } - public static Instrumenter dataSourceInstrumenter() { + public static Instrumenter dataSourceInstrumenter() { return DATASOURCE_INSTRUMENTER; } diff --git a/instrumentation/jdbc/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jdbc/PreparedStatementInstrumentation.java b/instrumentation/jdbc/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jdbc/PreparedStatementInstrumentation.java index 799e85df6431..6b6034e067b6 100644 --- a/instrumentation/jdbc/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jdbc/PreparedStatementInstrumentation.java +++ b/instrumentation/jdbc/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jdbc/PreparedStatementInstrumentation.java @@ -17,6 +17,7 @@ import io.opentelemetry.context.Context; import io.opentelemetry.context.Scope; import io.opentelemetry.instrumentation.jdbc.internal.DbRequest; +import io.opentelemetry.instrumentation.jdbc.internal.JdbcData; import io.opentelemetry.javaagent.bootstrap.CallDepth; import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; @@ -55,6 +56,12 @@ public static void onEnter( @Advice.Local("otelRequest") DbRequest request, @Advice.Local("otelContext") Context context, @Advice.Local("otelScope") Scope scope) { + // skip prepared statements without attached sql, probably a wrapper around the actual + // prepared statement + if (JdbcData.preparedStatement.get(statement) == null) { + return; + } + // 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 @@ -85,7 +92,7 @@ public static void stopSpan( @Advice.Local("otelRequest") DbRequest request, @Advice.Local("otelContext") Context context, @Advice.Local("otelScope") Scope scope) { - if (callDepth.decrementAndGet() > 0) { + if (callDepth == null || callDepth.decrementAndGet() > 0) { return; } diff --git a/instrumentation/jdbc/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jdbc/datasource/DataSourceInstrumentation.java b/instrumentation/jdbc/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jdbc/datasource/DataSourceInstrumentation.java index 809e84c6a00a..009c01d0323a 100644 --- a/instrumentation/jdbc/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jdbc/datasource/DataSourceInstrumentation.java +++ b/instrumentation/jdbc/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jdbc/datasource/DataSourceInstrumentation.java @@ -8,12 +8,16 @@ import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.implementsInterface; import static io.opentelemetry.javaagent.instrumentation.jdbc.JdbcSingletons.dataSourceInstrumenter; import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.returns; import io.opentelemetry.context.Context; import io.opentelemetry.context.Scope; +import io.opentelemetry.instrumentation.jdbc.internal.JdbcUtils; import io.opentelemetry.javaagent.bootstrap.Java8BytecodeBridge; +import io.opentelemetry.javaagent.bootstrap.jdbc.DbInfo; import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import java.sql.Connection; import javax.sql.DataSource; import net.bytebuddy.asm.Advice; import net.bytebuddy.description.type.TypeDescription; @@ -29,7 +33,8 @@ public ElementMatcher typeMatcher() { @Override public void transform(TypeTransformer transformer) { transformer.applyAdviceToMethod( - named("getConnection"), DataSourceInstrumentation.class.getName() + "$GetConnectionAdvice"); + named("getConnection").and(returns(named("java.sql.Connection"))), + DataSourceInstrumentation.class.getName() + "$GetConnectionAdvice"); } @SuppressWarnings("unused") @@ -47,21 +52,29 @@ public static void start( return; } - context = dataSourceInstrumenter().start(parentContext, ds); - scope = context.makeCurrent(); + if (dataSourceInstrumenter().shouldStart(parentContext, ds)) { + context = dataSourceInstrumenter().start(parentContext, ds); + scope = context.makeCurrent(); + } } @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) public static void stopSpan( @Advice.This DataSource ds, + @Advice.Return Connection connection, + @Advice.Thrown Throwable throwable, @Advice.Local("otelContext") Context context, - @Advice.Local("otelScope") Scope scope, - @Advice.Thrown Throwable throwable) { + @Advice.Local("otelScope") Scope scope) { if (scope == null) { return; } scope.close(); - dataSourceInstrumenter().end(context, ds, null, throwable); + DbInfo dbInfo = null; + Connection realConnection = JdbcUtils.unwrapConnection(connection); + if (realConnection != null) { + dbInfo = JdbcUtils.extractDbInfo(realConnection); + } + dataSourceInstrumenter().end(context, ds, dbInfo, throwable); } } } diff --git a/instrumentation/jdbc/javaagent/src/test/groovy/JdbcInstrumentationTest.groovy b/instrumentation/jdbc/javaagent/src/test/groovy/JdbcInstrumentationTest.groovy index 73ba8cffab6e..39eec0059569 100644 --- a/instrumentation/jdbc/javaagent/src/test/groovy/JdbcInstrumentationTest.groovy +++ b/instrumentation/jdbc/javaagent/src/test/groovy/JdbcInstrumentationTest.groovy @@ -11,7 +11,9 @@ import io.opentelemetry.instrumentation.jdbc.TestConnection import io.opentelemetry.instrumentation.jdbc.TestDriver import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification import io.opentelemetry.javaagent.instrumentation.jdbc.test.ProxyStatementFactory -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import io.opentelemetry.semconv.incubating.CodeIncubatingAttributes +import io.opentelemetry.semconv.incubating.DbIncubatingAttributes +import io.opentelemetry.semconv.ServerAttributes import org.apache.derby.jdbc.EmbeddedDataSource import org.apache.derby.jdbc.EmbeddedDriver import org.h2.Driver @@ -165,6 +167,7 @@ class JdbcInstrumentationTest extends AgentInstrumentationSpecification { } } + @SuppressWarnings("deprecation") // TODO DbIncubatingAttributes.DB_CONNECTION_STRING deprecation def "basic statement with #connection.getClass().getCanonicalName() on #system generates spans"() { setup: Statement statement = connection.createStatement() @@ -187,15 +190,15 @@ class JdbcInstrumentationTest extends AgentInstrumentationSpecification { kind CLIENT childOf span(0) attributes { - "$SemanticAttributes.DB_SYSTEM" system - "$SemanticAttributes.DB_NAME" dbNameLower + "$DbIncubatingAttributes.DB_SYSTEM" system + "$DbIncubatingAttributes.DB_NAME" dbNameLower if (username != null) { - "$SemanticAttributes.DB_USER" username + "$DbIncubatingAttributes.DB_USER" username } - "$SemanticAttributes.DB_CONNECTION_STRING" url - "$SemanticAttributes.DB_STATEMENT" sanitizedQuery - "$SemanticAttributes.DB_OPERATION" "SELECT" - "$SemanticAttributes.DB_SQL_TABLE" table + "$DbIncubatingAttributes.DB_CONNECTION_STRING" url + "$DbIncubatingAttributes.DB_STATEMENT" sanitizedQuery + "$DbIncubatingAttributes.DB_OPERATION" "SELECT" + "$DbIncubatingAttributes.DB_SQL_TABLE" table } } } @@ -247,15 +250,15 @@ class JdbcInstrumentationTest extends AgentInstrumentationSpecification { kind CLIENT childOf span(0) attributes { - "$SemanticAttributes.DB_SYSTEM" system - "$SemanticAttributes.DB_NAME" dbNameLower + "$DbIncubatingAttributes.DB_SYSTEM" system + "$DbIncubatingAttributes.DB_NAME" dbNameLower if (username != null) { - "$SemanticAttributes.DB_USER" username + "$DbIncubatingAttributes.DB_USER" username } - "$SemanticAttributes.DB_CONNECTION_STRING" url - "$SemanticAttributes.DB_STATEMENT" sanitizedQuery - "$SemanticAttributes.DB_OPERATION" "SELECT" - "$SemanticAttributes.DB_SQL_TABLE" table + "$DbIncubatingAttributes.DB_CONNECTION_STRING" url + "$DbIncubatingAttributes.DB_STATEMENT" sanitizedQuery + "$DbIncubatingAttributes.DB_OPERATION" "SELECT" + "$DbIncubatingAttributes.DB_SQL_TABLE" table } } } @@ -299,15 +302,15 @@ class JdbcInstrumentationTest extends AgentInstrumentationSpecification { kind CLIENT childOf span(0) attributes { - "$SemanticAttributes.DB_SYSTEM" system - "$SemanticAttributes.DB_NAME" dbNameLower + "$DbIncubatingAttributes.DB_SYSTEM" system + "$DbIncubatingAttributes.DB_NAME" dbNameLower if (username != null) { - "$SemanticAttributes.DB_USER" username + "$DbIncubatingAttributes.DB_USER" username } - "$SemanticAttributes.DB_CONNECTION_STRING" url - "$SemanticAttributes.DB_STATEMENT" sanitizedQuery - "$SemanticAttributes.DB_OPERATION" "SELECT" - "$SemanticAttributes.DB_SQL_TABLE" table + "$DbIncubatingAttributes.DB_CONNECTION_STRING" url + "$DbIncubatingAttributes.DB_STATEMENT" sanitizedQuery + "$DbIncubatingAttributes.DB_OPERATION" "SELECT" + "$DbIncubatingAttributes.DB_SQL_TABLE" table } } } @@ -351,15 +354,15 @@ class JdbcInstrumentationTest extends AgentInstrumentationSpecification { kind CLIENT childOf span(0) attributes { - "$SemanticAttributes.DB_SYSTEM" system - "$SemanticAttributes.DB_NAME" dbName.toLowerCase() + "$DbIncubatingAttributes.DB_SYSTEM" system + "$DbIncubatingAttributes.DB_NAME" dbName.toLowerCase() if (username != null) { - "$SemanticAttributes.DB_USER" username + "$DbIncubatingAttributes.DB_USER" username } - "$SemanticAttributes.DB_CONNECTION_STRING" url - "$SemanticAttributes.DB_STATEMENT" sanitizedQuery - "$SemanticAttributes.DB_OPERATION" "SELECT" - "$SemanticAttributes.DB_SQL_TABLE" table + "$DbIncubatingAttributes.DB_CONNECTION_STRING" url + "$DbIncubatingAttributes.DB_STATEMENT" sanitizedQuery + "$DbIncubatingAttributes.DB_OPERATION" "SELECT" + "$DbIncubatingAttributes.DB_SQL_TABLE" table } } } @@ -399,17 +402,19 @@ class JdbcInstrumentationTest extends AgentInstrumentationSpecification { hasNoParent() } span(1) { - name dbNameLower + name spanName kind CLIENT childOf span(0) attributes { - "$SemanticAttributes.DB_SYSTEM" system - "$SemanticAttributes.DB_NAME" dbNameLower + "$DbIncubatingAttributes.DB_SYSTEM" system + "$DbIncubatingAttributes.DB_NAME" dbNameLower if (username != null) { - "$SemanticAttributes.DB_USER" username + "$DbIncubatingAttributes.DB_USER" username } - "$SemanticAttributes.DB_STATEMENT" query - "$SemanticAttributes.DB_CONNECTION_STRING" url + "$DbIncubatingAttributes.DB_STATEMENT" query + "$DbIncubatingAttributes.DB_CONNECTION_STRING" url + "$DbIncubatingAttributes.DB_OPERATION" "CREATE TABLE" + "$DbIncubatingAttributes.DB_SQL_TABLE" table } } } @@ -420,19 +425,19 @@ class JdbcInstrumentationTest extends AgentInstrumentationSpecification { 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:" + system | connection | username | query | spanName | url | table + "h2" | new Driver().connect(jdbcUrls.get("h2"), null) | null | "CREATE TABLE S_H2 (id INTEGER not NULL, PRIMARY KEY ( id ))" | "CREATE TABLE jdbcunittest.S_H2" | "h2:mem:" | "S_H2" + "derby" | new EmbeddedDriver().connect(jdbcUrls.get("derby"), null) | "APP" | "CREATE TABLE S_DERBY (id INTEGER not NULL, PRIMARY KEY ( id ))" | "CREATE TABLE jdbcunittest.S_DERBY" | "derby:memory:" | "S_DERBY" + "hsqldb" | new JDBCDriver().connect(jdbcUrls.get("hsqldb"), null) | "SA" | "CREATE TABLE PUBLIC.S_HSQLDB (id INTEGER not NULL, PRIMARY KEY ( id ))" | "CREATE TABLE PUBLIC.S_HSQLDB" | "hsqldb:mem:" | "PUBLIC.S_HSQLDB" + "h2" | cpDatasources.get("tomcat").get("h2").getConnection() | null | "CREATE TABLE S_H2_TOMCAT (id INTEGER not NULL, PRIMARY KEY ( id ))" | "CREATE TABLE jdbcunittest.S_H2_TOMCAT" | "h2:mem:" | "S_H2_TOMCAT" + "derby" | cpDatasources.get("tomcat").get("derby").getConnection() | "APP" | "CREATE TABLE S_DERBY_TOMCAT (id INTEGER not NULL, PRIMARY KEY ( id ))" | "CREATE TABLE jdbcunittest.S_DERBY_TOMCAT" | "derby:memory:" | "S_DERBY_TOMCAT" + "hsqldb" | cpDatasources.get("tomcat").get("hsqldb").getConnection() | "SA" | "CREATE TABLE PUBLIC.S_HSQLDB_TOMCAT (id INTEGER not NULL, PRIMARY KEY ( id ))" | "CREATE TABLE PUBLIC.S_HSQLDB_TOMCAT" | "hsqldb:mem:" | "PUBLIC.S_HSQLDB_TOMCAT" + "h2" | cpDatasources.get("hikari").get("h2").getConnection() | null | "CREATE TABLE S_H2_HIKARI (id INTEGER not NULL, PRIMARY KEY ( id ))" | "CREATE TABLE jdbcunittest.S_H2_HIKARI" | "h2:mem:" | "S_H2_HIKARI" + "derby" | cpDatasources.get("hikari").get("derby").getConnection() | "APP" | "CREATE TABLE S_DERBY_HIKARI (id INTEGER not NULL, PRIMARY KEY ( id ))" | "CREATE TABLE jdbcunittest.S_DERBY_HIKARI" | "derby:memory:" | "S_DERBY_HIKARI" + "hsqldb" | cpDatasources.get("hikari").get("hsqldb").getConnection() | "SA" | "CREATE TABLE PUBLIC.S_HSQLDB_HIKARI (id INTEGER not NULL, PRIMARY KEY ( id ))" | "CREATE TABLE PUBLIC.S_HSQLDB_HIKARI" | "hsqldb:mem:" | "PUBLIC.S_HSQLDB_HIKARI" + "h2" | cpDatasources.get("c3p0").get("h2").getConnection() | null | "CREATE TABLE S_H2_C3P0 (id INTEGER not NULL, PRIMARY KEY ( id ))" | "CREATE TABLE jdbcunittest.S_H2_C3P0" | "h2:mem:" | "S_H2_C3P0" + "derby" | cpDatasources.get("c3p0").get("derby").getConnection() | "APP" | "CREATE TABLE S_DERBY_C3P0 (id INTEGER not NULL, PRIMARY KEY ( id ))" | "CREATE TABLE jdbcunittest.S_DERBY_C3P0" | "derby:memory:" | "S_DERBY_C3P0" + "hsqldb" | cpDatasources.get("c3p0").get("hsqldb").getConnection() | "SA" | "CREATE TABLE PUBLIC.S_HSQLDB_C3P0 (id INTEGER not NULL, PRIMARY KEY ( id ))" | "CREATE TABLE PUBLIC.S_HSQLDB_C3P0" | "hsqldb:mem:" | "PUBLIC.S_HSQLDB_C3P0" } def "prepared statement update on #system with #connection.getClass().getCanonicalName() generates a span"() { @@ -452,17 +457,19 @@ class JdbcInstrumentationTest extends AgentInstrumentationSpecification { hasNoParent() } span(1) { - name dbNameLower + name spanName kind CLIENT childOf span(0) attributes { - "$SemanticAttributes.DB_SYSTEM" system - "$SemanticAttributes.DB_NAME" dbName.toLowerCase() + "$DbIncubatingAttributes.DB_SYSTEM" system + "$DbIncubatingAttributes.DB_NAME" dbName.toLowerCase() if (username != null) { - "$SemanticAttributes.DB_USER" username + "$DbIncubatingAttributes.DB_USER" username } - "$SemanticAttributes.DB_STATEMENT" query - "$SemanticAttributes.DB_CONNECTION_STRING" url + "$DbIncubatingAttributes.DB_STATEMENT" query + "$DbIncubatingAttributes.DB_CONNECTION_STRING" url + "$DbIncubatingAttributes.DB_OPERATION" "CREATE TABLE" + "$DbIncubatingAttributes.DB_SQL_TABLE" table } } } @@ -473,15 +480,15 @@ class JdbcInstrumentationTest extends AgentInstrumentationSpecification { 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:" + system | connection | username | query | spanName | url | table + "h2" | new Driver().connect(jdbcUrls.get("h2"), null) | null | "CREATE TABLE PS_H2 (id INTEGER not NULL, PRIMARY KEY ( id ))" | "CREATE TABLE jdbcunittest.PS_H2" | "h2:mem:" | "PS_H2" + "derby" | new EmbeddedDriver().connect(jdbcUrls.get("derby"), null) | "APP" | "CREATE TABLE PS_DERBY (id INTEGER not NULL, PRIMARY KEY ( id ))" | "CREATE TABLE jdbcunittest.PS_DERBY" | "derby:memory:" | "PS_DERBY" + "h2" | cpDatasources.get("tomcat").get("h2").getConnection() | null | "CREATE TABLE PS_H2_TOMCAT (id INTEGER not NULL, PRIMARY KEY ( id ))" | "CREATE TABLE jdbcunittest.PS_H2_TOMCAT" | "h2:mem:" | "PS_H2_TOMCAT" + "derby" | cpDatasources.get("tomcat").get("derby").getConnection() | "APP" | "CREATE TABLE PS_DERBY_TOMCAT (id INTEGER not NULL, PRIMARY KEY ( id ))" | "CREATE TABLE jdbcunittest.PS_DERBY_TOMCAT" | "derby:memory:" | "PS_DERBY_TOMCAT" + "h2" | cpDatasources.get("hikari").get("h2").getConnection() | null | "CREATE TABLE PS_H2_HIKARI (id INTEGER not NULL, PRIMARY KEY ( id ))" | "CREATE TABLE jdbcunittest.PS_H2_HIKARI" | "h2:mem:" | "PS_H2_HIKARI" + "derby" | cpDatasources.get("hikari").get("derby").getConnection() | "APP" | "CREATE TABLE PS_DERBY_HIKARI (id INTEGER not NULL, PRIMARY KEY ( id ))" | "CREATE TABLE jdbcunittest.PS_DERBY_HIKARI" | "derby:memory:" | "PS_DERBY_HIKARI" + "h2" | cpDatasources.get("c3p0").get("h2").getConnection() | null | "CREATE TABLE PS_H2_C3P0 (id INTEGER not NULL, PRIMARY KEY ( id ))" | "CREATE TABLE jdbcunittest.PS_H2_C3P0" | "h2:mem:" | "PS_H2_C3P0" + "derby" | cpDatasources.get("c3p0").get("derby").getConnection() | "APP" | "CREATE TABLE PS_DERBY_C3P0 (id INTEGER not NULL, PRIMARY KEY ( id ))" | "CREATE TABLE jdbcunittest.PS_DERBY_C3P0" | "derby:memory:" | "PS_DERBY_C3P0" } def "connection constructor throwing then generating correct spans after recovery using #driver connection (prepare statement = #prepareStatement)"() { @@ -521,15 +528,15 @@ class JdbcInstrumentationTest extends AgentInstrumentationSpecification { kind CLIENT childOf span(0) attributes { - "$SemanticAttributes.DB_SYSTEM" system - "$SemanticAttributes.DB_NAME" dbNameLower + "$DbIncubatingAttributes.DB_SYSTEM" system + "$DbIncubatingAttributes.DB_NAME" dbNameLower if (username != null) { - "$SemanticAttributes.DB_USER" username + "$DbIncubatingAttributes.DB_USER" username } - "$SemanticAttributes.DB_CONNECTION_STRING" url - "$SemanticAttributes.DB_STATEMENT" sanitizedQuery - "$SemanticAttributes.DB_OPERATION" "SELECT" - "$SemanticAttributes.DB_SQL_TABLE" table + "$DbIncubatingAttributes.DB_CONNECTION_STRING" url + "$DbIncubatingAttributes.DB_STATEMENT" sanitizedQuery + "$DbIncubatingAttributes.DB_OPERATION" "SELECT" + "$DbIncubatingAttributes.DB_SQL_TABLE" table } } } @@ -578,8 +585,12 @@ class JdbcInstrumentationTest extends AgentInstrumentationSpecification { kind INTERNAL childOf span(0) attributes { - "$SemanticAttributes.CODE_NAMESPACE" datasource.class.name - "$SemanticAttributes.CODE_FUNCTION" "getConnection" + "$CodeIncubatingAttributes.CODE_NAMESPACE" datasource.class.name + "$CodeIncubatingAttributes.CODE_FUNCTION" "getConnection" + "$DbIncubatingAttributes.DB_SYSTEM" system + "$DbIncubatingAttributes.DB_USER" { user == null | user == it } + "$DbIncubatingAttributes.DB_NAME" "jdbcunittest" + "$DbIncubatingAttributes.DB_CONNECTION_STRING" connectionString } } if (recursive) { @@ -588,8 +599,12 @@ class JdbcInstrumentationTest extends AgentInstrumentationSpecification { kind INTERNAL childOf span(1) attributes { - "$SemanticAttributes.CODE_NAMESPACE" datasource.class.name - "$SemanticAttributes.CODE_FUNCTION" "getConnection" + "$CodeIncubatingAttributes.CODE_NAMESPACE" datasource.class.name + "$CodeIncubatingAttributes.CODE_FUNCTION" "getConnection" + "$DbIncubatingAttributes.DB_SYSTEM" system + "$DbIncubatingAttributes.DB_USER" { user == null | user == it } + "$DbIncubatingAttributes.DB_NAME" "jdbcunittest" + "$DbIncubatingAttributes.DB_CONNECTION_STRING" connectionString } } } @@ -597,13 +612,13 @@ class JdbcInstrumentationTest extends AgentInstrumentationSpecification { } 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 + datasource | init | system | user | connectionString + new JdbcDataSource() | { ds -> ds.setURL(jdbcUrls.get("h2")) } | "h2" | null | "h2:mem:" + new EmbeddedDataSource() | { ds -> ds.jdbcurl = jdbcUrls.get("derby") } | "derby" | "APP" | "derby:memory:" + cpDatasources.get("hikari").get("h2") | null | "h2" | null | "h2:mem:" + cpDatasources.get("hikari").get("derby") | null | "derby" | "APP" | "derby:memory:" + cpDatasources.get("c3p0").get("h2") | null | "h2" | null | "h2:mem:" + cpDatasources.get("c3p0").get("derby") | null | "derby" | "APP" | "derby:memory:" // Tomcat's pool doesn't work because the getConnection method is // implemented in a parent class that doesn't implement DataSource @@ -636,10 +651,10 @@ class JdbcInstrumentationTest extends AgentInstrumentationSpecification { kind CLIENT childOf span(0) attributes { - "$SemanticAttributes.DB_SYSTEM" "other_sql" - "$SemanticAttributes.DB_STATEMENT" "testing ?" - "$SemanticAttributes.DB_CONNECTION_STRING" "testdb://localhost" - "$SemanticAttributes.NET_PEER_NAME" "localhost" + "$DbIncubatingAttributes.DB_SYSTEM" "other_sql" + "$DbIncubatingAttributes.DB_STATEMENT" "testing ?" + "$DbIncubatingAttributes.DB_CONNECTION_STRING" "testdb://localhost" + "$ServerAttributes.SERVER_ADDRESS" "localhost" } } } @@ -677,25 +692,25 @@ class JdbcInstrumentationTest extends AgentInstrumentationSpecification { kind CLIENT childOf span(0) attributes { - "$SemanticAttributes.DB_SYSTEM" "other_sql" - "$SemanticAttributes.DB_NAME" databaseName - "$SemanticAttributes.DB_CONNECTION_STRING" "testdb://localhost" - "$SemanticAttributes.DB_STATEMENT" sanitizedQuery - "$SemanticAttributes.DB_OPERATION" operation - "$SemanticAttributes.DB_SQL_TABLE" table - "$SemanticAttributes.NET_PEER_NAME" "localhost" + "$DbIncubatingAttributes.DB_SYSTEM" "other_sql" + "$DbIncubatingAttributes.DB_NAME" databaseName + "$DbIncubatingAttributes.DB_CONNECTION_STRING" "testdb://localhost" + "$DbIncubatingAttributes.DB_STATEMENT" sanitizedQuery + "$DbIncubatingAttributes.DB_OPERATION" operation + "$DbIncubatingAttributes.DB_SQL_TABLE" table + "$ServerAttributes.SERVER_ADDRESS" "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 + 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" | "CREATE TABLE test.table" | "test" | "CREATE TABLE" | "table" + "jdbc:testdb://localhost" | "CREATE TABLE table" | "CREATE TABLE table" | "CREATE TABLE table" | null | "CREATE TABLE" | "table" } def "#connectionPoolName connections should be cached in case of wrapped connections"() { @@ -734,13 +749,13 @@ class JdbcInstrumentationTest extends AgentInstrumentationSpecification { name "SELECT INFORMATION_SCHEMA.SYSTEM_USERS" kind CLIENT attributes { - "$SemanticAttributes.DB_SYSTEM" "hsqldb" - "$SemanticAttributes.DB_NAME" dbNameLower - "$SemanticAttributes.DB_USER" "SA" - "$SemanticAttributes.DB_CONNECTION_STRING" "hsqldb:mem:" - "$SemanticAttributes.DB_STATEMENT" "SELECT ? FROM INFORMATION_SCHEMA.SYSTEM_USERS" - "$SemanticAttributes.DB_OPERATION" "SELECT" - "$SemanticAttributes.DB_SQL_TABLE" "INFORMATION_SCHEMA.SYSTEM_USERS" + "$DbIncubatingAttributes.DB_SYSTEM" "hsqldb" + "$DbIncubatingAttributes.DB_NAME" dbNameLower + "$DbIncubatingAttributes.DB_USER" "SA" + "$DbIncubatingAttributes.DB_CONNECTION_STRING" "hsqldb:mem:" + "$DbIncubatingAttributes.DB_STATEMENT" "SELECT ? FROM INFORMATION_SCHEMA.SYSTEM_USERS" + "$DbIncubatingAttributes.DB_OPERATION" "SELECT" + "$DbIncubatingAttributes.DB_SQL_TABLE" "INFORMATION_SCHEMA.SYSTEM_USERS" } } } @@ -783,12 +798,12 @@ class JdbcInstrumentationTest extends AgentInstrumentationSpecification { kind CLIENT childOf span(0) attributes { - "$SemanticAttributes.DB_SYSTEM" "other_sql" - "$SemanticAttributes.DB_CONNECTION_STRING" "testdb://localhost" - "$SemanticAttributes.DB_STATEMENT" "SELECT * FROM table" - "$SemanticAttributes.DB_OPERATION" "SELECT" - "$SemanticAttributes.DB_SQL_TABLE" "table" - "$SemanticAttributes.NET_PEER_NAME" "localhost" + "$DbIncubatingAttributes.DB_SYSTEM" "other_sql" + "$DbIncubatingAttributes.DB_CONNECTION_STRING" "testdb://localhost" + "$DbIncubatingAttributes.DB_STATEMENT" "SELECT * FROM table" + "$DbIncubatingAttributes.DB_OPERATION" "SELECT" + "$DbIncubatingAttributes.DB_SQL_TABLE" "table" + "$ServerAttributes.SERVER_ADDRESS" "localhost" } } } @@ -853,4 +868,36 @@ class JdbcInstrumentationTest extends AgentInstrumentationSpecification { statement.close() connection.close() } + + // regression test for https://github.com/open-telemetry/opentelemetry-java-instrumentation/issues/9359 + def "test proxy prepared statement"() { + def connection = new Driver().connect(jdbcUrls.get("h2"), null) + PreparedStatement statement = connection.prepareStatement("SELECT 3") + PreparedStatement proxyStatement = ProxyStatementFactory.proxyPreparedStatement(statement) + ResultSet resultSet = runWithSpan("parent") { + return proxyStatement.executeQuery() + } + + expect: + resultSet.next() + resultSet.getInt(1) == 3 + assertTraces(1) { + trace(0, 2) { + span(0) { + name "parent" + kind SpanKind.INTERNAL + hasNoParent() + } + span(1) { + name "SELECT $dbNameLower" + kind CLIENT + childOf span(0) + } + } + } + + cleanup: + statement.close() + connection.close() + } } diff --git a/instrumentation/jdbc/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/jdbc/test/ProxyStatementFactory.java b/instrumentation/jdbc/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/jdbc/test/ProxyStatementFactory.java index 95f139ddd52a..f44b26336d9a 100644 --- a/instrumentation/jdbc/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/jdbc/test/ProxyStatementFactory.java +++ b/instrumentation/jdbc/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/jdbc/test/ProxyStatementFactory.java @@ -7,6 +7,7 @@ import java.lang.reflect.InvocationHandler; import java.lang.reflect.Proxy; +import java.sql.PreparedStatement; import java.sql.Statement; public final class ProxyStatementFactory { @@ -34,5 +35,28 @@ public static Statement proxyStatement(Statement statement) throws Exception { return proxyStatement; } + public static PreparedStatement proxyPreparedStatement(PreparedStatement statement) { + InvocationHandler invocationHandler = (proxy, method, args) -> method.invoke(statement, args); + PreparedStatement proxyStatement = + (PreparedStatement) + Proxy.newProxyInstance( + ProxyStatementFactory.class.getClassLoader(), + new Class[] {PreparedStatement.class, TestInterface.class}, + invocationHandler); + + // adding package private interface TestInterface to jdk proxy forces defining the proxy class + // in the same package as the package private interface + // by default we ignore jdk proxies, having the proxy in a different package ensures it gets + // instrumented + if (!proxyStatement + .getClass() + .getName() + .startsWith("io.opentelemetry.javaagent.instrumentation.jdbc.test")) { + throw new IllegalStateException("proxy statement is in wrong package"); + } + + return proxyStatement; + } + private ProxyStatementFactory() {} } diff --git a/instrumentation/jdbc/javaagent/src/test/scala/io/opentelemetry/javaagent/instrumentation/scalaexecutors/SlickTest.scala b/instrumentation/jdbc/javaagent/src/test/scala/io/opentelemetry/javaagent/instrumentation/scalaexecutors/SlickTest.scala index 7ce6aad48494..dc1ff375acc6 100644 --- a/instrumentation/jdbc/javaagent/src/test/scala/io/opentelemetry/javaagent/instrumentation/scalaexecutors/SlickTest.scala +++ b/instrumentation/jdbc/javaagent/src/test/scala/io/opentelemetry/javaagent/instrumentation/scalaexecutors/SlickTest.scala @@ -13,8 +13,8 @@ import io.opentelemetry.instrumentation.testing.junit.{ import io.opentelemetry.javaagent.testing.common.Java8BytecodeBridge import io.opentelemetry.sdk.testing.assertj.{SpanDataAssert, TraceAssert} import io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes.DbSystemValues +import io.opentelemetry.semconv.incubating.DbIncubatingAttributes +import io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DbSystemValues import java.util.function.Consumer import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.{Test, TestInstance} @@ -79,12 +79,18 @@ class SlickTest { .hasKind(SpanKind.CLIENT) .hasParent(trace.getSpan(0)) .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.DB_SYSTEM, DbSystemValues.H2), - equalTo(SemanticAttributes.DB_NAME, Db), - equalTo(SemanticAttributes.DB_USER, Username), - equalTo(SemanticAttributes.DB_CONNECTION_STRING, "h2:mem:"), - equalTo(SemanticAttributes.DB_STATEMENT, "SELECT ?"), - equalTo(SemanticAttributes.DB_OPERATION, "SELECT") + equalTo( + DbIncubatingAttributes.DB_SYSTEM, + DbSystemValues.H2 + ), + equalTo(DbIncubatingAttributes.DB_NAME, Db), + equalTo(DbIncubatingAttributes.DB_USER, Username), + equalTo( + DbIncubatingAttributes.DB_CONNECTION_STRING, + "h2:mem:" + ), + equalTo(DbIncubatingAttributes.DB_STATEMENT, "SELECT ?"), + equalTo(DbIncubatingAttributes.DB_OPERATION, "SELECT") ) } ) diff --git a/instrumentation/jdbc/library/README.md b/instrumentation/jdbc/library/README.md index 4b940e8b4ad1..2c21cfd2146e 100644 --- a/instrumentation/jdbc/library/README.md +++ b/instrumentation/jdbc/library/README.md @@ -57,7 +57,7 @@ public class DataSourceConfig { dataSource.setUrl("jdbc:postgresql://127.0.0.1:5432/example"); dataSource.setUsername("postgres"); dataSource.setPassword("root"); - return new OpenTelemetryDataSource(dataSource); + return JdbcTelemetry.create(openTelemetry).wrap(dataSource); } } @@ -68,3 +68,7 @@ public class DataSourceConfig { 1. Activate tracing for JDBC connections by setting `jdbc:otel:` prefix to the JDBC URL, e.g. `jdbc:otel:h2:mem:test`. 2. Set the driver class to `io.opentelemetry.instrumentation.jdbc.OpenTelemetryDriver`. + +3. Inject `OpenTelemetry` into `io.opentelemetry.instrumentation.jdbc.OpenTelemetryDriver` _before the initialization of the database connection pool_. +You can do this with the `void setOpenTelemetry(OpenTelemetry openTelemetry)` method of `io.opentelemetry.instrumentation.jdbc.OpenTelemetryDriver`. +Another way is to use `OpenTelemetryDriver.install(OpenTelemetry openTelemetry)`. diff --git a/instrumentation/jdbc/library/build.gradle.kts b/instrumentation/jdbc/library/build.gradle.kts index e5c6649cff54..02af72e4c9dc 100644 --- a/instrumentation/jdbc/library/build.gradle.kts +++ b/instrumentation/jdbc/library/build.gradle.kts @@ -4,7 +4,7 @@ */ plugins { - id("com.github.johnrengelman.shadow") + id("com.gradleup.shadow") id("otel.library-instrumentation") } diff --git a/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/OpenTelemetryDriver.java b/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/OpenTelemetryDriver.java index 5b6646ced2c6..8f0e32d63f93 100644 --- a/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/OpenTelemetryDriver.java +++ b/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/OpenTelemetryDriver.java @@ -21,10 +21,13 @@ package io.opentelemetry.instrumentation.jdbc; import static io.opentelemetry.instrumentation.jdbc.internal.JdbcInstrumenterFactory.INSTRUMENTATION_NAME; -import static io.opentelemetry.instrumentation.jdbc.internal.JdbcSingletons.statementInstrumenter; +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; import io.opentelemetry.instrumentation.api.internal.EmbeddedInstrumentationProperties; +import io.opentelemetry.instrumentation.jdbc.internal.DbRequest; import io.opentelemetry.instrumentation.jdbc.internal.JdbcConnectionUrlParser; +import io.opentelemetry.instrumentation.jdbc.internal.JdbcInstrumenterFactory; import io.opentelemetry.instrumentation.jdbc.internal.OpenTelemetryConnection; import io.opentelemetry.instrumentation.jdbc.internal.dbinfo.DbInfo; import java.sql.Connection; @@ -35,6 +38,7 @@ import java.sql.SQLFeatureNotSupportedException; import java.util.Collection; import java.util.Collections; +import java.util.Enumeration; import java.util.List; import java.util.Properties; import java.util.concurrent.CopyOnWriteArrayList; @@ -48,6 +52,8 @@ public final class OpenTelemetryDriver implements Driver { // visible for testing static final OpenTelemetryDriver INSTANCE = new OpenTelemetryDriver(); + private volatile OpenTelemetry openTelemetry = OpenTelemetry.noop(); + private static final int MAJOR_VERSION; private static final int MINOR_VERSION; @@ -192,6 +198,30 @@ private static int[] parseInstrumentationVersion() { return new int[] {0, 0}; } + /** + * Installs the {@link OpenTelemetry} instance on the {@code OpenTelemetryDriver}. OpenTelemetry + * has to be set before the initialization of the database connection pool. + */ + public static void install(OpenTelemetry openTelemetry) { + Enumeration drivers = DriverManager.getDrivers(); + while (drivers.hasMoreElements()) { + Driver driver = drivers.nextElement(); + if (driver instanceof io.opentelemetry.instrumentation.jdbc.OpenTelemetryDriver) { + OpenTelemetryDriver openTelemetryDriver = (OpenTelemetryDriver) driver; + openTelemetryDriver.setOpenTelemetry(openTelemetry); + } + } + } + + /** + * Configures the {@link OpenTelemetry}. See {@link #install(OpenTelemetry)} for simple + * installation option. OpenTelemetry has to be set before the initialization of the database + * connection pool. + */ + public void setOpenTelemetry(OpenTelemetry openTelemetry) { + this.openTelemetry = openTelemetry; + } + @Nullable @Override public Connection connect(String url, Properties info) throws SQLException { @@ -212,7 +242,9 @@ public Connection connect(String url, Properties info) throws SQLException { DbInfo dbInfo = JdbcConnectionUrlParser.parse(realUrl, info); - return new OpenTelemetryConnection(connection, dbInfo, statementInstrumenter()); + Instrumenter statementInstrumenter = + JdbcInstrumenterFactory.createStatementInstrumenter(openTelemetry); + return new OpenTelemetryConnection(connection, dbInfo, statementInstrumenter); } @Override diff --git a/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/datasource/JdbcTelemetry.java b/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/datasource/JdbcTelemetry.java new file mode 100644 index 000000000000..cc857b823563 --- /dev/null +++ b/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/datasource/JdbcTelemetry.java @@ -0,0 +1,41 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.jdbc.datasource; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import io.opentelemetry.instrumentation.jdbc.internal.DbRequest; +import io.opentelemetry.instrumentation.jdbc.internal.dbinfo.DbInfo; +import javax.sql.DataSource; + +/** Entrypoint for instrumenting a JDBC DataSources. */ +public final class JdbcTelemetry { + + /** Returns a new {@link JdbcTelemetry} configured with the given {@link OpenTelemetry}. */ + public static JdbcTelemetry create(OpenTelemetry openTelemetry) { + return builder(openTelemetry).build(); + } + + /** Returns a new {@link JdbcTelemetryBuilder} configured with the given {@link OpenTelemetry}. */ + public static JdbcTelemetryBuilder builder(OpenTelemetry openTelemetry) { + return new JdbcTelemetryBuilder(openTelemetry); + } + + private final Instrumenter dataSourceInstrumenter; + private final Instrumenter statementInstrumenter; + + JdbcTelemetry( + Instrumenter dataSourceInstrumenter, + Instrumenter statementInstrumenter) { + this.dataSourceInstrumenter = dataSourceInstrumenter; + this.statementInstrumenter = statementInstrumenter; + } + + public DataSource wrap(DataSource dataSource) { + return new OpenTelemetryDataSource( + dataSource, this.dataSourceInstrumenter, this.statementInstrumenter); + } +} diff --git a/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/datasource/JdbcTelemetryBuilder.java b/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/datasource/JdbcTelemetryBuilder.java new file mode 100644 index 000000000000..825b29547334 --- /dev/null +++ b/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/datasource/JdbcTelemetryBuilder.java @@ -0,0 +1,53 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.jdbc.datasource; + +import com.google.errorprone.annotations.CanIgnoreReturnValue; +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.instrumentation.jdbc.internal.JdbcInstrumenterFactory; + +/** A builder of {@link JdbcTelemetry}. */ +public final class JdbcTelemetryBuilder { + + private final OpenTelemetry openTelemetry; + private boolean dataSourceInstrumenterEnabled = true; + private boolean statementInstrumenterEnabled = true; + private boolean statementSanitizationEnabled = true; + + JdbcTelemetryBuilder(OpenTelemetry openTelemetry) { + this.openTelemetry = openTelemetry; + } + + /** Configures whether spans are created for JDBC Connections. Enabled by default. */ + @CanIgnoreReturnValue + public JdbcTelemetryBuilder setDataSourceInstrumenterEnabled(boolean enabled) { + this.dataSourceInstrumenterEnabled = enabled; + return this; + } + + /** Configures whether spans are created for JDBC Statements. Enabled by default. */ + @CanIgnoreReturnValue + public JdbcTelemetryBuilder setStatementInstrumenterEnabled(boolean enabled) { + this.statementInstrumenterEnabled = enabled; + return this; + } + + /** Configures whether JDBC Statements are sanitized. Enabled by default. */ + @CanIgnoreReturnValue + public JdbcTelemetryBuilder setStatementSanitizationEnabled(boolean enabled) { + this.statementSanitizationEnabled = enabled; + return this; + } + + /** Returns a new {@link JdbcTelemetry} with the settings of this {@link JdbcTelemetryBuilder}. */ + public JdbcTelemetry build() { + return new JdbcTelemetry( + JdbcInstrumenterFactory.createDataSourceInstrumenter( + openTelemetry, dataSourceInstrumenterEnabled), + JdbcInstrumenterFactory.createStatementInstrumenter( + openTelemetry, statementInstrumenterEnabled, statementSanitizationEnabled)); + } +} diff --git a/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/datasource/OpenTelemetryDataSource.java b/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/datasource/OpenTelemetryDataSource.java index d32995f53d29..232d28b7aac8 100644 --- a/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/datasource/OpenTelemetryDataSource.java +++ b/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/datasource/OpenTelemetryDataSource.java @@ -20,7 +20,7 @@ package io.opentelemetry.instrumentation.jdbc.datasource; -import static io.opentelemetry.instrumentation.jdbc.internal.DataSourceInstrumenterFactory.createDataSourceInstrumenter; +import static io.opentelemetry.instrumentation.jdbc.internal.JdbcInstrumenterFactory.createDataSourceInstrumenter; import static io.opentelemetry.instrumentation.jdbc.internal.JdbcInstrumenterFactory.createStatementInstrumenter; import static io.opentelemetry.instrumentation.jdbc.internal.JdbcUtils.computeDbInfo; @@ -45,7 +45,7 @@ public class OpenTelemetryDataSource implements DataSource, AutoCloseable { private final DataSource delegate; - private final Instrumenter dataSourceInstrumenter; + private final Instrumenter dataSourceInstrumenter; private final Instrumenter statementInstrumenter; private volatile DbInfo cachedDbInfo; @@ -66,12 +66,29 @@ public OpenTelemetryDataSource(DataSource delegate) { * @param delegate the DataSource to wrap * @param openTelemetry the OpenTelemetry instance to setup for */ + @Deprecated public OpenTelemetryDataSource(DataSource delegate, OpenTelemetry openTelemetry) { this.delegate = delegate; - this.dataSourceInstrumenter = createDataSourceInstrumenter(openTelemetry); + this.dataSourceInstrumenter = createDataSourceInstrumenter(openTelemetry, true); this.statementInstrumenter = createStatementInstrumenter(openTelemetry); } + /** + * Create a OpenTelemetry DataSource wrapping another DataSource. + * + * @param delegate the DataSource to wrap + * @param dataSourceInstrumenter the DataSource Instrumenter to use + * @param statementInstrumenter the Statement Instrumenter to use + */ + OpenTelemetryDataSource( + DataSource delegate, + Instrumenter dataSourceInstrumenter, + Instrumenter statementInstrumenter) { + this.delegate = delegate; + this.dataSourceInstrumenter = dataSourceInstrumenter; + this.statementInstrumenter = statementInstrumenter; + } + @Override public Connection getConnection() throws SQLException { Connection connection = wrapCall(delegate::getConnection); @@ -128,25 +145,29 @@ public void close() throws Exception { } } - private T wrapCall(ThrowingSupplier callable) throws E { + private Connection wrapCall(ThrowingSupplier getConnection) + throws SQLException { Context parentContext = Context.current(); - if (!Span.fromContext(parentContext).getSpanContext().isValid()) { + if (!Span.fromContext(parentContext).getSpanContext().isValid() + || !dataSourceInstrumenter.shouldStart(parentContext, delegate)) { // this instrumentation is already very noisy, and calls to getConnection outside of an // existing trace do not tend to be very interesting - return callable.call(); + return getConnection.call(); } - Context context = this.dataSourceInstrumenter.start(parentContext, delegate); - T result; + Context context = dataSourceInstrumenter.start(parentContext, delegate); + Connection connection = null; + Throwable error = null; try (Scope ignored = context.makeCurrent()) { - result = callable.call(); + connection = getConnection.call(); + return connection; } catch (Throwable t) { - this.dataSourceInstrumenter.end(context, delegate, null, t); + error = t; throw t; + } finally { + dataSourceInstrumenter.end(context, delegate, getDbInfo(connection), error); } - this.dataSourceInstrumenter.end(context, delegate, null, null); - return result; } private DbInfo getDbInfo(Connection connection) { diff --git a/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/DataSourceCodeAttributesGetter.java b/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/DataSourceCodeAttributesGetter.java index e90fa0308597..2d9b3e4173e6 100644 --- a/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/DataSourceCodeAttributesGetter.java +++ b/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/DataSourceCodeAttributesGetter.java @@ -5,10 +5,11 @@ package io.opentelemetry.instrumentation.jdbc.internal; -import io.opentelemetry.instrumentation.api.instrumenter.code.CodeAttributesGetter; +import io.opentelemetry.instrumentation.api.incubator.semconv.code.CodeAttributesGetter; import javax.sql.DataSource; -final class DataSourceCodeAttributesGetter implements CodeAttributesGetter { +enum DataSourceCodeAttributesGetter implements CodeAttributesGetter { + INSTANCE; @Override public Class getCodeClass(DataSource dataSource) { diff --git a/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/DataSourceDbAttributesExtractor.java b/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/DataSourceDbAttributesExtractor.java new file mode 100644 index 000000000000..fb35e335d9d7 --- /dev/null +++ b/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/DataSourceDbAttributesExtractor.java @@ -0,0 +1,52 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.jdbc.internal; + +import static io.opentelemetry.instrumentation.api.internal.AttributesExtractorUtil.internalSet; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; +import io.opentelemetry.instrumentation.jdbc.internal.dbinfo.DbInfo; +import javax.annotation.Nullable; +import javax.sql.DataSource; + +enum DataSourceDbAttributesExtractor implements AttributesExtractor { + INSTANCE; + + // copied from DbIncubatingAttributes + private static final AttributeKey DB_NAME = AttributeKey.stringKey("db.name"); + private static final AttributeKey DB_SYSTEM = AttributeKey.stringKey("db.system"); + private static final AttributeKey DB_USER = AttributeKey.stringKey("db.user"); + private static final AttributeKey DB_CONNECTION_STRING = + AttributeKey.stringKey("db.connection_string"); + + @Override + public void onStart(AttributesBuilder attributes, Context parentContext, DataSource dataSource) {} + + @SuppressWarnings("deprecation") // TODO DbIncubatingAttributes.DB_CONNECTION_STRING deprecation + @Override + public void onEnd( + AttributesBuilder attributes, + Context context, + DataSource dataSource, + @Nullable DbInfo dbInfo, + @Nullable Throwable error) { + if (dbInfo == null) { + return; + } + internalSet(attributes, DB_SYSTEM, dbInfo.getSystem()); + internalSet(attributes, DB_USER, dbInfo.getUser()); + internalSet(attributes, DB_NAME, getName(dbInfo)); + internalSet(attributes, DB_CONNECTION_STRING, dbInfo.getShortUrl()); + } + + private static String getName(DbInfo dbInfo) { + String name = dbInfo.getName(); + return name == null ? dbInfo.getDb() : name; + } +} diff --git a/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/DataSourceInstrumenterFactory.java b/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/DataSourceInstrumenterFactory.java deleted file mode 100644 index d259a0fd7845..000000000000 --- a/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/DataSourceInstrumenterFactory.java +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.jdbc.internal; - -import io.opentelemetry.api.OpenTelemetry; -import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; -import io.opentelemetry.instrumentation.api.instrumenter.code.CodeAttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.code.CodeSpanNameExtractor; -import javax.sql.DataSource; - -/** - * This class is internal and is hence not for public use. Its APIs are unstable and can change at - * any time. - */ -public final class DataSourceInstrumenterFactory { - private static final String INSTRUMENTATION_NAME = "io.opentelemetry.jdbc"; - private static final DataSourceCodeAttributesGetter codeAttributesGetter = - new DataSourceCodeAttributesGetter(); - - public static Instrumenter createDataSourceInstrumenter( - OpenTelemetry openTelemetry) { - return Instrumenter.builder( - openTelemetry, INSTRUMENTATION_NAME, CodeSpanNameExtractor.create(codeAttributesGetter)) - .addAttributesExtractor(CodeAttributesExtractor.create(codeAttributesGetter)) - .buildInstrumenter(); - } - - private DataSourceInstrumenterFactory() {} -} diff --git a/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/JdbcAttributesGetter.java b/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/JdbcAttributesGetter.java index 45a4e26b95aa..ba04f3807395 100644 --- a/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/JdbcAttributesGetter.java +++ b/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/JdbcAttributesGetter.java @@ -5,7 +5,7 @@ package io.opentelemetry.instrumentation.jdbc.internal; -import io.opentelemetry.instrumentation.api.instrumenter.db.SqlClientAttributesGetter; +import io.opentelemetry.instrumentation.api.incubator.semconv.db.SqlClientAttributesGetter; import io.opentelemetry.instrumentation.jdbc.internal.dbinfo.DbInfo; import javax.annotation.Nullable; diff --git a/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/JdbcConnectionUrlParser.java b/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/JdbcConnectionUrlParser.java index cef0dc129667..aefab211aafc 100644 --- a/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/JdbcConnectionUrlParser.java +++ b/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/JdbcConnectionUrlParser.java @@ -11,7 +11,6 @@ import com.google.errorprone.annotations.CanIgnoreReturnValue; import io.opentelemetry.instrumentation.jdbc.internal.dbinfo.DbInfo; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes.DbSystemValues; import java.io.UnsupportedEncodingException; import java.net.URI; import java.net.URLDecoder; @@ -322,7 +321,7 @@ DbInfo.Builder doParse(String jdbcUrl, DbInfo.Builder builder) { int clusterSepLoc = jdbcUrl.indexOf(","); int ipv6End = jdbcUrl.startsWith("[") ? jdbcUrl.indexOf("]") : -1; int portLoc = jdbcUrl.indexOf(":", Math.max(0, ipv6End)); - portLoc = clusterSepLoc < portLoc ? -1 : portLoc; + portLoc = clusterSepLoc != -1 && clusterSepLoc < portLoc ? -1 : portLoc; int dbLoc = jdbcUrl.indexOf("/", Math.max(portLoc, clusterSepLoc)); int paramLoc = jdbcUrl.indexOf("?", dbLoc); @@ -667,8 +666,6 @@ DbInfo.Builder doParse(String jdbcUrl, DbInfo.Builder builder) { 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; @@ -832,6 +829,62 @@ DbInfo.Builder doParse(String jdbcUrl, DbInfo.Builder builder) { return MODIFIED_URL_LIKE.doParse(jdbcUrl, builder); } + }, + INFORMIX_SQLI("informix-sqli") { + private static final int DEFAULT_PORT = 9088; + + @Override + DbInfo.Builder doParse(String jdbcUrl, DbInfo.Builder builder) { + builder = MODIFIED_URL_LIKE.doParse(jdbcUrl, builder); + + DbInfo dbInfo = builder.build(); + if (dbInfo.getPort() == null) { + builder.port(DEFAULT_PORT); + } + + int hostIndex = jdbcUrl.indexOf("://"); + if (hostIndex == -1) { + return builder; + } + + int dbNameStartIndex = jdbcUrl.indexOf('/', hostIndex + 3); + if (dbNameStartIndex == -1) { + return builder; + } + int dbNameEndIndex = jdbcUrl.indexOf(':', dbNameStartIndex); + if (dbNameEndIndex == -1) { + dbNameEndIndex = jdbcUrl.length(); + } + String name = jdbcUrl.substring(dbNameStartIndex + 1, dbNameEndIndex); + if (name.isEmpty()) { + builder.name(null); + } else { + builder.name(name); + } + + return builder; + } + }, + + INFORMIX_DIRECT("informix-direct") { + private final Pattern pattern = Pattern.compile("://(.*?)(:|;|$)"); + + @Override + DbInfo.Builder doParse(String jdbcUrl, DbInfo.Builder builder) { + builder = MODIFIED_URL_LIKE.doParse(jdbcUrl, builder); + builder.host(null); + builder.port(null); + + Matcher matcher = pattern.matcher(jdbcUrl); + if (matcher.find()) { + String name = matcher.group(1); + if (!name.isEmpty()) { + builder.name(name); + } + } + + return builder; + } }; private static final Logger logger = Logger.getLogger(JdbcConnectionUrlParser.class.getName()); @@ -863,11 +916,15 @@ public static DbInfo parse(String connectionUrl, Properties props) { // Make this easier and ignore case. connectionUrl = connectionUrl.toLowerCase(Locale.ROOT); - if (!connectionUrl.startsWith("jdbc:")) { + String jdbcUrl; + if (connectionUrl.startsWith("jdbc:")) { + jdbcUrl = connectionUrl.substring("jdbc:".length()); + } else if (connectionUrl.startsWith("jdbc-secretsmanager:")) { + jdbcUrl = connectionUrl.substring("jdbc-secretsmanager:".length()); + } else { return DEFAULT; } - String jdbcUrl = connectionUrl.substring("jdbc:".length()); int typeLoc = jdbcUrl.indexOf(':'); if (typeLoc < 1) { @@ -985,7 +1042,7 @@ private static void populateStandardProperties(DbInfo.Builder builder, Map } // see - // https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/trace/semantic_conventions/database.md + // https://github.com/open-telemetry/semantic-conventions/blob/main/docs/database/database-spans.md private static String toDbSystem(String type) { switch (type) { case "as400": // IBM AS400 Database @@ -997,6 +1054,10 @@ private static String toDbSystem(String type) { return DbSystemValues.H2; case "hsqldb": // Hyper SQL Database return "hsqldb"; + case "informix-sqli": // IBM Informix + return DbSystemValues.INFORMIX_SQLI; + case "informix-direct": + return DbSystemValues.INFORMIX_DIRECT; case "mariadb": // MariaDB return DbSystemValues.MARIADB; case "mysql": // MySQL @@ -1015,4 +1076,22 @@ private static String toDbSystem(String type) { return DbSystemValues.OTHER_SQL; // Unknown DBMS } } + + // copied from DbIncubatingAttributes + private static final class DbSystemValues { + static final String OTHER_SQL = "other_sql"; + static final String MSSQL = "mssql"; + static final String MYSQL = "mysql"; + static final String ORACLE = "oracle"; + static final String DB2 = "db2"; + static final String INFORMIX_SQLI = "informix-sqli"; + static final String INFORMIX_DIRECT = "informix-direct"; + static final String POSTGRESQL = "postgresql"; + static final String HANADB = "hanadb"; + static final String DERBY = "derby"; + static final String MARIADB = "mariadb"; + static final String H2 = "h2"; + + private DbSystemValues() {} + } } diff --git a/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/JdbcInstrumenterFactory.java b/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/JdbcInstrumenterFactory.java index 6399476743fd..2e0b1e583bb7 100644 --- a/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/JdbcInstrumenterFactory.java +++ b/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/JdbcInstrumenterFactory.java @@ -7,12 +7,16 @@ import io.opentelemetry.api.GlobalOpenTelemetry; import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.instrumentation.api.incubator.semconv.code.CodeAttributesExtractor; +import io.opentelemetry.instrumentation.api.incubator.semconv.code.CodeSpanNameExtractor; +import io.opentelemetry.instrumentation.api.incubator.semconv.db.DbClientSpanNameExtractor; +import io.opentelemetry.instrumentation.api.incubator.semconv.db.SqlClientAttributesExtractor; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; import io.opentelemetry.instrumentation.api.instrumenter.SpanKindExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.db.DbClientSpanNameExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.db.SqlClientAttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.net.NetClientAttributesExtractor; import io.opentelemetry.instrumentation.api.internal.ConfigPropertiesUtil; +import io.opentelemetry.instrumentation.api.semconv.network.ServerAttributesExtractor; +import io.opentelemetry.instrumentation.jdbc.internal.dbinfo.DbInfo; +import javax.sql.DataSource; /** * This class is internal and is hence not for public use. Its APIs are unstable and can change at @@ -21,7 +25,8 @@ public final class JdbcInstrumenterFactory { public static final String INSTRUMENTATION_NAME = "io.opentelemetry.jdbc"; private static final JdbcAttributesGetter dbAttributesGetter = new JdbcAttributesGetter(); - private static final JdbcNetAttributesGetter netAttributesGetter = new JdbcNetAttributesGetter(); + private static final JdbcNetworkAttributesGetter netAttributesGetter = + new JdbcNetworkAttributesGetter(); public static Instrumenter createStatementInstrumenter() { return createStatementInstrumenter(GlobalOpenTelemetry.get()); @@ -29,19 +34,38 @@ public static Instrumenter createStatementInstrumenter() { public static Instrumenter createStatementInstrumenter( OpenTelemetry openTelemetry) { + return createStatementInstrumenter( + openTelemetry, + true, + ConfigPropertiesUtil.getBoolean( + "otel.instrumentation.common.db-statement-sanitizer.enabled", true)); + } + + public static Instrumenter createStatementInstrumenter( + OpenTelemetry openTelemetry, boolean enabled, boolean statementSanitizationEnabled) { return Instrumenter.builder( openTelemetry, INSTRUMENTATION_NAME, DbClientSpanNameExtractor.create(dbAttributesGetter)) .addAttributesExtractor( SqlClientAttributesExtractor.builder(dbAttributesGetter) - .setStatementSanitizationEnabled( - ConfigPropertiesUtil.getBoolean( - "otel.instrumentation.common.db-statement-sanitizer.enabled", true)) + .setStatementSanitizationEnabled(statementSanitizationEnabled) .build()) - .addAttributesExtractor(NetClientAttributesExtractor.create(netAttributesGetter)) + .addAttributesExtractor(ServerAttributesExtractor.create(netAttributesGetter)) + .setEnabled(enabled) .buildInstrumenter(SpanKindExtractor.alwaysClient()); } + public static Instrumenter createDataSourceInstrumenter( + OpenTelemetry openTelemetry, boolean enabled) { + DataSourceCodeAttributesGetter getter = DataSourceCodeAttributesGetter.INSTANCE; + return Instrumenter.builder( + openTelemetry, INSTRUMENTATION_NAME, CodeSpanNameExtractor.create(getter)) + .addAttributesExtractor(CodeAttributesExtractor.create(getter)) + .addAttributesExtractor(DataSourceDbAttributesExtractor.INSTANCE) + .setEnabled(enabled) + .buildInstrumenter(); + } + private JdbcInstrumenterFactory() {} } diff --git a/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/JdbcNetAttributesGetter.java b/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/JdbcNetworkAttributesGetter.java similarity index 74% rename from instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/JdbcNetAttributesGetter.java rename to instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/JdbcNetworkAttributesGetter.java index 168e4d7cbd04..a09ca312672b 100644 --- a/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/JdbcNetAttributesGetter.java +++ b/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/JdbcNetworkAttributesGetter.java @@ -5,14 +5,14 @@ package io.opentelemetry.instrumentation.jdbc.internal; -import io.opentelemetry.instrumentation.api.instrumenter.net.NetClientAttributesGetter; +import io.opentelemetry.instrumentation.api.semconv.network.ServerAttributesGetter; import javax.annotation.Nullable; /** * This class is internal and is hence not for public use. Its APIs are unstable and can change at * any time. */ -public final class JdbcNetAttributesGetter implements NetClientAttributesGetter { +public final class JdbcNetworkAttributesGetter implements ServerAttributesGetter { @Nullable @Override diff --git a/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/JdbcUtils.java b/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/JdbcUtils.java index 50da4609b5ac..9169891557c0 100644 --- a/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/JdbcUtils.java +++ b/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/JdbcUtils.java @@ -26,12 +26,19 @@ public final class JdbcUtils { @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(); + return unwrapConnection(statement.getConnection()); + } catch (Throwable e) { + // Had some problem getting the connection. + logger.log(FINE, "Could not get connection from a statement", e); + return null; + } + } + /** Returns the unwrapped connection or null if exception was thrown. */ + public static Connection unwrapConnection(Connection connection) { + try { if (c3poField != null) { if (connection.getClass().getName().equals("com.mchange.v2.c3p0.impl.NewProxyConnection")) { return (Connection) c3poField.get(connection); @@ -64,7 +71,7 @@ public static Connection connectionFromStatement(Statement statement) { } } catch (Throwable e) { // Had some problem getting the connection. - logger.log(FINE, "Could not get connection for StatementAdvice", e); + logger.log(FINE, "Could not unwrap connection", e); return null; } return connection; @@ -91,6 +98,9 @@ public static DbInfo computeDbInfo(Connection connection) { * connection will be stored with the DEFAULT DBInfo as the value in the connectionInfo map to * avoid retry overhead. */ + if (connection == null) { + return DbInfo.DEFAULT; + } try { DatabaseMetaData metaData = connection.getMetaData(); String url = metaData.getURL(); diff --git a/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/OpenTelemetryCallableStatement.java b/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/OpenTelemetryCallableStatement.java index 267786751225..2d829ffa3a06 100644 --- a/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/OpenTelemetryCallableStatement.java +++ b/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/OpenTelemetryCallableStatement.java @@ -49,8 +49,12 @@ public class OpenTelemetryCallableStatement extends OpenTelemetryPreparedStatement implements CallableStatement { public OpenTelemetryCallableStatement( - S delegate, DbInfo dbInfo, String query, Instrumenter instrumenter) { - super(delegate, dbInfo, query, instrumenter); + S delegate, + OpenTelemetryConnection connection, + DbInfo dbInfo, + String query, + Instrumenter instrumenter) { + super(delegate, connection, dbInfo, query, instrumenter); } @Override diff --git a/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/OpenTelemetryConnection.java b/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/OpenTelemetryConnection.java index 8b394eb9dabd..ad1a8a09f8cc 100644 --- a/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/OpenTelemetryConnection.java +++ b/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/OpenTelemetryConnection.java @@ -61,14 +61,14 @@ public OpenTelemetryConnection( @Override public Statement createStatement() throws SQLException { Statement statement = delegate.createStatement(); - return new OpenTelemetryStatement<>(statement, dbInfo, statementInstrumenter); + return new OpenTelemetryStatement<>(statement, this, dbInfo, statementInstrumenter); } @Override public Statement createStatement(int resultSetType, int resultSetConcurrency) throws SQLException { Statement statement = delegate.createStatement(resultSetType, resultSetConcurrency); - return new OpenTelemetryStatement<>(statement, dbInfo, statementInstrumenter); + return new OpenTelemetryStatement<>(statement, this, dbInfo, statementInstrumenter); } @Override @@ -76,13 +76,14 @@ public Statement createStatement( int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException { Statement statement = delegate.createStatement(resultSetType, resultSetConcurrency, resultSetHoldability); - return new OpenTelemetryStatement<>(statement, dbInfo, statementInstrumenter); + return new OpenTelemetryStatement<>(statement, this, dbInfo, statementInstrumenter); } @Override public PreparedStatement prepareStatement(String sql) throws SQLException { PreparedStatement statement = delegate.prepareStatement(sql); - return new OpenTelemetryPreparedStatement<>(statement, dbInfo, sql, statementInstrumenter); + return new OpenTelemetryPreparedStatement<>( + statement, this, dbInfo, sql, statementInstrumenter); } @Override @@ -90,7 +91,8 @@ public PreparedStatement prepareStatement(String sql, int resultSetType, int res throws SQLException { PreparedStatement statement = delegate.prepareStatement(sql, resultSetType, resultSetConcurrency); - return new OpenTelemetryPreparedStatement<>(statement, dbInfo, sql, statementInstrumenter); + return new OpenTelemetryPreparedStatement<>( + statement, this, dbInfo, sql, statementInstrumenter); } @Override @@ -99,38 +101,44 @@ public PreparedStatement prepareStatement( throws SQLException { PreparedStatement statement = delegate.prepareStatement(sql, resultSetType, resultSetConcurrency, resultSetHoldability); - return new OpenTelemetryPreparedStatement<>(statement, dbInfo, sql, statementInstrumenter); + return new OpenTelemetryPreparedStatement<>( + statement, this, dbInfo, sql, statementInstrumenter); } @Override public PreparedStatement prepareStatement(String sql, int autoGeneratedKeys) throws SQLException { PreparedStatement statement = delegate.prepareStatement(sql, autoGeneratedKeys); - return new OpenTelemetryPreparedStatement<>(statement, dbInfo, sql, statementInstrumenter); + return new OpenTelemetryPreparedStatement<>( + statement, this, dbInfo, sql, statementInstrumenter); } @Override public PreparedStatement prepareStatement(String sql, int[] columnIndexes) throws SQLException { PreparedStatement statement = delegate.prepareStatement(sql, columnIndexes); - return new OpenTelemetryPreparedStatement<>(statement, dbInfo, sql, statementInstrumenter); + return new OpenTelemetryPreparedStatement<>( + statement, this, dbInfo, sql, statementInstrumenter); } @Override public PreparedStatement prepareStatement(String sql, String[] columnNames) throws SQLException { PreparedStatement statement = delegate.prepareStatement(sql, columnNames); - return new OpenTelemetryPreparedStatement<>(statement, dbInfo, sql, statementInstrumenter); + return new OpenTelemetryPreparedStatement<>( + statement, this, dbInfo, sql, statementInstrumenter); } @Override public CallableStatement prepareCall(String sql) throws SQLException { CallableStatement statement = delegate.prepareCall(sql); - return new OpenTelemetryCallableStatement<>(statement, dbInfo, sql, statementInstrumenter); + return new OpenTelemetryCallableStatement<>( + statement, this, dbInfo, sql, statementInstrumenter); } @Override public CallableStatement prepareCall(String sql, int resultSetType, int resultSetConcurrency) throws SQLException { CallableStatement statement = delegate.prepareCall(sql, resultSetType, resultSetConcurrency); - return new OpenTelemetryCallableStatement<>(statement, dbInfo, sql, statementInstrumenter); + return new OpenTelemetryCallableStatement<>( + statement, this, dbInfo, sql, statementInstrumenter); } @Override @@ -139,7 +147,8 @@ public CallableStatement prepareCall( throws SQLException { CallableStatement statement = delegate.prepareCall(sql, resultSetType, resultSetConcurrency, resultSetHoldability); - return new OpenTelemetryCallableStatement<>(statement, dbInfo, sql, statementInstrumenter); + return new OpenTelemetryCallableStatement<>( + statement, this, dbInfo, sql, statementInstrumenter); } @Override diff --git a/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/OpenTelemetryPreparedStatement.java b/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/OpenTelemetryPreparedStatement.java index 74e9b306b62d..691e61304a1d 100644 --- a/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/OpenTelemetryPreparedStatement.java +++ b/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/OpenTelemetryPreparedStatement.java @@ -51,8 +51,12 @@ public class OpenTelemetryPreparedStatement extends OpenTelemetryStatement implements PreparedStatement { public OpenTelemetryPreparedStatement( - S delegate, DbInfo dbInfo, String query, Instrumenter instrumenter) { - super(delegate, dbInfo, query, instrumenter); + S delegate, + OpenTelemetryConnection connection, + DbInfo dbInfo, + String query, + Instrumenter instrumenter) { + super(delegate, connection, dbInfo, query, instrumenter); } @Override diff --git a/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/OpenTelemetryStatement.java b/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/OpenTelemetryStatement.java index ddaccb057eac..d2d5a8b6ca94 100644 --- a/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/OpenTelemetryStatement.java +++ b/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/OpenTelemetryStatement.java @@ -38,19 +38,29 @@ public class OpenTelemetryStatement implements Statement { protected final S delegate; + protected final OpenTelemetryConnection connection; protected final DbInfo dbInfo; protected final String query; protected final Instrumenter instrumenter; private final ArrayList batchCommands = new ArrayList<>(); - OpenTelemetryStatement(S delegate, DbInfo dbInfo, Instrumenter instrumenter) { - this(delegate, dbInfo, null, instrumenter); + OpenTelemetryStatement( + S delegate, + OpenTelemetryConnection connection, + DbInfo dbInfo, + Instrumenter instrumenter) { + this(delegate, connection, dbInfo, null, instrumenter); } OpenTelemetryStatement( - S delegate, DbInfo dbInfo, String query, Instrumenter instrumenter) { + S delegate, + OpenTelemetryConnection connection, + DbInfo dbInfo, + String query, + Instrumenter instrumenter) { this.delegate = delegate; + this.connection = connection; this.dbInfo = dbInfo; this.query = query; this.instrumenter = instrumenter; @@ -230,8 +240,8 @@ public void clearBatch() throws SQLException { } @Override - public Connection getConnection() throws SQLException { - return delegate.getConnection(); + public Connection getConnection() { + return connection; } @Override diff --git a/instrumentation/jdbc/library/src/test/groovy/io/opentelemetry/instrumentation/jdbc/OpenTelemetryConnectionTest.groovy b/instrumentation/jdbc/library/src/test/groovy/io/opentelemetry/instrumentation/jdbc/OpenTelemetryConnectionTest.groovy deleted file mode 100644 index 1b106b74ef6a..000000000000 --- a/instrumentation/jdbc/library/src/test/groovy/io/opentelemetry/instrumentation/jdbc/OpenTelemetryConnectionTest.groovy +++ /dev/null @@ -1,212 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.jdbc - - -import io.opentelemetry.api.OpenTelemetry -import io.opentelemetry.api.trace.SpanKind -import io.opentelemetry.context.propagation.ContextPropagators -import io.opentelemetry.instrumentation.jdbc.internal.OpenTelemetryCallableStatement -import io.opentelemetry.instrumentation.jdbc.internal.OpenTelemetryConnection -import io.opentelemetry.instrumentation.jdbc.internal.OpenTelemetryPreparedStatement -import io.opentelemetry.instrumentation.jdbc.internal.OpenTelemetryStatement -import io.opentelemetry.instrumentation.jdbc.internal.dbinfo.DbInfo -import io.opentelemetry.instrumentation.test.InstrumentationSpecification -import io.opentelemetry.instrumentation.test.LibraryTestTrait -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes - -import static io.opentelemetry.api.trace.SpanKind.CLIENT -import static io.opentelemetry.instrumentation.jdbc.internal.JdbcInstrumenterFactory.createStatementInstrumenter - -class OpenTelemetryConnectionTest extends InstrumentationSpecification implements LibraryTestTrait { - - def "verify create statement"() { - setup: - def instr = createStatementInstrumenter(openTelemetry) - def dbInfo = getDbInfo() - def connection = new OpenTelemetryConnection(new TestConnection(), dbInfo, instr) - String query = "SELECT * FROM users" - def statement = connection.createStatement() - runWithSpan("parent") { - assert statement.execute(query) - } - - expect: - assertTraces(1) { - trace(0, 2) { - span(0) { - name "parent" - kind SpanKind.INTERNAL - hasNoParent() - } - span(1) { - name "SELECT my_name.users" - kind CLIENT - childOf span(0) - attributes { - "$SemanticAttributes.DB_SYSTEM" dbInfo.system - "$SemanticAttributes.DB_NAME" dbInfo.name - "$SemanticAttributes.DB_USER" dbInfo.user - "$SemanticAttributes.DB_CONNECTION_STRING" dbInfo.shortUrl - "$SemanticAttributes.NET_PEER_NAME" dbInfo.host - "$SemanticAttributes.NET_PEER_PORT" dbInfo.port - "$SemanticAttributes.DB_STATEMENT" query - "$SemanticAttributes.DB_OPERATION" "SELECT" - "$SemanticAttributes.DB_SQL_TABLE" "users" - } - } - } - } - - cleanup: - statement.close() - connection.close() - } - - def "verify create statement returns otel wrapper"() { - when: - def ot = OpenTelemetry.propagating(ContextPropagators.noop()) - def instr = createStatementInstrumenter(ot) - def connection = new OpenTelemetryConnection(new TestConnection(), DbInfo.DEFAULT, instr) - - then: - connection.createStatement().class == OpenTelemetryStatement - connection.createStatement(0, 0).class == OpenTelemetryStatement - connection.createStatement(0, 0, 0).class == OpenTelemetryStatement - connection.createStatement().instrumenter == instr - } - - def "verify prepare statement"() { - setup: - def instr = createStatementInstrumenter(openTelemetry) - def dbInfo = getDbInfo() - def connection = new OpenTelemetryConnection(new TestConnection(), dbInfo, instr) - String query = "SELECT * FROM users" - def statement = connection.prepareStatement(query) - runWithSpan("parent") { - assert statement.execute() - } - - expect: - assertTraces(1) { - trace(0, 2) { - span(0) { - name "parent" - kind SpanKind.INTERNAL - hasNoParent() - } - span(1) { - name "SELECT my_name.users" - kind CLIENT - childOf span(0) - attributes { - "$SemanticAttributes.DB_SYSTEM" dbInfo.system - "$SemanticAttributes.DB_NAME" dbInfo.name - "$SemanticAttributes.DB_USER" dbInfo.user - "$SemanticAttributes.DB_CONNECTION_STRING" dbInfo.shortUrl - "$SemanticAttributes.NET_PEER_NAME" dbInfo.host - "$SemanticAttributes.NET_PEER_PORT" dbInfo.port - "$SemanticAttributes.DB_STATEMENT" query - "$SemanticAttributes.DB_OPERATION" "SELECT" - "$SemanticAttributes.DB_SQL_TABLE" "users" - } - } - } - } - - cleanup: - statement.close() - connection.close() - } - - def "verify prepare statement returns otel wrapper"() { - when: - def ot = OpenTelemetry.propagating(ContextPropagators.noop()) - def instr = createStatementInstrumenter(ot) - def connection = new OpenTelemetryConnection(new TestConnection(), DbInfo.DEFAULT, instr) - String query = "SELECT * FROM users" - - then: - connection.prepareStatement(query).class == OpenTelemetryPreparedStatement - connection.prepareStatement(query, [0] as int[]).class == OpenTelemetryPreparedStatement - connection.prepareStatement(query, ["id"] as String[]).class == OpenTelemetryPreparedStatement - connection.prepareStatement(query, 0).class == OpenTelemetryPreparedStatement - connection.prepareStatement(query, 0, 0).class == OpenTelemetryPreparedStatement - connection.prepareStatement(query, 0, 0, 0).class == OpenTelemetryPreparedStatement - connection.prepareStatement(query).instrumenter == instr - } - - def "verify prepare call"() { - setup: - def instr = createStatementInstrumenter(openTelemetry) - def dbInfo = getDbInfo() - def connection = new OpenTelemetryConnection(new TestConnection(), dbInfo, instr) - String query = "SELECT * FROM users" - def statement = connection.prepareCall(query) - runWithSpan("parent") { - assert statement.execute() - } - - expect: - assertTraces(1) { - trace(0, 2) { - span(0) { - name "parent" - kind SpanKind.INTERNAL - hasNoParent() - } - span(1) { - name "SELECT my_name.users" - kind CLIENT - childOf span(0) - attributes { - "$SemanticAttributes.DB_SYSTEM" dbInfo.system - "$SemanticAttributes.DB_NAME" dbInfo.name - "$SemanticAttributes.DB_USER" dbInfo.user - "$SemanticAttributes.DB_CONNECTION_STRING" dbInfo.shortUrl - "$SemanticAttributes.NET_PEER_NAME" dbInfo.host - "$SemanticAttributes.NET_PEER_PORT" dbInfo.port - "$SemanticAttributes.DB_STATEMENT" query - "$SemanticAttributes.DB_OPERATION" "SELECT" - "$SemanticAttributes.DB_SQL_TABLE" "users" - } - } - } - } - - cleanup: - statement.close() - connection.close() - } - - def "verify prepare call returns otel wrapper"() { - when: - def ot = OpenTelemetry.propagating(ContextPropagators.noop()) - def instr = createStatementInstrumenter(ot) - def connection = new OpenTelemetryConnection(new TestConnection(), DbInfo.DEFAULT, instr) - String query = "SELECT * FROM users" - - then: - connection.prepareCall(query).class == OpenTelemetryCallableStatement - connection.prepareCall(query, 0, 0).class == OpenTelemetryCallableStatement - connection.prepareCall(query, 0, 0, 0).class == OpenTelemetryCallableStatement - connection.prepareCall(query).instrumenter == instr - } - - private DbInfo getDbInfo() { - DbInfo.builder() - .system("my_system") - .subtype("my_sub_type") - .shortUrl("my_connection_string") - .user("my_user") - .name("my_name") - .db("my_db") - .host("my_host") - .port(1234) - .build() - } - -} diff --git a/instrumentation/jdbc/library/src/test/groovy/io/opentelemetry/instrumentation/jdbc/internal/JdbcConnectionUrlParserTest.groovy b/instrumentation/jdbc/library/src/test/groovy/io/opentelemetry/instrumentation/jdbc/internal/JdbcConnectionUrlParserTest.groovy deleted file mode 100644 index b4048831e7cc..000000000000 --- a/instrumentation/jdbc/library/src/test/groovy/io/opentelemetry/instrumentation/jdbc/internal/JdbcConnectionUrlParserTest.groovy +++ /dev/null @@ -1,224 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.jdbc.internal - -import io.opentelemetry.instrumentation.jdbc.internal.dbinfo.DbInfo -import spock.lang.Shared -import spock.lang.Specification - -import static io.opentelemetry.instrumentation.jdbc.internal.JdbcConnectionUrlParser.parse - -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.db == expected.db - - 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" - "jdbc:mysql://myuser:password@my.host:22/mydb" | null | "mysql://my.host:22" | "mysql" | null | "myuser" | "my.host" | 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" | "hanadb" | null | null | "sap.host" | null | null | null - "jdbc:sap://sap.host" | stdProps | "sap://sap.host:9999" | "hanadb" | null | "stdUserName" | "sap.host" | 9999 | null | "stdDatabaseName" - "jdbc:sap://sap.host:88/?databaseName=sapdb&user=sapuser&password=PW" | null | "sap://sap.host:88" | "hanadb" | 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 - - // https://docs.progress.com/bundle/datadirect-connect-jdbc-51/page/URL-Formats-DataDirect-Connect-for-JDBC-Drivers.html - "jdbc:datadirect:sqlserver://server_name:1433;DatabaseName=dbname" | null | "datadirect:sqlserver://server_name:1433" | "mssql" | "sqlserver" | null | "server_name" | 1433 | null | "dbname" - "jdbc:datadirect:oracle://server_name:1521;ServiceName=your_servicename" | null | "datadirect:oracle://server_name:1521" | "oracle" | "oracle" | null | "server_name" | 1521 | null | null - "jdbc:datadirect:mysql://server_name:3306" | null | "datadirect:mysql://server_name:3306" | "mysql" | "mysql" | null | "server_name" | 3306 | null | null - "jdbc:datadirect:postgresql://server_name:5432;DatabaseName=dbname" | null | "datadirect:postgresql://server_name:5432" | "postgresql" | "postgresql" | null | "server_name" | 5432 | null | "dbname" - "jdbc:datadirect:db2://server_name:50000;DatabaseName=dbname" | null | "datadirect:db2://server_name:50000" | "db2" | "db2" | null | "server_name" | 50000 | null | "dbname" - - // "the TIBCO JDBC drivers are based on the Progress DataDirect Connect drivers" - // https://community.jaspersoft.com/documentation/tibco-jasperreports-server-administrator-guide/v601/working-data-sources - "jdbc:tibcosoftware:sqlserver://server_name:1433;DatabaseName=dbname" | null | "tibcosoftware:sqlserver://server_name:1433" | "mssql" | "sqlserver" | null | "server_name" | 1433 | null | "dbname" - "jdbc:tibcosoftware:oracle://server_name:1521;ServiceName=your_servicename" | null | "tibcosoftware:oracle://server_name:1521" | "oracle" | "oracle" | null | "server_name" | 1521 | null | null - "jdbc:tibcosoftware:mysql://server_name:3306" | null | "tibcosoftware:mysql://server_name:3306" | "mysql" | "mysql" | null | "server_name" | 3306 | null | null - "jdbc:tibcosoftware:postgresql://server_name:5432;DatabaseName=dbname" | null | "tibcosoftware:postgresql://server_name:5432" | "postgresql" | "postgresql" | null | "server_name" | 5432 | null | "dbname" - "jdbc:tibcosoftware:db2://server_name:50000;DatabaseName=dbname" | null | "tibcosoftware:db2://server_name:50000" | "db2" | "db2" | null | "server_name" | 50000 | null | "dbname" - - expected = DbInfo.builder().system(system).subtype(subtype).user(user).name(name).db(db).host(host).port(port).shortUrl(shortUrl).build() - } -} diff --git a/instrumentation/jdbc/library/src/test/java/io/opentelemetry/instrumentation/jdbc/datasource/JdbcTelemetryTest.java b/instrumentation/jdbc/library/src/test/java/io/opentelemetry/instrumentation/jdbc/datasource/JdbcTelemetryTest.java new file mode 100644 index 000000000000..977a87f54c56 --- /dev/null +++ b/instrumentation/jdbc/library/src/test/java/io/opentelemetry/instrumentation/jdbc/datasource/JdbcTelemetryTest.java @@ -0,0 +1,135 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.jdbc.datasource; + +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.instrumentation.jdbc.internal.OpenTelemetryConnection; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.LibraryInstrumentationExtension; +import io.opentelemetry.semconv.incubating.DbIncubatingAttributes; +import java.sql.CallableStatement; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.sql.Statement; +import javax.sql.DataSource; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +class JdbcTelemetryTest { + + @RegisterExtension + static final InstrumentationExtension testing = LibraryInstrumentationExtension.create(); + + @Test + void buildWithDefaults() throws SQLException { + JdbcTelemetry telemetry = JdbcTelemetry.builder(testing.getOpenTelemetry()).build(); + DataSource dataSource = telemetry.wrap(new TestDataSource()); + + testing.runWithSpan( + "parent", () -> dataSource.getConnection().createStatement().execute("SELECT 1;")); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent"), + span -> span.hasName("TestDataSource.getConnection"), + span -> + span.hasName("SELECT dbname") + .hasAttribute(equalTo(DbIncubatingAttributes.DB_STATEMENT, "SELECT ?;")))); + } + + @Test + void buildWithAllInstrumentersDisabled() throws SQLException { + JdbcTelemetry telemetry = + JdbcTelemetry.builder(testing.getOpenTelemetry()) + .setDataSourceInstrumenterEnabled(false) + .setStatementInstrumenterEnabled(false) + .build(); + + DataSource dataSource = telemetry.wrap(new TestDataSource()); + + testing.runWithSpan( + "parent", () -> dataSource.getConnection().createStatement().execute("SELECT 1;")); + + testing.waitAndAssertTraces( + trace -> trace.hasSpansSatisfyingExactly(span -> span.hasName("parent"))); + } + + @Test + void buildWithDataSourceInstrumenterDisabled() throws SQLException { + JdbcTelemetry telemetry = + JdbcTelemetry.builder(testing.getOpenTelemetry()) + .setDataSourceInstrumenterEnabled(false) + .build(); + + DataSource dataSource = telemetry.wrap(new TestDataSource()); + + testing.runWithSpan( + "parent", () -> dataSource.getConnection().createStatement().execute("SELECT 1;")); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent"), span -> span.hasName("SELECT dbname"))); + } + + @Test + void buildWithStatementInstrumenterDisabled() throws SQLException { + JdbcTelemetry telemetry = + JdbcTelemetry.builder(testing.getOpenTelemetry()) + .setStatementInstrumenterEnabled(false) + .build(); + + DataSource dataSource = telemetry.wrap(new TestDataSource()); + + testing.runWithSpan( + "parent", () -> dataSource.getConnection().createStatement().execute("SELECT 1;")); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent"), + span -> span.hasName("TestDataSource.getConnection"))); + } + + @Test + void buildWithSanitizationDisabled() throws SQLException { + JdbcTelemetry telemetry = + JdbcTelemetry.builder(testing.getOpenTelemetry()) + .setStatementSanitizationEnabled(false) + .build(); + + DataSource dataSource = telemetry.wrap(new TestDataSource()); + + testing.runWithSpan( + "parent", () -> dataSource.getConnection().createStatement().execute("SELECT 1;")); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent"), + span -> span.hasName("TestDataSource.getConnection"), + span -> + span.hasName("SELECT dbname") + .hasAttribute(equalTo(DbIncubatingAttributes.DB_STATEMENT, "SELECT 1;")))); + } + + @Test + void statementReturnsWrappedConnection() throws SQLException { + JdbcTelemetry telemetry = JdbcTelemetry.builder(testing.getOpenTelemetry()).build(); + DataSource dataSource = telemetry.wrap(new TestDataSource()); + Connection connection = dataSource.getConnection(); + Statement statement = connection.createStatement(); + assertThat(statement.getConnection()).isInstanceOf(OpenTelemetryConnection.class); + PreparedStatement preparedStatement = connection.prepareStatement("SELECT 1"); + assertThat(preparedStatement.getConnection()).isInstanceOf(OpenTelemetryConnection.class); + CallableStatement callableStatement = connection.prepareCall("SELECT 1"); + assertThat(callableStatement.getConnection()).isInstanceOf(OpenTelemetryConnection.class); + } +} diff --git a/instrumentation/jdbc/library/src/test/java/io/opentelemetry/instrumentation/jdbc/datasource/OpenTelemetryDataSourceTest.java b/instrumentation/jdbc/library/src/test/java/io/opentelemetry/instrumentation/jdbc/datasource/OpenTelemetryDataSourceTest.java index a0b705d42d6b..6c775f9c2b7f 100644 --- a/instrumentation/jdbc/library/src/test/java/io/opentelemetry/instrumentation/jdbc/datasource/OpenTelemetryDataSourceTest.java +++ b/instrumentation/jdbc/library/src/test/java/io/opentelemetry/instrumentation/jdbc/datasource/OpenTelemetryDataSourceTest.java @@ -5,56 +5,100 @@ package io.opentelemetry.instrumentation.jdbc.datasource; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.params.provider.Arguments.arguments; -import io.opentelemetry.api.OpenTelemetry; -import io.opentelemetry.context.propagation.ContextPropagators; +import io.opentelemetry.api.trace.SpanKind; import io.opentelemetry.instrumentation.jdbc.internal.OpenTelemetryConnection; import io.opentelemetry.instrumentation.jdbc.internal.dbinfo.DbInfo; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.LibraryInstrumentationExtension; +import io.opentelemetry.semconv.incubating.CodeIncubatingAttributes; +import io.opentelemetry.semconv.incubating.DbIncubatingAttributes; import java.sql.Connection; import java.sql.SQLException; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; +import java.util.stream.Stream; +import javax.sql.DataSource; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.RegisterExtension; +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; class OpenTelemetryDataSourceTest { - @DisplayName("verify get connection") - @Test - void verifyGetConnection() throws SQLException { + @RegisterExtension + static final InstrumentationExtension testing = LibraryInstrumentationExtension.create(); - OpenTelemetry openTelemetry = OpenTelemetry.propagating(ContextPropagators.noop()); - TestDataSource testDataSource = new TestDataSource(); - OpenTelemetryDataSource dataSource = new OpenTelemetryDataSource(testDataSource, openTelemetry); - Connection connection = dataSource.getConnection(); + @SuppressWarnings("deprecation") // TODO DbIncubatingAttributes.DB_CONNECTION_STRING deprecation + @ParameterizedTest + @ArgumentsSource(GetConnectionMethods.class) + void shouldEmitGetConnectionSpans(GetConnectionFunction getConnection) throws SQLException { + JdbcTelemetry telemetry = JdbcTelemetry.create(testing.getOpenTelemetry()); + DataSource dataSource = telemetry.wrap(new TestDataSource()); - assertThat(connection).isExactlyInstanceOf(OpenTelemetryConnection.class); + Connection connection = testing.runWithSpan("parent", () -> getConnection.call(dataSource)); - DbInfo dbInfo = ((OpenTelemetryConnection) connection).getDbInfo(); + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent"), + span -> + span.hasName("TestDataSource.getConnection") + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo( + CodeIncubatingAttributes.CODE_NAMESPACE, + TestDataSource.class.getName()), + equalTo(CodeIncubatingAttributes.CODE_FUNCTION, "getConnection"), + equalTo(DbIncubatingAttributes.DB_SYSTEM, "postgresql"), + equalTo(DbIncubatingAttributes.DB_NAME, "dbname"), + equalTo( + DbIncubatingAttributes.DB_CONNECTION_STRING, + "postgresql://127.0.0.1:5432")))); - assertThat(dbInfo.getSystem()).isEqualTo("postgresql"); - assertNull(dbInfo.getSubtype()); - assertThat(dbInfo.getShortUrl()).isEqualTo("postgresql://127.0.0.1:5432"); - assertNull(dbInfo.getUser()); - assertNull(dbInfo.getName()); - assertThat(dbInfo.getDb()).isEqualTo("dbname"); - assertThat(dbInfo.getHost()).isEqualTo("127.0.0.1"); - assertThat(dbInfo.getPort()).isEqualTo(5432); + assertThat(connection).isExactlyInstanceOf(OpenTelemetryConnection.class); + DbInfo dbInfo = ((OpenTelemetryConnection) connection).getDbInfo(); + assertDbInfo(dbInfo); } - @DisplayName("verify get connection with username and password") - @Test - void verifyGetConnectionWithUserNameAndPassword() throws SQLException { + @ParameterizedTest + @ArgumentsSource(GetConnectionMethods.class) + void shouldNotEmitGetConnectionSpansWithoutParentSpan(GetConnectionFunction getConnection) + throws SQLException { + JdbcTelemetry telemetry = JdbcTelemetry.create(testing.getOpenTelemetry()); + DataSource dataSource = telemetry.wrap(new TestDataSource()); - OpenTelemetry openTelemetry = OpenTelemetry.propagating(ContextPropagators.noop()); - OpenTelemetryDataSource dataSource = - new OpenTelemetryDataSource(new TestDataSource(), openTelemetry); - Connection connection = dataSource.getConnection(null, null); + Connection connection = getConnection.call(dataSource); - assertThat(connection).isExactlyInstanceOf(OpenTelemetryConnection.class); + assertThat(testing.waitForTraces(0)).isEmpty(); + assertThat(connection).isExactlyInstanceOf(OpenTelemetryConnection.class); DbInfo dbInfo = ((OpenTelemetryConnection) connection).getDbInfo(); + assertDbInfo(dbInfo); + } + + static class GetConnectionMethods implements ArgumentsProvider { + + @Override + public Stream provideArguments(ExtensionContext context) throws Exception { + GetConnectionFunction getConnection = DataSource::getConnection; + GetConnectionFunction getConnectionWithUserAndPass = ds -> ds.getConnection(null, null); + return Stream.of(arguments(getConnection), arguments(getConnectionWithUserAndPass)); + } + } + + @FunctionalInterface + interface GetConnectionFunction { + + Connection call(DataSource dataSource) throws SQLException; + } + private static void assertDbInfo(DbInfo dbInfo) { assertThat(dbInfo.getSystem()).isEqualTo("postgresql"); assertNull(dbInfo.getSubtype()); assertThat(dbInfo.getShortUrl()).isEqualTo("postgresql://127.0.0.1:5432"); diff --git a/instrumentation/jdbc/library/src/test/java/io/opentelemetry/instrumentation/jdbc/internal/JdbcConnectionUrlParserTest.java b/instrumentation/jdbc/library/src/test/java/io/opentelemetry/instrumentation/jdbc/internal/JdbcConnectionUrlParserTest.java new file mode 100644 index 000000000000..c4a41ca631fd --- /dev/null +++ b/instrumentation/jdbc/library/src/test/java/io/opentelemetry/instrumentation/jdbc/internal/JdbcConnectionUrlParserTest.java @@ -0,0 +1,1221 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.jdbc.internal; + +import static io.opentelemetry.instrumentation.jdbc.internal.JdbcConnectionUrlParser.parse; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.params.provider.Arguments.arguments; + +import io.opentelemetry.instrumentation.jdbc.internal.dbinfo.DbInfo; +import java.util.ArrayList; +import java.util.List; +import java.util.Properties; +import java.util.stream.Stream; +import org.junit.jupiter.api.Test; +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.junit.jupiter.params.provider.ValueSource; + +class JdbcConnectionUrlParserTest { + + private static Properties stdProps() { + Properties 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; + } + + @ParameterizedTest + @ValueSource(strings = {"", "jdbc:", "jdbc::", "bogus:string"}) + void testInvalidUrlReturnsDefault(String url) { + assertThat(JdbcConnectionUrlParser.parse(url, null)).isEqualTo(DbInfo.DEFAULT); + } + + @Test + void testNullUrlReturnsDefault() { + assertThat(JdbcConnectionUrlParser.parse(null, null)).isEqualTo(DbInfo.DEFAULT); + } + + @ParameterizedTest(name = "{index}: {0}") + @ArgumentsSource(ParsingProvider.class) + void testVerifySystemSubtypeParsingOfUrl(ParseTestArgument argument) { + DbInfo info = parse(argument.url, argument.properties); + DbInfo expected = argument.dbInfo; + assertThat(info.getShortUrl()).isEqualTo(expected.getShortUrl()); + assertThat(info.getSystem()).isEqualTo(expected.getSystem()); + assertThat(info.getHost()).isEqualTo(expected.getHost()); + assertThat(info.getPort()).isEqualTo(expected.getPort()); + assertThat(info.getUser()).isEqualTo(expected.getUser()); + assertThat(info.getName()).isEqualTo(expected.getName()); + assertThat(info.getDb()).isEqualTo(expected.getDb()); + assertThat(info).isEqualTo(expected); + } + + static final class ParsingProvider implements ArgumentsProvider { + + @Override + public Stream provideArguments(ExtensionContext context) { + return args( + // https://jdbc.postgresql.org/documentation/94/connect.html + arg("jdbc:postgresql:///") + .setShortUrl("postgresql://localhost:5432") + .setSystem("postgresql") + .setHost("localhost") + .setPort(5432) + .build(), + arg("jdbc:postgresql:///") + .setProperties(stdProps()) + .setShortUrl("postgresql://stdServerName:9999") + .setSystem("postgresql") + .setUser("stdUserName") + .setHost("stdServerName") + .setPort(9999) + .setDb("stdDatabaseName") + .build(), + arg("jdbc:postgresql://pg.host") + .setShortUrl("postgresql://pg.host:5432") + .setSystem("postgresql") + .setHost("pg.host") + .setPort(5432) + .build(), + arg("jdbc:postgresql://pg.host:11/pgdb?user=pguser&password=PW") + .setShortUrl("postgresql://pg.host:11") + .setSystem("postgresql") + .setUser("pguser") + .setHost("pg.host") + .setPort(11) + .setDb("pgdb") + .build(), + arg("jdbc:postgresql://pg.host:11/pgdb?user=pguser&password=PW") + .setProperties(stdProps()) + .setShortUrl("postgresql://pg.host:11") + .setSystem("postgresql") + .setUser("pguser") + .setHost("pg.host") + .setPort(11) + .setDb("pgdb") + .build(), + + // 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 + arg("jdbc:mysql:///") + .setShortUrl("mysql://localhost:3306") + .setSystem("mysql") + .setHost("localhost") + .setPort(3306) + .build(), + arg("jdbc:mysql:///") + .setProperties(stdProps()) + .setShortUrl("mysql://stdServerName:9999") + .setSystem("mysql") + .setUser("stdUserName") + .setHost("stdServerName") + .setPort(9999) + .setDb("stdDatabaseName") + .build(), + arg("jdbc:mysql://my.host") + .setShortUrl("mysql://my.host:3306") + .setSystem("mysql") + .setHost("my.host") + .setPort(3306) + .build(), + arg("jdbc:mysql://my.host?user=myuser&password=PW") + .setShortUrl("mysql://my.host:3306") + .setSystem("mysql") + .setUser("myuser") + .setHost("my.host") + .setPort(3306) + .build(), + arg("jdbc:mysql://my.host:22/mydb?user=myuser&password=PW") + .setShortUrl("mysql://my.host:22") + .setSystem("mysql") + .setUser("myuser") + .setHost("my.host") + .setPort(22) + .setDb("mydb") + .build(), + arg("jdbc:mysql://127.0.0.1:22/mydb?user=myuser&password=PW") + .setProperties(stdProps()) + .setShortUrl("mysql://127.0.0.1:22") + .setSystem("mysql") + .setUser("myuser") + .setHost("127.0.0.1") + .setPort(22) + .setDb("mydb") + .build(), + arg("jdbc:mysql://myuser:password@my.host:22/mydb") + .setShortUrl("mysql://my.host:22") + .setSystem("mysql") + .setUser("myuser") + .setHost("my.host") + .setPort(22) + .setDb("mydb") + .build(), + + // https://mariadb.com/kb/en/library/about-mariadb-connector-j/#connection-strings + arg("jdbc:mariadb:127.0.0.1:33/mdbdb") + .setShortUrl("mariadb://127.0.0.1:33") + .setSystem("mariadb") + .setHost("127.0.0.1") + .setPort(33) + .setDb("mdbdb") + .build(), + arg("jdbc:mariadb:localhost/mdbdb") + .setShortUrl("mariadb://localhost:3306") + .setSystem("mariadb") + .setHost("localhost") + .setPort(3306) + .setDb("mdbdb") + .build(), + arg("jdbc:mariadb:localhost/mdbdb?user=mdbuser&password=PW") + .setProperties(stdProps()) + .setShortUrl("mariadb://localhost:9999") + .setSystem("mariadb") + .setUser("mdbuser") + .setHost("localhost") + .setPort(9999) + .setDb("mdbdb") + .build(), + arg("jdbc:mariadb:localhost:33/mdbdb") + .setProperties(stdProps()) + .setShortUrl("mariadb://localhost:33") + .setSystem("mariadb") + .setUser("stdUserName") + .setHost("localhost") + .setPort(33) + .setDb("mdbdb") + .build(), + arg("jdbc:mariadb://mdb.host:33/mdbdb?user=mdbuser&password=PW") + .setShortUrl("mariadb://mdb.host:33") + .setSystem("mariadb") + .setUser("mdbuser") + .setHost("mdb.host") + .setPort(33) + .setDb("mdbdb") + .build(), + arg("jdbc:mariadb:aurora://mdb.host/mdbdb") + .setShortUrl("mariadb:aurora://mdb.host:3306") + .setSystem("mariadb") + .setSubtype("aurora") + .setHost("mdb.host") + .setPort(3306) + .setDb("mdbdb") + .build(), + arg("jdbc:mysql:aurora://mdb.host/mdbdb") + .setShortUrl("mysql:aurora://mdb.host:3306") + .setSystem("mysql") + .setSubtype("aurora") + .setHost("mdb.host") + .setPort(3306) + .setDb("mdbdb") + .build(), + arg("jdbc:mysql:failover://localhost/mdbdb?autoReconnect=true") + .setShortUrl("mysql:failover://localhost:3306") + .setSystem("mysql") + .setSubtype("failover") + .setHost("localhost") + .setPort(3306) + .setDb("mdbdb") + .build(), + arg("jdbc:mariadb:failover://mdb.host1:33,mdb.host/mdbdb?characterEncoding=utf8") + .setShortUrl("mariadb:failover://mdb.host1:33") + .setSystem("mariadb") + .setSubtype("failover") + .setHost("mdb.host1") + .setPort(33) + .setDb("mdbdb") + .build(), + arg("jdbc:mariadb:sequential://mdb.host1,mdb.host2:33/mdbdb") + .setShortUrl("mariadb:sequential://mdb.host1:3306") + .setSystem("mariadb") + .setSubtype("sequential") + .setHost("mdb.host1") + .setPort(3306) + .setDb("mdbdb") + .build(), + arg("jdbc:mariadb:loadbalance://127.0.0.1:33,mdb.host/mdbdb") + .setShortUrl("mariadb:loadbalance://127.0.0.1:33") + .setSystem("mariadb") + .setSubtype("loadbalance") + .setHost("127.0.0.1") + .setPort(33) + .setDb("mdbdb") + .build(), + arg("jdbc:mariadb:loadbalance://127.0.0.1:33/mdbdb") + .setShortUrl("mariadb:loadbalance://127.0.0.1:33") + .setSystem("mariadb") + .setSubtype("loadbalance") + .setHost("127.0.0.1") + .setPort(33) + .setDb("mdbdb") + .build(), + arg("jdbc:mariadb:loadbalance://[2001:0660:7401:0200:0000:0000:0edf:bdd7]:33,mdb.host/mdbdb") + .setShortUrl("mariadb:loadbalance://2001:0660:7401:0200:0000:0000:0edf:bdd7:33") + .setSystem("mariadb") + .setSubtype("loadbalance") + .setHost("2001:0660:7401:0200:0000:0000:0edf:bdd7") + .setPort(33) + .setDb("mdbdb") + .build(), + arg("jdbc:mysql:loadbalance://127.0.0.1,127.0.0.1:3306/mdbdb?user=mdbuser&password=PW") + .setShortUrl("mysql:loadbalance://127.0.0.1:3306") + .setSystem("mysql") + .setSubtype("loadbalance") + .setUser("mdbuser") + .setHost("127.0.0.1") + .setPort(3306) + .setDb("mdbdb") + .build(), + arg("jdbc:mariadb:replication://localhost:33,anotherhost:3306/mdbdb") + .setShortUrl("mariadb:replication://localhost:33") + .setSystem("mariadb") + .setSubtype("replication") + .setHost("localhost") + .setPort(33) + .setDb("mdbdb") + .build(), + arg("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") + .setShortUrl("mysql:replication://127.0.0.1:33") + .setSystem("mysql") + .setSubtype("replication") + .setUser("mdbuser") + .setHost("127.0.0.1") + .setPort(33) + .setDb("mdbdb") + .build(), + arg("jdbc:mysql:replication://address=(HOST=mdb.host),address=(host=anotherhost)(port=3306)(user=wrong)(password=PW)/mdbdb?user=mdbuser&password=PW") + .setShortUrl("mysql:replication://mdb.host:3306") + .setSystem("mysql") + .setSubtype("replication") + .setUser("mdbuser") + .setHost("mdb.host") + .setPort(3306) + .setDb("mdbdb") + .build(), + + // https://docs.microsoft.com/en-us/sql/connect/jdbc/building-the-connection-url + arg("jdbc:microsoft:sqlserver://;") + .setShortUrl("microsoft:sqlserver://localhost:1433") + .setSystem("mssql") + .setSubtype("sqlserver") + .setHost("localhost") + .setPort(1433) + .build(), + arg("jdbc:sqlserver://;serverName=3ffe:8311:eeee:f70f:0:5eae:10.203.31.9") + .setShortUrl("sqlserver://[3ffe:8311:eeee:f70f:0:5eae:10.203.31.9]:1433") + .setSystem("mssql") + .setHost("[3ffe:8311:eeee:f70f:0:5eae:10.203.31.9]") + .setPort(1433) + .build(), + arg("jdbc:sqlserver://;serverName=2001:0db8:85a3:0000:0000:8a2e:0370:7334") + .setShortUrl("sqlserver://[2001:0db8:85a3:0000:0000:8a2e:0370:7334]:1433") + .setSystem("mssql") + .setHost("[2001:0db8:85a3:0000:0000:8a2e:0370:7334]") + .setPort(1433) + .build(), + arg("jdbc:sqlserver://;serverName=[3ffe:8311:eeee:f70f:0:5eae:10.203.31.9]:43") + .setShortUrl("sqlserver://[3ffe:8311:eeee:f70f:0:5eae:10.203.31.9]:43") + .setSystem("mssql") + .setHost("[3ffe:8311:eeee:f70f:0:5eae:10.203.31.9]") + .setPort(43) + .build(), + arg("jdbc:sqlserver://;serverName=3ffe:8311:eeee:f70f:0:5eae:10.203.31.9\\ssinstance") + .setShortUrl("sqlserver://[3ffe:8311:eeee:f70f:0:5eae:10.203.31.9]:1433") + .setSystem("mssql") + .setHost("[3ffe:8311:eeee:f70f:0:5eae:10.203.31.9]") + .setPort(1433) + .setName("ssinstance") + .build(), + arg("jdbc:sqlserver://;serverName=[3ffe:8311:eeee:f70f:0:5eae:10.203.31.9\\ssinstance]:43") + .setShortUrl("sqlserver://[3ffe:8311:eeee:f70f:0:5eae:10.203.31.9]:43") + .setSystem("mssql") + .setHost("[3ffe:8311:eeee:f70f:0:5eae:10.203.31.9]") + .setPort(43) + .setName("ssinstance") + .build(), + arg("jdbc:microsoft:sqlserver://;") + .setProperties(stdProps()) + .setShortUrl("microsoft:sqlserver://stdServerName:9999") + .setSystem("mssql") + .setSubtype("sqlserver") + .setUser("stdUserName") + .setHost("stdServerName") + .setPort(9999) + .setDb("stdDatabaseName") + .build(), + arg("jdbc:sqlserver://ss.host\\ssinstance:44;databaseName=ssdb;user=ssuser;password=pw") + .setShortUrl("sqlserver://ss.host:44") + .setSystem("mssql") + .setUser("ssuser") + .setHost("ss.host") + .setPort(44) + .setName("ssinstance") + .setDb("ssdb") + .build(), + arg("jdbc:sqlserver://;serverName=ss.host\\ssinstance:44;DatabaseName=;") + .setShortUrl("sqlserver://ss.host:44") + .setSystem("mssql") + .setHost("ss.host") + .setPort(44) + .setName("ssinstance") + .build(), + arg("jdbc:sqlserver://ss.host;serverName=althost;DatabaseName=ssdb;") + .setShortUrl("sqlserver://ss.host:1433") + .setSystem("mssql") + .setHost("ss.host") + .setPort(1433) + .setDb("ssdb") + .build(), + arg("jdbc:microsoft:sqlserver://ss.host:44;DatabaseName=ssdb;user=ssuser;password=pw;user=ssuser2;") + .setShortUrl("microsoft:sqlserver://ss.host:44") + .setSystem("mssql") + .setSubtype("sqlserver") + .setUser("ssuser") + .setHost("ss.host") + .setPort(44) + .setDb("ssdb") + .build(), + + // http://jtds.sourceforge.net/faq.html#urlFormat + arg("jdbc:jtds:sqlserver://ss.host/ssdb") + .setShortUrl("jtds:sqlserver://ss.host:1433") + .setSystem("mssql") + .setSubtype("sqlserver") + .setHost("ss.host") + .setPort(1433) + .setDb("ssdb") + .build(), + arg("jdbc:jtds:sqlserver://ss.host:1433/ssdb") + .setShortUrl("jtds:sqlserver://ss.host:1433") + .setSystem("mssql") + .setSubtype("sqlserver") + .setHost("ss.host") + .setPort(1433) + .setDb("ssdb") + .build(), + arg("jdbc:jtds:sqlserver://ss.host:1433/ssdb;user=ssuser") + .setShortUrl("jtds:sqlserver://ss.host:1433") + .setSystem("mssql") + .setSubtype("sqlserver") + .setUser("ssuser") + .setHost("ss.host") + .setPort(1433) + .setDb("ssdb") + .build(), + arg("jdbc:jtds:sqlserver://ss.host/ssdb;instance=ssinstance") + .setShortUrl("jtds:sqlserver://ss.host:1433") + .setSystem("mssql") + .setSubtype("sqlserver") + .setHost("ss.host") + .setPort(1433) + .setName("ssinstance") + .setDb("ssdb") + .build(), + arg("jdbc:jtds:sqlserver://ss.host:1444/ssdb;instance=ssinstance") + .setShortUrl("jtds:sqlserver://ss.host:1444") + .setSystem("mssql") + .setSubtype("sqlserver") + .setHost("ss.host") + .setPort(1444) + .setName("ssinstance") + .setDb("ssdb") + .build(), + arg("jdbc:jtds:sqlserver://ss.host:1433/ssdb;instance=ssinstance;user=ssuser") + .setShortUrl("jtds:sqlserver://ss.host:1433") + .setSystem("mssql") + .setSubtype("sqlserver") + .setUser("ssuser") + .setHost("ss.host") + .setPort(1433) + .setName("ssinstance") + .setDb("ssdb") + .build(), + + // https://docs.oracle.com/cd/B28359_01/java.111/b31224/urls.htm + // https://docs.oracle.com/cd/B28359_01/java.111/b31224/jdbcthin.htm + arg("jdbc:oracle:thin:orcluser/PW@localhost:55:orclsn") + .setShortUrl("oracle:thin://localhost:55") + .setSystem("oracle") + .setSubtype("thin") + .setUser("orcluser") + .setHost("localhost") + .setPort(55) + .setName("orclsn") + .build(), + arg("jdbc:oracle:thin:orcluser/PW@//orcl.host:55/orclsn") + .setShortUrl("oracle:thin://orcl.host:55") + .setSystem("oracle") + .setSubtype("thin") + .setUser("orcluser") + .setHost("orcl.host") + .setPort(55) + .setName("orclsn") + .build(), + arg("jdbc:oracle:thin:orcluser/PW@127.0.0.1:orclsn") + .setShortUrl("oracle:thin://127.0.0.1:1521") + .setSystem("oracle") + .setSubtype("thin") + .setUser("orcluser") + .setHost("127.0.0.1") + .setPort(1521) // Default Oracle port assumed as not specified in the URL + .setName("orclsn") + .build(), + arg("jdbc:oracle:thin:orcluser/PW@//orcl.host/orclsn") + .setShortUrl("oracle:thin://orcl.host:1521") + .setSystem("oracle") + .setSubtype("thin") + .setUser("orcluser") + .setHost("orcl.host") + .setPort(1521) // Default Oracle port assumed as not specified in the URL + .setName("orclsn") + .build(), + arg("jdbc:oracle:thin:@//orcl.host:55/orclsn") + .setShortUrl("oracle:thin://orcl.host:55") + .setSystem("oracle") + .setSubtype("thin") + .setHost("orcl.host") + .setPort(55) + .setName("orclsn") + .build(), + arg("jdbc:oracle:thin:@ldap://orcl.host:55/some,cn=OracleContext,dc=com") + .setShortUrl("oracle:thin://orcl.host:55") + .setSystem("oracle") + .setSubtype("thin") + .setHost("orcl.host") + .setPort(55) + .setName("some,cn=oraclecontext,dc=com") + .build(), + arg("jdbc:oracle:thin:127.0.0.1:orclsn") + .setShortUrl("oracle:thin://127.0.0.1:1521") + .setSystem("oracle") + .setSubtype("thin") + .setHost("127.0.0.1") + .setPort(1521) // Default Oracle port assumed as not specified in the URL + .setName("orclsn") + .build(), + arg("jdbc:oracle:thin:orcl.host:orclsn") + .setProperties(stdProps()) + .setShortUrl("oracle:thin://orcl.host:9999") + .setSystem("oracle") + .setSubtype("thin") + .setUser("stdUserName") + .setHost("orcl.host") + .setPort(9999) + .setName("orclsn") + .setDb("stdDatabaseName") + .build(), + arg("jdbc:oracle:thin:@(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST=127.0.0.1)(PORT=666))" + + "(CONNECT_DATA=(SERVER=DEDICATED)(SERVICE_NAME=orclsn)))") + .setShortUrl("oracle:thin://127.0.0.1:666") + .setSystem("oracle") + .setSubtype("thin") + .setHost("127.0.0.1") + .setPort(666) + .setName("orclsn") + .build(), + + // https://docs.oracle.com/cd/B28359_01/java.111/b31224/instclnt.htm + arg("jdbc:oracle:drivertype:orcluser/PW@orcl.host:55/orclsn") + .setShortUrl("oracle:drivertype://orcl.host:55") + .setSystem("oracle") + .setSubtype("drivertype") + .setUser("orcluser") + .setHost("orcl.host") + .setPort(55) + .setName("orclsn") + .build(), + arg("jdbc:oracle:oci8:@") + .setShortUrl("oracle:oci8:") + .setSystem("oracle") + .setSubtype("oci8") + .setPort(1521) + .build(), + arg("jdbc:oracle:oci8:@") + .setProperties(stdProps()) + .setShortUrl("oracle:oci8://stdServerName:9999") + .setSystem("oracle") + .setSubtype("oci8") + .setUser("stdUserName") + .setHost("stdServerName") + .setPort(9999) + .setDb("stdDatabaseName") + .build(), + arg("jdbc:oracle:oci8:@orclsn") + .setShortUrl("oracle:oci8:") + .setSystem("oracle") + .setSubtype("oci8") + .setPort(1521) + .setName("orclsn") + .build(), + arg("jdbc:oracle:oci:@(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST=orcl.host)(PORT=55))(CONNECT_DATA=(SERVICE_NAME=orclsn)))") + .setShortUrl("oracle:oci://orcl.host:55") + .setSystem("oracle") + .setSubtype("oci") + .setHost("orcl.host") + .setPort(55) + .setName("orclsn") + .build(), + + // 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 + arg("jdbc:db2://db2.host") + .setShortUrl("db2://db2.host:50000") + .setSystem("db2") + .setHost("db2.host") + .setPort(50000) + .build(), + arg("jdbc:db2://db2.host") + .setProperties(stdProps()) + .setShortUrl("db2://db2.host:9999") + .setSystem("db2") + .setUser("stdUserName") + .setHost("db2.host") + .setPort(9999) + .setDb("stdDatabaseName") + .build(), + arg("jdbc:db2://db2.host:77/db2db:user=db2user;password=PW;") + .setShortUrl("db2://db2.host:77") + .setSystem("db2") + .setUser("db2user") + .setHost("db2.host") + .setPort(77) + .setName("db2db") + .build(), + arg("jdbc:db2://db2.host:77/db2db:user=db2user;password=PW;") + .setProperties(stdProps()) + .setShortUrl("db2://db2.host:77") + .setSystem("db2") + .setUser("db2user") + .setHost("db2.host") + .setPort(77) + .setName("db2db") + .setDb("stdDatabaseName") + .build(), + arg("jdbc:as400://ashost:66/asdb:user=asuser;password=PW;") + .setShortUrl("as400://ashost:66") + .setSystem("db2") + .setUser("asuser") + .setHost("ashost") + .setPort(66) + .setName("asdb") + .build(), + + // https://help.sap.com/viewer/0eec0d68141541d1b07893a39944924e/2.0.03/en-US/ff15928cf5594d78b841fbbe649f04b4.html + arg("jdbc:sap://sap.host") + .setShortUrl("sap://sap.host") + .setSystem("hanadb") + .setHost("sap.host") + .build(), + arg("jdbc:sap://sap.host") + .setProperties(stdProps()) + .setShortUrl("sap://sap.host:9999") + .setSystem("hanadb") + .setUser("stdUserName") + .setHost("sap.host") + .setPort(9999) + .setDb("stdDatabaseName") + .build(), + arg("jdbc:sap://sap.host:88/?databaseName=sapdb&user=sapuser&password=PW") + .setShortUrl("sap://sap.host:88") + .setSystem("hanadb") + .setUser("sapuser") + .setHost("sap.host") + .setPort(88) + .setDb("sapdb") + .build(), + + // https://www.ibm.com/support/pages/how-configure-informix-jdbc-connection-string-connect-group + arg("jdbc:informix-sqli://infxhost:99/infxdb:INFORMIXSERVER=infxsn;user=infxuser;password=PW") + .setSystem("informix-sqli") + .setUser("infxuser") + .setShortUrl("informix-sqli://infxhost:99") + .setHost("infxhost") + .setPort(99) + .setName("infxdb") + .build(), + arg("jdbc:informix-sqli://localhost:9088/stores_demo:INFORMIXSERVER=informix") + .setSystem("informix-sqli") + .setShortUrl("informix-sqli://localhost:9088") + .setHost("localhost") + .setPort(9088) + .setName("stores_demo") + .build(), + arg("jdbc:informix-sqli://infxhost:99") + .setSystem("informix-sqli") + .setShortUrl("informix-sqli://infxhost:99") + .setHost("infxhost") + .setPort(99) + .build(), + arg("jdbc:informix-sqli://infxhost/") + .setSystem("informix-sqli") + .setShortUrl("informix-sqli://infxhost:9088") + .setHost("infxhost") + .setPort(9088) + .build(), + arg("jdbc:informix-sqli:") + .setSystem("informix-sqli") + .setShortUrl("informix-sqli:") + .setPort(9088) + .build(), + + // https://www.ibm.com/docs/en/informix-servers/12.10?topic=method-format-database-urls + arg("jdbc:informix-direct://infxdb:999;user=infxuser;password=PW") + .setSystem("informix-direct") + .setShortUrl("informix-direct:") + .setUser("infxuser") + .setName("infxdb") + .build(), + arg("jdbc:informix-direct://infxdb;user=infxuser;password=PW") + .setSystem("informix-direct") + .setShortUrl("informix-direct:") + .setUser("infxuser") + .setName("infxdb") + .build(), + arg("jdbc:informix-direct://infxdb") + .setSystem("informix-direct") + .setShortUrl("informix-direct:") + .setName("infxdb") + .build(), + arg("jdbc:informix-direct:") + .setSystem("informix-direct") + .setShortUrl("informix-direct:") + .build(), + + // http://www.h2database.com/html/features.html#database_url + arg("jdbc:h2:mem:").setShortUrl("h2:mem:").setSystem("h2").setSubtype("mem").build(), + arg("jdbc:h2:mem:") + .setProperties(stdProps()) + .setShortUrl("h2:mem:") + .setSystem("h2") + .setSubtype("mem") + .setUser("stdUserName") + .setDb("stdDatabaseName") + .build(), + arg("jdbc:h2:mem:h2db") + .setShortUrl("h2:mem:") + .setSystem("h2") + .setSubtype("mem") + .setName("h2db") + .build(), + arg("jdbc:h2:tcp://h2.host:111/path/h2db;user=h2user;password=PW") + .setShortUrl("h2:tcp://h2.host:111") + .setSystem("h2") + .setSubtype("tcp") + .setUser("h2user") + .setHost("h2.host") + .setPort(111) + .setName("path/h2db") + .build(), + arg("jdbc:h2:ssl://h2.host:111/path/h2db;user=h2user;password=PW") + .setShortUrl("h2:ssl://h2.host:111") + .setSystem("h2") + .setSubtype("ssl") + .setUser("h2user") + .setHost("h2.host") + .setPort(111) + .setName("path/h2db") + .build(), + arg("jdbc:h2:/data/h2file") + .setShortUrl("h2:file:") + .setSystem("h2") + .setSubtype("file") + .setName("/data/h2file") + .build(), + arg("jdbc:h2:file:~/h2file;USER=h2user;PASSWORD=PW") + .setShortUrl("h2:file:") + .setSystem("h2") + .setSubtype("file") + .setName("~/h2file") + .build(), + arg("jdbc:h2:file:/data/h2file") + .setShortUrl("h2:file:") + .setSystem("h2") + .setSubtype("file") + .setName("/data/h2file") + .build(), + arg("jdbc:h2:file:C:/data/h2file") + .setShortUrl("h2:file:") + .setSystem("h2") + .setSubtype("file") + .setName("c:/data/h2file") + .build(), + arg("jdbc:h2:zip:~/db.zip!/h2zip") + .setShortUrl("h2:zip:") + .setSystem("h2") + .setSubtype("zip") + .setName("~/db.zip!/h2zip") + .build(), + + // http://hsqldb.org/doc/2.0/guide/dbproperties-chapt.html + arg("jdbc:hsqldb:hsdb") + .setShortUrl("hsqldb:mem:") + .setSystem("hsqldb") + .setSubtype("mem") + .setUser("SA") + .setName("hsdb") + .build(), + arg("jdbc:hsqldb:hsdb") + .setProperties(stdProps()) + .setShortUrl("hsqldb:mem:") + .setSystem("hsqldb") + .setSubtype("mem") + .setUser("stdUserName") + .setName("hsdb") + .setDb("stdDatabaseName") + .build(), + arg("jdbc:hsqldb:mem:hsdb") + .setShortUrl("hsqldb:mem:") + .setSystem("hsqldb") + .setSubtype("mem") + .setUser("SA") + .setName("hsdb") + .build(), + arg("jdbc:hsqldb:mem:hsdb;shutdown=true") + .setShortUrl("hsqldb:mem:") + .setSystem("hsqldb") + .setSubtype("mem") + .setUser("SA") + .setName("hsdb") + .build(), + arg("jdbc:hsqldb:mem:hsdb?shutdown=true") + .setShortUrl("hsqldb:mem:") + .setSystem("hsqldb") + .setSubtype("mem") + .setUser("SA") + .setName("hsdb") + .build(), + arg("jdbc:hsqldb:file:hsdb") + .setShortUrl("hsqldb:file:") + .setSystem("hsqldb") + .setSubtype("file") + .setUser("SA") + .setName("hsdb") + .build(), + arg("jdbc:hsqldb:file:hsdb;user=aUserName;password=3xLVz") + .setShortUrl("hsqldb:file:") + .setSystem("hsqldb") + .setSubtype("file") + .setUser("SA") + .setName("hsdb") + .build(), + arg("jdbc:hsqldb:file:hsdb;create=false?user=aUserName&password=3xLVz") + .setShortUrl("hsqldb:file:") + .setSystem("hsqldb") + .setSubtype("file") + .setUser("SA") + .setName("hsdb") + .build(), + arg("jdbc:hsqldb:file:/loc/hsdb") + .setShortUrl("hsqldb:file:") + .setSystem("hsqldb") + .setSubtype("file") + .setUser("SA") + .setName("/loc/hsdb") + .build(), + arg("jdbc:hsqldb:file:C:/hsdb") + .setShortUrl("hsqldb:file:") + .setSystem("hsqldb") + .setSubtype("file") + .setUser("SA") + .setName("c:/hsdb") + .build(), + arg("jdbc:hsqldb:res:hsdb") + .setShortUrl("hsqldb:res:") + .setSystem("hsqldb") + .setSubtype("res") + .setUser("SA") + .setName("hsdb") + .build(), + arg("jdbc:hsqldb:res:/cp/hsdb") + .setShortUrl("hsqldb:res:") + .setSystem("hsqldb") + .setSubtype("res") + .setUser("SA") + .setName("/cp/hsdb") + .build(), + arg("jdbc:hsqldb:hsql://hs.host:333/hsdb") + .setShortUrl("hsqldb:hsql://hs.host:333") + .setSystem("hsqldb") + .setSubtype("hsql") + .setUser("SA") + .setHost("hs.host") + .setPort(333) + .setName("hsdb") + .build(), + arg("jdbc:hsqldb:hsqls://hs.host/hsdb") + .setShortUrl("hsqldb:hsqls://hs.host:9001") + .setSystem("hsqldb") + .setSubtype("hsqls") + .setUser("SA") + .setHost("hs.host") + .setPort(9001) + .setName("hsdb") + .build(), + arg("jdbc:hsqldb:http://hs.host") + .setShortUrl("hsqldb:http://hs.host:80") + .setSystem("hsqldb") + .setSubtype("http") + .setUser("SA") + .setHost("hs.host") + .setPort(80) + .build(), + arg("jdbc:hsqldb:http://hs.host:333/hsdb") + .setShortUrl("hsqldb:http://hs.host:333") + .setSystem("hsqldb") + .setSubtype("http") + .setUser("SA") + .setHost("hs.host") + .setPort(333) + .setName("hsdb") + .build(), + arg("jdbc:hsqldb:https://127.0.0.1/hsdb") + .setShortUrl("hsqldb:https://127.0.0.1:443") + .setSystem("hsqldb") + .setSubtype("https") + .setUser("SA") + .setHost("127.0.0.1") + .setPort(443) + .setName("hsdb") + .build(), + + // https://db.apache.org/derby/papers/DerbyClientSpec.html#Connection+URL+Format + // https://db.apache.org/derby/docs/10.8/devguide/cdevdvlp34964.html + arg("jdbc:derby:derbydb") + .setShortUrl("derby:directory:") + .setSystem("derby") + .setSubtype("directory") + .setUser("APP") + .setName("derbydb") + .build(), + arg("jdbc:derby:derbydb") + .setProperties(stdProps()) + .setShortUrl("derby:directory:") + .setSystem("derby") + .setSubtype("directory") + .setUser("stdUserName") + .setName("derbydb") + .setDb("stdDatabaseName") + .build(), + arg("jdbc:derby:derbydb;user=derbyuser;password=pw") + .setShortUrl("derby:directory:") + .setSystem("derby") + .setSubtype("directory") + .setUser("derbyuser") + .setName("derbydb") + .build(), + arg("jdbc:derby:memory:derbydb") + .setShortUrl("derby:memory:") + .setSystem("derby") + .setSubtype("memory") + .setUser("APP") + .setName("derbydb") + .build(), + arg("jdbc:derby:memory:;databaseName=derbydb") + .setShortUrl("derby:memory:") + .setSystem("derby") + .setSubtype("memory") + .setUser("APP") + .setDb("derbydb") + .build(), + arg("jdbc:derby:memory:derbydb;databaseName=altdb") + .setShortUrl("derby:memory:") + .setSystem("derby") + .setSubtype("memory") + .setUser("APP") + .setName("derbydb") + .setDb("altdb") + .build(), + arg("jdbc:derby:memory:derbydb;user=derbyuser;password=pw") + .setShortUrl("derby:memory:") + .setSystem("derby") + .setSubtype("memory") + .setUser("derbyuser") + .setName("derbydb") + .build(), + arg("jdbc:derby://derby.host:222/memory:derbydb;create=true") + .setShortUrl("derby:network://derby.host:222") + .setSystem("derby") + .setSubtype("network") + .setUser("APP") + .setHost("derby.host") + .setPort(222) + .setName("derbydb") + .build(), + arg("jdbc:derby://derby.host/memory:derbydb;create=true;user=derbyuser;password=pw") + .setShortUrl("derby:network://derby.host:1527") + .setSystem("derby") + .setSubtype("network") + .setUser("derbyuser") + .setHost("derby.host") + .setPort(1527) + .setName("derbydb") + .build(), + arg("jdbc:derby://127.0.0.1:1527/memory:derbydb;create=true;user=derbyuser;password=pw") + .setShortUrl("derby:network://127.0.0.1:1527") + .setSystem("derby") + .setSubtype("network") + .setUser("derbyuser") + .setHost("127.0.0.1") + .setPort(1527) + .setName("derbydb") + .build(), + arg("jdbc:derby:directory:derbydb;user=derbyuser;password=pw") + .setShortUrl("derby:directory:") + .setSystem("derby") + .setSubtype("directory") + .setUser("derbyuser") + .setName("derbydb") + .build(), + arg("jdbc:derby:classpath:/some/derbydb;user=derbyuser;password=pw") + .setShortUrl("derby:classpath:") + .setSystem("derby") + .setSubtype("classpath") + .setUser("derbyuser") + .setName("/some/derbydb") + .build(), + arg("jdbc:derby:jar:/derbydb;user=derbyuser;password=pw") + .setShortUrl("derby:jar:") + .setSystem("derby") + .setSubtype("jar") + .setUser("derbyuser") + .setName("/derbydb") + .build(), + arg("jdbc:derby:jar:(~/path/to/db.jar)/other/derbydb;user=derbyuser;password=pw") + .setShortUrl("derby:jar:") + .setSystem("derby") + .setSubtype("jar") + .setUser("derbyuser") + .setName("(~/path/to/db.jar)/other/derbydb") + .build(), + + // https://docs.progress.com/bundle/datadirect-connect-jdbc-51/page/URL-Formats-DataDirect-Connect-for-JDBC-Drivers.html + arg("jdbc:datadirect:sqlserver://server_name:1433;DatabaseName=dbname") + .setShortUrl("datadirect:sqlserver://server_name:1433") + .setSystem("mssql") + .setSubtype("sqlserver") + .setHost("server_name") + .setPort(1433) + .setDb("dbname") + .build(), + arg("jdbc:datadirect:oracle://server_name:1521;ServiceName=your_servicename") + .setShortUrl("datadirect:oracle://server_name:1521") + .setSystem("oracle") + .setSubtype("oracle") + .setHost("server_name") + .setPort(1521) + .build(), + arg("jdbc:datadirect:mysql://server_name:3306") + .setShortUrl("datadirect:mysql://server_name:3306") + .setSystem("mysql") + .setSubtype("mysql") + .setHost("server_name") + .setPort(3306) + .build(), + arg("jdbc:datadirect:postgresql://server_name:5432;DatabaseName=dbname") + .setShortUrl("datadirect:postgresql://server_name:5432") + .setSystem("postgresql") + .setSubtype("postgresql") + .setHost("server_name") + .setPort(5432) + .setDb("dbname") + .build(), + arg("jdbc:datadirect:db2://server_name:50000;DatabaseName=dbname") + .setShortUrl("datadirect:db2://server_name:50000") + .setSystem("db2") + .setSubtype("db2") + .setHost("server_name") + .setPort(50000) + .setDb("dbname") + .build(), + + // "the TIBCO JDBC drivers are based on the Progress DataDirect Connect drivers" + // https://community.jaspersoft.com/documentation/tibco-jasperreports-server-administrator-guide/v601/working-data-sources + arg("jdbc:tibcosoftware:sqlserver://server_name:1433;DatabaseName=dbname") + .setShortUrl("tibcosoftware:sqlserver://server_name:1433") + .setSystem("mssql") + .setSubtype("sqlserver") + .setHost("server_name") + .setPort(1433) + .setDb("dbname") + .build(), + arg("jdbc:tibcosoftware:oracle://server_name:1521;ServiceName=your_servicename") + .setShortUrl("tibcosoftware:oracle://server_name:1521") + .setSystem("oracle") + .setSubtype("oracle") + .setHost("server_name") + .setPort(1521) + .build(), + arg("jdbc:tibcosoftware:mysql://server_name:3306") + .setShortUrl("tibcosoftware:mysql://server_name:3306") + .setSystem("mysql") + .setSubtype("mysql") + .setHost("server_name") + .setPort(3306) + .build(), + arg("jdbc:tibcosoftware:postgresql://server_name:5432;DatabaseName=dbname") + .setShortUrl("tibcosoftware:postgresql://server_name:5432") + .setSystem("postgresql") + .setSubtype("postgresql") + .setHost("server_name") + .setPort(5432) + .setDb("dbname") + .build(), + arg("jdbc:tibcosoftware:db2://server_name:50000;DatabaseName=dbname") + .setShortUrl("tibcosoftware:db2://server_name:50000") + .setSystem("db2") + .setSubtype("db2") + .setHost("server_name") + .setPort(50000) + .setDb("dbname") + .build(), + + // https://docs.aws.amazon.com/secretsmanager/latest/userguide/retrieving-secrets_jdbc.html + arg("jdbc-secretsmanager:mysql://example.com:50000") + .setShortUrl("mysql://example.com:50000") + .setSystem("mysql") + .setHost("example.com") + .setPort(50000) + .build(), + arg("jdbc-secretsmanager:postgresql://example.com:50000/dbname") + .setShortUrl("postgresql://example.com:50000") + .setSystem("postgresql") + .setHost("example.com") + .setPort(50000) + .setDb("dbname") + .build(), + arg("jdbc-secretsmanager:oracle:thin:@example.com:50000/ORCL") + .setShortUrl("oracle:thin://example.com:50000") + .setSystem("oracle") + .setSubtype("thin") + .setHost("example.com") + .setPort(50000) + .setName("orcl") + .build(), + arg("jdbc-secretsmanager:sqlserver://example.com:50000") + .setShortUrl("sqlserver://example.com:50000") + .setSystem("mssql") + .setHost("example.com") + .setPort(50000) + .build()); + } + } + + static class ParseTestArgument { + final String url; + final Properties properties; + final DbInfo dbInfo; + + ParseTestArgument(ParseTestArgumentBuilder builder) { + this.url = builder.url; + this.properties = builder.properties; + this.dbInfo = + DbInfo.builder() + .shortUrl(builder.shortUrl) + .system(builder.system) + .subtype(builder.subtype) + .user(builder.user) + .name(builder.name) + .db(builder.db) + .host(builder.host) + .port(builder.port) + .build(); + } + + @Override + public String toString() { + return dbInfo.getSystem() + ":" + dbInfo.getSubtype() + " parsing of " + url; + } + } + + static class ParseTestArgumentBuilder { + String url; + Properties properties; + String shortUrl; + String system; + String subtype; + String user; + String host; + Integer port; + String name; + String db; + + ParseTestArgumentBuilder(String url) { + this.url = url; + } + + ParseTestArgumentBuilder setProperties(Properties properties) { + this.properties = properties; + return this; + } + + ParseTestArgumentBuilder setShortUrl(String shortUrl) { + this.shortUrl = shortUrl; + return this; + } + + ParseTestArgumentBuilder setSystem(String system) { + this.system = system; + return this; + } + + ParseTestArgumentBuilder setSubtype(String subtype) { + this.subtype = subtype; + return this; + } + + ParseTestArgumentBuilder setUser(String user) { + this.user = user; + return this; + } + + ParseTestArgumentBuilder setHost(String host) { + this.host = host; + return this; + } + + ParseTestArgumentBuilder setPort(Integer port) { + this.port = port; + return this; + } + + ParseTestArgumentBuilder setName(String name) { + this.name = name; + return this; + } + + ParseTestArgumentBuilder setDb(String db) { + this.db = db; + return this; + } + + ParseTestArgument build() { + return new ParseTestArgument(this); + } + } + + private static ParseTestArgumentBuilder arg(String url) { + return new ParseTestArgumentBuilder(url); + } + + static Stream args(ParseTestArgument... testArguments) { + List list = new ArrayList<>(); + for (ParseTestArgument arg : testArguments) { + list.add(arguments(arg)); + } + return list.stream(); + } +} diff --git a/instrumentation/jdbc/library/src/test/java/io/opentelemetry/instrumentation/jdbc/internal/OpenTelemetryConnectionTest.java b/instrumentation/jdbc/library/src/test/java/io/opentelemetry/instrumentation/jdbc/internal/OpenTelemetryConnectionTest.java new file mode 100644 index 000000000000..5405ddd14026 --- /dev/null +++ b/instrumentation/jdbc/library/src/test/java/io/opentelemetry/instrumentation/jdbc/internal/OpenTelemetryConnectionTest.java @@ -0,0 +1,201 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.jdbc.internal; + +import static io.opentelemetry.instrumentation.jdbc.internal.JdbcInstrumenterFactory.createStatementInstrumenter; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.context.propagation.ContextPropagators; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import io.opentelemetry.instrumentation.jdbc.TestConnection; +import io.opentelemetry.instrumentation.jdbc.internal.dbinfo.DbInfo; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.LibraryInstrumentationExtension; +import io.opentelemetry.semconv.ServerAttributes; +import io.opentelemetry.semconv.incubating.DbIncubatingAttributes; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.sql.Statement; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +class OpenTelemetryConnectionTest { + + @RegisterExtension + private static final InstrumentationExtension testing = LibraryInstrumentationExtension.create(); + + @Test + void testVerifyCreateStatement() throws SQLException { + Instrumenter instrumenter = + createStatementInstrumenter(testing.getOpenTelemetry()); + DbInfo dbInfo = getDbInfo(); + OpenTelemetryConnection connection = + new OpenTelemetryConnection(new TestConnection(), dbInfo, instrumenter); + String query = "SELECT * FROM users"; + Statement statement = connection.createStatement(); + + testing.runWithSpan( + "parent", + () -> { + assertThat(statement.execute(query)).isTrue(); + }); + + jdbcTraceAssertion(dbInfo, query); + + statement.close(); + connection.close(); + } + + @SuppressWarnings("unchecked") + @Test + void testVerifyCreateStatementReturnsOtelWrapper() throws Exception { + OpenTelemetry ot = OpenTelemetry.propagating(ContextPropagators.noop()); + Instrumenter instrumenter = createStatementInstrumenter(ot); + OpenTelemetryConnection connection = + new OpenTelemetryConnection(new TestConnection(), DbInfo.DEFAULT, instrumenter); + + assertThat(connection.createStatement()).isInstanceOf(OpenTelemetryStatement.class); + assertThat(connection.createStatement(0, 0)).isInstanceOf(OpenTelemetryStatement.class); + assertThat(connection.createStatement(0, 0, 0)).isInstanceOf(OpenTelemetryStatement.class); + assertThat(((OpenTelemetryStatement) connection.createStatement()).instrumenter) + .isEqualTo(instrumenter); + + connection.close(); + } + + @Test + void testVerifyPrepareStatement() throws SQLException { + Instrumenter instrumenter = + createStatementInstrumenter(testing.getOpenTelemetry()); + DbInfo dbInfo = getDbInfo(); + OpenTelemetryConnection connection = + new OpenTelemetryConnection(new TestConnection(), dbInfo, instrumenter); + String query = "SELECT * FROM users"; + PreparedStatement statement = connection.prepareStatement(query); + + testing.runWithSpan( + "parent", + () -> { + assertThat(statement.execute()).isTrue(); + }); + + jdbcTraceAssertion(dbInfo, query); + + statement.close(); + connection.close(); + } + + @SuppressWarnings("unchecked") + @Test + void testVerifyPrepareStatementReturnsOtelWrapper() throws Exception { + OpenTelemetry ot = OpenTelemetry.propagating(ContextPropagators.noop()); + Instrumenter instrumenter = createStatementInstrumenter(ot); + OpenTelemetryConnection connection = + new OpenTelemetryConnection(new TestConnection(), DbInfo.DEFAULT, instrumenter); + String query = "SELECT * FROM users"; + + assertThat(connection.prepareStatement(query)) + .isInstanceOf(OpenTelemetryPreparedStatement.class); + assertThat(connection.prepareStatement(query, new int[] {0})) + .isInstanceOf(OpenTelemetryPreparedStatement.class); + assertThat(connection.prepareStatement(query, new String[] {"id"})) + .isInstanceOf(OpenTelemetryPreparedStatement.class); + assertThat(connection.prepareStatement(query, 0)) + .isInstanceOf(OpenTelemetryPreparedStatement.class); + assertThat(connection.prepareStatement(query, 0, 0)) + .isInstanceOf(OpenTelemetryPreparedStatement.class); + assertThat(connection.prepareStatement(query, 0, 0, 0)) + .isInstanceOf(OpenTelemetryPreparedStatement.class); + assertThat( + ((OpenTelemetryStatement) connection.prepareStatement(query)).instrumenter) + .isEqualTo(instrumenter); + + connection.close(); + } + + @Test + void testVerifyPrepareCall() throws SQLException { + Instrumenter instrumenter = + createStatementInstrumenter(testing.getOpenTelemetry()); + DbInfo dbInfo = getDbInfo(); + OpenTelemetryConnection connection = + new OpenTelemetryConnection(new TestConnection(), dbInfo, instrumenter); + String query = "SELECT * FROM users"; + PreparedStatement statement = connection.prepareCall(query); + + testing.runWithSpan( + "parent", + () -> { + assertThat(statement.execute()).isTrue(); + }); + + jdbcTraceAssertion(dbInfo, query); + + statement.close(); + connection.close(); + } + + @SuppressWarnings("unchecked") + @Test + void testVerifyPrepareCallReturnsOtelWrapper() throws Exception { + OpenTelemetry ot = OpenTelemetry.propagating(ContextPropagators.noop()); + Instrumenter instrumenter = createStatementInstrumenter(ot); + OpenTelemetryConnection connection = + new OpenTelemetryConnection(new TestConnection(), DbInfo.DEFAULT, instrumenter); + String query = "SELECT * FROM users"; + + assertThat(connection.prepareCall(query)).isInstanceOf(OpenTelemetryCallableStatement.class); + + assertThat(connection.prepareCall(query)).isInstanceOf(OpenTelemetryCallableStatement.class); + assertThat(connection.prepareCall(query, 0, 0)) + .isInstanceOf(OpenTelemetryCallableStatement.class); + assertThat(connection.prepareCall(query, 0, 0, 0)) + .isInstanceOf(OpenTelemetryCallableStatement.class); + assertThat(((OpenTelemetryStatement) connection.prepareCall(query)).instrumenter) + .isEqualTo(instrumenter); + + connection.close(); + } + + private static DbInfo getDbInfo() { + return DbInfo.builder() + .system("my_system") + .subtype("my_sub_type") + .shortUrl("my_connection_string") + .user("my_user") + .name("my_name") + .db("my_db") + .host("my_host") + .port(1234) + .build(); + } + + @SuppressWarnings("deprecation") // old semconv + private static void jdbcTraceAssertion(DbInfo dbInfo, String query) { + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(), + span -> + span.hasName("SELECT my_name.users") + .hasKind(SpanKind.CLIENT) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo(DbIncubatingAttributes.DB_SYSTEM, dbInfo.getSystem()), + equalTo(DbIncubatingAttributes.DB_NAME, dbInfo.getName()), + equalTo(DbIncubatingAttributes.DB_USER, dbInfo.getUser()), + equalTo( + DbIncubatingAttributes.DB_CONNECTION_STRING, dbInfo.getShortUrl()), + equalTo(DbIncubatingAttributes.DB_STATEMENT, query), + equalTo(DbIncubatingAttributes.DB_OPERATION, "SELECT"), + equalTo(DbIncubatingAttributes.DB_SQL_TABLE, "users"), + equalTo(ServerAttributes.SERVER_ADDRESS, dbInfo.getHost()), + equalTo(ServerAttributes.SERVER_PORT, dbInfo.getPort())))); + } +} diff --git a/instrumentation/jedis/jedis-1.4/javaagent/build.gradle.kts b/instrumentation/jedis/jedis-1.4/javaagent/build.gradle.kts index 2e446ca8b53a..4af0c8ce1d33 100644 --- a/instrumentation/jedis/jedis-1.4/javaagent/build.gradle.kts +++ b/instrumentation/jedis/jedis-1.4/javaagent/build.gradle.kts @@ -19,14 +19,31 @@ dependencies { implementation(project(":instrumentation:jedis:jedis-common:javaagent")) + testImplementation(project(":instrumentation:jedis:jedis-1.4:testing")) + testInstrumentation(project(":instrumentation:jedis:jedis-3.0:javaagent")) testInstrumentation(project(":instrumentation:jedis:jedis-4.0:javaagent")) latestDepTestLibrary("redis.clients:jedis:2.+") // see jedis-3.0 module } +testing { + suites { + val version272 by registering(JvmTestSuite::class) { + dependencies { + implementation("redis.clients:jedis:2.7.2") + implementation(project(":instrumentation:jedis:jedis-1.4:testing")) + } + } + } +} + tasks { - test { + withType().configureEach { usesService(gradle.sharedServices.registrations["testcontainersBuildService"].service) } + + check { + dependsOn(testing.suites) + } } diff --git a/instrumentation/jedis/jedis-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v1_4/JedisConnectionInstrumentation.java b/instrumentation/jedis/jedis-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v1_4/JedisConnectionInstrumentation.java index c2386d5821e7..fcd6822e99bb 100644 --- a/instrumentation/jedis/jedis-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v1_4/JedisConnectionInstrumentation.java +++ b/instrumentation/jedis/jedis-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v1_4/JedisConnectionInstrumentation.java @@ -11,6 +11,7 @@ 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.namedOneOf; import static net.bytebuddy.matcher.ElementMatchers.takesArgument; import static net.bytebuddy.matcher.ElementMatchers.takesArguments; @@ -21,6 +22,7 @@ import io.opentelemetry.javaagent.instrumentation.jedis.JedisRequestContext; import net.bytebuddy.asm.Advice; import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.implementation.bytecode.assign.Assigner; import net.bytebuddy.matcher.ElementMatcher; import redis.clients.jedis.Connection; import redis.clients.jedis.Protocol; @@ -37,13 +39,23 @@ public void transform(TypeTransformer transformer) { isMethod() .and(named("sendCommand")) .and(takesArguments(1)) - .and(takesArgument(0, named("redis.clients.jedis.Protocol$Command"))), + .and( + takesArgument( + 0, + namedOneOf( + "redis.clients.jedis.Protocol$Command", + "redis.clients.jedis.ProtocolCommand"))), this.getClass().getName() + "$SendCommandNoArgsAdvice"); transformer.applyAdviceToMethod( isMethod() .and(named("sendCommand")) .and(takesArguments(2)) - .and(takesArgument(0, named("redis.clients.jedis.Protocol$Command"))) + .and( + takesArgument( + 0, + namedOneOf( + "redis.clients.jedis.Protocol$Command", + "redis.clients.jedis.ProtocolCommand"))) .and(takesArgument(1, is(byte[][].class))), this.getClass().getName() + "$SendCommandWithArgsAdvice"); } @@ -54,7 +66,7 @@ public static class SendCommandNoArgsAdvice { @Advice.OnMethodEnter(suppress = Throwable.class) public static void onEnter( @Advice.This Connection connection, - @Advice.Argument(0) Protocol.Command command, + @Advice.Argument(value = 0, typing = Assigner.Typing.DYNAMIC) Protocol.Command command, @Advice.Local("otelJedisRequest") JedisRequest request, @Advice.Local("otelContext") Context context, @Advice.Local("otelScope") Scope scope) { @@ -89,7 +101,7 @@ 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(value = 0, typing = Assigner.Typing.DYNAMIC) Protocol.Command command, @Advice.Argument(1) byte[][] args, @Advice.Local("otelJedisRequest") JedisRequest request, @Advice.Local("otelContext") Context context, diff --git a/instrumentation/jedis/jedis-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v1_4/JedisDbAttributesGetter.java b/instrumentation/jedis/jedis-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v1_4/JedisDbAttributesGetter.java index 4d85176c1d17..8ad1c67c30cb 100644 --- a/instrumentation/jedis/jedis-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v1_4/JedisDbAttributesGetter.java +++ b/instrumentation/jedis/jedis-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v1_4/JedisDbAttributesGetter.java @@ -5,20 +5,20 @@ package io.opentelemetry.javaagent.instrumentation.jedis.v1_4; -import io.opentelemetry.instrumentation.api.db.RedisCommandSanitizer; -import io.opentelemetry.instrumentation.api.instrumenter.db.DbClientAttributesGetter; -import io.opentelemetry.javaagent.bootstrap.internal.CommonConfig; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.opentelemetry.instrumentation.api.incubator.semconv.db.DbClientAttributesGetter; +import io.opentelemetry.instrumentation.api.incubator.semconv.db.RedisCommandSanitizer; +import io.opentelemetry.javaagent.bootstrap.internal.AgentCommonConfig; +import io.opentelemetry.semconv.incubating.DbIncubatingAttributes; import javax.annotation.Nullable; final class JedisDbAttributesGetter implements DbClientAttributesGetter { private static final RedisCommandSanitizer sanitizer = - RedisCommandSanitizer.create(CommonConfig.get().isStatementSanitizationEnabled()); + RedisCommandSanitizer.create(AgentCommonConfig.get().isStatementSanitizationEnabled()); @Override public String getSystem(JedisRequest request) { - return SemanticAttributes.DbSystemValues.REDIS; + return DbIncubatingAttributes.DbSystemValues.REDIS; } @Override diff --git a/instrumentation/jedis/jedis-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v1_4/JedisNetAttributesGetter.java b/instrumentation/jedis/jedis-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v1_4/JedisNetworkAttributesGetter.java similarity index 68% rename from instrumentation/jedis/jedis-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v1_4/JedisNetAttributesGetter.java rename to instrumentation/jedis/jedis-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v1_4/JedisNetworkAttributesGetter.java index acac62885b27..33d8ad53fd9c 100644 --- a/instrumentation/jedis/jedis-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v1_4/JedisNetAttributesGetter.java +++ b/instrumentation/jedis/jedis-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v1_4/JedisNetworkAttributesGetter.java @@ -5,9 +5,9 @@ package io.opentelemetry.javaagent.instrumentation.jedis.v1_4; -import io.opentelemetry.instrumentation.api.instrumenter.net.NetClientAttributesGetter; +import io.opentelemetry.instrumentation.api.semconv.network.ServerAttributesGetter; -final class JedisNetAttributesGetter implements NetClientAttributesGetter { +final class JedisNetworkAttributesGetter implements ServerAttributesGetter { @Override public String getServerAddress(JedisRequest request) { diff --git a/instrumentation/jedis/jedis-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v1_4/JedisSingletons.java b/instrumentation/jedis/jedis-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v1_4/JedisSingletons.java index 4ed9529b7b3a..6bf94ca5988d 100644 --- a/instrumentation/jedis/jedis-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v1_4/JedisSingletons.java +++ b/instrumentation/jedis/jedis-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v1_4/JedisSingletons.java @@ -6,13 +6,13 @@ package io.opentelemetry.javaagent.instrumentation.jedis.v1_4; import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.instrumentation.api.incubator.semconv.db.DbClientAttributesExtractor; +import io.opentelemetry.instrumentation.api.incubator.semconv.db.DbClientSpanNameExtractor; +import io.opentelemetry.instrumentation.api.incubator.semconv.net.PeerServiceAttributesExtractor; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; import io.opentelemetry.instrumentation.api.instrumenter.SpanKindExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.db.DbClientAttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.db.DbClientSpanNameExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.net.NetClientAttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.net.PeerServiceAttributesExtractor; -import io.opentelemetry.javaagent.bootstrap.internal.CommonConfig; +import io.opentelemetry.instrumentation.api.semconv.network.ServerAttributesExtractor; +import io.opentelemetry.javaagent.bootstrap.internal.AgentCommonConfig; public final class JedisSingletons { private static final String INSTRUMENTATION_NAME = "io.opentelemetry.jedis-1.4"; @@ -21,7 +21,7 @@ public final class JedisSingletons { static { JedisDbAttributesGetter dbAttributesGetter = new JedisDbAttributesGetter(); - JedisNetAttributesGetter netAttributesGetter = new JedisNetAttributesGetter(); + JedisNetworkAttributesGetter netAttributesGetter = new JedisNetworkAttributesGetter(); INSTRUMENTER = Instrumenter.builder( @@ -29,10 +29,10 @@ public final class JedisSingletons { INSTRUMENTATION_NAME, DbClientSpanNameExtractor.create(dbAttributesGetter)) .addAttributesExtractor(DbClientAttributesExtractor.create(dbAttributesGetter)) - .addAttributesExtractor(NetClientAttributesExtractor.create(netAttributesGetter)) + .addAttributesExtractor(ServerAttributesExtractor.create(netAttributesGetter)) .addAttributesExtractor( PeerServiceAttributesExtractor.create( - netAttributesGetter, CommonConfig.get().getPeerServiceMapping())) + netAttributesGetter, AgentCommonConfig.get().getPeerServiceResolver())) .buildInstrumenter(SpanKindExtractor.alwaysClient()); } diff --git a/instrumentation/jedis/jedis-1.4/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/jedis/v1_4/JedisClientTest.java b/instrumentation/jedis/jedis-1.4/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/jedis/v1_4/JedisClientTest.java index 9300eb362c87..99b5bfc6272d 100644 --- a/instrumentation/jedis/jedis-1.4/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/jedis/v1_4/JedisClientTest.java +++ b/instrumentation/jedis/jedis-1.4/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/jedis/v1_4/JedisClientTest.java @@ -5,129 +5,6 @@ package io.opentelemetry.javaagent.instrumentation.jedis.v1_4; -import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; -import static org.assertj.core.api.Assertions.assertThat; +import io.opentelemetry.javaagent.instrumentation.jedis.AbstractJedisTest; -import io.opentelemetry.api.trace.SpanKind; -import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; -import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.RegisterExtension; -import org.testcontainers.containers.GenericContainer; -import redis.clients.jedis.Jedis; - -class JedisClientTest { - @RegisterExtension - static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); - - static GenericContainer redisServer = - new GenericContainer<>("redis:6.2.3-alpine").withExposedPorts(6379); - - static int port; - - static Jedis jedis; - - @BeforeAll - static void setupSpec() { - redisServer.start(); - port = redisServer.getMappedPort(6379); - jedis = new Jedis("localhost", port); - } - - @AfterAll - static void cleanupSpec() { - redisServer.stop(); - } - - @BeforeEach - void setup() { - jedis.flushAll(); - testing.clearData(); - } - - @Test - void setCommand() { - jedis.set("foo", "bar"); - - testing.waitAndAssertTraces( - trace -> - trace.hasSpansSatisfyingExactly( - span -> - span.hasName("SET") - .hasKind(SpanKind.CLIENT) - .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.DB_SYSTEM, "redis"), - equalTo(SemanticAttributes.DB_STATEMENT, "SET foo ?"), - equalTo(SemanticAttributes.DB_OPERATION, "SET"), - equalTo(SemanticAttributes.NET_PEER_NAME, "localhost"), - equalTo(SemanticAttributes.NET_PEER_PORT, port)))); - } - - @Test - void getCommand() { - jedis.set("foo", "bar"); - String value = jedis.get("foo"); - - assertThat(value).isEqualTo("bar"); - - testing.waitAndAssertTraces( - trace -> - trace.hasSpansSatisfyingExactly( - span -> - span.hasName("SET") - .hasKind(SpanKind.CLIENT) - .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.DB_SYSTEM, "redis"), - equalTo(SemanticAttributes.DB_STATEMENT, "SET foo ?"), - equalTo(SemanticAttributes.DB_OPERATION, "SET"), - equalTo(SemanticAttributes.NET_PEER_NAME, "localhost"), - equalTo(SemanticAttributes.NET_PEER_PORT, port))), - trace -> - trace.hasSpansSatisfyingExactly( - span -> - span.hasName("GET") - .hasKind(SpanKind.CLIENT) - .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.DB_SYSTEM, "redis"), - equalTo(SemanticAttributes.DB_STATEMENT, "GET foo"), - equalTo(SemanticAttributes.DB_OPERATION, "GET"), - equalTo(SemanticAttributes.NET_PEER_NAME, "localhost"), - equalTo(SemanticAttributes.NET_PEER_PORT, port)))); - } - - @Test - void commandWithNoArguments() { - jedis.set("foo", "bar"); - String value = jedis.randomKey(); - - assertThat(value).isEqualTo("foo"); - - testing.waitAndAssertTraces( - trace -> - trace.hasSpansSatisfyingExactly( - span -> - span.hasName("SET") - .hasKind(SpanKind.CLIENT) - .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.DB_SYSTEM, "redis"), - equalTo(SemanticAttributes.DB_STATEMENT, "SET foo ?"), - equalTo(SemanticAttributes.DB_OPERATION, "SET"), - equalTo(SemanticAttributes.NET_PEER_NAME, "localhost"), - equalTo(SemanticAttributes.NET_PEER_PORT, port))), - trace -> - trace.hasSpansSatisfyingExactly( - span -> - span.hasName("RANDOMKEY") - .hasKind(SpanKind.CLIENT) - .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.DB_SYSTEM, "redis"), - equalTo(SemanticAttributes.DB_STATEMENT, "RANDOMKEY"), - equalTo(SemanticAttributes.DB_OPERATION, "RANDOMKEY"), - equalTo(SemanticAttributes.NET_PEER_NAME, "localhost"), - equalTo(SemanticAttributes.NET_PEER_PORT, port)))); - } -} +class JedisClientTest extends AbstractJedisTest {} diff --git a/instrumentation/jedis/jedis-1.4/javaagent/src/version272/java/io/opentelemetry/javaagent/instrumentation/jedis/v2_7_2/JedisClientTest.java b/instrumentation/jedis/jedis-1.4/javaagent/src/version272/java/io/opentelemetry/javaagent/instrumentation/jedis/v2_7_2/JedisClientTest.java new file mode 100644 index 000000000000..15d1e5d01084 --- /dev/null +++ b/instrumentation/jedis/jedis-1.4/javaagent/src/version272/java/io/opentelemetry/javaagent/instrumentation/jedis/v2_7_2/JedisClientTest.java @@ -0,0 +1,10 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jedis.v2_7_2; + +import io.opentelemetry.javaagent.instrumentation.jedis.AbstractJedisTest; + +class JedisClientTest extends AbstractJedisTest {} diff --git a/instrumentation/jedis/jedis-1.4/testing/build.gradle.kts b/instrumentation/jedis/jedis-1.4/testing/build.gradle.kts new file mode 100644 index 000000000000..38876e40dcf8 --- /dev/null +++ b/instrumentation/jedis/jedis-1.4/testing/build.gradle.kts @@ -0,0 +1,9 @@ +plugins { + id("otel.java-conventions") +} + +dependencies { + compileOnly("redis.clients:jedis:1.4.0") + api(project(":testing-common")) + implementation("org.testcontainers:testcontainers") +} diff --git a/instrumentation/jedis/jedis-1.4/testing/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/AbstractJedisTest.java b/instrumentation/jedis/jedis-1.4/testing/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/AbstractJedisTest.java new file mode 100644 index 000000000000..547a4fc0ba6e --- /dev/null +++ b/instrumentation/jedis/jedis-1.4/testing/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/AbstractJedisTest.java @@ -0,0 +1,137 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jedis; + +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.semconv.ServerAttributes; +import io.opentelemetry.semconv.incubating.DbIncubatingAttributes; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.testcontainers.containers.GenericContainer; +import redis.clients.jedis.Jedis; + +public abstract class AbstractJedisTest { + @RegisterExtension + private static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + private static final GenericContainer REDIS_SERVER = + new GenericContainer<>("redis:6.2.3-alpine").withExposedPorts(6379); + + private static String host; + + private static int port; + + private static Jedis jedis; + + @BeforeAll + static void setup() { + REDIS_SERVER.start(); + host = REDIS_SERVER.getHost(); + port = REDIS_SERVER.getMappedPort(6379); + jedis = new Jedis(host, port); + } + + @AfterAll + static void cleanup() { + REDIS_SERVER.stop(); + } + + @BeforeEach + void reset() { + jedis.flushAll(); + testing.clearData(); + } + + @Test + void setCommand() { + jedis.set("foo", "bar"); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("SET") + .hasKind(SpanKind.CLIENT) + .hasAttributesSatisfyingExactly( + equalTo(DbIncubatingAttributes.DB_SYSTEM, "redis"), + equalTo(DbIncubatingAttributes.DB_STATEMENT, "SET foo ?"), + equalTo(DbIncubatingAttributes.DB_OPERATION, "SET"), + equalTo(ServerAttributes.SERVER_ADDRESS, host), + equalTo(ServerAttributes.SERVER_PORT, port)))); + } + + @Test + void getCommand() { + jedis.set("foo", "bar"); + String value = jedis.get("foo"); + + assertThat(value).isEqualTo("bar"); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("SET") + .hasKind(SpanKind.CLIENT) + .hasAttributesSatisfyingExactly( + equalTo(DbIncubatingAttributes.DB_SYSTEM, "redis"), + equalTo(DbIncubatingAttributes.DB_STATEMENT, "SET foo ?"), + equalTo(DbIncubatingAttributes.DB_OPERATION, "SET"), + equalTo(ServerAttributes.SERVER_ADDRESS, host), + equalTo(ServerAttributes.SERVER_PORT, port))), + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("GET") + .hasKind(SpanKind.CLIENT) + .hasAttributesSatisfyingExactly( + equalTo(DbIncubatingAttributes.DB_SYSTEM, "redis"), + equalTo(DbIncubatingAttributes.DB_STATEMENT, "GET foo"), + equalTo(DbIncubatingAttributes.DB_OPERATION, "GET"), + equalTo(ServerAttributes.SERVER_ADDRESS, host), + equalTo(ServerAttributes.SERVER_PORT, port)))); + } + + @Test + void commandWithNoArguments() { + jedis.set("foo", "bar"); + String value = jedis.randomKey(); + + assertThat(value).isEqualTo("foo"); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("SET") + .hasKind(SpanKind.CLIENT) + .hasAttributesSatisfyingExactly( + equalTo(DbIncubatingAttributes.DB_SYSTEM, "redis"), + equalTo(DbIncubatingAttributes.DB_STATEMENT, "SET foo ?"), + equalTo(DbIncubatingAttributes.DB_OPERATION, "SET"), + equalTo(ServerAttributes.SERVER_ADDRESS, host), + equalTo(ServerAttributes.SERVER_PORT, port))), + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("RANDOMKEY") + .hasKind(SpanKind.CLIENT) + .hasAttributesSatisfyingExactly( + equalTo(DbIncubatingAttributes.DB_SYSTEM, "redis"), + equalTo(DbIncubatingAttributes.DB_STATEMENT, "RANDOMKEY"), + equalTo(DbIncubatingAttributes.DB_OPERATION, "RANDOMKEY"), + equalTo(ServerAttributes.SERVER_ADDRESS, host), + equalTo(ServerAttributes.SERVER_PORT, port)))); + } +} diff --git a/instrumentation/jedis/jedis-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v3_0/JedisDbAttributesGetter.java b/instrumentation/jedis/jedis-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v3_0/JedisDbAttributesGetter.java index 4c99b7a98a5a..e2580f553f96 100644 --- a/instrumentation/jedis/jedis-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v3_0/JedisDbAttributesGetter.java +++ b/instrumentation/jedis/jedis-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v3_0/JedisDbAttributesGetter.java @@ -5,15 +5,15 @@ package io.opentelemetry.javaagent.instrumentation.jedis.v3_0; -import io.opentelemetry.instrumentation.api.instrumenter.db.DbClientAttributesGetter; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.opentelemetry.instrumentation.api.incubator.semconv.db.DbClientAttributesGetter; +import io.opentelemetry.semconv.incubating.DbIncubatingAttributes; import javax.annotation.Nullable; final class JedisDbAttributesGetter implements DbClientAttributesGetter { @Override public String getSystem(JedisRequest request) { - return SemanticAttributes.DbSystemValues.REDIS; + return DbIncubatingAttributes.DbSystemValues.REDIS; } @Override diff --git a/instrumentation/jedis/jedis-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v3_0/JedisNetAttributesGetter.java b/instrumentation/jedis/jedis-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v3_0/JedisNetworkAttributesGetter.java similarity index 69% rename from instrumentation/jedis/jedis-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v3_0/JedisNetAttributesGetter.java rename to instrumentation/jedis/jedis-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v3_0/JedisNetworkAttributesGetter.java index 53bb9df01aaf..9856a2e4ec6c 100644 --- a/instrumentation/jedis/jedis-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v3_0/JedisNetAttributesGetter.java +++ b/instrumentation/jedis/jedis-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v3_0/JedisNetworkAttributesGetter.java @@ -5,12 +5,14 @@ package io.opentelemetry.javaagent.instrumentation.jedis.v3_0; -import io.opentelemetry.instrumentation.api.instrumenter.net.NetClientAttributesGetter; +import io.opentelemetry.instrumentation.api.semconv.network.NetworkAttributesGetter; +import io.opentelemetry.instrumentation.api.semconv.network.ServerAttributesGetter; import java.net.InetSocketAddress; import java.net.Socket; import javax.annotation.Nullable; -final class JedisNetAttributesGetter implements NetClientAttributesGetter { +final class JedisNetworkAttributesGetter + implements ServerAttributesGetter, NetworkAttributesGetter { @Nullable @Override @@ -25,7 +27,7 @@ public Integer getServerPort(JedisRequest jedisRequest) { @Override @Nullable - public InetSocketAddress getServerInetSocketAddress( + public InetSocketAddress getNetworkPeerInetSocketAddress( JedisRequest jedisRequest, @Nullable Void unused) { Socket socket = jedisRequest.getConnection().getSocket(); if (socket != null && socket.getRemoteSocketAddress() instanceof InetSocketAddress) { diff --git a/instrumentation/jedis/jedis-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v3_0/JedisRequest.java b/instrumentation/jedis/jedis-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v3_0/JedisRequest.java index dd69f5c9be0e..36b71513660e 100644 --- a/instrumentation/jedis/jedis-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v3_0/JedisRequest.java +++ b/instrumentation/jedis/jedis-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v3_0/JedisRequest.java @@ -6,8 +6,8 @@ package io.opentelemetry.javaagent.instrumentation.jedis.v3_0; import com.google.auto.value.AutoValue; -import io.opentelemetry.instrumentation.api.db.RedisCommandSanitizer; -import io.opentelemetry.javaagent.bootstrap.internal.CommonConfig; +import io.opentelemetry.instrumentation.api.incubator.semconv.db.RedisCommandSanitizer; +import io.opentelemetry.javaagent.bootstrap.internal.AgentCommonConfig; import java.nio.charset.StandardCharsets; import java.util.List; import redis.clients.jedis.Connection; @@ -18,7 +18,7 @@ public abstract class JedisRequest { private static final RedisCommandSanitizer sanitizer = - RedisCommandSanitizer.create(CommonConfig.get().isStatementSanitizationEnabled()); + RedisCommandSanitizer.create(AgentCommonConfig.get().isStatementSanitizationEnabled()); public static JedisRequest create( Connection connection, ProtocolCommand command, List args) { diff --git a/instrumentation/jedis/jedis-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v3_0/JedisSingletons.java b/instrumentation/jedis/jedis-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v3_0/JedisSingletons.java index 7e4d8483b000..d52152caccab 100644 --- a/instrumentation/jedis/jedis-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v3_0/JedisSingletons.java +++ b/instrumentation/jedis/jedis-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v3_0/JedisSingletons.java @@ -6,13 +6,14 @@ package io.opentelemetry.javaagent.instrumentation.jedis.v3_0; import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.instrumentation.api.incubator.semconv.db.DbClientAttributesExtractor; +import io.opentelemetry.instrumentation.api.incubator.semconv.db.DbClientSpanNameExtractor; +import io.opentelemetry.instrumentation.api.incubator.semconv.net.PeerServiceAttributesExtractor; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; import io.opentelemetry.instrumentation.api.instrumenter.SpanKindExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.db.DbClientAttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.db.DbClientSpanNameExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.net.NetClientAttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.net.PeerServiceAttributesExtractor; -import io.opentelemetry.javaagent.bootstrap.internal.CommonConfig; +import io.opentelemetry.instrumentation.api.semconv.network.NetworkAttributesExtractor; +import io.opentelemetry.instrumentation.api.semconv.network.ServerAttributesExtractor; +import io.opentelemetry.javaagent.bootstrap.internal.AgentCommonConfig; public final class JedisSingletons { private static final String INSTRUMENTATION_NAME = "io.opentelemetry.jedis-3.0"; @@ -21,7 +22,7 @@ public final class JedisSingletons { static { JedisDbAttributesGetter dbAttributesGetter = new JedisDbAttributesGetter(); - JedisNetAttributesGetter netAttributesGetter = new JedisNetAttributesGetter(); + JedisNetworkAttributesGetter netAttributesGetter = new JedisNetworkAttributesGetter(); INSTRUMENTER = Instrumenter.builder( @@ -29,10 +30,11 @@ public final class JedisSingletons { INSTRUMENTATION_NAME, DbClientSpanNameExtractor.create(dbAttributesGetter)) .addAttributesExtractor(DbClientAttributesExtractor.create(dbAttributesGetter)) - .addAttributesExtractor(NetClientAttributesExtractor.create(netAttributesGetter)) + .addAttributesExtractor(ServerAttributesExtractor.create(netAttributesGetter)) + .addAttributesExtractor(NetworkAttributesExtractor.create(netAttributesGetter)) .addAttributesExtractor( PeerServiceAttributesExtractor.create( - netAttributesGetter, CommonConfig.get().getPeerServiceMapping())) + netAttributesGetter, AgentCommonConfig.get().getPeerServiceResolver())) .buildInstrumenter(SpanKindExtractor.alwaysClient()); } diff --git a/instrumentation/jedis/jedis-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/jedis/v3_0/Jedis30ClientTest.java b/instrumentation/jedis/jedis-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/jedis/v3_0/Jedis30ClientTest.java index 041cd17c2fcf..be3e155d06fc 100644 --- a/instrumentation/jedis/jedis-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/jedis/v3_0/Jedis30ClientTest.java +++ b/instrumentation/jedis/jedis-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/jedis/v3_0/Jedis30ClientTest.java @@ -6,12 +6,18 @@ package io.opentelemetry.javaagent.instrumentation.jedis.v3_0; import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.satisfies; import static org.assertj.core.api.Assertions.assertThat; import io.opentelemetry.api.trace.SpanKind; import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.opentelemetry.semconv.NetworkAttributes; +import io.opentelemetry.semconv.ServerAttributes; +import io.opentelemetry.semconv.incubating.DbIncubatingAttributes; +import java.net.InetAddress; +import java.net.UnknownHostException; +import org.assertj.core.api.AbstractLongAssert; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; @@ -27,25 +33,31 @@ class Jedis30ClientTest { static GenericContainer redisServer = new GenericContainer<>("redis:6.2.3-alpine").withExposedPorts(6379); + static String host; + + static String ip; + static int port; static Jedis jedis; @BeforeAll - static void setupSpec() { + static void setup() throws UnknownHostException { redisServer.start(); + host = redisServer.getHost(); + ip = InetAddress.getByName(host).getHostAddress(); port = redisServer.getMappedPort(6379); - jedis = new Jedis("localhost", port); + jedis = new Jedis(host, port); } @AfterAll - static void cleanupSpec() { + static void cleanup() { redisServer.stop(); jedis.close(); } @BeforeEach - void setup() { + void reset() { jedis.flushAll(); testing.clearData(); } @@ -61,12 +73,16 @@ void setCommand() { span.hasName("SET") .hasKind(SpanKind.CLIENT) .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.DB_SYSTEM, "redis"), - equalTo(SemanticAttributes.DB_STATEMENT, "SET foo ?"), - equalTo(SemanticAttributes.DB_OPERATION, "SET"), - equalTo(SemanticAttributes.NET_PEER_NAME, "localhost"), - equalTo(SemanticAttributes.NET_PEER_PORT, port), - equalTo(SemanticAttributes.NET_SOCK_PEER_ADDR, "127.0.0.1")))); + equalTo(DbIncubatingAttributes.DB_SYSTEM, "redis"), + equalTo(DbIncubatingAttributes.DB_STATEMENT, "SET foo ?"), + equalTo(DbIncubatingAttributes.DB_OPERATION, "SET"), + equalTo(ServerAttributes.SERVER_ADDRESS, host), + equalTo(ServerAttributes.SERVER_PORT, port), + equalTo(NetworkAttributes.NETWORK_TYPE, "ipv4"), + equalTo(NetworkAttributes.NETWORK_PEER_ADDRESS, ip), + satisfies( + NetworkAttributes.NETWORK_PEER_PORT, + AbstractLongAssert::isNotNegative)))); } @Test @@ -83,24 +99,32 @@ void getCommand() { span.hasName("SET") .hasKind(SpanKind.CLIENT) .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.DB_SYSTEM, "redis"), - equalTo(SemanticAttributes.DB_STATEMENT, "SET foo ?"), - equalTo(SemanticAttributes.DB_OPERATION, "SET"), - equalTo(SemanticAttributes.NET_PEER_NAME, "localhost"), - equalTo(SemanticAttributes.NET_PEER_PORT, port), - equalTo(SemanticAttributes.NET_SOCK_PEER_ADDR, "127.0.0.1"))), + equalTo(DbIncubatingAttributes.DB_SYSTEM, "redis"), + equalTo(DbIncubatingAttributes.DB_STATEMENT, "SET foo ?"), + equalTo(DbIncubatingAttributes.DB_OPERATION, "SET"), + equalTo(ServerAttributes.SERVER_ADDRESS, host), + equalTo(ServerAttributes.SERVER_PORT, port), + equalTo(NetworkAttributes.NETWORK_TYPE, "ipv4"), + equalTo(NetworkAttributes.NETWORK_PEER_ADDRESS, ip), + satisfies( + NetworkAttributes.NETWORK_PEER_PORT, + AbstractLongAssert::isNotNegative))), trace -> trace.hasSpansSatisfyingExactly( span -> span.hasName("GET") .hasKind(SpanKind.CLIENT) .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.DB_SYSTEM, "redis"), - equalTo(SemanticAttributes.DB_STATEMENT, "GET foo"), - equalTo(SemanticAttributes.DB_OPERATION, "GET"), - equalTo(SemanticAttributes.NET_PEER_NAME, "localhost"), - equalTo(SemanticAttributes.NET_PEER_PORT, port), - equalTo(SemanticAttributes.NET_SOCK_PEER_ADDR, "127.0.0.1")))); + equalTo(DbIncubatingAttributes.DB_SYSTEM, "redis"), + equalTo(DbIncubatingAttributes.DB_STATEMENT, "GET foo"), + equalTo(DbIncubatingAttributes.DB_OPERATION, "GET"), + equalTo(ServerAttributes.SERVER_ADDRESS, host), + equalTo(ServerAttributes.SERVER_PORT, port), + equalTo(NetworkAttributes.NETWORK_TYPE, "ipv4"), + equalTo(NetworkAttributes.NETWORK_PEER_ADDRESS, ip), + satisfies( + NetworkAttributes.NETWORK_PEER_PORT, + AbstractLongAssert::isNotNegative)))); } @Test @@ -117,23 +141,31 @@ void commandWithNoArguments() { span.hasName("SET") .hasKind(SpanKind.CLIENT) .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.DB_SYSTEM, "redis"), - equalTo(SemanticAttributes.DB_STATEMENT, "SET foo ?"), - equalTo(SemanticAttributes.DB_OPERATION, "SET"), - equalTo(SemanticAttributes.NET_PEER_NAME, "localhost"), - equalTo(SemanticAttributes.NET_PEER_PORT, port), - equalTo(SemanticAttributes.NET_SOCK_PEER_ADDR, "127.0.0.1"))), + equalTo(DbIncubatingAttributes.DB_SYSTEM, "redis"), + equalTo(DbIncubatingAttributes.DB_STATEMENT, "SET foo ?"), + equalTo(DbIncubatingAttributes.DB_OPERATION, "SET"), + equalTo(ServerAttributes.SERVER_ADDRESS, host), + equalTo(ServerAttributes.SERVER_PORT, port), + equalTo(NetworkAttributes.NETWORK_TYPE, "ipv4"), + equalTo(NetworkAttributes.NETWORK_PEER_ADDRESS, ip), + satisfies( + NetworkAttributes.NETWORK_PEER_PORT, + AbstractLongAssert::isNotNegative))), trace -> trace.hasSpansSatisfyingExactly( span -> span.hasName("RANDOMKEY") .hasKind(SpanKind.CLIENT) .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.DB_SYSTEM, "redis"), - equalTo(SemanticAttributes.DB_STATEMENT, "RANDOMKEY"), - equalTo(SemanticAttributes.DB_OPERATION, "RANDOMKEY"), - equalTo(SemanticAttributes.NET_PEER_NAME, "localhost"), - equalTo(SemanticAttributes.NET_PEER_PORT, port), - equalTo(SemanticAttributes.NET_SOCK_PEER_ADDR, "127.0.0.1")))); + equalTo(DbIncubatingAttributes.DB_SYSTEM, "redis"), + equalTo(DbIncubatingAttributes.DB_STATEMENT, "RANDOMKEY"), + equalTo(DbIncubatingAttributes.DB_OPERATION, "RANDOMKEY"), + equalTo(ServerAttributes.SERVER_ADDRESS, host), + equalTo(ServerAttributes.SERVER_PORT, port), + equalTo(NetworkAttributes.NETWORK_TYPE, "ipv4"), + equalTo(NetworkAttributes.NETWORK_PEER_ADDRESS, ip), + satisfies( + NetworkAttributes.NETWORK_PEER_PORT, + AbstractLongAssert::isNotNegative)))); } } diff --git a/instrumentation/jedis/jedis-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v4_0/JedisDbAttributesGetter.java b/instrumentation/jedis/jedis-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v4_0/JedisDbAttributesGetter.java index 9a2aa6f91f92..4d77c785d768 100644 --- a/instrumentation/jedis/jedis-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v4_0/JedisDbAttributesGetter.java +++ b/instrumentation/jedis/jedis-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v4_0/JedisDbAttributesGetter.java @@ -5,15 +5,15 @@ package io.opentelemetry.javaagent.instrumentation.jedis.v4_0; -import io.opentelemetry.instrumentation.api.instrumenter.db.DbClientAttributesGetter; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.opentelemetry.instrumentation.api.incubator.semconv.db.DbClientAttributesGetter; +import io.opentelemetry.semconv.incubating.DbIncubatingAttributes; import javax.annotation.Nullable; final class JedisDbAttributesGetter implements DbClientAttributesGetter { @Override public String getSystem(JedisRequest request) { - return SemanticAttributes.DbSystemValues.REDIS; + return DbIncubatingAttributes.DbSystemValues.REDIS; } @Override diff --git a/instrumentation/jedis/jedis-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v4_0/JedisNetAttributesGetter.java b/instrumentation/jedis/jedis-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v4_0/JedisNetworkAttributesGetter.java similarity index 54% rename from instrumentation/jedis/jedis-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v4_0/JedisNetAttributesGetter.java rename to instrumentation/jedis/jedis-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v4_0/JedisNetworkAttributesGetter.java index 8e5c4ffa72f1..900ba8e08e69 100644 --- a/instrumentation/jedis/jedis-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v4_0/JedisNetAttributesGetter.java +++ b/instrumentation/jedis/jedis-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v4_0/JedisNetworkAttributesGetter.java @@ -5,28 +5,16 @@ package io.opentelemetry.javaagent.instrumentation.jedis.v4_0; -import io.opentelemetry.instrumentation.api.instrumenter.net.NetClientAttributesGetter; +import io.opentelemetry.instrumentation.api.semconv.network.NetworkAttributesGetter; import java.net.InetSocketAddress; import java.net.SocketAddress; import javax.annotation.Nullable; -final class JedisNetAttributesGetter implements NetClientAttributesGetter { - - @Nullable - @Override - public String getServerAddress(JedisRequest jedisRequest) { - return null; - } - - @Nullable - @Override - public Integer getServerPort(JedisRequest jedisRequest) { - return null; - } +final class JedisNetworkAttributesGetter implements NetworkAttributesGetter { @Override @Nullable - public InetSocketAddress getServerInetSocketAddress( + public InetSocketAddress getNetworkPeerInetSocketAddress( JedisRequest jedisRequest, @Nullable Void unused) { SocketAddress socketAddress = jedisRequest.getRemoteSocketAddress(); if (socketAddress instanceof InetSocketAddress) { diff --git a/instrumentation/jedis/jedis-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v4_0/JedisRequest.java b/instrumentation/jedis/jedis-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v4_0/JedisRequest.java index 6de09a4ea1cb..a8c26bfaabbb 100644 --- a/instrumentation/jedis/jedis-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v4_0/JedisRequest.java +++ b/instrumentation/jedis/jedis-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v4_0/JedisRequest.java @@ -6,8 +6,8 @@ package io.opentelemetry.javaagent.instrumentation.jedis.v4_0; import com.google.auto.value.AutoValue; -import io.opentelemetry.instrumentation.api.db.RedisCommandSanitizer; -import io.opentelemetry.javaagent.bootstrap.internal.CommonConfig; +import io.opentelemetry.instrumentation.api.incubator.semconv.db.RedisCommandSanitizer; +import io.opentelemetry.javaagent.bootstrap.internal.AgentCommonConfig; import java.net.Socket; import java.net.SocketAddress; import java.nio.charset.StandardCharsets; @@ -22,7 +22,7 @@ public abstract class JedisRequest { private static final RedisCommandSanitizer sanitizer = - RedisCommandSanitizer.create(CommonConfig.get().isStatementSanitizationEnabled()); + RedisCommandSanitizer.create(AgentCommonConfig.get().isStatementSanitizationEnabled()); public static JedisRequest create(ProtocolCommand command, List args) { return new AutoValue_JedisRequest(command, args); diff --git a/instrumentation/jedis/jedis-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v4_0/JedisSingletons.java b/instrumentation/jedis/jedis-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v4_0/JedisSingletons.java index 0e9c43c5f1ea..b22c482227a0 100644 --- a/instrumentation/jedis/jedis-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v4_0/JedisSingletons.java +++ b/instrumentation/jedis/jedis-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jedis/v4_0/JedisSingletons.java @@ -6,13 +6,11 @@ package io.opentelemetry.javaagent.instrumentation.jedis.v4_0; import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.instrumentation.api.incubator.semconv.db.DbClientAttributesExtractor; +import io.opentelemetry.instrumentation.api.incubator.semconv.db.DbClientSpanNameExtractor; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; import io.opentelemetry.instrumentation.api.instrumenter.SpanKindExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.db.DbClientAttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.db.DbClientSpanNameExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.net.NetClientAttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.net.PeerServiceAttributesExtractor; -import io.opentelemetry.javaagent.bootstrap.internal.CommonConfig; +import io.opentelemetry.instrumentation.api.semconv.network.NetworkAttributesExtractor; public final class JedisSingletons { private static final String INSTRUMENTATION_NAME = "io.opentelemetry.jedis-4.0"; @@ -21,7 +19,7 @@ public final class JedisSingletons { static { JedisDbAttributesGetter dbAttributesGetter = new JedisDbAttributesGetter(); - JedisNetAttributesGetter netAttributesGetter = new JedisNetAttributesGetter(); + JedisNetworkAttributesGetter netAttributesGetter = new JedisNetworkAttributesGetter(); INSTRUMENTER = Instrumenter.builder( @@ -29,10 +27,7 @@ public final class JedisSingletons { INSTRUMENTATION_NAME, DbClientSpanNameExtractor.create(dbAttributesGetter)) .addAttributesExtractor(DbClientAttributesExtractor.create(dbAttributesGetter)) - .addAttributesExtractor(NetClientAttributesExtractor.create(netAttributesGetter)) - .addAttributesExtractor( - PeerServiceAttributesExtractor.create( - netAttributesGetter, CommonConfig.get().getPeerServiceMapping())) + .addAttributesExtractor(NetworkAttributesExtractor.create(netAttributesGetter)) .buildInstrumenter(SpanKindExtractor.alwaysClient()); } diff --git a/instrumentation/jedis/jedis-4.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/jedis/v4_0/Jedis40ClientTest.java b/instrumentation/jedis/jedis-4.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/jedis/v4_0/Jedis40ClientTest.java index 7cc5c8e0e967..0d54274d9a35 100644 --- a/instrumentation/jedis/jedis-4.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/jedis/v4_0/Jedis40ClientTest.java +++ b/instrumentation/jedis/jedis-4.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/jedis/v4_0/Jedis40ClientTest.java @@ -6,13 +6,15 @@ package io.opentelemetry.javaagent.instrumentation.jedis.v4_0; import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; -import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.satisfies; import static org.assertj.core.api.Assertions.assertThat; import io.opentelemetry.api.trace.SpanKind; import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.opentelemetry.semconv.NetworkAttributes; +import io.opentelemetry.semconv.incubating.DbIncubatingAttributes; +import java.net.InetAddress; +import java.net.UnknownHostException; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; @@ -28,25 +30,28 @@ class Jedis40ClientTest { static GenericContainer redisServer = new GenericContainer<>("redis:6.2.3-alpine").withExposedPorts(6379); + static String ip; + static int port; static Jedis jedis; @BeforeAll - static void setupSpec() { + static void setup() throws UnknownHostException { redisServer.start(); port = redisServer.getMappedPort(6379); - jedis = new Jedis("localhost", port); + ip = InetAddress.getByName(redisServer.getHost()).getHostAddress(); + jedis = new Jedis(redisServer.getHost(), port); } @AfterAll - static void cleanupSpec() { + static void cleanup() { redisServer.stop(); jedis.close(); } @BeforeEach - void setup() { + void reset() { jedis.flushAll(); testing.clearData(); } @@ -62,14 +67,12 @@ void setCommand() { span.hasName("SET") .hasKind(SpanKind.CLIENT) .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.DB_SYSTEM, "redis"), - equalTo(SemanticAttributes.DB_STATEMENT, "SET foo ?"), - equalTo(SemanticAttributes.DB_OPERATION, "SET"), - equalTo(SemanticAttributes.NET_SOCK_PEER_PORT, port), - equalTo(SemanticAttributes.NET_SOCK_PEER_ADDR, "127.0.0.1"), - satisfies( - SemanticAttributes.NET_SOCK_PEER_NAME, - val -> val.isIn("localhost", "127.0.0.1"))))); + equalTo(DbIncubatingAttributes.DB_SYSTEM, "redis"), + equalTo(DbIncubatingAttributes.DB_STATEMENT, "SET foo ?"), + equalTo(DbIncubatingAttributes.DB_OPERATION, "SET"), + equalTo(NetworkAttributes.NETWORK_TYPE, "ipv4"), + equalTo(NetworkAttributes.NETWORK_PEER_PORT, port), + equalTo(NetworkAttributes.NETWORK_PEER_ADDRESS, ip)))); } @Test @@ -86,28 +89,24 @@ void getCommand() { span.hasName("SET") .hasKind(SpanKind.CLIENT) .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.DB_SYSTEM, "redis"), - equalTo(SemanticAttributes.DB_STATEMENT, "SET foo ?"), - equalTo(SemanticAttributes.DB_OPERATION, "SET"), - equalTo(SemanticAttributes.NET_SOCK_PEER_PORT, port), - equalTo(SemanticAttributes.NET_SOCK_PEER_ADDR, "127.0.0.1"), - satisfies( - SemanticAttributes.NET_SOCK_PEER_NAME, - val -> val.isIn("localhost", "127.0.0.1")))), + equalTo(DbIncubatingAttributes.DB_SYSTEM, "redis"), + equalTo(DbIncubatingAttributes.DB_STATEMENT, "SET foo ?"), + equalTo(DbIncubatingAttributes.DB_OPERATION, "SET"), + equalTo(NetworkAttributes.NETWORK_TYPE, "ipv4"), + equalTo(NetworkAttributes.NETWORK_PEER_PORT, port), + equalTo(NetworkAttributes.NETWORK_PEER_ADDRESS, ip))), trace -> trace.hasSpansSatisfyingExactly( span -> span.hasName("GET") .hasKind(SpanKind.CLIENT) .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.DB_SYSTEM, "redis"), - equalTo(SemanticAttributes.DB_STATEMENT, "GET foo"), - equalTo(SemanticAttributes.DB_OPERATION, "GET"), - equalTo(SemanticAttributes.NET_SOCK_PEER_PORT, port), - equalTo(SemanticAttributes.NET_SOCK_PEER_ADDR, "127.0.0.1"), - satisfies( - SemanticAttributes.NET_SOCK_PEER_NAME, - val -> val.isIn("localhost", "127.0.0.1"))))); + equalTo(DbIncubatingAttributes.DB_SYSTEM, "redis"), + equalTo(DbIncubatingAttributes.DB_STATEMENT, "GET foo"), + equalTo(DbIncubatingAttributes.DB_OPERATION, "GET"), + equalTo(NetworkAttributes.NETWORK_TYPE, "ipv4"), + equalTo(NetworkAttributes.NETWORK_PEER_PORT, port), + equalTo(NetworkAttributes.NETWORK_PEER_ADDRESS, ip)))); } @Test @@ -124,27 +123,23 @@ void commandWithNoArguments() { span.hasName("SET") .hasKind(SpanKind.CLIENT) .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.DB_SYSTEM, "redis"), - equalTo(SemanticAttributes.DB_STATEMENT, "SET foo ?"), - equalTo(SemanticAttributes.DB_OPERATION, "SET"), - equalTo(SemanticAttributes.NET_SOCK_PEER_PORT, port), - equalTo(SemanticAttributes.NET_SOCK_PEER_ADDR, "127.0.0.1"), - satisfies( - SemanticAttributes.NET_SOCK_PEER_NAME, - val -> val.isIn("localhost", "127.0.0.1")))), + equalTo(DbIncubatingAttributes.DB_SYSTEM, "redis"), + equalTo(DbIncubatingAttributes.DB_STATEMENT, "SET foo ?"), + equalTo(DbIncubatingAttributes.DB_OPERATION, "SET"), + equalTo(NetworkAttributes.NETWORK_TYPE, "ipv4"), + equalTo(NetworkAttributes.NETWORK_PEER_PORT, port), + equalTo(NetworkAttributes.NETWORK_PEER_ADDRESS, ip))), trace -> trace.hasSpansSatisfyingExactly( span -> span.hasName("RANDOMKEY") .hasKind(SpanKind.CLIENT) .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.DB_SYSTEM, "redis"), - equalTo(SemanticAttributes.DB_STATEMENT, "RANDOMKEY"), - equalTo(SemanticAttributes.DB_OPERATION, "RANDOMKEY"), - equalTo(SemanticAttributes.NET_SOCK_PEER_PORT, port), - equalTo(SemanticAttributes.NET_SOCK_PEER_ADDR, "127.0.0.1"), - satisfies( - SemanticAttributes.NET_SOCK_PEER_NAME, - val -> val.isIn("localhost", "127.0.0.1"))))); + equalTo(DbIncubatingAttributes.DB_SYSTEM, "redis"), + equalTo(DbIncubatingAttributes.DB_STATEMENT, "RANDOMKEY"), + equalTo(DbIncubatingAttributes.DB_OPERATION, "RANDOMKEY"), + equalTo(NetworkAttributes.NETWORK_TYPE, "ipv4"), + equalTo(NetworkAttributes.NETWORK_PEER_PORT, port), + equalTo(NetworkAttributes.NETWORK_PEER_ADDRESS, ip)))); } } diff --git a/instrumentation/jetty-httpclient/jetty-httpclient-12.0/javaagent/build.gradle.kts b/instrumentation/jetty-httpclient/jetty-httpclient-12.0/javaagent/build.gradle.kts new file mode 100644 index 000000000000..ae4b56855e0c --- /dev/null +++ b/instrumentation/jetty-httpclient/jetty-httpclient-12.0/javaagent/build.gradle.kts @@ -0,0 +1,25 @@ +plugins { + id("otel.javaagent-instrumentation") +} + +muzzle { + pass { + group.set("org.eclipse.jetty") + module.set("jetty-client") + versions.set("[12,)") + } +} + +otelJava { + minJavaVersionSupported.set(JavaVersion.VERSION_17) +} + +dependencies { + implementation(project(":instrumentation:jetty-httpclient:jetty-httpclient-12.0:library")) + + library("org.eclipse.jetty:jetty-client:12.0.0") + + testInstrumentation(project(":instrumentation:jetty-httpclient:jetty-httpclient-9.2:javaagent")) + + testImplementation(project(":instrumentation:jetty-httpclient:jetty-httpclient-12.0:testing")) +} diff --git a/instrumentation/jetty-httpclient/jetty-httpclient-12.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jetty/httpclient/v12_0/JettyClient12ResponseListenersInstrumentation.java b/instrumentation/jetty-httpclient/jetty-httpclient-12.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jetty/httpclient/v12_0/JettyClient12ResponseListenersInstrumentation.java new file mode 100644 index 000000000000..940ede1b69d1 --- /dev/null +++ b/instrumentation/jetty-httpclient/jetty-httpclient-12.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jetty/httpclient/v12_0/JettyClient12ResponseListenersInstrumentation.java @@ -0,0 +1,108 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jetty.httpclient.v12_0; + +import static io.opentelemetry.javaagent.instrumentation.jetty.httpclient.v12_0.JettyHttpClientSingletons.JETTY_CLIENT_CONTEXT_KEY; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.nameContains; +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.eclipse.jetty.client.Response; +import org.eclipse.jetty.client.Result; + +public class JettyClient12ResponseListenersInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("org.eclipse.jetty.client.transport.ResponseListeners"); + } + + @Override + public void transform(TypeTransformer transformer) { + // for response listeners + transformer.applyAdviceToMethod( + isMethod() + .and( + nameContains("notify") + .and(isPublic()) + .and(takesArgument(0, named("org.eclipse.jetty.client.Response")))), + JettyClient12ResponseListenersInstrumentation.class.getName() + + "$JettyHttpClient12RespListenersNotifyAdvice"); + + // for complete listeners + transformer.applyAdviceToMethod( + isMethod() + .and( + nameContains("notifyComplete") + .and(isPublic()) + .and(takesArgument(0, named("org.eclipse.jetty.client.Result")))), + JettyClient12ResponseListenersInstrumentation.class.getName() + + "$JettyHttpClient12CompleteListenersNotifyAdvice"); + } + + @SuppressWarnings("unused") + public static class JettyHttpClient12RespListenersNotifyAdvice { + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnterNotify( + @Advice.Argument(0) Response response, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + context = (Context) response.getRequest().getAttributes().get(JETTY_CLIENT_CONTEXT_KEY); + if (context != null) { + scope = context.makeCurrent(); + } + } + + @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class) + public static void onExitNotify( + @Advice.Argument(0) Response response, + @Advice.Thrown Throwable throwable, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + if (scope == null) { + return; + } + + scope.close(); + } + } + + @SuppressWarnings("unused") + public static class JettyHttpClient12CompleteListenersNotifyAdvice { + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnterComplete( + @Advice.Argument(0) Result result, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + context = (Context) result.getRequest().getAttributes().get(JETTY_CLIENT_CONTEXT_KEY); + if (context != null) { + scope = context.makeCurrent(); + } + } + + @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class) + public static void onExitComplete( + @Advice.Argument(0) Result result, + @Advice.Thrown Throwable throwable, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + if (scope == null) { + return; + } + + scope.close(); + } + } +} diff --git a/instrumentation/jetty-httpclient/jetty-httpclient-12.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jetty/httpclient/v12_0/JettyHttpClient12Instrumentation.java b/instrumentation/jetty-httpclient/jetty-httpclient-12.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jetty/httpclient/v12_0/JettyHttpClient12Instrumentation.java new file mode 100644 index 000000000000..9ba900164b36 --- /dev/null +++ b/instrumentation/jetty-httpclient/jetty-httpclient-12.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jetty/httpclient/v12_0/JettyHttpClient12Instrumentation.java @@ -0,0 +1,107 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jetty.httpclient.v12_0; + +import static io.opentelemetry.javaagent.instrumentation.jetty.httpclient.v12_0.JettyHttpClientSingletons.JETTY_CLIENT_CONTEXT_KEY; +import static io.opentelemetry.javaagent.instrumentation.jetty.httpclient.v12_0.JettyHttpClientSingletons.instrumenter; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.nameContains; +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.jetty.httpclient.v12_0.internal.JettyClientTracingListener; +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.eclipse.jetty.client.transport.HttpRequest; + +public class JettyHttpClient12Instrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("org.eclipse.jetty.client.transport.HttpRequest"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod() + .and(named("send")) + .and(takesArgument(0, named("org.eclipse.jetty.client.Response$CompleteListener"))), + JettyHttpClient12Instrumentation.class.getName() + "$JettyHttpClient12SendAdvice"); + // For request listeners + transformer.applyAdviceToMethod( + isMethod().and(nameContains("notify")), + JettyHttpClient12Instrumentation.class.getName() + "$JettyHttpClient12NotifyAdvice"); + } + + @SuppressWarnings("unused") + public static class JettyHttpClient12SendAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnterSend( + @Advice.This HttpRequest request, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + // start span + Context parentContext = Context.current(); + context = JettyClientTracingListener.handleRequest(parentContext, request, instrumenter()); + if (context == null) { + return; + } + // set context for responseListeners + request.attribute(JETTY_CLIENT_CONTEXT_KEY, parentContext); + + scope = context.makeCurrent(); + } + + @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class) + public static void onExitSend( + @Advice.This HttpRequest request, + @Advice.Thrown Throwable throwable, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + if (scope == null) { + return; + } + + // not ending span here unless error, span ended in the interceptor + scope.close(); + if (throwable != null) { + instrumenter().end(context, request, null, throwable); + } + } + } + + @SuppressWarnings("unused") + public static class JettyHttpClient12NotifyAdvice { + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnterNotify( + @Advice.This HttpRequest request, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + context = (Context) request.getAttributes().get(JETTY_CLIENT_CONTEXT_KEY); + if (context == null) { + return; + } + scope = context.makeCurrent(); + } + + @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class) + public static void onExitNotify( + @Advice.Local("otelContext") Context context, @Advice.Local("otelScope") Scope scope) { + if (scope == null) { + return; + } + + scope.close(); + } + } +} diff --git a/instrumentation/jetty-httpclient/jetty-httpclient-12.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jetty/httpclient/v12_0/JettyHttpClient12InstrumentationModule.java b/instrumentation/jetty-httpclient/jetty-httpclient-12.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jetty/httpclient/v12_0/JettyHttpClient12InstrumentationModule.java new file mode 100644 index 000000000000..5d9690dbc7f4 --- /dev/null +++ b/instrumentation/jetty-httpclient/jetty-httpclient-12.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jetty/httpclient/v12_0/JettyHttpClient12InstrumentationModule.java @@ -0,0 +1,27 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jetty.httpclient.v12_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 JettyHttpClient12InstrumentationModule extends InstrumentationModule { + public JettyHttpClient12InstrumentationModule() { + super("jetty-httpclient", "jetty-httpclient-12.0"); + } + + @Override + public List typeInstrumentations() { + return asList( + new JettyHttpClient12Instrumentation(), + new JettyClient12ResponseListenersInstrumentation()); + } +} diff --git a/instrumentation/jetty-httpclient/jetty-httpclient-12.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jetty/httpclient/v12_0/JettyHttpClientSingletons.java b/instrumentation/jetty-httpclient/jetty-httpclient-12.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jetty/httpclient/v12_0/JettyHttpClientSingletons.java new file mode 100644 index 000000000000..68a9bca5adda --- /dev/null +++ b/instrumentation/jetty-httpclient/jetty-httpclient-12.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jetty/httpclient/v12_0/JettyHttpClientSingletons.java @@ -0,0 +1,28 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jetty.httpclient.v12_0; + +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import io.opentelemetry.instrumentation.jetty.httpclient.v12_0.internal.JettyHttpClientInstrumenterBuilderFactory; +import io.opentelemetry.javaagent.bootstrap.internal.JavaagentHttpClientInstrumenters; +import org.eclipse.jetty.client.Request; +import org.eclipse.jetty.client.Response; + +public final class JettyHttpClientSingletons { + + static final String JETTY_CLIENT_CONTEXT_KEY = "otel-jetty-client-context"; + + private static final Instrumenter INSTRUMENTER = + JavaagentHttpClientInstrumenters.create( + JettyHttpClientInstrumenterBuilderFactory.create(GlobalOpenTelemetry.get())); + + public static Instrumenter instrumenter() { + return INSTRUMENTER; + } + + private JettyHttpClientSingletons() {} +} diff --git a/instrumentation/jetty-httpclient/jetty-httpclient-12.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/jetty/httpclient/v12_0/JettyHttpClient12AgentTest.java b/instrumentation/jetty-httpclient/jetty-httpclient-12.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/jetty/httpclient/v12_0/JettyHttpClient12AgentTest.java new file mode 100644 index 000000000000..7884e15938e7 --- /dev/null +++ b/instrumentation/jetty-httpclient/jetty-httpclient-12.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/jetty/httpclient/v12_0/JettyHttpClient12AgentTest.java @@ -0,0 +1,31 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jetty.httpclient.v12_0; + +import io.opentelemetry.instrumentation.jetty.httpclient.v12_0.AbstractJettyClient12Test; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.http.HttpClientInstrumentationExtension; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.util.ssl.SslContextFactory; +import org.junit.jupiter.api.extension.RegisterExtension; + +class JettyHttpClient12AgentTest extends AbstractJettyClient12Test { + + @RegisterExtension + static final InstrumentationExtension testing = HttpClientInstrumentationExtension.forAgent(); + + @Override + protected HttpClient createStandardClient() { + return new HttpClient(); + } + + @Override + protected HttpClient createHttpsClient(SslContextFactory.Client sslContextFactory) { + HttpClient httpClient = new HttpClient(); + httpClient.setSslContextFactory(sslContextFactory); + return httpClient; + } +} diff --git a/instrumentation/jetty-httpclient/jetty-httpclient-12.0/library/build.gradle.kts b/instrumentation/jetty-httpclient/jetty-httpclient-12.0/library/build.gradle.kts new file mode 100644 index 000000000000..da2df5c220a1 --- /dev/null +++ b/instrumentation/jetty-httpclient/jetty-httpclient-12.0/library/build.gradle.kts @@ -0,0 +1,13 @@ +plugins { + id("otel.library-instrumentation") +} + +otelJava { + minJavaVersionSupported.set(JavaVersion.VERSION_17) +} + +dependencies { + library("org.eclipse.jetty:jetty-client:12.0.0") + + testImplementation(project(":instrumentation:jetty-httpclient::jetty-httpclient-12.0:testing")) +} diff --git a/instrumentation/jetty-httpclient/jetty-httpclient-12.0/library/src/main/java/io/opentelemetry/instrumentation/jetty/httpclient/v12_0/JettyClientTelemetry.java b/instrumentation/jetty-httpclient/jetty-httpclient-12.0/library/src/main/java/io/opentelemetry/instrumentation/jetty/httpclient/v12_0/JettyClientTelemetry.java new file mode 100644 index 000000000000..3e123801321a --- /dev/null +++ b/instrumentation/jetty-httpclient/jetty-httpclient-12.0/library/src/main/java/io/opentelemetry/instrumentation/jetty/httpclient/v12_0/JettyClientTelemetry.java @@ -0,0 +1,37 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.jetty.httpclient.v12_0; + +import io.opentelemetry.api.OpenTelemetry; +import org.eclipse.jetty.client.HttpClient; + +/** Entrypoint for instrumenting Jetty client. */ +public final class JettyClientTelemetry { + + /** Returns a new {@link JettyClientTelemetry} configured with the given {@link OpenTelemetry}. */ + public static JettyClientTelemetry create(OpenTelemetry openTelemetry) { + JettyClientTelemetryBuilder builder = builder(openTelemetry); + return builder.build(); + } + + /** + * Returns a new {@link JettyClientTelemetryBuilder} configured with the given {@link + * OpenTelemetry}. + */ + public static JettyClientTelemetryBuilder builder(OpenTelemetry openTelemetry) { + return new JettyClientTelemetryBuilder(openTelemetry); + } + + private final HttpClient httpClient; + + JettyClientTelemetry(HttpClient httpClient) { + this.httpClient = httpClient; + } + + public HttpClient getHttpClient() { + return httpClient; + } +} diff --git a/instrumentation/jetty-httpclient/jetty-httpclient-12.0/library/src/main/java/io/opentelemetry/instrumentation/jetty/httpclient/v12_0/JettyClientTelemetryBuilder.java b/instrumentation/jetty-httpclient/jetty-httpclient-12.0/library/src/main/java/io/opentelemetry/instrumentation/jetty/httpclient/v12_0/JettyClientTelemetryBuilder.java new file mode 100644 index 000000000000..ec5c8b379d50 --- /dev/null +++ b/instrumentation/jetty-httpclient/jetty-httpclient-12.0/library/src/main/java/io/opentelemetry/instrumentation/jetty/httpclient/v12_0/JettyClientTelemetryBuilder.java @@ -0,0 +1,131 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.jetty.httpclient.v12_0; + +import com.google.errorprone.annotations.CanIgnoreReturnValue; +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.instrumentation.api.incubator.builder.internal.DefaultHttpClientInstrumenterBuilder; +import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor; +import io.opentelemetry.instrumentation.api.semconv.http.HttpClientAttributesExtractorBuilder; +import io.opentelemetry.instrumentation.jetty.httpclient.v12_0.internal.JettyHttpClientInstrumenterBuilderFactory; +import java.util.List; +import java.util.Set; +import java.util.function.Function; +import org.eclipse.jetty.client.HttpClientTransport; +import org.eclipse.jetty.client.Request; +import org.eclipse.jetty.client.Response; +import org.eclipse.jetty.util.ssl.SslContextFactory; + +public final class JettyClientTelemetryBuilder { + + private final DefaultHttpClientInstrumenterBuilder builder; + private HttpClientTransport httpClientTransport; + private SslContextFactory.Client sslContextFactory; + + JettyClientTelemetryBuilder(OpenTelemetry openTelemetry) { + builder = JettyHttpClientInstrumenterBuilderFactory.create(openTelemetry); + } + + @CanIgnoreReturnValue + public JettyClientTelemetryBuilder setHttpClientTransport( + HttpClientTransport httpClientTransport) { + this.httpClientTransport = httpClientTransport; + return this; + } + + @CanIgnoreReturnValue + public JettyClientTelemetryBuilder setSslContextFactory( + SslContextFactory.Client sslContextFactory) { + this.sslContextFactory = sslContextFactory; + return this; + } + + /** + * Adds an additional {@link AttributesExtractor} to invoke to set attributes to instrumented + * items. + */ + @CanIgnoreReturnValue + public JettyClientTelemetryBuilder addAttributeExtractor( + AttributesExtractor attributesExtractor) { + builder.addAttributeExtractor(attributesExtractor); + return this; + } + + /** + * Configures the HTTP request headers that will be captured as span attributes. + * + * @param requestHeaders A list of HTTP header names. + */ + @CanIgnoreReturnValue + public JettyClientTelemetryBuilder setCapturedRequestHeaders(List requestHeaders) { + builder.setCapturedRequestHeaders(requestHeaders); + return this; + } + + /** + * Configures the HTTP response headers that will be captured as span attributes. + * + * @param responseHeaders A list of HTTP header names. + */ + @CanIgnoreReturnValue + public JettyClientTelemetryBuilder setCapturedResponseHeaders(List responseHeaders) { + builder.setCapturedResponseHeaders(responseHeaders); + return this; + } + + /** + * Configures the instrumentation to recognize an alternative set of HTTP request methods. + * + *

By default, this instrumentation defines "known" methods as the ones listed in RFC9110 and the PATCH + * method defined in RFC5789. + * + *

Note: calling this method overrides the default known method sets completely; it does + * not supplement it. + * + * @param knownMethods A set of recognized HTTP request methods. + * @see HttpClientAttributesExtractorBuilder#setKnownMethods(Set) + */ + @CanIgnoreReturnValue + public JettyClientTelemetryBuilder setKnownMethods(Set knownMethods) { + builder.setKnownMethods(knownMethods); + return this; + } + + /** + * Configures the instrumentation to emit experimental HTTP client metrics. + * + * @param emitExperimentalHttpClientMetrics {@code true} if the experimental HTTP client metrics + * are to be emitted. + */ + @CanIgnoreReturnValue + public JettyClientTelemetryBuilder setEmitExperimentalHttpClientMetrics( + boolean emitExperimentalHttpClientMetrics) { + builder.setEmitExperimentalHttpClientMetrics(emitExperimentalHttpClientMetrics); + return this; + } + + /** Sets custom {@link SpanNameExtractor} via transform function. */ + @CanIgnoreReturnValue + public JettyClientTelemetryBuilder setSpanNameExtractor( + Function, ? extends SpanNameExtractor> + spanNameExtractorTransformer) { + builder.setSpanNameExtractor(spanNameExtractorTransformer); + return this; + } + + /** + * Returns a new {@link JettyClientTelemetry} with the settings of this {@link + * JettyClientTelemetryBuilder}. + */ + public JettyClientTelemetry build() { + TracingHttpClient tracingHttpClient = + TracingHttpClient.buildNew(builder.build(), sslContextFactory, httpClientTransport); + + return new JettyClientTelemetry(tracingHttpClient); + } +} diff --git a/instrumentation/jetty-httpclient/jetty-httpclient-12.0/library/src/main/java/io/opentelemetry/instrumentation/jetty/httpclient/v12_0/TracingHttpClient.java b/instrumentation/jetty-httpclient/jetty-httpclient-12.0/library/src/main/java/io/opentelemetry/instrumentation/jetty/httpclient/v12_0/TracingHttpClient.java new file mode 100644 index 000000000000..6507d00bd7dd --- /dev/null +++ b/instrumentation/jetty-httpclient/jetty-httpclient-12.0/library/src/main/java/io/opentelemetry/instrumentation/jetty/httpclient/v12_0/TracingHttpClient.java @@ -0,0 +1,60 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.jetty.httpclient.v12_0; + +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import java.net.URI; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.HttpClientTransport; +import org.eclipse.jetty.client.Request; +import org.eclipse.jetty.client.Response; +import org.eclipse.jetty.client.transport.HttpConversation; +import org.eclipse.jetty.util.ssl.SslContextFactory; + +class TracingHttpClient extends HttpClient { + + private final Instrumenter instrumenter; + + TracingHttpClient(Instrumenter instrumenter) { + this.instrumenter = instrumenter; + } + + TracingHttpClient( + Instrumenter instrumenter, SslContextFactory.Client sslContextFactory) { + setSslContextFactory(sslContextFactory); + this.instrumenter = instrumenter; + } + + TracingHttpClient( + Instrumenter instrumenter, + HttpClientTransport transport, + SslContextFactory.Client sslContextFactory) { + super(transport); + setSslContextFactory(sslContextFactory); + this.instrumenter = instrumenter; + } + + static TracingHttpClient buildNew( + Instrumenter instrumenter, + SslContextFactory.Client sslContextFactory, + HttpClientTransport httpClientTransport) { + TracingHttpClient tracingHttpClient; + if (sslContextFactory != null && httpClientTransport != null) { + tracingHttpClient = + new TracingHttpClient(instrumenter, httpClientTransport, sslContextFactory); + } else if (sslContextFactory != null) { + tracingHttpClient = new TracingHttpClient(instrumenter, sslContextFactory); + } else { + tracingHttpClient = new TracingHttpClient(instrumenter); + } + return tracingHttpClient; + } + + @Override + public Request newRequest(URI uri) { + return new TracingHttpRequest(this, new HttpConversation(), uri, instrumenter); + } +} diff --git a/instrumentation/jetty-httpclient/jetty-httpclient-12.0/library/src/main/java/io/opentelemetry/instrumentation/jetty/httpclient/v12_0/TracingHttpRequest.java b/instrumentation/jetty-httpclient/jetty-httpclient-12.0/library/src/main/java/io/opentelemetry/instrumentation/jetty/httpclient/v12_0/TracingHttpRequest.java new file mode 100644 index 000000000000..9bdc687c250d --- /dev/null +++ b/instrumentation/jetty-httpclient/jetty-httpclient-12.0/library/src/main/java/io/opentelemetry/instrumentation/jetty/httpclient/v12_0/TracingHttpRequest.java @@ -0,0 +1,99 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.jetty.httpclient.v12_0; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import io.opentelemetry.instrumentation.jetty.httpclient.v12_0.internal.JettyClientTracingListener; +import java.net.URI; +import java.nio.ByteBuffer; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.Request; +import org.eclipse.jetty.client.Response; +import org.eclipse.jetty.client.transport.HttpConversation; +import org.eclipse.jetty.client.transport.HttpRequest; + +class TracingHttpRequest extends HttpRequest { + + private final Instrumenter instrumenter; + private Context parentContext; + + public TracingHttpRequest( + HttpClient client, + HttpConversation conversation, + URI uri, + Instrumenter instrumenter) { + super(client, conversation, uri); + this.instrumenter = instrumenter; + } + + @Override + public void send(Response.CompleteListener listener) { + parentContext = Context.current(); + // start span and attach listeners. + JettyClientTracingListener.handleRequest(parentContext, this, instrumenter); + super.send( + result -> { + try (Scope scope = openScope()) { + listener.onComplete(result); + } + }); + } + + private Scope openScope() { + return parentContext != null ? parentContext.makeCurrent() : null; + } + + @Override + public void notifyQueued() { + try (Scope scope = openScope()) { + super.notifyQueued(); + } + } + + @Override + public void notifyBegin() { + try (Scope scope = openScope()) { + super.notifyBegin(); + } + } + + @Override + public void notifyHeaders() { + try (Scope scope = openScope()) { + super.notifyHeaders(); + } + } + + @Override + public void notifyCommit() { + try (Scope scope = openScope()) { + super.notifyCommit(); + } + } + + @Override + public void notifyContent(ByteBuffer byteBuffer) { + try (Scope scope = openScope()) { + super.notifyContent(byteBuffer); + } + } + + @Override + public void notifySuccess() { + try (Scope scope = openScope()) { + super.notifySuccess(); + } + } + + @Override + public void notifyFailure(Throwable failure) { + try (Scope scope = openScope()) { + super.notifyFailure(failure); + } + } +} diff --git a/instrumentation/jetty-httpclient/jetty-httpclient-12.0/library/src/main/java/io/opentelemetry/instrumentation/jetty/httpclient/v12_0/internal/HttpHeaderSetter.java b/instrumentation/jetty-httpclient/jetty-httpclient-12.0/library/src/main/java/io/opentelemetry/instrumentation/jetty/httpclient/v12_0/internal/HttpHeaderSetter.java new file mode 100644 index 000000000000..b094abd3a4dc --- /dev/null +++ b/instrumentation/jetty-httpclient/jetty-httpclient-12.0/library/src/main/java/io/opentelemetry/instrumentation/jetty/httpclient/v12_0/internal/HttpHeaderSetter.java @@ -0,0 +1,24 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.jetty.httpclient.v12_0.internal; + +import io.opentelemetry.context.propagation.TextMapSetter; +import org.eclipse.jetty.client.Request; +import org.eclipse.jetty.http.HttpField; + +enum HttpHeaderSetter implements TextMapSetter { + INSTANCE; + + @Override + public void set(Request request, String key, String value) { + if (request != null) { + request.headers( + httpFields -> { + httpFields.put(new HttpField(key, value)); + }); + } + } +} diff --git a/instrumentation/jetty-httpclient/jetty-httpclient-12.0/library/src/main/java/io/opentelemetry/instrumentation/jetty/httpclient/v12_0/internal/JettyClientHttpAttributesGetter.java b/instrumentation/jetty-httpclient/jetty-httpclient-12.0/library/src/main/java/io/opentelemetry/instrumentation/jetty/httpclient/v12_0/internal/JettyClientHttpAttributesGetter.java new file mode 100644 index 000000000000..6a241120fd55 --- /dev/null +++ b/instrumentation/jetty-httpclient/jetty-httpclient-12.0/library/src/main/java/io/opentelemetry/instrumentation/jetty/httpclient/v12_0/internal/JettyClientHttpAttributesGetter.java @@ -0,0 +1,84 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.jetty.httpclient.v12_0.internal; + +import io.opentelemetry.instrumentation.api.internal.HttpProtocolUtil; +import io.opentelemetry.instrumentation.api.semconv.http.HttpClientAttributesGetter; +import java.util.List; +import javax.annotation.Nullable; +import org.eclipse.jetty.client.Request; +import org.eclipse.jetty.client.Response; +import org.eclipse.jetty.http.HttpVersion; + +/** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time. + */ +public enum JettyClientHttpAttributesGetter + implements HttpClientAttributesGetter { + INSTANCE; + + @Override + @Nullable + public String getHttpRequestMethod(Request request) { + return request.getMethod(); + } + + @Override + @Nullable + public String getUrlFull(Request request) { + return request.getURI().toString(); + } + + @Override + public List getHttpRequestHeader(Request request, String name) { + return request.getHeaders().getValuesList(name); + } + + @Override + public Integer getHttpResponseStatusCode( + Request request, Response response, @Nullable Throwable error) { + return response.getStatus(); + } + + @Override + public List getHttpResponseHeader(Request request, Response response, String name) { + return response.getHeaders().getValuesList(name); + } + + @Nullable + @Override + public String getNetworkProtocolName(Request request, @Nullable Response response) { + return "http"; + } + + @Nullable + @Override + public String getNetworkProtocolVersion(Request request, @Nullable Response response) { + HttpVersion httpVersion = null; + if (response != null) { + httpVersion = response.getVersion(); + } + if (httpVersion == null) { + httpVersion = request.getVersion(); + } + if (httpVersion == null) { + return null; + } + return HttpProtocolUtil.getVersion(httpVersion.toString()); + } + + @Override + @Nullable + public String getServerAddress(Request request) { + return request.getHost(); + } + + @Override + public Integer getServerPort(Request request) { + return request.getPort(); + } +} diff --git a/instrumentation/jetty-httpclient/jetty-httpclient-12.0/library/src/main/java/io/opentelemetry/instrumentation/jetty/httpclient/v12_0/internal/JettyClientTracingListener.java b/instrumentation/jetty-httpclient/jetty-httpclient-12.0/library/src/main/java/io/opentelemetry/instrumentation/jetty/httpclient/v12_0/internal/JettyClientTracingListener.java new file mode 100644 index 000000000000..be20a319a1cc --- /dev/null +++ b/instrumentation/jetty-httpclient/jetty-httpclient-12.0/library/src/main/java/io/opentelemetry/instrumentation/jetty/httpclient/v12_0/internal/JettyClientTracingListener.java @@ -0,0 +1,63 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.jetty.httpclient.v12_0.internal; + +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import javax.annotation.Nullable; +import org.eclipse.jetty.client.Request; +import org.eclipse.jetty.client.Response; + +/** + * JettyClientTracingListener performs two actions when {@link #handleRequest(Context, Request, + * Instrumenter)} is called 1. Start the CLIENT span 2. Set the listener callbacks for each + * lifecycle action that signal end of the request. + * + *

This class is internal and is hence not for public use. Its APIs are unstable and can change + * at any time. + */ +public final class JettyClientTracingListener + implements Request.FailureListener, Response.SuccessListener, Response.FailureListener { + + private final Context context; + + private final Instrumenter instrumenter; + + private JettyClientTracingListener( + Context context, Instrumenter instrumenter) { + this.context = context; + this.instrumenter = instrumenter; + } + + @Nullable + public static Context handleRequest( + Context parentContext, Request jettyRequest, Instrumenter instrumenter) { + if (!instrumenter.shouldStart(parentContext, jettyRequest)) { + return null; + } + + Context context = instrumenter.start(parentContext, jettyRequest); + + JettyClientTracingListener listener = new JettyClientTracingListener(context, instrumenter); + jettyRequest.onRequestFailure(listener).onResponseFailure(listener).onResponseSuccess(listener); + return context; + } + + @Override + public void onFailure(Response response, Throwable t) { + instrumenter.end(this.context, response.getRequest(), response, t); + } + + @Override + public void onFailure(Request request, Throwable t) { + instrumenter.end(this.context, request, null, t); + } + + @Override + public void onSuccess(Response response) { + instrumenter.end(this.context, response.getRequest(), response, null); + } +} diff --git a/instrumentation/jetty-httpclient/jetty-httpclient-12.0/library/src/main/java/io/opentelemetry/instrumentation/jetty/httpclient/v12_0/internal/JettyHttpClientInstrumenterBuilderFactory.java b/instrumentation/jetty-httpclient/jetty-httpclient-12.0/library/src/main/java/io/opentelemetry/instrumentation/jetty/httpclient/v12_0/internal/JettyHttpClientInstrumenterBuilderFactory.java new file mode 100644 index 000000000000..386a5e6c816b --- /dev/null +++ b/instrumentation/jetty-httpclient/jetty-httpclient-12.0/library/src/main/java/io/opentelemetry/instrumentation/jetty/httpclient/v12_0/internal/JettyHttpClientInstrumenterBuilderFactory.java @@ -0,0 +1,28 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.jetty.httpclient.v12_0.internal; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.instrumentation.api.incubator.builder.internal.DefaultHttpClientInstrumenterBuilder; +import org.eclipse.jetty.client.Request; +import org.eclipse.jetty.client.Response; + +/** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time. + */ +public class JettyHttpClientInstrumenterBuilderFactory { + private static final String INSTRUMENTATION_NAME = "io.opentelemetry.jetty-httpclient-12.0"; + + private JettyHttpClientInstrumenterBuilderFactory() {} + + public static DefaultHttpClientInstrumenterBuilder create( + OpenTelemetry openTelemetry) { + return new DefaultHttpClientInstrumenterBuilder<>( + INSTRUMENTATION_NAME, openTelemetry, JettyClientHttpAttributesGetter.INSTANCE) + .setHeaderSetter(HttpHeaderSetter.INSTANCE); + } +} diff --git a/instrumentation/jetty-httpclient/jetty-httpclient-12.0/library/src/test/java/io/opentelemetry/instrumentation/jetty/httpclient/v12_0/JettyHttpClient12LibraryTest.java b/instrumentation/jetty-httpclient/jetty-httpclient-12.0/library/src/test/java/io/opentelemetry/instrumentation/jetty/httpclient/v12_0/JettyHttpClient12LibraryTest.java new file mode 100644 index 000000000000..e7cf65fb206d --- /dev/null +++ b/instrumentation/jetty-httpclient/jetty-httpclient-12.0/library/src/test/java/io/opentelemetry/instrumentation/jetty/httpclient/v12_0/JettyHttpClient12LibraryTest.java @@ -0,0 +1,39 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.jetty.httpclient.v12_0; + +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpClientTest; +import io.opentelemetry.instrumentation.testing.junit.http.HttpClientInstrumentationExtension; +import java.util.Collections; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.util.ssl.SslContextFactory; +import org.junit.jupiter.api.extension.RegisterExtension; + +class JettyHttpClient12LibraryTest extends AbstractJettyClient12Test { + + @RegisterExtension + static final InstrumentationExtension testing = HttpClientInstrumentationExtension.forLibrary(); + + @Override + protected HttpClient createStandardClient() { + return JettyClientTelemetry.builder(testing.getOpenTelemetry()) + .setCapturedRequestHeaders( + Collections.singletonList(AbstractHttpClientTest.TEST_REQUEST_HEADER)) + .setCapturedResponseHeaders( + Collections.singletonList(AbstractHttpClientTest.TEST_RESPONSE_HEADER)) + .build() + .getHttpClient(); + } + + @Override + protected HttpClient createHttpsClient(SslContextFactory.Client sslContextFactory) { + return JettyClientTelemetry.builder(testing.getOpenTelemetry()) + .setSslContextFactory(sslContextFactory) + .build() + .getHttpClient(); + } +} diff --git a/instrumentation/jetty-httpclient/jetty-httpclient-12.0/testing/build.gradle.kts b/instrumentation/jetty-httpclient/jetty-httpclient-12.0/testing/build.gradle.kts new file mode 100644 index 000000000000..6205a47c0461 --- /dev/null +++ b/instrumentation/jetty-httpclient/jetty-httpclient-12.0/testing/build.gradle.kts @@ -0,0 +1,11 @@ +plugins { + id("otel.java-conventions") +} + +dependencies { + api(project(":testing-common")) + + api("org.eclipse.jetty:jetty-client:12.0.0") + + implementation("io.opentelemetry:opentelemetry-api") +} diff --git a/instrumentation/jetty-httpclient/jetty-httpclient-12.0/testing/src/main/java/io/opentelemetry/instrumentation/jetty/httpclient/v12_0/AbstractJettyClient12Test.java b/instrumentation/jetty-httpclient/jetty-httpclient-12.0/testing/src/main/java/io/opentelemetry/instrumentation/jetty/httpclient/v12_0/AbstractJettyClient12Test.java new file mode 100644 index 000000000000..1352269c4030 --- /dev/null +++ b/instrumentation/jetty-httpclient/jetty-httpclient-12.0/testing/src/main/java/io/opentelemetry/instrumentation/jetty/httpclient/v12_0/AbstractJettyClient12Test.java @@ -0,0 +1,125 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.jetty.httpclient.v12_0; + +import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpClientTest; +import io.opentelemetry.instrumentation.testing.junit.http.HttpClientResult; +import io.opentelemetry.instrumentation.testing.junit.http.HttpClientTestOptions; +import java.net.URI; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import org.eclipse.jetty.client.ContentResponse; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.Request; +import org.eclipse.jetty.client.Response; +import org.eclipse.jetty.http.HttpField; +import org.eclipse.jetty.util.ssl.SslContextFactory; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; + +public abstract class AbstractJettyClient12Test extends AbstractHttpClientTest { + + protected abstract HttpClient createStandardClient(); + + protected abstract HttpClient createHttpsClient(SslContextFactory.Client sslContextFactory); + + protected HttpClient client = createStandardClient(); + protected HttpClient httpsClient; + + @BeforeEach + public void before() throws Exception { + client.setConnectTimeout(CONNECTION_TIMEOUT.toMillis()); + client.start(); + + SslContextFactory.Client tlsCtx = new SslContextFactory.Client(); + httpsClient = createHttpsClient(tlsCtx); + httpsClient.setFollowRedirects(false); + httpsClient.start(); + } + + @AfterEach + public void after() throws Exception { + client.stop(); + httpsClient.stop(); + } + + @Override + protected void configure(HttpClientTestOptions.Builder optionsBuilder) { + // disable redirect tests + optionsBuilder.disableTestRedirects(); + // jetty 12 does not support to reuse request + // use request.send() twice will block the program infinitely + optionsBuilder.disableTestReusedRequest(); + optionsBuilder.spanEndsAfterBody(); + } + + @Override + public Request buildRequest(String method, URI uri, Map headers) { + HttpClient theClient = Objects.equals(uri.getScheme(), "https") ? httpsClient : client; + + Request request = theClient.newRequest(uri); + request.agent("Jetty"); + + request.method(method); + if (uri.toString().contains("/read-timeout")) { + request.timeout(READ_TIMEOUT.toMillis(), TimeUnit.MILLISECONDS); + } + + return request; + } + + @Override + public int sendRequest(Request request, String method, URI uri, Map headers) + throws ExecutionException, InterruptedException, TimeoutException { + headers.forEach((k, v) -> request.headers(httpFields -> httpFields.put(new HttpField(k, v)))); + + ContentResponse response = request.send(); + + return response.getStatus(); + } + + @Override + public void sendRequestWithCallback( + Request request, + String method, + URI uri, + Map headers, + HttpClientResult requestResult) { + JettyClientListener clientListener = new JettyClientListener(); + + request.onRequestFailure(clientListener); + request.onResponseFailure(clientListener); + headers.forEach((k, v) -> request.headers(httpFields -> httpFields.put(new HttpField(k, v)))); + + request.send( + result -> { + if (clientListener.failure != null) { + requestResult.complete(clientListener.failure); + return; + } + + requestResult.complete(result.getResponse().getStatus()); + }); + } + + private static class JettyClientListener + implements Request.FailureListener, Response.FailureListener { + volatile Throwable failure; + + @Override + public void onFailure(Request request, Throwable failure) { + this.failure = failure; + } + + @Override + public void onFailure(Response response, Throwable failure) { + this.failure = failure; + } + } +} diff --git a/instrumentation/jetty-httpclient/jetty-httpclient-9.2/javaagent/build.gradle.kts b/instrumentation/jetty-httpclient/jetty-httpclient-9.2/javaagent/build.gradle.kts index 00e213117361..71d9062f5375 100644 --- a/instrumentation/jetty-httpclient/jetty-httpclient-9.2/javaagent/build.gradle.kts +++ b/instrumentation/jetty-httpclient/jetty-httpclient-9.2/javaagent/build.gradle.kts @@ -18,6 +18,8 @@ dependencies { library("org.eclipse.jetty:jetty-client:$jettyVers_base9") + testInstrumentation(project(":instrumentation:jetty-httpclient:jetty-httpclient-12.0:javaagent")) + testImplementation(project(":instrumentation:jetty-httpclient:jetty-httpclient-9.2:testing")) latestDepTestLibrary("org.eclipse.jetty:jetty-client:9.+") // documented limitation diff --git a/instrumentation/jetty-httpclient/jetty-httpclient-9.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jetty/httpclient/v9_2/JettyHttpClient9Instrumentation.java b/instrumentation/jetty-httpclient/jetty-httpclient-9.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jetty/httpclient/v9_2/JettyHttpClient9Instrumentation.java index 7ad866f0f525..42152897660c 100644 --- a/instrumentation/jetty-httpclient/jetty-httpclient-9.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jetty/httpclient/v9_2/JettyHttpClient9Instrumentation.java +++ b/instrumentation/jetty-httpclient/jetty-httpclient-9.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jetty/httpclient/v9_2/JettyHttpClient9Instrumentation.java @@ -14,7 +14,7 @@ import io.opentelemetry.context.Context; import io.opentelemetry.context.Scope; -import io.opentelemetry.instrumentation.jetty.httpclient.v9_2.internal.JettyHttpClient9TracingInterceptor; +import io.opentelemetry.instrumentation.jetty.httpclient.v9_2.internal.JettyClientTracingListener; import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; import java.util.List; @@ -50,19 +50,13 @@ public static void addTracingEnter( @Advice.Local("otelContext") Context context, @Advice.Local("otelScope") Scope scope) { Context parentContext = currentContext(); - if (!instrumenter().shouldStart(parentContext, httpRequest)) { + context = + JettyClientTracingListener.handleRequest(parentContext, httpRequest, instrumenter()); + if (context == null) { return; } - // First step is to attach the tracer to the Jetty request. Request listeners are wrapped here - JettyHttpClient9TracingInterceptor requestInterceptor = - new JettyHttpClient9TracingInterceptor(parentContext, instrumenter()); - requestInterceptor.attachToRequest(httpRequest); - - // Second step is to wrap all the important result callback listeners = wrapResponseListeners(parentContext, listeners); - - context = requestInterceptor.getContext(); scope = context.makeCurrent(); } diff --git a/instrumentation/jetty-httpclient/jetty-httpclient-9.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jetty/httpclient/v9_2/JettyHttpClientSingletons.java b/instrumentation/jetty-httpclient/jetty-httpclient-9.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jetty/httpclient/v9_2/JettyHttpClientSingletons.java index 8107767b319c..b86165be3d51 100644 --- a/instrumentation/jetty-httpclient/jetty-httpclient-9.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jetty/httpclient/v9_2/JettyHttpClientSingletons.java +++ b/instrumentation/jetty-httpclient/jetty-httpclient-9.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jetty/httpclient/v9_2/JettyHttpClientSingletons.java @@ -7,24 +7,16 @@ import io.opentelemetry.api.GlobalOpenTelemetry; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; -import io.opentelemetry.instrumentation.api.instrumenter.net.PeerServiceAttributesExtractor; -import io.opentelemetry.instrumentation.jetty.httpclient.v9_2.internal.JettyClientInstrumenterBuilder; -import io.opentelemetry.instrumentation.jetty.httpclient.v9_2.internal.JettyHttpClientNetAttributesGetter; -import io.opentelemetry.javaagent.bootstrap.internal.CommonConfig; +import io.opentelemetry.instrumentation.jetty.httpclient.v9_2.internal.JettyHttpClientInstrumenterBuilderFactory; +import io.opentelemetry.javaagent.bootstrap.internal.JavaagentHttpClientInstrumenters; import org.eclipse.jetty.client.api.Request; import org.eclipse.jetty.client.api.Response; public class JettyHttpClientSingletons { private static final Instrumenter INSTRUMENTER = - new JettyClientInstrumenterBuilder(GlobalOpenTelemetry.get()) - .addAttributeExtractor( - PeerServiceAttributesExtractor.create( - new JettyHttpClientNetAttributesGetter(), - CommonConfig.get().getPeerServiceMapping())) - .setCapturedRequestHeaders(CommonConfig.get().getClientRequestHeaders()) - .setCapturedResponseHeaders(CommonConfig.get().getClientResponseHeaders()) - .build(); + JavaagentHttpClientInstrumenters.create( + JettyHttpClientInstrumenterBuilderFactory.create(GlobalOpenTelemetry.get())); public static Instrumenter instrumenter() { return INSTRUMENTER; diff --git a/instrumentation/jetty-httpclient/jetty-httpclient-9.2/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/jetty/httpclient/v9_2/JettyHttpClient9AgentTest.groovy b/instrumentation/jetty-httpclient/jetty-httpclient-9.2/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/jetty/httpclient/v9_2/JettyHttpClient9AgentTest.groovy deleted file mode 100644 index 85e7bb3d9f9d..000000000000 --- a/instrumentation/jetty-httpclient/jetty-httpclient-9.2/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/jetty/httpclient/v9_2/JettyHttpClient9AgentTest.groovy +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.javaagent.instrumentation.jetty.httpclient.v9_2 - -import io.opentelemetry.instrumentation.jetty.httpclient.v9_2.AbstractJettyClient9Test -import io.opentelemetry.instrumentation.test.AgentTestTrait -import org.eclipse.jetty.client.HttpClient -import org.eclipse.jetty.util.ssl.SslContextFactory - -class JettyHttpClient9AgentTest extends AbstractJettyClient9Test implements AgentTestTrait { - - @Override - HttpClient createStandardClient() { - return new HttpClient() - } - - @Override - HttpClient createHttpsClient(SslContextFactory sslContextFactory) { - return new HttpClient(sslContextFactory) - } -} diff --git a/instrumentation/jetty-httpclient/jetty-httpclient-9.2/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/jetty/httpclient/v9_2/JettyHttpClient9AgentTest.java b/instrumentation/jetty-httpclient/jetty-httpclient-9.2/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/jetty/httpclient/v9_2/JettyHttpClient9AgentTest.java new file mode 100644 index 000000000000..55cf0b8530fa --- /dev/null +++ b/instrumentation/jetty-httpclient/jetty-httpclient-9.2/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/jetty/httpclient/v9_2/JettyHttpClient9AgentTest.java @@ -0,0 +1,29 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jetty.httpclient.v9_2; + +import io.opentelemetry.instrumentation.jetty.httpclient.v9_2.AbstractJettyClient9Test; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.http.HttpClientInstrumentationExtension; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.util.ssl.SslContextFactory; +import org.junit.jupiter.api.extension.RegisterExtension; + +class JettyHttpClient9AgentTest extends AbstractJettyClient9Test { + + @RegisterExtension + static final InstrumentationExtension extension = HttpClientInstrumentationExtension.forAgent(); + + @Override + public HttpClient createStandardClient() { + return new HttpClient(); + } + + @Override + public HttpClient createHttpsClient(SslContextFactory sslContextFactory) { + return new HttpClient(sslContextFactory); + } +} diff --git a/instrumentation/jetty-httpclient/jetty-httpclient-9.2/library/src/main/java/io/opentelemetry/instrumentation/jetty/httpclient/v9_2/JettyClientTelemetryBuilder.java b/instrumentation/jetty-httpclient/jetty-httpclient-9.2/library/src/main/java/io/opentelemetry/instrumentation/jetty/httpclient/v9_2/JettyClientTelemetryBuilder.java index ada867f212a3..a976fe7a2f30 100644 --- a/instrumentation/jetty-httpclient/jetty-httpclient-9.2/library/src/main/java/io/opentelemetry/instrumentation/jetty/httpclient/v9_2/JettyClientTelemetryBuilder.java +++ b/instrumentation/jetty-httpclient/jetty-httpclient-9.2/library/src/main/java/io/opentelemetry/instrumentation/jetty/httpclient/v9_2/JettyClientTelemetryBuilder.java @@ -7,9 +7,14 @@ import com.google.errorprone.annotations.CanIgnoreReturnValue; import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.instrumentation.api.incubator.builder.internal.DefaultHttpClientInstrumenterBuilder; import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; -import io.opentelemetry.instrumentation.jetty.httpclient.v9_2.internal.JettyClientInstrumenterBuilder; +import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor; +import io.opentelemetry.instrumentation.api.semconv.http.HttpClientAttributesExtractorBuilder; +import io.opentelemetry.instrumentation.jetty.httpclient.v9_2.internal.JettyHttpClientInstrumenterBuilderFactory; import java.util.List; +import java.util.Set; +import java.util.function.Function; import org.eclipse.jetty.client.HttpClientTransport; import org.eclipse.jetty.client.api.Request; import org.eclipse.jetty.client.api.Response; @@ -18,12 +23,12 @@ /** A builder of {@link JettyClientTelemetry}. */ public final class JettyClientTelemetryBuilder { - private final JettyClientInstrumenterBuilder instrumenterBuilder; + private final DefaultHttpClientInstrumenterBuilder builder; private HttpClientTransport httpClientTransport; private SslContextFactory sslContextFactory; JettyClientTelemetryBuilder(OpenTelemetry openTelemetry) { - instrumenterBuilder = new JettyClientInstrumenterBuilder(openTelemetry); + builder = JettyHttpClientInstrumenterBuilderFactory.create(openTelemetry); } @CanIgnoreReturnValue @@ -46,7 +51,7 @@ public JettyClientTelemetryBuilder setSslContextFactory(SslContextFactory sslCon @CanIgnoreReturnValue public JettyClientTelemetryBuilder addAttributeExtractor( AttributesExtractor attributesExtractor) { - instrumenterBuilder.addAttributeExtractor(attributesExtractor); + builder.addAttributeExtractor(attributesExtractor); return this; } @@ -57,7 +62,7 @@ public JettyClientTelemetryBuilder addAttributeExtractor( */ @CanIgnoreReturnValue public JettyClientTelemetryBuilder setCapturedRequestHeaders(List requestHeaders) { - instrumenterBuilder.setCapturedRequestHeaders(requestHeaders); + builder.setCapturedRequestHeaders(requestHeaders); return this; } @@ -68,7 +73,48 @@ public JettyClientTelemetryBuilder setCapturedRequestHeaders(List reques */ @CanIgnoreReturnValue public JettyClientTelemetryBuilder setCapturedResponseHeaders(List responseHeaders) { - instrumenterBuilder.setCapturedResponseHeaders(responseHeaders); + builder.setCapturedResponseHeaders(responseHeaders); + return this; + } + + /** + * Configures the instrumentation to recognize an alternative set of HTTP request methods. + * + *

By default, this instrumentation defines "known" methods as the ones listed in RFC9110 and the PATCH + * method defined in RFC5789. + * + *

Note: calling this method overrides the default known method sets completely; it does + * not supplement it. + * + * @param knownMethods A set of recognized HTTP request methods. + * @see HttpClientAttributesExtractorBuilder#setKnownMethods(Set) + */ + @CanIgnoreReturnValue + public JettyClientTelemetryBuilder setKnownMethods(Set knownMethods) { + builder.setKnownMethods(knownMethods); + return this; + } + + /** + * Configures the instrumentation to emit experimental HTTP client metrics. + * + * @param emitExperimentalHttpClientMetrics {@code true} if the experimental HTTP client metrics + * are to be emitted. + */ + @CanIgnoreReturnValue + public JettyClientTelemetryBuilder setEmitExperimentalHttpClientMetrics( + boolean emitExperimentalHttpClientMetrics) { + builder.setEmitExperimentalHttpClientMetrics(emitExperimentalHttpClientMetrics); + return this; + } + + /** Sets custom {@link SpanNameExtractor} via transform function. */ + @CanIgnoreReturnValue + public JettyClientTelemetryBuilder setSpanNameExtractor( + Function, ? extends SpanNameExtractor> + spanNameExtractorTransformer) { + builder.setSpanNameExtractor(spanNameExtractorTransformer); return this; } @@ -78,8 +124,7 @@ public JettyClientTelemetryBuilder setCapturedResponseHeaders(List respo */ public JettyClientTelemetry build() { TracingHttpClient tracingHttpClient = - TracingHttpClient.buildNew( - instrumenterBuilder.build(), this.sslContextFactory, this.httpClientTransport); + TracingHttpClient.buildNew(builder.build(), sslContextFactory, httpClientTransport); return new JettyClientTelemetry(tracingHttpClient); } diff --git a/instrumentation/jetty-httpclient/jetty-httpclient-9.2/library/src/main/java/io/opentelemetry/instrumentation/jetty/httpclient/v9_2/TracingHttpClient.java b/instrumentation/jetty-httpclient/jetty-httpclient-9.2/library/src/main/java/io/opentelemetry/instrumentation/jetty/httpclient/v9_2/TracingHttpClient.java index 094b38ced322..ffa837707f44 100644 --- a/instrumentation/jetty-httpclient/jetty-httpclient-9.2/library/src/main/java/io/opentelemetry/instrumentation/jetty/httpclient/v9_2/TracingHttpClient.java +++ b/instrumentation/jetty-httpclient/jetty-httpclient-9.2/library/src/main/java/io/opentelemetry/instrumentation/jetty/httpclient/v9_2/TracingHttpClient.java @@ -9,7 +9,7 @@ import io.opentelemetry.context.Context; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; -import io.opentelemetry.instrumentation.jetty.httpclient.v9_2.internal.JettyHttpClient9TracingInterceptor; +import io.opentelemetry.instrumentation.jetty.httpclient.v9_2.internal.JettyClientTracingListener; import java.util.List; import org.eclipse.jetty.client.HttpClient; import org.eclipse.jetty.client.HttpClientTransport; @@ -23,7 +23,6 @@ class TracingHttpClient extends HttpClient { private final Instrumenter instrumenter; TracingHttpClient(Instrumenter instrumenter) { - super(); this.instrumenter = instrumenter; } @@ -60,10 +59,12 @@ static TracingHttpClient buildNew( @Override protected void send(HttpRequest request, List listeners) { Context parentContext = Context.current(); - JettyHttpClient9TracingInterceptor requestInterceptor = - new JettyHttpClient9TracingInterceptor(parentContext, this.instrumenter); - requestInterceptor.attachToRequest(request); - List wrapped = wrapResponseListeners(parentContext, listeners); - super.send(request, wrapped); + Context context = + JettyClientTracingListener.handleRequest(parentContext, request, instrumenter); + // wrap listeners only when a span was started (context is not null) + if (context != null) { + listeners = wrapResponseListeners(parentContext, listeners); + } + super.send(request, listeners); } } diff --git a/instrumentation/jetty-httpclient/jetty-httpclient-9.2/library/src/main/java/io/opentelemetry/instrumentation/jetty/httpclient/v9_2/internal/JettyClientHttpAttributesGetter.java b/instrumentation/jetty-httpclient/jetty-httpclient-9.2/library/src/main/java/io/opentelemetry/instrumentation/jetty/httpclient/v9_2/internal/JettyClientHttpAttributesGetter.java index dad99a1a6f37..75609b760582 100644 --- a/instrumentation/jetty-httpclient/jetty-httpclient-9.2/library/src/main/java/io/opentelemetry/instrumentation/jetty/httpclient/v9_2/internal/JettyClientHttpAttributesGetter.java +++ b/instrumentation/jetty-httpclient/jetty-httpclient-9.2/library/src/main/java/io/opentelemetry/instrumentation/jetty/httpclient/v9_2/internal/JettyClientHttpAttributesGetter.java @@ -5,13 +5,20 @@ package io.opentelemetry.instrumentation.jetty.httpclient.v9_2.internal; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpClientAttributesGetter; +import io.opentelemetry.instrumentation.api.internal.HttpProtocolUtil; +import io.opentelemetry.instrumentation.api.semconv.http.HttpClientAttributesGetter; import java.util.List; import javax.annotation.Nullable; import org.eclipse.jetty.client.api.Request; import org.eclipse.jetty.client.api.Response; +import org.eclipse.jetty.http.HttpVersion; -enum JettyClientHttpAttributesGetter implements HttpClientAttributesGetter { +/** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time. + */ +public enum JettyClientHttpAttributesGetter + implements HttpClientAttributesGetter { INSTANCE; @Override @@ -41,4 +48,37 @@ public Integer getHttpResponseStatusCode( public List getHttpResponseHeader(Request request, Response response, String name) { return response.getHeaders().getValuesList(name); } + + @Nullable + @Override + public String getNetworkProtocolName(Request request, @Nullable Response response) { + return "http"; + } + + @Nullable + @Override + public String getNetworkProtocolVersion(Request request, @Nullable Response response) { + HttpVersion httpVersion = null; + if (response != null) { + httpVersion = response.getVersion(); + } + if (httpVersion == null) { + httpVersion = request.getVersion(); + } + if (httpVersion == null) { + return null; + } + return HttpProtocolUtil.getVersion(httpVersion.toString()); + } + + @Override + @Nullable + public String getServerAddress(Request request) { + return request.getHost(); + } + + @Override + public Integer getServerPort(Request request) { + return request.getPort(); + } } diff --git a/instrumentation/jetty-httpclient/jetty-httpclient-9.2/library/src/main/java/io/opentelemetry/instrumentation/jetty/httpclient/v9_2/internal/JettyClientInstrumenterBuilder.java b/instrumentation/jetty-httpclient/jetty-httpclient-9.2/library/src/main/java/io/opentelemetry/instrumentation/jetty/httpclient/v9_2/internal/JettyClientInstrumenterBuilder.java deleted file mode 100644 index f88269afb8c0..000000000000 --- a/instrumentation/jetty-httpclient/jetty-httpclient-9.2/library/src/main/java/io/opentelemetry/instrumentation/jetty/httpclient/v9_2/internal/JettyClientInstrumenterBuilder.java +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.jetty.httpclient.v9_2.internal; - -import com.google.errorprone.annotations.CanIgnoreReturnValue; -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.http.HttpClientAttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpClientAttributesExtractorBuilder; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpClientMetrics; -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 org.eclipse.jetty.client.api.Request; -import org.eclipse.jetty.client.api.Response; - -/** - * This class is internal and is hence not for public use. Its APIs are unstable and can change at - * any time. - */ -public final class JettyClientInstrumenterBuilder { - - private static final String INSTRUMENTATION_NAME = "io.opentelemetry.jetty-httpclient-9.2"; - - private final OpenTelemetry openTelemetry; - - private final List> additionalExtractors = - new ArrayList<>(); - private final HttpClientAttributesExtractorBuilder - httpAttributesExtractorBuilder = - HttpClientAttributesExtractor.builder( - JettyClientHttpAttributesGetter.INSTANCE, new JettyHttpClientNetAttributesGetter()); - - public JettyClientInstrumenterBuilder(OpenTelemetry openTelemetry) { - this.openTelemetry = openTelemetry; - } - - @CanIgnoreReturnValue - public JettyClientInstrumenterBuilder addAttributeExtractor( - AttributesExtractor attributesExtractor) { - additionalExtractors.add(attributesExtractor); - return this; - } - - @CanIgnoreReturnValue - public JettyClientInstrumenterBuilder setCapturedRequestHeaders(List requestHeaders) { - httpAttributesExtractorBuilder.setCapturedRequestHeaders(requestHeaders); - return this; - } - - @CanIgnoreReturnValue - public JettyClientInstrumenterBuilder setCapturedResponseHeaders(List responseHeaders) { - httpAttributesExtractorBuilder.setCapturedResponseHeaders(responseHeaders); - return this; - } - - public Instrumenter build() { - JettyClientHttpAttributesGetter httpAttributesGetter = JettyClientHttpAttributesGetter.INSTANCE; - - return Instrumenter.builder( - this.openTelemetry, - INSTRUMENTATION_NAME, - HttpSpanNameExtractor.create(httpAttributesGetter)) - .setSpanStatusExtractor(HttpSpanStatusExtractor.create(httpAttributesGetter)) - .addAttributesExtractor(httpAttributesExtractorBuilder.build()) - .addAttributesExtractors(additionalExtractors) - .addOperationMetrics(HttpClientMetrics.get()) - .buildClientInstrumenter(HttpHeaderSetter.INSTANCE); - } -} diff --git a/instrumentation/jetty-httpclient/jetty-httpclient-9.2/library/src/main/java/io/opentelemetry/instrumentation/jetty/httpclient/v9_2/internal/JettyClientTracingListener.java b/instrumentation/jetty-httpclient/jetty-httpclient-9.2/library/src/main/java/io/opentelemetry/instrumentation/jetty/httpclient/v9_2/internal/JettyClientTracingListener.java new file mode 100644 index 000000000000..71909ac0557e --- /dev/null +++ b/instrumentation/jetty-httpclient/jetty-httpclient-9.2/library/src/main/java/io/opentelemetry/instrumentation/jetty/httpclient/v9_2/internal/JettyClientTracingListener.java @@ -0,0 +1,133 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.jetty.httpclient.v9_2.internal; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Proxy; +import java.util.ArrayList; +import java.util.List; +import java.util.ListIterator; +import java.util.logging.Logger; +import javax.annotation.Nullable; +import org.eclipse.jetty.client.HttpRequest; +import org.eclipse.jetty.client.api.Request; +import org.eclipse.jetty.client.api.Response; + +/** + * JettyClientTracingListener performs three actions when {@link #handleRequest(Context, + * HttpRequest, Instrumenter)} is called 1. Start the CLIENT span 2. Set the listener callbacks for + * each lifecycle action that signal end of the request 3. Wrap request listeners to propagate + * context into the listeners + * + *

This class is internal and is hence not for public use. Its APIs are unstable and can change + * at any time. + */ +public final class JettyClientTracingListener + implements Request.FailureListener, Response.SuccessListener, Response.FailureListener { + + private static final Logger logger = Logger.getLogger(JettyClientTracingListener.class.getName()); + + private static final Class[] requestlistenerInterfaces = { + Request.BeginListener.class, + Request.FailureListener.class, + Request.SuccessListener.class, + Request.HeadersListener.class, + Request.ContentListener.class, + Request.CommitListener.class, + Request.QueuedListener.class + }; + + private final Context context; + private final Instrumenter instrumenter; + + private JettyClientTracingListener( + Context context, Instrumenter instrumenter) { + this.context = context; + this.instrumenter = instrumenter; + } + + @Nullable + public static Context handleRequest( + Context parentContext, HttpRequest request, Instrumenter instrumenter) { + List current = + request.getRequestListeners(JettyClientTracingListener.class); + if (!current.isEmpty()) { + logger.warning("A tracing request listener is already in place for this request!"); + return null; + } + + if (!instrumenter.shouldStart(parentContext, request)) { + return null; + } + + Context context = instrumenter.start(parentContext, request); + + // wrap all important request-based listeners that may already be attached, null should ensure + // that all listeners are returned here + List existingListeners = request.getRequestListeners(null); + wrapRequestListeners(existingListeners, context); + + JettyClientTracingListener listener = new JettyClientTracingListener(context, instrumenter); + request.onRequestFailure(listener).onResponseFailure(listener).onResponseSuccess(listener); + + return context; + } + + private static void wrapRequestListeners( + List requestListeners, Context context) { + ListIterator iterator = requestListeners.listIterator(); + + while (iterator.hasNext()) { + List> interfaces = new ArrayList<>(); + Request.RequestListener listener = iterator.next(); + + Class listenerClass = listener.getClass(); + + for (Class type : requestlistenerInterfaces) { + if (type.isInstance(listener)) { + interfaces.add(type); + } + } + + if (interfaces.isEmpty()) { + continue; + } + + Request.RequestListener proxiedListener = + (Request.RequestListener) + Proxy.newProxyInstance( + listenerClass.getClassLoader(), + interfaces.toArray(new Class[0]), + (proxy, method, args) -> { + try (Scope ignored = context.makeCurrent()) { + return method.invoke(listener, args); + } catch (InvocationTargetException exception) { + throw exception.getCause(); + } + }); + + iterator.set(proxiedListener); + } + } + + @Override + public void onSuccess(Response response) { + instrumenter.end(this.context, response.getRequest(), response, null); + } + + @Override + public void onFailure(Request request, Throwable t) { + instrumenter.end(this.context, request, null, t); + } + + @Override + public void onFailure(Response response, Throwable t) { + instrumenter.end(this.context, response.getRequest(), response, t); + } +} diff --git a/instrumentation/jetty-httpclient/jetty-httpclient-9.2/library/src/main/java/io/opentelemetry/instrumentation/jetty/httpclient/v9_2/internal/JettyClientWrapUtil.java b/instrumentation/jetty-httpclient/jetty-httpclient-9.2/library/src/main/java/io/opentelemetry/instrumentation/jetty/httpclient/v9_2/internal/JettyClientWrapUtil.java index b6885bd3edaf..53a76256daa2 100644 --- a/instrumentation/jetty-httpclient/jetty-httpclient-9.2/library/src/main/java/io/opentelemetry/instrumentation/jetty/httpclient/v9_2/internal/JettyClientWrapUtil.java +++ b/instrumentation/jetty-httpclient/jetty-httpclient-9.2/library/src/main/java/io/opentelemetry/instrumentation/jetty/httpclient/v9_2/internal/JettyClientWrapUtil.java @@ -50,7 +50,7 @@ public static List wrapResponseListeners( private static Response.ResponseListener wrapTheListener( Response.ResponseListener listener, Context context) { - if (listener == null || listener instanceof JettyHttpClient9TracingInterceptor) { + if (listener == null || listener instanceof JettyClientTracingListener) { return listener; } diff --git a/instrumentation/jetty-httpclient/jetty-httpclient-9.2/library/src/main/java/io/opentelemetry/instrumentation/jetty/httpclient/v9_2/internal/JettyHttpClient9TracingInterceptor.java b/instrumentation/jetty-httpclient/jetty-httpclient-9.2/library/src/main/java/io/opentelemetry/instrumentation/jetty/httpclient/v9_2/internal/JettyHttpClient9TracingInterceptor.java deleted file mode 100644 index 4a7a761e44f4..000000000000 --- a/instrumentation/jetty-httpclient/jetty-httpclient-9.2/library/src/main/java/io/opentelemetry/instrumentation/jetty/httpclient/v9_2/internal/JettyHttpClient9TracingInterceptor.java +++ /dev/null @@ -1,182 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.jetty.httpclient.v9_2.internal; - -import io.opentelemetry.api.trace.Span; -import io.opentelemetry.context.Context; -import io.opentelemetry.context.Scope; -import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Proxy; -import java.util.ArrayList; -import java.util.List; -import java.util.ListIterator; -import java.util.logging.Logger; -import javax.annotation.Nullable; -import org.eclipse.jetty.client.api.Request; -import org.eclipse.jetty.client.api.Response; -import org.eclipse.jetty.client.api.Result; -import org.eclipse.jetty.http.HttpField; -import org.eclipse.jetty.http.HttpHeader; - -/** - * JettyHttpClient9TracingInterceptor does three jobs stimulated from the Jetty Request object from - * attachToRequest() 1. Start the CLIENT span and create the tracer 2. Set the listener callbacks - * for each important lifecycle actions that would cause the start and close of the span 3. Set - * callback wrappers on two important request-based callbacks - * - *

This class is internal and is hence not for public use. Its APIs are unstable and can change - * at any time. - */ -public class JettyHttpClient9TracingInterceptor - implements Request.BeginListener, - Request.FailureListener, - Response.SuccessListener, - Response.FailureListener, - Response.CompleteListener { - - private static final Logger logger = - Logger.getLogger(JettyHttpClient9TracingInterceptor.class.getName()); - - private static final Class[] requestlistenerInterfaces = { - Request.BeginListener.class, - Request.FailureListener.class, - Request.SuccessListener.class, - Request.HeadersListener.class, - Request.ContentListener.class, - Request.CommitListener.class, - Request.QueuedListener.class - }; - - @Nullable private Context context; - - @Nullable - public Context getContext() { - return this.context; - } - - private final Context parentContext; - - private final Instrumenter instrumenter; - - public JettyHttpClient9TracingInterceptor( - Context parentCtx, Instrumenter instrumenter) { - this.parentContext = parentCtx; - this.instrumenter = instrumenter; - } - - public void attachToRequest(Request jettyRequest) { - List current = - jettyRequest.getRequestListeners(JettyHttpClient9TracingInterceptor.class); - - if (!current.isEmpty()) { - logger.warning("A tracing interceptor is already in place for this request!"); - return; - } - startSpan(jettyRequest); - - // wrap all important request-based listeners that may already be attached, null should ensure - // are returned here - List existingListeners = jettyRequest.getRequestListeners(null); - wrapRequestListeners(existingListeners); - - jettyRequest - .onRequestBegin(this) - .onRequestFailure(this) - .onResponseFailure(this) - .onResponseSuccess(this); - } - - private void wrapRequestListeners(List requestListeners) { - - ListIterator iterator = requestListeners.listIterator(); - - while (iterator.hasNext()) { - List> interfaces = new ArrayList<>(); - Request.RequestListener listener = iterator.next(); - - Class listenerClass = listener.getClass(); - - for (Class type : requestlistenerInterfaces) { - if (type.isInstance(listener)) { - interfaces.add(type); - } - } - - if (interfaces.isEmpty()) { - continue; - } - - Request.RequestListener proxiedListener = - (Request.RequestListener) - Proxy.newProxyInstance( - listenerClass.getClassLoader(), - interfaces.toArray(new Class[0]), - (proxy, method, args) -> { - try (Scope ignored = context.makeCurrent()) { - return method.invoke(listener, args); - } catch (InvocationTargetException exception) { - throw exception.getCause(); - } - }); - - iterator.set(proxiedListener); - } - } - - private void startSpan(Request request) { - - if (!instrumenter.shouldStart(this.parentContext, request)) { - return; - } - this.context = instrumenter.start(this.parentContext, request); - } - - @Override - public void onBegin(Request request) { - if (this.context != null) { - Span span = Span.fromContext(this.context); - HttpField agentField = request.getHeaders().getField(HttpHeader.USER_AGENT); - if (agentField != null) { - span.setAttribute(SemanticAttributes.USER_AGENT_ORIGINAL, agentField.getValue()); - } - } - } - - @Override - public void onComplete(Result result) { - closeIfPossible(result.getResponse()); - } - - @Override - public void onSuccess(Response response) { - closeIfPossible(response); - } - - @Override - public void onFailure(Request request, Throwable t) { - if (this.context != null) { - instrumenter.end(this.context, request, null, t); - } - } - - @Override - public void onFailure(Response response, Throwable t) { - if (this.context != null) { - instrumenter.end(this.context, response.getRequest(), response, t); - } - } - - private void closeIfPossible(Response response) { - - if (this.context != null) { - instrumenter.end(this.context, response.getRequest(), response, null); - } else { - logger.fine("onComplete - could not find an otel context"); - } - } -} diff --git a/instrumentation/jetty-httpclient/jetty-httpclient-9.2/library/src/main/java/io/opentelemetry/instrumentation/jetty/httpclient/v9_2/internal/JettyHttpClientInstrumenterBuilderFactory.java b/instrumentation/jetty-httpclient/jetty-httpclient-9.2/library/src/main/java/io/opentelemetry/instrumentation/jetty/httpclient/v9_2/internal/JettyHttpClientInstrumenterBuilderFactory.java new file mode 100644 index 000000000000..f2be57153580 --- /dev/null +++ b/instrumentation/jetty-httpclient/jetty-httpclient-9.2/library/src/main/java/io/opentelemetry/instrumentation/jetty/httpclient/v9_2/internal/JettyHttpClientInstrumenterBuilderFactory.java @@ -0,0 +1,28 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.jetty.httpclient.v9_2.internal; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.instrumentation.api.incubator.builder.internal.DefaultHttpClientInstrumenterBuilder; +import org.eclipse.jetty.client.api.Request; +import org.eclipse.jetty.client.api.Response; + +/** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time. + */ +public class JettyHttpClientInstrumenterBuilderFactory { + private static final String INSTRUMENTATION_NAME = "io.opentelemetry.jetty-httpclient-9.2"; + + private JettyHttpClientInstrumenterBuilderFactory() {} + + public static DefaultHttpClientInstrumenterBuilder create( + OpenTelemetry openTelemetry) { + return new DefaultHttpClientInstrumenterBuilder<>( + INSTRUMENTATION_NAME, openTelemetry, JettyClientHttpAttributesGetter.INSTANCE) + .setHeaderSetter(HttpHeaderSetter.INSTANCE); + } +} diff --git a/instrumentation/jetty-httpclient/jetty-httpclient-9.2/library/src/main/java/io/opentelemetry/instrumentation/jetty/httpclient/v9_2/internal/JettyHttpClientNetAttributesGetter.java b/instrumentation/jetty-httpclient/jetty-httpclient-9.2/library/src/main/java/io/opentelemetry/instrumentation/jetty/httpclient/v9_2/internal/JettyHttpClientNetAttributesGetter.java deleted file mode 100644 index 60a0303dfe43..000000000000 --- a/instrumentation/jetty-httpclient/jetty-httpclient-9.2/library/src/main/java/io/opentelemetry/instrumentation/jetty/httpclient/v9_2/internal/JettyHttpClientNetAttributesGetter.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.jetty.httpclient.v9_2.internal; - -import io.opentelemetry.instrumentation.api.instrumenter.net.NetClientAttributesGetter; -import javax.annotation.Nullable; -import org.eclipse.jetty.client.api.Request; -import org.eclipse.jetty.client.api.Response; -import org.eclipse.jetty.http.HttpVersion; - -/** - * This class is internal and is hence not for public use. Its APIs are unstable and can change at - * any time. - */ -public class JettyHttpClientNetAttributesGetter - implements NetClientAttributesGetter { - - @Nullable - @Override - public String getNetworkProtocolName(Request request, @Nullable Response response) { - return "http"; - } - - @Nullable - @Override - public String getNetworkProtocolVersion(Request request, @Nullable Response response) { - HttpVersion httpVersion = null; - if (response != null) { - httpVersion = response.getVersion(); - } - if (httpVersion == null) { - httpVersion = request.getVersion(); - } - if (httpVersion == null) { - return null; - } - String version = httpVersion.toString(); - if (version.startsWith("HTTP/")) { - version = version.substring("HTTP/".length()); - } - return version; - } - - @Override - @Nullable - public String getServerAddress(Request request) { - return request.getHost(); - } - - @Override - @Nullable - public Integer getServerPort(Request request) { - return request.getPort(); - } -} diff --git a/instrumentation/jetty-httpclient/jetty-httpclient-9.2/library/src/test/groovy/io/opentelemetry/instrumentation/jetty/httpclient/v9_2/JettyHttpClient9LibraryTest.groovy b/instrumentation/jetty-httpclient/jetty-httpclient-9.2/library/src/test/groovy/io/opentelemetry/instrumentation/jetty/httpclient/v9_2/JettyHttpClient9LibraryTest.groovy deleted file mode 100644 index 2608486ad29a..000000000000 --- a/instrumentation/jetty-httpclient/jetty-httpclient-9.2/library/src/test/groovy/io/opentelemetry/instrumentation/jetty/httpclient/v9_2/JettyHttpClient9LibraryTest.groovy +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.jetty.httpclient.v9_2 - - -import io.opentelemetry.instrumentation.test.LibraryTestTrait -import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpClientTest -import org.eclipse.jetty.client.HttpClient -import org.eclipse.jetty.util.ssl.SslContextFactory - -class JettyHttpClient9LibraryTest extends AbstractJettyClient9Test implements LibraryTestTrait { - - @Override - HttpClient createStandardClient() { - return JettyClientTelemetry.builder(getOpenTelemetry()) - .setCapturedRequestHeaders([AbstractHttpClientTest.TEST_REQUEST_HEADER]) - .setCapturedResponseHeaders([AbstractHttpClientTest.TEST_RESPONSE_HEADER]) - .build().getHttpClient() - } - - @Override - HttpClient createHttpsClient(SslContextFactory sslContextFactory) { - return JettyClientTelemetry.builder(getOpenTelemetry()) - .setSslContextFactory(sslContextFactory) - .build() - .getHttpClient() - } -} diff --git a/instrumentation/jetty-httpclient/jetty-httpclient-9.2/library/src/test/java/io/opentelemetry/instrumentation/jetty/httpclient/v9_2/JettyHttpClient9LibraryTest.java b/instrumentation/jetty-httpclient/jetty-httpclient-9.2/library/src/test/java/io/opentelemetry/instrumentation/jetty/httpclient/v9_2/JettyHttpClient9LibraryTest.java new file mode 100644 index 000000000000..b325c961f035 --- /dev/null +++ b/instrumentation/jetty-httpclient/jetty-httpclient-9.2/library/src/test/java/io/opentelemetry/instrumentation/jetty/httpclient/v9_2/JettyHttpClient9LibraryTest.java @@ -0,0 +1,39 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.jetty.httpclient.v9_2; + +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpClientTest; +import io.opentelemetry.instrumentation.testing.junit.http.HttpClientInstrumentationExtension; +import java.util.Collections; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.util.ssl.SslContextFactory; +import org.junit.jupiter.api.extension.RegisterExtension; + +class JettyHttpClient9LibraryTest extends AbstractJettyClient9Test { + + @RegisterExtension + static final InstrumentationExtension extension = HttpClientInstrumentationExtension.forLibrary(); + + @Override + protected HttpClient createStandardClient() { + return JettyClientTelemetry.builder(testing.getOpenTelemetry()) + .setCapturedRequestHeaders( + Collections.singletonList(AbstractHttpClientTest.TEST_REQUEST_HEADER)) + .setCapturedResponseHeaders( + Collections.singletonList(AbstractHttpClientTest.TEST_RESPONSE_HEADER)) + .build() + .getHttpClient(); + } + + @Override + protected HttpClient createHttpsClient(SslContextFactory sslContextFactory) { + return JettyClientTelemetry.builder(testing.getOpenTelemetry()) + .setSslContextFactory(sslContextFactory) + .build() + .getHttpClient(); + } +} diff --git a/instrumentation/jetty-httpclient/jetty-httpclient-9.2/testing/build.gradle.kts b/instrumentation/jetty-httpclient/jetty-httpclient-9.2/testing/build.gradle.kts index 468e1fb658b1..5a40059553b7 100644 --- a/instrumentation/jetty-httpclient/jetty-httpclient-9.2/testing/build.gradle.kts +++ b/instrumentation/jetty-httpclient/jetty-httpclient-9.2/testing/build.gradle.kts @@ -10,9 +10,5 @@ dependencies { api("org.eclipse.jetty:jetty-client:$jettyVers_base9") - implementation("org.junit.jupiter:junit-jupiter-api") - - implementation("org.apache.groovy:groovy") implementation("io.opentelemetry:opentelemetry-api") - implementation("org.spockframework:spock-core") } diff --git a/instrumentation/jetty-httpclient/jetty-httpclient-9.2/testing/src/main/groovy/io/opentelemetry/instrumentation/jetty/httpclient/v9_2/AbstractJettyClient9Test.groovy b/instrumentation/jetty-httpclient/jetty-httpclient-9.2/testing/src/main/groovy/io/opentelemetry/instrumentation/jetty/httpclient/v9_2/AbstractJettyClient9Test.groovy deleted file mode 100644 index b588010030f9..000000000000 --- a/instrumentation/jetty-httpclient/jetty-httpclient-9.2/testing/src/main/groovy/io/opentelemetry/instrumentation/jetty/httpclient/v9_2/AbstractJettyClient9Test.groovy +++ /dev/null @@ -1,145 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.jetty.httpclient.v9_2 - - -import io.opentelemetry.instrumentation.test.base.HttpClientTest -import io.opentelemetry.instrumentation.testing.junit.http.HttpClientResult -import org.eclipse.jetty.client.HttpClient -import org.eclipse.jetty.client.api.ContentResponse -import org.eclipse.jetty.client.api.Request -import org.eclipse.jetty.client.api.Response -import org.eclipse.jetty.client.api.Result -import org.eclipse.jetty.http.HttpMethod -import org.eclipse.jetty.util.ssl.SslContextFactory -import spock.lang.Shared -import spock.lang.Unroll - -import java.util.concurrent.TimeUnit - -abstract class AbstractJettyClient9Test extends HttpClientTest { - - abstract HttpClient createStandardClient() - - abstract HttpClient createHttpsClient(SslContextFactory sslContextFactory) - - - @Shared - def client = createStandardClient() - @Shared - def httpsClient = null - - Request jettyRequest = null - - def setupSpec() { - //Start the main Jetty HttpClient and a https client - client.setConnectTimeout(CONNECT_TIMEOUT_MS) - client.start() - - SslContextFactory tlsCtx = new SslContextFactory() - httpsClient = createHttpsClient(tlsCtx) - httpsClient.setFollowRedirects(false) - httpsClient.start() - } - - @Override - Request buildRequest(String method, URI uri, Map headers) { - HttpClient theClient = uri.scheme == 'https' ? httpsClient : client - - Request request = theClient.newRequest(uri) - request.agent("Jetty") - - HttpMethod methodObj = HttpMethod.valueOf(method) - request.method(methodObj) - request.timeout(CONNECT_TIMEOUT_MS, TimeUnit.MILLISECONDS) - - jettyRequest = request - - return request - } - - @Override - String userAgent() { - return "Jetty" - } - - @Override - int sendRequest(Request request, String method, URI uri, Map headers) { - headers.each { k, v -> - request.header(k, v) - } - - ContentResponse response = request.send() - - return response.status - } - - private static class JettyClientListener implements Request.FailureListener, Response.FailureListener { - volatile Throwable failure - - @Override - void onFailure(Request requestF, Throwable failure) { - this.failure = failure - } - - @Override - void onFailure(Response responseF, Throwable failure) { - this.failure = failure - } - } - - @Override - void sendRequestWithCallback(Request request, String method, URI uri, Map headers, HttpClientResult requestResult) { - JettyClientListener jcl = new JettyClientListener() - - request.onRequestFailure(jcl) - request.onResponseFailure(jcl) - headers.each { k, v -> - request.header(k, v) - } - - request.send(new Response.CompleteListener() { - @Override - void onComplete(Result result) { - if (jcl.failure != null) { - requestResult.complete(jcl.failure) - return - } - - requestResult.complete(result.response.status) - } - }) - } - - @Override - boolean testRedirects() { - false - } - - @Unroll - def "test content of #method request #url"() { - when: - def request = buildRequest(method, url, [:]) - def response = request.send() - - then: - response.status == 200 - response.getContentAsString() == "Hello." - - assertTraces(1) { - trace(0, 2) { - clientSpan(it, 0, null, method, url) - serverSpan(it, 1, span(0)) - } - } - - where: - path << ["/success"] - - method = "GET" - url = resolveAddress(path) - } -} diff --git a/instrumentation/jetty-httpclient/jetty-httpclient-9.2/testing/src/main/java/io/opentelemetry/instrumentation/jetty/httpclient/v9_2/AbstractJettyClient9Test.java b/instrumentation/jetty-httpclient/jetty-httpclient-9.2/testing/src/main/java/io/opentelemetry/instrumentation/jetty/httpclient/v9_2/AbstractJettyClient9Test.java new file mode 100644 index 000000000000..e85ca66601c2 --- /dev/null +++ b/instrumentation/jetty-httpclient/jetty-httpclient-9.2/testing/src/main/java/io/opentelemetry/instrumentation/jetty/httpclient/v9_2/AbstractJettyClient9Test.java @@ -0,0 +1,115 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.jetty.httpclient.v9_2; + +import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpClientTest; +import io.opentelemetry.instrumentation.testing.junit.http.HttpClientResult; +import io.opentelemetry.instrumentation.testing.junit.http.HttpClientTestOptions; +import java.net.URI; +import java.util.Map; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.api.Request; +import org.eclipse.jetty.client.api.Response; +import org.eclipse.jetty.util.ssl.SslContextFactory; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.TestInstance; + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +public abstract class AbstractJettyClient9Test extends AbstractHttpClientTest { + + private HttpClient client; + private HttpClient httpsClient; + + @BeforeEach + public void before() throws Exception { + // Start the main Jetty HttpClient and a https client + client = createStandardClient(); + client.setConnectTimeout(CONNECTION_TIMEOUT.toMillis()); + client.start(); + + SslContextFactory tlsCtx = new SslContextFactory(); + httpsClient = createHttpsClient(tlsCtx); + httpsClient.setFollowRedirects(false); + httpsClient.start(); + } + + @AfterEach + public void after() throws Exception { + client.stop(); + httpsClient.stop(); + } + + @Override + protected void configure(HttpClientTestOptions.Builder optionsBuilder) { + optionsBuilder.disableTestRedirects(); + optionsBuilder.spanEndsAfterBody(); + } + + @Override + public Request buildRequest(String method, URI uri, Map headers) { + HttpClient theClient = uri.getScheme().equalsIgnoreCase("https") ? httpsClient : client; + Request request = theClient.newRequest(uri).method(method).agent("Jetty"); + headers.forEach(request::header); + + if (uri.toString().contains("/read-timeout")) { + request.timeout(READ_TIMEOUT.toMillis(), TimeUnit.MILLISECONDS); + } else if (uri.toString().contains("192.0.2.1")) { + request.timeout(CONNECTION_TIMEOUT.toMillis(), TimeUnit.MILLISECONDS); + } + + return request; + } + + @Override + public int sendRequest(Request httpRequest, String method, URI uri, Map headers) + throws ExecutionException, InterruptedException, TimeoutException { + return httpRequest.send().getStatus(); + } + + @Override + public void sendRequestWithCallback( + Request request, + String method, + URI uri, + Map headers, + HttpClientResult httpClientResult) { + JettyClientListener jcl = new JettyClientListener(); + request.onRequestFailure(jcl); + request.onResponseFailure(jcl); + headers.forEach(request::header); + request.send( + result -> { + if (jcl.failure != null) { + httpClientResult.complete(jcl.failure); + return; + } + httpClientResult.complete(result.getResponse().getStatus()); + }); + } + + private static class JettyClientListener + implements Request.FailureListener, Response.FailureListener { + private volatile Throwable failure; + + @Override + public void onFailure(Request requestF, Throwable failure) { + this.failure = failure; + } + + @Override + public void onFailure(Response responseF, Throwable failure) { + this.failure = failure; + } + } + + protected abstract HttpClient createStandardClient(); + + protected abstract HttpClient createHttpsClient(SslContextFactory sslContextFactory); +} diff --git a/instrumentation/jetty/README.md b/instrumentation/jetty/README.md index 17fa8aaa2168..7d68ceecd89e 100644 --- a/instrumentation/jetty/README.md +++ b/instrumentation/jetty/README.md @@ -5,8 +5,5 @@ 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. +- `jetty-11.0:javaagent` applies Jetty request handler instrumentation for versions `[11,12)` +- `jetty-12.0:javaagent` applies Jetty request handler instrumentation for versions `[12,)` diff --git a/instrumentation/jetty/jetty-11.0/javaagent/build.gradle.kts b/instrumentation/jetty/jetty-11.0/javaagent/build.gradle.kts index ef602f0ef142..6e133bb309de 100644 --- a/instrumentation/jetty/jetty-11.0/javaagent/build.gradle.kts +++ b/instrumentation/jetty/jetty-11.0/javaagent/build.gradle.kts @@ -6,7 +6,7 @@ muzzle { pass { group.set("org.eclipse.jetty") module.set("jetty-server") - versions.set("[11,)") + versions.set("[11, 12)") } } @@ -18,8 +18,11 @@ dependencies { bootstrap(project(":instrumentation:servlet:servlet-common:bootstrap")) testInstrumentation(project(":instrumentation:jetty:jetty-8.0:javaagent")) + testInstrumentation(project(":instrumentation:jetty:jetty-12.0:javaagent")) + // jetty-servlet does not exist in jetty 12, so we don't need to explicitly pin it to 11.+ testLibrary("org.eclipse.jetty:jetty-servlet:11.0.0") + latestDepTestLibrary("org.eclipse.jetty:jetty-server:11.+") } otelJava { diff --git a/instrumentation/jetty/jetty-11.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jetty/v11_0/Jetty11Singletons.java b/instrumentation/jetty/jetty-11.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jetty/v11_0/Jetty11Singletons.java index 240a96c39aca..0e7fe4081a1e 100644 --- a/instrumentation/jetty/jetty-11.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jetty/v11_0/Jetty11Singletons.java +++ b/instrumentation/jetty/jetty-11.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jetty/v11_0/Jetty11Singletons.java @@ -24,6 +24,7 @@ public final class Jetty11Singletons { ServletInstrumenterBuilder.create() .addContextCustomizer( (context, request, attributes) -> new AppServerBridge.Builder().init(context)) + .propagateOperationListenersToOnEnd() .build(INSTRUMENTATION_NAME, Servlet5Accessor.INSTANCE); private static final JettyHelper HELPER = diff --git a/instrumentation/jetty/jetty-11.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/jetty/v11_0/JettyHandlerTest.java b/instrumentation/jetty/jetty-11.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/jetty/v11_0/JettyHandlerTest.java index 4f7ecb70e4b8..4b7fd9f9d3b8 100644 --- a/instrumentation/jetty/jetty-11.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/jetty/v11_0/JettyHandlerTest.java +++ b/instrumentation/jetty/jetty-11.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/jetty/v11_0/JettyHandlerTest.java @@ -25,7 +25,7 @@ import io.opentelemetry.instrumentation.testing.junit.http.HttpServerTestOptions; import io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint; import io.opentelemetry.sdk.testing.assertj.SpanDataAssert; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.opentelemetry.semconv.HttpAttributes; import jakarta.servlet.DispatcherType; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; @@ -40,7 +40,7 @@ import org.eclipse.jetty.server.handler.ErrorHandler; import org.junit.jupiter.api.extension.RegisterExtension; -public class JettyHandlerTest extends AbstractHttpServerTest { +class JettyHandlerTest extends AbstractHttpServerTest { @RegisterExtension static final InstrumentationExtension testing = HttpServerInstrumentationExtension.forAgent(); @@ -62,25 +62,17 @@ protected void handleErrorPage( private static final TestHandler testHandler = new TestHandler(); @Override - protected Server setupServer() { + protected Server setupServer() throws Exception { Server server = new Server(port); server.setHandler(testHandler); server.addBean(errorHandler); - try { - server.start(); - } catch (Exception e) { - throw new RuntimeException(e); - } + server.start(); return server; } @Override - protected void stopServer(Server server) { - try { - server.stop(); - } catch (Exception e) { - throw new RuntimeException(e); - } + protected void stopServer(Server server) throws Exception { + server.stop(); } @Override @@ -88,7 +80,7 @@ protected void configure(HttpServerTestOptions options) { options.setHttpAttributes( unused -> Sets.difference( - DEFAULT_HTTP_ATTRIBUTES, Collections.singleton(SemanticAttributes.HTTP_ROUTE))); + DEFAULT_HTTP_ATTRIBUTES, Collections.singleton(HttpAttributes.HTTP_ROUTE))); options.setHasResponseSpan(endpoint -> endpoint == REDIRECT || endpoint == ERROR); options.setExpectedException(new IllegalStateException(EXCEPTION.getBody())); options.setHasResponseCustomizer(endpoint -> true); diff --git a/instrumentation/jetty/jetty-12.0/javaagent/build.gradle.kts b/instrumentation/jetty/jetty-12.0/javaagent/build.gradle.kts new file mode 100644 index 000000000000..4c97e1e4d052 --- /dev/null +++ b/instrumentation/jetty/jetty-12.0/javaagent/build.gradle.kts @@ -0,0 +1,27 @@ +plugins { + id("otel.javaagent-instrumentation") +} + +muzzle { + pass { + group.set("org.eclipse.jetty") + module.set("jetty-server") + versions.set("[12,)") + } +} + +dependencies { + library("org.eclipse.jetty:jetty-server:12.0.0") + + bootstrap(project(":instrumentation:servlet:servlet-common:bootstrap")) + implementation(project(":instrumentation:servlet:servlet-common:javaagent")) + + testInstrumentation(project(":instrumentation:jetty:jetty-8.0:javaagent")) + testInstrumentation(project(":instrumentation:jetty:jetty-11.0:javaagent")) + + testLibrary("org.eclipse.jetty.ee10:jetty-ee10-servlet:12.0.0") +} + +otelJava { + minJavaVersionSupported.set(JavaVersion.VERSION_17) +} diff --git a/instrumentation/jetty/jetty-12.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jetty/v12_0/Jetty12Helper.java b/instrumentation/jetty/jetty-12.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jetty/v12_0/Jetty12Helper.java new file mode 100644 index 000000000000..d17aba9c6d00 --- /dev/null +++ b/instrumentation/jetty/jetty-12.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jetty/v12_0/Jetty12Helper.java @@ -0,0 +1,62 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jetty.v12_0; + +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import io.opentelemetry.javaagent.bootstrap.servlet.AppServerBridge; +import io.opentelemetry.javaagent.instrumentation.servlet.ServletHelper; +import javax.annotation.Nullable; +import org.eclipse.jetty.server.HttpStream; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Response; + +public class Jetty12Helper { + private final Instrumenter instrumenter; + + Jetty12Helper(Instrumenter instrumenter) { + this.instrumenter = instrumenter; + } + + public boolean shouldStart(Context parentContext, Request request) { + return instrumenter.shouldStart(parentContext, request); + } + + public Context start(Context parentContext, Request request, Response response) { + Context context = instrumenter.start(parentContext, request); + request.addFailureListener(throwable -> end(context, request, response, throwable)); + // detect request completion + // https://github.com/jetty/jetty.project/blob/52d94174e2c7a6e794c6377dcf9cd3ed0b9e1806/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/EventsHandler.java#L75 + request.addHttpStreamWrapper( + stream -> + new HttpStream.Wrapper(stream) { + @Override + public void succeeded() { + end(context, request, response, null); + super.succeeded(); + } + + @Override + public void failed(Throwable throwable) { + end(context, request, response, throwable); + super.failed(throwable); + } + }); + + return context; + } + + public void end(Context context, Request request, Response response, @Nullable Throwable error) { + if (error == null) { + error = AppServerBridge.getException(context); + } + if (error == null) { + error = (Throwable) request.getAttribute(ServletHelper.ASYNC_EXCEPTION_ATTRIBUTE); + } + + instrumenter.end(context, request, response, error); + } +} diff --git a/instrumentation/jetty/jetty-12.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jetty/v12_0/Jetty12HttpAttributesGetter.java b/instrumentation/jetty/jetty-12.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jetty/v12_0/Jetty12HttpAttributesGetter.java new file mode 100644 index 000000000000..456fae9e75b3 --- /dev/null +++ b/instrumentation/jetty/jetty-12.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jetty/v12_0/Jetty12HttpAttributesGetter.java @@ -0,0 +1,99 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jetty.v12_0; + +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerAttributesGetter; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.util.List; +import javax.annotation.Nullable; +import org.eclipse.jetty.http.HttpURI; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Response; + +class Jetty12HttpAttributesGetter implements HttpServerAttributesGetter { + + @Override + public String getHttpRequestMethod(Request request) { + return request.getMethod(); + } + + @Override + public List getHttpRequestHeader(Request request, String name) { + return request.getHeaders().getValuesList(name); + } + + @Override + public Integer getHttpResponseStatusCode( + Request request, Response response, @Nullable Throwable error) { + if (!response.isCommitted() && error != null) { + return 500; + } + return response.getStatus(); + } + + @Override + public List getHttpResponseHeader(Request request, Response response, String name) { + return response.getHeaders().getValuesList(name); + } + + @Override + @Nullable + public String getUrlScheme(Request request) { + HttpURI uri = request.getHttpURI(); + return uri == null ? null : uri.getScheme(); + } + + @Nullable + @Override + public String getUrlPath(Request request) { + HttpURI uri = request.getHttpURI(); + return uri == null ? null : uri.getPath(); + } + + @Nullable + @Override + public String getUrlQuery(Request request) { + HttpURI uri = request.getHttpURI(); + return uri == null ? null : uri.getQuery(); + } + + @Nullable + @Override + public String getNetworkProtocolName(Request request, @Nullable Response unused) { + String protocol = request.getConnectionMetaData().getProtocol(); + if (protocol != null && protocol.startsWith("HTTP/")) { + return "http"; + } + return null; + } + + @Nullable + @Override + public String getNetworkProtocolVersion(Request request, @Nullable Response unused) { + String protocol = request.getConnectionMetaData().getProtocol(); + if (protocol.startsWith("HTTP/")) { + return protocol.substring("HTTP/".length()); + } + return null; + } + + @Override + @Nullable + public InetSocketAddress getNetworkPeerInetSocketAddress( + Request request, @Nullable Response unused) { + SocketAddress address = request.getConnectionMetaData().getRemoteSocketAddress(); + return address instanceof InetSocketAddress ? (InetSocketAddress) address : null; + } + + @Nullable + @Override + public InetSocketAddress getNetworkLocalInetSocketAddress( + Request request, @Nullable Response unused) { + SocketAddress address = request.getConnectionMetaData().getLocalSocketAddress(); + return address instanceof InetSocketAddress ? (InetSocketAddress) address : null; + } +} diff --git a/instrumentation/jetty/jetty-12.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jetty/v12_0/Jetty12IgnoredTypesConfigurer.java b/instrumentation/jetty/jetty-12.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jetty/v12_0/Jetty12IgnoredTypesConfigurer.java new file mode 100644 index 000000000000..24b5d7d45bf2 --- /dev/null +++ b/instrumentation/jetty/jetty-12.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jetty/v12_0/Jetty12IgnoredTypesConfigurer.java @@ -0,0 +1,22 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jetty.v12_0; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.ignore.IgnoredTypesBuilder; +import io.opentelemetry.javaagent.extension.ignore.IgnoredTypesConfigurer; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; + +@AutoService(IgnoredTypesConfigurer.class) +public class Jetty12IgnoredTypesConfigurer implements IgnoredTypesConfigurer { + + @Override + public void configure(IgnoredTypesBuilder builder, ConfigProperties config) { + // handling pipelined request sends HttpConnection instance (implements Runnable) to executor + // while scope from the previous request is still active + builder.ignoreTaskClass("org.eclipse.jetty.server.internal.HttpConnection"); + } +} diff --git a/instrumentation/jetty/jetty-12.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jetty/v12_0/Jetty12InstrumentationModule.java b/instrumentation/jetty/jetty-12.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jetty/v12_0/Jetty12InstrumentationModule.java new file mode 100644 index 000000000000..7986a86bce78 --- /dev/null +++ b/instrumentation/jetty/jetty-12.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jetty/v12_0/Jetty12InstrumentationModule.java @@ -0,0 +1,33 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jetty.v12_0; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.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 Jetty12InstrumentationModule extends InstrumentationModule { + + public Jetty12InstrumentationModule() { + super("jetty", "jetty-12.0"); + } + + @Override + public ElementMatcher.Junction classLoaderMatcher() { + return hasClassesNamed("org.eclipse.jetty.server.Request$Handler"); + } + + @Override + public List typeInstrumentations() { + return singletonList(new Jetty12ServerInstrumentation()); + } +} diff --git a/instrumentation/jetty/jetty-12.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jetty/v12_0/Jetty12ResponseMutator.java b/instrumentation/jetty/jetty-12.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jetty/v12_0/Jetty12ResponseMutator.java new file mode 100644 index 000000000000..9a99adc1efa4 --- /dev/null +++ b/instrumentation/jetty/jetty-12.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jetty/v12_0/Jetty12ResponseMutator.java @@ -0,0 +1,18 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jetty.v12_0; + +import io.opentelemetry.javaagent.bootstrap.http.HttpServerResponseMutator; +import org.eclipse.jetty.server.Response; + +public enum Jetty12ResponseMutator implements HttpServerResponseMutator { + INSTANCE; + + @Override + public void appendHeader(Response response, String name, String value) { + response.getHeaders().add(name, value); + } +} diff --git a/instrumentation/jetty/jetty-12.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jetty/v12_0/Jetty12ServerInstrumentation.java b/instrumentation/jetty/jetty-12.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jetty/v12_0/Jetty12ServerInstrumentation.java new file mode 100644 index 000000000000..b0efc09e4e76 --- /dev/null +++ b/instrumentation/jetty/jetty-12.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jetty/v12_0/Jetty12ServerInstrumentation.java @@ -0,0 +1,82 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jetty.v12_0; + +import static io.opentelemetry.javaagent.instrumentation.jetty.v12_0.Jetty12Singletons.helper; +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.bootstrap.Java8BytecodeBridge; +import io.opentelemetry.javaagent.bootstrap.http.HttpServerResponseCustomizerHolder; +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.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Response; + +class Jetty12ServerInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("org.eclipse.jetty.server.Server"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + named("handle") + .and(takesArgument(0, named("org.eclipse.jetty.server.Request"))) + .and(takesArgument(1, named("org.eclipse.jetty.server.Response"))) + .and(takesArgument(2, named("org.eclipse.jetty.util.Callback"))) + .and(isPublic()), + this.getClass().getName() + "$HandlerAdvice"); + } + + @SuppressWarnings("unused") + public static class HandlerAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter( + @Advice.This Object source, + @Advice.Argument(0) Request request, + @Advice.Argument(1) Response response, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + + Context parentContext = Java8BytecodeBridge.currentContext(); + if (!helper().shouldStart(parentContext, request)) { + return; + } + + context = helper().start(parentContext, request, response); + scope = context.makeCurrent(); + + HttpServerResponseCustomizerHolder.getCustomizer() + .customize(context, response, Jetty12ResponseMutator.INSTANCE); + } + + @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) { + if (scope == null) { + return; + } + scope.close(); + if (throwable != null) { + helper().end(context, request, response, throwable); + } + } + } +} diff --git a/instrumentation/jetty/jetty-12.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jetty/v12_0/Jetty12Singletons.java b/instrumentation/jetty/jetty-12.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jetty/v12_0/Jetty12Singletons.java new file mode 100644 index 000000000000..b9866c2cef93 --- /dev/null +++ b/instrumentation/jetty/jetty-12.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jetty/v12_0/Jetty12Singletons.java @@ -0,0 +1,71 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jetty.v12_0; + +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.instrumentation.api.incubator.semconv.http.HttpExperimentalAttributesExtractor; +import io.opentelemetry.instrumentation.api.incubator.semconv.http.HttpServerExperimentalMetrics; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import io.opentelemetry.instrumentation.api.instrumenter.InstrumenterBuilder; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerAttributesExtractor; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerMetrics; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRoute; +import io.opentelemetry.instrumentation.api.semconv.http.HttpSpanNameExtractor; +import io.opentelemetry.instrumentation.api.semconv.http.HttpSpanStatusExtractor; +import io.opentelemetry.javaagent.bootstrap.internal.AgentCommonConfig; +import io.opentelemetry.javaagent.bootstrap.servlet.AppServerBridge; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Response; + +public final class Jetty12Singletons { + private static final String INSTRUMENTATION_NAME = "io.opentelemetry.jetty-12.0"; + + private static final Instrumenter INSTRUMENTER; + + static { + Jetty12HttpAttributesGetter httpAttributesGetter = new Jetty12HttpAttributesGetter(); + + InstrumenterBuilder builder = + Instrumenter.builder( + GlobalOpenTelemetry.get(), + INSTRUMENTATION_NAME, + HttpSpanNameExtractor.builder(httpAttributesGetter) + .setKnownMethods(AgentCommonConfig.get().getKnownHttpRequestMethods()) + .build()) + .setSpanStatusExtractor(HttpSpanStatusExtractor.create(httpAttributesGetter)) + .addAttributesExtractor( + HttpServerAttributesExtractor.builder(httpAttributesGetter) + .setCapturedRequestHeaders(AgentCommonConfig.get().getServerRequestHeaders()) + .setCapturedResponseHeaders(AgentCommonConfig.get().getServerResponseHeaders()) + .setKnownMethods(AgentCommonConfig.get().getKnownHttpRequestMethods()) + .build()) + .addContextCustomizer( + HttpServerRoute.builder(httpAttributesGetter) + .setKnownMethods(AgentCommonConfig.get().getKnownHttpRequestMethods()) + .build()) + .addContextCustomizer( + (context, request, attributes) -> + new AppServerBridge.Builder() + .captureServletAttributes() + .recordException() + .init(context)) + .addOperationMetrics(HttpServerMetrics.get()); + if (AgentCommonConfig.get().shouldEmitExperimentalHttpServerTelemetry()) { + builder + .addAttributesExtractor(HttpExperimentalAttributesExtractor.create(httpAttributesGetter)) + .addOperationMetrics(HttpServerExperimentalMetrics.get()); + } + INSTRUMENTER = builder.buildServerInstrumenter(Jetty12TextMapGetter.INSTANCE); + } + + private static final Jetty12Helper HELPER = new Jetty12Helper(INSTRUMENTER); + + public static Jetty12Helper helper() { + return HELPER; + } + + private Jetty12Singletons() {} +} diff --git a/instrumentation/jetty/jetty-12.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jetty/v12_0/Jetty12TextMapGetter.java b/instrumentation/jetty/jetty-12.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jetty/v12_0/Jetty12TextMapGetter.java new file mode 100644 index 000000000000..59e9248eb9b5 --- /dev/null +++ b/instrumentation/jetty/jetty-12.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jetty/v12_0/Jetty12TextMapGetter.java @@ -0,0 +1,23 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jetty.v12_0; + +import io.opentelemetry.context.propagation.TextMapGetter; +import org.eclipse.jetty.server.Request; + +enum Jetty12TextMapGetter implements TextMapGetter { + INSTANCE; + + @Override + public Iterable keys(Request carrier) { + return carrier.getHeaders().getFieldNamesCollection(); + } + + @Override + public String get(Request carrier, String key) { + return carrier.getHeaders().get(key); + } +} diff --git a/instrumentation/jetty/jetty-12.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/jetty/v12_0/Jetty12HandlerTest.java b/instrumentation/jetty/jetty-12.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/jetty/v12_0/Jetty12HandlerTest.java new file mode 100644 index 000000000000..46b178e06561 --- /dev/null +++ b/instrumentation/jetty/jetty-12.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/jetty/v12_0/Jetty12HandlerTest.java @@ -0,0 +1,137 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jetty.v12_0; + +import static io.opentelemetry.instrumentation.testing.junit.http.HttpServerTestOptions.DEFAULT_HTTP_ATTRIBUTES; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.CAPTURE_HEADERS; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.ERROR; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.EXCEPTION; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.INDEXED_CHILD; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.NOT_FOUND; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.QUERY_PARAM; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.REDIRECT; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.SUCCESS; +import static org.assertj.core.api.Assertions.assertThat; + +import com.google.common.collect.Sets; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpServerTest; +import io.opentelemetry.instrumentation.testing.junit.http.HttpServerInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.http.HttpServerTestOptions; +import io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint; +import io.opentelemetry.sdk.testing.assertj.SpanDataAssert; +import io.opentelemetry.semconv.HttpAttributes; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import org.eclipse.jetty.server.Handler; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Response; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.util.Callback; +import org.junit.jupiter.api.extension.RegisterExtension; + +class Jetty12HandlerTest extends AbstractHttpServerTest { + + @RegisterExtension + static final InstrumentationExtension testing = HttpServerInstrumentationExtension.forAgent(); + + private final TestHandler testHandler = new TestHandler(); + + @Override + protected Server setupServer() throws Exception { + Server server = new Server(port); + server.setHandler(testHandler); + server.start(); + return server; + } + + @Override + protected void stopServer(Server server) throws Exception { + server.stop(); + } + + @Override + protected void configure(HttpServerTestOptions options) { + options.setHttpAttributes( + unused -> + Sets.difference( + DEFAULT_HTTP_ATTRIBUTES, Collections.singleton(HttpAttributes.HTTP_ROUTE))); + options.setExpectedException(new IllegalStateException(EXCEPTION.getBody())); + options.setHasResponseCustomizer(endpoint -> endpoint != EXCEPTION); + } + + @Override + protected SpanDataAssert assertResponseSpan( + SpanDataAssert span, String method, ServerEndpoint endpoint) { + if (endpoint == REDIRECT) { + span.satisfies(spanData -> assertThat(spanData.getName()).endsWith(".sendRedirect")); + } else if (endpoint == ERROR) { + span.satisfies(spanData -> assertThat(spanData.getName()).endsWith(".sendError")); + } + span.hasKind(SpanKind.INTERNAL).hasAttributesSatisfying(Attributes::isEmpty); + return span; + } + + private void handleRequest(Request request, Response response) { + ServerEndpoint endpoint = ServerEndpoint.forPath(request.getHttpURI().getPath()); + controller( + endpoint, + () -> { + try { + response(request, response, endpoint); + } catch (IOException e) { + throw new RuntimeException(e); + } + return null; + }); + } + + private void response(Request request, Response response, ServerEndpoint endpoint) + throws IOException { + if (SUCCESS.equals(endpoint)) { + response.setStatus(endpoint.getStatus()); + response.write(true, StandardCharsets.UTF_8.encode(endpoint.getBody()), Callback.NOOP); + } else if (QUERY_PARAM.equals(endpoint)) { + response.setStatus(endpoint.getStatus()); + response.write( + true, StandardCharsets.UTF_8.encode(request.getHttpURI().getQuery()), Callback.NOOP); + } else if (REDIRECT.equals(endpoint)) { + response.setStatus(endpoint.getStatus()); + response.getHeaders().add("Location", "http://localhost:" + port + endpoint.getBody()); + } else if (ERROR.equals(endpoint)) { + response.setStatus(endpoint.getStatus()); + response.write(true, StandardCharsets.UTF_8.encode(endpoint.getBody()), Callback.NOOP); + } else if (CAPTURE_HEADERS.equals(endpoint)) { + response.getHeaders().add("X-Test-Response", request.getHeaders().get("X-Test-Request")); + response.setStatus(endpoint.getStatus()); + response.write(true, StandardCharsets.UTF_8.encode(endpoint.getBody()), Callback.NOOP); + } else if (EXCEPTION.equals(endpoint)) { + throw new IllegalStateException(endpoint.getBody()); + } else if (INDEXED_CHILD.equals(endpoint)) { + INDEXED_CHILD.collectSpanAttributes( + name -> Request.extractQueryParameters(request).getValue(name)); + response.setStatus(endpoint.getStatus()); + response.write(true, StandardCharsets.UTF_8.encode(endpoint.getBody()), Callback.NOOP); + } else { + response.setStatus(NOT_FOUND.getStatus()); + response.write(true, StandardCharsets.UTF_8.encode(NOT_FOUND.getBody()), Callback.NOOP); + } + } + + private class TestHandler extends Handler.Abstract { + + @Override + public boolean handle(Request baseRequest, Response response, Callback callback) { + handleRequest(baseRequest, response); + + callback.succeeded(); + return true; + } + } +} diff --git a/instrumentation/jetty/jetty-8.0/javaagent/build.gradle.kts b/instrumentation/jetty/jetty-8.0/javaagent/build.gradle.kts index dbdb36ea17c0..49d4649d3603 100644 --- a/instrumentation/jetty/jetty-8.0/javaagent/build.gradle.kts +++ b/instrumentation/jetty/jetty-8.0/javaagent/build.gradle.kts @@ -23,6 +23,7 @@ dependencies { testInstrumentation(project(":instrumentation:servlet:servlet-javax-common:javaagent")) testInstrumentation(project(":instrumentation:jetty:jetty-11.0:javaagent")) + testInstrumentation(project(":instrumentation:jetty:jetty-12.0:javaagent")) testLibrary("org.eclipse.jetty:jetty-servlet:8.0.0.v20110901") diff --git a/instrumentation/jetty/jetty-8.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jetty/v8_0/Jetty8Singletons.java b/instrumentation/jetty/jetty-8.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jetty/v8_0/Jetty8Singletons.java index ed503f12ab4e..fd26bb40a698 100644 --- a/instrumentation/jetty/jetty-8.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jetty/v8_0/Jetty8Singletons.java +++ b/instrumentation/jetty/jetty-8.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jetty/v8_0/Jetty8Singletons.java @@ -24,6 +24,7 @@ public final class Jetty8Singletons { ServletInstrumenterBuilder.create() .addContextCustomizer( (context, request, attributes) -> new AppServerBridge.Builder().init(context)) + .propagateOperationListenersToOnEnd() .build(INSTRUMENTATION_NAME, Servlet3Accessor.INSTANCE); private static final JettyHelper HELPER = diff --git a/instrumentation/jetty/jetty-8.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jetty/v8_0/JettyQueuedThreadPoolInstrumentation.java b/instrumentation/jetty/jetty-8.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jetty/v8_0/JettyQueuedThreadPoolInstrumentation.java index 1814cb58f393..5edcf93ee6c3 100644 --- a/instrumentation/jetty/jetty-8.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jetty/v8_0/JettyQueuedThreadPoolInstrumentation.java +++ b/instrumentation/jetty/jetty-8.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jetty/v8_0/JettyQueuedThreadPoolInstrumentation.java @@ -38,8 +38,7 @@ public void transform(TypeTransformer transformer) { public static class DispatchAdvice { @Advice.OnMethodEnter(suppress = Throwable.class) - public static PropagatedContext enterJobSubmit( - @Advice.Argument(value = 0, readOnly = false) Runnable task) { + public static PropagatedContext enterJobSubmit(@Advice.Argument(0) Runnable task) { Context context = Java8BytecodeBridge.currentContext(); if (ExecutorAdviceHelper.shouldPropagateContext(context, task)) { VirtualField virtualField = diff --git a/instrumentation/jetty/jetty-8.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/jetty/v8_0/JavaLambdaMaker.java b/instrumentation/jetty/jetty-8.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/jetty/v8_0/JavaLambdaMaker.java index 2f34db1b173d..8688cf3272b9 100644 --- a/instrumentation/jetty/jetty-8.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/jetty/v8_0/JavaLambdaMaker.java +++ b/instrumentation/jetty/jetty-8.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/jetty/v8_0/JavaLambdaMaker.java @@ -7,7 +7,7 @@ public class JavaLambdaMaker { - @SuppressWarnings("FunctionalExpressionCanBeFolded") + @SuppressWarnings({"FunctionalExpressionCanBeFolded", "UnnecessaryMethodReference"}) public static Runnable lambda(Runnable runnable) { return runnable::run; } diff --git a/instrumentation/jetty/jetty-8.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/jetty/v8_0/JettyHandlerTest.java b/instrumentation/jetty/jetty-8.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/jetty/v8_0/JettyHandlerTest.java index ab44a280b344..b18f7a292065 100644 --- a/instrumentation/jetty/jetty-8.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/jetty/v8_0/JettyHandlerTest.java +++ b/instrumentation/jetty/jetty-8.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/jetty/v8_0/JettyHandlerTest.java @@ -25,7 +25,7 @@ import io.opentelemetry.instrumentation.testing.junit.http.HttpServerTestOptions; import io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint; import io.opentelemetry.sdk.testing.assertj.SpanDataAssert; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.opentelemetry.semconv.HttpAttributes; import java.io.IOException; import java.io.Writer; import java.util.Collections; @@ -40,7 +40,7 @@ import org.eclipse.jetty.server.handler.ErrorHandler; import org.junit.jupiter.api.extension.RegisterExtension; -public class JettyHandlerTest extends AbstractHttpServerTest { +class JettyHandlerTest extends AbstractHttpServerTest { @RegisterExtension static final InstrumentationExtension testing = HttpServerInstrumentationExtension.forAgent(); @@ -62,25 +62,17 @@ protected void handleErrorPage( private static final TestHandler testHandler = new TestHandler(); @Override - protected Server setupServer() { + protected Server setupServer() throws Exception { Server server = new Server(port); server.setHandler(testHandler); server.addBean(errorHandler); - try { - server.start(); - } catch (Exception e) { - throw new RuntimeException(e); - } + server.start(); return server; } @Override - protected void stopServer(Server server) { - try { - server.stop(); - } catch (Exception e) { - throw new RuntimeException(e); - } + protected void stopServer(Server server) throws Exception { + server.stop(); } @Override @@ -88,7 +80,7 @@ protected void configure(HttpServerTestOptions options) { options.setHttpAttributes( unused -> Sets.difference( - DEFAULT_HTTP_ATTRIBUTES, Collections.singleton(SemanticAttributes.HTTP_ROUTE))); + DEFAULT_HTTP_ATTRIBUTES, Collections.singleton(HttpAttributes.HTTP_ROUTE))); options.setHasResponseSpan(endpoint -> endpoint == REDIRECT || endpoint == ERROR); options.setExpectedException(new IllegalStateException(EXCEPTION.getBody())); options.setHasResponseCustomizer(endpoint -> endpoint != EXCEPTION); diff --git a/instrumentation/jms/jms-1.1/javaagent/build.gradle.kts b/instrumentation/jms/jms-1.1/javaagent/build.gradle.kts index 2624be48d377..8c769fd89f04 100644 --- a/instrumentation/jms/jms-1.1/javaagent/build.gradle.kts +++ b/instrumentation/jms/jms-1.1/javaagent/build.gradle.kts @@ -36,18 +36,40 @@ testing { implementation("org.hornetq:hornetq-jms-client:2.4.7.Final") implementation("org.hornetq:hornetq-jms-server:2.4.7.Final") } + + targets { + all { + testTask.configure { + jvmArgs("-Dotel.instrumentation.messaging.experimental.receive-telemetry.enabled=true") + } + } + } } } } -tasks.withType().configureEach { - usesService(gradle.sharedServices.registrations["testcontainersBuildService"].service) - jvmArgs("-Dotel.instrumentation.messaging.experimental.receive-telemetry.enabled=true") -} - tasks { + withType().configureEach { + usesService(gradle.sharedServices.registrations["testcontainersBuildService"].service) + } + + val testReceiveSpansDisabled by registering(Test::class) { + filter { + includeTestsMatching("Jms1SuppressReceiveSpansTest") + } + include("**/Jms1SuppressReceiveSpansTest.*") + } + + test { + filter { + excludeTestsMatching("Jms1SuppressReceiveSpansTest") + } + jvmArgs("-Dotel.instrumentation.messaging.experimental.receive-telemetry.enabled=true") + } + check { dependsOn(testing.suites) + dependsOn(testReceiveSpansDisabled) } } diff --git a/instrumentation/jms/jms-1.1/javaagent/src/jms2Test/groovy/Jms2Test.groovy b/instrumentation/jms/jms-1.1/javaagent/src/jms2Test/groovy/Jms2Test.groovy deleted file mode 100644 index 770586e3cf87..000000000000 --- a/instrumentation/jms/jms-1.1/javaagent/src/jms2Test/groovy/Jms2Test.groovy +++ /dev/null @@ -1,317 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -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.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 - -import javax.jms.Message -import javax.jms.MessageListener -import javax.jms.Session -import javax.jms.TextMessage -import java.nio.file.Files -import java.util.concurrent.CountDownLatch -import java.util.concurrent.atomic.AtomicReference - -import static io.opentelemetry.api.trace.SpanKind.CONSUMER -import static io.opentelemetry.api.trace.SpanKind.PRODUCER - -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.createTempDirectory("jmsTempDir").toFile() - 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 generates spans"() { - setup: - def producer = session.createProducer(destination) - def consumer = session.createConsumer(destination) - - runWithSpan("producer parent") { - producer.send(message) - } - - TextMessage receivedMessage = runWithSpan("consumer parent") { - return consumer.receive() as TextMessage - } - String messageId = receivedMessage.getJMSMessageID() - - expect: - receivedMessage.text == messageText - assertTraces(2) { - SpanData producerSpanData - trace(0, 2) { - span(0) { - name "producer parent" - hasNoParent() - } - producerSpan(it, 1, destinationName, span(0)) - - producerSpanData = span(1) - } - trace(1, 2) { - span(0) { - name "consumer parent" - hasNoParent() - } - consumerSpan(it, 1, destinationName, messageId, "receive", span(0), producerSpanData) - } - } - - cleanup: - producer.close() - consumer.close() - - where: - destination | destinationName - session.createQueue("someQueue") | "someQueue" - session.createTopic("someTopic") | "someTopic" - session.createTemporaryQueue() | "(temporary)" - session.createTemporaryTopic() | "(temporary)" - } - - def "sending to a MessageListener on #destinationName 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 as TextMessage) - } - } - - runWithSpan("parent") { - producer.send(message) - } - lock.countDown() - - expect: - assertTraces(1) { - trace(0, 3) { - span(0) { - name "parent" - hasNoParent() - } - producerSpan(it, 1, destinationName, span(0)) - consumerSpan(it, 2, destinationName, messageRef.get().getJMSMessageID(), "process", span(1)) - } - } - // This check needs to go after all traces have been accounted for - messageRef.get().text == messageText - - cleanup: - producer.close() - consumer.close() - - where: - destination | destinationName - session.createQueue("someQueue") | "someQueue" - session.createTopic("someTopic") | "someTopic" - session.createTemporaryQueue() | "(temporary)" - session.createTemporaryTopic() | "(temporary)" - } - - def "failing to receive message with receiveNoWait on #destinationName works"() { - setup: - def consumer = session.createConsumer(destination) - - // Receive with timeout - Message receivedMessage = consumer.receiveNoWait() - - expect: - receivedMessage == null - // span is not created if no message is received - assertTraces(0) {} - - cleanup: - consumer.close() - - where: - destination | destinationName - session.createQueue("someQueue") | "someQueue" - session.createTopic("someTopic") | "someTopic" - } - - def "failing to receive message with wait(timeout) on #destinationName works"() { - setup: - def consumer = session.createConsumer(destination) - - // Receive with timeout - Message receivedMessage = consumer.receive(100) - - expect: - receivedMessage == null - // span is not created if no message is received - assertTraces(0) {} - - cleanup: - consumer.close() - - where: - destination | destinationName - session.createQueue("someQueue") | "someQueue" - session.createTopic("someTopic") | "someTopic" - } - - def "sending a message to #destinationName 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 as TextMessage) - } - } - - when: - runWithSpan("parent") { - producer.send(destination, message) - } - lock.countDown() - - then: - assertTraces(1) { - trace(0, 3) { - span(0) { - name "parent" - hasNoParent() - } - producerSpan(it, 1, destinationName, span(0)) - consumerSpan(it, 2, destinationName, messageRef.get().getJMSMessageID(), "process", span(1)) - } - } - // This check needs to go after all traces have been accounted for - messageRef.get().text == messageText - - cleanup: - producer.close() - consumer.close() - - where: - destination | destinationName - session.createQueue("someQueue") | "someQueue" - session.createTopic("someTopic") | "someTopic" - session.createTemporaryQueue() | "(temporary)" - session.createTemporaryTopic() | "(temporary)" - } - - static producerSpan(TraceAssert trace, int index, String destinationName, SpanData parentSpan = null) { - trace.span(index) { - name destinationName + " send" - kind PRODUCER - if (parentSpan == null) { - hasNoParent() - } else { - childOf(parentSpan) - } - attributes { - "$SemanticAttributes.MESSAGING_SYSTEM" "jms" - "$SemanticAttributes.MESSAGING_DESTINATION_NAME" destinationName - if (destinationName == "(temporary)") { - "$SemanticAttributes.MESSAGING_DESTINATION_TEMPORARY" true - } - "$SemanticAttributes.MESSAGING_MESSAGE_ID" 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 destinationName, String messageId, String operation, SpanData parentSpan, SpanData linkedSpan = null) { - trace.span(index) { - name destinationName + " " + operation - kind CONSUMER - if (parentSpan == null) { - hasNoParent() - } else { - childOf(parentSpan) - } - if (linkedSpan == null) { - hasNoLinks() - } else { - hasLink(linkedSpan) - } - attributes { - "$SemanticAttributes.MESSAGING_SYSTEM" "jms" - "$SemanticAttributes.MESSAGING_DESTINATION_NAME" destinationName - "$SemanticAttributes.MESSAGING_OPERATION" 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" { it == messageId || messageId == "" } - } - if (destinationName == "(temporary)") { - "$SemanticAttributes.MESSAGING_DESTINATION_TEMPORARY" true - } - } - } - } -} diff --git a/instrumentation/jms/jms-1.1/javaagent/src/jms2Test/java/io/opentelemetry/javaagent/instrumentation/jms/v1_1/Jms2InstrumentationTest.java b/instrumentation/jms/jms-1.1/javaagent/src/jms2Test/java/io/opentelemetry/javaagent/instrumentation/jms/v1_1/Jms2InstrumentationTest.java new file mode 100644 index 000000000000..439fa62de2ec --- /dev/null +++ b/instrumentation/jms/jms-1.1/javaagent/src/jms2Test/java/io/opentelemetry/javaagent/instrumentation/jms/v1_1/Jms2InstrumentationTest.java @@ -0,0 +1,331 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jms.v1_1; + +import static io.opentelemetry.api.trace.SpanKind.CONSUMER; +import static io.opentelemetry.api.trace.SpanKind.PRODUCER; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.satisfies; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.params.provider.Arguments.arguments; + +import io.opentelemetry.instrumentation.testing.internal.AutoCleanupExtension; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.sdk.testing.assertj.AttributeAssertion; +import io.opentelemetry.sdk.trace.data.LinkData; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes; +import java.io.File; +import java.nio.file.Files; +import java.util.Collections; +import java.util.HashSet; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Stream; +import javax.jms.Connection; +import javax.jms.Destination; +import javax.jms.JMSException; +import javax.jms.Message; +import javax.jms.MessageConsumer; +import javax.jms.MessageProducer; +import javax.jms.Session; +import javax.jms.TextMessage; +import org.assertj.core.api.AbstractAssert; +import org.hornetq.api.core.TransportConfiguration; +import org.hornetq.api.core.client.ClientSession; +import org.hornetq.api.core.client.ClientSessionFactory; +import org.hornetq.api.core.client.HornetQClient; +import org.hornetq.api.core.client.ServerLocator; +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.HornetQConnectionFactory; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.RegisterExtension; +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; + +public class Jms2InstrumentationTest { + + @RegisterExtension + static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + @RegisterExtension static final AutoCleanupExtension cleanup = AutoCleanupExtension.create(); + + static HornetQServer server; + static HornetQConnectionFactory connectionFactory; + static Session session; + static Connection connection; + + @BeforeAll + static void setUp() throws Exception { + File tempDir = Files.createTempDirectory("jmsTempDir").toFile(); + tempDir.deleteOnExit(); + + Configuration config = new ConfigurationImpl(); + config.setBindingsDirectory(tempDir.getPath()); + config.setJournalDirectory(tempDir.getPath()); + config.setCreateBindingsDir(false); + config.setCreateJournalDir(false); + config.setSecurityEnabled(false); + config.setPersistenceEnabled(false); + config.setQueueConfigurations( + Collections.singletonList( + new CoreQueueConfiguration("someQueue", "someQueue", null, true))); + config.setAcceptorConfigurations( + new HashSet<>( + Collections.singletonList( + new TransportConfiguration(InVMAcceptorFactory.class.getName())))); + + server = HornetQServers.newHornetQServer(config); + server.start(); + + ServerLocator serverLocator = + HornetQClient.createServerLocatorWithoutHA( + new TransportConfiguration(InVMConnectorFactory.class.getName())); + ClientSessionFactory sf = serverLocator.createSessionFactory(); + ClientSession 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(); + + connectionFactory = + HornetQJMSClient.createConnectionFactoryWithoutHA( + JMSFactoryType.CF, new TransportConfiguration(InVMConnectorFactory.class.getName())); + connection = connectionFactory.createConnection(); + connection.start(); + session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE); + session.run(); + } + + @AfterAll + static void tearDown() throws Exception { + if (session != null) { + session.close(); + } + if (connection != null) { + connection.close(); + } + if (connectionFactory != null) { + connectionFactory.close(); + } + if (server != null) { + server.stop(); + } + } + + @ArgumentsSource(DestinationsProvider.class) + @ParameterizedTest + void testMessageConsumer( + DestinationFactory destinationFactory, String destinationName, boolean isTemporary) + throws JMSException { + + // given + Destination destination = destinationFactory.create(session); + TextMessage sentMessage = session.createTextMessage("a message"); + + MessageProducer producer = session.createProducer(destination); + cleanup.deferCleanup(producer); + MessageConsumer consumer = session.createConsumer(destination); + cleanup.deferCleanup(consumer); + + // when + testing.runWithSpan("producer parent", () -> producer.send(sentMessage)); + + TextMessage receivedMessage = + testing.runWithSpan("consumer parent", () -> (TextMessage) consumer.receive()); + + // then + assertThat(receivedMessage.getText()).isEqualTo(sentMessage.getText()); + + String messageId = receivedMessage.getJMSMessageID(); + + AtomicReference producerSpan = new AtomicReference<>(); + testing.waitAndAssertTraces( + trace -> { + trace.hasSpansSatisfyingExactly( + span -> span.hasName("producer parent").hasNoParent(), + span -> + span.hasName(destinationName + " publish") + .hasKind(PRODUCER) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo(MessagingIncubatingAttributes.MESSAGING_SYSTEM, "jms"), + equalTo( + MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME, + destinationName), + equalTo(MessagingIncubatingAttributes.MESSAGING_OPERATION, "publish"), + equalTo(MessagingIncubatingAttributes.MESSAGING_MESSAGE_ID, messageId), + messagingTempDestination(isTemporary))); + + producerSpan.set(trace.getSpan(1)); + }, + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("consumer parent").hasNoParent(), + span -> + span.hasName(destinationName + " receive") + .hasKind(CONSUMER) + .hasParent(trace.getSpan(0)) + .hasLinks(LinkData.create(producerSpan.get().getSpanContext())) + .hasAttributesSatisfyingExactly( + equalTo(MessagingIncubatingAttributes.MESSAGING_SYSTEM, "jms"), + equalTo( + MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME, + destinationName), + equalTo(MessagingIncubatingAttributes.MESSAGING_OPERATION, "receive"), + equalTo(MessagingIncubatingAttributes.MESSAGING_MESSAGE_ID, messageId), + messagingTempDestination(isTemporary)))); + } + + @ArgumentsSource(DestinationsProvider.class) + @ParameterizedTest + void testMessageListener( + DestinationFactory destinationFactory, String destinationName, boolean isTemporary) + throws Exception { + + // given + Destination destination = destinationFactory.create(session); + TextMessage sentMessage = session.createTextMessage("a message"); + + MessageProducer producer = session.createProducer(null); + cleanup.deferCleanup(producer); + MessageConsumer consumer = session.createConsumer(destination); + cleanup.deferCleanup(consumer); + + CompletableFuture receivedMessageFuture = new CompletableFuture<>(); + consumer.setMessageListener( + message -> + testing.runWithSpan( + "consumer", () -> receivedMessageFuture.complete((TextMessage) message))); + + // when + testing.runWithSpan("producer parent", () -> producer.send(destination, sentMessage)); + + // then + TextMessage receivedMessage = receivedMessageFuture.get(10, TimeUnit.SECONDS); + assertThat(receivedMessage.getText()).isEqualTo(sentMessage.getText()); + + String messageId = receivedMessage.getJMSMessageID(); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("producer parent").hasNoParent(), + span -> + span.hasName(destinationName + " publish") + .hasKind(PRODUCER) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo(MessagingIncubatingAttributes.MESSAGING_SYSTEM, "jms"), + equalTo( + MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME, + destinationName), + equalTo(MessagingIncubatingAttributes.MESSAGING_OPERATION, "publish"), + equalTo(MessagingIncubatingAttributes.MESSAGING_MESSAGE_ID, messageId), + messagingTempDestination(isTemporary)), + span -> + span.hasName(destinationName + " process") + .hasKind(CONSUMER) + .hasParent(trace.getSpan(1)) + .hasAttributesSatisfyingExactly( + equalTo(MessagingIncubatingAttributes.MESSAGING_SYSTEM, "jms"), + equalTo( + MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME, + destinationName), + equalTo(MessagingIncubatingAttributes.MESSAGING_OPERATION, "process"), + equalTo(MessagingIncubatingAttributes.MESSAGING_MESSAGE_ID, messageId), + messagingTempDestination(isTemporary)), + span -> span.hasName("consumer").hasParent(trace.getSpan(2)))); + } + + @ArgumentsSource(EmptyReceiveArgumentsProvider.class) + @ParameterizedTest + void shouldNotEmitTelemetryOnEmptyReceive( + DestinationFactory destinationFactory, MessageReceiver receiver) throws JMSException { + + // given + Destination destination = destinationFactory.create(session); + + MessageConsumer consumer = session.createConsumer(destination); + cleanup.deferCleanup(consumer); + + // when + Message message = receiver.receive(consumer); + + // then + assertThat(message).isNull(); + + testing.waitForTraces(0); + } + + private static AttributeAssertion messagingTempDestination(boolean isTemporary) { + return isTemporary + ? equalTo(MessagingIncubatingAttributes.MESSAGING_DESTINATION_TEMPORARY, true) + : satisfies( + MessagingIncubatingAttributes.MESSAGING_DESTINATION_TEMPORARY, AbstractAssert::isNull); + } + + static final class EmptyReceiveArgumentsProvider implements ArgumentsProvider { + + @Override + public Stream provideArguments(ExtensionContext context) { + DestinationFactory topic = session -> session.createTopic("someTopic"); + DestinationFactory queue = session -> session.createQueue("someQueue"); + MessageReceiver receive = consumer -> consumer.receive(100); + MessageReceiver receiveNoWait = MessageConsumer::receiveNoWait; + + return Stream.of( + arguments(topic, receive), + arguments(queue, receive), + arguments(topic, receiveNoWait), + arguments(queue, receiveNoWait)); + } + } + + static final class DestinationsProvider implements ArgumentsProvider { + + @Override + public Stream provideArguments(ExtensionContext context) { + DestinationFactory topic = session -> session.createTopic("someTopic"); + DestinationFactory queue = session -> session.createQueue("someQueue"); + DestinationFactory tempTopic = Session::createTemporaryTopic; + DestinationFactory tempQueue = Session::createTemporaryQueue; + + return Stream.of( + arguments(topic, "someTopic", false), + arguments(queue, "someQueue", false), + arguments(tempTopic, "(temporary)", true), + arguments(tempQueue, "(temporary)", true)); + } + } + + @FunctionalInterface + interface DestinationFactory { + + Destination create(Session session) throws JMSException; + } + + @FunctionalInterface + interface MessageReceiver { + + Message receive(MessageConsumer consumer) throws JMSException; + } +} diff --git a/instrumentation/jms/jms-1.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jms/v1_1/JmsMessageConsumerInstrumentation.java b/instrumentation/jms/jms-1.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jms/v1_1/JmsMessageConsumerInstrumentation.java index 6176893de5c8..70c9d136bcd1 100644 --- a/instrumentation/jms/jms-1.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jms/v1_1/JmsMessageConsumerInstrumentation.java +++ b/instrumentation/jms/jms-1.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jms/v1_1/JmsMessageConsumerInstrumentation.java @@ -7,6 +7,7 @@ import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.hasClassesNamed; import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.implementsInterface; +import static io.opentelemetry.javaagent.instrumentation.jms.JmsReceiveSpanUtil.createReceiveSpan; import static io.opentelemetry.javaagent.instrumentation.jms.v1_1.JmsSingletons.consumerReceiveInstrumenter; import static net.bytebuddy.matcher.ElementMatchers.isPublic; import static net.bytebuddy.matcher.ElementMatchers.named; @@ -14,7 +15,6 @@ import static net.bytebuddy.matcher.ElementMatchers.takesArguments; import io.opentelemetry.context.Context; -import io.opentelemetry.instrumentation.api.internal.InstrumenterUtil; import io.opentelemetry.instrumentation.api.internal.Timer; import io.opentelemetry.javaagent.bootstrap.Java8BytecodeBridge; import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; @@ -75,16 +75,7 @@ public static void stopSpan( MessageWithDestination request = MessageWithDestination.create(JavaxMessageAdapter.create(message), null); - if (consumerReceiveInstrumenter().shouldStart(parentContext, request)) { - InstrumenterUtil.startAndEnd( - consumerReceiveInstrumenter(), - parentContext, - request, - null, - throwable, - timer.startTime(), - timer.now()); - } + createReceiveSpan(consumerReceiveInstrumenter(), request, timer, throwable); } } } diff --git a/instrumentation/jms/jms-1.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jms/v1_1/JmsSingletons.java b/instrumentation/jms/jms-1.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jms/v1_1/JmsSingletons.java index 9aabd1488277..d68de50afb9c 100644 --- a/instrumentation/jms/jms-1.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jms/v1_1/JmsSingletons.java +++ b/instrumentation/jms/jms-1.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jms/v1_1/JmsSingletons.java @@ -27,7 +27,7 @@ public final class JmsSingletons { PRODUCER_INSTRUMENTER = factory.createProducerInstrumenter(); CONSUMER_RECEIVE_INSTRUMENTER = factory.createConsumerReceiveInstrumenter(); - CONSUMER_PROCESS_INSTRUMENTER = factory.createConsumerProcessInstrumenter(); + CONSUMER_PROCESS_INSTRUMENTER = factory.createConsumerProcessInstrumenter(false); } public static Instrumenter producerInstrumenter() { diff --git a/instrumentation/jms/jms-1.1/javaagent/src/test/groovy/Jms1Test.groovy b/instrumentation/jms/jms-1.1/javaagent/src/test/groovy/Jms1Test.groovy deleted file mode 100644 index 330dc33851df..000000000000 --- a/instrumentation/jms/jms-1.1/javaagent/src/test/groovy/Jms1Test.groovy +++ /dev/null @@ -1,382 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -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.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 - -import javax.jms.Connection -import javax.jms.Message -import javax.jms.MessageListener -import javax.jms.Session -import javax.jms.TextMessage -import java.util.concurrent.CountDownLatch -import java.util.concurrent.atomic.AtomicReference - -import static io.opentelemetry.api.trace.SpanKind.CONSUMER -import static io.opentelemetry.api.trace.SpanKind.PRODUCER - -@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 generates spans"() { - setup: - def producer = session.createProducer(destination) - def consumer = session.createConsumer(destination) - - runWithSpan("producer parent") { - producer.send(message) - } - - TextMessage receivedMessage = runWithSpan("consumer parent") { - return consumer.receive() as TextMessage - } - String messageId = receivedMessage.getJMSMessageID() - - expect: - receivedMessage.text == messageText - assertTraces(2) { - SpanData producerSpanData - trace(0, 2) { - span(0) { - name "producer parent" - hasNoParent() - } - producerSpan(it, 1, destinationName, span(0)) - - producerSpanData = span(1) - } - trace(1, 2) { - span(0) { - name "consumer parent" - hasNoParent() - } - consumerSpan(it, 1, destinationName, messageId, "receive", span(0), producerSpanData) - } - } - - cleanup: - producer.close() - consumer.close() - - where: - destination | destinationName - session.createQueue("someQueue") | "someQueue" - session.createTopic("someTopic") | "someTopic" - session.createTemporaryQueue() | "(temporary)" - session.createTemporaryTopic() | "(temporary)" - } - - def "sending to a MessageListener on #destinationName 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 as TextMessage) - } - } - - producer.send(message) - lock.countDown() - - expect: - assertTraces(1) { - trace(0, 2) { - producerSpan(it, 0, destinationName) - consumerSpan(it, 1, destinationName, messageRef.get().getJMSMessageID(), "process", span(0)) - } - } - // This check needs to go after all traces have been accounted for - messageRef.get().text == messageText - - cleanup: - producer.close() - consumer.close() - - where: - destination | destinationName - session.createQueue("someQueue") | "someQueue" - session.createTopic("someTopic") | "someTopic" - session.createTemporaryQueue() | "(temporary)" - session.createTemporaryTopic() | "(temporary)" - } - - def "failing to receive message with receiveNoWait on #destinationName works"() { - setup: - def consumer = session.createConsumer(destination) - - // Receive with timeout - Message receivedMessage = consumer.receiveNoWait() - - expect: - receivedMessage == null - // span is not created if no message is received - assertTraces(0) {} - - cleanup: - consumer.close() - - where: - destination | destinationName - session.createQueue("someQueue") | "someQueue" - session.createTopic("someTopic") | "someTopic" - } - - def "failing to receive message with wait(timeout) on #destinationName works"() { - setup: - def consumer = session.createConsumer(destination) - - // Receive with timeout - Message receivedMessage = consumer.receive(100) - - expect: - receivedMessage == null - // span is not created if no message is received - assertTraces(0) {} - - cleanup: - consumer.close() - - where: - destination | destinationName - session.createQueue("someQueue") | "someQueue" - session.createTopic("someTopic") | "someTopic" - } - - def "sending a read-only message to #destinationName 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() as TextMessage - - 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, destinationName) - } - trace(1, 1) { - consumerSpan(it, 0, destinationName, "", "receive", null) - } - } - - cleanup: - producer.close() - consumer.close() - - where: - destination | destinationName - session.createQueue("someQueue") | "someQueue" - session.createTopic("someTopic") | "someTopic" - session.createTemporaryQueue() | "(temporary)" - session.createTemporaryTopic() | "(temporary)" - } - - def "sending a message to #destinationName 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 as TextMessage) - } - } - - when: - runWithSpan("parent") { - producer.send(destination, message) - } - lock.countDown() - - then: - assertTraces(1) { - trace(0, 3) { - span(0) { - name "parent" - hasNoParent() - } - producerSpan(it, 1, destinationName, span(0)) - consumerSpan(it, 2, destinationName, messageRef.get().getJMSMessageID(), "process", span(1)) - } - } - // This check needs to go after all traces have been accounted for - messageRef.get().text == messageText - - cleanup: - producer.close() - consumer.close() - - where: - destination | destinationName - session.createQueue("someQueue") | "someQueue" - session.createTopic("someTopic") | "someTopic" - session.createTemporaryQueue() | "(temporary)" - session.createTemporaryTopic() | "(temporary)" - } - - def "capture message header as span attribute"() { - setup: - def destinationName = "someQueue" - def destination = session.createQueue(destinationName) - def producer = session.createProducer(destination) - def consumer = session.createConsumer(destination) - - def message = session.createTextMessage(messageText) - message.setStringProperty("test-message-header", "test") - message.setIntProperty("test-message-int-header", 1234) - runWithSpan("producer parent") { - producer.send(message) - } - - TextMessage receivedMessage = runWithSpan("consumer parent") { - return consumer.receive() as TextMessage - } - String messageId = receivedMessage.getJMSMessageID() - - expect: - receivedMessage.text == messageText - assertTraces(2) { - SpanData producerSpanData - trace(0, 2) { - span(0) { - name "producer parent" - hasNoParent() - } - producerSpan(it, 1, destinationName, span(0), true) - - producerSpanData = span(1) - } - trace(1, 2) { - span(0) { - name "consumer parent" - hasNoParent() - } - consumerSpan(it, 1, destinationName, messageId, "receive", span(0), producerSpanData, true) - } - } - - cleanup: - producer.close() - consumer.close() - } - - static producerSpan(TraceAssert trace, int index, String destinationName, SpanData parentSpan = null, boolean testHeaders = false) { - trace.span(index) { - name destinationName + " send" - kind PRODUCER - if (parentSpan == null) { - hasNoParent() - } else { - childOf(parentSpan) - } - attributes { - "$SemanticAttributes.MESSAGING_SYSTEM" "jms" - "$SemanticAttributes.MESSAGING_DESTINATION_NAME" destinationName - if (destinationName == "(temporary)") { - "$SemanticAttributes.MESSAGING_DESTINATION_TEMPORARY" true - } - "$SemanticAttributes.MESSAGING_MESSAGE_ID" String - if (testHeaders) { - "messaging.header.test_message_header" { it == ["test"] } - "messaging.header.test_message_int_header" { it == ["1234"] } - } - } - } - } - - // 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 destinationName, String messageId, String operation, SpanData parentSpan, SpanData linkedSpan = null, boolean testHeaders = false) { - trace.span(index) { - name destinationName + " " + operation - kind CONSUMER - if (parentSpan == null) { - hasNoParent() - } else { - childOf(parentSpan) - } - if (linkedSpan == null) { - hasNoLinks() - } else { - hasLink(linkedSpan) - } - attributes { - "$SemanticAttributes.MESSAGING_SYSTEM" "jms" - "$SemanticAttributes.MESSAGING_DESTINATION_NAME" destinationName - "$SemanticAttributes.MESSAGING_OPERATION" 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" { it == messageId || messageId == "" } - } - if (destinationName == "(temporary)") { - "$SemanticAttributes.MESSAGING_DESTINATION_TEMPORARY" true - } - if (testHeaders) { - "messaging.header.test_message_header" { it == ["test"] } - "messaging.header.test_message_int_header" { it == ["1234"] } - } - } - } - } -} diff --git a/instrumentation/jms/jms-1.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/jms/v1_1/AbstractJms1Test.java b/instrumentation/jms/jms-1.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/jms/v1_1/AbstractJms1Test.java new file mode 100644 index 000000000000..76aba2ac0414 --- /dev/null +++ b/instrumentation/jms/jms-1.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/jms/v1_1/AbstractJms1Test.java @@ -0,0 +1,363 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jms.v1_1; + +import static io.opentelemetry.api.common.AttributeKey.stringArrayKey; +import static io.opentelemetry.api.trace.SpanKind.CONSUMER; +import static io.opentelemetry.api.trace.SpanKind.PRODUCER; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.satisfies; +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.params.provider.Arguments.arguments; + +import io.opentelemetry.instrumentation.testing.internal.AutoCleanupExtension; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.sdk.testing.assertj.AttributeAssertion; +import io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import java.util.stream.Stream; +import javax.jms.Connection; +import javax.jms.Destination; +import javax.jms.JMSException; +import javax.jms.Message; +import javax.jms.MessageConsumer; +import javax.jms.MessageProducer; +import javax.jms.Session; +import javax.jms.TextMessage; +import org.apache.activemq.ActiveMQConnectionFactory; +import org.apache.activemq.command.ActiveMQTextMessage; +import org.assertj.core.api.AbstractAssert; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.RegisterExtension; +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.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.output.Slf4jLogConsumer; + +abstract class AbstractJms1Test { + static final Logger logger = LoggerFactory.getLogger(AbstractJms1Test.class); + + @RegisterExtension + static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + @RegisterExtension static final AutoCleanupExtension cleanup = AutoCleanupExtension.create(); + + static GenericContainer broker; + static ActiveMQConnectionFactory connectionFactory; + static Connection connection; + static Session session; + + @BeforeAll + static void setUp() throws JMSException { + broker = + new GenericContainer<>("rmohr/activemq:latest") + .withExposedPorts(61616, 8161) + .withLogConsumer(new Slf4jLogConsumer(logger)); + broker.start(); + + connectionFactory = + new ActiveMQConnectionFactory( + "tcp://" + broker.getHost() + ":" + broker.getMappedPort(61616)); + Connection connection = connectionFactory.createConnection(); + connection.start(); + session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE); + } + + @AfterAll + static void tearDown() throws JMSException { + if (session != null) { + session.close(); + } + if (connection != null) { + connection.close(); + } + if (broker != null) { + broker.close(); + } + } + + @ArgumentsSource(DestinationsProvider.class) + @ParameterizedTest + void testMessageListener( + DestinationFactory destinationFactory, String destinationName, boolean isTemporary) + throws Exception { + + // given + Destination destination = destinationFactory.create(session); + TextMessage sentMessage = session.createTextMessage("a message"); + + MessageProducer producer = session.createProducer(null); + cleanup.deferCleanup(producer::close); + MessageConsumer consumer = session.createConsumer(destination); + cleanup.deferCleanup(consumer::close); + + CompletableFuture receivedMessageFuture = new CompletableFuture<>(); + consumer.setMessageListener( + message -> + testing.runWithSpan( + "consumer", () -> receivedMessageFuture.complete((TextMessage) message))); + + // when + testing.runWithSpan("producer parent", () -> producer.send(destination, sentMessage)); + + // then + TextMessage receivedMessage = receivedMessageFuture.get(10, TimeUnit.SECONDS); + assertThat(receivedMessage.getText()).isEqualTo(sentMessage.getText()); + + String messageId = receivedMessage.getJMSMessageID(); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("producer parent").hasNoParent(), + span -> + span.hasName(destinationName + " publish") + .hasKind(PRODUCER) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo(MessagingIncubatingAttributes.MESSAGING_SYSTEM, "jms"), + equalTo( + MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME, + destinationName), + equalTo(MessagingIncubatingAttributes.MESSAGING_OPERATION, "publish"), + equalTo(MessagingIncubatingAttributes.MESSAGING_MESSAGE_ID, messageId), + messagingTempDestination(isTemporary)), + span -> + span.hasName(destinationName + " process") + .hasKind(CONSUMER) + .hasParent(trace.getSpan(1)) + .hasAttributesSatisfyingExactly( + equalTo(MessagingIncubatingAttributes.MESSAGING_SYSTEM, "jms"), + equalTo( + MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME, + destinationName), + equalTo(MessagingIncubatingAttributes.MESSAGING_OPERATION, "process"), + equalTo(MessagingIncubatingAttributes.MESSAGING_MESSAGE_ID, messageId), + messagingTempDestination(isTemporary)), + span -> span.hasName("consumer").hasParent(trace.getSpan(2)))); + } + + @ArgumentsSource(EmptyReceiveArgumentsProvider.class) + @ParameterizedTest + void shouldNotEmitTelemetryOnEmptyReceive( + DestinationFactory destinationFactory, MessageReceiver receiver) throws JMSException { + + // given + Destination destination = destinationFactory.create(session); + + MessageConsumer consumer = session.createConsumer(destination); + cleanup.deferCleanup(consumer::close); + + // when + Message message = receiver.receive(consumer); + + // then + assertThat(message).isNull(); + + testing.waitForTraces(0); + } + + @ArgumentsSource(DestinationsProvider.class) + @ParameterizedTest + void shouldCaptureMessageHeaders( + DestinationFactory destinationFactory, String destinationName, boolean isTemporary) + throws Exception { + + // given + Destination destination = destinationFactory.create(session); + TextMessage sentMessage = session.createTextMessage("a message"); + sentMessage.setStringProperty("test_message_header", "test"); + sentMessage.setIntProperty("test_message_int_header", 1234); + + MessageProducer producer = session.createProducer(destination); + cleanup.deferCleanup(producer::close); + MessageConsumer consumer = session.createConsumer(destination); + cleanup.deferCleanup(consumer::close); + + CompletableFuture receivedMessageFuture = new CompletableFuture<>(); + consumer.setMessageListener( + message -> + testing.runWithSpan( + "consumer", () -> receivedMessageFuture.complete((TextMessage) message))); + + // when + testing.runWithSpan("producer parent", () -> producer.send(sentMessage)); + + // then + TextMessage receivedMessage = receivedMessageFuture.get(10, TimeUnit.SECONDS); + assertThat(receivedMessage.getText()).isEqualTo(sentMessage.getText()); + + String messageId = receivedMessage.getJMSMessageID(); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("producer parent").hasNoParent(), + span -> + span.hasName(destinationName + " publish") + .hasKind(PRODUCER) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo(MessagingIncubatingAttributes.MESSAGING_SYSTEM, "jms"), + equalTo( + MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME, + destinationName), + equalTo(MessagingIncubatingAttributes.MESSAGING_OPERATION, "publish"), + equalTo(MessagingIncubatingAttributes.MESSAGING_MESSAGE_ID, messageId), + messagingTempDestination(isTemporary), + equalTo( + stringArrayKey("messaging.header.test_message_header"), + singletonList("test")), + equalTo( + stringArrayKey("messaging.header.test_message_int_header"), + singletonList("1234"))), + span -> + span.hasName(destinationName + " process") + .hasKind(CONSUMER) + .hasParent(trace.getSpan(1)) + .hasAttributesSatisfyingExactly( + equalTo(MessagingIncubatingAttributes.MESSAGING_SYSTEM, "jms"), + equalTo( + MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME, + destinationName), + equalTo(MessagingIncubatingAttributes.MESSAGING_OPERATION, "process"), + equalTo(MessagingIncubatingAttributes.MESSAGING_MESSAGE_ID, messageId), + messagingTempDestination(isTemporary), + equalTo( + stringArrayKey("messaging.header.test_message_header"), + singletonList("test")), + equalTo( + stringArrayKey("messaging.header.test_message_int_header"), + singletonList("1234"))), + span -> span.hasName("consumer").hasParent(trace.getSpan(2)))); + } + + @ArgumentsSource(DestinationsProvider.class) + @ParameterizedTest + void shouldFailWhenSendingReadOnlyMessage( + DestinationFactory destinationFactory, String destinationName, boolean isTemporary) + throws Exception { + + // given + Destination destination = destinationFactory.create(session); + ActiveMQTextMessage sentMessage = (ActiveMQTextMessage) session.createTextMessage("a message"); + + MessageProducer producer = session.createProducer(destination); + cleanup.deferCleanup(producer::close); + MessageConsumer consumer = session.createConsumer(destination); + cleanup.deferCleanup(consumer::close); + + sentMessage.setReadOnlyProperties(true); + + // when + testing.runWithSpan("producer parent", () -> producer.send(sentMessage)); + + TextMessage receivedMessage = (TextMessage) consumer.receive(); + + // then + assertThat(receivedMessage.getText()).isEqualTo(sentMessage.getText()); + + String messageId = receivedMessage.getJMSMessageID(); + + // This will result in a logged failure because we tried to + // write properties in MessagePropertyTextMap when readOnlyProperties = true. + // As a result, the consumer span will not be linked to the producer span as we are unable to + // propagate the trace context as a message property. + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("producer parent").hasNoParent(), + span -> + span.hasName(destinationName + " publish") + .hasKind(PRODUCER) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo(MessagingIncubatingAttributes.MESSAGING_SYSTEM, "jms"), + equalTo( + MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME, + destinationName), + equalTo(MessagingIncubatingAttributes.MESSAGING_OPERATION, "publish"), + equalTo(MessagingIncubatingAttributes.MESSAGING_MESSAGE_ID, messageId), + messagingTempDestination(isTemporary))), + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName(destinationName + " receive") + .hasKind(CONSUMER) + .hasNoParent() + .hasTotalRecordedLinks(0) + .hasAttributesSatisfyingExactly( + equalTo(MessagingIncubatingAttributes.MESSAGING_SYSTEM, "jms"), + equalTo( + MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME, + destinationName), + equalTo(MessagingIncubatingAttributes.MESSAGING_OPERATION, "receive"), + equalTo(MessagingIncubatingAttributes.MESSAGING_MESSAGE_ID, messageId), + messagingTempDestination(isTemporary)))); + } + + static AttributeAssertion messagingTempDestination(boolean isTemporary) { + return isTemporary + ? equalTo(MessagingIncubatingAttributes.MESSAGING_DESTINATION_TEMPORARY, true) + : satisfies( + MessagingIncubatingAttributes.MESSAGING_DESTINATION_TEMPORARY, AbstractAssert::isNull); + } + + static final class EmptyReceiveArgumentsProvider implements ArgumentsProvider { + + @Override + public Stream provideArguments(ExtensionContext context) { + DestinationFactory topic = session -> session.createTopic("someTopic"); + DestinationFactory queue = session -> session.createQueue("someQueue"); + MessageReceiver receive = consumer -> consumer.receive(100); + MessageReceiver receiveNoWait = MessageConsumer::receiveNoWait; + + return Stream.of( + arguments(topic, receive), + arguments(queue, receive), + arguments(topic, receiveNoWait), + arguments(queue, receiveNoWait)); + } + } + + static final class DestinationsProvider implements ArgumentsProvider { + + @Override + public Stream provideArguments(ExtensionContext context) { + DestinationFactory topic = session -> session.createTopic("someTopic"); + DestinationFactory queue = session -> session.createQueue("someQueue"); + DestinationFactory tempTopic = Session::createTemporaryTopic; + DestinationFactory tempQueue = Session::createTemporaryQueue; + + return Stream.of( + arguments(topic, "someTopic", false), + arguments(queue, "someQueue", false), + arguments(tempTopic, "(temporary)", true), + arguments(tempQueue, "(temporary)", true)); + } + } + + @FunctionalInterface + interface DestinationFactory { + + Destination create(Session session) throws JMSException; + } + + @FunctionalInterface + interface MessageReceiver { + + Message receive(MessageConsumer consumer) throws JMSException; + } +} diff --git a/instrumentation/jms/jms-1.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/jms/v1_1/Jms1InstrumentationTest.java b/instrumentation/jms/jms-1.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/jms/v1_1/Jms1InstrumentationTest.java new file mode 100644 index 000000000000..dc21edad5f6d --- /dev/null +++ b/instrumentation/jms/jms-1.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/jms/v1_1/Jms1InstrumentationTest.java @@ -0,0 +1,90 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jms.v1_1; + +import static io.opentelemetry.api.trace.SpanKind.CONSUMER; +import static io.opentelemetry.api.trace.SpanKind.PRODUCER; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.sdk.trace.data.LinkData; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes; +import java.util.concurrent.atomic.AtomicReference; +import javax.jms.Destination; +import javax.jms.JMSException; +import javax.jms.MessageConsumer; +import javax.jms.MessageProducer; +import javax.jms.TextMessage; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ArgumentsSource; + +class Jms1InstrumentationTest extends AbstractJms1Test { + + @ArgumentsSource(DestinationsProvider.class) + @ParameterizedTest + void testMessageConsumer( + DestinationFactory destinationFactory, String destinationName, boolean isTemporary) + throws JMSException { + + // given + Destination destination = destinationFactory.create(session); + TextMessage sentMessage = session.createTextMessage("a message"); + + MessageProducer producer = session.createProducer(destination); + cleanup.deferCleanup(producer::close); + MessageConsumer consumer = session.createConsumer(destination); + cleanup.deferCleanup(consumer::close); + + // when + testing.runWithSpan("producer parent", () -> producer.send(sentMessage)); + + TextMessage receivedMessage = + testing.runWithSpan("consumer parent", () -> (TextMessage) consumer.receive()); + + // then + assertThat(receivedMessage.getText()).isEqualTo(sentMessage.getText()); + + String messageId = receivedMessage.getJMSMessageID(); + + AtomicReference producerSpan = new AtomicReference<>(); + testing.waitAndAssertTraces( + trace -> { + trace.hasSpansSatisfyingExactly( + span -> span.hasName("producer parent").hasNoParent(), + span -> + span.hasName(destinationName + " publish") + .hasKind(PRODUCER) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo(MessagingIncubatingAttributes.MESSAGING_SYSTEM, "jms"), + equalTo( + MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME, + destinationName), + equalTo(MessagingIncubatingAttributes.MESSAGING_OPERATION, "publish"), + equalTo(MessagingIncubatingAttributes.MESSAGING_MESSAGE_ID, messageId), + messagingTempDestination(isTemporary))); + + producerSpan.set(trace.getSpan(1)); + }, + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("consumer parent").hasNoParent(), + span -> + span.hasName(destinationName + " receive") + .hasKind(CONSUMER) + .hasParent(trace.getSpan(0)) + .hasLinks(LinkData.create(producerSpan.get().getSpanContext())) + .hasAttributesSatisfyingExactly( + equalTo(MessagingIncubatingAttributes.MESSAGING_SYSTEM, "jms"), + equalTo( + MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME, + destinationName), + equalTo(MessagingIncubatingAttributes.MESSAGING_OPERATION, "receive"), + equalTo(MessagingIncubatingAttributes.MESSAGING_MESSAGE_ID, messageId), + messagingTempDestination(isTemporary)))); + } +} diff --git a/instrumentation/jms/jms-1.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/jms/v1_1/Jms1SuppressReceiveSpansTest.java b/instrumentation/jms/jms-1.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/jms/v1_1/Jms1SuppressReceiveSpansTest.java new file mode 100644 index 000000000000..261f2c3e87f9 --- /dev/null +++ b/instrumentation/jms/jms-1.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/jms/v1_1/Jms1SuppressReceiveSpansTest.java @@ -0,0 +1,82 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jms.v1_1; + +import static io.opentelemetry.api.trace.SpanKind.CONSUMER; +import static io.opentelemetry.api.trace.SpanKind.PRODUCER; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes; +import javax.jms.Destination; +import javax.jms.JMSException; +import javax.jms.MessageConsumer; +import javax.jms.MessageProducer; +import javax.jms.TextMessage; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ArgumentsSource; + +class Jms1SuppressReceiveSpansTest extends AbstractJms1Test { + + @ArgumentsSource(DestinationsProvider.class) + @ParameterizedTest + void testMessageConsumer( + DestinationFactory destinationFactory, String destinationName, boolean isTemporary) + throws JMSException { + + // given + Destination destination = destinationFactory.create(session); + TextMessage sentMessage = session.createTextMessage("a message"); + + MessageProducer producer = session.createProducer(destination); + cleanup.deferCleanup(producer::close); + MessageConsumer consumer = session.createConsumer(destination); + cleanup.deferCleanup(consumer::close); + + // when + testing.runWithSpan("producer parent", () -> producer.send(sentMessage)); + + TextMessage receivedMessage = + testing.runWithSpan("consumer parent", () -> (TextMessage) consumer.receive()); + + // then + assertThat(receivedMessage.getText()).isEqualTo(sentMessage.getText()); + + String messageId = receivedMessage.getJMSMessageID(); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("producer parent").hasNoParent(), + span -> + span.hasName(destinationName + " publish") + .hasKind(PRODUCER) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo(MessagingIncubatingAttributes.MESSAGING_SYSTEM, "jms"), + equalTo( + MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME, + destinationName), + equalTo(MessagingIncubatingAttributes.MESSAGING_OPERATION, "publish"), + equalTo(MessagingIncubatingAttributes.MESSAGING_MESSAGE_ID, messageId), + messagingTempDestination(isTemporary)), + span -> + span.hasName(destinationName + " receive") + .hasKind(CONSUMER) + .hasParent(trace.getSpan(1)) + .hasTotalRecordedLinks(0) + .hasAttributesSatisfyingExactly( + equalTo(MessagingIncubatingAttributes.MESSAGING_SYSTEM, "jms"), + equalTo( + MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME, + destinationName), + equalTo(MessagingIncubatingAttributes.MESSAGING_OPERATION, "receive"), + equalTo(MessagingIncubatingAttributes.MESSAGING_MESSAGE_ID, messageId), + messagingTempDestination(isTemporary))), + trace -> + trace.hasSpansSatisfyingExactly(span -> span.hasName("consumer parent").hasNoParent())); + } +} diff --git a/instrumentation/jms/jms-3.0/javaagent/build.gradle.kts b/instrumentation/jms/jms-3.0/javaagent/build.gradle.kts index f80e344c8fbb..d52b6ce0be57 100644 --- a/instrumentation/jms/jms-3.0/javaagent/build.gradle.kts +++ b/instrumentation/jms/jms-3.0/javaagent/build.gradle.kts @@ -36,7 +36,23 @@ otelJava { tasks { test { usesService(gradle.sharedServices.registrations["testcontainersBuildService"].service) + } + + val testReceiveSpansDisabled by registering(Test::class) { + filter { + includeTestsMatching("Jms3SuppressReceiveSpansTest") + } + include("**/Jms3SuppressReceiveSpansTest.*") + } + test { + filter { + excludeTestsMatching("Jms3SuppressReceiveSpansTest") + } jvmArgs("-Dotel.instrumentation.messaging.experimental.receive-telemetry.enabled=true") } + + check { + dependsOn(testReceiveSpansDisabled) + } } diff --git a/instrumentation/jms/jms-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jms/v3_0/JmsMessageConsumerInstrumentation.java b/instrumentation/jms/jms-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jms/v3_0/JmsMessageConsumerInstrumentation.java index 527bd20702b0..013a56cce7ce 100644 --- a/instrumentation/jms/jms-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jms/v3_0/JmsMessageConsumerInstrumentation.java +++ b/instrumentation/jms/jms-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jms/v3_0/JmsMessageConsumerInstrumentation.java @@ -7,6 +7,7 @@ import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.hasClassesNamed; import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.implementsInterface; +import static io.opentelemetry.javaagent.instrumentation.jms.JmsReceiveSpanUtil.createReceiveSpan; import static io.opentelemetry.javaagent.instrumentation.jms.v3_0.JmsSingletons.consumerReceiveInstrumenter; import static net.bytebuddy.matcher.ElementMatchers.isPublic; import static net.bytebuddy.matcher.ElementMatchers.named; @@ -14,7 +15,6 @@ import static net.bytebuddy.matcher.ElementMatchers.takesArguments; import io.opentelemetry.context.Context; -import io.opentelemetry.instrumentation.api.internal.InstrumenterUtil; import io.opentelemetry.instrumentation.api.internal.Timer; import io.opentelemetry.javaagent.bootstrap.Java8BytecodeBridge; import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; @@ -75,16 +75,7 @@ public static void stopSpan( MessageWithDestination request = MessageWithDestination.create(JakartaMessageAdapter.create(message), null); - if (consumerReceiveInstrumenter().shouldStart(parentContext, request)) { - InstrumenterUtil.startAndEnd( - consumerReceiveInstrumenter(), - parentContext, - request, - null, - throwable, - timer.startTime(), - timer.now()); - } + createReceiveSpan(consumerReceiveInstrumenter(), request, timer, throwable); } } } diff --git a/instrumentation/jms/jms-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jms/v3_0/JmsSingletons.java b/instrumentation/jms/jms-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jms/v3_0/JmsSingletons.java index 179befc13ee3..d5595e75899f 100644 --- a/instrumentation/jms/jms-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jms/v3_0/JmsSingletons.java +++ b/instrumentation/jms/jms-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jms/v3_0/JmsSingletons.java @@ -27,7 +27,7 @@ public final class JmsSingletons { PRODUCER_INSTRUMENTER = factory.createProducerInstrumenter(); CONSUMER_RECEIVE_INSTRUMENTER = factory.createConsumerReceiveInstrumenter(); - CONSUMER_PROCESS_INSTRUMENTER = factory.createConsumerProcessInstrumenter(); + CONSUMER_PROCESS_INSTRUMENTER = factory.createConsumerProcessInstrumenter(false); } public static Instrumenter producerInstrumenter() { diff --git a/instrumentation/jms/jms-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/jms/v3_0/AbstractJms3Test.java b/instrumentation/jms/jms-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/jms/v3_0/AbstractJms3Test.java new file mode 100644 index 000000000000..0e90b1164c3b --- /dev/null +++ b/instrumentation/jms/jms-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/jms/v3_0/AbstractJms3Test.java @@ -0,0 +1,315 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jms.v3_0; + +import static io.opentelemetry.api.common.AttributeKey.stringArrayKey; +import static io.opentelemetry.api.trace.SpanKind.CONSUMER; +import static io.opentelemetry.api.trace.SpanKind.PRODUCER; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.satisfies; +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.params.provider.Arguments.arguments; + +import io.opentelemetry.instrumentation.testing.internal.AutoCleanupExtension; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.sdk.testing.assertj.AttributeAssertion; +import io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes; +import jakarta.jms.Connection; +import jakarta.jms.Destination; +import jakarta.jms.JMSException; +import jakarta.jms.Message; +import jakarta.jms.MessageConsumer; +import jakarta.jms.MessageProducer; +import jakarta.jms.Session; +import jakarta.jms.TextMessage; +import java.time.Duration; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import java.util.stream.Stream; +import org.apache.activemq.artemis.jms.client.ActiveMQConnectionFactory; +import org.apache.activemq.artemis.jms.client.ActiveMQDestination; +import org.assertj.core.api.AbstractAssert; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.RegisterExtension; +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.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.output.Slf4jLogConsumer; +import org.testcontainers.containers.wait.strategy.Wait; + +abstract class AbstractJms3Test { + static final Logger logger = LoggerFactory.getLogger(AbstractJms3Test.class); + + @RegisterExtension + static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + @RegisterExtension static final AutoCleanupExtension cleanup = AutoCleanupExtension.create(); + + static GenericContainer broker; + static ActiveMQConnectionFactory connectionFactory; + static Connection connection; + static Session session; + + @BeforeAll + static void setUp() throws JMSException { + broker = + new GenericContainer<>("quay.io/artemiscloud/activemq-artemis-broker:artemis.2.27.0") + .withEnv("AMQ_USER", "test") + .withEnv("AMQ_PASSWORD", "test") + .withEnv("JAVA_TOOL_OPTIONS", "-Dbrokerconfig.maxDiskUsage=-1") + .withExposedPorts(61616, 8161) + .waitingFor(Wait.forLogMessage(".*Server is now live.*", 1)) + .withStartupTimeout(Duration.ofMinutes(2)) + .withLogConsumer(new Slf4jLogConsumer(logger)); + broker.start(); + + connectionFactory = + new ActiveMQConnectionFactory( + "tcp://" + broker.getHost() + ":" + broker.getMappedPort(61616)); + connectionFactory.setUser("test"); + connectionFactory.setPassword("test"); + + connection = connectionFactory.createConnection(); + connection.start(); + + session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE); + } + + @AfterAll + static void tearDown() throws JMSException { + if (session != null) { + session.close(); + } + if (connection != null) { + connection.close(); + } + if (connectionFactory != null) { + connectionFactory.close(); + } + if (broker != null) { + broker.close(); + } + } + + @ArgumentsSource(DestinationsProvider.class) + @ParameterizedTest + void testMessageListener(DestinationFactory destinationFactory, boolean isTemporary) + throws Exception { + + // given + Destination destination = destinationFactory.create(session); + TextMessage sentMessage = session.createTextMessage("hello there"); + + MessageProducer producer = session.createProducer(null); + cleanup.deferCleanup(producer); + MessageConsumer consumer = session.createConsumer(destination); + cleanup.deferCleanup(consumer); + + CompletableFuture receivedMessageFuture = new CompletableFuture<>(); + consumer.setMessageListener( + message -> + testing.runWithSpan( + "consumer", () -> receivedMessageFuture.complete((TextMessage) message))); + + // when + testing.runWithSpan("parent", () -> producer.send(destination, sentMessage)); + + // then + TextMessage receivedMessage = receivedMessageFuture.get(10, TimeUnit.SECONDS); + assertThat(receivedMessage.getText()).isEqualTo(sentMessage.getText()); + + String actualDestinationName = ((ActiveMQDestination) destination).getName(); + // artemis consumers don't know whether the destination is temporary or not + String producerDestinationName = isTemporary ? "(temporary)" : actualDestinationName; + String messageId = receivedMessage.getJMSMessageID(); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasNoParent(), + span -> + span.hasName(producerDestinationName + " publish") + .hasKind(PRODUCER) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo(MessagingIncubatingAttributes.MESSAGING_SYSTEM, "jms"), + equalTo( + MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME, + producerDestinationName), + equalTo(MessagingIncubatingAttributes.MESSAGING_OPERATION, "publish"), + equalTo(MessagingIncubatingAttributes.MESSAGING_MESSAGE_ID, messageId), + messagingTempDestination(isTemporary)), + span -> + span.hasName(actualDestinationName + " process") + .hasKind(CONSUMER) + .hasParent(trace.getSpan(1)) + .hasAttributesSatisfyingExactly( + equalTo(MessagingIncubatingAttributes.MESSAGING_SYSTEM, "jms"), + equalTo( + MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME, + actualDestinationName), + equalTo(MessagingIncubatingAttributes.MESSAGING_OPERATION, "process"), + equalTo(MessagingIncubatingAttributes.MESSAGING_MESSAGE_ID, messageId)), + span -> span.hasName("consumer").hasParent(trace.getSpan(2)))); + } + + @ArgumentsSource(EmptyReceiveArgumentsProvider.class) + @ParameterizedTest + void shouldNotEmitTelemetryOnEmptyReceive( + DestinationFactory destinationFactory, MessageReceiver receiver) throws JMSException { + + // given + Destination destination = destinationFactory.create(session); + + MessageConsumer consumer = session.createConsumer(destination); + cleanup.deferCleanup(consumer); + + // when + Message message = receiver.receive(consumer); + + // then + assertThat(message).isNull(); + + testing.waitForTraces(0); + } + + @ArgumentsSource(DestinationsProvider.class) + @ParameterizedTest + void shouldCaptureMessageHeaders(DestinationFactory destinationFactory, boolean isTemporary) + throws Exception { + + // given + Destination destination = destinationFactory.create(session); + TextMessage sentMessage = session.createTextMessage("hello there"); + sentMessage.setStringProperty("test_message_header", "test"); + sentMessage.setIntProperty("test_message_int_header", 1234); + + MessageProducer producer = session.createProducer(destination); + cleanup.deferCleanup(producer); + MessageConsumer consumer = session.createConsumer(destination); + cleanup.deferCleanup(consumer); + + CompletableFuture receivedMessageFuture = new CompletableFuture<>(); + consumer.setMessageListener( + message -> + testing.runWithSpan( + "consumer", () -> receivedMessageFuture.complete((TextMessage) message))); + + // when + testing.runWithSpan("parent", () -> producer.send(sentMessage)); + + // then + TextMessage receivedMessage = receivedMessageFuture.get(10, TimeUnit.SECONDS); + assertThat(receivedMessage.getText()).isEqualTo(sentMessage.getText()); + + String actualDestinationName = ((ActiveMQDestination) destination).getName(); + // artemis consumers don't know whether the destination is temporary or not + String producerDestinationName = isTemporary ? "(temporary)" : actualDestinationName; + String messageId = receivedMessage.getJMSMessageID(); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasNoParent(), + span -> + span.hasName(producerDestinationName + " publish") + .hasKind(PRODUCER) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo(MessagingIncubatingAttributes.MESSAGING_SYSTEM, "jms"), + equalTo( + MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME, + producerDestinationName), + equalTo(MessagingIncubatingAttributes.MESSAGING_OPERATION, "publish"), + equalTo(MessagingIncubatingAttributes.MESSAGING_MESSAGE_ID, messageId), + messagingTempDestination(isTemporary), + equalTo( + stringArrayKey("messaging.header.test_message_header"), + singletonList("test")), + equalTo( + stringArrayKey("messaging.header.test_message_int_header"), + singletonList("1234"))), + span -> + span.hasName(actualDestinationName + " process") + .hasKind(CONSUMER) + .hasParent(trace.getSpan(1)) + .hasAttributesSatisfyingExactly( + equalTo(MessagingIncubatingAttributes.MESSAGING_SYSTEM, "jms"), + equalTo( + MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME, + actualDestinationName), + equalTo(MessagingIncubatingAttributes.MESSAGING_OPERATION, "process"), + equalTo(MessagingIncubatingAttributes.MESSAGING_MESSAGE_ID, messageId), + equalTo( + stringArrayKey("messaging.header.test_message_header"), + singletonList("test")), + equalTo( + stringArrayKey("messaging.header.test_message_int_header"), + singletonList("1234"))), + span -> span.hasName("consumer").hasParent(trace.getSpan(2)))); + } + + static AttributeAssertion messagingTempDestination(boolean isTemporary) { + return isTemporary + ? equalTo(MessagingIncubatingAttributes.MESSAGING_DESTINATION_TEMPORARY, true) + : satisfies( + MessagingIncubatingAttributes.MESSAGING_DESTINATION_TEMPORARY, AbstractAssert::isNull); + } + + static final class EmptyReceiveArgumentsProvider implements ArgumentsProvider { + + @Override + public Stream provideArguments(ExtensionContext context) { + DestinationFactory topic = session -> session.createTopic("someTopic"); + DestinationFactory queue = session -> session.createQueue("someQueue"); + MessageReceiver receive = consumer -> consumer.receive(100); + MessageReceiver receiveNoWait = MessageConsumer::receiveNoWait; + + return Stream.of( + arguments(topic, receive), + arguments(queue, receive), + arguments(topic, receiveNoWait), + arguments(queue, receiveNoWait)); + } + } + + static final class DestinationsProvider implements ArgumentsProvider { + + @Override + public Stream provideArguments(ExtensionContext context) { + DestinationFactory topic = session -> session.createTopic("someTopic"); + DestinationFactory queue = session -> session.createQueue("someQueue"); + DestinationFactory tempTopic = Session::createTemporaryTopic; + DestinationFactory tempQueue = Session::createTemporaryQueue; + + return Stream.of( + arguments(topic, false), + arguments(queue, false), + arguments(tempTopic, true), + arguments(tempQueue, true)); + } + } + + @FunctionalInterface + interface DestinationFactory { + + Destination create(Session session) throws JMSException; + } + + @FunctionalInterface + interface MessageReceiver { + + Message receive(MessageConsumer consumer) throws JMSException; + } +} diff --git a/instrumentation/jms/jms-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/jms/v3_0/Jms3InstrumentationTest.java b/instrumentation/jms/jms-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/jms/v3_0/Jms3InstrumentationTest.java index 463524cd2223..b5f8b1f3cd58 100644 --- a/instrumentation/jms/jms-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/jms/v3_0/Jms3InstrumentationTest.java +++ b/instrumentation/jms/jms-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/jms/v3_0/Jms3InstrumentationTest.java @@ -5,105 +5,25 @@ package io.opentelemetry.javaagent.instrumentation.jms.v3_0; -import static io.opentelemetry.api.common.AttributeKey.stringArrayKey; import static io.opentelemetry.api.trace.SpanKind.CONSUMER; import static io.opentelemetry.api.trace.SpanKind.PRODUCER; import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; -import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.satisfies; -import static java.util.Collections.singletonList; import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.params.provider.Arguments.arguments; -import io.opentelemetry.instrumentation.testing.internal.AutoCleanupExtension; -import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; -import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; -import io.opentelemetry.sdk.testing.assertj.AttributeAssertion; import io.opentelemetry.sdk.trace.data.LinkData; import io.opentelemetry.sdk.trace.data.SpanData; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; -import jakarta.jms.Connection; +import io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes; import jakarta.jms.Destination; import jakarta.jms.JMSException; -import jakarta.jms.Message; import jakarta.jms.MessageConsumer; import jakarta.jms.MessageProducer; -import jakarta.jms.Session; import jakarta.jms.TextMessage; -import java.time.Duration; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; -import java.util.stream.Stream; -import org.apache.activemq.artemis.jms.client.ActiveMQConnectionFactory; import org.apache.activemq.artemis.jms.client.ActiveMQDestination; -import org.assertj.core.api.AbstractAssert; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.extension.ExtensionContext; -import org.junit.jupiter.api.extension.RegisterExtension; 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.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.testcontainers.containers.GenericContainer; -import org.testcontainers.containers.output.Slf4jLogConsumer; -import org.testcontainers.containers.wait.strategy.Wait; -class Jms3InstrumentationTest { - - static final Logger logger = LoggerFactory.getLogger(Jms3InstrumentationTest.class); - - @RegisterExtension - static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); - - @RegisterExtension static final AutoCleanupExtension cleanup = AutoCleanupExtension.create(); - - static GenericContainer broker; - static ActiveMQConnectionFactory connectionFactory; - static Connection connection; - static Session session; - - @BeforeAll - static void setUp() throws JMSException { - broker = - new GenericContainer<>("quay.io/artemiscloud/activemq-artemis-broker:artemis.2.27.0") - .withEnv("AMQ_USER", "test") - .withEnv("AMQ_PASSWORD", "test") - .withEnv("JAVA_TOOL_OPTIONS", "-Dbrokerconfig.maxDiskUsage=-1") - .withExposedPorts(61616, 8161) - .waitingFor(Wait.forLogMessage(".*Server is now live.*", 1)) - .withStartupTimeout(Duration.ofMinutes(2)) - .withLogConsumer(new Slf4jLogConsumer(logger)); - broker.start(); - - connectionFactory = - new ActiveMQConnectionFactory("tcp://localhost:" + broker.getMappedPort(61616)); - connectionFactory.setUser("test"); - connectionFactory.setPassword("test"); - - connection = connectionFactory.createConnection(); - connection.start(); - - session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE); - } - - @AfterAll - static void tearDown() throws JMSException { - if (session != null) { - session.close(); - } - if (connection != null) { - connection.close(); - } - if (connectionFactory != null) { - connectionFactory.close(); - } - if (broker != null) { - broker.close(); - } - } +class Jms3InstrumentationTest extends AbstractJms3Test { @ArgumentsSource(DestinationsProvider.class) @ParameterizedTest @@ -139,15 +59,16 @@ void testMessageConsumer(DestinationFactory destinationFactory, boolean isTempor trace.hasSpansSatisfyingExactly( span -> span.hasName("producer parent").hasNoParent(), span -> - span.hasName(producerDestinationName + " send") + span.hasName(producerDestinationName + " publish") .hasKind(PRODUCER) .hasParent(trace.getSpan(0)) .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.MESSAGING_SYSTEM, "jms"), + equalTo(MessagingIncubatingAttributes.MESSAGING_SYSTEM, "jms"), equalTo( - SemanticAttributes.MESSAGING_DESTINATION_NAME, + MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME, producerDestinationName), - equalTo(SemanticAttributes.MESSAGING_MESSAGE_ID, messageId), + equalTo(MessagingIncubatingAttributes.MESSAGING_OPERATION, "publish"), + equalTo(MessagingIncubatingAttributes.MESSAGING_MESSAGE_ID, messageId), messagingTempDestination(isTemporary))); producerSpan.set(trace.getSpan(1)); @@ -161,219 +82,12 @@ void testMessageConsumer(DestinationFactory destinationFactory, boolean isTempor .hasParent(trace.getSpan(0)) .hasLinks(LinkData.create(producerSpan.get().getSpanContext())) .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.MESSAGING_SYSTEM, "jms"), + equalTo(MessagingIncubatingAttributes.MESSAGING_SYSTEM, "jms"), equalTo( - SemanticAttributes.MESSAGING_DESTINATION_NAME, + MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME, actualDestinationName), - equalTo(SemanticAttributes.MESSAGING_OPERATION, "receive"), - equalTo(SemanticAttributes.MESSAGING_MESSAGE_ID, messageId)))); - } - - @ArgumentsSource(DestinationsProvider.class) - @ParameterizedTest - void testMessageListener(DestinationFactory destinationFactory, boolean isTemporary) - throws Exception { - - // given - Destination destination = destinationFactory.create(session); - TextMessage sentMessage = session.createTextMessage("hello there"); - - MessageProducer producer = session.createProducer(null); - cleanup.deferCleanup(producer); - MessageConsumer consumer = session.createConsumer(destination); - cleanup.deferCleanup(consumer); - - CompletableFuture receivedMessageFuture = new CompletableFuture<>(); - consumer.setMessageListener( - message -> - testing.runWithSpan( - "consumer", () -> receivedMessageFuture.complete((TextMessage) message))); - - // when - testing.runWithSpan("parent", () -> producer.send(destination, sentMessage)); - - // then - TextMessage receivedMessage = receivedMessageFuture.get(10, TimeUnit.SECONDS); - assertThat(receivedMessage.getText()).isEqualTo(sentMessage.getText()); - - String actualDestinationName = ((ActiveMQDestination) destination).getName(); - // artemis consumers don't know whether the destination is temporary or not - String producerDestinationName = isTemporary ? "(temporary)" : actualDestinationName; - String messageId = receivedMessage.getJMSMessageID(); - - testing.waitAndAssertTraces( - trace -> - trace.hasSpansSatisfyingExactly( - span -> span.hasName("parent").hasNoParent(), - span -> - span.hasName(producerDestinationName + " send") - .hasKind(PRODUCER) - .hasParent(trace.getSpan(0)) - .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.MESSAGING_SYSTEM, "jms"), - equalTo( - SemanticAttributes.MESSAGING_DESTINATION_NAME, - producerDestinationName), - equalTo(SemanticAttributes.MESSAGING_MESSAGE_ID, messageId), - messagingTempDestination(isTemporary)), - span -> - span.hasName(actualDestinationName + " process") - .hasKind(CONSUMER) - .hasParent(trace.getSpan(1)) - .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.MESSAGING_SYSTEM, "jms"), + equalTo(MessagingIncubatingAttributes.MESSAGING_OPERATION, "receive"), equalTo( - SemanticAttributes.MESSAGING_DESTINATION_NAME, - actualDestinationName), - equalTo(SemanticAttributes.MESSAGING_OPERATION, "process"), - equalTo(SemanticAttributes.MESSAGING_MESSAGE_ID, messageId)), - span -> span.hasName("consumer").hasParent(trace.getSpan(2)))); - } - - @ArgumentsSource(EmptyReceiveArgumentsProvider.class) - @ParameterizedTest - void shouldNotEmitTelemetryOnEmptyReceive( - DestinationFactory destinationFactory, MessageReceiver receiver) throws JMSException { - - // given - Destination destination = destinationFactory.create(session); - - MessageConsumer consumer = session.createConsumer(destination); - cleanup.deferCleanup(consumer); - - // when - Message message = receiver.receive(consumer); - - // then - assertThat(message).isNull(); - - testing.waitForTraces(0); - } - - @ArgumentsSource(DestinationsProvider.class) - @ParameterizedTest - void shouldCaptureMessageHeaders(DestinationFactory destinationFactory, boolean isTemporary) - throws Exception { - - // given - Destination destination = destinationFactory.create(session); - TextMessage sentMessage = session.createTextMessage("hello there"); - sentMessage.setStringProperty("test_message_header", "test"); - sentMessage.setIntProperty("test_message_int_header", 1234); - - MessageProducer producer = session.createProducer(destination); - cleanup.deferCleanup(producer); - MessageConsumer consumer = session.createConsumer(destination); - cleanup.deferCleanup(consumer); - - CompletableFuture receivedMessageFuture = new CompletableFuture<>(); - consumer.setMessageListener( - message -> - testing.runWithSpan( - "consumer", () -> receivedMessageFuture.complete((TextMessage) message))); - - // when - testing.runWithSpan("parent", () -> producer.send(sentMessage)); - - // then - TextMessage receivedMessage = receivedMessageFuture.get(10, TimeUnit.SECONDS); - assertThat(receivedMessage.getText()).isEqualTo(sentMessage.getText()); - - String actualDestinationName = ((ActiveMQDestination) destination).getName(); - // artemis consumers don't know whether the destination is temporary or not - String producerDestinationName = isTemporary ? "(temporary)" : actualDestinationName; - String messageId = receivedMessage.getJMSMessageID(); - - testing.waitAndAssertTraces( - trace -> - trace.hasSpansSatisfyingExactly( - span -> span.hasName("parent").hasNoParent(), - span -> - span.hasName(producerDestinationName + " send") - .hasKind(PRODUCER) - .hasParent(trace.getSpan(0)) - .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.MESSAGING_SYSTEM, "jms"), - equalTo( - SemanticAttributes.MESSAGING_DESTINATION_NAME, - producerDestinationName), - equalTo(SemanticAttributes.MESSAGING_MESSAGE_ID, messageId), - messagingTempDestination(isTemporary), - equalTo( - stringArrayKey("messaging.header.test_message_header"), - singletonList("test")), - equalTo( - stringArrayKey("messaging.header.test_message_int_header"), - singletonList("1234"))), - span -> - span.hasName(actualDestinationName + " process") - .hasKind(CONSUMER) - .hasParent(trace.getSpan(1)) - .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.MESSAGING_SYSTEM, "jms"), - equalTo( - SemanticAttributes.MESSAGING_DESTINATION_NAME, - actualDestinationName), - equalTo(SemanticAttributes.MESSAGING_OPERATION, "process"), - equalTo(SemanticAttributes.MESSAGING_MESSAGE_ID, messageId), - equalTo( - stringArrayKey("messaging.header.test_message_header"), - singletonList("test")), - equalTo( - stringArrayKey("messaging.header.test_message_int_header"), - singletonList("1234"))), - span -> span.hasName("consumer").hasParent(trace.getSpan(2)))); - } - - private static AttributeAssertion messagingTempDestination(boolean isTemporary) { - return isTemporary - ? equalTo(SemanticAttributes.MESSAGING_DESTINATION_TEMPORARY, true) - : satisfies(SemanticAttributes.MESSAGING_DESTINATION_TEMPORARY, AbstractAssert::isNull); - } - - static final class EmptyReceiveArgumentsProvider implements ArgumentsProvider { - - @Override - public Stream provideArguments(ExtensionContext context) { - DestinationFactory topic = session -> session.createTopic("someTopic"); - DestinationFactory queue = session -> session.createQueue("someQueue"); - MessageReceiver receive = consumer -> consumer.receive(100); - MessageReceiver receiveNoWait = MessageConsumer::receiveNoWait; - - return Stream.of( - arguments(topic, receive), - arguments(queue, receive), - arguments(topic, receiveNoWait), - arguments(queue, receiveNoWait)); - } - } - - static final class DestinationsProvider implements ArgumentsProvider { - - @Override - public Stream provideArguments(ExtensionContext context) { - DestinationFactory topic = session -> session.createTopic("someTopic"); - DestinationFactory queue = session -> session.createQueue("someQueue"); - DestinationFactory tempTopic = Session::createTemporaryTopic; - DestinationFactory tempQueue = Session::createTemporaryQueue; - - return Stream.of( - arguments(topic, false), - arguments(queue, false), - arguments(tempTopic, true), - arguments(tempQueue, true)); - } - } - - @FunctionalInterface - interface DestinationFactory { - - Destination create(Session session) throws JMSException; - } - - @FunctionalInterface - interface MessageReceiver { - - Message receive(MessageConsumer consumer) throws JMSException; + MessagingIncubatingAttributes.MESSAGING_MESSAGE_ID, messageId)))); } } diff --git a/instrumentation/jms/jms-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/jms/v3_0/Jms3SuppressReceiveSpansTest.java b/instrumentation/jms/jms-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/jms/v3_0/Jms3SuppressReceiveSpansTest.java new file mode 100644 index 000000000000..3789a29acf34 --- /dev/null +++ b/instrumentation/jms/jms-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/jms/v3_0/Jms3SuppressReceiveSpansTest.java @@ -0,0 +1,85 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jms.v3_0; + +import static io.opentelemetry.api.trace.SpanKind.CONSUMER; +import static io.opentelemetry.api.trace.SpanKind.PRODUCER; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes; +import jakarta.jms.Destination; +import jakarta.jms.JMSException; +import jakarta.jms.MessageConsumer; +import jakarta.jms.MessageProducer; +import jakarta.jms.TextMessage; +import org.apache.activemq.artemis.jms.client.ActiveMQDestination; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ArgumentsSource; + +class Jms3SuppressReceiveSpansTest extends AbstractJms3Test { + + @ArgumentsSource(DestinationsProvider.class) + @ParameterizedTest + void testMessageConsumer(DestinationFactory destinationFactory, boolean isTemporary) + throws JMSException { + + // given + Destination destination = destinationFactory.create(session); + TextMessage sentMessage = session.createTextMessage("hello there"); + + MessageProducer producer = session.createProducer(destination); + cleanup.deferCleanup(producer); + MessageConsumer consumer = session.createConsumer(destination); + cleanup.deferCleanup(consumer); + + // when + testing.runWithSpan("producer parent", () -> producer.send(sentMessage)); + + TextMessage receivedMessage = + testing.runWithSpan("consumer parent", () -> (TextMessage) consumer.receive()); + + // then + assertThat(receivedMessage.getText()).isEqualTo(sentMessage.getText()); + + String actualDestinationName = ((ActiveMQDestination) destination).getName(); + // artemis consumers don't know whether the destination is temporary or not + String producerDestinationName = isTemporary ? "(temporary)" : actualDestinationName; + String messageId = receivedMessage.getJMSMessageID(); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("producer parent").hasNoParent(), + span -> + span.hasName(producerDestinationName + " publish") + .hasKind(PRODUCER) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo(MessagingIncubatingAttributes.MESSAGING_SYSTEM, "jms"), + equalTo( + MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME, + producerDestinationName), + equalTo(MessagingIncubatingAttributes.MESSAGING_OPERATION, "publish"), + equalTo(MessagingIncubatingAttributes.MESSAGING_MESSAGE_ID, messageId), + messagingTempDestination(isTemporary)), + span -> + span.hasName(actualDestinationName + " receive") + .hasKind(CONSUMER) + .hasParent(trace.getSpan(1)) + .hasTotalRecordedLinks(0) + .hasAttributesSatisfyingExactly( + equalTo(MessagingIncubatingAttributes.MESSAGING_SYSTEM, "jms"), + equalTo( + MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME, + actualDestinationName), + equalTo(MessagingIncubatingAttributes.MESSAGING_OPERATION, "receive"), + equalTo( + MessagingIncubatingAttributes.MESSAGING_MESSAGE_ID, messageId))), + trace -> + trace.hasSpansSatisfyingExactly(span -> span.hasName("consumer parent").hasNoParent())); + } +} diff --git a/instrumentation/jms/jms-common/bootstrap/build.gradle.kts b/instrumentation/jms/jms-common/bootstrap/build.gradle.kts new file mode 100644 index 000000000000..072a96df450f --- /dev/null +++ b/instrumentation/jms/jms-common/bootstrap/build.gradle.kts @@ -0,0 +1,3 @@ +plugins { + id("otel.javaagent-bootstrap") +} diff --git a/instrumentation/jms/jms-common/bootstrap/src/main/java/io/opentelemetry/javaagent/bootstrap/jms/JmsReceiveContextHolder.java b/instrumentation/jms/jms-common/bootstrap/src/main/java/io/opentelemetry/javaagent/bootstrap/jms/JmsReceiveContextHolder.java new file mode 100644 index 000000000000..2894fb2c305b --- /dev/null +++ b/instrumentation/jms/jms-common/bootstrap/src/main/java/io/opentelemetry/javaagent/bootstrap/jms/JmsReceiveContextHolder.java @@ -0,0 +1,47 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.bootstrap.jms; + +import static io.opentelemetry.context.ContextKey.named; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.ContextKey; +import io.opentelemetry.context.ImplicitContextKeyed; +import javax.annotation.Nullable; + +public final class JmsReceiveContextHolder implements ImplicitContextKeyed { + private static final ContextKey KEY = + named("opentelemetry-jms-receive-context"); + + private Context receiveContext; + + private JmsReceiveContextHolder() {} + + public static Context init(Context context) { + if (context.get(KEY) != null) { + return context; + } + return context.with(new JmsReceiveContextHolder()); + } + + public static void set(Context receiveContext) { + JmsReceiveContextHolder holder = receiveContext.get(KEY); + if (holder != null) { + holder.receiveContext = receiveContext; + } + } + + @Nullable + public static Context getReceiveContext(Context context) { + JmsReceiveContextHolder holder = context.get(KEY); + return holder != null ? holder.receiveContext : null; + } + + @Override + public Context storeInContext(Context context) { + return context.with(KEY, this); + } +} diff --git a/instrumentation/jms/jms-common/javaagent-unit-tests/build.gradle.kts b/instrumentation/jms/jms-common/javaagent-unit-tests/build.gradle.kts index 866586a5faa6..2a7bf50d684d 100644 --- a/instrumentation/jms/jms-common/javaagent-unit-tests/build.gradle.kts +++ b/instrumentation/jms/jms-common/javaagent-unit-tests/build.gradle.kts @@ -5,5 +5,5 @@ plugins { dependencies { testImplementation(project(":instrumentation:jms:jms-common:javaagent")) testImplementation(project(":instrumentation-api")) - testImplementation(project(":instrumentation-api-semconv")) + testImplementation(project(":instrumentation-api-incubator")) } diff --git a/instrumentation/jms/jms-common/javaagent/build.gradle.kts b/instrumentation/jms/jms-common/javaagent/build.gradle.kts index afe601decbdc..0125f1afcde1 100644 --- a/instrumentation/jms/jms-common/javaagent/build.gradle.kts +++ b/instrumentation/jms/jms-common/javaagent/build.gradle.kts @@ -5,4 +5,6 @@ plugins { dependencies { compileOnly("com.google.auto.value:auto-value-annotations") annotationProcessor("com.google.auto.value:auto-value") + + bootstrap(project(":instrumentation:jms:jms-common:bootstrap")) } diff --git a/instrumentation/jms/jms-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jms/JmsInstrumenterFactory.java b/instrumentation/jms/jms-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jms/JmsInstrumenterFactory.java index f8903ab378ff..b229bcb303ea 100644 --- a/instrumentation/jms/jms-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jms/JmsInstrumenterFactory.java +++ b/instrumentation/jms/jms-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jms/JmsInstrumenterFactory.java @@ -9,12 +9,13 @@ import com.google.errorprone.annotations.CanIgnoreReturnValue; import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.instrumentation.api.incubator.semconv.messaging.MessageOperation; +import io.opentelemetry.instrumentation.api.incubator.semconv.messaging.MessagingAttributesExtractor; +import io.opentelemetry.instrumentation.api.incubator.semconv.messaging.MessagingSpanNameExtractor; 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 io.opentelemetry.instrumentation.api.instrumenter.messaging.MessageOperation; -import io.opentelemetry.instrumentation.api.instrumenter.messaging.MessagingAttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.messaging.MessagingSpanNameExtractor; import io.opentelemetry.instrumentation.api.internal.PropagatorBasedSpanLinksExtractor; import java.util.List; @@ -45,7 +46,7 @@ public JmsInstrumenterFactory setMessagingReceiveInstrumentationEnabled( public Instrumenter createProducerInstrumenter() { JmsMessageAttributesGetter getter = JmsMessageAttributesGetter.INSTANCE; - MessageOperation operation = MessageOperation.SEND; + MessageOperation operation = MessageOperation.PUBLISH; return Instrumenter.builder( openTelemetry, @@ -59,30 +60,41 @@ public Instrumenter createConsumerReceiveInstrumen JmsMessageAttributesGetter getter = JmsMessageAttributesGetter.INSTANCE; MessageOperation operation = MessageOperation.RECEIVE; - // MessageConsumer does not do context propagation - return Instrumenter.builder( - openTelemetry, - instrumentationName, - MessagingSpanNameExtractor.create(getter, operation)) - .addAttributesExtractor(createMessagingAttributesExtractor(operation)) - .setEnabled(messagingReceiveInstrumentationEnabled) - .addSpanLinksExtractor( - new PropagatorBasedSpanLinksExtractor<>( - openTelemetry.getPropagators().getTextMapPropagator(), - MessagePropertyGetter.INSTANCE)) - .buildInstrumenter(SpanKindExtractor.alwaysConsumer()); + InstrumenterBuilder builder = + Instrumenter.builder( + openTelemetry, + instrumentationName, + MessagingSpanNameExtractor.create(getter, operation)) + .addAttributesExtractor(createMessagingAttributesExtractor(operation)); + if (messagingReceiveInstrumentationEnabled) { + builder.addSpanLinksExtractor( + new PropagatorBasedSpanLinksExtractor<>( + openTelemetry.getPropagators().getTextMapPropagator(), + MessagePropertyGetter.INSTANCE)); + } + return builder.buildInstrumenter(SpanKindExtractor.alwaysConsumer()); } - public Instrumenter createConsumerProcessInstrumenter() { + public Instrumenter createConsumerProcessInstrumenter( + boolean canHaveReceiveInstrumentation) { JmsMessageAttributesGetter getter = JmsMessageAttributesGetter.INSTANCE; MessageOperation operation = MessageOperation.PROCESS; - return Instrumenter.builder( - openTelemetry, - instrumentationName, - MessagingSpanNameExtractor.create(getter, operation)) - .addAttributesExtractor(createMessagingAttributesExtractor(operation)) - .buildConsumerInstrumenter(MessagePropertyGetter.INSTANCE); + InstrumenterBuilder builder = + Instrumenter.builder( + openTelemetry, + instrumentationName, + MessagingSpanNameExtractor.create(getter, operation)) + .addAttributesExtractor(createMessagingAttributesExtractor(operation)); + if (canHaveReceiveInstrumentation && messagingReceiveInstrumentationEnabled) { + builder.addSpanLinksExtractor( + new PropagatorBasedSpanLinksExtractor<>( + openTelemetry.getPropagators().getTextMapPropagator(), + MessagePropertyGetter.INSTANCE)); + return builder.buildInstrumenter(SpanKindExtractor.alwaysConsumer()); + } else { + return builder.buildConsumerInstrumenter(MessagePropertyGetter.INSTANCE); + } } private AttributesExtractor createMessagingAttributesExtractor( diff --git a/instrumentation/jms/jms-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jms/JmsMessageAttributesGetter.java b/instrumentation/jms/jms-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jms/JmsMessageAttributesGetter.java index 93c854bfb3f9..14786be860fc 100644 --- a/instrumentation/jms/jms-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jms/JmsMessageAttributesGetter.java +++ b/instrumentation/jms/jms-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jms/JmsMessageAttributesGetter.java @@ -7,7 +7,7 @@ import static java.util.logging.Level.FINE; -import io.opentelemetry.instrumentation.api.instrumenter.messaging.MessagingAttributesGetter; +import io.opentelemetry.instrumentation.api.incubator.semconv.messaging.MessagingAttributesGetter; import java.util.Collections; import java.util.List; import java.util.logging.Logger; @@ -29,11 +29,22 @@ public String getDestination(MessageWithDestination messageWithDestination) { return messageWithDestination.destinationName(); } + @Nullable + @Override + public String getDestinationTemplate(MessageWithDestination messageWithDestination) { + return null; + } + @Override public boolean isTemporaryDestination(MessageWithDestination messageWithDestination) { return messageWithDestination.isTemporaryDestination(); } + @Override + public boolean isAnonymousDestination(MessageWithDestination messageWithDestination) { + return false; + } + @Nullable @Override public String getConversationId(MessageWithDestination messageWithDestination) { @@ -47,13 +58,13 @@ public String getConversationId(MessageWithDestination messageWithDestination) { @Nullable @Override - public Long getMessagePayloadSize(MessageWithDestination messageWithDestination) { + public Long getMessageBodySize(MessageWithDestination messageWithDestination) { return null; } @Nullable @Override - public Long getMessagePayloadCompressedSize(MessageWithDestination messageWithDestination) { + public Long getMessageEnvelopeSize(MessageWithDestination messageWithDestination) { return null; } @@ -68,6 +79,19 @@ public String getMessageId(MessageWithDestination messageWithDestination, Void u } } + @Nullable + @Override + public String getClientId(MessageWithDestination messageWithDestination) { + return null; + } + + @Nullable + @Override + public Long getBatchMessageCount( + MessageWithDestination messageWithDestination, @Nullable Void unused) { + return null; + } + @Override public List getMessageHeader(MessageWithDestination messageWithDestination, String name) { try { diff --git a/instrumentation/jms/jms-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jms/JmsReceiveSpanUtil.java b/instrumentation/jms/jms-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jms/JmsReceiveSpanUtil.java new file mode 100644 index 000000000000..be0bbd88b243 --- /dev/null +++ b/instrumentation/jms/jms-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jms/JmsReceiveSpanUtil.java @@ -0,0 +1,51 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jms; + +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.propagation.ContextPropagators; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import io.opentelemetry.instrumentation.api.internal.InstrumenterUtil; +import io.opentelemetry.instrumentation.api.internal.Timer; +import io.opentelemetry.javaagent.bootstrap.internal.ExperimentalConfig; +import io.opentelemetry.javaagent.bootstrap.jms.JmsReceiveContextHolder; + +public final class JmsReceiveSpanUtil { + private static final ContextPropagators propagators = GlobalOpenTelemetry.getPropagators(); + private static final boolean receiveInstrumentationEnabled = + ExperimentalConfig.get().messagingReceiveInstrumentationEnabled(); + + public static void createReceiveSpan( + Instrumenter receiveInstrumenter, + MessageWithDestination request, + Timer timer, + Throwable throwable) { + Context parentContext = Context.current(); + // if receive instrumentation is not enabled we'll use the producer as parent + if (!receiveInstrumentationEnabled) { + parentContext = + propagators + .getTextMapPropagator() + .extract(parentContext, request, MessagePropertyGetter.INSTANCE); + } + + if (receiveInstrumenter.shouldStart(parentContext, request)) { + Context receiveContext = + InstrumenterUtil.startAndEnd( + receiveInstrumenter, + parentContext, + request, + null, + throwable, + timer.startTime(), + timer.now()); + JmsReceiveContextHolder.set(receiveContext); + } + } + + private JmsReceiveSpanUtil() {} +} diff --git a/instrumentation/jms/jms-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jms/MessageWithDestination.java b/instrumentation/jms/jms-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jms/MessageWithDestination.java index d394a77427bd..1c55fc693e17 100644 --- a/instrumentation/jms/jms-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jms/MessageWithDestination.java +++ b/instrumentation/jms/jms-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jms/MessageWithDestination.java @@ -31,11 +31,13 @@ public static MessageWithDestination create( jmsDestination = fallbackDestination; } - if (jmsDestination.isQueue()) { - return createMessageWithQueue(message, jmsDestination); - } - if (jmsDestination.isTopic()) { - return createMessageWithTopic(message, jmsDestination); + if (jmsDestination != null) { + if (jmsDestination.isQueue()) { + return createMessageWithQueue(message, jmsDestination); + } + if (jmsDestination.isTopic()) { + return createMessageWithTopic(message, jmsDestination); + } } return new AutoValue_MessageWithDestination( message, "unknown", /* isTemporaryDestination= */ false); diff --git a/instrumentation/jmx-metrics/javaagent/README.md b/instrumentation/jmx-metrics/javaagent/README.md index 8680e878814e..844137872f82 100644 --- a/instrumentation/jmx-metrics/javaagent/README.md +++ b/instrumentation/jmx-metrics/javaagent/README.md @@ -26,6 +26,7 @@ $ java -javaagent:path/to/opentelemetry-javaagent.jar \ No targets are enabled by default. The supported target environments are listed below. - [activemq](activemq.md) +- [camel](camel.md) - [jetty](jetty.md) - [kafka-broker](kafka-broker.md) - [tomcat](tomcat.md) diff --git a/instrumentation/jmx-metrics/javaagent/activemq.md b/instrumentation/jmx-metrics/javaagent/activemq.md index 8cdc14dec307..49a01985291e 100644 --- a/instrumentation/jmx-metrics/javaagent/activemq.md +++ b/instrumentation/jmx-metrics/javaagent/activemq.md @@ -2,16 +2,16 @@ Here is the list of metrics based on MBeans exposed by ActiveMQ. -| Metric Name | Type | Attributes | Description | -| ---------------- | --------------- | ---------------- | --------------- | -| activemq.ProducerCount | UpDownCounter | destination, broker | The number of producers attached to this destination | -| activemq.ConsumerCount | UpDownCounter | destination, broker | The number of consumers subscribed to this destination | -| activemq.memory.MemoryPercentUsage | Gauge | destination, broker | The percentage of configured memory used | -| activemq.message.QueueSize | UpDownCounter | destination, broker | The current number of messages waiting to be consumed | -| activemq.message.ExpiredCount | Counter | destination, broker | The number of messages not delivered because they expired | -| activemq.message.EnqueueCount | Counter | destination, broker | The number of messages sent to this destination | -| activemq.message.DequeueCount | Counter | destination, broker | The number of messages acknowledged and removed from this destination | -| activemq.message.AverageEnqueueTime | Gauge | destination, broker | The average time a message was held on this destination | -| activemq.connections.CurrentConnectionsCount | UpDownCounter | | The total number of current connections | -| activemq.disc.StorePercentUsage | Gauge | | The percentage of configured disk used for persistent messages | -| activemq.disc.TempPercentUsage | Gauge | | The percentage of configured disk used for non-persistent messages | +| Metric Name | Type | Attributes | Description | +| -------------------------------------------- | ------------- | ------------------- | --------------------------------------------------------------------- | +| activemq.ProducerCount | UpDownCounter | destination, broker | The number of producers attached to this destination | +| activemq.ConsumerCount | UpDownCounter | destination, broker | The number of consumers subscribed to this destination | +| activemq.memory.MemoryPercentUsage | Gauge | destination, broker | The percentage of configured memory used | +| activemq.message.QueueSize | UpDownCounter | destination, broker | The current number of messages waiting to be consumed | +| activemq.message.ExpiredCount | Counter | destination, broker | The number of messages not delivered because they expired | +| activemq.message.EnqueueCount | Counter | destination, broker | The number of messages sent to this destination | +| activemq.message.DequeueCount | Counter | destination, broker | The number of messages acknowledged and removed from this destination | +| activemq.message.AverageEnqueueTime | Gauge | destination, broker | The average time a message was held on this destination | +| activemq.connections.CurrentConnectionsCount | UpDownCounter | | The total number of current connections | +| activemq.disc.StorePercentUsage | Gauge | | The percentage of configured disk used for persistent messages | +| activemq.disc.TempPercentUsage | Gauge | | The percentage of configured disk used for non-persistent messages | diff --git a/instrumentation/jmx-metrics/javaagent/camel.md b/instrumentation/jmx-metrics/javaagent/camel.md new file mode 100644 index 000000000000..724fb488aa2c --- /dev/null +++ b/instrumentation/jmx-metrics/javaagent/camel.md @@ -0,0 +1,51 @@ +# Camel Metrics + +Here is the list of metrics based on MBeans exposed by Camel. + +| Metric Name | Type | Attributes | Description | +|------------------------------------------------|---------------|----------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| camel.context.exchange | Counter | context, camelVersion | Indicates the total number of exchanges, passed or failed, processed since context start-up or the last reset operation. | +| camel.context.exchange.completed | Counter | context, camelVersion | Indicates the total number of exchanges processed successfully since context start-up or the last reset operation. | +| camel.context.exchange.failed | Counter | context, camelVersion | Indicates the total number of exchanges that failed to process since context start-up or the last reset operation. | +| camel.context.exchange.failed_handled | Counter | context, camelVersion | Indicates the number of exchanges failed and handled by an ExceptionHandler in the context. | +| camel.context.exchange.inflight | UpDownCounter | context, camelVersion | Indicates the number of exchanges currently transiting the context. | +| camel.context.exchange.processing.delta_time | Gauge | context, camelVersion | Indicates the difference, in milliseconds, of the Processing Time of the last two exchanges transited the context. | +| camel.context.exchange.processing.last_time | Gauge | context, camelVersion | Indicates the time, in milliseconds, it took to process the last exchange. | +| camel.context.exchange.processing.max_time | Gauge | context, camelVersion | Indicates the longest time, in milliseconds, to process an exchange since context start-up or the last reset operation. | +| camel.context.exchange.processing.mean_time | Gauge | context, camelVersion | Indicates the mean processing time, in milliseconds, for all exchanges processed since context start-up or the last reset operation. | +| camel.context.exchange.processing.min_time | Gauge | context, camelVersion | Indicates the shortest time, in milliseconds, to process an exchange since context start-up or the last reset operation. | +| camel.context.exchange.processing.time | Counter | context, camelVersion | Indicates the total processing time, in milliseconds, to process all exchanges since context start-up or the last reset operation. | +| camel.context.exchange.redelivered | Counter | context, camelVersion | Number of exchanges redelivered (internal only) since context start-up or the last reset operation. | +| camel.context.exchange.redelivered_external | Counter | context, camelVersion | The total number of all external initiated redeliveries (such as from JMS broker) since context start-up or the last reset operation. | +| camel.route.exchange | Counter | context, route | Indicates the total number of exchanges, passed or failed, that the route has processed since route start-up or the last reset operation. | +| camel.route.exchange.completed | Counter | context, route | Indicates the total number of exchanges the route has processed successfully since route start-up or the last reset operation. | +| camel.route.exchange.failed | Counter | context, route | Indicates the total number of exchanges that the route has failed to process since route start-up or the last reset operation. | +| camel.route.exchange.failed_handled | Counter | context, route | Indicates the number of exchanges failed and handled by an ExceptionHandler in the route. | +| camel.route.exchange.inflight | UpDownCounter | context, route | Indicates the number of exchanges currently transiting the route. | +| camel.route.exchange.processing.delta_time | Gauge | context, route | Indicates the difference, in milliseconds, of the Processing Time of the last two exchanges transited the route. | +| camel.route.exchange.processing.last_time | Gauge | context, route | Indicates the time, in milliseconds, it took the route to process the last exchange. | +| camel.route.exchange.processing.max_time | Gauge | context, route | Indicates the longest time, in milliseconds, to process an exchange since the route start-up or the last reset operation. | +| camel.route.exchange.processing.mean_time | Gauge | context, route | Indicates the mean processing time, in milliseconds, for all exchanges processed since the route start-up or the last reset operation. | +| camel.route.exchange.processing.min_time | Gauge | context, route | Indicates the shortest time, in milliseconds, to process an exchange since the route start-up or the last reset operation. | +| camel.route.exchange.processing.time | Counter | context, route | Indicates the total processing time, in milliseconds, of all exchanges the selected processed since route start-up or the last reset operation. | +| camel.route.exchange.redelivered | Counter | context, route | Number of exchanges redelivered (internal only) since route start-up or the last reset operation. | +| camel.route.exchange.redelivered_external | Counter | context, route | The total number of all external initiated redeliveries (such as from JMS broker) since the route start-up or the last reset operation. | +| camel.processor.exchange | Counter | context, route, processor, destination | Indicates the total number of exchanges, passed or failed, that the selected processor has processed since processor start-up or the last reset operation. | +| camel.processor.exchange.completed | Counter | context, route, processor, destination | Indicates the total number of exchanges the selected processor has processed successfully since processor start-up or the last reset operation. | +| camel.processor.exchange.failed | Counter | context, route, processor, destination | Indicates the total number of exchanges that the selected processor has failed to process since processor start-up or the last reset operation. | +| camel.processor.exchange.inflight | UpDownCounter | context, route, processor, destination | Indicates the number of exchanges currently transiting the processor. | +| camel.processor.exchange.failed_handled | Counter | context, route, processor, destination | Indicates the number of exchanges failed and handled by an ExceptionHandler in the context. | +| camel.processor.exchange.processing.delta_time | Gauge | context, route, processor, destination | Indicates the difference, in milliseconds, of the Processing Time of the last two exchanges transited the selected processor. | +| camel.processor.exchange.processing.last_time | Gauge | context, route, processor, destination | Indicates the time, in milliseconds, it took the selected processor to process the last exchange. | +| camel.processor.exchange.processing.max_time | Gauge | context, route, processor, destination | Indicates the longest time, in milliseconds, to process an exchange since processor start-up or the last reset operation. | +| camel.processor.exchange.processing.mean_time | Gauge | context, route, processor, destination | Indicates the mean processing time, in milliseconds, for all exchanges processed since processor start-up or the last reset operation. | +| camel.processor.exchange.processing.min_time | Gauge | context, route, processor, destination | Indicates the shortest time, in milliseconds, to process an exchange since processor start-up or the last reset operation. | +| camel.processor.exchange.processing.time | Counter | context, route, processor, destination | Indicates the total processing time, in milliseconds, to process all exchanges since start-up or the last reset operation. | +| camel.processor.exchange.redelivered | Counter | context, route, processor, destination | Number of exchanges redelivered (internal only) since selected processor start-up or the last reset operation. | +| camel.processor.exchange.redelivered_external | Counter | context, route, processor, destination | The total number of all external initiated redeliveries (such as from JMS broker) since processor start-up or the last reset operation. | +| camel.threadpool.active | UpDownCounter | context, route | The approximate number of threads that are actively executing tasks. | +| camel.threadpool.pool.size | UpDownCounter | context, route | The current number of threads in the pool. | +| camel.threadpool.pool.largest_size | Gauge | context, route | The largest number of threads that have ever simultaneously been in the pool. | +| camel.threadpool.task | Counter | context, route | The approximate total number of tasks that have ever been scheduled for execution. | +| camel.threadpool.task.completed | Counter | context, route | The approximate total number of tasks that have completed execution. Because the states of tasks and threads may change dynamically during computation, the returned value is only an approximation, but one that does not ever decrease across successive calls. | +| camel.threadpool.task.queue_size | UpDownCounter | context, route | The number of Tasks in the Task Queue. | diff --git a/instrumentation/jmx-metrics/javaagent/hadoop.md b/instrumentation/jmx-metrics/javaagent/hadoop.md index 7e628fe0464a..24e2b7cf78be 100644 --- a/instrumentation/jmx-metrics/javaagent/hadoop.md +++ b/instrumentation/jmx-metrics/javaagent/hadoop.md @@ -3,7 +3,7 @@ Here is the list of metrics based on MBeans exposed by Hadoop. | Metric Name | Type | Attributes | Description | -|-----------------------------------|---------------|------------------|-------------------------------------------------------| +| --------------------------------- | ------------- | ---------------- | ----------------------------------------------------- | | hadoop.capacity.CapacityUsed | UpDownCounter | node_name | Current used capacity across all data nodes | | hadoop.capacity.CapacityTotal | UpDownCounter | node_name | Current raw capacity of data nodes | | hadoop.block.BlocksTotal | UpDownCounter | node_name | Current number of allocated blocks in the system | diff --git a/instrumentation/jmx-metrics/javaagent/jetty.md b/instrumentation/jmx-metrics/javaagent/jetty.md index e04622a17918..d771214cfc48 100644 --- a/instrumentation/jmx-metrics/javaagent/jetty.md +++ b/instrumentation/jmx-metrics/javaagent/jetty.md @@ -3,7 +3,7 @@ Here is the list of metrics based on MBeans exposed by Jetty. | Metric Name | Type | Attributes | Description | -|--------------------------------|---------------|--------------|------------------------------------------------------| +| ------------------------------ | ------------- | ------------ | ---------------------------------------------------- | | jetty.session.sessionsCreated | Counter | resource | The number of sessions established in total | | jetty.session.sessionTimeTotal | Counter | resource | The total time sessions have been active | | jetty.session.sessionTimeMax | Gauge | resource | The maximum amount of time a session has been active | diff --git a/instrumentation/jmx-metrics/javaagent/kafka-broker.md b/instrumentation/jmx-metrics/javaagent/kafka-broker.md index 2dddfbb19d82..c0b8f1d394c7 100644 --- a/instrumentation/jmx-metrics/javaagent/kafka-broker.md +++ b/instrumentation/jmx-metrics/javaagent/kafka-broker.md @@ -4,7 +4,7 @@ Here is the list of metrics based on MBeans exposed by Kafka broker.

Log metrics: | Metric Name | Type | Attributes | Description | -|---------------------------|---------|------------|----------------------------------| +| ------------------------- | ------- | ---------- | -------------------------------- | | kafka.logs.flush.count | Counter | | Log flush count | | kafka.logs.flush.time.50p | Gauge | | Log flush time - 50th percentile | | kafka.logs.flush.time.99p | Gauge | | Log flush time - 99th percentile | diff --git a/instrumentation/jmx-metrics/javaagent/src/main/java/io/opentelemetry/instrumentation/javaagent/jmx/JmxMetricInsightInstaller.java b/instrumentation/jmx-metrics/javaagent/src/main/java/io/opentelemetry/instrumentation/javaagent/jmx/JmxMetricInsightInstaller.java index 17687fec57ba..74a6f75ffb4f 100644 --- a/instrumentation/jmx-metrics/javaagent/src/main/java/io/opentelemetry/instrumentation/javaagent/jmx/JmxMetricInsightInstaller.java +++ b/instrumentation/jmx-metrics/javaagent/src/main/java/io/opentelemetry/instrumentation/javaagent/jmx/JmxMetricInsightInstaller.java @@ -15,10 +15,12 @@ import io.opentelemetry.instrumentation.jmx.yaml.RuleParser; import io.opentelemetry.javaagent.extension.AgentListener; import io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdk; +import io.opentelemetry.sdk.autoconfigure.internal.AutoConfigureUtil; import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; import java.io.InputStream; import java.nio.file.Files; import java.nio.file.Paths; +import java.time.Duration; import java.util.List; /** An {@link AgentListener} that enables JMX metrics during agent startup. */ @@ -27,26 +29,26 @@ public class JmxMetricInsightInstaller implements AgentListener { @Override public void afterAgent(AutoConfiguredOpenTelemetrySdk autoConfiguredSdk) { - ConfigProperties config = autoConfiguredSdk.getConfig(); + ConfigProperties config = AutoConfigureUtil.getConfig(autoConfiguredSdk); if (config.getBoolean("otel.jmx.enabled", true)) { JmxMetricInsight service = - JmxMetricInsight.createService(GlobalOpenTelemetry.get(), beanDiscoveryDelay(config)); + JmxMetricInsight.createService( + GlobalOpenTelemetry.get(), beanDiscoveryDelay(config).toMillis()); MetricConfiguration conf = buildMetricConfiguration(config); service.start(conf); } } - private static long beanDiscoveryDelay(ConfigProperties configProperties) { - Long discoveryDelay = configProperties.getLong("otel.jmx.discovery.delay"); + private static Duration beanDiscoveryDelay(ConfigProperties configProperties) { + Duration discoveryDelay = configProperties.getDuration("otel.jmx.discovery.delay"); if (discoveryDelay != null) { return discoveryDelay; } // If discovery delay has not been configured, have a peek at the metric export interval. // It makes sense for both of these values to be similar. - long exportInterval = configProperties.getLong("otel.metric.export.interval", 60000); - return exportInterval; + return configProperties.getDuration("otel.metric.export.interval", Duration.ofMinutes(1)); } private static String resourceFor(String platform) { diff --git a/instrumentation/jmx-metrics/javaagent/src/main/resources/jmx/rules/activemq.yaml b/instrumentation/jmx-metrics/javaagent/src/main/resources/jmx/rules/activemq.yaml index 3b4e03334fa5..c7d669d55282 100644 --- a/instrumentation/jmx-metrics/javaagent/src/main/resources/jmx/rules/activemq.yaml +++ b/instrumentation/jmx-metrics/javaagent/src/main/resources/jmx/rules/activemq.yaml @@ -1,6 +1,5 @@ --- rules: - - beans: - org.apache.activemq:type=Broker,brokerName=*,destinationType=Queue,destinationName=* - org.apache.activemq:type=Broker,brokerName=*,destinationType=Topic,destinationName=* @@ -10,36 +9,36 @@ rules: prefix: activemq. mapping: ProducerCount: - unit: '{producers}' + unit: "{producers}" type: updowncounter desc: The number of producers attached to this destination ConsumerCount: - unit: '{consumers}' + unit: "{consumers}" type: updowncounter desc: The number of consumers subscribed to this destination MemoryPercentUsage: metric: memory.MemoryPercentUsage - unit: '%' + unit: "%" type: gauge desc: The percentage of configured memory used QueueSize: metric: message.QueueSize - unit: '{messages}' + unit: "{messages}" type: updowncounter desc: The current number of messages waiting to be consumed ExpiredCount: metric: message.ExpiredCount - unit: '{messages}' + unit: "{messages}" type: counter desc: The number of messages not delivered because they expired EnqueueCount: metric: message.EnqueueCount - unit: '{messages}' + unit: "{messages}" type: counter desc: The number of messages sent to this destination DequeueCount: metric: message.DequeueCount - unit: '{messages}' + unit: "{messages}" type: counter desc: The number of messages acknowledged and removed from this destination AverageEnqueueTime: @@ -52,13 +51,13 @@ rules: metricAttribute: broker: param(brokerName) prefix: activemq. - unit: '%' + unit: "%" type: gauge mapping: CurrentConnectionsCount: metric: connections.CurrentConnectionsCount type: updowncounter - unit: '{connections}' + unit: "{connections}" desc: The total number of current connections StorePercentUsage: metric: disc.StorePercentUsage diff --git a/instrumentation/jmx-metrics/javaagent/src/main/resources/jmx/rules/camel.yaml b/instrumentation/jmx-metrics/javaagent/src/main/resources/jmx/rules/camel.yaml new file mode 100644 index 000000000000..a34fa04a437f --- /dev/null +++ b/instrumentation/jmx-metrics/javaagent/src/main/resources/jmx/rules/camel.yaml @@ -0,0 +1,263 @@ +--- +rules: + - bean: org.apache.camel:context=*,type=context,name=* + prefix: camel.context. + metricAttribute: + context: param(context) + camelVersion: beanattr(CamelVersion) + mapping: + ExchangesCompleted: + metric: exchange.completed + type: counter + unit: "{exchanges}" + desc: Indicates the total number of exchanges processed successfully since context start-up or the last reset operation. + ExchangesFailed: + metric: exchange.failed + type: counter + unit: "{exchanges}" + desc: Indicates the total number of exchanges that failed to process since context start-up or the last reset operation. + ExchangesInflight: + metric: exchange.inflight + type: updowncounter + unit: "{exchanges}" + desc: Indicates the number of exchanges currently transiting the context. + ExchangesTotal: + metric: exchange + type: counter + unit: "{exchanges}" + desc: Indicates the total number of exchanges, passed or failed, processed context start-up or the last reset operation. + FailuresHandled: + metric: exchange.failed_handled + unit: "{exchanges}" + type: counter + desc: Indicates the number of exchanges failed and handled by an ExceptionHandler in the context. + Redeliveries: + metric: exchange.redelivered + type: counter + unit: "{exchanges}" + desc: Number of exchanges redelivered (internal only) since context start-up or the last reset operation. + ExternalRedeliveries: + metric: exchange.redelivered_external + type: counter + unit: "{exchanges}" + desc: The total number of all external initiated redeliveries (such as from JMS broker) since context start-up or the last reset operation. + MaxProcessingTime: + metric: exchange.processing.max_time + unit: ms + type: gauge + desc: Indicates the longest time, in milliseconds, to process an exchange since context start-up or the last reset operation. + MeanProcessingTime: + metric: exchange.processing.mean_time + type: gauge + unit: ms + desc: Indicates the mean processing time, in milliseconds, for all exchanges processed since context start-up or the last reset operation. + MinProcessingTime: + metric: exchange.processing.min_time + unit: ms + type: gauge + desc: Indicates the shortest time, in milliseconds, to process an exchange since context start-up or the last reset operation. + LastProcessingTime: + metric: exchange.processing.last_time + unit: ms + type: gauge + desc: Indicates the time, in milliseconds, it took to process the last exchange. + DeltaProcessingTime: + metric: exchange.processing.delta_time + type: gauge + unit: ms + desc: Indicates the difference, in milliseconds, of the Processing Time of the last two exchanges transited the context. + TotalProcessingTime: + metric: exchange.processing.time + type: counter + unit: ms + desc: Indicates the total processing time, in milliseconds, to process all exchanges since context start-up or the last reset operation. + +# Route Level metrics + + - bean: org.apache.camel:context=*,type=routes,name=* + prefix: camel.route. + metricAttribute: + route: beanattr(RouteId) + context: param(context) + mapping: + ExchangesCompleted: + metric: exchange.completed + type: counter + unit: "{exchanges}" + desc: Indicates the total number of exchanges the route has processed successfully since route start-up or the last reset operation. + ExchangesFailed: + metric: exchange.failed + type: counter + unit: "{exchanges}" + desc: Indicates the total number of exchanges that the route has failed to process since route start-up or the last reset operation. + ExchangesInflight: + metric: exchange.inflight + type: updowncounter + unit: "{exchanges}" + desc: Indicates the number of exchanges currently transiting the route. + ExchangesTotal: + metric: exchange + unit: "{exchanges}" + type: counter + desc: Indicates the total number of exchanges, passed or failed, that the route has processed since route start-up or the last reset operation. + FailuresHandled: + metric: exchange.failed_handled + unit: "{exchanges}" + type: counter + desc: Indicates the number of exchanges failed and handled by an ExceptionHandler in the route. + ExternalRedeliveries: + metric: exchange.redelivered_external + unit: "{exchanges}" + type: counter + desc: The total number of all external initiated redeliveries (such as from JMS broker) since the route start-up or the last reset operation. + Redeliveries: + metric: exchange.redelivered + type: counter + unit: "{exchanges}" + desc: Number of exchanges redelivered (internal only) since route start-up or the last reset operation. + MaxProcessingTime: + metric: exchange.processing.max_time + unit: ms + type: gauge + desc: Indicates the longest time, in milliseconds, to process an exchange since the route start-up or the last reset operation. + MeanProcessingTime: + metric: exchange.processing.mean_time + unit: ms + type: gauge + desc: Indicates the mean processing time, in milliseconds, for all exchanges processed since the route start-up or the last reset operation. + MinProcessingTime: + metric: exchange.processing.min_time + type: gauge + unit: ms + desc: Indicates the shortest time, in milliseconds, to process an exchange since the route start-up or the last reset operation. + LastProcessingTime: + metric: exchange.processing.last_time + type: gauge + unit: ms + desc: Indicates the time, in milliseconds, it took the route to process the last exchange. + DeltaProcessingTime: + metric: exchange.processing.delta_time + type: gauge + unit: ms + desc: Indicates the difference, in milliseconds, of the Processing Time of the last two exchanges transited the route. + TotalProcessingTime: + metric: exchange.processing.time + type: counter + unit: ms + desc: Indicates the total processing time, in milliseconds, of all exchanges the selected processed since route start-up or the last reset operation. + + +# Processor level Metrics + + - bean: org.apache.camel:context=*,type=processors,name=* + prefix: camel.processor. + metricAttribute: + processor: beanattr(ProcessorId) + route: beanattr(RouteId) + context: param(context) + destination: beanattr(Destination) + mapping: + ExchangesCompleted: + metric: exchange.completed + type: counter + unit: "{exchanges}" + desc: Indicates the total number of exchanges the selected processor has processed successfully since processor start-up or the last reset operation. + ExchangesFailed: + metric: exchange.failed + type: counter + unit: "{exchanges}" + desc: Indicates the total number of exchanges that the selected processor has failed to process since processor start-up or the last reset operation. + ExchangesInflight: + metric: exchange.inflight + type: updowncounter + unit: "{exchanges}" + desc: Indicates the number of exchanges currently transiting the processor. + ExchangesTotal: + metric: exchange + type: counter + unit: "{exchanges}" + desc: Indicates the total number of exchanges, passed or failed, that the selected processor has processed since processor start-up or the last reset operation. + FailuresHandled: + metric: exchange.failed_handled + unit: "{exchanges}" + type: counter + desc: Indicates the number of exchanges failed and handled by an ExceptionHandler in the context. + ExternalRedeliveries: + metric: exchange.redelivered_external + type: counter + unit: "{exchanges}" + desc: The total number of all external initiated redeliveries (such as from JMS broker) since processor start-up or the last reset operation. + Redeliveries: + metric: exchange.redelivered + type: counter + unit: "{exchanges}" + desc: Number of exchanges redelivered (internal only) since selected processor start-up or the last reset operation. + MaxProcessingTime: + metric: exchange.processing.max_time + unit: ms + type: gauge + desc: Indicates the longest time, in milliseconds, to process an exchange since processor start-up or the last reset operation. + MeanProcessingTime: + metric: exchange.processing.mean_time + type: gauge + unit: ms + desc: Indicates the mean processing time, in milliseconds, for all exchanges processed since processor start-up or the last reset operation. + MinProcessingTime: + metric: exchange.processing.min_time + type: gauge + unit: ms + desc: Indicates the shortest time, in milliseconds, to process an exchange since processor start-up or the last reset operation. + LastProcessingTime: + metric: exchange.processing.last_time + type: gauge + unit: ms + desc: Indicates the time, in milliseconds, it took the selected processor to process the last exchange. + DeltaProcessingTime: + metric: exchange.processing.delta_time + type: gauge + unit: ms + desc: Indicates the difference, in milliseconds, of the Processing Time of the last two exchanges transited the selected processor. + TotalProcessingTime: + metric: exchange.processing.time + type: counter + unit: ms + desc: Indicates the total processing time, in milliseconds, to process all exchanges since start-up or the last reset operation. + + + + - bean: org.apache.camel:context=*,type=threadpools,name=* + prefix: camel.threadpool. + metricAttribute: + route: beanattr(RouteId) + context: param(context) + mapping: + ActiveCount: + metric: active + type: updowncounter + unit: "{threads}" + desc: The approximate number of threads that are actively executing tasks. + CompletedTaskCount: + metric: task.completed + type: counter + unit: "{tasks}" + desc: The approximate total number of tasks that have completed execution. Because the states of tasks and threads may change dynamically during computation, the returned value is only an approximation, but one that does not ever decrease across successive calls. + PoolSize: + metric: pool.size + type: updowncounter + unit: "{threads}" + desc: The current number of threads in the pool. + LargestPoolSize: + metric: pool.largest_size + type: gauge + unit: "{threads}" + desc: The largest number of threads that have ever simultaneously been in the pool. + TaskCount: + metric: task + type: counter + unit: "{tasks}" + desc: The approximate total number of tasks that have ever been scheduled for execution. + TaskQueueSize: + metric: task.queue_size + type: updowncounter + unit: "{threads}" + desc: The number of Tasks in the Task Queue. diff --git a/instrumentation/jmx-metrics/javaagent/src/main/resources/jmx/rules/hadoop.yaml b/instrumentation/jmx-metrics/javaagent/src/main/resources/jmx/rules/hadoop.yaml index b46591492b97..82c32bd95ebb 100644 --- a/instrumentation/jmx-metrics/javaagent/src/main/resources/jmx/rules/hadoop.yaml +++ b/instrumentation/jmx-metrics/javaagent/src/main/resources/jmx/rules/hadoop.yaml @@ -1,63 +1,63 @@ --- rules: - - bean: Hadoop:service=NameNode,name=FSNamesystem - unit: "1" - prefix: hadoop. - metricAttribute: - node_name: param(tag.Hostname) - mapping: - CapacityUsed: - metric: capacity.CapacityUsed - type: updowncounter - unit: By - desc: Current used capacity across all data nodes - CapacityTotal: - metric: capacity.CapacityTotal - type: updowncounter - unit: By - BlocksTotal: - metric: block.BlocksTotal - type: updowncounter - unit: '{blocks}' - desc: Current number of allocated blocks in the system - MissingBlocks: - metric: block.MissingBlocks - type: updowncounter - unit: '{blocks}' - desc: Current number of missing blocks - CorruptBlocks: - metric: block.CorruptBlocks - type: updowncounter - unit: '{blocks}' - desc: Current number of blocks with corrupt replicas - VolumeFailuresTotal: - metric: volume.VolumeFailuresTotal - type: updowncounter - unit: '{volumes}' - desc: Total number of volume failures across all data nodes - metricAttribute: - direction: const(sent) - FilesTotal: - metric: file.FilesTotal - type: updowncounter - unit: '{files}' - desc: Current number of files and directories - TotalLoad: - metric: file.TotalLoad - type: updowncounter - unit: '{operations}' - desc: Current number of connections - NumLiveDataNodes: - metric: datenode.Count - type: updowncounter - unit: '{nodes}' - desc: The Number of data nodes - metricAttribute: - state: const(live) - NumDeadDataNodes: - metric: datenode.Count - type: updowncounter - unit: '{nodes}' - desc: The Number of data nodes - metricAttribute: - state: const(dead) + - bean: Hadoop:service=NameNode,name=FSNamesystem + unit: "1" + prefix: hadoop. + metricAttribute: + node_name: param(tag.Hostname) + mapping: + CapacityUsed: + metric: capacity.CapacityUsed + type: updowncounter + unit: By + desc: Current used capacity across all data nodes + CapacityTotal: + metric: capacity.CapacityTotal + type: updowncounter + unit: By + BlocksTotal: + metric: block.BlocksTotal + type: updowncounter + unit: "{blocks}" + desc: Current number of allocated blocks in the system + MissingBlocks: + metric: block.MissingBlocks + type: updowncounter + unit: "{blocks}" + desc: Current number of missing blocks + CorruptBlocks: + metric: block.CorruptBlocks + type: updowncounter + unit: "{blocks}" + desc: Current number of blocks with corrupt replicas + VolumeFailuresTotal: + metric: volume.VolumeFailuresTotal + type: updowncounter + unit: "{volumes}" + desc: Total number of volume failures across all data nodes + metricAttribute: + direction: const(sent) + FilesTotal: + metric: file.FilesTotal + type: updowncounter + unit: "{files}" + desc: Current number of files and directories + TotalLoad: + metric: file.TotalLoad + type: updowncounter + unit: "{operations}" + desc: Current number of connections + NumLiveDataNodes: + metric: datenode.Count + type: updowncounter + unit: "{nodes}" + desc: The Number of data nodes + metricAttribute: + state: const(live) + NumDeadDataNodes: + metric: datenode.Count + type: updowncounter + unit: "{nodes}" + desc: The Number of data nodes + metricAttribute: + state: const(dead) diff --git a/instrumentation/jmx-metrics/javaagent/src/main/resources/jmx/rules/jetty.yaml b/instrumentation/jmx-metrics/javaagent/src/main/resources/jmx/rules/jetty.yaml index 48df62df9f10..ed5435d9cc20 100644 --- a/instrumentation/jmx-metrics/javaagent/src/main/resources/jmx/rules/jetty.yaml +++ b/instrumentation/jmx-metrics/javaagent/src/main/resources/jmx/rules/jetty.yaml @@ -1,6 +1,5 @@ --- rules: - - bean: org.eclipse.jetty.server.session:context=*,type=sessionhandler,id=* unit: s prefix: jetty.session. @@ -9,7 +8,7 @@ rules: resource: param(context) mapping: sessionsCreated: - unit: '{sessions}' + unit: "{sessions}" type: counter desc: The number of sessions established in total sessionTimeTotal: @@ -24,7 +23,7 @@ rules: - bean: org.eclipse.jetty.util.thread:type=queuedthreadpool,id=* prefix: jetty.threads. - unit: '{threads}' + unit: "{threads}" type: updowncounter mapping: busyThreads: diff --git a/instrumentation/jmx-metrics/javaagent/src/main/resources/jmx/rules/kafka-broker.yaml b/instrumentation/jmx-metrics/javaagent/src/main/resources/jmx/rules/kafka-broker.yaml index 251c83091b01..0c03966235e5 100644 --- a/instrumentation/jmx-metrics/javaagent/src/main/resources/jmx/rules/kafka-broker.yaml +++ b/instrumentation/jmx-metrics/javaagent/src/main/resources/jmx/rules/kafka-broker.yaml @@ -2,203 +2,203 @@ rules: # Broker metrics - - bean: kafka.server:type=BrokerTopicMetrics,name=MessagesInPerSec - mapping: - Count: - metric: kafka.message.count - type: counter - desc: The number of messages received by the broker - unit: '{messages}' - - - bean: kafka.server:type=BrokerTopicMetrics,name=TotalFetchRequestsPerSec - metricAttribute: - type: const(fetch) - mapping: - Count: - metric: kafka.request.count - type: counter - desc: The number of requests received by the broker - unit: '{requests}' - - - bean: kafka.server:type=BrokerTopicMetrics,name=TotalProduceRequestsPerSec - metricAttribute: - type: const(produce) - mapping: - Count: - metric: kafka.request.count - type: counter - desc: The number of requests received by the broker - unit: '{requests}' - - - bean: kafka.server:type=BrokerTopicMetrics,name=FailedFetchRequestsPerSec - metricAttribute: - type: const(fetch) - mapping: - Count: - metric: kafka.request.failed - type: counter - desc: The number of requests to the broker resulting in a failure - unit: '{requests}' - - - bean: kafka.server:type=BrokerTopicMetrics,name=FailedProduceRequestsPerSec - metricAttribute: - type: const(produce) - mapping: - Count: - metric: kafka.request.failed - type: counter - desc: The number of requests to the broker resulting in a failure - unit: '{requests}' - - - beans: - - kafka.network:type=RequestMetrics,name=TotalTimeMs,request=Produce - - kafka.network:type=RequestMetrics,name=TotalTimeMs,request=FetchConsumer - - kafka.network:type=RequestMetrics,name=TotalTimeMs,request=FetchFollower - metricAttribute: - type: param(request) - unit: ms - mapping: - Count: - metric: kafka.request.time.total - type: counter - desc: The total time the broker has taken to service requests - 50thPercentile: - metric: kafka.request.time.50p - type: gauge - desc: The 50th percentile time the broker has taken to service requests - 99thPercentile: - metric: kafka.request.time.99p - type: gauge - desc: The 99th percentile time the broker has taken to service requests - - - bean: kafka.network:type=RequestChannel,name=RequestQueueSize - mapping: - Value: - metric: kafka.request.queue - type: updowncounter - desc: Size of the request queue - unit: '{requests}' - - - bean: kafka.server:type=BrokerTopicMetrics,name=BytesInPerSec - metricAttribute: - direction: const(in) - mapping: - Count: - metric: kafka.network.io - type: counter - desc: The bytes received or sent by the broker - unit: By - - - bean: kafka.server:type=BrokerTopicMetrics,name=BytesOutPerSec - metricAttribute: - direction: const(out) - mapping: - Count: - metric: kafka.network.io - type: counter - desc: The bytes received or sent by the broker - unit: By - - - beans: - - kafka.server:type=DelayedOperationPurgatory,name=PurgatorySize,delayedOperation=Produce - - kafka.server:type=DelayedOperationPurgatory,name=PurgatorySize,delayedOperation=Fetch - metricAttribute: - type: param(delayedOperation) - mapping: - Value: - metric: kafka.purgatory.size - type: updowncounter - desc: The number of requests waiting in purgatory - unit: '{requests}' - - - bean: kafka.server:type=ReplicaManager,name=PartitionCount - mapping: - Value: - metric: kafka.partition.count - type: updowncounter - desc: The number of partitions on the broker - unit: '{partitions}' - - - bean: kafka.controller:type=KafkaController,name=OfflinePartitionsCount - mapping: - Value: - metric: kafka.partition.offline - type: updowncounter - desc: The number of partitions offline - unit: '{partitions}' - - - bean: kafka.server:type=ReplicaManager,name=UnderReplicatedPartitions - mapping: - Value: - metric: kafka.partition.underReplicated - type: updowncounter - desc: The number of under replicated partitions - unit: '{partitions}' - - - bean: kafka.server:type=ReplicaManager,name=IsrShrinksPerSec - metricAttribute: - operation: const(shrink) - mapping: - Count: - metric: kafka.isr.operation.count - type: updowncounter - desc: The number of in-sync replica shrink and expand operations - unit: '{operations}' - - - bean: kafka.server:type=ReplicaManager,name=IsrExpandsPerSec - metricAttribute: - operation: const(expand) - mapping: - Count: - metric: kafka.isr.operation.count - type: updowncounter - desc: The number of in-sync replica shrink and expand operations - unit: '{operations}' - - - bean: kafka.server:type=ReplicaFetcherManager,name=MaxLag,clientId=Replica - mapping: - Value: - metric: kafka.lag.max - desc: The max lag in messages between follower and leader replicas - unit: '{messages}' - - - bean: kafka.controller:type=KafkaController,name=ActiveControllerCount - mapping: - Value: - metric: kafka.controller.active.count - type: updowncounter - desc: The number of controllers active on the broker - unit: '{controllers}' - - - bean: kafka.controller:type=ControllerStats,name=LeaderElectionRateAndTimeMs - mapping: - Count: - metric: kafka.leaderElection.count - type: counter - desc: The leader election count - unit: '{elections}' - - - bean: kafka.controller:type=ControllerStats,name=UncleanLeaderElectionsPerSec - mapping: - Count: - metric: kafka.leaderElection.unclean.count - type: counter - desc: Unclean leader election count - increasing indicates broker failures - unit: '{elections}' - - # Log metrics - - - bean: kafka.log:type=LogFlushStats,name=LogFlushRateAndTimeMs - unit: ms - type: gauge - prefix: kafka.logs.flush. - mapping: - Count: - type: counter - desc: Log flush count - 50thPercentile: - metric: time.50p - desc: Log flush time - 50th percentile - 99thPercentile: - metric: time.99p - desc: Log flush time - 99th percentile + - bean: kafka.server:type=BrokerTopicMetrics,name=MessagesInPerSec + mapping: + Count: + metric: kafka.message.count + type: counter + desc: The number of messages received by the broker + unit: "{messages}" + + - bean: kafka.server:type=BrokerTopicMetrics,name=TotalFetchRequestsPerSec + metricAttribute: + type: const(fetch) + mapping: + Count: + metric: kafka.request.count + type: counter + desc: The number of requests received by the broker + unit: "{requests}" + + - bean: kafka.server:type=BrokerTopicMetrics,name=TotalProduceRequestsPerSec + metricAttribute: + type: const(produce) + mapping: + Count: + metric: kafka.request.count + type: counter + desc: The number of requests received by the broker + unit: "{requests}" + + - bean: kafka.server:type=BrokerTopicMetrics,name=FailedFetchRequestsPerSec + metricAttribute: + type: const(fetch) + mapping: + Count: + metric: kafka.request.failed + type: counter + desc: The number of requests to the broker resulting in a failure + unit: "{requests}" + + - bean: kafka.server:type=BrokerTopicMetrics,name=FailedProduceRequestsPerSec + metricAttribute: + type: const(produce) + mapping: + Count: + metric: kafka.request.failed + type: counter + desc: The number of requests to the broker resulting in a failure + unit: "{requests}" + + - beans: + - kafka.network:type=RequestMetrics,name=TotalTimeMs,request=Produce + - kafka.network:type=RequestMetrics,name=TotalTimeMs,request=FetchConsumer + - kafka.network:type=RequestMetrics,name=TotalTimeMs,request=FetchFollower + metricAttribute: + type: param(request) + unit: ms + mapping: + Count: + metric: kafka.request.time.total + type: counter + desc: The total time the broker has taken to service requests + 50thPercentile: + metric: kafka.request.time.50p + type: gauge + desc: The 50th percentile time the broker has taken to service requests + 99thPercentile: + metric: kafka.request.time.99p + type: gauge + desc: The 99th percentile time the broker has taken to service requests + + - bean: kafka.network:type=RequestChannel,name=RequestQueueSize + mapping: + Value: + metric: kafka.request.queue + type: updowncounter + desc: Size of the request queue + unit: "{requests}" + + - bean: kafka.server:type=BrokerTopicMetrics,name=BytesInPerSec + metricAttribute: + direction: const(in) + mapping: + Count: + metric: kafka.network.io + type: counter + desc: The bytes received or sent by the broker + unit: By + + - bean: kafka.server:type=BrokerTopicMetrics,name=BytesOutPerSec + metricAttribute: + direction: const(out) + mapping: + Count: + metric: kafka.network.io + type: counter + desc: The bytes received or sent by the broker + unit: By + + - beans: + - kafka.server:type=DelayedOperationPurgatory,name=PurgatorySize,delayedOperation=Produce + - kafka.server:type=DelayedOperationPurgatory,name=PurgatorySize,delayedOperation=Fetch + metricAttribute: + type: param(delayedOperation) + mapping: + Value: + metric: kafka.purgatory.size + type: updowncounter + desc: The number of requests waiting in purgatory + unit: "{requests}" + + - bean: kafka.server:type=ReplicaManager,name=PartitionCount + mapping: + Value: + metric: kafka.partition.count + type: updowncounter + desc: The number of partitions on the broker + unit: "{partitions}" + + - bean: kafka.controller:type=KafkaController,name=OfflinePartitionsCount + mapping: + Value: + metric: kafka.partition.offline + type: updowncounter + desc: The number of partitions offline + unit: "{partitions}" + + - bean: kafka.server:type=ReplicaManager,name=UnderReplicatedPartitions + mapping: + Value: + metric: kafka.partition.underReplicated + type: updowncounter + desc: The number of under replicated partitions + unit: "{partitions}" + + - bean: kafka.server:type=ReplicaManager,name=IsrShrinksPerSec + metricAttribute: + operation: const(shrink) + mapping: + Count: + metric: kafka.isr.operation.count + type: updowncounter + desc: The number of in-sync replica shrink and expand operations + unit: "{operations}" + + - bean: kafka.server:type=ReplicaManager,name=IsrExpandsPerSec + metricAttribute: + operation: const(expand) + mapping: + Count: + metric: kafka.isr.operation.count + type: updowncounter + desc: The number of in-sync replica shrink and expand operations + unit: "{operations}" + + - bean: kafka.server:type=ReplicaFetcherManager,name=MaxLag,clientId=Replica + mapping: + Value: + metric: kafka.lag.max + desc: The max lag in messages between follower and leader replicas + unit: "{messages}" + + - bean: kafka.controller:type=KafkaController,name=ActiveControllerCount + mapping: + Value: + metric: kafka.controller.active.count + type: updowncounter + desc: The number of controllers active on the broker + unit: "{controllers}" + + - bean: kafka.controller:type=ControllerStats,name=LeaderElectionRateAndTimeMs + mapping: + Count: + metric: kafka.leaderElection.count + type: counter + desc: The leader election count + unit: "{elections}" + + - bean: kafka.controller:type=ControllerStats,name=UncleanLeaderElectionsPerSec + mapping: + Count: + metric: kafka.leaderElection.unclean.count + type: counter + desc: Unclean leader election count - increasing indicates broker failures + unit: "{elections}" + + # Log metrics + + - bean: kafka.log:type=LogFlushStats,name=LogFlushRateAndTimeMs + unit: ms + type: gauge + prefix: kafka.logs.flush. + mapping: + Count: + type: counter + desc: Log flush count + 50thPercentile: + metric: time.50p + desc: Log flush time - 50th percentile + 99thPercentile: + metric: time.99p + desc: Log flush time - 99th percentile diff --git a/instrumentation/jmx-metrics/javaagent/src/main/resources/jmx/rules/tomcat.yaml b/instrumentation/jmx-metrics/javaagent/src/main/resources/jmx/rules/tomcat.yaml index 9a04bada0088..8d0f7f8835a5 100644 --- a/instrumentation/jmx-metrics/javaagent/src/main/resources/jmx/rules/tomcat.yaml +++ b/instrumentation/jmx-metrics/javaagent/src/main/resources/jmx/rules/tomcat.yaml @@ -1,67 +1,138 @@ --- +# For Tomcat, the default JMX domain is "Catalina:", however with some deployments like embedded in spring-boot +# we can have the "Tomcat:" domain used, thus we have to duplicate metrics definitions as using a wildcard +# would match too broadly + rules: - - bean: Catalina:type=GlobalRequestProcessor,name=* - unit: "1" - prefix: http.server.tomcat. - metricAttribute: - name: param(name) - mapping: - errorCount: - metric: errorCount - type: gauge - desc: The number of errors per second on all request processors - requestCount: - metric: requestCount - type: gauge - desc: The number of requests per second across all request processors - maxTime: - metric: maxTime - type: gauge - unit: ms - desc: The longest request processing time - processingTime: - metric: processingTime - type: counter - unit: ms - desc: Total time for processing all requests - bytesReceived: - metric: traffic - type: counter - unit: By - desc: The number of bytes transmitted - metricAttribute: - direction: const(received) - bytesSent: - metric: traffic - type: counter - unit: By - desc: The number of bytes transmitted - metricAttribute: - direction: const(sent) - - bean: Catalina:type=Manager,host=localhost,context=* - unit: "1" - prefix: http.server.tomcat. - type: updowncounter - metricAttribute: - context: param(context) - mapping: - activeSessions: - metric: sessions.activeSessions - desc: The number of active sessions - - bean: Catalina:type=ThreadPool,name=* - unit: '{threads}' - prefix: http.server.tomcat. - type: updowncounter - metricAttribute: - name: param(name) - mapping: - currentThreadCount: - metric: threads - desc: Thread Count of the Thread Pool - metricAttribute: - state: const(idle) - currentThreadsBusy: - metric: threads - desc: Thread Count of the Thread Pool - metricAttribute: - state: const(busy) + - bean: Catalina:type=GlobalRequestProcessor,name=* + unit: "1" + prefix: http.server.tomcat. + metricAttribute: + name: param(name) + mapping: + errorCount: + metric: errorCount + type: gauge + desc: The number of errors per second on all request processors + requestCount: + metric: requestCount + type: gauge + desc: The number of requests per second across all request processors + maxTime: + metric: maxTime + type: gauge + unit: ms + desc: The longest request processing time + processingTime: + metric: processingTime + type: counter + unit: ms + desc: Total time for processing all requests + bytesReceived: + metric: traffic + type: counter + unit: By + desc: The number of bytes transmitted + metricAttribute: + direction: const(received) + bytesSent: + metric: traffic + type: counter + unit: By + desc: The number of bytes transmitted + metricAttribute: + direction: const(sent) + - bean: Tomcat:type=GlobalRequestProcessor,name=* + unit: "1" + prefix: http.server.tomcat. + metricAttribute: + name: param(name) + mapping: + errorCount: + metric: errorCount + type: gauge + desc: The number of errors per second on all request processors + requestCount: + metric: requestCount + type: gauge + desc: The number of requests per second across all request processors + maxTime: + metric: maxTime + type: gauge + unit: ms + desc: The longest request processing time + processingTime: + metric: processingTime + type: counter + unit: ms + desc: Total time for processing all requests + bytesReceived: + metric: traffic + type: counter + unit: By + desc: The number of bytes transmitted + metricAttribute: + direction: const(received) + bytesSent: + metric: traffic + type: counter + unit: By + desc: The number of bytes transmitted + metricAttribute: + direction: const(sent) + + - bean: Catalina:type=Manager,host=localhost,context=* + unit: "1" + prefix: http.server.tomcat. + type: updowncounter + metricAttribute: + context: param(context) + mapping: + activeSessions: + metric: sessions.activeSessions + desc: The number of active sessions + - bean: Tomcat:type=Manager,host=localhost,context=* + unit: "1" + prefix: http.server.tomcat. + type: updowncounter + metricAttribute: + context: param(context) + mapping: + activeSessions: + metric: sessions.activeSessions + desc: The number of active sessions + + - bean: Catalina:type=ThreadPool,name=* + unit: "{threads}" + prefix: http.server.tomcat. + type: updowncounter + metricAttribute: + name: param(name) + mapping: + currentThreadCount: + metric: threads + desc: Thread Count of the Thread Pool + metricAttribute: + state: const(idle) + currentThreadsBusy: + metric: threads + desc: Thread Count of the Thread Pool + metricAttribute: + state: const(busy) + - bean: Tomcat:type=ThreadPool,name=* + unit: "{threads}" + prefix: http.server.tomcat. + type: updowncounter + metricAttribute: + name: param(name) + mapping: + currentThreadCount: + metric: threads + desc: Thread Count of the Thread Pool + metricAttribute: + state: const(idle) + currentThreadsBusy: + metric: threads + desc: Thread Count of the Thread Pool + metricAttribute: + state: const(busy) diff --git a/instrumentation/jmx-metrics/javaagent/src/main/resources/jmx/rules/wildfly.yaml b/instrumentation/jmx-metrics/javaagent/src/main/resources/jmx/rules/wildfly.yaml index 118f6bb924bc..b6d4c543257b 100644 --- a/instrumentation/jmx-metrics/javaagent/src/main/resources/jmx/rules/wildfly.yaml +++ b/instrumentation/jmx-metrics/javaagent/src/main/resources/jmx/rules/wildfly.yaml @@ -1,83 +1,83 @@ --- rules: - - bean: jboss.as:deployment=*,subsystem=undertow - metricAttribute: - deployment: param(deployment) - prefix: wildfly.session. - type: counter - unit: "1" - mapping: - sessionsCreated: - activeSessions: - type: updowncounter - expiredSessions: - rejectedSessions: - - bean: jboss.as:subsystem=undertow,server=*,http-listener=* - metricAttribute: - server: param(server) - listener: param(http-listener) - prefix: wildfly.request. - type: counter - unit: "1" - mapping: - requestCount: - processingTime: - unit: ns - errorCount: - - bean: jboss.as:subsystem=undertow,server=*,http-listener=* - metricAttribute: - server: param(server) - listener: param(http-listener) - type: counter - unit: By - mapping: - bytesSent: - metric: wildfly.network.io - desc: Total number of bytes transferred - metricAttribute: - direction: const(out) - bytesReceived: - metric: wildfly.network.io - desc: Total number of bytes transferred - metricAttribute: - direction: const(in) - - bean: jboss.as:subsystem=datasources,data-source=*,statistics=pool - unit: "1" - metricAttribute: - data_source: param(data-source) - mapping: - ActiveCount: - metric: wildfly.db.client.connections.usage - metricAttribute: - state: const(used) - desc: The number of open jdbc connections - IdleCount: - metric: wildfly.db.client.connections.usage - metricAttribute: - state: const(idle) - desc: The number of open jdbc connections - WaitCount: - metric: wildfly.db.client.connections.WaitCount - type: counter - - bean: jboss.as:subsystem=transactions - type: counter - prefix: wildfly.db.client. - unit: "{transactions}" - mapping: - numberOfTransactions: - metric: transaction.NumberOfTransactions - numberOfApplicationRollbacks: - metric: rollback.count - metricAttribute: - cause: const(application) - desc: The total number of transactions rolled back - numberOfResourceRollbacks: - metric: rollback.count - metricAttribute: - cause: const(resource) - desc: The total number of transactions rolled back - numberOfSystemRollbacks: - metric: rollback.count - metricAttribute: - cause: const(system) - desc: The total number of transactions rolled back + - bean: jboss.as:deployment=*,subsystem=undertow + metricAttribute: + deployment: param(deployment) + prefix: wildfly.session. + type: counter + unit: "1" + mapping: + sessionsCreated: + activeSessions: + type: updowncounter + expiredSessions: + rejectedSessions: + - bean: jboss.as:subsystem=undertow,server=*,http-listener=* + metricAttribute: + server: param(server) + listener: param(http-listener) + prefix: wildfly.request. + type: counter + unit: "1" + mapping: + requestCount: + processingTime: + unit: ns + errorCount: + - bean: jboss.as:subsystem=undertow,server=*,http-listener=* + metricAttribute: + server: param(server) + listener: param(http-listener) + type: counter + unit: By + mapping: + bytesSent: + metric: wildfly.network.io + desc: Total number of bytes transferred + metricAttribute: + direction: const(out) + bytesReceived: + metric: wildfly.network.io + desc: Total number of bytes transferred + metricAttribute: + direction: const(in) + - bean: jboss.as:subsystem=datasources,data-source=*,statistics=pool + unit: "1" + metricAttribute: + data_source: param(data-source) + mapping: + ActiveCount: + metric: wildfly.db.client.connections.usage + metricAttribute: + state: const(used) + desc: The number of open jdbc connections + IdleCount: + metric: wildfly.db.client.connections.usage + metricAttribute: + state: const(idle) + desc: The number of open jdbc connections + WaitCount: + metric: wildfly.db.client.connections.WaitCount + type: counter + - bean: jboss.as:subsystem=transactions + type: counter + prefix: wildfly.db.client. + unit: "{transactions}" + mapping: + numberOfTransactions: + metric: transaction.NumberOfTransactions + numberOfApplicationRollbacks: + metric: rollback.count + metricAttribute: + cause: const(application) + desc: The total number of transactions rolled back + numberOfResourceRollbacks: + metric: rollback.count + metricAttribute: + cause: const(resource) + desc: The total number of transactions rolled back + numberOfSystemRollbacks: + metric: rollback.count + metricAttribute: + cause: const(system) + desc: The total number of transactions rolled back diff --git a/instrumentation/jmx-metrics/javaagent/src/test/java/io/opentelemetry/instrumentation/javaagent/jmx/JmxMetricInsightInstallerTest.java b/instrumentation/jmx-metrics/javaagent/src/test/java/io/opentelemetry/instrumentation/javaagent/jmx/JmxMetricInsightInstallerTest.java index 0853505ae98b..253583660ddd 100644 --- a/instrumentation/jmx-metrics/javaagent/src/test/java/io/opentelemetry/instrumentation/javaagent/jmx/JmxMetricInsightInstallerTest.java +++ b/instrumentation/jmx-metrics/javaagent/src/test/java/io/opentelemetry/instrumentation/javaagent/jmx/JmxMetricInsightInstallerTest.java @@ -28,6 +28,7 @@ class JmxMetricInsightInstallerTest { new HashSet<>( Arrays.asList( "activemq.yaml", + "camel.yaml", "hadoop.yaml", "jetty.yaml", "kafka-broker.yaml", diff --git a/instrumentation/jmx-metrics/javaagent/tomcat.md b/instrumentation/jmx-metrics/javaagent/tomcat.md index a2ea859d8077..6ac2e3fea16e 100644 --- a/instrumentation/jmx-metrics/javaagent/tomcat.md +++ b/instrumentation/jmx-metrics/javaagent/tomcat.md @@ -3,7 +3,7 @@ Here is the list of metrics based on MBeans exposed by Tomcat. | Metric Name | Type | Attributes | Description | -|--------------------------------------------|---------------|-----------------|-----------------------------------------------------------------| +| ------------------------------------------ | ------------- | --------------- | --------------------------------------------------------------- | | http.server.tomcat.sessions.activeSessions | UpDownCounter | context | The number of active sessions | | http.server.tomcat.errorCount | Gauge | name | The number of errors per second on all request processors | | http.server.tomcat.requestCount | Gauge | name | The number of requests per second across all request processors | diff --git a/instrumentation/jmx-metrics/javaagent/wildfly.md b/instrumentation/jmx-metrics/javaagent/wildfly.md index c88eee63be0f..637453a4ee33 100644 --- a/instrumentation/jmx-metrics/javaagent/wildfly.md +++ b/instrumentation/jmx-metrics/javaagent/wildfly.md @@ -3,7 +3,7 @@ Here is the list of metrics based on MBeans exposed by Wildfly. | Metric Name | Type | Attributes | Description | -|----------------------------------------------------|---------------|--------------------|-------------------------------------------------------------------------| +| -------------------------------------------------- | ------------- | ------------------ | ----------------------------------------------------------------------- | | wildfly.network.io | Counter | direction, server | Total number of bytes transferred | | wildfly.request.errorCount | Counter | server, listener | The number of 500 responses that have been sent by this listener | | wildfly.request.requestCount | Counter | server, listener | The number of requests this listener has served | diff --git a/instrumentation/jmx-metrics/library/src/main/java/io/opentelemetry/instrumentation/jmx/engine/BeanFinder.java b/instrumentation/jmx-metrics/library/src/main/java/io/opentelemetry/instrumentation/jmx/engine/BeanFinder.java index fa44f293cc4e..b9856f1dc98c 100644 --- a/instrumentation/jmx-metrics/library/src/main/java/io/opentelemetry/instrumentation/jmx/engine/BeanFinder.java +++ b/instrumentation/jmx-metrics/library/src/main/java/io/opentelemetry/instrumentation/jmx/engine/BeanFinder.java @@ -5,6 +5,7 @@ package io.opentelemetry.instrumentation.jmx.engine; +import java.lang.management.ManagementFactory; import java.util.ArrayList; import java.util.HashSet; import java.util.List; @@ -24,7 +25,13 @@ class BeanFinder { private final MetricRegistrar registrar; private MetricConfiguration conf; - private final ScheduledExecutorService exec = Executors.newSingleThreadScheduledExecutor(); + private final ScheduledExecutorService exec = + Executors.newSingleThreadScheduledExecutor( + runnable -> { + Thread result = new Thread(runnable, "jmx_bean_finder"); + result.setDaemon(true); + return result; + }); private final long discoveryDelay; private final long maxDelay; private long delay = 1000; // number of milliseconds until first attempt to discover MBeans @@ -38,6 +45,19 @@ class BeanFinder { void discoverBeans(MetricConfiguration conf) { this.conf = conf; + exec.schedule( + () -> { + // Issue 9336: Corner case: PlatformMBeanServer will remain unitialized until a direct + // reference to it is made. This call makes sure that the PlatformMBeanServer will be in + // the set of MBeanServers reported by MBeanServerFactory. + // Issue 11143: This call initializes java.util.logging.LogManager. We should not call it + // before application has had a chance to configure custom log manager. This is needed for + // wildfly. + ManagementFactory.getPlatformMBeanServer(); + }, + discoveryDelay, + TimeUnit.MILLISECONDS); + exec.schedule( new Runnable() { @Override diff --git a/instrumentation/jodd-http-4.2/javaagent-unit-tests/build.gradle.kts b/instrumentation/jodd-http-4.2/javaagent-unit-tests/build.gradle.kts new file mode 100644 index 000000000000..3dcb530145ec --- /dev/null +++ b/instrumentation/jodd-http-4.2/javaagent-unit-tests/build.gradle.kts @@ -0,0 +1,10 @@ +plugins { + id("otel.java-conventions") +} + +dependencies { + testImplementation("org.jodd:jodd-http:4.2.0") + testImplementation(project(":instrumentation:jodd-http-4.2:javaagent")) + testImplementation(project(":instrumentation-api")) + testImplementation(project(":instrumentation-api-incubator")) +} diff --git a/instrumentation/jodd-http-4.2/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/joddhttp/v4_2/JoddHttpHttpAttributesGetterTest.java b/instrumentation/jodd-http-4.2/javaagent-unit-tests/src/test/java/io/opentelemetry/javaagent/instrumentation/joddhttp/v4_2/JoddHttpHttpAttributesGetterTest.java similarity index 100% rename from instrumentation/jodd-http-4.2/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/joddhttp/v4_2/JoddHttpHttpAttributesGetterTest.java rename to instrumentation/jodd-http-4.2/javaagent-unit-tests/src/test/java/io/opentelemetry/javaagent/instrumentation/joddhttp/v4_2/JoddHttpHttpAttributesGetterTest.java diff --git a/instrumentation/jodd-http-4.2/javaagent/build.gradle.kts b/instrumentation/jodd-http-4.2/javaagent/build.gradle.kts index e091128cbdbd..dfb5e0287dcc 100644 --- a/instrumentation/jodd-http-4.2/javaagent/build.gradle.kts +++ b/instrumentation/jodd-http-4.2/javaagent/build.gradle.kts @@ -15,5 +15,5 @@ dependencies { library("org.jodd:jodd-http:4.2.0") testImplementation(project(":instrumentation:jodd-http-4.2:javaagent")) - testImplementation(project(":instrumentation-api-semconv")) + testImplementation(project(":instrumentation-api-incubator")) } diff --git a/instrumentation/jodd-http-4.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/joddhttp/v4_2/JoddHttpHttpAttributesGetter.java b/instrumentation/jodd-http-4.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/joddhttp/v4_2/JoddHttpHttpAttributesGetter.java index 97e5d4fc04b4..1bbb1a67af46 100644 --- a/instrumentation/jodd-http-4.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/joddhttp/v4_2/JoddHttpHttpAttributesGetter.java +++ b/instrumentation/jodd-http-4.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/joddhttp/v4_2/JoddHttpHttpAttributesGetter.java @@ -5,7 +5,7 @@ package io.opentelemetry.javaagent.instrumentation.joddhttp.v4_2; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpClientAttributesGetter; +import io.opentelemetry.instrumentation.api.semconv.http.HttpClientAttributesGetter; import java.util.List; import javax.annotation.Nullable; import jodd.http.HttpRequest; @@ -40,4 +40,35 @@ public List getHttpResponseHeader( HttpRequest request, HttpResponse response, String name) { return response.headers(name); } + + @Override + public String getNetworkProtocolName(HttpRequest request, @Nullable HttpResponse response) { + return "http"; + } + + @Nullable + @Override + public String getNetworkProtocolVersion(HttpRequest request, @Nullable HttpResponse response) { + String httpVersion = request.httpVersion(); + if (httpVersion == null && response != null) { + httpVersion = response.httpVersion(); + } + if (httpVersion != null) { + if (httpVersion.contains("/")) { + httpVersion = httpVersion.substring(httpVersion.lastIndexOf("/") + 1); + } + } + return httpVersion; + } + + @Override + @Nullable + public String getServerAddress(HttpRequest request) { + return request.host(); + } + + @Override + public Integer getServerPort(HttpRequest request) { + return request.port(); + } } diff --git a/instrumentation/jodd-http-4.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/joddhttp/v4_2/JoddHttpNetAttributesGetter.java b/instrumentation/jodd-http-4.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/joddhttp/v4_2/JoddHttpNetAttributesGetter.java deleted file mode 100644 index 8d9839e36e33..000000000000 --- a/instrumentation/jodd-http-4.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/joddhttp/v4_2/JoddHttpNetAttributesGetter.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.javaagent.instrumentation.joddhttp.v4_2; - -import io.opentelemetry.instrumentation.api.instrumenter.net.NetClientAttributesGetter; -import javax.annotation.Nullable; -import jodd.http.HttpRequest; -import jodd.http.HttpResponse; - -final class JoddHttpNetAttributesGetter - implements NetClientAttributesGetter { - - @Override - public String getNetworkProtocolName(HttpRequest request, @Nullable HttpResponse response) { - return "http"; - } - - @Nullable - @Override - public String getNetworkProtocolVersion(HttpRequest request, @Nullable HttpResponse response) { - String httpVersion = request.httpVersion(); - if (httpVersion == null && response != null) { - httpVersion = response.httpVersion(); - } - if (httpVersion != null) { - if (httpVersion.contains("/")) { - httpVersion = httpVersion.substring(httpVersion.lastIndexOf("/") + 1); - } - } - return httpVersion; - } - - @Override - @Nullable - public String getServerAddress(HttpRequest request) { - return request.host(); - } - - @Override - public Integer getServerPort(HttpRequest request) { - return request.port(); - } -} diff --git a/instrumentation/jodd-http-4.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/joddhttp/v4_2/JoddHttpSingletons.java b/instrumentation/jodd-http-4.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/joddhttp/v4_2/JoddHttpSingletons.java index 639620e179ac..9bc4838f4b1f 100644 --- a/instrumentation/jodd-http-4.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/joddhttp/v4_2/JoddHttpSingletons.java +++ b/instrumentation/jodd-http-4.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/joddhttp/v4_2/JoddHttpSingletons.java @@ -5,14 +5,8 @@ package io.opentelemetry.javaagent.instrumentation.joddhttp.v4_2; -import io.opentelemetry.api.GlobalOpenTelemetry; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpClientAttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpClientMetrics; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpSpanNameExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpSpanStatusExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.net.PeerServiceAttributesExtractor; -import io.opentelemetry.javaagent.bootstrap.internal.CommonConfig; +import io.opentelemetry.javaagent.bootstrap.internal.JavaagentHttpClientInstrumenters; import jodd.http.HttpRequest; import jodd.http.HttpResponse; @@ -22,25 +16,9 @@ public final class JoddHttpSingletons { private static final Instrumenter INSTRUMENTER; static { - JoddHttpHttpAttributesGetter httpAttributesGetter = new JoddHttpHttpAttributesGetter(); - JoddHttpNetAttributesGetter netAttributesGetter = new JoddHttpNetAttributesGetter(); - INSTRUMENTER = - Instrumenter.builder( - GlobalOpenTelemetry.get(), - INSTRUMENTATION_NAME, - HttpSpanNameExtractor.create(httpAttributesGetter)) - .setSpanStatusExtractor(HttpSpanStatusExtractor.create(httpAttributesGetter)) - .addAttributesExtractor( - HttpClientAttributesExtractor.builder(httpAttributesGetter, netAttributesGetter) - .setCapturedRequestHeaders(CommonConfig.get().getClientRequestHeaders()) - .setCapturedResponseHeaders(CommonConfig.get().getClientResponseHeaders()) - .build()) - .addAttributesExtractor( - PeerServiceAttributesExtractor.create( - netAttributesGetter, CommonConfig.get().getPeerServiceMapping())) - .addOperationMetrics(HttpClientMetrics.get()) - .buildClientInstrumenter(HttpHeaderSetter.INSTANCE); + JavaagentHttpClientInstrumenters.create( + INSTRUMENTATION_NAME, new JoddHttpHttpAttributesGetter(), HttpHeaderSetter.INSTANCE); } public static Instrumenter instrumenter() { diff --git a/instrumentation/jodd-http-4.2/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/joddhttp/v4_2/JoddHttpTest.java b/instrumentation/jodd-http-4.2/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/joddhttp/v4_2/JoddHttpTest.java index ccf4445f4921..45b0f34858ec 100644 --- a/instrumentation/jodd-http-4.2/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/joddhttp/v4_2/JoddHttpTest.java +++ b/instrumentation/jodd-http-4.2/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/joddhttp/v4_2/JoddHttpTest.java @@ -16,8 +16,6 @@ public class JoddHttpTest extends AbstractHttpClientTest { - private static final String USER_AGENT = "Jodd HTTP"; - @RegisterExtension static final InstrumentationExtension testing = HttpClientInstrumentationExtension.forAgent(); @@ -30,7 +28,7 @@ public HttpRequest buildRequest(String method, URI uri, Map head .followRedirects(true) .connectionKeepAlive(true) .connectionTimeout((int) CONNECTION_TIMEOUT.toMillis()) - .header("user-agent", USER_AGENT); + .header("user-agent", "Jodd HTTP"); for (Map.Entry header : headers.entrySet()) { request.headerOverwrite(header.getKey(), header.getValue()); } @@ -54,6 +52,6 @@ protected void configure(HttpClientTestOptions.Builder optionsBuilder) { optionsBuilder.disableTestCallback(); // Circular Redirects are not explicitly handled by jodd-http optionsBuilder.disableTestCircularRedirects(); - optionsBuilder.setUserAgent(USER_AGENT); + optionsBuilder.spanEndsAfterBody(); } } diff --git a/instrumentation/jsf/jsf-jakarta-common/testing/src/main/groovy/BaseJsfTest.groovy b/instrumentation/jsf/jsf-jakarta-common/testing/src/main/groovy/BaseJsfTest.groovy deleted file mode 100644 index dd5a67b6b372..000000000000 --- a/instrumentation/jsf/jsf-jakarta-common/testing/src/main/groovy/BaseJsfTest.groovy +++ /dev/null @@ -1,263 +0,0 @@ -/* - * 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.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.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.server.Server -import org.eclipse.jetty.util.resource.Resource -import org.eclipse.jetty.webapp.WebAppContext -import org.jsoup.Jsoup -import spock.lang.Unroll - -import static io.opentelemetry.api.trace.SpanKind.INTERNAL -import static io.opentelemetry.api.trace.StatusCode.ERROR - -abstract class BaseJsfTest extends AgentInstrumentationSpecification implements HttpServerTestTrait { - - def setupSpec() { - setupServer() - } - - def cleanupSpec() { - cleanupServer() - } - - @Override - Server startServer(int port) { - WebAppContext webAppContext = new WebAppContext() - webAppContext.setContextPath(getContextPath()) - // set up test application - webAppContext.setBaseResource(Resource.newSystemResource("test-app")) - - Resource extraResource = Resource.newSystemResource("test-app-extra") - if (extraResource != null) { - webAppContext.getMetaData().addWebInfResource(extraResource) - } - - 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" - } - - @Unroll - def "test #path"() { - setup: - AggregatedHttpResponse response = client.get(address.resolve(path).toString()).aggregate().join() - - expect: - response.status().code() == 200 - response.contentUtf8().trim() == "Hello" - - and: - assertTraces(1) { - trace(0, 1) { - span(0) { - name getContextPath() + "/hello.xhtml" - kind SpanKind.SERVER - hasNoParent() - attributes { - "net.protocol.name" "http" - "net.protocol.version" "1.1" - "$SemanticAttributes.NET_HOST_NAME" "localhost" - "$SemanticAttributes.NET_HOST_PORT" port - "$SemanticAttributes.NET_SOCK_PEER_ADDR" "127.0.0.1" - "$SemanticAttributes.NET_SOCK_PEER_PORT" Long - "$SemanticAttributes.NET_SOCK_HOST_ADDR" "127.0.0.1" - "$SemanticAttributes.HTTP_METHOD" "GET" - "$SemanticAttributes.HTTP_SCHEME" "http" - "$SemanticAttributes.HTTP_TARGET" "/jetty-context/" + path - "$SemanticAttributes.USER_AGENT_ORIGINAL" TEST_USER_AGENT - "$SemanticAttributes.HTTP_STATUS_CODE" 200 - "$SemanticAttributes.HTTP_ROUTE" "/jetty-context/" + route - "$SemanticAttributes.HTTP_CLIENT_IP" { it == null || it == TEST_CLIENT_IP } - } - } - } - } - - where: - path | route - "hello.xhtml" | "*.xhtml" - "faces/hello.xhtml" | "faces/*" - } - - def "test greeting"() { - // we need to display the page first before posting data to it - setup: - AggregatedHttpResponse response = client.get(address.resolve("greeting.xhtml").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) { - span(0) { - name getContextPath() + "/greeting.xhtml" - kind SpanKind.SERVER - hasNoParent() - } - } - } - clearExportedData() - - when: - // extract parameters needed to post back form - def viewState = doc.selectFirst("[name=jakarta.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("jakarta.faces.ViewState", viewState) - .build() - // use the session created for first request - def request2 = AggregatedHttpRequest.of( - RequestHeaders.builder(HttpMethod.POST, address.resolve("greeting.xhtml;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) { - span(0) { - name getContextPath() + "/greeting.xhtml" - kind SpanKind.SERVER - hasNoParent() - } - 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.xhtml").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) { - span(0) { - name getContextPath() + "/greeting.xhtml" - kind SpanKind.SERVER - hasNoParent() - } - } - } - clearExportedData() - - when: - // extract parameters needed to post back form - def viewState = doc.selectFirst("[name=jakarta.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("jakarta.faces.ViewState", viewState) - .build() - // use the session created for first request - def request2 = AggregatedHttpRequest.of( - RequestHeaders.builder(HttpMethod.POST, address.resolve("greeting.xhtml;jsessionid=" + jsessionid).toString()) - .contentType(MediaType.FORM_DATA) - .build(), - HttpData.ofUtf8(formBody.toQueryString())) - AggregatedHttpResponse response2 = client.execute(request2).aggregate().join() - - then: - response2.status().code() == 500 - def ex = new Exception("submit exception") - - and: - assertTraces(1) { - trace(0, 2) { - span(0) { - name getContextPath() + "/greeting.xhtml" - kind SpanKind.SERVER - hasNoParent() - status ERROR - errorEvent(ex.class, ex.message) - } - handlerSpan(it, 1, span(0), "#{greetingForm.submit()}", ex) - } - } - } - - 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/instrumentation/jsf/jsf-jakarta-common/testing/src/main/groovy/ExceptionFilter.groovy b/instrumentation/jsf/jsf-jakarta-common/testing/src/main/groovy/ExceptionFilter.groovy deleted file mode 100644 index 40aa7ac24eaa..000000000000 --- a/instrumentation/jsf/jsf-jakarta-common/testing/src/main/groovy/ExceptionFilter.groovy +++ /dev/null @@ -1,38 +0,0 @@ -/* - * 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 - -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/instrumentation/jsf/jsf-jakarta-common/testing/src/main/groovy/GreetingForm.groovy b/instrumentation/jsf/jsf-jakarta-common/testing/src/main/groovy/GreetingForm.groovy deleted file mode 100644 index f8d429f4ee2a..000000000000 --- a/instrumentation/jsf/jsf-jakarta-common/testing/src/main/groovy/GreetingForm.groovy +++ /dev/null @@ -1,29 +0,0 @@ -/* - * 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/instrumentation/jsf/jsf-jakarta-common/testing/src/main/java/io/opentelemetry/javaagent/instrumentation/jsf/jakarta/BaseJsfTest.java b/instrumentation/jsf/jsf-jakarta-common/testing/src/main/java/io/opentelemetry/javaagent/instrumentation/jsf/jakarta/BaseJsfTest.java new file mode 100644 index 000000000000..d375a64b6e12 --- /dev/null +++ b/instrumentation/jsf/jsf-jakarta-common/testing/src/main/java/io/opentelemetry/javaagent/instrumentation/jsf/jakarta/BaseJsfTest.java @@ -0,0 +1,287 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jsf.jakarta; + +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.satisfies; +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpServerUsingTest; +import io.opentelemetry.instrumentation.testing.junit.http.HttpServerInstrumentationExtension; +import io.opentelemetry.sdk.testing.assertj.SpanDataAssert; +import io.opentelemetry.sdk.testing.assertj.TraceAssert; +import io.opentelemetry.sdk.trace.data.StatusData; +import io.opentelemetry.semconv.ClientAttributes; +import io.opentelemetry.semconv.HttpAttributes; +import io.opentelemetry.semconv.NetworkAttributes; +import io.opentelemetry.semconv.ServerAttributes; +import io.opentelemetry.semconv.UrlAttributes; +import io.opentelemetry.semconv.UserAgentAttributes; +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 java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.function.Consumer; +import java.util.stream.Stream; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.util.resource.Resource; +import org.eclipse.jetty.webapp.WebAppContext; +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.RegisterExtension; +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; + +public abstract class BaseJsfTest extends AbstractHttpServerUsingTest { + + @RegisterExtension + public static final InstrumentationExtension testing = + HttpServerInstrumentationExtension.forAgent(); + + @BeforeAll + protected void setUp() { + startServer(); + } + + @AfterAll + protected void cleanUp() { + cleanupServer(); + } + + @Override + protected Server setupServer() throws Exception { + WebAppContext webAppContext = new WebAppContext(); + webAppContext.setContextPath(getContextPath()); + // set up test application + webAppContext.setBaseResource(Resource.newSystemResource("test-app")); + + Resource extraResource = Resource.newSystemResource("test-app-extra"); + if (extraResource != null) { + webAppContext.getMetaData().addWebInfResource(extraResource); + } + + Server jettyServer = new Server(port); + jettyServer.setHandler(webAppContext); + jettyServer.start(); + + return jettyServer; + } + + @Override + protected void stopServer(Server server) throws Exception { + server.stop(); + server.destroy(); + } + + @Override + protected String getContextPath() { + return "/jetty-context"; + } + + @ParameterizedTest + @ArgumentsSource(PathTestArgs.class) + void testPath(String path, String route) { + AggregatedHttpResponse response = + client.get(address.resolve(path).toString()).aggregate().join(); + assertThat(response.status().code()).isEqualTo(200); + assertThat(response.contentUtf8().trim()).isEqualTo("Hello"); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName(getContextPath() + "/hello.xhtml") + .hasKind(SpanKind.SERVER) + .hasAttributesSatisfyingExactly( + equalTo(NetworkAttributes.NETWORK_PROTOCOL_VERSION, "1.1"), + equalTo(ServerAttributes.SERVER_ADDRESS, "localhost"), + equalTo(ServerAttributes.SERVER_PORT, port), + equalTo(NetworkAttributes.NETWORK_PEER_ADDRESS, "127.0.0.1"), + satisfies( + NetworkAttributes.NETWORK_PEER_PORT, + val -> val.isInstanceOf(Long.class)), + equalTo(HttpAttributes.HTTP_REQUEST_METHOD, "GET"), + equalTo(UrlAttributes.URL_SCHEME, "http"), + equalTo(UrlAttributes.URL_PATH, getContextPath() + "/" + path), + equalTo(UserAgentAttributes.USER_AGENT_ORIGINAL, TEST_USER_AGENT), + equalTo(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, 200), + equalTo(HttpAttributes.HTTP_ROUTE, getContextPath() + "/" + route), + satisfies( + ClientAttributes.CLIENT_ADDRESS, + val -> + val.satisfiesAnyOf( + v -> assertThat(v).isEqualTo(TEST_CLIENT_IP), + v -> assertThat(v).isNull()))))); + } + + static class PathTestArgs implements ArgumentsProvider { + @Override + public Stream provideArguments(ExtensionContext context) { + return Stream.of( + Arguments.of("hello.xhtml", "*.xhtml"), Arguments.of("faces/hello.xhtml", "faces/*")); + } + } + + @Test + void testGreeting() { + AggregatedHttpResponse response = + client.get(address.resolve("greeting.xhtml").toString()).aggregate().join(); + Document doc = Jsoup.parse(response.contentUtf8()); + + assertThat(response.status().code()).isEqualTo(200); + assertThat(doc.selectFirst("title").text()).isEqualTo("Hello, World!"); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName(getContextPath() + "/greeting.xhtml") + .hasKind(SpanKind.SERVER) + .hasNoParent())); + + testing.clearData(); + + String viewState = doc.selectFirst("[name=jakarta.faces.ViewState]").val(); + String formAction = doc.selectFirst("#app-form").attr("action"); + String jsessionid = + formAction.substring(formAction.indexOf("jsessionid=") + "jsessionid=".length()); + + assertThat(viewState).isNotNull(); + assertThat(jsessionid).isNotNull(); + + // 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("jakarta.faces.ViewState", viewState) + .build(); + + // use the session created for first request + AggregatedHttpRequest request2 = + AggregatedHttpRequest.of( + RequestHeaders.builder( + HttpMethod.POST, + address.resolve("greeting.xhtml;jsessionid=" + jsessionid).toString()) + .contentType(MediaType.FORM_DATA) + .build(), + HttpData.ofUtf8(formBody.toQueryString())); + AggregatedHttpResponse response2 = client.execute(request2).aggregate().join(); + String responseContent = response2.contentUtf8(); + Document doc2 = Jsoup.parse(responseContent); + + assertThat(response2.status().code()).isEqualTo(200); + assertThat(doc2.getElementById("app-form:output-message").text()).isEqualTo("Hello test"); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName(getContextPath() + "/greeting.xhtml") + .hasKind(SpanKind.SERVER) + .hasNoParent(), + span -> handlerSpan(trace, 0, "#{greetingForm.submit()}", null))); + } + + @Test + void testException() { + // we need to display the page first before posting data to it + AggregatedHttpResponse response = + client.get(address.resolve("greeting.xhtml").toString()).aggregate().join(); + Document doc = Jsoup.parse(response.contentUtf8()); + + assertThat(response.status().code()).isEqualTo(200); + assertThat(doc.selectFirst("title").text()).isEqualTo("Hello, World!"); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName(getContextPath() + "/greeting.xhtml") + .hasKind(SpanKind.SERVER) + .hasNoParent())); + + testing.clearData(); + + String viewState = doc.selectFirst("[name=jakarta.faces.ViewState]").val(); + String formAction = doc.selectFirst("#app-form").attr("action"); + String jsessionid = + formAction.substring(formAction.indexOf("jsessionid=") + "jsessionid=".length()); + + assertThat(viewState).isNotNull(); + assertThat(jsessionid).isNotNull(); + + // 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("jakarta.faces.ViewState", viewState) + .build(); + + // use the session created for first request + AggregatedHttpRequest request2 = + AggregatedHttpRequest.of( + RequestHeaders.builder( + HttpMethod.POST, + address.resolve("greeting.xhtml;jsessionid=" + jsessionid).toString()) + .contentType(MediaType.FORM_DATA) + .build(), + HttpData.ofUtf8(formBody.toQueryString())); + + AggregatedHttpResponse response2 = client.execute(request2).aggregate().join(); + assertThat(response2.status().code()).isEqualTo(500); + + IllegalStateException expectedException = new IllegalStateException("submit exception"); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName(getContextPath() + "/greeting.xhtml") + .hasKind(SpanKind.SERVER) + .hasNoParent() + .hasStatus(StatusData.error()) + .hasException(expectedException), + span -> handlerSpan(trace, 0, "#{greetingForm.submit()}", expectedException))); + } + + List> handlerSpan( + TraceAssert trace, int parentIndex, String spanName, Exception expectedException) { + List> assertions = + new ArrayList<>( + Arrays.asList( + span -> + span.hasName(spanName) + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(parentIndex)))); + + if (expectedException != null) { + assertions.add(span -> span.hasStatus(StatusData.error()).hasException(expectedException)); + } + return assertions; + } +} diff --git a/instrumentation/jsf/jsf-jakarta-common/testing/src/main/java/io/opentelemetry/javaagent/instrumentation/jsf/jakarta/ExceptionFilter.java b/instrumentation/jsf/jsf-jakarta-common/testing/src/main/java/io/opentelemetry/javaagent/instrumentation/jsf/jakarta/ExceptionFilter.java new file mode 100644 index 000000000000..217a0c38beea --- /dev/null +++ b/instrumentation/jsf/jsf-jakarta-common/testing/src/main/java/io/opentelemetry/javaagent/instrumentation/jsf/jakarta/ExceptionFilter.java @@ -0,0 +1,40 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jsf.jakarta; + +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 java.io.IOException; + +public class ExceptionFilter implements Filter { + @Override + public void init(FilterConfig filterConfig) {} + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws ServletException, IOException { + try { + chain.doFilter(request, response); + } catch (ServletException exception) { + // to ease testing unwrap our exception to root cause + Throwable tmp = exception; + while (tmp.getCause() != null) { + tmp = tmp.getCause(); + } + if (tmp.getMessage() != null && tmp.getMessage().contains("submit exception")) { + throw (IllegalStateException) tmp; + } + throw exception; + } + } + + @Override + public void destroy() {} +} diff --git a/instrumentation/jsf/jsf-jakarta-common/testing/src/main/java/io/opentelemetry/javaagent/instrumentation/jsf/jakarta/GreetingForm.java b/instrumentation/jsf/jsf-jakarta-common/testing/src/main/java/io/opentelemetry/javaagent/instrumentation/jsf/jakarta/GreetingForm.java new file mode 100644 index 000000000000..8ca071c34972 --- /dev/null +++ b/instrumentation/jsf/jsf-jakarta-common/testing/src/main/java/io/opentelemetry/javaagent/instrumentation/jsf/jakarta/GreetingForm.java @@ -0,0 +1,31 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jsf.jakarta; + +public class GreetingForm { + + private String name = ""; + private String message = ""; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getMessage() { + return message; + } + + public void submit() { + message = "Hello " + name; + if (name.equals("exception")) { + throw new IllegalStateException("submit exception"); + } + } +} diff --git a/instrumentation/jsf/jsf-jakarta-common/testing/src/main/resources/test-app/WEB-INF/faces-config.xml b/instrumentation/jsf/jsf-jakarta-common/testing/src/main/resources/test-app/WEB-INF/faces-config.xml index b076ae885c01..4fe311004299 100644 --- a/instrumentation/jsf/jsf-jakarta-common/testing/src/main/resources/test-app/WEB-INF/faces-config.xml +++ b/instrumentation/jsf/jsf-jakarta-common/testing/src/main/resources/test-app/WEB-INF/faces-config.xml @@ -5,7 +5,7 @@ greetingForm - GreetingForm + io.opentelemetry.javaagent.instrumentation.jsf.jakarta.GreetingForm request diff --git a/instrumentation/jsf/jsf-jakarta-common/testing/src/main/resources/test-app/WEB-INF/web.xml b/instrumentation/jsf/jsf-jakarta-common/testing/src/main/resources/test-app/WEB-INF/web.xml index 85042614b120..b22bc5526fae 100644 --- a/instrumentation/jsf/jsf-jakarta-common/testing/src/main/resources/test-app/WEB-INF/web.xml +++ b/instrumentation/jsf/jsf-jakarta-common/testing/src/main/resources/test-app/WEB-INF/web.xml @@ -5,7 +5,7 @@ ExceptionFilter - ExceptionFilter + io.opentelemetry.javaagent.instrumentation.jsf.jakarta.ExceptionFilter ExceptionFilter diff --git a/instrumentation/jsf/jsf-jakarta-common/testing/src/main/resources/test-app/greeting.xhtml b/instrumentation/jsf/jsf-jakarta-common/testing/src/main/resources/test-app/greeting.xhtml index 3bc9510abb1e..0db5323bfc0f 100644 --- a/instrumentation/jsf/jsf-jakarta-common/testing/src/main/resources/test-app/greeting.xhtml +++ b/instrumentation/jsf/jsf-jakarta-common/testing/src/main/resources/test-app/greeting.xhtml @@ -1,23 +1,21 @@ - + Hello, World!

- - - + + +

- +

- +

- \ No newline at end of file + diff --git a/instrumentation/jsf/jsf-jakarta-common/testing/src/main/resources/test-app/hello.xhtml b/instrumentation/jsf/jsf-jakarta-common/testing/src/main/resources/test-app/hello.xhtml index f98ba1c01799..eb91e62b4a52 100644 --- a/instrumentation/jsf/jsf-jakarta-common/testing/src/main/resources/test-app/hello.xhtml +++ b/instrumentation/jsf/jsf-jakarta-common/testing/src/main/resources/test-app/hello.xhtml @@ -1,8 +1,8 @@ - - - Hello - - \ No newline at end of file + + Hello + diff --git a/instrumentation/jsf/jsf-javax-common/testing/src/main/groovy/BaseJsfTest.groovy b/instrumentation/jsf/jsf-javax-common/testing/src/main/groovy/BaseJsfTest.groovy deleted file mode 100644 index d78ea5cb5c63..000000000000 --- a/instrumentation/jsf/jsf-javax-common/testing/src/main/groovy/BaseJsfTest.groovy +++ /dev/null @@ -1,274 +0,0 @@ -/* - * 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.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.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 - -import static io.opentelemetry.api.trace.SpanKind.INTERNAL -import static io.opentelemetry.api.trace.StatusCode.ERROR - -abstract class BaseJsfTest extends AgentInstrumentationSpecification implements HttpServerTestTrait { - - def setupSpec() { - setupServer() - } - - def cleanupSpec() { - cleanupServer() - } - - @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(path).toString()).aggregate().join() - - expect: - response.status().code() == 200 - response.contentUtf8().trim() == "Hello" - - and: - assertTraces(1) { - trace(0, 1) { - span(0) { - name getContextPath() + "/hello.xhtml" - kind SpanKind.SERVER - hasNoParent() - attributes { - "net.protocol.name" "http" - "net.protocol.version" "1.1" - "$SemanticAttributes.NET_HOST_NAME" "localhost" - "$SemanticAttributes.NET_HOST_PORT" port - "$SemanticAttributes.NET_SOCK_PEER_ADDR" "127.0.0.1" - "$SemanticAttributes.NET_SOCK_PEER_PORT" Long - "$SemanticAttributes.NET_SOCK_HOST_ADDR" "127.0.0.1" - "$SemanticAttributes.HTTP_METHOD" "GET" - "$SemanticAttributes.HTTP_SCHEME" "http" - "$SemanticAttributes.HTTP_TARGET" "/jetty-context/" + path - "$SemanticAttributes.USER_AGENT_ORIGINAL" TEST_USER_AGENT - "$SemanticAttributes.HTTP_STATUS_CODE" 200 - "$SemanticAttributes.HTTP_ROUTE" "/jetty-context/" + route - "$SemanticAttributes.HTTP_CLIENT_IP" { it == null || it == TEST_CLIENT_IP } - } - } - } - } - - where: - path | route - "hello.jsf" | "*.jsf" - "faces/hello.xhtml" | "faces/*" - } - - 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) { - span(0) { - name getContextPath() + "/greeting.xhtml" - kind SpanKind.SERVER - hasNoParent() - } - } - } - 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) { - span(0) { - name getContextPath() + "/greeting.xhtml" - kind SpanKind.SERVER - hasNoParent() - } - 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) { - span(0) { - name getContextPath() + "/greeting.xhtml" - kind SpanKind.SERVER - hasNoParent() - } - } - } - 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 - def ex = new Exception("submit exception") - - and: - assertTraces(1) { - trace(0, 2) { - span(0) { - name getContextPath() + "/greeting.xhtml" - kind SpanKind.SERVER - hasNoParent() - status ERROR - errorEvent(ex.class, ex.message) - } - handlerSpan(it, 1, span(0), "#{greetingForm.submit()}", ex) - } - } - } - - 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/instrumentation/jsf/jsf-javax-common/testing/src/main/groovy/ExceptionFilter.groovy b/instrumentation/jsf/jsf-javax-common/testing/src/main/groovy/ExceptionFilter.groovy deleted file mode 100644 index 9efcd3acc017..000000000000 --- a/instrumentation/jsf/jsf-javax-common/testing/src/main/groovy/ExceptionFilter.groovy +++ /dev/null @@ -1,38 +0,0 @@ -/* - * 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/instrumentation/jsf/jsf-javax-common/testing/src/main/groovy/GreetingForm.groovy b/instrumentation/jsf/jsf-javax-common/testing/src/main/groovy/GreetingForm.groovy deleted file mode 100644 index f8d429f4ee2a..000000000000 --- a/instrumentation/jsf/jsf-javax-common/testing/src/main/groovy/GreetingForm.groovy +++ /dev/null @@ -1,29 +0,0 @@ -/* - * 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/instrumentation/jsf/jsf-javax-common/testing/src/main/java/io/opentelemetry/javaagent/instrumentation/jsf/javax/BaseJsfTest.java b/instrumentation/jsf/jsf-javax-common/testing/src/main/java/io/opentelemetry/javaagent/instrumentation/jsf/javax/BaseJsfTest.java new file mode 100644 index 000000000000..01aaf48d159e --- /dev/null +++ b/instrumentation/jsf/jsf-javax-common/testing/src/main/java/io/opentelemetry/javaagent/instrumentation/jsf/javax/BaseJsfTest.java @@ -0,0 +1,296 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jsf.javax; + +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.satisfies; +import static java.util.Collections.addAll; +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpServerUsingTest; +import io.opentelemetry.instrumentation.testing.junit.http.HttpServerInstrumentationExtension; +import io.opentelemetry.sdk.testing.assertj.SpanDataAssert; +import io.opentelemetry.sdk.testing.assertj.TraceAssert; +import io.opentelemetry.sdk.trace.data.StatusData; +import io.opentelemetry.semconv.ClientAttributes; +import io.opentelemetry.semconv.HttpAttributes; +import io.opentelemetry.semconv.NetworkAttributes; +import io.opentelemetry.semconv.ServerAttributes; +import io.opentelemetry.semconv.UrlAttributes; +import io.opentelemetry.semconv.UserAgentAttributes; +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 java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.function.Consumer; +import java.util.stream.Stream; +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 org.jsoup.nodes.Document; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.RegisterExtension; +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; + +public abstract class BaseJsfTest extends AbstractHttpServerUsingTest { + @RegisterExtension + public static final InstrumentationExtension testing = + HttpServerInstrumentationExtension.forAgent(); + + protected abstract String getJsfVersion(); + + @BeforeAll + protected void setUp() { + startServer(); + } + + @AfterAll + protected void cleanUp() { + cleanupServer(); + } + + @Override + protected Server setupServer() throws Exception { + List configurationClasses = new ArrayList<>(); + addAll(configurationClasses, WebAppContext.getDefaultConfigurationClasses()); + configurationClasses.add(AnnotationConfiguration.class.getName()); + + WebAppContext webAppContext = new WebAppContext(); + webAppContext.setContextPath(getContextPath()); + webAppContext.setConfigurationClasses(configurationClasses); + // set up test application + webAppContext.setBaseResource(Resource.newSystemResource("test-app-" + getJsfVersion())); + // add additional resources for test app + Resource extraResource = Resource.newSystemResource("test-app-" + getJsfVersion() + "-extra"); + if (extraResource != null) { + webAppContext.getMetaData().addWebInfJar(extraResource); + } + webAppContext.getMetaData().getWebInfClassesDirs().add(Resource.newClassPathResource("/")); + + Server jettyServer = new Server(port); + jettyServer.setHandler(webAppContext); + jettyServer.start(); + + return jettyServer; + } + + @Override + protected String getContextPath() { + return "/jetty-context"; + } + + @Override + protected void stopServer(Server server) throws Exception { + server.stop(); + server.destroy(); + } + + @ParameterizedTest + @ArgumentsSource(PathTestArgs.class) + void testPath(String path, String route) { + AggregatedHttpResponse response = + client.get(address.resolve(path).toString()).aggregate().join(); + assertThat(response.status().code()).isEqualTo(200); + assertThat(response.contentUtf8().trim()).isEqualTo("Hello"); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName(getContextPath() + "/hello.xhtml") + .hasKind(SpanKind.SERVER) + .hasAttributesSatisfyingExactly( + equalTo(NetworkAttributes.NETWORK_PROTOCOL_VERSION, "1.1"), + equalTo(ServerAttributes.SERVER_ADDRESS, "localhost"), + equalTo(ServerAttributes.SERVER_PORT, port), + equalTo(NetworkAttributes.NETWORK_PEER_ADDRESS, "127.0.0.1"), + satisfies( + NetworkAttributes.NETWORK_PEER_PORT, + val -> val.isInstanceOf(Long.class)), + equalTo(HttpAttributes.HTTP_REQUEST_METHOD, "GET"), + equalTo(UrlAttributes.URL_SCHEME, "http"), + equalTo(UrlAttributes.URL_PATH, getContextPath() + "/" + path), + equalTo(UserAgentAttributes.USER_AGENT_ORIGINAL, TEST_USER_AGENT), + equalTo(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, 200), + equalTo(HttpAttributes.HTTP_ROUTE, getContextPath() + "/" + route), + satisfies( + ClientAttributes.CLIENT_ADDRESS, + val -> + val.satisfiesAnyOf( + v -> assertThat(v).isEqualTo(TEST_CLIENT_IP), + v -> assertThat(v).isNull()))))); + } + + static class PathTestArgs implements ArgumentsProvider { + @Override + public Stream provideArguments(ExtensionContext context) { + return Stream.of( + Arguments.of("hello.jsf", "*.jsf"), Arguments.of("faces/hello.xhtml", "faces/*")); + } + } + + @Test + void testGreeting() { + // we need to display the page first before posting data to it + AggregatedHttpResponse response = + client.get(address.resolve("greeting.jsf").toString()).aggregate().join(); + Document doc = Jsoup.parse(response.contentUtf8()); + + assertThat(response.status().code()).isEqualTo(200); + assertThat(doc.selectFirst("title").text()).isEqualTo("Hello, World!"); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName(getContextPath() + "/greeting.xhtml") + .hasKind(SpanKind.SERVER) + .hasNoParent())); + + testing.clearData(); + + String viewState = doc.selectFirst("[name=javax.faces.ViewState]").val(); + String formAction = doc.selectFirst("#app-form").attr("action"); + String jsessionid = + formAction.substring(formAction.indexOf("jsessionid=") + "jsessionid=".length()); + + assertThat(viewState).isNotNull(); + assertThat(jsessionid).isNotNull(); + + // 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 + AggregatedHttpRequest 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(); + String responseContent = response2.contentUtf8(); + Document doc2 = Jsoup.parse(responseContent); + + assertThat(response2.status().code()).isEqualTo(200); + assertThat(doc2.getElementById("app-form:output-message").text()).isEqualTo("Hello test"); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName(getContextPath() + "/greeting.xhtml") + .hasKind(SpanKind.SERVER) + .hasNoParent(), + span -> handlerSpan(trace, 0, "#{greetingForm.submit()}", null))); + } + + List> handlerSpan( + TraceAssert trace, int parentIndex, String spanName, Exception expectedException) { + List> assertions = + new ArrayList<>( + Arrays.asList( + span -> + span.hasName(spanName) + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(parentIndex)))); + + if (expectedException != null) { + assertions.add(span -> span.hasStatus(StatusData.error()).hasException(expectedException)); + } + return assertions; + } + + @Test + void testException() { + // we need to display the page first before posting data to it + AggregatedHttpResponse response = + client.get(address.resolve("greeting.jsf").toString()).aggregate().join(); + Document doc = Jsoup.parse(response.contentUtf8()); + + assertThat(response.status().code()).isEqualTo(200); + assertThat(doc.selectFirst("title").text()).isEqualTo("Hello, World!"); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName(getContextPath() + "/greeting.xhtml") + .hasKind(SpanKind.SERVER) + .hasNoParent())); + + testing.clearData(); + + String viewState = doc.selectFirst("[name=javax.faces.ViewState]").val(); + String formAction = doc.selectFirst("#app-form").attr("action"); + String jsessionid = + formAction.substring(formAction.indexOf("jsessionid=") + "jsessionid=".length()); + + assertThat(viewState).isNotNull(); + assertThat(jsessionid).isNotNull(); + + // 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 + AggregatedHttpRequest 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(); + + assertThat(response2.status().code()).isEqualTo(500); + IllegalStateException expectedException = new IllegalStateException("submit exception"); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName(getContextPath() + "/greeting.xhtml") + .hasKind(SpanKind.SERVER) + .hasNoParent() + .hasStatus(StatusData.error()) + .hasException(expectedException), + span -> handlerSpan(trace, 0, "#{greetingForm.submit()}", expectedException))); + } +} diff --git a/instrumentation/jsf/jsf-javax-common/testing/src/main/java/io/opentelemetry/javaagent/instrumentation/jsf/javax/ExceptionFilter.java b/instrumentation/jsf/jsf-javax-common/testing/src/main/java/io/opentelemetry/javaagent/instrumentation/jsf/javax/ExceptionFilter.java new file mode 100644 index 000000000000..9360760dd739 --- /dev/null +++ b/instrumentation/jsf/jsf-javax-common/testing/src/main/java/io/opentelemetry/javaagent/instrumentation/jsf/javax/ExceptionFilter.java @@ -0,0 +1,40 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jsf.javax; + +import java.io.IOException; +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; + +public class ExceptionFilter implements Filter { + @Override + public void init(FilterConfig filterConfig) {} + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws ServletException, IOException { + try { + chain.doFilter(request, response); + } catch (ServletException exception) { + // to ease testing unwrap our exception to root cause + Throwable tmp = exception; + while (tmp.getCause() != null) { + tmp = tmp.getCause(); + } + if (tmp.getMessage() != null && tmp.getMessage().contains("submit exception")) { + throw (IllegalStateException) tmp; + } + throw exception; + } + } + + @Override + public void destroy() {} +} diff --git a/instrumentation/jsf/jsf-javax-common/testing/src/main/java/io/opentelemetry/javaagent/instrumentation/jsf/javax/GreetingForm.java b/instrumentation/jsf/jsf-javax-common/testing/src/main/java/io/opentelemetry/javaagent/instrumentation/jsf/javax/GreetingForm.java new file mode 100644 index 000000000000..60e36f4a60d4 --- /dev/null +++ b/instrumentation/jsf/jsf-javax-common/testing/src/main/java/io/opentelemetry/javaagent/instrumentation/jsf/javax/GreetingForm.java @@ -0,0 +1,31 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jsf.javax; + +public class GreetingForm { + + private String name = ""; + private String message = ""; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getMessage() { + return message; + } + + public void submit() { + message = "Hello " + name; + if (name.equals("exception")) { + throw new IllegalStateException("submit exception"); + } + } +} diff --git a/instrumentation/jsf/jsf-javax-common/testing/src/main/resources/test-app-1.2/WEB-INF/faces-config.xml b/instrumentation/jsf/jsf-javax-common/testing/src/main/resources/test-app-1.2/WEB-INF/faces-config.xml index 5502993f7db5..de9a1c0a68d2 100644 --- a/instrumentation/jsf/jsf-javax-common/testing/src/main/resources/test-app-1.2/WEB-INF/faces-config.xml +++ b/instrumentation/jsf/jsf-javax-common/testing/src/main/resources/test-app-1.2/WEB-INF/faces-config.xml @@ -4,11 +4,11 @@ version="1.2"> greetingForm - GreetingForm + io.opentelemetry.javaagent.instrumentation.jsf.javax.GreetingForm request com.sun.facelets.FaceletViewHandler - \ No newline at end of file + diff --git a/instrumentation/jsf/jsf-javax-common/testing/src/main/resources/test-app-1.2/WEB-INF/web.xml b/instrumentation/jsf/jsf-javax-common/testing/src/main/resources/test-app-1.2/WEB-INF/web.xml index cb64b6ae8da1..2214cec902e6 100644 --- a/instrumentation/jsf/jsf-javax-common/testing/src/main/resources/test-app-1.2/WEB-INF/web.xml +++ b/instrumentation/jsf/jsf-javax-common/testing/src/main/resources/test-app-1.2/WEB-INF/web.xml @@ -20,7 +20,7 @@ ExceptionFilter - ExceptionFilter + io.opentelemetry.javaagent.instrumentation.jsf.javax.ExceptionFilter @@ -33,4 +33,4 @@ .xhtml - \ No newline at end of file + diff --git a/instrumentation/jsf/jsf-javax-common/testing/src/main/resources/test-app-1.2/greeting.xhtml b/instrumentation/jsf/jsf-javax-common/testing/src/main/resources/test-app-1.2/greeting.xhtml index 2b53eebbadbd..9a30050a47b7 100644 --- a/instrumentation/jsf/jsf-javax-common/testing/src/main/resources/test-app-1.2/greeting.xhtml +++ b/instrumentation/jsf/jsf-javax-common/testing/src/main/resources/test-app-1.2/greeting.xhtml @@ -1,24 +1,22 @@ - + Hello, World!

- - - + + +

- +

- \ No newline at end of file + diff --git a/instrumentation/jsf/jsf-javax-common/testing/src/main/resources/test-app-1.2/hello.xhtml b/instrumentation/jsf/jsf-javax-common/testing/src/main/resources/test-app-1.2/hello.xhtml index d3b79206bef5..35067ccf2f86 100644 --- a/instrumentation/jsf/jsf-javax-common/testing/src/main/resources/test-app-1.2/hello.xhtml +++ b/instrumentation/jsf/jsf-javax-common/testing/src/main/resources/test-app-1.2/hello.xhtml @@ -1,6 +1,3 @@ - - - Hello - + + Hello diff --git a/instrumentation/jsf/jsf-javax-common/testing/src/main/resources/test-app-2/WEB-INF/faces-config.xml b/instrumentation/jsf/jsf-javax-common/testing/src/main/resources/test-app-2/WEB-INF/faces-config.xml index 9a0ad7c89786..f83e949af4db 100644 --- a/instrumentation/jsf/jsf-javax-common/testing/src/main/resources/test-app-2/WEB-INF/faces-config.xml +++ b/instrumentation/jsf/jsf-javax-common/testing/src/main/resources/test-app-2/WEB-INF/faces-config.xml @@ -2,7 +2,7 @@ greetingForm - GreetingForm + io.opentelemetry.javaagent.instrumentation.jsf.javax.GreetingForm request - \ No newline at end of file + diff --git a/instrumentation/jsf/jsf-javax-common/testing/src/main/resources/test-app-2/WEB-INF/web.xml b/instrumentation/jsf/jsf-javax-common/testing/src/main/resources/test-app-2/WEB-INF/web.xml index 191c55b09da3..32949939fbb4 100644 --- a/instrumentation/jsf/jsf-javax-common/testing/src/main/resources/test-app-2/WEB-INF/web.xml +++ b/instrumentation/jsf/jsf-javax-common/testing/src/main/resources/test-app-2/WEB-INF/web.xml @@ -6,7 +6,7 @@ ExceptionFilter - ExceptionFilter + io.opentelemetry.javaagent.instrumentation.jsf.javax.ExceptionFilter @@ -14,4 +14,4 @@ /* - \ No newline at end of file + diff --git a/instrumentation/jsf/jsf-javax-common/testing/src/main/resources/test-app-2/greeting.xhtml b/instrumentation/jsf/jsf-javax-common/testing/src/main/resources/test-app-2/greeting.xhtml index 3bc9510abb1e..0db5323bfc0f 100644 --- a/instrumentation/jsf/jsf-javax-common/testing/src/main/resources/test-app-2/greeting.xhtml +++ b/instrumentation/jsf/jsf-javax-common/testing/src/main/resources/test-app-2/greeting.xhtml @@ -1,23 +1,21 @@ - + Hello, World!

- - - + + +

- +

- +

- \ No newline at end of file + diff --git a/instrumentation/jsf/jsf-javax-common/testing/src/main/resources/test-app-2/hello.xhtml b/instrumentation/jsf/jsf-javax-common/testing/src/main/resources/test-app-2/hello.xhtml index f98ba1c01799..eb91e62b4a52 100644 --- a/instrumentation/jsf/jsf-javax-common/testing/src/main/resources/test-app-2/hello.xhtml +++ b/instrumentation/jsf/jsf-javax-common/testing/src/main/resources/test-app-2/hello.xhtml @@ -1,8 +1,8 @@ - - - Hello - - \ No newline at end of file + + Hello + diff --git a/instrumentation/jsf/jsf-mojarra-1.2/javaagent/build.gradle.kts b/instrumentation/jsf/jsf-mojarra-1.2/javaagent/build.gradle.kts index 290bb4dc5d70..d20834a5162f 100644 --- a/instrumentation/jsf/jsf-mojarra-1.2/javaagent/build.gradle.kts +++ b/instrumentation/jsf/jsf-mojarra-1.2/javaagent/build.gradle.kts @@ -90,3 +90,6 @@ tasks { dependsOn(testing.suites) } } +tasks.withType().configureEach { + jvmArgs("-Dotel.instrumentation.common.experimental.controller-telemetry.enabled=true") +} diff --git a/instrumentation/jsf/jsf-mojarra-1.2/javaagent/src/mojarra12Test/groovy/Mojarra12Test.groovy b/instrumentation/jsf/jsf-mojarra-1.2/javaagent/src/mojarra12Test/groovy/Mojarra12Test.groovy deleted file mode 100644 index d47c830ed0c4..000000000000 --- a/instrumentation/jsf/jsf-mojarra-1.2/javaagent/src/mojarra12Test/groovy/Mojarra12Test.groovy +++ /dev/null @@ -1,11 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -class Mojarra12Test extends BaseJsfTest { - @Override - String getJsfVersion() { - "1.2" - } -} diff --git a/instrumentation/jsf/jsf-mojarra-1.2/javaagent/src/mojarra12Test/java/io/opentelemetry/javaagent/instrumentation/mojarra/Mojarra12Test.java b/instrumentation/jsf/jsf-mojarra-1.2/javaagent/src/mojarra12Test/java/io/opentelemetry/javaagent/instrumentation/mojarra/Mojarra12Test.java new file mode 100644 index 000000000000..991b5d9cfb25 --- /dev/null +++ b/instrumentation/jsf/jsf-mojarra-1.2/javaagent/src/mojarra12Test/java/io/opentelemetry/javaagent/instrumentation/mojarra/Mojarra12Test.java @@ -0,0 +1,15 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.mojarra; + +import io.opentelemetry.javaagent.instrumentation.jsf.javax.BaseJsfTest; + +class Mojarra12Test extends BaseJsfTest { + @Override + public String getJsfVersion() { + return "1.2"; + } +} diff --git a/instrumentation/jsf/jsf-mojarra-1.2/javaagent/src/mojarra2Test/groovy/Mojarra2Test.groovy b/instrumentation/jsf/jsf-mojarra-1.2/javaagent/src/mojarra2Test/groovy/Mojarra2Test.groovy deleted file mode 100644 index 1097bb1e6e8c..000000000000 --- a/instrumentation/jsf/jsf-mojarra-1.2/javaagent/src/mojarra2Test/groovy/Mojarra2Test.groovy +++ /dev/null @@ -1,11 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -class Mojarra2Test extends BaseJsfTest { - @Override - String getJsfVersion() { - "2" - } -} diff --git a/instrumentation/jsf/jsf-mojarra-1.2/javaagent/src/mojarra2Test/java/io/opentelemetry/javaagent/instrumentation/mojarra/Mojarra2Test.java b/instrumentation/jsf/jsf-mojarra-1.2/javaagent/src/mojarra2Test/java/io/opentelemetry/javaagent/instrumentation/mojarra/Mojarra2Test.java new file mode 100644 index 000000000000..3d6e615ab810 --- /dev/null +++ b/instrumentation/jsf/jsf-mojarra-1.2/javaagent/src/mojarra2Test/java/io/opentelemetry/javaagent/instrumentation/mojarra/Mojarra2Test.java @@ -0,0 +1,15 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.mojarra; + +import io.opentelemetry.javaagent.instrumentation.jsf.javax.BaseJsfTest; + +class Mojarra2Test extends BaseJsfTest { + @Override + public String getJsfVersion() { + return "2"; + } +} diff --git a/instrumentation/jsf/jsf-mojarra-3.0/javaagent/build.gradle.kts b/instrumentation/jsf/jsf-mojarra-3.0/javaagent/build.gradle.kts index b4bf3e9aefb4..dfdbac3417e7 100644 --- a/instrumentation/jsf/jsf-mojarra-3.0/javaagent/build.gradle.kts +++ b/instrumentation/jsf/jsf-mojarra-3.0/javaagent/build.gradle.kts @@ -33,3 +33,9 @@ dependencies { // JSF 4+ requires CDI instead of BeanManager, the test should be upgraded first // latestDepTestLibrary("org.glassfish:jakarta.faces:4.+") } + +tasks { + withType().configureEach { + jvmArgs("-Dotel.instrumentation.common.experimental.controller-telemetry.enabled=true") + } +} diff --git a/instrumentation/jsf/jsf-mojarra-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/mojarra/v3_0/Mojarra3Test.java b/instrumentation/jsf/jsf-mojarra-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/mojarra/v3_0/Mojarra3Test.java new file mode 100644 index 000000000000..64488faa23d5 --- /dev/null +++ b/instrumentation/jsf/jsf-mojarra-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/mojarra/v3_0/Mojarra3Test.java @@ -0,0 +1,10 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.mojarra.v3_0; + +import io.opentelemetry.javaagent.instrumentation.jsf.jakarta.BaseJsfTest; + +class Mojarra3Test extends BaseJsfTest {} diff --git a/instrumentation/jsf/jsf-myfaces-1.2/javaagent/build.gradle.kts b/instrumentation/jsf/jsf-myfaces-1.2/javaagent/build.gradle.kts index 9297b68c3817..c7d756e5b25a 100644 --- a/instrumentation/jsf/jsf-myfaces-1.2/javaagent/build.gradle.kts +++ b/instrumentation/jsf/jsf-myfaces-1.2/javaagent/build.gradle.kts @@ -60,3 +60,8 @@ tasks { dependsOn(testing.suites) } } + +tasks.withType().configureEach { + jvmArgs("-Dotel.instrumentation.common.experimental.controller-telemetry.enabled=true") + jvmArgs("-Dotel.instrumentation.common.experimental.view-telemetry.enabled=true") +} diff --git a/instrumentation/jsf/jsf-myfaces-1.2/javaagent/src/myfaces12Test/groovy/Myfaces12Test.groovy b/instrumentation/jsf/jsf-myfaces-1.2/javaagent/src/myfaces12Test/groovy/Myfaces12Test.groovy deleted file mode 100644 index bc532d1a2cad..000000000000 --- a/instrumentation/jsf/jsf-myfaces-1.2/javaagent/src/myfaces12Test/groovy/Myfaces12Test.groovy +++ /dev/null @@ -1,11 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -class Myfaces12Test extends BaseJsfTest { - @Override - String getJsfVersion() { - "1.2" - } -} diff --git a/instrumentation/jsf/jsf-myfaces-1.2/javaagent/src/myfaces12Test/java/io/opentelemetry/javaagent/instrumentation/myfaces/Myfaces12Test.java b/instrumentation/jsf/jsf-myfaces-1.2/javaagent/src/myfaces12Test/java/io/opentelemetry/javaagent/instrumentation/myfaces/Myfaces12Test.java new file mode 100644 index 000000000000..564091fd300f --- /dev/null +++ b/instrumentation/jsf/jsf-myfaces-1.2/javaagent/src/myfaces12Test/java/io/opentelemetry/javaagent/instrumentation/myfaces/Myfaces12Test.java @@ -0,0 +1,15 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.myfaces; + +import io.opentelemetry.javaagent.instrumentation.jsf.javax.BaseJsfTest; + +class Myfaces12Test extends BaseJsfTest { + @Override + public String getJsfVersion() { + return "1.2"; + } +} diff --git a/instrumentation/jsf/jsf-myfaces-1.2/javaagent/src/myfaces2Test/groovy/Myfaces2Test.groovy b/instrumentation/jsf/jsf-myfaces-1.2/javaagent/src/myfaces2Test/groovy/Myfaces2Test.groovy deleted file mode 100644 index bd08b6975d42..000000000000 --- a/instrumentation/jsf/jsf-myfaces-1.2/javaagent/src/myfaces2Test/groovy/Myfaces2Test.groovy +++ /dev/null @@ -1,11 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -class Myfaces2Test extends BaseJsfTest { - @Override - String getJsfVersion() { - "2" - } -} diff --git a/instrumentation/jsf/jsf-myfaces-1.2/javaagent/src/myfaces2Test/java/io/opentelemetry/javaagent/instrumentation/myfaces/Myfaces2Test.java b/instrumentation/jsf/jsf-myfaces-1.2/javaagent/src/myfaces2Test/java/io/opentelemetry/javaagent/instrumentation/myfaces/Myfaces2Test.java new file mode 100644 index 000000000000..67c9e38861e5 --- /dev/null +++ b/instrumentation/jsf/jsf-myfaces-1.2/javaagent/src/myfaces2Test/java/io/opentelemetry/javaagent/instrumentation/myfaces/Myfaces2Test.java @@ -0,0 +1,15 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.myfaces; + +import io.opentelemetry.javaagent.instrumentation.jsf.javax.BaseJsfTest; + +class Myfaces2Test extends BaseJsfTest { + @Override + public String getJsfVersion() { + return "2"; + } +} diff --git a/instrumentation/jsf/jsf-myfaces-3.0/javaagent/build.gradle.kts b/instrumentation/jsf/jsf-myfaces-3.0/javaagent/build.gradle.kts index faa2e83da176..e1da9bfd350e 100644 --- a/instrumentation/jsf/jsf-myfaces-3.0/javaagent/build.gradle.kts +++ b/instrumentation/jsf/jsf-myfaces-3.0/javaagent/build.gradle.kts @@ -33,3 +33,9 @@ dependencies { // JSF 4+ requires CDI instead of BeanManager, the test should be upgraded first // latestDepTestLibrary("org.apache.myfaces.core:myfaces-impl:4.+") } + +tasks { + withType().configureEach { + jvmArgs("-Dotel.instrumentation.common.experimental.controller-telemetry.enabled=true") + } +} diff --git a/instrumentation/jsf/jsf-myfaces-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/myfaces/v3_0/Myfaces3Test.java b/instrumentation/jsf/jsf-myfaces-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/myfaces/v3_0/Myfaces3Test.java new file mode 100644 index 000000000000..9d6d8b04c694 --- /dev/null +++ b/instrumentation/jsf/jsf-myfaces-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/myfaces/v3_0/Myfaces3Test.java @@ -0,0 +1,10 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.myfaces.v3_0; + +import io.opentelemetry.javaagent.instrumentation.jsf.jakarta.BaseJsfTest; + +class Myfaces3Test extends BaseJsfTest {} diff --git a/instrumentation/jsp-2.3/README.md b/instrumentation/jsp-2.3/README.md index 5e32f64188ed..95306dc8e5c7 100644 --- a/instrumentation/jsp-2.3/README.md +++ b/instrumentation/jsp-2.3/README.md @@ -1,5 +1,5 @@ # Settings for the JSP instrumentation -| System property | Type | Default | Description | -|---|---|---|---| +| System property | Type | Default | Description | +| ------------------------------------------------------- | ------- | ------- | --------------------------------------------------- | | `otel.instrumentation.jsp.experimental-span-attributes` | Boolean | `false` | Enable the capture of experimental span attributes. | diff --git a/instrumentation/jsp-2.3/javaagent/build.gradle.kts b/instrumentation/jsp-2.3/javaagent/build.gradle.kts index 915e510e50a3..497c7630f36e 100644 --- a/instrumentation/jsp-2.3/javaagent/build.gradle.kts +++ b/instrumentation/jsp-2.3/javaagent/build.gradle.kts @@ -51,4 +51,6 @@ tasks.withType().configureEach { // TODO run tests both with and without experimental span attributes jvmArgs("-Dotel.instrumentation.jsp.experimental-span-attributes=true") + + jvmArgs("-Dotel.instrumentation.common.experimental.view-telemetry.enabled=true") } diff --git a/instrumentation/jsp-2.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jsp/HttpJspPageInstrumentationSingletons.java b/instrumentation/jsp-2.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jsp/HttpJspPageInstrumentationSingletons.java index 301657c1d69a..fd8c74030f81 100644 --- a/instrumentation/jsp-2.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jsp/HttpJspPageInstrumentationSingletons.java +++ b/instrumentation/jsp-2.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jsp/HttpJspPageInstrumentationSingletons.java @@ -13,7 +13,8 @@ import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; import io.opentelemetry.instrumentation.api.instrumenter.SpanKindExtractor; -import io.opentelemetry.javaagent.bootstrap.internal.InstrumentationConfig; +import io.opentelemetry.javaagent.bootstrap.internal.AgentInstrumentationConfig; +import io.opentelemetry.javaagent.bootstrap.internal.ExperimentalConfig; import java.net.URI; import java.net.URISyntaxException; import java.util.logging.Logger; @@ -24,7 +25,7 @@ public class HttpJspPageInstrumentationSingletons { private static final boolean CAPTURE_EXPERIMENTAL_SPAN_ATTRIBUTES = - InstrumentationConfig.get() + AgentInstrumentationConfig.get() .getBoolean("otel.instrumentation.jsp.experimental-span-attributes", false); private static final Instrumenter INSTRUMENTER; @@ -36,6 +37,7 @@ public class HttpJspPageInstrumentationSingletons { "io.opentelemetry.jsp-2.3", HttpJspPageInstrumentationSingletons::spanNameOnRender) .addAttributesExtractor(new RenderAttributesExtractor()) + .setEnabled(ExperimentalConfig.get().viewTelemetryEnabled()) .buildInstrumenter(SpanKindExtractor.alwaysInternal()); } diff --git a/instrumentation/jsp-2.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jsp/JspCompilationContextInstrumentationSingletons.java b/instrumentation/jsp-2.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jsp/JspCompilationContextInstrumentationSingletons.java index c4c474c725b7..28ce32f9ecca 100644 --- a/instrumentation/jsp-2.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jsp/JspCompilationContextInstrumentationSingletons.java +++ b/instrumentation/jsp-2.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jsp/JspCompilationContextInstrumentationSingletons.java @@ -11,14 +11,14 @@ import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; import io.opentelemetry.instrumentation.api.instrumenter.SpanKindExtractor; -import io.opentelemetry.javaagent.bootstrap.internal.InstrumentationConfig; +import io.opentelemetry.javaagent.bootstrap.internal.AgentInstrumentationConfig; import javax.annotation.Nullable; import org.apache.jasper.JspCompilationContext; import org.apache.jasper.compiler.Compiler; public class JspCompilationContextInstrumentationSingletons { private static final boolean CAPTURE_EXPERIMENTAL_SPAN_ATTRIBUTES = - InstrumentationConfig.get() + AgentInstrumentationConfig.get() .getBoolean("otel.instrumentation.jsp.experimental-span-attributes", false); private static final Instrumenter INSTRUMENTER; diff --git a/instrumentation/jsp-2.3/javaagent/src/test/groovy/JspInstrumentationBasicTests.groovy b/instrumentation/jsp-2.3/javaagent/src/test/groovy/JspInstrumentationBasicTests.groovy deleted file mode 100644 index 3f479c193b6f..000000000000 --- a/instrumentation/jsp-2.3/javaagent/src/test/groovy/JspInstrumentationBasicTests.groovy +++ /dev/null @@ -1,538 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -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 org.apache.catalina.Context -import org.apache.catalina.startup.Tomcat -import org.apache.jasper.JasperException -import spock.lang.Shared -import spock.lang.Unroll - -import java.nio.file.Files - -import static io.opentelemetry.api.trace.SpanKind.SERVER -import static io.opentelemetry.api.trace.StatusCode.ERROR - -//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) { - def route = "/$jspWebappContext/$jspFileName" - - hasNoParent() - name "GET $route" - kind SERVER - attributes { - "$SemanticAttributes.HTTP_SCHEME" "http" - "$SemanticAttributes.HTTP_TARGET" route - "$SemanticAttributes.HTTP_METHOD" "GET" - "$SemanticAttributes.HTTP_STATUS_CODE" 200 - "$SemanticAttributes.USER_AGENT_ORIGINAL" String - "$SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH" { it == null || it instanceof Long } - "$SemanticAttributes.HTTP_ROUTE" route - "net.protocol.name" "http" - "net.protocol.version" "1.1" - "$SemanticAttributes.NET_HOST_NAME" "localhost" - "$SemanticAttributes.NET_HOST_PORT" port - "$SemanticAttributes.NET_SOCK_PEER_ADDR" "127.0.0.1" - "$SemanticAttributes.NET_SOCK_PEER_PORT" Long - "$SemanticAttributes.NET_SOCK_HOST_ADDR" "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) { - def route = "/$jspWebappContext/getQuery.jsp" - - hasNoParent() - name "GET $route" - kind SERVER - attributes { - "$SemanticAttributes.HTTP_SCHEME" "http" - "$SemanticAttributes.HTTP_TARGET" "$route?$queryString" - "$SemanticAttributes.HTTP_METHOD" "GET" - "$SemanticAttributes.HTTP_STATUS_CODE" 200 - "$SemanticAttributes.USER_AGENT_ORIGINAL" String - "$SemanticAttributes.HTTP_ROUTE" route - "net.protocol.name" "http" - "net.protocol.version" "1.1" - "$SemanticAttributes.NET_HOST_NAME" "localhost" - "$SemanticAttributes.NET_HOST_PORT" port - "$SemanticAttributes.NET_SOCK_PEER_ADDR" "127.0.0.1" - "$SemanticAttributes.NET_SOCK_PEER_PORT" Long - "$SemanticAttributes.NET_SOCK_HOST_ADDR" "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) { - def route = "/$jspWebappContext/post.jsp" - - hasNoParent() - name "POST $route" - kind SERVER - attributes { - "$SemanticAttributes.HTTP_SCHEME" "http" - "$SemanticAttributes.HTTP_TARGET" route - "$SemanticAttributes.HTTP_METHOD" "POST" - "$SemanticAttributes.HTTP_STATUS_CODE" 200 - "$SemanticAttributes.USER_AGENT_ORIGINAL" String - "$SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH" Long - "$SemanticAttributes.HTTP_ROUTE" route - "net.protocol.name" "http" - "net.protocol.version" "1.1" - "$SemanticAttributes.NET_HOST_NAME" "localhost" - "$SemanticAttributes.NET_HOST_PORT" port - "$SemanticAttributes.NET_SOCK_PEER_ADDR" "127.0.0.1" - "$SemanticAttributes.NET_SOCK_PEER_PORT" Long - "$SemanticAttributes.NET_SOCK_HOST_ADDR" "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) { - def route = "/$jspWebappContext/$jspFileName" - - hasNoParent() - name "GET $route" - kind SERVER - status ERROR - event(0) { - eventName(SemanticAttributes.EXCEPTION_EVENT_NAME) - attributes { - "$SemanticAttributes.EXCEPTION_TYPE" { String tagExceptionType -> - return tagExceptionType == exceptionClass.getName() || tagExceptionType.contains(exceptionClass.getSimpleName()) - } - "$SemanticAttributes.EXCEPTION_MESSAGE" { String tagErrorMsg -> - return errorMessageOptional || tagErrorMsg instanceof String - } - "$SemanticAttributes.EXCEPTION_STACKTRACE" String - } - } - attributes { - "$SemanticAttributes.HTTP_SCHEME" "http" - "$SemanticAttributes.HTTP_TARGET" route - "$SemanticAttributes.HTTP_METHOD" "GET" - "$SemanticAttributes.HTTP_STATUS_CODE" 500 - "$SemanticAttributes.USER_AGENT_ORIGINAL" String - "$SemanticAttributes.HTTP_ROUTE" route - "net.protocol.name" "http" - "net.protocol.version" "1.1" - "$SemanticAttributes.NET_HOST_NAME" "localhost" - "$SemanticAttributes.NET_HOST_PORT" port - "$SemanticAttributes.NET_SOCK_PEER_ADDR" "127.0.0.1" - "$SemanticAttributes.NET_SOCK_PEER_PORT" Long - "$SemanticAttributes.NET_SOCK_HOST_ADDR" "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" { String tagExceptionType -> - return tagExceptionType == exceptionClass.getName() || tagExceptionType.contains(exceptionClass.getSimpleName()) - } - "$SemanticAttributes.EXCEPTION_MESSAGE" { String tagErrorMsg -> - return errorMessageOptional || tagErrorMsg instanceof String - } - "$SemanticAttributes.EXCEPTION_STACKTRACE" 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) { - def route = "/$jspWebappContext/includes/includeHtml.jsp" - - hasNoParent() - name "GET $route" - kind SERVER - attributes { - "$SemanticAttributes.HTTP_SCHEME" "http" - "$SemanticAttributes.HTTP_TARGET" route - "$SemanticAttributes.HTTP_METHOD" "GET" - "$SemanticAttributes.HTTP_STATUS_CODE" 200 - "$SemanticAttributes.USER_AGENT_ORIGINAL" String - "$SemanticAttributes.HTTP_ROUTE" route - "net.protocol.name" "http" - "net.protocol.version" "1.1" - "$SemanticAttributes.NET_HOST_NAME" "localhost" - "$SemanticAttributes.NET_HOST_PORT" port - "$SemanticAttributes.NET_SOCK_PEER_ADDR" "127.0.0.1" - "$SemanticAttributes.NET_SOCK_PEER_PORT" Long - "$SemanticAttributes.NET_SOCK_HOST_ADDR" "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) { - def route = "/$jspWebappContext/includes/includeMulti.jsp" - - hasNoParent() - name "GET $route" - kind SERVER - attributes { - "$SemanticAttributes.HTTP_SCHEME" "http" - "$SemanticAttributes.HTTP_TARGET" route - "$SemanticAttributes.HTTP_METHOD" "GET" - "$SemanticAttributes.HTTP_STATUS_CODE" 200 - "$SemanticAttributes.USER_AGENT_ORIGINAL" String - "$SemanticAttributes.HTTP_ROUTE" route - "net.protocol.name" "http" - "net.protocol.version" "1.1" - "$SemanticAttributes.NET_HOST_NAME" "localhost" - "$SemanticAttributes.NET_HOST_PORT" port - "$SemanticAttributes.NET_SOCK_PEER_ADDR" "127.0.0.1" - "$SemanticAttributes.NET_SOCK_PEER_PORT" Long - "$SemanticAttributes.NET_SOCK_HOST_ADDR" "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) { - def route = "/$jspWebappContext/$jspFileName" - - hasNoParent() - name "GET $route" - kind SERVER - status ERROR - errorEvent(JasperException, String) - attributes { - "$SemanticAttributes.HTTP_SCHEME" "http" - "$SemanticAttributes.HTTP_TARGET" route - "$SemanticAttributes.HTTP_METHOD" "GET" - "$SemanticAttributes.HTTP_STATUS_CODE" 500 - "$SemanticAttributes.USER_AGENT_ORIGINAL" String - "$SemanticAttributes.HTTP_ROUTE" route - "net.protocol.name" "http" - "net.protocol.version" "1.1" - "$SemanticAttributes.NET_HOST_NAME" "localhost" - "$SemanticAttributes.NET_HOST_PORT" port - "$SemanticAttributes.NET_SOCK_PEER_ADDR" "127.0.0.1" - "$SemanticAttributes.NET_SOCK_PEER_PORT" Long - "$SemanticAttributes.NET_SOCK_HOST_ADDR" "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) { - def route = "/$jspWebappContext/*" - - hasNoParent() - name "GET $route" - kind SERVER - attributes { - "$SemanticAttributes.HTTP_SCHEME" "http" - "$SemanticAttributes.HTTP_TARGET" "/$jspWebappContext/$staticFile" - "$SemanticAttributes.HTTP_METHOD" "GET" - "$SemanticAttributes.HTTP_STATUS_CODE" 200 - "$SemanticAttributes.USER_AGENT_ORIGINAL" String - "$SemanticAttributes.HTTP_ROUTE" route - "net.protocol.name" "http" - "net.protocol.version" "1.1" - "$SemanticAttributes.NET_HOST_NAME" "localhost" - "$SemanticAttributes.NET_HOST_PORT" port - "$SemanticAttributes.NET_SOCK_PEER_ADDR" "127.0.0.1" - "$SemanticAttributes.NET_SOCK_PEER_PORT" Long - "$SemanticAttributes.NET_SOCK_HOST_ADDR" "127.0.0.1" - } - } - } - } - - where: - staticFile = "common/hello.html" - } -} diff --git a/instrumentation/jsp-2.3/javaagent/src/test/groovy/JspInstrumentationForwardTests.groovy b/instrumentation/jsp-2.3/javaagent/src/test/groovy/JspInstrumentationForwardTests.groovy deleted file mode 100644 index 87d23596db01..000000000000 --- a/instrumentation/jsp-2.3/javaagent/src/test/groovy/JspInstrumentationForwardTests.groovy +++ /dev/null @@ -1,489 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -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 org.apache.catalina.Context -import org.apache.catalina.startup.Tomcat -import org.apache.jasper.JasperException -import spock.lang.Shared -import spock.lang.Unroll - -import java.nio.file.Files - -import static io.opentelemetry.api.trace.SpanKind.SERVER -import static io.opentelemetry.api.trace.StatusCode.ERROR -import static io.opentelemetry.api.trace.StatusCode.UNSET - -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) { - def route = "/$jspWebappContext/$forwardFromFileName" - - hasNoParent() - name "GET $route" - kind SERVER - attributes { - "$SemanticAttributes.HTTP_SCHEME" "http" - "$SemanticAttributes.HTTP_TARGET" route - "$SemanticAttributes.HTTP_METHOD" "GET" - "$SemanticAttributes.HTTP_STATUS_CODE" 200 - "$SemanticAttributes.USER_AGENT_ORIGINAL" String - "$SemanticAttributes.HTTP_ROUTE" route - "net.protocol.name" "http" - "net.protocol.version" "1.1" - "$SemanticAttributes.NET_HOST_NAME" "localhost" - "$SemanticAttributes.NET_HOST_PORT" port - "$SemanticAttributes.NET_SOCK_PEER_ADDR" "127.0.0.1" - "$SemanticAttributes.NET_SOCK_PEER_PORT" Long - "$SemanticAttributes.NET_SOCK_HOST_ADDR" "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) { - def route = "/$jspWebappContext/forwards/forwardToHtml.jsp" - - hasNoParent() - name "GET $route" - kind SERVER - attributes { - "$SemanticAttributes.HTTP_SCHEME" "http" - "$SemanticAttributes.HTTP_TARGET" route - "$SemanticAttributes.HTTP_METHOD" "GET" - "$SemanticAttributes.HTTP_STATUS_CODE" 200 - "$SemanticAttributes.USER_AGENT_ORIGINAL" String - "$SemanticAttributes.HTTP_ROUTE" route - "net.protocol.name" "http" - "net.protocol.version" "1.1" - "$SemanticAttributes.NET_HOST_NAME" "localhost" - "$SemanticAttributes.NET_HOST_PORT" port - "$SemanticAttributes.NET_SOCK_PEER_ADDR" "127.0.0.1" - "$SemanticAttributes.NET_SOCK_PEER_PORT" Long - "$SemanticAttributes.NET_SOCK_HOST_ADDR" "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) { - def route = "/$jspWebappContext/forwards/forwardToIncludeMulti.jsp" - - hasNoParent() - name "GET $route" - kind SERVER - attributes { - "$SemanticAttributes.HTTP_SCHEME" "http" - "$SemanticAttributes.HTTP_TARGET" route - "$SemanticAttributes.HTTP_METHOD" "GET" - "$SemanticAttributes.HTTP_STATUS_CODE" 200 - "$SemanticAttributes.USER_AGENT_ORIGINAL" String - "$SemanticAttributes.HTTP_ROUTE" route - "net.protocol.name" "http" - "net.protocol.version" "1.1" - "$SemanticAttributes.NET_HOST_NAME" "localhost" - "$SemanticAttributes.NET_HOST_PORT" port - "$SemanticAttributes.NET_SOCK_PEER_ADDR" "127.0.0.1" - "$SemanticAttributes.NET_SOCK_PEER_PORT" Long - "$SemanticAttributes.NET_SOCK_HOST_ADDR" "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) { - def route = "/$jspWebappContext/forwards/forwardToJspForward.jsp" - - hasNoParent() - name "GET $route" - kind SERVER - attributes { - "$SemanticAttributes.HTTP_SCHEME" "http" - "$SemanticAttributes.HTTP_TARGET" route - "$SemanticAttributes.HTTP_METHOD" "GET" - "$SemanticAttributes.HTTP_STATUS_CODE" 200 - "$SemanticAttributes.USER_AGENT_ORIGINAL" String - "$SemanticAttributes.HTTP_ROUTE" route - "net.protocol.name" "http" - "net.protocol.version" "1.1" - "$SemanticAttributes.NET_HOST_NAME" "localhost" - "$SemanticAttributes.NET_HOST_PORT" port - "$SemanticAttributes.NET_SOCK_PEER_ADDR" "127.0.0.1" - "$SemanticAttributes.NET_SOCK_PEER_PORT" Long - "$SemanticAttributes.NET_SOCK_HOST_ADDR" "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) { - def route = "/$jspWebappContext/forwards/forwardToCompileError.jsp" - - hasNoParent() - name "GET $route" - kind SERVER - status ERROR - errorEvent(JasperException, String) - attributes { - "$SemanticAttributes.HTTP_SCHEME" "http" - "$SemanticAttributes.HTTP_TARGET" route - "$SemanticAttributes.HTTP_METHOD" "GET" - "$SemanticAttributes.HTTP_STATUS_CODE" 500 - "$SemanticAttributes.USER_AGENT_ORIGINAL" String - "$SemanticAttributes.HTTP_ROUTE" route - "net.protocol.name" "http" - "net.protocol.version" "1.1" - "$SemanticAttributes.NET_HOST_NAME" "localhost" - "$SemanticAttributes.NET_HOST_PORT" port - "$SemanticAttributes.NET_SOCK_PEER_ADDR" "127.0.0.1" - "$SemanticAttributes.NET_SOCK_PEER_PORT" Long - "$SemanticAttributes.NET_SOCK_HOST_ADDR" "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) { - def route = "/$jspWebappContext/forwards/forwardToNonExistent.jsp" - - hasNoParent() - name "GET $route" - kind SERVER - status UNSET - attributes { - "$SemanticAttributes.HTTP_SCHEME" "http" - "$SemanticAttributes.HTTP_TARGET" route - "$SemanticAttributes.HTTP_METHOD" "GET" - "$SemanticAttributes.HTTP_STATUS_CODE" 404 - "$SemanticAttributes.USER_AGENT_ORIGINAL" String - "$SemanticAttributes.HTTP_ROUTE" route - "net.protocol.name" "http" - "net.protocol.version" "1.1" - "$SemanticAttributes.NET_HOST_NAME" "localhost" - "$SemanticAttributes.NET_HOST_PORT" port - "$SemanticAttributes.NET_SOCK_PEER_ADDR" "127.0.0.1" - "$SemanticAttributes.NET_SOCK_PEER_PORT" Long - "$SemanticAttributes.NET_SOCK_HOST_ADDR" "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/instrumentation/jsp-2.3/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/jsp/JspInstrumentationBasicTests.java b/instrumentation/jsp-2.3/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/jsp/JspInstrumentationBasicTests.java new file mode 100644 index 000000000000..2114818ce287 --- /dev/null +++ b/instrumentation/jsp-2.3/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/jsp/JspInstrumentationBasicTests.java @@ -0,0 +1,488 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jsp; + +import static io.opentelemetry.api.common.AttributeKey.stringKey; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.satisfies; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpServerUsingTest; +import io.opentelemetry.instrumentation.testing.junit.http.HttpServerInstrumentationExtension; +import io.opentelemetry.sdk.trace.data.StatusData; +import io.opentelemetry.semconv.ClientAttributes; +import io.opentelemetry.semconv.ExceptionAttributes; +import io.opentelemetry.semconv.HttpAttributes; +import io.opentelemetry.semconv.NetworkAttributes; +import io.opentelemetry.semconv.ServerAttributes; +import io.opentelemetry.semconv.UrlAttributes; +import io.opentelemetry.semconv.UserAgentAttributes; +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.io.File; +import java.nio.file.Files; +import java.util.stream.Stream; +import org.apache.catalina.startup.Tomcat; +import org.apache.jasper.JasperException; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.RegisterExtension; +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.junit.jupiter.params.provider.ValueSource; + +class JspInstrumentationBasicTests extends AbstractHttpServerUsingTest { + + @RegisterExtension + public static final InstrumentationExtension testing = + HttpServerInstrumentationExtension.forAgent(); + + private static JspSpanAssertions spanAsserts; + + @Override + protected Tomcat setupServer() throws Exception { + File baseDir = Files.createTempDirectory("jsp").toFile(); + baseDir.deleteOnExit(); + + Tomcat 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(); + + String baseUrl = "http://localhost:" + port + "/" + getContextPath(); + spanAsserts = new JspSpanAssertions(baseUrl, port); + client = WebClient.of(baseUrl); + + tomcatServer.addWebapp( + "/" + getContextPath(), + JspInstrumentationBasicTests.class.getResource("/webapps/jsptest").getPath()); + + tomcatServer.start(); + return tomcatServer; + } + + @Override + protected void stopServer(Tomcat tomcat) throws Exception { + tomcat.stop(); + tomcat.destroy(); + } + + @Override + protected String getContextPath() { + return "jsptest-context"; + } + + @BeforeAll + protected void setUp() { + startServer(); + } + + @AfterAll + protected void cleanUp() { + cleanupServer(); + } + + @ParameterizedTest(name = "GET {0}") + @ArgumentsSource(NonErroneousArgs.class) + void testNonErroneousGet( + String testName, String jspFileName, String jspClassName, String jspClassNamePrefix) { + AggregatedHttpResponse res = client.get(jspFileName).aggregate().join(); + assertThat(res.status().code()).isEqualTo(200); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + spanAsserts.assertServerSpan( + span, + new JspSpanAssertionBuilder() + .withMethod("GET") + .withRoute("/" + getContextPath() + jspFileName) + .withResponseStatus(200) + .build()), + span -> + spanAsserts.assertCompileSpan( + span, + new JspSpanAssertionBuilder() + .withParent(trace.getSpan(0)) + .withRoute(jspFileName) + .withClassName(jspClassNamePrefix + jspClassName) + .build()), + span -> + spanAsserts.assertRenderSpan( + span, + new JspSpanAssertionBuilder() + .withParent(trace.getSpan(0)) + .withRoute(jspFileName) + .build()))); + } + + static class NonErroneousArgs implements ArgumentsProvider { + @Override + public Stream provideArguments(ExtensionContext context) { + return Stream.of( + Arguments.of("no java jsp", "/nojava.jsp", "nojava_jsp", ""), + Arguments.of("basic loop jsp", "/common/loop.jsp", "loop_jsp", "common."), + Arguments.of("invalid HTML markup", "/invalidMarkup.jsp", "invalidMarkup_jsp", "")); + } + } + + @Test + void testNonErroneousGetWithQueryString() { + String queryString = "HELLO"; + String route = "/" + getContextPath() + "/getQuery.jsp"; + + AggregatedHttpResponse res = client.get("/getQuery.jsp?" + queryString).aggregate().join(); + assertThat(res.status().code()).isEqualTo(200); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("GET " + route) + .hasNoParent() + .hasKind(SpanKind.SERVER) + .hasAttributesSatisfyingExactly( + equalTo(UrlAttributes.URL_SCHEME, "http"), + equalTo(UrlAttributes.URL_PATH, route), + equalTo(UrlAttributes.URL_QUERY, queryString), + equalTo(HttpAttributes.HTTP_REQUEST_METHOD, "GET"), + equalTo(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, 200), + satisfies( + UserAgentAttributes.USER_AGENT_ORIGINAL, + val -> val.isInstanceOf(String.class)), + equalTo(HttpAttributes.HTTP_ROUTE, route), + equalTo(NetworkAttributes.NETWORK_PROTOCOL_VERSION, "1.1"), + equalTo(ServerAttributes.SERVER_ADDRESS, "localhost"), + equalTo(ServerAttributes.SERVER_PORT, port), + equalTo(ClientAttributes.CLIENT_ADDRESS, "127.0.0.1"), + equalTo(NetworkAttributes.NETWORK_PEER_ADDRESS, "127.0.0.1"), + satisfies( + NetworkAttributes.NETWORK_PEER_PORT, + val -> val.isInstanceOf(Long.class))), + span -> + spanAsserts.assertCompileSpan( + span, + new JspSpanAssertionBuilder() + .withParent(trace.getSpan(0)) + .withRoute("/getQuery.jsp") + .withClassName("getQuery_jsp") + .build()), + span -> + spanAsserts.assertRenderSpan( + span, + new JspSpanAssertionBuilder() + .withParent(trace.getSpan(0)) + .withRoute("/getQuery.jsp") + .build()))); + } + + @Test + void testNonErroneousPost() { + RequestHeaders headers = + RequestHeaders.builder(HttpMethod.POST, "/post.jsp") + .contentType(MediaType.FORM_DATA) + .build(); + + AggregatedHttpResponse res = client.execute(headers, "name=world").aggregate().join(); + assertThat(res.status().code()).isEqualTo(200); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + spanAsserts.assertServerSpan( + span, + new JspSpanAssertionBuilder() + .withMethod("POST") + .withRoute("/" + getContextPath() + "/post.jsp") + .withResponseStatus(200) + .build()), + span -> + spanAsserts.assertCompileSpan( + span, + new JspSpanAssertionBuilder() + .withParent(trace.getSpan(0)) + .withRoute("/post.jsp") + .withClassName("post_jsp") + .build()), + span -> + spanAsserts.assertRenderSpan( + span, + new JspSpanAssertionBuilder() + .withParent(trace.getSpan(0)) + .withRoute("/post.jsp") + .build()))); + } + + @ParameterizedTest(name = "GET jsp with {0}") + @ArgumentsSource(ErroneousRuntimeErrorsArgs.class) + void testErroneousRuntimeErrorsGet( + String testName, + String jspFileName, + String jspClassName, + Class exceptionClass, + boolean errorMessageOptional) { + AggregatedHttpResponse res = client.get(jspFileName).aggregate().join(); + assertThat(res.status().code()).isEqualTo(500); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + spanAsserts.assertServerSpan( + span, + new JspSpanAssertionBuilder() + .withMethod("GET") + .withRoute("/" + getContextPath() + jspFileName) + .withResponseStatus(500) + .withExceptionClass(exceptionClass) + .withErrorMessageOptional(errorMessageOptional) + .build()), + span -> + spanAsserts.assertCompileSpan( + span, + new JspSpanAssertionBuilder() + .withParent(trace.getSpan(0)) + .withRoute(jspFileName) + .withClassName(jspClassName) + .build()), + span -> + spanAsserts.assertRenderSpan( + span, + new JspSpanAssertionBuilder() + .withParent(trace.getSpan(0)) + .withRoute(jspFileName) + .withErrorMessageOptional(errorMessageOptional) + .build()))); + } + + static class ErroneousRuntimeErrorsArgs implements ArgumentsProvider { + @Override + public Stream provideArguments(ExtensionContext context) { + return Stream.of( + Arguments.of( + "java runtime error", + "/runtimeError.jsp", + "runtimeError_jsp", + ArithmeticException.class, + false), + Arguments.of( + "invalid write", + "/invalidWrite.jsp", + "invalidWrite_jsp", + IndexOutOfBoundsException.class, + true), + Arguments.of( + "invalid write", "/getQuery.jsp", "getQuery_jsp", NullPointerException.class, true)); + } + } + + @Test + void testNonErroneousIncludePlainHtmlGet() { + AggregatedHttpResponse res = client.get("/includes/includeHtml.jsp").aggregate().join(); + assertThat(res.status().code()).isEqualTo(200); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + spanAsserts.assertServerSpan( + span, + new JspSpanAssertionBuilder() + .withMethod("GET") + .withRoute("/" + getContextPath() + "/includes/includeHtml.jsp") + .withResponseStatus(200) + .build()), + span -> + spanAsserts.assertCompileSpan( + span, + new JspSpanAssertionBuilder() + .withParent(trace.getSpan(0)) + .withRoute("/includes/includeHtml.jsp") + .withClassName("includes.includeHtml_jsp") + .build()), + span -> + spanAsserts.assertRenderSpan( + span, + new JspSpanAssertionBuilder() + .withParent(trace.getSpan(0)) + .withRoute("/includes/includeHtml.jsp") + .build()))); + } + + @Test + void testNonErroneousMultiGet() { + AggregatedHttpResponse res = client.get("/includes/includeMulti.jsp").aggregate().join(); + assertThat(res.status().code()).isEqualTo(200); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + spanAsserts.assertServerSpan( + span, + new JspSpanAssertionBuilder() + .withMethod("GET") + .withRoute("/" + getContextPath() + "/includes/includeMulti.jsp") + .withResponseStatus(200) + .build()), + span -> + spanAsserts.assertCompileSpan( + span, + new JspSpanAssertionBuilder() + .withParent(trace.getSpan(0)) + .withRoute("/includes/includeMulti.jsp") + .withClassName("includes.includeMulti_jsp") + .build()), + span -> + spanAsserts.assertRenderSpan( + span, + new JspSpanAssertionBuilder() + .withParent(trace.getSpan(0)) + .withRoute("/includes/includeMulti.jsp") + .build()), + span -> + spanAsserts.assertCompileSpan( + span, + new JspSpanAssertionBuilder() + .withParent(trace.getSpan(2)) + .withRoute("/common/javaLoopH2.jsp") + .withClassName("common.javaLoopH2_jsp") + .build()), + span -> + spanAsserts.assertRenderSpan( + span, + new JspSpanAssertionBuilder() + .withParent(trace.getSpan(2)) + .withRoute("/common/javaLoopH2.jsp") + .withRequestUrlOverride("/includes/includeMulti.jsp") + .build()), + span -> + spanAsserts.assertCompileSpan( + span, + new JspSpanAssertionBuilder() + .withParent(trace.getSpan(2)) + .withRoute("/common/javaLoopH2.jsp") + .withClassName("common.javaLoopH2_jsp") + .build()), + span -> + spanAsserts.assertRenderSpan( + span, + new JspSpanAssertionBuilder() + .withParent(trace.getSpan(2)) + .withRoute("/common/javaLoopH2.jsp") + .withRequestUrlOverride("/includes/includeMulti.jsp") + .build()))); + } + + @ParameterizedTest + @ArgumentsSource(CompileErrorsArgs.class) + void testCompileErrorShouldNotProduceRenderTracesAndSpans( + String jspFileName, String jspClassName, String jspClassNamePrefix) { + AggregatedHttpResponse res = client.get(jspFileName).aggregate().join(); + assertThat(res.status().code()).isEqualTo(500); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + spanAsserts.assertServerSpan( + span, + new JspSpanAssertionBuilder() + .withMethod("GET") + .withRoute("/" + getContextPath() + jspFileName) + .withResponseStatus(500) + .withExceptionClass(JasperException.class) + .build()), + span -> + span.hasName("Compile " + jspFileName) + .hasParent(trace.getSpan(0)) + .hasStatus(StatusData.error()) + .hasEventsSatisfyingExactly( + event -> + event + .hasName("exception") + .hasAttributesSatisfyingExactly( + equalTo( + ExceptionAttributes.EXCEPTION_TYPE, + JasperException.class.getCanonicalName()), + satisfies( + ExceptionAttributes.EXCEPTION_STACKTRACE, + val -> val.isInstanceOf(String.class)), + satisfies( + ExceptionAttributes.EXCEPTION_MESSAGE, + val -> val.isInstanceOf(String.class)))) + .hasAttributesSatisfyingExactly( + equalTo( + stringKey("jsp.classFQCN"), + "org.apache.jsp." + jspClassNamePrefix + jspClassName), + equalTo( + stringKey("jsp.compiler"), + "org.apache.jasper.compiler.JDTCompiler")))); + } + + static class CompileErrorsArgs implements ArgumentsProvider { + @Override + public Stream provideArguments(ExtensionContext context) { + return Stream.of( + Arguments.of("/compileError.jsp", "compileError_jsp", ""), + Arguments.of( + "/forwards/forwardWithCompileError.jsp", "forwardWithCompileError_jsp", "forwards.")); + } + } + + @ParameterizedTest + @ValueSource(strings = {"/common/hello.html"}) + void testDirectStaticFileReference(String staticFile) { + String route = "/" + getContextPath() + "/*"; + + AggregatedHttpResponse res = client.get(staticFile).aggregate().join(); + assertThat(res.status().code()).isEqualTo(200); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("GET " + route) + .hasNoParent() + .hasKind(SpanKind.SERVER) + .hasAttributesSatisfyingExactly( + equalTo(UrlAttributes.URL_SCHEME, "http"), + equalTo(UrlAttributes.URL_PATH, "/" + getContextPath() + staticFile), + equalTo(HttpAttributes.HTTP_REQUEST_METHOD, "GET"), + equalTo(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, 200), + satisfies( + UserAgentAttributes.USER_AGENT_ORIGINAL, + val -> val.isInstanceOf(String.class)), + equalTo(HttpAttributes.HTTP_ROUTE, route), + equalTo(NetworkAttributes.NETWORK_PROTOCOL_VERSION, "1.1"), + equalTo(ServerAttributes.SERVER_ADDRESS, "localhost"), + equalTo(ServerAttributes.SERVER_PORT, port), + equalTo(ClientAttributes.CLIENT_ADDRESS, "127.0.0.1"), + equalTo(NetworkAttributes.NETWORK_PEER_ADDRESS, "127.0.0.1"), + satisfies( + NetworkAttributes.NETWORK_PEER_PORT, + val -> val.isInstanceOf(Long.class))))); + } +} diff --git a/instrumentation/jsp-2.3/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/jsp/JspInstrumentationForwardTests.java b/instrumentation/jsp-2.3/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/jsp/JspInstrumentationForwardTests.java new file mode 100644 index 000000000000..5ba8d5df7c49 --- /dev/null +++ b/instrumentation/jsp-2.3/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/jsp/JspInstrumentationForwardTests.java @@ -0,0 +1,454 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jsp; + +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.satisfies; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpServerUsingTest; +import io.opentelemetry.instrumentation.testing.junit.http.HttpServerInstrumentationExtension; +import io.opentelemetry.sdk.trace.data.StatusData; +import io.opentelemetry.semconv.ClientAttributes; +import io.opentelemetry.semconv.HttpAttributes; +import io.opentelemetry.semconv.NetworkAttributes; +import io.opentelemetry.semconv.ServerAttributes; +import io.opentelemetry.semconv.UrlAttributes; +import io.opentelemetry.semconv.UserAgentAttributes; +import io.opentelemetry.testing.internal.armeria.client.WebClient; +import io.opentelemetry.testing.internal.armeria.common.AggregatedHttpResponse; +import java.io.File; +import java.nio.file.Files; +import java.util.stream.Stream; +import org.apache.catalina.startup.Tomcat; +import org.apache.jasper.JasperException; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.RegisterExtension; +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; + +class JspInstrumentationForwardTests extends AbstractHttpServerUsingTest { + + @RegisterExtension + public static final InstrumentationExtension testing = + HttpServerInstrumentationExtension.forAgent(); + + private static JspSpanAssertions spanAsserts; + + @Override + protected Tomcat setupServer() throws Exception { + File baseDir = Files.createTempDirectory("jsp").toFile(); + baseDir.deleteOnExit(); + + Tomcat 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(); + + String baseUrl = "http://localhost:" + port + "/" + getContextPath(); + spanAsserts = new JspSpanAssertions(baseUrl, port); + client = WebClient.of(baseUrl); + + tomcatServer.addWebapp( + "/" + getContextPath(), + JspInstrumentationForwardTests.class.getResource("/webapps/jsptest").getPath()); + + tomcatServer.start(); + return tomcatServer; + } + + @Override + protected void stopServer(Tomcat tomcat) throws Exception { + tomcat.stop(); + tomcat.destroy(); + } + + @Override + protected String getContextPath() { + return "jsptest-context"; + } + + @BeforeAll + protected void setUp() { + startServer(); + } + + @AfterAll + protected void cleanUp() { + cleanupServer(); + } + + @ParameterizedTest(name = "Forward to {0}") + @ArgumentsSource(NonErroneousGetForwardArgs.class) + void testNonErroneousGetForwardTo( + String name, + String forwardFromFileName, + String forwardDestFileName, + String jspForwardFromClassName, + String jspForwardFromClassPrefix, + String jspForwardDestClassName, + String jspForwardDestClassPrefix) { + AggregatedHttpResponse res = client.get(forwardFromFileName).aggregate().join(); + assertThat(res.status().code()).isEqualTo(200); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + spanAsserts.assertServerSpan( + span, + new JspSpanAssertionBuilder() + .withMethod("GET") + .withRoute("/" + getContextPath() + forwardFromFileName) + .withResponseStatus(200) + .build()), + span -> + spanAsserts.assertCompileSpan( + span, + new JspSpanAssertionBuilder() + .withParent(trace.getSpan(0)) + .withRoute(forwardFromFileName) + .withClassName(jspForwardFromClassPrefix + jspForwardFromClassName) + .build()), + span -> + spanAsserts.assertRenderSpan( + span, + new JspSpanAssertionBuilder() + .withParent(trace.getSpan(0)) + .withRoute(forwardFromFileName) + .build()), + span -> + spanAsserts.assertCompileSpan( + span, + new JspSpanAssertionBuilder() + .withParent(trace.getSpan(2)) + .withRoute(forwardDestFileName) + .withClassName(jspForwardDestClassPrefix + jspForwardDestClassName) + .build()), + span -> + spanAsserts.assertRenderSpan( + span, + new JspSpanAssertionBuilder() + .withParent(trace.getSpan(2)) + .withRoute(forwardDestFileName) + .build()))); + } + + static class NonErroneousGetForwardArgs implements ArgumentsProvider { + @Override + public Stream provideArguments(ExtensionContext context) { + return Stream.of( + Arguments.of( + "no java jsp", + "/forwards/forwardToNoJavaJsp.jsp", + "/nojava.jsp", + "forwardToNoJavaJsp_jsp", + "forwards.", + "nojava_jsp", + ""), + Arguments.of( + "normal java jsp", + "/forwards/forwardToSimpleJava.jsp", + "/common/loop.jsp", + "forwardToSimpleJava_jsp", + "forwards.", + "loop_jsp", + "common.")); + } + } + + @Test + void testNonErroneousGetForwardToPlainHtml() { + AggregatedHttpResponse res = client.get("/forwards/forwardToHtml.jsp").aggregate().join(); + assertThat(res.status().code()).isEqualTo(200); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + spanAsserts.assertServerSpan( + span, + new JspSpanAssertionBuilder() + .withMethod("GET") + .withRoute("/" + getContextPath() + "/forwards/forwardToHtml.jsp") + .withResponseStatus(200) + .build()), + span -> + spanAsserts.assertCompileSpan( + span, + new JspSpanAssertionBuilder() + .withParent(trace.getSpan(0)) + .withRoute("/forwards/forwardToHtml.jsp") + .withClassName("forwards.forwardToHtml_jsp") + .build()), + span -> + spanAsserts.assertRenderSpan( + span, + new JspSpanAssertionBuilder() + .withParent(trace.getSpan(0)) + .withRoute("/forwards/forwardToHtml.jsp") + .build()))); + } + + @Test + void testNonErroneousGetForwardedToJspWithMultipleIncludes() { + AggregatedHttpResponse res = + client.get("/forwards/forwardToIncludeMulti.jsp").aggregate().join(); + assertThat(res.status().code()).isEqualTo(200); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + spanAsserts.assertServerSpan( + span, + new JspSpanAssertionBuilder() + .withMethod("GET") + .withRoute( + "/" + getContextPath() + "/forwards/forwardToIncludeMulti.jsp") + .withResponseStatus(200) + .build()), + span -> + spanAsserts.assertCompileSpan( + span, + new JspSpanAssertionBuilder() + .withParent(trace.getSpan(0)) + .withRoute("/forwards/forwardToIncludeMulti.jsp") + .withClassName("forwards.forwardToIncludeMulti_jsp") + .build()), + span -> + spanAsserts.assertRenderSpan( + span, + new JspSpanAssertionBuilder() + .withParent(trace.getSpan(0)) + .withRoute("/forwards/forwardToIncludeMulti.jsp") + .build()), + span -> + spanAsserts.assertCompileSpan( + span, + new JspSpanAssertionBuilder() + .withParent(trace.getSpan(2)) + .withRoute("/includes/includeMulti.jsp") + .withClassName("includes.includeMulti_jsp") + .build()), + span -> + spanAsserts.assertRenderSpan( + span, + new JspSpanAssertionBuilder() + .withParent(trace.getSpan(2)) + .withRoute("/includes/includeMulti.jsp") + .build()), + span -> + spanAsserts.assertCompileSpan( + span, + new JspSpanAssertionBuilder() + .withParent(trace.getSpan(4)) + .withRoute("/common/javaLoopH2.jsp") + .withClassName("common.javaLoopH2_jsp") + .build()), + span -> + spanAsserts.assertRenderSpan( + span, + new JspSpanAssertionBuilder() + .withParent(trace.getSpan(4)) + .withRoute("/common/javaLoopH2.jsp") + .withRequestUrlOverride("/includes/includeMulti.jsp") + .withForwardOrigin("/forwards/forwardToIncludeMulti.jsp") + .build()), + span -> + spanAsserts.assertCompileSpan( + span, + new JspSpanAssertionBuilder() + .withParent(trace.getSpan(4)) + .withRoute("/common/javaLoopH2.jsp") + .withClassName("common.javaLoopH2_jsp") + .build()), + span -> + spanAsserts.assertRenderSpan( + span, + new JspSpanAssertionBuilder() + .withParent(trace.getSpan(4)) + .withRoute("/common/javaLoopH2.jsp") + .withRequestUrlOverride("/includes/includeMulti.jsp") + .withForwardOrigin("/forwards/forwardToIncludeMulti.jsp") + .build()))); + } + + @Test + void testNonErroneousGetForwardToAnotherForward() { + AggregatedHttpResponse res = client.get("/forwards/forwardToJspForward.jsp").aggregate().join(); + assertThat(res.status().code()).isEqualTo(200); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + spanAsserts.assertServerSpan( + span, + new JspSpanAssertionBuilder() + .withMethod("GET") + .withRoute("/" + getContextPath() + "/forwards/forwardToJspForward.jsp") + .withResponseStatus(200) + .build()), + span -> + spanAsserts.assertCompileSpan( + span, + new JspSpanAssertionBuilder() + .withParent(trace.getSpan(0)) + .withRoute("/forwards/forwardToJspForward.jsp") + .withClassName("forwards.forwardToJspForward_jsp") + .build()), + span -> + spanAsserts.assertRenderSpan( + span, + new JspSpanAssertionBuilder() + .withParent(trace.getSpan(0)) + .withRoute("/forwards/forwardToJspForward.jsp") + .build()), + span -> + spanAsserts.assertCompileSpan( + span, + new JspSpanAssertionBuilder() + .withParent(trace.getSpan(2)) + .withRoute("/forwards/forwardToSimpleJava.jsp") + .withClassName("forwards.forwardToSimpleJava_jsp") + .build()), + span -> + spanAsserts.assertRenderSpan( + span, + new JspSpanAssertionBuilder() + .withParent(trace.getSpan(2)) + .withRoute("/forwards/forwardToSimpleJava.jsp") + .build()), + span -> + spanAsserts.assertCompileSpan( + span, + new JspSpanAssertionBuilder() + .withParent(trace.getSpan(4)) + .withRoute("/common/loop.jsp") + .withClassName("common.loop_jsp") + .build()), + span -> + spanAsserts.assertRenderSpan( + span, + new JspSpanAssertionBuilder() + .withParent(trace.getSpan(4)) + .withRoute("/common/loop.jsp") + .build()))); + } + + @Test + void testForwardToJspWithCompileErrorShouldNotProduceSecondRenderSpan() { + AggregatedHttpResponse res = + client.get("/forwards/forwardToCompileError.jsp").aggregate().join(); + assertThat(res.status().code()).isEqualTo(500); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + spanAsserts.assertServerSpan( + span, + new JspSpanAssertionBuilder() + .withMethod("GET") + .withRoute( + "/" + getContextPath() + "/forwards/forwardToCompileError.jsp") + .withResponseStatus(500) + .withExceptionClass(JasperException.class) + .build()), + span -> + spanAsserts.assertCompileSpan( + span, + new JspSpanAssertionBuilder() + .withParent(trace.getSpan(0)) + .withRoute("/forwards/forwardToCompileError.jsp") + .withClassName("forwards.forwardToCompileError_jsp") + .build()), + span -> + spanAsserts.assertRenderSpan( + span, + new JspSpanAssertionBuilder() + .withParent(trace.getSpan(0)) + .withRoute("/forwards/forwardToCompileError.jsp") + .withExceptionClass(JasperException.class) + .build()), + span -> + spanAsserts.assertCompileSpan( + span, + new JspSpanAssertionBuilder() + .withParent(trace.getSpan(2)) + .withRoute("/compileError.jsp") + .withClassName("compileError_jsp") + .withExceptionClass(JasperException.class) + .build()))); + } + + @Test + void testForwardToNonExistentJspShouldBe404() { + String route = "/" + getContextPath() + "/forwards/forwardToNonExistent.jsp"; + + AggregatedHttpResponse res = + client.get("/forwards/forwardToNonExistent.jsp").aggregate().join(); + assertThat(res.status().code()).isEqualTo(404); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("GET " + route) + .hasNoParent() + .hasKind(SpanKind.SERVER) + .hasStatus(StatusData.unset()) + .hasAttributesSatisfyingExactly( + equalTo(UrlAttributes.URL_SCHEME, "http"), + equalTo(UrlAttributes.URL_PATH, route), + equalTo(HttpAttributes.HTTP_REQUEST_METHOD, "GET"), + equalTo(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, 404), + satisfies( + UserAgentAttributes.USER_AGENT_ORIGINAL, + val -> val.isInstanceOf(String.class)), + equalTo(HttpAttributes.HTTP_ROUTE, route), + equalTo(NetworkAttributes.NETWORK_PROTOCOL_VERSION, "1.1"), + equalTo(ServerAttributes.SERVER_ADDRESS, "localhost"), + equalTo(ServerAttributes.SERVER_PORT, port), + equalTo(ClientAttributes.CLIENT_ADDRESS, "127.0.0.1"), + equalTo(NetworkAttributes.NETWORK_PEER_ADDRESS, "127.0.0.1"), + satisfies( + NetworkAttributes.NETWORK_PEER_PORT, + val -> val.isInstanceOf(Long.class))), + span -> + spanAsserts.assertCompileSpan( + span, + new JspSpanAssertionBuilder() + .withParent(trace.getSpan(0)) + .withRoute("/forwards/forwardToNonExistent.jsp") + .withClassName("forwards.forwardToNonExistent_jsp") + .build()), + span -> + spanAsserts.assertRenderSpan( + span, + new JspSpanAssertionBuilder() + .withParent(trace.getSpan(0)) + .withRoute("/forwards/forwardToNonExistent.jsp") + .build()), + span -> span.hasName("ResponseFacade.sendError").hasParent(trace.getSpan(2)))); + } +} diff --git a/instrumentation/jsp-2.3/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/jsp/JspSpan.java b/instrumentation/jsp-2.3/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/jsp/JspSpan.java new file mode 100644 index 000000000000..094b047087d0 --- /dev/null +++ b/instrumentation/jsp-2.3/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/jsp/JspSpan.java @@ -0,0 +1,92 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jsp; + +import io.opentelemetry.sdk.trace.data.SpanData; + +class JspSpan { + private SpanData parent; + private String method; + private String className; + private String requestUrlOverride; + private String forwardOrigin; + private String route; + private int responseStatus; + private Class exceptionClass; + private boolean errorMessageOptional; + + public SpanData getParent() { + return parent; + } + + public void setParent(SpanData parent) { + this.parent = parent; + } + + public String getMethod() { + return method; + } + + public void setMethod(String method) { + this.method = method; + } + + public String getClassName() { + return className; + } + + public void setClassName(String className) { + this.className = className; + } + + public String getRequestUrlOverride() { + return requestUrlOverride; + } + + public void setRequestUrlOverride(String requestUrlOverride) { + this.requestUrlOverride = requestUrlOverride; + } + + public String getForwardOrigin() { + return forwardOrigin; + } + + public void setForwardOrigin(String forwardOrigin) { + this.forwardOrigin = forwardOrigin; + } + + public String getRoute() { + return route; + } + + public void setRoute(String route) { + this.route = route; + } + + public int getResponseStatus() { + return responseStatus; + } + + public void setResponseStatus(int responseStatus) { + this.responseStatus = responseStatus; + } + + public Class getExceptionClass() { + return exceptionClass; + } + + public void setExceptionClass(Class exceptionClass) { + this.exceptionClass = exceptionClass; + } + + public boolean getErrorMessageOptional() { + return errorMessageOptional; + } + + public void setErrorMessageOptional(boolean errorMessageOptional) { + this.errorMessageOptional = errorMessageOptional; + } +} diff --git a/instrumentation/jsp-2.3/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/jsp/JspSpanAssertionBuilder.java b/instrumentation/jsp-2.3/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/jsp/JspSpanAssertionBuilder.java new file mode 100644 index 000000000000..b24f592495c4 --- /dev/null +++ b/instrumentation/jsp-2.3/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/jsp/JspSpanAssertionBuilder.java @@ -0,0 +1,79 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jsp; + +import io.opentelemetry.sdk.trace.data.SpanData; + +class JspSpanAssertionBuilder { + private SpanData parent; + private String method; + private String route; + private String className; + private String requestUrlOverride; + private String forwardOrigin; + private int responseStatus; + private Class exceptionClass; + private boolean errorMessageOptional; + + public JspSpanAssertionBuilder withParent(SpanData parent) { + this.parent = parent; + return this; + } + + public JspSpanAssertionBuilder withMethod(String method) { + this.method = method; + return this; + } + + public JspSpanAssertionBuilder withRoute(String route) { + this.route = route; + return this; + } + + public JspSpanAssertionBuilder withClassName(String className) { + this.className = className; + return this; + } + + public JspSpanAssertionBuilder withRequestUrlOverride(String requestUrlOverride) { + this.requestUrlOverride = requestUrlOverride; + return this; + } + + public JspSpanAssertionBuilder withForwardOrigin(String forwardOrigin) { + this.forwardOrigin = forwardOrigin; + return this; + } + + public JspSpanAssertionBuilder withResponseStatus(int responseStatus) { + this.responseStatus = responseStatus; + return this; + } + + public JspSpanAssertionBuilder withExceptionClass(Class exceptionClass) { + this.exceptionClass = exceptionClass; + return this; + } + + public JspSpanAssertionBuilder withErrorMessageOptional(boolean errorMessageOptional) { + this.errorMessageOptional = errorMessageOptional; + return this; + } + + public JspSpan build() { + JspSpan serverSpan = new JspSpan(); + serverSpan.setParent(this.parent); + serverSpan.setMethod(this.method); + serverSpan.setRoute(this.route); + serverSpan.setClassName(this.className); + serverSpan.setRequestUrlOverride(this.requestUrlOverride); + serverSpan.setForwardOrigin(this.forwardOrigin); + serverSpan.setResponseStatus(this.responseStatus); + serverSpan.setExceptionClass(this.exceptionClass); + serverSpan.setErrorMessageOptional(this.errorMessageOptional); + return serverSpan; + } +} diff --git a/instrumentation/jsp-2.3/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/jsp/JspSpanAssertions.java b/instrumentation/jsp-2.3/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/jsp/JspSpanAssertions.java new file mode 100644 index 000000000000..7b9baa32c697 --- /dev/null +++ b/instrumentation/jsp-2.3/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/jsp/JspSpanAssertions.java @@ -0,0 +1,155 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jsp; + +import static io.opentelemetry.api.common.AttributeKey.stringKey; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.satisfies; +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.sdk.testing.assertj.SpanDataAssert; +import io.opentelemetry.sdk.trace.data.StatusData; +import io.opentelemetry.semconv.ClientAttributes; +import io.opentelemetry.semconv.ErrorAttributes; +import io.opentelemetry.semconv.ExceptionAttributes; +import io.opentelemetry.semconv.HttpAttributes; +import io.opentelemetry.semconv.NetworkAttributes; +import io.opentelemetry.semconv.ServerAttributes; +import io.opentelemetry.semconv.UrlAttributes; +import io.opentelemetry.semconv.UserAgentAttributes; + +class JspSpanAssertions { + private final String baseUrl; + private final int port; + + JspSpanAssertions(String baseUrl, int port) { + this.baseUrl = baseUrl; + this.port = port; + } + + SpanDataAssert assertServerSpan(SpanDataAssert span, JspSpan spanData) { + if (spanData.getExceptionClass() != null) { + span.hasStatus(StatusData.error()) + .hasEventsSatisfyingExactly( + event -> + event + .hasName("exception") + .hasAttributesSatisfyingExactly( + satisfies( + ExceptionAttributes.EXCEPTION_TYPE, + val -> + val.satisfiesAnyOf( + v -> val.isEqualTo(spanData.getExceptionClass().getName()), + v -> + val.contains( + spanData.getExceptionClass().getSimpleName()))), + satisfies( + ExceptionAttributes.EXCEPTION_MESSAGE, + val -> + val.satisfiesAnyOf( + v -> assertThat(spanData.getErrorMessageOptional()).isTrue(), + v -> val.isInstanceOf(String.class))), + satisfies( + ExceptionAttributes.EXCEPTION_STACKTRACE, + val -> val.isInstanceOf(String.class)))); + } + + return span.hasName(spanData.getMethod() + " " + spanData.getRoute()) + .hasNoParent() + .hasKind(SpanKind.SERVER) + .hasAttributesSatisfyingExactly( + equalTo(UrlAttributes.URL_SCHEME, "http"), + equalTo(UrlAttributes.URL_PATH, spanData.getRoute()), + equalTo(HttpAttributes.HTTP_REQUEST_METHOD, spanData.getMethod()), + equalTo(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, spanData.getResponseStatus()), + satisfies( + UserAgentAttributes.USER_AGENT_ORIGINAL, val -> val.isInstanceOf(String.class)), + equalTo(HttpAttributes.HTTP_ROUTE, spanData.getRoute()), + equalTo(NetworkAttributes.NETWORK_PROTOCOL_VERSION, "1.1"), + equalTo(ServerAttributes.SERVER_ADDRESS, "localhost"), + equalTo(ServerAttributes.SERVER_PORT, port), + equalTo(ClientAttributes.CLIENT_ADDRESS, "127.0.0.1"), + equalTo(NetworkAttributes.NETWORK_PEER_ADDRESS, "127.0.0.1"), + satisfies(NetworkAttributes.NETWORK_PEER_PORT, val -> val.isInstanceOf(Long.class)), + satisfies( + ErrorAttributes.ERROR_TYPE, + val -> + val.satisfiesAnyOf( + v -> assertThat(spanData.getExceptionClass()).isNull(), + v -> assertThat(v).isEqualTo("500")))); + } + + SpanDataAssert assertCompileSpan(SpanDataAssert span, JspSpan spanData) { + if (spanData.getExceptionClass() != null) { + span.hasStatus(StatusData.error()) + .hasEventsSatisfyingExactly( + event -> + event + .hasName("exception") + .hasAttributesSatisfyingExactly( + equalTo( + ExceptionAttributes.EXCEPTION_TYPE, + spanData.getExceptionClass().getCanonicalName()), + satisfies( + ExceptionAttributes.EXCEPTION_STACKTRACE, + val -> val.isInstanceOf(String.class)), + satisfies( + ExceptionAttributes.EXCEPTION_MESSAGE, + val -> val.isInstanceOf(String.class)))); + } + + return span.hasName("Compile " + spanData.getRoute()) + .hasParent(spanData.getParent()) + .hasAttributesSatisfyingExactly( + equalTo(stringKey("jsp.classFQCN"), "org.apache.jsp." + spanData.getClassName()), + equalTo(stringKey("jsp.compiler"), "org.apache.jasper.compiler.JDTCompiler")); + } + + SpanDataAssert assertRenderSpan(SpanDataAssert span, JspSpan spanData) { + String requestUrl = spanData.getRoute(); + if (spanData.getRequestUrlOverride() != null) { + requestUrl = spanData.getRequestUrlOverride(); + } + + if (spanData.getExceptionClass() != null) { + span.hasStatus(StatusData.error()) + .hasEventsSatisfyingExactly( + event -> + event + .hasName("exception") + .hasAttributesSatisfyingExactly( + satisfies( + ExceptionAttributes.EXCEPTION_TYPE, + val -> + val.satisfiesAnyOf( + v -> val.isEqualTo(spanData.getExceptionClass().getName()), + v -> + val.contains( + spanData.getExceptionClass().getSimpleName()))), + satisfies( + ExceptionAttributes.EXCEPTION_MESSAGE, + val -> + val.satisfiesAnyOf( + v -> assertThat(spanData.getErrorMessageOptional()).isTrue(), + v -> val.isInstanceOf(String.class))), + satisfies( + ExceptionAttributes.EXCEPTION_STACKTRACE, + val -> val.isInstanceOf(String.class)))); + } + + return span.hasName("Render " + spanData.getRoute()) + .hasParent(spanData.getParent()) + .hasAttributesSatisfyingExactly( + equalTo(stringKey("jsp.requestURL"), baseUrl + requestUrl), + satisfies( + stringKey("jsp.forwardOrigin"), + val -> + val.satisfiesAnyOf( + v -> assertThat(spanData.getForwardOrigin()).isNull(), + v -> assertThat(v).isEqualTo(spanData.getForwardOrigin())))); + } +} diff --git a/instrumentation/jsp-2.3/javaagent/src/test/resources/webapps/jsptest/common/hello.html b/instrumentation/jsp-2.3/javaagent/src/test/resources/webapps/jsptest/common/hello.html index 48dd2eff1cb4..c4945710cdb6 100644 --- a/instrumentation/jsp-2.3/javaagent/src/test/resources/webapps/jsptest/common/hello.html +++ b/instrumentation/jsp-2.3/javaagent/src/test/resources/webapps/jsptest/common/hello.html @@ -1,9 +1,9 @@ - - PLAIN HTML - + + PLAIN HTML + - -

HELLO!

- + +

HELLO!

+ diff --git a/instrumentation/kafka/README.md b/instrumentation/kafka/README.md new file mode 100644 index 000000000000..858a8bd8c671 --- /dev/null +++ b/instrumentation/kafka/README.md @@ -0,0 +1,7 @@ +# Settings for the Kafka instrumentation + +| System property | Type | Default | Description | +|-----------------------------------------------------------| ------- |---------|--------------------------------------------------------------------------------------------------------------------------------| +| `otel.instrumentation.kafka.experimental-span-attributes` | Boolean | `false` | Enable the capture of experimental span attributes. | +| `otel.instrumentation.kafka.producer-propagation.enabled` | Boolean | `true` | Enable context propagation for kafka message producer. | +| `otel.instrumentation.kafka.metric-reporter.enabled` | Boolean | `true` | Enable kafka consumer and producer metrics. **Deprecated**, disable instrumentation with name `kafka-clients-metrics` instead. | diff --git a/instrumentation/kafka/kafka-clients/kafka-clients-0.11/bootstrap/src/main/java/io/opentelemetry/javaagent/bootstrap/kafka/KafkaClientsConsumerProcessTracing.java b/instrumentation/kafka/kafka-clients/kafka-clients-0.11/bootstrap/src/main/java/io/opentelemetry/javaagent/bootstrap/kafka/KafkaClientsConsumerProcessTracing.java index 65e4c36c7696..b93ac873a025 100644 --- a/instrumentation/kafka/kafka-clients/kafka-clients-0.11/bootstrap/src/main/java/io/opentelemetry/javaagent/bootstrap/kafka/KafkaClientsConsumerProcessTracing.java +++ b/instrumentation/kafka/kafka-clients/kafka-clients-0.11/bootstrap/src/main/java/io/opentelemetry/javaagent/bootstrap/kafka/KafkaClientsConsumerProcessTracing.java @@ -5,6 +5,8 @@ package io.opentelemetry.javaagent.bootstrap.kafka; +import java.util.function.BooleanSupplier; + // Classes used by multiple instrumentations should be in a bootstrap module to ensure that all // instrumentations see the same class. Helper classes are injected into each class loader that // contains an instrumentation that uses them, so instrumentations in different class loaders will @@ -23,4 +25,8 @@ public static boolean setEnabled(boolean enabled) { public static boolean wrappingEnabled() { return wrappingEnabled.get(); } + + public static BooleanSupplier wrappingEnabledSupplier() { + return KafkaClientsConsumerProcessTracing::wrappingEnabled; + } } diff --git a/instrumentation/kafka/kafka-clients/kafka-clients-0.11/javaagent/build.gradle.kts b/instrumentation/kafka/kafka-clients/kafka-clients-0.11/javaagent/build.gradle.kts index 7cee5bd89a96..fd68ec78e48a 100644 --- a/instrumentation/kafka/kafka-clients/kafka-clients-0.11/javaagent/build.gradle.kts +++ b/instrumentation/kafka/kafka-clients/kafka-clients-0.11/javaagent/build.gradle.kts @@ -8,6 +8,7 @@ muzzle { module.set("kafka-clients") versions.set("[0.11.0.0,)") assertInverse.set(true) + excludeInstrumentationName("kafka-clients-metrics") } } diff --git a/instrumentation/kafka/kafka-clients/kafka-clients-0.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kafkaclients/v0_11/ConsumerRecordsInstrumentation.java b/instrumentation/kafka/kafka-clients/kafka-clients-0.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kafkaclients/v0_11/ConsumerRecordsInstrumentation.java index 5ba44989bedc..c65618306340 100644 --- a/instrumentation/kafka/kafka-clients/kafka-clients-0.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kafkaclients/v0_11/ConsumerRecordsInstrumentation.java +++ b/instrumentation/kafka/kafka-clients/kafka-clients-0.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kafkaclients/v0_11/ConsumerRecordsInstrumentation.java @@ -5,6 +5,8 @@ package io.opentelemetry.javaagent.instrumentation.kafkaclients.v0_11; +import static io.opentelemetry.javaagent.bootstrap.kafka.KafkaClientsConsumerProcessTracing.wrappingEnabledSupplier; +import static io.opentelemetry.javaagent.instrumentation.kafkaclients.v0_11.KafkaSingletons.consumerProcessInstrumenter; import static net.bytebuddy.matcher.ElementMatchers.isMethod; import static net.bytebuddy.matcher.ElementMatchers.isPublic; import static net.bytebuddy.matcher.ElementMatchers.named; @@ -14,6 +16,9 @@ import io.opentelemetry.instrumentation.kafka.internal.KafkaConsumerContext; import io.opentelemetry.instrumentation.kafka.internal.KafkaConsumerContextUtil; +import io.opentelemetry.instrumentation.kafka.internal.TracingIterable; +import io.opentelemetry.instrumentation.kafka.internal.TracingIterator; +import io.opentelemetry.instrumentation.kafka.internal.TracingList; import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; import java.util.Iterator; @@ -70,7 +75,9 @@ public static void wrap( // case it's important to overwrite the leaked span instead of suppressing the correct span // (https://github.com/open-telemetry/opentelemetry-java-instrumentation/issues/1947) KafkaConsumerContext consumerContext = KafkaConsumerContextUtil.get(records); - iterable = TracingIterable.wrap(iterable, consumerContext); + iterable = + TracingIterable.wrap( + iterable, consumerProcessInstrumenter(), wrappingEnabledSupplier(), consumerContext); } } @@ -88,7 +95,9 @@ public static void wrap( // case it's important to overwrite the leaked span instead of suppressing the correct span // (https://github.com/open-telemetry/opentelemetry-java-instrumentation/issues/1947) KafkaConsumerContext consumerContext = KafkaConsumerContextUtil.get(records); - list = TracingList.wrap(list, consumerContext); + list = + TracingList.wrap( + list, consumerProcessInstrumenter(), wrappingEnabledSupplier(), consumerContext); } } @@ -106,7 +115,9 @@ public static void wrap( // case it's important to overwrite the leaked span instead of suppressing the correct span // (https://github.com/open-telemetry/opentelemetry-java-instrumentation/issues/1947) KafkaConsumerContext consumerContext = KafkaConsumerContextUtil.get(records); - iterator = TracingIterator.wrap(iterator, consumerContext); + iterator = + TracingIterator.wrap( + iterator, consumerProcessInstrumenter(), wrappingEnabledSupplier(), consumerContext); } } } diff --git a/instrumentation/kafka/kafka-clients/kafka-clients-0.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kafkaclients/v0_11/KafkaConsumerInstrumentation.java b/instrumentation/kafka/kafka-clients/kafka-clients-0.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kafkaclients/v0_11/KafkaConsumerInstrumentation.java index dc5c94b69c1f..77426fe970fd 100644 --- a/instrumentation/kafka/kafka-clients/kafka-clients-0.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kafkaclients/v0_11/KafkaConsumerInstrumentation.java +++ b/instrumentation/kafka/kafka-clients/kafka-clients-0.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kafkaclients/v0_11/KafkaConsumerInstrumentation.java @@ -7,8 +7,6 @@ import static io.opentelemetry.javaagent.bootstrap.Java8BytecodeBridge.currentContext; import static io.opentelemetry.javaagent.instrumentation.kafkaclients.v0_11.KafkaSingletons.consumerReceiveInstrumenter; -import static io.opentelemetry.javaagent.instrumentation.kafkaclients.v0_11.KafkaSingletons.enhanceConfig; -import static net.bytebuddy.matcher.ElementMatchers.isConstructor; import static net.bytebuddy.matcher.ElementMatchers.isPublic; import static net.bytebuddy.matcher.ElementMatchers.named; import static net.bytebuddy.matcher.ElementMatchers.returns; @@ -24,8 +22,6 @@ import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; import java.time.Duration; -import java.util.Map; -import java.util.Properties; import net.bytebuddy.asm.Advice; import net.bytebuddy.description.type.TypeDescription; import net.bytebuddy.matcher.ElementMatcher; @@ -42,12 +38,6 @@ public ElementMatcher typeMatcher() { @Override public void transform(TypeTransformer transformer) { - transformer.applyAdviceToMethod( - isConstructor().and(takesArgument(0, Map.class)), - this.getClass().getName() + "$ConstructorMapAdvice"); - transformer.applyAdviceToMethod( - isConstructor().and(takesArgument(0, Properties.class)), - this.getClass().getName() + "$ConstructorPropertiesAdvice"); transformer.applyAdviceToMethod( named("poll") .and(isPublic()) @@ -57,24 +47,6 @@ public void transform(TypeTransformer transformer) { this.getClass().getName() + "$PollAdvice"); } - @SuppressWarnings("unused") - public static class ConstructorMapAdvice { - - @Advice.OnMethodEnter(suppress = Throwable.class) - public static void onEnter(@Advice.Argument(0) Map config) { - enhanceConfig(config); - } - } - - @SuppressWarnings("unused") - public static class ConstructorPropertiesAdvice { - - @Advice.OnMethodEnter(suppress = Throwable.class) - public static void onEnter(@Advice.Argument(0) Properties config) { - enhanceConfig(config); - } - } - @SuppressWarnings("unused") public static class PollAdvice { @Advice.OnMethodEnter(suppress = Throwable.class) @@ -116,7 +88,7 @@ public static void onExit( // we're storing the context of the receive span so that process spans can use it as // parent context even though the span has ended // this is the suggested behavior according to the spec batch receive scenario: - // https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/messaging.md#batch-receiving + // https://github.com/open-telemetry/semantic-conventions/blob/main/docs/messaging/messaging-spans.md#batch-receiving // we're attaching the consumer to the records to be able to retrieve things like consumer // group or clientId later KafkaConsumerContextUtil.set(records, context, consumer); diff --git a/instrumentation/kafka/kafka-clients/kafka-clients-0.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kafkaclients/v0_11/KafkaProducerInstrumentation.java b/instrumentation/kafka/kafka-clients/kafka-clients-0.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kafkaclients/v0_11/KafkaProducerInstrumentation.java index 4ce176d3897c..1e1b7bf1d34f 100644 --- a/instrumentation/kafka/kafka-clients/kafka-clients-0.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kafkaclients/v0_11/KafkaProducerInstrumentation.java +++ b/instrumentation/kafka/kafka-clients/kafka-clients-0.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kafkaclients/v0_11/KafkaProducerInstrumentation.java @@ -5,9 +5,7 @@ package io.opentelemetry.javaagent.instrumentation.kafkaclients.v0_11; -import static io.opentelemetry.javaagent.instrumentation.kafkaclients.v0_11.KafkaSingletons.enhanceConfig; import static io.opentelemetry.javaagent.instrumentation.kafkaclients.v0_11.KafkaSingletons.producerInstrumenter; -import static net.bytebuddy.matcher.ElementMatchers.isConstructor; import static net.bytebuddy.matcher.ElementMatchers.isMethod; import static net.bytebuddy.matcher.ElementMatchers.isPublic; import static net.bytebuddy.matcher.ElementMatchers.named; @@ -20,8 +18,6 @@ import io.opentelemetry.javaagent.bootstrap.Java8BytecodeBridge; import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; -import java.util.Map; -import java.util.Properties; import net.bytebuddy.asm.Advice; import net.bytebuddy.description.type.TypeDescription; import net.bytebuddy.matcher.ElementMatcher; @@ -38,12 +34,6 @@ public ElementMatcher typeMatcher() { @Override public void transform(TypeTransformer transformer) { - transformer.applyAdviceToMethod( - isConstructor().and(takesArgument(0, Map.class)), - this.getClass().getName() + "$ConstructorMapAdvice"); - transformer.applyAdviceToMethod( - isConstructor().and(takesArgument(0, Properties.class)), - this.getClass().getName() + "$ConstructorPropertiesAdvice"); transformer.applyAdviceToMethod( isMethod() .and(isPublic()) @@ -53,24 +43,6 @@ public void transform(TypeTransformer transformer) { KafkaProducerInstrumentation.class.getName() + "$SendAdvice"); } - @SuppressWarnings("unused") - public static class ConstructorMapAdvice { - - @Advice.OnMethodEnter(suppress = Throwable.class) - public static void onEnter(@Advice.Argument(0) Map config) { - enhanceConfig(config); - } - } - - @SuppressWarnings("unused") - public static class ConstructorPropertiesAdvice { - - @Advice.OnMethodEnter(suppress = Throwable.class) - public static void onEnter(@Advice.Argument(0) Properties config) { - enhanceConfig(config); - } - } - @SuppressWarnings("unused") public static class SendAdvice { diff --git a/instrumentation/kafka/kafka-clients/kafka-clients-0.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kafkaclients/v0_11/KafkaSingletons.java b/instrumentation/kafka/kafka-clients/kafka-clients-0.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kafkaclients/v0_11/KafkaSingletons.java index 0d79da42c454..bde04943f621 100644 --- a/instrumentation/kafka/kafka-clients/kafka-clients-0.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kafkaclients/v0_11/KafkaSingletons.java +++ b/instrumentation/kafka/kafka-clients/kafka-clients-0.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kafkaclients/v0_11/KafkaSingletons.java @@ -11,32 +11,16 @@ import io.opentelemetry.instrumentation.kafka.internal.KafkaProcessRequest; import io.opentelemetry.instrumentation.kafka.internal.KafkaProducerRequest; import io.opentelemetry.instrumentation.kafka.internal.KafkaReceiveRequest; -import io.opentelemetry.instrumentation.kafka.internal.OpenTelemetryMetricsReporter; -import io.opentelemetry.instrumentation.kafka.internal.OpenTelemetrySupplier; -import io.opentelemetry.javaagent.bootstrap.internal.DeprecatedConfigProperties; +import io.opentelemetry.javaagent.bootstrap.internal.AgentInstrumentationConfig; import io.opentelemetry.javaagent.bootstrap.internal.ExperimentalConfig; -import io.opentelemetry.javaagent.bootstrap.internal.InstrumentationConfig; -import java.util.Map; -import java.util.regex.Pattern; -import org.apache.kafka.clients.CommonClientConfigs; import org.apache.kafka.clients.producer.RecordMetadata; public final class KafkaSingletons { private static final String INSTRUMENTATION_NAME = "io.opentelemetry.kafka-clients-0.11"; private static final boolean PRODUCER_PROPAGATION_ENABLED = - DeprecatedConfigProperties.getBoolean( - InstrumentationConfig.get(), - "otel.instrumentation.kafka.client-propagation.enabled", - "otel.instrumentation.kafka.producer-propagation.enabled", - true); - private static final boolean METRICS_ENABLED = - InstrumentationConfig.get() - .getBoolean("otel.instrumentation.kafka.metric-reporter.enabled", true); - - private static final Pattern METRIC_REPORTER_PRESENT_PATTERN = - Pattern.compile( - "(^|,)" + Pattern.quote(OpenTelemetryMetricsReporter.class.getName()) + "($|,)"); + AgentInstrumentationConfig.get() + .getBoolean("otel.instrumentation.kafka.producer-propagation.enabled", true); private static final Instrumenter PRODUCER_INSTRUMENTER; private static final Instrumenter CONSUMER_RECEIVE_INSTRUMENTER; @@ -47,7 +31,7 @@ public final class KafkaSingletons { new KafkaInstrumenterFactory(GlobalOpenTelemetry.get(), INSTRUMENTATION_NAME) .setCapturedHeaders(ExperimentalConfig.get().getMessagingHeaders()) .setCaptureExperimentalSpanAttributes( - InstrumentationConfig.get() + AgentInstrumentationConfig.get() .getBoolean("otel.instrumentation.kafka.experimental-span-attributes", false)) .setMessagingReceiveInstrumentationEnabled( ExperimentalConfig.get().messagingReceiveInstrumentationEnabled()); @@ -72,32 +56,5 @@ public static Instrumenter consumerProcessInstrumente return CONSUMER_PROCESS_INSTRUMENTER; } - public static void enhanceConfig(Map config) { - if (!METRICS_ENABLED) { - return; - } - config.merge( - CommonClientConfigs.METRIC_REPORTER_CLASSES_CONFIG, - OpenTelemetryMetricsReporter.class.getName(), - (class1, class2) -> { - if (class1 instanceof String) { - String className1 = (String) class1; - if (className1.isEmpty()) { - return class2; - } - if (METRIC_REPORTER_PRESENT_PATTERN.matcher(className1).find()) { - return class1; - } - } - return class1 + "," + class2; - }); - config.put( - OpenTelemetryMetricsReporter.CONFIG_KEY_OPENTELEMETRY_SUPPLIER, - new OpenTelemetrySupplier(GlobalOpenTelemetry.get())); - config.put( - OpenTelemetryMetricsReporter.CONFIG_KEY_OPENTELEMETRY_INSTRUMENTATION_NAME, - INSTRUMENTATION_NAME); - } - private KafkaSingletons() {} } diff --git a/instrumentation/kafka/kafka-clients/kafka-clients-0.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kafkaclients/v0_11/TracingIterable.java b/instrumentation/kafka/kafka-clients/kafka-clients-0.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kafkaclients/v0_11/TracingIterable.java deleted file mode 100644 index 425a9e2d189d..000000000000 --- a/instrumentation/kafka/kafka-clients/kafka-clients-0.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kafkaclients/v0_11/TracingIterable.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.javaagent.instrumentation.kafkaclients.v0_11; - -import io.opentelemetry.instrumentation.kafka.internal.KafkaConsumerContext; -import io.opentelemetry.javaagent.bootstrap.kafka.KafkaClientsConsumerProcessTracing; -import java.util.Iterator; -import org.apache.kafka.clients.consumer.ConsumerRecord; - -public class TracingIterable implements Iterable> { - private final Iterable> delegate; - private final KafkaConsumerContext consumerContext; - private boolean firstIterator = true; - - protected TracingIterable( - Iterable> delegate, KafkaConsumerContext consumerContext) { - this.delegate = delegate; - this.consumerContext = consumerContext; - } - - public static Iterable> wrap( - Iterable> delegate, KafkaConsumerContext consumerContext) { - if (KafkaClientsConsumerProcessTracing.wrappingEnabled()) { - return new TracingIterable<>(delegate, consumerContext); - } - return delegate; - } - - @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 = TracingIterator.wrap(delegate.iterator(), consumerContext); - firstIterator = false; - } else { - it = delegate.iterator(); - } - - return it; - } -} diff --git a/instrumentation/kafka/kafka-clients/kafka-clients-0.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kafkaclients/v0_11/metrics/KafkaMetricsConsumerInstrumentation.java b/instrumentation/kafka/kafka-clients/kafka-clients-0.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kafkaclients/v0_11/metrics/KafkaMetricsConsumerInstrumentation.java new file mode 100644 index 000000000000..c9775596465d --- /dev/null +++ b/instrumentation/kafka/kafka-clients/kafka-clients-0.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kafkaclients/v0_11/metrics/KafkaMetricsConsumerInstrumentation.java @@ -0,0 +1,61 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.kafkaclients.v0_11.metrics; + +import static io.opentelemetry.javaagent.instrumentation.kafkaclients.v0_11.metrics.KafkaMetricsUtil.enhanceConfig; +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 java.util.HashMap; +import java.util.Map; +import java.util.Properties; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class KafkaMetricsConsumerInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("org.apache.kafka.clients.consumer.KafkaConsumer"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isConstructor().and(takesArgument(0, Map.class)), + this.getClass().getName() + "$ConstructorMapAdvice"); + transformer.applyAdviceToMethod( + isConstructor().and(takesArgument(0, Properties.class)), + this.getClass().getName() + "$ConstructorPropertiesAdvice"); + } + + @SuppressWarnings("unused") + public static class ConstructorMapAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter( + @Advice.Argument(value = 0, readOnly = false) Map config) { + // ensure config is a mutable map + if (config.getClass() != HashMap.class) { + config = new HashMap<>(config); + } + enhanceConfig(config); + } + } + + @SuppressWarnings("unused") + public static class ConstructorPropertiesAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter(@Advice.Argument(0) Properties config) { + enhanceConfig(config); + } + } +} diff --git a/instrumentation/kafka/kafka-clients/kafka-clients-0.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kafkaclients/v0_11/metrics/KafkaMetricsInstrumentationModule.java b/instrumentation/kafka/kafka-clients/kafka-clients-0.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kafkaclients/v0_11/metrics/KafkaMetricsInstrumentationModule.java new file mode 100644 index 000000000000..6749eb2cfe7e --- /dev/null +++ b/instrumentation/kafka/kafka-clients/kafka-clients-0.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kafkaclients/v0_11/metrics/KafkaMetricsInstrumentationModule.java @@ -0,0 +1,43 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.kafkaclients.v0_11.metrics; + +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.extension.instrumentation.internal.ExperimentalInstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.internal.injection.ClassInjector; +import io.opentelemetry.javaagent.extension.instrumentation.internal.injection.InjectionMode; +import java.util.List; + +@AutoService(InstrumentationModule.class) +public class KafkaMetricsInstrumentationModule extends InstrumentationModule + implements ExperimentalInstrumentationModule { + public KafkaMetricsInstrumentationModule() { + super( + "kafka-clients-metrics", + "kafka-clients", + "kafka-clients-metrics-0.11", + "kafka-clients-0.11", + "kafka"); + } + + @Override + public void injectClasses(ClassInjector injector) { + injector + .proxyBuilder( + "io.opentelemetry.instrumentation.kafka.internal.OpenTelemetryMetricsReporter") + .inject(InjectionMode.CLASS_ONLY); + } + + @Override + public List typeInstrumentations() { + return asList( + new KafkaMetricsProducerInstrumentation(), new KafkaMetricsConsumerInstrumentation()); + } +} diff --git a/instrumentation/kafka/kafka-clients/kafka-clients-0.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kafkaclients/v0_11/metrics/KafkaMetricsProducerInstrumentation.java b/instrumentation/kafka/kafka-clients/kafka-clients-0.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kafkaclients/v0_11/metrics/KafkaMetricsProducerInstrumentation.java new file mode 100644 index 000000000000..821bb8443a1d --- /dev/null +++ b/instrumentation/kafka/kafka-clients/kafka-clients-0.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kafkaclients/v0_11/metrics/KafkaMetricsProducerInstrumentation.java @@ -0,0 +1,61 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.kafkaclients.v0_11.metrics; + +import static io.opentelemetry.javaagent.instrumentation.kafkaclients.v0_11.metrics.KafkaMetricsUtil.enhanceConfig; +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 java.util.HashMap; +import java.util.Map; +import java.util.Properties; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class KafkaMetricsProducerInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("org.apache.kafka.clients.producer.KafkaProducer"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isConstructor().and(takesArgument(0, Map.class)), + this.getClass().getName() + "$ConstructorMapAdvice"); + transformer.applyAdviceToMethod( + isConstructor().and(takesArgument(0, Properties.class)), + this.getClass().getName() + "$ConstructorPropertiesAdvice"); + } + + @SuppressWarnings("unused") + public static class ConstructorMapAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter( + @Advice.Argument(value = 0, readOnly = false) Map config) { + // ensure config is a mutable map + if (config.getClass() != HashMap.class) { + config = new HashMap<>(config); + } + enhanceConfig(config); + } + } + + @SuppressWarnings("unused") + public static class ConstructorPropertiesAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter(@Advice.Argument(0) Properties config) { + enhanceConfig(config); + } + } +} diff --git a/instrumentation/kafka/kafka-clients/kafka-clients-0.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kafkaclients/v0_11/metrics/KafkaMetricsUtil.java b/instrumentation/kafka/kafka-clients/kafka-clients-0.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kafkaclients/v0_11/metrics/KafkaMetricsUtil.java new file mode 100644 index 000000000000..e484e0fbd06e --- /dev/null +++ b/instrumentation/kafka/kafka-clients/kafka-clients-0.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kafkaclients/v0_11/metrics/KafkaMetricsUtil.java @@ -0,0 +1,65 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.kafkaclients.v0_11.metrics; + +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.instrumentation.kafka.internal.MetricsReporterList; +import io.opentelemetry.instrumentation.kafka.internal.OpenTelemetryMetricsReporter; +import io.opentelemetry.instrumentation.kafka.internal.OpenTelemetrySupplier; +import io.opentelemetry.javaagent.bootstrap.internal.AgentInstrumentationConfig; +import io.opentelemetry.javaagent.bootstrap.internal.DeprecatedConfigProperties; +import java.util.List; +import java.util.Map; +import org.apache.kafka.clients.CommonClientConfigs; + +public final class KafkaMetricsUtil { + private static final String INSTRUMENTATION_NAME = "io.opentelemetry.kafka-clients-0.11"; + private static final boolean METRICS_ENABLED = + DeprecatedConfigProperties.getBoolean( + AgentInstrumentationConfig.get(), + "otel.instrumentation.kafka.metric-reporter.enabled", + "otel.instrumentation.kafka-clients-metrics.enabled", + true); + + @SuppressWarnings("unchecked") + public static void enhanceConfig(Map config) { + // skip enhancing configuration when metrics are disabled or when we have already enhanced it + if (!METRICS_ENABLED + || config.get(OpenTelemetryMetricsReporter.CONFIG_KEY_OPENTELEMETRY_INSTRUMENTATION_NAME) + != null) { + return; + } + config.merge( + CommonClientConfigs.METRIC_REPORTER_CLASSES_CONFIG, + MetricsReporterList.singletonList(OpenTelemetryMetricsReporter.class), + (class1, class2) -> { + // class1 is either a class name or List of class names or classes + if (class1 instanceof List) { + List result = new MetricsReporterList<>(); + result.addAll((List) class1); + result.addAll((List) class2); + return result; + } else if (class1 instanceof String) { + String className1 = (String) class1; + if (className1.isEmpty()) { + return class2; + } + } + List result = new MetricsReporterList<>(); + result.add(class1); + result.addAll((List) class2); + return result; + }); + config.put( + OpenTelemetryMetricsReporter.CONFIG_KEY_OPENTELEMETRY_SUPPLIER, + new OpenTelemetrySupplier(GlobalOpenTelemetry.get())); + config.put( + OpenTelemetryMetricsReporter.CONFIG_KEY_OPENTELEMETRY_INSTRUMENTATION_NAME, + INSTRUMENTATION_NAME); + } + + private KafkaMetricsUtil() {} +} diff --git a/instrumentation/kafka/kafka-clients/kafka-clients-0.11/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/kafkaclients/v0_11/KafkaClientDefaultTest.java b/instrumentation/kafka/kafka-clients/kafka-clients-0.11/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/kafkaclients/v0_11/KafkaClientDefaultTest.java index 093d6ea2e858..c7d6817975ee 100644 --- a/instrumentation/kafka/kafka-clients/kafka-clients-0.11/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/kafkaclients/v0_11/KafkaClientDefaultTest.java +++ b/instrumentation/kafka/kafka-clients/kafka-clients-0.11/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/kafkaclients/v0_11/KafkaClientDefaultTest.java @@ -85,7 +85,7 @@ void testKafkaProducerAndConsumerSpan(boolean testHeaders) throws Exception { trace.hasSpansSatisfyingExactly( span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(), span -> - span.hasName(SHARED_TOPIC + " send") + span.hasName(SHARED_TOPIC + " publish") .hasKind(SpanKind.PRODUCER) .hasParent(trace.getSpan(0)) .hasAttributesSatisfyingExactly(sendAttributes("10", greeting, testHeaders)), @@ -134,7 +134,7 @@ void testPassThroughTombstone() trace -> { trace.hasSpansSatisfyingExactly( span -> - span.hasName(SHARED_TOPIC + " send") + span.hasName(SHARED_TOPIC + " publish") .hasKind(SpanKind.PRODUCER) .hasNoParent() .hasAttributesSatisfyingExactly(sendAttributes(null, null, false))); @@ -185,7 +185,7 @@ void testRecordsWithTopicPartitionKafkaConsume() trace -> { trace.hasSpansSatisfyingExactly( span -> - span.hasName(SHARED_TOPIC + " send") + span.hasName(SHARED_TOPIC + " publish") .hasKind(SpanKind.PRODUCER) .hasNoParent() .hasAttributesSatisfyingExactly(sendAttributes(null, greeting, false))); diff --git a/instrumentation/kafka/kafka-clients/kafka-clients-0.11/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/kafkaclients/v0_11/KafkaClientPropagationDisabledTest.java b/instrumentation/kafka/kafka-clients/kafka-clients-0.11/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/kafkaclients/v0_11/KafkaClientPropagationDisabledTest.java index e465a4737b89..e3ff36a08f98 100644 --- a/instrumentation/kafka/kafka-clients/kafka-clients-0.11/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/kafkaclients/v0_11/KafkaClientPropagationDisabledTest.java +++ b/instrumentation/kafka/kafka-clients/kafka-clients-0.11/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/kafkaclients/v0_11/KafkaClientPropagationDisabledTest.java @@ -34,7 +34,7 @@ void testReadRemoteContextWhenPropagationIsDisabled() throws InterruptedExceptio trace -> trace.hasSpansSatisfyingExactly( span -> - span.hasName(SHARED_TOPIC + " send") + span.hasName(SHARED_TOPIC + " publish") .hasKind(SpanKind.PRODUCER) .hasNoParent() .hasAttributesSatisfyingExactly(sendAttributes(null, message, false)))); @@ -54,7 +54,7 @@ void testReadRemoteContextWhenPropagationIsDisabled() throws InterruptedExceptio trace -> trace.hasSpansSatisfyingExactly( span -> - span.hasName(SHARED_TOPIC + " send") + span.hasName(SHARED_TOPIC + " publish") .hasKind(SpanKind.PRODUCER) .hasNoParent() .hasAttributesSatisfyingExactly(sendAttributes(null, message, false))), diff --git a/instrumentation/kafka/kafka-clients/kafka-clients-0.11/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/kafkaclients/v0_11/KafkaClientSuppressReceiveSpansTest.java b/instrumentation/kafka/kafka-clients/kafka-clients-0.11/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/kafkaclients/v0_11/KafkaClientSuppressReceiveSpansTest.java index f2b951b69553..1e8208310a34 100644 --- a/instrumentation/kafka/kafka-clients/kafka-clients-0.11/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/kafkaclients/v0_11/KafkaClientSuppressReceiveSpansTest.java +++ b/instrumentation/kafka/kafka-clients/kafka-clients-0.11/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/kafkaclients/v0_11/KafkaClientSuppressReceiveSpansTest.java @@ -62,7 +62,7 @@ void testKafkaProduceAndConsume() throws InterruptedException { trace.hasSpansSatisfyingExactly( span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(), span -> - span.hasName(SHARED_TOPIC + " send") + span.hasName(SHARED_TOPIC + " publish") .hasKind(SpanKind.PRODUCER) .hasParent(trace.getSpan(0)) .hasAttributesSatisfyingExactly(sendAttributes("10", greeting, false)), @@ -100,7 +100,7 @@ void testPassThroughTombstone() trace -> trace.hasSpansSatisfyingExactly( span -> - span.hasName(SHARED_TOPIC + " send") + span.hasName(SHARED_TOPIC + " publish") .hasKind(SpanKind.PRODUCER) .hasNoParent() .hasAttributesSatisfyingExactly(sendAttributes(null, null, false)), @@ -138,7 +138,7 @@ void testRecordsWithTopicPartitionKafkaConsume() trace -> trace.hasSpansSatisfyingExactly( span -> - span.hasName(SHARED_TOPIC + " send") + span.hasName(SHARED_TOPIC + " publish") .hasKind(SpanKind.PRODUCER) .hasNoParent() .hasAttributesSatisfyingExactly(sendAttributes(null, greeting, false)), diff --git a/instrumentation/kafka/kafka-clients/kafka-clients-0.11/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/kafkaclients/v0_11/OpenTelemetryMetricsReporterTest.java b/instrumentation/kafka/kafka-clients/kafka-clients-0.11/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/kafkaclients/v0_11/OpenTelemetryMetricsReporterTest.java index b8ac69dfcd68..21c1d38f019f 100644 --- a/instrumentation/kafka/kafka-clients/kafka-clients-0.11/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/kafkaclients/v0_11/OpenTelemetryMetricsReporterTest.java +++ b/instrumentation/kafka/kafka-clients/kafka-clients-0.11/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/kafkaclients/v0_11/OpenTelemetryMetricsReporterTest.java @@ -10,6 +10,7 @@ import io.opentelemetry.instrumentation.kafka.internal.AbstractOpenTelemetryMetricsReporterTest; import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import java.util.Collections; import java.util.Map; import org.apache.kafka.clients.CommonClientConfigs; import org.apache.kafka.clients.consumer.KafkaConsumer; @@ -48,4 +49,34 @@ void emptyMetricsReporter() { producerConfig.put(CommonClientConfigs.METRIC_REPORTER_CLASSES_CONFIG, ""); new KafkaProducer<>(producerConfig).close(); } + + @Test + void classListMetricsReporter() { + Map consumerConfig = consumerConfig(); + consumerConfig.put( + CommonClientConfigs.METRIC_REPORTER_CLASSES_CONFIG, + Collections.singletonList(TestMetricsReporter.class)); + new KafkaConsumer<>(consumerConfig).close(); + + Map producerConfig = producerConfig(); + producerConfig.put( + CommonClientConfigs.METRIC_REPORTER_CLASSES_CONFIG, + Collections.singletonList(TestMetricsReporter.class)); + new KafkaProducer<>(producerConfig).close(); + } + + @Test + void stringListMetricsReporter() { + Map consumerConfig = consumerConfig(); + consumerConfig.put( + CommonClientConfigs.METRIC_REPORTER_CLASSES_CONFIG, + Collections.singletonList(TestMetricsReporter.class.getName())); + new KafkaConsumer<>(consumerConfig).close(); + + Map producerConfig = producerConfig(); + producerConfig.put( + CommonClientConfigs.METRIC_REPORTER_CLASSES_CONFIG, + Collections.singletonList(TestMetricsReporter.class.getName())); + new KafkaProducer<>(producerConfig).close(); + } } diff --git a/instrumentation/kafka/kafka-clients/kafka-clients-0.11/testing/src/main/java/io/opentelemetry/instrumentation/kafka/internal/AbstractOpenTelemetryMetricsReporterTest.java b/instrumentation/kafka/kafka-clients/kafka-clients-0.11/testing/src/main/java/io/opentelemetry/instrumentation/kafka/internal/AbstractOpenTelemetryMetricsReporterTest.java index 6069f337a0c1..141498e7836f 100644 --- a/instrumentation/kafka/kafka-clients/kafka-clients-0.11/testing/src/main/java/io/opentelemetry/instrumentation/kafka/internal/AbstractOpenTelemetryMetricsReporterTest.java +++ b/instrumentation/kafka/kafka-clients/kafka-clients-0.11/testing/src/main/java/io/opentelemetry/instrumentation/kafka/internal/AbstractOpenTelemetryMetricsReporterTest.java @@ -18,6 +18,8 @@ import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; import io.opentelemetry.sdk.metrics.data.MetricData; import io.opentelemetry.sdk.metrics.data.PointData; +import java.lang.reflect.Field; +import java.lang.reflect.Method; import java.nio.charset.StandardCharsets; import java.time.Duration; import java.time.Instant; @@ -30,16 +32,16 @@ import java.util.Optional; import java.util.Random; import java.util.Set; +import java.util.concurrent.CopyOnWriteArrayList; import org.apache.kafka.clients.CommonClientConfigs; import org.apache.kafka.clients.consumer.ConsumerConfig; import org.apache.kafka.clients.consumer.KafkaConsumer; -import org.apache.kafka.clients.consumer.KafkaConsumerAccess; import org.apache.kafka.clients.producer.KafkaProducer; -import org.apache.kafka.clients.producer.KafkaProducerAccess; import org.apache.kafka.clients.producer.ProducerConfig; import org.apache.kafka.clients.producer.ProducerRecord; import org.apache.kafka.common.MetricName; import org.apache.kafka.common.metrics.KafkaMetric; +import org.apache.kafka.common.metrics.Metrics; import org.apache.kafka.common.metrics.MetricsReporter; import org.apache.kafka.common.serialization.ByteArrayDeserializer; import org.apache.kafka.common.serialization.ByteArraySerializer; @@ -69,6 +71,13 @@ public abstract class AbstractOpenTelemetryMetricsReporterTest { private static KafkaProducer producer; private static KafkaConsumer consumer; + private static final List metricsReporters = + new CopyOnWriteArrayList<>(); + + static { + OpenTelemetryMetricsReporter.setListener(metricsReporters::add); + } + @BeforeEach void beforeAll() { // only start the kafka container the first time this runs @@ -77,7 +86,7 @@ void beforeAll() { } kafka = - new KafkaContainer(DockerImageName.parse("confluentinc/cp-kafka:6.2.10")) + new KafkaContainer(DockerImageName.parse("confluentinc/cp-kafka:7.7.0")) .withEnv("KAFKA_HEAP_OPTS", "-Xmx256m") .withLogConsumer(new Slf4jLogConsumer(logger)) .waitingFor(Wait.forLogMessage(".*started \\(kafka.server.KafkaServer\\).*", 1)) @@ -89,14 +98,16 @@ void beforeAll() { @AfterAll static void afterAll() { - kafka.stop(); producer.close(); consumer.close(); + kafka.stop(); } @AfterEach void tearDown() { - OpenTelemetryMetricsReporter.resetForTest(); + for (OpenTelemetryMetricsReporter metricsReporter : metricsReporters) { + metricsReporter.resetForTest(); + } } protected abstract InstrumentationExtension testing(); @@ -116,7 +127,7 @@ protected Map producerConfig() { producerConfig.merge( CommonClientConfigs.METRIC_REPORTER_CLASSES_CONFIG, TestMetricsReporter.class.getName(), - (o, o2) -> o + "," + o2); + AbstractOpenTelemetryMetricsReporterTest::mergeValue); return producerConfig; } @@ -133,20 +144,50 @@ protected Map consumerConfig() { consumerConfig.merge( CommonClientConfigs.METRIC_REPORTER_CLASSES_CONFIG, TestMetricsReporter.class.getName(), - (o, o2) -> o + "," + o2); + AbstractOpenTelemetryMetricsReporterTest::mergeValue); return consumerConfig; } + @SuppressWarnings("unchecked") + private static Object mergeValue(Object o1, Object o2) { + List result = new MetricsReporterList<>(); + result.addAll((List) o1); + result.add(o2); + return result; + } + @Test void noDuplicateMetricsReporter() { - List producerMetricsReporters = - KafkaProducerAccess.getMetricsReporters(producer); + List producerMetricsReporters = getMetricsReporters(producer); assertThat(countOpenTelemetryMetricsReporters(producerMetricsReporters)).isEqualTo(1); - List consumerMetricsReporters = - KafkaConsumerAccess.getMetricsReporters(consumer); + List consumerMetricsReporters = getMetricsReporters(consumer); assertThat(countOpenTelemetryMetricsReporters(consumerMetricsReporters)).isEqualTo(1); } + private static List getMetricsReporters(Object producerOrConsumer) { + return getMetricsRegistry(producerOrConsumer).reporters(); + } + + private static Metrics getMetricsRegistry(Object producerOrConsumer) { + Class clazz = producerOrConsumer.getClass(); + try { + Field field = clazz.getDeclaredField("metrics"); + field.setAccessible(true); + return (Metrics) field.get(producerOrConsumer); + } catch (Exception ignored) { + // Ignore + } + try { + Method method = clazz.getDeclaredMethod("metricsRegistry"); + method.setAccessible(true); + return (Metrics) method.invoke(producerOrConsumer); + } catch (Exception ignored) { + // Ignore + } + throw new IllegalStateException( + "Failed to get metrics registry from " + producerOrConsumer.getClass().getName()); + } + private static long countOpenTelemetryMetricsReporters(List metricsReporters) { return metricsReporters.stream() .filter(reporter -> reporter.getClass().getName().endsWith("OpenTelemetryMetricsReporter")) @@ -155,6 +196,14 @@ private static long countOpenTelemetryMetricsReporters(List met @Test void observeMetrics() { + // Firstly create new producer and consumer and close them. This is done tp verify that metrics + // are still produced after closing one producer/consumer. See + // https://github.com/open-telemetry/opentelemetry-java-instrumentation/issues/11880 + KafkaProducer producer2 = new KafkaProducer<>(producerConfig()); + KafkaConsumer consumer2 = new KafkaConsumer<>(consumerConfig()); + producer2.close(); + consumer2.close(); + produceRecords(); consumeRecords(); @@ -176,12 +225,6 @@ void observeMetrics() { "kafka.consumer.join_total", "kafka.consumer.last_heartbeat_seconds_ago", "kafka.consumer.last_rebalance_seconds_ago", - "kafka.consumer.partition_assigned_latency_avg", - "kafka.consumer.partition_assigned_latency_max", - "kafka.consumer.partition_lost_latency_avg", - "kafka.consumer.partition_lost_latency_max", - "kafka.consumer.partition_revoked_latency_avg", - "kafka.consumer.partition_revoked_latency_max", "kafka.consumer.rebalance_latency_avg", "kafka.consumer.rebalance_latency_max", "kafka.consumer.rebalance_latency_total", @@ -233,8 +276,6 @@ void observeMetrics() { "kafka.consumer.outgoing_byte_rate", "kafka.consumer.outgoing_byte_total", "kafka.consumer.poll_idle_ratio_avg", - "kafka.consumer.reauthentication_latency_avg", - "kafka.consumer.reauthentication_latency_max", "kafka.consumer.request_rate", "kafka.consumer.request_size_avg", "kafka.consumer.request_size_max", @@ -250,8 +291,6 @@ void observeMetrics() { "kafka.consumer.successful_reauthentication_total", "kafka.consumer.time_between_poll_avg", "kafka.consumer.time_between_poll_max", - "kafka.consumer.request_latency_avg", - "kafka.consumer.request_latency_max", "kafka.producer.batch_size_avg", "kafka.producer.batch_size_max", "kafka.producer.batch_split_rate", @@ -287,8 +326,6 @@ void observeMetrics() { "kafka.producer.outgoing_byte_total", "kafka.producer.produce_throttle_time_avg", "kafka.producer.produce_throttle_time_max", - "kafka.producer.reauthentication_latency_avg", - "kafka.producer.reauthentication_latency_max", "kafka.producer.record_error_rate", "kafka.producer.record_error_total", "kafka.producer.record_queue_time_avg", @@ -386,7 +423,9 @@ private static void printMappingTable() { Map> kafkaMetricsByGroup = TestMetricsReporter.seenMetrics.stream().collect(groupingBy(KafkaMetricId::getGroup)); List registeredObservables = - OpenTelemetryMetricsReporter.getRegisteredObservables(); + metricsReporters.stream() + .flatMap(metricsReporter -> metricsReporter.getRegisteredObservables().stream()) + .collect(toList()); // Iterate through groups in alpha order for (String group : kafkaMetricsByGroup.keySet().stream().sorted().collect(toList())) { List kafkaMetricIds = @@ -406,6 +445,9 @@ private static void printMappingTable() { .equals(kafkaMetricId)) .findFirst() .map(RegisteredObservable::getInstrumentDescriptor); + if (!descriptor.isPresent()) { + continue; + } // Append table row sb.append( String.format( @@ -416,13 +458,27 @@ private static void printMappingTable() { .map(key -> "`" + key + "`") .collect(joining(",")), descriptor.map(i -> "`" + i.getName() + "`").orElse(""), - descriptor.map(InstrumentDescriptor::getDescription).orElse(""), + descriptor.map(i -> toDescription(i)).orElse(""), descriptor.map(i -> "`" + i.getInstrumentType() + "`").orElse(""))); } } logger.info("Mapping table" + System.lineSeparator() + sb); } + private static String toDescription(InstrumentDescriptor instrumentDescriptor) { + String description = instrumentDescriptor.getDescription(); + if (!description.isEmpty() && !description.endsWith(".")) { + return description + "."; + } else if (description.isEmpty() + && "kafka.consumer.request_latency_avg".equals(instrumentDescriptor.getName())) { + return "The average request latency in ms."; + } else if (description.isEmpty() + && "kafka.consumer.request_latency_max".equals(instrumentDescriptor.getName())) { + return "The maximum request latency in ms."; + } + return description; + } + /** * This class is internal and is hence not for public use. Its APIs are unstable and can change at * any time. @@ -438,6 +494,12 @@ public void init(List list) { @Override public void metricChange(KafkaMetric kafkaMetric) { + try { + kafkaMetric.measurable(); + } catch (IllegalStateException exception) { + // ignore non-measurable metrics, we don't report them + return; + } seenMetrics.add(KafkaMetricId.create(kafkaMetric.metricName())); } diff --git a/instrumentation/kafka/kafka-clients/kafka-clients-0.11/testing/src/main/java/io/opentelemetry/instrumentation/kafka/internal/KafkaClientBaseTest.java b/instrumentation/kafka/kafka-clients/kafka-clients-0.11/testing/src/main/java/io/opentelemetry/instrumentation/kafka/internal/KafkaClientBaseTest.java index e0ad5ff5df71..078b3fc310f0 100644 --- a/instrumentation/kafka/kafka-clients/kafka-clients-0.11/testing/src/main/java/io/opentelemetry/instrumentation/kafka/internal/KafkaClientBaseTest.java +++ b/instrumentation/kafka/kafka-clients/kafka-clients-0.11/testing/src/main/java/io/opentelemetry/instrumentation/kafka/internal/KafkaClientBaseTest.java @@ -10,7 +10,7 @@ import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.sdk.testing.assertj.AttributeAssertion; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes; import java.nio.charset.StandardCharsets; import java.time.Duration; import java.util.ArrayList; @@ -19,6 +19,7 @@ import java.util.Collections; import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; @@ -36,6 +37,7 @@ import org.apache.kafka.common.serialization.StringDeserializer; import org.apache.kafka.common.serialization.StringSerializer; import org.assertj.core.api.AbstractLongAssert; +import org.assertj.core.api.AbstractStringAssert; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.TestInstance; @@ -64,7 +66,7 @@ public abstract class KafkaClientBaseTest { @BeforeAll void setupClass() throws ExecutionException, InterruptedException, TimeoutException { kafka = - new KafkaContainer(DockerImageName.parse("confluentinc/cp-kafka:6.2.10")) + new KafkaContainer(DockerImageName.parse("confluentinc/cp-kafka:7.7.0")) .withEnv("KAFKA_HEAP_OPTS", "-Xmx256m") .withLogConsumer(new Slf4jLogConsumer(logger)) .waitingFor(Wait.forLogMessage(".*started \\(kafka.server.KafkaServer\\).*", 1)) @@ -99,7 +101,7 @@ public void onPartitionsAssigned(Collection collection) { }); } - public HashMap consumerProps() { + public Map consumerProps() { HashMap props = new HashMap<>(); props.put("bootstrap.servers", kafka.getBootstrapServers()); props.put("group.id", "test"); @@ -111,7 +113,7 @@ public HashMap consumerProps() { return props; } - public HashMap producerProps() { + public Map producerProps() { HashMap props = new HashMap<>(); props.put("bootstrap.servers", kafka.getBootstrapServers()); props.put("retries", 0); @@ -156,22 +158,25 @@ protected static List sendAttributes( List assertions = new ArrayList<>( Arrays.asList( - equalTo(SemanticAttributes.MESSAGING_SYSTEM, "kafka"), - equalTo(SemanticAttributes.MESSAGING_DESTINATION_NAME, SHARED_TOPIC), + equalTo(MessagingIncubatingAttributes.MESSAGING_SYSTEM, "kafka"), + equalTo(MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME, SHARED_TOPIC), + equalTo(MessagingIncubatingAttributes.MESSAGING_OPERATION, "publish"), satisfies( - SemanticAttributes.MESSAGING_KAFKA_CLIENT_ID, + MessagingIncubatingAttributes.MESSAGING_CLIENT_ID, stringAssert -> stringAssert.startsWith("producer")), satisfies( - SemanticAttributes.MESSAGING_KAFKA_DESTINATION_PARTITION, - AbstractLongAssert::isNotNegative), + MessagingIncubatingAttributes.MESSAGING_DESTINATION_PARTITION_ID, + AbstractStringAssert::isNotEmpty), satisfies( - SemanticAttributes.MESSAGING_KAFKA_MESSAGE_OFFSET, + MessagingIncubatingAttributes.MESSAGING_KAFKA_MESSAGE_OFFSET, AbstractLongAssert::isNotNegative))); if (messageKey != null) { - assertions.add(equalTo(SemanticAttributes.MESSAGING_KAFKA_MESSAGE_KEY, messageKey)); + assertions.add( + equalTo(MessagingIncubatingAttributes.MESSAGING_KAFKA_MESSAGE_KEY, messageKey)); } if (messageValue == null) { - assertions.add(equalTo(SemanticAttributes.MESSAGING_KAFKA_MESSAGE_TOMBSTONE, true)); + assertions.add( + equalTo(MessagingIncubatingAttributes.MESSAGING_KAFKA_MESSAGE_TOMBSTONE, true)); } if (testHeaders) { assertions.add( @@ -186,19 +191,18 @@ protected static List receiveAttributes(boolean testHeaders) List assertions = new ArrayList<>( Arrays.asList( - equalTo(SemanticAttributes.MESSAGING_SYSTEM, "kafka"), - equalTo(SemanticAttributes.MESSAGING_DESTINATION_NAME, SHARED_TOPIC), - equalTo(SemanticAttributes.MESSAGING_OPERATION, "receive"), + equalTo(MessagingIncubatingAttributes.MESSAGING_SYSTEM, "kafka"), + equalTo(MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME, SHARED_TOPIC), + equalTo(MessagingIncubatingAttributes.MESSAGING_OPERATION, "receive"), + satisfies( + MessagingIncubatingAttributes.MESSAGING_CLIENT_ID, + stringAssert -> stringAssert.startsWith("consumer")), satisfies( - SemanticAttributes.MESSAGING_KAFKA_CLIENT_ID, - stringAssert -> stringAssert.startsWith("consumer")))); - // consumer group id is not available in version 0.11 + MessagingIncubatingAttributes.MESSAGING_BATCH_MESSAGE_COUNT, + AbstractLongAssert::isPositive))); + // consumer group is not available in version 0.11 if (Boolean.getBoolean("testLatestDeps")) { - assertions.add(equalTo(SemanticAttributes.MESSAGING_KAFKA_CONSUMER_GROUP, "test")); - assertions.add( - satisfies( - SemanticAttributes.MESSAGING_CONSUMER_ID, - stringAssert -> stringAssert.startsWith("test - consumer"))); + assertions.add(equalTo(MessagingIncubatingAttributes.MESSAGING_KAFKA_CONSUMER_GROUP, "test")); } if (testHeaders) { assertions.add( @@ -214,40 +218,36 @@ protected static List processAttributes( List assertions = new ArrayList<>( Arrays.asList( - equalTo(SemanticAttributes.MESSAGING_SYSTEM, "kafka"), - equalTo(SemanticAttributes.MESSAGING_DESTINATION_NAME, SHARED_TOPIC), - equalTo(SemanticAttributes.MESSAGING_OPERATION, "process"), + equalTo(MessagingIncubatingAttributes.MESSAGING_SYSTEM, "kafka"), + equalTo(MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME, SHARED_TOPIC), + equalTo(MessagingIncubatingAttributes.MESSAGING_OPERATION, "process"), satisfies( - SemanticAttributes.MESSAGING_KAFKA_CLIENT_ID, + MessagingIncubatingAttributes.MESSAGING_CLIENT_ID, stringAssert -> stringAssert.startsWith("consumer")), satisfies( - SemanticAttributes.MESSAGING_KAFKA_SOURCE_PARTITION, - AbstractLongAssert::isNotNegative), + MessagingIncubatingAttributes.MESSAGING_DESTINATION_PARTITION_ID, + AbstractStringAssert::isNotEmpty), satisfies( - SemanticAttributes.MESSAGING_KAFKA_MESSAGE_OFFSET, + MessagingIncubatingAttributes.MESSAGING_KAFKA_MESSAGE_OFFSET, AbstractLongAssert::isNotNegative), satisfies( AttributeKey.longKey("kafka.record.queue_time_ms"), AbstractLongAssert::isNotNegative))); - // consumer group id is not available in version 0.11 + // consumer group is not available in version 0.11 if (Boolean.getBoolean("testLatestDeps")) { - assertions.add(equalTo(SemanticAttributes.MESSAGING_KAFKA_CONSUMER_GROUP, "test")); - assertions.add( - satisfies( - SemanticAttributes.MESSAGING_CONSUMER_ID, - stringAssert -> stringAssert.startsWith("test - consumer"))); + assertions.add(equalTo(MessagingIncubatingAttributes.MESSAGING_KAFKA_CONSUMER_GROUP, "test")); } if (messageKey != null) { - assertions.add(equalTo(SemanticAttributes.MESSAGING_KAFKA_MESSAGE_KEY, messageKey)); + assertions.add( + equalTo(MessagingIncubatingAttributes.MESSAGING_KAFKA_MESSAGE_KEY, messageKey)); } if (messageValue == null) { - assertions.add(equalTo(SemanticAttributes.MESSAGING_KAFKA_MESSAGE_TOMBSTONE, true)); - // TODO shouldn't set -1 in this case - assertions.add(equalTo(SemanticAttributes.MESSAGING_MESSAGE_PAYLOAD_SIZE_BYTES, -1L)); + assertions.add( + equalTo(MessagingIncubatingAttributes.MESSAGING_KAFKA_MESSAGE_TOMBSTONE, true)); } else { assertions.add( equalTo( - SemanticAttributes.MESSAGING_MESSAGE_PAYLOAD_SIZE_BYTES, + MessagingIncubatingAttributes.MESSAGING_MESSAGE_BODY_SIZE, messageValue.getBytes(StandardCharsets.UTF_8).length)); } if (testHeaders) { diff --git a/instrumentation/kafka/kafka-clients/kafka-clients-0.11/testing/src/main/java/org/apache/kafka/clients/consumer/KafkaConsumerAccess.java b/instrumentation/kafka/kafka-clients/kafka-clients-0.11/testing/src/main/java/org/apache/kafka/clients/consumer/KafkaConsumerAccess.java deleted file mode 100644 index 2b698af5dd54..000000000000 --- a/instrumentation/kafka/kafka-clients/kafka-clients-0.11/testing/src/main/java/org/apache/kafka/clients/consumer/KafkaConsumerAccess.java +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package org.apache.kafka.clients.consumer; - -import java.util.List; -import org.apache.kafka.common.metrics.MetricsReporter; - -public class KafkaConsumerAccess { - - private KafkaConsumerAccess() {} - - public static List getMetricsReporters(KafkaConsumer consumer) { - return consumer.metrics.reporters(); - } -} diff --git a/instrumentation/kafka/kafka-clients/kafka-clients-0.11/testing/src/main/java/org/apache/kafka/clients/producer/KafkaProducerAccess.java b/instrumentation/kafka/kafka-clients/kafka-clients-0.11/testing/src/main/java/org/apache/kafka/clients/producer/KafkaProducerAccess.java deleted file mode 100644 index 6121524f6f92..000000000000 --- a/instrumentation/kafka/kafka-clients/kafka-clients-0.11/testing/src/main/java/org/apache/kafka/clients/producer/KafkaProducerAccess.java +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package org.apache.kafka.clients.producer; - -import java.util.List; -import org.apache.kafka.common.metrics.MetricsReporter; - -public class KafkaProducerAccess { - - private KafkaProducerAccess() {} - - public static List getMetricsReporters(KafkaProducer producer) { - return producer.metrics.reporters(); - } -} diff --git a/instrumentation/kafka/kafka-clients/kafka-clients-2.6/library/README.md b/instrumentation/kafka/kafka-clients/kafka-clients-2.6/library/README.md index 64481b7ac7b9..b244222c1815 100644 --- a/instrumentation/kafka/kafka-clients/kafka-clients-2.6/library/README.md +++ b/instrumentation/kafka/kafka-clients/kafka-clients-2.6/library/README.md @@ -90,207 +90,179 @@ OpenTelemetry metric each maps to (if available). Empty values in the Instrument Description, etc column indicates there is no registered mapping for the metric and data is NOT collected. -| Metric Group | Metric Name | Attribute Keys | Instrument Name | Instrument Description | Instrument Type | -|--------------|-------------|----------------|-----------------|------------------------|-----------------| -| `app-info` | `commit-id` | `client-id` | | | | -| `app-info` | `start-time-ms` | `client-id` | | | | -| `app-info` | `version` | `client-id` | | | | -| `consumer-coordinator-metrics` | `assigned-partitions` | `client-id` | `kafka.consumer.assigned_partitions` | The number of partitions currently assigned to this consumer | `DOUBLE_OBSERVABLE_GAUGE` | -| `consumer-coordinator-metrics` | `commit-latency-avg` | `client-id` | `kafka.consumer.commit_latency_avg` | The average time taken for a commit request | `DOUBLE_OBSERVABLE_GAUGE` | -| `consumer-coordinator-metrics` | `commit-latency-max` | `client-id` | `kafka.consumer.commit_latency_max` | The max time taken for a commit request | `DOUBLE_OBSERVABLE_GAUGE` | -| `consumer-coordinator-metrics` | `commit-rate` | `client-id` | `kafka.consumer.commit_rate` | The number of commit calls per second | `DOUBLE_OBSERVABLE_GAUGE` | -| `consumer-coordinator-metrics` | `commit-total` | `client-id` | `kafka.consumer.commit_total` | The total number of commit calls | `DOUBLE_OBSERVABLE_COUNTER` | -| `consumer-coordinator-metrics` | `failed-rebalance-rate-per-hour` | `client-id` | `kafka.consumer.failed_rebalance_rate_per_hour` | The number of failed rebalance events per hour | `DOUBLE_OBSERVABLE_GAUGE` | -| `consumer-coordinator-metrics` | `failed-rebalance-total` | `client-id` | `kafka.consumer.failed_rebalance_total` | The total number of failed rebalance events | `DOUBLE_OBSERVABLE_COUNTER` | -| `consumer-coordinator-metrics` | `heartbeat-rate` | `client-id` | `kafka.consumer.heartbeat_rate` | The number of heartbeats per second | `DOUBLE_OBSERVABLE_GAUGE` | -| `consumer-coordinator-metrics` | `heartbeat-response-time-max` | `client-id` | `kafka.consumer.heartbeat_response_time_max` | The max time taken to receive a response to a heartbeat request | `DOUBLE_OBSERVABLE_GAUGE` | -| `consumer-coordinator-metrics` | `heartbeat-total` | `client-id` | `kafka.consumer.heartbeat_total` | The total number of heartbeats | `DOUBLE_OBSERVABLE_COUNTER` | -| `consumer-coordinator-metrics` | `join-rate` | `client-id` | `kafka.consumer.join_rate` | The number of group joins per second | `DOUBLE_OBSERVABLE_GAUGE` | -| `consumer-coordinator-metrics` | `join-time-avg` | `client-id` | `kafka.consumer.join_time_avg` | The average time taken for a group rejoin | `DOUBLE_OBSERVABLE_GAUGE` | -| `consumer-coordinator-metrics` | `join-time-max` | `client-id` | `kafka.consumer.join_time_max` | The max time taken for a group rejoin | `DOUBLE_OBSERVABLE_GAUGE` | -| `consumer-coordinator-metrics` | `join-total` | `client-id` | `kafka.consumer.join_total` | The total number of group joins | `DOUBLE_OBSERVABLE_COUNTER` | -| `consumer-coordinator-metrics` | `last-heartbeat-seconds-ago` | `client-id` | `kafka.consumer.last_heartbeat_seconds_ago` | The number of seconds since the last coordinator heartbeat was sent | `DOUBLE_OBSERVABLE_GAUGE` | -| `consumer-coordinator-metrics` | `last-rebalance-seconds-ago` | `client-id` | `kafka.consumer.last_rebalance_seconds_ago` | The number of seconds since the last successful rebalance event | `DOUBLE_OBSERVABLE_GAUGE` | -| `consumer-coordinator-metrics` | `partition-assigned-latency-avg` | `client-id` | `kafka.consumer.partition_assigned_latency_avg` | The average time taken for a partition-assigned rebalance listener callback | `DOUBLE_OBSERVABLE_GAUGE` | -| `consumer-coordinator-metrics` | `partition-assigned-latency-max` | `client-id` | `kafka.consumer.partition_assigned_latency_max` | The max time taken for a partition-assigned rebalance listener callback | `DOUBLE_OBSERVABLE_GAUGE` | -| `consumer-coordinator-metrics` | `partition-lost-latency-avg` | `client-id` | `kafka.consumer.partition_lost_latency_avg` | The average time taken for a partition-lost rebalance listener callback | `DOUBLE_OBSERVABLE_GAUGE` | -| `consumer-coordinator-metrics` | `partition-lost-latency-max` | `client-id` | `kafka.consumer.partition_lost_latency_max` | The max time taken for a partition-lost rebalance listener callback | `DOUBLE_OBSERVABLE_GAUGE` | -| `consumer-coordinator-metrics` | `partition-revoked-latency-avg` | `client-id` | `kafka.consumer.partition_revoked_latency_avg` | The average time taken for a partition-revoked rebalance listener callback | `DOUBLE_OBSERVABLE_GAUGE` | -| `consumer-coordinator-metrics` | `partition-revoked-latency-max` | `client-id` | `kafka.consumer.partition_revoked_latency_max` | The max time taken for a partition-revoked rebalance listener callback | `DOUBLE_OBSERVABLE_GAUGE` | -| `consumer-coordinator-metrics` | `rebalance-latency-avg` | `client-id` | `kafka.consumer.rebalance_latency_avg` | The average time taken for a group to complete a successful rebalance, which may be composed of several failed re-trials until it succeeded | `DOUBLE_OBSERVABLE_GAUGE` | -| `consumer-coordinator-metrics` | `rebalance-latency-max` | `client-id` | `kafka.consumer.rebalance_latency_max` | The max time taken for a group to complete a successful rebalance, which may be composed of several failed re-trials until it succeeded | `DOUBLE_OBSERVABLE_GAUGE` | -| `consumer-coordinator-metrics` | `rebalance-latency-total` | `client-id` | `kafka.consumer.rebalance_latency_total` | The total number of milliseconds this consumer has spent in successful rebalances since creation | `DOUBLE_OBSERVABLE_COUNTER` | -| `consumer-coordinator-metrics` | `rebalance-rate-per-hour` | `client-id` | `kafka.consumer.rebalance_rate_per_hour` | The number of successful rebalance events per hour, each event is composed of several failed re-trials until it succeeded | `DOUBLE_OBSERVABLE_GAUGE` | -| `consumer-coordinator-metrics` | `rebalance-total` | `client-id` | `kafka.consumer.rebalance_total` | The total number of successful rebalance events, each event is composed of several failed re-trials until it succeeded | `DOUBLE_OBSERVABLE_COUNTER` | -| `consumer-coordinator-metrics` | `sync-rate` | `client-id` | `kafka.consumer.sync_rate` | The number of group syncs per second | `DOUBLE_OBSERVABLE_GAUGE` | -| `consumer-coordinator-metrics` | `sync-time-avg` | `client-id` | `kafka.consumer.sync_time_avg` | The average time taken for a group sync | `DOUBLE_OBSERVABLE_GAUGE` | -| `consumer-coordinator-metrics` | `sync-time-max` | `client-id` | `kafka.consumer.sync_time_max` | The max time taken for a group sync | `DOUBLE_OBSERVABLE_GAUGE` | -| `consumer-coordinator-metrics` | `sync-total` | `client-id` | `kafka.consumer.sync_total` | The total number of group syncs | `DOUBLE_OBSERVABLE_COUNTER` | -| `consumer-fetch-manager-metrics` | `bytes-consumed-rate` | `client-id` | | | | -| `consumer-fetch-manager-metrics` | `bytes-consumed-rate` | `client-id`,`topic` | `kafka.consumer.bytes_consumed_rate` | The average number of bytes consumed per second | `DOUBLE_OBSERVABLE_GAUGE` | -| `consumer-fetch-manager-metrics` | `bytes-consumed-total` | `client-id` | | | | -| `consumer-fetch-manager-metrics` | `bytes-consumed-total` | `client-id`,`topic` | `kafka.consumer.bytes_consumed_total` | The total number of bytes consumed | `DOUBLE_OBSERVABLE_COUNTER` | -| `consumer-fetch-manager-metrics` | `fetch-latency-avg` | `client-id` | `kafka.consumer.fetch_latency_avg` | The average time taken for a fetch request. | `DOUBLE_OBSERVABLE_GAUGE` | -| `consumer-fetch-manager-metrics` | `fetch-latency-max` | `client-id` | `kafka.consumer.fetch_latency_max` | The max time taken for any fetch request. | `DOUBLE_OBSERVABLE_GAUGE` | -| `consumer-fetch-manager-metrics` | `fetch-rate` | `client-id` | `kafka.consumer.fetch_rate` | The number of fetch requests per second. | `DOUBLE_OBSERVABLE_GAUGE` | -| `consumer-fetch-manager-metrics` | `fetch-size-avg` | `client-id` | | | | -| `consumer-fetch-manager-metrics` | `fetch-size-avg` | `client-id`,`topic` | `kafka.consumer.fetch_size_avg` | The average number of bytes fetched per request | `DOUBLE_OBSERVABLE_GAUGE` | -| `consumer-fetch-manager-metrics` | `fetch-size-max` | `client-id` | | | | -| `consumer-fetch-manager-metrics` | `fetch-size-max` | `client-id`,`topic` | `kafka.consumer.fetch_size_max` | The maximum number of bytes fetched per request | `DOUBLE_OBSERVABLE_GAUGE` | -| `consumer-fetch-manager-metrics` | `fetch-throttle-time-avg` | `client-id` | `kafka.consumer.fetch_throttle_time_avg` | The average throttle time in ms | `DOUBLE_OBSERVABLE_GAUGE` | -| `consumer-fetch-manager-metrics` | `fetch-throttle-time-max` | `client-id` | `kafka.consumer.fetch_throttle_time_max` | The maximum throttle time in ms | `DOUBLE_OBSERVABLE_GAUGE` | -| `consumer-fetch-manager-metrics` | `fetch-total` | `client-id` | `kafka.consumer.fetch_total` | The total number of fetch requests. | `DOUBLE_OBSERVABLE_COUNTER` | -| `consumer-fetch-manager-metrics` | `preferred-read-replica` | `client-id`,`topic`,`partition` | | | | -| `consumer-fetch-manager-metrics` | `records-consumed-rate` | `client-id` | | | | -| `consumer-fetch-manager-metrics` | `records-consumed-rate` | `client-id`,`topic` | `kafka.consumer.records_consumed_rate` | The average number of records consumed per second | `DOUBLE_OBSERVABLE_GAUGE` | -| `consumer-fetch-manager-metrics` | `records-consumed-total` | `client-id` | | | | -| `consumer-fetch-manager-metrics` | `records-consumed-total` | `client-id`,`topic` | `kafka.consumer.records_consumed_total` | The total number of records consumed | `DOUBLE_OBSERVABLE_COUNTER` | -| `consumer-fetch-manager-metrics` | `records-lag` | `client-id`,`topic`,`partition` | `kafka.consumer.records_lag` | The latest lag of the partition | `DOUBLE_OBSERVABLE_GAUGE` | -| `consumer-fetch-manager-metrics` | `records-lag-avg` | `client-id`,`topic`,`partition` | `kafka.consumer.records_lag_avg` | The average lag of the partition | `DOUBLE_OBSERVABLE_GAUGE` | -| `consumer-fetch-manager-metrics` | `records-lag-max` | `client-id` | | | | -| `consumer-fetch-manager-metrics` | `records-lag-max` | `client-id`,`topic`,`partition` | `kafka.consumer.records_lag_max` | The maximum lag in terms of number of records for any partition in this window | `DOUBLE_OBSERVABLE_GAUGE` | -| `consumer-fetch-manager-metrics` | `records-lead` | `client-id`,`topic`,`partition` | `kafka.consumer.records_lead` | The latest lead of the partition | `DOUBLE_OBSERVABLE_GAUGE` | -| `consumer-fetch-manager-metrics` | `records-lead-avg` | `client-id`,`topic`,`partition` | `kafka.consumer.records_lead_avg` | The average lead of the partition | `DOUBLE_OBSERVABLE_GAUGE` | -| `consumer-fetch-manager-metrics` | `records-lead-min` | `client-id` | | | | -| `consumer-fetch-manager-metrics` | `records-lead-min` | `client-id`,`topic`,`partition` | `kafka.consumer.records_lead_min` | The minimum lead in terms of number of records for any partition in this window | `DOUBLE_OBSERVABLE_GAUGE` | -| `consumer-fetch-manager-metrics` | `records-per-request-avg` | `client-id` | | | | -| `consumer-fetch-manager-metrics` | `records-per-request-avg` | `client-id`,`topic` | `kafka.consumer.records_per_request_avg` | The average number of records in each request | `DOUBLE_OBSERVABLE_GAUGE` | -| `consumer-metrics` | `connection-close-rate` | `client-id` | `kafka.consumer.connection_close_rate` | The number of connections closed per second | `DOUBLE_OBSERVABLE_GAUGE` | -| `consumer-metrics` | `connection-close-total` | `client-id` | `kafka.consumer.connection_close_total` | The total number of connections closed | `DOUBLE_OBSERVABLE_COUNTER` | -| `consumer-metrics` | `connection-count` | `client-id` | `kafka.consumer.connection_count` | The current number of active connections. | `DOUBLE_OBSERVABLE_GAUGE` | -| `consumer-metrics` | `connection-creation-rate` | `client-id` | `kafka.consumer.connection_creation_rate` | The number of new connections established per second | `DOUBLE_OBSERVABLE_GAUGE` | -| `consumer-metrics` | `connection-creation-total` | `client-id` | `kafka.consumer.connection_creation_total` | The total number of new connections established | `DOUBLE_OBSERVABLE_COUNTER` | -| `consumer-metrics` | `failed-authentication-rate` | `client-id` | `kafka.consumer.failed_authentication_rate` | The number of connections with failed authentication per second | `DOUBLE_OBSERVABLE_GAUGE` | -| `consumer-metrics` | `failed-authentication-total` | `client-id` | `kafka.consumer.failed_authentication_total` | The total number of connections with failed authentication | `DOUBLE_OBSERVABLE_COUNTER` | -| `consumer-metrics` | `failed-reauthentication-rate` | `client-id` | `kafka.consumer.failed_reauthentication_rate` | The number of failed re-authentication of connections per second | `DOUBLE_OBSERVABLE_GAUGE` | -| `consumer-metrics` | `failed-reauthentication-total` | `client-id` | `kafka.consumer.failed_reauthentication_total` | The total number of failed re-authentication of connections | `DOUBLE_OBSERVABLE_COUNTER` | -| `consumer-metrics` | `incoming-byte-rate` | `client-id` | | | | -| `consumer-metrics` | `incoming-byte-total` | `client-id` | | | | -| `consumer-metrics` | `io-ratio` | `client-id` | `kafka.consumer.io_ratio` | The fraction of time the I/O thread spent doing I/O | `DOUBLE_OBSERVABLE_GAUGE` | -| `consumer-metrics` | `io-time-ns-avg` | `client-id` | `kafka.consumer.io_time_ns_avg` | The average length of time for I/O per select call in nanoseconds. | `DOUBLE_OBSERVABLE_GAUGE` | -| `consumer-metrics` | `io-wait-ratio` | `client-id` | `kafka.consumer.io_wait_ratio` | The fraction of time the I/O thread spent waiting | `DOUBLE_OBSERVABLE_GAUGE` | -| `consumer-metrics` | `io-wait-time-ns-avg` | `client-id` | `kafka.consumer.io_wait_time_ns_avg` | The average length of time the I/O thread spent waiting for a socket ready for reads or writes in nanoseconds. | `DOUBLE_OBSERVABLE_GAUGE` | -| `consumer-metrics` | `io-waittime-total` | `client-id` | `kafka.consumer.io_waittime_total` | The total time the I/O thread spent waiting | `DOUBLE_OBSERVABLE_COUNTER` | -| `consumer-metrics` | `iotime-total` | `client-id` | `kafka.consumer.iotime_total` | The total time the I/O thread spent doing I/O | `DOUBLE_OBSERVABLE_COUNTER` | -| `consumer-metrics` | `last-poll-seconds-ago` | `client-id` | `kafka.consumer.last_poll_seconds_ago` | The number of seconds since the last poll() invocation. | `DOUBLE_OBSERVABLE_GAUGE` | -| `consumer-metrics` | `network-io-rate` | `client-id` | `kafka.consumer.network_io_rate` | The number of network operations (reads or writes) on all connections per second | `DOUBLE_OBSERVABLE_GAUGE` | -| `consumer-metrics` | `network-io-total` | `client-id` | `kafka.consumer.network_io_total` | The total number of network operations (reads or writes) on all connections | `DOUBLE_OBSERVABLE_COUNTER` | -| `consumer-metrics` | `outgoing-byte-rate` | `client-id` | | | | -| `consumer-metrics` | `outgoing-byte-total` | `client-id` | | | | -| `consumer-metrics` | `poll-idle-ratio-avg` | `client-id` | `kafka.consumer.poll_idle_ratio_avg` | The average fraction of time the consumer's poll() is idle as opposed to waiting for the user code to process records. | `DOUBLE_OBSERVABLE_GAUGE` | -| `consumer-metrics` | `reauthentication-latency-avg` | `client-id` | `kafka.consumer.reauthentication_latency_avg` | The average latency observed due to re-authentication | `DOUBLE_OBSERVABLE_GAUGE` | -| `consumer-metrics` | `reauthentication-latency-max` | `client-id` | `kafka.consumer.reauthentication_latency_max` | The max latency observed due to re-authentication | `DOUBLE_OBSERVABLE_GAUGE` | -| `consumer-metrics` | `request-rate` | `client-id` | | | | -| `consumer-metrics` | `request-size-avg` | `client-id` | | | | -| `consumer-metrics` | `request-size-max` | `client-id` | | | | -| `consumer-metrics` | `request-total` | `client-id` | | | | -| `consumer-metrics` | `response-rate` | `client-id` | | | | -| `consumer-metrics` | `response-total` | `client-id` | | | | -| `consumer-metrics` | `select-rate` | `client-id` | `kafka.consumer.select_rate` | The number of times the I/O layer checked for new I/O to perform per second | `DOUBLE_OBSERVABLE_GAUGE` | -| `consumer-metrics` | `select-total` | `client-id` | `kafka.consumer.select_total` | The total number of times the I/O layer checked for new I/O to perform | `DOUBLE_OBSERVABLE_COUNTER` | -| `consumer-metrics` | `successful-authentication-no-reauth-total` | `client-id` | `kafka.consumer.successful_authentication_no_reauth_total` | The total number of connections with successful authentication where the client does not support re-authentication | `DOUBLE_OBSERVABLE_COUNTER` | -| `consumer-metrics` | `successful-authentication-rate` | `client-id` | `kafka.consumer.successful_authentication_rate` | The number of connections with successful authentication per second | `DOUBLE_OBSERVABLE_GAUGE` | -| `consumer-metrics` | `successful-authentication-total` | `client-id` | `kafka.consumer.successful_authentication_total` | The total number of connections with successful authentication | `DOUBLE_OBSERVABLE_COUNTER` | -| `consumer-metrics` | `successful-reauthentication-rate` | `client-id` | `kafka.consumer.successful_reauthentication_rate` | The number of successful re-authentication of connections per second | `DOUBLE_OBSERVABLE_GAUGE` | -| `consumer-metrics` | `successful-reauthentication-total` | `client-id` | `kafka.consumer.successful_reauthentication_total` | The total number of successful re-authentication of connections | `DOUBLE_OBSERVABLE_COUNTER` | -| `consumer-metrics` | `time-between-poll-avg` | `client-id` | `kafka.consumer.time_between_poll_avg` | The average delay between invocations of poll(). | `DOUBLE_OBSERVABLE_GAUGE` | -| `consumer-metrics` | `time-between-poll-max` | `client-id` | `kafka.consumer.time_between_poll_max` | The max delay between invocations of poll(). | `DOUBLE_OBSERVABLE_GAUGE` | -| `consumer-node-metrics` | `incoming-byte-rate` | `client-id`,`node-id` | `kafka.consumer.incoming_byte_rate` | The number of bytes read off all sockets per second | `DOUBLE_OBSERVABLE_GAUGE` | -| `consumer-node-metrics` | `incoming-byte-total` | `client-id`,`node-id` | `kafka.consumer.incoming_byte_total` | The total number of bytes read off all sockets | `DOUBLE_OBSERVABLE_COUNTER` | -| `consumer-node-metrics` | `outgoing-byte-rate` | `client-id`,`node-id` | `kafka.consumer.outgoing_byte_rate` | The number of outgoing bytes sent to all servers per second | `DOUBLE_OBSERVABLE_GAUGE` | -| `consumer-node-metrics` | `outgoing-byte-total` | `client-id`,`node-id` | `kafka.consumer.outgoing_byte_total` | The total number of outgoing bytes sent to all servers | `DOUBLE_OBSERVABLE_COUNTER` | -| `consumer-node-metrics` | `request-latency-avg` | `client-id`,`node-id` | `kafka.consumer.request_latency_avg` | | `DOUBLE_OBSERVABLE_GAUGE` | -| `consumer-node-metrics` | `request-latency-max` | `client-id`,`node-id` | `kafka.consumer.request_latency_max` | | `DOUBLE_OBSERVABLE_GAUGE` | -| `consumer-node-metrics` | `request-rate` | `client-id`,`node-id` | `kafka.consumer.request_rate` | The number of requests sent per second | `DOUBLE_OBSERVABLE_GAUGE` | -| `consumer-node-metrics` | `request-size-avg` | `client-id`,`node-id` | `kafka.consumer.request_size_avg` | The average size of requests sent. | `DOUBLE_OBSERVABLE_GAUGE` | -| `consumer-node-metrics` | `request-size-max` | `client-id`,`node-id` | `kafka.consumer.request_size_max` | The maximum size of any request sent. | `DOUBLE_OBSERVABLE_GAUGE` | -| `consumer-node-metrics` | `request-total` | `client-id`,`node-id` | `kafka.consumer.request_total` | The total number of requests sent | `DOUBLE_OBSERVABLE_COUNTER` | -| `consumer-node-metrics` | `response-rate` | `client-id`,`node-id` | `kafka.consumer.response_rate` | The number of responses received per second | `DOUBLE_OBSERVABLE_GAUGE` | -| `consumer-node-metrics` | `response-total` | `client-id`,`node-id` | `kafka.consumer.response_total` | The total number of responses received | `DOUBLE_OBSERVABLE_COUNTER` | -| `kafka-metrics-count` | `count` | `client-id` | | | | -| `producer-metrics` | `batch-size-avg` | `client-id` | `kafka.producer.batch_size_avg` | The average number of bytes sent per partition per-request. | `DOUBLE_OBSERVABLE_GAUGE` | -| `producer-metrics` | `batch-size-max` | `client-id` | `kafka.producer.batch_size_max` | The max number of bytes sent per partition per-request. | `DOUBLE_OBSERVABLE_GAUGE` | -| `producer-metrics` | `batch-split-rate` | `client-id` | `kafka.producer.batch_split_rate` | The average number of batch splits per second | `DOUBLE_OBSERVABLE_GAUGE` | -| `producer-metrics` | `batch-split-total` | `client-id` | `kafka.producer.batch_split_total` | The total number of batch splits | `DOUBLE_OBSERVABLE_COUNTER` | -| `producer-metrics` | `buffer-available-bytes` | `client-id` | `kafka.producer.buffer_available_bytes` | The total amount of buffer memory that is not being used (either unallocated or in the free list). | `DOUBLE_OBSERVABLE_GAUGE` | -| `producer-metrics` | `buffer-exhausted-rate` | `client-id` | `kafka.producer.buffer_exhausted_rate` | The average per-second number of record sends that are dropped due to buffer exhaustion | `DOUBLE_OBSERVABLE_GAUGE` | -| `producer-metrics` | `buffer-exhausted-total` | `client-id` | `kafka.producer.buffer_exhausted_total` | The total number of record sends that are dropped due to buffer exhaustion | `DOUBLE_OBSERVABLE_COUNTER` | -| `producer-metrics` | `buffer-total-bytes` | `client-id` | `kafka.producer.buffer_total_bytes` | The maximum amount of buffer memory the client can use (whether or not it is currently used). | `DOUBLE_OBSERVABLE_GAUGE` | -| `producer-metrics` | `bufferpool-wait-ratio` | `client-id` | `kafka.producer.bufferpool_wait_ratio` | The fraction of time an appender waits for space allocation. | `DOUBLE_OBSERVABLE_GAUGE` | -| `producer-metrics` | `bufferpool-wait-time-total` | `client-id` | `kafka.producer.bufferpool_wait_time_total` | The total time an appender waits for space allocation. | `DOUBLE_OBSERVABLE_COUNTER` | -| `producer-metrics` | `compression-rate-avg` | `client-id` | `kafka.producer.compression_rate_avg` | The average compression rate of record batches. | `DOUBLE_OBSERVABLE_GAUGE` | -| `producer-metrics` | `connection-close-rate` | `client-id` | `kafka.producer.connection_close_rate` | The number of connections closed per second | `DOUBLE_OBSERVABLE_GAUGE` | -| `producer-metrics` | `connection-close-total` | `client-id` | `kafka.producer.connection_close_total` | The total number of connections closed | `DOUBLE_OBSERVABLE_COUNTER` | -| `producer-metrics` | `connection-count` | `client-id` | `kafka.producer.connection_count` | The current number of active connections. | `DOUBLE_OBSERVABLE_GAUGE` | -| `producer-metrics` | `connection-creation-rate` | `client-id` | `kafka.producer.connection_creation_rate` | The number of new connections established per second | `DOUBLE_OBSERVABLE_GAUGE` | -| `producer-metrics` | `connection-creation-total` | `client-id` | `kafka.producer.connection_creation_total` | The total number of new connections established | `DOUBLE_OBSERVABLE_COUNTER` | -| `producer-metrics` | `failed-authentication-rate` | `client-id` | `kafka.producer.failed_authentication_rate` | The number of connections with failed authentication per second | `DOUBLE_OBSERVABLE_GAUGE` | -| `producer-metrics` | `failed-authentication-total` | `client-id` | `kafka.producer.failed_authentication_total` | The total number of connections with failed authentication | `DOUBLE_OBSERVABLE_COUNTER` | -| `producer-metrics` | `failed-reauthentication-rate` | `client-id` | `kafka.producer.failed_reauthentication_rate` | The number of failed re-authentication of connections per second | `DOUBLE_OBSERVABLE_GAUGE` | -| `producer-metrics` | `failed-reauthentication-total` | `client-id` | `kafka.producer.failed_reauthentication_total` | The total number of failed re-authentication of connections | `DOUBLE_OBSERVABLE_COUNTER` | -| `producer-metrics` | `incoming-byte-rate` | `client-id` | | | | -| `producer-metrics` | `incoming-byte-total` | `client-id` | | | | -| `producer-metrics` | `io-ratio` | `client-id` | `kafka.producer.io_ratio` | The fraction of time the I/O thread spent doing I/O | `DOUBLE_OBSERVABLE_GAUGE` | -| `producer-metrics` | `io-time-ns-avg` | `client-id` | `kafka.producer.io_time_ns_avg` | The average length of time for I/O per select call in nanoseconds. | `DOUBLE_OBSERVABLE_GAUGE` | -| `producer-metrics` | `io-wait-ratio` | `client-id` | `kafka.producer.io_wait_ratio` | The fraction of time the I/O thread spent waiting | `DOUBLE_OBSERVABLE_GAUGE` | -| `producer-metrics` | `io-wait-time-ns-avg` | `client-id` | `kafka.producer.io_wait_time_ns_avg` | The average length of time the I/O thread spent waiting for a socket ready for reads or writes in nanoseconds. | `DOUBLE_OBSERVABLE_GAUGE` | -| `producer-metrics` | `io-waittime-total` | `client-id` | `kafka.producer.io_waittime_total` | The total time the I/O thread spent waiting | `DOUBLE_OBSERVABLE_COUNTER` | -| `producer-metrics` | `iotime-total` | `client-id` | `kafka.producer.iotime_total` | The total time the I/O thread spent doing I/O | `DOUBLE_OBSERVABLE_COUNTER` | -| `producer-metrics` | `metadata-age` | `client-id` | `kafka.producer.metadata_age` | The age in seconds of the current producer metadata being used. | `DOUBLE_OBSERVABLE_GAUGE` | -| `producer-metrics` | `network-io-rate` | `client-id` | `kafka.producer.network_io_rate` | The number of network operations (reads or writes) on all connections per second | `DOUBLE_OBSERVABLE_GAUGE` | -| `producer-metrics` | `network-io-total` | `client-id` | `kafka.producer.network_io_total` | The total number of network operations (reads or writes) on all connections | `DOUBLE_OBSERVABLE_COUNTER` | -| `producer-metrics` | `outgoing-byte-rate` | `client-id` | | | | -| `producer-metrics` | `outgoing-byte-total` | `client-id` | | | | -| `producer-metrics` | `produce-throttle-time-avg` | `client-id` | `kafka.producer.produce_throttle_time_avg` | The average time in ms a request was throttled by a broker | `DOUBLE_OBSERVABLE_GAUGE` | -| `producer-metrics` | `produce-throttle-time-max` | `client-id` | `kafka.producer.produce_throttle_time_max` | The maximum time in ms a request was throttled by a broker | `DOUBLE_OBSERVABLE_GAUGE` | -| `producer-metrics` | `reauthentication-latency-avg` | `client-id` | `kafka.producer.reauthentication_latency_avg` | The average latency observed due to re-authentication | `DOUBLE_OBSERVABLE_GAUGE` | -| `producer-metrics` | `reauthentication-latency-max` | `client-id` | `kafka.producer.reauthentication_latency_max` | The max latency observed due to re-authentication | `DOUBLE_OBSERVABLE_GAUGE` | -| `producer-metrics` | `record-error-rate` | `client-id` | | | | -| `producer-metrics` | `record-error-total` | `client-id` | | | | -| `producer-metrics` | `record-queue-time-avg` | `client-id` | `kafka.producer.record_queue_time_avg` | The average time in ms record batches spent in the send buffer. | `DOUBLE_OBSERVABLE_GAUGE` | -| `producer-metrics` | `record-queue-time-max` | `client-id` | `kafka.producer.record_queue_time_max` | The maximum time in ms record batches spent in the send buffer. | `DOUBLE_OBSERVABLE_GAUGE` | -| `producer-metrics` | `record-retry-rate` | `client-id` | | | | -| `producer-metrics` | `record-retry-total` | `client-id` | | | | -| `producer-metrics` | `record-send-rate` | `client-id` | | | | -| `producer-metrics` | `record-send-total` | `client-id` | | | | -| `producer-metrics` | `record-size-avg` | `client-id` | `kafka.producer.record_size_avg` | The average record size | `DOUBLE_OBSERVABLE_GAUGE` | -| `producer-metrics` | `record-size-max` | `client-id` | `kafka.producer.record_size_max` | The maximum record size | `DOUBLE_OBSERVABLE_GAUGE` | -| `producer-metrics` | `records-per-request-avg` | `client-id` | `kafka.producer.records_per_request_avg` | The average number of records per request. | `DOUBLE_OBSERVABLE_GAUGE` | -| `producer-metrics` | `request-latency-avg` | `client-id` | | | | -| `producer-metrics` | `request-latency-max` | `client-id` | | | | -| `producer-metrics` | `request-rate` | `client-id` | | | | -| `producer-metrics` | `request-size-avg` | `client-id` | | | | -| `producer-metrics` | `request-size-max` | `client-id` | | | | -| `producer-metrics` | `request-total` | `client-id` | | | | -| `producer-metrics` | `requests-in-flight` | `client-id` | `kafka.producer.requests_in_flight` | The current number of in-flight requests awaiting a response. | `DOUBLE_OBSERVABLE_GAUGE` | -| `producer-metrics` | `response-rate` | `client-id` | | | | -| `producer-metrics` | `response-total` | `client-id` | | | | -| `producer-metrics` | `select-rate` | `client-id` | `kafka.producer.select_rate` | The number of times the I/O layer checked for new I/O to perform per second | `DOUBLE_OBSERVABLE_GAUGE` | -| `producer-metrics` | `select-total` | `client-id` | `kafka.producer.select_total` | The total number of times the I/O layer checked for new I/O to perform | `DOUBLE_OBSERVABLE_COUNTER` | -| `producer-metrics` | `successful-authentication-no-reauth-total` | `client-id` | `kafka.producer.successful_authentication_no_reauth_total` | The total number of connections with successful authentication where the client does not support re-authentication | `DOUBLE_OBSERVABLE_COUNTER` | -| `producer-metrics` | `successful-authentication-rate` | `client-id` | `kafka.producer.successful_authentication_rate` | The number of connections with successful authentication per second | `DOUBLE_OBSERVABLE_GAUGE` | -| `producer-metrics` | `successful-authentication-total` | `client-id` | `kafka.producer.successful_authentication_total` | The total number of connections with successful authentication | `DOUBLE_OBSERVABLE_COUNTER` | -| `producer-metrics` | `successful-reauthentication-rate` | `client-id` | `kafka.producer.successful_reauthentication_rate` | The number of successful re-authentication of connections per second | `DOUBLE_OBSERVABLE_GAUGE` | -| `producer-metrics` | `successful-reauthentication-total` | `client-id` | `kafka.producer.successful_reauthentication_total` | The total number of successful re-authentication of connections | `DOUBLE_OBSERVABLE_COUNTER` | -| `producer-metrics` | `waiting-threads` | `client-id` | `kafka.producer.waiting_threads` | The number of user threads blocked waiting for buffer memory to enqueue their records | `DOUBLE_OBSERVABLE_GAUGE` | -| `producer-node-metrics` | `incoming-byte-rate` | `client-id`,`node-id` | `kafka.producer.incoming_byte_rate` | The number of bytes read off all sockets per second | `DOUBLE_OBSERVABLE_GAUGE` | -| `producer-node-metrics` | `incoming-byte-total` | `client-id`,`node-id` | `kafka.producer.incoming_byte_total` | The total number of bytes read off all sockets | `DOUBLE_OBSERVABLE_COUNTER` | -| `producer-node-metrics` | `outgoing-byte-rate` | `client-id`,`node-id` | `kafka.producer.outgoing_byte_rate` | The number of outgoing bytes sent to all servers per second | `DOUBLE_OBSERVABLE_GAUGE` | -| `producer-node-metrics` | `outgoing-byte-total` | `client-id`,`node-id` | `kafka.producer.outgoing_byte_total` | The total number of outgoing bytes sent to all servers | `DOUBLE_OBSERVABLE_COUNTER` | -| `producer-node-metrics` | `request-latency-avg` | `client-id`,`node-id` | `kafka.producer.request_latency_avg` | The average request latency in ms | `DOUBLE_OBSERVABLE_GAUGE` | -| `producer-node-metrics` | `request-latency-max` | `client-id`,`node-id` | `kafka.producer.request_latency_max` | The maximum request latency in ms | `DOUBLE_OBSERVABLE_GAUGE` | -| `producer-node-metrics` | `request-rate` | `client-id`,`node-id` | `kafka.producer.request_rate` | The number of requests sent per second | `DOUBLE_OBSERVABLE_GAUGE` | -| `producer-node-metrics` | `request-size-avg` | `client-id`,`node-id` | `kafka.producer.request_size_avg` | The average size of requests sent. | `DOUBLE_OBSERVABLE_GAUGE` | -| `producer-node-metrics` | `request-size-max` | `client-id`,`node-id` | `kafka.producer.request_size_max` | The maximum size of any request sent. | `DOUBLE_OBSERVABLE_GAUGE` | -| `producer-node-metrics` | `request-total` | `client-id`,`node-id` | `kafka.producer.request_total` | The total number of requests sent | `DOUBLE_OBSERVABLE_COUNTER` | -| `producer-node-metrics` | `response-rate` | `client-id`,`node-id` | `kafka.producer.response_rate` | The number of responses received per second | `DOUBLE_OBSERVABLE_GAUGE` | -| `producer-node-metrics` | `response-total` | `client-id`,`node-id` | `kafka.producer.response_total` | The total number of responses received | `DOUBLE_OBSERVABLE_COUNTER` | -| `producer-topic-metrics` | `byte-rate` | `client-id`,`topic` | `kafka.producer.byte_rate` | The average number of bytes sent per second for a topic. | `DOUBLE_OBSERVABLE_GAUGE` | -| `producer-topic-metrics` | `byte-total` | `client-id`,`topic` | `kafka.producer.byte_total` | The total number of bytes sent for a topic. | `DOUBLE_OBSERVABLE_COUNTER` | -| `producer-topic-metrics` | `compression-rate` | `client-id`,`topic` | `kafka.producer.compression_rate` | The average compression rate of record batches for a topic. | `DOUBLE_OBSERVABLE_GAUGE` | -| `producer-topic-metrics` | `record-error-rate` | `client-id`,`topic` | `kafka.producer.record_error_rate` | The average per-second number of record sends that resulted in errors | `DOUBLE_OBSERVABLE_GAUGE` | -| `producer-topic-metrics` | `record-error-total` | `client-id`,`topic` | `kafka.producer.record_error_total` | The total number of record sends that resulted in errors | `DOUBLE_OBSERVABLE_COUNTER` | -| `producer-topic-metrics` | `record-retry-rate` | `client-id`,`topic` | `kafka.producer.record_retry_rate` | The average per-second number of retried record sends | `DOUBLE_OBSERVABLE_GAUGE` | -| `producer-topic-metrics` | `record-retry-total` | `client-id`,`topic` | `kafka.producer.record_retry_total` | The total number of retried record sends | `DOUBLE_OBSERVABLE_COUNTER` | -| `producer-topic-metrics` | `record-send-rate` | `client-id`,`topic` | `kafka.producer.record_send_rate` | The average number of records sent per second. | `DOUBLE_OBSERVABLE_GAUGE` | -| `producer-topic-metrics` | `record-send-total` | `client-id`,`topic` | `kafka.producer.record_send_total` | The total number of records sent. | `DOUBLE_OBSERVABLE_COUNTER` | +| Metric Group | Metric Name | Attribute Keys | Instrument Name | Instrument Description | Instrument Type | +|----------------------------------|---------------------------------------------|---------------------------------|------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------| +| `consumer-coordinator-metrics` | `assigned-partitions` | `client-id` | `kafka.consumer.assigned_partitions` | The number of partitions currently assigned to this consumer. | `DOUBLE_OBSERVABLE_GAUGE` | +| `consumer-coordinator-metrics` | `commit-latency-avg` | `client-id` | `kafka.consumer.commit_latency_avg` | The average time taken for a commit request. | `DOUBLE_OBSERVABLE_GAUGE` | +| `consumer-coordinator-metrics` | `commit-latency-max` | `client-id` | `kafka.consumer.commit_latency_max` | The max time taken for a commit request. | `DOUBLE_OBSERVABLE_GAUGE` | +| `consumer-coordinator-metrics` | `commit-rate` | `client-id` | `kafka.consumer.commit_rate` | The number of commit calls per second. | `DOUBLE_OBSERVABLE_GAUGE` | +| `consumer-coordinator-metrics` | `commit-total` | `client-id` | `kafka.consumer.commit_total` | The total number of commit calls. | `DOUBLE_OBSERVABLE_COUNTER` | +| `consumer-coordinator-metrics` | `failed-rebalance-rate-per-hour` | `client-id` | `kafka.consumer.failed_rebalance_rate_per_hour` | The number of failed rebalance events per hour. | `DOUBLE_OBSERVABLE_GAUGE` | +| `consumer-coordinator-metrics` | `failed-rebalance-total` | `client-id` | `kafka.consumer.failed_rebalance_total` | The total number of failed rebalance events. | `DOUBLE_OBSERVABLE_COUNTER` | +| `consumer-coordinator-metrics` | `heartbeat-rate` | `client-id` | `kafka.consumer.heartbeat_rate` | The number of heartbeats per second. | `DOUBLE_OBSERVABLE_GAUGE` | +| `consumer-coordinator-metrics` | `heartbeat-response-time-max` | `client-id` | `kafka.consumer.heartbeat_response_time_max` | The max time taken to receive a response to a heartbeat request. | `DOUBLE_OBSERVABLE_GAUGE` | +| `consumer-coordinator-metrics` | `heartbeat-total` | `client-id` | `kafka.consumer.heartbeat_total` | The total number of heartbeats. | `DOUBLE_OBSERVABLE_COUNTER` | +| `consumer-coordinator-metrics` | `join-rate` | `client-id` | `kafka.consumer.join_rate` | The number of group joins per second. | `DOUBLE_OBSERVABLE_GAUGE` | +| `consumer-coordinator-metrics` | `join-time-avg` | `client-id` | `kafka.consumer.join_time_avg` | The average time taken for a group rejoin. | `DOUBLE_OBSERVABLE_GAUGE` | +| `consumer-coordinator-metrics` | `join-time-max` | `client-id` | `kafka.consumer.join_time_max` | The max time taken for a group rejoin. | `DOUBLE_OBSERVABLE_GAUGE` | +| `consumer-coordinator-metrics` | `join-total` | `client-id` | `kafka.consumer.join_total` | The total number of group joins. | `DOUBLE_OBSERVABLE_COUNTER` | +| `consumer-coordinator-metrics` | `last-heartbeat-seconds-ago` | `client-id` | `kafka.consumer.last_heartbeat_seconds_ago` | The number of seconds since the last coordinator heartbeat was sent. | `DOUBLE_OBSERVABLE_GAUGE` | +| `consumer-coordinator-metrics` | `last-rebalance-seconds-ago` | `client-id` | `kafka.consumer.last_rebalance_seconds_ago` | The number of seconds since the last successful rebalance event. | `DOUBLE_OBSERVABLE_GAUGE` | +| `consumer-coordinator-metrics` | `partition-assigned-latency-avg` | `client-id` | `kafka.consumer.partition_assigned_latency_avg` | The average time taken for a partition-assigned rebalance listener callback. | `DOUBLE_OBSERVABLE_GAUGE` | +| `consumer-coordinator-metrics` | `partition-assigned-latency-max` | `client-id` | `kafka.consumer.partition_assigned_latency_max` | The max time taken for a partition-assigned rebalance listener callback. | `DOUBLE_OBSERVABLE_GAUGE` | +| `consumer-coordinator-metrics` | `partition-lost-latency-avg` | `client-id` | `kafka.consumer.partition_lost_latency_avg` | The average time taken for a partition-lost rebalance listener callback. | `DOUBLE_OBSERVABLE_GAUGE` | +| `consumer-coordinator-metrics` | `partition-lost-latency-max` | `client-id` | `kafka.consumer.partition_lost_latency_max` | The max time taken for a partition-lost rebalance listener callback. | `DOUBLE_OBSERVABLE_GAUGE` | +| `consumer-coordinator-metrics` | `partition-revoked-latency-avg` | `client-id` | `kafka.consumer.partition_revoked_latency_avg` | The average time taken for a partition-revoked rebalance listener callback. | `DOUBLE_OBSERVABLE_GAUGE` | +| `consumer-coordinator-metrics` | `partition-revoked-latency-max` | `client-id` | `kafka.consumer.partition_revoked_latency_max` | The max time taken for a partition-revoked rebalance listener callback. | `DOUBLE_OBSERVABLE_GAUGE` | +| `consumer-coordinator-metrics` | `rebalance-latency-avg` | `client-id` | `kafka.consumer.rebalance_latency_avg` | The average time taken for a group to complete a successful rebalance, which may be composed of several failed re-trials until it succeeded. | `DOUBLE_OBSERVABLE_GAUGE` | +| `consumer-coordinator-metrics` | `rebalance-latency-max` | `client-id` | `kafka.consumer.rebalance_latency_max` | The max time taken for a group to complete a successful rebalance, which may be composed of several failed re-trials until it succeeded. | `DOUBLE_OBSERVABLE_GAUGE` | +| `consumer-coordinator-metrics` | `rebalance-latency-total` | `client-id` | `kafka.consumer.rebalance_latency_total` | The total number of milliseconds this consumer has spent in successful rebalances since creation. | `DOUBLE_OBSERVABLE_COUNTER` | +| `consumer-coordinator-metrics` | `rebalance-rate-per-hour` | `client-id` | `kafka.consumer.rebalance_rate_per_hour` | The number of successful rebalance events per hour, each event is composed of several failed re-trials until it succeeded. | `DOUBLE_OBSERVABLE_GAUGE` | +| `consumer-coordinator-metrics` | `rebalance-total` | `client-id` | `kafka.consumer.rebalance_total` | The total number of successful rebalance events, each event is composed of several failed re-trials until it succeeded. | `DOUBLE_OBSERVABLE_COUNTER` | +| `consumer-coordinator-metrics` | `sync-rate` | `client-id` | `kafka.consumer.sync_rate` | The number of group syncs per second. | `DOUBLE_OBSERVABLE_GAUGE` | +| `consumer-coordinator-metrics` | `sync-time-avg` | `client-id` | `kafka.consumer.sync_time_avg` | The average time taken for a group sync. | `DOUBLE_OBSERVABLE_GAUGE` | +| `consumer-coordinator-metrics` | `sync-time-max` | `client-id` | `kafka.consumer.sync_time_max` | The max time taken for a group sync. | `DOUBLE_OBSERVABLE_GAUGE` | +| `consumer-coordinator-metrics` | `sync-total` | `client-id` | `kafka.consumer.sync_total` | The total number of group syncs. | `DOUBLE_OBSERVABLE_COUNTER` | +| `consumer-fetch-manager-metrics` | `bytes-consumed-rate` | `client-id`,`topic` | `kafka.consumer.bytes_consumed_rate` | The average number of bytes consumed per second. | `DOUBLE_OBSERVABLE_GAUGE` | +| `consumer-fetch-manager-metrics` | `bytes-consumed-total` | `client-id`,`topic` | `kafka.consumer.bytes_consumed_total` | The total number of bytes consumed. | `DOUBLE_OBSERVABLE_COUNTER` | +| `consumer-fetch-manager-metrics` | `fetch-latency-avg` | `client-id` | `kafka.consumer.fetch_latency_avg` | The average time taken for a fetch request. | `DOUBLE_OBSERVABLE_GAUGE` | +| `consumer-fetch-manager-metrics` | `fetch-latency-max` | `client-id` | `kafka.consumer.fetch_latency_max` | The max time taken for any fetch request. | `DOUBLE_OBSERVABLE_GAUGE` | +| `consumer-fetch-manager-metrics` | `fetch-rate` | `client-id` | `kafka.consumer.fetch_rate` | The number of fetch requests per second. | `DOUBLE_OBSERVABLE_GAUGE` | +| `consumer-fetch-manager-metrics` | `fetch-size-avg` | `client-id`,`topic` | `kafka.consumer.fetch_size_avg` | The average number of bytes fetched per request. | `DOUBLE_OBSERVABLE_GAUGE` | +| `consumer-fetch-manager-metrics` | `fetch-size-max` | `client-id`,`topic` | `kafka.consumer.fetch_size_max` | The maximum number of bytes fetched per request. | `DOUBLE_OBSERVABLE_GAUGE` | +| `consumer-fetch-manager-metrics` | `fetch-throttle-time-avg` | `client-id` | `kafka.consumer.fetch_throttle_time_avg` | The average throttle time in ms. | `DOUBLE_OBSERVABLE_GAUGE` | +| `consumer-fetch-manager-metrics` | `fetch-throttle-time-max` | `client-id` | `kafka.consumer.fetch_throttle_time_max` | The maximum throttle time in ms. | `DOUBLE_OBSERVABLE_GAUGE` | +| `consumer-fetch-manager-metrics` | `fetch-total` | `client-id` | `kafka.consumer.fetch_total` | The total number of fetch requests. | `DOUBLE_OBSERVABLE_COUNTER` | +| `consumer-fetch-manager-metrics` | `records-consumed-rate` | `client-id`,`topic` | `kafka.consumer.records_consumed_rate` | The average number of records consumed per second. | `DOUBLE_OBSERVABLE_GAUGE` | +| `consumer-fetch-manager-metrics` | `records-consumed-total` | `client-id`,`topic` | `kafka.consumer.records_consumed_total` | The total number of records consumed. | `DOUBLE_OBSERVABLE_COUNTER` | +| `consumer-fetch-manager-metrics` | `records-lag` | `client-id`,`topic`,`partition` | `kafka.consumer.records_lag` | The latest lag of the partition. | `DOUBLE_OBSERVABLE_GAUGE` | +| `consumer-fetch-manager-metrics` | `records-lag-avg` | `client-id`,`topic`,`partition` | `kafka.consumer.records_lag_avg` | The average lag of the partition. | `DOUBLE_OBSERVABLE_GAUGE` | +| `consumer-fetch-manager-metrics` | `records-lag-max` | `client-id`,`topic`,`partition` | `kafka.consumer.records_lag_max` | The maximum lag in terms of number of records for any partition in this window. NOTE: This is based on current offset and not committed offset. | `DOUBLE_OBSERVABLE_GAUGE` | +| `consumer-fetch-manager-metrics` | `records-lead` | `client-id`,`topic`,`partition` | `kafka.consumer.records_lead` | The latest lead of the partition. | `DOUBLE_OBSERVABLE_GAUGE` | +| `consumer-fetch-manager-metrics` | `records-lead-avg` | `client-id`,`topic`,`partition` | `kafka.consumer.records_lead_avg` | The average lead of the partition. | `DOUBLE_OBSERVABLE_GAUGE` | +| `consumer-fetch-manager-metrics` | `records-lead-min` | `client-id`,`topic`,`partition` | `kafka.consumer.records_lead_min` | The minimum lead in terms of number of records for any partition in this window. | `DOUBLE_OBSERVABLE_GAUGE` | +| `consumer-fetch-manager-metrics` | `records-per-request-avg` | `client-id`,`topic` | `kafka.consumer.records_per_request_avg` | The average number of records in each request. | `DOUBLE_OBSERVABLE_GAUGE` | +| `consumer-metrics` | `commit-sync-time-ns-total` | `client-id` | `kafka.consumer.commit_sync_time_ns_total` | The total time the consumer has spent in commitSync in nanoseconds. | `DOUBLE_OBSERVABLE_COUNTER` | +| `consumer-metrics` | `committed-time-ns-total` | `client-id` | `kafka.consumer.committed_time_ns_total` | The total time the consumer has spent in committed in nanoseconds. | `DOUBLE_OBSERVABLE_COUNTER` | +| `consumer-metrics` | `connection-close-rate` | `client-id` | `kafka.consumer.connection_close_rate` | The number of connections closed per second. | `DOUBLE_OBSERVABLE_GAUGE` | +| `consumer-metrics` | `connection-close-total` | `client-id` | `kafka.consumer.connection_close_total` | The total number of connections closed. | `DOUBLE_OBSERVABLE_COUNTER` | +| `consumer-metrics` | `connection-count` | `client-id` | `kafka.consumer.connection_count` | The current number of active connections. | `DOUBLE_OBSERVABLE_GAUGE` | +| `consumer-metrics` | `connection-creation-rate` | `client-id` | `kafka.consumer.connection_creation_rate` | The number of new connections established per second. | `DOUBLE_OBSERVABLE_GAUGE` | +| `consumer-metrics` | `connection-creation-total` | `client-id` | `kafka.consumer.connection_creation_total` | The total number of new connections established. | `DOUBLE_OBSERVABLE_COUNTER` | +| `consumer-metrics` | `failed-authentication-rate` | `client-id` | `kafka.consumer.failed_authentication_rate` | The number of connections with failed authentication per second. | `DOUBLE_OBSERVABLE_GAUGE` | +| `consumer-metrics` | `failed-authentication-total` | `client-id` | `kafka.consumer.failed_authentication_total` | The total number of connections with failed authentication. | `DOUBLE_OBSERVABLE_COUNTER` | +| `consumer-metrics` | `failed-reauthentication-rate` | `client-id` | `kafka.consumer.failed_reauthentication_rate` | The number of failed re-authentication of connections per second. | `DOUBLE_OBSERVABLE_GAUGE` | +| `consumer-metrics` | `failed-reauthentication-total` | `client-id` | `kafka.consumer.failed_reauthentication_total` | The total number of failed re-authentication of connections. | `DOUBLE_OBSERVABLE_COUNTER` | +| `consumer-metrics` | `io-ratio` | `client-id` | `kafka.consumer.io_ratio` | *Deprecated* The fraction of time the I/O thread spent doing I/O. | `DOUBLE_OBSERVABLE_GAUGE` | +| `consumer-metrics` | `io-time-ns-avg` | `client-id` | `kafka.consumer.io_time_ns_avg` | The average length of time for I/O per select call in nanoseconds. | `DOUBLE_OBSERVABLE_GAUGE` | +| `consumer-metrics` | `io-time-ns-total` | `client-id` | `kafka.consumer.io_time_ns_total` | The total time the I/O thread spent doing I/O. | `DOUBLE_OBSERVABLE_COUNTER` | +| `consumer-metrics` | `io-wait-ratio` | `client-id` | `kafka.consumer.io_wait_ratio` | *Deprecated* The fraction of time the I/O thread spent waiting. | `DOUBLE_OBSERVABLE_GAUGE` | +| `consumer-metrics` | `io-wait-time-ns-avg` | `client-id` | `kafka.consumer.io_wait_time_ns_avg` | The average length of time the I/O thread spent waiting for a socket ready for reads or writes in nanoseconds. | `DOUBLE_OBSERVABLE_GAUGE` | +| `consumer-metrics` | `io-wait-time-ns-total` | `client-id` | `kafka.consumer.io_wait_time_ns_total` | The total time the I/O thread spent waiting. | `DOUBLE_OBSERVABLE_COUNTER` | +| `consumer-metrics` | `io-waittime-total` | `client-id` | `kafka.consumer.io_waittime_total` | *Deprecated* The total time the I/O thread spent waiting. | `DOUBLE_OBSERVABLE_COUNTER` | +| `consumer-metrics` | `iotime-total` | `client-id` | `kafka.consumer.iotime_total` | *Deprecated* The total time the I/O thread spent doing I/O. | `DOUBLE_OBSERVABLE_COUNTER` | +| `consumer-metrics` | `last-poll-seconds-ago` | `client-id` | `kafka.consumer.last_poll_seconds_ago` | The number of seconds since the last poll() invocation. | `DOUBLE_OBSERVABLE_GAUGE` | +| `consumer-metrics` | `network-io-rate` | `client-id` | `kafka.consumer.network_io_rate` | The number of network operations (reads or writes) on all connections per second. | `DOUBLE_OBSERVABLE_GAUGE` | +| `consumer-metrics` | `network-io-total` | `client-id` | `kafka.consumer.network_io_total` | The total number of network operations (reads or writes) on all connections. | `DOUBLE_OBSERVABLE_COUNTER` | +| `consumer-metrics` | `poll-idle-ratio-avg` | `client-id` | `kafka.consumer.poll_idle_ratio_avg` | The average fraction of time the consumer's poll() is idle as opposed to waiting for the user code to process records. | `DOUBLE_OBSERVABLE_GAUGE` | +| `consumer-metrics` | `reauthentication-latency-avg` | `client-id` | `kafka.consumer.reauthentication_latency_avg` | The average latency observed due to re-authentication. | `DOUBLE_OBSERVABLE_GAUGE` | +| `consumer-metrics` | `reauthentication-latency-max` | `client-id` | `kafka.consumer.reauthentication_latency_max` | The max latency observed due to re-authentication. | `DOUBLE_OBSERVABLE_GAUGE` | +| `consumer-metrics` | `select-rate` | `client-id` | `kafka.consumer.select_rate` | The number of times the I/O layer checked for new I/O to perform per second. | `DOUBLE_OBSERVABLE_GAUGE` | +| `consumer-metrics` | `select-total` | `client-id` | `kafka.consumer.select_total` | The total number of times the I/O layer checked for new I/O to perform. | `DOUBLE_OBSERVABLE_COUNTER` | +| `consumer-metrics` | `successful-authentication-no-reauth-total` | `client-id` | `kafka.consumer.successful_authentication_no_reauth_total` | The total number of connections with successful authentication where the client does not support re-authentication. | `DOUBLE_OBSERVABLE_COUNTER` | +| `consumer-metrics` | `successful-authentication-rate` | `client-id` | `kafka.consumer.successful_authentication_rate` | The number of connections with successful authentication per second. | `DOUBLE_OBSERVABLE_GAUGE` | +| `consumer-metrics` | `successful-authentication-total` | `client-id` | `kafka.consumer.successful_authentication_total` | The total number of connections with successful authentication. | `DOUBLE_OBSERVABLE_COUNTER` | +| `consumer-metrics` | `successful-reauthentication-rate` | `client-id` | `kafka.consumer.successful_reauthentication_rate` | The number of successful re-authentication of connections per second. | `DOUBLE_OBSERVABLE_GAUGE` | +| `consumer-metrics` | `successful-reauthentication-total` | `client-id` | `kafka.consumer.successful_reauthentication_total` | The total number of successful re-authentication of connections. | `DOUBLE_OBSERVABLE_COUNTER` | +| `consumer-metrics` | `time-between-poll-avg` | `client-id` | `kafka.consumer.time_between_poll_avg` | The average delay between invocations of poll() in milliseconds. | `DOUBLE_OBSERVABLE_GAUGE` | +| `consumer-metrics` | `time-between-poll-max` | `client-id` | `kafka.consumer.time_between_poll_max` | The max delay between invocations of poll() in milliseconds. | `DOUBLE_OBSERVABLE_GAUGE` | +| `consumer-node-metrics` | `incoming-byte-rate` | `client-id`,`node-id` | `kafka.consumer.incoming_byte_rate` | The number of bytes read off all sockets per second. | `DOUBLE_OBSERVABLE_GAUGE` | +| `consumer-node-metrics` | `incoming-byte-total` | `client-id`,`node-id` | `kafka.consumer.incoming_byte_total` | The total number of bytes read off all sockets. | `DOUBLE_OBSERVABLE_COUNTER` | +| `consumer-node-metrics` | `outgoing-byte-rate` | `client-id`,`node-id` | `kafka.consumer.outgoing_byte_rate` | The number of outgoing bytes sent to all servers per second. | `DOUBLE_OBSERVABLE_GAUGE` | +| `consumer-node-metrics` | `outgoing-byte-total` | `client-id`,`node-id` | `kafka.consumer.outgoing_byte_total` | The total number of outgoing bytes sent to all servers. | `DOUBLE_OBSERVABLE_COUNTER` | +| `consumer-node-metrics` | `request-latency-avg` | `client-id`,`node-id` | `kafka.consumer.request_latency_avg` | The average request latency in ms. | `DOUBLE_OBSERVABLE_GAUGE` | +| `consumer-node-metrics` | `request-latency-max` | `client-id`,`node-id` | `kafka.consumer.request_latency_max` | The maximum request latency in ms. | `DOUBLE_OBSERVABLE_GAUGE` | +| `consumer-node-metrics` | `request-rate` | `client-id`,`node-id` | `kafka.consumer.request_rate` | The number of requests sent per second. | `DOUBLE_OBSERVABLE_GAUGE` | +| `consumer-node-metrics` | `request-size-avg` | `client-id`,`node-id` | `kafka.consumer.request_size_avg` | The average size of requests sent. | `DOUBLE_OBSERVABLE_GAUGE` | +| `consumer-node-metrics` | `request-size-max` | `client-id`,`node-id` | `kafka.consumer.request_size_max` | The maximum size of any request sent. | `DOUBLE_OBSERVABLE_GAUGE` | +| `consumer-node-metrics` | `request-total` | `client-id`,`node-id` | `kafka.consumer.request_total` | The total number of requests sent. | `DOUBLE_OBSERVABLE_COUNTER` | +| `consumer-node-metrics` | `response-rate` | `client-id`,`node-id` | `kafka.consumer.response_rate` | The number of responses received per second. | `DOUBLE_OBSERVABLE_GAUGE` | +| `consumer-node-metrics` | `response-total` | `client-id`,`node-id` | `kafka.consumer.response_total` | The total number of responses received. | `DOUBLE_OBSERVABLE_COUNTER` | +| `producer-metrics` | `batch-size-avg` | `client-id` | `kafka.producer.batch_size_avg` | The average number of bytes sent per partition per-request. | `DOUBLE_OBSERVABLE_GAUGE` | +| `producer-metrics` | `batch-size-max` | `client-id` | `kafka.producer.batch_size_max` | The max number of bytes sent per partition per-request. | `DOUBLE_OBSERVABLE_GAUGE` | +| `producer-metrics` | `batch-split-rate` | `client-id` | `kafka.producer.batch_split_rate` | The average number of batch splits per second. | `DOUBLE_OBSERVABLE_GAUGE` | +| `producer-metrics` | `batch-split-total` | `client-id` | `kafka.producer.batch_split_total` | The total number of batch splits. | `DOUBLE_OBSERVABLE_COUNTER` | +| `producer-metrics` | `buffer-available-bytes` | `client-id` | `kafka.producer.buffer_available_bytes` | The total amount of buffer memory that is not being used (either unallocated or in the free list). | `DOUBLE_OBSERVABLE_GAUGE` | +| `producer-metrics` | `buffer-exhausted-rate` | `client-id` | `kafka.producer.buffer_exhausted_rate` | The average per-second number of record sends that are dropped due to buffer exhaustion. | `DOUBLE_OBSERVABLE_GAUGE` | +| `producer-metrics` | `buffer-exhausted-total` | `client-id` | `kafka.producer.buffer_exhausted_total` | The total number of record sends that are dropped due to buffer exhaustion. | `DOUBLE_OBSERVABLE_COUNTER` | +| `producer-metrics` | `buffer-total-bytes` | `client-id` | `kafka.producer.buffer_total_bytes` | The maximum amount of buffer memory the client can use (whether or not it is currently used). | `DOUBLE_OBSERVABLE_GAUGE` | +| `producer-metrics` | `bufferpool-wait-ratio` | `client-id` | `kafka.producer.bufferpool_wait_ratio` | The fraction of time an appender waits for space allocation. | `DOUBLE_OBSERVABLE_GAUGE` | +| `producer-metrics` | `bufferpool-wait-time-ns-total` | `client-id` | `kafka.producer.bufferpool_wait_time_ns_total` | The total time in nanoseconds an appender waits for space allocation. | `DOUBLE_OBSERVABLE_COUNTER` | +| `producer-metrics` | `bufferpool-wait-time-total` | `client-id` | `kafka.producer.bufferpool_wait_time_total` | *Deprecated* The total time an appender waits for space allocation. | `DOUBLE_OBSERVABLE_COUNTER` | +| `producer-metrics` | `compression-rate-avg` | `client-id` | `kafka.producer.compression_rate_avg` | The average compression rate of record batches, defined as the average ratio of the compressed batch size over the uncompressed size. | `DOUBLE_OBSERVABLE_GAUGE` | +| `producer-metrics` | `connection-close-rate` | `client-id` | `kafka.producer.connection_close_rate` | The number of connections closed per second. | `DOUBLE_OBSERVABLE_GAUGE` | +| `producer-metrics` | `connection-close-total` | `client-id` | `kafka.producer.connection_close_total` | The total number of connections closed. | `DOUBLE_OBSERVABLE_COUNTER` | +| `producer-metrics` | `connection-count` | `client-id` | `kafka.producer.connection_count` | The current number of active connections. | `DOUBLE_OBSERVABLE_GAUGE` | +| `producer-metrics` | `connection-creation-rate` | `client-id` | `kafka.producer.connection_creation_rate` | The number of new connections established per second. | `DOUBLE_OBSERVABLE_GAUGE` | +| `producer-metrics` | `connection-creation-total` | `client-id` | `kafka.producer.connection_creation_total` | The total number of new connections established. | `DOUBLE_OBSERVABLE_COUNTER` | +| `producer-metrics` | `failed-authentication-rate` | `client-id` | `kafka.producer.failed_authentication_rate` | The number of connections with failed authentication per second. | `DOUBLE_OBSERVABLE_GAUGE` | +| `producer-metrics` | `failed-authentication-total` | `client-id` | `kafka.producer.failed_authentication_total` | The total number of connections with failed authentication. | `DOUBLE_OBSERVABLE_COUNTER` | +| `producer-metrics` | `failed-reauthentication-rate` | `client-id` | `kafka.producer.failed_reauthentication_rate` | The number of failed re-authentication of connections per second. | `DOUBLE_OBSERVABLE_GAUGE` | +| `producer-metrics` | `failed-reauthentication-total` | `client-id` | `kafka.producer.failed_reauthentication_total` | The total number of failed re-authentication of connections. | `DOUBLE_OBSERVABLE_COUNTER` | +| `producer-metrics` | `flush-time-ns-total` | `client-id` | `kafka.producer.flush_time_ns_total` | Total time producer has spent in flush in nanoseconds. | `DOUBLE_OBSERVABLE_COUNTER` | +| `producer-metrics` | `io-ratio` | `client-id` | `kafka.producer.io_ratio` | *Deprecated* The fraction of time the I/O thread spent doing I/O. | `DOUBLE_OBSERVABLE_GAUGE` | +| `producer-metrics` | `io-time-ns-avg` | `client-id` | `kafka.producer.io_time_ns_avg` | The average length of time for I/O per select call in nanoseconds. | `DOUBLE_OBSERVABLE_GAUGE` | +| `producer-metrics` | `io-time-ns-total` | `client-id` | `kafka.producer.io_time_ns_total` | The total time the I/O thread spent doing I/O. | `DOUBLE_OBSERVABLE_COUNTER` | +| `producer-metrics` | `io-wait-ratio` | `client-id` | `kafka.producer.io_wait_ratio` | *Deprecated* The fraction of time the I/O thread spent waiting. | `DOUBLE_OBSERVABLE_GAUGE` | +| `producer-metrics` | `io-wait-time-ns-avg` | `client-id` | `kafka.producer.io_wait_time_ns_avg` | The average length of time the I/O thread spent waiting for a socket ready for reads or writes in nanoseconds. | `DOUBLE_OBSERVABLE_GAUGE` | +| `producer-metrics` | `io-wait-time-ns-total` | `client-id` | `kafka.producer.io_wait_time_ns_total` | The total time the I/O thread spent waiting. | `DOUBLE_OBSERVABLE_COUNTER` | +| `producer-metrics` | `io-waittime-total` | `client-id` | `kafka.producer.io_waittime_total` | *Deprecated* The total time the I/O thread spent waiting. | `DOUBLE_OBSERVABLE_COUNTER` | +| `producer-metrics` | `iotime-total` | `client-id` | `kafka.producer.iotime_total` | *Deprecated* The total time the I/O thread spent doing I/O. | `DOUBLE_OBSERVABLE_COUNTER` | +| `producer-metrics` | `metadata-age` | `client-id` | `kafka.producer.metadata_age` | The age in seconds of the current producer metadata being used. | `DOUBLE_OBSERVABLE_GAUGE` | +| `producer-metrics` | `metadata-wait-time-ns-total` | `client-id` | `kafka.producer.metadata_wait_time_ns_total` | Total time producer has spent waiting on topic metadata in nanoseconds. | `DOUBLE_OBSERVABLE_COUNTER` | +| `producer-metrics` | `network-io-rate` | `client-id` | `kafka.producer.network_io_rate` | The number of network operations (reads or writes) on all connections per second. | `DOUBLE_OBSERVABLE_GAUGE` | +| `producer-metrics` | `network-io-total` | `client-id` | `kafka.producer.network_io_total` | The total number of network operations (reads or writes) on all connections. | `DOUBLE_OBSERVABLE_COUNTER` | +| `producer-metrics` | `produce-throttle-time-avg` | `client-id` | `kafka.producer.produce_throttle_time_avg` | The average time in ms a request was throttled by a broker. | `DOUBLE_OBSERVABLE_GAUGE` | +| `producer-metrics` | `produce-throttle-time-max` | `client-id` | `kafka.producer.produce_throttle_time_max` | The maximum time in ms a request was throttled by a broker. | `DOUBLE_OBSERVABLE_GAUGE` | +| `producer-metrics` | `reauthentication-latency-avg` | `client-id` | `kafka.producer.reauthentication_latency_avg` | The average latency observed due to re-authentication. | `DOUBLE_OBSERVABLE_GAUGE` | +| `producer-metrics` | `reauthentication-latency-max` | `client-id` | `kafka.producer.reauthentication_latency_max` | The max latency observed due to re-authentication. | `DOUBLE_OBSERVABLE_GAUGE` | +| `producer-metrics` | `record-queue-time-avg` | `client-id` | `kafka.producer.record_queue_time_avg` | The average time in ms record batches spent in the send buffer. | `DOUBLE_OBSERVABLE_GAUGE` | +| `producer-metrics` | `record-queue-time-max` | `client-id` | `kafka.producer.record_queue_time_max` | The maximum time in ms record batches spent in the send buffer. | `DOUBLE_OBSERVABLE_GAUGE` | +| `producer-metrics` | `record-size-avg` | `client-id` | `kafka.producer.record_size_avg` | The average record size. | `DOUBLE_OBSERVABLE_GAUGE` | +| `producer-metrics` | `record-size-max` | `client-id` | `kafka.producer.record_size_max` | The maximum record size. | `DOUBLE_OBSERVABLE_GAUGE` | +| `producer-metrics` | `records-per-request-avg` | `client-id` | `kafka.producer.records_per_request_avg` | The average number of records per request. | `DOUBLE_OBSERVABLE_GAUGE` | +| `producer-metrics` | `requests-in-flight` | `client-id` | `kafka.producer.requests_in_flight` | The current number of in-flight requests awaiting a response. | `DOUBLE_OBSERVABLE_GAUGE` | +| `producer-metrics` | `select-rate` | `client-id` | `kafka.producer.select_rate` | The number of times the I/O layer checked for new I/O to perform per second. | `DOUBLE_OBSERVABLE_GAUGE` | +| `producer-metrics` | `select-total` | `client-id` | `kafka.producer.select_total` | The total number of times the I/O layer checked for new I/O to perform. | `DOUBLE_OBSERVABLE_COUNTER` | +| `producer-metrics` | `successful-authentication-no-reauth-total` | `client-id` | `kafka.producer.successful_authentication_no_reauth_total` | The total number of connections with successful authentication where the client does not support re-authentication. | `DOUBLE_OBSERVABLE_COUNTER` | +| `producer-metrics` | `successful-authentication-rate` | `client-id` | `kafka.producer.successful_authentication_rate` | The number of connections with successful authentication per second. | `DOUBLE_OBSERVABLE_GAUGE` | +| `producer-metrics` | `successful-authentication-total` | `client-id` | `kafka.producer.successful_authentication_total` | The total number of connections with successful authentication. | `DOUBLE_OBSERVABLE_COUNTER` | +| `producer-metrics` | `successful-reauthentication-rate` | `client-id` | `kafka.producer.successful_reauthentication_rate` | The number of successful re-authentication of connections per second. | `DOUBLE_OBSERVABLE_GAUGE` | +| `producer-metrics` | `successful-reauthentication-total` | `client-id` | `kafka.producer.successful_reauthentication_total` | The total number of successful re-authentication of connections. | `DOUBLE_OBSERVABLE_COUNTER` | +| `producer-metrics` | `txn-abort-time-ns-total` | `client-id` | `kafka.producer.txn_abort_time_ns_total` | Total time producer has spent in abortTransaction in nanoseconds. | `DOUBLE_OBSERVABLE_COUNTER` | +| `producer-metrics` | `txn-begin-time-ns-total` | `client-id` | `kafka.producer.txn_begin_time_ns_total` | Total time producer has spent in beginTransaction in nanoseconds. | `DOUBLE_OBSERVABLE_COUNTER` | +| `producer-metrics` | `txn-commit-time-ns-total` | `client-id` | `kafka.producer.txn_commit_time_ns_total` | Total time producer has spent in commitTransaction in nanoseconds. | `DOUBLE_OBSERVABLE_COUNTER` | +| `producer-metrics` | `txn-init-time-ns-total` | `client-id` | `kafka.producer.txn_init_time_ns_total` | Total time producer has spent in initTransactions in nanoseconds. | `DOUBLE_OBSERVABLE_COUNTER` | +| `producer-metrics` | `txn-send-offsets-time-ns-total` | `client-id` | `kafka.producer.txn_send_offsets_time_ns_total` | Total time producer has spent in sendOffsetsToTransaction in nanoseconds. | `DOUBLE_OBSERVABLE_COUNTER` | +| `producer-metrics` | `waiting-threads` | `client-id` | `kafka.producer.waiting_threads` | The number of user threads blocked waiting for buffer memory to enqueue their records. | `DOUBLE_OBSERVABLE_GAUGE` | +| `producer-node-metrics` | `incoming-byte-rate` | `client-id`,`node-id` | `kafka.producer.incoming_byte_rate` | The number of bytes read off all sockets per second. | `DOUBLE_OBSERVABLE_GAUGE` | +| `producer-node-metrics` | `incoming-byte-total` | `client-id`,`node-id` | `kafka.producer.incoming_byte_total` | The total number of bytes read off all sockets. | `DOUBLE_OBSERVABLE_COUNTER` | +| `producer-node-metrics` | `outgoing-byte-rate` | `client-id`,`node-id` | `kafka.producer.outgoing_byte_rate` | The number of outgoing bytes sent to all servers per second. | `DOUBLE_OBSERVABLE_GAUGE` | +| `producer-node-metrics` | `outgoing-byte-total` | `client-id`,`node-id` | `kafka.producer.outgoing_byte_total` | The total number of outgoing bytes sent to all servers. | `DOUBLE_OBSERVABLE_COUNTER` | +| `producer-node-metrics` | `request-latency-avg` | `client-id`,`node-id` | `kafka.producer.request_latency_avg` | The average request latency in ms. | `DOUBLE_OBSERVABLE_GAUGE` | +| `producer-node-metrics` | `request-latency-max` | `client-id`,`node-id` | `kafka.producer.request_latency_max` | The maximum request latency in ms. | `DOUBLE_OBSERVABLE_GAUGE` | +| `producer-node-metrics` | `request-rate` | `client-id`,`node-id` | `kafka.producer.request_rate` | The number of requests sent per second. | `DOUBLE_OBSERVABLE_GAUGE` | +| `producer-node-metrics` | `request-size-avg` | `client-id`,`node-id` | `kafka.producer.request_size_avg` | The average size of requests sent. | `DOUBLE_OBSERVABLE_GAUGE` | +| `producer-node-metrics` | `request-size-max` | `client-id`,`node-id` | `kafka.producer.request_size_max` | The maximum size of any request sent. | `DOUBLE_OBSERVABLE_GAUGE` | +| `producer-node-metrics` | `request-total` | `client-id`,`node-id` | `kafka.producer.request_total` | The total number of requests sent. | `DOUBLE_OBSERVABLE_COUNTER` | +| `producer-node-metrics` | `response-rate` | `client-id`,`node-id` | `kafka.producer.response_rate` | The number of responses received per second. | `DOUBLE_OBSERVABLE_GAUGE` | +| `producer-node-metrics` | `response-total` | `client-id`,`node-id` | `kafka.producer.response_total` | The total number of responses received. | `DOUBLE_OBSERVABLE_COUNTER` | +| `producer-topic-metrics` | `byte-rate` | `client-id`,`topic` | `kafka.producer.byte_rate` | The average number of bytes sent per second for a topic. | `DOUBLE_OBSERVABLE_GAUGE` | +| `producer-topic-metrics` | `byte-total` | `client-id`,`topic` | `kafka.producer.byte_total` | The total number of bytes sent for a topic. | `DOUBLE_OBSERVABLE_COUNTER` | +| `producer-topic-metrics` | `compression-rate` | `client-id`,`topic` | `kafka.producer.compression_rate` | The average compression rate of record batches for a topic, defined as the average ratio of the compressed batch size over the uncompressed size. | `DOUBLE_OBSERVABLE_GAUGE` | +| `producer-topic-metrics` | `record-error-rate` | `client-id`,`topic` | `kafka.producer.record_error_rate` | The average per-second number of record sends that resulted in errors. | `DOUBLE_OBSERVABLE_GAUGE` | +| `producer-topic-metrics` | `record-error-total` | `client-id`,`topic` | `kafka.producer.record_error_total` | The total number of record sends that resulted in errors. | `DOUBLE_OBSERVABLE_COUNTER` | +| `producer-topic-metrics` | `record-retry-rate` | `client-id`,`topic` | `kafka.producer.record_retry_rate` | The average per-second number of retried record sends. | `DOUBLE_OBSERVABLE_GAUGE` | +| `producer-topic-metrics` | `record-retry-total` | `client-id`,`topic` | `kafka.producer.record_retry_total` | The total number of retried record sends. | `DOUBLE_OBSERVABLE_COUNTER` | +| `producer-topic-metrics` | `record-send-rate` | `client-id`,`topic` | `kafka.producer.record_send_rate` | The average number of records sent per second. | `DOUBLE_OBSERVABLE_GAUGE` | +| `producer-topic-metrics` | `record-send-total` | `client-id`,`topic` | `kafka.producer.record_send_total` | The total number of records sent. | `DOUBLE_OBSERVABLE_COUNTER` | diff --git a/instrumentation/kafka/kafka-clients/kafka-clients-2.6/library/build.gradle.kts b/instrumentation/kafka/kafka-clients/kafka-clients-2.6/library/build.gradle.kts index 2b5e3a333aed..2d20bdf26dd0 100644 --- a/instrumentation/kafka/kafka-clients/kafka-clients-2.6/library/build.gradle.kts +++ b/instrumentation/kafka/kafka-clients/kafka-clients-2.6/library/build.gradle.kts @@ -19,5 +19,26 @@ dependencies { tasks { withType().configureEach { usesService(gradle.sharedServices.registrations["testcontainersBuildService"].service) + systemProperty("testLatestDeps", findProperty("testLatestDeps") as Boolean) + } + + val testReceiveSpansDisabled by registering(Test::class) { + filter { + includeTestsMatching("InterceptorsSuppressReceiveSpansTest") + includeTestsMatching("WrapperSuppressReceiveSpansTest") + } + include("**/InterceptorsSuppressReceiveSpansTest.*", "**/WrapperSuppressReceiveSpansTest.*") + } + + test { + filter { + excludeTestsMatching("InterceptorsSuppressReceiveSpansTest") + excludeTestsMatching("WrapperSuppressReceiveSpansTest") + } + jvmArgs("-Dotel.instrumentation.messaging.experimental.receive-telemetry.enabled=true") + } + + check { + dependsOn(testReceiveSpansDisabled) } } diff --git a/instrumentation/kafka/kafka-clients/kafka-clients-2.6/library/src/main/java/io/opentelemetry/instrumentation/kafkaclients/v2_6/KafkaTelemetry.java b/instrumentation/kafka/kafka-clients/kafka-clients-2.6/library/src/main/java/io/opentelemetry/instrumentation/kafkaclients/v2_6/KafkaTelemetry.java index c299baba2c49..7a7de1813739 100644 --- a/instrumentation/kafka/kafka-clients/kafka-clients-2.6/library/src/main/java/io/opentelemetry/instrumentation/kafkaclients/v2_6/KafkaTelemetry.java +++ b/instrumentation/kafka/kafka-clients/kafka-clients-2.6/library/src/main/java/io/opentelemetry/instrumentation/kafkaclients/v2_6/KafkaTelemetry.java @@ -13,16 +13,25 @@ import io.opentelemetry.context.propagation.TextMapPropagator; import io.opentelemetry.context.propagation.TextMapSetter; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import io.opentelemetry.instrumentation.api.internal.InstrumenterUtil; +import io.opentelemetry.instrumentation.api.internal.Timer; +import io.opentelemetry.instrumentation.kafka.internal.KafkaConsumerContext; +import io.opentelemetry.instrumentation.kafka.internal.KafkaConsumerContextUtil; import io.opentelemetry.instrumentation.kafka.internal.KafkaHeadersSetter; import io.opentelemetry.instrumentation.kafka.internal.KafkaProcessRequest; import io.opentelemetry.instrumentation.kafka.internal.KafkaProducerRequest; +import io.opentelemetry.instrumentation.kafka.internal.KafkaReceiveRequest; import io.opentelemetry.instrumentation.kafka.internal.KafkaUtil; +import io.opentelemetry.instrumentation.kafka.internal.MetricsReporterList; import io.opentelemetry.instrumentation.kafka.internal.OpenTelemetryMetricsReporter; import io.opentelemetry.instrumentation.kafka.internal.OpenTelemetrySupplier; +import io.opentelemetry.instrumentation.kafka.internal.TracingList; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Proxy; import java.util.Collections; import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; import java.util.concurrent.Future; import java.util.function.BiFunction; @@ -37,6 +46,7 @@ import org.apache.kafka.clients.producer.Producer; import org.apache.kafka.clients.producer.ProducerRecord; import org.apache.kafka.clients.producer.RecordMetadata; +import org.apache.kafka.common.TopicPartition; import org.apache.kafka.common.header.Headers; import org.apache.kafka.common.metrics.MetricsReporter; @@ -47,16 +57,19 @@ public final class KafkaTelemetry { private final OpenTelemetry openTelemetry; private final Instrumenter producerInstrumenter; + private final Instrumenter consumerReceiveInstrumenter; private final Instrumenter consumerProcessInstrumenter; private final boolean producerPropagationEnabled; KafkaTelemetry( OpenTelemetry openTelemetry, Instrumenter producerInstrumenter, + Instrumenter consumerReceiveInstrumenter, Instrumenter consumerProcessInstrumenter, boolean producerPropagationEnabled) { this.openTelemetry = openTelemetry; this.producerInstrumenter = producerInstrumenter; + this.consumerReceiveInstrumenter = consumerReceiveInstrumenter; this.consumerProcessInstrumenter = consumerProcessInstrumenter; this.producerPropagationEnabled = producerPropagationEnabled; } @@ -115,6 +128,7 @@ public Consumer wrap(Consumer consumer) { new Class[] {Consumer.class}, (proxy, method, args) -> { Object result; + Timer timer = "poll".equals(method.getName()) ? Timer.start() : null; try { result = method.invoke(consumer, args); } catch (InvocationTargetException exception) { @@ -123,12 +137,36 @@ public Consumer wrap(Consumer consumer) { // ConsumerRecords poll(long timeout) // ConsumerRecords poll(Duration duration) if ("poll".equals(method.getName()) && result instanceof ConsumerRecords) { - buildAndFinishSpan((ConsumerRecords) result, consumer); + ConsumerRecords consumerRecords = (ConsumerRecords) result; + Context receiveContext = buildAndFinishSpan(consumerRecords, consumer, timer); + if (receiveContext == null) { + receiveContext = Context.current(); + } + KafkaConsumerContext consumerContext = + KafkaConsumerContextUtil.create(receiveContext, consumer); + result = addTracing(consumerRecords, consumerContext); } return result; }); } + ConsumerRecords addTracing( + ConsumerRecords consumerRecords, KafkaConsumerContext consumerContext) { + if (consumerRecords.isEmpty()) { + return consumerRecords; + } + + Map>> records = new LinkedHashMap<>(); + for (TopicPartition partition : consumerRecords.partitions()) { + List> list = consumerRecords.records(partition); + if (list != null && !list.isEmpty()) { + list = TracingList.wrap(list, consumerProcessInstrumenter, () -> true, consumerContext); + } + records.put(partition, list); + } + return new ConsumerRecords<>(records); + } + /** * Produces a set of kafka client config properties (consumer or producer) to register a {@link * MetricsReporter} that records metrics to an {@code openTelemetry} instance. Add these resulting @@ -159,7 +197,7 @@ public Consumer wrap(Consumer consumer) { Map config = new HashMap<>(); config.put( CommonClientConfigs.METRIC_REPORTER_CLASSES_CONFIG, - OpenTelemetryMetricsReporter.class.getName()); + MetricsReporterList.singletonList(OpenTelemetryMetricsReporter.class)); config.put( OpenTelemetryMetricsReporter.CONFIG_KEY_OPENTELEMETRY_SUPPLIER, new OpenTelemetrySupplier(openTelemetry)); @@ -221,23 +259,37 @@ Future buildAndInjectSpan( } } - private void buildAndFinishSpan(ConsumerRecords records, Consumer consumer) { - buildAndFinishSpan( - records, KafkaUtil.getConsumerGroup(consumer), KafkaUtil.getClientId(consumer)); + private Context buildAndFinishSpan( + ConsumerRecords records, Consumer consumer, Timer timer) { + return buildAndFinishSpan( + records, KafkaUtil.getConsumerGroup(consumer), KafkaUtil.getClientId(consumer), timer); } - void buildAndFinishSpan( - ConsumerRecords records, String consumerGroup, String clientId) { + Context buildAndFinishSpan( + ConsumerRecords records, String consumerGroup, String clientId, Timer timer) { + if (records.isEmpty()) { + return null; + } Context parentContext = Context.current(); - for (ConsumerRecord record : records) { - KafkaProcessRequest request = KafkaProcessRequest.create(record, consumerGroup, clientId); - if (!consumerProcessInstrumenter.shouldStart(parentContext, request)) { - continue; - } - - Context context = consumerProcessInstrumenter.start(parentContext, request); - consumerProcessInstrumenter.end(context, request, null, null); + KafkaReceiveRequest request = KafkaReceiveRequest.create(records, consumerGroup, clientId); + Context context = null; + if (consumerReceiveInstrumenter.shouldStart(parentContext, request)) { + context = + InstrumenterUtil.startAndEnd( + consumerReceiveInstrumenter, + parentContext, + request, + null, + null, + timer.startTime(), + timer.now()); } + + // we're returning the context of the receive span so that process spans can use it as + // parent context even though the span has ended + // this is the suggested behavior according to the spec batch receive scenario: + // https://github.com/open-telemetry/semantic-conventions/blob/main/docs/messaging/messaging-spans.md#batch-receiving + return context; } private class ProducerCallback implements Callback { diff --git a/instrumentation/kafka/kafka-clients/kafka-clients-2.6/library/src/main/java/io/opentelemetry/instrumentation/kafkaclients/v2_6/KafkaTelemetryBuilder.java b/instrumentation/kafka/kafka-clients/kafka-clients-2.6/library/src/main/java/io/opentelemetry/instrumentation/kafkaclients/v2_6/KafkaTelemetryBuilder.java index 87937be28d7e..9fd1585323c5 100644 --- a/instrumentation/kafka/kafka-clients/kafka-clients-2.6/library/src/main/java/io/opentelemetry/instrumentation/kafkaclients/v2_6/KafkaTelemetryBuilder.java +++ b/instrumentation/kafka/kafka-clients/kafka-clients-2.6/library/src/main/java/io/opentelemetry/instrumentation/kafkaclients/v2_6/KafkaTelemetryBuilder.java @@ -10,10 +10,10 @@ import com.google.errorprone.annotations.CanIgnoreReturnValue; import io.opentelemetry.api.OpenTelemetry; import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.messaging.MessageOperation; import io.opentelemetry.instrumentation.kafka.internal.KafkaInstrumenterFactory; import io.opentelemetry.instrumentation.kafka.internal.KafkaProcessRequest; import io.opentelemetry.instrumentation.kafka.internal.KafkaProducerRequest; +import io.opentelemetry.instrumentation.kafka.internal.KafkaReceiveRequest; import java.util.ArrayList; import java.util.List; import java.util.Objects; @@ -25,11 +25,14 @@ public final class KafkaTelemetryBuilder { private final OpenTelemetry openTelemetry; private final List> producerAttributesExtractors = new ArrayList<>(); - private final List> consumerAttributesExtractors = - new ArrayList<>(); + private final List> + consumerProcessAttributesExtractors = new ArrayList<>(); + private final List> + consumerReceiveAttributesExtractors = new ArrayList<>(); private List capturedHeaders = emptyList(); private boolean captureExperimentalSpanAttributes = false; private boolean propagationEnabled = true; + private boolean messagingReceiveInstrumentationEnabled = false; KafkaTelemetryBuilder(OpenTelemetry openTelemetry) { this.openTelemetry = Objects.requireNonNull(openTelemetry); @@ -42,10 +45,25 @@ public KafkaTelemetryBuilder addProducerAttributesExtractors( return this; } + /** Use {@link #addConsumerProcessAttributesExtractors(AttributesExtractor)} instead. */ + @Deprecated @CanIgnoreReturnValue public KafkaTelemetryBuilder addConsumerAttributesExtractors( AttributesExtractor extractor) { - consumerAttributesExtractors.add(extractor); + return addConsumerProcessAttributesExtractors(extractor); + } + + @CanIgnoreReturnValue + public KafkaTelemetryBuilder addConsumerProcessAttributesExtractors( + AttributesExtractor extractor) { + consumerProcessAttributesExtractors.add(extractor); + return this; + } + + @CanIgnoreReturnValue + public KafkaTelemetryBuilder addConsumerReceiveAttributesExtractors( + AttributesExtractor extractor) { + consumerReceiveAttributesExtractors.add(extractor); return this; } @@ -85,17 +103,31 @@ public KafkaTelemetryBuilder setPropagationEnabled(boolean propagationEnabled) { return this; } + /** + * Set whether to capture the consumer message receive telemetry in messaging instrumentation. + * + *

Note that this will cause the consumer side to start a new trace, with only a span link + * connecting it to the producer trace. + */ + @CanIgnoreReturnValue + public KafkaTelemetryBuilder setMessagingReceiveInstrumentationEnabled( + boolean messagingReceiveInstrumentationEnabled) { + this.messagingReceiveInstrumentationEnabled = messagingReceiveInstrumentationEnabled; + return this; + } + public KafkaTelemetry build() { KafkaInstrumenterFactory instrumenterFactory = new KafkaInstrumenterFactory(openTelemetry, INSTRUMENTATION_NAME) .setCapturedHeaders(capturedHeaders) - .setCaptureExperimentalSpanAttributes(captureExperimentalSpanAttributes); + .setCaptureExperimentalSpanAttributes(captureExperimentalSpanAttributes) + .setMessagingReceiveInstrumentationEnabled(messagingReceiveInstrumentationEnabled); return new KafkaTelemetry( openTelemetry, instrumenterFactory.createProducerInstrumenter(producerAttributesExtractors), - instrumenterFactory.createConsumerOperationInstrumenter( - MessageOperation.RECEIVE, consumerAttributesExtractors), + instrumenterFactory.createConsumerReceiveInstrumenter(consumerReceiveAttributesExtractors), + instrumenterFactory.createConsumerProcessInstrumenter(consumerProcessAttributesExtractors), propagationEnabled); } } diff --git a/instrumentation/kafka/kafka-clients/kafka-clients-2.6/library/src/main/java/io/opentelemetry/instrumentation/kafkaclients/v2_6/TracingConsumerInterceptor.java b/instrumentation/kafka/kafka-clients/kafka-clients-2.6/library/src/main/java/io/opentelemetry/instrumentation/kafkaclients/v2_6/TracingConsumerInterceptor.java index e0995eb8170d..8c3018253b1a 100644 --- a/instrumentation/kafka/kafka-clients/kafka-clients-2.6/library/src/main/java/io/opentelemetry/instrumentation/kafkaclients/v2_6/TracingConsumerInterceptor.java +++ b/instrumentation/kafka/kafka-clients/kafka-clients-2.6/library/src/main/java/io/opentelemetry/instrumentation/kafkaclients/v2_6/TracingConsumerInterceptor.java @@ -7,6 +7,11 @@ import com.google.errorprone.annotations.CanIgnoreReturnValue; import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.internal.ConfigPropertiesUtil; +import io.opentelemetry.instrumentation.api.internal.Timer; +import io.opentelemetry.instrumentation.kafka.internal.KafkaConsumerContext; +import io.opentelemetry.instrumentation.kafka.internal.KafkaConsumerContextUtil; import java.util.Map; import java.util.Objects; import org.apache.kafka.clients.consumer.ConsumerConfig; @@ -22,7 +27,12 @@ */ public class TracingConsumerInterceptor implements ConsumerInterceptor { - private static final KafkaTelemetry telemetry = KafkaTelemetry.create(GlobalOpenTelemetry.get()); + private static final KafkaTelemetry telemetry = + KafkaTelemetry.builder(GlobalOpenTelemetry.get()) + .setMessagingReceiveInstrumentationEnabled( + ConfigPropertiesUtil.getBoolean( + "otel.instrumentation.messaging.experimental.receive-telemetry.enabled", false)) + .build(); private String consumerGroup; private String clientId; @@ -30,8 +40,15 @@ public class TracingConsumerInterceptor implements ConsumerInterceptor onConsume(ConsumerRecords records) { - telemetry.buildAndFinishSpan(records, consumerGroup, clientId); - return records; + // timer should be started before fetching ConsumerRecords, but there is no callback for that + Timer timer = Timer.start(); + Context receiveContext = telemetry.buildAndFinishSpan(records, consumerGroup, clientId, timer); + if (receiveContext == null) { + receiveContext = Context.current(); + } + KafkaConsumerContext consumerContext = + KafkaConsumerContextUtil.create(receiveContext, consumerGroup, clientId); + return telemetry.addTracing(records, consumerContext); } @Override diff --git a/instrumentation/kafka/kafka-clients/kafka-clients-2.6/library/src/test/java/io/opentelemetry/instrumentation/kafkaclients/v2_6/AbstractInterceptorsTest.java b/instrumentation/kafka/kafka-clients/kafka-clients-2.6/library/src/test/java/io/opentelemetry/instrumentation/kafkaclients/v2_6/AbstractInterceptorsTest.java new file mode 100644 index 000000000000..e1de24630529 --- /dev/null +++ b/instrumentation/kafka/kafka-clients/kafka-clients-2.6/library/src/test/java/io/opentelemetry/instrumentation/kafkaclients/v2_6/AbstractInterceptorsTest.java @@ -0,0 +1,76 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.kafkaclients.v2_6; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.instrumentation.kafka.internal.KafkaClientBaseTest; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.LibraryInstrumentationExtension; +import java.time.Duration; +import java.util.Map; +import org.apache.kafka.clients.consumer.ConsumerConfig; +import org.apache.kafka.clients.consumer.ConsumerRecord; +import org.apache.kafka.clients.consumer.ConsumerRecords; +import org.apache.kafka.clients.producer.ProducerConfig; +import org.apache.kafka.clients.producer.ProducerRecord; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +abstract class AbstractInterceptorsTest extends KafkaClientBaseTest { + + @RegisterExtension + static final InstrumentationExtension testing = LibraryInstrumentationExtension.create(); + + static final String greeting = "Hello Kafka!"; + + @Override + public Map producerProps() { + Map props = super.producerProps(); + props.put( + ProducerConfig.INTERCEPTOR_CLASSES_CONFIG, TracingProducerInterceptor.class.getName()); + return props; + } + + @Override + public Map consumerProps() { + Map props = super.consumerProps(); + props.put( + ConsumerConfig.INTERCEPTOR_CLASSES_CONFIG, TracingConsumerInterceptor.class.getName()); + return props; + } + + @Test + void testInterceptors() throws InterruptedException { + testing.runWithSpan( + "parent", + () -> { + producer.send( + new ProducerRecord<>(SHARED_TOPIC, greeting), + (meta, ex) -> { + if (ex == null) { + testing.runWithSpan("producer callback", () -> {}); + } else { + testing.runWithSpan("producer exception: " + ex, () -> {}); + } + }); + }); + + awaitUntilConsumerIsReady(); + // check that the message was received + ConsumerRecords records = consumer.poll(Duration.ofSeconds(5)); + assertThat(records.count()).isEqualTo(1); + for (ConsumerRecord record : records) { + assertThat(record.value()).isEqualTo(greeting); + assertThat(record.key()).isNull(); + testing.runWithSpan("process child", () -> {}); + } + + assertTraces(); + } + + abstract void assertTraces(); +} diff --git a/instrumentation/kafka/kafka-clients/kafka-clients-2.6/library/src/test/java/io/opentelemetry/instrumentation/kafkaclients/v2_6/AbstractWrapperTest.java b/instrumentation/kafka/kafka-clients/kafka-clients-2.6/library/src/test/java/io/opentelemetry/instrumentation/kafkaclients/v2_6/AbstractWrapperTest.java new file mode 100644 index 000000000000..8452e3850499 --- /dev/null +++ b/instrumentation/kafka/kafka-clients/kafka-clients-2.6/library/src/test/java/io/opentelemetry/instrumentation/kafkaclients/v2_6/AbstractWrapperTest.java @@ -0,0 +1,82 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.kafkaclients.v2_6; + +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.instrumentation.kafka.internal.KafkaClientBaseTest; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.LibraryInstrumentationExtension; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import org.apache.kafka.clients.consumer.Consumer; +import org.apache.kafka.clients.consumer.ConsumerRecord; +import org.apache.kafka.clients.consumer.ConsumerRecords; +import org.apache.kafka.clients.producer.Producer; +import org.apache.kafka.clients.producer.ProducerRecord; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +abstract class AbstractWrapperTest extends KafkaClientBaseTest { + + @RegisterExtension + static final InstrumentationExtension testing = LibraryInstrumentationExtension.create(); + + static final String greeting = "Hello Kafka!"; + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void testWrappers(boolean testHeaders) throws InterruptedException { + KafkaTelemetryBuilder telemetryBuilder = + KafkaTelemetry.builder(testing.getOpenTelemetry()) + .setCapturedHeaders(singletonList("test-message-header")) + // TODO run tests both with and without experimental span attributes + .setCaptureExperimentalSpanAttributes(true); + configure(telemetryBuilder); + KafkaTelemetry telemetry = telemetryBuilder.build(); + + Producer wrappedProducer = telemetry.wrap(producer); + + testing.runWithSpan( + "parent", + () -> { + ProducerRecord producerRecord = + new ProducerRecord<>(SHARED_TOPIC, greeting); + if (testHeaders) { + producerRecord + .headers() + .add("test-message-header", "test".getBytes(StandardCharsets.UTF_8)); + } + wrappedProducer.send( + producerRecord, + (meta, ex) -> { + if (ex == null) { + testing.runWithSpan("producer callback", () -> {}); + } else { + testing.runWithSpan("producer exception: " + ex, () -> {}); + } + }); + }); + + awaitUntilConsumerIsReady(); + Consumer wrappedConsumer = telemetry.wrap(consumer); + ConsumerRecords records = wrappedConsumer.poll(Duration.ofSeconds(10)); + assertThat(records.count()).isEqualTo(1); + for (ConsumerRecord record : records) { + assertThat(record.value()).isEqualTo(greeting); + assertThat(record.key()).isNull(); + testing.runWithSpan("process child", () -> {}); + } + + assertTraces(testHeaders); + } + + abstract void configure(KafkaTelemetryBuilder builder); + + abstract void assertTraces(boolean testHeaders); +} diff --git a/instrumentation/kafka/kafka-clients/kafka-clients-2.6/library/src/test/java/io/opentelemetry/instrumentation/kafkaclients/v2_6/InterceptorsSuppressReceiveSpansTest.java b/instrumentation/kafka/kafka-clients/kafka-clients-2.6/library/src/test/java/io/opentelemetry/instrumentation/kafkaclients/v2_6/InterceptorsSuppressReceiveSpansTest.java new file mode 100644 index 000000000000..8d70c340f60a --- /dev/null +++ b/instrumentation/kafka/kafka-clients/kafka-clients-2.6/library/src/test/java/io/opentelemetry/instrumentation/kafkaclients/v2_6/InterceptorsSuppressReceiveSpansTest.java @@ -0,0 +1,74 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.kafkaclients.v2_6; + +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.satisfies; + +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes; +import java.nio.charset.StandardCharsets; +import org.assertj.core.api.AbstractLongAssert; +import org.assertj.core.api.AbstractStringAssert; + +class InterceptorsSuppressReceiveSpansTest extends AbstractInterceptorsTest { + + @Override + void assertTraces() { + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(), + span -> + span.hasName(SHARED_TOPIC + " publish") + .hasKind(SpanKind.PRODUCER) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo(MessagingIncubatingAttributes.MESSAGING_SYSTEM, "kafka"), + equalTo( + MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME, + SHARED_TOPIC), + equalTo(MessagingIncubatingAttributes.MESSAGING_OPERATION, "publish"), + satisfies( + MessagingIncubatingAttributes.MESSAGING_CLIENT_ID, + stringAssert -> stringAssert.startsWith("producer"))), + span -> + span.hasName(SHARED_TOPIC + " process") + .hasKind(SpanKind.CONSUMER) + .hasParent(trace.getSpan(1)) + .hasAttributesSatisfyingExactly( + equalTo(MessagingIncubatingAttributes.MESSAGING_SYSTEM, "kafka"), + equalTo( + MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME, + SHARED_TOPIC), + equalTo(MessagingIncubatingAttributes.MESSAGING_OPERATION, "process"), + equalTo( + MessagingIncubatingAttributes.MESSAGING_MESSAGE_BODY_SIZE, + greeting.getBytes(StandardCharsets.UTF_8).length), + satisfies( + MessagingIncubatingAttributes.MESSAGING_DESTINATION_PARTITION_ID, + AbstractStringAssert::isNotEmpty), + satisfies( + MessagingIncubatingAttributes.MESSAGING_KAFKA_MESSAGE_OFFSET, + AbstractLongAssert::isNotNegative), + equalTo( + MessagingIncubatingAttributes.MESSAGING_KAFKA_CONSUMER_GROUP, + "test"), + satisfies( + MessagingIncubatingAttributes.MESSAGING_CLIENT_ID, + stringAssert -> stringAssert.startsWith("consumer"))), + span -> + span.hasName("process child") + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(2))), + // ideally we'd want producer callback to be part of the main trace, we just aren't able to + // instrument that + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("producer callback").hasKind(SpanKind.INTERNAL).hasNoParent())); + } +} diff --git a/instrumentation/kafka/kafka-clients/kafka-clients-2.6/library/src/test/java/io/opentelemetry/instrumentation/kafkaclients/v2_6/InterceptorsTest.java b/instrumentation/kafka/kafka-clients/kafka-clients-2.6/library/src/test/java/io/opentelemetry/instrumentation/kafkaclients/v2_6/InterceptorsTest.java index 90f658c53113..35e723247b44 100644 --- a/instrumentation/kafka/kafka-clients/kafka-clients-2.6/library/src/test/java/io/opentelemetry/instrumentation/kafkaclients/v2_6/InterceptorsTest.java +++ b/instrumentation/kafka/kafka-clients/kafka-clients-2.6/library/src/test/java/io/opentelemetry/instrumentation/kafkaclients/v2_6/InterceptorsTest.java @@ -5,121 +5,107 @@ package io.opentelemetry.instrumentation.kafkaclients.v2_6; +import static io.opentelemetry.instrumentation.testing.util.TelemetryDataUtil.orderByRootSpanName; import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.satisfies; import static org.assertj.core.api.Assertions.assertThat; +import io.opentelemetry.api.trace.SpanContext; import io.opentelemetry.api.trace.SpanKind; -import io.opentelemetry.instrumentation.kafka.internal.KafkaClientBaseTest; -import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; -import io.opentelemetry.instrumentation.testing.junit.LibraryInstrumentationExtension; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.opentelemetry.sdk.trace.data.LinkData; +import io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes; import java.nio.charset.StandardCharsets; -import java.time.Duration; -import java.util.HashMap; -import org.apache.kafka.clients.consumer.ConsumerConfig; -import org.apache.kafka.clients.consumer.ConsumerRecord; -import org.apache.kafka.clients.consumer.ConsumerRecords; -import org.apache.kafka.clients.producer.ProducerConfig; -import org.apache.kafka.clients.producer.ProducerRecord; +import java.util.concurrent.atomic.AtomicReference; import org.assertj.core.api.AbstractLongAssert; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.RegisterExtension; +import org.assertj.core.api.AbstractStringAssert; -class InterceptorsTest extends KafkaClientBaseTest { - @RegisterExtension - static final InstrumentationExtension testing = LibraryInstrumentationExtension.create(); +class InterceptorsTest extends AbstractInterceptorsTest { @Override - public HashMap producerProps() { - HashMap props = super.producerProps(); - props.put( - ProducerConfig.INTERCEPTOR_CLASSES_CONFIG, TracingProducerInterceptor.class.getName()); - return props; - } - - @Override - public HashMap consumerProps() { - HashMap props = super.consumerProps(); - props.put( - ConsumerConfig.INTERCEPTOR_CLASSES_CONFIG, TracingConsumerInterceptor.class.getName()); - return props; - } - - @Test - void testInterceptors() throws InterruptedException { - String greeting = "Hello Kafka!"; - testing.runWithSpan( - "parent", - () -> { - producer.send( - new ProducerRecord<>(SHARED_TOPIC, greeting), - (meta, ex) -> { - if (ex == null) { - testing.runWithSpan("producer callback", () -> {}); - } else { - testing.runWithSpan("producer exception: " + ex, () -> {}); - } - }); - }); - - awaitUntilConsumerIsReady(); - // check that the message was received - ConsumerRecords records = consumer.poll(Duration.ofSeconds(5)); - assertThat(records.count()).isEqualTo(1); - for (ConsumerRecord record : records) { - assertThat(record.value()).isEqualTo(greeting); - assertThat(record.key()).isNull(); - } - - testing.waitAndAssertTraces( + void assertTraces() { + AtomicReference producerSpanContext = new AtomicReference<>(); + testing.waitAndAssertSortedTraces( + orderByRootSpanName("parent", SHARED_TOPIC + " receive", "producer callback"), trace -> { trace.hasSpansSatisfyingExactly( - span -> { - span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(); - }, - span -> { - span.hasName(SHARED_TOPIC + " send") - .hasKind(SpanKind.PRODUCER) - .hasParent(trace.getSpan(0)) - .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.MESSAGING_SYSTEM, "kafka"), - equalTo(SemanticAttributes.MESSAGING_DESTINATION_NAME, SHARED_TOPIC), - satisfies( - SemanticAttributes.MESSAGING_KAFKA_CLIENT_ID, - stringAssert -> stringAssert.startsWith("producer"))); - }, - span -> { - span.hasName(SHARED_TOPIC + " receive") - .hasKind(SpanKind.CONSUMER) - .hasParent(trace.getSpan(1)) - .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.MESSAGING_SYSTEM, "kafka"), - equalTo(SemanticAttributes.MESSAGING_DESTINATION_NAME, SHARED_TOPIC), - equalTo(SemanticAttributes.MESSAGING_OPERATION, "receive"), - equalTo( - SemanticAttributes.MESSAGING_MESSAGE_PAYLOAD_SIZE_BYTES, - greeting.getBytes(StandardCharsets.UTF_8).length), - satisfies( - SemanticAttributes.MESSAGING_KAFKA_SOURCE_PARTITION, - AbstractLongAssert::isNotNegative), - satisfies( - SemanticAttributes.MESSAGING_KAFKA_MESSAGE_OFFSET, - AbstractLongAssert::isNotNegative), - equalTo(SemanticAttributes.MESSAGING_KAFKA_CONSUMER_GROUP, "test"), - satisfies( - SemanticAttributes.MESSAGING_KAFKA_CLIENT_ID, - stringAssert -> stringAssert.startsWith("consumer")), - satisfies( - SemanticAttributes.MESSAGING_CONSUMER_ID, - stringAssert -> stringAssert.startsWith("test - consumer"))); - }); + span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(), + span -> + span.hasName(SHARED_TOPIC + " publish") + .hasKind(SpanKind.PRODUCER) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo(MessagingIncubatingAttributes.MESSAGING_SYSTEM, "kafka"), + equalTo( + MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME, + SHARED_TOPIC), + equalTo(MessagingIncubatingAttributes.MESSAGING_OPERATION, "publish"), + satisfies( + MessagingIncubatingAttributes.MESSAGING_CLIENT_ID, + stringAssert -> stringAssert.startsWith("producer")))); + SpanContext spanContext = trace.getSpan(1).getSpanContext(); + producerSpanContext.set( + SpanContext.createFromRemoteParent( + spanContext.getTraceId(), + spanContext.getSpanId(), + spanContext.getTraceFlags(), + spanContext.getTraceState())); }, - trace -> { - trace.hasSpansSatisfyingExactly( - span -> { - span.hasName("producer callback").hasKind(SpanKind.INTERNAL).hasNoParent(); - }); - }); + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName(SHARED_TOPIC + " receive") + .hasKind(SpanKind.CONSUMER) + .hasNoParent() + .hasLinksSatisfying(links -> assertThat(links).isEmpty()) + .hasAttributesSatisfyingExactly( + equalTo(MessagingIncubatingAttributes.MESSAGING_SYSTEM, "kafka"), + equalTo( + MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME, + SHARED_TOPIC), + equalTo(MessagingIncubatingAttributes.MESSAGING_OPERATION, "receive"), + equalTo( + MessagingIncubatingAttributes.MESSAGING_KAFKA_CONSUMER_GROUP, + "test"), + satisfies( + MessagingIncubatingAttributes.MESSAGING_CLIENT_ID, + stringAssert -> stringAssert.startsWith("consumer")), + equalTo( + MessagingIncubatingAttributes.MESSAGING_BATCH_MESSAGE_COUNT, 1)), + span -> + span.hasName(SHARED_TOPIC + " process") + .hasKind(SpanKind.CONSUMER) + .hasParent(trace.getSpan(0)) + .hasLinks(LinkData.create(producerSpanContext.get())) + .hasAttributesSatisfyingExactly( + equalTo(MessagingIncubatingAttributes.MESSAGING_SYSTEM, "kafka"), + equalTo( + MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME, + SHARED_TOPIC), + equalTo(MessagingIncubatingAttributes.MESSAGING_OPERATION, "process"), + equalTo( + MessagingIncubatingAttributes.MESSAGING_MESSAGE_BODY_SIZE, + greeting.getBytes(StandardCharsets.UTF_8).length), + satisfies( + MessagingIncubatingAttributes.MESSAGING_DESTINATION_PARTITION_ID, + AbstractStringAssert::isNotEmpty), + satisfies( + MessagingIncubatingAttributes.MESSAGING_KAFKA_MESSAGE_OFFSET, + AbstractLongAssert::isNotNegative), + equalTo( + MessagingIncubatingAttributes.MESSAGING_KAFKA_CONSUMER_GROUP, + "test"), + satisfies( + MessagingIncubatingAttributes.MESSAGING_CLIENT_ID, + stringAssert -> stringAssert.startsWith("consumer"))), + span -> + span.hasName("process child") + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(1))), + // ideally we'd want producer callback to be part of the main trace, we just aren't able to + // instrument that + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("producer callback").hasKind(SpanKind.INTERNAL).hasNoParent())); } } diff --git a/instrumentation/kafka/kafka-clients/kafka-clients-2.6/library/src/test/java/io/opentelemetry/instrumentation/kafkaclients/v2_6/WrapperSuppressReceiveSpansTest.java b/instrumentation/kafka/kafka-clients/kafka-clients-2.6/library/src/test/java/io/opentelemetry/instrumentation/kafkaclients/v2_6/WrapperSuppressReceiveSpansTest.java new file mode 100644 index 000000000000..04c1babf5b60 --- /dev/null +++ b/instrumentation/kafka/kafka-clients/kafka-clients-2.6/library/src/test/java/io/opentelemetry/instrumentation/kafkaclients/v2_6/WrapperSuppressReceiveSpansTest.java @@ -0,0 +1,112 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.kafkaclients.v2_6; + +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.satisfies; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.sdk.testing.assertj.AttributeAssertion; +import io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import org.assertj.core.api.AbstractLongAssert; +import org.assertj.core.api.AbstractStringAssert; + +class WrapperSuppressReceiveSpansTest extends AbstractWrapperTest { + + @Override + void configure(KafkaTelemetryBuilder builder) { + builder.setMessagingReceiveInstrumentationEnabled(false); + } + + @Override + void assertTraces(boolean testHeaders) { + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(), + span -> + span.hasName(SHARED_TOPIC + " publish") + .hasKind(SpanKind.PRODUCER) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly(sendAttributes(testHeaders)), + span -> + span.hasName(SHARED_TOPIC + " process") + .hasKind(SpanKind.CONSUMER) + .hasParent(trace.getSpan(1)) + .hasAttributesSatisfyingExactly(processAttributes(greeting, testHeaders)), + span -> + span.hasName("process child") + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(2)), + span -> + span.hasName("producer callback") + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(0)))); + } + + protected static List sendAttributes(boolean testHeaders) { + List assertions = + new ArrayList<>( + Arrays.asList( + equalTo(MessagingIncubatingAttributes.MESSAGING_SYSTEM, "kafka"), + equalTo(MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME, SHARED_TOPIC), + equalTo(MessagingIncubatingAttributes.MESSAGING_OPERATION, "publish"), + satisfies( + MessagingIncubatingAttributes.MESSAGING_CLIENT_ID, + stringAssert -> stringAssert.startsWith("producer")), + satisfies( + MessagingIncubatingAttributes.MESSAGING_DESTINATION_PARTITION_ID, + AbstractStringAssert::isNotEmpty), + satisfies( + MessagingIncubatingAttributes.MESSAGING_KAFKA_MESSAGE_OFFSET, + AbstractLongAssert::isNotNegative))); + if (testHeaders) { + assertions.add( + equalTo( + AttributeKey.stringArrayKey("messaging.header.test_message_header"), + Collections.singletonList("test"))); + } + return assertions; + } + + private static List processAttributes(String greeting, boolean testHeaders) { + List assertions = + new ArrayList<>( + Arrays.asList( + equalTo(MessagingIncubatingAttributes.MESSAGING_SYSTEM, "kafka"), + equalTo(MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME, SHARED_TOPIC), + equalTo(MessagingIncubatingAttributes.MESSAGING_OPERATION, "process"), + equalTo( + MessagingIncubatingAttributes.MESSAGING_MESSAGE_BODY_SIZE, + greeting.getBytes(StandardCharsets.UTF_8).length), + satisfies( + MessagingIncubatingAttributes.MESSAGING_DESTINATION_PARTITION_ID, + AbstractStringAssert::isNotEmpty), + satisfies( + MessagingIncubatingAttributes.MESSAGING_KAFKA_MESSAGE_OFFSET, + AbstractLongAssert::isNotNegative), + satisfies( + AttributeKey.longKey("kafka.record.queue_time_ms"), + AbstractLongAssert::isNotNegative), + equalTo(MessagingIncubatingAttributes.MESSAGING_KAFKA_CONSUMER_GROUP, "test"), + satisfies( + MessagingIncubatingAttributes.MESSAGING_CLIENT_ID, + stringAssert -> stringAssert.startsWith("consumer")))); + if (testHeaders) { + assertions.add( + equalTo( + AttributeKey.stringArrayKey("messaging.header.test_message_header"), + Collections.singletonList("test"))); + } + return assertions; + } +} diff --git a/instrumentation/kafka/kafka-clients/kafka-clients-2.6/library/src/test/java/io/opentelemetry/instrumentation/kafkaclients/v2_6/WrapperTest.java b/instrumentation/kafka/kafka-clients/kafka-clients-2.6/library/src/test/java/io/opentelemetry/instrumentation/kafkaclients/v2_6/WrapperTest.java index 0777f55afbd1..aaa6730f1dd6 100644 --- a/instrumentation/kafka/kafka-clients/kafka-clients-2.6/library/src/test/java/io/opentelemetry/instrumentation/kafkaclients/v2_6/WrapperTest.java +++ b/instrumentation/kafka/kafka-clients/kafka-clients-2.6/library/src/test/java/io/opentelemetry/instrumentation/kafkaclients/v2_6/WrapperTest.java @@ -7,120 +7,89 @@ import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.satisfies; -import static java.util.Collections.singletonList; import static org.assertj.core.api.Assertions.assertThat; import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.trace.SpanContext; import io.opentelemetry.api.trace.SpanKind; -import io.opentelemetry.instrumentation.kafka.internal.KafkaClientBaseTest; -import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; -import io.opentelemetry.instrumentation.testing.junit.LibraryInstrumentationExtension; import io.opentelemetry.sdk.testing.assertj.AttributeAssertion; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.opentelemetry.sdk.trace.data.LinkData; +import io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes; import java.nio.charset.StandardCharsets; -import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; -import org.apache.kafka.clients.consumer.Consumer; -import org.apache.kafka.clients.consumer.ConsumerRecord; -import org.apache.kafka.clients.consumer.ConsumerRecords; -import org.apache.kafka.clients.producer.Producer; -import org.apache.kafka.clients.producer.ProducerRecord; +import java.util.concurrent.atomic.AtomicReference; import org.assertj.core.api.AbstractLongAssert; -import org.junit.jupiter.api.extension.RegisterExtension; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; +import org.assertj.core.api.AbstractStringAssert; -class WrapperTest extends KafkaClientBaseTest { +class WrapperTest extends AbstractWrapperTest { - @RegisterExtension - static final InstrumentationExtension testing = LibraryInstrumentationExtension.create(); - - @ParameterizedTest - @ValueSource(booleans = {true, false}) - void testWrappers(boolean testHeaders) throws InterruptedException { - KafkaTelemetry telemetry = - KafkaTelemetry.builder(testing.getOpenTelemetry()) - .setCapturedHeaders(singletonList("test-message-header")) - // TODO run tests both with and without experimental span attributes - .setCaptureExperimentalSpanAttributes(true) - .build(); - - String greeting = "Hello Kafka!"; - Producer wrappedProducer = telemetry.wrap(producer); - - testing.runWithSpan( - "parent", - () -> { - ProducerRecord producerRecord = - new ProducerRecord<>(SHARED_TOPIC, greeting); - if (testHeaders) { - producerRecord - .headers() - .add("test-message-header", "test".getBytes(StandardCharsets.UTF_8)); - } - wrappedProducer.send( - producerRecord, - (meta, ex) -> { - if (ex == null) { - testing.runWithSpan("producer callback", () -> {}); - } else { - testing.runWithSpan("producer exception: " + ex, () -> {}); - } - }); - }); - - awaitUntilConsumerIsReady(); - Consumer wrappedConsumer = telemetry.wrap(consumer); - ConsumerRecords records = wrappedConsumer.poll(Duration.ofSeconds(10)); - assertThat(records.count()).isEqualTo(1); - for (ConsumerRecord record : records) { - assertThat(record.value()).isEqualTo(greeting); - assertThat(record.key()).isNull(); - } + @Override + void configure(KafkaTelemetryBuilder builder) { + builder.setMessagingReceiveInstrumentationEnabled(true); + } + @Override + void assertTraces(boolean testHeaders) { + AtomicReference producerSpanContext = new AtomicReference<>(); testing.waitAndAssertTraces( trace -> { trace.hasSpansSatisfyingExactly( - span -> { - span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(); - }, - span -> { - span.hasName(SHARED_TOPIC + " send") - .hasKind(SpanKind.PRODUCER) - .hasParent(trace.getSpan(0)) - .hasAttributesSatisfyingExactly(sendAttributes(testHeaders)); - }, - span -> { - span.hasName(SHARED_TOPIC + " receive") - .hasKind(SpanKind.CONSUMER) - .hasParent(trace.getSpan(1)) - .hasAttributesSatisfyingExactly(receiveAttributes(greeting, testHeaders)); - }, - span -> { - span.hasName("producer callback") - .hasKind(SpanKind.INTERNAL) - .hasParent(trace.getSpan(0)); - }); - }); + span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(), + span -> + span.hasName(SHARED_TOPIC + " publish") + .hasKind(SpanKind.PRODUCER) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly(sendAttributes(testHeaders)), + span -> + span.hasName("producer callback") + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(0))); + SpanContext spanContext = trace.getSpan(1).getSpanContext(); + producerSpanContext.set( + SpanContext.createFromRemoteParent( + spanContext.getTraceId(), + spanContext.getSpanId(), + spanContext.getTraceFlags(), + spanContext.getTraceState())); + }, + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName(SHARED_TOPIC + " receive") + .hasKind(SpanKind.CONSUMER) + .hasNoParent() + .hasLinksSatisfying(links -> assertThat(links).isEmpty()) + .hasAttributesSatisfyingExactly(receiveAttributes(testHeaders)), + span -> + span.hasName(SHARED_TOPIC + " process") + .hasKind(SpanKind.CONSUMER) + .hasParent(trace.getSpan(0)) + .hasLinks(LinkData.create(producerSpanContext.get())) + .hasAttributesSatisfyingExactly(processAttributes(greeting, testHeaders)), + span -> + span.hasName("process child") + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(1)))); } protected static List sendAttributes(boolean testHeaders) { List assertions = new ArrayList<>( Arrays.asList( - equalTo(SemanticAttributes.MESSAGING_SYSTEM, "kafka"), - equalTo(SemanticAttributes.MESSAGING_DESTINATION_NAME, SHARED_TOPIC), + equalTo(MessagingIncubatingAttributes.MESSAGING_SYSTEM, "kafka"), + equalTo(MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME, SHARED_TOPIC), + equalTo(MessagingIncubatingAttributes.MESSAGING_OPERATION, "publish"), satisfies( - SemanticAttributes.MESSAGING_KAFKA_CLIENT_ID, + MessagingIncubatingAttributes.MESSAGING_CLIENT_ID, stringAssert -> stringAssert.startsWith("producer")), satisfies( - SemanticAttributes.MESSAGING_KAFKA_DESTINATION_PARTITION, - AbstractLongAssert::isNotNegative), + MessagingIncubatingAttributes.MESSAGING_DESTINATION_PARTITION_ID, + AbstractStringAssert::isNotEmpty), satisfies( - SemanticAttributes.MESSAGING_KAFKA_MESSAGE_OFFSET, + MessagingIncubatingAttributes.MESSAGING_KAFKA_MESSAGE_OFFSET, AbstractLongAssert::isNotNegative))); if (testHeaders) { assertions.add( @@ -131,32 +100,50 @@ protected static List sendAttributes(boolean testHeaders) { return assertions; } - private static List receiveAttributes(String greeting, boolean testHeaders) { + private static List processAttributes(String greeting, boolean testHeaders) { List assertions = new ArrayList<>( Arrays.asList( - equalTo(SemanticAttributes.MESSAGING_SYSTEM, "kafka"), - equalTo(SemanticAttributes.MESSAGING_DESTINATION_NAME, SHARED_TOPIC), - equalTo(SemanticAttributes.MESSAGING_OPERATION, "receive"), + equalTo(MessagingIncubatingAttributes.MESSAGING_SYSTEM, "kafka"), + equalTo(MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME, SHARED_TOPIC), + equalTo(MessagingIncubatingAttributes.MESSAGING_OPERATION, "process"), equalTo( - SemanticAttributes.MESSAGING_MESSAGE_PAYLOAD_SIZE_BYTES, + MessagingIncubatingAttributes.MESSAGING_MESSAGE_BODY_SIZE, greeting.getBytes(StandardCharsets.UTF_8).length), satisfies( - SemanticAttributes.MESSAGING_KAFKA_SOURCE_PARTITION, - AbstractLongAssert::isNotNegative), + MessagingIncubatingAttributes.MESSAGING_DESTINATION_PARTITION_ID, + AbstractStringAssert::isNotEmpty), satisfies( - SemanticAttributes.MESSAGING_KAFKA_MESSAGE_OFFSET, + MessagingIncubatingAttributes.MESSAGING_KAFKA_MESSAGE_OFFSET, AbstractLongAssert::isNotNegative), satisfies( AttributeKey.longKey("kafka.record.queue_time_ms"), AbstractLongAssert::isNotNegative), - equalTo(SemanticAttributes.MESSAGING_KAFKA_CONSUMER_GROUP, "test"), + equalTo(MessagingIncubatingAttributes.MESSAGING_KAFKA_CONSUMER_GROUP, "test"), satisfies( - SemanticAttributes.MESSAGING_KAFKA_CLIENT_ID, - stringAssert -> stringAssert.startsWith("consumer")), + MessagingIncubatingAttributes.MESSAGING_CLIENT_ID, + stringAssert -> stringAssert.startsWith("consumer")))); + if (testHeaders) { + assertions.add( + equalTo( + AttributeKey.stringArrayKey("messaging.header.test_message_header"), + Collections.singletonList("test"))); + } + return assertions; + } + + protected static List receiveAttributes(boolean testHeaders) { + List assertions = + new ArrayList<>( + Arrays.asList( + equalTo(MessagingIncubatingAttributes.MESSAGING_SYSTEM, "kafka"), + equalTo(MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME, SHARED_TOPIC), + equalTo(MessagingIncubatingAttributes.MESSAGING_OPERATION, "receive"), + equalTo(MessagingIncubatingAttributes.MESSAGING_KAFKA_CONSUMER_GROUP, "test"), satisfies( - SemanticAttributes.MESSAGING_CONSUMER_ID, - stringAssert -> stringAssert.startsWith("test - consumer")))); + MessagingIncubatingAttributes.MESSAGING_CLIENT_ID, + stringAssert -> stringAssert.startsWith("consumer")), + equalTo(MessagingIncubatingAttributes.MESSAGING_BATCH_MESSAGE_COUNT, 1))); if (testHeaders) { assertions.add( equalTo( diff --git a/instrumentation/kafka/kafka-clients/kafka-clients-2.6/library/src/test/java/io/opentelemetry/instrumentation/kafkaclients/v2_6/internal/OpenTelemetryMetricsReporterTest.java b/instrumentation/kafka/kafka-clients/kafka-clients-2.6/library/src/test/java/io/opentelemetry/instrumentation/kafkaclients/v2_6/internal/OpenTelemetryMetricsReporterTest.java index 11a48d71d2dc..acce809e39f1 100644 --- a/instrumentation/kafka/kafka-clients/kafka-clients-2.6/library/src/test/java/io/opentelemetry/instrumentation/kafkaclients/v2_6/internal/OpenTelemetryMetricsReporterTest.java +++ b/instrumentation/kafka/kafka-clients/kafka-clients-2.6/library/src/test/java/io/opentelemetry/instrumentation/kafkaclients/v2_6/internal/OpenTelemetryMetricsReporterTest.java @@ -24,6 +24,7 @@ import java.util.Map; import org.apache.kafka.clients.consumer.KafkaConsumer; import org.apache.kafka.clients.producer.KafkaProducer; +import org.junit.jupiter.api.Assumptions; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; @@ -44,6 +45,8 @@ protected InstrumentationExtension testing() { @Test void badConfig() { + Assumptions.assumeFalse(Boolean.getBoolean("testLatestDeps")); + // Bad producer config assertThatThrownBy( () -> { diff --git a/instrumentation/kafka/kafka-clients/kafka-clients-common/library/src/main/java/io/opentelemetry/instrumentation/kafka/internal/KafkaConsumerAttributesExtractor.java b/instrumentation/kafka/kafka-clients/kafka-clients-common/library/src/main/java/io/opentelemetry/instrumentation/kafka/internal/KafkaConsumerAttributesExtractor.java index cedad1c5e484..bb500ddeaa16 100644 --- a/instrumentation/kafka/kafka-clients/kafka-clients-common/library/src/main/java/io/opentelemetry/instrumentation/kafka/internal/KafkaConsumerAttributesExtractor.java +++ b/instrumentation/kafka/kafka-clients/kafka-clients-common/library/src/main/java/io/opentelemetry/instrumentation/kafka/internal/KafkaConsumerAttributesExtractor.java @@ -5,10 +5,10 @@ package io.opentelemetry.instrumentation.kafka.internal; +import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.common.AttributesBuilder; import io.opentelemetry.context.Context; import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; import java.nio.ByteBuffer; import javax.annotation.Nullable; import org.apache.kafka.clients.consumer.ConsumerRecord; @@ -16,36 +16,38 @@ final class KafkaConsumerAttributesExtractor implements AttributesExtractor { + // copied from MessagingIncubatingAttributes + private static final AttributeKey MESSAGING_DESTINATION_PARTITION_ID = + AttributeKey.stringKey("messaging.destination.partition.id"); + private static final AttributeKey MESSAGING_KAFKA_CONSUMER_GROUP = + AttributeKey.stringKey("messaging.kafka.consumer.group"); + private static final AttributeKey MESSAGING_KAFKA_MESSAGE_KEY = + AttributeKey.stringKey("messaging.kafka.message.key"); + private static final AttributeKey MESSAGING_KAFKA_MESSAGE_OFFSET = + AttributeKey.longKey("messaging.kafka.message.offset"); + private static final AttributeKey MESSAGING_KAFKA_MESSAGE_TOMBSTONE = + AttributeKey.booleanKey("messaging.kafka.message.tombstone"); + @Override public void onStart( AttributesBuilder attributes, Context parentContext, KafkaProcessRequest request) { ConsumerRecord record = request.getRecord(); - attributes.put(SemanticAttributes.MESSAGING_KAFKA_SOURCE_PARTITION, (long) record.partition()); - attributes.put(SemanticAttributes.MESSAGING_KAFKA_MESSAGE_OFFSET, record.offset()); + attributes.put(MESSAGING_DESTINATION_PARTITION_ID, String.valueOf(record.partition())); + attributes.put(MESSAGING_KAFKA_MESSAGE_OFFSET, record.offset()); Object key = record.key(); if (key != null && canSerialize(key.getClass())) { - attributes.put(SemanticAttributes.MESSAGING_KAFKA_MESSAGE_KEY, key.toString()); + attributes.put(MESSAGING_KAFKA_MESSAGE_KEY, key.toString()); } if (record.value() == null) { - attributes.put(SemanticAttributes.MESSAGING_KAFKA_MESSAGE_TOMBSTONE, true); + attributes.put(MESSAGING_KAFKA_MESSAGE_TOMBSTONE, true); } String consumerGroup = request.getConsumerGroup(); if (consumerGroup != null) { - attributes.put(SemanticAttributes.MESSAGING_KAFKA_CONSUMER_GROUP, consumerGroup); - } - - String clientId = request.getClientId(); - if (clientId != null) { - attributes.put(SemanticAttributes.MESSAGING_KAFKA_CLIENT_ID, clientId); - } - - String consumerId = request.getConsumerId(); - if (consumerId != null) { - attributes.put(SemanticAttributes.MESSAGING_CONSUMER_ID, consumerId); + attributes.put(MESSAGING_KAFKA_CONSUMER_GROUP, consumerGroup); } } diff --git a/instrumentation/kafka/kafka-clients/kafka-clients-common/library/src/main/java/io/opentelemetry/instrumentation/kafka/internal/KafkaConsumerAttributesGetter.java b/instrumentation/kafka/kafka-clients/kafka-clients-common/library/src/main/java/io/opentelemetry/instrumentation/kafka/internal/KafkaConsumerAttributesGetter.java index 18e3fa44a1ea..281226d0a68c 100644 --- a/instrumentation/kafka/kafka-clients/kafka-clients-common/library/src/main/java/io/opentelemetry/instrumentation/kafka/internal/KafkaConsumerAttributesGetter.java +++ b/instrumentation/kafka/kafka-clients/kafka-clients-common/library/src/main/java/io/opentelemetry/instrumentation/kafka/internal/KafkaConsumerAttributesGetter.java @@ -5,7 +5,7 @@ package io.opentelemetry.instrumentation.kafka.internal; -import io.opentelemetry.instrumentation.api.instrumenter.messaging.MessagingAttributesGetter; +import io.opentelemetry.instrumentation.api.incubator.semconv.messaging.MessagingAttributesGetter; import java.nio.charset.StandardCharsets; import java.util.List; import java.util.stream.Collectors; @@ -25,25 +25,38 @@ public String getDestination(KafkaProcessRequest request) { return request.getRecord().topic(); } + @Nullable + @Override + public String getDestinationTemplate(KafkaProcessRequest request) { + return null; + } + @Override public boolean isTemporaryDestination(KafkaProcessRequest request) { return false; } + @Override + public boolean isAnonymousDestination(KafkaProcessRequest request) { + return false; + } + @Override @Nullable public String getConversationId(KafkaProcessRequest request) { return null; } + @Nullable @Override - public Long getMessagePayloadSize(KafkaProcessRequest request) { - return (long) request.getRecord().serializedValueSize(); + public Long getMessageBodySize(KafkaProcessRequest request) { + long size = request.getRecord().serializedValueSize(); + return size >= 0 ? size : null; } - @Override @Nullable - public Long getMessagePayloadCompressedSize(KafkaProcessRequest request) { + @Override + public Long getMessageEnvelopeSize(KafkaProcessRequest request) { return null; } @@ -53,6 +66,18 @@ public String getMessageId(KafkaProcessRequest request, @Nullable Void unused) { return null; } + @Nullable + @Override + public String getClientId(KafkaProcessRequest request) { + return request.getClientId(); + } + + @Nullable + @Override + public Long getBatchMessageCount(KafkaProcessRequest request, @Nullable Void unused) { + return null; + } + @Override public List getMessageHeader(KafkaProcessRequest request, String name) { return StreamSupport.stream(request.getRecord().headers().headers(name).spliterator(), false) diff --git a/instrumentation/kafka/kafka-clients/kafka-clients-common/library/src/main/java/io/opentelemetry/instrumentation/kafka/internal/KafkaConsumerContext.java b/instrumentation/kafka/kafka-clients/kafka-clients-common/library/src/main/java/io/opentelemetry/instrumentation/kafka/internal/KafkaConsumerContext.java index 44ad04ebcba5..c5bf69532219 100644 --- a/instrumentation/kafka/kafka-clients/kafka-clients-common/library/src/main/java/io/opentelemetry/instrumentation/kafka/internal/KafkaConsumerContext.java +++ b/instrumentation/kafka/kafka-clients/kafka-clients-common/library/src/main/java/io/opentelemetry/instrumentation/kafka/internal/KafkaConsumerContext.java @@ -8,7 +8,6 @@ import com.google.auto.value.AutoValue; import io.opentelemetry.context.Context; import javax.annotation.Nullable; -import org.apache.kafka.clients.consumer.Consumer; /** * This class is internal and is hence not for public use. Its APIs are unstable and can change at @@ -17,13 +16,17 @@ @AutoValue public abstract class KafkaConsumerContext { - static KafkaConsumerContext create(@Nullable Context context, @Nullable Consumer consumer) { - return new AutoValue_KafkaConsumerContext(context, consumer); + static KafkaConsumerContext create( + @Nullable Context context, @Nullable String consumerGroup, @Nullable String clientId) { + return new AutoValue_KafkaConsumerContext(context, consumerGroup, clientId); } @Nullable public abstract Context getContext(); @Nullable - abstract Consumer getConsumer(); + abstract String getConsumerGroup(); + + @Nullable + abstract String getClientId(); } diff --git a/instrumentation/kafka/kafka-clients/kafka-clients-common/library/src/main/java/io/opentelemetry/instrumentation/kafka/internal/KafkaConsumerContextUtil.java b/instrumentation/kafka/kafka-clients/kafka-clients-common/library/src/main/java/io/opentelemetry/instrumentation/kafka/internal/KafkaConsumerContextUtil.java index 6bdc0383bf8c..d9b9be9d6886 100644 --- a/instrumentation/kafka/kafka-clients/kafka-clients-common/library/src/main/java/io/opentelemetry/instrumentation/kafka/internal/KafkaConsumerContextUtil.java +++ b/instrumentation/kafka/kafka-clients/kafka-clients-common/library/src/main/java/io/opentelemetry/instrumentation/kafka/internal/KafkaConsumerContextUtil.java @@ -16,44 +16,86 @@ * any time. */ public final class KafkaConsumerContextUtil { + // these fields can be used for multiple instrumentations because of that we don't use a helper + // class as field type private static final VirtualField, Context> recordContextField = VirtualField.find(ConsumerRecord.class, Context.class); - private static final VirtualField, Consumer> recordConsumerField = - VirtualField.find(ConsumerRecord.class, Consumer.class); + private static final VirtualField, String[]> recordConsumerInfoField = + VirtualField.find(ConsumerRecord.class, String[].class); private static final VirtualField, Context> recordsContextField = VirtualField.find(ConsumerRecords.class, Context.class); - private static final VirtualField, Consumer> recordsConsumerField = - VirtualField.find(ConsumerRecords.class, Consumer.class); + private static final VirtualField, String[]> recordsConsumerInfoField = + VirtualField.find(ConsumerRecords.class, String[].class); public static KafkaConsumerContext get(ConsumerRecord records) { Context receiveContext = recordContextField.get(records); - Consumer consumer = recordConsumerField.get(records); - return KafkaConsumerContext.create(receiveContext, consumer); + String consumerGroup = null; + String clientId = null; + String[] consumerInfo = recordConsumerInfoField.get(records); + if (consumerInfo != null) { + consumerGroup = consumerInfo[0]; + clientId = consumerInfo[1]; + } + return create(receiveContext, consumerGroup, clientId); } public static KafkaConsumerContext get(ConsumerRecords records) { Context receiveContext = recordsContextField.get(records); - Consumer consumer = recordsConsumerField.get(records); - return KafkaConsumerContext.create(receiveContext, consumer); + String consumerGroup = null; + String clientId = null; + String[] consumerInfo = recordsConsumerInfoField.get(records); + if (consumerInfo != null) { + consumerGroup = consumerInfo[0]; + clientId = consumerInfo[1]; + } + return create(receiveContext, consumerGroup, clientId); + } + + public static KafkaConsumerContext create(Context context, Consumer consumer) { + return create(context, KafkaUtil.getConsumerGroup(consumer), KafkaUtil.getClientId(consumer)); + } + + public static KafkaConsumerContext create( + Context context, String consumerGroup, String clientId) { + return KafkaConsumerContext.create(context, consumerGroup, clientId); } public static void set(ConsumerRecord record, Context context, Consumer consumer) { recordContextField.set(record, context); - recordConsumerField.set(record, consumer); + String consumerGroup = KafkaUtil.getConsumerGroup(consumer); + String clientId = KafkaUtil.getClientId(consumer); + set(record, context, consumerGroup, clientId); } public static void set(ConsumerRecord record, KafkaConsumerContext consumerContext) { - set(record, consumerContext.getContext(), consumerContext.getConsumer()); + set( + record, + consumerContext.getContext(), + consumerContext.getConsumerGroup(), + consumerContext.getClientId()); + } + + public static void set( + ConsumerRecord record, Context context, String consumerGroup, String clientId) { + recordContextField.set(record, context); + recordConsumerInfoField.set(record, new String[] {consumerGroup, clientId}); } public static void set(ConsumerRecords records, Context context, Consumer consumer) { + String consumerGroup = KafkaUtil.getConsumerGroup(consumer); + String clientId = KafkaUtil.getClientId(consumer); + set(records, context, consumerGroup, clientId); + } + + public static void set( + ConsumerRecords records, Context context, String consumerGroup, String clientId) { recordsContextField.set(records, context); - recordsConsumerField.set(records, consumer); + recordsConsumerInfoField.set(records, new String[] {consumerGroup, clientId}); } public static void copy(ConsumerRecord from, ConsumerRecord to) { recordContextField.set(to, recordContextField.get(from)); - recordConsumerField.set(to, recordConsumerField.get(from)); + recordConsumerInfoField.set(to, recordConsumerInfoField.get(from)); } private KafkaConsumerContextUtil() {} diff --git a/instrumentation/kafka/kafka-clients/kafka-clients-common/library/src/main/java/io/opentelemetry/instrumentation/kafka/internal/KafkaInstrumenterFactory.java b/instrumentation/kafka/kafka-clients/kafka-clients-common/library/src/main/java/io/opentelemetry/instrumentation/kafka/internal/KafkaInstrumenterFactory.java index 216fb00c9645..710107d24ae1 100644 --- a/instrumentation/kafka/kafka-clients/kafka-clients-common/library/src/main/java/io/opentelemetry/instrumentation/kafka/internal/KafkaInstrumenterFactory.java +++ b/instrumentation/kafka/kafka-clients/kafka-clients-common/library/src/main/java/io/opentelemetry/instrumentation/kafka/internal/KafkaInstrumenterFactory.java @@ -9,15 +9,15 @@ import com.google.errorprone.annotations.CanIgnoreReturnValue; import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.instrumentation.api.incubator.semconv.messaging.MessageOperation; +import io.opentelemetry.instrumentation.api.incubator.semconv.messaging.MessagingAttributesExtractor; +import io.opentelemetry.instrumentation.api.incubator.semconv.messaging.MessagingAttributesGetter; +import io.opentelemetry.instrumentation.api.incubator.semconv.messaging.MessagingSpanNameExtractor; import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; import io.opentelemetry.instrumentation.api.instrumenter.ErrorCauseExtractor; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; import io.opentelemetry.instrumentation.api.instrumenter.InstrumenterBuilder; import io.opentelemetry.instrumentation.api.instrumenter.SpanKindExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.messaging.MessageOperation; -import io.opentelemetry.instrumentation.api.instrumenter.messaging.MessagingAttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.messaging.MessagingAttributesGetter; -import io.opentelemetry.instrumentation.api.instrumenter.messaging.MessagingSpanNameExtractor; import io.opentelemetry.instrumentation.api.internal.PropagatorBasedSpanLinksExtractor; import java.util.Collections; import java.util.List; @@ -86,7 +86,7 @@ public Instrumenter createProducerInstrume Iterable> extractors) { KafkaProducerAttributesGetter getter = KafkaProducerAttributesGetter.INSTANCE; - MessageOperation operation = MessageOperation.SEND; + MessageOperation operation = MessageOperation.PUBLISH; return Instrumenter.builder( openTelemetry, @@ -101,6 +101,11 @@ public Instrumenter createProducerInstrume } public Instrumenter createConsumerReceiveInstrumenter() { + return createConsumerReceiveInstrumenter(Collections.emptyList()); + } + + public Instrumenter createConsumerReceiveInstrumenter( + Iterable> extractors) { KafkaReceiveAttributesGetter getter = KafkaReceiveAttributesGetter.INSTANCE; MessageOperation operation = MessageOperation.RECEIVE; @@ -111,20 +116,20 @@ public Instrumenter createConsumerReceiveInstrumenter .addAttributesExtractor( buildMessagingAttributesExtractor(getter, operation, capturedHeaders)) .addAttributesExtractor(KafkaReceiveAttributesExtractor.INSTANCE) + .addAttributesExtractors(extractors) .setErrorCauseExtractor(errorCauseExtractor) .setEnabled(messagingReceiveInstrumentationEnabled) .buildInstrumenter(SpanKindExtractor.alwaysConsumer()); } public Instrumenter createConsumerProcessInstrumenter() { - return createConsumerOperationInstrumenter(MessageOperation.PROCESS, Collections.emptyList()); + return createConsumerProcessInstrumenter(Collections.emptyList()); } - public Instrumenter createConsumerOperationInstrumenter( - MessageOperation operation, + public Instrumenter createConsumerProcessInstrumenter( Iterable> extractors) { - KafkaConsumerAttributesGetter getter = KafkaConsumerAttributesGetter.INSTANCE; + MessageOperation operation = MessageOperation.PROCESS; InstrumenterBuilder builder = Instrumenter.builder( diff --git a/instrumentation/kafka/kafka-clients/kafka-clients-common/library/src/main/java/io/opentelemetry/instrumentation/kafka/internal/KafkaProcessRequest.java b/instrumentation/kafka/kafka-clients/kafka-clients-common/library/src/main/java/io/opentelemetry/instrumentation/kafka/internal/KafkaProcessRequest.java index cc50c49f395a..57a7c7bc622f 100644 --- a/instrumentation/kafka/kafka-clients/kafka-clients-common/library/src/main/java/io/opentelemetry/instrumentation/kafka/internal/KafkaProcessRequest.java +++ b/instrumentation/kafka/kafka-clients/kafka-clients-common/library/src/main/java/io/opentelemetry/instrumentation/kafka/internal/KafkaProcessRequest.java @@ -22,7 +22,9 @@ public static KafkaProcessRequest create(ConsumerRecord record, Consumer record) { - return create(record, consumerContext != null ? consumerContext.getConsumer() : null); + String consumerGroup = consumerContext != null ? consumerContext.getConsumerGroup() : null; + String clientId = consumerContext != null ? consumerContext.getClientId() : null; + return create(record, consumerGroup, clientId); } public static KafkaProcessRequest create( diff --git a/instrumentation/kafka/kafka-clients/kafka-clients-common/library/src/main/java/io/opentelemetry/instrumentation/kafka/internal/KafkaProducerAttributesExtractor.java b/instrumentation/kafka/kafka-clients/kafka-clients-common/library/src/main/java/io/opentelemetry/instrumentation/kafka/internal/KafkaProducerAttributesExtractor.java index e6ccfb973966..37d2f16fdc9f 100644 --- a/instrumentation/kafka/kafka-clients/kafka-clients-common/library/src/main/java/io/opentelemetry/instrumentation/kafka/internal/KafkaProducerAttributesExtractor.java +++ b/instrumentation/kafka/kafka-clients/kafka-clients-common/library/src/main/java/io/opentelemetry/instrumentation/kafka/internal/KafkaProducerAttributesExtractor.java @@ -5,16 +5,25 @@ package io.opentelemetry.instrumentation.kafka.internal; +import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.common.AttributesBuilder; import io.opentelemetry.context.Context; import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; import java.nio.ByteBuffer; import javax.annotation.Nullable; import org.apache.kafka.clients.producer.RecordMetadata; final class KafkaProducerAttributesExtractor implements AttributesExtractor { + // copied from MessagingIncubatingAttributes + private static final AttributeKey MESSAGING_DESTINATION_PARTITION_ID = + AttributeKey.stringKey("messaging.destination.partition.id"); + private static final AttributeKey MESSAGING_KAFKA_MESSAGE_KEY = + AttributeKey.stringKey("messaging.kafka.message.key"); + private static final AttributeKey MESSAGING_KAFKA_MESSAGE_OFFSET = + AttributeKey.longKey("messaging.kafka.message.offset"); + private static final AttributeKey MESSAGING_KAFKA_MESSAGE_TOMBSTONE = + AttributeKey.booleanKey("messaging.kafka.message.tombstone"); @Override public void onStart( @@ -22,13 +31,10 @@ public void onStart( Object key = request.getRecord().key(); if (key != null && canSerialize(key.getClass())) { - attributes.put(SemanticAttributes.MESSAGING_KAFKA_MESSAGE_KEY, key.toString()); + attributes.put(MESSAGING_KAFKA_MESSAGE_KEY, key.toString()); } if (request.getRecord().value() == null) { - attributes.put(SemanticAttributes.MESSAGING_KAFKA_MESSAGE_TOMBSTONE, true); - } - if (request.getClientId() != null) { - attributes.put(SemanticAttributes.MESSAGING_KAFKA_CLIENT_ID, request.getClientId()); + attributes.put(MESSAGING_KAFKA_MESSAGE_TOMBSTONE, true); } } @@ -48,8 +54,8 @@ public void onEnd( if (recordMetadata != null) { attributes.put( - SemanticAttributes.MESSAGING_KAFKA_DESTINATION_PARTITION, recordMetadata.partition()); - attributes.put(SemanticAttributes.MESSAGING_KAFKA_MESSAGE_OFFSET, recordMetadata.offset()); + MESSAGING_DESTINATION_PARTITION_ID, String.valueOf(recordMetadata.partition())); + attributes.put(MESSAGING_KAFKA_MESSAGE_OFFSET, recordMetadata.offset()); } } } diff --git a/instrumentation/kafka/kafka-clients/kafka-clients-common/library/src/main/java/io/opentelemetry/instrumentation/kafka/internal/KafkaProducerAttributesGetter.java b/instrumentation/kafka/kafka-clients/kafka-clients-common/library/src/main/java/io/opentelemetry/instrumentation/kafka/internal/KafkaProducerAttributesGetter.java index 2fb90ce43cbe..73157d8f92fa 100644 --- a/instrumentation/kafka/kafka-clients/kafka-clients-common/library/src/main/java/io/opentelemetry/instrumentation/kafka/internal/KafkaProducerAttributesGetter.java +++ b/instrumentation/kafka/kafka-clients/kafka-clients-common/library/src/main/java/io/opentelemetry/instrumentation/kafka/internal/KafkaProducerAttributesGetter.java @@ -5,7 +5,7 @@ package io.opentelemetry.instrumentation.kafka.internal; -import io.opentelemetry.instrumentation.api.instrumenter.messaging.MessagingAttributesGetter; +import io.opentelemetry.instrumentation.api.incubator.semconv.messaging.MessagingAttributesGetter; import java.nio.charset.StandardCharsets; import java.util.List; import java.util.stream.Collectors; @@ -31,32 +31,56 @@ public String getDestination(KafkaProducerRequest request) { return request.getRecord().topic(); } + @Nullable + @Override + public String getDestinationTemplate(KafkaProducerRequest request) { + return null; + } + @Override public boolean isTemporaryDestination(KafkaProducerRequest request) { return false; } + @Override + public boolean isAnonymousDestination(KafkaProducerRequest request) { + return false; + } + @Override @Nullable public String getConversationId(KafkaProducerRequest request) { return null; } + @Nullable @Override + public Long getMessageBodySize(KafkaProducerRequest request) { + return null; + } + @Nullable - public Long getMessagePayloadSize(KafkaProducerRequest request) { + @Override + public Long getMessageEnvelopeSize(KafkaProducerRequest request) { return null; } @Override @Nullable - public Long getMessagePayloadCompressedSize(KafkaProducerRequest request) { + public String getMessageId( + KafkaProducerRequest request, @Nullable RecordMetadata recordMetadata) { return null; } + @Nullable @Override + public String getClientId(KafkaProducerRequest request) { + return request.getClientId(); + } + @Nullable - public String getMessageId( + @Override + public Long getBatchMessageCount( KafkaProducerRequest request, @Nullable RecordMetadata recordMetadata) { return null; } diff --git a/instrumentation/kafka/kafka-clients/kafka-clients-common/library/src/main/java/io/opentelemetry/instrumentation/kafka/internal/KafkaReceiveAttributesExtractor.java b/instrumentation/kafka/kafka-clients/kafka-clients-common/library/src/main/java/io/opentelemetry/instrumentation/kafka/internal/KafkaReceiveAttributesExtractor.java index c75f0945240d..7593e396f230 100644 --- a/instrumentation/kafka/kafka-clients/kafka-clients-common/library/src/main/java/io/opentelemetry/instrumentation/kafka/internal/KafkaReceiveAttributesExtractor.java +++ b/instrumentation/kafka/kafka-clients/kafka-clients-common/library/src/main/java/io/opentelemetry/instrumentation/kafka/internal/KafkaReceiveAttributesExtractor.java @@ -5,32 +5,26 @@ package io.opentelemetry.instrumentation.kafka.internal; +import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.common.AttributesBuilder; import io.opentelemetry.context.Context; import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; import javax.annotation.Nullable; enum KafkaReceiveAttributesExtractor implements AttributesExtractor { INSTANCE; + // copied from MessagingIncubatingAttributes + private static final AttributeKey MESSAGING_KAFKA_CONSUMER_GROUP = + AttributeKey.stringKey("messaging.kafka.consumer.group"); + @Override public void onStart( AttributesBuilder attributes, Context parentContext, KafkaReceiveRequest request) { String consumerGroup = request.getConsumerGroup(); if (consumerGroup != null) { - attributes.put(SemanticAttributes.MESSAGING_KAFKA_CONSUMER_GROUP, consumerGroup); - } - - String clientId = request.getClientId(); - if (clientId != null) { - attributes.put(SemanticAttributes.MESSAGING_KAFKA_CLIENT_ID, clientId); - } - - String consumerId = request.getConsumerId(); - if (consumerId != null) { - attributes.put(SemanticAttributes.MESSAGING_CONSUMER_ID, consumerId); + attributes.put(MESSAGING_KAFKA_CONSUMER_GROUP, consumerGroup); } } diff --git a/instrumentation/kafka/kafka-clients/kafka-clients-common/library/src/main/java/io/opentelemetry/instrumentation/kafka/internal/KafkaReceiveAttributesGetter.java b/instrumentation/kafka/kafka-clients/kafka-clients-common/library/src/main/java/io/opentelemetry/instrumentation/kafka/internal/KafkaReceiveAttributesGetter.java index 2c4ba4b99871..907b2610c89b 100644 --- a/instrumentation/kafka/kafka-clients/kafka-clients-common/library/src/main/java/io/opentelemetry/instrumentation/kafka/internal/KafkaReceiveAttributesGetter.java +++ b/instrumentation/kafka/kafka-clients/kafka-clients-common/library/src/main/java/io/opentelemetry/instrumentation/kafka/internal/KafkaReceiveAttributesGetter.java @@ -5,7 +5,7 @@ package io.opentelemetry.instrumentation.kafka.internal; -import io.opentelemetry.instrumentation.api.instrumenter.messaging.MessagingAttributesGetter; +import io.opentelemetry.instrumentation.api.incubator.semconv.messaging.MessagingAttributesGetter; import java.nio.charset.StandardCharsets; import java.util.List; import java.util.Set; @@ -33,26 +33,37 @@ public String getDestination(KafkaReceiveRequest request) { return topics.size() == 1 ? topics.iterator().next() : null; } + @Nullable + @Override + public String getDestinationTemplate(KafkaReceiveRequest request) { + return null; + } + @Override public boolean isTemporaryDestination(KafkaReceiveRequest request) { return false; } + @Override + public boolean isAnonymousDestination(KafkaReceiveRequest request) { + return false; + } + @Override @Nullable public String getConversationId(KafkaReceiveRequest request) { return null; } - @Override @Nullable - public Long getMessagePayloadSize(KafkaReceiveRequest request) { + @Override + public Long getMessageBodySize(KafkaReceiveRequest request) { return null; } - @Override @Nullable - public Long getMessagePayloadCompressedSize(KafkaReceiveRequest request) { + @Override + public Long getMessageEnvelopeSize(KafkaReceiveRequest request) { return null; } @@ -62,6 +73,17 @@ public String getMessageId(KafkaReceiveRequest request, @Nullable Void unused) { return null; } + @Nullable + @Override + public String getClientId(KafkaReceiveRequest request) { + return request.getClientId(); + } + + @Override + public Long getBatchMessageCount(KafkaReceiveRequest request, @Nullable Void unused) { + return (long) request.getRecords().count(); + } + @Override public List getMessageHeader(KafkaReceiveRequest request, String name) { return StreamSupport.stream(request.getRecords().spliterator(), false) diff --git a/instrumentation/kafka/kafka-clients/kafka-clients-common/library/src/main/java/io/opentelemetry/instrumentation/kafka/internal/KafkaReceiveRequest.java b/instrumentation/kafka/kafka-clients/kafka-clients-common/library/src/main/java/io/opentelemetry/instrumentation/kafka/internal/KafkaReceiveRequest.java index d95ffb90e037..cb09511dd7f8 100644 --- a/instrumentation/kafka/kafka-clients/kafka-clients-common/library/src/main/java/io/opentelemetry/instrumentation/kafka/internal/KafkaReceiveRequest.java +++ b/instrumentation/kafka/kafka-clients/kafka-clients-common/library/src/main/java/io/opentelemetry/instrumentation/kafka/internal/KafkaReceiveRequest.java @@ -24,7 +24,9 @@ public static KafkaReceiveRequest create( public static KafkaReceiveRequest create( KafkaConsumerContext consumerContext, ConsumerRecords records) { - return create(records, consumerContext != null ? consumerContext.getConsumer() : null); + String consumerGroup = consumerContext != null ? consumerContext.getConsumerGroup() : null; + String clientId = consumerContext != null ? consumerContext.getClientId() : null; + return create(records, consumerGroup, clientId); } public static KafkaReceiveRequest create( diff --git a/instrumentation/kafka/kafka-clients/kafka-clients-common/library/src/main/java/io/opentelemetry/instrumentation/kafka/internal/MetricsReporterList.java b/instrumentation/kafka/kafka-clients/kafka-clients-common/library/src/main/java/io/opentelemetry/instrumentation/kafka/internal/MetricsReporterList.java new file mode 100644 index 000000000000..2478dbcd7bf6 --- /dev/null +++ b/instrumentation/kafka/kafka-clients/kafka-clients-common/library/src/main/java/io/opentelemetry/instrumentation/kafka/internal/MetricsReporterList.java @@ -0,0 +1,34 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.kafka.internal; + +import java.util.ArrayList; +import java.util.List; + +/** + * List implementation that can be used to hold metrics reporters in kafka configuration without + * breaking serialization. When this list is serialized it removes OpenTelemetryMetricsReporter to + * ensure that the configuration can be deserialized even when the instrumentation is not present. + * + *

This class is internal and is hence not for public use. Its APIs are unstable and can change + * at any time. + */ +public class MetricsReporterList extends ArrayList { + private static final long serialVersionUID = 1L; + + public static List singletonList(T o) { + List list = new MetricsReporterList<>(); + list.add(o); + return list; + } + + private Object writeReplace() { + // serialize to plain ArrayList that does not contain OpenTelemetryMetricsReporter + List result = new ArrayList<>(); + this.stream().filter(x -> x != OpenTelemetryMetricsReporter.class).forEach(result::add); + return result; + } +} diff --git a/instrumentation/kafka/kafka-clients/kafka-clients-common/library/src/main/java/io/opentelemetry/instrumentation/kafka/internal/OpenTelemetryMetricsReporter.java b/instrumentation/kafka/kafka-clients/kafka-clients-common/library/src/main/java/io/opentelemetry/instrumentation/kafka/internal/OpenTelemetryMetricsReporter.java index 1c98642c3a4b..f58aa345581b 100644 --- a/instrumentation/kafka/kafka-clients/kafka-clients-common/library/src/main/java/io/opentelemetry/instrumentation/kafka/internal/OpenTelemetryMetricsReporter.java +++ b/instrumentation/kafka/kafka-clients/kafka-clients-common/library/src/main/java/io/opentelemetry/instrumentation/kafka/internal/OpenTelemetryMetricsReporter.java @@ -41,28 +41,35 @@ public final class OpenTelemetryMetricsReporter implements MetricsReporter { private static final Logger logger = Logger.getLogger(OpenTelemetryMetricsReporter.class.getName()); - private volatile Meter meter; + private static volatile Listener listener; - private static final Object lock = new Object(); + private volatile Meter meter; + private final Object lock = new Object(); @GuardedBy("lock") - private static final List registeredObservables = new ArrayList<>(); + private final List registeredObservables = new ArrayList<>(); /** * Reset for test by resetting the {@link #meter} to {@code null} and closing all registered * instruments. */ - static void resetForTest() { + void resetForTest() { closeAllInstruments(); } // Visible for test - static List getRegisteredObservables() { + List getRegisteredObservables() { synchronized (lock) { return new ArrayList<>(registeredObservables); } } + public OpenTelemetryMetricsReporter() { + if (listener != null) { + listener.metricsReporterCreated(this); + } + } + @Override public void init(List metrics) { metrics.forEach(this::metricChange); @@ -131,7 +138,7 @@ public void close() { closeAllInstruments(); } - private static void closeAllInstruments() { + private void closeAllInstruments() { synchronized (lock) { for (Iterator it = registeredObservables.iterator(); it.hasNext(); ) { closeInstrument(it.next().getObservable()); @@ -177,4 +184,14 @@ private static T getProperty(Map configs, String key, Class re } return (T) value; } + + // Visible for test + static void setListener(Listener listener) { + OpenTelemetryMetricsReporter.listener = listener; + } + + // used for testing + interface Listener { + void metricsReporterCreated(OpenTelemetryMetricsReporter metricsReporter); + } } diff --git a/instrumentation/kafka/kafka-clients/kafka-clients-common/library/src/main/java/io/opentelemetry/instrumentation/kafka/internal/TracingIterable.java b/instrumentation/kafka/kafka-clients/kafka-clients-common/library/src/main/java/io/opentelemetry/instrumentation/kafka/internal/TracingIterable.java new file mode 100644 index 000000000000..1442fa3573d6 --- /dev/null +++ b/instrumentation/kafka/kafka-clients/kafka-clients-common/library/src/main/java/io/opentelemetry/instrumentation/kafka/internal/TracingIterable.java @@ -0,0 +1,62 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.kafka.internal; + +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import java.util.Iterator; +import java.util.function.BooleanSupplier; +import org.apache.kafka.clients.consumer.ConsumerRecord; + +/** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time. + */ +public class TracingIterable implements Iterable> { + private final Iterable> delegate; + private final Instrumenter instrumenter; + private final BooleanSupplier wrappingEnabled; + private final KafkaConsumerContext consumerContext; + private boolean firstIterator = true; + + protected TracingIterable( + Iterable> delegate, + Instrumenter instrumenter, + BooleanSupplier wrappingEnabled, + KafkaConsumerContext consumerContext) { + this.delegate = delegate; + this.instrumenter = instrumenter; + this.wrappingEnabled = wrappingEnabled; + this.consumerContext = consumerContext; + } + + public static Iterable> wrap( + Iterable> delegate, + Instrumenter instrumenter, + BooleanSupplier wrappingEnabled, + KafkaConsumerContext consumerContext) { + if (wrappingEnabled.getAsBoolean()) { + return new TracingIterable<>(delegate, instrumenter, wrappingEnabled, consumerContext); + } + return delegate; + } + + @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 = + TracingIterator.wrap(delegate.iterator(), instrumenter, wrappingEnabled, consumerContext); + firstIterator = false; + } else { + it = delegate.iterator(); + } + + return it; + } +} diff --git a/instrumentation/kafka/kafka-clients/kafka-clients-0.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kafkaclients/v0_11/TracingIterator.java b/instrumentation/kafka/kafka-clients/kafka-clients-common/library/src/main/java/io/opentelemetry/instrumentation/kafka/internal/TracingIterator.java similarity index 65% rename from instrumentation/kafka/kafka-clients/kafka-clients-0.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kafkaclients/v0_11/TracingIterator.java rename to instrumentation/kafka/kafka-clients/kafka-clients-common/library/src/main/java/io/opentelemetry/instrumentation/kafka/internal/TracingIterator.java index 957439a4e721..e9a934d80c75 100644 --- a/instrumentation/kafka/kafka-clients/kafka-clients-0.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kafkaclients/v0_11/TracingIterator.java +++ b/instrumentation/kafka/kafka-clients/kafka-clients-common/library/src/main/java/io/opentelemetry/instrumentation/kafka/internal/TracingIterator.java @@ -3,22 +3,25 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.javaagent.instrumentation.kafkaclients.v0_11; - -import static io.opentelemetry.javaagent.instrumentation.kafkaclients.v0_11.KafkaSingletons.consumerProcessInstrumenter; +package io.opentelemetry.instrumentation.kafka.internal; import io.opentelemetry.context.Context; import io.opentelemetry.context.Scope; -import io.opentelemetry.instrumentation.kafka.internal.KafkaConsumerContext; -import io.opentelemetry.instrumentation.kafka.internal.KafkaProcessRequest; -import io.opentelemetry.javaagent.bootstrap.kafka.KafkaClientsConsumerProcessTracing; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; import java.util.Iterator; +import java.util.function.BooleanSupplier; import javax.annotation.Nullable; import org.apache.kafka.clients.consumer.ConsumerRecord; +/** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time. + */ public class TracingIterator implements Iterator> { private final Iterator> delegateIterator; + private final Instrumenter instrumenter; + private final BooleanSupplier wrappingEnabled; private final Context parentContext; private final KafkaConsumerContext consumerContext; @@ -31,8 +34,13 @@ public class TracingIterator implements Iterator> { @Nullable private Scope currentScope; private TracingIterator( - Iterator> delegateIterator, KafkaConsumerContext consumerContext) { + Iterator> delegateIterator, + Instrumenter instrumenter, + BooleanSupplier wrappingEnabled, + KafkaConsumerContext consumerContext) { this.delegateIterator = delegateIterator; + this.instrumenter = instrumenter; + this.wrappingEnabled = wrappingEnabled; Context receiveContext = consumerContext.getContext(); // use the receive CONSUMER as parent if it's available @@ -41,9 +49,13 @@ private TracingIterator( } public static Iterator> wrap( - Iterator> delegateIterator, KafkaConsumerContext consumerContext) { - if (KafkaClientsConsumerProcessTracing.wrappingEnabled()) { - return new TracingIterator<>(delegateIterator, consumerContext); + Iterator> delegateIterator, + Instrumenter instrumenter, + BooleanSupplier wrappingEnabled, + KafkaConsumerContext consumerContext) { + if (wrappingEnabled.getAsBoolean()) { + return new TracingIterator<>( + delegateIterator, instrumenter, wrappingEnabled, consumerContext); } return delegateIterator; } @@ -65,9 +77,9 @@ public ConsumerRecord next() { // suppressing the correct span // (https://github.com/open-telemetry/opentelemetry-java-instrumentation/issues/1947) ConsumerRecord next = delegateIterator.next(); - if (next != null && KafkaClientsConsumerProcessTracing.wrappingEnabled()) { + if (next != null && wrappingEnabled.getAsBoolean()) { currentRequest = KafkaProcessRequest.create(consumerContext, next); - currentContext = consumerProcessInstrumenter().start(parentContext, currentRequest); + currentContext = instrumenter.start(parentContext, currentRequest); currentScope = currentContext.makeCurrent(); } return next; @@ -76,7 +88,7 @@ public ConsumerRecord next() { private void closeScopeAndEndSpan() { if (currentScope != null) { currentScope.close(); - consumerProcessInstrumenter().end(currentContext, currentRequest, null, null); + instrumenter.end(currentContext, currentRequest, null, null); currentScope = null; currentRequest = null; currentContext = null; diff --git a/instrumentation/kafka/kafka-clients/kafka-clients-0.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kafkaclients/v0_11/TracingList.java b/instrumentation/kafka/kafka-clients/kafka-clients-common/library/src/main/java/io/opentelemetry/instrumentation/kafka/internal/TracingList.java similarity index 78% rename from instrumentation/kafka/kafka-clients/kafka-clients-0.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kafkaclients/v0_11/TracingList.java rename to instrumentation/kafka/kafka-clients/kafka-clients-common/library/src/main/java/io/opentelemetry/instrumentation/kafka/internal/TracingList.java index 7d326978fcc6..6d7e2edf8b91 100644 --- a/instrumentation/kafka/kafka-clients/kafka-clients-0.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kafkaclients/v0_11/TracingList.java +++ b/instrumentation/kafka/kafka-clients/kafka-clients-common/library/src/main/java/io/opentelemetry/instrumentation/kafka/internal/TracingList.java @@ -3,27 +3,38 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.javaagent.instrumentation.kafkaclients.v0_11; +package io.opentelemetry.instrumentation.kafka.internal; -import io.opentelemetry.instrumentation.kafka.internal.KafkaConsumerContext; -import io.opentelemetry.javaagent.bootstrap.kafka.KafkaClientsConsumerProcessTracing; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; import java.util.Collection; import java.util.List; import java.util.ListIterator; +import java.util.function.BooleanSupplier; import org.apache.kafka.clients.consumer.ConsumerRecord; +/** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time. + */ public class TracingList extends TracingIterable implements List> { private final List> delegate; - private TracingList(List> delegate, KafkaConsumerContext consumerContext) { - super(delegate, consumerContext); + private TracingList( + List> delegate, + Instrumenter instrumenter, + BooleanSupplier wrappingEnabled, + KafkaConsumerContext consumerContext) { + super(delegate, instrumenter, wrappingEnabled, consumerContext); this.delegate = delegate; } public static List> wrap( - List> delegate, KafkaConsumerContext consumerContext) { - if (KafkaClientsConsumerProcessTracing.wrappingEnabled()) { - return new TracingList<>(delegate, consumerContext); + List> delegate, + Instrumenter instrumenter, + BooleanSupplier wrappingEnabled, + KafkaConsumerContext consumerContext) { + if (wrappingEnabled.getAsBoolean()) { + return new TracingList<>(delegate, instrumenter, wrappingEnabled, consumerContext); } return delegate; } diff --git a/instrumentation/kafka/kafka-streams-0.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kafkastreams/KafkaStreamsSingletons.java b/instrumentation/kafka/kafka-streams-0.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kafkastreams/KafkaStreamsSingletons.java index 8abc39e5ea68..5733eaa2d18a 100644 --- a/instrumentation/kafka/kafka-streams-0.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kafkastreams/KafkaStreamsSingletons.java +++ b/instrumentation/kafka/kafka-streams-0.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kafkastreams/KafkaStreamsSingletons.java @@ -9,8 +9,8 @@ import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; import io.opentelemetry.instrumentation.kafka.internal.KafkaInstrumenterFactory; import io.opentelemetry.instrumentation.kafka.internal.KafkaProcessRequest; +import io.opentelemetry.javaagent.bootstrap.internal.AgentInstrumentationConfig; import io.opentelemetry.javaagent.bootstrap.internal.ExperimentalConfig; -import io.opentelemetry.javaagent.bootstrap.internal.InstrumentationConfig; public final class KafkaStreamsSingletons { @@ -20,7 +20,7 @@ public final class KafkaStreamsSingletons { new KafkaInstrumenterFactory(GlobalOpenTelemetry.get(), INSTRUMENTATION_NAME) .setCapturedHeaders(ExperimentalConfig.get().getMessagingHeaders()) .setCaptureExperimentalSpanAttributes( - InstrumentationConfig.get() + AgentInstrumentationConfig.get() .getBoolean("otel.instrumentation.kafka.experimental-span-attributes", false)) .setMessagingReceiveInstrumentationEnabled( ExperimentalConfig.get().messagingReceiveInstrumentationEnabled()) diff --git a/instrumentation/kafka/kafka-streams-0.11/javaagent/src/test/groovy/KafkaStreamsBaseTest.groovy b/instrumentation/kafka/kafka-streams-0.11/javaagent/src/test/groovy/KafkaStreamsBaseTest.groovy index d625531d1d53..ef3cff670b01 100644 --- a/instrumentation/kafka/kafka-streams-0.11/javaagent/src/test/groovy/KafkaStreamsBaseTest.groovy +++ b/instrumentation/kafka/kafka-streams-0.11/javaagent/src/test/groovy/KafkaStreamsBaseTest.groovy @@ -44,7 +44,7 @@ class KafkaStreamsBaseTest extends AgentInstrumentationSpecification { static CountDownLatch consumerReady = new CountDownLatch(1) def setupSpec() { - kafka = new KafkaContainer(DockerImageName.parse("confluentinc/cp-kafka:6.2.10")) + kafka = new KafkaContainer(DockerImageName.parse("confluentinc/cp-kafka:7.7.0")) .withEnv("KAFKA_HEAP_OPTS", "-Xmx256m") .withLogConsumer(new Slf4jLogConsumer(logger)) .waitingFor(Wait.forLogMessage(".*started \\(kafka.server.KafkaServer\\).*", 1)) diff --git a/instrumentation/kafka/kafka-streams-0.11/javaagent/src/test/groovy/KafkaStreamsDefaultTest.groovy b/instrumentation/kafka/kafka-streams-0.11/javaagent/src/test/groovy/KafkaStreamsDefaultTest.groovy index 385a078b8e52..d50f9e950dba 100644 --- a/instrumentation/kafka/kafka-streams-0.11/javaagent/src/test/groovy/KafkaStreamsDefaultTest.groovy +++ b/instrumentation/kafka/kafka-streams-0.11/javaagent/src/test/groovy/KafkaStreamsDefaultTest.groovy @@ -8,7 +8,7 @@ import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator import io.opentelemetry.context.Context import io.opentelemetry.context.propagation.TextMapGetter import io.opentelemetry.sdk.trace.data.SpanData -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes import org.apache.kafka.clients.producer.ProducerRecord import org.apache.kafka.common.header.Headers import org.apache.kafka.common.serialization.Serdes @@ -84,7 +84,7 @@ class KafkaStreamsDefaultTest extends KafkaStreamsBaseTest { assertTraces(3) { traces.sort(orderByRootSpanName( - STREAM_PENDING + " send", + STREAM_PENDING + " publish", STREAM_PENDING + " receive", STREAM_PROCESSED + " receive")) @@ -93,16 +93,17 @@ class KafkaStreamsDefaultTest extends KafkaStreamsBaseTest { trace(0, 1) { // kafka-clients PRODUCER span(0) { - name STREAM_PENDING + " send" + name STREAM_PENDING + " publish" kind PRODUCER hasNoParent() attributes { - "$SemanticAttributes.MESSAGING_SYSTEM" "kafka" - "$SemanticAttributes.MESSAGING_DESTINATION_NAME" STREAM_PENDING - "$SemanticAttributes.MESSAGING_KAFKA_CLIENT_ID" { it.startsWith("producer") } - "$SemanticAttributes.MESSAGING_KAFKA_DESTINATION_PARTITION" { it >= 0 } - "$SemanticAttributes.MESSAGING_KAFKA_MESSAGE_OFFSET" 0 - "$SemanticAttributes.MESSAGING_KAFKA_MESSAGE_KEY" "10" + "$MessagingIncubatingAttributes.MESSAGING_SYSTEM" "kafka" + "$MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME" STREAM_PENDING + "$MessagingIncubatingAttributes.MESSAGING_OPERATION" "publish" + "$MessagingIncubatingAttributes.MESSAGING_CLIENT_ID" { it.startsWith("producer") } + "$MessagingIncubatingAttributes.MESSAGING_DESTINATION_PARTITION_ID" String + "$MessagingIncubatingAttributes.MESSAGING_KAFKA_MESSAGE_OFFSET" 0 + "$MessagingIncubatingAttributes.MESSAGING_KAFKA_MESSAGE_KEY" "10" } } @@ -115,13 +116,13 @@ class KafkaStreamsDefaultTest extends KafkaStreamsBaseTest { kind CONSUMER hasNoParent() attributes { - "$SemanticAttributes.MESSAGING_SYSTEM" "kafka" - "$SemanticAttributes.MESSAGING_DESTINATION_NAME" STREAM_PENDING - "$SemanticAttributes.MESSAGING_OPERATION" "receive" - "$SemanticAttributes.MESSAGING_KAFKA_CLIENT_ID" { it.endsWith("consumer") } + "$MessagingIncubatingAttributes.MESSAGING_SYSTEM" "kafka" + "$MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME" STREAM_PENDING + "$MessagingIncubatingAttributes.MESSAGING_OPERATION" "receive" + "$MessagingIncubatingAttributes.MESSAGING_CLIENT_ID" { it.endsWith("consumer") } + "$MessagingIncubatingAttributes.MESSAGING_BATCH_MESSAGE_COUNT" 1 if (Boolean.getBoolean("testLatestDeps")) { - "$SemanticAttributes.MESSAGING_KAFKA_CONSUMER_GROUP" "test-application" - "$SemanticAttributes.MESSAGING_CONSUMER_ID" { it.startsWith("test-application - ") } + "$MessagingIncubatingAttributes.MESSAGING_KAFKA_CONSUMER_GROUP" "test-application" } } } @@ -132,33 +133,33 @@ class KafkaStreamsDefaultTest extends KafkaStreamsBaseTest { childOf span(0) hasLink(producerPending) attributes { - "$SemanticAttributes.MESSAGING_SYSTEM" "kafka" - "$SemanticAttributes.MESSAGING_DESTINATION_NAME" STREAM_PENDING - "$SemanticAttributes.MESSAGING_OPERATION" "process" - "$SemanticAttributes.MESSAGING_KAFKA_CLIENT_ID" { it.endsWith("consumer") } - "$SemanticAttributes.MESSAGING_MESSAGE_PAYLOAD_SIZE_BYTES" Long - "$SemanticAttributes.MESSAGING_KAFKA_SOURCE_PARTITION" { it >= 0 } - "$SemanticAttributes.MESSAGING_KAFKA_MESSAGE_OFFSET" 0 - "$SemanticAttributes.MESSAGING_KAFKA_MESSAGE_KEY" "10" + "$MessagingIncubatingAttributes.MESSAGING_SYSTEM" "kafka" + "$MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME" STREAM_PENDING + "$MessagingIncubatingAttributes.MESSAGING_OPERATION" "process" + "$MessagingIncubatingAttributes.MESSAGING_CLIENT_ID" { it.endsWith("consumer") } + "$MessagingIncubatingAttributes.MESSAGING_MESSAGE_BODY_SIZE" Long + "$MessagingIncubatingAttributes.MESSAGING_DESTINATION_PARTITION_ID" String + "$MessagingIncubatingAttributes.MESSAGING_KAFKA_MESSAGE_OFFSET" 0 + "$MessagingIncubatingAttributes.MESSAGING_KAFKA_MESSAGE_KEY" "10" "kafka.record.queue_time_ms" { it >= 0 } "asdf" "testing" if (Boolean.getBoolean("testLatestDeps")) { - "$SemanticAttributes.MESSAGING_KAFKA_CONSUMER_GROUP" "test-application" - "$SemanticAttributes.MESSAGING_CONSUMER_ID" { it.startsWith("test-application - ") } + "$MessagingIncubatingAttributes.MESSAGING_KAFKA_CONSUMER_GROUP" "test-application" } } } // kafka-clients PRODUCER span(2) { - name STREAM_PROCESSED + " send" + name STREAM_PROCESSED + " publish" kind PRODUCER childOf span(1) attributes { - "$SemanticAttributes.MESSAGING_SYSTEM" "kafka" - "$SemanticAttributes.MESSAGING_DESTINATION_NAME" STREAM_PROCESSED - "$SemanticAttributes.MESSAGING_KAFKA_CLIENT_ID" { it.endsWith("producer") } - "$SemanticAttributes.MESSAGING_KAFKA_DESTINATION_PARTITION" { it >= 0 } - "$SemanticAttributes.MESSAGING_KAFKA_MESSAGE_OFFSET" 0 + "$MessagingIncubatingAttributes.MESSAGING_SYSTEM" "kafka" + "$MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME" STREAM_PROCESSED + "$MessagingIncubatingAttributes.MESSAGING_OPERATION" "publish" + "$MessagingIncubatingAttributes.MESSAGING_CLIENT_ID" { it.endsWith("producer") } + "$MessagingIncubatingAttributes.MESSAGING_DESTINATION_PARTITION_ID" String + "$MessagingIncubatingAttributes.MESSAGING_KAFKA_MESSAGE_OFFSET" 0 } } @@ -171,13 +172,13 @@ class KafkaStreamsDefaultTest extends KafkaStreamsBaseTest { kind CONSUMER hasNoParent() attributes { - "$SemanticAttributes.MESSAGING_SYSTEM" "kafka" - "$SemanticAttributes.MESSAGING_DESTINATION_NAME" STREAM_PROCESSED - "$SemanticAttributes.MESSAGING_OPERATION" "receive" - "$SemanticAttributes.MESSAGING_KAFKA_CLIENT_ID" { it.startsWith("consumer") } + "$MessagingIncubatingAttributes.MESSAGING_SYSTEM" "kafka" + "$MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME" STREAM_PROCESSED + "$MessagingIncubatingAttributes.MESSAGING_OPERATION" "receive" + "$MessagingIncubatingAttributes.MESSAGING_CLIENT_ID" { it.startsWith("consumer") } + "$MessagingIncubatingAttributes.MESSAGING_BATCH_MESSAGE_COUNT" 1 if (Boolean.getBoolean("testLatestDeps")) { - "$SemanticAttributes.MESSAGING_KAFKA_CONSUMER_GROUP" "test" - "$SemanticAttributes.MESSAGING_CONSUMER_ID" { it.startsWith("test - ") } + "$MessagingIncubatingAttributes.MESSAGING_KAFKA_CONSUMER_GROUP" "test" } } } @@ -188,17 +189,16 @@ class KafkaStreamsDefaultTest extends KafkaStreamsBaseTest { childOf span(0) hasLink producerProcessed attributes { - "$SemanticAttributes.MESSAGING_SYSTEM" "kafka" - "$SemanticAttributes.MESSAGING_DESTINATION_NAME" STREAM_PROCESSED - "$SemanticAttributes.MESSAGING_OPERATION" "process" - "$SemanticAttributes.MESSAGING_KAFKA_CLIENT_ID" { it.startsWith("consumer") } - "$SemanticAttributes.MESSAGING_MESSAGE_PAYLOAD_SIZE_BYTES" Long - "$SemanticAttributes.MESSAGING_KAFKA_SOURCE_PARTITION" { it >= 0 } - "$SemanticAttributes.MESSAGING_KAFKA_MESSAGE_OFFSET" 0 - "$SemanticAttributes.MESSAGING_KAFKA_MESSAGE_KEY" "10" + "$MessagingIncubatingAttributes.MESSAGING_SYSTEM" "kafka" + "$MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME" STREAM_PROCESSED + "$MessagingIncubatingAttributes.MESSAGING_OPERATION" "process" + "$MessagingIncubatingAttributes.MESSAGING_CLIENT_ID" { it.startsWith("consumer") } + "$MessagingIncubatingAttributes.MESSAGING_MESSAGE_BODY_SIZE" Long + "$MessagingIncubatingAttributes.MESSAGING_DESTINATION_PARTITION_ID" String + "$MessagingIncubatingAttributes.MESSAGING_KAFKA_MESSAGE_OFFSET" 0 + "$MessagingIncubatingAttributes.MESSAGING_KAFKA_MESSAGE_KEY" "10" if (Boolean.getBoolean("testLatestDeps")) { - "$SemanticAttributes.MESSAGING_KAFKA_CONSUMER_GROUP" "test" - "$SemanticAttributes.MESSAGING_CONSUMER_ID" { it.startsWith("test - consumer") } + "$MessagingIncubatingAttributes.MESSAGING_KAFKA_CONSUMER_GROUP" "test" } "kafka.record.queue_time_ms" { it >= 0 } "testing" 123 diff --git a/instrumentation/kafka/kafka-streams-0.11/javaagent/src/test/groovy/KafkaStreamsSuppressReceiveSpansTest.groovy b/instrumentation/kafka/kafka-streams-0.11/javaagent/src/test/groovy/KafkaStreamsSuppressReceiveSpansTest.groovy index d686ef0f8c57..386e09b56efb 100644 --- a/instrumentation/kafka/kafka-streams-0.11/javaagent/src/test/groovy/KafkaStreamsSuppressReceiveSpansTest.groovy +++ b/instrumentation/kafka/kafka-streams-0.11/javaagent/src/test/groovy/KafkaStreamsSuppressReceiveSpansTest.groovy @@ -8,7 +8,7 @@ import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator import io.opentelemetry.context.Context import io.opentelemetry.context.propagation.TextMapGetter import io.opentelemetry.sdk.trace.data.SpanData -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes import org.apache.kafka.clients.producer.ProducerRecord import org.apache.kafka.common.header.Headers import org.apache.kafka.common.serialization.Serdes @@ -88,16 +88,17 @@ class KafkaStreamsSuppressReceiveSpansTest extends KafkaStreamsBaseTest { trace(0, 4) { // kafka-clients PRODUCER span(0) { - name STREAM_PENDING + " send" + name STREAM_PENDING + " publish" kind PRODUCER hasNoParent() attributes { - "$SemanticAttributes.MESSAGING_SYSTEM" "kafka" - "$SemanticAttributes.MESSAGING_DESTINATION_NAME" STREAM_PENDING - "$SemanticAttributes.MESSAGING_KAFKA_CLIENT_ID" "producer-1" - "$SemanticAttributes.MESSAGING_KAFKA_DESTINATION_PARTITION" { it >= 0 } - "$SemanticAttributes.MESSAGING_KAFKA_MESSAGE_OFFSET" 0 - "$SemanticAttributes.MESSAGING_KAFKA_MESSAGE_KEY" "10" + "$MessagingIncubatingAttributes.MESSAGING_SYSTEM" "kafka" + "$MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME" STREAM_PENDING + "$MessagingIncubatingAttributes.MESSAGING_OPERATION" "publish" + "$MessagingIncubatingAttributes.MESSAGING_CLIENT_ID" "producer-1" + "$MessagingIncubatingAttributes.MESSAGING_DESTINATION_PARTITION_ID" String + "$MessagingIncubatingAttributes.MESSAGING_KAFKA_MESSAGE_OFFSET" 0 + "$MessagingIncubatingAttributes.MESSAGING_KAFKA_MESSAGE_KEY" "10" } } // kafka-stream CONSUMER @@ -106,19 +107,18 @@ class KafkaStreamsSuppressReceiveSpansTest extends KafkaStreamsBaseTest { kind CONSUMER childOf span(0) attributes { - "$SemanticAttributes.MESSAGING_SYSTEM" "kafka" - "$SemanticAttributes.MESSAGING_DESTINATION_NAME" STREAM_PENDING - "$SemanticAttributes.MESSAGING_OPERATION" "process" - "$SemanticAttributes.MESSAGING_KAFKA_CLIENT_ID" { it.endsWith("consumer") } - "$SemanticAttributes.MESSAGING_MESSAGE_PAYLOAD_SIZE_BYTES" Long - "$SemanticAttributes.MESSAGING_KAFKA_SOURCE_PARTITION" { it >= 0 } - "$SemanticAttributes.MESSAGING_KAFKA_MESSAGE_OFFSET" 0 - "$SemanticAttributes.MESSAGING_KAFKA_MESSAGE_KEY" "10" + "$MessagingIncubatingAttributes.MESSAGING_SYSTEM" "kafka" + "$MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME" STREAM_PENDING + "$MessagingIncubatingAttributes.MESSAGING_OPERATION" "process" + "$MessagingIncubatingAttributes.MESSAGING_CLIENT_ID" { it.endsWith("consumer") } + "$MessagingIncubatingAttributes.MESSAGING_MESSAGE_BODY_SIZE" Long + "$MessagingIncubatingAttributes.MESSAGING_DESTINATION_PARTITION_ID" String + "$MessagingIncubatingAttributes.MESSAGING_KAFKA_MESSAGE_OFFSET" 0 + "$MessagingIncubatingAttributes.MESSAGING_KAFKA_MESSAGE_KEY" "10" "kafka.record.queue_time_ms" { it >= 0 } "asdf" "testing" if (Boolean.getBoolean("testLatestDeps")) { - "$SemanticAttributes.MESSAGING_KAFKA_CONSUMER_GROUP" "test-application" - "$SemanticAttributes.MESSAGING_CONSUMER_ID" { it.startsWith("test-application - ") } + "$MessagingIncubatingAttributes.MESSAGING_KAFKA_CONSUMER_GROUP" "test-application" } } } @@ -127,15 +127,16 @@ class KafkaStreamsSuppressReceiveSpansTest extends KafkaStreamsBaseTest { // kafka-clients PRODUCER span(2) { - name STREAM_PROCESSED + " send" + name STREAM_PROCESSED + " publish" kind PRODUCER childOf span(1) attributes { - "$SemanticAttributes.MESSAGING_SYSTEM" "kafka" - "$SemanticAttributes.MESSAGING_DESTINATION_NAME" STREAM_PROCESSED - "$SemanticAttributes.MESSAGING_KAFKA_CLIENT_ID" String - "$SemanticAttributes.MESSAGING_KAFKA_DESTINATION_PARTITION" { it >= 0 } - "$SemanticAttributes.MESSAGING_KAFKA_MESSAGE_OFFSET" 0 + "$MessagingIncubatingAttributes.MESSAGING_SYSTEM" "kafka" + "$MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME" STREAM_PROCESSED + "$MessagingIncubatingAttributes.MESSAGING_OPERATION" "publish" + "$MessagingIncubatingAttributes.MESSAGING_CLIENT_ID" String + "$MessagingIncubatingAttributes.MESSAGING_DESTINATION_PARTITION_ID" String + "$MessagingIncubatingAttributes.MESSAGING_KAFKA_MESSAGE_OFFSET" 0 } } // kafka-clients CONSUMER process @@ -144,17 +145,16 @@ class KafkaStreamsSuppressReceiveSpansTest extends KafkaStreamsBaseTest { kind CONSUMER childOf span(2) attributes { - "$SemanticAttributes.MESSAGING_SYSTEM" "kafka" - "$SemanticAttributes.MESSAGING_DESTINATION_NAME" STREAM_PROCESSED - "$SemanticAttributes.MESSAGING_OPERATION" "process" - "$SemanticAttributes.MESSAGING_KAFKA_CLIENT_ID" { it.startsWith("consumer") } - "$SemanticAttributes.MESSAGING_MESSAGE_PAYLOAD_SIZE_BYTES" Long - "$SemanticAttributes.MESSAGING_KAFKA_SOURCE_PARTITION" { it >= 0 } - "$SemanticAttributes.MESSAGING_KAFKA_MESSAGE_OFFSET" 0 - "$SemanticAttributes.MESSAGING_KAFKA_MESSAGE_KEY" "10" + "$MessagingIncubatingAttributes.MESSAGING_SYSTEM" "kafka" + "$MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME" STREAM_PROCESSED + "$MessagingIncubatingAttributes.MESSAGING_OPERATION" "process" + "$MessagingIncubatingAttributes.MESSAGING_CLIENT_ID" { it.startsWith("consumer") } + "$MessagingIncubatingAttributes.MESSAGING_MESSAGE_BODY_SIZE" Long + "$MessagingIncubatingAttributes.MESSAGING_DESTINATION_PARTITION_ID" String + "$MessagingIncubatingAttributes.MESSAGING_KAFKA_MESSAGE_OFFSET" 0 + "$MessagingIncubatingAttributes.MESSAGING_KAFKA_MESSAGE_KEY" "10" if (Boolean.getBoolean("testLatestDeps")) { - "$SemanticAttributes.MESSAGING_KAFKA_CONSUMER_GROUP" "test" - "$SemanticAttributes.MESSAGING_CONSUMER_ID" { it.startsWith("test - consumer") } + "$MessagingIncubatingAttributes.MESSAGING_KAFKA_CONSUMER_GROUP" "test" } "kafka.record.queue_time_ms" { it >= 0 } "testing" 123 diff --git a/instrumentation/kotlinx-coroutines/javaagent/gradle.properties b/instrumentation/kotlinx-coroutines/javaagent/gradle.properties deleted file mode 100644 index 0d6aa7b61fbc..000000000000 --- a/instrumentation/kotlinx-coroutines/javaagent/gradle.properties +++ /dev/null @@ -1 +0,0 @@ -kotlin.stdlib.default.dependency=false diff --git a/instrumentation/kotlinx-coroutines/kotlinx-coroutines-1.0/javaagent/build.gradle.kts b/instrumentation/kotlinx-coroutines/kotlinx-coroutines-1.0/javaagent/build.gradle.kts new file mode 100644 index 000000000000..3e1be05c41ff --- /dev/null +++ b/instrumentation/kotlinx-coroutines/kotlinx-coroutines-1.0/javaagent/build.gradle.kts @@ -0,0 +1,54 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + +plugins { + id("org.jetbrains.kotlin.jvm") + id("otel.javaagent-instrumentation") +} + +muzzle { + pass { + group.set("org.jetbrains.kotlinx") + module.set("kotlinx-coroutines-core") + versions.set("[1.0.0,1.3.8)") + extraDependency(project(":instrumentation-annotations")) + extraDependency("io.opentelemetry:opentelemetry-api:1.27.0") + } + // 1.3.9 (and beyond?) have changed how artifact names are resolved due to multiplatform variants + pass { + group.set("org.jetbrains.kotlinx") + module.set("kotlinx-coroutines-core-jvm") + versions.set("[1.3.9,)") + extraDependency(project(":instrumentation-annotations")) + extraDependency("io.opentelemetry:opentelemetry-api:1.27.0") + } +} + +dependencies { + compileOnly("io.opentelemetry:opentelemetry-extension-kotlin") + compileOnly("org.jetbrains.kotlin:kotlin-stdlib-jdk8") + compileOnly(project(":opentelemetry-instrumentation-annotations-shaded-for-instrumenting", configuration = "shadow")) + + implementation("org.ow2.asm:asm-tree") + implementation("org.ow2.asm:asm-util") + implementation(project(":instrumentation:opentelemetry-instrumentation-annotations-1.16:javaagent")) + + testInstrumentation(project(":instrumentation:opentelemetry-extension-kotlin-1.0:javaagent")) + testInstrumentation(project(":instrumentation:reactor:reactor-3.1:javaagent")) + + testImplementation("io.opentelemetry:opentelemetry-extension-kotlin") + testImplementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") + testImplementation(project(":instrumentation:reactor:reactor-3.1:library")) + testImplementation(project(":instrumentation-annotations")) + + testLibrary("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.0.0") + testLibrary("org.jetbrains.kotlinx:kotlinx-coroutines-reactor:1.0.0") + testLibrary("io.vertx:vertx-lang-kotlin-coroutines:3.6.0") +} + +kotlin { + compilerOptions { + jvmTarget.set(JvmTarget.JVM_1_8) + // generate metadata for Java 1.8 reflection on method parameters, used in @WithSpan tests + javaParameters = true + } +} diff --git a/instrumentation/kotlinx-coroutines/kotlinx-coroutines-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kotlinxcoroutines/KotlinCoroutineDispatcherInstrumentation.java b/instrumentation/kotlinx-coroutines/kotlinx-coroutines-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kotlinxcoroutines/KotlinCoroutineDispatcherInstrumentation.java new file mode 100644 index 000000000000..d3bd853428a0 --- /dev/null +++ b/instrumentation/kotlinx-coroutines/kotlinx-coroutines-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kotlinxcoroutines/KotlinCoroutineDispatcherInstrumentation.java @@ -0,0 +1,48 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.kotlinxcoroutines; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.extendsClass; +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.hasClassesNamed; +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 KotlinCoroutineDispatcherInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher classLoaderOptimization() { + return hasClassesNamed("kotlinx.coroutines.CoroutineDispatcher"); + } + + @Override + public ElementMatcher typeMatcher() { + return extendsClass(named("kotlinx.coroutines.CoroutineDispatcher")); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + named("dispatch").and(takesArgument(1, Runnable.class)), + this.getClass().getName() + "$StopContextPropagationAdvice"); + } + + @SuppressWarnings("unused") + public static class StopContextPropagationAdvice { + + @Advice.OnMethodEnter + public static void enter(@Advice.Argument(value = 1, readOnly = false) Runnable runnable) { + if (runnable != null) { + runnable = RunnableWrapper.stopPropagation(runnable); + } + } + } +} diff --git a/instrumentation/kotlinx-coroutines/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kotlinxcoroutines/KotlinCoroutinesInstrumentation.java b/instrumentation/kotlinx-coroutines/kotlinx-coroutines-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kotlinxcoroutines/KotlinCoroutinesInstrumentation.java similarity index 100% rename from instrumentation/kotlinx-coroutines/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kotlinxcoroutines/KotlinCoroutinesInstrumentation.java rename to instrumentation/kotlinx-coroutines/kotlinx-coroutines-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kotlinxcoroutines/KotlinCoroutinesInstrumentation.java diff --git a/instrumentation/kotlinx-coroutines/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kotlinxcoroutines/KotlinCoroutinesInstrumentationHelper.java b/instrumentation/kotlinx-coroutines/kotlinx-coroutines-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kotlinxcoroutines/KotlinCoroutinesInstrumentationHelper.java similarity index 100% rename from instrumentation/kotlinx-coroutines/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kotlinxcoroutines/KotlinCoroutinesInstrumentationHelper.java rename to instrumentation/kotlinx-coroutines/kotlinx-coroutines-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kotlinxcoroutines/KotlinCoroutinesInstrumentationHelper.java diff --git a/instrumentation/kotlinx-coroutines/kotlinx-coroutines-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kotlinxcoroutines/KotlinCoroutinesInstrumentationModule.java b/instrumentation/kotlinx-coroutines/kotlinx-coroutines-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kotlinxcoroutines/KotlinCoroutinesInstrumentationModule.java new file mode 100644 index 000000000000..8ed90c855ccf --- /dev/null +++ b/instrumentation/kotlinx-coroutines/kotlinx-coroutines-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kotlinxcoroutines/KotlinCoroutinesInstrumentationModule.java @@ -0,0 +1,40 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.kotlinxcoroutines; + +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.extension.instrumentation.internal.ExperimentalInstrumentationModule; +import java.util.List; + +@AutoService(InstrumentationModule.class) +public class KotlinCoroutinesInstrumentationModule extends InstrumentationModule + implements ExperimentalInstrumentationModule { + + public KotlinCoroutinesInstrumentationModule() { + super("kotlinx-coroutines"); + } + + @Override + public boolean isHelperClass(String className) { + return className.startsWith("io.opentelemetry.extension.kotlin."); + } + + @Override + public String getModuleGroup() { + // This module uses the api context bridge helpers, therefore must be in the same classloader + return "opentelemetry-api-bridge"; + } + + @Override + public List typeInstrumentations() { + return asList( + new KotlinCoroutinesInstrumentation(), new KotlinCoroutineDispatcherInstrumentation()); + } +} diff --git a/instrumentation/kotlinx-coroutines/kotlinx-coroutines-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kotlinxcoroutines/RunnableWrapper.java b/instrumentation/kotlinx-coroutines/kotlinx-coroutines-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kotlinxcoroutines/RunnableWrapper.java new file mode 100644 index 000000000000..3bdc9b708c54 --- /dev/null +++ b/instrumentation/kotlinx-coroutines/kotlinx-coroutines-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kotlinxcoroutines/RunnableWrapper.java @@ -0,0 +1,22 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.kotlinxcoroutines; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; + +public final class RunnableWrapper { + + public static Runnable stopPropagation(Runnable runnable) { + return () -> { + try (Scope ignored = Context.root().makeCurrent()) { + runnable.run(); + } + }; + } + + private RunnableWrapper() {} +} diff --git a/instrumentation/kotlinx-coroutines/kotlinx-coroutines-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kotlinxcoroutines/instrumentationannotations/AnnotationInstrumentationHelper.java b/instrumentation/kotlinx-coroutines/kotlinx-coroutines-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kotlinxcoroutines/instrumentationannotations/AnnotationInstrumentationHelper.java new file mode 100644 index 000000000000..a0f9c004f8d3 --- /dev/null +++ b/instrumentation/kotlinx-coroutines/kotlinx-coroutines-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kotlinxcoroutines/instrumentationannotations/AnnotationInstrumentationHelper.java @@ -0,0 +1,169 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.kotlinxcoroutines.instrumentationannotations; + +import static io.opentelemetry.javaagent.instrumentation.kotlinxcoroutines.instrumentationannotations.AnnotationSingletons.instrumenter; + +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.util.VirtualField; +import kotlin.coroutines.Continuation; +import kotlin.coroutines.intrinsics.IntrinsicsKt; + +public final class AnnotationInstrumentationHelper { + + private static final VirtualField, Context> contextField = + VirtualField.find(Continuation.class, Context.class); + + public static MethodRequest createMethodRequest( + Class declaringClass, String methodName, String withSpanValue, String spanKindString) { + SpanKind spanKind = SpanKind.INTERNAL; + if (spanKindString != null) { + try { + spanKind = SpanKind.valueOf(spanKindString); + } catch (IllegalArgumentException exception) { + // ignore + } + } + + return MethodRequest.create(declaringClass, methodName, withSpanValue, spanKind); + } + + public static Context enterCoroutine( + int label, Continuation continuation, MethodRequest request) { + // label 0 means that coroutine is started, any other label means that coroutine is resumed + if (label == 0) { + Context context = instrumenter().start(Context.current(), request); + // null continuation means that this method is not going to be resumed, and we don't need to + // store the context + if (continuation != null) { + contextField.set(continuation, context); + } + return context; + } else { + return continuation != null ? contextField.get(continuation) : null; + } + } + + public static Scope openScope(Context context) { + return context != null ? context.makeCurrent() : null; + } + + public static void exitCoroutine( + Object result, + MethodRequest request, + Continuation continuation, + Context context, + Scope scope) { + exitCoroutine(null, result, request, continuation, context, scope); + } + + public static void exitCoroutine( + Throwable error, + Object result, + MethodRequest request, + Continuation continuation, + Context context, + Scope scope) { + if (scope == null) { + return; + } + scope.close(); + + // end the span when this method can not be resumed (coroutine is null) or if it has reached + // final state (returns anything else besides COROUTINE_SUSPENDED) + if (continuation == null || result != IntrinsicsKt.getCOROUTINE_SUSPENDED()) { + instrumenter().end(context, request, null, error); + } + } + + public static void setSpanAttribute(int label, String name, boolean value) { + // only add the attribute when coroutine is started + if (label == 0) { + Span.current().setAttribute(name, value); + } + } + + public static void setSpanAttribute(int label, String name, byte value) { + // only add the attribute when coroutine is started + if (label == 0) { + Span.current().setAttribute(name, value); + } + } + + public static void setSpanAttribute(int label, String name, char value) { + // only add the attribute when coroutine is started + if (label == 0) { + Span.current().setAttribute(name, String.valueOf(value)); + } + } + + public static void setSpanAttribute(int label, String name, double value) { + // only add the attribute when coroutine is started + if (label == 0) { + Span.current().setAttribute(name, value); + } + } + + public static void setSpanAttribute(int label, String name, float value) { + // only add the attribute when coroutine is started + if (label == 0) { + Span.current().setAttribute(name, value); + } + } + + public static void setSpanAttribute(int label, String name, int value) { + // only add the attribute when coroutine is started + if (label == 0) { + Span.current().setAttribute(name, value); + } + } + + public static void setSpanAttribute(int label, String name, long value) { + // only add the attribute when coroutine is started + if (label == 0) { + Span.current().setAttribute(name, value); + } + } + + public static void setSpanAttribute(int label, String name, short value) { + // only add the attribute when coroutine is started + if (label == 0) { + Span.current().setAttribute(name, value); + } + } + + public static void setSpanAttribute(int label, String name, Object value) { + // only add the attribute when coroutine is started + if (label != 0) { + return; + } + if (value instanceof String) { + Span.current().setAttribute(name, (String) value); + } else if (value instanceof Boolean) { + Span.current().setAttribute(name, (Boolean) value); + } else if (value instanceof Byte) { + Span.current().setAttribute(name, (Byte) value); + } else if (value instanceof Character) { + Span.current().setAttribute(name, (Character) value); + } else if (value instanceof Double) { + Span.current().setAttribute(name, (Double) value); + } else if (value instanceof Float) { + Span.current().setAttribute(name, (Float) value); + } else if (value instanceof Integer) { + Span.current().setAttribute(name, (Integer) value); + } else if (value instanceof Long) { + Span.current().setAttribute(name, (Long) value); + } + // TODO: arrays and List not supported see AttributeBindingFactoryTest + } + + public static void init() {} + + private AnnotationInstrumentationHelper() {} +} diff --git a/instrumentation/kotlinx-coroutines/kotlinx-coroutines-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kotlinxcoroutines/instrumentationannotations/AnnotationInstrumentationModule.java b/instrumentation/kotlinx-coroutines/kotlinx-coroutines-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kotlinxcoroutines/instrumentationannotations/AnnotationInstrumentationModule.java new file mode 100644 index 000000000000..a6ce11e4ebe4 --- /dev/null +++ b/instrumentation/kotlinx-coroutines/kotlinx-coroutines-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kotlinxcoroutines/instrumentationannotations/AnnotationInstrumentationModule.java @@ -0,0 +1,52 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.kotlinxcoroutines.instrumentationannotations; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.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; + +/** Instrumentation for methods annotated with {@code WithSpan} annotation. */ +@AutoService(InstrumentationModule.class) +public class AnnotationInstrumentationModule extends InstrumentationModule { + + public AnnotationInstrumentationModule() { + super( + "kotlinx-coroutines-opentelemetry-instrumentation-annotations", + "kotlinx-coroutines", + "opentelemetry-instrumentation-annotations"); + } + + @Override + public boolean isIndyModule() { + // needs helper classes in the same class loader + return false; + } + + @Override + public int order() { + // Run first to ensure other automatic instrumentation is added after and therefore is executed + // earlier in the instrumented method and create the span to attach attributes to. + return -1000; + } + + @Override + public ElementMatcher.Junction classLoaderMatcher() { + return hasClassesNamed( + "application.io.opentelemetry.instrumentation.annotations.WithSpan", + "kotlinx.coroutines.CoroutineContextKt"); + } + + @Override + public List typeInstrumentations() { + return singletonList(new WithSpanInstrumentation()); + } +} diff --git a/instrumentation/kotlinx-coroutines/kotlinx-coroutines-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kotlinxcoroutines/instrumentationannotations/AnnotationSingletons.java b/instrumentation/kotlinx-coroutines/kotlinx-coroutines-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kotlinxcoroutines/instrumentationannotations/AnnotationSingletons.java new file mode 100644 index 000000000000..134b1a3ac309 --- /dev/null +++ b/instrumentation/kotlinx-coroutines/kotlinx-coroutines-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kotlinxcoroutines/instrumentationannotations/AnnotationSingletons.java @@ -0,0 +1,42 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.kotlinxcoroutines.instrumentationannotations; + +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.instrumentation.api.incubator.semconv.code.CodeAttributesExtractor; +import io.opentelemetry.instrumentation.api.incubator.semconv.util.SpanNames; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; + +public final class AnnotationSingletons { + + private static final String INSTRUMENTATION_NAME = "io.opentelemetry.kotlinx-coroutines-1.0"; + + private static final Instrumenter INSTRUMENTER = createInstrumenter(); + + public static Instrumenter instrumenter() { + return INSTRUMENTER; + } + + private static Instrumenter createInstrumenter() { + return Instrumenter.builder( + GlobalOpenTelemetry.get(), + INSTRUMENTATION_NAME, + AnnotationSingletons::spanNameFromMethodRequest) + .addAttributesExtractor( + CodeAttributesExtractor.create(MethodRequestCodeAttributesGetter.INSTANCE)) + .buildInstrumenter(MethodRequest::getSpanKind); + } + + private static String spanNameFromMethodRequest(MethodRequest request) { + String spanName = request.getWithSpanValue(); + if (spanName == null || spanName.isEmpty()) { + spanName = SpanNames.fromMethod(request.getDeclaringClass(), request.getMethodName()); + } + return spanName; + } + + private AnnotationSingletons() {} +} diff --git a/instrumentation/kotlinx-coroutines/kotlinx-coroutines-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kotlinxcoroutines/instrumentationannotations/ExpandFramesClassVisitor.java b/instrumentation/kotlinx-coroutines/kotlinx-coroutines-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kotlinxcoroutines/instrumentationannotations/ExpandFramesClassVisitor.java new file mode 100644 index 000000000000..71a7dcd55c52 --- /dev/null +++ b/instrumentation/kotlinx-coroutines/kotlinx-coroutines-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kotlinxcoroutines/instrumentationannotations/ExpandFramesClassVisitor.java @@ -0,0 +1,148 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.kotlinxcoroutines.instrumentationannotations; + +import io.opentelemetry.javaagent.extension.instrumentation.internal.AsmApi; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; + +/** + * Converts compressed frames (F_FULL, F_SAME etc.) into expanded frames (F_NEW). Using this visitor + * should give the same result as using ClassReader.EXPAND_FRAMES. + */ +class ExpandFramesClassVisitor extends ClassVisitor { + private String className; + + ExpandFramesClassVisitor(ClassVisitor classVisitor) { + super(AsmApi.VERSION, classVisitor); + } + + @Override + public void visit( + int version, + int access, + String name, + String signature, + String superName, + String[] interfaces) { + super.visit(version, access, name, signature, superName, interfaces); + className = name; + } + + @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 ExpandFramesMethodVisitor(mv, className, name, access, descriptor); + } + + private static class ExpandFramesMethodVisitor extends MethodVisitor { + final List currentLocals = new ArrayList<>(); + final List currentStack = new ArrayList<>(); + + ExpandFramesMethodVisitor( + MethodVisitor mv, String className, String methodName, int access, String descriptor) { + super(AsmApi.VERSION, mv); + if ("".equals(methodName)) { + currentLocals.add(Opcodes.UNINITIALIZED_THIS); + } else if (!Modifier.isStatic(access)) { + currentLocals.add(className); + } + for (Type type : Type.getArgumentTypes(descriptor)) { + switch (type.getSort()) { + case Type.BOOLEAN: + case Type.BYTE: + case Type.CHAR: + case Type.INT: + case Type.SHORT: + currentLocals.add(Opcodes.INTEGER); + break; + case Type.DOUBLE: + currentLocals.add(Opcodes.DOUBLE); + break; + case Type.FLOAT: + currentLocals.add(Opcodes.FLOAT); + break; + case Type.LONG: + currentLocals.add(Opcodes.LONG); + break; + case Type.ARRAY: + case Type.OBJECT: + currentLocals.add(type.getInternalName()); + break; + default: + throw new IllegalStateException("Unexpected type " + type.getSort() + " " + type); + } + } + } + + private static void copy(Object[] array, int count, List list) { + list.clear(); + for (int i = 0; i < count; i++) { + list.add(array[i]); + } + } + + @Override + public void visitFrame(int type, int numLocal, Object[] local, int numStack, Object[] stack) { + switch (type) { + // An expanded frame. + case Opcodes.F_NEW: + // A compressed frame with complete frame data. + case Opcodes.F_FULL: + copy(local, numLocal, currentLocals); + copy(stack, numStack, currentStack); + break; + // A compressed frame with exactly the same locals as the previous frame and with an empty + // stack. + case Opcodes.F_SAME: + currentStack.clear(); + break; + // A compressed frame with exactly the same locals as the previous frame and with a single + // value on the stack. + case Opcodes.F_SAME1: + currentStack.clear(); + currentStack.add(stack[0]); + break; + // A compressed frame where locals are the same as the locals in the previous frame, + // except that additional 1-3 locals are defined, and with an empty stack. + case Opcodes.F_APPEND: + currentStack.clear(); + for (int i = 0; i < numLocal; i++) { + currentLocals.add(local[i]); + } + break; + // A compressed frame where locals are the same as the locals in the previous frame, + // except that the last 1-3 locals are absent and with an empty stack. + case Opcodes.F_CHOP: + currentStack.clear(); + for (Iterator iterator = + currentLocals.listIterator(currentLocals.size() - numLocal); + iterator.hasNext(); ) { + iterator.next(); + iterator.remove(); + } + break; + default: + throw new IllegalStateException("Unexpected frame type " + type); + } + + // visit expanded frame + super.visitFrame( + Opcodes.F_NEW, + currentLocals.size(), + currentLocals.toArray(), + currentStack.size(), + currentStack.toArray()); + } + } +} diff --git a/instrumentation/kotlinx-coroutines/kotlinx-coroutines-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kotlinxcoroutines/instrumentationannotations/KotlinCoroutinesIgnoredTypesConfigurer.java b/instrumentation/kotlinx-coroutines/kotlinx-coroutines-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kotlinxcoroutines/instrumentationannotations/KotlinCoroutinesIgnoredTypesConfigurer.java new file mode 100644 index 000000000000..1ee607fb8dc9 --- /dev/null +++ b/instrumentation/kotlinx-coroutines/kotlinx-coroutines-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kotlinxcoroutines/instrumentationannotations/KotlinCoroutinesIgnoredTypesConfigurer.java @@ -0,0 +1,20 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.kotlinxcoroutines.instrumentationannotations; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.ignore.IgnoredTypesBuilder; +import io.opentelemetry.javaagent.extension.ignore.IgnoredTypesConfigurer; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; + +@AutoService(IgnoredTypesConfigurer.class) +public class KotlinCoroutinesIgnoredTypesConfigurer implements IgnoredTypesConfigurer { + + @Override + public void configure(IgnoredTypesBuilder builder, ConfigProperties config) { + builder.allowClass("kotlin.coroutines.jvm.internal.CompletedContinuation"); + } +} diff --git a/instrumentation/kotlinx-coroutines/kotlinx-coroutines-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kotlinxcoroutines/instrumentationannotations/MethodRequest.java b/instrumentation/kotlinx-coroutines/kotlinx-coroutines-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kotlinxcoroutines/instrumentationannotations/MethodRequest.java new file mode 100644 index 000000000000..3f2aedc8eb66 --- /dev/null +++ b/instrumentation/kotlinx-coroutines/kotlinx-coroutines-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kotlinxcoroutines/instrumentationannotations/MethodRequest.java @@ -0,0 +1,44 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.kotlinxcoroutines.instrumentationannotations; + +import io.opentelemetry.api.trace.SpanKind; + +public final class MethodRequest { + private final Class declaringClass; + private final String methodName; + private final String withSpanValue; + private final SpanKind spanKind; + + private MethodRequest( + Class declaringClass, String methodName, String withSpanValue, SpanKind spanKind) { + this.declaringClass = declaringClass; + this.methodName = methodName; + this.withSpanValue = withSpanValue; + this.spanKind = spanKind; + } + + public static MethodRequest create( + Class declaringClass, String methodName, String withSpanValue, SpanKind spanKind) { + return new MethodRequest(declaringClass, methodName, withSpanValue, spanKind); + } + + public Class getDeclaringClass() { + return declaringClass; + } + + public String getMethodName() { + return methodName; + } + + public String getWithSpanValue() { + return withSpanValue; + } + + public SpanKind getSpanKind() { + return spanKind; + } +} diff --git a/instrumentation/kotlinx-coroutines/kotlinx-coroutines-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kotlinxcoroutines/instrumentationannotations/MethodRequestCodeAttributesGetter.java b/instrumentation/kotlinx-coroutines/kotlinx-coroutines-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kotlinxcoroutines/instrumentationannotations/MethodRequestCodeAttributesGetter.java new file mode 100644 index 000000000000..03191da0651c --- /dev/null +++ b/instrumentation/kotlinx-coroutines/kotlinx-coroutines-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kotlinxcoroutines/instrumentationannotations/MethodRequestCodeAttributesGetter.java @@ -0,0 +1,22 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.kotlinxcoroutines.instrumentationannotations; + +import io.opentelemetry.instrumentation.api.incubator.semconv.code.CodeAttributesGetter; + +enum MethodRequestCodeAttributesGetter implements CodeAttributesGetter { + INSTANCE; + + @Override + public Class getCodeClass(MethodRequest methodRequest) { + return methodRequest.getDeclaringClass(); + } + + @Override + public String getMethodName(MethodRequest methodRequest) { + return methodRequest.getMethodName(); + } +} diff --git a/instrumentation/kotlinx-coroutines/kotlinx-coroutines-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kotlinxcoroutines/instrumentationannotations/SpanAttributeUtil.java b/instrumentation/kotlinx-coroutines/kotlinx-coroutines-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kotlinxcoroutines/instrumentationannotations/SpanAttributeUtil.java new file mode 100644 index 000000000000..f67fe3434272 --- /dev/null +++ b/instrumentation/kotlinx-coroutines/kotlinx-coroutines-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kotlinxcoroutines/instrumentationannotations/SpanAttributeUtil.java @@ -0,0 +1,99 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.kotlinxcoroutines.instrumentationannotations; + +import java.util.ArrayList; +import java.util.List; +import org.objectweb.asm.Type; +import org.objectweb.asm.tree.AnnotationNode; +import org.objectweb.asm.tree.MethodNode; +import org.objectweb.asm.tree.ParameterNode; + +class SpanAttributeUtil { + + static class Parameter { + final int var; + final String name; + final Type type; + + Parameter(int var, String name, Type type) { + this.var = var; + this.name = name; + this.type = type; + } + } + + /** + * Collect method parameters with @SpanAttribute annotation. Span attribute is named based on the + * value of the annotation or using the parameter name in the source code, if neither is set then + * the parameter is ignored. + */ + static List collectAnnotatedParameters(MethodNode source) { + List annotatedParameters = new ArrayList<>(); + if (source.visibleParameterAnnotations != null) { + int slot = 1; // this is in slot 0 + Type[] parameterTypes = Type.getArgumentTypes(source.desc); + for (int i = 0; i < parameterTypes.length; i++) { + Type type = parameterTypes[i]; + // if current parameter index is equal or larger than the count of annotated parameters + // we have already checked all the parameters with annotations + if (i >= source.visibleParameterAnnotations.length) { + break; + } + boolean hasSpanAttributeAnnotation = false; + String name = getParameterName(source, i); + List parameterAnnotations = source.visibleParameterAnnotations[i]; + if (parameterAnnotations != null) { + for (AnnotationNode annotationNode : parameterAnnotations) { + if ("Lapplication/io/opentelemetry/instrumentation/annotations/SpanAttribute;" + .equals(annotationNode.desc)) { + // check whether SpanAttribute annotation has a value, if it has use that as + // parameter name + Object attributeValue = getAnnotationValue(annotationNode); + if (attributeValue instanceof String) { + name = (String) attributeValue; + } + + hasSpanAttributeAnnotation = true; + break; + } + } + } + if (hasSpanAttributeAnnotation && name != null) { + annotatedParameters.add(new Parameter(slot, name, type)); + } + slot += type.getSize(); + } + } + + return annotatedParameters; + } + + private static String getParameterName(MethodNode methodNode, int parameter) { + ParameterNode parameterNode = + methodNode.parameters != null && methodNode.parameters.size() > parameter + ? methodNode.parameters.get(parameter) + : null; + return parameterNode != null ? parameterNode.name : null; + } + + private static Object getAnnotationValue(AnnotationNode annotationNode) { + if (annotationNode.values != null && !annotationNode.values.isEmpty()) { + List values = annotationNode.values; + for (int i = 0; i < values.size(); i += 2) { + String attributeName = (String) values.get(i); + Object attributeValue = values.get(i + 1); + if ("value".equals(attributeName)) { + return attributeValue; + } + } + } + + return null; + } + + private SpanAttributeUtil() {} +} diff --git a/instrumentation/kotlinx-coroutines/kotlinx-coroutines-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kotlinxcoroutines/instrumentationannotations/WithSpanInstrumentation.java b/instrumentation/kotlinx-coroutines/kotlinx-coroutines-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kotlinxcoroutines/instrumentationannotations/WithSpanInstrumentation.java new file mode 100644 index 000000000000..e98a11fef3fe --- /dev/null +++ b/instrumentation/kotlinx-coroutines/kotlinx-coroutines-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kotlinxcoroutines/instrumentationannotations/WithSpanInstrumentation.java @@ -0,0 +1,523 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.kotlinxcoroutines.instrumentationannotations; + +import static io.opentelemetry.javaagent.instrumentation.instrumentationannotations.KotlinCoroutineUtil.isKotlinSuspendMethod; +import static net.bytebuddy.matcher.ElementMatchers.declaresMethod; +import static net.bytebuddy.matcher.ElementMatchers.isAnnotatedWith; +import static net.bytebuddy.matcher.ElementMatchers.nameStartsWith; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.none; +import static net.bytebuddy.matcher.ElementMatchers.not; + +import com.google.errorprone.annotations.CanIgnoreReturnValue; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.bootstrap.internal.AgentInstrumentationConfig; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.extension.instrumentation.internal.AsmApi; +import io.opentelemetry.javaagent.instrumentation.instrumentationannotations.AnnotationExcludedMethods; +import io.opentelemetry.javaagent.instrumentation.kotlinxcoroutines.instrumentationannotations.SpanAttributeUtil.Parameter; +import java.util.Arrays; +import java.util.List; +import kotlin.coroutines.Continuation; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.asm.AsmVisitorWrapper; +import net.bytebuddy.description.annotation.AnnotationSource; +import net.bytebuddy.description.field.FieldDescription; +import net.bytebuddy.description.field.FieldList; +import net.bytebuddy.description.method.MethodDescription; +import net.bytebuddy.description.method.MethodList; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.implementation.Implementation; +import net.bytebuddy.matcher.ElementMatcher; +import net.bytebuddy.pool.TypePool; +import org.objectweb.asm.AnnotationVisitor; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.ClassWriter; +import org.objectweb.asm.Label; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; +import org.objectweb.asm.commons.GeneratorAdapter; +import org.objectweb.asm.tree.AbstractInsnNode; +import org.objectweb.asm.tree.AnnotationNode; +import org.objectweb.asm.tree.FieldInsnNode; +import org.objectweb.asm.tree.MethodNode; +import org.objectweb.asm.tree.VarInsnNode; +import org.objectweb.asm.util.CheckClassAdapter; + +class WithSpanInstrumentation implements TypeInstrumentation { + // whether to check the transformed bytecode with asm CheckClassAdapter + private static final boolean CHECK_CLASS = + AgentInstrumentationConfig.get() + .getBoolean( + "otel.instrumentation.kotlinx-coroutines.check-class", + AgentInstrumentationConfig.get().getBoolean("otel.javaagent.debug", false)); + + 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.instrumentation.annotations.WithSpan")); + excludedMethodsMatcher = AnnotationExcludedMethods.configureExcludedMethods(); + } + + @Override + public ElementMatcher typeMatcher() { + return not(nameStartsWith("kotlin.coroutines.")) + .and( + declaresMethod( + annotatedMethodMatcher + .and(isKotlinSuspendMethod()) + .and(not(excludedMethodsMatcher)))); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + none(), WithSpanInstrumentation.class.getName() + "$InitAdvice"); + + transformer.applyTransformer( + (builder, typeDescription, classLoader, javaModule, protectionDomain) -> + builder.visit( + new AsmVisitorWrapper() { + @Override + public int mergeWriter(int flags) { + return flags | ClassWriter.COMPUTE_MAXS; + } + + @Override + @CanIgnoreReturnValue + 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) { + if (CHECK_CLASS) { + classVisitor = new CheckClassAdapter(classVisitor); + } + // we are using a visitor that converts compressed frames into expanded frames + // because WithSpanClassVisitor uses GeneratorAdapter for adding new local + // variables that requires expanded frames. We are not using + // ClassReader.EXPAND_FRAMES because ExceptionHandlers class generates + // compressed F_SAME frame that we can't easily replace with an expanded frame + // because we don't know what locals are available at that point. + return new ExpandFramesClassVisitor(new WithSpanClassVisitor(classVisitor)); + } + })); + } + + @SuppressWarnings("unused") + public static class InitAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter() { + // this advice is here only to get AnnotationInstrumentationHelper injected + AnnotationInstrumentationHelper.init(); + } + } + + private static class WithSpanClassVisitor extends ClassVisitor { + String className; + + WithSpanClassVisitor(ClassVisitor cv) { + super(AsmApi.VERSION, cv); + } + + @Override + public void visit( + int version, + int access, + String name, + String signature, + String superName, + String[] interfaces) { + super.visit(version, access, name, signature, superName, interfaces); + className = name; + } + + @Override + public MethodVisitor visitMethod( + int access, String name, String descriptor, String signature, String[] exceptions) { + MethodVisitor target = super.visitMethod(access, name, descriptor, signature, exceptions); + // firstly check whether this method could be a suspend method + // kotlin suspend methods take kotlin.coroutines.Continuation as last argument and return + // java.lang.Object + Type[] argumentTypes = Type.getArgumentTypes(descriptor); + if (argumentTypes.length > 0 + && "kotlin/coroutines/Continuation" + .equals(argumentTypes[argumentTypes.length - 1].getInternalName()) + && "java/lang/Object".equals(Type.getReturnType(descriptor).getInternalName())) { + // store method in MethodNode, so we could test whether it has the WithSpan annotation and + // depending on that either instrument it or leave it as it is + return new MethodNode(api, access, name, descriptor, signature, exceptions) { + @Override + public void visitEnd() { + super.visitEnd(); + + MethodVisitor mv = target; + if (hasWithSpanAnnotation(this)) { + mv = instrument(mv, this, className); + } + this.accept(mv); + } + }; + } + + return target; + } + + private static boolean hasAnnotation(List annotations, String annotationDesc) { + if (annotations != null) { + for (AnnotationNode annotationNode : annotations) { + if (annotationDesc.equals(annotationNode.desc)) { + return true; + } + } + } + return false; + } + + private static boolean hasWithSpanAnnotation(MethodNode methodNode) { + return hasAnnotation( + methodNode.visibleAnnotations, + "Lapplication/io/opentelemetry/instrumentation/annotations/WithSpan;"); + } + + private static MethodVisitor instrument( + MethodVisitor target, MethodNode source, String className) { + // collect method arguments with @SpanAttribute annotation + List annotatedParameters = SpanAttributeUtil.collectAnnotatedParameters(source); + + String methodName = source.name; + MethodNode methodNode = + new MethodNode( + source.access, + source.name, + source.desc, + source.signature, + source.exceptions.toArray(new String[0])); + GeneratorAdapter generatorAdapter = + new GeneratorAdapter( + AsmApi.VERSION, methodNode, source.access, source.name, source.desc) { + int requestLocal; + int ourContinuationLocal; + int contextLocal; + int scopeLocal; + int lastLocal; + + final Label start = new Label(); + final Label handler = new Label(); + + String withSpanValue = null; + String spanKind = null; + + @Override + public void visitCode() { + super.visitCode(); + // add our local variables after method arguments, this will shift rest of the locals + requestLocal = newLocal(Type.getType(MethodRequest.class)); + ourContinuationLocal = newLocal(Type.getType(Continuation.class)); + contextLocal = newLocal(Type.getType(Context.class)); + scopeLocal = newLocal(Type.getType(Scope.class)); + // set lastLocal to the last local we added + lastLocal = scopeLocal; + + visitLabel(start); + } + + @Override + public void visitMaxs(int maxStack, int maxLocals) { + visitLabel(handler); + visitTryCatchBlock(start, handler, handler, null); + super.visitMaxs(maxStack, maxLocals); + } + + @Override + public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) { + AnnotationVisitor annotationVisitor = super.visitAnnotation(descriptor, visible); + // remember value and kind from the @WithSpan annotation + if ("Lapplication/io/opentelemetry/instrumentation/annotations/WithSpan;" + .equals(descriptor)) { + return new AnnotationVisitor(api, annotationVisitor) { + @Override + public void visit(String name, Object value) { + if ("value".equals(name) && value instanceof String) { + withSpanValue = (String) value; + } + super.visit(name, value); + } + + @Override + public void visitEnum(String name, String descriptor, String value) { + if ("kind".equals(name) + && "Lapplication/io/opentelemetry/api/trace/SpanKind;".equals(descriptor)) { + spanKind = value; + } + super.visitEnum(name, descriptor, value); + } + }; + } + return annotationVisitor; + } + + @Override + public void visitEnd() { + super.visitEnd(); + + // If a suspend method does not contain any blocking operations or has no code after + // the blocking operation it gets compiled to a regular method that we instrument the + // same way as the regular @WithSpan handling does. We create the span at the start of + // the method and end it in before every return instruction and in exception handler. + // If a suspend method has a blocking operation and code that needs to be executed + // after it, we start the span only when the coroutine was started, on resume we just + // activate the scope. We end the span when coroutine completes, otherwise we only + // close the scope. + // First we'll search for a bytecode sequence that looks like + // 64: aload 6 + // 66: getfield #444 // Field + // io/opentelemetry/javaagent/instrumentation/kotlinxcoroutines/KotlinCoroutinesInstrumentationTest$b2$1.label:I + // 69: tableswitch { // 0 to 1 + // 0: 92 + // 1: 181 + // default: 210 + // We are interested in the continuation local (here slot 6) and the value of the + // label field. To get the value of the label we'll insert our code between the + // getfield and tableswitch instructions. + int continuationLocal = -1; + AbstractInsnNode insertAfterInsn = null; + for (int i = 1; i < methodNode.instructions.size() - 1; i++) { + AbstractInsnNode instruction = methodNode.instructions.get(i); + if (instruction.getOpcode() == Opcodes.GETFIELD + && "label".equals(((FieldInsnNode) instruction).name) + && "I".equals(((FieldInsnNode) instruction).desc)) { + if (methodNode.instructions.get(i + 1).getOpcode() != Opcodes.TABLESWITCH) { + continue; + } + if (methodNode.instructions.get(i - 1).getOpcode() != Opcodes.ALOAD) { + continue; + } + insertAfterInsn = instruction; + continuationLocal = ((VarInsnNode) methodNode.instructions.get(i - 1)).var; + break; + } + } + + boolean hasBlockingOperation = insertAfterInsn != null && continuationLocal != -1; + + // initialize our local variables, start span and open scope + { + MethodNode temp = new MethodNode(); + // insert + // request = + // AnnotationInstrumentationHelper.createMethodRequest(InstrumentedClass.class, + // instrumentedMethodName, withSpanValue, withSpanKind) + // context = AnnotationInstrumentationHelper.enterCoroutine(label, continuation, + // request) + // scope = AnnotationInstrumentationHelper.openScope(context) + if (hasBlockingOperation) { + // value of label is on stack + // label is used in call to enterCoroutine and later in @SpanAttribute handling + temp.visitInsn(Opcodes.DUP); + temp.visitInsn(Opcodes.DUP); + temp.visitVarInsn(Opcodes.ALOAD, continuationLocal); + temp.visitInsn(Opcodes.DUP); + temp.visitVarInsn(Opcodes.ASTORE, ourContinuationLocal); + } else { + // nothing on stack, we are inserting code at the start of the method + // we'll use 0 for label and null for continuation object + temp.visitInsn(Opcodes.ICONST_0); + temp.visitInsn(Opcodes.ICONST_0); + temp.visitInsn(Opcodes.ACONST_NULL); + temp.visitInsn(Opcodes.DUP); + temp.visitVarInsn(Opcodes.ASTORE, ourContinuationLocal); + } + temp.visitLdcInsn(Type.getObjectType(className)); + temp.visitLdcInsn(methodName); + if (withSpanValue != null) { + temp.visitLdcInsn(withSpanValue); + } else { + temp.visitInsn(Opcodes.ACONST_NULL); + } + if (spanKind != null) { + temp.visitLdcInsn(spanKind); + } else { + temp.visitInsn(Opcodes.ACONST_NULL); + } + temp.visitMethodInsn( + Opcodes.INVOKESTATIC, + Type.getInternalName(AnnotationInstrumentationHelper.class), + "createMethodRequest", + "(Ljava/lang/Class;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)" + + Type.getDescriptor(MethodRequest.class), + false); + temp.visitInsn(Opcodes.DUP); + temp.visitVarInsn(Opcodes.ASTORE, requestLocal); + temp.visitMethodInsn( + Opcodes.INVOKESTATIC, + Type.getInternalName(AnnotationInstrumentationHelper.class), + "enterCoroutine", + "(ILkotlin/coroutines/Continuation;" + + Type.getDescriptor(MethodRequest.class) + + ")" + + Type.getDescriptor(Context.class), + false); + temp.visitInsn(Opcodes.DUP); + temp.visitVarInsn(Opcodes.ASTORE, contextLocal); + temp.visitMethodInsn( + Opcodes.INVOKESTATIC, + Type.getInternalName(AnnotationInstrumentationHelper.class), + "openScope", + "(" + Type.getDescriptor(Context.class) + ")" + Type.getDescriptor(Scope.class), + false); + temp.visitVarInsn(Opcodes.ASTORE, scopeLocal); + // @SpanAttribute handling + for (Parameter parameter : annotatedParameters) { + // label on stack, make a copy + temp.visitInsn(Opcodes.DUP); + temp.visitLdcInsn(parameter.name); + temp.visitVarInsn(parameter.type.getOpcode(Opcodes.ILOAD), parameter.var); + boolean primitive = + parameter.type.getSort() != Type.ARRAY + && parameter.type.getSort() != Type.OBJECT; + temp.visitMethodInsn( + Opcodes.INVOKESTATIC, + Type.getInternalName(AnnotationInstrumentationHelper.class), + "setSpanAttribute", + "(ILjava/lang/String;" + + (primitive ? parameter.type.getDescriptor() : "Ljava/lang/Object;") + + ")V", + false); + } + // pop label + temp.visitInsn(Opcodes.POP); + if (hasBlockingOperation) { + methodNode.instructions.insert(insertAfterInsn, temp.instructions); + } else { + methodNode.instructions.insertBefore( + methodNode.instructions.get(0), temp.instructions); + } + } + + // insert at the start of the method + // null the local variables we added + // this is needed because jvm requires that a value needs to be assigned to the local + // before it is used, we need to initialize the locals that we use in the exception + // handler + // if the previous block was added at the start of the method this nulling step isn't + // necessary + if (hasBlockingOperation) { + MethodNode temp = new MethodNode(); + temp.visitInsn(Opcodes.ACONST_NULL); + temp.visitVarInsn(Opcodes.ASTORE, requestLocal); + temp.visitInsn(Opcodes.ACONST_NULL); + temp.visitVarInsn(Opcodes.ASTORE, ourContinuationLocal); + temp.visitInsn(Opcodes.ACONST_NULL); + temp.visitVarInsn(Opcodes.ASTORE, contextLocal); + temp.visitInsn(Opcodes.ACONST_NULL); + temp.visitVarInsn(Opcodes.ASTORE, scopeLocal); + + methodNode.instructions.insertBefore( + methodNode.instructions.get(0), temp.instructions); + } + + // insert exception handler code, this exception handler will catch Throwable + { + MethodNode temp = new MethodNode(); + // lastLocal is the last local we added before the start of try block + int numLocals = lastLocal + 1; + Object[] locals = new Object[numLocals]; + // in this handler we are using only the locals we added, we don't care about method + // arguments and this, so we don't list them in the stack frame + Arrays.fill(locals, Opcodes.TOP); + locals[requestLocal] = Type.getInternalName(MethodRequest.class); + locals[ourContinuationLocal] = Type.getInternalName(Continuation.class); + locals[contextLocal] = Type.getInternalName(Context.class); + locals[scopeLocal] = Type.getInternalName(Scope.class); + + temp.visitFrame( + Opcodes.F_NEW, numLocals, locals, 1, new Object[] {"java/lang/Throwable"}); + // we have throwable on stack + // insert AnnotationInstrumentationHelper.exitCoroutine(exception, null, request, + // context, scope) + // that will close the scope and end span + temp.visitInsn(Opcodes.DUP); + temp.visitInsn(Opcodes.ACONST_NULL); + temp.visitVarInsn(Opcodes.ALOAD, requestLocal); + temp.visitVarInsn(Opcodes.ALOAD, ourContinuationLocal); + temp.visitVarInsn(Opcodes.ALOAD, contextLocal); + temp.visitVarInsn(Opcodes.ALOAD, scopeLocal); + temp.visitMethodInsn( + Opcodes.INVOKESTATIC, + Type.getInternalName(AnnotationInstrumentationHelper.class), + "exitCoroutine", + "(Ljava/lang/Throwable;Ljava/lang/Object;" + + Type.getDescriptor(MethodRequest.class) + + Type.getDescriptor(Continuation.class) + + Type.getDescriptor(Context.class) + + Type.getDescriptor(Scope.class) + + ")V", + false); + + // rethrow the exception + temp.visitInsn(Opcodes.ATHROW); + + methodNode.instructions.add(temp.instructions); + } + + // insert code before each return instruction + // iterating instructions in reverse order to avoid having to deal with the + // instructions that we just added + for (int i = methodNode.instructions.size() - 1; i >= 0; i--) { + AbstractInsnNode instruction = methodNode.instructions.get(i); + // this method returns Object, so we don't need to handle other return instructions + if (instruction.getOpcode() == Opcodes.ARETURN) { + MethodNode temp = new MethodNode(); + // we have return value on stack + // insert AnnotationInstrumentationHelper.exitCoroutine(returnValue, request, + // context, scope) + // that will close the scope and end span if needed + temp.visitInsn(Opcodes.DUP); + temp.visitVarInsn(Opcodes.ALOAD, requestLocal); + temp.visitVarInsn(Opcodes.ALOAD, ourContinuationLocal); + temp.visitVarInsn(Opcodes.ALOAD, contextLocal); + temp.visitVarInsn(Opcodes.ALOAD, scopeLocal); + temp.visitMethodInsn( + Opcodes.INVOKESTATIC, + Type.getInternalName(AnnotationInstrumentationHelper.class), + "exitCoroutine", + "(Ljava/lang/Object;" + + Type.getDescriptor(MethodRequest.class) + + Type.getDescriptor(Continuation.class) + + Type.getDescriptor(Context.class) + + Type.getDescriptor(Scope.class) + + ")V", + false); + methodNode.instructions.insertBefore(instruction, temp.instructions); + } + } + + methodNode.accept(target); + } + }; + + return generatorAdapter; + } + } +} diff --git a/instrumentation/kotlinx-coroutines/kotlinx-coroutines-1.0/javaagent/src/test/kotlin/io/opentelemetry/javaagent/instrumentation/kotlinxcoroutines/ClazzWithDefaultConstructorArguments.kt b/instrumentation/kotlinx-coroutines/kotlinx-coroutines-1.0/javaagent/src/test/kotlin/io/opentelemetry/javaagent/instrumentation/kotlinxcoroutines/ClazzWithDefaultConstructorArguments.kt new file mode 100644 index 000000000000..5be13c6958a9 --- /dev/null +++ b/instrumentation/kotlinx-coroutines/kotlinx-coroutines-1.0/javaagent/src/test/kotlin/io/opentelemetry/javaagent/instrumentation/kotlinxcoroutines/ClazzWithDefaultConstructorArguments.kt @@ -0,0 +1,18 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.kotlinxcoroutines + +import io.opentelemetry.instrumentation.annotations.WithSpan +import kotlinx.coroutines.delay + +class ClazzWithDefaultConstructorArguments(val name: String = "Ktor") { + + @WithSpan + suspend fun sayHello(): String { + delay(10) + return "Hello World $name from ${ClazzWithDefaultConstructorArguments::class.simpleName}!" + } +} diff --git a/instrumentation/kotlinx-coroutines/javaagent/src/test/kotlin/io/opentelemetry/javaagent/instrumentation/kotlinxcoroutines/KotlinCoroutinesInstrumentationTest.kt b/instrumentation/kotlinx-coroutines/kotlinx-coroutines-1.0/javaagent/src/test/kotlin/io/opentelemetry/javaagent/instrumentation/kotlinxcoroutines/KotlinCoroutinesInstrumentationTest.kt similarity index 73% rename from instrumentation/kotlinx-coroutines/javaagent/src/test/kotlin/io/opentelemetry/javaagent/instrumentation/kotlinxcoroutines/KotlinCoroutinesInstrumentationTest.kt rename to instrumentation/kotlinx-coroutines/kotlinx-coroutines-1.0/javaagent/src/test/kotlin/io/opentelemetry/javaagent/instrumentation/kotlinxcoroutines/KotlinCoroutinesInstrumentationTest.kt index 8a0ae43cb8ea..9feeb198cefa 100644 --- a/instrumentation/kotlinx-coroutines/javaagent/src/test/kotlin/io/opentelemetry/javaagent/instrumentation/kotlinxcoroutines/KotlinCoroutinesInstrumentationTest.kt +++ b/instrumentation/kotlinx-coroutines/kotlinx-coroutines-1.0/javaagent/src/test/kotlin/io/opentelemetry/javaagent/instrumentation/kotlinxcoroutines/KotlinCoroutinesInstrumentationTest.kt @@ -5,15 +5,22 @@ package io.opentelemetry.javaagent.instrumentation.kotlinxcoroutines +import io.opentelemetry.api.common.AttributeKey +import io.opentelemetry.api.trace.SpanKind import io.opentelemetry.context.Context import io.opentelemetry.context.ContextKey import io.opentelemetry.context.Scope import io.opentelemetry.extension.kotlin.asContextElement import io.opentelemetry.extension.kotlin.getOpenTelemetryContext -import io.opentelemetry.instrumentation.reactor.v3_1.ContextPropagationOperator +import io.opentelemetry.instrumentation.annotations.SpanAttribute +import io.opentelemetry.instrumentation.annotations.WithSpan import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension import io.opentelemetry.instrumentation.testing.util.TelemetryDataUtil.orderByRootSpanName +import io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo import io.opentelemetry.sdk.testing.assertj.TraceAssert +import io.opentelemetry.semconv.incubating.CodeIncubatingAttributes +import io.vertx.core.Vertx +import io.vertx.kotlin.coroutines.dispatcher import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope @@ -25,16 +32,9 @@ import kotlinx.coroutines.ThreadContextElement import kotlinx.coroutines.asCoroutineDispatcher 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.flow.onEach import kotlinx.coroutines.launch import kotlinx.coroutines.reactive.awaitSingle -import kotlinx.coroutines.reactive.collect -import kotlinx.coroutines.reactor.ReactorContext -import kotlinx.coroutines.reactor.flux import kotlinx.coroutines.reactor.mono import kotlinx.coroutines.runBlocking import kotlinx.coroutines.selects.select @@ -43,6 +43,7 @@ import kotlinx.coroutines.withTimeout import kotlinx.coroutines.yield import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.AfterAll +import org.junit.jupiter.api.Assumptions import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance import org.junit.jupiter.api.extension.ExtensionContext @@ -65,71 +66,22 @@ class KotlinCoroutinesInstrumentationTest { companion object { val threadPool = Executors.newFixedThreadPool(2) val singleThread = Executors.newSingleThreadExecutor() + val vertx = Vertx.vertx() + + @JvmStatic + @RegisterExtension + val testing = AgentInstrumentationExtension.create() } @AfterAll fun shutdown() { threadPool.shutdown() singleThread.shutdown() + vertx.close() } - @RegisterExtension - val testing = AgentInstrumentationExtension.create() - val tracer = testing.openTelemetry.getTracer("test") - @ParameterizedTest - @ArgumentsSource(DispatchersSource::class) - fun `traced across channels`(dispatcher: DispatcherWrapper) { - runTest(dispatcher) { - val producer = produce { - repeat(3) { - tracedChild("produce_$it") - send(it) - } - } - - producer.consumeAsFlow().onEach { - tracedChild("consume_$it") - }.collect() - } - - testing.waitAndAssertTraces( - { trace -> - trace.hasSpansSatisfyingExactlyInAnyOrder( - { - it.hasName("parent") - .hasNoParent() - }, - { - it.hasName("produce_0") - .hasParent(trace.getSpan(0)) - }, - { - it.hasName("consume_0") - .hasParent(trace.getSpan(0)) - }, - { - it.hasName("produce_1") - .hasParent(trace.getSpan(0)) - }, - { - it.hasName("consume_1") - .hasParent(trace.getSpan(0)) - }, - { - it.hasName("produce_2") - .hasParent(trace.getSpan(0)) - }, - { - it.hasName("consume_2") - .hasParent(trace.getSpan(0)) - }, - ) - }, - ) - } - @ParameterizedTest @ArgumentsSource(DispatchersSource::class) fun `cancellation prevents trace`(dispatcher: DispatcherWrapper) { @@ -382,22 +334,22 @@ class KotlinCoroutinesInstrumentationTest { ) } + private val animalKey: ContextKey = ContextKey.named("animal") + @ParameterizedTest @ArgumentsSource(DispatchersSource::class) - fun `traced mono with context propagation operator`(dispatcherWrapper: DispatcherWrapper) { - runTest(dispatcherWrapper) { - val currentContext = Context.current() - // clear current context to ensure that ContextPropagationOperator is used for context propagation - withContext(Context.root().asContextElement()) { - val mono = mono(dispatcherWrapper.dispatcher) { - // extract context from reactor and propagate it into coroutine - val reactorContext = coroutineContext[ReactorContext.Key]?.context - val otelContext = ContextPropagationOperator.getOpenTelemetryContext(reactorContext, Context.current()) - withContext(otelContext.asContextElement()) { - tracedChild("child") - } + fun `context contains expected value`(dispatcher: DispatcherWrapper) { + runTest(dispatcher) { + val context1 = Context.current().with(animalKey, "cat") + runBlocking(context1.asContextElement()) { + assertThat(Context.current().get(animalKey)).isEqualTo("cat") + assertThat(coroutineContext.getOpenTelemetryContext().get(animalKey)).isEqualTo("cat") + tracedChild("nested1") + withContext(context1.with(animalKey, "dog").asContextElement()) { + assertThat(Context.current().get(animalKey)).isEqualTo("dog") + assertThat(coroutineContext.getOpenTelemetryContext().get(animalKey)).isEqualTo("dog") + tracedChild("nested2") } - ContextPropagationOperator.runWithContext(mono, currentContext).awaitSingle() } } @@ -409,7 +361,11 @@ class KotlinCoroutinesInstrumentationTest { .hasNoParent() }, { - it.hasName("child") + it.hasName("nested1") + .hasParent(trace.getSpan(0)) + }, + { + it.hasName("nested2") .hasParent(trace.getSpan(0)) }, ) @@ -417,79 +373,87 @@ class KotlinCoroutinesInstrumentationTest { ) } - @ParameterizedTest - @ArgumentsSource(DispatchersSource::class) - fun `traced flux`(dispatcherWrapper: DispatcherWrapper) { - runTest(dispatcherWrapper) { - flux(dispatcherWrapper.dispatcher) { - repeat(3) { - tracedChild("child_$it") - send(it) - } - }.collect { - } + @Test + fun `test WithSpan annotation`() { + runBlocking { + annotated1() } testing.waitAndAssertTraces( { trace -> trace.hasSpansSatisfyingExactly( { - it.hasName("parent") + it.hasName("a1") .hasNoParent() + .hasAttributesSatisfyingExactly( + equalTo(CodeIncubatingAttributes.CODE_NAMESPACE, this.javaClass.name), + equalTo(CodeIncubatingAttributes.CODE_FUNCTION, "annotated1") + ) }, { - it.hasName("child_0") + it.hasName("KotlinCoroutinesInstrumentationTest.annotated2") .hasParent(trace.getSpan(0)) - }, - { - it.hasName("child_1") - .hasParent(trace.getSpan(0)) - }, - { - it.hasName("child_2") - .hasParent(trace.getSpan(0)) - }, + .hasAttributesSatisfyingExactly( + equalTo(CodeIncubatingAttributes.CODE_NAMESPACE, this.javaClass.name), + equalTo(CodeIncubatingAttributes.CODE_FUNCTION, "annotated2"), + equalTo(AttributeKey.longKey("byteValue"), 1), + equalTo(AttributeKey.longKey("intValue"), 4), + equalTo(AttributeKey.longKey("longValue"), 5), + equalTo(AttributeKey.longKey("shortValue"), 6), + equalTo(AttributeKey.doubleKey("doubleValue"), 2.0), + equalTo(AttributeKey.doubleKey("floatValue"), 3.0), + equalTo(AttributeKey.booleanKey("booleanValue"), true), + equalTo(AttributeKey.stringKey("charValue"), "a"), + equalTo(AttributeKey.stringKey("stringValue"), "test") + ) + } ) - }, + } ) } - private val ANIMAL: ContextKey = ContextKey.named("animal") + @WithSpan(value = "a1", kind = SpanKind.CLIENT) + private suspend fun annotated1() { + delay(10) + annotated2(1, true, 'a', 2.0, 3.0f, 4, 5, 6, "test") + } - @ParameterizedTest - @ArgumentsSource(DispatchersSource::class) - fun `context contains expected value`(dispatcher: DispatcherWrapper) { - runTest(dispatcher) { - val context1 = Context.current().with(ANIMAL, "cat") - runBlocking(context1.asContextElement()) { - assertThat(Context.current().get(ANIMAL)).isEqualTo("cat") - assertThat(coroutineContext.getOpenTelemetryContext().get(ANIMAL)).isEqualTo("cat") - tracedChild("nested1") - withContext(context1.with(ANIMAL, "dog").asContextElement()) { - assertThat(Context.current().get(ANIMAL)).isEqualTo("dog") - assertThat(coroutineContext.getOpenTelemetryContext().get(ANIMAL)).isEqualTo("dog") - tracedChild("nested2") - } - } + @WithSpan + private suspend fun annotated2( + @SpanAttribute byteValue: Byte, + @SpanAttribute booleanValue: Boolean, + @SpanAttribute charValue: Char, + @SpanAttribute doubleValue: Double, + @SpanAttribute floatValue: Float, + @SpanAttribute intValue: Int, + @SpanAttribute longValue: Long, + @SpanAttribute shortValue: Short, + @SpanAttribute("stringValue") s: String + ) { + delay(10) + } + + // regression test for https://github.com/open-telemetry/opentelemetry-java-instrumentation/issues/9312 + @Test + fun `test class with default constructor argument`() { + runBlocking { + val classDefaultConstructorArguments = ClazzWithDefaultConstructorArguments() + classDefaultConstructorArguments.sayHello() } testing.waitAndAssertTraces( { trace -> trace.hasSpansSatisfyingExactly( { - it.hasName("parent") + it.hasName("ClazzWithDefaultConstructorArguments.sayHello") .hasNoParent() - }, - { - it.hasName("nested1") - .hasParent(trace.getSpan(0)) - }, - { - it.hasName("nested2") - .hasParent(trace.getSpan(0)) - }, + .hasAttributesSatisfyingExactly( + equalTo(CodeIncubatingAttributes.CODE_NAMESPACE, ClazzWithDefaultConstructorArguments::class.qualifiedName), + equalTo(CodeIncubatingAttributes.CODE_FUNCTION, "sayHello") + ) + } ) - }, + } ) } @@ -551,16 +515,16 @@ class KotlinCoroutinesInstrumentationTest { } class DispatchersSource : ArgumentsProvider { - override fun provideArguments(context: ExtensionContext?): Stream = - Stream.of( - // Wrap dispatchers since it seems that ParameterizedTest tries to automatically close - // Closeable arguments with no way to avoid it. - arguments(DispatcherWrapper(Dispatchers.Default)), - arguments(DispatcherWrapper(Dispatchers.IO)), - arguments(DispatcherWrapper(Dispatchers.Unconfined)), - arguments(DispatcherWrapper(threadPool.asCoroutineDispatcher())), - arguments(DispatcherWrapper(singleThread.asCoroutineDispatcher())), - ) + override fun provideArguments(context: ExtensionContext?): Stream = Stream.of( + // Wrap dispatchers since it seems that ParameterizedTest tries to automatically close + // Closeable arguments with no way to avoid it. + arguments(DispatcherWrapper(Dispatchers.Default)), + arguments(DispatcherWrapper(Dispatchers.IO)), + arguments(DispatcherWrapper(Dispatchers.Unconfined)), + arguments(DispatcherWrapper(threadPool.asCoroutineDispatcher())), + arguments(DispatcherWrapper(singleThread.asCoroutineDispatcher())), + arguments(DispatcherWrapper(vertx.dispatcher())) + ) } class DispatcherWrapper(val dispatcher: CoroutineDispatcher) { @@ -602,4 +566,33 @@ class KotlinCoroutinesInstrumentationTest { return otelContext.makeCurrent() } } + + // regression test for + // https://github.com/open-telemetry/opentelemetry-java-instrumentation/issues/11411 + @ParameterizedTest + @ArgumentsSource(DispatchersSource::class) + fun `dispatch does not propagate context`(dispatcher: DispatcherWrapper) { + Assumptions.assumeTrue(dispatcher.dispatcher != Dispatchers.Unconfined) + + runTest(dispatcher) { + dispatcher.dispatcher.dispatch(coroutineContext) { + tracer.spanBuilder("dispatched").startSpan().end() + } + } + + testing.waitAndAssertTraces( + { trace -> + trace.hasSpansSatisfyingExactly({ + it.hasName("parent") + .hasNoParent() + }) + }, + { trace -> + trace.hasSpansSatisfyingExactly({ + it.hasName("dispatched") + .hasNoParent() + }) + } + ) + } } diff --git a/instrumentation/kotlinx-coroutines/kotlinx-coroutines-flow-1.3/javaagent-kotlin/build.gradle.kts b/instrumentation/kotlinx-coroutines/kotlinx-coroutines-flow-1.3/javaagent-kotlin/build.gradle.kts new file mode 100644 index 000000000000..755a2a9fa85f --- /dev/null +++ b/instrumentation/kotlinx-coroutines/kotlinx-coroutines-flow-1.3/javaagent-kotlin/build.gradle.kts @@ -0,0 +1,21 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + +// We are using a separate module for kotlin source instead of placing them in +// instrumentation/kotlinx-coroutines/kotlinx-coroutines-flow-1.3/javaagent because muzzle +// generation plugin currently doesn't handle kotlin sources correctly. +plugins { + id("org.jetbrains.kotlin.jvm") + id("otel.java-conventions") +} + +dependencies { + compileOnly("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.0") + compileOnly("org.jetbrains.kotlin:kotlin-stdlib-jdk8") + compileOnly(project(":instrumentation-api")) +} + +kotlin { + compilerOptions { + jvmTarget.set(JvmTarget.JVM_1_8) + } +} diff --git a/instrumentation/kotlinx-coroutines/kotlinx-coroutines-flow-1.3/javaagent-kotlin/src/main/kotlin/io/opentelemetry/javaagent/instrumentation/kotlinxcoroutines/flow/FlowUtil.kt b/instrumentation/kotlinx-coroutines/kotlinx-coroutines-flow-1.3/javaagent-kotlin/src/main/kotlin/io/opentelemetry/javaagent/instrumentation/kotlinxcoroutines/flow/FlowUtil.kt new file mode 100644 index 000000000000..65008f55ca62 --- /dev/null +++ b/instrumentation/kotlinx-coroutines/kotlinx-coroutines-flow-1.3/javaagent-kotlin/src/main/kotlin/io/opentelemetry/javaagent/instrumentation/kotlinxcoroutines/flow/FlowUtil.kt @@ -0,0 +1,17 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.kotlinxcoroutines.flow + +import io.opentelemetry.context.Context +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.onCompletion + +fun onComplete(flow: Flow<*>, instrumenter: Instrumenter, context: Context, request: REQUEST & Any): Flow<*> { + return flow.onCompletion { cause: Throwable? -> + instrumenter.end(context, request, null, cause) + } +} diff --git a/instrumentation/kotlinx-coroutines/javaagent/build.gradle.kts b/instrumentation/kotlinx-coroutines/kotlinx-coroutines-flow-1.3/javaagent/build.gradle.kts similarity index 56% rename from instrumentation/kotlinx-coroutines/javaagent/build.gradle.kts rename to instrumentation/kotlinx-coroutines/kotlinx-coroutines-flow-1.3/javaagent/build.gradle.kts index 21f304a056b0..d9106b5245af 100644 --- a/instrumentation/kotlinx-coroutines/javaagent/build.gradle.kts +++ b/instrumentation/kotlinx-coroutines/kotlinx-coroutines-flow-1.3/javaagent/build.gradle.kts @@ -1,4 +1,4 @@ -import org.jetbrains.kotlin.gradle.tasks.KotlinCompile +import org.jetbrains.kotlin.gradle.dsl.JvmTarget plugins { id("org.jetbrains.kotlin.jvm") @@ -9,7 +9,7 @@ muzzle { pass { group.set("org.jetbrains.kotlinx") module.set("kotlinx-coroutines-core") - versions.set("[1.0.0,1.3.8)") + versions.set("[1.3.0,1.3.8)") } // 1.3.9 (and beyond?) have changed how artifact names are resolved due to multiplatform variants pass { @@ -20,25 +20,28 @@ muzzle { } dependencies { - compileOnly("io.opentelemetry:opentelemetry-extension-kotlin") - compileOnly("org.jetbrains.kotlin:kotlin-stdlib-jdk8") + library("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.0") + compileOnly(project(":instrumentation-annotations-support")) + implementation(project(":instrumentation:kotlinx-coroutines:kotlinx-coroutines-flow-1.3:javaagent-kotlin")) + testInstrumentation(project(":instrumentation:kotlinx-coroutines:kotlinx-coroutines-1.0:javaagent")) testInstrumentation(project(":instrumentation:opentelemetry-extension-kotlin-1.0:javaagent")) testInstrumentation(project(":instrumentation:reactor:reactor-3.1:javaagent")) testImplementation("io.opentelemetry:opentelemetry-extension-kotlin") testImplementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") testImplementation(project(":instrumentation:reactor:reactor-3.1:library")) + testImplementation(project(":instrumentation-annotations")) - // Use first version with flow support since we have tests for it. - testLibrary("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.0") testLibrary("org.jetbrains.kotlinx:kotlinx-coroutines-reactor:1.3.0") } -tasks { - withType(KotlinCompile::class).configureEach { - kotlinOptions { - jvmTarget = "1.8" - } +kotlin { + compilerOptions { + jvmTarget.set(JvmTarget.JVM_1_8) } } + +tasks.withType().configureEach { + jvmArgs("-Dio.opentelemetry.javaagent.shaded.io.opentelemetry.context.enableStrictContext=false") +} diff --git a/instrumentation/kotlinx-coroutines/kotlinx-coroutines-flow-1.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kotlinxcoroutines/flow/AbstractFlowInstrumentation.java b/instrumentation/kotlinx-coroutines/kotlinx-coroutines-flow-1.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kotlinxcoroutines/flow/AbstractFlowInstrumentation.java new file mode 100644 index 000000000000..458a8b6f35a1 --- /dev/null +++ b/instrumentation/kotlinx-coroutines/kotlinx-coroutines-flow-1.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kotlinxcoroutines/flow/AbstractFlowInstrumentation.java @@ -0,0 +1,45 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.kotlinxcoroutines.flow; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.hasClassesNamed; +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.implementsInterface; +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 net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class AbstractFlowInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher classLoaderOptimization() { + return hasClassesNamed("kotlinx.coroutines.flow.Flow"); + } + + @Override + public ElementMatcher typeMatcher() { + return implementsInterface(named("kotlinx.coroutines.flow.Flow")); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isConstructor(), this.getClass().getName() + "$ConstructorAdvice"); + } + + @SuppressWarnings("unused") + public static class ConstructorAdvice { + + @Advice.OnMethodEnter + public static void enter() { + FlowInstrumentationHelper.initialize(); + } + } +} diff --git a/instrumentation/kotlinx-coroutines/kotlinx-coroutines-flow-1.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kotlinxcoroutines/flow/FlowInstrumentationHelper.java b/instrumentation/kotlinx-coroutines/kotlinx-coroutines-flow-1.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kotlinxcoroutines/flow/FlowInstrumentationHelper.java new file mode 100644 index 000000000000..02102afe01d0 --- /dev/null +++ b/instrumentation/kotlinx-coroutines/kotlinx-coroutines-flow-1.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kotlinxcoroutines/flow/FlowInstrumentationHelper.java @@ -0,0 +1,44 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.kotlinxcoroutines.flow; + +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.annotation.support.async.AsyncOperationEndStrategies; +import io.opentelemetry.instrumentation.api.annotation.support.async.AsyncOperationEndStrategy; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import kotlinx.coroutines.flow.Flow; + +public final class FlowInstrumentationHelper { + private static final FlowAsyncOperationEndStrategy asyncOperationEndStrategy = + new FlowAsyncOperationEndStrategy(); + + static { + AsyncOperationEndStrategies.instance().registerStrategy(asyncOperationEndStrategy); + } + + public static void initialize() {} + + private FlowInstrumentationHelper() {} + + private static final class FlowAsyncOperationEndStrategy implements AsyncOperationEndStrategy { + + @Override + public boolean supports(Class returnType) { + return Flow.class.isAssignableFrom(returnType); + } + + @Override + public Object end( + Instrumenter instrumenter, + Context context, + REQUEST request, + Object asyncValue, + Class responseType) { + Flow flow = (Flow) asyncValue; + return FlowUtilKt.onComplete(flow, instrumenter, context, request); + } + } +} diff --git a/instrumentation/kotlinx-coroutines/kotlinx-coroutines-flow-1.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kotlinxcoroutines/flow/KotlinCoroutinesFlowInstrumentationModule.java b/instrumentation/kotlinx-coroutines/kotlinx-coroutines-flow-1.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kotlinxcoroutines/flow/KotlinCoroutinesFlowInstrumentationModule.java new file mode 100644 index 000000000000..d280f231f430 --- /dev/null +++ b/instrumentation/kotlinx-coroutines/kotlinx-coroutines-flow-1.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kotlinxcoroutines/flow/KotlinCoroutinesFlowInstrumentationModule.java @@ -0,0 +1,26 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.kotlinxcoroutines.flow; + +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 KotlinCoroutinesFlowInstrumentationModule extends InstrumentationModule { + + public KotlinCoroutinesFlowInstrumentationModule() { + super("kotlinx-coroutines", "kotlinx-coroutines-flow"); + } + + @Override + public List typeInstrumentations() { + return singletonList(new AbstractFlowInstrumentation()); + } +} diff --git a/instrumentation/kotlinx-coroutines/kotlinx-coroutines-flow-1.3/javaagent/src/test/kotlin/io/opentelemetry/javaagent/instrumentation/kotlinxcoroutines/KotlinCoroutines13InstrumentationTest.kt b/instrumentation/kotlinx-coroutines/kotlinx-coroutines-flow-1.3/javaagent/src/test/kotlin/io/opentelemetry/javaagent/instrumentation/kotlinxcoroutines/KotlinCoroutines13InstrumentationTest.kt new file mode 100644 index 000000000000..ed6fc90b61a4 --- /dev/null +++ b/instrumentation/kotlinx-coroutines/kotlinx-coroutines-flow-1.3/javaagent/src/test/kotlin/io/opentelemetry/javaagent/instrumentation/kotlinxcoroutines/KotlinCoroutines13InstrumentationTest.kt @@ -0,0 +1,218 @@ +/* + * 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.asContextElement +import io.opentelemetry.instrumentation.reactor.v3_1.ContextPropagationOperator +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.asCoroutineDispatcher +import kotlinx.coroutines.channels.produce +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.consumeAsFlow +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.reactive.awaitSingle +import kotlinx.coroutines.reactive.collect +import kotlinx.coroutines.reactor.ReactorContext +import kotlinx.coroutines.reactor.flux +import kotlinx.coroutines.reactor.mono +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withContext +import org.junit.jupiter.api.AfterAll +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.api.extension.ExtensionContext +import org.junit.jupiter.api.extension.RegisterExtension +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.Arguments.arguments +import org.junit.jupiter.params.provider.ArgumentsProvider +import org.junit.jupiter.params.provider.ArgumentsSource +import java.util.concurrent.Executors +import java.util.stream.Stream + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +@ExperimentalCoroutinesApi +class KotlinCoroutines13InstrumentationTest { + + companion object { + val threadPool = Executors.newFixedThreadPool(2) + val singleThread = Executors.newSingleThreadExecutor() + } + + @AfterAll + fun shutdown() { + threadPool.shutdown() + singleThread.shutdown() + } + + @RegisterExtension + val testing = AgentInstrumentationExtension.create() + + val tracer = testing.openTelemetry.getTracer("test") + + @ParameterizedTest + @ArgumentsSource(DispatchersSource::class) + fun `traced across channels`(dispatcher: DispatcherWrapper) { + runTest(dispatcher) { + val producer = produce { + repeat(3) { + tracedChild("produce_$it") + send(it) + } + } + + producer.consumeAsFlow().onEach { + tracedChild("consume_$it") + }.collect() + } + + testing.waitAndAssertTraces( + { trace -> + trace.hasSpansSatisfyingExactlyInAnyOrder( + { + it.hasName("parent") + .hasNoParent() + }, + { + it.hasName("produce_0") + .hasParent(trace.getSpan(0)) + }, + { + it.hasName("consume_0") + .hasParent(trace.getSpan(0)) + }, + { + it.hasName("produce_1") + .hasParent(trace.getSpan(0)) + }, + { + it.hasName("consume_1") + .hasParent(trace.getSpan(0)) + }, + { + it.hasName("produce_2") + .hasParent(trace.getSpan(0)) + }, + { + it.hasName("consume_2") + .hasParent(trace.getSpan(0)) + }, + ) + }, + ) + } + + @ParameterizedTest + @ArgumentsSource(DispatchersSource::class) + fun `traced mono with context propagation operator`(dispatcherWrapper: DispatcherWrapper) { + runTest(dispatcherWrapper) { + val currentContext = Context.current() + // clear current context to ensure that ContextPropagationOperator is used for context propagation + withContext(Context.root().asContextElement()) { + val mono = mono(dispatcherWrapper.dispatcher) { + // extract context from reactor and propagate it into coroutine + val reactorContext = coroutineContext[ReactorContext.Key]?.context + val otelContext = ContextPropagationOperator.getOpenTelemetryContext(reactorContext, Context.current()) + withContext(otelContext.asContextElement()) { + tracedChild("child") + } + } + ContextPropagationOperator.runWithContext(mono, currentContext).awaitSingle() + } + } + + testing.waitAndAssertTraces( + { trace -> + trace.hasSpansSatisfyingExactly( + { + it.hasName("parent") + .hasNoParent() + }, + { + it.hasName("child") + .hasParent(trace.getSpan(0)) + }, + ) + }, + ) + } + + @ParameterizedTest + @ArgumentsSource(DispatchersSource::class) + fun `traced flux`(dispatcherWrapper: DispatcherWrapper) { + runTest(dispatcherWrapper) { + flux(dispatcherWrapper.dispatcher) { + repeat(3) { + tracedChild("child_$it") + send(it) + } + }.collect { + } + } + + testing.waitAndAssertTraces( + { trace -> + trace.hasSpansSatisfyingExactly( + { + it.hasName("parent") + .hasNoParent() + }, + { + it.hasName("child_0") + .hasParent(trace.getSpan(0)) + }, + { + it.hasName("child_1") + .hasParent(trace.getSpan(0)) + }, + { + it.hasName("child_2") + .hasParent(trace.getSpan(0)) + }, + ) + }, + ) + } + + private fun tracedChild(opName: String) { + tracer.spanBuilder(opName).startSpan().end() + } + + private fun runTest(dispatcherWrapper: DispatcherWrapper, block: suspend CoroutineScope.() -> T): T { + return runTest(dispatcherWrapper.dispatcher, block) + } + + private fun runTest(dispatcher: CoroutineDispatcher, 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() + } + } + + class DispatchersSource : ArgumentsProvider { + override fun provideArguments(context: ExtensionContext?): Stream = Stream.of( + // Wrap dispatchers since it seems that ParameterizedTest tries to automatically close + // Closeable arguments with no way to avoid it. + arguments(DispatcherWrapper(Dispatchers.Default)), + arguments(DispatcherWrapper(Dispatchers.IO)), + arguments(DispatcherWrapper(Dispatchers.Unconfined)), + arguments(DispatcherWrapper(threadPool.asCoroutineDispatcher())), + arguments(DispatcherWrapper(singleThread.asCoroutineDispatcher())), + ) + } + + class DispatcherWrapper(val dispatcher: CoroutineDispatcher) { + override fun toString(): String = dispatcher.toString() + } +} diff --git a/instrumentation/kotlinx-coroutines/kotlinx-coroutines-flow-1.3/javaagent/src/test/kotlin/io/opentelemetry/javaagent/instrumentation/kotlinxcoroutines/flow/FlowWithSpanTest.kt b/instrumentation/kotlinx-coroutines/kotlinx-coroutines-flow-1.3/javaagent/src/test/kotlin/io/opentelemetry/javaagent/instrumentation/kotlinxcoroutines/flow/FlowWithSpanTest.kt new file mode 100644 index 000000000000..d99e5dd5125b --- /dev/null +++ b/instrumentation/kotlinx-coroutines/kotlinx-coroutines-flow-1.3/javaagent/src/test/kotlin/io/opentelemetry/javaagent/instrumentation/kotlinxcoroutines/flow/FlowWithSpanTest.kt @@ -0,0 +1,66 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.kotlinxcoroutines.flow + +import io.opentelemetry.instrumentation.annotations.WithSpan +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension +import io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo +import io.opentelemetry.semconv.incubating.CodeIncubatingAttributes +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.count +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.runBlocking +import org.assertj.core.api.Condition +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.api.extension.RegisterExtension +import java.time.Clock +import java.util.concurrent.TimeUnit + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +@ExperimentalCoroutinesApi +class FlowWithSpanTest { + + @RegisterExtension + val testing = AgentInstrumentationExtension.create() + + @Test + fun `test method returning Flow with WithSpan annotation`() { + var flowStartTime: Long = 0 + runBlocking { + val flow = simple() + val now = Clock.systemUTC().instant() + flowStartTime = TimeUnit.SECONDS.toNanos(now.epochSecond) + now.nano + flow.count() + } + + testing.waitAndAssertTraces( + { trace -> + trace.hasSpansSatisfyingExactly( + { + it.hasName("FlowWithSpanTest.simple") + .hasNoParent() + .hasAttributesSatisfyingExactly( + equalTo(CodeIncubatingAttributes.CODE_NAMESPACE, this.javaClass.name), + equalTo(CodeIncubatingAttributes.CODE_FUNCTION, "simple") + ) + .has(Condition({ spanData -> spanData.endEpochNanos > flowStartTime }, "end time after $flowStartTime")) + } + ) + } + ) + } + + @WithSpan + fun simple(): Flow = flow { + for (i in 1..3) { + delay(100) + emit(i) + } + } +} diff --git a/instrumentation/ktor/ktor-1.0/library/README.md b/instrumentation/ktor/ktor-1.0/library/README.md index 492954f86f2c..9fa477f1d825 100644 --- a/instrumentation/ktor/ktor-1.0/library/README.md +++ b/instrumentation/ktor/ktor-1.0/library/README.md @@ -33,7 +33,7 @@ Initialize instrumentation by installing the `KtorServerTracing` feature. You mu the feature. ```kotlin -OpenTelemetry openTelemetry = initializeOpenTelemetryForMe() +OpenTelemetry openTelemetry = ... embeddedServer(Netty, 8080) { install(KtorServerTracing) { diff --git a/instrumentation/ktor/ktor-1.0/library/build.gradle.kts b/instrumentation/ktor/ktor-1.0/library/build.gradle.kts index 85102d338c4a..eebab4319ca9 100644 --- a/instrumentation/ktor/ktor-1.0/library/build.gradle.kts +++ b/instrumentation/ktor/ktor-1.0/library/build.gradle.kts @@ -1,4 +1,5 @@ -import org.jetbrains.kotlin.gradle.tasks.KotlinCompile +import org.jetbrains.kotlin.gradle.dsl.JvmTarget +import org.jetbrains.kotlin.gradle.dsl.KotlinVersion plugins { id("otel.library-instrumentation") @@ -22,10 +23,14 @@ dependencies { latestDepTestLibrary("io.ktor:ktor-server-netty:1.+") // see ktor-2.0 module } -tasks { - withType(KotlinCompile::class).configureEach { - kotlinOptions { - jvmTarget = "1.8" - } +kotlin { + compilerOptions { + jvmTarget.set(JvmTarget.JVM_1_8) + @Suppress("deprecation") + languageVersion.set(KotlinVersion.KOTLIN_1_4) } } + +tasks.withType().configureEach { + jvmArgs("-Dotel.instrumentation.common.experimental.controller-telemetry.enabled=true") +} diff --git a/instrumentation/ktor/ktor-1.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v1_0/KtorHttpServerAttributesGetter.kt b/instrumentation/ktor/ktor-1.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v1_0/KtorHttpServerAttributesGetter.kt index 884638e07f85..8b4c5e51015b 100644 --- a/instrumentation/ktor/ktor-1.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v1_0/KtorHttpServerAttributesGetter.kt +++ b/instrumentation/ktor/ktor-1.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v1_0/KtorHttpServerAttributesGetter.kt @@ -8,7 +8,8 @@ package io.opentelemetry.instrumentation.ktor.v1_0 import io.ktor.features.* import io.ktor.request.* import io.ktor.response.* -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpServerAttributesGetter +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerAttributesGetter +import io.opentelemetry.instrumentation.ktor.isIpAddress internal enum class KtorHttpServerAttributesGetter : HttpServerAttributesGetter { @@ -41,4 +42,18 @@ internal enum class KtorHttpServerAttributesGetter : override fun getUrlQuery(request: ApplicationRequest): String { return request.queryString() } + + override fun getNetworkProtocolName(request: ApplicationRequest, response: ApplicationResponse?): String? = + if (request.httpVersion.startsWith("HTTP/")) "http" else null + + override fun getNetworkProtocolVersion(request: ApplicationRequest, response: ApplicationResponse?): String? = + if (request.httpVersion.startsWith("HTTP/")) request.httpVersion.substring("HTTP/".length) else null + + override fun getNetworkPeerAddress(request: ApplicationRequest, response: ApplicationResponse?): String? { + val remote = request.local.remoteHost + if ("unknown" != remote && isIpAddress(remote)) { + return remote + } + return null + } } diff --git a/instrumentation/ktor/ktor-1.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v1_0/KtorNetServerAttributesGetter.kt b/instrumentation/ktor/ktor-1.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v1_0/KtorNetServerAttributesGetter.kt deleted file mode 100644 index 50aeda5af36b..000000000000 --- a/instrumentation/ktor/ktor-1.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v1_0/KtorNetServerAttributesGetter.kt +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.ktor.v1_0 - -import io.ktor.request.* -import io.ktor.response.* -import io.opentelemetry.instrumentation.api.instrumenter.net.NetServerAttributesGetter -import io.opentelemetry.instrumentation.ktor.isIpAddress - -internal class KtorNetServerAttributesGetter : NetServerAttributesGetter { - - override fun getNetworkProtocolName(request: ApplicationRequest, response: ApplicationResponse?): String? = - if (request.httpVersion.startsWith("HTTP/")) "http" else null - - override fun getNetworkProtocolVersion(request: ApplicationRequest, response: ApplicationResponse?): String? = - if (request.httpVersion.startsWith("HTTP/")) request.httpVersion.substring("HTTP/".length) else null - - override fun getClientSocketAddress(request: ApplicationRequest, response: ApplicationResponse?): String? { - val remote = request.local.remoteHost - if ("unknown" != remote && isIpAddress(remote)) { - return remote - } - return null - } - - override fun getServerAddress(request: ApplicationRequest): String { - return request.local.host - } - - override fun getServerPort(request: ApplicationRequest): Int { - return request.local.port - } -} diff --git a/instrumentation/ktor/ktor-1.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v1_0/KtorServerTracing.kt b/instrumentation/ktor/ktor-1.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v1_0/KtorServerTracing.kt index ec2969866a09..949efcf2c313 100644 --- a/instrumentation/ktor/ktor-1.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v1_0/KtorServerTracing.kt +++ b/instrumentation/ktor/ktor-1.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v1_0/KtorServerTracing.kt @@ -18,13 +18,13 @@ 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.SpanStatusExtractor -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteHolder -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteSource -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpServerAttributesExtractor -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 io.opentelemetry.instrumentation.api.internal.InstrumenterUtil +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerAttributesExtractor +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerMetrics +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRoute +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRouteSource +import io.opentelemetry.instrumentation.api.semconv.http.HttpSpanNameExtractor +import io.opentelemetry.instrumentation.api.semconv.http.HttpSpanStatusExtractor import kotlinx.coroutines.withContext class KtorServerTracing private constructor( @@ -36,10 +36,11 @@ class KtorServerTracing private constructor( internal val additionalExtractors = mutableListOf>() - internal val httpAttributesExtractorBuilder = HttpServerAttributesExtractor.builder( - KtorHttpServerAttributesGetter.INSTANCE, - KtorNetServerAttributesGetter() - ) + internal val httpAttributesExtractorBuilder = HttpServerAttributesExtractor.builder(KtorHttpServerAttributesGetter.INSTANCE) + + internal val httpSpanNameExtractorBuilder = HttpSpanNameExtractor.builder(KtorHttpServerAttributesGetter.INSTANCE) + + internal val httpServerRouteBuilder = HttpServerRoute.builder(KtorHttpServerAttributesGetter.INSTANCE) internal var statusExtractor: (SpanStatusExtractor) -> SpanStatusExtractor = { a -> a } @@ -57,9 +58,7 @@ class KtorServerTracing private constructor( this.statusExtractor = extractor } - fun setSpanKindExtractor( - extractor: (SpanKindExtractor) -> SpanKindExtractor - ) { + fun setSpanKindExtractor(extractor: (SpanKindExtractor) -> SpanKindExtractor) { this.spanKindExtractor = extractor } @@ -75,6 +74,12 @@ class KtorServerTracing private constructor( httpAttributesExtractorBuilder.setCapturedResponseHeaders(responseHeaders) } + fun setKnownMethods(knownMethods: Set) { + httpAttributesExtractorBuilder.setKnownMethods(knownMethods) + httpSpanNameExtractorBuilder.setKnownMethods(knownMethods) + httpServerRouteBuilder.setKnownMethods(knownMethods) + } + internal fun isOpenTelemetryInitialized(): Boolean = this::openTelemetry.isInitialized } @@ -111,7 +116,7 @@ class KtorServerTracing private constructor( val instrumenterBuilder = Instrumenter.builder( configuration.openTelemetry, INSTRUMENTATION_NAME, - HttpSpanNameExtractor.create(httpAttributesGetter), + configuration.httpSpanNameExtractorBuilder.build() ) configuration.additionalExtractors.forEach { instrumenterBuilder.addAttributesExtractor(it) } @@ -120,7 +125,7 @@ class KtorServerTracing private constructor( setSpanStatusExtractor(configuration.statusExtractor(HttpSpanStatusExtractor.create(httpAttributesGetter))) addAttributesExtractor(configuration.httpAttributesExtractorBuilder.build()) addOperationMetrics(HttpServerMetrics.get()) - addContextCustomizer(HttpRouteHolder.create(httpAttributesGetter)) + addContextCustomizer(configuration.httpServerRouteBuilder.build()) } val instrumenter = InstrumenterUtil.buildUpstreamInstrumenter( @@ -174,7 +179,7 @@ class KtorServerTracing private constructor( pipeline.environment.monitor.subscribe(Routing.RoutingCallStarted) { call -> val context = call.attributes.getOrNull(contextKey) if (context != null) { - HttpRouteHolder.updateHttpRoute(context, HttpRouteSource.SERVLET, { _, arg -> arg.route.parent.toString() }, call) + HttpServerRoute.update(context, HttpServerRouteSource.SERVER, { _, arg -> arg.route.parent.toString() }, call) } } diff --git a/instrumentation/ktor/ktor-1.0/library/src/test/kotlin/io/opentelemetry/instrumentation/ktor/v1_0/KtorHttpServerTest.kt b/instrumentation/ktor/ktor-1.0/library/src/test/kotlin/io/opentelemetry/instrumentation/ktor/v1_0/KtorHttpServerTest.kt index 2d80e4ecb7da..a00bb78a9d5b 100644 --- a/instrumentation/ktor/ktor-1.0/library/src/test/kotlin/io/opentelemetry/instrumentation/ktor/v1_0/KtorHttpServerTest.kt +++ b/instrumentation/ktor/ktor-1.0/library/src/test/kotlin/io/opentelemetry/instrumentation/ktor/v1_0/KtorHttpServerTest.kt @@ -21,7 +21,7 @@ import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpServerTes import io.opentelemetry.instrumentation.testing.junit.http.HttpServerInstrumentationExtension import io.opentelemetry.instrumentation.testing.junit.http.HttpServerTestOptions import io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import io.opentelemetry.semconv.ServerAttributes import kotlinx.coroutines.withContext import org.junit.jupiter.api.extension.RegisterExtension import java.util.concurrent.ExecutionException @@ -125,17 +125,19 @@ class KtorHttpServerTest : AbstractHttpServerTest() { options.setTestPathParam(true) options.setHttpAttributes { - HttpServerTestOptions.DEFAULT_HTTP_ATTRIBUTES - SemanticAttributes.NET_PEER_PORT + HttpServerTestOptions.DEFAULT_HTTP_ATTRIBUTES - ServerAttributes.SERVER_PORT } - options.setExpectedHttpRoute { - when (it) { + options.setExpectedHttpRoute { endpoint, method -> + when (endpoint) { ServerEndpoint.PATH_PARAM -> "/path/{id}/param" - else -> expectedHttpRoute(it) + else -> expectedHttpRoute(endpoint, method) } } // ktor does not have a controller lifecycle so the server span ends immediately when the // response is sent, which is before the controller span finishes. options.setVerifyServerSpanEndTime(false) + + options.setResponseCodeOnNonStandardHttpMethod(404) } } diff --git a/instrumentation/ktor/ktor-2.0/javaagent/build.gradle.kts b/instrumentation/ktor/ktor-2.0/javaagent/build.gradle.kts new file mode 100644 index 000000000000..0c2fdd249e41 --- /dev/null +++ b/instrumentation/ktor/ktor-2.0/javaagent/build.gradle.kts @@ -0,0 +1,43 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + +plugins { + id("org.jetbrains.kotlin.jvm") + id("otel.javaagent-instrumentation") +} + +muzzle { + pass { + group.set("org.jetbrains.kotlinx") + module.set("ktor-server-core") + versions.set("[2.0.0,)") + assertInverse.set(true) + } +} + +val ktorVersion = "2.0.0" + +dependencies { + library("io.ktor:ktor-client-core:$ktorVersion") + library("io.ktor:ktor-server-core:$ktorVersion") + + implementation(project(":instrumentation:ktor:ktor-2.0:library")) + + compileOnly("org.jetbrains.kotlin:kotlin-stdlib-jdk8") + + testInstrumentation(project(":instrumentation:netty:netty-4.1:javaagent")) + + testImplementation(project(":instrumentation:ktor:ktor-2.0:testing")) + testImplementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") + testImplementation("io.opentelemetry:opentelemetry-extension-kotlin") + + testLibrary("io.ktor:ktor-server-netty:$ktorVersion") + testLibrary("io.ktor:ktor-client-cio:$ktorVersion") +} + +kotlin { + compilerOptions { + jvmTarget.set(JvmTarget.JVM_1_8) + // generate metadata for Java 1.8 reflection on method parameters, used in @WithSpan tests + javaParameters = true + } +} diff --git a/instrumentation/ktor/ktor-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/ktor/v2_0/HttpClientInstrumentation.java b/instrumentation/ktor/ktor-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/ktor/v2_0/HttpClientInstrumentation.java new file mode 100644 index 000000000000..43f89f9968a5 --- /dev/null +++ b/instrumentation/ktor/ktor-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/ktor/v2_0/HttpClientInstrumentation.java @@ -0,0 +1,66 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.ktor.v2_0; + +import static net.bytebuddy.matcher.ElementMatchers.isConstructor; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import io.ktor.client.HttpClientConfig; +import io.ktor.client.engine.HttpClientEngineConfig; +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.instrumentation.ktor.v2_0.client.KtorClientTracing; +import io.opentelemetry.instrumentation.ktor.v2_0.client.KtorClientTracingBuilder; +import io.opentelemetry.javaagent.bootstrap.internal.AgentCommonConfig; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import kotlin.Unit; +import kotlin.jvm.functions.Function1; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class HttpClientInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher typeMatcher() { + return named("io.ktor.client.HttpClient"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isConstructor() + .and(takesArguments(2)) + .and(takesArgument(1, named("io.ktor.client.HttpClientConfig"))), + this.getClass().getName() + "$ConstructorAdvice"); + } + + @SuppressWarnings("unused") + public static class ConstructorAdvice { + + @Advice.OnMethodEnter + public static void onEnter( + @Advice.Argument(1) HttpClientConfig httpClientConfig) { + httpClientConfig.install(KtorClientTracing.Companion, new SetupFunction()); + } + } + + public static class SetupFunction implements Function1 { + + @Override + public Unit invoke(KtorClientTracingBuilder builder) { + OpenTelemetry openTelemetry = GlobalOpenTelemetry.get(); + builder.setOpenTelemetry(openTelemetry); + builder.capturedRequestHeaders(AgentCommonConfig.get().getClientRequestHeaders()); + builder.capturedResponseHeaders(AgentCommonConfig.get().getClientResponseHeaders()); + builder.knownMethods(AgentCommonConfig.get().getKnownHttpRequestMethods()); + + return kotlin.Unit.INSTANCE; + } + } +} diff --git a/instrumentation/ktor/ktor-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/ktor/v2_0/KtorClientInstrumentationModule.java b/instrumentation/ktor/ktor-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/ktor/v2_0/KtorClientInstrumentationModule.java new file mode 100644 index 000000000000..7893fc86f893 --- /dev/null +++ b/instrumentation/ktor/ktor-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/ktor/v2_0/KtorClientInstrumentationModule.java @@ -0,0 +1,31 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.ktor.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 KtorClientInstrumentationModule extends InstrumentationModule { + + public KtorClientInstrumentationModule() { + super("ktor", "ktor-client", "ktor-2.0", "ktor-client-2.0"); + } + + @Override + public boolean isHelperClass(String className) { + return className.startsWith("io.opentelemetry.extension.kotlin."); + } + + @Override + public List typeInstrumentations() { + return singletonList(new HttpClientInstrumentation()); + } +} diff --git a/instrumentation/kotlinx-coroutines/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kotlinxcoroutines/KotlinCoroutinesInstrumentationModule.java b/instrumentation/ktor/ktor-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/ktor/v2_0/KtorServerInstrumentationModule.java similarity index 67% rename from instrumentation/kotlinx-coroutines/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kotlinxcoroutines/KotlinCoroutinesInstrumentationModule.java rename to instrumentation/ktor/ktor-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/ktor/v2_0/KtorServerInstrumentationModule.java index c5727635527b..0eea031ea21e 100644 --- a/instrumentation/kotlinx-coroutines/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kotlinxcoroutines/KotlinCoroutinesInstrumentationModule.java +++ b/instrumentation/ktor/ktor-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/ktor/v2_0/KtorServerInstrumentationModule.java @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.javaagent.instrumentation.kotlinxcoroutines; +package io.opentelemetry.javaagent.instrumentation.ktor.v2_0; import static java.util.Collections.singletonList; @@ -13,10 +13,10 @@ import java.util.List; @AutoService(InstrumentationModule.class) -public class KotlinCoroutinesInstrumentationModule extends InstrumentationModule { +public class KtorServerInstrumentationModule extends InstrumentationModule { - public KotlinCoroutinesInstrumentationModule() { - super("kotlinx-coroutines"); + public KtorServerInstrumentationModule() { + super("ktor", "ktor-server", "ktor-2.0", "ktor-server-2.0"); } @Override @@ -26,6 +26,6 @@ public boolean isHelperClass(String className) { @Override public List typeInstrumentations() { - return singletonList(new KotlinCoroutinesInstrumentation()); + return singletonList(new ServerInstrumentation()); } } diff --git a/instrumentation/ktor/ktor-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/ktor/v2_0/ServerInstrumentation.java b/instrumentation/ktor/ktor-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/ktor/v2_0/ServerInstrumentation.java new file mode 100644 index 000000000000..adbaaa38003d --- /dev/null +++ b/instrumentation/ktor/ktor-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/ktor/v2_0/ServerInstrumentation.java @@ -0,0 +1,60 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.ktor.v2_0; + +import static net.bytebuddy.matcher.ElementMatchers.isConstructor; +import static net.bytebuddy.matcher.ElementMatchers.named; + +import io.ktor.server.application.Application; +import io.ktor.server.application.ApplicationPluginKt; +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.instrumentation.ktor.v2_0.server.KtorServerTracing; +import io.opentelemetry.javaagent.bootstrap.internal.AgentCommonConfig; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import kotlin.Unit; +import kotlin.jvm.functions.Function1; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class ServerInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher typeMatcher() { + return named("io.ktor.server.engine.ApplicationEngineEnvironmentReloading"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isConstructor(), this.getClass().getName() + "$ConstructorAdvice"); + } + + @SuppressWarnings("unused") + public static class ConstructorAdvice { + + @Advice.OnMethodExit + public static void onExit(@Advice.FieldValue("_applicationInstance") Application application) { + ApplicationPluginKt.install(application, KtorServerTracing.Feature, new SetupFunction()); + } + } + + public static class SetupFunction + implements Function1 { + + @Override + public Unit invoke(KtorServerTracing.Configuration configuration) { + OpenTelemetry openTelemetry = GlobalOpenTelemetry.get(); + configuration.setOpenTelemetry(openTelemetry); + configuration.capturedRequestHeaders(AgentCommonConfig.get().getServerRequestHeaders()); + configuration.capturedResponseHeaders(AgentCommonConfig.get().getServerResponseHeaders()); + configuration.knownMethods(AgentCommonConfig.get().getKnownHttpRequestMethods()); + + return kotlin.Unit.INSTANCE; + } + } +} diff --git a/instrumentation/ktor/ktor-2.0/javaagent/src/test/java/io/opentelemetry/instrumentation/ktor/v2_0/client/KtorHttpClientTest.kt b/instrumentation/ktor/ktor-2.0/javaagent/src/test/java/io/opentelemetry/instrumentation/ktor/v2_0/client/KtorHttpClientTest.kt new file mode 100644 index 000000000000..8b8b655f8475 --- /dev/null +++ b/instrumentation/ktor/ktor-2.0/javaagent/src/test/java/io/opentelemetry/instrumentation/ktor/v2_0/client/KtorHttpClientTest.kt @@ -0,0 +1,22 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.ktor.v2_0.client + +import io.ktor.client.* +import io.opentelemetry.instrumentation.testing.junit.http.HttpClientInstrumentationExtension +import org.junit.jupiter.api.extension.RegisterExtension + +class KtorHttpClientTest : AbstractKtorHttpClientTest() { + + companion object { + @JvmStatic + @RegisterExtension + private val TESTING = HttpClientInstrumentationExtension.forAgent() + } + + override fun HttpClientConfig<*>.installTracing() { + } +} diff --git a/instrumentation/ktor/ktor-2.0/javaagent/src/test/java/io/opentelemetry/instrumentation/ktor/v2_0/server/KtorHttpServerTest.kt b/instrumentation/ktor/ktor-2.0/javaagent/src/test/java/io/opentelemetry/instrumentation/ktor/v2_0/server/KtorHttpServerTest.kt new file mode 100644 index 000000000000..f9776431becf --- /dev/null +++ b/instrumentation/ktor/ktor-2.0/javaagent/src/test/java/io/opentelemetry/instrumentation/ktor/v2_0/server/KtorHttpServerTest.kt @@ -0,0 +1,33 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.ktor.v2_0.server + +import io.ktor.server.application.* +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension +import io.opentelemetry.instrumentation.testing.junit.http.HttpServerInstrumentationExtension +import io.opentelemetry.instrumentation.testing.junit.http.HttpServerTestOptions +import org.junit.jupiter.api.extension.RegisterExtension + +class KtorHttpServerTest : AbstractKtorHttpServerTest() { + + companion object { + @JvmStatic + @RegisterExtension + val TESTING: InstrumentationExtension = HttpServerInstrumentationExtension.forAgent() + } + + override fun getTesting(): InstrumentationExtension { + return TESTING + } + + override fun installOpenTelemetry(application: Application) { + } + + override fun configure(options: HttpServerTestOptions) { + super.configure(options) + options.setTestException(false) + } +} diff --git a/instrumentation/ktor/ktor-2.0/library/README.md b/instrumentation/ktor/ktor-2.0/library/README.md index 83f5bb949bf0..46e0be300abe 100644 --- a/instrumentation/ktor/ktor-2.0/library/README.md +++ b/instrumentation/ktor/ktor-2.0/library/README.md @@ -35,7 +35,7 @@ Initialize instrumentation by installing the `KtorServerTracing` feature. You mu the feature. ```kotlin -val openTelemetry: OpenTelemetry = initializeOpenTelemetryForMe() +val openTelemetry: OpenTelemetry = ... embeddedServer(Netty, 8080) { install(KtorServerTracing) { @@ -50,7 +50,7 @@ Initialize instrumentation by installing the `KtorClientTracing` feature. You mu the feature. ```kotlin -val openTelemetry: OpenTelemetry = initializeOpenTelemetryForMe() +val openTelemetry: OpenTelemetry = ... val client = HttpClient { install(KtorClientTracing) { diff --git a/instrumentation/ktor/ktor-2.0/library/build.gradle.kts b/instrumentation/ktor/ktor-2.0/library/build.gradle.kts index 0a723cd48685..c29e1cf733b2 100644 --- a/instrumentation/ktor/ktor-2.0/library/build.gradle.kts +++ b/instrumentation/ktor/ktor-2.0/library/build.gradle.kts @@ -1,4 +1,5 @@ -import org.jetbrains.kotlin.gradle.tasks.KotlinCompile +import org.jetbrains.kotlin.gradle.dsl.JvmTarget +import org.jetbrains.kotlin.gradle.dsl.KotlinVersion plugins { id("otel.library-instrumentation") @@ -17,16 +18,17 @@ dependencies { compileOnly("org.jetbrains.kotlin:kotlin-stdlib-jdk8") + testImplementation(project(":instrumentation:ktor:ktor-2.0:testing")) testImplementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") testLibrary("io.ktor:ktor-server-netty:$ktorVersion") testLibrary("io.ktor:ktor-client-cio:$ktorVersion") } -tasks { - withType(KotlinCompile::class).configureEach { - kotlinOptions { - jvmTarget = "1.8" - } +kotlin { + compilerOptions { + jvmTarget.set(JvmTarget.JVM_1_8) + @Suppress("deprecation") + languageVersion.set(KotlinVersion.KOTLIN_1_6) } } diff --git a/instrumentation/ktor/ktor-2.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/client/KtorClientTracing.kt b/instrumentation/ktor/ktor-2.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/client/KtorClientTracing.kt index a98d77d60c15..421a642ac87d 100644 --- a/instrumentation/ktor/ktor-2.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/client/KtorClientTracing.kt +++ b/instrumentation/ktor/ktor-2.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/client/KtorClientTracing.kt @@ -16,6 +16,9 @@ import io.opentelemetry.context.Context import io.opentelemetry.context.propagation.ContextPropagators import io.opentelemetry.extension.kotlin.asContextElement import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter +import kotlinx.coroutines.InternalCoroutinesApi +import kotlinx.coroutines.job +import kotlinx.coroutines.launch import kotlinx.coroutines.withContext class KtorClientTracing internal constructor( @@ -83,23 +86,25 @@ class KtorClientTracing internal constructor( } } + @OptIn(InternalCoroutinesApi::class) private fun installSpanEnd(plugin: KtorClientTracing, scope: HttpClient) { val endSpanPhase = PipelinePhase("OpenTelemetryEndSpan") scope.receivePipeline.insertPhaseBefore(HttpReceivePipeline.State, endSpanPhase) scope.receivePipeline.intercept(endSpanPhase) { val openTelemetryContext = it.call.attributes.getOrNull(openTelemetryContextKey) - - if (openTelemetryContext != null) { - try { - withContext(openTelemetryContext.asContextElement()) { proceed() } - plugin.endSpan(openTelemetryContext, it.call, null) - } catch (e: Throwable) { - plugin.endSpan(openTelemetryContext, it.call, e) - throw e + openTelemetryContext ?: return@intercept + + scope.launch { + val job = it.call.coroutineContext.job + job.join() + val cause = if (!job.isCancelled) { + null + } else { + kotlin.runCatching { job.getCancellationException() }.getOrNull() } - } else { - proceed() + + plugin.endSpan(openTelemetryContext, it.call, cause) } } } diff --git a/instrumentation/ktor/ktor-2.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/client/KtorClientTracingBuilder.kt b/instrumentation/ktor/ktor-2.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/client/KtorClientTracingBuilder.kt index d226b4e114d7..654bacda482e 100644 --- a/instrumentation/ktor/ktor-2.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/client/KtorClientTracingBuilder.kt +++ b/instrumentation/ktor/ktor-2.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/client/KtorClientTracingBuilder.kt @@ -6,49 +6,165 @@ package io.opentelemetry.instrumentation.ktor.v2_0.client import io.ktor.client.request.* -import io.ktor.client.statement.HttpResponse +import io.ktor.client.statement.* +import io.ktor.http.* import io.opentelemetry.api.OpenTelemetry +import io.opentelemetry.api.common.AttributesBuilder +import io.opentelemetry.context.Context +import io.opentelemetry.instrumentation.api.incubator.semconv.http.HttpClientExperimentalMetrics +import io.opentelemetry.instrumentation.api.incubator.semconv.http.HttpExperimentalAttributesExtractor import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter import io.opentelemetry.instrumentation.api.instrumenter.SpanKindExtractor.alwaysClient -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpClientAttributesExtractor -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpClientMetrics -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpSpanNameExtractor -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpSpanStatusExtractor +import io.opentelemetry.instrumentation.api.semconv.http.HttpClientAttributesExtractor +import io.opentelemetry.instrumentation.api.semconv.http.HttpClientMetrics +import io.opentelemetry.instrumentation.api.semconv.http.HttpSpanNameExtractor +import io.opentelemetry.instrumentation.api.semconv.http.HttpSpanStatusExtractor import io.opentelemetry.instrumentation.ktor.v2_0.InstrumentationProperties.INSTRUMENTATION_NAME class KtorClientTracingBuilder { private var openTelemetry: OpenTelemetry? = null private val additionalExtractors = mutableListOf>() - private val httpAttributesExtractorBuilder = HttpClientAttributesExtractor.builder( - KtorHttpClientAttributesGetter, - KtorNetClientAttributesGetter, - ) + private val httpAttributesExtractorBuilder = HttpClientAttributesExtractor.builder(KtorHttpClientAttributesGetter) + private val httpSpanNameExtractorBuilder = HttpSpanNameExtractor.builder(KtorHttpClientAttributesGetter) + private var emitExperimentalHttpClientMetrics = false fun setOpenTelemetry(openTelemetry: OpenTelemetry) { this.openTelemetry = openTelemetry } - fun setCapturedRequestHeaders(vararg headers: String) = - setCapturedRequestHeaders(headers.asList()) + @Deprecated( + "Please use method `capturedRequestHeaders`", + ReplaceWith("capturedRequestHeaders(headers.asIterable())") + ) + fun setCapturedRequestHeaders(vararg headers: String) = capturedRequestHeaders(headers.asIterable()) + + @Deprecated( + "Please use method `capturedRequestHeaders`", + ReplaceWith("capturedRequestHeaders(headers)") + ) + fun setCapturedRequestHeaders(headers: List) = capturedRequestHeaders(headers) + + fun capturedRequestHeaders(vararg headers: String) = capturedRequestHeaders(headers.asIterable()) + + fun capturedRequestHeaders(headers: Iterable) { + httpAttributesExtractorBuilder.setCapturedRequestHeaders(headers.toList()) + } + + @Deprecated( + "Please use method `capturedResponseHeaders`", + ReplaceWith("capturedResponseHeaders(headers.asIterable())") + ) + fun setCapturedResponseHeaders(vararg headers: String) = capturedResponseHeaders(headers.asIterable()) + + @Deprecated( + "Please use method `capturedResponseHeaders`", + ReplaceWith("capturedResponseHeaders(headers)") + ) + fun setCapturedResponseHeaders(headers: List) = capturedResponseHeaders(headers) + + fun capturedResponseHeaders(vararg headers: String) = capturedResponseHeaders(headers.asIterable()) - fun setCapturedRequestHeaders(headers: List) { - httpAttributesExtractorBuilder.setCapturedRequestHeaders(headers) + fun capturedResponseHeaders(headers: Iterable) { + httpAttributesExtractorBuilder.setCapturedResponseHeaders(headers.toList()) } - fun setCapturedResponseHeaders(vararg headers: String) = - setCapturedResponseHeaders(headers.asList()) + @Deprecated( + "Please use method `knownMethods`", + ReplaceWith("knownMethods(knownMethods)") + ) + fun setKnownMethods(knownMethods: Set) = knownMethods(knownMethods) + + fun knownMethods(vararg methods: String) = knownMethods(methods.asIterable()) + + fun knownMethods(vararg methods: HttpMethod) = knownMethods(methods.asIterable()) - fun setCapturedResponseHeaders(headers: List) { - httpAttributesExtractorBuilder.setCapturedResponseHeaders(headers) + @JvmName("knownMethodsJvm") + fun knownMethods(methods: Iterable) = knownMethods(methods.map { it.value }) + + fun knownMethods(methods: Iterable) { + methods.toSet().apply { + httpAttributesExtractorBuilder.setKnownMethods(this) + httpSpanNameExtractorBuilder.setKnownMethods(this) + } } - fun addAttributesExtractors(vararg extractors: AttributesExtractor) = - addAttributesExtractors(extractors.asList()) + @Deprecated("Please use method `attributeExtractor`") + fun addAttributesExtractors(vararg extractors: AttributesExtractor) = addAttributesExtractors(extractors.asList()) + @Deprecated("Please use method `attributeExtractor`") fun addAttributesExtractors(extractors: Iterable>) { - additionalExtractors += extractors + extractors.forEach { + attributeExtractor { + onStart { it.onStart(attributes, parentContext, request) } + onEnd { it.onEnd(attributes, parentContext, request, response, error) } + } + } + } + + fun attributeExtractor(extractorBuilder: ExtractorBuilder.() -> Unit = {}) { + val builder = ExtractorBuilder().apply(extractorBuilder).build() + additionalExtractors.add( + object : AttributesExtractor { + override fun onStart(attributes: AttributesBuilder, parentContext: Context, request: HttpRequestData) { + builder.onStart(OnStartData(attributes, parentContext, request)) + } + + override fun onEnd(attributes: AttributesBuilder, context: Context, request: HttpRequestData, response: HttpResponse?, error: Throwable?) { + builder.onEnd(OnEndData(attributes, context, request, response, error)) + } + } + ) + } + + class ExtractorBuilder { + private var onStart: OnStartData.() -> Unit = {} + private var onEnd: OnEndData.() -> Unit = {} + + fun onStart(block: OnStartData.() -> Unit) { + onStart = block + } + + fun onEnd(block: OnEndData.() -> Unit) { + onEnd = block + } + + internal fun build(): Extractor { + return Extractor(onStart, onEnd) + } + } + + internal class Extractor(val onStart: OnStartData.() -> Unit, val onEnd: OnEndData.() -> Unit) + + data class OnStartData( + val attributes: AttributesBuilder, + val parentContext: Context, + val request: HttpRequestData + ) + + data class OnEndData( + val attributes: AttributesBuilder, + val parentContext: Context, + val request: HttpRequestData, + val response: HttpResponse?, + val error: Throwable? + ) + + /** + * Configures the instrumentation to emit experimental HTTP client metrics. + * + * @param emitExperimentalHttpClientMetrics `true` if the experimental HTTP client metrics are to be emitted. + */ + @Deprecated("Please use method `emitExperimentalHttpClientMetrics`") + fun setEmitExperimentalHttpClientMetrics(emitExperimentalHttpClientMetrics: Boolean) { + if (emitExperimentalHttpClientMetrics) { + emitExperimentalHttpClientMetrics() + } + } + + fun emitExperimentalHttpClientMetrics() { + emitExperimentalHttpClientMetrics = true } internal fun build(): KtorClientTracing { @@ -58,14 +174,20 @@ class KtorClientTracingBuilder { val instrumenterBuilder = Instrumenter.builder( initializedOpenTelemetry, INSTRUMENTATION_NAME, - HttpSpanNameExtractor.create(KtorHttpClientAttributesGetter), + httpSpanNameExtractorBuilder.build() ) - - val instrumenter = instrumenterBuilder .setSpanStatusExtractor(HttpSpanStatusExtractor.create(KtorHttpClientAttributesGetter)) .addAttributesExtractor(httpAttributesExtractorBuilder.build()) .addAttributesExtractors(additionalExtractors) .addOperationMetrics(HttpClientMetrics.get()) + + if (emitExperimentalHttpClientMetrics) { + instrumenterBuilder + .addAttributesExtractor(HttpExperimentalAttributesExtractor.create(KtorHttpClientAttributesGetter)) + .addOperationMetrics(HttpClientExperimentalMetrics.get()) + } + + val instrumenter = instrumenterBuilder .buildInstrumenter(alwaysClient()) return KtorClientTracing( diff --git a/instrumentation/ktor/ktor-2.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/client/KtorHttpClientAttributesGetter.kt b/instrumentation/ktor/ktor-2.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/client/KtorHttpClientAttributesGetter.kt index 470c539aa1d6..62e61090b927 100644 --- a/instrumentation/ktor/ktor-2.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/client/KtorHttpClientAttributesGetter.kt +++ b/instrumentation/ktor/ktor-2.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/client/KtorHttpClientAttributesGetter.kt @@ -6,23 +6,32 @@ package io.opentelemetry.instrumentation.ktor.v2_0.client import io.ktor.client.request.* -import io.ktor.client.statement.HttpResponse -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpClientAttributesGetter +import io.ktor.client.statement.* +import io.opentelemetry.instrumentation.api.semconv.http.HttpClientAttributesGetter internal object KtorHttpClientAttributesGetter : HttpClientAttributesGetter { - override fun getUrlFull(request: HttpRequestData) = - request.url.toString() + override fun getUrlFull(request: HttpRequestData) = request.url.toString() - override fun getHttpRequestMethod(request: HttpRequestData) = - request.method.value + override fun getHttpRequestMethod(request: HttpRequestData) = request.method.value - override fun getHttpRequestHeader(request: HttpRequestData, name: String) = - request.headers.getAll(name).orEmpty() + override fun getHttpRequestHeader(request: HttpRequestData, name: String) = request.headers.getAll(name).orEmpty() - override fun getHttpResponseStatusCode(request: HttpRequestData, response: HttpResponse, error: Throwable?) = - response.status.value + override fun getHttpResponseStatusCode(request: HttpRequestData, response: HttpResponse, error: Throwable?) = response.status.value - override fun getHttpResponseHeader(request: HttpRequestData, response: HttpResponse, name: String) = - response.headers.getAll(name).orEmpty() + override fun getHttpResponseHeader(request: HttpRequestData, response: HttpResponse, name: String) = response.headers.getAll(name).orEmpty() + + override fun getNetworkProtocolName(request: HttpRequestData, response: HttpResponse?): String? = response?.version?.name + + override fun getNetworkProtocolVersion(request: HttpRequestData, response: HttpResponse?): String? { + val version = response?.version ?: return null + if (version.minor == 0) { + return "${version.major}" + } + return "${version.major}.${version.minor}" + } + + override fun getServerAddress(request: HttpRequestData) = request.url.host + + override fun getServerPort(request: HttpRequestData) = request.url.port } diff --git a/instrumentation/ktor/ktor-2.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/client/KtorNetClientAttributesGetter.kt b/instrumentation/ktor/ktor-2.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/client/KtorNetClientAttributesGetter.kt deleted file mode 100644 index ba587e2331dc..000000000000 --- a/instrumentation/ktor/ktor-2.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/client/KtorNetClientAttributesGetter.kt +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.ktor.v2_0.client - -import io.ktor.client.request.* -import io.ktor.client.statement.* -import io.opentelemetry.instrumentation.api.instrumenter.net.NetClientAttributesGetter - -internal object KtorNetClientAttributesGetter : NetClientAttributesGetter { - - override fun getNetworkProtocolName(request: HttpRequestData?, response: HttpResponse?): String? = - response?.version?.name - - override fun getNetworkProtocolVersion(request: HttpRequestData?, response: HttpResponse?): String? { - val version = response?.version ?: return null - return "${version.major}.${version.minor}" - } - - override fun getServerAddress(request: HttpRequestData) = request.url.host - - override fun getServerPort(request: HttpRequestData) = request.url.port -} diff --git a/instrumentation/ktor/ktor-2.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/server/KtorHttpServerAttributesGetter.kt b/instrumentation/ktor/ktor-2.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/server/KtorHttpServerAttributesGetter.kt index 330b75a71134..49bdedf85d6d 100644 --- a/instrumentation/ktor/ktor-2.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/server/KtorHttpServerAttributesGetter.kt +++ b/instrumentation/ktor/ktor-2.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/server/KtorHttpServerAttributesGetter.kt @@ -8,7 +8,8 @@ package io.opentelemetry.instrumentation.ktor.v2_0.server import io.ktor.server.plugins.* import io.ktor.server.request.* import io.ktor.server.response.* -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpServerAttributesGetter +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerAttributesGetter +import io.opentelemetry.instrumentation.ktor.isIpAddress internal enum class KtorHttpServerAttributesGetter : HttpServerAttributesGetter { @@ -41,4 +42,18 @@ internal enum class KtorHttpServerAttributesGetter : override fun getUrlQuery(request: ApplicationRequest): String { return request.queryString() } + + override fun getNetworkProtocolName(request: ApplicationRequest, response: ApplicationResponse?): String? = + if (request.httpVersion.startsWith("HTTP/")) "http" else null + + override fun getNetworkProtocolVersion(request: ApplicationRequest, response: ApplicationResponse?): String? = + if (request.httpVersion.startsWith("HTTP/")) request.httpVersion.substring("HTTP/".length) else null + + override fun getNetworkPeerAddress(request: ApplicationRequest, response: ApplicationResponse?): String? { + val remote = request.local.remoteHost + if ("unknown" != remote && isIpAddress(remote)) { + return remote + } + return null + } } diff --git a/instrumentation/ktor/ktor-2.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/server/KtorNetServerAttributesGetter.kt b/instrumentation/ktor/ktor-2.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/server/KtorNetServerAttributesGetter.kt deleted file mode 100644 index 39d2e65532e0..000000000000 --- a/instrumentation/ktor/ktor-2.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/server/KtorNetServerAttributesGetter.kt +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.ktor.v2_0.server - -import io.ktor.server.request.* -import io.ktor.server.response.* -import io.opentelemetry.instrumentation.api.instrumenter.net.NetServerAttributesGetter -import io.opentelemetry.instrumentation.ktor.isIpAddress - -internal class KtorNetServerAttributesGetter : NetServerAttributesGetter { - - override fun getNetworkProtocolName(request: ApplicationRequest, response: ApplicationResponse?): String? = - if (request.httpVersion.startsWith("HTTP/")) "http" else null - - override fun getNetworkProtocolVersion(request: ApplicationRequest, response: ApplicationResponse?): String? = - if (request.httpVersion.startsWith("HTTP/")) request.httpVersion.substring("HTTP/".length) else null - - override fun getServerAddress(request: ApplicationRequest): String { - return request.local.host - } - - override fun getServerPort(request: ApplicationRequest): Int { - return request.local.port - } - - override fun getClientSocketAddress(request: ApplicationRequest, response: ApplicationResponse?): String? { - val remote = request.local.remoteHost - if ("unknown" != remote && isIpAddress(remote)) { - return remote - } - return null - } -} diff --git a/instrumentation/ktor/ktor-2.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/server/KtorServerTracing.kt b/instrumentation/ktor/ktor-2.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/server/KtorServerTracing.kt index c8e2c034ef79..2e4c203eb3ed 100644 --- a/instrumentation/ktor/ktor-2.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/server/KtorServerTracing.kt +++ b/instrumentation/ktor/ktor-2.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/server/KtorServerTracing.kt @@ -5,6 +5,7 @@ package io.opentelemetry.instrumentation.ktor.v2_0.server +import io.ktor.http.* import io.ktor.server.application.* import io.ktor.server.request.* import io.ktor.server.response.* @@ -12,19 +13,22 @@ import io.ktor.server.routing.* import io.ktor.util.* import io.ktor.util.pipeline.* import io.opentelemetry.api.OpenTelemetry +import io.opentelemetry.api.common.AttributesBuilder +import io.opentelemetry.api.trace.SpanKind import io.opentelemetry.context.Context import io.opentelemetry.extension.kotlin.asContextElement 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.SpanStatusBuilder import io.opentelemetry.instrumentation.api.instrumenter.SpanStatusExtractor -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteHolder -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteSource -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpServerAttributesExtractor -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 io.opentelemetry.instrumentation.api.internal.InstrumenterUtil +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerAttributesExtractor +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerMetrics +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRoute +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRouteSource +import io.opentelemetry.instrumentation.api.semconv.http.HttpSpanNameExtractor +import io.opentelemetry.instrumentation.api.semconv.http.HttpSpanStatusExtractor import io.opentelemetry.instrumentation.ktor.v2_0.InstrumentationProperties.INSTRUMENTATION_NAME import kotlinx.coroutines.withContext @@ -37,10 +41,11 @@ class KtorServerTracing private constructor( internal val additionalExtractors = mutableListOf>() - internal val httpAttributesExtractorBuilder = HttpServerAttributesExtractor.builder( - KtorHttpServerAttributesGetter.INSTANCE, - KtorNetServerAttributesGetter() - ) + internal val httpAttributesExtractorBuilder = HttpServerAttributesExtractor.builder(KtorHttpServerAttributesGetter.INSTANCE) + + internal val httpSpanNameExtractorBuilder = HttpSpanNameExtractor.builder(KtorHttpServerAttributesGetter.INSTANCE) + + internal val httpServerRouteBuilder = HttpServerRoute.builder(KtorHttpServerAttributesGetter.INSTANCE) internal var statusExtractor: (SpanStatusExtractor) -> SpanStatusExtractor = { a -> a } @@ -52,28 +57,151 @@ class KtorServerTracing private constructor( this.openTelemetry = openTelemetry } + @Deprecated("Please use method `spanStatusExtractor`") fun setStatusExtractor( extractor: (SpanStatusExtractor) -> SpanStatusExtractor ) { - this.statusExtractor = extractor + spanStatusExtractor { prevStatusExtractor -> + extractor(prevStatusExtractor).extract(spanStatusBuilder, request, response, error) + } } - fun setSpanKindExtractor( - extractor: (SpanKindExtractor) -> SpanKindExtractor - ) { - this.spanKindExtractor = extractor + fun spanStatusExtractor(extract: SpanStatusData.(SpanStatusExtractor) -> Unit) { + statusExtractor = { prevExtractor -> + SpanStatusExtractor { spanStatusBuilder: SpanStatusBuilder, + request: ApplicationRequest, + response: ApplicationResponse?, + throwable: Throwable? -> + extract(SpanStatusData(spanStatusBuilder, request, response, throwable), prevExtractor) + } + } } + data class SpanStatusData( + val spanStatusBuilder: SpanStatusBuilder, + val request: ApplicationRequest, + val response: ApplicationResponse?, + val error: Throwable? + ) + + @Deprecated("Please use method `spanKindExtractor`") + fun setSpanKindExtractor(extractor: (SpanKindExtractor) -> SpanKindExtractor) { + spanKindExtractor { prevSpanKindExtractor -> + extractor(prevSpanKindExtractor).extract(this) + } + } + + fun spanKindExtractor(extract: ApplicationRequest.(SpanKindExtractor) -> SpanKind) { + spanKindExtractor = { prevExtractor -> + SpanKindExtractor { request: ApplicationRequest -> + extract(request, prevExtractor) + } + } + } + + @Deprecated("Please use method `attributeExtractor`") fun addAttributeExtractor(extractor: AttributesExtractor) { - additionalExtractors.add(extractor) + attributeExtractor { + onStart { + extractor.onStart(attributes, parentContext, request) + } + onEnd { + extractor.onEnd(attributes, parentContext, request, response, error) + } + } } - fun setCapturedRequestHeaders(requestHeaders: List) { - httpAttributesExtractorBuilder.setCapturedRequestHeaders(requestHeaders) + fun attributeExtractor(extractorBuilder: ExtractorBuilder.() -> Unit = {}) { + val builder = ExtractorBuilder().apply(extractorBuilder).build() + additionalExtractors.add( + object : AttributesExtractor { + override fun onStart(attributes: AttributesBuilder, parentContext: Context, request: ApplicationRequest) { + builder.onStart(OnStartData(attributes, parentContext, request)) + } + + override fun onEnd(attributes: AttributesBuilder, context: Context, request: ApplicationRequest, response: ApplicationResponse?, error: Throwable?) { + builder.onEnd(OnEndData(attributes, context, request, response, error)) + } + } + ) } - fun setCapturedResponseHeaders(responseHeaders: List) { - httpAttributesExtractorBuilder.setCapturedResponseHeaders(responseHeaders) + class ExtractorBuilder { + private var onStart: OnStartData.() -> Unit = {} + private var onEnd: OnEndData.() -> Unit = {} + + fun onStart(block: OnStartData.() -> Unit) { + onStart = block + } + + fun onEnd(block: OnEndData.() -> Unit) { + onEnd = block + } + + internal fun build(): Extractor { + return Extractor(onStart, onEnd) + } + } + + internal class Extractor(val onStart: OnStartData.() -> Unit, val onEnd: OnEndData.() -> Unit) + + data class OnStartData( + val attributes: AttributesBuilder, + val parentContext: Context, + val request: ApplicationRequest + ) + + data class OnEndData( + val attributes: AttributesBuilder, + val parentContext: Context, + val request: ApplicationRequest, + val response: ApplicationResponse?, + val error: Throwable? + ) + + @Deprecated( + "Please use method `capturedRequestHeaders`", + ReplaceWith("capturedRequestHeaders(headers)") + ) + fun setCapturedRequestHeaders(headers: List) = capturedRequestHeaders(headers) + + fun capturedRequestHeaders(vararg headers: String) = capturedRequestHeaders(headers.asIterable()) + + fun capturedRequestHeaders(headers: Iterable) { + httpAttributesExtractorBuilder.setCapturedRequestHeaders(headers.toList()) + } + + @Deprecated( + "Please use method `capturedResponseHeaders`", + ReplaceWith("capturedResponseHeaders(headers)") + ) + fun setCapturedResponseHeaders(headers: List) = capturedResponseHeaders(headers) + + fun capturedResponseHeaders(vararg headers: String) = capturedResponseHeaders(headers.asIterable()) + + fun capturedResponseHeaders(headers: Iterable) { + httpAttributesExtractorBuilder.setCapturedResponseHeaders(headers.toList()) + } + + @Deprecated( + "Please use method `knownMethods`", + ReplaceWith("knownMethods(knownMethods)") + ) + fun setKnownMethods(knownMethods: Set) = knownMethods(knownMethods) + + fun knownMethods(vararg methods: String) = knownMethods(methods.asIterable()) + + fun knownMethods(vararg methods: HttpMethod) = knownMethods(methods.asIterable()) + + @JvmName("knownMethodsJvm") + fun knownMethods(methods: Iterable) = knownMethods(methods.map { it.value }) + + fun knownMethods(methods: Iterable) { + methods.toSet().apply { + httpAttributesExtractorBuilder.setKnownMethods(this) + httpSpanNameExtractorBuilder.setKnownMethods(this) + httpServerRouteBuilder.setKnownMethods(this) + } } internal fun isOpenTelemetryInitialized(): Boolean = this::openTelemetry.isInitialized @@ -102,16 +230,14 @@ class KtorServerTracing private constructor( override fun install(pipeline: Application, configure: Configuration.() -> Unit): KtorServerTracing { val configuration = Configuration().apply(configure) - if (!configuration.isOpenTelemetryInitialized()) { - throw IllegalArgumentException("OpenTelemetry must be set") - } + require(configuration.isOpenTelemetryInitialized()) { "OpenTelemetry must be set" } val httpAttributesGetter = KtorHttpServerAttributesGetter.INSTANCE val instrumenterBuilder = Instrumenter.builder( configuration.openTelemetry, INSTRUMENTATION_NAME, - HttpSpanNameExtractor.create(httpAttributesGetter), + configuration.httpSpanNameExtractorBuilder.build() ) configuration.additionalExtractors.forEach { instrumenterBuilder.addAttributesExtractor(it) } @@ -120,7 +246,7 @@ class KtorServerTracing private constructor( setSpanStatusExtractor(configuration.statusExtractor(HttpSpanStatusExtractor.create(httpAttributesGetter))) addAttributesExtractor(configuration.httpAttributesExtractorBuilder.build()) addOperationMetrics(HttpServerMetrics.get()) - addContextCustomizer(HttpRouteHolder.create(httpAttributesGetter)) + addContextCustomizer(configuration.httpServerRouteBuilder.build()) } val instrumenter = InstrumenterUtil.buildUpstreamInstrumenter( @@ -172,10 +298,7 @@ class KtorServerTracing private constructor( } pipeline.environment.monitor.subscribe(Routing.RoutingCallStarted) { call -> - val context = call.attributes.getOrNull(contextKey) - if (context != null) { - HttpRouteHolder.updateHttpRoute(context, HttpRouteSource.SERVLET, { _, arg -> arg.route.parent.toString() }, call) - } + HttpServerRoute.update(Context.current(), HttpServerRouteSource.SERVER, { _, arg -> arg.route.parent.toString() }, call) } return feature diff --git a/instrumentation/ktor/ktor-2.0/library/src/test/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/client/KtorHttpClientTest.kt b/instrumentation/ktor/ktor-2.0/library/src/test/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/client/KtorHttpClientTest.kt index d56b6e30ddc9..ada0183d30a1 100644 --- a/instrumentation/ktor/ktor-2.0/library/src/test/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/client/KtorHttpClientTest.kt +++ b/instrumentation/ktor/ktor-2.0/library/src/test/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/client/KtorHttpClientTest.kt @@ -6,84 +6,22 @@ package io.opentelemetry.instrumentation.ktor.v2_0.client import io.ktor.client.* -import io.ktor.client.engine.cio.* -import io.ktor.client.plugins.* -import io.ktor.client.request.* -import io.ktor.http.* -import io.opentelemetry.context.Context -import io.opentelemetry.extension.kotlin.asContextElement -import io.opentelemetry.instrumentation.api.instrumenter.net.internal.NetAttributes -import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpClientTest import io.opentelemetry.instrumentation.testing.junit.http.HttpClientInstrumentationExtension -import io.opentelemetry.instrumentation.testing.junit.http.HttpClientResult -import io.opentelemetry.instrumentation.testing.junit.http.HttpClientTestOptions -import io.opentelemetry.instrumentation.testing.junit.http.HttpClientTestOptions.DEFAULT_HTTP_ATTRIBUTES -import kotlinx.coroutines.* import org.junit.jupiter.api.extension.RegisterExtension -import java.net.URI -class KtorHttpClientTest : AbstractHttpClientTest() { - - override fun buildRequest(requestMethod: String, uri: URI, requestHeaders: MutableMap) = - HttpRequestBuilder(uri.toURL()).apply { - method = HttpMethod.parse(requestMethod) - - requestHeaders.forEach { (header, value) -> headers.append(header, value) } - } - - override fun sendRequest(request: HttpRequestBuilder, method: String, uri: URI, headers: MutableMap) = runBlocking { - CLIENT.request(request).status.value - } - - override fun sendRequestWithCallback( - request: HttpRequestBuilder, - method: String, - uri: URI, - headers: MutableMap, - httpClientResult: HttpClientResult, - ) { - CoroutineScope(Dispatchers.Default + Context.current().asContextElement()).launch { - try { - val statusCode = CLIENT.request(request).status.value - httpClientResult.complete(statusCode) - } catch (e: Throwable) { - httpClientResult.complete(e) - } - } - } - - override fun configure(optionsBuilder: HttpClientTestOptions.Builder) { - with(optionsBuilder) { - disableTestReadTimeout() - // this instrumentation creates a span per each physical request - // related issue https://github.com/open-telemetry/opentelemetry-java-instrumentation/issues/5722 - disableTestRedirects() - - setHttpAttributes { DEFAULT_HTTP_ATTRIBUTES - setOf(NetAttributes.NET_PROTOCOL_NAME, NetAttributes.NET_PROTOCOL_VERSION) } - - setSingleConnectionFactory { host, port -> - KtorHttpClientSingleConnection(host, port) { installTracing() } - } - } - } +class KtorHttpClientTest : AbstractKtorHttpClientTest() { companion object { @JvmStatic @RegisterExtension private val TESTING = HttpClientInstrumentationExtension.forLibrary() + } - private val CLIENT = HttpClient(CIO) { - install(HttpRedirect) - - installTracing() - } - - private fun HttpClientConfig<*>.installTracing() { - install(KtorClientTracing) { - setOpenTelemetry(TESTING.openTelemetry) - setCapturedRequestHeaders(listOf(AbstractHttpClientTest.TEST_REQUEST_HEADER)) - setCapturedResponseHeaders(listOf(AbstractHttpClientTest.TEST_RESPONSE_HEADER)) - } + override fun HttpClientConfig<*>.installTracing() { + install(KtorClientTracing) { + setOpenTelemetry(TESTING.openTelemetry) + capturedRequestHeaders(TEST_REQUEST_HEADER) + capturedResponseHeaders(TEST_RESPONSE_HEADER) } } } diff --git a/instrumentation/ktor/ktor-2.0/library/src/test/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/server/KtorHttpServerTest.kt b/instrumentation/ktor/ktor-2.0/library/src/test/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/server/KtorHttpServerTest.kt index 177261fdeabb..97f4022c3102 100644 --- a/instrumentation/ktor/ktor-2.0/library/src/test/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/server/KtorHttpServerTest.kt +++ b/instrumentation/ktor/ktor-2.0/library/src/test/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/server/KtorHttpServerTest.kt @@ -5,138 +5,24 @@ package io.opentelemetry.instrumentation.ktor.v2_0.server -import io.ktor.http.* import io.ktor.server.application.* -import io.ktor.server.engine.* -import io.ktor.server.netty.* -import io.ktor.server.request.* -import io.ktor.server.response.* -import io.ktor.server.routing.* -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.extension.kotlin.asContextElement -import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpServerTest +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension import io.opentelemetry.instrumentation.testing.junit.http.HttpServerInstrumentationExtension -import io.opentelemetry.instrumentation.testing.junit.http.HttpServerTestOptions -import io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes -import kotlinx.coroutines.withContext import org.junit.jupiter.api.extension.RegisterExtension -import java.util.concurrent.ExecutionException -import java.util.concurrent.TimeUnit -class KtorHttpServerTest : AbstractHttpServerTest() { +class KtorHttpServerTest : AbstractKtorHttpServerTest() { companion object { @JvmStatic @RegisterExtension - val testing = HttpServerInstrumentationExtension.forLibrary() + val TESTING: InstrumentationExtension = HttpServerInstrumentationExtension.forLibrary() } - override fun setupServer(): ApplicationEngine { - return embeddedServer(Netty, port = port) { - KtorTestUtil.installOpenTelemetry(this, testing.openTelemetry) - - routing { - get(ServerEndpoint.SUCCESS.path) { - controller(ServerEndpoint.SUCCESS) { - call.respondText(ServerEndpoint.SUCCESS.body, status = HttpStatusCode.fromValue(ServerEndpoint.SUCCESS.status)) - } - } - - get(ServerEndpoint.REDIRECT.path) { - controller(ServerEndpoint.REDIRECT) { - call.respondRedirect(ServerEndpoint.REDIRECT.body) - } - } - - get(ServerEndpoint.ERROR.path) { - controller(ServerEndpoint.ERROR) { - call.respondText(ServerEndpoint.ERROR.body, status = HttpStatusCode.fromValue(ServerEndpoint.ERROR.status)) - } - } - - get(ServerEndpoint.EXCEPTION.path) { - controller(ServerEndpoint.EXCEPTION) { - throw Exception(ServerEndpoint.EXCEPTION.body) - } - } - - get("/query") { - controller(ServerEndpoint.QUERY_PARAM) { - call.respondText("some=${call.request.queryParameters["some"]}", status = HttpStatusCode.fromValue(ServerEndpoint.QUERY_PARAM.status)) - } - } - - get("/path/{id}/param") { - controller(ServerEndpoint.PATH_PARAM) { - call.respondText( - call.parameters["id"] - ?: "", - status = HttpStatusCode.fromValue(ServerEndpoint.PATH_PARAM.status), - ) - } - } - - get("/child") { - controller(ServerEndpoint.INDEXED_CHILD) { - ServerEndpoint.INDEXED_CHILD.collectSpanAttributes { call.request.queryParameters[it] } - call.respondText(ServerEndpoint.INDEXED_CHILD.body, status = HttpStatusCode.fromValue(ServerEndpoint.INDEXED_CHILD.status)) - } - } - - get("/captureHeaders") { - controller(ServerEndpoint.CAPTURE_HEADERS) { - call.response.header("X-Test-Response", call.request.header("X-Test-Request") ?: "") - call.respondText(ServerEndpoint.CAPTURE_HEADERS.body, status = HttpStatusCode.fromValue(ServerEndpoint.CAPTURE_HEADERS.status)) - } - } - } - }.start() + override fun getTesting(): InstrumentationExtension { + return TESTING } - override fun stopServer(server: ApplicationEngine) { - server.stop(0, 10, TimeUnit.SECONDS) - } - - // Copy in HttpServerTest.controller but make it a suspending function - private suspend fun controller(endpoint: ServerEndpoint, wrapped: suspend () -> Unit) { - assert(Span.current().spanContext.isValid, { "Controller should have a parent span. " }) - if (endpoint == ServerEndpoint.NOT_FOUND) { - wrapped() - } - val span = testing.openTelemetry.getTracer("test").spanBuilder("controller").setSpanKind(SpanKind.INTERNAL).startSpan() - try { - withContext(Context.current().with(span).asContextElement()) { - wrapped() - } - span.end() - } catch (e: Exception) { - span.setStatus(StatusCode.ERROR) - span.recordException(if (e is ExecutionException) e.cause ?: e else e) - span.end() - throw e - } - } - - override fun configure(options: HttpServerTestOptions) { - options.setTestPathParam(true) - - options.setHttpAttributes { - HttpServerTestOptions.DEFAULT_HTTP_ATTRIBUTES - SemanticAttributes.NET_PEER_PORT - } - - options.setExpectedHttpRoute { - when (it) { - ServerEndpoint.PATH_PARAM -> "/path/{id}/param" - else -> expectedHttpRoute(it) - } - } - - // ktor does not have a controller lifecycle so the server span ends immediately when the - // response is sent, which is before the controller span finishes. - options.setVerifyServerSpanEndTime(false) + override fun installOpenTelemetry(application: Application) { + KtorTestUtil.installOpenTelemetry(application, TESTING.openTelemetry) } } diff --git a/instrumentation/ktor/ktor-2.0/library/src/test/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/server/KtorServerSpanKindExtractorTest.kt b/instrumentation/ktor/ktor-2.0/library/src/test/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/server/KtorServerSpanKindExtractorTest.kt index 165c6afb7393..e4ab5d89391a 100644 --- a/instrumentation/ktor/ktor-2.0/library/src/test/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/server/KtorServerSpanKindExtractorTest.kt +++ b/instrumentation/ktor/ktor-2.0/library/src/test/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/server/KtorServerSpanKindExtractorTest.kt @@ -13,7 +13,6 @@ import io.ktor.server.request.* import io.ktor.server.response.* import io.ktor.server.routing.* import io.opentelemetry.api.trace.SpanKind -import io.opentelemetry.instrumentation.api.instrumenter.SpanKindExtractor import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpServerUsingTest import io.opentelemetry.instrumentation.testing.junit.http.HttpServerInstrumentationExtension @@ -60,13 +59,11 @@ class KtorServerSpanKindExtractorTest : AbstractHttpServerUsingTest - if (req.uri.startsWith("/from-pubsub/")) { - SpanKind.CONSUMER - } else { - SpanKind.SERVER - } + spanKindExtractor { + if (uri.startsWith("/from-pubsub/")) { + SpanKind.CONSUMER + } else { + SpanKind.SERVER } } } diff --git a/instrumentation/ktor/ktor-2.0/library/src/test/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/server/KtorTestUtil.kt b/instrumentation/ktor/ktor-2.0/library/src/test/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/server/KtorTestUtil.kt index e6decdaf77d6..6a799635c1c5 100644 --- a/instrumentation/ktor/ktor-2.0/library/src/test/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/server/KtorTestUtil.kt +++ b/instrumentation/ktor/ktor-2.0/library/src/test/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/server/KtorTestUtil.kt @@ -14,8 +14,8 @@ class KtorTestUtil { fun installOpenTelemetry(application: Application, openTelemetry: OpenTelemetry) { application.install(KtorServerTracing) { setOpenTelemetry(openTelemetry) - setCapturedRequestHeaders(listOf(AbstractHttpServerTest.TEST_REQUEST_HEADER)) - setCapturedResponseHeaders(listOf(AbstractHttpServerTest.TEST_RESPONSE_HEADER)) + capturedRequestHeaders(AbstractHttpServerTest.TEST_REQUEST_HEADER) + capturedResponseHeaders(AbstractHttpServerTest.TEST_RESPONSE_HEADER) } } } diff --git a/instrumentation/ktor/ktor-2.0/testing/build.gradle.kts b/instrumentation/ktor/ktor-2.0/testing/build.gradle.kts new file mode 100644 index 000000000000..cae989a04964 --- /dev/null +++ b/instrumentation/ktor/ktor-2.0/testing/build.gradle.kts @@ -0,0 +1,28 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + +plugins { + id("otel.java-conventions") + + id("org.jetbrains.kotlin.jvm") +} + +val ktorVersion = "2.0.0" + +dependencies { + api(project(":testing-common")) + + implementation("io.ktor:ktor-client-core:$ktorVersion") + implementation("io.ktor:ktor-server-core:$ktorVersion") + + implementation("io.opentelemetry:opentelemetry-extension-kotlin") + + compileOnly("org.jetbrains.kotlin:kotlin-stdlib-jdk8") + compileOnly("io.ktor:ktor-server-netty:$ktorVersion") + compileOnly("io.ktor:ktor-client-cio:$ktorVersion") +} + +kotlin { + compilerOptions { + jvmTarget.set(JvmTarget.JVM_1_8) + } +} diff --git a/instrumentation/ktor/ktor-2.0/testing/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/client/AbstractKtorHttpClientTest.kt b/instrumentation/ktor/ktor-2.0/testing/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/client/AbstractKtorHttpClientTest.kt new file mode 100644 index 000000000000..7bc94ae41208 --- /dev/null +++ b/instrumentation/ktor/ktor-2.0/testing/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/client/AbstractKtorHttpClientTest.kt @@ -0,0 +1,75 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.ktor.v2_0.client + +import io.ktor.client.* +import io.ktor.client.engine.cio.* +import io.ktor.client.plugins.* +import io.ktor.client.request.* +import io.ktor.http.* +import io.opentelemetry.context.Context +import io.opentelemetry.extension.kotlin.asContextElement +import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpClientTest +import io.opentelemetry.instrumentation.testing.junit.http.HttpClientResult +import io.opentelemetry.instrumentation.testing.junit.http.HttpClientTestOptions +import io.opentelemetry.instrumentation.testing.junit.http.HttpClientTestOptions.DEFAULT_HTTP_ATTRIBUTES +import io.opentelemetry.semconv.NetworkAttributes +import kotlinx.coroutines.* +import java.net.URI + +abstract class AbstractKtorHttpClientTest : AbstractHttpClientTest() { + + private val client = HttpClient(CIO) { + install(HttpRedirect) + + installTracing() + } + + abstract fun HttpClientConfig<*>.installTracing() + + override fun buildRequest(requestMethod: String, uri: URI, requestHeaders: MutableMap) = HttpRequestBuilder(uri.toURL()).apply { + method = HttpMethod.parse(requestMethod) + + requestHeaders.forEach { (header, value) -> headers.append(header, value) } + } + + override fun sendRequest(request: HttpRequestBuilder, method: String, uri: URI, headers: MutableMap) = runBlocking { + client.request(request).status.value + } + + override fun sendRequestWithCallback( + request: HttpRequestBuilder, + method: String, + uri: URI, + headers: MutableMap, + httpClientResult: HttpClientResult, + ) { + CoroutineScope(Dispatchers.Default + Context.current().asContextElement()).launch { + try { + val statusCode = client.request(request).status.value + httpClientResult.complete(statusCode) + } catch (e: Throwable) { + httpClientResult.complete(e) + } + } + } + + override fun configure(optionsBuilder: HttpClientTestOptions.Builder) { + with(optionsBuilder) { + disableTestReadTimeout() + // this instrumentation creates a span per each physical request + // related issue https://github.com/open-telemetry/opentelemetry-java-instrumentation/issues/5722 + disableTestRedirects() + spanEndsAfterBody() + + setHttpAttributes { DEFAULT_HTTP_ATTRIBUTES - setOf(NetworkAttributes.NETWORK_PROTOCOL_VERSION) } + + setSingleConnectionFactory { host, port -> + KtorHttpClientSingleConnection(host, port) { installTracing() } + } + } + } +} diff --git a/instrumentation/ktor/ktor-2.0/library/src/test/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/client/KtorHttpClientSingleConnection.kt b/instrumentation/ktor/ktor-2.0/testing/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/client/KtorHttpClientSingleConnection.kt similarity index 100% rename from instrumentation/ktor/ktor-2.0/library/src/test/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/client/KtorHttpClientSingleConnection.kt rename to instrumentation/ktor/ktor-2.0/testing/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/client/KtorHttpClientSingleConnection.kt diff --git a/instrumentation/ktor/ktor-2.0/testing/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/server/AbstractKtorHttpServerTest.kt b/instrumentation/ktor/ktor-2.0/testing/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/server/AbstractKtorHttpServerTest.kt new file mode 100644 index 000000000000..600973a1a29b --- /dev/null +++ b/instrumentation/ktor/ktor-2.0/testing/src/main/kotlin/io/opentelemetry/instrumentation/ktor/v2_0/server/AbstractKtorHttpServerTest.kt @@ -0,0 +1,141 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.ktor.v2_0.server + +import io.ktor.http.* +import io.ktor.server.application.* +import io.ktor.server.engine.* +import io.ktor.server.netty.* +import io.ktor.server.request.* +import io.ktor.server.response.* +import io.ktor.server.routing.* +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.extension.kotlin.asContextElement +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension +import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpServerTest +import io.opentelemetry.instrumentation.testing.junit.http.HttpServerTestOptions +import io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint +import io.opentelemetry.semconv.ServerAttributes +import kotlinx.coroutines.withContext +import java.util.concurrent.ExecutionException +import java.util.concurrent.TimeUnit + +abstract class AbstractKtorHttpServerTest : AbstractHttpServerTest() { + + abstract fun getTesting(): InstrumentationExtension + + abstract fun installOpenTelemetry(application: Application) + + override fun setupServer(): ApplicationEngine { + return embeddedServer(Netty, port = port) { + installOpenTelemetry(this) + + routing { + get(ServerEndpoint.SUCCESS.path) { + controller(ServerEndpoint.SUCCESS) { + call.respondText(ServerEndpoint.SUCCESS.body, status = HttpStatusCode.fromValue(ServerEndpoint.SUCCESS.status)) + } + } + + get(ServerEndpoint.REDIRECT.path) { + controller(ServerEndpoint.REDIRECT) { + call.respondRedirect(ServerEndpoint.REDIRECT.body) + } + } + + get(ServerEndpoint.ERROR.path) { + controller(ServerEndpoint.ERROR) { + call.respondText(ServerEndpoint.ERROR.body, status = HttpStatusCode.fromValue(ServerEndpoint.ERROR.status)) + } + } + + get(ServerEndpoint.EXCEPTION.path) { + controller(ServerEndpoint.EXCEPTION) { + throw Exception(ServerEndpoint.EXCEPTION.body) + } + } + + get("/query") { + controller(ServerEndpoint.QUERY_PARAM) { + call.respondText("some=${call.request.queryParameters["some"]}", status = HttpStatusCode.fromValue(ServerEndpoint.QUERY_PARAM.status)) + } + } + + get("/path/{id}/param") { + controller(ServerEndpoint.PATH_PARAM) { + call.respondText( + call.parameters["id"] + ?: "", + status = HttpStatusCode.fromValue(ServerEndpoint.PATH_PARAM.status), + ) + } + } + + get("/child") { + controller(ServerEndpoint.INDEXED_CHILD) { + ServerEndpoint.INDEXED_CHILD.collectSpanAttributes { call.request.queryParameters[it] } + call.respondText(ServerEndpoint.INDEXED_CHILD.body, status = HttpStatusCode.fromValue(ServerEndpoint.INDEXED_CHILD.status)) + } + } + + get("/captureHeaders") { + controller(ServerEndpoint.CAPTURE_HEADERS) { + call.response.header("X-Test-Response", call.request.header("X-Test-Request") ?: "") + call.respondText(ServerEndpoint.CAPTURE_HEADERS.body, status = HttpStatusCode.fromValue(ServerEndpoint.CAPTURE_HEADERS.status)) + } + } + } + }.start() + } + + override fun stopServer(server: ApplicationEngine) { + server.stop(0, 10, TimeUnit.SECONDS) + } + + // Copy in HttpServerTest.controller but make it a suspending function + private suspend fun controller(endpoint: ServerEndpoint, wrapped: suspend () -> Unit) { + assert(Span.current().spanContext.isValid, { "Controller should have a parent span. " }) + if (endpoint == ServerEndpoint.NOT_FOUND) { + wrapped() + } + val span = getTesting().openTelemetry.getTracer("test").spanBuilder("controller").setSpanKind(SpanKind.INTERNAL).startSpan() + try { + withContext(Context.current().with(span).asContextElement()) { + wrapped() + } + span.end() + } catch (e: Exception) { + span.setStatus(StatusCode.ERROR) + span.recordException(if (e is ExecutionException) e.cause ?: e else e) + span.end() + throw e + } + } + + override fun configure(options: HttpServerTestOptions) { + options.setTestPathParam(true) + + options.setHttpAttributes { + HttpServerTestOptions.DEFAULT_HTTP_ATTRIBUTES - ServerAttributes.SERVER_PORT + } + + options.setExpectedHttpRoute { endpoint, method -> + when (endpoint) { + ServerEndpoint.PATH_PARAM -> "/path/{id}/param" + else -> expectedHttpRoute(endpoint, method) + } + } + + // ktor does not have a controller lifecycle so the server span ends immediately when the + // response is sent, which is before the controller span finishes. + options.setVerifyServerSpanEndTime(false) + + options.setResponseCodeOnNonStandardHttpMethod(405) + } +} diff --git a/instrumentation/ktor/ktor-common/library/build.gradle.kts b/instrumentation/ktor/ktor-common/library/build.gradle.kts index 0362618225fc..e56b2eb54779 100644 --- a/instrumentation/ktor/ktor-common/library/build.gradle.kts +++ b/instrumentation/ktor/ktor-common/library/build.gradle.kts @@ -1,4 +1,5 @@ -import org.jetbrains.kotlin.gradle.tasks.KotlinCompile +import org.jetbrains.kotlin.gradle.dsl.JvmTarget +import org.jetbrains.kotlin.gradle.dsl.KotlinVersion plugins { id("otel.library-instrumentation") @@ -10,10 +11,10 @@ dependencies { testImplementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") } -tasks { - withType(KotlinCompile::class).configureEach { - kotlinOptions { - jvmTarget = "1.8" - } +kotlin { + compilerOptions { + jvmTarget.set(JvmTarget.JVM_1_8) + @Suppress("deprecation") + languageVersion.set(KotlinVersion.KOTLIN_1_4) } } diff --git a/instrumentation/ktor/ktor-common/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/IsIpAddress.kt b/instrumentation/ktor/ktor-common/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/IsIpAddress.kt index 6c02eb39ee1b..ebc49ea136d0 100644 --- a/instrumentation/ktor/ktor-common/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/IsIpAddress.kt +++ b/instrumentation/ktor/ktor-common/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/IsIpAddress.kt @@ -9,17 +9,28 @@ import java.util.regex.Pattern // Source: Regular Expressions Cookbook 2nd edition - 8.17. // Matching IPv6 Addresses -private val 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 +private val 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])\$", Pattern.CASE_INSENSITIVE, ) diff --git a/instrumentation/kubernetes-client-7.0/README.md b/instrumentation/kubernetes-client-7.0/README.md index 90b1a6713943..ce23687523c0 100644 --- a/instrumentation/kubernetes-client-7.0/README.md +++ b/instrumentation/kubernetes-client-7.0/README.md @@ -1,5 +1,5 @@ # Settings for the Kubernetes client instrumentation -| System property | Type | Default | Description | -|---|---|---|---| +| System property | Type | Default | Description | +| --------------------------------------------------------------------- | ------- | ------- | --------------------------------------------------- | | `otel.instrumentation.kubernetes-client.experimental-span-attributes` | Boolean | `false` | Enable the capture of experimental span attributes. | diff --git a/instrumentation/kubernetes-client-7.0/javaagent-unit-tests/src/test/groovy/KubernetesRequestUtilsTest.groovy b/instrumentation/kubernetes-client-7.0/javaagent-unit-tests/src/test/groovy/KubernetesRequestUtilsTest.groovy deleted file mode 100644 index 19b7f6b5b0a6..000000000000 --- a/instrumentation/kubernetes-client-7.0/javaagent-unit-tests/src/test/groovy/KubernetesRequestUtilsTest.groovy +++ /dev/null @@ -1,87 +0,0 @@ -/* - * 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/instrumentation/kubernetes-client-7.0/javaagent-unit-tests/src/test/java/io/opentelemetry/javaagent/instrumentation/kubernetesclient/KubernetesRequestUtilsTest.java b/instrumentation/kubernetes-client-7.0/javaagent-unit-tests/src/test/java/io/opentelemetry/javaagent/instrumentation/kubernetesclient/KubernetesRequestUtilsTest.java new file mode 100644 index 000000000000..f50285b5a7c2 --- /dev/null +++ b/instrumentation/kubernetes-client-7.0/javaagent-unit-tests/src/test/java/io/opentelemetry/javaagent/instrumentation/kubernetesclient/KubernetesRequestUtilsTest.java @@ -0,0 +1,192 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.kubernetesclient; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.stream.Stream; +import org.junit.jupiter.api.Test; +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; + +class KubernetesRequestUtilsTest { + + @Test + void isResourceRequest() { + assertThat(KubernetesRequestDigest.isResourceRequest("/api")).isFalse(); + assertThat(KubernetesRequestDigest.isResourceRequest("/apis")).isFalse(); + assertThat(KubernetesRequestDigest.isResourceRequest("/apis/v1")).isFalse(); + assertThat(KubernetesRequestDigest.isResourceRequest("/healthz")).isFalse(); + assertThat(KubernetesRequestDigest.isResourceRequest("/swagger.json")).isFalse(); + assertThat(KubernetesRequestDigest.isResourceRequest("/api/v1")).isFalse(); + assertThat(KubernetesRequestDigest.isResourceRequest("/api/v1/")).isFalse(); + assertThat(KubernetesRequestDigest.isResourceRequest("/apis/apps/v1")).isFalse(); + assertThat(KubernetesRequestDigest.isResourceRequest("/apis/apps/v1/")).isFalse(); + + assertThat(KubernetesRequestDigest.isResourceRequest("/apis/example.io/v1/foos")).isTrue(); + assertThat( + KubernetesRequestDigest.isResourceRequest( + "/apis/example.io/v1/namespaces/default/foos")) + .isTrue(); + assertThat(KubernetesRequestDigest.isResourceRequest("/api/v1/namespaces")).isTrue(); + assertThat(KubernetesRequestDigest.isResourceRequest("/api/v1/pods")).isTrue(); + assertThat(KubernetesRequestDigest.isResourceRequest("/api/v1/namespaces/default/pods")) + .isTrue(); + } + + @ParameterizedTest + @ArgumentsSource(ParseCoreResourceArgumentsProvider.class) + void parseCoreResource( + String urlPath, + String apiGroup, + String apiVersion, + String resource, + String subResource, + String namespace, + String name) + throws ParseKubernetesResourceException { + assertThat(KubernetesResource.parseCoreResource(urlPath).getApiGroup()).isEqualTo(apiGroup); + assertThat(KubernetesResource.parseCoreResource(urlPath).getApiVersion()).isEqualTo(apiVersion); + assertThat(KubernetesResource.parseCoreResource(urlPath).getResource()).isEqualTo(resource); + assertThat(KubernetesResource.parseCoreResource(urlPath).getSubResource()) + .isEqualTo(subResource); + assertThat(KubernetesResource.parseCoreResource(urlPath).getNamespace()).isEqualTo(namespace); + assertThat(KubernetesResource.parseCoreResource(urlPath).getName()).isEqualTo(name); + } + + @ParameterizedTest + @ArgumentsSource(ParseRegularResourceArgumentsProvider.class) + void parseRegularResource( + String urlPath, + String apiGroup, + String apiVersion, + String resource, + String subResource, + String namespace, + String name) + throws ParseKubernetesResourceException { + assertThat(KubernetesResource.parseRegularResource(urlPath).getApiGroup()).isEqualTo(apiGroup); + assertThat(KubernetesResource.parseRegularResource(urlPath).getApiVersion()) + .isEqualTo(apiVersion); + assertThat(KubernetesResource.parseRegularResource(urlPath).getResource()).isEqualTo(resource); + assertThat(KubernetesResource.parseRegularResource(urlPath).getSubResource()) + .isEqualTo(subResource); + assertThat(KubernetesResource.parseRegularResource(urlPath).getNamespace()) + .isEqualTo(namespace); + assertThat(KubernetesResource.parseRegularResource(urlPath).getName()).isEqualTo(name); + } + + @ParameterizedTest + @ArgumentsSource(K8sRequestVerbsArgumentsProvider.class) + void k8sRequestVerbs( + String httpVerb, + boolean hasNamePathParam, + boolean hasWatchParam, + KubernetesVerb kubernetesVerb) { + assertThat(KubernetesVerb.of(httpVerb, hasNamePathParam, hasWatchParam)) + .isEqualTo(kubernetesVerb); + } + + private static class K8sRequestVerbsArgumentsProvider implements ArgumentsProvider { + + @Override + public Stream provideArguments(ExtensionContext extensionContext) + throws Exception { + return Stream.of( + Arguments.of("GET", true, false, KubernetesVerb.GET), + Arguments.of("GET", false, true, KubernetesVerb.WATCH), + Arguments.of("GET", false, false, KubernetesVerb.LIST), + Arguments.of("POST", false, false, KubernetesVerb.CREATE), + Arguments.of("PUT", false, false, KubernetesVerb.UPDATE), + Arguments.of("PATCH", false, false, KubernetesVerb.PATCH), + Arguments.of("DELETE", true, false, KubernetesVerb.DELETE), + Arguments.of("DELETE", false, false, KubernetesVerb.DELETE_COLLECTION)); + } + } + + private static class ParseRegularResourceArgumentsProvider implements ArgumentsProvider { + + @Override + public Stream provideArguments(ExtensionContext extensionContext) + throws Exception { + return Stream.of( + Arguments.of("/apis/apps/v1/deployments", "apps", "v1", "deployments", null, null, null), + Arguments.of( + "/apis/apps/v1/namespaces/default/deployments", + "apps", + "v1", + "deployments", + null, + "default", + null), + Arguments.of( + "/apis/apps/v1/namespaces/default/deployments/foo", + "apps", + "v1", + "deployments", + null, + "default", + "foo"), + Arguments.of( + "/apis/apps/v1/namespaces/default/deployments/foo/status", + "apps", + "v1", + "deployments", + "status", + "default", + "foo"), + Arguments.of( + "/apis/example.io/v1alpha1/foos", "example.io", "v1alpha1", "foos", null, null, null), + Arguments.of( + "/apis/example.io/v1alpha1/namespaces/default/foos", + "example.io", + "v1alpha1", + "foos", + null, + "default", + null), + Arguments.of( + "/apis/example.io/v1alpha1/namespaces/default/foos/foo", + "example.io", + "v1alpha1", + "foos", + null, + "default", + "foo"), + Arguments.of( + "/apis/example.io/v1alpha1/namespaces/default/foos/foo/status", + "example.io", + "v1alpha1", + "foos", + "status", + "default", + "foo")); + } + } + + private static class ParseCoreResourceArgumentsProvider implements ArgumentsProvider { + + @Override + public Stream provideArguments(ExtensionContext context) { + return Stream.of( + Arguments.of("/api/v1/pods", "", "v1", "pods", null, null, null), + Arguments.of("/api/v1/namespaces/default/pods", "", "v1", "pods", null, "default", null), + Arguments.of( + "/api/v1/namespaces/default/pods/foo", "", "v1", "pods", null, "default", "foo"), + Arguments.of( + "/api/v1/namespaces/default/pods/foo/exec", + "", + "v1", + "pods", + "exec", + "default", + "foo")); + } + } +} diff --git a/instrumentation/kubernetes-client-7.0/javaagent/build.gradle.kts b/instrumentation/kubernetes-client-7.0/javaagent/build.gradle.kts index 67cf51b7e269..d3d085e6dfbe 100644 --- a/instrumentation/kubernetes-client-7.0/javaagent/build.gradle.kts +++ b/instrumentation/kubernetes-client-7.0/javaagent/build.gradle.kts @@ -14,9 +14,29 @@ muzzle { 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")) + + latestDepTestLibrary("io.kubernetes:client-java-api:19.+") +} + +testing { + suites { + val version20Test by registering(JvmTestSuite::class) { + dependencies { + if (findProperty("testLatestDeps") as Boolean) { + implementation("io.kubernetes:client-java-api:+") + } else { + implementation("io.kubernetes:client-java-api:20.0.0") + } + } + } + } +} + +tasks { + check { + dependsOn(testing.suites) + } } tasks.withType().configureEach { diff --git a/instrumentation/kubernetes-client-7.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kubernetesclient/ApiClientInstrumentation.java b/instrumentation/kubernetes-client-7.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kubernetesclient/ApiClientInstrumentation.java index 7b95f6a95893..1800e32aaca4 100644 --- a/instrumentation/kubernetes-client-7.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kubernetesclient/ApiClientInstrumentation.java +++ b/instrumentation/kubernetes-client-7.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kubernetesclient/ApiClientInstrumentation.java @@ -35,7 +35,7 @@ public ElementMatcher typeMatcher() { @Override public void transform(TypeTransformer transformer) { transformer.applyAdviceToMethod( - isPublic().and(named("buildRequest")).and(takesArguments(10)), + isPublic().and(named("buildRequest")).and(takesArguments(10).or(takesArguments(11))), this.getClass().getName() + "$BuildRequestAdvice"); transformer.applyAdviceToMethod( isPublic() diff --git a/instrumentation/kubernetes-client-7.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kubernetesclient/KubernetesClientSingletons.java b/instrumentation/kubernetes-client-7.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kubernetesclient/KubernetesClientSingletons.java index 7bf4aaef9a31..31bad69e3bbb 100644 --- a/instrumentation/kubernetes-client-7.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kubernetesclient/KubernetesClientSingletons.java +++ b/instrumentation/kubernetes-client-7.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kubernetesclient/KubernetesClientSingletons.java @@ -13,23 +13,22 @@ import io.opentelemetry.context.propagation.ContextPropagators; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; import io.opentelemetry.instrumentation.api.instrumenter.InstrumenterBuilder; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpClientAttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpSpanStatusExtractor; -import io.opentelemetry.javaagent.bootstrap.internal.CommonConfig; -import io.opentelemetry.javaagent.bootstrap.internal.InstrumentationConfig; +import io.opentelemetry.instrumentation.api.semconv.http.HttpClientAttributesExtractor; +import io.opentelemetry.instrumentation.api.semconv.http.HttpSpanStatusExtractor; +import io.opentelemetry.javaagent.bootstrap.internal.AgentCommonConfig; +import io.opentelemetry.javaagent.bootstrap.internal.AgentInstrumentationConfig; import okhttp3.Request; public class KubernetesClientSingletons { private static final Instrumenter> INSTRUMENTER; private static final boolean CAPTURE_EXPERIMENTAL_SPAN_ATTRIBUTES = - InstrumentationConfig.get() + AgentInstrumentationConfig.get() .getBoolean("otel.instrumentation.kubernetes-client.experimental-span-attributes", false); private static final ContextPropagators CONTEXT_PROPAGATORS; static { KubernetesHttpAttributesGetter httpAttributesGetter = new KubernetesHttpAttributesGetter(); - KubernetesNetAttributesGetter netAttributesGetter = new KubernetesNetAttributesGetter(); InstrumenterBuilder> instrumenterBuilder = Instrumenter.>builder( @@ -38,9 +37,10 @@ public class KubernetesClientSingletons { request -> KubernetesRequestDigest.parse(request).toString()) .setSpanStatusExtractor(HttpSpanStatusExtractor.create(httpAttributesGetter)) .addAttributesExtractor( - HttpClientAttributesExtractor.builder(httpAttributesGetter, netAttributesGetter) - .setCapturedRequestHeaders(CommonConfig.get().getClientRequestHeaders()) - .setCapturedResponseHeaders(CommonConfig.get().getClientResponseHeaders()) + HttpClientAttributesExtractor.builder(httpAttributesGetter) + .setCapturedRequestHeaders(AgentCommonConfig.get().getClientRequestHeaders()) + .setCapturedResponseHeaders(AgentCommonConfig.get().getClientResponseHeaders()) + .setKnownMethods(AgentCommonConfig.get().getKnownHttpRequestMethods()) .build()); if (CAPTURE_EXPERIMENTAL_SPAN_ATTRIBUTES) { diff --git a/instrumentation/kubernetes-client-7.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kubernetesclient/KubernetesHttpAttributesGetter.java b/instrumentation/kubernetes-client-7.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kubernetesclient/KubernetesHttpAttributesGetter.java index a909efd7fa07..16acabc0084f 100644 --- a/instrumentation/kubernetes-client-7.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kubernetesclient/KubernetesHttpAttributesGetter.java +++ b/instrumentation/kubernetes-client-7.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kubernetesclient/KubernetesHttpAttributesGetter.java @@ -8,7 +8,7 @@ import static java.util.Collections.emptyList; import io.kubernetes.client.openapi.ApiResponse; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpClientAttributesGetter; +import io.opentelemetry.instrumentation.api.semconv.http.HttpClientAttributesGetter; import java.util.List; import javax.annotation.Nullable; import okhttp3.Request; @@ -42,4 +42,14 @@ public List getHttpResponseHeader( Request request, ApiResponse apiResponse, String name) { return apiResponse.getHeaders().getOrDefault(name, emptyList()); } + + @Override + public String getServerAddress(Request request) { + return request.url().host(); + } + + @Override + public Integer getServerPort(Request request) { + return request.url().port(); + } } diff --git a/instrumentation/kubernetes-client-7.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kubernetesclient/KubernetesNetAttributesGetter.java b/instrumentation/kubernetes-client-7.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kubernetesclient/KubernetesNetAttributesGetter.java deleted file mode 100644 index a6da47e43d3f..000000000000 --- a/instrumentation/kubernetes-client-7.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kubernetesclient/KubernetesNetAttributesGetter.java +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.javaagent.instrumentation.kubernetesclient; - -import io.kubernetes.client.openapi.ApiResponse; -import io.opentelemetry.instrumentation.api.instrumenter.net.NetClientAttributesGetter; -import okhttp3.Request; - -class KubernetesNetAttributesGetter implements NetClientAttributesGetter> { - - @Override - public String getServerAddress(Request request) { - return request.url().host(); - } - - @Override - public Integer getServerPort(Request request) { - return request.url().port(); - } -} diff --git a/instrumentation/kubernetes-client-7.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kubernetesclient/KubernetesRequestDigest.java b/instrumentation/kubernetes-client-7.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kubernetesclient/KubernetesRequestDigest.java index e53504ff1a58..a3365c342224 100644 --- a/instrumentation/kubernetes-client-7.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kubernetesclient/KubernetesRequestDigest.java +++ b/instrumentation/kubernetes-client-7.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kubernetesclient/KubernetesRequestDigest.java @@ -89,7 +89,7 @@ public KubernetesVerb getVerb() { @Override public String toString() { if (isNonResourceRequest) { - return verb.value() + ' ' + urlPath; + return (verb != null ? verb.value() + ' ' : "") + urlPath; } String groupVersion; diff --git a/instrumentation/kubernetes-client-7.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kubernetesclient/KubernetesResource.java b/instrumentation/kubernetes-client-7.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kubernetesclient/KubernetesResource.java index 57b10b3db5f2..31413a790c08 100644 --- a/instrumentation/kubernetes-client-7.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kubernetesclient/KubernetesResource.java +++ b/instrumentation/kubernetes-client-7.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kubernetesclient/KubernetesResource.java @@ -12,7 +12,7 @@ class KubernetesResource { public static final Pattern CORE_RESOURCE_URL_PATH_PATTERN = Pattern.compile( - "^/api/v1(/namespaces/(?[\\w-]+))?/(?[\\w-]+)(/(?[\\w-]+))?(/(?[\\w-]+))?"); + "^/api/v1(/namespaces/(?[\\w-]+))?/(?[\\w-]+)(/(?[\\w-]+))?(/(?[\\w-]+))?(/.*)?"); public static final Pattern REGULAR_RESOURCE_URL_PATH_PATTERN = Pattern.compile( diff --git a/instrumentation/kubernetes-client-7.0/javaagent/src/test/groovy/KubernetesClientTest.groovy b/instrumentation/kubernetes-client-7.0/javaagent/src/test/groovy/KubernetesClientTest.groovy deleted file mode 100644 index f7becdd2dcbe..000000000000 --- a/instrumentation/kubernetes-client-7.0/javaagent/src/test/groovy/KubernetesClientTest.groovy +++ /dev/null @@ -1,222 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -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.api.trace.SpanKind -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 spock.lang.Shared - -import java.util.concurrent.CountDownLatch -import java.util.concurrent.atomic.AtomicReference - -import static io.opentelemetry.api.trace.SpanKind.CLIENT -import static io.opentelemetry.api.trace.StatusCode.ERROR - -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 = runWithSpan("parent") { - api.connectGetNamespacedPodProxy("name", "namespace", "path") - } - - then: - response == "42" - server.takeRequest().request().headers().get("traceparent") != null - - assertTraces(1) { - trace(0, 2) { - span(0) { - name "parent" - kind SpanKind.INTERNAL - hasNoParent() - } - 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: - runWithSpan("parent") { - api.connectGetNamespacedPodProxy("name", "namespace", "path") - } - - then: - def exception = thrown(ApiException) - server.takeRequest().request().headers().get("traceparent") != null - - assertTraces(1) { - trace(0, 2) { - span(0) { - name "parent" - kind SpanKind.INTERNAL - hasNoParent() - status ERROR - errorEvent(exception.class, exception.message) - } - 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) - - runWithSpan("parent") { - api.connectGetNamespacedPodProxyAsync("name", "namespace", "path", new ApiCallbackTemplate() { - @Override - void onSuccess(String result, int statusCode, Map> responseHeaders) { - responseBody.set(result) - latch.countDown() - runWithSpan("callback") {} - } - }) - } - - then: - latch.await() - responseBody.get() == "42" - server.takeRequest().request().headers().get("traceparent") != null - - assertTraces(1) { - trace(0, 3) { - span(0) { - name "parent" - kind SpanKind.INTERNAL - hasNoParent() - } - apiClientSpan(it, 1, "get pods/proxy", "${server.httpUri()}/api/v1/namespaces/namespace/pods/name/proxy?path=path", 200) - span(2) { - name "callback" - kind SpanKind.INTERNAL - childOf 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) - - runWithSpan("parent") { - api.connectGetNamespacedPodProxyAsync("name", "namespace", "path", new ApiCallbackTemplate() { - @Override - void onFailure(ApiException e, int statusCode, Map> responseHeaders) { - exception.set(e) - latch.countDown() - runWithSpan("callback") {} - } - }) - } - - then: - latch.await() - exception.get() != null - server.takeRequest().request().headers().get("traceparent") != null - - assertTraces(1) { - trace(0, 3) { - span(0) { - name "parent" - kind SpanKind.INTERNAL - hasNoParent() - } - apiClientSpan(it, 1, "get pods/proxy", "${server.httpUri()}/api/v1/namespaces/namespace/pods/name/proxy?path=path", 451, exception.get()) - span(2) { - name "callback" - kind SpanKind.INTERNAL - childOf 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" url - "$SemanticAttributes.HTTP_METHOD" "GET" - "$SemanticAttributes.USER_AGENT_ORIGINAL" TEST_USER_AGENT - "$SemanticAttributes.HTTP_STATUS_CODE" statusCode - "$SemanticAttributes.NET_PEER_NAME" "127.0.0.1" - "$SemanticAttributes.NET_PEER_PORT" server.httpPort() - "$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" Long - "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/instrumentation/kubernetes-client-7.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/kubernetesclient/KubernetesClientTest.java b/instrumentation/kubernetes-client-7.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/kubernetesclient/KubernetesClientTest.java new file mode 100644 index 000000000000..61c0919084fd --- /dev/null +++ b/instrumentation/kubernetes-client-7.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/kubernetesclient/KubernetesClientTest.java @@ -0,0 +1,277 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.kubernetesclient; + +import static io.opentelemetry.instrumentation.testing.util.TelemetryDataUtil.orderByRootSpanName; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; +import static org.assertj.core.api.Assertions.assertThat; + +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.api.common.AttributeKey; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.sdk.trace.data.StatusData; +import io.opentelemetry.semconv.ErrorAttributes; +import io.opentelemetry.semconv.HttpAttributes; +import io.opentelemetry.semconv.ServerAttributes; +import io.opentelemetry.semconv.UrlAttributes; +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.List; +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicReference; +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 KubernetesClientTest { + + private static final String TEST_USER_AGENT = "test-user-agent"; + + @RegisterExtension + private static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + private final MockWebServerExtension mockWebServer = new MockWebServerExtension(); + + private CoreV1Api coreV1Api; + + @BeforeEach + void beforeEach() { + mockWebServer.start(); + ApiClient apiClient = new ApiClient(); + apiClient.setUserAgent(TEST_USER_AGENT); + apiClient.setBasePath(mockWebServer.httpUri().toString()); + coreV1Api = new CoreV1Api(apiClient); + } + + @AfterEach + void afterEach() { + mockWebServer.stop(); + } + + @Test + void synchronousCall() throws ApiException { + mockWebServer.enqueue(HttpResponse.of(HttpStatus.OK, MediaType.PLAIN_TEXT_UTF_8, "42")); + String response = + testing.runWithSpan( + "parent", () -> coreV1Api.connectGetNamespacedPodProxy("name", "namespace", "path")); + + assertThat(response).isEqualTo("42"); + assertThat(mockWebServer.takeRequest().request().headers().get("traceparent")).isNotBlank(); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(), + span -> + span.hasName("get pods/proxy") + .hasKind(SpanKind.CLIENT) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo( + UrlAttributes.URL_FULL, + mockWebServer.httpUri() + + "/api/v1/namespaces/namespace/pods/name/proxy?path=path"), + equalTo(HttpAttributes.HTTP_REQUEST_METHOD, "GET"), + equalTo(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, 200), + equalTo(ServerAttributes.SERVER_ADDRESS, "127.0.0.1"), + equalTo(ServerAttributes.SERVER_PORT, mockWebServer.httpPort()), + equalTo( + AttributeKey.stringKey("kubernetes-client.namespace"), "namespace"), + equalTo(AttributeKey.stringKey("kubernetes-client.name"), "name")))); + } + + @Test + void handleErrorsInSyncCall() { + mockWebServer.enqueue( + HttpResponse.of(HttpStatus.valueOf(451), MediaType.PLAIN_TEXT_UTF_8, "42")); + ApiException exception = null; + try { + testing.runWithSpan( + "parent", + () -> { + coreV1Api.connectGetNamespacedPodProxy("name", "namespace", "path"); + }); + } catch (ApiException e) { + exception = e; + } + ApiException apiException = exception; + assertThat(apiException).isNotNull(); + assertThat(mockWebServer.takeRequest().request().headers().get("traceparent")).isNotBlank(); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("parent") + .hasKind(SpanKind.INTERNAL) + .hasNoParent() + .hasStatus(StatusData.error()) + .hasException(apiException), + span -> + span.hasName("get pods/proxy") + .hasKind(SpanKind.CLIENT) + .hasParent(trace.getSpan(0)) + .hasStatus(StatusData.error()) + .hasException(apiException) + .hasAttributesSatisfyingExactly( + equalTo( + UrlAttributes.URL_FULL, + mockWebServer.httpUri() + + "/api/v1/namespaces/namespace/pods/name/proxy?path=path"), + equalTo(HttpAttributes.HTTP_REQUEST_METHOD, "GET"), + equalTo(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, 451), + equalTo(ServerAttributes.SERVER_ADDRESS, "127.0.0.1"), + equalTo(ServerAttributes.SERVER_PORT, mockWebServer.httpPort()), + equalTo(ErrorAttributes.ERROR_TYPE, "451"), + equalTo( + AttributeKey.stringKey("kubernetes-client.namespace"), "namespace"), + equalTo(AttributeKey.stringKey("kubernetes-client.name"), "name")))); + } + + @Test + void asynchronousCall() throws ApiException, InterruptedException { + mockWebServer.enqueue(HttpResponse.of(HttpStatus.OK, MediaType.PLAIN_TEXT_UTF_8, "42")); + + AtomicReference responseBodyReference = new AtomicReference<>(); + CountDownLatch countDownLatch = new CountDownLatch(1); + + testing.runWithSpan( + "parent", + () -> { + coreV1Api.connectGetNamespacedPodProxyAsync( + "name", + "namespace", + "path", + new ApiCallbackTemplate() { + @Override + public void onSuccess( + String result, int statusCode, Map> responseHeaders) { + responseBodyReference.set(result); + countDownLatch.countDown(); + testing.runWithSpan("callback", () -> {}); + } + }); + }); + + countDownLatch.await(); + + assertThat(responseBodyReference.get()).isEqualTo("42"); + assertThat(mockWebServer.takeRequest().request().headers().get("traceparent")).isNotBlank(); + + testing.waitAndAssertSortedTraces( + orderByRootSpanName("parent", "get pods/proxy", "callback"), + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(), + span -> + span.hasName("get pods/proxy") + .hasKind(SpanKind.CLIENT) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo( + UrlAttributes.URL_FULL, + mockWebServer.httpUri() + + "/api/v1/namespaces/namespace/pods/name/proxy?path=path"), + equalTo(HttpAttributes.HTTP_REQUEST_METHOD, "GET"), + equalTo(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, 200), + equalTo(ServerAttributes.SERVER_ADDRESS, "127.0.0.1"), + equalTo(ServerAttributes.SERVER_PORT, mockWebServer.httpPort()), + equalTo( + AttributeKey.stringKey("kubernetes-client.namespace"), "namespace"), + equalTo(AttributeKey.stringKey("kubernetes-client.name"), "name")), + span -> + span.hasName("callback") + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(0)))); + } + + @Test + void handleErrorsInAsynchronousCall() throws ApiException, InterruptedException { + + mockWebServer.enqueue( + HttpResponse.of(HttpStatus.valueOf(451), MediaType.PLAIN_TEXT_UTF_8, "42")); + + AtomicReference exceptionReference = new AtomicReference<>(); + CountDownLatch countDownLatch = new CountDownLatch(1); + + testing.runWithSpan( + "parent", + () -> { + coreV1Api.connectGetNamespacedPodProxyAsync( + "name", + "namespace", + "path", + new ApiCallbackTemplate() { + @Override + public void onFailure( + ApiException e, int statusCode, Map> responseHeaders) { + exceptionReference.set(e); + countDownLatch.countDown(); + testing.runWithSpan("callback", () -> {}); + } + }); + }); + + countDownLatch.await(); + + assertThat(exceptionReference.get()).isNotNull(); + assertThat(mockWebServer.takeRequest().request().headers().get("traceparent")).isNotBlank(); + + testing.waitAndAssertSortedTraces( + orderByRootSpanName("parent", "get pods/proxy", "callback"), + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(), + span -> + span.hasName("get pods/proxy") + .hasKind(SpanKind.CLIENT) + .hasParent(trace.getSpan(0)) + .hasStatus(StatusData.error()) + .hasException(exceptionReference.get()) + .hasAttributesSatisfyingExactly( + equalTo( + UrlAttributes.URL_FULL, + mockWebServer.httpUri() + + "/api/v1/namespaces/namespace/pods/name/proxy?path=path"), + equalTo(HttpAttributes.HTTP_REQUEST_METHOD, "GET"), + equalTo(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, 451), + equalTo(ServerAttributes.SERVER_ADDRESS, "127.0.0.1"), + equalTo(ServerAttributes.SERVER_PORT, mockWebServer.httpPort()), + equalTo(ErrorAttributes.ERROR_TYPE, "451"), + equalTo( + AttributeKey.stringKey("kubernetes-client.namespace"), "namespace"), + equalTo(AttributeKey.stringKey("kubernetes-client.name"), "name")), + span -> + span.hasName("callback") + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(0)))); + } + + private static class ApiCallbackTemplate implements ApiCallback { + @Override + public void onFailure( + ApiException e, int statusCode, Map> responseHeaders) {} + + @Override + public void onSuccess( + String result, int statusCode, Map> responseHeaders) {} + + @Override + public void onUploadProgress(long bytesWritten, long contentLength, boolean done) {} + + @Override + public void onDownloadProgress(long bytesRead, long contentLength, boolean done) {} + } +} diff --git a/instrumentation/kubernetes-client-7.0/javaagent/src/version20Test/java/io/opentelemetry/javaagent/instrumentation/kubernetesclient/KubernetesClientVer20Test.java b/instrumentation/kubernetes-client-7.0/javaagent/src/version20Test/java/io/opentelemetry/javaagent/instrumentation/kubernetesclient/KubernetesClientVer20Test.java new file mode 100644 index 000000000000..00ef9fea1546 --- /dev/null +++ b/instrumentation/kubernetes-client-7.0/javaagent/src/version20Test/java/io/opentelemetry/javaagent/instrumentation/kubernetesclient/KubernetesClientVer20Test.java @@ -0,0 +1,279 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.kubernetesclient; + +import static io.opentelemetry.instrumentation.testing.util.TelemetryDataUtil.orderByRootSpanName; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; +import static org.assertj.core.api.Assertions.assertThat; + +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.api.common.AttributeKey; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.sdk.trace.data.StatusData; +import io.opentelemetry.semconv.ErrorAttributes; +import io.opentelemetry.semconv.HttpAttributes; +import io.opentelemetry.semconv.ServerAttributes; +import io.opentelemetry.semconv.UrlAttributes; +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.List; +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicReference; +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 KubernetesClientVer20Test { + + private static final String TEST_USER_AGENT = "test-user-agent"; + + @RegisterExtension + private static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + private final MockWebServerExtension mockWebServer = new MockWebServerExtension(); + + private CoreV1Api coreV1Api; + + @BeforeEach + void beforeEach() { + mockWebServer.start(); + ApiClient apiClient = new ApiClient(); + apiClient.setUserAgent(TEST_USER_AGENT); + apiClient.setBasePath(mockWebServer.httpUri().toString()); + coreV1Api = new CoreV1Api(apiClient); + } + + @AfterEach + void afterEach() { + mockWebServer.stop(); + } + + @Test + void synchronousCall() throws ApiException { + mockWebServer.enqueue(HttpResponse.of(HttpStatus.OK, MediaType.PLAIN_TEXT_UTF_8, "42")); + String response = + testing.runWithSpan( + "parent", + () -> + coreV1Api + .connectGetNamespacedPodProxyWithPath("name", "namespace", "path") + .execute()); + + assertThat(response).isEqualTo("42"); + assertThat(mockWebServer.takeRequest().request().headers().get("traceparent")).isNotBlank(); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(), + span -> + span.hasName("get pods/proxy") + .hasKind(SpanKind.CLIENT) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo( + UrlAttributes.URL_FULL, + mockWebServer.httpUri() + + "/api/v1/namespaces/namespace/pods/name/proxy/path"), + equalTo(HttpAttributes.HTTP_REQUEST_METHOD, "GET"), + equalTo(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, 200), + equalTo(ServerAttributes.SERVER_ADDRESS, "127.0.0.1"), + equalTo(ServerAttributes.SERVER_PORT, mockWebServer.httpPort()), + equalTo( + AttributeKey.stringKey("kubernetes-client.namespace"), "namespace"), + equalTo(AttributeKey.stringKey("kubernetes-client.name"), "name")))); + } + + @Test + void handleErrorsInSyncCall() { + mockWebServer.enqueue( + HttpResponse.of(HttpStatus.valueOf(451), MediaType.PLAIN_TEXT_UTF_8, "42")); + ApiException exception = null; + try { + testing.runWithSpan( + "parent", + () -> { + coreV1Api.connectGetNamespacedPodProxyWithPath("name", "namespace", "path").execute(); + }); + } catch (ApiException e) { + exception = e; + } + ApiException apiException = exception; + assertThat(apiException).isNotNull(); + assertThat(mockWebServer.takeRequest().request().headers().get("traceparent")).isNotBlank(); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("parent") + .hasKind(SpanKind.INTERNAL) + .hasNoParent() + .hasStatus(StatusData.error()) + .hasException(apiException), + span -> + span.hasName("get pods/proxy") + .hasKind(SpanKind.CLIENT) + .hasParent(trace.getSpan(0)) + .hasStatus(StatusData.error()) + .hasException(apiException) + .hasAttributesSatisfyingExactly( + equalTo( + UrlAttributes.URL_FULL, + mockWebServer.httpUri() + + "/api/v1/namespaces/namespace/pods/name/proxy/path"), + equalTo(HttpAttributes.HTTP_REQUEST_METHOD, "GET"), + equalTo(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, 451), + equalTo(ServerAttributes.SERVER_ADDRESS, "127.0.0.1"), + equalTo(ServerAttributes.SERVER_PORT, mockWebServer.httpPort()), + equalTo(ErrorAttributes.ERROR_TYPE, "451"), + equalTo( + AttributeKey.stringKey("kubernetes-client.namespace"), "namespace"), + equalTo(AttributeKey.stringKey("kubernetes-client.name"), "name")))); + } + + @Test + void asynchronousCall() throws ApiException, InterruptedException { + mockWebServer.enqueue(HttpResponse.of(HttpStatus.OK, MediaType.PLAIN_TEXT_UTF_8, "42")); + + AtomicReference responseBodyReference = new AtomicReference<>(); + CountDownLatch countDownLatch = new CountDownLatch(1); + + testing.runWithSpan( + "parent", + () -> { + coreV1Api + .connectGetNamespacedPodProxyWithPath("name", "namespace", "path") + .executeAsync( + new ApiCallbackTemplate() { + @Override + public void onSuccess( + String result, int statusCode, Map> responseHeaders) { + responseBodyReference.set(result); + countDownLatch.countDown(); + testing.runWithSpan("callback", () -> {}); + } + }); + }); + + countDownLatch.await(); + + assertThat(responseBodyReference.get()).isEqualTo("42"); + assertThat(mockWebServer.takeRequest().request().headers().get("traceparent")).isNotBlank(); + + testing.waitAndAssertSortedTraces( + orderByRootSpanName("parent", "get pods/proxy", "callback"), + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(), + span -> + span.hasName("get pods/proxy") + .hasKind(SpanKind.CLIENT) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo( + UrlAttributes.URL_FULL, + mockWebServer.httpUri() + + "/api/v1/namespaces/namespace/pods/name/proxy/path"), + equalTo(HttpAttributes.HTTP_REQUEST_METHOD, "GET"), + equalTo(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, 200), + equalTo(ServerAttributes.SERVER_ADDRESS, "127.0.0.1"), + equalTo(ServerAttributes.SERVER_PORT, mockWebServer.httpPort()), + equalTo( + AttributeKey.stringKey("kubernetes-client.namespace"), "namespace"), + equalTo(AttributeKey.stringKey("kubernetes-client.name"), "name")), + span -> + span.hasName("callback") + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(0)))); + } + + @Test + void handleErrorsInAsynchronousCall() throws ApiException, InterruptedException { + + mockWebServer.enqueue( + HttpResponse.of(HttpStatus.valueOf(451), MediaType.PLAIN_TEXT_UTF_8, "42")); + + AtomicReference exceptionReference = new AtomicReference<>(); + CountDownLatch countDownLatch = new CountDownLatch(1); + + testing.runWithSpan( + "parent", + () -> { + coreV1Api + .connectGetNamespacedPodProxyWithPath("name", "namespace", "path") + .executeAsync( + new ApiCallbackTemplate() { + @Override + public void onFailure( + ApiException e, int statusCode, Map> responseHeaders) { + exceptionReference.set(e); + countDownLatch.countDown(); + testing.runWithSpan("callback", () -> {}); + } + }); + }); + + countDownLatch.await(); + + assertThat(exceptionReference.get()).isNotNull(); + assertThat(mockWebServer.takeRequest().request().headers().get("traceparent")).isNotBlank(); + + testing.waitAndAssertSortedTraces( + orderByRootSpanName("parent", "get pods/proxy", "callback"), + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(), + span -> + span.hasName("get pods/proxy") + .hasKind(SpanKind.CLIENT) + .hasParent(trace.getSpan(0)) + .hasStatus(StatusData.error()) + .hasException(exceptionReference.get()) + .hasAttributesSatisfyingExactly( + equalTo( + UrlAttributes.URL_FULL, + mockWebServer.httpUri() + + "/api/v1/namespaces/namespace/pods/name/proxy/path"), + equalTo(HttpAttributes.HTTP_REQUEST_METHOD, "GET"), + equalTo(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, 451), + equalTo(ServerAttributes.SERVER_ADDRESS, "127.0.0.1"), + equalTo(ServerAttributes.SERVER_PORT, mockWebServer.httpPort()), + equalTo(ErrorAttributes.ERROR_TYPE, "451"), + equalTo( + AttributeKey.stringKey("kubernetes-client.namespace"), "namespace"), + equalTo(AttributeKey.stringKey("kubernetes-client.name"), "name")), + span -> + span.hasName("callback") + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(0)))); + } + + private static class ApiCallbackTemplate implements ApiCallback { + @Override + public void onFailure( + ApiException e, int statusCode, Map> responseHeaders) {} + + @Override + public void onSuccess( + String result, int statusCode, Map> responseHeaders) {} + + @Override + public void onUploadProgress(long bytesWritten, long contentLength, boolean done) {} + + @Override + public void onDownloadProgress(long bytesRead, long contentLength, boolean done) {} + } +} diff --git a/instrumentation/lettuce/README.md b/instrumentation/lettuce/README.md index d2c67a39416a..ed102ec06218 100644 --- a/instrumentation/lettuce/README.md +++ b/instrumentation/lettuce/README.md @@ -1,5 +1,6 @@ # Settings for the Lettuce instrumentation -| System property | Type | Default | Description | -|---|---|---|---| +| System property | Type | Default | Description | +|-------------------------------------------------------------|---------|---------|-----------------------------------------------------| | `otel.instrumentation.lettuce.experimental-span-attributes` | Boolean | `false` | Enable the capture of experimental span attributes. | +| `otel.instrumentation.lettuce.connection-telemetry.enabled` | Boolean | `false` | Enable the creation of Connect spans. | diff --git a/instrumentation/lettuce/lettuce-4.0/javaagent/build.gradle.kts b/instrumentation/lettuce/lettuce-4.0/javaagent/build.gradle.kts index 5fbe28c84de7..e462a677642f 100644 --- a/instrumentation/lettuce/lettuce-4.0/javaagent/build.gradle.kts +++ b/instrumentation/lettuce/lettuce-4.0/javaagent/build.gradle.kts @@ -20,5 +20,6 @@ dependencies { tasks.withType().configureEach { // TODO run tests both with and without experimental span attributes jvmArgs("-Dotel.instrumentation.lettuce.experimental-span-attributes=true") + jvmArgs("-Dotel.instrumentation.lettuce.connection-telemetry.enabled=true") usesService(gradle.sharedServices.registrations["testcontainersBuildService"].service) } diff --git a/instrumentation/lettuce/lettuce-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v4_0/InstrumentationPoints.java b/instrumentation/lettuce/lettuce-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v4_0/InstrumentationPoints.java index a6f0902635fe..514379987135 100644 --- a/instrumentation/lettuce/lettuce-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v4_0/InstrumentationPoints.java +++ b/instrumentation/lettuce/lettuce-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v4_0/InstrumentationPoints.java @@ -16,7 +16,7 @@ import com.lambdaworks.redis.protocol.RedisCommand; import io.opentelemetry.api.trace.Span; import io.opentelemetry.context.Context; -import io.opentelemetry.javaagent.bootstrap.internal.InstrumentationConfig; +import io.opentelemetry.javaagent.bootstrap.internal.AgentInstrumentationConfig; import java.util.EnumSet; import java.util.Set; import java.util.concurrent.CancellationException; @@ -24,7 +24,7 @@ public final class InstrumentationPoints { private static final boolean CAPTURE_EXPERIMENTAL_SPAN_ATTRIBUTES = - InstrumentationConfig.get() + AgentInstrumentationConfig.get() .getBoolean("otel.instrumentation.lettuce.experimental-span-attributes", false); private static final Set NON_INSTRUMENTING_COMMANDS = EnumSet.of(SHUTDOWN, DEBUG); diff --git a/instrumentation/lettuce/lettuce-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v4_0/LettuceAsyncCommandInstrumentation.java b/instrumentation/lettuce/lettuce-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v4_0/LettuceAsyncCommandInstrumentation.java index 62d57f6bee46..bd277dfae351 100644 --- a/instrumentation/lettuce/lettuce-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v4_0/LettuceAsyncCommandInstrumentation.java +++ b/instrumentation/lettuce/lettuce-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v4_0/LettuceAsyncCommandInstrumentation.java @@ -7,6 +7,7 @@ import static net.bytebuddy.matcher.ElementMatchers.isConstructor; import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.namedOneOf; import com.lambdaworks.redis.protocol.AsyncCommand; import io.opentelemetry.context.Context; @@ -31,7 +32,7 @@ public void transform(TypeTransformer transformer) { transformer.applyAdviceToMethod( isConstructor(), LettuceAsyncCommandInstrumentation.class.getName() + "$SaveContextAdvice"); transformer.applyAdviceToMethod( - named("complete").or(named("completeExceptionally")).or(named("cancel")), + namedOneOf("complete", "completeExceptionally", "cancel"), LettuceAsyncCommandInstrumentation.class.getName() + "$RestoreContextAdvice"); } diff --git a/instrumentation/lettuce/lettuce-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v4_0/LettuceConnectAttributesExtractor.java b/instrumentation/lettuce/lettuce-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v4_0/LettuceConnectAttributesExtractor.java index cb669ddc2da2..7609edbc402c 100644 --- a/instrumentation/lettuce/lettuce-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v4_0/LettuceConnectAttributesExtractor.java +++ b/instrumentation/lettuce/lettuce-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v4_0/LettuceConnectAttributesExtractor.java @@ -9,18 +9,18 @@ import io.opentelemetry.api.common.AttributesBuilder; import io.opentelemetry.context.Context; import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.opentelemetry.semconv.incubating.DbIncubatingAttributes; import javax.annotation.Nullable; final class LettuceConnectAttributesExtractor implements AttributesExtractor { @Override public void onStart(AttributesBuilder attributes, Context parentContext, RedisURI redisUri) { - attributes.put(SemanticAttributes.DB_SYSTEM, SemanticAttributes.DbSystemValues.REDIS); + attributes.put(DbIncubatingAttributes.DB_SYSTEM, DbIncubatingAttributes.DbSystemValues.REDIS); int database = redisUri.getDatabase(); if (database != 0) { - attributes.put(SemanticAttributes.DB_REDIS_DATABASE_INDEX, (long) database); + attributes.put(DbIncubatingAttributes.DB_REDIS_DATABASE_INDEX, (long) database); } } diff --git a/instrumentation/lettuce/lettuce-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v4_0/LettuceConnectNetAttributesGetter.java b/instrumentation/lettuce/lettuce-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v4_0/LettuceConnectNetworkAttributesGetter.java similarity index 67% rename from instrumentation/lettuce/lettuce-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v4_0/LettuceConnectNetAttributesGetter.java rename to instrumentation/lettuce/lettuce-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v4_0/LettuceConnectNetworkAttributesGetter.java index 9ff6a9526f4f..51f5189642e9 100644 --- a/instrumentation/lettuce/lettuce-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v4_0/LettuceConnectNetAttributesGetter.java +++ b/instrumentation/lettuce/lettuce-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v4_0/LettuceConnectNetworkAttributesGetter.java @@ -6,9 +6,9 @@ package io.opentelemetry.javaagent.instrumentation.lettuce.v4_0; import com.lambdaworks.redis.RedisURI; -import io.opentelemetry.instrumentation.api.instrumenter.net.NetClientAttributesGetter; +import io.opentelemetry.instrumentation.api.semconv.network.ServerAttributesGetter; -final class LettuceConnectNetAttributesGetter implements NetClientAttributesGetter { +final class LettuceConnectNetworkAttributesGetter implements ServerAttributesGetter { @Override public String getServerAddress(RedisURI redisUri) { diff --git a/instrumentation/lettuce/lettuce-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v4_0/LettuceDbAttributesGetter.java b/instrumentation/lettuce/lettuce-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v4_0/LettuceDbAttributesGetter.java index 31cef64648b6..0322e0e61dd9 100644 --- a/instrumentation/lettuce/lettuce-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v4_0/LettuceDbAttributesGetter.java +++ b/instrumentation/lettuce/lettuce-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v4_0/LettuceDbAttributesGetter.java @@ -6,15 +6,15 @@ package io.opentelemetry.javaagent.instrumentation.lettuce.v4_0; import com.lambdaworks.redis.protocol.RedisCommand; -import io.opentelemetry.instrumentation.api.instrumenter.db.DbClientAttributesGetter; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.opentelemetry.instrumentation.api.incubator.semconv.db.DbClientAttributesGetter; +import io.opentelemetry.semconv.incubating.DbIncubatingAttributes; import javax.annotation.Nullable; final class LettuceDbAttributesGetter implements DbClientAttributesGetter> { @Override public String getSystem(RedisCommand request) { - return SemanticAttributes.DbSystemValues.REDIS; + return DbIncubatingAttributes.DbSystemValues.REDIS; } @Override diff --git a/instrumentation/lettuce/lettuce-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v4_0/LettuceSingletons.java b/instrumentation/lettuce/lettuce-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v4_0/LettuceSingletons.java index 3e88ae511399..4c970bdf4973 100644 --- a/instrumentation/lettuce/lettuce-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v4_0/LettuceSingletons.java +++ b/instrumentation/lettuce/lettuce-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v4_0/LettuceSingletons.java @@ -10,13 +10,14 @@ import io.opentelemetry.api.GlobalOpenTelemetry; import io.opentelemetry.context.Context; import io.opentelemetry.context.ContextKey; +import io.opentelemetry.instrumentation.api.incubator.semconv.db.DbClientAttributesExtractor; +import io.opentelemetry.instrumentation.api.incubator.semconv.db.DbClientSpanNameExtractor; +import io.opentelemetry.instrumentation.api.incubator.semconv.net.PeerServiceAttributesExtractor; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; import io.opentelemetry.instrumentation.api.instrumenter.SpanKindExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.db.DbClientAttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.db.DbClientSpanNameExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.net.NetClientAttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.net.PeerServiceAttributesExtractor; -import io.opentelemetry.javaagent.bootstrap.internal.CommonConfig; +import io.opentelemetry.instrumentation.api.semconv.network.ServerAttributesExtractor; +import io.opentelemetry.javaagent.bootstrap.internal.AgentCommonConfig; +import io.opentelemetry.javaagent.bootstrap.internal.AgentInstrumentationConfig; public final class LettuceSingletons { private static final String INSTRUMENTATION_NAME = "io.opentelemetry.lettuce-4.0"; @@ -38,16 +39,20 @@ public final class LettuceSingletons { .addAttributesExtractor(DbClientAttributesExtractor.create(dbAttributesGetter)) .buildInstrumenter(SpanKindExtractor.alwaysClient()); - LettuceConnectNetAttributesGetter netAttributesGetter = new LettuceConnectNetAttributesGetter(); + LettuceConnectNetworkAttributesGetter netAttributesGetter = + new LettuceConnectNetworkAttributesGetter(); CONNECT_INSTRUMENTER = Instrumenter.builder( GlobalOpenTelemetry.get(), INSTRUMENTATION_NAME, redisUri -> "CONNECT") - .addAttributesExtractor(NetClientAttributesExtractor.create(netAttributesGetter)) + .addAttributesExtractor(ServerAttributesExtractor.create(netAttributesGetter)) .addAttributesExtractor( PeerServiceAttributesExtractor.create( - netAttributesGetter, CommonConfig.get().getPeerServiceMapping())) + netAttributesGetter, AgentCommonConfig.get().getPeerServiceResolver())) .addAttributesExtractor(new LettuceConnectAttributesExtractor()) + .setEnabled( + AgentInstrumentationConfig.get() + .getBoolean("otel.instrumentation.lettuce.connection-telemetry.enabled", false)) .buildInstrumenter(SpanKindExtractor.alwaysClient()); } diff --git a/instrumentation/lettuce/lettuce-4.0/javaagent/src/test/groovy/LettuceAsyncClientTest.groovy b/instrumentation/lettuce/lettuce-4.0/javaagent/src/test/groovy/LettuceAsyncClientTest.groovy deleted file mode 100644 index 250799fa2791..000000000000 --- a/instrumentation/lettuce/lettuce-4.0/javaagent/src/test/groovy/LettuceAsyncClientTest.groovy +++ /dev/null @@ -1,529 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -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 org.testcontainers.containers.GenericContainer -import spock.lang.Shared -import spock.util.concurrent.AsyncConditions - -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 static io.opentelemetry.api.trace.SpanKind.CLIENT -import static io.opentelemetry.api.trace.SpanKind.INTERNAL -import static io.opentelemetry.api.trace.StatusCode.ERROR - -class LettuceAsyncClientTest extends AgentInstrumentationSpecification { - 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 GenericContainer redisServer = new GenericContainer<>("redis:6.2.3-alpine").withExposedPorts(6379) - - @Shared - String host - @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 setup() { - redisServer.start() - host = redisServer.getHost() - port = redisServer.getMappedPort(6379) - dbAddr = host + ":" + port + "/" + DB_INDEX - embeddedDbUri = "redis://" + dbAddr - - incorrectPort = PortUtils.findOpenPort() - dbAddrNonExistent = host + ":" + incorrectPort + "/" + DB_INDEX - dbUriNonExistent = "redis://" + dbAddrNonExistent - - redisClient = RedisClient.create(embeddedDbUri) - 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" host - "$SemanticAttributes.NET_PEER_PORT" port - "$SemanticAttributes.DB_SYSTEM" "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" host - "$SemanticAttributes.NET_PEER_PORT" incorrectPort - "$SemanticAttributes.DB_SYSTEM" "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" "redis" - "$SemanticAttributes.DB_OPERATION" "SET" - } - } - } - } - } - - def "get command chained with thenAccept"() { - setup: - def conds = new AsyncConditions() - Consumer consumer = new Consumer() { - @Override - void accept(String res) { - runWithSpan("callback") { - conds.evaluate { - assert res == "TESTVAL" - } - } - } - } - - when: - runWithSpan("parent") { - RedisFuture redisFuture = asyncCommands.get("TESTKEY") - redisFuture.thenAccept(consumer) - } - - then: - conds.await(10) - assertTraces(1) { - trace(0, 3) { - span(0) { - name "parent" - kind INTERNAL - hasNoParent() - } - span(1) { - name "GET" - kind CLIENT - childOf(span(0)) - attributes { - "$SemanticAttributes.DB_SYSTEM" "redis" - "$SemanticAttributes.DB_OPERATION" "GET" - } - } - span(2) { - name "callback" - kind INTERNAL - childOf(span(0)) - } - } - } - } - - // 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 error) { - runWithSpan("callback1") { - conds.evaluate { - assert res == null - assert error == null - } - } - return (res == null ? successStr : res) - } - } - Function secondStage = new Function() { - @Override - Object apply(String input) { - runWithSpan("callback2") { - conds.evaluate { - assert input == successStr - } - } - return null - } - } - - when: - runWithSpan("parent") { - RedisFuture redisFuture = asyncCommands.get("NON_EXISTENT_KEY") - redisFuture.handle(firstStage).thenApply(secondStage) - } - - then: - conds.await(10) - assertTraces(1) { - trace(0, 4) { - span(0) { - name "parent" - kind INTERNAL - hasNoParent() - } - span(1) { - name "GET" - kind CLIENT - childOf(span(0)) - attributes { - "$SemanticAttributes.DB_SYSTEM" "redis" - "$SemanticAttributes.DB_OPERATION" "GET" - } - } - span(2) { - name "callback1" - kind INTERNAL - childOf(span(0)) - } - span(3) { - name "callback2" - kind INTERNAL - childOf(span(0)) - } - } - } - } - - def "command with no arguments using a biconsumer"() { - setup: - def conds = new AsyncConditions() - BiConsumer biConsumer = new BiConsumer() { - @Override - void accept(String keyRetrieved, Throwable error) { - runWithSpan("callback") { - conds.evaluate { - assert keyRetrieved != null - } - } - } - } - - when: - runWithSpan("parent") { - RedisFuture redisFuture = asyncCommands.randomkey() - redisFuture.whenCompleteAsync(biConsumer) - } - - then: - conds.await(10) - assertTraces(1) { - trace(0, 3) { - span(0) { - name "parent" - kind INTERNAL - hasNoParent() - } - span(1) { - name "RANDOMKEY" - kind CLIENT - childOf(span(0)) - attributes { - "$SemanticAttributes.DB_SYSTEM" "redis" - "$SemanticAttributes.DB_OPERATION" "RANDOMKEY" - } - } - span(2) { - name "callback" - kind INTERNAL - childOf(span(0)) - } - } - } - } - - 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 error) { - println("unexpected:" + error.toString()) - error.printStackTrace() - assert false - return null - } - }) - hmGetAllFuture.thenAccept(new Consumer>() { - @Override - void accept(Map hmGetAllResult) { - conds.evaluate { - assert testHashMap == hmGetAllResult - } - } - }) - return null - } - }) - - then: - conds.await(10) - assertTraces(2) { - trace(0, 1) { - span(0) { - name "HMSET" - kind CLIENT - attributes { - "$SemanticAttributes.DB_SYSTEM" "redis" - "$SemanticAttributes.DB_OPERATION" "HMSET" - } - } - } - trace(1, 1) { - span(0) { - name "HGETALL" - kind CLIENT - attributes { - "$SemanticAttributes.DB_SYSTEM" "redis" - "$SemanticAttributes.DB_OPERATION" "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({ - error -> - conds.evaluate { - assert error != null - assert error instanceof IllegalStateException - assert error.getMessage() == "TestException" - } - throw error - }) - - when: - // now flush and execute the command - asyncCommands.flushCommands() - redisFuture.get() - - then: - conds.await(10) - completedExceptionally == true - thrown Exception - assertTraces(1) { - trace(0, 1) { - span(0) { - name "DEL" - kind CLIENT - status ERROR - errorEvent(IllegalStateException, "TestException") - attributes { - "$SemanticAttributes.DB_SYSTEM" "redis" - "$SemanticAttributes.DB_OPERATION" "DEL" - } - } - } - } - } - - def "cancel command before it finishes"() { - setup: - asyncCommands.setAutoFlushCommands(false) - def conds = new AsyncConditions() - RedisFuture redisFuture = runWithSpan("parent") { - asyncCommands.sadd("SKEY", "1", "2") - } - redisFuture.whenCompleteAsync({ - res, error -> - runWithSpan("callback") { - conds.evaluate { - assert error != null - assert error instanceof CancellationException - } - } - }) - - when: - boolean cancelSuccess = redisFuture.cancel(true) - asyncCommands.flushCommands() - - then: - conds.await(10) - cancelSuccess == true - assertTraces(1) { - trace(0, 3) { - span(0) { - name "parent" - kind INTERNAL - hasNoParent() - } - span(1) { - name "SADD" - kind CLIENT - childOf(span(0)) - attributes { - "$SemanticAttributes.DB_SYSTEM" "redis" - "$SemanticAttributes.DB_OPERATION" "SADD" - "lettuce.command.cancelled" true - } - } - span(2) { - name "callback" - kind INTERNAL - childOf(span(0)) - } - } - } - } - - 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" "redis" - "$SemanticAttributes.DB_OPERATION" "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" "redis" - "$SemanticAttributes.DB_OPERATION" "SHUTDOWN" - } - } - } - } - } -} diff --git a/instrumentation/lettuce/lettuce-4.0/javaagent/src/test/groovy/LettuceSyncClientTest.groovy b/instrumentation/lettuce/lettuce-4.0/javaagent/src/test/groovy/LettuceSyncClientTest.groovy deleted file mode 100644 index 94cf11300121..000000000000 --- a/instrumentation/lettuce/lettuce-4.0/javaagent/src/test/groovy/LettuceSyncClientTest.groovy +++ /dev/null @@ -1,314 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -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.GenericContainer -import spock.lang.Shared - -import static io.opentelemetry.api.trace.SpanKind.CLIENT -import static io.opentelemetry.api.trace.StatusCode.ERROR - -class LettuceSyncClientTest extends AgentInstrumentationSpecification { - 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 GenericContainer redisServer = new GenericContainer<>("redis:6.2.3-alpine").withExposedPorts(6379) - - @Shared - String host - @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 setup() { - //TODO do not restart server for every test - redisServer.start() - - host = redisServer.getHost() - port = redisServer.getMappedPort(6379) - dbAddr = host + ":" + port + "/" + DB_INDEX - embeddedDbUri = "redis://" + dbAddr - - incorrectPort = PortUtils.findOpenPort() - dbAddrNonExistent = host + ":" + incorrectPort + "/" + DB_INDEX - dbUriNonExistent = "redis://" + dbAddrNonExistent - - 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() - 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" host - "$SemanticAttributes.NET_PEER_PORT" port - "$SemanticAttributes.DB_SYSTEM" "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" host - "$SemanticAttributes.NET_PEER_PORT" incorrectPort - "$SemanticAttributes.DB_SYSTEM" "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" "redis" - "$SemanticAttributes.DB_OPERATION" "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" "redis" - "$SemanticAttributes.DB_OPERATION" "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" "redis" - "$SemanticAttributes.DB_OPERATION" "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" "redis" - "$SemanticAttributes.DB_OPERATION" "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" "redis" - "$SemanticAttributes.DB_OPERATION" "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" "redis" - "$SemanticAttributes.DB_OPERATION" "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" "redis" - "$SemanticAttributes.DB_OPERATION" "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" "redis" - "$SemanticAttributes.DB_OPERATION" "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" "redis" - "$SemanticAttributes.DB_OPERATION" "SHUTDOWN" - } - } - } - } - } -} diff --git a/instrumentation/lettuce/lettuce-4.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/lettuce/v4_0/LettuceAsyncClientTest.java b/instrumentation/lettuce/lettuce-4.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/lettuce/v4_0/LettuceAsyncClientTest.java new file mode 100644 index 000000000000..74bc895b59a9 --- /dev/null +++ b/instrumentation/lettuce/lettuce-4.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/lettuce/v4_0/LettuceAsyncClientTest.java @@ -0,0 +1,531 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.lettuce.v4_0; + +import static io.opentelemetry.api.common.AttributeKey.booleanKey; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.catchException; +import static org.assertj.core.api.Assertions.catchThrowable; +import static org.awaitility.Awaitility.await; + +import com.google.common.collect.ImmutableMap; +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.StatefulRedisConnection; +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.api.common.Attributes; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.test.utils.PortUtils; +import io.opentelemetry.instrumentation.testing.internal.AutoCleanupExtension; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.sdk.trace.data.StatusData; +import io.opentelemetry.semconv.ServerAttributes; +import io.opentelemetry.semconv.incubating.DbIncubatingAttributes; +import java.util.Map; +import java.util.concurrent.CancellationException; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.function.BiConsumer; +import java.util.function.BiFunction; +import java.util.function.Consumer; +import java.util.function.Function; +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; +import org.slf4j.Logger; +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.utility.DockerImageName; + +class LettuceAsyncClientTest { + private static final Logger logger = LoggerFactory.getLogger(LettuceAsyncClientTest.class); + + @RegisterExtension + protected static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + @RegisterExtension static final AutoCleanupExtension cleanup = AutoCleanupExtension.create(); + + static final DockerImageName containerImage = DockerImageName.parse("redis:6.2.3-alpine"); + + private static final int DB_INDEX = 0; + + // Disable auto reconnect, so we do not get stray traces popping up on server shutdown + private static final ClientOptions CLIENT_OPTIONS = + new ClientOptions.Builder().autoReconnect(false).build(); + + private static final GenericContainer redisServer = + new GenericContainer<>(containerImage) + .withExposedPorts(6379) + .withLogConsumer(new Slf4jLogConsumer(logger)) + .waitingFor(Wait.forLogMessage(".*Ready to accept connections.*", 1)); + + private static String host; + private static int port; + private static int incorrectPort; + private static String dbUriNonExistent; + private static String embeddedDbUri; + + private static final ImmutableMap testHashMap = + ImmutableMap.of( + "firstname", "John", + "lastname", "Doe", + "age", "53"); + + static RedisClient redisClient; + private static StatefulRedisConnection connection; + static RedisAsyncCommands asyncCommands; + + @BeforeAll + static void setUp() { + redisServer.start(); + host = redisServer.getHost(); + port = redisServer.getMappedPort(6379); + embeddedDbUri = "redis://" + host + ":" + port + "/" + DB_INDEX; + + incorrectPort = PortUtils.findOpenPort(); + dbUriNonExistent = "redis://" + host + ":" + incorrectPort + "/" + DB_INDEX; + + redisClient = RedisClient.create(embeddedDbUri); + redisClient.setOptions(CLIENT_OPTIONS); + + connection = redisClient.connect(); + asyncCommands = connection.async(); + RedisCommands syncCommands = connection.sync(); + + syncCommands.set("TESTKEY", "TESTVAL"); + + // 1 set + 1 connect trace + testing.waitForTraces(2); + testing.clearData(); + } + + @AfterAll + static void cleanUp() { + connection.close(); + redisClient.shutdown(); + redisServer.stop(); + } + + @Test + void testConnectUsingGetOnConnectionFuture() { + RedisClient testConnectionClient = RedisClient.create(embeddedDbUri); + testConnectionClient.setOptions(CLIENT_OPTIONS); + + StatefulRedisConnection connection1 = + testConnectionClient.connect( + new Utf8StringCodec(), new RedisURI(host, port, 3, TimeUnit.SECONDS)); + cleanup.deferCleanup(connection1); + cleanup.deferCleanup(testConnectionClient::shutdown); + + assertThat(connection1).isNotNull(); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("CONNECT") + .hasKind(SpanKind.CLIENT) + .hasAttributesSatisfyingExactly( + equalTo(ServerAttributes.SERVER_ADDRESS, host), + equalTo(ServerAttributes.SERVER_PORT, port), + equalTo(DbIncubatingAttributes.DB_SYSTEM, "redis")))); + } + + @Test + void testExceptionInsideTheConnectionFuture() { + RedisClient testConnectionClient = RedisClient.create(dbUriNonExistent); + testConnectionClient.setOptions(CLIENT_OPTIONS); + cleanup.deferCleanup(testConnectionClient::shutdown); + + Exception exception = + catchException( + () -> + testConnectionClient.connect( + new Utf8StringCodec(), new RedisURI(host, incorrectPort, 3, TimeUnit.SECONDS))); + + assertThat(exception).isInstanceOf(RedisConnectionException.class); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("CONNECT") + .hasKind(SpanKind.CLIENT) + .hasStatus(StatusData.error()) + .hasException(exception) + .hasAttributesSatisfyingExactly( + equalTo(ServerAttributes.SERVER_ADDRESS, host), + equalTo(ServerAttributes.SERVER_PORT, incorrectPort), + equalTo(DbIncubatingAttributes.DB_SYSTEM, "redis")))); + } + + @Test + void testSetCommandUsingFutureGetWithTimeout() + throws ExecutionException, InterruptedException, TimeoutException { + RedisFuture redisFuture = asyncCommands.set("TESTSETKEY", "TESTSETVAL"); + String res = redisFuture.get(3, TimeUnit.SECONDS); + + assertThat(res).isEqualTo("OK"); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("SET") + .hasKind(SpanKind.CLIENT) + .hasAttributesSatisfyingExactly( + equalTo(DbIncubatingAttributes.DB_SYSTEM, "redis"), + equalTo(DbIncubatingAttributes.DB_OPERATION, "SET")))); + } + + @Test + void testCommandChainedWithThenAccept() { + CompletableFuture future = new CompletableFuture<>(); + Consumer consumer = + res -> { + testing.runWithSpan("callback", () -> assertThat(res).isEqualTo("TESTVAL")); + future.complete(res); + }; + + testing.runWithSpan( + "parent", + () -> { + RedisFuture redisFuture = asyncCommands.get("TESTKEY"); + redisFuture.thenAccept(consumer); + }); + + await().untilAsserted(() -> assertThat(future).isCompletedWithValue("TESTVAL")); + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(), + span -> + span.hasName("GET") + .hasKind(SpanKind.CLIENT) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo(DbIncubatingAttributes.DB_SYSTEM, "redis"), + equalTo(DbIncubatingAttributes.DB_OPERATION, "GET")), + span -> + span.hasName("callback") + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(0)))); + } + + // to make sure instrumentation's chained completion stages won't interfere with user's, while + // still recording spans + @Test + void getNonExistentKeyCommandWithHandleAsyncAndChainedWithThenApply() { + CompletableFuture future1 = new CompletableFuture<>(); + CompletableFuture future2 = new CompletableFuture<>(); + + String successStr = "KEY MISSING"; + + BiFunction firstStage = + (res, error) -> { + testing.runWithSpan( + "callback1", + () -> { + assertThat(res).isNull(); + assertThat(error).isNull(); + future1.complete(null); + }); + return (res == null ? successStr : res); + }; + Function secondStage = + input -> { + testing.runWithSpan( + "callback2", + () -> { + assertThat(input).isEqualTo(successStr); + future2.complete(successStr); + }); + return null; + }; + + testing.runWithSpan( + "parent", + () -> { + RedisFuture redisFuture = asyncCommands.get("NON_EXISTENT_KEY"); + redisFuture.handle(firstStage).thenApply(secondStage); + }); + + await() + .untilAsserted( + () -> { + assertThat(future1).isCompletedWithValue(null); + assertThat(future2).isCompletedWithValue(successStr); + }); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(), + span -> + span.hasName("GET") + .hasKind(SpanKind.CLIENT) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo(DbIncubatingAttributes.DB_SYSTEM, "redis"), + equalTo(DbIncubatingAttributes.DB_OPERATION, "GET")), + span -> + span.hasName("callback1") + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(0)), + span -> + span.hasName("callback2") + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(0)))); + } + + @Test + void testCommandWithNoArgumentsUsingBiconsumer() { + CompletableFuture future = new CompletableFuture<>(); + BiConsumer biConsumer = + (keyRetrieved, error) -> + testing.runWithSpan( + "callback", + () -> { + assertThat(keyRetrieved).isNotNull(); + future.complete(keyRetrieved); + }); + + testing.runWithSpan( + "parent", + () -> { + RedisFuture redisFuture = asyncCommands.randomkey(); + redisFuture.whenCompleteAsync(biConsumer); + }); + + await().untilAsserted(() -> assertThat(future).isCompleted()); + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(), + span -> + span.hasName("RANDOMKEY") + .hasKind(SpanKind.CLIENT) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo(DbIncubatingAttributes.DB_SYSTEM, "redis"), + equalTo(DbIncubatingAttributes.DB_OPERATION, "RANDOMKEY")), + span -> + span.hasName("callback") + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(0)))); + } + + @Test + void testHashSetAndThenNestApplyToHashGetall() { + CompletableFuture> future = new CompletableFuture<>(); + + RedisFuture hmsetFuture = asyncCommands.hmset("TESTHM", testHashMap); + hmsetFuture.thenApplyAsync( + setResult -> { + // Wait for 'hmset' trace to get written + testing.waitForTraces(1); + + if (!"OK".equals(setResult)) { + future.completeExceptionally(new AssertionError("Wrong hmset result " + setResult)); + return null; + } + + RedisFuture> hmGetAllFuture = asyncCommands.hgetall("TESTHM"); + hmGetAllFuture.whenComplete( + (result, exception) -> { + if (exception != null) { + future.completeExceptionally(exception); + } else { + future.complete(result); + } + }); + return null; + }); + + await().untilAsserted(() -> assertThat(future).isCompletedWithValue(testHashMap)); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("HMSET") + .hasKind(SpanKind.CLIENT) + .hasAttributesSatisfyingExactly( + equalTo(DbIncubatingAttributes.DB_SYSTEM, "redis"), + equalTo(DbIncubatingAttributes.DB_OPERATION, "HMSET"))), + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("HGETALL") + .hasKind(SpanKind.CLIENT) + .hasAttributesSatisfyingExactly( + equalTo(DbIncubatingAttributes.DB_SYSTEM, "redis"), + equalTo(DbIncubatingAttributes.DB_OPERATION, "HGETALL")))); + } + + @Test + void testCommandCompletesExceptionally() { + // turn off auto flush to complete the command exceptionally manually + asyncCommands.setAutoFlushCommands(false); + cleanup.deferCleanup(() -> asyncCommands.setAutoFlushCommands(true)); + + RedisFuture redisFuture = asyncCommands.del("key1", "key2"); + boolean completedExceptionally = + ((AsyncCommand) redisFuture) + .completeExceptionally(new IllegalStateException("TestException")); + + redisFuture.exceptionally( + error -> { + assertThat(error).isNotNull(); + assertThat(error).isInstanceOf(IllegalStateException.class); + assertThat(error.getMessage()).isEqualTo("TestException"); + throw new RuntimeException(error); + }); + + asyncCommands.flushCommands(); + Throwable thrown = catchThrowable(redisFuture::get); + + await() + .untilAsserted( + () -> { + assertThat(thrown).isInstanceOf(ExecutionException.class); + assertThat(completedExceptionally).isTrue(); + }); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("DEL") + .hasKind(SpanKind.CLIENT) + .hasStatus(StatusData.error()) + .hasException(new IllegalStateException("TestException")) + .hasAttributesSatisfyingExactly( + equalTo(DbIncubatingAttributes.DB_SYSTEM, "redis"), + equalTo(DbIncubatingAttributes.DB_OPERATION, "DEL")))); + } + + @Test + void testCommandBeforeItFinished() { + asyncCommands.setAutoFlushCommands(false); + cleanup.deferCleanup(() -> asyncCommands.setAutoFlushCommands(true)); + + RedisFuture redisFuture = + testing.runWithSpan("parent", () -> asyncCommands.sadd("SKEY", "1", "2")); + redisFuture.whenCompleteAsync( + (res, error) -> + testing.runWithSpan( + "callback", + () -> { + assertThat(error).isNotNull(); + assertThat(error).isInstanceOf(CancellationException.class); + })); + + boolean cancelSuccess = redisFuture.cancel(true); + asyncCommands.flushCommands(); + + await().untilAsserted(() -> assertThat(cancelSuccess).isTrue()); + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("parent") + .hasKind(SpanKind.INTERNAL) + .hasNoParent() + .hasAttributes(Attributes.empty()), + span -> + span.hasName("SADD") + .hasKind(SpanKind.CLIENT) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo(DbIncubatingAttributes.DB_SYSTEM, "redis"), + equalTo(DbIncubatingAttributes.DB_OPERATION, "SADD"), + equalTo(booleanKey("lettuce.command.cancelled"), true)), + span -> + span.hasName("callback") + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(0)))); + } + + @Test + void testDebugSegfaultCommandWithNoArgumentShouldProduceSpan() { + // Test Causes redis to crash therefore it needs its own container + GenericContainer server = new GenericContainer<>(containerImage).withExposedPorts(6379); + server.start(); + cleanup.deferCleanup(server::stop); + + long serverPort = server.getMappedPort(6379); + RedisClient client = RedisClient.create("redis://" + host + ":" + serverPort + "/" + DB_INDEX); + client.setOptions(CLIENT_OPTIONS); + StatefulRedisConnection connection1 = client.connect(); + cleanup.deferCleanup(connection1); + cleanup.deferCleanup(client::shutdown); + + RedisAsyncCommands commands = connection1.async(); + // 1 connect trace + testing.waitForTraces(1); + testing.clearData(); + + commands.debugSegfault(); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("DEBUG") + .hasKind(SpanKind.CLIENT) + .hasAttributesSatisfyingExactly( + equalTo(DbIncubatingAttributes.DB_SYSTEM, "redis"), + equalTo(DbIncubatingAttributes.DB_OPERATION, "DEBUG")))); + } + + @Test + void testShutdownCommandShouldProduceSpan() { + // Test Causes redis to crash therefore it needs its own container + GenericContainer server = new GenericContainer<>(containerImage).withExposedPorts(6379); + server.start(); + cleanup.deferCleanup(server::stop); + + long shutdownServerPort = server.getMappedPort(6379); + + RedisClient client = + RedisClient.create("redis://" + host + ":" + shutdownServerPort + "/" + DB_INDEX); + client.setOptions(CLIENT_OPTIONS); + StatefulRedisConnection connection1 = client.connect(); + cleanup.deferCleanup(connection1); + cleanup.deferCleanup(client::shutdown); + + RedisAsyncCommands commands = connection1.async(); + // 1 connect trace + testing.waitForTraces(1); + testing.clearData(); + + commands.shutdown(false); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("SHUTDOWN") + .hasKind(SpanKind.CLIENT) + .hasAttributesSatisfyingExactly( + equalTo(DbIncubatingAttributes.DB_SYSTEM, "redis"), + equalTo(DbIncubatingAttributes.DB_OPERATION, "SHUTDOWN")))); + } +} diff --git a/instrumentation/lettuce/lettuce-4.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/lettuce/v4_0/LettuceSyncClientTest.java b/instrumentation/lettuce/lettuce-4.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/lettuce/v4_0/LettuceSyncClientTest.java new file mode 100644 index 000000000000..49fa0ba716ee --- /dev/null +++ b/instrumentation/lettuce/lettuce-4.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/lettuce/v4_0/LettuceSyncClientTest.java @@ -0,0 +1,328 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.lettuce.v4_0; + +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.catchException; + +import com.google.common.collect.ImmutableMap; +import com.lambdaworks.redis.ClientOptions; +import com.lambdaworks.redis.RedisClient; +import com.lambdaworks.redis.RedisConnectionException; +import com.lambdaworks.redis.api.StatefulRedisConnection; +import com.lambdaworks.redis.api.sync.RedisCommands; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.test.utils.PortUtils; +import io.opentelemetry.instrumentation.testing.internal.AutoCleanupExtension; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.semconv.ServerAttributes; +import io.opentelemetry.semconv.incubating.DbIncubatingAttributes; +import java.util.Map; +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; +import org.slf4j.Logger; +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.utility.DockerImageName; + +class LettuceSyncClientTest { + private static final Logger logger = LoggerFactory.getLogger(LettuceSyncClientTest.class); + + @RegisterExtension + protected static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + @RegisterExtension static final AutoCleanupExtension cleanup = AutoCleanupExtension.create(); + + static final DockerImageName containerImage = DockerImageName.parse("redis:6.2.3-alpine"); + + private static final int DB_INDEX = 0; + + // Disable auto reconnect, so we do not get stray traces popping up on server shutdown + private static final ClientOptions CLIENT_OPTIONS = + new ClientOptions.Builder().autoReconnect(false).build(); + + private static final GenericContainer redisServer = + new GenericContainer<>(containerImage) + .withExposedPorts(6379) + .withLogConsumer(new Slf4jLogConsumer(logger)) + .waitingFor(Wait.forLogMessage(".*Ready to accept connections.*", 1)); + + private static String host; + private static int port; + private static int incorrectPort; + private static String dbUriNonExistent; + private static String embeddedDbUri; + + private static final ImmutableMap testHashMap = + ImmutableMap.of( + "firstname", "John", + "lastname", "Doe", + "age", "53"); + + static RedisClient redisClient; + + private static StatefulRedisConnection connection; + static RedisCommands syncCommands; + + @BeforeAll + static void setUp() { + redisServer.start(); + + host = redisServer.getHost(); + port = redisServer.getMappedPort(6379); + embeddedDbUri = "redis://" + host + ":" + port + "/" + DB_INDEX; + + incorrectPort = PortUtils.findOpenPort(); + dbUriNonExistent = "redis://" + host + ":" + incorrectPort + "/" + DB_INDEX; + + redisClient = RedisClient.create(embeddedDbUri); + + connection = redisClient.connect(); + syncCommands = connection.sync(); + + syncCommands.set("TESTKEY", "TESTVAL"); + syncCommands.hmset("TESTHM", testHashMap); + + // 2 sets + 1 connect trace + testing.waitForTraces(3); + testing.clearData(); + } + + @AfterAll + static void cleanUp() { + connection.close(); + redisClient.shutdown(); + redisServer.stop(); + } + + @Test + void testConnect() { + RedisClient testConnectionClient = RedisClient.create(embeddedDbUri); + testConnectionClient.setOptions(CLIENT_OPTIONS); + + StatefulRedisConnection testConnection = testConnectionClient.connect(); + cleanup.deferCleanup(() -> testConnection.close()); + cleanup.deferCleanup(testConnectionClient::shutdown); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("CONNECT") + .hasKind(SpanKind.CLIENT) + .hasAttributesSatisfyingExactly( + equalTo(ServerAttributes.SERVER_ADDRESS, host), + equalTo(ServerAttributes.SERVER_PORT, port), + equalTo(DbIncubatingAttributes.DB_SYSTEM, "redis")))); + } + + @Test + void testConnectException() { + RedisClient testConnectionClient = RedisClient.create(dbUriNonExistent); + testConnectionClient.setOptions(CLIENT_OPTIONS); + cleanup.deferCleanup(testConnectionClient::shutdown); + + Exception exception = catchException(testConnectionClient::connect); + + assertThat(exception).isInstanceOf(RedisConnectionException.class); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("CONNECT") + .hasKind(SpanKind.CLIENT) + .hasException(exception) + .hasAttributesSatisfyingExactly( + equalTo(ServerAttributes.SERVER_ADDRESS, host), + equalTo(ServerAttributes.SERVER_PORT, incorrectPort), + equalTo(DbIncubatingAttributes.DB_SYSTEM, "redis")))); + } + + @Test + void testSetCommand() { + String res = syncCommands.set("TESTSETKEY", "TESTSETVAL"); + assertThat(res).isEqualTo("OK"); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("SET") + .hasKind(SpanKind.CLIENT) + .hasAttributesSatisfyingExactly( + equalTo(DbIncubatingAttributes.DB_SYSTEM, "redis"), + equalTo(DbIncubatingAttributes.DB_OPERATION, "SET")))); + } + + @Test + void testGetCommand() { + String res = syncCommands.get("TESTKEY"); + assertThat(res).isEqualTo("TESTVAL"); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("GET") + .hasKind(SpanKind.CLIENT) + .hasAttributesSatisfyingExactly( + equalTo(DbIncubatingAttributes.DB_SYSTEM, "redis"), + equalTo(DbIncubatingAttributes.DB_OPERATION, "GET")))); + } + + @Test + void testGetNonExistentKeyCommand() { + String res = syncCommands.get("NON_EXISTENT_KEY"); + assertThat(res).isNull(); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("GET") + .hasKind(SpanKind.CLIENT) + .hasAttributesSatisfyingExactly( + equalTo(DbIncubatingAttributes.DB_SYSTEM, "redis"), + equalTo(DbIncubatingAttributes.DB_OPERATION, "GET")))); + } + + @Test + void testCommandWithNoArguments() { + String res = syncCommands.randomkey(); + assertThat(res).isNotNull(); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("RANDOMKEY") + .hasKind(SpanKind.CLIENT) + .hasAttributesSatisfyingExactly( + equalTo(DbIncubatingAttributes.DB_SYSTEM, "redis"), + equalTo(DbIncubatingAttributes.DB_OPERATION, "RANDOMKEY")))); + } + + @Test + void testListCommand() { + long res = syncCommands.lpush("TESTLIST", "TESTLIST ELEMENT"); + assertThat(res).isEqualTo(1); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("LPUSH") + .hasKind(SpanKind.CLIENT) + .hasAttributesSatisfyingExactly( + equalTo(DbIncubatingAttributes.DB_SYSTEM, "redis"), + equalTo(DbIncubatingAttributes.DB_OPERATION, "LPUSH")))); + } + + @Test + void testHashSetCommand() { + String res = syncCommands.hmset("user", testHashMap); + assertThat(res).isEqualTo("OK"); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("HMSET") + .hasKind(SpanKind.CLIENT) + .hasAttributesSatisfyingExactly( + equalTo(DbIncubatingAttributes.DB_SYSTEM, "redis"), + equalTo(DbIncubatingAttributes.DB_OPERATION, "HMSET")))); + } + + @Test + void testHashGetallCommand() { + Map res = syncCommands.hgetall("TESTHM"); + assertThat(res).isEqualTo(testHashMap); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("HGETALL") + .hasKind(SpanKind.CLIENT) + .hasAttributesSatisfyingExactly( + equalTo(DbIncubatingAttributes.DB_SYSTEM, "redis"), + equalTo(DbIncubatingAttributes.DB_OPERATION, "HGETALL")))); + } + + @Test + void testDebugSegfaultCommandWithNoArgumentShouldProduceSpan() { + // Test Causes redis to crash therefore it needs its own container + GenericContainer server = new GenericContainer<>(containerImage).withExposedPorts(6379); + server.start(); + cleanup.deferCleanup(server::stop); + + long serverPort = server.getMappedPort(6379); + RedisClient client = RedisClient.create("redis://" + host + ":" + serverPort + "/" + DB_INDEX); + client.setOptions(CLIENT_OPTIONS); + StatefulRedisConnection connection1 = client.connect(); + cleanup.deferCleanup(connection1); + cleanup.deferCleanup(client::shutdown); + + RedisCommands commands = connection1.sync(); + // 1 connect trace + testing.waitForTraces(1); + testing.clearData(); + + commands.debugSegfault(); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("DEBUG") + .hasKind(SpanKind.CLIENT) + .hasAttributesSatisfyingExactly( + equalTo(DbIncubatingAttributes.DB_SYSTEM, "redis"), + equalTo(DbIncubatingAttributes.DB_OPERATION, "DEBUG")))); + } + + @Test + void testShutdownCommandShouldProduceSpan() { + // Test Causes redis to crash therefore it needs its own container + GenericContainer server = new GenericContainer<>(containerImage).withExposedPorts(6379); + server.start(); + cleanup.deferCleanup(server::stop); + + long shutdownServerPort = server.getMappedPort(6379); + + RedisClient client = + RedisClient.create("redis://" + host + ":" + shutdownServerPort + "/" + DB_INDEX); + client.setOptions(CLIENT_OPTIONS); + StatefulRedisConnection connection1 = client.connect(); + cleanup.deferCleanup(connection1); + cleanup.deferCleanup(client::shutdown); + + RedisCommands commands = connection1.sync(); + // 1 connect trace + testing.waitForTraces(1); + testing.clearData(); + + commands.shutdown(false); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("SHUTDOWN") + .hasKind(SpanKind.CLIENT) + .hasAttributesSatisfyingExactly( + equalTo(DbIncubatingAttributes.DB_SYSTEM, "redis"), + equalTo(DbIncubatingAttributes.DB_OPERATION, "SHUTDOWN")))); + } +} diff --git a/instrumentation/lettuce/lettuce-5.0/javaagent/build.gradle.kts b/instrumentation/lettuce/lettuce-5.0/javaagent/build.gradle.kts index a3c49e15b398..0ebb0fa515ac 100644 --- a/instrumentation/lettuce/lettuce-5.0/javaagent/build.gradle.kts +++ b/instrumentation/lettuce/lettuce-5.0/javaagent/build.gradle.kts @@ -25,5 +25,6 @@ dependencies { tasks.withType().configureEach { // TODO run tests both with and without experimental span attributes jvmArgs("-Dotel.instrumentation.lettuce.experimental-span-attributes=true") + jvmArgs("-Dotel.instrumentation.lettuce.connection-telemetry.enabled=true") usesService(gradle.sharedServices.registrations["testcontainersBuildService"].service) } diff --git a/instrumentation/lettuce/lettuce-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v5_0/EndCommandAsyncBiFunction.java b/instrumentation/lettuce/lettuce-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v5_0/EndCommandAsyncBiFunction.java index 19f86beb0f6d..2baad6fd0692 100644 --- a/instrumentation/lettuce/lettuce-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v5_0/EndCommandAsyncBiFunction.java +++ b/instrumentation/lettuce/lettuce-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v5_0/EndCommandAsyncBiFunction.java @@ -10,7 +10,7 @@ import io.lettuce.core.protocol.RedisCommand; import io.opentelemetry.api.trace.Span; import io.opentelemetry.context.Context; -import io.opentelemetry.javaagent.bootstrap.internal.InstrumentationConfig; +import io.opentelemetry.javaagent.bootstrap.internal.AgentInstrumentationConfig; import java.util.concurrent.CancellationException; import java.util.function.BiFunction; @@ -27,7 +27,7 @@ public class EndCommandAsyncBiFunction implements BiFunction { private static final boolean CAPTURE_EXPERIMENTAL_SPAN_ATTRIBUTES = - InstrumentationConfig.get() + AgentInstrumentationConfig.get() .getBoolean("otel.instrumentation.lettuce.experimental-span-attributes", false); private final Context context; diff --git a/instrumentation/lettuce/lettuce-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v5_0/EndConnectAsyncBiFunction.java b/instrumentation/lettuce/lettuce-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v5_0/EndConnectAsyncBiFunction.java index 93ee72ce0b76..d179605e5ccc 100644 --- a/instrumentation/lettuce/lettuce-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v5_0/EndConnectAsyncBiFunction.java +++ b/instrumentation/lettuce/lettuce-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v5_0/EndConnectAsyncBiFunction.java @@ -10,7 +10,7 @@ import io.lettuce.core.RedisURI; import io.opentelemetry.api.trace.Span; import io.opentelemetry.context.Context; -import io.opentelemetry.javaagent.bootstrap.internal.InstrumentationConfig; +import io.opentelemetry.javaagent.bootstrap.internal.AgentInstrumentationConfig; import java.util.concurrent.CancellationException; import java.util.function.BiFunction; @@ -27,7 +27,7 @@ public class EndConnectAsyncBiFunction implements BiFunction { private static final boolean CAPTURE_EXPERIMENTAL_SPAN_ATTRIBUTES = - InstrumentationConfig.get() + AgentInstrumentationConfig.get() .getBoolean("otel.instrumentation.lettuce.experimental-span-attributes", false); private final Context context; diff --git a/instrumentation/lettuce/lettuce-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v5_0/LettuceAsyncCommandInstrumentation.java b/instrumentation/lettuce/lettuce-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v5_0/LettuceAsyncCommandInstrumentation.java index dd24b57f2601..7662aaf89de0 100644 --- a/instrumentation/lettuce/lettuce-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v5_0/LettuceAsyncCommandInstrumentation.java +++ b/instrumentation/lettuce/lettuce-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v5_0/LettuceAsyncCommandInstrumentation.java @@ -7,6 +7,7 @@ import static net.bytebuddy.matcher.ElementMatchers.isConstructor; import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.namedOneOf; import io.lettuce.core.protocol.AsyncCommand; import io.opentelemetry.context.Context; @@ -31,7 +32,7 @@ public void transform(TypeTransformer transformer) { transformer.applyAdviceToMethod( isConstructor(), LettuceAsyncCommandInstrumentation.class.getName() + "$SaveContextAdvice"); transformer.applyAdviceToMethod( - named("complete").or(named("completeExceptionally")).or(named("cancel")), + namedOneOf("complete", "completeExceptionally", "cancel"), LettuceAsyncCommandInstrumentation.class.getName() + "$RestoreContextAdvice"); } diff --git a/instrumentation/lettuce/lettuce-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v5_0/LettuceConnectAttributesExtractor.java b/instrumentation/lettuce/lettuce-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v5_0/LettuceConnectAttributesExtractor.java index 6ad11a37a72c..142e7b15abb6 100644 --- a/instrumentation/lettuce/lettuce-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v5_0/LettuceConnectAttributesExtractor.java +++ b/instrumentation/lettuce/lettuce-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v5_0/LettuceConnectAttributesExtractor.java @@ -9,18 +9,18 @@ import io.opentelemetry.api.common.AttributesBuilder; import io.opentelemetry.context.Context; import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.opentelemetry.semconv.incubating.DbIncubatingAttributes; import javax.annotation.Nullable; final class LettuceConnectAttributesExtractor implements AttributesExtractor { @Override public void onStart(AttributesBuilder attributes, Context parentContext, RedisURI redisUri) { - attributes.put(SemanticAttributes.DB_SYSTEM, SemanticAttributes.DbSystemValues.REDIS); + attributes.put(DbIncubatingAttributes.DB_SYSTEM, DbIncubatingAttributes.DbSystemValues.REDIS); int database = redisUri.getDatabase(); if (database != 0) { - attributes.put(SemanticAttributes.DB_REDIS_DATABASE_INDEX, (long) database); + attributes.put(DbIncubatingAttributes.DB_REDIS_DATABASE_INDEX, (long) database); } } diff --git a/instrumentation/lettuce/lettuce-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v5_0/LettuceConnectNetAttributesGetter.java b/instrumentation/lettuce/lettuce-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v5_0/LettuceConnectNetworkAttributesGetter.java similarity index 67% rename from instrumentation/lettuce/lettuce-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v5_0/LettuceConnectNetAttributesGetter.java rename to instrumentation/lettuce/lettuce-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v5_0/LettuceConnectNetworkAttributesGetter.java index 872d813e6287..0751707ec575 100644 --- a/instrumentation/lettuce/lettuce-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v5_0/LettuceConnectNetAttributesGetter.java +++ b/instrumentation/lettuce/lettuce-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v5_0/LettuceConnectNetworkAttributesGetter.java @@ -6,9 +6,9 @@ package io.opentelemetry.javaagent.instrumentation.lettuce.v5_0; import io.lettuce.core.RedisURI; -import io.opentelemetry.instrumentation.api.instrumenter.net.NetClientAttributesGetter; +import io.opentelemetry.instrumentation.api.semconv.network.ServerAttributesGetter; -final class LettuceConnectNetAttributesGetter implements NetClientAttributesGetter { +final class LettuceConnectNetworkAttributesGetter implements ServerAttributesGetter { @Override public String getServerAddress(RedisURI redisUri) { diff --git a/instrumentation/lettuce/lettuce-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v5_0/LettuceDbAttributesGetter.java b/instrumentation/lettuce/lettuce-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v5_0/LettuceDbAttributesGetter.java index f048d3fdcb02..a3c7050e3c79 100644 --- a/instrumentation/lettuce/lettuce-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v5_0/LettuceDbAttributesGetter.java +++ b/instrumentation/lettuce/lettuce-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v5_0/LettuceDbAttributesGetter.java @@ -6,11 +6,11 @@ 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.DbClientAttributesGetter; +import io.opentelemetry.instrumentation.api.incubator.semconv.db.DbClientAttributesGetter; +import io.opentelemetry.instrumentation.api.incubator.semconv.db.RedisCommandSanitizer; import io.opentelemetry.instrumentation.lettuce.common.LettuceArgSplitter; -import io.opentelemetry.javaagent.bootstrap.internal.CommonConfig; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.opentelemetry.javaagent.bootstrap.internal.AgentCommonConfig; +import io.opentelemetry.semconv.incubating.DbIncubatingAttributes; import java.util.Collections; import java.util.List; import javax.annotation.Nullable; @@ -18,11 +18,11 @@ final class LettuceDbAttributesGetter implements DbClientAttributesGetter> { private static final RedisCommandSanitizer sanitizer = - RedisCommandSanitizer.create(CommonConfig.get().isStatementSanitizationEnabled()); + RedisCommandSanitizer.create(AgentCommonConfig.get().isStatementSanitizationEnabled()); @Override public String getSystem(RedisCommand request) { - return SemanticAttributes.DbSystemValues.REDIS; + return DbIncubatingAttributes.DbSystemValues.REDIS; } @Override diff --git a/instrumentation/lettuce/lettuce-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v5_0/LettuceSingletons.java b/instrumentation/lettuce/lettuce-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v5_0/LettuceSingletons.java index c0727677f34b..f4de0e03fbfb 100644 --- a/instrumentation/lettuce/lettuce-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v5_0/LettuceSingletons.java +++ b/instrumentation/lettuce/lettuce-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v5_0/LettuceSingletons.java @@ -10,13 +10,14 @@ import io.opentelemetry.api.GlobalOpenTelemetry; import io.opentelemetry.context.Context; import io.opentelemetry.context.ContextKey; +import io.opentelemetry.instrumentation.api.incubator.semconv.db.DbClientAttributesExtractor; +import io.opentelemetry.instrumentation.api.incubator.semconv.db.DbClientSpanNameExtractor; +import io.opentelemetry.instrumentation.api.incubator.semconv.net.PeerServiceAttributesExtractor; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; import io.opentelemetry.instrumentation.api.instrumenter.SpanKindExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.db.DbClientAttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.db.DbClientSpanNameExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.net.NetClientAttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.net.PeerServiceAttributesExtractor; -import io.opentelemetry.javaagent.bootstrap.internal.CommonConfig; +import io.opentelemetry.instrumentation.api.semconv.network.ServerAttributesExtractor; +import io.opentelemetry.javaagent.bootstrap.internal.AgentCommonConfig; +import io.opentelemetry.javaagent.bootstrap.internal.AgentInstrumentationConfig; public final class LettuceSingletons { private static final String INSTRUMENTATION_NAME = "io.opentelemetry.lettuce-5.0"; @@ -38,17 +39,22 @@ public final class LettuceSingletons { .addAttributesExtractor(DbClientAttributesExtractor.create(dbAttributesGetter)) .buildInstrumenter(SpanKindExtractor.alwaysClient()); - LettuceConnectNetAttributesGetter connectNetAttributesGetter = - new LettuceConnectNetAttributesGetter(); + LettuceConnectNetworkAttributesGetter connectNetworkAttributesGetter = + new LettuceConnectNetworkAttributesGetter(); CONNECT_INSTRUMENTER = Instrumenter.builder( GlobalOpenTelemetry.get(), INSTRUMENTATION_NAME, redisUri -> "CONNECT") - .addAttributesExtractor(NetClientAttributesExtractor.create(connectNetAttributesGetter)) + .addAttributesExtractor( + ServerAttributesExtractor.create(connectNetworkAttributesGetter)) .addAttributesExtractor( PeerServiceAttributesExtractor.create( - connectNetAttributesGetter, CommonConfig.get().getPeerServiceMapping())) + connectNetworkAttributesGetter, + AgentCommonConfig.get().getPeerServiceResolver())) .addAttributesExtractor(new LettuceConnectAttributesExtractor()) + .setEnabled( + AgentInstrumentationConfig.get() + .getBoolean("otel.instrumentation.lettuce.connection-telemetry.enabled", false)) .buildInstrumenter(SpanKindExtractor.alwaysClient()); } diff --git a/instrumentation/lettuce/lettuce-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v5_0/rx/LettuceFluxTerminationRunnable.java b/instrumentation/lettuce/lettuce-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v5_0/rx/LettuceFluxTerminationRunnable.java index 03ca65e4f8d3..af20fc892495 100644 --- a/instrumentation/lettuce/lettuce-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v5_0/rx/LettuceFluxTerminationRunnable.java +++ b/instrumentation/lettuce/lettuce-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v5_0/rx/LettuceFluxTerminationRunnable.java @@ -10,7 +10,7 @@ import io.lettuce.core.protocol.RedisCommand; import io.opentelemetry.api.trace.Span; import io.opentelemetry.context.Context; -import io.opentelemetry.javaagent.bootstrap.internal.InstrumentationConfig; +import io.opentelemetry.javaagent.bootstrap.internal.AgentInstrumentationConfig; import java.util.function.Consumer; import java.util.logging.Logger; import org.reactivestreams.Subscription; @@ -21,7 +21,7 @@ public class LettuceFluxTerminationRunnable implements Consumer>, Runnable { private static final boolean CAPTURE_EXPERIMENTAL_SPAN_ATTRIBUTES = - InstrumentationConfig.get() + AgentInstrumentationConfig.get() .getBoolean("otel.instrumentation.lettuce.experimental-span-attributes", false); private Context context; diff --git a/instrumentation/lettuce/lettuce-5.0/javaagent/src/test/groovy/LettuceAsyncClientTest.groovy b/instrumentation/lettuce/lettuce-5.0/javaagent/src/test/groovy/LettuceAsyncClientTest.groovy deleted file mode 100644 index 9af43e6d26c8..000000000000 --- a/instrumentation/lettuce/lettuce-5.0/javaagent/src/test/groovy/LettuceAsyncClientTest.groovy +++ /dev/null @@ -1,544 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -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 org.testcontainers.containers.GenericContainer -import spock.lang.Shared -import spock.util.concurrent.AsyncConditions - -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 static io.opentelemetry.api.trace.SpanKind.CLIENT -import static io.opentelemetry.api.trace.SpanKind.INTERNAL -import static io.opentelemetry.api.trace.StatusCode.ERROR - -class LettuceAsyncClientTest extends AgentInstrumentationSpecification { - 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 GenericContainer redisServer = new GenericContainer<>("redis:6.2.3-alpine").withExposedPorts(6379) - - @Shared - String host - @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 setup() { - redisServer.start() - - host = redisServer.getHost() - port = redisServer.getMappedPort(6379) - dbAddr = host + ":" + port + "/" + DB_INDEX - embeddedDbUri = "redis://" + dbAddr - - incorrectPort = PortUtils.findOpenPort() - dbAddrNonExistent = host + ":" + incorrectPort + "/" + DB_INDEX - dbUriNonExistent = "redis://" + dbAddrNonExistent - - redisClient = RedisClient.create(embeddedDbUri) - - 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(host, 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" host - "$SemanticAttributes.NET_PEER_PORT" port - "$SemanticAttributes.DB_SYSTEM" "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(host, 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" host - "$SemanticAttributes.NET_PEER_PORT" incorrectPort - "$SemanticAttributes.DB_SYSTEM" "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" "redis" - "$SemanticAttributes.DB_STATEMENT" "SET TESTSETKEY ?" - "$SemanticAttributes.DB_OPERATION" "SET" - } - } - } - } - } - - def "get command chained with thenAccept"() { - setup: - def conds = new AsyncConditions() - Consumer consumer = new Consumer() { - @Override - void accept(String res) { - runWithSpan("callback") { - conds.evaluate { - assert res == "TESTVAL" - } - } - } - } - - when: - runWithSpan("parent") { - RedisFuture redisFuture = asyncCommands.get("TESTKEY") - redisFuture.thenAccept(consumer) - } - - then: - conds.await(10) - assertTraces(1) { - trace(0, 3) { - span(0) { - name "parent" - kind INTERNAL - hasNoParent() - } - span(1) { - name "GET" - kind CLIENT - childOf(span(0)) - attributes { - "$SemanticAttributes.DB_SYSTEM" "redis" - "$SemanticAttributes.DB_STATEMENT" "GET TESTKEY" - "$SemanticAttributes.DB_OPERATION" "GET" - } - } - span(2) { - name "callback" - kind INTERNAL - childOf(span(0)) - } - } - } - } - - // 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 error) { - runWithSpan("callback1") { - conds.evaluate { - assert res == null - assert error == null - } - } - return (res == null ? successStr : res) - } - } - Function secondStage = new Function() { - @Override - Object apply(String input) { - runWithSpan("callback2") { - conds.evaluate { - assert input == successStr - } - } - return null - } - } - - when: - runWithSpan("parent") { - RedisFuture redisFuture = asyncCommands.get("NON_EXISTENT_KEY") - redisFuture.handleAsync(firstStage).thenApply(secondStage) - } - - then: - conds.await(10) - assertTraces(1) { - trace(0, 4) { - span(0) { - name "parent" - kind INTERNAL - hasNoParent() - } - span(1) { - name "GET" - kind CLIENT - childOf(span(0)) - attributes { - "$SemanticAttributes.DB_SYSTEM" "redis" - "$SemanticAttributes.DB_STATEMENT" "GET NON_EXISTENT_KEY" - "$SemanticAttributes.DB_OPERATION" "GET" - } - } - span(2) { - name "callback1" - kind INTERNAL - childOf(span(0)) - } - span(3) { - name "callback2" - kind INTERNAL - childOf(span(0)) - } - } - } - } - - def "command with no arguments using a biconsumer"() { - setup: - def conds = new AsyncConditions() - BiConsumer biConsumer = new BiConsumer() { - @Override - void accept(String keyRetrieved, Throwable error) { - runWithSpan("callback") { - conds.evaluate { - assert keyRetrieved != null - } - } - } - } - - when: - runWithSpan("parent") { - RedisFuture redisFuture = asyncCommands.randomkey() - redisFuture.whenCompleteAsync(biConsumer) - } - - then: - conds.await(10) - assertTraces(1) { - trace(0, 3) { - span(0) { - name "parent" - kind INTERNAL - hasNoParent() - } - span(1) { - name "RANDOMKEY" - kind CLIENT - childOf(span(0)) - attributes { - "$SemanticAttributes.DB_SYSTEM" "redis" - "$SemanticAttributes.DB_STATEMENT" "RANDOMKEY" - "$SemanticAttributes.DB_OPERATION" "RANDOMKEY" - } - } - span(2) { - name "callback" - kind INTERNAL - childOf(span(0)) - } - } - } - } - - 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 error) { - println("unexpected:" + error.toString()) - error.printStackTrace() - assert false - return null - } - }) - hmGetAllFuture.thenAccept(new Consumer>() { - @Override - void accept(Map hmGetAllResult) { - conds.evaluate { - assert testHashMap == hmGetAllResult - } - } - }) - return null - } - }) - - then: - conds.await(10) - assertTraces(2) { - trace(0, 1) { - span(0) { - name "HMSET" - kind CLIENT - attributes { - "$SemanticAttributes.DB_SYSTEM" "redis" - "$SemanticAttributes.DB_STATEMENT" "HMSET TESTHM firstname ? lastname ? age ?" - "$SemanticAttributes.DB_OPERATION" "HMSET" - } - } - } - trace(1, 1) { - span(0) { - name "HGETALL" - kind CLIENT - attributes { - "$SemanticAttributes.DB_SYSTEM" "redis" - "$SemanticAttributes.DB_STATEMENT" "HGETALL TESTHM" - "$SemanticAttributes.DB_OPERATION" "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({ - error -> - conds.evaluate { - assert error != null - assert error instanceof IllegalStateException - assert error.getMessage() == "TestException" - } - throw error - }) - - when: - // now flush and execute the command - asyncCommands.flushCommands() - redisFuture.get() - - then: - conds.await(10) - completedExceptionally == true - thrown Exception - assertTraces(1) { - trace(0, 1) { - span(0) { - name "DEL" - kind CLIENT - status ERROR - errorEvent(IllegalStateException, "TestException") - attributes { - "$SemanticAttributes.DB_SYSTEM" "redis" - "$SemanticAttributes.DB_STATEMENT" "DEL key1 key2" - "$SemanticAttributes.DB_OPERATION" "DEL" - } - } - } - } - } - - def "cancel command before it finishes"() { - setup: - asyncCommands.setAutoFlushCommands(false) - def conds = new AsyncConditions() - RedisFuture redisFuture = runWithSpan("parent") { - asyncCommands.sadd("SKEY", "1", "2") - } - redisFuture.whenCompleteAsync({ - res, error -> - runWithSpan("callback") { - conds.evaluate { - assert error != null - assert error instanceof CancellationException - } - } - }) - - when: - boolean cancelSuccess = redisFuture.cancel(true) - asyncCommands.flushCommands() - - then: - conds.await(10) - cancelSuccess == true - assertTraces(1) { - trace(0, 3) { - span(0) { - name "parent" - kind INTERNAL - hasNoParent() - } - span(1) { - name "SADD" - kind CLIENT - childOf(span(0)) - attributes { - "$SemanticAttributes.DB_SYSTEM" "redis" - "$SemanticAttributes.DB_STATEMENT" "SADD SKEY ? ?" - "$SemanticAttributes.DB_OPERATION" "SADD" - "lettuce.command.cancelled" true - } - } - span(2) { - name "callback" - kind INTERNAL - childOf(span(0)) - } - } - } - } - - 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" "redis" - "$SemanticAttributes.DB_STATEMENT" "DEBUG SEGFAULT" - "$SemanticAttributes.DB_OPERATION" "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" "redis" - "$SemanticAttributes.DB_STATEMENT" "SHUTDOWN NOSAVE" - "$SemanticAttributes.DB_OPERATION" "SHUTDOWN" - } - } - } - } - } -} diff --git a/instrumentation/lettuce/lettuce-5.0/javaagent/src/test/groovy/LettuceReactiveClientTest.groovy b/instrumentation/lettuce/lettuce-5.0/javaagent/src/test/groovy/LettuceReactiveClientTest.groovy deleted file mode 100644 index 992c6e6a3014..000000000000 --- a/instrumentation/lettuce/lettuce-5.0/javaagent/src/test/groovy/LettuceReactiveClientTest.groovy +++ /dev/null @@ -1,430 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -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.semconv.trace.attributes.SemanticAttributes -import org.testcontainers.containers.GenericContainer -import reactor.core.scheduler.Schedulers -import spock.lang.Shared -import spock.util.concurrent.AsyncConditions - -import java.util.function.Consumer - -import static io.opentelemetry.api.trace.SpanKind.CLIENT -import static io.opentelemetry.api.trace.SpanKind.INTERNAL - -class LettuceReactiveClientTest extends AgentInstrumentationSpecification { - 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 GenericContainer redisServer = new GenericContainer<>("redis:6.2.3-alpine").withExposedPorts(6379) - - @Shared - String embeddedDbUri - - - RedisClient redisClient - StatefulConnection connection - RedisReactiveCommands reactiveCommands - RedisCommands syncCommands - - def setupSpec() { - } - - def setup() { - redisServer.start() - - String host = redisServer.getHost() - int port = redisServer.getMappedPort(6379) - String dbAddr = host + ":" + port + "/" + DB_INDEX - embeddedDbUri = "redis://" + dbAddr - - redisClient = RedisClient.create(embeddedDbUri) - - 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) { - runWithSpan("callback") { - conds.evaluate { - assert res == "OK" - } - } - } - } - - when: - runWithSpan("parent") { - reactiveCommands.set("TESTSETKEY", "TESTSETVAL").subscribe(consumer) - } - - then: - conds.await(10) - assertTraces(1) { - trace(0, 3) { - span(0) { - name "parent" - kind INTERNAL - hasNoParent() - } - span(1) { - name "SET" - kind CLIENT - childOf(span(0)) - attributes { - "$SemanticAttributes.DB_SYSTEM" "redis" - "$SemanticAttributes.DB_STATEMENT" "SET TESTSETKEY ?" - "$SemanticAttributes.DB_OPERATION" "SET" - } - } - span(2) { - name "callback" - kind INTERNAL - childOf(span(0)) - } - } - } - } - - 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(10) - assertTraces(1) { - trace(0, 1) { - span(0) { - name "GET" - kind CLIENT - attributes { - "$SemanticAttributes.DB_SYSTEM" "redis" - "$SemanticAttributes.DB_STATEMENT" "GET TESTKEY" - "$SemanticAttributes.DB_OPERATION" "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: - runWithSpan("parent") { - reactiveCommands.get("NON_EXISTENT_KEY").defaultIfEmpty(defaultVal).subscribe { - res -> - runWithSpan("callback") { - conds.evaluate { - assert res == defaultVal - } - } - } - } - - then: - conds.await(10) - assertTraces(1) { - trace(0, 3) { - span(0) { - name "parent" - kind INTERNAL - hasNoParent() - } - span(1) { - name "GET" - kind CLIENT - childOf(span(0)) - attributes { - "$SemanticAttributes.DB_SYSTEM" "redis" - "$SemanticAttributes.DB_STATEMENT" "GET NON_EXISTENT_KEY" - "$SemanticAttributes.DB_OPERATION" "GET" - } - } - span(2) { - name "callback" - kind INTERNAL - childOf(span(0)) - } - } - } - - } - - def "command with no arguments"() { - setup: - def conds = new AsyncConditions() - - when: - reactiveCommands.randomkey().subscribe { - res -> - conds.evaluate { - assert res == "TESTKEY" - } - } - - then: - conds.await(10) - assertTraces(1) { - trace(0, 1) { - span(0) { - name "RANDOMKEY" - kind CLIENT - attributes { - "$SemanticAttributes.DB_SYSTEM" "redis" - "$SemanticAttributes.DB_STATEMENT" "RANDOMKEY" - "$SemanticAttributes.DB_OPERATION" "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" "redis" - "$SemanticAttributes.DB_STATEMENT" "COMMAND" - "$SemanticAttributes.DB_OPERATION" "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" "redis" - "$SemanticAttributes.DB_STATEMENT" "COMMAND" - "$SemanticAttributes.DB_OPERATION" "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" "redis" - "$SemanticAttributes.DB_STATEMENT" "DEBUG SEGFAULT" - "$SemanticAttributes.DB_OPERATION" "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" "redis" - "$SemanticAttributes.DB_STATEMENT" "SHUTDOWN NOSAVE" - "$SemanticAttributes.DB_OPERATION" "SHUTDOWN" - } - } - } - } - } - - def "blocking subscriber"() { - when: - runWithSpan("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" "redis" - "$SemanticAttributes.DB_STATEMENT" "SET a ?" - "$SemanticAttributes.DB_OPERATION" "SET" - } - } - span(2) { - name "GET" - kind CLIENT - childOf span(0) - attributes { - "$SemanticAttributes.DB_SYSTEM" "redis" - "$SemanticAttributes.DB_STATEMENT" "GET a" - "$SemanticAttributes.DB_OPERATION" "GET" - } - } - } - } - } - - def "async subscriber"() { - when: - runWithSpan("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" "redis" - "$SemanticAttributes.DB_STATEMENT" "SET a ?" - "$SemanticAttributes.DB_OPERATION" "SET" - } - } - span(2) { - name "GET" - kind CLIENT - childOf span(0) - attributes { - "$SemanticAttributes.DB_SYSTEM" "redis" - "$SemanticAttributes.DB_STATEMENT" "GET a" - "$SemanticAttributes.DB_OPERATION" "GET" - } - } - } - } - } - - def "async subscriber with specific thread pool"() { - when: - runWithSpan("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" "redis" - "$SemanticAttributes.DB_STATEMENT" "SET a ?" - "$SemanticAttributes.DB_OPERATION" "SET" - } - } - span(2) { - name "GET" - kind CLIENT - childOf span(0) - attributes { - "$SemanticAttributes.DB_SYSTEM" "redis" - "$SemanticAttributes.DB_STATEMENT" "GET a" - "$SemanticAttributes.DB_OPERATION" "GET" - } - } - } - } - } -} diff --git a/instrumentation/lettuce/lettuce-5.0/javaagent/src/test/groovy/LettuceSyncClientTest.groovy b/instrumentation/lettuce/lettuce-5.0/javaagent/src/test/groovy/LettuceSyncClientTest.groovy deleted file mode 100644 index 36037f5f30b8..000000000000 --- a/instrumentation/lettuce/lettuce-5.0/javaagent/src/test/groovy/LettuceSyncClientTest.groovy +++ /dev/null @@ -1,323 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -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.GenericContainer -import spock.lang.Shared - -import static io.opentelemetry.api.trace.SpanKind.CLIENT -import static io.opentelemetry.api.trace.StatusCode.ERROR - -class LettuceSyncClientTest extends AgentInstrumentationSpecification { - 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 GenericContainer redisServer = new GenericContainer<>("redis:6.2.3-alpine").withExposedPorts(6379) - - @Shared - String host - @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 setup() { - redisServer.start() - - host = redisServer.getHost() - port = redisServer.getMappedPort(6379) - dbAddr = host + ":" + port + "/" + DB_INDEX - embeddedDbUri = "redis://" + dbAddr - - incorrectPort = PortUtils.findOpenPort() - dbAddrNonExistent = host + ":" + incorrectPort + "/" + DB_INDEX - dbUriNonExistent = "redis://" + dbAddrNonExistent - - 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() - 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" host - "$SemanticAttributes.NET_PEER_PORT" port - "$SemanticAttributes.DB_SYSTEM" "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" host - "$SemanticAttributes.NET_PEER_PORT" incorrectPort - "$SemanticAttributes.DB_SYSTEM" "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" "redis" - "$SemanticAttributes.DB_STATEMENT" "SET TESTSETKEY ?" - "$SemanticAttributes.DB_OPERATION" "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" "redis" - "$SemanticAttributes.DB_STATEMENT" "GET TESTKEY" - "$SemanticAttributes.DB_OPERATION" "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" "redis" - "$SemanticAttributes.DB_STATEMENT" "GET NON_EXISTENT_KEY" - "$SemanticAttributes.DB_OPERATION" "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" "redis" - "$SemanticAttributes.DB_STATEMENT" "RANDOMKEY" - "$SemanticAttributes.DB_OPERATION" "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" "redis" - "$SemanticAttributes.DB_STATEMENT" "LPUSH TESTLIST ?" - "$SemanticAttributes.DB_OPERATION" "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" "redis" - "$SemanticAttributes.DB_STATEMENT" "HMSET user firstname ? lastname ? age ?" - "$SemanticAttributes.DB_OPERATION" "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" "redis" - "$SemanticAttributes.DB_STATEMENT" "HGETALL TESTHM" - "$SemanticAttributes.DB_OPERATION" "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" "redis" - "$SemanticAttributes.DB_STATEMENT" "DEBUG SEGFAULT" - "$SemanticAttributes.DB_OPERATION" "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" "redis" - "$SemanticAttributes.DB_STATEMENT" "SHUTDOWN NOSAVE" - "$SemanticAttributes.DB_OPERATION" "SHUTDOWN" - } - } - } - } - } -} diff --git a/instrumentation/lettuce/lettuce-5.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/lettuce/v5_0/AbstractLettuceClientTest.java b/instrumentation/lettuce/lettuce-5.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/lettuce/v5_0/AbstractLettuceClientTest.java new file mode 100644 index 000000000000..18a58f875881 --- /dev/null +++ b/instrumentation/lettuce/lettuce-5.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/lettuce/v5_0/AbstractLettuceClientTest.java @@ -0,0 +1,81 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.lettuce.v5_0; + +import io.lettuce.core.ClientOptions; +import io.lettuce.core.RedisClient; +import io.lettuce.core.api.StatefulRedisConnection; +import io.opentelemetry.instrumentation.testing.internal.AutoCleanupExtension; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.slf4j.Logger; +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.utility.DockerImageName; + +abstract class AbstractLettuceClientTest { + + protected static final Logger logger = LoggerFactory.getLogger(AbstractLettuceClientTest.class); + + @RegisterExtension + protected static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + @RegisterExtension static final AutoCleanupExtension cleanup = AutoCleanupExtension.create(); + + protected static final int DB_INDEX = 0; + + // Disable autoreconnect so we do not get stray traces popping up on server shutdown + protected static final ClientOptions CLIENT_OPTIONS = + ClientOptions.builder().autoReconnect(false).build(); + + static final DockerImageName containerImage = DockerImageName.parse("redis:6.2.3-alpine"); + + protected static final GenericContainer redisServer = + new GenericContainer<>(containerImage) + .withExposedPorts(6379) + .withLogConsumer(new Slf4jLogConsumer(logger)) + .waitingFor(Wait.forLogMessage(".*Ready to accept connections.*", 1)); + + protected static RedisClient redisClient; + + protected static StatefulRedisConnection connection; + + protected static String ip; + + protected static String host; + + protected static int port; + + protected static String embeddedDbUri; + + protected static StatefulRedisConnection newContainerConnection() { + GenericContainer server = + new GenericContainer<>(containerImage) + .withExposedPorts(6379) + .withLogConsumer(new Slf4jLogConsumer(logger)) + .waitingFor(Wait.forLogMessage(".*Ready to accept connections.*", 1)); + server.start(); + cleanup.deferCleanup(server::stop); + + long serverPort = server.getMappedPort(6379); + + RedisClient client = RedisClient.create("redis://" + host + ":" + serverPort + "/" + DB_INDEX); + client.setOptions(CLIENT_OPTIONS); + cleanup.deferCleanup(client::shutdown); + + StatefulRedisConnection statefulConnection = client.connect(); + cleanup.deferCleanup(statefulConnection); + + // 1 connect trace + testing.waitForTraces(1); + testing.clearData(); + + return statefulConnection; + } +} diff --git a/instrumentation/lettuce/lettuce-5.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/lettuce/v5_0/LettuceAsyncClientTest.java b/instrumentation/lettuce/lettuce-5.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/lettuce/v5_0/LettuceAsyncClientTest.java new file mode 100644 index 000000000000..408f2ba72d91 --- /dev/null +++ b/instrumentation/lettuce/lettuce-5.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/lettuce/v5_0/LettuceAsyncClientTest.java @@ -0,0 +1,506 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.lettuce.v5_0; + +import static io.opentelemetry.api.common.AttributeKey.booleanKey; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.satisfies; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.catchException; +import static org.assertj.core.api.Assertions.catchThrowable; +import static org.awaitility.Awaitility.await; + +import com.google.common.collect.ImmutableMap; +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.StatefulRedisConnection; +import io.lettuce.core.api.async.RedisAsyncCommands; +import io.lettuce.core.api.sync.RedisCommands; +import io.lettuce.core.codec.Utf8StringCodec; +import io.lettuce.core.protocol.AsyncCommand; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.test.utils.PortUtils; +import io.opentelemetry.sdk.trace.data.StatusData; +import io.opentelemetry.semconv.ServerAttributes; +import io.opentelemetry.semconv.incubating.DbIncubatingAttributes; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.Map; +import java.util.concurrent.CancellationException; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.function.BiConsumer; +import java.util.function.BiFunction; +import java.util.function.Consumer; +import java.util.function.Function; +import org.assertj.core.api.AbstractAssert; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +class LettuceAsyncClientTest extends AbstractLettuceClientTest { + private static int incorrectPort; + private static String dbUriNonExistent; + + private static final ImmutableMap testHashMap = + ImmutableMap.of( + "firstname", "John", + "lastname", "Doe", + "age", "53"); + + private static RedisAsyncCommands asyncCommands; + + @BeforeAll + static void setUp() throws UnknownHostException { + redisServer.start(); + + host = redisServer.getHost(); + ip = InetAddress.getByName(host).getHostAddress(); + port = redisServer.getMappedPort(6379); + embeddedDbUri = "redis://" + host + ":" + port + "/" + DB_INDEX; + + incorrectPort = PortUtils.findOpenPort(); + dbUriNonExistent = "redis://" + host + ":" + incorrectPort + "/" + DB_INDEX; + + redisClient = RedisClient.create(embeddedDbUri); + redisClient.setOptions(CLIENT_OPTIONS); + + connection = redisClient.connect(); + asyncCommands = connection.async(); + RedisCommands syncCommands = connection.sync(); + + syncCommands.set("TESTKEY", "TESTVAL"); + + // 1 set + 1 connect trace + testing.waitForTraces(2); + testing.clearData(); + } + + @AfterAll + static void cleanUp() { + connection.close(); + redisClient.shutdown(); + redisServer.stop(); + } + + @SuppressWarnings("deprecation") // RedisURI constructor + @Test + void testConnectUsingGetOnConnectionFuture() throws ExecutionException, InterruptedException { + RedisClient testConnectionClient = RedisClient.create(embeddedDbUri); + testConnectionClient.setOptions(CLIENT_OPTIONS); + + ConnectionFuture> connectionFuture = + testConnectionClient.connectAsync( + new Utf8StringCodec(), new RedisURI(host, port, 3, TimeUnit.SECONDS)); + StatefulRedisConnection connection1 = connectionFuture.get(); + cleanup.deferCleanup(connection1); + cleanup.deferCleanup(testConnectionClient::shutdown); + + assertThat(connection1).isNotNull(); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("CONNECT") + .hasKind(SpanKind.CLIENT) + .hasAttributesSatisfyingExactly( + equalTo(ServerAttributes.SERVER_ADDRESS, host), + equalTo(ServerAttributes.SERVER_PORT, port), + equalTo(DbIncubatingAttributes.DB_SYSTEM, "redis")))); + } + + @SuppressWarnings("deprecation") // RedisURI constructor + @Test + void testConnectExceptionInsideTheConnectionFuture() { + RedisClient testConnectionClient = RedisClient.create(dbUriNonExistent); + testConnectionClient.setOptions(CLIENT_OPTIONS); + + Exception exception = + catchException( + () -> { + ConnectionFuture> connectionFuture = + testConnectionClient.connectAsync( + new Utf8StringCodec(), + new RedisURI(host, incorrectPort, 3, TimeUnit.SECONDS)); + connectionFuture.get(); + }); + + assertThat(exception).isInstanceOf(ExecutionException.class); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("CONNECT") + .hasKind(SpanKind.CLIENT) + .hasStatus(StatusData.error()) + .hasAttributesSatisfyingExactly( + equalTo(ServerAttributes.SERVER_ADDRESS, host), + equalTo(ServerAttributes.SERVER_PORT, incorrectPort), + equalTo(DbIncubatingAttributes.DB_SYSTEM, "redis")) + .hasEventsSatisfyingExactly( + event -> + event + .hasName("exception") + .hasAttributesSatisfyingExactly( + equalTo( + AttributeKey.stringKey("exception.type"), + "io.netty.channel.AbstractChannel.AnnotatedConnectException"), + equalTo( + AttributeKey.stringKey("exception.message"), + "Connection refused: " + + host + + "/" + + ip + + ":" + + incorrectPort), + satisfies( + AttributeKey.stringKey("exception.stacktrace"), + AbstractAssert::isNotNull))))); + } + + @Test + void testSetCommandUsingFutureGetWithTimeout() + throws ExecutionException, InterruptedException, TimeoutException { + RedisFuture redisFuture = asyncCommands.set("TESTSETKEY", "TESTSETVAL"); + String res = redisFuture.get(3, TimeUnit.SECONDS); + + assertThat(res).isEqualTo("OK"); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("SET") + .hasKind(SpanKind.CLIENT) + .hasAttributesSatisfyingExactly( + equalTo(DbIncubatingAttributes.DB_SYSTEM, "redis"), + equalTo(DbIncubatingAttributes.DB_STATEMENT, "SET TESTSETKEY ?"), + equalTo(DbIncubatingAttributes.DB_OPERATION, "SET")))); + } + + @Test + void testGetCommandChainedWithThenAccept() + throws ExecutionException, InterruptedException, TimeoutException { + CompletableFuture future = new CompletableFuture<>(); + Consumer consumer = + res -> { + testing.runWithSpan("callback", () -> assertThat(res).isEqualTo("TESTVAL")); + future.complete(res); + }; + + testing.runWithSpan( + "parent", + () -> { + RedisFuture redisFuture = asyncCommands.get("TESTKEY"); + redisFuture.thenAccept(consumer); + }); + + assertThat(future.get(10, TimeUnit.SECONDS)).isEqualTo("TESTVAL"); + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(), + span -> + span.hasName("GET") + .hasKind(SpanKind.CLIENT) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo(DbIncubatingAttributes.DB_SYSTEM, "redis"), + equalTo(DbIncubatingAttributes.DB_STATEMENT, "GET TESTKEY"), + equalTo(DbIncubatingAttributes.DB_OPERATION, "GET")), + span -> + span.hasName("callback") + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(0)))); + } + + // to make sure instrumentation's chained completion stages won't interfere with user's, while + // still recording spans + @Test + void testGetNonExistentKeyCommandWithHandleAsyncAndChainedWithThenApply() + throws ExecutionException, InterruptedException, TimeoutException { + CompletableFuture future = new CompletableFuture<>(); + + String successStr = "KEY MISSING"; + + BiFunction firstStage = + (res, error) -> { + testing.runWithSpan( + "callback1", + () -> { + assertThat(res).isNull(); + assertThat(error).isNull(); + }); + return (res == null ? successStr : res); + }; + Function secondStage = + input -> { + testing.runWithSpan( + "callback2", + () -> { + assertThat(input).isEqualTo(successStr); + future.complete(successStr); + }); + return null; + }; + + testing.runWithSpan( + "parent", + () -> { + RedisFuture redisFuture = asyncCommands.get("NON_EXISTENT_KEY"); + redisFuture.handle(firstStage).thenApply(secondStage); + }); + + assertThat(future.get(10, TimeUnit.SECONDS)).isEqualTo(successStr); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(), + span -> + span.hasName("GET") + .hasKind(SpanKind.CLIENT) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo(DbIncubatingAttributes.DB_SYSTEM, "redis"), + equalTo(DbIncubatingAttributes.DB_STATEMENT, "GET NON_EXISTENT_KEY"), + equalTo(DbIncubatingAttributes.DB_OPERATION, "GET")), + span -> + span.hasName("callback1") + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(0)), + span -> + span.hasName("callback2") + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(0)))); + } + + @Test + void testCommandWithNoArgumentsUsingBiconsumer() + throws ExecutionException, InterruptedException, TimeoutException { + CompletableFuture future = new CompletableFuture<>(); + BiConsumer biConsumer = + (keyRetrieved, error) -> + testing.runWithSpan( + "callback", + () -> { + assertThat(keyRetrieved).isNotNull(); + future.complete(keyRetrieved); + }); + + testing.runWithSpan( + "parent", + () -> { + RedisFuture redisFuture = asyncCommands.randomkey(); + redisFuture.whenCompleteAsync(biConsumer); + }); + + assertThat(future.get(10, TimeUnit.SECONDS)).isNotNull(); + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(), + span -> + span.hasName("RANDOMKEY") + .hasKind(SpanKind.CLIENT) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo(DbIncubatingAttributes.DB_SYSTEM, "redis"), + equalTo(DbIncubatingAttributes.DB_STATEMENT, "RANDOMKEY"), + equalTo(DbIncubatingAttributes.DB_OPERATION, "RANDOMKEY")), + span -> + span.hasName("callback") + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(0)))); + } + + @Test + void testHashSetAndThenNestApplyToHashGetall() + throws ExecutionException, InterruptedException, TimeoutException { + CompletableFuture> future = new CompletableFuture<>(); + + RedisFuture hmsetFuture = asyncCommands.hmset("TESTHM", testHashMap); + hmsetFuture.thenApplyAsync( + setResult -> { + // Wait for 'hmset' trace to get written + testing.waitForTraces(1); + + if (!"OK".equals(setResult)) { + future.completeExceptionally(new AssertionError("Wrong hmset result " + setResult)); + return null; + } + + RedisFuture> hmGetAllFuture = asyncCommands.hgetall("TESTHM"); + hmGetAllFuture.whenComplete( + (result, exception) -> { + if (exception != null) { + future.completeExceptionally(exception); + } else { + future.complete(result); + } + }); + return null; + }); + + assertThat(future.get(10, TimeUnit.SECONDS)).isEqualTo(testHashMap); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("HMSET") + .hasKind(SpanKind.CLIENT) + .hasAttributesSatisfyingExactly( + equalTo(DbIncubatingAttributes.DB_SYSTEM, "redis"), + equalTo( + DbIncubatingAttributes.DB_STATEMENT, + "HMSET TESTHM firstname ? lastname ? age ?"), + equalTo(DbIncubatingAttributes.DB_OPERATION, "HMSET"))), + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("HGETALL") + .hasKind(SpanKind.CLIENT) + .hasAttributesSatisfyingExactly( + equalTo(DbIncubatingAttributes.DB_SYSTEM, "redis"), + equalTo(DbIncubatingAttributes.DB_STATEMENT, "HGETALL TESTHM"), + equalTo(DbIncubatingAttributes.DB_OPERATION, "HGETALL")))); + } + + @Test + void testCommandCompletesExceptionally() { + // turn off auto flush to complete the command exceptionally manually + asyncCommands.setAutoFlushCommands(false); + cleanup.deferCleanup(() -> asyncCommands.setAutoFlushCommands(true)); + + RedisFuture redisFuture = asyncCommands.del("key1", "key2"); + boolean completedExceptionally = + ((AsyncCommand) redisFuture) + .completeExceptionally(new IllegalStateException("TestException")); + + redisFuture.exceptionally( + error -> { + assertThat(error).isNotNull(); + assertThat(error).isInstanceOf(IllegalStateException.class); + assertThat(error.getMessage()).isEqualTo("TestException"); + throw new RuntimeException(error); + }); + + asyncCommands.flushCommands(); + Throwable thrown = catchThrowable(redisFuture::get); + + await() + .untilAsserted( + () -> { + assertThat(thrown).isInstanceOf(ExecutionException.class); + assertThat(completedExceptionally).isTrue(); + }); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("DEL") + .hasKind(SpanKind.CLIENT) + .hasStatus(StatusData.error()) + .hasException(new IllegalStateException("TestException")) + .hasAttributesSatisfyingExactly( + equalTo(DbIncubatingAttributes.DB_SYSTEM, "redis"), + equalTo(DbIncubatingAttributes.DB_STATEMENT, "DEL key1 key2"), + equalTo(DbIncubatingAttributes.DB_OPERATION, "DEL")))); + } + + @Test + void testCancelCommandBeforeItFinishes() { + asyncCommands.setAutoFlushCommands(false); + cleanup.deferCleanup(() -> asyncCommands.setAutoFlushCommands(true)); + + RedisFuture redisFuture = + testing.runWithSpan("parent", () -> asyncCommands.sadd("SKEY", "1", "2")); + redisFuture.whenCompleteAsync( + (res, error) -> + testing.runWithSpan( + "callback", + () -> { + assertThat(error).isNotNull(); + assertThat(error).isInstanceOf(CancellationException.class); + })); + + boolean cancelSuccess = redisFuture.cancel(true); + asyncCommands.flushCommands(); + + await().untilAsserted(() -> assertThat(cancelSuccess).isTrue()); + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("parent") + .hasKind(SpanKind.INTERNAL) + .hasNoParent() + .hasAttributes(Attributes.empty()), + span -> + span.hasName("SADD") + .hasKind(SpanKind.CLIENT) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo(DbIncubatingAttributes.DB_SYSTEM, "redis"), + equalTo(DbIncubatingAttributes.DB_STATEMENT, "SADD SKEY ? ?"), + equalTo(DbIncubatingAttributes.DB_OPERATION, "SADD"), + equalTo(booleanKey("lettuce.command.cancelled"), true)), + span -> + span.hasName("callback") + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(0)))); + } + + @Test + void testDebugSegfaultCommandWithNoArgumentShouldProduceSpan() { + // Test Causes redis to crash therefore it needs its own container + try (StatefulRedisConnection statefulConnection = newContainerConnection()) { + RedisAsyncCommands commands = statefulConnection.async(); + commands.debugSegfault(); + } + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("DEBUG") + .hasKind(SpanKind.CLIENT) + .hasAttributesSatisfyingExactly( + equalTo(DbIncubatingAttributes.DB_SYSTEM, "redis"), + equalTo(DbIncubatingAttributes.DB_STATEMENT, "DEBUG SEGFAULT"), + equalTo(DbIncubatingAttributes.DB_OPERATION, "DEBUG")))); + } + + @Test + void testShutdownCommandShouldProduceSpan() { + // Test Causes redis to crash therefore it needs its own container + try (StatefulRedisConnection statefulConnection = newContainerConnection()) { + RedisAsyncCommands commands = statefulConnection.async(); + commands.shutdown(false); + } + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("SHUTDOWN") + .hasKind(SpanKind.CLIENT) + .hasAttributesSatisfyingExactly( + equalTo(DbIncubatingAttributes.DB_SYSTEM, "redis"), + equalTo(DbIncubatingAttributes.DB_STATEMENT, "SHUTDOWN NOSAVE"), + equalTo(DbIncubatingAttributes.DB_OPERATION, "SHUTDOWN")))); + } +} diff --git a/instrumentation/lettuce/lettuce-5.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/lettuce/v5_0/LettuceReactiveClientTest.java b/instrumentation/lettuce/lettuce-5.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/lettuce/v5_0/LettuceReactiveClientTest.java new file mode 100644 index 000000000000..feb5f7ead45c --- /dev/null +++ b/instrumentation/lettuce/lettuce-5.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/lettuce/v5_0/LettuceReactiveClientTest.java @@ -0,0 +1,376 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.lettuce.v5_0; + +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.satisfies; +import static org.assertj.core.api.Assertions.assertThat; + +import io.lettuce.core.RedisClient; +import io.lettuce.core.api.StatefulRedisConnection; +import io.lettuce.core.api.reactive.RedisReactiveCommands; +import io.lettuce.core.api.sync.RedisCommands; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.semconv.incubating.DbIncubatingAttributes; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.function.Consumer; +import org.assertj.core.api.AbstractBooleanAssert; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import reactor.core.scheduler.Schedulers; + +class LettuceReactiveClientTest extends AbstractLettuceClientTest { + private static RedisReactiveCommands reactiveCommands; + + @BeforeAll + static void setUp() throws UnknownHostException { + redisServer.start(); + + host = redisServer.getHost(); + ip = InetAddress.getByName(host).getHostAddress(); + port = redisServer.getMappedPort(6379); + embeddedDbUri = "redis://" + host + ":" + port + "/" + DB_INDEX; + + redisClient = RedisClient.create(embeddedDbUri); + redisClient.setOptions(CLIENT_OPTIONS); + + connection = redisClient.connect(); + reactiveCommands = connection.reactive(); + RedisCommands syncCommands = connection.sync(); + + syncCommands.set("TESTKEY", "TESTVAL"); + + // 1 set + 1 connect trace + testing.waitForTraces(2); + testing.clearData(); + } + + @AfterAll + static void cleanUp() { + connection.close(); + redisClient.shutdown(); + redisServer.stop(); + } + + @Test + void testSetCommandWithSubscribeOnDefinedConsumer() + throws ExecutionException, InterruptedException, TimeoutException { + CompletableFuture future = new CompletableFuture<>(); + + Consumer consumer = + res -> + testing.runWithSpan( + "callback", + () -> { + assertThat(res).isEqualTo("OK"); + future.complete(res); + }); + + testing.runWithSpan( + "parent", () -> reactiveCommands.set("TESTSETKEY", "TESTSETVAL").subscribe(consumer)); + + assertThat(future.get(10, TimeUnit.SECONDS)).isEqualTo("OK"); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(), + span -> + span.hasName("SET") + .hasKind(SpanKind.CLIENT) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo(DbIncubatingAttributes.DB_SYSTEM, "redis"), + equalTo(DbIncubatingAttributes.DB_STATEMENT, "SET TESTSETKEY ?"), + equalTo(DbIncubatingAttributes.DB_OPERATION, "SET")), + span -> + span.hasName("callback") + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(0)))); + } + + @Test + void testGetCommandWithLambdaFunction() + throws ExecutionException, InterruptedException, TimeoutException { + CompletableFuture future = new CompletableFuture<>(); + + reactiveCommands + .get("TESTKEY") + .subscribe( + res -> { + assertThat(res).isEqualTo("TESTVAL"); + future.complete(res); + }); + + assertThat(future.get(10, TimeUnit.SECONDS)).isEqualTo("TESTVAL"); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("GET") + .hasKind(SpanKind.CLIENT) + .hasAttributesSatisfyingExactly( + equalTo(DbIncubatingAttributes.DB_SYSTEM, "redis"), + equalTo(DbIncubatingAttributes.DB_STATEMENT, "GET TESTKEY"), + equalTo(DbIncubatingAttributes.DB_OPERATION, "GET")))); + } + + // to make sure instrumentation's chained completion stages won't interfere with user's, while + // still recording spans + @Test + void testGetNonExistentKeyCommand() + throws ExecutionException, InterruptedException, TimeoutException { + CompletableFuture future = new CompletableFuture<>(); + String defaultVal = "NOT THIS VALUE"; + + testing.runWithSpan( + "parent", + () -> { + reactiveCommands + .get("NON_EXISTENT_KEY") + .defaultIfEmpty(defaultVal) + .subscribe( + res -> + testing.runWithSpan( + "callback", + () -> { + assertThat(res).isEqualTo(defaultVal); + future.complete(res); + })); + }); + + assertThat(future.get(10, TimeUnit.SECONDS)).isEqualTo(defaultVal); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(), + span -> + span.hasName("GET") + .hasKind(SpanKind.CLIENT) + .hasAttributesSatisfyingExactly( + equalTo(DbIncubatingAttributes.DB_SYSTEM, "redis"), + equalTo(DbIncubatingAttributes.DB_STATEMENT, "GET NON_EXISTENT_KEY"), + equalTo(DbIncubatingAttributes.DB_OPERATION, "GET")), + span -> + span.hasName("callback") + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(0)))); + } + + @Test + void testCommandWithNoArguments() + throws ExecutionException, InterruptedException, TimeoutException { + CompletableFuture future = new CompletableFuture<>(); + + reactiveCommands + .randomkey() + .subscribe( + res -> { + assertThat(res).isEqualTo("TESTKEY"); + future.complete(res); + }); + + assertThat(future.get(10, TimeUnit.SECONDS)).isEqualTo("TESTKEY"); + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("RANDOMKEY") + .hasKind(SpanKind.CLIENT) + .hasAttributesSatisfyingExactly( + equalTo(DbIncubatingAttributes.DB_SYSTEM, "redis"), + equalTo(DbIncubatingAttributes.DB_STATEMENT, "RANDOMKEY"), + equalTo(DbIncubatingAttributes.DB_OPERATION, "RANDOMKEY")))); + } + + @Test + void testCommandFluxPublisher() { + reactiveCommands.command().subscribe(); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("COMMAND") + .hasKind(SpanKind.CLIENT) + .hasAttributesSatisfyingExactly( + equalTo(DbIncubatingAttributes.DB_SYSTEM, "redis"), + equalTo(DbIncubatingAttributes.DB_STATEMENT, "COMMAND"), + equalTo(DbIncubatingAttributes.DB_OPERATION, "COMMAND"), + satisfies( + AttributeKey.longKey("lettuce.command.results.count"), + val -> val.isGreaterThan(100))))); + } + + @Test + void testCommandCancelAfter2OnFluxPublisher() { + reactiveCommands.command().take(2).subscribe(); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("COMMAND") + .hasKind(SpanKind.CLIENT) + .hasAttributesSatisfyingExactly( + equalTo(DbIncubatingAttributes.DB_SYSTEM, "redis"), + equalTo(DbIncubatingAttributes.DB_STATEMENT, "COMMAND"), + equalTo(DbIncubatingAttributes.DB_OPERATION, "COMMAND"), + satisfies( + AttributeKey.booleanKey("lettuce.command.cancelled"), + AbstractBooleanAssert::isTrue), + satisfies( + AttributeKey.longKey("lettuce.command.results.count"), + val -> val.isEqualTo(2))))); + } + + @Test + void testNonReactiveCommandShouldNotProduceSpan() { + String res = reactiveCommands.digest(null); + + assertThat(res).isNotNull(); + assertThat(testing.spans().size()).isEqualTo(0); + } + + @Test + void testDebugSegfaultCommandReturnsMonoVoidWithNoArgumentShouldProduceSpan() { + // Test Causes redis to crash therefore it needs its own container + try (StatefulRedisConnection statefulConnection = newContainerConnection()) { + RedisReactiveCommands commands = statefulConnection.reactive(); + commands.debugSegfault().subscribe(); + } + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("DEBUG") + .hasKind(SpanKind.CLIENT) + .hasAttributesSatisfyingExactly( + equalTo(DbIncubatingAttributes.DB_SYSTEM, "redis"), + equalTo(DbIncubatingAttributes.DB_STATEMENT, "DEBUG SEGFAULT"), + equalTo(DbIncubatingAttributes.DB_OPERATION, "DEBUG")))); + } + + @Test + void testShutdownCommandShouldProduceSpan() { + // Test Causes redis to crash therefore it needs its own container + try (StatefulRedisConnection statefulConnection = newContainerConnection()) { + RedisReactiveCommands commands = statefulConnection.reactive(); + commands.shutdown(false).subscribe(); + } + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("SHUTDOWN") + .hasKind(SpanKind.CLIENT) + .hasAttributesSatisfyingExactly( + equalTo(DbIncubatingAttributes.DB_SYSTEM, "redis"), + equalTo(DbIncubatingAttributes.DB_STATEMENT, "SHUTDOWN NOSAVE"), + equalTo(DbIncubatingAttributes.DB_OPERATION, "SHUTDOWN")))); + } + + @Test + void testBlockingSubscriber() { + testing.runWithSpan( + "test-parent", + () -> reactiveCommands.set("a", "1").then(reactiveCommands.get("a")).block()); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("test-parent").hasAttributes(Attributes.empty()), + span -> + span.hasName("SET") + .hasKind(SpanKind.CLIENT) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo(DbIncubatingAttributes.DB_SYSTEM, "redis"), + equalTo(DbIncubatingAttributes.DB_STATEMENT, "SET a ?"), + equalTo(DbIncubatingAttributes.DB_OPERATION, "SET")), + span -> + span.hasName("GET") + .hasKind(SpanKind.CLIENT) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo(DbIncubatingAttributes.DB_SYSTEM, "redis"), + equalTo(DbIncubatingAttributes.DB_STATEMENT, "GET a"), + equalTo(DbIncubatingAttributes.DB_OPERATION, "GET")))); + } + + @Test + void testAsyncSubscriber() { + testing.runWithSpan( + "test-parent", + () -> reactiveCommands.set("a", "1").then(reactiveCommands.get("a")).subscribe()); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("test-parent").hasAttributes(Attributes.empty()), + span -> + span.hasName("SET") + .hasKind(SpanKind.CLIENT) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo(DbIncubatingAttributes.DB_SYSTEM, "redis"), + equalTo(DbIncubatingAttributes.DB_STATEMENT, "SET a ?"), + equalTo(DbIncubatingAttributes.DB_OPERATION, "SET")), + span -> + span.hasName("GET") + .hasKind(SpanKind.CLIENT) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo(DbIncubatingAttributes.DB_SYSTEM, "redis"), + equalTo(DbIncubatingAttributes.DB_STATEMENT, "GET a"), + equalTo(DbIncubatingAttributes.DB_OPERATION, "GET")))); + } + + @Test + void testAsyncSubscriberWithSpecificThreadPool() { + testing.runWithSpan( + "test-parent", + () -> + reactiveCommands + .set("a", "1") + .then(reactiveCommands.get("a")) + .subscribeOn(Schedulers.elastic()) + .subscribe()); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("test-parent").hasAttributes(Attributes.empty()), + span -> + span.hasName("SET") + .hasKind(SpanKind.CLIENT) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo(DbIncubatingAttributes.DB_SYSTEM, "redis"), + equalTo(DbIncubatingAttributes.DB_STATEMENT, "SET a ?"), + equalTo(DbIncubatingAttributes.DB_OPERATION, "SET")), + span -> + span.hasName("GET") + .hasKind(SpanKind.CLIENT) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo(DbIncubatingAttributes.DB_SYSTEM, "redis"), + equalTo(DbIncubatingAttributes.DB_STATEMENT, "GET a"), + equalTo(DbIncubatingAttributes.DB_OPERATION, "GET")))); + } +} diff --git a/instrumentation/lettuce/lettuce-5.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/lettuce/v5_0/LettuceSyncClientTest.java b/instrumentation/lettuce/lettuce-5.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/lettuce/v5_0/LettuceSyncClientTest.java new file mode 100644 index 000000000000..7828a3c5ef8e --- /dev/null +++ b/instrumentation/lettuce/lettuce-5.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/lettuce/v5_0/LettuceSyncClientTest.java @@ -0,0 +1,298 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.lettuce.v5_0; + +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.satisfies; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.catchException; + +import com.google.common.collect.ImmutableMap; +import io.lettuce.core.RedisClient; +import io.lettuce.core.RedisConnectionException; +import io.lettuce.core.api.StatefulRedisConnection; +import io.lettuce.core.api.sync.RedisCommands; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.test.utils.PortUtils; +import io.opentelemetry.sdk.trace.data.StatusData; +import io.opentelemetry.semconv.ServerAttributes; +import io.opentelemetry.semconv.incubating.DbIncubatingAttributes; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.Map; +import org.assertj.core.api.AbstractAssert; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +class LettuceSyncClientTest extends AbstractLettuceClientTest { + private static int incorrectPort; + private static String dbUriNonExistent; + + private static final ImmutableMap testHashMap = + ImmutableMap.of( + "firstname", "John", + "lastname", "Doe", + "age", "53"); + + private static RedisCommands syncCommands; + + @BeforeAll + static void setUp() throws UnknownHostException { + redisServer.start(); + host = redisServer.getHost(); + ip = InetAddress.getByName(host).getHostAddress(); + port = redisServer.getMappedPort(6379); + embeddedDbUri = "redis://" + host + ":" + port + "/" + DB_INDEX; + + incorrectPort = PortUtils.findOpenPort(); + dbUriNonExistent = "redis://" + host + ":" + incorrectPort + "/" + DB_INDEX; + + redisClient = RedisClient.create(embeddedDbUri); + redisClient.setOptions(CLIENT_OPTIONS); + + connection = redisClient.connect(); + syncCommands = connection.sync(); + + syncCommands.set("TESTKEY", "TESTVAL"); + syncCommands.hmset("TESTHM", testHashMap); + + // 2 sets + 1 connect trace + testing.waitForTraces(3); + testing.clearData(); + } + + @AfterAll + static void cleanUp() { + connection.close(); + redisClient.shutdown(); + redisServer.stop(); + } + + @Test + void testConnect() { + RedisClient testConnectionClient = RedisClient.create(embeddedDbUri); + testConnectionClient.setOptions(CLIENT_OPTIONS); + + StatefulRedisConnection testConnection = testConnectionClient.connect(); + cleanup.deferCleanup(testConnection); + cleanup.deferCleanup(testConnectionClient::shutdown); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("CONNECT") + .hasKind(SpanKind.CLIENT) + .hasAttributesSatisfyingExactly( + equalTo(ServerAttributes.SERVER_ADDRESS, host), + equalTo(ServerAttributes.SERVER_PORT, port), + equalTo(DbIncubatingAttributes.DB_SYSTEM, "redis")))); + } + + @Test + void testConnectException() { + RedisClient testConnectionClient = RedisClient.create(dbUriNonExistent); + testConnectionClient.setOptions(CLIENT_OPTIONS); + + Exception exception = catchException(testConnectionClient::connect); + + assertThat(exception).isInstanceOf(RedisConnectionException.class); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("CONNECT") + .hasKind(SpanKind.CLIENT) + .hasStatus(StatusData.error()) + .hasAttributesSatisfyingExactly( + equalTo(ServerAttributes.SERVER_ADDRESS, host), + equalTo(ServerAttributes.SERVER_PORT, incorrectPort), + equalTo(DbIncubatingAttributes.DB_SYSTEM, "redis")) + .hasEventsSatisfyingExactly( + event -> + event + .hasName("exception") + .hasAttributesSatisfyingExactly( + equalTo( + AttributeKey.stringKey("exception.type"), + "io.netty.channel.AbstractChannel.AnnotatedConnectException"), + equalTo( + AttributeKey.stringKey("exception.message"), + "Connection refused: " + + host + + "/" + + ip + + ":" + + incorrectPort), + satisfies( + AttributeKey.stringKey("exception.stacktrace"), + AbstractAssert::isNotNull))))); + } + + @Test + void testSetCommand() { + String res = syncCommands.set("TESTSETKEY", "TESTSETVAL"); + assertThat(res).isEqualTo("OK"); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("SET") + .hasKind(SpanKind.CLIENT) + .hasAttributesSatisfyingExactly( + equalTo(DbIncubatingAttributes.DB_SYSTEM, "redis"), + equalTo(DbIncubatingAttributes.DB_STATEMENT, "SET TESTSETKEY ?"), + equalTo(DbIncubatingAttributes.DB_OPERATION, "SET")))); + } + + @Test + void testGetCommand() { + String res = syncCommands.get("TESTKEY"); + assertThat(res).isEqualTo("TESTVAL"); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("GET") + .hasKind(SpanKind.CLIENT) + .hasAttributesSatisfyingExactly( + equalTo(DbIncubatingAttributes.DB_SYSTEM, "redis"), + equalTo(DbIncubatingAttributes.DB_STATEMENT, "GET TESTKEY"), + equalTo(DbIncubatingAttributes.DB_OPERATION, "GET")))); + } + + @Test + void testGetNonExistentKeyCommand() { + String res = syncCommands.get("NON_EXISTENT_KEY"); + assertThat(res).isNull(); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("GET") + .hasKind(SpanKind.CLIENT) + .hasAttributesSatisfyingExactly( + equalTo(DbIncubatingAttributes.DB_SYSTEM, "redis"), + equalTo(DbIncubatingAttributes.DB_STATEMENT, "GET NON_EXISTENT_KEY"), + equalTo(DbIncubatingAttributes.DB_OPERATION, "GET")))); + } + + @Test + void testCommandWithNoArguments() { + String res = syncCommands.randomkey(); + assertThat(res).isNotNull(); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("RANDOMKEY") + .hasKind(SpanKind.CLIENT) + .hasAttributesSatisfyingExactly( + equalTo(DbIncubatingAttributes.DB_SYSTEM, "redis"), + equalTo(DbIncubatingAttributes.DB_STATEMENT, "RANDOMKEY"), + equalTo(DbIncubatingAttributes.DB_OPERATION, "RANDOMKEY")))); + } + + @Test + void testListCommand() { + long res = syncCommands.lpush("TESTLIST", "TESTLIST ELEMENT"); + assertThat(res).isEqualTo(1); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("LPUSH") + .hasKind(SpanKind.CLIENT) + .hasAttributesSatisfyingExactly( + equalTo(DbIncubatingAttributes.DB_SYSTEM, "redis"), + equalTo(DbIncubatingAttributes.DB_STATEMENT, "LPUSH TESTLIST ?"), + equalTo(DbIncubatingAttributes.DB_OPERATION, "LPUSH")))); + } + + @Test + void testHashSetCommand() { + String res = syncCommands.hmset("user", testHashMap); + assertThat(res).isEqualTo("OK"); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("HMSET") + .hasKind(SpanKind.CLIENT) + .hasAttributesSatisfyingExactly( + equalTo(DbIncubatingAttributes.DB_SYSTEM, "redis"), + equalTo( + DbIncubatingAttributes.DB_STATEMENT, + "HMSET user firstname ? lastname ? age ?"), + equalTo(DbIncubatingAttributes.DB_OPERATION, "HMSET")))); + } + + @Test + void testHashGetallCommand() { + Map res = syncCommands.hgetall("TESTHM"); + assertThat(res).isEqualTo(testHashMap); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("HGETALL") + .hasKind(SpanKind.CLIENT) + .hasAttributesSatisfyingExactly( + equalTo(DbIncubatingAttributes.DB_SYSTEM, "redis"), + equalTo(DbIncubatingAttributes.DB_STATEMENT, "HGETALL TESTHM"), + equalTo(DbIncubatingAttributes.DB_OPERATION, "HGETALL")))); + } + + @Test + void testDebugSegfaultCommandWithNoArgumentShouldProduceSpan() { + // Test causes redis to crash therefore it needs its own container + try (StatefulRedisConnection statefulConnection = newContainerConnection()) { + RedisCommands commands = statefulConnection.sync(); + commands.debugSegfault(); + } + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("DEBUG") + .hasKind(SpanKind.CLIENT) + .hasAttributesSatisfyingExactly( + equalTo(DbIncubatingAttributes.DB_SYSTEM, "redis"), + equalTo(DbIncubatingAttributes.DB_STATEMENT, "DEBUG SEGFAULT"), + equalTo(DbIncubatingAttributes.DB_OPERATION, "DEBUG")))); + } + + @Test + void testShutdownCommandShouldProduceSpan() { + // Test causes redis to crash therefore it needs its own container + try (StatefulRedisConnection statefulConnection = newContainerConnection()) { + RedisCommands commands = statefulConnection.sync(); + commands.shutdown(false); + } + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("SHUTDOWN") + .hasKind(SpanKind.CLIENT) + .hasAttributesSatisfyingExactly( + equalTo(DbIncubatingAttributes.DB_SYSTEM, "redis"), + equalTo(DbIncubatingAttributes.DB_STATEMENT, "SHUTDOWN NOSAVE"), + equalTo(DbIncubatingAttributes.DB_OPERATION, "SHUTDOWN")))); + } +} diff --git a/instrumentation/lettuce/lettuce-5.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v5_1/LettuceAsyncCommandInstrumentation.java b/instrumentation/lettuce/lettuce-5.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v5_1/LettuceAsyncCommandInstrumentation.java index c7165d36bb4f..069d077586a0 100644 --- a/instrumentation/lettuce/lettuce-5.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v5_1/LettuceAsyncCommandInstrumentation.java +++ b/instrumentation/lettuce/lettuce-5.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v5_1/LettuceAsyncCommandInstrumentation.java @@ -7,6 +7,7 @@ import static net.bytebuddy.matcher.ElementMatchers.isConstructor; import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.namedOneOf; import io.lettuce.core.protocol.AsyncCommand; import io.opentelemetry.context.Context; @@ -31,7 +32,7 @@ public void transform(TypeTransformer transformer) { transformer.applyAdviceToMethod( isConstructor(), LettuceAsyncCommandInstrumentation.class.getName() + "$SaveContextAdvice"); transformer.applyAdviceToMethod( - named("complete").or(named("completeExceptionally")).or(named("cancel")), + namedOneOf("complete", "completeExceptionally", "cancel"), LettuceAsyncCommandInstrumentation.class.getName() + "$RestoreContextAdvice"); } diff --git a/instrumentation/lettuce/lettuce-5.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v5_1/LettuceInstrumentationModule.java b/instrumentation/lettuce/lettuce-5.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v5_1/LettuceInstrumentationModule.java index a9a003fc98de..fb909ced0afa 100644 --- a/instrumentation/lettuce/lettuce-5.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v5_1/LettuceInstrumentationModule.java +++ b/instrumentation/lettuce/lettuce-5.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v5_1/LettuceInstrumentationModule.java @@ -26,6 +26,11 @@ public ElementMatcher.Junction classLoaderMatcher() { return hasClassesNamed("io.lettuce.core.tracing.Tracing"); } + @Override + public boolean isHelperClass(String className) { + return className.startsWith("io.lettuce.core.protocol.OtelCommandArgsUtil"); + } + @Override public List typeInstrumentations() { return asList( diff --git a/instrumentation/lettuce/lettuce-5.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v5_1/TracingHolder.java b/instrumentation/lettuce/lettuce-5.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v5_1/TracingHolder.java index e029936b7ab7..7b354788b5f9 100644 --- a/instrumentation/lettuce/lettuce-5.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v5_1/TracingHolder.java +++ b/instrumentation/lettuce/lettuce-5.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/lettuce/v5_1/TracingHolder.java @@ -8,11 +8,15 @@ import io.lettuce.core.tracing.Tracing; import io.opentelemetry.api.GlobalOpenTelemetry; import io.opentelemetry.instrumentation.lettuce.v5_1.LettuceTelemetry; +import io.opentelemetry.javaagent.bootstrap.internal.AgentCommonConfig; public final class TracingHolder { public static final Tracing TRACING = - LettuceTelemetry.create(GlobalOpenTelemetry.get()).newTracing(); + LettuceTelemetry.builder(GlobalOpenTelemetry.get()) + .setStatementSanitizationEnabled(AgentCommonConfig.get().isStatementSanitizationEnabled()) + .build() + .newTracing(); private TracingHolder() {} } diff --git a/instrumentation/lettuce/lettuce-5.1/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/lettuce/v5_1/LettuceAsyncClientTest.groovy b/instrumentation/lettuce/lettuce-5.1/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/lettuce/v5_1/LettuceAsyncClientTest.groovy deleted file mode 100644 index 04fa6160c87c..000000000000 --- a/instrumentation/lettuce/lettuce-5.1/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/lettuce/v5_1/LettuceAsyncClientTest.groovy +++ /dev/null @@ -1,17 +0,0 @@ -/* - * 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/instrumentation/lettuce/lettuce-5.1/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/lettuce/v5_1/LettuceReactiveClientTest.groovy b/instrumentation/lettuce/lettuce-5.1/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/lettuce/v5_1/LettuceReactiveClientTest.groovy deleted file mode 100644 index 34325b19483a..000000000000 --- a/instrumentation/lettuce/lettuce-5.1/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/lettuce/v5_1/LettuceReactiveClientTest.groovy +++ /dev/null @@ -1,79 +0,0 @@ -/* - * 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.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: - runWithSpan("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_SOCK_PEER_ADDR" "127.0.0.1" - "$SemanticAttributes.NET_SOCK_PEER_NAME" expectedHostAttributeValue - "$SemanticAttributes.NET_SOCK_PEER_PORT" port - "$SemanticAttributes.DB_SYSTEM" "redis" - "$SemanticAttributes.DB_STATEMENT" "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_SOCK_PEER_ADDR" "127.0.0.1" - "$SemanticAttributes.NET_SOCK_PEER_NAME" expectedHostAttributeValue - "$SemanticAttributes.NET_SOCK_PEER_PORT" port - "$SemanticAttributes.DB_SYSTEM" "redis" - "$SemanticAttributes.DB_STATEMENT" "GET a" - } - event(0) { - eventName "redis.encode.start" - } - event(1) { - eventName "redis.encode.end" - } - } - } - } - } -} diff --git a/instrumentation/lettuce/lettuce-5.1/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/lettuce/v5_1/LettuceSyncClientAuthTest.groovy b/instrumentation/lettuce/lettuce-5.1/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/lettuce/v5_1/LettuceSyncClientAuthTest.groovy deleted file mode 100644 index 664e501841cd..000000000000 --- a/instrumentation/lettuce/lettuce-5.1/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/lettuce/v5_1/LettuceSyncClientAuthTest.groovy +++ /dev/null @@ -1,17 +0,0 @@ -/* - * 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/instrumentation/lettuce/lettuce-5.1/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/lettuce/v5_1/LettuceSyncClientTest.groovy b/instrumentation/lettuce/lettuce-5.1/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/lettuce/v5_1/LettuceSyncClientTest.groovy deleted file mode 100644 index 0b1d15baab6b..000000000000 --- a/instrumentation/lettuce/lettuce-5.1/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/lettuce/v5_1/LettuceSyncClientTest.groovy +++ /dev/null @@ -1,17 +0,0 @@ -/* - * 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/instrumentation/lettuce/lettuce-5.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/lettuce/v5_1/LettuceAsyncClientTest.java b/instrumentation/lettuce/lettuce-5.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/lettuce/v5_1/LettuceAsyncClientTest.java new file mode 100644 index 000000000000..692c49243901 --- /dev/null +++ b/instrumentation/lettuce/lettuce-5.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/lettuce/v5_1/LettuceAsyncClientTest.java @@ -0,0 +1,32 @@ +/* + * 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.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import org.junit.jupiter.api.extension.RegisterExtension; + +class LettuceAsyncClientTest extends AbstractLettuceAsyncClientTest { + @RegisterExtension + static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + @Override + public InstrumentationExtension getInstrumentationExtension() { + return testing; + } + + @Override + protected RedisClient createClient(String uri) { + return RedisClient.create(uri); + } + + @Override + protected boolean connectHasSpans() { + return Boolean.getBoolean("testLatestDeps"); + } +} diff --git a/instrumentation/lettuce/lettuce-5.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/lettuce/v5_1/LettuceReactiveClientTest.java b/instrumentation/lettuce/lettuce-5.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/lettuce/v5_1/LettuceReactiveClientTest.java new file mode 100644 index 000000000000..2121d2500510 --- /dev/null +++ b/instrumentation/lettuce/lettuce-5.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/lettuce/v5_1/LettuceReactiveClientTest.java @@ -0,0 +1,81 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.lettuce.v5_1; + +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; + +import io.lettuce.core.RedisClient; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.lettuce.v5_1.AbstractLettuceReactiveClientTest; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.semconv.NetworkAttributes; +import io.opentelemetry.semconv.ServerAttributes; +import io.opentelemetry.semconv.incubating.DbIncubatingAttributes; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +class LettuceReactiveClientTest extends AbstractLettuceReactiveClientTest { + @RegisterExtension + static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + @Override + public InstrumentationExtension getInstrumentationExtension() { + return testing; + } + + @Override + protected 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. + @Test + void testAsyncSubscriberWithSpecificThreadPool() { + getInstrumentationExtension() + .runWithSpan( + "test-parent", + () -> reactiveCommands.set("a", "1").then(reactiveCommands.get("a")).subscribe()); + + getInstrumentationExtension() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("test-parent").hasAttributes(Attributes.empty()), + span -> + span.hasName("SET") + .hasKind(SpanKind.CLIENT) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo(NetworkAttributes.NETWORK_TYPE, "ipv4"), + equalTo(NetworkAttributes.NETWORK_PEER_ADDRESS, ip), + equalTo(NetworkAttributes.NETWORK_PEER_PORT, port), + equalTo(ServerAttributes.SERVER_ADDRESS, host), + equalTo(ServerAttributes.SERVER_PORT, port), + equalTo(DbIncubatingAttributes.DB_SYSTEM, "redis"), + equalTo(DbIncubatingAttributes.DB_STATEMENT, "SET a ?")) + .hasEventsSatisfyingExactly( + event -> event.hasName("redis.encode.start"), + event -> event.hasName("redis.encode.end")), + span -> + span.hasName("GET") + .hasKind(SpanKind.CLIENT) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo(NetworkAttributes.NETWORK_TYPE, "ipv4"), + equalTo(NetworkAttributes.NETWORK_PEER_ADDRESS, ip), + equalTo(NetworkAttributes.NETWORK_PEER_PORT, port), + equalTo(ServerAttributes.SERVER_ADDRESS, host), + equalTo(ServerAttributes.SERVER_PORT, port), + equalTo(DbIncubatingAttributes.DB_SYSTEM, "redis"), + equalTo(DbIncubatingAttributes.DB_STATEMENT, "GET a")) + .hasEventsSatisfyingExactly( + event -> event.hasName("redis.encode.start"), + event -> event.hasName("redis.encode.end")))); + } +} diff --git a/instrumentation/lettuce/lettuce-5.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/lettuce/v5_1/LettuceSyncClientAuthTest.java b/instrumentation/lettuce/lettuce-5.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/lettuce/v5_1/LettuceSyncClientAuthTest.java new file mode 100644 index 000000000000..7f79509b9eba --- /dev/null +++ b/instrumentation/lettuce/lettuce-5.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/lettuce/v5_1/LettuceSyncClientAuthTest.java @@ -0,0 +1,27 @@ +/* + * 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.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import org.junit.jupiter.api.extension.RegisterExtension; + +class LettuceSyncClientAuthTest extends AbstractLettuceSyncClientTest { + @RegisterExtension + static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + @Override + public InstrumentationExtension getInstrumentationExtension() { + return testing; + } + + @Override + protected RedisClient createClient(String uri) { + return RedisClient.create(uri); + } +} diff --git a/instrumentation/lettuce/lettuce-5.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/lettuce/v5_1/LettuceSyncClientTest.java b/instrumentation/lettuce/lettuce-5.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/lettuce/v5_1/LettuceSyncClientTest.java new file mode 100644 index 000000000000..417de8c61d62 --- /dev/null +++ b/instrumentation/lettuce/lettuce-5.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/lettuce/v5_1/LettuceSyncClientTest.java @@ -0,0 +1,27 @@ +/* + * 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.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import org.junit.jupiter.api.extension.RegisterExtension; + +class LettuceSyncClientTest extends AbstractLettuceSyncClientTest { + @RegisterExtension + static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + @Override + public InstrumentationExtension getInstrumentationExtension() { + return testing; + } + + @Override + protected RedisClient createClient(String uri) { + return RedisClient.create(uri); + } +} diff --git a/instrumentation/lettuce/lettuce-5.1/library/src/main/java/io/lettuce/core/protocol/OtelCommandArgsUtil.java b/instrumentation/lettuce/lettuce-5.1/library/src/main/java/io/lettuce/core/protocol/OtelCommandArgsUtil.java new file mode 100644 index 000000000000..571a19c8821f --- /dev/null +++ b/instrumentation/lettuce/lettuce-5.1/library/src/main/java/io/lettuce/core/protocol/OtelCommandArgsUtil.java @@ -0,0 +1,49 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.lettuce.core.protocol; + +import io.lettuce.core.codec.StringCodec; +import io.lettuce.core.protocol.CommandArgs.KeyArgument; +import io.lettuce.core.protocol.CommandArgs.SingularArgument; +import io.lettuce.core.protocol.CommandArgs.ValueArgument; +import io.opentelemetry.instrumentation.lettuce.common.LettuceArgSplitter; +import java.util.ArrayList; +import java.util.List; + +// Helper class for accessing package private fields in CommandArgs and its inner classes. +// https://github.com/lettuce-io/lettuce-core/blob/main/src/main/java/io/lettuce/core/protocol/CommandArgs.java +public final class OtelCommandArgsUtil { + + /** + * Extract argument {@link List} from {@link CommandArgs} so that we wouldn't need to parse them + * from command {@link String} with {@link LettuceArgSplitter#splitArgs}. + */ + public static List getCommandArgs(CommandArgs commandArgs) { + List result = new ArrayList<>(); + StringCodec stringCodec = new StringCodec(); + + for (SingularArgument argument : commandArgs.singularArguments) { + String value = getArgValue(stringCodec, argument); + result.add(value); + } + return result; + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + private static String getArgValue(StringCodec stringCodec, SingularArgument argument) { + if (argument instanceof KeyArgument) { + KeyArgument keyArg = (KeyArgument) argument; + return stringCodec.decodeKey(keyArg.codec.encodeKey(keyArg.key)); + } + if (argument instanceof ValueArgument) { + ValueArgument valueArg = (ValueArgument) argument; + return stringCodec.decodeValue(valueArg.codec.encodeValue(valueArg.val)); + } + return argument.toString(); + } + + private OtelCommandArgsUtil() {} +} diff --git a/instrumentation/lettuce/lettuce-5.1/library/src/main/java/io/opentelemetry/instrumentation/lettuce/v5_1/LettuceNetAttributesGetter.java b/instrumentation/lettuce/lettuce-5.1/library/src/main/java/io/opentelemetry/instrumentation/lettuce/v5_1/LettuceNetAttributesGetter.java deleted file mode 100644 index b779ac08fe4b..000000000000 --- a/instrumentation/lettuce/lettuce-5.1/library/src/main/java/io/opentelemetry/instrumentation/lettuce/v5_1/LettuceNetAttributesGetter.java +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.lettuce.v5_1; - -import io.opentelemetry.instrumentation.api.instrumenter.net.NetClientAttributesGetter; -import io.opentelemetry.instrumentation.lettuce.v5_1.OpenTelemetryTracing.OpenTelemetryEndpoint; -import java.net.InetSocketAddress; -import javax.annotation.Nullable; - -final class LettuceNetAttributesGetter - implements NetClientAttributesGetter { - - @Nullable - @Override - public String getServerAddress(OpenTelemetryEndpoint openTelemetryEndpoint) { - return null; - } - - @Nullable - @Override - public Integer getServerPort(OpenTelemetryEndpoint openTelemetryEndpoint) { - return null; - } - - @Nullable - @Override - public InetSocketAddress getServerInetSocketAddress( - OpenTelemetryEndpoint openTelemetryEndpoint, @Nullable Void unused) { - return openTelemetryEndpoint.address; - } -} diff --git a/instrumentation/lettuce/lettuce-5.1/library/src/main/java/io/opentelemetry/instrumentation/lettuce/v5_1/LettuceServerAttributesGetter.java b/instrumentation/lettuce/lettuce-5.1/library/src/main/java/io/opentelemetry/instrumentation/lettuce/v5_1/LettuceServerAttributesGetter.java new file mode 100644 index 000000000000..4a26de0af5a2 --- /dev/null +++ b/instrumentation/lettuce/lettuce-5.1/library/src/main/java/io/opentelemetry/instrumentation/lettuce/v5_1/LettuceServerAttributesGetter.java @@ -0,0 +1,42 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.lettuce.v5_1; + +import io.opentelemetry.instrumentation.api.semconv.network.NetworkAttributesGetter; +import io.opentelemetry.instrumentation.api.semconv.network.ServerAttributesGetter; +import io.opentelemetry.instrumentation.lettuce.v5_1.OpenTelemetryTracing.OpenTelemetryEndpoint; +import java.net.InetSocketAddress; +import javax.annotation.Nullable; + +class LettuceServerAttributesGetter + implements ServerAttributesGetter, + NetworkAttributesGetter { + + @Nullable + @Override + public String getServerAddress(OpenTelemetryEndpoint request) { + if (request.address != null) { + return request.address.getHostString(); + } + return null; + } + + @Nullable + @Override + public Integer getServerPort(OpenTelemetryEndpoint request) { + if (request.address != null) { + return request.address.getPort(); + } + return null; + } + + @Nullable + @Override + public InetSocketAddress getNetworkPeerInetSocketAddress( + OpenTelemetryEndpoint openTelemetryEndpoint, @Nullable Void unused) { + return openTelemetryEndpoint.address; + } +} diff --git a/instrumentation/lettuce/lettuce-5.1/library/src/main/java/io/opentelemetry/instrumentation/lettuce/v5_1/LettuceTelemetry.java b/instrumentation/lettuce/lettuce-5.1/library/src/main/java/io/opentelemetry/instrumentation/lettuce/v5_1/LettuceTelemetry.java index 037e9215d993..665f5f236fb3 100644 --- a/instrumentation/lettuce/lettuce-5.1/library/src/main/java/io/opentelemetry/instrumentation/lettuce/v5_1/LettuceTelemetry.java +++ b/instrumentation/lettuce/lettuce-5.1/library/src/main/java/io/opentelemetry/instrumentation/lettuce/v5_1/LettuceTelemetry.java @@ -9,7 +9,7 @@ import io.opentelemetry.api.OpenTelemetry; import io.opentelemetry.api.trace.Tracer; import io.opentelemetry.api.trace.TracerBuilder; -import io.opentelemetry.instrumentation.api.db.RedisCommandSanitizer; +import io.opentelemetry.instrumentation.api.incubator.semconv.db.RedisCommandSanitizer; import io.opentelemetry.instrumentation.api.internal.EmbeddedInstrumentationProperties; /** Entrypoint for instrumenting Lettuce or clients. */ diff --git a/instrumentation/lettuce/lettuce-5.1/library/src/main/java/io/opentelemetry/instrumentation/lettuce/v5_1/OpenTelemetryTracing.java b/instrumentation/lettuce/lettuce-5.1/library/src/main/java/io/opentelemetry/instrumentation/lettuce/v5_1/OpenTelemetryTracing.java index f71bf0d29402..d040634872b6 100644 --- a/instrumentation/lettuce/lettuce-5.1/library/src/main/java/io/opentelemetry/instrumentation/lettuce/v5_1/OpenTelemetryTracing.java +++ b/instrumentation/lettuce/lettuce-5.1/library/src/main/java/io/opentelemetry/instrumentation/lettuce/v5_1/OpenTelemetryTracing.java @@ -10,12 +10,14 @@ import com.google.errorprone.annotations.CanIgnoreReturnValue; import io.lettuce.core.output.CommandOutput; import io.lettuce.core.protocol.CompleteableCommand; +import io.lettuce.core.protocol.OtelCommandArgsUtil; 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.AttributeKey; import io.opentelemetry.api.common.Attributes; import io.opentelemetry.api.common.AttributesBuilder; import io.opentelemetry.api.trace.Span; @@ -23,11 +25,10 @@ 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.incubator.semconv.db.RedisCommandSanitizer; import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.net.NetClientAttributesExtractor; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes.DbSystemValues; +import io.opentelemetry.instrumentation.api.semconv.network.NetworkAttributesExtractor; +import io.opentelemetry.instrumentation.api.semconv.network.ServerAttributesExtractor; import java.net.InetSocketAddress; import java.net.SocketAddress; import java.time.Instant; @@ -37,8 +38,16 @@ final class OpenTelemetryTracing implements Tracing { - private static final AttributesExtractor netAttributesExtractor = - NetClientAttributesExtractor.create(new LettuceNetAttributesGetter()); + // copied from DbIncubatingAttributes + private static final AttributeKey DB_SYSTEM = AttributeKey.stringKey("db.system"); + private static final AttributeKey DB_STATEMENT = AttributeKey.stringKey("db.statement"); + // copied from DbIncubatingAttributes.DbSystemValues + private static final String REDIS = "redis"; + + private static final AttributesExtractor serverAttributesExtractor = + ServerAttributesExtractor.create(new LettuceServerAttributesGetter()); + private static final AttributesExtractor networkAttributesExtractor = + NetworkAttributesExtractor.create(new LettuceServerAttributesGetter()); private final TracerProvider tracerProvider; OpenTelemetryTracing(io.opentelemetry.api.trace.Tracer tracer, RedisCommandSanitizer sanitizer) { @@ -151,7 +160,7 @@ private OpenTelemetrySpan nextSpan(Context context) { .spanBuilder("redis") .setSpanKind(SpanKind.CLIENT) .setParent(context) - .setAttribute(SemanticAttributes.DB_SYSTEM, DbSystemValues.REDIS); + .setAttribute(DB_SYSTEM, REDIS); return new OpenTelemetrySpan(context, spanBuilder, sanitizer); } } @@ -170,7 +179,8 @@ private static class OpenTelemetrySpan extends Tracer.Span { @Nullable private List events; @Nullable private Throwable error; @Nullable private Span span; - @Nullable private String args; + @Nullable private List argsList; + @Nullable private String argsString; OpenTelemetrySpan(Context context, SpanBuilder spanBuilder, RedisCommandSanitizer sanitizer) { this.context = context; @@ -202,7 +212,8 @@ public synchronized Tracer.Span remoteEndpoint(Endpoint endpoint) { private void fillEndpoint(OpenTelemetryEndpoint endpoint) { AttributesBuilder attributesBuilder = Attributes.builder(); Context currentContext = span == null ? context : context.with(span); - netAttributesExtractor.onEnd(attributesBuilder, currentContext, endpoint, null, null); + serverAttributesExtractor.onStart(attributesBuilder, currentContext, endpoint); + networkAttributesExtractor.onEnd(attributesBuilder, currentContext, endpoint, null, null); if (span != null) { span.setAllAttributes(attributesBuilder.build()); } else { @@ -224,7 +235,7 @@ public synchronized Tracer.Span start(RedisCommand command) { span.updateName(command.getType().name()); if (command.getArgs() != null) { - args = command.getArgs().toCommandString(); + argsList = OtelCommandArgsUtil.getCommandArgs(command.getArgs()); } if (command instanceof CompleteableCommand) { @@ -294,7 +305,7 @@ public synchronized Tracer.Span annotate(String value) { @CanIgnoreReturnValue public synchronized Tracer.Span tag(String key, String value) { if (key.equals("redis.args")) { - args = value; + argsString = value; return this; } if (span != null) { @@ -325,8 +336,9 @@ public synchronized void finish() { private void finish(Span span) { if (name != null) { - String statement = sanitizer.sanitize(name, splitArgs(args)); - span.setAttribute(SemanticAttributes.DB_STATEMENT, statement); + String statement = + sanitizer.sanitize(name, argsList != null ? argsList : splitArgs(argsString)); + span.setAttribute(DB_STATEMENT, statement); } span.end(); } diff --git a/instrumentation/lettuce/lettuce-5.1/library/src/test/groovy/io/opentelemetry/instrumentation/lettuce/v5_1/LettuceAsyncClientTest.groovy b/instrumentation/lettuce/lettuce-5.1/library/src/test/groovy/io/opentelemetry/instrumentation/lettuce/v5_1/LettuceAsyncClientTest.groovy deleted file mode 100644 index ce86466732b9..000000000000 --- a/instrumentation/lettuce/lettuce-5.1/library/src/test/groovy/io/opentelemetry/instrumentation/lettuce/v5_1/LettuceAsyncClientTest.groovy +++ /dev/null @@ -1,27 +0,0 @@ -/* - * 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 LettuceAsyncClientTest extends AbstractLettuceAsyncClientTest implements LibraryTestTrait { - @Override - RedisClient createClient(String uri) { - return RedisClient.create( - ClientResources.builder() - .tracing(LettuceTelemetry.create(getOpenTelemetry()).newTracing()) - .build(), - uri) - } - - @Override - boolean testCallback() { - // context is not propagated into callbacks - return false - } -} diff --git a/instrumentation/lettuce/lettuce-5.1/library/src/test/groovy/io/opentelemetry/instrumentation/lettuce/v5_1/LettuceReactiveClientTest.groovy b/instrumentation/lettuce/lettuce-5.1/library/src/test/groovy/io/opentelemetry/instrumentation/lettuce/v5_1/LettuceReactiveClientTest.groovy deleted file mode 100644 index be4dbcf8cb20..000000000000 --- a/instrumentation/lettuce/lettuce-5.1/library/src/test/groovy/io/opentelemetry/instrumentation/lettuce/v5_1/LettuceReactiveClientTest.groovy +++ /dev/null @@ -1,34 +0,0 @@ -/* - * 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.v3_1.ContextPropagationOperator -import io.opentelemetry.instrumentation.test.LibraryTestTrait -import spock.lang.Shared - -class LettuceReactiveClientTest extends AbstractLettuceReactiveClientTest implements LibraryTestTrait { - @Shared - ContextPropagationOperator tracingOperator = ContextPropagationOperator.create() - - @Override - RedisClient createClient(String uri) { - return RedisClient.create( - ClientResources.builder() - .tracing(LettuceTelemetry.create(getOpenTelemetry()).newTracing()) - .build(), - uri) - } - - def setupSpec() { - tracingOperator.registerOnEachOperator() - } - - def cleanupSpec() { - tracingOperator.resetOnEachOperator() - } -} diff --git a/instrumentation/lettuce/lettuce-5.1/library/src/test/groovy/io/opentelemetry/instrumentation/lettuce/v5_1/LettuceSyncClientAuthTest.groovy b/instrumentation/lettuce/lettuce-5.1/library/src/test/groovy/io/opentelemetry/instrumentation/lettuce/v5_1/LettuceSyncClientAuthTest.groovy deleted file mode 100644 index 7cb208b5b2ff..000000000000 --- a/instrumentation/lettuce/lettuce-5.1/library/src/test/groovy/io/opentelemetry/instrumentation/lettuce/v5_1/LettuceSyncClientAuthTest.groovy +++ /dev/null @@ -1,21 +0,0 @@ -/* - * 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(LettuceTelemetry.create(getOpenTelemetry()).newTracing()) - .build(), - uri) - } -} diff --git a/instrumentation/lettuce/lettuce-5.1/library/src/test/groovy/io/opentelemetry/instrumentation/lettuce/v5_1/LettuceSyncClientTest.groovy b/instrumentation/lettuce/lettuce-5.1/library/src/test/groovy/io/opentelemetry/instrumentation/lettuce/v5_1/LettuceSyncClientTest.groovy deleted file mode 100644 index 76c536376387..000000000000 --- a/instrumentation/lettuce/lettuce-5.1/library/src/test/groovy/io/opentelemetry/instrumentation/lettuce/v5_1/LettuceSyncClientTest.groovy +++ /dev/null @@ -1,21 +0,0 @@ -/* - * 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(LettuceTelemetry.create(getOpenTelemetry()).newTracing()) - .build(), - uri) - } -} diff --git a/instrumentation/lettuce/lettuce-5.1/library/src/test/java/io/opentelemetry/instrumentation/lettuce/v5_1/LettuceAsyncClientTest.java b/instrumentation/lettuce/lettuce-5.1/library/src/test/java/io/opentelemetry/instrumentation/lettuce/v5_1/LettuceAsyncClientTest.java new file mode 100644 index 000000000000..38bf96282f25 --- /dev/null +++ b/instrumentation/lettuce/lettuce-5.1/library/src/test/java/io/opentelemetry/instrumentation/lettuce/v5_1/LettuceAsyncClientTest.java @@ -0,0 +1,39 @@ +/* + * 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.testing.junit.InstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.LibraryInstrumentationExtension; +import org.junit.jupiter.api.extension.RegisterExtension; + +class LettuceAsyncClientTest extends AbstractLettuceAsyncClientTest { + @RegisterExtension + static InstrumentationExtension testing = LibraryInstrumentationExtension.create(); + + @Override + public InstrumentationExtension getInstrumentationExtension() { + return testing; + } + + @Override + protected RedisClient createClient(String uri) { + return RedisClient.create( + ClientResources.builder() + .tracing( + LettuceTelemetry.create(getInstrumentationExtension().getOpenTelemetry()) + .newTracing()) + .build(), + uri); + } + + @Override + boolean testCallback() { + // context is not propagated into callbacks + return false; + } +} diff --git a/instrumentation/lettuce/lettuce-5.1/library/src/test/java/io/opentelemetry/instrumentation/lettuce/v5_1/LettuceReactiveClientTest.java b/instrumentation/lettuce/lettuce-5.1/library/src/test/java/io/opentelemetry/instrumentation/lettuce/v5_1/LettuceReactiveClientTest.java new file mode 100644 index 000000000000..5a256549e18b --- /dev/null +++ b/instrumentation/lettuce/lettuce-5.1/library/src/test/java/io/opentelemetry/instrumentation/lettuce/v5_1/LettuceReactiveClientTest.java @@ -0,0 +1,48 @@ +/* + * 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.v3_1.ContextPropagationOperator; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.LibraryInstrumentationExtension; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.RegisterExtension; + +class LettuceReactiveClientTest extends AbstractLettuceReactiveClientTest { + @RegisterExtension + static final InstrumentationExtension testing = LibraryInstrumentationExtension.create(); + + @Override + public InstrumentationExtension getInstrumentationExtension() { + return testing; + } + + static final ContextPropagationOperator tracingOperator = ContextPropagationOperator.create(); + + @Override + protected RedisClient createClient(String uri) { + return RedisClient.create( + ClientResources.builder() + .tracing( + LettuceTelemetry.create(getInstrumentationExtension().getOpenTelemetry()) + .newTracing()) + .build(), + uri); + } + + @BeforeEach + void setup() { + tracingOperator.registerOnEachOperator(); + } + + @AfterEach + void cleanup() { + tracingOperator.resetOnEachOperator(); + } +} diff --git a/instrumentation/lettuce/lettuce-5.1/library/src/test/java/io/opentelemetry/instrumentation/lettuce/v5_1/LettuceSyncClientAuthTest.java b/instrumentation/lettuce/lettuce-5.1/library/src/test/java/io/opentelemetry/instrumentation/lettuce/v5_1/LettuceSyncClientAuthTest.java new file mode 100644 index 000000000000..357ae8e09e74 --- /dev/null +++ b/instrumentation/lettuce/lettuce-5.1/library/src/test/java/io/opentelemetry/instrumentation/lettuce/v5_1/LettuceSyncClientAuthTest.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.RedisClient; +import io.lettuce.core.resource.ClientResources; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.LibraryInstrumentationExtension; +import org.junit.jupiter.api.extension.RegisterExtension; + +class LettuceSyncClientAuthTest extends AbstractLettuceSyncClientAuthTest { + @RegisterExtension + static InstrumentationExtension testing = LibraryInstrumentationExtension.create(); + + @Override + public InstrumentationExtension getInstrumentationExtension() { + return testing; + } + + @Override + protected RedisClient createClient(String uri) { + return RedisClient.create( + ClientResources.builder() + .tracing( + LettuceTelemetry.create(getInstrumentationExtension().getOpenTelemetry()) + .newTracing()) + .build(), + uri); + } +} diff --git a/instrumentation/lettuce/lettuce-5.1/library/src/test/java/io/opentelemetry/instrumentation/lettuce/v5_1/LettuceSyncClientTest.java b/instrumentation/lettuce/lettuce-5.1/library/src/test/java/io/opentelemetry/instrumentation/lettuce/v5_1/LettuceSyncClientTest.java new file mode 100644 index 000000000000..7bfb8e452769 --- /dev/null +++ b/instrumentation/lettuce/lettuce-5.1/library/src/test/java/io/opentelemetry/instrumentation/lettuce/v5_1/LettuceSyncClientTest.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.RedisClient; +import io.lettuce.core.resource.ClientResources; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.LibraryInstrumentationExtension; +import org.junit.jupiter.api.extension.RegisterExtension; + +class LettuceSyncClientTest extends AbstractLettuceSyncClientTest { + @RegisterExtension + static final InstrumentationExtension testing = LibraryInstrumentationExtension.create(); + + @Override + public InstrumentationExtension getInstrumentationExtension() { + return testing; + } + + @Override + protected RedisClient createClient(String uri) { + return RedisClient.create( + ClientResources.builder() + .tracing( + LettuceTelemetry.create(getInstrumentationExtension().getOpenTelemetry()) + .newTracing()) + .build(), + uri); + } +} diff --git a/instrumentation/lettuce/lettuce-5.1/testing/build.gradle.kts b/instrumentation/lettuce/lettuce-5.1/testing/build.gradle.kts index e8780d1fa018..577605b637a4 100644 --- a/instrumentation/lettuce/lettuce-5.1/testing/build.gradle.kts +++ b/instrumentation/lettuce/lettuce-5.1/testing/build.gradle.kts @@ -5,12 +5,11 @@ plugins { dependencies { api(project(":testing-common")) - api("io.lettuce:lettuce-core:5.1.0.RELEASE") + // 6.0+ added protocolVersion access which allows forcing RESP2 for consistency in tests + compileOnly("io.lettuce:lettuce-core:6.0.0.RELEASE") implementation("org.testcontainers:testcontainers") implementation("com.google.guava:guava") - implementation("org.apache.groovy:groovy") implementation("io.opentelemetry:opentelemetry-api") - implementation("org.spockframework:spock-core") } diff --git a/instrumentation/lettuce/lettuce-5.1/testing/src/main/groovy/io/opentelemetry/instrumentation/lettuce/v5_1/AbstractLettuceAsyncClientTest.groovy b/instrumentation/lettuce/lettuce-5.1/testing/src/main/groovy/io/opentelemetry/instrumentation/lettuce/v5_1/AbstractLettuceAsyncClientTest.groovy deleted file mode 100644 index 0cc86d33efdb..000000000000 --- a/instrumentation/lettuce/lettuce-5.1/testing/src/main/groovy/io/opentelemetry/instrumentation/lettuce/v5_1/AbstractLettuceAsyncClientTest.groovy +++ /dev/null @@ -1,449 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.lettuce.v5_1 - -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 org.testcontainers.containers.GenericContainer -import spock.lang.Shared -import spock.util.concurrent.AsyncConditions - -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 static io.opentelemetry.api.trace.SpanKind.CLIENT -import static io.opentelemetry.api.trace.SpanKind.INTERNAL - -abstract class AbstractLettuceAsyncClientTest extends InstrumentationSpecification { - public static final int DB_INDEX = 0 - - private static GenericContainer redisServer = new GenericContainer<>("redis:6.2.3-alpine").withExposedPorts(6379) - - abstract RedisClient createClient(String uri) - - @Shared - String host - @Shared - String expectedHostAttributeValue - @Shared - int port - @Shared - int incorrectPort - @Shared - String dbUriNonExistent - @Shared - String embeddedDbUri - - @Shared - Map testHashMap = [ - firstname: "John", - lastname : "Doe", - age : "53" - ] - - RedisClient redisClient - StatefulConnection connection - RedisAsyncCommands asyncCommands - RedisCommands syncCommands - - def setup() { - redisServer.start() - - port = redisServer.getMappedPort(6379) - host = redisServer.getHost() - String dbAddr = host + ":" + port + "/" + DB_INDEX - embeddedDbUri = "redis://" + dbAddr - expectedHostAttributeValue = host == "127.0.0.1" ? null : host - - incorrectPort = PortUtils.findOpenPort() - String dbAddrNonExistent = host + ":" + incorrectPort + "/" + DB_INDEX - dbUriNonExistent = "redis://" + dbAddrNonExistent - - redisClient = createClient(embeddedDbUri) - 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() - redisClient.shutdown() - redisServer.stop() - } - - boolean testCallback() { - return true - } - - def T runWithCallbackSpan(String spanName, Closure callback) { - if (testCallback()) { - return runWithSpan(spanName, callback) - } - return callback.call() - } - - 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() - testConnectionClient.shutdown() - } - - 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) {} - - cleanup: - testConnectionClient.shutdown() - } - - 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_SOCK_PEER_ADDR" "127.0.0.1" - "$SemanticAttributes.NET_SOCK_PEER_NAME" expectedHostAttributeValue - "$SemanticAttributes.NET_SOCK_PEER_PORT" port - "$SemanticAttributes.DB_SYSTEM" "redis" - "$SemanticAttributes.DB_STATEMENT" "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) { - runWithCallbackSpan("callback") { - conds.evaluate { - assert res == "TESTVAL" - } - } - } - } - - when: - runWithSpan("parent") { - RedisFuture redisFuture = asyncCommands.get("TESTKEY") - redisFuture.thenAccept(consumer) - } - - then: - conds.await(10) - assertTraces(1) { - trace(0, 2 + (testCallback() ? 1 : 0)) { - span(0) { - name "parent" - kind INTERNAL - hasNoParent() - } - span(1) { - name "GET" - kind CLIENT - attributes { - "$SemanticAttributes.NET_SOCK_PEER_ADDR" "127.0.0.1" - "$SemanticAttributes.NET_SOCK_PEER_NAME" expectedHostAttributeValue - "$SemanticAttributes.NET_SOCK_PEER_PORT" port - "$SemanticAttributes.DB_SYSTEM" "redis" - "$SemanticAttributes.DB_STATEMENT" "GET TESTKEY" - } - event(0) { - eventName "redis.encode.start" - } - event(1) { - eventName "redis.encode.end" - } - } - if (testCallback()) { - span(2) { - name "callback" - kind INTERNAL - childOf(span(0)) - } - } - } - } - } - - // 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) { - runWithCallbackSpan("callback1") { - conds.evaluate { - assert res == null - assert throwable == null - } - } - return (res == null ? successStr : res) - } - } - Function secondStage = new Function() { - @Override - Object apply(String input) { - runWithCallbackSpan("callback2") { - conds.evaluate { - assert input == successStr - } - } - return null - } - } - - when: - runWithSpan("parent") { - RedisFuture redisFuture = asyncCommands.get("NON_EXISTENT_KEY") - redisFuture.handleAsync(firstStage).thenApply(secondStage) - } - - then: - conds.await(10) - assertTraces(1) { - trace(0, 2 + (testCallback() ? 2 : 0)) { - span(0) { - name "parent" - kind INTERNAL - hasNoParent() - } - span(1) { - name "GET" - kind CLIENT - childOf(span(0)) - attributes { - "$SemanticAttributes.NET_SOCK_PEER_ADDR" "127.0.0.1" - "$SemanticAttributes.NET_SOCK_PEER_NAME" expectedHostAttributeValue - "$SemanticAttributes.NET_SOCK_PEER_PORT" port - "$SemanticAttributes.DB_SYSTEM" "redis" - "$SemanticAttributes.DB_STATEMENT" "GET NON_EXISTENT_KEY" - } - event(0) { - eventName "redis.encode.start" - } - event(1) { - eventName "redis.encode.end" - } - } - if (testCallback()) { - span(2) { - name "callback1" - kind INTERNAL - childOf(span(0)) - } - span(3) { - name "callback2" - kind INTERNAL - childOf(span(0)) - } - } - } - } - } - - def "command with no arguments using a biconsumer"() { - setup: - def conds = new AsyncConditions() - BiConsumer biConsumer = new BiConsumer() { - @Override - void accept(String keyRetrieved, Throwable throwable) { - runWithCallbackSpan("callback") { - conds.evaluate { - assert keyRetrieved != null - } - } - } - } - - when: - runWithSpan("parent") { - RedisFuture redisFuture = asyncCommands.randomkey() - redisFuture.whenCompleteAsync(biConsumer) - } - - then: - conds.await(10) - assertTraces(1) { - trace(0, 2 + (testCallback() ? 1 : 0)) { - span(0) { - name "parent" - kind INTERNAL - hasNoParent() - } - span(1) { - name "RANDOMKEY" - kind CLIENT - childOf(span(0)) - attributes { - "$SemanticAttributes.NET_SOCK_PEER_ADDR" "127.0.0.1" - "$SemanticAttributes.NET_SOCK_PEER_NAME" expectedHostAttributeValue - "$SemanticAttributes.NET_SOCK_PEER_PORT" port - "$SemanticAttributes.DB_STATEMENT" "RANDOMKEY" - "$SemanticAttributes.DB_SYSTEM" "redis" - } - event(0) { - eventName "redis.encode.start" - } - event(1) { - eventName "redis.encode.end" - } - } - if (testCallback()) { - span(2) { - name "callback" - kind INTERNAL - childOf(span(0)) - } - } - } - } - } - - 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(10) - assertTraces(2) { - trace(0, 1) { - span(0) { - name "HMSET" - kind CLIENT - attributes { - "$SemanticAttributes.NET_SOCK_PEER_ADDR" "127.0.0.1" - "$SemanticAttributes.NET_SOCK_PEER_NAME" expectedHostAttributeValue - "$SemanticAttributes.NET_SOCK_PEER_PORT" port - "$SemanticAttributes.DB_SYSTEM" "redis" - "$SemanticAttributes.DB_STATEMENT" "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_SOCK_PEER_ADDR" "127.0.0.1" - "$SemanticAttributes.NET_SOCK_PEER_NAME" expectedHostAttributeValue - "$SemanticAttributes.NET_SOCK_PEER_PORT" port - "$SemanticAttributes.DB_SYSTEM" "redis" - "$SemanticAttributes.DB_STATEMENT" "HGETALL TESTHM" - } - event(0) { - eventName "redis.encode.start" - } - event(1) { - eventName "redis.encode.end" - } - } - } - } - } -} diff --git a/instrumentation/lettuce/lettuce-5.1/testing/src/main/groovy/io/opentelemetry/instrumentation/lettuce/v5_1/AbstractLettuceReactiveClientTest.groovy b/instrumentation/lettuce/lettuce-5.1/testing/src/main/groovy/io/opentelemetry/instrumentation/lettuce/v5_1/AbstractLettuceReactiveClientTest.groovy deleted file mode 100644 index 5f3e84915ff1..000000000000 --- a/instrumentation/lettuce/lettuce-5.1/testing/src/main/groovy/io/opentelemetry/instrumentation/lettuce/v5_1/AbstractLettuceReactiveClientTest.groovy +++ /dev/null @@ -1,398 +0,0 @@ -/* - * 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.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.semconv.trace.attributes.SemanticAttributes -import org.testcontainers.containers.GenericContainer -import spock.lang.Shared -import spock.util.concurrent.AsyncConditions - -import java.util.function.Consumer - -import static io.opentelemetry.api.trace.SpanKind.CLIENT -import static io.opentelemetry.api.trace.SpanKind.INTERNAL - -abstract class AbstractLettuceReactiveClientTest extends InstrumentationSpecification { - public static final int DB_INDEX = 0 - - private static GenericContainer redisServer = new GenericContainer<>("redis:6.2.3-alpine").withExposedPorts(6379) - - abstract RedisClient createClient(String uri) - - @Shared - String expectedHostAttributeValue - @Shared - int port - @Shared - String embeddedDbUri - - RedisClient redisClient - StatefulConnection connection - RedisReactiveCommands reactiveCommands - RedisCommands syncCommands - - def setup() { - redisServer.start() - - port = redisServer.getMappedPort(6379) - String host = redisServer.getHost() - String dbAddr = host + ":" + port + "/" + DB_INDEX - embeddedDbUri = "redis://" + dbAddr - expectedHostAttributeValue = host == "127.0.0.1" ? null : host - - redisClient = createClient(embeddedDbUri) - 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) { - runWithSpan("callback") { - conds.evaluate { - assert res == "OK" - } - } - } - } - - when: - runWithSpan("parent") { - reactiveCommands.set("TESTSETKEY", "TESTSETVAL").subscribe(consumer) - } - - then: - conds.await(10) - assertTraces(1) { - trace(0, 3) { - span(0) { - name "parent" - kind INTERNAL - hasNoParent() - } - span(1) { - name "SET" - kind CLIENT - childOf(span(0)) - attributes { - "$SemanticAttributes.NET_SOCK_PEER_ADDR" "127.0.0.1" - "$SemanticAttributes.NET_SOCK_PEER_NAME" expectedHostAttributeValue - "$SemanticAttributes.NET_SOCK_PEER_PORT" port - "$SemanticAttributes.DB_SYSTEM" "redis" - "$SemanticAttributes.DB_STATEMENT" "SET TESTSETKEY ?" - } - event(0) { - eventName "redis.encode.start" - } - event(1) { - eventName "redis.encode.end" - } - } - span(2) { - name "callback" - kind INTERNAL - childOf(span(0)) - } - } - } - } - - 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(10) - assertTraces(1) { - trace(0, 1) { - span(0) { - name "GET" - kind CLIENT - attributes { - "$SemanticAttributes.NET_SOCK_PEER_ADDR" "127.0.0.1" - "$SemanticAttributes.NET_SOCK_PEER_NAME" expectedHostAttributeValue - "$SemanticAttributes.NET_SOCK_PEER_PORT" port - "$SemanticAttributes.DB_SYSTEM" "redis" - "$SemanticAttributes.DB_STATEMENT" "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: - runWithSpan("parent") { - reactiveCommands.get("NON_EXISTENT_KEY").defaultIfEmpty(defaultVal).subscribe { - res -> - runWithSpan("callback") { - conds.evaluate { - assert res == defaultVal - } - } - } - } - - then: - conds.await(10) - assertTraces(1) { - trace(0, 3) { - span(0) { - name "parent" - kind INTERNAL - hasNoParent() - } - span(1) { - name "GET" - kind CLIENT - childOf(span(0)) - attributes { - "$SemanticAttributes.NET_SOCK_PEER_ADDR" "127.0.0.1" - "$SemanticAttributes.NET_SOCK_PEER_NAME" expectedHostAttributeValue - "$SemanticAttributes.NET_SOCK_PEER_PORT" port - "$SemanticAttributes.DB_SYSTEM" "redis" - "$SemanticAttributes.DB_STATEMENT" "GET NON_EXISTENT_KEY" - } - event(0) { - eventName "redis.encode.start" - } - event(1) { - eventName "redis.encode.end" - } - } - span(2) { - name "callback" - kind INTERNAL - childOf(span(0)) - } - } - } - - } - - def "command with no arguments"() { - setup: - def conds = new AsyncConditions() - - when: - reactiveCommands.randomkey().subscribe { - res -> - conds.evaluate { - assert res == "TESTKEY" - } - } - - then: - conds.await(10) - assertTraces(1) { - trace(0, 1) { - span(0) { - name "RANDOMKEY" - kind CLIENT - attributes { - "$SemanticAttributes.NET_SOCK_PEER_ADDR" "127.0.0.1" - "$SemanticAttributes.NET_SOCK_PEER_NAME" expectedHostAttributeValue - "$SemanticAttributes.NET_SOCK_PEER_PORT" port - "$SemanticAttributes.DB_STATEMENT" "RANDOMKEY" - "$SemanticAttributes.DB_SYSTEM" "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_SOCK_PEER_ADDR" "127.0.0.1" - "$SemanticAttributes.NET_SOCK_PEER_NAME" expectedHostAttributeValue - "$SemanticAttributes.NET_SOCK_PEER_PORT" port - "$SemanticAttributes.DB_STATEMENT" "COMMAND" - "$SemanticAttributes.DB_SYSTEM" "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: - runWithSpan("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_SOCK_PEER_ADDR" "127.0.0.1" - "$SemanticAttributes.NET_SOCK_PEER_NAME" expectedHostAttributeValue - "$SemanticAttributes.NET_SOCK_PEER_PORT" port - "$SemanticAttributes.DB_SYSTEM" "redis" - "$SemanticAttributes.DB_STATEMENT" "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_SOCK_PEER_ADDR" "127.0.0.1" - "$SemanticAttributes.NET_SOCK_PEER_NAME" expectedHostAttributeValue - "$SemanticAttributes.NET_SOCK_PEER_PORT" port - "$SemanticAttributes.DB_SYSTEM" "redis" - "$SemanticAttributes.DB_STATEMENT" "GET a" - } - event(0) { - eventName "redis.encode.start" - } - event(1) { - eventName "redis.encode.end" - } - } - } - } - } - - def "async subscriber"() { - when: - runWithSpan("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_SOCK_PEER_ADDR" "127.0.0.1" - "$SemanticAttributes.NET_SOCK_PEER_NAME" expectedHostAttributeValue - "$SemanticAttributes.NET_SOCK_PEER_PORT" port - "$SemanticAttributes.DB_SYSTEM" "redis" - "$SemanticAttributes.DB_STATEMENT" "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_SOCK_PEER_ADDR" "127.0.0.1" - "$SemanticAttributes.NET_SOCK_PEER_NAME" expectedHostAttributeValue - "$SemanticAttributes.NET_SOCK_PEER_PORT" port - "$SemanticAttributes.DB_SYSTEM" "redis" - "$SemanticAttributes.DB_STATEMENT" "GET a" - } - event(0) { - eventName "redis.encode.start" - } - event(1) { - eventName "redis.encode.end" - } - } - } - } - } -} diff --git a/instrumentation/lettuce/lettuce-5.1/testing/src/main/groovy/io/opentelemetry/instrumentation/lettuce/v5_1/AbstractLettuceSyncClientAuthTest.groovy b/instrumentation/lettuce/lettuce-5.1/testing/src/main/groovy/io/opentelemetry/instrumentation/lettuce/v5_1/AbstractLettuceSyncClientAuthTest.groovy deleted file mode 100644 index b0da937f24b2..000000000000 --- a/instrumentation/lettuce/lettuce-5.1/testing/src/main/groovy/io/opentelemetry/instrumentation/lettuce/v5_1/AbstractLettuceSyncClientAuthTest.groovy +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.lettuce.v5_1 - -import io.lettuce.core.RedisClient -import io.opentelemetry.instrumentation.test.InstrumentationSpecification -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes -import org.testcontainers.containers.GenericContainer -import spock.lang.Shared - -import static io.opentelemetry.api.trace.SpanKind.CLIENT - -abstract class AbstractLettuceSyncClientAuthTest extends InstrumentationSpecification { - public static final int DB_INDEX = 0 - - private static GenericContainer redisServer = new GenericContainer<>("redis:6.2.3-alpine").withExposedPorts(6379) - - abstract RedisClient createClient(String uri) - - @Shared - String expectedHostAttributeValue - @Shared - int port - @Shared - String password - - RedisClient redisClient - - def setupSpec() { - password = "password" - - redisServer = redisServer - .withCommand("redis-server", "--requirepass $password") - } - - def setup() { - redisServer.start() - - port = redisServer.getMappedPort(6379) - String host = redisServer.getHost() - String dbAddr = host + ":" + port + "/" + DB_INDEX - String embeddedDbUri = "redis://" + dbAddr - expectedHostAttributeValue = host == "127.0.0.1" ? null : host - - redisClient = createClient(embeddedDbUri) - redisClient.setOptions(LettuceTestUtil.CLIENT_OPTIONS) - } - - def cleanup() { - redisClient.shutdown() - 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_SOCK_PEER_ADDR" "127.0.0.1" - "$SemanticAttributes.NET_SOCK_PEER_NAME" expectedHostAttributeValue - "$SemanticAttributes.NET_SOCK_PEER_PORT" port - "$SemanticAttributes.DB_SYSTEM" "redis" - "$SemanticAttributes.DB_STATEMENT" "AUTH ?" - } - event(0) { - eventName "redis.encode.start" - } - event(1) { - eventName "redis.encode.end" - } - } - } - } - } -} diff --git a/instrumentation/lettuce/lettuce-5.1/testing/src/main/groovy/io/opentelemetry/instrumentation/lettuce/v5_1/AbstractLettuceSyncClientTest.groovy b/instrumentation/lettuce/lettuce-5.1/testing/src/main/groovy/io/opentelemetry/instrumentation/lettuce/v5_1/AbstractLettuceSyncClientTest.groovy deleted file mode 100644 index 98ead1521875..000000000000 --- a/instrumentation/lettuce/lettuce-5.1/testing/src/main/groovy/io/opentelemetry/instrumentation/lettuce/v5_1/AbstractLettuceSyncClientTest.groovy +++ /dev/null @@ -1,490 +0,0 @@ -/* - * 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.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.GenericContainer -import spock.lang.Shared - -import static io.opentelemetry.api.trace.SpanKind.CLIENT -import static io.opentelemetry.api.trace.StatusCode.ERROR -import static java.nio.charset.StandardCharsets.UTF_8 - -abstract class AbstractLettuceSyncClientTest extends InstrumentationSpecification { - public static final int DB_INDEX = 0 - - private static GenericContainer redisServer = new GenericContainer<>("redis:6.2.3-alpine").withExposedPorts(6379) - - abstract RedisClient createClient(String uri) - - @Shared - String expectedHostAttributeValue - @Shared - int port - @Shared - String dbUriNonExistent - @Shared - String embeddedDbLocalhostUri - - @Shared - Map testHashMap = [ - firstname: "John", - lastname : "Doe", - age : "53" - ] - - RedisClient redisClient - StatefulConnection connection - RedisCommands syncCommands - - def setup() { - redisServer.start() - - port = redisServer.getMappedPort(6379) - String host = redisServer.getHost() - String dbAddr = host + ":" + port + "/" + DB_INDEX - String embeddedDbUri = "redis://" + dbAddr - embeddedDbLocalhostUri = "redis://localhost:" + port + "/" + DB_INDEX - expectedHostAttributeValue = host == "127.0.0.1" ? null : host - - int incorrectPort = PortUtils.findOpenPort() - String dbAddrNonExistent = host + ":" + incorrectPort + "/" + DB_INDEX - dbUriNonExistent = "redis://" + dbAddrNonExistent - - redisClient = createClient(embeddedDbUri) - redisClient.setOptions(LettuceTestUtil.CLIENT_OPTIONS) - - connection = redisClient.connect() - syncCommands = connection.sync() - - syncCommands.set("TESTKEY", "TESTVAL") - syncCommands.hmset("TESTHM", testHashMap) - - // 2 sets - ignoreTracesAndClear(2) - } - - def cleanup() { - connection.close() - redisClient.shutdown() - redisServer.stop() - } - - def "connect"() { - when: - StatefulConnection connection = redisClient.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) {} - - cleanup: - testConnectionClient.shutdown() - } - - 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_SOCK_PEER_ADDR" "127.0.0.1" - "$SemanticAttributes.NET_SOCK_PEER_NAME" expectedHostAttributeValue - "$SemanticAttributes.NET_SOCK_PEER_PORT" port - "$SemanticAttributes.DB_SYSTEM" "redis" - "$SemanticAttributes.DB_STATEMENT" "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_SOCK_PEER_ADDR" "127.0.0.1" - "$SemanticAttributes.NET_SOCK_PEER_NAME" expectedHostAttributeValue - "$SemanticAttributes.NET_SOCK_PEER_PORT" port - "$SemanticAttributes.DB_SYSTEM" "redis" - "$SemanticAttributes.DB_STATEMENT" "SET TESTSETKEY ?" - } - event(0) { - eventName "redis.encode.start" - } - event(1) { - eventName "redis.encode.end" - } - } - } - } - - cleanup: - connection.close() - testConnectionClient.shutdown() - } - - 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_SOCK_PEER_ADDR" "127.0.0.1" - "$SemanticAttributes.NET_SOCK_PEER_NAME" expectedHostAttributeValue - "$SemanticAttributes.NET_SOCK_PEER_PORT" port - "$SemanticAttributes.DB_SYSTEM" "redis" - "$SemanticAttributes.DB_STATEMENT" "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_SOCK_PEER_ADDR" "127.0.0.1" - "$SemanticAttributes.NET_SOCK_PEER_NAME" expectedHostAttributeValue - "$SemanticAttributes.NET_SOCK_PEER_PORT" port - "$SemanticAttributes.DB_SYSTEM" "redis" - "$SemanticAttributes.DB_STATEMENT" "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_SOCK_PEER_ADDR" "127.0.0.1" - "$SemanticAttributes.NET_SOCK_PEER_NAME" expectedHostAttributeValue - "$SemanticAttributes.NET_SOCK_PEER_PORT" port - "$SemanticAttributes.DB_STATEMENT" "RANDOMKEY" - "$SemanticAttributes.DB_SYSTEM" "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_SOCK_PEER_ADDR" "127.0.0.1" - "$SemanticAttributes.NET_SOCK_PEER_NAME" expectedHostAttributeValue - "$SemanticAttributes.NET_SOCK_PEER_PORT" port - "$SemanticAttributes.DB_SYSTEM" "redis" - "$SemanticAttributes.DB_STATEMENT" "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_SOCK_PEER_ADDR" "127.0.0.1" - "$SemanticAttributes.NET_SOCK_PEER_NAME" expectedHostAttributeValue - "$SemanticAttributes.NET_SOCK_PEER_PORT" port - "$SemanticAttributes.DB_SYSTEM" "redis" - "$SemanticAttributes.DB_STATEMENT" "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_SOCK_PEER_ADDR" "127.0.0.1" - "$SemanticAttributes.NET_SOCK_PEER_NAME" expectedHostAttributeValue - "$SemanticAttributes.NET_SOCK_PEER_PORT" port - "$SemanticAttributes.DB_SYSTEM" "redis" - "$SemanticAttributes.DB_STATEMENT" "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_SOCK_PEER_ADDR" "127.0.0.1" - "$SemanticAttributes.NET_SOCK_PEER_NAME" expectedHostAttributeValue - "$SemanticAttributes.NET_SOCK_PEER_PORT" port - "$SemanticAttributes.DB_SYSTEM" "redis" - "$SemanticAttributes.DB_STATEMENT" "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_SOCK_PEER_ADDR" "127.0.0.1" - "$SemanticAttributes.NET_SOCK_PEER_NAME" expectedHostAttributeValue - "$SemanticAttributes.NET_SOCK_PEER_PORT" port - "$SemanticAttributes.DB_SYSTEM" "redis" - "$SemanticAttributes.DB_STATEMENT" "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_SOCK_PEER_ADDR" "127.0.0.1" - "$SemanticAttributes.NET_SOCK_PEER_NAME" expectedHostAttributeValue - "$SemanticAttributes.NET_SOCK_PEER_PORT" port - "$SemanticAttributes.DB_SYSTEM" "redis" - "$SemanticAttributes.DB_STATEMENT" "DEBUG SEGFAULT" - } - if (!Boolean.getBoolean("testLatestDeps")) { - // these are no longer recorded since Lettuce 6.1.6 - event(0) { - eventName "redis.encode.start" - } - event(1) { - eventName "redis.encode.end" - } - } - } - } - } - } - - 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_SOCK_PEER_ADDR" "127.0.0.1" - "$SemanticAttributes.NET_SOCK_PEER_NAME" expectedHostAttributeValue - "$SemanticAttributes.NET_SOCK_PEER_PORT" port - "$SemanticAttributes.DB_SYSTEM" "redis" - "$SemanticAttributes.DB_STATEMENT" "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/instrumentation/lettuce/lettuce-5.1/testing/src/main/groovy/io/opentelemetry/instrumentation/lettuce/v5_1/LettuceTestUtil.groovy b/instrumentation/lettuce/lettuce-5.1/testing/src/main/groovy/io/opentelemetry/instrumentation/lettuce/v5_1/LettuceTestUtil.groovy deleted file mode 100644 index 25fd109eab30..000000000000 --- a/instrumentation/lettuce/lettuce-5.1/testing/src/main/groovy/io/opentelemetry/instrumentation/lettuce/v5_1/LettuceTestUtil.groovy +++ /dev/null @@ -1,30 +0,0 @@ -/* - * 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/instrumentation/lettuce/lettuce-5.1/testing/src/main/java/io/opentelemetry/instrumentation/lettuce/v5_1/AbstractLettuceAsyncClientTest.java b/instrumentation/lettuce/lettuce-5.1/testing/src/main/java/io/opentelemetry/instrumentation/lettuce/v5_1/AbstractLettuceAsyncClientTest.java new file mode 100644 index 000000000000..4cb3f7ff171b --- /dev/null +++ b/instrumentation/lettuce/lettuce-5.1/testing/src/main/java/io/opentelemetry/instrumentation/lettuce/v5_1/AbstractLettuceAsyncClientTest.java @@ -0,0 +1,437 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.lettuce.v5_1; + +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.catchThrowable; + +import com.google.common.collect.ImmutableMap; +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.StatefulRedisConnection; +import io.lettuce.core.api.async.RedisAsyncCommands; +import io.lettuce.core.api.sync.RedisCommands; +import io.lettuce.core.codec.StringCodec; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.test.utils.PortUtils; +import io.opentelemetry.sdk.testing.assertj.SpanDataAssert; +import io.opentelemetry.semconv.NetworkAttributes; +import io.opentelemetry.semconv.ServerAttributes; +import io.opentelemetry.semconv.incubating.DbIncubatingAttributes; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +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.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +@SuppressWarnings({"InterruptedExceptionSwallowed"}) +public abstract class AbstractLettuceAsyncClientTest extends AbstractLettuceClientTest { + private static String dbUriNonExistent; + private static int incorrectPort; + + private static final ImmutableMap testHashMap = + ImmutableMap.of( + "firstname", "John", + "lastname", "Doe", + "age", "53"); + + private static RedisAsyncCommands asyncCommands; + + @BeforeAll + void setUp() throws UnknownHostException { + redisServer.start(); + host = redisServer.getHost(); + ip = InetAddress.getByName(host).getHostAddress(); + port = redisServer.getMappedPort(6379); + embeddedDbUri = "redis://" + host + ":" + port + "/" + DB_INDEX; + + incorrectPort = PortUtils.findOpenPort(); + dbUriNonExistent = "redis://" + host + ":" + incorrectPort + "/" + DB_INDEX; + + redisClient = createClient(embeddedDbUri); + redisClient.setOptions(LettuceTestUtil.CLIENT_OPTIONS); + + connection = redisClient.connect(); + asyncCommands = connection.async(); + RedisCommands syncCommands = connection.sync(); + + syncCommands.set("TESTKEY", "TESTVAL"); + + // 1 set trace + getInstrumentationExtension().waitForTraces(1); + getInstrumentationExtension().clearData(); + } + + @AfterAll + static void cleanUp() { + connection.close(); + redisClient.shutdown(); + redisServer.stop(); + } + + boolean testCallback() { + return true; + } + + protected boolean connectHasSpans() { + return false; + } + + @Test + void testConnectUsingGetOnConnectionFuture() throws Exception { + RedisClient testConnectionClient = RedisClient.create(embeddedDbUri); + testConnectionClient.setOptions(LettuceTestUtil.CLIENT_OPTIONS); + + ConnectionFuture> connectionFuture = + testConnectionClient.connectAsync( + StringCodec.UTF8, RedisURI.create("redis://" + host + ":" + port + "?timeout=3s")); + StatefulRedisConnection connection1 = connectionFuture.get(); + cleanup.deferCleanup(connection1); + cleanup.deferCleanup(testConnectionClient::shutdown); + + assertThat(connection1).isNotNull(); + if (connectHasSpans()) { + // ignore CLIENT SETINFO traces + getInstrumentationExtension().waitForTraces(2); + } else { + // Lettuce tracing does not trace connect + assertThat(getInstrumentationExtension().spans()).isEmpty(); + } + } + + @Test + void testConnectExceptionInsideTheConnectionFuture() { + RedisClient testConnectionClient = RedisClient.create(dbUriNonExistent); + testConnectionClient.setOptions(LettuceTestUtil.CLIENT_OPTIONS); + cleanup.deferCleanup(testConnectionClient::shutdown); + + Throwable thrown = + catchThrowable( + () -> { + ConnectionFuture> connectionFuture = + testConnectionClient.connectAsync( + StringCodec.UTF8, + RedisURI.create("redis://" + host + ":" + incorrectPort + "?timeout=3s")); + StatefulRedisConnection connection1 = connectionFuture.get(); + cleanup.deferCleanup(connection1); + assertThat(connection1).isNull(); + }); + + assertThat(thrown).isInstanceOf(ExecutionException.class); + + // Lettuce tracing does not trace connect + assertThat(getInstrumentationExtension().spans()).isEmpty(); + } + + @Test + void testSetCommandUsingFutureGetWithTimeout() throws Exception { + RedisFuture redisFuture = asyncCommands.set("TESTSETKEY", "TESTSETVAL"); + String res = redisFuture.get(3, TimeUnit.SECONDS); + + assertThat(res).isEqualTo("OK"); + + getInstrumentationExtension() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("SET") + .hasKind(SpanKind.CLIENT) + .hasAttributesSatisfyingExactly( + equalTo(NetworkAttributes.NETWORK_TYPE, "ipv4"), + equalTo(NetworkAttributes.NETWORK_PEER_ADDRESS, ip), + equalTo(NetworkAttributes.NETWORK_PEER_PORT, port), + equalTo(ServerAttributes.SERVER_ADDRESS, host), + equalTo(ServerAttributes.SERVER_PORT, port), + equalTo(DbIncubatingAttributes.DB_SYSTEM, "redis"), + equalTo(DbIncubatingAttributes.DB_STATEMENT, "SET TESTSETKEY ?")) + .hasEventsSatisfyingExactly( + event -> event.hasName("redis.encode.start"), + event -> event.hasName("redis.encode.end")))); + } + + @Test + void testGetCommandChainedWithThenAccept() throws Exception { + CompletableFuture future = new CompletableFuture<>(); + Consumer consumer = + res -> { + if (testCallback()) { + getInstrumentationExtension() + .runWithSpan("callback", () -> assertThat(res).isEqualTo("TESTVAL")); + } + future.complete(res); + }; + + getInstrumentationExtension() + .runWithSpan( + "parent", + () -> { + RedisFuture redisFuture = asyncCommands.get("TESTKEY"); + redisFuture.thenAccept(consumer); + }); + + assertThat(future.get(10, TimeUnit.SECONDS)).isEqualTo("TESTVAL"); + + getInstrumentationExtension() + .waitAndAssertTraces( + trace -> { + List> spanAsserts = + new ArrayList<>( + Arrays.asList( + span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(), + span -> + span.hasName("GET") + .hasKind(SpanKind.CLIENT) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo(NetworkAttributes.NETWORK_TYPE, "ipv4"), + equalTo(NetworkAttributes.NETWORK_PEER_ADDRESS, ip), + equalTo(NetworkAttributes.NETWORK_PEER_PORT, port), + equalTo(ServerAttributes.SERVER_ADDRESS, host), + equalTo(ServerAttributes.SERVER_PORT, port), + equalTo(DbIncubatingAttributes.DB_SYSTEM, "redis"), + equalTo(DbIncubatingAttributes.DB_STATEMENT, "GET TESTKEY")) + .hasEventsSatisfyingExactly( + event -> event.hasName("redis.encode.start"), + event -> event.hasName("redis.encode.end")))); + + if (testCallback()) { + spanAsserts.add( + span -> + span.hasName("callback") + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(0))); + } + trace.hasSpansSatisfyingExactly(spanAsserts); + }); + } + + // to make sure instrumentation's chained completion stages won't interfere with user's, while + // still recording spans + @Test + void testGetNonExistentKeyCommandWithHandleAsyncAndChainedWithThenApply() throws Exception { + CompletableFuture future = new CompletableFuture<>(); + String successStr = "KEY MISSING"; + + BiFunction firstStage = + (res, error) -> { + if (testCallback()) { + getInstrumentationExtension() + .runWithSpan( + "callback1", + () -> { + assertThat(res).isNull(); + assertThat(error).isNull(); + }); + } + return (res == null ? successStr : res); + }; + Function secondStage = + input -> { + if (testCallback()) { + getInstrumentationExtension() + .runWithSpan( + "callback2", + () -> { + assertThat(input).isEqualTo(successStr); + }); + } + future.complete(successStr); + return null; + }; + + getInstrumentationExtension() + .runWithSpan( + "parent", + () -> { + RedisFuture redisFuture = asyncCommands.get("NON_EXISTENT_KEY"); + redisFuture.handleAsync(firstStage).thenApply(secondStage); + }); + + assertThat(future.get(10, TimeUnit.SECONDS)).isEqualTo(successStr); + + getInstrumentationExtension() + .waitAndAssertTraces( + trace -> { + List> spanAsserts = + new ArrayList<>( + Arrays.asList( + span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(), + span -> + span.hasName("GET") + .hasKind(SpanKind.CLIENT) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo(NetworkAttributes.NETWORK_TYPE, "ipv4"), + equalTo(NetworkAttributes.NETWORK_PEER_ADDRESS, ip), + equalTo(NetworkAttributes.NETWORK_PEER_PORT, port), + equalTo(ServerAttributes.SERVER_ADDRESS, host), + equalTo(ServerAttributes.SERVER_PORT, port), + equalTo(DbIncubatingAttributes.DB_SYSTEM, "redis"), + equalTo( + DbIncubatingAttributes.DB_STATEMENT, + "GET NON_EXISTENT_KEY")) + .hasEventsSatisfyingExactly( + event -> event.hasName("redis.encode.start"), + event -> event.hasName("redis.encode.end")))); + + if (testCallback()) { + spanAsserts.addAll( + Arrays.asList( + span -> + span.hasName("callback1") + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(0)), + span -> + span.hasName("callback2") + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(0)))); + } + trace.hasSpansSatisfyingExactly(spanAsserts); + }); + } + + @Test + void testCommandWithNoArgumentsUsingBiconsumer() throws Exception { + CompletableFuture future = new CompletableFuture<>(); + BiConsumer biConsumer = + (keyRetrieved, error) -> { + if (testCallback()) { + getInstrumentationExtension() + .runWithSpan( + "callback", + () -> { + assertThat(keyRetrieved).isNotNull(); + }); + } + future.complete(keyRetrieved); + }; + + getInstrumentationExtension() + .runWithSpan( + "parent", + () -> { + RedisFuture redisFuture = asyncCommands.randomkey(); + redisFuture.whenCompleteAsync(biConsumer); + }); + + assertThat(future.get(10, TimeUnit.SECONDS)).isNotNull(); + + getInstrumentationExtension() + .waitAndAssertTraces( + trace -> { + List> spanAsserts = + new ArrayList<>( + Arrays.asList( + span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(), + span -> + span.hasName("RANDOMKEY") + .hasKind(SpanKind.CLIENT) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo(NetworkAttributes.NETWORK_TYPE, "ipv4"), + equalTo(NetworkAttributes.NETWORK_PEER_ADDRESS, ip), + equalTo(NetworkAttributes.NETWORK_PEER_PORT, port), + equalTo(ServerAttributes.SERVER_ADDRESS, host), + equalTo(ServerAttributes.SERVER_PORT, port), + equalTo(DbIncubatingAttributes.DB_SYSTEM, "redis"), + equalTo(DbIncubatingAttributes.DB_STATEMENT, "RANDOMKEY")) + .hasEventsSatisfyingExactly( + event -> event.hasName("redis.encode.start"), + event -> event.hasName("redis.encode.end")))); + + if (testCallback()) { + spanAsserts.add( + span -> + span.hasName("callback") + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(0))); + } + trace.hasSpansSatisfyingExactly(spanAsserts); + }); + } + + @Test + void testHashSetAndThenNestApplyToHashGetall() throws Exception { + CompletableFuture> future = new CompletableFuture<>(); + + RedisFuture hmsetFuture = asyncCommands.hmset("TESTHM", testHashMap); + hmsetFuture.thenApplyAsync( + setResult -> { + // Wait for 'hmset' trace to get written + getInstrumentationExtension().waitForTraces(1); + + if (!"OK".equals(setResult)) { + future.completeExceptionally(new AssertionError("Wrong hmset result " + setResult)); + return null; + } + + RedisFuture> hmGetAllFuture = asyncCommands.hgetall("TESTHM"); + hmGetAllFuture.whenComplete( + (result, exception) -> { + if (exception != null) { + future.completeExceptionally(exception); + } else { + future.complete(result); + } + }); + return null; + }); + + assertThat(future.get(10, TimeUnit.SECONDS)).isEqualTo(testHashMap); + + getInstrumentationExtension() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("HMSET") + .hasKind(SpanKind.CLIENT) + .hasAttributesSatisfyingExactly( + equalTo(NetworkAttributes.NETWORK_TYPE, "ipv4"), + equalTo(NetworkAttributes.NETWORK_PEER_ADDRESS, ip), + equalTo(NetworkAttributes.NETWORK_PEER_PORT, port), + equalTo(ServerAttributes.SERVER_ADDRESS, host), + equalTo(ServerAttributes.SERVER_PORT, port), + equalTo(DbIncubatingAttributes.DB_SYSTEM, "redis"), + equalTo( + DbIncubatingAttributes.DB_STATEMENT, + "HMSET TESTHM firstname ? lastname ? age ?")) + .hasEventsSatisfyingExactly( + event -> event.hasName("redis.encode.start"), + event -> event.hasName("redis.encode.end"))), + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("HGETALL") + .hasKind(SpanKind.CLIENT) + .hasAttributesSatisfyingExactly( + equalTo(NetworkAttributes.NETWORK_TYPE, "ipv4"), + equalTo(NetworkAttributes.NETWORK_PEER_ADDRESS, ip), + equalTo(NetworkAttributes.NETWORK_PEER_PORT, port), + equalTo(ServerAttributes.SERVER_ADDRESS, host), + equalTo(ServerAttributes.SERVER_PORT, port), + equalTo(DbIncubatingAttributes.DB_SYSTEM, "redis"), + equalTo(DbIncubatingAttributes.DB_STATEMENT, "HGETALL TESTHM")) + .hasEventsSatisfyingExactly( + event -> event.hasName("redis.encode.start"), + event -> event.hasName("redis.encode.end")))); + } +} diff --git a/instrumentation/lettuce/lettuce-5.1/testing/src/main/java/io/opentelemetry/instrumentation/lettuce/v5_1/AbstractLettuceClientTest.java b/instrumentation/lettuce/lettuce-5.1/testing/src/main/java/io/opentelemetry/instrumentation/lettuce/v5_1/AbstractLettuceClientTest.java new file mode 100644 index 000000000000..38b5df08085c --- /dev/null +++ b/instrumentation/lettuce/lettuce-5.1/testing/src/main/java/io/opentelemetry/instrumentation/lettuce/v5_1/AbstractLettuceClientTest.java @@ -0,0 +1,75 @@ +/* + * 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.api.StatefulRedisConnection; +import io.opentelemetry.instrumentation.testing.internal.AutoCleanupExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.output.Slf4jLogConsumer; +import org.testcontainers.containers.wait.strategy.Wait; + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +abstract class AbstractLettuceClientTest { + protected static final Logger logger = LoggerFactory.getLogger(AbstractLettuceClientTest.class); + + @RegisterExtension static final AutoCleanupExtension cleanup = AutoCleanupExtension.create(); + + protected static final int DB_INDEX = 0; + + protected static GenericContainer redisServer = + new GenericContainer<>("redis:6.2.3-alpine") + .withExposedPorts(6379) + .withLogConsumer(new Slf4jLogConsumer(logger)) + .waitingFor(Wait.forLogMessage(".*Ready to accept connections.*", 1)); + + protected static RedisClient redisClient; + protected static StatefulRedisConnection connection; + protected static String host; + protected static String ip; + protected static int port; + protected static String embeddedDbUri; + + protected abstract RedisClient createClient(String uri); + + protected abstract InstrumentationExtension getInstrumentationExtension(); + + protected ContainerConnection newContainerConnection() { + GenericContainer server = + new GenericContainer<>("redis:6.2.3-alpine") + .withExposedPorts(6379) + .withLogConsumer(new Slf4jLogConsumer(logger)) + .waitingFor(Wait.forLogMessage(".*Ready to accept connections.*", 1)); + server.start(); + cleanup.deferCleanup(server::stop); + + long serverPort = server.getMappedPort(6379); + + RedisClient client = createClient("redis://" + host + ":" + serverPort + "/" + DB_INDEX); + client.setOptions(LettuceTestUtil.CLIENT_OPTIONS); + cleanup.deferCleanup(client::shutdown); + + StatefulRedisConnection statefulConnection = client.connect(); + cleanup.deferCleanup(statefulConnection); + + return new ContainerConnection(statefulConnection, serverPort); + } + + protected static class ContainerConnection { + public final StatefulRedisConnection connection; + public final long port; + + private ContainerConnection(StatefulRedisConnection connection, long port) { + this.connection = connection; + this.port = port; + } + } +} diff --git a/instrumentation/lettuce/lettuce-5.1/testing/src/main/java/io/opentelemetry/instrumentation/lettuce/v5_1/AbstractLettuceReactiveClientTest.java b/instrumentation/lettuce/lettuce-5.1/testing/src/main/java/io/opentelemetry/instrumentation/lettuce/v5_1/AbstractLettuceReactiveClientTest.java new file mode 100644 index 000000000000..a2e157952953 --- /dev/null +++ b/instrumentation/lettuce/lettuce-5.1/testing/src/main/java/io/opentelemetry/instrumentation/lettuce/v5_1/AbstractLettuceReactiveClientTest.java @@ -0,0 +1,360 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.lettuce.v5_1; + +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; +import static org.assertj.core.api.Assertions.assertThat; + +import io.lettuce.core.api.reactive.RedisReactiveCommands; +import io.lettuce.core.api.sync.RedisCommands; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.semconv.NetworkAttributes; +import io.opentelemetry.semconv.ServerAttributes; +import io.opentelemetry.semconv.incubating.DbIncubatingAttributes; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +@SuppressWarnings({"InterruptedExceptionSwallowed"}) +public abstract class AbstractLettuceReactiveClientTest extends AbstractLettuceClientTest { + + protected static String expectedHostAttributeValue; + + protected static RedisReactiveCommands reactiveCommands; + + @BeforeAll + void setUp() throws UnknownHostException { + redisServer.start(); + + host = redisServer.getHost(); + ip = InetAddress.getByName(host).getHostAddress(); + port = redisServer.getMappedPort(6379); + embeddedDbUri = "redis://" + host + ":" + port + "/" + DB_INDEX; + expectedHostAttributeValue = Objects.equals(host, "127.0.0.1") ? null : host; + + redisClient = createClient(embeddedDbUri); + redisClient.setOptions(LettuceTestUtil.CLIENT_OPTIONS); + + connection = redisClient.connect(); + reactiveCommands = connection.reactive(); + RedisCommands syncCommands = connection.sync(); + + syncCommands.set("TESTKEY", "TESTVAL"); + + // 1 set trace + getInstrumentationExtension().waitForTraces(1); + getInstrumentationExtension().clearData(); + } + + @AfterAll + static void cleanUp() { + connection.close(); + redisClient.shutdown(); + redisServer.stop(); + } + + @Test + void testSetCommandWithSubscribeOnDefinedConsumer() throws Exception { + CompletableFuture future = new CompletableFuture<>(); + + Consumer consumer = + res -> + getInstrumentationExtension() + .runWithSpan( + "callback", + () -> { + assertThat(res).isEqualTo("OK"); + future.complete(res); + }); + + getInstrumentationExtension() + .runWithSpan( + "parent", () -> reactiveCommands.set("TESTSETKEY", "TESTSETVAL").subscribe(consumer)); + + assertThat(future.get(10, TimeUnit.SECONDS)).isEqualTo("OK"); + + getInstrumentationExtension() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(), + span -> + span.hasName("SET") + .hasKind(SpanKind.CLIENT) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo(NetworkAttributes.NETWORK_TYPE, "ipv4"), + equalTo(NetworkAttributes.NETWORK_PEER_ADDRESS, ip), + equalTo(NetworkAttributes.NETWORK_PEER_PORT, port), + equalTo(ServerAttributes.SERVER_ADDRESS, host), + equalTo(ServerAttributes.SERVER_PORT, port), + equalTo(DbIncubatingAttributes.DB_SYSTEM, "redis"), + equalTo(DbIncubatingAttributes.DB_STATEMENT, "SET TESTSETKEY ?")) + .hasEventsSatisfyingExactly( + event -> event.hasName("redis.encode.start"), + event -> event.hasName("redis.encode.end")), + span -> + span.hasName("callback") + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(0)))); + } + + @Test + void testGetCommandWithLambdaFunction() throws Exception { + CompletableFuture future = new CompletableFuture<>(); + + reactiveCommands + .get("TESTKEY") + .subscribe( + res -> { + assertThat(res).isEqualTo("TESTVAL"); + future.complete(res); + }); + + assertThat(future.get(10, TimeUnit.SECONDS)).isEqualTo("TESTVAL"); + + getInstrumentationExtension() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("GET") + .hasKind(SpanKind.CLIENT) + .hasAttributesSatisfyingExactly( + equalTo(NetworkAttributes.NETWORK_TYPE, "ipv4"), + equalTo(NetworkAttributes.NETWORK_PEER_ADDRESS, ip), + equalTo(NetworkAttributes.NETWORK_PEER_PORT, port), + equalTo(ServerAttributes.SERVER_ADDRESS, host), + equalTo(ServerAttributes.SERVER_PORT, port), + equalTo(DbIncubatingAttributes.DB_SYSTEM, "redis"), + equalTo(DbIncubatingAttributes.DB_STATEMENT, "GET TESTKEY")) + .hasEventsSatisfyingExactly( + event -> event.hasName("redis.encode.start"), + event -> event.hasName("redis.encode.end")))); + } + + // to make sure instrumentation's chained completion stages won't interfere with user's, while + // still recording spans + @Test + void testGetNonExistentKeyCommand() throws Exception { + CompletableFuture future = new CompletableFuture<>(); + String defaultVal = "NOT THIS VALUE"; + + getInstrumentationExtension() + .runWithSpan( + "parent", + () -> { + reactiveCommands + .get("NON_EXISTENT_KEY") + .defaultIfEmpty(defaultVal) + .subscribe( + res -> + getInstrumentationExtension() + .runWithSpan( + "callback", + () -> { + assertThat(res).isEqualTo(defaultVal); + future.complete(res); + })); + }); + + assertThat(future.get(10, TimeUnit.SECONDS)).isEqualTo(defaultVal); + + getInstrumentationExtension() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(), + span -> + span.hasName("GET") + .hasKind(SpanKind.CLIENT) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo(NetworkAttributes.NETWORK_TYPE, "ipv4"), + equalTo(NetworkAttributes.NETWORK_PEER_ADDRESS, ip), + equalTo(NetworkAttributes.NETWORK_PEER_PORT, port), + equalTo(ServerAttributes.SERVER_ADDRESS, host), + equalTo(ServerAttributes.SERVER_PORT, port), + equalTo(DbIncubatingAttributes.DB_SYSTEM, "redis"), + equalTo( + DbIncubatingAttributes.DB_STATEMENT, "GET NON_EXISTENT_KEY")) + .hasEventsSatisfyingExactly( + event -> event.hasName("redis.encode.start"), + event -> event.hasName("redis.encode.end")), + span -> + span.hasName("callback") + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(0)))); + } + + @Test + void testCommandWithNoArguments() throws Exception { + CompletableFuture future = new CompletableFuture<>(); + + reactiveCommands + .randomkey() + .subscribe( + res -> { + assertThat(res).isEqualTo("TESTKEY"); + future.complete(res); + }); + + assertThat(future.get(10, TimeUnit.SECONDS)).isEqualTo("TESTKEY"); + getInstrumentationExtension() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("RANDOMKEY") + .hasKind(SpanKind.CLIENT) + .hasAttributesSatisfyingExactly( + equalTo(NetworkAttributes.NETWORK_TYPE, "ipv4"), + equalTo(NetworkAttributes.NETWORK_PEER_ADDRESS, ip), + equalTo(NetworkAttributes.NETWORK_PEER_PORT, port), + equalTo(ServerAttributes.SERVER_ADDRESS, host), + equalTo(ServerAttributes.SERVER_PORT, port), + equalTo(DbIncubatingAttributes.DB_SYSTEM, "redis"), + equalTo(DbIncubatingAttributes.DB_STATEMENT, "RANDOMKEY")) + .hasEventsSatisfyingExactly( + event -> event.hasName("redis.encode.start"), + event -> event.hasName("redis.encode.end")))); + } + + @Test + void testCommandFluxPublisher() { + reactiveCommands.command().subscribe(); + + getInstrumentationExtension() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("COMMAND") + .hasKind(SpanKind.CLIENT) + .hasAttributesSatisfyingExactly( + equalTo(NetworkAttributes.NETWORK_TYPE, "ipv4"), + equalTo(NetworkAttributes.NETWORK_PEER_ADDRESS, ip), + equalTo(NetworkAttributes.NETWORK_PEER_PORT, port), + equalTo(ServerAttributes.SERVER_ADDRESS, host), + equalTo(ServerAttributes.SERVER_PORT, port), + equalTo(DbIncubatingAttributes.DB_SYSTEM, "redis"), + equalTo(DbIncubatingAttributes.DB_STATEMENT, "COMMAND")) + .hasEventsSatisfyingExactly( + event -> event.hasName("redis.encode.start"), + event -> event.hasName("redis.encode.end")))); + } + + @Test + void testNonReactiveCommandShouldNotProduceSpan() throws Exception { + Class commandsClass = RedisReactiveCommands.class; + java.lang.reflect.Method digestMethod; + // The digest() signature changed between 5 -> 6 + try { + digestMethod = commandsClass.getMethod("digest", String.class); + } catch (NoSuchMethodException unused) { + digestMethod = commandsClass.getMethod("digest", Object.class); + } + String res = (String) digestMethod.invoke(reactiveCommands, "test"); + + assertThat(res).isNotNull(); + assertThat(getInstrumentationExtension().spans().size()).isEqualTo(0); + } + + @Test + void testBlockingSubscriber() { + getInstrumentationExtension() + .runWithSpan( + "test-parent", + () -> reactiveCommands.set("a", "1").then(reactiveCommands.get("a")).block()); + + getInstrumentationExtension() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("test-parent").hasAttributes(Attributes.empty()), + span -> + span.hasName("SET") + .hasKind(SpanKind.CLIENT) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo(NetworkAttributes.NETWORK_TYPE, "ipv4"), + equalTo(NetworkAttributes.NETWORK_PEER_ADDRESS, ip), + equalTo(NetworkAttributes.NETWORK_PEER_PORT, port), + equalTo(ServerAttributes.SERVER_ADDRESS, host), + equalTo(ServerAttributes.SERVER_PORT, port), + equalTo(DbIncubatingAttributes.DB_SYSTEM, "redis"), + equalTo(DbIncubatingAttributes.DB_STATEMENT, "SET a ?")) + .hasEventsSatisfyingExactly( + event -> event.hasName("redis.encode.start"), + event -> event.hasName("redis.encode.end")), + span -> + span.hasName("GET") + .hasKind(SpanKind.CLIENT) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo(NetworkAttributes.NETWORK_TYPE, "ipv4"), + equalTo(NetworkAttributes.NETWORK_PEER_ADDRESS, ip), + equalTo(NetworkAttributes.NETWORK_PEER_PORT, port), + equalTo(ServerAttributes.SERVER_ADDRESS, host), + equalTo(ServerAttributes.SERVER_PORT, port), + equalTo(DbIncubatingAttributes.DB_SYSTEM, "redis"), + equalTo(DbIncubatingAttributes.DB_STATEMENT, "GET a")) + .hasEventsSatisfyingExactly( + event -> event.hasName("redis.encode.start"), + event -> event.hasName("redis.encode.end")))); + } + + @Test + void testAsyncSubscriber() { + getInstrumentationExtension() + .runWithSpan( + "test-parent", + () -> reactiveCommands.set("a", "1").then(reactiveCommands.get("a")).subscribe()); + + getInstrumentationExtension() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("test-parent").hasAttributes(Attributes.empty()), + span -> + span.hasName("SET") + .hasKind(SpanKind.CLIENT) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo(NetworkAttributes.NETWORK_TYPE, "ipv4"), + equalTo(NetworkAttributes.NETWORK_PEER_ADDRESS, ip), + equalTo(NetworkAttributes.NETWORK_PEER_PORT, port), + equalTo(ServerAttributes.SERVER_ADDRESS, host), + equalTo(ServerAttributes.SERVER_PORT, port), + equalTo(DbIncubatingAttributes.DB_SYSTEM, "redis"), + equalTo(DbIncubatingAttributes.DB_STATEMENT, "SET a ?")) + .hasEventsSatisfyingExactly( + event -> event.hasName("redis.encode.start"), + event -> event.hasName("redis.encode.end")), + span -> + span.hasName("GET") + .hasKind(SpanKind.CLIENT) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo(NetworkAttributes.NETWORK_TYPE, "ipv4"), + equalTo(NetworkAttributes.NETWORK_PEER_ADDRESS, ip), + equalTo(NetworkAttributes.NETWORK_PEER_PORT, port), + equalTo(ServerAttributes.SERVER_ADDRESS, host), + equalTo(ServerAttributes.SERVER_PORT, port), + equalTo(DbIncubatingAttributes.DB_SYSTEM, "redis"), + equalTo(DbIncubatingAttributes.DB_STATEMENT, "GET a")) + .hasEventsSatisfyingExactly( + event -> event.hasName("redis.encode.start"), + event -> event.hasName("redis.encode.end")))); + } +} diff --git a/instrumentation/lettuce/lettuce-5.1/testing/src/main/java/io/opentelemetry/instrumentation/lettuce/v5_1/AbstractLettuceSyncClientAuthTest.java b/instrumentation/lettuce/lettuce-5.1/testing/src/main/java/io/opentelemetry/instrumentation/lettuce/v5_1/AbstractLettuceSyncClientAuthTest.java new file mode 100644 index 000000000000..19e354bdf381 --- /dev/null +++ b/instrumentation/lettuce/lettuce-5.1/testing/src/main/java/io/opentelemetry/instrumentation/lettuce/v5_1/AbstractLettuceSyncClientAuthTest.java @@ -0,0 +1,135 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.lettuce.v5_1; + +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.satisfies; +import static org.assertj.core.api.Assertions.assertThat; + +import io.lettuce.core.api.sync.RedisCommands; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.semconv.NetworkAttributes; +import io.opentelemetry.semconv.ServerAttributes; +import io.opentelemetry.semconv.incubating.DbIncubatingAttributes; +import java.net.InetAddress; +import java.net.UnknownHostException; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +public abstract class AbstractLettuceSyncClientAuthTest extends AbstractLettuceClientTest { + + @BeforeAll + void setUp() throws UnknownHostException { + redisServer = redisServer.withCommand("redis-server", "--requirepass password"); + redisServer.start(); + + host = redisServer.getHost(); + ip = InetAddress.getByName(host).getHostAddress(); + port = redisServer.getMappedPort(6379); + embeddedDbUri = "redis://" + host + ":" + port + "/" + DB_INDEX; + + redisClient = createClient(embeddedDbUri); + redisClient.setOptions(LettuceTestUtil.CLIENT_OPTIONS); + } + + @AfterAll + static void cleanUp() { + redisClient.shutdown(); + redisServer.stop(); + + // Set back so other tests don't fail due to NOAUTH error + redisServer = redisServer.withCommand("redis-server", "--requirepass \"\""); + } + + @Test + void testAuthCommand() throws Exception { + Class commandsClass = RedisCommands.class; + java.lang.reflect.Method authMethod; + // the auth() argument type changed between 5.x -> 6.x + try { + authMethod = commandsClass.getMethod("auth", String.class); + } catch (NoSuchMethodException unused) { + authMethod = commandsClass.getMethod("auth", CharSequence.class); + } + + String result = (String) authMethod.invoke(redisClient.connect().sync(), "password"); + + assertThat(result).isEqualTo("OK"); + + if (Boolean.getBoolean("testLatestDeps")) { + getInstrumentationExtension() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("CLIENT") + .hasKind(SpanKind.CLIENT) + .hasAttributesSatisfyingExactly( + equalTo(NetworkAttributes.NETWORK_TYPE, "ipv4"), + equalTo(NetworkAttributes.NETWORK_PEER_ADDRESS, ip), + equalTo(NetworkAttributes.NETWORK_PEER_PORT, port), + equalTo(ServerAttributes.SERVER_ADDRESS, host), + equalTo(ServerAttributes.SERVER_PORT, port), + equalTo(DbIncubatingAttributes.DB_SYSTEM, "redis"), + equalTo( + DbIncubatingAttributes.DB_STATEMENT, + "CLIENT SETINFO lib-name Lettuce"))), + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("CLIENT") + .hasKind(SpanKind.CLIENT) + .hasAttributesSatisfyingExactly( + equalTo(NetworkAttributes.NETWORK_TYPE, "ipv4"), + equalTo(NetworkAttributes.NETWORK_PEER_ADDRESS, ip), + equalTo(NetworkAttributes.NETWORK_PEER_PORT, port), + equalTo(ServerAttributes.SERVER_ADDRESS, host), + equalTo(ServerAttributes.SERVER_PORT, port), + equalTo(DbIncubatingAttributes.DB_SYSTEM, "redis"), + satisfies( + DbIncubatingAttributes.DB_STATEMENT, + stringAssert -> + stringAssert.startsWith("CLIENT SETINFO lib-ver")))), + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("AUTH") + .hasKind(SpanKind.CLIENT) + .hasAttributesSatisfyingExactly( + equalTo(NetworkAttributes.NETWORK_TYPE, "ipv4"), + equalTo(NetworkAttributes.NETWORK_PEER_ADDRESS, ip), + equalTo(NetworkAttributes.NETWORK_PEER_PORT, port), + equalTo(ServerAttributes.SERVER_ADDRESS, host), + equalTo(ServerAttributes.SERVER_PORT, port), + equalTo(DbIncubatingAttributes.DB_SYSTEM, "redis"), + equalTo(DbIncubatingAttributes.DB_STATEMENT, "AUTH ?")) + .hasEventsSatisfyingExactly( + event -> event.hasName("redis.encode.start"), + event -> event.hasName("redis.encode.end")))); + + } else { + getInstrumentationExtension() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("AUTH") + .hasKind(SpanKind.CLIENT) + .hasAttributesSatisfyingExactly( + equalTo(NetworkAttributes.NETWORK_TYPE, "ipv4"), + equalTo(NetworkAttributes.NETWORK_PEER_ADDRESS, ip), + equalTo(NetworkAttributes.NETWORK_PEER_PORT, port), + equalTo(ServerAttributes.SERVER_ADDRESS, host), + equalTo(ServerAttributes.SERVER_PORT, port), + equalTo(DbIncubatingAttributes.DB_SYSTEM, "redis"), + equalTo(DbIncubatingAttributes.DB_STATEMENT, "AUTH ?")) + .hasEventsSatisfyingExactly( + event -> event.hasName("redis.encode.start"), + event -> event.hasName("redis.encode.end")))); + } + } +} diff --git a/instrumentation/lettuce/lettuce-5.1/testing/src/main/java/io/opentelemetry/instrumentation/lettuce/v5_1/AbstractLettuceSyncClientTest.java b/instrumentation/lettuce/lettuce-5.1/testing/src/main/java/io/opentelemetry/instrumentation/lettuce/v5_1/AbstractLettuceSyncClientTest.java new file mode 100644 index 000000000000..156a8b49df7f --- /dev/null +++ b/instrumentation/lettuce/lettuce-5.1/testing/src/main/java/io/opentelemetry/instrumentation/lettuce/v5_1/AbstractLettuceSyncClientTest.java @@ -0,0 +1,497 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.lettuce.v5_1; + +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.satisfies; +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.catchThrowable; + +import com.google.common.collect.ImmutableMap; +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.StatefulRedisConnection; +import io.lettuce.core.api.sync.RedisCommands; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.test.utils.PortUtils; +import io.opentelemetry.semconv.NetworkAttributes; +import io.opentelemetry.semconv.ServerAttributes; +import io.opentelemetry.semconv.incubating.DbIncubatingAttributes; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.Base64; +import java.util.Map; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +public abstract class AbstractLettuceSyncClientTest extends AbstractLettuceClientTest { + + private static String dbUriNonExistent; + + private static final ImmutableMap testHashMap = + ImmutableMap.of( + "firstname", "John", + "lastname", "Doe", + "age", "53"); + + private static RedisCommands syncCommands; + + @BeforeAll + void setUp() throws UnknownHostException { + redisServer.start(); + + host = redisServer.getHost(); + ip = InetAddress.getByName(host).getHostAddress(); + port = redisServer.getMappedPort(6379); + embeddedDbUri = "redis://" + host + ":" + port + "/" + DB_INDEX; + + int incorrectPort = PortUtils.findOpenPort(); + dbUriNonExistent = "redis://" + host + ":" + incorrectPort + "/" + DB_INDEX; + + redisClient = createClient(embeddedDbUri); + redisClient.setOptions(LettuceTestUtil.CLIENT_OPTIONS); + + connection = redisClient.connect(); + syncCommands = connection.sync(); + + syncCommands.set("TESTKEY", "TESTVAL"); + syncCommands.hmset("TESTHM", testHashMap); + + // 2 sets + getInstrumentationExtension().waitForTraces(2); + getInstrumentationExtension().clearData(); + } + + @AfterAll + static void cleanUp() { + connection.close(); + redisClient.shutdown(); + redisServer.stop(); + } + + @Test + void testConnect() { + StatefulRedisConnection testConnection = redisClient.connect(); + cleanup.deferCleanup(testConnection); + + if (Boolean.getBoolean("testLatestDeps")) { + // ignore CLIENT SETINFO traces + getInstrumentationExtension().waitForTraces(2); + } else { + // Lettuce tracing does not trace connect + assertThat(getInstrumentationExtension().spans()).isEmpty(); + } + } + + @Test + void testConnectException() { + RedisClient testConnectionClient = RedisClient.create(dbUriNonExistent); + testConnectionClient.setOptions(LettuceTestUtil.CLIENT_OPTIONS); + cleanup.deferCleanup(testConnectionClient::shutdown); + + Throwable thrown = catchThrowable(testConnectionClient::connect); + + assertThat(thrown).isInstanceOf(RedisConnectionException.class); + + // Lettuce tracing does not trace connect + assertThat(getInstrumentationExtension().spans()).isEmpty(); + } + + @Test + void testSetCommand() { + String res = syncCommands.set("TESTSETKEY", "TESTSETVAL"); + assertThat(res).isEqualTo("OK"); + + getInstrumentationExtension() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("SET") + .hasKind(SpanKind.CLIENT) + .hasAttributesSatisfyingExactly( + equalTo(NetworkAttributes.NETWORK_TYPE, "ipv4"), + equalTo(NetworkAttributes.NETWORK_PEER_ADDRESS, ip), + equalTo(NetworkAttributes.NETWORK_PEER_PORT, port), + equalTo(ServerAttributes.SERVER_ADDRESS, host), + equalTo(ServerAttributes.SERVER_PORT, port), + equalTo(DbIncubatingAttributes.DB_SYSTEM, "redis"), + equalTo(DbIncubatingAttributes.DB_STATEMENT, "SET TESTSETKEY ?")) + .hasEventsSatisfyingExactly( + event -> event.hasName("redis.encode.start"), + event -> event.hasName("redis.encode.end")))); + } + + @Test + void testGetCommand() { + String res = syncCommands.get("TESTKEY"); + assertThat(res).isEqualTo("TESTVAL"); + + getInstrumentationExtension() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("GET") + .hasKind(SpanKind.CLIENT) + .hasAttributesSatisfyingExactly( + equalTo(NetworkAttributes.NETWORK_TYPE, "ipv4"), + equalTo(NetworkAttributes.NETWORK_PEER_ADDRESS, ip), + equalTo(NetworkAttributes.NETWORK_PEER_PORT, port), + equalTo(ServerAttributes.SERVER_ADDRESS, host), + equalTo(ServerAttributes.SERVER_PORT, port), + equalTo(DbIncubatingAttributes.DB_SYSTEM, "redis"), + equalTo(DbIncubatingAttributes.DB_STATEMENT, "GET TESTKEY")) + .hasEventsSatisfyingExactly( + event -> event.hasName("redis.encode.start"), + event -> event.hasName("redis.encode.end")))); + } + + @Test + void testGetNonExistentKeyCommand() { + String res = syncCommands.get("NON_EXISTENT_KEY"); + assertThat(res).isNull(); + + getInstrumentationExtension() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("GET") + .hasKind(SpanKind.CLIENT) + .hasAttributesSatisfyingExactly( + equalTo(NetworkAttributes.NETWORK_TYPE, "ipv4"), + equalTo(NetworkAttributes.NETWORK_PEER_ADDRESS, ip), + equalTo(NetworkAttributes.NETWORK_PEER_PORT, port), + equalTo(ServerAttributes.SERVER_ADDRESS, host), + equalTo(ServerAttributes.SERVER_PORT, port), + equalTo(DbIncubatingAttributes.DB_SYSTEM, "redis"), + equalTo( + DbIncubatingAttributes.DB_STATEMENT, "GET NON_EXISTENT_KEY")) + .hasEventsSatisfyingExactly( + event -> event.hasName("redis.encode.start"), + event -> event.hasName("redis.encode.end")))); + } + + @Test + void testCommandWithNoArguments() { + String res = syncCommands.randomkey(); + assertThat(res).isNotNull(); + + getInstrumentationExtension() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("RANDOMKEY") + .hasKind(SpanKind.CLIENT) + .hasAttributesSatisfyingExactly( + equalTo(NetworkAttributes.NETWORK_TYPE, "ipv4"), + equalTo(NetworkAttributes.NETWORK_PEER_ADDRESS, ip), + equalTo(NetworkAttributes.NETWORK_PEER_PORT, port), + equalTo(ServerAttributes.SERVER_ADDRESS, host), + equalTo(ServerAttributes.SERVER_PORT, port), + equalTo(DbIncubatingAttributes.DB_SYSTEM, "redis"), + equalTo(DbIncubatingAttributes.DB_STATEMENT, "RANDOMKEY")) + .hasEventsSatisfyingExactly( + event -> event.hasName("redis.encode.start"), + event -> event.hasName("redis.encode.end")))); + } + + @Test + void testListCommand() { + // Needs its own container or flaky from inconsistent command count + ContainerConnection containerConnection = newContainerConnection(); + RedisCommands commands = containerConnection.connection.sync(); + + if (Boolean.getBoolean("testLatestDeps")) { + // ignore CLIENT SETINFO traces + getInstrumentationExtension().waitForTraces(2); + getInstrumentationExtension().clearData(); + } + + long res = commands.lpush("TESTLIST", "TESTLIST ELEMENT"); + assertThat(res).isEqualTo(1); + + getInstrumentationExtension() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("LPUSH") + .hasKind(SpanKind.CLIENT) + .hasAttributesSatisfyingExactly( + equalTo(NetworkAttributes.NETWORK_TYPE, "ipv4"), + equalTo(NetworkAttributes.NETWORK_PEER_ADDRESS, ip), + equalTo( + NetworkAttributes.NETWORK_PEER_PORT, containerConnection.port), + equalTo(ServerAttributes.SERVER_ADDRESS, host), + equalTo(ServerAttributes.SERVER_PORT, containerConnection.port), + equalTo(DbIncubatingAttributes.DB_SYSTEM, "redis"), + equalTo(DbIncubatingAttributes.DB_STATEMENT, "LPUSH TESTLIST ?")) + .hasEventsSatisfyingExactly( + event -> event.hasName("redis.encode.start"), + event -> event.hasName("redis.encode.end")))); + } + + @Test + void testHashSetCommand() { + String res = syncCommands.hmset("user", testHashMap); + assertThat(res).isEqualTo("OK"); + + getInstrumentationExtension() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("HMSET") + .hasKind(SpanKind.CLIENT) + .hasAttributesSatisfyingExactly( + equalTo(NetworkAttributes.NETWORK_TYPE, "ipv4"), + equalTo(NetworkAttributes.NETWORK_PEER_ADDRESS, ip), + equalTo(NetworkAttributes.NETWORK_PEER_PORT, port), + equalTo(ServerAttributes.SERVER_ADDRESS, host), + equalTo(ServerAttributes.SERVER_PORT, port), + equalTo(DbIncubatingAttributes.DB_SYSTEM, "redis"), + equalTo( + DbIncubatingAttributes.DB_STATEMENT, + "HMSET user firstname ? lastname ? age ?")) + .hasEventsSatisfyingExactly( + event -> event.hasName("redis.encode.start"), + event -> event.hasName("redis.encode.end")))); + } + + @Test + void testHashGetallCommand() { + Map res = syncCommands.hgetall("TESTHM"); + assertThat(res).isEqualTo(testHashMap); + + getInstrumentationExtension() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("HGETALL") + .hasKind(SpanKind.CLIENT) + .hasAttributesSatisfyingExactly( + equalTo(NetworkAttributes.NETWORK_TYPE, "ipv4"), + equalTo(NetworkAttributes.NETWORK_PEER_ADDRESS, ip), + equalTo(NetworkAttributes.NETWORK_PEER_PORT, port), + equalTo(ServerAttributes.SERVER_ADDRESS, host), + equalTo(ServerAttributes.SERVER_PORT, port), + equalTo(DbIncubatingAttributes.DB_SYSTEM, "redis"), + equalTo(DbIncubatingAttributes.DB_STATEMENT, "HGETALL TESTHM")) + .hasEventsSatisfyingExactly( + event -> event.hasName("redis.encode.start"), + event -> event.hasName("redis.encode.end")))); + } + + @Test + void testEvalCommand() { + String script = + "redis.call('lpush', KEYS[1], ARGV[1], ARGV[2]); return redis.call('llen', KEYS[1])"; + + Long result = + syncCommands.eval( + script, ScriptOutputType.INTEGER, new String[] {"TESTLIST"}, "abc", "def"); + assertThat(result).isEqualTo(2); + + String b64Script = Base64.getEncoder().encodeToString(script.getBytes(UTF_8)); + + getInstrumentationExtension() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("EVAL") + .hasKind(SpanKind.CLIENT) + .hasAttributesSatisfyingExactly( + equalTo(NetworkAttributes.NETWORK_TYPE, "ipv4"), + equalTo(NetworkAttributes.NETWORK_PEER_ADDRESS, ip), + equalTo(NetworkAttributes.NETWORK_PEER_PORT, port), + equalTo(ServerAttributes.SERVER_ADDRESS, host), + equalTo(ServerAttributes.SERVER_PORT, port), + equalTo(DbIncubatingAttributes.DB_SYSTEM, "redis"), + equalTo( + DbIncubatingAttributes.DB_STATEMENT, + "EVAL " + b64Script + " 1 TESTLIST ? ?")) + .hasEventsSatisfyingExactly( + event -> event.hasName("redis.encode.start"), + event -> event.hasName("redis.encode.end")))); + } + + @Test + void testMsetCommand() { + String result = syncCommands.mset(ImmutableMap.of("key1", "value1", "key2", "value2")); + + assertThat(result).isEqualTo("OK"); + + getInstrumentationExtension() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("MSET") + .hasKind(SpanKind.CLIENT) + .hasAttributesSatisfyingExactly( + equalTo(NetworkAttributes.NETWORK_TYPE, "ipv4"), + equalTo(NetworkAttributes.NETWORK_PEER_ADDRESS, ip), + equalTo(NetworkAttributes.NETWORK_PEER_PORT, port), + equalTo(ServerAttributes.SERVER_ADDRESS, host), + equalTo(ServerAttributes.SERVER_PORT, port), + equalTo(DbIncubatingAttributes.DB_SYSTEM, "redis"), + equalTo(DbIncubatingAttributes.DB_STATEMENT, "MSET key1 ? key2 ?")) + .hasEventsSatisfyingExactly( + event -> event.hasName("redis.encode.start"), + event -> event.hasName("redis.encode.end")))); + } + + @Test + void testDebugSegfaultCommandWithNoArgumentProducesNoSpan() { + // Test causes redis to crash therefore it needs its own container + ContainerConnection containerConnection = newContainerConnection(); + RedisCommands commands = containerConnection.connection.sync(); + + commands.debugSegfault(); + + if (Boolean.getBoolean("testLatestDeps")) { + getInstrumentationExtension() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("CLIENT") + .hasKind(SpanKind.CLIENT) + .hasAttributesSatisfyingExactly( + equalTo(NetworkAttributes.NETWORK_TYPE, "ipv4"), + equalTo(NetworkAttributes.NETWORK_PEER_ADDRESS, ip), + equalTo( + NetworkAttributes.NETWORK_PEER_PORT, + containerConnection.port), + equalTo(ServerAttributes.SERVER_ADDRESS, host), + equalTo(ServerAttributes.SERVER_PORT, containerConnection.port), + equalTo(DbIncubatingAttributes.DB_SYSTEM, "redis"), + equalTo( + DbIncubatingAttributes.DB_STATEMENT, + "CLIENT SETINFO lib-name Lettuce"))), + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("CLIENT") + .hasKind(SpanKind.CLIENT) + .hasAttributesSatisfyingExactly( + equalTo(NetworkAttributes.NETWORK_TYPE, "ipv4"), + equalTo(NetworkAttributes.NETWORK_PEER_ADDRESS, ip), + equalTo( + NetworkAttributes.NETWORK_PEER_PORT, + containerConnection.port), + equalTo(ServerAttributes.SERVER_ADDRESS, host), + equalTo(ServerAttributes.SERVER_PORT, containerConnection.port), + equalTo(DbIncubatingAttributes.DB_SYSTEM, "redis"), + satisfies( + DbIncubatingAttributes.DB_STATEMENT, + stringAssert -> + stringAssert.startsWith("CLIENT SETINFO lib-ver")))), + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("DEBUG") + .hasKind(SpanKind.CLIENT) + .hasAttributesSatisfyingExactly( + equalTo(NetworkAttributes.NETWORK_TYPE, "ipv4"), + equalTo(NetworkAttributes.NETWORK_PEER_ADDRESS, ip), + equalTo( + NetworkAttributes.NETWORK_PEER_PORT, + containerConnection.port), + equalTo(ServerAttributes.SERVER_ADDRESS, host), + equalTo(ServerAttributes.SERVER_PORT, containerConnection.port), + equalTo(DbIncubatingAttributes.DB_SYSTEM, "redis"), + equalTo(DbIncubatingAttributes.DB_STATEMENT, "DEBUG SEGFAULT")))); + } else { + getInstrumentationExtension() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("DEBUG") + .hasKind(SpanKind.CLIENT) + .hasAttributesSatisfyingExactly( + equalTo(NetworkAttributes.NETWORK_TYPE, "ipv4"), + equalTo(NetworkAttributes.NETWORK_PEER_ADDRESS, ip), + equalTo( + NetworkAttributes.NETWORK_PEER_PORT, + containerConnection.port), + equalTo(ServerAttributes.SERVER_ADDRESS, host), + equalTo(ServerAttributes.SERVER_PORT, containerConnection.port), + equalTo(DbIncubatingAttributes.DB_SYSTEM, "redis"), + equalTo(DbIncubatingAttributes.DB_STATEMENT, "DEBUG SEGFAULT")) + // these are no longer recorded since Lettuce 6.1.6 + .hasEventsSatisfyingExactly( + event -> event.hasName("redis.encode.start"), + event -> event.hasName("redis.encode.end")))); + } + } + + @Test + void testShutdownCommandProducesNoSpan() { + // Test causes redis to crash therefore it needs its own container + ContainerConnection containerConnection = newContainerConnection(); + RedisCommands commands = containerConnection.connection.sync(); + + if (Boolean.getBoolean("testLatestDeps")) { + // ignore CLIENT SETINFO traces + getInstrumentationExtension().waitForTraces(2); + getInstrumentationExtension().clearData(); + } + + commands.shutdown(false); + + getInstrumentationExtension() + .waitAndAssertTraces( + trace -> { + if (Boolean.getBoolean("testLatestDeps")) { + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("SHUTDOWN") + .hasKind(SpanKind.CLIENT) + // Seems to only be treated as an error with Lettuce 6+ + .hasException(new RedisException("Connection disconnected")) + .hasAttributesSatisfyingExactly( + equalTo(NetworkAttributes.NETWORK_TYPE, "ipv4"), + equalTo(NetworkAttributes.NETWORK_PEER_ADDRESS, ip), + equalTo( + NetworkAttributes.NETWORK_PEER_PORT, containerConnection.port), + equalTo(ServerAttributes.SERVER_ADDRESS, host), + equalTo(ServerAttributes.SERVER_PORT, containerConnection.port), + equalTo(DbIncubatingAttributes.DB_SYSTEM, "redis"), + equalTo(DbIncubatingAttributes.DB_STATEMENT, "SHUTDOWN NOSAVE"))); + } else { + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("SHUTDOWN") + .hasKind(SpanKind.CLIENT) + .hasAttributesSatisfyingExactly( + equalTo(AttributeKey.stringKey("error"), "Connection disconnected"), + equalTo(NetworkAttributes.NETWORK_TYPE, "ipv4"), + equalTo(NetworkAttributes.NETWORK_PEER_ADDRESS, ip), + equalTo( + NetworkAttributes.NETWORK_PEER_PORT, containerConnection.port), + equalTo(ServerAttributes.SERVER_ADDRESS, host), + equalTo(ServerAttributes.SERVER_PORT, containerConnection.port), + equalTo(DbIncubatingAttributes.DB_SYSTEM, "redis"), + equalTo(DbIncubatingAttributes.DB_STATEMENT, "SHUTDOWN NOSAVE")) + .hasEventsSatisfyingExactly( + event -> event.hasName("redis.encode.start"), + event -> event.hasName("redis.encode.end"))); + } + }); + } +} diff --git a/instrumentation/lettuce/lettuce-5.1/testing/src/main/java/io/opentelemetry/instrumentation/lettuce/v5_1/LettuceTestUtil.java b/instrumentation/lettuce/lettuce-5.1/testing/src/main/java/io/opentelemetry/instrumentation/lettuce/v5_1/LettuceTestUtil.java new file mode 100644 index 000000000000..01023ebac498 --- /dev/null +++ b/instrumentation/lettuce/lettuce-5.1/testing/src/main/java/io/opentelemetry/instrumentation/lettuce/v5_1/LettuceTestUtil.java @@ -0,0 +1,27 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.lettuce.v5_1; + +import io.lettuce.core.ClientOptions; +import io.lettuce.core.protocol.ProtocolVersion; + +final class LettuceTestUtil { + static final ClientOptions CLIENT_OPTIONS; + + static { + ClientOptions.Builder 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(ProtocolVersion.RESP2); + } + CLIENT_OPTIONS = options.build(); + } + + private LettuceTestUtil() {} +} diff --git a/instrumentation/lettuce/lettuce-common/library/src/test/groovy/io/opentelemetry/instrumentation/lettuce/common/LettuceArgSplitterTest.groovy b/instrumentation/lettuce/lettuce-common/library/src/test/groovy/io/opentelemetry/instrumentation/lettuce/common/LettuceArgSplitterTest.groovy deleted file mode 100644 index a708f83ee4b2..000000000000 --- a/instrumentation/lettuce/lettuce-common/library/src/test/groovy/io/opentelemetry/instrumentation/lettuce/common/LettuceArgSplitterTest.groovy +++ /dev/null @@ -1,28 +0,0 @@ -/* - * 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/instrumentation/lettuce/lettuce-common/library/src/test/java/io/opentelemetry/instrumentation/lettuce/common/LettuceArgSplitterTest.java b/instrumentation/lettuce/lettuce-common/library/src/test/java/io/opentelemetry/instrumentation/lettuce/common/LettuceArgSplitterTest.java new file mode 100644 index 000000000000..ccb3b00f431e --- /dev/null +++ b/instrumentation/lettuce/lettuce-common/library/src/test/java/io/opentelemetry/instrumentation/lettuce/common/LettuceArgSplitterTest.java @@ -0,0 +1,54 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.lettuce.common; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Named.named; + +import com.google.common.collect.ImmutableList; +import java.util.stream.Stream; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +class LettuceArgSplitterTest { + + @ParameterizedTest + @MethodSource("providesArguments") + void testShouldProperlySplitArguments(Parameter parameter) { + assertThat(LettuceArgSplitter.splitArgs(parameter.args)).isEqualTo(parameter.result); + } + + private static Stream providesArguments() { + return Stream.of( + Arguments.of(named("a null value", new Parameter(null, ImmutableList.of()))), + Arguments.of(named("an empty value", new Parameter("", ImmutableList.of()))), + Arguments.of(named("a single key", new Parameter("key", ImmutableList.of("key")))), + Arguments.of( + named("a single value", new Parameter("value", ImmutableList.of("value")))), + Arguments.of( + named("a plain string", new Parameter("teststring", ImmutableList.of("teststring")))), + Arguments.of(named("an integer", new Parameter("42", ImmutableList.of("42")))), + Arguments.of( + named("a base64 value", new Parameter("TeST123==", ImmutableList.of("TeST123==")))), + Arguments.of( + named( + "a complex list of args", + new Parameter( + "key aSDFgh4321= 5 test value", + ImmutableList.of("key", "aSDFgh4321=", "5", "test", "val"))))); + } + + private static class Parameter { + public final String args; + public final ImmutableList result; + + public Parameter(String query, ImmutableList result) { + this.args = query; + this.result = result; + } + } +} diff --git a/instrumentation/liberty/compile-stub/src/main/java/com/ibm/wsspi/http/channel/HttpRequestMessage.java b/instrumentation/liberty/compile-stub/src/main/java/com/ibm/wsspi/http/channel/HttpRequestMessage.java index 5705c162a0a8..25163026b1d5 100644 --- a/instrumentation/liberty/compile-stub/src/main/java/com/ibm/wsspi/http/channel/HttpRequestMessage.java +++ b/instrumentation/liberty/compile-stub/src/main/java/com/ibm/wsspi/http/channel/HttpRequestMessage.java @@ -9,6 +9,7 @@ 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 +@SuppressWarnings("MemberName") public interface HttpRequestMessage { String getMethod(); @@ -26,8 +27,4 @@ public interface HttpRequestMessage { String getVersion(); List getAllHeaderNames(); - - String getURLHost(); - - int getURLPort(); } diff --git a/instrumentation/liberty/liberty-20.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/liberty/LibertySingletons.java b/instrumentation/liberty/liberty-20.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/liberty/LibertySingletons.java index 7468415faf50..7cfcb21c5538 100644 --- a/instrumentation/liberty/liberty-20.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/liberty/LibertySingletons.java +++ b/instrumentation/liberty/liberty-20.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/liberty/LibertySingletons.java @@ -24,6 +24,7 @@ public final class LibertySingletons { .addContextCustomizer( (context, request, attributes) -> new AppServerBridge.Builder().recordException().init(context)) + .propagateOperationListenersToOnEnd() .build(INSTRUMENTATION_NAME, Servlet3Accessor.INSTANCE); private static final LibertyHelper HELPER = diff --git a/instrumentation/liberty/liberty-dispatcher-20.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/liberty/dispatcher/LibertyDispatcherHttpAttributesGetter.java b/instrumentation/liberty/liberty-dispatcher-20.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/liberty/dispatcher/LibertyDispatcherHttpAttributesGetter.java index 62fd1959200b..7f8908ea335e 100644 --- a/instrumentation/liberty/liberty-dispatcher-20.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/liberty/dispatcher/LibertyDispatcherHttpAttributesGetter.java +++ b/instrumentation/liberty/liberty-dispatcher-20.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/liberty/dispatcher/LibertyDispatcherHttpAttributesGetter.java @@ -5,7 +5,7 @@ package io.opentelemetry.javaagent.instrumentation.liberty.dispatcher; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpServerAttributesGetter; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerAttributesGetter; import java.util.List; import javax.annotation.Nullable; @@ -53,4 +53,49 @@ public String getUrlPath(LibertyRequest request) { public String getUrlQuery(LibertyRequest request) { return request.getQueryString(); } + + @Nullable + @Override + public String getNetworkProtocolName( + LibertyRequest request, @Nullable LibertyResponse libertyResponse) { + String protocol = request.getProtocol(); + if (protocol != null && protocol.startsWith("HTTP/")) { + return "http"; + } + return null; + } + + @Nullable + @Override + public String getNetworkProtocolVersion( + LibertyRequest request, @Nullable LibertyResponse libertyResponse) { + String protocol = request.getProtocol(); + if (protocol != null && protocol.startsWith("HTTP/")) { + return protocol.substring("HTTP/".length()); + } + return null; + } + + @Override + @Nullable + public String getNetworkPeerAddress(LibertyRequest request, @Nullable LibertyResponse response) { + return request.getClientSocketAddress(); + } + + @Override + public Integer getNetworkPeerPort(LibertyRequest request, @Nullable LibertyResponse response) { + return request.getClientSocketPort(); + } + + @Nullable + @Override + public String getNetworkLocalAddress(LibertyRequest request, @Nullable LibertyResponse response) { + return request.getServerSocketAddress(); + } + + @Nullable + @Override + public Integer getNetworkLocalPort(LibertyRequest request, @Nullable LibertyResponse response) { + return request.getServerSocketPort(); + } } diff --git a/instrumentation/liberty/liberty-dispatcher-20.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/liberty/dispatcher/LibertyDispatcherNetAttributesGetter.java b/instrumentation/liberty/liberty-dispatcher-20.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/liberty/dispatcher/LibertyDispatcherNetAttributesGetter.java deleted file mode 100644 index 18f4b4fbb04c..000000000000 --- a/instrumentation/liberty/liberty-dispatcher-20.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/liberty/dispatcher/LibertyDispatcherNetAttributesGetter.java +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.javaagent.instrumentation.liberty.dispatcher; - -import io.opentelemetry.instrumentation.api.instrumenter.net.NetServerAttributesGetter; -import javax.annotation.Nullable; - -public class LibertyDispatcherNetAttributesGetter - implements NetServerAttributesGetter { - - @Nullable - @Override - public String getNetworkProtocolName( - LibertyRequest request, @Nullable LibertyResponse libertyResponse) { - String protocol = request.getProtocol(); - if (protocol != null && protocol.startsWith("HTTP/")) { - return "http"; - } - return null; - } - - @Nullable - @Override - public String getNetworkProtocolVersion( - LibertyRequest request, @Nullable LibertyResponse libertyResponse) { - String protocol = request.getProtocol(); - if (protocol != null && protocol.startsWith("HTTP/")) { - return protocol.substring("HTTP/".length()); - } - return null; - } - - @Nullable - @Override - public String getServerAddress(LibertyRequest request) { - return request.request().getURLHost(); - } - - @Override - public Integer getServerPort(LibertyRequest request) { - return request.request().getURLPort(); - } - - @Override - @Nullable - public String getClientSocketAddress(LibertyRequest request, @Nullable LibertyResponse response) { - return request.getClientSocketAddress(); - } - - @Override - public Integer getClientSocketPort(LibertyRequest request, @Nullable LibertyResponse response) { - return request.getClientSocketPort(); - } - - @Nullable - @Override - public String getServerSocketAddress(LibertyRequest request, @Nullable LibertyResponse response) { - return request.getServerSocketAddress(); - } - - @Nullable - @Override - public Integer getServerSocketPort(LibertyRequest request, @Nullable LibertyResponse response) { - return request.getServerSocketPort(); - } -} diff --git a/instrumentation/liberty/liberty-dispatcher-20.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/liberty/dispatcher/LibertyDispatcherSingletons.java b/instrumentation/liberty/liberty-dispatcher-20.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/liberty/dispatcher/LibertyDispatcherSingletons.java index d96eefc1bf83..5dc10034fd21 100644 --- a/instrumentation/liberty/liberty-dispatcher-20.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/liberty/dispatcher/LibertyDispatcherSingletons.java +++ b/instrumentation/liberty/liberty-dispatcher-20.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/liberty/dispatcher/LibertyDispatcherSingletons.java @@ -6,13 +6,16 @@ package io.opentelemetry.javaagent.instrumentation.liberty.dispatcher; import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.instrumentation.api.incubator.semconv.http.HttpExperimentalAttributesExtractor; +import io.opentelemetry.instrumentation.api.incubator.semconv.http.HttpServerExperimentalMetrics; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteHolder; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpServerAttributesExtractor; -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 io.opentelemetry.javaagent.bootstrap.internal.CommonConfig; +import io.opentelemetry.instrumentation.api.instrumenter.InstrumenterBuilder; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerAttributesExtractor; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerMetrics; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRoute; +import io.opentelemetry.instrumentation.api.semconv.http.HttpSpanNameExtractor; +import io.opentelemetry.instrumentation.api.semconv.http.HttpSpanStatusExtractor; +import io.opentelemetry.javaagent.bootstrap.internal.AgentCommonConfig; public final class LibertyDispatcherSingletons { private static final String INSTRUMENTATION_NAME = "io.opentelemetry.liberty-dispatcher-20.0"; @@ -22,23 +25,32 @@ public final class LibertyDispatcherSingletons { static { LibertyDispatcherHttpAttributesGetter httpAttributesGetter = new LibertyDispatcherHttpAttributesGetter(); - LibertyDispatcherNetAttributesGetter netAttributesGetter = - new LibertyDispatcherNetAttributesGetter(); - INSTRUMENTER = + InstrumenterBuilder builder = Instrumenter.builder( GlobalOpenTelemetry.get(), INSTRUMENTATION_NAME, - HttpSpanNameExtractor.create(httpAttributesGetter)) + HttpSpanNameExtractor.builder(httpAttributesGetter) + .setKnownMethods(AgentCommonConfig.get().getKnownHttpRequestMethods()) + .build()) .setSpanStatusExtractor(HttpSpanStatusExtractor.create(httpAttributesGetter)) .addAttributesExtractor( - HttpServerAttributesExtractor.builder(httpAttributesGetter, netAttributesGetter) - .setCapturedRequestHeaders(CommonConfig.get().getServerRequestHeaders()) - .setCapturedResponseHeaders(CommonConfig.get().getServerResponseHeaders()) + HttpServerAttributesExtractor.builder(httpAttributesGetter) + .setCapturedRequestHeaders(AgentCommonConfig.get().getServerRequestHeaders()) + .setCapturedResponseHeaders(AgentCommonConfig.get().getServerResponseHeaders()) + .setKnownMethods(AgentCommonConfig.get().getKnownHttpRequestMethods()) + .build()) + .addContextCustomizer( + HttpServerRoute.builder(httpAttributesGetter) + .setKnownMethods(AgentCommonConfig.get().getKnownHttpRequestMethods()) .build()) - .addContextCustomizer(HttpRouteHolder.create(httpAttributesGetter)) - .addOperationMetrics(HttpServerMetrics.get()) - .buildServerInstrumenter(LibertyDispatcherRequestGetter.INSTANCE); + .addOperationMetrics(HttpServerMetrics.get()); + if (AgentCommonConfig.get().shouldEmitExperimentalHttpServerTelemetry()) { + builder + .addAttributesExtractor(HttpExperimentalAttributesExtractor.create(httpAttributesGetter)) + .addOperationMetrics(HttpServerExperimentalMetrics.get()); + } + INSTRUMENTER = builder.buildServerInstrumenter(LibertyDispatcherRequestGetter.INSTANCE); } public static Instrumenter instrumenter() { diff --git a/instrumentation/liberty/liberty-dispatcher-20.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/liberty/dispatcher/LibertyRequest.java b/instrumentation/liberty/liberty-dispatcher-20.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/liberty/dispatcher/LibertyRequest.java index fbce488b16b8..4f6020a7dd25 100644 --- a/instrumentation/liberty/liberty-dispatcher-20.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/liberty/dispatcher/LibertyRequest.java +++ b/instrumentation/liberty/liberty-dispatcher-20.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/liberty/dispatcher/LibertyRequest.java @@ -91,8 +91,4 @@ public String getClientSocketAddress() { public int getClientSocketPort() { return clientSocketPort; } - - public HttpRequestMessage request() { - return httpRequestMessage; - } } diff --git a/instrumentation/log4j/log4j-appender-1.2/javaagent/build.gradle.kts b/instrumentation/log4j/log4j-appender-1.2/javaagent/build.gradle.kts index 0cf053ff67e3..6fdfe9074f03 100644 --- a/instrumentation/log4j/log4j-appender-1.2/javaagent/build.gradle.kts +++ b/instrumentation/log4j/log4j-appender-1.2/javaagent/build.gradle.kts @@ -34,4 +34,6 @@ tasks.withType().configureEach { // TODO run tests both with and without experimental log attributes jvmArgs("-Dotel.instrumentation.log4j-appender.experimental.capture-mdc-attributes=*") jvmArgs("-Dotel.instrumentation.log4j-appender.experimental-log-attributes=true") + jvmArgs("-Dotel.instrumentation.log4j-appender.experimental-log-attributes=true") + jvmArgs("-Dotel.instrumentation.common.experimental.controller-telemetry.enabled=true") } diff --git a/instrumentation/log4j/log4j-appender-1.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/log4j/appender/v1_2/LogEventMapper.java b/instrumentation/log4j/log4j-appender-1.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/log4j/appender/v1_2/LogEventMapper.java index 21d57b044e5f..522ef4ad9fb2 100644 --- a/instrumentation/log4j/log4j-appender-1.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/log4j/appender/v1_2/LogEventMapper.java +++ b/instrumentation/log4j/log4j-appender-1.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/log4j/appender/v1_2/LogEventMapper.java @@ -15,10 +15,12 @@ import io.opentelemetry.api.logs.Severity; import io.opentelemetry.context.Context; import io.opentelemetry.instrumentation.api.internal.cache.Cache; -import io.opentelemetry.javaagent.bootstrap.internal.InstrumentationConfig; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.opentelemetry.javaagent.bootstrap.internal.AgentInstrumentationConfig; +import io.opentelemetry.semconv.ExceptionAttributes; +import io.opentelemetry.semconv.incubating.ThreadIncubatingAttributes; import java.io.PrintWriter; import java.io.StringWriter; +import java.time.Instant; import java.util.Hashtable; import java.util.List; import java.util.Map; @@ -37,7 +39,7 @@ public final class LogEventMapper { private static final int TRACE_INT = 5000; private static final boolean captureExperimentalAttributes = - InstrumentationConfig.get() + AgentInstrumentationConfig.get() .getBoolean("otel.instrumentation.log4j-appender.experimental-log-attributes", false); private final Map> captureMdcAttributes; @@ -47,7 +49,7 @@ public final class LogEventMapper { private LogEventMapper() { List captureMdcAttributes = - InstrumentationConfig.get() + AgentInstrumentationConfig.get() .getList( "otel.instrumentation.log4j-appender.experimental.capture-mdc-attributes", emptyList()); @@ -87,19 +89,19 @@ public void capture(Category logger, Priority level, Object message, Throwable t if (throwable != null) { // TODO (trask) extract method for recording exception into // io.opentelemetry:opentelemetry-api - attributes.put(SemanticAttributes.EXCEPTION_TYPE, throwable.getClass().getName()); - attributes.put(SemanticAttributes.EXCEPTION_MESSAGE, throwable.getMessage()); + attributes.put(ExceptionAttributes.EXCEPTION_TYPE, throwable.getClass().getName()); + attributes.put(ExceptionAttributes.EXCEPTION_MESSAGE, throwable.getMessage()); StringWriter writer = new StringWriter(); throwable.printStackTrace(new PrintWriter(writer)); - attributes.put(SemanticAttributes.EXCEPTION_STACKTRACE, writer.toString()); + attributes.put(ExceptionAttributes.EXCEPTION_STACKTRACE, writer.toString()); } captureMdcAttributes(attributes); if (captureExperimentalAttributes) { Thread currentThread = Thread.currentThread(); - attributes.put(SemanticAttributes.THREAD_NAME, currentThread.getName()); - attributes.put(SemanticAttributes.THREAD_ID, currentThread.getId()); + attributes.put(ThreadIncubatingAttributes.THREAD_NAME, currentThread.getName()); + attributes.put(ThreadIncubatingAttributes.THREAD_ID, currentThread.getId()); } builder.setAllAttributes(attributes.build()); @@ -107,6 +109,7 @@ public void capture(Category logger, Priority level, Object message, Throwable t // span context builder.setContext(Context.current()); + builder.setTimestamp(Instant.now()); builder.emit(); } @@ -133,7 +136,7 @@ private void captureMdcAttributes(AttributesBuilder attributes) { } private static AttributeKey getMdcAttributeKey(String key) { - return mdcAttributeKeys.computeIfAbsent(key, k -> AttributeKey.stringKey("log4j.mdc." + k)); + return mdcAttributeKeys.computeIfAbsent(key, AttributeKey::stringKey); } private static Severity levelToSeverity(Priority level) { diff --git a/instrumentation/log4j/log4j-appender-1.2/javaagent/src/test/java/io/opentelemetry/instrumentation/log4j/appender/v1_2/Log4j1Test.java b/instrumentation/log4j/log4j-appender-1.2/javaagent/src/test/java/io/opentelemetry/instrumentation/log4j/appender/v1_2/Log4j1Test.java index f4bd3ae46e65..a5b36a9e5ad9 100644 --- a/instrumentation/log4j/log4j-appender-1.2/javaagent/src/test/java/io/opentelemetry/instrumentation/log4j/appender/v1_2/Log4j1Test.java +++ b/instrumentation/log4j/log4j-appender-1.2/javaagent/src/test/java/io/opentelemetry/instrumentation/log4j/appender/v1_2/Log4j1Test.java @@ -8,19 +8,28 @@ import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.satisfies; +import static java.util.concurrent.TimeUnit.MILLISECONDS; import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.logs.Severity; +import io.opentelemetry.api.trace.SpanContext; import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; import io.opentelemetry.sdk.common.InstrumentationScopeInfo; import io.opentelemetry.sdk.logs.data.LogRecordData; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.opentelemetry.sdk.testing.assertj.AttributeAssertion; +import io.opentelemetry.semconv.ExceptionAttributes; +import io.opentelemetry.semconv.incubating.ThreadIncubatingAttributes; import java.lang.reflect.Field; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; import java.util.stream.Stream; import org.apache.log4j.Logger; import org.apache.log4j.MDC; import org.apache.log4j.helpers.Loader; +import org.assertj.core.api.AssertAccess; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.junit.jupiter.params.ParameterizedTest; @@ -79,6 +88,8 @@ private static void test( String expectedSeverityText) throws InterruptedException { + Instant start = Instant.now(); + // when if (withParent) { testing.runWithSpan( @@ -93,35 +104,45 @@ private static void test( } if (expectedSeverity != null) { - LogRecordData log = testing.waitForLogRecords(1).get(0); - assertThat(log) - .hasBody("xyz") - .hasInstrumentationScope(InstrumentationScopeInfo.builder(expectedLoggerName).build()) - .hasSeverity(expectedSeverity) - .hasSeverityText(expectedSeverityText); - if (logException) { - assertThat(log) - .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.THREAD_NAME, Thread.currentThread().getName()), - equalTo(SemanticAttributes.THREAD_ID, Thread.currentThread().getId()), - equalTo(SemanticAttributes.EXCEPTION_TYPE, IllegalStateException.class.getName()), - equalTo(SemanticAttributes.EXCEPTION_MESSAGE, "hello"), - satisfies( - SemanticAttributes.EXCEPTION_STACKTRACE, - v -> v.contains(Log4j1Test.class.getName()))); - } else { - assertThat(log) - .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.THREAD_NAME, Thread.currentThread().getName()), - equalTo(SemanticAttributes.THREAD_ID, Thread.currentThread().getId())); - } - - if (withParent) { - assertThat(log).hasSpanContext(testing.spans().get(0).getSpanContext()); - } else { - assertThat(log.getSpanContext().isValid()).isFalse(); - } - + testing.waitAndAssertLogRecords( + logRecord -> { + logRecord + .hasBody("xyz") + .hasInstrumentationScope( + InstrumentationScopeInfo.builder(expectedLoggerName).build()) + .hasSeverity(expectedSeverity) + .hasSeverityText(expectedSeverityText) + .hasSpanContext( + withParent + ? testing.spans().get(0).getSpanContext() + : SpanContext.getInvalid()); + + List attributeAsserts = + new ArrayList<>( + Arrays.asList( + equalTo( + ThreadIncubatingAttributes.THREAD_NAME, + Thread.currentThread().getName()), + equalTo( + ThreadIncubatingAttributes.THREAD_ID, Thread.currentThread().getId()))); + if (logException) { + attributeAsserts.addAll( + Arrays.asList( + equalTo( + ExceptionAttributes.EXCEPTION_TYPE, + IllegalStateException.class.getName()), + equalTo(ExceptionAttributes.EXCEPTION_MESSAGE, "hello"), + satisfies( + ExceptionAttributes.EXCEPTION_STACKTRACE, + v -> v.contains(Log4j1Test.class.getName())))); + } + logRecord.hasAttributesSatisfyingExactly(attributeAsserts); + + LogRecordData logRecordData = AssertAccess.getActual(logRecord); + assertThat(logRecordData.getTimestampEpochNanos()) + .isGreaterThanOrEqualTo(MILLISECONDS.toNanos(start.toEpochMilli())) + .isLessThanOrEqualTo(MILLISECONDS.toNanos(Instant.now().toEpochMilli())); + }); } else { Thread.sleep(500); // sleep a bit just to make sure no log is captured assertThat(testing.logRecords()).isEmpty(); @@ -139,17 +160,19 @@ void testMdc() { MDC.remove("key2"); } - LogRecordData log = testing.waitForLogRecords(1).get(0); - assertThat(log) - .hasBody("xyz") - .hasInstrumentationScope(InstrumentationScopeInfo.builder("abc").build()) - .hasSeverity(Severity.INFO) - .hasSeverityText("INFO") - .hasAttributesSatisfyingExactly( - equalTo(AttributeKey.stringKey("log4j.mdc.key1"), "val1"), - equalTo(AttributeKey.stringKey("log4j.mdc.key2"), "val2"), - equalTo(SemanticAttributes.THREAD_NAME, Thread.currentThread().getName()), - equalTo(SemanticAttributes.THREAD_ID, Thread.currentThread().getId())); + testing.waitAndAssertLogRecords( + logRecord -> + logRecord + .hasBody("xyz") + .hasInstrumentationScope(InstrumentationScopeInfo.builder("abc").build()) + .hasSeverity(Severity.INFO) + .hasSeverityText("INFO") + .hasAttributesSatisfyingExactly( + equalTo(AttributeKey.stringKey("key1"), "val1"), + equalTo(AttributeKey.stringKey("key2"), "val2"), + equalTo( + ThreadIncubatingAttributes.THREAD_NAME, Thread.currentThread().getName()), + equalTo(ThreadIncubatingAttributes.THREAD_ID, Thread.currentThread().getId()))); } private static void performLogging( diff --git a/instrumentation/log4j/log4j-appender-2.17/javaagent/README.md b/instrumentation/log4j/log4j-appender-2.17/javaagent/README.md index ad1fde9bb344..a78c4d9c395e 100644 --- a/instrumentation/log4j/log4j-appender-2.17/javaagent/README.md +++ b/instrumentation/log4j/log4j-appender-2.17/javaagent/README.md @@ -1,10 +1,8 @@ # Settings for the Log4j Appender instrumentation -| System property | Type | Default | Description | -|---|---------|--|------------------------------------------------------| -| `otel.instrumentation.log4j-appender.experimental-log-attributes` | Boolean | `false` | Enable the capture of experimental span attributes `thread.name` and `thread.id`. | -| `otel.instrumentation.log4j-appender.experimental.capture-map-message-attributes` | Boolean | `false` | Enable the capture of `MapMessage` attributes. | -| `otel.instrumentation.log4j-appender.experimental.capture-marker-attribute` | Boolean | `false` | Enable the capture of Log4j markers as attributes. | -| `otel.instrumentation.log4j-appender.experimental.capture-context-data-attributes` | String | | List of context data attributes to capture. Use the wildcard character `*` to capture all attributes. | - -[source code attributes]: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/span-general.md#source-code-attributes +| System property | Type | Default | Description | +|-----------------------------------------------------------------------------------| ------- | ------- |-----------------------------------------------------------------------------------------------------------------------| +| `otel.instrumentation.log4j-appender.experimental-log-attributes` | Boolean | `false` | Enable the capture of experimental log attributes `thread.name` and `thread.id`. | +| `otel.instrumentation.log4j-appender.experimental.capture-map-message-attributes` | Boolean | `false` | Enable the capture of `MapMessage` attributes. | +| `otel.instrumentation.log4j-appender.experimental.capture-marker-attribute` | Boolean | `false` | Enable the capture of Log4j markers as attributes. | +| `otel.instrumentation.log4j-appender.experimental.capture-mdc-attributes` | String | | Comma separated list of context data attributes to capture. Use the wildcard character `*` to capture all attributes. | diff --git a/instrumentation/log4j/log4j-appender-2.17/javaagent/build.gradle.kts b/instrumentation/log4j/log4j-appender-2.17/javaagent/build.gradle.kts index 022c1c049eb9..037784a7c781 100644 --- a/instrumentation/log4j/log4j-appender-2.17/javaagent/build.gradle.kts +++ b/instrumentation/log4j/log4j-appender-2.17/javaagent/build.gradle.kts @@ -22,9 +22,10 @@ dependencies { testImplementation("org.awaitility:awaitility") - // this dependency is needed for the slf4j->log4j test if (testLatestDeps) { - testImplementation("org.apache.logging.log4j:log4j-slf4j2-impl:2.19.0") + // this dependency is needed for the slf4j->log4j test + testImplementation("org.apache.logging.log4j:log4j-slf4j2-impl:2.+") + testCompileOnly("biz.aQute.bnd:biz.aQute.bnd.annotation:7.0.0") } else { // log4j 2.17 doesn't have an slf4j2 bridge testImplementation("org.apache.logging.log4j:log4j-slf4j-impl:2.17.0") @@ -56,7 +57,7 @@ tasks { tasks.withType().configureEach { // TODO run tests both with and without experimental log attributes jvmArgs("-Dotel.instrumentation.log4j-appender.experimental.capture-map-message-attributes=true") - jvmArgs("-Dotel.instrumentation.log4j-appender.experimental.capture-context-data-attributes=*") + jvmArgs("-Dotel.instrumentation.log4j-appender.experimental.capture-mdc-attributes=*") jvmArgs("-Dotel.instrumentation.log4j-appender.experimental-log-attributes=true") jvmArgs("-Dotel.instrumentation.log4j-appender.experimental.capture-marker-attribute=true") } diff --git a/instrumentation/log4j/log4j-appender-2.17/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/log4j/appender/v2_17/Log4jHelper.java b/instrumentation/log4j/log4j-appender-2.17/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/log4j/appender/v2_17/Log4jHelper.java index 6a31624a42bf..db4099bcfa84 100644 --- a/instrumentation/log4j/log4j-appender-2.17/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/log4j/appender/v2_17/Log4jHelper.java +++ b/instrumentation/log4j/log4j-appender-2.17/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/log4j/appender/v2_17/Log4jHelper.java @@ -9,9 +9,10 @@ import io.opentelemetry.api.GlobalOpenTelemetry; import io.opentelemetry.api.logs.LogRecordBuilder; +import io.opentelemetry.instrumentation.api.incubator.config.internal.InstrumentationConfig; import io.opentelemetry.instrumentation.log4j.appender.v2_17.internal.ContextDataAccessor; import io.opentelemetry.instrumentation.log4j.appender.v2_17.internal.LogEventMapper; -import io.opentelemetry.javaagent.bootstrap.internal.InstrumentationConfig; +import io.opentelemetry.javaagent.bootstrap.internal.AgentInstrumentationConfig; import java.time.Instant; import java.util.List; import java.util.Map; @@ -27,10 +28,12 @@ public final class Log4jHelper { private static final LogEventMapper> mapper; + private static final boolean captureExperimentalAttributes; + static { - InstrumentationConfig config = InstrumentationConfig.get(); + InstrumentationConfig config = AgentInstrumentationConfig.get(); - boolean captureExperimentalAttributes = + captureExperimentalAttributes = config.getBoolean("otel.instrumentation.log4j-appender.experimental-log-attributes", false); boolean captureMapMessageAttributes = config.getBoolean( @@ -41,8 +44,7 @@ public final class Log4jHelper { "otel.instrumentation.log4j-appender.experimental.capture-marker-attribute", false); List captureContextDataAttributes = config.getList( - "otel.instrumentation.log4j-appender.experimental.capture-context-data-attributes", - emptyList()); + "otel.instrumentation.log4j-appender.experimental.capture-mdc-attributes", emptyList()); mapper = new LogEventMapper<>( @@ -66,7 +68,16 @@ public static void capture( .build() .logRecordBuilder(); Map contextData = ThreadContext.getImmutableContext(); - mapper.mapLogEvent(builder, message, level, marker, throwable, contextData); + + String threadName = null; + long threadId = -1; + if (captureExperimentalAttributes) { + Thread currentThread = Thread.currentThread(); + threadName = currentThread.getName(); + threadId = currentThread.getId(); + } + mapper.mapLogEvent( + builder, message, level, marker, throwable, contextData, threadName, threadId); builder.setTimestamp(Instant.now()); builder.emit(); } diff --git a/instrumentation/log4j/log4j-appender-2.17/javaagent/src/test/java/io/opentelemetry/instrumentation/log4j/appender/v2_17/Log4j2Test.java b/instrumentation/log4j/log4j-appender-2.17/javaagent/src/test/java/io/opentelemetry/instrumentation/log4j/appender/v2_17/Log4j2Test.java index 612d9fce4cf5..50c0a2999c9e 100644 --- a/instrumentation/log4j/log4j-appender-2.17/javaagent/src/test/java/io/opentelemetry/instrumentation/log4j/appender/v2_17/Log4j2Test.java +++ b/instrumentation/log4j/log4j-appender-2.17/javaagent/src/test/java/io/opentelemetry/instrumentation/log4j/appender/v2_17/Log4j2Test.java @@ -12,12 +12,18 @@ import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.logs.Severity; +import io.opentelemetry.api.trace.SpanContext; import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; import io.opentelemetry.sdk.common.InstrumentationScopeInfo; import io.opentelemetry.sdk.logs.data.LogRecordData; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.opentelemetry.sdk.testing.assertj.AttributeAssertion; +import io.opentelemetry.semconv.ExceptionAttributes; +import io.opentelemetry.semconv.incubating.ThreadIncubatingAttributes; import java.time.Instant; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; import java.util.stream.Stream; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -26,6 +32,7 @@ import org.apache.logging.log4j.ThreadContext; import org.apache.logging.log4j.message.StringMapMessage; import org.apache.logging.log4j.message.StructuredDataMessage; +import org.assertj.core.api.AssertAccess; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.junit.jupiter.params.ParameterizedTest; @@ -86,40 +93,45 @@ private static void test( } if (expectedSeverity != null) { - LogRecordData log = testing.waitForLogRecords(1).get(0); - assertThat(log) - .hasBody("xyz: 123") - .hasInstrumentationScope(InstrumentationScopeInfo.builder(expectedLoggerName).build()) - .hasSeverity(expectedSeverity) - .hasSeverityText(expectedSeverityText); - - assertThat(log.getTimestampEpochNanos()) - .isGreaterThanOrEqualTo(MILLISECONDS.toNanos(start.toEpochMilli())) - .isLessThanOrEqualTo(MILLISECONDS.toNanos(Instant.now().toEpochMilli())); - - if (logException) { - assertThat(log) - .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.THREAD_NAME, Thread.currentThread().getName()), - equalTo(SemanticAttributes.THREAD_ID, Thread.currentThread().getId()), - equalTo(SemanticAttributes.EXCEPTION_TYPE, IllegalStateException.class.getName()), - equalTo(SemanticAttributes.EXCEPTION_MESSAGE, "hello"), - satisfies( - SemanticAttributes.EXCEPTION_STACKTRACE, - v -> v.contains(Log4j2Test.class.getName()))); - } else { - assertThat(log) - .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.THREAD_NAME, Thread.currentThread().getName()), - equalTo(SemanticAttributes.THREAD_ID, Thread.currentThread().getId())); - } - - if (withParent) { - assertThat(log).hasSpanContext(testing.spans().get(0).getSpanContext()); - } else { - assertThat(log.getSpanContext().isValid()).isFalse(); - } - + testing.waitAndAssertLogRecords( + logRecord -> { + logRecord + .hasBody("xyz: 123") + .hasInstrumentationScope( + InstrumentationScopeInfo.builder(expectedLoggerName).build()) + .hasSeverity(expectedSeverity) + .hasSeverityText(expectedSeverityText) + .hasSpanContext( + withParent + ? testing.spans().get(0).getSpanContext() + : SpanContext.getInvalid()); + + List attributeAsserts = + new ArrayList<>( + Arrays.asList( + equalTo( + ThreadIncubatingAttributes.THREAD_NAME, + Thread.currentThread().getName()), + equalTo( + ThreadIncubatingAttributes.THREAD_ID, Thread.currentThread().getId()))); + if (logException) { + attributeAsserts.addAll( + Arrays.asList( + equalTo( + ExceptionAttributes.EXCEPTION_TYPE, + IllegalStateException.class.getName()), + equalTo(ExceptionAttributes.EXCEPTION_MESSAGE, "hello"), + satisfies( + ExceptionAttributes.EXCEPTION_STACKTRACE, + v -> v.contains(Log4j2Test.class.getName())))); + } + logRecord.hasAttributesSatisfyingExactly(attributeAsserts); + + LogRecordData logRecordData = AssertAccess.getActual(logRecord); + assertThat(logRecordData.getTimestampEpochNanos()) + .isGreaterThanOrEqualTo(MILLISECONDS.toNanos(start.toEpochMilli())) + .isLessThanOrEqualTo(MILLISECONDS.toNanos(Instant.now().toEpochMilli())); + }); } else { Thread.sleep(500); // sleep a bit just to make sure no log is captured assertThat(testing.logRecords()).isEmpty(); @@ -136,17 +148,19 @@ void testContextData() { ThreadContext.clearMap(); } - LogRecordData log = testing.waitForLogRecords(1).get(0); - assertThat(log) - .hasBody("xyz: 123") - .hasInstrumentationScope(InstrumentationScopeInfo.builder("abc").build()) - .hasSeverity(Severity.INFO) - .hasSeverityText("INFO") - .hasAttributesSatisfyingExactly( - equalTo(AttributeKey.stringKey("log4j.context_data.key1"), "val1"), - equalTo(AttributeKey.stringKey("log4j.context_data.key2"), "val2"), - equalTo(SemanticAttributes.THREAD_NAME, Thread.currentThread().getName()), - equalTo(SemanticAttributes.THREAD_ID, Thread.currentThread().getId())); + testing.waitAndAssertLogRecords( + logRecord -> + logRecord + .hasBody("xyz: 123") + .hasInstrumentationScope(InstrumentationScopeInfo.builder("abc").build()) + .hasSeverity(Severity.INFO) + .hasSeverityText("INFO") + .hasAttributesSatisfyingExactly( + equalTo(AttributeKey.stringKey("key1"), "val1"), + equalTo(AttributeKey.stringKey("key2"), "val2"), + equalTo( + ThreadIncubatingAttributes.THREAD_NAME, Thread.currentThread().getName()), + equalTo(ThreadIncubatingAttributes.THREAD_ID, Thread.currentThread().getId()))); } @Test @@ -156,17 +170,19 @@ void testStringMapMessage() { message.put("key2", "val2"); logger.info(message); - LogRecordData log = testing.waitForLogRecords(1).get(0); - assertThat(log) - .hasBody("") - .hasInstrumentationScope(InstrumentationScopeInfo.builder("abc").build()) - .hasSeverity(Severity.INFO) - .hasSeverityText("INFO") - .hasAttributesSatisfyingExactly( - equalTo(AttributeKey.stringKey("log4j.map_message.key1"), "val1"), - equalTo(AttributeKey.stringKey("log4j.map_message.key2"), "val2"), - equalTo(SemanticAttributes.THREAD_NAME, Thread.currentThread().getName()), - equalTo(SemanticAttributes.THREAD_ID, Thread.currentThread().getId())); + testing.waitAndAssertLogRecords( + logRecord -> + logRecord + .hasBody("") + .hasInstrumentationScope(InstrumentationScopeInfo.builder("abc").build()) + .hasSeverity(Severity.INFO) + .hasSeverityText("INFO") + .hasAttributesSatisfyingExactly( + equalTo(AttributeKey.stringKey("log4j.map_message.key1"), "val1"), + equalTo(AttributeKey.stringKey("log4j.map_message.key2"), "val2"), + equalTo( + ThreadIncubatingAttributes.THREAD_NAME, Thread.currentThread().getName()), + equalTo(ThreadIncubatingAttributes.THREAD_ID, Thread.currentThread().getId()))); } @Test @@ -176,16 +192,18 @@ void testStringMapMessageWithSpecialAttribute() { message.put("message", "val2"); logger.info(message); - LogRecordData log = testing.waitForLogRecords(1).get(0); - assertThat(log) - .hasBody("val2") - .hasInstrumentationScope(InstrumentationScopeInfo.builder("abc").build()) - .hasSeverity(Severity.INFO) - .hasSeverityText("INFO") - .hasAttributesSatisfyingExactly( - equalTo(AttributeKey.stringKey("log4j.map_message.key1"), "val1"), - equalTo(SemanticAttributes.THREAD_NAME, Thread.currentThread().getName()), - equalTo(SemanticAttributes.THREAD_ID, Thread.currentThread().getId())); + testing.waitAndAssertLogRecords( + logRecord -> + logRecord + .hasBody("val2") + .hasInstrumentationScope(InstrumentationScopeInfo.builder("abc").build()) + .hasSeverity(Severity.INFO) + .hasSeverityText("INFO") + .hasAttributesSatisfyingExactly( + equalTo(AttributeKey.stringKey("log4j.map_message.key1"), "val1"), + equalTo( + ThreadIncubatingAttributes.THREAD_NAME, Thread.currentThread().getName()), + equalTo(ThreadIncubatingAttributes.THREAD_ID, Thread.currentThread().getId()))); } @Test @@ -195,33 +213,34 @@ void testStructuredDataMapMessage() { message.put("key2", "val2"); logger.info(message); - LogRecordData log = testing.waitForLogRecords(1).get(0); - assertThat(log) - .hasBody("a message") - .hasInstrumentationScope(InstrumentationScopeInfo.builder("abc").build()) - .hasSeverity(Severity.INFO) - .hasSeverityText("INFO") - .hasAttributesSatisfyingExactly( - equalTo(AttributeKey.stringKey("log4j.map_message.key1"), "val1"), - equalTo(AttributeKey.stringKey("log4j.map_message.key2"), "val2"), - equalTo(SemanticAttributes.THREAD_NAME, Thread.currentThread().getName()), - equalTo(SemanticAttributes.THREAD_ID, Thread.currentThread().getId())); + testing.waitAndAssertLogRecords( + logRecord -> + logRecord + .hasBody("a message") + .hasInstrumentationScope(InstrumentationScopeInfo.builder("abc").build()) + .hasSeverity(Severity.INFO) + .hasSeverityText("INFO") + .hasAttributesSatisfyingExactly( + equalTo(AttributeKey.stringKey("log4j.map_message.key1"), "val1"), + equalTo(AttributeKey.stringKey("log4j.map_message.key2"), "val2"), + equalTo( + ThreadIncubatingAttributes.THREAD_NAME, Thread.currentThread().getName()), + equalTo(ThreadIncubatingAttributes.THREAD_ID, Thread.currentThread().getId()))); } @Test public void testMarker() { - String markerName = "aMarker"; Marker marker = MarkerManager.getMarker(markerName); logger.info(marker, "Message"); - LogRecordData log = testing.waitForLogRecords(1).get(0); - assertThat(log) - .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.THREAD_NAME, Thread.currentThread().getName()), - equalTo(SemanticAttributes.THREAD_ID, Thread.currentThread().getId()), - equalTo(AttributeKey.stringKey("log4j.marker"), markerName)); + testing.waitAndAssertLogRecords( + logRecord -> + logRecord.hasAttributesSatisfyingExactly( + equalTo(ThreadIncubatingAttributes.THREAD_NAME, Thread.currentThread().getName()), + equalTo(ThreadIncubatingAttributes.THREAD_ID, Thread.currentThread().getId()), + equalTo(AttributeKey.stringKey("log4j.marker"), markerName))); } private static void performLogging( diff --git a/instrumentation/log4j/log4j-appender-2.17/javaagent/src/test/java/io/opentelemetry/instrumentation/log4j/appender/v2_17/Slf4jToLog4jTest.java b/instrumentation/log4j/log4j-appender-2.17/javaagent/src/test/java/io/opentelemetry/instrumentation/log4j/appender/v2_17/Slf4jToLog4jTest.java index 130069352c62..0a842bbd16cc 100644 --- a/instrumentation/log4j/log4j-appender-2.17/javaagent/src/test/java/io/opentelemetry/instrumentation/log4j/appender/v2_17/Slf4jToLog4jTest.java +++ b/instrumentation/log4j/log4j-appender-2.17/javaagent/src/test/java/io/opentelemetry/instrumentation/log4j/appender/v2_17/Slf4jToLog4jTest.java @@ -11,11 +11,16 @@ import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.logs.Severity; +import io.opentelemetry.api.trace.SpanContext; import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; import io.opentelemetry.sdk.common.InstrumentationScopeInfo; -import io.opentelemetry.sdk.logs.data.LogRecordData; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.opentelemetry.sdk.testing.assertj.AttributeAssertion; +import io.opentelemetry.semconv.ExceptionAttributes; +import io.opentelemetry.semconv.incubating.ThreadIncubatingAttributes; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; import java.util.stream.Stream; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; @@ -80,35 +85,40 @@ private static void test( } if (expectedSeverity != null) { - LogRecordData log = testing.waitForLogRecords(1).get(0); - assertThat(log) - .hasBody("xyz: 123") - .hasInstrumentationScope(InstrumentationScopeInfo.builder(expectedLoggerName).build()) - .hasSeverity(expectedSeverity) - .hasSeverityText(expectedSeverityText); - if (logException) { - assertThat(log) - .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.THREAD_NAME, Thread.currentThread().getName()), - equalTo(SemanticAttributes.THREAD_ID, Thread.currentThread().getId()), - equalTo(SemanticAttributes.EXCEPTION_TYPE, IllegalStateException.class.getName()), - equalTo(SemanticAttributes.EXCEPTION_MESSAGE, "hello"), - satisfies( - SemanticAttributes.EXCEPTION_STACKTRACE, - v -> v.contains(Slf4jToLog4jTest.class.getName()))); - } else { - assertThat(log) - .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.THREAD_NAME, Thread.currentThread().getName()), - equalTo(SemanticAttributes.THREAD_ID, Thread.currentThread().getId())); - } - - if (withParent) { - assertThat(log).hasSpanContext(testing.spans().get(0).getSpanContext()); - } else { - assertThat(log.getSpanContext().isValid()).isFalse(); - } - + testing.waitAndAssertLogRecords( + logRecord -> { + logRecord + .hasBody("xyz: 123") + .hasInstrumentationScope( + InstrumentationScopeInfo.builder(expectedLoggerName).build()) + .hasSeverity(expectedSeverity) + .hasSeverityText(expectedSeverityText) + .hasSpanContext( + withParent + ? testing.spans().get(0).getSpanContext() + : SpanContext.getInvalid()); + + List attributeAsserts = + new ArrayList<>( + Arrays.asList( + equalTo( + ThreadIncubatingAttributes.THREAD_NAME, + Thread.currentThread().getName()), + equalTo( + ThreadIncubatingAttributes.THREAD_ID, Thread.currentThread().getId()))); + if (logException) { + attributeAsserts.addAll( + Arrays.asList( + equalTo( + ExceptionAttributes.EXCEPTION_TYPE, + IllegalStateException.class.getName()), + equalTo(ExceptionAttributes.EXCEPTION_MESSAGE, "hello"), + satisfies( + ExceptionAttributes.EXCEPTION_STACKTRACE, + v -> v.contains(Slf4jToLog4jTest.class.getName())))); + } + logRecord.hasAttributesSatisfyingExactly(attributeAsserts); + }); } else { Thread.sleep(500); // sleep a bit just to make sure no log is captured assertThat(testing.logRecords()).isEmpty(); @@ -125,17 +135,19 @@ void testMdc() { MDC.clear(); } - LogRecordData log = testing.waitForLogRecords(1).get(0); - assertThat(log) - .hasBody("xyz: 123") - .hasInstrumentationScope(InstrumentationScopeInfo.builder("abc").build()) - .hasSeverity(Severity.INFO) - .hasSeverityText("INFO") - .hasAttributesSatisfyingExactly( - equalTo(AttributeKey.stringKey("log4j.context_data.key1"), "val1"), - equalTo(AttributeKey.stringKey("log4j.context_data.key2"), "val2"), - equalTo(SemanticAttributes.THREAD_NAME, Thread.currentThread().getName()), - equalTo(SemanticAttributes.THREAD_ID, Thread.currentThread().getId())); + testing.waitAndAssertLogRecords( + logRecord -> + logRecord + .hasBody("xyz: 123") + .hasInstrumentationScope(InstrumentationScopeInfo.builder("abc").build()) + .hasSeverity(Severity.INFO) + .hasSeverityText("INFO") + .hasAttributesSatisfyingExactly( + equalTo(AttributeKey.stringKey("key1"), "val1"), + equalTo(AttributeKey.stringKey("key2"), "val2"), + equalTo( + ThreadIncubatingAttributes.THREAD_NAME, Thread.currentThread().getName()), + equalTo(ThreadIncubatingAttributes.THREAD_ID, Thread.currentThread().getId()))); } @Test @@ -146,12 +158,12 @@ public void testMarker() { logger.info(marker, "Message"); - LogRecordData log = testing.waitForLogRecords(1).get(0); - assertThat(log) - .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.THREAD_NAME, Thread.currentThread().getName()), - equalTo(SemanticAttributes.THREAD_ID, Thread.currentThread().getId()), - equalTo(AttributeKey.stringKey("log4j.marker"), markerName)); + testing.waitAndAssertLogRecords( + logRecord -> + logRecord.hasAttributesSatisfyingExactly( + equalTo(ThreadIncubatingAttributes.THREAD_NAME, Thread.currentThread().getName()), + equalTo(ThreadIncubatingAttributes.THREAD_ID, Thread.currentThread().getId()), + equalTo(AttributeKey.stringKey("log4j.marker"), markerName))); } private static void performLogging( diff --git a/instrumentation/log4j/log4j-appender-2.17/library/README.md b/instrumentation/log4j/log4j-appender-2.17/library/README.md index 4944d252da91..4b765b1d4a36 100644 --- a/instrumentation/log4j/log4j-appender-2.17/library/README.md +++ b/instrumentation/log4j/log4j-appender-2.17/library/README.md @@ -37,7 +37,7 @@ The following demonstrates how you might configure the appender in your `log4j.x ```xml - + + + +``` + +The available settings are: + +| XML Attribute | Type | Default | Description | +|------------------------------------|---------|---------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `captureExperimentalAttributes` | Boolean | `false` | Enable the capture of experimental log attributes `thread.name` and `thread.id`. | +| `captureMapMessageAttributes` | Boolean | `false` | Enable the capture of `MapMessage` attributes. | +| `captureMarkerAttribute` | Boolean | `false` | Enable the capture of Log4j markers as attributes. | +| `captureContextDataAttributes` | String | | Comma separated list of context data attributes to capture. Use the wildcard character `*` to capture all attributes. | +| `numLogsCapturedBeforeOtelInstall` | Integer | 1000 | Log telemetry is emitted after the initialization of the OpenTelemetry Log4j appender with an OpenTelemetry object. This setting allows you to modify the size of the cache used to replay the first logs. | diff --git a/instrumentation/log4j/log4j-appender-2.17/library/build.gradle.kts b/instrumentation/log4j/log4j-appender-2.17/library/build.gradle.kts index 6a48ee260f96..a93a67fc67b6 100644 --- a/instrumentation/log4j/log4j-appender-2.17/library/build.gradle.kts +++ b/instrumentation/log4j/log4j-appender-2.17/library/build.gradle.kts @@ -4,6 +4,15 @@ plugins { dependencies { library("org.apache.logging.log4j:log4j-core:2.17.0") + annotationProcessor("org.apache.logging.log4j:log4j-core:2.17.0") testImplementation("io.opentelemetry:opentelemetry-sdk-testing") + + if (findProperty("testLatestDeps") as Boolean) { + testCompileOnly("biz.aQute.bnd:biz.aQute.bnd.annotation:7.0.0") + } +} + +tasks.withType().configureEach { + jvmArgs("-Dotel.instrumentation.common.experimental.controller-telemetry.enabled=true") } diff --git a/instrumentation/log4j/log4j-appender-2.17/library/src/main/java/io/opentelemetry/instrumentation/log4j/appender/v2_17/LogEventToReplay.java b/instrumentation/log4j/log4j-appender-2.17/library/src/main/java/io/opentelemetry/instrumentation/log4j/appender/v2_17/LogEventToReplay.java new file mode 100644 index 000000000000..d0b887450703 --- /dev/null +++ b/instrumentation/log4j/log4j-appender-2.17/library/src/main/java/io/opentelemetry/instrumentation/log4j/appender/v2_17/LogEventToReplay.java @@ -0,0 +1,210 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.log4j.appender.v2_17; + +import java.util.Collections; +import java.util.Map; +import javax.annotation.Nullable; +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.Marker; +import org.apache.logging.log4j.ThreadContext; +import org.apache.logging.log4j.core.LogEvent; +import org.apache.logging.log4j.core.impl.ThrowableProxy; +import org.apache.logging.log4j.core.time.Instant; +import org.apache.logging.log4j.message.Message; +import org.apache.logging.log4j.message.StringMapMessage; +import org.apache.logging.log4j.message.StructuredDataMessage; +import org.apache.logging.log4j.util.ReadOnlyStringMap; + +class LogEventToReplay implements LogEvent { + + private static final long serialVersionUID = 1L; + + // Log4j 2 reuses LogEvent object, so we make a copy of all the fields that are used during export + // in order to be able to replay the log event later. + + private final String loggerName; + private final Message message; + private final Level level; + private final Instant instant; + private final Throwable thrown; + private final Marker marker; + private final ReadOnlyStringMap contextData; + private final String threadName; + private final long threadId; + + LogEventToReplay(LogEvent logEvent) { + this.loggerName = logEvent.getLoggerName(); + Message messageOrigin = logEvent.getMessage(); + if (messageOrigin instanceof StructuredDataMessage) { + StructuredDataMessage structuredDataMessage = (StructuredDataMessage) messageOrigin; + this.message = + // Log4j 2 reuses StructuredDataMessage object + new StructuredDataMessage( + structuredDataMessage.getId(), + structuredDataMessage.getFormat(), + structuredDataMessage.getType(), + structuredDataMessage.getData()); + } else if (messageOrigin instanceof StringMapMessage) { + // StringMapMessage objects are not reused by Log4j 2 + this.message = messageOrigin; + } else { + this.message = new MessageCopy(logEvent.getMessage()); + } + + this.level = logEvent.getLevel(); + this.instant = logEvent.getInstant(); + this.thrown = logEvent.getThrown(); + this.marker = logEvent.getMarker(); + this.contextData = logEvent.getContextData(); + this.threadName = logEvent.getThreadName(); + this.threadId = logEvent.getThreadId(); + } + + @Override + public LogEvent toImmutable() { + return null; + } + + @SuppressWarnings("deprecation") // Override + @Override + public Map getContextMap() { + return Collections.emptyMap(); + } + + @Override + public ReadOnlyStringMap getContextData() { + return contextData; + } + + @Nullable + @Override + public ThreadContext.ContextStack getContextStack() { + return null; + } + + @Override + public String getLoggerFqcn() { + return null; + } + + @Override + public Level getLevel() { + return level; + } + + @Override + public String getLoggerName() { + return loggerName; + } + + @Override + public Marker getMarker() { + return marker; + } + + @Override + public Message getMessage() { + return message; + } + + @Override + public long getTimeMillis() { + return 0; + } + + @Override + public Instant getInstant() { + return instant; + } + + @Override + public StackTraceElement getSource() { + return null; + } + + @Override + public String getThreadName() { + return threadName; + } + + @Override + public long getThreadId() { + return threadId; + } + + @Override + public int getThreadPriority() { + return 0; + } + + @Override + public Throwable getThrown() { + return thrown; + } + + @Override + public ThrowableProxy getThrownProxy() { + return null; + } + + @Override + public boolean isEndOfBatch() { + return false; + } + + @Override + public boolean isIncludeLocation() { + return false; + } + + @Override + public void setEndOfBatch(boolean endOfBatch) {} + + @Override + public void setIncludeLocation(boolean locationRequired) {} + + @Override + public long getNanoTime() { + return 0; + } + + private static class MessageCopy implements Message { + + private static final long serialVersionUID = 1L; + private final String formattedMessage; + private final String format; + private final Object[] parameters; + private final Throwable throwable; + + public MessageCopy(Message message) { + this.formattedMessage = message.getFormattedMessage(); + this.format = message.getFormat(); + this.parameters = message.getParameters(); + this.throwable = message.getThrowable(); + } + + @Override + public String getFormattedMessage() { + return formattedMessage; + } + + @Override + public String getFormat() { + return format; + } + + @Override + public Object[] getParameters() { + return parameters; + } + + @Override + public Throwable getThrowable() { + return throwable; + } + } +} diff --git a/instrumentation/log4j/log4j-appender-2.17/library/src/main/java/io/opentelemetry/instrumentation/log4j/appender/v2_17/OpenTelemetryAppender.java b/instrumentation/log4j/log4j-appender-2.17/library/src/main/java/io/opentelemetry/instrumentation/log4j/appender/v2_17/OpenTelemetryAppender.java index cef3c084e8c4..67b396c24311 100644 --- a/instrumentation/log4j/log4j-appender-2.17/library/src/main/java/io/opentelemetry/instrumentation/log4j/appender/v2_17/OpenTelemetryAppender.java +++ b/instrumentation/log4j/log4j-appender-2.17/library/src/main/java/io/opentelemetry/instrumentation/log4j/appender/v2_17/OpenTelemetryAppender.java @@ -8,25 +8,34 @@ import static java.util.Collections.emptyList; import com.google.errorprone.annotations.CanIgnoreReturnValue; -import io.opentelemetry.api.GlobalOpenTelemetry; import io.opentelemetry.api.OpenTelemetry; import io.opentelemetry.api.logs.LogRecordBuilder; import io.opentelemetry.instrumentation.log4j.appender.v2_17.internal.ContextDataAccessor; import io.opentelemetry.instrumentation.log4j.appender.v2_17.internal.LogEventMapper; import java.io.Serializable; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; +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.locks.Lock; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.function.BiConsumer; import java.util.stream.Collectors; import javax.annotation.Nullable; +import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.ThreadContext; import org.apache.logging.log4j.core.Appender; import org.apache.logging.log4j.core.Core; import org.apache.logging.log4j.core.Filter; import org.apache.logging.log4j.core.Layout; import org.apache.logging.log4j.core.LogEvent; +import org.apache.logging.log4j.core.LoggerContext; import org.apache.logging.log4j.core.appender.AbstractAppender; +import org.apache.logging.log4j.core.config.Configuration; import org.apache.logging.log4j.core.config.Property; import org.apache.logging.log4j.core.config.plugins.Plugin; import org.apache.logging.log4j.core.config.plugins.PluginBuilderAttribute; @@ -44,7 +53,35 @@ public class OpenTelemetryAppender extends AbstractAppender { static final String PLUGIN_NAME = "OpenTelemetry"; private final LogEventMapper mapper; - @Nullable private OpenTelemetry openTelemetry; + private volatile OpenTelemetry openTelemetry; + + private final BlockingQueue eventsToReplay; + + private final AtomicBoolean replayLimitWarningLogged = new AtomicBoolean(); + + private final ReadWriteLock lock = new ReentrantReadWriteLock(); + + /** + * Installs the {@code openTelemetry} instance on any {@link OpenTelemetryAppender}s identified in + * the {@link LoggerContext}. + */ + public static void install(OpenTelemetry openTelemetry) { + org.apache.logging.log4j.spi.LoggerContext loggerContextSpi = LogManager.getContext(false); + if (!(loggerContextSpi instanceof LoggerContext)) { + return; + } + LoggerContext loggerContext = (LoggerContext) loggerContextSpi; + Configuration config = loggerContext.getConfiguration(); + config + .getAppenders() + .values() + .forEach( + appender -> { + if (appender instanceof OpenTelemetryAppender) { + ((OpenTelemetryAppender) appender).setOpenTelemetry(openTelemetry); + } + }); + } @PluginBuilderFactory public static > B builder() { @@ -58,6 +95,7 @@ public static class Builder> extends AbstractAppender.Build @PluginBuilderAttribute private boolean captureMapMessageAttributes; @PluginBuilderAttribute private boolean captureMarkerAttribute; @PluginBuilderAttribute private String captureContextDataAttributes; + @PluginBuilderAttribute private int numLogsCapturedBeforeOtelInstall; @Nullable private OpenTelemetry openTelemetry; @@ -97,6 +135,19 @@ public B setCaptureContextDataAttributes(String captureContextDataAttributes) { return asBuilder(); } + /** + * Log telemetry is emitted after the initialization of the OpenTelemetry Logback appender with + * an {@link OpenTelemetry} object. This setting allows you to modify the size of the cache used + * to replay the logs that were emitted prior to setting the OpenTelemetry instance into the + * Logback appender. + */ + @CanIgnoreReturnValue + public B setNumLogsCapturedBeforeOtelInstall(int numLogsCapturedBeforeOtelInstall) { + this.numLogsCapturedBeforeOtelInstall = numLogsCapturedBeforeOtelInstall; + return asBuilder(); + } + + /** Configures the {@link OpenTelemetry} used to append logs. */ @CanIgnoreReturnValue public B setOpenTelemetry(OpenTelemetry openTelemetry) { this.openTelemetry = openTelemetry; @@ -105,6 +156,7 @@ public B setOpenTelemetry(OpenTelemetry openTelemetry) { @Override public OpenTelemetryAppender build() { + OpenTelemetry openTelemetry = this.openTelemetry; return new OpenTelemetryAppender( getName(), getLayout(), @@ -115,6 +167,7 @@ public OpenTelemetryAppender build() { captureMapMessageAttributes, captureMarkerAttribute, captureContextDataAttributes, + numLogsCapturedBeforeOtelInstall, openTelemetry); } } @@ -129,6 +182,7 @@ private OpenTelemetryAppender( boolean captureMapMessageAttributes, boolean captureMarkerAttribute, String captureContextDataAttributes, + int numLogsCapturedBeforeOtelInstall, OpenTelemetry openTelemetry) { super(name, filter, layout, ignoreExceptions, properties); @@ -140,6 +194,11 @@ private OpenTelemetryAppender( captureMarkerAttribute, splitAndFilterBlanksAndNulls(captureContextDataAttributes)); this.openTelemetry = openTelemetry; + if (numLogsCapturedBeforeOtelInstall != 0) { + this.eventsToReplay = new ArrayBlockingQueue<>(numLogsCapturedBeforeOtelInstall); + } else { + this.eventsToReplay = new ArrayBlockingQueue<>(1000); + } } private static List splitAndFilterBlanksAndNulls(String value) { @@ -152,27 +211,66 @@ private static List splitAndFilterBlanksAndNulls(String value) { .collect(Collectors.toList()); } + /** + * Configures the {@link OpenTelemetry} used to append logs. This MUST be called for the appender + * to function. See {@link #install(OpenTelemetry)} for simple installation option. + */ public void setOpenTelemetry(OpenTelemetry openTelemetry) { - this.openTelemetry = openTelemetry; - } - - private OpenTelemetry getOpenTelemetry() { - return openTelemetry == null ? GlobalOpenTelemetry.get() : openTelemetry; + List eventsToReplay = new ArrayList<>(); + Lock writeLock = lock.writeLock(); + writeLock.lock(); + try { + // minimize scope of write lock + this.openTelemetry = openTelemetry; + this.eventsToReplay.drainTo(eventsToReplay); + } finally { + writeLock.unlock(); + } + // now emit + for (LogEventToReplay eventToReplay : eventsToReplay) { + emit(openTelemetry, eventToReplay); + } } + @SuppressWarnings("SystemOut") @Override public void append(LogEvent event) { + OpenTelemetry openTelemetry = this.openTelemetry; + if (openTelemetry != null) { + // optimization to avoid locking after the OpenTelemetry instance is set + emit(openTelemetry, event); + return; + } + + Lock readLock = lock.readLock(); + readLock.lock(); + try { + openTelemetry = this.openTelemetry; + if (openTelemetry != null) { + emit(openTelemetry, event); + return; + } + + LogEventToReplay logEventToReplay = new LogEventToReplay(event); + + if (!eventsToReplay.offer(logEventToReplay) && !replayLimitWarningLogged.getAndSet(true)) { + String message = + "numLogsCapturedBeforeOtelInstall value of the OpenTelemetry appender is too small."; + System.err.println(message); + } + } finally { + readLock.unlock(); + } + } + + private void emit(OpenTelemetry openTelemetry, LogEvent event) { String instrumentationName = event.getLoggerName(); if (instrumentationName == null || instrumentationName.isEmpty()) { instrumentationName = "ROOT"; } LogRecordBuilder builder = - getOpenTelemetry() - .getLogsBridge() - .loggerBuilder(instrumentationName) - .build() - .logRecordBuilder(); + openTelemetry.getLogsBridge().loggerBuilder(instrumentationName).build().logRecordBuilder(); ReadOnlyStringMap contextData = event.getContextData(); mapper.mapLogEvent( builder, @@ -180,7 +278,9 @@ public void append(LogEvent event) { event.getLevel(), event.getMarker(), event.getThrown(), - contextData); + contextData, + event.getThreadName(), + event.getThreadId()); Instant timestamp = event.getInstant(); if (timestamp != null) { diff --git a/instrumentation/log4j/log4j-appender-2.17/library/src/main/java/io/opentelemetry/instrumentation/log4j/appender/v2_17/internal/LogEventMapper.java b/instrumentation/log4j/log4j-appender-2.17/library/src/main/java/io/opentelemetry/instrumentation/log4j/appender/v2_17/internal/LogEventMapper.java index 3b1aa702b104..04dcba7d32fe 100644 --- a/instrumentation/log4j/log4j-appender-2.17/library/src/main/java/io/opentelemetry/instrumentation/log4j/appender/v2_17/internal/LogEventMapper.java +++ b/instrumentation/log4j/log4j-appender-2.17/library/src/main/java/io/opentelemetry/instrumentation/log4j/appender/v2_17/internal/LogEventMapper.java @@ -12,7 +12,7 @@ import io.opentelemetry.api.logs.Severity; import io.opentelemetry.context.Context; import io.opentelemetry.instrumentation.api.internal.cache.Cache; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.opentelemetry.semconv.ExceptionAttributes; import java.io.PrintWriter; import java.io.StringWriter; import java.util.List; @@ -29,6 +29,10 @@ */ public final class LogEventMapper { + // copied from ThreadIncubatingAttributes + private static final AttributeKey THREAD_ID = AttributeKey.longKey("thread.id"); + private static final AttributeKey THREAD_NAME = AttributeKey.stringKey("thread.name"); + private static final String SPECIAL_MAP_MESSAGE_ATTRIBUTE = "message"; private static final Cache> contextDataAttributeKeyCache = @@ -80,7 +84,9 @@ public void mapLogEvent( Level level, @Nullable Marker marker, @Nullable Throwable throwable, - T contextData) { + T contextData, + String threadName, + long threadId) { AttributesBuilder attributes = Attributes.builder(); @@ -105,9 +111,8 @@ public void mapLogEvent( captureContextDataAttributes(attributes, contextData); if (captureExperimentalAttributes) { - Thread currentThread = Thread.currentThread(); - attributes.put(SemanticAttributes.THREAD_NAME, currentThread.getName()); - attributes.put(SemanticAttributes.THREAD_ID, currentThread.getId()); + attributes.put(THREAD_NAME, threadName); + attributes.put(THREAD_ID, threadId); } builder.setAllAttributes(attributes.build()); @@ -175,8 +180,7 @@ void captureContextDataAttributes(AttributesBuilder attributes, T contextData) { } public static AttributeKey getContextDataAttributeKey(String key) { - return contextDataAttributeKeyCache.computeIfAbsent( - key, k -> AttributeKey.stringKey("log4j.context_data." + k)); + return contextDataAttributeKeyCache.computeIfAbsent(key, AttributeKey::stringKey); } public static AttributeKey getMapMessageAttributeKey(String key) { @@ -187,11 +191,11 @@ public static AttributeKey getMapMessageAttributeKey(String key) { private static void setThrowable(AttributesBuilder attributes, Throwable throwable) { // TODO (trask) extract method for recording exception into // io.opentelemetry:opentelemetry-api - attributes.put(SemanticAttributes.EXCEPTION_TYPE, throwable.getClass().getName()); - attributes.put(SemanticAttributes.EXCEPTION_MESSAGE, throwable.getMessage()); + attributes.put(ExceptionAttributes.EXCEPTION_TYPE, throwable.getClass().getName()); + attributes.put(ExceptionAttributes.EXCEPTION_MESSAGE, throwable.getMessage()); StringWriter writer = new StringWriter(); throwable.printStackTrace(new PrintWriter(writer)); - attributes.put(SemanticAttributes.EXCEPTION_STACKTRACE, writer.toString()); + attributes.put(ExceptionAttributes.EXCEPTION_STACKTRACE, writer.toString()); } private static Severity levelToSeverity(Level level) { diff --git a/instrumentation/log4j/log4j-appender-2.17/library/src/test/java/io/opentelemetry/instrumentation/log4j/appender/v2_17/AbstractOpenTelemetryAppenderTest.java b/instrumentation/log4j/log4j-appender-2.17/library/src/test/java/io/opentelemetry/instrumentation/log4j/appender/v2_17/AbstractOpenTelemetryAppenderTest.java new file mode 100644 index 000000000000..c548e99cfc61 --- /dev/null +++ b/instrumentation/log4j/log4j-appender-2.17/library/src/test/java/io/opentelemetry/instrumentation/log4j/appender/v2_17/AbstractOpenTelemetryAppenderTest.java @@ -0,0 +1,239 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.log4j.appender.v2_17; + +import static io.opentelemetry.api.common.AttributeKey.stringKey; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.satisfies; +import static java.util.concurrent.TimeUnit.MILLISECONDS; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.logs.Severity; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.sdk.common.InstrumentationScopeInfo; +import io.opentelemetry.sdk.logs.data.LogRecordData; +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.semconv.ExceptionAttributes; +import java.time.Instant; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.Marker; +import org.apache.logging.log4j.MarkerManager; +import org.apache.logging.log4j.ThreadContext; +import org.apache.logging.log4j.core.impl.Log4jLogEvent; +import org.apache.logging.log4j.message.FormattedMessage; +import org.apache.logging.log4j.message.StringMapMessage; +import org.apache.logging.log4j.message.StructuredDataMessage; +import org.assertj.core.api.AssertAccess; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +abstract class AbstractOpenTelemetryAppenderTest { + + static final Logger logger = LogManager.getLogger("TestLogger"); + + static Resource resource; + static InstrumentationScopeInfo instrumentationScopeInfo; + + void executeAfterLogsExecution() {} + + @BeforeAll + static void setupAll() { + resource = Resource.getDefault(); + instrumentationScopeInfo = InstrumentationScopeInfo.create("TestLogger"); + } + + static void generalBeforeEachSetup() { + ThreadContext.clearAll(); + } + + @AfterAll + static void cleanupAll() { + // This is to make sure that other test classes don't have issues with the logger provider set + OpenTelemetryAppender.install(null); + } + + protected abstract InstrumentationExtension getTesting(); + + @Test + void initializeWithBuilder() { + OpenTelemetryAppender appender = + OpenTelemetryAppender.builder() + .setName("OpenTelemetryAppender") + .setOpenTelemetry(getTesting().getOpenTelemetry()) + .build(); + appender.start(); + + appender.append( + Log4jLogEvent.newBuilder() + .setMessage(new FormattedMessage("log message 1", (Object) null)) + .build()); + + executeAfterLogsExecution(); + + getTesting().waitAndAssertLogRecords(logRecord -> logRecord.hasBody("log message 1")); + } + + @Test + void logNoSpan() { + logger.info("log message 1"); + + executeAfterLogsExecution(); + + getTesting() + .waitAndAssertLogRecords( + logRecord -> + logRecord + .hasResource(resource) + .hasInstrumentationScope(instrumentationScopeInfo) + .hasBody("log message 1") + .hasAttributes(Attributes.empty())); + } + + @Test + void logWithSpanInvalid() { + logger.info("log message"); + + executeAfterLogsExecution(); + + getTesting() + .waitAndAssertLogRecords(logRecord -> logRecord.hasSpanContext(SpanContext.getInvalid())); + } + + @Test + void logWithExtras() { + Instant start = Instant.now(); + logger.info("log message 1", new IllegalStateException("Error!")); + + executeAfterLogsExecution(); + + getTesting() + .waitAndAssertLogRecords( + logRecord -> { + logRecord + .hasResource(resource) + .hasInstrumentationScope(instrumentationScopeInfo) + .hasBody("log message 1") + .hasSeverity(Severity.INFO) + .hasSeverityText("INFO") + .hasAttributesSatisfyingExactly( + equalTo( + ExceptionAttributes.EXCEPTION_TYPE, + IllegalStateException.class.getName()), + equalTo(ExceptionAttributes.EXCEPTION_MESSAGE, "Error!"), + satisfies( + ExceptionAttributes.EXCEPTION_STACKTRACE, + v -> v.contains("logWithExtras"))); + + LogRecordData logRecordData = AssertAccess.getActual(logRecord); + assertThat(logRecordData.getTimestampEpochNanos()) + .isGreaterThanOrEqualTo(MILLISECONDS.toNanos(start.toEpochMilli())) + .isLessThanOrEqualTo(MILLISECONDS.toNanos(Instant.now().toEpochMilli())); + }); + } + + @Test + void logContextData() { + ThreadContext.put("key1", "val1"); + ThreadContext.put("key2", "val2"); + try { + logger.info("log message 1"); + } finally { + ThreadContext.clearMap(); + } + + executeAfterLogsExecution(); + + getTesting() + .waitAndAssertLogRecords( + logRecord -> + logRecord + .hasResource(resource) + .hasInstrumentationScope(instrumentationScopeInfo) + .hasBody("log message 1") + .hasAttributesSatisfyingExactly( + equalTo(stringKey("key1"), "val1"), equalTo(stringKey("key2"), "val2"))); + } + + @Test + void logStringMapMessage() { + StringMapMessage message = new StringMapMessage(); + message.put("key1", "val1"); + message.put("key2", "val2"); + logger.info(message); + + executeAfterLogsExecution(); + + getTesting() + .waitAndAssertLogRecords( + logRecord -> + logRecord + .hasResource(resource) + .hasInstrumentationScope(instrumentationScopeInfo) + .hasAttributesSatisfyingExactly( + equalTo(stringKey("log4j.map_message.key1"), "val1"), + equalTo(stringKey("log4j.map_message.key2"), "val2"))); + } + + @Test + void logStringMapMessageWithSpecialAttribute() { + StringMapMessage message = new StringMapMessage(); + message.put("key1", "val1"); + message.put("message", "val2"); + logger.info(message); + + executeAfterLogsExecution(); + + getTesting() + .waitAndAssertLogRecords( + logRecord -> + logRecord + .hasResource(resource) + .hasInstrumentationScope(instrumentationScopeInfo) + .hasBody("val2") + .hasAttributesSatisfyingExactly( + equalTo(stringKey("log4j.map_message.key1"), "val1"))); + } + + @Test + void testCaptureMarkerAttribute() { + String markerName = "aMarker"; + Marker marker = MarkerManager.getMarker(markerName); + + logger.info(marker, "Message"); + + executeAfterLogsExecution(); + + getTesting() + .waitAndAssertLogRecords( + logRecord -> + logRecord.hasAttributesSatisfying(equalTo(stringKey("log4j.marker"), markerName))); + } + + @Test + void logStructuredDataMessage() { + StructuredDataMessage message = new StructuredDataMessage("an id", "a message", "a type"); + message.put("key1", "val1"); + message.put("key2", "val2"); + logger.info(message); + + executeAfterLogsExecution(); + + getTesting() + .waitAndAssertLogRecords( + logRecord -> + logRecord + .hasResource(resource) + .hasInstrumentationScope(instrumentationScopeInfo) + .hasBody("a message") + .hasAttributesSatisfyingExactly( + equalTo(stringKey("log4j.map_message.key1"), "val1"), + equalTo(stringKey("log4j.map_message.key2"), "val2"))); + } +} diff --git a/instrumentation/log4j/log4j-appender-2.17/library/src/test/java/io/opentelemetry/instrumentation/log4j/appender/v2_17/LogReplayOpenTelemetryAppenderTest.java b/instrumentation/log4j/log4j-appender-2.17/library/src/test/java/io/opentelemetry/instrumentation/log4j/appender/v2_17/LogReplayOpenTelemetryAppenderTest.java new file mode 100644 index 000000000000..c5de31180fab --- /dev/null +++ b/instrumentation/log4j/log4j-appender-2.17/library/src/test/java/io/opentelemetry/instrumentation/log4j/appender/v2_17/LogReplayOpenTelemetryAppenderTest.java @@ -0,0 +1,113 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.log4j.appender.v2_17; + +import static io.opentelemetry.api.common.AttributeKey.stringKey; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; + +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.LibraryInstrumentationExtension; +import org.apache.logging.log4j.message.StringMapMessage; +import org.apache.logging.log4j.message.StructuredDataMessage; +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 LogReplayOpenTelemetryAppenderTest extends AbstractOpenTelemetryAppenderTest { + + @RegisterExtension + private static final LibraryInstrumentationExtension testing = + LibraryInstrumentationExtension.create(); + + @BeforeEach + void setup() { + generalBeforeEachSetup(); + } + + @AfterEach + void resetOpenTelemetry() { + OpenTelemetryAppender.install(null); + } + + @Override + protected InstrumentationExtension getTesting() { + return testing; + } + + @Override + void executeAfterLogsExecution() { + OpenTelemetryAppender.install(testing.getOpenTelemetry()); + } + + @Test + void twoLogs() { + logger.info("log message 1"); + logger.info( + "log message 2"); // Won't be instrumented because cache size is 1 (see log4j2.xml file) + + OpenTelemetryAppender.install(testing.getOpenTelemetry()); + + testing.waitAndAssertLogRecords( + logRecord -> + logRecord + .hasResource(resource) + .hasInstrumentationScope(instrumentationScopeInfo) + .hasBody("log message 1")); + } + + @Test + void twoLogsStringMapMessage() { + StringMapMessage message = new StringMapMessage(); + message.put("key1", "val1"); + message.put("key2", "val2"); + + logger.info(message); + + StringMapMessage message2 = new StringMapMessage(); + message2.put("key1-2", "val1-2"); + message2.put("key2-2", "val2-2"); + + logger.info(message2); // Won't be instrumented because cache size is 1 (see log4j2.xml file) + + OpenTelemetryAppender.install(testing.getOpenTelemetry()); + + testing.waitAndAssertLogRecords( + logRecord -> + logRecord + .hasResource(resource) + .hasInstrumentationScope(instrumentationScopeInfo) + .hasAttributesSatisfyingExactly( + equalTo(stringKey("log4j.map_message.key1"), "val1"), + equalTo(stringKey("log4j.map_message.key2"), "val2"))); + } + + @Test + void twoLogsStructuredDataMessage() { + StructuredDataMessage message = new StructuredDataMessage("an id", "a message", "a type"); + message.put("key1", "val1"); + message.put("key2", "val2"); + logger.info(message); + + StructuredDataMessage message2 = + new StructuredDataMessage("an id 2", "a message 2", "a type 2"); + message.put("key1-2", "val1-2"); + message.put("key2-2", "val2-2"); + logger.info(message2); // Won't be instrumented because cache size is 1 (see log4j2.xml file) + + OpenTelemetryAppender.install(testing.getOpenTelemetry()); + + testing.waitAndAssertLogRecords( + logRecord -> + logRecord + .hasResource(resource) + .hasInstrumentationScope(instrumentationScopeInfo) + .hasBody("a message") + .hasAttributesSatisfyingExactly( + equalTo(stringKey("log4j.map_message.key1"), "val1"), + equalTo(stringKey("log4j.map_message.key2"), "val2"))); + } +} diff --git a/instrumentation/log4j/log4j-appender-2.17/library/src/test/java/io/opentelemetry/instrumentation/log4j/appender/v2_17/OpenTelemetryAppenderConfigTest.java b/instrumentation/log4j/log4j-appender-2.17/library/src/test/java/io/opentelemetry/instrumentation/log4j/appender/v2_17/OpenTelemetryAppenderConfigTest.java deleted file mode 100644 index f442c807fb41..000000000000 --- a/instrumentation/log4j/log4j-appender-2.17/library/src/test/java/io/opentelemetry/instrumentation/log4j/appender/v2_17/OpenTelemetryAppenderConfigTest.java +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.log4j.appender.v2_17; - -import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; - -import io.opentelemetry.api.GlobalOpenTelemetry; -import io.opentelemetry.sdk.OpenTelemetrySdk; -import io.opentelemetry.sdk.common.InstrumentationScopeInfo; -import io.opentelemetry.sdk.logs.SdkLoggerProvider; -import io.opentelemetry.sdk.logs.data.LogRecordData; -import io.opentelemetry.sdk.logs.export.SimpleLogRecordProcessor; -import io.opentelemetry.sdk.resources.Resource; -import io.opentelemetry.sdk.testing.exporter.InMemoryLogRecordExporter; -import java.util.List; -import org.apache.logging.log4j.core.impl.Log4jLogEvent; -import org.apache.logging.log4j.message.FormattedMessage; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; - -class OpenTelemetryAppenderConfigTest extends OpenTelemetryAppenderConfigTestBase { - - @BeforeAll - static void setupAll() { - logRecordExporter = InMemoryLogRecordExporter.create(); - resource = Resource.getDefault(); - instrumentationScopeInfo = InstrumentationScopeInfo.create("TestLogger"); - - SdkLoggerProvider loggerProvider = - SdkLoggerProvider.builder() - .setResource(resource) - .addLogRecordProcessor(SimpleLogRecordProcessor.create(logRecordExporter)) - .build(); - - GlobalOpenTelemetry.resetForTest(); - GlobalOpenTelemetry.set(OpenTelemetrySdk.builder().setLoggerProvider(loggerProvider).build()); - } - - @Test - void initializeWithBuilder() { - OpenTelemetryAppender appender = - OpenTelemetryAppender.builder().setName("OpenTelemetryAppender").build(); - appender.start(); - - appender.append( - Log4jLogEvent.newBuilder() - .setMessage(new FormattedMessage("log message 1", (Object) null)) - .build()); - - List logDataList = logRecordExporter.getFinishedLogRecordItems(); - assertThat(logDataList) - .satisfiesExactly(logRecordData -> assertThat(logDataList.get(0)).hasBody("log message 1")); - } -} diff --git a/instrumentation/log4j/log4j-appender-2.17/library/src/test/java/io/opentelemetry/instrumentation/log4j/appender/v2_17/OpenTelemetryAppenderConfigTestBase.java b/instrumentation/log4j/log4j-appender-2.17/library/src/test/java/io/opentelemetry/instrumentation/log4j/appender/v2_17/OpenTelemetryAppenderConfigTestBase.java deleted file mode 100644 index 535937e5fc3f..000000000000 --- a/instrumentation/log4j/log4j-appender-2.17/library/src/test/java/io/opentelemetry/instrumentation/log4j/appender/v2_17/OpenTelemetryAppenderConfigTestBase.java +++ /dev/null @@ -1,197 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.log4j.appender.v2_17; - -import static io.opentelemetry.api.common.AttributeKey.stringKey; -import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; -import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; -import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.satisfies; -import static java.util.concurrent.TimeUnit.MILLISECONDS; - -import io.opentelemetry.api.OpenTelemetry; -import io.opentelemetry.api.common.Attributes; -import io.opentelemetry.api.logs.Severity; -import io.opentelemetry.api.trace.Span; -import io.opentelemetry.api.trace.SpanContext; -import io.opentelemetry.context.Scope; -import io.opentelemetry.sdk.common.InstrumentationScopeInfo; -import io.opentelemetry.sdk.logs.data.LogRecordData; -import io.opentelemetry.sdk.resources.Resource; -import io.opentelemetry.sdk.testing.exporter.InMemoryLogRecordExporter; -import io.opentelemetry.sdk.trace.SdkTracerProvider; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; -import java.time.Instant; -import java.util.List; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.apache.logging.log4j.Marker; -import org.apache.logging.log4j.MarkerManager; -import org.apache.logging.log4j.ThreadContext; -import org.apache.logging.log4j.message.StringMapMessage; -import org.apache.logging.log4j.message.StructuredDataMessage; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -abstract class OpenTelemetryAppenderConfigTestBase { - - static final Logger logger = LogManager.getLogger("TestLogger"); - - static InMemoryLogRecordExporter logRecordExporter; - static Resource resource; - static InstrumentationScopeInfo instrumentationScopeInfo; - static OpenTelemetry openTelemetry; - - @BeforeEach - void setup() { - logRecordExporter.reset(); - ThreadContext.clearAll(); - } - - @Test - void logNoSpan() { - logger.info("log message 1"); - - List logDataList = logRecordExporter.getFinishedLogRecordItems(); - assertThat(logDataList).hasSize(1); - assertThat(logDataList.get(0)) - .hasResource(resource) - .hasInstrumentationScope(instrumentationScopeInfo) - .hasBody("log message 1") - .hasAttributes(Attributes.empty()); - } - - @Test - void logWithSpan() { - Span span1 = runWithSpan("span1", () -> logger.info("log message 1")); - - logger.info("log message 2"); - - Span span2 = runWithSpan("span2", () -> logger.info("log message 3")); - - List logDataList = logRecordExporter.getFinishedLogRecordItems(); - assertThat(logDataList).hasSize(3); - assertThat(logDataList.get(0).getSpanContext()).isEqualTo(span1.getSpanContext()); - assertThat(logDataList.get(1).getSpanContext()).isEqualTo(SpanContext.getInvalid()); - assertThat(logDataList.get(2).getSpanContext()).isEqualTo(span2.getSpanContext()); - } - - private static Span runWithSpan(String spanName, Runnable runnable) { - Span span = SdkTracerProvider.builder().build().get("tracer").spanBuilder(spanName).startSpan(); - try (Scope ignored = span.makeCurrent()) { - runnable.run(); - } finally { - span.end(); - } - return span; - } - - @Test - void logWithExtras() { - Instant start = Instant.now(); - logger.info("log message 1", new IllegalStateException("Error!")); - - List logDataList = logRecordExporter.getFinishedLogRecordItems(); - assertThat(logDataList).hasSize(1); - assertThat(logDataList.get(0)) - .hasResource(resource) - .hasInstrumentationScope(instrumentationScopeInfo) - .hasBody("log message 1") - .hasSeverity(Severity.INFO) - .hasSeverityText("INFO") - .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.EXCEPTION_TYPE, IllegalStateException.class.getName()), - equalTo(SemanticAttributes.EXCEPTION_MESSAGE, "Error!"), - satisfies(SemanticAttributes.EXCEPTION_STACKTRACE, v -> v.contains("logWithExtras"))); - - assertThat(logDataList.get(0).getTimestampEpochNanos()) - .isGreaterThanOrEqualTo(MILLISECONDS.toNanos(start.toEpochMilli())) - .isLessThanOrEqualTo(MILLISECONDS.toNanos(Instant.now().toEpochMilli())); - } - - @Test - void logContextData() { - ThreadContext.put("key1", "val1"); - ThreadContext.put("key2", "val2"); - try { - logger.info("log message 1"); - } finally { - ThreadContext.clearMap(); - } - - List logDataList = logRecordExporter.getFinishedLogRecordItems(); - assertThat(logDataList).hasSize(1); - assertThat(logDataList.get(0)) - .hasResource(resource) - .hasInstrumentationScope(instrumentationScopeInfo) - .hasBody("log message 1") - .hasAttributesSatisfyingExactly( - equalTo(stringKey("log4j.context_data.key1"), "val1"), - equalTo(stringKey("log4j.context_data.key2"), "val2")); - } - - @Test - void logStringMapMessage() { - StringMapMessage message = new StringMapMessage(); - message.put("key1", "val1"); - message.put("key2", "val2"); - logger.info(message); - - List logDataList = logRecordExporter.getFinishedLogRecordItems(); - assertThat(logDataList).hasSize(1); - assertThat(logDataList.get(0)) - .hasResource(resource) - .hasInstrumentationScope(instrumentationScopeInfo) - .hasAttributesSatisfyingExactly( - equalTo(stringKey("log4j.map_message.key1"), "val1"), - equalTo(stringKey("log4j.map_message.key2"), "val2")); - } - - @Test - void logStringMapMessageWithSpecialAttribute() { - StringMapMessage message = new StringMapMessage(); - message.put("key1", "val1"); - message.put("message", "val2"); - logger.info(message); - - List logDataList = logRecordExporter.getFinishedLogRecordItems(); - assertThat(logDataList).hasSize(1); - assertThat(logDataList.get(0)) - .hasResource(resource) - .hasInstrumentationScope(instrumentationScopeInfo) - .hasBody("val2") - .hasAttributesSatisfyingExactly(equalTo(stringKey("log4j.map_message.key1"), "val1")); - } - - @Test - void testCaptureMarkerAttribute() { - String markerName = "aMarker"; - Marker marker = MarkerManager.getMarker(markerName); - - logger.info(marker, "Message"); - - List logDataList = logRecordExporter.getFinishedLogRecordItems(); - LogRecordData logData = logDataList.get(0); - assertThat(logData.getAttributes().get(stringKey("log4j.marker"))).isEqualTo(markerName); - } - - @Test - void logStructuredDataMessage() { - StructuredDataMessage message = new StructuredDataMessage("an id", "a message", "a type"); - message.put("key1", "val1"); - message.put("key2", "val2"); - logger.info(message); - - List logDataList = logRecordExporter.getFinishedLogRecordItems(); - assertThat(logDataList).hasSize(1); - assertThat(logDataList.get(0)) - .hasResource(resource) - .hasInstrumentationScope(instrumentationScopeInfo) - .hasBody("a message") - .hasAttributesSatisfyingExactly( - equalTo(stringKey("log4j.map_message.key1"), "val1"), - equalTo(stringKey("log4j.map_message.key2"), "val2")); - } -} diff --git a/instrumentation/log4j/log4j-appender-2.17/library/src/test/java/io/opentelemetry/instrumentation/log4j/appender/v2_17/OpenTelemetryAppenderConfigWithOpenTelemetryTest.java b/instrumentation/log4j/log4j-appender-2.17/library/src/test/java/io/opentelemetry/instrumentation/log4j/appender/v2_17/OpenTelemetryAppenderConfigWithOpenTelemetryTest.java deleted file mode 100644 index 897bc7be916f..000000000000 --- a/instrumentation/log4j/log4j-appender-2.17/library/src/test/java/io/opentelemetry/instrumentation/log4j/appender/v2_17/OpenTelemetryAppenderConfigWithOpenTelemetryTest.java +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.log4j.appender.v2_17; - -import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; - -import io.opentelemetry.api.GlobalOpenTelemetry; -import io.opentelemetry.api.OpenTelemetry; -import io.opentelemetry.sdk.OpenTelemetrySdk; -import io.opentelemetry.sdk.common.InstrumentationScopeInfo; -import io.opentelemetry.sdk.logs.SdkLoggerProvider; -import io.opentelemetry.sdk.logs.data.LogRecordData; -import io.opentelemetry.sdk.logs.export.SimpleLogRecordProcessor; -import io.opentelemetry.sdk.resources.Resource; -import io.opentelemetry.sdk.testing.exporter.InMemoryLogRecordExporter; -import java.util.List; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.core.LoggerContext; -import org.apache.logging.log4j.core.config.Configuration; -import org.apache.logging.log4j.core.impl.Log4jLogEvent; -import org.apache.logging.log4j.message.FormattedMessage; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; - -class OpenTelemetryAppenderConfigWithOpenTelemetryTest extends OpenTelemetryAppenderConfigTestBase { - @BeforeAll - static void setupAll() { - logRecordExporter = InMemoryLogRecordExporter.create(); - resource = Resource.getDefault(); - instrumentationScopeInfo = InstrumentationScopeInfo.create("TestLogger"); - - SdkLoggerProvider loggerProvider = - SdkLoggerProvider.builder() - .setResource(resource) - .addLogRecordProcessor(SimpleLogRecordProcessor.create(logRecordExporter)) - .build(); - - GlobalOpenTelemetry.resetForTest(); - openTelemetry = OpenTelemetrySdk.builder().setLoggerProvider(loggerProvider).build(); - setOpenTelemetry(openTelemetry); - } - - private static void setOpenTelemetry(OpenTelemetry openTelemetry) { - Configuration config = ((LoggerContext) LogManager.getContext(false)).getConfiguration(); - config.getAppenders().values().stream() - .filter(a -> a instanceof OpenTelemetryAppender) - .forEach(a -> ((OpenTelemetryAppender) a).setOpenTelemetry(openTelemetry)); - } - - @AfterAll - static void cleanupAll() { - // This is to make sure that other test classes don't have issues with the logger provider set - setOpenTelemetry(null); - } - - @Test - void initializeWithBuilder() { - OpenTelemetryAppender appender = - OpenTelemetryAppender.builder() - .setName("OpenTelemetryAppender") - .setOpenTelemetry(openTelemetry) - .build(); - appender.start(); - - appender.append( - Log4jLogEvent.newBuilder() - .setMessage(new FormattedMessage("log message 1", (Object) null)) - .build()); - - List logDataList = logRecordExporter.getFinishedLogRecordItems(); - assertThat(logDataList) - .satisfiesExactly(logRecordData -> assertThat(logDataList.get(0)).hasBody("log message 1")); - } -} diff --git a/instrumentation/log4j/log4j-appender-2.17/library/src/test/java/io/opentelemetry/instrumentation/log4j/appender/v2_17/OpenTelemetryAppenderTest.java b/instrumentation/log4j/log4j-appender-2.17/library/src/test/java/io/opentelemetry/instrumentation/log4j/appender/v2_17/OpenTelemetryAppenderTest.java new file mode 100644 index 000000000000..e675f56c0fdc --- /dev/null +++ b/instrumentation/log4j/log4j-appender-2.17/library/src/test/java/io/opentelemetry/instrumentation/log4j/appender/v2_17/OpenTelemetryAppenderTest.java @@ -0,0 +1,49 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.log4j.appender.v2_17; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.LibraryInstrumentationExtension; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +class OpenTelemetryAppenderTest extends AbstractOpenTelemetryAppenderTest { + + @RegisterExtension + private static final LibraryInstrumentationExtension testing = + LibraryInstrumentationExtension.create(); + + @BeforeEach + void setup() { + generalBeforeEachSetup(); + OpenTelemetryAppender.install(testing.getOpenTelemetry()); + } + + @Override + protected InstrumentationExtension getTesting() { + return testing; + } + + @Test + void logWithSpan() { // Does not work for log replay but it is not likely to occur because + // the log replay is related to the case where an OpenTelemetry object is not yet available + // at the time the log is executed (and if no OpenTelemetry is available, the context + // propagation can't happen) + Span span1 = + testing.runWithSpan( + "span1", + () -> { + logger.info("log message"); + return Span.current(); + }); + + executeAfterLogsExecution(); + + testing.waitAndAssertLogRecords(logRecord -> logRecord.hasSpanContext(span1.getSpanContext())); + } +} diff --git a/instrumentation/log4j/log4j-appender-2.17/library/src/test/java/io/opentelemetry/instrumentation/log4j/appender/v2_17/internal/LogEventMapperTest.java b/instrumentation/log4j/log4j-appender-2.17/library/src/test/java/io/opentelemetry/instrumentation/log4j/appender/v2_17/internal/LogEventMapperTest.java index 1cc86606e129..793da82e0ba7 100644 --- a/instrumentation/log4j/log4j-appender-2.17/library/src/test/java/io/opentelemetry/instrumentation/log4j/appender/v2_17/internal/LogEventMapperTest.java +++ b/instrumentation/log4j/log4j-appender-2.17/library/src/test/java/io/opentelemetry/instrumentation/log4j/appender/v2_17/internal/LogEventMapperTest.java @@ -59,8 +59,7 @@ void testSome() { mapper.captureContextDataAttributes(attributes, contextData); // then - assertThat(attributes.build()) - .containsOnly(attributeEntry("log4j.context_data.key2", "value2")); + assertThat(attributes.build()).containsOnly(attributeEntry("key2", "value2")); } @Test @@ -79,9 +78,7 @@ void testAll() { // then assertThat(attributes.build()) - .containsOnly( - attributeEntry("log4j.context_data.key1", "value1"), - attributeEntry("log4j.context_data.key2", "value2")); + .containsOnly(attributeEntry("key1", "value1"), attributeEntry("key2", "value2")); } @Test diff --git a/instrumentation/log4j/log4j-appender-2.17/library/src/test/resources/log4j2-test.xml b/instrumentation/log4j/log4j-appender-2.17/library/src/test/resources/log4j2.xml similarity index 70% rename from instrumentation/log4j/log4j-appender-2.17/library/src/test/resources/log4j2-test.xml rename to instrumentation/log4j/log4j-appender-2.17/library/src/test/resources/log4j2.xml index 47be48e7ccac..cd4610375e1f 100644 --- a/instrumentation/log4j/log4j-appender-2.17/library/src/test/resources/log4j2-test.xml +++ b/instrumentation/log4j/log4j-appender-2.17/library/src/test/resources/log4j2.xml @@ -1,13 +1,12 @@ - + - + diff --git a/instrumentation/log4j/log4j-context-data/log4j-context-data-2.17/javaagent/README.md b/instrumentation/log4j/log4j-context-data/log4j-context-data-2.17/javaagent/README.md new file mode 100644 index 000000000000..d3609df54a6b --- /dev/null +++ b/instrumentation/log4j/log4j-context-data/log4j-context-data-2.17/javaagent/README.md @@ -0,0 +1,9 @@ +# Settings for the Log4j MDC instrumentation + +| System property | Type | Default | Description | +|-------------------------------------------------------|---------|---------------|--------------------------------------------------------------------| +| `otel.instrumentation.log4j-context-data.add-baggage` | Boolean | `false` | Enable exposing baggage attributes through MDC. | +| `otel.instrumentation.common.mdc.resource-attributes` | String | | Comma separated list of resource attributes to expose through MDC. | +| `otel.instrumentation.common.logging.trace-id` | String | `trace_id` | Customize MDC key name for the trace id. | +| `otel.instrumentation.common.logging.span-id` | String | `span_id` | Customize MDC key name for the span id. | +| `otel.instrumentation.common.logging.trace-flags` | String | `trace_flags` | Customize MDC key name for the trace flags. | diff --git a/instrumentation/log4j/log4j-context-data/log4j-context-data-2.17/javaagent/build.gradle.kts b/instrumentation/log4j/log4j-context-data/log4j-context-data-2.17/javaagent/build.gradle.kts index b8f7e18d8475..ccc092864620 100644 --- a/instrumentation/log4j/log4j-context-data/log4j-context-data-2.17/javaagent/build.gradle.kts +++ b/instrumentation/log4j/log4j-context-data/log4j-context-data-2.17/javaagent/build.gradle.kts @@ -27,8 +27,8 @@ testing { // Regression test for https://github.com/open-telemetry/opentelemetry-java-instrumentation/issues/2403 val testDisableThreadLocals by registering(JvmTestSuite::class) { sources { - groovy { - setSrcDirs(listOf("src/test/groovy")) + java { + setSrcDirs(listOf("src/test/java")) } } dependencies { @@ -40,6 +40,41 @@ testing { testTask.configure { jvmArgs("-Dlog4j2.is.webapp=false") jvmArgs("-Dlog4j2.enable.threadlocals=false") + jvmArgs("-Dotel.instrumentation.common.mdc.resource-attributes=service.name,telemetry.sdk.language") + } + } + } + } + + val testAddBaggage by registering(JvmTestSuite::class) { + dependencies { + implementation(project(":instrumentation:log4j:log4j-context-data:log4j-context-data-common:testing")) + } + + targets { + all { + testTask.configure { + jvmArgs("-Dotel.instrumentation.log4j-context-data.add-baggage=true") + jvmArgs("-Dlog4j2.is.webapp=false") + jvmArgs("-Dlog4j2.enable.threadlocals=true") + } + } + } + } + + val testLoggingKeys by registering(JvmTestSuite::class) { + dependencies { + implementation(project(":instrumentation:log4j:log4j-context-data:log4j-context-data-common:testing")) + } + + targets { + all { + testTask.configure { + jvmArgs("-Dotel.instrumentation.common.logging.trace-id=trace_id_test") + jvmArgs("-Dotel.instrumentation.common.logging.span-id=span_id_test") + jvmArgs("-Dotel.instrumentation.common.logging.trace-flags=trace_flags_test") + jvmArgs("-Dlog4j2.is.webapp=false") + jvmArgs("-Dlog4j2.enable.threadlocals=true") } } } @@ -53,6 +88,7 @@ tasks { test { jvmArgs("-Dlog4j2.is.webapp=false") jvmArgs("-Dlog4j2.enable.threadlocals=true") + jvmArgs("-Dotel.instrumentation.common.mdc.resource-attributes=service.name,telemetry.sdk.language") } named("check") { diff --git a/instrumentation/log4j/log4j-context-data/log4j-context-data-2.17/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/log4j/contextdata/v2_17/Log4j2InstrumentationModule.java b/instrumentation/log4j/log4j-context-data/log4j-context-data-2.17/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/log4j/contextdata/v2_17/Log4j2InstrumentationModule.java index 0689e27fc2a1..238122e7fd7c 100644 --- a/instrumentation/log4j/log4j-context-data/log4j-context-data-2.17/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/log4j/contextdata/v2_17/Log4j2InstrumentationModule.java +++ b/instrumentation/log4j/log4j-context-data/log4j-context-data-2.17/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/log4j/contextdata/v2_17/Log4j2InstrumentationModule.java @@ -14,12 +14,16 @@ import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.extension.instrumentation.internal.ExperimentalInstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.internal.injection.ClassInjector; +import io.opentelemetry.javaagent.extension.instrumentation.internal.injection.InjectionMode; import java.util.List; import net.bytebuddy.description.type.TypeDescription; import net.bytebuddy.matcher.ElementMatcher; @AutoService(InstrumentationModule.class) -public class Log4j2InstrumentationModule extends InstrumentationModule { +public class Log4j2InstrumentationModule extends InstrumentationModule + implements ExperimentalInstrumentationModule { public Log4j2InstrumentationModule() { super("log4j-context-data", "log4j-context-data-2.17"); } @@ -30,6 +34,14 @@ public void registerHelperResources(HelperResourceBuilder helperResourceBuilder) "META-INF/services/org.apache.logging.log4j.core.util.ContextDataProvider"); } + @Override + public void injectClasses(ClassInjector injector) { + injector + .proxyBuilder( + "io.opentelemetry.instrumentation.log4j.contextdata.v2_17.OpenTelemetryContextDataProvider") + .inject(InjectionMode.CLASS_ONLY); + } + @Override public ElementMatcher.Junction classLoaderMatcher() { return hasClassesNamed( diff --git a/instrumentation/log4j/log4j-context-data/log4j-context-data-2.17/javaagent/src/test/groovy/AutoLog4j2Test.groovy b/instrumentation/log4j/log4j-context-data/log4j-context-data-2.17/javaagent/src/test/groovy/AutoLog4j2Test.groovy deleted file mode 100644 index 260ff5915f07..000000000000 --- a/instrumentation/log4j/log4j-context-data/log4j-context-data-2.17/javaagent/src/test/groovy/AutoLog4j2Test.groovy +++ /dev/null @@ -1,9 +0,0 @@ -/* - * 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/instrumentation/log4j/log4j-context-data/log4j-context-data-2.17/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/log4j/contextdata/v2_17/AutoLog4j2Test.java b/instrumentation/log4j/log4j-context-data/log4j-context-data-2.17/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/log4j/contextdata/v2_17/AutoLog4j2Test.java new file mode 100644 index 000000000000..21bd52f872e6 --- /dev/null +++ b/instrumentation/log4j/log4j-context-data/log4j-context-data-2.17/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/log4j/contextdata/v2_17/AutoLog4j2Test.java @@ -0,0 +1,45 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.log4j.contextdata.v2_17; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.instrumentation.log4j.contextdata.ListAppender; +import io.opentelemetry.instrumentation.log4j.contextdata.Log4j2Test; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import java.util.List; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +class AutoLog4j2Test extends Log4j2Test { + @RegisterExtension + static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + @Override + public InstrumentationExtension getInstrumentationExtension() { + return testing; + } + + @Test + void testResourceAttributes() { + Logger logger = LogManager.getLogger("TestLogger"); + + logger.info("log message 1"); + + List events = ListAppender.get().getEvents(); + + assertThat(events.size()).isEqualTo(1); + assertThat(events.get(0).getMessage()).isEqualTo("log message 1"); + assertThat(events.get(0).getContextData().get("trace_id")).isNull(); + assertThat(events.get(0).getContextData().get("span_id")).isNull(); + assertThat(events.get(0).getContextData().get("service.name")) + .isEqualTo("unknown_service:java"); + assertThat(events.get(0).getContextData().get("telemetry.sdk.language")).isEqualTo("java"); + } +} diff --git a/instrumentation/log4j/log4j-context-data/log4j-context-data-2.17/javaagent/src/testAddBaggage/java/io/opentelemetry/javaagent/instrumentation/log4j/contextdata/v2_17/AutoLog4jBaggageTest.java b/instrumentation/log4j/log4j-context-data/log4j-context-data-2.17/javaagent/src/testAddBaggage/java/io/opentelemetry/javaagent/instrumentation/log4j/contextdata/v2_17/AutoLog4jBaggageTest.java new file mode 100644 index 000000000000..8c96fe89ab47 --- /dev/null +++ b/instrumentation/log4j/log4j-context-data/log4j-context-data-2.17/javaagent/src/testAddBaggage/java/io/opentelemetry/javaagent/instrumentation/log4j/contextdata/v2_17/AutoLog4jBaggageTest.java @@ -0,0 +1,21 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.log4j.contextdata.v2_17; + +import io.opentelemetry.instrumentation.log4j.contextdata.Log4j2BaggageTest; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import org.junit.jupiter.api.extension.RegisterExtension; + +class AutoLog4jBaggageTest extends Log4j2BaggageTest { + @RegisterExtension + static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + @Override + public InstrumentationExtension getInstrumentationExtension() { + return testing; + } +} diff --git a/instrumentation/log4j/log4j-context-data/log4j-context-data-2.17/javaagent/src/testLoggingKeys/java/io/opentelemetry/javaagent/instrumentation/log4j/contextdata/v2_17/AutoLog4jLoggingKeysTest.java b/instrumentation/log4j/log4j-context-data/log4j-context-data-2.17/javaagent/src/testLoggingKeys/java/io/opentelemetry/javaagent/instrumentation/log4j/contextdata/v2_17/AutoLog4jLoggingKeysTest.java new file mode 100644 index 000000000000..c46391c8f387 --- /dev/null +++ b/instrumentation/log4j/log4j-context-data/log4j-context-data-2.17/javaagent/src/testLoggingKeys/java/io/opentelemetry/javaagent/instrumentation/log4j/contextdata/v2_17/AutoLog4jLoggingKeysTest.java @@ -0,0 +1,21 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.log4j.contextdata.v2_17; + +import io.opentelemetry.instrumentation.log4j.contextdata.Log4j2LoggingKeysTest; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import org.junit.jupiter.api.extension.RegisterExtension; + +class AutoLog4jLoggingKeysTest extends Log4j2LoggingKeysTest { + @RegisterExtension + static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + @Override + public InstrumentationExtension getInstrumentationExtension() { + return testing; + } +} diff --git a/instrumentation/log4j/log4j-context-data/log4j-context-data-2.17/library-autoconfigure/README.md b/instrumentation/log4j/log4j-context-data/log4j-context-data-2.17/library-autoconfigure/README.md index 5d4c761a3106..903427f4e920 100644 --- a/instrumentation/log4j/log4j-context-data/log4j-context-data-2.17/library-autoconfigure/README.md +++ b/instrumentation/log4j/log4j-context-data/log4j-context-data-2.17/library-autoconfigure/README.md @@ -42,6 +42,20 @@ will be added to the context when a log statement is made when a span is active: - `span_id` - `trace_flags` +These keys can be customized using the following system properties or environment variables: + +| System property | Environment variable | +|-------------------------------------------------------|---------------------------------------------------| +| `otel.instrumentation.common.logging.trace-id` | `OTEL_INSTRUMENTATION_COMMON_LOGGING_TRACE_ID` | +| `otel.instrumentation.common.logging.span-id` | `OTEL_INSTRUMENTATION_COMMON_LOGGING_SPAN_ID` | +| `otel.instrumentation.common.logging.trace-flags` | `OTEL_INSTRUMENTATION_COMMON_LOGGING_TRACE_FLAGS` | + +If the `otel.instrumentation.log4j-context-data.add-baggage` system property (or the +`OTEL_INSTRUMENTATION_LOG4J_CONTEXT_DATA_ADD_BAGGAGE` environment variable) is set to `true`, +key/value pairs in [baggage](https://opentelemetry.io/docs/concepts/signals/baggage/) will also be added to the context data. + +- `baggage.` + You can use these keys when defining an appender in your `log4j.xml` configuration, for example: ```xml diff --git a/instrumentation/log4j/log4j-context-data/log4j-context-data-2.17/library-autoconfigure/build.gradle.kts b/instrumentation/log4j/log4j-context-data/log4j-context-data-2.17/library-autoconfigure/build.gradle.kts index 04f5be021045..b0f3d1a23c67 100644 --- a/instrumentation/log4j/log4j-context-data/log4j-context-data-2.17/library-autoconfigure/build.gradle.kts +++ b/instrumentation/log4j/log4j-context-data/log4j-context-data-2.17/library-autoconfigure/build.gradle.kts @@ -5,7 +5,38 @@ plugins { base.archivesName.set("${base.archivesName.get()}-autoconfigure") dependencies { + compileOnly(project(":javaagent-extension-api")) library("org.apache.logging.log4j:log4j-core:2.17.0") testImplementation(project(":instrumentation:log4j:log4j-context-data:log4j-context-data-common:testing")) } + +tasks { + test { + filter { + excludeTestsMatching("LibraryLog4j2BaggageTest") + excludeTestsMatching("LibraryLog4j2LoggingKeysTest") + } + } + + val testAddBaggage by registering(Test::class) { + filter { + includeTestsMatching("LibraryLog4j2BaggageTest") + } + jvmArgs("-Dotel.instrumentation.log4j-context-data.add-baggage=true") + } + + val testLoggingKeys by registering(Test::class) { + filter { + includeTestsMatching("LibraryLog4j2LoggingKeysTest") + } + jvmArgs("-Dotel.instrumentation.common.logging.trace-id=trace_id_test") + jvmArgs("-Dotel.instrumentation.common.logging.span-id=span_id_test") + jvmArgs("-Dotel.instrumentation.common.logging.trace-flags=trace_flags_test") + } + + named("check") { + dependsOn(testAddBaggage) + dependsOn(testLoggingKeys) + } +} diff --git a/instrumentation/log4j/log4j-context-data/log4j-context-data-2.17/library-autoconfigure/src/main/java/io/opentelemetry/instrumentation/log4j/contextdata/v2_17/OpenTelemetryContextDataProvider.java b/instrumentation/log4j/log4j-context-data/log4j-context-data-2.17/library-autoconfigure/src/main/java/io/opentelemetry/instrumentation/log4j/contextdata/v2_17/OpenTelemetryContextDataProvider.java index 8aa905c02937..368d56f05735 100644 --- a/instrumentation/log4j/log4j-context-data/log4j-context-data-2.17/library-autoconfigure/src/main/java/io/opentelemetry/instrumentation/log4j/contextdata/v2_17/OpenTelemetryContextDataProvider.java +++ b/instrumentation/log4j/log4j-context-data/log4j-context-data-2.17/library-autoconfigure/src/main/java/io/opentelemetry/instrumentation/log4j/contextdata/v2_17/OpenTelemetryContextDataProvider.java @@ -5,12 +5,14 @@ package io.opentelemetry.instrumentation.log4j.contextdata.v2_17; -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.baggage.Baggage; +import io.opentelemetry.api.baggage.BaggageEntry; import io.opentelemetry.api.trace.Span; import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.incubator.log.LoggingContextConstants; +import io.opentelemetry.instrumentation.api.internal.ConfigPropertiesUtil; +import io.opentelemetry.javaagent.bootstrap.internal.ConfiguredResourceAttributesHolder; import java.util.Collections; import java.util.HashMap; import java.util.Map; @@ -21,6 +23,44 @@ * #supplyContextData()} is called when a log entry is created. */ public class OpenTelemetryContextDataProvider implements ContextDataProvider { + private static final boolean BAGGAGE_ENABLED = + ConfigPropertiesUtil.getBoolean("otel.instrumentation.log4j-context-data.add-baggage", false); + private static final String TRACE_ID_KEY = + ConfigPropertiesUtil.getString( + "otel.instrumentation.common.logging.trace-id", LoggingContextConstants.TRACE_ID); + private static final String SPAN_ID_KEY = + ConfigPropertiesUtil.getString( + "otel.instrumentation.common.logging.span-id", LoggingContextConstants.SPAN_ID); + private static final String TRACE_FLAGS_KEY = + ConfigPropertiesUtil.getString( + "otel.instrumentation.common.logging.trace-flags", LoggingContextConstants.TRACE_FLAGS); + private static final boolean configuredResourceAttributeAccessible = + isConfiguredResourceAttributeAccessible(); + private static final Map staticContextData = getStaticContextData(); + + private static Map getStaticContextData() { + if (configuredResourceAttributeAccessible) { + return ConfiguredResourceAttributesHolder.getResourceAttributes(); + } + return Collections.emptyMap(); + } + + /** + * Checks whether {@link ConfiguredResourceAttributesHolder} is available in classpath. The result + * is true if {@link ConfiguredResourceAttributesHolder} can be loaded, false otherwise. + * + * @return A boolean + */ + private static boolean isConfiguredResourceAttributeAccessible() { + try { + Class.forName( + "io.opentelemetry.javaagent.bootstrap.internal.ConfiguredResourceAttributesHolder"); + return true; + + } catch (ClassNotFoundException ok) { + return false; + } + } /** * Returns context from the current span when available. @@ -30,16 +70,28 @@ public class OpenTelemetryContextDataProvider implements ContextDataProvider { */ @Override public Map supplyContextData() { - Span currentSpan = Span.current(); + Context context = Context.current(); + Span currentSpan = Span.fromContext(context); if (!currentSpan.getSpanContext().isValid()) { - return Collections.emptyMap(); + return staticContextData; } Map contextData = new HashMap<>(); + contextData.putAll(staticContextData); + SpanContext spanContext = currentSpan.getSpanContext(); - contextData.put(TRACE_ID, spanContext.getTraceId()); - contextData.put(SPAN_ID, spanContext.getSpanId()); - contextData.put(TRACE_FLAGS, spanContext.getTraceFlags().asHex()); + contextData.put(TRACE_ID_KEY, spanContext.getTraceId()); + contextData.put(SPAN_ID_KEY, spanContext.getSpanId()); + contextData.put(TRACE_FLAGS_KEY, spanContext.getTraceFlags().asHex()); + + if (BAGGAGE_ENABLED) { + Baggage baggage = Baggage.fromContext(context); + for (Map.Entry entry : baggage.asMap().entrySet()) { + // prefix all baggage values to avoid clashes with existing context + contextData.put("baggage." + entry.getKey(), entry.getValue().getValue()); + } + } + return contextData; } } diff --git a/instrumentation/log4j/log4j-context-data/log4j-context-data-2.17/library-autoconfigure/src/test/groovy/LibraryLog4j2Test.groovy b/instrumentation/log4j/log4j-context-data/log4j-context-data-2.17/library-autoconfigure/src/test/groovy/LibraryLog4j2Test.groovy deleted file mode 100644 index 342e49f0d3d3..000000000000 --- a/instrumentation/log4j/log4j-context-data/log4j-context-data-2.17/library-autoconfigure/src/test/groovy/LibraryLog4j2Test.groovy +++ /dev/null @@ -1,9 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -import io.opentelemetry.instrumentation.test.LibraryTestTrait - -class LibraryLog4j2Test extends Log4j2Test implements LibraryTestTrait { -} diff --git a/instrumentation/log4j/log4j-context-data/log4j-context-data-2.17/library-autoconfigure/src/test/java/io/opentelemetry/instrumentation/log4j/contextdata/v2_17/LibraryLog4j2BaggageTest.java b/instrumentation/log4j/log4j-context-data/log4j-context-data-2.17/library-autoconfigure/src/test/java/io/opentelemetry/instrumentation/log4j/contextdata/v2_17/LibraryLog4j2BaggageTest.java new file mode 100644 index 000000000000..f99493282860 --- /dev/null +++ b/instrumentation/log4j/log4j-context-data/log4j-context-data-2.17/library-autoconfigure/src/test/java/io/opentelemetry/instrumentation/log4j/contextdata/v2_17/LibraryLog4j2BaggageTest.java @@ -0,0 +1,21 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.log4j.contextdata.v2_17; + +import io.opentelemetry.instrumentation.log4j.contextdata.Log4j2BaggageTest; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.LibraryInstrumentationExtension; +import org.junit.jupiter.api.extension.RegisterExtension; + +class LibraryLog4j2BaggageTest extends Log4j2BaggageTest { + @RegisterExtension + static final InstrumentationExtension testing = LibraryInstrumentationExtension.create(); + + @Override + public InstrumentationExtension getInstrumentationExtension() { + return testing; + } +} diff --git a/instrumentation/log4j/log4j-context-data/log4j-context-data-2.17/library-autoconfigure/src/test/java/io/opentelemetry/instrumentation/log4j/contextdata/v2_17/LibraryLog4j2LoggingKeysTest.java b/instrumentation/log4j/log4j-context-data/log4j-context-data-2.17/library-autoconfigure/src/test/java/io/opentelemetry/instrumentation/log4j/contextdata/v2_17/LibraryLog4j2LoggingKeysTest.java new file mode 100644 index 000000000000..430f536a6efc --- /dev/null +++ b/instrumentation/log4j/log4j-context-data/log4j-context-data-2.17/library-autoconfigure/src/test/java/io/opentelemetry/instrumentation/log4j/contextdata/v2_17/LibraryLog4j2LoggingKeysTest.java @@ -0,0 +1,21 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.log4j.contextdata.v2_17; + +import io.opentelemetry.instrumentation.log4j.contextdata.Log4j2LoggingKeysTest; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.LibraryInstrumentationExtension; +import org.junit.jupiter.api.extension.RegisterExtension; + +class LibraryLog4j2LoggingKeysTest extends Log4j2LoggingKeysTest { + @RegisterExtension + static final InstrumentationExtension testing = LibraryInstrumentationExtension.create(); + + @Override + public InstrumentationExtension getInstrumentationExtension() { + return testing; + } +} diff --git a/instrumentation/log4j/log4j-context-data/log4j-context-data-2.17/library-autoconfigure/src/test/java/io/opentelemetry/instrumentation/log4j/contextdata/v2_17/LibraryLog4j2Test.java b/instrumentation/log4j/log4j-context-data/log4j-context-data-2.17/library-autoconfigure/src/test/java/io/opentelemetry/instrumentation/log4j/contextdata/v2_17/LibraryLog4j2Test.java new file mode 100644 index 000000000000..eaa08b65b34c --- /dev/null +++ b/instrumentation/log4j/log4j-context-data/log4j-context-data-2.17/library-autoconfigure/src/test/java/io/opentelemetry/instrumentation/log4j/contextdata/v2_17/LibraryLog4j2Test.java @@ -0,0 +1,21 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.log4j.contextdata.v2_17; + +import io.opentelemetry.instrumentation.log4j.contextdata.Log4j2Test; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.LibraryInstrumentationExtension; +import org.junit.jupiter.api.extension.RegisterExtension; + +class LibraryLog4j2Test extends Log4j2Test { + @RegisterExtension + static final InstrumentationExtension testing = LibraryInstrumentationExtension.create(); + + @Override + public InstrumentationExtension getInstrumentationExtension() { + return testing; + } +} diff --git a/instrumentation/log4j/log4j-context-data/log4j-context-data-2.7/javaagent/build.gradle.kts b/instrumentation/log4j/log4j-context-data/log4j-context-data-2.7/javaagent/build.gradle.kts index 1bf995beec1d..678322b8df6b 100644 --- a/instrumentation/log4j/log4j-context-data/log4j-context-data-2.7/javaagent/build.gradle.kts +++ b/instrumentation/log4j/log4j-context-data/log4j-context-data-2.7/javaagent/build.gradle.kts @@ -20,3 +20,34 @@ dependencies { latestDepTestLibrary("org.apache.logging.log4j:log4j-core:2.16.+") // see log4j-context-data-2.17 module } + +tasks { + test { + filter { + excludeTestsMatching("Log4j27BaggageTest") + excludeTestsMatching("Log4j27LoggingKeysTest") + } + jvmArgs("-Dotel.instrumentation.common.mdc.resource-attributes=service.name,telemetry.sdk.language") + } + + val testAddBaggage by registering(Test::class) { + filter { + includeTestsMatching("Log4j27BaggageTest") + } + jvmArgs("-Dotel.instrumentation.log4j-context-data.add-baggage=true") + } + + val testLoggingKeys by registering(Test::class) { + filter { + includeTestsMatching("Log4j27LoggingKeysTest") + } + jvmArgs("-Dotel.instrumentation.common.logging.trace-id=trace_id_test") + jvmArgs("-Dotel.instrumentation.common.logging.span-id=span_id_test") + jvmArgs("-Dotel.instrumentation.common.logging.trace-flags=trace_flags_test") + } + + named("check") { + dependsOn(testAddBaggage) + dependsOn(testLoggingKeys) + } +} diff --git a/instrumentation/log4j/log4j-context-data/log4j-context-data-2.7/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/log4j/contextdata/v2_7/SpanDecoratingContextDataInjector.java b/instrumentation/log4j/log4j-context-data/log4j-context-data-2.7/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/log4j/contextdata/v2_7/SpanDecoratingContextDataInjector.java index d597faf36e32..8cd437da9d38 100644 --- a/instrumentation/log4j/log4j-context-data/log4j-context-data-2.7/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/log4j/contextdata/v2_7/SpanDecoratingContextDataInjector.java +++ b/instrumentation/log4j/log4j-context-data/log4j-context-data-2.7/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/log4j/contextdata/v2_7/SpanDecoratingContextDataInjector.java @@ -5,13 +5,16 @@ package io.opentelemetry.javaagent.instrumentation.log4j.contextdata.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.baggage.Baggage; +import io.opentelemetry.api.baggage.BaggageEntry; +import io.opentelemetry.api.trace.Span; import io.opentelemetry.api.trace.SpanContext; -import io.opentelemetry.javaagent.bootstrap.Java8BytecodeBridge; +import io.opentelemetry.context.Context; +import io.opentelemetry.javaagent.bootstrap.internal.AgentCommonConfig; +import io.opentelemetry.javaagent.bootstrap.internal.AgentInstrumentationConfig; +import io.opentelemetry.javaagent.bootstrap.internal.ConfiguredResourceAttributesHolder; import java.util.List; +import java.util.Map; import org.apache.logging.log4j.core.ContextDataInjector; import org.apache.logging.log4j.core.config.Property; import org.apache.logging.log4j.util.ReadOnlyStringMap; @@ -19,6 +22,15 @@ import org.apache.logging.log4j.util.StringMap; public final class SpanDecoratingContextDataInjector implements ContextDataInjector { + private static final boolean BAGGAGE_ENABLED = + AgentInstrumentationConfig.get() + .getBoolean("otel.instrumentation.log4j-context-data.add-baggage", false); + private static final String TRACE_ID_KEY = AgentCommonConfig.get().getTraceIdKey(); + private static final String SPAN_ID_KEY = AgentCommonConfig.get().getSpanIdKey(); + private static final String TRACE_FLAGS_KEY = AgentCommonConfig.get().getTraceFlagsKey(); + + private static final StringMap staticContextData = getStaticContextData(); + private final ContextDataInjector delegate; public SpanDecoratingContextDataInjector(ContextDataInjector delegate) { @@ -29,20 +41,30 @@ public SpanDecoratingContextDataInjector(ContextDataInjector delegate) { public StringMap injectContextData(List list, StringMap stringMap) { StringMap contextData = delegate.injectContextData(list, stringMap); - if (contextData.containsKey(TRACE_ID)) { + if (contextData.containsKey(TRACE_ID_KEY)) { // Assume already instrumented event if traceId is present. - return contextData; + return staticContextData.isEmpty() ? contextData : newContextData(contextData); } - SpanContext currentContext = Java8BytecodeBridge.currentSpan().getSpanContext(); + Context context = Context.current(); + Span span = Span.fromContext(context); + SpanContext currentContext = span.getSpanContext(); if (!currentContext.isValid()) { - return contextData; + return staticContextData.isEmpty() ? contextData : newContextData(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()); + StringMap newContextData = newContextData(contextData); + newContextData.putValue(TRACE_ID_KEY, currentContext.getTraceId()); + newContextData.putValue(SPAN_ID_KEY, currentContext.getSpanId()); + newContextData.putValue(TRACE_FLAGS_KEY, currentContext.getTraceFlags().asHex()); + + if (BAGGAGE_ENABLED) { + Baggage baggage = Baggage.fromContext(context); + for (Map.Entry entry : baggage.asMap().entrySet()) { + // prefix all baggage values to avoid clashes with existing context + newContextData.putValue("baggage." + entry.getKey(), entry.getValue().getValue()); + } + } return newContextData; } @@ -50,4 +72,19 @@ public StringMap injectContextData(List list, StringMap stringMap) { public ReadOnlyStringMap rawContextData() { return delegate.rawContextData(); } + + private static StringMap newContextData(StringMap contextData) { + StringMap newContextData = new SortedArrayStringMap(contextData); + newContextData.putAll(staticContextData); + return newContextData; + } + + private static StringMap getStaticContextData() { + StringMap map = new SortedArrayStringMap(); + for (Map.Entry entry : + ConfiguredResourceAttributesHolder.getResourceAttributes().entrySet()) { + map.putValue(entry.getKey(), entry.getValue()); + } + return map; + } } diff --git a/instrumentation/log4j/log4j-context-data/log4j-context-data-2.7/javaagent/src/test/groovy/Log4j27Test.groovy b/instrumentation/log4j/log4j-context-data/log4j-context-data-2.7/javaagent/src/test/groovy/Log4j27Test.groovy deleted file mode 100644 index 24e5b98f4cfe..000000000000 --- a/instrumentation/log4j/log4j-context-data/log4j-context-data-2.7/javaagent/src/test/groovy/Log4j27Test.groovy +++ /dev/null @@ -1,9 +0,0 @@ -/* - * 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/instrumentation/log4j/log4j-context-data/log4j-context-data-2.7/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/log4j/contextdata/v2_7/Log4j27BaggageTest.java b/instrumentation/log4j/log4j-context-data/log4j-context-data-2.7/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/log4j/contextdata/v2_7/Log4j27BaggageTest.java new file mode 100644 index 000000000000..ce06d4d9dd31 --- /dev/null +++ b/instrumentation/log4j/log4j-context-data/log4j-context-data-2.7/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/log4j/contextdata/v2_7/Log4j27BaggageTest.java @@ -0,0 +1,21 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.log4j.contextdata.v2_7; + +import io.opentelemetry.instrumentation.log4j.contextdata.Log4j2BaggageTest; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import org.junit.jupiter.api.extension.RegisterExtension; + +class Log4j27BaggageTest extends Log4j2BaggageTest { + @RegisterExtension + static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + @Override + public InstrumentationExtension getInstrumentationExtension() { + return testing; + } +} diff --git a/instrumentation/log4j/log4j-context-data/log4j-context-data-2.7/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/log4j/contextdata/v2_7/Log4j27LoggingKeysTest.java b/instrumentation/log4j/log4j-context-data/log4j-context-data-2.7/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/log4j/contextdata/v2_7/Log4j27LoggingKeysTest.java new file mode 100644 index 000000000000..03be86d29bdd --- /dev/null +++ b/instrumentation/log4j/log4j-context-data/log4j-context-data-2.7/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/log4j/contextdata/v2_7/Log4j27LoggingKeysTest.java @@ -0,0 +1,21 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.log4j.contextdata.v2_7; + +import io.opentelemetry.instrumentation.log4j.contextdata.Log4j2LoggingKeysTest; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import org.junit.jupiter.api.extension.RegisterExtension; + +class Log4j27LoggingKeysTest extends Log4j2LoggingKeysTest { + @RegisterExtension + static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + @Override + public InstrumentationExtension getInstrumentationExtension() { + return testing; + } +} diff --git a/instrumentation/log4j/log4j-context-data/log4j-context-data-2.7/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/log4j/contextdata/v2_7/Log4j27Test.java b/instrumentation/log4j/log4j-context-data/log4j-context-data-2.7/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/log4j/contextdata/v2_7/Log4j27Test.java new file mode 100644 index 000000000000..d3cbd2c8ca29 --- /dev/null +++ b/instrumentation/log4j/log4j-context-data/log4j-context-data-2.7/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/log4j/contextdata/v2_7/Log4j27Test.java @@ -0,0 +1,45 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.log4j.contextdata.v2_7; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.instrumentation.log4j.contextdata.ListAppender; +import io.opentelemetry.instrumentation.log4j.contextdata.Log4j2Test; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import java.util.List; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +class Log4j27Test extends Log4j2Test { + @RegisterExtension + static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + @Override + public InstrumentationExtension getInstrumentationExtension() { + return testing; + } + + @Test + void testResourceAttributes() { + Logger logger = LogManager.getLogger("TestLogger"); + + logger.info("log message 1"); + + List events = ListAppender.get().getEvents(); + + assertThat(events.size()).isEqualTo(1); + assertThat(events.get(0).getMessage()).isEqualTo("log message 1"); + assertThat(events.get(0).getContextData().get("trace_id")).isNull(); + assertThat(events.get(0).getContextData().get("span_id")).isNull(); + assertThat(events.get(0).getContextData().get("service.name")) + .isEqualTo("unknown_service:java"); + assertThat(events.get(0).getContextData().get("telemetry.sdk.language")).isEqualTo("java"); + } +} diff --git a/instrumentation/log4j/log4j-context-data/log4j-context-data-common/testing/build.gradle.kts b/instrumentation/log4j/log4j-context-data/log4j-context-data-common/testing/build.gradle.kts index 89cd7732cd59..898c7d73e9a0 100644 --- a/instrumentation/log4j/log4j-context-data/log4j-context-data-common/testing/build.gradle.kts +++ b/instrumentation/log4j/log4j-context-data/log4j-context-data-common/testing/build.gradle.kts @@ -9,9 +9,7 @@ dependencies { implementation("com.google.guava:guava") - implementation("org.apache.groovy:groovy") implementation("io.opentelemetry:opentelemetry-api") - implementation("org.spockframework:spock-core") annotationProcessor("org.apache.logging.log4j:log4j-core:2.7") } diff --git a/instrumentation/log4j/log4j-context-data/log4j-context-data-common/testing/src/main/groovy/Log4j2Test.groovy b/instrumentation/log4j/log4j-context-data/log4j-context-data-common/testing/src/main/groovy/Log4j2Test.groovy deleted file mode 100644 index 091e38914654..000000000000 --- a/instrumentation/log4j/log4j-context-data/log4j-context-data-common/testing/src/main/groovy/Log4j2Test.groovy +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -import io.opentelemetry.api.trace.Span -import io.opentelemetry.instrumentation.log4j.contextdata.ListAppender -import io.opentelemetry.instrumentation.test.InstrumentationSpecification -import org.apache.logging.log4j.LogManager - -abstract class Log4j2Test extends InstrumentationSpecification { - def setup() { - 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 = runWithSpan("test") { - logger.info("log message 1") - Span.current() - } - - logger.info("log message 2") - - Span span2 = runWithSpan("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/instrumentation/log4j/log4j-context-data/log4j-context-data-common/testing/src/main/java/io/opentelemetry/instrumentation/log4j/contextdata/Log4j2BaggageTest.java b/instrumentation/log4j/log4j-context-data/log4j-context-data-common/testing/src/main/java/io/opentelemetry/instrumentation/log4j/contextdata/Log4j2BaggageTest.java new file mode 100644 index 000000000000..b9f9217894c3 --- /dev/null +++ b/instrumentation/log4j/log4j-context-data/log4j-context-data-common/testing/src/main/java/io/opentelemetry/instrumentation/log4j/contextdata/Log4j2BaggageTest.java @@ -0,0 +1,13 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.log4j.contextdata; + +public abstract class Log4j2BaggageTest extends Log4j2Test { + @Override + boolean expectBaggage() { + return true; + } +} diff --git a/instrumentation/log4j/log4j-context-data/log4j-context-data-common/testing/src/main/java/io/opentelemetry/instrumentation/log4j/contextdata/Log4j2LoggingKeysTest.java b/instrumentation/log4j/log4j-context-data/log4j-context-data-common/testing/src/main/java/io/opentelemetry/instrumentation/log4j/contextdata/Log4j2LoggingKeysTest.java new file mode 100644 index 000000000000..4cde948068e5 --- /dev/null +++ b/instrumentation/log4j/log4j-context-data/log4j-context-data-common/testing/src/main/java/io/opentelemetry/instrumentation/log4j/contextdata/Log4j2LoggingKeysTest.java @@ -0,0 +1,13 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.log4j.contextdata; + +public abstract class Log4j2LoggingKeysTest extends Log4j2Test { + @Override + boolean expectLoggingKeys() { + return true; + } +} diff --git a/instrumentation/log4j/log4j-context-data/log4j-context-data-common/testing/src/main/java/io/opentelemetry/instrumentation/log4j/contextdata/Log4j2Test.java b/instrumentation/log4j/log4j-context-data/log4j-context-data-common/testing/src/main/java/io/opentelemetry/instrumentation/log4j/contextdata/Log4j2Test.java new file mode 100644 index 000000000000..5f24d0892d53 --- /dev/null +++ b/instrumentation/log4j/log4j-context-data/log4j-context-data-common/testing/src/main/java/io/opentelemetry/instrumentation/log4j/contextdata/Log4j2Test.java @@ -0,0 +1,135 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.log4j.contextdata; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.baggage.Baggage; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Scope; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +public abstract class Log4j2Test { + public abstract InstrumentationExtension getInstrumentationExtension(); + + @BeforeEach + void setUp() { + ListAppender.get().clearEvents(); + } + + boolean expectBaggage() { + return false; + } + + boolean expectLoggingKeys() { + return false; + } + + private String getLoggingKey(String key) { + return expectLoggingKeys() ? key + "_test" : key; + } + + @Test + void testNoIdsWhenNoSpan() { + Logger logger = LogManager.getLogger("TestLogger"); + + logger.info("log message 1"); + logger.info("log message 2"); + + List events = ListAppender.get().getEvents(); + + assertThat(events.size()).isEqualTo(2); + + assertThat(events.get(0).getMessage()).isEqualTo("log message 1"); + assertThat(events.get(0).getContextData().get(getLoggingKey("trace_id"))).isNull(); + assertThat(events.get(0).getContextData().get(getLoggingKey("span_id"))).isNull(); + assertThat(events.get(0).getContextData().get(getLoggingKey("trace_flags"))).isNull(); + + assertThat(events.get(1).getMessage()).isEqualTo("log message 2"); + assertThat(events.get(1).getContextData().get(getLoggingKey("trace_id"))).isNull(); + assertThat(events.get(1).getContextData().get(getLoggingKey("span_id"))).isNull(); + assertThat(events.get(1).getContextData().get(getLoggingKey("trace_flags"))).isNull(); + } + + @Test + void testIdsWhenSpan() { + Logger logger = LogManager.getLogger("TestLogger"); + + Baggage baggage = Baggage.empty().toBuilder().put("baggage_key", "baggage_value").build(); + AtomicReference spanParent = new AtomicReference<>(); + AtomicReference spanChild = new AtomicReference<>(); + try (Scope unusedScope = baggage.makeCurrent()) { + getInstrumentationExtension() + .runWithSpan( + "test", + () -> { + spanParent.set(Span.current()); + logger.info("log span parent"); + + getInstrumentationExtension() + .runWithSpan( + "test-child", + () -> { + logger.info("log span child"); + spanChild.set(Span.current()); + }); + }); + } + + logger.info("log message 2"); + + Span span2 = + getInstrumentationExtension() + .runWithSpan( + "test 2", + () -> { + logger.info("log message 3"); + return Span.current(); + }); + + List events = ListAppender.get().getEvents(); + + assertThat(events.size()).isEqualTo(4); + + assertThat(events.get(0).getMessage()).isEqualTo("log span parent"); + assertThat(events.get(0).getContextData().get(getLoggingKey("trace_id"))) + .isEqualTo(spanParent.get().getSpanContext().getTraceId()); + assertThat(events.get(0).getContextData().get(getLoggingKey("span_id"))) + .isEqualTo(spanParent.get().getSpanContext().getSpanId()); + assertThat(events.get(0).getContextData().get(getLoggingKey("trace_flags"))).isEqualTo("01"); + assertThat(events.get(0).getContextData().get("baggage.baggage_key")) + .isEqualTo((expectBaggage() ? "baggage_value" : null)); + + assertThat(events.get(1).getMessage()).isEqualTo("log span child"); + assertThat(events.get(1).getContextData().get(getLoggingKey("trace_id"))) + .isEqualTo(spanChild.get().getSpanContext().getTraceId()); + assertThat(events.get(1).getContextData().get(getLoggingKey("span_id"))) + .isEqualTo(spanChild.get().getSpanContext().getSpanId()); + assertThat(events.get(1).getContextData().get(getLoggingKey("trace_flags"))).isEqualTo("01"); + assertThat(events.get(1).getContextData().get("baggage.baggage_key")) + .isEqualTo((expectBaggage() ? "baggage_value" : null)); + + assertThat(events.get(2).getMessage()).isEqualTo("log message 2"); + assertThat(events.get(2).getContextData().get(getLoggingKey("trace_id"))).isNull(); + assertThat(events.get(2).getContextData().get(getLoggingKey("span_id"))).isNull(); + assertThat(events.get(2).getContextData().get(getLoggingKey("trace_flags"))).isNull(); + assertThat(events.get(2).getContextData().get("baggage.baggage_key")).isNull(); + + assertThat(events.get(3).getMessage()).isEqualTo("log message 3"); + assertThat(events.get(3).getContextData().get(getLoggingKey("trace_id"))) + .isEqualTo(span2.getSpanContext().getTraceId()); + assertThat(events.get(3).getContextData().get(getLoggingKey("span_id"))) + .isEqualTo(span2.getSpanContext().getSpanId()); + assertThat(events.get(3).getContextData().get(getLoggingKey("trace_flags"))).isEqualTo("01"); + assertThat(events.get(3).getContextData().get("baggage.baggage_key")).isNull(); + } +} diff --git a/instrumentation/log4j/log4j-context-data/log4j-context-data-common/testing/src/main/resources/log4j2-test.xml b/instrumentation/log4j/log4j-context-data/log4j-context-data-common/testing/src/main/resources/log4j2-test.xml index 17138ea6e065..fce95a4f693a 100644 --- a/instrumentation/log4j/log4j-context-data/log4j-context-data-common/testing/src/main/resources/log4j2-test.xml +++ b/instrumentation/log4j/log4j-context-data/log4j-context-data-common/testing/src/main/resources/log4j2-test.xml @@ -1,8 +1,8 @@ - + - + diff --git a/instrumentation/log4j/log4j-mdc-1.2/javaagent/README.md b/instrumentation/log4j/log4j-mdc-1.2/javaagent/README.md new file mode 100644 index 000000000000..91cee07fcff6 --- /dev/null +++ b/instrumentation/log4j/log4j-mdc-1.2/javaagent/README.md @@ -0,0 +1,8 @@ +# Settings for the Log4j MDC instrumentation + +| System property | Type | Default | Description | +|-------------------------------------------------------|---------|---------------|--------------------------------------------------------------------| +| `otel.instrumentation.common.mdc.resource-attributes` | String | | Comma separated list of resource attributes to expose through MDC. | +| `otel.instrumentation.common.logging.trace-id` | String | `trace_id` | Customize MDC key name for the trace id. | +| `otel.instrumentation.common.logging.span-id` | String | `span_id` | Customize MDC key name for the span id. | +| `otel.instrumentation.common.logging.trace-flags` | String | `trace_flags` | Customize MDC key name for the trace flags. | diff --git a/instrumentation/log4j/log4j-mdc-1.2/javaagent/build.gradle.kts b/instrumentation/log4j/log4j-mdc-1.2/javaagent/build.gradle.kts index 9ede7f4fc5b1..5054629f77fc 100644 --- a/instrumentation/log4j/log4j-mdc-1.2/javaagent/build.gradle.kts +++ b/instrumentation/log4j/log4j-mdc-1.2/javaagent/build.gradle.kts @@ -30,3 +30,9 @@ configurations { exclude("javax.jms", "jms") } } + +tasks { + test { + jvmArgs("-Dotel.instrumentation.common.mdc.resource-attributes=service.name,telemetry.sdk.language") + } +} diff --git a/instrumentation/log4j/log4j-mdc-1.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/log4j/mdc/v1_2/LoggingEventInstrumentation.java b/instrumentation/log4j/log4j-mdc-1.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/log4j/mdc/v1_2/LoggingEventInstrumentation.java index e8b0c425ed6f..4f2ac37101d5 100644 --- a/instrumentation/log4j/log4j-mdc-1.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/log4j/mdc/v1_2/LoggingEventInstrumentation.java +++ b/instrumentation/log4j/log4j-mdc-1.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/log4j/mdc/v1_2/LoggingEventInstrumentation.java @@ -5,9 +5,6 @@ package io.opentelemetry.javaagent.instrumentation.log4j.mdc.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; @@ -18,6 +15,8 @@ import io.opentelemetry.context.Context; import io.opentelemetry.instrumentation.api.util.VirtualField; import io.opentelemetry.javaagent.bootstrap.Java8BytecodeBridge; +import io.opentelemetry.javaagent.bootstrap.internal.AgentCommonConfig; +import io.opentelemetry.javaagent.bootstrap.internal.ConfiguredResourceAttributesHolder; import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; import net.bytebuddy.asm.Advice; @@ -50,7 +49,9 @@ 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 (AgentCommonConfig.get().getTraceIdKey().equals(key) + || AgentCommonConfig.get().getSpanIdKey().equals(key) + || AgentCommonConfig.get().getTraceFlagsKey().equals(key)) { if (value != null) { // Assume already instrumented event if traceId/spanId/sampled is present. return; @@ -66,19 +67,17 @@ public static void onExit( return; } - 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 + if (AgentCommonConfig.get().getTraceIdKey().equals(key)) { + value = spanContext.getTraceId(); } + if (AgentCommonConfig.get().getSpanIdKey().equals(key)) { + value = spanContext.getSpanId(); + } + if (AgentCommonConfig.get().getTraceFlagsKey().equals(key)) { + value = spanContext.getTraceFlags().asHex(); + } + } else if (value == null) { + value = ConfiguredResourceAttributesHolder.getAttributeValue(key); } } } diff --git a/instrumentation/log4j/log4j-mdc-1.2/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/log4j/mdc/v1_2/Log4j1MdcTest.java b/instrumentation/log4j/log4j-mdc-1.2/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/log4j/mdc/v1_2/Log4j1MdcTest.java index 5a3bcc190628..3661ac8ecbab 100644 --- a/instrumentation/log4j/log4j-mdc-1.2/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/log4j/mdc/v1_2/Log4j1MdcTest.java +++ b/instrumentation/log4j/log4j-mdc-1.2/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/log4j/mdc/v1_2/Log4j1MdcTest.java @@ -90,4 +90,19 @@ void idsWhenSpan() { assertEquals(events.get(2).getMDC("span_id"), span2.getSpanContext().getSpanId()); assertEquals(events.get(2).getMDC("trace_flags"), "01"); } + + @Test + void resourceAttributes() { + logger.info("log message 1"); + + List events = ListAppender.getEvents(); + + assertEquals(1, events.size()); + assertEquals("log message 1", events.get(0).getMessage()); + assertNull(events.get(0).getMDC("trace_id")); + assertNull(events.get(0).getMDC("span_id")); + assertNull(events.get(0).getMDC("trace_flags")); + assertEquals("unknown_service:java", events.get(0).getMDC("service.name")); + assertEquals("java", events.get(0).getMDC("telemetry.sdk.language")); + } } diff --git a/instrumentation/logback/logback-appender-1.0/javaagent/README.md b/instrumentation/logback/logback-appender-1.0/javaagent/README.md index 521677796341..2a962ae4e739 100644 --- a/instrumentation/logback/logback-appender-1.0/javaagent/README.md +++ b/instrumentation/logback/logback-appender-1.0/javaagent/README.md @@ -1,11 +1,13 @@ # Settings for the Logback Appender instrumentation -| System property | Type | Default | Description | -|---|---------|--|------------------------------------------------------| -| `otel.instrumentation.logback-appender.experimental-log-attributes` | Boolean | `false` | Enable the capture of experimental span attributes `thread.name` and `thread.id`. | -| `otel.instrumentation.logback-appender.experimental.capture-code-attributes` | Boolean | `false` | Enable the capture of [source code attributes]. Note that capturing source code attributes at logging sites might add a performance overhead. | -| `otel.instrumentation.logback-appender.experimental.capture-marker-attribute` | Boolean | `false` | Enable the capture of Logback markers as attributes. | -| `otel.instrumentation.logback-appender.experimental.capture-key-value-pair-attributes` | Boolean | `false` | Enable the capture of Logback key value pairs as attributes. | -| `otel.instrumentation.logback-appender.experimental.capture-mdc-attributes` | String | | List of MDC attributes to capture. Use the wildcard character `*` to capture all attributes. | +| System property | Type | Default | Description | +|----------------------------------------------------------------------------------------|---------|---------|-----------------------------------------------------------------------------------------------------------------------------------------------| +| `otel.instrumentation.logback-appender.experimental-log-attributes` | Boolean | `false` | Enable the capture of experimental log attributes `thread.name` and `thread.id`. | +| `otel.instrumentation.logback-appender.experimental.capture-code-attributes` | Boolean | `false` | Enable the capture of [source code attributes]. Note that capturing source code attributes at logging sites might add a performance overhead. | +| `otel.instrumentation.logback-appender.experimental.capture-marker-attribute` | Boolean | `false` | Enable the capture of Logback markers as attributes. | +| `otel.instrumentation.logback-appender.experimental.capture-key-value-pair-attributes` | Boolean | `false` | Enable the capture of Logback key value pairs as attributes. | +| `otel.instrumentation.logback-appender.experimental.capture-logger-context-attributes` | Boolean | `false` | Enable the capture of Logback logger context properties as attributes. | +| `otel.instrumentation.logback-appender.experimental.capture-arguments` | Boolean | `false` | Enable the capture of Logback logger arguments. | +| `otel.instrumentation.logback-appender.experimental.capture-mdc-attributes` | String | | Comma separated list of MDC attributes to capture. Use the wildcard character `*` to capture all attributes. | -[source code attributes]: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/span-general.md#source-code-attributes +[source code attributes]: https://github.com/open-telemetry/semantic-conventions/blob/main/docs/general/attributes.md#source-code-attributes diff --git a/instrumentation/logback/logback-appender-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/logback/appender/v1_0/LogbackInstrumentation.java b/instrumentation/logback/logback-appender-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/logback/appender/v1_0/LogbackInstrumentation.java index cbd366729750..1e1052707a31 100644 --- a/instrumentation/logback/logback-appender-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/logback/appender/v1_0/LogbackInstrumentation.java +++ b/instrumentation/logback/logback-appender-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/logback/appender/v1_0/LogbackInstrumentation.java @@ -51,7 +51,8 @@ public static void methodEnter( // logging framework delegates to another callDepth = CallDepth.forClass(LoggerProvider.class); if (callDepth.getAndIncrement() == 0) { - mapper().emit(GlobalOpenTelemetry.get().getLogsBridge(), event); + mapper() + .emit(GlobalOpenTelemetry.get().getLogsBridge(), event, Thread.currentThread().getId()); } } diff --git a/instrumentation/logback/logback-appender-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/logback/appender/v1_0/LogbackSingletons.java b/instrumentation/logback/logback-appender-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/logback/appender/v1_0/LogbackSingletons.java index 509498142d50..a3d1c6d90688 100644 --- a/instrumentation/logback/logback-appender-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/logback/appender/v1_0/LogbackSingletons.java +++ b/instrumentation/logback/logback-appender-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/logback/appender/v1_0/LogbackSingletons.java @@ -7,8 +7,9 @@ import static java.util.Collections.emptyList; +import io.opentelemetry.instrumentation.api.incubator.config.internal.InstrumentationConfig; import io.opentelemetry.instrumentation.logback.appender.v1_0.internal.LoggingEventMapper; -import io.opentelemetry.javaagent.bootstrap.internal.InstrumentationConfig; +import io.opentelemetry.javaagent.bootstrap.internal.AgentInstrumentationConfig; import java.util.List; public final class LogbackSingletons { @@ -16,7 +17,7 @@ public final class LogbackSingletons { private static final LoggingEventMapper mapper; static { - InstrumentationConfig config = InstrumentationConfig.get(); + InstrumentationConfig config = AgentInstrumentationConfig.get(); boolean captureExperimentalAttributes = config.getBoolean( @@ -31,18 +32,28 @@ public final class LogbackSingletons { config.getBoolean( "otel.instrumentation.logback-appender.experimental.capture-key-value-pair-attributes", false); + boolean captureLoggerContext = + config.getBoolean( + "otel.instrumentation.logback-appender.experimental.capture-logger-context-attributes", + false); + boolean captureArguments = + config.getBoolean( + "otel.instrumentation.logback-appender.experimental.capture-arguments", false); List captureMdcAttributes = config.getList( "otel.instrumentation.logback-appender.experimental.capture-mdc-attributes", emptyList()); mapper = - new LoggingEventMapper( - captureExperimentalAttributes, - captureMdcAttributes, - captureCodeAttributes, - captureMarkerAttribute, - captureKeyValuePairAttributes); + LoggingEventMapper.builder() + .setCaptureExperimentalAttributes(captureExperimentalAttributes) + .setCaptureMdcAttributes(captureMdcAttributes) + .setCaptureCodeAttributes(captureCodeAttributes) + .setCaptureMarkerAttribute(captureMarkerAttribute) + .setCaptureKeyValuePairAttributes(captureKeyValuePairAttributes) + .setCaptureLoggerContext(captureLoggerContext) + .setCaptureArguments(captureArguments) + .build(); } public static LoggingEventMapper mapper() { diff --git a/instrumentation/logback/logback-appender-1.0/javaagent/src/test/java/io/opentelemetry/instrumentation/logback/appender/v1_0/LogbackTest.java b/instrumentation/logback/logback-appender-1.0/javaagent/src/test/java/io/opentelemetry/instrumentation/logback/appender/v1_0/LogbackTest.java index c327677fcd16..985915317f9d 100644 --- a/instrumentation/logback/logback-appender-1.0/javaagent/src/test/java/io/opentelemetry/instrumentation/logback/appender/v1_0/LogbackTest.java +++ b/instrumentation/logback/logback-appender-1.0/javaagent/src/test/java/io/opentelemetry/instrumentation/logback/appender/v1_0/LogbackTest.java @@ -11,12 +11,18 @@ import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.logs.Severity; +import io.opentelemetry.api.trace.SpanContext; import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; import io.opentelemetry.sdk.common.InstrumentationScopeInfo; -import io.opentelemetry.sdk.logs.data.LogRecordData; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.opentelemetry.sdk.testing.assertj.AttributeAssertion; +import io.opentelemetry.semconv.ExceptionAttributes; +import io.opentelemetry.semconv.incubating.CodeIncubatingAttributes; +import io.opentelemetry.semconv.incubating.ThreadIncubatingAttributes; +import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; +import java.util.List; import java.util.stream.Stream; import org.assertj.core.api.AbstractLongAssert; import org.junit.jupiter.api.Test; @@ -133,43 +139,46 @@ private static void test( } if (expectedSeverity != null) { - LogRecordData log = testing.waitForLogRecords(1).get(0); - assertThat(log) - .hasBody("xyz: 123") - .hasInstrumentationScope(InstrumentationScopeInfo.builder(expectedLoggerName).build()) - .hasSeverity(expectedSeverity) - .hasSeverityText(expectedSeverityText); - if (logException) { - assertThat(log) - .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.THREAD_NAME, Thread.currentThread().getName()), - equalTo(SemanticAttributes.THREAD_ID, Thread.currentThread().getId()), - equalTo(SemanticAttributes.CODE_NAMESPACE, LogbackTest.class.getName()), - equalTo(SemanticAttributes.CODE_FUNCTION, "performLogging"), - satisfies(SemanticAttributes.CODE_LINENO, AbstractLongAssert::isPositive), - equalTo(SemanticAttributes.CODE_FILEPATH, "LogbackTest.java"), - equalTo(SemanticAttributes.EXCEPTION_TYPE, IllegalStateException.class.getName()), - equalTo(SemanticAttributes.EXCEPTION_MESSAGE, "hello"), - satisfies( - SemanticAttributes.EXCEPTION_STACKTRACE, - v -> v.contains(LogbackTest.class.getName()))); - } else { - assertThat(log) - .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.THREAD_NAME, Thread.currentThread().getName()), - equalTo(SemanticAttributes.THREAD_ID, Thread.currentThread().getId()), - equalTo(SemanticAttributes.CODE_NAMESPACE, LogbackTest.class.getName()), - equalTo(SemanticAttributes.CODE_FUNCTION, "performLogging"), - satisfies(SemanticAttributes.CODE_LINENO, AbstractLongAssert::isPositive), - equalTo(SemanticAttributes.CODE_FILEPATH, "LogbackTest.java")); - } - - if (withParent) { - assertThat(log).hasSpanContext(testing.spans().get(0).getSpanContext()); - } else { - assertThat(log.getSpanContext().isValid()).isFalse(); - } + testing.waitAndAssertLogRecords( + logRecord -> { + logRecord + .hasBody("xyz: 123") + .hasInstrumentationScope( + InstrumentationScopeInfo.builder(expectedLoggerName).build()) + .hasSeverity(expectedSeverity) + .hasSeverityText(expectedSeverityText) + .hasSpanContext( + withParent + ? testing.spans().get(0).getSpanContext() + : SpanContext.getInvalid()); + List attributeAsserts = + new ArrayList<>( + Arrays.asList( + equalTo( + ThreadIncubatingAttributes.THREAD_NAME, + Thread.currentThread().getName()), + equalTo( + ThreadIncubatingAttributes.THREAD_ID, Thread.currentThread().getId()), + equalTo( + CodeIncubatingAttributes.CODE_NAMESPACE, LogbackTest.class.getName()), + equalTo(CodeIncubatingAttributes.CODE_FUNCTION, "performLogging"), + satisfies( + CodeIncubatingAttributes.CODE_LINENO, AbstractLongAssert::isPositive), + equalTo(CodeIncubatingAttributes.CODE_FILEPATH, "LogbackTest.java"))); + if (logException) { + attributeAsserts.addAll( + Arrays.asList( + equalTo( + ExceptionAttributes.EXCEPTION_TYPE, + IllegalStateException.class.getName()), + equalTo(ExceptionAttributes.EXCEPTION_MESSAGE, "hello"), + satisfies( + ExceptionAttributes.EXCEPTION_STACKTRACE, + v -> v.contains(LogbackTest.class.getName())))); + } + logRecord.hasAttributesSatisfyingExactly(attributeAsserts); + }); } else { Thread.sleep(500); // sleep a bit just to make sure no log is captured assertThat(testing.logRecords()).isEmpty(); @@ -186,21 +195,23 @@ void testMdc() { MDC.clear(); } - LogRecordData log = testing.waitForLogRecords(1).get(0); - assertThat(log) - .hasBody("xyz: 123") - .hasInstrumentationScope(InstrumentationScopeInfo.builder("abc").build()) - .hasSeverity(Severity.INFO) - .hasSeverityText("INFO") - .hasAttributesSatisfyingExactly( - equalTo(AttributeKey.stringKey("logback.mdc.key1"), "val1"), - equalTo(AttributeKey.stringKey("logback.mdc.key2"), "val2"), - equalTo(SemanticAttributes.THREAD_NAME, Thread.currentThread().getName()), - equalTo(SemanticAttributes.THREAD_ID, Thread.currentThread().getId()), - equalTo(SemanticAttributes.CODE_NAMESPACE, LogbackTest.class.getName()), - equalTo(SemanticAttributes.CODE_FUNCTION, "testMdc"), - satisfies(SemanticAttributes.CODE_LINENO, AbstractLongAssert::isPositive), - equalTo(SemanticAttributes.CODE_FILEPATH, "LogbackTest.java")); + testing.waitAndAssertLogRecords( + logRecord -> + logRecord + .hasBody("xyz: 123") + .hasInstrumentationScope(InstrumentationScopeInfo.builder("abc").build()) + .hasSeverity(Severity.INFO) + .hasSeverityText("INFO") + .hasAttributesSatisfyingExactly( + equalTo(AttributeKey.stringKey("key1"), "val1"), + equalTo(AttributeKey.stringKey("key2"), "val2"), + equalTo( + ThreadIncubatingAttributes.THREAD_NAME, Thread.currentThread().getName()), + equalTo(ThreadIncubatingAttributes.THREAD_ID, Thread.currentThread().getId()), + equalTo(CodeIncubatingAttributes.CODE_NAMESPACE, LogbackTest.class.getName()), + equalTo(CodeIncubatingAttributes.CODE_FUNCTION, "testMdc"), + satisfies(CodeIncubatingAttributes.CODE_LINENO, AbstractLongAssert::isPositive), + equalTo(CodeIncubatingAttributes.CODE_FILEPATH, "LogbackTest.java"))); } @Test @@ -211,16 +222,18 @@ public void testMarker() { abcLogger.info(marker, "Message"); - LogRecordData log = testing.waitForLogRecords(1).get(0); - assertThat(log) - .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.THREAD_NAME, Thread.currentThread().getName()), - equalTo(SemanticAttributes.THREAD_ID, Thread.currentThread().getId()), - equalTo(AttributeKey.stringArrayKey("logback.marker"), Arrays.asList(markerName)), - equalTo(SemanticAttributes.CODE_NAMESPACE, LogbackTest.class.getName()), - equalTo(SemanticAttributes.CODE_FUNCTION, "testMarker"), - satisfies(SemanticAttributes.CODE_LINENO, AbstractLongAssert::isPositive), - equalTo(SemanticAttributes.CODE_FILEPATH, "LogbackTest.java")); + testing.waitAndAssertLogRecords( + logRecord -> + logRecord.hasAttributesSatisfyingExactly( + equalTo(ThreadIncubatingAttributes.THREAD_NAME, Thread.currentThread().getName()), + equalTo(ThreadIncubatingAttributes.THREAD_ID, Thread.currentThread().getId()), + equalTo( + AttributeKey.stringArrayKey("logback.marker"), + Collections.singletonList(markerName)), + equalTo(CodeIncubatingAttributes.CODE_NAMESPACE, LogbackTest.class.getName()), + equalTo(CodeIncubatingAttributes.CODE_FUNCTION, "testMarker"), + satisfies(CodeIncubatingAttributes.CODE_LINENO, AbstractLongAssert::isPositive), + equalTo(CodeIncubatingAttributes.CODE_FILEPATH, "LogbackTest.java"))); } private static void performLogging( diff --git a/instrumentation/logback/logback-appender-1.0/library/README.md b/instrumentation/logback/logback-appender-1.0/library/README.md index 171d02b63efb..14c515071d76 100644 --- a/instrumentation/logback/logback-appender-1.0/library/README.md +++ b/instrumentation/logback/logback-appender-1.0/library/README.md @@ -20,7 +20,6 @@ For Maven, add to your `pom.xml` dependencies: io.opentelemetry.instrumentation opentelemetry-logback-appender-1.0 OPENTELEMETRY_VERSION - runtime ``` @@ -28,7 +27,7 @@ For Maven, add to your `pom.xml` dependencies: For Gradle, add to your dependencies: ```groovy -runtimeOnly("io.opentelemetry.instrumentation:opentelemetry-logback-appender-1.0:OPENTELEMETRY_VERSION") +implementation("io.opentelemetry.instrumentation:opentelemetry-logback-appender-1.0:OPENTELEMETRY_VERSION") ``` ### Usage @@ -60,3 +59,50 @@ The following demonstrates how you might configure the appender in your `logback In this example Logback log events will be sent to both the console appender and the `OpenTelemetryAppender`. + +In order to function, `OpenTelemetryAppender` needs access to an `OpenTelemetry` instance. This must +be set programmatically during application startup as follows: + +```java +import io.opentelemetry.instrumentation.logback.appender.v1_0.OpenTelemetryAppender; +import io.opentelemetry.sdk.OpenTelemetrySdk; + +public class Application { + + public static void main(String[] args) { + OpenTelemetrySdk openTelemetrySdk = // Configure OpenTelemetrySdk + + // Find OpenTelemetryAppender in logback configuration and install openTelemetrySdk + OpenTelemetryAppender.install(openTelemetrySdk); + + // ... proceed with application + } +} +``` + +#### Settings for the Logback Appender + +Settings can be configured in `logback.xml`, for example: + +```xml + + true + * + +``` + +The available settings are: + +| XML Element | Type | Default | Description | +|------------------------------------|---------|---------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `captureExperimentalAttributes` | Boolean | `false` | Enable the capture of experimental log attributes `thread.name` and `thread.id`. | +| `captureCodeAttributes` | Boolean | `false` | Enable the capture of [source code attributes]. Note that capturing source code attributes at logging sites might add a performance overhead. | +| `captureMarkerAttribute` | Boolean | `false` | Enable the capture of Logback markers as attributes. | +| `captureKeyValuePairAttributes` | Boolean | `false` | Enable the capture of Logback key value pairs as attributes. | +| `captureLoggerContext` | Boolean | `false` | Enable the capture of Logback logger context properties as attributes. | +| `captureArguments` | Boolean | `false` | Enable the capture of Logback logger arguments. | +| `captureMdcAttributes` | String | | Comma separated list of MDC attributes to capture. Use the wildcard character `*` to capture all attributes. | +| `numLogsCapturedBeforeOtelInstall` | Integer | 1000 | Log telemetry is emitted after the initialization of the OpenTelemetry Logback appender with an OpenTelemetry object. This setting allows you to modify the size of the cache used to replay the first logs. thread.id attribute is not captured. | + + +[source code attributes]: https://github.com/open-telemetry/semantic-conventions/blob/main/docs/general/attributes.md#source-code-attributes diff --git a/instrumentation/logback/logback-appender-1.0/library/build.gradle.kts b/instrumentation/logback/logback-appender-1.0/library/build.gradle.kts index dc625d36ad02..87a21f3aa5ea 100644 --- a/instrumentation/logback/logback-appender-1.0/library/build.gradle.kts +++ b/instrumentation/logback/logback-appender-1.0/library/build.gradle.kts @@ -42,6 +42,15 @@ graalvmNative { binaries.all { resources.autodetect() + + // Workaround for https://github.com/junit-team/junit5/issues/3405 + buildArgs.add("--initialize-at-build-time=org.junit.platform.launcher.core.LauncherConfig") + buildArgs.add("--initialize-at-build-time=org.junit.jupiter.engine.config.InstantiatingConfigurationParameterConverter") + } + + // See https://github.com/graalvm/native-build-tools/issues/572 + metadataRepository { + enabled.set(false) } toolchainDetection.set(false) @@ -61,6 +70,7 @@ testing { dependencies { implementation(project(":instrumentation:logback:logback-appender-1.0:library")) implementation("io.opentelemetry:opentelemetry-sdk-testing") + implementation(project(":testing-common")) if (latestDepTest) { implementation("ch.qos.logback:logback-classic:+") @@ -87,3 +97,7 @@ tasks { dependsOn(testing.suites) } } + +tasks.withType().configureEach { + jvmArgs("-Dotel.instrumentation.common.experimental.controller-telemetry.enabled=true") +} diff --git a/instrumentation/logback/logback-appender-1.0/library/src/main/java/io/opentelemetry/instrumentation/logback/appender/v1_0/LoggingEventToReplay.java b/instrumentation/logback/logback-appender-1.0/library/src/main/java/io/opentelemetry/instrumentation/logback/appender/v1_0/LoggingEventToReplay.java new file mode 100644 index 000000000000..ae19b2185704 --- /dev/null +++ b/instrumentation/logback/logback-appender-1.0/library/src/main/java/io/opentelemetry/instrumentation/logback/appender/v1_0/LoggingEventToReplay.java @@ -0,0 +1,136 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.logback.appender.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.List; +import java.util.Map; +import org.slf4j.Marker; +import org.slf4j.event.KeyValuePair; + +class LoggingEventToReplay implements ILoggingEvent { + + private final ILoggingEvent loggingEvent; + private final long timeStamp; + private StackTraceElement[] callerData; + private String threadName; + + LoggingEventToReplay( + ILoggingEvent loggingEvent, + boolean captureExperimentalAttributes, + boolean captureCodeAttributes) { + this.loggingEvent = loggingEvent; + // The values are copied because the current values are not more available when the log is + // replayed + this.timeStamp = loggingEvent.getTimeStamp(); + if (captureExperimentalAttributes) { + this.threadName = loggingEvent.getThreadName(); + } + if (captureCodeAttributes) { + this.callerData = loggingEvent.getCallerData(); + } + } + + @Override + public String getThreadName() { + return threadName; + } + + @Override + public Level getLevel() { + return loggingEvent.getLevel(); + } + + @Override + public String getMessage() { + return loggingEvent.getMessage(); + } + + @Override + public Object[] getArgumentArray() { + return loggingEvent.getArgumentArray(); + } + + @Override + public String getFormattedMessage() { + return loggingEvent.getFormattedMessage(); + } + + @Override + public String getLoggerName() { + return loggingEvent.getLoggerName(); + } + + @Override + public LoggerContextVO getLoggerContextVO() { + return loggingEvent.getLoggerContextVO(); + } + + @Override + public IThrowableProxy getThrowableProxy() { + return loggingEvent.getThrowableProxy(); + } + + @Override + public StackTraceElement[] getCallerData() { + return callerData; + } + + @Override + public boolean hasCallerData() { + return loggingEvent.hasCallerData(); + } + + @SuppressWarnings("deprecation") // Delegate + @Override + public Marker getMarker() { + return loggingEvent.getMarker(); + } + + @Override + public List getMarkerList() { + return loggingEvent.getMarkerList(); + } + + @Override + public Map getMDCPropertyMap() { + return loggingEvent.getMDCPropertyMap(); + } + + @SuppressWarnings("deprecation") // Delegate + @Override + public Map getMdc() { + return loggingEvent.getMdc(); + } + + @Override + public long getTimeStamp() { + return timeStamp; + } + + @Override + public int getNanoseconds() { + return loggingEvent.getNanoseconds(); + } + + @Override + public long getSequenceNumber() { + return loggingEvent.getSequenceNumber(); + } + + @Override + public List getKeyValuePairs() { + return loggingEvent.getKeyValuePairs(); + } + + @Override + public void prepareForDeferredProcessing() { + loggingEvent.prepareForDeferredProcessing(); + } +} diff --git a/instrumentation/logback/logback-appender-1.0/library/src/main/java/io/opentelemetry/instrumentation/logback/appender/v1_0/OpenTelemetryAppender.java b/instrumentation/logback/logback-appender-1.0/library/src/main/java/io/opentelemetry/instrumentation/logback/appender/v1_0/OpenTelemetryAppender.java index b161d54d6009..06904c219c81 100644 --- a/instrumentation/logback/logback-appender-1.0/library/src/main/java/io/opentelemetry/instrumentation/logback/appender/v1_0/OpenTelemetryAppender.java +++ b/instrumentation/logback/logback-appender-1.0/library/src/main/java/io/opentelemetry/instrumentation/logback/appender/v1_0/OpenTelemetryAppender.java @@ -7,13 +7,23 @@ import static java.util.Collections.emptyList; +import ch.qos.logback.classic.LoggerContext; import ch.qos.logback.classic.spi.ILoggingEvent; import ch.qos.logback.core.UnsynchronizedAppenderBase; -import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.api.OpenTelemetry; import io.opentelemetry.instrumentation.logback.appender.v1_0.internal.LoggingEventMapper; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.stream.Collectors; +import org.slf4j.ILoggerFactory; +import org.slf4j.LoggerFactory; import org.slf4j.MDC; public class OpenTelemetryAppender extends UnsynchronizedAppenderBase { @@ -22,27 +32,90 @@ public class OpenTelemetryAppender extends UnsynchronizedAppenderBase captureMdcAttributes = emptyList(); + private volatile OpenTelemetry openTelemetry; private LoggingEventMapper mapper; + private int numLogsCapturedBeforeOtelInstall = 1000; + private BlockingQueue eventsToReplay = + new ArrayBlockingQueue<>(numLogsCapturedBeforeOtelInstall); + private final AtomicBoolean replayLimitWarningLogged = new AtomicBoolean(); + + private final ReadWriteLock lock = new ReentrantReadWriteLock(); + public OpenTelemetryAppender() {} + /** + * Installs the {@code openTelemetry} instance on any {@link OpenTelemetryAppender}s identified in + * the {@link LoggerContext}. + */ + public static void install(OpenTelemetry openTelemetry) { + ILoggerFactory loggerFactorySpi = LoggerFactory.getILoggerFactory(); + if (!(loggerFactorySpi instanceof LoggerContext)) { + return; + } + LoggerContext loggerContext = (LoggerContext) loggerFactorySpi; + for (ch.qos.logback.classic.Logger logger : loggerContext.getLoggerList()) { + logger + .iteratorForAppenders() + .forEachRemaining( + appender -> { + if (appender instanceof OpenTelemetryAppender) { + ((OpenTelemetryAppender) appender).setOpenTelemetry(openTelemetry); + } + }); + } + } + @Override public void start() { mapper = - new LoggingEventMapper( - captureExperimentalAttributes, - captureMdcAttributes, - captureCodeAttributes, - captureMarkerAttribute, - captureKeyValuePairAttributes); + LoggingEventMapper.builder() + .setCaptureExperimentalAttributes(captureExperimentalAttributes) + .setCaptureMdcAttributes(captureMdcAttributes) + .setCaptureCodeAttributes(captureCodeAttributes) + .setCaptureMarkerAttribute(captureMarkerAttribute) + .setCaptureKeyValuePairAttributes(captureKeyValuePairAttributes) + .setCaptureLoggerContext(captureLoggerContext) + .setCaptureArguments(captureArguments) + .build(); + eventsToReplay = new ArrayBlockingQueue<>(numLogsCapturedBeforeOtelInstall); super.start(); } + @SuppressWarnings("SystemOut") @Override protected void append(ILoggingEvent event) { - mapper.emit(GlobalOpenTelemetry.get().getLogsBridge(), event); + OpenTelemetry openTelemetry = this.openTelemetry; + if (openTelemetry != null) { + // optimization to avoid locking after the OpenTelemetry instance is set + emit(openTelemetry, event); + return; + } + + Lock readLock = lock.readLock(); + readLock.lock(); + try { + openTelemetry = this.openTelemetry; + if (openTelemetry != null) { + emit(openTelemetry, event); + return; + } + + LoggingEventToReplay logEventToReplay = + new LoggingEventToReplay(event, captureExperimentalAttributes, captureCodeAttributes); + + if (!eventsToReplay.offer(logEventToReplay) && !replayLimitWarningLogged.getAndSet(true)) { + String message = + "numLogsCapturedBeforeOtelInstall value of the OpenTelemetry appender is too small."; + System.err.println(message); + } + } finally { + readLock.unlock(); + } } /** @@ -69,7 +142,7 @@ public void setCaptureCodeAttributes(boolean captureCodeAttributes) { /** * Sets whether the marker attribute should be set to logs. * - * @param captureMarkerAttribute To enable or disable the marker attribute + * @param captureMarkerAttribute To enable or disable capturing the marker attribute */ public void setCaptureMarkerAttribute(boolean captureMarkerAttribute) { this.captureMarkerAttribute = captureMarkerAttribute; @@ -78,12 +151,30 @@ public void setCaptureMarkerAttribute(boolean captureMarkerAttribute) { /** * Sets whether the key value pair attributes should be set to logs. * - * @param captureKeyValuePairAttributes To enable or disable the marker attribute + * @param captureKeyValuePairAttributes To enable or disable capturing key value pairs */ public void setCaptureKeyValuePairAttributes(boolean captureKeyValuePairAttributes) { this.captureKeyValuePairAttributes = captureKeyValuePairAttributes; } + /** + * Sets whether the logger context properties should be set to logs. + * + * @param captureLoggerContext To enable or disable capturing logger context properties + */ + public void setCaptureLoggerContext(boolean captureLoggerContext) { + this.captureLoggerContext = captureLoggerContext; + } + + /** + * Sets whether the arguments should be set to logs. + * + * @param captureArguments To enable or disable capturing logger arguments + */ + public void setCaptureArguments(boolean captureArguments) { + this.captureArguments = captureArguments; + } + /** Configures the {@link MDC} attributes that will be copied to logs. */ public void setCaptureMdcAttributes(String attributes) { if (attributes != null) { @@ -93,6 +184,40 @@ public void setCaptureMdcAttributes(String attributes) { } } + /** + * Log telemetry is emitted after the initialization of the OpenTelemetry Logback appender with an + * {@link OpenTelemetry} object. This setting allows you to modify the size of the cache used to + * replay the first logs. + */ + public void setNumLogsCapturedBeforeOtelInstall(int size) { + this.numLogsCapturedBeforeOtelInstall = size; + } + + /** + * Configures the {@link OpenTelemetry} used to append logs. This MUST be called for the appender + * to function. See {@link #install(OpenTelemetry)} for simple installation option. + */ + public void setOpenTelemetry(OpenTelemetry openTelemetry) { + List eventsToReplay = new ArrayList<>(); + Lock writeLock = lock.writeLock(); + writeLock.lock(); + try { + // minimize scope of write lock + this.openTelemetry = openTelemetry; + this.eventsToReplay.drainTo(eventsToReplay); + } finally { + writeLock.unlock(); + } + // now emit + for (LoggingEventToReplay eventToReplay : eventsToReplay) { + emit(openTelemetry, eventToReplay); + } + } + + private void emit(OpenTelemetry openTelemetry, ILoggingEvent event) { + mapper.emit(openTelemetry.getLogsBridge(), event, -1); + } + // copied from SDK's DefaultConfigProperties private static List filterBlanksAndNulls(String[] values) { return Arrays.stream(values) diff --git a/instrumentation/logback/logback-appender-1.0/library/src/main/java/io/opentelemetry/instrumentation/logback/appender/v1_0/internal/LoggingEventMapper.java b/instrumentation/logback/logback-appender-1.0/library/src/main/java/io/opentelemetry/instrumentation/logback/appender/v1_0/internal/LoggingEventMapper.java index 2393ae8e444c..8f0af6048202 100644 --- a/instrumentation/logback/logback-appender-1.0/library/src/main/java/io/opentelemetry/instrumentation/logback/appender/v1_0/internal/LoggingEventMapper.java +++ b/instrumentation/logback/logback-appender-1.0/library/src/main/java/io/opentelemetry/instrumentation/logback/appender/v1_0/internal/LoggingEventMapper.java @@ -5,9 +5,12 @@ package io.opentelemetry.instrumentation.logback.appender.v1_0.internal; +import static java.util.Collections.emptyList; + import ch.qos.logback.classic.Level; import ch.qos.logback.classic.spi.ILoggingEvent; import ch.qos.logback.classic.spi.ThrowableProxy; +import com.google.errorprone.annotations.CanIgnoreReturnValue; import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.common.Attributes; import io.opentelemetry.api.common.AttributesBuilder; @@ -17,13 +20,15 @@ import io.opentelemetry.context.Context; import io.opentelemetry.instrumentation.api.internal.cache.Cache; import io.opentelemetry.javaagent.tooling.muzzle.NoMuzzle; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.opentelemetry.semconv.ExceptionAttributes; import java.io.PrintWriter; import java.io.StringWriter; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; import org.slf4j.Marker; import org.slf4j.event.KeyValuePair; @@ -32,13 +37,28 @@ * any time. */ public final class LoggingEventMapper { - + // copied from CodeIncubatingAttributes + private static final AttributeKey CODE_FILEPATH = AttributeKey.stringKey("code.filepath"); + private static final AttributeKey CODE_FUNCTION = AttributeKey.stringKey("code.function"); + private static final AttributeKey CODE_LINENO = AttributeKey.longKey("code.lineno"); + private static final AttributeKey CODE_NAMESPACE = + AttributeKey.stringKey("code.namespace"); + // copied from + private static final AttributeKey THREAD_ID = AttributeKey.longKey("thread.id"); + private static final AttributeKey THREAD_NAME = AttributeKey.stringKey("thread.name"); + + private static final boolean supportsInstant = supportsInstant(); private static final boolean supportsKeyValuePairs = supportsKeyValuePairs(); private static final boolean supportsMultipleMarkers = supportsMultipleMarkers(); private static final Cache> mdcAttributeKeys = Cache.bounded(100); + private static final Cache> attributeKeys = Cache.bounded(100); private static final AttributeKey> LOG_MARKER = AttributeKey.stringArrayKey("logback.marker"); + private static final AttributeKey LOG_BODY_TEMPLATE = + AttributeKey.stringKey("log.body.template"); + private static final AttributeKey> LOG_BODY_PARAMETERS = + AttributeKey.stringArrayKey("log.body.parameters"); private final boolean captureExperimentalAttributes; private final List captureMdcAttributes; @@ -46,30 +66,33 @@ public final class LoggingEventMapper { private final boolean captureCodeAttributes; private final boolean captureMarkerAttribute; private final boolean captureKeyValuePairAttributes; - - public LoggingEventMapper( - boolean captureExperimentalAttributes, - List captureMdcAttributes, - boolean captureCodeAttributes, - boolean captureMarkerAttribute, - boolean captureKeyValuePairAttributes) { - this.captureExperimentalAttributes = captureExperimentalAttributes; - this.captureCodeAttributes = captureCodeAttributes; - this.captureMdcAttributes = captureMdcAttributes; - this.captureMarkerAttribute = captureMarkerAttribute; - this.captureKeyValuePairAttributes = captureKeyValuePairAttributes; + private final boolean captureLoggerContext; + private final boolean captureArguments; + + private LoggingEventMapper(Builder builder) { + this.captureExperimentalAttributes = builder.captureExperimentalAttributes; + this.captureCodeAttributes = builder.captureCodeAttributes; + this.captureMdcAttributes = builder.captureMdcAttributes; + this.captureMarkerAttribute = builder.captureMarkerAttribute; + this.captureKeyValuePairAttributes = builder.captureKeyValuePairAttributes; + this.captureLoggerContext = builder.captureLoggerContext; + this.captureArguments = builder.captureArguments; this.captureAllMdcAttributes = - captureMdcAttributes.size() == 1 && captureMdcAttributes.get(0).equals("*"); + builder.captureMdcAttributes.size() == 1 && builder.captureMdcAttributes.get(0).equals("*"); } - public void emit(LoggerProvider loggerProvider, ILoggingEvent event) { + public static Builder builder() { + return new Builder(); + } + + public void emit(LoggerProvider loggerProvider, ILoggingEvent event, long threadId) { String instrumentationName = event.getLoggerName(); if (instrumentationName == null || instrumentationName.isEmpty()) { instrumentationName = "ROOT"; } LogRecordBuilder builder = loggerProvider.loggerBuilder(instrumentationName).build().logRecordBuilder(); - mapLoggingEvent(builder, event); + mapLoggingEvent(builder, event, threadId); builder.emit(); } @@ -83,7 +106,8 @@ public void emit(LoggerProvider loggerProvider, ILoggingEvent event) { *
  • Mapped diagnostic context - {@link ILoggingEvent#getMDCPropertyMap()} * */ - private void mapLoggingEvent(LogRecordBuilder builder, ILoggingEvent loggingEvent) { + private void mapLoggingEvent( + LogRecordBuilder builder, ILoggingEvent loggingEvent, long threadId) { // message String message = loggingEvent.getFormattedMessage(); if (message != null) { @@ -91,8 +115,12 @@ private void mapLoggingEvent(LogRecordBuilder builder, ILoggingEvent loggingEven } // time - long timestamp = loggingEvent.getTimeStamp(); - builder.setTimestamp(timestamp, TimeUnit.MILLISECONDS); + if (supportsInstant && hasInstant(loggingEvent)) { + setTimestampFromInstant(builder, loggingEvent); + } else { + long timestamp = loggingEvent.getTimeStamp(); + builder.setTimestamp(timestamp, TimeUnit.MILLISECONDS); + } // level Level level = loggingEvent.getLevel(); @@ -118,9 +146,10 @@ private void mapLoggingEvent(LogRecordBuilder builder, ILoggingEvent loggingEven captureMdcAttributes(attributes, loggingEvent.getMDCPropertyMap()); if (captureExperimentalAttributes) { - Thread currentThread = Thread.currentThread(); - attributes.put(SemanticAttributes.THREAD_NAME, currentThread.getName()); - attributes.put(SemanticAttributes.THREAD_ID, currentThread.getId()); + attributes.put(THREAD_NAME, loggingEvent.getThreadName()); + if (threadId != -1) { + attributes.put(THREAD_ID, threadId); + } } if (captureCodeAttributes) { @@ -129,13 +158,13 @@ private void mapLoggingEvent(LogRecordBuilder builder, ILoggingEvent loggingEven StackTraceElement firstStackElement = callerData[0]; String fileName = firstStackElement.getFileName(); if (fileName != null) { - attributes.put(SemanticAttributes.CODE_FILEPATH, fileName); + attributes.put(CODE_FILEPATH, fileName); } - attributes.put(SemanticAttributes.CODE_NAMESPACE, firstStackElement.getClassName()); - attributes.put(SemanticAttributes.CODE_FUNCTION, firstStackElement.getMethodName()); + attributes.put(CODE_NAMESPACE, firstStackElement.getClassName()); + attributes.put(CODE_FUNCTION, firstStackElement.getMethodName()); int lineNumber = firstStackElement.getLineNumber(); if (lineNumber > 0) { - attributes.put(SemanticAttributes.CODE_LINENO, lineNumber); + attributes.put(CODE_LINENO, lineNumber); } } } @@ -148,15 +177,46 @@ private void mapLoggingEvent(LogRecordBuilder builder, ILoggingEvent loggingEven captureKeyValuePairAttributes(attributes, loggingEvent); } + if (captureLoggerContext) { + captureLoggerContext(attributes, loggingEvent.getLoggerContextVO().getPropertyMap()); + } + + if (captureArguments + && loggingEvent.getArgumentArray() != null + && loggingEvent.getArgumentArray().length > 0) { + captureArguments(attributes, loggingEvent.getMessage(), loggingEvent.getArgumentArray()); + } + builder.setAllAttributes(attributes.build()); // span context builder.setContext(Context.current()); } + // getInstant is available since Logback 1.3 + private static boolean supportsInstant() { + try { + ILoggingEvent.class.getMethod("getInstant"); + } catch (NoSuchMethodException e) { + return false; + } + + return true; + } + + @NoMuzzle + private static boolean hasInstant(ILoggingEvent loggingEvent) { + return loggingEvent.getInstant() != null; + } + + @NoMuzzle + private static void setTimestampFromInstant( + LogRecordBuilder builder, ILoggingEvent loggingEvent) { + builder.setTimestamp(loggingEvent.getInstant()); + } + // visible for testing void captureMdcAttributes(AttributesBuilder attributes, Map mdcProperties) { - if (captureAllMdcAttributes) { for (Map.Entry entry : mdcProperties.entrySet()) { attributes.put(getMdcAttributeKey(entry.getKey()), entry.getValue()); @@ -172,18 +232,25 @@ void captureMdcAttributes(AttributesBuilder attributes, Map mdcP } } + void captureArguments(AttributesBuilder attributes, String message, Object[] arguments) { + attributes.put(LOG_BODY_TEMPLATE, message); + attributes.put( + LOG_BODY_PARAMETERS, + Arrays.stream(arguments).map(String::valueOf).collect(Collectors.toList())); + } + public static AttributeKey getMdcAttributeKey(String key) { - return mdcAttributeKeys.computeIfAbsent(key, k -> AttributeKey.stringKey("logback.mdc." + k)); + return mdcAttributeKeys.computeIfAbsent(key, AttributeKey::stringKey); } private static void setThrowable(AttributesBuilder attributes, Throwable throwable) { // TODO (trask) extract method for recording exception into // io.opentelemetry:opentelemetry-api - attributes.put(SemanticAttributes.EXCEPTION_TYPE, throwable.getClass().getName()); - attributes.put(SemanticAttributes.EXCEPTION_MESSAGE, throwable.getMessage()); + attributes.put(ExceptionAttributes.EXCEPTION_TYPE, throwable.getClass().getName()); + attributes.put(ExceptionAttributes.EXCEPTION_MESSAGE, throwable.getMessage()); StringWriter writer = new StringWriter(); throwable.printStackTrace(new PrintWriter(writer)); - attributes.put(SemanticAttributes.EXCEPTION_STACKTRACE, writer.toString()); + attributes.put(ExceptionAttributes.EXCEPTION_STACKTRACE, writer.toString()); } private static Severity levelToSeverity(Level level) { @@ -211,13 +278,38 @@ private static void captureKeyValuePairAttributes( List keyValuePairs = loggingEvent.getKeyValuePairs(); if (keyValuePairs != null) { for (KeyValuePair keyValuePair : keyValuePairs) { - if (keyValuePair.value != null) { - attributes.put(keyValuePair.key, keyValuePair.value.toString()); + Object value = keyValuePair.value; + if (value != null) { + String key = keyValuePair.key; + // preserve type for boolean and numeric values, everything else is converted to String + if (value instanceof Boolean) { + attributes.put(key, (Boolean) value); + } else if (value instanceof Byte + || value instanceof Integer + || value instanceof Long + || value instanceof Short) { + attributes.put(key, ((Number) value).longValue()); + } else if (value instanceof Double || value instanceof Float) { + attributes.put(key, ((Number) value).doubleValue()); + } else { + attributes.put(getAttributeKey(key), value.toString()); + } } } } } + private static void captureLoggerContext( + AttributesBuilder attributes, Map loggerContextProperties) { + for (Map.Entry entry : loggerContextProperties.entrySet()) { + attributes.put(getAttributeKey(entry.getKey()), entry.getValue()); + } + } + + public static AttributeKey getAttributeKey(String key) { + return attributeKeys.computeIfAbsent(key, AttributeKey::stringKey); + } + private static boolean supportsKeyValuePairs() { try { Class.forName("org.slf4j.event.KeyValuePair"); @@ -276,4 +368,66 @@ private static boolean supportsMultipleMarkers() { return true; } + + /** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time. + */ + public static final class Builder { + private boolean captureExperimentalAttributes; + private List captureMdcAttributes = emptyList(); + private boolean captureCodeAttributes; + private boolean captureMarkerAttribute; + private boolean captureKeyValuePairAttributes; + private boolean captureLoggerContext; + private boolean captureArguments; + + Builder() {} + + @CanIgnoreReturnValue + public Builder setCaptureExperimentalAttributes(boolean captureExperimentalAttributes) { + this.captureExperimentalAttributes = captureExperimentalAttributes; + return this; + } + + @CanIgnoreReturnValue + public Builder setCaptureMdcAttributes(List captureMdcAttributes) { + this.captureMdcAttributes = captureMdcAttributes; + return this; + } + + @CanIgnoreReturnValue + public Builder setCaptureCodeAttributes(boolean captureCodeAttributes) { + this.captureCodeAttributes = captureCodeAttributes; + return this; + } + + @CanIgnoreReturnValue + public Builder setCaptureMarkerAttribute(boolean captureMarkerAttribute) { + this.captureMarkerAttribute = captureMarkerAttribute; + return this; + } + + @CanIgnoreReturnValue + public Builder setCaptureKeyValuePairAttributes(boolean captureKeyValuePairAttributes) { + this.captureKeyValuePairAttributes = captureKeyValuePairAttributes; + return this; + } + + @CanIgnoreReturnValue + public Builder setCaptureLoggerContext(boolean captureLoggerContext) { + this.captureLoggerContext = captureLoggerContext; + return this; + } + + @CanIgnoreReturnValue + public Builder setCaptureArguments(boolean captureArguments) { + this.captureArguments = captureArguments; + return this; + } + + public LoggingEventMapper build() { + return new LoggingEventMapper(this); + } + } } diff --git a/instrumentation/logback/logback-appender-1.0/library/src/slf4j2ApiTest/java/io/opentelemetry/instrumentation/logback/appender/v1_0/Slf4j2Test.java b/instrumentation/logback/logback-appender-1.0/library/src/slf4j2ApiTest/java/io/opentelemetry/instrumentation/logback/appender/v1_0/Slf4j2Test.java index 80734e80fc02..ddccdeb5a84a 100644 --- a/instrumentation/logback/logback-appender-1.0/library/src/slf4j2ApiTest/java/io/opentelemetry/instrumentation/logback/appender/v1_0/Slf4j2Test.java +++ b/instrumentation/logback/logback-appender-1.0/library/src/slf4j2ApiTest/java/io/opentelemetry/instrumentation/logback/appender/v1_0/Slf4j2Test.java @@ -5,22 +5,16 @@ package io.opentelemetry.instrumentation.logback.appender.v1_0; -import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; -import io.opentelemetry.api.GlobalOpenTelemetry; import io.opentelemetry.api.common.AttributeKey; -import io.opentelemetry.sdk.OpenTelemetrySdk; +import io.opentelemetry.instrumentation.testing.junit.LibraryInstrumentationExtension; import io.opentelemetry.sdk.common.InstrumentationScopeInfo; -import io.opentelemetry.sdk.logs.SdkLoggerProvider; -import io.opentelemetry.sdk.logs.data.LogRecordData; -import io.opentelemetry.sdk.logs.export.SimpleLogRecordProcessor; import io.opentelemetry.sdk.resources.Resource; -import io.opentelemetry.sdk.testing.exporter.InMemoryLogRecordExporter; import java.util.Arrays; -import java.util.List; import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.slf4j.MarkerFactory; @@ -28,45 +22,52 @@ public class Slf4j2Test { private static final Logger logger = LoggerFactory.getLogger("TestLogger"); - private static InMemoryLogRecordExporter logRecordExporter; + @RegisterExtension + private static final LibraryInstrumentationExtension testing = + LibraryInstrumentationExtension.create(); + private static Resource resource; private static InstrumentationScopeInfo instrumentationScopeInfo; @BeforeAll static void setupAll() { - logRecordExporter = InMemoryLogRecordExporter.create(); resource = Resource.getDefault(); instrumentationScopeInfo = InstrumentationScopeInfo.create("TestLogger"); - SdkLoggerProvider loggerProvider = - SdkLoggerProvider.builder() - .setResource(resource) - .addLogRecordProcessor(SimpleLogRecordProcessor.create(logRecordExporter)) - .build(); - - GlobalOpenTelemetry.resetForTest(); - GlobalOpenTelemetry.set(OpenTelemetrySdk.builder().setLoggerProvider(loggerProvider).build()); - } - - @BeforeEach - void setup() { - logRecordExporter.reset(); + OpenTelemetryAppender.install(testing.getOpenTelemetry()); } @Test void keyValue() { - logger.atInfo().setMessage("log message 1").addKeyValue("key", "value").log(); + logger + .atInfo() + .setMessage("log message 1") + .addKeyValue("string key", "string value") + .addKeyValue("boolean key", true) + .addKeyValue("byte key", (byte) 1) + .addKeyValue("short key", (short) 2) + .addKeyValue("int key", 3) + .addKeyValue("long key", 4L) + .addKeyValue("float key", 5.0f) + .addKeyValue("double key", 6.0) + .log(); - List logDataList = logRecordExporter.getFinishedLogRecordItems(); - assertThat(logDataList).hasSize(1); - LogRecordData logData = logDataList.get(0); - assertThat(logData.getResource()).isEqualTo(resource); - assertThat(logData.getInstrumentationScopeInfo()).isEqualTo(instrumentationScopeInfo); - assertThat(logData.getBody().asString()).isEqualTo("log message 1"); - assertThat(logData.getAttributes().size()).isEqualTo(5); // 4 code attributes + 1 key value pair - assertThat(logData.getAttributes()) - .hasEntrySatisfying( - AttributeKey.stringKey("key"), value -> assertThat(value).isEqualTo("value")); + testing.waitAndAssertLogRecords( + logRecord -> + logRecord + .hasResource(resource) + .hasInstrumentationScope(instrumentationScopeInfo) + .hasBody("log message 1") + .hasTotalAttributeCount(12) // 4 code attributes + 8 key value pairs + .hasAttributesSatisfying( + equalTo(AttributeKey.stringKey("string key"), "string value"), + equalTo(AttributeKey.booleanKey("boolean key"), true), + equalTo(AttributeKey.longKey("byte key"), 1), + equalTo(AttributeKey.longKey("short key"), 2), + equalTo(AttributeKey.longKey("int key"), 3), + equalTo(AttributeKey.longKey("long key"), 4), + equalTo(AttributeKey.doubleKey("float key"), 5.0), + equalTo(AttributeKey.doubleKey("double key"), 6.0))); } @Test @@ -80,16 +81,48 @@ void multipleMarkers() { .addMarker(MarkerFactory.getMarker(markerName2)) .log(); - List logDataList = logRecordExporter.getFinishedLogRecordItems(); - assertThat(logDataList).hasSize(1); - LogRecordData logData = logDataList.get(0); - assertThat(logData.getResource()).isEqualTo(resource); - assertThat(logData.getInstrumentationScopeInfo()).isEqualTo(instrumentationScopeInfo); - assertThat(logData.getBody().asString()).isEqualTo("log message 1"); - assertThat(logData.getAttributes().size()).isEqualTo(5); // 4 code attributes + 1 marker - assertThat(logData.getAttributes()) - .hasEntrySatisfying( - AttributeKey.stringArrayKey("logback.marker"), - value -> assertThat(value).isEqualTo(Arrays.asList(markerName1, markerName2))); + testing.waitAndAssertLogRecords( + logRecord -> + logRecord + .hasResource(resource) + .hasInstrumentationScope(instrumentationScopeInfo) + .hasBody("log message 1") + .hasTotalAttributeCount(5) // 4 code attributes + 1 marker + .hasAttributesSatisfying( + equalTo( + AttributeKey.stringArrayKey("logback.marker"), + Arrays.asList(markerName1, markerName2)))); + } + + @Test + void arguments() { + logger + .atInfo() + .setMessage("log message {} and {}, bool {}, long {}") + .addArgument("'world'") + .addArgument(Math.PI) + .addArgument(true) + .addArgument(Long.MAX_VALUE) + .log(); + + testing.waitAndAssertLogRecords( + logRecord -> + logRecord + .hasResource(resource) + .hasInstrumentationScope(instrumentationScopeInfo) + .hasBody( + "log message 'world' and 3.141592653589793, bool true, long 9223372036854775807") + .hasTotalAttributeCount(6) + .hasAttributesSatisfying( + equalTo( + AttributeKey.stringArrayKey("log.body.parameters"), + Arrays.asList( + "'world'", + String.valueOf(Math.PI), + String.valueOf(true), + String.valueOf(Long.MAX_VALUE))), + equalTo( + AttributeKey.stringKey("log.body.template"), + "log message {} and {}, bool {}, long {}"))); } } diff --git a/instrumentation/logback/logback-appender-1.0/library/src/slf4j2ApiTest/resources/logback-test.xml b/instrumentation/logback/logback-appender-1.0/library/src/slf4j2ApiTest/resources/logback-test.xml index d02bf772803d..366678be3369 100644 --- a/instrumentation/logback/logback-appender-1.0/library/src/slf4j2ApiTest/resources/logback-test.xml +++ b/instrumentation/logback/logback-appender-1.0/library/src/slf4j2ApiTest/resources/logback-test.xml @@ -14,6 +14,7 @@ true true true + true * diff --git a/instrumentation/logback/logback-appender-1.0/library/src/test/java/io/opentelemetry/instrumentation/logback/appender/v1_0/AbstractOpenTelemetryAppenderTest.java b/instrumentation/logback/logback-appender-1.0/library/src/test/java/io/opentelemetry/instrumentation/logback/appender/v1_0/AbstractOpenTelemetryAppenderTest.java new file mode 100644 index 000000000000..bd73cdc9dd4c --- /dev/null +++ b/instrumentation/logback/logback-appender-1.0/library/src/test/java/io/opentelemetry/instrumentation/logback/appender/v1_0/AbstractOpenTelemetryAppenderTest.java @@ -0,0 +1,188 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.logback.appender.v1_0; + +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.satisfies; + +import ch.qos.logback.classic.LoggerContext; +import ch.qos.logback.core.ContextBase; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.logs.Severity; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.sdk.common.InstrumentationScopeInfo; +import io.opentelemetry.sdk.logs.data.LogRecordData; +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.semconv.ExceptionAttributes; +import io.opentelemetry.semconv.incubating.CodeIncubatingAttributes; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.time.Instant; +import java.util.Collections; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import org.assertj.core.api.AssertAccess; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.slf4j.MDC; +import org.slf4j.Marker; +import org.slf4j.MarkerFactory; + +abstract class AbstractOpenTelemetryAppenderTest { + + static final Logger logger = LoggerFactory.getLogger("TestLogger"); + + static Resource resource; + static InstrumentationScopeInfo instrumentationScopeInfo; + + void executeAfterLogsExecution() {} + + @BeforeAll + static void setupAll() { + resource = Resource.getDefault(); + instrumentationScopeInfo = InstrumentationScopeInfo.create("TestLogger"); + // by default LoggerContext contains HOSTNAME property we clear it to start with empty context + resetLoggerContext(); + } + + static void resetLoggerContext() { + try { + LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory(); + Field field = ContextBase.class.getDeclaredField("propertyMap"); + field.setAccessible(true); + Map map = (Map) field.get(loggerContext); + map.clear(); + + Method method; + try { + method = LoggerContext.class.getDeclaredMethod("syncRemoteView"); + } catch (NoSuchMethodException noSuchMethodException) { + method = LoggerContext.class.getDeclaredMethod("updateLoggerContextVO"); + } + method.setAccessible(true); + method.invoke(loggerContext); + } catch (Exception exception) { + throw new IllegalStateException("Failed to reset logger context", exception); + } + } + + protected abstract InstrumentationExtension getTesting(); + + @Test + void logNoSpan() { + logger.info("log message 1"); + + executeAfterLogsExecution(); + + getTesting() + .waitAndAssertLogRecords( + logRecord -> + logRecord + .hasResource(resource) + .hasInstrumentationScope(instrumentationScopeInfo) + .hasBody("log message 1") + .hasTotalAttributeCount(4)); + } + + @Test + void logWithExtras() { + Instant start = Instant.now(); + String markerName = "aMarker"; + Marker marker = MarkerFactory.getMarker(markerName); + logger.info(marker, "log message 1", new IllegalStateException("Error!")); + + executeAfterLogsExecution(); + + Instant now = Instant.now(); + getTesting() + .waitAndAssertLogRecords( + logRecord -> { + logRecord + .hasResource(resource) + .hasInstrumentationScope(instrumentationScopeInfo) + .hasBody("log message 1") + .hasSeverity(Severity.INFO) + .hasSeverityText("INFO") + .hasAttributesSatisfyingExactly( + equalTo( + ExceptionAttributes.EXCEPTION_TYPE, + IllegalStateException.class.getName()), + equalTo(ExceptionAttributes.EXCEPTION_MESSAGE, "Error!"), + satisfies( + ExceptionAttributes.EXCEPTION_STACKTRACE, + stackTrace -> stackTrace.contains("logWithExtras")), + equalTo( + CodeIncubatingAttributes.CODE_FILEPATH, + AbstractOpenTelemetryAppenderTest.class.getSimpleName() + ".java"), + equalTo( + CodeIncubatingAttributes.CODE_NAMESPACE, + AbstractOpenTelemetryAppenderTest.class.getName()), + equalTo(CodeIncubatingAttributes.CODE_FUNCTION, "logWithExtras"), + satisfies( + CodeIncubatingAttributes.CODE_LINENO, lineNo -> lineNo.isGreaterThan(1)), + equalTo( + AttributeKey.stringArrayKey("logback.marker"), + Collections.singletonList(markerName))); + + LogRecordData logRecordData = AssertAccess.getActual(logRecord); + assertThat(logRecordData.getTimestampEpochNanos()) + .isGreaterThanOrEqualTo(TimeUnit.MILLISECONDS.toNanos(start.toEpochMilli())) + .isLessThanOrEqualTo( + TimeUnit.SECONDS.toNanos(now.getEpochSecond()) + now.getNano()); + }); + } + + @Test + void logContextData() { + MDC.put("key1", "val1"); + MDC.put("key2", "val2"); + try { + logger.info("log message 1"); + } finally { + MDC.clear(); + } + + executeAfterLogsExecution(); + + getTesting() + .waitAndAssertLogRecords( + logRecord -> + logRecord + .hasResource(resource) + .hasInstrumentationScope(instrumentationScopeInfo) + .hasBody("log message 1") + .hasTotalAttributeCount(2 + 4) // 4 code attributes + .hasAttributesSatisfying( + equalTo(AttributeKey.stringKey("key1"), "val1"), + equalTo(AttributeKey.stringKey("key2"), "val2"))); + } + + @Test + void logLoggerContext() { + LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory(); + loggerContext.putProperty("test-property", "test-value"); + try { + logger.info("log message 1"); + executeAfterLogsExecution(); + } finally { + resetLoggerContext(); + } + + getTesting() + .waitAndAssertLogRecords( + logRecord -> + logRecord + .hasResource(resource) + .hasInstrumentationScope(instrumentationScopeInfo) + .hasBody("log message 1") + .hasTotalAttributeCount(1 + 4) // 4 code attributes + .hasAttributesSatisfying( + equalTo(AttributeKey.stringKey("test-property"), "test-value"))); + } +} diff --git a/instrumentation/logback/logback-appender-1.0/library/src/test/java/io/opentelemetry/instrumentation/logback/appender/v1_0/LogReplayOpenTelemetryAppenderTest.java b/instrumentation/logback/logback-appender-1.0/library/src/test/java/io/opentelemetry/instrumentation/logback/appender/v1_0/LogReplayOpenTelemetryAppenderTest.java new file mode 100644 index 000000000000..0d5dab96f219 --- /dev/null +++ b/instrumentation/logback/logback-appender-1.0/library/src/test/java/io/opentelemetry/instrumentation/logback/appender/v1_0/LogReplayOpenTelemetryAppenderTest.java @@ -0,0 +1,79 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.logback.appender.v1_0; + +import ch.qos.logback.classic.LoggerContext; +import ch.qos.logback.classic.util.ContextInitializer; +import ch.qos.logback.core.spi.ContextAware; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.LibraryInstrumentationExtension; +import java.net.URL; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.slf4j.LoggerFactory; + +class LogReplayOpenTelemetryAppenderTest extends AbstractOpenTelemetryAppenderTest { + + @RegisterExtension + private static final LibraryInstrumentationExtension testing = + LibraryInstrumentationExtension.create(); + + @BeforeEach + void setup() throws Exception { + // to make sure we start fresh with a new OpenTelemetryAppender for each test + reloadLoggerConfiguration(); + } + + @Override + protected InstrumentationExtension getTesting() { + return testing; + } + + private static void reloadLoggerConfiguration() throws Exception { + LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory(); + loggerContext.reset(); + try { + Class configuratorClass = + Class.forName("ch.qos.logback.classic.util.DefaultJoranConfigurator"); + Object configurator = configuratorClass.getConstructor().newInstance(); + ((ContextAware) configurator).setContext(loggerContext); + configuratorClass + .getMethod("configure", LoggerContext.class) + .invoke(configurator, loggerContext); + } catch (Exception e) { + // logback versions prior to 1.3.0 + ContextInitializer ci = new ContextInitializer(loggerContext); + URL url = LogReplayOpenTelemetryAppenderTest.class.getResource("/logback-test.xml"); + ContextInitializer.class.getMethod("configureByResource", URL.class).invoke(ci, url); + // by default LoggerContext contains HOSTNAME property we clear it to start with empty context + resetLoggerContext(); + } + } + + @Override + void executeAfterLogsExecution() { + OpenTelemetryAppender.install(testing.getOpenTelemetry()); + } + + @Test + void twoLogs() { + logger.info("log message 1"); + logger.info( + "log message 2"); // Won't be instrumented because cache size is 1 (see logback-test.xml + // file) + + OpenTelemetryAppender.install(testing.getOpenTelemetry()); + + testing.waitAndAssertLogRecords( + logRecord -> + logRecord + .hasResource(resource) + .hasInstrumentationScope(instrumentationScopeInfo) + .hasBody("log message 1") + .hasTotalAttributeCount(4)); + } +} diff --git a/instrumentation/logback/logback-appender-1.0/library/src/test/java/io/opentelemetry/instrumentation/logback/appender/v1_0/OpenTelemetryAppenderConfigTest.java b/instrumentation/logback/logback-appender-1.0/library/src/test/java/io/opentelemetry/instrumentation/logback/appender/v1_0/OpenTelemetryAppenderConfigTest.java deleted file mode 100644 index 2fc5271141a8..000000000000 --- a/instrumentation/logback/logback-appender-1.0/library/src/test/java/io/opentelemetry/instrumentation/logback/appender/v1_0/OpenTelemetryAppenderConfigTest.java +++ /dev/null @@ -1,173 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.logback.appender.v1_0; - -import static org.assertj.core.api.Assertions.assertThat; - -import io.opentelemetry.api.GlobalOpenTelemetry; -import io.opentelemetry.api.common.AttributeKey; -import io.opentelemetry.api.logs.Severity; -import io.opentelemetry.api.trace.Span; -import io.opentelemetry.api.trace.SpanContext; -import io.opentelemetry.context.Scope; -import io.opentelemetry.sdk.OpenTelemetrySdk; -import io.opentelemetry.sdk.common.InstrumentationScopeInfo; -import io.opentelemetry.sdk.logs.SdkLoggerProvider; -import io.opentelemetry.sdk.logs.data.LogRecordData; -import io.opentelemetry.sdk.logs.export.SimpleLogRecordProcessor; -import io.opentelemetry.sdk.resources.Resource; -import io.opentelemetry.sdk.testing.exporter.InMemoryLogRecordExporter; -import io.opentelemetry.sdk.trace.SdkTracerProvider; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; -import java.time.Instant; -import java.util.Arrays; -import java.util.List; -import java.util.concurrent.TimeUnit; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.slf4j.MDC; -import org.slf4j.Marker; -import org.slf4j.MarkerFactory; - -class OpenTelemetryAppenderConfigTest { - - private static final Logger logger = LoggerFactory.getLogger("TestLogger"); - - private static InMemoryLogRecordExporter logRecordExporter; - private static Resource resource; - private static InstrumentationScopeInfo instrumentationScopeInfo; - - @BeforeAll - static void setupAll() { - logRecordExporter = InMemoryLogRecordExporter.create(); - resource = Resource.getDefault(); - instrumentationScopeInfo = InstrumentationScopeInfo.create("TestLogger"); - - SdkLoggerProvider loggerProvider = - SdkLoggerProvider.builder() - .setResource(resource) - .addLogRecordProcessor(SimpleLogRecordProcessor.create(logRecordExporter)) - .build(); - - GlobalOpenTelemetry.resetForTest(); - GlobalOpenTelemetry.set(OpenTelemetrySdk.builder().setLoggerProvider(loggerProvider).build()); - } - - @BeforeEach - void setup() { - logRecordExporter.reset(); - } - - @Test - void logNoSpan() { - logger.info("log message 1"); - - List logDataList = logRecordExporter.getFinishedLogRecordItems(); - assertThat(logDataList).hasSize(1); - LogRecordData logData = logDataList.get(0); - assertThat(logData.getResource()).isEqualTo(resource); - assertThat(logData.getInstrumentationScopeInfo()).isEqualTo(instrumentationScopeInfo); - assertThat(logData.getBody().asString()).isEqualTo("log message 1"); - assertThat(logData.getAttributes().size()).isEqualTo(4); // 4 code attributes - } - - @Test - void logWithSpan() { - Span span1 = runWithSpan("span1", () -> logger.info("log message 1")); - - logger.info("log message 2"); - - Span span2 = runWithSpan("span2", () -> logger.info("log message 3")); - - List logDataList = logRecordExporter.getFinishedLogRecordItems(); - assertThat(logDataList).hasSize(3); - assertThat(logDataList.get(0).getSpanContext()).isEqualTo(span1.getSpanContext()); - assertThat(logDataList.get(1).getSpanContext()).isEqualTo(SpanContext.getInvalid()); - assertThat(logDataList.get(2).getSpanContext()).isEqualTo(span2.getSpanContext()); - } - - private static Span runWithSpan(String spanName, Runnable runnable) { - Span span = SdkTracerProvider.builder().build().get("tracer").spanBuilder(spanName).startSpan(); - try (Scope ignored = span.makeCurrent()) { - runnable.run(); - } finally { - span.end(); - } - return span; - } - - @Test - void logWithExtras() { - Instant start = Instant.now(); - String markerName = "aMarker"; - Marker marker = MarkerFactory.getMarker(markerName); - logger.info(marker, "log message 1", new IllegalStateException("Error!")); - - List logDataList = logRecordExporter.getFinishedLogRecordItems(); - assertThat(logDataList).hasSize(1); - LogRecordData logData = logDataList.get(0); - assertThat(logData.getResource()).isEqualTo(resource); - assertThat(logData.getInstrumentationScopeInfo()).isEqualTo(instrumentationScopeInfo); - assertThat(logData.getBody().asString()).isEqualTo("log message 1"); - assertThat(logData.getTimestampEpochNanos()) - .isGreaterThanOrEqualTo(TimeUnit.MILLISECONDS.toNanos(start.toEpochMilli())) - .isLessThanOrEqualTo(TimeUnit.MILLISECONDS.toNanos(Instant.now().toEpochMilli())); - assertThat(logData.getSeverity()).isEqualTo(Severity.INFO); - assertThat(logData.getSeverityText()).isEqualTo("INFO"); - assertThat(logData.getAttributes().size()) - .isEqualTo(3 + 4 + 1); // 3 exception attributes, 4 code attributes, 1 marker attribute - assertThat(logData.getAttributes().get(SemanticAttributes.EXCEPTION_TYPE)) - .isEqualTo(IllegalStateException.class.getName()); - assertThat(logData.getAttributes().get(SemanticAttributes.EXCEPTION_MESSAGE)) - .isEqualTo("Error!"); - assertThat(logData.getAttributes().get(SemanticAttributes.EXCEPTION_STACKTRACE)) - .contains("logWithExtras"); - - String file = logData.getAttributes().get(SemanticAttributes.CODE_FILEPATH); - assertThat(file).isEqualTo("OpenTelemetryAppenderConfigTest.java"); - - String codeClass = logData.getAttributes().get(SemanticAttributes.CODE_NAMESPACE); - assertThat(codeClass) - .isEqualTo( - "io.opentelemetry.instrumentation.logback.appender.v1_0.OpenTelemetryAppenderConfigTest"); - - String method = logData.getAttributes().get(SemanticAttributes.CODE_FUNCTION); - assertThat(method).isEqualTo("logWithExtras"); - - Long lineNumber = logData.getAttributes().get(SemanticAttributes.CODE_LINENO); - assertThat(lineNumber).isGreaterThan(1); - - List logMarker = - logData.getAttributes().get(AttributeKey.stringArrayKey("logback.marker")); - assertThat(logMarker).isEqualTo(Arrays.asList(markerName)); - } - - @Test - void logContextData() { - MDC.put("key1", "val1"); - MDC.put("key2", "val2"); - try { - logger.info("log message 1"); - } finally { - MDC.clear(); - } - - List logDataList = logRecordExporter.getFinishedLogRecordItems(); - assertThat(logDataList).hasSize(1); - LogRecordData logData = logDataList.get(0); - assertThat(logData.getResource()).isEqualTo(resource); - assertThat(logData.getInstrumentationScopeInfo()).isEqualTo(instrumentationScopeInfo); - assertThat(logData.getBody().asString()).isEqualTo("log message 1"); - assertThat(logData.getAttributes().size()).isEqualTo(2 + 4); // 4 code attributes - assertThat(logData.getAttributes().get(AttributeKey.stringKey("logback.mdc.key1"))) - .isEqualTo("val1"); - assertThat(logData.getAttributes().get(AttributeKey.stringKey("logback.mdc.key2"))) - .isEqualTo("val2"); - } -} diff --git a/instrumentation/logback/logback-appender-1.0/library/src/test/java/io/opentelemetry/instrumentation/logback/appender/v1_0/OpenTelemetryAppenderTest.java b/instrumentation/logback/logback-appender-1.0/library/src/test/java/io/opentelemetry/instrumentation/logback/appender/v1_0/OpenTelemetryAppenderTest.java new file mode 100644 index 000000000000..6ed54d67537f --- /dev/null +++ b/instrumentation/logback/logback-appender-1.0/library/src/test/java/io/opentelemetry/instrumentation/logback/appender/v1_0/OpenTelemetryAppenderTest.java @@ -0,0 +1,62 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.logback.appender.v1_0; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.LibraryInstrumentationExtension; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +class OpenTelemetryAppenderTest extends AbstractOpenTelemetryAppenderTest { + + @RegisterExtension + private static final LibraryInstrumentationExtension testing = + LibraryInstrumentationExtension.create(); + + @BeforeEach + void setup() { + OpenTelemetryAppender.install(testing.getOpenTelemetry()); + } + + @Override + protected InstrumentationExtension getTesting() { + return testing; + } + + @Test + void logWithSpan() { // Does not work for log replay but it is not likely to occur because + // the log replay is related to the case where an OpenTelemetry object is not yet available + // at the time the log is executed (and if no OpenTelemetry is available, the context + // propagation can't happen) + Span span1 = + testing.runWithSpan( + "span1", + () -> { + logger.info("log message 1"); + return Span.current(); + }); + + logger.info("log message 2"); + + executeAfterLogsExecution(); + + Span span2 = + testing.runWithSpan( + "span2", + () -> { + logger.info("log message 3"); + return Span.current(); + }); + + testing.waitAndAssertLogRecords( + logRecord -> logRecord.hasSpanContext(span1.getSpanContext()), + logRecord -> logRecord.hasSpanContext(SpanContext.getInvalid()), + logRecord -> logRecord.hasSpanContext(span2.getSpanContext())); + } +} diff --git a/instrumentation/logback/logback-appender-1.0/library/src/test/java/io/opentelemetry/instrumentation/logback/appender/v1_0/internal/LoggingEventMapperTest.java b/instrumentation/logback/logback-appender-1.0/library/src/test/java/io/opentelemetry/instrumentation/logback/appender/v1_0/internal/LoggingEventMapperTest.java index 84b53284b512..1f43c1df62cc 100644 --- a/instrumentation/logback/logback-appender-1.0/library/src/test/java/io/opentelemetry/instrumentation/logback/appender/v1_0/internal/LoggingEventMapperTest.java +++ b/instrumentation/logback/logback-appender-1.0/library/src/test/java/io/opentelemetry/instrumentation/logback/appender/v1_0/internal/LoggingEventMapperTest.java @@ -6,7 +6,6 @@ package io.opentelemetry.instrumentation.logback.appender.v1_0.internal; import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; -import static java.util.Collections.emptyList; import static java.util.Collections.singletonList; import static org.assertj.core.api.Assertions.entry; @@ -22,7 +21,7 @@ class LoggingEventMapperTest { @Test void testDefault() { // given - LoggingEventMapper mapper = new LoggingEventMapper(false, emptyList(), false, false, false); + LoggingEventMapper mapper = LoggingEventMapper.builder().build(); Map contextData = new HashMap<>(); contextData.put("key1", "value1"); contextData.put("key2", "value2"); @@ -39,7 +38,7 @@ void testDefault() { void testSome() { // given LoggingEventMapper mapper = - new LoggingEventMapper(false, singletonList("key2"), false, false, false); + LoggingEventMapper.builder().setCaptureMdcAttributes(singletonList("key2")).build(); Map contextData = new HashMap<>(); contextData.put("key1", "value1"); contextData.put("key2", "value2"); @@ -49,15 +48,14 @@ void testSome() { mapper.captureMdcAttributes(attributes, contextData); // then - assertThat(attributes.build()) - .containsOnly(entry(AttributeKey.stringKey("logback.mdc.key2"), "value2")); + assertThat(attributes.build()).containsOnly(entry(AttributeKey.stringKey("key2"), "value2")); } @Test void testAll() { // given LoggingEventMapper mapper = - new LoggingEventMapper(false, singletonList("*"), false, false, false); + LoggingEventMapper.builder().setCaptureMdcAttributes(singletonList("*")).build(); Map contextData = new HashMap<>(); contextData.put("key1", "value1"); contextData.put("key2", "value2"); @@ -69,7 +67,7 @@ void testAll() { // then assertThat(attributes.build()) .containsOnly( - entry(AttributeKey.stringKey("logback.mdc.key1"), "value1"), - entry(AttributeKey.stringKey("logback.mdc.key2"), "value2")); + entry(AttributeKey.stringKey("key1"), "value1"), + entry(AttributeKey.stringKey("key2"), "value2")); } } diff --git a/instrumentation/logback/logback-appender-1.0/library/src/test/resources/logback-test.xml b/instrumentation/logback/logback-appender-1.0/library/src/test/resources/logback-test.xml index a3a4273a1222..7bc078e69050 100644 --- a/instrumentation/logback/logback-appender-1.0/library/src/test/resources/logback-test.xml +++ b/instrumentation/logback/logback-appender-1.0/library/src/test/resources/logback-test.xml @@ -13,7 +13,9 @@ false true true + true * + 1 diff --git a/instrumentation/logback/logback-mdc-1.0/javaagent/README.md b/instrumentation/logback/logback-mdc-1.0/javaagent/README.md index ab349cf6a269..5ec5bbafb6ac 100644 --- a/instrumentation/logback/logback-mdc-1.0/javaagent/README.md +++ b/instrumentation/logback/logback-mdc-1.0/javaagent/README.md @@ -1,5 +1,9 @@ # Settings for the Logback MDC instrumentation -| System property | Type | Default | Description | -|---|---|---|---| -| `otel.instrumentation.logback-mdc.add-baggage` | Boolean | `false` | Enable exposing baggage attributes through MDC. | +| System property | Type | Default | Description | +|-------------------------------------------------------|---------|---------------|--------------------------------------------------------------------| +| `otel.instrumentation.logback-mdc.add-baggage` | Boolean | `false` | Enable exposing baggage attributes through MDC. | +| `otel.instrumentation.common.mdc.resource-attributes` | String | | Comma separated list of resource attributes to expose through MDC. | +| `otel.instrumentation.common.logging.trace-id` | String | `trace_id` | Customize MDC key name for the trace id. | +| `otel.instrumentation.common.logging.span-id` | String | `span_id` | Customize MDC key name for the span id. | +| `otel.instrumentation.common.logging.trace-flags` | String | `trace_flags` | Customize MDC key name for the trace flags. | diff --git a/instrumentation/logback/logback-mdc-1.0/javaagent/build.gradle.kts b/instrumentation/logback/logback-mdc-1.0/javaagent/build.gradle.kts index 9d616d079f9a..a9352dd6f025 100644 --- a/instrumentation/logback/logback-mdc-1.0/javaagent/build.gradle.kts +++ b/instrumentation/logback/logback-mdc-1.0/javaagent/build.gradle.kts @@ -22,6 +22,18 @@ testing { } } + val loggingKeysTest by registering(JvmTestSuite::class) { + targets { + all { + testTask.configure { + jvmArgs("-Dotel.instrumentation.common.logging.trace-id=trace_id_test") + jvmArgs("-Dotel.instrumentation.common.logging.span-id=span_id_test") + jvmArgs("-Dotel.instrumentation.common.logging.trace-flags=trace_flags_test") + } + } + } + } + withType(JvmTestSuite::class) { dependencies { if (findProperty("testLatestDeps") as Boolean) { @@ -63,6 +75,10 @@ dependencies { } tasks { + test { + jvmArgs("-Dotel.instrumentation.common.mdc.resource-attributes=service.name,telemetry.sdk.language") + } + named("check") { dependsOn(testing.suites) } diff --git a/instrumentation/logback/logback-mdc-1.0/javaagent/src/addBaggageTest/groovy/io/opentelemetry/javaagent/instrumentation/logback/v1_0/LogbackWithBaggageTest.groovy b/instrumentation/logback/logback-mdc-1.0/javaagent/src/addBaggageTest/groovy/io/opentelemetry/javaagent/instrumentation/logback/v1_0/LogbackWithBaggageTest.groovy deleted file mode 100644 index e2468e46cc9a..000000000000 --- a/instrumentation/logback/logback-mdc-1.0/javaagent/src/addBaggageTest/groovy/io/opentelemetry/javaagent/instrumentation/logback/v1_0/LogbackWithBaggageTest.groovy +++ /dev/null @@ -1,12 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.javaagent.instrumentation.logback.v1_0 - -import io.opentelemetry.instrumentation.logback.mdc.v1_0.AbstractLogbackWithBaggageTest -import io.opentelemetry.instrumentation.test.AgentTestTrait - -class LogbackWithBaggageTest extends AbstractLogbackWithBaggageTest implements AgentTestTrait { -} diff --git a/instrumentation/logback/logback-mdc-1.0/javaagent/src/addBaggageTest/java/io/opentelemetry/javaagent/instrumentation/logback/v1_0/LogbackWithBaggageTest.java b/instrumentation/logback/logback-mdc-1.0/javaagent/src/addBaggageTest/java/io/opentelemetry/javaagent/instrumentation/logback/v1_0/LogbackWithBaggageTest.java new file mode 100644 index 000000000000..ada86492a46a --- /dev/null +++ b/instrumentation/logback/logback-mdc-1.0/javaagent/src/addBaggageTest/java/io/opentelemetry/javaagent/instrumentation/logback/v1_0/LogbackWithBaggageTest.java @@ -0,0 +1,27 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.logback.v1_0; + +import io.opentelemetry.instrumentation.logback.mdc.v1_0.AbstractLogbackTest; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import org.junit.jupiter.api.extension.RegisterExtension; + +class LogbackWithBaggageTest extends AbstractLogbackTest { + + @RegisterExtension + static InstrumentationExtension agentTesting = AgentInstrumentationExtension.create(); + + @Override + public InstrumentationExtension getInstrumentationExtension() { + return agentTesting; + } + + @Override + protected boolean expectBaggage() { + return true; + } +} diff --git a/instrumentation/logback/logback-mdc-1.0/javaagent/src/loggingKeysTest/java/io/opentelemetry/javaagent/instrumentation/logback/v1_0/LogbackWithLoggingKeysTest.java b/instrumentation/logback/logback-mdc-1.0/javaagent/src/loggingKeysTest/java/io/opentelemetry/javaagent/instrumentation/logback/v1_0/LogbackWithLoggingKeysTest.java new file mode 100644 index 000000000000..b74ee5d25817 --- /dev/null +++ b/instrumentation/logback/logback-mdc-1.0/javaagent/src/loggingKeysTest/java/io/opentelemetry/javaagent/instrumentation/logback/v1_0/LogbackWithLoggingKeysTest.java @@ -0,0 +1,27 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.logback.v1_0; + +import io.opentelemetry.instrumentation.logback.mdc.v1_0.AbstractLogbackTest; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import org.junit.jupiter.api.extension.RegisterExtension; + +class LogbackWithLoggingKeysTest extends AbstractLogbackTest { + + @RegisterExtension + static InstrumentationExtension agentTesting = AgentInstrumentationExtension.create(); + + @Override + public InstrumentationExtension getInstrumentationExtension() { + return agentTesting; + } + + @Override + protected boolean expectLoggingKeys() { + return true; + } +} diff --git a/instrumentation/logback/logback-mdc-1.0/javaagent/src/loggingKeysTest/resources/logback.xml b/instrumentation/logback/logback-mdc-1.0/javaagent/src/loggingKeysTest/resources/logback.xml new file mode 100644 index 000000000000..3434fbaaab59 --- /dev/null +++ b/instrumentation/logback/logback-mdc-1.0/javaagent/src/loggingKeysTest/resources/logback.xml @@ -0,0 +1,19 @@ + + + + + + + %coloredLevel %logger{15} - %message%n%xException{10} + + + + + + + + + + + + diff --git a/instrumentation/logback/logback-mdc-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/logback/mdc/v1_0/LogbackSingletons.java b/instrumentation/logback/logback-mdc-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/logback/mdc/v1_0/LogbackSingletons.java index a14cac81706e..0292488a8150 100644 --- a/instrumentation/logback/logback-mdc-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/logback/mdc/v1_0/LogbackSingletons.java +++ b/instrumentation/logback/logback-mdc-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/logback/mdc/v1_0/LogbackSingletons.java @@ -5,15 +5,32 @@ package io.opentelemetry.javaagent.instrumentation.logback.mdc.v1_0; -import io.opentelemetry.instrumentation.api.internal.ConfigPropertiesUtil; +import io.opentelemetry.javaagent.bootstrap.internal.AgentCommonConfig; +import io.opentelemetry.javaagent.bootstrap.internal.AgentInstrumentationConfig; public final class LogbackSingletons { private static final boolean ADD_BAGGAGE = - ConfigPropertiesUtil.getBoolean("otel.instrumentation.logback-mdc.add-baggage", false); + AgentInstrumentationConfig.get() + .getBoolean("otel.instrumentation.logback-mdc.add-baggage", false); + private static final String TRACE_ID_KEY = AgentCommonConfig.get().getTraceIdKey(); + private static final String SPAN_ID_KEY = AgentCommonConfig.get().getSpanIdKey(); + private static final String TRACE_FLAGS_KEY = AgentCommonConfig.get().getTraceFlagsKey(); public static boolean addBaggage() { return ADD_BAGGAGE; } + public static String traceIdKey() { + return TRACE_ID_KEY; + } + + public static String spanIdKey() { + return SPAN_ID_KEY; + } + + public static String traceFlagsKey() { + return TRACE_FLAGS_KEY; + } + private LogbackSingletons() {} } diff --git a/instrumentation/logback/logback-mdc-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/logback/mdc/v1_0/LoggingEventInstrumentation.java b/instrumentation/logback/logback-mdc-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/logback/mdc/v1_0/LoggingEventInstrumentation.java index c45a77984310..93768e2f9857 100644 --- a/instrumentation/logback/logback-mdc-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/logback/mdc/v1_0/LoggingEventInstrumentation.java +++ b/instrumentation/logback/logback-mdc-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/logback/mdc/v1_0/LoggingEventInstrumentation.java @@ -5,11 +5,11 @@ package io.opentelemetry.javaagent.instrumentation.logback.mdc.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.hasClassesNamed; import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.implementsInterface; +import static io.opentelemetry.javaagent.instrumentation.logback.mdc.v1_0.LogbackSingletons.spanIdKey; +import static io.opentelemetry.javaagent.instrumentation.logback.mdc.v1_0.LogbackSingletons.traceFlagsKey; +import static io.opentelemetry.javaagent.instrumentation.logback.mdc.v1_0.LogbackSingletons.traceIdKey; import static net.bytebuddy.matcher.ElementMatchers.isMethod; import static net.bytebuddy.matcher.ElementMatchers.isPublic; import static net.bytebuddy.matcher.ElementMatchers.named; @@ -24,6 +24,8 @@ import io.opentelemetry.instrumentation.api.util.VirtualField; import io.opentelemetry.instrumentation.logback.mdc.v1_0.internal.UnionMap; import io.opentelemetry.javaagent.bootstrap.Java8BytecodeBridge; +import io.opentelemetry.javaagent.bootstrap.internal.AgentCommonConfig; +import io.opentelemetry.javaagent.bootstrap.internal.ConfiguredResourceAttributesHolder; import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; import java.util.HashMap; @@ -61,7 +63,7 @@ public static void onExit( @Advice.This ILoggingEvent event, @Advice.Return(typing = Typing.DYNAMIC, readOnly = false) Map contextData) { - if (contextData != null && contextData.containsKey(TRACE_ID)) { + if (contextData != null && contextData.containsKey(AgentCommonConfig.get().getTraceIdKey())) { // Assume already instrumented event if traceId is present. return; } @@ -76,10 +78,11 @@ public static void onExit( SpanContext spanContext = Java8BytecodeBridge.spanFromContext(context).getSpanContext(); if (spanContext.isValid()) { - spanContextData.put(TRACE_ID, spanContext.getTraceId()); - spanContextData.put(SPAN_ID, spanContext.getSpanId()); - spanContextData.put(TRACE_FLAGS, spanContext.getTraceFlags().asHex()); + spanContextData.put(traceIdKey(), spanContext.getTraceId()); + spanContextData.put(spanIdKey(), spanContext.getSpanId()); + spanContextData.put(traceFlagsKey(), spanContext.getTraceFlags().asHex()); } + spanContextData.putAll(ConfiguredResourceAttributesHolder.getResourceAttributes()); if (LogbackSingletons.addBaggage()) { Baggage baggage = Java8BytecodeBridge.baggageFromContext(context); diff --git a/instrumentation/logback/logback-mdc-1.0/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/logback/v1_0/LogbackTest.groovy b/instrumentation/logback/logback-mdc-1.0/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/logback/v1_0/LogbackTest.groovy deleted file mode 100644 index a666d5f99a91..000000000000 --- a/instrumentation/logback/logback-mdc-1.0/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/logback/v1_0/LogbackTest.groovy +++ /dev/null @@ -1,12 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.javaagent.instrumentation.logback.v1_0 - -import io.opentelemetry.instrumentation.logback.mdc.v1_0.AbstractLogbackTest -import io.opentelemetry.instrumentation.test.AgentTestTrait - -class LogbackTest extends AbstractLogbackTest implements AgentTestTrait { -} diff --git a/instrumentation/logback/logback-mdc-1.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/logback/v1_0/LogbackTest.java b/instrumentation/logback/logback-mdc-1.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/logback/v1_0/LogbackTest.java new file mode 100644 index 000000000000..a3a062bf50ca --- /dev/null +++ b/instrumentation/logback/logback-mdc-1.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/logback/v1_0/LogbackTest.java @@ -0,0 +1,43 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.logback.v1_0; + +import static org.assertj.core.api.Assertions.assertThat; + +import ch.qos.logback.classic.spi.ILoggingEvent; +import io.opentelemetry.instrumentation.logback.mdc.v1_0.AbstractLogbackTest; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import java.util.List; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +class LogbackTest extends AbstractLogbackTest { + + @RegisterExtension + static InstrumentationExtension agentTesting = AgentInstrumentationExtension.create(); + + @Override + public InstrumentationExtension getInstrumentationExtension() { + return agentTesting; + } + + @Test + void resourceAttributes() { + logger.info("log message 1"); + + List events = listAppender.list; + + assertThat(events.size()).isEqualTo(1); + assertThat(events.get(0).getMessage()).isEqualTo("log message 1"); + assertThat(events.get(0).getMDCPropertyMap().get("trace_id")).isNull(); + assertThat(events.get(0).getMDCPropertyMap().get("span_id")).isNull(); + assertThat(events.get(0).getMDCPropertyMap().get("trace_flags")).isNull(); + assertThat(events.get(0).getMDCPropertyMap().get("service.name")) + .isEqualTo("unknown_service:java"); + assertThat(events.get(0).getMDCPropertyMap().get("telemetry.sdk.language")).isEqualTo("java"); + } +} diff --git a/instrumentation/logback/logback-mdc-1.0/library/README.md b/instrumentation/logback/logback-mdc-1.0/library/README.md index 7dbd4d155672..e2d1de2391d5 100644 --- a/instrumentation/logback/logback-mdc-1.0/library/README.md +++ b/instrumentation/logback/logback-mdc-1.0/library/README.md @@ -32,7 +32,7 @@ dependencies { ### Usage -logback.xml: +The following demonstrates how you might configure the appender in your `logback.xml` configuration: ```xml @@ -47,13 +47,36 @@ logback.xml: - ... + + + + + + ``` +> It's important to note you can also use other encoders in the `ConsoleAppender` like [logstash-logback-encoder](https://github.com/logfellow/logstash-logback-encoder). +> This can be helpful when the `Span` is invalid and the `trace_id`, `span_id`, and `trace_flags` are all `null` and are hidden entirely from the logs. + 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` + +These keys can be customized in your `logback.xml` configuration, for example: + +```xml + + example_trace_id + example_span_id + example_trace_flags + +``` + +If you set `true` in your `logback.xml` configuration, +key/value pairs in [baggage](https://opentelemetry.io/docs/concepts/signals/baggage/) will also be added to the MDC. + +- `baggage.` diff --git a/instrumentation/logback/logback-mdc-1.0/library/build.gradle.kts b/instrumentation/logback/logback-mdc-1.0/library/build.gradle.kts index 76537740d178..92bfced210fe 100644 --- a/instrumentation/logback/logback-mdc-1.0/library/build.gradle.kts +++ b/instrumentation/logback/logback-mdc-1.0/library/build.gradle.kts @@ -14,6 +14,18 @@ testing { } } + val loggingKeysTest by registering(JvmTestSuite::class) { + targets { + all { + testTask.configure { + jvmArgs("-Dotel.instrumentation.common.logging.trace-id=trace_id_test") + jvmArgs("-Dotel.instrumentation.common.logging.span-id=span_id_test") + jvmArgs("-Dotel.instrumentation.common.logging.trace-flags=trace_flags_test") + } + } + } + } + withType(JvmTestSuite::class) { dependencies { if (findProperty("testLatestDeps") as Boolean) { diff --git a/instrumentation/logback/logback-mdc-1.0/library/src/addBaggageTest/groovy/io/opentelemetry/instrumentation/logback/mdc/v1_0/LogbackWithBaggageTest.groovy b/instrumentation/logback/logback-mdc-1.0/library/src/addBaggageTest/groovy/io/opentelemetry/instrumentation/logback/mdc/v1_0/LogbackWithBaggageTest.groovy deleted file mode 100644 index b1ddcc750212..000000000000 --- a/instrumentation/logback/logback-mdc-1.0/library/src/addBaggageTest/groovy/io/opentelemetry/instrumentation/logback/mdc/v1_0/LogbackWithBaggageTest.groovy +++ /dev/null @@ -1,12 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.logback.mdc.v1_0 - - -import io.opentelemetry.instrumentation.test.LibraryTestTrait - -class LogbackWithBaggageTest extends AbstractLogbackWithBaggageTest implements LibraryTestTrait { -} diff --git a/instrumentation/logback/logback-mdc-1.0/library/src/addBaggageTest/java/io/opentelemetry/instrumentation/logback/mdc/v1_0/LogbackWithBaggageTest.java b/instrumentation/logback/logback-mdc-1.0/library/src/addBaggageTest/java/io/opentelemetry/instrumentation/logback/mdc/v1_0/LogbackWithBaggageTest.java new file mode 100644 index 000000000000..dd964a7458db --- /dev/null +++ b/instrumentation/logback/logback-mdc-1.0/library/src/addBaggageTest/java/io/opentelemetry/instrumentation/logback/mdc/v1_0/LogbackWithBaggageTest.java @@ -0,0 +1,26 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.logback.mdc.v1_0; + +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.LibraryInstrumentationExtension; +import org.junit.jupiter.api.extension.RegisterExtension; + +class LogbackWithBaggageTest extends AbstractLogbackTest { + + @RegisterExtension + static InstrumentationExtension testing = LibraryInstrumentationExtension.create(); + + @Override + public InstrumentationExtension getInstrumentationExtension() { + return testing; + } + + @Override + protected boolean expectBaggage() { + return true; + } +} diff --git a/instrumentation/logback/logback-mdc-1.0/library/src/loggingKeysTest/java/io/opentelemetry/instrumentation/logback/mdc/v1_0/LogbackWithLoggingKeysTest.java b/instrumentation/logback/logback-mdc-1.0/library/src/loggingKeysTest/java/io/opentelemetry/instrumentation/logback/mdc/v1_0/LogbackWithLoggingKeysTest.java new file mode 100644 index 000000000000..484491ae15b8 --- /dev/null +++ b/instrumentation/logback/logback-mdc-1.0/library/src/loggingKeysTest/java/io/opentelemetry/instrumentation/logback/mdc/v1_0/LogbackWithLoggingKeysTest.java @@ -0,0 +1,26 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.logback.mdc.v1_0; + +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.LibraryInstrumentationExtension; +import org.junit.jupiter.api.extension.RegisterExtension; + +class LogbackWithLoggingKeysTest extends AbstractLogbackTest { + + @RegisterExtension + static InstrumentationExtension testing = LibraryInstrumentationExtension.create(); + + @Override + public InstrumentationExtension getInstrumentationExtension() { + return testing; + } + + @Override + protected boolean expectLoggingKeys() { + return true; + } +} diff --git a/instrumentation/logback/logback-mdc-1.0/library/src/loggingKeysTest/resources/logback.xml b/instrumentation/logback/logback-mdc-1.0/library/src/loggingKeysTest/resources/logback.xml new file mode 100644 index 000000000000..088e37d267b0 --- /dev/null +++ b/instrumentation/logback/logback-mdc-1.0/library/src/loggingKeysTest/resources/logback.xml @@ -0,0 +1,33 @@ + + + + + + + + trace_id_test + span_id_test + trace_flags_test + + + + + + + + diff --git a/instrumentation/logback/logback-mdc-1.0/library/src/main/java/io/opentelemetry/instrumentation/logback/mdc/v1_0/OpenTelemetryAppender.java b/instrumentation/logback/logback-mdc-1.0/library/src/main/java/io/opentelemetry/instrumentation/logback/mdc/v1_0/OpenTelemetryAppender.java index f3c148b9647c..9ecc1ce69fe5 100644 --- a/instrumentation/logback/logback-mdc-1.0/library/src/main/java/io/opentelemetry/instrumentation/logback/mdc/v1_0/OpenTelemetryAppender.java +++ b/instrumentation/logback/logback-mdc-1.0/library/src/main/java/io/opentelemetry/instrumentation/logback/mdc/v1_0/OpenTelemetryAppender.java @@ -5,10 +5,6 @@ package io.opentelemetry.instrumentation.logback.mdc.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.classic.spi.LoggerContextVO; import ch.qos.logback.classic.spi.LoggingEventVO; @@ -20,6 +16,7 @@ import io.opentelemetry.api.trace.Span; import io.opentelemetry.api.trace.SpanContext; import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.incubator.log.LoggingContextConstants; import io.opentelemetry.instrumentation.logback.mdc.v1_0.internal.UnionMap; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Proxy; @@ -30,6 +27,9 @@ public class OpenTelemetryAppender extends UnsynchronizedAppenderBase implements AppenderAttachable { private boolean addBaggage; + private String traceIdKey = LoggingContextConstants.TRACE_ID; + private String spanIdKey = LoggingContextConstants.SPAN_ID; + private String traceFlagsKey = LoggingContextConstants.TRACE_FLAGS; private final AppenderAttachableImpl aai = new AppenderAttachableImpl<>(); @@ -44,9 +44,24 @@ public void setAddBaggage(boolean addBaggage) { this.addBaggage = addBaggage; } + /** Customize MDC key name for the trace id. */ + public void setTraceIdKey(String traceIdKey) { + this.traceIdKey = traceIdKey; + } + + /** Customize MDC key name for the span id. */ + public void setSpanIdKey(String spanIdKey) { + this.spanIdKey = spanIdKey; + } + + /** Customize MDC key name for the trace flags. */ + public void setTraceFlagsKey(String traceFlagsKey) { + this.traceFlagsKey = traceFlagsKey; + } + public ILoggingEvent wrapEvent(ILoggingEvent event) { Map eventContext = event.getMDCPropertyMap(); - if (eventContext != null && eventContext.containsKey(TRACE_ID)) { + if (eventContext != null && eventContext.containsKey(traceIdKey)) { // Assume already instrumented event if traceId is present. return event; } @@ -57,9 +72,9 @@ public ILoggingEvent wrapEvent(ILoggingEvent event) { if (currentSpan.getSpanContext().isValid()) { SpanContext spanContext = currentSpan.getSpanContext(); - contextData.put(TRACE_ID, spanContext.getTraceId()); - contextData.put(SPAN_ID, spanContext.getSpanId()); - contextData.put(TRACE_FLAGS, spanContext.getTraceFlags().asHex()); + contextData.put(traceIdKey, spanContext.getTraceId()); + contextData.put(spanIdKey, spanContext.getSpanId()); + contextData.put(traceFlagsKey, spanContext.getTraceFlags().asHex()); } if (addBaggage) { diff --git a/instrumentation/logback/logback-mdc-1.0/library/src/test/groovy/io/opentelemetry/instrumentation/logback/mdc/v1_0/LogbackTest.groovy b/instrumentation/logback/logback-mdc-1.0/library/src/test/groovy/io/opentelemetry/instrumentation/logback/mdc/v1_0/LogbackTest.groovy deleted file mode 100644 index 3bd3f72f39cc..000000000000 --- a/instrumentation/logback/logback-mdc-1.0/library/src/test/groovy/io/opentelemetry/instrumentation/logback/mdc/v1_0/LogbackTest.groovy +++ /dev/null @@ -1,11 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.logback.mdc.v1_0 - -import io.opentelemetry.instrumentation.test.LibraryTestTrait - -class LogbackTest extends AbstractLogbackTest implements LibraryTestTrait { -} diff --git a/instrumentation/logback/logback-mdc-1.0/library/src/test/groovy/io/opentelemetry/instrumentation/logback/mdc/v1_0/internal/UnionMapTest.groovy b/instrumentation/logback/logback-mdc-1.0/library/src/test/groovy/io/opentelemetry/instrumentation/logback/mdc/v1_0/internal/UnionMapTest.groovy deleted file mode 100644 index 6c8709bb93f4..000000000000 --- a/instrumentation/logback/logback-mdc-1.0/library/src/test/groovy/io/opentelemetry/instrumentation/logback/mdc/v1_0/internal/UnionMapTest.groovy +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.logback.mdc.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/instrumentation/logback/logback-mdc-1.0/library/src/test/java/io/opentelemetry/instrumentation/logback/mdc/v1_0/LogbackTest.java b/instrumentation/logback/logback-mdc-1.0/library/src/test/java/io/opentelemetry/instrumentation/logback/mdc/v1_0/LogbackTest.java new file mode 100644 index 000000000000..d0de1c10e672 --- /dev/null +++ b/instrumentation/logback/logback-mdc-1.0/library/src/test/java/io/opentelemetry/instrumentation/logback/mdc/v1_0/LogbackTest.java @@ -0,0 +1,21 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.logback.mdc.v1_0; + +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.LibraryInstrumentationExtension; +import org.junit.jupiter.api.extension.RegisterExtension; + +class LogbackTest extends AbstractLogbackTest { + + @RegisterExtension + static InstrumentationExtension testing = LibraryInstrumentationExtension.create(); + + @Override + public InstrumentationExtension getInstrumentationExtension() { + return testing; + } +} diff --git a/instrumentation/logback/logback-mdc-1.0/library/src/test/java/io/opentelemetry/instrumentation/logback/mdc/v1_0/internal/UnionMapTest.java b/instrumentation/logback/logback-mdc-1.0/library/src/test/java/io/opentelemetry/instrumentation/logback/mdc/v1_0/internal/UnionMapTest.java new file mode 100644 index 000000000000..1656c55f41c1 --- /dev/null +++ b/instrumentation/logback/logback-mdc-1.0/library/src/test/java/io/opentelemetry/instrumentation/logback/mdc/v1_0/internal/UnionMapTest.java @@ -0,0 +1,104 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.logback.mdc.v1_0.internal; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.google.common.collect.ImmutableMap; +import java.util.Collections; +import java.util.Map; +import java.util.Set; +import java.util.stream.Stream; +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 UnionMapTest { + + @ParameterizedTest + @MethodSource("providesMapsArguments") + void testMaps(Map first, Map second) { + UnionMap union = new UnionMap<>(first, second); + + assertThat(union.get("cat")).isEqualTo("meow"); + assertThat(union.get("dog")).isEqualTo("bark"); + assertThat(union.get("foo")).isEqualTo("bar"); + assertThat(union.get("hello")).isEqualTo("world"); + assertThat(union.get("giraffe")).isNull(); + + assertThat(union.isEmpty()).isFalse(); + assertThat(union.size()).isEqualTo(4); + assertThat(union.containsKey("cat")).isTrue(); + assertThat(union.containsKey("dog")).isTrue(); + assertThat(union.containsKey("foo")).isTrue(); + assertThat(union.containsKey("hello")).isTrue(); + assertThat(union.containsKey("giraffe")).isFalse(); + + Set> set = union.entrySet(); + assertThat(set.isEmpty()).isFalse(); + assertThat(set.size()).isEqualTo(4); + assertThat(set.toArray().length).isEqualTo(4); + } + + private static Stream providesMapsArguments() { + ImmutableMap firstArg = + ImmutableMap.of( + "cat", "meow", + "dog", "bark"); + + return Stream.of( + Arguments.of( + firstArg, + ImmutableMap.of( + "foo", "bar", + "hello", "world")), + Arguments.of( + firstArg, + ImmutableMap.of( + "foo", "bar", + "hello", "world", + "cat", "moo"))); + } + + @Test + void testBothEmpty() { + UnionMap union = new UnionMap<>(Collections.emptyMap(), Collections.emptyMap()); + + assertThat(union.isEmpty()).isTrue(); + assertThat(union.size()).isEqualTo(0); + assertThat(union.get("cat")).isNull(); + + Set> set = union.entrySet(); + assertThat(set.isEmpty()).isTrue(); + assertThat(set.size()).isEqualTo(0); + + assertThat(set.toArray().length).isEqualTo(0); + } + + @ParameterizedTest + @MethodSource("providesOneEmptyArguments") + void testOneEmpty(Map first, Map second) { + UnionMap union = new UnionMap<>(first, second); + + assertThat(union.isEmpty()).isFalse(); + assertThat(union.size()).isEqualTo(1); + assertThat(union.get("cat")).isEqualTo("meow"); + assertThat(union.get("dog")).isNull(); + + Set> set = union.entrySet(); + assertThat(set.isEmpty()).isFalse(); + assertThat(set.size()).isEqualTo(1); + + assertThat(set.toArray().length).isEqualTo(1); + } + + private static Stream providesOneEmptyArguments() { + return Stream.of( + Arguments.of(ImmutableMap.of("cat", "meow"), Collections.emptyMap()), + Arguments.of(Collections.emptyMap(), ImmutableMap.of("cat", "meow"))); + } +} diff --git a/instrumentation/logback/logback-mdc-1.0/testing/build.gradle.kts b/instrumentation/logback/logback-mdc-1.0/testing/build.gradle.kts index c6c0d80341ec..31b6162f0acd 100644 --- a/instrumentation/logback/logback-mdc-1.0/testing/build.gradle.kts +++ b/instrumentation/logback/logback-mdc-1.0/testing/build.gradle.kts @@ -10,8 +10,5 @@ dependencies { api("ch.qos.logback:logback-classic:1.0.0") implementation("com.google.guava:guava") - - implementation("org.apache.groovy:groovy") implementation("io.opentelemetry:opentelemetry-api") - implementation("org.spockframework:spock-core") } diff --git a/instrumentation/logback/logback-mdc-1.0/testing/src/main/groovy/io/opentelemetry/instrumentation/logback/mdc/v1_0/AbstractLogbackTest.groovy b/instrumentation/logback/logback-mdc-1.0/testing/src/main/groovy/io/opentelemetry/instrumentation/logback/mdc/v1_0/AbstractLogbackTest.groovy deleted file mode 100644 index 93212d192bef..000000000000 --- a/instrumentation/logback/logback-mdc-1.0/testing/src/main/groovy/io/opentelemetry/instrumentation/logback/mdc/v1_0/AbstractLogbackTest.groovy +++ /dev/null @@ -1,120 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.logback.mdc.v1_0 - -import ch.qos.logback.classic.spi.ILoggingEvent -import ch.qos.logback.core.read.ListAppender -import io.opentelemetry.api.baggage.Baggage -import io.opentelemetry.api.trace.Span -import io.opentelemetry.instrumentation.test.InstrumentationSpecification -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: - Baggage baggage = Baggage.empty().toBuilder().put("baggage_key", "baggage_value").build() - - runWithBaggage(baggage) { - AbstractLogbackTest.logger.info("log message 1") - AbstractLogbackTest.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[0].getMDCPropertyMap().get("baggage.baggage_key") == (expectBaggage() ? "baggage_value" : 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 - events[1].getMDCPropertyMap().get("baggage.baggage_key") == (expectBaggage() ? "baggage_value" : null) - } - - def "ids when span"() { - when: - Baggage baggage = Baggage.empty().toBuilder().put("baggage_key", "baggage_value").build() - - Span span1 = runWithSpanAndBaggage("test", baggage) { - AbstractLogbackTest.logger.info("log message 1") - } - - logger.info("log message 2") - - Span span2 = runWithSpanAndBaggage("test 2", baggage) { - AbstractLogbackTest.logger.info("log message 3") - } - - 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[0].getMDCPropertyMap().get("baggage.baggage_key") == (expectBaggage() ? "baggage_value" : 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 - events[1].getMDCPropertyMap().get("baggage.baggage_key") == 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" - events[2].getMDCPropertyMap().get("baggage.baggage_key") == (expectBaggage() ? "baggage_value" : null) - } - - Span runWithSpanAndBaggage(String spanName, Baggage baggage, Closure callback) { - return runWithSpan(spanName) { - runWithBaggage(baggage, callback) - Span.current() - } - } - - void runWithBaggage(Baggage baggage, Closure callback) { - try (var unusedScope = baggage.makeCurrent()) { - callback.call() - } - } - - boolean expectBaggage() { - return false - } -} diff --git a/instrumentation/logback/logback-mdc-1.0/testing/src/main/groovy/io/opentelemetry/instrumentation/logback/mdc/v1_0/AbstractLogbackWithBaggageTest.groovy b/instrumentation/logback/logback-mdc-1.0/testing/src/main/groovy/io/opentelemetry/instrumentation/logback/mdc/v1_0/AbstractLogbackWithBaggageTest.groovy deleted file mode 100644 index 66762efeac06..000000000000 --- a/instrumentation/logback/logback-mdc-1.0/testing/src/main/groovy/io/opentelemetry/instrumentation/logback/mdc/v1_0/AbstractLogbackWithBaggageTest.groovy +++ /dev/null @@ -1,13 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.logback.mdc.v1_0 - -abstract class AbstractLogbackWithBaggageTest extends AbstractLogbackTest { - @Override - boolean expectBaggage() { - return true - } -} diff --git a/instrumentation/logback/logback-mdc-1.0/testing/src/main/java/io/opentelemetry/instrumentation/logback/mdc/v1_0/AbstractLogbackTest.java b/instrumentation/logback/logback-mdc-1.0/testing/src/main/java/io/opentelemetry/instrumentation/logback/mdc/v1_0/AbstractLogbackTest.java new file mode 100644 index 000000000000..2d175f847123 --- /dev/null +++ b/instrumentation/logback/logback-mdc-1.0/testing/src/main/java/io/opentelemetry/instrumentation/logback/mdc/v1_0/AbstractLogbackTest.java @@ -0,0 +1,145 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.logback.mdc.v1_0; + +import static org.assertj.core.api.Assertions.assertThat; + +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.Appender; +import ch.qos.logback.core.read.ListAppender; +import io.opentelemetry.api.baggage.Baggage; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Scope; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import java.util.List; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public abstract class AbstractLogbackTest { + + protected static final Logger logger = LoggerFactory.getLogger("test"); + + protected static ListAppender listAppender = new ListAppender<>(); + + protected final Baggage baggage = + Baggage.empty().toBuilder().put("baggage_key", "baggage_value").build(); + + @BeforeAll + static void setUp() { + ch.qos.logback.classic.Logger logbackLogger = (ch.qos.logback.classic.Logger) logger; + Appender topLevelListAppender = logbackLogger.getAppender("LIST"); + if (topLevelListAppender != null) { + // Auto instrumentation test + listAppender = (ListAppender) topLevelListAppender; + } else { + // Library instrumentation test. + OpenTelemetryAppender otelAppender = + (OpenTelemetryAppender) logbackLogger.getAppender("OTEL"); + listAppender = (ListAppender) otelAppender.getAppender("LIST"); + } + } + + @BeforeEach + void setUpData() { + listAppender.list.clear(); + } + + protected abstract InstrumentationExtension getInstrumentationExtension(); + + @Test + void testNoIdsWhenNoSpan() { + runWithBaggage( + baggage, + () -> { + logger.info("log message 1"); + logger.info("log message 2"); + }); + + List events = listAppender.list; + + assertThat(events.size()).isEqualTo(2); + assertThat(events.get(0).getMessage()).isEqualTo("log message 1"); + assertThat(events.get(0).getMDCPropertyMap().get(getLoggingKey("trace_id"))).isNull(); + assertThat(events.get(0).getMDCPropertyMap().get(getLoggingKey("span_id"))).isNull(); + assertThat(events.get(0).getMDCPropertyMap().get(getLoggingKey("trace_flags"))).isNull(); + assertThat(events.get(0).getMDCPropertyMap().get("baggage.baggage_key")) + .isEqualTo(expectBaggage() ? "baggage_value" : null); + + assertThat(events.get(1).getMessage()).isEqualTo("log message 2"); + assertThat(events.get(1).getMDCPropertyMap().get(getLoggingKey("trace_id"))).isNull(); + assertThat(events.get(1).getMDCPropertyMap().get(getLoggingKey("span_id"))).isNull(); + assertThat(events.get(1).getMDCPropertyMap().get(getLoggingKey("trace_flags"))).isNull(); + assertThat(events.get(1).getMDCPropertyMap().get("baggage.baggage_key")) + .isEqualTo(expectBaggage() ? "baggage_value" : null); + } + + @Test + void testIdsWhenSpan() { + Span span1 = runWithSpanAndBaggage("test", baggage, () -> logger.info("log message 1")); + + logger.info("log message 2"); + + Span span2 = runWithSpanAndBaggage("test 2", baggage, () -> logger.info("log message 3")); + + List events = listAppender.list; + + assertThat(events.size()).isEqualTo(3); + assertThat(events.get(0).getMessage()).isEqualTo("log message 1"); + assertThat(events.get(0).getMDCPropertyMap().get(getLoggingKey("trace_id"))) + .isEqualTo(span1.getSpanContext().getTraceId()); + assertThat(events.get(0).getMDCPropertyMap().get(getLoggingKey("span_id"))) + .isEqualTo(span1.getSpanContext().getSpanId()); + assertThat(events.get(0).getMDCPropertyMap().get(getLoggingKey("trace_flags"))).isEqualTo("01"); + assertThat(events.get(0).getMDCPropertyMap().get("baggage.baggage_key")) + .isEqualTo(expectBaggage() ? "baggage_value" : null); + + assertThat(events.get(1).getMessage()).isEqualTo("log message 2"); + assertThat(events.get(1).getMDCPropertyMap().get(getLoggingKey("trace_id"))).isNull(); + assertThat(events.get(1).getMDCPropertyMap().get(getLoggingKey("span_id"))).isNull(); + assertThat(events.get(1).getMDCPropertyMap().get(getLoggingKey("trace_flags"))).isNull(); + assertThat(events.get(1).getMDCPropertyMap().get("baggage.baggage_key")).isNull(); + + assertThat(events.get(2).getMessage()).isEqualTo("log message 3"); + assertThat(events.get(2).getMDCPropertyMap().get(getLoggingKey("trace_id"))) + .isEqualTo(span2.getSpanContext().getTraceId()); + assertThat(events.get(2).getMDCPropertyMap().get(getLoggingKey("span_id"))) + .isEqualTo(span2.getSpanContext().getSpanId()); + assertThat(events.get(2).getMDCPropertyMap().get(getLoggingKey("trace_flags"))).isEqualTo("01"); + assertThat(events.get(2).getMDCPropertyMap().get("baggage.baggage_key")) + .isEqualTo(expectBaggage() ? "baggage_value" : null); + } + + void runWithBaggage(Baggage baggage, Runnable runnable) { + try (Scope unusedScope = baggage.makeCurrent()) { + runnable.run(); + } + } + + Span runWithSpanAndBaggage(String spanName, Baggage baggage, Runnable runnable) { + return getInstrumentationExtension() + .runWithSpan( + spanName, + () -> { + runWithBaggage(baggage, runnable); + return Span.current(); + }); + } + + protected boolean expectBaggage() { + return false; + } + + protected boolean expectLoggingKeys() { + return false; + } + + private String getLoggingKey(String key) { + return expectLoggingKeys() ? key + "_test" : key; + } +} diff --git a/instrumentation/methods/README.md b/instrumentation/methods/README.md index 217691c322a7..7ecc008ab62d 100644 --- a/instrumentation/methods/README.md +++ b/instrumentation/methods/README.md @@ -1,7 +1,7 @@ # Settings for the methods instrumentation -| System property | Type | Default | Description | -|----------------- |------ |--------- |------------- | -| `otel.instrumentation.methods.include` | String| None | List of methods to include for tracing. For more information, see [Creating spans around methods with `otel.instrumentation.methods.include`][cs]. +| System property | Type | Default | Description | +| -------------------------------------- | ------ | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | +| `otel.instrumentation.methods.include` | String | None | List of methods to include for tracing. For more information, see [Creating spans around methods with `otel.instrumentation.methods.include`][cs]. | -[cs]: https://opentelemetry.io/docs/instrumentation/java/annotations/#creating-spans-around-methods-with-otelinstrumentationmethodsinclude +[cs]: https://opentelemetry.io/docs/zero-code/java/agent/annotations/#creating-spans-around-methods-with-otelinstrumentationmethodsinclude diff --git a/instrumentation/methods/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/methods/MethodInstrumentation.java b/instrumentation/methods/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/methods/MethodInstrumentation.java index 95880d990d81..126101bd1caa 100644 --- a/instrumentation/methods/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/methods/MethodInstrumentation.java +++ b/instrumentation/methods/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/methods/MethodInstrumentation.java @@ -15,7 +15,7 @@ import io.opentelemetry.context.Context; import io.opentelemetry.context.Scope; import io.opentelemetry.instrumentation.api.annotation.support.async.AsyncOperationEndSupport; -import io.opentelemetry.instrumentation.api.instrumenter.util.ClassAndMethod; +import io.opentelemetry.instrumentation.api.incubator.semconv.util.ClassAndMethod; import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; import java.lang.reflect.Method; @@ -79,6 +79,9 @@ public static void stopSpan( @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(); returnValue = diff --git a/instrumentation/methods/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/methods/MethodInstrumentationModule.java b/instrumentation/methods/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/methods/MethodInstrumentationModule.java index 38f76de04f71..120088cc77c2 100644 --- a/instrumentation/methods/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/methods/MethodInstrumentationModule.java +++ b/instrumentation/methods/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/methods/MethodInstrumentationModule.java @@ -9,7 +9,7 @@ import static java.util.Collections.singletonList; import com.google.auto.service.AutoService; -import io.opentelemetry.javaagent.bootstrap.internal.InstrumentationConfig; +import io.opentelemetry.javaagent.bootstrap.internal.AgentInstrumentationConfig; import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; import io.opentelemetry.javaagent.tooling.config.MethodsConfigurationParser; @@ -30,7 +30,7 @@ public MethodInstrumentationModule() { Map> classMethodsToTrace = MethodsConfigurationParser.parse( - InstrumentationConfig.get().getString(TRACE_METHODS_CONFIG)); + AgentInstrumentationConfig.get().getString(TRACE_METHODS_CONFIG)); typeInstrumentations = classMethodsToTrace.entrySet().stream() diff --git a/instrumentation/methods/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/methods/MethodSingletons.java b/instrumentation/methods/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/methods/MethodSingletons.java index a50893bbb656..953da28464d4 100644 --- a/instrumentation/methods/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/methods/MethodSingletons.java +++ b/instrumentation/methods/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/methods/MethodSingletons.java @@ -6,12 +6,12 @@ package io.opentelemetry.javaagent.instrumentation.methods; import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.instrumentation.api.incubator.semconv.code.CodeAttributesExtractor; +import io.opentelemetry.instrumentation.api.incubator.semconv.code.CodeAttributesGetter; +import io.opentelemetry.instrumentation.api.incubator.semconv.code.CodeSpanNameExtractor; +import io.opentelemetry.instrumentation.api.incubator.semconv.util.ClassAndMethod; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; import io.opentelemetry.instrumentation.api.instrumenter.SpanKindExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.code.CodeAttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.code.CodeAttributesGetter; -import io.opentelemetry.instrumentation.api.instrumenter.code.CodeSpanNameExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.util.ClassAndMethod; public final class MethodSingletons { private static final String INSTRUMENTATION_NAME = "io.opentelemetry.methods"; diff --git a/instrumentation/methods/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/methods/MethodTest.java b/instrumentation/methods/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/methods/MethodTest.java index 41c75e870c75..846ec64410e7 100644 --- a/instrumentation/methods/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/methods/MethodTest.java +++ b/instrumentation/methods/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/methods/MethodTest.java @@ -7,8 +7,8 @@ import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.CODE_FUNCTION; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.CODE_NAMESPACE; +import static io.opentelemetry.semconv.incubating.CodeIncubatingAttributes.CODE_FUNCTION; +import static io.opentelemetry.semconv.incubating.CodeIncubatingAttributes.CODE_NAMESPACE; import io.opentelemetry.api.trace.Span; import io.opentelemetry.api.trace.SpanKind; diff --git a/instrumentation/micrometer/micrometer-1.5/README.md b/instrumentation/micrometer/micrometer-1.5/javaagent/README.md similarity index 76% rename from instrumentation/micrometer/micrometer-1.5/README.md rename to instrumentation/micrometer/micrometer-1.5/javaagent/README.md index 8978c6ffedeb..d21c6fec4530 100644 --- a/instrumentation/micrometer/micrometer-1.5/README.md +++ b/instrumentation/micrometer/micrometer-1.5/javaagent/README.md @@ -1,7 +1,7 @@ # Settings for the Micrometer bridge instrumentation | System property | Type | Default | Description | -|------------------------------------------------------------|---------|---------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| ---------------------------------------------------------- |---------| ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `otel.instrumentation.micrometer.base-time-unit` | String | `s` | Set the base time unit for the OpenTelemetry `MeterRegistry` implementation.
    Valid values`ns`, `nanoseconds`, `us`, `microseconds`, `ms`, `milliseconds`, `s`, `seconds`, `min`, `minutes`, `h`, `hours`, `d`, `days`
    | -| `otel.instrumentation.micrometer.prometheus-mode.enabled` | boolean | false | Enable the "Prometheus mode" this will simulate the behavior of Micrometer's PrometheusMeterRegistry. The instruments will be renamed to match Micrometer instrument naming, and the base time unit will be set to seconds. | -| `otel.instrumentation.micrometer.histogram-gauges.enabled` | boolean | false | Enables the generation of gauge-based Micrometer histograms for `DistributionSummary` and `Timer` instruments. | +| `otel.instrumentation.micrometer.prometheus-mode.enabled` | Boolean | `false` | Enable the "Prometheus mode" this will simulate the behavior of Micrometer's PrometheusMeterRegistry. The instruments will be renamed to match Micrometer instrument naming, and the base time unit will be set to seconds. | +| `otel.instrumentation.micrometer.histogram-gauges.enabled` | Boolean | `false` | Enables the generation of gauge-based Micrometer histograms for `DistributionSummary` and `Timer` instruments. | diff --git a/instrumentation/micrometer/micrometer-1.5/javaagent/build.gradle.kts b/instrumentation/micrometer/micrometer-1.5/javaagent/build.gradle.kts index 4711af48edcf..66ab5db32fdc 100644 --- a/instrumentation/micrometer/micrometer-1.5/javaagent/build.gradle.kts +++ b/instrumentation/micrometer/micrometer-1.5/javaagent/build.gradle.kts @@ -57,4 +57,8 @@ tasks { dependsOn(testPrometheusMode) dependsOn(testHistogramGauges) } + + withType().configureEach { + jvmArgs("-Dotel.instrumentation.micrometer.enabled=true") + } } diff --git a/instrumentation/micrometer/micrometer-1.5/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/micrometer/v1_5/MicrometerInstrumentationModule.java b/instrumentation/micrometer/micrometer-1.5/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/micrometer/v1_5/MicrometerInstrumentationModule.java index 22e6e0096711..ec72937bc8c5 100644 --- a/instrumentation/micrometer/micrometer-1.5/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/micrometer/v1_5/MicrometerInstrumentationModule.java +++ b/instrumentation/micrometer/micrometer-1.5/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/micrometer/v1_5/MicrometerInstrumentationModule.java @@ -10,6 +10,7 @@ import com.google.auto.service.AutoService; import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; import java.util.Collections; import java.util.List; import net.bytebuddy.matcher.ElementMatcher; @@ -21,6 +22,12 @@ public MicrometerInstrumentationModule() { super("micrometer", "micrometer-1.5"); } + @Override + public boolean defaultEnabled(ConfigProperties config) { + // produces a lot of metrics that are already captured - e.g. JVM memory usage + return false; + } + @Override public ElementMatcher.Junction classLoaderMatcher() { // added in 1.5 diff --git a/instrumentation/micrometer/micrometer-1.5/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/micrometer/v1_5/MicrometerSingletons.java b/instrumentation/micrometer/micrometer-1.5/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/micrometer/v1_5/MicrometerSingletons.java index 8416c35fe9f2..5c08575d0659 100644 --- a/instrumentation/micrometer/micrometer-1.5/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/micrometer/v1_5/MicrometerSingletons.java +++ b/instrumentation/micrometer/micrometer-1.5/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/micrometer/v1_5/MicrometerSingletons.java @@ -7,15 +7,16 @@ import io.micrometer.core.instrument.MeterRegistry; import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.instrumentation.api.incubator.config.internal.InstrumentationConfig; import io.opentelemetry.instrumentation.micrometer.v1_5.OpenTelemetryMeterRegistry; -import io.opentelemetry.javaagent.bootstrap.internal.InstrumentationConfig; +import io.opentelemetry.javaagent.bootstrap.internal.AgentInstrumentationConfig; public final class MicrometerSingletons { private static final MeterRegistry METER_REGISTRY; static { - InstrumentationConfig config = InstrumentationConfig.get(); + InstrumentationConfig config = AgentInstrumentationConfig.get(); METER_REGISTRY = OpenTelemetryMeterRegistry.builder(GlobalOpenTelemetry.get()) .setPrometheusMode( diff --git a/instrumentation/micrometer/micrometer-1.5/library/README.md b/instrumentation/micrometer/micrometer-1.5/library/README.md new file mode 100644 index 000000000000..938c0047345c --- /dev/null +++ b/instrumentation/micrometer/micrometer-1.5/library/README.md @@ -0,0 +1,38 @@ +# Micrometer Instrumentation for Micrometer version 1.5 and higher + +This module provides a [Micrometer registry](https://micrometer.io/docs/concepts#_registry) which +sends Micrometer metrics to the +[OpenTelemetry Metrics SDK](https://github.com/open-telemetry/opentelemetry-java/tree/main/sdk/metrics). + +## Quickstart + +### Add these dependencies to your project + +Replace `OPENTELEMETRY_VERSION` with the [latest +release](https://search.maven.org/search?q=g:io.opentelemetry.instrumentation%20AND%20a:opentelemetry-micrometer-1.5). + +For Maven, add to your `pom.xml` dependencies: + +```xml + + + io.opentelemetry.instrumentation + opentelemetry-micrometer-1.5 + OPENTELEMETRY_VERSION + + +``` + +For Gradle, add to your dependencies: + +```groovy +implementation("io.opentelemetry.instrumentation:opentelemetry-micrometer-1.5:OPENTELEMETRY_VERSION") +``` + +### Usage + +The instrumentation library provides an implementation of `MeterRegistry` to bridge Micrometer API to OpenTelemetry Metrics. + +```java +MeterRegistry meterRegistry = OpenTelemetryMeterRegistry.builder(openTelemetry).build(); +``` diff --git a/instrumentation/micrometer/micrometer-1.5/library/build.gradle.kts b/instrumentation/micrometer/micrometer-1.5/library/build.gradle.kts index 997677f7de3f..a8782fcac158 100644 --- a/instrumentation/micrometer/micrometer-1.5/library/build.gradle.kts +++ b/instrumentation/micrometer/micrometer-1.5/library/build.gradle.kts @@ -3,7 +3,7 @@ plugins { } dependencies { - compileOnly("io.opentelemetry:opentelemetry-extension-incubator") + compileOnly("io.opentelemetry:opentelemetry-api-incubator") library("io.micrometer:micrometer-core:1.5.0") diff --git a/instrumentation/micrometer/micrometer-1.5/library/src/main/java/io/opentelemetry/instrumentation/micrometer/v1_5/DistributionStatisticConfigModifier.java b/instrumentation/micrometer/micrometer-1.5/library/src/main/java/io/opentelemetry/instrumentation/micrometer/v1_5/DistributionStatisticConfigModifier.java index 40223d460c22..ac2c0ffbd56e 100644 --- a/instrumentation/micrometer/micrometer-1.5/library/src/main/java/io/opentelemetry/instrumentation/micrometer/v1_5/DistributionStatisticConfigModifier.java +++ b/instrumentation/micrometer/micrometer-1.5/library/src/main/java/io/opentelemetry/instrumentation/micrometer/v1_5/DistributionStatisticConfigModifier.java @@ -7,7 +7,7 @@ import io.micrometer.core.instrument.distribution.DistributionStatisticConfig; -@SuppressWarnings("CanIgnoreReturnValueSuggester") +@SuppressWarnings("OtelCanIgnoreReturnValueSuggester") enum DistributionStatisticConfigModifier { DISABLE_HISTOGRAM_GAUGES { @Override diff --git a/instrumentation/micrometer/micrometer-1.5/library/src/main/java/io/opentelemetry/instrumentation/micrometer/v1_5/DoubleMeasurementRecorder.java b/instrumentation/micrometer/micrometer-1.5/library/src/main/java/io/opentelemetry/instrumentation/micrometer/v1_5/DoubleMeasurementRecorder.java index 5eddf0473a5c..ba21bae817ca 100644 --- a/instrumentation/micrometer/micrometer-1.5/library/src/main/java/io/opentelemetry/instrumentation/micrometer/v1_5/DoubleMeasurementRecorder.java +++ b/instrumentation/micrometer/micrometer-1.5/library/src/main/java/io/opentelemetry/instrumentation/micrometer/v1_5/DoubleMeasurementRecorder.java @@ -20,19 +20,23 @@ final class DoubleMeasurementRecorder implements Consumer objWeakRef; private final ToDoubleFunction metricFunction; private final Attributes attributes; + private final WeakReference contextClassLoader; DoubleMeasurementRecorder( @Nullable T obj, ToDoubleFunction metricFunction, Attributes attributes) { this.objWeakRef = new WeakReference<>(obj); this.metricFunction = metricFunction; this.attributes = attributes; + contextClassLoader = new WeakReference<>(Thread.currentThread().getContextClassLoader()); } @Override public void accept(ObservableDoubleMeasurement measurement) { T obj = objWeakRef.get(); if (obj != null) { - measurement.record(metricFunction.applyAsDouble(obj), attributes); + MeasurementRecorderUtil.runInThreadContextClassLoader( + contextClassLoader.get(), + () -> measurement.record(metricFunction.applyAsDouble(obj), attributes)); } } } diff --git a/instrumentation/micrometer/micrometer-1.5/library/src/main/java/io/opentelemetry/instrumentation/micrometer/v1_5/HistogramAdviceUtil.java b/instrumentation/micrometer/micrometer-1.5/library/src/main/java/io/opentelemetry/instrumentation/micrometer/v1_5/HistogramAdviceUtil.java index 28167918957b..0c25f318e1c6 100644 --- a/instrumentation/micrometer/micrometer-1.5/library/src/main/java/io/opentelemetry/instrumentation/micrometer/v1_5/HistogramAdviceUtil.java +++ b/instrumentation/micrometer/micrometer-1.5/library/src/main/java/io/opentelemetry/instrumentation/micrometer/v1_5/HistogramAdviceUtil.java @@ -9,10 +9,11 @@ import io.micrometer.core.instrument.distribution.DistributionStatisticConfig; import io.micrometer.core.instrument.util.TimeUtils; +import io.opentelemetry.api.incubator.metrics.ExtendedDoubleHistogramBuilder; import io.opentelemetry.api.metrics.DoubleHistogramBuilder; -import io.opentelemetry.extension.incubator.metrics.ExtendedDoubleHistogramBuilder; import java.util.ArrayList; import java.util.List; +import java.util.NavigableSet; import java.util.concurrent.TimeUnit; import javax.annotation.Nullable; @@ -27,22 +28,22 @@ static void setExplicitBucketsIfConfigured( DoubleHistogramBuilder builder, DistributionStatisticConfig config, @Nullable TimeUnit timeUnit) { - double[] buckets = config.getServiceLevelObjectiveBoundaries(); - if (buckets == null || !(builder instanceof ExtendedDoubleHistogramBuilder)) { + if (!(builder instanceof ExtendedDoubleHistogramBuilder)) { return; } + NavigableSet buckets = config.getHistogramBuckets(false); ExtendedDoubleHistogramBuilder extendedBuilder = (ExtendedDoubleHistogramBuilder) builder; - extendedBuilder.setAdvice( - advice -> advice.setExplicitBucketBoundaries(computeBuckets(buckets, timeUnit))); + extendedBuilder.setExplicitBucketBoundariesAdvice(computeBuckets(buckets, timeUnit)); } - private static List computeBuckets(double[] buckets, @Nullable TimeUnit timeUnit) { - if (buckets.length == 0) { + private static List computeBuckets( + NavigableSet buckets, @Nullable TimeUnit timeUnit) { + if (buckets.isEmpty()) { return emptyList(); } // micrometer Timers always specify buckets in nanoseconds, we need to convert them to base unit double timeUnitMultiplier = timeUnit == null ? 1.0 : TimeUtils.nanosToUnit(1, timeUnit); - List result = new ArrayList<>(buckets.length); + List result = new ArrayList<>(buckets.size()); for (double b : buckets) { result.add(b * timeUnitMultiplier); } diff --git a/instrumentation/micrometer/micrometer-1.5/library/src/main/java/io/opentelemetry/instrumentation/micrometer/v1_5/LongMeasurementRecorder.java b/instrumentation/micrometer/micrometer-1.5/library/src/main/java/io/opentelemetry/instrumentation/micrometer/v1_5/LongMeasurementRecorder.java index 75ecd5f031c4..4829a5956b46 100644 --- a/instrumentation/micrometer/micrometer-1.5/library/src/main/java/io/opentelemetry/instrumentation/micrometer/v1_5/LongMeasurementRecorder.java +++ b/instrumentation/micrometer/micrometer-1.5/library/src/main/java/io/opentelemetry/instrumentation/micrometer/v1_5/LongMeasurementRecorder.java @@ -20,19 +20,23 @@ final class LongMeasurementRecorder implements Consumer objWeakRef; private final ToLongFunction metricFunction; private final Attributes attributes; + private final WeakReference contextClassLoader; LongMeasurementRecorder( @Nullable T obj, ToLongFunction metricFunction, Attributes attributes) { this.objWeakRef = new WeakReference<>(obj); this.metricFunction = metricFunction; this.attributes = attributes; + contextClassLoader = new WeakReference<>(Thread.currentThread().getContextClassLoader()); } @Override public void accept(ObservableLongMeasurement measurement) { T obj = objWeakRef.get(); if (obj != null) { - measurement.record(metricFunction.applyAsLong(obj), attributes); + MeasurementRecorderUtil.runInThreadContextClassLoader( + contextClassLoader.get(), + () -> measurement.record(metricFunction.applyAsLong(obj), attributes)); } } } diff --git a/instrumentation/micrometer/micrometer-1.5/library/src/main/java/io/opentelemetry/instrumentation/micrometer/v1_5/MeasurementRecorderUtil.java b/instrumentation/micrometer/micrometer-1.5/library/src/main/java/io/opentelemetry/instrumentation/micrometer/v1_5/MeasurementRecorderUtil.java new file mode 100644 index 000000000000..70bb10642bab --- /dev/null +++ b/instrumentation/micrometer/micrometer-1.5/library/src/main/java/io/opentelemetry/instrumentation/micrometer/v1_5/MeasurementRecorderUtil.java @@ -0,0 +1,21 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.micrometer.v1_5; + +class MeasurementRecorderUtil { + + static void runInThreadContextClassLoader(ClassLoader loader, Runnable runnable) { + ClassLoader prior = Thread.currentThread().getContextClassLoader(); + Thread.currentThread().setContextClassLoader(loader); + try { + runnable.run(); + } finally { + Thread.currentThread().setContextClassLoader(prior); + } + } + + private MeasurementRecorderUtil() {} +} diff --git a/instrumentation/micrometer/micrometer-1.5/library/src/main/java/io/opentelemetry/instrumentation/micrometer/v1_5/PrometheusModeNamingConvention.java b/instrumentation/micrometer/micrometer-1.5/library/src/main/java/io/opentelemetry/instrumentation/micrometer/v1_5/PrometheusModeNamingConvention.java index 2a4143512756..f876ddb7d111 100644 --- a/instrumentation/micrometer/micrometer-1.5/library/src/main/java/io/opentelemetry/instrumentation/micrometer/v1_5/PrometheusModeNamingConvention.java +++ b/instrumentation/micrometer/micrometer-1.5/library/src/main/java/io/opentelemetry/instrumentation/micrometer/v1_5/PrometheusModeNamingConvention.java @@ -20,7 +20,7 @@ public String name(String name, Meter.Type type, @Nullable String baseUnit) { if (type == Meter.Type.COUNTER || type == Meter.Type.DISTRIBUTION_SUMMARY || type == Meter.Type.GAUGE) { - if (baseUnit != null && !name.endsWith("." + baseUnit)) { + if (baseUnit != null && !baseUnit.equals("") && !name.endsWith("." + baseUnit)) { name = name + "." + baseUnit; } } diff --git a/instrumentation/micrometer/micrometer-1.5/testing/src/main/java/io/opentelemetry/instrumentation/micrometer/v1_5/AbstractDistributionSummaryTest.java b/instrumentation/micrometer/micrometer-1.5/testing/src/main/java/io/opentelemetry/instrumentation/micrometer/v1_5/AbstractDistributionSummaryTest.java index 324adae5063a..44ec09510727 100644 --- a/instrumentation/micrometer/micrometer-1.5/testing/src/main/java/io/opentelemetry/instrumentation/micrometer/v1_5/AbstractDistributionSummaryTest.java +++ b/instrumentation/micrometer/micrometer-1.5/testing/src/main/java/io/opentelemetry/instrumentation/micrometer/v1_5/AbstractDistributionSummaryTest.java @@ -12,17 +12,13 @@ import io.micrometer.core.instrument.DistributionSummary; import io.micrometer.core.instrument.Metrics; import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; -import io.opentelemetry.sdk.metrics.internal.aggregator.ExplicitBucketHistogramUtils; import org.assertj.core.api.AbstractIterableAssert; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; public abstract class AbstractDistributionSummaryTest { - static final double[] DEFAULT_BUCKETS = - ExplicitBucketHistogramUtils.DEFAULT_HISTOGRAM_BUCKET_BOUNDARIES.stream() - .mapToDouble(d -> d) - .toArray(); + static final double[] NO_BUCKETS = new double[0]; protected abstract InstrumentationExtension testing(); @@ -65,7 +61,7 @@ void testMicrometerDistributionSummary() { .hasSum(7) .hasCount(3) .hasAttributes(attributeEntry("tag", "value")) - .hasBucketBoundaries(DEFAULT_BUCKETS))))); + .hasBucketBoundaries(NO_BUCKETS))))); testing() .waitAndAssertMetrics( INSTRUMENTATION_NAME, diff --git a/instrumentation/micrometer/micrometer-1.5/testing/src/main/java/io/opentelemetry/instrumentation/micrometer/v1_5/AbstractFunctionCounterTest.java b/instrumentation/micrometer/micrometer-1.5/testing/src/main/java/io/opentelemetry/instrumentation/micrometer/v1_5/AbstractFunctionCounterTest.java index 36e3f43401eb..5c0f7251df12 100644 --- a/instrumentation/micrometer/micrometer-1.5/testing/src/main/java/io/opentelemetry/instrumentation/micrometer/v1_5/AbstractFunctionCounterTest.java +++ b/instrumentation/micrometer/micrometer-1.5/testing/src/main/java/io/opentelemetry/instrumentation/micrometer/v1_5/AbstractFunctionCounterTest.java @@ -12,6 +12,8 @@ import io.micrometer.core.instrument.FunctionCounter; import io.micrometer.core.instrument.Metrics; import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import java.net.URL; +import java.net.URLClassLoader; import java.util.concurrent.atomic.AtomicLong; import org.assertj.core.api.AbstractIterableAssert; import org.junit.jupiter.api.BeforeEach; @@ -30,7 +32,7 @@ void cleanupMeters() { final AtomicLong anotherNum = new AtomicLong(13); @Test - void testFunctionCounter() throws InterruptedException { + void testFunctionCounter() { // given FunctionCounter counter = FunctionCounter.builder("testFunctionCounter", num, AtomicLong::get) @@ -68,6 +70,61 @@ void testFunctionCounter() throws InterruptedException { INSTRUMENTATION_NAME, "testFunctionCounter", AbstractIterableAssert::isEmpty); } + @Test + void testFunctionCounterDependingOnThreadContextClassLoader() { + // given + ClassLoader dummy = new URLClassLoader(new URL[0]); + ClassLoader prior = Thread.currentThread().getContextClassLoader(); + FunctionCounter counter; + try { + Thread.currentThread().setContextClassLoader(dummy); + counter = + FunctionCounter.builder( + "testFunctionCounter", + num, + num -> { + // will throw an exception before value is reported if assertion fails + // then we assert below that value was reported + assertThat(Thread.currentThread().getContextClassLoader()).isEqualTo(dummy); + return num.get(); + }) + .description("This is a test function counter") + .tags("tag", "value") + .baseUnit("items") + .register(Metrics.globalRegistry); + } finally { + Thread.currentThread().setContextClassLoader(prior); + } + + // then + testing() + .waitAndAssertMetrics( + INSTRUMENTATION_NAME, + "testFunctionCounter", + metrics -> + metrics.anySatisfy( + metric -> + assertThat(metric) + .hasDescription("This is a test function counter") + .hasUnit("items") + .hasDoubleSumSatisfying( + sum -> + sum.hasPointsSatisfying( + point -> + point + .hasValue(12) + .hasAttributes(attributeEntry("tag", "value")))))); + + // when + Metrics.globalRegistry.remove(counter); + testing().clearData(); + + // then + testing() + .waitAndAssertMetrics( + INSTRUMENTATION_NAME, "testFunctionCounter", AbstractIterableAssert::isEmpty); + } + @Test void functionCountersWithSameNameAndDifferentTags() { // given diff --git a/instrumentation/micrometer/micrometer-1.5/testing/src/main/java/io/opentelemetry/instrumentation/micrometer/v1_5/AbstractFunctionTimerTest.java b/instrumentation/micrometer/micrometer-1.5/testing/src/main/java/io/opentelemetry/instrumentation/micrometer/v1_5/AbstractFunctionTimerTest.java index d0cff95338ce..1204a026c560 100644 --- a/instrumentation/micrometer/micrometer-1.5/testing/src/main/java/io/opentelemetry/instrumentation/micrometer/v1_5/AbstractFunctionTimerTest.java +++ b/instrumentation/micrometer/micrometer-1.5/testing/src/main/java/io/opentelemetry/instrumentation/micrometer/v1_5/AbstractFunctionTimerTest.java @@ -13,6 +13,8 @@ import io.micrometer.core.instrument.Metrics; import io.opentelemetry.api.common.Attributes; import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import java.net.URL; +import java.net.URLClassLoader; import java.util.concurrent.TimeUnit; import org.assertj.core.api.AbstractIterableAssert; import org.junit.jupiter.api.BeforeEach; @@ -97,6 +99,89 @@ void testFunctionTimer() { INSTRUMENTATION_NAME, "testFunctionTimer.count", AbstractIterableAssert::isEmpty); } + @Test + void testFunctionTimerDependingOnThreadContextClassLoader() { + // given + ClassLoader dummy = new URLClassLoader(new URL[0]); + ClassLoader prior = Thread.currentThread().getContextClassLoader(); + FunctionTimer functionTimer; + try { + Thread.currentThread().setContextClassLoader(dummy); + functionTimer = + FunctionTimer.builder( + "testFunctionTimer", + timerObj, + timerObj -> { + // will throw an exception before value is reported if assertion fails + // then we assert below that value was reported + assertThat(Thread.currentThread().getContextClassLoader()).isEqualTo(dummy); + return timerObj.getCount(); + }, + timerObj -> { + // will throw an exception before value is reported if assertion fails + // then we assert below that value was reported + assertThat(Thread.currentThread().getContextClassLoader()).isEqualTo(dummy); + return timerObj.getTotalTimeNanos(); + }, + TimeUnit.NANOSECONDS) + .description("This is a test function timer") + .tags("tag", "value") + .register(Metrics.globalRegistry); + } finally { + Thread.currentThread().setContextClassLoader(prior); + } + + // when + timerObj.add(42, TimeUnit.SECONDS); + + // then + testing() + .waitAndAssertMetrics( + INSTRUMENTATION_NAME, + "testFunctionTimer.count", + metrics -> + metrics.anySatisfy( + metric -> + assertThat(metric) + .hasDescription("This is a test function timer") + .hasUnit("{invocation}") + .hasLongSumSatisfying( + sum -> + sum.isMonotonic() + .hasPointsSatisfying( + point -> + point + .hasValue(1) + .hasAttributes( + attributeEntry("tag", "value")))))); + testing() + .waitAndAssertMetrics( + INSTRUMENTATION_NAME, + "testFunctionTimer.sum", + metrics -> + metrics.anySatisfy( + metric -> + assertThat(metric) + .hasDescription("This is a test function timer") + .hasUnit("s") + .hasDoubleSumSatisfying( + sum -> + sum.hasPointsSatisfying( + point -> + point + .hasValue(42) + .hasAttributes(attributeEntry("tag", "value")))))); + + // when + Metrics.globalRegistry.remove(functionTimer); + testing().clearData(); + + // then + testing() + .waitAndAssertMetrics( + INSTRUMENTATION_NAME, "testFunctionTimer.count", AbstractIterableAssert::isEmpty); + } + @Test void testNanoPrecision() { // given diff --git a/instrumentation/micrometer/micrometer-1.5/testing/src/main/java/io/opentelemetry/instrumentation/micrometer/v1_5/AbstractGaugeTest.java b/instrumentation/micrometer/micrometer-1.5/testing/src/main/java/io/opentelemetry/instrumentation/micrometer/v1_5/AbstractGaugeTest.java index 95b526211432..550537c33bd6 100644 --- a/instrumentation/micrometer/micrometer-1.5/testing/src/main/java/io/opentelemetry/instrumentation/micrometer/v1_5/AbstractGaugeTest.java +++ b/instrumentation/micrometer/micrometer-1.5/testing/src/main/java/io/opentelemetry/instrumentation/micrometer/v1_5/AbstractGaugeTest.java @@ -6,6 +6,7 @@ package io.opentelemetry.instrumentation.micrometer.v1_5; import static io.opentelemetry.instrumentation.micrometer.v1_5.AbstractCounterTest.INSTRUMENTATION_NAME; +import static io.opentelemetry.instrumentation.test.utils.GcUtils.awaitGc; import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.attributeEntry; @@ -13,6 +14,10 @@ import io.micrometer.core.instrument.Metrics; import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; import java.lang.ref.WeakReference; +import java.net.URL; +import java.net.URLClassLoader; +import java.time.Duration; +import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicLong; import org.assertj.core.api.AbstractIterableAssert; import org.junit.jupiter.api.Test; @@ -59,6 +64,59 @@ void testGauge() { .waitAndAssertMetrics(INSTRUMENTATION_NAME, "testGauge", AbstractIterableAssert::isEmpty); } + @Test + void testGaugeDependingOnThreadContextClassLoader() { + // given + ClassLoader dummy = new URLClassLoader(new URL[0]); + ClassLoader prior = Thread.currentThread().getContextClassLoader(); + Gauge gauge; + try { + Thread.currentThread().setContextClassLoader(dummy); + gauge = + Gauge.builder( + "testGauge", + () -> { + // will throw an exception before value is reported if assertion fails + // then we assert below that value was reported + assertThat(Thread.currentThread().getContextClassLoader()).isEqualTo(dummy); + return 42; + }) + .description("This is a test gauge") + .tags("tag", "value") + .baseUnit("items") + .register(Metrics.globalRegistry); + } finally { + Thread.currentThread().setContextClassLoader(prior); + } + + // then + testing() + .waitAndAssertMetrics( + INSTRUMENTATION_NAME, + "testGauge", + metrics -> + metrics.anySatisfy( + metric -> + assertThat(metric) + .hasDescription("This is a test gauge") + .hasUnit("items") + .hasDoubleGaugeSatisfying( + doubleGauge -> + doubleGauge.hasPointsSatisfying( + point -> + point + .hasValue(42) + .hasAttributes(attributeEntry("tag", "value")))))); + + // when + Metrics.globalRegistry.remove(gauge); + testing().clearData(); + + // then + testing() + .waitAndAssertMetrics(INSTRUMENTATION_NAME, "testGauge", AbstractIterableAssert::isEmpty); + } + @Test void gaugesWithSameNameAndDifferentTags() { // given @@ -98,7 +156,7 @@ void gaugesWithSameNameAndDifferentTags() { } @Test - void testWeakRefGauge() throws InterruptedException { + void testWeakRefGauge() throws InterruptedException, TimeoutException { // given AtomicLong num = new AtomicLong(42); Gauge.builder("testWeakRefGauge", num, AtomicLong::get) @@ -120,7 +178,7 @@ void testWeakRefGauge() throws InterruptedException { // when WeakReference numWeakRef = new WeakReference<>(num); num = null; - awaitGc(numWeakRef); + awaitGc(numWeakRef, Duration.ofSeconds(10)); testing().clearData(); // then @@ -128,14 +186,4 @@ void testWeakRefGauge() throws InterruptedException { .waitAndAssertMetrics( INSTRUMENTATION_NAME, "testWeakRefGauge", AbstractIterableAssert::isEmpty); } - - private static void awaitGc(WeakReference ref) throws InterruptedException { - while (ref.get() != null) { - if (Thread.interrupted()) { - throw new InterruptedException(); - } - System.gc(); - System.runFinalization(); - } - } } diff --git a/instrumentation/micrometer/micrometer-1.5/testing/src/main/java/io/opentelemetry/instrumentation/micrometer/v1_5/AbstractPrometheusModeTest.java b/instrumentation/micrometer/micrometer-1.5/testing/src/main/java/io/opentelemetry/instrumentation/micrometer/v1_5/AbstractPrometheusModeTest.java index 2666a1308812..65f3ea93e943 100644 --- a/instrumentation/micrometer/micrometer-1.5/testing/src/main/java/io/opentelemetry/instrumentation/micrometer/v1_5/AbstractPrometheusModeTest.java +++ b/instrumentation/micrometer/micrometer-1.5/testing/src/main/java/io/opentelemetry/instrumentation/micrometer/v1_5/AbstractPrometheusModeTest.java @@ -34,7 +34,7 @@ void testCounter() { Counter.builder("testPrometheusCounter") .description("This is a test counter") .tags("tag", "value") - .baseUnit("items") + .baseUnit("") .register(Metrics.globalRegistry); // when @@ -44,13 +44,13 @@ void testCounter() { testing() .waitAndAssertMetrics( INSTRUMENTATION_NAME, - "testPrometheusCounter.items", + "testPrometheusCounter", metrics -> metrics.anySatisfy( metric -> assertThat(metric) .hasDescription("This is a test counter") - .hasUnit("items") + .hasUnit("") .hasDoubleSumSatisfying( sum -> sum.isMonotonic() diff --git a/instrumentation/micrometer/micrometer-1.5/testing/src/main/java/io/opentelemetry/instrumentation/micrometer/v1_5/AbstractTimerTest.java b/instrumentation/micrometer/micrometer-1.5/testing/src/main/java/io/opentelemetry/instrumentation/micrometer/v1_5/AbstractTimerTest.java index 50e126425597..de6f29b332c9 100644 --- a/instrumentation/micrometer/micrometer-1.5/testing/src/main/java/io/opentelemetry/instrumentation/micrometer/v1_5/AbstractTimerTest.java +++ b/instrumentation/micrometer/micrometer-1.5/testing/src/main/java/io/opentelemetry/instrumentation/micrometer/v1_5/AbstractTimerTest.java @@ -15,7 +15,6 @@ import io.opentelemetry.api.common.Attributes; import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; import io.opentelemetry.sdk.metrics.data.HistogramPointData; -import io.opentelemetry.sdk.metrics.internal.aggregator.ExplicitBucketHistogramUtils; import java.time.Duration; import java.util.concurrent.TimeUnit; import org.assertj.core.api.AbstractIterableAssert; @@ -25,10 +24,7 @@ @SuppressWarnings("PreferJavaTimeOverload") public abstract class AbstractTimerTest { - static final double[] DEFAULT_BUCKETS = - ExplicitBucketHistogramUtils.DEFAULT_HISTOGRAM_BUCKET_BOUNDARIES.stream() - .mapToDouble(d -> d) - .toArray(); + static final double[] NO_BUCKETS = new double[0]; protected abstract InstrumentationExtension testing(); @@ -63,7 +59,7 @@ void testTimer() { .hasSum(42) .hasCount(1) .hasAttributes(attributeEntry("tag", "value")) - .hasBucketBoundaries(DEFAULT_BUCKETS))))); + .hasBucketBoundaries(NO_BUCKETS))))); testing() .waitAndAssertMetrics( INSTRUMENTATION_NAME, diff --git a/instrumentation/mongo/README.md b/instrumentation/mongo/README.md new file mode 100644 index 000000000000..71f1503df56f --- /dev/null +++ b/instrumentation/mongo/README.md @@ -0,0 +1,5 @@ +# Settings for the MongoDB Java Driver instrumentation + +| System property | Type | Default | Description | +|----------------------------------------------------------|---------|---------|----------------------------------------| +| `otel.instrumentation.mongo.statement-sanitizer.enabled` | Boolean | `true` | Enables the DB statement sanitization. | diff --git a/instrumentation/mongo/mongo-3.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mongo/v3_1/MongoInstrumentationSingletons.java b/instrumentation/mongo/mongo-3.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mongo/v3_1/MongoInstrumentationSingletons.java index f9357d0fa3e6..e943f6fbc78c 100644 --- a/instrumentation/mongo/mongo-3.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mongo/v3_1/MongoInstrumentationSingletons.java +++ b/instrumentation/mongo/mongo-3.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mongo/v3_1/MongoInstrumentationSingletons.java @@ -8,18 +8,18 @@ import com.mongodb.event.CommandListener; import io.opentelemetry.api.GlobalOpenTelemetry; import io.opentelemetry.instrumentation.mongo.v3_1.MongoTelemetry; -import io.opentelemetry.javaagent.bootstrap.internal.CommonConfig; -import io.opentelemetry.javaagent.bootstrap.internal.InstrumentationConfig; +import io.opentelemetry.javaagent.bootstrap.internal.AgentCommonConfig; +import io.opentelemetry.javaagent.bootstrap.internal.AgentInstrumentationConfig; public final class MongoInstrumentationSingletons { public static final CommandListener LISTENER = MongoTelemetry.builder(GlobalOpenTelemetry.get()) .setStatementSanitizationEnabled( - InstrumentationConfig.get() + AgentInstrumentationConfig.get() .getBoolean( "otel.instrumentation.mongo.statement-sanitizer.enabled", - CommonConfig.get().isStatementSanitizationEnabled())) + AgentCommonConfig.get().isStatementSanitizationEnabled())) .build() .newCommandListener(); diff --git a/instrumentation/mongo/mongo-3.1/library/README.md b/instrumentation/mongo/mongo-3.1/library/README.md new file mode 100644 index 000000000000..c78f11278cf4 --- /dev/null +++ b/instrumentation/mongo/mongo-3.1/library/README.md @@ -0,0 +1,45 @@ +# MongoDB library instrumentation + +This package contains the library to help instrument MongoDB Client. + +## Quickstart + +### Dependencies + +Replace OPENTELEMETRY_VERSION with the [latest release](https://central.sonatype.com/search?q=g%3Aio.opentelemetry.instrumentation+a%3Aopentelemetry-mongo-3.1). + +For Maven, add to your `pom.xml` dependencies: + +```xml + + + io.opentelemetry.instrumentation + opentelemetry-mongo-3.1 + OPENTELEMETRY_VERSION + + +``` + +For Gradle, add to your dependencies: + +```gradle +implementation("io.opentelemetry.instrumentation:instrumentation:opentelemetry-mongo-3.1:OPENTELEMETRY_VERSION") +``` + +## Usage + +The instrumentation is initialized by passing a `MongoTelemetry::newCommandListener()` to the `MongoClientSettings` builder. You must set the `OpenTelemetry` to use with the feature. + +```java +OpenTelemetry openTelemetry = ...; + +MongoTelemetry mongoTelemetry = MongoTelemetry.builder(openTelemetry).build(); + +MongoClientSettings settings = MongoClientSettings.builder() + .applyConnectionString(ConnectionString("mongodb://localhost:27017")) + .addCommandListener(mongoTelemetry.newCommandListener()) + .build(); + +// With Reactive Streams +MongoClient client = MongoClients.create(settings); +``` diff --git a/instrumentation/mongo/mongo-3.1/library/src/main/java/io/opentelemetry/instrumentation/mongo/v3_1/MongoAttributesExtractor.java b/instrumentation/mongo/mongo-3.1/library/src/main/java/io/opentelemetry/instrumentation/mongo/v3_1/MongoAttributesExtractor.java index 59f03d076e4f..6890025d5c8a 100644 --- a/instrumentation/mongo/mongo-3.1/library/src/main/java/io/opentelemetry/instrumentation/mongo/v3_1/MongoAttributesExtractor.java +++ b/instrumentation/mongo/mongo-3.1/library/src/main/java/io/opentelemetry/instrumentation/mongo/v3_1/MongoAttributesExtractor.java @@ -5,10 +5,10 @@ package io.opentelemetry.instrumentation.mongo.v3_1; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.DB_MONGODB_COLLECTION; import static java.util.Arrays.asList; import com.mongodb.event.CommandStartedEvent; +import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.common.AttributesBuilder; import io.opentelemetry.context.Context; import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; @@ -18,6 +18,10 @@ import org.bson.BsonValue; class MongoAttributesExtractor implements AttributesExtractor { + // copied from DbIncubatingAttributes + private static final AttributeKey DB_MONGODB_COLLECTION = + AttributeKey.stringKey("db.mongodb.collection"); + @Override public void onStart( AttributesBuilder attributes, Context parentContext, CommandStartedEvent event) { diff --git a/instrumentation/mongo/mongo-3.1/library/src/main/java/io/opentelemetry/instrumentation/mongo/v3_1/MongoDbAttributesGetter.java b/instrumentation/mongo/mongo-3.1/library/src/main/java/io/opentelemetry/instrumentation/mongo/v3_1/MongoDbAttributesGetter.java index 631cf3d4f672..edb3ffda13a6 100644 --- a/instrumentation/mongo/mongo-3.1/library/src/main/java/io/opentelemetry/instrumentation/mongo/v3_1/MongoDbAttributesGetter.java +++ b/instrumentation/mongo/mongo-3.1/library/src/main/java/io/opentelemetry/instrumentation/mongo/v3_1/MongoDbAttributesGetter.java @@ -8,8 +8,7 @@ import com.mongodb.ServerAddress; import com.mongodb.connection.ConnectionDescription; import com.mongodb.event.CommandStartedEvent; -import io.opentelemetry.instrumentation.api.instrumenter.db.DbClientAttributesGetter; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.opentelemetry.instrumentation.api.incubator.semconv.db.DbClientAttributesGetter; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.Arrays; @@ -26,6 +25,9 @@ class MongoDbAttributesGetter implements DbClientAttributesGetter { + // copied from DbIncubatingAttributes.DbSystemValues + private static final String MONGODB = "mongodb"; + @Nullable private static final Method IS_TRUNCATED_METHOD; private static final String HIDDEN_CHAR = "?"; @@ -49,7 +51,7 @@ class MongoDbAttributesGetter implements DbClientAttributesGetter netAttributesExtractor = - NetClientAttributesExtractor.create(new MongoNetAttributesGetter()); static Instrumenter createInstrumenter( OpenTelemetry openTelemetry, @@ -34,7 +31,8 @@ static Instrumenter createInstrumenter( return Instrumenter.builder( openTelemetry, "io.opentelemetry.mongo-3.1", spanNameExtractor) .addAttributesExtractor(DbClientAttributesExtractor.create(dbAttributesGetter)) - .addAttributesExtractor(netAttributesExtractor) + .addAttributesExtractor( + ServerAttributesExtractor.create(new MongoNetworkAttributesGetter())) .addAttributesExtractor(attributesExtractor) .buildInstrumenter(SpanKindExtractor.alwaysClient()); } diff --git a/instrumentation/mongo/mongo-3.1/library/src/main/java/io/opentelemetry/instrumentation/mongo/v3_1/MongoNetAttributesGetter.java b/instrumentation/mongo/mongo-3.1/library/src/main/java/io/opentelemetry/instrumentation/mongo/v3_1/MongoNetworkAttributesGetter.java similarity index 82% rename from instrumentation/mongo/mongo-3.1/library/src/main/java/io/opentelemetry/instrumentation/mongo/v3_1/MongoNetAttributesGetter.java rename to instrumentation/mongo/mongo-3.1/library/src/main/java/io/opentelemetry/instrumentation/mongo/v3_1/MongoNetworkAttributesGetter.java index 65e6cab8751a..28cd334047c1 100644 --- a/instrumentation/mongo/mongo-3.1/library/src/main/java/io/opentelemetry/instrumentation/mongo/v3_1/MongoNetAttributesGetter.java +++ b/instrumentation/mongo/mongo-3.1/library/src/main/java/io/opentelemetry/instrumentation/mongo/v3_1/MongoNetworkAttributesGetter.java @@ -6,10 +6,10 @@ package io.opentelemetry.instrumentation.mongo.v3_1; import com.mongodb.event.CommandStartedEvent; -import io.opentelemetry.instrumentation.api.instrumenter.net.NetClientAttributesGetter; +import io.opentelemetry.instrumentation.api.semconv.network.ServerAttributesGetter; import javax.annotation.Nullable; -class MongoNetAttributesGetter implements NetClientAttributesGetter { +class MongoNetworkAttributesGetter implements ServerAttributesGetter { @Nullable @Override diff --git a/instrumentation/mongo/mongo-3.1/testing/src/main/groovy/io/opentelemetry/instrumentation/mongo/v3_1/AbstractMongo31ClientTest.groovy b/instrumentation/mongo/mongo-3.1/testing/src/main/groovy/io/opentelemetry/instrumentation/mongo/v3_1/AbstractMongo31ClientTest.groovy index a994f34b3718..63d74fc90341 100644 --- a/instrumentation/mongo/mongo-3.1/testing/src/main/groovy/io/opentelemetry/instrumentation/mongo/v3_1/AbstractMongo31ClientTest.groovy +++ b/instrumentation/mongo/mongo-3.1/testing/src/main/groovy/io/opentelemetry/instrumentation/mongo/v3_1/AbstractMongo31ClientTest.groovy @@ -29,7 +29,7 @@ abstract class AbstractMongo31ClientTest extends AbstractMongoClientTest> client = MongoClients.create(MongoClientSettings.builder() .applyToClusterSettings({ builder -> builder.hosts(Arrays.asList( - new ServerAddress("localhost", port))) + new ServerAddress(host, port))) .description("some-description") }) .build()) @@ -47,7 +47,7 @@ class MongoClientTest extends AbstractMongoClientTest> @Override void createCollectionNoDescription(String dbName, String collectionName) { - MongoDatabase db = MongoClients.create("mongodb://localhost:${port}").getDatabase(dbName) + MongoDatabase db = MongoClients.create("mongodb://$host:${port}").getDatabase(dbName) db.createCollection(collectionName) } @@ -56,7 +56,7 @@ class MongoClientTest extends AbstractMongoClientTest> def clientSettings = MongoClientSettings.builder() .applyToClusterSettings({ builder -> builder.hosts(Arrays.asList( - new ServerAddress("localhost", port))) + new ServerAddress(host, port))) .description("some-description") }) .build() @@ -70,7 +70,7 @@ class MongoClientTest extends AbstractMongoClientTest> def clientSettings = MongoClientSettings.builder() .applyToClusterSettings({ builder -> builder.hosts(Arrays.asList( - new ServerAddress("localhost", port))) + new ServerAddress(host, port))) .description("some-description") }) clientSettings.build() @@ -174,7 +174,7 @@ class MongoClientTest extends AbstractMongoClientTest> def "test client failure"() { setup: - def client = MongoClients.create("mongodb://localhost:" + UNUSABLE_PORT + "/?connectTimeoutMS=10") + def client = MongoClients.create("mongodb://" + host + ":" + UNUSABLE_PORT + "/?connectTimeoutMS=10") when: MongoDatabase db = client.getDatabase(dbName) diff --git a/instrumentation/mongo/mongo-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mongo/v4_0/BaseClusterInstrumentation.java b/instrumentation/mongo/mongo-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mongo/v4_0/BaseClusterInstrumentation.java index 0eff79c30dd2..24994d4d1972 100644 --- a/instrumentation/mongo/mongo-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mongo/v4_0/BaseClusterInstrumentation.java +++ b/instrumentation/mongo/mongo-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mongo/v4_0/BaseClusterInstrumentation.java @@ -34,6 +34,14 @@ public void transform(TypeTransformer transformer) { .and(takesArgument(0, named("com.mongodb.selector.ServerSelector"))) .and(takesArgument(1, named("com.mongodb.internal.async.SingleResultCallback"))), this.getClass().getName() + "$SingleResultCallbackArg1Advice"); + + transformer.applyAdviceToMethod( + isMethod() + .and(isPublic()) + .and(named("selectServerAsync")) + .and(takesArgument(0, named("com.mongodb.selector.ServerSelector"))) + .and(takesArgument(2, named("com.mongodb.internal.async.SingleResultCallback"))), + this.getClass().getName() + "$SingleResultCallbackArg2Advice"); } @SuppressWarnings("unused") @@ -45,4 +53,14 @@ public static void wrapCallback( 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/instrumentation/mongo/mongo-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mongo/v4_0/DefaultConnectionPoolTaskInstrumentation.java b/instrumentation/mongo/mongo-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mongo/v4_0/DefaultConnectionPoolTaskInstrumentation.java new file mode 100644 index 000000000000..a87070f926eb --- /dev/null +++ b/instrumentation/mongo/mongo-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mongo/v4_0/DefaultConnectionPoolTaskInstrumentation.java @@ -0,0 +1,43 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.mongo.v4_0; + +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.bootstrap.Java8BytecodeBridge; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import java.util.function.Consumer; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class DefaultConnectionPoolTaskInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher typeMatcher() { + return named("com.mongodb.internal.connection.DefaultConnectionPool$Task"); + } + + @Override + public void transform(TypeTransformer transformer) { + // outer class this is passed as arg 0 to constructor + transformer.applyAdviceToMethod( + isConstructor().and(takesArgument(2, Consumer.class)), + this.getClass().getName() + "$TaskAdvice"); + } + + @SuppressWarnings("unused") + public static class TaskAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void wrapCallback( + @Advice.Argument(value = 2, readOnly = false) Consumer action) { + action = new TaskWrapper(Java8BytecodeBridge.currentContext(), action); + } + } +} diff --git a/instrumentation/mongo/mongo-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mongo/v4_0/MongoClientInstrumentationModule.java b/instrumentation/mongo/mongo-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mongo/v4_0/MongoClientInstrumentationModule.java index ecc578f7bced..25b8abe5c0c2 100644 --- a/instrumentation/mongo/mongo-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mongo/v4_0/MongoClientInstrumentationModule.java +++ b/instrumentation/mongo/mongo-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mongo/v4_0/MongoClientInstrumentationModule.java @@ -33,6 +33,7 @@ public List typeInstrumentations() { new InternalStreamConnectionInstrumentation(), new BaseClusterInstrumentation(), new DefaultConnectionPoolInstrumentation(), + new DefaultConnectionPoolTaskInstrumentation(), new AsyncWorkManagerInstrumentation()); } } diff --git a/instrumentation/mongo/mongo-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mongo/v4_0/MongoInstrumentationSingletons.java b/instrumentation/mongo/mongo-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mongo/v4_0/MongoInstrumentationSingletons.java index 74f3bf6cb0b8..3389cc5bb6a3 100644 --- a/instrumentation/mongo/mongo-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mongo/v4_0/MongoInstrumentationSingletons.java +++ b/instrumentation/mongo/mongo-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mongo/v4_0/MongoInstrumentationSingletons.java @@ -8,18 +8,18 @@ import com.mongodb.event.CommandListener; import io.opentelemetry.api.GlobalOpenTelemetry; import io.opentelemetry.instrumentation.mongo.v3_1.MongoTelemetry; -import io.opentelemetry.javaagent.bootstrap.internal.CommonConfig; -import io.opentelemetry.javaagent.bootstrap.internal.InstrumentationConfig; +import io.opentelemetry.javaagent.bootstrap.internal.AgentCommonConfig; +import io.opentelemetry.javaagent.bootstrap.internal.AgentInstrumentationConfig; public final class MongoInstrumentationSingletons { public static final CommandListener LISTENER = MongoTelemetry.builder(GlobalOpenTelemetry.get()) .setStatementSanitizationEnabled( - InstrumentationConfig.get() + AgentInstrumentationConfig.get() .getBoolean( "otel.instrumentation.mongo.statement-sanitizer.enabled", - CommonConfig.get().isStatementSanitizationEnabled())) + AgentCommonConfig.get().isStatementSanitizationEnabled())) .build() .newCommandListener(); diff --git a/instrumentation/mongo/mongo-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mongo/v4_0/TaskWrapper.java b/instrumentation/mongo/mongo-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mongo/v4_0/TaskWrapper.java new file mode 100644 index 000000000000..ea4f72229143 --- /dev/null +++ b/instrumentation/mongo/mongo-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mongo/v4_0/TaskWrapper.java @@ -0,0 +1,27 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.mongo.v4_0; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import java.util.function.Consumer; + +public class TaskWrapper implements Consumer { + private final Context context; + private final Consumer delegate; + + public TaskWrapper(Context context, Consumer delegate) { + this.context = context; + this.delegate = delegate; + } + + @Override + public void accept(Object value) { + try (Scope ignored = context.makeCurrent()) { + delegate.accept(value); + } + } +} diff --git a/instrumentation/mongo/mongo-4.0/javaagent/src/test/groovy/Mongo4ReactiveClientTest.groovy b/instrumentation/mongo/mongo-4.0/javaagent/src/test/groovy/Mongo4ReactiveClientTest.groovy index 8d4f9a4e9e49..c9afc1d5ca03 100644 --- a/instrumentation/mongo/mongo-4.0/javaagent/src/test/groovy/Mongo4ReactiveClientTest.groovy +++ b/instrumentation/mongo/mongo-4.0/javaagent/src/test/groovy/Mongo4ReactiveClientTest.groovy @@ -23,31 +23,43 @@ import spock.lang.Shared import java.util.concurrent.CompletableFuture import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit class Mongo4ReactiveClientTest extends AbstractMongoClientTest> implements AgentTestTrait { @Shared MongoClient client + @Shared + List cleanup = [] def setupSpec() throws Exception { - client = MongoClients.create("mongodb://localhost:$port") + client = MongoClients.create("mongodb://$host:$port") } def cleanupSpec() throws Exception { client?.close() client = null + cleanup.forEach { + it.close() + } } @Override void createCollection(String dbName, String collectionName) { MongoDatabase db = client.getDatabase(dbName) - db.createCollection(collectionName).subscribe(toSubscriber {}) + def latch = new CountDownLatch(1) + db.createCollection(collectionName).subscribe(toSubscriber { latch.countDown() }) + latch.await(30, TimeUnit.SECONDS) } @Override void createCollectionNoDescription(String dbName, String collectionName) { - MongoDatabase db = MongoClients.create("mongodb://localhost:${port}").getDatabase(dbName) - db.createCollection(collectionName).subscribe(toSubscriber {}) + def tmpClient = MongoClients.create("mongodb://$host:${port}") + cleanup.add(tmpClient) + MongoDatabase db = tmpClient.getDatabase(dbName) + def latch = new CountDownLatch(1) + db.createCollection(collectionName).subscribe(toSubscriber { latch.countDown() }) + latch.await(30, TimeUnit.SECONDS) } @Override @@ -60,11 +72,15 @@ class Mongo4ReactiveClientTest extends AbstractMongoClientTest builder.hosts(Arrays.asList( - new ServerAddress("localhost", port))) + new ServerAddress(host, port))) }) settings.build() - MongoDatabase db = MongoClients.create(settings.build()).getDatabase(dbName) - db.createCollection(collectionName).subscribe(toSubscriber {}) + def tmpClient = MongoClients.create(settings.build()) + cleanup.add(tmpClient) + MongoDatabase db = tmpClient.getDatabase(dbName) + def latch = new CountDownLatch(1) + db.createCollection(collectionName).subscribe(toSubscriber { latch.countDown() }) + latch.await(30, TimeUnit.SECONDS) } @Override @@ -72,7 +88,7 @@ class Mongo4ReactiveClientTest extends AbstractMongoClientTest() db.getCollection(collectionName).estimatedDocumentCount().subscribe(toSubscriber { count.complete(it) }) - return count.join() + return count.get(30, TimeUnit.SECONDS) } @Override @@ -81,7 +97,7 @@ class Mongo4ReactiveClientTest extends AbstractMongoClientTest toSubscriber(Closure closure) { + Subscriber toSubscriber(Closure closure) { return new Subscriber() { boolean hasResult diff --git a/instrumentation/mongo/mongo-4.0/javaagent/src/test/groovy/MongoClientTest.groovy b/instrumentation/mongo/mongo-4.0/javaagent/src/test/groovy/MongoClientTest.groovy index a206a6cf0409..5daec24039fe 100644 --- a/instrumentation/mongo/mongo-4.0/javaagent/src/test/groovy/MongoClientTest.groovy +++ b/instrumentation/mongo/mongo-4.0/javaagent/src/test/groovy/MongoClientTest.groovy @@ -23,7 +23,7 @@ class MongoClientTest extends AbstractMongoClientTest> MongoClient client def setupSpec() throws Exception { - client = MongoClients.create("mongodb://localhost:$port") + client = MongoClients.create("mongodb://$host:$port") } def cleanupSpec() throws Exception { @@ -39,7 +39,7 @@ class MongoClientTest extends AbstractMongoClientTest> @Override void createCollectionNoDescription(String dbName, String collectionName) { - MongoDatabase db = MongoClients.create("mongodb://localhost:${port}").getDatabase(dbName) + MongoDatabase db = MongoClients.create("mongodb://$host:${port}").getDatabase(dbName) db.createCollection(collectionName) } @@ -53,7 +53,7 @@ class MongoClientTest extends AbstractMongoClientTest> def settings = MongoClientSettings.builder() .applyToClusterSettings({ builder -> builder.hosts(Arrays.asList( - new ServerAddress("localhost", port))) + new ServerAddress(host, port))) }) settings.build() MongoDatabase db = MongoClients.create(settings.build()).getDatabase(dbName) diff --git a/instrumentation/mongo/mongo-async-3.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mongoasync/v3_3/MongoInstrumentationSingletons.java b/instrumentation/mongo/mongo-async-3.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mongoasync/v3_3/MongoInstrumentationSingletons.java index f502009eb409..2280ce533637 100644 --- a/instrumentation/mongo/mongo-async-3.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mongoasync/v3_3/MongoInstrumentationSingletons.java +++ b/instrumentation/mongo/mongo-async-3.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mongoasync/v3_3/MongoInstrumentationSingletons.java @@ -8,18 +8,18 @@ import com.mongodb.event.CommandListener; import io.opentelemetry.api.GlobalOpenTelemetry; import io.opentelemetry.instrumentation.mongo.v3_1.MongoTelemetry; -import io.opentelemetry.javaagent.bootstrap.internal.CommonConfig; -import io.opentelemetry.javaagent.bootstrap.internal.InstrumentationConfig; +import io.opentelemetry.javaagent.bootstrap.internal.AgentCommonConfig; +import io.opentelemetry.javaagent.bootstrap.internal.AgentInstrumentationConfig; public final class MongoInstrumentationSingletons { public static final CommandListener LISTENER = MongoTelemetry.builder(GlobalOpenTelemetry.get()) .setStatementSanitizationEnabled( - InstrumentationConfig.get() + AgentInstrumentationConfig.get() .getBoolean( "otel.instrumentation.mongo.statement-sanitizer.enabled", - CommonConfig.get().isStatementSanitizationEnabled())) + AgentCommonConfig.get().isStatementSanitizationEnabled())) .build() .newCommandListener(); diff --git a/instrumentation/mongo/mongo-async-3.3/javaagent/src/test/groovy/MongoAsyncClientTest.groovy b/instrumentation/mongo/mongo-async-3.3/javaagent/src/test/groovy/MongoAsyncClientTest.groovy index fecdcf77154e..dea675af2c98 100644 --- a/instrumentation/mongo/mongo-async-3.3/javaagent/src/test/groovy/MongoAsyncClientTest.groovy +++ b/instrumentation/mongo/mongo-async-3.3/javaagent/src/test/groovy/MongoAsyncClientTest.groovy @@ -35,7 +35,7 @@ class MongoAsyncClientTest extends AbstractMongoClientTest extends InstrumentationSpecification { @Shared GenericContainer mongodb + @Shared + String host + @Shared int port @@ -32,7 +36,7 @@ abstract class AbstractMongoClientTest extends InstrumentationSpecification { .withExposedPorts(27017) .withLogConsumer(new Slf4jLogConsumer(LoggerFactory.getLogger("mongodb"))) mongodb.start() - + host = mongodb.getHost() port = mongodb.getMappedPort(27017) } @@ -76,7 +80,7 @@ abstract class AbstractMongoClientTest extends InstrumentationSpecification { def "test port open"() { when: - new Socket("localhost", port) + new Socket(host, port) then: noExceptionThrown() @@ -398,6 +402,7 @@ abstract class AbstractMongoClientTest extends InstrumentationSpecification { return "testCollection-${collectionIndex.getAndIncrement()}" } + @SuppressWarnings("deprecation") // TODO DbIncubatingAttributes.DB_CONNECTION_STRING deprecation def mongoSpan(TraceAssert trace, int index, String operation, String collection, String dbName, Object parentSpan, @@ -411,16 +416,16 @@ abstract class AbstractMongoClientTest extends InstrumentationSpecification { childOf((SpanData) parentSpan) } attributes { - "$SemanticAttributes.NET_PEER_NAME" "localhost" - "$SemanticAttributes.NET_PEER_PORT" port - "$SemanticAttributes.DB_STATEMENT" { + "$ServerAttributes.SERVER_ADDRESS" host + "$ServerAttributes.SERVER_PORT" port + "$DbIncubatingAttributes.DB_STATEMENT" { statementEval.call(it.replaceAll(" ", "")) } - "$SemanticAttributes.DB_SYSTEM" "mongodb" - "$SemanticAttributes.DB_CONNECTION_STRING" "mongodb://localhost:" + port - "$SemanticAttributes.DB_NAME" dbName - "$SemanticAttributes.DB_OPERATION" operation - "$SemanticAttributes.DB_MONGODB_COLLECTION" collection + "$DbIncubatingAttributes.DB_SYSTEM" "mongodb" + "$DbIncubatingAttributes.DB_CONNECTION_STRING" "mongodb://localhost:" + port + "$DbIncubatingAttributes.DB_NAME" dbName + "$DbIncubatingAttributes.DB_OPERATION" operation + "$DbIncubatingAttributes.DB_MONGODB_COLLECTION" collection } } } diff --git a/instrumentation/mybatis-3.2/javaagent/build.gradle.kts b/instrumentation/mybatis-3.2/javaagent/build.gradle.kts new file mode 100644 index 000000000000..c7c8a098ea77 --- /dev/null +++ b/instrumentation/mybatis-3.2/javaagent/build.gradle.kts @@ -0,0 +1,26 @@ +plugins { + id("otel.javaagent-instrumentation") +} + +muzzle { + pass { + group.set("org.mybatis") + module.set("mybatis") + versions.set("[3.2.0,)") + assertInverse.set(true) + } +} + +dependencies { + library("org.mybatis:mybatis:3.2.0") + + testImplementation("com.h2database:h2:1.4.191") +} + +tasks.withType().configureEach { + jvmArgs("-Dotel.instrumentation.mybatis.enabled=true") + + // required on jdk17 + jvmArgs("--add-opens=java.base/java.lang=ALL-UNNAMED") + jvmArgs("-XX:+IgnoreUnrecognizedVMOptions") +} diff --git a/instrumentation/mybatis-3.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mybatis/v3_2/MapperMethodInstrumentation.java b/instrumentation/mybatis-3.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mybatis/v3_2/MapperMethodInstrumentation.java new file mode 100644 index 000000000000..741005717f60 --- /dev/null +++ b/instrumentation/mybatis-3.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mybatis/v3_2/MapperMethodInstrumentation.java @@ -0,0 +1,71 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.mybatis.v3_2; + +import static io.opentelemetry.javaagent.bootstrap.Java8BytecodeBridge.currentContext; +import static io.opentelemetry.javaagent.instrumentation.mybatis.v3_2.MyBatisSingletons.instrumenter; +import static net.bytebuddy.matcher.ElementMatchers.named; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.instrumentation.api.incubator.semconv.util.ClassAndMethod; +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.ibatis.binding.MapperMethod.SqlCommand; + +public class MapperMethodInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("org.apache.ibatis.binding.MapperMethod"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + named("execute"), MapperMethodInstrumentation.class.getName() + "$ExecuteAdvice"); + } + + @SuppressWarnings("unused") + public static class ExecuteAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void getMapperInfo( + @Advice.FieldValue("command") SqlCommand command, + @Advice.Local("otelRequest") ClassAndMethod request, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + if (command == null) { + return; + } + request = SqlCommandUtil.getClassAndMethod(command); + if (request == null) { + return; + } + 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 stopSpan( + @Advice.Thrown Throwable throwable, + @Advice.Local("otelRequest") ClassAndMethod request, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + if (scope != null) { + scope.close(); + instrumenter().end(context, request, null, throwable); + } + } + } +} diff --git a/instrumentation/mybatis-3.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mybatis/v3_2/MyBatisInstrumentationModule.java b/instrumentation/mybatis-3.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mybatis/v3_2/MyBatisInstrumentationModule.java new file mode 100644 index 000000000000..55d4910ef232 --- /dev/null +++ b/instrumentation/mybatis-3.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mybatis/v3_2/MyBatisInstrumentationModule.java @@ -0,0 +1,32 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.mybatis.v3_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 io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import java.util.List; + +@AutoService(InstrumentationModule.class) +public class MyBatisInstrumentationModule extends InstrumentationModule { + + public MyBatisInstrumentationModule() { + super("mybatis", "mybatis-3.2"); + } + + @Override + public List typeInstrumentations() { + return asList(new MapperMethodInstrumentation(), new SqlCommandInstrumentation()); + } + + @Override + public boolean defaultEnabled(ConfigProperties config) { + return false; + } +} diff --git a/instrumentation/mybatis-3.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mybatis/v3_2/MyBatisSingletons.java b/instrumentation/mybatis-3.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mybatis/v3_2/MyBatisSingletons.java new file mode 100644 index 000000000000..b24eac8cce1e --- /dev/null +++ b/instrumentation/mybatis-3.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mybatis/v3_2/MyBatisSingletons.java @@ -0,0 +1,38 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.mybatis.v3_2; + +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.instrumentation.api.incubator.semconv.code.CodeAttributesExtractor; +import io.opentelemetry.instrumentation.api.incubator.semconv.code.CodeAttributesGetter; +import io.opentelemetry.instrumentation.api.incubator.semconv.code.CodeSpanNameExtractor; +import io.opentelemetry.instrumentation.api.incubator.semconv.util.ClassAndMethod; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import io.opentelemetry.instrumentation.api.instrumenter.SpanKindExtractor; + +public final class MyBatisSingletons { + private static final String INSTRUMENTATION_NAME = "io.opentelemetry.mybatis-3.2"; + private static final Instrumenter INSTRUMENTER; + + static { + CodeAttributesGetter codeAttributesGetter = + ClassAndMethod.codeAttributesGetter(); + + INSTRUMENTER = + Instrumenter.builder( + GlobalOpenTelemetry.get(), + INSTRUMENTATION_NAME, + CodeSpanNameExtractor.create(codeAttributesGetter)) + .addAttributesExtractor(CodeAttributesExtractor.create(codeAttributesGetter)) + .buildInstrumenter(SpanKindExtractor.alwaysInternal()); + } + + public static Instrumenter instrumenter() { + return INSTRUMENTER; + } + + private MyBatisSingletons() {} +} diff --git a/instrumentation/mybatis-3.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mybatis/v3_2/SqlCommandInstrumentation.java b/instrumentation/mybatis-3.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mybatis/v3_2/SqlCommandInstrumentation.java new file mode 100644 index 000000000000..c137bd294082 --- /dev/null +++ b/instrumentation/mybatis-3.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mybatis/v3_2/SqlCommandInstrumentation.java @@ -0,0 +1,44 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.mybatis.v3_2; + +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 java.lang.reflect.Method; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import org.apache.ibatis.binding.MapperMethod.SqlCommand; + +public class SqlCommandInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher typeMatcher() { + return named("org.apache.ibatis.binding.MapperMethod$SqlCommand"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isConstructor().and(takesArgument(1, Class.class)).and(takesArgument(2, Method.class)), + SqlCommandInstrumentation.class.getName() + "$ConstructorAdvice"); + } + + @SuppressWarnings("unused") + public static class ConstructorAdvice { + + @Advice.OnMethodExit(suppress = Throwable.class) + public static void onExit( + @Advice.This SqlCommand command, + @Advice.Argument(1) Class mapperInterface, + @Advice.Argument(2) Method method) { + SqlCommandUtil.setClassAndMethod(command, mapperInterface, method); + } + } +} diff --git a/instrumentation/mybatis-3.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mybatis/v3_2/SqlCommandUtil.java b/instrumentation/mybatis-3.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mybatis/v3_2/SqlCommandUtil.java new file mode 100644 index 000000000000..f67692f47229 --- /dev/null +++ b/instrumentation/mybatis-3.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mybatis/v3_2/SqlCommandUtil.java @@ -0,0 +1,29 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.mybatis.v3_2; + +import io.opentelemetry.instrumentation.api.incubator.semconv.util.ClassAndMethod; +import io.opentelemetry.instrumentation.api.util.VirtualField; +import java.lang.reflect.Method; +import org.apache.ibatis.binding.MapperMethod.SqlCommand; + +public final class SqlCommandUtil { + private static final VirtualField field = + VirtualField.find(SqlCommand.class, ClassAndMethod.class); + + public static void setClassAndMethod(SqlCommand command, Class clazz, Method method) { + if (clazz == null || method == null || method.getName() == null) { + return; + } + field.set(command, ClassAndMethod.create(clazz, method.getName())); + } + + public static ClassAndMethod getClassAndMethod(SqlCommand command) { + return field.get(command); + } + + private SqlCommandUtil() {} +} diff --git a/instrumentation/mybatis-3.2/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/mybatis/v3_2/MyBatisTest.java b/instrumentation/mybatis-3.2/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/mybatis/v3_2/MyBatisTest.java new file mode 100644 index 000000000000..a4ca40a00987 --- /dev/null +++ b/instrumentation/mybatis-3.2/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/mybatis/v3_2/MyBatisTest.java @@ -0,0 +1,66 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.mybatis.v3_2; + +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; + +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.semconv.incubating.CodeIncubatingAttributes; +import org.apache.ibatis.mapping.Environment; +import org.apache.ibatis.session.Configuration; +import org.apache.ibatis.session.SqlSession; +import org.apache.ibatis.session.SqlSessionFactoryBuilder; +import org.apache.ibatis.transaction.jdbc.JdbcTransactionFactory; +import org.h2.jdbcx.JdbcDataSource; +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; + +class MyBatisTest { + + @RegisterExtension + private static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + private static SqlSession sqlSession; + + @BeforeAll + static void setUp() { + JdbcDataSource dataSource = new JdbcDataSource(); + dataSource.setURL("jdbc:h2:mem:test;DB_CLOSE_DELAY=-1"); + Configuration configuration = new Configuration(); + configuration.setEnvironment(new Environment("test", new JdbcTransactionFactory(), dataSource)); + configuration.addMapper(TestMapper.class); + sqlSession = new SqlSessionFactoryBuilder().build(configuration).openSession(); + } + + @AfterAll + static void cleanUp() { + if (sqlSession != null) { + sqlSession.close(); + } + } + + @Test + void testSelect() { + TestMapper testMapper = sqlSession.getMapper(TestMapper.class); + testMapper.select(); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasKind(SpanKind.INTERNAL) + .hasName("TestMapper.select") + .hasAttributesSatisfyingExactly( + equalTo( + CodeIncubatingAttributes.CODE_NAMESPACE, + TestMapper.class.getName()), + equalTo(CodeIncubatingAttributes.CODE_FUNCTION, "select")))); + } +} diff --git a/instrumentation/mybatis-3.2/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/mybatis/v3_2/TestMapper.java b/instrumentation/mybatis-3.2/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/mybatis/v3_2/TestMapper.java new file mode 100644 index 000000000000..1d5e858a91ce --- /dev/null +++ b/instrumentation/mybatis-3.2/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/mybatis/v3_2/TestMapper.java @@ -0,0 +1,14 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.mybatis.v3_2; + +import org.apache.ibatis.annotations.Select; + +public interface TestMapper { + + @Select("SELECT 1") + int select(); +} diff --git a/instrumentation/netty/README.md b/instrumentation/netty/README.md index d21cd145e6ca..0ca7e5415437 100644 --- a/instrumentation/netty/README.md +++ b/instrumentation/netty/README.md @@ -1,6 +1,6 @@ # Settings for the Netty instrumentation | System property | Type | Default | Description | -|-----------------------------------------------------------|---------|---------|---------------------------------------------------------------------------------------------------| +| --------------------------------------------------------- | ------- | ------- | ------------------------------------------------------------------------------------------------- | | `otel.instrumentation.netty.connection-telemetry.enabled` | Boolean | `false` | Enable the creation of Connect and DNS spans by default for Netty 4.0 and higher instrumentation. | | `otel.instrumentation.netty.ssl-telemetry.enabled` | Boolean | `false` | Enable SSL telemetry for Netty 4.0 and higher instrumentation. | diff --git a/instrumentation/netty/netty-3.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v3_8/NettyChannelInstrumentation.java b/instrumentation/netty/netty-3.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v3_8/NettyChannelInstrumentation.java index ae09fe1377e7..6625cfbdb322 100644 --- a/instrumentation/netty/netty-3.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v3_8/NettyChannelInstrumentation.java +++ b/instrumentation/netty/netty-3.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v3_8/NettyChannelInstrumentation.java @@ -56,55 +56,54 @@ public void transform(TypeTransformer transformer) { public static class ChannelConnectAdvice { @Advice.OnMethodEnter(suppress = Throwable.class) - public static void onEnter( - @Advice.This Channel channel, - @Advice.Argument(0) SocketAddress remoteAddress, - @Advice.Local("otelParentContext") Context parentContext, - @Advice.Local("otelRequest") NettyConnectionRequest request, - @Advice.Local("otelTimer") Timer timer) { - - parentContext = Java8BytecodeBridge.currentContext(); + public static NettyScope onEnter( + @Advice.This Channel channel, @Advice.Argument(0) SocketAddress remoteAddress) { + + Context parentContext = Java8BytecodeBridge.currentContext(); Span span = Java8BytecodeBridge.spanFromContext(parentContext); if (!span.getSpanContext().isValid()) { - return; + return null; } VirtualField virtualField = VirtualField.find(Channel.class, NettyConnectionContext.class); if (virtualField.get(channel) != null) { - return; + return null; } virtualField.set(channel, new NettyConnectionContext(parentContext)); - request = NettyConnectionRequest.connect(remoteAddress); - timer = Timer.start(); + NettyConnectionRequest request = NettyConnectionRequest.connect(remoteAddress); + Timer timer = Timer.start(); + + return new NettyScope(parentContext, request, timer); } @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class) public static void onExit( @Advice.Return ChannelFuture channelFuture, @Advice.Thrown Throwable error, - @Advice.Local("otelParentContext") Context parentContext, - @Advice.Local("otelRequest") NettyConnectionRequest request, - @Advice.Local("otelTimer") Timer timer) { + @Advice.Enter NettyScope nettyScope) { - if (request == null) { + if (nettyScope == null) { return; } if (error != null) { - if (connectionInstrumenter().shouldStart(parentContext, request)) { + if (connectionInstrumenter() + .shouldStart(nettyScope.getParentContext(), nettyScope.getRequest())) { InstrumenterUtil.startAndEnd( connectionInstrumenter(), - parentContext, - request, + nettyScope.getParentContext(), + nettyScope.getRequest(), null, error, - timer.startTime(), - timer.now()); + nettyScope.getTimer().startTime(), + nettyScope.getTimer().now()); } } else { - channelFuture.addListener(new ConnectionListener(parentContext, request, timer)); + channelFuture.addListener( + new ConnectionListener( + nettyScope.getParentContext(), nettyScope.getRequest(), nettyScope.getTimer())); } } } diff --git a/instrumentation/netty/netty-3.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v3_8/NettyChannelPipelineInstrumentation.java b/instrumentation/netty/netty-3.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v3_8/NettyChannelPipelineInstrumentation.java index 0c9b546f7cb1..f5ef00b0ba0c 100644 --- a/instrumentation/netty/netty-3.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v3_8/NettyChannelPipelineInstrumentation.java +++ b/instrumentation/netty/netty-3.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v3_8/NettyChannelPipelineInstrumentation.java @@ -51,25 +51,25 @@ public void transform(TypeTransformer transformer) { public static class ChannelPipelineAdd2ArgsAdvice { @Advice.OnMethodEnter - public static void checkDepth( - @Advice.This ChannelPipeline pipeline, - @Advice.Argument(1) ChannelHandler handler, - @Advice.Local("otelCallDepth") CallDepth callDepth) { + public static CallDepth 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()); } - callDepth = CallDepth.forClass(ChannelPipeline.class); + CallDepth callDepth = CallDepth.forClass(ChannelPipeline.class); callDepth.getAndIncrement(); + return callDepth; } @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) public static void addHandler( @Advice.This ChannelPipeline pipeline, @Advice.Argument(1) ChannelHandler handler, - @Advice.Local("otelCallDepth") CallDepth callDepth) { + @Advice.Enter CallDepth callDepth) { + if (callDepth.decrementAndGet() > 0) { return; } @@ -82,29 +82,28 @@ public static void addHandler( public static class ChannelPipelineAdd3ArgsAdvice { @Advice.OnMethodEnter - public static void checkDepth( - @Advice.This ChannelPipeline pipeline, - @Advice.Argument(2) ChannelHandler handler, - @Advice.Local("otelCallDepth") CallDepth callDepth) { + public static CallDepth 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()); } - callDepth = CallDepth.forClass(ChannelPipeline.class); + CallDepth callDepth = CallDepth.forClass(ChannelPipeline.class); callDepth.getAndIncrement(); + return callDepth; } @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) public static void addHandler( @Advice.This ChannelPipeline pipeline, @Advice.Argument(2) ChannelHandler handler, - @Advice.Local("otelCallDepth") CallDepth callDepth) { + @Advice.Enter CallDepth callDepth) { + if (callDepth.decrementAndGet() > 0) { return; } - ChannelPipelineUtil.wrapHandler(pipeline, handler); } } diff --git a/instrumentation/netty/netty-3.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v3_8/NettyInstrumentationModule.java b/instrumentation/netty/netty-3.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v3_8/NettyInstrumentationModule.java index 54b65990b13f..a6cb34099421 100644 --- a/instrumentation/netty/netty-3.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v3_8/NettyInstrumentationModule.java +++ b/instrumentation/netty/netty-3.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v3_8/NettyInstrumentationModule.java @@ -11,11 +11,13 @@ 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.internal.ExperimentalInstrumentationModule; import java.util.List; import net.bytebuddy.matcher.ElementMatcher; @AutoService(InstrumentationModule.class) -public class NettyInstrumentationModule extends InstrumentationModule { +public class NettyInstrumentationModule extends InstrumentationModule + implements ExperimentalInstrumentationModule { public NettyInstrumentationModule() { super("netty", "netty-3.8"); } @@ -25,6 +27,11 @@ public ElementMatcher.Junction classLoaderMatcher() { return hasClassesNamed("org.jboss.netty.handler.codec.http.HttpMessage"); } + @Override + public String getModuleGroup() { + return "netty"; + } + @Override public List typeInstrumentations() { return asList( diff --git a/instrumentation/netty/netty-3.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v3_8/NettyScope.java b/instrumentation/netty/netty-3.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v3_8/NettyScope.java new file mode 100644 index 000000000000..56d4a34ecef7 --- /dev/null +++ b/instrumentation/netty/netty-3.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v3_8/NettyScope.java @@ -0,0 +1,36 @@ +/* + * 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.instrumentation.netty.common.internal.NettyConnectionRequest; +import io.opentelemetry.instrumentation.netty.common.internal.Timer; + +/** Container used to carry state between enter and exit advices */ +public class NettyScope { + + private final Context parentContext; + private final NettyConnectionRequest request; + private final Timer timer; + + public NettyScope(Context parentContext, NettyConnectionRequest request, Timer timer) { + this.parentContext = parentContext; + this.request = request; + this.timer = timer; + } + + public Context getParentContext() { + return parentContext; + } + + public NettyConnectionRequest getRequest() { + return request; + } + + public Timer getTimer() { + return timer; + } +} diff --git a/instrumentation/netty/netty-3.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v3_8/client/NettyClientSingletons.java b/instrumentation/netty/netty-3.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v3_8/client/NettyClientSingletons.java index c5049b1e2d7d..3db67f23dc80 100644 --- a/instrumentation/netty/netty-3.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v3_8/client/NettyClientSingletons.java +++ b/instrumentation/netty/netty-3.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v3_8/client/NettyClientSingletons.java @@ -6,16 +6,14 @@ package io.opentelemetry.javaagent.instrumentation.netty.v3_8.client; import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.instrumentation.api.incubator.semconv.http.HttpClientPeerServiceAttributesExtractor; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; import io.opentelemetry.instrumentation.api.instrumenter.SpanKindExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpClientAttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpClientMetrics; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpSpanNameExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpSpanStatusExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.net.PeerServiceAttributesExtractor; +import io.opentelemetry.instrumentation.api.semconv.http.HttpClientAttributesExtractor; import io.opentelemetry.instrumentation.netty.common.internal.NettyConnectionRequest; import io.opentelemetry.instrumentation.netty.common.internal.NettyErrorHolder; -import io.opentelemetry.javaagent.bootstrap.internal.CommonConfig; +import io.opentelemetry.javaagent.bootstrap.internal.AgentCommonConfig; +import io.opentelemetry.javaagent.bootstrap.internal.JavaagentHttpClientInstrumenters; import io.opentelemetry.javaagent.instrumentation.netty.v3_8.HttpRequestAndChannel; import org.jboss.netty.channel.Channel; import org.jboss.netty.handler.codec.http.HttpResponse; @@ -28,39 +26,25 @@ public final class NettyClientSingletons { private static final Instrumenter CONNECTION_INSTRUMENTER; static { - NettyHttpClientAttributesGetter httpAttributesGetter = new NettyHttpClientAttributesGetter(); - NettyNetClientAttributesGetter netAttributesGetter = new NettyNetClientAttributesGetter(); - INSTRUMENTER = - Instrumenter.builder( - GlobalOpenTelemetry.get(), - INSTRUMENTATION_NAME, - HttpSpanNameExtractor.create(httpAttributesGetter)) - .setSpanStatusExtractor(HttpSpanStatusExtractor.create(httpAttributesGetter)) - .addAttributesExtractor( - HttpClientAttributesExtractor.builder(httpAttributesGetter, netAttributesGetter) - .setCapturedRequestHeaders(CommonConfig.get().getClientRequestHeaders()) - .setCapturedResponseHeaders(CommonConfig.get().getClientResponseHeaders()) - .build()) - .addAttributesExtractor( - PeerServiceAttributesExtractor.create( - netAttributesGetter, CommonConfig.get().getPeerServiceMapping())) - .addOperationMetrics(HttpClientMetrics.get()) - .addContextCustomizer( - (context, requestAndChannel, startAttributes) -> NettyErrorHolder.init(context)) - .buildClientInstrumenter(HttpRequestHeadersSetter.INSTANCE); + JavaagentHttpClientInstrumenters.create( + INSTRUMENTATION_NAME, + new NettyHttpClientAttributesGetter(), + HttpRequestHeadersSetter.INSTANCE, + builder -> { + builder.addContextCustomizer( + (context, requestAndChannel, startAttributes) -> NettyErrorHolder.init(context)); + }); - NettyConnectNetAttributesGetter nettyConnectAttributesGetter = - new NettyConnectNetAttributesGetter(); CONNECTION_INSTRUMENTER = Instrumenter.builder( GlobalOpenTelemetry.get(), INSTRUMENTATION_NAME, NettyConnectionRequest::spanName) .addAttributesExtractor( - HttpClientAttributesExtractor.create( - NettyConnectHttpAttributesGetter.INSTANCE, nettyConnectAttributesGetter)) + HttpClientAttributesExtractor.create(NettyConnectHttpAttributesGetter.INSTANCE)) .addAttributesExtractor( - PeerServiceAttributesExtractor.create( - nettyConnectAttributesGetter, CommonConfig.get().getPeerServiceMapping())) + HttpClientPeerServiceAttributesExtractor.create( + NettyConnectHttpAttributesGetter.INSTANCE, + AgentCommonConfig.get().getPeerServiceResolver())) .buildInstrumenter(SpanKindExtractor.alwaysClient()); } diff --git a/instrumentation/netty/netty-3.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v3_8/client/NettyConnectHttpAttributesGetter.java b/instrumentation/netty/netty-3.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v3_8/client/NettyConnectHttpAttributesGetter.java index 133dcae511ad..e95327ccdfd8 100644 --- a/instrumentation/netty/netty-3.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v3_8/client/NettyConnectHttpAttributesGetter.java +++ b/instrumentation/netty/netty-3.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v3_8/client/NettyConnectHttpAttributesGetter.java @@ -5,8 +5,11 @@ package io.opentelemetry.javaagent.instrumentation.netty.v3_8.client; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpClientAttributesGetter; +import io.opentelemetry.instrumentation.api.semconv.http.HttpClientAttributesGetter; import io.opentelemetry.instrumentation.netty.common.internal.NettyConnectionRequest; +import io.opentelemetry.javaagent.instrumentation.netty.v3_8.util.ChannelUtil; +import java.net.InetSocketAddress; +import java.net.SocketAddress; import java.util.Collections; import java.util.List; import javax.annotation.Nullable; @@ -46,4 +49,43 @@ public List getHttpResponseHeader( NettyConnectionRequest nettyConnectionRequest, Channel channel, String name) { return Collections.emptyList(); } + + @Override + public String getNetworkTransport(NettyConnectionRequest request, @Nullable Channel channel) { + return ChannelUtil.getNetworkTransport(channel); + } + + @Nullable + @Override + public String getServerAddress(NettyConnectionRequest request) { + SocketAddress requestedAddress = request.remoteAddressOnStart(); + if (requestedAddress instanceof InetSocketAddress) { + return ((InetSocketAddress) requestedAddress).getHostString(); + } + return null; + } + + @Nullable + @Override + public Integer getServerPort(NettyConnectionRequest request) { + SocketAddress requestedAddress = request.remoteAddressOnStart(); + if (requestedAddress instanceof InetSocketAddress) { + return ((InetSocketAddress) requestedAddress).getPort(); + } + return null; + } + + @Nullable + @Override + public InetSocketAddress getNetworkPeerInetSocketAddress( + NettyConnectionRequest request, @Nullable Channel channel) { + if (channel == null) { + return null; + } + SocketAddress remoteAddress = channel.getRemoteAddress(); + if (remoteAddress instanceof InetSocketAddress) { + return (InetSocketAddress) remoteAddress; + } + return null; + } } diff --git a/instrumentation/netty/netty-3.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v3_8/client/NettyConnectNetAttributesGetter.java b/instrumentation/netty/netty-3.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v3_8/client/NettyConnectNetAttributesGetter.java deleted file mode 100644 index 1222249525a9..000000000000 --- a/instrumentation/netty/netty-3.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v3_8/client/NettyConnectNetAttributesGetter.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.javaagent.instrumentation.netty.v3_8.client; - -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.NetTransportValues.IP_TCP; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.NetTransportValues.IP_UDP; - -import io.opentelemetry.instrumentation.api.instrumenter.net.NetClientAttributesGetter; -import io.opentelemetry.instrumentation.netty.common.internal.NettyConnectionRequest; -import io.opentelemetry.javaagent.instrumentation.netty.v3_8.util.ChannelUtil; -import java.net.InetSocketAddress; -import java.net.SocketAddress; -import javax.annotation.Nullable; -import org.jboss.netty.channel.Channel; -import org.jboss.netty.channel.socket.DatagramChannel; - -final class NettyConnectNetAttributesGetter - implements NetClientAttributesGetter { - - @Override - public String getTransport(NettyConnectionRequest request, @Nullable Channel channel) { - return channel instanceof DatagramChannel ? IP_UDP : IP_TCP; - } - - @Override - public String getNetworkTransport(NettyConnectionRequest request, @Nullable Channel channel) { - return ChannelUtil.getNetworkTransport(channel); - } - - @Nullable - @Override - public String getServerAddress(NettyConnectionRequest request) { - SocketAddress requestedAddress = request.remoteAddressOnStart(); - if (requestedAddress instanceof InetSocketAddress) { - return ((InetSocketAddress) requestedAddress).getHostString(); - } - return null; - } - - @Nullable - @Override - public Integer getServerPort(NettyConnectionRequest request) { - SocketAddress requestedAddress = request.remoteAddressOnStart(); - if (requestedAddress instanceof InetSocketAddress) { - return ((InetSocketAddress) requestedAddress).getPort(); - } - return null; - } - - @Nullable - @Override - public InetSocketAddress getServerInetSocketAddress( - NettyConnectionRequest request, @Nullable Channel channel) { - if (channel == null) { - return null; - } - SocketAddress remoteAddress = channel.getRemoteAddress(); - if (remoteAddress instanceof InetSocketAddress) { - return (InetSocketAddress) remoteAddress; - } - return null; - } -} diff --git a/instrumentation/netty/netty-3.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v3_8/client/NettyHttpClientAttributesGetter.java b/instrumentation/netty/netty-3.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v3_8/client/NettyHttpClientAttributesGetter.java index 2d61fb563185..ff9a53e02ba8 100644 --- a/instrumentation/netty/netty-3.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v3_8/client/NettyHttpClientAttributesGetter.java +++ b/instrumentation/netty/netty-3.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v3_8/client/NettyHttpClientAttributesGetter.java @@ -7,13 +7,17 @@ import static io.opentelemetry.javaagent.instrumentation.netty.v3_8.util.HttpSchemeUtil.getScheme; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpClientAttributesGetter; +import io.opentelemetry.instrumentation.api.semconv.http.HttpClientAttributesGetter; import io.opentelemetry.javaagent.instrumentation.netty.v3_8.HttpRequestAndChannel; +import io.opentelemetry.javaagent.instrumentation.netty.v3_8.util.ChannelUtil; +import java.net.InetSocketAddress; +import java.net.SocketAddress; import java.net.URI; import java.net.URISyntaxException; import java.util.List; import javax.annotation.Nullable; import org.jboss.netty.handler.codec.http.HttpResponse; +import org.jboss.netty.handler.codec.http.HttpVersion; final class NettyHttpClientAttributesGetter implements HttpClientAttributesGetter { @@ -60,4 +64,49 @@ public List getHttpResponseHeader( HttpRequestAndChannel requestAndChannel, HttpResponse response, String name) { return response.headers().getAll(name); } + + @Override + public String getNetworkTransport( + HttpRequestAndChannel requestAndChannel, @Nullable HttpResponse response) { + return ChannelUtil.getNetworkTransport(requestAndChannel.channel()); + } + + @Override + public String getNetworkProtocolName( + HttpRequestAndChannel requestAndChannel, @Nullable HttpResponse httpResponse) { + return requestAndChannel.request().getProtocolVersion().getProtocolName(); + } + + @Override + public String getNetworkProtocolVersion( + HttpRequestAndChannel requestAndChannel, @Nullable HttpResponse httpResponse) { + HttpVersion version = requestAndChannel.request().getProtocolVersion(); + if (version.getMinorVersion() == 0) { + return Integer.toString(version.getMajorVersion()); + } + return version.getMajorVersion() + "." + version.getMinorVersion(); + } + + @Nullable + @Override + public String getServerAddress(HttpRequestAndChannel requestAndChannel) { + return null; + } + + @Nullable + @Override + public Integer getServerPort(HttpRequestAndChannel requestAndChannel) { + return null; + } + + @Override + @Nullable + public InetSocketAddress getNetworkPeerInetSocketAddress( + HttpRequestAndChannel requestAndChannel, @Nullable HttpResponse response) { + SocketAddress address = requestAndChannel.channel().getRemoteAddress(); + if (address instanceof InetSocketAddress) { + return (InetSocketAddress) address; + } + return null; + } } diff --git a/instrumentation/netty/netty-3.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v3_8/client/NettyNetClientAttributesGetter.java b/instrumentation/netty/netty-3.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v3_8/client/NettyNetClientAttributesGetter.java deleted file mode 100644 index 67acad5035ed..000000000000 --- a/instrumentation/netty/netty-3.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v3_8/client/NettyNetClientAttributesGetter.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.javaagent.instrumentation.netty.v3_8.client; - -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.NetTransportValues.IP_TCP; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.NetTransportValues.IP_UDP; - -import io.opentelemetry.instrumentation.api.instrumenter.net.NetClientAttributesGetter; -import io.opentelemetry.javaagent.instrumentation.netty.v3_8.HttpRequestAndChannel; -import io.opentelemetry.javaagent.instrumentation.netty.v3_8.util.ChannelUtil; -import java.net.InetSocketAddress; -import java.net.SocketAddress; -import javax.annotation.Nullable; -import org.jboss.netty.channel.socket.DatagramChannel; -import org.jboss.netty.handler.codec.http.HttpResponse; -import org.jboss.netty.handler.codec.http.HttpVersion; - -final class NettyNetClientAttributesGetter - implements NetClientAttributesGetter { - - @Override - public String getTransport( - HttpRequestAndChannel requestAndChannel, @Nullable HttpResponse response) { - return requestAndChannel.channel() instanceof DatagramChannel ? IP_UDP : IP_TCP; - } - - @Override - public String getNetworkTransport( - HttpRequestAndChannel requestAndChannel, @Nullable HttpResponse response) { - return ChannelUtil.getNetworkTransport(requestAndChannel.channel()); - } - - @Override - public String getNetworkProtocolName( - HttpRequestAndChannel requestAndChannel, @Nullable HttpResponse httpResponse) { - return requestAndChannel.request().getProtocolVersion().getProtocolName(); - } - - @Override - public String getNetworkProtocolVersion( - HttpRequestAndChannel requestAndChannel, @Nullable HttpResponse httpResponse) { - HttpVersion version = requestAndChannel.request().getProtocolVersion(); - return version.getMajorVersion() + "." + version.getMinorVersion(); - } - - @Nullable - @Override - public String getServerAddress(HttpRequestAndChannel requestAndChannel) { - return null; - } - - @Nullable - @Override - public Integer getServerPort(HttpRequestAndChannel requestAndChannel) { - return null; - } - - @Override - @Nullable - public InetSocketAddress getServerInetSocketAddress( - HttpRequestAndChannel requestAndChannel, @Nullable HttpResponse response) { - SocketAddress address = requestAndChannel.channel().getRemoteAddress(); - if (address instanceof InetSocketAddress) { - return (InetSocketAddress) address; - } - return null; - } -} diff --git a/instrumentation/netty/netty-3.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v3_8/server/NettyHttpServerAttributesGetter.java b/instrumentation/netty/netty-3.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v3_8/server/NettyHttpServerAttributesGetter.java index 7b2661430014..237e83a01a53 100644 --- a/instrumentation/netty/netty-3.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v3_8/server/NettyHttpServerAttributesGetter.java +++ b/instrumentation/netty/netty-3.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v3_8/server/NettyHttpServerAttributesGetter.java @@ -5,12 +5,16 @@ package io.opentelemetry.javaagent.instrumentation.netty.v3_8.server; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpServerAttributesGetter; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerAttributesGetter; import io.opentelemetry.javaagent.instrumentation.netty.v3_8.HttpRequestAndChannel; +import io.opentelemetry.javaagent.instrumentation.netty.v3_8.util.ChannelUtil; import io.opentelemetry.javaagent.instrumentation.netty.v3_8.util.HttpSchemeUtil; +import java.net.InetSocketAddress; +import java.net.SocketAddress; import java.util.List; import javax.annotation.Nullable; import org.jboss.netty.handler.codec.http.HttpResponse; +import org.jboss.netty.handler.codec.http.HttpVersion; final class NettyHttpServerAttributesGetter implements HttpServerAttributesGetter { @@ -55,4 +59,48 @@ public String getUrlQuery(HttpRequestAndChannel requestAndChannel) { int separatorPos = fullPath.indexOf('?'); return separatorPos == -1 ? null : fullPath.substring(separatorPos + 1); } + + @Override + public String getNetworkTransport( + HttpRequestAndChannel requestAndChannel, HttpResponse response) { + return ChannelUtil.getNetworkTransport(requestAndChannel.channel()); + } + + @Override + public String getNetworkProtocolName( + HttpRequestAndChannel requestAndChannel, @Nullable HttpResponse response) { + return requestAndChannel.request().getProtocolVersion().getProtocolName(); + } + + @Override + public String getNetworkProtocolVersion( + HttpRequestAndChannel requestAndChannel, @Nullable HttpResponse response) { + HttpVersion version = requestAndChannel.request().getProtocolVersion(); + if (version.getMinorVersion() == 0) { + return Integer.toString(version.getMajorVersion()); + } + return version.getMajorVersion() + "." + version.getMinorVersion(); + } + + @Override + @Nullable + public InetSocketAddress getNetworkPeerInetSocketAddress( + HttpRequestAndChannel requestAndChannel, @Nullable HttpResponse response) { + SocketAddress address = requestAndChannel.channel().getRemoteAddress(); + if (address instanceof InetSocketAddress) { + return (InetSocketAddress) address; + } + return null; + } + + @Nullable + @Override + public InetSocketAddress getNetworkLocalInetSocketAddress( + HttpRequestAndChannel requestAndChannel, @Nullable HttpResponse response) { + SocketAddress address = requestAndChannel.channel().getLocalAddress(); + if (address instanceof InetSocketAddress) { + return (InetSocketAddress) address; + } + return null; + } } diff --git a/instrumentation/netty/netty-3.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v3_8/server/NettyNetServerAttributesGetter.java b/instrumentation/netty/netty-3.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v3_8/server/NettyNetServerAttributesGetter.java deleted file mode 100644 index 269a7b16a219..000000000000 --- a/instrumentation/netty/netty-3.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v3_8/server/NettyNetServerAttributesGetter.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.javaagent.instrumentation.netty.v3_8.server; - -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.NetTransportValues.IP_TCP; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.NetTransportValues.IP_UDP; - -import io.opentelemetry.instrumentation.api.instrumenter.net.NetServerAttributesGetter; -import io.opentelemetry.javaagent.instrumentation.netty.v3_8.HttpRequestAndChannel; -import io.opentelemetry.javaagent.instrumentation.netty.v3_8.util.ChannelUtil; -import java.net.InetSocketAddress; -import java.net.SocketAddress; -import javax.annotation.Nullable; -import org.jboss.netty.channel.socket.DatagramChannel; -import org.jboss.netty.handler.codec.http.HttpResponse; -import org.jboss.netty.handler.codec.http.HttpVersion; - -final class NettyNetServerAttributesGetter - implements NetServerAttributesGetter { - - @Override - public String getTransport(HttpRequestAndChannel requestAndChannel) { - return requestAndChannel.channel() instanceof DatagramChannel ? IP_UDP : IP_TCP; - } - - @Override - public String getNetworkTransport( - HttpRequestAndChannel requestAndChannel, HttpResponse response) { - return ChannelUtil.getNetworkTransport(requestAndChannel.channel()); - } - - @Override - public String getNetworkProtocolName( - HttpRequestAndChannel requestAndChannel, @Nullable HttpResponse response) { - return requestAndChannel.request().getProtocolVersion().getProtocolName(); - } - - @Override - public String getNetworkProtocolVersion( - HttpRequestAndChannel requestAndChannel, @Nullable HttpResponse response) { - HttpVersion version = requestAndChannel.request().getProtocolVersion(); - return version.getMajorVersion() + "." + version.getMinorVersion(); - } - - @Nullable - @Override - public String getServerAddress(HttpRequestAndChannel requestAndChannel) { - return null; - } - - @Nullable - @Override - public Integer getServerPort(HttpRequestAndChannel requestAndChannel) { - return null; - } - - @Override - @Nullable - public InetSocketAddress getClientInetSocketAddress( - HttpRequestAndChannel requestAndChannel, @Nullable HttpResponse response) { - SocketAddress address = requestAndChannel.channel().getRemoteAddress(); - if (address instanceof InetSocketAddress) { - return (InetSocketAddress) address; - } - return null; - } - - @Nullable - @Override - public InetSocketAddress getServerInetSocketAddress( - HttpRequestAndChannel requestAndChannel, @Nullable HttpResponse response) { - SocketAddress address = requestAndChannel.channel().getLocalAddress(); - if (address instanceof InetSocketAddress) { - return (InetSocketAddress) address; - } - return null; - } -} diff --git a/instrumentation/netty/netty-3.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v3_8/server/NettyServerSingletons.java b/instrumentation/netty/netty-3.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v3_8/server/NettyServerSingletons.java index cc99eb009d7c..5a5719efe676 100644 --- a/instrumentation/netty/netty-3.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v3_8/server/NettyServerSingletons.java +++ b/instrumentation/netty/netty-3.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v3_8/server/NettyServerSingletons.java @@ -6,14 +6,17 @@ package io.opentelemetry.javaagent.instrumentation.netty.v3_8.server; import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.instrumentation.api.incubator.semconv.http.HttpExperimentalAttributesExtractor; +import io.opentelemetry.instrumentation.api.incubator.semconv.http.HttpServerExperimentalMetrics; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteHolder; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpServerAttributesExtractor; -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 io.opentelemetry.instrumentation.api.instrumenter.InstrumenterBuilder; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerAttributesExtractor; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerMetrics; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRoute; +import io.opentelemetry.instrumentation.api.semconv.http.HttpSpanNameExtractor; +import io.opentelemetry.instrumentation.api.semconv.http.HttpSpanStatusExtractor; import io.opentelemetry.instrumentation.netty.common.internal.NettyErrorHolder; -import io.opentelemetry.javaagent.bootstrap.internal.CommonConfig; +import io.opentelemetry.javaagent.bootstrap.internal.AgentCommonConfig; import io.opentelemetry.javaagent.instrumentation.netty.v3_8.HttpRequestAndChannel; import org.jboss.netty.handler.codec.http.HttpResponse; @@ -22,25 +25,36 @@ final class NettyServerSingletons { private static final Instrumenter INSTRUMENTER; static { - NettyHttpServerAttributesGetter httpServerAttributesGetter = - new NettyHttpServerAttributesGetter(); + NettyHttpServerAttributesGetter httpAttributesGetter = new NettyHttpServerAttributesGetter(); - INSTRUMENTER = + InstrumenterBuilder builder = Instrumenter.builder( GlobalOpenTelemetry.get(), "io.opentelemetry.netty-3.8", - HttpSpanNameExtractor.create(httpServerAttributesGetter)) - .setSpanStatusExtractor(HttpSpanStatusExtractor.create(httpServerAttributesGetter)) + HttpSpanNameExtractor.builder(httpAttributesGetter) + .setKnownMethods(AgentCommonConfig.get().getKnownHttpRequestMethods()) + .build()) + .setSpanStatusExtractor(HttpSpanStatusExtractor.create(httpAttributesGetter)) .addAttributesExtractor( - HttpServerAttributesExtractor.builder( - httpServerAttributesGetter, new NettyNetServerAttributesGetter()) - .setCapturedRequestHeaders(CommonConfig.get().getServerRequestHeaders()) - .setCapturedResponseHeaders(CommonConfig.get().getServerResponseHeaders()) + HttpServerAttributesExtractor.builder(httpAttributesGetter) + .setCapturedRequestHeaders(AgentCommonConfig.get().getServerRequestHeaders()) + .setCapturedResponseHeaders(AgentCommonConfig.get().getServerResponseHeaders()) + .setKnownMethods(AgentCommonConfig.get().getKnownHttpRequestMethods()) .build()) - .addOperationMetrics(HttpServerMetrics.get()) + .addOperationMetrics(HttpServerMetrics.get()); + if (AgentCommonConfig.get().shouldEmitExperimentalHttpServerTelemetry()) { + builder + .addAttributesExtractor(HttpExperimentalAttributesExtractor.create(httpAttributesGetter)) + .addOperationMetrics(HttpServerExperimentalMetrics.get()); + } + INSTRUMENTER = + builder .addContextCustomizer( (context, requestAndChannel, startAttributes) -> NettyErrorHolder.init(context)) - .addContextCustomizer(HttpRouteHolder.create(httpServerAttributesGetter)) + .addContextCustomizer( + HttpServerRoute.builder(httpAttributesGetter) + .setKnownMethods(AgentCommonConfig.get().getKnownHttpRequestMethods()) + .build()) .buildServerInstrumenter(NettyHeadersGetter.INSTANCE); } diff --git a/instrumentation/netty/netty-3.8/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/netty/v3_8/client/Netty38ClientTest.java b/instrumentation/netty/netty-3.8/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/netty/v3_8/client/Netty38ClientTest.java index aa0c57b90dc3..30cfa49d7377 100644 --- a/instrumentation/netty/netty-3.8/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/netty/v3_8/client/Netty38ClientTest.java +++ b/instrumentation/netty/netty-3.8/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/netty/v3_8/client/Netty38ClientTest.java @@ -21,7 +21,7 @@ import io.opentelemetry.instrumentation.testing.junit.http.HttpClientInstrumentationExtension; import io.opentelemetry.instrumentation.testing.junit.http.HttpClientResult; import io.opentelemetry.instrumentation.testing.junit.http.HttpClientTestOptions; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.opentelemetry.semconv.ServerAttributes; import java.lang.reflect.Method; import java.net.ConnectException; import java.net.URI; @@ -37,14 +37,12 @@ class Netty38ClientTest extends AbstractHttpClientTest { @RegisterExtension static final InstrumentationExtension testing = HttpClientInstrumentationExtension.forAgent(); - static final String USER_AGENT = "test-user-agent"; - AsyncHttpClient client; @BeforeEach void setUp() throws Exception { AsyncHttpClientConfig.Builder builder = - new AsyncHttpClientConfig.Builder().setUserAgent(USER_AGENT); + new AsyncHttpClientConfig.Builder().setUserAgent("test-user-agent"); Method setConnectTimeout; try { @@ -140,8 +138,6 @@ protected void configure(HttpClientTestOptions.Builder optionsBuilder) { optionsBuilder.disableTestHttps(); optionsBuilder.disableTestReadTimeout(); - optionsBuilder.setUserAgent(USER_AGENT); - optionsBuilder.setExpectedClientSpanNameMapper( (uri, method) -> { // unopened port or non routable address @@ -174,8 +170,8 @@ protected void configure(HttpClientTestOptions.Builder optionsBuilder) { } Set> attributes = new HashSet<>(HttpClientTestOptions.DEFAULT_HTTP_ATTRIBUTES); - attributes.remove(SemanticAttributes.NET_PEER_NAME); - attributes.remove(SemanticAttributes.NET_PEER_PORT); + attributes.remove(ServerAttributes.SERVER_ADDRESS); + attributes.remove(ServerAttributes.SERVER_PORT); return attributes; }); } diff --git a/instrumentation/netty/netty-3.8/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/netty/v3_8/server/Netty38ServerTest.java b/instrumentation/netty/netty-3.8/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/netty/v3_8/server/Netty38ServerTest.java index c8f466d79d79..a1809bc80c69 100644 --- a/instrumentation/netty/netty-3.8/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/netty/v3_8/server/Netty38ServerTest.java +++ b/instrumentation/netty/netty-3.8/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/netty/v3_8/server/Netty38ServerTest.java @@ -25,7 +25,7 @@ import io.opentelemetry.instrumentation.testing.junit.http.HttpServerInstrumentationExtension; import io.opentelemetry.instrumentation.testing.junit.http.HttpServerTestOptions; import io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.opentelemetry.semconv.HttpAttributes; import java.net.InetSocketAddress; import java.net.URI; import java.util.HashSet; @@ -91,7 +91,7 @@ protected void configure(HttpServerTestOptions options) { serverEndpoint -> { Set> attributes = new HashSet<>(HttpServerTestOptions.DEFAULT_HTTP_ATTRIBUTES); - attributes.remove(SemanticAttributes.HTTP_ROUTE); + attributes.remove(HttpAttributes.HTTP_ROUTE); return attributes; }); diff --git a/instrumentation/netty/netty-4-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4/common/AbstractNettyChannelPipelineInstrumentation.java b/instrumentation/netty/netty-4-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4/common/AbstractNettyChannelPipelineInstrumentation.java index c2c65fb42d92..155927b915fb 100644 --- a/instrumentation/netty/netty-4-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4/common/AbstractNettyChannelPipelineInstrumentation.java +++ b/instrumentation/netty/netty-4-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4/common/AbstractNettyChannelPipelineInstrumentation.java @@ -9,6 +9,7 @@ import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.implementsInterface; 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; @@ -18,7 +19,10 @@ import io.opentelemetry.instrumentation.api.util.VirtualField; import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import java.util.Iterator; +import java.util.Map; import net.bytebuddy.asm.Advice; +import net.bytebuddy.asm.Advice.AssignReturned.ToArguments.ToArgument; import net.bytebuddy.description.type.TypeDescription; import net.bytebuddy.matcher.ElementMatcher; @@ -38,14 +42,14 @@ public ElementMatcher typeMatcher() { public void transform(TypeTransformer transformer) { transformer.applyAdviceToMethod( isMethod() - .and(named("remove").or(named("replace"))) + .and(namedOneOf("remove", "replace")) .and(takesArgument(0, named("io.netty.channel.ChannelHandler"))), AbstractNettyChannelPipelineInstrumentation.class.getName() + "$RemoveAdvice"); transformer.applyAdviceToMethod( - isMethod().and(named("remove").or(named("replace"))).and(takesArgument(0, String.class)), + isMethod().and(namedOneOf("remove", "replace")).and(takesArgument(0, String.class)), AbstractNettyChannelPipelineInstrumentation.class.getName() + "$RemoveByNameAdvice"); transformer.applyAdviceToMethod( - isMethod().and(named("remove").or(named("replace"))).and(takesArgument(0, Class.class)), + isMethod().and(namedOneOf("remove", "replace")).and(takesArgument(0, Class.class)), AbstractNettyChannelPipelineInstrumentation.class.getName() + "$RemoveByClassAdvice"); transformer.applyAdviceToMethod( isMethod().and(named("removeFirst")).and(returns(named("io.netty.channel.ChannelHandler"))), @@ -59,6 +63,9 @@ public void transform(TypeTransformer transformer) { .and(takesArgument(1, String.class)) .and(takesArguments(4)), AbstractNettyChannelPipelineInstrumentation.class.getName() + "$AddAfterAdvice"); + transformer.applyAdviceToMethod( + isMethod().and(named("toMap")).and(takesArguments(0)).and(returns(Map.class)), + AbstractNettyChannelPipelineInstrumentation.class.getName() + "$ToMapAdvice"); } @SuppressWarnings("unused") @@ -71,7 +78,9 @@ public static void removeHandler( VirtualField.find(ChannelHandler.class, ChannelHandler.class); ChannelHandler ourHandler = virtualField.get(handler); if (ourHandler != null) { - pipeline.remove(ourHandler); + if (pipeline.context(ourHandler) != null) { + pipeline.remove(ourHandler); + } virtualField.set(handler, null); } } @@ -92,7 +101,9 @@ public static void removeHandler( VirtualField.find(ChannelHandler.class, ChannelHandler.class); ChannelHandler ourHandler = virtualField.get(handler); if (ourHandler != null) { - pipeline.remove(ourHandler); + if (pipeline.context(ourHandler) != null) { + pipeline.remove(ourHandler); + } virtualField.set(handler, null); } } @@ -114,7 +125,9 @@ public static void removeHandler( VirtualField.find(ChannelHandler.class, ChannelHandler.class); ChannelHandler ourHandler = virtualField.get(handler); if (ourHandler != null) { - pipeline.remove(ourHandler); + if (pipeline.context(ourHandler) != null) { + pipeline.remove(ourHandler); + } virtualField.set(handler, null); } } @@ -130,7 +143,9 @@ public static void removeHandler( VirtualField.find(ChannelHandler.class, ChannelHandler.class); ChannelHandler ourHandler = virtualField.get(handler); if (ourHandler != null) { - pipeline.remove(ourHandler); + if (pipeline.context(ourHandler) != null) { + pipeline.remove(ourHandler); + } virtualField.set(handler, null); } } @@ -140,25 +155,31 @@ public static void removeHandler( public static class RemoveLastAdvice { @Advice.OnMethodExit(suppress = Throwable.class) - public static void removeHandler( - @Advice.This ChannelPipeline pipeline, @Advice.Return ChannelHandler handler) { + @Advice.AssignReturned.ToReturned + public static ChannelHandler removeHandler( + @Advice.This ChannelPipeline pipeline, @Advice.Return ChannelHandler returnHandler) { VirtualField virtualField = VirtualField.find(ChannelHandler.class, ChannelHandler.class); + // TODO remove this extra variable when migrating to "indy only" instrumentation. + ChannelHandler handler = returnHandler; ChannelHandler ourHandler = virtualField.get(handler); if (ourHandler != null) { - pipeline.remove(ourHandler); + // Context is null when our handler has already been removed. This happens when calling + // removeLast first removed our handler and we called removeLast again to remove the http + // handler. + if (pipeline.context(ourHandler) != null) { + pipeline.remove(ourHandler); + } virtualField.set(handler, null); - } else if (handler - .getClass() - .getName() - .startsWith("io.opentelemetry.javaagent.instrumentation.netty.")) { - pipeline.removeLast(); - } else if (handler - .getClass() - .getName() - .startsWith("io.opentelemetry.instrumentation.netty.")) { - pipeline.removeLast(); + } else { + String handlerClassName = handler.getClass().getName(); + if (handlerClassName.endsWith("TracingHandler") + && (handlerClassName.startsWith("io.opentelemetry.javaagent.instrumentation.netty.") + || handlerClassName.startsWith("io.opentelemetry.instrumentation.netty."))) { + handler = pipeline.removeLast(); + } } + return handler; } } @@ -166,9 +187,14 @@ public static void removeHandler( public static class AddAfterAdvice { @Advice.OnMethodEnter(suppress = Throwable.class) - public static void addAfterHandler( - @Advice.This ChannelPipeline pipeline, - @Advice.Argument(value = 1, readOnly = false) String name) { + @Advice.AssignReturned.ToArguments(@ToArgument(1)) + public static String addAfterHandler( + @Advice.This ChannelPipeline pipeline, @Advice.Argument(value = 1) String nameArg) { + // TODO remove this extra variable when migrating to "indy only" instrumentation. + // using an intermediate variable is required to keep the advice work with "inlined" and + // "indy" this is probably a minor side-effect of using @Advice.AssignReturned.ToArguments + // with and inlined advice. + String name = nameArg; ChannelHandler handler = pipeline.get(name); if (handler != null) { VirtualField virtualField = @@ -178,6 +204,26 @@ public static void addAfterHandler( name = ourHandler.getClass().getName(); } } + return name; + } + } + + @SuppressWarnings("unused") + public static class ToMapAdvice { + + @Advice.OnMethodExit(suppress = Throwable.class) + public static void wrapIterator(@Advice.Return Map map) { + VirtualField virtualField = + VirtualField.find(ChannelHandler.class, ChannelHandler.class); + for (Iterator iterator = map.values().iterator(); iterator.hasNext(); ) { + ChannelHandler handler = iterator.next(); + String handlerClassName = handler.getClass().getName(); + if (handlerClassName.endsWith("TracingHandler") + && (handlerClassName.startsWith("io.opentelemetry.javaagent.instrumentation.netty.") + || handlerClassName.startsWith("io.opentelemetry.instrumentation.netty."))) { + iterator.remove(); + } + } } } } diff --git a/instrumentation/netty/netty-4-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4/common/FutureListenerWrappers.java b/instrumentation/netty/netty-4-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4/common/FutureListenerWrappers.java index 3f0d3f764e3d..b1a7e6eb24c4 100644 --- a/instrumentation/netty/netty-4-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4/common/FutureListenerWrappers.java +++ b/instrumentation/netty/netty-4-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4/common/FutureListenerWrappers.java @@ -43,7 +43,7 @@ protected Boolean computeValue(Class type) { }; public static boolean shouldWrap(GenericFutureListener> listener) { - return shouldWrap.get(listener.getClass()); + return listener != null && shouldWrap.get(listener.getClass()); } @SuppressWarnings("unchecked") diff --git a/instrumentation/netty/netty-4-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4/common/NettyFutureInstrumentation.java b/instrumentation/netty/netty-4-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4/common/NettyFutureInstrumentation.java index 2948fd77b656..697cca48f05e 100644 --- a/instrumentation/netty/netty-4-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4/common/NettyFutureInstrumentation.java +++ b/instrumentation/netty/netty-4-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4/common/NettyFutureInstrumentation.java @@ -19,6 +19,7 @@ import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; import net.bytebuddy.asm.Advice; +import net.bytebuddy.asm.Advice.AssignReturned.ToArguments.ToArgument; import net.bytebuddy.description.type.TypeDescription; import net.bytebuddy.matcher.ElementMatcher; @@ -58,22 +59,30 @@ public void transform(TypeTransformer transformer) { public static class AddListenerAdvice { @Advice.OnMethodEnter - public static void wrapListener( - @Advice.Argument(value = 0, readOnly = false) - GenericFutureListener> listener) { + @Advice.AssignReturned.ToArguments(@ToArgument(0)) + public static GenericFutureListener> wrapListener( + @Advice.Argument(value = 0) GenericFutureListener> listenerArg) { + + // TODO remove this extra variable when migrating to "indy only" instrumentation. + GenericFutureListener> listener = listenerArg; if (FutureListenerWrappers.shouldWrap(listener)) { listener = FutureListenerWrappers.wrap(Java8BytecodeBridge.currentContext(), listener); } + return listener; } } @SuppressWarnings("unused") public static class AddListenersAdvice { + // here the AsScalar allows to assign the value of the returned array to the argument value, + // otherwise it's considered to be an Object[] that contains the arguments/return value/thrown + // exception assignments that bytebuddy has to do after the advice is invoked. @Advice.OnMethodEnter - public static void wrapListener( - @Advice.Argument(value = 0, readOnly = false) - GenericFutureListener>[] listeners) { + @Advice.AssignReturned.AsScalar + @Advice.AssignReturned.ToArguments(@ToArgument(0)) + public static Object[] wrapListener( + @Advice.Argument(value = 0) GenericFutureListener>[] listeners) { Context context = Java8BytecodeBridge.currentContext(); @SuppressWarnings({"unchecked", "rawtypes"}) @@ -82,9 +91,11 @@ public static void wrapListener( for (int i = 0; i < listeners.length; ++i) { if (FutureListenerWrappers.shouldWrap(listeners[i])) { wrappedListeners[i] = FutureListenerWrappers.wrap(context, listeners[i]); + } else { + wrappedListeners[i] = listeners[i]; } } - listeners = wrappedListeners; + return wrappedListeners; } } @@ -92,20 +103,24 @@ public static void wrapListener( public static class RemoveListenerAdvice { @Advice.OnMethodEnter - public static void wrapListener( - @Advice.Argument(value = 0, readOnly = false) - GenericFutureListener> listener) { - listener = FutureListenerWrappers.getWrapper(listener); + @Advice.AssignReturned.ToArguments(@ToArgument(0)) + public static GenericFutureListener> wrapListener( + @Advice.Argument(value = 0) GenericFutureListener> listener) { + return FutureListenerWrappers.getWrapper(listener); } } @SuppressWarnings("unused") public static class RemoveListenersAdvice { + // here the AsScalar allows to assign the value of the returned array to the argument value, + // otherwise it's considered to be an Object[] that contains the arguments/return value/thrown + // exception assignments that bytebuddy has to do after the advice is invoked. @Advice.OnMethodEnter - public static void wrapListener( - @Advice.Argument(value = 0, readOnly = false) - GenericFutureListener>[] listeners) { + @Advice.AssignReturned.AsScalar + @Advice.AssignReturned.ToArguments(@ToArgument(0)) + public static Object[] wrapListener( + @Advice.Argument(value = 0) GenericFutureListener>[] listeners) { @SuppressWarnings({"unchecked", "rawtypes"}) GenericFutureListener>[] wrappedListeners = @@ -113,7 +128,7 @@ public static void wrapListener( for (int i = 0; i < listeners.length; ++i) { wrappedListeners[i] = FutureListenerWrappers.getWrapper(listeners[i]); } - listeners = wrappedListeners; + return wrappedListeners; } } } diff --git a/instrumentation/netty/netty-4-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4/common/NettyScope.java b/instrumentation/netty/netty-4-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4/common/NettyScope.java new file mode 100644 index 000000000000..94b7f997b24f --- /dev/null +++ b/instrumentation/netty/netty-4-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4/common/NettyScope.java @@ -0,0 +1,55 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.netty.v4.common; + +import io.netty.channel.ChannelPromise; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.instrumentation.netty.common.internal.NettyConnectionRequest; +import io.opentelemetry.instrumentation.netty.v4.common.internal.client.ConnectionCompleteListener; +import io.opentelemetry.instrumentation.netty.v4.common.internal.client.NettyConnectionInstrumenter; + +/** Container used to carry state between enter and exit advices */ +public class NettyScope { + + private final Context context; + private final NettyConnectionRequest request; + private final Scope scope; + + private NettyScope(Context context, NettyConnectionRequest request, Scope scope) { + this.context = context; + this.request = request; + this.scope = scope; + } + + public static NettyScope startNew( + NettyConnectionInstrumenter instrumenter, + Context parentContext, + NettyConnectionRequest request) { + Context context = instrumenter.start(parentContext, request); + return new NettyScope(context, request, context.makeCurrent()); + } + + public static void end( + NettyScope nettyScope, + NettyConnectionInstrumenter instrumenter, + ChannelPromise channelPromise, + Throwable throwable) { + + if (nettyScope == null) { + return; + } + + nettyScope.scope.close(); + + if (throwable != null) { + instrumenter.end(nettyScope.context, nettyScope.request, null, throwable); + } else { + channelPromise.addListener( + new ConnectionCompleteListener(instrumenter, nettyScope.context, nettyScope.request)); + } + } +} diff --git a/instrumentation/netty/netty-4-common/library/src/main/java/io/opentelemetry/instrumentation/netty/v4/common/internal/client/NettyClientInstrumenterFactory.java b/instrumentation/netty/netty-4-common/library/src/main/java/io/opentelemetry/instrumentation/netty/v4/common/internal/client/NettyClientInstrumenterFactory.java index 7ba58222abc9..333d43e21f41 100644 --- a/instrumentation/netty/netty-4-common/library/src/main/java/io/opentelemetry/instrumentation/netty/v4/common/internal/client/NettyClientInstrumenterFactory.java +++ b/instrumentation/netty/netty-4-common/library/src/main/java/io/opentelemetry/instrumentation/netty/v4/common/internal/client/NettyClientInstrumenterFactory.java @@ -8,20 +8,28 @@ import io.netty.channel.Channel; import io.netty.handler.codec.http.HttpResponse; import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.instrumentation.api.incubator.semconv.http.HttpClientExperimentalMetrics; +import io.opentelemetry.instrumentation.api.incubator.semconv.http.HttpClientPeerServiceAttributesExtractor; +import io.opentelemetry.instrumentation.api.incubator.semconv.http.HttpExperimentalAttributesExtractor; +import io.opentelemetry.instrumentation.api.incubator.semconv.net.PeerServiceResolver; 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 io.opentelemetry.instrumentation.api.instrumenter.http.HttpClientAttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpClientMetrics; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpSpanNameExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpSpanStatusExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.net.NetClientAttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.net.PeerServiceAttributesExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor; +import io.opentelemetry.instrumentation.api.semconv.http.HttpClientAttributesExtractor; +import io.opentelemetry.instrumentation.api.semconv.http.HttpClientAttributesExtractorBuilder; +import io.opentelemetry.instrumentation.api.semconv.http.HttpClientMetrics; +import io.opentelemetry.instrumentation.api.semconv.http.HttpSpanNameExtractor; +import io.opentelemetry.instrumentation.api.semconv.http.HttpSpanNameExtractorBuilder; +import io.opentelemetry.instrumentation.api.semconv.http.HttpSpanStatusExtractor; +import io.opentelemetry.instrumentation.api.semconv.network.NetworkAttributesExtractor; +import io.opentelemetry.instrumentation.api.semconv.network.ServerAttributesExtractor; import io.opentelemetry.instrumentation.netty.common.internal.NettyConnectionRequest; import io.opentelemetry.instrumentation.netty.v4.common.HttpRequestAndChannel; import java.util.List; -import java.util.Map; +import java.util.function.Consumer; +import java.util.function.Function; /** * This class is internal and is hence not for public use. Its APIs are unstable and can change at @@ -31,101 +39,122 @@ public final class NettyClientInstrumenterFactory { private final OpenTelemetry openTelemetry; private final String instrumentationName; - private final boolean connectionTelemetryEnabled; - private final boolean sslTelemetryEnabled; - private final Map peerServiceMapping; + private final NettyConnectionInstrumentationFlag connectionTelemetryState; + private final NettyConnectionInstrumentationFlag sslTelemetryState; + private final PeerServiceResolver peerServiceResolver; + private final boolean emitExperimentalHttpClientMetrics; public NettyClientInstrumenterFactory( OpenTelemetry openTelemetry, String instrumentationName, - boolean connectionTelemetryEnabled, - boolean sslTelemetryEnabled, - Map peerServiceMapping) { + NettyConnectionInstrumentationFlag connectionTelemetryState, + NettyConnectionInstrumentationFlag sslTelemetryState, + PeerServiceResolver peerServiceResolver, + boolean emitExperimentalHttpClientMetrics) { this.openTelemetry = openTelemetry; this.instrumentationName = instrumentationName; - this.connectionTelemetryEnabled = connectionTelemetryEnabled; - this.sslTelemetryEnabled = sslTelemetryEnabled; - this.peerServiceMapping = peerServiceMapping; + this.connectionTelemetryState = connectionTelemetryState; + this.sslTelemetryState = sslTelemetryState; + this.peerServiceResolver = peerServiceResolver; + this.emitExperimentalHttpClientMetrics = emitExperimentalHttpClientMetrics; } public Instrumenter createHttpInstrumenter( - List capturedRequestHeaders, - List capturedResponseHeaders, + Consumer> + extractorConfigurer, + Consumer> spanNameExtractorConfigurer, + Function< + SpanNameExtractor, + ? extends SpanNameExtractor> + spanNameExtractorTransformer, List> additionalHttpAttributeExtractors) { NettyHttpClientAttributesGetter httpAttributesGetter = new NettyHttpClientAttributesGetter(); - NettyNetClientAttributesGetter netAttributesGetter = new NettyNetClientAttributesGetter(); - - return Instrumenter.builder( - openTelemetry, instrumentationName, HttpSpanNameExtractor.create(httpAttributesGetter)) - .setSpanStatusExtractor(HttpSpanStatusExtractor.create(httpAttributesGetter)) - .addAttributesExtractor( - HttpClientAttributesExtractor.builder(httpAttributesGetter, netAttributesGetter) - .setCapturedRequestHeaders(capturedRequestHeaders) - .setCapturedResponseHeaders(capturedResponseHeaders) - .build()) - .addAttributesExtractor( - PeerServiceAttributesExtractor.create(netAttributesGetter, peerServiceMapping)) - .addAttributesExtractors(additionalHttpAttributeExtractors) - .addOperationMetrics(HttpClientMetrics.get()) - .buildClientInstrumenter(HttpRequestHeadersSetter.INSTANCE); + + HttpClientAttributesExtractorBuilder extractorBuilder = + HttpClientAttributesExtractor.builder(httpAttributesGetter); + extractorConfigurer.accept(extractorBuilder); + + HttpSpanNameExtractorBuilder httpSpanNameExtractorBuilder = + HttpSpanNameExtractor.builder(httpAttributesGetter); + spanNameExtractorConfigurer.accept(httpSpanNameExtractorBuilder); + SpanNameExtractor spanNameExtractor = + spanNameExtractorTransformer.apply(httpSpanNameExtractorBuilder.build()); + + InstrumenterBuilder builder = + Instrumenter.builder( + openTelemetry, instrumentationName, spanNameExtractor) + .setSpanStatusExtractor(HttpSpanStatusExtractor.create(httpAttributesGetter)) + .addAttributesExtractor(extractorBuilder.build()) + .addAttributesExtractor( + HttpClientPeerServiceAttributesExtractor.create( + httpAttributesGetter, peerServiceResolver)) + .addAttributesExtractors(additionalHttpAttributeExtractors) + .addOperationMetrics(HttpClientMetrics.get()); + if (emitExperimentalHttpClientMetrics) { + builder + .addAttributesExtractor(HttpExperimentalAttributesExtractor.create(httpAttributesGetter)) + .addOperationMetrics(HttpClientExperimentalMetrics.get()); + } + return builder.buildClientInstrumenter(HttpRequestHeadersSetter.INSTANCE); } public NettyConnectionInstrumenter createConnectionInstrumenter() { - NettyConnectNetAttributesGetter netAttributesGetter = new NettyConnectNetAttributesGetter(); + if (connectionTelemetryState == NettyConnectionInstrumentationFlag.DISABLED) { + return NoopConnectionInstrumenter.INSTANCE; + } - InstrumenterBuilder instrumenterBuilder = + boolean connectionTelemetryFullyEnabled = + connectionTelemetryState == NettyConnectionInstrumentationFlag.ENABLED; + NettyConnectHttpAttributesGetter getter = NettyConnectHttpAttributesGetter.INSTANCE; + + InstrumenterBuilder builder = Instrumenter.builder( openTelemetry, instrumentationName, NettyConnectionRequest::spanName) .addAttributesExtractor( - PeerServiceAttributesExtractor.create(netAttributesGetter, peerServiceMapping)); - - if (connectionTelemetryEnabled) { - // when the connection telemetry is enabled, we do not want these CONNECT spans to be - // suppressed by higher-level HTTP spans - this would happen in the reactor-netty - // instrumentation - // the solution for this is to deliberately avoid using the HTTP extractor and use the plain - // net attributes extractor instead - instrumenterBuilder.addAttributesExtractor( - NetClientAttributesExtractor.create(netAttributesGetter)); + HttpClientPeerServiceAttributesExtractor.create(getter, peerServiceResolver)); + + if (connectionTelemetryFullyEnabled) { + // when the connection telemetry is fully enabled, CONNECT spans are created for every + // request; and semantically they're not HTTP spans, they must not use the HTTP client + // extractor + builder.addAttributesExtractor(NetworkAttributesExtractor.create(getter)); + builder.addAttributesExtractor(ServerAttributesExtractor.create(getter)); } else { - // when the connection telemetry is not enabled, netty creates CONNECT spans whenever a - // connection error occurs - because there is no HTTP span in that scenario, if raw netty - // connection occurs before an HTTP message is even formed - // we don't want that span when a higher-level HTTP library (like reactor-netty or async http - // client) is used, the connection phase is a part of the HTTP span for these - // for that to happen, the CONNECT span will "pretend" to be a full HTTP span when connection - // telemetry is off - instrumenterBuilder.addAttributesExtractor( - HttpClientAttributesExtractor.create( - NettyConnectHttpAttributesGetter.INSTANCE, netAttributesGetter)); + // in case the connection telemetry is emitted only on errors, the CONNECT span is a stand-in + // for the HTTP client span + builder.addAttributesExtractor(HttpClientAttributesExtractor.create(getter)); } Instrumenter instrumenter = - instrumenterBuilder.buildInstrumenter( - connectionTelemetryEnabled + builder.buildInstrumenter( + connectionTelemetryFullyEnabled ? SpanKindExtractor.alwaysInternal() : SpanKindExtractor.alwaysClient()); - return connectionTelemetryEnabled + return connectionTelemetryFullyEnabled ? new NettyConnectionInstrumenterImpl(instrumenter) : new NettyErrorOnlyConnectionInstrumenter(instrumenter); } public NettySslInstrumenter createSslInstrumenter() { + if (sslTelemetryState == NettyConnectionInstrumentationFlag.DISABLED) { + return NoopSslInstrumenter.INSTANCE; + } + + boolean sslTelemetryFullyEnabled = + sslTelemetryState == NettyConnectionInstrumentationFlag.ENABLED; NettySslNetAttributesGetter netAttributesGetter = new NettySslNetAttributesGetter(); Instrumenter instrumenter = Instrumenter.builder( openTelemetry, instrumentationName, NettySslRequest::spanName) - .addAttributesExtractor(NetClientAttributesExtractor.create(netAttributesGetter)) - .addAttributesExtractor( - PeerServiceAttributesExtractor.create(netAttributesGetter, peerServiceMapping)) + .addAttributesExtractor(NetworkAttributesExtractor.create(netAttributesGetter)) .buildInstrumenter( - sslTelemetryEnabled + sslTelemetryFullyEnabled ? SpanKindExtractor.alwaysInternal() : SpanKindExtractor.alwaysClient()); - return sslTelemetryEnabled + return sslTelemetryFullyEnabled ? new NettySslInstrumenterImpl(instrumenter) : new NettySslErrorOnlyInstrumenter(instrumenter); } diff --git a/instrumentation/netty/netty-4-common/library/src/main/java/io/opentelemetry/instrumentation/netty/v4/common/internal/client/NettyConnectHttpAttributesGetter.java b/instrumentation/netty/netty-4-common/library/src/main/java/io/opentelemetry/instrumentation/netty/v4/common/internal/client/NettyConnectHttpAttributesGetter.java index 12fa65d30a4c..02eb686ed752 100644 --- a/instrumentation/netty/netty-4-common/library/src/main/java/io/opentelemetry/instrumentation/netty/v4/common/internal/client/NettyConnectHttpAttributesGetter.java +++ b/instrumentation/netty/netty-4-common/library/src/main/java/io/opentelemetry/instrumentation/netty/v4/common/internal/client/NettyConnectHttpAttributesGetter.java @@ -6,8 +6,11 @@ package io.opentelemetry.instrumentation.netty.v4.common.internal.client; import io.netty.channel.Channel; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpClientAttributesGetter; +import io.opentelemetry.instrumentation.api.semconv.http.HttpClientAttributesGetter; import io.opentelemetry.instrumentation.netty.common.internal.NettyConnectionRequest; +import io.opentelemetry.instrumentation.netty.v4.common.internal.ChannelUtil; +import java.net.InetSocketAddress; +import java.net.SocketAddress; import java.util.Collections; import java.util.List; import javax.annotation.Nullable; @@ -46,4 +49,43 @@ public List getHttpResponseHeader( NettyConnectionRequest nettyConnectionRequest, Channel channel, String name) { return Collections.emptyList(); } + + @Override + public String getNetworkTransport(NettyConnectionRequest request, @Nullable Channel channel) { + return ChannelUtil.getNetworkTransport(channel); + } + + @Nullable + @Override + public String getServerAddress(NettyConnectionRequest request) { + SocketAddress requestedAddress = request.remoteAddressOnStart(); + if (requestedAddress instanceof InetSocketAddress) { + return ((InetSocketAddress) requestedAddress).getHostString(); + } + return null; + } + + @Nullable + @Override + public Integer getServerPort(NettyConnectionRequest request) { + SocketAddress requestedAddress = request.remoteAddressOnStart(); + if (requestedAddress instanceof InetSocketAddress) { + return ((InetSocketAddress) requestedAddress).getPort(); + } + return null; + } + + @Nullable + @Override + public InetSocketAddress getNetworkPeerInetSocketAddress( + NettyConnectionRequest request, @Nullable Channel channel) { + if (channel == null) { + return null; + } + SocketAddress remoteAddress = channel.remoteAddress(); + if (remoteAddress instanceof InetSocketAddress) { + return (InetSocketAddress) remoteAddress; + } + return null; + } } diff --git a/instrumentation/netty/netty-4-common/library/src/main/java/io/opentelemetry/instrumentation/netty/v4/common/internal/client/NettyConnectNetAttributesGetter.java b/instrumentation/netty/netty-4-common/library/src/main/java/io/opentelemetry/instrumentation/netty/v4/common/internal/client/NettyConnectNetAttributesGetter.java deleted file mode 100644 index 06bff506088c..000000000000 --- a/instrumentation/netty/netty-4-common/library/src/main/java/io/opentelemetry/instrumentation/netty/v4/common/internal/client/NettyConnectNetAttributesGetter.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.netty.v4.common.internal.client; - -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.socket.DatagramChannel; -import io.opentelemetry.instrumentation.api.instrumenter.net.NetClientAttributesGetter; -import io.opentelemetry.instrumentation.netty.common.internal.NettyConnectionRequest; -import io.opentelemetry.instrumentation.netty.v4.common.internal.ChannelUtil; -import java.net.InetSocketAddress; -import java.net.SocketAddress; -import javax.annotation.Nullable; - -final class NettyConnectNetAttributesGetter - implements NetClientAttributesGetter { - - @Override - public String getTransport(NettyConnectionRequest request, @Nullable Channel channel) { - return channel instanceof DatagramChannel ? IP_UDP : IP_TCP; - } - - @Override - public String getNetworkTransport(NettyConnectionRequest request, @Nullable Channel channel) { - return ChannelUtil.getNetworkTransport(channel); - } - - @Nullable - @Override - public String getServerAddress(NettyConnectionRequest request) { - SocketAddress requestedAddress = request.remoteAddressOnStart(); - if (requestedAddress instanceof InetSocketAddress) { - return ((InetSocketAddress) requestedAddress).getHostString(); - } - return null; - } - - @Nullable - @Override - public Integer getServerPort(NettyConnectionRequest request) { - SocketAddress requestedAddress = request.remoteAddressOnStart(); - if (requestedAddress instanceof InetSocketAddress) { - return ((InetSocketAddress) requestedAddress).getPort(); - } - return null; - } - - @Nullable - @Override - public InetSocketAddress getServerInetSocketAddress( - NettyConnectionRequest request, @Nullable Channel channel) { - if (channel == null) { - return null; - } - SocketAddress remoteAddress = channel.remoteAddress(); - if (remoteAddress instanceof InetSocketAddress) { - return (InetSocketAddress) remoteAddress; - } - return null; - } -} diff --git a/instrumentation/netty/netty-4-common/library/src/main/java/io/opentelemetry/instrumentation/netty/v4/common/internal/client/NettyConnectionInstrumentationFlag.java b/instrumentation/netty/netty-4-common/library/src/main/java/io/opentelemetry/instrumentation/netty/v4/common/internal/client/NettyConnectionInstrumentationFlag.java new file mode 100644 index 000000000000..58400aec230d --- /dev/null +++ b/instrumentation/netty/netty-4-common/library/src/main/java/io/opentelemetry/instrumentation/netty/v4/common/internal/client/NettyConnectionInstrumentationFlag.java @@ -0,0 +1,20 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.netty.v4.common.internal.client; + +/** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time. + */ +public enum NettyConnectionInstrumentationFlag { + ENABLED, + ERROR_ONLY, + DISABLED; + + public static NettyConnectionInstrumentationFlag enabledOrErrorOnly(boolean b) { + return b ? ENABLED : ERROR_ONLY; + } +} diff --git a/instrumentation/netty/netty-4-common/library/src/main/java/io/opentelemetry/instrumentation/netty/v4/common/internal/client/NettyHttpClientAttributesGetter.java b/instrumentation/netty/netty-4-common/library/src/main/java/io/opentelemetry/instrumentation/netty/v4/common/internal/client/NettyHttpClientAttributesGetter.java index 1770b6e97acc..966a6d5aba0c 100644 --- a/instrumentation/netty/netty-4-common/library/src/main/java/io/opentelemetry/instrumentation/netty/v4/common/internal/client/NettyHttpClientAttributesGetter.java +++ b/instrumentation/netty/netty-4-common/library/src/main/java/io/opentelemetry/instrumentation/netty/v4/common/internal/client/NettyHttpClientAttributesGetter.java @@ -8,8 +8,12 @@ import static io.opentelemetry.instrumentation.netty.v4.common.internal.HttpSchemeUtil.getScheme; import io.netty.handler.codec.http.HttpResponse; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpClientAttributesGetter; +import io.netty.handler.codec.http.HttpVersion; +import io.opentelemetry.instrumentation.api.semconv.http.HttpClientAttributesGetter; import io.opentelemetry.instrumentation.netty.v4.common.HttpRequestAndChannel; +import io.opentelemetry.instrumentation.netty.v4.common.internal.ChannelUtil; +import java.net.InetSocketAddress; +import java.net.SocketAddress; import java.net.URI; import java.net.URISyntaxException; import java.util.List; @@ -60,4 +64,49 @@ public List getHttpResponseHeader( HttpRequestAndChannel requestAndChannel, HttpResponse response, String name) { return response.headers().getAll(name); } + + @Override + public String getNetworkTransport( + HttpRequestAndChannel requestAndChannel, @Nullable HttpResponse response) { + return ChannelUtil.getNetworkTransport(requestAndChannel.channel()); + } + + @Override + public String getNetworkProtocolName( + HttpRequestAndChannel requestAndChannel, @Nullable HttpResponse response) { + return requestAndChannel.request().getProtocolVersion().protocolName(); + } + + @Override + public String getNetworkProtocolVersion( + HttpRequestAndChannel requestAndChannel, @Nullable HttpResponse response) { + HttpVersion version = requestAndChannel.request().getProtocolVersion(); + if (version.minorVersion() == 0) { + return Integer.toString(version.majorVersion()); + } + return version.majorVersion() + "." + version.minorVersion(); + } + + @Nullable + @Override + public String getServerAddress(HttpRequestAndChannel requestAndChannel) { + return null; + } + + @Nullable + @Override + public Integer getServerPort(HttpRequestAndChannel requestAndChannel) { + return null; + } + + @Override + @Nullable + public InetSocketAddress getNetworkPeerInetSocketAddress( + HttpRequestAndChannel requestAndChannel, @Nullable HttpResponse response) { + SocketAddress address = requestAndChannel.remoteAddress(); + if (address instanceof InetSocketAddress) { + return (InetSocketAddress) address; + } + return null; + } } diff --git a/instrumentation/netty/netty-4-common/library/src/main/java/io/opentelemetry/instrumentation/netty/v4/common/internal/client/NettyNetClientAttributesGetter.java b/instrumentation/netty/netty-4-common/library/src/main/java/io/opentelemetry/instrumentation/netty/v4/common/internal/client/NettyNetClientAttributesGetter.java deleted file mode 100644 index 9f4fda881f91..000000000000 --- a/instrumentation/netty/netty-4-common/library/src/main/java/io/opentelemetry/instrumentation/netty/v4/common/internal/client/NettyNetClientAttributesGetter.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.netty.v4.common.internal.client; - -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.socket.DatagramChannel; -import io.netty.handler.codec.http.HttpResponse; -import io.netty.handler.codec.http.HttpVersion; -import io.opentelemetry.instrumentation.api.instrumenter.net.NetClientAttributesGetter; -import io.opentelemetry.instrumentation.netty.v4.common.HttpRequestAndChannel; -import io.opentelemetry.instrumentation.netty.v4.common.internal.ChannelUtil; -import java.net.InetSocketAddress; -import java.net.SocketAddress; -import javax.annotation.Nullable; - -final class NettyNetClientAttributesGetter - implements NetClientAttributesGetter { - - @Override - public String getTransport( - HttpRequestAndChannel requestAndChannel, @Nullable HttpResponse response) { - return requestAndChannel.channel() instanceof DatagramChannel ? IP_UDP : IP_TCP; - } - - @Override - public String getNetworkTransport( - HttpRequestAndChannel requestAndChannel, @Nullable HttpResponse response) { - return ChannelUtil.getNetworkTransport(requestAndChannel.channel()); - } - - @Override - public String getNetworkProtocolName( - HttpRequestAndChannel requestAndChannel, @Nullable HttpResponse response) { - return requestAndChannel.request().getProtocolVersion().protocolName(); - } - - @Override - public String getNetworkProtocolVersion( - HttpRequestAndChannel requestAndChannel, @Nullable HttpResponse response) { - HttpVersion version = requestAndChannel.request().getProtocolVersion(); - return version.majorVersion() + "." + version.minorVersion(); - } - - @Nullable - @Override - public String getServerAddress(HttpRequestAndChannel requestAndChannel) { - return null; - } - - @Nullable - @Override - public Integer getServerPort(HttpRequestAndChannel requestAndChannel) { - return null; - } - - @Override - @Nullable - public InetSocketAddress getServerInetSocketAddress( - HttpRequestAndChannel requestAndChannel, @Nullable HttpResponse response) { - SocketAddress address = requestAndChannel.remoteAddress(); - if (address instanceof InetSocketAddress) { - return (InetSocketAddress) address; - } - return null; - } -} diff --git a/instrumentation/netty/netty-4-common/library/src/main/java/io/opentelemetry/instrumentation/netty/v4/common/internal/client/NettySslInstrumentationHandler.java b/instrumentation/netty/netty-4-common/library/src/main/java/io/opentelemetry/instrumentation/netty/v4/common/internal/client/NettySslInstrumentationHandler.java index 24731fc427fe..774181808fc4 100644 --- a/instrumentation/netty/netty-4-common/library/src/main/java/io/opentelemetry/instrumentation/netty/v4/common/internal/client/NettySslInstrumentationHandler.java +++ b/instrumentation/netty/netty-4-common/library/src/main/java/io/opentelemetry/instrumentation/netty/v4/common/internal/client/NettySslInstrumentationHandler.java @@ -94,7 +94,8 @@ public void connect( promise.addListener( future -> { // there won't be any SSL handshake if the channel fails to connect - if (!future.isSuccess()) { + // give up when channelRegistered wasn't called and parentContext is null + if (!future.isSuccess() || parentContext == null) { return; } request = NettySslRequest.create(ctx.channel()); diff --git a/instrumentation/netty/netty-4-common/library/src/main/java/io/opentelemetry/instrumentation/netty/v4/common/internal/client/NettySslNetAttributesGetter.java b/instrumentation/netty/netty-4-common/library/src/main/java/io/opentelemetry/instrumentation/netty/v4/common/internal/client/NettySslNetAttributesGetter.java index 97fdca556bde..5384636c0b75 100644 --- a/instrumentation/netty/netty-4-common/library/src/main/java/io/opentelemetry/instrumentation/netty/v4/common/internal/client/NettySslNetAttributesGetter.java +++ b/instrumentation/netty/netty-4-common/library/src/main/java/io/opentelemetry/instrumentation/netty/v4/common/internal/client/NettySslNetAttributesGetter.java @@ -5,22 +5,12 @@ package io.opentelemetry.instrumentation.netty.v4.common.internal.client; -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.socket.DatagramChannel; -import io.opentelemetry.instrumentation.api.instrumenter.net.NetClientAttributesGetter; +import io.opentelemetry.instrumentation.api.semconv.network.NetworkAttributesGetter; import io.opentelemetry.instrumentation.netty.v4.common.internal.ChannelUtil; import java.net.InetSocketAddress; import javax.annotation.Nullable; -final class NettySslNetAttributesGetter - implements NetClientAttributesGetter { - - @Override - public String getTransport(NettySslRequest request, @Nullable Void unused) { - return request.channel() instanceof DatagramChannel ? IP_UDP : IP_TCP; - } +final class NettySslNetAttributesGetter implements NetworkAttributesGetter { @Override public String getNetworkTransport(NettySslRequest request, @Nullable Void unused) { @@ -29,19 +19,7 @@ public String getNetworkTransport(NettySslRequest request, @Nullable Void unused @Nullable @Override - public String getServerAddress(NettySslRequest nettySslRequest) { - return null; - } - - @Nullable - @Override - public Integer getServerPort(NettySslRequest nettySslRequest) { - return null; - } - - @Nullable - @Override - public InetSocketAddress getServerInetSocketAddress( + public InetSocketAddress getNetworkPeerInetSocketAddress( NettySslRequest request, @Nullable Void unused) { if (request.remoteAddress() instanceof InetSocketAddress) { return (InetSocketAddress) request.remoteAddress(); diff --git a/instrumentation/netty/netty-4-common/library/src/main/java/io/opentelemetry/instrumentation/netty/v4/common/internal/client/NoopConnectionInstrumenter.java b/instrumentation/netty/netty-4-common/library/src/main/java/io/opentelemetry/instrumentation/netty/v4/common/internal/client/NoopConnectionInstrumenter.java new file mode 100644 index 000000000000..bea14d35a73d --- /dev/null +++ b/instrumentation/netty/netty-4-common/library/src/main/java/io/opentelemetry/instrumentation/netty/v4/common/internal/client/NoopConnectionInstrumenter.java @@ -0,0 +1,34 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.netty.v4.common.internal.client; + +import com.google.errorprone.annotations.CanIgnoreReturnValue; +import io.netty.channel.Channel; +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.netty.common.internal.NettyConnectionRequest; +import javax.annotation.Nullable; + +enum NoopConnectionInstrumenter implements NettyConnectionInstrumenter { + INSTANCE; + + @Override + public boolean shouldStart(Context parentContext, NettyConnectionRequest request) { + return false; + } + + @CanIgnoreReturnValue + @Override + public Context start(Context parentContext, NettyConnectionRequest request) { + return parentContext; + } + + @Override + public void end( + Context context, + NettyConnectionRequest request, + Channel channel, + @Nullable Throwable error) {} +} diff --git a/instrumentation/netty/netty-4-common/library/src/main/java/io/opentelemetry/instrumentation/netty/v4/common/internal/client/NoopSslInstrumenter.java b/instrumentation/netty/netty-4-common/library/src/main/java/io/opentelemetry/instrumentation/netty/v4/common/internal/client/NoopSslInstrumenter.java new file mode 100644 index 000000000000..cc5bbcdd0669 --- /dev/null +++ b/instrumentation/netty/netty-4-common/library/src/main/java/io/opentelemetry/instrumentation/netty/v4/common/internal/client/NoopSslInstrumenter.java @@ -0,0 +1,28 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.netty.v4.common.internal.client; + +import com.google.errorprone.annotations.CanIgnoreReturnValue; +import io.opentelemetry.context.Context; +import javax.annotation.Nullable; + +enum NoopSslInstrumenter implements NettySslInstrumenter { + INSTANCE; + + @Override + public boolean shouldStart(Context parentContext, NettySslRequest request) { + return false; + } + + @CanIgnoreReturnValue + @Override + public Context start(Context parentContext, NettySslRequest request) { + return parentContext; + } + + @Override + public void end(Context context, NettySslRequest request, @Nullable Throwable error) {} +} diff --git a/instrumentation/netty/netty-4-common/library/src/main/java/io/opentelemetry/instrumentation/netty/v4/common/internal/server/NettyHttpServerAttributesGetter.java b/instrumentation/netty/netty-4-common/library/src/main/java/io/opentelemetry/instrumentation/netty/v4/common/internal/server/NettyHttpServerAttributesGetter.java index 27ca3dda734c..953ae3fbdb09 100644 --- a/instrumentation/netty/netty-4-common/library/src/main/java/io/opentelemetry/instrumentation/netty/v4/common/internal/server/NettyHttpServerAttributesGetter.java +++ b/instrumentation/netty/netty-4-common/library/src/main/java/io/opentelemetry/instrumentation/netty/v4/common/internal/server/NettyHttpServerAttributesGetter.java @@ -6,9 +6,13 @@ package io.opentelemetry.instrumentation.netty.v4.common.internal.server; import io.netty.handler.codec.http.HttpResponse; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpServerAttributesGetter; +import io.netty.handler.codec.http.HttpVersion; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerAttributesGetter; import io.opentelemetry.instrumentation.netty.v4.common.HttpRequestAndChannel; +import io.opentelemetry.instrumentation.netty.v4.common.internal.ChannelUtil; import io.opentelemetry.instrumentation.netty.v4.common.internal.HttpSchemeUtil; +import java.net.InetSocketAddress; +import java.net.SocketAddress; import java.util.List; import javax.annotation.Nullable; @@ -55,4 +59,48 @@ public String getUrlQuery(HttpRequestAndChannel requestAndChannel) { int separatorPos = fullPath.indexOf('?'); return separatorPos == -1 ? null : fullPath.substring(separatorPos + 1); } + + @Override + public String getNetworkTransport( + HttpRequestAndChannel requestAndChannel, HttpResponse response) { + return ChannelUtil.getNetworkTransport(requestAndChannel.channel()); + } + + @Override + public String getNetworkProtocolName( + HttpRequestAndChannel requestAndChannel, @Nullable HttpResponse response) { + return requestAndChannel.request().getProtocolVersion().protocolName(); + } + + @Override + public String getNetworkProtocolVersion( + HttpRequestAndChannel requestAndChannel, @Nullable HttpResponse response) { + HttpVersion version = requestAndChannel.request().getProtocolVersion(); + if (version.minorVersion() == 0) { + return Integer.toString(version.majorVersion()); + } + return version.majorVersion() + "." + version.minorVersion(); + } + + @Override + @Nullable + public InetSocketAddress getNetworkPeerInetSocketAddress( + HttpRequestAndChannel requestAndChannel, @Nullable HttpResponse response) { + SocketAddress address = requestAndChannel.remoteAddress(); + if (address instanceof InetSocketAddress) { + return (InetSocketAddress) address; + } + return null; + } + + @Nullable + @Override + public InetSocketAddress getNetworkLocalInetSocketAddress( + HttpRequestAndChannel requestAndChannel, @Nullable HttpResponse response) { + SocketAddress address = requestAndChannel.channel().localAddress(); + if (address instanceof InetSocketAddress) { + return (InetSocketAddress) address; + } + return null; + } } diff --git a/instrumentation/netty/netty-4-common/library/src/main/java/io/opentelemetry/instrumentation/netty/v4/common/internal/server/NettyNetServerAttributesGetter.java b/instrumentation/netty/netty-4-common/library/src/main/java/io/opentelemetry/instrumentation/netty/v4/common/internal/server/NettyNetServerAttributesGetter.java deleted file mode 100644 index b2a837c141ba..000000000000 --- a/instrumentation/netty/netty-4-common/library/src/main/java/io/opentelemetry/instrumentation/netty/v4/common/internal/server/NettyNetServerAttributesGetter.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.netty.v4.common.internal.server; - -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.socket.DatagramChannel; -import io.netty.handler.codec.http.HttpResponse; -import io.netty.handler.codec.http.HttpVersion; -import io.opentelemetry.instrumentation.api.instrumenter.net.NetServerAttributesGetter; -import io.opentelemetry.instrumentation.netty.v4.common.HttpRequestAndChannel; -import io.opentelemetry.instrumentation.netty.v4.common.internal.ChannelUtil; -import java.net.InetSocketAddress; -import java.net.SocketAddress; -import javax.annotation.Nullable; - -final class NettyNetServerAttributesGetter - implements NetServerAttributesGetter { - - @Override - public String getTransport(HttpRequestAndChannel requestAndChannel) { - return requestAndChannel.channel() instanceof DatagramChannel ? IP_UDP : IP_TCP; - } - - @Override - public String getNetworkTransport( - HttpRequestAndChannel requestAndChannel, HttpResponse response) { - return ChannelUtil.getNetworkTransport(requestAndChannel.channel()); - } - - @Override - public String getNetworkProtocolName( - HttpRequestAndChannel requestAndChannel, @Nullable HttpResponse response) { - return requestAndChannel.request().getProtocolVersion().protocolName(); - } - - @Override - public String getNetworkProtocolVersion( - HttpRequestAndChannel requestAndChannel, @Nullable HttpResponse response) { - HttpVersion version = requestAndChannel.request().getProtocolVersion(); - return version.majorVersion() + "." + version.minorVersion(); - } - - @Nullable - @Override - public String getServerAddress(HttpRequestAndChannel requestAndChannel) { - return null; - } - - @Nullable - @Override - public Integer getServerPort(HttpRequestAndChannel requestAndChannel) { - return null; - } - - @Override - @Nullable - public InetSocketAddress getClientInetSocketAddress( - HttpRequestAndChannel requestAndChannel, @Nullable HttpResponse response) { - SocketAddress address = requestAndChannel.remoteAddress(); - if (address instanceof InetSocketAddress) { - return (InetSocketAddress) address; - } - return null; - } - - @Nullable - @Override - public InetSocketAddress getServerInetSocketAddress( - HttpRequestAndChannel requestAndChannel, @Nullable HttpResponse response) { - SocketAddress address = requestAndChannel.channel().localAddress(); - if (address instanceof InetSocketAddress) { - return (InetSocketAddress) address; - } - return null; - } -} diff --git a/instrumentation/netty/netty-4-common/library/src/main/java/io/opentelemetry/instrumentation/netty/v4/common/internal/server/NettyServerInstrumenterFactory.java b/instrumentation/netty/netty-4-common/library/src/main/java/io/opentelemetry/instrumentation/netty/v4/common/internal/server/NettyServerInstrumenterFactory.java index 2dc9c3446837..a89901359353 100644 --- a/instrumentation/netty/netty-4-common/library/src/main/java/io/opentelemetry/instrumentation/netty/v4/common/internal/server/NettyServerInstrumenterFactory.java +++ b/instrumentation/netty/netty-4-common/library/src/main/java/io/opentelemetry/instrumentation/netty/v4/common/internal/server/NettyServerInstrumenterFactory.java @@ -7,15 +7,21 @@ import io.netty.handler.codec.http.HttpResponse; import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.instrumentation.api.incubator.semconv.http.HttpExperimentalAttributesExtractor; +import io.opentelemetry.instrumentation.api.incubator.semconv.http.HttpServerExperimentalMetrics; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteHolder; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpServerAttributesExtractor; -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 io.opentelemetry.instrumentation.api.instrumenter.InstrumenterBuilder; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerAttributesExtractor; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerAttributesExtractorBuilder; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerMetrics; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRoute; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRouteBuilder; +import io.opentelemetry.instrumentation.api.semconv.http.HttpSpanNameExtractor; +import io.opentelemetry.instrumentation.api.semconv.http.HttpSpanNameExtractorBuilder; +import io.opentelemetry.instrumentation.api.semconv.http.HttpSpanStatusExtractor; import io.opentelemetry.instrumentation.netty.common.internal.NettyErrorHolder; import io.opentelemetry.instrumentation.netty.v4.common.HttpRequestAndChannel; -import java.util.List; +import java.util.function.Consumer; /** * This class is internal and is hence not for public use. Its APIs are unstable and can change at @@ -26,23 +32,41 @@ public final class NettyServerInstrumenterFactory { public static Instrumenter create( OpenTelemetry openTelemetry, String instrumentationName, - List capturedRequestHeaders, - List capturedResponseHeaders) { + Consumer> + extractorConfigurer, + Consumer> spanNameExtractorConfigurer, + Consumer> httpServerRouteConfigurer, + boolean emitExperimentalHttpServerMetrics) { NettyHttpServerAttributesGetter httpAttributesGetter = new NettyHttpServerAttributesGetter(); - return Instrumenter.builder( - openTelemetry, instrumentationName, HttpSpanNameExtractor.create(httpAttributesGetter)) - .setSpanStatusExtractor(HttpSpanStatusExtractor.create(httpAttributesGetter)) - .addAttributesExtractor( - HttpServerAttributesExtractor.builder( - httpAttributesGetter, new NettyNetServerAttributesGetter()) - .setCapturedRequestHeaders(capturedRequestHeaders) - .setCapturedResponseHeaders(capturedResponseHeaders) - .build()) - .addOperationMetrics(HttpServerMetrics.get()) + HttpServerAttributesExtractorBuilder extractorBuilder = + HttpServerAttributesExtractor.builder(httpAttributesGetter); + extractorConfigurer.accept(extractorBuilder); + + HttpSpanNameExtractorBuilder httpSpanNameExtractorBuilder = + HttpSpanNameExtractor.builder(httpAttributesGetter); + spanNameExtractorConfigurer.accept(httpSpanNameExtractorBuilder); + + InstrumenterBuilder builder = + Instrumenter.builder( + openTelemetry, instrumentationName, httpSpanNameExtractorBuilder.build()) + .setSpanStatusExtractor(HttpSpanStatusExtractor.create(httpAttributesGetter)) + .addAttributesExtractor(extractorBuilder.build()) + .addOperationMetrics(HttpServerMetrics.get()); + if (emitExperimentalHttpServerMetrics) { + builder + .addAttributesExtractor(HttpExperimentalAttributesExtractor.create(httpAttributesGetter)) + .addOperationMetrics(HttpServerExperimentalMetrics.get()); + } + + HttpServerRouteBuilder httpServerRouteBuilder = + HttpServerRoute.builder(httpAttributesGetter); + httpServerRouteConfigurer.accept(httpServerRouteBuilder); + + return builder .addContextCustomizer((context, request, attributes) -> NettyErrorHolder.init(context)) - .addContextCustomizer(HttpRouteHolder.create(httpAttributesGetter)) + .addContextCustomizer(httpServerRouteBuilder.build()) .buildServerInstrumenter(HttpRequestHeadersGetter.INSTANCE); } diff --git a/instrumentation/netty/netty-4.0/javaagent/build.gradle.kts b/instrumentation/netty/netty-4.0/javaagent/build.gradle.kts index 09451d3cfdb4..421b2b49a0f5 100644 --- a/instrumentation/netty/netty-4.0/javaagent/build.gradle.kts +++ b/instrumentation/netty/netty-4.0/javaagent/build.gradle.kts @@ -42,6 +42,7 @@ tasks { includeTestsMatching("Netty40ClientSslTest") } include("**/Netty40ConnectionSpanTest.*", "**/Netty40ClientSslTest.*") + jvmArgs("-Dotel.instrumentation.netty.connection-telemetry.enabled=true") jvmArgs("-Dotel.instrumentation.netty.ssl-telemetry.enabled=true") } diff --git a/instrumentation/netty/netty-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4_0/BootstrapInstrumentation.java b/instrumentation/netty/netty-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4_0/BootstrapInstrumentation.java index e7260ed353be..37dcd6cfcd0c 100644 --- a/instrumentation/netty/netty-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4_0/BootstrapInstrumentation.java +++ b/instrumentation/netty/netty-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4_0/BootstrapInstrumentation.java @@ -11,12 +11,11 @@ import io.netty.channel.ChannelPromise; import io.opentelemetry.context.Context; -import io.opentelemetry.context.Scope; import io.opentelemetry.instrumentation.netty.common.internal.NettyConnectionRequest; -import io.opentelemetry.instrumentation.netty.v4.common.internal.client.ConnectionCompleteListener; import io.opentelemetry.javaagent.bootstrap.Java8BytecodeBridge; import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.netty.v4.common.NettyScope; import java.net.SocketAddress; import net.bytebuddy.asm.Advice; import net.bytebuddy.description.type.TypeDescription; @@ -40,42 +39,24 @@ public void transform(TypeTransformer transformer) { @SuppressWarnings("unused") public static class ConnectAdvice { @Advice.OnMethodEnter - public static void startConnect( - @Advice.Argument(2) SocketAddress remoteAddress, - @Advice.Local("otelContext") Context context, - @Advice.Local("otelRequest") NettyConnectionRequest request, - @Advice.Local("otelScope") Scope scope) { + public static NettyScope startConnect(@Advice.Argument(2) SocketAddress remoteAddress) { Context parentContext = Java8BytecodeBridge.currentContext(); - request = NettyConnectionRequest.connect(remoteAddress); + NettyConnectionRequest request = NettyConnectionRequest.connect(remoteAddress); if (!connectionInstrumenter().shouldStart(parentContext, request)) { - return; + return null; } - - context = connectionInstrumenter().start(parentContext, request); - scope = context.makeCurrent(); + return NettyScope.startNew(connectionInstrumenter(), parentContext, request); } @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) public static void endConnect( @Advice.Thrown Throwable throwable, @Advice.Argument(4) ChannelPromise channelPromise, - @Advice.Local("otelContext") Context context, - @Advice.Local("otelRequest") NettyConnectionRequest request, - @Advice.Local("otelScope") Scope scope) { - - if (scope == null) { - return; - } - scope.close(); + @Advice.Enter NettyScope enterScope) { - if (throwable != null) { - connectionInstrumenter().end(context, request, null, throwable); - } else { - channelPromise.addListener( - new ConnectionCompleteListener(connectionInstrumenter(), context, request)); - } + NettyScope.end(enterScope, connectionInstrumenter(), channelPromise, throwable); } } } diff --git a/instrumentation/netty/netty-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4_0/NettyChannelPipelineInstrumentation.java b/instrumentation/netty/netty-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4_0/NettyChannelPipelineInstrumentation.java index f3dcd2eb463b..0db21d84da39 100644 --- a/instrumentation/netty/netty-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4_0/NettyChannelPipelineInstrumentation.java +++ b/instrumentation/netty/netty-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4_0/NettyChannelPipelineInstrumentation.java @@ -48,23 +48,24 @@ public void transform(TypeTransformer transformer) { /** * 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. + * handlers. If those handlers are later removed, we may want to remove our handlers. */ @SuppressWarnings("unused") public static class ChannelPipelineAddAdvice { @Advice.OnMethodEnter - public static void trackCallDepth(@Advice.Local("otelCallDepth") CallDepth callDepth) { - callDepth = CallDepth.forClass(ChannelPipeline.class); + public static CallDepth trackCallDepth() { + CallDepth callDepth = CallDepth.forClass(ChannelPipeline.class); callDepth.getAndIncrement(); + return callDepth; } @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) public static void addHandler( @Advice.This ChannelPipeline pipeline, @Advice.Argument(2) ChannelHandler handler, - @Advice.Local("otelCallDepth") CallDepth callDepth) { + @Advice.Enter CallDepth callDepth) { + if (callDepth.decrementAndGet() > 0) { return; } diff --git a/instrumentation/netty/netty-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4_0/NettyInstrumentationModule.java b/instrumentation/netty/netty-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4_0/NettyInstrumentationModule.java index 1d2151795892..a20246a5d1e3 100644 --- a/instrumentation/netty/netty-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4_0/NettyInstrumentationModule.java +++ b/instrumentation/netty/netty-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4_0/NettyInstrumentationModule.java @@ -12,12 +12,14 @@ 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.internal.ExperimentalInstrumentationModule; import io.opentelemetry.javaagent.instrumentation.netty.v4.common.NettyFutureInstrumentation; import java.util.List; import net.bytebuddy.matcher.ElementMatcher; @AutoService(InstrumentationModule.class) -public class NettyInstrumentationModule extends InstrumentationModule { +public class NettyInstrumentationModule extends InstrumentationModule + implements ExperimentalInstrumentationModule { public NettyInstrumentationModule() { super("netty", "netty-4.0"); } @@ -31,6 +33,11 @@ public ElementMatcher.Junction classLoaderMatcher() { not(hasClassesNamed("io.netty.handler.codec.http.CombinedHttpHeaders"))); } + @Override + public String getModuleGroup() { + return "netty"; + } + @Override public List typeInstrumentations() { return asList( diff --git a/instrumentation/netty/netty-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4_0/client/NettyClientSingletons.java b/instrumentation/netty/netty-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4_0/client/NettyClientSingletons.java index 620f46487e23..1c5a49abaef8 100644 --- a/instrumentation/netty/netty-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4_0/client/NettyClientSingletons.java +++ b/instrumentation/netty/netty-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4_0/client/NettyClientSingletons.java @@ -5,6 +5,8 @@ package io.opentelemetry.javaagent.instrumentation.netty.v4_0.client; +import static io.opentelemetry.instrumentation.netty.v4.common.internal.client.NettyConnectionInstrumentationFlag.enabledOrErrorOnly; + import io.netty.handler.codec.http.HttpResponse; import io.opentelemetry.api.GlobalOpenTelemetry; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; @@ -12,27 +14,19 @@ import io.opentelemetry.instrumentation.netty.v4.common.internal.client.NettyClientInstrumenterFactory; import io.opentelemetry.instrumentation.netty.v4.common.internal.client.NettyConnectionInstrumenter; import io.opentelemetry.instrumentation.netty.v4.common.internal.client.NettySslInstrumenter; -import io.opentelemetry.javaagent.bootstrap.internal.CommonConfig; -import io.opentelemetry.javaagent.bootstrap.internal.DeprecatedConfigProperties; -import io.opentelemetry.javaagent.bootstrap.internal.InstrumentationConfig; +import io.opentelemetry.javaagent.bootstrap.internal.AgentCommonConfig; +import io.opentelemetry.javaagent.bootstrap.internal.AgentInstrumentationConfig; import java.util.Collections; +import java.util.function.Function; public final class NettyClientSingletons { - private static final boolean connectionTelemetryEnabled; - private static final boolean sslTelemetryEnabled; - - static { - InstrumentationConfig config = InstrumentationConfig.get(); - connectionTelemetryEnabled = - DeprecatedConfigProperties.getBoolean( - config, - "otel.instrumentation.netty.always-create-connect-span", - "otel.instrumentation.netty.connection-telemetry.enabled", - false); - sslTelemetryEnabled = - config.getBoolean("otel.instrumentation.netty.ssl-telemetry.enabled", false); - } + private static final boolean connectionTelemetryEnabled = + AgentInstrumentationConfig.get() + .getBoolean("otel.instrumentation.netty.connection-telemetry.enabled", false); + private static final boolean sslTelemetryEnabled = + AgentInstrumentationConfig.get() + .getBoolean("otel.instrumentation.netty.ssl-telemetry.enabled", false); private static final Instrumenter INSTRUMENTER; private static final NettyConnectionInstrumenter CONNECTION_INSTRUMENTER; @@ -43,13 +37,20 @@ public final class NettyClientSingletons { new NettyClientInstrumenterFactory( GlobalOpenTelemetry.get(), "io.opentelemetry.netty-4.0", - connectionTelemetryEnabled, - sslTelemetryEnabled, - CommonConfig.get().getPeerServiceMapping()); + enabledOrErrorOnly(connectionTelemetryEnabled), + enabledOrErrorOnly(sslTelemetryEnabled), + AgentCommonConfig.get().getPeerServiceResolver(), + AgentCommonConfig.get().shouldEmitExperimentalHttpClientTelemetry()); INSTRUMENTER = factory.createHttpInstrumenter( - CommonConfig.get().getClientRequestHeaders(), - CommonConfig.get().getClientResponseHeaders(), + builder -> + builder + .setCapturedRequestHeaders(AgentCommonConfig.get().getClientRequestHeaders()) + .setCapturedResponseHeaders(AgentCommonConfig.get().getClientResponseHeaders()) + .setKnownMethods(AgentCommonConfig.get().getKnownHttpRequestMethods()), + builder -> + builder.setKnownMethods(AgentCommonConfig.get().getKnownHttpRequestMethods()), + Function.identity(), Collections.emptyList()); CONNECTION_INSTRUMENTER = factory.createConnectionInstrumenter(); SSL_INSTRUMENTER = factory.createSslInstrumenter(); diff --git a/instrumentation/netty/netty-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4_0/server/NettyServerSingletons.java b/instrumentation/netty/netty-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4_0/server/NettyServerSingletons.java index 6af51fad8b4d..e85212f363cc 100644 --- a/instrumentation/netty/netty-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4_0/server/NettyServerSingletons.java +++ b/instrumentation/netty/netty-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4_0/server/NettyServerSingletons.java @@ -10,7 +10,7 @@ import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; import io.opentelemetry.instrumentation.netty.v4.common.HttpRequestAndChannel; import io.opentelemetry.instrumentation.netty.v4.common.internal.server.NettyServerInstrumenterFactory; -import io.opentelemetry.javaagent.bootstrap.internal.CommonConfig; +import io.opentelemetry.javaagent.bootstrap.internal.AgentCommonConfig; public final class NettyServerSingletons { @@ -18,8 +18,14 @@ public final class NettyServerSingletons { NettyServerInstrumenterFactory.create( GlobalOpenTelemetry.get(), "io.opentelemetry.netty-4.0", - CommonConfig.get().getServerRequestHeaders(), - CommonConfig.get().getServerResponseHeaders()); + builder -> + builder + .setCapturedRequestHeaders(AgentCommonConfig.get().getServerRequestHeaders()) + .setCapturedResponseHeaders(AgentCommonConfig.get().getServerResponseHeaders()) + .setKnownMethods(AgentCommonConfig.get().getKnownHttpRequestMethods()), + builder -> builder.setKnownMethods(AgentCommonConfig.get().getKnownHttpRequestMethods()), + builder -> builder.setKnownMethods(AgentCommonConfig.get().getKnownHttpRequestMethods()), + AgentCommonConfig.get().shouldEmitExperimentalHttpServerTelemetry()); public static Instrumenter instrumenter() { return INSTRUMENTER; diff --git a/instrumentation/netty/netty-4.0/javaagent/src/test/groovy/ChannelFutureTest.groovy b/instrumentation/netty/netty-4.0/javaagent/src/test/groovy/ChannelFutureTest.groovy deleted file mode 100644 index 8bc83466706c..000000000000 --- a/instrumentation/netty/netty-4.0/javaagent/src/test/groovy/ChannelFutureTest.groovy +++ /dev/null @@ -1,74 +0,0 @@ -/* - * 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/instrumentation/netty/netty-4.0/javaagent/src/test/groovy/ChannelPipelineTest.groovy b/instrumentation/netty/netty-4.0/javaagent/src/test/groovy/ChannelPipelineTest.groovy deleted file mode 100644 index e25a22ae5ff8..000000000000 --- a/instrumentation/netty/netty-4.0/javaagent/src/test/groovy/ChannelPipelineTest.groovy +++ /dev/null @@ -1,136 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -import io.netty.channel.ChannelHandlerAdapter -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 - -@Unroll -class ChannelPipelineTest extends AgentInstrumentationSpecification { - - // regression test for https://github.com/open-telemetry/opentelemetry-java-instrumentation/issues/1373 - // and https://github.com/open-telemetry/opentelemetry-java-instrumentation/issues/4040 - def "test remove our handler #testName"() { - setup: - def channel = new EmbeddedChannel(new NoopChannelHandler()) - 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") } - "first" | { pipeline, h -> pipeline.removeFirst() } - } - - // regression test for https://github.com/open-telemetry/opentelemetry-java-instrumentation/issues/4040 - def "should replace handler #desc"() { - setup: - def channel = new EmbeddedChannel(new NoopChannelHandler()) - def channelPipeline = new DefaultChannelPipeline(channel) - def httpHandler = new HttpClientCodec() - - expect: "no handlers initially" - channelPipeline.size() == 0 - - when: - def noopHandler = new NoopChannelHandler() - channelPipeline.addFirst("test", noopHandler) - - then: "only the noop handler" - channelPipeline.size() == 1 - channelPipeline.first() == noopHandler - - when: - replaceMethod(channelPipeline, "test", noopHandler, "http", httpHandler) - - then: "noop handler was removed; http and instrumentation handlers were added" - channelPipeline.size() == 2 - channelPipeline.first() == httpHandler - channelPipeline.last().getClass() == HttpClientTracingHandler - - when: - def anotherNoopHandler = new NoopChannelHandler() - replaceMethod(channelPipeline, "http", httpHandler, "test", anotherNoopHandler) - - then: "http and instrumentation handlers were removed; noop handler was added" - channelPipeline.size() == 1 - channelPipeline.first() == anotherNoopHandler - - where: - desc | replaceMethod - "by instance" | { pipeline, oldName, oldHandler, newName, newHandler -> pipeline.replace(oldHandler, newName, newHandler) } - "by class" | { pipeline, oldName, oldHandler, newName, newHandler -> pipeline.replace(oldHandler.getClass(), newName, newHandler) } - "by name" | { pipeline, oldName, oldHandler, newName, newHandler -> pipeline.replace(oldName, newName, newHandler) } - } - - // regression test for https://github.com/open-telemetry/opentelemetry-java-instrumentation/issues/4056 - def "should addAfter and removeLast handler #desc"() { - setup: - def channel = new EmbeddedChannel(new NoopChannelHandler()) - def channelPipeline = new DefaultChannelPipeline(channel) - def httpHandler = new HttpClientCodec() - - expect: "no handlers initially" - channelPipeline.size() == 0 - - when: - channelPipeline.addLast("http", httpHandler) - - then: "add http and instrumentation handlers" - channelPipeline.size() == 2 - channelPipeline.first() == httpHandler - channelPipeline.last().getClass() == HttpClientTracingHandler - - when: - def noopHandler = new NoopChannelHandler() - channelPipeline.addAfter("http", "noop", noopHandler) - - then: "instrumentation handler is between with http and noop" - channelPipeline.size() == 3 - channelPipeline.first() == httpHandler - channelPipeline.last() == noopHandler - - when: - channelPipeline.removeLast() - - then: "http and instrumentation handlers will be remained" - channelPipeline.size() == 2 - channelPipeline.first() == httpHandler - channelPipeline.last().getClass() == HttpClientTracingHandler - - when: - channelPipeline.removeLast() - - then: "there is no handler in pipeline" - channelPipeline.size() == 0 - } - - private static class NoopChannelHandler extends ChannelHandlerAdapter { - } -} diff --git a/instrumentation/netty/netty-4.0/javaagent/src/test/groovy/Netty40ClientSslTest.groovy b/instrumentation/netty/netty-4.0/javaagent/src/test/groovy/Netty40ClientSslTest.groovy deleted file mode 100644 index 15e3886f6caf..000000000000 --- a/instrumentation/netty/netty-4.0/javaagent/src/test/groovy/Netty40ClientSslTest.groovy +++ /dev/null @@ -1,215 +0,0 @@ -/* - * 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.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.netty.handler.ssl.SslHandler -import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification -import io.opentelemetry.instrumentation.testing.junit.http.HttpClientTestServer -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes -import spock.lang.Shared - -import javax.net.ssl.SSLContext -import javax.net.ssl.SSLHandshakeException -import java.util.concurrent.CompletableFuture -import java.util.concurrent.ExecutionException -import java.util.concurrent.TimeUnit - -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.api.trace.StatusCode.ERROR -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.NetTransportValues.IP_TCP - -class Netty40ClientSslTest extends AgentInstrumentationSpecification { - - @Shared - HttpClientTestServer server - @Shared - EventLoopGroup eventLoopGroup - - def setupSpec() { - server = new HttpClientTestServer(openTelemetry) - server.start() - eventLoopGroup = new NioEventLoopGroup() - } - - def cleanupSpec() { - server.stop().get(10, TimeUnit.SECONDS) - eventLoopGroup.shutdownGracefully() - } - - def "should fail SSL handshake"() { - given: - def bootstrap = createBootstrap(eventLoopGroup, ["SSLv3"]) - - def uri = server.resolveHttpsAddress("/success") - def request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, uri.path, Unpooled.EMPTY_BUFFER) - HttpHeaders.setHost(request, uri.host + ":" + uri.port) - - when: - Channel channel = null - runWithSpan("parent") { - channel = bootstrap.connect(uri.host, uri.port).sync().channel() - def result = new CompletableFuture() - channel.pipeline().addLast(new ClientHandler(result)) - channel.writeAndFlush(request).get(10, TimeUnit.SECONDS) - result.get(10, TimeUnit.SECONDS) - } - - then: - Throwable thrownException = thrown() - if (thrownException instanceof ExecutionException) { - thrownException = thrownException.cause - } - - assertTraces(1) { - trace(0, 3) { - span(0) { - name "parent" - status ERROR - errorEvent(thrownException.class, thrownException.message) - } - span(1) { - name "CONNECT" - kind INTERNAL - childOf span(0) - attributes { - "$SemanticAttributes.NET_TRANSPORT" IP_TCP - "$SemanticAttributes.NET_PEER_NAME" uri.host - "$SemanticAttributes.NET_PEER_PORT" uri.port - "$SemanticAttributes.NET_SOCK_PEER_ADDR" { it == "127.0.0.1" || it == null } - } - } - span(2) { - name "SSL handshake" - kind INTERNAL - childOf span(0) - status ERROR - // netty swallows the exception, it doesn't make any sense to hard-code the message - errorEventWithAnyMessage(SSLHandshakeException) - attributes { - "$SemanticAttributes.NET_TRANSPORT" IP_TCP - "$SemanticAttributes.NET_SOCK_PEER_ADDR" { it == "127.0.0.1" || it == null } - "$SemanticAttributes.NET_SOCK_PEER_NAME" uri.host - "$SemanticAttributes.NET_SOCK_PEER_PORT" uri.port - } - } - } - } - - cleanup: - channel?.close()?.sync() - } - - def "should successfully establish SSL handshake"() { - given: - def bootstrap = createBootstrap(eventLoopGroup, ["TLSv1", "TLSv1.1", "TLSv1.2", "TLSv1.3"]) - - def uri = server.resolveHttpsAddress("/success") - def request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, uri.path, Unpooled.EMPTY_BUFFER) - HttpHeaders.setHost(request, uri.host + ":" + uri.port) - - when: - Channel channel = null - runWithSpan("parent") { - channel = bootstrap.connect(uri.host, uri.port).sync().channel() - def result = new CompletableFuture() - channel.pipeline().addLast(new ClientHandler(result)) - channel.writeAndFlush(request).get(10, TimeUnit.SECONDS) - result.get(10, TimeUnit.SECONDS) - } - - then: - assertTraces(1) { - trace(0, 5) { - span(0) { - name "parent" - } - span(1) { - name "CONNECT" - kind INTERNAL - childOf span(0) - attributes { - "$SemanticAttributes.NET_TRANSPORT" IP_TCP - "$SemanticAttributes.NET_PEER_NAME" uri.host - "$SemanticAttributes.NET_PEER_PORT" uri.port - "$SemanticAttributes.NET_SOCK_PEER_ADDR" { it == "127.0.0.1" || it == null } - } - } - span(2) { - name "SSL handshake" - kind INTERNAL - childOf span(0) - attributes { - "$SemanticAttributes.NET_TRANSPORT" IP_TCP - "$SemanticAttributes.NET_SOCK_PEER_ADDR" { it == "127.0.0.1" || it == null } - "$SemanticAttributes.NET_SOCK_PEER_NAME" uri.host - "$SemanticAttributes.NET_SOCK_PEER_PORT" uri.port - } - } - span(3) { - name "GET" - kind CLIENT - childOf(span(0)) - } - span(4) { - name "test-http-server" - kind SERVER - childOf(span(3)) - } - } - } - - cleanup: - channel?.close()?.sync() - } - - // list of default ciphers copied from netty's JdkSslContext - private static final String[] SUPPORTED_CIPHERS = [ - "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", - "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA", - "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA", - "TLS_RSA_WITH_AES_128_GCM_SHA256", - "TLS_RSA_WITH_AES_128_CBC_SHA", - "TLS_RSA_WITH_AES_256_CBC_SHA", - "SSL_RSA_WITH_3DES_EDE_CBC_SHA" - ] - - private static Bootstrap createBootstrap(EventLoopGroup eventLoopGroup, List enabledProtocols) { - def bootstrap = new Bootstrap() - bootstrap.group(eventLoopGroup) - .channel(NioSocketChannel) - .handler(new ChannelInitializer() { - @Override - protected void initChannel(SocketChannel socketChannel) throws Exception { - ChannelPipeline pipeline = socketChannel.pipeline() - - def sslContext = SSLContext.getInstance("TLS") - sslContext.init(null, null, null) - def sslEngine = sslContext.createSSLEngine() - sslEngine.setUseClientMode(true) - sslEngine.setEnabledProtocols(enabledProtocols as String[]) - sslEngine.setEnabledCipherSuites(SUPPORTED_CIPHERS) - pipeline.addLast(new SslHandler(sslEngine)) - - pipeline.addLast(new HttpClientCodec()) - } - }) - bootstrap - } -} diff --git a/instrumentation/netty/netty-4.0/javaagent/src/test/groovy/Netty40ClientTest.groovy b/instrumentation/netty/netty-4.0/javaagent/src/test/groovy/Netty40ClientTest.groovy deleted file mode 100644 index 92f9a198c974..000000000000 --- a/instrumentation/netty/netty-4.0/javaagent/src/test/groovy/Netty40ClientTest.groovy +++ /dev/null @@ -1,150 +0,0 @@ -/* - * 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.netty.handler.timeout.ReadTimeoutHandler -import io.opentelemetry.api.common.AttributeKey -import io.opentelemetry.instrumentation.test.AgentTestTrait -import io.opentelemetry.instrumentation.test.base.HttpClientTest -import io.opentelemetry.instrumentation.testing.junit.http.HttpClientResult -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes -import spock.lang.Shared - -import java.util.concurrent.CompletableFuture -import java.util.concurrent.TimeUnit - -class Netty40ClientTest extends HttpClientTest implements AgentTestTrait { - - @Shared - private EventLoopGroup eventLoopGroup = new NioEventLoopGroup() - - @Shared - private Bootstrap bootstrap = buildBootstrap() - - @Shared - private Bootstrap readTimeoutBootstrap = buildBootstrap(true) - - def cleanupSpec() { - eventLoopGroup?.shutdownGracefully() - } - - Bootstrap buildBootstrap(boolean readTimeout = false) { - Bootstrap 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() - if (readTimeout) { - pipeline.addLast(new ReadTimeoutHandler(READ_TIMEOUT_MS, TimeUnit.MILLISECONDS)) - } - pipeline.addLast(new HttpClientCodec()) - } - }) - - return bootstrap - } - - Bootstrap getBootstrap(URI uri) { - if (uri.getPath() == "/read-timeout") { - return readTimeoutBootstrap - } - return bootstrap - } - - @Override - DefaultFullHttpRequest buildRequest(String method, URI uri, Map headers) { - def target = uri.path - if (uri.query != null) { - target += "?" + uri.query - } - def request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.valueOf(method), target, Unpooled.EMPTY_BUFFER) - HttpHeaders.setHost(request, uri.host + ":" + uri.port) - 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 = 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, HttpClientResult 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 "http://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 "http://192.0.2.1/": // non routable address - return [] - } - def attributes = super.httpAttributes(uri) - attributes.remove(SemanticAttributes.NET_PEER_NAME) - attributes.remove(SemanticAttributes.NET_PEER_PORT) - return attributes - } - - @Override - String userAgent() { - return "Netty" - } - - @Override - boolean testRedirects() { - false - } - - @Override - boolean testHttps() { - false - } -} diff --git a/instrumentation/netty/netty-4.0/javaagent/src/test/groovy/Netty40ConnectionSpanTest.groovy b/instrumentation/netty/netty-4.0/javaagent/src/test/groovy/Netty40ConnectionSpanTest.groovy deleted file mode 100644 index 8f4c8c242c38..000000000000 --- a/instrumentation/netty/netty-4.0/javaagent/src/test/groovy/Netty40ConnectionSpanTest.groovy +++ /dev/null @@ -1,165 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -import io.netty.bootstrap.Bootstrap -import io.netty.buffer.Unpooled -import io.netty.channel.ChannelInitializer -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.instrumentation.test.AgentTestTrait -import io.opentelemetry.instrumentation.test.InstrumentationSpecification -import io.opentelemetry.instrumentation.test.utils.PortUtils -import io.opentelemetry.instrumentation.testing.junit.http.HttpClientTestServer -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes -import spock.lang.Shared - -import java.util.concurrent.CompletableFuture -import java.util.concurrent.TimeUnit - -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.api.trace.StatusCode.ERROR -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.NetTransportValues.IP_TCP - -class Netty40ConnectionSpanTest extends InstrumentationSpecification implements AgentTestTrait { - - @Shared - private HttpClientTestServer server - - @Shared - private EventLoopGroup eventLoopGroup = new NioEventLoopGroup() - - @Shared - private Bootstrap bootstrap = buildBootstrap() - - def setupSpec() { - server = new HttpClientTestServer(openTelemetry) - server.start() - } - - def cleanupSpec() { - eventLoopGroup.shutdownGracefully() - server.stop() - } - - Bootstrap buildBootstrap() { - Bootstrap bootstrap = new Bootstrap() - bootstrap.group(eventLoopGroup) - .channel(NioSocketChannel) - .handler(new ChannelInitializer() { - @Override - protected void initChannel(SocketChannel socketChannel) throws Exception { - ChannelPipeline pipeline = socketChannel.pipeline() - pipeline.addLast(new HttpClientCodec()) - } - }) - - return bootstrap - } - - DefaultFullHttpRequest buildRequest(String method, URI uri, Map headers) { - def request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.valueOf(method), uri.path, Unpooled.EMPTY_BUFFER) - HttpHeaders.setHost(request, uri.host + ":" + uri.port) - headers.each { k, v -> request.headers().set(k, v) } - return request - } - - int sendRequest(DefaultFullHttpRequest request, URI uri) { - def channel = bootstrap.connect(uri.host, uri.port).sync().channel() - def result = new CompletableFuture() - channel.pipeline().addLast(new ClientHandler(result)) - channel.writeAndFlush(request).get() - return result.get(20, TimeUnit.SECONDS) - } - - def "test successful request"() { - when: - def uri = URI.create("http://localhost:${server.httpPort()}/success") - def request = buildRequest("GET", uri, [:]) - def responseCode = runWithSpan("parent") { - sendRequest(request, uri) - } - - then: - responseCode == 200 - assertTraces(1) { - trace(0, 4) { - span(0) { - name "parent" - kind INTERNAL - hasNoParent() - } - span(1) { - name "CONNECT" - kind INTERNAL - childOf(span(0)) - attributes { - "$SemanticAttributes.NET_TRANSPORT" IP_TCP - "$SemanticAttributes.NET_PEER_NAME" uri.host - "$SemanticAttributes.NET_PEER_PORT" uri.port - "$SemanticAttributes.NET_SOCK_PEER_ADDR" "127.0.0.1" - } - } - span(2) { - name "GET" - kind CLIENT - childOf(span(0)) - } - span(3) { - name "test-http-server" - kind SERVER - childOf(span(2)) - } - } - } - } - - def "test failing request"() { - when: - URI uri = URI.create("http://localhost:${PortUtils.UNUSABLE_PORT}") - def request = buildRequest("GET", uri, [:]) - runWithSpan("parent") { - sendRequest(request, uri) - } - - then: - def thrownException = thrown(Exception) - - and: - assertTraces(1) { - trace(0, 2) { - span(0) { - name "parent" - kind INTERNAL - hasNoParent() - status ERROR - errorEvent(thrownException.class, thrownException.message) - } - span(1) { - name "CONNECT" - kind INTERNAL - childOf(span(0)) - status ERROR - errorEvent(thrownException.class, thrownException.message) - attributes { - "$SemanticAttributes.NET_TRANSPORT" IP_TCP - "$SemanticAttributes.NET_PEER_NAME" uri.host - "$SemanticAttributes.NET_PEER_PORT" uri.port - "$SemanticAttributes.NET_SOCK_PEER_ADDR" { it == "127.0.0.1" || it == null } - } - } - } - } - } -} diff --git a/instrumentation/netty/netty-4.0/javaagent/src/test/groovy/Netty40ServerTest.groovy b/instrumentation/netty/netty-4.0/javaagent/src/test/groovy/Netty40ServerTest.groovy deleted file mode 100644 index 25e112391abd..000000000000 --- a/instrumentation/netty/netty-4.0/javaagent/src/test/groovy/Netty40ServerTest.groovy +++ /dev/null @@ -1,153 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -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.api.common.AttributeKey -import io.opentelemetry.instrumentation.test.AgentTestTrait -import io.opentelemetry.instrumentation.test.base.HttpServerTest -import io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes - -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.testing.junit.http.ServerEndpoint.CAPTURE_HEADERS -import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.ERROR -import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.EXCEPTION -import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.INDEXED_CHILD -import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.NOT_FOUND -import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.QUERY_PARAM -import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.REDIRECT -import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.SUCCESS - -class Netty40ServerTest extends HttpServerTest implements AgentTestTrait { - - static final LoggingHandler LOGGING_HANDLER = new LoggingHandler(SERVER_LOGGER.name, LogLevel.DEBUG) - - @Override - boolean hasResponseCustomizer(ServerEndpoint endpoint) { - true - } - - @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(new SimpleChannelInboundHandler() { - - @Override - protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception { - if (msg instanceof HttpRequest) { - def request = msg as HttpRequest - def uri = URI.create(request.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 CAPTURE_HEADERS: - content = Unpooled.copiedBuffer(endpoint.body, CharsetUtil.UTF_8) - response = new DefaultFullHttpResponse(HTTP_1_1, HttpResponseStatus.valueOf(endpoint.status), content) - response.headers().set("X-Test-Response", request.headers().get("X-Test-Request")) - 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 - } - } - } - - @Override - void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { - 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) - } - - @Override - void channelReadComplete(ChannelHandlerContext ctx) throws Exception { - ctx.flush() - } - }) - } - ] as ChannelInitializer).channel(NioServerSocketChannel) - bootstrap.bind(port).sync() - - return eventLoopGroup - } - - @Override - void stopServer(EventLoopGroup server) { - server?.shutdownGracefully() - } - - @Override - Set> httpAttributes(ServerEndpoint endpoint) { - def attributes = super.httpAttributes(endpoint) - attributes.remove(SemanticAttributes.HTTP_ROUTE) - attributes - } -} diff --git a/instrumentation/netty/netty-4.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/netty/v4_0/ChannelFutureTest.java b/instrumentation/netty/netty-4.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/netty/v4_0/ChannelFutureTest.java new file mode 100644 index 000000000000..efbc4a828b99 --- /dev/null +++ b/instrumentation/netty/netty-4.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/netty/v4_0/ChannelFutureTest.java @@ -0,0 +1,82 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.netty.v4_0; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +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.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +class ChannelFutureTest { + + @RegisterExtension + static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + @SuppressWarnings("unchecked") + @Test + void shouldCleanUpWrappedListeners() throws Exception { + EmbeddedChannel channel = new EmbeddedChannel(new EmptyChannelHandler()); + AtomicInteger counter = new AtomicInteger(); + + GenericFutureListener> listener1 = newListener(counter); + channel.closeFuture().addListener(listener1); + channel.closeFuture().removeListener(listener1); + + GenericFutureListener> listener2 = newListener(counter); + GenericProgressiveFutureListener> listener3 = + newProgressiveListener(counter); + channel.closeFuture().addListener(listener2); + channel.closeFuture().addListener(listener3); + channel.closeFuture().removeListeners(listener2, listener3); + + channel.close().await(5, TimeUnit.SECONDS); + + assertEquals(0, counter.get()); + } + + private static GenericFutureListener> newListener(AtomicInteger counter) { + return future -> counter.incrementAndGet(); + } + + private static GenericProgressiveFutureListener> newProgressiveListener( + AtomicInteger counter) { + return new GenericProgressiveFutureListener>() { + @Override + public void operationComplete(@NotNull ProgressiveFuture future) throws Exception { + counter.incrementAndGet(); + } + + @Override + public void operationProgressed(ProgressiveFuture future, long progress, long total) + throws Exception { + counter.incrementAndGet(); + } + }; + } + + private static class EmptyChannelHandler implements ChannelHandler { + @Override + public void handlerAdded(ChannelHandlerContext ctx) throws Exception {} + + @Override + public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {} + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {} + } +} diff --git a/instrumentation/netty/netty-4.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/netty/v4_0/ChannelPipelineTest.java b/instrumentation/netty/netty-4.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/netty/v4_0/ChannelPipelineTest.java new file mode 100644 index 000000000000..ecc5486c7b54 --- /dev/null +++ b/instrumentation/netty/netty-4.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/netty/v4_0/ChannelPipelineTest.java @@ -0,0 +1,228 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.netty.v4_0; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerAdapter; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.embedded.EmbeddedChannel; +import io.netty.handler.codec.http.HttpClientCodec; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import java.lang.reflect.Constructor; +import java.util.ArrayList; +import java.util.List; +import javax.annotation.Nullable; +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +class ChannelPipelineTest { + + @RegisterExtension + static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + private static final Class defaultChannelPipelineClass = getDefaultChannelPipelineClass(); + + @Nullable + private static Class getDefaultChannelPipelineClass() { + try { + return Class.forName("io.netty.channel.DefaultChannelPipeline"); + } catch (Exception e) { + return null; + } + } + + @NotNull + private static Constructor getConstructor() throws NoSuchMethodException { + assertNotNull(defaultChannelPipelineClass); + Constructor constructor = defaultChannelPipelineClass.getDeclaredConstructor(Channel.class); + constructor.setAccessible(true); + return constructor; + } + + // regression test for + // https://github.com/open-telemetry/opentelemetry-java-instrumentation/issues/1373 + // and https://github.com/open-telemetry/opentelemetry-java-instrumentation/issues/4040 + @ParameterizedTest + @CsvSource({"by instance", "by class", "by name", "first"}) + void testRemoveOurHandler(String testName) throws Exception { + EmbeddedChannel channel = new EmbeddedChannel(new NoopChannelHandler()); + ChannelPipeline channelPipeline = (ChannelPipeline) getConstructor().newInstance(channel); + HttpClientCodec handler = new HttpClientCodec(); + + // no handlers initially except the default one + assertTrue( + channelPipeline.first() == null + || "io.netty.channel.DefaultChannelPipeline$TailHandler" + .equals(channelPipeline.first().getClass().getName())); + assertNull(channelPipeline.last()); + assertEquals(0, channelPipeline.toMap().size()); + + // add handler + channelPipeline.addLast("http", handler); + assertEquals(handler, channelPipeline.first()); + // our handler was also added + assertEquals("HttpClientTracingHandler", channelPipeline.last().getClass().getSimpleName()); + assertEquals(1, channelPipeline.toMap().size()); + + if ("by instance".equals(testName)) { + channelPipeline.remove(handler); + } else if ("by class".equals(testName)) { + channelPipeline.remove(handler.getClass()); + } else if ("by name".equals(testName)) { + channelPipeline.remove("http"); + } else if ("first".equals(testName)) { + channelPipeline.removeFirst(); + } + + // removing handler also removes our handler + assertTrue( + channelPipeline.first() == null + || "io.netty.channel.DefaultChannelPipeline$TailHandler" + .equals(channelPipeline.first().getClass().getName())); + assertNull(channelPipeline.last()); + assertEquals(0, channelPipeline.toMap().size()); + } + + // regression test for + // https://github.com/open-telemetry/opentelemetry-java-instrumentation/issues/4040 + @ParameterizedTest + @CsvSource({"by instance", "by class", "by name"}) + void shouldReplaceHandler(String desc) throws Exception { + EmbeddedChannel channel = new EmbeddedChannel(new NoopChannelHandler()); + ChannelPipeline channelPipeline = (ChannelPipeline) getConstructor().newInstance(channel); + HttpClientCodec httpHandler = new HttpClientCodec(); + + // no handlers initially except the default one + assertTrue( + channelPipeline.first() == null + || "io.netty.channel.DefaultChannelPipeline$TailHandler" + .equals(channelPipeline.first().getClass().getName())); + assertNull(channelPipeline.last()); + assertEquals(0, channelPipeline.toMap().size()); + + NoopChannelHandler noopHandler = new NoopChannelHandler(); + channelPipeline.addFirst("test", noopHandler); + + // only the noop handler + assertEquals(noopHandler, channelPipeline.first()); + assertEquals(1, channelPipeline.toMap().size()); + + if ("by instance".equals(desc)) { + channelPipeline.replace(noopHandler, "http", httpHandler); + } else if ("by class".equals(desc)) { + channelPipeline.replace(noopHandler.getClass(), "http", httpHandler); + } else if ("by name".equals(desc)) { + channelPipeline.replace("test", "http", httpHandler); + } + + // noop handler was removed; http and instrumentation handlers were added + assertEquals(httpHandler, channelPipeline.first()); + assertEquals("HttpClientTracingHandler", channelPipeline.last().getClass().getSimpleName()); + assertEquals(1, channelPipeline.toMap().size()); + + NoopChannelHandler anotherNoopHandler = new NoopChannelHandler(); + channelPipeline.replace("http", "test", anotherNoopHandler); + + // http and instrumentation handlers were removed; noop handler was added + assertEquals(anotherNoopHandler, channelPipeline.first()); + } + + // regression test for + // https://github.com/open-telemetry/opentelemetry-java-instrumentation/issues/4056 + @Test + void shouldAddAfterAndRemoveLastHandler() throws Exception { + EmbeddedChannel channel = new EmbeddedChannel(new NoopChannelHandler()); + ChannelPipeline channelPipeline = (ChannelPipeline) getConstructor().newInstance(channel); + HttpClientCodec httpHandler = new HttpClientCodec(); + + // no handlers initially + assertTrue( + channelPipeline.first() == null + || "io.netty.channel.DefaultChannelPipeline$TailHandler" + .equals(channelPipeline.first().getClass().getName())); + assertNull(channelPipeline.last()); + assertEquals(0, channelPipeline.toMap().size()); + + // Add http and instrumentation handlers + channelPipeline.addLast("http", httpHandler); + assertEquals(channelPipeline.first(), httpHandler); + assertEquals("HttpClientTracingHandler", channelPipeline.last().getClass().getSimpleName()); + assertEquals(1, channelPipeline.toMap().size()); + + NoopChannelHandler noopHandler = new NoopChannelHandler(); + channelPipeline.addAfter("http", "noop", noopHandler); + + // instrumentation handler is between http and noop handlers + assertEquals(channelPipeline.first(), httpHandler); + assertEquals(channelPipeline.last(), noopHandler); + assertEquals(2, channelPipeline.toMap().size()); + + // http and instrumentation handlers will remain when last handler is removed + { + ChannelHandler removed = channelPipeline.removeLast(); + assertEquals(noopHandler, removed); + assertEquals(channelPipeline.first(), httpHandler); + assertEquals("HttpClientTracingHandler", channelPipeline.last().getClass().getSimpleName()); + assertEquals(1, channelPipeline.toMap().size()); + } + + // there is no handler in pipeline when last handler is removed + { + ChannelHandler removed = channelPipeline.removeLast(); + assertEquals(httpHandler, removed); + assertEquals(0, channelPipeline.toMap().size()); + } + } + + // regression test for + // https://github.com/open-telemetry/opentelemetry-java-instrumentation/issues/10377 + @Test + void ourHandlerNotInHandlerMap() throws Exception { + EmbeddedChannel channel = new EmbeddedChannel(new NoopChannelHandler()); + ChannelPipeline channelPipeline = (ChannelPipeline) getConstructor().newInstance(channel); + HttpClientCodec httpHandler = new HttpClientCodec(); + + // no handlers initially + assertTrue( + channelPipeline.first() == null + || "io.netty.channel.DefaultChannelPipeline$TailHandler" + .equals(channelPipeline.first().getClass().getName())); + assertNull(channelPipeline.last()); + assertEquals(0, channelPipeline.toMap().size()); + + // add handler + channelPipeline.addLast("http", httpHandler); + assertEquals(httpHandler, channelPipeline.first()); + + // our handler was also added + assertEquals("HttpClientTracingHandler", channelPipeline.last().getClass().getSimpleName()); + + // our handler is not in handlers map + assertEquals(1, channelPipeline.toMap().size()); + + // our handler is not in handlers iterator + List list = new ArrayList<>(); + channelPipeline + .iterator() + .forEachRemaining( + entry -> { + list.add(entry.getValue()); + }); + assertEquals(1, list.size()); + } + + private static class NoopChannelHandler extends ChannelHandlerAdapter {} +} diff --git a/instrumentation/netty/netty-4.0/javaagent/src/test/groovy/ClientHandler.java b/instrumentation/netty/netty-4.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/netty/v4_0/client/ClientHandler.java similarity index 54% rename from instrumentation/netty/netty-4.0/javaagent/src/test/groovy/ClientHandler.java rename to instrumentation/netty/netty-4.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/netty/v4_0/client/ClientHandler.java index c96c9697dcbb..b2fc03b9be4a 100644 --- a/instrumentation/netty/netty-4.0/javaagent/src/test/groovy/ClientHandler.java +++ b/instrumentation/netty/netty-4.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/netty/v4_0/client/ClientHandler.java @@ -3,9 +3,10 @@ * SPDX-License-Identifier: Apache-2.0 */ +package io.opentelemetry.javaagent.instrumentation.netty.v4_0.client; + import io.netty.channel.ChannelHandlerContext; -import io.netty.channel.SimpleChannelInboundHandler; -import io.netty.handler.codec.http.HttpObject; +import io.netty.channel.ChannelInboundHandlerAdapter; import io.netty.handler.codec.http.HttpResponse; import java.util.concurrent.CompletableFuture; @@ -14,26 +15,25 @@ 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; +class ClientHandler extends ChannelInboundHandlerAdapter { + private final CompletableFuture result; - public ClientHandler(CompletableFuture responseCode) { - this.responseCode = responseCode; + public ClientHandler(CompletableFuture result) { + this.result = result; } @Override - public void channelRead0(ChannelHandlerContext ctx, HttpObject msg) { + public void channelRead(ChannelHandlerContext ctx, Object msg) { if (msg instanceof HttpResponse) { - ctx.pipeline().remove(this); - HttpResponse response = (HttpResponse) msg; - responseCode.complete(response.getStatus().code()); + result.complete(response.getStatus().code()); } + ctx.fireChannelRead(msg); } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { - responseCode.completeExceptionally(cause); + result.completeExceptionally(cause); ctx.close(); } } diff --git a/instrumentation/netty/netty-4.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/netty/v4_0/client/Netty40ClientSslTest.java b/instrumentation/netty/netty-4.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/netty/v4_0/client/Netty40ClientSslTest.java new file mode 100644 index 000000000000..5269eb128e5e --- /dev/null +++ b/instrumentation/netty/netty-4.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/netty/v4_0/client/Netty40ClientSslTest.java @@ -0,0 +1,249 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.netty.v4_0.client; + +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.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; +import static org.assertj.core.api.Assertions.catchThrowable; + +import io.netty.bootstrap.Bootstrap; +import io.netty.buffer.Unpooled; +import io.netty.channel.Channel; +import io.netty.channel.ChannelInitializer; +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.netty.handler.ssl.SslHandler; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.http.HttpClientTestServer; +import io.opentelemetry.sdk.trace.data.StatusData; +import io.opentelemetry.semconv.NetworkAttributes; +import io.opentelemetry.semconv.ServerAttributes; +import java.net.URI; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicReference; +import javax.net.ssl.SSLContext; +import org.jetbrains.annotations.NotNull; +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; + +class Netty40ClientSslTest { + + @RegisterExtension + static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + private static HttpClientTestServer server; + private static EventLoopGroup eventLoopGroup; + + @BeforeAll + static void setup() { + server = new HttpClientTestServer(testing.getOpenTelemetry()); + server.start(); + eventLoopGroup = new NioEventLoopGroup(); + } + + @AfterAll + static void cleanup() throws InterruptedException, ExecutionException, TimeoutException { + eventLoopGroup.shutdownGracefully(); + server.stop().get(10, TimeUnit.SECONDS); + } + + @Test + public void shouldFailSslHandshake() { + Bootstrap bootstrap = createBootstrap(eventLoopGroup, Collections.singletonList("SSLv3")); + + URI uri = server.resolveHttpsAddress("/success"); + DefaultFullHttpRequest request = + new DefaultFullHttpRequest( + HttpVersion.HTTP_1_1, HttpMethod.GET, uri.getPath(), Unpooled.EMPTY_BUFFER); + HttpHeaders.setHost(request, uri.getHost() + ":" + uri.getPort()); + + Throwable thrownException = getThrowable(bootstrap, uri, request); + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("parent") + .hasStatus(StatusData.error()) + .hasException(thrownException), + span -> { + span.hasName("CONNECT").hasKind(INTERNAL).hasParent(trace.getSpan(0)); + span.hasAttributesSatisfyingExactly( + equalTo(NetworkAttributes.NETWORK_TRANSPORT, "tcp"), + equalTo(NetworkAttributes.NETWORK_TYPE, "ipv4"), + equalTo(ServerAttributes.SERVER_ADDRESS, uri.getHost()), + equalTo(ServerAttributes.SERVER_PORT, uri.getPort()), + equalTo(NetworkAttributes.NETWORK_PEER_PORT, uri.getPort()), + equalTo(NetworkAttributes.NETWORK_PEER_ADDRESS, "127.0.0.1")); + }, + span -> { + span.hasName("SSL handshake") + .hasKind(INTERNAL) + .hasParent(trace.getSpan(0)) + .hasStatus(StatusData.error()); + span.hasAttributesSatisfyingExactly( + equalTo(NetworkAttributes.NETWORK_TRANSPORT, "tcp"), + equalTo(NetworkAttributes.NETWORK_TYPE, "ipv4"), + equalTo(NetworkAttributes.NETWORK_PEER_PORT, uri.getPort()), + equalTo(NetworkAttributes.NETWORK_PEER_ADDRESS, "127.0.0.1")); + })); + } + + private static Throwable getThrowable( + Bootstrap bootstrap, URI uri, DefaultFullHttpRequest request) { + AtomicReference channel = new AtomicReference<>(); + Throwable thrown = + catchThrowable( + () -> + testing.runWithSpan( + "parent", + () -> { + try { + channel.set( + bootstrap.connect(uri.getHost(), uri.getPort()).sync().channel()); + CompletableFuture result = new CompletableFuture<>(); + channel.get().pipeline().addLast(new ClientHandler(result)); + channel.get().writeAndFlush(request).get(10, TimeUnit.SECONDS); + result.get(10, TimeUnit.SECONDS); + } finally { + if (channel.get() != null) { + channel.get().close(); + } + } + })); + + // Then + Throwable thrownException; + if (thrown instanceof ExecutionException) { + thrownException = thrown.getCause(); + } else { + thrownException = thrown; + } + return thrownException; + } + + @SuppressWarnings("InterruptedExceptionSwallowed") + @Test + public void shouldSuccessfullyEstablishSslHandshake() throws Exception { + // given + Bootstrap bootstrap = + createBootstrap(eventLoopGroup, Arrays.asList("TLSv1", "TLSv1.1", "TLSv1.2", "TLSv1.3")); + + URI uri = server.resolveHttpsAddress("/success"); + DefaultFullHttpRequest request = + new DefaultFullHttpRequest( + HttpVersion.HTTP_1_1, HttpMethod.GET, uri.getPath(), Unpooled.EMPTY_BUFFER); + HttpHeaders.setHost(request, uri.getHost() + ":" + uri.getPort()); + + AtomicReference channel = new AtomicReference<>(); + // when + testing.runWithSpan( + "parent", + () -> { + try { + channel.set(bootstrap.connect(uri.getHost(), uri.getPort()).sync().channel()); + CompletableFuture result = new CompletableFuture<>(); + channel.get().pipeline().addLast(new ClientHandler(result)); + channel.get().writeAndFlush(request).get(10, TimeUnit.SECONDS); + result.get(10, TimeUnit.SECONDS); + } finally { + if (channel.get() != null) { + channel.get().close(); + } + } + }); + + // then + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent"), + span -> { + span.hasName("CONNECT").hasKind(INTERNAL).hasParent(trace.getSpan(0)); + span.hasAttributesSatisfyingExactly( + equalTo(NetworkAttributes.NETWORK_TRANSPORT, "tcp"), + equalTo(NetworkAttributes.NETWORK_TYPE, "ipv4"), + equalTo(ServerAttributes.SERVER_ADDRESS, uri.getHost()), + equalTo(ServerAttributes.SERVER_PORT, uri.getPort()), + equalTo(NetworkAttributes.NETWORK_PEER_PORT, uri.getPort()), + equalTo(NetworkAttributes.NETWORK_PEER_ADDRESS, "127.0.0.1")); + }, + span -> { + span.hasName("SSL handshake").hasKind(INTERNAL).hasParent(trace.getSpan(0)); + span.hasAttributesSatisfyingExactly( + equalTo(NetworkAttributes.NETWORK_TRANSPORT, "tcp"), + equalTo(NetworkAttributes.NETWORK_TYPE, "ipv4"), + equalTo(NetworkAttributes.NETWORK_PEER_PORT, uri.getPort()), + equalTo(NetworkAttributes.NETWORK_PEER_ADDRESS, "127.0.0.1")); + }, + span -> { + span.hasName("GET").hasKind(CLIENT).hasParent(trace.getSpan(0)); + }, + span -> { + span.hasName("test-http-server").hasKind(SERVER).hasParent(trace.getSpan(3)); + })); + + if (channel.get() != null) { + channel.get().close().sync(); + } + } + + // list of default ciphers copied from netty's JdkSslContext + private static final String[] SUPPORTED_CIPHERS = + new String[] { + "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA", + "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA", + "TLS_RSA_WITH_AES_128_GCM_SHA256", + "TLS_RSA_WITH_AES_128_CBC_SHA", + "TLS_RSA_WITH_AES_256_CBC_SHA", + "SSL_RSA_WITH_3DES_EDE_CBC_SHA" + }; + + private static Bootstrap createBootstrap( + EventLoopGroup eventLoopGroup, List enabledProtocols) { + Bootstrap bootstrap = new Bootstrap(); + bootstrap + .group(eventLoopGroup) + .channel(NioSocketChannel.class) + .handler( + new ChannelInitializer() { + @Override + protected void initChannel(@NotNull SocketChannel socketChannel) throws Exception { + ChannelPipeline pipeline = socketChannel.pipeline(); + + SSLContext sslContext = SSLContext.getInstance("TLS"); + sslContext.init(null, null, null); + javax.net.ssl.SSLEngine sslEngine = sslContext.createSSLEngine(); + sslEngine.setUseClientMode(true); + sslEngine.setEnabledProtocols(enabledProtocols.toArray(new String[0])); + sslEngine.setEnabledCipherSuites(SUPPORTED_CIPHERS); + pipeline.addLast(new SslHandler(sslEngine)); + + pipeline.addLast(new HttpClientCodec()); + } + }); + return bootstrap; + } +} diff --git a/instrumentation/netty/netty-4.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/netty/v4_0/client/Netty40ClientTest.java b/instrumentation/netty/netty-4.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/netty/v4_0/client/Netty40ClientTest.java new file mode 100644 index 000000000000..2bdc6fc32d88 --- /dev/null +++ b/instrumentation/netty/netty-4.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/netty/v4_0/client/Netty40ClientTest.java @@ -0,0 +1,181 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.netty.v4_0.client; + +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.netty.handler.timeout.ReadTimeoutHandler; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpClientTest; +import io.opentelemetry.instrumentation.testing.junit.http.HttpClientInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.http.HttpClientResult; +import io.opentelemetry.instrumentation.testing.junit.http.HttpClientTestOptions; +import io.opentelemetry.semconv.ServerAttributes; +import java.net.URI; +import java.util.Collections; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.extension.RegisterExtension; + +class Netty40ClientTest extends AbstractHttpClientTest { + + @RegisterExtension + static final InstrumentationExtension testing = HttpClientInstrumentationExtension.forAgent(); + + private final EventLoopGroup eventLoopGroup = new NioEventLoopGroup(); + private final Bootstrap bootstrap = buildBootstrap(false); + private final Bootstrap readTimeoutBootstrap = buildBootstrap(true); + + @AfterAll + void cleanup() { + eventLoopGroup.shutdownGracefully(); + } + + Bootstrap buildBootstrap(boolean readTimeout) { + Bootstrap bootstrap = new Bootstrap(); + bootstrap + .group(eventLoopGroup) + .channel(NioSocketChannel.class) + .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000) + .handler( + new ChannelInitializer() { + @Override + protected void initChannel(@NotNull SocketChannel socketChannel) throws Exception { + ChannelPipeline pipeline = socketChannel.pipeline(); + if (readTimeout) { + pipeline.addLast(new ReadTimeoutHandler(2000, TimeUnit.MILLISECONDS)); + } + pipeline.addLast(new HttpClientCodec()); + } + }); + + return bootstrap; + } + + private Bootstrap getBootstrap(URI uri) { + if ("/read-timeout".equals(uri.getPath())) { + return readTimeoutBootstrap; + } + return bootstrap; + } + + @Override + public DefaultFullHttpRequest buildRequest(String method, URI uri, Map headers) { + String target = uri.getPath(); + if (uri.getQuery() != null) { + target += "?" + uri.getQuery(); + } + DefaultFullHttpRequest request = + new DefaultFullHttpRequest( + HttpVersion.HTTP_1_1, HttpMethod.valueOf(method), target, Unpooled.EMPTY_BUFFER); + HttpHeaders.setHost(request, uri.getHost() + ":" + uri.getPort()); + request.headers().set("user-agent", "Netty"); + headers.forEach((k, v) -> request.headers().set(k, v)); + return request; + } + + @Override + public int sendRequest( + DefaultFullHttpRequest request, String method, URI uri, Map headers) + throws Exception { + Channel channel = getBootstrap(uri).connect(uri.getHost(), getPort(uri)).sync().channel(); + CompletableFuture result = new CompletableFuture<>(); + channel.pipeline().addLast(new ClientHandler(result)); + channel.writeAndFlush(request).get(); + return result.get(20, TimeUnit.SECONDS); + } + + @Override + public void sendRequestWithCallback( + DefaultFullHttpRequest request, + String method, + URI uri, + Map headers, + HttpClientResult httpClientResult) + throws Exception { + Channel ch; + try { + ch = getBootstrap(uri).connect(uri.getHost(), getPort(uri)).sync().channel(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return; + } catch (Throwable th) { + httpClientResult.complete(th); + return; + } + CompletableFuture result = new CompletableFuture<>(); + result.whenComplete((status, throwable) -> httpClientResult.complete(() -> status, throwable)); + ch.pipeline().addLast(new ClientHandler(result)); + ch.writeAndFlush(request); + } + + @Override + protected void configure(HttpClientTestOptions.Builder optionsBuilder) { + optionsBuilder.disableTestRedirects(); + optionsBuilder.disableTestHttps(); + optionsBuilder.spanEndsAfterBody(); + + optionsBuilder.setExpectedClientSpanNameMapper(Netty40ClientTest::expectedClientSpanName); + optionsBuilder.setHttpAttributes(Netty40ClientTest::httpAttributes); + } + + private static int getPort(URI uri) { + int port = uri.getPort(); + if (port == -1) { + switch (uri.getScheme()) { + case "http": + return 80; + case "https": + return 443; + default: + throw new IllegalArgumentException("Unknown scheme: " + uri.getScheme()); + } + } + return port; + } + + private static String expectedClientSpanName(URI uri, String method) { + switch (uri.toString()) { + case "http://localhost:61/": // unopened port + case "http://192.0.2.1/": // non routable address + return "CONNECT"; + default: + return HttpClientTestOptions.DEFAULT_EXPECTED_CLIENT_SPAN_NAME_MAPPER.apply(uri, method); + } + } + + @SuppressWarnings("MissingDefault") + private static Set> httpAttributes(URI uri) { + switch (uri.toString()) { + case "http://localhost:61/": // unopened port + case "http://192.0.2.1/": // non routable address + return Collections.emptySet(); + } + Set> attributes = new HashSet<>(HttpClientTestOptions.DEFAULT_HTTP_ATTRIBUTES); + attributes.remove(ServerAttributes.SERVER_ADDRESS); + attributes.remove(ServerAttributes.SERVER_PORT); + return attributes; + } +} diff --git a/instrumentation/netty/netty-4.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/netty/v4_0/client/Netty40ConnectionSpanTest.java b/instrumentation/netty/netty-4.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/netty/v4_0/client/Netty40ConnectionSpanTest.java new file mode 100644 index 000000000000..45c36c980f2f --- /dev/null +++ b/instrumentation/netty/netty-4.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/netty/v4_0/client/Netty40ConnectionSpanTest.java @@ -0,0 +1,175 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.netty.v4_0.client; + +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.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.satisfies; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.catchThrowable; + +import io.netty.bootstrap.Bootstrap; +import io.netty.buffer.Unpooled; +import io.netty.channel.Channel; +import io.netty.channel.ChannelInitializer; +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.instrumentation.test.utils.PortUtils; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.http.HttpClientTestServer; +import io.opentelemetry.semconv.NetworkAttributes; +import io.opentelemetry.semconv.ServerAttributes; +import java.net.URI; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import org.jetbrains.annotations.NotNull; +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; + +class Netty40ConnectionSpanTest { + + @RegisterExtension + static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + private static HttpClientTestServer server; + private static final EventLoopGroup eventLoopGroup = new NioEventLoopGroup(); + private static final Bootstrap bootstrap = buildBootstrap(); + + @BeforeAll + static void setupSpec() { + server = new HttpClientTestServer(testing.getOpenTelemetry()); + server.start(); + } + + @AfterAll + static void cleanupSpec() throws InterruptedException, ExecutionException, TimeoutException { + eventLoopGroup.shutdownGracefully(); + server.stop().get(10, TimeUnit.SECONDS); + } + + static Bootstrap buildBootstrap() { + Bootstrap bootstrap = new Bootstrap(); + bootstrap + .group(eventLoopGroup) + .channel(NioSocketChannel.class) + .handler( + new ChannelInitializer() { + @Override + protected void initChannel(@NotNull SocketChannel socketChannel) throws Exception { + ChannelPipeline pipeline = socketChannel.pipeline(); + pipeline.addLast(new HttpClientCodec()); + } + }); + + return bootstrap; + } + + @Test + void successfulRequest() throws Exception { + // when + URI uri = URI.create("http://localhost:" + server.httpPort() + "/success"); + + DefaultFullHttpRequest request = buildRequest("GET", uri, new HashMap<>()); + int responseCode = testing.runWithSpan("parent", () -> sendRequest(request, uri)); + + // then + assertThat(responseCode).isEqualTo(200); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasKind(INTERNAL).hasNoParent(), + span -> { + span.hasName("CONNECT").hasKind(INTERNAL).hasParent(trace.getSpan(0)); + span.hasAttributesSatisfyingExactly( + equalTo(NetworkAttributes.NETWORK_TRANSPORT, "tcp"), + equalTo(NetworkAttributes.NETWORK_TYPE, "ipv4"), + equalTo(ServerAttributes.SERVER_ADDRESS, uri.getHost()), + equalTo(ServerAttributes.SERVER_PORT, uri.getPort()), + equalTo(NetworkAttributes.NETWORK_PEER_PORT, uri.getPort()), + equalTo(NetworkAttributes.NETWORK_PEER_ADDRESS, "127.0.0.1")); + }, + span -> span.hasName("GET").hasKind(CLIENT).hasParent(trace.getSpan(0)), + span -> + span.hasName("test-http-server").hasKind(SERVER).hasParent(trace.getSpan(2)))); + } + + @Test + void failedRequest() throws Exception { + // when + URI uri = URI.create("http://localhost:" + PortUtils.UNUSABLE_PORT); + + DefaultFullHttpRequest request = buildRequest("GET", uri, new HashMap<>()); + Throwable thrown = + catchThrowable(() -> testing.runWithSpan("parent", () -> sendRequest(request, uri))); + + // then + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasKind(INTERNAL).hasNoParent().hasException(thrown), + span -> { + span.hasName("CONNECT").hasKind(INTERNAL).hasParent(trace.getSpan(0)); + span.hasAttributesSatisfying(equalTo(NetworkAttributes.NETWORK_TRANSPORT, "tcp")); + satisfies( + NetworkAttributes.NETWORK_TYPE, + val -> + val.satisfiesAnyOf( + v -> assertThat(val).isNull(), v -> assertThat(v).isEqualTo("ipv4"))); + span.hasAttributesSatisfying( + equalTo(ServerAttributes.SERVER_ADDRESS, uri.getHost()), + equalTo(ServerAttributes.SERVER_PORT, uri.getPort())); + satisfies( + NetworkAttributes.NETWORK_PEER_PORT, + val -> + val.satisfiesAnyOf( + v -> assertThat(val).isNull(), + v -> assertThat(v).isEqualTo(uri.getPort()))); + satisfies( + NetworkAttributes.NETWORK_PEER_ADDRESS, + val -> + val.satisfiesAnyOf( + v -> assertThat(val).isNull(), + v -> assertThat(v).isEqualTo("127.0.0.1"))); + })); + } + + private static DefaultFullHttpRequest buildRequest( + String method, URI uri, Map headers) { + DefaultFullHttpRequest request = + new DefaultFullHttpRequest( + HttpVersion.HTTP_1_1, HttpMethod.valueOf(method), uri.getPath(), Unpooled.EMPTY_BUFFER); + HttpHeaders.setHost(request, uri.getHost() + ":" + uri.getPort()); + headers.forEach((k, v) -> request.headers().set(k, v)); + return request; + } + + private static int sendRequest(DefaultFullHttpRequest request, URI uri) + throws InterruptedException, ExecutionException, TimeoutException { + Channel channel = bootstrap.connect(uri.getHost(), uri.getPort()).sync().channel(); + CompletableFuture result = new CompletableFuture(); + channel.pipeline().addLast(new ClientHandler(result)); + channel.writeAndFlush(request).get(); + return result.get(20, TimeUnit.SECONDS); + } +} diff --git a/instrumentation/netty/netty-4.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/netty/v4_0/server/Netty40ServerTest.java b/instrumentation/netty/netty-4.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/netty/v4_0/server/Netty40ServerTest.java new file mode 100644 index 000000000000..17cc99260974 --- /dev/null +++ b/instrumentation/netty/netty-4.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/netty/v4_0/server/Netty40ServerTest.java @@ -0,0 +1,222 @@ +/* + * 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.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.testing.junit.http.ServerEndpoint.CAPTURE_HEADERS; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.ERROR; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.EXCEPTION; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.INDEXED_CHILD; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.NOT_FOUND; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.QUERY_PARAM; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.REDIRECT; +import static io.opentelemetry.instrumentation.testing.junit.http.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.SocketChannel; +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.api.common.AttributeKey; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpServerTest; +import io.opentelemetry.instrumentation.testing.junit.http.HttpServerInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.http.HttpServerTestOptions; +import io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint; +import io.opentelemetry.semconv.HttpAttributes; +import java.net.URI; +import java.util.HashSet; +import java.util.Set; +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.extension.RegisterExtension; + +class Netty40ServerTest extends AbstractHttpServerTest { + + @RegisterExtension + static final InstrumentationExtension testing = HttpServerInstrumentationExtension.forAgent(); + + private static final LoggingHandler LOGGING_HANDLER = + new LoggingHandler(Netty40ServerTest.class, LogLevel.DEBUG); + + @Override + protected EventLoopGroup setupServer() throws InterruptedException { + EventLoopGroup eventLoopGroup = new NioEventLoopGroup(); + + ServerBootstrap serverBootstrap = + new ServerBootstrap() + .group(eventLoopGroup) + .handler(LOGGING_HANDLER) + .childHandler( + new ChannelInitializer() { + @Override + protected void initChannel(@NotNull SocketChannel socketChannel) + throws Exception { + ChannelPipeline pipeline = socketChannel.pipeline(); + pipeline.addFirst("logger", LOGGING_HANDLER); + pipeline.addLast(new HttpRequestDecoder()); + pipeline.addLast(new HttpResponseEncoder()); + pipeline.addLast( + new SimpleChannelInboundHandler() { + + @Override + protected void channelRead0(ChannelHandlerContext ctx, Object msg) + throws Exception { + if (!(msg instanceof HttpRequest)) { + return; + } + HttpRequest request = (HttpRequest) msg; + URI uri = URI.create(request.getUri()); + ServerEndpoint endpoint = ServerEndpoint.forPath(uri.getPath()); + ctx.write( + controller( + endpoint, + () -> { + ByteBuf content; + FullHttpResponse response; + if (endpoint.equals(SUCCESS) || endpoint.equals(ERROR)) { + content = + Unpooled.copiedBuffer( + endpoint.getBody(), CharsetUtil.UTF_8); + response = + new DefaultFullHttpResponse( + HTTP_1_1, + HttpResponseStatus.valueOf(endpoint.getStatus()), + content); + } else if (endpoint.equals(INDEXED_CHILD)) { + content = Unpooled.EMPTY_BUFFER; + endpoint.collectSpanAttributes( + it -> + new QueryStringDecoder(uri) + .parameters() + .get(it) + .get(0)); + response = + new DefaultFullHttpResponse( + HTTP_1_1, + HttpResponseStatus.valueOf(endpoint.getStatus()), + content); + } else if (endpoint.equals(QUERY_PARAM)) { + content = + Unpooled.copiedBuffer( + uri.getQuery(), CharsetUtil.UTF_8); + response = + new DefaultFullHttpResponse( + HTTP_1_1, + HttpResponseStatus.valueOf(endpoint.getStatus()), + content); + } else if (endpoint.equals(REDIRECT)) { + content = Unpooled.EMPTY_BUFFER; + response = + new DefaultFullHttpResponse( + HTTP_1_1, + HttpResponseStatus.valueOf(endpoint.getStatus()), + content); + response + .headers() + .set(HttpHeaders.Names.LOCATION, endpoint.getBody()); + } else if (endpoint.equals(CAPTURE_HEADERS)) { + content = + Unpooled.copiedBuffer( + endpoint.getBody(), CharsetUtil.UTF_8); + response = + new DefaultFullHttpResponse( + HTTP_1_1, + HttpResponseStatus.valueOf(endpoint.getStatus()), + content); + response + .headers() + .set( + "X-Test-Response", + request.headers().get("X-Test-Request")); + } else if (endpoint.equals(EXCEPTION)) { + throw new IllegalArgumentException(endpoint.getBody()); + } else { + content = + Unpooled.copiedBuffer( + NOT_FOUND.getBody(), CharsetUtil.UTF_8); + response = + new DefaultFullHttpResponse( + HTTP_1_1, + HttpResponseStatus.valueOf(NOT_FOUND.getStatus()), + content); + } + + response.headers().set(CONTENT_TYPE, "text/plain"); + response + .headers() + .set(CONTENT_LENGTH, content.readableBytes()); + return response; + })); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable ex) + throws Exception { + ByteBuf content = + Unpooled.copiedBuffer(ex.getMessage(), 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); + } + + @Override + public void channelReadComplete(ChannelHandlerContext ctx) + throws Exception { + ctx.flush(); + } + }); + } + }) + .channel(NioServerSocketChannel.class); + serverBootstrap.bind(port).sync(); + + return eventLoopGroup; + } + + @Override + protected void stopServer(EventLoopGroup server) { + if (server != null) { + server.shutdownGracefully(); + } + } + + @Override + protected void configure(HttpServerTestOptions options) { + options.setHttpAttributes( + serverEndpoint -> { + Set> attributes = + new HashSet<>(HttpServerTestOptions.DEFAULT_HTTP_ATTRIBUTES); + attributes.remove(HttpAttributes.HTTP_ROUTE); + return attributes; + }); + + options.setExpectedException(new IllegalArgumentException(EXCEPTION.getBody())); + options.setHasResponseCustomizer(serverEndpoint -> true); + } +} diff --git a/instrumentation/netty/netty-4.1/javaagent/build.gradle.kts b/instrumentation/netty/netty-4.1/javaagent/build.gradle.kts index 7d4f80ba35cf..050eea01a4c3 100644 --- a/instrumentation/netty/netty-4.1/javaagent/build.gradle.kts +++ b/instrumentation/netty/netty-4.1/javaagent/build.gradle.kts @@ -49,6 +49,7 @@ tasks { includeTestsMatching("Netty41ClientSslTest") } include("**/Netty41ConnectionSpanTest.*", "**/Netty41ClientSslTest.*") + jvmArgs("-Dotel.instrumentation.netty.connection-telemetry.enabled=true") jvmArgs("-Dotel.instrumentation.netty.ssl-telemetry.enabled=true") } @@ -73,7 +74,9 @@ if (!(findProperty("testLatestDeps") as Boolean)) { configurations.configureEach { if (!name.contains("muzzle")) { resolutionStrategy.eachDependency { - if (requested.group == "io.netty" && requested.name != "netty-bom" && !requested.name.startsWith("netty-transport-native")) { + if (requested.group == "io.netty" && requested.name != "netty-bom" && + !requested.name.startsWith("netty-transport-native") && + !requested.name.startsWith("netty-transport-classes")) { useVersion("4.1.0.Final") } } diff --git a/instrumentation/netty/netty-4.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4_1/AbstractChannelHandlerContextInstrumentation.java b/instrumentation/netty/netty-4.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4_1/AbstractChannelHandlerContextInstrumentation.java index 4944daacf3c4..474701939f06 100644 --- a/instrumentation/netty/netty-4.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4_1/AbstractChannelHandlerContextInstrumentation.java +++ b/instrumentation/netty/netty-4.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4_1/AbstractChannelHandlerContextInstrumentation.java @@ -17,9 +17,10 @@ import io.opentelemetry.instrumentation.netty.common.internal.NettyErrorHolder; import io.opentelemetry.instrumentation.netty.v4.common.HttpRequestAndChannel; import io.opentelemetry.instrumentation.netty.v4_1.internal.AttributeKeys; +import io.opentelemetry.instrumentation.netty.v4_1.internal.ServerContext; +import io.opentelemetry.instrumentation.netty.v4_1.internal.ServerContexts; import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; -import java.util.Deque; import net.bytebuddy.asm.Advice; import net.bytebuddy.description.type.TypeDescription; import net.bytebuddy.matcher.ElementMatcher; @@ -60,10 +61,9 @@ public static void onEnter( instrumenter().end(clientContext, request, null, throwable); return; } - Deque contexts = ctx.channel().attr(AttributeKeys.SERVER_CONTEXT).get(); - Context serverContext = contexts != null ? contexts.peekFirst() : null; + ServerContext serverContext = ServerContexts.peekFirst(ctx.channel()); if (serverContext != null) { - NettyErrorHolder.set(serverContext, throwable); + NettyErrorHolder.set(serverContext.context(), throwable); } } } diff --git a/instrumentation/netty/netty-4.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4_1/BootstrapInstrumentation.java b/instrumentation/netty/netty-4.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4_1/BootstrapInstrumentation.java index aa91de4d1bb3..2801c847eb5a 100644 --- a/instrumentation/netty/netty-4.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4_1/BootstrapInstrumentation.java +++ b/instrumentation/netty/netty-4.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4_1/BootstrapInstrumentation.java @@ -16,14 +16,14 @@ import io.netty.resolver.AddressResolverGroup; import io.netty.resolver.DefaultAddressResolverGroup; import io.opentelemetry.context.Context; -import io.opentelemetry.context.Scope; import io.opentelemetry.instrumentation.netty.common.internal.NettyConnectionRequest; -import io.opentelemetry.instrumentation.netty.v4.common.internal.client.ConnectionCompleteListener; import io.opentelemetry.javaagent.bootstrap.Java8BytecodeBridge; import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.netty.v4.common.NettyScope; import java.net.SocketAddress; import net.bytebuddy.asm.Advice; +import net.bytebuddy.asm.Advice.AssignReturned.ToArguments.ToArgument; import net.bytebuddy.description.type.TypeDescription; import net.bytebuddy.matcher.ElementMatcher; @@ -66,50 +66,34 @@ public static void onExit(@Advice.This Bootstrap bootstrap) { public static class SetResolverAdvice { @Advice.OnMethodEnter(suppress = Throwable.class) - public static void onEnter( - @Advice.Argument(value = 0, readOnly = false) AddressResolverGroup resolver) { - resolver = InstrumentedAddressResolverGroup.wrap(connectionInstrumenter(), resolver); + @Advice.AssignReturned.ToArguments(@ToArgument(0)) + public static AddressResolverGroup onEnter( + @Advice.Argument(value = 0) AddressResolverGroup resolver) { + return InstrumentedAddressResolverGroup.wrap(connectionInstrumenter(), resolver); } } @SuppressWarnings("unused") public static class ConnectAdvice { @Advice.OnMethodEnter - public static void startConnect( - @Advice.Argument(0) SocketAddress remoteAddress, - @Advice.Local("otelContext") Context context, - @Advice.Local("otelRequest") NettyConnectionRequest request, - @Advice.Local("otelScope") Scope scope) { + public static NettyScope startConnect(@Advice.Argument(0) SocketAddress remoteAddress) { Context parentContext = Java8BytecodeBridge.currentContext(); - request = NettyConnectionRequest.connect(remoteAddress); + NettyConnectionRequest request = NettyConnectionRequest.connect(remoteAddress); if (!connectionInstrumenter().shouldStart(parentContext, request)) { - return; + return null; } - context = connectionInstrumenter().start(parentContext, request); - scope = context.makeCurrent(); + return NettyScope.startNew(connectionInstrumenter(), parentContext, request); } @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) public static void endConnect( @Advice.Thrown Throwable throwable, @Advice.Argument(2) ChannelPromise channelPromise, - @Advice.Local("otelContext") Context context, - @Advice.Local("otelRequest") NettyConnectionRequest request, - @Advice.Local("otelScope") Scope scope) { + @Advice.Enter NettyScope enterScope) { - if (scope == null) { - return; - } - scope.close(); - - if (throwable != null) { - connectionInstrumenter().end(context, request, null, throwable); - } else { - channelPromise.addListener( - new ConnectionCompleteListener(connectionInstrumenter(), context, request)); - } + NettyScope.end(enterScope, connectionInstrumenter(), channelPromise, throwable); } } } diff --git a/instrumentation/netty/netty-4.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4_1/NettyChannelPipelineInstrumentation.java b/instrumentation/netty/netty-4.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4_1/NettyChannelPipelineInstrumentation.java index e410a9a351af..9bdb10ef14c0 100644 --- a/instrumentation/netty/netty-4.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4_1/NettyChannelPipelineInstrumentation.java +++ b/instrumentation/netty/netty-4.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4_1/NettyChannelPipelineInstrumentation.java @@ -5,7 +5,9 @@ package io.opentelemetry.javaagent.instrumentation.netty.v4_1; +import static io.opentelemetry.javaagent.instrumentation.netty.v4_1.NettyClientSingletons.clientHandlerFactory; import static io.opentelemetry.javaagent.instrumentation.netty.v4_1.NettyClientSingletons.sslInstrumenter; +import static io.opentelemetry.javaagent.instrumentation.netty.v4_1.NettyServerSingletons.serverTelemetry; import static net.bytebuddy.matcher.ElementMatchers.isMethod; import static net.bytebuddy.matcher.ElementMatchers.nameStartsWith; import static net.bytebuddy.matcher.ElementMatchers.named; @@ -22,12 +24,6 @@ import io.netty.handler.codec.http.HttpServerCodec; import io.opentelemetry.instrumentation.api.util.VirtualField; import io.opentelemetry.instrumentation.netty.v4.common.internal.client.NettySslInstrumentationHandler; -import io.opentelemetry.instrumentation.netty.v4_1.internal.client.HttpClientRequestTracingHandler; -import io.opentelemetry.instrumentation.netty.v4_1.internal.client.HttpClientResponseTracingHandler; -import io.opentelemetry.instrumentation.netty.v4_1.internal.client.HttpClientTracingHandler; -import io.opentelemetry.instrumentation.netty.v4_1.internal.server.HttpServerRequestTracingHandler; -import io.opentelemetry.instrumentation.netty.v4_1.internal.server.HttpServerResponseTracingHandler; -import io.opentelemetry.instrumentation.netty.v4_1.internal.server.HttpServerTracingHandler; import io.opentelemetry.javaagent.bootstrap.CallDepth; import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; import io.opentelemetry.javaagent.instrumentation.netty.v4.common.AbstractNettyChannelPipelineInstrumentation; @@ -50,16 +46,13 @@ public void transform(TypeTransformer transformer) { /** * 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. + * handlers. If those handlers are later removed, we also remove our handlers. */ @SuppressWarnings("unused") public static class ChannelPipelineAddAdvice { @Advice.OnMethodEnter - public static void trackCallDepth( - @Advice.Argument(2) ChannelHandler handler, - @Advice.Local("otelCallDepth") CallDepth callDepth) { + public static CallDepth 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 @@ -69,8 +62,9 @@ public static void trackCallDepth( // 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. - callDepth = CallDepth.forClass(handler.getClass()); + CallDepth callDepth = CallDepth.forClass(handler.getClass()); callDepth.getAndIncrement(); + return callDepth; } @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) @@ -78,7 +72,8 @@ public static void addHandler( @Advice.This ChannelPipeline pipeline, @Advice.Argument(1) String handlerName, @Advice.Argument(2) ChannelHandler handler, - @Advice.Local("otelCallDepth") CallDepth callDepth) { + @Advice.Enter CallDepth callDepth) { + if (callDepth.decrementAndGet() > 0) { return; } @@ -106,23 +101,21 @@ public static void addHandler( // Server pipeline handlers if (handler instanceof HttpServerCodec) { ourHandler = - new HttpServerTracingHandler( - NettyServerSingletons.instrumenter(), - NettyHttpServerResponseBeforeCommitHandler.INSTANCE); + serverTelemetry() + .createCombinedHandler(NettyHttpServerResponseBeforeCommitHandler.INSTANCE); } else if (handler instanceof HttpRequestDecoder) { - ourHandler = new HttpServerRequestTracingHandler(NettyServerSingletons.instrumenter()); + ourHandler = serverTelemetry().createRequestHandler(); } else if (handler instanceof HttpResponseEncoder) { ourHandler = - new HttpServerResponseTracingHandler( - NettyServerSingletons.instrumenter(), - NettyHttpServerResponseBeforeCommitHandler.INSTANCE); + serverTelemetry() + .createCombinedHandler(NettyHttpServerResponseBeforeCommitHandler.INSTANCE); // Client pipeline handlers } else if (handler instanceof HttpClientCodec) { - ourHandler = new HttpClientTracingHandler(NettyClientSingletons.instrumenter()); + ourHandler = clientHandlerFactory().createCombinedHandler(); } else if (handler instanceof HttpRequestEncoder) { - ourHandler = new HttpClientRequestTracingHandler(NettyClientSingletons.instrumenter()); + ourHandler = clientHandlerFactory().createRequestHandler(); } else if (handler instanceof HttpResponseDecoder) { - ourHandler = new HttpClientResponseTracingHandler(NettyClientSingletons.instrumenter()); + ourHandler = clientHandlerFactory().createResponseHandler(); // the SslHandler lives in the netty-handler module, using class name comparison to avoid // adding a dependency } else if (handler.getClass().getName().equals("io.netty.handler.ssl.SslHandler")) { diff --git a/instrumentation/netty/netty-4.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4_1/NettyClientSingletons.java b/instrumentation/netty/netty-4.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4_1/NettyClientSingletons.java index 98d7dc07575b..72886c37ce3d 100644 --- a/instrumentation/netty/netty-4.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4_1/NettyClientSingletons.java +++ b/instrumentation/netty/netty-4.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4_1/NettyClientSingletons.java @@ -5,6 +5,8 @@ package io.opentelemetry.javaagent.instrumentation.netty.v4_1; +import static io.opentelemetry.instrumentation.netty.v4.common.internal.client.NettyConnectionInstrumentationFlag.enabledOrErrorOnly; + import io.netty.handler.codec.http.HttpResponse; import io.opentelemetry.api.GlobalOpenTelemetry; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; @@ -12,47 +14,51 @@ import io.opentelemetry.instrumentation.netty.v4.common.internal.client.NettyClientInstrumenterFactory; import io.opentelemetry.instrumentation.netty.v4.common.internal.client.NettyConnectionInstrumenter; import io.opentelemetry.instrumentation.netty.v4.common.internal.client.NettySslInstrumenter; -import io.opentelemetry.javaagent.bootstrap.internal.CommonConfig; -import io.opentelemetry.javaagent.bootstrap.internal.DeprecatedConfigProperties; -import io.opentelemetry.javaagent.bootstrap.internal.InstrumentationConfig; +import io.opentelemetry.instrumentation.netty.v4_1.internal.client.NettyClientHandlerFactory; +import io.opentelemetry.javaagent.bootstrap.internal.AgentCommonConfig; +import io.opentelemetry.javaagent.bootstrap.internal.AgentInstrumentationConfig; import java.util.Collections; +import java.util.function.Function; public final class NettyClientSingletons { - private static final boolean connectionTelemetryEnabled; - private static final boolean sslTelemetryEnabled; - - static { - InstrumentationConfig config = InstrumentationConfig.get(); - connectionTelemetryEnabled = - DeprecatedConfigProperties.getBoolean( - config, - "otel.instrumentation.netty.always-create-connect-span", - "otel.instrumentation.netty.connection-telemetry.enabled", - false); - sslTelemetryEnabled = - config.getBoolean("otel.instrumentation.netty.ssl-telemetry.enabled", false); - } + private static final boolean connectionTelemetryEnabled = + AgentInstrumentationConfig.get() + .getBoolean("otel.instrumentation.netty.connection-telemetry.enabled", false); + private static final boolean sslTelemetryEnabled = + AgentInstrumentationConfig.get() + .getBoolean("otel.instrumentation.netty.ssl-telemetry.enabled", false); private static final Instrumenter INSTRUMENTER; private static final NettyConnectionInstrumenter CONNECTION_INSTRUMENTER; private static final NettySslInstrumenter SSL_INSTRUMENTER; + private static final NettyClientHandlerFactory CLIENT_HANDLER_FACTORY; static { NettyClientInstrumenterFactory factory = new NettyClientInstrumenterFactory( GlobalOpenTelemetry.get(), "io.opentelemetry.netty-4.1", - connectionTelemetryEnabled, - sslTelemetryEnabled, - CommonConfig.get().getPeerServiceMapping()); + enabledOrErrorOnly(connectionTelemetryEnabled), + enabledOrErrorOnly(sslTelemetryEnabled), + AgentCommonConfig.get().getPeerServiceResolver(), + AgentCommonConfig.get().shouldEmitExperimentalHttpClientTelemetry()); INSTRUMENTER = factory.createHttpInstrumenter( - CommonConfig.get().getClientRequestHeaders(), - CommonConfig.get().getClientResponseHeaders(), + builder -> + builder + .setCapturedRequestHeaders(AgentCommonConfig.get().getClientRequestHeaders()) + .setCapturedResponseHeaders(AgentCommonConfig.get().getClientResponseHeaders()) + .setKnownMethods(AgentCommonConfig.get().getKnownHttpRequestMethods()), + builder -> + builder.setKnownMethods(AgentCommonConfig.get().getKnownHttpRequestMethods()), + Function.identity(), Collections.emptyList()); CONNECTION_INSTRUMENTER = factory.createConnectionInstrumenter(); SSL_INSTRUMENTER = factory.createSslInstrumenter(); + CLIENT_HANDLER_FACTORY = + new NettyClientHandlerFactory( + INSTRUMENTER, AgentCommonConfig.get().shouldEmitExperimentalHttpClientTelemetry()); } public static Instrumenter instrumenter() { @@ -67,5 +73,9 @@ public static NettySslInstrumenter sslInstrumenter() { return SSL_INSTRUMENTER; } + public static NettyClientHandlerFactory clientHandlerFactory() { + return CLIENT_HANDLER_FACTORY; + } + private NettyClientSingletons() {} } diff --git a/instrumentation/netty/netty-4.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4_1/NettyInstrumentationModule.java b/instrumentation/netty/netty-4.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4_1/NettyInstrumentationModule.java index 604e51b0578e..9d9c71505726 100644 --- a/instrumentation/netty/netty-4.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4_1/NettyInstrumentationModule.java +++ b/instrumentation/netty/netty-4.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4_1/NettyInstrumentationModule.java @@ -11,12 +11,14 @@ 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.internal.ExperimentalInstrumentationModule; import io.opentelemetry.javaagent.instrumentation.netty.v4.common.NettyFutureInstrumentation; import java.util.List; import net.bytebuddy.matcher.ElementMatcher; @AutoService(InstrumentationModule.class) -public class NettyInstrumentationModule extends InstrumentationModule { +public class NettyInstrumentationModule extends InstrumentationModule + implements ExperimentalInstrumentationModule { public NettyInstrumentationModule() { super("netty", "netty-4.1"); } @@ -28,12 +30,18 @@ public ElementMatcher.Junction classLoaderMatcher() { return hasClassesNamed("io.netty.handler.codec.http.CombinedHttpHeaders"); } + @Override + public String getModuleGroup() { + return "netty"; + } + @Override public List typeInstrumentations() { return asList( new BootstrapInstrumentation(), new NettyFutureInstrumentation(), new NettyChannelPipelineInstrumentation(), - new AbstractChannelHandlerContextInstrumentation()); + new AbstractChannelHandlerContextInstrumentation(), + new SingleThreadEventExecutorInstrumentation()); } } diff --git a/instrumentation/netty/netty-4.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4_1/NettyServerSingletons.java b/instrumentation/netty/netty-4.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4_1/NettyServerSingletons.java index c4239f6b0d40..6203880260a8 100644 --- a/instrumentation/netty/netty-4.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4_1/NettyServerSingletons.java +++ b/instrumentation/netty/netty-4.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4_1/NettyServerSingletons.java @@ -5,24 +5,29 @@ package io.opentelemetry.javaagent.instrumentation.netty.v4_1; -import io.netty.handler.codec.http.HttpResponse; import io.opentelemetry.api.GlobalOpenTelemetry; -import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; -import io.opentelemetry.instrumentation.netty.v4.common.HttpRequestAndChannel; -import io.opentelemetry.instrumentation.netty.v4.common.internal.server.NettyServerInstrumenterFactory; -import io.opentelemetry.javaagent.bootstrap.internal.CommonConfig; +import io.opentelemetry.instrumentation.netty.v4_1.NettyServerTelemetry; +import io.opentelemetry.javaagent.bootstrap.internal.AgentCommonConfig; public final class NettyServerSingletons { - private static final Instrumenter INSTRUMENTER = - NettyServerInstrumenterFactory.create( - GlobalOpenTelemetry.get(), - "io.opentelemetry.netty-4.1", - CommonConfig.get().getServerRequestHeaders(), - CommonConfig.get().getServerResponseHeaders()); + static { + SERVER_TELEMETRY = + NettyServerTelemetry.builder(GlobalOpenTelemetry.get()) + .setEmitExperimentalHttpServerEvents( + AgentCommonConfig.get().shouldEmitExperimentalHttpServerTelemetry()) + .setEmitExperimentalHttpServerMetrics( + AgentCommonConfig.get().shouldEmitExperimentalHttpServerTelemetry()) + .setKnownMethods(AgentCommonConfig.get().getKnownHttpRequestMethods()) + .setCapturedRequestHeaders(AgentCommonConfig.get().getServerRequestHeaders()) + .setCapturedResponseHeaders(AgentCommonConfig.get().getServerResponseHeaders()) + .build(); + } + + private static final NettyServerTelemetry SERVER_TELEMETRY; - public static Instrumenter instrumenter() { - return INSTRUMENTER; + public static NettyServerTelemetry serverTelemetry() { + return SERVER_TELEMETRY; } private NettyServerSingletons() {} diff --git a/instrumentation/netty/netty-4.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4_1/SingleThreadEventExecutorInstrumentation.java b/instrumentation/netty/netty-4.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4_1/SingleThreadEventExecutorInstrumentation.java new file mode 100644 index 000000000000..38a34ad4d23a --- /dev/null +++ b/instrumentation/netty/netty-4.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/netty/v4_1/SingleThreadEventExecutorInstrumentation.java @@ -0,0 +1,51 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.netty.v4_1; + +import static net.bytebuddy.matcher.ElementMatchers.named; + +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.bootstrap.Java8BytecodeBridge; +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 SingleThreadEventExecutorInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher typeMatcher() { + return named("io.netty.util.concurrent.SingleThreadEventExecutor"); + } + + @Override + public void transform(TypeTransformer transformer) { + // this method submits a task that runs for forever to an executor, propagating context there + // would result in a context leak + transformer.applyAdviceToMethod( + named("startThread"), this.getClass().getName() + "$DisablePropagationAdvice"); + } + + @SuppressWarnings("unused") + public static class DisablePropagationAdvice { + @Advice.OnMethodEnter(suppress = Throwable.class) + public static Scope onEnter() { + if (Java8BytecodeBridge.currentContext() != Java8BytecodeBridge.rootContext()) { + // Prevent context from leaking by running this method under root context. + // Root context is not propagated by executor instrumentation. + return Java8BytecodeBridge.rootContext().makeCurrent(); + } + return null; + } + + @Advice.OnMethodExit(suppress = Throwable.class) + public static void onExit(@Advice.Enter Scope scope) { + if (scope != null) { + scope.close(); + } + } + } +} diff --git a/instrumentation/netty/netty-4.1/javaagent/src/test/groovy/ChannelPipelineTest.groovy b/instrumentation/netty/netty-4.1/javaagent/src/test/groovy/ChannelPipelineTest.groovy index 6d0d79060eca..6093df1187c5 100644 --- a/instrumentation/netty/netty-4.1/javaagent/src/test/groovy/ChannelPipelineTest.groovy +++ b/instrumentation/netty/netty-4.1/javaagent/src/test/groovy/ChannelPipelineTest.groovy @@ -68,7 +68,7 @@ class ChannelPipelineTest extends AgentInstrumentationSpecification { replaceMethod(channelPipeline, "test", noopHandler, "http", httpHandler) then: "noop handler was removed; http and instrumentation handlers were added" - channelPipeline.size() == 2 + channelPipeline.size() == 1 channelPipeline.first() == httpHandler channelPipeline.last().getClass().simpleName == "HttpClientTracingHandler" @@ -101,7 +101,7 @@ class ChannelPipelineTest extends AgentInstrumentationSpecification { channelPipeline.addLast("http", httpHandler) then: "add http and instrumentation handlers" - channelPipeline.size() == 2 + channelPipeline.size() == 1 channelPipeline.first() == httpHandler channelPipeline.last().getClass().simpleName == "HttpClientTracingHandler" @@ -110,7 +110,7 @@ class ChannelPipelineTest extends AgentInstrumentationSpecification { channelPipeline.addAfter("http", "noop", noopHandler) then: "instrumentation handler is between with http and noop" - channelPipeline.size() == 3 + channelPipeline.size() == 2 channelPipeline.first() == httpHandler channelPipeline.last() == noopHandler @@ -118,15 +118,44 @@ class ChannelPipelineTest extends AgentInstrumentationSpecification { channelPipeline.removeLast() then: "http and instrumentation handlers will be remained" - channelPipeline.size() == 2 + channelPipeline.size() == 1 channelPipeline.first() == httpHandler channelPipeline.last().getClass().simpleName == "HttpClientTracingHandler" when: - channelPipeline.removeLast() + def removed = channelPipeline.removeLast() then: "there is no handler in pipeline" channelPipeline.size() == 0 + // removing tracing handler also removes the http handler and returns it + removed == httpHandler + } + + // regression test for https://github.com/open-telemetry/opentelemetry-java-instrumentation/issues/10377 + def "our handler not in handlers map"() { + 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().simpleName == "HttpClientTracingHandler" + // our handler not counted + channelPipeline.size() == 1 + // our handler is not in handlers map + channelPipeline.toMap().size() == 1 + // our handler is not in handlers iterator + def list = [] + channelPipeline.iterator().forEachRemaining {list.add(it) } + list.size() == 1 } private static class NoopChannelHandler extends ChannelHandlerAdapter { diff --git a/instrumentation/netty/netty-4.1/javaagent/src/test/groovy/Netty41ClientSslTest.groovy b/instrumentation/netty/netty-4.1/javaagent/src/test/groovy/Netty41ClientSslTest.groovy index b536281ba0da..5cd08cefdcc4 100644 --- a/instrumentation/netty/netty-4.1/javaagent/src/test/groovy/Netty41ClientSslTest.groovy +++ b/instrumentation/netty/netty-4.1/javaagent/src/test/groovy/Netty41ClientSslTest.groovy @@ -23,7 +23,8 @@ import io.netty.handler.ssl.SslHandler import io.opentelemetry.instrumentation.netty.v4_1.ClientHandler import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification import io.opentelemetry.instrumentation.testing.junit.http.HttpClientTestServer -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import io.opentelemetry.semconv.ServerAttributes +import io.opentelemetry.semconv.NetworkAttributes import spock.lang.Shared import javax.net.ssl.SSLEngine @@ -36,7 +37,6 @@ 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.api.trace.StatusCode.ERROR -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.NetTransportValues.IP_TCP class Netty41ClientSslTest extends AgentInstrumentationSpecification { @@ -94,9 +94,8 @@ class Netty41ClientSslTest extends AgentInstrumentationSpecification { kind INTERNAL childOf span(0) attributes { - "$SemanticAttributes.NET_TRANSPORT" IP_TCP - "$SemanticAttributes.NET_PEER_NAME" uri.host - "$SemanticAttributes.NET_PEER_PORT" uri.port + "$ServerAttributes.SERVER_ADDRESS" uri.host + "$ServerAttributes.SERVER_PORT" uri.port } } span(2) { @@ -104,10 +103,12 @@ class Netty41ClientSslTest extends AgentInstrumentationSpecification { kind INTERNAL childOf span(0) attributes { - "$SemanticAttributes.NET_TRANSPORT" IP_TCP - "$SemanticAttributes.NET_PEER_NAME" uri.host - "$SemanticAttributes.NET_PEER_PORT" uri.port - "$SemanticAttributes.NET_SOCK_PEER_ADDR" { it == "127.0.0.1" || it == null } + "$NetworkAttributes.NETWORK_TRANSPORT" "tcp" + "$NetworkAttributes.NETWORK_TYPE" "ipv4" + "$ServerAttributes.SERVER_ADDRESS" uri.host + "$ServerAttributes.SERVER_PORT" uri.port + "$NetworkAttributes.NETWORK_PEER_PORT" uri.port + "$NetworkAttributes.NETWORK_PEER_ADDRESS" "127.0.0.1" } } span(3) { @@ -118,10 +119,10 @@ class Netty41ClientSslTest extends AgentInstrumentationSpecification { // netty swallows the exception, it doesn't make any sense to hard-code the message errorEventWithAnyMessage(SSLHandshakeException) attributes { - "$SemanticAttributes.NET_TRANSPORT" IP_TCP - "$SemanticAttributes.NET_SOCK_PEER_ADDR" { it == "127.0.0.1" || it == null } - "$SemanticAttributes.NET_SOCK_PEER_NAME" uri.host - "$SemanticAttributes.NET_SOCK_PEER_PORT" uri.port + "$NetworkAttributes.NETWORK_TRANSPORT" "tcp" + "$NetworkAttributes.NETWORK_TYPE" "ipv4" + "$NetworkAttributes.NETWORK_PEER_PORT" uri.port + "$NetworkAttributes.NETWORK_PEER_ADDRESS" "127.0.0.1" } } } @@ -162,9 +163,8 @@ class Netty41ClientSslTest extends AgentInstrumentationSpecification { kind INTERNAL childOf span(0) attributes { - "$SemanticAttributes.NET_TRANSPORT" IP_TCP - "$SemanticAttributes.NET_PEER_NAME" uri.host - "$SemanticAttributes.NET_PEER_PORT" uri.port + "$ServerAttributes.SERVER_ADDRESS" uri.host + "$ServerAttributes.SERVER_PORT" uri.port } } span(2) { @@ -172,10 +172,12 @@ class Netty41ClientSslTest extends AgentInstrumentationSpecification { kind INTERNAL childOf span(0) attributes { - "$SemanticAttributes.NET_TRANSPORT" IP_TCP - "$SemanticAttributes.NET_PEER_NAME" uri.host - "$SemanticAttributes.NET_PEER_PORT" uri.port - "$SemanticAttributes.NET_SOCK_PEER_ADDR" { it == "127.0.0.1" || it == null } + "$NetworkAttributes.NETWORK_TRANSPORT" "tcp" + "$NetworkAttributes.NETWORK_TYPE" "ipv4" + "$ServerAttributes.SERVER_ADDRESS" uri.host + "$ServerAttributes.SERVER_PORT" uri.port + "$NetworkAttributes.NETWORK_PEER_PORT" uri.port + "$NetworkAttributes.NETWORK_PEER_ADDRESS" "127.0.0.1" } } span(3) { @@ -183,10 +185,10 @@ class Netty41ClientSslTest extends AgentInstrumentationSpecification { kind INTERNAL childOf span(0) attributes { - "$SemanticAttributes.NET_TRANSPORT" IP_TCP - "$SemanticAttributes.NET_SOCK_PEER_ADDR" { it == "127.0.0.1" || it == null } - "$SemanticAttributes.NET_SOCK_PEER_NAME" uri.host - "$SemanticAttributes.NET_SOCK_PEER_PORT" uri.port + "$NetworkAttributes.NETWORK_TRANSPORT" "tcp" + "$NetworkAttributes.NETWORK_TYPE" "ipv4" + "$NetworkAttributes.NETWORK_PEER_PORT" uri.port + "$NetworkAttributes.NETWORK_PEER_ADDRESS" "127.0.0.1" } } span(4) { diff --git a/instrumentation/netty/netty-4.1/javaagent/src/test/groovy/Netty41ConnectionSpanTest.groovy b/instrumentation/netty/netty-4.1/javaagent/src/test/groovy/Netty41ConnectionSpanTest.groovy index 056474b02b6c..c938b053c84b 100644 --- a/instrumentation/netty/netty-4.1/javaagent/src/test/groovy/Netty41ConnectionSpanTest.groovy +++ b/instrumentation/netty/netty-4.1/javaagent/src/test/groovy/Netty41ConnectionSpanTest.groovy @@ -21,7 +21,8 @@ import io.opentelemetry.instrumentation.test.AgentTestTrait import io.opentelemetry.instrumentation.test.InstrumentationSpecification import io.opentelemetry.instrumentation.test.utils.PortUtils import io.opentelemetry.instrumentation.testing.junit.http.HttpClientTestServer -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import io.opentelemetry.semconv.ServerAttributes +import io.opentelemetry.semconv.NetworkAttributes import spock.lang.Shared import java.util.concurrent.CompletableFuture @@ -31,7 +32,6 @@ 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.api.trace.StatusCode.ERROR -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.NetTransportValues.IP_TCP class Netty41ConnectionSpanTest extends InstrumentationSpecification implements AgentTestTrait { @@ -108,9 +108,8 @@ class Netty41ConnectionSpanTest extends InstrumentationSpecification implements kind INTERNAL childOf span(0) attributes { - "$SemanticAttributes.NET_TRANSPORT" IP_TCP - "$SemanticAttributes.NET_PEER_NAME" uri.host - "$SemanticAttributes.NET_PEER_PORT" uri.port + "$ServerAttributes.SERVER_ADDRESS" uri.host + "$ServerAttributes.SERVER_PORT" uri.port } } span(2) { @@ -118,10 +117,12 @@ class Netty41ConnectionSpanTest extends InstrumentationSpecification implements kind INTERNAL childOf(span(0)) attributes { - "$SemanticAttributes.NET_TRANSPORT" IP_TCP - "$SemanticAttributes.NET_PEER_NAME" uri.host - "$SemanticAttributes.NET_PEER_PORT" uri.port - "$SemanticAttributes.NET_SOCK_PEER_ADDR" "127.0.0.1" + "$NetworkAttributes.NETWORK_TRANSPORT" "tcp" + "$NetworkAttributes.NETWORK_TYPE" "ipv4" + "$ServerAttributes.SERVER_ADDRESS" uri.host + "$ServerAttributes.SERVER_PORT" uri.port + "$NetworkAttributes.NETWORK_PEER_PORT" uri.port + "$NetworkAttributes.NETWORK_PEER_ADDRESS" "127.0.0.1" } } span(3) { @@ -166,9 +167,8 @@ class Netty41ConnectionSpanTest extends InstrumentationSpecification implements kind INTERNAL childOf span(0) attributes { - "$SemanticAttributes.NET_TRANSPORT" IP_TCP - "$SemanticAttributes.NET_PEER_NAME" uri.host - "$SemanticAttributes.NET_PEER_PORT" uri.port + "$ServerAttributes.SERVER_ADDRESS" uri.host + "$ServerAttributes.SERVER_PORT" uri.port } } span(2) { @@ -178,10 +178,12 @@ class Netty41ConnectionSpanTest extends InstrumentationSpecification implements status ERROR errorEvent(thrownException.class, thrownException.message) attributes { - "$SemanticAttributes.NET_TRANSPORT" IP_TCP - "$SemanticAttributes.NET_PEER_NAME" uri.host - "$SemanticAttributes.NET_PEER_PORT" uri.port - "$SemanticAttributes.NET_SOCK_PEER_ADDR" { it == "127.0.0.1" || it == null } + "$NetworkAttributes.NETWORK_TRANSPORT" "tcp" + "$NetworkAttributes.NETWORK_TYPE" { it == "ipv4" || it == null } + "$ServerAttributes.SERVER_ADDRESS" uri.host + "$ServerAttributes.SERVER_PORT" uri.port + "$NetworkAttributes.NETWORK_PEER_ADDRESS" { it == "127.0.0.1" || it == null } + "$NetworkAttributes.NETWORK_PEER_PORT" { it == uri.port || it == null } } } } diff --git a/instrumentation/netty/netty-4.1/library/build.gradle.kts b/instrumentation/netty/netty-4.1/library/build.gradle.kts index eb20fd9db5a8..08f8adf1da09 100644 --- a/instrumentation/netty/netty-4.1/library/build.gradle.kts +++ b/instrumentation/netty/netty-4.1/library/build.gradle.kts @@ -7,5 +7,8 @@ dependencies { implementation(project(":instrumentation:netty:netty-4-common:library")) implementation(project(":instrumentation:netty:netty-common:library")) + compileOnly("com.google.auto.value:auto-value-annotations") + annotationProcessor("com.google.auto.value:auto-value") + testImplementation(project(":instrumentation:netty:netty-4.1:testing")) } diff --git a/instrumentation/netty/netty-4.1/library/src/main/java/io/opentelemetry/instrumentation/netty/v4_1/NettyClientTelemetry.java b/instrumentation/netty/netty-4.1/library/src/main/java/io/opentelemetry/instrumentation/netty/v4_1/NettyClientTelemetry.java index fcd8abc9583b..c4f2face6c72 100644 --- a/instrumentation/netty/netty-4.1/library/src/main/java/io/opentelemetry/instrumentation/netty/v4_1/NettyClientTelemetry.java +++ b/instrumentation/netty/netty-4.1/library/src/main/java/io/opentelemetry/instrumentation/netty/v4_1/NettyClientTelemetry.java @@ -15,17 +15,18 @@ import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; import io.opentelemetry.instrumentation.netty.v4.common.HttpRequestAndChannel; import io.opentelemetry.instrumentation.netty.v4_1.internal.AttributeKeys; -import io.opentelemetry.instrumentation.netty.v4_1.internal.client.HttpClientRequestTracingHandler; -import io.opentelemetry.instrumentation.netty.v4_1.internal.client.HttpClientResponseTracingHandler; -import io.opentelemetry.instrumentation.netty.v4_1.internal.client.HttpClientTracingHandler; +import io.opentelemetry.instrumentation.netty.v4_1.internal.client.NettyClientHandlerFactory; /** Entrypoint for instrumenting Netty HTTP clients. */ public final class NettyClientTelemetry { - private final Instrumenter instrumenter; + private final NettyClientHandlerFactory handlerFactory; - NettyClientTelemetry(Instrumenter instrumenter) { - this.instrumenter = instrumenter; + NettyClientTelemetry( + Instrumenter instrumenter, + boolean emitExperimentalHttpClientEvents) { + this.handlerFactory = + new NettyClientHandlerFactory(instrumenter, emitExperimentalHttpClientEvents); } /** Returns a new {@link NettyClientTelemetry} configured with the given {@link OpenTelemetry}. */ @@ -42,11 +43,11 @@ public static NettyClientTelemetryBuilder builder(OpenTelemetry openTelemetry) { } /** - * /** Returns a new {@link ChannelOutboundHandlerAdapter} that generates telemetry for outgoing - * HTTP requests. Must be paired with {@link #createResponseHandler()}. + * Returns a new {@link ChannelOutboundHandlerAdapter} that generates telemetry for outgoing HTTP + * requests. Must be paired with {@link #createResponseHandler()}. */ public ChannelOutboundHandlerAdapter createRequestHandler() { - return new HttpClientRequestTracingHandler(instrumenter); + return handlerFactory.createRequestHandler(); } /** @@ -54,7 +55,7 @@ public ChannelOutboundHandlerAdapter createRequestHandler() { * responses. Must be paired with {@link #createRequestHandler()}. */ public ChannelInboundHandlerAdapter createResponseHandler() { - return new HttpClientResponseTracingHandler(instrumenter); + return handlerFactory.createResponseHandler(); } /** @@ -64,7 +65,7 @@ public ChannelInboundHandlerAdapter createResponseHandler() { public CombinedChannelDuplexHandler< ? extends ChannelInboundHandlerAdapter, ? extends ChannelOutboundHandlerAdapter> createCombinedHandler() { - return new HttpClientTracingHandler(instrumenter); + return handlerFactory.createCombinedHandler(); } /** diff --git a/instrumentation/netty/netty-4.1/library/src/main/java/io/opentelemetry/instrumentation/netty/v4_1/NettyClientTelemetryBuilder.java b/instrumentation/netty/netty-4.1/library/src/main/java/io/opentelemetry/instrumentation/netty/v4_1/NettyClientTelemetryBuilder.java index bf3a9080dd01..5d32493bc33f 100644 --- a/instrumentation/netty/netty-4.1/library/src/main/java/io/opentelemetry/instrumentation/netty/v4_1/NettyClientTelemetryBuilder.java +++ b/instrumentation/netty/netty-4.1/library/src/main/java/io/opentelemetry/instrumentation/netty/v4_1/NettyClientTelemetryBuilder.java @@ -8,26 +8,51 @@ import com.google.errorprone.annotations.CanIgnoreReturnValue; import io.netty.handler.codec.http.HttpResponse; import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.instrumentation.api.incubator.semconv.net.PeerServiceResolver; import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor; +import io.opentelemetry.instrumentation.api.semconv.http.HttpClientAttributesExtractorBuilder; +import io.opentelemetry.instrumentation.api.semconv.http.HttpSpanNameExtractorBuilder; import io.opentelemetry.instrumentation.netty.v4.common.HttpRequestAndChannel; import io.opentelemetry.instrumentation.netty.v4.common.internal.client.NettyClientInstrumenterFactory; +import io.opentelemetry.instrumentation.netty.v4.common.internal.client.NettyConnectionInstrumentationFlag; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Set; +import java.util.function.Consumer; +import java.util.function.Function; /** A builder of {@link NettyClientTelemetry}. */ public final class NettyClientTelemetryBuilder { private final OpenTelemetry openTelemetry; - private List capturedRequestHeaders = Collections.emptyList(); - private List capturedResponseHeaders = Collections.emptyList(); private final List> additionalAttributesExtractors = new ArrayList<>(); + private Consumer> + extractorConfigurer = builder -> {}; + private Consumer> + spanNameExtractorConfigurer = builder -> {}; + private Function< + SpanNameExtractor, + ? extends SpanNameExtractor> + spanNameExtractorTransformer = Function.identity(); + + private boolean emitExperimentalHttpClientMetrics = false; + private boolean emitExperimentalHttpClientEvents = false; + NettyClientTelemetryBuilder(OpenTelemetry openTelemetry) { this.openTelemetry = openTelemetry; } + @CanIgnoreReturnValue + public NettyClientTelemetryBuilder setEmitExperimentalHttpClientEvents( + boolean emitExperimentalHttpClientEvents) { + this.emitExperimentalHttpClientEvents = emitExperimentalHttpClientEvents; + return this; + } + /** * Configures the HTTP request headers that will be captured as span attributes. * @@ -36,7 +61,9 @@ public final class NettyClientTelemetryBuilder { @CanIgnoreReturnValue public NettyClientTelemetryBuilder setCapturedRequestHeaders( List capturedRequestHeaders) { - this.capturedRequestHeaders = capturedRequestHeaders; + extractorConfigurer = + extractorConfigurer.andThen( + builder -> builder.setCapturedRequestHeaders(capturedRequestHeaders)); return this; } @@ -48,7 +75,9 @@ public NettyClientTelemetryBuilder setCapturedRequestHeaders( @CanIgnoreReturnValue public NettyClientTelemetryBuilder setCapturedResponseHeaders( List capturedResponseHeaders) { - this.capturedResponseHeaders = capturedResponseHeaders; + extractorConfigurer = + extractorConfigurer.andThen( + builder -> builder.setCapturedResponseHeaders(capturedResponseHeaders)); return this; } @@ -63,12 +92,67 @@ public NettyClientTelemetryBuilder addAttributesExtractor( return this; } + /** + * Configures the instrumentation to recognize an alternative set of HTTP request methods. + * + *

    By default, this instrumentation defines "known" methods as the ones listed in RFC9110 and the PATCH + * method defined in RFC5789. + * + *

    Note: calling this method overrides the default known method sets completely; it does + * not supplement it. + * + * @param knownMethods A set of recognized HTTP request methods. + * @see HttpClientAttributesExtractorBuilder#setKnownMethods(Set) + */ + @CanIgnoreReturnValue + public NettyClientTelemetryBuilder setKnownMethods(Set knownMethods) { + extractorConfigurer = + extractorConfigurer.andThen(builder -> builder.setKnownMethods(knownMethods)); + spanNameExtractorConfigurer = + spanNameExtractorConfigurer.andThen(builder -> builder.setKnownMethods(knownMethods)); + return this; + } + + /** + * Configures the instrumentation to emit experimental HTTP client metrics. + * + * @param emitExperimentalHttpClientMetrics {@code true} if the experimental HTTP client metrics + * are to be emitted. + */ + @CanIgnoreReturnValue + public NettyClientTelemetryBuilder setEmitExperimentalHttpClientMetrics( + boolean emitExperimentalHttpClientMetrics) { + this.emitExperimentalHttpClientMetrics = emitExperimentalHttpClientMetrics; + return this; + } + + /** Sets custom {@link SpanNameExtractor} via transform function. */ + @CanIgnoreReturnValue + public NettyClientTelemetryBuilder setSpanNameExtractor( + Function< + SpanNameExtractor, + ? extends SpanNameExtractor> + spanNameExtractorTransformer) { + this.spanNameExtractorTransformer = spanNameExtractorTransformer; + return this; + } + /** Returns a new {@link NettyClientTelemetry} with the given configuration. */ public NettyClientTelemetry build() { return new NettyClientTelemetry( new NettyClientInstrumenterFactory( - openTelemetry, "io.opentelemetry.netty-4.1", false, false, Collections.emptyMap()) + openTelemetry, + "io.opentelemetry.netty-4.1", + NettyConnectionInstrumentationFlag.DISABLED, + NettyConnectionInstrumentationFlag.DISABLED, + PeerServiceResolver.create(Collections.emptyMap()), + emitExperimentalHttpClientMetrics) .createHttpInstrumenter( - capturedRequestHeaders, capturedResponseHeaders, additionalAttributesExtractors)); + extractorConfigurer, + spanNameExtractorConfigurer, + spanNameExtractorTransformer, + additionalAttributesExtractors), + emitExperimentalHttpClientEvents); } } diff --git a/instrumentation/netty/netty-4.1/library/src/main/java/io/opentelemetry/instrumentation/netty/v4_1/NettyServerTelemetry.java b/instrumentation/netty/netty-4.1/library/src/main/java/io/opentelemetry/instrumentation/netty/v4_1/NettyServerTelemetry.java index 1160ff1ca410..65c4ac158be7 100644 --- a/instrumentation/netty/netty-4.1/library/src/main/java/io/opentelemetry/instrumentation/netty/v4_1/NettyServerTelemetry.java +++ b/instrumentation/netty/netty-4.1/library/src/main/java/io/opentelemetry/instrumentation/netty/v4_1/NettyServerTelemetry.java @@ -12,6 +12,7 @@ import io.opentelemetry.api.OpenTelemetry; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; import io.opentelemetry.instrumentation.netty.v4.common.HttpRequestAndChannel; +import io.opentelemetry.instrumentation.netty.v4_1.internal.ProtocolEventHandler; import io.opentelemetry.instrumentation.netty.v4_1.internal.server.HttpServerRequestTracingHandler; import io.opentelemetry.instrumentation.netty.v4_1.internal.server.HttpServerResponseBeforeCommitHandler; import io.opentelemetry.instrumentation.netty.v4_1.internal.server.HttpServerResponseTracingHandler; @@ -21,9 +22,13 @@ public final class NettyServerTelemetry { private final Instrumenter instrumenter; + private final ProtocolEventHandler protocolEventHandler; - NettyServerTelemetry(Instrumenter instrumenter) { + NettyServerTelemetry( + Instrumenter instrumenter, + ProtocolEventHandler protocolEventHandler) { this.instrumenter = instrumenter; + this.protocolEventHandler = protocolEventHandler; } /** Returns a new {@link NettyServerTelemetry} configured with the given {@link OpenTelemetry}. */ @@ -52,8 +57,12 @@ public ChannelInboundHandlerAdapter createRequestHandler() { * responses. Must be paired with {@link #createRequestHandler()}. */ public ChannelOutboundHandlerAdapter createResponseHandler() { - return new HttpServerResponseTracingHandler( - instrumenter, HttpServerResponseBeforeCommitHandler.Noop.INSTANCE); + return createResponseHandler(HttpServerResponseBeforeCommitHandler.Noop.INSTANCE); + } + + public ChannelOutboundHandlerAdapter createResponseHandler( + HttpServerResponseBeforeCommitHandler commitHandler) { + return new HttpServerResponseTracingHandler(instrumenter, commitHandler, protocolEventHandler); } /** @@ -63,7 +72,12 @@ public ChannelOutboundHandlerAdapter createResponseHandler() { public CombinedChannelDuplexHandler< ? extends ChannelInboundHandlerAdapter, ? extends ChannelOutboundHandlerAdapter> createCombinedHandler() { - return new HttpServerTracingHandler( - instrumenter, HttpServerResponseBeforeCommitHandler.Noop.INSTANCE); + return createCombinedHandler(HttpServerResponseBeforeCommitHandler.Noop.INSTANCE); + } + + public CombinedChannelDuplexHandler< + ? extends ChannelInboundHandlerAdapter, ? extends ChannelOutboundHandlerAdapter> + createCombinedHandler(HttpServerResponseBeforeCommitHandler commitHandler) { + return new HttpServerTracingHandler(instrumenter, commitHandler, protocolEventHandler); } } diff --git a/instrumentation/netty/netty-4.1/library/src/main/java/io/opentelemetry/instrumentation/netty/v4_1/NettyServerTelemetryBuilder.java b/instrumentation/netty/netty-4.1/library/src/main/java/io/opentelemetry/instrumentation/netty/v4_1/NettyServerTelemetryBuilder.java index 9e802cfae9c2..6a2d417515b2 100644 --- a/instrumentation/netty/netty-4.1/library/src/main/java/io/opentelemetry/instrumentation/netty/v4_1/NettyServerTelemetryBuilder.java +++ b/instrumentation/netty/netty-4.1/library/src/main/java/io/opentelemetry/instrumentation/netty/v4_1/NettyServerTelemetryBuilder.java @@ -6,22 +6,48 @@ package io.opentelemetry.instrumentation.netty.v4_1; import com.google.errorprone.annotations.CanIgnoreReturnValue; +import io.netty.handler.codec.http.HttpResponse; import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerAttributesExtractorBuilder; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRouteBuilder; +import io.opentelemetry.instrumentation.api.semconv.http.HttpSpanNameExtractorBuilder; +import io.opentelemetry.instrumentation.netty.v4.common.HttpRequestAndChannel; import io.opentelemetry.instrumentation.netty.v4.common.internal.server.NettyServerInstrumenterFactory; -import java.util.Collections; +import io.opentelemetry.instrumentation.netty.v4_1.internal.ProtocolEventHandler; import java.util.List; +import java.util.Set; +import java.util.function.Consumer; /** A builder of {@link NettyServerTelemetry}. */ public final class NettyServerTelemetryBuilder { private final OpenTelemetry openTelemetry; - private List capturedRequestHeaders = Collections.emptyList(); - private List capturedResponseHeaders = Collections.emptyList(); + + private Consumer> + extractorConfigurer = builder -> {}; + private Consumer> + spanNameExtractorConfigurer = builder -> {}; + private Consumer> httpServerRouteConfigurer = + builder -> {}; + private boolean emitExperimentalHttpServerMetrics = false; + private boolean emitExperimentalHttpServerEvents = false; NettyServerTelemetryBuilder(OpenTelemetry openTelemetry) { this.openTelemetry = openTelemetry; } + /** + * Configures emission of experimental events. + * + * @param emitExperimentalHttpServerEvents set to true to emit events + */ + @CanIgnoreReturnValue + public NettyServerTelemetryBuilder setEmitExperimentalHttpServerEvents( + boolean emitExperimentalHttpServerEvents) { + this.emitExperimentalHttpServerEvents = emitExperimentalHttpServerEvents; + return this; + } + /** * Configures the HTTP request headers that will be captured as span attributes. * @@ -30,7 +56,9 @@ public final class NettyServerTelemetryBuilder { @CanIgnoreReturnValue public NettyServerTelemetryBuilder setCapturedRequestHeaders( List capturedRequestHeaders) { - this.capturedRequestHeaders = capturedRequestHeaders; + extractorConfigurer = + extractorConfigurer.andThen( + builder -> builder.setCapturedRequestHeaders(capturedRequestHeaders)); return this; } @@ -42,7 +70,46 @@ public NettyServerTelemetryBuilder setCapturedRequestHeaders( @CanIgnoreReturnValue public NettyServerTelemetryBuilder setCapturedResponseHeaders( List capturedResponseHeaders) { - this.capturedResponseHeaders = capturedResponseHeaders; + extractorConfigurer = + extractorConfigurer.andThen( + builder -> builder.setCapturedResponseHeaders(capturedResponseHeaders)); + return this; + } + + /** + * Configures the instrumentation to recognize an alternative set of HTTP request methods. + * + *

    By default, this instrumentation defines "known" methods as the ones listed in RFC9110 and the PATCH + * method defined in RFC5789. + * + *

    Note: calling this method overrides the default known method sets completely; it does + * not supplement it. + * + * @param knownMethods A set of recognized HTTP request methods. + * @see HttpServerAttributesExtractorBuilder#setKnownMethods(Set) + */ + @CanIgnoreReturnValue + public NettyServerTelemetryBuilder setKnownMethods(Set knownMethods) { + extractorConfigurer = + extractorConfigurer.andThen(builder -> builder.setKnownMethods(knownMethods)); + spanNameExtractorConfigurer = + spanNameExtractorConfigurer.andThen(builder -> builder.setKnownMethods(knownMethods)); + httpServerRouteConfigurer = + httpServerRouteConfigurer.andThen(builder -> builder.setKnownMethods(knownMethods)); + return this; + } + + /** + * Configures the instrumentation to emit experimental HTTP server metrics. + * + * @param emitExperimentalHttpServerMetrics {@code true} if the experimental HTTP server metrics + * are to be emitted. + */ + @CanIgnoreReturnValue + public NettyServerTelemetryBuilder setEmitExperimentalHttpServerMetrics( + boolean emitExperimentalHttpServerMetrics) { + this.emitExperimentalHttpServerMetrics = emitExperimentalHttpServerMetrics; return this; } @@ -52,7 +119,12 @@ public NettyServerTelemetry build() { NettyServerInstrumenterFactory.create( openTelemetry, "io.opentelemetry.netty-4.1", - capturedRequestHeaders, - capturedResponseHeaders)); + extractorConfigurer, + spanNameExtractorConfigurer, + httpServerRouteConfigurer, + emitExperimentalHttpServerMetrics), + emitExperimentalHttpServerEvents + ? ProtocolEventHandler.Enabled.INSTANCE + : ProtocolEventHandler.Noop.INSTANCE); } } diff --git a/instrumentation/netty/netty-4.1/library/src/main/java/io/opentelemetry/instrumentation/netty/v4_1/internal/AttributeKeys.java b/instrumentation/netty/netty-4.1/library/src/main/java/io/opentelemetry/instrumentation/netty/v4_1/internal/AttributeKeys.java index 2cbb44573b8b..ea9947441b89 100644 --- a/instrumentation/netty/netty-4.1/library/src/main/java/io/opentelemetry/instrumentation/netty/v4_1/internal/AttributeKeys.java +++ b/instrumentation/netty/netty-4.1/library/src/main/java/io/opentelemetry/instrumentation/netty/v4_1/internal/AttributeKeys.java @@ -7,7 +7,6 @@ import io.netty.util.AttributeKey; import io.opentelemetry.context.Context; -import java.util.Deque; /** * This class is internal and is hence not for public use. Its APIs are unstable and can change at @@ -17,9 +16,9 @@ public final class AttributeKeys { // this is the context that has the server span // - // note: this attribute key is also used by ratpack instrumentation - public static final AttributeKey> SERVER_CONTEXT = - AttributeKey.valueOf(AttributeKeys.class, "server-context"); + // note: this attribute key is also used by finagle instrumentation + public static final AttributeKey SERVER_CONTEXTS = + AttributeKey.valueOf(AttributeKeys.class, "server-contexts"); public static final AttributeKey CLIENT_CONTEXT = AttributeKey.valueOf(AttributeKeys.class, "client-context"); diff --git a/instrumentation/netty/netty-4.1/library/src/main/java/io/opentelemetry/instrumentation/netty/v4_1/internal/ProtocolEventHandler.java b/instrumentation/netty/netty-4.1/library/src/main/java/io/opentelemetry/instrumentation/netty/v4_1/internal/ProtocolEventHandler.java new file mode 100644 index 000000000000..96fc191172c2 --- /dev/null +++ b/instrumentation/netty/netty-4.1/library/src/main/java/io/opentelemetry/instrumentation/netty/v4_1/internal/ProtocolEventHandler.java @@ -0,0 +1,45 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.netty.v4_1.internal; + +import io.netty.handler.codec.http.HttpRequest; +import io.netty.handler.codec.http.HttpResponse; +import io.opentelemetry.context.Context; + +/** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time. + */ +public interface ProtocolEventHandler { + void handle( + ProtocolSpecificEvent event, Context context, HttpRequest request, HttpResponse response); + + /** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time. + */ + enum Noop implements ProtocolEventHandler { + INSTANCE; + + @Override + public void handle( + ProtocolSpecificEvent event, Context context, HttpRequest request, HttpResponse response) {} + } + + /** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time. + */ + enum Enabled implements ProtocolEventHandler { + INSTANCE; + + @Override + public void handle( + ProtocolSpecificEvent event, Context context, HttpRequest request, HttpResponse response) { + event.addEvent(context, request, response); + } + } +} diff --git a/instrumentation/netty/netty-4.1/library/src/main/java/io/opentelemetry/instrumentation/netty/v4_1/internal/ProtocolSpecificEvent.java b/instrumentation/netty/netty-4.1/library/src/main/java/io/opentelemetry/instrumentation/netty/v4_1/internal/ProtocolSpecificEvent.java new file mode 100644 index 000000000000..4af896c0597d --- /dev/null +++ b/instrumentation/netty/netty-4.1/library/src/main/java/io/opentelemetry/instrumentation/netty/v4_1/internal/ProtocolSpecificEvent.java @@ -0,0 +1,67 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.netty.v4_1.internal; + +import static io.opentelemetry.api.common.AttributeKey.stringArrayKey; +import static io.opentelemetry.api.common.AttributeKey.stringKey; + +import io.netty.handler.codec.http.HttpRequest; +import io.netty.handler.codec.http.HttpResponse; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Context; +import java.util.List; +import javax.annotation.Nullable; + +/** + * Adds events to {@link Span}s for the enumerated protocols and situations + * + *

    This class is internal and is hence not for public use. Its APIs are unstable and can change + * at any time. + */ +public enum ProtocolSpecificEvent { + /** + * The event after which point the server or client transmits or receives, respectively, in one of + * the signified upgraded protocols, per protocol + * upgrade mechanism. + */ + SWITCHING_PROTOCOLS("http.response.status_code.101.upgrade") { + + @Override + void addEvent(Context context, HttpRequest request, HttpResponse response) { + Span.fromContext(context) + .addEvent( + eventName(), + Attributes.of( + SWITCHING_PROTOCOLS_FROM_KEY, + request != null ? request.protocolVersion().text() : "unknown", + // pulls out all possible values emitted by upgrade header, per: + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Upgrade + SWITCHING_PROTOCOLS_TO_KEY, + response.headers().getAll("upgrade"))); + } + }; + + public static final AttributeKey SWITCHING_PROTOCOLS_FROM_KEY = + stringKey("network.protocol.from"); + public static final AttributeKey> SWITCHING_PROTOCOLS_TO_KEY = + stringArrayKey("network.protocol.to"); + + private final String eventName; + + ProtocolSpecificEvent(String eventName) { + this.eventName = eventName; + } + + public String eventName() { + return eventName; + } + + abstract void addEvent( + Context context, @Nullable HttpRequest request, @Nullable HttpResponse response); +} diff --git a/instrumentation/netty/netty-4.1/library/src/main/java/io/opentelemetry/instrumentation/netty/v4_1/internal/ServerContext.java b/instrumentation/netty/netty-4.1/library/src/main/java/io/opentelemetry/instrumentation/netty/v4_1/internal/ServerContext.java new file mode 100644 index 000000000000..0bbbd7ec3767 --- /dev/null +++ b/instrumentation/netty/netty-4.1/library/src/main/java/io/opentelemetry/instrumentation/netty/v4_1/internal/ServerContext.java @@ -0,0 +1,31 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.netty.v4_1.internal; + +import com.google.auto.value.AutoValue; +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.netty.v4.common.HttpRequestAndChannel; + +/** + * A tuple of an {@link Context} and a {@link HttpRequestAndChannel}. + * + *

    This class is internal and is hence not for public use. Its APIs are unstable and can change + * at any time. + */ +@AutoValue +public abstract class ServerContext { + + /** Create a new {@link ServerContext}. */ + public static ServerContext create(Context context, HttpRequestAndChannel request) { + return new AutoValue_ServerContext(context, request); + } + + /** Returns the {@link Context}. */ + public abstract Context context(); + + /** Returns the {@link HttpRequestAndChannel}. */ + public abstract HttpRequestAndChannel request(); +} diff --git a/instrumentation/netty/netty-4.1/library/src/main/java/io/opentelemetry/instrumentation/netty/v4_1/internal/ServerContexts.java b/instrumentation/netty/netty-4.1/library/src/main/java/io/opentelemetry/instrumentation/netty/v4_1/internal/ServerContexts.java new file mode 100644 index 000000000000..34697a006200 --- /dev/null +++ b/instrumentation/netty/netty-4.1/library/src/main/java/io/opentelemetry/instrumentation/netty/v4_1/internal/ServerContexts.java @@ -0,0 +1,79 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.netty.v4_1.internal; + +import io.netty.channel.Channel; +import io.netty.util.Attribute; +import java.util.ArrayDeque; +import java.util.Deque; + +/** + * A helper class for keeping track of incoming requests and spans associated with them. + * + *

    This class is internal and is hence not for public use. Its APIs are unstable and can change + * at any time. + */ +public final class ServerContexts { + private static final int PIPELINING_LIMIT = 1000; + // With http pipelining multiple requests can be sent on the same connection. Responses should be + // sent in the same order the requests came in. We use this deque to store the request context + // and pop elements as responses are sent. + private final Deque serverContexts = new ArrayDeque<>(); + private volatile boolean broken = false; + + private ServerContexts() {} + + public static ServerContexts get(Channel channel) { + return channel.attr(AttributeKeys.SERVER_CONTEXTS).get(); + } + + public static ServerContexts getOrCreate(Channel channel) { + Attribute attribute = channel.attr(AttributeKeys.SERVER_CONTEXTS); + ServerContexts result = attribute.get(); + if (result == null) { + result = new ServerContexts(); + attribute.set(result); + } + return result; + } + + public static ServerContext peekFirst(Channel channel) { + ServerContexts serverContexts = get(channel); + return serverContexts != null ? serverContexts.peekFirst() : null; + } + + public ServerContext peekFirst() { + return serverContexts.peekFirst(); + } + + public ServerContext peekLast() { + return serverContexts.peekFirst(); + } + + public ServerContext pollFirst() { + return serverContexts.pollFirst(); + } + + public ServerContext pollLast() { + return serverContexts.pollLast(); + } + + public void addLast(ServerContext context) { + if (broken) { + return; + } + // If the pipelining limit is exceeded we'll stop tracing and mark the channel as broken. + // Exceeding the limit indicates that there is good chance that server context are not removed + // from the deque and there could be a memory leak. This could happen when http server decides + // not to send response to some requests, for example see + // https://github.com/open-telemetry/opentelemetry-java-instrumentation/issues/11942 + if (serverContexts.size() > PIPELINING_LIMIT) { + broken = true; + serverContexts.clear(); + } + serverContexts.addLast(context); + } +} diff --git a/instrumentation/netty/netty-4.1/library/src/main/java/io/opentelemetry/instrumentation/netty/v4_1/internal/client/HttpClientResponseTracingHandler.java b/instrumentation/netty/netty-4.1/library/src/main/java/io/opentelemetry/instrumentation/netty/v4_1/internal/client/HttpClientResponseTracingHandler.java index b57ed99f6839..c82b2bb61b8e 100644 --- a/instrumentation/netty/netty-4.1/library/src/main/java/io/opentelemetry/instrumentation/netty/v4_1/internal/client/HttpClientResponseTracingHandler.java +++ b/instrumentation/netty/netty-4.1/library/src/main/java/io/opentelemetry/instrumentation/netty/v4_1/internal/client/HttpClientResponseTracingHandler.java @@ -11,6 +11,7 @@ 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.HttpResponseStatus; import io.netty.handler.codec.http.LastHttpContent; import io.netty.util.Attribute; import io.netty.util.AttributeKey; @@ -19,6 +20,8 @@ import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; import io.opentelemetry.instrumentation.netty.v4.common.HttpRequestAndChannel; import io.opentelemetry.instrumentation.netty.v4_1.internal.AttributeKeys; +import io.opentelemetry.instrumentation.netty.v4_1.internal.ProtocolEventHandler; +import io.opentelemetry.instrumentation.netty.v4_1.internal.ProtocolSpecificEvent; /** * This class is internal and is hence not for public use. Its APIs are unstable and can change at @@ -30,10 +33,13 @@ public class HttpClientResponseTracingHandler extends ChannelInboundHandlerAdapt AttributeKey.valueOf(HttpClientResponseTracingHandler.class, "http-client-response"); private final Instrumenter instrumenter; + private final ProtocolEventHandler protocolEventHandler; public HttpClientResponseTracingHandler( - Instrumenter instrumenter) { + Instrumenter instrumenter, + ProtocolEventHandler protocolEventHandler) { this.instrumenter = instrumenter; + this.protocolEventHandler = protocolEventHandler; } @Override @@ -49,21 +55,48 @@ public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception Context parentContext = parentContextAttr.get(); if (msg instanceof FullHttpResponse) { - HttpRequestAndChannel request = ctx.channel().attr(HTTP_CLIENT_REQUEST).getAndSet(null); - instrumenter.end(context, request, (HttpResponse) msg, null); - contextAttr.set(null); - parentContextAttr.set(null); + FullHttpResponse response = (FullHttpResponse) msg; + if (response.status().equals(HttpResponseStatus.SWITCHING_PROTOCOLS)) { + HttpRequestAndChannel request = ctx.channel().attr(HTTP_CLIENT_REQUEST).get(); + protocolEventHandler.handle( + ProtocolSpecificEvent.SWITCHING_PROTOCOLS, + context, + request != null ? request.request() : null, + response); + } else { + HttpRequestAndChannel request = ctx.channel().attr(HTTP_CLIENT_REQUEST).getAndSet(null); + instrumenter.end(context, request, (HttpResponse) msg, null); + contextAttr.set(null); + parentContextAttr.set(null); + } } else if (msg instanceof HttpResponse) { + HttpResponse response = (HttpResponse) msg; + if (response.status().equals(HttpResponseStatus.SWITCHING_PROTOCOLS)) { + HttpRequestAndChannel request = ctx.channel().attr(HTTP_CLIENT_REQUEST).get(); + protocolEventHandler.handle( + ProtocolSpecificEvent.SWITCHING_PROTOCOLS, + context, + request != null ? request.request() : null, + response); + } + // HTTP 101 proto switch note: netty sends EmptyLastHttpContent upon proto upgrade; + // setting this here ensures we can see in the next if-block (LastHttpContent) whether + // the latest http status was indeed 101 or something else. + // Headers before body have been received, store them to use when finishing the span. ctx.channel().attr(HTTP_CLIENT_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. - HttpRequestAndChannel request = ctx.channel().attr(HTTP_CLIENT_REQUEST).getAndSet(null); - HttpResponse response = ctx.channel().attr(HTTP_CLIENT_RESPONSE).getAndSet(null); - instrumenter.end(context, request, response, null); - contextAttr.set(null); - parentContextAttr.set(null); + HttpResponse responseTest = ctx.channel().attr(HTTP_CLIENT_RESPONSE).get(); + if (responseTest == null + || !responseTest.status().equals(HttpResponseStatus.SWITCHING_PROTOCOLS)) { + // Not a FullHttpResponse so this is content that has been received after headers. + // Finish the span using what we stored in attrs. + HttpRequestAndChannel request = ctx.channel().attr(HTTP_CLIENT_REQUEST).getAndSet(null); + HttpResponse response = ctx.channel().attr(HTTP_CLIENT_RESPONSE).getAndSet(null); + instrumenter.end(context, request, response, null); + contextAttr.set(null); + parentContextAttr.set(null); + } } // We want the callback in the scope of the parent, not the client span diff --git a/instrumentation/netty/netty-4.1/library/src/main/java/io/opentelemetry/instrumentation/netty/v4_1/internal/client/HttpClientTracingHandler.java b/instrumentation/netty/netty-4.1/library/src/main/java/io/opentelemetry/instrumentation/netty/v4_1/internal/client/HttpClientTracingHandler.java index 98b10ebfcda6..a95bb9a4e6b9 100644 --- a/instrumentation/netty/netty-4.1/library/src/main/java/io/opentelemetry/instrumentation/netty/v4_1/internal/client/HttpClientTracingHandler.java +++ b/instrumentation/netty/netty-4.1/library/src/main/java/io/opentelemetry/instrumentation/netty/v4_1/internal/client/HttpClientTracingHandler.java @@ -17,6 +17,7 @@ import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; import io.opentelemetry.instrumentation.netty.v4.common.HttpRequestAndChannel; import io.opentelemetry.instrumentation.netty.v4_1.internal.AttributeKeys; +import io.opentelemetry.instrumentation.netty.v4_1.internal.ProtocolEventHandler; /** * This class is internal and is hence not for public use. Its APIs are unstable and can change at @@ -27,9 +28,11 @@ public class HttpClientTracingHandler HttpClientResponseTracingHandler, HttpClientRequestTracingHandler> { private final Instrumenter instrumenter; - public HttpClientTracingHandler(Instrumenter instrumenter) { + public HttpClientTracingHandler( + Instrumenter instrumenter, + ProtocolEventHandler protocolEventHandler) { super( - new HttpClientResponseTracingHandler(instrumenter), + new HttpClientResponseTracingHandler(instrumenter, protocolEventHandler), new HttpClientRequestTracingHandler(instrumenter)); this.instrumenter = instrumenter; } diff --git a/instrumentation/netty/netty-4.1/library/src/main/java/io/opentelemetry/instrumentation/netty/v4_1/internal/client/NettyClientHandlerFactory.java b/instrumentation/netty/netty-4.1/library/src/main/java/io/opentelemetry/instrumentation/netty/v4_1/internal/client/NettyClientHandlerFactory.java new file mode 100644 index 000000000000..6d86061a31b5 --- /dev/null +++ b/instrumentation/netty/netty-4.1/library/src/main/java/io/opentelemetry/instrumentation/netty/v4_1/internal/client/NettyClientHandlerFactory.java @@ -0,0 +1,60 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.netty.v4_1.internal.client; + +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.channel.ChannelOutboundHandlerAdapter; +import io.netty.channel.CombinedChannelDuplexHandler; +import io.netty.handler.codec.http.HttpResponse; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import io.opentelemetry.instrumentation.netty.v4.common.HttpRequestAndChannel; +import io.opentelemetry.instrumentation.netty.v4_1.internal.ProtocolEventHandler; + +/** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time. + */ +public class NettyClientHandlerFactory { + + private final Instrumenter instrumenter; + private final ProtocolEventHandler protocolEventHandler; + + public NettyClientHandlerFactory( + Instrumenter instrumenter, + boolean emitExperimentalHttpClientEvents) { + this.instrumenter = instrumenter; + this.protocolEventHandler = + emitExperimentalHttpClientEvents + ? ProtocolEventHandler.Enabled.INSTANCE + : ProtocolEventHandler.Noop.INSTANCE; + } + + /** + * Returns a new {@link ChannelOutboundHandlerAdapter} that generates telemetry for outgoing HTTP + * requests. Must be paired with {@link #createResponseHandler()}. + */ + public ChannelOutboundHandlerAdapter createRequestHandler() { + return new HttpClientRequestTracingHandler(instrumenter); + } + + /** + * Returns a new {@link ChannelInboundHandlerAdapter} that generates telemetry for incoming HTTP + * responses. Must be paired with {@link #createRequestHandler()}. + */ + public ChannelInboundHandlerAdapter createResponseHandler() { + return new HttpClientResponseTracingHandler(instrumenter, protocolEventHandler); + } + + /** + * Returns a new {@link CombinedChannelDuplexHandler} that generates telemetry for outgoing HTTP + * requests and incoming responses in a single handler. + */ + public CombinedChannelDuplexHandler< + ? extends ChannelInboundHandlerAdapter, ? extends ChannelOutboundHandlerAdapter> + createCombinedHandler() { + return new HttpClientTracingHandler(instrumenter, protocolEventHandler); + } +} diff --git a/instrumentation/netty/netty-4.1/library/src/main/java/io/opentelemetry/instrumentation/netty/v4_1/internal/server/HttpServerRequestTracingHandler.java b/instrumentation/netty/netty-4.1/library/src/main/java/io/opentelemetry/instrumentation/netty/v4_1/internal/server/HttpServerRequestTracingHandler.java index 28e81cd89c76..8f5b9a56fe84 100644 --- a/instrumentation/netty/netty-4.1/library/src/main/java/io/opentelemetry/instrumentation/netty/v4_1/internal/server/HttpServerRequestTracingHandler.java +++ b/instrumentation/netty/netty-4.1/library/src/main/java/io/opentelemetry/instrumentation/netty/v4_1/internal/server/HttpServerRequestTracingHandler.java @@ -10,15 +10,12 @@ import io.netty.channel.ChannelInboundHandlerAdapter; import io.netty.handler.codec.http.HttpRequest; import io.netty.handler.codec.http.HttpResponse; -import io.netty.util.Attribute; -import io.netty.util.AttributeKey; import io.opentelemetry.context.Context; import io.opentelemetry.context.Scope; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; import io.opentelemetry.instrumentation.netty.v4.common.HttpRequestAndChannel; -import io.opentelemetry.instrumentation.netty.v4_1.internal.AttributeKeys; -import java.util.ArrayDeque; -import java.util.Deque; +import io.opentelemetry.instrumentation.netty.v4_1.internal.ServerContext; +import io.opentelemetry.instrumentation.netty.v4_1.internal.ServerContexts; /** * This class is internal and is hence not for public use. Its APIs are unstable and can change at @@ -26,9 +23,6 @@ */ public class HttpServerRequestTracingHandler extends ChannelInboundHandlerAdapter { - static final AttributeKey> HTTP_SERVER_REQUEST = - AttributeKey.valueOf(HttpServerRequestTracingHandler.class, "http-server-request"); - private final Instrumenter instrumenter; public HttpServerRequestTracingHandler( @@ -39,14 +33,14 @@ public HttpServerRequestTracingHandler( @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { Channel channel = ctx.channel(); - Deque contexts = getOrCreate(channel, AttributeKeys.SERVER_CONTEXT); + ServerContexts serverContexts = ServerContexts.getOrCreate(channel); if (!(msg instanceof HttpRequest)) { - Context serverContext = contexts.peekLast(); + ServerContext serverContext = serverContexts.peekLast(); if (serverContext == null) { super.channelRead(ctx, msg); } else { - try (Scope ignored = serverContext.makeCurrent()) { + try (Scope ignored = serverContext.context().makeCurrent()) { super.channelRead(ctx, msg); } } @@ -61,27 +55,35 @@ public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception } Context context = instrumenter.start(parentContext, request); - contexts.addLast(context); - Deque requests = getOrCreate(channel, HTTP_SERVER_REQUEST); - requests.addLast(request); + serverContexts.addLast(ServerContext.create(context, request)); try (Scope ignored = context.makeCurrent()) { super.channelRead(ctx, msg); // the span is ended normally in HttpServerResponseTracingHandler } catch (Throwable throwable) { // make sure to remove the server context on end() call - instrumenter.end(contexts.removeLast(), requests.removeLast(), null, throwable); + ServerContext serverContext = serverContexts.pollLast(); + if (serverContext != null) { + instrumenter.end(serverContext.context(), serverContext.request(), null, throwable); + } throw throwable; } } - private static Deque getOrCreate(Channel channel, AttributeKey> key) { - Attribute> attribute = channel.attr(key); - Deque deque = attribute.get(); - if (deque == null) { - deque = new ArrayDeque<>(); - attribute.set(deque); + @Override + public void channelInactive(ChannelHandlerContext ctx) throws Exception { + // connection was closed, close all remaining requests + ServerContexts serverContexts = ServerContexts.get(ctx.channel()); + + if (serverContexts == null) { + super.channelInactive(ctx); + return; + } + + ServerContext serverContext; + while ((serverContext = serverContexts.pollFirst()) != null) { + instrumenter.end(serverContext.context(), serverContext.request(), null, null); } - return deque; + super.channelInactive(ctx); } } diff --git a/instrumentation/netty/netty-4.1/library/src/main/java/io/opentelemetry/instrumentation/netty/v4_1/internal/server/HttpServerResponseTracingHandler.java b/instrumentation/netty/netty-4.1/library/src/main/java/io/opentelemetry/instrumentation/netty/v4_1/internal/server/HttpServerResponseTracingHandler.java index 4fee50df8864..2c89e310c6e5 100644 --- a/instrumentation/netty/netty-4.1/library/src/main/java/io/opentelemetry/instrumentation/netty/v4_1/internal/server/HttpServerResponseTracingHandler.java +++ b/instrumentation/netty/netty-4.1/library/src/main/java/io/opentelemetry/instrumentation/netty/v4_1/internal/server/HttpServerResponseTracingHandler.java @@ -5,24 +5,24 @@ package io.opentelemetry.instrumentation.netty.v4_1.internal.server; -import static io.opentelemetry.instrumentation.netty.v4_1.internal.server.HttpServerRequestTracingHandler.HTTP_SERVER_REQUEST; - 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.HttpResponseStatus; 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.api.instrumenter.Instrumenter; import io.opentelemetry.instrumentation.netty.common.internal.NettyErrorHolder; import io.opentelemetry.instrumentation.netty.v4.common.HttpRequestAndChannel; -import io.opentelemetry.instrumentation.netty.v4_1.internal.AttributeKeys; -import java.util.Deque; +import io.opentelemetry.instrumentation.netty.v4_1.internal.ProtocolEventHandler; +import io.opentelemetry.instrumentation.netty.v4_1.internal.ProtocolSpecificEvent; +import io.opentelemetry.instrumentation.netty.v4_1.internal.ServerContext; +import io.opentelemetry.instrumentation.netty.v4_1.internal.ServerContexts; import javax.annotation.Nullable; /** @@ -36,27 +36,25 @@ public class HttpServerResponseTracingHandler extends ChannelOutboundHandlerAdap private final Instrumenter instrumenter; private final HttpServerResponseBeforeCommitHandler beforeCommitHandler; + private final ProtocolEventHandler eventHandler; public HttpServerResponseTracingHandler( Instrumenter instrumenter, - HttpServerResponseBeforeCommitHandler beforeCommitHandler) { + HttpServerResponseBeforeCommitHandler beforeCommitHandler, + ProtocolEventHandler eventHandler) { this.instrumenter = instrumenter; this.beforeCommitHandler = beforeCommitHandler; + this.eventHandler = eventHandler; } @Override public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise prm) throws Exception { - Attribute> contextAttr = ctx.channel().attr(AttributeKeys.SERVER_CONTEXT); - - Deque contexts = contextAttr.get(); - Context context = contexts != null ? contexts.peekFirst() : null; - if (context == null) { + ServerContexts serverContexts = ServerContexts.get(ctx.channel()); + ServerContext serverContext = serverContexts != null ? serverContexts.peekFirst() : null; + if (serverContext == null) { super.write(ctx, msg, prm); return; } - Attribute> requestAttr = ctx.channel().attr(HTTP_SERVER_REQUEST); - Deque requests = requestAttr.get(); - HttpRequestAndChannel request = requests != null ? requests.peekFirst() : null; ChannelPromise writePromise; @@ -72,36 +70,62 @@ public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise prm) thr // 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. - beforeCommitHandler.handle(context, (HttpResponse) msg); - contexts.removeFirst(); - requests.removeFirst(); - writePromise.addListener( - future -> end(context, request, (FullHttpResponse) msg, writePromise)); + FullHttpResponse response = (FullHttpResponse) msg; + if (response.status().equals(HttpResponseStatus.SWITCHING_PROTOCOLS)) { + eventHandler.handle( + ProtocolSpecificEvent.SWITCHING_PROTOCOLS, + serverContext.context(), + serverContext.request().request(), + response); + } else { + // Headers and body all sent together, we have the response information in the msg. + beforeCommitHandler.handle(serverContext.context(), (HttpResponse) msg); + serverContexts.pollFirst(); + writePromise.addListener( + future -> + end( + serverContext.context(), + serverContext.request(), + (FullHttpResponse) msg, + writePromise)); + } } 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). - contexts.removeFirst(); - requests.removeFirst(); - HttpResponse response = ctx.channel().attr(HTTP_SERVER_RESPONSE).getAndSet(null); - writePromise.addListener(future -> end(context, request, response, writePromise)); + HttpResponse responseTest = ctx.channel().attr(HTTP_SERVER_RESPONSE).get(); + if (responseTest == null + || !responseTest.status().equals(HttpResponseStatus.SWITCHING_PROTOCOLS)) { + // Body sent after headers. We stored the response information in the context when + // encountering HttpResponse (which was not FullHttpResponse since it's not + // LastHttpContent). + serverContexts.pollFirst(); + HttpResponse response = ctx.channel().attr(HTTP_SERVER_RESPONSE).getAndSet(null); + writePromise.addListener( + future -> + end(serverContext.context(), serverContext.request(), response, writePromise)); + } } } else { writePromise = prm; if (msg instanceof HttpResponse) { - // Headers before body has been sent, store them to use when finishing the span. - beforeCommitHandler.handle(context, (HttpResponse) msg); - ctx.channel().attr(HTTP_SERVER_RESPONSE).set((HttpResponse) msg); + HttpResponse response = (HttpResponse) msg; + if (response.status().equals(HttpResponseStatus.SWITCHING_PROTOCOLS)) { + eventHandler.handle( + ProtocolSpecificEvent.SWITCHING_PROTOCOLS, + serverContext.context(), + serverContext.request().request(), + response); + } else { + // Headers before body has been sent, store them to use when finishing the span. + beforeCommitHandler.handle(serverContext.context(), response); + ctx.channel().attr(HTTP_SERVER_RESPONSE).set(response); + } } } - try (Scope ignored = context.makeCurrent()) { + try (Scope ignored = serverContext.context().makeCurrent()) { super.write(ctx, msg, writePromise); } catch (Throwable throwable) { - contexts.removeFirst(); - requests.removeFirst(); - end(context, request, null, throwable); + serverContexts.pollFirst(); + end(serverContext.context(), serverContext.request(), null, throwable); throw throwable; } } diff --git a/instrumentation/netty/netty-4.1/library/src/main/java/io/opentelemetry/instrumentation/netty/v4_1/internal/server/HttpServerTracingHandler.java b/instrumentation/netty/netty-4.1/library/src/main/java/io/opentelemetry/instrumentation/netty/v4_1/internal/server/HttpServerTracingHandler.java index 0cf9484bafb6..458a26ffd2f8 100644 --- a/instrumentation/netty/netty-4.1/library/src/main/java/io/opentelemetry/instrumentation/netty/v4_1/internal/server/HttpServerTracingHandler.java +++ b/instrumentation/netty/netty-4.1/library/src/main/java/io/opentelemetry/instrumentation/netty/v4_1/internal/server/HttpServerTracingHandler.java @@ -9,6 +9,7 @@ import io.netty.handler.codec.http.HttpResponse; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; import io.opentelemetry.instrumentation.netty.v4.common.HttpRequestAndChannel; +import io.opentelemetry.instrumentation.netty.v4_1.internal.ProtocolEventHandler; /** * This class is internal and is hence not for public use. Its APIs are unstable and can change at @@ -20,9 +21,11 @@ public class HttpServerTracingHandler public HttpServerTracingHandler( Instrumenter instrumenter, - HttpServerResponseBeforeCommitHandler responseBeforeCommitHandler) { + HttpServerResponseBeforeCommitHandler responseBeforeCommitHandler, + ProtocolEventHandler protocolEventHandler) { super( new HttpServerRequestTracingHandler(instrumenter), - new HttpServerResponseTracingHandler(instrumenter, responseBeforeCommitHandler)); + new HttpServerResponseTracingHandler( + instrumenter, responseBeforeCommitHandler, protocolEventHandler)); } } diff --git a/instrumentation/netty/netty-4.1/testing/src/main/java/io/opentelemetry/instrumentation/netty/v4_1/AbstractNetty41ClientTest.java b/instrumentation/netty/netty-4.1/testing/src/main/java/io/opentelemetry/instrumentation/netty/v4_1/AbstractNetty41ClientTest.java index 69d9ba278d4e..cae60c300ecd 100644 --- a/instrumentation/netty/netty-4.1/testing/src/main/java/io/opentelemetry/instrumentation/netty/v4_1/AbstractNetty41ClientTest.java +++ b/instrumentation/netty/netty-4.1/testing/src/main/java/io/opentelemetry/instrumentation/netty/v4_1/AbstractNetty41ClientTest.java @@ -19,7 +19,7 @@ import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpClientTest; import io.opentelemetry.instrumentation.testing.junit.http.HttpClientResult; import io.opentelemetry.instrumentation.testing.junit.http.HttpClientTestOptions; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.opentelemetry.semconv.ServerAttributes; import java.net.URI; import java.util.Collections; import java.util.HashSet; @@ -112,6 +112,7 @@ protected void configure(HttpClientTestOptions.Builder optionsBuilder) { this::configureChannel)); optionsBuilder.disableTestRedirects(); + optionsBuilder.spanEndsAfterBody(); } private static Set> getHttpAttributes(URI uri) { @@ -121,8 +122,8 @@ private static Set> getHttpAttributes(URI uri) { return Collections.emptySet(); } Set> attributes = new HashSet<>(HttpClientTestOptions.DEFAULT_HTTP_ATTRIBUTES); - attributes.remove(SemanticAttributes.NET_PEER_NAME); - attributes.remove(SemanticAttributes.NET_PEER_PORT); + attributes.remove(ServerAttributes.SERVER_ADDRESS); + attributes.remove(ServerAttributes.SERVER_PORT); return attributes; } diff --git a/instrumentation/netty/netty-4.1/testing/src/main/java/io/opentelemetry/instrumentation/netty/v4_1/AbstractNetty41ServerTest.java b/instrumentation/netty/netty-4.1/testing/src/main/java/io/opentelemetry/instrumentation/netty/v4_1/AbstractNetty41ServerTest.java index 30f1012d89c2..6b4a81db73e3 100644 --- a/instrumentation/netty/netty-4.1/testing/src/main/java/io/opentelemetry/instrumentation/netty/v4_1/AbstractNetty41ServerTest.java +++ b/instrumentation/netty/netty-4.1/testing/src/main/java/io/opentelemetry/instrumentation/netty/v4_1/AbstractNetty41ServerTest.java @@ -44,7 +44,7 @@ import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpServerTest; import io.opentelemetry.instrumentation.testing.junit.http.HttpServerTestOptions; import io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.opentelemetry.semconv.HttpAttributes; import java.net.URI; import java.util.Collections; @@ -61,7 +61,7 @@ protected void configure(HttpServerTestOptions options) { options.setHttpAttributes( unused -> Sets.difference( - DEFAULT_HTTP_ATTRIBUTES, Collections.singleton(SemanticAttributes.HTTP_ROUTE))); + DEFAULT_HTTP_ATTRIBUTES, Collections.singleton(HttpAttributes.HTTP_ROUTE))); } @Override diff --git a/instrumentation/okhttp/okhttp-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/okhttp/v2_2/OkHttp2HttpAttributesGetter.java b/instrumentation/okhttp/okhttp-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/okhttp/v2_2/OkHttp2HttpAttributesGetter.java index e2002656d6b0..5ee0b5ec33f0 100644 --- a/instrumentation/okhttp/okhttp-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/okhttp/v2_2/OkHttp2HttpAttributesGetter.java +++ b/instrumentation/okhttp/okhttp-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/okhttp/v2_2/OkHttp2HttpAttributesGetter.java @@ -7,7 +7,7 @@ import com.squareup.okhttp.Request; import com.squareup.okhttp.Response; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpClientAttributesGetter; +import io.opentelemetry.instrumentation.api.semconv.http.HttpClientAttributesGetter; import java.util.List; import javax.annotation.Nullable; @@ -38,4 +38,51 @@ public Integer getHttpResponseStatusCode( public List getHttpResponseHeader(Request request, Response response, String name) { return response.headers(name); } + + @Nullable + @Override + public String getNetworkProtocolName(Request request, @Nullable Response response) { + if (response == null) { + return null; + } + switch (response.protocol()) { + case HTTP_1_0: + case HTTP_1_1: + case HTTP_2: + return "http"; + case SPDY_3: + return "spdy"; + } + return null; + } + + @Nullable + @Override + public String getNetworkProtocolVersion(Request request, @Nullable Response response) { + if (response == null) { + return null; + } + switch (response.protocol()) { + case HTTP_1_0: + return "1.0"; + case HTTP_1_1: + return "1.1"; + case HTTP_2: + return "2"; + case SPDY_3: + return "3.1"; + } + return null; + } + + @Override + @Nullable + public String getServerAddress(Request request) { + return request.url().getHost(); + } + + @Override + public Integer getServerPort(Request request) { + return request.url().getPort(); + } } diff --git a/instrumentation/okhttp/okhttp-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/okhttp/v2_2/OkHttp2NetAttributesGetter.java b/instrumentation/okhttp/okhttp-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/okhttp/v2_2/OkHttp2NetAttributesGetter.java deleted file mode 100644 index e44d62b17910..000000000000 --- a/instrumentation/okhttp/okhttp-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/okhttp/v2_2/OkHttp2NetAttributesGetter.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.javaagent.instrumentation.okhttp.v2_2; - -import com.squareup.okhttp.Request; -import com.squareup.okhttp.Response; -import io.opentelemetry.instrumentation.api.instrumenter.net.NetClientAttributesGetter; -import javax.annotation.Nullable; - -public final class OkHttp2NetAttributesGetter - implements NetClientAttributesGetter { - - @Nullable - @Override - public String getNetworkProtocolName(Request request, @Nullable Response response) { - if (response == null) { - return null; - } - switch (response.protocol()) { - case HTTP_1_0: - case HTTP_1_1: - case HTTP_2: - return "http"; - case SPDY_3: - return "spdy"; - } - return null; - } - - @Nullable - @Override - public String getNetworkProtocolVersion(Request request, @Nullable Response response) { - if (response == null) { - return null; - } - switch (response.protocol()) { - case HTTP_1_0: - return "1.0"; - case HTTP_1_1: - return "1.1"; - case HTTP_2: - return "2.0"; - case SPDY_3: - return "3.1"; - } - return null; - } - - @Override - @Nullable - public String getServerAddress(Request request) { - return request.url().getHost(); - } - - @Override - public Integer getServerPort(Request request) { - return request.url().getPort(); - } -} diff --git a/instrumentation/okhttp/okhttp-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/okhttp/v2_2/OkHttp2Singletons.java b/instrumentation/okhttp/okhttp-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/okhttp/v2_2/OkHttp2Singletons.java index 4f4471acb480..0cc51730f0b9 100644 --- a/instrumentation/okhttp/okhttp-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/okhttp/v2_2/OkHttp2Singletons.java +++ b/instrumentation/okhttp/okhttp-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/okhttp/v2_2/OkHttp2Singletons.java @@ -5,20 +5,12 @@ package io.opentelemetry.javaagent.instrumentation.okhttp.v2_2; -import static io.opentelemetry.instrumentation.api.instrumenter.SpanKindExtractor.alwaysClient; - import com.squareup.okhttp.Interceptor; import com.squareup.okhttp.Request; import com.squareup.okhttp.Response; import io.opentelemetry.api.GlobalOpenTelemetry; -import io.opentelemetry.api.OpenTelemetry; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpClientAttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpClientMetrics; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpSpanNameExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpSpanStatusExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.net.PeerServiceAttributesExtractor; -import io.opentelemetry.javaagent.bootstrap.internal.CommonConfig; +import io.opentelemetry.javaagent.bootstrap.internal.JavaagentHttpClientInstrumenters; public final class OkHttp2Singletons { private static final String INSTRUMENTATION_NAME = "io.opentelemetry.okhttp-2.2"; @@ -27,29 +19,12 @@ public final class OkHttp2Singletons { private static final TracingInterceptor TRACING_INTERCEPTOR; static { - OkHttp2HttpAttributesGetter httpAttributesGetter = new OkHttp2HttpAttributesGetter(); - OkHttp2NetAttributesGetter netAttributesGetter = new OkHttp2NetAttributesGetter(); - - OpenTelemetry openTelemetry = GlobalOpenTelemetry.get(); - INSTRUMENTER = - Instrumenter.builder( - openTelemetry, - INSTRUMENTATION_NAME, - HttpSpanNameExtractor.create(httpAttributesGetter)) - .setSpanStatusExtractor(HttpSpanStatusExtractor.create(httpAttributesGetter)) - .addAttributesExtractor( - HttpClientAttributesExtractor.builder(httpAttributesGetter, netAttributesGetter) - .setCapturedRequestHeaders(CommonConfig.get().getClientRequestHeaders()) - .setCapturedResponseHeaders(CommonConfig.get().getClientResponseHeaders()) - .build()) - .addAttributesExtractor( - PeerServiceAttributesExtractor.create( - netAttributesGetter, CommonConfig.get().getPeerServiceMapping())) - .addOperationMetrics(HttpClientMetrics.get()) - .buildInstrumenter(alwaysClient()); + JavaagentHttpClientInstrumenters.create( + INSTRUMENTATION_NAME, new OkHttp2HttpAttributesGetter()); - TRACING_INTERCEPTOR = new TracingInterceptor(INSTRUMENTER, openTelemetry.getPropagators()); + TRACING_INTERCEPTOR = + new TracingInterceptor(INSTRUMENTER, GlobalOpenTelemetry.get().getPropagators()); } public static Instrumenter instrumenter() { diff --git a/instrumentation/okhttp/okhttp-2.2/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/okhttp/v2_2/OkHttp2Test.java b/instrumentation/okhttp/okhttp-2.2/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/okhttp/v2_2/OkHttp2Test.java index 41807753bf6a..2f2c56c8f1af 100644 --- a/instrumentation/okhttp/okhttp-2.2/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/okhttp/v2_2/OkHttp2Test.java +++ b/instrumentation/okhttp/okhttp-2.2/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/okhttp/v2_2/OkHttp2Test.java @@ -5,8 +5,6 @@ package io.opentelemetry.javaagent.instrumentation.okhttp.v2_2; -import static io.opentelemetry.api.common.AttributeKey.stringKey; - import com.squareup.okhttp.Callback; import com.squareup.okhttp.MediaType; import com.squareup.okhttp.OkHttpClient; @@ -20,6 +18,7 @@ import io.opentelemetry.instrumentation.testing.junit.http.HttpClientInstrumentationExtension; import io.opentelemetry.instrumentation.testing.junit.http.HttpClientResult; import io.opentelemetry.instrumentation.testing.junit.http.HttpClientTestOptions; +import io.opentelemetry.semconv.NetworkAttributes; import java.io.IOException; import java.net.URI; import java.util.HashSet; @@ -38,7 +37,7 @@ public class OkHttp2Test extends AbstractHttpClientTest { private static final OkHttpClient clientWithReadTimeout = new OkHttpClient(); @BeforeAll - void setupSpec() { + void setup() { client.setConnectTimeout(CONNECTION_TIMEOUT.toMillis(), TimeUnit.MILLISECONDS); clientWithReadTimeout.setConnectTimeout(CONNECTION_TIMEOUT.toMillis(), TimeUnit.MILLISECONDS); clientWithReadTimeout.setReadTimeout(READ_TIMEOUT.toMillis(), TimeUnit.MILLISECONDS); @@ -100,16 +99,13 @@ protected void configure(HttpClientTestOptions.Builder optionsBuilder) { uri -> { Set> attributes = new HashSet<>(HttpClientTestOptions.DEFAULT_HTTP_ATTRIBUTES); - // protocol is extracted from the response, and those URLs cause exceptions (= null // response) if ("http://localhost:61/".equals(uri.toString()) || "https://192.0.2.1/".equals(uri.toString()) || resolveAddress("/read-timeout").toString().equals(uri.toString())) { - attributes.remove(stringKey("net.protocol.name")); - attributes.remove(stringKey("net.protocol.version")); + attributes.remove(NetworkAttributes.NETWORK_PROTOCOL_VERSION); } - return attributes; }); } diff --git a/instrumentation/okhttp/okhttp-3.0/javaagent/build.gradle.kts b/instrumentation/okhttp/okhttp-3.0/javaagent/build.gradle.kts index db0f92fafc53..8b903da61592 100644 --- a/instrumentation/okhttp/okhttp-3.0/javaagent/build.gradle.kts +++ b/instrumentation/okhttp/okhttp-3.0/javaagent/build.gradle.kts @@ -20,3 +20,27 @@ dependencies { testImplementation(project(":instrumentation:okhttp:okhttp-3.0:testing")) } + +val testLatestDeps = findProperty("testLatestDeps") as Boolean + +testing { + suites { + val http2Test by registering(JvmTestSuite::class) { + dependencies { + if (testLatestDeps) { + implementation("com.squareup.okhttp3:okhttp:+") + compileOnly("com.google.android:annotations:4.1.1.4") + } else { + implementation("com.squareup.okhttp3:okhttp:3.11.0") + } + implementation(project(":instrumentation:okhttp:okhttp-3.0:testing")) + } + } + } +} + +tasks { + check { + dependsOn(testing.suites) + } +} diff --git a/instrumentation/okhttp/okhttp-3.0/javaagent/src/http2Test/java/io/opentelemetry/javaagent/instrumentation/okhttp/v3_0/OkHttp3Http2Test.java b/instrumentation/okhttp/okhttp-3.0/javaagent/src/http2Test/java/io/opentelemetry/javaagent/instrumentation/okhttp/v3_0/OkHttp3Http2Test.java new file mode 100644 index 000000000000..7d4f29b543a2 --- /dev/null +++ b/instrumentation/okhttp/okhttp-3.0/javaagent/src/http2Test/java/io/opentelemetry/javaagent/instrumentation/okhttp/v3_0/OkHttp3Http2Test.java @@ -0,0 +1,38 @@ +/* + * 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 io.opentelemetry.instrumentation.okhttp.v3_0.AbstractOkHttp3Test; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.http.HttpClientInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.http.HttpClientTestOptions; +import okhttp3.Call; +import okhttp3.OkHttpClient; +import okhttp3.Protocol; +import org.junit.jupiter.api.extension.RegisterExtension; + +class OkHttp3Http2Test extends AbstractOkHttp3Test { + + @RegisterExtension + static final InstrumentationExtension testing = HttpClientInstrumentationExtension.forAgent(); + + @Override + public Call.Factory createCallFactory(OkHttpClient.Builder clientBuilder) { + clientBuilder.protocols(singletonList(Protocol.H2_PRIOR_KNOWLEDGE)); + return clientBuilder.build(); + } + + @Override + protected void configure(HttpClientTestOptions.Builder optionsBuilder) { + super.configure(optionsBuilder); + + // https does not work with H2_PRIOR_KNOWLEDGE + optionsBuilder.disableTestHttps(); + optionsBuilder.setHttpProtocolVersion(uri -> "2"); + } +} diff --git a/instrumentation/okhttp/okhttp-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/okhttp/v3_0/OkHttp3DispatcherInstrumentation.java b/instrumentation/okhttp/okhttp-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/okhttp/v3_0/OkHttp3DispatcherInstrumentation.java index 7800c70ffe2e..242b0340fa2c 100644 --- a/instrumentation/okhttp/okhttp-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/okhttp/v3_0/OkHttp3DispatcherInstrumentation.java +++ b/instrumentation/okhttp/okhttp-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/okhttp/v3_0/OkHttp3DispatcherInstrumentation.java @@ -7,6 +7,7 @@ import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.implementsInterface; 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; @@ -29,8 +30,7 @@ public ElementMatcher typeMatcher() { @Override public void transform(TypeTransformer transformer) { transformer.applyAdviceToMethod( - named("enqueue") - .or(named("enqueue$okhttp")) + namedOneOf("enqueue", "enqueue$okhttp") .and(takesArgument(0, implementsInterface(named(Runnable.class.getName())))), OkHttp3DispatcherInstrumentation.class.getName() + "$AttachStateAdvice"); } diff --git a/instrumentation/okhttp/okhttp-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/okhttp/v3_0/OkHttp3Singletons.java b/instrumentation/okhttp/okhttp-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/okhttp/v3_0/OkHttp3Singletons.java index f3b1eb7539c3..f7e982f7edab 100644 --- a/instrumentation/okhttp/okhttp-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/okhttp/v3_0/OkHttp3Singletons.java +++ b/instrumentation/okhttp/okhttp-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/okhttp/v3_0/OkHttp3Singletons.java @@ -5,34 +5,29 @@ package io.opentelemetry.javaagent.instrumentation.okhttp.v3_0; -import static java.util.Collections.emptyList; - import io.opentelemetry.api.GlobalOpenTelemetry; import io.opentelemetry.context.Context; import io.opentelemetry.context.Scope; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpClientResend; +import io.opentelemetry.instrumentation.api.semconv.http.HttpClientRequestResendCount; import io.opentelemetry.instrumentation.okhttp.v3_0.internal.ConnectionErrorSpanInterceptor; -import io.opentelemetry.instrumentation.okhttp.v3_0.internal.OkHttpInstrumenterFactory; +import io.opentelemetry.instrumentation.okhttp.v3_0.internal.OkHttpClientInstrumenterBuilderFactory; import io.opentelemetry.instrumentation.okhttp.v3_0.internal.TracingInterceptor; -import io.opentelemetry.javaagent.bootstrap.internal.CommonConfig; +import io.opentelemetry.javaagent.bootstrap.internal.JavaagentHttpClientInstrumenters; import okhttp3.Interceptor; -import okhttp3.Request; import okhttp3.Response; /** Holder of singleton interceptors for adding to instrumented clients. */ public final class OkHttp3Singletons { - private static final Instrumenter INSTRUMENTER = - OkHttpInstrumenterFactory.create( - GlobalOpenTelemetry.get(), - CommonConfig.get().getClientRequestHeaders(), - CommonConfig.get().getClientResponseHeaders(), - emptyList()); + private static final Instrumenter INSTRUMENTER = + JavaagentHttpClientInstrumenters.create( + OkHttpClientInstrumenterBuilderFactory.create(GlobalOpenTelemetry.get())); public static final Interceptor CONTEXT_INTERCEPTOR = chain -> { - try (Scope ignored = HttpClientResend.initialize(Context.current()).makeCurrent()) { + try (Scope ignored = + HttpClientRequestResendCount.initialize(Context.current()).makeCurrent()) { return chain.proceed(chain.request()); } }; diff --git a/instrumentation/okhttp/okhttp-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/okhttp/v3_0/OkHttp3Test.java b/instrumentation/okhttp/okhttp-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/okhttp/v3_0/OkHttp3Test.java index 37653b1c0790..f7c4d0face27 100644 --- a/instrumentation/okhttp/okhttp-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/okhttp/v3_0/OkHttp3Test.java +++ b/instrumentation/okhttp/okhttp-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/okhttp/v3_0/OkHttp3Test.java @@ -5,6 +5,7 @@ package io.opentelemetry.javaagent.instrumentation.okhttp.v3_0; +import static java.util.Collections.singletonList; import static org.assertj.core.api.Assertions.assertThat; import io.opentelemetry.instrumentation.okhttp.v3_0.AbstractOkHttp3Test; @@ -13,16 +14,18 @@ import java.util.concurrent.TimeUnit; import okhttp3.Call; import okhttp3.OkHttpClient; +import okhttp3.Protocol; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; -public class OkHttp3Test extends AbstractOkHttp3Test { +class OkHttp3Test extends AbstractOkHttp3Test { @RegisterExtension static final InstrumentationExtension testing = HttpClientInstrumentationExtension.forAgent(); @Override public Call.Factory createCallFactory(OkHttpClient.Builder clientBuilder) { + clientBuilder.protocols(singletonList(Protocol.HTTP_1_1)); return clientBuilder.build(); } diff --git a/instrumentation/okhttp/okhttp-3.0/library/build.gradle.kts b/instrumentation/okhttp/okhttp-3.0/library/build.gradle.kts index 4d262a4cf477..c241f44fb83b 100644 --- a/instrumentation/okhttp/okhttp-3.0/library/build.gradle.kts +++ b/instrumentation/okhttp/okhttp-3.0/library/build.gradle.kts @@ -9,3 +9,28 @@ dependencies { testImplementation(project(":instrumentation:okhttp:okhttp-3.0:testing")) } + +val testLatestDeps = findProperty("testLatestDeps") as Boolean + +testing { + suites { + val http2Test by registering(JvmTestSuite::class) { + dependencies { + implementation(project()) + if (testLatestDeps) { + implementation("com.squareup.okhttp3:okhttp:+") + compileOnly("com.google.android:annotations:4.1.1.4") + } else { + implementation("com.squareup.okhttp3:okhttp:3.11.0") + } + implementation(project(":instrumentation:okhttp:okhttp-3.0:testing")) + } + } + } +} + +tasks { + check { + dependsOn(testing.suites) + } +} diff --git a/instrumentation/okhttp/okhttp-3.0/library/src/http2Test/java/io/opentelemetry/instrumentation/okhttp/v3_0/OkHttp3Http2Test.java b/instrumentation/okhttp/okhttp-3.0/library/src/http2Test/java/io/opentelemetry/instrumentation/okhttp/v3_0/OkHttp3Http2Test.java new file mode 100644 index 000000000000..1ce2679d818a --- /dev/null +++ b/instrumentation/okhttp/okhttp-3.0/library/src/http2Test/java/io/opentelemetry/instrumentation/okhttp/v3_0/OkHttp3Http2Test.java @@ -0,0 +1,42 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.okhttp.v3_0; + +import static java.util.Collections.singletonList; + +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpClientTest; +import io.opentelemetry.instrumentation.testing.junit.http.HttpClientInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.http.HttpClientTestOptions; +import okhttp3.Call; +import okhttp3.OkHttpClient; +import okhttp3.Protocol; +import org.junit.jupiter.api.extension.RegisterExtension; + +class OkHttp3Http2Test extends AbstractOkHttp3Test { + + @RegisterExtension + static final InstrumentationExtension testing = HttpClientInstrumentationExtension.forLibrary(); + + @Override + public Call.Factory createCallFactory(OkHttpClient.Builder clientBuilder) { + clientBuilder.protocols(singletonList(Protocol.H2_PRIOR_KNOWLEDGE)); + return OkHttpTelemetry.builder(testing.getOpenTelemetry()) + .setCapturedRequestHeaders(singletonList(AbstractHttpClientTest.TEST_REQUEST_HEADER)) + .setCapturedResponseHeaders(singletonList(AbstractHttpClientTest.TEST_RESPONSE_HEADER)) + .build() + .newCallFactory(clientBuilder.build()); + } + + @Override + protected void configure(HttpClientTestOptions.Builder optionsBuilder) { + super.configure(optionsBuilder); + + // https does not work with H2_PRIOR_KNOWLEDGE + optionsBuilder.disableTestHttps(); + optionsBuilder.setHttpProtocolVersion(uri -> "2"); + } +} diff --git a/instrumentation/okhttp/okhttp-3.0/library/src/main/java/io/opentelemetry/instrumentation/okhttp/v3_0/ContextInterceptor.java b/instrumentation/okhttp/okhttp-3.0/library/src/main/java/io/opentelemetry/instrumentation/okhttp/v3_0/ContextInterceptor.java index c003b629ac3c..3b70a0ae66f7 100644 --- a/instrumentation/okhttp/okhttp-3.0/library/src/main/java/io/opentelemetry/instrumentation/okhttp/v3_0/ContextInterceptor.java +++ b/instrumentation/okhttp/okhttp-3.0/library/src/main/java/io/opentelemetry/instrumentation/okhttp/v3_0/ContextInterceptor.java @@ -7,7 +7,7 @@ import io.opentelemetry.context.Context; import io.opentelemetry.context.Scope; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpClientResend; +import io.opentelemetry.instrumentation.api.semconv.http.HttpClientRequestResendCount; import java.io.IOException; import okhttp3.Interceptor; import okhttp3.Request; @@ -23,7 +23,7 @@ public Response intercept(Chain chain) throws IOException { parentContext = Context.current(); } // include the resend counter - Context context = HttpClientResend.initialize(parentContext); + Context context = HttpClientRequestResendCount.initialize(parentContext); try (Scope ignored = context.makeCurrent()) { return chain.proceed(request); } diff --git a/instrumentation/okhttp/okhttp-3.0/library/src/main/java/io/opentelemetry/instrumentation/okhttp/v3_0/OkHttpTelemetry.java b/instrumentation/okhttp/okhttp-3.0/library/src/main/java/io/opentelemetry/instrumentation/okhttp/v3_0/OkHttpTelemetry.java index 22e6b1cf9995..cd4963f24654 100644 --- a/instrumentation/okhttp/okhttp-3.0/library/src/main/java/io/opentelemetry/instrumentation/okhttp/v3_0/OkHttpTelemetry.java +++ b/instrumentation/okhttp/okhttp-3.0/library/src/main/java/io/opentelemetry/instrumentation/okhttp/v3_0/OkHttpTelemetry.java @@ -14,7 +14,6 @@ import okhttp3.Callback; import okhttp3.Interceptor; import okhttp3.OkHttpClient; -import okhttp3.Request; import okhttp3.Response; /** Entrypoint for instrumenting OkHttp clients. */ @@ -32,10 +31,11 @@ public static OkHttpTelemetryBuilder builder(OpenTelemetry openTelemetry) { return new OkHttpTelemetryBuilder(openTelemetry); } - private final Instrumenter instrumenter; + private final Instrumenter instrumenter; private final ContextPropagators propagators; - OkHttpTelemetry(Instrumenter instrumenter, ContextPropagators propagators) { + OkHttpTelemetry( + Instrumenter instrumenter, ContextPropagators propagators) { this.instrumenter = instrumenter; this.propagators = propagators; } diff --git a/instrumentation/okhttp/okhttp-3.0/library/src/main/java/io/opentelemetry/instrumentation/okhttp/v3_0/OkHttpTelemetryBuilder.java b/instrumentation/okhttp/okhttp-3.0/library/src/main/java/io/opentelemetry/instrumentation/okhttp/v3_0/OkHttpTelemetryBuilder.java index f00cb28f0068..1623ad127d8b 100644 --- a/instrumentation/okhttp/okhttp-3.0/library/src/main/java/io/opentelemetry/instrumentation/okhttp/v3_0/OkHttpTelemetryBuilder.java +++ b/instrumentation/okhttp/okhttp-3.0/library/src/main/java/io/opentelemetry/instrumentation/okhttp/v3_0/OkHttpTelemetryBuilder.java @@ -5,28 +5,26 @@ package io.opentelemetry.instrumentation.okhttp.v3_0; -import static java.util.Collections.emptyList; - import com.google.errorprone.annotations.CanIgnoreReturnValue; import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.instrumentation.api.incubator.builder.internal.DefaultHttpClientInstrumenterBuilder; import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; -import io.opentelemetry.instrumentation.okhttp.v3_0.internal.OkHttpInstrumenterFactory; -import java.util.ArrayList; +import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor; +import io.opentelemetry.instrumentation.api.semconv.http.HttpClientAttributesExtractorBuilder; +import io.opentelemetry.instrumentation.okhttp.v3_0.internal.OkHttpClientInstrumenterBuilderFactory; import java.util.List; -import okhttp3.Request; +import java.util.Set; +import java.util.function.Function; +import okhttp3.Interceptor; import okhttp3.Response; /** A builder of {@link OkHttpTelemetry}. */ public final class OkHttpTelemetryBuilder { - private final OpenTelemetry openTelemetry; - private final List> additionalExtractors = - new ArrayList<>(); - private List capturedRequestHeaders = emptyList(); - private List capturedResponseHeaders = emptyList(); + private final DefaultHttpClientInstrumenterBuilder builder; OkHttpTelemetryBuilder(OpenTelemetry openTelemetry) { - this.openTelemetry = openTelemetry; + builder = OkHttpClientInstrumenterBuilderFactory.create(openTelemetry); } /** @@ -34,9 +32,9 @@ public final class OkHttpTelemetryBuilder { * items. */ @CanIgnoreReturnValue - public OkHttpTelemetryBuilder addAttributesExtractor( - AttributesExtractor attributesExtractor) { - additionalExtractors.add(attributesExtractor); + public OkHttpTelemetryBuilder addAttributeExtractor( + AttributesExtractor attributesExtractor) { + builder.addAttributeExtractor(attributesExtractor); return this; } @@ -47,7 +45,7 @@ public OkHttpTelemetryBuilder addAttributesExtractor( */ @CanIgnoreReturnValue public OkHttpTelemetryBuilder setCapturedRequestHeaders(List requestHeaders) { - capturedRequestHeaders = new ArrayList<>(requestHeaders); + builder.setCapturedRequestHeaders(requestHeaders); return this; } @@ -58,7 +56,50 @@ public OkHttpTelemetryBuilder setCapturedRequestHeaders(List requestHead */ @CanIgnoreReturnValue public OkHttpTelemetryBuilder setCapturedResponseHeaders(List responseHeaders) { - capturedResponseHeaders = new ArrayList<>(responseHeaders); + builder.setCapturedResponseHeaders(responseHeaders); + return this; + } + + /** + * Configures the instrumentation to recognize an alternative set of HTTP request methods. + * + *

    By default, this instrumentation defines "known" methods as the ones listed in RFC9110 and the PATCH + * method defined in RFC5789. + * + *

    Note: calling this method overrides the default known method sets completely; it does + * not supplement it. + * + * @param knownMethods A set of recognized HTTP request methods. + * @see HttpClientAttributesExtractorBuilder#setKnownMethods(Set) + */ + @CanIgnoreReturnValue + public OkHttpTelemetryBuilder setKnownMethods(Set knownMethods) { + builder.setKnownMethods(knownMethods); + return this; + } + + /** + * Configures the instrumentation to emit experimental HTTP client metrics. + * + * @param emitExperimentalHttpClientMetrics {@code true} if the experimental HTTP client metrics + * are to be emitted. + */ + @CanIgnoreReturnValue + public OkHttpTelemetryBuilder setEmitExperimentalHttpClientMetrics( + boolean emitExperimentalHttpClientMetrics) { + builder.setEmitExperimentalHttpClientMetrics(emitExperimentalHttpClientMetrics); + return this; + } + + /** Sets custom {@link SpanNameExtractor} via transform function. */ + @CanIgnoreReturnValue + public OkHttpTelemetryBuilder setSpanNameExtractor( + Function< + SpanNameExtractor, + ? extends SpanNameExtractor> + spanNameExtractorTransformer) { + builder.setSpanNameExtractor(spanNameExtractorTransformer); return this; } @@ -66,9 +107,6 @@ public OkHttpTelemetryBuilder setCapturedResponseHeaders(List responseHe * Returns a new {@link OkHttpTelemetry} with the settings of this {@link OkHttpTelemetryBuilder}. */ public OkHttpTelemetry build() { - return new OkHttpTelemetry( - OkHttpInstrumenterFactory.create( - openTelemetry, capturedRequestHeaders, capturedResponseHeaders, additionalExtractors), - openTelemetry.getPropagators()); + return new OkHttpTelemetry(builder.build(), builder.getOpenTelemetry().getPropagators()); } } diff --git a/instrumentation/okhttp/okhttp-3.0/library/src/main/java/io/opentelemetry/instrumentation/okhttp/v3_0/internal/ConnectionErrorSpanInterceptor.java b/instrumentation/okhttp/okhttp-3.0/library/src/main/java/io/opentelemetry/instrumentation/okhttp/v3_0/internal/ConnectionErrorSpanInterceptor.java index b877c3416f0b..3da6df3b25ce 100644 --- a/instrumentation/okhttp/okhttp-3.0/library/src/main/java/io/opentelemetry/instrumentation/okhttp/v3_0/internal/ConnectionErrorSpanInterceptor.java +++ b/instrumentation/okhttp/okhttp-3.0/library/src/main/java/io/opentelemetry/instrumentation/okhttp/v3_0/internal/ConnectionErrorSpanInterceptor.java @@ -7,8 +7,8 @@ import io.opentelemetry.context.Context; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpClientResend; import io.opentelemetry.instrumentation.api.internal.InstrumenterUtil; +import io.opentelemetry.instrumentation.api.semconv.http.HttpClientRequestResendCount; import java.io.IOException; import java.time.Instant; import okhttp3.Interceptor; @@ -21,9 +21,9 @@ */ public final class ConnectionErrorSpanInterceptor implements Interceptor { - private final Instrumenter instrumenter; + private final Instrumenter instrumenter; - public ConnectionErrorSpanInterceptor(Instrumenter instrumenter) { + public ConnectionErrorSpanInterceptor(Instrumenter instrumenter) { this.instrumenter = instrumenter; } @@ -42,10 +42,10 @@ public Response intercept(Chain chain) throws IOException { throw t; } finally { // only create a span when there wasn't any HTTP request - if (HttpClientResend.get(parentContext) == 0) { - if (instrumenter.shouldStart(parentContext, request)) { + if (HttpClientRequestResendCount.get(parentContext) == 0) { + if (instrumenter.shouldStart(parentContext, chain)) { InstrumenterUtil.startAndEnd( - instrumenter, parentContext, request, response, error, startTime, Instant.now()); + instrumenter, parentContext, chain, response, error, startTime, Instant.now()); } } } diff --git a/instrumentation/okhttp/okhttp-3.0/library/src/main/java/io/opentelemetry/instrumentation/okhttp/v3_0/internal/OkHttpAttributesGetter.java b/instrumentation/okhttp/okhttp-3.0/library/src/main/java/io/opentelemetry/instrumentation/okhttp/v3_0/internal/OkHttpAttributesGetter.java index a3e1d12181bf..0043f7dd52ed 100644 --- a/instrumentation/okhttp/okhttp-3.0/library/src/main/java/io/opentelemetry/instrumentation/okhttp/v3_0/internal/OkHttpAttributesGetter.java +++ b/instrumentation/okhttp/okhttp-3.0/library/src/main/java/io/opentelemetry/instrumentation/okhttp/v3_0/internal/OkHttpAttributesGetter.java @@ -5,38 +5,118 @@ package io.opentelemetry.instrumentation.okhttp.v3_0.internal; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpClientAttributesGetter; +import io.opentelemetry.instrumentation.api.semconv.http.HttpClientAttributesGetter; +import java.net.InetSocketAddress; +import java.net.SocketAddress; import java.util.List; import javax.annotation.Nullable; -import okhttp3.Request; +import okhttp3.Connection; +import okhttp3.Interceptor; import okhttp3.Response; -enum OkHttpAttributesGetter implements HttpClientAttributesGetter { +/** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time. + */ +public enum OkHttpAttributesGetter + implements HttpClientAttributesGetter { INSTANCE; @Override - public String getHttpRequestMethod(Request request) { - return request.method(); + public String getHttpRequestMethod(Interceptor.Chain chain) { + return chain.request().method(); } @Override - public String getUrlFull(Request request) { - return request.url().toString(); + public String getUrlFull(Interceptor.Chain chain) { + return chain.request().url().toString(); } @Override - public List getHttpRequestHeader(Request request, String name) { - return request.headers(name); + public List getHttpRequestHeader(Interceptor.Chain chain, String name) { + return chain.request().headers(name); } @Override public Integer getHttpResponseStatusCode( - Request request, Response response, @Nullable Throwable error) { + Interceptor.Chain chain, Response response, @Nullable Throwable error) { return response.code(); } @Override - public List getHttpResponseHeader(Request request, Response response, String name) { + public List getHttpResponseHeader( + Interceptor.Chain chain, Response response, String name) { return response.headers(name); } + + @Nullable + @Override + public String getNetworkProtocolName(Interceptor.Chain chain, @Nullable Response response) { + if (response == null) { + return null; + } + switch (response.protocol()) { + case HTTP_1_0: + case HTTP_1_1: + case HTTP_2: + return "http"; + case SPDY_3: + return "spdy"; + } + // added in 3.11.0 + if ("H2_PRIOR_KNOWLEDGE".equals(response.protocol().name())) { + return "http"; + } + return null; + } + + @Nullable + @Override + public String getNetworkProtocolVersion(Interceptor.Chain chain, @Nullable Response response) { + if (response == null) { + return null; + } + switch (response.protocol()) { + case HTTP_1_0: + return "1.0"; + case HTTP_1_1: + return "1.1"; + case HTTP_2: + return "2"; + case SPDY_3: + return "3.1"; + } + // added in 3.11.0 + if ("H2_PRIOR_KNOWLEDGE".equals(response.protocol().name())) { + return "2"; + } + return null; + } + + @Override + @Nullable + public String getServerAddress(Interceptor.Chain chain) { + return chain.request().url().host(); + } + + @Override + public Integer getServerPort(Interceptor.Chain chain) { + return chain.request().url().port(); + } + + @Nullable + @Override + public InetSocketAddress getNetworkPeerInetSocketAddress( + Interceptor.Chain chain, @Nullable Response response) { + Connection connection = chain.connection(); + if (connection == null) { + return null; + } + SocketAddress socketAddress = connection.socket().getRemoteSocketAddress(); + if (socketAddress instanceof InetSocketAddress) { + return (InetSocketAddress) socketAddress; + } else { + return null; + } + } } diff --git a/instrumentation/okhttp/okhttp-3.0/library/src/main/java/io/opentelemetry/instrumentation/okhttp/v3_0/internal/OkHttpClientInstrumenterBuilderFactory.java b/instrumentation/okhttp/okhttp-3.0/library/src/main/java/io/opentelemetry/instrumentation/okhttp/v3_0/internal/OkHttpClientInstrumenterBuilderFactory.java new file mode 100644 index 000000000000..4dbaf771d24d --- /dev/null +++ b/instrumentation/okhttp/okhttp-3.0/library/src/main/java/io/opentelemetry/instrumentation/okhttp/v3_0/internal/OkHttpClientInstrumenterBuilderFactory.java @@ -0,0 +1,27 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.okhttp.v3_0.internal; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.instrumentation.api.incubator.builder.internal.DefaultHttpClientInstrumenterBuilder; +import okhttp3.Interceptor; +import okhttp3.Response; + +/** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time. + */ +public class OkHttpClientInstrumenterBuilderFactory { + private static final String INSTRUMENTATION_NAME = "io.opentelemetry.okhttp-3.0"; + + private OkHttpClientInstrumenterBuilderFactory() {} + + public static DefaultHttpClientInstrumenterBuilder create( + OpenTelemetry openTelemetry) { + return new DefaultHttpClientInstrumenterBuilder<>( + INSTRUMENTATION_NAME, openTelemetry, OkHttpAttributesGetter.INSTANCE); + } +} diff --git a/instrumentation/okhttp/okhttp-3.0/library/src/main/java/io/opentelemetry/instrumentation/okhttp/v3_0/internal/OkHttpInstrumenterFactory.java b/instrumentation/okhttp/okhttp-3.0/library/src/main/java/io/opentelemetry/instrumentation/okhttp/v3_0/internal/OkHttpInstrumenterFactory.java deleted file mode 100644 index 2deed122b8d3..000000000000 --- a/instrumentation/okhttp/okhttp-3.0/library/src/main/java/io/opentelemetry/instrumentation/okhttp/v3_0/internal/OkHttpInstrumenterFactory.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.okhttp.v3_0.internal; - -import static io.opentelemetry.instrumentation.api.instrumenter.SpanKindExtractor.alwaysClient; - -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.http.HttpClientAttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpClientMetrics; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpSpanNameExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpSpanStatusExtractor; -import java.util.List; -import okhttp3.Request; -import okhttp3.Response; - -/** - * This class is internal and is hence not for public use. Its APIs are unstable and can change at - * any time. - */ -public final class OkHttpInstrumenterFactory { - - private static final String INSTRUMENTATION_NAME = "io.opentelemetry.okhttp-3.0"; - - public static Instrumenter create( - OpenTelemetry openTelemetry, - List capturedRequestHeaders, - List capturedResponseHeaders, - List> additionalAttributesExtractors) { - - OkHttpAttributesGetter httpAttributesGetter = OkHttpAttributesGetter.INSTANCE; - OkHttpNetAttributesGetter netAttributesGetter = OkHttpNetAttributesGetter.INSTANCE; - - return Instrumenter.builder( - openTelemetry, INSTRUMENTATION_NAME, HttpSpanNameExtractor.create(httpAttributesGetter)) - .setSpanStatusExtractor(HttpSpanStatusExtractor.create(httpAttributesGetter)) - .addAttributesExtractor( - HttpClientAttributesExtractor.builder(httpAttributesGetter, netAttributesGetter) - .setCapturedRequestHeaders(capturedRequestHeaders) - .setCapturedResponseHeaders(capturedResponseHeaders) - .build()) - .addAttributesExtractors(additionalAttributesExtractors) - .addOperationMetrics(HttpClientMetrics.get()) - .buildInstrumenter(alwaysClient()); - } - - private OkHttpInstrumenterFactory() {} -} diff --git a/instrumentation/okhttp/okhttp-3.0/library/src/main/java/io/opentelemetry/instrumentation/okhttp/v3_0/internal/OkHttpNetAttributesGetter.java b/instrumentation/okhttp/okhttp-3.0/library/src/main/java/io/opentelemetry/instrumentation/okhttp/v3_0/internal/OkHttpNetAttributesGetter.java deleted file mode 100644 index 51b105446558..000000000000 --- a/instrumentation/okhttp/okhttp-3.0/library/src/main/java/io/opentelemetry/instrumentation/okhttp/v3_0/internal/OkHttpNetAttributesGetter.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.okhttp.v3_0.internal; - -import io.opentelemetry.instrumentation.api.instrumenter.net.NetClientAttributesGetter; -import javax.annotation.Nullable; -import okhttp3.Request; -import okhttp3.Response; - -/** - * This class is internal and is hence not for public use. Its APIs are unstable and can change at - * any time. - */ -public enum OkHttpNetAttributesGetter implements NetClientAttributesGetter { - INSTANCE; - - @Nullable - @Override - public String getNetworkProtocolName(Request request, @Nullable Response response) { - if (response == null) { - return null; - } - switch (response.protocol()) { - case HTTP_1_0: - case HTTP_1_1: - case HTTP_2: - return "http"; - case SPDY_3: - return "spdy"; - } - return null; - } - - @Nullable - @Override - public String getNetworkProtocolVersion(Request request, @Nullable Response response) { - if (response == null) { - return null; - } - switch (response.protocol()) { - case HTTP_1_0: - return "1.0"; - case HTTP_1_1: - return "1.1"; - case HTTP_2: - return "2.0"; - case SPDY_3: - return "3.1"; - } - return null; - } - - @Override - @Nullable - public String getServerAddress(Request request) { - return request.url().host(); - } - - @Override - public Integer getServerPort(Request request) { - return request.url().port(); - } -} diff --git a/instrumentation/okhttp/okhttp-3.0/library/src/main/java/io/opentelemetry/instrumentation/okhttp/v3_0/internal/TracingInterceptor.java b/instrumentation/okhttp/okhttp-3.0/library/src/main/java/io/opentelemetry/instrumentation/okhttp/v3_0/internal/TracingInterceptor.java index 202dfabf30e6..14f37fb687f8 100644 --- a/instrumentation/okhttp/okhttp-3.0/library/src/main/java/io/opentelemetry/instrumentation/okhttp/v3_0/internal/TracingInterceptor.java +++ b/instrumentation/okhttp/okhttp-3.0/library/src/main/java/io/opentelemetry/instrumentation/okhttp/v3_0/internal/TracingInterceptor.java @@ -20,11 +20,11 @@ */ public final class TracingInterceptor implements Interceptor { - private final Instrumenter instrumenter; + private final Instrumenter instrumenter; private final ContextPropagators propagators; public TracingInterceptor( - Instrumenter instrumenter, ContextPropagators propagators) { + Instrumenter instrumenter, ContextPropagators propagators) { this.instrumenter = instrumenter; this.propagators = propagators; } @@ -34,11 +34,11 @@ public Response intercept(Chain chain) throws IOException { Request request = chain.request(); Context parentContext = Context.current(); - if (!instrumenter.shouldStart(parentContext, request)) { + if (!instrumenter.shouldStart(parentContext, chain)) { return chain.proceed(chain.request()); } - Context context = instrumenter.start(parentContext, request); + Context context = instrumenter.start(parentContext, chain); request = injectContextToRequest(request, context); Response response = null; @@ -50,7 +50,7 @@ public Response intercept(Chain chain) throws IOException { error = e; throw e; } finally { - instrumenter.end(context, request, response, error); + instrumenter.end(context, chain, response, error); } } diff --git a/instrumentation/okhttp/okhttp-3.0/library/src/test/java/io/opentelemetry/instrumentation/okhttp/v3_0/OkHttp3Test.java b/instrumentation/okhttp/okhttp-3.0/library/src/test/java/io/opentelemetry/instrumentation/okhttp/v3_0/OkHttp3Test.java index f7acc71252df..b2f8bbddc636 100644 --- a/instrumentation/okhttp/okhttp-3.0/library/src/test/java/io/opentelemetry/instrumentation/okhttp/v3_0/OkHttp3Test.java +++ b/instrumentation/okhttp/okhttp-3.0/library/src/test/java/io/opentelemetry/instrumentation/okhttp/v3_0/OkHttp3Test.java @@ -5,26 +5,27 @@ package io.opentelemetry.instrumentation.okhttp.v3_0; +import static java.util.Collections.singletonList; + import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpClientTest; import io.opentelemetry.instrumentation.testing.junit.http.HttpClientInstrumentationExtension; -import java.util.Collections; import okhttp3.Call; import okhttp3.OkHttpClient; +import okhttp3.Protocol; import org.junit.jupiter.api.extension.RegisterExtension; -public class OkHttp3Test extends AbstractOkHttp3Test { +class OkHttp3Test extends AbstractOkHttp3Test { @RegisterExtension static final InstrumentationExtension testing = HttpClientInstrumentationExtension.forLibrary(); @Override public Call.Factory createCallFactory(OkHttpClient.Builder clientBuilder) { + clientBuilder.protocols(singletonList(Protocol.HTTP_1_1)); return OkHttpTelemetry.builder(testing.getOpenTelemetry()) - .setCapturedRequestHeaders( - Collections.singletonList(AbstractHttpClientTest.TEST_REQUEST_HEADER)) - .setCapturedResponseHeaders( - Collections.singletonList(AbstractHttpClientTest.TEST_RESPONSE_HEADER)) + .setCapturedRequestHeaders(singletonList(AbstractHttpClientTest.TEST_REQUEST_HEADER)) + .setCapturedResponseHeaders(singletonList(AbstractHttpClientTest.TEST_RESPONSE_HEADER)) .build() .newCallFactory(clientBuilder.build()); } diff --git a/instrumentation/okhttp/okhttp-3.0/testing/src/main/java/io/opentelemetry/instrumentation/okhttp/v3_0/AbstractOkHttp3Test.java b/instrumentation/okhttp/okhttp-3.0/testing/src/main/java/io/opentelemetry/instrumentation/okhttp/v3_0/AbstractOkHttp3Test.java index fc3b3bfd9cc4..5f051141f7f5 100644 --- a/instrumentation/okhttp/okhttp-3.0/testing/src/main/java/io/opentelemetry/instrumentation/okhttp/v3_0/AbstractOkHttp3Test.java +++ b/instrumentation/okhttp/okhttp-3.0/testing/src/main/java/io/opentelemetry/instrumentation/okhttp/v3_0/AbstractOkHttp3Test.java @@ -5,8 +5,6 @@ package io.opentelemetry.instrumentation.okhttp.v3_0; -import static io.opentelemetry.api.common.AttributeKey.stringKey; -import static java.util.Collections.singletonList; import static org.assertj.core.api.Assertions.assertThat; import io.opentelemetry.api.common.AttributeKey; @@ -14,7 +12,7 @@ import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpClientTest; import io.opentelemetry.instrumentation.testing.junit.http.HttpClientResult; import io.opentelemetry.instrumentation.testing.junit.http.HttpClientTestOptions; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.opentelemetry.semconv.NetworkAttributes; import java.io.IOException; import java.net.URI; import java.util.Collections; @@ -28,7 +26,6 @@ import okhttp3.Interceptor; import okhttp3.MediaType; import okhttp3.OkHttpClient; -import okhttp3.Protocol; import okhttp3.Request; import okhttp3.RequestBody; import okhttp3.Response; @@ -46,8 +43,7 @@ public abstract class AbstractOkHttp3Test extends AbstractHttpClientTest { Set> attributes = new HashSet<>(HttpClientTestOptions.DEFAULT_HTTP_ATTRIBUTES); - // the tests are capturing the user-agent, but since it's not possible to override it in - // the builder, and since it contains the okhttp library version, let's just skip - // verification on this attribute - attributes.remove(SemanticAttributes.USER_AGENT_ORIGINAL); // protocol is extracted from the response, and those URLs cause exceptions (= null // response) if ("http://localhost:61/".equals(uri.toString()) || "https://192.0.2.1/".equals(uri.toString()) + || "http://192.0.2.1/".equals(uri.toString()) || resolveAddress("/read-timeout").toString().equals(uri.toString())) { - attributes.remove(stringKey("net.protocol.name")); - attributes.remove(stringKey("net.protocol.version")); + attributes.remove(NetworkAttributes.NETWORK_PROTOCOL_VERSION); } return attributes; diff --git a/instrumentation/opencensus-shim/testing/src/test/java/io/opentelemetry/opencensusshim/JavaagentInstrumentationTest.java b/instrumentation/opencensus-shim/testing/src/test/java/io/opentelemetry/opencensusshim/JavaagentInstrumentationTest.java index 6731250b5685..02a11c5754d4 100644 --- a/instrumentation/opencensus-shim/testing/src/test/java/io/opentelemetry/opencensusshim/JavaagentInstrumentationTest.java +++ b/instrumentation/opencensus-shim/testing/src/test/java/io/opentelemetry/opencensusshim/JavaagentInstrumentationTest.java @@ -5,6 +5,8 @@ package io.opentelemetry.opencensusshim; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.satisfies; + import io.opencensus.trace.AttributeValue; import io.opencensus.trace.Tracing; import io.opencensus.trace.samplers.Samplers; @@ -15,7 +17,6 @@ import io.opentelemetry.context.Scope; import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; -import io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions; import org.assertj.core.api.AbstractBooleanAssert; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -84,9 +85,9 @@ void testInterleavedSpansOcFirst() { .hasNoParent() .hasAttribute(AttributeKey.booleanKey("outer"), true) .hasAttributesSatisfying( - OpenTelemetryAssertions.satisfies( + satisfies( AttributeKey.booleanKey("inner"), AbstractBooleanAssert::isNull), - OpenTelemetryAssertions.satisfies( + satisfies( AttributeKey.booleanKey("middle"), AbstractBooleanAssert::isNull)), // middle span sa -> @@ -94,9 +95,9 @@ void testInterleavedSpansOcFirst() { .hasParent(ta.getSpan(0)) .hasAttribute(AttributeKey.booleanKey("middle"), true) .hasAttributesSatisfying( - OpenTelemetryAssertions.satisfies( + satisfies( AttributeKey.booleanKey("inner"), AbstractBooleanAssert::isNull), - OpenTelemetryAssertions.satisfies( + satisfies( AttributeKey.booleanKey("outer"), AbstractBooleanAssert::isNull)), // inner span sa -> @@ -104,9 +105,9 @@ void testInterleavedSpansOcFirst() { .hasParent(ta.getSpan(1)) .hasAttribute(AttributeKey.booleanKey("inner"), true) .hasAttributesSatisfying( - OpenTelemetryAssertions.satisfies( + satisfies( AttributeKey.booleanKey("middle"), AbstractBooleanAssert::isNull), - OpenTelemetryAssertions.satisfies( + satisfies( AttributeKey.booleanKey("outer"), AbstractBooleanAssert::isNull)))); } @@ -159,9 +160,9 @@ void testInterleavedSpansOtelFirst() { .hasNoParent() .hasAttribute(AttributeKey.booleanKey("outer"), true) .hasAttributesSatisfying( - OpenTelemetryAssertions.satisfies( + satisfies( AttributeKey.booleanKey("inner"), AbstractBooleanAssert::isNull), - OpenTelemetryAssertions.satisfies( + satisfies( AttributeKey.booleanKey("middle"), AbstractBooleanAssert::isNull)), // middle span sa -> @@ -169,9 +170,9 @@ void testInterleavedSpansOtelFirst() { .hasParent(ta.getSpan(0)) .hasAttribute(AttributeKey.booleanKey("middle"), true) .hasAttributesSatisfying( - OpenTelemetryAssertions.satisfies( + satisfies( AttributeKey.booleanKey("inner"), AbstractBooleanAssert::isNull), - OpenTelemetryAssertions.satisfies( + satisfies( AttributeKey.booleanKey("outer"), AbstractBooleanAssert::isNull)), // inner span sa -> @@ -179,9 +180,9 @@ void testInterleavedSpansOtelFirst() { .hasParent(ta.getSpan(1)) .hasAttribute(AttributeKey.booleanKey("inner"), true) .hasAttributesSatisfying( - OpenTelemetryAssertions.satisfies( + satisfies( AttributeKey.booleanKey("middle"), AbstractBooleanAssert::isNull), - OpenTelemetryAssertions.satisfies( + satisfies( AttributeKey.booleanKey("outer"), AbstractBooleanAssert::isNull)))); } @@ -299,9 +300,9 @@ void testNestedOpenCensusSpans() { .hasNoParent() .hasAttribute(AttributeKey.booleanKey("outer"), true) .hasAttributesSatisfying( - OpenTelemetryAssertions.satisfies( + satisfies( AttributeKey.booleanKey("inner"), AbstractBooleanAssert::isNull), - OpenTelemetryAssertions.satisfies( + satisfies( AttributeKey.booleanKey("middle"), AbstractBooleanAssert::isNull)), // middle span sa -> @@ -309,9 +310,9 @@ void testNestedOpenCensusSpans() { .hasParent(ta.getSpan(0)) .hasAttribute(AttributeKey.booleanKey("middle"), true) .hasAttributesSatisfying( - OpenTelemetryAssertions.satisfies( + satisfies( AttributeKey.booleanKey("inner"), AbstractBooleanAssert::isNull), - OpenTelemetryAssertions.satisfies( + satisfies( AttributeKey.booleanKey("outer"), AbstractBooleanAssert::isNull)), // inner span sa -> @@ -319,9 +320,9 @@ void testNestedOpenCensusSpans() { .hasParent(ta.getSpan(1)) .hasAttribute(AttributeKey.booleanKey("inner"), true) .hasAttributesSatisfying( - OpenTelemetryAssertions.satisfies( + satisfies( AttributeKey.booleanKey("middle"), AbstractBooleanAssert::isNull), - OpenTelemetryAssertions.satisfies( + satisfies( AttributeKey.booleanKey("outer"), AbstractBooleanAssert::isNull)))); } } diff --git a/instrumentation/opensearch/README.md b/instrumentation/opensearch/README.md index 74c57e21803a..8bc5f198736d 100644 --- a/instrumentation/opensearch/README.md +++ b/instrumentation/opensearch/README.md @@ -1,5 +1,5 @@ # Settings for the OpenSearch instrumentation -| System property | Type | Default | Description | -|----------------------------------------------------------------|-----------|---------|-----------------------------------------------------| -| `otel.instrumentation.opensearch.experimental-span-attributes` | `Boolean` | `false` | Enable the capture of experimental span attributes. | +| System property | Type | Default | Description | +| -------------------------------------------------------------- | ------- | ------- | --------------------------------------------------- | +| `otel.instrumentation.opensearch.experimental-span-attributes` | Boolean | `false` | Enable the capture of experimental span attributes. | diff --git a/instrumentation/opensearch/opensearch-rest-1.0/javaagent/src/test/java/OpenSearchRestTest.java b/instrumentation/opensearch/opensearch-rest-1.0/javaagent/src/test/java/OpenSearchRestTest.java index 2d758380588c..cb51f8efb076 100644 --- a/instrumentation/opensearch/opensearch-rest-1.0/javaagent/src/test/java/OpenSearchRestTest.java +++ b/instrumentation/opensearch/opensearch-rest-1.0/javaagent/src/test/java/OpenSearchRestTest.java @@ -3,12 +3,15 @@ * SPDX-License-Identifier: Apache-2.0 */ -import static io.opentelemetry.api.common.AttributeKey.stringKey; import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; import io.opentelemetry.api.trace.SpanKind; import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.opentelemetry.semconv.HttpAttributes; +import io.opentelemetry.semconv.NetworkAttributes; +import io.opentelemetry.semconv.ServerAttributes; +import io.opentelemetry.semconv.UrlAttributes; +import io.opentelemetry.semconv.incubating.DbIncubatingAttributes; import java.io.IOException; import java.security.KeyManagementException; import java.security.KeyStoreException; @@ -20,6 +23,7 @@ import org.apache.http.auth.AuthScope; import org.apache.http.auth.UsernamePasswordCredentials; import org.apache.http.client.CredentialsProvider; +import org.apache.http.conn.ssl.NoopHostnameVerifier; import org.apache.http.conn.ssl.TrustAllStrategy; import org.apache.http.impl.client.BasicCredentialsProvider; import org.apache.http.ssl.SSLContextBuilder; @@ -69,6 +73,9 @@ static void setUpOpenSearch() httpClientBuilder -> httpClientBuilder .setSSLContext(sslContext) + // Required for non-localhost Docker runtimes, the SSL cert in the + // OpenSearch image is registered to "localhost" + .setSSLHostnameVerifier(new NoopHostnameVerifier()) .setDefaultCredentialsProvider(credentialsProvider)) .build(); } @@ -90,23 +97,20 @@ void shouldGetStatusWithTraces() throws IOException { span.hasName("GET") .hasKind(SpanKind.CLIENT) .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.DB_SYSTEM, "opensearch"), - equalTo(SemanticAttributes.DB_OPERATION, "GET"), - equalTo(SemanticAttributes.DB_STATEMENT, "GET _cluster/health")), + equalTo(DbIncubatingAttributes.DB_SYSTEM, "opensearch"), + equalTo(DbIncubatingAttributes.DB_OPERATION, "GET"), + equalTo(DbIncubatingAttributes.DB_STATEMENT, "GET _cluster/health")), span -> span.hasName("GET") .hasKind(SpanKind.CLIENT) .hasParent(trace.getSpan(0)) .hasAttributesSatisfyingExactly( - equalTo(stringKey("net.protocol.name"), "http"), - equalTo(stringKey("net.protocol.version"), "1.1"), - equalTo(SemanticAttributes.NET_PEER_NAME, httpHost.getHostName()), - equalTo(SemanticAttributes.NET_PEER_PORT, httpHost.getPort()), - equalTo(SemanticAttributes.HTTP_METHOD, "GET"), - equalTo( - SemanticAttributes.HTTP_URL, httpHost.toURI() + "/_cluster/health"), - equalTo(SemanticAttributes.HTTP_STATUS_CODE, 200L), - equalTo(SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH, 415L)))); + equalTo(NetworkAttributes.NETWORK_PROTOCOL_VERSION, "1.1"), + equalTo(ServerAttributes.SERVER_ADDRESS, httpHost.getHostName()), + equalTo(ServerAttributes.SERVER_PORT, httpHost.getPort()), + equalTo(HttpAttributes.HTTP_REQUEST_METHOD, "GET"), + equalTo(UrlAttributes.URL_FULL, httpHost.toURI() + "/_cluster/health"), + equalTo(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, 200L)))); } @Test @@ -158,23 +162,20 @@ public void onFailure(Exception e) { .hasKind(SpanKind.CLIENT) .hasParent(trace.getSpan(0)) .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.DB_SYSTEM, "opensearch"), - equalTo(SemanticAttributes.DB_OPERATION, "GET"), - equalTo(SemanticAttributes.DB_STATEMENT, "GET _cluster/health")), + equalTo(DbIncubatingAttributes.DB_SYSTEM, "opensearch"), + equalTo(DbIncubatingAttributes.DB_OPERATION, "GET"), + equalTo(DbIncubatingAttributes.DB_STATEMENT, "GET _cluster/health")), span -> span.hasName("GET") .hasKind(SpanKind.CLIENT) .hasParent(trace.getSpan(1)) .hasAttributesSatisfyingExactly( - equalTo(stringKey("net.protocol.name"), "http"), - equalTo(stringKey("net.protocol.version"), "1.1"), - equalTo(SemanticAttributes.NET_PEER_NAME, httpHost.getHostName()), - equalTo(SemanticAttributes.NET_PEER_PORT, httpHost.getPort()), - equalTo(SemanticAttributes.HTTP_METHOD, "GET"), - equalTo( - SemanticAttributes.HTTP_URL, httpHost.toURI() + "/_cluster/health"), - equalTo(SemanticAttributes.HTTP_STATUS_CODE, 200L), - equalTo(SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH, 415L)), + equalTo(NetworkAttributes.NETWORK_PROTOCOL_VERSION, "1.1"), + equalTo(ServerAttributes.SERVER_ADDRESS, httpHost.getHostName()), + equalTo(ServerAttributes.SERVER_PORT, httpHost.getPort()), + equalTo(HttpAttributes.HTTP_REQUEST_METHOD, "GET"), + equalTo(UrlAttributes.URL_FULL, httpHost.toURI() + "/_cluster/health"), + equalTo(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, 200L)), span -> span.hasName("callback") .hasKind(SpanKind.INTERNAL) diff --git a/instrumentation/opensearch/opensearch-rest-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opensearch/rest/OpenSearchRestAttributesGetter.java b/instrumentation/opensearch/opensearch-rest-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opensearch/rest/OpenSearchRestAttributesGetter.java index deb77a4211c4..79e70641dfd9 100644 --- a/instrumentation/opensearch/opensearch-rest-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opensearch/rest/OpenSearchRestAttributesGetter.java +++ b/instrumentation/opensearch/opensearch-rest-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opensearch/rest/OpenSearchRestAttributesGetter.java @@ -5,8 +5,8 @@ package io.opentelemetry.javaagent.instrumentation.opensearch.rest; -import io.opentelemetry.instrumentation.api.instrumenter.db.DbClientAttributesGetter; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.opentelemetry.instrumentation.api.incubator.semconv.db.DbClientAttributesGetter; +import io.opentelemetry.semconv.incubating.DbIncubatingAttributes; import javax.annotation.Nullable; final class OpenSearchRestAttributesGetter @@ -14,7 +14,7 @@ final class OpenSearchRestAttributesGetter @Override public String getSystem(OpenSearchRestRequest request) { - return SemanticAttributes.DbSystemValues.OPENSEARCH; + return DbIncubatingAttributes.DbSystemValues.OPENSEARCH; } @Override diff --git a/instrumentation/opensearch/opensearch-rest-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opensearch/rest/OpenSearchRestInstrumenterFactory.java b/instrumentation/opensearch/opensearch-rest-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opensearch/rest/OpenSearchRestInstrumenterFactory.java index 2d05930701b0..aed2d5b8fd9a 100644 --- a/instrumentation/opensearch/opensearch-rest-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opensearch/rest/OpenSearchRestInstrumenterFactory.java +++ b/instrumentation/opensearch/opensearch-rest-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opensearch/rest/OpenSearchRestInstrumenterFactory.java @@ -6,13 +6,11 @@ package io.opentelemetry.javaagent.instrumentation.opensearch.rest; import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.instrumentation.api.incubator.semconv.db.DbClientAttributesExtractor; +import io.opentelemetry.instrumentation.api.incubator.semconv.db.DbClientSpanNameExtractor; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; import io.opentelemetry.instrumentation.api.instrumenter.SpanKindExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.db.DbClientAttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.db.DbClientSpanNameExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.net.NetClientAttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.net.PeerServiceAttributesExtractor; -import io.opentelemetry.javaagent.bootstrap.internal.CommonConfig; +import io.opentelemetry.instrumentation.api.semconv.network.NetworkAttributesExtractor; import org.opensearch.client.Response; public final class OpenSearchRestInstrumenterFactory { @@ -27,10 +25,7 @@ public static Instrumenter create(String instru instrumentationName, DbClientSpanNameExtractor.create(dbClientAttributesGetter)) .addAttributesExtractor(DbClientAttributesExtractor.create(dbClientAttributesGetter)) - .addAttributesExtractor(NetClientAttributesExtractor.create(netAttributesGetter)) - .addAttributesExtractor( - PeerServiceAttributesExtractor.create( - netAttributesGetter, CommonConfig.get().getPeerServiceMapping())) + .addAttributesExtractor(NetworkAttributesExtractor.create(netAttributesGetter)) .buildInstrumenter(SpanKindExtractor.alwaysClient()); } diff --git a/instrumentation/opensearch/opensearch-rest-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opensearch/rest/OpenSearchRestNetResponseAttributesGetter.java b/instrumentation/opensearch/opensearch-rest-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opensearch/rest/OpenSearchRestNetResponseAttributesGetter.java index a7539c1afb45..195e851e4ea3 100644 --- a/instrumentation/opensearch/opensearch-rest-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opensearch/rest/OpenSearchRestNetResponseAttributesGetter.java +++ b/instrumentation/opensearch/opensearch-rest-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opensearch/rest/OpenSearchRestNetResponseAttributesGetter.java @@ -5,39 +5,34 @@ package io.opentelemetry.javaagent.instrumentation.opensearch.rest; -import io.opentelemetry.instrumentation.api.instrumenter.net.NetClientAttributesGetter; +import io.opentelemetry.instrumentation.api.semconv.network.NetworkAttributesGetter; +import java.net.Inet4Address; import java.net.Inet6Address; +import java.net.InetAddress; import javax.annotation.Nullable; import org.opensearch.client.Response; final class OpenSearchRestNetResponseAttributesGetter - implements NetClientAttributesGetter { + implements NetworkAttributesGetter { - @Override @Nullable - public String getServerAddress(OpenSearchRestRequest request) { - return null; - } - @Override - @Nullable - public Integer getServerPort(OpenSearchRestRequest request) { - return null; - } - - @Nullable - @Override - public String getSockFamily( - OpenSearchRestRequest elasticsearchRestRequest, @Nullable Response response) { - if (response != null && response.getHost().getAddress() instanceof Inet6Address) { - return "inet6"; + public String getNetworkType(OpenSearchRestRequest request, @Nullable Response response) { + if (response == null) { + return null; + } + InetAddress address = response.getHost().getAddress(); + if (address instanceof Inet4Address) { + return "ipv4"; + } else if (address instanceof Inet6Address) { + return "ipv6"; } return null; } @Override @Nullable - public String getServerSocketAddress(OpenSearchRestRequest request, @Nullable Response response) { + public String getNetworkPeerAddress(OpenSearchRestRequest request, @Nullable Response response) { if (response != null && response.getHost().getAddress() != null) { return response.getHost().getAddress().getHostAddress(); } diff --git a/instrumentation/opentelemetry-api/opentelemetry-api-1.0/javaagent/build.gradle.kts b/instrumentation/opentelemetry-api/opentelemetry-api-1.0/javaagent/build.gradle.kts index c686848ad94c..869efe4d5a61 100644 --- a/instrumentation/opentelemetry-api/opentelemetry-api-1.0/javaagent/build.gradle.kts +++ b/instrumentation/opentelemetry-api/opentelemetry-api-1.0/javaagent/build.gradle.kts @@ -37,8 +37,17 @@ dependencies { // using OpenTelemetry SDK to make sure that instrumentation doesn't cause // OpenTelemetrySdk.getTracerProvider() to throw ClassCastException testImplementation("io.opentelemetry:opentelemetry-sdk") - testImplementation(project(":instrumentation-api-semconv")) + testImplementation(project(":instrumentation-api-incubator")) // @WithSpan annotation is used to generate spans in ContextBridgeTest testImplementation(project(":instrumentation-annotations")) } + +configurations.configureEach { + if (name == "testRuntimeClasspath" || name == "testCompileClasspath") { + resolutionStrategy { + // earliest version that works with out test harness + force("io.opentelemetry:opentelemetry-api:1.4.0") + } + } +} diff --git a/instrumentation/opentelemetry-api/opentelemetry-api-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/OpenTelemetryApiInstrumentationModule.java b/instrumentation/opentelemetry-api/opentelemetry-api-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/OpenTelemetryApiInstrumentationModule.java index d48f19887ebc..6ce2d6ddcec4 100644 --- a/instrumentation/opentelemetry-api/opentelemetry-api-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/OpenTelemetryApiInstrumentationModule.java +++ b/instrumentation/opentelemetry-api/opentelemetry-api-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/OpenTelemetryApiInstrumentationModule.java @@ -10,10 +10,13 @@ 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.internal.ExperimentalInstrumentationModule; +import java.util.Collections; import java.util.List; @AutoService(InstrumentationModule.class) -public class OpenTelemetryApiInstrumentationModule extends InstrumentationModule { +public class OpenTelemetryApiInstrumentationModule extends InstrumentationModule + implements ExperimentalInstrumentationModule { public OpenTelemetryApiInstrumentationModule() { super("opentelemetry-api", "opentelemetry-api-1.0"); } @@ -26,4 +29,17 @@ public List typeInstrumentations() { new OpenTelemetryInstrumentation(), new SpanInstrumentation()); } + + @Override + public String getModuleGroup() { + return "opentelemetry-api-bridge"; + } + + @Override + public List agentPackagesToHide() { + // These are helper classes injected by api-version specific instrumentation modules + // We don't want to fall back to accidentally trying to load those from the agent CL + // when they haven't been injected + return Collections.singletonList("io.opentelemetry.javaagent.instrumentation.opentelemetryapi"); + } } diff --git a/instrumentation/opentelemetry-api/opentelemetry-api-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/context/InstrumentationApiContextBridging.java b/instrumentation/opentelemetry-api/opentelemetry-api-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/context/InstrumentationApiContextBridging.java index 2c1e36124395..eed3c4d281df 100644 --- a/instrumentation/opentelemetry-api/opentelemetry-api-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/context/InstrumentationApiContextBridging.java +++ b/instrumentation/opentelemetry-api/opentelemetry-api-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/context/InstrumentationApiContextBridging.java @@ -59,12 +59,14 @@ final class InstrumentationApiContextBridging { private static final MethodHandle AGENT_GET_METHOD; private static final MethodHandle AGENT_GET_ROUTE; private static final MethodHandle AGENT_GET_UPDATED_BY_SOURCE_ORDER; + private static final MethodHandle AGENT_GET_SPAN; private static final Class APPLICATION_HTTP_ROUTE_STATE; private static final MethodHandle APPLICATION_CREATE; private static final MethodHandle APPLICATION_GET_METHOD; private static final MethodHandle APPLICATION_GET_ROUTE; private static final MethodHandle APPLICATION_GET_UPDATED_BY_SOURCE_ORDER; + private static final MethodHandle APPLICATION_GET_SPAN; static { MethodHandles.Lookup lookup = MethodHandles.lookup(); @@ -74,11 +76,13 @@ final class InstrumentationApiContextBridging { MethodHandle agentGetMethod = null; MethodHandle agentGetRoute = null; MethodHandle agentGetUpdatedBySourceOrder = null; + MethodHandle agentGetSpan = null; Class applicationHttpRouteState = null; MethodHandle applicationCreate = null; MethodHandle applicationGetMethod = null; MethodHandle applicationGetRoute = null; MethodHandle applicationGetUpdatedBySourceOrder = null; + MethodHandle applicationGetSpan = null; try { agentHttpRouteState = @@ -87,7 +91,12 @@ final class InstrumentationApiContextBridging { lookup.findStatic( agentHttpRouteState, "create", - MethodType.methodType(agentHttpRouteState, String.class, String.class, int.class)); + MethodType.methodType( + agentHttpRouteState, + String.class, + String.class, + int.class, + io.opentelemetry.api.trace.Span.class)); agentGetMethod = lookup.findVirtual(agentHttpRouteState, "getMethod", MethodType.methodType(String.class)); agentGetRoute = @@ -95,15 +104,30 @@ final class InstrumentationApiContextBridging { agentGetUpdatedBySourceOrder = lookup.findVirtual( agentHttpRouteState, "getUpdatedBySourceOrder", MethodType.methodType(int.class)); + agentGetSpan = + lookup.findVirtual( + agentHttpRouteState, + "getSpan", + MethodType.methodType(io.opentelemetry.api.trace.Span.class)); applicationHttpRouteState = Class.forName("application.io.opentelemetry.instrumentation.api.internal.HttpRouteState"); - applicationCreate = - lookup.findStatic( - applicationHttpRouteState, - "create", - MethodType.methodType( - applicationHttpRouteState, String.class, String.class, int.class)); + try { + applicationCreate = + lookup.findStatic( + applicationHttpRouteState, + "create", + MethodType.methodType( + applicationHttpRouteState, String.class, String.class, int.class, Span.class)); + } catch (NoSuchMethodException exception) { + // older instrumentation-api has only the variant that does not take span + applicationCreate = + lookup.findStatic( + applicationHttpRouteState, + "create", + MethodType.methodType( + applicationHttpRouteState, String.class, String.class, int.class)); + } applicationGetMethod = lookup.findVirtual( applicationHttpRouteState, "getMethod", MethodType.methodType(String.class)); @@ -115,6 +139,13 @@ final class InstrumentationApiContextBridging { applicationHttpRouteState, "getUpdatedBySourceOrder", MethodType.methodType(int.class)); + try { + applicationGetSpan = + lookup.findVirtual( + applicationHttpRouteState, "getSpan", MethodType.methodType(Span.class)); + } catch (NoSuchMethodException ignored) { + // not present in older instrumentation-api + } } catch (Throwable ignored) { // instrumentation-api may be absent on the classpath, or it might be an older version } @@ -124,11 +155,13 @@ final class InstrumentationApiContextBridging { AGENT_GET_METHOD = agentGetMethod; AGENT_GET_ROUTE = agentGetRoute; AGENT_GET_UPDATED_BY_SOURCE_ORDER = agentGetUpdatedBySourceOrder; + AGENT_GET_SPAN = agentGetSpan; APPLICATION_HTTP_ROUTE_STATE = applicationHttpRouteState; APPLICATION_CREATE = applicationCreate; APPLICATION_GET_METHOD = applicationGetMethod; APPLICATION_GET_ROUTE = applicationGetRoute; APPLICATION_GET_UPDATED_BY_SOURCE_ORDER = applicationGetUpdatedBySourceOrder; + APPLICATION_GET_SPAN = applicationGetSpan; } @Nullable @@ -151,12 +184,16 @@ final class InstrumentationApiContextBridging { APPLICATION_CREATE, AGENT_GET_METHOD, AGENT_GET_ROUTE, - AGENT_GET_UPDATED_BY_SOURCE_ORDER), + AGENT_GET_UPDATED_BY_SOURCE_ORDER, + AGENT_GET_SPAN, + o -> o != null ? Bridging.toApplication((io.opentelemetry.api.trace.Span) o) : null), httpRouteStateConvert( AGENT_CREATE, APPLICATION_GET_METHOD, APPLICATION_GET_ROUTE, - APPLICATION_GET_UPDATED_BY_SOURCE_ORDER)); + APPLICATION_GET_UPDATED_BY_SOURCE_ORDER, + APPLICATION_GET_SPAN, + o -> o != null ? Bridging.toAgentOrNull((Span) o) : null)); } catch (Throwable ignored) { return null; } @@ -166,12 +203,18 @@ private static Function httpRouteStateConvert( MethodHandle create, MethodHandle getMethod, MethodHandle getRoute, - MethodHandle getUpdatedBySourceOrder) { + MethodHandle getUpdatedBySourceOrder, + MethodHandle getSpan, + Function convertSpan) { return httpRouteHolder -> { try { String method = (String) getMethod.invoke(httpRouteHolder); String route = (String) getRoute.invoke(httpRouteHolder); int updatedBySourceOrder = (int) getUpdatedBySourceOrder.invoke(httpRouteHolder); + if (create.type().parameterCount() == 4 && getSpan != null) { + Object span = convertSpan.apply(getSpan.invoke(httpRouteHolder)); + return create.invoke(method, route, updatedBySourceOrder, span); + } return create.invoke(method, route, updatedBySourceOrder); } catch (Throwable e) { return null; diff --git a/instrumentation/opentelemetry-api/opentelemetry-api-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/trace/ApplicationSpan.java b/instrumentation/opentelemetry-api/opentelemetry-api-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/trace/ApplicationSpan.java index 5897c6dc2537..ffdd8c46a6fd 100644 --- a/instrumentation/opentelemetry-api/opentelemetry-api-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/trace/ApplicationSpan.java +++ b/instrumentation/opentelemetry-api/opentelemetry-api-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/trace/ApplicationSpan.java @@ -100,6 +100,20 @@ public Span addEvent( return this; } + @Override + @CanIgnoreReturnValue + public Span addLink(SpanContext spanContext) { + agentSpan.addLink(Bridging.toAgent(spanContext)); + return this; + } + + @Override + @CanIgnoreReturnValue + public Span addLink(SpanContext spanContext, Attributes attributes) { + agentSpan.addLink(Bridging.toAgent(spanContext), Bridging.toAgent(attributes)); + return this; + } + @Override @CanIgnoreReturnValue public Span setStatus(StatusCode status) { diff --git a/instrumentation/opentelemetry-api/opentelemetry-api-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/trace/Bridging.java b/instrumentation/opentelemetry-api/opentelemetry-api-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/trace/Bridging.java index b4373ccc9313..636b1e6bbfe7 100644 --- a/instrumentation/opentelemetry-api/opentelemetry-api-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/trace/Bridging.java +++ b/instrumentation/opentelemetry-api/opentelemetry-api-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/trace/Bridging.java @@ -15,6 +15,8 @@ import application.io.opentelemetry.api.trace.StatusCode; import application.io.opentelemetry.api.trace.TraceState; import application.io.opentelemetry.api.trace.TraceStateBuilder; +import java.util.ArrayList; +import java.util.List; import java.util.logging.Logger; /** @@ -143,6 +145,16 @@ public static io.opentelemetry.api.common.AttributeKey toAgent(AttributeKey appl return null; } + public static List> toAgent( + List> attributeKeys) { + List> result = + new ArrayList<>(attributeKeys.size()); + for (AttributeKey attributeKey : attributeKeys) { + result.add(toAgent(attributeKey)); + } + return result; + } + public static io.opentelemetry.api.trace.StatusCode toAgent(StatusCode applicationStatus) { io.opentelemetry.api.trace.StatusCode agentCanonicalCode; try { diff --git a/instrumentation/opentelemetry-api/opentelemetry-api-1.0/javaagent/src/test/groovy/ContextBridgeTest.groovy b/instrumentation/opentelemetry-api/opentelemetry-api-1.0/javaagent/src/test/groovy/ContextBridgeTest.groovy deleted file mode 100644 index b4194399fb75..000000000000 --- a/instrumentation/opentelemetry-api/opentelemetry-api-1.0/javaagent/src/test/groovy/ContextBridgeTest.groovy +++ /dev/null @@ -1,169 +0,0 @@ -/* - * 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.instrumentation.annotations.WithSpan -import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes - -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"() { - given: - def runnable = 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") - } - } - } - } - - when: - runnable.run() - - then: - assertTraces(1) { - trace(0, 1) { - span(0) { - name "test" - hasNoParent() - attributes { - "$SemanticAttributes.CODE_NAMESPACE" runnable.class.name - "$SemanticAttributes.CODE_FUNCTION" "run" - "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"() { - given: - def runnable = 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") - } - } - } - } - - when: - runnable.run() - - then: - assertTraces(1) { - trace(0, 1) { - span(0) { - name "test" - hasNoParent() - attributes { - "$SemanticAttributes.CODE_NAMESPACE" runnable.class.name - "$SemanticAttributes.CODE_FUNCTION" "run" - "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" - } - - def "test empty current context is root context"() { - expect: - Context.current() == Context.root() - } - - // 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/instrumentation/opentelemetry-api/opentelemetry-api-1.0/javaagent/src/test/groovy/ContextTest.groovy b/instrumentation/opentelemetry-api/opentelemetry-api-1.0/javaagent/src/test/groovy/ContextTest.groovy deleted file mode 100644 index ba2d52a68708..000000000000 --- a/instrumentation/opentelemetry-api/opentelemetry-api-1.0/javaagent/src/test/groovy/ContextTest.groovy +++ /dev/null @@ -1,72 +0,0 @@ -/* - * 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/instrumentation/opentelemetry-api/opentelemetry-api-1.0/javaagent/src/test/groovy/TracerTest.groovy b/instrumentation/opentelemetry-api/opentelemetry-api-1.0/javaagent/src/test/groovy/TracerTest.groovy deleted file mode 100644 index 88fc4f53fd19..000000000000 --- a/instrumentation/opentelemetry-api/opentelemetry-api-1.0/javaagent/src/test/groovy/TracerTest.groovy +++ /dev/null @@ -1,357 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -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 - -import static io.opentelemetry.api.trace.SpanKind.PRODUCER -import static io.opentelemetry.api.trace.StatusCode.ERROR - -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" "java.lang.IllegalStateException" - "$SemanticAttributes.EXCEPTION_STACKTRACE" 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" "java.lang.IllegalStateException" - "$SemanticAttributes.EXCEPTION_STACKTRACE" 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() - } - - // this test uses opentelemetry-api-1.4 instrumentation - def "test tracer builder"() { - when: - def tracer = GlobalOpenTelemetry.get().tracerBuilder("test").setInstrumentationVersion("1.2.3").build() - def testSpan = tracer.spanBuilder("test").setSpanKind(PRODUCER).startSpan() - testSpan.end() - - then: - assertTraces(1) { - trace(0, 1) { - span(0) { - name "test" - kind PRODUCER - hasNoParent() - instrumentationLibraryVersion "1.2.3" - attributes { - } - } - } - } - } -} diff --git a/instrumentation/opentelemetry-api/opentelemetry-api-1.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/ContextBridgeTest.java b/instrumentation/opentelemetry-api/opentelemetry-api-1.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/ContextBridgeTest.java new file mode 100644 index 000000000000..d73dc4b80fa6 --- /dev/null +++ b/instrumentation/opentelemetry-api/opentelemetry-api-1.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/ContextBridgeTest.java @@ -0,0 +1,197 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.opentelemetryapi; + +import static io.opentelemetry.api.common.AttributeKey.stringKey; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.api.baggage.Baggage; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.ContextKey; +import io.opentelemetry.context.Scope; +import io.opentelemetry.instrumentation.annotations.WithSpan; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.semconv.incubating.CodeIncubatingAttributes; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicReference; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +class ContextBridgeTest { + + @RegisterExtension + private static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + private static final ContextKey ANIMAL = ContextKey.named("animal"); + + @Test + @DisplayName("agent propagates application's context") + void agentPropagatesApplicationsContext() throws Exception { + // When + Context context = Context.current().with(ANIMAL, "cat"); + AtomicReference captured = new AtomicReference<>(); + try (Scope ignored = context.makeCurrent()) { + Executors.newSingleThreadExecutor() + .submit(() -> captured.set(Context.current().get(ANIMAL))) + .get(); + } + + // Then + assertThat(captured.get()).isEqualTo("cat"); + } + + @Test + @DisplayName("application propagates agent's context") + void applicationPropagatesAgentsContext() { + // Given + Runnable runnable = + new Runnable() { + @WithSpan("test") + @Override + public 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 + Context context = Context.current(); + try (Scope ignored = Context.root().makeCurrent()) { + Span.current().setAttribute("dog", "no"); + try (Scope ignored2 = context.makeCurrent()) { + Span.current().setAttribute("cat", "yes"); + } + } + } + }; + + // When + runnable.run(); + // Then + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("test") + .hasNoParent() + .hasAttributesSatisfyingExactly( + equalTo( + CodeIncubatingAttributes.CODE_NAMESPACE, + runnable.getClass().getName()), + equalTo(CodeIncubatingAttributes.CODE_FUNCTION, "run"), + equalTo(stringKey("cat"), "yes")))); + } + + @Test + @DisplayName("agent propagates application's span") + void agentPropagatesApplicationsSpan() throws Exception { + // When + Tracer tracer = GlobalOpenTelemetry.getTracer("test"); + + Span testSpan = tracer.spanBuilder("test").startSpan(); + try (Scope ignored = testSpan.makeCurrent()) { + Executors.newSingleThreadExecutor() + .submit( + () -> { + Span.current().setAttribute("cat", "yes"); + }) + .get(); + } + testSpan.end(); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("test") + .hasNoParent() + .hasAttributesSatisfyingExactly(equalTo(stringKey("cat"), "yes")))); + } + + @Test + @DisplayName("application propagates agent's span") + void applicationPropagatesAgentsSpan() { + // Given + Runnable runnable = + new Runnable() { + @WithSpan("test") + @Override + public 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 + Span span = Span.current(); + try (Scope ignored = Context.root().makeCurrent()) { + Span.current().setAttribute("dog", "no"); + try (Scope ignored2 = span.makeCurrent()) { + Span.current().setAttribute("cat", "yes"); + } + } + } + }; + + // When + runnable.run(); + + // Then + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("test") + .hasNoParent() + .hasAttributesSatisfyingExactly( + equalTo( + CodeIncubatingAttributes.CODE_NAMESPACE, + runnable.getClass().getName()), + equalTo(CodeIncubatingAttributes.CODE_FUNCTION, "run"), + equalTo(stringKey("cat"), "yes")))); + } + + @Test + @DisplayName("agent propagates application's baggage") + void agentPropagatesApplicationsBaggage() throws Exception { + // When + Baggage testBaggage = Baggage.builder().put("cat", "yes").build(); + AtomicReference ref = new AtomicReference<>(); + CountDownLatch latch = new CountDownLatch(1); + try (Scope ignored = testBaggage.makeCurrent()) { + Executors.newSingleThreadExecutor() + .submit( + () -> { + ref.set(Baggage.current()); + latch.countDown(); + }) + .get(); + } + + // Then + latch.await(); + assertThat(ref.get().size()).isEqualTo(1); + assertThat(ref.get().getEntryValue("cat")).isEqualTo("yes"); + } + + @Test + @DisplayName("test empty current context is root context") + void testEmptyCurrentContextIsRootContext() { + // Expect + assertThat(Context.current()).isEqualTo(Context.root()); + } + + // 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/instrumentation/opentelemetry-api/opentelemetry-api-1.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/ContextTest.java b/instrumentation/opentelemetry-api/opentelemetry-api-1.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/ContextTest.java new file mode 100644 index 000000000000..6f90fd3bffea --- /dev/null +++ b/instrumentation/opentelemetry-api/opentelemetry-api-1.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/ContextTest.java @@ -0,0 +1,90 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.opentelemetryapi; + +import static org.assertj.core.api.Assertions.assertThat; + +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 org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class ContextTest { + + @Test + @DisplayName("Span.current() should return invalid") + void spanCurrentShouldReturnInvalid() { + // When + Span span = Span.current(); + // Then + assertThat(span.getSpanContext().isValid()).isFalse(); + } + + @Test + @DisplayName("Span.current() should return span") + void spanCurrentShouldReturnSpan() { + // When + Tracer tracer = GlobalOpenTelemetry.getTracer("test"); + Span testSpan = tracer.spanBuilder("test").startSpan(); + try (Scope ignored = testSpan.makeCurrent()) { + Span span = Span.current(); + + // Then + assertThat(span).isEqualTo(testSpan); + } + } + + @Test + @DisplayName("Span.fromContext should return invalid") + void spanFromContextShouldReturnInvalid() { + // When + Span span = Span.fromContext(Context.current()); + + // Then + assertThat(span.getSpanContext().isValid()).isFalse(); + } + + @Test + @DisplayName("getSpan should return span") + void getSpanShouldReturnSpan() { + // When + Tracer tracer = GlobalOpenTelemetry.getTracer("test"); + Span testSpan = tracer.spanBuilder("test").startSpan(); + try (Scope ignored = testSpan.makeCurrent()) { + Span span = Span.fromContext(Context.current()); + + // Then + assertThat(span).isEqualTo(testSpan); + } + } + + @Test + @DisplayName("Span.fromContextOrNull should return null") + void spanFromContextOrNullShouldReturnNull() { + // When + Span span = Span.fromContextOrNull(Context.current()); + + // Then + assertThat(span).isNull(); + } + + @Test + @DisplayName("Span.fromContextOrNull should return span") + void spanFromContextOrNullShouldReturnSpan() { + // When + Tracer tracer = GlobalOpenTelemetry.getTracer("test"); + Span testSpan = tracer.spanBuilder("test").startSpan(); + try (Scope ignored = testSpan.makeCurrent()) { + Span span = Span.fromContextOrNull(Context.current()); + + // Then + assertThat(span).isEqualTo(testSpan); + } + } +} diff --git a/instrumentation/opentelemetry-api/opentelemetry-api-1.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/TracerTest.java b/instrumentation/opentelemetry-api/opentelemetry-api-1.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/TracerTest.java new file mode 100644 index 000000000000..404d47131fa6 --- /dev/null +++ b/instrumentation/opentelemetry-api/opentelemetry-api-1.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/TracerTest.java @@ -0,0 +1,328 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.opentelemetryapi; + +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.trace.SpanKind.PRODUCER; +import static io.opentelemetry.api.trace.StatusCode.ERROR; +import static io.opentelemetry.instrumentation.testing.util.TelemetryDataUtil.orderByRootSpanName; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.sdk.common.InstrumentationScopeInfo; +import io.opentelemetry.sdk.trace.data.StatusData; +import io.opentelemetry.semconv.ExceptionAttributes; +import java.io.PrintWriter; +import java.io.StringWriter; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +class TracerTest { + + @RegisterExtension + private static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + @Test + @DisplayName("capture span, kind, attributes, and status") + void captureSpanKindAttributesAndStatus() { + // When + Tracer tracer = GlobalOpenTelemetry.getTracer("test"); + Span testSpan = tracer.spanBuilder("test").setSpanKind(PRODUCER).startSpan(); + testSpan.setAttribute("string", "1"); + testSpan.setAttribute("long", 2L); + testSpan.setAttribute("double", 3.0); + testSpan.setAttribute("boolean", true); + testSpan.setStatus(ERROR); + testSpan.end(); + + // Then + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("test") + .hasKind(PRODUCER) + .hasNoParent() + .hasStatus(StatusData.error()) + .hasAttributesSatisfyingExactly( + equalTo(stringKey("string"), "1"), + equalTo(longKey("long"), 2L), + equalTo(doubleKey("double"), 3.0), + equalTo(booleanKey("boolean"), true)))); + } + + @Test + @DisplayName("capture span with implicit parent using Tracer.withSpan()") + void captureSpanWithImplicitParentUsingTracerWithSpan() { + // When + Tracer tracer = GlobalOpenTelemetry.getTracer("test"); + Span parentSpan = tracer.spanBuilder("parent").startSpan(); + Scope parentScope = Context.current().with(parentSpan).makeCurrent(); + + Span testSpan = tracer.spanBuilder("test").startSpan(); + testSpan.end(); + + parentSpan.end(); + parentScope.close(); + + // Then + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasNoParent().hasTotalAttributeCount(0), + span -> + span.hasName("test").hasParent(trace.getSpan(0)).hasTotalAttributeCount(0))); + } + + @Test + @DisplayName("capture span with implicit parent using makeCurrent") + void captureSpanWithImplicitParentUsingMakeCurrent() { + // When + Tracer tracer = GlobalOpenTelemetry.getTracer("test"); + Span parentSpan = tracer.spanBuilder("parent").startSpan(); + Scope parentScope = parentSpan.makeCurrent(); + + Span testSpan = tracer.spanBuilder("test").startSpan(); + testSpan.end(); + + parentSpan.end(); + parentScope.close(); + + // Then + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasNoParent().hasTotalAttributeCount(0), + span -> + span.hasName("test").hasParent(trace.getSpan(0)).hasTotalAttributeCount(0))); + } + + @Test + @DisplayName( + "capture span with implicit parent using TracingContextUtils.withSpan and makeCurrent") + void captureSpanWithImplicitParentUsingTracingContextUtilsWithSpanAndMakeCurrent() { + // When + Tracer tracer = GlobalOpenTelemetry.getTracer("test"); + Span parentSpan = tracer.spanBuilder("parent").startSpan(); + Context parentContext = Context.current().with(parentSpan); + Scope parentScope = parentContext.makeCurrent(); + + Span testSpan = tracer.spanBuilder("test").startSpan(); + testSpan.end(); + + parentSpan.end(); + parentScope.close(); + + // Then + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasNoParent().hasTotalAttributeCount(0), + span -> + span.hasName("test").hasParent(trace.getSpan(0)).hasTotalAttributeCount(0))); + } + + @Test + @DisplayName("capture span with explicit parent") + void captureSpanWithExplicitParent() { + // When + Tracer tracer = GlobalOpenTelemetry.getTracer("test"); + Span parentSpan = tracer.spanBuilder("parent").startSpan(); + Context context = Context.root().with(parentSpan); + Span testSpan = tracer.spanBuilder("test").setParent(context).startSpan(); + testSpan.end(); + parentSpan.end(); + + // Then + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasNoParent().hasTotalAttributeCount(0), + span -> + span.hasName("test").hasParent(trace.getSpan(0)).hasTotalAttributeCount(0))); + } + + @Test + @DisplayName("capture span with explicit no parent") + void captureSpanWithExplicitNoParent() { + // When + Tracer tracer = GlobalOpenTelemetry.getTracer("test"); + Span parentSpan = tracer.spanBuilder("parent").startSpan(); + Scope parentScope = parentSpan.makeCurrent(); + Span testSpan = tracer.spanBuilder("test").setNoParent().startSpan(); + testSpan.end(); + parentSpan.end(); + parentScope.close(); + + // Then + testing.waitAndAssertSortedTraces( + orderByRootSpanName("parent", "test"), + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasNoParent().hasTotalAttributeCount(0)), + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("test").hasNoParent().hasTotalAttributeCount(0))); + } + + @Test + @DisplayName("capture name update") + void captureNameUpdate() { + // When + Tracer tracer = GlobalOpenTelemetry.getTracer("test"); + Span testSpan = tracer.spanBuilder("test").startSpan(); + testSpan.updateName("test2"); + testSpan.end(); + + // Then + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("test2").hasNoParent().hasTotalAttributeCount(0))); + } + + @Test + @DisplayName("capture exception") + void captureException() { + // When + Tracer tracer = GlobalOpenTelemetry.getTracer("test"); + Span testSpan = tracer.spanBuilder("test").startSpan(); + IllegalStateException throwable = new IllegalStateException(); + testSpan.recordException(throwable); + testSpan.end(); + + StringWriter writer = new StringWriter(); + throwable.printStackTrace(new PrintWriter(writer)); + + // Then + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("test").hasTotalAttributeCount(0).hasException(throwable))); + } + + @Test + @DisplayName("capture exception with Attributes") + void captureExceptionWithAttributes() { + // When + Tracer tracer = GlobalOpenTelemetry.getTracer("test"); + Span testSpan = tracer.spanBuilder("test").startSpan(); + IllegalStateException throwable = new IllegalStateException(); + testSpan.recordException(throwable, Attributes.builder().put("dog", "bark").build()); + testSpan.end(); + + StringWriter writer = new StringWriter(); + throwable.printStackTrace(new PrintWriter(writer)); + + // Then + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("test") + .hasTotalAttributeCount(0) + .hasEventsSatisfyingExactly( + event -> + event + .hasName("exception") + .hasAttributesSatisfyingExactly( + equalTo( + ExceptionAttributes.EXCEPTION_TYPE, + "java.lang.IllegalStateException"), + equalTo( + ExceptionAttributes.EXCEPTION_STACKTRACE, + writer.toString()), + equalTo(stringKey("dog"), "bark"))))); + } + + @Test + @DisplayName("capture name update using TracingContextUtils.getCurrentSpan()") + void captureNameUpdateUsingTracingContextUtilsGetCurrentSpan() { + // When + Tracer tracer = GlobalOpenTelemetry.getTracer("test"); + Span testSpan = tracer.spanBuilder("test").startSpan(); + Scope testScope = Context.current().with(testSpan).makeCurrent(); + Span.current().updateName("test2"); + testScope.close(); + testSpan.end(); + + // Then + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("test2").hasNoParent().hasTotalAttributeCount(0))); + } + + @Test + @DisplayName("capture name update using TracingContextUtils.Span.fromContext(Context.current())") + void captureNameUpdateUsingTracingContextUtilsSpanFromContextCurrent() { + // When + Tracer tracer = GlobalOpenTelemetry.getTracer("test"); + Span testSpan = tracer.spanBuilder("test").startSpan(); + Scope testScope = Context.current().with(testSpan).makeCurrent(); + Span.fromContext(Context.current()).updateName("test2"); + testScope.close(); + testSpan.end(); + + // Then + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("test2").hasNoParent().hasTotalAttributeCount(0))); + } + + @Test + @DisplayName("add wrapped span to context") + void addWrappedSpanToContext() { + // When + // Lazy way to get a span context + Tracer tracer = GlobalOpenTelemetry.getTracer("test"); + Span testSpan = tracer.spanBuilder("test").setSpanKind(PRODUCER).startSpan(); + testSpan.end(); + + Span span = Span.wrap(testSpan.getSpanContext()); + Context context = Context.current().with(span); + + // Then + assertThat(Span.fromContext(context).getSpanContext().getSpanId()) + .isEqualTo(span.getSpanContext().getSpanId()); + } + + // this test uses opentelemetry-api-1.4 instrumentation + @Test + @DisplayName("test tracer builder") + void testTracerBuilder() { + // When + Tracer tracer = + GlobalOpenTelemetry.get().tracerBuilder("test").setInstrumentationVersion("1.2.3").build(); + Span testSpan = tracer.spanBuilder("test").setSpanKind(PRODUCER).startSpan(); + testSpan.end(); + + // Then + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("test") + .hasKind(PRODUCER) + .hasNoParent() + .hasTotalAttributeCount(0) + .hasInstrumentationScopeInfo( + InstrumentationScopeInfo.builder("test").setVersion("1.2.3").build()))); + } +} diff --git a/instrumentation/opentelemetry-api/opentelemetry-api-1.10/javaagent/build.gradle.kts b/instrumentation/opentelemetry-api/opentelemetry-api-1.10/javaagent/build.gradle.kts index de5df0b95397..fd028894c92f 100644 --- a/instrumentation/opentelemetry-api/opentelemetry-api-1.10/javaagent/build.gradle.kts +++ b/instrumentation/opentelemetry-api/opentelemetry-api-1.10/javaagent/build.gradle.kts @@ -7,3 +7,11 @@ dependencies { implementation(project(":instrumentation:opentelemetry-api:opentelemetry-api-1.0:javaagent")) implementation(project(":instrumentation:opentelemetry-api:opentelemetry-api-1.4:javaagent")) } + +configurations.configureEach { + if (name == "testRuntimeClasspath" || name == "testCompileClasspath") { + resolutionStrategy { + force("io.opentelemetry:opentelemetry-api:1.10.0") + } + } +} diff --git a/instrumentation/opentelemetry-api/opentelemetry-api-1.10/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_10/OpenTelemetryApiInstrumentationModule.java b/instrumentation/opentelemetry-api/opentelemetry-api-1.10/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_10/OpenTelemetryApiInstrumentationModule.java index 620c6b68a40c..81506ec514d8 100644 --- a/instrumentation/opentelemetry-api/opentelemetry-api-1.10/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_10/OpenTelemetryApiInstrumentationModule.java +++ b/instrumentation/opentelemetry-api/opentelemetry-api-1.10/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_10/OpenTelemetryApiInstrumentationModule.java @@ -10,10 +10,12 @@ 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.internal.ExperimentalInstrumentationModule; import java.util.List; @AutoService(InstrumentationModule.class) -public class OpenTelemetryApiInstrumentationModule extends InstrumentationModule { +public class OpenTelemetryApiInstrumentationModule extends InstrumentationModule + implements ExperimentalInstrumentationModule { public OpenTelemetryApiInstrumentationModule() { super("opentelemetry-api", "opentelemetry-api-1.10"); @@ -23,4 +25,9 @@ public OpenTelemetryApiInstrumentationModule() { public List typeInstrumentations() { return singletonList(new OpenTelemetryInstrumentation()); } + + @Override + public String getModuleGroup() { + return "opentelemetry-api-bridge"; + } } diff --git a/instrumentation/opentelemetry-api/opentelemetry-api-1.10/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_10/metrics/ApplicationDoubleCounterBuilder.java b/instrumentation/opentelemetry-api/opentelemetry-api-1.10/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_10/metrics/ApplicationDoubleCounterBuilder.java index 3243f5dd0144..0dacb8afdec0 100644 --- a/instrumentation/opentelemetry-api/opentelemetry-api-1.10/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_10/metrics/ApplicationDoubleCounterBuilder.java +++ b/instrumentation/opentelemetry-api/opentelemetry-api-1.10/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_10/metrics/ApplicationDoubleCounterBuilder.java @@ -12,11 +12,12 @@ import com.google.errorprone.annotations.CanIgnoreReturnValue; import java.util.function.Consumer; -final class ApplicationDoubleCounterBuilder implements DoubleCounterBuilder { +public class ApplicationDoubleCounterBuilder implements DoubleCounterBuilder { private final io.opentelemetry.api.metrics.DoubleCounterBuilder agentBuilder; - ApplicationDoubleCounterBuilder(io.opentelemetry.api.metrics.DoubleCounterBuilder agentBuilder) { + protected ApplicationDoubleCounterBuilder( + io.opentelemetry.api.metrics.DoubleCounterBuilder agentBuilder) { this.agentBuilder = agentBuilder; } diff --git a/instrumentation/opentelemetry-api/opentelemetry-api-1.10/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_10/metrics/ApplicationDoubleGaugeBuilder.java b/instrumentation/opentelemetry-api/opentelemetry-api-1.10/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_10/metrics/ApplicationDoubleGaugeBuilder.java index 56c496abb505..365a6bcb5a6f 100644 --- a/instrumentation/opentelemetry-api/opentelemetry-api-1.10/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_10/metrics/ApplicationDoubleGaugeBuilder.java +++ b/instrumentation/opentelemetry-api/opentelemetry-api-1.10/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_10/metrics/ApplicationDoubleGaugeBuilder.java @@ -12,11 +12,12 @@ import com.google.errorprone.annotations.CanIgnoreReturnValue; import java.util.function.Consumer; -final class ApplicationDoubleGaugeBuilder implements DoubleGaugeBuilder { +public class ApplicationDoubleGaugeBuilder implements DoubleGaugeBuilder { private final io.opentelemetry.api.metrics.DoubleGaugeBuilder agentBuilder; - ApplicationDoubleGaugeBuilder(io.opentelemetry.api.metrics.DoubleGaugeBuilder agentBuilder) { + public ApplicationDoubleGaugeBuilder( + io.opentelemetry.api.metrics.DoubleGaugeBuilder agentBuilder) { this.agentBuilder = agentBuilder; } diff --git a/instrumentation/opentelemetry-api/opentelemetry-api-1.10/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_10/metrics/ApplicationDoubleHistogramBuilder.java b/instrumentation/opentelemetry-api/opentelemetry-api-1.10/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_10/metrics/ApplicationDoubleHistogramBuilder.java index 41a228acfc1b..f08ef581ebc1 100644 --- a/instrumentation/opentelemetry-api/opentelemetry-api-1.10/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_10/metrics/ApplicationDoubleHistogramBuilder.java +++ b/instrumentation/opentelemetry-api/opentelemetry-api-1.10/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_10/metrics/ApplicationDoubleHistogramBuilder.java @@ -10,11 +10,11 @@ import application.io.opentelemetry.api.metrics.LongHistogramBuilder; import com.google.errorprone.annotations.CanIgnoreReturnValue; -final class ApplicationDoubleHistogramBuilder implements DoubleHistogramBuilder { +public class ApplicationDoubleHistogramBuilder implements DoubleHistogramBuilder { private final io.opentelemetry.api.metrics.DoubleHistogramBuilder agentBuilder; - ApplicationDoubleHistogramBuilder( + public ApplicationDoubleHistogramBuilder( io.opentelemetry.api.metrics.DoubleHistogramBuilder agentBuilder) { this.agentBuilder = agentBuilder; } diff --git a/instrumentation/opentelemetry-api/opentelemetry-api-1.10/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_10/metrics/ApplicationDoubleUpDownCounterBuilder.java b/instrumentation/opentelemetry-api/opentelemetry-api-1.10/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_10/metrics/ApplicationDoubleUpDownCounterBuilder.java index 9095895abaaf..43a3569112b2 100644 --- a/instrumentation/opentelemetry-api/opentelemetry-api-1.10/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_10/metrics/ApplicationDoubleUpDownCounterBuilder.java +++ b/instrumentation/opentelemetry-api/opentelemetry-api-1.10/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_10/metrics/ApplicationDoubleUpDownCounterBuilder.java @@ -12,11 +12,11 @@ import com.google.errorprone.annotations.CanIgnoreReturnValue; import java.util.function.Consumer; -final class ApplicationDoubleUpDownCounterBuilder implements DoubleUpDownCounterBuilder { +public class ApplicationDoubleUpDownCounterBuilder implements DoubleUpDownCounterBuilder { private final io.opentelemetry.api.metrics.DoubleUpDownCounterBuilder agentBuilder; - ApplicationDoubleUpDownCounterBuilder( + protected ApplicationDoubleUpDownCounterBuilder( io.opentelemetry.api.metrics.DoubleUpDownCounterBuilder agentBuilder) { this.agentBuilder = agentBuilder; } diff --git a/instrumentation/opentelemetry-api/opentelemetry-api-1.10/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_10/metrics/ApplicationLongCounterBuilder.java b/instrumentation/opentelemetry-api/opentelemetry-api-1.10/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_10/metrics/ApplicationLongCounterBuilder.java index c38360d9d892..4251c93c5396 100644 --- a/instrumentation/opentelemetry-api/opentelemetry-api-1.10/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_10/metrics/ApplicationLongCounterBuilder.java +++ b/instrumentation/opentelemetry-api/opentelemetry-api-1.10/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_10/metrics/ApplicationLongCounterBuilder.java @@ -13,11 +13,12 @@ import com.google.errorprone.annotations.CanIgnoreReturnValue; import java.util.function.Consumer; -final class ApplicationLongCounterBuilder implements LongCounterBuilder { +public class ApplicationLongCounterBuilder implements LongCounterBuilder { private final io.opentelemetry.api.metrics.LongCounterBuilder agentBuilder; - ApplicationLongCounterBuilder(io.opentelemetry.api.metrics.LongCounterBuilder agentBuilder) { + public ApplicationLongCounterBuilder( + io.opentelemetry.api.metrics.LongCounterBuilder agentBuilder) { this.agentBuilder = agentBuilder; } diff --git a/instrumentation/opentelemetry-api/opentelemetry-api-1.10/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_10/metrics/ApplicationLongGaugeBuilder.java b/instrumentation/opentelemetry-api/opentelemetry-api-1.10/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_10/metrics/ApplicationLongGaugeBuilder.java index 34d3f395b993..552dba2e297e 100644 --- a/instrumentation/opentelemetry-api/opentelemetry-api-1.10/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_10/metrics/ApplicationLongGaugeBuilder.java +++ b/instrumentation/opentelemetry-api/opentelemetry-api-1.10/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_10/metrics/ApplicationLongGaugeBuilder.java @@ -11,11 +11,12 @@ import com.google.errorprone.annotations.CanIgnoreReturnValue; import java.util.function.Consumer; -final class ApplicationLongGaugeBuilder implements LongGaugeBuilder { +public class ApplicationLongGaugeBuilder implements LongGaugeBuilder { private final io.opentelemetry.api.metrics.LongGaugeBuilder agentBuilder; - ApplicationLongGaugeBuilder(io.opentelemetry.api.metrics.LongGaugeBuilder agentBuilder) { + protected ApplicationLongGaugeBuilder( + io.opentelemetry.api.metrics.LongGaugeBuilder agentBuilder) { this.agentBuilder = agentBuilder; } diff --git a/instrumentation/opentelemetry-api/opentelemetry-api-1.10/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_10/metrics/ApplicationLongHistogramBuilder.java b/instrumentation/opentelemetry-api/opentelemetry-api-1.10/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_10/metrics/ApplicationLongHistogramBuilder.java index 3ddd92ef53b6..f96523d75c70 100644 --- a/instrumentation/opentelemetry-api/opentelemetry-api-1.10/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_10/metrics/ApplicationLongHistogramBuilder.java +++ b/instrumentation/opentelemetry-api/opentelemetry-api-1.10/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_10/metrics/ApplicationLongHistogramBuilder.java @@ -9,11 +9,12 @@ import application.io.opentelemetry.api.metrics.LongHistogramBuilder; import com.google.errorprone.annotations.CanIgnoreReturnValue; -final class ApplicationLongHistogramBuilder implements LongHistogramBuilder { +public class ApplicationLongHistogramBuilder implements LongHistogramBuilder { private final io.opentelemetry.api.metrics.LongHistogramBuilder agentBuilder; - ApplicationLongHistogramBuilder(io.opentelemetry.api.metrics.LongHistogramBuilder agentBuilder) { + protected ApplicationLongHistogramBuilder( + io.opentelemetry.api.metrics.LongHistogramBuilder agentBuilder) { this.agentBuilder = agentBuilder; } diff --git a/instrumentation/opentelemetry-api/opentelemetry-api-1.10/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_10/metrics/ApplicationLongUpDownCounterBuilder.java b/instrumentation/opentelemetry-api/opentelemetry-api-1.10/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_10/metrics/ApplicationLongUpDownCounterBuilder.java index bdee17ed731c..5471c4cddf4e 100644 --- a/instrumentation/opentelemetry-api/opentelemetry-api-1.10/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_10/metrics/ApplicationLongUpDownCounterBuilder.java +++ b/instrumentation/opentelemetry-api/opentelemetry-api-1.10/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_10/metrics/ApplicationLongUpDownCounterBuilder.java @@ -13,11 +13,11 @@ import com.google.errorprone.annotations.CanIgnoreReturnValue; import java.util.function.Consumer; -final class ApplicationLongUpDownCounterBuilder implements LongUpDownCounterBuilder { +public class ApplicationLongUpDownCounterBuilder implements LongUpDownCounterBuilder { private final io.opentelemetry.api.metrics.LongUpDownCounterBuilder agentBuilder; - ApplicationLongUpDownCounterBuilder( + public ApplicationLongUpDownCounterBuilder( io.opentelemetry.api.metrics.LongUpDownCounterBuilder agentBuilder) { this.agentBuilder = agentBuilder; } diff --git a/instrumentation/opentelemetry-api/opentelemetry-api-1.10/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_10/metrics/MeterTest.java b/instrumentation/opentelemetry-api/opentelemetry-api-1.10/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_10/metrics/MeterTest.java index 65a798b93bec..2213bac3006f 100644 --- a/instrumentation/opentelemetry-api/opentelemetry-api-1.10/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_10/metrics/MeterTest.java +++ b/instrumentation/opentelemetry-api/opentelemetry-api-1.10/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_10/metrics/MeterTest.java @@ -25,6 +25,7 @@ import io.opentelemetry.api.metrics.ObservableLongUpDownCounter; import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; import io.opentelemetry.sdk.common.InstrumentationScopeInfo; +import java.lang.reflect.Method; import org.assertj.core.api.AbstractIterableAssert; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -116,7 +117,7 @@ void observableLongCounter() throws InterruptedException { .hasAttributesSatisfying( equalTo(AttributeKey.stringKey("q"), "r")))))); - observableCounter.close(); + close(observableCounter); // sleep exporter interval Thread.sleep(100); @@ -192,7 +193,7 @@ void observableDoubleCounter() throws InterruptedException { .hasAttributesSatisfying( equalTo(AttributeKey.stringKey("q"), "r")))))); - observableCounter.close(); + close(observableCounter); // sleep exporter interval Thread.sleep(100); @@ -267,7 +268,7 @@ void observableLongUpDownCounter() throws InterruptedException { .hasAttributesSatisfying( equalTo(AttributeKey.stringKey("q"), "r")))))); - observableCounter.close(); + close(observableCounter); // sleep exporter interval Thread.sleep(100); @@ -343,7 +344,7 @@ void observableDoubleUpDownCounter() throws InterruptedException { .hasAttributesSatisfying( equalTo(AttributeKey.stringKey("q"), "r")))))); - observableCounter.close(); + close(observableCounter); // sleep exporter interval Thread.sleep(100); @@ -448,7 +449,7 @@ void longGauge() throws InterruptedException { .hasAttributesSatisfying( equalTo(AttributeKey.stringKey("q"), "r")))))); - observableGauge.close(); + close(observableGauge); // sleep exporter interval Thread.sleep(100); @@ -490,7 +491,7 @@ void doubleGauge() throws InterruptedException { .hasAttributesSatisfying( equalTo(AttributeKey.stringKey("q"), "r")))))); - observableGauge.close(); + close(observableGauge); // sleep exporter interval Thread.sleep(100); @@ -499,4 +500,14 @@ void doubleGauge() throws InterruptedException { testing.waitAndAssertMetrics(instrumentationName, "test", AbstractIterableAssert::isEmpty); } + + private static void close(Object observableInstrument) { + // our bridge includes close method, although it was added in 1.12 + try { + Method close = observableInstrument.getClass().getDeclaredMethod("close"); + close.invoke(observableInstrument); + } catch (Exception exception) { + throw new IllegalStateException("Failed to call close", exception); + } + } } diff --git a/instrumentation/opentelemetry-api/opentelemetry-api-1.15/javaagent/build.gradle.kts b/instrumentation/opentelemetry-api/opentelemetry-api-1.15/javaagent/build.gradle.kts index d4a4f8701cb6..4b91c4e5f5da 100644 --- a/instrumentation/opentelemetry-api/opentelemetry-api-1.15/javaagent/build.gradle.kts +++ b/instrumentation/opentelemetry-api/opentelemetry-api-1.15/javaagent/build.gradle.kts @@ -8,3 +8,11 @@ dependencies { implementation(project(":instrumentation:opentelemetry-api:opentelemetry-api-1.4:javaagent")) implementation(project(":instrumentation:opentelemetry-api:opentelemetry-api-1.10:javaagent")) } + +configurations.configureEach { + if (name == "testRuntimeClasspath" || name == "testCompileClasspath") { + resolutionStrategy { + force("io.opentelemetry:opentelemetry-api:1.15.0") + } + } +} diff --git a/instrumentation/opentelemetry-api/opentelemetry-api-1.15/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_15/OpenTelemetryApiInstrumentationModule.java b/instrumentation/opentelemetry-api/opentelemetry-api-1.15/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_15/OpenTelemetryApiInstrumentationModule.java index 217af84506ff..8793787e71b6 100644 --- a/instrumentation/opentelemetry-api/opentelemetry-api-1.15/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_15/OpenTelemetryApiInstrumentationModule.java +++ b/instrumentation/opentelemetry-api/opentelemetry-api-1.15/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_15/OpenTelemetryApiInstrumentationModule.java @@ -10,10 +10,12 @@ 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.internal.ExperimentalInstrumentationModule; import java.util.List; @AutoService(InstrumentationModule.class) -public class OpenTelemetryApiInstrumentationModule extends InstrumentationModule { +public class OpenTelemetryApiInstrumentationModule extends InstrumentationModule + implements ExperimentalInstrumentationModule { public OpenTelemetryApiInstrumentationModule() { super("opentelemetry-api", "opentelemetry-api-1.15"); } @@ -22,4 +24,9 @@ public OpenTelemetryApiInstrumentationModule() { public List typeInstrumentations() { return singletonList(new OpenTelemetryInstrumentation()); } + + @Override + public String getModuleGroup() { + return "opentelemetry-api-bridge"; + } } diff --git a/instrumentation/opentelemetry-api/opentelemetry-api-1.15/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_15/metrics/ApplicationMeter115.java b/instrumentation/opentelemetry-api/opentelemetry-api-1.15/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_15/metrics/ApplicationMeter115.java index 3d0518771d8f..2e28e9f068fb 100644 --- a/instrumentation/opentelemetry-api/opentelemetry-api-1.15/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_15/metrics/ApplicationMeter115.java +++ b/instrumentation/opentelemetry-api/opentelemetry-api-1.15/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_15/metrics/ApplicationMeter115.java @@ -10,11 +10,11 @@ import io.opentelemetry.javaagent.instrumentation.opentelemetryapi.v1_10.metrics.ApplicationMeter; import io.opentelemetry.javaagent.instrumentation.opentelemetryapi.v1_10.metrics.ObservableMeasurementWrapper; -class ApplicationMeter115 extends ApplicationMeter { +public class ApplicationMeter115 extends ApplicationMeter { private final io.opentelemetry.api.metrics.Meter agentMeter; - ApplicationMeter115(io.opentelemetry.api.metrics.Meter agentMeter) { + protected ApplicationMeter115(io.opentelemetry.api.metrics.Meter agentMeter) { super(agentMeter); this.agentMeter = agentMeter; } diff --git a/instrumentation/opentelemetry-api/opentelemetry-api-1.27/javaagent/build.gradle.kts b/instrumentation/opentelemetry-api/opentelemetry-api-1.27/javaagent/build.gradle.kts index fd89755d62b1..bbbb75e6da99 100644 --- a/instrumentation/opentelemetry-api/opentelemetry-api-1.27/javaagent/build.gradle.kts +++ b/instrumentation/opentelemetry-api/opentelemetry-api-1.27/javaagent/build.gradle.kts @@ -9,3 +9,11 @@ dependencies { implementation(project(":instrumentation:opentelemetry-api:opentelemetry-api-1.10:javaagent")) implementation(project(":instrumentation:opentelemetry-api:opentelemetry-api-1.15:javaagent")) } + +configurations.configureEach { + if (name == "testRuntimeClasspath" || name == "testCompileClasspath") { + resolutionStrategy { + force("io.opentelemetry:opentelemetry-api:1.27.0") + } + } +} diff --git a/instrumentation/opentelemetry-api/opentelemetry-api-1.27/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_27/ApplicationOpenTelemetry127.java b/instrumentation/opentelemetry-api/opentelemetry-api-1.27/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_27/ApplicationOpenTelemetry127.java index 2b66f448dd78..148e3e7d9000 100644 --- a/instrumentation/opentelemetry-api/opentelemetry-api-1.27/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_27/ApplicationOpenTelemetry127.java +++ b/instrumentation/opentelemetry-api/opentelemetry-api-1.27/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_27/ApplicationOpenTelemetry127.java @@ -11,10 +11,12 @@ 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.v1_10.metrics.ApplicationMeterFactory; import io.opentelemetry.javaagent.instrumentation.opentelemetryapi.v1_10.metrics.ApplicationMeterProvider; import io.opentelemetry.javaagent.instrumentation.opentelemetryapi.v1_15.metrics.ApplicationMeterFactory115; import io.opentelemetry.javaagent.instrumentation.opentelemetryapi.v1_27.logs.ApplicationLoggerProvider; import io.opentelemetry.javaagent.instrumentation.opentelemetryapi.v1_4.trace.ApplicationTracerProvider14; +import java.lang.reflect.InvocationTargetException; public final class ApplicationOpenTelemetry127 implements OpenTelemetry { @@ -36,8 +38,7 @@ private ApplicationOpenTelemetry127() { applicationContextPropagators = new ApplicationContextPropagators(agentOpenTelemetry.getPropagators()); applicationMeterProvider = - new ApplicationMeterProvider( - new ApplicationMeterFactory115(), agentOpenTelemetry.getMeterProvider()); + new ApplicationMeterProvider(getMeterFactory(), agentOpenTelemetry.getMeterProvider()); applicationLoggerProvider = new ApplicationLoggerProvider(agentOpenTelemetry.getLogsBridge()); } @@ -60,4 +61,59 @@ public LoggerProvider getLogsBridge() { public ContextPropagators getPropagators() { return applicationContextPropagators; } + + private static ApplicationMeterFactory getMeterFactory() { + // this class is defined in opentelemetry-api-1.38 + ApplicationMeterFactory meterFactory = + getMeterFactory( + "io.opentelemetry.javaagent.instrumentation.opentelemetryapi.v1_38.incubator.metrics.ApplicationMeterFactory138Incubator"); + if (meterFactory == null) { + // this class is defined in opentelemetry-api-1.38 + meterFactory = + getMeterFactory( + "io.opentelemetry.javaagent.instrumentation.opentelemetryapi.v1_38.metrics.ApplicationMeterFactory138"); + } + if (meterFactory == null) { + // this class is defined in opentelemetry-api-1.37 + meterFactory = + getMeterFactory( + "io.opentelemetry.javaagent.instrumentation.opentelemetryapi.v1_37.incubator.metrics.ApplicationMeterFactory137"); + } + if (meterFactory == null) { + // this class is defined in opentelemetry-api-1.32 + meterFactory = + getMeterFactory( + "io.opentelemetry.javaagent.instrumentation.opentelemetryapi.v1_32.incubator.metrics.ApplicationMeterFactory132Incubator"); + } + if (meterFactory == null) { + // this class is defined in opentelemetry-api-1.32 + meterFactory = + getMeterFactory( + "io.opentelemetry.javaagent.instrumentation.opentelemetryapi.v1_32.metrics.ApplicationMeterFactory132"); + } + if (meterFactory == null) { + // this class is defined in opentelemetry-api-1.31 + meterFactory = + getMeterFactory( + "io.opentelemetry.javaagent.instrumentation.opentelemetryapi.v1_31.incubator.metrics.ApplicationMeterFactory131"); + } + if (meterFactory == null) { + meterFactory = new ApplicationMeterFactory115(); + } + + return meterFactory; + } + + private static ApplicationMeterFactory getMeterFactory(String className) { + try { + Class clazz = Class.forName(className); + return (ApplicationMeterFactory) clazz.getConstructor().newInstance(); + } catch (ClassNotFoundException + | NoSuchMethodException + | InstantiationException + | IllegalAccessException + | InvocationTargetException exception) { + return null; + } + } } diff --git a/instrumentation/opentelemetry-api/opentelemetry-api-1.27/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_27/OpenTelemetryApiInstrumentationModule.java b/instrumentation/opentelemetry-api/opentelemetry-api-1.27/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_27/OpenTelemetryApiInstrumentationModule.java index 1949f24cd1ef..aed58d617504 100644 --- a/instrumentation/opentelemetry-api/opentelemetry-api-1.27/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_27/OpenTelemetryApiInstrumentationModule.java +++ b/instrumentation/opentelemetry-api/opentelemetry-api-1.27/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_27/OpenTelemetryApiInstrumentationModule.java @@ -10,10 +10,12 @@ 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.internal.ExperimentalInstrumentationModule; import java.util.List; @AutoService(InstrumentationModule.class) -public class OpenTelemetryApiInstrumentationModule extends InstrumentationModule { +public class OpenTelemetryApiInstrumentationModule extends InstrumentationModule + implements ExperimentalInstrumentationModule { public OpenTelemetryApiInstrumentationModule() { super("opentelemetry-api", "opentelemetry-api-1.27"); } @@ -22,4 +24,9 @@ public OpenTelemetryApiInstrumentationModule() { public List typeInstrumentations() { return singletonList(new OpenTelemetryInstrumentation()); } + + @Override + public String getModuleGroup() { + return "opentelemetry-api-bridge"; + } } diff --git a/instrumentation/opentelemetry-api/opentelemetry-api-1.31/javaagent/build.gradle.kts b/instrumentation/opentelemetry-api/opentelemetry-api-1.31/javaagent/build.gradle.kts new file mode 100644 index 000000000000..18e2d27be404 --- /dev/null +++ b/instrumentation/opentelemetry-api/opentelemetry-api-1.31/javaagent/build.gradle.kts @@ -0,0 +1,48 @@ +plugins { + id("otel.javaagent-instrumentation") +} + +dependencies { + compileOnly(project(":opentelemetry-api-shaded-for-instrumenting", configuration = "v1_31")) + compileOnly("io.opentelemetry:opentelemetry-api-incubator") + + implementation(project(":instrumentation:opentelemetry-api:opentelemetry-api-1.0:javaagent")) + implementation(project(":instrumentation:opentelemetry-api:opentelemetry-api-1.4:javaagent")) + implementation(project(":instrumentation:opentelemetry-api:opentelemetry-api-1.10:javaagent")) + implementation(project(":instrumentation:opentelemetry-api:opentelemetry-api-1.15:javaagent")) + implementation(project(":instrumentation:opentelemetry-api:opentelemetry-api-1.27:javaagent")) + + testImplementation("io.opentelemetry:opentelemetry-extension-incubator:1.31.0-alpha") +} + +configurations.configureEach { + if (name == "testRuntimeClasspath" || name == "testCompileClasspath" || name.startsWith("noopTest")) { + resolutionStrategy { + force("io.opentelemetry:opentelemetry-api:1.31.0") + } + } +} + +testing { + suites { + val noopTest by registering(JvmTestSuite::class) { + dependencies { + implementation("io.opentelemetry:opentelemetry-extension-incubator:1.31.0-alpha") + } + + targets { + all { + testTask.configure { + jvmArgs("-Dtesting.exporter.enabled=false") + } + } + } + } + } +} + +tasks { + check { + dependsOn(testing.suites) + } +} diff --git a/instrumentation/opentelemetry-api/opentelemetry-api-1.31/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_31/incubator/OpenTelemetryApiInstrumentationModule.java b/instrumentation/opentelemetry-api/opentelemetry-api-1.31/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_31/incubator/OpenTelemetryApiInstrumentationModule.java new file mode 100644 index 000000000000..273bb3b755f2 --- /dev/null +++ b/instrumentation/opentelemetry-api/opentelemetry-api-1.31/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_31/incubator/OpenTelemetryApiInstrumentationModule.java @@ -0,0 +1,40 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.opentelemetryapi.v1_31.incubator; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.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 io.opentelemetry.javaagent.extension.instrumentation.internal.ExperimentalInstrumentationModule; +import java.util.List; +import net.bytebuddy.matcher.ElementMatcher; + +@AutoService(InstrumentationModule.class) +public class OpenTelemetryApiInstrumentationModule extends InstrumentationModule + implements ExperimentalInstrumentationModule { + public OpenTelemetryApiInstrumentationModule() { + super("opentelemetry-api", "opentelemetry-api-1.31", "opentelemetry-api-incubator-1.31"); + } + + @Override + public List typeInstrumentations() { + return singletonList(new OpenTelemetryInstrumentation()); + } + + @Override + public ElementMatcher.Junction classLoaderMatcher() { + return hasClassesNamed( + "application.io.opentelemetry.extension.incubator.metrics.ExtendedDoubleHistogramBuilder"); + } + + @Override + public String getModuleGroup() { + return "opentelemetry-api-bridge"; + } +} diff --git a/instrumentation/opentelemetry-api/opentelemetry-api-1.31/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_31/incubator/OpenTelemetryInstrumentation.java b/instrumentation/opentelemetry-api/opentelemetry-api-1.31/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_31/incubator/OpenTelemetryInstrumentation.java new file mode 100644 index 000000000000..08ffb37f8eee --- /dev/null +++ b/instrumentation/opentelemetry-api/opentelemetry-api-1.31/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_31/incubator/OpenTelemetryInstrumentation.java @@ -0,0 +1,40 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.opentelemetryapi.v1_31.incubator; + +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.none; + +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.opentelemetryapi.v1_31.incubator.metrics.ApplicationMeterFactory131; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class OpenTelemetryInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("application.io.opentelemetry.api.GlobalOpenTelemetry"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + none(), OpenTelemetryInstrumentation.class.getName() + "$InitAdvice"); + } + + @SuppressWarnings({"ReturnValueIgnored", "unused"}) + public static class InitAdvice { + @Advice.OnMethodEnter + public static void init() { + // the sole purpose of this advice is to ensure that ApplicationMeterFactory131 is recognized + // as helper class and injected into class loader + ApplicationMeterFactory131.class.getName(); + } + } +} diff --git a/instrumentation/opentelemetry-api/opentelemetry-api-1.31/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_31/incubator/metrics/ApplicationDoubleCounterBuilder131.java b/instrumentation/opentelemetry-api/opentelemetry-api-1.31/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_31/incubator/metrics/ApplicationDoubleCounterBuilder131.java new file mode 100644 index 000000000000..959fb363f55f --- /dev/null +++ b/instrumentation/opentelemetry-api/opentelemetry-api-1.31/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_31/incubator/metrics/ApplicationDoubleCounterBuilder131.java @@ -0,0 +1,31 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.opentelemetryapi.v1_31.incubator.metrics; + +import application.io.opentelemetry.api.common.AttributeKey; +import application.io.opentelemetry.extension.incubator.metrics.ExtendedDoubleCounterBuilder; +import io.opentelemetry.javaagent.instrumentation.opentelemetryapi.trace.Bridging; +import io.opentelemetry.javaagent.instrumentation.opentelemetryapi.v1_10.metrics.ApplicationDoubleCounterBuilder; +import java.util.List; + +final class ApplicationDoubleCounterBuilder131 extends ApplicationDoubleCounterBuilder + implements ExtendedDoubleCounterBuilder { + + private final io.opentelemetry.api.metrics.DoubleCounterBuilder agentBuilder; + + ApplicationDoubleCounterBuilder131( + io.opentelemetry.api.metrics.DoubleCounterBuilder agentBuilder) { + super(agentBuilder); + this.agentBuilder = agentBuilder; + } + + @Override + public ExtendedDoubleCounterBuilder setAttributesAdvice(List> attributes) { + ((io.opentelemetry.api.incubator.metrics.ExtendedDoubleCounterBuilder) agentBuilder) + .setAttributesAdvice(Bridging.toAgent(attributes)); + return this; + } +} diff --git a/instrumentation/opentelemetry-api/opentelemetry-api-1.31/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_31/incubator/metrics/ApplicationDoubleGaugeBuilder131.java b/instrumentation/opentelemetry-api/opentelemetry-api-1.31/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_31/incubator/metrics/ApplicationDoubleGaugeBuilder131.java new file mode 100644 index 000000000000..e35104e68553 --- /dev/null +++ b/instrumentation/opentelemetry-api/opentelemetry-api-1.31/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_31/incubator/metrics/ApplicationDoubleGaugeBuilder131.java @@ -0,0 +1,55 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.opentelemetryapi.v1_31.incubator.metrics; + +import application.io.opentelemetry.api.common.AttributeKey; +import application.io.opentelemetry.api.common.Attributes; +import application.io.opentelemetry.api.metrics.LongGaugeBuilder; +import application.io.opentelemetry.extension.incubator.metrics.DoubleGauge; +import application.io.opentelemetry.extension.incubator.metrics.ExtendedDoubleGaugeBuilder; +import io.opentelemetry.javaagent.instrumentation.opentelemetryapi.trace.Bridging; +import io.opentelemetry.javaagent.instrumentation.opentelemetryapi.v1_10.metrics.ApplicationDoubleGaugeBuilder; +import java.util.List; + +final class ApplicationDoubleGaugeBuilder131 extends ApplicationDoubleGaugeBuilder + implements ExtendedDoubleGaugeBuilder { + + private final io.opentelemetry.api.metrics.DoubleGaugeBuilder agentBuilder; + + ApplicationDoubleGaugeBuilder131(io.opentelemetry.api.metrics.DoubleGaugeBuilder agentBuilder) { + super(agentBuilder); + this.agentBuilder = agentBuilder; + } + + @Override + public LongGaugeBuilder ofLongs() { + return new ApplicationLongGaugeBuilder131(agentBuilder.ofLongs()); + } + + @Override + public DoubleGauge build() { + io.opentelemetry.api.metrics.DoubleGauge agentDoubleGauge = agentBuilder.build(); + return new DoubleGauge() { + + @Override + public void set(double value) { + agentDoubleGauge.set(value); + } + + @Override + public void set(double value, Attributes attributes) { + agentDoubleGauge.set(value, Bridging.toAgent(attributes)); + } + }; + } + + @Override + public ExtendedDoubleGaugeBuilder setAttributesAdvice(List> attributes) { + ((io.opentelemetry.api.incubator.metrics.ExtendedDoubleGaugeBuilder) agentBuilder) + .setAttributesAdvice(Bridging.toAgent(attributes)); + return this; + } +} diff --git a/instrumentation/opentelemetry-api/opentelemetry-api-1.31/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_31/incubator/metrics/ApplicationDoubleHistogramBuilder131.java b/instrumentation/opentelemetry-api/opentelemetry-api-1.31/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_31/incubator/metrics/ApplicationDoubleHistogramBuilder131.java new file mode 100644 index 000000000000..679371fce6c9 --- /dev/null +++ b/instrumentation/opentelemetry-api/opentelemetry-api-1.31/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_31/incubator/metrics/ApplicationDoubleHistogramBuilder131.java @@ -0,0 +1,44 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.opentelemetryapi.v1_31.incubator.metrics; + +import application.io.opentelemetry.api.common.AttributeKey; +import application.io.opentelemetry.api.metrics.LongHistogramBuilder; +import application.io.opentelemetry.extension.incubator.metrics.ExtendedDoubleHistogramBuilder; +import io.opentelemetry.javaagent.instrumentation.opentelemetryapi.trace.Bridging; +import io.opentelemetry.javaagent.instrumentation.opentelemetryapi.v1_10.metrics.ApplicationDoubleHistogramBuilder; +import java.util.List; + +final class ApplicationDoubleHistogramBuilder131 extends ApplicationDoubleHistogramBuilder + implements ExtendedDoubleHistogramBuilder { + + private final io.opentelemetry.api.metrics.DoubleHistogramBuilder agentBuilder; + + ApplicationDoubleHistogramBuilder131( + io.opentelemetry.api.metrics.DoubleHistogramBuilder agentBuilder) { + super(agentBuilder); + this.agentBuilder = agentBuilder; + } + + @Override + public LongHistogramBuilder ofLongs() { + return new ApplicationLongHistogramBuilder131(agentBuilder.ofLongs()); + } + + @Override + public ExtendedDoubleHistogramBuilder setExplicitBucketBoundariesAdvice( + List bucketBoundaries) { + agentBuilder.setExplicitBucketBoundariesAdvice(bucketBoundaries); + return this; + } + + @Override + public ExtendedDoubleHistogramBuilder setAttributesAdvice(List> attributes) { + ((io.opentelemetry.api.incubator.metrics.ExtendedDoubleHistogramBuilder) agentBuilder) + .setAttributesAdvice(Bridging.toAgent(attributes)); + return this; + } +} diff --git a/instrumentation/opentelemetry-api/opentelemetry-api-1.31/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_31/incubator/metrics/ApplicationDoubleUpDownCounterBuilder131.java b/instrumentation/opentelemetry-api/opentelemetry-api-1.31/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_31/incubator/metrics/ApplicationDoubleUpDownCounterBuilder131.java new file mode 100644 index 000000000000..f26dc473c5d9 --- /dev/null +++ b/instrumentation/opentelemetry-api/opentelemetry-api-1.31/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_31/incubator/metrics/ApplicationDoubleUpDownCounterBuilder131.java @@ -0,0 +1,31 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.opentelemetryapi.v1_31.incubator.metrics; + +import application.io.opentelemetry.api.common.AttributeKey; +import application.io.opentelemetry.extension.incubator.metrics.ExtendedDoubleUpDownCounterBuilder; +import io.opentelemetry.javaagent.instrumentation.opentelemetryapi.trace.Bridging; +import io.opentelemetry.javaagent.instrumentation.opentelemetryapi.v1_10.metrics.ApplicationDoubleUpDownCounterBuilder; +import java.util.List; + +final class ApplicationDoubleUpDownCounterBuilder131 extends ApplicationDoubleUpDownCounterBuilder + implements ExtendedDoubleUpDownCounterBuilder { + + private final io.opentelemetry.api.metrics.DoubleUpDownCounterBuilder agentBuilder; + + ApplicationDoubleUpDownCounterBuilder131( + io.opentelemetry.api.metrics.DoubleUpDownCounterBuilder agentBuilder) { + super(agentBuilder); + this.agentBuilder = agentBuilder; + } + + @Override + public ExtendedDoubleUpDownCounterBuilder setAttributesAdvice(List> attributes) { + ((io.opentelemetry.api.incubator.metrics.ExtendedDoubleUpDownCounterBuilder) agentBuilder) + .setAttributesAdvice(Bridging.toAgent(attributes)); + return this; + } +} diff --git a/instrumentation/opentelemetry-api/opentelemetry-api-1.31/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_31/incubator/metrics/ApplicationLongCounterBuilder131.java b/instrumentation/opentelemetry-api/opentelemetry-api-1.31/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_31/incubator/metrics/ApplicationLongCounterBuilder131.java new file mode 100644 index 000000000000..d11d1ec7a921 --- /dev/null +++ b/instrumentation/opentelemetry-api/opentelemetry-api-1.31/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_31/incubator/metrics/ApplicationLongCounterBuilder131.java @@ -0,0 +1,36 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.opentelemetryapi.v1_31.incubator.metrics; + +import application.io.opentelemetry.api.common.AttributeKey; +import application.io.opentelemetry.api.metrics.DoubleCounterBuilder; +import application.io.opentelemetry.extension.incubator.metrics.ExtendedLongCounterBuilder; +import io.opentelemetry.javaagent.instrumentation.opentelemetryapi.trace.Bridging; +import io.opentelemetry.javaagent.instrumentation.opentelemetryapi.v1_10.metrics.ApplicationLongCounterBuilder; +import java.util.List; + +final class ApplicationLongCounterBuilder131 extends ApplicationLongCounterBuilder + implements ExtendedLongCounterBuilder { + + private final io.opentelemetry.api.metrics.LongCounterBuilder agentBuilder; + + ApplicationLongCounterBuilder131(io.opentelemetry.api.metrics.LongCounterBuilder agentBuilder) { + super(agentBuilder); + this.agentBuilder = agentBuilder; + } + + @Override + public DoubleCounterBuilder ofDoubles() { + return new ApplicationDoubleCounterBuilder131(agentBuilder.ofDoubles()); + } + + @Override + public ExtendedLongCounterBuilder setAttributesAdvice(List> attributes) { + ((io.opentelemetry.api.incubator.metrics.ExtendedLongCounterBuilder) agentBuilder) + .setAttributesAdvice(Bridging.toAgent(attributes)); + return this; + } +} diff --git a/instrumentation/opentelemetry-api/opentelemetry-api-1.31/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_31/incubator/metrics/ApplicationLongGaugeBuilder131.java b/instrumentation/opentelemetry-api/opentelemetry-api-1.31/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_31/incubator/metrics/ApplicationLongGaugeBuilder131.java new file mode 100644 index 000000000000..137432f27862 --- /dev/null +++ b/instrumentation/opentelemetry-api/opentelemetry-api-1.31/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_31/incubator/metrics/ApplicationLongGaugeBuilder131.java @@ -0,0 +1,48 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.opentelemetryapi.v1_31.incubator.metrics; + +import application.io.opentelemetry.api.common.AttributeKey; +import application.io.opentelemetry.api.common.Attributes; +import application.io.opentelemetry.extension.incubator.metrics.ExtendedLongGaugeBuilder; +import application.io.opentelemetry.extension.incubator.metrics.LongGauge; +import io.opentelemetry.javaagent.instrumentation.opentelemetryapi.trace.Bridging; +import io.opentelemetry.javaagent.instrumentation.opentelemetryapi.v1_10.metrics.ApplicationLongGaugeBuilder; +import java.util.List; + +final class ApplicationLongGaugeBuilder131 extends ApplicationLongGaugeBuilder + implements ExtendedLongGaugeBuilder { + + private final io.opentelemetry.api.metrics.LongGaugeBuilder agentBuilder; + + ApplicationLongGaugeBuilder131(io.opentelemetry.api.metrics.LongGaugeBuilder agentBuilder) { + super(agentBuilder); + this.agentBuilder = agentBuilder; + } + + @Override + public LongGauge build() { + io.opentelemetry.api.metrics.LongGauge agentLongGauge = agentBuilder.build(); + return new LongGauge() { + @Override + public void set(long value) { + agentLongGauge.set(value); + } + + @Override + public void set(long value, Attributes attributes) { + agentLongGauge.set(value, Bridging.toAgent(attributes)); + } + }; + } + + @Override + public ExtendedLongGaugeBuilder setAttributesAdvice(List> attributes) { + ((io.opentelemetry.api.incubator.metrics.ExtendedLongGaugeBuilder) agentBuilder) + .setAttributesAdvice(Bridging.toAgent(attributes)); + return this; + } +} diff --git a/instrumentation/opentelemetry-api/opentelemetry-api-1.31/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_31/incubator/metrics/ApplicationLongHistogramBuilder131.java b/instrumentation/opentelemetry-api/opentelemetry-api-1.31/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_31/incubator/metrics/ApplicationLongHistogramBuilder131.java new file mode 100644 index 000000000000..809064f8021a --- /dev/null +++ b/instrumentation/opentelemetry-api/opentelemetry-api-1.31/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_31/incubator/metrics/ApplicationLongHistogramBuilder131.java @@ -0,0 +1,38 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.opentelemetryapi.v1_31.incubator.metrics; + +import application.io.opentelemetry.api.common.AttributeKey; +import application.io.opentelemetry.extension.incubator.metrics.ExtendedLongHistogramBuilder; +import io.opentelemetry.javaagent.instrumentation.opentelemetryapi.trace.Bridging; +import io.opentelemetry.javaagent.instrumentation.opentelemetryapi.v1_10.metrics.ApplicationLongHistogramBuilder; +import java.util.List; + +final class ApplicationLongHistogramBuilder131 extends ApplicationLongHistogramBuilder + implements ExtendedLongHistogramBuilder { + + private final io.opentelemetry.api.metrics.LongHistogramBuilder agentBuilder; + + ApplicationLongHistogramBuilder131( + io.opentelemetry.api.metrics.LongHistogramBuilder agentBuilder) { + super(agentBuilder); + this.agentBuilder = agentBuilder; + } + + @Override + public ExtendedLongHistogramBuilder setExplicitBucketBoundariesAdvice( + List bucketBoundaries) { + agentBuilder.setExplicitBucketBoundariesAdvice(bucketBoundaries); + return this; + } + + @Override + public ExtendedLongHistogramBuilder setAttributesAdvice(List> attributes) { + ((io.opentelemetry.api.incubator.metrics.ExtendedLongHistogramBuilder) agentBuilder) + .setAttributesAdvice(Bridging.toAgent(attributes)); + return this; + } +} diff --git a/instrumentation/opentelemetry-api/opentelemetry-api-1.31/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_31/incubator/metrics/ApplicationLongUpDownCounterBuilder131.java b/instrumentation/opentelemetry-api/opentelemetry-api-1.31/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_31/incubator/metrics/ApplicationLongUpDownCounterBuilder131.java new file mode 100644 index 000000000000..f6eecd1679dd --- /dev/null +++ b/instrumentation/opentelemetry-api/opentelemetry-api-1.31/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_31/incubator/metrics/ApplicationLongUpDownCounterBuilder131.java @@ -0,0 +1,37 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.opentelemetryapi.v1_31.incubator.metrics; + +import application.io.opentelemetry.api.common.AttributeKey; +import application.io.opentelemetry.api.metrics.DoubleUpDownCounterBuilder; +import application.io.opentelemetry.extension.incubator.metrics.ExtendedLongUpDownCounterBuilder; +import io.opentelemetry.javaagent.instrumentation.opentelemetryapi.trace.Bridging; +import io.opentelemetry.javaagent.instrumentation.opentelemetryapi.v1_10.metrics.ApplicationLongUpDownCounterBuilder; +import java.util.List; + +final class ApplicationLongUpDownCounterBuilder131 extends ApplicationLongUpDownCounterBuilder + implements ExtendedLongUpDownCounterBuilder { + + private final io.opentelemetry.api.metrics.LongUpDownCounterBuilder agentBuilder; + + ApplicationLongUpDownCounterBuilder131( + io.opentelemetry.api.metrics.LongUpDownCounterBuilder agentBuilder) { + super(agentBuilder); + this.agentBuilder = agentBuilder; + } + + @Override + public DoubleUpDownCounterBuilder ofDoubles() { + return new ApplicationDoubleUpDownCounterBuilder131(agentBuilder.ofDoubles()); + } + + @Override + public ExtendedLongUpDownCounterBuilder setAttributesAdvice(List> attributes) { + ((io.opentelemetry.api.incubator.metrics.ExtendedLongUpDownCounterBuilder) agentBuilder) + .setAttributesAdvice(Bridging.toAgent(attributes)); + return this; + } +} diff --git a/instrumentation/opentelemetry-api/opentelemetry-api-1.31/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_31/incubator/metrics/ApplicationMeter131.java b/instrumentation/opentelemetry-api/opentelemetry-api-1.31/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_31/incubator/metrics/ApplicationMeter131.java new file mode 100644 index 000000000000..105b38a0aaf3 --- /dev/null +++ b/instrumentation/opentelemetry-api/opentelemetry-api-1.31/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_31/incubator/metrics/ApplicationMeter131.java @@ -0,0 +1,64 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.opentelemetryapi.v1_31.incubator.metrics; + +import application.io.opentelemetry.api.metrics.DoubleGaugeBuilder; +import application.io.opentelemetry.api.metrics.DoubleHistogramBuilder; +import application.io.opentelemetry.api.metrics.LongCounterBuilder; +import application.io.opentelemetry.api.metrics.LongUpDownCounterBuilder; +import io.opentelemetry.javaagent.instrumentation.opentelemetryapi.v1_10.metrics.ApplicationDoubleGaugeBuilder; +import io.opentelemetry.javaagent.instrumentation.opentelemetryapi.v1_10.metrics.ApplicationDoubleHistogramBuilder; +import io.opentelemetry.javaagent.instrumentation.opentelemetryapi.v1_10.metrics.ApplicationLongCounterBuilder; +import io.opentelemetry.javaagent.instrumentation.opentelemetryapi.v1_10.metrics.ApplicationLongUpDownCounterBuilder; +import io.opentelemetry.javaagent.instrumentation.opentelemetryapi.v1_15.metrics.ApplicationMeter115; + +public class ApplicationMeter131 extends ApplicationMeter115 { + + private final io.opentelemetry.api.metrics.Meter agentMeter; + + protected ApplicationMeter131(io.opentelemetry.api.metrics.Meter agentMeter) { + super(agentMeter); + this.agentMeter = agentMeter; + } + + @Override + public LongCounterBuilder counterBuilder(String name) { + io.opentelemetry.api.metrics.LongCounterBuilder builder = agentMeter.counterBuilder(name); + if (builder instanceof io.opentelemetry.api.incubator.metrics.ExtendedLongCounterBuilder) { + return new ApplicationLongCounterBuilder131(builder); + } + return new ApplicationLongCounterBuilder(builder); + } + + @Override + public LongUpDownCounterBuilder upDownCounterBuilder(String name) { + io.opentelemetry.api.metrics.LongUpDownCounterBuilder builder = + agentMeter.upDownCounterBuilder(name); + if (builder + instanceof io.opentelemetry.api.incubator.metrics.ExtendedLongUpDownCounterBuilder) { + return new ApplicationLongUpDownCounterBuilder131(builder); + } + return new ApplicationLongUpDownCounterBuilder(builder); + } + + @Override + public DoubleHistogramBuilder histogramBuilder(String name) { + io.opentelemetry.api.metrics.DoubleHistogramBuilder builder = agentMeter.histogramBuilder(name); + if (builder instanceof io.opentelemetry.api.incubator.metrics.ExtendedDoubleHistogramBuilder) { + return new ApplicationDoubleHistogramBuilder131(builder); + } + return new ApplicationDoubleHistogramBuilder(builder); + } + + @Override + public DoubleGaugeBuilder gaugeBuilder(String name) { + io.opentelemetry.api.metrics.DoubleGaugeBuilder builder = agentMeter.gaugeBuilder(name); + if (builder instanceof io.opentelemetry.api.incubator.metrics.ExtendedDoubleGaugeBuilder) { + return new ApplicationDoubleGaugeBuilder131(builder); + } + return new ApplicationDoubleGaugeBuilder(builder); + } +} diff --git a/instrumentation/opentelemetry-api/opentelemetry-api-1.31/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_31/incubator/metrics/ApplicationMeterFactory131.java b/instrumentation/opentelemetry-api/opentelemetry-api-1.31/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_31/incubator/metrics/ApplicationMeterFactory131.java new file mode 100644 index 000000000000..2c05b7a4ba63 --- /dev/null +++ b/instrumentation/opentelemetry-api/opentelemetry-api-1.31/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_31/incubator/metrics/ApplicationMeterFactory131.java @@ -0,0 +1,18 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.opentelemetryapi.v1_31.incubator.metrics; + +import io.opentelemetry.api.metrics.Meter; +import io.opentelemetry.javaagent.instrumentation.opentelemetryapi.v1_10.metrics.ApplicationMeter; +import io.opentelemetry.javaagent.instrumentation.opentelemetryapi.v1_10.metrics.ApplicationMeterFactory; + +// this class is used from opentelemetry-api-1.27 via reflection +public final class ApplicationMeterFactory131 implements ApplicationMeterFactory { + @Override + public ApplicationMeter newMeter(Meter agentMeter) { + return new ApplicationMeter131(agentMeter); + } +} diff --git a/instrumentation/opentelemetry-api/opentelemetry-api-1.31/javaagent/src/noopTest/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_31/metrics/NoopTest.java b/instrumentation/opentelemetry-api/opentelemetry-api-1.31/javaagent/src/noopTest/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_31/metrics/NoopTest.java new file mode 100644 index 000000000000..bf93cb95f539 --- /dev/null +++ b/instrumentation/opentelemetry-api/opentelemetry-api-1.31/javaagent/src/noopTest/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_31/metrics/NoopTest.java @@ -0,0 +1,43 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.opentelemetryapi.v1_31.metrics; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.metrics.DoubleGaugeBuilder; +import io.opentelemetry.api.metrics.DoubleHistogramBuilder; +import io.opentelemetry.api.metrics.LongCounterBuilder; +import io.opentelemetry.api.metrics.LongUpDownCounterBuilder; +import io.opentelemetry.api.metrics.Meter; +import io.opentelemetry.extension.incubator.metrics.ExtendedDoubleGaugeBuilder; +import io.opentelemetry.extension.incubator.metrics.ExtendedDoubleHistogramBuilder; +import io.opentelemetry.extension.incubator.metrics.ExtendedLongCounterBuilder; +import io.opentelemetry.extension.incubator.metrics.ExtendedLongUpDownCounterBuilder; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +class NoopTest { + @RegisterExtension + static final AgentInstrumentationExtension testing = AgentInstrumentationExtension.create(); + + @Test + void noopInstance() { + Meter meter = testing.getOpenTelemetry().getMeter("test"); + + LongCounterBuilder counterBuilder = meter.counterBuilder("test"); + assertThat(counterBuilder).isNotInstanceOf(ExtendedLongCounterBuilder.class); + + LongUpDownCounterBuilder upDownCounterBuilder = meter.upDownCounterBuilder("test"); + assertThat(upDownCounterBuilder).isNotInstanceOf(ExtendedLongUpDownCounterBuilder.class); + + DoubleGaugeBuilder gaugeBuilder = meter.gaugeBuilder("test"); + assertThat(gaugeBuilder).isNotInstanceOf(ExtendedDoubleGaugeBuilder.class); + + DoubleHistogramBuilder histogramBuilder = meter.histogramBuilder("test"); + assertThat(histogramBuilder).isNotInstanceOf(ExtendedDoubleHistogramBuilder.class); + } +} diff --git a/instrumentation/opentelemetry-api/opentelemetry-api-1.31/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_31/metrics/MeterTest.java b/instrumentation/opentelemetry-api/opentelemetry-api-1.31/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_31/metrics/MeterTest.java new file mode 100644 index 000000000000..6c954a646d8f --- /dev/null +++ b/instrumentation/opentelemetry-api/opentelemetry-api-1.31/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_31/metrics/MeterTest.java @@ -0,0 +1,467 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.opentelemetryapi.v1_31.metrics; + +import static io.opentelemetry.api.common.AttributeKey.stringKey; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; +import static java.util.Collections.singletonList; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.metrics.DoubleCounter; +import io.opentelemetry.api.metrics.DoubleCounterBuilder; +import io.opentelemetry.api.metrics.DoubleGaugeBuilder; +import io.opentelemetry.api.metrics.DoubleHistogram; +import io.opentelemetry.api.metrics.DoubleHistogramBuilder; +import io.opentelemetry.api.metrics.DoubleUpDownCounter; +import io.opentelemetry.api.metrics.DoubleUpDownCounterBuilder; +import io.opentelemetry.api.metrics.LongCounter; +import io.opentelemetry.api.metrics.LongCounterBuilder; +import io.opentelemetry.api.metrics.LongGaugeBuilder; +import io.opentelemetry.api.metrics.LongHistogram; +import io.opentelemetry.api.metrics.LongHistogramBuilder; +import io.opentelemetry.api.metrics.LongUpDownCounter; +import io.opentelemetry.api.metrics.LongUpDownCounterBuilder; +import io.opentelemetry.api.metrics.Meter; +import io.opentelemetry.api.metrics.ObservableDoubleGauge; +import io.opentelemetry.api.metrics.ObservableLongGauge; +import io.opentelemetry.extension.incubator.metrics.DoubleGauge; +import io.opentelemetry.extension.incubator.metrics.ExtendedDoubleCounterBuilder; +import io.opentelemetry.extension.incubator.metrics.ExtendedDoubleGaugeBuilder; +import io.opentelemetry.extension.incubator.metrics.ExtendedDoubleHistogramBuilder; +import io.opentelemetry.extension.incubator.metrics.ExtendedDoubleUpDownCounterBuilder; +import io.opentelemetry.extension.incubator.metrics.ExtendedLongCounterBuilder; +import io.opentelemetry.extension.incubator.metrics.ExtendedLongGaugeBuilder; +import io.opentelemetry.extension.incubator.metrics.ExtendedLongHistogramBuilder; +import io.opentelemetry.extension.incubator.metrics.ExtendedLongUpDownCounterBuilder; +import io.opentelemetry.extension.incubator.metrics.LongGauge; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.sdk.common.InstrumentationScopeInfo; +import org.assertj.core.api.AbstractIterableAssert; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInfo; +import org.junit.jupiter.api.extension.RegisterExtension; + +class MeterTest { + + @RegisterExtension + static final AgentInstrumentationExtension testing = AgentInstrumentationExtension.create(); + + private String instrumentationName; + private Meter meter; + + @BeforeEach + void setupMeter(TestInfo test) { + instrumentationName = "test-" + test.getDisplayName(); + meter = + testing + .getOpenTelemetry() + .getMeterProvider() + .meterBuilder(instrumentationName) + .setInstrumentationVersion("1.2.3") + .setSchemaUrl("http://schema.org") + .build(); + } + + @Test + void longCounter() { + LongCounterBuilder builder = meter.counterBuilder("test").setDescription("d").setUnit("u"); + assertThat(builder).isInstanceOf(ExtendedLongCounterBuilder.class); + ExtendedLongCounterBuilder extendedBuilder = (ExtendedLongCounterBuilder) builder; + extendedBuilder.setAttributesAdvice(singletonList(stringKey("test"))); + + LongCounter instrument = builder.build(); + + instrument.add(5, Attributes.of(stringKey("test"), "test", stringKey("q"), "r")); + instrument.add(6, Attributes.of(stringKey("test"), "test", stringKey("q"), "r")); + + testing.waitAndAssertMetrics( + instrumentationName, + "test", + metrics -> + metrics.anySatisfy( + metric -> + assertThat(metric) + .hasDescription("d") + .hasUnit("u") + .hasInstrumentationScope( + InstrumentationScopeInfo.builder(instrumentationName) + .setVersion("1.2.3") + .build()) + .hasLongSumSatisfying( + sum -> + sum.isMonotonic() + .hasPointsSatisfying( + point -> + point + .hasValue(11) + .hasAttributesSatisfying( + equalTo(stringKey("test"), "test")))))); + } + + @Test + void doubleCounter() { + DoubleCounterBuilder builder = + meter.counterBuilder("test").ofDoubles().setDescription("d").setUnit("u"); + assertThat(builder).isInstanceOf(ExtendedDoubleCounterBuilder.class); + ExtendedDoubleCounterBuilder extendedBuilder = (ExtendedDoubleCounterBuilder) builder; + extendedBuilder.setAttributesAdvice(singletonList(stringKey("test"))); + + DoubleCounter instrument = builder.build(); + + instrument.add(5.5, Attributes.of(stringKey("test"), "test", stringKey("q"), "r")); + instrument.add(6.6, Attributes.of(stringKey("test"), "test", stringKey("q"), "r")); + + testing.waitAndAssertMetrics( + instrumentationName, + "test", + metrics -> + metrics.anySatisfy( + metric -> + assertThat(metric) + .hasDescription("d") + .hasUnit("u") + .hasInstrumentationScope( + InstrumentationScopeInfo.builder(instrumentationName) + .setVersion("1.2.3") + .build()) + .hasDoubleSumSatisfying( + sum -> + sum.isMonotonic() + .hasPointsSatisfying( + point -> + point + .hasValue(12.1) + .hasAttributesSatisfying( + equalTo(stringKey("test"), "test")))))); + } + + @Test + void longUpDownCounter() { + LongUpDownCounterBuilder builder = + meter.upDownCounterBuilder("test").setDescription("d").setUnit("u"); + assertThat(builder).isInstanceOf(ExtendedLongUpDownCounterBuilder.class); + ExtendedLongUpDownCounterBuilder extendedBuilder = (ExtendedLongUpDownCounterBuilder) builder; + extendedBuilder.setAttributesAdvice(singletonList(stringKey("test"))); + + LongUpDownCounter instrument = builder.build(); + + instrument.add(5, Attributes.of(stringKey("test"), "test", stringKey("q"), "r")); + instrument.add(6, Attributes.of(stringKey("test"), "test", stringKey("q"), "r")); + + testing.waitAndAssertMetrics( + instrumentationName, + "test", + metrics -> + metrics.anySatisfy( + metric -> + assertThat(metric) + .hasDescription("d") + .hasUnit("u") + .hasInstrumentationScope( + InstrumentationScopeInfo.builder(instrumentationName) + .setVersion("1.2.3") + .build()) + .hasLongSumSatisfying( + sum -> + sum.isNotMonotonic() + .hasPointsSatisfying( + point -> + point + .hasValue(11) + .hasAttributesSatisfying( + equalTo(stringKey("test"), "test")))))); + } + + @Test + void doubleUpDownCounter() { + DoubleUpDownCounterBuilder builder = + meter.upDownCounterBuilder("test").ofDoubles().setDescription("d").setUnit("u"); + assertThat(builder).isInstanceOf(ExtendedDoubleUpDownCounterBuilder.class); + ExtendedDoubleUpDownCounterBuilder extendedBuilder = + (ExtendedDoubleUpDownCounterBuilder) builder; + extendedBuilder.setAttributesAdvice(singletonList(stringKey("test"))); + + DoubleUpDownCounter instrument = builder.build(); + + instrument.add(5.5, Attributes.of(stringKey("test"), "test", stringKey("q"), "r")); + instrument.add(6.6, Attributes.of(stringKey("test"), "test", stringKey("q"), "r")); + + testing.waitAndAssertMetrics( + instrumentationName, + "test", + metrics -> + metrics.anySatisfy( + metric -> + assertThat(metric) + .hasDescription("d") + .hasUnit("u") + .hasInstrumentationScope( + InstrumentationScopeInfo.builder(instrumentationName) + .setVersion("1.2.3") + .build()) + .hasDoubleSumSatisfying( + sum -> + sum.isNotMonotonic() + .hasPointsSatisfying( + point -> + point + .hasValue(12.1) + .hasAttributesSatisfying( + equalTo(stringKey("test"), "test")))))); + } + + @Test + void longHistogram() { + LongHistogramBuilder builder = + meter.histogramBuilder("test").ofLongs().setDescription("d").setUnit("u"); + assertThat(builder).isInstanceOf(ExtendedLongHistogramBuilder.class); + ExtendedLongHistogramBuilder extendedBuilder = (ExtendedLongHistogramBuilder) builder; + extendedBuilder.setAttributesAdvice(singletonList(stringKey("test"))); + extendedBuilder.setExplicitBucketBoundariesAdvice(singletonList(10L)); + + LongHistogram instrument = builder.build(); + + instrument.record(5, Attributes.of(stringKey("test"), "test", stringKey("q"), "r")); + instrument.record(6, Attributes.of(stringKey("test"), "test", stringKey("q"), "r")); + + testing.waitAndAssertMetrics( + instrumentationName, + "test", + metrics -> + metrics.anySatisfy( + metric -> + assertThat(metric) + .hasDescription("d") + .hasUnit("u") + .hasInstrumentationScope( + InstrumentationScopeInfo.builder(instrumentationName) + .setVersion("1.2.3") + .build()) + .hasHistogramSatisfying( + histogram -> + histogram.hasPointsSatisfying( + point -> + point + .hasSum(11.0) + .hasBucketBoundaries(10.0) + .hasAttributesSatisfying( + equalTo(stringKey("test"), "test")))))); + } + + @Test + void doubleHistogram() { + DoubleHistogramBuilder builder = + meter.histogramBuilder("test").setDescription("d").setUnit("u"); + assertThat(builder).isInstanceOf(ExtendedDoubleHistogramBuilder.class); + ExtendedDoubleHistogramBuilder extendedBuilder = (ExtendedDoubleHistogramBuilder) builder; + extendedBuilder.setAttributesAdvice(singletonList(stringKey("test"))); + extendedBuilder.setExplicitBucketBoundariesAdvice(singletonList(10.0)); + + DoubleHistogram instrument = builder.build(); + + instrument.record(5.5, Attributes.of(stringKey("test"), "test", stringKey("q"), "r")); + instrument.record(6.6, Attributes.of(stringKey("test"), "test", stringKey("q"), "r")); + + testing.waitAndAssertMetrics( + instrumentationName, + "test", + metrics -> + metrics.anySatisfy( + metric -> + assertThat(metric) + .hasDescription("d") + .hasUnit("u") + .hasInstrumentationScope( + InstrumentationScopeInfo.builder(instrumentationName) + .setVersion("1.2.3") + .build()) + .hasHistogramSatisfying( + histogram -> + histogram.hasPointsSatisfying( + point -> + point + .hasSum(12.1) + .hasBucketBoundaries(10.0) + .hasAttributesSatisfying( + equalTo(stringKey("test"), "test")))))); + } + + @Test + void longGauge() throws InterruptedException { + LongGaugeBuilder builder = + meter.gaugeBuilder("test").ofLongs().setDescription("d").setUnit("u"); + assertThat(builder).isInstanceOf(ExtendedLongGaugeBuilder.class); + ExtendedLongGaugeBuilder extendedBuilder = (ExtendedLongGaugeBuilder) builder; + extendedBuilder.setAttributesAdvice(singletonList(stringKey("test"))); + + ObservableLongGauge observableGauge = + builder.buildWithCallback( + result -> + result.record(123, Attributes.of(stringKey("test"), "test", stringKey("q"), "r"))); + + testing.waitAndAssertMetrics( + instrumentationName, + "test", + metrics -> + metrics.anySatisfy( + metric -> + assertThat(metric) + .hasDescription("d") + .hasUnit("u") + .hasInstrumentationScope( + InstrumentationScopeInfo.builder(instrumentationName) + .setVersion("1.2.3") + .build()) + .hasLongGaugeSatisfying( + gauge -> + gauge.hasPointsSatisfying( + point -> + point + .hasValue(123) + .hasAttributesSatisfying( + equalTo(stringKey("test"), "test")))))); + + observableGauge.close(); + + // sleep exporter interval + Thread.sleep(100); + testing.clearData(); + Thread.sleep(100); + + testing.waitAndAssertMetrics(instrumentationName, "test", AbstractIterableAssert::isEmpty); + } + + @Test + void syncLongGauge() throws InterruptedException { + LongGaugeBuilder builder = + meter.gaugeBuilder("test").ofLongs().setDescription("d").setUnit("u"); + assertThat(builder).isInstanceOf(ExtendedLongGaugeBuilder.class); + ExtendedLongGaugeBuilder extendedBuilder = (ExtendedLongGaugeBuilder) builder; + extendedBuilder.setAttributesAdvice(singletonList(stringKey("test"))); + + LongGauge longGauge = extendedBuilder.build(); + longGauge.set(321); + longGauge.set(123, Attributes.of(stringKey("test"), "test", stringKey("q"), "r")); + + testing.waitAndAssertMetrics( + instrumentationName, + "test", + metrics -> + metrics.anySatisfy( + metric -> + assertThat(metric) + .hasDescription("d") + .hasUnit("u") + .hasInstrumentationScope( + InstrumentationScopeInfo.builder(instrumentationName) + .setVersion("1.2.3") + .build()) + .hasLongGaugeSatisfying( + gauge -> + gauge.hasPointsSatisfying( + point -> point.hasValue(321).hasAttributes(Attributes.empty()), + point -> + point + .hasValue(123) + .hasAttributesSatisfying( + equalTo(stringKey("test"), "test")))))); + + // sleep exporter interval + Thread.sleep(100); + testing.clearData(); + Thread.sleep(100); + + testing.waitAndAssertMetrics(instrumentationName, "test", AbstractIterableAssert::isEmpty); + } + + @Test + void doubleGauge() throws InterruptedException { + DoubleGaugeBuilder builder = meter.gaugeBuilder("test").setDescription("d").setUnit("u"); + assertThat(builder).isInstanceOf(ExtendedDoubleGaugeBuilder.class); + ExtendedDoubleGaugeBuilder extendedBuilder = (ExtendedDoubleGaugeBuilder) builder; + extendedBuilder.setAttributesAdvice(singletonList(stringKey("test"))); + + ObservableDoubleGauge observableGauge = + builder.buildWithCallback( + result -> + result.record(1.23, Attributes.of(stringKey("test"), "test", stringKey("q"), "r"))); + + testing.waitAndAssertMetrics( + instrumentationName, + "test", + metrics -> + metrics.anySatisfy( + metric -> + assertThat(metric) + .hasDescription("d") + .hasUnit("u") + .hasInstrumentationScope( + InstrumentationScopeInfo.builder(instrumentationName) + .setVersion("1.2.3") + .build()) + .hasDoubleGaugeSatisfying( + gauge -> + gauge.hasPointsSatisfying( + point -> + point + .hasValue(1.23) + .hasAttributesSatisfying( + equalTo(stringKey("test"), "test")))))); + + observableGauge.close(); + + // sleep exporter interval + Thread.sleep(100); + testing.clearData(); + Thread.sleep(100); + + testing.waitAndAssertMetrics(instrumentationName, "test", AbstractIterableAssert::isEmpty); + } + + @Test + void syncDoubleGauge() throws InterruptedException { + DoubleGaugeBuilder builder = meter.gaugeBuilder("test").setDescription("d").setUnit("u"); + assertThat(builder).isInstanceOf(ExtendedDoubleGaugeBuilder.class); + ExtendedDoubleGaugeBuilder extendedBuilder = (ExtendedDoubleGaugeBuilder) builder; + extendedBuilder.setAttributesAdvice(singletonList(stringKey("test"))); + + DoubleGauge doubleGauge = extendedBuilder.build(); + doubleGauge.set(3.21); + doubleGauge.set(1.23, Attributes.of(stringKey("test"), "test", stringKey("q"), "r")); + + testing.waitAndAssertMetrics( + instrumentationName, + "test", + metrics -> + metrics.anySatisfy( + metric -> + assertThat(metric) + .hasDescription("d") + .hasUnit("u") + .hasInstrumentationScope( + InstrumentationScopeInfo.builder(instrumentationName) + .setVersion("1.2.3") + .build()) + .hasDoubleGaugeSatisfying( + gauge -> + gauge.hasPointsSatisfying( + point -> point.hasValue(3.21).hasAttributes(Attributes.empty()), + point -> + point + .hasValue(1.23) + .hasAttributesSatisfying( + equalTo(stringKey("test"), "test")))))); + + // sleep exporter interval + Thread.sleep(100); + testing.clearData(); + Thread.sleep(100); + + testing.waitAndAssertMetrics(instrumentationName, "test", AbstractIterableAssert::isEmpty); + } +} diff --git a/instrumentation/opentelemetry-api/opentelemetry-api-1.32/javaagent/build.gradle.kts b/instrumentation/opentelemetry-api/opentelemetry-api-1.32/javaagent/build.gradle.kts new file mode 100644 index 000000000000..5acbf5a4d4ae --- /dev/null +++ b/instrumentation/opentelemetry-api/opentelemetry-api-1.32/javaagent/build.gradle.kts @@ -0,0 +1,54 @@ +plugins { + id("otel.javaagent-instrumentation") +} + +dependencies { + compileOnly(project(":opentelemetry-api-shaded-for-instrumenting", configuration = "v1_32")) + compileOnly("io.opentelemetry:opentelemetry-api-incubator") + + implementation(project(":instrumentation:opentelemetry-api:opentelemetry-api-1.0:javaagent")) + implementation(project(":instrumentation:opentelemetry-api:opentelemetry-api-1.4:javaagent")) + implementation(project(":instrumentation:opentelemetry-api:opentelemetry-api-1.10:javaagent")) + implementation(project(":instrumentation:opentelemetry-api:opentelemetry-api-1.15:javaagent")) + implementation(project(":instrumentation:opentelemetry-api:opentelemetry-api-1.27:javaagent")) + implementation(project(":instrumentation:opentelemetry-api:opentelemetry-api-1.31:javaagent")) +} + +configurations.configureEach { + if (name.endsWith("testRuntimeClasspath", true) || + name.endsWith("testCompileClasspath", true) || + name.startsWith("noopTest")) { + resolutionStrategy { + force("io.opentelemetry:opentelemetry-api:1.32.0") + } + } +} + +testing { + suites { + val incubatorTest by registering(JvmTestSuite::class) { + dependencies { + implementation("io.opentelemetry:opentelemetry-extension-incubator:1.32.0-alpha") + } + } + val noopTest by registering(JvmTestSuite::class) { + dependencies { + implementation("io.opentelemetry:opentelemetry-extension-incubator:1.32.0-alpha") + } + + targets { + all { + testTask.configure { + jvmArgs("-Dtesting.exporter.enabled=false") + } + } + } + } + } +} + +tasks { + check { + dependsOn(testing.suites) + } +} diff --git a/instrumentation/opentelemetry-api/opentelemetry-api-1.32/javaagent/src/incubatorTest/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_32/incubator/metrics/MeterTest.java b/instrumentation/opentelemetry-api/opentelemetry-api-1.32/javaagent/src/incubatorTest/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_32/incubator/metrics/MeterTest.java new file mode 100644 index 000000000000..4bd4470e2797 --- /dev/null +++ b/instrumentation/opentelemetry-api/opentelemetry-api-1.32/javaagent/src/incubatorTest/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_32/incubator/metrics/MeterTest.java @@ -0,0 +1,467 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.opentelemetryapi.v1_32.incubator.metrics; + +import static io.opentelemetry.api.common.AttributeKey.stringKey; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; +import static java.util.Collections.singletonList; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.metrics.DoubleCounter; +import io.opentelemetry.api.metrics.DoubleCounterBuilder; +import io.opentelemetry.api.metrics.DoubleGaugeBuilder; +import io.opentelemetry.api.metrics.DoubleHistogram; +import io.opentelemetry.api.metrics.DoubleHistogramBuilder; +import io.opentelemetry.api.metrics.DoubleUpDownCounter; +import io.opentelemetry.api.metrics.DoubleUpDownCounterBuilder; +import io.opentelemetry.api.metrics.LongCounter; +import io.opentelemetry.api.metrics.LongCounterBuilder; +import io.opentelemetry.api.metrics.LongGaugeBuilder; +import io.opentelemetry.api.metrics.LongHistogram; +import io.opentelemetry.api.metrics.LongHistogramBuilder; +import io.opentelemetry.api.metrics.LongUpDownCounter; +import io.opentelemetry.api.metrics.LongUpDownCounterBuilder; +import io.opentelemetry.api.metrics.Meter; +import io.opentelemetry.api.metrics.ObservableDoubleGauge; +import io.opentelemetry.api.metrics.ObservableLongGauge; +import io.opentelemetry.extension.incubator.metrics.DoubleGauge; +import io.opentelemetry.extension.incubator.metrics.ExtendedDoubleCounterBuilder; +import io.opentelemetry.extension.incubator.metrics.ExtendedDoubleGaugeBuilder; +import io.opentelemetry.extension.incubator.metrics.ExtendedDoubleHistogramBuilder; +import io.opentelemetry.extension.incubator.metrics.ExtendedDoubleUpDownCounterBuilder; +import io.opentelemetry.extension.incubator.metrics.ExtendedLongCounterBuilder; +import io.opentelemetry.extension.incubator.metrics.ExtendedLongGaugeBuilder; +import io.opentelemetry.extension.incubator.metrics.ExtendedLongHistogramBuilder; +import io.opentelemetry.extension.incubator.metrics.ExtendedLongUpDownCounterBuilder; +import io.opentelemetry.extension.incubator.metrics.LongGauge; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.sdk.common.InstrumentationScopeInfo; +import org.assertj.core.api.AbstractIterableAssert; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInfo; +import org.junit.jupiter.api.extension.RegisterExtension; + +class MeterTest { + + @RegisterExtension + static final AgentInstrumentationExtension testing = AgentInstrumentationExtension.create(); + + private String instrumentationName; + private Meter meter; + + @BeforeEach + void setupMeter(TestInfo test) { + instrumentationName = "test-" + test.getDisplayName(); + meter = + testing + .getOpenTelemetry() + .getMeterProvider() + .meterBuilder(instrumentationName) + .setInstrumentationVersion("1.2.3") + .setSchemaUrl("http://schema.org") + .build(); + } + + @Test + void longCounter() { + LongCounterBuilder builder = meter.counterBuilder("test").setDescription("d").setUnit("u"); + assertThat(builder).isInstanceOf(ExtendedLongCounterBuilder.class); + ExtendedLongCounterBuilder extendedBuilder = (ExtendedLongCounterBuilder) builder; + extendedBuilder.setAttributesAdvice(singletonList(stringKey("test"))); + + LongCounter instrument = builder.build(); + + instrument.add(5, Attributes.of(stringKey("test"), "test", stringKey("q"), "r")); + instrument.add(6, Attributes.of(stringKey("test"), "test", stringKey("q"), "r")); + + testing.waitAndAssertMetrics( + instrumentationName, + "test", + metrics -> + metrics.anySatisfy( + metric -> + assertThat(metric) + .hasDescription("d") + .hasUnit("u") + .hasInstrumentationScope( + InstrumentationScopeInfo.builder(instrumentationName) + .setVersion("1.2.3") + .build()) + .hasLongSumSatisfying( + sum -> + sum.isMonotonic() + .hasPointsSatisfying( + point -> + point + .hasValue(11) + .hasAttributesSatisfying( + equalTo(stringKey("test"), "test")))))); + } + + @Test + void doubleCounter() { + DoubleCounterBuilder builder = + meter.counterBuilder("test").ofDoubles().setDescription("d").setUnit("u"); + assertThat(builder).isInstanceOf(ExtendedDoubleCounterBuilder.class); + ExtendedDoubleCounterBuilder extendedBuilder = (ExtendedDoubleCounterBuilder) builder; + extendedBuilder.setAttributesAdvice(singletonList(stringKey("test"))); + + DoubleCounter instrument = builder.build(); + + instrument.add(5.5, Attributes.of(stringKey("test"), "test", stringKey("q"), "r")); + instrument.add(6.6, Attributes.of(stringKey("test"), "test", stringKey("q"), "r")); + + testing.waitAndAssertMetrics( + instrumentationName, + "test", + metrics -> + metrics.anySatisfy( + metric -> + assertThat(metric) + .hasDescription("d") + .hasUnit("u") + .hasInstrumentationScope( + InstrumentationScopeInfo.builder(instrumentationName) + .setVersion("1.2.3") + .build()) + .hasDoubleSumSatisfying( + sum -> + sum.isMonotonic() + .hasPointsSatisfying( + point -> + point + .hasValue(12.1) + .hasAttributesSatisfying( + equalTo(stringKey("test"), "test")))))); + } + + @Test + void longUpDownCounter() { + LongUpDownCounterBuilder builder = + meter.upDownCounterBuilder("test").setDescription("d").setUnit("u"); + assertThat(builder).isInstanceOf(ExtendedLongUpDownCounterBuilder.class); + ExtendedLongUpDownCounterBuilder extendedBuilder = (ExtendedLongUpDownCounterBuilder) builder; + extendedBuilder.setAttributesAdvice(singletonList(stringKey("test"))); + + LongUpDownCounter instrument = builder.build(); + + instrument.add(5, Attributes.of(stringKey("test"), "test", stringKey("q"), "r")); + instrument.add(6, Attributes.of(stringKey("test"), "test", stringKey("q"), "r")); + + testing.waitAndAssertMetrics( + instrumentationName, + "test", + metrics -> + metrics.anySatisfy( + metric -> + assertThat(metric) + .hasDescription("d") + .hasUnit("u") + .hasInstrumentationScope( + InstrumentationScopeInfo.builder(instrumentationName) + .setVersion("1.2.3") + .build()) + .hasLongSumSatisfying( + sum -> + sum.isNotMonotonic() + .hasPointsSatisfying( + point -> + point + .hasValue(11) + .hasAttributesSatisfying( + equalTo(stringKey("test"), "test")))))); + } + + @Test + void doubleUpDownCounter() { + DoubleUpDownCounterBuilder builder = + meter.upDownCounterBuilder("test").ofDoubles().setDescription("d").setUnit("u"); + assertThat(builder).isInstanceOf(ExtendedDoubleUpDownCounterBuilder.class); + ExtendedDoubleUpDownCounterBuilder extendedBuilder = + (ExtendedDoubleUpDownCounterBuilder) builder; + extendedBuilder.setAttributesAdvice(singletonList(stringKey("test"))); + + DoubleUpDownCounter instrument = builder.build(); + + instrument.add(5.5, Attributes.of(stringKey("test"), "test", stringKey("q"), "r")); + instrument.add(6.6, Attributes.of(stringKey("test"), "test", stringKey("q"), "r")); + + testing.waitAndAssertMetrics( + instrumentationName, + "test", + metrics -> + metrics.anySatisfy( + metric -> + assertThat(metric) + .hasDescription("d") + .hasUnit("u") + .hasInstrumentationScope( + InstrumentationScopeInfo.builder(instrumentationName) + .setVersion("1.2.3") + .build()) + .hasDoubleSumSatisfying( + sum -> + sum.isNotMonotonic() + .hasPointsSatisfying( + point -> + point + .hasValue(12.1) + .hasAttributesSatisfying( + equalTo(stringKey("test"), "test")))))); + } + + @Test + void longHistogram() { + LongHistogramBuilder builder = + meter.histogramBuilder("test").ofLongs().setDescription("d").setUnit("u"); + builder.setExplicitBucketBoundariesAdvice(singletonList(10L)); + assertThat(builder).isInstanceOf(ExtendedLongHistogramBuilder.class); + ExtendedLongHistogramBuilder extendedBuilder = (ExtendedLongHistogramBuilder) builder; + extendedBuilder.setAttributesAdvice(singletonList(stringKey("test"))); + + LongHistogram instrument = builder.build(); + + instrument.record(5, Attributes.of(stringKey("test"), "test", stringKey("q"), "r")); + instrument.record(6, Attributes.of(stringKey("test"), "test", stringKey("q"), "r")); + + testing.waitAndAssertMetrics( + instrumentationName, + "test", + metrics -> + metrics.anySatisfy( + metric -> + assertThat(metric) + .hasDescription("d") + .hasUnit("u") + .hasInstrumentationScope( + InstrumentationScopeInfo.builder(instrumentationName) + .setVersion("1.2.3") + .build()) + .hasHistogramSatisfying( + histogram -> + histogram.hasPointsSatisfying( + point -> + point + .hasSum(11.0) + .hasBucketBoundaries(10.0) + .hasAttributesSatisfying( + equalTo(stringKey("test"), "test")))))); + } + + @Test + void doubleHistogram() { + DoubleHistogramBuilder builder = + meter.histogramBuilder("test").setDescription("d").setUnit("u"); + builder.setExplicitBucketBoundariesAdvice(singletonList(10.0)); + assertThat(builder).isInstanceOf(ExtendedDoubleHistogramBuilder.class); + ExtendedDoubleHistogramBuilder extendedBuilder = (ExtendedDoubleHistogramBuilder) builder; + extendedBuilder.setAttributesAdvice(singletonList(stringKey("test"))); + + DoubleHistogram instrument = builder.build(); + + instrument.record(5.5, Attributes.of(stringKey("test"), "test", stringKey("q"), "r")); + instrument.record(6.6, Attributes.of(stringKey("test"), "test", stringKey("q"), "r")); + + testing.waitAndAssertMetrics( + instrumentationName, + "test", + metrics -> + metrics.anySatisfy( + metric -> + assertThat(metric) + .hasDescription("d") + .hasUnit("u") + .hasInstrumentationScope( + InstrumentationScopeInfo.builder(instrumentationName) + .setVersion("1.2.3") + .build()) + .hasHistogramSatisfying( + histogram -> + histogram.hasPointsSatisfying( + point -> + point + .hasSum(12.1) + .hasBucketBoundaries(10.0) + .hasAttributesSatisfying( + equalTo(stringKey("test"), "test")))))); + } + + @Test + void longGauge() throws InterruptedException { + LongGaugeBuilder builder = + meter.gaugeBuilder("test").ofLongs().setDescription("d").setUnit("u"); + assertThat(builder).isInstanceOf(ExtendedLongGaugeBuilder.class); + ExtendedLongGaugeBuilder extendedBuilder = (ExtendedLongGaugeBuilder) builder; + extendedBuilder.setAttributesAdvice(singletonList(stringKey("test"))); + + ObservableLongGauge observableGauge = + builder.buildWithCallback( + result -> + result.record(123, Attributes.of(stringKey("test"), "test", stringKey("q"), "r"))); + + testing.waitAndAssertMetrics( + instrumentationName, + "test", + metrics -> + metrics.anySatisfy( + metric -> + assertThat(metric) + .hasDescription("d") + .hasUnit("u") + .hasInstrumentationScope( + InstrumentationScopeInfo.builder(instrumentationName) + .setVersion("1.2.3") + .build()) + .hasLongGaugeSatisfying( + gauge -> + gauge.hasPointsSatisfying( + point -> + point + .hasValue(123) + .hasAttributesSatisfying( + equalTo(stringKey("test"), "test")))))); + + observableGauge.close(); + + // sleep exporter interval + Thread.sleep(100); + testing.clearData(); + Thread.sleep(100); + + testing.waitAndAssertMetrics(instrumentationName, "test", AbstractIterableAssert::isEmpty); + } + + @Test + void syncLongGauge() throws InterruptedException { + LongGaugeBuilder builder = + meter.gaugeBuilder("test").ofLongs().setDescription("d").setUnit("u"); + assertThat(builder).isInstanceOf(ExtendedLongGaugeBuilder.class); + ExtendedLongGaugeBuilder extendedBuilder = (ExtendedLongGaugeBuilder) builder; + extendedBuilder.setAttributesAdvice(singletonList(stringKey("test"))); + + LongGauge longGauge = extendedBuilder.build(); + longGauge.set(321); + longGauge.set(123, Attributes.of(stringKey("test"), "test", stringKey("q"), "r")); + + testing.waitAndAssertMetrics( + instrumentationName, + "test", + metrics -> + metrics.anySatisfy( + metric -> + assertThat(metric) + .hasDescription("d") + .hasUnit("u") + .hasInstrumentationScope( + InstrumentationScopeInfo.builder(instrumentationName) + .setVersion("1.2.3") + .build()) + .hasLongGaugeSatisfying( + gauge -> + gauge.hasPointsSatisfying( + point -> point.hasValue(321).hasAttributes(Attributes.empty()), + point -> + point + .hasValue(123) + .hasAttributesSatisfying( + equalTo(stringKey("test"), "test")))))); + + // sleep exporter interval + Thread.sleep(100); + testing.clearData(); + Thread.sleep(100); + + testing.waitAndAssertMetrics(instrumentationName, "test", AbstractIterableAssert::isEmpty); + } + + @Test + void doubleGauge() throws InterruptedException { + DoubleGaugeBuilder builder = meter.gaugeBuilder("test").setDescription("d").setUnit("u"); + assertThat(builder).isInstanceOf(ExtendedDoubleGaugeBuilder.class); + ExtendedDoubleGaugeBuilder extendedBuilder = (ExtendedDoubleGaugeBuilder) builder; + extendedBuilder.setAttributesAdvice(singletonList(stringKey("test"))); + + ObservableDoubleGauge observableGauge = + builder.buildWithCallback( + result -> + result.record(1.23, Attributes.of(stringKey("test"), "test", stringKey("q"), "r"))); + + testing.waitAndAssertMetrics( + instrumentationName, + "test", + metrics -> + metrics.anySatisfy( + metric -> + assertThat(metric) + .hasDescription("d") + .hasUnit("u") + .hasInstrumentationScope( + InstrumentationScopeInfo.builder(instrumentationName) + .setVersion("1.2.3") + .build()) + .hasDoubleGaugeSatisfying( + gauge -> + gauge.hasPointsSatisfying( + point -> + point + .hasValue(1.23) + .hasAttributesSatisfying( + equalTo(stringKey("test"), "test")))))); + + observableGauge.close(); + + // sleep exporter interval + Thread.sleep(100); + testing.clearData(); + Thread.sleep(100); + + testing.waitAndAssertMetrics(instrumentationName, "test", AbstractIterableAssert::isEmpty); + } + + @Test + void syncDoubleGauge() throws InterruptedException { + DoubleGaugeBuilder builder = meter.gaugeBuilder("test").setDescription("d").setUnit("u"); + assertThat(builder).isInstanceOf(ExtendedDoubleGaugeBuilder.class); + ExtendedDoubleGaugeBuilder extendedBuilder = (ExtendedDoubleGaugeBuilder) builder; + extendedBuilder.setAttributesAdvice(singletonList(stringKey("test"))); + + DoubleGauge doubleGauge = extendedBuilder.build(); + doubleGauge.set(3.21); + doubleGauge.set(1.23, Attributes.of(stringKey("test"), "test", stringKey("q"), "r")); + + testing.waitAndAssertMetrics( + instrumentationName, + "test", + metrics -> + metrics.anySatisfy( + metric -> + assertThat(metric) + .hasDescription("d") + .hasUnit("u") + .hasInstrumentationScope( + InstrumentationScopeInfo.builder(instrumentationName) + .setVersion("1.2.3") + .build()) + .hasDoubleGaugeSatisfying( + gauge -> + gauge.hasPointsSatisfying( + point -> point.hasValue(3.21).hasAttributes(Attributes.empty()), + point -> + point + .hasValue(1.23) + .hasAttributesSatisfying( + equalTo(stringKey("test"), "test")))))); + + // sleep exporter interval + Thread.sleep(100); + testing.clearData(); + Thread.sleep(100); + + testing.waitAndAssertMetrics(instrumentationName, "test", AbstractIterableAssert::isEmpty); + } +} diff --git a/instrumentation/opentelemetry-api/opentelemetry-api-1.32/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_32/OpenTelemetryApiInstrumentationModule.java b/instrumentation/opentelemetry-api/opentelemetry-api-1.32/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_32/OpenTelemetryApiInstrumentationModule.java new file mode 100644 index 000000000000..49e8dbd8eed1 --- /dev/null +++ b/instrumentation/opentelemetry-api/opentelemetry-api-1.32/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_32/OpenTelemetryApiInstrumentationModule.java @@ -0,0 +1,44 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.opentelemetryapi.v1_32; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.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 io.opentelemetry.javaagent.extension.instrumentation.internal.ExperimentalInstrumentationModule; +import java.util.List; +import net.bytebuddy.matcher.ElementMatcher; + +@AutoService(InstrumentationModule.class) +public class OpenTelemetryApiInstrumentationModule extends InstrumentationModule + implements ExperimentalInstrumentationModule { + public OpenTelemetryApiInstrumentationModule() { + super("opentelemetry-api", "opentelemetry-api-1.32"); + } + + @Override + public ElementMatcher.Junction classLoaderMatcher() { + // skip instrumentation when opentelemetry-extension-incubator is present, instrumentation is + // handled by OpenTelemetryApiIncubatorInstrumentationModule + return not( + hasClassesNamed( + "application.io.opentelemetry.extension.incubator.metrics.ExtendedDoubleHistogramBuilder")); + } + + @Override + public List typeInstrumentations() { + return singletonList(new OpenTelemetryInstrumentation()); + } + + @Override + public String getModuleGroup() { + return "opentelemetry-api-bridge"; + } +} diff --git a/instrumentation/opentelemetry-api/opentelemetry-api-1.32/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_32/OpenTelemetryInstrumentation.java b/instrumentation/opentelemetry-api/opentelemetry-api-1.32/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_32/OpenTelemetryInstrumentation.java new file mode 100644 index 000000000000..147c18b51b33 --- /dev/null +++ b/instrumentation/opentelemetry-api/opentelemetry-api-1.32/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_32/OpenTelemetryInstrumentation.java @@ -0,0 +1,40 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.opentelemetryapi.v1_32; + +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.none; + +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.opentelemetryapi.v1_32.metrics.ApplicationMeterFactory132; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class OpenTelemetryInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("application.io.opentelemetry.api.GlobalOpenTelemetry"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + none(), OpenTelemetryInstrumentation.class.getName() + "$InitAdvice"); + } + + @SuppressWarnings({"ReturnValueIgnored", "unused"}) + public static class InitAdvice { + @Advice.OnMethodEnter + public static void init() { + // the sole purpose of this advice is to ensure that ApplicationMeterFactory132 is recognized + // as helper class and injected into class loader + ApplicationMeterFactory132.class.getName(); + } + } +} diff --git a/instrumentation/opentelemetry-api/opentelemetry-api-1.32/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_32/incubator/OpenTelemetryApiIncubatorInstrumentationModule.java b/instrumentation/opentelemetry-api/opentelemetry-api-1.32/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_32/incubator/OpenTelemetryApiIncubatorInstrumentationModule.java new file mode 100644 index 000000000000..f4e67fc35fe5 --- /dev/null +++ b/instrumentation/opentelemetry-api/opentelemetry-api-1.32/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_32/incubator/OpenTelemetryApiIncubatorInstrumentationModule.java @@ -0,0 +1,42 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.opentelemetryapi.v1_32.incubator; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.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 io.opentelemetry.javaagent.extension.instrumentation.internal.ExperimentalInstrumentationModule; +import java.util.List; +import net.bytebuddy.matcher.ElementMatcher; + +@AutoService(InstrumentationModule.class) +public class OpenTelemetryApiIncubatorInstrumentationModule extends InstrumentationModule + implements ExperimentalInstrumentationModule { + public OpenTelemetryApiIncubatorInstrumentationModule() { + super("opentelemetry-api", "opentelemetry-api-1.32", "opentelemetry-api-incubator-1.32"); + } + + @Override + public ElementMatcher.Junction classLoaderMatcher() { + // skip instrumentation when opentelemetry-extension-incubator is not present, instrumentation + // is handled by OpenTelemetryApiInstrumentationModule + return hasClassesNamed( + "application.io.opentelemetry.extension.incubator.metrics.ExtendedDoubleHistogramBuilder"); + } + + @Override + public List typeInstrumentations() { + return singletonList(new OpenTelemetryIncubatorInstrumentation()); + } + + @Override + public String getModuleGroup() { + return "opentelemetry-api-bridge"; + } +} diff --git a/instrumentation/opentelemetry-api/opentelemetry-api-1.32/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_32/incubator/OpenTelemetryIncubatorInstrumentation.java b/instrumentation/opentelemetry-api/opentelemetry-api-1.32/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_32/incubator/OpenTelemetryIncubatorInstrumentation.java new file mode 100644 index 000000000000..76c1469c3a0d --- /dev/null +++ b/instrumentation/opentelemetry-api/opentelemetry-api-1.32/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_32/incubator/OpenTelemetryIncubatorInstrumentation.java @@ -0,0 +1,40 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.opentelemetryapi.v1_32.incubator; + +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.none; + +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.opentelemetryapi.v1_32.incubator.metrics.ApplicationMeterFactory132Incubator; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class OpenTelemetryIncubatorInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("application.io.opentelemetry.api.GlobalOpenTelemetry"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + none(), OpenTelemetryIncubatorInstrumentation.class.getName() + "$InitAdvice"); + } + + @SuppressWarnings({"ReturnValueIgnored", "unused"}) + public static class InitAdvice { + @Advice.OnMethodEnter + public static void init() { + // the sole purpose of this advice is to ensure that ApplicationMeterFactory132Incubator + // is recognized as helper class and injected into class loader + ApplicationMeterFactory132Incubator.class.getName(); + } + } +} diff --git a/instrumentation/opentelemetry-api/opentelemetry-api-1.32/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_32/incubator/metrics/ApplicationDoubleHistogramBuilder132Incubator.java b/instrumentation/opentelemetry-api/opentelemetry-api-1.32/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_32/incubator/metrics/ApplicationDoubleHistogramBuilder132Incubator.java new file mode 100644 index 000000000000..89e04a3b7026 --- /dev/null +++ b/instrumentation/opentelemetry-api/opentelemetry-api-1.32/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_32/incubator/metrics/ApplicationDoubleHistogramBuilder132Incubator.java @@ -0,0 +1,44 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.opentelemetryapi.v1_32.incubator.metrics; + +import application.io.opentelemetry.api.common.AttributeKey; +import application.io.opentelemetry.api.metrics.LongHistogramBuilder; +import application.io.opentelemetry.extension.incubator.metrics.ExtendedDoubleHistogramBuilder; +import io.opentelemetry.javaagent.instrumentation.opentelemetryapi.trace.Bridging; +import io.opentelemetry.javaagent.instrumentation.opentelemetryapi.v1_10.metrics.ApplicationDoubleHistogramBuilder; +import java.util.List; + +class ApplicationDoubleHistogramBuilder132Incubator extends ApplicationDoubleHistogramBuilder + implements ExtendedDoubleHistogramBuilder { + + private final io.opentelemetry.api.metrics.DoubleHistogramBuilder agentBuilder; + + ApplicationDoubleHistogramBuilder132Incubator( + io.opentelemetry.api.metrics.DoubleHistogramBuilder agentBuilder) { + super(agentBuilder); + this.agentBuilder = agentBuilder; + } + + @Override + public LongHistogramBuilder ofLongs() { + return new ApplicationLongHistogramBuilder132Incubator(agentBuilder.ofLongs()); + } + + @Override + public ExtendedDoubleHistogramBuilder setExplicitBucketBoundariesAdvice( + List bucketBoundaries) { + agentBuilder.setExplicitBucketBoundariesAdvice(bucketBoundaries); + return this; + } + + @Override + public ExtendedDoubleHistogramBuilder setAttributesAdvice(List> attributes) { + ((io.opentelemetry.api.incubator.metrics.ExtendedDoubleHistogramBuilder) agentBuilder) + .setAttributesAdvice(Bridging.toAgent(attributes)); + return this; + } +} diff --git a/instrumentation/opentelemetry-api/opentelemetry-api-1.32/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_32/incubator/metrics/ApplicationLongHistogramBuilder132Incubator.java b/instrumentation/opentelemetry-api/opentelemetry-api-1.32/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_32/incubator/metrics/ApplicationLongHistogramBuilder132Incubator.java new file mode 100644 index 000000000000..96a39a978bb3 --- /dev/null +++ b/instrumentation/opentelemetry-api/opentelemetry-api-1.32/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_32/incubator/metrics/ApplicationLongHistogramBuilder132Incubator.java @@ -0,0 +1,38 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.opentelemetryapi.v1_32.incubator.metrics; + +import application.io.opentelemetry.api.common.AttributeKey; +import application.io.opentelemetry.extension.incubator.metrics.ExtendedLongHistogramBuilder; +import io.opentelemetry.javaagent.instrumentation.opentelemetryapi.trace.Bridging; +import io.opentelemetry.javaagent.instrumentation.opentelemetryapi.v1_10.metrics.ApplicationLongHistogramBuilder; +import java.util.List; + +class ApplicationLongHistogramBuilder132Incubator extends ApplicationLongHistogramBuilder + implements ExtendedLongHistogramBuilder { + + private final io.opentelemetry.api.metrics.LongHistogramBuilder agentBuilder; + + ApplicationLongHistogramBuilder132Incubator( + io.opentelemetry.api.metrics.LongHistogramBuilder agentBuilder) { + super(agentBuilder); + this.agentBuilder = agentBuilder; + } + + @Override + public ExtendedLongHistogramBuilder setExplicitBucketBoundariesAdvice( + List bucketBoundaries) { + agentBuilder.setExplicitBucketBoundariesAdvice(bucketBoundaries); + return this; + } + + @Override + public ExtendedLongHistogramBuilder setAttributesAdvice(List> attributes) { + ((io.opentelemetry.api.incubator.metrics.ExtendedLongHistogramBuilder) agentBuilder) + .setAttributesAdvice(Bridging.toAgent(attributes)); + return this; + } +} diff --git a/instrumentation/opentelemetry-api/opentelemetry-api-1.32/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_32/incubator/metrics/ApplicationMeter132Incubator.java b/instrumentation/opentelemetry-api/opentelemetry-api-1.32/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_32/incubator/metrics/ApplicationMeter132Incubator.java new file mode 100644 index 000000000000..d8347dc3adec --- /dev/null +++ b/instrumentation/opentelemetry-api/opentelemetry-api-1.32/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_32/incubator/metrics/ApplicationMeter132Incubator.java @@ -0,0 +1,29 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.opentelemetryapi.v1_32.incubator.metrics; + +import application.io.opentelemetry.api.metrics.DoubleHistogramBuilder; +import io.opentelemetry.javaagent.instrumentation.opentelemetryapi.v1_10.metrics.ApplicationDoubleHistogramBuilder; +import io.opentelemetry.javaagent.instrumentation.opentelemetryapi.v1_31.incubator.metrics.ApplicationMeter131; + +class ApplicationMeter132Incubator extends ApplicationMeter131 { + + private final io.opentelemetry.api.metrics.Meter agentMeter; + + ApplicationMeter132Incubator(io.opentelemetry.api.metrics.Meter agentMeter) { + super(agentMeter); + this.agentMeter = agentMeter; + } + + @Override + public DoubleHistogramBuilder histogramBuilder(String name) { + io.opentelemetry.api.metrics.DoubleHistogramBuilder builder = agentMeter.histogramBuilder(name); + if (builder instanceof io.opentelemetry.api.incubator.metrics.ExtendedDoubleHistogramBuilder) { + return new ApplicationDoubleHistogramBuilder132Incubator(builder); + } + return new ApplicationDoubleHistogramBuilder(builder); + } +} diff --git a/instrumentation/opentelemetry-api/opentelemetry-api-1.32/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_32/incubator/metrics/ApplicationMeterFactory132Incubator.java b/instrumentation/opentelemetry-api/opentelemetry-api-1.32/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_32/incubator/metrics/ApplicationMeterFactory132Incubator.java new file mode 100644 index 000000000000..75ac0773c7a4 --- /dev/null +++ b/instrumentation/opentelemetry-api/opentelemetry-api-1.32/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_32/incubator/metrics/ApplicationMeterFactory132Incubator.java @@ -0,0 +1,18 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.opentelemetryapi.v1_32.incubator.metrics; + +import io.opentelemetry.api.metrics.Meter; +import io.opentelemetry.javaagent.instrumentation.opentelemetryapi.v1_10.metrics.ApplicationMeter; +import io.opentelemetry.javaagent.instrumentation.opentelemetryapi.v1_10.metrics.ApplicationMeterFactory; + +// this class is used from opentelemetry-api-1.27 via reflection +public final class ApplicationMeterFactory132Incubator implements ApplicationMeterFactory { + @Override + public ApplicationMeter newMeter(Meter agentMeter) { + return new ApplicationMeter132Incubator(agentMeter); + } +} diff --git a/instrumentation/opentelemetry-api/opentelemetry-api-1.32/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_32/metrics/ApplicationDoubleHistogramBuilder132.java b/instrumentation/opentelemetry-api/opentelemetry-api-1.32/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_32/metrics/ApplicationDoubleHistogramBuilder132.java new file mode 100644 index 000000000000..8b116a7b01fb --- /dev/null +++ b/instrumentation/opentelemetry-api/opentelemetry-api-1.32/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_32/metrics/ApplicationDoubleHistogramBuilder132.java @@ -0,0 +1,33 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.opentelemetryapi.v1_32.metrics; + +import application.io.opentelemetry.api.metrics.DoubleHistogramBuilder; +import application.io.opentelemetry.api.metrics.LongHistogramBuilder; +import io.opentelemetry.javaagent.instrumentation.opentelemetryapi.v1_10.metrics.ApplicationDoubleHistogramBuilder; +import java.util.List; + +class ApplicationDoubleHistogramBuilder132 extends ApplicationDoubleHistogramBuilder { + + private final io.opentelemetry.api.metrics.DoubleHistogramBuilder agentBuilder; + + ApplicationDoubleHistogramBuilder132( + io.opentelemetry.api.metrics.DoubleHistogramBuilder agentBuilder) { + super(agentBuilder); + this.agentBuilder = agentBuilder; + } + + @Override + public LongHistogramBuilder ofLongs() { + return new ApplicationLongHistogramBuilder132(agentBuilder.ofLongs()); + } + + @Override + public DoubleHistogramBuilder setExplicitBucketBoundariesAdvice(List bucketBoundaries) { + agentBuilder.setExplicitBucketBoundariesAdvice(bucketBoundaries); + return this; + } +} diff --git a/instrumentation/opentelemetry-api/opentelemetry-api-1.32/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_32/metrics/ApplicationLongHistogramBuilder132.java b/instrumentation/opentelemetry-api/opentelemetry-api-1.32/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_32/metrics/ApplicationLongHistogramBuilder132.java new file mode 100644 index 000000000000..fa494e250ef9 --- /dev/null +++ b/instrumentation/opentelemetry-api/opentelemetry-api-1.32/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_32/metrics/ApplicationLongHistogramBuilder132.java @@ -0,0 +1,27 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.opentelemetryapi.v1_32.metrics; + +import application.io.opentelemetry.api.metrics.LongHistogramBuilder; +import io.opentelemetry.javaagent.instrumentation.opentelemetryapi.v1_10.metrics.ApplicationLongHistogramBuilder; +import java.util.List; + +class ApplicationLongHistogramBuilder132 extends ApplicationLongHistogramBuilder { + + private final io.opentelemetry.api.metrics.LongHistogramBuilder agentBuilder; + + ApplicationLongHistogramBuilder132( + io.opentelemetry.api.metrics.LongHistogramBuilder agentBuilder) { + super(agentBuilder); + this.agentBuilder = agentBuilder; + } + + @Override + public LongHistogramBuilder setExplicitBucketBoundariesAdvice(List bucketBoundaries) { + agentBuilder.setExplicitBucketBoundariesAdvice(bucketBoundaries); + return this; + } +} diff --git a/instrumentation/opentelemetry-api/opentelemetry-api-1.32/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_32/metrics/ApplicationMeter132.java b/instrumentation/opentelemetry-api/opentelemetry-api-1.32/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_32/metrics/ApplicationMeter132.java new file mode 100644 index 000000000000..6958d86a613c --- /dev/null +++ b/instrumentation/opentelemetry-api/opentelemetry-api-1.32/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_32/metrics/ApplicationMeter132.java @@ -0,0 +1,24 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.opentelemetryapi.v1_32.metrics; + +import application.io.opentelemetry.api.metrics.DoubleHistogramBuilder; +import io.opentelemetry.javaagent.instrumentation.opentelemetryapi.v1_15.metrics.ApplicationMeter115; + +public class ApplicationMeter132 extends ApplicationMeter115 { + + private final io.opentelemetry.api.metrics.Meter agentMeter; + + public ApplicationMeter132(io.opentelemetry.api.metrics.Meter agentMeter) { + super(agentMeter); + this.agentMeter = agentMeter; + } + + @Override + public DoubleHistogramBuilder histogramBuilder(String name) { + return new ApplicationDoubleHistogramBuilder132(agentMeter.histogramBuilder(name)); + } +} diff --git a/instrumentation/opentelemetry-api/opentelemetry-api-1.32/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_32/metrics/ApplicationMeterFactory132.java b/instrumentation/opentelemetry-api/opentelemetry-api-1.32/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_32/metrics/ApplicationMeterFactory132.java new file mode 100644 index 000000000000..4adde3791bba --- /dev/null +++ b/instrumentation/opentelemetry-api/opentelemetry-api-1.32/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_32/metrics/ApplicationMeterFactory132.java @@ -0,0 +1,18 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.opentelemetryapi.v1_32.metrics; + +import io.opentelemetry.api.metrics.Meter; +import io.opentelemetry.javaagent.instrumentation.opentelemetryapi.v1_10.metrics.ApplicationMeter; +import io.opentelemetry.javaagent.instrumentation.opentelemetryapi.v1_10.metrics.ApplicationMeterFactory; + +// this class is used from opentelemetry-api-1.27 via reflection +public final class ApplicationMeterFactory132 implements ApplicationMeterFactory { + @Override + public ApplicationMeter newMeter(Meter agentMeter) { + return new ApplicationMeter132(agentMeter); + } +} diff --git a/instrumentation/opentelemetry-api/opentelemetry-api-1.32/javaagent/src/noopTest/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_32/incubator/metrics/NoopTest.java b/instrumentation/opentelemetry-api/opentelemetry-api-1.32/javaagent/src/noopTest/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_32/incubator/metrics/NoopTest.java new file mode 100644 index 000000000000..91a555add5a5 --- /dev/null +++ b/instrumentation/opentelemetry-api/opentelemetry-api-1.32/javaagent/src/noopTest/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_32/incubator/metrics/NoopTest.java @@ -0,0 +1,43 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.opentelemetryapi.v1_32.incubator.metrics; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.metrics.DoubleGaugeBuilder; +import io.opentelemetry.api.metrics.DoubleHistogramBuilder; +import io.opentelemetry.api.metrics.LongCounterBuilder; +import io.opentelemetry.api.metrics.LongUpDownCounterBuilder; +import io.opentelemetry.api.metrics.Meter; +import io.opentelemetry.extension.incubator.metrics.ExtendedDoubleGaugeBuilder; +import io.opentelemetry.extension.incubator.metrics.ExtendedDoubleHistogramBuilder; +import io.opentelemetry.extension.incubator.metrics.ExtendedLongCounterBuilder; +import io.opentelemetry.extension.incubator.metrics.ExtendedLongUpDownCounterBuilder; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +class NoopTest { + @RegisterExtension + static final AgentInstrumentationExtension testing = AgentInstrumentationExtension.create(); + + @Test + void noopInstance() { + Meter meter = testing.getOpenTelemetry().getMeter("test"); + + LongCounterBuilder counterBuilder = meter.counterBuilder("test"); + assertThat(counterBuilder).isNotInstanceOf(ExtendedLongCounterBuilder.class); + + LongUpDownCounterBuilder upDownCounterBuilder = meter.upDownCounterBuilder("test"); + assertThat(upDownCounterBuilder).isNotInstanceOf(ExtendedLongUpDownCounterBuilder.class); + + DoubleGaugeBuilder gaugeBuilder = meter.gaugeBuilder("test"); + assertThat(gaugeBuilder).isNotInstanceOf(ExtendedDoubleGaugeBuilder.class); + + DoubleHistogramBuilder histogramBuilder = meter.histogramBuilder("test"); + assertThat(histogramBuilder).isNotInstanceOf(ExtendedDoubleHistogramBuilder.class); + } +} diff --git a/instrumentation/opentelemetry-api/opentelemetry-api-1.32/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_32/metrics/MeterTest.java b/instrumentation/opentelemetry-api/opentelemetry-api-1.32/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_32/metrics/MeterTest.java new file mode 100644 index 000000000000..aae3a68e7418 --- /dev/null +++ b/instrumentation/opentelemetry-api/opentelemetry-api-1.32/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_32/metrics/MeterTest.java @@ -0,0 +1,116 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.opentelemetryapi.v1_32.metrics; + +import static io.opentelemetry.api.common.AttributeKey.stringKey; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; +import static java.util.Collections.singletonList; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.metrics.DoubleHistogram; +import io.opentelemetry.api.metrics.DoubleHistogramBuilder; +import io.opentelemetry.api.metrics.LongHistogram; +import io.opentelemetry.api.metrics.LongHistogramBuilder; +import io.opentelemetry.api.metrics.Meter; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.sdk.common.InstrumentationScopeInfo; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInfo; +import org.junit.jupiter.api.extension.RegisterExtension; + +class MeterTest { + + @RegisterExtension + static final AgentInstrumentationExtension testing = AgentInstrumentationExtension.create(); + + private String instrumentationName; + private Meter meter; + + @BeforeEach + void setupMeter(TestInfo test) { + instrumentationName = "test-" + test.getDisplayName(); + meter = + testing + .getOpenTelemetry() + .getMeterProvider() + .meterBuilder(instrumentationName) + .setInstrumentationVersion("1.2.3") + .setSchemaUrl("http://schema.org") + .build(); + } + + @Test + void longHistogram() { + LongHistogramBuilder builder = + meter.histogramBuilder("test").ofLongs().setDescription("d").setUnit("u"); + builder.setExplicitBucketBoundariesAdvice(singletonList(10L)); + + LongHistogram instrument = builder.build(); + + instrument.record(5, Attributes.of(stringKey("test"), "test")); + instrument.record(6, Attributes.of(stringKey("test"), "test")); + + testing.waitAndAssertMetrics( + instrumentationName, + "test", + metrics -> + metrics.anySatisfy( + metric -> + assertThat(metric) + .hasDescription("d") + .hasUnit("u") + .hasInstrumentationScope( + InstrumentationScopeInfo.builder(instrumentationName) + .setVersion("1.2.3") + .build()) + .hasHistogramSatisfying( + histogram -> + histogram.hasPointsSatisfying( + point -> + point + .hasSum(11.0) + .hasBucketBoundaries(10.0) + .hasAttributesSatisfying( + equalTo(stringKey("test"), "test")))))); + } + + @Test + void doubleHistogram() { + DoubleHistogramBuilder builder = + meter.histogramBuilder("test").setDescription("d").setUnit("u"); + builder.setExplicitBucketBoundariesAdvice(singletonList(10.0)); + + DoubleHistogram instrument = builder.build(); + + instrument.record(5.5, Attributes.of(stringKey("test"), "test")); + instrument.record(6.6, Attributes.of(stringKey("test"), "test")); + + testing.waitAndAssertMetrics( + instrumentationName, + "test", + metrics -> + metrics.anySatisfy( + metric -> + assertThat(metric) + .hasDescription("d") + .hasUnit("u") + .hasInstrumentationScope( + InstrumentationScopeInfo.builder(instrumentationName) + .setVersion("1.2.3") + .build()) + .hasHistogramSatisfying( + histogram -> + histogram.hasPointsSatisfying( + point -> + point + .hasSum(12.1) + .hasBucketBoundaries(10.0) + .hasAttributesSatisfying( + equalTo(stringKey("test"), "test")))))); + } +} diff --git a/instrumentation/opentelemetry-api/opentelemetry-api-1.37/javaagent/build.gradle.kts b/instrumentation/opentelemetry-api/opentelemetry-api-1.37/javaagent/build.gradle.kts new file mode 100644 index 000000000000..0298ea52c101 --- /dev/null +++ b/instrumentation/opentelemetry-api/opentelemetry-api-1.37/javaagent/build.gradle.kts @@ -0,0 +1,70 @@ +plugins { + id("otel.javaagent-instrumentation") +} + +dependencies { + compileOnly(project(":opentelemetry-api-shaded-for-instrumenting", configuration = "v1_37")) + compileOnly("io.opentelemetry:opentelemetry-api-incubator") + + implementation(project(":instrumentation:opentelemetry-api:opentelemetry-api-1.0:javaagent")) + implementation(project(":instrumentation:opentelemetry-api:opentelemetry-api-1.4:javaagent")) + implementation(project(":instrumentation:opentelemetry-api:opentelemetry-api-1.10:javaagent")) + implementation(project(":instrumentation:opentelemetry-api:opentelemetry-api-1.15:javaagent")) + implementation(project(":instrumentation:opentelemetry-api:opentelemetry-api-1.27:javaagent")) + implementation(project(":instrumentation:opentelemetry-api:opentelemetry-api-1.31:javaagent")) + implementation(project(":instrumentation:opentelemetry-api:opentelemetry-api-1.32:javaagent")) +} + +configurations.configureEach { + if (name.endsWith("testRuntimeClasspath", true) || name.endsWith("testCompileClasspath", true)) { + resolutionStrategy { + force("io.opentelemetry:opentelemetry-api:1.37.0") + force("io.opentelemetry:opentelemetry-api-incubator:1.37.0-alpha") + } + if (name.startsWith("incubatorTest") || name.startsWith("noopTest")) { + resolutionStrategy { + force("io.opentelemetry:opentelemetry-api-incubator:1.37.0-alpha") + } + } else if (name.startsWith("oldAndNewIncubatorTest")) { + resolutionStrategy { + force("io.opentelemetry:opentelemetry-api-incubator:1.37.0-alpha") + force("io.opentelemetry:opentelemetry-extension-incubator:1.32.0-alpha") + } + } + } +} + +testing { + suites { + val incubatorTest by registering(JvmTestSuite::class) { + dependencies { + implementation("io.opentelemetry:opentelemetry-api-incubator:1.37.0-alpha") + } + } + val oldAndNewIncubatorTest by registering(JvmTestSuite::class) { + dependencies { + implementation("io.opentelemetry:opentelemetry-api-incubator:1.37.0-alpha") + implementation("io.opentelemetry:opentelemetry-extension-incubator:1.32.0-alpha") + } + } + val noopTest by registering(JvmTestSuite::class) { + dependencies { + implementation("io.opentelemetry:opentelemetry-api-incubator:1.37.0-alpha") + } + + targets { + all { + testTask.configure { + jvmArgs("-Dtesting.exporter.enabled=false") + } + } + } + } + } +} + +tasks { + check { + dependsOn(testing.suites) + } +} diff --git a/instrumentation/opentelemetry-api/opentelemetry-api-1.37/javaagent/src/incubatorTest/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_37/incubator/metrics/MeterTest.java b/instrumentation/opentelemetry-api/opentelemetry-api-1.37/javaagent/src/incubatorTest/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_37/incubator/metrics/MeterTest.java new file mode 100644 index 000000000000..ca78917ba4a9 --- /dev/null +++ b/instrumentation/opentelemetry-api/opentelemetry-api-1.37/javaagent/src/incubatorTest/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_37/incubator/metrics/MeterTest.java @@ -0,0 +1,467 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.opentelemetryapi.v1_37.incubator.metrics; + +import static io.opentelemetry.api.common.AttributeKey.stringKey; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; +import static java.util.Collections.singletonList; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.incubator.metrics.DoubleGauge; +import io.opentelemetry.api.incubator.metrics.ExtendedDoubleCounterBuilder; +import io.opentelemetry.api.incubator.metrics.ExtendedDoubleGaugeBuilder; +import io.opentelemetry.api.incubator.metrics.ExtendedDoubleHistogramBuilder; +import io.opentelemetry.api.incubator.metrics.ExtendedDoubleUpDownCounterBuilder; +import io.opentelemetry.api.incubator.metrics.ExtendedLongCounterBuilder; +import io.opentelemetry.api.incubator.metrics.ExtendedLongGaugeBuilder; +import io.opentelemetry.api.incubator.metrics.ExtendedLongHistogramBuilder; +import io.opentelemetry.api.incubator.metrics.ExtendedLongUpDownCounterBuilder; +import io.opentelemetry.api.incubator.metrics.LongGauge; +import io.opentelemetry.api.metrics.DoubleCounter; +import io.opentelemetry.api.metrics.DoubleCounterBuilder; +import io.opentelemetry.api.metrics.DoubleGaugeBuilder; +import io.opentelemetry.api.metrics.DoubleHistogram; +import io.opentelemetry.api.metrics.DoubleHistogramBuilder; +import io.opentelemetry.api.metrics.DoubleUpDownCounter; +import io.opentelemetry.api.metrics.DoubleUpDownCounterBuilder; +import io.opentelemetry.api.metrics.LongCounter; +import io.opentelemetry.api.metrics.LongCounterBuilder; +import io.opentelemetry.api.metrics.LongGaugeBuilder; +import io.opentelemetry.api.metrics.LongHistogram; +import io.opentelemetry.api.metrics.LongHistogramBuilder; +import io.opentelemetry.api.metrics.LongUpDownCounter; +import io.opentelemetry.api.metrics.LongUpDownCounterBuilder; +import io.opentelemetry.api.metrics.Meter; +import io.opentelemetry.api.metrics.ObservableDoubleGauge; +import io.opentelemetry.api.metrics.ObservableLongGauge; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.sdk.common.InstrumentationScopeInfo; +import org.assertj.core.api.AbstractIterableAssert; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInfo; +import org.junit.jupiter.api.extension.RegisterExtension; + +class MeterTest { + + @RegisterExtension + static final AgentInstrumentationExtension testing = AgentInstrumentationExtension.create(); + + private String instrumentationName; + private Meter meter; + + @BeforeEach + void setupMeter(TestInfo test) { + instrumentationName = "test-" + test.getDisplayName(); + meter = + testing + .getOpenTelemetry() + .getMeterProvider() + .meterBuilder(instrumentationName) + .setInstrumentationVersion("1.2.3") + .setSchemaUrl("http://schema.org") + .build(); + } + + @Test + void longCounter() { + LongCounterBuilder builder = meter.counterBuilder("test").setDescription("d").setUnit("u"); + assertThat(builder).isInstanceOf(ExtendedLongCounterBuilder.class); + ExtendedLongCounterBuilder extendedBuilder = (ExtendedLongCounterBuilder) builder; + extendedBuilder.setAttributesAdvice(singletonList(stringKey("test"))); + + LongCounter instrument = builder.build(); + + instrument.add(5, Attributes.of(stringKey("test"), "test", stringKey("q"), "r")); + instrument.add(6, Attributes.of(stringKey("test"), "test", stringKey("q"), "r")); + + testing.waitAndAssertMetrics( + instrumentationName, + "test", + metrics -> + metrics.anySatisfy( + metric -> + assertThat(metric) + .hasDescription("d") + .hasUnit("u") + .hasInstrumentationScope( + InstrumentationScopeInfo.builder(instrumentationName) + .setVersion("1.2.3") + .build()) + .hasLongSumSatisfying( + sum -> + sum.isMonotonic() + .hasPointsSatisfying( + point -> + point + .hasValue(11) + .hasAttributesSatisfying( + equalTo(stringKey("test"), "test")))))); + } + + @Test + void doubleCounter() { + DoubleCounterBuilder builder = + meter.counterBuilder("test").ofDoubles().setDescription("d").setUnit("u"); + assertThat(builder).isInstanceOf(ExtendedDoubleCounterBuilder.class); + ExtendedDoubleCounterBuilder extendedBuilder = (ExtendedDoubleCounterBuilder) builder; + extendedBuilder.setAttributesAdvice(singletonList(stringKey("test"))); + + DoubleCounter instrument = builder.build(); + + instrument.add(5.5, Attributes.of(stringKey("test"), "test", stringKey("q"), "r")); + instrument.add(6.6, Attributes.of(stringKey("test"), "test", stringKey("q"), "r")); + + testing.waitAndAssertMetrics( + instrumentationName, + "test", + metrics -> + metrics.anySatisfy( + metric -> + assertThat(metric) + .hasDescription("d") + .hasUnit("u") + .hasInstrumentationScope( + InstrumentationScopeInfo.builder(instrumentationName) + .setVersion("1.2.3") + .build()) + .hasDoubleSumSatisfying( + sum -> + sum.isMonotonic() + .hasPointsSatisfying( + point -> + point + .hasValue(12.1) + .hasAttributesSatisfying( + equalTo(stringKey("test"), "test")))))); + } + + @Test + void longUpDownCounter() { + LongUpDownCounterBuilder builder = + meter.upDownCounterBuilder("test").setDescription("d").setUnit("u"); + assertThat(builder).isInstanceOf(ExtendedLongUpDownCounterBuilder.class); + ExtendedLongUpDownCounterBuilder extendedBuilder = (ExtendedLongUpDownCounterBuilder) builder; + extendedBuilder.setAttributesAdvice(singletonList(stringKey("test"))); + + LongUpDownCounter instrument = builder.build(); + + instrument.add(5, Attributes.of(stringKey("test"), "test", stringKey("q"), "r")); + instrument.add(6, Attributes.of(stringKey("test"), "test", stringKey("q"), "r")); + + testing.waitAndAssertMetrics( + instrumentationName, + "test", + metrics -> + metrics.anySatisfy( + metric -> + assertThat(metric) + .hasDescription("d") + .hasUnit("u") + .hasInstrumentationScope( + InstrumentationScopeInfo.builder(instrumentationName) + .setVersion("1.2.3") + .build()) + .hasLongSumSatisfying( + sum -> + sum.isNotMonotonic() + .hasPointsSatisfying( + point -> + point + .hasValue(11) + .hasAttributesSatisfying( + equalTo(stringKey("test"), "test")))))); + } + + @Test + void doubleUpDownCounter() { + DoubleUpDownCounterBuilder builder = + meter.upDownCounterBuilder("test").ofDoubles().setDescription("d").setUnit("u"); + assertThat(builder).isInstanceOf(ExtendedDoubleUpDownCounterBuilder.class); + ExtendedDoubleUpDownCounterBuilder extendedBuilder = + (ExtendedDoubleUpDownCounterBuilder) builder; + extendedBuilder.setAttributesAdvice(singletonList(stringKey("test"))); + + DoubleUpDownCounter instrument = builder.build(); + + instrument.add(5.5, Attributes.of(stringKey("test"), "test", stringKey("q"), "r")); + instrument.add(6.6, Attributes.of(stringKey("test"), "test", stringKey("q"), "r")); + + testing.waitAndAssertMetrics( + instrumentationName, + "test", + metrics -> + metrics.anySatisfy( + metric -> + assertThat(metric) + .hasDescription("d") + .hasUnit("u") + .hasInstrumentationScope( + InstrumentationScopeInfo.builder(instrumentationName) + .setVersion("1.2.3") + .build()) + .hasDoubleSumSatisfying( + sum -> + sum.isNotMonotonic() + .hasPointsSatisfying( + point -> + point + .hasValue(12.1) + .hasAttributesSatisfying( + equalTo(stringKey("test"), "test")))))); + } + + @Test + void longHistogram() { + LongHistogramBuilder builder = + meter.histogramBuilder("test").ofLongs().setDescription("d").setUnit("u"); + builder.setExplicitBucketBoundariesAdvice(singletonList(10L)); + assertThat(builder).isInstanceOf(ExtendedLongHistogramBuilder.class); + ExtendedLongHistogramBuilder extendedBuilder = (ExtendedLongHistogramBuilder) builder; + extendedBuilder.setAttributesAdvice(singletonList(stringKey("test"))); + + LongHistogram instrument = builder.build(); + + instrument.record(5, Attributes.of(stringKey("test"), "test", stringKey("q"), "r")); + instrument.record(6, Attributes.of(stringKey("test"), "test", stringKey("q"), "r")); + + testing.waitAndAssertMetrics( + instrumentationName, + "test", + metrics -> + metrics.anySatisfy( + metric -> + assertThat(metric) + .hasDescription("d") + .hasUnit("u") + .hasInstrumentationScope( + InstrumentationScopeInfo.builder(instrumentationName) + .setVersion("1.2.3") + .build()) + .hasHistogramSatisfying( + histogram -> + histogram.hasPointsSatisfying( + point -> + point + .hasSum(11.0) + .hasBucketBoundaries(10.0) + .hasAttributesSatisfying( + equalTo(stringKey("test"), "test")))))); + } + + @Test + void doubleHistogram() { + DoubleHistogramBuilder builder = + meter.histogramBuilder("test").setDescription("d").setUnit("u"); + builder.setExplicitBucketBoundariesAdvice(singletonList(10.0)); + assertThat(builder).isInstanceOf(ExtendedDoubleHistogramBuilder.class); + ExtendedDoubleHistogramBuilder extendedBuilder = (ExtendedDoubleHistogramBuilder) builder; + extendedBuilder.setAttributesAdvice(singletonList(stringKey("test"))); + + DoubleHistogram instrument = builder.build(); + + instrument.record(5.5, Attributes.of(stringKey("test"), "test", stringKey("q"), "r")); + instrument.record(6.6, Attributes.of(stringKey("test"), "test", stringKey("q"), "r")); + + testing.waitAndAssertMetrics( + instrumentationName, + "test", + metrics -> + metrics.anySatisfy( + metric -> + assertThat(metric) + .hasDescription("d") + .hasUnit("u") + .hasInstrumentationScope( + InstrumentationScopeInfo.builder(instrumentationName) + .setVersion("1.2.3") + .build()) + .hasHistogramSatisfying( + histogram -> + histogram.hasPointsSatisfying( + point -> + point + .hasSum(12.1) + .hasBucketBoundaries(10.0) + .hasAttributesSatisfying( + equalTo(stringKey("test"), "test")))))); + } + + @Test + void longGauge() throws InterruptedException { + LongGaugeBuilder builder = + meter.gaugeBuilder("test").ofLongs().setDescription("d").setUnit("u"); + assertThat(builder).isInstanceOf(ExtendedLongGaugeBuilder.class); + ExtendedLongGaugeBuilder extendedBuilder = (ExtendedLongGaugeBuilder) builder; + extendedBuilder.setAttributesAdvice(singletonList(stringKey("test"))); + + ObservableLongGauge observableGauge = + builder.buildWithCallback( + result -> + result.record(123, Attributes.of(stringKey("test"), "test", stringKey("q"), "r"))); + + testing.waitAndAssertMetrics( + instrumentationName, + "test", + metrics -> + metrics.anySatisfy( + metric -> + assertThat(metric) + .hasDescription("d") + .hasUnit("u") + .hasInstrumentationScope( + InstrumentationScopeInfo.builder(instrumentationName) + .setVersion("1.2.3") + .build()) + .hasLongGaugeSatisfying( + gauge -> + gauge.hasPointsSatisfying( + point -> + point + .hasValue(123) + .hasAttributesSatisfying( + equalTo(stringKey("test"), "test")))))); + + observableGauge.close(); + + // sleep exporter interval + Thread.sleep(100); + testing.clearData(); + Thread.sleep(100); + + testing.waitAndAssertMetrics(instrumentationName, "test", AbstractIterableAssert::isEmpty); + } + + @Test + void syncLongGauge() throws InterruptedException { + LongGaugeBuilder builder = + meter.gaugeBuilder("test").ofLongs().setDescription("d").setUnit("u"); + assertThat(builder).isInstanceOf(ExtendedLongGaugeBuilder.class); + ExtendedLongGaugeBuilder extendedBuilder = (ExtendedLongGaugeBuilder) builder; + extendedBuilder.setAttributesAdvice(singletonList(stringKey("test"))); + + LongGauge longGauge = extendedBuilder.build(); + longGauge.set(321); + longGauge.set(123, Attributes.of(stringKey("test"), "test", stringKey("q"), "r")); + + testing.waitAndAssertMetrics( + instrumentationName, + "test", + metrics -> + metrics.anySatisfy( + metric -> + assertThat(metric) + .hasDescription("d") + .hasUnit("u") + .hasInstrumentationScope( + InstrumentationScopeInfo.builder(instrumentationName) + .setVersion("1.2.3") + .build()) + .hasLongGaugeSatisfying( + gauge -> + gauge.hasPointsSatisfying( + point -> point.hasValue(321).hasAttributes(Attributes.empty()), + point -> + point + .hasValue(123) + .hasAttributesSatisfying( + equalTo(stringKey("test"), "test")))))); + + // sleep exporter interval + Thread.sleep(100); + testing.clearData(); + Thread.sleep(100); + + testing.waitAndAssertMetrics(instrumentationName, "test", AbstractIterableAssert::isEmpty); + } + + @Test + void doubleGauge() throws InterruptedException { + DoubleGaugeBuilder builder = meter.gaugeBuilder("test").setDescription("d").setUnit("u"); + assertThat(builder).isInstanceOf(ExtendedDoubleGaugeBuilder.class); + ExtendedDoubleGaugeBuilder extendedBuilder = (ExtendedDoubleGaugeBuilder) builder; + extendedBuilder.setAttributesAdvice(singletonList(stringKey("test"))); + + ObservableDoubleGauge observableGauge = + builder.buildWithCallback( + result -> + result.record(1.23, Attributes.of(stringKey("test"), "test", stringKey("q"), "r"))); + + testing.waitAndAssertMetrics( + instrumentationName, + "test", + metrics -> + metrics.anySatisfy( + metric -> + assertThat(metric) + .hasDescription("d") + .hasUnit("u") + .hasInstrumentationScope( + InstrumentationScopeInfo.builder(instrumentationName) + .setVersion("1.2.3") + .build()) + .hasDoubleGaugeSatisfying( + gauge -> + gauge.hasPointsSatisfying( + point -> + point + .hasValue(1.23) + .hasAttributesSatisfying( + equalTo(stringKey("test"), "test")))))); + + observableGauge.close(); + + // sleep exporter interval + Thread.sleep(100); + testing.clearData(); + Thread.sleep(100); + + testing.waitAndAssertMetrics(instrumentationName, "test", AbstractIterableAssert::isEmpty); + } + + @Test + void syncDoubleGauge() throws InterruptedException { + DoubleGaugeBuilder builder = meter.gaugeBuilder("test").setDescription("d").setUnit("u"); + assertThat(builder).isInstanceOf(ExtendedDoubleGaugeBuilder.class); + ExtendedDoubleGaugeBuilder extendedBuilder = (ExtendedDoubleGaugeBuilder) builder; + extendedBuilder.setAttributesAdvice(singletonList(stringKey("test"))); + + DoubleGauge doubleGauge = extendedBuilder.build(); + doubleGauge.set(3.21); + doubleGauge.set(1.23, Attributes.of(stringKey("test"), "test", stringKey("q"), "r")); + + testing.waitAndAssertMetrics( + instrumentationName, + "test", + metrics -> + metrics.anySatisfy( + metric -> + assertThat(metric) + .hasDescription("d") + .hasUnit("u") + .hasInstrumentationScope( + InstrumentationScopeInfo.builder(instrumentationName) + .setVersion("1.2.3") + .build()) + .hasDoubleGaugeSatisfying( + gauge -> + gauge.hasPointsSatisfying( + point -> point.hasValue(3.21).hasAttributes(Attributes.empty()), + point -> + point + .hasValue(1.23) + .hasAttributesSatisfying( + equalTo(stringKey("test"), "test")))))); + + // sleep exporter interval + Thread.sleep(100); + testing.clearData(); + Thread.sleep(100); + + testing.waitAndAssertMetrics(instrumentationName, "test", AbstractIterableAssert::isEmpty); + } +} diff --git a/instrumentation/opentelemetry-api/opentelemetry-api-1.37/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_37/incubator/OpenTelemetryApiInstrumentationModule.java b/instrumentation/opentelemetry-api/opentelemetry-api-1.37/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_37/incubator/OpenTelemetryApiInstrumentationModule.java new file mode 100644 index 000000000000..5971d77a6ade --- /dev/null +++ b/instrumentation/opentelemetry-api/opentelemetry-api-1.37/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_37/incubator/OpenTelemetryApiInstrumentationModule.java @@ -0,0 +1,40 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.opentelemetryapi.v1_37.incubator; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.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 io.opentelemetry.javaagent.extension.instrumentation.internal.ExperimentalInstrumentationModule; +import java.util.List; +import net.bytebuddy.matcher.ElementMatcher; + +@AutoService(InstrumentationModule.class) +public class OpenTelemetryApiInstrumentationModule extends InstrumentationModule + implements ExperimentalInstrumentationModule { + public OpenTelemetryApiInstrumentationModule() { + super("opentelemetry-api", "opentelemetry-api-1.37", "opentelemetry-api-incubator-1.37"); + } + + @Override + public ElementMatcher.Junction classLoaderMatcher() { + // skip instrumentation when opentelemetry-api-incubator is not present + return hasClassesNamed("application.io.opentelemetry.api.incubator.metrics.DoubleGauge"); + } + + @Override + public List typeInstrumentations() { + return singletonList(new OpenTelemetryInstrumentation()); + } + + @Override + public String getModuleGroup() { + return "opentelemetry-api-bridge"; + } +} diff --git a/instrumentation/opentelemetry-api/opentelemetry-api-1.37/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_37/incubator/OpenTelemetryInstrumentation.java b/instrumentation/opentelemetry-api/opentelemetry-api-1.37/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_37/incubator/OpenTelemetryInstrumentation.java new file mode 100644 index 000000000000..34fa24172e96 --- /dev/null +++ b/instrumentation/opentelemetry-api/opentelemetry-api-1.37/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_37/incubator/OpenTelemetryInstrumentation.java @@ -0,0 +1,40 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.opentelemetryapi.v1_37.incubator; + +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.none; + +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.opentelemetryapi.v1_37.incubator.metrics.ApplicationMeterFactory137; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class OpenTelemetryInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("application.io.opentelemetry.api.GlobalOpenTelemetry"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + none(), OpenTelemetryInstrumentation.class.getName() + "$InitAdvice"); + } + + @SuppressWarnings({"ReturnValueIgnored", "unused"}) + public static class InitAdvice { + @Advice.OnMethodEnter + public static void init() { + // the sole purpose of this advice is to ensure that ApplicationMeterFactory137 + // is recognized as helper class and injected into class loader + ApplicationMeterFactory137.class.getName(); + } + } +} diff --git a/instrumentation/opentelemetry-api/opentelemetry-api-1.37/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_37/incubator/metrics/ApplicationDoubleCounterBuilder137.java b/instrumentation/opentelemetry-api/opentelemetry-api-1.37/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_37/incubator/metrics/ApplicationDoubleCounterBuilder137.java new file mode 100644 index 000000000000..821d340b9573 --- /dev/null +++ b/instrumentation/opentelemetry-api/opentelemetry-api-1.37/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_37/incubator/metrics/ApplicationDoubleCounterBuilder137.java @@ -0,0 +1,31 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.opentelemetryapi.v1_37.incubator.metrics; + +import application.io.opentelemetry.api.common.AttributeKey; +import application.io.opentelemetry.api.incubator.metrics.ExtendedDoubleCounterBuilder; +import io.opentelemetry.javaagent.instrumentation.opentelemetryapi.trace.Bridging; +import io.opentelemetry.javaagent.instrumentation.opentelemetryapi.v1_10.metrics.ApplicationDoubleCounterBuilder; +import java.util.List; + +final class ApplicationDoubleCounterBuilder137 extends ApplicationDoubleCounterBuilder + implements ExtendedDoubleCounterBuilder { + + private final io.opentelemetry.api.metrics.DoubleCounterBuilder agentBuilder; + + ApplicationDoubleCounterBuilder137( + io.opentelemetry.api.metrics.DoubleCounterBuilder agentBuilder) { + super(agentBuilder); + this.agentBuilder = agentBuilder; + } + + @Override + public ExtendedDoubleCounterBuilder setAttributesAdvice(List> attributes) { + ((io.opentelemetry.api.incubator.metrics.ExtendedDoubleCounterBuilder) agentBuilder) + .setAttributesAdvice(Bridging.toAgent(attributes)); + return this; + } +} diff --git a/instrumentation/opentelemetry-api/opentelemetry-api-1.37/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_37/incubator/metrics/ApplicationDoubleGaugeBuilder137.java b/instrumentation/opentelemetry-api/opentelemetry-api-1.37/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_37/incubator/metrics/ApplicationDoubleGaugeBuilder137.java new file mode 100644 index 000000000000..d628a9407bff --- /dev/null +++ b/instrumentation/opentelemetry-api/opentelemetry-api-1.37/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_37/incubator/metrics/ApplicationDoubleGaugeBuilder137.java @@ -0,0 +1,55 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.opentelemetryapi.v1_37.incubator.metrics; + +import application.io.opentelemetry.api.common.AttributeKey; +import application.io.opentelemetry.api.common.Attributes; +import application.io.opentelemetry.api.incubator.metrics.DoubleGauge; +import application.io.opentelemetry.api.incubator.metrics.ExtendedDoubleGaugeBuilder; +import application.io.opentelemetry.api.metrics.LongGaugeBuilder; +import io.opentelemetry.javaagent.instrumentation.opentelemetryapi.trace.Bridging; +import io.opentelemetry.javaagent.instrumentation.opentelemetryapi.v1_10.metrics.ApplicationDoubleGaugeBuilder; +import java.util.List; + +final class ApplicationDoubleGaugeBuilder137 extends ApplicationDoubleGaugeBuilder + implements ExtendedDoubleGaugeBuilder { + + private final io.opentelemetry.api.metrics.DoubleGaugeBuilder agentBuilder; + + ApplicationDoubleGaugeBuilder137(io.opentelemetry.api.metrics.DoubleGaugeBuilder agentBuilder) { + super(agentBuilder); + this.agentBuilder = agentBuilder; + } + + @Override + public LongGaugeBuilder ofLongs() { + return new ApplicationLongGaugeBuilder137(agentBuilder.ofLongs()); + } + + @Override + public DoubleGauge build() { + io.opentelemetry.api.metrics.DoubleGauge agentDoubleGauge = agentBuilder.build(); + return new DoubleGauge() { + + @Override + public void set(double value) { + agentDoubleGauge.set(value); + } + + @Override + public void set(double value, Attributes attributes) { + agentDoubleGauge.set(value, Bridging.toAgent(attributes)); + } + }; + } + + @Override + public ExtendedDoubleGaugeBuilder setAttributesAdvice(List> attributes) { + ((io.opentelemetry.api.incubator.metrics.ExtendedDoubleGaugeBuilder) agentBuilder) + .setAttributesAdvice(Bridging.toAgent(attributes)); + return this; + } +} diff --git a/instrumentation/opentelemetry-api/opentelemetry-api-1.37/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_37/incubator/metrics/ApplicationDoubleHistogramBuilder137.java b/instrumentation/opentelemetry-api/opentelemetry-api-1.37/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_37/incubator/metrics/ApplicationDoubleHistogramBuilder137.java new file mode 100644 index 000000000000..71abc45bc6c0 --- /dev/null +++ b/instrumentation/opentelemetry-api/opentelemetry-api-1.37/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_37/incubator/metrics/ApplicationDoubleHistogramBuilder137.java @@ -0,0 +1,44 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.opentelemetryapi.v1_37.incubator.metrics; + +import application.io.opentelemetry.api.common.AttributeKey; +import application.io.opentelemetry.api.incubator.metrics.ExtendedDoubleHistogramBuilder; +import application.io.opentelemetry.api.metrics.LongHistogramBuilder; +import io.opentelemetry.javaagent.instrumentation.opentelemetryapi.trace.Bridging; +import io.opentelemetry.javaagent.instrumentation.opentelemetryapi.v1_10.metrics.ApplicationDoubleHistogramBuilder; +import java.util.List; + +final class ApplicationDoubleHistogramBuilder137 extends ApplicationDoubleHistogramBuilder + implements ExtendedDoubleHistogramBuilder { + + private final io.opentelemetry.api.metrics.DoubleHistogramBuilder agentBuilder; + + ApplicationDoubleHistogramBuilder137( + io.opentelemetry.api.metrics.DoubleHistogramBuilder agentBuilder) { + super(agentBuilder); + this.agentBuilder = agentBuilder; + } + + @Override + public LongHistogramBuilder ofLongs() { + return new ApplicationLongHistogramBuilder137(agentBuilder.ofLongs()); + } + + @Override + public ExtendedDoubleHistogramBuilder setExplicitBucketBoundariesAdvice( + List bucketBoundaries) { + agentBuilder.setExplicitBucketBoundariesAdvice(bucketBoundaries); + return this; + } + + @Override + public ExtendedDoubleHistogramBuilder setAttributesAdvice(List> attributes) { + ((io.opentelemetry.api.incubator.metrics.ExtendedDoubleHistogramBuilder) agentBuilder) + .setAttributesAdvice(Bridging.toAgent(attributes)); + return this; + } +} diff --git a/instrumentation/opentelemetry-api/opentelemetry-api-1.37/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_37/incubator/metrics/ApplicationDoubleUpDownCounterBuilder137.java b/instrumentation/opentelemetry-api/opentelemetry-api-1.37/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_37/incubator/metrics/ApplicationDoubleUpDownCounterBuilder137.java new file mode 100644 index 000000000000..5256ae19c79a --- /dev/null +++ b/instrumentation/opentelemetry-api/opentelemetry-api-1.37/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_37/incubator/metrics/ApplicationDoubleUpDownCounterBuilder137.java @@ -0,0 +1,31 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.opentelemetryapi.v1_37.incubator.metrics; + +import application.io.opentelemetry.api.common.AttributeKey; +import application.io.opentelemetry.api.incubator.metrics.ExtendedDoubleUpDownCounterBuilder; +import io.opentelemetry.javaagent.instrumentation.opentelemetryapi.trace.Bridging; +import io.opentelemetry.javaagent.instrumentation.opentelemetryapi.v1_10.metrics.ApplicationDoubleUpDownCounterBuilder; +import java.util.List; + +final class ApplicationDoubleUpDownCounterBuilder137 extends ApplicationDoubleUpDownCounterBuilder + implements ExtendedDoubleUpDownCounterBuilder { + + private final io.opentelemetry.api.metrics.DoubleUpDownCounterBuilder agentBuilder; + + ApplicationDoubleUpDownCounterBuilder137( + io.opentelemetry.api.metrics.DoubleUpDownCounterBuilder agentBuilder) { + super(agentBuilder); + this.agentBuilder = agentBuilder; + } + + @Override + public ExtendedDoubleUpDownCounterBuilder setAttributesAdvice(List> attributes) { + ((io.opentelemetry.api.incubator.metrics.ExtendedDoubleUpDownCounterBuilder) agentBuilder) + .setAttributesAdvice(Bridging.toAgent(attributes)); + return this; + } +} diff --git a/instrumentation/opentelemetry-api/opentelemetry-api-1.37/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_37/incubator/metrics/ApplicationLongCounterBuilder137.java b/instrumentation/opentelemetry-api/opentelemetry-api-1.37/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_37/incubator/metrics/ApplicationLongCounterBuilder137.java new file mode 100644 index 000000000000..c72320e59f1b --- /dev/null +++ b/instrumentation/opentelemetry-api/opentelemetry-api-1.37/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_37/incubator/metrics/ApplicationLongCounterBuilder137.java @@ -0,0 +1,36 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.opentelemetryapi.v1_37.incubator.metrics; + +import application.io.opentelemetry.api.common.AttributeKey; +import application.io.opentelemetry.api.incubator.metrics.ExtendedLongCounterBuilder; +import application.io.opentelemetry.api.metrics.DoubleCounterBuilder; +import io.opentelemetry.javaagent.instrumentation.opentelemetryapi.trace.Bridging; +import io.opentelemetry.javaagent.instrumentation.opentelemetryapi.v1_10.metrics.ApplicationLongCounterBuilder; +import java.util.List; + +final class ApplicationLongCounterBuilder137 extends ApplicationLongCounterBuilder + implements ExtendedLongCounterBuilder { + + private final io.opentelemetry.api.metrics.LongCounterBuilder agentBuilder; + + ApplicationLongCounterBuilder137(io.opentelemetry.api.metrics.LongCounterBuilder agentBuilder) { + super(agentBuilder); + this.agentBuilder = agentBuilder; + } + + @Override + public DoubleCounterBuilder ofDoubles() { + return new ApplicationDoubleCounterBuilder137(agentBuilder.ofDoubles()); + } + + @Override + public ExtendedLongCounterBuilder setAttributesAdvice(List> attributes) { + ((io.opentelemetry.api.incubator.metrics.ExtendedLongCounterBuilder) agentBuilder) + .setAttributesAdvice(Bridging.toAgent(attributes)); + return this; + } +} diff --git a/instrumentation/opentelemetry-api/opentelemetry-api-1.37/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_37/incubator/metrics/ApplicationLongGaugeBuilder137.java b/instrumentation/opentelemetry-api/opentelemetry-api-1.37/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_37/incubator/metrics/ApplicationLongGaugeBuilder137.java new file mode 100644 index 000000000000..4bf5a2439168 --- /dev/null +++ b/instrumentation/opentelemetry-api/opentelemetry-api-1.37/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_37/incubator/metrics/ApplicationLongGaugeBuilder137.java @@ -0,0 +1,48 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.opentelemetryapi.v1_37.incubator.metrics; + +import application.io.opentelemetry.api.common.AttributeKey; +import application.io.opentelemetry.api.common.Attributes; +import application.io.opentelemetry.api.incubator.metrics.ExtendedLongGaugeBuilder; +import application.io.opentelemetry.api.incubator.metrics.LongGauge; +import io.opentelemetry.javaagent.instrumentation.opentelemetryapi.trace.Bridging; +import io.opentelemetry.javaagent.instrumentation.opentelemetryapi.v1_10.metrics.ApplicationLongGaugeBuilder; +import java.util.List; + +final class ApplicationLongGaugeBuilder137 extends ApplicationLongGaugeBuilder + implements ExtendedLongGaugeBuilder { + + private final io.opentelemetry.api.metrics.LongGaugeBuilder agentBuilder; + + ApplicationLongGaugeBuilder137(io.opentelemetry.api.metrics.LongGaugeBuilder agentBuilder) { + super(agentBuilder); + this.agentBuilder = agentBuilder; + } + + @Override + public LongGauge build() { + io.opentelemetry.api.metrics.LongGauge agentLongGauge = agentBuilder.build(); + return new LongGauge() { + @Override + public void set(long value) { + agentLongGauge.set(value); + } + + @Override + public void set(long value, Attributes attributes) { + agentLongGauge.set(value, Bridging.toAgent(attributes)); + } + }; + } + + @Override + public ExtendedLongGaugeBuilder setAttributesAdvice(List> attributes) { + ((io.opentelemetry.api.incubator.metrics.ExtendedLongGaugeBuilder) agentBuilder) + .setAttributesAdvice(Bridging.toAgent(attributes)); + return this; + } +} diff --git a/instrumentation/opentelemetry-api/opentelemetry-api-1.37/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_37/incubator/metrics/ApplicationLongHistogramBuilder137.java b/instrumentation/opentelemetry-api/opentelemetry-api-1.37/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_37/incubator/metrics/ApplicationLongHistogramBuilder137.java new file mode 100644 index 000000000000..053d14c4ed85 --- /dev/null +++ b/instrumentation/opentelemetry-api/opentelemetry-api-1.37/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_37/incubator/metrics/ApplicationLongHistogramBuilder137.java @@ -0,0 +1,38 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.opentelemetryapi.v1_37.incubator.metrics; + +import application.io.opentelemetry.api.common.AttributeKey; +import application.io.opentelemetry.api.incubator.metrics.ExtendedLongHistogramBuilder; +import io.opentelemetry.javaagent.instrumentation.opentelemetryapi.trace.Bridging; +import io.opentelemetry.javaagent.instrumentation.opentelemetryapi.v1_10.metrics.ApplicationLongHistogramBuilder; +import java.util.List; + +final class ApplicationLongHistogramBuilder137 extends ApplicationLongHistogramBuilder + implements ExtendedLongHistogramBuilder { + + private final io.opentelemetry.api.metrics.LongHistogramBuilder agentBuilder; + + ApplicationLongHistogramBuilder137( + io.opentelemetry.api.metrics.LongHistogramBuilder agentBuilder) { + super(agentBuilder); + this.agentBuilder = agentBuilder; + } + + @Override + public ExtendedLongHistogramBuilder setExplicitBucketBoundariesAdvice( + List bucketBoundaries) { + agentBuilder.setExplicitBucketBoundariesAdvice(bucketBoundaries); + return this; + } + + @Override + public ExtendedLongHistogramBuilder setAttributesAdvice(List> attributes) { + ((io.opentelemetry.api.incubator.metrics.ExtendedLongHistogramBuilder) agentBuilder) + .setAttributesAdvice(Bridging.toAgent(attributes)); + return this; + } +} diff --git a/instrumentation/opentelemetry-api/opentelemetry-api-1.37/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_37/incubator/metrics/ApplicationLongUpDownCounterBuilder137.java b/instrumentation/opentelemetry-api/opentelemetry-api-1.37/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_37/incubator/metrics/ApplicationLongUpDownCounterBuilder137.java new file mode 100644 index 000000000000..3dda4d32fbd9 --- /dev/null +++ b/instrumentation/opentelemetry-api/opentelemetry-api-1.37/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_37/incubator/metrics/ApplicationLongUpDownCounterBuilder137.java @@ -0,0 +1,37 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.opentelemetryapi.v1_37.incubator.metrics; + +import application.io.opentelemetry.api.common.AttributeKey; +import application.io.opentelemetry.api.incubator.metrics.ExtendedLongUpDownCounterBuilder; +import application.io.opentelemetry.api.metrics.DoubleUpDownCounterBuilder; +import io.opentelemetry.javaagent.instrumentation.opentelemetryapi.trace.Bridging; +import io.opentelemetry.javaagent.instrumentation.opentelemetryapi.v1_10.metrics.ApplicationLongUpDownCounterBuilder; +import java.util.List; + +final class ApplicationLongUpDownCounterBuilder137 extends ApplicationLongUpDownCounterBuilder + implements ExtendedLongUpDownCounterBuilder { + + private final io.opentelemetry.api.metrics.LongUpDownCounterBuilder agentBuilder; + + ApplicationLongUpDownCounterBuilder137( + io.opentelemetry.api.metrics.LongUpDownCounterBuilder agentBuilder) { + super(agentBuilder); + this.agentBuilder = agentBuilder; + } + + @Override + public DoubleUpDownCounterBuilder ofDoubles() { + return new ApplicationDoubleUpDownCounterBuilder137(agentBuilder.ofDoubles()); + } + + @Override + public ExtendedLongUpDownCounterBuilder setAttributesAdvice(List> attributes) { + ((io.opentelemetry.api.incubator.metrics.ExtendedLongUpDownCounterBuilder) agentBuilder) + .setAttributesAdvice(Bridging.toAgent(attributes)); + return this; + } +} diff --git a/instrumentation/opentelemetry-api/opentelemetry-api-1.37/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_37/incubator/metrics/ApplicationMeter137.java b/instrumentation/opentelemetry-api/opentelemetry-api-1.37/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_37/incubator/metrics/ApplicationMeter137.java new file mode 100644 index 000000000000..65860afca72a --- /dev/null +++ b/instrumentation/opentelemetry-api/opentelemetry-api-1.37/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_37/incubator/metrics/ApplicationMeter137.java @@ -0,0 +1,28 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.opentelemetryapi.v1_37.incubator.metrics; + +import application.io.opentelemetry.api.metrics.DoubleGaugeBuilder; +import io.opentelemetry.javaagent.instrumentation.opentelemetryapi.v1_10.metrics.ApplicationDoubleGaugeBuilder; + +class ApplicationMeter137 extends BaseApplicationMeter137 { + + private final io.opentelemetry.api.metrics.Meter agentMeter; + + protected ApplicationMeter137(io.opentelemetry.api.metrics.Meter agentMeter) { + super(agentMeter); + this.agentMeter = agentMeter; + } + + @Override + public DoubleGaugeBuilder gaugeBuilder(String name) { + io.opentelemetry.api.metrics.DoubleGaugeBuilder builder = agentMeter.gaugeBuilder(name); + if (builder instanceof io.opentelemetry.api.incubator.metrics.ExtendedDoubleGaugeBuilder) { + return new ApplicationDoubleGaugeBuilder137(builder); + } + return new ApplicationDoubleGaugeBuilder(builder); + } +} diff --git a/instrumentation/opentelemetry-api/opentelemetry-api-1.37/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_37/incubator/metrics/ApplicationMeterFactory137.java b/instrumentation/opentelemetry-api/opentelemetry-api-1.37/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_37/incubator/metrics/ApplicationMeterFactory137.java new file mode 100644 index 000000000000..99faef092aaa --- /dev/null +++ b/instrumentation/opentelemetry-api/opentelemetry-api-1.37/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_37/incubator/metrics/ApplicationMeterFactory137.java @@ -0,0 +1,18 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.opentelemetryapi.v1_37.incubator.metrics; + +import io.opentelemetry.api.metrics.Meter; +import io.opentelemetry.javaagent.instrumentation.opentelemetryapi.v1_10.metrics.ApplicationMeter; +import io.opentelemetry.javaagent.instrumentation.opentelemetryapi.v1_10.metrics.ApplicationMeterFactory; + +// this class is used from opentelemetry-api-1.27 via reflection +public final class ApplicationMeterFactory137 implements ApplicationMeterFactory { + @Override + public ApplicationMeter newMeter(Meter agentMeter) { + return new ApplicationMeter137(agentMeter); + } +} diff --git a/instrumentation/opentelemetry-api/opentelemetry-api-1.37/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_37/incubator/metrics/BaseApplicationMeter137.java b/instrumentation/opentelemetry-api/opentelemetry-api-1.37/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_37/incubator/metrics/BaseApplicationMeter137.java new file mode 100644 index 000000000000..b3fd0b095142 --- /dev/null +++ b/instrumentation/opentelemetry-api/opentelemetry-api-1.37/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_37/incubator/metrics/BaseApplicationMeter137.java @@ -0,0 +1,54 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.opentelemetryapi.v1_37.incubator.metrics; + +import application.io.opentelemetry.api.metrics.DoubleHistogramBuilder; +import application.io.opentelemetry.api.metrics.LongCounterBuilder; +import application.io.opentelemetry.api.metrics.LongUpDownCounterBuilder; +import io.opentelemetry.javaagent.instrumentation.opentelemetryapi.v1_10.metrics.ApplicationDoubleHistogramBuilder; +import io.opentelemetry.javaagent.instrumentation.opentelemetryapi.v1_10.metrics.ApplicationLongCounterBuilder; +import io.opentelemetry.javaagent.instrumentation.opentelemetryapi.v1_10.metrics.ApplicationLongUpDownCounterBuilder; +import io.opentelemetry.javaagent.instrumentation.opentelemetryapi.v1_15.metrics.ApplicationMeter115; + +// used by both 1.37 and 1.38 +public class BaseApplicationMeter137 extends ApplicationMeter115 { + + private final io.opentelemetry.api.metrics.Meter agentMeter; + + protected BaseApplicationMeter137(io.opentelemetry.api.metrics.Meter agentMeter) { + super(agentMeter); + this.agentMeter = agentMeter; + } + + @Override + public LongCounterBuilder counterBuilder(String name) { + io.opentelemetry.api.metrics.LongCounterBuilder builder = agentMeter.counterBuilder(name); + if (builder instanceof io.opentelemetry.api.incubator.metrics.ExtendedLongCounterBuilder) { + return new ApplicationLongCounterBuilder137(builder); + } + return new ApplicationLongCounterBuilder(builder); + } + + @Override + public LongUpDownCounterBuilder upDownCounterBuilder(String name) { + io.opentelemetry.api.metrics.LongUpDownCounterBuilder builder = + agentMeter.upDownCounterBuilder(name); + if (builder + instanceof io.opentelemetry.api.incubator.metrics.ExtendedLongUpDownCounterBuilder) { + return new ApplicationLongUpDownCounterBuilder137(builder); + } + return new ApplicationLongUpDownCounterBuilder(builder); + } + + @Override + public DoubleHistogramBuilder histogramBuilder(String name) { + io.opentelemetry.api.metrics.DoubleHistogramBuilder builder = agentMeter.histogramBuilder(name); + if (builder instanceof io.opentelemetry.api.incubator.metrics.ExtendedDoubleHistogramBuilder) { + return new ApplicationDoubleHistogramBuilder137(builder); + } + return new ApplicationDoubleHistogramBuilder(builder); + } +} diff --git a/instrumentation/opentelemetry-api/opentelemetry-api-1.37/javaagent/src/noopTest/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_37/incubator/metrics/NoopTest.java b/instrumentation/opentelemetry-api/opentelemetry-api-1.37/javaagent/src/noopTest/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_37/incubator/metrics/NoopTest.java new file mode 100644 index 000000000000..47e92b4a862b --- /dev/null +++ b/instrumentation/opentelemetry-api/opentelemetry-api-1.37/javaagent/src/noopTest/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_37/incubator/metrics/NoopTest.java @@ -0,0 +1,43 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.opentelemetryapi.v1_37.incubator.metrics; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.incubator.metrics.ExtendedDoubleGaugeBuilder; +import io.opentelemetry.api.incubator.metrics.ExtendedDoubleHistogramBuilder; +import io.opentelemetry.api.incubator.metrics.ExtendedLongCounterBuilder; +import io.opentelemetry.api.incubator.metrics.ExtendedLongUpDownCounterBuilder; +import io.opentelemetry.api.metrics.DoubleGaugeBuilder; +import io.opentelemetry.api.metrics.DoubleHistogramBuilder; +import io.opentelemetry.api.metrics.LongCounterBuilder; +import io.opentelemetry.api.metrics.LongUpDownCounterBuilder; +import io.opentelemetry.api.metrics.Meter; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +class NoopTest { + @RegisterExtension + static final AgentInstrumentationExtension testing = AgentInstrumentationExtension.create(); + + @Test + void noopInstance() { + Meter meter = testing.getOpenTelemetry().getMeter("test"); + + LongCounterBuilder counterBuilder = meter.counterBuilder("test"); + assertThat(counterBuilder).isNotInstanceOf(ExtendedLongCounterBuilder.class); + + LongUpDownCounterBuilder upDownCounterBuilder = meter.upDownCounterBuilder("test"); + assertThat(upDownCounterBuilder).isNotInstanceOf(ExtendedLongUpDownCounterBuilder.class); + + DoubleGaugeBuilder gaugeBuilder = meter.gaugeBuilder("test"); + assertThat(gaugeBuilder).isNotInstanceOf(ExtendedDoubleGaugeBuilder.class); + + DoubleHistogramBuilder histogramBuilder = meter.histogramBuilder("test"); + assertThat(histogramBuilder).isNotInstanceOf(ExtendedDoubleHistogramBuilder.class); + } +} diff --git a/instrumentation/opentelemetry-api/opentelemetry-api-1.37/javaagent/src/oldAndNewIncubatorTest/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_37/incubator/metrics/MeterTest.java b/instrumentation/opentelemetry-api/opentelemetry-api-1.37/javaagent/src/oldAndNewIncubatorTest/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_37/incubator/metrics/MeterTest.java new file mode 100644 index 000000000000..ca78917ba4a9 --- /dev/null +++ b/instrumentation/opentelemetry-api/opentelemetry-api-1.37/javaagent/src/oldAndNewIncubatorTest/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_37/incubator/metrics/MeterTest.java @@ -0,0 +1,467 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.opentelemetryapi.v1_37.incubator.metrics; + +import static io.opentelemetry.api.common.AttributeKey.stringKey; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; +import static java.util.Collections.singletonList; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.incubator.metrics.DoubleGauge; +import io.opentelemetry.api.incubator.metrics.ExtendedDoubleCounterBuilder; +import io.opentelemetry.api.incubator.metrics.ExtendedDoubleGaugeBuilder; +import io.opentelemetry.api.incubator.metrics.ExtendedDoubleHistogramBuilder; +import io.opentelemetry.api.incubator.metrics.ExtendedDoubleUpDownCounterBuilder; +import io.opentelemetry.api.incubator.metrics.ExtendedLongCounterBuilder; +import io.opentelemetry.api.incubator.metrics.ExtendedLongGaugeBuilder; +import io.opentelemetry.api.incubator.metrics.ExtendedLongHistogramBuilder; +import io.opentelemetry.api.incubator.metrics.ExtendedLongUpDownCounterBuilder; +import io.opentelemetry.api.incubator.metrics.LongGauge; +import io.opentelemetry.api.metrics.DoubleCounter; +import io.opentelemetry.api.metrics.DoubleCounterBuilder; +import io.opentelemetry.api.metrics.DoubleGaugeBuilder; +import io.opentelemetry.api.metrics.DoubleHistogram; +import io.opentelemetry.api.metrics.DoubleHistogramBuilder; +import io.opentelemetry.api.metrics.DoubleUpDownCounter; +import io.opentelemetry.api.metrics.DoubleUpDownCounterBuilder; +import io.opentelemetry.api.metrics.LongCounter; +import io.opentelemetry.api.metrics.LongCounterBuilder; +import io.opentelemetry.api.metrics.LongGaugeBuilder; +import io.opentelemetry.api.metrics.LongHistogram; +import io.opentelemetry.api.metrics.LongHistogramBuilder; +import io.opentelemetry.api.metrics.LongUpDownCounter; +import io.opentelemetry.api.metrics.LongUpDownCounterBuilder; +import io.opentelemetry.api.metrics.Meter; +import io.opentelemetry.api.metrics.ObservableDoubleGauge; +import io.opentelemetry.api.metrics.ObservableLongGauge; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.sdk.common.InstrumentationScopeInfo; +import org.assertj.core.api.AbstractIterableAssert; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInfo; +import org.junit.jupiter.api.extension.RegisterExtension; + +class MeterTest { + + @RegisterExtension + static final AgentInstrumentationExtension testing = AgentInstrumentationExtension.create(); + + private String instrumentationName; + private Meter meter; + + @BeforeEach + void setupMeter(TestInfo test) { + instrumentationName = "test-" + test.getDisplayName(); + meter = + testing + .getOpenTelemetry() + .getMeterProvider() + .meterBuilder(instrumentationName) + .setInstrumentationVersion("1.2.3") + .setSchemaUrl("http://schema.org") + .build(); + } + + @Test + void longCounter() { + LongCounterBuilder builder = meter.counterBuilder("test").setDescription("d").setUnit("u"); + assertThat(builder).isInstanceOf(ExtendedLongCounterBuilder.class); + ExtendedLongCounterBuilder extendedBuilder = (ExtendedLongCounterBuilder) builder; + extendedBuilder.setAttributesAdvice(singletonList(stringKey("test"))); + + LongCounter instrument = builder.build(); + + instrument.add(5, Attributes.of(stringKey("test"), "test", stringKey("q"), "r")); + instrument.add(6, Attributes.of(stringKey("test"), "test", stringKey("q"), "r")); + + testing.waitAndAssertMetrics( + instrumentationName, + "test", + metrics -> + metrics.anySatisfy( + metric -> + assertThat(metric) + .hasDescription("d") + .hasUnit("u") + .hasInstrumentationScope( + InstrumentationScopeInfo.builder(instrumentationName) + .setVersion("1.2.3") + .build()) + .hasLongSumSatisfying( + sum -> + sum.isMonotonic() + .hasPointsSatisfying( + point -> + point + .hasValue(11) + .hasAttributesSatisfying( + equalTo(stringKey("test"), "test")))))); + } + + @Test + void doubleCounter() { + DoubleCounterBuilder builder = + meter.counterBuilder("test").ofDoubles().setDescription("d").setUnit("u"); + assertThat(builder).isInstanceOf(ExtendedDoubleCounterBuilder.class); + ExtendedDoubleCounterBuilder extendedBuilder = (ExtendedDoubleCounterBuilder) builder; + extendedBuilder.setAttributesAdvice(singletonList(stringKey("test"))); + + DoubleCounter instrument = builder.build(); + + instrument.add(5.5, Attributes.of(stringKey("test"), "test", stringKey("q"), "r")); + instrument.add(6.6, Attributes.of(stringKey("test"), "test", stringKey("q"), "r")); + + testing.waitAndAssertMetrics( + instrumentationName, + "test", + metrics -> + metrics.anySatisfy( + metric -> + assertThat(metric) + .hasDescription("d") + .hasUnit("u") + .hasInstrumentationScope( + InstrumentationScopeInfo.builder(instrumentationName) + .setVersion("1.2.3") + .build()) + .hasDoubleSumSatisfying( + sum -> + sum.isMonotonic() + .hasPointsSatisfying( + point -> + point + .hasValue(12.1) + .hasAttributesSatisfying( + equalTo(stringKey("test"), "test")))))); + } + + @Test + void longUpDownCounter() { + LongUpDownCounterBuilder builder = + meter.upDownCounterBuilder("test").setDescription("d").setUnit("u"); + assertThat(builder).isInstanceOf(ExtendedLongUpDownCounterBuilder.class); + ExtendedLongUpDownCounterBuilder extendedBuilder = (ExtendedLongUpDownCounterBuilder) builder; + extendedBuilder.setAttributesAdvice(singletonList(stringKey("test"))); + + LongUpDownCounter instrument = builder.build(); + + instrument.add(5, Attributes.of(stringKey("test"), "test", stringKey("q"), "r")); + instrument.add(6, Attributes.of(stringKey("test"), "test", stringKey("q"), "r")); + + testing.waitAndAssertMetrics( + instrumentationName, + "test", + metrics -> + metrics.anySatisfy( + metric -> + assertThat(metric) + .hasDescription("d") + .hasUnit("u") + .hasInstrumentationScope( + InstrumentationScopeInfo.builder(instrumentationName) + .setVersion("1.2.3") + .build()) + .hasLongSumSatisfying( + sum -> + sum.isNotMonotonic() + .hasPointsSatisfying( + point -> + point + .hasValue(11) + .hasAttributesSatisfying( + equalTo(stringKey("test"), "test")))))); + } + + @Test + void doubleUpDownCounter() { + DoubleUpDownCounterBuilder builder = + meter.upDownCounterBuilder("test").ofDoubles().setDescription("d").setUnit("u"); + assertThat(builder).isInstanceOf(ExtendedDoubleUpDownCounterBuilder.class); + ExtendedDoubleUpDownCounterBuilder extendedBuilder = + (ExtendedDoubleUpDownCounterBuilder) builder; + extendedBuilder.setAttributesAdvice(singletonList(stringKey("test"))); + + DoubleUpDownCounter instrument = builder.build(); + + instrument.add(5.5, Attributes.of(stringKey("test"), "test", stringKey("q"), "r")); + instrument.add(6.6, Attributes.of(stringKey("test"), "test", stringKey("q"), "r")); + + testing.waitAndAssertMetrics( + instrumentationName, + "test", + metrics -> + metrics.anySatisfy( + metric -> + assertThat(metric) + .hasDescription("d") + .hasUnit("u") + .hasInstrumentationScope( + InstrumentationScopeInfo.builder(instrumentationName) + .setVersion("1.2.3") + .build()) + .hasDoubleSumSatisfying( + sum -> + sum.isNotMonotonic() + .hasPointsSatisfying( + point -> + point + .hasValue(12.1) + .hasAttributesSatisfying( + equalTo(stringKey("test"), "test")))))); + } + + @Test + void longHistogram() { + LongHistogramBuilder builder = + meter.histogramBuilder("test").ofLongs().setDescription("d").setUnit("u"); + builder.setExplicitBucketBoundariesAdvice(singletonList(10L)); + assertThat(builder).isInstanceOf(ExtendedLongHistogramBuilder.class); + ExtendedLongHistogramBuilder extendedBuilder = (ExtendedLongHistogramBuilder) builder; + extendedBuilder.setAttributesAdvice(singletonList(stringKey("test"))); + + LongHistogram instrument = builder.build(); + + instrument.record(5, Attributes.of(stringKey("test"), "test", stringKey("q"), "r")); + instrument.record(6, Attributes.of(stringKey("test"), "test", stringKey("q"), "r")); + + testing.waitAndAssertMetrics( + instrumentationName, + "test", + metrics -> + metrics.anySatisfy( + metric -> + assertThat(metric) + .hasDescription("d") + .hasUnit("u") + .hasInstrumentationScope( + InstrumentationScopeInfo.builder(instrumentationName) + .setVersion("1.2.3") + .build()) + .hasHistogramSatisfying( + histogram -> + histogram.hasPointsSatisfying( + point -> + point + .hasSum(11.0) + .hasBucketBoundaries(10.0) + .hasAttributesSatisfying( + equalTo(stringKey("test"), "test")))))); + } + + @Test + void doubleHistogram() { + DoubleHistogramBuilder builder = + meter.histogramBuilder("test").setDescription("d").setUnit("u"); + builder.setExplicitBucketBoundariesAdvice(singletonList(10.0)); + assertThat(builder).isInstanceOf(ExtendedDoubleHistogramBuilder.class); + ExtendedDoubleHistogramBuilder extendedBuilder = (ExtendedDoubleHistogramBuilder) builder; + extendedBuilder.setAttributesAdvice(singletonList(stringKey("test"))); + + DoubleHistogram instrument = builder.build(); + + instrument.record(5.5, Attributes.of(stringKey("test"), "test", stringKey("q"), "r")); + instrument.record(6.6, Attributes.of(stringKey("test"), "test", stringKey("q"), "r")); + + testing.waitAndAssertMetrics( + instrumentationName, + "test", + metrics -> + metrics.anySatisfy( + metric -> + assertThat(metric) + .hasDescription("d") + .hasUnit("u") + .hasInstrumentationScope( + InstrumentationScopeInfo.builder(instrumentationName) + .setVersion("1.2.3") + .build()) + .hasHistogramSatisfying( + histogram -> + histogram.hasPointsSatisfying( + point -> + point + .hasSum(12.1) + .hasBucketBoundaries(10.0) + .hasAttributesSatisfying( + equalTo(stringKey("test"), "test")))))); + } + + @Test + void longGauge() throws InterruptedException { + LongGaugeBuilder builder = + meter.gaugeBuilder("test").ofLongs().setDescription("d").setUnit("u"); + assertThat(builder).isInstanceOf(ExtendedLongGaugeBuilder.class); + ExtendedLongGaugeBuilder extendedBuilder = (ExtendedLongGaugeBuilder) builder; + extendedBuilder.setAttributesAdvice(singletonList(stringKey("test"))); + + ObservableLongGauge observableGauge = + builder.buildWithCallback( + result -> + result.record(123, Attributes.of(stringKey("test"), "test", stringKey("q"), "r"))); + + testing.waitAndAssertMetrics( + instrumentationName, + "test", + metrics -> + metrics.anySatisfy( + metric -> + assertThat(metric) + .hasDescription("d") + .hasUnit("u") + .hasInstrumentationScope( + InstrumentationScopeInfo.builder(instrumentationName) + .setVersion("1.2.3") + .build()) + .hasLongGaugeSatisfying( + gauge -> + gauge.hasPointsSatisfying( + point -> + point + .hasValue(123) + .hasAttributesSatisfying( + equalTo(stringKey("test"), "test")))))); + + observableGauge.close(); + + // sleep exporter interval + Thread.sleep(100); + testing.clearData(); + Thread.sleep(100); + + testing.waitAndAssertMetrics(instrumentationName, "test", AbstractIterableAssert::isEmpty); + } + + @Test + void syncLongGauge() throws InterruptedException { + LongGaugeBuilder builder = + meter.gaugeBuilder("test").ofLongs().setDescription("d").setUnit("u"); + assertThat(builder).isInstanceOf(ExtendedLongGaugeBuilder.class); + ExtendedLongGaugeBuilder extendedBuilder = (ExtendedLongGaugeBuilder) builder; + extendedBuilder.setAttributesAdvice(singletonList(stringKey("test"))); + + LongGauge longGauge = extendedBuilder.build(); + longGauge.set(321); + longGauge.set(123, Attributes.of(stringKey("test"), "test", stringKey("q"), "r")); + + testing.waitAndAssertMetrics( + instrumentationName, + "test", + metrics -> + metrics.anySatisfy( + metric -> + assertThat(metric) + .hasDescription("d") + .hasUnit("u") + .hasInstrumentationScope( + InstrumentationScopeInfo.builder(instrumentationName) + .setVersion("1.2.3") + .build()) + .hasLongGaugeSatisfying( + gauge -> + gauge.hasPointsSatisfying( + point -> point.hasValue(321).hasAttributes(Attributes.empty()), + point -> + point + .hasValue(123) + .hasAttributesSatisfying( + equalTo(stringKey("test"), "test")))))); + + // sleep exporter interval + Thread.sleep(100); + testing.clearData(); + Thread.sleep(100); + + testing.waitAndAssertMetrics(instrumentationName, "test", AbstractIterableAssert::isEmpty); + } + + @Test + void doubleGauge() throws InterruptedException { + DoubleGaugeBuilder builder = meter.gaugeBuilder("test").setDescription("d").setUnit("u"); + assertThat(builder).isInstanceOf(ExtendedDoubleGaugeBuilder.class); + ExtendedDoubleGaugeBuilder extendedBuilder = (ExtendedDoubleGaugeBuilder) builder; + extendedBuilder.setAttributesAdvice(singletonList(stringKey("test"))); + + ObservableDoubleGauge observableGauge = + builder.buildWithCallback( + result -> + result.record(1.23, Attributes.of(stringKey("test"), "test", stringKey("q"), "r"))); + + testing.waitAndAssertMetrics( + instrumentationName, + "test", + metrics -> + metrics.anySatisfy( + metric -> + assertThat(metric) + .hasDescription("d") + .hasUnit("u") + .hasInstrumentationScope( + InstrumentationScopeInfo.builder(instrumentationName) + .setVersion("1.2.3") + .build()) + .hasDoubleGaugeSatisfying( + gauge -> + gauge.hasPointsSatisfying( + point -> + point + .hasValue(1.23) + .hasAttributesSatisfying( + equalTo(stringKey("test"), "test")))))); + + observableGauge.close(); + + // sleep exporter interval + Thread.sleep(100); + testing.clearData(); + Thread.sleep(100); + + testing.waitAndAssertMetrics(instrumentationName, "test", AbstractIterableAssert::isEmpty); + } + + @Test + void syncDoubleGauge() throws InterruptedException { + DoubleGaugeBuilder builder = meter.gaugeBuilder("test").setDescription("d").setUnit("u"); + assertThat(builder).isInstanceOf(ExtendedDoubleGaugeBuilder.class); + ExtendedDoubleGaugeBuilder extendedBuilder = (ExtendedDoubleGaugeBuilder) builder; + extendedBuilder.setAttributesAdvice(singletonList(stringKey("test"))); + + DoubleGauge doubleGauge = extendedBuilder.build(); + doubleGauge.set(3.21); + doubleGauge.set(1.23, Attributes.of(stringKey("test"), "test", stringKey("q"), "r")); + + testing.waitAndAssertMetrics( + instrumentationName, + "test", + metrics -> + metrics.anySatisfy( + metric -> + assertThat(metric) + .hasDescription("d") + .hasUnit("u") + .hasInstrumentationScope( + InstrumentationScopeInfo.builder(instrumentationName) + .setVersion("1.2.3") + .build()) + .hasDoubleGaugeSatisfying( + gauge -> + gauge.hasPointsSatisfying( + point -> point.hasValue(3.21).hasAttributes(Attributes.empty()), + point -> + point + .hasValue(1.23) + .hasAttributesSatisfying( + equalTo(stringKey("test"), "test")))))); + + // sleep exporter interval + Thread.sleep(100); + testing.clearData(); + Thread.sleep(100); + + testing.waitAndAssertMetrics(instrumentationName, "test", AbstractIterableAssert::isEmpty); + } +} diff --git a/instrumentation/opentelemetry-api/opentelemetry-api-1.37/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_37/metrics/MeterTest.java b/instrumentation/opentelemetry-api/opentelemetry-api-1.37/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_37/metrics/MeterTest.java new file mode 100644 index 000000000000..f5d2ecaf23fc --- /dev/null +++ b/instrumentation/opentelemetry-api/opentelemetry-api-1.37/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_37/metrics/MeterTest.java @@ -0,0 +1,116 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.opentelemetryapi.v1_37.metrics; + +import static io.opentelemetry.api.common.AttributeKey.stringKey; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; +import static java.util.Collections.singletonList; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.metrics.DoubleHistogram; +import io.opentelemetry.api.metrics.DoubleHistogramBuilder; +import io.opentelemetry.api.metrics.LongHistogram; +import io.opentelemetry.api.metrics.LongHistogramBuilder; +import io.opentelemetry.api.metrics.Meter; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.sdk.common.InstrumentationScopeInfo; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInfo; +import org.junit.jupiter.api.extension.RegisterExtension; + +class MeterTest { + + @RegisterExtension + static final AgentInstrumentationExtension testing = AgentInstrumentationExtension.create(); + + private String instrumentationName; + private Meter meter; + + @BeforeEach + void setupMeter(TestInfo test) { + instrumentationName = "test-" + test.getDisplayName(); + meter = + testing + .getOpenTelemetry() + .getMeterProvider() + .meterBuilder(instrumentationName) + .setInstrumentationVersion("1.2.3") + .setSchemaUrl("http://schema.org") + .build(); + } + + @Test + void longHistogram() { + LongHistogramBuilder builder = + meter.histogramBuilder("test").ofLongs().setDescription("d").setUnit("u"); + builder.setExplicitBucketBoundariesAdvice(singletonList(10L)); + + LongHistogram instrument = builder.build(); + + instrument.record(5, Attributes.of(stringKey("test"), "test")); + instrument.record(6, Attributes.of(stringKey("test"), "test")); + + testing.waitAndAssertMetrics( + instrumentationName, + "test", + metrics -> + metrics.anySatisfy( + metric -> + assertThat(metric) + .hasDescription("d") + .hasUnit("u") + .hasInstrumentationScope( + InstrumentationScopeInfo.builder(instrumentationName) + .setVersion("1.2.3") + .build()) + .hasHistogramSatisfying( + histogram -> + histogram.hasPointsSatisfying( + point -> + point + .hasSum(11.0) + .hasBucketBoundaries(10.0) + .hasAttributesSatisfying( + equalTo(stringKey("test"), "test")))))); + } + + @Test + void doubleHistogram() { + DoubleHistogramBuilder builder = + meter.histogramBuilder("test").setDescription("d").setUnit("u"); + builder.setExplicitBucketBoundariesAdvice(singletonList(10.0)); + + DoubleHistogram instrument = builder.build(); + + instrument.record(5.5, Attributes.of(stringKey("test"), "test")); + instrument.record(6.6, Attributes.of(stringKey("test"), "test")); + + testing.waitAndAssertMetrics( + instrumentationName, + "test", + metrics -> + metrics.anySatisfy( + metric -> + assertThat(metric) + .hasDescription("d") + .hasUnit("u") + .hasInstrumentationScope( + InstrumentationScopeInfo.builder(instrumentationName) + .setVersion("1.2.3") + .build()) + .hasHistogramSatisfying( + histogram -> + histogram.hasPointsSatisfying( + point -> + point + .hasSum(12.1) + .hasBucketBoundaries(10.0) + .hasAttributesSatisfying( + equalTo(stringKey("test"), "test")))))); + } +} diff --git a/instrumentation/opentelemetry-api/opentelemetry-api-1.38/javaagent/build.gradle.kts b/instrumentation/opentelemetry-api/opentelemetry-api-1.38/javaagent/build.gradle.kts new file mode 100644 index 000000000000..a5f854ac5856 --- /dev/null +++ b/instrumentation/opentelemetry-api/opentelemetry-api-1.38/javaagent/build.gradle.kts @@ -0,0 +1,62 @@ +plugins { + id("otel.javaagent-instrumentation") +} + +dependencies { + compileOnly(project(":opentelemetry-api-shaded-for-instrumenting", configuration = "v1_38")) + compileOnly("io.opentelemetry:opentelemetry-api-incubator") + + implementation(project(":instrumentation:opentelemetry-api:opentelemetry-api-1.0:javaagent")) + implementation(project(":instrumentation:opentelemetry-api:opentelemetry-api-1.4:javaagent")) + implementation(project(":instrumentation:opentelemetry-api:opentelemetry-api-1.10:javaagent")) + implementation(project(":instrumentation:opentelemetry-api:opentelemetry-api-1.15:javaagent")) + implementation(project(":instrumentation:opentelemetry-api:opentelemetry-api-1.27:javaagent")) + implementation(project(":instrumentation:opentelemetry-api:opentelemetry-api-1.31:javaagent")) + implementation(project(":instrumentation:opentelemetry-api:opentelemetry-api-1.32:javaagent")) + implementation(project(":instrumentation:opentelemetry-api:opentelemetry-api-1.37:javaagent")) +} + +configurations.configureEach { + if (name.endsWith("testRuntimeClasspath", true) || name.endsWith("testCompileClasspath", true)) { + resolutionStrategy { + force("io.opentelemetry:opentelemetry-api:1.38.0") + } + if (name.startsWith("incubatorTest") || name.startsWith("noopTest")) { + resolutionStrategy { + force("io.opentelemetry:opentelemetry-api-incubator:1.38.0-alpha") + } + } + } +} + +testing { + suites { + val incubatorTest by registering(JvmTestSuite::class) { + dependencies { + implementation("io.opentelemetry:opentelemetry-api-incubator:1.38.0-alpha") + } + } + } + + suites { + val noopTest by registering(JvmTestSuite::class) { + dependencies { + implementation("io.opentelemetry:opentelemetry-api-incubator:1.38.0-alpha") + } + + targets { + all { + testTask.configure { + jvmArgs("-Dtesting.exporter.enabled=false") + } + } + } + } + } +} + +tasks { + check { + dependsOn(testing.suites) + } +} diff --git a/instrumentation/opentelemetry-api/opentelemetry-api-1.38/javaagent/src/incubatorTest/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_38/incubator/metrics/MeterTest.java b/instrumentation/opentelemetry-api/opentelemetry-api-1.38/javaagent/src/incubatorTest/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_38/incubator/metrics/MeterTest.java new file mode 100644 index 000000000000..f8b9b1fbf94b --- /dev/null +++ b/instrumentation/opentelemetry-api/opentelemetry-api-1.38/javaagent/src/incubatorTest/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_38/incubator/metrics/MeterTest.java @@ -0,0 +1,134 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.opentelemetryapi.v1_38.incubator.metrics; + +import static io.opentelemetry.api.common.AttributeKey.stringKey; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; +import static java.util.Collections.singletonList; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.incubator.metrics.ExtendedDoubleGaugeBuilder; +import io.opentelemetry.api.incubator.metrics.ExtendedLongGaugeBuilder; +import io.opentelemetry.api.metrics.DoubleGauge; +import io.opentelemetry.api.metrics.DoubleGaugeBuilder; +import io.opentelemetry.api.metrics.LongGauge; +import io.opentelemetry.api.metrics.LongGaugeBuilder; +import io.opentelemetry.api.metrics.Meter; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.sdk.common.InstrumentationScopeInfo; +import org.assertj.core.api.AbstractIterableAssert; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInfo; +import org.junit.jupiter.api.extension.RegisterExtension; + +class MeterTest { + + @RegisterExtension + static final AgentInstrumentationExtension testing = AgentInstrumentationExtension.create(); + + private String instrumentationName; + private Meter meter; + + @BeforeEach + void setupMeter(TestInfo test) { + instrumentationName = "test-" + test.getDisplayName(); + meter = + testing + .getOpenTelemetry() + .getMeterProvider() + .meterBuilder(instrumentationName) + .setInstrumentationVersion("1.2.3") + .setSchemaUrl("http://schema.org") + .build(); + } + + @Test + void syncLongGauge() throws InterruptedException { + LongGaugeBuilder builder = + meter.gaugeBuilder("test").ofLongs().setDescription("d").setUnit("u"); + assertThat(builder).isInstanceOf(ExtendedLongGaugeBuilder.class); + ExtendedLongGaugeBuilder extendedBuilder = (ExtendedLongGaugeBuilder) builder; + extendedBuilder.setAttributesAdvice(singletonList(stringKey("test"))); + + LongGauge longGauge = builder.build(); + longGauge.set(321); + longGauge.set(123, Attributes.of(stringKey("test"), "test", stringKey("q"), "r")); + + testing.waitAndAssertMetrics( + instrumentationName, + "test", + metrics -> + metrics.anySatisfy( + metric -> + assertThat(metric) + .hasDescription("d") + .hasUnit("u") + .hasInstrumentationScope( + InstrumentationScopeInfo.builder(instrumentationName) + .setVersion("1.2.3") + .build()) + .hasLongGaugeSatisfying( + gauge -> + gauge.hasPointsSatisfying( + point -> point.hasValue(321).hasAttributes(Attributes.empty()), + point -> + point + .hasValue(123) + .hasAttributesSatisfying( + equalTo(stringKey("test"), "test")))))); + + // sleep exporter interval + Thread.sleep(100); + testing.clearData(); + Thread.sleep(100); + + testing.waitAndAssertMetrics(instrumentationName, "test", AbstractIterableAssert::isEmpty); + } + + @Test + void syncDoubleGauge() throws InterruptedException { + DoubleGaugeBuilder builder = meter.gaugeBuilder("test").setDescription("d").setUnit("u"); + assertThat(builder).isInstanceOf(ExtendedDoubleGaugeBuilder.class); + ExtendedDoubleGaugeBuilder extendedBuilder = (ExtendedDoubleGaugeBuilder) builder; + extendedBuilder.setAttributesAdvice(singletonList(stringKey("test"))); + + DoubleGauge doubleGauge = builder.build(); + doubleGauge.set(3.21); + doubleGauge.set(1.23, Attributes.of(stringKey("test"), "test", stringKey("q"), "r")); + + testing.waitAndAssertMetrics( + instrumentationName, + "test", + metrics -> + metrics.anySatisfy( + metric -> + assertThat(metric) + .hasDescription("d") + .hasUnit("u") + .hasInstrumentationScope( + InstrumentationScopeInfo.builder(instrumentationName) + .setVersion("1.2.3") + .build()) + .hasDoubleGaugeSatisfying( + gauge -> + gauge.hasPointsSatisfying( + point -> point.hasValue(3.21).hasAttributes(Attributes.empty()), + point -> + point + .hasValue(1.23) + .hasAttributesSatisfying( + equalTo(stringKey("test"), "test")))))); + + // sleep exporter interval + Thread.sleep(100); + testing.clearData(); + Thread.sleep(100); + + testing.waitAndAssertMetrics(instrumentationName, "test", AbstractIterableAssert::isEmpty); + } +} diff --git a/instrumentation/opentelemetry-api/opentelemetry-api-1.38/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_38/OpenTelemetryApiInstrumentationModule.java b/instrumentation/opentelemetry-api/opentelemetry-api-1.38/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_38/OpenTelemetryApiInstrumentationModule.java new file mode 100644 index 000000000000..fc0d9573a9cf --- /dev/null +++ b/instrumentation/opentelemetry-api/opentelemetry-api-1.38/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_38/OpenTelemetryApiInstrumentationModule.java @@ -0,0 +1,44 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.opentelemetryapi.v1_38; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.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 io.opentelemetry.javaagent.extension.instrumentation.internal.ExperimentalInstrumentationModule; +import java.util.List; +import net.bytebuddy.matcher.ElementMatcher; + +@AutoService(InstrumentationModule.class) +public class OpenTelemetryApiInstrumentationModule extends InstrumentationModule + implements ExperimentalInstrumentationModule { + public OpenTelemetryApiInstrumentationModule() { + super("opentelemetry-api", "opentelemetry-api-1.38"); + } + + @Override + public ElementMatcher.Junction classLoaderMatcher() { + // skip instrumentation when opentelemetry-api-incubator is present, instrumentation is + // handled by OpenTelemetryApiIncubatorInstrumentationModule + return not( + hasClassesNamed( + "application.io.opentelemetry.api.incubator.metrics.ExtendedDoubleHistogramBuilder")); + } + + @Override + public List typeInstrumentations() { + return singletonList(new OpenTelemetryInstrumentation()); + } + + @Override + public String getModuleGroup() { + return "opentelemetry-api-bridge"; + } +} diff --git a/instrumentation/opentelemetry-api/opentelemetry-api-1.38/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_38/OpenTelemetryInstrumentation.java b/instrumentation/opentelemetry-api/opentelemetry-api-1.38/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_38/OpenTelemetryInstrumentation.java new file mode 100644 index 000000000000..012e78df8232 --- /dev/null +++ b/instrumentation/opentelemetry-api/opentelemetry-api-1.38/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_38/OpenTelemetryInstrumentation.java @@ -0,0 +1,40 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.opentelemetryapi.v1_38; + +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.none; + +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.opentelemetryapi.v1_38.metrics.ApplicationMeterFactory138; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class OpenTelemetryInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("application.io.opentelemetry.api.GlobalOpenTelemetry"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + none(), OpenTelemetryInstrumentation.class.getName() + "$InitAdvice"); + } + + @SuppressWarnings({"ReturnValueIgnored", "unused"}) + public static class InitAdvice { + @Advice.OnMethodEnter + public static void init() { + // the sole purpose of this advice is to ensure that ApplicationMeterFactory138 is recognized + // as helper class and injected into class loader + ApplicationMeterFactory138.class.getName(); + } + } +} diff --git a/instrumentation/opentelemetry-api/opentelemetry-api-1.38/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_38/incubator/OpenTelemetryApiIncubatorInstrumentationModule.java b/instrumentation/opentelemetry-api/opentelemetry-api-1.38/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_38/incubator/OpenTelemetryApiIncubatorInstrumentationModule.java new file mode 100644 index 000000000000..a70e8fffbd08 --- /dev/null +++ b/instrumentation/opentelemetry-api/opentelemetry-api-1.38/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_38/incubator/OpenTelemetryApiIncubatorInstrumentationModule.java @@ -0,0 +1,42 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.opentelemetryapi.v1_38.incubator; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.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 io.opentelemetry.javaagent.extension.instrumentation.internal.ExperimentalInstrumentationModule; +import java.util.List; +import net.bytebuddy.matcher.ElementMatcher; + +@AutoService(InstrumentationModule.class) +public class OpenTelemetryApiIncubatorInstrumentationModule extends InstrumentationModule + implements ExperimentalInstrumentationModule { + public OpenTelemetryApiIncubatorInstrumentationModule() { + super("opentelemetry-api", "opentelemetry-api-1.38", "opentelemetry-api-incubator-1.38"); + } + + @Override + public ElementMatcher.Junction classLoaderMatcher() { + // skip instrumentation when opentelemetry-api-incubator is not present, instrumentation + // is handled by OpenTelemetryApiInstrumentationModule + return hasClassesNamed( + "application.io.opentelemetry.api.incubator.metrics.ExtendedDoubleHistogramBuilder"); + } + + @Override + public List typeInstrumentations() { + return singletonList(new OpenTelemetryIncubatorInstrumentation()); + } + + @Override + public String getModuleGroup() { + return "opentelemetry-api-bridge"; + } +} diff --git a/instrumentation/opentelemetry-api/opentelemetry-api-1.38/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_38/incubator/OpenTelemetryIncubatorInstrumentation.java b/instrumentation/opentelemetry-api/opentelemetry-api-1.38/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_38/incubator/OpenTelemetryIncubatorInstrumentation.java new file mode 100644 index 000000000000..ee7649b0622b --- /dev/null +++ b/instrumentation/opentelemetry-api/opentelemetry-api-1.38/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_38/incubator/OpenTelemetryIncubatorInstrumentation.java @@ -0,0 +1,40 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.opentelemetryapi.v1_38.incubator; + +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.none; + +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.opentelemetryapi.v1_38.incubator.metrics.ApplicationMeterFactory138Incubator; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class OpenTelemetryIncubatorInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("application.io.opentelemetry.api.GlobalOpenTelemetry"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + none(), OpenTelemetryIncubatorInstrumentation.class.getName() + "$InitAdvice"); + } + + @SuppressWarnings({"ReturnValueIgnored", "unused"}) + public static class InitAdvice { + @Advice.OnMethodEnter + public static void init() { + // the sole purpose of this advice is to ensure that ApplicationMeterFactory138Incubator + // is recognized as helper class and injected into class loader + ApplicationMeterFactory138Incubator.class.getName(); + } + } +} diff --git a/instrumentation/opentelemetry-api/opentelemetry-api-1.38/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_38/incubator/metrics/ApplicationDoubleGaugeBuilder138Incubator.java b/instrumentation/opentelemetry-api/opentelemetry-api-1.38/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_38/incubator/metrics/ApplicationDoubleGaugeBuilder138Incubator.java new file mode 100644 index 000000000000..738c377a28a2 --- /dev/null +++ b/instrumentation/opentelemetry-api/opentelemetry-api-1.38/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_38/incubator/metrics/ApplicationDoubleGaugeBuilder138Incubator.java @@ -0,0 +1,37 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.opentelemetryapi.v1_38.incubator.metrics; + +import application.io.opentelemetry.api.common.AttributeKey; +import application.io.opentelemetry.api.incubator.metrics.ExtendedDoubleGaugeBuilder; +import application.io.opentelemetry.api.metrics.LongGaugeBuilder; +import io.opentelemetry.javaagent.instrumentation.opentelemetryapi.trace.Bridging; +import io.opentelemetry.javaagent.instrumentation.opentelemetryapi.v1_38.metrics.ApplicationDoubleGaugeBuilder138; +import java.util.List; + +final class ApplicationDoubleGaugeBuilder138Incubator extends ApplicationDoubleGaugeBuilder138 + implements ExtendedDoubleGaugeBuilder { + + private final io.opentelemetry.api.metrics.DoubleGaugeBuilder agentBuilder; + + ApplicationDoubleGaugeBuilder138Incubator( + io.opentelemetry.api.metrics.DoubleGaugeBuilder agentBuilder) { + super(agentBuilder); + this.agentBuilder = agentBuilder; + } + + @Override + public LongGaugeBuilder ofLongs() { + return new ApplicationLongGaugeBuilder138Incubator(agentBuilder.ofLongs()); + } + + @Override + public ExtendedDoubleGaugeBuilder setAttributesAdvice(List> attributes) { + ((io.opentelemetry.api.incubator.metrics.ExtendedDoubleGaugeBuilder) agentBuilder) + .setAttributesAdvice(Bridging.toAgent(attributes)); + return this; + } +} diff --git a/instrumentation/opentelemetry-api/opentelemetry-api-1.38/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_38/incubator/metrics/ApplicationLongGaugeBuilder138Incubator.java b/instrumentation/opentelemetry-api/opentelemetry-api-1.38/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_38/incubator/metrics/ApplicationLongGaugeBuilder138Incubator.java new file mode 100644 index 000000000000..ecb96be16642 --- /dev/null +++ b/instrumentation/opentelemetry-api/opentelemetry-api-1.38/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_38/incubator/metrics/ApplicationLongGaugeBuilder138Incubator.java @@ -0,0 +1,31 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.opentelemetryapi.v1_38.incubator.metrics; + +import application.io.opentelemetry.api.common.AttributeKey; +import application.io.opentelemetry.api.incubator.metrics.ExtendedLongGaugeBuilder; +import io.opentelemetry.javaagent.instrumentation.opentelemetryapi.trace.Bridging; +import io.opentelemetry.javaagent.instrumentation.opentelemetryapi.v1_38.metrics.ApplicationLongGaugeBuilder138; +import java.util.List; + +final class ApplicationLongGaugeBuilder138Incubator extends ApplicationLongGaugeBuilder138 + implements ExtendedLongGaugeBuilder { + + private final io.opentelemetry.api.metrics.LongGaugeBuilder agentBuilder; + + ApplicationLongGaugeBuilder138Incubator( + io.opentelemetry.api.metrics.LongGaugeBuilder agentBuilder) { + super(agentBuilder); + this.agentBuilder = agentBuilder; + } + + @Override + public ExtendedLongGaugeBuilder setAttributesAdvice(List> attributes) { + ((io.opentelemetry.api.incubator.metrics.ExtendedLongGaugeBuilder) agentBuilder) + .setAttributesAdvice(Bridging.toAgent(attributes)); + return this; + } +} diff --git a/instrumentation/opentelemetry-api/opentelemetry-api-1.38/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_38/incubator/metrics/ApplicationMeter138Incubator.java b/instrumentation/opentelemetry-api/opentelemetry-api-1.38/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_38/incubator/metrics/ApplicationMeter138Incubator.java new file mode 100644 index 000000000000..f28e9f5f0ebc --- /dev/null +++ b/instrumentation/opentelemetry-api/opentelemetry-api-1.38/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_38/incubator/metrics/ApplicationMeter138Incubator.java @@ -0,0 +1,29 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.opentelemetryapi.v1_38.incubator.metrics; + +import application.io.opentelemetry.api.metrics.DoubleGaugeBuilder; +import io.opentelemetry.javaagent.instrumentation.opentelemetryapi.v1_37.incubator.metrics.BaseApplicationMeter137; +import io.opentelemetry.javaagent.instrumentation.opentelemetryapi.v1_38.metrics.ApplicationDoubleGaugeBuilder138; + +class ApplicationMeter138Incubator extends BaseApplicationMeter137 { + + private final io.opentelemetry.api.metrics.Meter agentMeter; + + ApplicationMeter138Incubator(io.opentelemetry.api.metrics.Meter agentMeter) { + super(agentMeter); + this.agentMeter = agentMeter; + } + + @Override + public DoubleGaugeBuilder gaugeBuilder(String name) { + io.opentelemetry.api.metrics.DoubleGaugeBuilder builder = agentMeter.gaugeBuilder(name); + if (builder instanceof io.opentelemetry.api.incubator.metrics.ExtendedDoubleGaugeBuilder) { + return new ApplicationDoubleGaugeBuilder138Incubator(builder); + } + return new ApplicationDoubleGaugeBuilder138(builder); + } +} diff --git a/instrumentation/opentelemetry-api/opentelemetry-api-1.38/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_38/incubator/metrics/ApplicationMeterFactory138Incubator.java b/instrumentation/opentelemetry-api/opentelemetry-api-1.38/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_38/incubator/metrics/ApplicationMeterFactory138Incubator.java new file mode 100644 index 000000000000..fad0f208a66e --- /dev/null +++ b/instrumentation/opentelemetry-api/opentelemetry-api-1.38/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_38/incubator/metrics/ApplicationMeterFactory138Incubator.java @@ -0,0 +1,18 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.opentelemetryapi.v1_38.incubator.metrics; + +import io.opentelemetry.api.metrics.Meter; +import io.opentelemetry.javaagent.instrumentation.opentelemetryapi.v1_10.metrics.ApplicationMeter; +import io.opentelemetry.javaagent.instrumentation.opentelemetryapi.v1_10.metrics.ApplicationMeterFactory; + +// this class is used from opentelemetry-api-1.27 via reflection +public final class ApplicationMeterFactory138Incubator implements ApplicationMeterFactory { + @Override + public ApplicationMeter newMeter(Meter agentMeter) { + return new ApplicationMeter138Incubator(agentMeter); + } +} diff --git a/instrumentation/opentelemetry-api/opentelemetry-api-1.38/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_38/metrics/ApplicationDoubleGaugeBuilder138.java b/instrumentation/opentelemetry-api/opentelemetry-api-1.38/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_38/metrics/ApplicationDoubleGaugeBuilder138.java new file mode 100644 index 000000000000..58e0c17c6ad2 --- /dev/null +++ b/instrumentation/opentelemetry-api/opentelemetry-api-1.38/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_38/metrics/ApplicationDoubleGaugeBuilder138.java @@ -0,0 +1,55 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.opentelemetryapi.v1_38.metrics; + +import application.io.opentelemetry.api.common.Attributes; +import application.io.opentelemetry.api.metrics.DoubleGauge; +import application.io.opentelemetry.api.metrics.LongGaugeBuilder; +import application.io.opentelemetry.context.Context; +import io.opentelemetry.javaagent.instrumentation.opentelemetryapi.context.AgentContextStorage; +import io.opentelemetry.javaagent.instrumentation.opentelemetryapi.trace.Bridging; +import io.opentelemetry.javaagent.instrumentation.opentelemetryapi.v1_10.metrics.ApplicationDoubleGaugeBuilder; + +public class ApplicationDoubleGaugeBuilder138 extends ApplicationDoubleGaugeBuilder { + + private final io.opentelemetry.api.metrics.DoubleGaugeBuilder agentBuilder; + + public ApplicationDoubleGaugeBuilder138( + io.opentelemetry.api.metrics.DoubleGaugeBuilder agentBuilder) { + super(agentBuilder); + this.agentBuilder = agentBuilder; + } + + @Override + public LongGaugeBuilder ofLongs() { + return new ApplicationLongGaugeBuilder138(agentBuilder.ofLongs()); + } + + @Override + public DoubleGauge build() { + io.opentelemetry.api.metrics.DoubleGauge agentDoubleGauge = agentBuilder.build(); + return new DoubleGauge() { + + @Override + public void set(double value) { + agentDoubleGauge.set(value); + } + + @Override + public void set(double value, Attributes attributes) { + agentDoubleGauge.set(value, Bridging.toAgent(attributes)); + } + + @Override + public void set(double value, Attributes attributes, Context applicationContext) { + agentDoubleGauge.set( + value, + Bridging.toAgent(attributes), + AgentContextStorage.getAgentContext(applicationContext)); + } + }; + } +} diff --git a/instrumentation/opentelemetry-api/opentelemetry-api-1.38/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_38/metrics/ApplicationLongGaugeBuilder138.java b/instrumentation/opentelemetry-api/opentelemetry-api-1.38/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_38/metrics/ApplicationLongGaugeBuilder138.java new file mode 100644 index 000000000000..288c16a53c7a --- /dev/null +++ b/instrumentation/opentelemetry-api/opentelemetry-api-1.38/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_38/metrics/ApplicationLongGaugeBuilder138.java @@ -0,0 +1,48 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.opentelemetryapi.v1_38.metrics; + +import application.io.opentelemetry.api.common.Attributes; +import application.io.opentelemetry.api.metrics.LongGauge; +import application.io.opentelemetry.context.Context; +import io.opentelemetry.javaagent.instrumentation.opentelemetryapi.context.AgentContextStorage; +import io.opentelemetry.javaagent.instrumentation.opentelemetryapi.trace.Bridging; +import io.opentelemetry.javaagent.instrumentation.opentelemetryapi.v1_10.metrics.ApplicationLongGaugeBuilder; + +public class ApplicationLongGaugeBuilder138 extends ApplicationLongGaugeBuilder { + + private final io.opentelemetry.api.metrics.LongGaugeBuilder agentBuilder; + + protected ApplicationLongGaugeBuilder138( + io.opentelemetry.api.metrics.LongGaugeBuilder agentBuilder) { + super(agentBuilder); + this.agentBuilder = agentBuilder; + } + + @Override + public LongGauge build() { + io.opentelemetry.api.metrics.LongGauge agentLongGauge = agentBuilder.build(); + return new LongGauge() { + @Override + public void set(long value) { + agentLongGauge.set(value); + } + + @Override + public void set(long value, Attributes attributes) { + agentLongGauge.set(value, Bridging.toAgent(attributes)); + } + + @Override + public void set(long value, Attributes attributes, Context applicationContext) { + agentLongGauge.set( + value, + Bridging.toAgent(attributes), + AgentContextStorage.getAgentContext(applicationContext)); + } + }; + } +} diff --git a/instrumentation/opentelemetry-api/opentelemetry-api-1.38/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_38/metrics/ApplicationMeter138.java b/instrumentation/opentelemetry-api/opentelemetry-api-1.38/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_38/metrics/ApplicationMeter138.java new file mode 100644 index 000000000000..e9fbb76a5d7b --- /dev/null +++ b/instrumentation/opentelemetry-api/opentelemetry-api-1.38/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_38/metrics/ApplicationMeter138.java @@ -0,0 +1,24 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.opentelemetryapi.v1_38.metrics; + +import application.io.opentelemetry.api.metrics.DoubleGaugeBuilder; +import io.opentelemetry.javaagent.instrumentation.opentelemetryapi.v1_32.metrics.ApplicationMeter132; + +public class ApplicationMeter138 extends ApplicationMeter132 { + + private final io.opentelemetry.api.metrics.Meter agentMeter; + + protected ApplicationMeter138(io.opentelemetry.api.metrics.Meter agentMeter) { + super(agentMeter); + this.agentMeter = agentMeter; + } + + @Override + public DoubleGaugeBuilder gaugeBuilder(String name) { + return new ApplicationDoubleGaugeBuilder138(agentMeter.gaugeBuilder(name)); + } +} diff --git a/instrumentation/opentelemetry-api/opentelemetry-api-1.38/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_38/metrics/ApplicationMeterFactory138.java b/instrumentation/opentelemetry-api/opentelemetry-api-1.38/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_38/metrics/ApplicationMeterFactory138.java new file mode 100644 index 000000000000..79b1e200d280 --- /dev/null +++ b/instrumentation/opentelemetry-api/opentelemetry-api-1.38/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_38/metrics/ApplicationMeterFactory138.java @@ -0,0 +1,18 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.opentelemetryapi.v1_38.metrics; + +import io.opentelemetry.api.metrics.Meter; +import io.opentelemetry.javaagent.instrumentation.opentelemetryapi.v1_10.metrics.ApplicationMeter; +import io.opentelemetry.javaagent.instrumentation.opentelemetryapi.v1_10.metrics.ApplicationMeterFactory; + +// this class is used from opentelemetry-api-1.27 via reflection +public final class ApplicationMeterFactory138 implements ApplicationMeterFactory { + @Override + public ApplicationMeter newMeter(Meter agentMeter) { + return new ApplicationMeter138(agentMeter); + } +} diff --git a/instrumentation/opentelemetry-api/opentelemetry-api-1.38/javaagent/src/noopTest/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_38/incubator/metrics/NoopTest.java b/instrumentation/opentelemetry-api/opentelemetry-api-1.38/javaagent/src/noopTest/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_38/incubator/metrics/NoopTest.java new file mode 100644 index 000000000000..00406416976c --- /dev/null +++ b/instrumentation/opentelemetry-api/opentelemetry-api-1.38/javaagent/src/noopTest/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_38/incubator/metrics/NoopTest.java @@ -0,0 +1,43 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.opentelemetryapi.v1_38.incubator.metrics; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.incubator.metrics.ExtendedDoubleGaugeBuilder; +import io.opentelemetry.api.incubator.metrics.ExtendedDoubleHistogramBuilder; +import io.opentelemetry.api.incubator.metrics.ExtendedLongCounterBuilder; +import io.opentelemetry.api.incubator.metrics.ExtendedLongUpDownCounterBuilder; +import io.opentelemetry.api.metrics.DoubleGaugeBuilder; +import io.opentelemetry.api.metrics.DoubleHistogramBuilder; +import io.opentelemetry.api.metrics.LongCounterBuilder; +import io.opentelemetry.api.metrics.LongUpDownCounterBuilder; +import io.opentelemetry.api.metrics.Meter; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +class NoopTest { + @RegisterExtension + static final AgentInstrumentationExtension testing = AgentInstrumentationExtension.create(); + + @Test + void noopInstance() { + Meter meter = testing.getOpenTelemetry().getMeter("test"); + + LongCounterBuilder counterBuilder = meter.counterBuilder("test"); + assertThat(counterBuilder).isNotInstanceOf(ExtendedLongCounterBuilder.class); + + LongUpDownCounterBuilder upDownCounterBuilder = meter.upDownCounterBuilder("test"); + assertThat(upDownCounterBuilder).isNotInstanceOf(ExtendedLongUpDownCounterBuilder.class); + + DoubleGaugeBuilder gaugeBuilder = meter.gaugeBuilder("test"); + assertThat(gaugeBuilder).isNotInstanceOf(ExtendedDoubleGaugeBuilder.class); + + DoubleHistogramBuilder histogramBuilder = meter.histogramBuilder("test"); + assertThat(histogramBuilder).isNotInstanceOf(ExtendedDoubleHistogramBuilder.class); + } +} diff --git a/instrumentation/opentelemetry-api/opentelemetry-api-1.38/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_38/metrics/MeterTest.java b/instrumentation/opentelemetry-api/opentelemetry-api-1.38/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_38/metrics/MeterTest.java new file mode 100644 index 000000000000..b2719df723e6 --- /dev/null +++ b/instrumentation/opentelemetry-api/opentelemetry-api-1.38/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_38/metrics/MeterTest.java @@ -0,0 +1,125 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.opentelemetryapi.v1_38.metrics; + +import static io.opentelemetry.api.common.AttributeKey.stringKey; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.metrics.DoubleGauge; +import io.opentelemetry.api.metrics.DoubleGaugeBuilder; +import io.opentelemetry.api.metrics.LongGauge; +import io.opentelemetry.api.metrics.LongGaugeBuilder; +import io.opentelemetry.api.metrics.Meter; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.sdk.common.InstrumentationScopeInfo; +import org.assertj.core.api.AbstractIterableAssert; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInfo; +import org.junit.jupiter.api.extension.RegisterExtension; + +class MeterTest { + + @RegisterExtension + static final AgentInstrumentationExtension testing = AgentInstrumentationExtension.create(); + + private String instrumentationName; + private Meter meter; + + @BeforeEach + void setupMeter(TestInfo test) { + instrumentationName = "test-" + test.getDisplayName(); + meter = + testing + .getOpenTelemetry() + .getMeterProvider() + .meterBuilder(instrumentationName) + .setInstrumentationVersion("1.2.3") + .setSchemaUrl("http://schema.org") + .build(); + } + + @Test + void syncLongGauge() throws InterruptedException { + LongGaugeBuilder builder = + meter.gaugeBuilder("test").ofLongs().setDescription("d").setUnit("u"); + + LongGauge longGauge = builder.build(); + longGauge.set(321); + longGauge.set(123, Attributes.of(stringKey("test"), "test")); + + testing.waitAndAssertMetrics( + instrumentationName, + "test", + metrics -> + metrics.anySatisfy( + metric -> + assertThat(metric) + .hasDescription("d") + .hasUnit("u") + .hasInstrumentationScope( + InstrumentationScopeInfo.builder(instrumentationName) + .setVersion("1.2.3") + .build()) + .hasLongGaugeSatisfying( + gauge -> + gauge.hasPointsSatisfying( + point -> point.hasValue(321).hasAttributes(Attributes.empty()), + point -> + point + .hasValue(123) + .hasAttributesSatisfying( + equalTo(stringKey("test"), "test")))))); + + // sleep exporter interval + Thread.sleep(100); + testing.clearData(); + Thread.sleep(100); + + testing.waitAndAssertMetrics(instrumentationName, "test", AbstractIterableAssert::isEmpty); + } + + @Test + void syncDoubleGauge() throws InterruptedException { + DoubleGaugeBuilder builder = meter.gaugeBuilder("test").setDescription("d").setUnit("u"); + + DoubleGauge doubleGauge = builder.build(); + doubleGauge.set(3.21); + doubleGauge.set(1.23, Attributes.of(stringKey("test"), "test")); + + testing.waitAndAssertMetrics( + instrumentationName, + "test", + metrics -> + metrics.anySatisfy( + metric -> + assertThat(metric) + .hasDescription("d") + .hasUnit("u") + .hasInstrumentationScope( + InstrumentationScopeInfo.builder(instrumentationName) + .setVersion("1.2.3") + .build()) + .hasDoubleGaugeSatisfying( + gauge -> + gauge.hasPointsSatisfying( + point -> point.hasValue(3.21).hasAttributes(Attributes.empty()), + point -> + point + .hasValue(1.23) + .hasAttributesSatisfying( + equalTo(stringKey("test"), "test")))))); + + // sleep exporter interval + Thread.sleep(100); + testing.clearData(); + Thread.sleep(100); + + testing.waitAndAssertMetrics(instrumentationName, "test", AbstractIterableAssert::isEmpty); + } +} diff --git a/instrumentation/opentelemetry-api/opentelemetry-api-1.4/javaagent/build.gradle.kts b/instrumentation/opentelemetry-api/opentelemetry-api-1.4/javaagent/build.gradle.kts index 357b795da7be..5861e77b22de 100644 --- a/instrumentation/opentelemetry-api/opentelemetry-api-1.4/javaagent/build.gradle.kts +++ b/instrumentation/opentelemetry-api/opentelemetry-api-1.4/javaagent/build.gradle.kts @@ -6,3 +6,11 @@ dependencies { compileOnly(project(":opentelemetry-api-shaded-for-instrumenting", configuration = "shadow")) implementation(project(":instrumentation:opentelemetry-api:opentelemetry-api-1.0:javaagent")) } + +configurations.configureEach { + if (name == "testRuntimeClasspath" || name == "testCompileClasspath") { + resolutionStrategy { + force("io.opentelemetry:opentelemetry-api:1.4.0") + } + } +} diff --git a/instrumentation/opentelemetry-api/opentelemetry-api-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_4/OpenTelemetryApiInstrumentationModule.java b/instrumentation/opentelemetry-api/opentelemetry-api-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_4/OpenTelemetryApiInstrumentationModule.java index 25db7cd01d90..de37424b8fbc 100644 --- a/instrumentation/opentelemetry-api/opentelemetry-api-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_4/OpenTelemetryApiInstrumentationModule.java +++ b/instrumentation/opentelemetry-api/opentelemetry-api-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opentelemetryapi/v1_4/OpenTelemetryApiInstrumentationModule.java @@ -10,10 +10,12 @@ 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.internal.ExperimentalInstrumentationModule; import java.util.List; @AutoService(InstrumentationModule.class) -public class OpenTelemetryApiInstrumentationModule extends InstrumentationModule { +public class OpenTelemetryApiInstrumentationModule extends InstrumentationModule + implements ExperimentalInstrumentationModule { public OpenTelemetryApiInstrumentationModule() { super("opentelemetry-api", "opentelemetry-api-1.4"); } @@ -22,4 +24,9 @@ public OpenTelemetryApiInstrumentationModule() { public List typeInstrumentations() { return singletonList(new OpenTelemetryInstrumentation()); } + + @Override + public String getModuleGroup() { + return "opentelemetry-api-bridge"; + } } diff --git a/instrumentation/opentelemetry-extension-annotations-1.0/README.md b/instrumentation/opentelemetry-extension-annotations-1.0/README.md index 6c4aae743ffa..f36aec9fc6fc 100644 --- a/instrumentation/opentelemetry-extension-annotations-1.0/README.md +++ b/instrumentation/opentelemetry-extension-annotations-1.0/README.md @@ -1,5 +1,5 @@ # Settings for the OpenTelemetry Extension Annotations integration -| Environment variable | Type | Default | Description | -|----------------- |------ |--------- |------------- | -| `otel.instrumentation.opentelemetry-annotations.exclude-methods` | String | | All methods to be excluded from auto-instrumentation by annotation-based advices. | +| Environment variable | Type | Default | Description | +| ---------------------------------------------------------------- | ------ | ------- | --------------------------------------------------------------------------------- | +| `otel.instrumentation.opentelemetry-annotations.exclude-methods` | String | | All methods to be excluded from auto-instrumentation by annotation-based advices. | diff --git a/instrumentation/opentelemetry-extension-annotations-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/extensionannotations/MethodCodeAttributesGetter.java b/instrumentation/opentelemetry-extension-annotations-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/extensionannotations/MethodCodeAttributesGetter.java index a9765596c05f..17b0fb2e1500 100644 --- a/instrumentation/opentelemetry-extension-annotations-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/extensionannotations/MethodCodeAttributesGetter.java +++ b/instrumentation/opentelemetry-extension-annotations-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/extensionannotations/MethodCodeAttributesGetter.java @@ -5,7 +5,7 @@ package io.opentelemetry.javaagent.instrumentation.extensionannotations; -import io.opentelemetry.instrumentation.api.instrumenter.code.CodeAttributesGetter; +import io.opentelemetry.instrumentation.api.incubator.semconv.code.CodeAttributesGetter; import java.lang.reflect.Method; enum MethodCodeAttributesGetter implements CodeAttributesGetter { diff --git a/instrumentation/opentelemetry-extension-annotations-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/extensionannotations/MethodRequestCodeAttributesGetter.java b/instrumentation/opentelemetry-extension-annotations-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/extensionannotations/MethodRequestCodeAttributesGetter.java index c5a64a4b9ed2..499b51852127 100644 --- a/instrumentation/opentelemetry-extension-annotations-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/extensionannotations/MethodRequestCodeAttributesGetter.java +++ b/instrumentation/opentelemetry-extension-annotations-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/extensionannotations/MethodRequestCodeAttributesGetter.java @@ -5,7 +5,7 @@ package io.opentelemetry.javaagent.instrumentation.extensionannotations; -import io.opentelemetry.instrumentation.api.instrumenter.code.CodeAttributesGetter; +import io.opentelemetry.instrumentation.api.incubator.semconv.code.CodeAttributesGetter; enum MethodRequestCodeAttributesGetter implements CodeAttributesGetter { INSTANCE; diff --git a/instrumentation/opentelemetry-extension-annotations-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/extensionannotations/WithSpanInstrumentation.java b/instrumentation/opentelemetry-extension-annotations-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/extensionannotations/WithSpanInstrumentation.java index f3c39511af12..4d99fcafbd3d 100644 --- a/instrumentation/opentelemetry-extension-annotations-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/extensionannotations/WithSpanInstrumentation.java +++ b/instrumentation/opentelemetry-extension-annotations-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/extensionannotations/WithSpanInstrumentation.java @@ -21,7 +21,7 @@ import io.opentelemetry.instrumentation.api.annotation.support.async.AsyncOperationEndSupport; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; import io.opentelemetry.javaagent.bootstrap.Java8BytecodeBridge; -import io.opentelemetry.javaagent.bootstrap.internal.InstrumentationConfig; +import io.opentelemetry.javaagent.bootstrap.internal.AgentInstrumentationConfig; import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; import io.opentelemetry.javaagent.tooling.config.MethodsConfigurationParser; @@ -98,7 +98,7 @@ static ElementMatcher.Junction configureExcludedMethods() { Map> excludedMethods = MethodsConfigurationParser.parse( - InstrumentationConfig.get().getString(TRACE_ANNOTATED_METHODS_EXCLUDE_CONFIG)); + AgentInstrumentationConfig.get().getString(TRACE_ANNOTATED_METHODS_EXCLUDE_CONFIG)); for (Map.Entry> entry : excludedMethods.entrySet()) { String className = entry.getKey(); ElementMatcher.Junction matcher = diff --git a/instrumentation/opentelemetry-extension-annotations-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/extensionannotations/WithSpanSingletons.java b/instrumentation/opentelemetry-extension-annotations-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/extensionannotations/WithSpanSingletons.java index 86479423ce85..ea29e091a461 100644 --- a/instrumentation/opentelemetry-extension-annotations-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/extensionannotations/WithSpanSingletons.java +++ b/instrumentation/opentelemetry-extension-annotations-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/extensionannotations/WithSpanSingletons.java @@ -10,9 +10,9 @@ import io.opentelemetry.api.GlobalOpenTelemetry; import io.opentelemetry.api.trace.SpanKind; import io.opentelemetry.instrumentation.api.annotation.support.MethodSpanAttributesExtractor; +import io.opentelemetry.instrumentation.api.incubator.semconv.code.CodeAttributesExtractor; +import io.opentelemetry.instrumentation.api.incubator.semconv.util.SpanNames; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; -import io.opentelemetry.instrumentation.api.instrumenter.code.CodeAttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.util.SpanNames; import java.lang.reflect.Method; import java.util.logging.Logger; diff --git a/instrumentation/opentelemetry-extension-annotations-1.0/javaagent/src/test/java/io/opentelemetry/test/annotation/WithSpanInstrumentationTest.java b/instrumentation/opentelemetry-extension-annotations-1.0/javaagent/src/test/java/io/opentelemetry/test/annotation/WithSpanInstrumentationTest.java index 9736f67c387a..c82608d2a89c 100644 --- a/instrumentation/opentelemetry-extension-annotations-1.0/javaagent/src/test/java/io/opentelemetry/test/annotation/WithSpanInstrumentationTest.java +++ b/instrumentation/opentelemetry-extension-annotations-1.0/javaagent/src/test/java/io/opentelemetry/test/annotation/WithSpanInstrumentationTest.java @@ -6,7 +6,6 @@ package io.opentelemetry.test.annotation; import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; -import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.entry; import io.opentelemetry.api.common.AttributeKey; @@ -14,7 +13,7 @@ import io.opentelemetry.api.trace.SpanKind; import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; import io.opentelemetry.sdk.trace.data.StatusData; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.opentelemetry.semconv.incubating.CodeIncubatingAttributes; import java.lang.reflect.Modifier; import java.util.concurrent.CompletableFuture; import net.bytebuddy.ByteBuddy; @@ -55,9 +54,11 @@ void deriveAutomaticName() throws Exception { assertThat(attributes) .containsOnly( entry( - SemanticAttributes.CODE_NAMESPACE, + CodeIncubatingAttributes.CODE_NAMESPACE, TracedWithSpan.class.getName()), - entry(SemanticAttributes.CODE_FUNCTION, "otel"))))); + entry( + CodeIncubatingAttributes.CODE_FUNCTION, + "otel"))))); } @Test @@ -80,10 +81,10 @@ void manualName() throws Exception { assertThat(attributes) .containsOnly( entry( - SemanticAttributes.CODE_NAMESPACE, + CodeIncubatingAttributes.CODE_NAMESPACE, TracedWithSpan.class.getName()), entry( - SemanticAttributes.CODE_FUNCTION, + CodeIncubatingAttributes.CODE_FUNCTION, "namedOtel"))))); } @@ -107,10 +108,10 @@ void manualKind() throws Exception { assertThat(attributes) .containsOnly( entry( - SemanticAttributes.CODE_NAMESPACE, + CodeIncubatingAttributes.CODE_NAMESPACE, TracedWithSpan.class.getName()), entry( - SemanticAttributes.CODE_FUNCTION, + CodeIncubatingAttributes.CODE_FUNCTION, "someKind"))))); } @@ -134,9 +135,11 @@ void multipleSpans() throws Exception { assertThat(attributes) .containsOnly( entry( - SemanticAttributes.CODE_NAMESPACE, + CodeIncubatingAttributes.CODE_NAMESPACE, TracedWithSpan.class.getName()), - entry(SemanticAttributes.CODE_FUNCTION, "server"))), + entry( + CodeIncubatingAttributes.CODE_FUNCTION, + "server"))), span -> assertThat(span) .hasName("TracedWithSpan.otel") @@ -147,9 +150,11 @@ void multipleSpans() throws Exception { assertThat(attributes) .containsOnly( entry( - SemanticAttributes.CODE_NAMESPACE, + CodeIncubatingAttributes.CODE_NAMESPACE, TracedWithSpan.class.getName()), - entry(SemanticAttributes.CODE_FUNCTION, "otel"))))); + entry( + CodeIncubatingAttributes.CODE_FUNCTION, + "otel"))))); } @Test @@ -158,7 +163,7 @@ void excludedMethod() throws Exception { new TracedWithSpan().ignored(); Thread.sleep(500); // sleep a bit just to make sure no span is captured - assertThat(testing.waitForTraces(0)); + assertThat(testing.waitForTraces(0)).isEmpty(); } @Test @@ -182,10 +187,10 @@ void completedCompletionStage() throws Exception { assertThat(attributes) .containsOnly( entry( - SemanticAttributes.CODE_NAMESPACE, + CodeIncubatingAttributes.CODE_NAMESPACE, TracedWithSpan.class.getName()), entry( - SemanticAttributes.CODE_FUNCTION, + CodeIncubatingAttributes.CODE_FUNCTION, "completionStage"))))); } @@ -212,10 +217,10 @@ void exceptionallyCompletedCompletionStage() throws Exception { assertThat(attributes) .containsOnly( entry( - SemanticAttributes.CODE_NAMESPACE, + CodeIncubatingAttributes.CODE_NAMESPACE, TracedWithSpan.class.getName()), entry( - SemanticAttributes.CODE_FUNCTION, + CodeIncubatingAttributes.CODE_FUNCTION, "completionStage"))))); } @@ -239,10 +244,10 @@ void nullCompletionStage() throws Exception { assertThat(attributes) .containsOnly( entry( - SemanticAttributes.CODE_NAMESPACE, + CodeIncubatingAttributes.CODE_NAMESPACE, TracedWithSpan.class.getName()), entry( - SemanticAttributes.CODE_FUNCTION, + CodeIncubatingAttributes.CODE_FUNCTION, "completionStage"))))); } @@ -253,7 +258,7 @@ void completingCompletionStage() throws Exception { new TracedWithSpan().completionStage(future); Thread.sleep(500); // sleep a bit just to make sure no span is captured - assertThat(testing.waitForTraces(0)); + assertThat(testing.waitForTraces(0)).isEmpty(); future.complete("Done"); @@ -272,10 +277,10 @@ void completingCompletionStage() throws Exception { assertThat(attributes) .containsOnly( entry( - SemanticAttributes.CODE_NAMESPACE, + CodeIncubatingAttributes.CODE_NAMESPACE, TracedWithSpan.class.getName()), entry( - SemanticAttributes.CODE_FUNCTION, + CodeIncubatingAttributes.CODE_FUNCTION, "completionStage"))))); } @@ -286,7 +291,7 @@ void exceptionallyCompletingCompletionStage() throws Exception { new TracedWithSpan().completionStage(future); Thread.sleep(500); // sleep a bit just to make sure no span is captured - assertThat(testing.waitForTraces(0)); + assertThat(testing.waitForTraces(0)).isEmpty(); future.completeExceptionally(new IllegalArgumentException("Boom")); @@ -306,10 +311,10 @@ void exceptionallyCompletingCompletionStage() throws Exception { assertThat(attributes) .containsOnly( entry( - SemanticAttributes.CODE_NAMESPACE, + CodeIncubatingAttributes.CODE_NAMESPACE, TracedWithSpan.class.getName()), entry( - SemanticAttributes.CODE_FUNCTION, + CodeIncubatingAttributes.CODE_FUNCTION, "completionStage"))))); } @@ -334,10 +339,10 @@ void completedCompletableFuture() throws Exception { assertThat(attributes) .containsOnly( entry( - SemanticAttributes.CODE_NAMESPACE, + CodeIncubatingAttributes.CODE_NAMESPACE, TracedWithSpan.class.getName()), entry( - SemanticAttributes.CODE_FUNCTION, + CodeIncubatingAttributes.CODE_FUNCTION, "completableFuture"))))); } @@ -364,10 +369,10 @@ void exceptionallyCompletedCompletableFuture() throws Exception { assertThat(attributes) .containsOnly( entry( - SemanticAttributes.CODE_NAMESPACE, + CodeIncubatingAttributes.CODE_NAMESPACE, TracedWithSpan.class.getName()), entry( - SemanticAttributes.CODE_FUNCTION, + CodeIncubatingAttributes.CODE_FUNCTION, "completableFuture"))))); } @@ -391,10 +396,10 @@ void nullCompletableFuture() throws Exception { assertThat(attributes) .containsOnly( entry( - SemanticAttributes.CODE_NAMESPACE, + CodeIncubatingAttributes.CODE_NAMESPACE, TracedWithSpan.class.getName()), entry( - SemanticAttributes.CODE_FUNCTION, + CodeIncubatingAttributes.CODE_FUNCTION, "completableFuture"))))); } @@ -405,7 +410,7 @@ void completingCompletableFuture() throws Exception { new TracedWithSpan().completableFuture(future); Thread.sleep(500); // sleep a bit just to make sure no span is captured - assertThat(testing.waitForTraces(0)); + assertThat(testing.waitForTraces(0)).isEmpty(); future.complete("Done"); @@ -424,10 +429,10 @@ void completingCompletableFuture() throws Exception { assertThat(attributes) .containsOnly( entry( - SemanticAttributes.CODE_NAMESPACE, + CodeIncubatingAttributes.CODE_NAMESPACE, TracedWithSpan.class.getName()), entry( - SemanticAttributes.CODE_FUNCTION, + CodeIncubatingAttributes.CODE_FUNCTION, "completableFuture"))))); } @@ -438,7 +443,7 @@ void exceptionallyCompletingCompletableFuture() throws Exception { new TracedWithSpan().completableFuture(future); Thread.sleep(500); // sleep a bit just to make sure no span is captured - assertThat(testing.waitForTraces(0)); + assertThat(testing.waitForTraces(0)).isEmpty(); future.completeExceptionally(new IllegalArgumentException("Boom")); @@ -458,10 +463,10 @@ void exceptionallyCompletingCompletableFuture() throws Exception { assertThat(attributes) .containsOnly( entry( - SemanticAttributes.CODE_NAMESPACE, + CodeIncubatingAttributes.CODE_NAMESPACE, TracedWithSpan.class.getName()), entry( - SemanticAttributes.CODE_FUNCTION, + CodeIncubatingAttributes.CODE_FUNCTION, "completableFuture"))))); } @@ -485,10 +490,10 @@ void captureAttributes() throws Exception { assertThat(attributes) .containsOnly( entry( - SemanticAttributes.CODE_NAMESPACE, + CodeIncubatingAttributes.CODE_NAMESPACE, TracedWithSpan.class.getName()), entry( - SemanticAttributes.CODE_FUNCTION, + CodeIncubatingAttributes.CODE_FUNCTION, "withSpanAttributes"), entry( AttributeKey.stringKey("implicitName"), "foo"), @@ -551,9 +556,11 @@ public void run() { assertThat(attributes) .containsOnly( entry( - SemanticAttributes.CODE_NAMESPACE, + CodeIncubatingAttributes.CODE_NAMESPACE, "GeneratedJava6TestClass"), - entry(SemanticAttributes.CODE_FUNCTION, "run"))), + entry( + CodeIncubatingAttributes.CODE_FUNCTION, + "run"))), span -> assertThat(span) .hasName("intercept") diff --git a/instrumentation/opentelemetry-extension-kotlin-1.0/javaagent/build.gradle.kts b/instrumentation/opentelemetry-extension-kotlin-1.0/javaagent/build.gradle.kts index 87d70ae0fe01..e1a91cc09888 100644 --- a/instrumentation/opentelemetry-extension-kotlin-1.0/javaagent/build.gradle.kts +++ b/instrumentation/opentelemetry-extension-kotlin-1.0/javaagent/build.gradle.kts @@ -1,3 +1,5 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + plugins { id("org.jetbrains.kotlin.jvm") id("otel.javaagent-instrumentation") @@ -42,10 +44,8 @@ if (!(findProperty("testLatestDeps") as Boolean)) { } } -tasks { - withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile::class).configureEach { - kotlinOptions { - jvmTarget = "1.8" - } +kotlin { + compilerOptions { + jvmTarget.set(JvmTarget.JVM_1_8) } } diff --git a/instrumentation/opentelemetry-extension-kotlin-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/extensionkotlin/ContextExtensionInstrumentationModule.java b/instrumentation/opentelemetry-extension-kotlin-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/extensionkotlin/ContextExtensionInstrumentationModule.java index 364a1591113a..4d90667c42f1 100644 --- a/instrumentation/opentelemetry-extension-kotlin-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/extensionkotlin/ContextExtensionInstrumentationModule.java +++ b/instrumentation/opentelemetry-extension-kotlin-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/extensionkotlin/ContextExtensionInstrumentationModule.java @@ -10,10 +10,12 @@ 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.internal.ExperimentalInstrumentationModule; import java.util.List; @AutoService(InstrumentationModule.class) -public class ContextExtensionInstrumentationModule extends InstrumentationModule { +public class ContextExtensionInstrumentationModule extends InstrumentationModule + implements ExperimentalInstrumentationModule { public ContextExtensionInstrumentationModule() { super("opentelemetry-extension-kotlin"); @@ -28,4 +30,9 @@ public boolean isHelperClass(String className) { public List typeInstrumentations() { return singletonList(new ContextExtensionInstrumentation()); } + + @Override + public String getModuleGroup() { + return "opentelemetry-api-bridge"; + } } diff --git a/instrumentation/opentelemetry-extension-kotlin-1.0/javaagent/src/test/kotlin/io/opentelemetry/javaagent/instrumentation/extensionkotlin/ContextExtensionInstrumentationTest.kt b/instrumentation/opentelemetry-extension-kotlin-1.0/javaagent/src/test/kotlin/io/opentelemetry/javaagent/instrumentation/extensionkotlin/ContextExtensionInstrumentationTest.kt index f128de112a97..15fc7c6c28f9 100644 --- a/instrumentation/opentelemetry-extension-kotlin-1.0/javaagent/src/test/kotlin/io/opentelemetry/javaagent/instrumentation/extensionkotlin/ContextExtensionInstrumentationTest.kt +++ b/instrumentation/opentelemetry-extension-kotlin-1.0/javaagent/src/test/kotlin/io/opentelemetry/javaagent/instrumentation/extensionkotlin/ContextExtensionInstrumentationTest.kt @@ -13,17 +13,17 @@ import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Test class ContextExtensionInstrumentationTest { - private val ANIMAL: ContextKey = ContextKey.named("animal") + private val animalKey: ContextKey = ContextKey.named("animal") @Test fun `is instrumented`() { - val context1 = Context.root().with(ANIMAL, "cat") + val context1 = Context.root().with(animalKey, "cat") val contextElement = context1.asContextElement() // check that the context element is from the opentelemetry-extension-kotlin that is shaded // inside the agent assertThat(contextElement.javaClass.name).startsWith("io.opentelemetry.javaagent.shaded") val context2 = contextElement.getOpenTelemetryContext() - assertThat(context2.get(ANIMAL)).isEqualTo("cat") + assertThat(context2.get(animalKey)).isEqualTo("cat") // instrumentation does not preserve context identity due to conversion between application and // agent context assert(context1 != context2) { "Not instrumented" } diff --git a/instrumentation/opentelemetry-instrumentation-annotations-1.16/README.md b/instrumentation/opentelemetry-instrumentation-annotations-1.16/README.md index ca1f2498c920..33bec5e25772 100644 --- a/instrumentation/opentelemetry-instrumentation-annotations-1.16/README.md +++ b/instrumentation/opentelemetry-instrumentation-annotations-1.16/README.md @@ -1,5 +1,5 @@ # Settings for the OpenTelemetry Instrumentation Annotations integration -| Environment variable | Type | Default | Description | -|----------------- |------ |--------- |------------- | -| `otel.instrumentation.opentelemetry-instrumentation-annotations.exclude-methods` | String | | All methods to be excluded from auto-instrumentation by annotation-based advices. | +| Environment variable | Type | Default | Description | +| -------------------------------------------------------------------------------- | ------ | ------- | --------------------------------------------------------------------------------- | +| `otel.instrumentation.opentelemetry-instrumentation-annotations.exclude-methods` | String | | All methods to be excluded from auto-instrumentation by annotation-based advices. | diff --git a/instrumentation/opentelemetry-instrumentation-annotations-1.16/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationannotations/AnnotationExcludedMethods.java b/instrumentation/opentelemetry-instrumentation-annotations-1.16/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationannotations/AnnotationExcludedMethods.java index 264d83390246..9bec0120fe70 100644 --- a/instrumentation/opentelemetry-instrumentation-annotations-1.16/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationannotations/AnnotationExcludedMethods.java +++ b/instrumentation/opentelemetry-instrumentation-annotations-1.16/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationannotations/AnnotationExcludedMethods.java @@ -9,7 +9,7 @@ import static net.bytebuddy.matcher.ElementMatchers.namedOneOf; import static net.bytebuddy.matcher.ElementMatchers.none; -import io.opentelemetry.javaagent.bootstrap.internal.InstrumentationConfig; +import io.opentelemetry.javaagent.bootstrap.internal.AgentInstrumentationConfig; import io.opentelemetry.javaagent.tooling.config.MethodsConfigurationParser; import java.util.Map; import java.util.Set; @@ -18,7 +18,7 @@ import net.bytebuddy.matcher.ElementMatcher; import net.bytebuddy.matcher.ElementMatchers; -final class AnnotationExcludedMethods { +public final class AnnotationExcludedMethods { private static final String TRACE_ANNOTATED_METHODS_EXCLUDE_CONFIG = "otel.instrumentation.opentelemetry-instrumentation-annotations.exclude-methods"; @@ -27,12 +27,12 @@ final class AnnotationExcludedMethods { Returns a matcher for all methods that should be excluded from auto-instrumentation by annotation-based advices. */ - static ElementMatcher.Junction configureExcludedMethods() { + public static ElementMatcher.Junction configureExcludedMethods() { ElementMatcher.Junction result = none(); Map> excludedMethods = MethodsConfigurationParser.parse( - InstrumentationConfig.get().getString(TRACE_ANNOTATED_METHODS_EXCLUDE_CONFIG)); + AgentInstrumentationConfig.get().getString(TRACE_ANNOTATED_METHODS_EXCLUDE_CONFIG)); for (Map.Entry> entry : excludedMethods.entrySet()) { String className = entry.getKey(); ElementMatcher.Junction matcher = diff --git a/instrumentation/opentelemetry-instrumentation-annotations-1.16/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationannotations/AnnotationInstrumentationModule.java b/instrumentation/opentelemetry-instrumentation-annotations-1.16/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationannotations/AnnotationInstrumentationModule.java index c6c4f3ce79c6..71af12b47ea8 100644 --- a/instrumentation/opentelemetry-instrumentation-annotations-1.16/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationannotations/AnnotationInstrumentationModule.java +++ b/instrumentation/opentelemetry-instrumentation-annotations-1.16/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationannotations/AnnotationInstrumentationModule.java @@ -24,7 +24,7 @@ public class AnnotationInstrumentationModule extends InstrumentationModule { public AnnotationInstrumentationModule() { - super("opentelemetry-instrumentation-annotations"); + super("opentelemetry-instrumentation-annotations", "annotations"); } @Override diff --git a/instrumentation/opentelemetry-instrumentation-annotations-1.16/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationannotations/AnnotationSingletons.java b/instrumentation/opentelemetry-instrumentation-annotations-1.16/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationannotations/AnnotationSingletons.java index ce2061c4e4bf..ff1d75aec95e 100644 --- a/instrumentation/opentelemetry-instrumentation-annotations-1.16/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationannotations/AnnotationSingletons.java +++ b/instrumentation/opentelemetry-instrumentation-annotations-1.16/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationannotations/AnnotationSingletons.java @@ -12,9 +12,9 @@ import io.opentelemetry.api.trace.SpanKind; import io.opentelemetry.instrumentation.api.annotation.support.MethodSpanAttributesExtractor; import io.opentelemetry.instrumentation.api.annotation.support.SpanAttributesExtractor; +import io.opentelemetry.instrumentation.api.incubator.semconv.code.CodeAttributesExtractor; +import io.opentelemetry.instrumentation.api.incubator.semconv.util.SpanNames; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; -import io.opentelemetry.instrumentation.api.instrumenter.code.CodeAttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.util.SpanNames; import java.lang.reflect.Method; import java.util.logging.Logger; diff --git a/instrumentation/opentelemetry-instrumentation-annotations-1.16/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationannotations/KotlinCoroutineUtil.java b/instrumentation/opentelemetry-instrumentation-annotations-1.16/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationannotations/KotlinCoroutineUtil.java new file mode 100644 index 000000000000..7c2e38e3d1b5 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-annotations-1.16/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationannotations/KotlinCoroutineUtil.java @@ -0,0 +1,32 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.instrumentationannotations; + +import static net.bytebuddy.matcher.ElementMatchers.returns; + +import net.bytebuddy.description.method.MethodDescription; +import net.bytebuddy.description.method.ParameterList; +import net.bytebuddy.matcher.ElementMatcher; + +public final class KotlinCoroutineUtil { + + private KotlinCoroutineUtil() {} + + public static ElementMatcher isKotlinSuspendMethod() { + // kotlin suspend methods return Object and take kotlin.coroutines.Continuation as last argument + return returns(Object.class) + .and( + target -> { + ParameterList parameterList = target.getParameters(); + if (!parameterList.isEmpty()) { + String lastParameter = + parameterList.get(parameterList.size() - 1).getType().asErasure().getName(); + return "kotlin.coroutines.Continuation".equals(lastParameter); + } + return false; + }); + } +} diff --git a/instrumentation/opentelemetry-instrumentation-annotations-1.16/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationannotations/MethodCodeAttributesGetter.java b/instrumentation/opentelemetry-instrumentation-annotations-1.16/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationannotations/MethodCodeAttributesGetter.java index d15d02b127d6..8f1347f30e6d 100644 --- a/instrumentation/opentelemetry-instrumentation-annotations-1.16/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationannotations/MethodCodeAttributesGetter.java +++ b/instrumentation/opentelemetry-instrumentation-annotations-1.16/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationannotations/MethodCodeAttributesGetter.java @@ -5,7 +5,7 @@ package io.opentelemetry.javaagent.instrumentation.instrumentationannotations; -import io.opentelemetry.instrumentation.api.instrumenter.code.CodeAttributesGetter; +import io.opentelemetry.instrumentation.api.incubator.semconv.code.CodeAttributesGetter; import java.lang.reflect.Method; enum MethodCodeAttributesGetter implements CodeAttributesGetter { diff --git a/instrumentation/opentelemetry-instrumentation-annotations-1.16/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationannotations/MethodRequestCodeAttributesGetter.java b/instrumentation/opentelemetry-instrumentation-annotations-1.16/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationannotations/MethodRequestCodeAttributesGetter.java index 3e4d2be4e61d..d45600939f49 100644 --- a/instrumentation/opentelemetry-instrumentation-annotations-1.16/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationannotations/MethodRequestCodeAttributesGetter.java +++ b/instrumentation/opentelemetry-instrumentation-annotations-1.16/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationannotations/MethodRequestCodeAttributesGetter.java @@ -5,7 +5,7 @@ package io.opentelemetry.javaagent.instrumentation.instrumentationannotations; -import io.opentelemetry.instrumentation.api.instrumenter.code.CodeAttributesGetter; +import io.opentelemetry.instrumentation.api.incubator.semconv.code.CodeAttributesGetter; enum MethodRequestCodeAttributesGetter implements CodeAttributesGetter { INSTANCE; diff --git a/instrumentation/opentelemetry-instrumentation-annotations-1.16/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationannotations/WithSpanInstrumentation.java b/instrumentation/opentelemetry-instrumentation-annotations-1.16/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationannotations/WithSpanInstrumentation.java index 71ce8a8b36fd..9cfcc18666aa 100644 --- a/instrumentation/opentelemetry-instrumentation-annotations-1.16/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationannotations/WithSpanInstrumentation.java +++ b/instrumentation/opentelemetry-instrumentation-annotations-1.16/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationannotations/WithSpanInstrumentation.java @@ -7,6 +7,7 @@ import static io.opentelemetry.javaagent.instrumentation.instrumentationannotations.AnnotationSingletons.instrumenter; import static io.opentelemetry.javaagent.instrumentation.instrumentationannotations.AnnotationSingletons.instrumenterWithAttributes; +import static io.opentelemetry.javaagent.instrumentation.instrumentationannotations.KotlinCoroutineUtil.isKotlinSuspendMethod; import static net.bytebuddy.matcher.ElementMatchers.declaresMethod; import static net.bytebuddy.matcher.ElementMatchers.hasParameters; import static net.bytebuddy.matcher.ElementMatchers.isAnnotatedWith; @@ -29,7 +30,7 @@ import net.bytebuddy.implementation.bytecode.assign.Assigner; import net.bytebuddy.matcher.ElementMatcher; -public class WithSpanInstrumentation implements TypeInstrumentation { +class WithSpanInstrumentation implements TypeInstrumentation { private final ElementMatcher.Junction annotatedMethodMatcher; private final ElementMatcher.Junction annotatedParametersMatcher; @@ -45,7 +46,9 @@ public class WithSpanInstrumentation implements TypeInstrumentation { isAnnotatedWith( named( "application.io.opentelemetry.instrumentation.annotations.SpanAttribute")))); - excludedMethodsMatcher = AnnotationExcludedMethods.configureExcludedMethods(); + // exclude all kotlin suspend methods, these are handle in kotlinx-coroutines instrumentation + excludedMethodsMatcher = + AnnotationExcludedMethods.configureExcludedMethods().or(isKotlinSuspendMethod()); } @Override diff --git a/instrumentation/opentelemetry-instrumentation-annotations-1.16/javaagent/src/test/java/io/opentelemetry/test/annotation/AddingSpanAttributesInstrumentationTest.java b/instrumentation/opentelemetry-instrumentation-annotations-1.16/javaagent/src/test/java/io/opentelemetry/test/annotation/AddingSpanAttributesInstrumentationTest.java index b52356e6bc8e..686a8264f92e 100644 --- a/instrumentation/opentelemetry-instrumentation-annotations-1.16/javaagent/src/test/java/io/opentelemetry/test/annotation/AddingSpanAttributesInstrumentationTest.java +++ b/instrumentation/opentelemetry-instrumentation-annotations-1.16/javaagent/src/test/java/io/opentelemetry/test/annotation/AddingSpanAttributesInstrumentationTest.java @@ -13,7 +13,7 @@ import io.opentelemetry.api.trace.SpanId; import io.opentelemetry.api.trace.SpanKind; import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.opentelemetry.semconv.incubating.CodeIncubatingAttributes; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; @@ -53,11 +53,11 @@ void captureAttributesInNewSpan() throws Exception { assertThat(attributes) .containsOnly( entry( - SemanticAttributes.CODE_NAMESPACE, + CodeIncubatingAttributes.CODE_NAMESPACE, ExtractAttributesUsingAddingSpanAttributes.class .getName()), entry( - SemanticAttributes.CODE_FUNCTION, + CodeIncubatingAttributes.CODE_FUNCTION, "withSpanTakesPrecedence"), entry( AttributeKey.stringKey("implicitName"), "foo"), @@ -101,7 +101,7 @@ void noExistingSpan() throws Exception { new ExtractAttributesUsingAddingSpanAttributes().withSpanAttributes("foo", "bar", null, "baz"); - assertThat(testing.waitForTraces(0)); + assertThat(testing.waitForTraces(0)).isEmpty(); } @Test diff --git a/instrumentation/opentelemetry-instrumentation-annotations-1.16/javaagent/src/test/java/io/opentelemetry/test/annotation/WithSpanInstrumentationTest.java b/instrumentation/opentelemetry-instrumentation-annotations-1.16/javaagent/src/test/java/io/opentelemetry/test/annotation/WithSpanInstrumentationTest.java index 2ec26e4c5d07..aa15a47c5ce6 100644 --- a/instrumentation/opentelemetry-instrumentation-annotations-1.16/javaagent/src/test/java/io/opentelemetry/test/annotation/WithSpanInstrumentationTest.java +++ b/instrumentation/opentelemetry-instrumentation-annotations-1.16/javaagent/src/test/java/io/opentelemetry/test/annotation/WithSpanInstrumentationTest.java @@ -15,7 +15,7 @@ import io.opentelemetry.instrumentation.annotations.WithSpan; import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; import io.opentelemetry.sdk.trace.data.StatusData; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.opentelemetry.semconv.incubating.CodeIncubatingAttributes; import java.lang.reflect.Modifier; import java.util.concurrent.CompletableFuture; import net.bytebuddy.ByteBuddy; @@ -55,9 +55,11 @@ void deriveAutomaticName() throws Exception { assertThat(attributes) .containsOnly( entry( - SemanticAttributes.CODE_NAMESPACE, + CodeIncubatingAttributes.CODE_NAMESPACE, TracedWithSpan.class.getName()), - entry(SemanticAttributes.CODE_FUNCTION, "otel"))))); + entry( + CodeIncubatingAttributes.CODE_FUNCTION, + "otel"))))); } @Test @@ -80,10 +82,10 @@ void manualName() throws Exception { assertThat(attributes) .containsOnly( entry( - SemanticAttributes.CODE_NAMESPACE, + CodeIncubatingAttributes.CODE_NAMESPACE, TracedWithSpan.class.getName()), entry( - SemanticAttributes.CODE_FUNCTION, + CodeIncubatingAttributes.CODE_FUNCTION, "namedOtel"))))); } @@ -107,10 +109,10 @@ void manualKind() throws Exception { assertThat(attributes) .containsOnly( entry( - SemanticAttributes.CODE_NAMESPACE, + CodeIncubatingAttributes.CODE_NAMESPACE, TracedWithSpan.class.getName()), entry( - SemanticAttributes.CODE_FUNCTION, + CodeIncubatingAttributes.CODE_FUNCTION, "someKind"))))); } @@ -134,9 +136,11 @@ void multipleSpans() throws Exception { assertThat(attributes) .containsOnly( entry( - SemanticAttributes.CODE_NAMESPACE, + CodeIncubatingAttributes.CODE_NAMESPACE, TracedWithSpan.class.getName()), - entry(SemanticAttributes.CODE_FUNCTION, "server"))), + entry( + CodeIncubatingAttributes.CODE_FUNCTION, + "server"))), span -> assertThat(span) .hasName("TracedWithSpan.otel") @@ -147,9 +151,11 @@ void multipleSpans() throws Exception { assertThat(attributes) .containsOnly( entry( - SemanticAttributes.CODE_NAMESPACE, + CodeIncubatingAttributes.CODE_NAMESPACE, TracedWithSpan.class.getName()), - entry(SemanticAttributes.CODE_FUNCTION, "otel"))))); + entry( + CodeIncubatingAttributes.CODE_FUNCTION, + "otel"))))); } @Test @@ -158,7 +164,7 @@ void excludedMethod() throws Exception { new TracedWithSpan().ignored(); Thread.sleep(500); // sleep a bit just to make sure no span is captured - assertThat(testing.waitForTraces(0)); + assertThat(testing.waitForTraces(0)).isEmpty(); } @Test @@ -182,10 +188,10 @@ void completedCompletionStage() throws Exception { assertThat(attributes) .containsOnly( entry( - SemanticAttributes.CODE_NAMESPACE, + CodeIncubatingAttributes.CODE_NAMESPACE, TracedWithSpan.class.getName()), entry( - SemanticAttributes.CODE_FUNCTION, + CodeIncubatingAttributes.CODE_FUNCTION, "completionStage"))))); } @@ -212,10 +218,10 @@ void exceptionallyCompletedCompletionStage() throws Exception { assertThat(attributes) .containsOnly( entry( - SemanticAttributes.CODE_NAMESPACE, + CodeIncubatingAttributes.CODE_NAMESPACE, TracedWithSpan.class.getName()), entry( - SemanticAttributes.CODE_FUNCTION, + CodeIncubatingAttributes.CODE_FUNCTION, "completionStage"))))); } @@ -239,10 +245,10 @@ void nullCompletionStage() throws Exception { assertThat(attributes) .containsOnly( entry( - SemanticAttributes.CODE_NAMESPACE, + CodeIncubatingAttributes.CODE_NAMESPACE, TracedWithSpan.class.getName()), entry( - SemanticAttributes.CODE_FUNCTION, + CodeIncubatingAttributes.CODE_FUNCTION, "completionStage"))))); } @@ -253,7 +259,7 @@ void completingCompletionStage() throws Exception { new TracedWithSpan().completionStage(future); Thread.sleep(500); // sleep a bit just to make sure no span is captured - assertThat(testing.waitForTraces(0)); + assertThat(testing.waitForTraces(0)).isEmpty(); future.complete("Done"); @@ -272,10 +278,10 @@ void completingCompletionStage() throws Exception { assertThat(attributes) .containsOnly( entry( - SemanticAttributes.CODE_NAMESPACE, + CodeIncubatingAttributes.CODE_NAMESPACE, TracedWithSpan.class.getName()), entry( - SemanticAttributes.CODE_FUNCTION, + CodeIncubatingAttributes.CODE_FUNCTION, "completionStage"))))); } @@ -286,7 +292,7 @@ void exceptionallyCompletingCompletionStage() throws Exception { new TracedWithSpan().completionStage(future); Thread.sleep(500); // sleep a bit just to make sure no span is captured - assertThat(testing.waitForTraces(0)); + assertThat(testing.waitForTraces(0)).isEmpty(); future.completeExceptionally(new IllegalArgumentException("Boom")); @@ -306,10 +312,10 @@ void exceptionallyCompletingCompletionStage() throws Exception { assertThat(attributes) .containsOnly( entry( - SemanticAttributes.CODE_NAMESPACE, + CodeIncubatingAttributes.CODE_NAMESPACE, TracedWithSpan.class.getName()), entry( - SemanticAttributes.CODE_FUNCTION, + CodeIncubatingAttributes.CODE_FUNCTION, "completionStage"))))); } @@ -334,10 +340,10 @@ void completedCompletableFuture() throws Exception { assertThat(attributes) .containsOnly( entry( - SemanticAttributes.CODE_NAMESPACE, + CodeIncubatingAttributes.CODE_NAMESPACE, TracedWithSpan.class.getName()), entry( - SemanticAttributes.CODE_FUNCTION, + CodeIncubatingAttributes.CODE_FUNCTION, "completableFuture"))))); } @@ -364,10 +370,10 @@ void exceptionallyCompletedCompletableFuture() throws Exception { assertThat(attributes) .containsOnly( entry( - SemanticAttributes.CODE_NAMESPACE, + CodeIncubatingAttributes.CODE_NAMESPACE, TracedWithSpan.class.getName()), entry( - SemanticAttributes.CODE_FUNCTION, + CodeIncubatingAttributes.CODE_FUNCTION, "completableFuture"))))); } @@ -391,10 +397,10 @@ void nullCompletableFuture() throws Exception { assertThat(attributes) .containsOnly( entry( - SemanticAttributes.CODE_NAMESPACE, + CodeIncubatingAttributes.CODE_NAMESPACE, TracedWithSpan.class.getName()), entry( - SemanticAttributes.CODE_FUNCTION, + CodeIncubatingAttributes.CODE_FUNCTION, "completableFuture"))))); } @@ -405,7 +411,7 @@ void completingCompletableFuture() throws Exception { new TracedWithSpan().completableFuture(future); Thread.sleep(500); // sleep a bit just to make sure no span is captured - assertThat(testing.waitForTraces(0)); + assertThat(testing.waitForTraces(0)).isEmpty(); future.complete("Done"); @@ -424,10 +430,10 @@ void completingCompletableFuture() throws Exception { assertThat(attributes) .containsOnly( entry( - SemanticAttributes.CODE_NAMESPACE, + CodeIncubatingAttributes.CODE_NAMESPACE, TracedWithSpan.class.getName()), entry( - SemanticAttributes.CODE_FUNCTION, + CodeIncubatingAttributes.CODE_FUNCTION, "completableFuture"))))); } @@ -438,7 +444,7 @@ void exceptionallyCompletingCompletableFuture() throws Exception { new TracedWithSpan().completableFuture(future); Thread.sleep(500); // sleep a bit just to make sure no span is captured - assertThat(testing.waitForTraces(0)); + assertThat(testing.waitForTraces(0)).isEmpty(); future.completeExceptionally(new IllegalArgumentException("Boom")); @@ -458,10 +464,10 @@ void exceptionallyCompletingCompletableFuture() throws Exception { assertThat(attributes) .containsOnly( entry( - SemanticAttributes.CODE_NAMESPACE, + CodeIncubatingAttributes.CODE_NAMESPACE, TracedWithSpan.class.getName()), entry( - SemanticAttributes.CODE_FUNCTION, + CodeIncubatingAttributes.CODE_FUNCTION, "completableFuture"))))); } @@ -485,10 +491,10 @@ void captureAttributes() throws Exception { assertThat(attributes) .containsOnly( entry( - SemanticAttributes.CODE_NAMESPACE, + CodeIncubatingAttributes.CODE_NAMESPACE, TracedWithSpan.class.getName()), entry( - SemanticAttributes.CODE_FUNCTION, + CodeIncubatingAttributes.CODE_FUNCTION, "withSpanAttributes"), entry( AttributeKey.stringKey("implicitName"), "foo"), @@ -548,9 +554,11 @@ public void run() { assertThat(attributes) .containsOnly( entry( - SemanticAttributes.CODE_NAMESPACE, + CodeIncubatingAttributes.CODE_NAMESPACE, "GeneratedJava6TestClass"), - entry(SemanticAttributes.CODE_FUNCTION, "run"))), + entry( + CodeIncubatingAttributes.CODE_FUNCTION, + "run"))), span -> assertThat(span) .hasName("intercept") diff --git a/instrumentation/opentelemetry-instrumentation-api/javaagent/build.gradle.kts b/instrumentation/opentelemetry-instrumentation-api/javaagent/build.gradle.kts index 1eb6814658f8..9a993dfb6434 100644 --- a/instrumentation/opentelemetry-instrumentation-api/javaagent/build.gradle.kts +++ b/instrumentation/opentelemetry-instrumentation-api/javaagent/build.gradle.kts @@ -24,16 +24,19 @@ dependencies { compileOnly(project(":opentelemetry-api-shaded-for-instrumenting", configuration = "shadow")) compileOnly(project(":opentelemetry-instrumentation-api-shaded-for-instrumenting", configuration = "shadow")) - testImplementation(project(":instrumentation-api-semconv")) + testImplementation(project(":instrumentation-api-incubator")) testImplementation(project(":instrumentation:opentelemetry-instrumentation-api:testing")) testInstrumentation(project(":instrumentation:opentelemetry-instrumentation-api:testing")) } +// version 1.13.0 contains the old ServerSpan implementation that uses SERVER_KEY context key +val oldServerSpanVersion = "1.13.0-alpha" + testing { suites { val testOldServerSpan by registering(JvmTestSuite::class) { dependencies { - implementation("io.opentelemetry.instrumentation:opentelemetry-instrumentation-api-semconv") + implementation("io.opentelemetry.instrumentation:opentelemetry-instrumentation-api-semconv:$oldServerSpanVersion") implementation("io.opentelemetry.instrumentation:opentelemetry-instrumentation-api") implementation(project(":instrumentation:opentelemetry-instrumentation-api:testing")) } @@ -55,11 +58,8 @@ configurations.configureEach { if (name.startsWith("testOldServerSpan")) { resolutionStrategy { dependencySubstitution { - // version 1.13.0 contains the old ServerSpan implementation that uses SERVER_KEY context key - substitute(module("io.opentelemetry.instrumentation:opentelemetry-instrumentation-api-semconv")) - .using(module("io.opentelemetry.instrumentation:opentelemetry-instrumentation-api-semconv:1.13.0-alpha")) substitute(module("io.opentelemetry.instrumentation:opentelemetry-instrumentation-api")) - .using(module("io.opentelemetry.instrumentation:opentelemetry-instrumentation-api:1.13.0-alpha")) + .using(module("io.opentelemetry.instrumentation:opentelemetry-instrumentation-api:$oldServerSpanVersion")) } } } diff --git a/instrumentation/opentelemetry-instrumentation-api/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationapi/HttpRouteStateInstrumentation.java b/instrumentation/opentelemetry-instrumentation-api/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationapi/HttpRouteStateInstrumentation.java index e1438dc085ff..52f93afba7c5 100644 --- a/instrumentation/opentelemetry-instrumentation-api/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationapi/HttpRouteStateInstrumentation.java +++ b/instrumentation/opentelemetry-instrumentation-api/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationapi/HttpRouteStateInstrumentation.java @@ -8,10 +8,12 @@ import static net.bytebuddy.matcher.ElementMatchers.named; import static net.bytebuddy.matcher.ElementMatchers.takesArgument; +import application.io.opentelemetry.api.trace.Span; import application.io.opentelemetry.context.Context; import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; import io.opentelemetry.javaagent.instrumentation.opentelemetryapi.context.AgentContextStorage; +import io.opentelemetry.javaagent.instrumentation.opentelemetryapi.trace.Bridging; import net.bytebuddy.asm.Advice; import net.bytebuddy.description.type.TypeDescription; import net.bytebuddy.matcher.ElementMatcher; @@ -31,6 +33,11 @@ public void transform(TypeTransformer transformer) { .and(takesArgument(1, int.class)) .and(takesArgument(2, String.class)), this.getClass().getName() + "$UpdateAdvice"); + transformer.applyAdviceToMethod( + named("updateSpan") + .and(takesArgument(0, named("application.io.opentelemetry.context.Context"))) + .and(takesArgument(1, named("application.io.opentelemetry.api.trace.Span"))), + this.getClass().getName() + "$UpdateSpanAdvice"); } @SuppressWarnings("unused") @@ -54,4 +61,17 @@ public static void onEnter( agentRouteState.update(agentContext, updatedBySourceOrder, route); } } + + @SuppressWarnings("unused") + public static class UpdateSpanAdvice { + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter( + @Advice.Argument(0) Context applicationContext, @Advice.Argument(1) Span applicationSpan) { + + io.opentelemetry.context.Context agentContext = + AgentContextStorage.getAgentContext(applicationContext); + io.opentelemetry.instrumentation.api.internal.HttpRouteState.updateSpan( + agentContext, Bridging.toAgentOrNull(applicationSpan)); + } + } } diff --git a/instrumentation/opentelemetry-instrumentation-api/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationapi/InstrumentationApiInstrumentationModule.java b/instrumentation/opentelemetry-instrumentation-api/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationapi/InstrumentationApiInstrumentationModule.java index 7fa3004db58f..e6754cb818fe 100644 --- a/instrumentation/opentelemetry-instrumentation-api/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationapi/InstrumentationApiInstrumentationModule.java +++ b/instrumentation/opentelemetry-instrumentation-api/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationapi/InstrumentationApiInstrumentationModule.java @@ -11,11 +11,13 @@ 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.internal.ExperimentalInstrumentationModule; import java.util.List; import net.bytebuddy.matcher.ElementMatcher; @AutoService(InstrumentationModule.class) -public class InstrumentationApiInstrumentationModule extends InstrumentationModule { +public class InstrumentationApiInstrumentationModule extends InstrumentationModule + implements ExperimentalInstrumentationModule { public InstrumentationApiInstrumentationModule() { super("opentelemetry-instrumentation-api"); @@ -30,4 +32,9 @@ public ElementMatcher.Junction classLoaderMatcher() { public List typeInstrumentations() { return asList(new HttpRouteStateInstrumentation(), new SpanKeyInstrumentation()); } + + @Override + public String getModuleGroup() { + return "opentelemetry-api-bridge"; + } } diff --git a/instrumentation/opentelemetry-instrumentation-api/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/instrumentationapi/ContextBridgeTest.java b/instrumentation/opentelemetry-instrumentation-api/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/instrumentationapi/ContextBridgeTest.java index b3de3e53a8ad..cd5078cd2211 100644 --- a/instrumentation/opentelemetry-instrumentation-api/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/instrumentationapi/ContextBridgeTest.java +++ b/instrumentation/opentelemetry-instrumentation-api/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/instrumentationapi/ContextBridgeTest.java @@ -12,13 +12,14 @@ import io.opentelemetry.api.trace.SpanKind; import io.opentelemetry.context.Context; import io.opentelemetry.instrumentation.api.instrumenter.LocalRootSpan; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteHolder; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteSource; import io.opentelemetry.instrumentation.api.internal.SpanKey; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRoute; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRouteSource; import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; import io.opentelemetry.javaagent.instrumentation.testing.AgentSpanTesting; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.opentelemetry.semconv.ErrorAttributes; +import io.opentelemetry.semconv.HttpAttributes; import java.util.Arrays; import java.util.List; import org.junit.jupiter.api.Test; @@ -80,8 +81,8 @@ void testHttpRouteHolder_SameSourceAsServerInstrumentationDoesNotOverrideRoute() AgentSpanTesting.runWithHttpServerSpan( "server", () -> - HttpRouteHolder.updateHttpRoute( - Context.current(), HttpRouteSource.SERVLET, "/test/controller/:id")); + HttpServerRoute.update( + Context.current(), HttpServerRouteSource.SERVER, "/test/controller/:id")); testing.waitAndAssertTraces( trace -> @@ -91,8 +92,9 @@ void testHttpRouteHolder_SameSourceAsServerInstrumentationDoesNotOverrideRoute() .hasKind(SpanKind.SERVER) .hasNoParent() .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.HTTP_METHOD, "GET"), - equalTo(SemanticAttributes.HTTP_ROUTE, "/test/server/*")))); + equalTo(HttpAttributes.HTTP_REQUEST_METHOD, "GET"), + equalTo(HttpAttributes.HTTP_ROUTE, "/test/server/*"), + equalTo(ErrorAttributes.ERROR_TYPE, "_OTHER")))); } @Test @@ -100,8 +102,8 @@ void testHttpRouteHolder_SourceWithHigherOrderValueOverridesRoute() { AgentSpanTesting.runWithHttpServerSpan( "server", () -> - HttpRouteHolder.updateHttpRoute( - Context.current(), HttpRouteSource.CONTROLLER, "/test/controller/:id")); + HttpServerRoute.update( + Context.current(), HttpServerRouteSource.CONTROLLER, "/test/controller/:id")); testing.waitAndAssertTraces( trace -> @@ -111,7 +113,8 @@ void testHttpRouteHolder_SourceWithHigherOrderValueOverridesRoute() { .hasKind(SpanKind.SERVER) .hasNoParent() .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.HTTP_METHOD, "GET"), - equalTo(SemanticAttributes.HTTP_ROUTE, "/test/controller/:id")))); + equalTo(HttpAttributes.HTTP_REQUEST_METHOD, "GET"), + equalTo(HttpAttributes.HTTP_ROUTE, "/test/controller/:id"), + equalTo(ErrorAttributes.ERROR_TYPE, "_OTHER")))); } } diff --git a/instrumentation/opentelemetry-instrumentation-api/testing/src/main/java/io/opentelemetry/javaagent/instrumentation/testing/AgentSpanTestingInstrumenter.java b/instrumentation/opentelemetry-instrumentation-api/testing/src/main/java/io/opentelemetry/javaagent/instrumentation/testing/AgentSpanTestingInstrumenter.java index 39a95e5afed7..188f736a929f 100644 --- a/instrumentation/opentelemetry-instrumentation-api/testing/src/main/java/io/opentelemetry/javaagent/instrumentation/testing/AgentSpanTestingInstrumenter.java +++ b/instrumentation/opentelemetry-instrumentation-api/testing/src/main/java/io/opentelemetry/javaagent/instrumentation/testing/AgentSpanTestingInstrumenter.java @@ -11,10 +11,10 @@ import io.opentelemetry.context.ContextKey; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; import io.opentelemetry.instrumentation.api.instrumenter.SpanKindExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteHolder; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteSource; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpServerAttributesExtractor; import io.opentelemetry.instrumentation.api.internal.SpanKey; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerAttributesExtractor; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRoute; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRouteSource; public final class AgentSpanTestingInstrumenter { @@ -30,16 +30,15 @@ public final class AgentSpanTestingInstrumenter { private static final Instrumenter HTTP_SERVER_INSTRUMENTER = Instrumenter.builder(GlobalOpenTelemetry.get(), "test", request -> request) .addAttributesExtractor( - HttpServerAttributesExtractor.create( - MockHttpServerAttributesGetter.INSTANCE, MockNetServerAttributesGetter.INSTANCE)) - .addContextCustomizer(HttpRouteHolder.create(MockHttpServerAttributesGetter.INSTANCE)) + HttpServerAttributesExtractor.create(MockHttpServerAttributesGetter.INSTANCE)) + .addContextCustomizer(HttpServerRoute.create(MockHttpServerAttributesGetter.INSTANCE)) .addContextCustomizer( (context, request, startAttributes) -> context.with(REQUEST_CONTEXT_KEY, request)) .buildInstrumenter(SpanKindExtractor.alwaysServer()); public static Context startHttpServerSpan(String name) { Context context = HTTP_SERVER_INSTRUMENTER.start(Context.current(), name); - HttpRouteHolder.updateHttpRoute(context, HttpRouteSource.SERVLET, "/test/server/*"); + HttpServerRoute.update(context, HttpServerRouteSource.SERVER, "/test/server/*"); return context; } diff --git a/instrumentation/opentelemetry-instrumentation-api/testing/src/main/java/io/opentelemetry/javaagent/instrumentation/testing/MockHttpServerAttributesGetter.java b/instrumentation/opentelemetry-instrumentation-api/testing/src/main/java/io/opentelemetry/javaagent/instrumentation/testing/MockHttpServerAttributesGetter.java index 4718afd97d8d..efdd335aa431 100644 --- a/instrumentation/opentelemetry-instrumentation-api/testing/src/main/java/io/opentelemetry/javaagent/instrumentation/testing/MockHttpServerAttributesGetter.java +++ b/instrumentation/opentelemetry-instrumentation-api/testing/src/main/java/io/opentelemetry/javaagent/instrumentation/testing/MockHttpServerAttributesGetter.java @@ -7,7 +7,7 @@ import static java.util.Collections.emptyList; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpServerAttributesGetter; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerAttributesGetter; import java.util.List; import javax.annotation.Nullable; diff --git a/instrumentation/opentelemetry-instrumentation-api/testing/src/main/java/io/opentelemetry/javaagent/instrumentation/testing/MockNetServerAttributesGetter.java b/instrumentation/opentelemetry-instrumentation-api/testing/src/main/java/io/opentelemetry/javaagent/instrumentation/testing/MockNetServerAttributesGetter.java deleted file mode 100644 index 7dec1b276123..000000000000 --- a/instrumentation/opentelemetry-instrumentation-api/testing/src/main/java/io/opentelemetry/javaagent/instrumentation/testing/MockNetServerAttributesGetter.java +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.javaagent.instrumentation.testing; - -import io.opentelemetry.instrumentation.api.instrumenter.net.NetServerAttributesGetter; -import javax.annotation.Nullable; - -// only needed so that HttpServerAttributesExtractor can be added to the HTTP server instrumenter -enum MockNetServerAttributesGetter implements NetServerAttributesGetter { - INSTANCE; - - @Nullable - @Override - public String getServerAddress(String s) { - return null; - } - - @Nullable - @Override - public Integer getServerPort(String s) { - return null; - } -} diff --git a/instrumentation/oracle-ucp-11.2/javaagent/build.gradle.kts b/instrumentation/oracle-ucp-11.2/javaagent/build.gradle.kts index 2a887cc34d88..136a8c53bf76 100644 --- a/instrumentation/oracle-ucp-11.2/javaagent/build.gradle.kts +++ b/instrumentation/oracle-ucp-11.2/javaagent/build.gradle.kts @@ -13,10 +13,15 @@ muzzle { dependencies { library("com.oracle.database.jdbc:ucp:11.2.0.4") + library("com.oracle.database.jdbc:ojdbc8:12.2.0.1") implementation(project(":instrumentation:oracle-ucp-11.2:library")) testImplementation(project(":instrumentation:oracle-ucp-11.2:testing")) +} - latestDepTestLibrary("com.oracle.database.jdbc:ucp:21.9.0.0") +tasks { + test { + usesService(gradle.sharedServices.registrations["testcontainersBuildService"].service) + } } diff --git a/instrumentation/oracle-ucp-11.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/oracleucp/v11_2/UniversalConnectionPoolInstrumentation.java b/instrumentation/oracle-ucp-11.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/oracleucp/v11_2/UniversalConnectionPoolInstrumentation.java index 2f0bd50f4b11..e048a7badc41 100644 --- a/instrumentation/oracle-ucp-11.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/oracleucp/v11_2/UniversalConnectionPoolInstrumentation.java +++ b/instrumentation/oracle-ucp-11.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/oracleucp/v11_2/UniversalConnectionPoolInstrumentation.java @@ -8,10 +8,11 @@ import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.hasClassesNamed; import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.implementsInterface; import static io.opentelemetry.javaagent.instrumentation.oracleucp.v11_2.OracleUcpSingletons.telemetry; +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.bootstrap.CallDepth; import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; import net.bytebuddy.asm.Advice; @@ -34,9 +35,7 @@ public ElementMatcher typeMatcher() { @Override public void transform(TypeTransformer transformer) { transformer.applyAdviceToMethod( - named("start") - .and(takesArguments(0).or(takesArguments(1).and(takesArgument(0, boolean.class)))), - this.getClass().getName() + "$StartAdvice"); + named("start").and(isPublic()), this.getClass().getName() + "$StartAdvice"); transformer.applyAdviceToMethod( named("stop").and(takesArguments(0)), this.getClass().getName() + "$StopAdvice"); } @@ -53,8 +52,20 @@ public static void onExit(@Advice.This UniversalConnectionPool connectionPool) { @SuppressWarnings("unused") public static class StopAdvice { + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter(@Advice.Local("otelCallDepth") CallDepth callDepth) { + callDepth = CallDepth.forClass(UniversalConnectionPool.class); + callDepth.getAndIncrement(); + } + @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class) - public static void onExit(@Advice.This UniversalConnectionPool connectionPool) { + public static void onExit( + @Advice.This UniversalConnectionPool connectionPool, + @Advice.Local("otelCallDepth") CallDepth callDepth) { + if (callDepth == null || callDepth.decrementAndGet() > 0) { + return; + } + telemetry().unregisterMetrics(connectionPool); } } diff --git a/instrumentation/oracle-ucp-11.2/library/build.gradle.kts b/instrumentation/oracle-ucp-11.2/library/build.gradle.kts index a2fa7819af1c..937adaaf315b 100644 --- a/instrumentation/oracle-ucp-11.2/library/build.gradle.kts +++ b/instrumentation/oracle-ucp-11.2/library/build.gradle.kts @@ -5,8 +5,13 @@ plugins { dependencies { library("com.oracle.database.jdbc:ucp:11.2.0.4") + library("com.oracle.database.jdbc:ojdbc8:12.2.0.1") testImplementation(project(":instrumentation:oracle-ucp-11.2:testing")) +} - latestDepTestLibrary("com.oracle.database.jdbc:ucp:21.9.0.0") +tasks { + test { + usesService(gradle.sharedServices.registrations["testcontainersBuildService"].service) + } } diff --git a/instrumentation/oracle-ucp-11.2/library/src/main/java/io/opentelemetry/instrumentation/oracleucp/v11_2/ConnectionPoolMetrics.java b/instrumentation/oracle-ucp-11.2/library/src/main/java/io/opentelemetry/instrumentation/oracleucp/v11_2/ConnectionPoolMetrics.java index d09e01f75f1b..860fc246e5e7 100644 --- a/instrumentation/oracle-ucp-11.2/library/src/main/java/io/opentelemetry/instrumentation/oracleucp/v11_2/ConnectionPoolMetrics.java +++ b/instrumentation/oracle-ucp-11.2/library/src/main/java/io/opentelemetry/instrumentation/oracleucp/v11_2/ConnectionPoolMetrics.java @@ -9,7 +9,7 @@ import io.opentelemetry.api.common.Attributes; import io.opentelemetry.api.metrics.BatchCallback; import io.opentelemetry.api.metrics.ObservableLongMeasurement; -import io.opentelemetry.instrumentation.api.metrics.db.DbConnectionPoolMetrics; +import io.opentelemetry.instrumentation.api.incubator.semconv.db.DbConnectionPoolMetrics; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import oracle.ucp.UniversalConnectionPool; diff --git a/instrumentation/oracle-ucp-11.2/testing/build.gradle.kts b/instrumentation/oracle-ucp-11.2/testing/build.gradle.kts index 2326f1bd8220..af0b5a74a3c6 100644 --- a/instrumentation/oracle-ucp-11.2/testing/build.gradle.kts +++ b/instrumentation/oracle-ucp-11.2/testing/build.gradle.kts @@ -4,8 +4,7 @@ plugins { dependencies { api(project(":testing-common")) - api("org.mockito:mockito-core") - api("org.mockito:mockito-junit-jupiter") + implementation("org.testcontainers:oracle-free") compileOnly("com.oracle.database.jdbc:ucp:11.2.0.4") } diff --git a/instrumentation/oracle-ucp-11.2/testing/src/main/java/io/opentelemetry/instrumentation/oracleucp/AbstractOracleUcpInstrumentationTest.java b/instrumentation/oracle-ucp-11.2/testing/src/main/java/io/opentelemetry/instrumentation/oracleucp/AbstractOracleUcpInstrumentationTest.java index 639240214874..548c8eb17ecd 100644 --- a/instrumentation/oracle-ucp-11.2/testing/src/main/java/io/opentelemetry/instrumentation/oracleucp/AbstractOracleUcpInstrumentationTest.java +++ b/instrumentation/oracle-ucp-11.2/testing/src/main/java/io/opentelemetry/instrumentation/oracleucp/AbstractOracleUcpInstrumentationTest.java @@ -9,9 +9,8 @@ import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; import io.opentelemetry.instrumentation.testing.junit.db.DbConnectionPoolMetricsAssertions; -import io.opentelemetry.instrumentation.testing.junit.db.MockDriver; import java.sql.Connection; -import java.sql.SQLException; +import java.time.Duration; import java.util.Arrays; import java.util.HashSet; import java.util.Set; @@ -19,15 +18,22 @@ import oracle.ucp.admin.UniversalConnectionPoolManagerImpl; import oracle.ucp.jdbc.PoolDataSource; import oracle.ucp.jdbc.PoolDataSourceFactory; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Assumptions; import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; -import org.mockito.junit.jupiter.MockitoExtension; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testcontainers.containers.output.Slf4jLogConsumer; +import org.testcontainers.oracle.OracleContainer; -@ExtendWith(MockitoExtension.class) public abstract class AbstractOracleUcpInstrumentationTest { + private static final Logger logger = + LoggerFactory.getLogger(AbstractOracleUcpInstrumentationTest.class); + private static final String INSTRUMENTATION_NAME = "io.opentelemetry.orcale-ucp-11.2"; + private static OracleContainer oracle; protected abstract InstrumentationExtension testing(); @@ -36,17 +42,42 @@ public abstract class AbstractOracleUcpInstrumentationTest { protected abstract void shutdown(PoolDataSource connectionPool) throws Exception; @BeforeAll - static void setUpMocks() throws SQLException { - MockDriver.register(); + static void setUp() { + // This docker image does not work on arm mac. To run this test on arm mac read + // https://blog.jdriven.com/2022/07/running-oracle-xe-with-testcontainers-on-apple-silicon/ + // install colima with brew install colima + // colima start --arch x86_64 --memory 4 + // export TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE=/var/run/docker.sock + // export DOCKER_HOST="unix://${HOME}/.colima/docker.sock" + String dockerHost = System.getenv("DOCKER_HOST"); + if (!"aarch64".equals(System.getProperty("os.arch")) + || (dockerHost != null && dockerHost.contains("colima"))) { + oracle = + new OracleContainer("gvenzl/oracle-free:23.4-slim-faststart") + .withLogConsumer(new Slf4jLogConsumer(logger)) + .withStartupTimeout(Duration.ofMinutes(2)); + oracle.start(); + } + } + + @AfterAll + static void cleanUp() { + if (oracle != null) { + oracle.stop(); + } } @ParameterizedTest @ValueSource(booleans = {true, false}) void shouldReportMetrics(boolean setExplicitPoolName) throws Exception { + Assumptions.assumeTrue(oracle != null); + // given PoolDataSource connectionPool = PoolDataSourceFactory.getPoolDataSource(); - connectionPool.setConnectionFactoryClassName(MockDriver.class.getName()); - connectionPool.setURL("jdbc:mock:testDatabase"); + connectionPool.setConnectionFactoryClassName("oracle.jdbc.pool.OracleDataSource"); + connectionPool.setURL(oracle.getJdbcUrl()); + connectionPool.setUser(oracle.getUsername()); + connectionPool.setPassword(oracle.getPassword()); if (setExplicitPoolName) { connectionPool.setConnectionPoolName("testPool"); } diff --git a/instrumentation/oshi/README.md b/instrumentation/oshi/README.md new file mode 100644 index 000000000000..165f43d35236 --- /dev/null +++ b/instrumentation/oshi/README.md @@ -0,0 +1,9 @@ +# Settings for the OSHI instrumentation + +| System property | Type | Default | Description | +|-----------------------------------------------------------| ------- | ------- |--------------------------| +| `otel.instrumentation.oshi.experimental-metrics.enabled` | Boolean | `false` | Enable the OSHI metrics. | + +# Using OSHI with OpenTelemetry Java agent + +Download oshi-core jar from https://search.maven.org/search?q=g:com.github.oshi%20AND%20a:oshi-core and place it on the class path. OpenTelemetry Java agent uses system class loader to load classes from the oshi-core jar that are used for the metrics. diff --git a/instrumentation/oshi/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/oshi/MetricsRegistration.java b/instrumentation/oshi/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/oshi/MetricsRegistration.java index cf03f435dd30..742e20909781 100644 --- a/instrumentation/oshi/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/oshi/MetricsRegistration.java +++ b/instrumentation/oshi/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/oshi/MetricsRegistration.java @@ -8,7 +8,9 @@ import io.opentelemetry.api.GlobalOpenTelemetry; import io.opentelemetry.instrumentation.oshi.ProcessMetrics; import io.opentelemetry.instrumentation.oshi.SystemMetrics; -import io.opentelemetry.javaagent.bootstrap.internal.InstrumentationConfig; +import io.opentelemetry.javaagent.bootstrap.internal.AgentInstrumentationConfig; +import java.util.ArrayList; +import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; public final class MetricsRegistration { @@ -17,13 +19,28 @@ public final class MetricsRegistration { public static void register() { if (registered.compareAndSet(false, true)) { - SystemMetrics.registerObservers(GlobalOpenTelemetry.get()); + List observables = new ArrayList<>(); + observables.addAll(SystemMetrics.registerObservers(GlobalOpenTelemetry.get())); // ProcessMetrics don't follow the spec - if (InstrumentationConfig.get() + if (AgentInstrumentationConfig.get() .getBoolean("otel.instrumentation.oshi.experimental-metrics.enabled", false)) { - ProcessMetrics.registerObservers(GlobalOpenTelemetry.get()); + observables.addAll(ProcessMetrics.registerObservers(GlobalOpenTelemetry.get())); } + Thread cleanupTelemetry = new Thread(() -> MetricsRegistration.closeObservables(observables)); + Runtime.getRuntime().addShutdownHook(cleanupTelemetry); + } + } + + private static void closeObservables(List observables) { + observables.forEach(MetricsRegistration::closeObservable); + } + + private static void closeObservable(AutoCloseable observable) { + try { + observable.close(); + } catch (Exception e) { + throw new IllegalStateException("Error occurred closing observable", e); } } diff --git a/instrumentation/oshi/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/oshi/OshiMetricsInstaller.java b/instrumentation/oshi/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/oshi/OshiMetricsInstaller.java index 0496c39e8122..5a5b04443d2e 100644 --- a/instrumentation/oshi/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/oshi/OshiMetricsInstaller.java +++ b/instrumentation/oshi/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/oshi/OshiMetricsInstaller.java @@ -8,6 +8,7 @@ import com.google.auto.service.AutoService; import io.opentelemetry.javaagent.extension.AgentListener; import io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdk; +import io.opentelemetry.sdk.autoconfigure.internal.AutoConfigureUtil; import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; import java.lang.reflect.Method; @@ -20,7 +21,7 @@ public class OshiMetricsInstaller implements AgentListener { @Override public void afterAgent(AutoConfiguredOpenTelemetrySdk autoConfiguredSdk) { - ConfigProperties config = autoConfiguredSdk.getConfig(); + ConfigProperties config = AutoConfigureUtil.getConfig(autoConfiguredSdk); boolean defaultEnabled = config.getBoolean("otel.instrumentation.common.default-enabled", true); if (!config.getBoolean("otel.instrumentation.oshi.enabled", defaultEnabled)) { diff --git a/instrumentation/oshi/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/oshi/SystemInfoInstrumentation.java b/instrumentation/oshi/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/oshi/SystemInfoInstrumentation.java index c7d8f374c1fe..a380338f7f8a 100644 --- a/instrumentation/oshi/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/oshi/SystemInfoInstrumentation.java +++ b/instrumentation/oshi/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/oshi/SystemInfoInstrumentation.java @@ -9,6 +9,7 @@ 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 io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; @@ -28,7 +29,7 @@ public void transform(TypeTransformer transformer) { isMethod() .and(isPublic()) .and(isStatic()) - .and(named("getCurrentPlatformEnum").or(named("getCurrentPlatform"))), + .and(namedOneOf("getCurrentPlatformEnum", "getCurrentPlatform")), this.getClass().getName() + "$GetCurrentPlatformEnumAdvice"); } diff --git a/instrumentation/oshi/library/build.gradle.kts b/instrumentation/oshi/library/build.gradle.kts index 23a3b0382096..1535c97d8376 100644 --- a/instrumentation/oshi/library/build.gradle.kts +++ b/instrumentation/oshi/library/build.gradle.kts @@ -1,9 +1,13 @@ plugins { id("otel.library-instrumentation") + id("com.google.osdetector") } +// 5.5.0 is the first version that works on arm mac +val oshiVersion = if (osdetector.os == "osx" && osdetector.arch == "aarch_64") "5.5.0" else "5.3.1" + dependencies { - library("com.github.oshi:oshi-core:5.3.1") + library("com.github.oshi:oshi-core:$oshiVersion") testImplementation(project(":instrumentation:oshi:testing")) } diff --git a/instrumentation/oshi/library/src/main/java/io/opentelemetry/instrumentation/oshi/ProcessMetrics.java b/instrumentation/oshi/library/src/main/java/io/opentelemetry/instrumentation/oshi/ProcessMetrics.java index be364b3ac18f..a4f1f79cbe54 100644 --- a/instrumentation/oshi/library/src/main/java/io/opentelemetry/instrumentation/oshi/ProcessMetrics.java +++ b/instrumentation/oshi/library/src/main/java/io/opentelemetry/instrumentation/oshi/ProcessMetrics.java @@ -9,6 +9,8 @@ import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.common.Attributes; import io.opentelemetry.api.metrics.Meter; +import java.util.ArrayList; +import java.util.List; import oshi.SystemInfo; import oshi.software.os.OSProcess; import oshi.software.os.OperatingSystem; @@ -20,33 +22,36 @@ public class ProcessMetrics { private ProcessMetrics() {} /** Register observers for java runtime metrics. */ - public static void registerObservers(OpenTelemetry openTelemetry) { + public static List registerObservers(OpenTelemetry openTelemetry) { Meter meter = openTelemetry.getMeterProvider().get("io.opentelemetry.oshi"); SystemInfo systemInfo = new SystemInfo(); OperatingSystem osInfo = systemInfo.getOperatingSystem(); OSProcess processInfo = osInfo.getProcess(osInfo.getProcessId()); + List observables = new ArrayList<>(); + observables.add( + meter + .upDownCounterBuilder("runtime.java.memory") + .setDescription("Runtime Java memory") + .setUnit("By") + .buildWithCallback( + r -> { + processInfo.updateAttributes(); + r.record(processInfo.getResidentSetSize(), Attributes.of(TYPE_KEY, "rss")); + r.record(processInfo.getVirtualSize(), Attributes.of(TYPE_KEY, "vms")); + })); - meter - .upDownCounterBuilder("runtime.java.memory") - .setDescription("Runtime Java memory") - .setUnit("By") - .buildWithCallback( - r -> { - processInfo.updateAttributes(); - r.record(processInfo.getResidentSetSize(), Attributes.of(TYPE_KEY, "rss")); - r.record(processInfo.getVirtualSize(), Attributes.of(TYPE_KEY, "vms")); - }); - - meter - .gaugeBuilder("runtime.java.cpu_time") - .setDescription("Runtime Java CPU time") - .setUnit("ms") - .ofLongs() - .buildWithCallback( - r -> { - processInfo.updateAttributes(); - r.record(processInfo.getUserTime(), Attributes.of(TYPE_KEY, "user")); - r.record(processInfo.getKernelTime(), Attributes.of(TYPE_KEY, "system")); - }); + observables.add( + meter + .gaugeBuilder("runtime.java.cpu_time") + .setDescription("Runtime Java CPU time") + .setUnit("ms") + .ofLongs() + .buildWithCallback( + r -> { + processInfo.updateAttributes(); + r.record(processInfo.getUserTime(), Attributes.of(TYPE_KEY, "user")); + r.record(processInfo.getKernelTime(), Attributes.of(TYPE_KEY, "system")); + })); + return observables; } } diff --git a/instrumentation/oshi/library/src/main/java/io/opentelemetry/instrumentation/oshi/SystemMetrics.java b/instrumentation/oshi/library/src/main/java/io/opentelemetry/instrumentation/oshi/SystemMetrics.java index ccdcf17c62a2..960d6c0bccdc 100644 --- a/instrumentation/oshi/library/src/main/java/io/opentelemetry/instrumentation/oshi/SystemMetrics.java +++ b/instrumentation/oshi/library/src/main/java/io/opentelemetry/instrumentation/oshi/SystemMetrics.java @@ -9,6 +9,8 @@ import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.common.Attributes; import io.opentelemetry.api.metrics.Meter; +import java.util.ArrayList; +import java.util.List; import oshi.SystemInfo; import oshi.hardware.GlobalMemory; import oshi.hardware.HWDiskStore; @@ -28,111 +30,121 @@ public class SystemMetrics { private SystemMetrics() {} /** Register observers for system metrics. */ - public static void registerObservers(OpenTelemetry openTelemetry) { + public static List registerObservers(OpenTelemetry openTelemetry) { Meter meter = openTelemetry.getMeterProvider().get("io.opentelemetry.oshi"); SystemInfo systemInfo = new SystemInfo(); HardwareAbstractionLayer hal = systemInfo.getHardware(); + List observables = new ArrayList<>(); - meter - .upDownCounterBuilder("system.memory.usage") - .setDescription("System memory usage") - .setUnit("By") - .buildWithCallback( - r -> { - GlobalMemory mem = hal.getMemory(); - r.record(mem.getTotal() - mem.getAvailable(), ATTRIBUTES_USED); - r.record(mem.getAvailable(), ATTRIBUTES_FREE); - }); + observables.add( + meter + .upDownCounterBuilder("system.memory.usage") + .setDescription("System memory usage") + .setUnit("By") + .buildWithCallback( + r -> { + GlobalMemory mem = hal.getMemory(); + r.record(mem.getTotal() - mem.getAvailable(), ATTRIBUTES_USED); + r.record(mem.getAvailable(), ATTRIBUTES_FREE); + })); - meter - .gaugeBuilder("system.memory.utilization") - .setDescription("System memory utilization") - .setUnit("1") - .buildWithCallback( - r -> { - GlobalMemory mem = hal.getMemory(); - r.record( - ((double) (mem.getTotal() - mem.getAvailable())) / mem.getTotal(), - ATTRIBUTES_USED); - r.record(((double) mem.getAvailable()) / mem.getTotal(), ATTRIBUTES_FREE); - }); + observables.add( + meter + .gaugeBuilder("system.memory.utilization") + .setDescription("System memory utilization") + .setUnit("1") + .buildWithCallback( + r -> { + GlobalMemory mem = hal.getMemory(); + r.record( + ((double) (mem.getTotal() - mem.getAvailable())) / mem.getTotal(), + ATTRIBUTES_USED); + r.record(((double) mem.getAvailable()) / mem.getTotal(), ATTRIBUTES_FREE); + })); - meter - .counterBuilder("system.network.io") - .setDescription("System network IO") - .setUnit("By") - .buildWithCallback( - r -> { - for (NetworkIF networkIf : hal.getNetworkIFs()) { - networkIf.updateAttributes(); - long recv = networkIf.getBytesRecv(); - long sent = networkIf.getBytesSent(); - String device = networkIf.getName(); - r.record(recv, Attributes.of(DEVICE_KEY, device, DIRECTION_KEY, "receive")); - r.record(sent, Attributes.of(DEVICE_KEY, device, DIRECTION_KEY, "transmit")); - } - }); + observables.add( + meter + .counterBuilder("system.network.io") + .setDescription("System network IO") + .setUnit("By") + .buildWithCallback( + r -> { + for (NetworkIF networkIf : hal.getNetworkIFs()) { + networkIf.updateAttributes(); + long recv = networkIf.getBytesRecv(); + long sent = networkIf.getBytesSent(); + String device = networkIf.getName(); + r.record(recv, Attributes.of(DEVICE_KEY, device, DIRECTION_KEY, "receive")); + r.record(sent, Attributes.of(DEVICE_KEY, device, DIRECTION_KEY, "transmit")); + } + })); - meter - .counterBuilder("system.network.packets") - .setDescription("System network packets") - .setUnit("{packets}") - .buildWithCallback( - r -> { - for (NetworkIF networkIf : hal.getNetworkIFs()) { - networkIf.updateAttributes(); - long recv = networkIf.getPacketsRecv(); - long sent = networkIf.getPacketsSent(); - String device = networkIf.getName(); - r.record(recv, Attributes.of(DEVICE_KEY, device, DIRECTION_KEY, "receive")); - r.record(sent, Attributes.of(DEVICE_KEY, device, DIRECTION_KEY, "transmit")); - } - }); + observables.add( + meter + .counterBuilder("system.network.packets") + .setDescription("System network packets") + .setUnit("{packets}") + .buildWithCallback( + r -> { + for (NetworkIF networkIf : hal.getNetworkIFs()) { + networkIf.updateAttributes(); + long recv = networkIf.getPacketsRecv(); + long sent = networkIf.getPacketsSent(); + String device = networkIf.getName(); + r.record(recv, Attributes.of(DEVICE_KEY, device, DIRECTION_KEY, "receive")); + r.record(sent, Attributes.of(DEVICE_KEY, device, DIRECTION_KEY, "transmit")); + } + })); - meter - .counterBuilder("system.network.errors") - .setDescription("System network errors") - .setUnit("{errors}") - .buildWithCallback( - r -> { - for (NetworkIF networkIf : hal.getNetworkIFs()) { - networkIf.updateAttributes(); - long recv = networkIf.getInErrors(); - long sent = networkIf.getOutErrors(); - String device = networkIf.getName(); - r.record(recv, Attributes.of(DEVICE_KEY, device, DIRECTION_KEY, "receive")); - r.record(sent, Attributes.of(DEVICE_KEY, device, DIRECTION_KEY, "transmit")); - } - }); + observables.add( + meter + .counterBuilder("system.network.errors") + .setDescription("System network errors") + .setUnit("{errors}") + .buildWithCallback( + r -> { + for (NetworkIF networkIf : hal.getNetworkIFs()) { + networkIf.updateAttributes(); + long recv = networkIf.getInErrors(); + long sent = networkIf.getOutErrors(); + String device = networkIf.getName(); + r.record(recv, Attributes.of(DEVICE_KEY, device, DIRECTION_KEY, "receive")); + r.record(sent, Attributes.of(DEVICE_KEY, device, DIRECTION_KEY, "transmit")); + } + })); - meter - .counterBuilder("system.disk.io") - .setDescription("System disk IO") - .setUnit("By") - .buildWithCallback( - r -> { - for (HWDiskStore diskStore : hal.getDiskStores()) { - long read = diskStore.getReadBytes(); - long write = diskStore.getWriteBytes(); - String device = diskStore.getName(); - r.record(read, Attributes.of(DEVICE_KEY, device, DIRECTION_KEY, "read")); - r.record(write, Attributes.of(DEVICE_KEY, device, DIRECTION_KEY, "write")); - } - }); + observables.add( + meter + .counterBuilder("system.disk.io") + .setDescription("System disk IO") + .setUnit("By") + .buildWithCallback( + r -> { + for (HWDiskStore diskStore : hal.getDiskStores()) { + long read = diskStore.getReadBytes(); + long write = diskStore.getWriteBytes(); + String device = diskStore.getName(); + r.record(read, Attributes.of(DEVICE_KEY, device, DIRECTION_KEY, "read")); + r.record(write, Attributes.of(DEVICE_KEY, device, DIRECTION_KEY, "write")); + } + })); - meter - .counterBuilder("system.disk.operations") - .setDescription("System disk operations") - .setUnit("{operations}") - .buildWithCallback( - r -> { - for (HWDiskStore diskStore : hal.getDiskStores()) { - long read = diskStore.getReads(); - long write = diskStore.getWrites(); - String device = diskStore.getName(); - r.record(read, Attributes.of(DEVICE_KEY, device, DIRECTION_KEY, "read")); - r.record(write, Attributes.of(DEVICE_KEY, device, DIRECTION_KEY, "write")); - } - }); + observables.add( + meter + .counterBuilder("system.disk.operations") + .setDescription("System disk operations") + .setUnit("{operations}") + .buildWithCallback( + r -> { + for (HWDiskStore diskStore : hal.getDiskStores()) { + long read = diskStore.getReads(); + long write = diskStore.getWrites(); + String device = diskStore.getName(); + r.record(read, Attributes.of(DEVICE_KEY, device, DIRECTION_KEY, "read")); + r.record(write, Attributes.of(DEVICE_KEY, device, DIRECTION_KEY, "write")); + } + })); + + return observables; } } diff --git a/instrumentation/oshi/library/src/test/java/io/opentelemetry/instrumentation/oshi/ProcessMetricsTest.java b/instrumentation/oshi/library/src/test/java/io/opentelemetry/instrumentation/oshi/ProcessMetricsTest.java index e61ca7e02556..f2b3dc14bdb0 100644 --- a/instrumentation/oshi/library/src/test/java/io/opentelemetry/instrumentation/oshi/ProcessMetricsTest.java +++ b/instrumentation/oshi/library/src/test/java/io/opentelemetry/instrumentation/oshi/ProcessMetricsTest.java @@ -5,9 +5,15 @@ package io.opentelemetry.instrumentation.oshi; +import static org.assertj.core.api.Assertions.assertThat; + import io.opentelemetry.api.GlobalOpenTelemetry; import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; import io.opentelemetry.instrumentation.testing.junit.LibraryInstrumentationExtension; +import java.util.List; +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; class ProcessMetricsTest extends AbstractProcessMetricsTest { @@ -15,13 +21,34 @@ class ProcessMetricsTest extends AbstractProcessMetricsTest { @RegisterExtension public static final InstrumentationExtension testing = LibraryInstrumentationExtension.create(); - @Override - protected void registerMetrics() { - ProcessMetrics.registerObservers(GlobalOpenTelemetry.get()); + private static List observables; + + @BeforeAll + static void setUp() { + observables = ProcessMetrics.registerObservers(GlobalOpenTelemetry.get()); + } + + @AfterAll + static void tearDown() { + for (AutoCloseable observable : observables) { + try { + observable.close(); + } catch (Exception e) { + // ignore + } + } } + @Override + protected void registerMetrics() {} + @Override protected InstrumentationExtension testing() { return testing; } + + @Test + void verifyObservablesAreNotEmpty() { + assertThat(observables).as("List of observables").isNotEmpty(); + } } diff --git a/instrumentation/oshi/library/src/test/java/io/opentelemetry/instrumentation/oshi/SystemMetricsTest.java b/instrumentation/oshi/library/src/test/java/io/opentelemetry/instrumentation/oshi/SystemMetricsTest.java index c1085d129a2c..958378004852 100644 --- a/instrumentation/oshi/library/src/test/java/io/opentelemetry/instrumentation/oshi/SystemMetricsTest.java +++ b/instrumentation/oshi/library/src/test/java/io/opentelemetry/instrumentation/oshi/SystemMetricsTest.java @@ -5,9 +5,15 @@ package io.opentelemetry.instrumentation.oshi; +import static org.assertj.core.api.Assertions.assertThat; + import io.opentelemetry.api.GlobalOpenTelemetry; import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; import io.opentelemetry.instrumentation.testing.junit.LibraryInstrumentationExtension; +import java.util.List; +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; class SystemMetricsTest extends AbstractSystemMetricsTest { @@ -15,13 +21,34 @@ class SystemMetricsTest extends AbstractSystemMetricsTest { @RegisterExtension public static final InstrumentationExtension testing = LibraryInstrumentationExtension.create(); - @Override - protected void registerMetrics() { - SystemMetrics.registerObservers(GlobalOpenTelemetry.get()); + private static List observables; + + @BeforeAll + static void setUp() { + observables = SystemMetrics.registerObservers(GlobalOpenTelemetry.get()); + } + + @AfterAll + static void tearDown() { + for (AutoCloseable observable : observables) { + try { + observable.close(); + } catch (Exception e) { + // ignore + } + } } + @Override + protected void registerMetrics() {} + @Override protected InstrumentationExtension testing() { return testing; } + + @Test + void verifyObservablesAreNotEmpty() { + assertThat(observables).as("List of observables").isNotEmpty(); + } } diff --git a/instrumentation/payara/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/payara/StandardWrapperInstrumentation.java b/instrumentation/payara/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/payara/StandardWrapperInstrumentation.java index 5930012009f0..93f3e6a034d1 100644 --- a/instrumentation/payara/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/payara/StandardWrapperInstrumentation.java +++ b/instrumentation/payara/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/payara/StandardWrapperInstrumentation.java @@ -10,6 +10,7 @@ import com.google.errorprone.annotations.CanIgnoreReturnValue; import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.extension.instrumentation.internal.AsmApi; import net.bytebuddy.asm.AsmVisitorWrapper; import net.bytebuddy.description.field.FieldDescription; import net.bytebuddy.description.field.FieldList; @@ -64,7 +65,7 @@ public ClassVisitor wrap( private static class StandardWrapperClassVisitor extends ClassVisitor { StandardWrapperClassVisitor(ClassVisitor cv) { - super(Opcodes.ASM7, cv); + super(AsmApi.VERSION, cv); } @Override diff --git a/instrumentation/pekko/pekko-actor-1.0/javaagent/build.gradle.kts b/instrumentation/pekko/pekko-actor-1.0/javaagent/build.gradle.kts new file mode 100644 index 000000000000..a89b3b1df7a1 --- /dev/null +++ b/instrumentation/pekko/pekko-actor-1.0/javaagent/build.gradle.kts @@ -0,0 +1,38 @@ +plugins { + id("otel.javaagent-instrumentation") + id("otel.scala-conventions") +} + +muzzle { + pass { + group.set("org.apache.pekko") + module.set("pekko-actor_2.12") + versions.set("[1.0,)") + assertInverse.set(true) + } + pass { + group.set("org.apache.pekko") + module.set("pekko-actor_2.13") + versions.set("[1.0,)") + assertInverse.set(true) + } +} + +dependencies { + bootstrap(project(":instrumentation:executors:bootstrap")) + + library("org.apache.pekko:pekko-actor_2.12:1.0.1") + + latestDepTestLibrary("org.apache.pekko:pekko-actor_2.13:+") + + testImplementation(project(":instrumentation:executors:testing")) +} + +if (findProperty("testLatestDeps") as Boolean) { + configurations { + // pekko artifact name is different for regular and latest tests + testImplementation { + exclude("org.apache.pekko", "pekko-actor_2.12") + } + } +} diff --git a/instrumentation/pekko/pekko-actor-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkoactor/v1_0/PekkoActorCellInstrumentation.java b/instrumentation/pekko/pekko-actor-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkoactor/v1_0/PekkoActorCellInstrumentation.java new file mode 100644 index 000000000000..4f67fb22d427 --- /dev/null +++ b/instrumentation/pekko/pekko-actor-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkoactor/v1_0/PekkoActorCellInstrumentation.java @@ -0,0 +1,76 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.pekkoactor.v1_0; + +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import io.opentelemetry.context.Scope; +import io.opentelemetry.instrumentation.api.util.VirtualField; +import io.opentelemetry.javaagent.bootstrap.executors.PropagatedContext; +import io.opentelemetry.javaagent.bootstrap.executors.TaskAdviceHelper; +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.pekko.dispatch.Envelope; +import org.apache.pekko.dispatch.sysmsg.SystemMessage; + +public class PekkoActorCellInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("org.apache.pekko.actor.ActorCell"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + named("invoke").and(takesArgument(0, named("org.apache.pekko.dispatch.Envelope"))), + PekkoActorCellInstrumentation.class.getName() + "$InvokeAdvice"); + transformer.applyAdviceToMethod( + named("systemInvoke") + .and(takesArgument(0, named("org.apache.pekko.dispatch.sysmsg.SystemMessage"))), + PekkoActorCellInstrumentation.class.getName() + "$SystemInvokeAdvice"); + } + + @SuppressWarnings("unused") + public static class InvokeAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static Scope enter(@Advice.Argument(0) Envelope envelope) { + VirtualField virtualField = + VirtualField.find(Envelope.class, PropagatedContext.class); + return TaskAdviceHelper.makePropagatedContextCurrent(virtualField, 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) { + VirtualField virtualField = + VirtualField.find(SystemMessage.class, PropagatedContext.class); + return TaskAdviceHelper.makePropagatedContextCurrent(virtualField, 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/instrumentation/pekko/pekko-actor-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkoactor/v1_0/PekkoActorInstrumentationModule.java b/instrumentation/pekko/pekko-actor-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkoactor/v1_0/PekkoActorInstrumentationModule.java new file mode 100644 index 000000000000..bbf748fdf7fb --- /dev/null +++ b/instrumentation/pekko/pekko-actor-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkoactor/v1_0/PekkoActorInstrumentationModule.java @@ -0,0 +1,28 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.pekkoactor.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 PekkoActorInstrumentationModule extends InstrumentationModule { + public PekkoActorInstrumentationModule() { + super("pekko-actor", "pekko-actor-1.0"); + } + + @Override + public List typeInstrumentations() { + return asList( + new PekkoDispatcherInstrumentation(), + new PekkoActorCellInstrumentation(), + new PekkoDefaultSystemMessageQueueInstrumentation()); + } +} diff --git a/instrumentation/pekko/pekko-actor-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkoactor/v1_0/PekkoDefaultSystemMessageQueueInstrumentation.java b/instrumentation/pekko/pekko-actor-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkoactor/v1_0/PekkoDefaultSystemMessageQueueInstrumentation.java new file mode 100644 index 000000000000..226eec53ba83 --- /dev/null +++ b/instrumentation/pekko/pekko-actor-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkoactor/v1_0/PekkoDefaultSystemMessageQueueInstrumentation.java @@ -0,0 +1,65 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.pekkoactor.v1_0; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.hasClassesNamed; +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.implementsInterface; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.util.VirtualField; +import io.opentelemetry.javaagent.bootstrap.Java8BytecodeBridge; +import io.opentelemetry.javaagent.bootstrap.executors.ExecutorAdviceHelper; +import io.opentelemetry.javaagent.bootstrap.executors.PropagatedContext; +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.pekko.dispatch.sysmsg.SystemMessage; + +public class PekkoDefaultSystemMessageQueueInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher typeMatcher() { + return implementsInterface(named("org.apache.pekko.dispatch.DefaultSystemMessageQueue")); + } + + @Override + public ElementMatcher classLoaderOptimization() { + return hasClassesNamed("org.apache.pekko.dispatch.DefaultSystemMessageQueue"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + named("systemEnqueue") + .and(takesArgument(0, named("org.apache.pekko.actor.ActorRef"))) + .and(takesArgument(1, named("org.apache.pekko.dispatch.sysmsg.SystemMessage"))), + PekkoDefaultSystemMessageQueueInstrumentation.class.getName() + "$DispatchSystemAdvice"); + } + + @SuppressWarnings("unused") + public static class DispatchSystemAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static PropagatedContext enter(@Advice.Argument(1) SystemMessage systemMessage) { + Context context = Java8BytecodeBridge.currentContext(); + if (ExecutorAdviceHelper.shouldPropagateContext(context, systemMessage)) { + VirtualField virtualField = + VirtualField.find(SystemMessage.class, PropagatedContext.class); + return ExecutorAdviceHelper.attachContextToTask(context, virtualField, systemMessage); + } + return null; + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void exit( + @Advice.Enter PropagatedContext propagatedContext, @Advice.Thrown Throwable throwable) { + ExecutorAdviceHelper.cleanUpAfterSubmit(propagatedContext, throwable); + } + } +} diff --git a/instrumentation/pekko/pekko-actor-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkoactor/v1_0/PekkoDispatcherInstrumentation.java b/instrumentation/pekko/pekko-actor-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkoactor/v1_0/PekkoDispatcherInstrumentation.java new file mode 100644 index 000000000000..08201220dbfa --- /dev/null +++ b/instrumentation/pekko/pekko-actor-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkoactor/v1_0/PekkoDispatcherInstrumentation.java @@ -0,0 +1,59 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.pekkoactor.v1_0; + +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.util.VirtualField; +import io.opentelemetry.javaagent.bootstrap.Java8BytecodeBridge; +import io.opentelemetry.javaagent.bootstrap.executors.ExecutorAdviceHelper; +import io.opentelemetry.javaagent.bootstrap.executors.PropagatedContext; +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.pekko.dispatch.Envelope; + +public class PekkoDispatcherInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("org.apache.pekko.dispatch.Dispatcher"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + named("dispatch") + .and(takesArgument(0, named("org.apache.pekko.actor.ActorCell"))) + .and(takesArgument(1, named("org.apache.pekko.dispatch.Envelope"))), + PekkoDispatcherInstrumentation.class.getName() + "$DispatchEnvelopeAdvice"); + } + + @SuppressWarnings("unused") + public static class DispatchEnvelopeAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static PropagatedContext enterDispatch(@Advice.Argument(1) Envelope envelope) { + Context context = Java8BytecodeBridge.currentContext(); + if (ExecutorAdviceHelper.shouldPropagateContext(context, envelope.message())) { + VirtualField virtualField = + VirtualField.find(Envelope.class, PropagatedContext.class); + return ExecutorAdviceHelper.attachContextToTask(context, virtualField, envelope); + } + return null; + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void exitDispatch( + @Advice.Enter PropagatedContext propagatedContext, @Advice.Thrown Throwable throwable) { + ExecutorAdviceHelper.cleanUpAfterSubmit(propagatedContext, throwable); + } + } +} diff --git a/instrumentation/pekko/pekko-actor-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkoactor/v1_0/PekkoIgnoredTypesConfigurer.java b/instrumentation/pekko/pekko-actor-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkoactor/v1_0/PekkoIgnoredTypesConfigurer.java new file mode 100644 index 000000000000..da2ee3dff985 --- /dev/null +++ b/instrumentation/pekko/pekko-actor-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkoactor/v1_0/PekkoIgnoredTypesConfigurer.java @@ -0,0 +1,23 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.pekkoactor.v1_0; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.ignore.IgnoredTypesBuilder; +import io.opentelemetry.javaagent.extension.ignore.IgnoredTypesConfigurer; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; + +@AutoService(IgnoredTypesConfigurer.class) +public class PekkoIgnoredTypesConfigurer implements IgnoredTypesConfigurer { + + @Override + public void configure(IgnoredTypesBuilder builder, ConfigProperties config) { + // This is a Mailbox created by org.apache.pekko.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. + builder.ignoreTaskClass("org.apache.pekko.dispatch.Dispatcher$$anon$1"); + } +} diff --git a/instrumentation/pekko/pekko-actor-1.0/javaagent/src/test/scala/io/opentelemetry/javaagent/instrumentation/pekkoactor/v1_0/PekkoActorTest.scala b/instrumentation/pekko/pekko-actor-1.0/javaagent/src/test/scala/io/opentelemetry/javaagent/instrumentation/pekkoactor/v1_0/PekkoActorTest.scala new file mode 100644 index 000000000000..41c766092189 --- /dev/null +++ b/instrumentation/pekko/pekko-actor-1.0/javaagent/src/test/scala/io/opentelemetry/javaagent/instrumentation/pekkoactor/v1_0/PekkoActorTest.scala @@ -0,0 +1,135 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.pekkoactor.v1_0 + +import io.opentelemetry.api.common.Attributes +import io.opentelemetry.instrumentation.testing.junit.{ + AgentInstrumentationExtension, + InstrumentationExtension +} +import io.opentelemetry.sdk.testing.assertj.{SpanDataAssert, TraceAssert} +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.api.extension.RegisterExtension +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.ValueSource + +import java.util.function.Consumer +import scala.collection.JavaConverters._ + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class PekkoActorTest { + + @RegisterExtension val testing: InstrumentationExtension = + AgentInstrumentationExtension.create + + @ParameterizedTest + @ValueSource(ints = Array(1, 150)) + def basicTell(count: Int): Unit = { + val tester = new PekkoActors + (1 to count).foreach { _ => + tester.basicTell() + } + + val assertions = (1 to count) + .map(_ => + new Consumer[TraceAssert] { + override def accept(trace: TraceAssert): Unit = + trace.hasSpansSatisfyingExactly( + new Consumer[SpanDataAssert] { + override def accept(span: SpanDataAssert): Unit = { + span + .hasName("parent") + .hasAttributes(Attributes.empty()) + } + }, + new Consumer[SpanDataAssert] { + override def accept(span: SpanDataAssert): Unit = { + span + .hasName("Howdy, Pekko") + .hasParent(trace.getSpan(0)) + .hasAttributes(Attributes.empty()) + } + } + ) + } + ) + .asJava + + testing.waitAndAssertTraces(assertions) + } + + @ParameterizedTest + @ValueSource(ints = Array(1, 150)) + def basicAsk(count: Int): Unit = { + val tester = new PekkoActors + (1 to count).foreach { _ => + tester.basicAsk() + } + + val assertions = (1 to count) + .map(_ => + new Consumer[TraceAssert] { + override def accept(trace: TraceAssert): Unit = + trace.hasSpansSatisfyingExactly( + new Consumer[SpanDataAssert] { + override def accept(span: SpanDataAssert): Unit = { + span + .hasName("parent") + .hasAttributes(Attributes.empty()) + } + }, + new Consumer[SpanDataAssert] { + override def accept(span: SpanDataAssert): Unit = { + span + .hasName("Howdy, Pekko") + .hasParent(trace.getSpan(0)) + .hasAttributes(Attributes.empty()) + } + } + ) + } + ) + .asJava + + testing.waitAndAssertTraces(assertions) + } + + @ParameterizedTest + @ValueSource(ints = Array(1, 150)) + def basicForward(count: Int): Unit = { + val tester = new PekkoActors + (1 to count).foreach { _ => + tester.basicForward() + } + + val assertions = (1 to count) + .map(_ => + new Consumer[TraceAssert] { + override def accept(trace: TraceAssert): Unit = + trace.hasSpansSatisfyingExactly( + new Consumer[SpanDataAssert] { + override def accept(span: SpanDataAssert): Unit = { + span + .hasName("parent") + .hasAttributes(Attributes.empty()) + } + }, + new Consumer[SpanDataAssert] { + override def accept(span: SpanDataAssert): Unit = { + span + .hasName("Hello, Pekko") + .hasParent(trace.getSpan(0)) + .hasAttributes(Attributes.empty()) + } + } + ) + } + ) + .asJava + + testing.waitAndAssertTraces(assertions) + } +} diff --git a/instrumentation/pekko/pekko-actor-1.0/javaagent/src/test/scala/io/opentelemetry/javaagent/instrumentation/pekkoactor/v1_0/PekkoActors.scala b/instrumentation/pekko/pekko-actor-1.0/javaagent/src/test/scala/io/opentelemetry/javaagent/instrumentation/pekkoactor/v1_0/PekkoActors.scala new file mode 100644 index 000000000000..2eddb80d5aa9 --- /dev/null +++ b/instrumentation/pekko/pekko-actor-1.0/javaagent/src/test/scala/io/opentelemetry/javaagent/instrumentation/pekkoactor/v1_0/PekkoActors.scala @@ -0,0 +1,146 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.pekkoactor.v1_0 + +import org.apache.pekko.actor.{ + Actor, + ActorLogging, + ActorRef, + ActorSystem, + Props +} +import org.apache.pekko.pattern.ask +import org.apache.pekko.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 PekkoActors { + val tracer: Tracer = GlobalOpenTelemetry.getTracer("test") + + val system: ActorSystem = ActorSystem("helloPekko") + + 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 PekkoActors { + + import PekkoActors._ + 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("Pekko") + 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("Pekko") + 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("Pekko") + 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) => { + PekkoActors.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/instrumentation/pekko/pekko-http-1.0/javaagent/build.gradle.kts b/instrumentation/pekko/pekko-http-1.0/javaagent/build.gradle.kts new file mode 100644 index 000000000000..f72ae7ddb45a --- /dev/null +++ b/instrumentation/pekko/pekko-http-1.0/javaagent/build.gradle.kts @@ -0,0 +1,54 @@ +plugins { + id("otel.javaagent-instrumentation") + id("otel.scala-conventions") +} + +muzzle { + pass { + group.set("org.apache.pekko") + module.set("pekko-http_2.12") + versions.set("[1.0,)") + assertInverse.set(true) + extraDependency("org.apache.pekko:pekko-stream_2.12:1.0.1") + } + pass { + group.set("org.apache.pekko") + module.set("pekko-http_2.13") + versions.set("[1.0,)") + assertInverse.set(true) + extraDependency("org.apache.pekko:pekko-stream_2.13:1.0.1") + } +} + +dependencies { + library("org.apache.pekko:pekko-http_2.12:1.0.0") + library("org.apache.pekko:pekko-stream_2.12:1.0.1") + + testInstrumentation(project(":instrumentation:pekko:pekko-actor-1.0:javaagent")) + testInstrumentation(project(":instrumentation:executors:javaagent")) + + latestDepTestLibrary("org.apache.pekko:pekko-http_2.13:+") + latestDepTestLibrary("org.apache.pekko:pekko-stream_2.13:+") +} + +tasks { + withType().configureEach { + // required on jdk17 + jvmArgs("--add-exports=java.base/sun.security.util=ALL-UNNAMED") + jvmArgs("-XX:+IgnoreUnrecognizedVMOptions") + + jvmArgs("-Dio.opentelemetry.javaagent.shaded.io.opentelemetry.context.enableStrictContext=false") + + systemProperty("testLatestDeps", findProperty("testLatestDeps") as Boolean) + } +} + +if (findProperty("testLatestDeps") as Boolean) { + configurations { + // pekko artifact name is different for regular and latest tests + testImplementation { + exclude("org.apache.pekko", "pekko-http_2.12") + exclude("org.apache.pekko", "pekko-stream_2.12") + } + } +} diff --git a/instrumentation/pekko/pekko-http-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/PekkoHttpUtil.java b/instrumentation/pekko/pekko-http-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/PekkoHttpUtil.java new file mode 100644 index 000000000000..8c23644dd103 --- /dev/null +++ b/instrumentation/pekko/pekko-http-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/PekkoHttpUtil.java @@ -0,0 +1,52 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.pekkohttp.v1_0; + +import java.util.Collections; +import java.util.List; +import org.apache.pekko.http.scaladsl.model.HttpRequest; +import org.apache.pekko.http.scaladsl.model.HttpResponse; + +public class PekkoHttpUtil { + + private static final String INSTRUMENTATION_NAME = "io.opentelemetry.pekko-http-1.0"; + + public static String instrumentationName() { + return INSTRUMENTATION_NAME; + } + + public static List requestHeader(HttpRequest httpRequest, String name) { + return httpRequest + .getHeader(name) + .map(httpHeader -> Collections.singletonList(httpHeader.value())) + .orElse(Collections.emptyList()); + } + + public static List responseHeader(HttpResponse httpResponse, String name) { + return httpResponse + .getHeader(name) + .map(httpHeader -> Collections.singletonList(httpHeader.value())) + .orElse(Collections.emptyList()); + } + + public static String protocolName(HttpRequest request) { + String protocol = request.protocol().value(); + if (protocol.startsWith("HTTP/")) { + return "http"; + } + return null; + } + + public static String protocolVersion(HttpRequest request) { + String protocol = request.protocol().value(); + if (protocol.startsWith("HTTP/")) { + return protocol.substring("HTTP/".length()); + } + return null; + } + + private PekkoHttpUtil() {} +} diff --git a/instrumentation/pekko/pekko-http-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/client/FutureWrapper.java b/instrumentation/pekko/pekko-http-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/client/FutureWrapper.java new file mode 100644 index 000000000000..e5840884e6b0 --- /dev/null +++ b/instrumentation/pekko/pekko-http-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/client/FutureWrapper.java @@ -0,0 +1,37 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.pekkohttp.v1_0.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 final 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; + } + + private FutureWrapper() {} +} diff --git a/instrumentation/pekko/pekko-http-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/client/HttpExtClientInstrumentation.java b/instrumentation/pekko/pekko-http-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/client/HttpExtClientInstrumentation.java new file mode 100644 index 000000000000..caa45c72012d --- /dev/null +++ b/instrumentation/pekko/pekko-http-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/client/HttpExtClientInstrumentation.java @@ -0,0 +1,84 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.pekkohttp.v1_0.client; + +import static io.opentelemetry.javaagent.bootstrap.Java8BytecodeBridge.currentContext; +import static io.opentelemetry.javaagent.instrumentation.pekkohttp.v1_0.client.PekkoHttpClientSingletons.instrumenter; +import static io.opentelemetry.javaagent.instrumentation.pekkohttp.v1_0.client.PekkoHttpClientSingletons.setter; +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.pekko.http.scaladsl.HttpExt; +import org.apache.pekko.http.scaladsl.model.HttpRequest; +import org.apache.pekko.http.scaladsl.model.HttpResponse; +import scala.concurrent.Future; + +public class HttpExtClientInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher typeMatcher() { + return named("org.apache.pekko.http.scaladsl.HttpExt"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + named("singleRequest") + .and(takesArgument(0, named("org.apache.pekko.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) { + Context parentContext = currentContext(); + if (!instrumenter().shouldStart(parentContext, request)) { + return; + } + + context = instrumenter().start(parentContext, request); + scope = context.makeCurrent(); + // Request is immutable, so we have to assign new value once we update headers + request = setter().inject(request); + } + + @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, request), thiz.system().dispatcher()); + } else { + instrumenter().end(context, request, null, throwable); + } + if (responseFuture != null) { + responseFuture = + FutureWrapper.wrap(responseFuture, thiz.system().dispatcher(), currentContext()); + } + } + } +} diff --git a/instrumentation/pekko/pekko-http-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/client/HttpHeaderSetter.java b/instrumentation/pekko/pekko-http-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/client/HttpHeaderSetter.java new file mode 100644 index 000000000000..d3438e38cb8d --- /dev/null +++ b/instrumentation/pekko/pekko-http-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/client/HttpHeaderSetter.java @@ -0,0 +1,53 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.pekkohttp.v1_0.client; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.propagation.ContextPropagators; +import io.opentelemetry.context.propagation.TextMapSetter; +import org.apache.pekko.http.javadsl.model.headers.RawHeader; +import org.apache.pekko.http.scaladsl.model.HttpRequest; + +public class HttpHeaderSetter implements TextMapSetter { + + private final ContextPropagators contextPropagators; + + public HttpHeaderSetter(ContextPropagators contextPropagators) { + this.contextPropagators = contextPropagators; + } + + @Override + public void set(PekkoHttpHeaders 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))); + } + } + + public HttpRequest inject(HttpRequest original) { + PekkoHttpHeaders carrier = new PekkoHttpHeaders(original); + contextPropagators.getTextMapPropagator().inject(Context.current(), carrier, this); + return carrier.getRequest(); + } + + static class PekkoHttpHeaders { + private HttpRequest request; + + public PekkoHttpHeaders(HttpRequest request) { + this.request = request; + } + + public HttpRequest getRequest() { + return request; + } + + public void setRequest(HttpRequest request) { + this.request = request; + } + } +} diff --git a/instrumentation/pekko/pekko-http-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/client/OnCompleteHandler.java b/instrumentation/pekko/pekko-http-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/client/OnCompleteHandler.java new file mode 100644 index 000000000000..7cbc13bc079a --- /dev/null +++ b/instrumentation/pekko/pekko-http-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/client/OnCompleteHandler.java @@ -0,0 +1,34 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.pekkohttp.v1_0.client; + +import static io.opentelemetry.javaagent.instrumentation.pekkohttp.v1_0.client.PekkoHttpClientSingletons.instrumenter; + +import io.opentelemetry.context.Context; +import org.apache.pekko.http.scaladsl.model.HttpRequest; +import org.apache.pekko.http.scaladsl.model.HttpResponse; +import scala.runtime.AbstractFunction1; +import scala.util.Try; + +public class OnCompleteHandler extends AbstractFunction1, Void> { + private final Context context; + private final HttpRequest request; + + public OnCompleteHandler(Context context, HttpRequest request) { + this.context = context; + this.request = request; + } + + @Override + public Void apply(Try result) { + if (result.isSuccess()) { + instrumenter().end(context, request, result.get(), null); + } else { + instrumenter().end(context, request, null, result.failed().get()); + } + return null; + } +} diff --git a/instrumentation/pekko/pekko-http-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/client/PekkoHttpClientAttributesGetter.java b/instrumentation/pekko/pekko-http-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/client/PekkoHttpClientAttributesGetter.java new file mode 100644 index 000000000000..fb48cc5a7599 --- /dev/null +++ b/instrumentation/pekko/pekko-http-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/client/PekkoHttpClientAttributesGetter.java @@ -0,0 +1,68 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.pekkohttp.v1_0.client; + +import io.opentelemetry.instrumentation.api.semconv.http.HttpClientAttributesGetter; +import io.opentelemetry.javaagent.instrumentation.pekkohttp.v1_0.PekkoHttpUtil; +import java.util.List; +import javax.annotation.Nullable; +import org.apache.pekko.http.scaladsl.model.HttpRequest; +import org.apache.pekko.http.scaladsl.model.HttpResponse; + +class PekkoHttpClientAttributesGetter + implements HttpClientAttributesGetter { + + @Override + public String getUrlFull(HttpRequest httpRequest) { + return httpRequest.uri().toString(); + } + + @Override + public String getHttpRequestMethod(HttpRequest httpRequest) { + return httpRequest.method().value(); + } + + @Override + public List getHttpRequestHeader(HttpRequest httpRequest, String name) { + return PekkoHttpUtil.requestHeader(httpRequest, name); + } + + @Override + public Integer getHttpResponseStatusCode( + HttpRequest httpRequest, HttpResponse httpResponse, @Nullable Throwable error) { + return httpResponse.status().intValue(); + } + + @Override + public List getHttpResponseHeader( + HttpRequest httpRequest, HttpResponse httpResponse, String name) { + return PekkoHttpUtil.responseHeader(httpResponse, name); + } + + @Nullable + @Override + public String getNetworkProtocolName( + HttpRequest httpRequest, @Nullable HttpResponse httpResponse) { + return PekkoHttpUtil.protocolName(httpRequest); + } + + @Nullable + @Override + public String getNetworkProtocolVersion( + HttpRequest httpRequest, @Nullable HttpResponse httpResponse) { + return PekkoHttpUtil.protocolVersion(httpRequest); + } + + @Override + public String getServerAddress(HttpRequest httpRequest) { + return httpRequest.uri().authority().host().address(); + } + + @Override + public Integer getServerPort(HttpRequest httpRequest) { + return httpRequest.uri().authority().port(); + } +} diff --git a/instrumentation/pekko/pekko-http-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/client/PekkoHttpClientInstrumentationModule.java b/instrumentation/pekko/pekko-http-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/client/PekkoHttpClientInstrumentationModule.java new file mode 100644 index 000000000000..a5833f2b320d --- /dev/null +++ b/instrumentation/pekko/pekko-http-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/client/PekkoHttpClientInstrumentationModule.java @@ -0,0 +1,25 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.pekkohttp.v1_0.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 PekkoHttpClientInstrumentationModule extends InstrumentationModule { + public PekkoHttpClientInstrumentationModule() { + super("pekko-http", "pekko-http-1.0", "pekko-http-client"); + } + + @Override + public List typeInstrumentations() { + return asList(new HttpExtClientInstrumentation(), new PoolMasterActorInstrumentation()); + } +} diff --git a/instrumentation/pekko/pekko-http-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/client/PekkoHttpClientSingletons.java b/instrumentation/pekko/pekko-http-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/client/PekkoHttpClientSingletons.java new file mode 100644 index 000000000000..79062bf38803 --- /dev/null +++ b/instrumentation/pekko/pekko-http-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/client/PekkoHttpClientSingletons.java @@ -0,0 +1,37 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.pekkohttp.v1_0.client; + +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import io.opentelemetry.javaagent.bootstrap.internal.JavaagentHttpClientInstrumenters; +import io.opentelemetry.javaagent.instrumentation.pekkohttp.v1_0.PekkoHttpUtil; +import org.apache.pekko.http.scaladsl.model.HttpRequest; +import org.apache.pekko.http.scaladsl.model.HttpResponse; + +public class PekkoHttpClientSingletons { + + private static final HttpHeaderSetter SETTER; + private static final Instrumenter INSTRUMENTER; + + static { + SETTER = new HttpHeaderSetter(GlobalOpenTelemetry.getPropagators()); + + INSTRUMENTER = + JavaagentHttpClientInstrumenters.create( + PekkoHttpUtil.instrumentationName(), new PekkoHttpClientAttributesGetter()); + } + + public static Instrumenter instrumenter() { + return INSTRUMENTER; + } + + public static HttpHeaderSetter setter() { + return SETTER; + } + + private PekkoHttpClientSingletons() {} +} diff --git a/instrumentation/pekko/pekko-http-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/client/PoolMasterActorInstrumentation.java b/instrumentation/pekko/pekko-http-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/client/PoolMasterActorInstrumentation.java new file mode 100644 index 000000000000..1f3e34c909af --- /dev/null +++ b/instrumentation/pekko/pekko-http-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/client/PoolMasterActorInstrumentation.java @@ -0,0 +1,50 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.pekkohttp.v1_0.client; + +import static net.bytebuddy.matcher.ElementMatchers.named; + +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.bootstrap.Java8BytecodeBridge; +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 PoolMasterActorInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher typeMatcher() { + return named("org.apache.pekko.http.impl.engine.client.PoolMasterActor"); + } + + @Override + public void transform(TypeTransformer transformer) { + // scala compiler mangles method names + transformer.applyAdviceToMethod( + named("org$apache$pekko$http$impl$engine$client$PoolMasterActor$$startPoolInterface") + .or( + named( + "org$apache$pekko$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/instrumentation/pekko/pekko-http-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/server/GraphInterpreterInstrumentation.java b/instrumentation/pekko/pekko-http-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/server/GraphInterpreterInstrumentation.java new file mode 100644 index 000000000000..01ca12adc89c --- /dev/null +++ b/instrumentation/pekko/pekko-http-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/server/GraphInterpreterInstrumentation.java @@ -0,0 +1,52 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.pekkohttp.v1_0.server; + +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 net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import org.apache.pekko.stream.impl.fusing.GraphInterpreter; + +public class GraphInterpreterInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher typeMatcher() { + return named("org.apache.pekko.stream.impl.fusing.GraphInterpreter"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + named("processPush"), GraphInterpreterInstrumentation.class.getName() + "$PushAdvice"); + } + + @SuppressWarnings("unused") + public static class PushAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static Scope onEnter(@Advice.Argument(0) GraphInterpreter.Connection connection) { + // processPush is called when execution passes to application or server. Here we propagate the + // context to the application code. + Context context = PekkoFlowWrapper.getContext(connection.outHandler()); + if (context != null) { + return context.makeCurrent(); + } + return null; + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void exit(@Advice.Enter Scope scope) { + if (scope != null) { + scope.close(); + } + } + } +} diff --git a/instrumentation/pekko/pekko-http-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/server/HttpExtServerInstrumentation.java b/instrumentation/pekko/pekko-http-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/server/HttpExtServerInstrumentation.java new file mode 100644 index 000000000000..428e57a7d8ab --- /dev/null +++ b/instrumentation/pekko/pekko-http-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/server/HttpExtServerInstrumentation.java @@ -0,0 +1,43 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.pekkohttp.v1_0.server; + +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.pekko.http.scaladsl.model.HttpRequest; +import org.apache.pekko.http.scaladsl.model.HttpResponse; +import org.apache.pekko.stream.scaladsl.Flow; + +public class HttpExtServerInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher typeMatcher() { + return named("org.apache.pekko.http.scaladsl.HttpExt"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + named("bindAndHandle") + .and(takesArgument(0, named("org.apache.pekko.stream.scaladsl.Flow"))), + this.getClass().getName() + "$PekkoBindAndHandleAdvice"); + } + + @SuppressWarnings("unused") + public static class PekkoBindAndHandleAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void wrapHandler( + @Advice.Argument(value = 0, readOnly = false) Flow handler) { + handler = PekkoFlowWrapper.wrap(handler); + } + } +} diff --git a/instrumentation/pekko/pekko-http-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/server/PekkoFlowWrapper.java b/instrumentation/pekko/pekko-http-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/server/PekkoFlowWrapper.java new file mode 100644 index 000000000000..b6d8d986a3fa --- /dev/null +++ b/instrumentation/pekko/pekko-http-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/server/PekkoFlowWrapper.java @@ -0,0 +1,210 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.pekkohttp.v1_0.server; + +import static io.opentelemetry.javaagent.bootstrap.Java8BytecodeBridge.currentContext; + +import io.opentelemetry.context.Context; +import io.opentelemetry.javaagent.bootstrap.http.HttpServerResponseCustomizerHolder; +import io.opentelemetry.javaagent.instrumentation.pekkohttp.v1_0.server.route.PekkoRouteHolder; +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.List; +import org.apache.pekko.http.javadsl.model.HttpHeader; +import org.apache.pekko.http.scaladsl.model.HttpRequest; +import org.apache.pekko.http.scaladsl.model.HttpResponse; +import org.apache.pekko.stream.Attributes; +import org.apache.pekko.stream.BidiShape; +import org.apache.pekko.stream.Inlet; +import org.apache.pekko.stream.Outlet; +import org.apache.pekko.stream.scaladsl.Flow; +import org.apache.pekko.stream.stage.AbstractInHandler; +import org.apache.pekko.stream.stage.AbstractOutHandler; +import org.apache.pekko.stream.stage.GraphStage; +import org.apache.pekko.stream.stage.GraphStageLogic; +import org.apache.pekko.stream.stage.OutHandler; + +public class PekkoFlowWrapper + extends GraphStage> { + private final Inlet requestIn = Inlet.create("otel.requestIn"); + private final Outlet requestOut = Outlet.create("otel.requestOut"); + private final Inlet responseIn = Inlet.create("otel.responseIn"); + private final Outlet responseOut = Outlet.create("otel.responseOut"); + + private final BidiShape shape = + BidiShape.of(responseIn, responseOut, requestIn, requestOut); + + public static Flow wrap( + Flow handler) { + return handler.join(new PekkoFlowWrapper()); + } + + public static Context getContext(OutHandler outHandler) { + if (outHandler instanceof TracingLogic.ApplicationOutHandler) { + // We have multiple requests here only when requests are pipelined on the same connection. + // It appears that these requests are processed one by one so processing next request won't + // be started before the first one has returned a response, because of this the first request + // in the queue is always the one that is currently being processed. + TracingRequest request = + ((TracingLogic.ApplicationOutHandler) outHandler).getRequests().peek(); + if (request != null) { + return request.context; + } + } + + return null; + } + + @Override + public BidiShape shape() { + return shape; + } + + @Override + public GraphStageLogic createLogic(Attributes attributes) { + return new TracingLogic(); + } + + private class TracingLogic extends GraphStageLogic { + private final Deque requests = new ArrayDeque<>(); + + public TracingLogic() { + super(shape); + + // server pulls response, pass response from user code to server + setHandler( + responseOut, + new AbstractOutHandler() { + @Override + public void onPull() { + pull(responseIn); + } + + @Override + public void onDownstreamFinish(Throwable cause) { + cancel(responseIn); + } + }); + + // user code pulls request, pass request from server to user code + setHandler( + requestOut, + new ApplicationOutHandler() { + @Override + public void onPull() { + pull(requestIn); + } + + @Override + public void onDownstreamFinish(Throwable cause) { + // Invoked on errors. Don't complete this stage to allow error-capturing + cancel(requestIn); + } + }); + + // new request from server + setHandler( + requestIn, + new AbstractInHandler() { + @Override + public void onPush() { + HttpRequest request = grab(requestIn); + + TracingRequest tracingRequest = TracingRequest.EMPTY; + Context parentContext = currentContext(); + if (PekkoHttpServerSingletons.instrumenter().shouldStart(parentContext, request)) { + Context context = + PekkoHttpServerSingletons.instrumenter().start(parentContext, request); + context = PekkoRouteHolder.init(context); + tracingRequest = new TracingRequest(context, request); + } + // event if span wasn't started we need to push TracingRequest to match response + // with request + requests.push(tracingRequest); + + push(requestOut, request); + } + + @Override + public void onUpstreamFinish() { + complete(requestOut); + } + + @Override + public void onUpstreamFailure(Throwable exception) { + fail(requestOut, exception); + } + }); + + // response from user code + setHandler( + responseIn, + new AbstractInHandler() { + @Override + public void onPush() { + HttpResponse response = grab(responseIn); + + TracingRequest tracingRequest = requests.poll(); + if (tracingRequest != null && tracingRequest != TracingRequest.EMPTY) { + // pekko response is immutable so the customizer just captures the added headers + PekkoHttpResponseMutator responseMutator = new PekkoHttpResponseMutator(); + HttpServerResponseCustomizerHolder.getCustomizer() + .customize(tracingRequest.context, response, responseMutator); + // build a new response with the added headers + List headers = responseMutator.getHeaders(); + if (!headers.isEmpty()) { + response = (HttpResponse) response.addHeaders(headers); + } + + PekkoHttpServerSingletons.instrumenter() + .end(tracingRequest.context, tracingRequest.request, response, null); + } + push(responseOut, response); + } + + @Override + public void onUpstreamFailure(Throwable exception) { + TracingRequest tracingRequest; + while ((tracingRequest = requests.poll()) != null) { + if (tracingRequest == TracingRequest.EMPTY) { + continue; + } + PekkoHttpServerSingletons.instrumenter() + .end( + tracingRequest.context, + tracingRequest.request, + PekkoHttpServerSingletons.errorResponse(), + exception); + } + + fail(responseOut, exception); + } + + @Override + public void onUpstreamFinish() { + completeStage(); + } + }); + } + + abstract class ApplicationOutHandler extends AbstractOutHandler { + Deque getRequests() { + return requests; + } + } + } + + private static class TracingRequest { + static final TracingRequest EMPTY = new TracingRequest(null, null); + final Context context; + final HttpRequest request; + + TracingRequest(Context context, HttpRequest request) { + this.context = context; + this.request = request; + } + } +} diff --git a/instrumentation/pekko/pekko-http-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/server/PekkoHttpResponseMutator.java b/instrumentation/pekko/pekko-http-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/server/PekkoHttpResponseMutator.java new file mode 100644 index 000000000000..4ddcada746ba --- /dev/null +++ b/instrumentation/pekko/pekko-http-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/server/PekkoHttpResponseMutator.java @@ -0,0 +1,27 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.pekkohttp.v1_0.server; + +import io.opentelemetry.javaagent.bootstrap.http.HttpServerResponseMutator; +import java.util.ArrayList; +import java.util.List; +import org.apache.pekko.http.javadsl.model.HttpHeader; +import org.apache.pekko.http.javadsl.model.HttpResponse; +import org.apache.pekko.http.javadsl.model.headers.RawHeader; + +final class PekkoHttpResponseMutator implements HttpServerResponseMutator { + + private final List headers = new ArrayList<>(); + + @Override + public void appendHeader(HttpResponse response, String name, String value) { + headers.add(RawHeader.create(name, value)); + } + + List getHeaders() { + return headers; + } +} diff --git a/instrumentation/pekko/pekko-http-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/server/PekkoHttpServerAttributesGetter.java b/instrumentation/pekko/pekko-http-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/server/PekkoHttpServerAttributesGetter.java new file mode 100644 index 000000000000..04f828477296 --- /dev/null +++ b/instrumentation/pekko/pekko-http-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/server/PekkoHttpServerAttributesGetter.java @@ -0,0 +1,70 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.pekkohttp.v1_0.server; + +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerAttributesGetter; +import io.opentelemetry.javaagent.instrumentation.pekkohttp.v1_0.PekkoHttpUtil; +import java.util.List; +import javax.annotation.Nullable; +import org.apache.pekko.http.scaladsl.model.HttpRequest; +import org.apache.pekko.http.scaladsl.model.HttpResponse; +import scala.Option; + +class PekkoHttpServerAttributesGetter + implements HttpServerAttributesGetter { + + @Override + public String getHttpRequestMethod(HttpRequest request) { + return request.method().value(); + } + + @Override + public List getHttpRequestHeader(HttpRequest request, String name) { + return PekkoHttpUtil.requestHeader(request, name); + } + + @Override + public Integer getHttpResponseStatusCode( + HttpRequest request, HttpResponse httpResponse, @Nullable Throwable error) { + return httpResponse.status().intValue(); + } + + @Override + public List getHttpResponseHeader( + HttpRequest request, HttpResponse httpResponse, String name) { + return PekkoHttpUtil.responseHeader(httpResponse, name); + } + + @Override + public String getUrlScheme(HttpRequest request) { + return request.uri().scheme(); + } + + @Override + public String getUrlPath(HttpRequest request) { + return request.uri().path().toString(); + } + + @Nullable + @Override + public String getUrlQuery(HttpRequest request) { + Option queryString = request.uri().rawQueryString(); + return queryString.isDefined() ? queryString.get() : null; + } + + @Nullable + @Override + public String getNetworkProtocolName(HttpRequest request, @Nullable HttpResponse httpResponse) { + return PekkoHttpUtil.protocolName(request); + } + + @Nullable + @Override + public String getNetworkProtocolVersion( + HttpRequest request, @Nullable HttpResponse httpResponse) { + return PekkoHttpUtil.protocolVersion(request); + } +} diff --git a/instrumentation/pekko/pekko-http-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/server/PekkoHttpServerHeaders.java b/instrumentation/pekko/pekko-http-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/server/PekkoHttpServerHeaders.java new file mode 100644 index 000000000000..12636cf58889 --- /dev/null +++ b/instrumentation/pekko/pekko-http-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/server/PekkoHttpServerHeaders.java @@ -0,0 +1,30 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.pekkohttp.v1_0.server; + +import io.opentelemetry.context.propagation.TextMapGetter; +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; +import org.apache.pekko.http.javadsl.model.HttpHeader; +import org.apache.pekko.http.scaladsl.model.HttpRequest; + +enum PekkoHttpServerHeaders implements TextMapGetter { + INSTANCE; + + @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/instrumentation/pekko/pekko-http-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/server/PekkoHttpServerInstrumentationModule.java b/instrumentation/pekko/pekko-http-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/server/PekkoHttpServerInstrumentationModule.java new file mode 100644 index 000000000000..0062defaf526 --- /dev/null +++ b/instrumentation/pekko/pekko-http-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/server/PekkoHttpServerInstrumentationModule.java @@ -0,0 +1,44 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.pekkohttp.v1_0.server; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.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 PekkoHttpServerInstrumentationModule extends InstrumentationModule { + public PekkoHttpServerInstrumentationModule() { + super("pekko-http", "pekko-http-1.0", "pekko-http-server"); + } + + @Override + public ElementMatcher.Junction classLoaderMatcher() { + // in GraphInterpreterInstrumentation we instrument a class that belongs to pekko-streams, make + // sure this runs only when pekko-http is present to avoid muzzle failures + return hasClassesNamed("org.apache.pekko.http.scaladsl.HttpExt"); + } + + @Override + public boolean isIndyModule() { + // PekkoHttpServerInstrumentationModule and PekkoHttpServerRouteInstrumentationModule share + // PekkoRouteHolder class + return false; + } + + @Override + public List typeInstrumentations() { + return asList( + new HttpExtServerInstrumentation(), + new GraphInterpreterInstrumentation(), + new PekkoHttpServerSourceInstrumentation()); + } +} diff --git a/instrumentation/pekko/pekko-http-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/server/PekkoHttpServerSingletons.java b/instrumentation/pekko/pekko-http-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/server/PekkoHttpServerSingletons.java new file mode 100644 index 000000000000..8618492181d6 --- /dev/null +++ b/instrumentation/pekko/pekko-http-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/server/PekkoHttpServerSingletons.java @@ -0,0 +1,63 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.pekkohttp.v1_0.server; + +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.instrumentation.api.incubator.semconv.http.HttpExperimentalAttributesExtractor; +import io.opentelemetry.instrumentation.api.incubator.semconv.http.HttpServerExperimentalMetrics; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import io.opentelemetry.instrumentation.api.instrumenter.InstrumenterBuilder; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerAttributesExtractor; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerMetrics; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRoute; +import io.opentelemetry.instrumentation.api.semconv.http.HttpSpanNameExtractor; +import io.opentelemetry.instrumentation.api.semconv.http.HttpSpanStatusExtractor; +import io.opentelemetry.javaagent.bootstrap.internal.AgentCommonConfig; +import io.opentelemetry.javaagent.instrumentation.pekkohttp.v1_0.PekkoHttpUtil; +import org.apache.pekko.http.scaladsl.model.HttpRequest; +import org.apache.pekko.http.scaladsl.model.HttpResponse; + +public final class PekkoHttpServerSingletons { + + private static final Instrumenter INSTRUMENTER; + + static { + PekkoHttpServerAttributesGetter httpAttributesGetter = new PekkoHttpServerAttributesGetter(); + InstrumenterBuilder builder = + Instrumenter.builder( + GlobalOpenTelemetry.get(), + PekkoHttpUtil.instrumentationName(), + HttpSpanNameExtractor.create(httpAttributesGetter)) + .setSpanStatusExtractor(HttpSpanStatusExtractor.create(httpAttributesGetter)) + .addAttributesExtractor( + HttpServerAttributesExtractor.builder(httpAttributesGetter) + .setCapturedRequestHeaders(AgentCommonConfig.get().getServerRequestHeaders()) + .setCapturedResponseHeaders(AgentCommonConfig.get().getServerResponseHeaders()) + .setKnownMethods(AgentCommonConfig.get().getKnownHttpRequestMethods()) + .build()) + .addOperationMetrics(HttpServerMetrics.get()) + .addContextCustomizer( + HttpServerRoute.builder(httpAttributesGetter) + .setKnownMethods(AgentCommonConfig.get().getKnownHttpRequestMethods()) + .build()); + if (AgentCommonConfig.get().shouldEmitExperimentalHttpServerTelemetry()) { + builder + .addAttributesExtractor(HttpExperimentalAttributesExtractor.create(httpAttributesGetter)) + .addOperationMetrics(HttpServerExperimentalMetrics.get()); + } + INSTRUMENTER = builder.buildServerInstrumenter(PekkoHttpServerHeaders.INSTANCE); + } + + public static Instrumenter instrumenter() { + return INSTRUMENTER; + } + + public static HttpResponse errorResponse() { + return (HttpResponse) HttpResponse.create().withStatus(500); + } + + private PekkoHttpServerSingletons() {} +} diff --git a/instrumentation/pekko/pekko-http-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/server/PekkoHttpServerSourceInstrumentation.java b/instrumentation/pekko/pekko-http-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/server/PekkoHttpServerSourceInstrumentation.java new file mode 100644 index 000000000000..52539a2056fb --- /dev/null +++ b/instrumentation/pekko/pekko-http-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/server/PekkoHttpServerSourceInstrumentation.java @@ -0,0 +1,42 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.pekkohttp.v1_0.server; + +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.pekko.http.scaladsl.model.HttpRequest; +import org.apache.pekko.http.scaladsl.model.HttpResponse; +import org.apache.pekko.stream.scaladsl.Flow; + +public class PekkoHttpServerSourceInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher typeMatcher() { + return named("org.apache.pekko.http.scaladsl.Http$IncomingConnection"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + named("handleWith").and(takesArgument(0, named("org.apache.pekko.stream.scaladsl.Flow"))), + this.getClass().getName() + "$PekkoBindAndHandleAdvice"); + } + + @SuppressWarnings("unused") + public static class PekkoBindAndHandleAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void wrapHandler( + @Advice.Argument(value = 0, readOnly = false) Flow handler) { + handler = PekkoFlowWrapper.wrap(handler); + } + } +} diff --git a/instrumentation/pekko/pekko-http-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/server/PekkoServerIgnoredTypesConfigurer.java b/instrumentation/pekko/pekko-http-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/server/PekkoServerIgnoredTypesConfigurer.java new file mode 100644 index 000000000000..d8dc971ee5a0 --- /dev/null +++ b/instrumentation/pekko/pekko-http-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/server/PekkoServerIgnoredTypesConfigurer.java @@ -0,0 +1,22 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.pekkohttp.v1_0.server; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.ignore.IgnoredTypesBuilder; +import io.opentelemetry.javaagent.extension.ignore.IgnoredTypesConfigurer; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; + +@AutoService(IgnoredTypesConfigurer.class) +public class PekkoServerIgnoredTypesConfigurer implements IgnoredTypesConfigurer { + + @Override + public void configure(IgnoredTypesBuilder builder, ConfigProperties config) { + // in PekkoHttpServerInstrumentationTestAsync http pipeline test sending this message trigger + // processing next request, we don't want to propagate context there + builder.ignoreTaskClass("org.apache.pekko.stream.impl.fusing.ActorGraphInterpreter$AsyncInput"); + } +} diff --git a/instrumentation/pekko/pekko-http-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/server/route/PathConcatenationInstrumentation.java b/instrumentation/pekko/pekko-http-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/server/route/PathConcatenationInstrumentation.java new file mode 100644 index 000000000000..d11310208bb6 --- /dev/null +++ b/instrumentation/pekko/pekko-http-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/server/route/PathConcatenationInstrumentation.java @@ -0,0 +1,41 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.pekkohttp.v1_0.server.route; + +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 PathConcatenationInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher typeMatcher() { + return named("org.apache.pekko.http.scaladsl.server.PathMatcher"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + named("$anonfun$append$1"), this.getClass().getName() + "$ApplyAdvice"); + } + + @SuppressWarnings("unused") + public static class ApplyAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter() { + // https://github.com/apache/incubator-pekko-http/blob/bea7d2b5c21e23d55556409226d136c282da27a3/http/src/main/scala/org/apache/pekko/http/scaladsl/server/PathMatcher.scala#L53 + // https://github.com/apache/incubator-pekko-http/blob/bea7d2b5c21e23d55556409226d136c282da27a3/http/src/main/scala/org/apache/pekko/http/scaladsl/server/PathMatcher.scala#L57 + // when routing dsl uses path("path1" / "path2") we are concatenating 3 segments "path1" and / + // and "path2" we need to notify the matcher that a new segment has started, so it could be + // captured in the route + PekkoRouteHolder.startSegment(); + } + } +} diff --git a/instrumentation/pekko/pekko-http-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/server/route/PathMatcherInstrumentation.java b/instrumentation/pekko/pekko-http-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/server/route/PathMatcherInstrumentation.java new file mode 100644 index 000000000000..91c9366876a8 --- /dev/null +++ b/instrumentation/pekko/pekko-http-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/server/route/PathMatcherInstrumentation.java @@ -0,0 +1,47 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.pekkohttp.v1_0.server.route; + +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.returns; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import io.opentelemetry.instrumentation.api.util.VirtualField; +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.pekko.http.scaladsl.model.Uri; +import org.apache.pekko.http.scaladsl.server.PathMatcher; + +public class PathMatcherInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher typeMatcher() { + return named("org.apache.pekko.http.scaladsl.server.PathMatcher$"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + named("apply") + .and(takesArgument(0, named("org.apache.pekko.http.scaladsl.model.Uri$Path"))) + .and(returns(named("org.apache.pekko.http.scaladsl.server.PathMatcher"))), + this.getClass().getName() + "$ApplyAdvice"); + } + + @SuppressWarnings("unused") + public static class ApplyAdvice { + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void onEnter( + @Advice.Argument(0) Uri.Path prefix, @Advice.Return PathMatcher result) { + // store the path being matched inside a VirtualField on the given matcher, so it can be used + // for constructing the route + VirtualField.find(PathMatcher.class, String.class).set(result, prefix.toString()); + } + } +} diff --git a/instrumentation/pekko/pekko-http-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/server/route/PathMatcherStaticInstrumentation.java b/instrumentation/pekko/pekko-http-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/server/route/PathMatcherStaticInstrumentation.java new file mode 100644 index 000000000000..11e1da64cb55 --- /dev/null +++ b/instrumentation/pekko/pekko-http-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/server/route/PathMatcherStaticInstrumentation.java @@ -0,0 +1,67 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.pekkohttp.v1_0.server.route; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.extendsClass; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import io.opentelemetry.instrumentation.api.util.VirtualField; +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.pekko.http.scaladsl.model.Uri; +import org.apache.pekko.http.scaladsl.server.PathMatcher; +import org.apache.pekko.http.scaladsl.server.PathMatchers; +import org.apache.pekko.http.scaladsl.server.PathMatchers$; + +public class PathMatcherStaticInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher typeMatcher() { + return extendsClass(named("org.apache.pekko.http.scaladsl.server.PathMatcher")); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + named("apply") + .and(takesArgument(0, named("org.apache.pekko.http.scaladsl.model.Uri$Path"))), + this.getClass().getName() + "$ApplyAdvice"); + } + + @SuppressWarnings("unused") + public static class ApplyAdvice { + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void onExit( + @Advice.This PathMatcher pathMatcher, + @Advice.Argument(0) Uri.Path path, + @Advice.Return PathMatcher.Matching result) { + // result is either matched or unmatched, we only care about the matches + if (result.getClass() == PathMatcher.Matched.class) { + if (PathMatchers$.PathEnd$.class == pathMatcher.getClass()) { + PekkoRouteHolder.endMatched(); + return; + } + // if present use the matched path that was remembered in PathMatcherInstrumentation, + // otherwise just use a * + String prefix = VirtualField.find(PathMatcher.class, String.class).get(pathMatcher); + if (prefix == null) { + if (PathMatchers.Slash$.class == pathMatcher.getClass()) { + prefix = "/"; + } else { + prefix = "*"; + } + } + if (prefix != null) { + PekkoRouteHolder.push(prefix); + } + } + } + } +} diff --git a/instrumentation/pekko/pekko-http-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/server/route/PekkoHttpServerRouteInstrumentationModule.java b/instrumentation/pekko/pekko-http-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/server/route/PekkoHttpServerRouteInstrumentationModule.java new file mode 100644 index 000000000000..710d8c6616b6 --- /dev/null +++ b/instrumentation/pekko/pekko-http-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/server/route/PekkoHttpServerRouteInstrumentationModule.java @@ -0,0 +1,40 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.pekkohttp.v1_0.server.route; + +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; + +/** + * This instrumentation applies to classes in pekko-http.jar while + * PekkoHttpServerInstrumentationModule applies to classes in pekko-http-core.jar + */ +@AutoService(InstrumentationModule.class) +public class PekkoHttpServerRouteInstrumentationModule extends InstrumentationModule { + public PekkoHttpServerRouteInstrumentationModule() { + super("pekko-http", "pekko-http-1.0", "pekko-http-server", "pekko-http-server-route"); + } + + @Override + public boolean isIndyModule() { + // PekkoHttpServerInstrumentationModule and PekkoHttpServerRouteInstrumentationModule share + // PekkoRouteHolder class + return false; + } + + @Override + public List typeInstrumentations() { + return asList( + new PathMatcherInstrumentation(), + new PathMatcherStaticInstrumentation(), + new RouteConcatenationInstrumentation(), + new PathConcatenationInstrumentation()); + } +} diff --git a/instrumentation/pekko/pekko-http-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/server/route/PekkoRouteHolder.java b/instrumentation/pekko/pekko-http-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/server/route/PekkoRouteHolder.java new file mode 100644 index 000000000000..91ece866fdbd --- /dev/null +++ b/instrumentation/pekko/pekko-http-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/server/route/PekkoRouteHolder.java @@ -0,0 +1,87 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.pekkohttp.v1_0.server.route; + +import static io.opentelemetry.context.ContextKey.named; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.ContextKey; +import io.opentelemetry.context.ImplicitContextKeyed; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRoute; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRouteSource; +import java.util.ArrayDeque; +import java.util.Deque; + +public class PekkoRouteHolder implements ImplicitContextKeyed { + private static final ContextKey KEY = named("opentelemetry-pekko-route"); + + private String route = ""; + private boolean newSegment = true; + private boolean endMatched; + private final Deque stack = new ArrayDeque<>(); + + public static Context init(Context context) { + if (context.get(KEY) != null) { + return context; + } + return context.with(new PekkoRouteHolder()); + } + + public static void push(String path) { + PekkoRouteHolder holder = Context.current().get(KEY); + if (holder != null && holder.newSegment && !holder.endMatched) { + holder.route += path; + holder.newSegment = false; + } + } + + public static void startSegment() { + PekkoRouteHolder holder = Context.current().get(KEY); + if (holder != null) { + holder.newSegment = true; + } + } + + public static void endMatched() { + Context context = Context.current(); + PekkoRouteHolder holder = context.get(KEY); + if (holder != null) { + holder.endMatched = true; + HttpServerRoute.update(context, HttpServerRouteSource.CONTROLLER, holder.route); + } + } + + public static void save() { + PekkoRouteHolder holder = Context.current().get(KEY); + if (holder != null) { + holder.stack.push(holder.route); + holder.newSegment = true; + } + } + + public static void reset() { + PekkoRouteHolder holder = Context.current().get(KEY); + if (holder != null) { + holder.route = holder.stack.peek(); + holder.newSegment = true; + } + } + + public static void restore() { + PekkoRouteHolder holder = Context.current().get(KEY); + if (holder != null) { + holder.route = holder.stack.pop(); + holder.newSegment = true; + } + } + + @Override + public Context storeInContext(Context context) { + return context.with(KEY, this); + } + + private PekkoRouteHolder() {} +} diff --git a/instrumentation/pekko/pekko-http-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/server/route/RouteConcatenationInstrumentation.java b/instrumentation/pekko/pekko-http-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/server/route/RouteConcatenationInstrumentation.java new file mode 100644 index 000000000000..9a00975e6b8f --- /dev/null +++ b/instrumentation/pekko/pekko-http-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/server/route/RouteConcatenationInstrumentation.java @@ -0,0 +1,55 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.pekkohttp.v1_0.server.route; + +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 RouteConcatenationInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher typeMatcher() { + return named("org.apache.pekko.http.scaladsl.server.RouteConcatenation$RouteWithConcatenation"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + named("$anonfun$$tilde$1"), this.getClass().getName() + "$ApplyAdvice"); + transformer.applyAdviceToMethod( + named("$anonfun$$tilde$2"), this.getClass().getName() + "$Apply2Advice"); + } + + @SuppressWarnings("unused") + public static class ApplyAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter() { + // when routing dsl uses concat(path(...) {...}, path(...) {...}) we'll restore the currently + // matched route after each matcher so that match attempts that failed wouldn't get recorded + // in the route + PekkoRouteHolder.save(); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void onExit() { + PekkoRouteHolder.restore(); + } + } + + @SuppressWarnings("unused") + public static class Apply2Advice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter() { + PekkoRouteHolder.reset(); + } + } +} diff --git a/instrumentation/pekko/pekko-http-1.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/PekkoHttpServerJavaRouteTest.java b/instrumentation/pekko/pekko-http-1.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/PekkoHttpServerJavaRouteTest.java new file mode 100644 index 000000000000..bb239e4058c3 --- /dev/null +++ b/instrumentation/pekko/pekko-http-1.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/PekkoHttpServerJavaRouteTest.java @@ -0,0 +1,64 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.pekkohttp.v1_0; + +import static org.apache.pekko.http.javadsl.server.PathMatchers.integerSegment; +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.instrumentation.test.utils.PortUtils; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.testing.internal.armeria.client.WebClient; +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 java.util.concurrent.CompletionStage; +import org.apache.pekko.actor.ActorSystem; +import org.apache.pekko.http.javadsl.Http; +import org.apache.pekko.http.javadsl.ServerBinding; +import org.apache.pekko.http.javadsl.server.AllDirectives; +import org.apache.pekko.http.javadsl.server.Route; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +class PekkoHttpServerJavaRouteTest extends AllDirectives { + @RegisterExtension + private static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + private final WebClient client = WebClient.of(); + + @Test + void testRoute() { + ActorSystem system = ActorSystem.create("my-system"); + int port = PortUtils.findOpenPort(); + Http http = Http.get(system); + + Route route = + concat( + pathEndOrSingleSlash(() -> complete("root")), + pathPrefix( + "test", + () -> + concat( + pathSingleSlash(() -> complete("test")), + path(integerSegment(), (i) -> complete("ok"))))); + + CompletionStage binding = http.newServerAt("localhost", port).bind(route); + try { + AggregatedHttpRequest request = + AggregatedHttpRequest.of(HttpMethod.GET, "h1c://localhost:" + port + "/test/1"); + AggregatedHttpResponse response = client.execute(request).aggregate().join(); + + assertThat(response.status().code()).isEqualTo(200); + assertThat(response.contentUtf8()).isEqualTo("ok"); + + testing.waitAndAssertTraces( + trace -> trace.hasSpansSatisfyingExactly(span -> span.hasName("GET /test/*"))); + } finally { + binding.thenCompose(ServerBinding::unbind).thenAccept(unbound -> system.terminate()); + } + } +} diff --git a/instrumentation/pekko/pekko-http-1.0/javaagent/src/test/resources/application.conf b/instrumentation/pekko/pekko-http-1.0/javaagent/src/test/resources/application.conf new file mode 100644 index 000000000000..77b13890aee8 --- /dev/null +++ b/instrumentation/pekko/pekko-http-1.0/javaagent/src/test/resources/application.conf @@ -0,0 +1,11 @@ +pekko.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/instrumentation/pekko/pekko-http-1.0/javaagent/src/test/scala/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/AbstractHttpServerInstrumentationTest.scala b/instrumentation/pekko/pekko-http-1.0/javaagent/src/test/scala/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/AbstractHttpServerInstrumentationTest.scala new file mode 100644 index 000000000000..5d14a42a8521 --- /dev/null +++ b/instrumentation/pekko/pekko-http-1.0/javaagent/src/test/scala/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/AbstractHttpServerInstrumentationTest.scala @@ -0,0 +1,47 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.pekkohttp.v1_0 + +import io.opentelemetry.api.common.AttributeKey +import io.opentelemetry.instrumentation.testing.junit.http.{ + AbstractHttpServerTest, + HttpServerTestOptions, + ServerEndpoint +} +import io.opentelemetry.semconv.HttpAttributes + +import java.util +import java.util.Collections +import java.util.function.{Function, Predicate} + +abstract class AbstractHttpServerInstrumentationTest + extends AbstractHttpServerTest[Object] { + + override protected def configure( + options: HttpServerTestOptions + ): Unit = { + options.setTestCaptureHttpHeaders(false) + options.setHttpAttributes( + new Function[ServerEndpoint, util.Set[AttributeKey[_]]] { + override def apply(v1: ServerEndpoint): util.Set[AttributeKey[_]] = { + val set = new util.HashSet[AttributeKey[_]]( + HttpServerTestOptions.DEFAULT_HTTP_ATTRIBUTES + ) + set.remove(HttpAttributes.HTTP_ROUTE) + set + } + } + ) + options.setHasResponseCustomizer( + new Predicate[ServerEndpoint] { + override def test(t: ServerEndpoint): Boolean = + t != ServerEndpoint.EXCEPTION + } + ) + // instrumentation does not create a span at all + options.disableTestNonStandardHttpMethod + } +} diff --git a/instrumentation/pekko/pekko-http-1.0/javaagent/src/test/scala/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/PekkoHttpClientInstrumentationTest.scala b/instrumentation/pekko/pekko-http-1.0/javaagent/src/test/scala/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/PekkoHttpClientInstrumentationTest.scala new file mode 100644 index 000000000000..eef1b31345c0 --- /dev/null +++ b/instrumentation/pekko/pekko-http-1.0/javaagent/src/test/scala/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/PekkoHttpClientInstrumentationTest.scala @@ -0,0 +1,129 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.pekkohttp.v1_0 + +import org.apache.pekko.actor.ActorSystem +import org.apache.pekko.dispatch.ExecutionContexts +import org.apache.pekko.http.scaladsl.Http +import org.apache.pekko.http.scaladsl.model._ +import org.apache.pekko.http.javadsl.model.HttpHeader +import org.apache.pekko.http.scaladsl.settings.ClientConnectionSettings +import org.apache.pekko.http.scaladsl.model.headers.RawHeader +import org.apache.pekko.http.scaladsl.settings.ConnectionPoolSettings +import org.apache.pekko.stream.ActorMaterializer +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension +import io.opentelemetry.instrumentation.testing.junit.http.{ + AbstractHttpClientTest, + HttpClientInstrumentationExtension, + HttpClientResult, + HttpClientTestOptions, + SingleConnection +} + +import java.net.URI +import java.util +import java.util.concurrent.Executor +import org.junit.jupiter.api.extension.RegisterExtension + +import java.util.function.BiFunction +import scala.collection.JavaConverters._ +import scala.concurrent.Await +import scala.concurrent.ExecutionContext +import scala.concurrent.duration._ +import scala.language.postfixOps +import scala.util.{Failure, Success} + +class PekkoHttpClientInstrumentationTest + extends AbstractHttpClientTest[HttpRequest] { + + @RegisterExtension val extension: InstrumentationExtension = + HttpClientInstrumentationExtension.forAgent() + + val system: ActorSystem = ActorSystem.create() + implicit val materializer: ActorMaterializer = + ActorMaterializer.create(system) + + override def buildRequest( + method: String, + uri: URI, + h: util.Map[String, String] + ): HttpRequest = + HttpRequest(HttpMethods.getForKey(method).get) + .withUri(uri.toString) + .addHeaders( + h.entrySet() + .asScala + .map(e => RawHeader(e.getKey, e.getValue): HttpHeader) + .asJava + ) + + override def sendRequest( + request: HttpRequest, + method: String, + uri: URI, + headers: util.Map[String, String] + ): Int = { + val settings = ConnectionPoolSettings(system) + .withConnectionSettings( + ClientConnectionSettings(system) + .withConnectingTimeout( + FiniteDuration( + AbstractHttpClientTest.CONNECTION_TIMEOUT.toMillis, + MILLISECONDS + ) + ) + .withIdleTimeout( + FiniteDuration( + AbstractHttpClientTest.READ_TIMEOUT.toMillis, + MILLISECONDS + ) + ) + ) + val response = Await.result( + Http.get(system).singleRequest(request, settings = settings), + 10 seconds + ) + response.discardEntityBytes(materializer) + response.status.intValue() + } + + override def sendRequestWithCallback( + request: HttpRequest, + method: String, + uri: URI, + headers: util.Map[String, String], + requestResult: HttpClientResult + ): Unit = { + implicit val ec: ExecutionContext = + ExecutionContexts.fromExecutor(new Executor { + override def execute(command: Runnable): Unit = command.run() + }) + Http + .get(system) + .singleRequest(request) + .onComplete { + case Success(response: HttpResponse) => { + response.discardEntityBytes(materializer) + requestResult.complete(response.status.intValue()) + } + case Failure(error) => requestResult.complete(error) + } + } + + override protected def configure( + options: HttpClientTestOptions.Builder + ): Unit = { + options.disableTestRedirects() + options.disableTestNonStandardHttpMethod() + // singleConnection test would require instrumentation to support requests made through pools + // (newHostConnectionPool, superPool, etc), which is currently not supported. + options.setSingleConnectionFactory( + new BiFunction[String, Integer, SingleConnection] { + override def apply(t: String, u: Integer): SingleConnection = null + } + ) + } +} diff --git a/instrumentation/pekko/pekko-http-1.0/javaagent/src/test/scala/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/PekkoHttpServerInstrumentationTest.scala b/instrumentation/pekko/pekko-http-1.0/javaagent/src/test/scala/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/PekkoHttpServerInstrumentationTest.scala new file mode 100644 index 000000000000..92558e96c7a9 --- /dev/null +++ b/instrumentation/pekko/pekko-http-1.0/javaagent/src/test/scala/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/PekkoHttpServerInstrumentationTest.scala @@ -0,0 +1,109 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.pekkohttp.v1_0 + +import io.opentelemetry.api.common.AttributeKey +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension +import io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.SUCCESS +import io.opentelemetry.instrumentation.testing.junit.http.{ + HttpServerInstrumentationExtension, + HttpServerTestOptions, + ServerEndpoint +} +import io.opentelemetry.sdk.testing.assertj.{SpanDataAssert, TraceAssert} +import io.opentelemetry.testing.internal.armeria.common.{ + AggregatedHttpRequest, + HttpMethod +} +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.RegisterExtension + +import java.util +import java.util.function.{BiFunction, Consumer, Function} + +class PekkoHttpServerInstrumentationTest + extends AbstractHttpServerInstrumentationTest { + @RegisterExtension val extension: InstrumentationExtension = + HttpServerInstrumentationExtension.forAgent() + + override protected def setupServer(): AnyRef = { + PekkoHttpTestWebServer.start(port) + null + } + + override protected def stopServer(server: Object): Unit = + PekkoHttpTestWebServer.stop() + + override protected def configure( + options: HttpServerTestOptions + ): Unit = { + super.configure(options) + // exception doesn't propagate + options.setTestException(false) + options.setTestPathParam(true) + + options.setHttpAttributes( + new Function[ServerEndpoint, util.Set[AttributeKey[_]]] { + override def apply(v1: ServerEndpoint): util.Set[AttributeKey[_]] = { + HttpServerTestOptions.DEFAULT_HTTP_ATTRIBUTES + } + } + ) + + val expectedRoute = new BiFunction[ServerEndpoint, String, String] { + def apply(endpoint: ServerEndpoint, method: String): String = { + if (endpoint eq ServerEndpoint.PATH_PARAM) + return "/path/*/param" + expectedHttpRoute(endpoint, method) + } + } + options.setExpectedHttpRoute(expectedRoute) + } + + @Test def testPathMatchers(): Unit = { + // /test1 / IntNumber / HexIntNumber / LongNumber / HexLongNumber / DoubleNumber / JavaUUID / Remaining + val request = AggregatedHttpRequest.of( + HttpMethod.GET, + address + .resolve( + "/test1/1/a1/2/b2/3.0/e58ed763-928c-4155-bee9-fdbaaadc15f3/remaining" + ) + .toString + ) + val response = client.execute(request).aggregate.join + assertThat(response.status.code).isEqualTo(SUCCESS.getStatus) + assertThat(response.contentUtf8).isEqualTo(SUCCESS.getBody) + + testing.waitAndAssertTraces(new Consumer[TraceAssert] { + override def accept(trace: TraceAssert): Unit = + trace.hasSpansSatisfyingExactly(new Consumer[SpanDataAssert] { + override def accept(span: SpanDataAssert): Unit = { + span.hasName("GET /test1/*/*/*/*/*/*/*") + } + }) + }) + } + + @Test def testConcat(): Unit = { + val request = AggregatedHttpRequest.of( + HttpMethod.GET, + address.resolve("/test2/second").toString + ) + val response = client.execute(request).aggregate.join + assertThat(response.status.code).isEqualTo(SUCCESS.getStatus) + assertThat(response.contentUtf8).isEqualTo(SUCCESS.getBody) + + testing.waitAndAssertTraces(new Consumer[TraceAssert] { + override def accept(trace: TraceAssert): Unit = + trace.hasSpansSatisfyingExactly(new Consumer[SpanDataAssert] { + override def accept(span: SpanDataAssert): Unit = { + span.hasName("GET /test2/second") + } + }) + }) + } +} diff --git a/instrumentation/pekko/pekko-http-1.0/javaagent/src/test/scala/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/PekkoHttpServerInstrumentationTestAsync.scala b/instrumentation/pekko/pekko-http-1.0/javaagent/src/test/scala/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/PekkoHttpServerInstrumentationTestAsync.scala new file mode 100644 index 000000000000..a0789cc7d10d --- /dev/null +++ b/instrumentation/pekko/pekko-http-1.0/javaagent/src/test/scala/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/PekkoHttpServerInstrumentationTestAsync.scala @@ -0,0 +1,35 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.pekkohttp.v1_0 + +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension +import io.opentelemetry.instrumentation.testing.junit.http.{ + HttpServerInstrumentationExtension, + HttpServerTestOptions +} +import org.junit.jupiter.api.extension.RegisterExtension + +class PekkoHttpServerInstrumentationTestAsync + extends AbstractHttpServerInstrumentationTest { + + @RegisterExtension val extension: InstrumentationExtension = + HttpServerInstrumentationExtension.forAgent() + + override protected def setupServer(): AnyRef = { + PekkoHttpTestAsyncWebServer.start(port) + null + } + + override protected def stopServer(server: Object): Unit = + PekkoHttpTestAsyncWebServer.stop() + + override protected def configure( + options: HttpServerTestOptions + ): Unit = { + super.configure(options) + options.setTestHttpPipelining(false) + } +} diff --git a/instrumentation/pekko/pekko-http-1.0/javaagent/src/test/scala/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/PekkoHttpServerInstrumentationTestSync.scala b/instrumentation/pekko/pekko-http-1.0/javaagent/src/test/scala/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/PekkoHttpServerInstrumentationTestSync.scala new file mode 100644 index 000000000000..0abd4ee1aa05 --- /dev/null +++ b/instrumentation/pekko/pekko-http-1.0/javaagent/src/test/scala/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/PekkoHttpServerInstrumentationTestSync.scala @@ -0,0 +1,36 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.pekkohttp.v1_0 + +import io.opentelemetry.instrumentation.testing.junit.http.{ + HttpServerInstrumentationExtension, + HttpServerTestOptions +} +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension +import org.junit.jupiter.api.extension.RegisterExtension + +class PekkoHttpServerInstrumentationTestSync + extends AbstractHttpServerInstrumentationTest { + + @RegisterExtension val extension: InstrumentationExtension = + HttpServerInstrumentationExtension.forAgent() + + override protected def setupServer(): AnyRef = { + PekkoHttpTestSyncWebServer.start(port) + null + } + + override protected def stopServer(server: Object): Unit = + PekkoHttpTestSyncWebServer.stop() + + override protected def configure( + options: HttpServerTestOptions + ): Unit = { + super.configure(options) + // FIXME: latest deps does not fill http.status_code + options.setTestException(!java.lang.Boolean.getBoolean("testLatestDeps")) + } +} diff --git a/instrumentation/pekko/pekko-http-1.0/javaagent/src/test/scala/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/PekkoHttpServerRouteTest.scala b/instrumentation/pekko/pekko-http-1.0/javaagent/src/test/scala/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/PekkoHttpServerRouteTest.scala new file mode 100644 index 000000000000..e01f6eee0460 --- /dev/null +++ b/instrumentation/pekko/pekko-http-1.0/javaagent/src/test/scala/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/PekkoHttpServerRouteTest.scala @@ -0,0 +1,113 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.pekkohttp.v1_0 + +import io.opentelemetry.instrumentation.test.utils.PortUtils +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension +import io.opentelemetry.sdk.testing.assertj.{SpanDataAssert, TraceAssert} +import io.opentelemetry.testing.internal.armeria.client.WebClient +import io.opentelemetry.testing.internal.armeria.common.{ + AggregatedHttpRequest, + HttpMethod +} +import org.apache.pekko.actor.ActorSystem +import org.apache.pekko.http.scaladsl.Http +import org.apache.pekko.http.scaladsl.server.Route +import org.apache.pekko.http.scaladsl.server.Directives.{ + IntNumber, + complete, + concat, + path, + pathEndOrSingleSlash, + pathPrefix, + pathSingleSlash +} +import org.apache.pekko.stream.ActorMaterializer +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.extension.RegisterExtension +import org.junit.jupiter.api.{AfterAll, Test, TestInstance} + +import java.net.{URI, URISyntaxException} +import java.util.function.Consumer +import scala.concurrent.Await +import scala.concurrent.duration.DurationInt + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class PekkoHttpServerRouteTest { + @RegisterExtension private val testing: AgentInstrumentationExtension = + AgentInstrumentationExtension.create + private val client: WebClient = WebClient.of() + + implicit val system: ActorSystem = ActorSystem("my-system") + implicit val materializer: ActorMaterializer = ActorMaterializer() + + private def buildAddress(port: Int): URI = try + new URI("http://localhost:" + port + "/") + catch { + case exception: URISyntaxException => + throw new IllegalStateException(exception) + } + + @Test def testSimple(): Unit = { + val route = path("test") { + complete("ok") + } + + test(route, "/test", "GET /test") + } + + @Test def testRoute(): Unit = { + val route = concat( + pathEndOrSingleSlash { + complete("root") + }, + pathPrefix("test") { + concat( + pathSingleSlash { + complete("test") + }, + path(IntNumber) { _ => + complete("ok") + } + ) + } + ) + + test(route, "/test/1", "GET /test/*") + } + + def test(route: Route, path: String, spanName: String): Unit = { + val port = PortUtils.findOpenPort + val address: URI = buildAddress(port) + val binding = + Await.result(Http().bindAndHandle(route, "localhost", port), 10.seconds) + try { + val request = AggregatedHttpRequest.of( + HttpMethod.GET, + address.resolve(path).toString + ) + val response = client.execute(request).aggregate.join + assertThat(response.status.code).isEqualTo(200) + assertThat(response.contentUtf8).isEqualTo("ok") + + testing.waitAndAssertTraces(new Consumer[TraceAssert] { + override def accept(trace: TraceAssert): Unit = + trace.hasSpansSatisfyingExactly(new Consumer[SpanDataAssert] { + override def accept(span: SpanDataAssert): Unit = { + span.hasName(spanName) + } + }) + }) + } finally { + binding.unbind() + } + } + + @AfterAll + def cleanUp(): Unit = { + system.terminate() + } +} diff --git a/instrumentation/pekko/pekko-http-1.0/javaagent/src/test/scala/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/PekkoHttpTestAsyncWebServer.scala b/instrumentation/pekko/pekko-http-1.0/javaagent/src/test/scala/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/PekkoHttpTestAsyncWebServer.scala new file mode 100644 index 000000000000..1c1fe2bf2516 --- /dev/null +++ b/instrumentation/pekko/pekko-http-1.0/javaagent/src/test/scala/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/PekkoHttpTestAsyncWebServer.scala @@ -0,0 +1,81 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.pekkohttp.v1_0 + +import org.apache.pekko.actor.ActorSystem +import org.apache.pekko.http.scaladsl.Http +import org.apache.pekko.http.scaladsl.Http.ServerBinding +import org.apache.pekko.http.scaladsl.model.HttpMethods.GET +import org.apache.pekko.http.scaladsl.model._ +import org.apache.pekko.stream.ActorMaterializer +import io.opentelemetry.instrumentation.testing.junit.http.{ + AbstractHttpServerTest, + ServerEndpoint +} +import io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint._ + +import java.util.function.Supplier +import scala.concurrent.{Await, ExecutionContextExecutor, Future} + +object PekkoHttpTestAsyncWebServer { + 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 = ServerEndpoint.forPath(uri.path.toString()) + AbstractHttpServerTest.controller( + endpoint, + new Supplier[HttpResponse] { + def get(): 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/instrumentation/pekko/pekko-http-1.0/javaagent/src/test/scala/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/PekkoHttpTestServerSourceWebServer.scala b/instrumentation/pekko/pekko-http-1.0/javaagent/src/test/scala/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/PekkoHttpTestServerSourceWebServer.scala new file mode 100644 index 000000000000..cf57f7ea4575 --- /dev/null +++ b/instrumentation/pekko/pekko-http-1.0/javaagent/src/test/scala/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/PekkoHttpTestServerSourceWebServer.scala @@ -0,0 +1,124 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.pekkohttp.v1_0 + +import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpServerTest +import io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint._ +import org.apache.pekko.actor.ActorSystem +import org.apache.pekko.http.scaladsl.Http +import org.apache.pekko.http.scaladsl.Http.ServerBinding +import org.apache.pekko.http.scaladsl.model.StatusCodes.Found +import org.apache.pekko.http.scaladsl.server.Directives._ +import org.apache.pekko.stream.ActorMaterializer +import org.apache.pekko.stream.scaladsl.Sink + +import java.util.function.Supplier +import scala.concurrent.Await + +object PekkoHttpTestServerSourceWebServer { + implicit val system = ActorSystem("my-system") + implicit val materializer = ActorMaterializer() + // needed for the future flatMap/onComplete in the end + implicit val executionContext = system.dispatcher + + var route = get { + concat( + path(SUCCESS.rawPath()) { + complete( + AbstractHttpServerTest.controller(SUCCESS, supplier(SUCCESS.getBody)) + ) + }, + path(INDEXED_CHILD.rawPath()) { + parameterMap { map => + val supplier = new Supplier[String] { + def get(): String = { + INDEXED_CHILD.collectSpanAttributes(new UrlParameterProvider { + override def getParameter(name: String): String = + map.get(name).orNull + }) + "" + } + } + complete(AbstractHttpServerTest.controller(INDEXED_CHILD, supplier)) + } + }, + path(QUERY_PARAM.rawPath()) { + extractUri { uri => + complete( + AbstractHttpServerTest + .controller(INDEXED_CHILD, supplier(uri.queryString().orNull)) + ) + } + }, + path(REDIRECT.rawPath()) { + redirect( + AbstractHttpServerTest + .controller(REDIRECT, supplier(REDIRECT.getBody)), + Found + ) + }, + path(ERROR.rawPath()) { + complete( + 500 -> AbstractHttpServerTest + .controller(ERROR, supplier(ERROR.getBody)) + ) + }, + path("path" / LongNumber / "param") { id => + complete( + AbstractHttpServerTest.controller(PATH_PARAM, supplier(id.toString)) + ) + }, + path( + "test1" / IntNumber / HexIntNumber / LongNumber / HexLongNumber / + DoubleNumber / JavaUUID / Remaining + ) { (_, _, _, _, _, _, _) => + complete(SUCCESS.getBody) + }, + pathPrefix("test2") { + concat( + path("first") { + complete(SUCCESS.getBody) + }, + path("second") { + complete(SUCCESS.getBody) + } + ) + } + ) + } + + private var binding: ServerBinding = null + + def start(port: Int): Unit = synchronized { + if (null == binding) { + import scala.concurrent.duration._ + binding = Await.result( + Http() + .bind("localhost", port) + .map(_.handleWith(route)) + .to(Sink.ignore) + .run(), + 10.seconds + ) + } + } + + def stop(): Unit = synchronized { + if (null != binding) { + binding.unbind() + system.terminate() + binding = null + } + } + + def supplier(string: String): Supplier[String] = { + new Supplier[String] { + def get(): String = { + string + } + } + } +} diff --git a/instrumentation/pekko/pekko-http-1.0/javaagent/src/test/scala/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/PekkoHttpTestSyncWebServer.scala b/instrumentation/pekko/pekko-http-1.0/javaagent/src/test/scala/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/PekkoHttpTestSyncWebServer.scala new file mode 100644 index 000000000000..9e129268f877 --- /dev/null +++ b/instrumentation/pekko/pekko-http-1.0/javaagent/src/test/scala/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/PekkoHttpTestSyncWebServer.scala @@ -0,0 +1,78 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.pekkohttp.v1_0 + +import org.apache.pekko.actor.ActorSystem +import org.apache.pekko.http.scaladsl.Http +import org.apache.pekko.http.scaladsl.Http.ServerBinding +import org.apache.pekko.http.scaladsl.model.HttpMethods.GET +import org.apache.pekko.http.scaladsl.model._ +import org.apache.pekko.stream.ActorMaterializer +import io.opentelemetry.instrumentation.testing.junit.http.{ + AbstractHttpServerTest, + ServerEndpoint +} +import io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint._ + +import java.util.function.Supplier +import scala.concurrent.Await + +object PekkoHttpTestSyncWebServer { + 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 = ServerEndpoint.forPath(uri.path.toString()) + AbstractHttpServerTest.controller( + endpoint, + new Supplier[HttpResponse] { + def get(): 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/instrumentation/pekko/pekko-http-1.0/javaagent/src/test/scala/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/PekkoHttpTestWebServer.scala b/instrumentation/pekko/pekko-http-1.0/javaagent/src/test/scala/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/PekkoHttpTestWebServer.scala new file mode 100644 index 000000000000..32fd00bf6833 --- /dev/null +++ b/instrumentation/pekko/pekko-http-1.0/javaagent/src/test/scala/io/opentelemetry/javaagent/instrumentation/pekkohttp/v1_0/PekkoHttpTestWebServer.scala @@ -0,0 +1,117 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.pekkohttp.v1_0 + +import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpServerTest +import io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint._ +import org.apache.pekko.actor.ActorSystem +import org.apache.pekko.http.scaladsl.Http +import org.apache.pekko.http.scaladsl.Http.ServerBinding +import org.apache.pekko.http.scaladsl.model.StatusCodes.Found +import org.apache.pekko.http.scaladsl.server.Directives._ +import org.apache.pekko.stream.ActorMaterializer + +import java.util.function.Supplier +import scala.concurrent.Await + +object PekkoHttpTestWebServer { + implicit val system = ActorSystem("my-system") + implicit val materializer = ActorMaterializer() + // needed for the future flatMap/onComplete in the end + implicit val executionContext = system.dispatcher + + var route = get { + concat( + path(SUCCESS.rawPath()) { + complete( + AbstractHttpServerTest.controller(SUCCESS, supplier(SUCCESS.getBody)) + ) + }, + path(INDEXED_CHILD.rawPath()) { + parameterMap { map => + val supplier = new Supplier[String] { + def get(): String = { + INDEXED_CHILD.collectSpanAttributes(new UrlParameterProvider { + override def getParameter(name: String): String = + map.get(name).orNull + }) + "" + } + } + complete(AbstractHttpServerTest.controller(INDEXED_CHILD, supplier)) + } + }, + path(QUERY_PARAM.rawPath()) { + extractUri { uri => + complete( + AbstractHttpServerTest + .controller(INDEXED_CHILD, supplier(uri.queryString().orNull)) + ) + } + }, + path(REDIRECT.rawPath()) { + redirect( + AbstractHttpServerTest + .controller(REDIRECT, supplier(REDIRECT.getBody)), + Found + ) + }, + path(ERROR.rawPath()) { + complete( + 500 -> AbstractHttpServerTest + .controller(ERROR, supplier(ERROR.getBody)) + ) + }, + path("path" / LongNumber / "param") { id => + complete( + AbstractHttpServerTest.controller(PATH_PARAM, supplier(id.toString)) + ) + }, + path( + "test1" / IntNumber / HexIntNumber / LongNumber / HexLongNumber / + DoubleNumber / JavaUUID / Remaining + ) { (_, _, _, _, _, _, _) => + complete(SUCCESS.getBody) + }, + pathPrefix("test2") { + concat( + path("first") { + complete(SUCCESS.getBody) + }, + path("second") { + complete(SUCCESS.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 + } + } + + def supplier(string: String): Supplier[String] = { + new Supplier[String] { + def get(): String = { + string + } + } + } +} diff --git a/instrumentation/play/play-mvc/play-mvc-2.4/javaagent/build.gradle.kts b/instrumentation/play/play-mvc/play-mvc-2.4/javaagent/build.gradle.kts index 599b5b4efd64..0d046f74fbdf 100644 --- a/instrumentation/play/play-mvc/play-mvc-2.4/javaagent/build.gradle.kts +++ b/instrumentation/play/play-mvc/play-mvc-2.4/javaagent/build.gradle.kts @@ -85,3 +85,6 @@ if (!(findProperty("testLatestDeps") as Boolean)) { } } } +tasks.withType().configureEach { + jvmArgs("-Dotel.instrumentation.common.experimental.controller-telemetry.enabled=true") +} diff --git a/instrumentation/play/play-mvc/play-mvc-2.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/play/v2_4/ActionInstrumentation.java b/instrumentation/play/play-mvc/play-mvc-2.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/play/v2_4/ActionInstrumentation.java index 191cc6f838f1..4d376817c6f0 100644 --- a/instrumentation/play/play-mvc/play-mvc-2.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/play/v2_4/ActionInstrumentation.java +++ b/instrumentation/play/play-mvc/play-mvc-2.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/play/v2_4/ActionInstrumentation.java @@ -71,6 +71,9 @@ public static void stopTraceOnResponse( @Advice.Return(readOnly = false) Future responseFuture, @Advice.Local("otelContext") Context context, @Advice.Local("otelScope") Scope scope) { + if (scope == null) { + return; + } scope.close(); updateSpan(context, req); diff --git a/instrumentation/play/play-mvc/play-mvc-2.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/play/v2_4/Play24Singletons.java b/instrumentation/play/play-mvc/play-mvc-2.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/play/v2_4/Play24Singletons.java index 62c62af1c034..26c9581ddb36 100644 --- a/instrumentation/play/play-mvc/play-mvc-2.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/play/v2_4/Play24Singletons.java +++ b/instrumentation/play/play-mvc/play-mvc-2.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/play/v2_4/Play24Singletons.java @@ -9,8 +9,9 @@ import io.opentelemetry.api.trace.Span; import io.opentelemetry.context.Context; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteHolder; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteSource; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRoute; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRouteSource; +import io.opentelemetry.javaagent.bootstrap.internal.ExperimentalConfig; import play.api.mvc.Request; import scala.Option; @@ -20,6 +21,7 @@ public final class Play24Singletons { private static final Instrumenter INSTRUMENTER = Instrumenter.builder( GlobalOpenTelemetry.get(), "io.opentelemetry.play-mvc-2.4", s -> SPAN_NAME) + .setEnabled(ExperimentalConfig.get().controllerTelemetryEnabled()) .buildInstrumenter(); public static Instrumenter instrumenter() { @@ -33,7 +35,7 @@ public static void updateSpan(Context context, Request request) { } Span.fromContext(context).updateName(route); - HttpRouteHolder.updateHttpRoute(context, HttpRouteSource.CONTROLLER, route); + HttpServerRoute.update(context, HttpServerRouteSource.CONTROLLER, route); } private static String getRoute(Request request) { diff --git a/instrumentation/play/play-mvc/play-mvc-2.4/javaagent/src/play24Test/groovy/client/PlayWsClientTest.groovy b/instrumentation/play/play-mvc/play-mvc-2.4/javaagent/src/play24Test/groovy/client/PlayWsClientTest.groovy index 01c2431a8e1f..1cfe325aca58 100644 --- a/instrumentation/play/play-mvc/play-mvc-2.4/javaagent/src/play24Test/groovy/client/PlayWsClientTest.groovy +++ b/instrumentation/play/play-mvc/play-mvc-2.4/javaagent/src/play24Test/groovy/client/PlayWsClientTest.groovy @@ -10,6 +10,7 @@ import io.opentelemetry.instrumentation.test.AgentTestTrait import io.opentelemetry.instrumentation.test.base.HttpClientTest import io.opentelemetry.instrumentation.testing.junit.http.HttpClientResult import io.opentelemetry.instrumentation.testing.junit.http.SingleConnection +import io.opentelemetry.semconv.NetworkAttributes import play.libs.ws.WS import play.libs.ws.WSRequest import play.libs.ws.WSResponse @@ -20,8 +21,6 @@ import spock.lang.Subject import java.util.concurrent.CompletableFuture import java.util.concurrent.CompletionStage -import static io.opentelemetry.api.common.AttributeKey.stringKey - class PlayWsClientTest extends HttpClientTest implements AgentTestTrait { @Subject @Shared @@ -80,8 +79,7 @@ class PlayWsClientTest extends HttpClientTest implements AgentTestTra @Override Set> httpAttributes(URI uri) { def attributes = super.httpAttributes(uri) - attributes.remove(stringKey("net.protocol.name")) - attributes.remove(stringKey("net.protocol.version")) + attributes.remove(NetworkAttributes.NETWORK_PROTOCOL_VERSION) attributes } diff --git a/instrumentation/play/play-mvc/play-mvc-2.4/javaagent/src/play24Test/groovy/server/PlayServerTest.groovy b/instrumentation/play/play-mvc/play-mvc-2.4/javaagent/src/play24Test/groovy/server/PlayServerTest.groovy index 2a9efd97f8f1..69383be02d11 100644 --- a/instrumentation/play/play-mvc/play-mvc-2.4/javaagent/src/play24Test/groovy/server/PlayServerTest.groovy +++ b/instrumentation/play/play-mvc/play-mvc-2.4/javaagent/src/play24Test/groovy/server/PlayServerTest.groovy @@ -12,7 +12,7 @@ import io.opentelemetry.instrumentation.test.asserts.TraceAssert import io.opentelemetry.instrumentation.test.base.HttpServerTest import io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint import io.opentelemetry.sdk.trace.data.SpanData -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import io.opentelemetry.semconv.HttpAttributes import play.libs.F.Function0 import play.mvc.Results import play.routing.RoutingDsl @@ -117,7 +117,12 @@ class PlayServerTest extends HttpServerTest implements AgentTestTrait { @Override Set> httpAttributes(ServerEndpoint endpoint) { def attributes = super.httpAttributes(endpoint) - attributes.remove(SemanticAttributes.HTTP_ROUTE) + attributes.remove(HttpAttributes.HTTP_ROUTE) attributes } + + @Override + int getResponseCodeOnNonStandardHttpMethod() { + 404 + } } diff --git a/instrumentation/play/play-mvc/play-mvc-2.4/javaagent/src/test/groovy/client/PlayWsClientTest.groovy b/instrumentation/play/play-mvc/play-mvc-2.4/javaagent/src/test/groovy/client/PlayWsClientTest.groovy index 324c959945bf..4d10e1ed4fbc 100644 --- a/instrumentation/play/play-mvc/play-mvc-2.4/javaagent/src/test/groovy/client/PlayWsClientTest.groovy +++ b/instrumentation/play/play-mvc/play-mvc-2.4/javaagent/src/test/groovy/client/PlayWsClientTest.groovy @@ -5,12 +5,11 @@ 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.testing.junit.http.HttpClientResult import io.opentelemetry.instrumentation.testing.junit.http.SingleConnection -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes import play.libs.ws.WS import play.libs.ws.WSRequest import play.libs.ws.WSResponse @@ -53,11 +52,6 @@ class PlayWsClientTest extends HttpClientTest implements AgentTestTra return request.execute(method) } - @Override - String userAgent() { - return "AHC" - } - @Override boolean testRedirects() { false @@ -68,15 +62,6 @@ class PlayWsClientTest extends HttpClientTest implements AgentTestTra return 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 diff --git a/instrumentation/play/play-mvc/play-mvc-2.4/javaagent/src/test/groovy/server/PlayServerTest.groovy b/instrumentation/play/play-mvc/play-mvc-2.4/javaagent/src/test/groovy/server/PlayServerTest.groovy index 9c6bcecb764c..101890bd7068 100644 --- a/instrumentation/play/play-mvc/play-mvc-2.4/javaagent/src/test/groovy/server/PlayServerTest.groovy +++ b/instrumentation/play/play-mvc/play-mvc-2.4/javaagent/src/test/groovy/server/PlayServerTest.groovy @@ -12,7 +12,7 @@ import io.opentelemetry.instrumentation.test.asserts.TraceAssert import io.opentelemetry.instrumentation.test.base.HttpServerTest import io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint import io.opentelemetry.sdk.trace.data.SpanData -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import io.opentelemetry.semconv.HttpAttributes import play.mvc.Results import play.routing.RoutingDsl import play.server.Server @@ -112,7 +112,12 @@ class PlayServerTest extends HttpServerTest implements AgentTestTrait { @Override Set> httpAttributes(ServerEndpoint endpoint) { def attributes = super.httpAttributes(endpoint) - attributes.remove(SemanticAttributes.HTTP_ROUTE) + attributes.remove(HttpAttributes.HTTP_ROUTE) attributes } + + @Override + int getResponseCodeOnNonStandardHttpMethod() { + 404 + } } diff --git a/instrumentation/play/play-mvc/play-mvc-2.6/javaagent/build.gradle.kts b/instrumentation/play/play-mvc/play-mvc-2.6/javaagent/build.gradle.kts index b8f11ca2edaa..b310b68ac5f5 100644 --- a/instrumentation/play/play-mvc/play-mvc-2.6/javaagent/build.gradle.kts +++ b/instrumentation/play/play-mvc/play-mvc-2.6/javaagent/build.gradle.kts @@ -80,15 +80,6 @@ tasks { configurations.configureEach { exclude("org.eclipse.jetty.websocket", "websocket-client") } - -// com.fasterxml.jackson.module:jackson-module-scala_2.13:2.15.2 is missing force using jackson 2.15.1 -// remove this when a new version of jackson is released -configurations.configureEach { - resolutionStrategy { - eachDependency { - if (requested.group == "com.fasterxml.jackson" && requested.name == "jackson-bom" && requested.version == "2.15.2") { - useVersion("2.15.1") - } - } - } +tasks.withType().configureEach { + jvmArgs("-Dotel.instrumentation.common.experimental.controller-telemetry.enabled=true") } diff --git a/instrumentation/play/play-mvc/play-mvc-2.6/javaagent/src/latestDepTest/groovy/server/PlayServerTest.groovy b/instrumentation/play/play-mvc/play-mvc-2.6/javaagent/src/latestDepTest/groovy/server/PlayServerTest.groovy index a377b8f87e73..2119ec65217a 100644 --- a/instrumentation/play/play-mvc/play-mvc-2.6/javaagent/src/latestDepTest/groovy/server/PlayServerTest.groovy +++ b/instrumentation/play/play-mvc/play-mvc-2.6/javaagent/src/latestDepTest/groovy/server/PlayServerTest.groovy @@ -92,6 +92,12 @@ class PlayServerTest extends HttpServerTest implements AgentTestTrait { false } + // play does not emit a span at all when a non standard HTTP method is used + @Override + boolean testNonStandardHttpMethod() { + false + } + @Override void handlerSpan(TraceAssert trace, int index, Object parent, String method = "GET", ServerEndpoint endpoint = SUCCESS) { trace.span(index) { diff --git a/instrumentation/play/play-mvc/play-mvc-2.6/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/play/v2_6/ActionInstrumentation.java b/instrumentation/play/play-mvc/play-mvc-2.6/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/play/v2_6/ActionInstrumentation.java index 3e0a7bca0de5..dd82526a5875 100644 --- a/instrumentation/play/play-mvc/play-mvc-2.6/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/play/v2_6/ActionInstrumentation.java +++ b/instrumentation/play/play-mvc/play-mvc-2.6/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/play/v2_6/ActionInstrumentation.java @@ -71,6 +71,9 @@ public static void stopTraceOnResponse( @Advice.Return(readOnly = false) Future responseFuture, @Advice.Local("otelContext") Context context, @Advice.Local("otelScope") Scope scope) { + if (scope == null) { + return; + } scope.close(); updateSpan(context, req); diff --git a/instrumentation/play/play-mvc/play-mvc-2.6/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/play/v2_6/Play26Singletons.java b/instrumentation/play/play-mvc/play-mvc-2.6/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/play/v2_6/Play26Singletons.java index 40c5e8857ad2..716019ca599c 100644 --- a/instrumentation/play/play-mvc/play-mvc-2.6/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/play/v2_6/Play26Singletons.java +++ b/instrumentation/play/play-mvc/play-mvc-2.6/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/play/v2_6/Play26Singletons.java @@ -9,8 +9,8 @@ import io.opentelemetry.api.trace.Span; import io.opentelemetry.context.Context; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteHolder; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteSource; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRoute; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRouteSource; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import javax.annotation.Nullable; @@ -60,7 +60,7 @@ public static void updateSpan(Context context, Request request) { } Span.fromContext(context).updateName(route); - HttpRouteHolder.updateHttpRoute(context, HttpRouteSource.CONTROLLER, route); + HttpServerRoute.update(context, HttpServerRouteSource.CONTROLLER, route); } private static String getRoute(Request request) { diff --git a/instrumentation/play/play-mvc/play-mvc-2.6/javaagent/src/test/groovy/server/PlayServerTest.groovy b/instrumentation/play/play-mvc/play-mvc-2.6/javaagent/src/test/groovy/server/PlayServerTest.groovy index 0bc7c3802b41..9f3ab3cb7759 100644 --- a/instrumentation/play/play-mvc/play-mvc-2.6/javaagent/src/test/groovy/server/PlayServerTest.groovy +++ b/instrumentation/play/play-mvc/play-mvc-2.6/javaagent/src/test/groovy/server/PlayServerTest.groovy @@ -105,4 +105,10 @@ class PlayServerTest extends HttpServerTest implements AgentTestTrait { Set> httpAttributes(ServerEndpoint endpoint) { [] } + + @Override + boolean testNonStandardHttpMethod() { + // instrumentation does not create a server span at all + false + } } diff --git a/instrumentation/play/play-ws/play-ws-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/playws/PlayWsClientHttpAttributesGetter.java b/instrumentation/play/play-ws/play-ws-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/playws/PlayWsClientHttpAttributesGetter.java index 7e6670cabae3..e5228fadf0f0 100644 --- a/instrumentation/play/play-ws/play-ws-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/playws/PlayWsClientHttpAttributesGetter.java +++ b/instrumentation/play/play-ws/play-ws-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/playws/PlayWsClientHttpAttributesGetter.java @@ -5,7 +5,8 @@ package io.opentelemetry.javaagent.instrumentation.playws; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpClientAttributesGetter; +import io.opentelemetry.instrumentation.api.semconv.http.HttpClientAttributesGetter; +import java.net.InetSocketAddress; import java.util.List; import javax.annotation.Nullable; import play.shaded.ahc.org.asynchttpclient.Request; @@ -39,4 +40,25 @@ public Integer getHttpResponseStatusCode( public List getHttpResponseHeader(Request request, Response response, String name) { return response.getHeaders().getAll(name); } + + @Nullable + @Override + public String getServerAddress(Request request) { + return request.getUri().getHost(); + } + + @Override + public Integer getServerPort(Request request) { + return request.getUri().getPort(); + } + + @Override + @Nullable + public InetSocketAddress getNetworkPeerInetSocketAddress( + Request request, @Nullable Response response) { + if (response != null && response.getRemoteAddress() instanceof InetSocketAddress) { + return (InetSocketAddress) response.getRemoteAddress(); + } + return null; + } } diff --git a/instrumentation/play/play-ws/play-ws-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/playws/PlayWsClientInstrumenterFactory.java b/instrumentation/play/play-ws/play-ws-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/playws/PlayWsClientInstrumenterFactory.java index a494f341a500..3aa1baa07114 100644 --- a/instrumentation/play/play-ws/play-ws-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/playws/PlayWsClientInstrumenterFactory.java +++ b/instrumentation/play/play-ws/play-ws-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/playws/PlayWsClientInstrumenterFactory.java @@ -5,38 +5,16 @@ package io.opentelemetry.javaagent.instrumentation.playws; -import io.opentelemetry.api.GlobalOpenTelemetry; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpClientAttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpClientMetrics; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpSpanNameExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpSpanStatusExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.net.PeerServiceAttributesExtractor; -import io.opentelemetry.javaagent.bootstrap.internal.CommonConfig; +import io.opentelemetry.javaagent.bootstrap.internal.JavaagentHttpClientInstrumenters; import play.shaded.ahc.org.asynchttpclient.Request; import play.shaded.ahc.org.asynchttpclient.Response; public final class PlayWsClientInstrumenterFactory { public static Instrumenter createInstrumenter(String instrumentationName) { - PlayWsClientHttpAttributesGetter httpAttributesGetter = new PlayWsClientHttpAttributesGetter(); - PlayWsClientNetAttributesGetter netAttributesGetter = new PlayWsClientNetAttributesGetter(); - - return Instrumenter.builder( - GlobalOpenTelemetry.get(), - instrumentationName, - HttpSpanNameExtractor.create(httpAttributesGetter)) - .setSpanStatusExtractor(HttpSpanStatusExtractor.create(httpAttributesGetter)) - .addAttributesExtractor( - HttpClientAttributesExtractor.builder(httpAttributesGetter, netAttributesGetter) - .setCapturedRequestHeaders(CommonConfig.get().getClientRequestHeaders()) - .setCapturedResponseHeaders(CommonConfig.get().getClientResponseHeaders()) - .build()) - .addAttributesExtractor( - PeerServiceAttributesExtractor.create( - netAttributesGetter, CommonConfig.get().getPeerServiceMapping())) - .addOperationMetrics(HttpClientMetrics.get()) - .buildClientInstrumenter(HttpHeaderSetter.INSTANCE); + return JavaagentHttpClientInstrumenters.create( + instrumentationName, new PlayWsClientHttpAttributesGetter(), HttpHeaderSetter.INSTANCE); } private PlayWsClientInstrumenterFactory() {} diff --git a/instrumentation/play/play-ws/play-ws-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/playws/PlayWsClientNetAttributesGetter.java b/instrumentation/play/play-ws/play-ws-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/playws/PlayWsClientNetAttributesGetter.java deleted file mode 100644 index 4f5ea36b34df..000000000000 --- a/instrumentation/play/play-ws/play-ws-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/playws/PlayWsClientNetAttributesGetter.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.javaagent.instrumentation.playws; - -import io.opentelemetry.instrumentation.api.instrumenter.net.NetClientAttributesGetter; -import java.net.InetSocketAddress; -import javax.annotation.Nullable; -import play.shaded.ahc.org.asynchttpclient.Request; -import play.shaded.ahc.org.asynchttpclient.Response; - -final class PlayWsClientNetAttributesGetter - implements NetClientAttributesGetter { - - @Nullable - @Override - public String getServerAddress(Request request) { - return request.getUri().getHost(); - } - - @Override - public Integer getServerPort(Request request) { - return request.getUri().getPort(); - } - - @Override - @Nullable - public InetSocketAddress getServerInetSocketAddress( - Request request, @Nullable Response response) { - if (response != null && response.getRemoteAddress() instanceof InetSocketAddress) { - return (InetSocketAddress) response.getRemoteAddress(); - } - return null; - } -} diff --git a/instrumentation/play/play-ws/play-ws-common/testing/src/main/groovy/PlayWsClientTestBase.groovy b/instrumentation/play/play-ws/play-ws-common/testing/src/main/groovy/PlayWsClientTestBase.groovy index 351e70bdedef..52f47021b4d3 100644 --- a/instrumentation/play/play-ws/play-ws-common/testing/src/main/groovy/PlayWsClientTestBase.groovy +++ b/instrumentation/play/play-ws/play-ws-common/testing/src/main/groovy/PlayWsClientTestBase.groovy @@ -23,10 +23,12 @@ import java.util.concurrent.TimeUnit class PlayJavaWsClientTestBase extends PlayWsClientTestBaseBase { @Shared StandaloneWSClient wsClient + @Shared + StandaloneWSClient wsClientWithReadTimeout @Override StandaloneWSRequest buildRequest(String method, URI uri, Map headers) { - def request = wsClient.url(uri.toURL().toString()).setFollowRedirects(true) + def request = getClient(uri).url(uri.toURL().toString()).setFollowRedirects(true) headers.entrySet().each { entry -> request.addHeader(entry.getKey(), entry.getValue()) } return request.setMethod(method) } @@ -43,22 +45,33 @@ class PlayJavaWsClientTestBase extends PlayWsClientTestBaseBase { @Shared StandaloneWSClient wsClient + @Shared + StandaloneWSClient wsClientWithReadTimeout @Override StandaloneWSRequest buildRequest(String method, URI uri, Map headers) { - def request = wsClient.url(uri.toURL().toString()).setFollowRedirects(true) + def request = getClient(uri).url(uri.toURL().toString()).setFollowRedirects(true) headers.entrySet().each { entry -> request.addHeader(entry.getKey(), entry.getValue()) } request.setMethod(method) return request @@ -88,22 +101,33 @@ class PlayJavaStreamedWsClientTestBase extends PlayWsClientTestBaseBase { @Shared play.api.libs.ws.StandaloneWSClient wsClient + @Shared + play.api.libs.ws.StandaloneWSClient wsClientWithReadTimeout @Override play.api.libs.ws.StandaloneWSRequest buildRequest(String method, URI uri, Map headers) { - return wsClient.url(uri.toURL().toString()) + return getClient(uri).url(uri.toURL().toString()) .withMethod(method) .withFollowRedirects(true) .withHttpHeaders(JavaConverters.mapAsScalaMap(headers).toSeq()) @@ -135,22 +159,33 @@ class PlayScalaWsClientTestBase extends PlayWsClientTestBaseBase { @Shared play.api.libs.ws.StandaloneWSClient wsClient + @Shared + play.api.libs.ws.StandaloneWSClient wsClientWithReadTimeout @Override play.api.libs.ws.StandaloneWSRequest buildRequest(String method, URI uri, Map headers) { - return wsClient.url(uri.toURL().toString()) + return getClient(uri).url(uri.toURL().toString()) .withMethod(method) .withFollowRedirects(true) .withHttpHeaders(JavaConverters.mapAsScalaMap(headers).toSeq()) @@ -193,11 +228,20 @@ class PlayScalaStreamedWsClientTestBase extends PlayWsClientTestBaseBase extends HttpClientTest implements AgentTestTrait { @Shared ActorSystem system @@ -30,6 +29,9 @@ abstract class PlayWsClientTestBaseBase extends HttpClientTest @Shared AsyncHttpClient asyncHttpClient + @Shared + AsyncHttpClient asyncHttpClientWithReadTimeout + @Shared ActorMaterializer materializer @@ -44,21 +46,30 @@ abstract class PlayWsClientTestBaseBase extends HttpClientTest // 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) - .setReadTimeout(READ_TIMEOUT_MS) - .build() - - asyncHttpClient = new DefaultAsyncHttpClient(asyncHttpClientConfig) + asyncHttpClient = createClient(false) + asyncHttpClientWithReadTimeout = createClient(true) + } + + def createClient(boolean readTimeout) { + DefaultAsyncHttpClientConfig.Builder builder = new DefaultAsyncHttpClientConfig.Builder() + .setMaxRequestRetry(0) + .setShutdownQuietPeriod(0) + .setShutdownTimeout(0) + .setMaxRedirects(3) + .setConnectTimeout(CONNECT_TIMEOUT_MS) + + if (readTimeout) { + builder.setReadTimeout(READ_TIMEOUT_MS) + } + + AsyncHttpClientConfig asyncHttpClientConfig =builder.build() + return new DefaultAsyncHttpClient(asyncHttpClientConfig) } def cleanupSpec() { system?.terminate() + asyncHttpClient?.close() + asyncHttpClientWithReadTimeout?.close() } @Override @@ -75,11 +86,10 @@ abstract class PlayWsClientTestBaseBase extends HttpClientTest @Override Set> httpAttributes(URI uri) { def attributes = super.httpAttributes(uri) - attributes.remove(stringKey("net.protocol.name")) - attributes.remove(stringKey("net.protocol.version")) + attributes.remove(NetworkAttributes.NETWORK_PROTOCOL_VERSION) if (uri.toString().endsWith("/circular-redirect")) { - attributes.remove(SemanticAttributes.NET_PEER_NAME) - attributes.remove(SemanticAttributes.NET_PEER_PORT) + attributes.remove(ServerAttributes.SERVER_ADDRESS) + attributes.remove(ServerAttributes.SERVER_PORT) } return attributes } diff --git a/instrumentation/pulsar/pulsar-2.8/README.md b/instrumentation/pulsar/pulsar-2.8/README.md index 50278457c890..a3859252d3d0 100644 --- a/instrumentation/pulsar/pulsar-2.8/README.md +++ b/instrumentation/pulsar/pulsar-2.8/README.md @@ -1,5 +1,5 @@ # Settings for the Apache Pulsar instrumentation -| System property | Type | Default | Description | -|---|---|---|---| -| `otel.instrumentation.pulsar.experimental-span-attributes` | `Boolean` | `false` | Enable the capture of experimental span attributes. | +| System property | Type | Default | Description | +| ---------------------------------------------------------- | ------- | ------- | --------------------------------------------------- | +| `otel.instrumentation.pulsar.experimental-span-attributes` | Boolean | `false` | Enable the capture of experimental span attributes. | diff --git a/instrumentation/pulsar/pulsar-2.8/javaagent-unit-tests/src/test/java/io/opentelemetry/javaagent/instrumentation/pulsar/v2_8/UrlParserTest.java b/instrumentation/pulsar/pulsar-2.8/javaagent-unit-tests/src/test/java/io/opentelemetry/javaagent/instrumentation/pulsar/v2_8/UrlParserTest.java index 06dce6643f18..bb109a99c6f6 100644 --- a/instrumentation/pulsar/pulsar-2.8/javaagent-unit-tests/src/test/java/io/opentelemetry/javaagent/instrumentation/pulsar/v2_8/UrlParserTest.java +++ b/instrumentation/pulsar/pulsar-2.8/javaagent-unit-tests/src/test/java/io/opentelemetry/javaagent/instrumentation/pulsar/v2_8/UrlParserTest.java @@ -16,6 +16,8 @@ public class UrlParserTest { void parseUrl() { assertThat(UrlParser.parseUrl(null)).isNull(); assertThat(UrlParser.parseUrl("localhost:1")).isNull(); + assertThat(UrlParser.parseUrl("localhost:1,localhost:2")).isNull(); + assertThat(UrlParser.parseUrl("localhost:1;localhost:2")).isNull(); { UrlData url = UrlParser.parseUrl("pulsar://localhost:1"); @@ -32,5 +34,10 @@ void parseUrl() { assertThat(url.getHost()).isEqualTo("localhost"); assertThat(url.getPort()).isNull(); } + { + UrlData url = UrlParser.parseUrl("pulsar://localhost:xxx"); + assertThat(url.getHost()).isEqualTo("localhost"); + assertThat(url.getPort()).isNull(); + } } } diff --git a/instrumentation/pulsar/pulsar-2.8/javaagent/build.gradle.kts b/instrumentation/pulsar/pulsar-2.8/javaagent/build.gradle.kts index df33c367c452..a0f01abb60b3 100644 --- a/instrumentation/pulsar/pulsar-2.8/javaagent/build.gradle.kts +++ b/instrumentation/pulsar/pulsar-2.8/javaagent/build.gradle.kts @@ -15,13 +15,32 @@ dependencies { library("org.apache.pulsar:pulsar-client:2.8.0") testImplementation("javax.annotation:javax.annotation-api:1.3.2") - testImplementation("org.testcontainers:pulsar:1.17.1") + testImplementation("org.testcontainers:pulsar") testImplementation("org.apache.pulsar:pulsar-client-admin:2.8.0") } +tasks { + val testReceiveSpanDisabled by registering(Test::class) { + filter { + includeTestsMatching("PulsarClientSuppressReceiveSpansTest") + } + include("**/PulsarClientSuppressReceiveSpansTest.*") + } + + test { + filter { + excludeTestsMatching("PulsarClientSuppressReceiveSpansTest") + } + jvmArgs("-Dotel.instrumentation.messaging.experimental.receive-telemetry.enabled=true") + } + + check { + dependsOn(testReceiveSpanDisabled) + } +} + tasks.withType().configureEach { // TODO run tests both with and without experimental span attributes jvmArgs("-Dotel.instrumentation.pulsar.experimental-span-attributes=true") - jvmArgs("-Dotel.instrumentation.messaging.experimental.receive-telemetry.enabled=true") usesService(gradle.sharedServices.registrations["testcontainersBuildService"].service) } diff --git a/instrumentation/pulsar/pulsar-2.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pulsar/v2_8/ConsumerBaseInstrumentation.java b/instrumentation/pulsar/pulsar-2.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pulsar/v2_8/ConsumerBaseInstrumentation.java new file mode 100644 index 000000000000..dc2e3baea918 --- /dev/null +++ b/instrumentation/pulsar/pulsar-2.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pulsar/v2_8/ConsumerBaseInstrumentation.java @@ -0,0 +1,50 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.pulsar.v2_8; + +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.pulsar.v2_8.telemetry.MessageListenerContext; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class ConsumerBaseInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return namedOneOf( + "org.apache.pulsar.client.impl.ConsumerBase", + "org.apache.pulsar.client.impl.MultiTopicsConsumerImpl"); + } + + @Override + public void transform(TypeTransformer transformer) { + // these methods receive a message and pass it on to a message listener + // we instrument them so that the span for the receive operation could be suppressed + transformer.applyAdviceToMethod( + named("triggerListener").and(takesArguments(0)).or(named("receiveMessageFromConsumer")), + this.getClass().getName() + "$TriggerListenerAdvice"); + } + + @SuppressWarnings("unused") + public static class TriggerListenerAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter() { + MessageListenerContext.startProcessing(); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void onExit() { + MessageListenerContext.endProcessing(); + } + } +} diff --git a/instrumentation/pulsar/pulsar-2.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pulsar/v2_8/ConsumerImplInstrumentation.java b/instrumentation/pulsar/pulsar-2.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pulsar/v2_8/ConsumerImplInstrumentation.java index 25864824e9c0..0919440dc11e 100644 --- a/instrumentation/pulsar/pulsar-2.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pulsar/v2_8/ConsumerImplInstrumentation.java +++ b/instrumentation/pulsar/pulsar-2.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pulsar/v2_8/ConsumerImplInstrumentation.java @@ -88,12 +88,12 @@ public static void after( @SuppressWarnings("unused") public static class ConsumerInternalReceiveAdvice { - @Advice.OnMethodEnter + @Advice.OnMethodEnter(suppress = Throwable.class) public static Timer before() { return Timer.start(); } - @Advice.OnMethodExit(onThrowable = Throwable.class) + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) public static void after( @Advice.Enter Timer timer, @Advice.This Consumer consumer, @@ -117,7 +117,7 @@ public static Timer before() { return Timer.start(); } - @Advice.OnMethodExit(onThrowable = Throwable.class) + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) public static void after( @Advice.Enter Timer timer, @Advice.This Consumer consumer, @@ -137,7 +137,7 @@ public static Timer before() { return Timer.start(); } - @Advice.OnMethodExit(onThrowable = Throwable.class) + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) public static void after( @Advice.Enter Timer timer, @Advice.This Consumer consumer, @@ -154,7 +154,7 @@ public static Timer before() { return Timer.start(); } - @Advice.OnMethodExit(onThrowable = Throwable.class) + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) public static void after( @Advice.Enter Timer timer, @Advice.This Consumer consumer, diff --git a/instrumentation/pulsar/pulsar-2.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pulsar/v2_8/MessageInstrumentation.java b/instrumentation/pulsar/pulsar-2.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pulsar/v2_8/MessageInstrumentation.java index 1899a50151ce..f1ff1814b4f7 100644 --- a/instrumentation/pulsar/pulsar-2.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pulsar/v2_8/MessageInstrumentation.java +++ b/instrumentation/pulsar/pulsar-2.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pulsar/v2_8/MessageInstrumentation.java @@ -33,7 +33,7 @@ public void transform(TypeTransformer transformer) { @SuppressWarnings("unused") public static class MessageRecycleAdvice { - @Advice.OnMethodExit + @Advice.OnMethodExit(suppress = Throwable.class) public static void after(@Advice.This Message message) { // Clean context to prevent memory leak. VirtualFieldStore.inject(message, null); diff --git a/instrumentation/pulsar/pulsar-2.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pulsar/v2_8/MessageListenerInstrumentation.java b/instrumentation/pulsar/pulsar-2.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pulsar/v2_8/MessageListenerInstrumentation.java index a09187652ed1..d0b8dfe6a8ff 100644 --- a/instrumentation/pulsar/pulsar-2.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pulsar/v2_8/MessageListenerInstrumentation.java +++ b/instrumentation/pulsar/pulsar-2.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pulsar/v2_8/MessageListenerInstrumentation.java @@ -44,7 +44,7 @@ public void transform(TypeTransformer transformer) { @SuppressWarnings("unused") public static class ConsumerConfigurationDataMethodAdvice { - @Advice.OnMethodExit + @Advice.OnMethodExit(suppress = Throwable.class) public static void after( @Advice.This ConsumerConfigurationData data, @Advice.Return(readOnly = false, typing = Assigner.Typing.DYNAMIC) diff --git a/instrumentation/pulsar/pulsar-2.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pulsar/v2_8/ProducerImplInstrumentation.java b/instrumentation/pulsar/pulsar-2.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pulsar/v2_8/ProducerImplInstrumentation.java index 18525a63399d..79211824e317 100644 --- a/instrumentation/pulsar/pulsar-2.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pulsar/v2_8/ProducerImplInstrumentation.java +++ b/instrumentation/pulsar/pulsar-2.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pulsar/v2_8/ProducerImplInstrumentation.java @@ -14,18 +14,14 @@ 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.pulsar.v2_8.telemetry.PulsarRequest; -import java.util.concurrent.CompletableFuture; import net.bytebuddy.asm.Advice; import net.bytebuddy.description.type.TypeDescription; import net.bytebuddy.matcher.ElementMatcher; import org.apache.pulsar.client.api.Message; -import org.apache.pulsar.client.api.MessageId; import org.apache.pulsar.client.api.PulsarClient; -import org.apache.pulsar.client.impl.MessageImpl; import org.apache.pulsar.client.impl.ProducerImpl; import org.apache.pulsar.client.impl.PulsarClientImpl; import org.apache.pulsar.client.impl.SendCallback; @@ -56,7 +52,7 @@ public void transform(TypeTransformer transformer) { @SuppressWarnings("unused") public static class ProducerImplConstructorAdvice { - @Advice.OnMethodExit + @Advice.OnMethodExit(suppress = Throwable.class) public static void intercept( @Advice.This ProducerImpl producer, @Advice.Argument(value = 0) PulsarClient client) { PulsarClientImpl pulsarClient = (PulsarClientImpl) client; @@ -69,11 +65,11 @@ public static void intercept( @SuppressWarnings("unused") public static class ProducerSendAsyncMethodAdvice { - @Advice.OnMethodEnter + @Advice.OnMethodEnter(suppress = Throwable.class) public static void before( @Advice.This ProducerImpl producer, @Advice.Argument(value = 0) Message message, - @Advice.Argument(value = 1, readOnly = false) SendCallback callback) { + @Advice.Argument(value = 1) SendCallback callback) { Context parent = Context.current(); PulsarRequest request = PulsarRequest.create(message, VirtualFieldStore.extract(producer)); @@ -82,54 +78,9 @@ public static void before( } Context context = producerInstrumenter().start(parent, request); - callback = new SendCallbackWrapper(context, request, callback); - } - } - - public static class SendCallbackWrapper implements SendCallback { - - private final Context context; - private final PulsarRequest request; - private final SendCallback delegate; - - public SendCallbackWrapper(Context context, PulsarRequest request, SendCallback callback) { - this.context = context; - this.request = request; - this.delegate = callback; - } - - @Override - public void sendComplete(Exception e) { - if (context == null) { - this.delegate.sendComplete(e); - return; - } - - try (Scope ignore = context.makeCurrent()) { - this.delegate.sendComplete(e); - } finally { - producerInstrumenter().end(context, request, null, e); - } - } - - @Override - public void addCallback(MessageImpl msg, SendCallback scb) { - this.delegate.addCallback(msg, scb); - } - - @Override - public SendCallback getNextSendCallback() { - return this.delegate.getNextSendCallback(); - } - - @Override - public MessageImpl getNextMessage() { - return this.delegate.getNextMessage(); - } - - @Override - public CompletableFuture getFuture() { - return this.delegate.getFuture(); + // Inject the context/request into the SendCallback. This will be extracted and used when the + // message is sent and the callback is invoked. see `SendCallbackInstrumentation`. + VirtualFieldStore.inject(callback, context, request); } } } diff --git a/instrumentation/pulsar/pulsar-2.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pulsar/v2_8/PulsarInstrumentationModule.java b/instrumentation/pulsar/pulsar-2.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pulsar/v2_8/PulsarInstrumentationModule.java index 849b1fc86c94..172538815cfb 100644 --- a/instrumentation/pulsar/pulsar-2.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pulsar/v2_8/PulsarInstrumentationModule.java +++ b/instrumentation/pulsar/pulsar-2.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pulsar/v2_8/PulsarInstrumentationModule.java @@ -20,9 +20,11 @@ public PulsarInstrumentationModule() { @Override public List typeInstrumentations() { return Arrays.asList( + new ConsumerBaseInstrumentation(), new ConsumerImplInstrumentation(), new ProducerImplInstrumentation(), new MessageInstrumentation(), - new MessageListenerInstrumentation()); + new MessageListenerInstrumentation(), + new SendCallbackInstrumentation()); } } diff --git a/instrumentation/pulsar/pulsar-2.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pulsar/v2_8/SendCallbackData.java b/instrumentation/pulsar/pulsar-2.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pulsar/v2_8/SendCallbackData.java new file mode 100644 index 000000000000..462843d70e31 --- /dev/null +++ b/instrumentation/pulsar/pulsar-2.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pulsar/v2_8/SendCallbackData.java @@ -0,0 +1,23 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.pulsar.v2_8; + +import io.opentelemetry.context.Context; +import io.opentelemetry.javaagent.instrumentation.pulsar.v2_8.telemetry.PulsarRequest; + +public final class SendCallbackData { + public final Context context; + public final PulsarRequest request; + + private SendCallbackData(Context context, PulsarRequest request) { + this.context = context; + this.request = request; + } + + public static SendCallbackData create(Context context, PulsarRequest request) { + return new SendCallbackData(context, request); + } +} diff --git a/instrumentation/pulsar/pulsar-2.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pulsar/v2_8/SendCallbackInstrumentation.java b/instrumentation/pulsar/pulsar-2.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pulsar/v2_8/SendCallbackInstrumentation.java new file mode 100644 index 000000000000..85295042e335 --- /dev/null +++ b/instrumentation/pulsar/pulsar-2.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pulsar/v2_8/SendCallbackInstrumentation.java @@ -0,0 +1,75 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.pulsar.v2_8; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.hasClassesNamed; +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.hasSuperType; +import static io.opentelemetry.javaagent.instrumentation.pulsar.v2_8.telemetry.PulsarSingletons.producerInstrumenter; +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.pulsar.v2_8.telemetry.PulsarRequest; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import org.apache.pulsar.client.impl.SendCallback; + +public class SendCallbackInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher classLoaderOptimization() { + return hasClassesNamed("org.apache.pulsar.client.impl.SendCallback"); + } + + @Override + public ElementMatcher typeMatcher() { + return hasSuperType(named("org.apache.pulsar.client.impl.SendCallback")); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod().and(named("sendComplete")), + SendCallbackInstrumentation.class.getName() + "$SendCallbackSendCompleteAdvice"); + } + + @SuppressWarnings("unused") + public static class SendCallbackSendCompleteAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter( + @Advice.This SendCallback callback, + @Advice.Local("otelContext") Context otelContext, + @Advice.Local("otelScope") Scope otelScope, + @Advice.Local("otelRequest") PulsarRequest request) { + // Extract the Context and PulsarRequest from the SendCallback instance. + SendCallbackData callBackData = VirtualFieldStore.extract(callback); + if (callBackData != null) { + // If the extraction was successful, store the Context and PulsarRequest in local variables. + otelContext = callBackData.context; + request = callBackData.request; + otelScope = otelContext.makeCurrent(); + } + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void onExit( + @Advice.Argument(0) Throwable t, + @Advice.Local("otelContext") Context otelContext, + @Advice.Local("otelScope") Scope otelScope, + @Advice.Local("otelRequest") PulsarRequest request) { + if (otelScope != null) { + // Close the Scope and end the span. + otelScope.close(); + producerInstrumenter().end(otelContext, request, null, t); + } + } + } +} diff --git a/instrumentation/pulsar/pulsar-2.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pulsar/v2_8/UrlParser.java b/instrumentation/pulsar/pulsar-2.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pulsar/v2_8/UrlParser.java index 5e0087d766d1..cc04d9083f6f 100644 --- a/instrumentation/pulsar/pulsar-2.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pulsar/v2_8/UrlParser.java +++ b/instrumentation/pulsar/pulsar-2.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pulsar/v2_8/UrlParser.java @@ -10,7 +10,8 @@ public class UrlParser { private UrlParser() {} public static UrlData parseUrl(String url) { - if (url == null) { + // if there are multiple addresses then they are separated with , or ; + if (url == null || url.indexOf(',') != -1 || url.indexOf(';') != -1) { return null; } @@ -33,7 +34,11 @@ public static UrlData parseUrl(String url) { port = null; } else { host = authority.substring(0, portStart); - port = Integer.parseInt(authority.substring(portStart + 1)); + try { + port = Integer.parseInt(authority.substring(portStart + 1)); + } catch (NumberFormatException exception) { + port = null; + } } return new UrlData(host, port); diff --git a/instrumentation/pulsar/pulsar-2.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pulsar/v2_8/VirtualFieldStore.java b/instrumentation/pulsar/pulsar-2.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pulsar/v2_8/VirtualFieldStore.java index 193079174c9e..9ec84944feaf 100644 --- a/instrumentation/pulsar/pulsar-2.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pulsar/v2_8/VirtualFieldStore.java +++ b/instrumentation/pulsar/pulsar-2.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pulsar/v2_8/VirtualFieldStore.java @@ -7,9 +7,11 @@ import io.opentelemetry.context.Context; import io.opentelemetry.instrumentation.api.util.VirtualField; +import io.opentelemetry.javaagent.instrumentation.pulsar.v2_8.telemetry.PulsarRequest; import org.apache.pulsar.client.api.Consumer; import org.apache.pulsar.client.api.Message; import org.apache.pulsar.client.api.Producer; +import org.apache.pulsar.client.impl.SendCallback; import org.apache.pulsar.client.impl.TopicMessageImpl; public class VirtualFieldStore { @@ -19,6 +21,8 @@ public class VirtualFieldStore { VirtualField.find(Producer.class, ProducerData.class); private static final VirtualField, String> CONSUMER_FIELD = VirtualField.find(Consumer.class, String.class); + private static final VirtualField CALLBACK_FIELD = + VirtualField.find(SendCallback.class, SendCallbackData.class); private VirtualFieldStore() {} @@ -40,6 +44,12 @@ public static void inject(Consumer instance, String serviceUrl) { CONSUMER_FIELD.set(instance, serviceUrl); } + public static void inject(SendCallback instance, Context context, PulsarRequest request) { + if (instance != null) { + CALLBACK_FIELD.set(instance, SendCallbackData.create(context, request)); + } + } + public static Context extract(Message instance) { if (instance instanceof TopicMessageImpl) { TopicMessageImpl topicMessage = (TopicMessageImpl) instance; @@ -59,4 +69,8 @@ public static ProducerData extract(Producer instance) { public static String extract(Consumer instance) { return CONSUMER_FIELD.get(instance); } + + public static SendCallbackData extract(SendCallback instance) { + return CALLBACK_FIELD.get(instance); + } } diff --git a/instrumentation/pulsar/pulsar-2.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pulsar/v2_8/telemetry/MessageListenerContext.java b/instrumentation/pulsar/pulsar-2.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pulsar/v2_8/telemetry/MessageListenerContext.java new file mode 100644 index 000000000000..7bcea2245431 --- /dev/null +++ b/instrumentation/pulsar/pulsar-2.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pulsar/v2_8/telemetry/MessageListenerContext.java @@ -0,0 +1,31 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.pulsar.v2_8.telemetry; + +/** + * Helper class used to determine whether message is going to be processed by a listener. If we know + * that message is going to be passed to a message listener, that would produce a span for the + * "process" operation, we are going to suppress the span from the message "receive" operation. + */ +public final class MessageListenerContext { + private static final ThreadLocal processing = new ThreadLocal<>(); + + private MessageListenerContext() {} + + /** Call on entry to a method that will pass the received message to a message listener. */ + public static void startProcessing() { + processing.set(Boolean.TRUE); + } + + public static void endProcessing() { + processing.remove(); + } + + /** Returns true if we expect a received message to be passed to a listener. */ + public static boolean isProcessing() { + return processing.get() != null; + } +} diff --git a/instrumentation/pulsar/pulsar-2.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pulsar/v2_8/telemetry/PulsarBatchMessagingAttributesGetter.java b/instrumentation/pulsar/pulsar-2.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pulsar/v2_8/telemetry/PulsarBatchMessagingAttributesGetter.java index addbd0647374..7e934de2b666 100644 --- a/instrumentation/pulsar/pulsar-2.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pulsar/v2_8/telemetry/PulsarBatchMessagingAttributesGetter.java +++ b/instrumentation/pulsar/pulsar-2.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pulsar/v2_8/telemetry/PulsarBatchMessagingAttributesGetter.java @@ -5,12 +5,13 @@ package io.opentelemetry.javaagent.instrumentation.pulsar.v2_8.telemetry; -import io.opentelemetry.instrumentation.api.instrumenter.messaging.MessagingAttributesGetter; +import io.opentelemetry.instrumentation.api.incubator.semconv.messaging.MessagingAttributesGetter; import java.util.List; import java.util.Objects; import java.util.stream.Collectors; import java.util.stream.StreamSupport; import javax.annotation.Nullable; +import org.apache.pulsar.common.naming.TopicName; enum PulsarBatchMessagingAttributesGetter implements MessagingAttributesGetter { @@ -27,11 +28,22 @@ public String getDestination(PulsarBatchRequest request) { return request.getDestination(); } + @Nullable + @Override + public String getDestinationTemplate(PulsarBatchRequest request) { + return null; + } + @Override public boolean isTemporaryDestination(PulsarBatchRequest request) { return false; } + @Override + public boolean isAnonymousDestination(PulsarBatchRequest request) { + return false; + } + @Nullable @Override public String getConversationId(PulsarBatchRequest message) { @@ -40,7 +52,7 @@ public String getConversationId(PulsarBatchRequest message) { @Nullable @Override - public Long getMessagePayloadSize(PulsarBatchRequest request) { + public Long getMessageBodySize(PulsarBatchRequest request) { return StreamSupport.stream(request.getMessages().spliterator(), false) .map(message -> (long) message.size()) .reduce(Long::sum) @@ -49,7 +61,7 @@ public Long getMessagePayloadSize(PulsarBatchRequest request) { @Nullable @Override - public Long getMessagePayloadCompressedSize(PulsarBatchRequest request) { + public Long getMessageEnvelopeSize(PulsarBatchRequest request) { return null; } @@ -59,6 +71,27 @@ public String getMessageId(PulsarBatchRequest request, @Nullable Void response) return null; } + @Nullable + @Override + public String getClientId(PulsarBatchRequest request) { + return null; + } + + @Override + public Long getBatchMessageCount(PulsarBatchRequest request, @Nullable Void unused) { + return (long) request.getMessages().size(); + } + + @Nullable + @Override + public String getDestinationPartitionId(PulsarBatchRequest request) { + int partitionIndex = TopicName.getPartitionIndex(request.getDestination()); + if (partitionIndex == -1) { + return null; + } + return String.valueOf(partitionIndex); + } + @Override public List getMessageHeader(PulsarBatchRequest request, String name) { return StreamSupport.stream(request.getMessages().spliterator(), false) diff --git a/instrumentation/pulsar/pulsar-2.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pulsar/v2_8/telemetry/PulsarBatchRequest.java b/instrumentation/pulsar/pulsar-2.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pulsar/v2_8/telemetry/PulsarBatchRequest.java index 201c01ed3635..ae1c81cf188c 100644 --- a/instrumentation/pulsar/pulsar-2.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pulsar/v2_8/telemetry/PulsarBatchRequest.java +++ b/instrumentation/pulsar/pulsar-2.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pulsar/v2_8/telemetry/PulsarBatchRequest.java @@ -10,6 +10,7 @@ import io.opentelemetry.javaagent.instrumentation.pulsar.v2_8.UrlParser.UrlData; import org.apache.pulsar.client.api.Message; import org.apache.pulsar.client.api.Messages; +import org.apache.pulsar.common.naming.TopicName; public final class PulsarBatchRequest extends BasePulsarRequest { private final Messages messages; @@ -30,7 +31,10 @@ private static String getTopicName(Messages messages) { if (topicName == null) { topicName = name; } else if (!topicName.equals(name)) { - return null; + // this is a partitioned topic + // persistent://public/default/test-partition-0 persistent://public/default/test-partition-1 + // return persistent://public/default/test + return TopicName.get(topicName).getPartitionedTopicName(); } } return topicName; diff --git a/instrumentation/pulsar/pulsar-2.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pulsar/v2_8/telemetry/PulsarMessagingAttributesGetter.java b/instrumentation/pulsar/pulsar-2.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pulsar/v2_8/telemetry/PulsarMessagingAttributesGetter.java index 92596589f49d..08492480c143 100644 --- a/instrumentation/pulsar/pulsar-2.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pulsar/v2_8/telemetry/PulsarMessagingAttributesGetter.java +++ b/instrumentation/pulsar/pulsar-2.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pulsar/v2_8/telemetry/PulsarMessagingAttributesGetter.java @@ -8,10 +8,11 @@ import static java.util.Collections.emptyList; import static java.util.Collections.singletonList; -import io.opentelemetry.instrumentation.api.instrumenter.messaging.MessagingAttributesGetter; +import io.opentelemetry.instrumentation.api.incubator.semconv.messaging.MessagingAttributesGetter; import java.util.List; import javax.annotation.Nullable; import org.apache.pulsar.client.api.Message; +import org.apache.pulsar.common.naming.TopicName; enum PulsarMessagingAttributesGetter implements MessagingAttributesGetter { INSTANCE; @@ -27,26 +28,36 @@ public String getDestination(PulsarRequest request) { return request.getDestination(); } + @Nullable + @Override + public String getDestinationTemplate(PulsarRequest request) { + return null; + } + @Override public boolean isTemporaryDestination(PulsarRequest request) { return false; } + @Override + public boolean isAnonymousDestination(PulsarRequest request) { + return false; + } + @Nullable @Override public String getConversationId(PulsarRequest message) { return null; } - @Nullable @Override - public Long getMessagePayloadSize(PulsarRequest request) { + public Long getMessageBodySize(PulsarRequest request) { return (long) request.getMessage().size(); } @Nullable @Override - public Long getMessagePayloadCompressedSize(PulsarRequest request) { + public Long getMessageEnvelopeSize(PulsarRequest request) { return null; } @@ -61,6 +72,28 @@ public String getMessageId(PulsarRequest request, @Nullable Void response) { return null; } + @Nullable + @Override + public String getClientId(PulsarRequest request) { + return null; + } + + @Nullable + @Override + public Long getBatchMessageCount(PulsarRequest request, @Nullable Void unused) { + return null; + } + + @Nullable + @Override + public String getDestinationPartitionId(PulsarRequest request) { + int partitionIndex = TopicName.getPartitionIndex(request.getDestination()); + if (partitionIndex == -1) { + return null; + } + return String.valueOf(partitionIndex); + } + @Override public List getMessageHeader(PulsarRequest request, String name) { String value = request.getMessage().getProperty(name); diff --git a/instrumentation/pulsar/pulsar-2.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pulsar/v2_8/telemetry/PulsarNetClientAttributesGetter.java b/instrumentation/pulsar/pulsar-2.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pulsar/v2_8/telemetry/PulsarNetClientAttributesGetter.java index 11bbf09fcd25..91391d088cac 100644 --- a/instrumentation/pulsar/pulsar-2.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pulsar/v2_8/telemetry/PulsarNetClientAttributesGetter.java +++ b/instrumentation/pulsar/pulsar-2.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pulsar/v2_8/telemetry/PulsarNetClientAttributesGetter.java @@ -5,16 +5,11 @@ package io.opentelemetry.javaagent.instrumentation.pulsar.v2_8.telemetry; -import io.opentelemetry.instrumentation.api.instrumenter.net.NetClientAttributesGetter; +import io.opentelemetry.instrumentation.api.semconv.network.ServerAttributesGetter; import javax.annotation.Nullable; public final class PulsarNetClientAttributesGetter - implements NetClientAttributesGetter { - @Nullable - @Override - public String getTransport(BasePulsarRequest request, @Nullable Void unused) { - return null; - } + implements ServerAttributesGetter { @Nullable @Override diff --git a/instrumentation/pulsar/pulsar-2.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pulsar/v2_8/telemetry/PulsarSingletons.java b/instrumentation/pulsar/pulsar-2.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pulsar/v2_8/telemetry/PulsarSingletons.java index 20f4975962e0..51d534f8158b 100644 --- a/instrumentation/pulsar/pulsar-2.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pulsar/v2_8/telemetry/PulsarSingletons.java +++ b/instrumentation/pulsar/pulsar-2.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/pulsar/v2_8/telemetry/PulsarSingletons.java @@ -10,19 +10,23 @@ import io.opentelemetry.context.Context; import io.opentelemetry.context.Scope; import io.opentelemetry.context.propagation.TextMapPropagator; +import io.opentelemetry.instrumentation.api.incubator.semconv.messaging.MessageOperation; +import io.opentelemetry.instrumentation.api.incubator.semconv.messaging.MessagingAttributesExtractor; +import io.opentelemetry.instrumentation.api.incubator.semconv.messaging.MessagingAttributesGetter; +import io.opentelemetry.instrumentation.api.incubator.semconv.messaging.MessagingConsumerMetrics; +import io.opentelemetry.instrumentation.api.incubator.semconv.messaging.MessagingProducerMetrics; +import io.opentelemetry.instrumentation.api.incubator.semconv.messaging.MessagingSpanNameExtractor; 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 io.opentelemetry.instrumentation.api.instrumenter.messaging.MessageOperation; -import io.opentelemetry.instrumentation.api.instrumenter.messaging.MessagingAttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.messaging.MessagingAttributesGetter; -import io.opentelemetry.instrumentation.api.instrumenter.messaging.MessagingSpanNameExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.net.NetClientAttributesExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.SpanLinksExtractor; import io.opentelemetry.instrumentation.api.internal.InstrumenterUtil; +import io.opentelemetry.instrumentation.api.internal.PropagatorBasedSpanLinksExtractor; import io.opentelemetry.instrumentation.api.internal.Timer; +import io.opentelemetry.instrumentation.api.semconv.network.ServerAttributesExtractor; +import io.opentelemetry.javaagent.bootstrap.internal.AgentInstrumentationConfig; import io.opentelemetry.javaagent.bootstrap.internal.ExperimentalConfig; -import io.opentelemetry.javaagent.bootstrap.internal.InstrumentationConfig; import io.opentelemetry.javaagent.instrumentation.pulsar.v2_8.VirtualFieldStore; import java.util.List; import java.util.concurrent.CompletableFuture; @@ -38,6 +42,8 @@ public final class PulsarSingletons { TELEMETRY.getPropagators().getTextMapPropagator(); private static final List capturedHeaders = ExperimentalConfig.get().getMessagingHeaders(); + private static final boolean receiveInstrumentationEnabled = + ExperimentalConfig.get().messagingReceiveInstrumentationEnabled(); private static final Instrumenter CONSUMER_PROCESS_INSTRUMENTER = createConsumerProcessInstrumenter(); @@ -64,15 +70,24 @@ private static Instrumenter createConsumerReceiveInstrument MessagingAttributesGetter getter = PulsarMessagingAttributesGetter.INSTANCE; - return Instrumenter.builder( - TELEMETRY, - INSTRUMENTATION_NAME, - MessagingSpanNameExtractor.create(getter, MessageOperation.RECEIVE)) - .addAttributesExtractor( - createMessagingAttributesExtractor(getter, MessageOperation.RECEIVE)) - .addAttributesExtractor( - NetClientAttributesExtractor.create(new PulsarNetClientAttributesGetter())) - .buildConsumerInstrumenter(MessageTextMapGetter.INSTANCE); + InstrumenterBuilder instrumenterBuilder = + Instrumenter.builder( + TELEMETRY, + INSTRUMENTATION_NAME, + MessagingSpanNameExtractor.create(getter, MessageOperation.RECEIVE)) + .addAttributesExtractor( + createMessagingAttributesExtractor(getter, MessageOperation.RECEIVE)) + .addOperationMetrics(MessagingConsumerMetrics.get()) + .addAttributesExtractor( + ServerAttributesExtractor.create(new PulsarNetClientAttributesGetter())); + + if (receiveInstrumentationEnabled) { + return instrumenterBuilder + .addSpanLinksExtractor( + new PropagatorBasedSpanLinksExtractor<>(PROPAGATOR, MessageTextMapGetter.INSTANCE)) + .buildInstrumenter(SpanKindExtractor.alwaysConsumer()); + } + return instrumenterBuilder.buildConsumerInstrumenter(MessageTextMapGetter.INSTANCE); } private static Instrumenter createConsumerBatchReceiveInstrumenter() { @@ -86,11 +101,9 @@ private static Instrumenter createConsumerBatchReceive .addAttributesExtractor( createMessagingAttributesExtractor(getter, MessageOperation.RECEIVE)) .addAttributesExtractor( - NetClientAttributesExtractor.create(new PulsarNetClientAttributesGetter())) - .setEnabled(ExperimentalConfig.get().messagingReceiveInstrumentationEnabled()) - .addSpanLinksExtractor( - new PulsarBatchRequestSpanLinksExtractor( - GlobalOpenTelemetry.getPropagators().getTextMapPropagator())) + ServerAttributesExtractor.create(new PulsarNetClientAttributesGetter())) + .addSpanLinksExtractor(new PulsarBatchRequestSpanLinksExtractor(PROPAGATOR)) + .addOperationMetrics(MessagingConsumerMetrics.get()) .buildInstrumenter(SpanKindExtractor.alwaysConsumer()); } @@ -98,13 +111,21 @@ private static Instrumenter createConsumerProcessInstrument MessagingAttributesGetter getter = PulsarMessagingAttributesGetter.INSTANCE; - return Instrumenter.builder( - TELEMETRY, - INSTRUMENTATION_NAME, - MessagingSpanNameExtractor.create(getter, MessageOperation.PROCESS)) - .addAttributesExtractor( - createMessagingAttributesExtractor(getter, MessageOperation.PROCESS)) - .buildInstrumenter(); + InstrumenterBuilder instrumenterBuilder = + Instrumenter.builder( + TELEMETRY, + INSTRUMENTATION_NAME, + MessagingSpanNameExtractor.create(getter, MessageOperation.PROCESS)) + .addAttributesExtractor( + createMessagingAttributesExtractor(getter, MessageOperation.PROCESS)); + + if (receiveInstrumentationEnabled) { + SpanLinksExtractor spanLinksExtractor = + new PropagatorBasedSpanLinksExtractor<>(PROPAGATOR, MessageTextMapGetter.INSTANCE); + instrumenterBuilder.addSpanLinksExtractor(spanLinksExtractor); + return instrumenterBuilder.buildInstrumenter(SpanKindExtractor.alwaysConsumer()); + } + return instrumenterBuilder.buildConsumerInstrumenter(MessageTextMapGetter.INSTANCE); } private static Instrumenter createProducerInstrumenter() { @@ -115,13 +136,14 @@ private static Instrumenter createProducerInstrumenter() { Instrumenter.builder( TELEMETRY, INSTRUMENTATION_NAME, - MessagingSpanNameExtractor.create(getter, MessageOperation.SEND)) + MessagingSpanNameExtractor.create(getter, MessageOperation.PUBLISH)) .addAttributesExtractor( - createMessagingAttributesExtractor(getter, MessageOperation.SEND)) + createMessagingAttributesExtractor(getter, MessageOperation.PUBLISH)) .addAttributesExtractor( - NetClientAttributesExtractor.create(new PulsarNetClientAttributesGetter())); + ServerAttributesExtractor.create(new PulsarNetClientAttributesGetter())) + .addOperationMetrics(MessagingProducerMetrics.get()); - if (InstrumentationConfig.get() + if (AgentInstrumentationConfig.get() .getBoolean("otel.instrumentation.pulsar.experimental-span-attributes", false)) { builder.addAttributesExtractor(ExperimentalProducerAttributesExtractor.INSTANCE); } @@ -146,12 +168,17 @@ public static Context startAndEndConsumerReceive( if (!CONSUMER_RECEIVE_INSTRUMENTER.shouldStart(parent, request)) { return null; } - // startAndEnd not supports extract trace context from carrier - // start not supports custom startTime - // extract trace context by using TEXT_MAP_PROPAGATOR here. + if (!receiveInstrumentationEnabled) { + // suppress receive span when receive telemetry is not enabled and message is going to be + // processed by a listener + if (MessageListenerContext.isProcessing()) { + return null; + } + parent = PROPAGATOR.extract(parent, request, MessageTextMapGetter.INSTANCE); + } return InstrumenterUtil.startAndEnd( CONSUMER_RECEIVE_INSTRUMENTER, - PROPAGATOR.extract(parent, request, MessageTextMapGetter.INSTANCE), + parent, request, null, throwable, @@ -165,7 +192,7 @@ private static Context startAndEndConsumerReceive( Timer timer, Consumer consumer, Throwable throwable) { - if (messages == null) { + if (messages == null || messages.size() == 0) { return null; } String brokerUrl = VirtualFieldStore.extract(consumer); @@ -185,11 +212,17 @@ private static Context startAndEndConsumerReceive( public static CompletableFuture> wrap( CompletableFuture> future, Timer timer, Consumer consumer) { + boolean listenerContextActive = MessageListenerContext.isProcessing(); Context parent = Context.current(); CompletableFuture> result = new CompletableFuture<>(); future.whenComplete( (message, throwable) -> { - Context context = startAndEndConsumerReceive(parent, message, timer, consumer, throwable); + // we create a "receive" span when receive telemetry is enabled or when we know that + // this message will not be passed to a listener that would create the "process" span + Context context = + receiveInstrumentationEnabled || !listenerContextActive + ? startAndEndConsumerReceive(parent, message, timer, consumer, throwable) + : parent; runWithContext( context, () -> { diff --git a/instrumentation/pulsar/pulsar-2.8/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/pulsar/v2_8/PulsarClientTest.groovy b/instrumentation/pulsar/pulsar-2.8/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/pulsar/v2_8/PulsarClientTest.groovy deleted file mode 100644 index d81238353cff..000000000000 --- a/instrumentation/pulsar/pulsar-2.8/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/pulsar/v2_8/PulsarClientTest.groovy +++ /dev/null @@ -1,670 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.javaagent.instrumentation.pulsar.v2_8 - -import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification -import io.opentelemetry.instrumentation.test.asserts.TraceAssert -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes -import org.apache.pulsar.client.admin.PulsarAdmin -import org.apache.pulsar.client.api.Consumer -import org.apache.pulsar.client.api.Message -import org.apache.pulsar.client.api.MessageListener -import org.apache.pulsar.client.api.Messages -import org.apache.pulsar.client.api.Producer -import org.apache.pulsar.client.api.PulsarClient -import org.apache.pulsar.client.api.Schema -import org.apache.pulsar.client.api.SubscriptionInitialPosition -import org.slf4j.Logger -import org.slf4j.LoggerFactory -import org.testcontainers.containers.PulsarContainer -import org.testcontainers.containers.output.Slf4jLogConsumer -import org.testcontainers.utility.DockerImageName -import spock.lang.Shared - -import java.time.Duration -import java.util.concurrent.CompletableFuture -import java.util.concurrent.CountDownLatch -import java.util.concurrent.TimeUnit -import java.util.regex.Pattern - -import static io.opentelemetry.api.trace.SpanKind.CONSUMER -import static io.opentelemetry.api.trace.SpanKind.INTERNAL -import static io.opentelemetry.api.trace.SpanKind.PRODUCER - -class PulsarClientTest extends AgentInstrumentationSpecification { - private static final Logger logger = LoggerFactory.getLogger(PulsarClientTest) - - private static final DockerImageName DEFAULT_IMAGE_NAME = - DockerImageName.parse("apachepulsar/pulsar:2.8.0") - - @Shared - private PulsarContainer pulsar - @Shared - private PulsarClient client - @Shared - private PulsarAdmin admin - @Shared - private Producer producer - @Shared - private Consumer consumer - @Shared - private Producer producer2 - - @Shared - private String brokerHost - @Shared - private int brokerPort - - @Override - def setupSpec() { - pulsar = new PulsarContainer(DEFAULT_IMAGE_NAME) - .withEnv("PULSAR_MEM", "-Xmx128m") - .withLogConsumer(new Slf4jLogConsumer(logger)) - .withStartupTimeout(Duration.ofMinutes(2)) - pulsar.start() - - brokerHost = pulsar.host - brokerPort = pulsar.getMappedPort(6650) - client = PulsarClient.builder().serviceUrl(pulsar.pulsarBrokerUrl).build() - admin = PulsarAdmin.builder().serviceHttpUrl(pulsar.httpServiceUrl).build() - } - - @Override - def cleanupSpec() { - producer?.close() - consumer?.close() - producer2?.close() - client?.close() - admin?.close() - pulsar.close() - } - - def "test send non-partitioned topic"() { - setup: - def topic = "persistent://public/default/testSendNonPartitionedTopic" - admin.topics().createNonPartitionedTopic(topic) - producer = - client.newProducer(Schema.STRING).topic(topic) - .enableBatching(false).create() - - when: - String msg = "test" - def msgId = runWithSpan("parent") { - producer.send(msg) - } - - then: - assertTraces(1) { - trace(0, 2) { - span(0) { - name "parent" - kind INTERNAL - hasNoParent() - } - producerSpan(it, 1, span(0), topic, msgId) - } - } - } - - def "test consume non-partitioned topic"() { - setup: - def topic = "persistent://public/default/testConsumeNonPartitionedTopic" - def latch = new CountDownLatch(1) - admin.topics().createNonPartitionedTopic(topic) - consumer = client.newConsumer(Schema.STRING) - .subscriptionName("test_sub") - .topic(topic) - .subscriptionInitialPosition(SubscriptionInitialPosition.Earliest) - .messageListener(new MessageListener() { - @Override - void received(Consumer consumer, Message msg) { - consumer.acknowledge(msg) - latch.countDown() - } - }) - .subscribe() - - producer = client.newProducer(Schema.STRING) - .topic(topic) - .enableBatching(false) - .create() - - when: - def msg = "test" - def msgId = runWithSpan("parent") { - producer.send(msg) - } - - latch.await(1, TimeUnit.MINUTES) - - then: - assertTraces(1) { - trace(0, 4) { - span(0) { - name "parent" - kind INTERNAL - hasNoParent() - } - producerSpan(it, 1, span(0), topic, msgId) - receiveSpan(it, 2, span(1), topic, msgId) - processSpan(it, 3, span(2), topic, msgId) - } - } - } - - def "test consume non-partitioned topic using receive"() { - setup: - def topic = "persistent://public/default/testConsumeNonPartitionedTopicCallReceive" - admin.topics().createNonPartitionedTopic(topic) - consumer = client.newConsumer(Schema.STRING) - .subscriptionName("test_sub") - .topic(topic) - .subscriptionInitialPosition(SubscriptionInitialPosition.Earliest) - .subscribe() - - producer = client.newProducer(Schema.STRING) - .topic(topic) - .enableBatching(false) - .create() - - when: - def msg = "test" - def msgId = runWithSpan("parent") { - producer.send(msg) - } - - def receivedMsg = consumer.receive() - consumer.acknowledge(receivedMsg) - - then: - assertTraces(1) { - trace(0, 3) { - span(0) { - name "parent" - kind INTERNAL - hasNoParent() - } - producerSpan(it, 1, span(0), topic, msgId) - receiveSpan(it, 2, span(1), topic, msgId) - } - } - } - - def "test consume non-partitioned topic using receiveAsync"() { - setup: - def topic = "persistent://public/default/testConsumeNonPartitionedTopicCallReceiveAsync" - admin.topics().createNonPartitionedTopic(topic) - consumer = client.newConsumer(Schema.STRING) - .subscriptionName("test_sub") - .topic(topic) - .subscriptionInitialPosition(SubscriptionInitialPosition.Earliest) - .subscribe() - - producer = client.newProducer(Schema.STRING) - .topic(topic) - .enableBatching(false) - .create() - - when: - CompletableFuture> result = consumer.receiveAsync().whenComplete { receivedMsg, throwable -> - runWithSpan("callback") { - consumer.acknowledge(receivedMsg) - } - } - - def msg = "test" - def msgId = runWithSpan("parent") { - producer.send(msg) - } - - result.get(1, TimeUnit.MINUTES) - - then: - assertTraces(1) { - trace(0, 4) { - span(0) { - name "parent" - kind INTERNAL - hasNoParent() - } - producerSpan(it, 1, span(0), topic, msgId) - receiveSpan(it, 2, span(1), topic, msgId) - span(3) { - name "callback" - kind INTERNAL - childOf span(2) - attributes { - } - } - } - } - } - - def "test consume non-partitioned topic using receive with timeout"() { - setup: - def topic = "persistent://public/default/testConsumeNonPartitionedTopicCallReceiveWithTimeout" - admin.topics().createNonPartitionedTopic(topic) - consumer = client.newConsumer(Schema.STRING) - .subscriptionName("test_sub") - .topic(topic) - .subscriptionInitialPosition(SubscriptionInitialPosition.Earliest) - .subscribe() - - producer = client.newProducer(Schema.STRING) - .topic(topic) - .enableBatching(false) - .create() - - when: - def msg = "test" - def msgId = runWithSpan("parent") { - producer.send(msg) - } - - def receivedMsg = consumer.receive(1, TimeUnit.MINUTES) - consumer.acknowledge(receivedMsg) - - then: - assertTraces(1) { - trace(0, 3) { - span(0) { - name "parent" - kind INTERNAL - hasNoParent() - } - producerSpan(it, 1, span(0), topic, msgId) - receiveSpan(it, 2, span(1), topic, msgId) - } - } - } - - def "test consume non-partitioned topic using batchReceive"() { - setup: - def topic = "persistent://public/default/testConsumeNonPartitionedTopicCallBatchReceive" - admin.topics().createNonPartitionedTopic(topic) - consumer = client.newConsumer(Schema.STRING) - .subscriptionName("test_sub") - .topic(topic) - .subscriptionInitialPosition(SubscriptionInitialPosition.Earliest) - .subscribe() - - producer = client.newProducer(Schema.STRING) - .topic(topic) - .enableBatching(false) - .create() - - when: - def msg = "test" - def msgId = runWithSpan("parent") { - producer.send(msg) - } - - runWithSpan("receive-parent") { - def receivedMsg = consumer.batchReceive() - consumer.acknowledge(receivedMsg) - } - - then: - def producer - assertTraces(2) { - trace(0, 2) { - span(0) { - name "parent" - kind INTERNAL - hasNoParent() - } - producerSpan(it, 1, span(0), topic, msgId) - producer = span(1) - } - trace(1, 2) { - span(0) { - name "receive-parent" - kind INTERNAL - hasNoParent() - } - receiveSpan(it, 1, span(0), topic, null, producer) - } - } - } - - def "test consume non-partitioned topic using batchReceiveAsync"() { - setup: - def topic = "persistent://public/default/testConsumeNonPartitionedTopicCallBatchReceiveAsync" - admin.topics().createNonPartitionedTopic(topic) - consumer = client.newConsumer(Schema.STRING) - .subscriptionName("test_sub") - .topic(topic) - .subscriptionInitialPosition(SubscriptionInitialPosition.Earliest) - .subscribe() - - producer = client.newProducer(Schema.STRING) - .topic(topic) - .enableBatching(false) - .create() - - when: - def msg = "test" - def msgId = runWithSpan("parent") { - producer.send(msg) - } - - CompletableFuture> result = runWithSpan("receive-parent") { - consumer.batchReceiveAsync().whenComplete { receivedMsg, throwable -> - runWithSpan("callback") { - consumer.acknowledge(receivedMsg) - } - } - } - result.get(1, TimeUnit.MINUTES).size() == 1 - - then: - def producer - assertTraces(2) { - trace(0, 2) { - span(0) { - name "parent" - kind INTERNAL - hasNoParent() - } - producerSpan(it, 1, span(0), topic, msgId) - producer = span(1) - } - trace(1, 3) { - span(0) { - name "receive-parent" - kind INTERNAL - hasNoParent() - } - receiveSpan(it, 1, span(0), topic, null, producer) - span(2) { - name "callback" - kind INTERNAL - childOf span(1) - attributes { - } - } - } - } - } - - def "capture message header as span attribute"() { - setup: - def topic = "persistent://public/default/testCaptureMessageHeaderTopic" - def latch = new CountDownLatch(1) - admin.topics().createNonPartitionedTopic(topic) - consumer = client.newConsumer(Schema.STRING) - .subscriptionName("test_sub") - .topic(topic) - .subscriptionInitialPosition(SubscriptionInitialPosition.Earliest) - .messageListener(new MessageListener() { - @Override - void received(Consumer consumer, Message msg) { - consumer.acknowledge(msg) - latch.countDown() - } - }) - .subscribe() - - producer = client.newProducer(Schema.STRING) - .topic(topic) - .enableBatching(false) - .create() - - when: - def msg = "test" - def msgId = runWithSpan("parent") { - producer.newMessage().value(msg).property("test-message-header", "test").send() - } - - latch.await(1, TimeUnit.MINUTES) - - then: - assertTraces(1) { - trace(0, 4) { - span(0) { - name "parent" - kind INTERNAL - hasNoParent() - } - producerSpan(it, 1, span(0), topic, msgId, true) - receiveSpan(it, 2, span(1), topic, msgId, null, true) - processSpan(it, 3, span(2), topic, msgId, true) - } - } - } - - def "test send partitioned topic"() { - setup: - def topic = "persistent://public/default/testSendPartitionedTopic" - admin.topics().createPartitionedTopic(topic, 2) - producer = - client.newProducer(Schema.STRING).topic(topic) - .enableBatching(false).create() - - when: - String msg = "test" - def msgId = runWithSpan("parent") { - producer.send(msg) - } - - then: - assertTraces(1) { - trace(0, 2) { - span(0) { - name "parent" - kind INTERNAL - hasNoParent() - } - producerSpan(it, 1, span(0), topic, ~/${topic}-partition-.*send/, { it.startsWith(topic) }, msgId) - } - } - } - - def "test consume partitioned topic"() { - setup: - def topic = "persistent://public/default/testConsumePartitionedTopic" - admin.topics().createPartitionedTopic(topic, 2) - - def latch = new CountDownLatch(1) - consumer = client.newConsumer(Schema.STRING) - .subscriptionName("test_sub") - .subscriptionInitialPosition(SubscriptionInitialPosition.Earliest) - .topic(topic) - .messageListener(new MessageListener() { - @Override - void received(Consumer consumer, Message msg) { - consumer.acknowledge(msg) - latch.countDown() - } - }) - .subscribe() - - producer = client.newProducer(Schema.STRING) - .topic(topic) - .enableBatching(false) - .create() - - when: - def msg = "test" - def msgId = runWithSpan("parent") { - producer.send(msg) - } - - latch.await(1, TimeUnit.MINUTES) - - then: - assertTraces(1) { - trace(0, 4) { - span(0) { - name "parent" - kind INTERNAL - hasNoParent() - } - producerSpan(it, 1, span(0), topic, ~/${topic}-partition-.*send/, { it.startsWith(topic) }, msgId) - receiveSpan(it, 2, span(1), topic, ~/${topic}-partition-.*receive/, { it.startsWith(topic) }, msgId) - processSpan(it, 3, span(2), topic, ~/${topic}-partition-.*process/, { it.startsWith(topic) }, msgId) - } - } - } - - def "test consume multi-topics"() { - setup: - - def topicNamePrefix = "persistent://public/default/testConsumeMulti_" - def topic1 = topicNamePrefix + "1" - def topic2 = topicNamePrefix + "2" - - def latch = new CountDownLatch(2) - producer = client.newProducer(Schema.STRING) - .topic(topic1) - .enableBatching(false) - .create() - producer2 = client.newProducer(Schema.STRING) - .topic(topic2) - .enableBatching(false) - .create() - - when: - runWithSpan("parent1") { - producer.send("test1") - } - runWithSpan("parent2") { - producer2.send("test2") - } - - consumer = client.newConsumer(Schema.STRING) - .topic(topic2, topic1) - .subscriptionName("test_sub") - .subscriptionInitialPosition(SubscriptionInitialPosition.Earliest) - .messageListener(new MessageListener() { - @Override - void received(Consumer consumer, Message msg) { - consumer.acknowledge(msg) - latch.countDown() - } - }) - .subscribe() - - latch.await(1, TimeUnit.MINUTES) - - then: - assertTraces(2) { - traces.sort(orderByRootSpanName("parent1", "parent2")) - for (int i in 1..2) { - def topic = i == 1 ? topic1 : topic2 - trace(i - 1, 4) { - span(0) { - name "parent" + i - kind INTERNAL - hasNoParent() - } - producerSpan(it, 1, span(0), topic, null, { it.startsWith(topicNamePrefix) }, String) - receiveSpan(it, 2, span(1), topic, null, { it.startsWith(topicNamePrefix) }, String) - processSpan(it, 3, span(2), topic, null, { it.startsWith(topicNamePrefix) }, String) - } - } - } - } - - def producerSpan(TraceAssert trace, int index, Object parentSpan, String topic, Object msgId, boolean headers = false) { - producerSpan(trace, index, parentSpan, topic, null, { it == topic }, msgId, headers) - } - - def producerSpan(TraceAssert trace, int index, Object parentSpan, String topic, Pattern namePattern, Closure destination, Object msgId, boolean headers = false) { - trace.span(index) { - if (namePattern != null) { - name namePattern - } else { - name "$topic send" - } - kind PRODUCER - childOf parentSpan - attributes { - "$SemanticAttributes.MESSAGING_SYSTEM" "pulsar" - "$SemanticAttributes.NET_PEER_NAME" brokerHost - "$SemanticAttributes.NET_PEER_PORT" brokerPort - "$SemanticAttributes.MESSAGING_DESTINATION_NAME" destination - if (msgId == String) { - "$SemanticAttributes.MESSAGING_MESSAGE_ID" String - } else if (msgId != null) { - "$SemanticAttributes.MESSAGING_MESSAGE_ID" msgId.toString() - } - "$SemanticAttributes.MESSAGING_MESSAGE_PAYLOAD_SIZE_BYTES" Long - "messaging.pulsar.message.type" "normal" - if (headers) { - "messaging.header.test_message_header" { it == ["test"] } - } - } - } - } - - def receiveSpan(TraceAssert trace, int index, Object parentSpan, String topic, Object msgId, Object linkedSpan = null, boolean headers = false) { - receiveSpan(trace, index, parentSpan, topic, null, { it == topic }, msgId, linkedSpan, headers) - } - - def receiveSpan(TraceAssert trace, int index, Object parentSpan, String topic, Pattern namePattern, Closure destination, Object msgId, Object linkedSpan = null, boolean headers = false) { - trace.span(index) { - if (namePattern != null) { - name namePattern - } else { - name "$topic receive" - } - kind CONSUMER - childOf parentSpan - if (linkedSpan == null) { - hasNoLinks() - } else { - hasLink linkedSpan - } - attributes { - "$SemanticAttributes.MESSAGING_SYSTEM" "pulsar" - "$SemanticAttributes.NET_PEER_NAME" brokerHost - "$SemanticAttributes.NET_PEER_PORT" brokerPort - "$SemanticAttributes.MESSAGING_DESTINATION_NAME" destination - if (msgId == String) { - "$SemanticAttributes.MESSAGING_MESSAGE_ID" String - } else if (msgId != null) { - "$SemanticAttributes.MESSAGING_MESSAGE_ID" msgId.toString() - } - "$SemanticAttributes.MESSAGING_MESSAGE_PAYLOAD_SIZE_BYTES" Long - "$SemanticAttributes.MESSAGING_OPERATION" "receive" - if (headers) { - "messaging.header.test_message_header" { it == ["test"] } - } - } - } - } - - def processSpan(TraceAssert trace, int index, Object parentSpan, String topic, Object msgId, boolean headers = false) { - processSpan(trace, index, parentSpan, topic, null, { it == topic }, msgId, headers) - } - - def processSpan(TraceAssert trace, int index, Object parentSpan, String topic, Pattern namePattern, Closure destination, Object msgId, boolean headers = false) { - trace.span(index) { - if (namePattern != null) { - name namePattern - } else { - name "$topic process" - } - kind INTERNAL - childOf parentSpan - attributes { - "$SemanticAttributes.MESSAGING_SYSTEM" "pulsar" - "$SemanticAttributes.MESSAGING_DESTINATION_NAME" destination - if (msgId == String) { - "$SemanticAttributes.MESSAGING_MESSAGE_ID" String - } else if (msgId != null) { - "$SemanticAttributes.MESSAGING_MESSAGE_ID" msgId.toString() - } - "$SemanticAttributes.MESSAGING_OPERATION" "process" - "$SemanticAttributes.MESSAGING_MESSAGE_PAYLOAD_SIZE_BYTES" Long - if (headers) { - "messaging.header.test_message_header" { it == ["test"] } - } - } - } - } -} diff --git a/instrumentation/pulsar/pulsar-2.8/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/pulsar/v2_8/AbstractPulsarClientTest.java b/instrumentation/pulsar/pulsar-2.8/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/pulsar/v2_8/AbstractPulsarClientTest.java new file mode 100644 index 000000000000..d87708e196bb --- /dev/null +++ b/instrumentation/pulsar/pulsar-2.8/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/pulsar/v2_8/AbstractPulsarClientTest.java @@ -0,0 +1,446 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.pulsar.v2_8; + +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.satisfies; +import static io.opentelemetry.semconv.ServerAttributes.SERVER_ADDRESS; +import static io.opentelemetry.semconv.ServerAttributes.SERVER_PORT; +import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_BATCH_MESSAGE_COUNT; +import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME; +import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_DESTINATION_PARTITION_ID; +import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_MESSAGE_BODY_SIZE; +import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_MESSAGE_ID; +import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_OPERATION; +import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_SYSTEM; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.sdk.testing.assertj.AttributeAssertion; +import io.opentelemetry.sdk.trace.data.LinkData; +import io.opentelemetry.sdk.trace.data.SpanData; +import java.time.Duration; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; +import org.apache.pulsar.client.admin.PulsarAdmin; +import org.apache.pulsar.client.api.Consumer; +import org.apache.pulsar.client.api.Message; +import org.apache.pulsar.client.api.MessageId; +import org.apache.pulsar.client.api.Messages; +import org.apache.pulsar.client.api.Producer; +import org.apache.pulsar.client.api.PulsarClient; +import org.apache.pulsar.client.api.PulsarClientException; +import org.apache.pulsar.client.api.Schema; +import org.apache.pulsar.client.api.SubscriptionInitialPosition; +import org.apache.pulsar.common.naming.TopicName; +import org.assertj.core.api.AbstractLongAssert; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testcontainers.containers.PulsarContainer; +import org.testcontainers.containers.output.Slf4jLogConsumer; +import org.testcontainers.utility.DockerImageName; + +abstract class AbstractPulsarClientTest { + + private static final Logger logger = LoggerFactory.getLogger(AbstractPulsarClientTest.class); + + private static final DockerImageName DEFAULT_IMAGE_NAME = + DockerImageName.parse("apachepulsar/pulsar:2.8.0"); + + @RegisterExtension + static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + private static PulsarContainer pulsar; + static PulsarClient client; + static PulsarAdmin admin; + static Producer producer; + static Consumer consumer; + static Producer producer2; + + static String brokerHost; + static int brokerPort; + + private static final AttributeKey MESSAGE_TYPE = + AttributeKey.stringKey("messaging.pulsar.message.type"); + static final double[] DURATION_BUCKETS = + new double[] { + 0.005, 0.01, 0.025, 0.05, 0.075, 0.1, 0.25, 0.5, 0.75, 1.0, 2.5, 5.0, 7.5, 10.0 + }; + + @BeforeAll + static void beforeAll() throws PulsarClientException { + pulsar = + new PulsarContainer(DEFAULT_IMAGE_NAME) + .withEnv("PULSAR_MEM", "-Xmx128m") + .withLogConsumer(new Slf4jLogConsumer(logger)) + .withStartupTimeout(Duration.ofMinutes(2)); + pulsar.start(); + + brokerHost = pulsar.getHost(); + brokerPort = pulsar.getMappedPort(6650); + client = PulsarClient.builder().serviceUrl(pulsar.getPulsarBrokerUrl()).build(); + admin = PulsarAdmin.builder().serviceHttpUrl(pulsar.getHttpServiceUrl()).build(); + } + + @AfterEach + void afterEach() throws PulsarClientException { + if (producer != null) { + producer.close(); + } + if (consumer != null) { + consumer.close(); + } + if (producer2 != null) { + producer2.close(); + } + } + + @AfterAll + static void afterAll() throws PulsarClientException { + if (client != null) { + client.close(); + } + if (admin != null) { + admin.close(); + } + pulsar.close(); + } + + @Test + void testConsumeNonPartitionedTopicUsingBatchReceive() throws Exception { + String topic = "persistent://public/default/testConsumeNonPartitionedTopicCallBatchReceive"; + admin.topics().createNonPartitionedTopic(topic); + consumer = + client + .newConsumer(Schema.STRING) + .subscriptionName("test_sub") + .topic(topic) + .subscriptionInitialPosition(SubscriptionInitialPosition.Earliest) + .subscribe(); + + producer = client.newProducer(Schema.STRING).topic(topic).enableBatching(false).create(); + + String msg = "test"; + MessageId msgId = testing.runWithSpan("parent", () -> producer.send(msg)); + + testing.runWithSpan( + "receive-parent", + () -> { + Messages receivedMsg = consumer.batchReceive(); + consumer.acknowledge(receivedMsg); + }); + AtomicReference producerSpan = new AtomicReference<>(); + + testing.waitAndAssertTraces( + trace -> { + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(), + span -> + span.hasName(topic + " publish") + .hasKind(SpanKind.PRODUCER) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + sendAttributes(topic, msgId.toString(), false))); + producerSpan.set(trace.getSpan(1)); + }, + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("receive-parent").hasKind(SpanKind.INTERNAL).hasNoParent(), + span -> + span.hasName(topic + " receive") + .hasKind(SpanKind.CONSUMER) + .hasLinks(LinkData.create(producerSpan.get().getSpanContext())) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + batchReceiveAttributes(topic, null, false)))); + + assertThat(testing.metrics()) + .satisfiesExactlyInAnyOrder( + metric -> + assertThat(metric) + .hasName("messaging.receive.duration") + .hasUnit("s") + .hasDescription("Measures the duration of receive operation.") + .hasHistogramSatisfying( + histogram -> + histogram.hasPointsSatisfying( + point -> + point + .hasSumGreaterThan(0.0) + .hasAttributesSatisfying( + equalTo(MESSAGING_SYSTEM, "pulsar"), + equalTo(MESSAGING_DESTINATION_NAME, topic), + equalTo(SERVER_PORT, brokerPort), + equalTo(SERVER_ADDRESS, brokerHost)) + .hasBucketBoundaries(DURATION_BUCKETS))), + metric -> + assertThat(metric) + .hasName("messaging.publish.duration") + .hasUnit("s") + .hasDescription("Measures the duration of publish operation.") + .hasHistogramSatisfying( + histogram -> + histogram.hasPointsSatisfying( + point -> + point + .hasSumGreaterThan(0.0) + .hasAttributesSatisfying( + equalTo(MESSAGING_SYSTEM, "pulsar"), + equalTo(MESSAGING_DESTINATION_NAME, topic), + equalTo(SERVER_PORT, brokerPort), + equalTo(SERVER_ADDRESS, brokerHost)) + .hasBucketBoundaries(DURATION_BUCKETS))), + metric -> + assertThat(metric) + .hasName("messaging.receive.messages") + .hasUnit("{message}") + .hasDescription("Measures the number of received messages.") + .hasLongSumSatisfying( + sum -> + sum.hasPointsSatisfying( + point -> + point + .hasValue(1) + .hasAttributesSatisfying( + equalTo(MESSAGING_SYSTEM, "pulsar"), + equalTo(MESSAGING_DESTINATION_NAME, topic), + equalTo(SERVER_PORT, brokerPort), + equalTo(SERVER_ADDRESS, brokerHost))))); + } + + @Test + void testConsumeNonPartitionedTopicUsingBatchReceiveAsync() throws Exception { + String topic = + "persistent://public/default/testConsumeNonPartitionedTopicCallBatchReceiveAsync"; + admin.topics().createNonPartitionedTopic(topic); + consumer = + client + .newConsumer(Schema.STRING) + .subscriptionName("test_sub") + .topic(topic) + .subscriptionInitialPosition(SubscriptionInitialPosition.Earliest) + .subscribe(); + + producer = client.newProducer(Schema.STRING).topic(topic).enableBatching(false).create(); + + String msg = "test"; + MessageId msgId = testing.runWithSpan("parent", () -> producer.send(msg)); + + CompletableFuture> result = + testing.runWithSpan( + "receive-parent", + () -> + consumer + .batchReceiveAsync() + .whenComplete( + (messages, throwable) -> { + if (messages != null) { + testing.runWithSpan( + "callback", () -> acknowledgeMessages(consumer, messages)); + } + })); + + assertThat(result.get(1, TimeUnit.MINUTES).size()).isEqualTo(1); + + AtomicReference producerSpan = new AtomicReference<>(); + testing.waitAndAssertTraces( + trace -> { + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(), + span -> + span.hasName(topic + " publish") + .hasKind(SpanKind.PRODUCER) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + sendAttributes(topic, msgId.toString(), false))); + + producerSpan.set(trace.getSpan(1)); + }, + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("receive-parent").hasKind(SpanKind.INTERNAL).hasNoParent(), + span -> + span.hasName(topic + " receive") + .hasKind(SpanKind.CONSUMER) + .hasParent(trace.getSpan(0)) + .hasLinks(LinkData.create(producerSpan.get().getSpanContext())) + .hasAttributesSatisfyingExactly(batchReceiveAttributes(topic, null, false)), + span -> + span.hasName("callback") + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(1)))); + + assertThat(testing.metrics()) + .satisfiesExactlyInAnyOrder( + metric -> + assertThat(metric) + .hasName("messaging.receive.duration") + .hasUnit("s") + .hasDescription("Measures the duration of receive operation.") + .hasHistogramSatisfying( + histogram -> + histogram.hasPointsSatisfying( + point -> + point + .hasSumGreaterThan(0.0) + .hasAttributesSatisfying( + equalTo(MESSAGING_SYSTEM, "pulsar"), + equalTo(MESSAGING_DESTINATION_NAME, topic), + equalTo(SERVER_PORT, brokerPort), + equalTo(SERVER_ADDRESS, brokerHost)) + .hasBucketBoundaries(DURATION_BUCKETS))), + metric -> + assertThat(metric) + .hasName("messaging.publish.duration") + .hasUnit("s") + .hasDescription("Measures the duration of publish operation.") + .hasHistogramSatisfying( + histogram -> + histogram.hasPointsSatisfying( + point -> + point + .hasSumGreaterThan(0.0) + .hasAttributesSatisfying( + equalTo(MESSAGING_SYSTEM, "pulsar"), + equalTo(MESSAGING_DESTINATION_NAME, topic), + equalTo(SERVER_PORT, brokerPort), + equalTo(SERVER_ADDRESS, brokerHost)) + .hasBucketBoundaries(DURATION_BUCKETS))), + metric -> + assertThat(metric) + .hasName("messaging.receive.messages") + .hasUnit("{message}") + .hasDescription("Measures the number of received messages.") + .hasLongSumSatisfying( + sum -> + sum.hasPointsSatisfying( + point -> + point + .hasValue(1) + .hasAttributesSatisfying( + equalTo(MESSAGING_SYSTEM, "pulsar"), + equalTo(MESSAGING_DESTINATION_NAME, topic), + equalTo(SERVER_PORT, brokerPort), + equalTo(SERVER_ADDRESS, brokerHost))))); + } + + static List sendAttributes( + String destination, String messageId, boolean testHeaders) { + List assertions = + new ArrayList<>( + Arrays.asList( + equalTo(MESSAGING_SYSTEM, "pulsar"), + equalTo(SERVER_ADDRESS, brokerHost), + equalTo(SERVER_PORT, brokerPort), + equalTo(MESSAGING_DESTINATION_NAME, destination), + equalTo(MESSAGING_OPERATION, "publish"), + equalTo(MESSAGING_MESSAGE_ID, messageId), + satisfies(MESSAGING_MESSAGE_BODY_SIZE, AbstractLongAssert::isNotNegative), + equalTo(MESSAGE_TYPE, "normal"))); + + if (testHeaders) { + assertions.add( + equalTo( + AttributeKey.stringArrayKey("messaging.header.test_message_header"), + Collections.singletonList("test"))); + } + int partitionIndex = TopicName.getPartitionIndex(destination); + if (partitionIndex != -1) { + assertions.add(equalTo(MESSAGING_DESTINATION_PARTITION_ID, String.valueOf(partitionIndex))); + } + return assertions; + } + + static List batchReceiveAttributes( + String destination, String messageId, boolean testHeaders) { + return receiveAttributes(destination, messageId, testHeaders, true); + } + + static List receiveAttributes( + String destination, String messageId, boolean testHeaders) { + return receiveAttributes(destination, messageId, testHeaders, false); + } + + static List receiveAttributes( + String destination, String messageId, boolean testHeaders, boolean isBatch) { + List assertions = + new ArrayList<>( + Arrays.asList( + equalTo(MESSAGING_SYSTEM, "pulsar"), + equalTo(SERVER_ADDRESS, brokerHost), + equalTo(SERVER_PORT, brokerPort), + equalTo(MESSAGING_DESTINATION_NAME, destination), + equalTo(MESSAGING_OPERATION, "receive"), + equalTo(MESSAGING_MESSAGE_ID, messageId), + satisfies(MESSAGING_MESSAGE_BODY_SIZE, AbstractLongAssert::isNotNegative))); + if (testHeaders) { + assertions.add( + equalTo( + AttributeKey.stringArrayKey("messaging.header.test_message_header"), + Collections.singletonList("test"))); + } + if (isBatch) { + assertions.add(satisfies(MESSAGING_BATCH_MESSAGE_COUNT, AbstractLongAssert::isPositive)); + } + int partitionIndex = TopicName.getPartitionIndex(destination); + if (partitionIndex != -1) { + assertions.add(equalTo(MESSAGING_DESTINATION_PARTITION_ID, String.valueOf(partitionIndex))); + } + return assertions; + } + + static List processAttributes( + String destination, String messageId, boolean testHeaders) { + List assertions = + new ArrayList<>( + Arrays.asList( + equalTo(MESSAGING_SYSTEM, "pulsar"), + equalTo(MESSAGING_DESTINATION_NAME, destination), + equalTo(MESSAGING_OPERATION, "process"), + equalTo(MESSAGING_MESSAGE_ID, messageId), + satisfies(MESSAGING_MESSAGE_BODY_SIZE, AbstractLongAssert::isNotNegative))); + if (testHeaders) { + assertions.add( + equalTo( + AttributeKey.stringArrayKey("messaging.header.test_message_header"), + Collections.singletonList("test"))); + } + int partitionIndex = TopicName.getPartitionIndex(destination); + if (partitionIndex != -1) { + assertions.add(equalTo(MESSAGING_DESTINATION_PARTITION_ID, String.valueOf(partitionIndex))); + } + return assertions; + } + + static void acknowledgeMessage(Consumer consumer, Message message) { + try { + consumer.acknowledge(message); + } catch (PulsarClientException exception) { + throw new RuntimeException(exception); + } + } + + static void acknowledgeMessages(Consumer consumer, Messages messages) { + try { + consumer.acknowledge(messages); + } catch (PulsarClientException exception) { + throw new RuntimeException(exception); + } + } +} diff --git a/instrumentation/pulsar/pulsar-2.8/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/pulsar/v2_8/PulsarClientSuppressReceiveSpansTest.java b/instrumentation/pulsar/pulsar-2.8/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/pulsar/v2_8/PulsarClientSuppressReceiveSpansTest.java new file mode 100644 index 000000000000..13d41145c141 --- /dev/null +++ b/instrumentation/pulsar/pulsar-2.8/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/pulsar/v2_8/PulsarClientSuppressReceiveSpansTest.java @@ -0,0 +1,346 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.pulsar.v2_8; + +import static io.opentelemetry.instrumentation.testing.util.TelemetryDataUtil.orderByRootSpanName; + +import io.opentelemetry.api.trace.SpanKind; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import org.apache.pulsar.client.api.Message; +import org.apache.pulsar.client.api.MessageId; +import org.apache.pulsar.client.api.MessageListener; +import org.apache.pulsar.client.api.Schema; +import org.apache.pulsar.client.api.SubscriptionInitialPosition; +import org.junit.jupiter.api.Test; + +class PulsarClientSuppressReceiveSpansTest extends AbstractPulsarClientTest { + + @Test + void testConsumeNonPartitionedTopic() throws Exception { + String topic = "persistent://public/default/testConsumeNonPartitionedTopic"; + CountDownLatch latch = new CountDownLatch(1); + admin.topics().createNonPartitionedTopic(topic); + consumer = + client + .newConsumer(Schema.STRING) + .subscriptionName("test_sub") + .topic(topic) + .subscriptionInitialPosition(SubscriptionInitialPosition.Earliest) + .messageListener( + (MessageListener) + (consumer, msg) -> { + acknowledgeMessage(consumer, msg); + latch.countDown(); + }) + .subscribe(); + + producer = client.newProducer(Schema.STRING).topic(topic).enableBatching(false).create(); + + String msg = "test"; + MessageId msgId = testing.runWithSpan("parent", () -> producer.send(msg)); + + latch.await(1, TimeUnit.MINUTES); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(), + span -> + span.hasName(topic + " publish") + .hasKind(SpanKind.PRODUCER) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + sendAttributes(topic, msgId.toString(), false)), + span -> + span.hasName(topic + " process") + .hasKind(SpanKind.CONSUMER) + .hasParent(trace.getSpan(1)) + .hasAttributesSatisfyingExactly( + processAttributes(topic, msgId.toString(), false)))); + } + + @Test + void testConsumeNonPartitionedTopicUsingReceive() throws Exception { + String topic = "persistent://public/default/testConsumeNonPartitionedTopicCallReceive"; + admin.topics().createNonPartitionedTopic(topic); + consumer = + client + .newConsumer(Schema.STRING) + .subscriptionName("test_sub") + .topic(topic) + .subscriptionInitialPosition(SubscriptionInitialPosition.Earliest) + .subscribe(); + producer = client.newProducer(Schema.STRING).topic(topic).enableBatching(false).create(); + + String msg = "test"; + MessageId msgId = testing.runWithSpan("parent", () -> producer.send(msg)); + + Message receivedMsg = consumer.receive(); + consumer.acknowledge(receivedMsg); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(), + span -> + span.hasName(topic + " publish") + .hasKind(SpanKind.PRODUCER) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + sendAttributes(topic, msgId.toString(), false)), + span -> + span.hasName(topic + " receive") + .hasKind(SpanKind.CONSUMER) + .hasParent(trace.getSpan(1)) + .hasAttributesSatisfyingExactly( + receiveAttributes(topic, msgId.toString(), false)))); + } + + @Test + void testConsumeNonPartitionedTopicUsingReceiveAsync() throws Exception { + String topic = "persistent://public/default/testConsumeNonPartitionedTopicCallReceiveAsync"; + admin.topics().createNonPartitionedTopic(topic); + consumer = + client + .newConsumer(Schema.STRING) + .subscriptionName("test_sub") + .topic(topic) + .subscriptionInitialPosition(SubscriptionInitialPosition.Earliest) + .subscribe(); + + producer = client.newProducer(Schema.STRING).topic(topic).enableBatching(false).create(); + + CompletableFuture> result = + consumer + .receiveAsync() + .whenComplete( + (message, throwable) -> { + if (message != null) { + testing.runWithSpan("callback", () -> acknowledgeMessage(consumer, message)); + } + }); + + String msg = "test"; + MessageId msgId = testing.runWithSpan("parent", () -> producer.send(msg)); + + result.get(1, TimeUnit.MINUTES); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(), + span -> + span.hasName(topic + " publish") + .hasKind(SpanKind.PRODUCER) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + sendAttributes(topic, msgId.toString(), false)), + span -> + span.hasName(topic + " receive") + .hasKind(SpanKind.CONSUMER) + .hasParent(trace.getSpan(1)) + .hasAttributesSatisfyingExactly( + receiveAttributes(topic, msgId.toString(), false)), + span -> + span.hasName("callback") + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(2)))); + } + + @Test + void testConsumeNonPartitionedTopicUsingReceiveWithTimeout() throws Exception { + String topic = + "persistent://public/default/testConsumeNonPartitionedTopicCallReceiveWithTimeout"; + admin.topics().createNonPartitionedTopic(topic); + consumer = + client + .newConsumer(Schema.STRING) + .subscriptionName("test_sub") + .topic(topic) + .subscriptionInitialPosition(SubscriptionInitialPosition.Earliest) + .subscribe(); + producer = client.newProducer(Schema.STRING).topic(topic).enableBatching(false).create(); + + String msg = "test"; + MessageId msgId = testing.runWithSpan("parent", () -> producer.send(msg)); + + Message receivedMsg = consumer.receive(1, TimeUnit.MINUTES); + consumer.acknowledge(receivedMsg); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(), + span -> + span.hasName(topic + " publish") + .hasKind(SpanKind.PRODUCER) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + sendAttributes(topic, msgId.toString(), false)), + span -> + span.hasName(topic + " receive") + .hasKind(SpanKind.CONSUMER) + .hasParent(trace.getSpan(1)) + .hasAttributesSatisfyingExactly( + receiveAttributes(topic, msgId.toString(), false)))); + } + + @Test + void captureMessageHeaderAsSpanAttribute() throws Exception { + String topic = "persistent://public/default/testCaptureMessageHeaderTopic"; + CountDownLatch latch = new CountDownLatch(1); + admin.topics().createNonPartitionedTopic(topic); + consumer = + client + .newConsumer(Schema.STRING) + .subscriptionName("test_sub") + .topic(topic) + .subscriptionInitialPosition(SubscriptionInitialPosition.Earliest) + .messageListener( + (MessageListener) + (consumer, msg) -> { + acknowledgeMessage(consumer, msg); + latch.countDown(); + }) + .subscribe(); + + producer = client.newProducer(Schema.STRING).topic(topic).enableBatching(false).create(); + + String msg = "test"; + MessageId msgId = + testing.runWithSpan( + "parent", + () -> producer.newMessage().value(msg).property("test-message-header", "test").send()); + + latch.await(1, TimeUnit.MINUTES); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(), + span -> + span.hasName(topic + " publish") + .hasKind(SpanKind.PRODUCER) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + sendAttributes(topic, msgId.toString(), true)), + span -> + span.hasName(topic + " process") + .hasKind(SpanKind.CONSUMER) + .hasParent(trace.getSpan(1)) + .hasAttributesSatisfyingExactly( + processAttributes(topic, msgId.toString(), true)))); + } + + @Test + void testConsumePartitionedTopic() throws Exception { + String topic = "persistent://public/default/testConsumePartitionedTopic"; + admin.topics().createPartitionedTopic(topic, 1); + CountDownLatch latch = new CountDownLatch(1); + + consumer = + client + .newConsumer(Schema.STRING) + .subscriptionName("test_sub") + .subscriptionInitialPosition(SubscriptionInitialPosition.Earliest) + .topic(topic) + .messageListener( + (MessageListener) + (consumer, msg) -> { + acknowledgeMessage(consumer, msg); + latch.countDown(); + }) + .subscribe(); + + producer = client.newProducer(Schema.STRING).topic(topic).enableBatching(false).create(); + + String msg = "test"; + MessageId msgId = testing.runWithSpan("parent", () -> producer.send(msg)); + + latch.await(1, TimeUnit.MINUTES); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(), + span -> + span.hasName(topic + "-partition-0 publish") + .hasKind(SpanKind.PRODUCER) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + sendAttributes(topic + "-partition-0", msgId.toString(), false)), + span -> + span.hasName(topic + "-partition-0 process") + .hasKind(SpanKind.CONSUMER) + .hasParent(trace.getSpan(1)) + .hasAttributesSatisfyingExactly( + processAttributes(topic + "-partition-0", msgId.toString(), false)))); + } + + @Test + void testConsumeMultiTopics() throws Exception { + String topicNamePrefix = "persistent://public/default/testConsumeMulti_"; + String topic1 = topicNamePrefix + "1"; + String topic2 = topicNamePrefix + "2"; + CountDownLatch latch = new CountDownLatch(2); + producer = client.newProducer(Schema.STRING).topic(topic1).enableBatching(false).create(); + producer2 = client.newProducer(Schema.STRING).topic(topic2).enableBatching(false).create(); + + MessageId msgId1 = testing.runWithSpan("parent1", () -> producer.send("test1")); + MessageId msgId2 = testing.runWithSpan("parent2", () -> producer2.send("test2")); + + consumer = + client + .newConsumer(Schema.STRING) + .topic(topic2, topic1) + .subscriptionName("test_sub") + .subscriptionInitialPosition(SubscriptionInitialPosition.Earliest) + .messageListener( + (MessageListener) + (consumer, msg) -> { + acknowledgeMessage(consumer, msg); + latch.countDown(); + }) + .subscribe(); + + latch.await(1, TimeUnit.MINUTES); + + testing.waitAndAssertSortedTraces( + orderByRootSpanName("parent1", "parent2"), + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent1").hasKind(SpanKind.INTERNAL).hasNoParent(), + span -> + span.hasName(topic1 + " publish") + .hasKind(SpanKind.PRODUCER) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + sendAttributes(topic1, msgId1.toString(), false)), + span -> + span.hasName(topic1 + " process") + .hasKind(SpanKind.CONSUMER) + .hasParent(trace.getSpan(1)) + .hasAttributesSatisfyingExactly( + processAttributes(topic1, msgId1.toString(), false))), + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent2").hasKind(SpanKind.INTERNAL).hasNoParent(), + span -> + span.hasName(topic2 + " publish") + .hasKind(SpanKind.PRODUCER) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + sendAttributes(topic2, msgId2.toString(), false)), + span -> + span.hasName(topic2 + " process") + .hasKind(SpanKind.CONSUMER) + .hasParent(trace.getSpan(1)) + .hasAttributesSatisfyingExactly( + processAttributes(topic2, msgId2.toString(), false)))); + } +} diff --git a/instrumentation/pulsar/pulsar-2.8/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/pulsar/v2_8/PulsarClientTest.java b/instrumentation/pulsar/pulsar-2.8/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/pulsar/v2_8/PulsarClientTest.java new file mode 100644 index 000000000000..93f13f9ee1d8 --- /dev/null +++ b/instrumentation/pulsar/pulsar-2.8/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/pulsar/v2_8/PulsarClientTest.java @@ -0,0 +1,674 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.pulsar.v2_8; + +import static io.opentelemetry.instrumentation.testing.util.TelemetryDataUtil.orderByRootSpanKind; +import static io.opentelemetry.instrumentation.testing.util.TelemetryDataUtil.orderByRootSpanName; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; +import static io.opentelemetry.semconv.ServerAttributes.SERVER_ADDRESS; +import static io.opentelemetry.semconv.ServerAttributes.SERVER_PORT; +import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME; +import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_SYSTEM; + +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.sdk.trace.data.LinkData; +import io.opentelemetry.sdk.trace.data.SpanData; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; +import org.apache.pulsar.client.api.Message; +import org.apache.pulsar.client.api.MessageId; +import org.apache.pulsar.client.api.MessageListener; +import org.apache.pulsar.client.api.Messages; +import org.apache.pulsar.client.api.Schema; +import org.apache.pulsar.client.api.SubscriptionInitialPosition; +import org.junit.jupiter.api.Test; + +class PulsarClientTest extends AbstractPulsarClientTest { + + @Test + void testConsumeNonPartitionedTopic() throws Exception { + String topic = "persistent://public/default/testConsumeNonPartitionedTopic"; + CountDownLatch latch = new CountDownLatch(1); + admin.topics().createNonPartitionedTopic(topic); + consumer = + client + .newConsumer(Schema.STRING) + .subscriptionName("test_sub") + .topic(topic) + .subscriptionInitialPosition(SubscriptionInitialPosition.Earliest) + .messageListener( + (MessageListener) + (consumer, msg) -> { + acknowledgeMessage(consumer, msg); + latch.countDown(); + }) + .subscribe(); + + producer = client.newProducer(Schema.STRING).topic(topic).enableBatching(false).create(); + + String msg = "test"; + MessageId msgId = testing.runWithSpan("parent", () -> producer.send(msg)); + + latch.await(1, TimeUnit.MINUTES); + + AtomicReference producerSpan = new AtomicReference<>(); + testing.waitAndAssertSortedTraces( + orderByRootSpanKind(SpanKind.INTERNAL, SpanKind.CONSUMER), + trace -> { + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(), + span -> + span.hasName(topic + " publish") + .hasKind(SpanKind.PRODUCER) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + sendAttributes(topic, msgId.toString(), false))); + + producerSpan.set(trace.getSpan(1)); + }, + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName(topic + " receive") + .hasKind(SpanKind.CONSUMER) + .hasNoParent() + .hasLinks(LinkData.create(producerSpan.get().getSpanContext())) + .hasAttributesSatisfyingExactly( + receiveAttributes(topic, msgId.toString(), false)), + span -> + span.hasName(topic + " process") + .hasKind(SpanKind.CONSUMER) + .hasParent(trace.getSpan(0)) + .hasLinks(LinkData.create(producerSpan.get().getSpanContext())) + .hasAttributesSatisfyingExactly( + processAttributes(topic, msgId.toString(), false)))); + } + + @Test + void testConsumeNonPartitionedTopicUsingReceive() throws Exception { + String topic = "persistent://public/default/testConsumeNonPartitionedTopicCallReceive"; + admin.topics().createNonPartitionedTopic(topic); + consumer = + client + .newConsumer(Schema.STRING) + .subscriptionName("test_sub") + .topic(topic) + .subscriptionInitialPosition(SubscriptionInitialPosition.Earliest) + .subscribe(); + producer = client.newProducer(Schema.STRING).topic(topic).enableBatching(false).create(); + + String msg = "test"; + MessageId msgId = testing.runWithSpan("parent", () -> producer.send(msg)); + + Message receivedMsg = consumer.receive(); + consumer.acknowledge(receivedMsg); + + AtomicReference producerSpan = new AtomicReference<>(); + testing.waitAndAssertSortedTraces( + orderByRootSpanKind(SpanKind.INTERNAL, SpanKind.CONSUMER), + trace -> { + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(), + span -> + span.hasName(topic + " publish") + .hasKind(SpanKind.PRODUCER) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + sendAttributes(topic, msgId.toString(), false))); + + producerSpan.set(trace.getSpan(1)); + }, + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName(topic + " receive") + .hasKind(SpanKind.CONSUMER) + .hasNoParent() + .hasLinks(LinkData.create(producerSpan.get().getSpanContext())) + .hasAttributesSatisfyingExactly( + receiveAttributes(topic, msgId.toString(), false)))); + + assertThat(testing.metrics()) + .satisfiesExactlyInAnyOrder( + metric -> + assertThat(metric) + .hasName("messaging.receive.duration") + .hasUnit("s") + .hasDescription("Measures the duration of receive operation.") + .hasHistogramSatisfying( + histogram -> + histogram.hasPointsSatisfying( + point -> + point + .hasSumGreaterThan(0.0) + .hasAttributesSatisfying( + equalTo(MESSAGING_SYSTEM, "pulsar"), + equalTo(MESSAGING_DESTINATION_NAME, topic), + equalTo(SERVER_PORT, brokerPort), + equalTo(SERVER_ADDRESS, brokerHost)) + .hasBucketBoundaries(DURATION_BUCKETS))), + metric -> + assertThat(metric) + .hasName("messaging.receive.messages") + .hasUnit("{message}") + .hasDescription("Measures the number of received messages.") + .hasLongSumSatisfying( + sum -> { + sum.hasPointsSatisfying( + point -> { + point + .hasValue(1) + .hasAttributesSatisfying( + equalTo(MESSAGING_SYSTEM, "pulsar"), + equalTo(MESSAGING_DESTINATION_NAME, topic), + equalTo(SERVER_PORT, brokerPort), + equalTo(SERVER_ADDRESS, brokerHost)); + }); + }), + metric -> + assertThat(metric) + .hasName("messaging.publish.duration") + .hasUnit("s") + .hasDescription("Measures the duration of publish operation.") + .hasHistogramSatisfying( + histogram -> + histogram.hasPointsSatisfying( + point -> + point + .hasSumGreaterThan(0.0) + .hasAttributesSatisfying( + equalTo(MESSAGING_SYSTEM, "pulsar"), + equalTo(MESSAGING_DESTINATION_NAME, topic), + equalTo(SERVER_PORT, brokerPort), + equalTo(SERVER_ADDRESS, brokerHost)) + .hasBucketBoundaries(DURATION_BUCKETS)))); + } + + @Test + void testConsumeNonPartitionedTopicUsingReceiveAsync() throws Exception { + String topic = "persistent://public/default/testConsumeNonPartitionedTopicCallReceiveAsync"; + admin.topics().createNonPartitionedTopic(topic); + consumer = + client + .newConsumer(Schema.STRING) + .subscriptionName("test_sub") + .topic(topic) + .subscriptionInitialPosition(SubscriptionInitialPosition.Earliest) + .subscribe(); + + producer = client.newProducer(Schema.STRING).topic(topic).enableBatching(false).create(); + + CompletableFuture> result = + consumer + .receiveAsync() + .whenComplete( + (message, throwable) -> { + if (message != null) { + testing.runWithSpan("callback", () -> acknowledgeMessage(consumer, message)); + } + }); + + String msg = "test"; + MessageId msgId = testing.runWithSpan("parent", () -> producer.send(msg)); + + result.get(1, TimeUnit.MINUTES); + + AtomicReference producerSpan = new AtomicReference<>(); + testing.waitAndAssertSortedTraces( + orderByRootSpanKind(SpanKind.INTERNAL, SpanKind.CONSUMER), + trace -> { + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(), + span -> + span.hasName(topic + " publish") + .hasKind(SpanKind.PRODUCER) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + sendAttributes(topic, msgId.toString(), false))); + + producerSpan.set(trace.getSpan(1)); + }, + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName(topic + " receive") + .hasKind(SpanKind.CONSUMER) + .hasNoParent() + .hasLinks(LinkData.create(producerSpan.get().getSpanContext())) + .hasAttributesSatisfyingExactly( + receiveAttributes(topic, msgId.toString(), false)), + span -> + span.hasName("callback") + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(0)))); + + assertThat(testing.metrics()) + .satisfiesExactlyInAnyOrder( + metric -> + assertThat(metric) + .hasName("messaging.receive.duration") + .hasUnit("s") + .hasDescription("Measures the duration of receive operation.") + .hasHistogramSatisfying( + histogram -> + histogram.hasPointsSatisfying( + point -> + point + .hasSumGreaterThan(0.0) + .hasAttributesSatisfying( + equalTo(MESSAGING_SYSTEM, "pulsar"), + equalTo(MESSAGING_DESTINATION_NAME, topic), + equalTo(SERVER_PORT, brokerPort), + equalTo(SERVER_ADDRESS, brokerHost)) + .hasBucketBoundaries(DURATION_BUCKETS))), + metric -> + assertThat(metric) + .hasName("messaging.receive.messages") + .hasUnit("{message}") + .hasDescription("Measures the number of received messages.") + .hasLongSumSatisfying( + sum -> { + sum.hasPointsSatisfying( + point -> { + point + .hasValue(1) + .hasAttributesSatisfying( + equalTo(MESSAGING_SYSTEM, "pulsar"), + equalTo(MESSAGING_DESTINATION_NAME, topic), + equalTo(SERVER_PORT, brokerPort), + equalTo(SERVER_ADDRESS, brokerHost)); + }); + }), + metric -> + assertThat(metric) + .hasName("messaging.publish.duration") + .hasUnit("s") + .hasDescription("Measures the duration of publish operation.") + .hasHistogramSatisfying( + histogram -> + histogram.hasPointsSatisfying( + point -> + point + .hasSumGreaterThan(0.0) + .hasAttributesSatisfying( + equalTo(MESSAGING_SYSTEM, "pulsar"), + equalTo(MESSAGING_DESTINATION_NAME, topic), + equalTo(SERVER_PORT, brokerPort), + equalTo(SERVER_ADDRESS, brokerHost)) + .hasBucketBoundaries(DURATION_BUCKETS)))); + } + + @Test + void testConsumeNonPartitionedTopicUsingReceiveWithTimeout() throws Exception { + String topic = + "persistent://public/default/testConsumeNonPartitionedTopicCallReceiveWithTimeout"; + admin.topics().createNonPartitionedTopic(topic); + consumer = + client + .newConsumer(Schema.STRING) + .subscriptionName("test_sub") + .topic(topic) + .subscriptionInitialPosition(SubscriptionInitialPosition.Earliest) + .subscribe(); + producer = client.newProducer(Schema.STRING).topic(topic).enableBatching(false).create(); + + String msg = "test"; + MessageId msgId = testing.runWithSpan("parent", () -> producer.send(msg)); + + Message receivedMsg = consumer.receive(1, TimeUnit.MINUTES); + consumer.acknowledge(receivedMsg); + + AtomicReference producerSpan = new AtomicReference<>(); + testing.waitAndAssertSortedTraces( + orderByRootSpanKind(SpanKind.INTERNAL, SpanKind.CONSUMER), + trace -> { + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(), + span -> + span.hasName(topic + " publish") + .hasKind(SpanKind.PRODUCER) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + sendAttributes(topic, msgId.toString(), false))); + + producerSpan.set(trace.getSpan(1)); + }, + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName(topic + " receive") + .hasKind(SpanKind.CONSUMER) + .hasNoParent() + .hasLinks(LinkData.create(producerSpan.get().getSpanContext())) + .hasAttributesSatisfyingExactly( + receiveAttributes(topic, msgId.toString(), false)))); + + assertThat(testing.metrics()) + .satisfiesExactlyInAnyOrder( + metric -> + assertThat(metric) + .hasName("messaging.receive.duration") + .hasUnit("s") + .hasDescription("Measures the duration of receive operation.") + .hasHistogramSatisfying( + histogram -> + histogram.hasPointsSatisfying( + point -> + point + .hasSumGreaterThan(0.0) + .hasAttributesSatisfying( + equalTo(MESSAGING_SYSTEM, "pulsar"), + equalTo(MESSAGING_DESTINATION_NAME, topic), + equalTo(SERVER_PORT, brokerPort), + equalTo(SERVER_ADDRESS, brokerHost)) + .hasBucketBoundaries(DURATION_BUCKETS))), + metric -> + assertThat(metric) + .hasName("messaging.receive.messages") + .hasUnit("{message}") + .hasDescription("Measures the number of received messages.") + .hasLongSumSatisfying( + sum -> { + sum.hasPointsSatisfying( + point -> { + point + .hasValue(1) + .hasAttributesSatisfying( + equalTo(MESSAGING_SYSTEM, "pulsar"), + equalTo(MESSAGING_DESTINATION_NAME, topic), + equalTo(SERVER_PORT, brokerPort), + equalTo(SERVER_ADDRESS, brokerHost)); + }); + }), + metric -> + assertThat(metric) + .hasName("messaging.publish.duration") + .hasUnit("s") + .hasDescription("Measures the duration of publish operation.") + .hasHistogramSatisfying( + histogram -> + histogram.hasPointsSatisfying( + point -> + point + .hasSumGreaterThan(0.0) + .hasAttributesSatisfying( + equalTo(MESSAGING_SYSTEM, "pulsar"), + equalTo(MESSAGING_DESTINATION_NAME, topic), + equalTo(SERVER_PORT, brokerPort), + equalTo(SERVER_ADDRESS, brokerHost)) + .hasBucketBoundaries(DURATION_BUCKETS)))); + } + + @Test + void captureMessageHeaderAsSpanAttribute() throws Exception { + String topic = "persistent://public/default/testCaptureMessageHeaderTopic"; + CountDownLatch latch = new CountDownLatch(1); + admin.topics().createNonPartitionedTopic(topic); + consumer = + client + .newConsumer(Schema.STRING) + .subscriptionName("test_sub") + .topic(topic) + .subscriptionInitialPosition(SubscriptionInitialPosition.Earliest) + .messageListener( + (MessageListener) + (consumer, msg) -> { + acknowledgeMessage(consumer, msg); + latch.countDown(); + }) + .subscribe(); + + producer = client.newProducer(Schema.STRING).topic(topic).enableBatching(false).create(); + + String msg = "test"; + MessageId msgId = + testing.runWithSpan( + "parent", + () -> producer.newMessage().value(msg).property("test-message-header", "test").send()); + + latch.await(1, TimeUnit.MINUTES); + + AtomicReference producerSpan = new AtomicReference<>(); + testing.waitAndAssertSortedTraces( + orderByRootSpanKind(SpanKind.INTERNAL, SpanKind.CONSUMER), + trace -> { + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(), + span -> + span.hasName(topic + " publish") + .hasKind(SpanKind.PRODUCER) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + sendAttributes(topic, msgId.toString(), true))); + + producerSpan.set(trace.getSpan(1)); + }, + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName(topic + " receive") + .hasKind(SpanKind.CONSUMER) + .hasNoParent() + .hasLinks(LinkData.create(producerSpan.get().getSpanContext())) + .hasAttributesSatisfyingExactly( + receiveAttributes(topic, msgId.toString(), true)), + span -> + span.hasName(topic + " process") + .hasKind(SpanKind.CONSUMER) + .hasParent(trace.getSpan(0)) + .hasLinks(LinkData.create(producerSpan.get().getSpanContext())) + .hasAttributesSatisfyingExactly( + processAttributes(topic, msgId.toString(), true)))); + } + + @Test + void testConsumePartitionedTopic() throws Exception { + String topic = "persistent://public/default/testConsumePartitionedTopic"; + admin.topics().createPartitionedTopic(topic, 1); + CountDownLatch latch = new CountDownLatch(1); + + consumer = + client + .newConsumer(Schema.STRING) + .subscriptionName("test_sub") + .subscriptionInitialPosition(SubscriptionInitialPosition.Earliest) + .topic(topic) + .messageListener( + (MessageListener) + (consumer, msg) -> { + acknowledgeMessage(consumer, msg); + latch.countDown(); + }) + .subscribe(); + + producer = client.newProducer(Schema.STRING).topic(topic).enableBatching(false).create(); + + String msg = "test"; + MessageId msgId = testing.runWithSpan("parent", () -> producer.send(msg)); + + latch.await(1, TimeUnit.MINUTES); + + AtomicReference producerSpan = new AtomicReference<>(); + testing.waitAndAssertSortedTraces( + orderByRootSpanKind(SpanKind.INTERNAL, SpanKind.CONSUMER), + trace -> { + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(), + span -> + span.hasName(topic + "-partition-0 publish") + .hasKind(SpanKind.PRODUCER) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + sendAttributes(topic + "-partition-0", msgId.toString(), false))); + + producerSpan.set(trace.getSpan(1)); + }, + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName(topic + "-partition-0 receive") + .hasKind(SpanKind.CONSUMER) + .hasNoParent() + .hasLinks(LinkData.create(producerSpan.get().getSpanContext())) + .hasAttributesSatisfyingExactly( + receiveAttributes(topic + "-partition-0", msgId.toString(), false)), + span -> + span.hasName(topic + "-partition-0 process") + .hasKind(SpanKind.CONSUMER) + .hasParent(trace.getSpan(0)) + .hasLinks(LinkData.create(producerSpan.get().getSpanContext())) + .hasAttributesSatisfyingExactly( + processAttributes(topic + "-partition-0", msgId.toString(), false)))); + } + + @Test + void testConsumeMultiTopics() throws Exception { + String topicNamePrefix = "persistent://public/default/testConsumeMulti_"; + String topic1 = topicNamePrefix + "1"; + String topic2 = topicNamePrefix + "2"; + CountDownLatch latch = new CountDownLatch(2); + producer = client.newProducer(Schema.STRING).topic(topic1).enableBatching(false).create(); + producer2 = client.newProducer(Schema.STRING).topic(topic2).enableBatching(false).create(); + + MessageId msgId1 = testing.runWithSpan("parent1", () -> producer.send("test1")); + MessageId msgId2 = testing.runWithSpan("parent2", () -> producer2.send("test2")); + + consumer = + client + .newConsumer(Schema.STRING) + .topic(topic2, topic1) + .subscriptionName("test_sub") + .subscriptionInitialPosition(SubscriptionInitialPosition.Earliest) + .messageListener( + (MessageListener) + (consumer, msg) -> { + acknowledgeMessage(consumer, msg); + latch.countDown(); + }) + .subscribe(); + + latch.await(1, TimeUnit.MINUTES); + + AtomicReference producerSpan = new AtomicReference<>(); + AtomicReference producerSpan2 = new AtomicReference<>(); + testing.waitAndAssertSortedTraces( + orderByRootSpanName("parent1", topic1 + " receive", "parent2", topic2 + " receive"), + trace -> { + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent1").hasKind(SpanKind.INTERNAL).hasNoParent(), + span -> + span.hasName(topic1 + " publish") + .hasKind(SpanKind.PRODUCER) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + sendAttributes(topic1, msgId1.toString(), false))); + + producerSpan.set(trace.getSpan(1)); + }, + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName(topic1 + " receive") + .hasKind(SpanKind.CONSUMER) + .hasNoParent() + .hasLinks(LinkData.create(producerSpan.get().getSpanContext())) + .hasAttributesSatisfyingExactly( + receiveAttributes(topic1, msgId1.toString(), false)), + span -> + span.hasName(topic1 + " process") + .hasKind(SpanKind.CONSUMER) + .hasParent(trace.getSpan(0)) + .hasLinks(LinkData.create(producerSpan.get().getSpanContext())) + .hasAttributesSatisfyingExactly( + processAttributes(topic1, msgId1.toString(), false))), + trace -> { + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent2").hasKind(SpanKind.INTERNAL).hasNoParent(), + span -> + span.hasName(topic2 + " publish") + .hasKind(SpanKind.PRODUCER) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + sendAttributes(topic2, msgId2.toString(), false))); + + producerSpan2.set(trace.getSpan(1)); + }, + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName(topic2 + " receive") + .hasKind(SpanKind.CONSUMER) + .hasNoParent() + .hasLinks(LinkData.create(producerSpan2.get().getSpanContext())) + .hasAttributesSatisfyingExactly( + receiveAttributes(topic2, msgId2.toString(), false)), + span -> + span.hasName(topic2 + " process") + .hasKind(SpanKind.CONSUMER) + .hasParent(trace.getSpan(0)) + .hasLinks(LinkData.create(producerSpan2.get().getSpanContext())) + .hasAttributesSatisfyingExactly( + processAttributes(topic2, msgId2.toString(), false)))); + } + + @Test + void testConsumePartitionedTopicUsingBatchReceive() throws Exception { + String topic = "persistent://public/default/testConsumePartitionedTopicUsingBatchReceive"; + admin.topics().createPartitionedTopic(topic, 4); + consumer = + client + .newConsumer(Schema.STRING) + .subscriptionName("test_sub") + .topic(topic) + .subscriptionInitialPosition(SubscriptionInitialPosition.Earliest) + .subscribe(); + + producer = client.newProducer(Schema.STRING).topic(topic).enableBatching(false).create(); + + String msg = "test"; + for (int i = 0; i < 10; i++) { + producer.send(msg); + } + + Messages receivedMsg = consumer.batchReceive(); + consumer.acknowledge(receivedMsg); + + assertThat(testing.metrics()) + .satisfiesOnlyOnce( + metric -> + assertThat(metric) + .hasName("messaging.receive.messages") + .hasUnit("{message}") + .hasDescription("Measures the number of received messages.") + .hasLongSumSatisfying( + sum -> { + sum.satisfies( + pointData -> { + pointData + .getPoints() + .forEach( + p -> { + assertThat(p.getValue()).isPositive(); + if (p.getValue() == receivedMsg.size()) { + assertThat( + p.getAttributes() + .get(MESSAGING_DESTINATION_NAME)) + .isEqualTo(topic); + assertThat(p.getAttributes().get(MESSAGING_SYSTEM)) + .isEqualTo("pulsar"); + assertThat(p.getAttributes().get(SERVER_PORT)) + .isEqualTo(brokerPort); + assertThat(p.getAttributes().get(SERVER_ADDRESS)) + .isEqualTo(brokerHost); + } + }); + }); + })); + } +} diff --git a/instrumentation/quarkus-resteasy-reactive/common-testing/src/main/java/io/opentelemetry/instrumentation/quarkus/resteasy/reactive/AbstractQuarkusJaxRsTest.java b/instrumentation/quarkus-resteasy-reactive/common-testing/src/main/java/io/opentelemetry/instrumentation/quarkus/resteasy/reactive/AbstractQuarkusJaxRsTest.java index 058a2c3e1608..e41bc99d4923 100644 --- a/instrumentation/quarkus-resteasy-reactive/common-testing/src/main/java/io/opentelemetry/instrumentation/quarkus/resteasy/reactive/AbstractQuarkusJaxRsTest.java +++ b/instrumentation/quarkus-resteasy-reactive/common-testing/src/main/java/io/opentelemetry/instrumentation/quarkus/resteasy/reactive/AbstractQuarkusJaxRsTest.java @@ -17,6 +17,8 @@ import io.opentelemetry.testing.internal.armeria.common.AggregatedHttpResponse; import io.opentelemetry.testing.internal.armeria.common.HttpMethod; import java.time.Duration; +import java.util.Collections; +import java.util.Map; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; @@ -42,8 +44,15 @@ static void setUp() { } private static AggregatedHttpResponse request(String path) { + return request(path, Collections.emptyMap()); + } + + private static AggregatedHttpResponse request(String path, Map headers) { AggregatedHttpRequest request = AggregatedHttpRequest.of(HttpMethod.GET, "h1c://localhost:" + port + path); + if (!headers.isEmpty()) { + request = AggregatedHttpRequest.of(request.headers().toBuilder().add(headers).build()); + } return client.execute(request).aggregate().join(); } @@ -96,4 +105,16 @@ void testSubResourceLocator() { span.hasName("GET /test-sub-resource-locator/call/sub") .hasKind(SpanKind.SERVER))); } + + @Test + void testAbort() { + AggregatedHttpResponse response = request("/hello", Collections.singletonMap("abort", "true")); + assertThat(response.status().code()).isEqualTo(401); + assertThat(response.contentUtf8()).isEqualTo("Aborted"); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("GET /hello").hasKind(SpanKind.SERVER))); + } } diff --git a/instrumentation/quarkus-resteasy-reactive/javaagent/build.gradle.kts b/instrumentation/quarkus-resteasy-reactive/javaagent/build.gradle.kts index 656de8bce7d6..a2e7a6d5a8cc 100644 --- a/instrumentation/quarkus-resteasy-reactive/javaagent/build.gradle.kts +++ b/instrumentation/quarkus-resteasy-reactive/javaagent/build.gradle.kts @@ -6,7 +6,8 @@ muzzle { pass { group.set("io.quarkus") module.set("quarkus-resteasy-reactive") - versions.set("(,)") + // renamed to quarkus-rest in 3.9.0 + versions.set("(,3.9.0)") } } diff --git a/instrumentation/quarkus-resteasy-reactive/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/quarkus/resteasy/reactive/AbstractResteasyReactiveContextInstrumentation.java b/instrumentation/quarkus-resteasy-reactive/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/quarkus/resteasy/reactive/AbstractResteasyReactiveContextInstrumentation.java new file mode 100644 index 000000000000..14959ea11bd5 --- /dev/null +++ b/instrumentation/quarkus-resteasy-reactive/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/quarkus/resteasy/reactive/AbstractResteasyReactiveContextInstrumentation.java @@ -0,0 +1,51 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.quarkus.resteasy.reactive; + +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.jboss.resteasy.reactive.common.core.AbstractResteasyReactiveContext; +import org.jboss.resteasy.reactive.server.core.ResteasyReactiveRequestContext; + +public class AbstractResteasyReactiveContextInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher typeMatcher() { + return named("org.jboss.resteasy.reactive.common.core.AbstractResteasyReactiveContext"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + named("run"), + AbstractResteasyReactiveContextInstrumentation.class.getName() + "$RunAdvice"); + } + + @SuppressWarnings("unused") + public static class RunAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static OtelRequestContext onEnter( + @Advice.This AbstractResteasyReactiveContext requestContext) { + if (requestContext instanceof ResteasyReactiveRequestContext) { + ResteasyReactiveRequestContext context = (ResteasyReactiveRequestContext) requestContext; + return OtelRequestContext.start(context); + } + return null; + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void onExit(@Advice.Enter OtelRequestContext context) { + if (context != null) { + context.close(); + } + } + } +} diff --git a/instrumentation/quarkus-resteasy-reactive/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/quarkus/resteasy/reactive/InvocationHandlerInstrumentation.java b/instrumentation/quarkus-resteasy-reactive/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/quarkus/resteasy/reactive/InvocationHandlerInstrumentation.java index 204ac9eee860..19025aa666f7 100644 --- a/instrumentation/quarkus-resteasy-reactive/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/quarkus/resteasy/reactive/InvocationHandlerInstrumentation.java +++ b/instrumentation/quarkus-resteasy-reactive/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/quarkus/resteasy/reactive/InvocationHandlerInstrumentation.java @@ -7,7 +7,6 @@ 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 net.bytebuddy.asm.Advice; @@ -31,10 +30,8 @@ public void transform(TypeTransformer transformer) { public static class HandleAdvice { @Advice.OnMethodEnter(suppress = Throwable.class) - public static void onEnter( - @Advice.Argument(0) ResteasyReactiveRequestContext requestContext, - @Advice.Local("otelScope") Scope scope) { - ResteasyReactiveSpanName.INSTANCE.updateServerSpanName(requestContext); + public static void onEnter(@Advice.Argument(0) ResteasyReactiveRequestContext requestContext) { + OtelRequestContext.onInvoke(requestContext); } } } diff --git a/instrumentation/quarkus-resteasy-reactive/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/quarkus/resteasy/reactive/OtelRequestContext.java b/instrumentation/quarkus-resteasy-reactive/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/quarkus/resteasy/reactive/OtelRequestContext.java new file mode 100644 index 000000000000..7471da487174 --- /dev/null +++ b/instrumentation/quarkus-resteasy-reactive/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/quarkus/resteasy/reactive/OtelRequestContext.java @@ -0,0 +1,40 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.quarkus.resteasy.reactive; + +import org.jboss.resteasy.reactive.server.core.ResteasyReactiveRequestContext; + +public final class OtelRequestContext { + private static final ThreadLocal contextThreadLocal = new ThreadLocal<>(); + private boolean firstInvoke = true; + + public static OtelRequestContext start(ResteasyReactiveRequestContext requestContext) { + OtelRequestContext context = new OtelRequestContext(); + contextThreadLocal.set(context); + ResteasyReactiveSpanName.INSTANCE.updateServerSpanName(requestContext); + return context; + } + + public static void onInvoke(ResteasyReactiveRequestContext requestContext) { + OtelRequestContext context = contextThreadLocal.get(); + if (context == null) { + return; + } + // we ignore the first invoke as it uses the same context that we get in start, the second etc. + // invoke will be for sub resource locator that changes the path + if (context.firstInvoke) { + context.firstInvoke = false; + return; + } + ResteasyReactiveSpanName.INSTANCE.updateServerSpanName(requestContext); + } + + public void close() { + contextThreadLocal.remove(); + } + + private OtelRequestContext() {} +} diff --git a/instrumentation/quarkus-resteasy-reactive/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/quarkus/resteasy/reactive/QuarkusResteasyReactiveInstrumentationModule.java b/instrumentation/quarkus-resteasy-reactive/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/quarkus/resteasy/reactive/QuarkusResteasyReactiveInstrumentationModule.java index ecc32a279d10..1a1be882bde4 100644 --- a/instrumentation/quarkus-resteasy-reactive/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/quarkus/resteasy/reactive/QuarkusResteasyReactiveInstrumentationModule.java +++ b/instrumentation/quarkus-resteasy-reactive/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/quarkus/resteasy/reactive/QuarkusResteasyReactiveInstrumentationModule.java @@ -5,7 +5,7 @@ package io.opentelemetry.javaagent.instrumentation.quarkus.resteasy.reactive; -import static java.util.Collections.singletonList; +import static java.util.Arrays.asList; import com.google.auto.service.AutoService; import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; @@ -21,6 +21,8 @@ public QuarkusResteasyReactiveInstrumentationModule() { @Override public List typeInstrumentations() { - return singletonList(new InvocationHandlerInstrumentation()); + return asList( + new AbstractResteasyReactiveContextInstrumentation(), + new InvocationHandlerInstrumentation()); } } diff --git a/instrumentation/quarkus-resteasy-reactive/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/quarkus/resteasy/reactive/ResteasyReactiveSpanName.java b/instrumentation/quarkus-resteasy-reactive/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/quarkus/resteasy/reactive/ResteasyReactiveSpanName.java index d1e47490fe56..6ef87e5e4bad 100644 --- a/instrumentation/quarkus-resteasy-reactive/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/quarkus/resteasy/reactive/ResteasyReactiveSpanName.java +++ b/instrumentation/quarkus-resteasy-reactive/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/quarkus/resteasy/reactive/ResteasyReactiveSpanName.java @@ -8,29 +8,32 @@ import static io.opentelemetry.javaagent.instrumentation.jaxrs.JaxrsPathUtil.normalizePath; import io.opentelemetry.context.Context; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteHolder; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteSource; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRoute; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRouteSource; import io.opentelemetry.instrumentation.api.util.VirtualField; import org.jboss.resteasy.reactive.server.core.ResteasyReactiveRequestContext; import org.jboss.resteasy.reactive.server.mapping.RuntimeResource; import org.jboss.resteasy.reactive.server.mapping.URITemplate; -public final class ResteasyReactiveSpanName { +final class ResteasyReactiveSpanName { // remember previous path to handle sub path locators private static final VirtualField pathField = VirtualField.find(ResteasyReactiveRequestContext.class, String.class); public static final ResteasyReactiveSpanName INSTANCE = new ResteasyReactiveSpanName(); - public void updateServerSpanName(ResteasyReactiveRequestContext requestContext) { + void updateServerSpanName(ResteasyReactiveRequestContext requestContext) { Context context = Context.current(); String jaxRsName = calculateJaxRsName(requestContext); - HttpRouteHolder.updateHttpRoute(context, HttpRouteSource.NESTED_CONTROLLER, jaxRsName); + HttpServerRoute.update(context, HttpServerRouteSource.NESTED_CONTROLLER, jaxRsName); pathField.set(requestContext, jaxRsName); } private static String calculateJaxRsName(ResteasyReactiveRequestContext requestContext) { RuntimeResource target = requestContext.getTarget(); + if (target == null) { + return null; + } URITemplate classPath = target.getClassPath(); URITemplate path = target.getPath(); String name = normalize(classPath) + normalize(path); diff --git a/instrumentation/quarkus-resteasy-reactive/quarkus2-testing/build.gradle.kts b/instrumentation/quarkus-resteasy-reactive/quarkus2-testing/build.gradle.kts index cad9462ca63d..ff63fb854165 100644 --- a/instrumentation/quarkus-resteasy-reactive/quarkus2-testing/build.gradle.kts +++ b/instrumentation/quarkus-resteasy-reactive/quarkus2-testing/build.gradle.kts @@ -10,10 +10,13 @@ otelJava { dependencies { implementation(enforcedPlatform("io.quarkus.platform:quarkus-bom:2.16.7.Final")) + // fails with junit 5.11.+ + implementation(enforcedPlatform("org.junit:junit-bom:5.10.3")) implementation("io.quarkus:quarkus-resteasy-reactive") testInstrumentation(project(":instrumentation:netty:netty-4.1:javaagent")) testInstrumentation(project(":instrumentation:quarkus-resteasy-reactive:javaagent")) + testInstrumentation(project(":instrumentation:vertx:vertx-web-3.0:javaagent")) testImplementation(project(":instrumentation:quarkus-resteasy-reactive:common-testing")) testImplementation("io.quarkus:quarkus-junit5") diff --git a/instrumentation/quarkus-resteasy-reactive/quarkus2-testing/src/main/java/io/opentelemetry/instrumentation/quarkus/resteasy/reactive/v2_0/TestFilter.java b/instrumentation/quarkus-resteasy-reactive/quarkus2-testing/src/main/java/io/opentelemetry/instrumentation/quarkus/resteasy/reactive/v2_0/TestFilter.java new file mode 100644 index 000000000000..01696d6fb74a --- /dev/null +++ b/instrumentation/quarkus-resteasy-reactive/quarkus2-testing/src/main/java/io/opentelemetry/instrumentation/quarkus/resteasy/reactive/v2_0/TestFilter.java @@ -0,0 +1,27 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.quarkus.resteasy.reactive.v2_0; + +import javax.ws.rs.container.ContainerRequestContext; +import javax.ws.rs.container.ContainerRequestFilter; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.Provider; + +@Provider +public class TestFilter implements ContainerRequestFilter { + + @Override + public void filter(ContainerRequestContext containerRequestContext) { + if (containerRequestContext.getHeaderString("abort") != null) { + containerRequestContext.abortWith( + Response.status(Response.Status.UNAUTHORIZED) + .entity("Aborted") + .type(MediaType.TEXT_PLAIN_TYPE) + .build()); + } + } +} diff --git a/instrumentation/quarkus-resteasy-reactive/quarkus3-testing/build.gradle.kts b/instrumentation/quarkus-resteasy-reactive/quarkus3-testing/build.gradle.kts index 0da5e8ff54c7..afd93f30ed41 100644 --- a/instrumentation/quarkus-resteasy-reactive/quarkus3-testing/build.gradle.kts +++ b/instrumentation/quarkus-resteasy-reactive/quarkus3-testing/build.gradle.kts @@ -11,15 +11,18 @@ otelJava { // io.quarkus.platform:quarkus-bom is missing for 3.0.0.Final var quarkusVersion = "3.0.1.Final" if (findProperty("testLatestDeps") as Boolean) { - quarkusVersion = "+" + quarkusVersion = "3.5.+" } dependencies { implementation(enforcedPlatform("io.quarkus.platform:quarkus-bom:$quarkusVersion")) + // fails with junit 5.11.+ + implementation(enforcedPlatform("org.junit:junit-bom:5.10.3")) implementation("io.quarkus:quarkus-resteasy-reactive") testInstrumentation(project(":instrumentation:netty:netty-4.1:javaagent")) testInstrumentation(project(":instrumentation:quarkus-resteasy-reactive:javaagent")) + testInstrumentation(project(":instrumentation:vertx:vertx-web-3.0:javaagent")) testImplementation(project(":instrumentation:quarkus-resteasy-reactive:common-testing")) testImplementation("io.quarkus:quarkus-junit5") diff --git a/instrumentation/quarkus-resteasy-reactive/quarkus3-testing/src/main/java/io/opentelemetry/instrumentation/quarkus/resteasy/reactive/v3_0/TestFilter.java b/instrumentation/quarkus-resteasy-reactive/quarkus3-testing/src/main/java/io/opentelemetry/instrumentation/quarkus/resteasy/reactive/v3_0/TestFilter.java new file mode 100644 index 000000000000..2ab6db124779 --- /dev/null +++ b/instrumentation/quarkus-resteasy-reactive/quarkus3-testing/src/main/java/io/opentelemetry/instrumentation/quarkus/resteasy/reactive/v3_0/TestFilter.java @@ -0,0 +1,27 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.quarkus.resteasy.reactive.v3_0; + +import jakarta.ws.rs.container.ContainerRequestContext; +import jakarta.ws.rs.container.ContainerRequestFilter; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.ext.Provider; + +@Provider +public class TestFilter implements ContainerRequestFilter { + + @Override + public void filter(ContainerRequestContext containerRequestContext) { + if (containerRequestContext.getHeaderString("abort") != null) { + containerRequestContext.abortWith( + Response.status(Response.Status.UNAUTHORIZED) + .entity("Aborted") + .type(MediaType.TEXT_PLAIN_TYPE) + .build()); + } + } +} diff --git a/instrumentation/quartz-2.0/README.md b/instrumentation/quartz-2.0/README.md new file mode 100644 index 000000000000..c86beb9850a0 --- /dev/null +++ b/instrumentation/quartz-2.0/README.md @@ -0,0 +1,5 @@ +# Settings for the Quartz instrumentation + +| System property | Type | Default | Description | +| ---------------------------------------------------------- | ------- | ------- | --------------------------------------------------- | +| `otel.instrumentation.quartz.experimental-span-attributes` | Boolean | `false` | Enable the capture of experimental span attributes. | diff --git a/instrumentation/quartz-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/quartz/v2_0/QuartzSingletons.java b/instrumentation/quartz-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/quartz/v2_0/QuartzSingletons.java index f467c4d40acf..6d0b5c046575 100644 --- a/instrumentation/quartz-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/quartz/v2_0/QuartzSingletons.java +++ b/instrumentation/quartz-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/quartz/v2_0/QuartzSingletons.java @@ -7,14 +7,14 @@ import io.opentelemetry.api.GlobalOpenTelemetry; import io.opentelemetry.instrumentation.quartz.v2_0.QuartzTelemetry; -import io.opentelemetry.javaagent.bootstrap.internal.InstrumentationConfig; +import io.opentelemetry.javaagent.bootstrap.internal.AgentInstrumentationConfig; public final class QuartzSingletons { public static final QuartzTelemetry TELEMETRY = QuartzTelemetry.builder(GlobalOpenTelemetry.get()) .setCaptureExperimentalSpanAttributes( - InstrumentationConfig.get() + AgentInstrumentationConfig.get() .getBoolean("otel.instrumentation.quartz.experimental-span-attributes", false)) .build(); diff --git a/instrumentation/quartz-2.0/library/src/main/java/io/opentelemetry/instrumentation/quartz/v2_0/QuartzCodeAttributesGetter.java b/instrumentation/quartz-2.0/library/src/main/java/io/opentelemetry/instrumentation/quartz/v2_0/QuartzCodeAttributesGetter.java index 04792015b28d..5f715be2ef65 100644 --- a/instrumentation/quartz-2.0/library/src/main/java/io/opentelemetry/instrumentation/quartz/v2_0/QuartzCodeAttributesGetter.java +++ b/instrumentation/quartz-2.0/library/src/main/java/io/opentelemetry/instrumentation/quartz/v2_0/QuartzCodeAttributesGetter.java @@ -5,7 +5,7 @@ package io.opentelemetry.instrumentation.quartz.v2_0; -import io.opentelemetry.instrumentation.api.instrumenter.code.CodeAttributesGetter; +import io.opentelemetry.instrumentation.api.incubator.semconv.code.CodeAttributesGetter; import org.quartz.JobExecutionContext; final class QuartzCodeAttributesGetter implements CodeAttributesGetter { diff --git a/instrumentation/quartz-2.0/library/src/main/java/io/opentelemetry/instrumentation/quartz/v2_0/QuartzTelemetryBuilder.java b/instrumentation/quartz-2.0/library/src/main/java/io/opentelemetry/instrumentation/quartz/v2_0/QuartzTelemetryBuilder.java index 7ad1cfef9659..27269c4b45e1 100644 --- a/instrumentation/quartz-2.0/library/src/main/java/io/opentelemetry/instrumentation/quartz/v2_0/QuartzTelemetryBuilder.java +++ b/instrumentation/quartz-2.0/library/src/main/java/io/opentelemetry/instrumentation/quartz/v2_0/QuartzTelemetryBuilder.java @@ -8,25 +8,28 @@ import com.google.errorprone.annotations.CanIgnoreReturnValue; import io.opentelemetry.api.OpenTelemetry; import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.instrumentation.api.incubator.semconv.code.CodeAttributesExtractor; 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.code.CodeAttributesExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor; import java.util.ArrayList; import java.util.List; +import java.util.function.Function; import org.quartz.JobExecutionContext; /** A builder of {@link QuartzTelemetry}. */ public final class QuartzTelemetryBuilder { private static final String INSTRUMENTATION_NAME = "io.opentelemetry.quartz-2.0"; - private final OpenTelemetry openTelemetry; - private final List> additionalExtractors = new ArrayList<>(); - private boolean captureExperimentalSpanAttributes; + private Function< + SpanNameExtractor, + ? extends SpanNameExtractor> + spanNameExtractorTransformer = Function.identity(); QuartzTelemetryBuilder(OpenTelemetry openTelemetry) { this.openTelemetry = openTelemetry; @@ -55,12 +58,26 @@ public QuartzTelemetryBuilder setCaptureExperimentalSpanAttributes( return this; } + /** Sets custom {@link SpanNameExtractor} via transform function. */ + @CanIgnoreReturnValue + public QuartzTelemetryBuilder setSpanNameExtractor( + Function< + SpanNameExtractor, + ? extends SpanNameExtractor> + spanNameExtractorTransformer) { + this.spanNameExtractorTransformer = spanNameExtractorTransformer; + return this; + } + /** * Returns a new {@link QuartzTelemetry} with the settings of this {@link QuartzTelemetryBuilder}. */ public QuartzTelemetry build() { + SpanNameExtractor spanNameExtractor = + spanNameExtractorTransformer.apply(new QuartzSpanNameExtractor()); + InstrumenterBuilder instrumenter = - Instrumenter.builder(openTelemetry, INSTRUMENTATION_NAME, new QuartzSpanNameExtractor()); + Instrumenter.builder(openTelemetry, INSTRUMENTATION_NAME, spanNameExtractor); if (captureExperimentalSpanAttributes) { instrumenter.addAttributesExtractor( diff --git a/instrumentation/quartz-2.0/testing/src/main/java/io/opentelemetry/instrumentation/quartz/v2_0/AbstractQuartzTest.java b/instrumentation/quartz-2.0/testing/src/main/java/io/opentelemetry/instrumentation/quartz/v2_0/AbstractQuartzTest.java index 18a184449ad0..49a33c0f1e31 100644 --- a/instrumentation/quartz-2.0/testing/src/main/java/io/opentelemetry/instrumentation/quartz/v2_0/AbstractQuartzTest.java +++ b/instrumentation/quartz-2.0/testing/src/main/java/io/opentelemetry/instrumentation/quartz/v2_0/AbstractQuartzTest.java @@ -14,7 +14,7 @@ import io.opentelemetry.api.trace.SpanKind; import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; import io.opentelemetry.sdk.trace.data.StatusData; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.opentelemetry.semconv.incubating.CodeIncubatingAttributes; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.ObjectOutputStream; @@ -71,9 +71,9 @@ void successfulJob() throws Exception { .hasAttributesSatisfyingExactly( equalTo(AttributeKey.stringKey("job.system"), "quartz"), equalTo( - SemanticAttributes.CODE_NAMESPACE, + CodeIncubatingAttributes.CODE_NAMESPACE, SuccessfulJob.class.getName()), - equalTo(SemanticAttributes.CODE_FUNCTION, "execute")), + equalTo(CodeIncubatingAttributes.CODE_FUNCTION, "execute")), span -> span.hasName("child") .hasKind(SpanKind.INTERNAL) @@ -101,8 +101,9 @@ void failingJob() throws Exception { .hasAttributesSatisfyingExactly( equalTo(AttributeKey.stringKey("job.system"), "quartz"), equalTo( - SemanticAttributes.CODE_NAMESPACE, FailingJob.class.getName()), - equalTo(SemanticAttributes.CODE_FUNCTION, "execute")))); + CodeIncubatingAttributes.CODE_NAMESPACE, + FailingJob.class.getName()), + equalTo(CodeIncubatingAttributes.CODE_FUNCTION, "execute")))); } private static Scheduler createScheduler(String name) throws Exception { diff --git a/instrumentation/r2dbc-1.0/README.md b/instrumentation/r2dbc-1.0/README.md new file mode 100644 index 000000000000..bffe333ef136 --- /dev/null +++ b/instrumentation/r2dbc-1.0/README.md @@ -0,0 +1,5 @@ +# Settings for the JDBC instrumentation + +| System property | Type | Default | Description | +|----------------------------------------------------------|---------|---------|----------------------------------------| +| `otel.instrumentation.r2dbc.statement-sanitizer.enabled` | Boolean | `true` | Enables the DB statement sanitization. | diff --git a/instrumentation/r2dbc-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/r2dbc/v1_0/R2dbcSingletons.java b/instrumentation/r2dbc-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/r2dbc/v1_0/R2dbcSingletons.java index 5829464bc8de..36ebe81accbf 100644 --- a/instrumentation/r2dbc-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/r2dbc/v1_0/R2dbcSingletons.java +++ b/instrumentation/r2dbc-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/r2dbc/v1_0/R2dbcSingletons.java @@ -6,19 +6,25 @@ package io.opentelemetry.javaagent.instrumentation.r2dbc.v1_0; import io.opentelemetry.api.GlobalOpenTelemetry; -import io.opentelemetry.instrumentation.api.instrumenter.net.PeerServiceAttributesExtractor; -import io.opentelemetry.instrumentation.r2dbc.v1_0.R2dbcTelemetry; -import io.opentelemetry.instrumentation.r2dbc.v1_0.internal.R2dbcNetAttributesGetter; -import io.opentelemetry.javaagent.bootstrap.internal.CommonConfig; +import io.opentelemetry.instrumentation.api.incubator.semconv.net.PeerServiceAttributesExtractor; +import io.opentelemetry.instrumentation.r2dbc.v1_0.internal.shaded.R2dbcTelemetry; +import io.opentelemetry.instrumentation.r2dbc.v1_0.internal.shaded.internal.R2dbcNetAttributesGetter; +import io.opentelemetry.javaagent.bootstrap.internal.AgentCommonConfig; +import io.opentelemetry.javaagent.bootstrap.internal.AgentInstrumentationConfig; public final class R2dbcSingletons { private static final R2dbcTelemetry TELEMETRY = R2dbcTelemetry.builder(GlobalOpenTelemetry.get()) - .setStatementSanitizationEnabled(CommonConfig.get().isStatementSanitizationEnabled()) + .setStatementSanitizationEnabled( + AgentInstrumentationConfig.get() + .getBoolean( + "otel.instrumentation.r2dbc.statement-sanitizer.enabled", + AgentCommonConfig.get().isStatementSanitizationEnabled())) .addAttributeExtractor( PeerServiceAttributesExtractor.create( - R2dbcNetAttributesGetter.INSTANCE, CommonConfig.get().getPeerServiceMapping())) + R2dbcNetAttributesGetter.INSTANCE, + AgentCommonConfig.get().getPeerServiceResolver())) .build(); public static R2dbcTelemetry telemetry() { diff --git a/instrumentation/r2dbc-1.0/library-instrumentation-shaded/build.gradle.kts b/instrumentation/r2dbc-1.0/library-instrumentation-shaded/build.gradle.kts index d943196ba003..c750072d17ab 100644 --- a/instrumentation/r2dbc-1.0/library-instrumentation-shaded/build.gradle.kts +++ b/instrumentation/r2dbc-1.0/library-instrumentation-shaded/build.gradle.kts @@ -1,6 +1,5 @@ plugins { - id("com.github.johnrengelman.shadow") - + id("com.gradleup.shadow") id("otel.java-conventions") } @@ -12,7 +11,9 @@ dependencies { tasks { shadowJar { - exclude("META-INF/**/*") + exclude { + it.path.startsWith("META-INF") && !it.path.startsWith("META-INF/io/opentelemetry/instrumentation/") + } dependencies { // including only :r2dbc-1.0:library excludes its transitive dependencies @@ -21,13 +22,24 @@ tasks { } relocate( "io.r2dbc.proxy", - "io.opentelemetry.javaagent.instrumentation.r2dbc.v1_0.shaded.io.r2dbc.proxy" + "io.opentelemetry.instrumentation.r2dbc.v1_0.internal.shaded.io.r2dbc.proxy" + ) + relocate( + "io.opentelemetry.instrumentation.r2dbc.v1_0", + "io.opentelemetry.instrumentation.r2dbc.v1_0.internal.shaded" ) } val extractShadowJar by registering(Copy::class) { dependsOn(shadowJar) from(zipTree(shadowJar.get().archiveFile)) + exclude("META-INF/**") into("build/extracted/shadow") } + + val extractShadowJarSpring by registering(Copy::class) { + dependsOn(shadowJar) + from(zipTree(shadowJar.get().archiveFile)) + into("build/extracted/shadow-spring") + } } diff --git a/instrumentation/r2dbc-1.0/library/build.gradle.kts b/instrumentation/r2dbc-1.0/library/build.gradle.kts index bad31be4f606..511b6354abd3 100644 --- a/instrumentation/r2dbc-1.0/library/build.gradle.kts +++ b/instrumentation/r2dbc-1.0/library/build.gradle.kts @@ -4,7 +4,7 @@ plugins { dependencies { library("io.r2dbc:r2dbc-spi:1.0.0.RELEASE") - implementation("io.r2dbc:r2dbc-proxy:1.0.1.RELEASE") + implementation("io.r2dbc:r2dbc-proxy") testImplementation(project(":instrumentation:r2dbc-1.0:testing")) testImplementation(project(":instrumentation:reactor:reactor-3.1:library")) diff --git a/instrumentation/r2dbc-1.0/library/src/main/java/io/opentelemetry/instrumentation/r2dbc/v1_0/R2dbcTelemetryBuilder.java b/instrumentation/r2dbc-1.0/library/src/main/java/io/opentelemetry/instrumentation/r2dbc/v1_0/R2dbcTelemetryBuilder.java index 16627220bb42..3cd1f16e0eaf 100644 --- a/instrumentation/r2dbc-1.0/library/src/main/java/io/opentelemetry/instrumentation/r2dbc/v1_0/R2dbcTelemetryBuilder.java +++ b/instrumentation/r2dbc-1.0/library/src/main/java/io/opentelemetry/instrumentation/r2dbc/v1_0/R2dbcTelemetryBuilder.java @@ -8,14 +8,18 @@ import com.google.errorprone.annotations.CanIgnoreReturnValue; import io.opentelemetry.api.OpenTelemetry; import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor; import io.opentelemetry.instrumentation.r2dbc.v1_0.internal.DbExecution; import io.opentelemetry.instrumentation.r2dbc.v1_0.internal.R2dbcInstrumenterBuilder; +import java.util.function.Function; /** A builder of {@link R2dbcTelemetry}. */ public final class R2dbcTelemetryBuilder { private final R2dbcInstrumenterBuilder instrumenterBuilder; private boolean statementSanitizationEnabled = true; + private Function, ? extends SpanNameExtractor> + spanNameExtractorTransformer = Function.identity(); R2dbcTelemetryBuilder(OpenTelemetry openTelemetry) { instrumenterBuilder = new R2dbcInstrumenterBuilder(openTelemetry); @@ -39,10 +43,20 @@ public R2dbcTelemetryBuilder setStatementSanitizationEnabled(boolean enabled) { return this; } + /** Sets custom {@link SpanNameExtractor} via transform function. */ + @CanIgnoreReturnValue + public R2dbcTelemetryBuilder setSpanNameExtractor( + Function, ? extends SpanNameExtractor> + spanNameExtractorTransformer) { + this.spanNameExtractorTransformer = spanNameExtractorTransformer; + return this; + } + /** * Returns a new {@link R2dbcTelemetry} with the settings of this {@link R2dbcTelemetryBuilder}. */ public R2dbcTelemetry build() { - return new R2dbcTelemetry(instrumenterBuilder.build(statementSanitizationEnabled)); + return new R2dbcTelemetry( + instrumenterBuilder.build(spanNameExtractorTransformer, statementSanitizationEnabled)); } } diff --git a/instrumentation/r2dbc-1.0/library/src/main/java/io/opentelemetry/instrumentation/r2dbc/v1_0/internal/DbExecution.java b/instrumentation/r2dbc-1.0/library/src/main/java/io/opentelemetry/instrumentation/r2dbc/v1_0/internal/DbExecution.java index bf2a11f666b7..bcdca4014339 100644 --- a/instrumentation/r2dbc-1.0/library/src/main/java/io/opentelemetry/instrumentation/r2dbc/v1_0/internal/DbExecution.java +++ b/instrumentation/r2dbc-1.0/library/src/main/java/io/opentelemetry/instrumentation/r2dbc/v1_0/internal/DbExecution.java @@ -13,7 +13,6 @@ import static io.r2dbc.spi.ConnectionFactoryOptions.USER; import io.opentelemetry.context.Context; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; import io.r2dbc.proxy.core.QueryExecutionInfo; import io.r2dbc.proxy.core.QueryInfo; import io.r2dbc.spi.Connection; @@ -26,6 +25,9 @@ * any time. */ public final class DbExecution { + // copied from DbIncubatingAttributes.DbSystemValues + private static final String OTHER_SQL = "other_sql"; + private final String system; private final String user; private final String name; @@ -45,7 +47,7 @@ public DbExecution(QueryExecutionInfo queryInfo, ConnectionFactoryOptions factor .getDatabaseProductName() .toLowerCase(Locale.ROOT) .split(" ")[0] - : SemanticAttributes.DbSystemValues.OTHER_SQL; + : OTHER_SQL; this.user = factoryOptions.hasOption(USER) ? (String) factoryOptions.getValue(USER) : null; this.name = factoryOptions.hasOption(DATABASE) diff --git a/instrumentation/r2dbc-1.0/library/src/main/java/io/opentelemetry/instrumentation/r2dbc/v1_0/internal/R2dbcInstrumenterBuilder.java b/instrumentation/r2dbc-1.0/library/src/main/java/io/opentelemetry/instrumentation/r2dbc/v1_0/internal/R2dbcInstrumenterBuilder.java index 77de2bca2d46..0b15861ff915 100644 --- a/instrumentation/r2dbc-1.0/library/src/main/java/io/opentelemetry/instrumentation/r2dbc/v1_0/internal/R2dbcInstrumenterBuilder.java +++ b/instrumentation/r2dbc-1.0/library/src/main/java/io/opentelemetry/instrumentation/r2dbc/v1_0/internal/R2dbcInstrumenterBuilder.java @@ -7,14 +7,16 @@ import com.google.errorprone.annotations.CanIgnoreReturnValue; import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.instrumentation.api.incubator.semconv.db.DbClientSpanNameExtractor; +import io.opentelemetry.instrumentation.api.incubator.semconv.db.SqlClientAttributesExtractor; 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.db.DbClientSpanNameExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.db.SqlClientAttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.net.NetClientAttributesExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor; +import io.opentelemetry.instrumentation.api.semconv.network.ServerAttributesExtractor; import java.util.ArrayList; import java.util.List; +import java.util.function.Function; /** * This class is internal and is hence not for public use. Its APIs are unstable and can change at @@ -40,18 +42,22 @@ public R2dbcInstrumenterBuilder addAttributeExtractor( return this; } - public Instrumenter build(boolean statementSanitizationEnabled) { + public Instrumenter build( + Function, ? extends SpanNameExtractor> + spanNameExtractorTransformer, + boolean statementSanitizationEnabled) { + SpanNameExtractor spanNameExtractor = + spanNameExtractorTransformer.apply( + DbClientSpanNameExtractor.create(R2dbcSqlAttributesGetter.INSTANCE)); return Instrumenter.builder( - openTelemetry, - INSTRUMENTATION_NAME, - DbClientSpanNameExtractor.create(R2dbcSqlAttributesGetter.INSTANCE)) + openTelemetry, INSTRUMENTATION_NAME, spanNameExtractor) .addAttributesExtractor( SqlClientAttributesExtractor.builder(R2dbcSqlAttributesGetter.INSTANCE) .setStatementSanitizationEnabled(statementSanitizationEnabled) .build()) - .addAttributesExtractor( - NetClientAttributesExtractor.create(R2dbcNetAttributesGetter.INSTANCE)) + .addAttributesExtractor(ServerAttributesExtractor.create(R2dbcNetAttributesGetter.INSTANCE)) + .addAttributesExtractors(additionalExtractors) .buildInstrumenter(SpanKindExtractor.alwaysClient()); } } diff --git a/instrumentation/r2dbc-1.0/library/src/main/java/io/opentelemetry/instrumentation/r2dbc/v1_0/internal/R2dbcNetAttributesGetter.java b/instrumentation/r2dbc-1.0/library/src/main/java/io/opentelemetry/instrumentation/r2dbc/v1_0/internal/R2dbcNetAttributesGetter.java index 09425ca69792..a50799c26984 100644 --- a/instrumentation/r2dbc-1.0/library/src/main/java/io/opentelemetry/instrumentation/r2dbc/v1_0/internal/R2dbcNetAttributesGetter.java +++ b/instrumentation/r2dbc-1.0/library/src/main/java/io/opentelemetry/instrumentation/r2dbc/v1_0/internal/R2dbcNetAttributesGetter.java @@ -5,14 +5,14 @@ package io.opentelemetry.instrumentation.r2dbc.v1_0.internal; -import io.opentelemetry.instrumentation.api.instrumenter.net.NetClientAttributesGetter; +import io.opentelemetry.instrumentation.api.semconv.network.ServerAttributesGetter; import javax.annotation.Nullable; /** * This class is internal and is hence not for public use. Its APIs are unstable and can change at * any time. */ -public enum R2dbcNetAttributesGetter implements NetClientAttributesGetter { +public enum R2dbcNetAttributesGetter implements ServerAttributesGetter { INSTANCE; @Nullable diff --git a/instrumentation/r2dbc-1.0/library/src/main/java/io/opentelemetry/instrumentation/r2dbc/v1_0/internal/R2dbcSqlAttributesGetter.java b/instrumentation/r2dbc-1.0/library/src/main/java/io/opentelemetry/instrumentation/r2dbc/v1_0/internal/R2dbcSqlAttributesGetter.java index e5b46319ac81..6b7bcbb41ff6 100644 --- a/instrumentation/r2dbc-1.0/library/src/main/java/io/opentelemetry/instrumentation/r2dbc/v1_0/internal/R2dbcSqlAttributesGetter.java +++ b/instrumentation/r2dbc-1.0/library/src/main/java/io/opentelemetry/instrumentation/r2dbc/v1_0/internal/R2dbcSqlAttributesGetter.java @@ -5,7 +5,7 @@ package io.opentelemetry.instrumentation.r2dbc.v1_0.internal; -import io.opentelemetry.instrumentation.api.instrumenter.db.SqlClientAttributesGetter; +import io.opentelemetry.instrumentation.api.incubator.semconv.db.SqlClientAttributesGetter; import javax.annotation.Nullable; /** diff --git a/instrumentation/r2dbc-1.0/testing/build.gradle.kts b/instrumentation/r2dbc-1.0/testing/build.gradle.kts index 17940f130480..2484407b0461 100644 --- a/instrumentation/r2dbc-1.0/testing/build.gradle.kts +++ b/instrumentation/r2dbc-1.0/testing/build.gradle.kts @@ -7,7 +7,7 @@ dependencies { implementation("io.r2dbc:r2dbc-spi:1.0.0.RELEASE") - implementation(project(":instrumentation-api-semconv")) + implementation(project(":instrumentation-api-incubator")) implementation("org.testcontainers:junit-jupiter") compileOnly("io.projectreactor:reactor-core:3.4.12") diff --git a/instrumentation/r2dbc-1.0/testing/src/main/java/io/opentelemetry/instrumentation/r2dbc/v1_0/AbstractR2dbcStatementTest.java b/instrumentation/r2dbc-1.0/testing/src/main/java/io/opentelemetry/instrumentation/r2dbc/v1_0/AbstractR2dbcStatementTest.java index 5bc3814f5070..465c0e40bef6 100644 --- a/instrumentation/r2dbc-1.0/testing/src/main/java/io/opentelemetry/instrumentation/r2dbc/v1_0/AbstractR2dbcStatementTest.java +++ b/instrumentation/r2dbc-1.0/testing/src/main/java/io/opentelemetry/instrumentation/r2dbc/v1_0/AbstractR2dbcStatementTest.java @@ -6,15 +6,15 @@ package io.opentelemetry.instrumentation.r2dbc.v1_0; import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.DB_CONNECTION_STRING; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.DB_NAME; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.DB_OPERATION; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.DB_SQL_TABLE; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.DB_STATEMENT; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.DB_SYSTEM; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.DB_USER; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.NET_PEER_NAME; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.NET_PEER_PORT; +import static io.opentelemetry.semconv.ServerAttributes.SERVER_ADDRESS; +import static io.opentelemetry.semconv.ServerAttributes.SERVER_PORT; +import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DB_CONNECTION_STRING; +import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DB_NAME; +import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DB_OPERATION; +import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DB_SQL_TABLE; +import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DB_STATEMENT; +import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DB_SYSTEM; +import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DB_USER; import static io.r2dbc.spi.ConnectionFactoryOptions.DATABASE; import static io.r2dbc.spi.ConnectionFactoryOptions.DRIVER; import static io.r2dbc.spi.ConnectionFactoryOptions.HOST; @@ -124,6 +124,7 @@ void startContainer(DbSystemProps props) { } } + @SuppressWarnings("deprecation") // TODO DbIncubatingAttributes.DB_CONNECTION_STRING deprecation @ParameterizedTest(name = "{index}: {0}") @MethodSource("provideParameters") void testQueries(Parameter parameter) { @@ -133,7 +134,7 @@ void testQueries(Parameter parameter) { createProxyConnectionFactory( ConnectionFactoryOptions.builder() .option(DRIVER, props.system) - .option(HOST, "localhost") + .option(HOST, container.getHost()) .option(PORT, port) .option(USER, USER_DB) .option(PASSWORD, PW_DB) @@ -174,8 +175,8 @@ void testQueries(Parameter parameter) { equalTo(DB_STATEMENT, parameter.expectedStatement), equalTo(DB_OPERATION, parameter.operation), equalTo(DB_SQL_TABLE, parameter.table), - equalTo(NET_PEER_NAME, "localhost"), - equalTo(NET_PEER_PORT, port)), + equalTo(SERVER_ADDRESS, container.getHost()), + equalTo(SERVER_PORT, port)), span -> span.hasName("child") .hasKind(SpanKind.INTERNAL) @@ -204,9 +205,9 @@ private static Stream provideParameters() { system.system, "CREATE TABLE person (id SERIAL PRIMARY KEY, first_name VARCHAR(255), last_name VARCHAR(255))", "CREATE TABLE person (id SERIAL PRIMARY KEY, first_name VARCHAR(?), last_name VARCHAR(?))", - DB, - null, - null))), + "CREATE TABLE " + DB + ".person", + "person", + "CREATE TABLE"))), Arguments.of( named( system.system + " Insert", diff --git a/instrumentation/rabbitmq-2.7/README.md b/instrumentation/rabbitmq-2.7/README.md index cd3f82a5b122..7021d3cf9a9f 100644 --- a/instrumentation/rabbitmq-2.7/README.md +++ b/instrumentation/rabbitmq-2.7/README.md @@ -1,5 +1,5 @@ # Settings for the RabbitMQ instrumentation -| System property | Type | Default | Description | -|---|---|---|---| +| System property | Type | Default | Description | +| ------------------------------------------------------------ | ------- | ------- | --------------------------------------------------- | | `otel.instrumentation.rabbitmq.experimental-span-attributes` | Boolean | `false` | Enable the capture of experimental span attributes. | diff --git a/instrumentation/rabbitmq-2.7/javaagent/build.gradle.kts b/instrumentation/rabbitmq-2.7/javaagent/build.gradle.kts index 92ac9c194e1d..bef0f1bc89bd 100644 --- a/instrumentation/rabbitmq-2.7/javaagent/build.gradle.kts +++ b/instrumentation/rabbitmq-2.7/javaagent/build.gradle.kts @@ -24,9 +24,6 @@ dependencies { testInstrumentation(project(":instrumentation:reactor:reactor-3.1:javaagent")) testLibrary("io.projectreactor.rabbitmq:reactor-rabbitmq:1.0.0.RELEASE") - // since reactor-rabbitmq:1.5.4 there is only a runtime dependency to reactor-core but spock - // needs it at compile time - testCompileOnly("io.projectreactor:reactor-core:3.4.12") } tasks.withType().configureEach { diff --git a/instrumentation/rabbitmq-2.7/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rabbitmq/DeliveryRequest.java b/instrumentation/rabbitmq-2.7/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rabbitmq/DeliveryRequest.java index d8e8a7351787..1507f537bf5c 100644 --- a/instrumentation/rabbitmq-2.7/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rabbitmq/DeliveryRequest.java +++ b/instrumentation/rabbitmq-2.7/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rabbitmq/DeliveryRequest.java @@ -7,20 +7,27 @@ import com.google.auto.value.AutoValue; import com.rabbitmq.client.AMQP; +import com.rabbitmq.client.Connection; import com.rabbitmq.client.Envelope; @AutoValue abstract class DeliveryRequest { static DeliveryRequest create( - String queue, Envelope envelope, AMQP.BasicProperties properties, byte[] body) { - return new AutoValue_DeliveryRequest(queue, envelope, properties, body); + String queue, + Envelope envelope, + Connection connection, + AMQP.BasicProperties properties, + byte[] body) { + return new AutoValue_DeliveryRequest(queue, envelope, connection, properties, body); } abstract String getQueue(); abstract Envelope getEnvelope(); + abstract Connection getConnection(); + abstract AMQP.BasicProperties getProperties(); @SuppressWarnings("mutable") @@ -30,7 +37,8 @@ String spanName() { String queue = getQueue(); if (queue == null || queue.isEmpty()) { return " process"; - } else if (queue.startsWith("amq.gen-")) { + } else if (queue.startsWith("amq.gen-") || queue.startsWith("spring.gen-")) { + // The spring.gen- name comes from AnonymousQueue in the Spring AMQP library return " process"; } else { return queue + " process"; diff --git a/instrumentation/rabbitmq-2.7/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rabbitmq/RabbitChannelAttributesGetter.java b/instrumentation/rabbitmq-2.7/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rabbitmq/RabbitChannelAttributesGetter.java index 661838d71bec..7bbe5e5828ba 100644 --- a/instrumentation/rabbitmq-2.7/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rabbitmq/RabbitChannelAttributesGetter.java +++ b/instrumentation/rabbitmq-2.7/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rabbitmq/RabbitChannelAttributesGetter.java @@ -5,7 +5,7 @@ package io.opentelemetry.javaagent.instrumentation.rabbitmq; -import io.opentelemetry.instrumentation.api.instrumenter.messaging.MessagingAttributesGetter; +import io.opentelemetry.instrumentation.api.incubator.semconv.messaging.MessagingAttributesGetter; import java.util.Collections; import java.util.List; import javax.annotation.Nullable; @@ -24,11 +24,22 @@ public String getDestination(ChannelAndMethod channelAndMethod) { return null; } + @Nullable + @Override + public String getDestinationTemplate(ChannelAndMethod channelAndMethod) { + return null; + } + @Override public boolean isTemporaryDestination(ChannelAndMethod channelAndMethod) { return false; } + @Override + public boolean isAnonymousDestination(ChannelAndMethod channelAndMethod) { + return false; + } + @Nullable @Override public String getConversationId(ChannelAndMethod channelAndMethod) { @@ -37,13 +48,13 @@ public String getConversationId(ChannelAndMethod channelAndMethod) { @Nullable @Override - public Long getMessagePayloadSize(ChannelAndMethod channelAndMethod) { + public Long getMessageBodySize(ChannelAndMethod channelAndMethod) { return null; } @Nullable @Override - public Long getMessagePayloadCompressedSize(ChannelAndMethod channelAndMethod) { + public Long getMessageEnvelopeSize(ChannelAndMethod channelAndMethod) { return null; } @@ -53,6 +64,18 @@ public String getMessageId(ChannelAndMethod channelAndMethod, @Nullable Void unu return null; } + @Nullable + @Override + public String getClientId(ChannelAndMethod channelAndMethod) { + return null; + } + + @Nullable + @Override + public Long getBatchMessageCount(ChannelAndMethod channelAndMethod, @Nullable Void unused) { + return null; + } + @Override public List getMessageHeader(ChannelAndMethod channelAndMethod, String name) { if (channelAndMethod.getHeaders() != null) { diff --git a/instrumentation/rabbitmq-2.7/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rabbitmq/RabbitChannelInstrumentation.java b/instrumentation/rabbitmq-2.7/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rabbitmq/RabbitChannelInstrumentation.java index 1e76928fb41f..99b4861ac6ae 100644 --- a/instrumentation/rabbitmq-2.7/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rabbitmq/RabbitChannelInstrumentation.java +++ b/instrumentation/rabbitmq-2.7/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rabbitmq/RabbitChannelInstrumentation.java @@ -37,7 +37,7 @@ import io.opentelemetry.javaagent.bootstrap.Java8BytecodeBridge; import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes; import java.io.IOException; import java.util.HashMap; import java.util.Map; @@ -108,11 +108,11 @@ public static void onEnter( Context parentContext = Java8BytecodeBridge.currentContext(); request = ChannelAndMethod.create(channel, method); - if (!channelInstrumenter().shouldStart(parentContext, request)) { + if (!channelInstrumenter(request).shouldStart(parentContext, request)) { return; } - context = channelInstrumenter().start(parentContext, request); + context = channelInstrumenter(request).start(parentContext, request); CURRENT_RABBIT_CONTEXT.set(context); helper().setChannelAndMethod(context, request); scope = context.makeCurrent(); @@ -128,11 +128,14 @@ public static void stopSpan( if (callDepth.decrementAndGet() > 0) { return; } + if (scope == null) { + return; + } scope.close(); CURRENT_RABBIT_CONTEXT.remove(); - channelInstrumenter().end(context, request, null, throwable); + channelInstrumenter(request).end(context, request, null, throwable); } } @@ -152,7 +155,7 @@ public static void setSpanNameAddHeaders( helper().onPublish(span, exchange, routingKey); if (body != null) { span.setAttribute( - SemanticAttributes.MESSAGING_MESSAGE_PAYLOAD_SIZE_BYTES, (long) body.length); + MessagingIncubatingAttributes.MESSAGING_MESSAGE_BODY_SIZE, (long) body.length); } // This is the internal behavior when props are null. We're just doing it earlier now. @@ -235,11 +238,12 @@ public static class ChannelConsumeAdvice { @Advice.OnMethodEnter(suppress = Throwable.class) public static void wrapConsumer( + @Advice.This Channel channel, @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); + consumer = new TracedDelegatingConsumer(queue, consumer, channel.getConnection()); } } } diff --git a/instrumentation/rabbitmq-2.7/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rabbitmq/RabbitChannelNetAttributesGetter.java b/instrumentation/rabbitmq-2.7/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rabbitmq/RabbitChannelNetAttributesGetter.java index 6c099279fb22..2388fff37510 100644 --- a/instrumentation/rabbitmq-2.7/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rabbitmq/RabbitChannelNetAttributesGetter.java +++ b/instrumentation/rabbitmq-2.7/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rabbitmq/RabbitChannelNetAttributesGetter.java @@ -5,42 +5,35 @@ package io.opentelemetry.javaagent.instrumentation.rabbitmq; -import io.opentelemetry.instrumentation.api.instrumenter.net.NetClientAttributesGetter; +import io.opentelemetry.instrumentation.api.semconv.network.NetworkAttributesGetter; +import java.net.Inet4Address; import java.net.Inet6Address; +import java.net.InetAddress; import javax.annotation.Nullable; public class RabbitChannelNetAttributesGetter - implements NetClientAttributesGetter { + implements NetworkAttributesGetter { @Nullable @Override - public String getServerAddress(ChannelAndMethod channelAndMethod) { - return null; - } - - @Nullable - @Override - public Integer getServerPort(ChannelAndMethod channelAndMethod) { + public String getNetworkType(ChannelAndMethod channelAndMethod, @Nullable Void unused) { + InetAddress address = channelAndMethod.getChannel().getConnection().getAddress(); + if (address instanceof Inet4Address) { + return "ipv4"; + } else if (address instanceof Inet6Address) { + return "ipv6"; + } return null; } @Nullable @Override - public String getServerSocketAddress(ChannelAndMethod channelAndMethod, @Nullable Void unused) { + public String getNetworkPeerAddress(ChannelAndMethod channelAndMethod, @Nullable Void unused) { return channelAndMethod.getChannel().getConnection().getAddress().getHostAddress(); } @Override - public Integer getServerSocketPort(ChannelAndMethod channelAndMethod, @Nullable Void unused) { + public Integer getNetworkPeerPort(ChannelAndMethod channelAndMethod, @Nullable Void unused) { return channelAndMethod.getChannel().getConnection().getPort(); } - - @Nullable - @Override - public String getSockFamily(ChannelAndMethod channelAndMethod, @Nullable Void unused) { - if (channelAndMethod.getChannel().getConnection().getAddress() instanceof Inet6Address) { - return "inet6"; - } - return null; - } } diff --git a/instrumentation/rabbitmq-2.7/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rabbitmq/RabbitDeliveryAttributesGetter.java b/instrumentation/rabbitmq-2.7/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rabbitmq/RabbitDeliveryAttributesGetter.java index 41486c6691b6..d5bf72668e6b 100644 --- a/instrumentation/rabbitmq-2.7/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rabbitmq/RabbitDeliveryAttributesGetter.java +++ b/instrumentation/rabbitmq-2.7/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rabbitmq/RabbitDeliveryAttributesGetter.java @@ -8,7 +8,7 @@ import static java.util.Collections.emptyList; import static java.util.Collections.singletonList; -import io.opentelemetry.instrumentation.api.instrumenter.messaging.MessagingAttributesGetter; +import io.opentelemetry.instrumentation.api.incubator.semconv.messaging.MessagingAttributesGetter; import java.util.List; import java.util.Map; import javax.annotation.Nullable; @@ -31,6 +31,12 @@ public String getDestination(DeliveryRequest request) { } } + @Nullable + @Override + public String getDestinationTemplate(DeliveryRequest request) { + return null; + } + private static String normalizeExchangeName(String exchange) { return exchange == null || exchange.isEmpty() ? "" : exchange; } @@ -40,6 +46,11 @@ public boolean isTemporaryDestination(DeliveryRequest request) { return false; } + @Override + public boolean isAnonymousDestination(DeliveryRequest request) { + return false; + } + @Nullable @Override public String getConversationId(DeliveryRequest request) { @@ -48,7 +59,7 @@ public String getConversationId(DeliveryRequest request) { @Nullable @Override - public Long getMessagePayloadSize(DeliveryRequest request) { + public Long getMessageBodySize(DeliveryRequest request) { if (request.getBody() != null) { return (long) request.getBody().length; } @@ -57,7 +68,7 @@ public Long getMessagePayloadSize(DeliveryRequest request) { @Nullable @Override - public Long getMessagePayloadCompressedSize(DeliveryRequest request) { + public Long getMessageEnvelopeSize(DeliveryRequest request) { return null; } @@ -67,6 +78,18 @@ public String getMessageId(DeliveryRequest request, @Nullable Void unused) { return null; } + @Nullable + @Override + public String getClientId(DeliveryRequest request) { + return null; + } + + @Nullable + @Override + public Long getBatchMessageCount(DeliveryRequest request, @Nullable Void unused) { + return null; + } + @Override public List getMessageHeader(DeliveryRequest request, String name) { Map headers = request.getProperties().getHeaders(); diff --git a/instrumentation/rabbitmq-2.7/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rabbitmq/RabbitDeliveryExtraAttributesExtractor.java b/instrumentation/rabbitmq-2.7/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rabbitmq/RabbitDeliveryExtraAttributesExtractor.java index c586a334070b..ffe9e510542b 100644 --- a/instrumentation/rabbitmq-2.7/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rabbitmq/RabbitDeliveryExtraAttributesExtractor.java +++ b/instrumentation/rabbitmq-2.7/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rabbitmq/RabbitDeliveryExtraAttributesExtractor.java @@ -9,7 +9,7 @@ import io.opentelemetry.api.common.AttributesBuilder; import io.opentelemetry.context.Context; import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes; import javax.annotation.Nullable; class RabbitDeliveryExtraAttributesExtractor implements AttributesExtractor { @@ -20,7 +20,8 @@ public void onStart( Envelope envelope = request.getEnvelope(); String routingKey = envelope.getRoutingKey(); if (routingKey != null && !routingKey.isEmpty()) { - attributes.put(SemanticAttributes.MESSAGING_RABBITMQ_DESTINATION_ROUTING_KEY, routingKey); + attributes.put( + MessagingIncubatingAttributes.MESSAGING_RABBITMQ_DESTINATION_ROUTING_KEY, routingKey); } } diff --git a/instrumentation/rabbitmq-2.7/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rabbitmq/RabbitDeliveryNetAttributesGetter.java b/instrumentation/rabbitmq-2.7/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rabbitmq/RabbitDeliveryNetAttributesGetter.java new file mode 100644 index 000000000000..047c0ac18acb --- /dev/null +++ b/instrumentation/rabbitmq-2.7/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rabbitmq/RabbitDeliveryNetAttributesGetter.java @@ -0,0 +1,40 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.rabbitmq; + +import io.opentelemetry.instrumentation.api.semconv.network.NetworkAttributesGetter; +import java.net.Inet4Address; +import java.net.Inet6Address; +import java.net.InetAddress; +import javax.annotation.Nullable; + +public class RabbitDeliveryNetAttributesGetter + implements NetworkAttributesGetter { + + @Nullable + @Override + public String getNetworkType(DeliveryRequest request, @Nullable Void response) { + InetAddress address = request.getConnection().getAddress(); + if (address instanceof Inet4Address) { + return "ipv4"; + } else if (address instanceof Inet6Address) { + return "ipv6"; + } + return null; + } + + @Nullable + @Override + public String getNetworkPeerAddress(DeliveryRequest request, @Nullable Void response) { + return request.getConnection().getAddress().getHostAddress(); + } + + @Nullable + @Override + public Integer getNetworkPeerPort(DeliveryRequest request, @Nullable Void response) { + return request.getConnection().getPort(); + } +} diff --git a/instrumentation/rabbitmq-2.7/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rabbitmq/RabbitInstrumenterHelper.java b/instrumentation/rabbitmq-2.7/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rabbitmq/RabbitInstrumenterHelper.java index 512efdf54a21..2f0c77addc01 100644 --- a/instrumentation/rabbitmq-2.7/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rabbitmq/RabbitInstrumenterHelper.java +++ b/instrumentation/rabbitmq-2.7/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rabbitmq/RabbitInstrumenterHelper.java @@ -13,15 +13,15 @@ import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.trace.Span; import io.opentelemetry.context.Context; -import io.opentelemetry.javaagent.bootstrap.internal.InstrumentationConfig; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.opentelemetry.javaagent.bootstrap.internal.AgentInstrumentationConfig; +import io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes; import java.util.Map; public class RabbitInstrumenterHelper { static final AttributeKey RABBITMQ_COMMAND = AttributeKey.stringKey("rabbitmq.command"); private static final boolean CAPTURE_EXPERIMENTAL_SPAN_ATTRIBUTES = - InstrumentationConfig.get() + AgentInstrumentationConfig.get() .getBoolean("otel.instrumentation.rabbitmq.experimental-span-attributes", false); private static final RabbitInstrumenterHelper INSTRUMENTER_HELPER = @@ -33,10 +33,11 @@ public static RabbitInstrumenterHelper helper() { public void onPublish(Span span, String exchange, String routingKey) { String exchangeName = normalizeExchangeName(exchange); - span.setAttribute(SemanticAttributes.MESSAGING_DESTINATION_NAME, exchangeName); - span.updateName(exchangeName + " send"); + span.setAttribute(MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME, exchangeName); + span.updateName(exchangeName + " publish"); if (routingKey != null && !routingKey.isEmpty()) { - span.setAttribute(SemanticAttributes.MESSAGING_RABBITMQ_DESTINATION_ROUTING_KEY, routingKey); + span.setAttribute( + MessagingIncubatingAttributes.MESSAGING_RABBITMQ_DESTINATION_ROUTING_KEY, routingKey); } if (CAPTURE_EXPERIMENTAL_SPAN_ATTRIBUTES) { span.setAttribute(RABBITMQ_COMMAND, "basic.publish"); diff --git a/instrumentation/rabbitmq-2.7/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rabbitmq/RabbitReceiveAttributesGetter.java b/instrumentation/rabbitmq-2.7/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rabbitmq/RabbitReceiveAttributesGetter.java index 758b37365ce1..1f9e1e743cd8 100644 --- a/instrumentation/rabbitmq-2.7/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rabbitmq/RabbitReceiveAttributesGetter.java +++ b/instrumentation/rabbitmq-2.7/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rabbitmq/RabbitReceiveAttributesGetter.java @@ -6,7 +6,7 @@ package io.opentelemetry.javaagent.instrumentation.rabbitmq; import com.rabbitmq.client.GetResponse; -import io.opentelemetry.instrumentation.api.instrumenter.messaging.MessagingAttributesGetter; +import io.opentelemetry.instrumentation.api.incubator.semconv.messaging.MessagingAttributesGetter; import java.util.Collections; import java.util.List; import javax.annotation.Nullable; @@ -30,6 +30,12 @@ public String getDestination(ReceiveRequest request) { } } + @Nullable + @Override + public String getDestinationTemplate(ReceiveRequest request) { + return null; + } + private static String normalizeExchangeName(String exchange) { return exchange == null || exchange.isEmpty() ? "" : exchange; } @@ -39,6 +45,11 @@ public boolean isTemporaryDestination(ReceiveRequest request) { return false; } + @Override + public boolean isAnonymousDestination(ReceiveRequest request) { + return false; + } + @Nullable @Override public String getConversationId(ReceiveRequest request) { @@ -47,13 +58,13 @@ public String getConversationId(ReceiveRequest request) { @Nullable @Override - public Long getMessagePayloadSize(ReceiveRequest request) { + public Long getMessageBodySize(ReceiveRequest request) { return null; } @Nullable @Override - public Long getMessagePayloadCompressedSize(ReceiveRequest request) { + public Long getMessageEnvelopeSize(ReceiveRequest request) { return null; } @@ -63,6 +74,18 @@ public String getMessageId(ReceiveRequest request, @Nullable GetResponse respons return null; } + @Nullable + @Override + public String getClientId(ReceiveRequest request) { + return null; + } + + @Nullable + @Override + public Long getBatchMessageCount(ReceiveRequest request, @Nullable GetResponse response) { + return null; + } + @Override public List getMessageHeader(ReceiveRequest request, String name) { GetResponse response = request.getResponse(); diff --git a/instrumentation/rabbitmq-2.7/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rabbitmq/RabbitReceiveNetAttributesGetter.java b/instrumentation/rabbitmq-2.7/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rabbitmq/RabbitReceiveNetAttributesGetter.java index 1b7adbf3e529..621d72a27ede 100644 --- a/instrumentation/rabbitmq-2.7/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rabbitmq/RabbitReceiveNetAttributesGetter.java +++ b/instrumentation/rabbitmq-2.7/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rabbitmq/RabbitReceiveNetAttributesGetter.java @@ -6,43 +6,36 @@ package io.opentelemetry.javaagent.instrumentation.rabbitmq; import com.rabbitmq.client.GetResponse; -import io.opentelemetry.instrumentation.api.instrumenter.net.NetClientAttributesGetter; +import io.opentelemetry.instrumentation.api.semconv.network.NetworkAttributesGetter; +import java.net.Inet4Address; import java.net.Inet6Address; +import java.net.InetAddress; import javax.annotation.Nullable; public class RabbitReceiveNetAttributesGetter - implements NetClientAttributesGetter { + implements NetworkAttributesGetter { @Nullable @Override - public String getServerAddress(ReceiveRequest request) { - return null; - } - - @Nullable - @Override - public Integer getServerPort(ReceiveRequest request) { + public String getNetworkType(ReceiveRequest request, @Nullable GetResponse response) { + InetAddress address = request.getConnection().getAddress(); + if (address instanceof Inet4Address) { + return "ipv4"; + } else if (address instanceof Inet6Address) { + return "ipv6"; + } return null; } @Nullable @Override - public String getServerSocketAddress(ReceiveRequest request, @Nullable GetResponse response) { + public String getNetworkPeerAddress(ReceiveRequest request, @Nullable GetResponse response) { return request.getConnection().getAddress().getHostAddress(); } @Nullable @Override - public Integer getServerSocketPort(ReceiveRequest request, @Nullable GetResponse response) { + public Integer getNetworkPeerPort(ReceiveRequest request, @Nullable GetResponse response) { return request.getConnection().getPort(); } - - @Nullable - @Override - public String getSockFamily(ReceiveRequest request, @Nullable GetResponse response) { - if (request.getConnection().getAddress() instanceof Inet6Address) { - return "inet6"; - } - return null; - } } diff --git a/instrumentation/rabbitmq-2.7/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rabbitmq/RabbitSingletons.java b/instrumentation/rabbitmq-2.7/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rabbitmq/RabbitSingletons.java index 070ba7f6a61d..c8af3a8784d6 100644 --- a/instrumentation/rabbitmq-2.7/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rabbitmq/RabbitSingletons.java +++ b/instrumentation/rabbitmq-2.7/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rabbitmq/RabbitSingletons.java @@ -11,27 +11,29 @@ import com.rabbitmq.client.GetResponse; import io.opentelemetry.api.GlobalOpenTelemetry; import io.opentelemetry.context.ContextKey; +import io.opentelemetry.instrumentation.api.incubator.semconv.messaging.MessageOperation; +import io.opentelemetry.instrumentation.api.incubator.semconv.messaging.MessagingAttributesExtractor; +import io.opentelemetry.instrumentation.api.incubator.semconv.messaging.MessagingAttributesGetter; 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.messaging.MessageOperation; -import io.opentelemetry.instrumentation.api.instrumenter.messaging.MessagingAttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.messaging.MessagingAttributesGetter; -import io.opentelemetry.instrumentation.api.instrumenter.net.NetClientAttributesExtractor; import io.opentelemetry.instrumentation.api.internal.PropagatorBasedSpanLinksExtractor; +import io.opentelemetry.instrumentation.api.semconv.network.NetworkAttributesExtractor; +import io.opentelemetry.javaagent.bootstrap.internal.AgentInstrumentationConfig; import io.opentelemetry.javaagent.bootstrap.internal.ExperimentalConfig; -import io.opentelemetry.javaagent.bootstrap.internal.InstrumentationConfig; import java.util.ArrayList; import java.util.List; public final class RabbitSingletons { private static final boolean CAPTURE_EXPERIMENTAL_SPAN_ATTRIBUTES = - InstrumentationConfig.get() + AgentInstrumentationConfig.get() .getBoolean("otel.instrumentation.rabbitmq.experimental-span-attributes", false); private static final String instrumentationName = "io.opentelemetry.rabbitmq-2.7"; private static final Instrumenter channelInstrumenter = - createChannelInstrumenter(); + createChannelInstrumenter(false); + private static final Instrumenter channelPublishInstrumenter = + createChannelInstrumenter(true); private static final Instrumenter receiveInstrumenter = createReceiveInstrumenter(); private static final Instrumenter deliverInstrumenter = @@ -39,8 +41,11 @@ public final class RabbitSingletons { static final ContextKey CHANNEL_AND_METHOD_CONTEXT_KEY = ContextKey.named("opentelemetry-rabbitmq-channel-and-method-context-key"); - public static Instrumenter channelInstrumenter() { - return channelInstrumenter; + public static Instrumenter channelInstrumenter( + ChannelAndMethod channelAndMethod) { + return channelAndMethod.getMethod().equals("Channel.basicPublish") + ? channelPublishInstrumenter + : channelInstrumenter; } public static Instrumenter receiveInstrumenter() { @@ -51,20 +56,18 @@ static Instrumenter deliverInstrumenter() { return deliverInstrumenter; } - private static Instrumenter createChannelInstrumenter() { + private static Instrumenter createChannelInstrumenter(boolean publish) { return Instrumenter.builder( GlobalOpenTelemetry.get(), instrumentationName, ChannelAndMethod::getMethod) .addAttributesExtractor( buildMessagingAttributesExtractor( - RabbitChannelAttributesGetter.INSTANCE, MessageOperation.SEND)) + RabbitChannelAttributesGetter.INSTANCE, publish ? MessageOperation.PUBLISH : null)) .addAttributesExtractor( - NetClientAttributesExtractor.create(new RabbitChannelNetAttributesGetter())) + NetworkAttributesExtractor.create(new RabbitChannelNetAttributesGetter())) .addContextCustomizer( (context, request, startAttributes) -> context.with(CHANNEL_AND_METHOD_CONTEXT_KEY, new RabbitChannelAndMethodHolder())) - .buildInstrumenter( - channelAndMethod -> - channelAndMethod.getMethod().equals("Channel.basicPublish") ? PRODUCER : CLIENT); + .buildInstrumenter(channelAndMethod -> publish ? PRODUCER : CLIENT); } private static Instrumenter createReceiveInstrumenter() { @@ -72,7 +75,7 @@ private static Instrumenter createReceiveInstrument extractors.add( buildMessagingAttributesExtractor( RabbitReceiveAttributesGetter.INSTANCE, MessageOperation.RECEIVE)); - extractors.add(NetClientAttributesExtractor.create(new RabbitReceiveNetAttributesGetter())); + extractors.add(NetworkAttributesExtractor.create(new RabbitReceiveNetAttributesGetter())); if (CAPTURE_EXPERIMENTAL_SPAN_ATTRIBUTES) { extractors.add(new RabbitReceiveExperimentalAttributesExtractor()); } @@ -93,6 +96,7 @@ private static Instrumenter createDeliverInstrumenter() { extractors.add( buildMessagingAttributesExtractor( RabbitDeliveryAttributesGetter.INSTANCE, MessageOperation.PROCESS)); + extractors.add(NetworkAttributesExtractor.create(new RabbitDeliveryNetAttributesGetter())); extractors.add(new RabbitDeliveryExtraAttributesExtractor()); if (CAPTURE_EXPERIMENTAL_SPAN_ATTRIBUTES) { extractors.add(new RabbitDeliveryExperimentalAttributesExtractor()); diff --git a/instrumentation/rabbitmq-2.7/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rabbitmq/TracedDelegatingConsumer.java b/instrumentation/rabbitmq-2.7/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rabbitmq/TracedDelegatingConsumer.java index b94fb624d5eb..2737d0a947a5 100644 --- a/instrumentation/rabbitmq-2.7/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rabbitmq/TracedDelegatingConsumer.java +++ b/instrumentation/rabbitmq-2.7/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rabbitmq/TracedDelegatingConsumer.java @@ -8,6 +8,7 @@ import static io.opentelemetry.javaagent.instrumentation.rabbitmq.RabbitSingletons.deliverInstrumenter; import com.rabbitmq.client.AMQP; +import com.rabbitmq.client.Connection; import com.rabbitmq.client.Consumer; import com.rabbitmq.client.Envelope; import com.rabbitmq.client.ShutdownSignalException; @@ -23,10 +24,12 @@ public class TracedDelegatingConsumer implements Consumer { private final String queue; private final Consumer delegate; + private final Connection connection; - public TracedDelegatingConsumer(String queue, Consumer delegate) { + public TracedDelegatingConsumer(String queue, Consumer delegate, Connection connection) { this.queue = queue; this.delegate = delegate; + this.connection = connection; } @Override @@ -59,7 +62,7 @@ public void handleDelivery( String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { Context parentContext = Context.current(); - DeliveryRequest request = DeliveryRequest.create(queue, envelope, properties, body); + DeliveryRequest request = DeliveryRequest.create(queue, envelope, connection, properties, body); if (!deliverInstrumenter().shouldStart(parentContext, request)) { delegate.handleDelivery(consumerTag, envelope, properties, body); diff --git a/instrumentation/rabbitmq-2.7/javaagent/src/test/groovy/RabbitMqTest.groovy b/instrumentation/rabbitmq-2.7/javaagent/src/test/groovy/RabbitMqTest.groovy deleted file mode 100644 index 9245f875a976..000000000000 --- a/instrumentation/rabbitmq-2.7/javaagent/src/test/groovy/RabbitMqTest.groovy +++ /dev/null @@ -1,466 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -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 - -import java.util.concurrent.CountDownLatch -import java.util.concurrent.TimeUnit - -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 - -class RabbitMqTest extends AgentInstrumentationSpecification implements WithRabbitMqTrait { - - Connection conn = connectionFactory.newConnection() - Channel channel = conn.createChannel() - - def setupSpec() { - startRabbit() - connectionFactory.setAutomaticRecoveryEnabled(false) - } - - def cleanupSpec() { - stopRabbit() - } - - def cleanup() { - try { - channel.close() - conn.close() - } catch (ShutdownSignalException ignored) { - } - } - - def "test rabbit publish/get"() { - setup: - String queueName = runWithSpan("producer 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 queueName - } - GetResponse response = runWithSpan("consumer parent") { - return channel.basicGet(queueName, true) - } - - expect: - new String(response.getBody()) == "Hello, world!" - - and: - assertTraces(2) { - SpanData producerSpan - - trace(0, 5) { - span(0) { - name "producer parent" - hasNoParent() - } - 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", span(0)) - - producerSpan = span(4) - } - - trace(1, 2) { - span(0) { - name "consumer parent" - hasNoParent() - } - rabbitSpan(it, 1, exchangeName, routingKey, "receive", "", span(0), producerSpan) - } - } - - where: - exchangeName | routingKey - "some-exchange" | "some-routing-key" - } - - def "test rabbit publish/get default exchange"() { - setup: - String queueName = runWithSpan("producer parent") { - String queueName = channel.queueDeclare().getQueue() - channel.basicPublish("", queueName, null, "Hello, world!".getBytes()) - return queueName - } - GetResponse response = runWithSpan("consumer parent") { - return channel.basicGet(queueName, true) - } - - expect: - new String(response.getBody()) == "Hello, world!" - - and: - assertTraces(2) { - SpanData producerSpan - - trace(0, 3) { - span(0) { - name "producer parent" - hasNoParent() - } - rabbitSpan(it, 1, null, null, null, "queue.declare", span(0)) - rabbitSpan(it, 2, "", null, "send", "", span(0)) - - producerSpan = span(2) - } - - trace(1, 2) { - span(0) { - name "consumer parent" - hasNoParent() - } - rabbitSpan(it, 1, "", null, "receive", "", span(0), producerSpan) - } - } - } - - 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, 0, null, null, null, "exchange.declare") - } - trace(1, 1) { - rabbitSpan(it, 0, null, null, null, "queue.declare") - } - trace(2, 1) { - rabbitSpan(it, 0, null, null, null, "queue.bind") - } - trace(3, 1) { - rabbitSpan(it, 0, 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, 0, null, null, null, "exchange.declare") - } - trace(1, 1) { - rabbitSpan(it, 0, null, null, null, "queue.declare") - } - trace(2, 1) { - rabbitSpan(it, 0, null, null, null, "queue.bind") - } - trace(3, 1) { - rabbitSpan(it, 0, 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 error = thrown(exception) - - and: - - assertTraces(1) { - trace(0, 1) { - rabbitSpan(it, 0, null, null, operation, command, null, null, error, 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 cachingConnectionFactory = new CachingConnectionFactory(connectionFactory) - AmqpAdmin admin = new RabbitAdmin(cachingConnectionFactory) - AmqpTemplate template = new RabbitTemplate(cachingConnectionFactory) - - def queue = new Queue("some-routing-queue", false, true, true, null) - runWithSpan("producer parent") { - admin.declareQueue(queue) - template.convertAndSend(queue.name, "foo") - } - String message = runWithSpan("consumer parent") { - return template.receiveAndConvert(queue.name) as String - } - - expect: - message == "foo" - - and: - assertTraces(2) { - SpanData producerSpan - - trace(0, 3) { - span(0) { - name "producer parent" - hasNoParent() - } - rabbitSpan(it, 1, null, null, null, "queue.declare", span(0)) - rabbitSpan(it, 2, "", "some-routing-queue", "send", "", span(0)) - - producerSpan = span(2) - } - - trace(1, 2) { - span(0) { - name "consumer parent" - hasNoParent() - } - rabbitSpan(it, 1, "", "some-routing-queue", "receive", queue.name, span(0), producerSpan) - } - } - - cleanup: - cachingConnectionFactory.destroy() - } - - def "capture message header as span attributes"() { - setup: - String queueName = channel.queueDeclare().getQueue() - def properties = new AMQP.BasicProperties.Builder().headers(["test-message-header": "test"]).build() - channel.basicPublish("", queueName, properties, "Hello, world!".getBytes()) - - def latch = new CountDownLatch(1) - def deliveries = [] - - Consumer callback = new DefaultConsumer(channel) { - @Override - void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties props, byte[] body) throws IOException { - deliveries << new String(body) - latch.countDown() - } - } - - channel.basicConsume(queueName, callback) - latch.await(10, TimeUnit.SECONDS) - expect: - deliveries[0] == "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, 2) { - rabbitSpan(it, 0, "", null, "send", "", null, null, null, null, false, true) - rabbitSpan(it, 1, "", null, "process", "", span(0), null, null, null, false, true) - } - trace(2, 1) { - rabbitSpan(it, 0, null, null, null, "basic.consume") - } - } - } - - def rabbitSpan( - TraceAssert trace, - int index, - String exchange, - String routingKey, - String operation, - String resource, - SpanData parentSpan = null, - SpanData linkSpan = null, - Throwable exception = null, - String errorMsg = null, - boolean expectTimestamp = false, - boolean testHeaders = false - ) { - - def spanName = resource - if (operation != null) { - spanName = spanName + " " + operation - } - - def rabbitCommand = trace.span(index).attributes.get(AttributeKey.stringKey("rabbitmq.command")) - - def spanKind - switch (rabbitCommand) { - case "basic.publish": - spanKind = PRODUCER - break - case "basic.get": // fallthrough - case "basic.deliver": - spanKind = CONSUMER - break - default: - spanKind = CLIENT - } - - trace.span(index) { - name spanName - kind spanKind - - if (parentSpan == null) { - hasNoParent() - } else { - childOf(parentSpan) - } - - if (linkSpan == null) { - hasNoLinks() - } else { - hasLink(linkSpan) - } - - if (exception) { - status ERROR - errorEvent(exception.class, errorMsg) - } - - attributes { - // listener does not have access to net attributes - if (rabbitCommand != "basic.deliver") { - "$SemanticAttributes.NET_SOCK_PEER_ADDR" { it == "127.0.0.1" || it == "0:0:0:0:0:0:0:1" || it == null } - "$SemanticAttributes.NET_SOCK_PEER_PORT" Long - "$SemanticAttributes.NET_SOCK_FAMILY" { it == SemanticAttributes.NetSockFamilyValues.INET6 || it == null } - } - - "$SemanticAttributes.MESSAGING_SYSTEM" "rabbitmq" - "$SemanticAttributes.MESSAGING_DESTINATION_NAME" exchange - - "$SemanticAttributes.MESSAGING_RABBITMQ_DESTINATION_ROUTING_KEY" { it == null || it == routingKey || it.startsWith("amq.gen-") } - if (operation != null && operation != "send") { - "$SemanticAttributes.MESSAGING_OPERATION" operation - } - if (expectTimestamp) { - "rabbitmq.record.queue_time_ms" { it instanceof Long && it >= 0 } - } - if (testHeaders) { - "messaging.header.test_message_header" { it == ["test"] } - } - - switch (rabbitCommand) { - case "basic.publish": - "rabbitmq.command" "basic.publish" - "$SemanticAttributes.MESSAGING_RABBITMQ_DESTINATION_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" 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" { it == null || it instanceof Long } - break - case "basic.deliver": - "rabbitmq.command" "basic.deliver" - "$SemanticAttributes.MESSAGING_MESSAGE_PAYLOAD_SIZE_BYTES" Long - break - default: - "rabbitmq.command" { it == null || it == resource } - } - } - } - } -} diff --git a/instrumentation/rabbitmq-2.7/javaagent/src/test/groovy/ReactorRabbitMqTest.groovy b/instrumentation/rabbitmq-2.7/javaagent/src/test/groovy/ReactorRabbitMqTest.groovy deleted file mode 100644 index 6b4abf84ea99..000000000000 --- a/instrumentation/rabbitmq-2.7/javaagent/src/test/groovy/ReactorRabbitMqTest.groovy +++ /dev/null @@ -1,54 +0,0 @@ -/* - * 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() - connectionFactory.setAutomaticRecoveryEnabled(false) - } - - 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_SOCK_PEER_ADDR" { it == "127.0.0.1" || it == "0:0:0:0:0:0:0:1" || it == null } - "$SemanticAttributes.NET_SOCK_PEER_PORT" Long - "$SemanticAttributes.NET_SOCK_FAMILY" { it == SemanticAttributes.NetSockFamilyValues.INET6 || it == null } - "$SemanticAttributes.MESSAGING_SYSTEM" "rabbitmq" - "rabbitmq.command" "exchange.declare" - } - } - } - } - - cleanup: - sender?.close() - } -} diff --git a/instrumentation/rabbitmq-2.7/javaagent/src/test/groovy/WithRabbitMqTrait.groovy b/instrumentation/rabbitmq-2.7/javaagent/src/test/groovy/WithRabbitMqTrait.groovy deleted file mode 100644 index 0cc23489e86e..000000000000 --- a/instrumentation/rabbitmq-2.7/javaagent/src/test/groovy/WithRabbitMqTrait.groovy +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -import com.rabbitmq.client.ConnectionFactory -import org.slf4j.Logger -import org.slf4j.LoggerFactory -import org.testcontainers.containers.GenericContainer -import org.testcontainers.containers.output.Slf4jLogConsumer -import org.testcontainers.containers.wait.strategy.Wait - -import java.time.Duration - -trait WithRabbitMqTrait { - private static final Logger logger = LoggerFactory.getLogger("io.opentelemetry.testing.rabbitmq-container") - - static GenericContainer rabbitMqContainer - static ConnectionFactory connectionFactory - - def startRabbit() { - rabbitMqContainer = new GenericContainer('rabbitmq:latest') - .withExposedPorts(5672) - .withLogConsumer(new Slf4jLogConsumer(logger)) - .waitingFor(Wait.forLogMessage(".*Server startup complete.*", 1)) - .withStartupTimeout(Duration.ofMinutes(2)) - rabbitMqContainer.start() - - connectionFactory = new ConnectionFactory( - host: rabbitMqContainer.host, - port: rabbitMqContainer.getMappedPort(5672) - ) - } - - def stopRabbit() { - rabbitMqContainer?.stop() - } -} diff --git a/instrumentation/rabbitmq-2.7/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/rabbitmq/AbstractRabbitMqTest.java b/instrumentation/rabbitmq-2.7/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/rabbitmq/AbstractRabbitMqTest.java new file mode 100644 index 000000000000..e25af11e1f0f --- /dev/null +++ b/instrumentation/rabbitmq-2.7/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/rabbitmq/AbstractRabbitMqTest.java @@ -0,0 +1,59 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.rabbitmq; + +import com.rabbitmq.client.ConnectionFactory; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.time.Duration; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.output.Slf4jLogConsumer; +import org.testcontainers.containers.wait.strategy.Wait; + +public abstract class AbstractRabbitMqTest { + private static final Logger logger = + LoggerFactory.getLogger("io.opentelemetry.testing.rabbitmq-container"); + + private static GenericContainer rabbitMqContainer; + protected static ConnectionFactory connectionFactory; + + protected static String rabbitMqHost; + + protected static String rabbitMqIp; + + protected static int rabbitMqPort; + + @BeforeAll + static void startRabbit() throws UnknownHostException { + rabbitMqContainer = + new GenericContainer<>("rabbitmq:latest") + .withExposedPorts(5672) + .withLogConsumer(new Slf4jLogConsumer(logger)) + .waitingFor(Wait.forLogMessage(".*Server startup complete.*", 1)) + .withStartupTimeout(Duration.ofMinutes(2)); + rabbitMqContainer.start(); + + connectionFactory = new ConnectionFactory(); + connectionFactory.setHost(rabbitMqContainer.getHost()); + connectionFactory.setPort(rabbitMqContainer.getMappedPort(5672)); + connectionFactory.setAutomaticRecoveryEnabled(false); + + rabbitMqHost = rabbitMqContainer.getHost(); + rabbitMqIp = InetAddress.getByName(rabbitMqContainer.getHost()).getHostAddress(); + rabbitMqPort = rabbitMqContainer.getMappedPort(5672); + } + + @AfterAll + static void stopRabbit() { + if (null != rabbitMqContainer) { + rabbitMqContainer.stop(); + } + } +} diff --git a/instrumentation/rabbitmq-2.7/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/rabbitmq/RabbitMqTest.java b/instrumentation/rabbitmq-2.7/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/rabbitmq/RabbitMqTest.java new file mode 100644 index 000000000000..8347f81df4f8 --- /dev/null +++ b/instrumentation/rabbitmq-2.7/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/rabbitmq/RabbitMqTest.java @@ -0,0 +1,912 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.rabbitmq; + +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.satisfies; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +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.api.trace.SpanKind; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.sdk.testing.assertj.SpanDataAssert; +import io.opentelemetry.sdk.testing.assertj.TraceAssert; +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.ExceptionAttributes; +import io.opentelemetry.semconv.NetworkAttributes; +import io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicReference; +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.junit.jupiter.api.extension.RegisterExtension; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.aggregator.ArgumentsAccessor; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +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 AbstractRabbitMqTest { + @RegisterExtension + private static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + Connection conn; + Channel channel; + + @BeforeEach + public void setup() throws IOException, TimeoutException { + conn = connectionFactory.newConnection(); + channel = conn.createChannel(); + } + + @AfterEach + public void cleanup() throws IOException, TimeoutException { + try { + if (null != channel) { + channel.close(); + } + if (null != conn) { + conn.close(); + } + } catch (ShutdownSignalException ignored) { + } + } + + @Test + void testRabbitPublishGet() throws IOException { + String exchangeName = "some-exchange"; + String routingKey = "some-routing-key"; + + String queueName = + testing.runWithSpan( + "producer parent", + () -> { + channel.exchangeDeclare(exchangeName, "direct", false); + String internalQueueName = channel.queueDeclare().getQueue(); + channel.queueBind(internalQueueName, exchangeName, routingKey); + channel.basicPublish( + exchangeName, + routingKey, + null, + "Hello, world!".getBytes(Charset.defaultCharset())); + return internalQueueName; + }); + GetResponse response = + testing.runWithSpan( + "consumer parent", + () -> { + return channel.basicGet(queueName, true); + }); + + assertEquals("Hello, world!", new String(response.getBody(), Charset.defaultCharset())); + + AtomicReference producerSpan = new AtomicReference<>(); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> { + span.hasName("producer parent").hasKind(SpanKind.INTERNAL).hasNoParent(); + }, + span -> { + verifySpan(trace, span, 1, "exchange.declare", trace.getSpan(0)); + }, + span -> { + verifySpan(trace, span, 2, "queue.declare", trace.getSpan(0)); + }, + span -> { + verifySpan(trace, span, 3, "queue.bind", trace.getSpan(0)); + }, + span -> { + verifySpan( + trace, + span, + 4, + exchangeName, + routingKey, + "publish", + exchangeName, + trace.getSpan(0)); + producerSpan.set(trace.getSpan(4)); + }), + trace -> { + trace.hasSpansSatisfyingExactly( + span -> { + span.hasName("consumer parent").hasKind(SpanKind.INTERNAL).hasNoParent(); + }, + span -> { + verifySpan( + trace, + span, + 1, + exchangeName, + routingKey, + "receive", + "", + trace.getSpan(0), + producerSpan.get(), + null, + null, + false); + }); + }); + } + + @Test + void testRabbitPublishGetWithDefaultExchange() throws IOException { + String queueName = + testing.runWithSpan( + "producer parent", + () -> { + String internalQueueName = channel.queueDeclare().getQueue(); + channel.basicPublish( + "", internalQueueName, null, "Hello, world!".getBytes(Charset.defaultCharset())); + return internalQueueName; + }); + GetResponse response = + testing.runWithSpan( + "consumer parent", + () -> { + return channel.basicGet(queueName, true); + }); + + assertEquals("Hello, world!", new String(response.getBody(), Charset.defaultCharset())); + + AtomicReference producerSpan = new AtomicReference<>(); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> { + span.hasName("producer parent").hasKind(SpanKind.INTERNAL).hasNoParent(); + }, + span -> { + verifySpan(trace, span, 1, "queue.declare", trace.getSpan(0)); + }, + span -> { + verifySpan( + trace, span, 2, "", null, "publish", "", trace.getSpan(0)); + producerSpan.set(trace.getSpan(2)); + }), + trace -> { + trace.hasSpansSatisfyingExactly( + span -> { + span.hasName("consumer parent").hasKind(SpanKind.INTERNAL).hasNoParent(); + }, + span -> { + verifySpan( + trace, + span, + 1, + "", + null, + "receive", + "", + trace.getSpan(0), + producerSpan.get(), + null, + null, + false); + }); + }); + } + + @ParameterizedTest(name = "test rabbit consume {1} messages and setTimestamp={2}") + @MethodSource("provideParametersForMessageCountAndTimestamp") + void testRabbitConsumeMessageCountAndSetTimestamp( + String exchangeName, int messageCount, boolean setTimestamp) throws IOException { + 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, ""); + + List deliveries = new ArrayList<>(); + + Consumer callback = + new DefaultConsumer(channel) { + @Override + public void handleDelivery( + String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) + throws IOException { + deliveries.add(new String(body, Charset.defaultCharset())); + } + }; + + channel.basicConsume(queueName, callback); + + for (int i = 1; i <= messageCount; i++) { + if (setTimestamp) { + channel.basicPublish( + exchangeName, + "", + new AMQP.BasicProperties.Builder().timestamp(new Date()).build(), + ("msg " + i).getBytes(Charset.defaultCharset())); + } else { + channel.basicPublish( + exchangeName, "", null, ("msg " + i).getBytes(Charset.defaultCharset())); + } + } + + String resource = messageCount % 2 == 0 ? "" : queueName; + + List> traceAssertions = new ArrayList<>(); + traceAssertions.add( + trace -> { + trace.hasSpansSatisfyingExactly( + span -> { + verifySpan(trace, span, 0, "exchange.declare"); + }); + }); + traceAssertions.add( + trace -> { + trace.hasSpansSatisfyingExactly( + span -> { + verifySpan(trace, span, 0, "queue.declare"); + }); + }); + traceAssertions.add( + trace -> { + trace.hasSpansSatisfyingExactly( + span -> { + verifySpan(trace, span, 0, "queue.bind"); + }); + }); + traceAssertions.add( + trace -> { + trace.hasSpansSatisfyingExactly( + span -> { + verifySpan(trace, span, 0, "basic.consume"); + }); + }); + + for (int i = 1; i <= messageCount; i++) { + traceAssertions.add( + trace -> { + trace.hasSpansSatisfyingExactly( + span -> { + verifySpan(trace, span, 0, exchangeName, null, "publish", exchangeName); + }, + span -> { + verifySpan( + trace, + span, + 1, + exchangeName, + null, + "process", + resource, + trace.getSpan(0), + null, + null, + null, + setTimestamp); + }); + }); + } + + testing.waitAndAssertTraces(traceAssertions); + + assertEquals(messageCount, deliveries.size()); + for (int i = 1; i <= messageCount; i++) { + assertEquals("msg " + i, deliveries.get(i - 1)); + } + } + + @Test + void testRabbitConsumeError() throws IOException { + String exchangeName = "some-error-exchange"; + FileNotFoundException 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 + public 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(Charset.defaultCharset())); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> { + verifySpan(trace, span, 0, "exchange.declare"); + }), + trace -> + trace.hasSpansSatisfyingExactly( + span -> { + verifySpan(trace, span, 0, "queue.declare"); + }), + trace -> + trace.hasSpansSatisfyingExactly( + span -> { + verifySpan(trace, span, 0, "queue.bind"); + }), + trace -> + trace.hasSpansSatisfyingExactly( + span -> { + verifySpan(trace, span, 0, "basic.consume"); + }), + trace -> { + trace.hasSpansSatisfyingExactly( + span -> { + verifySpan(trace, span, 0, exchangeName, null, "publish", exchangeName); + }, + span -> { + verifySpan( + trace, + span, + 1, + exchangeName, + null, + "process", + "", + trace.getSpan(0), + null, + error, + error.getMessage(), + false); + }); + }); + } + + @SuppressWarnings("unchecked") + @ParameterizedTest(name = "test rabbit error {0}") + @MethodSource("provideParametersForCommandError") + void testRabbitCommandError(ArgumentsAccessor accessor) { + java.util.function.Consumer callback = + (java.util.function.Consumer) accessor.get(4); + Throwable thrown = null; + try { + callback.accept(channel); + } catch (RuntimeException re) { + thrown = re.getCause(); + assertTrue(thrown.getClass().getName().contains(accessor.getString(1))); + } + + Throwable finalThrown = thrown; + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> { + verifySpan( + trace, + span, + 0, + null, + null, + accessor.getString(3), + accessor.getString(0), + null, + null, + finalThrown, + accessor.getString(2), + false); + })); + } + + @Test + void testSpringRabbit() { + CachingConnectionFactory cachingConnectionFactory = + new CachingConnectionFactory(connectionFactory); + AmqpAdmin admin = new RabbitAdmin(cachingConnectionFactory); + AmqpTemplate template = new RabbitTemplate(cachingConnectionFactory); + + Queue queue = new Queue("some-routing-queue", false, true, true, null); + testing.runWithSpan( + "producer parent", + () -> { + admin.declareQueue(queue); + template.convertAndSend(queue.getName(), "foo"); + }); + + String message = + testing.runWithSpan( + "consumer parent", () -> (String) template.receiveAndConvert(queue.getName())); + + assertEquals("foo", message); + + AtomicReference producerSpan = new AtomicReference<>(); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> { + span.hasName("producer parent").hasKind(SpanKind.INTERNAL).hasNoParent(); + }, + span -> { + verifySpan(trace, span, 1, "queue.declare", trace.getSpan(0)); + }, + span -> { + verifySpan( + trace, + span, + 2, + "", + "some-routing-queue", + "publish", + "", + trace.getSpan(0)); + producerSpan.set(trace.getSpan(2)); + }), + trace -> { + trace.hasSpansSatisfyingExactly( + span -> { + span.hasName("consumer parent").hasKind(SpanKind.INTERNAL).hasNoParent(); + }, + span -> { + verifySpan( + trace, + span, + 1, + "", + "some-routing-queue", + "receive", + queue.getName(), + trace.getSpan(0), + producerSpan.get(), + null, + null, + false); + }); + }); + + cachingConnectionFactory.destroy(); + } + + @Test + void captureMessageHeaderAsSpanAttributes() throws IOException, InterruptedException { + String queueName = channel.queueDeclare().getQueue(); + Map headers = new java.util.HashMap<>(); + headers.put("test-message-header", "test"); + AMQP.BasicProperties properties = new AMQP.BasicProperties.Builder().headers(headers).build(); + channel.basicPublish( + "", queueName, properties, "Hello, world!".getBytes(Charset.defaultCharset())); + + CountDownLatch latch = new CountDownLatch(1); + List deliveries = new ArrayList<>(); + + Consumer callback = + new DefaultConsumer(channel) { + @Override + public void handleDelivery( + String consumerTag, Envelope envelope, AMQP.BasicProperties props, byte[] body) + throws IOException { + deliveries.add(new String(body, Charset.defaultCharset())); + latch.countDown(); + } + }; + + channel.basicConsume(queueName, callback); + assertTrue(latch.await(10, TimeUnit.SECONDS)); + + assertEquals("Hello, world!", deliveries.get(0)); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> { + verifySpan(trace, span, 0, "queue.declare"); + }), + trace -> + trace.hasSpansSatisfyingExactly( + span -> { + verifySpan(trace, span, 0, "", null, "publish", ""); + span.hasAttributesSatisfying( + attributes -> { + assertThat(attributes) + .satisfies( + attrs -> { + List verifyHeaders = + attrs.get( + AttributeKey.stringArrayKey( + "messaging.header.test_message_header")); + assertNotNull(verifyHeaders); + assertTrue(verifyHeaders.contains("test")); + }); + }); + }, + span -> { + verifySpan( + trace, + span, + 1, + "", + null, + "process", + "", + trace.getSpan(0)); + span.hasAttributesSatisfying( + attributes -> { + assertThat(attributes) + .satisfies( + attrs -> { + List verifyHeaders = + attrs.get( + AttributeKey.stringArrayKey( + "messaging.header.test_message_header")); + assertNotNull(verifyHeaders); + assertTrue(verifyHeaders.contains("test")); + }); + }); + }), + trace -> + trace.hasSpansSatisfyingExactly( + span -> { + verifySpan(trace, span, 0, "basic.consume"); + })); + } + + private static Stream provideParametersForMessageCountAndTimestamp() { + return Stream.of( + Arguments.of("some-exchange", 1, false), + Arguments.of("some-exchange", 2, false), + Arguments.of("some-exchange", 3, false), + Arguments.of("some-exchange", 4, false), + Arguments.of("some-exchange", 1, true), + Arguments.of("some-exchange", 2, true), + Arguments.of("some-exchange", 3, true), + Arguments.of("some-exchange", 4, true)); + } + + private static Stream provideParametersForCommandError() { + return Stream.of( + Arguments.of( + "exchange.declare", + "IOException", + null, + null, + (java.util.function.Consumer) + channel -> { + try { + channel.exchangeDeclare("some-exchange", "invalid-type", true); + } catch (Exception e) { + throw new RuntimeException(e); + } + }), + Arguments.of( + "Channel.basicConsume", + "IllegalStateException", + "Invalid configuration: 'queue' must be non-null.", + null, + (java.util.function.Consumer) + channel -> { + try { + channel.basicConsume(null, null); + } catch (Exception e) { + throw new RuntimeException(e); + } + }), + Arguments.of( + "", + "IOException", + null, + "receive", + (java.util.function.Consumer) + channel -> { + try { + channel.basicGet("amq.gen-invalid-channel", true); + } catch (Exception e) { + throw new RuntimeException(e); + } + })); + } + + private static void verifySpan( + TraceAssert trace, SpanDataAssert span, int index, String resource) { + verifySpan(trace, span, index, null, null, null, resource, null); + } + + private static void verifySpan( + TraceAssert trace, SpanDataAssert span, int index, String resource, SpanData parentSpan) { + verifySpan(trace, span, index, null, null, null, resource, parentSpan); + } + + private static void verifySpan( + TraceAssert trace, + SpanDataAssert span, + int index, + String exchange, + String routingKey, + String operation, + String resource) { + verifySpan(trace, span, index, exchange, routingKey, operation, resource, null); + } + + private static void verifySpan( + TraceAssert trace, + SpanDataAssert span, + int index, + String exchange, + String routingKey, + String operation, + String resource, + SpanData parentSpan) { + verifySpan( + trace, + span, + index, + exchange, + routingKey, + operation, + resource, + parentSpan, + null, + null, + null, + false); + } + + private static void verifySpan( + TraceAssert trace, + SpanDataAssert span, + int index, + String exchange, + String routingKey, + String operation, + String resource, + SpanData parentSpan, + SpanData linkSpan, + Throwable exception, + String errorMsg, + boolean expectTimestamp) { + String spanName = resource; + if (operation != null) { + spanName += " " + operation; + } + + String rabbitCommand = + trace.getSpan(index).getAttributes().get(AttributeKey.stringKey("rabbitmq.command")); + + SpanKind spanKind = captureSpanKind(rabbitCommand); + + span.hasName(spanName).hasKind(spanKind); + + verifyParentAndLink(span, parentSpan, linkSpan); + + if (null != exception) { + verifyException(span, exception, errorMsg); + } + + verifyNetAttributes(span); + verifyMessagingAttributes(span, exchange, routingKey, operation); + + if (expectTimestamp) { + span.hasAttributesSatisfying( + attributes -> { + assertThat(attributes) + .satisfies( + attrs -> { + Long timestamp = + attrs.get(AttributeKey.longKey("rabbitmq.record.queue_time_ms")); + assertNotNull(timestamp); + assertTrue(timestamp >= 0); + }); + }); + } + + if (null != rabbitCommand) { + switch (rabbitCommand) { + case "basic.publish": + span.hasAttribute(AttributeKey.stringKey("rabbitmq.command"), "basic.publish") + .hasAttributesSatisfying( + attributes -> { + assertThat(attributes) + .satisfies( + attrs -> { + String routingKeyAttr = + attrs.get( + MessagingIncubatingAttributes + .MESSAGING_RABBITMQ_DESTINATION_ROUTING_KEY); + assertTrue( + routingKeyAttr == null + || routingKeyAttr.equals("some-routing-key") + || routingKeyAttr.equals("some-routing-queue") + || routingKeyAttr.startsWith("amq.gen-")); + + Long deliveryMode = + attrs.get(AttributeKey.longKey("rabbitmq.delivery_mode")); + assertTrue(deliveryMode == null || deliveryMode == 2); + + assertNotNull( + attrs.get( + MessagingIncubatingAttributes.MESSAGING_MESSAGE_BODY_SIZE)); + }); + }); + break; + case "basic.get": + span.hasAttribute(AttributeKey.stringKey("rabbitmq.command"), "basic.get") + .hasAttributesSatisfying( + attributes -> { + assertThat(attributes) + .satisfies( + attrs -> { + // TODO why this queue name is not a destination for semantic + // convention + String queue = attrs.get(AttributeKey.stringKey("rabbitmq.queue")); + assertNotNull(queue); + assertTrue( + queue.equals("some-queue") + || queue.equals("some-routing-queue") + || queue.startsWith("amq.gen-")); + + attrs.get(MessagingIncubatingAttributes.MESSAGING_MESSAGE_BODY_SIZE); + }); + }); + break; + case "basic.deliver": + span.hasAttribute(AttributeKey.stringKey("rabbitmq.command"), "basic.deliver") + .hasAttributesSatisfying( + attributes -> { + assertThat(attributes) + .satisfies( + attrs -> { + attrs.get(MessagingIncubatingAttributes.MESSAGING_MESSAGE_BODY_SIZE); + }); + }); + break; + default: + span.hasAttributesSatisfying( + attributes -> { + assertThat(attributes) + .satisfies( + attrs -> { + String command = attrs.get(AttributeKey.stringKey("rabbitmq.command")); + assertTrue(command == null || command.equals(resource)); + }); + }); + } + } else { + span.hasAttributesSatisfying( + attributes -> { + assertThat(attributes) + .satisfies( + attrs -> { + String command = attrs.get(AttributeKey.stringKey("rabbitmq.command")); + assertTrue(command == null || command.equals(resource)); + }); + }); + } + } + + // Ignoring deprecation warning for use of SemanticAttributes + private static void verifyMessagingAttributes( + SpanDataAssert span, String exchange, String routingKey, String operation) { + span.hasAttribute(MessagingIncubatingAttributes.MESSAGING_SYSTEM, "rabbitmq") + .hasAttributesSatisfying( + attributes -> { + assertThat(attributes) + .satisfies( + attrs -> { + String destinationName = + attrs.get(MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME); + assertTrue(destinationName == null || destinationName.equals(exchange)); + String routingKeyAttr = + attrs.get( + MessagingIncubatingAttributes + .MESSAGING_RABBITMQ_DESTINATION_ROUTING_KEY); + assertTrue( + routingKeyAttr == null + || routingKeyAttr.equals(routingKey) + || routingKeyAttr.startsWith("amq.gen-")); + }); + }); + + if (operation != null && !operation.equals("publish")) { + span.hasAttribute(MessagingIncubatingAttributes.MESSAGING_OPERATION, operation); + } + } + + private static SpanKind captureSpanKind(String rabbitCommand) { + SpanKind spanKind = SpanKind.CLIENT; + + if (null != rabbitCommand) { + switch (rabbitCommand) { + case "basic.publish": + spanKind = SpanKind.PRODUCER; + break; + case "basic.get": // fallthrough + case "basic.deliver": + spanKind = SpanKind.CONSUMER; + break; + default: + break; + } + } + + return spanKind; + } + + private static void verifyNetAttributes(SpanDataAssert span) { + span.hasAttributesSatisfying( + attributes -> { + assertThat(attributes) + .satisfies( + attrs -> { + String peerAddr = attrs.get(NetworkAttributes.NETWORK_PEER_ADDRESS); + assertThat(peerAddr).isIn(rabbitMqIp, null); + + String networkType = attrs.get(NetworkAttributes.NETWORK_TYPE); + assertThat(networkType).isIn("ipv4", "ipv6", null); + + assertNotNull(attrs.get(NetworkAttributes.NETWORK_PEER_PORT)); + }); + }); + } + + private static void verifyException(SpanDataAssert span, Throwable exception, String errorMsg) { + span.hasStatus(StatusData.error()) + .hasEventsSatisfying( + events -> { + assertThat(events.get(0)) + .hasName("exception") + .hasAttributesSatisfying( + equalTo(ExceptionAttributes.EXCEPTION_TYPE, exception.getClass().getName()), + equalTo(ExceptionAttributes.EXCEPTION_MESSAGE, errorMsg), + satisfies( + ExceptionAttributes.EXCEPTION_STACKTRACE, + val -> val.isInstanceOf(String.class))); + }); + } + + private static void verifyParentAndLink( + SpanDataAssert span, SpanData parentSpan, SpanData linkSpan) { + if (null != parentSpan) { + span.hasParent(parentSpan); + } else { + span.hasNoParent(); + } + + if (null != linkSpan) { + span.hasLinks(LinkData.create(linkSpan.getSpanContext())); + } else { + span.hasTotalRecordedLinks(0); + } + } +} diff --git a/instrumentation/rabbitmq-2.7/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/rabbitmq/ReactorRabbitMqTest.java b/instrumentation/rabbitmq-2.7/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/rabbitmq/ReactorRabbitMqTest.java new file mode 100644 index 000000000000..cf52b5e3a84b --- /dev/null +++ b/instrumentation/rabbitmq-2.7/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/rabbitmq/ReactorRabbitMqTest.java @@ -0,0 +1,66 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.rabbitmq; + +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.semconv.NetworkAttributes; +import io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import reactor.rabbitmq.ExchangeSpecification; +import reactor.rabbitmq.RabbitFlux; +import reactor.rabbitmq.Sender; +import reactor.rabbitmq.SenderOptions; + +class ReactorRabbitMqTest extends AbstractRabbitMqTest { + @RegisterExtension + private static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + @Test + void testShouldNotFailDeclaringExchange() { + Sender sender = + RabbitFlux.createSender(new SenderOptions().connectionFactory(connectionFactory)); + + try { + sender.declareExchange(ExchangeSpecification.exchange("testExchange")).block(); + } catch (RuntimeException e) { + Assertions.fail("Should not fail declaring exchange", e); + } + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> { + span.hasName("exchange.declare") + .hasKind(SpanKind.CLIENT) + .hasAttribute(MessagingIncubatingAttributes.MESSAGING_SYSTEM, "rabbitmq") + .hasAttribute(AttributeKey.stringKey("rabbitmq.command"), "exchange.declare") + .hasAttributesSatisfying( + attributes -> + assertThat(attributes) + .satisfies( + attrs -> { + String peerAddr = + attrs.get(NetworkAttributes.NETWORK_PEER_ADDRESS); + assertThat(peerAddr).isIn(rabbitMqIp, null); + + String networkType = + attrs.get(NetworkAttributes.NETWORK_TYPE); + assertThat(networkType).isIn("ipv4", "ipv6", null); + + assertNotNull( + attrs.get(NetworkAttributes.NETWORK_PEER_PORT)); + })); + })); + } +} diff --git a/instrumentation/ratpack/ratpack-1.4/javaagent/build.gradle.kts b/instrumentation/ratpack/ratpack-1.4/javaagent/build.gradle.kts index b592c9535280..878a5f6f2685 100644 --- a/instrumentation/ratpack/ratpack-1.4/javaagent/build.gradle.kts +++ b/instrumentation/ratpack/ratpack-1.4/javaagent/build.gradle.kts @@ -45,3 +45,13 @@ if (!(findProperty("testLatestDeps") as Boolean)) { } } } + +tasks { + withType().configureEach { + systemProperty("testLatestDeps", findProperty("testLatestDeps") as Boolean) + } +} + +tasks.withType().configureEach { + jvmArgs("-Dotel.instrumentation.common.experimental.controller-telemetry.enabled=true") +} diff --git a/instrumentation/ratpack/ratpack-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/ratpack/DefaultExecStarterInstrumentation.java b/instrumentation/ratpack/ratpack-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/ratpack/DefaultExecStarterInstrumentation.java index 8c4eebe1dcf8..19757e1604d4 100644 --- a/instrumentation/ratpack/ratpack-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/ratpack/DefaultExecStarterInstrumentation.java +++ b/instrumentation/ratpack/ratpack-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/ratpack/DefaultExecStarterInstrumentation.java @@ -8,6 +8,7 @@ import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.implementsInterface; 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.Scope; @@ -30,9 +31,7 @@ public ElementMatcher typeMatcher() { @Override public void transform(TypeTransformer transformer) { transformer.applyAdviceToMethod( - named("onComplete") - .or(named("onError")) - .or(named("onStart")) + namedOneOf("onComplete", "onError", "onStart") .and(takesArgument(0, named("ratpack.func.Action"))), DefaultExecStarterInstrumentation.class.getName() + "$WrapActionAdvice"); transformer.applyAdviceToMethod( diff --git a/instrumentation/ratpack/ratpack-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/ratpack/RatpackInstrumentationModule.java b/instrumentation/ratpack/ratpack-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/ratpack/RatpackInstrumentationModule.java index 506fb6d43891..ea02d096b3db 100644 --- a/instrumentation/ratpack/ratpack-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/ratpack/RatpackInstrumentationModule.java +++ b/instrumentation/ratpack/ratpack-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/ratpack/RatpackInstrumentationModule.java @@ -10,14 +10,22 @@ 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.internal.ExperimentalInstrumentationModule; import java.util.List; @AutoService(InstrumentationModule.class) -public class RatpackInstrumentationModule extends InstrumentationModule { +public class RatpackInstrumentationModule extends InstrumentationModule + implements ExperimentalInstrumentationModule { public RatpackInstrumentationModule() { super("ratpack", "ratpack-1.4"); } + @Override + public String getModuleGroup() { + // relies on netty + return "netty"; + } + @Override public List typeInstrumentations() { return asList( diff --git a/instrumentation/ratpack/ratpack-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/ratpack/RatpackSingletons.java b/instrumentation/ratpack/ratpack-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/ratpack/RatpackSingletons.java index 4e386bf6ef29..dca161ebffef 100644 --- a/instrumentation/ratpack/ratpack-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/ratpack/RatpackSingletons.java +++ b/instrumentation/ratpack/ratpack-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/ratpack/RatpackSingletons.java @@ -10,8 +10,8 @@ import io.opentelemetry.api.trace.StatusCode; import io.opentelemetry.instrumentation.api.instrumenter.ErrorCauseExtractor; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteHolder; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteSource; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRoute; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRouteSource; import ratpack.handling.Context; public final class RatpackSingletons { @@ -41,8 +41,7 @@ public static String updateServerSpanName( } // update the netty server span name; FILTER is probably the best match for ratpack Handlers - HttpRouteHolder.updateHttpRoute( - otelContext, HttpRouteSource.FILTER, (context, name) -> name, matchedRoute); + HttpServerRoute.update(otelContext, HttpServerRouteSource.SERVER_FILTER, matchedRoute); return matchedRoute; } diff --git a/instrumentation/ratpack/ratpack-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/ratpack/TracingHandler.java b/instrumentation/ratpack/ratpack-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/ratpack/TracingHandler.java index 1cc7d5f82e1a..cdfd48657a56 100644 --- a/instrumentation/ratpack/ratpack-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/ratpack/TracingHandler.java +++ b/instrumentation/ratpack/ratpack-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/ratpack/TracingHandler.java @@ -9,11 +9,10 @@ import static io.opentelemetry.javaagent.instrumentation.ratpack.RatpackSingletons.updateServerSpanName; import static io.opentelemetry.javaagent.instrumentation.ratpack.RatpackSingletons.updateSpanNames; -import io.netty.util.Attribute; import io.opentelemetry.context.Scope; -import io.opentelemetry.instrumentation.netty.v4_1.internal.AttributeKeys; +import io.opentelemetry.instrumentation.netty.v4_1.internal.ServerContext; +import io.opentelemetry.instrumentation.netty.v4_1.internal.ServerContexts; import io.opentelemetry.javaagent.bootstrap.Java8BytecodeBridge; -import java.util.Deque; import ratpack.handling.Context; import ratpack.handling.Handler; @@ -25,17 +24,14 @@ public final class TracingHandler implements Handler { @Override public void handle(Context ctx) { - Attribute> serverContextAttribute = - ctx.getDirectChannelAccess().getChannel().attr(AttributeKeys.SERVER_CONTEXT); - Deque contexts = serverContextAttribute.get(); - io.opentelemetry.context.Context serverSpanContext = - contexts != null ? contexts.peekFirst() : null; + ServerContext serverContext = + ServerContexts.peekFirst(ctx.getDirectChannelAccess().getChannel()); // 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 parentOtelContext = - serverSpanContext != null ? serverSpanContext : Java8BytecodeBridge.currentContext(); + serverContext != null ? serverContext.context() : Java8BytecodeBridge.currentContext(); io.opentelemetry.context.Context callbackContext; if (instrumenter().shouldStart(parentOtelContext, INITIAL_SPAN_NAME)) { diff --git a/instrumentation/ratpack/ratpack-1.4/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/ratpack/RatpackForkedHttpClientTest.java b/instrumentation/ratpack/ratpack-1.4/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/ratpack/RatpackForkedHttpClientTest.java index 84dee00acc4e..c5e646532ea0 100644 --- a/instrumentation/ratpack/ratpack-1.4/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/ratpack/RatpackForkedHttpClientTest.java +++ b/instrumentation/ratpack/ratpack-1.4/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/ratpack/RatpackForkedHttpClientTest.java @@ -9,7 +9,7 @@ import io.opentelemetry.instrumentation.ratpack.client.AbstractRatpackForkedHttpClientTest; import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; import io.opentelemetry.instrumentation.testing.junit.http.HttpClientInstrumentationExtension; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.opentelemetry.semconv.ServerAttributes; import java.net.URI; import java.util.HashSet; import java.util.Set; @@ -24,8 +24,8 @@ class RatpackForkedHttpClientTest extends AbstractRatpackForkedHttpClientTest { protected Set> computeHttpAttributes(URI uri) { Set> attributes = new HashSet<>(super.computeHttpAttributes(uri)); // underlying netty instrumentation does not provide these - attributes.remove(SemanticAttributes.NET_PEER_NAME); - attributes.remove(SemanticAttributes.NET_PEER_PORT); + attributes.remove(ServerAttributes.SERVER_ADDRESS); + attributes.remove(ServerAttributes.SERVER_PORT); return attributes; } } diff --git a/instrumentation/ratpack/ratpack-1.4/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/ratpack/RatpackHttpClientTest.java b/instrumentation/ratpack/ratpack-1.4/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/ratpack/RatpackHttpClientTest.java index 19ec05c26707..c8481f24f7e3 100644 --- a/instrumentation/ratpack/ratpack-1.4/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/ratpack/RatpackHttpClientTest.java +++ b/instrumentation/ratpack/ratpack-1.4/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/ratpack/RatpackHttpClientTest.java @@ -9,7 +9,7 @@ import io.opentelemetry.instrumentation.ratpack.client.AbstractRatpackHttpClientTest; import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; import io.opentelemetry.instrumentation.testing.junit.http.HttpClientInstrumentationExtension; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.opentelemetry.semconv.ServerAttributes; import java.net.URI; import java.util.HashSet; import java.util.Set; @@ -24,8 +24,8 @@ class RatpackHttpClientTest extends AbstractRatpackHttpClientTest { protected Set> computeHttpAttributes(URI uri) { Set> attributes = new HashSet<>(super.computeHttpAttributes(uri)); // underlying netty instrumentation does not provide these - attributes.remove(SemanticAttributes.NET_PEER_NAME); - attributes.remove(SemanticAttributes.NET_PEER_PORT); + attributes.remove(ServerAttributes.SERVER_ADDRESS); + attributes.remove(ServerAttributes.SERVER_PORT); return attributes; } } diff --git a/instrumentation/ratpack/ratpack-1.4/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/ratpack/RatpackPooledHttpClientTest.java b/instrumentation/ratpack/ratpack-1.4/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/ratpack/RatpackPooledHttpClientTest.java index 4ebc83c89b26..bfc27b42eb0f 100644 --- a/instrumentation/ratpack/ratpack-1.4/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/ratpack/RatpackPooledHttpClientTest.java +++ b/instrumentation/ratpack/ratpack-1.4/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/ratpack/RatpackPooledHttpClientTest.java @@ -9,7 +9,7 @@ import io.opentelemetry.instrumentation.ratpack.client.AbstractRatpackPooledHttpClientTest; import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; import io.opentelemetry.instrumentation.testing.junit.http.HttpClientInstrumentationExtension; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.opentelemetry.semconv.ServerAttributes; import java.net.URI; import java.util.HashSet; import java.util.Set; @@ -24,8 +24,8 @@ class RatpackPooledHttpClientTest extends AbstractRatpackPooledHttpClientTest { protected Set> computeHttpAttributes(URI uri) { Set> attributes = new HashSet<>(super.computeHttpAttributes(uri)); // underlying netty instrumentation does not provide these - attributes.remove(SemanticAttributes.NET_PEER_NAME); - attributes.remove(SemanticAttributes.NET_PEER_PORT); + attributes.remove(ServerAttributes.SERVER_ADDRESS); + attributes.remove(ServerAttributes.SERVER_PORT); return attributes; } } diff --git a/instrumentation/ratpack/ratpack-1.4/testing/src/main/groovy/io/opentelemetry/instrumentation/ratpack/server/AbstractRatpackHttpServerTest.groovy b/instrumentation/ratpack/ratpack-1.4/testing/src/main/groovy/io/opentelemetry/instrumentation/ratpack/server/AbstractRatpackHttpServerTest.groovy index 67b73a52c652..61735d098984 100644 --- a/instrumentation/ratpack/ratpack-1.4/testing/src/main/groovy/io/opentelemetry/instrumentation/ratpack/server/AbstractRatpackHttpServerTest.groovy +++ b/instrumentation/ratpack/ratpack-1.4/testing/src/main/groovy/io/opentelemetry/instrumentation/ratpack/server/AbstractRatpackHttpServerTest.groovy @@ -153,7 +153,7 @@ abstract class AbstractRatpackHttpServerTest extends HttpServerTest { if (uri.toString().equals("https://192.0.2.1/")) { - return new ConnectTimeoutException("connection timed out: /192.0.2.1:443"); + return new ConnectTimeoutException( + "connection timed out" + + (Boolean.getBoolean("testLatestDeps") ? " after 2000 ms" : "") + + ": /192.0.2.1:443"); } else if (OS.WINDOWS.isCurrentOs() && uri.toString().equals("http://localhost:61/")) { return new ConnectTimeoutException("connection timed out: localhost/127.0.0.1:61"); } else if (uri.getPath().equals("/read-timeout")) { @@ -145,9 +148,10 @@ protected void configure(HttpClientTestOptions.Builder optionsBuilder) { optionsBuilder.setHttpAttributes(this::computeHttpAttributes); optionsBuilder.disableTestRedirects(); - // these tests will pass, but they don't really test anything since REQUEST is Void optionsBuilder.disableTestReusedRequest(); + + optionsBuilder.spanEndsAfterBody(); } protected Set> computeHttpAttributes(URI uri) { diff --git a/instrumentation/ratpack/ratpack-1.7/library/build.gradle.kts b/instrumentation/ratpack/ratpack-1.7/library/build.gradle.kts index 7386823c002e..60c7754e5258 100644 --- a/instrumentation/ratpack/ratpack-1.7/library/build.gradle.kts +++ b/instrumentation/ratpack/ratpack-1.7/library/build.gradle.kts @@ -14,3 +14,11 @@ dependencies { testImplementation("com.sun.activation:jakarta.activation:1.2.2") } } + +tasks { + withType().configureEach { + // required on jdk17 + jvmArgs("--add-opens=java.base/java.lang=ALL-UNNAMED") + jvmArgs("-XX:+IgnoreUnrecognizedVMOptions") + } +} diff --git a/instrumentation/ratpack/ratpack-1.7/library/src/main/java/io/opentelemetry/instrumentation/ratpack/v1_7/OpenTelemetryHttpClient.java b/instrumentation/ratpack/ratpack-1.7/library/src/main/java/io/opentelemetry/instrumentation/ratpack/v1_7/OpenTelemetryHttpClient.java index ee5c4fc7791d..aea5fc96faf6 100644 --- a/instrumentation/ratpack/ratpack-1.7/library/src/main/java/io/opentelemetry/instrumentation/ratpack/v1_7/OpenTelemetryHttpClient.java +++ b/instrumentation/ratpack/ratpack-1.7/library/src/main/java/io/opentelemetry/instrumentation/ratpack/v1_7/OpenTelemetryHttpClient.java @@ -9,7 +9,7 @@ import io.opentelemetry.context.Context; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; import io.opentelemetry.instrumentation.ratpack.v1_7.internal.ContextHolder; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.opentelemetry.semconv.HttpAttributes; import ratpack.exec.Execution; import ratpack.http.client.HttpClient; import ratpack.http.client.HttpResponse; @@ -37,7 +37,7 @@ public HttpClient instrument(HttpClient httpClient) throws Exception { Context otelCtx = instrumenter.start(parentOtelCtx, requestSpec); Span span = Span.fromContext(otelCtx); String path = requestSpec.getUri().getPath(); - span.setAttribute(SemanticAttributes.HTTP_ROUTE, path); + span.setAttribute(HttpAttributes.HTTP_ROUTE, path); Execution.current().add(new ContextHolder(otelCtx, requestSpec)); }); diff --git a/instrumentation/ratpack/ratpack-1.7/library/src/main/java/io/opentelemetry/instrumentation/ratpack/v1_7/OpenTelemetryServerHandler.java b/instrumentation/ratpack/ratpack-1.7/library/src/main/java/io/opentelemetry/instrumentation/ratpack/v1_7/OpenTelemetryServerHandler.java index a4be09605aeb..7dedf77ef9ab 100644 --- a/instrumentation/ratpack/ratpack-1.7/library/src/main/java/io/opentelemetry/instrumentation/ratpack/v1_7/OpenTelemetryServerHandler.java +++ b/instrumentation/ratpack/ratpack-1.7/library/src/main/java/io/opentelemetry/instrumentation/ratpack/v1_7/OpenTelemetryServerHandler.java @@ -5,11 +5,11 @@ package io.opentelemetry.instrumentation.ratpack.v1_7; -import static io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteSource.CONTROLLER; +import static io.opentelemetry.instrumentation.api.semconv.http.HttpServerRouteSource.CONTROLLER; import io.opentelemetry.context.Scope; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteHolder; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRoute; import ratpack.error.ServerErrorHandler; import ratpack.handling.Context; import ratpack.handling.Handler; @@ -39,7 +39,7 @@ public void handle(Context context) { context.onClose( outcome -> { // Route not available in beginning of request so handle it manually here. - HttpRouteHolder.updateHttpRoute( + HttpServerRoute.update( otelCtx, CONTROLLER, OpenTelemetryServerHandler::getRoute, context); Throwable error = diff --git a/instrumentation/ratpack/ratpack-1.7/library/src/main/java/io/opentelemetry/instrumentation/ratpack/v1_7/RatpackHttpAttributesGetter.java b/instrumentation/ratpack/ratpack-1.7/library/src/main/java/io/opentelemetry/instrumentation/ratpack/v1_7/RatpackHttpAttributesGetter.java index f580816cb4e5..95a3d4f888c5 100644 --- a/instrumentation/ratpack/ratpack-1.7/library/src/main/java/io/opentelemetry/instrumentation/ratpack/v1_7/RatpackHttpAttributesGetter.java +++ b/instrumentation/ratpack/ratpack-1.7/library/src/main/java/io/opentelemetry/instrumentation/ratpack/v1_7/RatpackHttpAttributesGetter.java @@ -5,7 +5,7 @@ package io.opentelemetry.instrumentation.ratpack.v1_7; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpServerAttributesGetter; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerAttributesGetter; import java.util.List; import javax.annotation.Nullable; import ratpack.handling.Context; @@ -62,4 +62,29 @@ public Integer getHttpResponseStatusCode( public List getHttpResponseHeader(Request request, Response response, String name) { return response.getHeaders().getAll(name); } + + @Nullable + @Override + public String getNetworkProtocolName(Request request, @Nullable Response response) { + String protocol = request.getProtocol(); + if (protocol.startsWith("HTTP/")) { + return "http"; + } + return null; + } + + @Nullable + @Override + public String getNetworkProtocolVersion(Request request, @Nullable Response response) { + String protocol = request.getProtocol(); + if (protocol.startsWith("HTTP/")) { + return protocol.substring("HTTP/".length()); + } + return null; + } + + @Override + public Integer getNetworkPeerPort(Request request, @Nullable Response response) { + return request.getRemoteAddress().getPort(); + } } diff --git a/instrumentation/ratpack/ratpack-1.7/library/src/main/java/io/opentelemetry/instrumentation/ratpack/v1_7/RatpackHttpClientAttributesGetter.java b/instrumentation/ratpack/ratpack-1.7/library/src/main/java/io/opentelemetry/instrumentation/ratpack/v1_7/RatpackHttpClientAttributesGetter.java index 251f52a5ffbc..6aa8f0227fa5 100644 --- a/instrumentation/ratpack/ratpack-1.7/library/src/main/java/io/opentelemetry/instrumentation/ratpack/v1_7/RatpackHttpClientAttributesGetter.java +++ b/instrumentation/ratpack/ratpack-1.7/library/src/main/java/io/opentelemetry/instrumentation/ratpack/v1_7/RatpackHttpClientAttributesGetter.java @@ -5,7 +5,7 @@ package io.opentelemetry.instrumentation.ratpack.v1_7; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpClientAttributesGetter; +import io.opentelemetry.instrumentation.api.semconv.http.HttpClientAttributesGetter; import java.util.List; import javax.annotation.Nullable; import ratpack.http.client.HttpResponse; @@ -43,4 +43,15 @@ public List getHttpResponseHeader( RequestSpec requestSpec, HttpResponse httpResponse, String name) { return httpResponse.getHeaders().getAll(name); } + + @Override + @Nullable + public String getServerAddress(RequestSpec request) { + return request.getUri().getHost(); + } + + @Override + public Integer getServerPort(RequestSpec request) { + return request.getUri().getPort(); + } } diff --git a/instrumentation/ratpack/ratpack-1.7/library/src/main/java/io/opentelemetry/instrumentation/ratpack/v1_7/RatpackTelemetryBuilder.java b/instrumentation/ratpack/ratpack-1.7/library/src/main/java/io/opentelemetry/instrumentation/ratpack/v1_7/RatpackTelemetryBuilder.java index 9d75c4aed6e5..1542d5070dab 100644 --- a/instrumentation/ratpack/ratpack-1.7/library/src/main/java/io/opentelemetry/instrumentation/ratpack/v1_7/RatpackTelemetryBuilder.java +++ b/instrumentation/ratpack/ratpack-1.7/library/src/main/java/io/opentelemetry/instrumentation/ratpack/v1_7/RatpackTelemetryBuilder.java @@ -7,20 +7,28 @@ import com.google.errorprone.annotations.CanIgnoreReturnValue; import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.instrumentation.api.incubator.semconv.http.HttpClientExperimentalMetrics; +import io.opentelemetry.instrumentation.api.incubator.semconv.http.HttpExperimentalAttributesExtractor; +import io.opentelemetry.instrumentation.api.incubator.semconv.http.HttpServerExperimentalMetrics; import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpClientAttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpClientAttributesExtractorBuilder; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteHolder; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpServerAttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpServerAttributesExtractorBuilder; -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 io.opentelemetry.instrumentation.ratpack.v1_7.internal.RatpackNetClientAttributesGetter; -import io.opentelemetry.instrumentation.ratpack.v1_7.internal.RatpackNetServerAttributesGetter; +import io.opentelemetry.instrumentation.api.instrumenter.InstrumenterBuilder; +import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor; +import io.opentelemetry.instrumentation.api.semconv.http.HttpClientAttributesExtractor; +import io.opentelemetry.instrumentation.api.semconv.http.HttpClientAttributesExtractorBuilder; +import io.opentelemetry.instrumentation.api.semconv.http.HttpClientMetrics; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerAttributesExtractor; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerAttributesExtractorBuilder; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerMetrics; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRoute; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRouteBuilder; +import io.opentelemetry.instrumentation.api.semconv.http.HttpSpanNameExtractor; +import io.opentelemetry.instrumentation.api.semconv.http.HttpSpanNameExtractorBuilder; +import io.opentelemetry.instrumentation.api.semconv.http.HttpSpanStatusExtractor; import java.util.ArrayList; import java.util.List; +import java.util.Set; +import java.util.function.Function; import ratpack.http.Request; import ratpack.http.Response; import ratpack.http.client.HttpResponse; @@ -37,16 +45,30 @@ public final class RatpackTelemetryBuilder { new ArrayList<>(); private final HttpClientAttributesExtractorBuilder httpClientAttributesExtractorBuilder = - HttpClientAttributesExtractor.builder( - RatpackHttpClientAttributesGetter.INSTANCE, new RatpackNetClientAttributesGetter()); + HttpClientAttributesExtractor.builder(RatpackHttpClientAttributesGetter.INSTANCE); private final HttpServerAttributesExtractorBuilder httpServerAttributesExtractorBuilder = - HttpServerAttributesExtractor.builder( - RatpackHttpAttributesGetter.INSTANCE, new RatpackNetServerAttributesGetter()); + HttpServerAttributesExtractor.builder(RatpackHttpAttributesGetter.INSTANCE); + + private final HttpSpanNameExtractorBuilder httpClientSpanNameExtractorBuilder = + HttpSpanNameExtractor.builder(RatpackHttpClientAttributesGetter.INSTANCE); + private final HttpSpanNameExtractorBuilder httpServerSpanNameExtractorBuilder = + HttpSpanNameExtractor.builder(RatpackHttpAttributesGetter.INSTANCE); + + private Function, ? extends SpanNameExtractor> + clientSpanNameExtractorTransformer = Function.identity(); + private Function, ? extends SpanNameExtractor> + serverSpanNameExtractorTransformer = Function.identity(); + + private final HttpServerRouteBuilder httpServerRouteBuilder = + HttpServerRoute.builder(RatpackHttpAttributesGetter.INSTANCE); private final List> additionalHttpClientExtractors = new ArrayList<>(); + private boolean emitExperimentalHttpClientMetrics = false; + private boolean emitExperimentalHttpServerMetrics = false; + RatpackTelemetryBuilder(OpenTelemetry openTelemetry) { this.openTelemetry = openTelemetry; } @@ -113,32 +135,117 @@ public RatpackTelemetryBuilder setCapturedClientResponseHeaders(List res return this; } + /** + * Configures the instrumentation to recognize an alternative set of HTTP request methods. + * + *

    By default, this instrumentation defines "known" methods as the ones listed in RFC9110 and the PATCH + * method defined in RFC5789. + * + *

    Note: calling this method overrides the default known method sets completely; it does + * not supplement it. + * + * @param knownMethods A set of recognized HTTP request methods. + * @see HttpClientAttributesExtractorBuilder#setKnownMethods(Set) + * @see HttpServerAttributesExtractorBuilder#setKnownMethods(Set) + */ + @CanIgnoreReturnValue + public RatpackTelemetryBuilder setKnownMethods(Set knownMethods) { + httpClientAttributesExtractorBuilder.setKnownMethods(knownMethods); + httpServerAttributesExtractorBuilder.setKnownMethods(knownMethods); + httpClientSpanNameExtractorBuilder.setKnownMethods(knownMethods); + httpServerSpanNameExtractorBuilder.setKnownMethods(knownMethods); + httpServerRouteBuilder.setKnownMethods(knownMethods); + return this; + } + + /** + * Configures the instrumentation to emit experimental HTTP client metrics. + * + * @param emitExperimentalHttpClientMetrics {@code true} if the experimental HTTP client metrics + * are to be emitted. + */ + @CanIgnoreReturnValue + public RatpackTelemetryBuilder setEmitExperimentalHttpClientMetrics( + boolean emitExperimentalHttpClientMetrics) { + this.emitExperimentalHttpClientMetrics = emitExperimentalHttpClientMetrics; + return this; + } + + /** + * Configures the instrumentation to emit experimental HTTP server metrics. + * + * @param emitExperimentalHttpServerMetrics {@code true} if the experimental HTTP server metrics + * are to be emitted. + */ + @CanIgnoreReturnValue + public RatpackTelemetryBuilder setEmitExperimentalHttpServerMetrics( + boolean emitExperimentalHttpServerMetrics) { + this.emitExperimentalHttpServerMetrics = emitExperimentalHttpServerMetrics; + return this; + } + + /** Sets custom client {@link SpanNameExtractor} via transform function. */ + @CanIgnoreReturnValue + public RatpackTelemetryBuilder setClientSpanNameExtractor( + Function, ? extends SpanNameExtractor> + clientSpanNameExtractor) { + this.clientSpanNameExtractorTransformer = clientSpanNameExtractor; + return this; + } + + /** Sets custom server {@link SpanNameExtractor} via transform function. */ + @CanIgnoreReturnValue + public RatpackTelemetryBuilder setServerSpanNameExtractor( + Function, ? extends SpanNameExtractor> + serverSpanNameExtractor) { + this.serverSpanNameExtractorTransformer = serverSpanNameExtractor; + return this; + } + /** Returns a new {@link RatpackTelemetry} with the configuration of this builder. */ public RatpackTelemetry build() { + return new RatpackTelemetry(buildServerInstrumenter(), httpClientInstrumenter()); + } + + private Instrumenter buildServerInstrumenter() { RatpackHttpAttributesGetter httpAttributes = RatpackHttpAttributesGetter.INSTANCE; + SpanNameExtractor spanNameExtractor = + serverSpanNameExtractorTransformer.apply(httpServerSpanNameExtractorBuilder.build()); - Instrumenter instrumenter = + InstrumenterBuilder builder = Instrumenter.builder( - openTelemetry, INSTRUMENTATION_NAME, HttpSpanNameExtractor.create(httpAttributes)) + openTelemetry, INSTRUMENTATION_NAME, spanNameExtractor) .setSpanStatusExtractor(HttpSpanStatusExtractor.create(httpAttributes)) .addAttributesExtractor(httpServerAttributesExtractorBuilder.build()) .addAttributesExtractors(additionalExtractors) .addOperationMetrics(HttpServerMetrics.get()) - .addContextCustomizer(HttpRouteHolder.create(httpAttributes)) - .buildServerInstrumenter(RatpackGetter.INSTANCE); - - return new RatpackTelemetry(instrumenter, httpClientInstrumenter()); + .addContextCustomizer(httpServerRouteBuilder.build()); + if (emitExperimentalHttpServerMetrics) { + builder + .addAttributesExtractor(HttpExperimentalAttributesExtractor.create(httpAttributes)) + .addOperationMetrics(HttpServerExperimentalMetrics.get()); + } + return builder.buildServerInstrumenter(RatpackGetter.INSTANCE); } private Instrumenter httpClientInstrumenter() { RatpackHttpClientAttributesGetter httpAttributes = RatpackHttpClientAttributesGetter.INSTANCE; + SpanNameExtractor spanNameExtractor = + clientSpanNameExtractorTransformer.apply(httpClientSpanNameExtractorBuilder.build()); - return Instrumenter.builder( - openTelemetry, INSTRUMENTATION_NAME, HttpSpanNameExtractor.create(httpAttributes)) - .setSpanStatusExtractor(HttpSpanStatusExtractor.create(httpAttributes)) - .addAttributesExtractor(httpClientAttributesExtractorBuilder.build()) - .addAttributesExtractors(additionalHttpClientExtractors) - .addOperationMetrics(HttpServerMetrics.get()) - .buildClientInstrumenter(RequestHeaderSetter.INSTANCE); + InstrumenterBuilder builder = + Instrumenter.builder( + openTelemetry, INSTRUMENTATION_NAME, spanNameExtractor) + .setSpanStatusExtractor(HttpSpanStatusExtractor.create(httpAttributes)) + .addAttributesExtractor(httpClientAttributesExtractorBuilder.build()) + .addAttributesExtractors(additionalHttpClientExtractors) + .addOperationMetrics(HttpClientMetrics.get()); + if (emitExperimentalHttpClientMetrics) { + builder + .addAttributesExtractor(HttpExperimentalAttributesExtractor.create(httpAttributes)) + .addOperationMetrics(HttpClientExperimentalMetrics.get()); + } + return builder.buildClientInstrumenter(RequestHeaderSetter.INSTANCE); } } diff --git a/instrumentation/ratpack/ratpack-1.7/library/src/main/java/io/opentelemetry/instrumentation/ratpack/v1_7/internal/RatpackNetClientAttributesGetter.java b/instrumentation/ratpack/ratpack-1.7/library/src/main/java/io/opentelemetry/instrumentation/ratpack/v1_7/internal/RatpackNetClientAttributesGetter.java deleted file mode 100644 index c207e27447dd..000000000000 --- a/instrumentation/ratpack/ratpack-1.7/library/src/main/java/io/opentelemetry/instrumentation/ratpack/v1_7/internal/RatpackNetClientAttributesGetter.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.ratpack.v1_7.internal; - -import io.opentelemetry.instrumentation.api.instrumenter.net.NetClientAttributesGetter; -import javax.annotation.Nullable; -import ratpack.http.client.HttpResponse; -import ratpack.http.client.RequestSpec; - -/** - * This class is internal and is hence not for public use. Its APIs are unstable and can change at - * any time. - */ -public final class RatpackNetClientAttributesGetter - implements NetClientAttributesGetter { - - @Override - @Nullable - public String getServerAddress(RequestSpec request) { - return request.getUri().getHost(); - } - - @Override - public Integer getServerPort(RequestSpec request) { - return request.getUri().getPort(); - } -} diff --git a/instrumentation/ratpack/ratpack-1.7/library/src/main/java/io/opentelemetry/instrumentation/ratpack/v1_7/internal/RatpackNetServerAttributesGetter.java b/instrumentation/ratpack/ratpack-1.7/library/src/main/java/io/opentelemetry/instrumentation/ratpack/v1_7/internal/RatpackNetServerAttributesGetter.java deleted file mode 100644 index 33e588e590fe..000000000000 --- a/instrumentation/ratpack/ratpack-1.7/library/src/main/java/io/opentelemetry/instrumentation/ratpack/v1_7/internal/RatpackNetServerAttributesGetter.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.ratpack.v1_7.internal; - -import io.opentelemetry.instrumentation.api.instrumenter.net.NetServerAttributesGetter; -import javax.annotation.Nullable; -import ratpack.handling.Context; -import ratpack.http.Request; -import ratpack.http.Response; -import ratpack.server.PublicAddress; - -/** - * This class is internal and is hence not for public use. Its APIs are unstable and can change at - * any time. - */ -public final class RatpackNetServerAttributesGetter - implements NetServerAttributesGetter { - - @Nullable - @Override - public String getNetworkProtocolName(Request request, @Nullable Response response) { - String protocol = request.getProtocol(); - if (protocol.startsWith("HTTP/")) { - return "http"; - } - return null; - } - - @Nullable - @Override - public String getNetworkProtocolVersion(Request request, @Nullable Response response) { - String protocol = request.getProtocol(); - if (protocol.startsWith("HTTP/")) { - return protocol.substring("HTTP/".length()); - } - return null; - } - - @Nullable - @Override - public String getServerAddress(Request request) { - PublicAddress publicAddress = getPublicAddress(request); - return publicAddress == null ? null : publicAddress.get().getHost(); - } - - @Nullable - @Override - public Integer getServerPort(Request request) { - PublicAddress publicAddress = getPublicAddress(request); - return publicAddress == null ? null : publicAddress.get().getPort(); - } - - private static PublicAddress getPublicAddress(Request request) { - Context ratpackContext = request.get(Context.class); - if (ratpackContext == null) { - return null; - } - return ratpackContext.get(PublicAddress.class); - } - - @Override - public Integer getClientSocketPort(Request request, @Nullable Response response) { - return request.getRemoteAddress().getPort(); - } -} diff --git a/instrumentation/ratpack/ratpack-1.7/library/src/test/groovy/io/opentelemetry/instrumentation/ratpack/v1_7/client/InstrumentedHttpClientTest.groovy b/instrumentation/ratpack/ratpack-1.7/library/src/test/groovy/io/opentelemetry/instrumentation/ratpack/v1_7/client/InstrumentedHttpClientTest.groovy index dbf11883d7ed..7a6de6f3d980 100644 --- a/instrumentation/ratpack/ratpack-1.7/library/src/test/groovy/io/opentelemetry/instrumentation/ratpack/v1_7/client/InstrumentedHttpClientTest.groovy +++ b/instrumentation/ratpack/ratpack-1.7/library/src/test/groovy/io/opentelemetry/instrumentation/ratpack/v1_7/client/InstrumentedHttpClientTest.groovy @@ -16,7 +16,7 @@ import io.opentelemetry.sdk.OpenTelemetrySdk import io.opentelemetry.sdk.testing.exporter.InMemorySpanExporter import io.opentelemetry.sdk.trace.SdkTracerProvider import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import io.opentelemetry.semconv.UrlAttributes import ratpack.exec.Execution import ratpack.exec.Promise import ratpack.func.Action @@ -34,9 +34,9 @@ import java.util.concurrent.TimeUnit import static io.opentelemetry.api.trace.SpanKind.CLIENT import static io.opentelemetry.api.trace.SpanKind.SERVER -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.HTTP_METHOD -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.HTTP_ROUTE -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.HTTP_STATUS_CODE +import static io.opentelemetry.semconv.HttpAttributes.HTTP_REQUEST_METHOD +import static io.opentelemetry.semconv.HttpAttributes.HTTP_ROUTE +import static io.opentelemetry.semconv.HttpAttributes.HTTP_RESPONSE_STATUS_CODE class InstrumentedHttpClientTest extends Specification { @@ -86,35 +86,35 @@ class InstrumentedHttpClientTest extends Specification { } app.test { httpClient -> - "bar" == httpClient.get("foo").body.text - } - - new PollingConditions().eventually { - def spanData = spanExporter.finishedSpanItems.find { it.name == "GET /foo" } - def spanClientData = spanExporter.finishedSpanItems.find { it.name == "GET" && it.kind == CLIENT } - def spanDataApi = spanExporter.finishedSpanItems.find { it.name == "GET /bar" && it.kind == SERVER } - - spanData.traceId == spanClientData.traceId - spanData.traceId == spanDataApi.traceId - - spanData.kind == SERVER - spanClientData.kind == CLIENT - def atts = spanClientData.attributes.asMap() - atts[HTTP_ROUTE] == "/bar" - atts[HTTP_METHOD] == "GET" - atts[HTTP_STATUS_CODE] == 200L - - def attributes = spanData.attributes.asMap() - attributes[HTTP_ROUTE] == "/foo" - attributes[SemanticAttributes.HTTP_TARGET] == "/foo" - attributes[HTTP_METHOD] == "GET" - attributes[HTTP_STATUS_CODE] == 200L - - def attsApi = spanDataApi.attributes.asMap() - attsApi[HTTP_ROUTE] == "/bar" - attsApi[SemanticAttributes.HTTP_TARGET] == "/bar" - attsApi[HTTP_METHOD] == "GET" - attsApi[HTTP_STATUS_CODE] == 200L + assert "bar" == httpClient.get("foo").body.text + + new PollingConditions().eventually { + def spanData = spanExporter.finishedSpanItems.find { it.name == "GET /foo" } + def spanClientData = spanExporter.finishedSpanItems.find { it.name == "GET" && it.kind == CLIENT } + def spanDataApi = spanExporter.finishedSpanItems.find { it.name == "GET /bar" && it.kind == SERVER } + + spanData.traceId == spanClientData.traceId + spanData.traceId == spanDataApi.traceId + + spanData.kind == SERVER + spanClientData.kind == CLIENT + def atts = spanClientData.attributes.asMap() + atts[HTTP_ROUTE] == "/bar" + atts[HTTP_REQUEST_METHOD] == "GET" + atts[HTTP_RESPONSE_STATUS_CODE] == 200L + + def attributes = spanData.attributes.asMap() + attributes[HTTP_ROUTE] == "/foo" + attributes[UrlAttributes.URL_PATH] == "/foo" + attributes[HTTP_REQUEST_METHOD] == "GET" + attributes[HTTP_RESPONSE_STATUS_CODE] == 200L + + def attsApi = spanDataApi.attributes.asMap() + attsApi[HTTP_ROUTE] == "/bar" + attsApi[UrlAttributes.URL_PATH] == "/bar" + attsApi[HTTP_REQUEST_METHOD] == "GET" + attsApi[HTTP_RESPONSE_STATUS_CODE] == 200L + } } } @@ -148,38 +148,38 @@ class InstrumentedHttpClientTest extends Specification { } app.test { httpClient -> - "hello" == httpClient.get("path-name").body.text + assert "hello" == httpClient.get("path-name").body.text latch.await(1, TimeUnit.SECONDS) - } - new PollingConditions().eventually { - spanExporter.finishedSpanItems.size() == 3 - def spanData = spanExporter.finishedSpanItems.find { spanData -> spanData.name == "GET /path-name" } - def spanClientData1 = spanExporter.finishedSpanItems.find { s -> s.name == "GET" && s.attributes.asMap()[HTTP_ROUTE] == "/foo" } - def spanClientData2 = spanExporter.finishedSpanItems.find { s -> s.name == "GET" && s.attributes.asMap()[HTTP_ROUTE] == "/bar" } - - spanData.traceId == spanClientData1.traceId - spanData.traceId == spanClientData2.traceId - - spanData.kind == SERVER - - spanClientData1.kind == CLIENT - def atts = spanClientData1.attributes.asMap() - atts[HTTP_ROUTE] == "/foo" - atts[HTTP_METHOD] == "GET" - atts[HTTP_STATUS_CODE] == 200L - - spanClientData2.kind == CLIENT - def atts2 = spanClientData2.attributes.asMap() - atts2[HTTP_ROUTE] == "/bar" - atts2[HTTP_METHOD] == "GET" - atts2[HTTP_STATUS_CODE] == 200L - - def attributes = spanData.attributes.asMap() - attributes[HTTP_ROUTE] == "/path-name" - attributes[SemanticAttributes.HTTP_TARGET] == "/path-name" - attributes[HTTP_METHOD] == "GET" - attributes[HTTP_STATUS_CODE] == 200L + new PollingConditions().eventually { + spanExporter.finishedSpanItems.size() == 3 + def spanData = spanExporter.finishedSpanItems.find { spanData -> spanData.name == "GET /path-name" } + def spanClientData1 = spanExporter.finishedSpanItems.find { s -> s.name == "GET" && s.attributes.asMap()[HTTP_ROUTE] == "/foo" } + def spanClientData2 = spanExporter.finishedSpanItems.find { s -> s.name == "GET" && s.attributes.asMap()[HTTP_ROUTE] == "/bar" } + + spanData.traceId == spanClientData1.traceId + spanData.traceId == spanClientData2.traceId + + spanData.kind == SERVER + + spanClientData1.kind == CLIENT + def atts = spanClientData1.attributes.asMap() + atts[HTTP_ROUTE] == "/foo" + atts[HTTP_REQUEST_METHOD] == "GET" + atts[HTTP_RESPONSE_STATUS_CODE] == 200L + + spanClientData2.kind == CLIENT + def atts2 = spanClientData2.attributes.asMap() + atts2[HTTP_ROUTE] == "/bar" + atts2[HTTP_REQUEST_METHOD] == "GET" + atts2[HTTP_RESPONSE_STATUS_CODE] == 200L + + def attributes = spanData.attributes.asMap() + attributes[HTTP_ROUTE] == "/path-name" + attributes[UrlAttributes.URL_PATH] == "/path-name" + attributes[HTTP_REQUEST_METHOD] == "GET" + attributes[HTTP_RESPONSE_STATUS_CODE] == 200L + } } } @@ -214,28 +214,30 @@ class InstrumentedHttpClientTest extends Specification { } } - app.test { httpClient -> "error" == httpClient.get("path-name").body.text } - - new PollingConditions().eventually { - def spanData = spanExporter.finishedSpanItems.find { it.name == "GET /path-name" } - def spanClientData = spanExporter.finishedSpanItems.find { it.name == "GET" } - - spanData.traceId == spanClientData.traceId - - spanData.kind == SERVER - spanClientData.kind == CLIENT - def atts = spanClientData.attributes.asMap() - atts[HTTP_ROUTE] == "/foo" - atts[HTTP_METHOD] == "GET" - atts[HTTP_STATUS_CODE] == null - spanClientData.status.statusCode == StatusCode.ERROR - spanClientData.events.first().name == "exception" - - def attributes = spanData.attributes.asMap() - attributes[HTTP_ROUTE] == "/path-name" - attributes[SemanticAttributes.HTTP_TARGET] == "/path-name" - attributes[HTTP_METHOD] == "GET" - attributes[HTTP_STATUS_CODE] == 200L + app.test { httpClient -> + assert "error" == httpClient.get("path-name").body.text + + new PollingConditions().eventually { + def spanData = spanExporter.finishedSpanItems.find { it.name == "GET /path-name" } + def spanClientData = spanExporter.finishedSpanItems.find { it.name == "GET" } + + spanData.traceId == spanClientData.traceId + + spanData.kind == SERVER + spanClientData.kind == CLIENT + def atts = spanClientData.attributes.asMap() + atts[HTTP_ROUTE] == "/foo" + atts[HTTP_REQUEST_METHOD] == "GET" + atts[HTTP_RESPONSE_STATUS_CODE] == null + spanClientData.status.statusCode == StatusCode.ERROR + spanClientData.events.first().name == "exception" + + def attributes = spanData.attributes.asMap() + attributes[HTTP_ROUTE] == "/path-name" + attributes[UrlAttributes.URL_PATH] == "/path-name" + attributes[HTTP_REQUEST_METHOD] == "GET" + attributes[HTTP_RESPONSE_STATUS_CODE] == 200L + } } } @@ -309,15 +311,15 @@ class InstrumentedHttpClientTest extends Specification { class BarService implements Service { private final String url private final CountDownLatch latch - private final OpenTelemetry opentelemetry + private final OpenTelemetry openTelemetry - BarService(CountDownLatch latch, String url, OpenTelemetry opentelemetry) { + BarService(CountDownLatch latch, String url, OpenTelemetry openTelemetry) { this.latch = latch this.url = url - this.opentelemetry = opentelemetry + this.openTelemetry = openTelemetry } - private Tracer tracer = opentelemetry.tracerProvider.tracerBuilder("testing").build() + private Tracer tracer = openTelemetry.tracerProvider.tracerBuilder("testing").build() void onStart(StartEvent event) { def parentContext = Context.current() @@ -343,15 +345,15 @@ class BarService implements Service { class BarForkService implements Service { private final String url private final CountDownLatch latch - private final OpenTelemetry opentelemetry + private final OpenTelemetry openTelemetry - BarForkService(CountDownLatch latch, String url, OpenTelemetry opentelemetry) { + BarForkService(CountDownLatch latch, String url, OpenTelemetry openTelemetry) { this.latch = latch this.url = url - this.opentelemetry = opentelemetry + this.openTelemetry = openTelemetry } - private Tracer tracer = opentelemetry.tracerProvider.tracerBuilder("testing").build() + private Tracer tracer = openTelemetry.tracerProvider.tracerBuilder("testing").build() void onStart(StartEvent event) { Execution.fork().start { diff --git a/instrumentation/ratpack/ratpack-1.7/library/src/test/groovy/io/opentelemetry/instrumentation/ratpack/v1_7/server/RatpackServerApplicationTest.groovy b/instrumentation/ratpack/ratpack-1.7/library/src/test/groovy/io/opentelemetry/instrumentation/ratpack/v1_7/server/RatpackServerApplicationTest.groovy index 8d696fdcc928..ced2052acce7 100644 --- a/instrumentation/ratpack/ratpack-1.7/library/src/test/groovy/io/opentelemetry/instrumentation/ratpack/v1_7/server/RatpackServerApplicationTest.groovy +++ b/instrumentation/ratpack/ratpack-1.7/library/src/test/groovy/io/opentelemetry/instrumentation/ratpack/v1_7/server/RatpackServerApplicationTest.groovy @@ -28,10 +28,10 @@ import spock.util.concurrent.PollingConditions import javax.inject.Singleton -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.HTTP_METHOD -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.HTTP_ROUTE -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.HTTP_STATUS_CODE -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.HTTP_TARGET +import static io.opentelemetry.semconv.HttpAttributes.HTTP_REQUEST_METHOD +import static io.opentelemetry.semconv.HttpAttributes.HTTP_RESPONSE_STATUS_CODE +import static io.opentelemetry.semconv.HttpAttributes.HTTP_ROUTE +import static io.opentelemetry.semconv.UrlAttributes.URL_PATH class RatpackServerApplicationTest extends Specification { @@ -48,9 +48,9 @@ class RatpackServerApplicationTest extends Specification { spanData.kind == SpanKind.SERVER attributes[HTTP_ROUTE] == "/foo" - attributes[HTTP_TARGET] == "/foo" - attributes[HTTP_METHOD] == "GET" - attributes[HTTP_STATUS_CODE] == 200L + attributes[URL_PATH] == "/foo" + attributes[HTTP_REQUEST_METHOD] == "GET" + attributes[HTTP_RESPONSE_STATUS_CODE] == 200L } } } @@ -69,15 +69,15 @@ class RatpackServerApplicationTest extends Specification { spanData.kind == SpanKind.SERVER attributes[HTTP_ROUTE] == "/bar" - attributes[HTTP_TARGET] == "/bar" - attributes[HTTP_METHOD] == "GET" - attributes[HTTP_STATUS_CODE] == 200L + attributes[URL_PATH] == "/bar" + attributes[HTTP_REQUEST_METHOD] == "GET" + attributes[HTTP_RESPONSE_STATUS_CODE] == 200L spanDataClient.kind == SpanKind.CLIENT def attributesClient = spanDataClient.attributes.asMap() attributesClient[HTTP_ROUTE] == "/other" - attributesClient[HTTP_METHOD] == "GET" - attributesClient[HTTP_STATUS_CODE] == 200L + attributesClient[HTTP_REQUEST_METHOD] == "GET" + attributesClient[HTTP_RESPONSE_STATUS_CODE] == 200L } } } diff --git a/instrumentation/ratpack/ratpack-1.7/library/src/test/groovy/io/opentelemetry/instrumentation/ratpack/v1_7/server/RatpackServerTest.groovy b/instrumentation/ratpack/ratpack-1.7/library/src/test/groovy/io/opentelemetry/instrumentation/ratpack/v1_7/server/RatpackServerTest.groovy index bea98619c0ef..f04d97f458ee 100644 --- a/instrumentation/ratpack/ratpack-1.7/library/src/test/groovy/io/opentelemetry/instrumentation/ratpack/v1_7/server/RatpackServerTest.groovy +++ b/instrumentation/ratpack/ratpack-1.7/library/src/test/groovy/io/opentelemetry/instrumentation/ratpack/v1_7/server/RatpackServerTest.groovy @@ -13,7 +13,8 @@ import io.opentelemetry.sdk.OpenTelemetrySdk import io.opentelemetry.sdk.testing.exporter.InMemorySpanExporter import io.opentelemetry.sdk.trace.SdkTracerProvider import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import io.opentelemetry.semconv.HttpAttributes +import io.opentelemetry.semconv.UrlAttributes import ratpack.exec.Blocking import ratpack.registry.Registry import ratpack.test.embed.EmbeddedApp @@ -55,10 +56,10 @@ class RatpackServerTest extends Specification { def attributes = spanData.attributes.asMap() spanData.kind == SpanKind.SERVER - attributes[SemanticAttributes.HTTP_ROUTE] == "/foo" - attributes[SemanticAttributes.HTTP_TARGET] == "/foo" - attributes[SemanticAttributes.HTTP_METHOD] == "GET" - attributes[SemanticAttributes.HTTP_STATUS_CODE] == 200L + attributes[HttpAttributes.HTTP_ROUTE] == "/foo" + attributes[UrlAttributes.URL_PATH] == "/foo" + attributes[HttpAttributes.HTTP_REQUEST_METHOD] == "GET" + attributes[HttpAttributes.HTTP_RESPONSE_STATUS_CODE] == 200L } } } @@ -94,10 +95,10 @@ class RatpackServerTest extends Specification { spanDataChild.events.any { it.name == "an-event" } def attributes = spanData.attributes.asMap() - attributes[SemanticAttributes.HTTP_ROUTE] == "/foo" - attributes[SemanticAttributes.HTTP_TARGET] == "/foo" - attributes[SemanticAttributes.HTTP_METHOD] == "GET" - attributes[SemanticAttributes.HTTP_STATUS_CODE] == 200L + attributes[HttpAttributes.HTTP_ROUTE] == "/foo" + attributes[UrlAttributes.URL_PATH] == "/foo" + attributes[HttpAttributes.HTTP_REQUEST_METHOD] == "GET" + attributes[HttpAttributes.HTTP_RESPONSE_STATUS_CODE] == 200L } } } @@ -152,10 +153,10 @@ class RatpackServerTest extends Specification { spanDataChild2.events.any { it.name == "an-event" } def attributes = spanData.attributes.asMap() - attributes[SemanticAttributes.HTTP_ROUTE] == "/foo" - attributes[SemanticAttributes.HTTP_TARGET] == "/foo" - attributes[SemanticAttributes.HTTP_METHOD] == "GET" - attributes[SemanticAttributes.HTTP_STATUS_CODE] == 200L + attributes[HttpAttributes.HTTP_ROUTE] == "/foo" + attributes[UrlAttributes.URL_PATH] == "/foo" + attributes[HttpAttributes.HTTP_REQUEST_METHOD] == "GET" + attributes[HttpAttributes.HTTP_RESPONSE_STATUS_CODE] == 200L } } } diff --git a/instrumentation/ratpack/ratpack-1.7/library/src/test/java/io/opentelemetry/instrumentation/ratpack/v1_7/AbstractRatpackHttpClientTest.java b/instrumentation/ratpack/ratpack-1.7/library/src/test/java/io/opentelemetry/instrumentation/ratpack/v1_7/AbstractRatpackHttpClientTest.java index 9c1ed3fec507..affbe4197c3b 100644 --- a/instrumentation/ratpack/ratpack-1.7/library/src/test/java/io/opentelemetry/instrumentation/ratpack/v1_7/AbstractRatpackHttpClientTest.java +++ b/instrumentation/ratpack/ratpack-1.7/library/src/test/java/io/opentelemetry/instrumentation/ratpack/v1_7/AbstractRatpackHttpClientTest.java @@ -5,8 +5,6 @@ package io.opentelemetry.instrumentation.ratpack.v1_7; -import static io.opentelemetry.api.common.AttributeKey.stringKey; - import com.google.common.collect.ImmutableList; import io.netty.channel.ConnectTimeoutException; import io.opentelemetry.api.common.AttributeKey; @@ -14,6 +12,7 @@ import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpClientTest; import io.opentelemetry.instrumentation.testing.junit.http.HttpClientResult; import io.opentelemetry.instrumentation.testing.junit.http.HttpClientTestOptions; +import io.opentelemetry.semconv.NetworkAttributes; import java.net.URI; import java.time.Duration; import java.util.HashSet; @@ -153,12 +152,13 @@ protected void configure(HttpClientTestOptions.Builder optionsBuilder) { optionsBuilder.disableTestReusedRequest(); optionsBuilder.setHttpAttributes(this::getHttpAttributes); + + optionsBuilder.spanEndsAfterBody(); } protected Set> getHttpAttributes(URI uri) { Set> attributes = new HashSet<>(HttpClientTestOptions.DEFAULT_HTTP_ATTRIBUTES); - attributes.remove(stringKey("net.protocol.name")); - attributes.remove(stringKey("net.protocol.version")); + attributes.remove(NetworkAttributes.NETWORK_PROTOCOL_VERSION); return attributes; } } diff --git a/instrumentation/reactor/reactor-3.1/README.md b/instrumentation/reactor/reactor-3.1/README.md index 7807abfa0a84..fbbab7c17a09 100644 --- a/instrumentation/reactor/reactor-3.1/README.md +++ b/instrumentation/reactor/reactor-3.1/README.md @@ -1,5 +1,5 @@ # Settings for the Reactor 3.1 instrumentation -| System property | Type | Default | Description | -|---|---|---|---| +| System property | Type | Default | Description | +| ----------------------------------------------------------- | ------- | ------- | --------------------------------------------------- | | `otel.instrumentation.reactor.experimental-span-attributes` | Boolean | `false` | Enable the capture of experimental span attributes. | diff --git a/instrumentation/reactor/reactor-3.1/javaagent/build.gradle.kts b/instrumentation/reactor/reactor-3.1/javaagent/build.gradle.kts index 6ee953f266f9..43ac9c7926c7 100644 --- a/instrumentation/reactor/reactor-3.1/javaagent/build.gradle.kts +++ b/instrumentation/reactor/reactor-3.1/javaagent/build.gradle.kts @@ -14,13 +14,16 @@ muzzle { } tasks.withType().configureEach { + systemProperty("testLatestDeps", findProperty("testLatestDeps") as Boolean) // TODO run tests both with and without experimental span attributes jvmArgs("-Dotel.instrumentation.reactor.experimental-span-attributes=true") } dependencies { + // we compile against 3.4.0, so we could use reactor.util.context.ContextView + // instrumentation is tested against 3.1.0.RELEASE + compileOnly("io.projectreactor:reactor-core:3.4.0") implementation(project(":instrumentation:reactor:reactor-3.1:library")) - library("io.projectreactor:reactor-core:3.1.0.RELEASE") implementation(project(":instrumentation:opentelemetry-api:opentelemetry-api-1.0:javaagent")) @@ -30,14 +33,12 @@ dependencies { testInstrumentation(project(":instrumentation:opentelemetry-extension-annotations-1.0:javaagent")) + testLibrary("io.projectreactor:reactor-core:3.1.0.RELEASE") testLibrary("io.projectreactor:reactor-test:3.1.0.RELEASE") testImplementation(project(":instrumentation-annotations-support-testing")) testImplementation(project(":instrumentation:reactor:reactor-3.1:testing")) testImplementation(project(":instrumentation-annotations")) testImplementation("io.opentelemetry:opentelemetry-extension-annotations") - - latestDepTestLibrary("io.projectreactor:reactor-core:3.4.+") - latestDepTestLibrary("io.projectreactor:reactor-test:3.4.+") } testing { @@ -46,7 +47,11 @@ testing { dependencies { implementation(project(":instrumentation:reactor:reactor-3.1:library")) implementation(project(":instrumentation-annotations")) - implementation("io.projectreactor:reactor-test:3.1.0.RELEASE") + if (findProperty("testLatestDeps") as Boolean) { + implementation("io.projectreactor:reactor-test:+") + } else { + implementation("io.projectreactor:reactor-test:3.1.0.RELEASE") + } } } } diff --git a/instrumentation/reactor/reactor-3.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/reactor/v3_1/HooksInstrumentation.java b/instrumentation/reactor/reactor-3.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/reactor/v3_1/HooksInstrumentation.java index aeb32f290435..0591db7569ea 100644 --- a/instrumentation/reactor/reactor-3.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/reactor/v3_1/HooksInstrumentation.java +++ b/instrumentation/reactor/reactor-3.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/reactor/v3_1/HooksInstrumentation.java @@ -10,7 +10,7 @@ import static net.bytebuddy.matcher.ElementMatchers.namedOneOf; import io.opentelemetry.instrumentation.reactor.v3_1.ContextPropagationOperator; -import io.opentelemetry.javaagent.bootstrap.internal.InstrumentationConfig; +import io.opentelemetry.javaagent.bootstrap.internal.AgentInstrumentationConfig; import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; import net.bytebuddy.asm.Advice; @@ -41,7 +41,7 @@ public static class ResetOnEachOperatorAdvice { public static void postStaticInitializer() { ContextPropagationOperator.builder() .setCaptureExperimentalSpanAttributes( - InstrumentationConfig.get() + AgentInstrumentationConfig.get() .getBoolean("otel.instrumentation.reactor.experimental-span-attributes", false)) .build() .registerOnEachOperator(); diff --git a/instrumentation/reactor/reactor-3.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/reactor/v3_1/ReactorInstrumentationModule.java b/instrumentation/reactor/reactor-3.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/reactor/v3_1/ReactorInstrumentationModule.java index 31c1a1ff2d5d..22a1571a62d6 100644 --- a/instrumentation/reactor/reactor-3.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/reactor/v3_1/ReactorInstrumentationModule.java +++ b/instrumentation/reactor/reactor-3.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/reactor/v3_1/ReactorInstrumentationModule.java @@ -10,15 +10,23 @@ 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.internal.ExperimentalInstrumentationModule; import java.util.List; @AutoService(InstrumentationModule.class) -public class ReactorInstrumentationModule extends InstrumentationModule { +public class ReactorInstrumentationModule extends InstrumentationModule + implements ExperimentalInstrumentationModule { public ReactorInstrumentationModule() { super("reactor", "reactor-3.1"); } + @Override + public String getModuleGroup() { + // needs to be in the same module as ContextPropagationOperatorInstrumentation + return "opentelemetry-api-bridge"; + } + @Override public List typeInstrumentations() { return singletonList(new HooksInstrumentation()); diff --git a/instrumentation/reactor/reactor-3.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/reactor/v3_1/operator/ContextPropagationOperatorInstrumentation.java b/instrumentation/reactor/reactor-3.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/reactor/v3_1/operator/ContextPropagationOperatorInstrumentation.java index 670f63dd2607..cde1a4d4d8c5 100644 --- a/instrumentation/reactor/reactor-3.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/reactor/v3_1/operator/ContextPropagationOperatorInstrumentation.java +++ b/instrumentation/reactor/reactor-3.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/reactor/v3_1/operator/ContextPropagationOperatorInstrumentation.java @@ -106,9 +106,9 @@ public static void methodExit( @SuppressWarnings("unused") public static class RunWithAdvice { @Advice.OnMethodEnter - public static void methodEnter( - @Advice.FieldValue(value = "enabled", readOnly = false) boolean enabled) { - enabled = true; + @Advice.AssignReturned.ToFields(@Advice.AssignReturned.ToFields.ToField("enabled")) + public static boolean methodEnter() { + return true; } } } diff --git a/instrumentation/reactor/reactor-3.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/reactor/v3_1/operator/ContextPropagationOperatorInstrumentationModule.java b/instrumentation/reactor/reactor-3.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/reactor/v3_1/operator/ContextPropagationOperatorInstrumentationModule.java index db96c7618c08..1f40d19a3f1b 100644 --- a/instrumentation/reactor/reactor-3.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/reactor/v3_1/operator/ContextPropagationOperatorInstrumentationModule.java +++ b/instrumentation/reactor/reactor-3.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/reactor/v3_1/operator/ContextPropagationOperatorInstrumentationModule.java @@ -11,11 +11,13 @@ 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.internal.ExperimentalInstrumentationModule; import java.util.List; import net.bytebuddy.matcher.ElementMatcher; @AutoService(InstrumentationModule.class) -public class ContextPropagationOperatorInstrumentationModule extends InstrumentationModule { +public class ContextPropagationOperatorInstrumentationModule extends InstrumentationModule + implements ExperimentalInstrumentationModule { public ContextPropagationOperatorInstrumentationModule() { super("reactor", "reactor-3.1", "reactor-context-propagation-operator"); @@ -30,4 +32,10 @@ public ElementMatcher.Junction classLoaderMatcher() { public List typeInstrumentations() { return singletonList(new ContextPropagationOperatorInstrumentation()); } + + @Override + public String getModuleGroup() { + // This module uses the api context bridge helpers, therefore must be in the same classloader + return "opentelemetry-api-bridge"; + } } diff --git a/instrumentation/reactor/reactor-3.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/reactor/v3_1/BaseFluxWithSpanTest.java b/instrumentation/reactor/reactor-3.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/reactor/v3_1/BaseFluxWithSpanTest.java index ddc290b80d0f..1c3eb39f5650 100644 --- a/instrumentation/reactor/reactor-3.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/reactor/v3_1/BaseFluxWithSpanTest.java +++ b/instrumentation/reactor/reactor-3.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/reactor/v3_1/BaseFluxWithSpanTest.java @@ -6,8 +6,8 @@ package io.opentelemetry.javaagent.instrumentation.reactor.v3_1; import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.CODE_FUNCTION; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.CODE_NAMESPACE; +import static io.opentelemetry.semconv.incubating.CodeIncubatingAttributes.CODE_FUNCTION; +import static io.opentelemetry.semconv.incubating.CodeIncubatingAttributes.CODE_NAMESPACE; import io.opentelemetry.api.common.Attributes; import io.opentelemetry.api.trace.SpanKind; diff --git a/instrumentation/reactor/reactor-3.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/reactor/v3_1/BaseMonoWithSpanTest.java b/instrumentation/reactor/reactor-3.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/reactor/v3_1/BaseMonoWithSpanTest.java index 2a49fbe50320..2e0b3b2f7ecf 100644 --- a/instrumentation/reactor/reactor-3.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/reactor/v3_1/BaseMonoWithSpanTest.java +++ b/instrumentation/reactor/reactor-3.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/reactor/v3_1/BaseMonoWithSpanTest.java @@ -6,8 +6,8 @@ package io.opentelemetry.javaagent.instrumentation.reactor.v3_1; import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.CODE_FUNCTION; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.CODE_NAMESPACE; +import static io.opentelemetry.semconv.incubating.CodeIncubatingAttributes.CODE_FUNCTION; +import static io.opentelemetry.semconv.incubating.CodeIncubatingAttributes.CODE_NAMESPACE; import io.opentelemetry.api.common.Attributes; import io.opentelemetry.api.trace.SpanKind; @@ -88,7 +88,8 @@ void nested() { span -> span.hasName("inner-manual") .hasKind(SpanKind.INTERNAL) - .hasParent(trace.getSpan(1)) + // earliest tested and latest version behave differently + .hasParent(trace.getSpan(Boolean.getBoolean("testLatestDeps") ? 0 : 1)) .hasAttributes(Attributes.empty()))); } @@ -130,7 +131,7 @@ void nestedFromCurrent() { span -> span.hasName("inner-manual") .hasKind(SpanKind.INTERNAL) - .hasParent(trace.getSpan(1)) + .hasParent(trace.getSpan(Boolean.getBoolean("testLatestDeps") ? 0 : 1)) .hasAttributes(Attributes.empty()))); } diff --git a/instrumentation/reactor/reactor-3.1/library/build.gradle.kts b/instrumentation/reactor/reactor-3.1/library/build.gradle.kts index 5ec7deacdfa5..8b2da177a1c2 100644 --- a/instrumentation/reactor/reactor-3.1/library/build.gradle.kts +++ b/instrumentation/reactor/reactor-3.1/library/build.gradle.kts @@ -3,12 +3,19 @@ plugins { } dependencies { - library("io.projectreactor:reactor-core:3.1.0.RELEASE") + // we compile against 3.4.0, so we could use reactor.util.context.ContextView + // instrumentation is expected it to work with 3.1.0.RELEASE + compileOnly("io.projectreactor:reactor-core:3.4.0") + compileOnly(project(":muzzle")) // For @NoMuzzle implementation(project(":instrumentation-annotations-support")) + testLibrary("io.projectreactor:reactor-core:3.1.0.RELEASE") testLibrary("io.projectreactor:reactor-test:3.1.0.RELEASE") testImplementation(project(":instrumentation:reactor:reactor-3.1:testing")) +} - latestDepTestLibrary("io.projectreactor:reactor-core:3.4.+") - latestDepTestLibrary("io.projectreactor:reactor-test:3.4.+") +tasks { + withType().configureEach { + systemProperty("testLatestDeps", findProperty("testLatestDeps") as Boolean) + } } diff --git a/instrumentation/reactor/reactor-3.1/library/src/main/java/io/opentelemetry/instrumentation/reactor/v3_1/ContextPropagationOperator.java b/instrumentation/reactor/reactor-3.1/library/src/main/java/io/opentelemetry/instrumentation/reactor/v3_1/ContextPropagationOperator.java index faefac0f1861..3d554240c21b 100644 --- a/instrumentation/reactor/reactor-3.1/library/src/main/java/io/opentelemetry/instrumentation/reactor/v3_1/ContextPropagationOperator.java +++ b/instrumentation/reactor/reactor-3.1/library/src/main/java/io/opentelemetry/instrumentation/reactor/v3_1/ContextPropagationOperator.java @@ -27,10 +27,13 @@ import io.opentelemetry.context.Context; import io.opentelemetry.context.Scope; import io.opentelemetry.instrumentation.api.annotation.support.async.AsyncOperationEndStrategies; +import io.opentelemetry.javaagent.tooling.muzzle.NoMuzzle; import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; import java.util.function.BiFunction; import java.util.function.Function; +import java.util.logging.Level; +import java.util.logging.Logger; import javax.annotation.Nullable; import org.reactivestreams.Publisher; import reactor.core.CoreSubscriber; @@ -40,9 +43,11 @@ import reactor.core.publisher.Hooks; import reactor.core.publisher.Mono; import reactor.core.publisher.Operators; +import reactor.core.scheduler.Schedulers; /** Based on Spring Sleuth's Reactor instrumentation. */ public final class ContextPropagationOperator { + private static final Logger logger = Logger.getLogger(ContextPropagationOperator.class.getName()); private static final Object VALUE = new Object(); @@ -52,6 +57,8 @@ public final class ContextPropagationOperator { @Nullable private static final MethodHandle FLUX_CONTEXT_WRITE_METHOD = getContextWriteMethod(Flux.class); + @Nullable private static final MethodHandle SCHEDULERS_HOOK_METHOD = getSchedulersHookMethod(); + @Nullable private static MethodHandle getContextWriteMethod(Class type) { MethodHandles.Lookup lookup = MethodHandles.publicLookup(); @@ -68,6 +75,18 @@ private static MethodHandle getContextWriteMethod(Class type) { return null; } + @Nullable + private static MethodHandle getSchedulersHookMethod() { + MethodHandles.Lookup lookup = MethodHandles.publicLookup(); + try { + return lookup.findStatic( + Schedulers.class, "onScheduleHook", methodType(void.class, String.class, Function.class)); + } catch (NoSuchMethodException | IllegalAccessException e) { + // ignore + } + return null; + } + public static ContextPropagationOperator create() { return builder().build(); } @@ -116,6 +135,20 @@ public static Context getOpenTelemetryContext( return context.getOrDefault(TRACE_CONTEXT_KEY, defaultTraceContext); } + /** + * Gets Trace {@link Context} from Reactor {@link reactor.util.context.ContextView}. + * + * @param contextView Reactor's context to get trace context from. + * @param defaultTraceContext Default value to be returned if no trace context is found on Reactor + * context. + * @return Trace context or default value. + */ + @NoMuzzle + public static Context getOpenTelemetryContextFromContextView( + reactor.util.context.ContextView contextView, Context defaultTraceContext) { + return contextView.getOrDefault(TRACE_CONTEXT_KEY, defaultTraceContext); + } + ContextPropagationOperator(boolean captureExperimentalSpanAttributes) { this.asyncOperationEndStrategy = ReactorAsyncOperationEndStrategy.builder() @@ -137,10 +170,22 @@ public void registerOnEachOperator() { Hooks.onEachOperator( TracingSubscriber.class.getName(), tracingLift(asyncOperationEndStrategy)); AsyncOperationEndStrategies.instance().registerStrategy(asyncOperationEndStrategy); + registerScheduleHook(RunnableWrapper.class.getName(), RunnableWrapper::new); enabled = true; } } + private static void registerScheduleHook(String key, Function function) { + if (SCHEDULERS_HOOK_METHOD == null) { + return; + } + try { + SCHEDULERS_HOOK_METHOD.invoke(key, function); + } catch (Throwable throwable) { + logger.log(Level.WARNING, "Failed to install scheduler hook", throwable); + } + } + /** Unregisters the hook registered by {@link #registerOnEachOperator()}. */ public void resetOnEachOperator() { synchronized (lock) { @@ -312,4 +357,21 @@ public Object scanUnsafe(Scannable.Attr attr) { return null; } } + + private static class RunnableWrapper implements Runnable { + private final Runnable delegate; + private final Context context; + + RunnableWrapper(Runnable delegate) { + this.delegate = delegate; + context = Context.current(); + } + + @Override + public void run() { + try (Scope ignore = context.makeCurrent()) { + delegate.run(); + } + } + } } diff --git a/instrumentation/reactor/reactor-3.1/library/src/main/java/io/opentelemetry/instrumentation/reactor/v3_1/TracingSubscriber.java b/instrumentation/reactor/reactor-3.1/library/src/main/java/io/opentelemetry/instrumentation/reactor/v3_1/TracingSubscriber.java index f9195d56360b..e3f81bc900af 100644 --- a/instrumentation/reactor/reactor-3.1/library/src/main/java/io/opentelemetry/instrumentation/reactor/v3_1/TracingSubscriber.java +++ b/instrumentation/reactor/reactor-3.1/library/src/main/java/io/opentelemetry/instrumentation/reactor/v3_1/TracingSubscriber.java @@ -22,6 +22,7 @@ import io.opentelemetry.api.trace.Span; import io.opentelemetry.context.Scope; +import java.util.function.Supplier; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; import reactor.core.CoreSubscriber; @@ -56,30 +57,40 @@ public TracingSubscriber( @Override public void onSubscribe(Subscription subscription) { - withActiveSpan(() -> subscriber.onSubscribe(subscription)); + try (Scope ignore = openScope()) { + subscriber.onSubscribe(subscription); + } } @Override public void onNext(T o) { - withActiveSpan(() -> subscriber.onNext(o)); + try (Scope ignore = openScope()) { + subscriber.onNext(o); + } } @Override public void onError(Throwable throwable) { + Supplier scopeSupplier; if (!hasContextToPropagate && (fluxRetrySubscriberClass == subscriber.getClass() || fluxRetryWhenSubscriberClass == subscriber.getClass())) { // clear context for retry to avoid having retried operations run with currently active // context as parent context - withActiveSpan(io.opentelemetry.context.Context.root(), () -> subscriber.onError(throwable)); + scopeSupplier = () -> openScope(io.opentelemetry.context.Context.root()); } else { - withActiveSpan(() -> subscriber.onError(throwable)); + scopeSupplier = () -> openScope(); + } + try (Scope ignore = scopeSupplier.get()) { + subscriber.onError(throwable); } } @Override public void onComplete() { - withActiveSpan(subscriber::onComplete); + try (Scope ignore = openScope()) { + subscriber.onComplete(); + } } @Override @@ -87,18 +98,12 @@ public Context currentContext() { return context; } - private void withActiveSpan(Runnable runnable) { - withActiveSpan(hasContextToPropagate ? traceContext : null, runnable); + private Scope openScope() { + return openScope(hasContextToPropagate ? traceContext : null); } - private static void withActiveSpan(io.opentelemetry.context.Context context, Runnable runnable) { - if (context != null) { - try (Scope ignored = context.makeCurrent()) { - runnable.run(); - } - } else { - runnable.run(); - } + private static Scope openScope(io.opentelemetry.context.Context context) { + return context != null ? context.makeCurrent() : null; } private static Class getFluxRetrySubscriberClass() { diff --git a/instrumentation/reactor/reactor-3.1/library/src/test/java/io/opentelemetry/instrumentation/reactor/v3_1/HooksTest.java b/instrumentation/reactor/reactor-3.1/library/src/test/java/io/opentelemetry/instrumentation/reactor/v3_1/HooksTest.java index 43256891396d..31aab80800fc 100644 --- a/instrumentation/reactor/reactor-3.1/library/src/test/java/io/opentelemetry/instrumentation/reactor/v3_1/HooksTest.java +++ b/instrumentation/reactor/reactor-3.1/library/src/test/java/io/opentelemetry/instrumentation/reactor/v3_1/HooksTest.java @@ -62,7 +62,7 @@ void testInvalidBlockUsage() throws InterruptedException { Disposable disposable = Mono.defer( () -> - Mono.fromCallable(callable).publishOn(Schedulers.elastic()).flatMap(Mono::just)) + Mono.fromCallable(callable).publishOn(Schedulers.single()).flatMap(Mono::just)) .subscribeOn(Schedulers.single()) .subscribe(); diff --git a/instrumentation/reactor/reactor-3.1/library/src/test/java/io/opentelemetry/instrumentation/reactor/v3_1/ReactorCoreTest.java b/instrumentation/reactor/reactor-3.1/library/src/test/java/io/opentelemetry/instrumentation/reactor/v3_1/ReactorCoreTest.java index 1e419b0b962e..cbc696656ab3 100644 --- a/instrumentation/reactor/reactor-3.1/library/src/test/java/io/opentelemetry/instrumentation/reactor/v3_1/ReactorCoreTest.java +++ b/instrumentation/reactor/reactor-3.1/library/src/test/java/io/opentelemetry/instrumentation/reactor/v3_1/ReactorCoreTest.java @@ -160,13 +160,18 @@ void fluxInNonBlockingPublisherAssembly() { @Test void nestedNonBlocking() { + boolean testLatestDeps = Boolean.getBoolean("testLatestDeps"); int result = testing.runWithSpan( "parent", () -> Mono.defer( () -> { - Span.current().setAttribute("middle", "foo"); + // earliest tested and latest version behave differently + // in latest dep test current span is "parent" not "middle" + if (!testLatestDeps) { + Span.current().setAttribute("middle", "foo"); + } return Mono.fromCallable( () -> { Span.current().setAttribute("inner", "bar"); @@ -183,10 +188,12 @@ void nestedNonBlocking() { trace -> trace.hasSpansSatisfyingExactly( span -> span.hasName("parent").hasNoParent(), - span -> - span.hasName("middle") - .hasParent(trace.getSpan(0)) - .hasAttributes(attributeEntry("middle", "foo")), + span -> { + span.hasName("middle").hasParent(trace.getSpan(0)); + if (!testLatestDeps) { + span.hasAttributes(attributeEntry("middle", "foo")); + } + }, span -> span.hasName("inner") .hasParent(trace.getSpan(1)) diff --git a/instrumentation/reactor/reactor-3.4/javaagent/build.gradle.kts b/instrumentation/reactor/reactor-3.4/javaagent/build.gradle.kts new file mode 100644 index 000000000000..adc48103a8c9 --- /dev/null +++ b/instrumentation/reactor/reactor-3.4/javaagent/build.gradle.kts @@ -0,0 +1,34 @@ +plugins { + id("otel.javaagent-instrumentation") +} + +muzzle { + pass { + group.set("io.projectreactor") + module.set("reactor-core") + versions.set("[3.4.0,)") + extraDependency("io.opentelemetry:opentelemetry-api:1.0.0") + assertInverse.set(true) + excludeInstrumentationName("opentelemetry-api") + } +} + +dependencies { + library("io.projectreactor:reactor-core:3.4.0") + implementation(project(":instrumentation:reactor:reactor-3.1:library")) + + implementation(project(":instrumentation:opentelemetry-api:opentelemetry-api-1.0:javaagent")) + + compileOnly(project(":javaagent-tooling")) + compileOnly(project(":instrumentation-annotations-support")) + compileOnly(project(":opentelemetry-api-shaded-for-instrumenting", configuration = "shadow")) + + testInstrumentation(project(":instrumentation:reactor:reactor-3.1:javaagent")) + testInstrumentation(project(":instrumentation:opentelemetry-extension-annotations-1.0:javaagent")) + + testLibrary("io.projectreactor:reactor-test:3.1.0.RELEASE") + testImplementation(project(":instrumentation-annotations-support-testing")) + testImplementation(project(":instrumentation:reactor:reactor-3.1:testing")) + testImplementation(project(":instrumentation-annotations")) + testImplementation("io.opentelemetry:opentelemetry-extension-annotations") +} diff --git a/instrumentation/reactor/reactor-3.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/reactor/v3_4/operator/ContextPropagationOperator34Instrumentation.java b/instrumentation/reactor/reactor-3.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/reactor/v3_4/operator/ContextPropagationOperator34Instrumentation.java new file mode 100644 index 000000000000..4c4ab8e8bb7c --- /dev/null +++ b/instrumentation/reactor/reactor-3.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/reactor/v3_4/operator/ContextPropagationOperator34Instrumentation.java @@ -0,0 +1,66 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.reactor.v3_4.operator; + +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 static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import application.io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.reactor.v3_1.ContextPropagationOperator; +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; + +public class ContextPropagationOperator34Instrumentation implements TypeInstrumentation { + @Override + public ElementMatcher typeMatcher() { + return named( + "application.io.opentelemetry.instrumentation.reactor.v3_1.ContextPropagationOperator"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod() + .and(isPublic()) + .and(isStatic()) + .and(named("getOpenTelemetryContextFromContextView")) + .and(takesArgument(0, named("reactor.util.context.ContextView"))) + .and(takesArgument(1, named("application.io.opentelemetry.context.Context"))) + .and(returns(named("application.io.opentelemetry.context.Context"))), + ContextPropagationOperator34Instrumentation.class.getName() + "$GetContextViewAdvice"); + } + + @SuppressWarnings("unused") + public static class GetContextViewAdvice { + @Advice.OnMethodEnter(skipOn = Advice.OnDefaultValue.class) + public static boolean methodEnter() { + return false; + } + + @Advice.OnMethodExit(suppress = Throwable.class) + public static void methodExit( + @Advice.Argument(0) reactor.util.context.ContextView reactorContext, + @Advice.Argument(1) Context defaultContext, + @Advice.Return(readOnly = false) Context applicationContext) { + + io.opentelemetry.context.Context agentContext = + ContextPropagationOperator.getOpenTelemetryContextFromContextView(reactorContext, null); + if (agentContext == null) { + applicationContext = defaultContext; + } else { + applicationContext = AgentContextStorage.toApplicationContext(agentContext); + } + } + } +} diff --git a/instrumentation/reactor/reactor-3.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/reactor/v3_4/operator/ContextPropagationOperator34InstrumentationModule.java b/instrumentation/reactor/reactor-3.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/reactor/v3_4/operator/ContextPropagationOperator34InstrumentationModule.java new file mode 100644 index 000000000000..c5fabc4dccbf --- /dev/null +++ b/instrumentation/reactor/reactor-3.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/reactor/v3_4/operator/ContextPropagationOperator34InstrumentationModule.java @@ -0,0 +1,42 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.reactor.v3_4.operator; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.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 io.opentelemetry.javaagent.extension.instrumentation.internal.ExperimentalInstrumentationModule; +import java.util.List; +import net.bytebuddy.matcher.ElementMatcher; + +@AutoService(InstrumentationModule.class) +public class ContextPropagationOperator34InstrumentationModule extends InstrumentationModule + implements ExperimentalInstrumentationModule { + + public ContextPropagationOperator34InstrumentationModule() { + super("reactor", "reactor-3.4", "reactor-context-propagation-operator"); + } + + @Override + public ElementMatcher.Junction classLoaderMatcher() { + return hasClassesNamed( + "application.io.opentelemetry.context.Context", "reactor.util.context.ContextView"); + } + + @Override + public List typeInstrumentations() { + return singletonList(new ContextPropagationOperator34Instrumentation()); + } + + @Override + public String getModuleGroup() { + // This module uses the api context bridge helpers, therefore must be in the same classloader + return "opentelemetry-api-bridge"; + } +} diff --git a/instrumentation/reactor/reactor-3.4/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/reactor/v3_4/ContextPropagationOperator34InstrumentationTest.java b/instrumentation/reactor/reactor-3.4/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/reactor/v3_4/ContextPropagationOperator34InstrumentationTest.java new file mode 100644 index 000000000000..04c59624f9ac --- /dev/null +++ b/instrumentation/reactor/reactor-3.4/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/reactor/v3_4/ContextPropagationOperator34InstrumentationTest.java @@ -0,0 +1,55 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.reactor.v3_4; + +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.attributeEntry; +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.reactor.v3_1.ContextPropagationOperator; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +class ContextPropagationOperator34InstrumentationTest { + + @RegisterExtension + static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + @Test + void storeAndGetContext() { + reactor.util.context.Context reactorContext = reactor.util.context.Context.empty(); + testing.runWithSpan( + "parent", + () -> { + reactor.util.context.Context newReactorContext = + ContextPropagationOperator.storeOpenTelemetryContext( + reactorContext, Context.current()); + Context otelContext = + ContextPropagationOperator.getOpenTelemetryContextFromContextView( + newReactorContext, null); + assertThat(otelContext).isNotNull(); + Span.fromContext(otelContext).setAttribute("foo", "bar"); + Context otelContext2 = + ContextPropagationOperator.getOpenTelemetryContext(newReactorContext, null); + assertThat(otelContext2).isNotNull(); + Span.fromContext(otelContext2).setAttribute("foo2", "bar2"); + }); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("parent") + .hasKind(SpanKind.INTERNAL) + .hasNoParent() + .hasAttributes( + attributeEntry("foo", "bar"), attributeEntry("foo2", "bar2")))); + } +} diff --git a/instrumentation/reactor/reactor-kafka-1.0/javaagent/build.gradle.kts b/instrumentation/reactor/reactor-kafka-1.0/javaagent/build.gradle.kts index 68a9b40d28a1..5f6e527c4897 100644 --- a/instrumentation/reactor/reactor-kafka-1.0/javaagent/build.gradle.kts +++ b/instrumentation/reactor/reactor-kafka-1.0/javaagent/build.gradle.kts @@ -22,9 +22,9 @@ dependencies { implementation(project(":instrumentation:kafka:kafka-clients:kafka-clients-common:library")) implementation(project(":instrumentation:reactor:reactor-3.1:library")) - // using 1.3.0 to be able to implement several new KafkaReceiver methods added in 1.3.3 + // using 1.3 to be able to implement several new KafkaReceiver methods added in 1.3.3 and 1.3.21 // @NoMuzzle is used to ensure that this does not break muzzle checks - compileOnly("io.projectreactor.kafka:reactor-kafka:1.3.3") + compileOnly("io.projectreactor.kafka:reactor-kafka:1.3.21") testInstrumentation(project(":instrumentation:kafka:kafka-clients:kafka-clients-0.11:javaagent")) testInstrumentation(project(":instrumentation:reactor:reactor-3.1:javaagent")) @@ -55,7 +55,28 @@ testing { targets { all { testTask.configure { - systemProperty("hasConsumerGroupAndId", true) + systemProperty("hasConsumerGroup", true) + } + } + } + } + + val testV1_3_21 by registering(JvmTestSuite::class) { + dependencies { + implementation(project(":instrumentation:reactor:reactor-kafka-1.0:testing")) + + if (testLatestDeps) { + implementation("io.projectreactor.kafka:reactor-kafka:+") + implementation("io.projectreactor:reactor-core:3.4.+") + } else { + implementation("io.projectreactor.kafka:reactor-kafka:1.3.21") + } + } + + targets { + all { + testTask.configure { + systemProperty("hasConsumerGroup", true) } } } @@ -72,7 +93,7 @@ tasks { } test { - systemProperty("hasConsumerGroupAndId", testLatestDeps) + systemProperty("hasConsumerGroup", testLatestDeps) } check { diff --git a/instrumentation/reactor/reactor-kafka-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/reactor/kafka/v1_0/InstrumentedKafkaReceiver.java b/instrumentation/reactor/reactor-kafka-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/reactor/kafka/v1_0/InstrumentedKafkaReceiver.java index 62b827f2fdf7..1ecb17c1ff77 100644 --- a/instrumentation/reactor/reactor-kafka-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/reactor/kafka/v1_0/InstrumentedKafkaReceiver.java +++ b/instrumentation/reactor/reactor-kafka-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/reactor/kafka/v1_0/InstrumentedKafkaReceiver.java @@ -25,7 +25,7 @@ public InstrumentedKafkaReceiver(KafkaReceiver actual) { // added in 1.3.3 @Override public Flux> receive(Integer prefetch) { - return wrap(KafkaReceiver133Access.receive(actual, prefetch)); + return wrap(KafkaReceiver13Access.receive(actual, prefetch)); } @Override @@ -36,7 +36,7 @@ public Flux> receive() { // added in 1.3.3 @Override public Flux>> receiveAutoAck(Integer prefetch) { - return KafkaReceiver133Access.receiveAutoAck(actual, prefetch) + return KafkaReceiver13Access.receiveAutoAck(actual, prefetch) .map(InstrumentedKafkaReceiver::wrap); } @@ -48,7 +48,7 @@ public Flux>> receiveAutoAck() { // added in 1.3.3 @Override public Flux> receiveAtmostOnce(Integer prefetch) { - return wrap(KafkaReceiver133Access.receiveAtmostOnce(actual, prefetch)); + return wrap(KafkaReceiver13Access.receiveAtmostOnce(actual, prefetch)); } @Override @@ -59,14 +59,14 @@ public Flux> receiveAtmostOnce() { @Override public Flux>> receiveExactlyOnce( TransactionManager transactionManager) { - return actual.receiveAutoAck().map(InstrumentedKafkaReceiver::wrap); + return actual.receiveExactlyOnce(transactionManager).map(InstrumentedKafkaReceiver::wrap); } // added in 1.3.3 @Override public Flux>> receiveExactlyOnce( TransactionManager transactionManager, Integer prefetch) { - return KafkaReceiver133Access.receiveExactlyOnce(actual, transactionManager, prefetch) + return KafkaReceiver13Access.receiveExactlyOnce(actual, transactionManager, prefetch) .map(InstrumentedKafkaReceiver::wrap); } @@ -75,6 +75,19 @@ public Mono doOnConsumer(Function, ? extends T> function) return actual.doOnConsumer(function); } + // added in 1.3.21 + @Override + public Flux>> receiveBatch(Integer prefetch) { + return KafkaReceiver13Access.receiveBatch(actual, prefetch) + .map(InstrumentedKafkaReceiver::wrap); + } + + // added in 1.3.21 + @Override + public Flux>> receiveBatch() { + return KafkaReceiver13Access.receiveBatch(actual).map(InstrumentedKafkaReceiver::wrap); + } + private static > Flux wrap(Flux flux) { return flux instanceof InstrumentedKafkaFlux ? flux : new InstrumentedKafkaFlux<>(flux); } diff --git a/instrumentation/reactor/reactor-kafka-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/reactor/kafka/v1_0/KafkaReceiver133Access.java b/instrumentation/reactor/reactor-kafka-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/reactor/kafka/v1_0/KafkaReceiver13Access.java similarity index 75% rename from instrumentation/reactor/reactor-kafka-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/reactor/kafka/v1_0/KafkaReceiver133Access.java rename to instrumentation/reactor/reactor-kafka-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/reactor/kafka/v1_0/KafkaReceiver13Access.java index 2e21f39d8829..de50fa9b353e 100644 --- a/instrumentation/reactor/reactor-kafka-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/reactor/kafka/v1_0/KafkaReceiver133Access.java +++ b/instrumentation/reactor/reactor-kafka-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/reactor/kafka/v1_0/KafkaReceiver13Access.java @@ -12,7 +12,7 @@ import reactor.kafka.receiver.ReceiverRecord; import reactor.kafka.sender.TransactionManager; -final class KafkaReceiver133Access { +final class KafkaReceiver13Access { @NoMuzzle static Flux> receive(KafkaReceiver receiver, Integer prefetch) { @@ -37,5 +37,16 @@ static Flux>> receiveExactlyOnce( return receiver.receiveExactlyOnce(transactionManager, prefetch); } - private KafkaReceiver133Access() {} + @NoMuzzle + static Flux>> receiveBatch( + KafkaReceiver receiver, Integer prefetch) { + return receiver.receiveBatch(prefetch); + } + + @NoMuzzle + static Flux>> receiveBatch(KafkaReceiver receiver) { + return receiver.receiveBatch(); + } + + private KafkaReceiver13Access() {} } diff --git a/instrumentation/reactor/reactor-kafka-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/reactor/kafka/v1_0/ReactorKafkaSingletons.java b/instrumentation/reactor/reactor-kafka-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/reactor/kafka/v1_0/ReactorKafkaSingletons.java index b5b8e309f58a..66b7ace29c92 100644 --- a/instrumentation/reactor/reactor-kafka-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/reactor/kafka/v1_0/ReactorKafkaSingletons.java +++ b/instrumentation/reactor/reactor-kafka-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/reactor/kafka/v1_0/ReactorKafkaSingletons.java @@ -9,8 +9,8 @@ import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; import io.opentelemetry.instrumentation.kafka.internal.KafkaInstrumenterFactory; import io.opentelemetry.instrumentation.kafka.internal.KafkaProcessRequest; +import io.opentelemetry.javaagent.bootstrap.internal.AgentInstrumentationConfig; import io.opentelemetry.javaagent.bootstrap.internal.ExperimentalConfig; -import io.opentelemetry.javaagent.bootstrap.internal.InstrumentationConfig; final class ReactorKafkaSingletons { @@ -20,7 +20,7 @@ final class ReactorKafkaSingletons { new KafkaInstrumenterFactory(GlobalOpenTelemetry.get(), INSTRUMENTATION_NAME) .setCapturedHeaders(ExperimentalConfig.get().getMessagingHeaders()) .setCaptureExperimentalSpanAttributes( - InstrumentationConfig.get() + AgentInstrumentationConfig.get() .getBoolean("otel.instrumentation.kafka.experimental-span-attributes", false)) .setMessagingReceiveInstrumentationEnabled( ExperimentalConfig.get().messagingReceiveInstrumentationEnabled()) diff --git a/instrumentation/reactor/reactor-kafka-1.0/javaagent/src/testV1_3_21/java/io/opentelemetry/javaagent/instrumentation/reactor/kafka/v1_0/ReactorKafka1321InstrumentationTest.java b/instrumentation/reactor/reactor-kafka-1.0/javaagent/src/testV1_3_21/java/io/opentelemetry/javaagent/instrumentation/reactor/kafka/v1_0/ReactorKafka1321InstrumentationTest.java new file mode 100644 index 000000000000..c146c009cb5f --- /dev/null +++ b/instrumentation/reactor/reactor-kafka-1.0/javaagent/src/testV1_3_21/java/io/opentelemetry/javaagent/instrumentation/reactor/kafka/v1_0/ReactorKafka1321InstrumentationTest.java @@ -0,0 +1,33 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.reactor.kafka.v1_0; + +import org.junit.jupiter.api.Test; + +class ReactorKafka1321InstrumentationTest extends AbstractReactorKafkaTest { + + @Test + void receiveBatch() { + testSingleRecordProcess( + recordConsumer -> + receiver + .receiveBatch() + .concatMap(r -> r) + .doOnNext(r -> r.receiverOffset().acknowledge()) + .subscribe(recordConsumer)); + } + + @Test + void receiveBatchWithSize() { + testSingleRecordProcess( + recordConsumer -> + receiver + .receiveBatch(1) + .concatMap(r -> r) + .doOnNext(r -> r.receiverOffset().acknowledge()) + .subscribe(recordConsumer)); + } +} diff --git a/instrumentation/reactor/reactor-kafka-1.0/testing/src/main/java/io/opentelemetry/javaagent/instrumentation/reactor/kafka/v1_0/AbstractReactorKafkaTest.java b/instrumentation/reactor/reactor-kafka-1.0/testing/src/main/java/io/opentelemetry/javaagent/instrumentation/reactor/kafka/v1_0/AbstractReactorKafkaTest.java index 068a1f5858a8..cefb87a770c1 100644 --- a/instrumentation/reactor/reactor-kafka-1.0/testing/src/main/java/io/opentelemetry/javaagent/instrumentation/reactor/kafka/v1_0/AbstractReactorKafkaTest.java +++ b/instrumentation/reactor/reactor-kafka-1.0/testing/src/main/java/io/opentelemetry/javaagent/instrumentation/reactor/kafka/v1_0/AbstractReactorKafkaTest.java @@ -19,7 +19,7 @@ import io.opentelemetry.sdk.testing.assertj.AttributeAssertion; import io.opentelemetry.sdk.trace.data.LinkData; import io.opentelemetry.sdk.trace.data.SpanData; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes; import java.lang.reflect.InvocationTargetException; import java.nio.charset.StandardCharsets; import java.time.Duration; @@ -35,6 +35,7 @@ import org.apache.kafka.common.serialization.StringDeserializer; import org.apache.kafka.common.serialization.StringSerializer; import org.assertj.core.api.AbstractLongAssert; +import org.assertj.core.api.AbstractStringAssert; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.extension.RegisterExtension; @@ -68,7 +69,7 @@ public abstract class AbstractReactorKafkaTest { @BeforeAll static void setUpAll() { kafka = - new KafkaContainer(DockerImageName.parse("confluentinc/cp-kafka:6.2.10")) + new KafkaContainer(DockerImageName.parse("confluentinc/cp-kafka:7.7.0")) .withEnv("KAFKA_HEAP_OPTS", "-Xmx256m") .withLogConsumer(new Slf4jLogConsumer(logger)) .waitingFor(Wait.forLogMessage(".*started \\(kafka.server.KafkaServer\\).*", 1)) @@ -151,7 +152,7 @@ protected void testSingleRecordProcess( trace.hasSpansSatisfyingExactly( span -> span.hasName("producer"), span -> - span.hasName("testTopic send") + span.hasName("testTopic publish") .hasKind(SpanKind.PRODUCER) .hasParent(trace.getSpan(0)) .hasAttributesSatisfyingExactly(sendAttributes(record))); @@ -178,20 +179,22 @@ protected static List sendAttributes(ProducerRecord assertions = new ArrayList<>( asList( - equalTo(SemanticAttributes.MESSAGING_SYSTEM, "kafka"), - equalTo(SemanticAttributes.MESSAGING_DESTINATION_NAME, record.topic()), + equalTo(MessagingIncubatingAttributes.MESSAGING_SYSTEM, "kafka"), + equalTo(MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME, record.topic()), + equalTo(MessagingIncubatingAttributes.MESSAGING_OPERATION, "publish"), satisfies( - SemanticAttributes.MESSAGING_KAFKA_CLIENT_ID, + MessagingIncubatingAttributes.MESSAGING_CLIENT_ID, stringAssert -> stringAssert.startsWith("producer")), satisfies( - SemanticAttributes.MESSAGING_KAFKA_DESTINATION_PARTITION, - AbstractLongAssert::isNotNegative), + MessagingIncubatingAttributes.MESSAGING_DESTINATION_PARTITION_ID, + AbstractStringAssert::isNotEmpty), satisfies( - SemanticAttributes.MESSAGING_KAFKA_MESSAGE_OFFSET, + MessagingIncubatingAttributes.MESSAGING_KAFKA_MESSAGE_OFFSET, AbstractLongAssert::isNotNegative))); String messageKey = record.key(); if (messageKey != null) { - assertions.add(equalTo(SemanticAttributes.MESSAGING_KAFKA_MESSAGE_KEY, messageKey)); + assertions.add( + equalTo(MessagingIncubatingAttributes.MESSAGING_KAFKA_MESSAGE_KEY, messageKey)); } return assertions; } @@ -200,18 +203,15 @@ protected static List receiveAttributes(String topic) { ArrayList assertions = new ArrayList<>( asList( - equalTo(SemanticAttributes.MESSAGING_SYSTEM, "kafka"), - equalTo(SemanticAttributes.MESSAGING_DESTINATION_NAME, topic), - equalTo(SemanticAttributes.MESSAGING_OPERATION, "receive"), + equalTo(MessagingIncubatingAttributes.MESSAGING_SYSTEM, "kafka"), + equalTo(MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME, topic), + equalTo(MessagingIncubatingAttributes.MESSAGING_OPERATION, "receive"), satisfies( - SemanticAttributes.MESSAGING_KAFKA_CLIENT_ID, - stringAssert -> stringAssert.startsWith("consumer")))); - if (Boolean.getBoolean("hasConsumerGroupAndId")) { - assertions.add(equalTo(SemanticAttributes.MESSAGING_KAFKA_CONSUMER_GROUP, "test")); - assertions.add( - satisfies( - SemanticAttributes.MESSAGING_CONSUMER_ID, - stringAssert -> stringAssert.startsWith("test - consumer"))); + MessagingIncubatingAttributes.MESSAGING_CLIENT_ID, + stringAssert -> stringAssert.startsWith("consumer")), + equalTo(MessagingIncubatingAttributes.MESSAGING_BATCH_MESSAGE_COUNT, 1))); + if (Boolean.getBoolean("hasConsumerGroup")) { + assertions.add(equalTo(MessagingIncubatingAttributes.MESSAGING_KAFKA_CONSUMER_GROUP, "test")); } return assertions; } @@ -221,24 +221,20 @@ protected static List processAttributes( List assertions = new ArrayList<>( asList( - equalTo(SemanticAttributes.MESSAGING_SYSTEM, "kafka"), - equalTo(SemanticAttributes.MESSAGING_DESTINATION_NAME, record.topic()), - equalTo(SemanticAttributes.MESSAGING_OPERATION, "process"), + equalTo(MessagingIncubatingAttributes.MESSAGING_SYSTEM, "kafka"), + equalTo(MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME, record.topic()), + equalTo(MessagingIncubatingAttributes.MESSAGING_OPERATION, "process"), satisfies( - SemanticAttributes.MESSAGING_KAFKA_CLIENT_ID, + MessagingIncubatingAttributes.MESSAGING_CLIENT_ID, stringAssert -> stringAssert.startsWith("consumer")), satisfies( - SemanticAttributes.MESSAGING_KAFKA_SOURCE_PARTITION, - AbstractLongAssert::isNotNegative), + MessagingIncubatingAttributes.MESSAGING_DESTINATION_PARTITION_ID, + AbstractStringAssert::isNotEmpty), satisfies( - SemanticAttributes.MESSAGING_KAFKA_MESSAGE_OFFSET, + MessagingIncubatingAttributes.MESSAGING_KAFKA_MESSAGE_OFFSET, AbstractLongAssert::isNotNegative))); - if (Boolean.getBoolean("hasConsumerGroupAndId")) { - assertions.add(equalTo(SemanticAttributes.MESSAGING_KAFKA_CONSUMER_GROUP, "test")); - assertions.add( - satisfies( - SemanticAttributes.MESSAGING_CONSUMER_ID, - stringAssert -> stringAssert.startsWith("test - consumer"))); + if (Boolean.getBoolean("hasConsumerGroup")) { + assertions.add(equalTo(MessagingIncubatingAttributes.MESSAGING_KAFKA_CONSUMER_GROUP, "test")); } if (Boolean.getBoolean("otel.instrumentation.kafka.experimental-span-attributes")) { assertions.add( @@ -246,13 +242,14 @@ protected static List processAttributes( } String messageKey = record.key(); if (messageKey != null) { - assertions.add(equalTo(SemanticAttributes.MESSAGING_KAFKA_MESSAGE_KEY, messageKey)); + assertions.add( + equalTo(MessagingIncubatingAttributes.MESSAGING_KAFKA_MESSAGE_KEY, messageKey)); } String messageValue = record.value(); if (messageValue != null) { assertions.add( equalTo( - SemanticAttributes.MESSAGING_MESSAGE_PAYLOAD_SIZE_BYTES, + MessagingIncubatingAttributes.MESSAGING_MESSAGE_BODY_SIZE, messageValue.getBytes(StandardCharsets.UTF_8).length)); } return assertions; diff --git a/instrumentation/reactor/reactor-netty/README.md b/instrumentation/reactor/reactor-netty/README.md index b1717e45a552..413d40321ecb 100644 --- a/instrumentation/reactor/reactor-netty/README.md +++ b/instrumentation/reactor/reactor-netty/README.md @@ -1,5 +1,5 @@ # Settings for the Reactor Netty instrumentation | System property | Type | Default | Description | -|-------------------------------------------------------------------|---------|---------|----------------------------------------------------------| +| ----------------------------------------------------------------- | ------- | ------- | -------------------------------------------------------- | | `otel.instrumentation.reactor-netty.connection-telemetry.enabled` | Boolean | `false` | Enable the creation of Connect and DNS spans by default. | diff --git a/instrumentation/reactor/reactor-netty/reactor-netty-0.9/javaagent/build.gradle.kts b/instrumentation/reactor/reactor-netty/reactor-netty-0.9/javaagent/build.gradle.kts index a15491f4680b..13d925e5b9d8 100644 --- a/instrumentation/reactor/reactor-netty/reactor-netty-0.9/javaagent/build.gradle.kts +++ b/instrumentation/reactor/reactor-netty/reactor-netty-0.9/javaagent/build.gradle.kts @@ -38,6 +38,7 @@ tasks { } include("**/ReactorNettyConnectionSpanTest.*") jvmArgs("-Dotel.instrumentation.netty.connection-telemetry.enabled=true") + jvmArgs("-Dotel.instrumentation.common.experimental.controller-telemetry.enabled=true") } test { diff --git a/instrumentation/reactor/reactor-netty/reactor-netty-0.9/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/reactornetty/v0_9/AbstractReactorNettyHttpClientTest.java b/instrumentation/reactor/reactor-netty/reactor-netty-0.9/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/reactornetty/v0_9/AbstractReactorNettyHttpClientTest.java index 8e07147da59e..bbaf5a20ed73 100644 --- a/instrumentation/reactor/reactor-netty/reactor-netty-0.9/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/reactornetty/v0_9/AbstractReactorNettyHttpClientTest.java +++ b/instrumentation/reactor/reactor-netty/reactor-netty-0.9/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/reactornetty/v0_9/AbstractReactorNettyHttpClientTest.java @@ -23,7 +23,7 @@ import io.opentelemetry.instrumentation.testing.junit.http.HttpClientTestOptions; import io.opentelemetry.sdk.trace.data.SpanData; import io.opentelemetry.sdk.trace.data.StatusData; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.opentelemetry.semconv.ServerAttributes; import java.net.URI; import java.util.HashSet; import java.util.Map; @@ -41,8 +41,6 @@ abstract class AbstractReactorNettyHttpClientTest @RegisterExtension static final InstrumentationExtension testing = HttpClientInstrumentationExtension.forAgent(); - static final String USER_AGENT = "ReactorNetty"; - abstract HttpClient createHttpClient(boolean readTimeout); @Override @@ -92,7 +90,7 @@ public void sendRequestWithCallback( @Override protected void configure(HttpClientTestOptions.Builder optionsBuilder) { optionsBuilder.disableTestRedirects(); - optionsBuilder.setUserAgent(USER_AGENT); + optionsBuilder.spanEndsAfterBody(); optionsBuilder.setExpectedClientSpanNameMapper( (uri, method) -> { @@ -128,8 +126,8 @@ protected void configure(HttpClientTestOptions.Builder optionsBuilder) { Set> attributes = new HashSet<>(HttpClientTestOptions.DEFAULT_HTTP_ATTRIBUTES); - attributes.remove(SemanticAttributes.NET_PEER_NAME); - attributes.remove(SemanticAttributes.NET_PEER_PORT); + attributes.remove(ServerAttributes.SERVER_ADDRESS); + attributes.remove(ServerAttributes.SERVER_PORT); return attributes; }); } diff --git a/instrumentation/reactor/reactor-netty/reactor-netty-0.9/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/reactornetty/v0_9/ReactorNettyConnectionSpanTest.java b/instrumentation/reactor/reactor-netty/reactor-netty-0.9/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/reactornetty/v0_9/ReactorNettyConnectionSpanTest.java index 5be7e5063c4b..4a1bf05b125a 100644 --- a/instrumentation/reactor/reactor-netty/reactor-netty-0.9/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/reactornetty/v0_9/ReactorNettyConnectionSpanTest.java +++ b/instrumentation/reactor/reactor-netty/reactor-netty-0.9/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/reactornetty/v0_9/ReactorNettyConnectionSpanTest.java @@ -10,7 +10,6 @@ import static io.opentelemetry.api.trace.SpanKind.SERVER; import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.satisfies; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.NetTransportValues.IP_TCP; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.catchThrowable; @@ -19,7 +18,9 @@ import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; import io.opentelemetry.instrumentation.testing.junit.http.HttpClientTestServer; import io.opentelemetry.sdk.trace.data.StatusData; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.opentelemetry.semconv.NetworkAttributes; +import io.opentelemetry.semconv.ServerAttributes; +import org.assertj.core.api.AbstractLongAssert; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -76,18 +77,21 @@ void testSuccessfulRequest() { .hasKind(INTERNAL) .hasParent(trace.getSpan(0)) .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.NET_TRANSPORT, IP_TCP), - equalTo(SemanticAttributes.NET_PEER_NAME, "localhost"), - equalTo(SemanticAttributes.NET_PEER_PORT, server.httpPort())), + equalTo(ServerAttributes.SERVER_ADDRESS, "localhost"), + equalTo(ServerAttributes.SERVER_PORT, server.httpPort())), span -> span.hasName("CONNECT") .hasKind(INTERNAL) .hasParent(trace.getSpan(0)) .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.NET_TRANSPORT, IP_TCP), - equalTo(SemanticAttributes.NET_PEER_NAME, "localhost"), - equalTo(SemanticAttributes.NET_PEER_PORT, server.httpPort()), - equalTo(SemanticAttributes.NET_SOCK_PEER_ADDR, "127.0.0.1")), + equalTo(NetworkAttributes.NETWORK_TRANSPORT, "tcp"), + equalTo(NetworkAttributes.NETWORK_TYPE, "ipv4"), + equalTo(ServerAttributes.SERVER_ADDRESS, "localhost"), + equalTo(ServerAttributes.SERVER_PORT, server.httpPort()), + equalTo(NetworkAttributes.NETWORK_PEER_ADDRESS, "127.0.0.1"), + satisfies( + NetworkAttributes.NETWORK_PEER_PORT, + AbstractLongAssert::isNotNegative)), span -> span.hasName("GET").hasKind(CLIENT).hasParent(trace.getSpan(0)), span -> span.hasName("test-http-server").hasKind(SERVER).hasParent(trace.getSpan(3)))); @@ -133,9 +137,8 @@ void testFailingRequest() { .hasKind(INTERNAL) .hasParent(trace.getSpan(0)) .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.NET_TRANSPORT, IP_TCP), - equalTo(SemanticAttributes.NET_PEER_NAME, "localhost"), - equalTo(SemanticAttributes.NET_PEER_PORT, PortUtils.UNUSABLE_PORT)), + equalTo(ServerAttributes.SERVER_ADDRESS, "localhost"), + equalTo(ServerAttributes.SERVER_PORT, PortUtils.UNUSABLE_PORT)), span -> span.hasName("CONNECT") .hasKind(INTERNAL) @@ -143,11 +146,16 @@ void testFailingRequest() { .hasStatus(StatusData.error()) .hasException(connectException) .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.NET_TRANSPORT, IP_TCP), - equalTo(SemanticAttributes.NET_PEER_NAME, "localhost"), - equalTo(SemanticAttributes.NET_PEER_PORT, PortUtils.UNUSABLE_PORT), + equalTo(NetworkAttributes.NETWORK_TRANSPORT, "tcp"), + satisfies( + NetworkAttributes.NETWORK_TYPE, val -> val.isIn(null, "ipv4")), + equalTo(ServerAttributes.SERVER_ADDRESS, "localhost"), + equalTo(ServerAttributes.SERVER_PORT, PortUtils.UNUSABLE_PORT), + satisfies( + NetworkAttributes.NETWORK_PEER_ADDRESS, + val -> val.isIn(null, "127.0.0.1")), satisfies( - SemanticAttributes.NET_SOCK_PEER_ADDR, - val -> val.isIn(null, "127.0.0.1"))))); + NetworkAttributes.NETWORK_PEER_PORT, + val -> val.isIn(null, (long) PortUtils.UNUSABLE_PORT))))); } } diff --git a/instrumentation/reactor/reactor-netty/reactor-netty-1.0/javaagent-unit-tests/build.gradle.kts b/instrumentation/reactor/reactor-netty/reactor-netty-1.0/javaagent-unit-tests/build.gradle.kts index 653d5f5fbae1..d1d70c210673 100644 --- a/instrumentation/reactor/reactor-netty/reactor-netty-1.0/javaagent-unit-tests/build.gradle.kts +++ b/instrumentation/reactor/reactor-netty/reactor-netty-1.0/javaagent-unit-tests/build.gradle.kts @@ -4,4 +4,5 @@ plugins { dependencies { testImplementation(project(":instrumentation:reactor:reactor-netty:reactor-netty-1.0:javaagent")) + testImplementation("io.projectreactor.netty:reactor-netty-http:1.0.0") } diff --git a/instrumentation/reactor/reactor-netty/reactor-netty-1.0/javaagent-unit-tests/src/test/java/io/opentelemetry/javaagent/instrumentation/reactornetty/v1_0/FailedRequestWithUrlMakerTest.java b/instrumentation/reactor/reactor-netty/reactor-netty-1.0/javaagent-unit-tests/src/test/java/io/opentelemetry/javaagent/instrumentation/reactornetty/v1_0/FailedRequestWithUrlMakerTest.java new file mode 100644 index 000000000000..f5fe58794b2b --- /dev/null +++ b/instrumentation/reactor/reactor-netty/reactor-netty-1.0/javaagent-unit-tests/src/test/java/io/opentelemetry/javaagent/instrumentation/reactornetty/v1_0/FailedRequestWithUrlMakerTest.java @@ -0,0 +1,94 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.reactornetty.v1_0; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.params.provider.Arguments.arguments; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.when; + +import java.net.InetSocketAddress; +import java.util.function.Supplier; +import java.util.stream.Stream; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +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.junit.jupiter.params.provider.ValueSource; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import reactor.netty.http.client.HttpClientConfig; +import reactor.netty.http.client.HttpClientRequest; + +@ExtendWith(MockitoExtension.class) +class FailedRequestWithUrlMakerTest { + + @Mock HttpClientConfig config; + @Mock HttpClientRequest originalRequest; + + @Test + void shouldUseAbsoluteUri() { + when(config.uri()).thenReturn("https://opentelemetry.io"); + + HttpClientRequest request = FailedRequestWithUrlMaker.create(config, originalRequest); + + assertThat(request.resourceUrl()).isEqualTo("https://opentelemetry.io"); + } + + @ParameterizedTest + @ValueSource(strings = {"https://opentelemetry.io", "https://opentelemetry.io/"}) + void shouldPrependBaseUrl(String baseUrl) { + when(config.baseUrl()).thenReturn(baseUrl); + when(config.uri()).thenReturn("/docs"); + + HttpClientRequest request = FailedRequestWithUrlMaker.create(config, originalRequest); + + assertThat(request.resourceUrl()).isEqualTo("https://opentelemetry.io/docs"); + } + + @Test + @SuppressWarnings("MockitoDoSetup") + void shouldPrependRemoteAddress() { + when(config.baseUrl()).thenReturn("/"); + when(config.uri()).thenReturn("/docs"); + Supplier remoteAddress = + () -> InetSocketAddress.createUnresolved("opentelemetry.io", 8080); + doReturn(remoteAddress).when(config).remoteAddress(); + when(config.isSecure()).thenReturn(true); + + HttpClientRequest request = FailedRequestWithUrlMaker.create(config, originalRequest); + + assertThat(request.resourceUrl()).isEqualTo("https://opentelemetry.io:8080/docs"); + } + + @ParameterizedTest + @ArgumentsSource(DefaultPortsArguments.class) + @SuppressWarnings("MockitoDoSetup") + void shouldSkipDefaultPorts(int port, boolean isSecure) { + when(config.baseUrl()).thenReturn("/"); + when(config.uri()).thenReturn("/docs"); + Supplier remoteAddress = + () -> InetSocketAddress.createUnresolved("opentelemetry.io", port); + doReturn(remoteAddress).when(config).remoteAddress(); + when(config.isSecure()).thenReturn(isSecure); + + HttpClientRequest request = FailedRequestWithUrlMaker.create(config, originalRequest); + + assertThat(request.resourceUrl()) + .isEqualTo((isSecure ? "https" : "http") + "://opentelemetry.io/docs"); + } + + static final class DefaultPortsArguments implements ArgumentsProvider { + + @Override + public Stream provideArguments(ExtensionContext extensionContext) { + return Stream.of(arguments(80, false), arguments(443, true)); + } + } +} diff --git a/instrumentation/reactor/reactor-netty/reactor-netty-1.0/javaagent/build.gradle.kts b/instrumentation/reactor/reactor-netty/reactor-netty-1.0/javaagent/build.gradle.kts index 68e48491aa9b..d2c002fdac9c 100644 --- a/instrumentation/reactor/reactor-netty/reactor-netty-1.0/javaagent/build.gradle.kts +++ b/instrumentation/reactor/reactor-netty/reactor-netty-1.0/javaagent/build.gradle.kts @@ -35,7 +35,10 @@ dependencies { testInstrumentation(project(":instrumentation:netty:netty-4.1:javaagent")) testInstrumentation(project(":instrumentation:reactor:reactor-3.1:javaagent")) - testLibrary("io.projectreactor:reactor-test:3.1.0.RELEASE") + // using 3.4.3 to avoid the "Spec. Rule 1.3" issue in reactor-core during tests + // https://github.com/reactor/reactor-core/issues/2579 + testLibrary("io.projectreactor:reactor-test:3.4.3") + testLibrary("io.projectreactor:reactor-core:3.4.3") testImplementation(project(":instrumentation-annotations")) latestDepTestLibrary("io.projectreactor:reactor-core:3.4.+") @@ -51,6 +54,7 @@ tasks { include("**/ReactorNettyConnectionSpanTest.*", "**/ReactorNettyClientSslTest.*") jvmArgs("-Dotel.instrumentation.netty.ssl-telemetry.enabled=true") jvmArgs("-Dotel.instrumentation.reactor-netty.connection-telemetry.enabled=true") + jvmArgs("-Dotel.instrumentation.common.experimental.controller-telemetry.enabled=true") } test { diff --git a/instrumentation/reactor/reactor-netty/reactor-netty-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/reactornetty/v1_0/DecoratorFunctions.java b/instrumentation/reactor/reactor-netty/reactor-netty-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/reactornetty/v1_0/DecoratorFunctions.java index 17f3ec75e4b8..d806c76e796b 100644 --- a/instrumentation/reactor/reactor-netty/reactor-netty-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/reactornetty/v1_0/DecoratorFunctions.java +++ b/instrumentation/reactor/reactor-netty/reactor-netty-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/reactornetty/v1_0/DecoratorFunctions.java @@ -74,12 +74,19 @@ public void accept(M message, Throwable throwable) { @Nullable private static Context getChannelContext( ContextView contextView, PropagatedContext propagatedContext) { + + InstrumentationContexts contexts = + contextView.getOrDefault(ReactorContextKeys.CONTEXTS_HOLDER_KEY, null); + if (contexts == null) { + return null; + } + Context context = null; if (propagatedContext.useClientContext) { - context = contextView.getOrDefault(ReactorContextKeys.CLIENT_CONTEXT_KEY, null); + context = contexts.getClientContext(); } if (context == null) { - context = contextView.getOrDefault(ReactorContextKeys.CLIENT_PARENT_CONTEXT_KEY, null); + context = contexts.getParentContext(); } return context; } diff --git a/instrumentation/reactor/reactor-netty/reactor-netty-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/reactornetty/v1_0/FailedRequestWithUrlMaker.java b/instrumentation/reactor/reactor-netty/reactor-netty-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/reactornetty/v1_0/FailedRequestWithUrlMaker.java new file mode 100644 index 000000000000..1b033346674b --- /dev/null +++ b/instrumentation/reactor/reactor-netty/reactor-netty-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/reactornetty/v1_0/FailedRequestWithUrlMaker.java @@ -0,0 +1,90 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.reactornetty.v1_0; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import reactor.netty.http.client.HttpClientConfig; +import reactor.netty.http.client.HttpClientRequest; + +final class FailedRequestWithUrlMaker { + + static HttpClientRequest create(HttpClientConfig config, HttpClientRequest failedRequest) { + return (HttpClientRequest) + Proxy.newProxyInstance( + FailedRequestWithUrlMaker.class.getClassLoader(), + new Class[] {HttpClientRequest.class}, + new HttpRequestInvocationHandler(config, failedRequest)); + } + + private static final class HttpRequestInvocationHandler implements InvocationHandler { + + private final HttpClientConfig config; + private final HttpClientRequest failedRequest; + + private HttpRequestInvocationHandler(HttpClientConfig config, HttpClientRequest failedRequest) { + this.config = config; + this.failedRequest = failedRequest; + } + + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + if ("resourceUrl".equals(method.getName())) { + return computeUrlFromConfig(); + } + try { + return method.invoke(failedRequest, args); + } catch (InvocationTargetException exception) { + throw exception.getCause(); + } + } + + private String computeUrlFromConfig() { + String uri = config.uri(); + if (isAbsolute(uri)) { + return uri; + } + + // use the baseUrl if it was configured + String baseUrl = config.baseUrl(); + // baseUrl is an actual scheme+host+port base url, and not just "/" + if (baseUrl != null && baseUrl.length() > 1) { + if (baseUrl.endsWith("/")) { + baseUrl = baseUrl.substring(0, baseUrl.length() - 1); + } + return baseUrl + uri; + } + + // otherwise, use the host+port config to construct the full url + SocketAddress hostAddress = config.remoteAddress().get(); + if (hostAddress instanceof InetSocketAddress) { + InetSocketAddress inetHostAddress = (InetSocketAddress) hostAddress; + return (config.isSecure() ? "https://" : "http://") + + inetHostAddress.getHostString() + + computePortPart(inetHostAddress.getPort()) + + uri; + } + + return uri; + } + + private static boolean isAbsolute(String uri) { + return uri != null && !uri.isEmpty() && !uri.startsWith("/"); + } + + private String computePortPart(int port) { + boolean defaultPortValue = + (config.isSecure() && port == 443) || (!config.isSecure() && port == 80); + return defaultPortValue ? "" : (":" + port); + } + } + + private FailedRequestWithUrlMaker() {} +} diff --git a/instrumentation/reactor/reactor-netty/reactor-netty-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/reactornetty/v1_0/HttpClientConnectInstrumentation.java b/instrumentation/reactor/reactor-netty/reactor-netty-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/reactornetty/v1_0/HttpClientConnectInstrumentation.java new file mode 100644 index 000000000000..089f0bedd12d --- /dev/null +++ b/instrumentation/reactor/reactor-netty/reactor-netty-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/reactornetty/v1_0/HttpClientConnectInstrumentation.java @@ -0,0 +1,53 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.reactornetty.v1_0; + +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.matcher.ElementMatcher; +import reactor.core.publisher.Mono; +import reactor.netty.Connection; +import reactor.netty.http.client.HttpClient; +import reactor.netty.http.client.HttpClientConfig; +import reactor.netty.http.client.HttpClientConfigBuddy; + +public class HttpClientConnectInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher typeMatcher() { + return named("reactor.netty.http.client.HttpClientConnect"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + named("connect").and(returns(named("reactor.core.publisher.Mono"))), + this.getClass().getName() + "$ConnectAdvice"); + } + + @SuppressWarnings("unused") + public static class ConnectAdvice { + + @Advice.OnMethodExit(suppress = Throwable.class) + public static void onExit( + @Advice.Return(readOnly = false) Mono connection, + @Advice.This HttpClient httpClient) { + + HttpClientConfig config = httpClient.configuration(); + // reactor-netty 1.0.x has a bug: the .mapConnect() function is not applied when deferred + // configuration is used + // we're fixing this bug here, so that our instrumentation can safely add its own + // .mapConnect() listener + if (HttpClientConfigBuddy.hasDeferredConfig(config)) { + connection = HttpClientConfigBuddy.getConnector(config).apply(connection); + } + } + } +} diff --git a/instrumentation/reactor/reactor-netty/reactor-netty-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/reactornetty/v1_0/HttpResponseReceiverInstrumenter.java b/instrumentation/reactor/reactor-netty/reactor-netty-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/reactornetty/v1_0/HttpResponseReceiverInstrumenter.java index 05169d73a63b..0804f184632f 100644 --- a/instrumentation/reactor/reactor-netty/reactor-netty-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/reactornetty/v1_0/HttpResponseReceiverInstrumenter.java +++ b/instrumentation/reactor/reactor-netty/reactor-netty-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/reactornetty/v1_0/HttpResponseReceiverInstrumenter.java @@ -5,15 +5,11 @@ package io.opentelemetry.javaagent.instrumentation.reactornetty.v1_0; -import static io.opentelemetry.javaagent.instrumentation.reactornetty.v1_0.ReactorContextKeys.CLIENT_CONTEXT_KEY; -import static io.opentelemetry.javaagent.instrumentation.reactornetty.v1_0.ReactorContextKeys.CLIENT_PARENT_CONTEXT_KEY; -import static io.opentelemetry.javaagent.instrumentation.reactornetty.v1_0.ReactorNettySingletons.instrumenter; +import static io.opentelemetry.javaagent.instrumentation.reactornetty.v1_0.ReactorContextKeys.CONTEXTS_HOLDER_KEY; -import io.opentelemetry.api.GlobalOpenTelemetry; import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.semconv.http.HttpClientRequestResendCount; import io.opentelemetry.instrumentation.netty.v4_1.NettyClientTelemetry; -import io.opentelemetry.instrumentation.reactor.v3_1.ContextPropagationOperator; -import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; import java.util.function.BiConsumer; import java.util.function.Function; import javax.annotation.Nullable; @@ -29,8 +25,7 @@ public final class HttpResponseReceiverInstrumenter { // this method adds several stateful listeners that execute the instrumenter lifecycle during HTTP // request processing // it should be used just before one of the response*() methods is called - after this point the - // HTTP - // request is no longer modifiable by the user + // HTTP request is no longer modifiable by the user @Nullable public static HttpClient.ResponseReceiver instrument(HttpClient.ResponseReceiver receiver) { // receiver should always be an HttpClientFinalizer, which both extends HttpClient and @@ -39,15 +34,18 @@ public static HttpClient.ResponseReceiver instrument(HttpClient.ResponseRecei HttpClient client = (HttpClient) receiver; HttpClientConfig config = client.configuration(); - ContextHolder contextHolder = new ContextHolder(); + InstrumentationContexts instrumentationContexts = new InstrumentationContexts(); HttpClient modified = client - .mapConnect(new StartOperation(contextHolder, config)) - .doOnRequest(new PropagateContext(contextHolder)) - .doOnRequestError(new EndOperationWithRequestError(contextHolder, config)) - .doOnResponseError(new EndOperationWithResponseError(contextHolder, config)) - .doAfterResponseSuccess(new EndOperationWithSuccess(contextHolder, config)); + .mapConnect(new CaptureParentContext(instrumentationContexts)) + .doOnRequestError(new EndOperationWithRequestError(config, instrumentationContexts)) + .doOnRequest(new StartOperation(instrumentationContexts)) + .doOnResponseError(new EndOperationWithResponseError(instrumentationContexts)) + .doAfterResponseSuccess(new EndOperationWithSuccess(instrumentationContexts)) + // end the current span on redirects; StartOperation will start another one for the + // next resend + .doOnRedirect(new EndOperationWithSuccess(instrumentationContexts)); // modified should always be an HttpClientFinalizer too if (modified instanceof HttpClient.ResponseReceiver) { @@ -58,32 +56,13 @@ public static HttpClient.ResponseReceiver instrument(HttpClient.ResponseRecei return null; } - static final class ContextHolder { - - private static final AtomicReferenceFieldUpdater contextUpdater = - AtomicReferenceFieldUpdater.newUpdater(ContextHolder.class, Context.class, "context"); - - volatile Context parentContext; - volatile Context context; - - void setContext(Context context) { - contextUpdater.set(this, context); - } - - Context getAndRemoveContext() { - return contextUpdater.getAndSet(this, null); - } - } - - static final class StartOperation + private static final class CaptureParentContext implements Function, Mono> { - private final ContextHolder contextHolder; - private final HttpClientConfig config; + private final InstrumentationContexts instrumentationContexts; - StartOperation(ContextHolder contextHolder, HttpClientConfig config) { - this.contextHolder = contextHolder; - this.config = config; + CaptureParentContext(InstrumentationContexts instrumentationContexts) { + this.instrumentationContexts = instrumentationContexts; } @Override @@ -91,118 +70,90 @@ public Mono apply(Mono mono) { return Mono.defer( () -> { Context parentContext = Context.current(); - contextHolder.parentContext = parentContext; - if (!instrumenter().shouldStart(parentContext, config)) { - // make context accessible via the reactor ContextView - the doOn* callbacks - // instrumentation uses this to set the proper context for callbacks - return mono.contextWrite( - ctx -> ctx.put(CLIENT_PARENT_CONTEXT_KEY, parentContext)); - } - - Context context = instrumenter().start(parentContext, config); - contextHolder.setContext(context); - return ContextPropagationOperator.runWithContext(mono, context) - // make contexts accessible via the reactor ContextView - the doOn* callbacks - // instrumentation uses the parent context to set the proper context for - // callbacks - .contextWrite(ctx -> ctx.put(CLIENT_PARENT_CONTEXT_KEY, parentContext)) - .contextWrite(ctx -> ctx.put(CLIENT_CONTEXT_KEY, context)); + instrumentationContexts.initialize(parentContext); + // make contexts accessible via the reactor ContextView - the doOn* callbacks + // instrumentation uses this to set the proper context for callbacks + return mono.contextWrite( + ctx -> ctx.put(CONTEXTS_HOLDER_KEY, instrumentationContexts)); }) - .doOnCancel( - () -> { - Context context = contextHolder.getAndRemoveContext(); - if (context == null) { - return; - } - instrumenter().end(context, config, null, null); - }); + // if there's still any span in flight, end it + .doOnCancel(() -> instrumentationContexts.endClientSpan(null, null)); } } - static final class PropagateContext implements BiConsumer { + private static final class StartOperation implements BiConsumer { - private final ContextHolder contextHolder; + private final InstrumentationContexts instrumentationContexts; - PropagateContext(ContextHolder contextHolder) { - this.contextHolder = contextHolder; + StartOperation(InstrumentationContexts instrumentationContexts) { + this.instrumentationContexts = instrumentationContexts; } @Override - public void accept(HttpClientRequest httpClientRequest, Connection connection) { - Context context = contextHolder.context; - if (context != null) { - GlobalOpenTelemetry.getPropagators() - .getTextMapPropagator() - .inject(context, httpClientRequest, HttpClientRequestHeadersSetter.INSTANCE); - } + public void accept(HttpClientRequest request, Connection connection) { + Context context = instrumentationContexts.startClientSpan(request); // also propagate the context to the underlying netty instrumentation // if this span was suppressed and context is null, propagate parentContext - this will allow // netty spans to be suppressed too - Context nettyParentContext = context == null ? contextHolder.parentContext : context; + Context nettyParentContext = + context == null ? instrumentationContexts.getParentContext() : context; NettyClientTelemetry.setChannelContext(connection.channel(), nettyParentContext); } } - static final class EndOperationWithRequestError + private static final class EndOperationWithRequestError implements BiConsumer { - private final ContextHolder contextHolder; private final HttpClientConfig config; + private final InstrumentationContexts instrumentationContexts; - EndOperationWithRequestError(ContextHolder contextHolder, HttpClientConfig config) { - this.contextHolder = contextHolder; + EndOperationWithRequestError( + HttpClientConfig config, InstrumentationContexts instrumentationContexts) { this.config = config; + this.instrumentationContexts = instrumentationContexts; } @Override - public void accept(HttpClientRequest httpClientRequest, Throwable error) { - Context context = contextHolder.getAndRemoveContext(); - if (context == null) { - return; + public void accept(HttpClientRequest request, Throwable error) { + instrumentationContexts.endClientSpan(null, error); + + if (HttpClientRequestResendCount.get(instrumentationContexts.getParentContext()) == 0) { + // request is an instance of FailedHttpClientRequest, which does not implement a correct + // resourceUrl() method -- we have to work around that + request = FailedRequestWithUrlMaker.create(config, request); + instrumentationContexts.startAndEndConnectionErrorSpan(request, error); } - instrumenter().end(context, config, null, error); } } - static final class EndOperationWithResponseError + private static final class EndOperationWithResponseError implements BiConsumer { - private final ContextHolder contextHolder; - private final HttpClientConfig config; + private final InstrumentationContexts instrumentationContexts; - EndOperationWithResponseError(ContextHolder contextHolder, HttpClientConfig config) { - this.contextHolder = contextHolder; - this.config = config; + EndOperationWithResponseError(InstrumentationContexts instrumentationContexts) { + this.instrumentationContexts = instrumentationContexts; } @Override public void accept(HttpClientResponse response, Throwable error) { - Context context = contextHolder.getAndRemoveContext(); - if (context == null) { - return; - } - instrumenter().end(context, config, response, error); + instrumentationContexts.endClientSpan(response, error); } } - static final class EndOperationWithSuccess implements BiConsumer { + private static final class EndOperationWithSuccess + implements BiConsumer { - private final ContextHolder contextHolder; - private final HttpClientConfig config; + private final InstrumentationContexts instrumentationContexts; - EndOperationWithSuccess(ContextHolder contextHolder, HttpClientConfig config) { - this.contextHolder = contextHolder; - this.config = config; + EndOperationWithSuccess(InstrumentationContexts instrumentationContexts) { + this.instrumentationContexts = instrumentationContexts; } @Override public void accept(HttpClientResponse response, Connection connection) { - Context context = contextHolder.getAndRemoveContext(); - if (context == null) { - return; - } - instrumenter().end(context, config, response, null); + instrumentationContexts.endClientSpan(response, null); } } diff --git a/instrumentation/reactor/reactor-netty/reactor-netty-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/reactornetty/v1_0/InstrumentationContexts.java b/instrumentation/reactor/reactor-netty/reactor-netty-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/reactornetty/v1_0/InstrumentationContexts.java new file mode 100644 index 000000000000..e81a2d6eef7a --- /dev/null +++ b/instrumentation/reactor/reactor-netty/reactor-netty-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/reactornetty/v1_0/InstrumentationContexts.java @@ -0,0 +1,104 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.reactornetty.v1_0; + +import static io.opentelemetry.javaagent.instrumentation.reactornetty.v1_0.ReactorNettySingletons.instrumenter; + +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.internal.InstrumenterUtil; +import io.opentelemetry.instrumentation.api.internal.Timer; +import io.opentelemetry.instrumentation.api.semconv.http.HttpClientRequestResendCount; +import io.opentelemetry.instrumentation.api.util.VirtualField; +import java.util.Queue; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; +import javax.annotation.Nullable; +import reactor.netty.http.client.HttpClientRequest; +import reactor.netty.http.client.HttpClientResponse; + +final class InstrumentationContexts { + private static final VirtualField requestContextVirtualField = + VirtualField.find(HttpClientRequest.class, Context.class); + + private static final AtomicReferenceFieldUpdater + parentContextUpdater = + AtomicReferenceFieldUpdater.newUpdater( + InstrumentationContexts.class, Context.class, "parentContext"); + + private volatile Context parentContext; + private volatile Timer timer; + // on retries, reactor-netty starts the next resend attempt before it ends the previous one (i.e. + // it calls the callback functions in that order); thus for a short moment there can be multiple + // coexisting HTTP client spans + private final Queue clientContexts = new LinkedBlockingQueue<>(); + + void initialize(Context parentContext) { + Context parentContextWithResends = HttpClientRequestResendCount.initialize(parentContext); + // make sure initialization happens only once + if (parentContextUpdater.compareAndSet(this, null, parentContextWithResends)) { + timer = Timer.start(); + } + } + + Context getParentContext() { + return parentContext; + } + + @Nullable + Context getClientContext() { + RequestAndContext requestAndContext = clientContexts.peek(); + return requestAndContext == null ? null : requestAndContext.context; + } + + @Nullable + Context startClientSpan(HttpClientRequest request) { + Context parentContext = this.parentContext; + Context context = null; + if (instrumenter().shouldStart(parentContext, request)) { + context = instrumenter().start(parentContext, request); + requestContextVirtualField.set(request, context); + clientContexts.offer(new RequestAndContext(request, context)); + } + return context; + } + + void endClientSpan(@Nullable HttpClientResponse response, @Nullable Throwable error) { + HttpClientRequest request = null; + Context context = null; + RequestAndContext requestAndContext = clientContexts.poll(); + if (response instanceof HttpClientRequest) { + request = (HttpClientRequest) response; + context = requestContextVirtualField.get(request); + } else if (requestAndContext != null) { + // this branch is taken when there was an error (e.g. timeout) and response was null + request = requestAndContext.request; + context = requestAndContext.context; + } + + if (request != null && context != null) { + instrumenter().end(context, request, response, error); + } + } + + void startAndEndConnectionErrorSpan(HttpClientRequest request, Throwable error) { + Context parentContext = this.parentContext; + if (instrumenter().shouldStart(parentContext, request)) { + Timer timer = this.timer; + InstrumenterUtil.startAndEnd( + instrumenter(), parentContext, request, null, error, timer.startTime(), timer.now()); + } + } + + static final class RequestAndContext { + final HttpClientRequest request; + final Context context; + + RequestAndContext(HttpClientRequest request, Context context) { + this.request = request; + this.context = context; + } + } +} diff --git a/instrumentation/reactor/reactor-netty/reactor-netty-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/reactornetty/v1_0/ReactorContextKeys.java b/instrumentation/reactor/reactor-netty/reactor-netty-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/reactornetty/v1_0/ReactorContextKeys.java index d76ceb43c779..25f754499052 100644 --- a/instrumentation/reactor/reactor-netty/reactor-netty-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/reactornetty/v1_0/ReactorContextKeys.java +++ b/instrumentation/reactor/reactor-netty/reactor-netty-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/reactornetty/v1_0/ReactorContextKeys.java @@ -7,10 +7,8 @@ public final class ReactorContextKeys { - public static final String CLIENT_PARENT_CONTEXT_KEY = - ReactorContextKeys.class.getName() + ".client-parent-context"; - public static final String CLIENT_CONTEXT_KEY = - ReactorContextKeys.class.getName() + ".client-context"; + public static final String CONTEXTS_HOLDER_KEY = + ReactorContextKeys.class.getName() + ".contexts-holder"; private ReactorContextKeys() {} } diff --git a/instrumentation/reactor/reactor-netty/reactor-netty-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/reactornetty/v1_0/ReactorNettyHttpClientAttributesGetter.java b/instrumentation/reactor/reactor-netty/reactor-netty-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/reactornetty/v1_0/ReactorNettyHttpClientAttributesGetter.java index 458812d11889..2aa7a163d351 100644 --- a/instrumentation/reactor/reactor-netty/reactor-netty-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/reactornetty/v1_0/ReactorNettyHttpClientAttributesGetter.java +++ b/instrumentation/reactor/reactor-netty/reactor-netty-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/reactornetty/v1_0/ReactorNettyHttpClientAttributesGetter.java @@ -5,77 +5,110 @@ package io.opentelemetry.javaagent.instrumentation.reactornetty.v1_0; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpClientAttributesGetter; +import io.netty.handler.codec.http.HttpVersion; +import io.opentelemetry.instrumentation.api.semconv.http.HttpClientAttributesGetter; import java.net.InetSocketAddress; import java.net.SocketAddress; import java.util.List; import javax.annotation.Nullable; -import reactor.netty.http.client.HttpClientConfig; +import reactor.netty.Connection; +import reactor.netty.http.client.HttpClientRequest; import reactor.netty.http.client.HttpClientResponse; final class ReactorNettyHttpClientAttributesGetter - implements HttpClientAttributesGetter { + implements HttpClientAttributesGetter { @Override - public String getUrlFull(HttpClientConfig request) { - String uri = request.uri(); - if (isAbsolute(uri)) { - return uri; - } + public String getUrlFull(HttpClientRequest request) { + return request.resourceUrl(); + } - // use the baseUrl if it was configured - String baseUrl = request.baseUrl(); + @Override + public String getHttpRequestMethod(HttpClientRequest request) { + return request.method().name(); + } - if (uri == null) { - // internally reactor netty appends "/" to the baseUrl - return baseUrl.endsWith("/") ? baseUrl : baseUrl + "/"; - } + @Override + public List getHttpRequestHeader(HttpClientRequest request, String name) { + return request.requestHeaders().getAll(name); + } - if (baseUrl != null) { - if (baseUrl.endsWith("/") && uri.startsWith("/")) { - baseUrl = baseUrl.substring(0, baseUrl.length() - 1); - } - return baseUrl + uri; - } + @Override + public Integer getHttpResponseStatusCode( + HttpClientRequest request, HttpClientResponse response, @Nullable Throwable error) { + return response.status().code(); + } - // otherwise, use the host+port config to construct the full url - SocketAddress hostAddress = request.remoteAddress().get(); - if (hostAddress instanceof InetSocketAddress) { - InetSocketAddress inetHostAddress = (InetSocketAddress) hostAddress; - return (request.isSecure() ? "https://" : "http://") - + inetHostAddress.getHostString() - + ":" - + inetHostAddress.getPort() - + (uri.startsWith("/") ? "" : "/") - + uri; - } + @Override + public List getHttpResponseHeader( + HttpClientRequest request, HttpClientResponse response, String name) { + return response.responseHeaders().getAll(name); + } - return uri; + @Nullable + @Override + public String getNetworkProtocolName( + HttpClientRequest request, @Nullable HttpClientResponse response) { + if (response == null) { + return null; + } + return response.version().protocolName(); } - private static boolean isAbsolute(String uri) { - return uri != null && !uri.isEmpty() && !uri.startsWith("/"); + @Nullable + @Override + public String getNetworkProtocolVersion( + HttpClientRequest request, @Nullable HttpClientResponse response) { + if (response == null) { + return null; + } + HttpVersion version = response.version(); + if (version.minorVersion() == 0) { + return Integer.toString(version.majorVersion()); + } + return version.majorVersion() + "." + version.minorVersion(); } + @Nullable @Override - public String getHttpRequestMethod(HttpClientConfig request) { - return request.method().name(); + public String getServerAddress(HttpClientRequest request) { + String resourceUrl = request.resourceUrl(); + return resourceUrl == null ? null : UrlParser.getHost(resourceUrl); } + @Nullable @Override - public List getHttpRequestHeader(HttpClientConfig request, String name) { - return request.headers().getAll(name); + public Integer getServerPort(HttpClientRequest request) { + String resourceUrl = request.resourceUrl(); + return resourceUrl == null ? null : UrlParser.getPort(resourceUrl); } + @Nullable @Override - public Integer getHttpResponseStatusCode( - HttpClientConfig request, HttpClientResponse response, @Nullable Throwable error) { - return response.status().code(); + public InetSocketAddress getNetworkPeerInetSocketAddress( + HttpClientRequest request, @Nullable HttpClientResponse response) { + + // we're making use of the fact that HttpClientOperations is both a Connection and an + // HttpClientResponse + if (response instanceof Connection) { + Connection connection = (Connection) response; + SocketAddress address = connection.channel().remoteAddress(); + if (address instanceof InetSocketAddress) { + return (InetSocketAddress) address; + } + } + return null; } + @Nullable @Override - public List getHttpResponseHeader( - HttpClientConfig request, HttpClientResponse response, String name) { - return response.responseHeaders().getAll(name); + public String getErrorType( + HttpClientRequest request, @Nullable HttpClientResponse response, @Nullable Throwable error) { + // if both response and error are null it means the request has been cancelled -- see the + // ConnectionWrapper class + if (response == null && error == null) { + return "cancelled"; + } + return null; } } diff --git a/instrumentation/reactor/reactor-netty/reactor-netty-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/reactornetty/v1_0/ReactorNettyInstrumentationModule.java b/instrumentation/reactor/reactor-netty/reactor-netty-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/reactornetty/v1_0/ReactorNettyInstrumentationModule.java index 68f14dc4972f..2517d4ccbc69 100644 --- a/instrumentation/reactor/reactor-netty/reactor-netty-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/reactornetty/v1_0/ReactorNettyInstrumentationModule.java +++ b/instrumentation/reactor/reactor-netty/reactor-netty-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/reactornetty/v1_0/ReactorNettyInstrumentationModule.java @@ -7,10 +7,12 @@ import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.hasClassesNamed; import static java.util.Arrays.asList; +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.instrumentation.internal.ExperimentalInstrumentationModule; import java.util.List; import java.util.function.BiConsumer; import java.util.function.Function; @@ -24,7 +26,8 @@ * HttpClient#doOnRequest(BiConsumer)} to pass context from the caller to Reactor to Netty. */ @AutoService(InstrumentationModule.class) -public class ReactorNettyInstrumentationModule extends InstrumentationModule { +public class ReactorNettyInstrumentationModule extends InstrumentationModule + implements ExperimentalInstrumentationModule { public ReactorNettyInstrumentationModule() { super("reactor-netty", "reactor-netty-1.0"); @@ -36,10 +39,21 @@ public ElementMatcher.Junction classLoaderMatcher() { return hasClassesNamed("reactor.netty.transport.AddressUtils"); } + @Override + public boolean isHelperClass(String className) { + return className.startsWith("reactor.netty.http.client.HttpClientConfigBuddy"); + } + + @Override + public List injectedClassNames() { + return singletonList("reactor.netty.http.client.HttpClientConfigBuddy"); + } + @Override public List typeInstrumentations() { return asList( new HttpClientInstrumentation(), + new HttpClientConnectInstrumentation(), new ResponseReceiverInstrumentation(), new TransportConnectorInstrumentation()); } diff --git a/instrumentation/reactor/reactor-netty/reactor-netty-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/reactornetty/v1_0/ReactorNettyNetClientAttributesGetter.java b/instrumentation/reactor/reactor-netty/reactor-netty-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/reactornetty/v1_0/ReactorNettyNetClientAttributesGetter.java deleted file mode 100644 index 15dc513e317e..000000000000 --- a/instrumentation/reactor/reactor-netty/reactor-netty-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/reactornetty/v1_0/ReactorNettyNetClientAttributesGetter.java +++ /dev/null @@ -1,97 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.javaagent.instrumentation.reactornetty.v1_0; - -import io.netty.handler.codec.http.HttpVersion; -import io.opentelemetry.instrumentation.api.instrumenter.net.NetClientAttributesGetter; -import java.net.InetSocketAddress; -import java.net.SocketAddress; -import javax.annotation.Nullable; -import reactor.netty.Connection; -import reactor.netty.http.client.HttpClientConfig; -import reactor.netty.http.client.HttpClientResponse; - -final class ReactorNettyNetClientAttributesGetter - implements NetClientAttributesGetter { - - @Nullable - @Override - public String getNetworkProtocolName( - HttpClientConfig request, @Nullable HttpClientResponse response) { - if (response == null) { - return null; - } - return response.version().protocolName(); - } - - @Nullable - @Override - public String getNetworkProtocolVersion( - HttpClientConfig request, @Nullable HttpClientResponse response) { - if (response == null) { - return null; - } - HttpVersion version = response.version(); - return version.majorVersion() + "." + version.minorVersion(); - } - - @Nullable - @Override - public String getServerAddress(HttpClientConfig request) { - return getHost(request); - } - - @Nullable - @Override - public Integer getServerPort(HttpClientConfig request) { - return getPort(request); - } - - @Nullable - @Override - public InetSocketAddress getServerInetSocketAddress( - HttpClientConfig request, @Nullable HttpClientResponse response) { - - // we're making use of the fact that HttpClientOperations is both a Connection and an - // HttpClientResponse - if (response instanceof Connection) { - Connection connection = (Connection) response; - SocketAddress address = connection.channel().remoteAddress(); - if (address instanceof InetSocketAddress) { - return (InetSocketAddress) address; - } - } - return null; - } - - @Nullable - private static String getHost(HttpClientConfig request) { - String baseUrl = request.baseUrl(); - String uri = request.uri(); - - if (baseUrl != null && !isAbsolute(uri)) { - return UrlParser.getHost(baseUrl); - } else { - return UrlParser.getHost(uri); - } - } - - @Nullable - private static Integer getPort(HttpClientConfig request) { - String baseUrl = request.baseUrl(); - String uri = request.uri(); - - if (baseUrl != null && !isAbsolute(uri)) { - return UrlParser.getPort(baseUrl); - } else { - return UrlParser.getPort(uri); - } - } - - private static boolean isAbsolute(String uri) { - return uri != null && !uri.isEmpty() && !uri.startsWith("/"); - } -} diff --git a/instrumentation/reactor/reactor-netty/reactor-netty-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/reactornetty/v1_0/ReactorNettySingletons.java b/instrumentation/reactor/reactor-netty/reactor-netty-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/reactornetty/v1_0/ReactorNettySingletons.java index badd746a1b86..955a27503a70 100644 --- a/instrumentation/reactor/reactor-netty/reactor-netty-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/reactornetty/v1_0/ReactorNettySingletons.java +++ b/instrumentation/reactor/reactor-netty/reactor-netty-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/reactornetty/v1_0/ReactorNettySingletons.java @@ -7,74 +7,47 @@ 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.http.HttpClientAttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpClientMetrics; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpSpanNameExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpSpanStatusExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.net.PeerServiceAttributesExtractor; import io.opentelemetry.instrumentation.netty.v4.common.internal.client.NettyClientInstrumenterFactory; +import io.opentelemetry.instrumentation.netty.v4.common.internal.client.NettyConnectionInstrumentationFlag; import io.opentelemetry.instrumentation.netty.v4.common.internal.client.NettyConnectionInstrumenter; -import io.opentelemetry.javaagent.bootstrap.internal.CommonConfig; -import io.opentelemetry.javaagent.bootstrap.internal.DeprecatedConfigProperties; -import io.opentelemetry.javaagent.bootstrap.internal.InstrumentationConfig; -import reactor.netty.http.client.HttpClientConfig; +import io.opentelemetry.javaagent.bootstrap.internal.AgentCommonConfig; +import io.opentelemetry.javaagent.bootstrap.internal.AgentInstrumentationConfig; +import io.opentelemetry.javaagent.bootstrap.internal.JavaagentHttpClientInstrumenters; +import reactor.netty.http.client.HttpClientRequest; import reactor.netty.http.client.HttpClientResponse; public final class ReactorNettySingletons { private static final String INSTRUMENTATION_NAME = "io.opentelemetry.reactor-netty-1.0"; - private static final boolean connectionTelemetryEnabled; + private static final boolean connectionTelemetryEnabled = + AgentInstrumentationConfig.get() + .getBoolean("otel.instrumentation.reactor-netty.connection-telemetry.enabled", false); - static { - InstrumentationConfig config = InstrumentationConfig.get(); - connectionTelemetryEnabled = - DeprecatedConfigProperties.getBoolean( - config, - "otel.instrumentation.reactor-netty.always-create-connect-span", - "otel.instrumentation.reactor-netty.connection-telemetry.enabled", - false); - } - - private static final Instrumenter INSTRUMENTER; + private static final Instrumenter INSTRUMENTER; private static final NettyConnectionInstrumenter CONNECTION_INSTRUMENTER; static { - ReactorNettyHttpClientAttributesGetter httpAttributesGetter = - new ReactorNettyHttpClientAttributesGetter(); - ReactorNettyNetClientAttributesGetter netAttributesGetter = - new ReactorNettyNetClientAttributesGetter(); - INSTRUMENTER = - Instrumenter.builder( - GlobalOpenTelemetry.get(), - INSTRUMENTATION_NAME, - HttpSpanNameExtractor.create(httpAttributesGetter)) - .setSpanStatusExtractor(HttpSpanStatusExtractor.create(httpAttributesGetter)) - .addAttributesExtractor( - HttpClientAttributesExtractor.builder(httpAttributesGetter, netAttributesGetter) - .setCapturedRequestHeaders(CommonConfig.get().getClientRequestHeaders()) - .setCapturedResponseHeaders(CommonConfig.get().getClientResponseHeaders()) - .build()) - .addAttributesExtractor( - PeerServiceAttributesExtractor.create( - netAttributesGetter, CommonConfig.get().getPeerServiceMapping())) - .addOperationMetrics(HttpClientMetrics.get()) - // headers are injected in ResponseReceiverInstrumenter - .buildInstrumenter(SpanKindExtractor.alwaysClient()); + JavaagentHttpClientInstrumenters.create( + INSTRUMENTATION_NAME, + new ReactorNettyHttpClientAttributesGetter(), + HttpClientRequestHeadersSetter.INSTANCE); NettyClientInstrumenterFactory instrumenterFactory = new NettyClientInstrumenterFactory( GlobalOpenTelemetry.get(), INSTRUMENTATION_NAME, - connectionTelemetryEnabled, - false, - CommonConfig.get().getPeerServiceMapping()); + connectionTelemetryEnabled + ? NettyConnectionInstrumentationFlag.ENABLED + : NettyConnectionInstrumentationFlag.DISABLED, + NettyConnectionInstrumentationFlag.DISABLED, + AgentCommonConfig.get().getPeerServiceResolver(), + AgentCommonConfig.get().shouldEmitExperimentalHttpClientTelemetry()); CONNECTION_INSTRUMENTER = instrumenterFactory.createConnectionInstrumenter(); } - public static Instrumenter instrumenter() { + public static Instrumenter instrumenter() { return INSTRUMENTER; } diff --git a/instrumentation/reactor/reactor-netty/reactor-netty-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/reactornetty/v1_0/TransportConnectorInstrumentation.java b/instrumentation/reactor/reactor-netty/reactor-netty-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/reactornetty/v1_0/TransportConnectorInstrumentation.java index 690724a79440..4b165de020ee 100644 --- a/instrumentation/reactor/reactor-netty/reactor-netty-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/reactornetty/v1_0/TransportConnectorInstrumentation.java +++ b/instrumentation/reactor/reactor-netty/reactor-netty-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/reactornetty/v1_0/TransportConnectorInstrumentation.java @@ -7,6 +7,7 @@ import static io.opentelemetry.javaagent.instrumentation.reactornetty.v1_0.ReactorNettySingletons.connectionInstrumenter; 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; @@ -53,7 +54,13 @@ public void transform(TypeTransformer transformer) { transformer.applyAdviceToMethod( named("doConnect") .and(takesArgument(0, List.class)) - .and(takesArgument(2, named("io.netty.channel.ChannelPromise"))) + .and( + takesArgument( + 2, + namedOneOf( + "io.netty.channel.ChannelPromise", + // since 1.0.34 + "reactor.netty.transport.TransportConnector$MonoChannelPromise"))) .and(takesArgument(3, int.class)), TransportConnectorInstrumentation.class.getName() + "$ConnectNewAdvice"); } diff --git a/instrumentation/reactor/reactor-netty/reactor-netty-1.0/javaagent/src/main/java/reactor/netty/http/client/HttpClientConfigBuddy.java b/instrumentation/reactor/reactor-netty/reactor-netty-1.0/javaagent/src/main/java/reactor/netty/http/client/HttpClientConfigBuddy.java new file mode 100644 index 000000000000..ade93dd4d922 --- /dev/null +++ b/instrumentation/reactor/reactor-netty/reactor-netty-1.0/javaagent/src/main/java/reactor/netty/http/client/HttpClientConfigBuddy.java @@ -0,0 +1,25 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package reactor.netty.http.client; + +import java.util.function.Function; +import reactor.core.publisher.Mono; +import reactor.netty.Connection; + +// class in reactor package to access package-private code +public final class HttpClientConfigBuddy { + + public static boolean hasDeferredConfig(HttpClientConfig config) { + return config.deferredConf != null; + } + + public static Function, ? extends Mono> + getConnector(HttpClientConfig config) { + return config.connector == null ? Function.identity() : config.connector; + } + + private HttpClientConfigBuddy() {} +} diff --git a/instrumentation/reactor/reactor-netty/reactor-netty-1.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/reactornetty/v1_0/AbstractReactorNettyHttpClientTest.java b/instrumentation/reactor/reactor-netty/reactor-netty-1.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/reactornetty/v1_0/AbstractReactorNettyHttpClientTest.java index bc6550036f8b..9b2e4a5c67f5 100644 --- a/instrumentation/reactor/reactor-netty/reactor-netty-1.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/reactornetty/v1_0/AbstractReactorNettyHttpClientTest.java +++ b/instrumentation/reactor/reactor-netty/reactor-netty-1.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/reactornetty/v1_0/AbstractReactorNettyHttpClientTest.java @@ -5,13 +5,11 @@ package io.opentelemetry.javaagent.instrumentation.reactornetty.v1_0; -import static io.opentelemetry.api.common.AttributeKey.stringKey; 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.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; -import static java.util.Collections.emptySet; import static org.assertj.core.api.Assertions.catchThrowable; import io.netty.handler.codec.http.HttpMethod; @@ -28,7 +26,11 @@ import io.opentelemetry.instrumentation.testing.junit.http.HttpClientTestOptions; import io.opentelemetry.sdk.trace.data.SpanData; import io.opentelemetry.sdk.trace.data.StatusData; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.opentelemetry.semconv.ErrorAttributes; +import io.opentelemetry.semconv.HttpAttributes; +import io.opentelemetry.semconv.NetworkAttributes; +import io.opentelemetry.semconv.ServerAttributes; +import io.opentelemetry.semconv.UrlAttributes; import java.net.InetSocketAddress; import java.net.URI; import java.time.Duration; @@ -105,9 +107,13 @@ public void sendRequestWithCallback( @Override protected void configure(HttpClientTestOptions.Builder optionsBuilder) { - optionsBuilder.disableTestRedirects(); - optionsBuilder.setUserAgent(USER_AGENT); - optionsBuilder.enableTestCallbackWithImplicitParent(); + optionsBuilder.markAsLowLevelInstrumentation(); + optionsBuilder.setMaxRedirects(52); + optionsBuilder.spanEndsAfterBody(); + + // TODO: remove this test altogether? this scenario is (was) only implemented in reactor-netty, + // all other HTTP clients worked in a different way + // optionsBuilder.enableTestCallbackWithImplicitParent(); optionsBuilder.setClientSpanErrorMapper( (uri, exception) -> { @@ -125,18 +131,18 @@ protected void configure(HttpClientTestOptions.Builder optionsBuilder) { } protected Set> getHttpAttributes(URI uri) { + Set> attributes = new HashSet<>(HttpClientTestOptions.DEFAULT_HTTP_ATTRIBUTES); + // unopened port or non routable address if ("http://localhost:61/".equals(uri.toString()) || "https://192.0.2.1/".equals(uri.toString())) { - return emptySet(); + attributes.remove(NetworkAttributes.NETWORK_PROTOCOL_VERSION); } - Set> attributes = new HashSet<>(HttpClientTestOptions.DEFAULT_HTTP_ATTRIBUTES); if (uri.toString().contains("/read-timeout")) { - attributes.remove(stringKey("net.protocol.name")); - attributes.remove(stringKey("net.protocol.version")); - attributes.remove(SemanticAttributes.NET_PEER_NAME); - attributes.remove(SemanticAttributes.NET_PEER_PORT); + attributes.remove(NetworkAttributes.NETWORK_PROTOCOL_VERSION); + attributes.remove(ServerAttributes.SERVER_ADDRESS); + attributes.remove(ServerAttributes.SERVER_PORT); } return attributes; } @@ -186,7 +192,7 @@ void shouldExposeContextToHttpClientCallbacks() throws InterruptedException { span -> span.hasName("GET").hasKind(CLIENT).hasParent(parentSpan), span -> span.hasName("test-http-server").hasKind(SERVER).hasParent(nettyClientSpan)); - assertSameSpan(nettyClientSpan, onRequestSpan); + assertSameSpan(parentSpan, onRequestSpan); assertSameSpan(nettyClientSpan, afterRequestSpan); assertSameSpan(nettyClientSpan, onResponseSpan); assertSameSpan(parentSpan, afterResponseSpan); @@ -304,11 +310,11 @@ void shouldEndSpanOnMonoTimeout() { .hasKind(CLIENT) .hasParent(trace.getSpan(0)) .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.HTTP_METHOD, "GET"), - equalTo(SemanticAttributes.HTTP_URL, uri.toString()), - equalTo(SemanticAttributes.USER_AGENT_ORIGINAL, USER_AGENT), - equalTo(SemanticAttributes.NET_PEER_NAME, "localhost"), - equalTo(SemanticAttributes.NET_PEER_PORT, uri.getPort())), + equalTo(HttpAttributes.HTTP_REQUEST_METHOD, "GET"), + equalTo(UrlAttributes.URL_FULL, uri.toString()), + equalTo(ServerAttributes.SERVER_ADDRESS, "localhost"), + equalTo(ServerAttributes.SERVER_PORT, uri.getPort()), + equalTo(ErrorAttributes.ERROR_TYPE, "cancelled")), span -> span.hasName("test-http-server") .hasKind(SpanKind.SERVER) diff --git a/instrumentation/reactor/reactor-netty/reactor-netty-1.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/reactornetty/v1_0/CustomNameResolverGroup.java b/instrumentation/reactor/reactor-netty/reactor-netty-1.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/reactornetty/v1_0/CustomNameResolverGroup.java index de5ddee31fce..28938de13282 100644 --- a/instrumentation/reactor/reactor-netty/reactor-netty-1.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/reactornetty/v1_0/CustomNameResolverGroup.java +++ b/instrumentation/reactor/reactor-netty/reactor-netty-1.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/reactornetty/v1_0/CustomNameResolverGroup.java @@ -35,6 +35,7 @@ private CustomNameResolver(EventExecutor executor) { } @Override + @SuppressWarnings("AddressSelection") protected void doResolve(String inetHost, Promise promise) { try { promise.setSuccess(InetAddress.getByName(inetHost)); @@ -44,6 +45,7 @@ protected void doResolve(String inetHost, Promise promise) { } @Override + @SuppressWarnings("AddressSelection") protected void doResolveAll(String inetHost, Promise> promise) { try { // default implementation calls InetAddress.getAllByName diff --git a/instrumentation/reactor/reactor-netty/reactor-netty-1.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/reactornetty/v1_0/ReactorNettyBaseUrlOnlyTest.java b/instrumentation/reactor/reactor-netty/reactor-netty-1.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/reactornetty/v1_0/ReactorNettyBaseUrlOnlyTest.java index a07b8f33b63f..cced686ae905 100644 --- a/instrumentation/reactor/reactor-netty/reactor-netty-1.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/reactornetty/v1_0/ReactorNettyBaseUrlOnlyTest.java +++ b/instrumentation/reactor/reactor-netty/reactor-netty-1.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/reactornetty/v1_0/ReactorNettyBaseUrlOnlyTest.java @@ -5,10 +5,10 @@ package io.opentelemetry.javaagent.instrumentation.reactornetty.v1_0; -import static io.opentelemetry.api.common.AttributeKey.stringKey; 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.javaagent.instrumentation.reactornetty.v1_0.AbstractReactorNettyHttpClientTest.USER_AGENT; import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.satisfies; import static org.assertj.core.api.Assertions.assertThat; @@ -19,7 +19,10 @@ import io.opentelemetry.instrumentation.test.server.http.RequestContextGetter; import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.opentelemetry.semconv.HttpAttributes; +import io.opentelemetry.semconv.NetworkAttributes; +import io.opentelemetry.semconv.ServerAttributes; +import io.opentelemetry.semconv.UrlAttributes; import io.opentelemetry.testing.internal.armeria.common.HttpData; import io.opentelemetry.testing.internal.armeria.common.HttpResponse; import io.opentelemetry.testing.internal.armeria.common.HttpStatus; @@ -86,6 +89,7 @@ void testSuccessfulRequest() { () -> httpClient .baseUrl(uri) + .headers(h -> h.set("User-Agent", USER_AGENT)) .get() .responseSingle( (resp, content) -> { @@ -107,17 +111,16 @@ void testSuccessfulRequest() { .hasKind(CLIENT) .hasParent(trace.getSpan(0)) .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.HTTP_METHOD, "GET"), - equalTo(SemanticAttributes.HTTP_URL, uri + "/"), - equalTo(SemanticAttributes.HTTP_STATUS_CODE, 200), + equalTo(HttpAttributes.HTTP_REQUEST_METHOD, "GET"), + equalTo(UrlAttributes.URL_FULL, uri + "/"), + equalTo(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, 200), + equalTo(NetworkAttributes.NETWORK_PROTOCOL_VERSION, "1.1"), + equalTo(ServerAttributes.SERVER_ADDRESS, "localhost"), + equalTo(ServerAttributes.SERVER_PORT, server.httpPort()), + equalTo(NetworkAttributes.NETWORK_PEER_ADDRESS, "127.0.0.1"), satisfies( - SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH, - AbstractLongAssert::isNotNegative), - equalTo(stringKey("net.protocol.name"), "http"), - equalTo(stringKey("net.protocol.version"), "1.1"), - equalTo(SemanticAttributes.NET_PEER_NAME, "localhost"), - equalTo(SemanticAttributes.NET_PEER_PORT, server.httpPort()), - equalTo(SemanticAttributes.NET_SOCK_PEER_ADDR, "127.0.0.1")), + NetworkAttributes.NETWORK_PEER_PORT, + AbstractLongAssert::isNotNegative)), span -> span.hasName("test-http-server").hasKind(SERVER).hasParent(trace.getSpan(1)))); } diff --git a/instrumentation/reactor/reactor-netty/reactor-netty-1.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/reactornetty/v1_0/ReactorNettyClientSslTest.java b/instrumentation/reactor/reactor-netty/reactor-netty-1.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/reactornetty/v1_0/ReactorNettyClientSslTest.java index c2c8c6eaa227..0d89311e213e 100644 --- a/instrumentation/reactor/reactor-netty/reactor-netty-1.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/reactornetty/v1_0/ReactorNettyClientSslTest.java +++ b/instrumentation/reactor/reactor-netty/reactor-netty-1.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/reactornetty/v1_0/ReactorNettyClientSslTest.java @@ -5,14 +5,13 @@ package io.opentelemetry.javaagent.instrumentation.reactornetty.v1_0; -import static io.opentelemetry.api.common.AttributeKey.stringKey; 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.javaagent.instrumentation.reactornetty.v1_0.AbstractReactorNettyHttpClientTest.USER_AGENT; import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.satisfies; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.NetTransportValues.IP_TCP; import static org.assertj.core.api.Assertions.catchThrowable; import io.netty.handler.ssl.SslContextBuilder; @@ -21,7 +20,12 @@ import io.opentelemetry.instrumentation.testing.junit.http.HttpClientTestServer; import io.opentelemetry.sdk.trace.data.EventData; import io.opentelemetry.sdk.trace.data.StatusData; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.opentelemetry.semconv.ErrorAttributes; +import io.opentelemetry.semconv.ExceptionAttributes; +import io.opentelemetry.semconv.HttpAttributes; +import io.opentelemetry.semconv.NetworkAttributes; +import io.opentelemetry.semconv.ServerAttributes; +import io.opentelemetry.semconv.UrlAttributes; import java.util.List; import javax.annotation.Nullable; import javax.net.ssl.SSLException; @@ -90,40 +94,46 @@ void shouldFailSslHandshake() throws SSLException { // message .hasEventsSatisfying(ReactorNettyClientSslTest::isSslHandshakeException) .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.HTTP_METHOD, "GET"), - equalTo(SemanticAttributes.HTTP_URL, uri), - equalTo(SemanticAttributes.NET_PEER_NAME, "localhost"), - equalTo(SemanticAttributes.NET_PEER_PORT, server.httpsPort())), + equalTo(HttpAttributes.HTTP_REQUEST_METHOD, "GET"), + equalTo(UrlAttributes.URL_FULL, uri), + equalTo(ServerAttributes.SERVER_ADDRESS, "localhost"), + equalTo(ServerAttributes.SERVER_PORT, server.httpsPort()), + equalTo( + ErrorAttributes.ERROR_TYPE, + SSLHandshakeException.class.getCanonicalName())), span -> span.hasName("RESOLVE") .hasKind(INTERNAL) - .hasParent(trace.getSpan(1)) + .hasParent(trace.getSpan(0)) .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.NET_TRANSPORT, IP_TCP), - equalTo(SemanticAttributes.NET_PEER_NAME, "localhost"), - equalTo(SemanticAttributes.NET_PEER_PORT, server.httpsPort())), + equalTo(ServerAttributes.SERVER_ADDRESS, "localhost"), + equalTo(ServerAttributes.SERVER_PORT, server.httpsPort())), span -> span.hasName("CONNECT") .hasKind(INTERNAL) - .hasParent(trace.getSpan(1)) + .hasParent(trace.getSpan(0)) .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.NET_TRANSPORT, IP_TCP), - equalTo(SemanticAttributes.NET_PEER_NAME, "localhost"), - equalTo(SemanticAttributes.NET_PEER_PORT, server.httpsPort()), - equalTo(SemanticAttributes.NET_SOCK_PEER_ADDR, "127.0.0.1")), + equalTo(NetworkAttributes.NETWORK_TRANSPORT, "tcp"), + equalTo(NetworkAttributes.NETWORK_TYPE, "ipv4"), + equalTo(ServerAttributes.SERVER_ADDRESS, "localhost"), + equalTo(ServerAttributes.SERVER_PORT, server.httpsPort()), + equalTo(NetworkAttributes.NETWORK_PEER_ADDRESS, "127.0.0.1"), + satisfies( + NetworkAttributes.NETWORK_PEER_PORT, + AbstractLongAssert::isNotNegative)), span -> span.hasName("SSL handshake") .hasKind(INTERNAL) - .hasParent(trace.getSpan(1)) + .hasParent(trace.getSpan(0)) .hasStatus(StatusData.error()) // netty swallows the exception, it doesn't make any sense to hard-code the // message .hasEventsSatisfying(ReactorNettyClientSslTest::isSslHandshakeException) .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.NET_TRANSPORT, IP_TCP), - equalTo(SemanticAttributes.NET_SOCK_PEER_ADDR, "127.0.0.1"), - equalTo(SemanticAttributes.NET_SOCK_PEER_NAME, "localhost"), - equalTo(SemanticAttributes.NET_SOCK_PEER_PORT, server.httpsPort())))); + equalTo(NetworkAttributes.NETWORK_TRANSPORT, "tcp"), + equalTo(NetworkAttributes.NETWORK_TYPE, "ipv4"), + equalTo(NetworkAttributes.NETWORK_PEER_ADDRESS, "127.0.0.1"), + equalTo(NetworkAttributes.NETWORK_PEER_PORT, server.httpsPort())))); } @Test @@ -133,6 +143,7 @@ void shouldSuccessfullyEstablishSslHandshake() throws SSLException { Mono responseMono = httpClient + .headers(h -> h.set("User-Agent", USER_AGENT)) .get() .uri(uri) .responseSingle( @@ -147,50 +158,52 @@ void shouldSuccessfullyEstablishSslHandshake() throws SSLException { trace -> trace.hasSpansSatisfyingExactlyInAnyOrder( span -> span.hasName("parent").hasKind(INTERNAL).hasNoParent(), - span -> - span.hasName("GET") - .hasKind(CLIENT) - .hasParent(trace.getSpan(0)) - .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.HTTP_METHOD, "GET"), - equalTo(SemanticAttributes.HTTP_URL, uri), - equalTo(SemanticAttributes.HTTP_STATUS_CODE, 200), - satisfies( - SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH, - AbstractLongAssert::isNotNegative), - equalTo(stringKey("net.protocol.name"), "http"), - equalTo(stringKey("net.protocol.version"), "1.1"), - equalTo(SemanticAttributes.NET_PEER_NAME, "localhost"), - equalTo(SemanticAttributes.NET_PEER_PORT, server.httpsPort()), - equalTo(SemanticAttributes.NET_SOCK_PEER_ADDR, "127.0.0.1")), span -> span.hasName("RESOLVE") .hasKind(INTERNAL) - .hasParent(trace.getSpan(1)) + .hasParent(trace.getSpan(0)) .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.NET_TRANSPORT, IP_TCP), - equalTo(SemanticAttributes.NET_PEER_NAME, "localhost"), - equalTo(SemanticAttributes.NET_PEER_PORT, server.httpsPort())), + equalTo(ServerAttributes.SERVER_ADDRESS, "localhost"), + equalTo(ServerAttributes.SERVER_PORT, server.httpsPort())), span -> span.hasName("CONNECT") .hasKind(INTERNAL) - .hasParent(trace.getSpan(1)) + .hasParent(trace.getSpan(0)) .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.NET_TRANSPORT, IP_TCP), - equalTo(SemanticAttributes.NET_PEER_NAME, "localhost"), - equalTo(SemanticAttributes.NET_PEER_PORT, server.httpsPort()), - equalTo(SemanticAttributes.NET_SOCK_PEER_ADDR, "127.0.0.1")), + equalTo(NetworkAttributes.NETWORK_TRANSPORT, "tcp"), + equalTo(NetworkAttributes.NETWORK_TYPE, "ipv4"), + equalTo(ServerAttributes.SERVER_ADDRESS, "localhost"), + equalTo(ServerAttributes.SERVER_PORT, server.httpsPort()), + equalTo(NetworkAttributes.NETWORK_PEER_ADDRESS, "127.0.0.1"), + satisfies( + NetworkAttributes.NETWORK_PEER_PORT, + AbstractLongAssert::isNotNegative)), span -> span.hasName("SSL handshake") .hasKind(INTERNAL) - .hasParent(trace.getSpan(1)) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo(NetworkAttributes.NETWORK_TRANSPORT, "tcp"), + equalTo(NetworkAttributes.NETWORK_TYPE, "ipv4"), + equalTo(NetworkAttributes.NETWORK_PEER_ADDRESS, "127.0.0.1"), + equalTo(NetworkAttributes.NETWORK_PEER_PORT, server.httpsPort())), + span -> + span.hasName("GET") + .hasKind(CLIENT) + .hasParent(trace.getSpan(0)) .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.NET_TRANSPORT, IP_TCP), - equalTo(SemanticAttributes.NET_SOCK_PEER_ADDR, "127.0.0.1"), - equalTo(SemanticAttributes.NET_SOCK_PEER_NAME, "localhost"), - equalTo(SemanticAttributes.NET_SOCK_PEER_PORT, server.httpsPort())), + equalTo(HttpAttributes.HTTP_REQUEST_METHOD, "GET"), + equalTo(UrlAttributes.URL_FULL, uri), + equalTo(NetworkAttributes.NETWORK_PROTOCOL_VERSION, "1.1"), + equalTo(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, 200), + equalTo(ServerAttributes.SERVER_ADDRESS, "localhost"), + equalTo(ServerAttributes.SERVER_PORT, server.httpsPort()), + equalTo(NetworkAttributes.NETWORK_PEER_ADDRESS, "127.0.0.1"), + satisfies( + NetworkAttributes.NETWORK_PEER_PORT, + AbstractLongAssert::isNotNegative)), span -> - span.hasName("test-http-server").hasKind(SERVER).hasParent(trace.getSpan(1)))); + span.hasName("test-http-server").hasKind(SERVER).hasParent(trace.getSpan(4)))); } private static HttpClient createHttpClient() throws SSLException { @@ -209,15 +222,15 @@ private static HttpClient createHttpClient(@Nullable String enabledProtocol) thr private static void isSslHandshakeException(List events) { assertThat(events) - .filteredOn(event -> event.getName().equals(SemanticAttributes.EXCEPTION_EVENT_NAME)) + .filteredOn(event -> event.getName().equals("exception")) .satisfiesExactly( event -> assertThat(event) .hasAttributesSatisfyingExactly( equalTo( - SemanticAttributes.EXCEPTION_TYPE, + ExceptionAttributes.EXCEPTION_TYPE, SSLHandshakeException.class.getCanonicalName()), - satisfies(SemanticAttributes.EXCEPTION_MESSAGE, s -> s.isNotEmpty()), - satisfies(SemanticAttributes.EXCEPTION_STACKTRACE, s -> s.isNotEmpty()))); + satisfies(ExceptionAttributes.EXCEPTION_MESSAGE, s -> s.isNotEmpty()), + satisfies(ExceptionAttributes.EXCEPTION_STACKTRACE, s -> s.isNotEmpty()))); } } diff --git a/instrumentation/reactor/reactor-netty/reactor-netty-1.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/reactornetty/v1_0/ReactorNettyConnectionSpanTest.java b/instrumentation/reactor/reactor-netty/reactor-netty-1.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/reactornetty/v1_0/ReactorNettyConnectionSpanTest.java index ad7da3593664..ce040d4b50ee 100644 --- a/instrumentation/reactor/reactor-netty/reactor-netty-1.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/reactornetty/v1_0/ReactorNettyConnectionSpanTest.java +++ b/instrumentation/reactor/reactor-netty/reactor-netty-1.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/reactornetty/v1_0/ReactorNettyConnectionSpanTest.java @@ -5,13 +5,12 @@ package io.opentelemetry.javaagent.instrumentation.reactornetty.v1_0; -import static io.opentelemetry.api.common.AttributeKey.stringKey; 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.javaagent.instrumentation.reactornetty.v1_0.AbstractReactorNettyHttpClientTest.USER_AGENT; import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.satisfies; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.NetTransportValues.IP_TCP; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.catchThrowable; @@ -20,7 +19,11 @@ import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; import io.opentelemetry.instrumentation.testing.junit.http.HttpClientTestServer; import io.opentelemetry.sdk.trace.data.StatusData; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.opentelemetry.semconv.ErrorAttributes; +import io.opentelemetry.semconv.HttpAttributes; +import io.opentelemetry.semconv.NetworkAttributes; +import io.opentelemetry.semconv.ServerAttributes; +import io.opentelemetry.semconv.UrlAttributes; import org.assertj.core.api.AbstractLongAssert; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; @@ -56,6 +59,7 @@ void testSuccessfulRequest() { "parent", () -> httpClient + .headers(h -> h.set("User-Agent", USER_AGENT)) .get() .uri(uri) .responseSingle( @@ -73,41 +77,43 @@ void testSuccessfulRequest() { trace -> trace.hasSpansSatisfyingExactlyInAnyOrder( span -> span.hasName("parent").hasKind(INTERNAL).hasNoParent(), - span -> - span.hasName("GET") - .hasKind(CLIENT) - .hasParent(trace.getSpan(0)) - .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.HTTP_METHOD, "GET"), - equalTo(SemanticAttributes.HTTP_URL, uri), - equalTo(SemanticAttributes.HTTP_STATUS_CODE, 200), - satisfies( - SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH, - AbstractLongAssert::isNotNegative), - equalTo(stringKey("net.protocol.name"), "http"), - equalTo(stringKey("net.protocol.version"), "1.1"), - equalTo(SemanticAttributes.NET_PEER_NAME, "localhost"), - equalTo(SemanticAttributes.NET_PEER_PORT, server.httpPort()), - equalTo(SemanticAttributes.NET_SOCK_PEER_ADDR, "127.0.0.1")), span -> span.hasName("RESOLVE") .hasKind(INTERNAL) - .hasParent(trace.getSpan(1)) + .hasParent(trace.getSpan(0)) .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.NET_TRANSPORT, IP_TCP), - equalTo(SemanticAttributes.NET_PEER_NAME, "localhost"), - equalTo(SemanticAttributes.NET_PEER_PORT, server.httpPort())), + equalTo(ServerAttributes.SERVER_ADDRESS, "localhost"), + equalTo(ServerAttributes.SERVER_PORT, server.httpPort())), span -> span.hasName("CONNECT") .hasKind(INTERNAL) - .hasParent(trace.getSpan(1)) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo(NetworkAttributes.NETWORK_TRANSPORT, "tcp"), + equalTo(NetworkAttributes.NETWORK_TYPE, "ipv4"), + equalTo(ServerAttributes.SERVER_ADDRESS, "localhost"), + equalTo(ServerAttributes.SERVER_PORT, server.httpPort()), + equalTo(NetworkAttributes.NETWORK_PEER_ADDRESS, "127.0.0.1"), + satisfies( + NetworkAttributes.NETWORK_PEER_PORT, + AbstractLongAssert::isNotNegative)), + span -> + span.hasName("GET") + .hasKind(CLIENT) + .hasParent(trace.getSpan(0)) .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.NET_TRANSPORT, IP_TCP), - equalTo(SemanticAttributes.NET_PEER_NAME, "localhost"), - equalTo(SemanticAttributes.NET_PEER_PORT, server.httpPort()), - equalTo(SemanticAttributes.NET_SOCK_PEER_ADDR, "127.0.0.1")), + equalTo(HttpAttributes.HTTP_REQUEST_METHOD, "GET"), + equalTo(UrlAttributes.URL_FULL, uri), + equalTo(NetworkAttributes.NETWORK_PROTOCOL_VERSION, "1.1"), + equalTo(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, 200), + equalTo(ServerAttributes.SERVER_ADDRESS, "localhost"), + equalTo(ServerAttributes.SERVER_PORT, server.httpPort()), + equalTo(NetworkAttributes.NETWORK_PEER_ADDRESS, "127.0.0.1"), + satisfies( + NetworkAttributes.NETWORK_PEER_PORT, + AbstractLongAssert::isNotNegative)), span -> - span.hasName("test-http-server").hasKind(SERVER).hasParent(trace.getSpan(1)))); + span.hasName("test-http-server").hasKind(SERVER).hasParent(trace.getSpan(3)))); } @Test @@ -138,7 +144,7 @@ void testFailingRequest() { testing.waitAndAssertTraces( trace -> - trace.hasSpansSatisfyingExactlyInAnyOrder( + trace.hasSpansSatisfyingExactly( span -> span.hasName("parent") .hasKind(INTERNAL) @@ -152,30 +158,30 @@ void testFailingRequest() { .hasStatus(StatusData.error()) .hasException(connectException) .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.HTTP_METHOD, "GET"), - equalTo(SemanticAttributes.HTTP_URL, uri), - equalTo(SemanticAttributes.NET_PEER_NAME, "localhost"), - equalTo(SemanticAttributes.NET_PEER_PORT, PortUtils.UNUSABLE_PORT)), + equalTo(HttpAttributes.HTTP_REQUEST_METHOD, "GET"), + equalTo(UrlAttributes.URL_FULL, uri), + equalTo(ServerAttributes.SERVER_ADDRESS, "localhost"), + equalTo(ServerAttributes.SERVER_PORT, PortUtils.UNUSABLE_PORT), + equalTo( + ErrorAttributes.ERROR_TYPE, connectException.getClass().getName())), span -> span.hasName("RESOLVE") .hasKind(INTERNAL) - .hasParent(trace.getSpan(1)) + .hasParent(trace.getSpan(0)) .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.NET_TRANSPORT, IP_TCP), - equalTo(SemanticAttributes.NET_PEER_NAME, "localhost"), - equalTo(SemanticAttributes.NET_PEER_PORT, PortUtils.UNUSABLE_PORT)), + equalTo(ServerAttributes.SERVER_ADDRESS, "localhost"), + equalTo(ServerAttributes.SERVER_PORT, PortUtils.UNUSABLE_PORT)), span -> span.hasName("CONNECT") .hasKind(INTERNAL) - .hasParent(trace.getSpan(1)) + .hasParent(trace.getSpan(0)) .hasStatus(StatusData.error()) .hasException(connectException) .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.NET_TRANSPORT, IP_TCP), - equalTo(SemanticAttributes.NET_PEER_NAME, "localhost"), - equalTo(SemanticAttributes.NET_PEER_PORT, PortUtils.UNUSABLE_PORT), + equalTo(ServerAttributes.SERVER_ADDRESS, "localhost"), + equalTo(ServerAttributes.SERVER_PORT, PortUtils.UNUSABLE_PORT), satisfies( - SemanticAttributes.NET_SOCK_PEER_ADDR, + NetworkAttributes.NETWORK_PEER_ADDRESS, val -> val.isIn(null, "127.0.0.1"))))); } } diff --git a/instrumentation/reactor/reactor-netty/reactor-netty-1.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/reactornetty/v1_0/ReactorNettyHttpClientDeferredHeadersTest.java b/instrumentation/reactor/reactor-netty/reactor-netty-1.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/reactornetty/v1_0/ReactorNettyHttpClientDeferredHeadersTest.java new file mode 100644 index 000000000000..4076df95a6e8 --- /dev/null +++ b/instrumentation/reactor/reactor-netty/reactor-netty-1.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/reactornetty/v1_0/ReactorNettyHttpClientDeferredHeadersTest.java @@ -0,0 +1,56 @@ +/* + * 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.netty.handler.codec.http.HttpMethod; +import io.opentelemetry.instrumentation.testing.junit.http.HttpClientTestOptions; +import io.opentelemetry.testing.internal.armeria.common.HttpHeaderNames; +import java.net.URI; +import java.util.Map; +import reactor.core.publisher.Mono; +import reactor.netty.http.client.HttpClient; + +class ReactorNettyHttpClientDeferredHeadersTest extends AbstractReactorNettyHttpClientTest { + + @Override + protected HttpClient createHttpClient() { + int connectionTimeoutMillis = (int) CONNECTION_TIMEOUT.toMillis(); + return HttpClient.create() + .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, connectionTimeoutMillis) + .resolver(getAddressResolverGroup()) + .headers(headers -> headers.set(HttpHeaderNames.USER_AGENT, USER_AGENT)); + } + + @Override + public HttpClient.ResponseReceiver buildRequest( + String method, URI uri, Map headers) { + HttpClient client = + createHttpClient() + .followRedirect(true) + .headersWhen( + h -> { + headers.forEach(h::add); + return Mono.just(h); + }) + .baseUrl(resolveAddress("").toString()); + if (uri.toString().contains("/read-timeout")) { + client = client.responseTimeout(READ_TIMEOUT); + } + return client.request(HttpMethod.valueOf(method)).uri(uri.toString()); + } + + @Override + protected void configure(HttpClientTestOptions.Builder optionsBuilder) { + super.configure(optionsBuilder); + + // these scenarios don't work because deferred config does not apply the doOnRequestError() + // callback + optionsBuilder.disableTestReadTimeout(); + optionsBuilder.disableTestConnectionFailure(); + optionsBuilder.disableTestRemoteConnection(); + } +} diff --git a/instrumentation/reactor/reactor-netty/reactor-netty-1.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/reactornetty/v1_0/ReactorNettyHttpClientTest.java b/instrumentation/reactor/reactor-netty/reactor-netty-1.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/reactornetty/v1_0/ReactorNettyHttpClientTest.java index 92bfcfadded7..e4b3a090572c 100644 --- a/instrumentation/reactor/reactor-netty/reactor-netty-1.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/reactornetty/v1_0/ReactorNettyHttpClientTest.java +++ b/instrumentation/reactor/reactor-netty/reactor-netty-1.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/reactornetty/v1_0/ReactorNettyHttpClientTest.java @@ -8,7 +8,7 @@ import io.netty.channel.ChannelOption; import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.instrumentation.testing.junit.http.HttpClientTestOptions; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.opentelemetry.semconv.ServerAttributes; import io.opentelemetry.testing.internal.armeria.common.HttpHeaderNames; import java.net.URI; import java.util.HashSet; @@ -61,8 +61,8 @@ protected Set> getHttpAttributes(URI uri) { // net.peer.sock.* attributes Set> attributes = new HashSet<>(HttpClientTestOptions.DEFAULT_HTTP_ATTRIBUTES); - attributes.remove(SemanticAttributes.NET_PEER_NAME); - attributes.remove(SemanticAttributes.NET_PEER_PORT); + attributes.remove(ServerAttributes.SERVER_ADDRESS); + attributes.remove(ServerAttributes.SERVER_PORT); return attributes; } return super.getHttpAttributes(uri); diff --git a/instrumentation/rediscala-1.8/javaagent/build.gradle.kts b/instrumentation/rediscala-1.8/javaagent/build.gradle.kts index bd73085bfe16..8442cb3dbf1b 100644 --- a/instrumentation/rediscala-1.8/javaagent/build.gradle.kts +++ b/instrumentation/rediscala-1.8/javaagent/build.gradle.kts @@ -44,12 +44,19 @@ muzzle { versions.set("[1.9.0,)") assertInverse.set(true) } + + pass { + group.set("io.github.rediscala") + module.set("rediscala_2.13") + versions.set("[1.10.0,)") + assertInverse.set(true) + } } dependencies { library("com.github.etaty:rediscala_2.11:1.8.0") - latestDepTestLibrary("com.github.etaty:rediscala_2.13:+") + latestDepTestLibrary("io.github.rediscala:rediscala_2.13:+") } tasks { diff --git a/instrumentation/rediscala-1.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rediscala/RediscalaAttributesGetter.java b/instrumentation/rediscala-1.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rediscala/RediscalaAttributesGetter.java index 66ce1229c6d8..a99f63faebc2 100644 --- a/instrumentation/rediscala-1.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rediscala/RediscalaAttributesGetter.java +++ b/instrumentation/rediscala-1.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rediscala/RediscalaAttributesGetter.java @@ -5,8 +5,8 @@ package io.opentelemetry.javaagent.instrumentation.rediscala; -import io.opentelemetry.instrumentation.api.instrumenter.db.DbClientAttributesGetter; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.opentelemetry.instrumentation.api.incubator.semconv.db.DbClientAttributesGetter; +import io.opentelemetry.semconv.incubating.DbIncubatingAttributes; import java.util.Locale; import javax.annotation.Nullable; import redis.RedisCommand; @@ -15,7 +15,7 @@ final class RediscalaAttributesGetter implements DbClientAttributesGetter redisCommand) { - return SemanticAttributes.DbSystemValues.REDIS; + return DbIncubatingAttributes.DbSystemValues.REDIS; } @Override diff --git a/instrumentation/rediscala-1.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rediscala/RediscalaSingletons.java b/instrumentation/rediscala-1.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rediscala/RediscalaSingletons.java index fa92b17a2633..457154730ba1 100644 --- a/instrumentation/rediscala-1.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rediscala/RediscalaSingletons.java +++ b/instrumentation/rediscala-1.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rediscala/RediscalaSingletons.java @@ -6,10 +6,10 @@ package io.opentelemetry.javaagent.instrumentation.rediscala; import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.instrumentation.api.incubator.semconv.db.DbClientAttributesExtractor; +import io.opentelemetry.instrumentation.api.incubator.semconv.db.DbClientSpanNameExtractor; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; import io.opentelemetry.instrumentation.api.instrumenter.SpanKindExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.db.DbClientAttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.db.DbClientSpanNameExtractor; import redis.RedisCommand; public final class RediscalaSingletons { diff --git a/instrumentation/rediscala-1.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rediscala/RequestInstrumentation.java b/instrumentation/rediscala-1.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rediscala/RequestInstrumentation.java index 018cd0cf855e..0daf60166a41 100644 --- a/instrumentation/rediscala-1.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rediscala/RequestInstrumentation.java +++ b/instrumentation/rediscala-1.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rediscala/RequestInstrumentation.java @@ -23,7 +23,11 @@ import net.bytebuddy.asm.Advice; import net.bytebuddy.description.type.TypeDescription; import net.bytebuddy.matcher.ElementMatcher; +import redis.ActorRequest; +import redis.BufferedRequest; import redis.RedisCommand; +import redis.Request; +import redis.RoundRobinPoolRequest; import scala.concurrent.ExecutionContext; import scala.concurrent.Future; @@ -74,11 +78,11 @@ public static void onEnter( @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) public static void onExit( + @Advice.This Object action, @Advice.Argument(0) RedisCommand cmd, @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) { if (scope == null) { @@ -86,6 +90,17 @@ public static void onExit( } scope.close(); + ExecutionContext ctx = null; + if (action instanceof ActorRequest) { + ctx = ((ActorRequest) action).executionContext(); + } else if (action instanceof Request) { + ctx = ((Request) action).executionContext(); + } else if (action instanceof BufferedRequest) { + ctx = ((BufferedRequest) action).executionContext(); + } else if (action instanceof RoundRobinPoolRequest) { + ctx = ((RoundRobinPoolRequest) action).executionContext(); + } + if (throwable != null) { instrumenter().end(context, cmd, null, throwable); } else { diff --git a/instrumentation/rediscala-1.8/javaagent/src/test/groovy/RediscalaClientTest.groovy b/instrumentation/rediscala-1.8/javaagent/src/test/groovy/RediscalaClientTest.groovy index b49513318bd4..5f6e10ab0acc 100644 --- a/instrumentation/rediscala-1.8/javaagent/src/test/groovy/RediscalaClientTest.groovy +++ b/instrumentation/rediscala-1.8/javaagent/src/test/groovy/RediscalaClientTest.groovy @@ -3,9 +3,8 @@ * SPDX-License-Identifier: Apache-2.0 */ -import akka.actor.ActorSystem import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import io.opentelemetry.semconv.incubating.DbIncubatingAttributes import org.testcontainers.containers.GenericContainer import redis.ByteStringDeserializerDefault import redis.ByteStringSerializerLowPriority @@ -26,23 +25,45 @@ class RediscalaClientTest extends AgentInstrumentationSpecification { int port @Shared - ActorSystem system + def system @Shared RedisClient redisClient def setupSpec() { redisServer.start() + String host = redisServer.getHost() 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")) + // latest has separate artifacts for akka an pekko, currently latestDepTestLibrary picks the + // pekko one + try { + def clazz = Class.forName("akka.actor.ActorSystem") + system = clazz.getMethod("create").invoke(null) + } catch (ClassNotFoundException exception) { + def clazz = Class.forName("org.apache.pekko.actor.ActorSystem") + system = clazz.getMethod("create").invoke(null) + } + // latest RedisClient constructor takes username as argument + if (RedisClient.metaClass.getMetaMethod("username") != null) { + redisClient = new RedisClient(host, + port, + Option.apply(null), + Option.apply(null), + Option.apply(null), + "RedisClient", + Option.apply(null), + system, + new RedisDispatcher("rediscala.rediscala-client-worker-dispatcher")) + } else { + redisClient = new RedisClient(host, + port, + Option.apply(null), + Option.apply(null), + "RedisClient", + Option.apply(null), + system, + new RedisDispatcher("rediscala.rediscala-client-worker-dispatcher")) + } } def cleanupSpec() { @@ -69,8 +90,8 @@ class RediscalaClientTest extends AgentInstrumentationSpecification { name "SET" kind CLIENT attributes { - "$SemanticAttributes.DB_SYSTEM" "redis" - "$SemanticAttributes.DB_OPERATION" "SET" + "$DbIncubatingAttributes.DB_SYSTEM" "redis" + "$DbIncubatingAttributes.DB_OPERATION" "SET" } } } @@ -104,8 +125,8 @@ class RediscalaClientTest extends AgentInstrumentationSpecification { kind CLIENT childOf span(0) attributes { - "$SemanticAttributes.DB_SYSTEM" "redis" - "$SemanticAttributes.DB_OPERATION" "SET" + "$DbIncubatingAttributes.DB_SYSTEM" "redis" + "$DbIncubatingAttributes.DB_OPERATION" "SET" } } span(2) { @@ -113,8 +134,8 @@ class RediscalaClientTest extends AgentInstrumentationSpecification { kind CLIENT childOf span(0) attributes { - "$SemanticAttributes.DB_SYSTEM" "redis" - "$SemanticAttributes.DB_OPERATION" "GET" + "$DbIncubatingAttributes.DB_SYSTEM" "redis" + "$DbIncubatingAttributes.DB_OPERATION" "GET" } } } diff --git a/instrumentation/redisson/redisson-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/redisson/v3_0/RedissonAsyncClientTest.java b/instrumentation/redisson/redisson-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/redisson/v3_0/RedissonAsyncClientTest.java new file mode 100644 index 000000000000..f22206a467f9 --- /dev/null +++ b/instrumentation/redisson/redisson-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/redisson/v3_0/RedissonAsyncClientTest.java @@ -0,0 +1,10 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.redisson.v3_0; + +import io.opentelemetry.javaagent.instrumentation.redisson.AbstractRedissonAsyncClientTest; + +public class RedissonAsyncClientTest extends AbstractRedissonAsyncClientTest {} diff --git a/instrumentation/redisson/redisson-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/redisson/v3_0/RedissonClientTest.java b/instrumentation/redisson/redisson-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/redisson/v3_0/RedissonClientTest.java new file mode 100644 index 000000000000..a81ad609bc6f --- /dev/null +++ b/instrumentation/redisson/redisson-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/redisson/v3_0/RedissonClientTest.java @@ -0,0 +1,17 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.redisson.v3_0; + +import io.opentelemetry.javaagent.instrumentation.redisson.AbstractRedissonClientTest; +import org.redisson.api.RBatch; +import org.redisson.api.RedissonClient; + +public class RedissonClientTest extends AbstractRedissonClientTest { + @Override + protected RBatch createBatch(RedissonClient redisson) { + return redisson.createBatch(); + } +} diff --git a/instrumentation/redisson/redisson-3.17/javaagent/src/test/groovy/RedissonAsyncClientTest.groovy b/instrumentation/redisson/redisson-3.17/javaagent/src/test/groovy/RedissonAsyncClientTest.groovy deleted file mode 100644 index af0876acb7cb..000000000000 --- a/instrumentation/redisson/redisson-3.17/javaagent/src/test/groovy/RedissonAsyncClientTest.groovy +++ /dev/null @@ -1,12 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -class RedissonAsyncClientTest extends AbstractRedissonAsyncClientTest { - - @Override - boolean useRedisProtocol() { - true - } -} diff --git a/instrumentation/redisson/redisson-3.17/javaagent/src/test/groovy/RedissonClientTest.groovy b/instrumentation/redisson/redisson-3.17/javaagent/src/test/groovy/RedissonClientTest.groovy deleted file mode 100644 index 54e2b5850bd3..000000000000 --- a/instrumentation/redisson/redisson-3.17/javaagent/src/test/groovy/RedissonClientTest.groovy +++ /dev/null @@ -1,12 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -class RedissonClientTest extends AbstractRedissonClientTest { - - @Override - boolean useRedisProtocol() { - true - } -} diff --git a/instrumentation/redisson/redisson-3.17/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/redisson/v3_17/RedissonAsyncClientTest.java b/instrumentation/redisson/redisson-3.17/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/redisson/v3_17/RedissonAsyncClientTest.java new file mode 100644 index 000000000000..ab8a9b629c50 --- /dev/null +++ b/instrumentation/redisson/redisson-3.17/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/redisson/v3_17/RedissonAsyncClientTest.java @@ -0,0 +1,16 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.redisson.v3_17; + +import io.opentelemetry.javaagent.instrumentation.redisson.AbstractRedissonAsyncClientTest; + +class RedissonAsyncClientTest extends AbstractRedissonAsyncClientTest { + + @Override + protected boolean useRedisProtocol() { + return true; + } +} diff --git a/instrumentation/redisson/redisson-3.17/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/redisson/v3_17/RedissonClientTest.java b/instrumentation/redisson/redisson-3.17/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/redisson/v3_17/RedissonClientTest.java new file mode 100644 index 000000000000..c354f4040ff1 --- /dev/null +++ b/instrumentation/redisson/redisson-3.17/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/redisson/v3_17/RedissonClientTest.java @@ -0,0 +1,20 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.redisson.v3_17; + +import io.opentelemetry.javaagent.instrumentation.redisson.AbstractRedissonClientTest; + +class RedissonClientTest extends AbstractRedissonClientTest { + @Override + protected boolean useRedisProtocol() { + return true; + } + + @Override + protected boolean lockHas3Traces() { + return Boolean.getBoolean("testLatestDeps"); + } +} diff --git a/instrumentation/redisson/redisson-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/redisson/RedissonDbAttributesGetter.java b/instrumentation/redisson/redisson-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/redisson/RedissonDbAttributesGetter.java index 0faf8c22518b..e11f73ec316a 100644 --- a/instrumentation/redisson/redisson-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/redisson/RedissonDbAttributesGetter.java +++ b/instrumentation/redisson/redisson-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/redisson/RedissonDbAttributesGetter.java @@ -5,15 +5,15 @@ package io.opentelemetry.javaagent.instrumentation.redisson; -import io.opentelemetry.instrumentation.api.instrumenter.db.DbClientAttributesGetter; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.opentelemetry.instrumentation.api.incubator.semconv.db.DbClientAttributesGetter; +import io.opentelemetry.semconv.incubating.DbIncubatingAttributes; import javax.annotation.Nullable; final class RedissonDbAttributesGetter implements DbClientAttributesGetter { @Override public String getSystem(RedissonRequest request) { - return SemanticAttributes.DbSystemValues.REDIS; + return DbIncubatingAttributes.DbSystemValues.REDIS; } @Nullable diff --git a/instrumentation/redisson/redisson-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/redisson/RedissonInstrumenterFactory.java b/instrumentation/redisson/redisson-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/redisson/RedissonInstrumenterFactory.java index 745d55e72659..39dc2034ec19 100644 --- a/instrumentation/redisson/redisson-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/redisson/RedissonInstrumenterFactory.java +++ b/instrumentation/redisson/redisson-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/redisson/RedissonInstrumenterFactory.java @@ -6,11 +6,11 @@ package io.opentelemetry.javaagent.instrumentation.redisson; import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.instrumentation.api.incubator.semconv.db.DbClientAttributesExtractor; +import io.opentelemetry.instrumentation.api.incubator.semconv.db.DbClientSpanNameExtractor; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; import io.opentelemetry.instrumentation.api.instrumenter.SpanKindExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.db.DbClientAttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.db.DbClientSpanNameExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.net.NetClientAttributesExtractor; +import io.opentelemetry.instrumentation.api.semconv.network.NetworkAttributesExtractor; public final class RedissonInstrumenterFactory { @@ -23,7 +23,7 @@ public static Instrumenter createInstrumenter(String inst instrumentationName, DbClientSpanNameExtractor.create(dbAttributesGetter)) .addAttributesExtractor(DbClientAttributesExtractor.create(dbAttributesGetter)) - .addAttributesExtractor(NetClientAttributesExtractor.create(netAttributesGetter)) + .addAttributesExtractor(NetworkAttributesExtractor.create(netAttributesGetter)) .buildInstrumenter(SpanKindExtractor.alwaysClient()); } diff --git a/instrumentation/redisson/redisson-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/redisson/RedissonNetAttributesGetter.java b/instrumentation/redisson/redisson-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/redisson/RedissonNetAttributesGetter.java index d06e68b52654..a1739cff1104 100644 --- a/instrumentation/redisson/redisson-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/redisson/RedissonNetAttributesGetter.java +++ b/instrumentation/redisson/redisson-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/redisson/RedissonNetAttributesGetter.java @@ -5,27 +5,14 @@ package io.opentelemetry.javaagent.instrumentation.redisson; -import io.opentelemetry.instrumentation.api.instrumenter.net.NetClientAttributesGetter; +import io.opentelemetry.instrumentation.api.semconv.network.NetworkAttributesGetter; import java.net.InetSocketAddress; import javax.annotation.Nullable; -final class RedissonNetAttributesGetter - implements NetClientAttributesGetter { +final class RedissonNetAttributesGetter implements NetworkAttributesGetter { - @Nullable @Override - public String getServerAddress(RedissonRequest redissonRequest) { - return null; - } - - @Nullable - @Override - public Integer getServerPort(RedissonRequest redissonRequest) { - return null; - } - - @Override - public InetSocketAddress getServerInetSocketAddress( + public InetSocketAddress getNetworkPeerInetSocketAddress( RedissonRequest request, @Nullable Void unused) { return request.getAddress(); } diff --git a/instrumentation/redisson/redisson-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/redisson/RedissonRequest.java b/instrumentation/redisson/redisson-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/redisson/RedissonRequest.java index 4a3d1e651846..4d0bbc4606a7 100644 --- a/instrumentation/redisson/redisson-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/redisson/RedissonRequest.java +++ b/instrumentation/redisson/redisson-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/redisson/RedissonRequest.java @@ -10,8 +10,8 @@ import com.google.auto.value.AutoValue; import io.netty.buffer.ByteBuf; -import io.opentelemetry.instrumentation.api.db.RedisCommandSanitizer; -import io.opentelemetry.javaagent.bootstrap.internal.CommonConfig; +import io.opentelemetry.instrumentation.api.incubator.semconv.db.RedisCommandSanitizer; +import io.opentelemetry.javaagent.bootstrap.internal.AgentCommonConfig; import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodType; @@ -29,7 +29,7 @@ public abstract class RedissonRequest { private static final RedisCommandSanitizer sanitizer = - RedisCommandSanitizer.create(CommonConfig.get().isStatementSanitizationEnabled()); + RedisCommandSanitizer.create(AgentCommonConfig.get().isStatementSanitizationEnabled()); public static RedissonRequest create(InetSocketAddress address, Object command) { return new AutoValue_RedissonRequest(address, command); diff --git a/instrumentation/redisson/redisson-common/testing/build.gradle.kts b/instrumentation/redisson/redisson-common/testing/build.gradle.kts index 4eba7a46cb7a..7b807126c8a3 100644 --- a/instrumentation/redisson/redisson-common/testing/build.gradle.kts +++ b/instrumentation/redisson/redisson-common/testing/build.gradle.kts @@ -5,9 +5,7 @@ plugins { dependencies { api(project(":testing-common")) - implementation("org.apache.groovy:groovy") implementation("io.opentelemetry:opentelemetry-api") - implementation("org.spockframework:spock-core") implementation("org.testcontainers:testcontainers") compileOnly("org.redisson:redisson:3.7.2") diff --git a/instrumentation/redisson/redisson-common/testing/src/main/groovy/AbstractRedissonAsyncClientTest.groovy b/instrumentation/redisson/redisson-common/testing/src/main/groovy/AbstractRedissonAsyncClientTest.groovy deleted file mode 100644 index 4be1622468dc..000000000000 --- a/instrumentation/redisson/redisson-common/testing/src/main/groovy/AbstractRedissonAsyncClientTest.groovy +++ /dev/null @@ -1,238 +0,0 @@ -/* - * 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.semconv.trace.attributes.SemanticAttributes -import org.junit.Assume -import org.redisson.Redisson -import org.redisson.api.BatchOptions -import org.redisson.api.RBatch -import org.redisson.api.RBucket -import org.redisson.api.RFuture -import org.redisson.api.RScheduledExecutorService -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 - -import java.util.concurrent.Callable -import java.util.concurrent.CompletionStage -import java.util.concurrent.TimeUnit - -import static io.opentelemetry.api.trace.SpanKind.CLIENT -import static io.opentelemetry.api.trace.SpanKind.INTERNAL - -abstract class AbstractRedissonAsyncClientTest 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 (useRedisProtocol()) { - // 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) - singleServerConfig.setTimeout(30_000) - // disable connection ping if it exists - singleServerConfig.metaClass.getMetaMethod("setPingConnectionInterval", int)?.invoke(singleServerConfig, 0) - redisson = Redisson.create(config) - clearExportedData() - } - - boolean useRedisProtocol() { - return Boolean.getBoolean("testLatestDeps") - } - - def "test future set"() { - when: - RBucket keyObject = redisson.getBucket("foo") - RFuture future = keyObject.setAsync("bar") - future.get(30, TimeUnit.SECONDS) - - then: - assertTraces(1) { - trace(0, 1) { - span(0) { - name "SET" - kind CLIENT - attributes { - "$SemanticAttributes.NET_SOCK_PEER_ADDR" "127.0.0.1" - "$SemanticAttributes.NET_SOCK_PEER_NAME" "localhost" - "$SemanticAttributes.NET_SOCK_PEER_PORT" port - "$SemanticAttributes.DB_SYSTEM" "redis" - "$SemanticAttributes.DB_STATEMENT" "SET foo ?" - "$SemanticAttributes.DB_OPERATION" "SET" - } - } - } - } - } - - def "test future whenComplete"() { - when: - RSet rSet = redisson.getSet("set1") - CompletionStage result = runWithSpan("parent") { - RFuture future = rSet.addAsync("s1") - return future.whenComplete({ res, throwable -> - if (!Span.current().getSpanContext().isValid()) { - new Exception("Callback should have a parent span.").printStackTrace() - } - runWithSpan("callback") { - } - }) - } - - then: - result.toCompletableFuture().get(30, TimeUnit.SECONDS) - assertTraces(1) { - trace(0, 3) { - span(0) { - name "parent" - kind INTERNAL - hasNoParent() - } - span(1) { - name "SADD" - kind CLIENT - childOf(span(0)) - attributes { - "$SemanticAttributes.NET_SOCK_PEER_ADDR" "127.0.0.1" - "$SemanticAttributes.NET_SOCK_PEER_NAME" "localhost" - "$SemanticAttributes.NET_SOCK_PEER_PORT" port - "$SemanticAttributes.DB_SYSTEM" "redis" - "$SemanticAttributes.DB_STATEMENT" "SADD set1 ?" - "$SemanticAttributes.DB_OPERATION" "SADD" - } - } - span(2) { - name "callback" - kind INTERNAL - childOf(span(0)) - } - } - } - } - - // regression test for https://github.com/open-telemetry/opentelemetry-java-instrumentation/issues/6033 - def "test schedule"() { - RScheduledExecutorService executorService = redisson.getExecutorService("EXECUTOR") - def taskId = executorService.schedule(new MyCallable(), 0, TimeUnit.SECONDS) - .getTaskId() - expect: - taskId != null - } - - private static class MyCallable implements Callable, Serializable { - - @Override - Object call() throws Exception { - return null - } - } - - def "test atomic batch command"() { - try { - // available since 3.7.2 - Class.forName('org.redisson.api.BatchOptions$ExecutionMode') - } catch (ClassNotFoundException exception) { - Assume.assumeNoException(exception) - } - - when: - CompletionStage result = runWithSpan("parent") { - def batchOptions = BatchOptions.defaults().executionMode(BatchOptions.ExecutionMode.REDIS_WRITE_ATOMIC) - RBatch batch = redisson.createBatch(batchOptions) - batch.getBucket("batch1").setAsync("v1") - batch.getBucket("batch2").setAsync("v2") - RFuture future = batch.executeAsync() - return future.whenComplete({ res, throwable -> - if (!Span.current().getSpanContext().isValid()) { - new Exception("Callback should have a parent span.").printStackTrace() - } - runWithSpan("callback") { - } - }) - } - - then: - result.toCompletableFuture().get(30, TimeUnit.SECONDS) - assertTraces(1) { - trace(0, 5) { - span(0) { - name "parent" - kind INTERNAL - hasNoParent() - } - span(1) { - name "DB Query" - kind CLIENT - childOf(span(0)) - attributes { - "$SemanticAttributes.NET_SOCK_PEER_ADDR" "127.0.0.1" - "$SemanticAttributes.NET_SOCK_PEER_NAME" "localhost" - "$SemanticAttributes.NET_SOCK_PEER_PORT" port - "$SemanticAttributes.DB_SYSTEM" "redis" - "$SemanticAttributes.DB_STATEMENT" "MULTI;SET batch1 ?" - "$SemanticAttributes.DB_OPERATION" null - } - } - span(2) { - name "SET" - kind CLIENT - childOf(span(0)) - attributes { - "$SemanticAttributes.NET_SOCK_PEER_ADDR" "127.0.0.1" - "$SemanticAttributes.NET_SOCK_PEER_NAME" "localhost" - "$SemanticAttributes.NET_SOCK_PEER_PORT" port - "$SemanticAttributes.DB_SYSTEM" "redis" - "$SemanticAttributes.DB_STATEMENT" "SET batch2 ?" - "$SemanticAttributes.DB_OPERATION" "SET" - } - } - span(3) { - name "EXEC" - kind CLIENT - childOf(span(0)) - attributes { - "$SemanticAttributes.NET_SOCK_PEER_ADDR" "127.0.0.1" - "$SemanticAttributes.NET_SOCK_PEER_NAME" "localhost" - "$SemanticAttributes.NET_SOCK_PEER_PORT" port - "$SemanticAttributes.DB_SYSTEM" "redis" - "$SemanticAttributes.DB_STATEMENT" "EXEC" - "$SemanticAttributes.DB_OPERATION" "EXEC" - } - } - span(4) { - name "callback" - kind INTERNAL - childOf(span(0)) - } - } - } - } -} diff --git a/instrumentation/redisson/redisson-common/testing/src/main/groovy/AbstractRedissonClientTest.groovy b/instrumentation/redisson/redisson-common/testing/src/main/groovy/AbstractRedissonClientTest.groovy deleted file mode 100644 index 4fd4e486e7c1..000000000000 --- a/instrumentation/redisson/redisson-common/testing/src/main/groovy/AbstractRedissonClientTest.groovy +++ /dev/null @@ -1,388 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes -import org.junit.Assume -import org.redisson.Redisson -import org.redisson.api.BatchOptions -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 - -import static io.opentelemetry.api.trace.SpanKind.CLIENT -import static io.opentelemetry.api.trace.SpanKind.INTERNAL -import static java.util.regex.Pattern.compile -import static java.util.regex.Pattern.quote - -abstract class AbstractRedissonClientTest 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 (useRedisProtocol()) { - // 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() - } - - boolean useRedisProtocol() { - return Boolean.getBoolean("testLatestDeps") - } - - 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.NET_SOCK_PEER_ADDR" "127.0.0.1" - "$SemanticAttributes.NET_SOCK_PEER_NAME" "localhost" - "$SemanticAttributes.NET_SOCK_PEER_PORT" port - "$SemanticAttributes.DB_SYSTEM" "redis" - "$SemanticAttributes.DB_STATEMENT" "SET foo ?" - "$SemanticAttributes.DB_OPERATION" "SET" - } - } - } - trace(1, 1) { - span(0) { - name "GET" - kind CLIENT - attributes { - "$SemanticAttributes.NET_SOCK_PEER_ADDR" "127.0.0.1" - "$SemanticAttributes.NET_SOCK_PEER_NAME" "localhost" - "$SemanticAttributes.NET_SOCK_PEER_PORT" port - "$SemanticAttributes.DB_SYSTEM" "redis" - "$SemanticAttributes.DB_STATEMENT" "GET foo" - "$SemanticAttributes.DB_OPERATION" "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.NET_SOCK_PEER_ADDR" "127.0.0.1" - "$SemanticAttributes.NET_SOCK_PEER_NAME" "localhost" - "$SemanticAttributes.NET_SOCK_PEER_PORT" port - "$SemanticAttributes.DB_SYSTEM" "redis" - "$SemanticAttributes.DB_STATEMENT" "SET batch1 ?;SET batch2 ?" - "$SemanticAttributes.DB_OPERATION" null - } - } - } - } - } - - def "test atomic batch command"() { - try { - // available since 3.7.2 - Class.forName('org.redisson.api.BatchOptions$ExecutionMode') - } catch (ClassNotFoundException exception) { - Assume.assumeNoException(exception) - } - - when: - runWithSpan("parent") { - def batchOptions = BatchOptions.defaults().executionMode(BatchOptions.ExecutionMode.REDIS_WRITE_ATOMIC) - RBatch batch = redisson.createBatch(batchOptions) - batch.getBucket("batch1").setAsync("v1") - batch.getBucket("batch2").setAsync("v2") - batch.execute() - } - - then: - assertTraces(1) { - trace(0, 4) { - span(0) { - name "parent" - kind INTERNAL - hasNoParent() - } - span(1) { - name "DB Query" - kind CLIENT - childOf(span(0)) - attributes { - "$SemanticAttributes.NET_SOCK_PEER_ADDR" "127.0.0.1" - "$SemanticAttributes.NET_SOCK_PEER_NAME" "localhost" - "$SemanticAttributes.NET_SOCK_PEER_PORT" port - "$SemanticAttributes.DB_SYSTEM" "redis" - "$SemanticAttributes.DB_STATEMENT" "MULTI;SET batch1 ?" - "$SemanticAttributes.DB_OPERATION" null - } - } - span(2) { - name "SET" - kind CLIENT - childOf(span(0)) - attributes { - "$SemanticAttributes.NET_SOCK_PEER_ADDR" "127.0.0.1" - "$SemanticAttributes.NET_SOCK_PEER_NAME" "localhost" - "$SemanticAttributes.NET_SOCK_PEER_PORT" port - "$SemanticAttributes.DB_SYSTEM" "redis" - "$SemanticAttributes.DB_STATEMENT" "SET batch2 ?" - "$SemanticAttributes.DB_OPERATION" "SET" - } - } - span(3) { - name "EXEC" - kind CLIENT - childOf(span(0)) - attributes { - "$SemanticAttributes.NET_SOCK_PEER_ADDR" "127.0.0.1" - "$SemanticAttributes.NET_SOCK_PEER_NAME" "localhost" - "$SemanticAttributes.NET_SOCK_PEER_PORT" port - "$SemanticAttributes.DB_SYSTEM" "redis" - "$SemanticAttributes.DB_STATEMENT" "EXEC" - "$SemanticAttributes.DB_OPERATION" "EXEC" - } - } - } - } - } - - 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.NET_SOCK_PEER_ADDR" "127.0.0.1" - "$SemanticAttributes.NET_SOCK_PEER_NAME" "localhost" - "$SemanticAttributes.NET_SOCK_PEER_PORT" port - "$SemanticAttributes.DB_SYSTEM" "redis" - "$SemanticAttributes.DB_STATEMENT" "RPUSH list1 ?" - "$SemanticAttributes.DB_OPERATION" "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.NET_SOCK_PEER_ADDR" "127.0.0.1" - "$SemanticAttributes.NET_SOCK_PEER_NAME" "localhost" - "$SemanticAttributes.NET_SOCK_PEER_PORT" port - "$SemanticAttributes.DB_SYSTEM" "redis" - "$SemanticAttributes.DB_STATEMENT" "EVAL $script 1 map1 ? ?" - "$SemanticAttributes.DB_OPERATION" "EVAL" - } - } - } - trace(1, 1) { - span(0) { - name "HGET" - kind CLIENT - attributes { - "$SemanticAttributes.NET_SOCK_PEER_ADDR" "127.0.0.1" - "$SemanticAttributes.NET_SOCK_PEER_NAME" "localhost" - "$SemanticAttributes.NET_SOCK_PEER_PORT" port - "$SemanticAttributes.DB_SYSTEM" "redis" - "$SemanticAttributes.DB_STATEMENT" "HGET map1 key1" - "$SemanticAttributes.DB_OPERATION" "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.NET_SOCK_PEER_ADDR" "127.0.0.1" - "$SemanticAttributes.NET_SOCK_PEER_NAME" "localhost" - "$SemanticAttributes.NET_SOCK_PEER_PORT" port - "$SemanticAttributes.DB_SYSTEM" "redis" - "$SemanticAttributes.DB_STATEMENT" "SADD set1 ?" - "$SemanticAttributes.DB_OPERATION" "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.NET_SOCK_PEER_ADDR" "127.0.0.1" - "$SemanticAttributes.NET_SOCK_PEER_NAME" "localhost" - "$SemanticAttributes.NET_SOCK_PEER_PORT" port - "$SemanticAttributes.DB_SYSTEM" "redis" - "$SemanticAttributes.DB_STATEMENT" "ZADD sort_set1 ? ? ? ? ? ?" - "$SemanticAttributes.DB_OPERATION" "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.NET_SOCK_PEER_ADDR" "127.0.0.1" - "$SemanticAttributes.NET_SOCK_PEER_NAME" "localhost" - "$SemanticAttributes.NET_SOCK_PEER_PORT" port - "$SemanticAttributes.DB_SYSTEM" "redis" - "$SemanticAttributes.DB_STATEMENT" "INCR AtomicLong" - "$SemanticAttributes.DB_OPERATION" "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.NET_SOCK_PEER_ADDR" "127.0.0.1" - "$SemanticAttributes.NET_SOCK_PEER_NAME" "localhost" - "$SemanticAttributes.NET_SOCK_PEER_PORT" port - "$SemanticAttributes.DB_SYSTEM" "redis" - "$SemanticAttributes.DB_STATEMENT" { lockScriptPattern.matcher(it).matches() } - "$SemanticAttributes.DB_OPERATION" "EVAL" - } - } - } - trace(1, 1) { - span(0) { - def lockScriptPattern = compile("^" + quote("EVAL ") + ".*" + quote(" 2 lock ") + "\\S+" + "(" + quote(" ?") + ")+\$") - - name "EVAL" - kind CLIENT - attributes { - "$SemanticAttributes.NET_SOCK_PEER_ADDR" "127.0.0.1" - "$SemanticAttributes.NET_SOCK_PEER_NAME" "localhost" - "$SemanticAttributes.NET_SOCK_PEER_PORT" port - "$SemanticAttributes.DB_SYSTEM" "redis" - "$SemanticAttributes.DB_STATEMENT" { lockScriptPattern.matcher(it).matches() } - "$SemanticAttributes.DB_OPERATION" "EVAL" - } - } - } - } - } -} diff --git a/instrumentation/redisson/redisson-common/testing/src/main/java/io/opentelemetry/javaagent/instrumentation/redisson/AbstractRedissonAsyncClientTest.java b/instrumentation/redisson/redisson-common/testing/src/main/java/io/opentelemetry/javaagent/instrumentation/redisson/AbstractRedissonAsyncClientTest.java new file mode 100644 index 000000000000..9a005c90315c --- /dev/null +++ b/instrumentation/redisson/redisson-common/testing/src/main/java/io/opentelemetry/javaagent/instrumentation/redisson/AbstractRedissonAsyncClientTest.java @@ -0,0 +1,267 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.redisson; + +import static io.opentelemetry.api.trace.SpanKind.CLIENT; +import static io.opentelemetry.api.trace.SpanKind.INTERNAL; +import static io.opentelemetry.instrumentation.testing.util.TelemetryDataUtil.orderByRootSpanKind; +import static io.opentelemetry.instrumentation.testing.util.TelemetryDataUtil.orderByRootSpanName; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.semconv.NetworkAttributes; +import io.opentelemetry.semconv.incubating.DbIncubatingAttributes; +import java.io.Serializable; +import java.lang.reflect.InvocationTargetException; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.concurrent.Callable; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import org.junit.Assume; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.redisson.Redisson; +import org.redisson.api.BatchOptions; +import org.redisson.api.RBatch; +import org.redisson.api.RBucket; +import org.redisson.api.RFuture; +import org.redisson.api.RScheduledExecutorService; +import org.redisson.api.RScheduledFuture; +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; + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +public abstract class AbstractRedissonAsyncClientTest { + + @RegisterExtension + protected static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + private static final GenericContainer redisServer = + new GenericContainer<>("redis:6.2.3-alpine").withExposedPorts(6379); + + private static String ip; + + private static int port; + + private static String address; + private static RedissonClient redisson; + + @BeforeAll + static void setupAll() throws UnknownHostException { + redisServer.start(); + ip = InetAddress.getByName(redisServer.getHost()).getHostAddress(); + port = redisServer.getMappedPort(6379); + address = redisServer.getHost() + ":" + port; + } + + @AfterAll + static void cleanupAll() { + redisson.shutdown(); + redisServer.stop(); + } + + @BeforeEach + void setup() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { + String newAddress = address; + if (useRedisProtocol()) { + // Newer versions of redisson require scheme, older versions forbid it + newAddress = "redis://" + address; + } + Config config = new Config(); + SingleServerConfig singleServerConfig = config.useSingleServer(); + singleServerConfig.setAddress(newAddress); + singleServerConfig.setTimeout(30_000); + try { + // disable connection ping if it exists + singleServerConfig + .getClass() + .getMethod("setPingConnectionInterval", int.class) + .invoke(singleServerConfig, 0); + } catch (NoSuchMethodException ignored) { + } + redisson = Redisson.create(config); + testing.clearData(); + } + + @Test + void futureSet() throws ExecutionException, InterruptedException, TimeoutException { + RBucket keyObject = redisson.getBucket("foo"); + RFuture future = keyObject.setAsync("bar"); + future.get(30, TimeUnit.SECONDS); + + testing.waitAndAssertSortedTraces( + orderByRootSpanKind(SpanKind.INTERNAL, SpanKind.CLIENT), + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("SET") + .hasKind(CLIENT) + .hasAttributesSatisfyingExactly( + equalTo(NetworkAttributes.NETWORK_TYPE, "ipv4"), + equalTo(NetworkAttributes.NETWORK_PEER_ADDRESS, ip), + equalTo(NetworkAttributes.NETWORK_PEER_PORT, (long) port), + equalTo(DbIncubatingAttributes.DB_SYSTEM, "redis"), + equalTo(DbIncubatingAttributes.DB_STATEMENT, "SET foo ?"), + equalTo(DbIncubatingAttributes.DB_OPERATION, "SET")))); + } + + @Test + void futureWhenComplete() throws ExecutionException, InterruptedException, TimeoutException { + RSet set = redisson.getSet("set1"); + CompletionStage result = + testing.runWithSpan( + "parent", + () -> { + RFuture future = set.addAsync("s1"); + return future.whenComplete( + (res, throwable) -> { + assertThat(Span.current().getSpanContext().isValid()).isTrue(); + testing.runWithSpan("callback", () -> {}); + }); + }); + result.toCompletableFuture().get(30, TimeUnit.SECONDS); + + testing.waitAndAssertSortedTraces( + orderByRootSpanName("parent", "SADD", "callback"), + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasKind(INTERNAL).hasNoParent(), + span -> + span.hasName("SADD") + .hasKind(CLIENT) + .hasAttributesSatisfyingExactly( + equalTo(NetworkAttributes.NETWORK_TYPE, "ipv4"), + equalTo(NetworkAttributes.NETWORK_PEER_ADDRESS, ip), + equalTo(NetworkAttributes.NETWORK_PEER_PORT, (long) port), + equalTo(DbIncubatingAttributes.DB_SYSTEM, "redis"), + equalTo(DbIncubatingAttributes.DB_STATEMENT, "SADD set1 ?"), + equalTo(DbIncubatingAttributes.DB_OPERATION, "SADD")) + .hasParent(trace.getSpan(0)), + span -> span.hasName("callback").hasKind(INTERNAL).hasParent(trace.getSpan(0)))); + } + + // regression test for + // https://github.com/open-telemetry/opentelemetry-java-instrumentation/issues/6033 + @Test + void scheduleCallable() + throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { + RScheduledExecutorService executorService = redisson.getExecutorService("EXECUTOR"); + // Adapt different method signature: + // `java.util.concurrent.ScheduledFuture schedule(Callable,long,TimeUnit)` in 3.0.1 + // and `org.redisson.api.RScheduledFuture#schedule(java.lang.Runnable, long,TimeUnit)` + // in other versions + Object future = invokeSchedule(executorService); + // In 3.0.1 getTaskId method doesn't exist in`ScheduledFuture` as it belongs to java.util.* + // package, + // but in RScheduledFuture that is an implementation of `ScheduledFuture` + assertThat(future instanceof RScheduledFuture).isTrue(); + assertThat(((RScheduledFuture) future).getTaskId()).isNotBlank(); + } + + private static Object invokeSchedule(RScheduledExecutorService executorService) + throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { + return executorService + .getClass() + .getMethod("schedule", Callable.class, long.class, TimeUnit.class) + .invoke(executorService, new MyCallable(), 0, TimeUnit.SECONDS); + } + + @Test + void atomicBatchCommand() throws ExecutionException, InterruptedException, TimeoutException { + try { + // available since 3.7.2 + Class.forName("org.redisson.api.BatchOptions$ExecutionMode"); + } catch (ClassNotFoundException exception) { + Assume.assumeNoException(exception); + } + // Don't specify explicit generic type, because `BatchResult` not exist in some versions. + CompletionStage result = + testing.runWithSpan( + "parent", + () -> { + BatchOptions batchOptions = + BatchOptions.defaults() + .executionMode(BatchOptions.ExecutionMode.REDIS_WRITE_ATOMIC); + RBatch batch = redisson.createBatch(batchOptions); + batch.getBucket("batch1").setAsync("v1"); + batch.getBucket("batch2").setAsync("v2"); + RFuture batchResultFuture = batch.executeAsync(); + + return batchResultFuture.whenComplete( + (res, throwable) -> { + assertThat(Span.current().getSpanContext().isValid()).isTrue(); + testing.runWithSpan("callback", () -> {}); + }); + }); + result.toCompletableFuture().get(30, TimeUnit.SECONDS); + + testing.waitAndAssertSortedTraces( + orderByRootSpanName("parent", "SADD", "callback"), + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasKind(INTERNAL).hasNoParent(), + span -> + span.hasName("DB Query") + .hasKind(CLIENT) + .hasAttributesSatisfyingExactly( + equalTo(NetworkAttributes.NETWORK_TYPE, "ipv4"), + equalTo(NetworkAttributes.NETWORK_PEER_ADDRESS, ip), + equalTo(NetworkAttributes.NETWORK_PEER_PORT, (long) port), + equalTo(DbIncubatingAttributes.DB_SYSTEM, "redis"), + equalTo(DbIncubatingAttributes.DB_STATEMENT, "MULTI;SET batch1 ?")) + .hasParent(trace.getSpan(0)), + span -> + span.hasName("SET") + .hasKind(CLIENT) + .hasAttributesSatisfyingExactly( + equalTo(NetworkAttributes.NETWORK_TYPE, "ipv4"), + equalTo(NetworkAttributes.NETWORK_PEER_ADDRESS, ip), + equalTo(NetworkAttributes.NETWORK_PEER_PORT, (long) port), + equalTo(DbIncubatingAttributes.DB_SYSTEM, "redis"), + equalTo(DbIncubatingAttributes.DB_STATEMENT, "SET batch2 ?"), + equalTo(DbIncubatingAttributes.DB_OPERATION, "SET")) + .hasParent(trace.getSpan(0)), + span -> + span.hasName("EXEC") + .hasKind(CLIENT) + .hasAttributesSatisfyingExactly( + equalTo(NetworkAttributes.NETWORK_TYPE, "ipv4"), + equalTo(NetworkAttributes.NETWORK_PEER_ADDRESS, ip), + equalTo(NetworkAttributes.NETWORK_PEER_PORT, (long) port), + equalTo(DbIncubatingAttributes.DB_SYSTEM, "redis"), + equalTo(DbIncubatingAttributes.DB_STATEMENT, "EXEC"), + equalTo(DbIncubatingAttributes.DB_OPERATION, "EXEC")) + .hasParent(trace.getSpan(0)), + span -> span.hasName("callback").hasKind(INTERNAL).hasParent(trace.getSpan(0)))); + } + + protected boolean useRedisProtocol() { + return Boolean.getBoolean("testLatestDeps"); + } + + private static class MyCallable implements Serializable, Callable { + private static final long serialVersionUID = 1L; + + @Override + public Object call() throws Exception { + return null; + } + } +} diff --git a/instrumentation/redisson/redisson-common/testing/src/main/java/io/opentelemetry/javaagent/instrumentation/redisson/AbstractRedissonClientTest.java b/instrumentation/redisson/redisson-common/testing/src/main/java/io/opentelemetry/javaagent/instrumentation/redisson/AbstractRedissonClientTest.java new file mode 100644 index 000000000000..d25aa7e4fcd4 --- /dev/null +++ b/instrumentation/redisson/redisson-common/testing/src/main/java/io/opentelemetry/javaagent/instrumentation/redisson/AbstractRedissonClientTest.java @@ -0,0 +1,439 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.redisson; + +import static io.opentelemetry.api.trace.SpanKind.CLIENT; +import static io.opentelemetry.api.trace.SpanKind.INTERNAL; +import static io.opentelemetry.instrumentation.testing.util.TelemetryDataUtil.orderByRootSpanKind; +import static io.opentelemetry.instrumentation.testing.util.TelemetryDataUtil.orderByRootSpanName; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.satisfies; +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.sdk.testing.assertj.TraceAssert; +import io.opentelemetry.semconv.NetworkAttributes; +import io.opentelemetry.semconv.incubating.DbIncubatingAttributes; +import java.lang.reflect.InvocationTargetException; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; +import org.junit.Assume; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.redisson.Redisson; +import org.redisson.api.BatchOptions; +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.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testcontainers.containers.GenericContainer; + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +public abstract class AbstractRedissonClientTest { + + private static final Logger logger = LoggerFactory.getLogger(AbstractRedissonClientTest.class); + + @RegisterExtension + static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + private static final GenericContainer redisServer = + new GenericContainer<>("redis:6.2.3-alpine").withExposedPorts(6379); + + private static String ip; + + private static int port; + private static String address; + private RedissonClient redisson; + + @BeforeAll + static void setupAll() throws UnknownHostException { + redisServer.start(); + ip = InetAddress.getByName(redisServer.getHost()).getHostAddress(); + port = redisServer.getMappedPort(6379); + address = redisServer.getHost() + ":" + port; + } + + @AfterAll + static void cleanupAll() { + redisServer.stop(); + } + + @BeforeEach + void setup() throws InvocationTargetException, IllegalAccessException { + String newAddress = address; + if (useRedisProtocol()) { + // Newer versions of redisson require scheme, older versions forbid it + newAddress = "redis://" + address; + } + Config config = new Config(); + SingleServerConfig singleServerConfig = config.useSingleServer(); + singleServerConfig.setAddress(newAddress); + singleServerConfig.setTimeout(30_000); + try { + // disable connection ping if it exists + singleServerConfig + .getClass() + .getMethod("setPingConnectionInterval", int.class) + .invoke(singleServerConfig, 0); + } catch (NoSuchMethodException ignored) { + } + redisson = Redisson.create(config); + testing.clearData(); + } + + @Test + void stringCommand() { + RBucket keyObject = redisson.getBucket("foo"); + keyObject.set("bar"); + keyObject.get(); + testing.waitAndAssertSortedTraces( + orderByRootSpanName("SET", "GET"), + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("SET") + .hasKind(CLIENT) + .hasAttributesSatisfyingExactly( + equalTo(NetworkAttributes.NETWORK_TYPE, "ipv4"), + equalTo(NetworkAttributes.NETWORK_PEER_ADDRESS, ip), + equalTo(NetworkAttributes.NETWORK_PEER_PORT, (long) port), + equalTo(DbIncubatingAttributes.DB_SYSTEM, "redis"), + equalTo(DbIncubatingAttributes.DB_STATEMENT, "SET foo ?"), + equalTo(DbIncubatingAttributes.DB_OPERATION, "SET"))), + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("GET") + .hasKind(CLIENT) + .hasAttributesSatisfyingExactly( + equalTo(NetworkAttributes.NETWORK_TYPE, "ipv4"), + equalTo(NetworkAttributes.NETWORK_PEER_ADDRESS, ip), + equalTo(NetworkAttributes.NETWORK_PEER_PORT, (long) port), + equalTo(DbIncubatingAttributes.DB_SYSTEM, "redis"), + equalTo(DbIncubatingAttributes.DB_STATEMENT, "GET foo"), + equalTo(DbIncubatingAttributes.DB_OPERATION, "GET")))); + } + + @Test + void batchCommand() + throws InvocationTargetException, NoSuchMethodException, IllegalAccessException { + RBatch batch = createBatch(redisson); + assertThat(batch).isNotNull(); + batch.getBucket("batch1").setAsync("v1"); + batch.getBucket("batch2").setAsync("v2"); + // Adapt different method signature: + // `BatchResult execute()` and `List execute()` + invokeExecute(batch); + testing.waitAndAssertSortedTraces( + orderByRootSpanName("SET", "GET"), + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("DB Query") + .hasKind(CLIENT) + .hasAttributesSatisfyingExactly( + equalTo(NetworkAttributes.NETWORK_TYPE, "ipv4"), + equalTo(NetworkAttributes.NETWORK_PEER_ADDRESS, ip), + equalTo(NetworkAttributes.NETWORK_PEER_PORT, (long) port), + equalTo(DbIncubatingAttributes.DB_SYSTEM, "redis"), + equalTo( + DbIncubatingAttributes.DB_STATEMENT, + "SET batch1 ?;SET batch2 ?")))); + } + + private static void invokeExecute(RBatch batch) + throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { + batch.getClass().getMethod("execute").invoke(batch); + } + + @Test + void atomicBatchCommand() { + try { + // available since 3.7.2 + Class.forName("org.redisson.api.BatchOptions$ExecutionMode"); + } catch (ClassNotFoundException exception) { + Assume.assumeNoException(exception); + } + + testing.runWithSpan( + "parent", + () -> { + BatchOptions batchOptions = + BatchOptions.defaults().executionMode(BatchOptions.ExecutionMode.REDIS_WRITE_ATOMIC); + RBatch batch = redisson.createBatch(batchOptions); + batch.getBucket("batch1").setAsync("v1"); + batch.getBucket("batch2").setAsync("v2"); + batch.execute(); + }); + testing.waitAndAssertSortedTraces( + orderByRootSpanName("DB Query", "SET", "EXEC"), + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasNoParent().hasKind(INTERNAL), + span -> + span.hasName("DB Query") + .hasKind(CLIENT) + .hasAttributesSatisfyingExactly( + equalTo(NetworkAttributes.NETWORK_TYPE, "ipv4"), + equalTo(NetworkAttributes.NETWORK_PEER_ADDRESS, ip), + equalTo(NetworkAttributes.NETWORK_PEER_PORT, (long) port), + equalTo(DbIncubatingAttributes.DB_SYSTEM, "redis"), + equalTo(DbIncubatingAttributes.DB_STATEMENT, "MULTI;SET batch1 ?")) + .hasParent(trace.getSpan(0)), + span -> + span.hasName("SET") + .hasKind(CLIENT) + .hasAttributesSatisfyingExactly( + equalTo(NetworkAttributes.NETWORK_TYPE, "ipv4"), + equalTo(NetworkAttributes.NETWORK_PEER_ADDRESS, ip), + equalTo(NetworkAttributes.NETWORK_PEER_PORT, (long) port), + equalTo(DbIncubatingAttributes.DB_SYSTEM, "redis"), + equalTo(DbIncubatingAttributes.DB_STATEMENT, "SET batch2 ?"), + equalTo(DbIncubatingAttributes.DB_OPERATION, "SET")) + .hasParent(trace.getSpan(0)), + span -> + span.hasName("EXEC") + .hasKind(CLIENT) + .hasAttributesSatisfyingExactly( + equalTo(NetworkAttributes.NETWORK_TYPE, "ipv4"), + equalTo(NetworkAttributes.NETWORK_PEER_ADDRESS, ip), + equalTo(NetworkAttributes.NETWORK_PEER_PORT, (long) port), + equalTo(DbIncubatingAttributes.DB_SYSTEM, "redis"), + equalTo(DbIncubatingAttributes.DB_STATEMENT, "EXEC"), + equalTo(DbIncubatingAttributes.DB_OPERATION, "EXEC")) + .hasParent(trace.getSpan(0)))); + } + + @Test + void listCommand() { + RList strings = redisson.getList("list1"); + strings.add("a"); + + testing.waitAndAssertSortedTraces( + orderByRootSpanKind(SpanKind.CLIENT), + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("RPUSH") + .hasKind(CLIENT) + .hasAttributesSatisfyingExactly( + equalTo(NetworkAttributes.NETWORK_TYPE, "ipv4"), + equalTo(NetworkAttributes.NETWORK_PEER_ADDRESS, ip), + equalTo(NetworkAttributes.NETWORK_PEER_PORT, (long) port), + equalTo(DbIncubatingAttributes.DB_SYSTEM, "redis"), + equalTo(DbIncubatingAttributes.DB_STATEMENT, "RPUSH list1 ?"), + equalTo(DbIncubatingAttributes.DB_OPERATION, "RPUSH")) + .hasNoParent())); + } + + @Test + void hashCommand() { + RMap map = redisson.getMap("map1"); + map.put("key1", "value1"); + map.get("key1"); + + String script = + "local v = redis.call('hget', KEYS[1], ARGV[1]); redis.call('hset', KEYS[1], ARGV[1], ARGV[2]); return v"; + testing.waitAndAssertSortedTraces( + orderByRootSpanKind(SpanKind.INTERNAL, SpanKind.CLIENT), + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("EVAL") + .hasKind(CLIENT) + .hasAttributesSatisfyingExactly( + equalTo(NetworkAttributes.NETWORK_TYPE, "ipv4"), + equalTo(NetworkAttributes.NETWORK_PEER_ADDRESS, ip), + equalTo(NetworkAttributes.NETWORK_PEER_PORT, (long) port), + equalTo(DbIncubatingAttributes.DB_SYSTEM, "redis"), + equalTo( + DbIncubatingAttributes.DB_STATEMENT, + String.format("EVAL %s 1 map1 ? ?", script)), + equalTo(DbIncubatingAttributes.DB_OPERATION, "EVAL"))), + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("HGET") + .hasKind(CLIENT) + .hasAttributesSatisfyingExactly( + equalTo(NetworkAttributes.NETWORK_TYPE, "ipv4"), + equalTo(NetworkAttributes.NETWORK_PEER_ADDRESS, ip), + equalTo(NetworkAttributes.NETWORK_PEER_PORT, (long) port), + equalTo(DbIncubatingAttributes.DB_SYSTEM, "redis"), + equalTo(DbIncubatingAttributes.DB_STATEMENT, "HGET map1 key1"), + equalTo(DbIncubatingAttributes.DB_OPERATION, "HGET")))); + } + + @Test + void setCommand() { + RSet set = redisson.getSet("set1"); + set.add("s1"); + + testing.waitAndAssertSortedTraces( + orderByRootSpanKind(SpanKind.INTERNAL, SpanKind.CLIENT), + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("SADD") + .hasKind(CLIENT) + .hasAttributesSatisfyingExactly( + equalTo(NetworkAttributes.NETWORK_TYPE, "ipv4"), + equalTo(NetworkAttributes.NETWORK_PEER_ADDRESS, ip), + equalTo(NetworkAttributes.NETWORK_PEER_PORT, (long) port), + equalTo(DbIncubatingAttributes.DB_SYSTEM, "redis"), + equalTo(DbIncubatingAttributes.DB_STATEMENT, "SADD set1 ?"), + equalTo(DbIncubatingAttributes.DB_OPERATION, "SADD")))); + } + + @Test + void sortedSetCommand() + throws InvocationTargetException, NoSuchMethodException, IllegalAccessException { + 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"); + // Adapt different method signature: + // `Long addAll(Map objects);` and `int addAll(Map objects);` + invokeAddAll(sortSet, scores); + + testing.waitAndAssertSortedTraces( + orderByRootSpanName("ZADD"), + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("ZADD") + .hasKind(CLIENT) + .hasAttributesSatisfyingExactly( + equalTo(NetworkAttributes.NETWORK_TYPE, "ipv4"), + equalTo(NetworkAttributes.NETWORK_PEER_ADDRESS, ip), + equalTo(NetworkAttributes.NETWORK_PEER_PORT, (long) port), + equalTo(DbIncubatingAttributes.DB_SYSTEM, "redis"), + equalTo( + DbIncubatingAttributes.DB_STATEMENT, "ZADD sort_set1 ? ? ? ? ? ?"), + equalTo(DbIncubatingAttributes.DB_OPERATION, "ZADD")))); + } + + private static void invokeAddAll(RScoredSortedSet object, Map arg) + throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { + object.getClass().getMethod("addAll", Map.class).invoke(object, arg); + } + + @Test + void atomicLongCommand() { + RAtomicLong atomicLong = redisson.getAtomicLong("AtomicLong"); + atomicLong.incrementAndGet(); + + testing.waitAndAssertSortedTraces( + orderByRootSpanKind(SpanKind.INTERNAL, SpanKind.CLIENT), + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("INCR") + .hasKind(CLIENT) + .hasAttributesSatisfyingExactly( + equalTo(NetworkAttributes.NETWORK_TYPE, "ipv4"), + equalTo(NetworkAttributes.NETWORK_PEER_ADDRESS, ip), + equalTo(NetworkAttributes.NETWORK_PEER_PORT, (long) port), + equalTo(DbIncubatingAttributes.DB_SYSTEM, "redis"), + equalTo(DbIncubatingAttributes.DB_STATEMENT, "INCR AtomicLong"), + equalTo(DbIncubatingAttributes.DB_OPERATION, "INCR")))); + } + + @Test + void lockCommand() { + RLock lock = redisson.getLock("lock"); + lock.lock(); + try { + logger.info("enter lock block"); + } finally { + lock.unlock(); + } + + List> traceAsserts = new ArrayList<>(); + traceAsserts.add( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("EVAL") + .hasKind(CLIENT) + .hasAttributesSatisfyingExactly( + equalTo(NetworkAttributes.NETWORK_TYPE, "ipv4"), + equalTo(NetworkAttributes.NETWORK_PEER_ADDRESS, ip), + equalTo(NetworkAttributes.NETWORK_PEER_PORT, (long) port), + equalTo(DbIncubatingAttributes.DB_SYSTEM, "redis"), + equalTo(DbIncubatingAttributes.DB_OPERATION, "EVAL"), + satisfies( + DbIncubatingAttributes.DB_STATEMENT, + stringAssert -> stringAssert.startsWith("EVAL"))))); + traceAsserts.add( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("EVAL") + .hasKind(CLIENT) + .hasAttributesSatisfyingExactly( + equalTo(NetworkAttributes.NETWORK_TYPE, "ipv4"), + equalTo(NetworkAttributes.NETWORK_PEER_ADDRESS, ip), + equalTo(NetworkAttributes.NETWORK_PEER_PORT, (long) port), + equalTo(DbIncubatingAttributes.DB_SYSTEM, "redis"), + equalTo(DbIncubatingAttributes.DB_OPERATION, "EVAL"), + satisfies( + DbIncubatingAttributes.DB_STATEMENT, + stringAssert -> stringAssert.startsWith("EVAL"))))); + if (lockHas3Traces()) { + traceAsserts.add( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("DEL") + .hasKind(CLIENT) + .hasAttributesSatisfyingExactly( + equalTo(NetworkAttributes.NETWORK_TYPE, "ipv4"), + equalTo(NetworkAttributes.NETWORK_PEER_ADDRESS, ip), + equalTo(NetworkAttributes.NETWORK_PEER_PORT, (long) port), + equalTo(DbIncubatingAttributes.DB_SYSTEM, "redis"), + equalTo(DbIncubatingAttributes.DB_OPERATION, "DEL"), + satisfies( + DbIncubatingAttributes.DB_STATEMENT, + stringAssert -> stringAssert.startsWith("DEL"))))); + } + + testing.waitAndAssertSortedTraces(orderByRootSpanKind(SpanKind.CLIENT), traceAsserts); + } + + protected boolean useRedisProtocol() { + return Boolean.getBoolean("testLatestDeps"); + } + + protected boolean lockHas3Traces() { + return false; + } + + protected RBatch createBatch(RedissonClient redisson) { + return redisson.createBatch(BatchOptions.defaults()); + } +} diff --git a/instrumentation/resources/library/README.md b/instrumentation/resources/library/README.md index 9e377ded6908..ca7a790ec2cc 100644 --- a/instrumentation/resources/library/README.md +++ b/instrumentation/resources/library/README.md @@ -9,7 +9,7 @@ common environments. Currently, the resources provide the following semantic con Provider: `io.opentelemetry.instrumentation.resources.ContainerResource` -Specification: +Specification: Implemented attributes: @@ -19,18 +19,26 @@ Implemented attributes: Provider: `io.opentelemetry.instrumentation.resources.HostResource` -Specification: +Specification: Implemented attributes: - `host.name` - `host.arch` +Provider: `io.opentelemetry.instrumentation.resources.HostIdResourceProvider` + +Specification: + +Implemented attributes: + +- `host.id` + ### Operating System Provider: `io.opentelemetry.instrumentation.resources.OsResource` -Specification: +Specification: Implemented attributes: @@ -41,7 +49,7 @@ Implemented attributes: Implementation: `io.opentelemetry.instrumentation.resources.ProcessResource` -Specification: +Specification: Implemented attributes: @@ -53,7 +61,7 @@ Implemented attributes: Implementation: `io.opentelemetry.instrumentation.resources.ProcessRuntimeResource` -Specification: +Specification: Implemented attributes: diff --git a/instrumentation/resources/library/build.gradle.kts b/instrumentation/resources/library/build.gradle.kts index 52e35a61d3b6..cf3969d90d12 100644 --- a/instrumentation/resources/library/build.gradle.kts +++ b/instrumentation/resources/library/build.gradle.kts @@ -6,8 +6,8 @@ val mrJarVersions = listOf(9, 11) dependencies { implementation("io.opentelemetry:opentelemetry-sdk-common") - implementation("io.opentelemetry:opentelemetry-semconv") implementation("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure-spi") + implementation("io.opentelemetry.semconv:opentelemetry-semconv") annotationProcessor("com.google.auto.service:auto-service") compileOnly("com.google.auto.service:auto-service-annotations") @@ -50,9 +50,17 @@ for (version in mrJarVersions) { tasks { withType(Jar::class) { + val sourcePathProvider = if (name == "jar") { + { ss: SourceSet? -> ss?.output } + } else if (name == "sourcesJar") { + { ss: SourceSet? -> ss?.java } + } else { + { project.objects.fileCollection() } + } + for (version in mrJarVersions) { into("META-INF/versions/$version") { - from(sourceSets["java$version"].output) + from(sourcePathProvider(sourceSets["java$version"])) } } manifest.attributes( diff --git a/instrumentation/resources/library/src/main/java/io/opentelemetry/instrumentation/resources/AttributeProvider.java b/instrumentation/resources/library/src/main/java/io/opentelemetry/instrumentation/resources/AttributeProvider.java new file mode 100644 index 000000000000..0e74368ff3fd --- /dev/null +++ b/instrumentation/resources/library/src/main/java/io/opentelemetry/instrumentation/resources/AttributeProvider.java @@ -0,0 +1,28 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.resources; + +import com.google.errorprone.annotations.CanIgnoreReturnValue; +import io.opentelemetry.api.common.AttributeKey; +import java.util.Optional; +import java.util.function.Function; + +/** + * An easier alternative to {@link io.opentelemetry.sdk.autoconfigure.spi.ResourceProvider}, which + * avoids some common pitfalls and boilerplate. + * + *

    An example of how to use this interface can be found in {@link ManifestResourceProvider}. + */ +interface AttributeProvider { + Optional readData(); + + void registerAttributes(Builder builder); + + interface Builder { + @CanIgnoreReturnValue + Builder add(AttributeKey key, Function> getter); + } +} diff --git a/instrumentation/resources/library/src/main/java/io/opentelemetry/instrumentation/resources/AttributeResourceProvider.java b/instrumentation/resources/library/src/main/java/io/opentelemetry/instrumentation/resources/AttributeResourceProvider.java new file mode 100644 index 000000000000..594b69cad2cb --- /dev/null +++ b/instrumentation/resources/library/src/main/java/io/opentelemetry/instrumentation/resources/AttributeResourceProvider.java @@ -0,0 +1,110 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.resources; + +import com.google.errorprone.annotations.CanIgnoreReturnValue; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import io.opentelemetry.sdk.autoconfigure.spi.internal.ConditionalResourceProvider; +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.semconv.ServiceAttributes; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * An easier alternative to {@link io.opentelemetry.sdk.autoconfigure.spi.ResourceProvider}, which + * avoids some common pitfalls and boilerplate. + * + *

    An example of how to use this interface can be found in {@link ManifestResourceProvider}. + */ +public abstract class AttributeResourceProvider implements ConditionalResourceProvider { + + private final AttributeProvider attributeProvider; + + public class AttributeBuilder implements AttributeProvider.Builder { + + private AttributeBuilder() {} + + @CanIgnoreReturnValue + @Override + @SuppressWarnings({"unchecked", "rawtypes"}) + public AttributeBuilder add(AttributeKey key, Function> getter) { + attributeGetters.put((AttributeKey) key, Objects.requireNonNull((Function) getter)); + return this; + } + } + + private Set> filteredKeys; + + private final Map, Function>> attributeGetters = + new HashMap<>(); + + AttributeResourceProvider(AttributeProvider attributeProvider) { + this.attributeProvider = attributeProvider; + attributeProvider.registerAttributes(new AttributeBuilder()); + } + + @Override + public final boolean shouldApply(ConfigProperties config, Resource existing) { + Map resourceAttributes = config.getMap("otel.resource.attributes"); + filteredKeys = + attributeGetters.keySet().stream() + .filter(key -> shouldUpdate(config, existing, key, resourceAttributes)) + .collect(Collectors.toSet()); + return !filteredKeys.isEmpty(); + } + + @Override + public final Resource createResource(ConfigProperties config) { + return attributeProvider + .readData() + .map( + data -> { + if (filteredKeys == null) { + throw new IllegalStateException("shouldApply should be called first"); + } + AttributesBuilder builder = Attributes.builder(); + attributeGetters.entrySet().stream() + .filter(e -> filteredKeys.contains(e.getKey())) + .forEach( + e -> + e.getValue() + .apply(data) + .ifPresent(value -> putAttribute(builder, e.getKey(), value))); + return Resource.create(builder.build()); + }) + .orElse(Resource.empty()); + } + + private static void putAttribute(AttributesBuilder builder, AttributeKey key, T value) { + builder.put(key, value); + } + + private static boolean shouldUpdate( + ConfigProperties config, + Resource existing, + AttributeKey key, + Map resourceAttributes) { + if (resourceAttributes.containsKey(key.getKey())) { + return false; + } + + Object value = existing.getAttribute(key); + + if (key.equals(ServiceAttributes.SERVICE_NAME)) { + return config.getString("otel.service.name") == null && "unknown_service:java".equals(value); + } + + return value == null; + } +} diff --git a/instrumentation/resources/library/src/main/java/io/opentelemetry/instrumentation/resources/CgroupV2ContainerIdExtractor.java b/instrumentation/resources/library/src/main/java/io/opentelemetry/instrumentation/resources/CgroupV2ContainerIdExtractor.java index f7fc784d1a77..4aa13a15acfb 100644 --- a/instrumentation/resources/library/src/main/java/io/opentelemetry/instrumentation/resources/CgroupV2ContainerIdExtractor.java +++ b/instrumentation/resources/library/src/main/java/io/opentelemetry/instrumentation/resources/CgroupV2ContainerIdExtractor.java @@ -10,6 +10,7 @@ import java.io.IOException; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.List; import java.util.Optional; import java.util.logging.Level; import java.util.logging.Logger; @@ -25,6 +26,9 @@ class CgroupV2ContainerIdExtractor { static final Path V2_CGROUP_PATH = Paths.get("/proc/self/mountinfo"); private static final Pattern CONTAINER_ID_RE = Pattern.compile("^[0-9a-f]{64}$"); + private static final Pattern CONTAINERD_CONTAINER_ID_RE = + Pattern.compile("cri-containerd:[0-9a-f]{64}"); + private static final Pattern CRIO_CONTAINER_ID_RE = Pattern.compile("\\/crio-[0-9a-f]{64}"); private final ContainerResource.Filesystem filesystem; @@ -41,18 +45,43 @@ Optional extractContainerId() { if (!filesystem.isReadable(V2_CGROUP_PATH)) { return empty(); } + + List fileAsList; try { - return filesystem - .lines(V2_CGROUP_PATH) - .filter(line -> line.contains("hostname")) - .flatMap(line -> Stream.of(line.split("/"))) - .map(CONTAINER_ID_RE::matcher) - .filter(Matcher::matches) - .findFirst() - .map(matcher -> matcher.group(0)); + fileAsList = filesystem.lineList(V2_CGROUP_PATH); } catch (IOException e) { logger.log(Level.WARNING, "Unable to read v2 cgroup path", e); + return empty(); + } + + Optional optCid = + fileAsList.stream() + .filter(line -> line.contains("/crio-")) + .map(CRIO_CONTAINER_ID_RE::matcher) + .filter(Matcher::find) + .findFirst() + .map(matcher -> matcher.group(0).substring(6)); + if (optCid.isPresent()) { + return optCid; } - return empty(); + + optCid = + fileAsList.stream() + .filter(line -> line.contains("cri-containerd:")) + .map(CONTAINERD_CONTAINER_ID_RE::matcher) + .filter(Matcher::find) + .findFirst() + .map(matcher -> matcher.group(0).substring(15)); + if (optCid.isPresent()) { + return optCid; + } + + return fileAsList.stream() + .filter(line -> line.contains("/containers/")) + .flatMap(line -> Stream.of(line.split("/"))) + .map(CONTAINER_ID_RE::matcher) + .filter(Matcher::matches) + .reduce((first, second) -> second) + .map(matcher -> matcher.group(0)); } } diff --git a/instrumentation/resources/library/src/main/java/io/opentelemetry/instrumentation/resources/ContainerResource.java b/instrumentation/resources/library/src/main/java/io/opentelemetry/instrumentation/resources/ContainerResource.java index 9e745475cd20..db46fbb138cb 100644 --- a/instrumentation/resources/library/src/main/java/io/opentelemetry/instrumentation/resources/ContainerResource.java +++ b/instrumentation/resources/library/src/main/java/io/opentelemetry/instrumentation/resources/ContainerResource.java @@ -5,15 +5,16 @@ package io.opentelemetry.instrumentation.resources; -import static io.opentelemetry.semconv.resource.attributes.ResourceAttributes.CONTAINER_ID; - import com.google.errorprone.annotations.MustBeClosed; +import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.common.Attributes; import io.opentelemetry.sdk.resources.Resource; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; +import java.util.List; import java.util.Optional; +import java.util.stream.Collectors; import java.util.stream.Stream; /** @@ -22,6 +23,9 @@ */ public final class ContainerResource { + // copied from ContainerIncubatingAttributes + private static final AttributeKey CONTAINER_ID = AttributeKey.stringKey("container.id"); + static final Filesystem FILESYSTEM_INSTANCE = new Filesystem(); private static final Resource INSTANCE = buildSingleton(); @@ -75,5 +79,11 @@ boolean isReadable(Path path) { Stream lines(Path path) throws IOException { return Files.lines(path); } + + List lineList(Path path) throws IOException { + try (Stream lines = lines(path)) { + return lines.collect(Collectors.toList()); + } + } } } diff --git a/instrumentation/resources/library/src/main/java/io/opentelemetry/instrumentation/resources/HostIdResourceProvider.java b/instrumentation/resources/library/src/main/java/io/opentelemetry/instrumentation/resources/HostIdResourceProvider.java new file mode 100644 index 000000000000..04766d899835 --- /dev/null +++ b/instrumentation/resources/library/src/main/java/io/opentelemetry/instrumentation/resources/HostIdResourceProvider.java @@ -0,0 +1,185 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.resources; + +import static java.util.logging.Level.FINE; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import io.opentelemetry.sdk.autoconfigure.spi.ResourceProvider; +import io.opentelemetry.sdk.autoconfigure.spi.internal.ConditionalResourceProvider; +import io.opentelemetry.sdk.resources.Resource; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Locale; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.logging.Logger; + +/** + * {@link ResourceProvider} for automatically configuring host.id according to the + * semantic conventions + */ +public final class HostIdResourceProvider implements ConditionalResourceProvider { + + private static final Logger logger = Logger.getLogger(HostIdResourceProvider.class.getName()); + + // copied from HostIncubatingAttributes + private static final AttributeKey HOST_ID = AttributeKey.stringKey("host.id"); + + public static final String REGISTRY_QUERY = + "reg query HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Cryptography /v MachineGuid"; + + private final Supplier getOsType; + + private final Function> machineIdReader; + + private final Supplier> queryWindowsRegistry; + + public HostIdResourceProvider() { + this( + HostIdResourceProvider::getOsTypeSystemProperty, + HostIdResourceProvider::readMachineIdFile, + HostIdResourceProvider::queryWindowsRegistry); + } + + // Visible for testing + + HostIdResourceProvider( + Supplier getOsType, + Function> machineIdReader, + Supplier> queryWindowsRegistry) { + this.getOsType = getOsType; + this.machineIdReader = machineIdReader; + this.queryWindowsRegistry = queryWindowsRegistry; + } + + @Override + public Resource createResource(ConfigProperties config) { + if (runningWindows()) { + return readWindowsGuid(); + } + if (runningLinux()) { + return readLinuxMachineId(); + } + logger.log(FINE, "Unsupported OS type: {0}", getOsType.get()); + return Resource.empty(); + } + + private boolean runningLinux() { + return getOsType.get().toLowerCase(Locale.ROOT).equals("linux"); + } + + private boolean runningWindows() { + return getOsType.get().startsWith("Windows"); + } + + // see + // https://github.com/apache/commons-lang/blob/master/src/main/java/org/apache/commons/lang3/SystemUtils.java + // for values + private static String getOsTypeSystemProperty() { + return System.getProperty("os.name", ""); + } + + private Resource readLinuxMachineId() { + Path path = FileSystems.getDefault().getPath("/etc/machine-id"); + List lines = machineIdReader.apply(path); + if (lines.isEmpty()) { + return Resource.empty(); + } + return Resource.create(Attributes.of(HOST_ID, lines.get(0))); + } + + private static List readMachineIdFile(Path path) { + try { + List lines = Files.readAllLines(path); + if (lines.isEmpty()) { + logger.fine("Failed to read /etc/machine-id: empty file"); + } + return lines; + } catch (IOException e) { + logger.log(FINE, "Failed to read /etc/machine-id", e); + return Collections.emptyList(); + } + } + + private Resource readWindowsGuid() { + List lines = queryWindowsRegistry.get(); + + for (String line : lines) { + if (line.contains("MachineGuid")) { + String[] parts = line.trim().split("\\s+"); + if (parts.length == 3) { + return Resource.create(Attributes.of(HOST_ID, parts[2])); + } + } + } + logger.fine("Failed to read Windows registry: No MachineGuid found in output: " + lines); + return Resource.empty(); + } + + private static List queryWindowsRegistry() { + try { + ProcessBuilder processBuilder = new ProcessBuilder("cmd", "/c", REGISTRY_QUERY); + processBuilder.redirectErrorStream(true); + Process process = processBuilder.start(); + + List output = getProcessOutput(process); + int exitedValue = process.waitFor(); + if (exitedValue != 0) { + logger.fine( + "Failed to read Windows registry. Exit code: " + + exitedValue + + " Output: " + + String.join("\n", output)); + + return Collections.emptyList(); + } + + return output; + } catch (IOException | InterruptedException e) { + logger.log(FINE, "Failed to read Windows registry", e); + return Collections.emptyList(); + } + } + + public static List getProcessOutput(Process process) throws IOException { + List result = new ArrayList<>(); + + try (BufferedReader processOutputReader = + new BufferedReader( + new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8))) { + String readLine; + + while ((readLine = processOutputReader.readLine()) != null) { + result.add(readLine); + } + } + return result; + } + + @Override + public boolean shouldApply(ConfigProperties config, Resource existing) { + return !config.getMap("otel.resource.attributes").containsKey(HOST_ID.getKey()) + && existing.getAttribute(HOST_ID) == null; + } + + @Override + public int order() { + // Run after cloud provider resource providers + return Integer.MAX_VALUE - 1; + } +} diff --git a/instrumentation/resources/library/src/main/java/io/opentelemetry/instrumentation/resources/HostResource.java b/instrumentation/resources/library/src/main/java/io/opentelemetry/instrumentation/resources/HostResource.java index e322927cd940..82810aeb9178 100644 --- a/instrumentation/resources/library/src/main/java/io/opentelemetry/instrumentation/resources/HostResource.java +++ b/instrumentation/resources/library/src/main/java/io/opentelemetry/instrumentation/resources/HostResource.java @@ -5,16 +5,21 @@ package io.opentelemetry.instrumentation.resources; +import io.opentelemetry.api.common.AttributeKey; 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 io.opentelemetry.semconv.SchemaUrls; import java.net.InetAddress; import java.net.UnknownHostException; /** Factory for a {@link Resource} which provides information about the host info. */ public final class HostResource { + // copied from HostIncubatingAttributes + public static final AttributeKey HOST_ARCH = AttributeKey.stringKey("host.arch"); + public static final AttributeKey HOST_NAME = AttributeKey.stringKey("host.name"); + private static final Resource INSTANCE = buildResource(); /** Returns a {@link Resource} which provides information about host. */ @@ -26,7 +31,7 @@ public static Resource get() { static Resource buildResource() { AttributesBuilder attributes = Attributes.builder(); try { - attributes.put(ResourceAttributes.HOST_NAME, InetAddress.getLocalHost().getHostName()); + attributes.put(HOST_NAME, InetAddress.getLocalHost().getHostName()); } catch (UnknownHostException e) { // Ignore } @@ -37,10 +42,10 @@ static Resource buildResource() { // Ignore } if (hostArch != null) { - attributes.put(ResourceAttributes.HOST_ARCH, hostArch); + attributes.put(HOST_ARCH, hostArch); } - return Resource.create(attributes.build(), ResourceAttributes.SCHEMA_URL); + return Resource.create(attributes.build(), SchemaUrls.V1_24_0); } private HostResource() {} diff --git a/instrumentation/resources/library/src/main/java/io/opentelemetry/instrumentation/resources/JarServiceNameDetector.java b/instrumentation/resources/library/src/main/java/io/opentelemetry/instrumentation/resources/JarServiceNameDetector.java index d54ba022aeb7..7ffaeaa2d170 100644 --- a/instrumentation/resources/library/src/main/java/io/opentelemetry/instrumentation/resources/JarServiceNameDetector.java +++ b/instrumentation/resources/library/src/main/java/io/opentelemetry/instrumentation/resources/JarServiceNameDetector.java @@ -13,17 +13,12 @@ import io.opentelemetry.sdk.autoconfigure.spi.ResourceProvider; import io.opentelemetry.sdk.autoconfigure.spi.internal.ConditionalResourceProvider; import io.opentelemetry.sdk.resources.Resource; -import io.opentelemetry.semconv.resource.attributes.ResourceAttributes; -import java.nio.file.Files; -import java.nio.file.InvalidPathException; +import io.opentelemetry.semconv.ServiceAttributes; import java.nio.file.Path; -import java.nio.file.Paths; import java.util.Map; -import java.util.function.Function; -import java.util.function.Predicate; +import java.util.Optional; import java.util.function.Supplier; import java.util.logging.Logger; -import javax.annotation.Nullable; /** * A {@link ResourceProvider} that will attempt to detect the application name from the jar name. @@ -33,37 +28,34 @@ public final class JarServiceNameDetector implements ConditionalResourceProvider private static final Logger logger = Logger.getLogger(JarServiceNameDetector.class.getName()); - private final Supplier getProcessHandleArguments; - private final Function getSystemProperty; - private final Predicate fileExists; + private final Supplier> jarPathSupplier; @SuppressWarnings("unused") // SPI public JarServiceNameDetector() { - this(ProcessArguments::getProcessArguments, System::getProperty, Files::isRegularFile); + this(MainJarPathHolder::getJarPath); + } + + private JarServiceNameDetector(Supplier> jarPathSupplier) { + this.jarPathSupplier = jarPathSupplier; } // visible for tests - JarServiceNameDetector( - Supplier getProcessHandleArguments, - Function getSystemProperty, - Predicate fileExists) { - this.getProcessHandleArguments = getProcessHandleArguments; - this.getSystemProperty = getSystemProperty; - this.fileExists = fileExists; + JarServiceNameDetector(MainJarPathFinder jarPathFinder) { + this(() -> Optional.ofNullable(jarPathFinder.detectJarPath())); } @Override public Resource createResource(ConfigProperties config) { - Path jarPath = getJarPathFromProcessHandle(); - if (jarPath == null) { - jarPath = getJarPathFromSunCommandLine(); - } - if (jarPath == null) { - return Resource.empty(); - } - String serviceName = getServiceName(jarPath); - logger.log(FINE, "Auto-detected service name from the jar file name: {0}", serviceName); - return Resource.create(Attributes.of(ResourceAttributes.SERVICE_NAME, serviceName)); + return jarPathSupplier + .get() + .map( + jarPath -> { + String serviceName = getServiceName(jarPath); + logger.log( + FINE, "Auto-detected service name from the jar file name: {0}", serviceName); + return Resource.create(Attributes.of(ServiceAttributes.SERVICE_NAME, serviceName)); + }) + .orElseGet(Resource::empty); } @Override @@ -71,54 +63,8 @@ public boolean shouldApply(ConfigProperties config, Resource existing) { String serviceName = config.getString("otel.service.name"); Map resourceAttributes = config.getMap("otel.resource.attributes"); return serviceName == null - && !resourceAttributes.containsKey(ResourceAttributes.SERVICE_NAME.getKey()) - && "unknown_service:java".equals(existing.getAttribute(ResourceAttributes.SERVICE_NAME)); - } - - @Nullable - private Path getJarPathFromProcessHandle() { - String[] javaArgs = getProcessHandleArguments.get(); - for (int i = 0; i < javaArgs.length; ++i) { - if ("-jar".equals(javaArgs[i]) && (i < javaArgs.length - 1)) { - return Paths.get(javaArgs[i + 1]); - } - } - return null; - } - - @Nullable - private Path getJarPathFromSunCommandLine() { - // the jar file is the first argument in the command line string - String programArguments = getSystemProperty.apply("sun.java.command"); - if (programArguments == null) { - return null; - } - - // Take the path until the first space. If the path doesn't exist extend it up to the next - // space. Repeat until a path that exists is found or input runs out. - int next = 0; - while (true) { - int nextSpace = programArguments.indexOf(' ', next); - if (nextSpace == -1) { - return pathIfExists(programArguments); - } - Path path = pathIfExists(programArguments.substring(0, nextSpace)); - next = nextSpace + 1; - if (path != null) { - return path; - } - } - } - - @Nullable - private Path pathIfExists(String programArguments) { - Path candidate; - try { - candidate = Paths.get(programArguments); - } catch (InvalidPathException e) { - return null; - } - return fileExists.test(candidate) ? candidate : null; + && !resourceAttributes.containsKey(ServiceAttributes.SERVICE_NAME.getKey()) + && "unknown_service:java".equals(existing.getAttribute(ServiceAttributes.SERVICE_NAME)); } private static String getServiceName(Path jarPath) { diff --git a/instrumentation/resources/library/src/main/java/io/opentelemetry/instrumentation/resources/MainJarPathFinder.java b/instrumentation/resources/library/src/main/java/io/opentelemetry/instrumentation/resources/MainJarPathFinder.java new file mode 100644 index 000000000000..8f9b9287ac68 --- /dev/null +++ b/instrumentation/resources/library/src/main/java/io/opentelemetry/instrumentation/resources/MainJarPathFinder.java @@ -0,0 +1,89 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.resources; + +import java.nio.file.Files; +import java.nio.file.InvalidPathException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.function.Supplier; +import javax.annotation.Nullable; + +class MainJarPathFinder { + private final Supplier getProcessHandleArguments; + private final Function getSystemProperty; + private final Predicate fileExists; + + public MainJarPathFinder() { + this(ProcessArguments::getProcessArguments, System::getProperty, Files::isRegularFile); + } + + // visible for tests + MainJarPathFinder( + Supplier getProcessHandleArguments, + Function getSystemProperty, + Predicate fileExists) { + this.getProcessHandleArguments = getProcessHandleArguments; + this.getSystemProperty = getSystemProperty; + this.fileExists = fileExists; + } + + Path detectJarPath() { + Path jarPath = getJarPathFromProcessHandle(); + if (jarPath != null) { + return jarPath; + } + return getJarPathFromSunCommandLine(); + } + + @Nullable + private Path getJarPathFromProcessHandle() { + String[] javaArgs = getProcessHandleArguments.get(); + for (int i = 0; i < javaArgs.length; ++i) { + if ("-jar".equals(javaArgs[i]) && (i < javaArgs.length - 1)) { + return Paths.get(javaArgs[i + 1]); + } + } + return null; + } + + @Nullable + private Path getJarPathFromSunCommandLine() { + // the jar file is the first argument in the command line string + String programArguments = getSystemProperty.apply("sun.java.command"); + if (programArguments == null) { + return null; + } + + // Take the path until the first space. If the path doesn't exist extend it up to the next + // space. Repeat until a path that exists is found or input runs out. + int next = 0; + while (true) { + int nextSpace = programArguments.indexOf(' ', next); + if (nextSpace == -1) { + return pathIfExists(programArguments); + } + Path path = pathIfExists(programArguments.substring(0, nextSpace)); + next = nextSpace + 1; + if (path != null) { + return path; + } + } + } + + @Nullable + private Path pathIfExists(String programArguments) { + Path candidate; + try { + candidate = Paths.get(programArguments); + } catch (InvalidPathException e) { + return null; + } + return fileExists.test(candidate) ? candidate : null; + } +} diff --git a/instrumentation/resources/library/src/main/java/io/opentelemetry/instrumentation/resources/MainJarPathHolder.java b/instrumentation/resources/library/src/main/java/io/opentelemetry/instrumentation/resources/MainJarPathHolder.java new file mode 100644 index 000000000000..c6948d3eca39 --- /dev/null +++ b/instrumentation/resources/library/src/main/java/io/opentelemetry/instrumentation/resources/MainJarPathHolder.java @@ -0,0 +1,20 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.resources; + +import java.nio.file.Path; +import java.util.Optional; + +class MainJarPathHolder { + private static final Optional jarPath = + Optional.ofNullable(new MainJarPathFinder().detectJarPath()); + + static Optional getJarPath() { + return jarPath; + } + + private MainJarPathHolder() {} +} diff --git a/instrumentation/resources/library/src/main/java/io/opentelemetry/instrumentation/resources/ManifestResourceProvider.java b/instrumentation/resources/library/src/main/java/io/opentelemetry/instrumentation/resources/ManifestResourceProvider.java new file mode 100644 index 000000000000..c5cb94af8de6 --- /dev/null +++ b/instrumentation/resources/library/src/main/java/io/opentelemetry/instrumentation/resources/ManifestResourceProvider.java @@ -0,0 +1,86 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.resources; + +import static java.util.logging.Level.WARNING; + +import com.google.auto.service.AutoService; +import io.opentelemetry.sdk.autoconfigure.spi.ResourceProvider; +import io.opentelemetry.semconv.ServiceAttributes; +import java.io.IOException; +import java.nio.file.Path; +import java.util.Optional; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.jar.JarFile; +import java.util.jar.Manifest; +import java.util.logging.Logger; + +/** + * A {@link ResourceProvider} that will attempt to detect the service.name and + * service.version from META-INF/MANIFEST.MF. + */ +@AutoService(ResourceProvider.class) +public final class ManifestResourceProvider extends AttributeResourceProvider { + + private static final Logger logger = Logger.getLogger(ManifestResourceProvider.class.getName()); + + @SuppressWarnings("unused") // SPI + public ManifestResourceProvider() { + this(MainJarPathHolder::getJarPath, ManifestResourceProvider::readManifest); + } + + private ManifestResourceProvider( + Supplier> jarPathSupplier, Function> manifestReader) { + super( + new AttributeProvider() { + @Override + public Optional readData() { + return jarPathSupplier.get().flatMap(manifestReader); + } + + @Override + public void registerAttributes(Builder builder) { + builder + .add( + ServiceAttributes.SERVICE_NAME, + manifest -> { + String serviceName = + manifest.getMainAttributes().getValue("Implementation-Title"); + return Optional.ofNullable(serviceName); + }) + .add( + ServiceAttributes.SERVICE_VERSION, + manifest -> { + String serviceVersion = + manifest.getMainAttributes().getValue("Implementation-Version"); + return Optional.ofNullable(serviceVersion); + }); + } + }); + } + + // Visible for testing + ManifestResourceProvider( + MainJarPathFinder jarPathFinder, Function> manifestReader) { + this(() -> Optional.ofNullable(jarPathFinder.detectJarPath()), manifestReader); + } + + private static Optional readManifest(Path jarPath) { + try (JarFile jarFile = new JarFile(jarPath.toFile(), false)) { + return Optional.of(jarFile.getManifest()); + } catch (IOException exception) { + logger.log(WARNING, "Error reading manifest", exception); + return Optional.empty(); + } + } + + @Override + public int order() { + // make it run later than SpringBootServiceNameDetector + return 300; + } +} diff --git a/instrumentation/resources/library/src/main/java/io/opentelemetry/instrumentation/resources/OsResource.java b/instrumentation/resources/library/src/main/java/io/opentelemetry/instrumentation/resources/OsResource.java index 3ae54f30f396..3de866869cfa 100644 --- a/instrumentation/resources/library/src/main/java/io/opentelemetry/instrumentation/resources/OsResource.java +++ b/instrumentation/resources/library/src/main/java/io/opentelemetry/instrumentation/resources/OsResource.java @@ -5,16 +5,22 @@ package io.opentelemetry.instrumentation.resources; +import io.opentelemetry.api.common.AttributeKey; 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 io.opentelemetry.semconv.SchemaUrls; import java.util.Locale; import javax.annotation.Nullable; /** Factory of a {@link Resource} which provides information about the current operating system. */ public final class OsResource { + // copied from OsIncubatingAttributes + private static final AttributeKey OS_DESCRIPTION = + AttributeKey.stringKey("os.description"); + private static final AttributeKey OS_TYPE = AttributeKey.stringKey("os.type"); + private static final Resource INSTANCE = buildResource(); /** @@ -44,7 +50,7 @@ static Resource buildResource() { String osName = getOs(os); if (osName != null) { - attributes.put(ResourceAttributes.OS_TYPE, osName); + attributes.put(OS_TYPE, osName); } String version = null; @@ -54,39 +60,56 @@ static Resource buildResource() { // Ignore } String osDescription = version != null ? os + ' ' + version : os; - attributes.put(ResourceAttributes.OS_DESCRIPTION, osDescription); + attributes.put(OS_DESCRIPTION, osDescription); - return Resource.create(attributes.build(), ResourceAttributes.SCHEMA_URL); + return Resource.create(attributes.build(), SchemaUrls.V1_24_0); } @Nullable private static String getOs(String os) { os = os.toLowerCase(Locale.ROOT); if (os.startsWith("windows")) { - return ResourceAttributes.OsTypeValues.WINDOWS; + return OsTypeValues.WINDOWS; } else if (os.startsWith("linux")) { - return ResourceAttributes.OsTypeValues.LINUX; + return OsTypeValues.LINUX; } else if (os.startsWith("mac")) { - return ResourceAttributes.OsTypeValues.DARWIN; + return OsTypeValues.DARWIN; } else if (os.startsWith("freebsd")) { - return ResourceAttributes.OsTypeValues.FREEBSD; + return OsTypeValues.FREEBSD; } else if (os.startsWith("netbsd")) { - return ResourceAttributes.OsTypeValues.NETBSD; + return OsTypeValues.NETBSD; } else if (os.startsWith("openbsd")) { - return ResourceAttributes.OsTypeValues.OPENBSD; + return OsTypeValues.OPENBSD; } else if (os.startsWith("dragonflybsd")) { - return ResourceAttributes.OsTypeValues.DRAGONFLYBSD; + return OsTypeValues.DRAGONFLYBSD; } else if (os.startsWith("hp-ux")) { - return ResourceAttributes.OsTypeValues.HPUX; + return OsTypeValues.HPUX; } else if (os.startsWith("aix")) { - return ResourceAttributes.OsTypeValues.AIX; + return OsTypeValues.AIX; } else if (os.startsWith("solaris")) { - return ResourceAttributes.OsTypeValues.SOLARIS; + return OsTypeValues.SOLARIS; } else if (os.startsWith("z/os")) { - return ResourceAttributes.OsTypeValues.Z_OS; + return OsTypeValues.Z_OS; } return null; } private OsResource() {} + + // copied from OsIncubatingAttributes + private static final class OsTypeValues { + static final String WINDOWS = "windows"; + static final String LINUX = "linux"; + static final String DARWIN = "darwin"; + static final String FREEBSD = "freebsd"; + static final String NETBSD = "netbsd"; + static final String OPENBSD = "openbsd"; + static final String DRAGONFLYBSD = "dragonflybsd"; + static final String HPUX = "hpux"; + static final String AIX = "aix"; + static final String SOLARIS = "solaris"; + static final String Z_OS = "z_os"; + + private OsTypeValues() {} + } } diff --git a/instrumentation/resources/library/src/main/java/io/opentelemetry/instrumentation/resources/ProcessResource.java b/instrumentation/resources/library/src/main/java/io/opentelemetry/instrumentation/resources/ProcessResource.java index d208f1024a82..fe9b398abe79 100644 --- a/instrumentation/resources/library/src/main/java/io/opentelemetry/instrumentation/resources/ProcessResource.java +++ b/instrumentation/resources/library/src/main/java/io/opentelemetry/instrumentation/resources/ProcessResource.java @@ -5,10 +5,11 @@ package io.opentelemetry.instrumentation.resources; +import io.opentelemetry.api.common.AttributeKey; 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 io.opentelemetry.semconv.SchemaUrls; import java.io.File; import java.lang.management.ManagementFactory; import java.lang.management.RuntimeMXBean; @@ -21,6 +22,15 @@ /** Factory of a {@link Resource} which provides information about the current running process. */ public final class ProcessResource { + // copied from ProcessIncubatingAttributes + private static final AttributeKey> PROCESS_COMMAND_ARGS = + AttributeKey.stringArrayKey("process.command_args"); + private static final AttributeKey PROCESS_COMMAND_LINE = + AttributeKey.stringKey("process.command_line"); + private static final AttributeKey PROCESS_EXECUTABLE_PATH = + AttributeKey.stringKey("process.executable.path"); + private static final AttributeKey PROCESS_PID = AttributeKey.longKey("process.pid"); + // Note: This pattern doesn't support file paths with spaces in them. // Important: This is statically used in buildResource, so must be declared/initialized first. private static final Pattern JAR_FILE_PATTERN = @@ -55,7 +65,7 @@ private static Resource doBuildResource() { long pid = ProcessPid.getPid(); if (pid >= 0) { - attributes.put(ResourceAttributes.PROCESS_PID, pid); + attributes.put(PROCESS_PID, pid); } String javaHome = null; @@ -77,7 +87,7 @@ private static Resource doBuildResource() { executablePath.append(".exe"); } - attributes.put(ResourceAttributes.PROCESS_EXECUTABLE_PATH, executablePath.toString()); + attributes.put(PROCESS_EXECUTABLE_PATH, executablePath.toString()); String[] args = ProcessArguments.getProcessArguments(); // This will only work with Java 9+ but provides everything except the executablePath. @@ -85,7 +95,7 @@ private static Resource doBuildResource() { List commandArgs = new ArrayList<>(args.length + 1); commandArgs.add(executablePath.toString()); commandArgs.addAll(Arrays.asList(args)); - attributes.put(ResourceAttributes.PROCESS_COMMAND_ARGS, commandArgs); + attributes.put(PROCESS_COMMAND_ARGS, commandArgs); } else { // Java 8 StringBuilder commandLine = new StringBuilder(executablePath); for (String arg : runtime.getInputArguments()) { @@ -101,11 +111,11 @@ private static Resource doBuildResource() { } commandLine.append(' ').append(javaCommand); } - attributes.put(ResourceAttributes.PROCESS_COMMAND_LINE, commandLine.toString()); + attributes.put(PROCESS_COMMAND_LINE, commandLine.toString()); } } - return Resource.create(attributes.build(), ResourceAttributes.SCHEMA_URL); + return Resource.create(attributes.build(), SchemaUrls.V1_24_0); } private ProcessResource() {} diff --git a/instrumentation/resources/library/src/main/java/io/opentelemetry/instrumentation/resources/ProcessRuntimeResource.java b/instrumentation/resources/library/src/main/java/io/opentelemetry/instrumentation/resources/ProcessRuntimeResource.java index 1c1222c7ede7..d296f4f33dab 100644 --- a/instrumentation/resources/library/src/main/java/io/opentelemetry/instrumentation/resources/ProcessRuntimeResource.java +++ b/instrumentation/resources/library/src/main/java/io/opentelemetry/instrumentation/resources/ProcessRuntimeResource.java @@ -5,17 +5,22 @@ package io.opentelemetry.instrumentation.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.AttributeKey; import io.opentelemetry.api.common.Attributes; import io.opentelemetry.sdk.resources.Resource; -import io.opentelemetry.semconv.resource.attributes.ResourceAttributes; +import io.opentelemetry.semconv.SchemaUrls; /** Factory of a {@link Resource} which provides information about the Java runtime. */ public final class ProcessRuntimeResource { + // copied from ProcessIncubatingAttributes + private static final AttributeKey PROCESS_RUNTIME_DESCRIPTION = + AttributeKey.stringKey("process.runtime.description"); + private static final AttributeKey PROCESS_RUNTIME_NAME = + AttributeKey.stringKey("process.runtime.name"); + private static final AttributeKey PROCESS_RUNTIME_VERSION = + AttributeKey.stringKey("process.runtime.version"); + private static final Resource INSTANCE = buildResource(); /** Returns a factory for a {@link Resource} which provides information about the Java runtime. */ @@ -43,7 +48,7 @@ static Resource buildResource() { version, PROCESS_RUNTIME_DESCRIPTION, description), - ResourceAttributes.SCHEMA_URL); + SchemaUrls.V1_24_0); } catch (SecurityException ignored) { return Resource.empty(); } diff --git a/instrumentation/resources/library/src/test/java/io/opentelemetry/instrumentation/resources/CgroupV2ContainerIdExtractorTest.java b/instrumentation/resources/library/src/test/java/io/opentelemetry/instrumentation/resources/CgroupV2ContainerIdExtractorTest.java index 141ce0cf1683..04e9aa4d1b1f 100644 --- a/instrumentation/resources/library/src/test/java/io/opentelemetry/instrumentation/resources/CgroupV2ContainerIdExtractorTest.java +++ b/instrumentation/resources/library/src/test/java/io/opentelemetry/instrumentation/resources/CgroupV2ContainerIdExtractorTest.java @@ -13,8 +13,9 @@ import java.io.BufferedReader; import java.io.InputStream; import java.io.InputStreamReader; +import java.util.List; import java.util.Optional; -import java.util.stream.Stream; +import java.util.stream.Collectors; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; @@ -33,39 +34,68 @@ void fileNotReadable() { assertThat(result).isSameAs(Optional.empty()); } - @Test - void extractSuccess_docker() throws Exception { + private void verifyContainerId(String rawFileContent, String containerId) throws Exception { when(filesystem.isReadable(V2_CGROUP_PATH)).thenReturn(true); - Stream fileContent = getTestDockerFileContent(); - when(filesystem.lines(V2_CGROUP_PATH)).thenReturn(fileContent); + when(filesystem.lineList(V2_CGROUP_PATH)).thenReturn(fileToListOfLines(rawFileContent)); CgroupV2ContainerIdExtractor extractor = new CgroupV2ContainerIdExtractor(filesystem); Optional result = extractor.extractContainerId(); - assertThat(result.orElse("fail")) - .isEqualTo("be522444b60caf2d3934b8b24b916a8a314f4b68d4595aa419874657e8d103f2"); + assertThat(result.orElse("fail")).isEqualTo(containerId); + } + + @Test + void extractSuccess_docker() throws Exception { + verifyContainerId( + "docker_proc_self_mountinfo", + "be522444b60caf2d3934b8b24b916a8a314f4b68d4595aa419874657e8d103f2"); + } + + @Test + void extractSuccess_docker1() throws Exception { + verifyContainerId( + "docker_proc_self_mountinfo1", + "188329f95b930c32eeeffd34658ed2538960947e166743fa3743f5ce3d739b40"); + } + + @Test + void extractSuccess_containerd() throws Exception { + verifyContainerId( + "containerd_proc_self_mountinfo", + "f2a44bc8e090f93a2b4d7f510bdaff0615ad52906e3287ee956dcf5aa5012a91"); } @Test void extractSuccess_podman() throws Exception { - when(filesystem.isReadable(V2_CGROUP_PATH)).thenReturn(true); - Stream fileContent = getTestPodmanFileContent(); - when(filesystem.lines(V2_CGROUP_PATH)).thenReturn(fileContent); - CgroupV2ContainerIdExtractor extractor = new CgroupV2ContainerIdExtractor(filesystem); - Optional result = extractor.extractContainerId(); - assertThat(result.orElse("fail")) - .isEqualTo("2a33efc76e519c137fe6093179653788bed6162d4a15e5131c8e835c968afbe6"); + verifyContainerId( + "podman_proc_self_mountinfo", + "2a33efc76e519c137fe6093179653788bed6162d4a15e5131c8e835c968afbe6"); } - private static Stream getTestDockerFileContent() throws Exception { - return fileToStreamOfLines("docker_proc_self_mountinfo"); + @Test + void extractSuccess_crio() throws Exception { + verifyContainerId( + "crio_proc_self_mountinfo", + "a8f62e52ed7c2cd85242dcf0eb1d727b643540ceca7f328ad7d2f31aedf07731"); } - private static Stream getTestPodmanFileContent() throws Exception { - return fileToStreamOfLines("podman_proc_self_mountinfo"); + @Test + void extractSuccess_crio1() throws Exception { + verifyContainerId( + "crio_proc_self_mountinfo1", + "f23ec1d4b715c6531a17e9c549222fbbe1f7ffff697a29a2212b3b4cdc37f52e"); + } + + @Test + void extractSuccess_crio2() throws Exception { + verifyContainerId( + "crio_proc_self_mountinfo2", + "b4873629b312dc1d77472aba6fb177c6ce9a8f7c205ad7a03302726805007fe6"); } - private static Stream fileToStreamOfLines(String filename) { + private static List fileToListOfLines(String filename) { InputStream in = CgroupV2ContainerIdExtractorTest.class.getClassLoader().getResourceAsStream(filename); - return new BufferedReader(new InputStreamReader(in, UTF_8)).lines(); + return new BufferedReader(new InputStreamReader(in, UTF_8)) + .lines() + .collect(Collectors.toList()); } } diff --git a/instrumentation/resources/library/src/test/java/io/opentelemetry/instrumentation/resources/ContainerResourceTest.java b/instrumentation/resources/library/src/test/java/io/opentelemetry/instrumentation/resources/ContainerResourceTest.java index 0cfc09576648..8e826a6f2b6c 100644 --- a/instrumentation/resources/library/src/test/java/io/opentelemetry/instrumentation/resources/ContainerResourceTest.java +++ b/instrumentation/resources/library/src/test/java/io/opentelemetry/instrumentation/resources/ContainerResourceTest.java @@ -5,7 +5,7 @@ package io.opentelemetry.instrumentation.resources; -import static io.opentelemetry.semconv.resource.attributes.ResourceAttributes.CONTAINER_ID; +import static io.opentelemetry.semconv.incubating.ContainerIncubatingAttributes.CONTAINER_ID; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; diff --git a/instrumentation/resources/library/src/test/java/io/opentelemetry/instrumentation/resources/HostIdResourceProviderTest.java b/instrumentation/resources/library/src/test/java/io/opentelemetry/instrumentation/resources/HostIdResourceProviderTest.java new file mode 100644 index 000000000000..2b88957b7b32 --- /dev/null +++ b/instrumentation/resources/library/src/test/java/io/opentelemetry/instrumentation/resources/HostIdResourceProviderTest.java @@ -0,0 +1,125 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.resources; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.sdk.autoconfigure.spi.internal.DefaultConfigProperties; +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.semconv.incubating.HostIncubatingAttributes; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.assertj.core.api.MapAssert; +import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestFactory; + +class HostIdResourceProviderTest { + + private static class LinuxTestCase { + private final String name; + private final String expectedValue; + private final Function> pathReader; + + private LinuxTestCase( + String name, String expectedValue, Function> pathReader) { + this.name = name; + this.expectedValue = expectedValue; + this.pathReader = pathReader; + } + } + + private static class WindowsTestCase { + private final String name; + private final String expectedValue; + private final Supplier> queryWindowsRegistry; + + private WindowsTestCase( + String name, String expectedValue, Supplier> queryWindowsRegistry) { + this.name = name; + this.expectedValue = expectedValue; + this.queryWindowsRegistry = queryWindowsRegistry; + } + } + + @TestFactory + Collection createResourceLinux() { + return Stream.of( + new LinuxTestCase("default", "test", path -> Collections.singletonList("test")), + new LinuxTestCase("empty file or error reading", null, path -> Collections.emptyList())) + .map( + testCase -> + DynamicTest.dynamicTest( + testCase.name, + () -> { + HostIdResourceProvider provider = + new HostIdResourceProvider(() -> "linux", testCase.pathReader, null); + + assertHostId(testCase.expectedValue, provider); + })) + .collect(Collectors.toList()); + } + + @TestFactory + Collection createResourceWindows() { + return Stream.of( + new WindowsTestCase( + "default", + "test", + () -> + Arrays.asList( + "HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Cryptography", + " MachineGuid REG_SZ test")), + new WindowsTestCase("short output", null, Collections::emptyList)) + .map( + testCase -> + DynamicTest.dynamicTest( + testCase.name, + () -> { + HostIdResourceProvider provider = + new HostIdResourceProvider( + () -> "Windows 95", null, testCase.queryWindowsRegistry); + + assertHostId(testCase.expectedValue, provider); + })) + .collect(Collectors.toList()); + } + + private static void assertHostId(String expectedValue, HostIdResourceProvider provider) { + MapAssert, Object> that = + assertThat(provider.createResource(null).getAttributes().asMap()); + + if (expectedValue == null) { + that.isEmpty(); + } else { + that.containsEntry(HostIncubatingAttributes.HOST_ID, expectedValue); + } + } + + @Test + void shouldApply() { + HostIdResourceProvider provider = new HostIdResourceProvider(); + assertThat( + provider.shouldApply( + DefaultConfigProperties.createFromMap(Collections.emptyMap()), + Resource.getDefault())) + .isTrue(); + assertThat( + provider.shouldApply( + DefaultConfigProperties.createFromMap( + Collections.singletonMap("otel.resource.attributes", "host.id=foo")), + null)) + .isFalse(); + } +} diff --git a/instrumentation/resources/library/src/test/java/io/opentelemetry/instrumentation/resources/HostResourceTest.java b/instrumentation/resources/library/src/test/java/io/opentelemetry/instrumentation/resources/HostResourceTest.java index d85c10d62708..4c2e0ca45514 100644 --- a/instrumentation/resources/library/src/test/java/io/opentelemetry/instrumentation/resources/HostResourceTest.java +++ b/instrumentation/resources/library/src/test/java/io/opentelemetry/instrumentation/resources/HostResourceTest.java @@ -9,7 +9,8 @@ import io.opentelemetry.api.common.Attributes; import io.opentelemetry.sdk.resources.Resource; -import io.opentelemetry.semconv.resource.attributes.ResourceAttributes; +import io.opentelemetry.semconv.SchemaUrls; +import io.opentelemetry.semconv.incubating.HostIncubatingAttributes; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; @@ -25,9 +26,9 @@ void shouldCreateRuntimeAttributes() { Attributes attributes = resource.getAttributes(); // then - assertThat(resource.getSchemaUrl()).isEqualTo(ResourceAttributes.SCHEMA_URL); - assertThat(attributes.get(ResourceAttributes.HOST_NAME)).isNotBlank(); - assertThat(attributes.get(ResourceAttributes.HOST_ARCH)).isNotBlank(); + assertThat(resource.getSchemaUrl()).isEqualTo(SchemaUrls.V1_24_0); + assertThat(attributes.get(HostIncubatingAttributes.HOST_NAME)).isNotBlank(); + assertThat(attributes.get(HostIncubatingAttributes.HOST_ARCH)).isNotBlank(); } @Nested @@ -40,7 +41,7 @@ static class SecurityManagerEnabled { @Test void empty() { Attributes attributes = HostResource.buildResource().getAttributes(); - assertThat(attributes.asMap()).containsOnlyKeys(ResourceAttributes.HOST_NAME); + assertThat(attributes.asMap()).containsOnlyKeys(HostIncubatingAttributes.HOST_NAME); } } } diff --git a/instrumentation/resources/library/src/test/java/io/opentelemetry/instrumentation/resources/JarServiceNameDetectorTest.java b/instrumentation/resources/library/src/test/java/io/opentelemetry/instrumentation/resources/JarServiceNameDetectorTest.java index f1842dd4d6f9..bcb0ff2c631d 100644 --- a/instrumentation/resources/library/src/test/java/io/opentelemetry/instrumentation/resources/JarServiceNameDetectorTest.java +++ b/instrumentation/resources/library/src/test/java/io/opentelemetry/instrumentation/resources/JarServiceNameDetectorTest.java @@ -10,7 +10,7 @@ import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; import io.opentelemetry.sdk.resources.Resource; -import io.opentelemetry.semconv.resource.attributes.ResourceAttributes; +import io.opentelemetry.semconv.ServiceAttributes; import java.nio.file.Path; import java.nio.file.Paths; import java.util.function.Function; @@ -27,26 +27,34 @@ import org.mockito.junit.jupiter.MockitoExtension; @ExtendWith(MockitoExtension.class) +// todo split JarFileDetectorTest and JarServiceNameDetectorTest class JarServiceNameDetectorTest { @Mock ConfigProperties config; @Test void createResource_empty() { - JarServiceNameDetector serviceNameProvider = - new JarServiceNameDetector( - () -> new String[0], prop -> null, JarServiceNameDetectorTest::failPath); + String[] processArgs = new String[0]; + Function getProperty = prop -> null; + Predicate fileExists = JarServiceNameDetectorTest::failPath; + JarServiceNameDetector serviceNameProvider = getDetector(processArgs, getProperty, fileExists); Resource resource = serviceNameProvider.createResource(config); assertThat(resource.getAttributes()).isEmpty(); } + private static JarServiceNameDetector getDetector( + String[] processArgs, Function getProperty, Predicate fileExists) { + return new JarServiceNameDetector( + new MainJarPathFinder(() -> processArgs, getProperty, fileExists)); + } + @Test void createResource_noJarFileInArgs() { String[] args = new String[] {"-Dtest=42", "-Xmx666m", "-jar"}; JarServiceNameDetector serviceNameProvider = - new JarServiceNameDetector(() -> args, prop -> null, JarServiceNameDetectorTest::failPath); + getDetector(args, prop -> null, JarServiceNameDetectorTest::failPath); Resource resource = serviceNameProvider.createResource(config); @@ -55,30 +63,31 @@ void createResource_noJarFileInArgs() { @Test void createResource_processHandleJar() { - String path = Paths.get("path", "to", "app", "my-service.jar").toString(); - String[] args = new String[] {"-Dtest=42", "-Xmx666m", "-jar", path, "abc", "def"}; JarServiceNameDetector serviceNameProvider = - new JarServiceNameDetector(() -> args, prop -> null, JarServiceNameDetectorTest::failPath); + getDetector(getArgs("my-service.jar"), prop -> null, JarServiceNameDetectorTest::failPath); Resource resource = serviceNameProvider.createResource(config); assertThat(resource.getAttributes()) .hasSize(1) - .containsEntry(ResourceAttributes.SERVICE_NAME, "my-service"); + .containsEntry(ServiceAttributes.SERVICE_NAME, "my-service"); } @Test void createResource_processHandleJarWithoutExtension() { - String path = Paths.get("path", "to", "app", "my-service.jar").toString(); - String[] args = new String[] {"-Dtest=42", "-Xmx666m", "-jar", path}; JarServiceNameDetector serviceNameProvider = - new JarServiceNameDetector(() -> args, prop -> null, JarServiceNameDetectorTest::failPath); + getDetector(getArgs("my-service"), prop -> null, JarServiceNameDetectorTest::failPath); Resource resource = serviceNameProvider.createResource(config); assertThat(resource.getAttributes()) .hasSize(1) - .containsEntry(ResourceAttributes.SERVICE_NAME, "my-service"); + .containsEntry(ServiceAttributes.SERVICE_NAME, "my-service"); + } + + static String[] getArgs(String jarName) { + String path = Paths.get("path", "to", "app", jarName).toString(); + return new String[] {"-Dtest=42", "-Xmx666m", "-jar", path, "abc", "def"}; } @ParameterizedTest @@ -89,13 +98,13 @@ void createResource_sunCommandLine(String commandLine, Path jarPath) { Predicate fileExists = jarPath::equals; JarServiceNameDetector serviceNameProvider = - new JarServiceNameDetector(() -> new String[0], getProperty, fileExists); + getDetector(new String[0], getProperty, fileExists); Resource resource = serviceNameProvider.createResource(config); assertThat(resource.getAttributes()) .hasSize(1) - .containsEntry(ResourceAttributes.SERVICE_NAME, "my-service"); + .containsEntry(ServiceAttributes.SERVICE_NAME, "my-service"); } // regression test for @@ -107,7 +116,7 @@ void createResource_sunCommandLineProblematicArgs() { Predicate fileExists = path -> false; JarServiceNameDetector serviceNameProvider = - new JarServiceNameDetector(() -> new String[0], getProperty, fileExists); + getDetector(new String[0], getProperty, fileExists); Resource resource = serviceNameProvider.createResource(config); @@ -128,7 +137,7 @@ public Stream provideArguments(ExtensionContext context) { } } - private static boolean failPath(Path file) { + static boolean failPath(Path file) { throw new AssertionError("Unexpected call to Files.isRegularFile()"); } } diff --git a/instrumentation/resources/library/src/test/java/io/opentelemetry/instrumentation/resources/ManifestResourceProviderTest.java b/instrumentation/resources/library/src/test/java/io/opentelemetry/instrumentation/resources/ManifestResourceProviderTest.java new file mode 100644 index 000000000000..c617f58a6dbc --- /dev/null +++ b/instrumentation/resources/library/src/test/java/io/opentelemetry/instrumentation/resources/ManifestResourceProviderTest.java @@ -0,0 +1,106 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.resources; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import io.opentelemetry.sdk.autoconfigure.spi.internal.DefaultConfigProperties; +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.semconv.ServiceAttributes; +import java.io.InputStream; +import java.util.Collection; +import java.util.Collections; +import java.util.Optional; +import java.util.jar.Manifest; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.TestFactory; + +class ManifestResourceProviderTest { + + private static class TestCase { + private final String name; + private final String expectedName; + private final String expectedVersion; + private final InputStream input; + private final Resource existing; + + public TestCase( + String name, + String expectedName, + String expectedVersion, + InputStream input, + Resource existing) { + this.name = name; + this.expectedName = expectedName; + this.expectedVersion = expectedVersion; + this.input = input; + this.existing = existing; + } + } + + @TestFactory + Collection createResource() { + ConfigProperties config = DefaultConfigProperties.createFromMap(Collections.emptyMap()); + + return Stream.of( + new TestCase( + "name ok", + "demo", + "0.0.1-SNAPSHOT", + openClasspathResource("MANIFEST.MF"), + Resource.getDefault()), + new TestCase("name - no resource", null, null, null, Resource.getDefault()), + new TestCase( + "name - empty resource", + null, + null, + openClasspathResource("empty-MANIFEST.MF"), + Resource.getDefault()), + new TestCase( + "name already detected", + null, + "0.0.1-SNAPSHOT", + openClasspathResource("MANIFEST.MF"), + Resource.create(Attributes.of(ServiceAttributes.SERVICE_NAME, "old")))) + .map( + t -> + DynamicTest.dynamicTest( + t.name, + () -> { + ManifestResourceProvider provider = + new ManifestResourceProvider( + new MainJarPathFinder( + () -> JarServiceNameDetectorTest.getArgs("app.jar"), + prop -> null, + JarServiceNameDetectorTest::failPath), + p -> { + try { + Manifest manifest = new Manifest(); + manifest.read(t.input); + return Optional.of(manifest); + } catch (Exception e) { + return Optional.empty(); + } + }); + provider.shouldApply(config, t.existing); + + Resource resource = provider.createResource(config); + assertThat(resource.getAttribute(ServiceAttributes.SERVICE_NAME)) + .isEqualTo(t.expectedName); + assertThat(resource.getAttribute(ServiceAttributes.SERVICE_VERSION)) + .isEqualTo(t.expectedVersion); + })) + .collect(Collectors.toList()); + } + + private static InputStream openClasspathResource(String resource) { + return ManifestResourceProviderTest.class.getClassLoader().getResourceAsStream(resource); + } +} diff --git a/instrumentation/resources/library/src/test/java/io/opentelemetry/instrumentation/resources/OsResourceTest.java b/instrumentation/resources/library/src/test/java/io/opentelemetry/instrumentation/resources/OsResourceTest.java index 988514c47105..682de97565a1 100644 --- a/instrumentation/resources/library/src/test/java/io/opentelemetry/instrumentation/resources/OsResourceTest.java +++ b/instrumentation/resources/library/src/test/java/io/opentelemetry/instrumentation/resources/OsResourceTest.java @@ -8,7 +8,8 @@ import static org.assertj.core.api.Assertions.assertThat; import io.opentelemetry.sdk.resources.Resource; -import io.opentelemetry.semconv.resource.attributes.ResourceAttributes; +import io.opentelemetry.semconv.SchemaUrls; +import io.opentelemetry.semconv.incubating.OsIncubatingAttributes; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; @@ -23,119 +24,119 @@ class OsResourceTest { @SetSystemProperty(key = "os.name", value = "Linux 4.11") void linux() { Resource resource = OsResource.buildResource(); - assertThat(resource.getSchemaUrl()).isEqualTo(ResourceAttributes.SCHEMA_URL); - assertThat(resource.getAttribute(ResourceAttributes.OS_TYPE)) - .isEqualTo(ResourceAttributes.OsTypeValues.LINUX); - assertThat(resource.getAttribute(ResourceAttributes.OS_DESCRIPTION)).isNotEmpty(); + assertThat(resource.getSchemaUrl()).isEqualTo(SchemaUrls.V1_24_0); + assertThat(resource.getAttribute(OsIncubatingAttributes.OS_TYPE)) + .isEqualTo(OsIncubatingAttributes.OsTypeValues.LINUX); + assertThat(resource.getAttribute(OsIncubatingAttributes.OS_DESCRIPTION)).isNotEmpty(); } @Test @SetSystemProperty(key = "os.name", value = "MacOS X 11") void macos() { Resource resource = OsResource.buildResource(); - assertThat(resource.getSchemaUrl()).isEqualTo(ResourceAttributes.SCHEMA_URL); - assertThat(resource.getAttribute(ResourceAttributes.OS_TYPE)) - .isEqualTo(ResourceAttributes.OsTypeValues.DARWIN); - assertThat(resource.getAttribute(ResourceAttributes.OS_DESCRIPTION)).isNotEmpty(); + assertThat(resource.getSchemaUrl()).isEqualTo(SchemaUrls.V1_24_0); + assertThat(resource.getAttribute(OsIncubatingAttributes.OS_TYPE)) + .isEqualTo(OsIncubatingAttributes.OsTypeValues.DARWIN); + assertThat(resource.getAttribute(OsIncubatingAttributes.OS_DESCRIPTION)).isNotEmpty(); } @Test @SetSystemProperty(key = "os.name", value = "Windows 10") void windows() { Resource resource = OsResource.buildResource(); - assertThat(resource.getSchemaUrl()).isEqualTo(ResourceAttributes.SCHEMA_URL); - assertThat(resource.getAttribute(ResourceAttributes.OS_TYPE)) - .isEqualTo(ResourceAttributes.OsTypeValues.WINDOWS); - assertThat(resource.getAttribute(ResourceAttributes.OS_DESCRIPTION)).isNotEmpty(); + assertThat(resource.getSchemaUrl()).isEqualTo(SchemaUrls.V1_24_0); + assertThat(resource.getAttribute(OsIncubatingAttributes.OS_TYPE)) + .isEqualTo(OsIncubatingAttributes.OsTypeValues.WINDOWS); + assertThat(resource.getAttribute(OsIncubatingAttributes.OS_DESCRIPTION)).isNotEmpty(); } @Test @SetSystemProperty(key = "os.name", value = "FreeBSD 10") void freebsd() { Resource resource = OsResource.buildResource(); - assertThat(resource.getSchemaUrl()).isEqualTo(ResourceAttributes.SCHEMA_URL); - assertThat(resource.getAttribute(ResourceAttributes.OS_TYPE)) - .isEqualTo(ResourceAttributes.OsTypeValues.FREEBSD); - assertThat(resource.getAttribute(ResourceAttributes.OS_DESCRIPTION)).isNotEmpty(); + assertThat(resource.getSchemaUrl()).isEqualTo(SchemaUrls.V1_24_0); + assertThat(resource.getAttribute(OsIncubatingAttributes.OS_TYPE)) + .isEqualTo(OsIncubatingAttributes.OsTypeValues.FREEBSD); + assertThat(resource.getAttribute(OsIncubatingAttributes.OS_DESCRIPTION)).isNotEmpty(); } @Test @SetSystemProperty(key = "os.name", value = "NetBSD 10") void netbsd() { Resource resource = OsResource.buildResource(); - assertThat(resource.getSchemaUrl()).isEqualTo(ResourceAttributes.SCHEMA_URL); - assertThat(resource.getAttribute(ResourceAttributes.OS_TYPE)) - .isEqualTo(ResourceAttributes.OsTypeValues.NETBSD); - assertThat(resource.getAttribute(ResourceAttributes.OS_DESCRIPTION)).isNotEmpty(); + assertThat(resource.getSchemaUrl()).isEqualTo(SchemaUrls.V1_24_0); + assertThat(resource.getAttribute(OsIncubatingAttributes.OS_TYPE)) + .isEqualTo(OsIncubatingAttributes.OsTypeValues.NETBSD); + assertThat(resource.getAttribute(OsIncubatingAttributes.OS_DESCRIPTION)).isNotEmpty(); } @Test @SetSystemProperty(key = "os.name", value = "OpenBSD 10") void openbsd() { Resource resource = OsResource.buildResource(); - assertThat(resource.getSchemaUrl()).isEqualTo(ResourceAttributes.SCHEMA_URL); - assertThat(resource.getAttribute(ResourceAttributes.OS_TYPE)) - .isEqualTo(ResourceAttributes.OsTypeValues.OPENBSD); - assertThat(resource.getAttribute(ResourceAttributes.OS_DESCRIPTION)).isNotEmpty(); + assertThat(resource.getSchemaUrl()).isEqualTo(SchemaUrls.V1_24_0); + assertThat(resource.getAttribute(OsIncubatingAttributes.OS_TYPE)) + .isEqualTo(OsIncubatingAttributes.OsTypeValues.OPENBSD); + assertThat(resource.getAttribute(OsIncubatingAttributes.OS_DESCRIPTION)).isNotEmpty(); } @Test @SetSystemProperty(key = "os.name", value = "DragonFlyBSD 10") void dragonflybsd() { Resource resource = OsResource.buildResource(); - assertThat(resource.getSchemaUrl()).isEqualTo(ResourceAttributes.SCHEMA_URL); - assertThat(resource.getAttribute(ResourceAttributes.OS_TYPE)) - .isEqualTo(ResourceAttributes.OsTypeValues.DRAGONFLYBSD); - assertThat(resource.getAttribute(ResourceAttributes.OS_DESCRIPTION)).isNotEmpty(); + assertThat(resource.getSchemaUrl()).isEqualTo(SchemaUrls.V1_24_0); + assertThat(resource.getAttribute(OsIncubatingAttributes.OS_TYPE)) + .isEqualTo(OsIncubatingAttributes.OsTypeValues.DRAGONFLYBSD); + assertThat(resource.getAttribute(OsIncubatingAttributes.OS_DESCRIPTION)).isNotEmpty(); } @Test @SetSystemProperty(key = "os.name", value = "HP-UX 10") void hpux() { Resource resource = OsResource.buildResource(); - assertThat(resource.getSchemaUrl()).isEqualTo(ResourceAttributes.SCHEMA_URL); - assertThat(resource.getAttribute(ResourceAttributes.OS_TYPE)) - .isEqualTo(ResourceAttributes.OsTypeValues.HPUX); - assertThat(resource.getAttribute(ResourceAttributes.OS_DESCRIPTION)).isNotEmpty(); + assertThat(resource.getSchemaUrl()).isEqualTo(SchemaUrls.V1_24_0); + assertThat(resource.getAttribute(OsIncubatingAttributes.OS_TYPE)) + .isEqualTo(OsIncubatingAttributes.OsTypeValues.HPUX); + assertThat(resource.getAttribute(OsIncubatingAttributes.OS_DESCRIPTION)).isNotEmpty(); } @Test @SetSystemProperty(key = "os.name", value = "AIX 10") void aix() { Resource resource = OsResource.buildResource(); - assertThat(resource.getSchemaUrl()).isEqualTo(ResourceAttributes.SCHEMA_URL); - assertThat(resource.getAttribute(ResourceAttributes.OS_TYPE)) - .isEqualTo(ResourceAttributes.OsTypeValues.AIX); - assertThat(resource.getAttribute(ResourceAttributes.OS_DESCRIPTION)).isNotEmpty(); + assertThat(resource.getSchemaUrl()).isEqualTo(SchemaUrls.V1_24_0); + assertThat(resource.getAttribute(OsIncubatingAttributes.OS_TYPE)) + .isEqualTo(OsIncubatingAttributes.OsTypeValues.AIX); + assertThat(resource.getAttribute(OsIncubatingAttributes.OS_DESCRIPTION)).isNotEmpty(); } @Test @SetSystemProperty(key = "os.name", value = "Solaris 10") void solaris() { Resource resource = OsResource.buildResource(); - assertThat(resource.getSchemaUrl()).isEqualTo(ResourceAttributes.SCHEMA_URL); - assertThat(resource.getAttribute(ResourceAttributes.OS_TYPE)) - .isEqualTo(ResourceAttributes.OsTypeValues.SOLARIS); - assertThat(resource.getAttribute(ResourceAttributes.OS_DESCRIPTION)).isNotEmpty(); + assertThat(resource.getSchemaUrl()).isEqualTo(SchemaUrls.V1_24_0); + assertThat(resource.getAttribute(OsIncubatingAttributes.OS_TYPE)) + .isEqualTo(OsIncubatingAttributes.OsTypeValues.SOLARIS); + assertThat(resource.getAttribute(OsIncubatingAttributes.OS_DESCRIPTION)).isNotEmpty(); } @Test @SetSystemProperty(key = "os.name", value = "Z/OS 10") void zos() { Resource resource = OsResource.buildResource(); - assertThat(resource.getSchemaUrl()).isEqualTo(ResourceAttributes.SCHEMA_URL); - assertThat(resource.getAttribute(ResourceAttributes.OS_TYPE)) - .isEqualTo(ResourceAttributes.OsTypeValues.Z_OS); - assertThat(resource.getAttribute(ResourceAttributes.OS_DESCRIPTION)).isNotEmpty(); + assertThat(resource.getSchemaUrl()).isEqualTo(SchemaUrls.V1_24_0); + assertThat(resource.getAttribute(OsIncubatingAttributes.OS_TYPE)) + .isEqualTo(OsIncubatingAttributes.OsTypeValues.Z_OS); + assertThat(resource.getAttribute(OsIncubatingAttributes.OS_DESCRIPTION)).isNotEmpty(); } @Test @SetSystemProperty(key = "os.name", value = "RagOS 10") void unknown() { Resource resource = OsResource.buildResource(); - assertThat(resource.getSchemaUrl()).isEqualTo(ResourceAttributes.SCHEMA_URL); - assertThat(resource.getAttribute(ResourceAttributes.OS_TYPE)).isNull(); - assertThat(resource.getAttribute(ResourceAttributes.OS_DESCRIPTION)).isNotEmpty(); + assertThat(resource.getSchemaUrl()).isEqualTo(SchemaUrls.V1_24_0); + assertThat(resource.getAttribute(OsIncubatingAttributes.OS_TYPE)).isNull(); + assertThat(resource.getAttribute(OsIncubatingAttributes.OS_DESCRIPTION)).isNotEmpty(); } @Nested diff --git a/instrumentation/resources/library/src/test/java/io/opentelemetry/instrumentation/resources/ProcessResourceTest.java b/instrumentation/resources/library/src/test/java/io/opentelemetry/instrumentation/resources/ProcessResourceTest.java index 80147d337632..27bfe409e2c2 100644 --- a/instrumentation/resources/library/src/test/java/io/opentelemetry/instrumentation/resources/ProcessResourceTest.java +++ b/instrumentation/resources/library/src/test/java/io/opentelemetry/instrumentation/resources/ProcessResourceTest.java @@ -9,7 +9,8 @@ import io.opentelemetry.api.common.Attributes; import io.opentelemetry.sdk.resources.Resource; -import io.opentelemetry.semconv.resource.attributes.ResourceAttributes; +import io.opentelemetry.semconv.SchemaUrls; +import io.opentelemetry.semconv.incubating.ProcessIncubatingAttributes; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; @@ -24,13 +25,14 @@ class ProcessResourceTest { @SetSystemProperty(key = "os.name", value = "Linux 4.12") void notWindows() { Resource resource = ProcessResource.buildResource(); - assertThat(resource.getSchemaUrl()).isEqualTo(ResourceAttributes.SCHEMA_URL); + assertThat(resource.getSchemaUrl()).isEqualTo(SchemaUrls.V1_24_0); Attributes attributes = resource.getAttributes(); - assertThat(attributes.get(ResourceAttributes.PROCESS_PID)).isGreaterThan(1); - assertThat(attributes.get(ResourceAttributes.PROCESS_EXECUTABLE_PATH)).matches(".*[/\\\\]java"); - assertThat(attributes.get(ResourceAttributes.PROCESS_COMMAND_LINE)) - .contains(attributes.get(ResourceAttributes.PROCESS_EXECUTABLE_PATH)); + assertThat(attributes.get(ProcessIncubatingAttributes.PROCESS_PID)).isGreaterThan(1); + assertThat(attributes.get(ProcessIncubatingAttributes.PROCESS_EXECUTABLE_PATH)) + .matches(".*[/\\\\]java"); + assertThat(attributes.get(ProcessIncubatingAttributes.PROCESS_COMMAND_LINE)) + .contains(attributes.get(ProcessIncubatingAttributes.PROCESS_EXECUTABLE_PATH)); // With Java 9+ and a compiled jar, ResourceAttributes.PROCESS_COMMAND_ARGS // will be set instead of ResourceAttributes.PROCESS_COMMAND_LINE } @@ -39,14 +41,14 @@ void notWindows() { @SetSystemProperty(key = "os.name", value = "Windows 10") void windows() { Resource resource = ProcessResource.buildResource(); - assertThat(resource.getSchemaUrl()).isEqualTo(ResourceAttributes.SCHEMA_URL); + assertThat(resource.getSchemaUrl()).isEqualTo(SchemaUrls.V1_24_0); Attributes attributes = resource.getAttributes(); - assertThat(attributes.get(ResourceAttributes.PROCESS_PID)).isGreaterThan(1); - assertThat(attributes.get(ResourceAttributes.PROCESS_EXECUTABLE_PATH)) + assertThat(attributes.get(ProcessIncubatingAttributes.PROCESS_PID)).isGreaterThan(1); + assertThat(attributes.get(ProcessIncubatingAttributes.PROCESS_EXECUTABLE_PATH)) .matches(".*[/\\\\]java\\.exe"); - assertThat(attributes.get(ResourceAttributes.PROCESS_COMMAND_LINE)) - .contains(attributes.get(ResourceAttributes.PROCESS_EXECUTABLE_PATH)); + assertThat(attributes.get(ProcessIncubatingAttributes.PROCESS_COMMAND_LINE)) + .contains(attributes.get(ProcessIncubatingAttributes.PROCESS_EXECUTABLE_PATH)); // With Java 9+ and a compiled jar, ResourceAttributes.PROCESS_COMMAND_ARGS // will be set instead of ResourceAttributes.PROCESS_COMMAND_LINE } @@ -61,7 +63,7 @@ static class SecurityManagerEnabled { @Test void empty() { Attributes attributes = ProcessResource.buildResource().getAttributes(); - assertThat(attributes.asMap()).containsOnlyKeys(ResourceAttributes.PROCESS_PID); + assertThat(attributes.asMap()).containsOnlyKeys(ProcessIncubatingAttributes.PROCESS_PID); } } } diff --git a/instrumentation/resources/library/src/test/java/io/opentelemetry/instrumentation/resources/ProcessRuntimeResourceTest.java b/instrumentation/resources/library/src/test/java/io/opentelemetry/instrumentation/resources/ProcessRuntimeResourceTest.java index 2893d40a1df7..d8fd1084c009 100644 --- a/instrumentation/resources/library/src/test/java/io/opentelemetry/instrumentation/resources/ProcessRuntimeResourceTest.java +++ b/instrumentation/resources/library/src/test/java/io/opentelemetry/instrumentation/resources/ProcessRuntimeResourceTest.java @@ -9,7 +9,8 @@ import io.opentelemetry.api.common.Attributes; import io.opentelemetry.sdk.resources.Resource; -import io.opentelemetry.semconv.resource.attributes.ResourceAttributes; +import io.opentelemetry.semconv.SchemaUrls; +import io.opentelemetry.semconv.incubating.ProcessIncubatingAttributes; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; @@ -25,10 +26,11 @@ void shouldCreateRuntimeAttributes() { Attributes attributes = resource.getAttributes(); // then - assertThat(resource.getSchemaUrl()).isEqualTo(ResourceAttributes.SCHEMA_URL); - assertThat(attributes.get(ResourceAttributes.PROCESS_RUNTIME_NAME)).isNotBlank(); - assertThat(attributes.get(ResourceAttributes.PROCESS_RUNTIME_VERSION)).isNotBlank(); - assertThat(attributes.get(ResourceAttributes.PROCESS_RUNTIME_DESCRIPTION)).isNotBlank(); + assertThat(resource.getSchemaUrl()).isEqualTo(SchemaUrls.V1_24_0); + assertThat(attributes.get(ProcessIncubatingAttributes.PROCESS_RUNTIME_NAME)).isNotBlank(); + assertThat(attributes.get(ProcessIncubatingAttributes.PROCESS_RUNTIME_VERSION)).isNotBlank(); + assertThat(attributes.get(ProcessIncubatingAttributes.PROCESS_RUNTIME_DESCRIPTION)) + .isNotBlank(); } @Nested diff --git a/instrumentation/resources/library/src/test/resources/MANIFEST.MF b/instrumentation/resources/library/src/test/resources/MANIFEST.MF new file mode 100644 index 000000000000..28f63e3a3517 --- /dev/null +++ b/instrumentation/resources/library/src/test/resources/MANIFEST.MF @@ -0,0 +1,3 @@ +Manifest-Version: 1.0 +Implementation-Title: demo +Implementation-Version: 0.0.1-SNAPSHOT diff --git a/instrumentation/resources/library/src/test/resources/containerd_proc_self_mountinfo b/instrumentation/resources/library/src/test/resources/containerd_proc_self_mountinfo new file mode 100644 index 000000000000..a8281ad76738 --- /dev/null +++ b/instrumentation/resources/library/src/test/resources/containerd_proc_self_mountinfo @@ -0,0 +1,38 @@ +2002 1895 0:226 / / rw,relatime master:629 - overlay overlay rw,lowerdir=/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/75438/fs:/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/75437/fs:/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/75394/fs,upperdir=/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/75439/fs,workdir=/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/75439/work,xino=off +2003 2002 0:227 / /proc rw,nosuid,nodev,noexec,relatime - proc proc rw +2004 2002 0:228 / /dev rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755 +2005 2004 0:229 / /dev/pts rw,nosuid,noexec,relatime - devpts devpts rw,gid=5,mode=620,ptmxmode=666 +2006 2004 0:117 / /dev/mqueue rw,nosuid,nodev,noexec,relatime - mqueue mqueue rw +2007 2002 0:124 / /sys ro,nosuid,nodev,noexec,relatime - sysfs sysfs ro +2008 2007 0:230 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755 +2009 2008 0:32 /system.slice/containerd.service/kubepods-burstable-pod321c09bf_282b_44e4_a467_39daf144ef1f.slice:cri-containerd:f2a44bc8e090f93a2b4d7f510bdaff0615ad52906e3287ee956dcf5aa5012a91 /sys/fs/cgroup/systemd ro,nosuid,nodev,noexec,relatime master:11 - cgroup cgroup rw,xattr,name=systemd +2010 2008 0:35 /system.slice/containerd.service/kubepods-burstable-pod321c09bf_282b_44e4_a467_39daf144ef1f.slice:cri-containerd:f2a44bc8e090f93a2b4d7f510bdaff0615ad52906e3287ee956dcf5aa5012a91 /sys/fs/cgroup/pids ro,nosuid,nodev,noexec,relatime master:15 - cgroup cgroup rw,pids +2011 2008 0:36 /kubepods-burstable-pod321c09bf_282b_44e4_a467_39daf144ef1f.slice:cri-containerd:f2a44bc8e090f93a2b4d7f510bdaff0615ad52906e3287ee956dcf5aa5012a91 /sys/fs/cgroup/freezer ro,nosuid,nodev,noexec,relatime master:16 - cgroup cgroup rw,freezer +2012 2008 0:37 /system.slice/containerd.service/kubepods-burstable-pod321c09bf_282b_44e4_a467_39daf144ef1f.slice:cri-containerd:f2a44bc8e090f93a2b4d7f510bdaff0615ad52906e3287ee956dcf5aa5012a91 /sys/fs/cgroup/cpu,cpuacct ro,nosuid,nodev,noexec,relatime master:17 - cgroup cgroup rw,cpu,cpuacct +2013 2008 0:38 /system.slice/containerd.service/kubepods-burstable-pod321c09bf_282b_44e4_a467_39daf144ef1f.slice:cri-containerd:f2a44bc8e090f93a2b4d7f510bdaff0615ad52906e3287ee956dcf5aa5012a91 /sys/fs/cgroup/blkio ro,nosuid,nodev,noexec,relatime master:18 - cgroup cgroup rw,blkio +2014 2008 0:39 /kubepods-burstable-pod321c09bf_282b_44e4_a467_39daf144ef1f.slice:cri-containerd:f2a44bc8e090f93a2b4d7f510bdaff0615ad52906e3287ee956dcf5aa5012a91 /sys/fs/cgroup/cpuset ro,nosuid,nodev,noexec,relatime master:19 - cgroup cgroup rw,cpuset +2015 2008 0:40 /kubepods-burstable-pod321c09bf_282b_44e4_a467_39daf144ef1f.slice:cri-containerd:f2a44bc8e090f93a2b4d7f510bdaff0615ad52906e3287ee956dcf5aa5012a91 /sys/fs/cgroup/hugetlb ro,nosuid,nodev,noexec,relatime master:20 - cgroup cgroup rw,hugetlb +2016 2008 0:41 /system.slice/containerd.service/kubepods-burstable-pod321c09bf_282b_44e4_a467_39daf144ef1f.slice:cri-containerd:f2a44bc8e090f93a2b4d7f510bdaff0615ad52906e3287ee956dcf5aa5012a91 /sys/fs/cgroup/memory ro,nosuid,nodev,noexec,relatime master:21 - cgroup cgroup rw,memory +2017 2008 0:42 /kubepods-burstable-pod321c09bf_282b_44e4_a467_39daf144ef1f.slice:cri-containerd:f2a44bc8e090f93a2b4d7f510bdaff0615ad52906e3287ee956dcf5aa5012a91 /sys/fs/cgroup/net_cls,net_prio ro,nosuid,nodev,noexec,relatime master:22 - cgroup cgroup rw,net_cls,net_prio +2018 2008 0:43 /system.slice/containerd.service/kubepods-burstable-pod321c09bf_282b_44e4_a467_39daf144ef1f.slice:cri-containerd:f2a44bc8e090f93a2b4d7f510bdaff0615ad52906e3287ee956dcf5aa5012a91 /sys/fs/cgroup/devices ro,nosuid,nodev,noexec,relatime master:23 - cgroup cgroup rw,devices +2019 2008 0:44 /kubepods-burstable-pod321c09bf_282b_44e4_a467_39daf144ef1f.slice:cri-containerd:f2a44bc8e090f93a2b4d7f510bdaff0615ad52906e3287ee956dcf5aa5012a91 /sys/fs/cgroup/rdma ro,nosuid,nodev,noexec,relatime master:24 - cgroup cgroup rw,rdma +2020 2008 0:45 /kubepods-burstable-pod321c09bf_282b_44e4_a467_39daf144ef1f.slice:cri-containerd:f2a44bc8e090f93a2b4d7f510bdaff0615ad52906e3287ee956dcf5aa5012a91 /sys/fs/cgroup/perf_event ro,nosuid,nodev,noexec,relatime master:25 - cgroup cgroup rw,perf_event +2021 2002 253:1 /var/lib/kubelet/pods/321c09bf-282b-44e4-a467-39daf144ef1f/etc-hosts /etc/hosts rw,relatime - xfs /dev/mapper/ubuntu--vg-root rw,attr2,inode64,logbufs=8,logbsize=32k,noquota +2022 2004 253:1 /var/lib/kubelet/pods/321c09bf-282b-44e4-a467-39daf144ef1f/containers/accountingservice/82b03b66 /dev/termination-log rw,relatime - xfs /dev/mapper/ubuntu--vg-root rw,attr2,inode64,logbufs=8,logbsize=32k,noquota +2023 2002 253:1 /var/lib/containerd/io.containerd.grpc.v1.cri/sandboxes/b136f3d296b4c2024b3e7ad816f2a804a47cf1acc3d445075c6d78cf159ef58d/hostname /etc/hostname rw,relatime - xfs /dev/mapper/ubuntu--vg-root rw,attr2,inode64,logbufs=8,logbsize=32k,noquota +2024 2002 253:1 /var/lib/containerd/io.containerd.grpc.v1.cri/sandboxes/b136f3d296b4c2024b3e7ad816f2a804a47cf1acc3d445075c6d78cf159ef58d/resolv.conf /etc/resolv.conf rw,relatime - xfs /dev/mapper/ubuntu--vg-root rw,attr2,inode64,logbufs=8,logbsize=32k,noquota +2025 2004 0:115 / /dev/shm rw,nosuid,nodev,noexec,relatime - tmpfs shm rw,size=65536k +2026 2002 0:96 / /run/secrets/kubernetes.io/serviceaccount ro,relatime - tmpfs tmpfs rw,size=32768456k +1896 2003 0:227 /bus /proc/bus ro,nosuid,nodev,noexec,relatime - proc proc rw +1897 2003 0:227 /fs /proc/fs ro,nosuid,nodev,noexec,relatime - proc proc rw +1898 2003 0:227 /irq /proc/irq ro,nosuid,nodev,noexec,relatime - proc proc rw +1899 2003 0:227 /sys /proc/sys ro,nosuid,nodev,noexec,relatime - proc proc rw +1900 2003 0:227 /sysrq-trigger /proc/sysrq-trigger ro,nosuid,nodev,noexec,relatime - proc proc rw +1901 2003 0:231 / /proc/asound ro,relatime - tmpfs tmpfs ro +1902 2003 0:232 / /proc/acpi ro,relatime - tmpfs tmpfs ro +1903 2003 0:228 /null /proc/kcore rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755 +1904 2003 0:228 /null /proc/keys rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755 +1905 2003 0:228 /null /proc/timer_list rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755 +1906 2003 0:228 /null /proc/sched_debug rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755 +1907 2003 0:233 / /proc/scsi ro,relatime - tmpfs tmpfs ro +1908 2007 0:234 / /sys/firmware ro,relatime - tmpfs tmpfs ro diff --git a/instrumentation/resources/library/src/test/resources/crio_proc_self_mountinfo b/instrumentation/resources/library/src/test/resources/crio_proc_self_mountinfo new file mode 100644 index 000000000000..8f94c3bee579 --- /dev/null +++ b/instrumentation/resources/library/src/test/resources/crio_proc_self_mountinfo @@ -0,0 +1,26 @@ +10303 9025 0:676 / / rw,relatime master:2633 - overlay overlay rw,context="system_u:object_r:container_file_t:s0:c285,c353",lowerdir=/var/lib/containers/storage/overlay/l/MOUYF2QTVBFHJCEJ7L4FQSJBYL:/var/lib/containers/storage/overlay/l/G6UHPBRIDD4LUQGKZ3B3LQNNBF:/var/lib/containers/storage/overlay/l/NYLNBZF5BPFKTTPCUH2NV2CI76,upperdir=/var/lib/containers/storage/overlay/7af2ec0ca188ec1e39e53f5a89d81ddcc721d39b6d2b818d0b00c6accf871382/diff,workdir=/var/lib/containers/storage/overlay/7af2ec0ca188ec1e39e53f5a89d81ddcc721d39b6d2b818d0b00c6accf871382/work,volatile +10304 10303 0:680 / /proc rw,nosuid,nodev,noexec,relatime - proc proc rw +10305 10303 0:681 / /dev rw,nosuid - tmpfs tmpfs rw,context="system_u:object_r:container_file_t:s0:c285,c353",size=65536k,mode=755,inode64 +10306 10305 0:689 / /dev/pts rw,nosuid,noexec,relatime - devpts devpts rw,context="system_u:object_r:container_file_t:s0:c285,c353",gid=5,mode=620,ptmxmode=666 +10307 10305 0:668 / /dev/mqueue rw,nosuid,nodev,noexec,relatime - mqueue mqueue rw,seclabel +10308 10303 0:690 / /sys ro,nosuid,nodev,noexec,relatime - sysfs sysfs ro,seclabel +10309 10308 0:26 / /sys/fs/cgroup ro,nosuid,nodev,noexec,relatime - cgroup2 cgroup rw,seclabel +10310 10305 0:667 / /dev/shm rw,nosuid,nodev,noexec,relatime master:2583 - tmpfs shm rw,context="system_u:object_r:container_file_t:s0:c285,c353",size=65536k,inode64 +10311 10303 0:25 /containers/storage/overlay-containers/2ac4c84cb0d3c3beb04beeef6ccf71c17b5fdd0252ce3a2b66bc2fdd0aaa1814/userdata/resolv.conf /etc/resolv.conf rw,nosuid,nodev,noexec master:15 - tmpfs tmpfs rw,seclabel,size=6416204k,nr_inodes=819200,mode=755,inode64 +10312 10303 0:25 /containers/storage/overlay-containers/2ac4c84cb0d3c3beb04beeef6ccf71c17b5fdd0252ce3a2b66bc2fdd0aaa1814/userdata/hostname /etc/hostname rw,nosuid,nodev master:15 - tmpfs tmpfs rw,seclabel,size=6416204k,nr_inodes=819200,mode=755,inode64 +10313 10303 0:25 /containers/storage/overlay-containers/2ac4c84cb0d3c3beb04beeef6ccf71c17b5fdd0252ce3a2b66bc2fdd0aaa1814/userdata/.containerenv /run/.containerenv rw,nosuid,nodev master:15 - tmpfs tmpfs rw,seclabel,size=6416204k,nr_inodes=819200,mode=755,inode64 +10314 10303 252:4 /ostree/deploy/rhcos/var/lib/kubelet/pods/0a947273-7214-4824-8411-875ebd7626e4/etc-hosts /etc/hosts rw,relatime - xfs /dev/vda4 rw,seclabel,attr2,inode64,logbufs=8,logbsize=32k,prjquota +10315 10305 252:4 /ostree/deploy/rhcos/var/lib/kubelet/pods/0a947273-7214-4824-8411-875ebd7626e4/containers/ubuntu23/354653d0 /dev/termination-log rw,relatime - xfs /dev/vda4 rw,seclabel,attr2,inode64,logbufs=8,logbsize=32k,prjquota +10316 10303 0:25 /containers/storage/overlay-containers/a8f62e52ed7c2cd85242dcf0eb1d727b643540ceca7f328ad7d2f31aedf07731/userdata/run/secrets /run/secrets rw,nosuid,nodev - tmpfs tmpfs rw,seclabel,size=6416204k,nr_inodes=819200,mode=755,inode64 +10317 10316 0:666 / /run/secrets/kubernetes.io/serviceaccount ro,relatime - tmpfs tmpfs rw,seclabel,size=30930028k,inode64 +9026 10304 0:680 /bus /proc/bus ro,nosuid,nodev,noexec,relatime - proc proc rw +9027 10304 0:680 /fs /proc/fs ro,nosuid,nodev,noexec,relatime - proc proc rw +9029 10304 0:680 /irq /proc/irq ro,nosuid,nodev,noexec,relatime - proc proc rw +9030 10304 0:680 /sys /proc/sys ro,nosuid,nodev,noexec,relatime - proc proc rw +9031 10304 0:680 /sysrq-trigger /proc/sysrq-trigger ro,nosuid,nodev,noexec,relatime - proc proc rw +9032 10304 0:691 / /proc/acpi ro,relatime - tmpfs tmpfs ro,context="system_u:object_r:container_file_t:s0:c285,c353",inode64 +9033 10304 0:681 /null /proc/kcore rw,nosuid - tmpfs tmpfs rw,context="system_u:object_r:container_file_t:s0:c285,c353",size=65536k,mode=755,inode64 +9034 10304 0:681 /null /proc/keys rw,nosuid - tmpfs tmpfs rw,context="system_u:object_r:container_file_t:s0:c285,c353",size=65536k,mode=755,inode64 +9035 10304 0:681 /null /proc/timer_list rw,nosuid - tmpfs tmpfs rw,context="system_u:object_r:container_file_t:s0:c285,c353",size=65536k,mode=755,inode64 +9036 10304 0:692 / /proc/scsi ro,relatime - tmpfs tmpfs ro,context="system_u:object_r:container_file_t:s0:c285,c353",inode64 +9037 10308 0:693 / /sys/firmware ro,relatime - tmpfs tmpfs ro,context="system_u:object_r:container_file_t:s0:c285,c353",inode64 diff --git a/instrumentation/resources/library/src/test/resources/crio_proc_self_mountinfo1 b/instrumentation/resources/library/src/test/resources/crio_proc_self_mountinfo1 new file mode 100644 index 000000000000..dad97baab98d --- /dev/null +++ b/instrumentation/resources/library/src/test/resources/crio_proc_self_mountinfo1 @@ -0,0 +1,39 @@ +7276 6904 0:507 / / rw,relatime - overlay overlay rw,context="system_u:object_r:container_file_t:s0:c316,c673",lowerdir=/var/lib/containers/storage/overlay/l/4HQBBEBQQ7RYBYIRWG7IXYYZGD:/var/lib/containers/storage/overlay/l/EB6QLCMK46AXWSZ7J7IGCILPAE:/var/lib/containers/storage/overlay/l/OUK2FRIJID36QBEM6RUSRYQQOZ,upperdir=/var/lib/containers/storage/overlay/5a3fa5d05d5e872c74880dc836cdf6946529f311dfa3b423b5411f02aca1d895/diff,workdir=/var/lib/containers/storage/overlay/5a3fa5d05d5e872c74880dc836cdf6946529f311dfa3b423b5411f02aca1d895/work,volatile +7277 7276 0:509 / /proc rw,nosuid,nodev,noexec,relatime - proc proc rw +7278 7276 0:510 / /dev rw,nosuid - tmpfs tmpfs rw,context="system_u:object_r:container_file_t:s0:c316,c673",size=65536k,mode=755,inode64 +7298 7278 0:511 / /dev/pts rw,nosuid,noexec,relatime - devpts devpts rw,context="system_u:object_r:container_file_t:s0:c316,c673",gid=5,mode=620,ptmxmode=666 +7535 7278 0:334 / /dev/mqueue rw,nosuid,nodev,noexec,relatime - mqueue mqueue rw,seclabel +7538 7276 0:512 / /sys ro,nosuid,nodev,noexec,relatime - sysfs sysfs ro,seclabel +7281 7538 0:517 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,context="system_u:object_r:container_file_t:s0:c316,c673",mode=755,inode64 +7282 7281 0:27 /kubepods.slice/kubepods-burstable.slice/kubepods-burstable-pod8f215fa2_6177_4ab9_b1f4_c802d19657bc.slice/crio-f23ec1d4b715c6531a17e9c549222fbbe1f7ffff697a29a2212b3b4cdc37f52e.scope /sys/fs/cgroup/systemd ro,nosuid,nodev,noexec,relatime master:9 - cgroup cgroup rw,seclabel,xattr,release_agent=/usr/lib/systemd/systemd-cgroups-agent,name=systemd +7283 7281 0:30 /kubepods.slice/kubepods-burstable.slice/kubepods-burstable-pod8f215fa2_6177_4ab9_b1f4_c802d19657bc.slice/crio-f23ec1d4b715c6531a17e9c549222fbbe1f7ffff697a29a2212b3b4cdc37f52e.scope /sys/fs/cgroup/memory ro,nosuid,nodev,noexec,relatime master:10 - cgroup cgroup rw,seclabel,memory +7284 7281 0:31 /kubepods.slice/kubepods-burstable.slice/kubepods-burstable-pod8f215fa2_6177_4ab9_b1f4_c802d19657bc.slice/crio-f23ec1d4b715c6531a17e9c549222fbbe1f7ffff697a29a2212b3b4cdc37f52e.scope /sys/fs/cgroup/cpu,cpuacct ro,nosuid,nodev,noexec,relatime master:11 - cgroup cgroup rw,seclabel,cpu,cpuacct +7285 7281 0:32 /kubepods.slice/kubepods-burstable.slice/kubepods-burstable-pod8f215fa2_6177_4ab9_b1f4_c802d19657bc.slice/crio-f23ec1d4b715c6531a17e9c549222fbbe1f7ffff697a29a2212b3b4cdc37f52e.scope /sys/fs/cgroup/rdma ro,nosuid,nodev,noexec,relatime master:12 - cgroup cgroup rw,seclabel,rdma +7286 7281 0:33 /kubepods.slice/kubepods-burstable.slice/kubepods-burstable-pod8f215fa2_6177_4ab9_b1f4_c802d19657bc.slice/crio-f23ec1d4b715c6531a17e9c549222fbbe1f7ffff697a29a2212b3b4cdc37f52e.scope /sys/fs/cgroup/devices ro,nosuid,nodev,noexec,relatime master:13 - cgroup cgroup rw,seclabel,devices +7287 7281 0:34 /kubepods.slice/kubepods-burstable.slice/kubepods-burstable-pod8f215fa2_6177_4ab9_b1f4_c802d19657bc.slice/crio-f23ec1d4b715c6531a17e9c549222fbbe1f7ffff697a29a2212b3b4cdc37f52e.scope /sys/fs/cgroup/pids ro,nosuid,nodev,noexec,relatime master:14 - cgroup cgroup rw,seclabel,pids +7288 7281 0:35 /kubepods.slice/kubepods-burstable.slice/kubepods-burstable-pod8f215fa2_6177_4ab9_b1f4_c802d19657bc.slice/crio-f23ec1d4b715c6531a17e9c549222fbbe1f7ffff697a29a2212b3b4cdc37f52e.scope /sys/fs/cgroup/blkio ro,nosuid,nodev,noexec,relatime master:15 - cgroup cgroup rw,seclabel,blkio +7289 7281 0:36 /kubepods.slice/kubepods-burstable.slice/kubepods-burstable-pod8f215fa2_6177_4ab9_b1f4_c802d19657bc.slice/crio-f23ec1d4b715c6531a17e9c549222fbbe1f7ffff697a29a2212b3b4cdc37f52e.scope /sys/fs/cgroup/net_cls,net_prio ro,nosuid,nodev,noexec,relatime master:16 - cgroup cgroup rw,seclabel,net_cls,net_prio +7291 7281 0:37 /kubepods.slice/kubepods-burstable.slice/kubepods-burstable-pod8f215fa2_6177_4ab9_b1f4_c802d19657bc.slice/crio-f23ec1d4b715c6531a17e9c549222fbbe1f7ffff697a29a2212b3b4cdc37f52e.scope /sys/fs/cgroup/hugetlb ro,nosuid,nodev,noexec,relatime master:17 - cgroup cgroup rw,seclabel,hugetlb +7293 7281 0:38 /kubepods.slice/kubepods-burstable.slice/kubepods-burstable-pod8f215fa2_6177_4ab9_b1f4_c802d19657bc.slice/crio-f23ec1d4b715c6531a17e9c549222fbbe1f7ffff697a29a2212b3b4cdc37f52e.scope /sys/fs/cgroup/perf_event ro,nosuid,nodev,noexec,relatime master:18 - cgroup cgroup rw,seclabel,perf_event +7294 7281 0:39 /kubepods.slice/kubepods-burstable.slice/kubepods-burstable-pod8f215fa2_6177_4ab9_b1f4_c802d19657bc.slice/crio-f23ec1d4b715c6531a17e9c549222fbbe1f7ffff697a29a2212b3b4cdc37f52e.scope /sys/fs/cgroup/freezer ro,nosuid,nodev,noexec,relatime master:19 - cgroup cgroup rw,seclabel,freezer +7296 7281 0:40 /kubepods.slice/kubepods-burstable.slice/kubepods-burstable-pod8f215fa2_6177_4ab9_b1f4_c802d19657bc.slice/crio-f23ec1d4b715c6531a17e9c549222fbbe1f7ffff697a29a2212b3b4cdc37f52e.scope /sys/fs/cgroup/cpuset ro,nosuid,nodev,noexec,relatime master:20 - cgroup cgroup rw,seclabel,cpuset +7299 7281 0:41 /kubepods.slice/kubepods-burstable.slice/kubepods-burstable-pod8f215fa2_6177_4ab9_b1f4_c802d19657bc.slice/crio-f23ec1d4b715c6531a17e9c549222fbbe1f7ffff697a29a2212b3b4cdc37f52e.scope /sys/fs/cgroup/misc ro,nosuid,nodev,noexec,relatime master:21 - cgroup cgroup rw,seclabel,misc +7300 7278 0:333 / /dev/shm rw,nosuid,nodev,noexec,relatime master:1420 - tmpfs shm rw,context="system_u:object_r:container_file_t:s0:c316,c673",size=65536k,inode64 +7302 7276 0:25 /containers/storage/overlay-containers/757a1c14bdd68b907c41f15436c0c2f9ec5a4cd4317135fcc1c4a64188db98d0/userdata/resolv.conf /etc/resolv.conf rw,nosuid,nodev,noexec master:28 - tmpfs tmpfs rw,seclabel,size=6416204k,nr_inodes=819200,mode=755,inode64 +7304 7276 0:25 /containers/storage/overlay-containers/757a1c14bdd68b907c41f15436c0c2f9ec5a4cd4317135fcc1c4a64188db98d0/userdata/hostname /etc/hostname rw,nosuid,nodev master:28 - tmpfs tmpfs rw,seclabel,size=6416204k,nr_inodes=819200,mode=755,inode64 +7305 7276 0:25 /containers/storage/overlay-containers/757a1c14bdd68b907c41f15436c0c2f9ec5a4cd4317135fcc1c4a64188db98d0/userdata/.containerenv /run/.containerenv rw,nosuid,nodev master:28 - tmpfs tmpfs rw,seclabel,size=6416204k,nr_inodes=819200,mode=755,inode64 +7307 7276 252:4 /ostree/deploy/rhcos/var/lib/kubelet/pods/8f215fa2-6177-4ab9-b1f4-c802d19657bc/etc-hosts /etc/hosts rw,relatime - xfs /dev/vda4 rw,seclabel,attr2,inode64,logbufs=8,logbsize=32k,prjquota +7308 7278 252:4 /ostree/deploy/rhcos/var/lib/kubelet/pods/8f215fa2-6177-4ab9-b1f4-c802d19657bc/containers/accountingservice/e1dc5db8 /dev/termination-log rw,relatime - xfs /dev/vda4 rw,seclabel,attr2,inode64,logbufs=8,logbsize=32k,prjquota +7310 7276 0:25 /containers/storage/overlay-containers/f23ec1d4b715c6531a17e9c549222fbbe1f7ffff697a29a2212b3b4cdc37f52e/userdata/run/secrets /run/secrets rw,nosuid,nodev - tmpfs tmpfs rw,seclabel,size=6416204k,nr_inodes=819200,mode=755,inode64 +7312 7310 0:313 / /run/secrets/kubernetes.io/serviceaccount ro,relatime - tmpfs tmpfs rw,seclabel,size=30930028k,inode64 +6905 7277 0:509 /bus /proc/bus ro,nosuid,nodev,noexec,relatime - proc proc rw +6906 7277 0:509 /fs /proc/fs ro,nosuid,nodev,noexec,relatime - proc proc rw +6908 7277 0:509 /irq /proc/irq ro,nosuid,nodev,noexec,relatime - proc proc rw +6909 7277 0:509 /sys /proc/sys ro,nosuid,nodev,noexec,relatime - proc proc rw +6910 7277 0:509 /sysrq-trigger /proc/sysrq-trigger ro,nosuid,nodev,noexec,relatime - proc proc rw +6911 7277 0:519 / /proc/acpi ro,relatime - tmpfs tmpfs ro,context="system_u:object_r:container_file_t:s0:c316,c673",inode64 +6912 7277 0:510 /null /proc/kcore rw,nosuid - tmpfs tmpfs rw,context="system_u:object_r:container_file_t:s0:c316,c673",size=65536k,mode=755,inode64 +6913 7277 0:510 /null /proc/keys rw,nosuid - tmpfs tmpfs rw,context="system_u:object_r:container_file_t:s0:c316,c673",size=65536k,mode=755,inode64 +6914 7277 0:510 /null /proc/timer_list rw,nosuid - tmpfs tmpfs rw,context="system_u:object_r:container_file_t:s0:c316,c673",size=65536k,mode=755,inode64 +6915 7277 0:520 / /proc/scsi ro,relatime - tmpfs tmpfs ro,context="system_u:object_r:container_file_t:s0:c316,c673",inode64 +6916 7538 0:521 / /sys/firmware ro,relatime - tmpfs tmpfs ro,context="system_u:object_r:container_file_t:s0:c316,c673",inode64 diff --git a/instrumentation/resources/library/src/test/resources/crio_proc_self_mountinfo2 b/instrumentation/resources/library/src/test/resources/crio_proc_self_mountinfo2 new file mode 100644 index 000000000000..68e7a064e352 --- /dev/null +++ b/instrumentation/resources/library/src/test/resources/crio_proc_self_mountinfo2 @@ -0,0 +1,38 @@ +5989 5586 0:433 / / rw,relatime - overlay overlay rw,context="system_u:object_r:container_file_t:s0:c898,c999",lowerdir=/var/lib/containers/storage/overlay/l/6L7GBRDSDQ5M2S7KRI6X24TWEK:/var/lib/containers/storage/overlay/l/BDMEMSU6EKIVTDHY6DPE6ERXTU:/var/lib/containers/storage/overlay/l/I64YWGP3Z4E6ZFBAKJS22PN5R7,upperdir=/var/lib/containers/storage/overlay/92e5b09335dc1206f36bc26dfc2c6672082b02fc6eb21f8c38334523fdef835e/diff,workdir=/var/lib/containers/storage/overlay/92e5b09335dc1206f36bc26dfc2c6672082b02fc6eb21f8c38334523fdef835e/work,volatile +5990 5989 0:437 / /proc rw,nosuid,nodev,noexec,relatime - proc proc rw +5991 5989 0:438 / /dev rw,nosuid - tmpfs tmpfs rw,context="system_u:object_r:container_file_t:s0:c898,c999",size=65536k,mode=755,inode64 +5992 5991 0:439 / /dev/pts rw,nosuid,noexec,relatime - devpts devpts rw,context="system_u:object_r:container_file_t:s0:c898,c999",gid=5,mode=620,ptmxmode=666 +5993 5991 0:316 / /dev/mqueue rw,nosuid,nodev,noexec,relatime - mqueue mqueue rw,seclabel +5994 5989 0:440 / /sys ro,nosuid,nodev,noexec,relatime - sysfs sysfs ro,seclabel +5995 5994 0:441 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,context="system_u:object_r:container_file_t:s0:c898,c999",mode=755,inode64 +5996 5995 0:27 /kubepods.slice/kubepods-burstable.slice/kubepods-burstable-pod19ccacf5_fcff_4bd3_a33a_19ff41f6ccd2.slice/crio-b4873629b312dc1d77472aba6fb177c6ce9a8f7c205ad7a03302726805007fe6.scope /sys/fs/cgroup/systemd ro,nosuid,nodev,noexec,relatime master:9 - cgroup cgroup rw,seclabel,xattr,release_agent=/usr/lib/systemd/systemd-cgroups-agent,name=systemd +5997 5995 0:30 /kubepods.slice/kubepods-burstable.slice/kubepods-burstable-pod19ccacf5_fcff_4bd3_a33a_19ff41f6ccd2.slice/crio-b4873629b312dc1d77472aba6fb177c6ce9a8f7c205ad7a03302726805007fe6.scope /sys/fs/cgroup/blkio ro,nosuid,nodev,noexec,relatime master:10 - cgroup cgroup rw,seclabel,blkio +5998 5995 0:31 /kubepods.slice/kubepods-burstable.slice/kubepods-burstable-pod19ccacf5_fcff_4bd3_a33a_19ff41f6ccd2.slice/crio-b4873629b312dc1d77472aba6fb177c6ce9a8f7c205ad7a03302726805007fe6.scope /sys/fs/cgroup/net_cls,net_prio ro,nosuid,nodev,noexec,relatime master:11 - cgroup cgroup rw,seclabel,net_cls,net_prio +5999 5995 0:32 /kubepods.slice/kubepods-burstable.slice/kubepods-burstable-pod19ccacf5_fcff_4bd3_a33a_19ff41f6ccd2.slice/crio-b4873629b312dc1d77472aba6fb177c6ce9a8f7c205ad7a03302726805007fe6.scope /sys/fs/cgroup/rdma ro,nosuid,nodev,noexec,relatime master:12 - cgroup cgroup rw,seclabel,rdma +6000 5995 0:33 /kubepods.slice/kubepods-burstable.slice/kubepods-burstable-pod19ccacf5_fcff_4bd3_a33a_19ff41f6ccd2.slice/crio-b4873629b312dc1d77472aba6fb177c6ce9a8f7c205ad7a03302726805007fe6.scope /sys/fs/cgroup/memory ro,nosuid,nodev,noexec,relatime master:13 - cgroup cgroup rw,seclabel,memory +6001 5995 0:34 /kubepods.slice/kubepods-burstable.slice/kubepods-burstable-pod19ccacf5_fcff_4bd3_a33a_19ff41f6ccd2.slice/crio-b4873629b312dc1d77472aba6fb177c6ce9a8f7c205ad7a03302726805007fe6.scope /sys/fs/cgroup/cpuset ro,nosuid,nodev,noexec,relatime master:14 - cgroup cgroup rw,seclabel,cpuset +6002 5995 0:35 /kubepods.slice/kubepods-burstable.slice/kubepods-burstable-pod19ccacf5_fcff_4bd3_a33a_19ff41f6ccd2.slice/crio-b4873629b312dc1d77472aba6fb177c6ce9a8f7c205ad7a03302726805007fe6.scope /sys/fs/cgroup/pids ro,nosuid,nodev,noexec,relatime master:15 - cgroup cgroup rw,seclabel,pids +6003 5995 0:36 /kubepods.slice/kubepods-burstable.slice/kubepods-burstable-pod19ccacf5_fcff_4bd3_a33a_19ff41f6ccd2.slice/crio-b4873629b312dc1d77472aba6fb177c6ce9a8f7c205ad7a03302726805007fe6.scope /sys/fs/cgroup/hugetlb ro,nosuid,nodev,noexec,relatime master:16 - cgroup cgroup rw,seclabel,hugetlb +6004 5995 0:37 /kubepods.slice/kubepods-burstable.slice/kubepods-burstable-pod19ccacf5_fcff_4bd3_a33a_19ff41f6ccd2.slice/crio-b4873629b312dc1d77472aba6fb177c6ce9a8f7c205ad7a03302726805007fe6.scope /sys/fs/cgroup/freezer ro,nosuid,nodev,noexec,relatime master:17 - cgroup cgroup rw,seclabel,freezer +6005 5995 0:38 /kubepods.slice/kubepods-burstable.slice/kubepods-burstable-pod19ccacf5_fcff_4bd3_a33a_19ff41f6ccd2.slice/crio-b4873629b312dc1d77472aba6fb177c6ce9a8f7c205ad7a03302726805007fe6.scope /sys/fs/cgroup/perf_event ro,nosuid,nodev,noexec,relatime master:18 - cgroup cgroup rw,seclabel,perf_event +6006 5995 0:39 /kubepods.slice/kubepods-burstable.slice/kubepods-burstable-pod19ccacf5_fcff_4bd3_a33a_19ff41f6ccd2.slice/crio-b4873629b312dc1d77472aba6fb177c6ce9a8f7c205ad7a03302726805007fe6.scope /sys/fs/cgroup/cpu,cpuacct ro,nosuid,nodev,noexec,relatime master:19 - cgroup cgroup rw,seclabel,cpu,cpuacct +6007 5995 0:40 /kubepods.slice/kubepods-burstable.slice/kubepods-burstable-pod19ccacf5_fcff_4bd3_a33a_19ff41f6ccd2.slice/crio-b4873629b312dc1d77472aba6fb177c6ce9a8f7c205ad7a03302726805007fe6.scope /sys/fs/cgroup/devices ro,nosuid,nodev,noexec,relatime master:20 - cgroup cgroup rw,seclabel,devices +6008 5995 0:41 /kubepods.slice/kubepods-burstable.slice/kubepods-burstable-pod19ccacf5_fcff_4bd3_a33a_19ff41f6ccd2.slice/crio-b4873629b312dc1d77472aba6fb177c6ce9a8f7c205ad7a03302726805007fe6.scope /sys/fs/cgroup/misc ro,nosuid,nodev,noexec,relatime master:21 - cgroup cgroup rw,seclabel,misc +6009 5991 0:315 / /dev/shm rw,nosuid,nodev,noexec,relatime master:766 - tmpfs shm rw,context="system_u:object_r:container_file_t:s0:c898,c999",size=65536k,inode64 +6010 5989 0:25 /containers/storage/overlay-containers/eff9d85aabab6d1fd3a244b1f22c6545132ef459ee6e4ead13f7abc2c963e6dc/userdata/resolv.conf /etc/resolv.conf rw,nosuid,nodev,noexec master:28 - tmpfs tmpfs rw,seclabel,size=6418660k,nr_inodes=819200,mode=755,inode64 +6011 5989 0:25 /containers/storage/overlay-containers/eff9d85aabab6d1fd3a244b1f22c6545132ef459ee6e4ead13f7abc2c963e6dc/userdata/hostname /etc/hostname rw,nosuid,nodev master:28 - tmpfs tmpfs rw,seclabel,size=6418660k,nr_inodes=819200,mode=755,inode64 +6012 5989 0:25 /containers/storage/overlay-containers/eff9d85aabab6d1fd3a244b1f22c6545132ef459ee6e4ead13f7abc2c963e6dc/userdata/.containerenv /run/.containerenv rw,nosuid,nodev master:28 - tmpfs tmpfs rw,seclabel,size=6418660k,nr_inodes=819200,mode=755,inode64 +6013 5989 252:4 /ostree/deploy/rhcos/var/lib/kubelet/pods/19ccacf5-fcff-4bd3-a33a-19ff41f6ccd2/etc-hosts /etc/hosts rw,relatime - xfs /dev/vda4 rw,seclabel,attr2,inode64,logbufs=8,logbsize=32k,prjquota +6014 5991 252:4 /ostree/deploy/rhcos/var/lib/kubelet/pods/19ccacf5-fcff-4bd3-a33a-19ff41f6ccd2/containers/accountingservice/640fe7ac /dev/termination-log rw,relatime - xfs /dev/vda4 rw,seclabel,attr2,inode64,logbufs=8,logbsize=32k,prjquota +6015 5989 0:307 / /run/secrets/kubernetes.io/serviceaccount ro,relatime - tmpfs tmpfs rw,seclabel,size=30942320k,inode64 +5587 5990 0:437 /bus /proc/bus ro,nosuid,nodev,noexec,relatime - proc proc rw +5588 5990 0:437 /fs /proc/fs ro,nosuid,nodev,noexec,relatime - proc proc rw +5590 5990 0:437 /irq /proc/irq ro,nosuid,nodev,noexec,relatime - proc proc rw +5591 5990 0:437 /sys /proc/sys ro,nosuid,nodev,noexec,relatime - proc proc rw +5592 5990 0:437 /sysrq-trigger /proc/sysrq-trigger ro,nosuid,nodev,noexec,relatime - proc proc rw +5593 5990 0:442 / /proc/acpi ro,relatime - tmpfs tmpfs ro,context="system_u:object_r:container_file_t:s0:c898,c999",inode64 +5594 5990 0:438 /null /proc/kcore rw,nosuid - tmpfs tmpfs rw,context="system_u:object_r:container_file_t:s0:c898,c999",size=65536k,mode=755,inode64 +5595 5990 0:438 /null /proc/keys rw,nosuid - tmpfs tmpfs rw,context="system_u:object_r:container_file_t:s0:c898,c999",size=65536k,mode=755,inode64 +5596 5990 0:438 /null /proc/timer_list rw,nosuid - tmpfs tmpfs rw,context="system_u:object_r:container_file_t:s0:c898,c999",size=65536k,mode=755,inode64 +5597 5990 0:443 / /proc/scsi ro,relatime - tmpfs tmpfs ro,context="system_u:object_r:container_file_t:s0:c898,c999",inode64 +5598 5994 0:444 / /sys/firmware ro,relatime - tmpfs tmpfs ro,context="system_u:object_r:container_file_t:s0:c898,c999",inode64 diff --git a/instrumentation/resources/library/src/test/resources/docker_proc_self_mountinfo b/instrumentation/resources/library/src/test/resources/docker_proc_self_mountinfo index d707c05184d9..28af893acf71 100644 --- a/instrumentation/resources/library/src/test/resources/docker_proc_self_mountinfo +++ b/instrumentation/resources/library/src/test/resources/docker_proc_self_mountinfo @@ -1,5 +1,3 @@ -473 456 254:1 /docker/containers/be522444b60caf2d3934b8b24b916a8a314f4b68d4595aa419874657e8d103f2/hostname /etc/hostname rw,relatime - ext4 /dev/vda1 rw -root@be522444b60c:/# cat /proc/self/mountinfo 456 375 0:143 / / rw,relatime master:175 - overlay overlay rw,lowerdir=/var/lib/docker/overlay2/l/CBPR2ETR4Z3UMOOGIIRDVT2P27:/var/lib/docker/overlay2/l/46FCA2JFPCSNFGAR5TSYLLNHLK,upperdir=/var/lib/docker/overlay2/3ef3e5a1a87b4e220c1da9a7901654e945b0ef5398e1b67fccb42fdb7750829e/diff,workdir=/var/lib/docker/overlay2/3ef3e5a1a87b4e220c1da9a7901654e945b0ef5398e1b67fccb42fdb7750829e/work 457 456 0:146 / /proc rw,nosuid,nodev,noexec,relatime - proc proc rw 466 456 0:147 / /dev rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755 diff --git a/instrumentation/resources/library/src/test/resources/docker_proc_self_mountinfo1 b/instrumentation/resources/library/src/test/resources/docker_proc_self_mountinfo1 new file mode 100644 index 000000000000..b63fbb3228ff --- /dev/null +++ b/instrumentation/resources/library/src/test/resources/docker_proc_self_mountinfo1 @@ -0,0 +1,36 @@ +1239 872 0:60 / / rw,relatime master:451 - overlay overlay rw,lowerdir=/var/lib/docker/overlay2/l/BWXJHJ2QLSLHKSPZDSYAMJ7H5E:/var/lib/docker/overlay2/l/S4UZYKEZFLMKROMNZL5VDZ3KDK:/var/lib/docker/overlay2/l/TIRNJBVGFYKRQYZWNHHJBNJMRQ,upperdir=/var/lib/docker/overlay2/b641d6be86a232bd770e75f480444a3e1c7e61a8ff8a4ed89198a4838e86916b/diff,workdir=/var/lib/docker/overlay2/b641d6be86a232bd770e75f480444a3e1c7e61a8ff8a4ed89198a4838e86916b/work,xino=off +1271 1239 0:62 / /proc rw,nosuid,nodev,noexec,relatime - proc proc rw +1272 1239 0:63 / /dev rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755 +1319 1272 0:64 / /dev/pts rw,nosuid,noexec,relatime - devpts devpts rw,gid=5,mode=620,ptmxmode=666 +1320 1239 0:65 / /sys ro,nosuid,nodev,noexec,relatime - sysfs sysfs ro +1507 1320 0:66 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755 +1508 1507 0:32 /docker/188329f95b930c32eeeffd34658ed2538960947e166743fa3743f5ce3d739b40 /sys/fs/cgroup/systemd ro,nosuid,nodev,noexec,relatime master:11 - cgroup cgroup rw,xattr,name=systemd +1509 1507 0:35 /docker/188329f95b930c32eeeffd34658ed2538960947e166743fa3743f5ce3d739b40 /sys/fs/cgroup/cpuset ro,nosuid,nodev,noexec,relatime master:15 - cgroup cgroup rw,cpuset +1510 1507 0:36 /docker/188329f95b930c32eeeffd34658ed2538960947e166743fa3743f5ce3d739b40 /sys/fs/cgroup/pids ro,nosuid,nodev,noexec,relatime master:16 - cgroup cgroup rw,pids +1511 1507 0:37 /docker/188329f95b930c32eeeffd34658ed2538960947e166743fa3743f5ce3d739b40 /sys/fs/cgroup/net_cls,net_prio ro,nosuid,nodev,noexec,relatime master:17 - cgroup cgroup rw,net_cls,net_prio +1512 1507 0:38 /docker/188329f95b930c32eeeffd34658ed2538960947e166743fa3743f5ce3d739b40 /sys/fs/cgroup/freezer ro,nosuid,nodev,noexec,relatime master:18 - cgroup cgroup rw,freezer +1513 1507 0:39 /docker/188329f95b930c32eeeffd34658ed2538960947e166743fa3743f5ce3d739b40 /sys/fs/cgroup/cpu,cpuacct ro,nosuid,nodev,noexec,relatime master:19 - cgroup cgroup rw,cpu,cpuacct +1514 1507 0:40 /docker/188329f95b930c32eeeffd34658ed2538960947e166743fa3743f5ce3d739b40 /sys/fs/cgroup/rdma ro,nosuid,nodev,noexec,relatime master:20 - cgroup cgroup rw,rdma +1515 1507 0:41 /docker/188329f95b930c32eeeffd34658ed2538960947e166743fa3743f5ce3d739b40 /sys/fs/cgroup/devices ro,nosuid,nodev,noexec,relatime master:21 - cgroup cgroup rw,devices +1516 1507 0:42 /docker/188329f95b930c32eeeffd34658ed2538960947e166743fa3743f5ce3d739b40 /sys/fs/cgroup/perf_event ro,nosuid,nodev,noexec,relatime master:22 - cgroup cgroup rw,perf_event +1517 1507 0:43 /docker/188329f95b930c32eeeffd34658ed2538960947e166743fa3743f5ce3d739b40 /sys/fs/cgroup/hugetlb ro,nosuid,nodev,noexec,relatime master:23 - cgroup cgroup rw,hugetlb +1518 1507 0:44 /docker/188329f95b930c32eeeffd34658ed2538960947e166743fa3743f5ce3d739b40 /sys/fs/cgroup/blkio ro,nosuid,nodev,noexec,relatime master:24 - cgroup cgroup rw,blkio +1519 1507 0:45 /docker/188329f95b930c32eeeffd34658ed2538960947e166743fa3743f5ce3d739b40 /sys/fs/cgroup/memory ro,nosuid,nodev,noexec,relatime master:25 - cgroup cgroup rw,memory +1520 1272 0:61 / /dev/mqueue rw,nosuid,nodev,noexec,relatime - mqueue mqueue rw +1521 1272 0:67 / /dev/shm rw,nosuid,nodev,noexec,relatime - tmpfs shm rw,size=65536k +1522 1239 253:1 /var/lib/docker/containers/188329f95b930c32eeeffd34658ed2538960947e166743fa3743f5ce3d739b40/resolv.conf /etc/resolv.conf rw,relatime - xfs /dev/mapper/ubuntu--vg-root rw,attr2,inode64,logbufs=8,logbsize=32k,noquota +1523 1239 253:1 /var/lib/docker/containers/188329f95b930c32eeeffd34658ed2538960947e166743fa3743f5ce3d739b40/hostname /etc/hostname rw,relatime - xfs /dev/mapper/ubuntu--vg-root rw,attr2,inode64,logbufs=8,logbsize=32k,noquota +1524 1239 253:1 /var/lib/docker/containers/188329f95b930c32eeeffd34658ed2538960947e166743fa3743f5ce3d739b40/hosts /etc/hosts rw,relatime - xfs /dev/mapper/ubuntu--vg-root rw,attr2,inode64,logbufs=8,logbsize=32k,noquota +873 1271 0:62 /bus /proc/bus ro,nosuid,nodev,noexec,relatime - proc proc rw +874 1271 0:62 /fs /proc/fs ro,nosuid,nodev,noexec,relatime - proc proc rw +875 1271 0:62 /irq /proc/irq ro,nosuid,nodev,noexec,relatime - proc proc rw +876 1271 0:62 /sys /proc/sys ro,nosuid,nodev,noexec,relatime - proc proc rw +877 1271 0:62 /sysrq-trigger /proc/sysrq-trigger ro,nosuid,nodev,noexec,relatime - proc proc rw +878 1271 0:68 / /proc/asound ro,relatime - tmpfs tmpfs ro +879 1271 0:69 / /proc/acpi ro,relatime - tmpfs tmpfs ro +880 1271 0:63 /null /proc/kcore rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755 +881 1271 0:63 /null /proc/keys rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755 +882 1271 0:63 /null /proc/timer_list rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755 +883 1271 0:63 /null /proc/sched_debug rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755 +884 1271 0:70 / /proc/scsi ro,relatime - tmpfs tmpfs ro +885 1320 0:71 / /sys/firmware ro,relatime - tmpfs tmpfs ro diff --git a/instrumentation/resources/library/src/test/resources/empty-MANIFEST.MF b/instrumentation/resources/library/src/test/resources/empty-MANIFEST.MF new file mode 100644 index 000000000000..9d885be53412 --- /dev/null +++ b/instrumentation/resources/library/src/test/resources/empty-MANIFEST.MF @@ -0,0 +1 @@ +Manifest-Version: 1.0 diff --git a/instrumentation/resources/library/src/test/resources/podman_proc_self_mountinfo b/instrumentation/resources/library/src/test/resources/podman_proc_self_mountinfo index 5d67bcdd1a60..6fb20214fc5d 100644 --- a/instrumentation/resources/library/src/test/resources/podman_proc_self_mountinfo +++ b/instrumentation/resources/library/src/test/resources/podman_proc_self_mountinfo @@ -1,5 +1,3 @@ -983 961 0:56 /containers/overlay-containers/2a33efc76e519c137fe6093179653788bed6162d4a15e5131c8e835c968afbe6/userdata/hostname /etc/hostname ro,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,size=783888k,nr_inodes=195972,mode=700,uid=2024,gid=2024,inode64 -[root@2a33efc76e51 /]# cat /proc/self/mountinfo 961 812 0:58 / / ro,relatime - overlay overlay rw,lowerdir=/home/dracula/.local/share/containers/storage/overlay/l/4NB35A5Z4YGWDHXYEUZU4FN6BU,upperdir=/home/dracula/.local/share/containers/storage/overlay/a73044caca1b918335d1db6f0052d21d35045136f3aa86976dbad1ec96e2fdde/diff,workdir=/home/dracula/.local/share/containers/storage/overlay/a73044caca1b918335d1db6f0052d21d35045136f3aa86976dbad1ec96e2fdde/work,userxattr 962 961 0:63 / /sys ro,nosuid,nodev,noexec,relatime - sysfs sysfs rw 963 961 0:64 / /run rw,nosuid,nodev,relatime - tmpfs tmpfs rw,uid=2024,gid=2024,inode64 diff --git a/instrumentation/restlet/restlet-1.1/javaagent/build.gradle.kts b/instrumentation/restlet/restlet-1.1/javaagent/build.gradle.kts index 47437565ccaa..b2ed4f14ed41 100644 --- a/instrumentation/restlet/restlet-1.1/javaagent/build.gradle.kts +++ b/instrumentation/restlet/restlet-1.1/javaagent/build.gradle.kts @@ -31,3 +31,7 @@ dependencies { testInstrumentation(project(":instrumentation:servlet:servlet-3.0:javaagent")) testInstrumentation(project(":instrumentation:servlet:servlet-javax-common:javaagent")) } + +tasks.withType().configureEach { + jvmArgs("-Dotel.instrumentation.common.experimental.controller-telemetry.enabled=true") +} diff --git a/instrumentation/restlet/restlet-1.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/restlet/v1_1/RestletSingletons.java b/instrumentation/restlet/restlet-1.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/restlet/v1_1/RestletSingletons.java index 76ae94863e9d..0e2332b1e984 100644 --- a/instrumentation/restlet/restlet-1.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/restlet/v1_1/RestletSingletons.java +++ b/instrumentation/restlet/restlet-1.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/restlet/v1_1/RestletSingletons.java @@ -7,9 +7,9 @@ import io.opentelemetry.api.GlobalOpenTelemetry; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteGetter; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRouteGetter; import io.opentelemetry.instrumentation.restlet.v1_1.RestletTelemetry; -import io.opentelemetry.javaagent.bootstrap.internal.CommonConfig; +import io.opentelemetry.javaagent.bootstrap.internal.AgentCommonConfig; import io.opentelemetry.javaagent.bootstrap.servlet.ServletContextPath; import org.restlet.data.Request; import org.restlet.data.Response; @@ -18,8 +18,9 @@ public final class RestletSingletons { private static final Instrumenter INSTRUMENTER = RestletTelemetry.builder(GlobalOpenTelemetry.get()) - .setCapturedRequestHeaders(CommonConfig.get().getServerRequestHeaders()) - .setCapturedResponseHeaders(CommonConfig.get().getServerResponseHeaders()) + .setCapturedRequestHeaders(AgentCommonConfig.get().getServerRequestHeaders()) + .setCapturedResponseHeaders(AgentCommonConfig.get().getServerResponseHeaders()) + .setKnownMethods(AgentCommonConfig.get().getKnownHttpRequestMethods()) .build() .getServerInstrumenter(); @@ -27,7 +28,7 @@ public static Instrumenter instrumenter() { return INSTRUMENTER; } - public static HttpRouteGetter serverSpanName() { + public static HttpServerRouteGetter serverSpanName() { return ServletContextPath::prepend; } diff --git a/instrumentation/restlet/restlet-1.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/restlet/v1_1/RouteInstrumentation.java b/instrumentation/restlet/restlet-1.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/restlet/v1_1/RouteInstrumentation.java index ebd1c7f55fe1..31ffdf950b81 100644 --- a/instrumentation/restlet/restlet-1.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/restlet/v1_1/RouteInstrumentation.java +++ b/instrumentation/restlet/restlet-1.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/restlet/v1_1/RouteInstrumentation.java @@ -5,14 +5,14 @@ package io.opentelemetry.javaagent.instrumentation.restlet.v1_1; -import static io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteSource.CONTROLLER; +import static io.opentelemetry.instrumentation.api.semconv.http.HttpServerRouteSource.CONTROLLER; import static io.opentelemetry.javaagent.bootstrap.Java8BytecodeBridge.currentContext; import static io.opentelemetry.javaagent.instrumentation.restlet.v1_1.RestletSingletons.serverSpanName; import static net.bytebuddy.matcher.ElementMatchers.isMethod; import static net.bytebuddy.matcher.ElementMatchers.named; import static net.bytebuddy.matcher.ElementMatchers.takesArgument; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteHolder; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRoute; import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; import net.bytebuddy.asm.Advice; @@ -44,7 +44,7 @@ public static class RouteBeforeHandleAdvice { public static void getRouteInfo(@Advice.This Route route, @Advice.Argument(0) Request request) { String pattern = route.getTemplate().getPattern(); - HttpRouteHolder.updateHttpRoute(currentContext(), CONTROLLER, serverSpanName(), pattern); + HttpServerRoute.update(currentContext(), CONTROLLER, serverSpanName(), pattern); } } } diff --git a/instrumentation/restlet/restlet-1.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/restlet/v1_1/ServerInstrumentation.java b/instrumentation/restlet/restlet-1.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/restlet/v1_1/ServerInstrumentation.java index b62a490ecbd9..1fe56462efba 100644 --- a/instrumentation/restlet/restlet-1.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/restlet/v1_1/ServerInstrumentation.java +++ b/instrumentation/restlet/restlet-1.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/restlet/v1_1/ServerInstrumentation.java @@ -5,7 +5,7 @@ package io.opentelemetry.javaagent.instrumentation.restlet.v1_1; -import static io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteSource.CONTROLLER; +import static io.opentelemetry.instrumentation.api.semconv.http.HttpServerRouteSource.CONTROLLER; import static io.opentelemetry.javaagent.bootstrap.Java8BytecodeBridge.currentContext; import static io.opentelemetry.javaagent.instrumentation.restlet.v1_1.RestletSingletons.instrumenter; import static io.opentelemetry.javaagent.instrumentation.restlet.v1_1.RestletSingletons.serverSpanName; @@ -15,7 +15,7 @@ import io.opentelemetry.context.Context; import io.opentelemetry.context.Scope; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteHolder; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRoute; import io.opentelemetry.javaagent.bootstrap.http.HttpServerResponseCustomizerHolder; import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; @@ -77,7 +77,7 @@ public static void finishRequest( scope.close(); if (Status.CLIENT_ERROR_NOT_FOUND.equals(response.getStatus())) { - HttpRouteHolder.updateHttpRoute(context, CONTROLLER, serverSpanName(), "/*"); + HttpServerRoute.update(context, CONTROLLER, serverSpanName(), "/*"); } HttpServerResponseCustomizerHolder.getCustomizer() diff --git a/instrumentation/restlet/restlet-1.1/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/restlet/v1_1/RestletServerTest.groovy b/instrumentation/restlet/restlet-1.1/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/restlet/v1_1/RestletServerTest.groovy index d6d738354297..76aff28f054b 100644 --- a/instrumentation/restlet/restlet-1.1/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/restlet/v1_1/RestletServerTest.groovy +++ b/instrumentation/restlet/restlet-1.1/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/restlet/v1_1/RestletServerTest.groovy @@ -14,12 +14,12 @@ import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint class RestletServerTest extends AbstractRestletServerTest implements AgentTestTrait { @Override - String expectedHttpRoute(ServerEndpoint endpoint) { + String expectedHttpRoute(ServerEndpoint endpoint, String method) { switch (endpoint) { case NOT_FOUND: return getContextPath() + "/" default: - return super.expectedHttpRoute(endpoint) + return super.expectedHttpRoute(endpoint, method) } } diff --git a/instrumentation/restlet/restlet-1.1/library/build.gradle.kts b/instrumentation/restlet/restlet-1.1/library/build.gradle.kts index 7a6ba61f4994..71bb895c7da6 100644 --- a/instrumentation/restlet/restlet-1.1/library/build.gradle.kts +++ b/instrumentation/restlet/restlet-1.1/library/build.gradle.kts @@ -15,3 +15,7 @@ dependencies { testImplementation(project(":instrumentation:restlet:restlet-1.1:testing")) } + +tasks.withType().configureEach { + jvmArgs("-Dotel.instrumentation.common.experimental.controller-telemetry.enabled=true") +} diff --git a/instrumentation/restlet/restlet-1.1/library/src/main/java/io/opentelemetry/instrumentation/restlet/v1_1/RestletHttpAttributesGetter.java b/instrumentation/restlet/restlet-1.1/library/src/main/java/io/opentelemetry/instrumentation/restlet/v1_1/RestletHttpAttributesGetter.java index aee20cf4bfb0..8378b494045e 100644 --- a/instrumentation/restlet/restlet-1.1/library/src/main/java/io/opentelemetry/instrumentation/restlet/v1_1/RestletHttpAttributesGetter.java +++ b/instrumentation/restlet/restlet-1.1/library/src/main/java/io/opentelemetry/instrumentation/restlet/v1_1/RestletHttpAttributesGetter.java @@ -7,7 +7,9 @@ import static io.opentelemetry.instrumentation.restlet.v1_1.RestletHeadersGetter.getHeaders; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpServerAttributesGetter; +import com.noelios.restlet.http.HttpCall; +import com.noelios.restlet.http.HttpRequest; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerAttributesGetter; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -79,4 +81,54 @@ private static List parametersToList(Series headers) { } return stringHeaders; } + + @Nullable + @Override + public String getNetworkProtocolName(Request request, @Nullable Response response) { + String protocol = getProtocolString(request); + if (protocol.startsWith("HTTP/")) { + return "http"; + } + return null; + } + + @Nullable + @Override + public String getNetworkProtocolVersion(Request request, @Nullable Response response) { + String protocol = getProtocolString(request); + if (protocol.startsWith("HTTP/")) { + return protocol.substring("HTTP/".length()); + } + return null; + } + + private static String getProtocolString(Request request) { + return (String) request.getAttributes().get("org.restlet.http.version"); + } + + @Override + @Nullable + public String getNetworkPeerAddress(Request request, @Nullable Response response) { + return request.getClientInfo().getAddress(); + } + + @Override + public Integer getNetworkPeerPort(Request request, @Nullable Response response) { + return request.getClientInfo().getPort(); + } + + @Nullable + @Override + public String getNetworkLocalAddress(Request request, @Nullable Response response) { + HttpCall call = httpCall(request); + return call == null ? null : call.getServerAddress(); + } + + @Nullable + private static HttpCall httpCall(Request request) { + if (request instanceof HttpRequest) { + return ((HttpRequest) request).getHttpCall(); + } + return null; + } } diff --git a/instrumentation/restlet/restlet-1.1/library/src/main/java/io/opentelemetry/instrumentation/restlet/v1_1/RestletNetAttributesGetter.java b/instrumentation/restlet/restlet-1.1/library/src/main/java/io/opentelemetry/instrumentation/restlet/v1_1/RestletNetAttributesGetter.java deleted file mode 100644 index 65777fae1d1e..000000000000 --- a/instrumentation/restlet/restlet-1.1/library/src/main/java/io/opentelemetry/instrumentation/restlet/v1_1/RestletNetAttributesGetter.java +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.restlet.v1_1; - -import com.noelios.restlet.http.HttpCall; -import com.noelios.restlet.http.HttpRequest; -import io.opentelemetry.instrumentation.api.instrumenter.net.NetServerAttributesGetter; -import javax.annotation.Nullable; -import org.restlet.data.Request; -import org.restlet.data.Response; - -final class RestletNetAttributesGetter implements NetServerAttributesGetter { - - @Nullable - @Override - public String getNetworkProtocolName(Request request, @Nullable Response response) { - String protocol = getProtocolString(request); - if (protocol.startsWith("HTTP/")) { - return "http"; - } - return null; - } - - @Nullable - @Override - public String getNetworkProtocolVersion(Request request, @Nullable Response response) { - String protocol = getProtocolString(request); - if (protocol.startsWith("HTTP/")) { - return protocol.substring("HTTP/".length()); - } - return null; - } - - private static String getProtocolString(Request request) { - return (String) request.getAttributes().get("org.restlet.http.version"); - } - - @Nullable - @Override - public String getServerAddress(Request request) { - HttpCall call = httpCall(request); - return call == null ? null : call.getHostDomain(); - } - - @Nullable - @Override - public Integer getServerPort(Request request) { - HttpCall call = httpCall(request); - return call == null ? null : call.getServerPort(); - } - - @Override - @Nullable - public String getClientSocketAddress(Request request, @Nullable Response response) { - return request.getClientInfo().getAddress(); - } - - @Override - public Integer getClientSocketPort(Request request, @Nullable Response response) { - return request.getClientInfo().getPort(); - } - - @Nullable - @Override - public String getServerSocketAddress(Request request, @Nullable Response response) { - HttpCall call = httpCall(request); - return call == null ? null : call.getServerAddress(); - } - - @Nullable - private static HttpCall httpCall(Request request) { - if (request instanceof HttpRequest) { - return ((HttpRequest) request).getHttpCall(); - } - return null; - } -} diff --git a/instrumentation/restlet/restlet-1.1/library/src/main/java/io/opentelemetry/instrumentation/restlet/v1_1/RestletTelemetryBuilder.java b/instrumentation/restlet/restlet-1.1/library/src/main/java/io/opentelemetry/instrumentation/restlet/v1_1/RestletTelemetryBuilder.java index 45f34ac8de23..d5711895e9d5 100644 --- a/instrumentation/restlet/restlet-1.1/library/src/main/java/io/opentelemetry/instrumentation/restlet/v1_1/RestletTelemetryBuilder.java +++ b/instrumentation/restlet/restlet-1.1/library/src/main/java/io/opentelemetry/instrumentation/restlet/v1_1/RestletTelemetryBuilder.java @@ -7,16 +7,24 @@ import com.google.errorprone.annotations.CanIgnoreReturnValue; import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.instrumentation.api.incubator.semconv.http.HttpExperimentalAttributesExtractor; +import io.opentelemetry.instrumentation.api.incubator.semconv.http.HttpServerExperimentalMetrics; import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteHolder; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpServerAttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpServerAttributesExtractorBuilder; -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 io.opentelemetry.instrumentation.api.instrumenter.InstrumenterBuilder; +import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerAttributesExtractor; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerAttributesExtractorBuilder; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerMetrics; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRoute; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRouteBuilder; +import io.opentelemetry.instrumentation.api.semconv.http.HttpSpanNameExtractor; +import io.opentelemetry.instrumentation.api.semconv.http.HttpSpanNameExtractorBuilder; +import io.opentelemetry.instrumentation.api.semconv.http.HttpSpanStatusExtractor; import java.util.ArrayList; import java.util.List; +import java.util.Set; +import java.util.function.Function; import org.restlet.data.Request; import org.restlet.data.Response; @@ -30,8 +38,14 @@ public final class RestletTelemetryBuilder { new ArrayList<>(); private final HttpServerAttributesExtractorBuilder httpAttributesExtractorBuilder = - HttpServerAttributesExtractor.builder( - RestletHttpAttributesGetter.INSTANCE, new RestletNetAttributesGetter()); + HttpServerAttributesExtractor.builder(RestletHttpAttributesGetter.INSTANCE); + private final HttpSpanNameExtractorBuilder httpSpanNameExtractorBuilder = + HttpSpanNameExtractor.builder(RestletHttpAttributesGetter.INSTANCE); + private Function, ? extends SpanNameExtractor> + spanNameExtractorTransformer = Function.identity(); + private final HttpServerRouteBuilder httpServerRouteBuilder = + HttpServerRoute.builder(RestletHttpAttributesGetter.INSTANCE); + private boolean emitExperimentalHttpServerMetrics = false; RestletTelemetryBuilder(OpenTelemetry openTelemetry) { this.openTelemetry = openTelemetry; @@ -70,25 +84,72 @@ public RestletTelemetryBuilder setCapturedResponseHeaders(List responseH return this; } + /** + * Configures the instrumentation to recognize an alternative set of HTTP request methods. + * + *

    By default, this instrumentation defines "known" methods as the ones listed in RFC9110 and the PATCH + * method defined in RFC5789. + * + *

    Note: calling this method overrides the default known method sets completely; it does + * not supplement it. + * + * @param knownMethods A set of recognized HTTP request methods. + * @see HttpServerAttributesExtractorBuilder#setKnownMethods(Set) + */ + @CanIgnoreReturnValue + public RestletTelemetryBuilder setKnownMethods(Set knownMethods) { + httpAttributesExtractorBuilder.setKnownMethods(knownMethods); + httpSpanNameExtractorBuilder.setKnownMethods(knownMethods); + httpServerRouteBuilder.setKnownMethods(knownMethods); + return this; + } + + /** + * Configures the instrumentation to emit experimental HTTP server metrics. + * + * @param emitExperimentalHttpServerMetrics {@code true} if the experimental HTTP server metrics + * are to be emitted. + */ + @CanIgnoreReturnValue + public RestletTelemetryBuilder setEmitExperimentalHttpServerMetrics( + boolean emitExperimentalHttpServerMetrics) { + this.emitExperimentalHttpServerMetrics = emitExperimentalHttpServerMetrics; + return this; + } + + /** Sets custom {@link SpanNameExtractor} via transform function. */ + @CanIgnoreReturnValue + public RestletTelemetryBuilder setSpanNameExtractor( + Function, ? extends SpanNameExtractor> + spanNameExtractorTransformer) { + this.spanNameExtractorTransformer = spanNameExtractorTransformer; + return this; + } + /** * Returns a new {@link RestletTelemetry} with the settings of this {@link * RestletTelemetryBuilder}. */ public RestletTelemetry build() { RestletHttpAttributesGetter httpAttributesGetter = RestletHttpAttributesGetter.INSTANCE; + SpanNameExtractor spanNameExtractor = + spanNameExtractorTransformer.apply(httpSpanNameExtractorBuilder.build()); - Instrumenter instrumenter = + InstrumenterBuilder builder = Instrumenter.builder( - openTelemetry, - INSTRUMENTATION_NAME, - HttpSpanNameExtractor.create(httpAttributesGetter)) + openTelemetry, INSTRUMENTATION_NAME, spanNameExtractor) .setSpanStatusExtractor(HttpSpanStatusExtractor.create(httpAttributesGetter)) .addAttributesExtractor(httpAttributesExtractorBuilder.build()) .addAttributesExtractors(additionalExtractors) - .addOperationMetrics(HttpServerMetrics.get()) - .addContextCustomizer(HttpRouteHolder.create(httpAttributesGetter)) - .buildServerInstrumenter(RestletHeadersGetter.INSTANCE); + .addContextCustomizer(httpServerRouteBuilder.build()) + .addOperationMetrics(HttpServerMetrics.get()); + if (emitExperimentalHttpServerMetrics) { + builder + .addAttributesExtractor(HttpExperimentalAttributesExtractor.create(httpAttributesGetter)) + .addOperationMetrics(HttpServerExperimentalMetrics.get()); + } - return new RestletTelemetry(instrumenter); + return new RestletTelemetry(builder.buildServerInstrumenter(RestletHeadersGetter.INSTANCE)); } } diff --git a/instrumentation/restlet/restlet-1.1/library/src/main/java/io/opentelemetry/instrumentation/restlet/v1_1/TracingFilter.java b/instrumentation/restlet/restlet-1.1/library/src/main/java/io/opentelemetry/instrumentation/restlet/v1_1/TracingFilter.java index 1b13e5b206aa..376336b5d66a 100644 --- a/instrumentation/restlet/restlet-1.1/library/src/main/java/io/opentelemetry/instrumentation/restlet/v1_1/TracingFilter.java +++ b/instrumentation/restlet/restlet-1.1/library/src/main/java/io/opentelemetry/instrumentation/restlet/v1_1/TracingFilter.java @@ -5,12 +5,12 @@ package io.opentelemetry.instrumentation.restlet.v1_1; -import static io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteSource.CONTROLLER; +import static io.opentelemetry.instrumentation.api.semconv.http.HttpServerRouteSource.CONTROLLER; import io.opentelemetry.context.Context; import io.opentelemetry.context.Scope; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteHolder; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRoute; import org.restlet.Filter; import org.restlet.data.Request; import org.restlet.data.Response; @@ -38,7 +38,7 @@ public int doHandle(Request request, Response response) { scope = context.makeCurrent(); } - HttpRouteHolder.updateHttpRoute(context, CONTROLLER, (ctx, s) -> s, path); + HttpServerRoute.update(context, CONTROLLER, path); Throwable statusThrowable = null; try { diff --git a/instrumentation/restlet/restlet-1.1/testing/src/main/groovy/io/opentelemetry/instrumentation/restlet/v1_1/AbstractRestletServerTest.groovy b/instrumentation/restlet/restlet-1.1/testing/src/main/groovy/io/opentelemetry/instrumentation/restlet/v1_1/AbstractRestletServerTest.groovy index 158ecb7a4269..d92f0f07c6c3 100644 --- a/instrumentation/restlet/restlet-1.1/testing/src/main/groovy/io/opentelemetry/instrumentation/restlet/v1_1/AbstractRestletServerTest.groovy +++ b/instrumentation/restlet/restlet-1.1/testing/src/main/groovy/io/opentelemetry/instrumentation/restlet/v1_1/AbstractRestletServerTest.groovy @@ -5,7 +5,7 @@ package io.opentelemetry.instrumentation.restlet.v1_1 - +import io.opentelemetry.instrumentation.api.internal.HttpConstants import io.opentelemetry.instrumentation.test.base.HttpServerTest import io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint import org.restlet.Component @@ -168,14 +168,17 @@ abstract class AbstractRestletServerTest extends HttpServerTest { } @Override - String expectedHttpRoute(ServerEndpoint endpoint) { + String expectedHttpRoute(ServerEndpoint endpoint, String method) { + if (method == HttpConstants._OTHER) { + return getContextPath() + endpoint.path + } switch (endpoint) { case PATH_PARAM: return getContextPath() + "/path/{id}/param" case NOT_FOUND: return getContextPath() + "/*" default: - return super.expectedHttpRoute(endpoint) + return super.expectedHttpRoute(endpoint, method) } } diff --git a/instrumentation/restlet/restlet-1.1/testing/src/main/groovy/io/opentelemetry/instrumentation/restlet/v1_1/AbstractServletServerTest.groovy b/instrumentation/restlet/restlet-1.1/testing/src/main/groovy/io/opentelemetry/instrumentation/restlet/v1_1/AbstractServletServerTest.groovy index 3c764ae8e065..d6655ab173db 100644 --- a/instrumentation/restlet/restlet-1.1/testing/src/main/groovy/io/opentelemetry/instrumentation/restlet/v1_1/AbstractServletServerTest.groovy +++ b/instrumentation/restlet/restlet-1.1/testing/src/main/groovy/io/opentelemetry/instrumentation/restlet/v1_1/AbstractServletServerTest.groovy @@ -5,7 +5,7 @@ package io.opentelemetry.instrumentation.restlet.v1_1 - +import io.opentelemetry.instrumentation.api.internal.HttpConstants import io.opentelemetry.instrumentation.test.base.HttpServerTest import io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint import org.eclipse.jetty.server.Server @@ -63,17 +63,25 @@ abstract class AbstractServletServerTest extends HttpServerTest { } @Override - String expectedHttpRoute(ServerEndpoint endpoint) { + String expectedHttpRoute(ServerEndpoint endpoint, String method) { + if (method == HttpConstants._OTHER) { + return getContextPath() + endpoint.path + } switch (endpoint) { case PATH_PARAM: return getContextPath() + "/path/{id}/param" case NOT_FOUND: return getContextPath() + "/*" default: - return super.expectedHttpRoute(endpoint) + return super.expectedHttpRoute(endpoint, method) } } + @Override + int getResponseCodeOnNonStandardHttpMethod() { + 405 + } + static class TestApp extends Application { @Override diff --git a/instrumentation/restlet/restlet-1.1/testing/src/main/groovy/io/opentelemetry/instrumentation/restlet/v1_1/spring/AbstractSpringServerTest.groovy b/instrumentation/restlet/restlet-1.1/testing/src/main/groovy/io/opentelemetry/instrumentation/restlet/v1_1/spring/AbstractSpringServerTest.groovy index 0700158f93be..4c87a3a57cec 100644 --- a/instrumentation/restlet/restlet-1.1/testing/src/main/groovy/io/opentelemetry/instrumentation/restlet/v1_1/spring/AbstractSpringServerTest.groovy +++ b/instrumentation/restlet/restlet-1.1/testing/src/main/groovy/io/opentelemetry/instrumentation/restlet/v1_1/spring/AbstractSpringServerTest.groovy @@ -31,4 +31,8 @@ abstract class AbstractSpringServerTest extends AbstractRestletServerTest { host.attach(router) } + @Override + int getResponseCodeOnNonStandardHttpMethod() { + 405 + } } diff --git a/instrumentation/restlet/restlet-2.0/javaagent/build.gradle.kts b/instrumentation/restlet/restlet-2.0/javaagent/build.gradle.kts index c88a2e6cad51..d5be7a1c0de6 100644 --- a/instrumentation/restlet/restlet-2.0/javaagent/build.gradle.kts +++ b/instrumentation/restlet/restlet-2.0/javaagent/build.gradle.kts @@ -41,3 +41,7 @@ if (findProperty("testLatestDeps") as Boolean) { } } } + +tasks.withType().configureEach { + jvmArgs("-Dotel.instrumentation.common.experimental.controller-telemetry.enabled=true") +} diff --git a/instrumentation/restlet/restlet-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/restlet/v2_0/RestletSingletons.java b/instrumentation/restlet/restlet-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/restlet/v2_0/RestletSingletons.java index 1526fd2a5b2a..0b8f6607964c 100644 --- a/instrumentation/restlet/restlet-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/restlet/v2_0/RestletSingletons.java +++ b/instrumentation/restlet/restlet-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/restlet/v2_0/RestletSingletons.java @@ -7,12 +7,13 @@ import io.opentelemetry.api.GlobalOpenTelemetry; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteGetter; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpServerAttributesExtractor; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerAttributesExtractor; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRoute; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRouteGetter; +import io.opentelemetry.instrumentation.api.semconv.http.HttpSpanNameExtractor; import io.opentelemetry.instrumentation.restlet.v2_0.internal.RestletHttpAttributesGetter; import io.opentelemetry.instrumentation.restlet.v2_0.internal.RestletInstrumenterFactory; -import io.opentelemetry.instrumentation.restlet.v2_0.internal.RestletNetAttributesGetter; -import io.opentelemetry.javaagent.bootstrap.internal.CommonConfig; +import io.opentelemetry.javaagent.bootstrap.internal.AgentCommonConfig; import io.opentelemetry.javaagent.bootstrap.servlet.ServletContextPath; import java.util.Collections; import org.restlet.Request; @@ -23,18 +24,25 @@ public final class RestletSingletons { private static final Instrumenter INSTRUMENTER = RestletInstrumenterFactory.newServerInstrumenter( GlobalOpenTelemetry.get(), - HttpServerAttributesExtractor.builder( - RestletHttpAttributesGetter.INSTANCE, new RestletNetAttributesGetter()) - .setCapturedRequestHeaders(CommonConfig.get().getServerRequestHeaders()) - .setCapturedResponseHeaders(CommonConfig.get().getServerResponseHeaders()) + HttpServerAttributesExtractor.builder(RestletHttpAttributesGetter.INSTANCE) + .setCapturedRequestHeaders(AgentCommonConfig.get().getServerRequestHeaders()) + .setCapturedResponseHeaders(AgentCommonConfig.get().getServerResponseHeaders()) + .setKnownMethods(AgentCommonConfig.get().getKnownHttpRequestMethods()) .build(), - Collections.emptyList()); + HttpSpanNameExtractor.builder(RestletHttpAttributesGetter.INSTANCE) + .setKnownMethods(AgentCommonConfig.get().getKnownHttpRequestMethods()) + .build(), + HttpServerRoute.builder(RestletHttpAttributesGetter.INSTANCE) + .setKnownMethods(AgentCommonConfig.get().getKnownHttpRequestMethods()) + .build(), + Collections.emptyList(), + AgentCommonConfig.get().shouldEmitExperimentalHttpServerTelemetry()); public static Instrumenter instrumenter() { return INSTRUMENTER; } - public static HttpRouteGetter serverSpanName() { + public static HttpServerRouteGetter serverSpanName() { return ServletContextPath::prepend; } diff --git a/instrumentation/restlet/restlet-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/restlet/v2_0/RouteInstrumentation.java b/instrumentation/restlet/restlet-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/restlet/v2_0/RouteInstrumentation.java index 29b588710f9e..2d02c4b57331 100644 --- a/instrumentation/restlet/restlet-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/restlet/v2_0/RouteInstrumentation.java +++ b/instrumentation/restlet/restlet-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/restlet/v2_0/RouteInstrumentation.java @@ -5,7 +5,7 @@ package io.opentelemetry.javaagent.instrumentation.restlet.v2_0; -import static io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteSource.CONTROLLER; +import static io.opentelemetry.instrumentation.api.semconv.http.HttpServerRouteSource.CONTROLLER; import static io.opentelemetry.javaagent.bootstrap.Java8BytecodeBridge.currentContext; import static io.opentelemetry.javaagent.instrumentation.restlet.v2_0.RestletSingletons.serverSpanName; import static net.bytebuddy.matcher.ElementMatchers.isMethod; @@ -13,7 +13,7 @@ import static net.bytebuddy.matcher.ElementMatchers.namedOneOf; import static net.bytebuddy.matcher.ElementMatchers.takesArgument; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteHolder; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRoute; import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; import net.bytebuddy.asm.Advice; @@ -46,7 +46,7 @@ public static void getRouteInfo( @Advice.This TemplateRoute route, @Advice.Argument(0) Request request) { String pattern = route.getTemplate().getPattern(); - HttpRouteHolder.updateHttpRoute(currentContext(), CONTROLLER, serverSpanName(), pattern); + HttpServerRoute.update(currentContext(), CONTROLLER, serverSpanName(), pattern); } } } diff --git a/instrumentation/restlet/restlet-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/restlet/v2_0/ServerInstrumentation.java b/instrumentation/restlet/restlet-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/restlet/v2_0/ServerInstrumentation.java index 5c6023cb788e..09bfa1fbd7e4 100644 --- a/instrumentation/restlet/restlet-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/restlet/v2_0/ServerInstrumentation.java +++ b/instrumentation/restlet/restlet-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/restlet/v2_0/ServerInstrumentation.java @@ -5,7 +5,7 @@ package io.opentelemetry.javaagent.instrumentation.restlet.v2_0; -import static io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteSource.CONTROLLER; +import static io.opentelemetry.instrumentation.api.semconv.http.HttpServerRouteSource.CONTROLLER; import static io.opentelemetry.javaagent.bootstrap.Java8BytecodeBridge.currentContext; import static io.opentelemetry.javaagent.instrumentation.restlet.v2_0.RestletSingletons.instrumenter; import static io.opentelemetry.javaagent.instrumentation.restlet.v2_0.RestletSingletons.serverSpanName; @@ -15,7 +15,7 @@ import io.opentelemetry.context.Context; import io.opentelemetry.context.Scope; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteHolder; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRoute; import io.opentelemetry.javaagent.bootstrap.http.HttpServerResponseCustomizerHolder; import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; @@ -77,7 +77,7 @@ public static void finishRequest( scope.close(); if (Status.CLIENT_ERROR_NOT_FOUND.equals(response.getStatus())) { - HttpRouteHolder.updateHttpRoute(context, CONTROLLER, serverSpanName(), "/*"); + HttpServerRoute.update(context, CONTROLLER, serverSpanName(), "/*"); } HttpServerResponseCustomizerHolder.getCustomizer() diff --git a/instrumentation/restlet/restlet-2.0/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/restlet/v2_0/RestletServerTest.groovy b/instrumentation/restlet/restlet-2.0/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/restlet/v2_0/RestletServerTest.groovy index f3cefae657ab..3fe0f9a93c6f 100644 --- a/instrumentation/restlet/restlet-2.0/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/restlet/v2_0/RestletServerTest.groovy +++ b/instrumentation/restlet/restlet-2.0/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/restlet/v2_0/RestletServerTest.groovy @@ -14,12 +14,12 @@ import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint class RestletServerTest extends AbstractRestletServerTest implements AgentTestTrait { @Override - String expectedHttpRoute(ServerEndpoint endpoint) { + String expectedHttpRoute(ServerEndpoint endpoint, String method) { switch (endpoint) { case NOT_FOUND: return getContextPath() + "/" default: - return super.expectedHttpRoute(endpoint) + return super.expectedHttpRoute(endpoint, method) } } diff --git a/instrumentation/restlet/restlet-2.0/library/build.gradle.kts b/instrumentation/restlet/restlet-2.0/library/build.gradle.kts index 2c5dc577f6e8..01ec8f936bee 100644 --- a/instrumentation/restlet/restlet-2.0/library/build.gradle.kts +++ b/instrumentation/restlet/restlet-2.0/library/build.gradle.kts @@ -27,3 +27,7 @@ if (findProperty("testLatestDeps") as Boolean) { } } } + +tasks.withType().configureEach { + jvmArgs("-Dotel.instrumentation.common.experimental.controller-telemetry.enabled=true") +} diff --git a/instrumentation/restlet/restlet-2.0/library/src/main/java/io/opentelemetry/instrumentation/restlet/v2_0/RestletTelemetryBuilder.java b/instrumentation/restlet/restlet-2.0/library/src/main/java/io/opentelemetry/instrumentation/restlet/v2_0/RestletTelemetryBuilder.java index 86236231a2ac..86cc9544a729 100644 --- a/instrumentation/restlet/restlet-2.0/library/src/main/java/io/opentelemetry/instrumentation/restlet/v2_0/RestletTelemetryBuilder.java +++ b/instrumentation/restlet/restlet-2.0/library/src/main/java/io/opentelemetry/instrumentation/restlet/v2_0/RestletTelemetryBuilder.java @@ -9,13 +9,19 @@ 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.http.HttpServerAttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpServerAttributesExtractorBuilder; +import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerAttributesExtractor; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerAttributesExtractorBuilder; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRoute; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRouteBuilder; +import io.opentelemetry.instrumentation.api.semconv.http.HttpSpanNameExtractor; +import io.opentelemetry.instrumentation.api.semconv.http.HttpSpanNameExtractorBuilder; import io.opentelemetry.instrumentation.restlet.v2_0.internal.RestletHttpAttributesGetter; import io.opentelemetry.instrumentation.restlet.v2_0.internal.RestletInstrumenterFactory; -import io.opentelemetry.instrumentation.restlet.v2_0.internal.RestletNetAttributesGetter; import java.util.ArrayList; import java.util.List; +import java.util.Set; +import java.util.function.Function; import org.restlet.Request; import org.restlet.Response; @@ -27,8 +33,14 @@ public final class RestletTelemetryBuilder { new ArrayList<>(); private final HttpServerAttributesExtractorBuilder httpAttributesExtractorBuilder = - HttpServerAttributesExtractor.builder( - RestletHttpAttributesGetter.INSTANCE, new RestletNetAttributesGetter()); + HttpServerAttributesExtractor.builder(RestletHttpAttributesGetter.INSTANCE); + private final HttpSpanNameExtractorBuilder httpSpanNameExtractorBuilder = + HttpSpanNameExtractor.builder(RestletHttpAttributesGetter.INSTANCE); + private Function, ? extends SpanNameExtractor> + spanNameExtractorTransformer = Function.identity(); + private final HttpServerRouteBuilder httpServerRouteBuilder = + HttpServerRoute.builder(RestletHttpAttributesGetter.INSTANCE); + private boolean emitExperimentalHttpServerMetrics = false; RestletTelemetryBuilder(OpenTelemetry openTelemetry) { this.openTelemetry = openTelemetry; @@ -67,14 +79,65 @@ public RestletTelemetryBuilder setCapturedResponseHeaders(List responseH return this; } + /** + * Configures the instrumentation to recognize an alternative set of HTTP request methods. + * + *

    By default, this instrumentation defines "known" methods as the ones listed in RFC9110 and the PATCH + * method defined in RFC5789. + * + *

    Note: calling this method overrides the default known method sets completely; it does + * not supplement it. + * + * @param knownMethods A set of recognized HTTP request methods. + * @see HttpServerAttributesExtractorBuilder#setKnownMethods(Set) + */ + @CanIgnoreReturnValue + public RestletTelemetryBuilder setKnownMethods(Set knownMethods) { + httpAttributesExtractorBuilder.setKnownMethods(knownMethods); + httpSpanNameExtractorBuilder.setKnownMethods(knownMethods); + httpServerRouteBuilder.setKnownMethods(knownMethods); + return this; + } + + /** + * Configures the instrumentation to emit experimental HTTP server metrics. + * + * @param emitExperimentalHttpServerMetrics {@code true} if the experimental HTTP server metrics + * are to be emitted. + */ + @CanIgnoreReturnValue + public RestletTelemetryBuilder setEmitExperimentalHttpServerMetrics( + boolean emitExperimentalHttpServerMetrics) { + this.emitExperimentalHttpServerMetrics = emitExperimentalHttpServerMetrics; + return this; + } + + /** Sets custom {@link SpanNameExtractor} via transform function. */ + @CanIgnoreReturnValue + public RestletTelemetryBuilder setSpanNameExtractor( + Function, ? extends SpanNameExtractor> + spanNameExtractorTransformer) { + this.spanNameExtractorTransformer = spanNameExtractorTransformer; + return this; + } + /** * Returns a new {@link RestletTelemetry} with the settings of this {@link * RestletTelemetryBuilder}. */ public RestletTelemetry build() { + SpanNameExtractor spanNameExtractor = + spanNameExtractorTransformer.apply(httpSpanNameExtractorBuilder.build()); + Instrumenter serverInstrumenter = RestletInstrumenterFactory.newServerInstrumenter( - openTelemetry, httpAttributesExtractorBuilder.build(), additionalExtractors); + openTelemetry, + httpAttributesExtractorBuilder.build(), + spanNameExtractor, + httpServerRouteBuilder.build(), + additionalExtractors, + emitExperimentalHttpServerMetrics); return new RestletTelemetry(serverInstrumenter); } diff --git a/instrumentation/restlet/restlet-2.0/library/src/main/java/io/opentelemetry/instrumentation/restlet/v2_0/TracingFilter.java b/instrumentation/restlet/restlet-2.0/library/src/main/java/io/opentelemetry/instrumentation/restlet/v2_0/TracingFilter.java index 05d092361035..8de688556b93 100644 --- a/instrumentation/restlet/restlet-2.0/library/src/main/java/io/opentelemetry/instrumentation/restlet/v2_0/TracingFilter.java +++ b/instrumentation/restlet/restlet-2.0/library/src/main/java/io/opentelemetry/instrumentation/restlet/v2_0/TracingFilter.java @@ -5,12 +5,12 @@ package io.opentelemetry.instrumentation.restlet.v2_0; -import static io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteSource.CONTROLLER; +import static io.opentelemetry.instrumentation.api.semconv.http.HttpServerRouteSource.CONTROLLER; import io.opentelemetry.context.Context; import io.opentelemetry.context.Scope; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteHolder; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRoute; import org.restlet.Request; import org.restlet.Response; import org.restlet.resource.ResourceException; @@ -39,7 +39,7 @@ public int doHandle(Request request, Response response) { scope = context.makeCurrent(); } - HttpRouteHolder.updateHttpRoute(context, CONTROLLER, (ctx, s) -> s, path); + HttpServerRoute.update(context, CONTROLLER, path); Throwable statusThrowable = null; try { diff --git a/instrumentation/restlet/restlet-2.0/library/src/main/java/io/opentelemetry/instrumentation/restlet/v2_0/internal/RestletHttpAttributesGetter.java b/instrumentation/restlet/restlet-2.0/library/src/main/java/io/opentelemetry/instrumentation/restlet/v2_0/internal/RestletHttpAttributesGetter.java index 43daf7658c39..38c5a96c2ffc 100644 --- a/instrumentation/restlet/restlet-2.0/library/src/main/java/io/opentelemetry/instrumentation/restlet/v2_0/internal/RestletHttpAttributesGetter.java +++ b/instrumentation/restlet/restlet-2.0/library/src/main/java/io/opentelemetry/instrumentation/restlet/v2_0/internal/RestletHttpAttributesGetter.java @@ -7,7 +7,7 @@ import static io.opentelemetry.instrumentation.restlet.v2_0.internal.RestletHeadersGetter.getHeaders; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpServerAttributesGetter; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerAttributesGetter; import java.util.Arrays; import java.util.Collections; import java.util.List; @@ -69,4 +69,33 @@ public List getHttpResponseHeader(Request request, Response response, St } return Arrays.asList(headers.getValuesArray(name, true)); } + + @Nullable + @Override + public String getNetworkProtocolName(Request request, @Nullable Response response) { + return request.getProtocol().getSchemeName(); + } + + @Nullable + @Override + public String getNetworkProtocolVersion(Request request, @Nullable Response response) { + return request.getProtocol().getVersion(); + } + + @Override + @Nullable + public String getNetworkPeerAddress(Request request, @Nullable Response response) { + return request.getClientInfo().getAddress(); + } + + @Override + public Integer getNetworkPeerPort(Request request, @Nullable Response response) { + return request.getClientInfo().getPort(); + } + + @Nullable + @Override + public String getNetworkLocalAddress(Request request, @Nullable Response response) { + return ServerCallAccess.getServerAddress(request); + } } diff --git a/instrumentation/restlet/restlet-2.0/library/src/main/java/io/opentelemetry/instrumentation/restlet/v2_0/internal/RestletInstrumenterFactory.java b/instrumentation/restlet/restlet-2.0/library/src/main/java/io/opentelemetry/instrumentation/restlet/v2_0/internal/RestletInstrumenterFactory.java index 9a67815f71e1..54a60a980ec4 100644 --- a/instrumentation/restlet/restlet-2.0/library/src/main/java/io/opentelemetry/instrumentation/restlet/v2_0/internal/RestletInstrumenterFactory.java +++ b/instrumentation/restlet/restlet-2.0/library/src/main/java/io/opentelemetry/instrumentation/restlet/v2_0/internal/RestletInstrumenterFactory.java @@ -6,12 +6,15 @@ package io.opentelemetry.instrumentation.restlet.v2_0.internal; import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.instrumentation.api.incubator.semconv.http.HttpExperimentalAttributesExtractor; +import io.opentelemetry.instrumentation.api.incubator.semconv.http.HttpServerExperimentalMetrics; import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.ContextCustomizer; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteHolder; -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 io.opentelemetry.instrumentation.api.instrumenter.InstrumenterBuilder; +import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerMetrics; +import io.opentelemetry.instrumentation.api.semconv.http.HttpSpanStatusExtractor; import java.util.List; import org.restlet.Request; import org.restlet.Response; @@ -27,18 +30,27 @@ public final class RestletInstrumenterFactory { public static Instrumenter newServerInstrumenter( OpenTelemetry openTelemetry, AttributesExtractor httpServerAttributesExtractor, - List> additionalExtractors) { + SpanNameExtractor httpServerSpanNameExtractor, + ContextCustomizer httpServerRoute, + List> additionalExtractors, + boolean emitExperimentalHttpServerMetrics) { RestletHttpAttributesGetter httpAttributesGetter = RestletHttpAttributesGetter.INSTANCE; - return Instrumenter.builder( - openTelemetry, INSTRUMENTATION_NAME, HttpSpanNameExtractor.create(httpAttributesGetter)) - .setSpanStatusExtractor(HttpSpanStatusExtractor.create(httpAttributesGetter)) - .addAttributesExtractor(httpServerAttributesExtractor) - .addAttributesExtractors(additionalExtractors) - .addOperationMetrics(HttpServerMetrics.get()) - .addContextCustomizer(HttpRouteHolder.create(httpAttributesGetter)) - .buildServerInstrumenter(new RestletHeadersGetter()); + InstrumenterBuilder builder = + Instrumenter.builder( + openTelemetry, INSTRUMENTATION_NAME, httpServerSpanNameExtractor) + .setSpanStatusExtractor(HttpSpanStatusExtractor.create(httpAttributesGetter)) + .addAttributesExtractor(httpServerAttributesExtractor) + .addAttributesExtractors(additionalExtractors) + .addContextCustomizer(httpServerRoute) + .addOperationMetrics(HttpServerMetrics.get()); + if (emitExperimentalHttpServerMetrics) { + builder + .addAttributesExtractor(HttpExperimentalAttributesExtractor.create(httpAttributesGetter)) + .addOperationMetrics(HttpServerExperimentalMetrics.get()); + } + return builder.buildServerInstrumenter(new RestletHeadersGetter()); } private RestletInstrumenterFactory() {} diff --git a/instrumentation/restlet/restlet-2.0/library/src/main/java/io/opentelemetry/instrumentation/restlet/v2_0/internal/RestletNetAttributesGetter.java b/instrumentation/restlet/restlet-2.0/library/src/main/java/io/opentelemetry/instrumentation/restlet/v2_0/internal/ServerCallAccess.java similarity index 53% rename from instrumentation/restlet/restlet-2.0/library/src/main/java/io/opentelemetry/instrumentation/restlet/v2_0/internal/RestletNetAttributesGetter.java rename to instrumentation/restlet/restlet-2.0/library/src/main/java/io/opentelemetry/instrumentation/restlet/v2_0/internal/ServerCallAccess.java index a42ba7da5097..61a0aada1463 100644 --- a/instrumentation/restlet/restlet-2.0/library/src/main/java/io/opentelemetry/instrumentation/restlet/v2_0/internal/RestletNetAttributesGetter.java +++ b/instrumentation/restlet/restlet-2.0/library/src/main/java/io/opentelemetry/instrumentation/restlet/v2_0/internal/ServerCallAccess.java @@ -7,32 +7,21 @@ import static java.lang.invoke.MethodType.methodType; -import io.opentelemetry.instrumentation.api.instrumenter.net.NetServerAttributesGetter; import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; import javax.annotation.Nullable; import org.restlet.Request; -import org.restlet.Response; -/** - * This class is internal and is hence not for public use. Its APIs are unstable and can change at - * any time. - */ -public final class RestletNetAttributesGetter - implements NetServerAttributesGetter { +final class ServerCallAccess { private static final Class HTTP_REQUEST_CLASS; private static final MethodHandle GET_HTTP_CALL; - private static final MethodHandle GET_HOST_DOMAIN; - private static final MethodHandle GET_SERVER_PORT; private static final MethodHandle GET_SERVER_ADDRESS; static { Class httpRequestClass = null; Class serverCallClass = null; MethodHandle getHttpCall = null; - MethodHandle getHostDomain = null; - MethodHandle getServerPort = null; MethodHandle getServerAddress = null; try { @@ -62,9 +51,6 @@ public final class RestletNetAttributesGetter MethodHandles.Lookup lookup = MethodHandles.publicLookup(); getHttpCall = lookup.findVirtual(httpRequestClass, "getHttpCall", methodType(serverCallClass)); - getHostDomain = - lookup.findVirtual(serverCallClass, "getHostDomain", methodType(String.class)); - getServerPort = lookup.findVirtual(serverCallClass, "getServerPort", methodType(int.class)); getServerAddress = lookup.findVirtual(serverCallClass, "getServerAddress", methodType(String.class)); } catch (NoSuchMethodException | IllegalAccessException e) { @@ -74,71 +60,11 @@ public final class RestletNetAttributesGetter HTTP_REQUEST_CLASS = httpRequestClass; GET_HTTP_CALL = getHttpCall; - GET_HOST_DOMAIN = getHostDomain; - GET_SERVER_PORT = getServerPort; GET_SERVER_ADDRESS = getServerAddress; } @Nullable - @Override - public String getNetworkProtocolName(Request request, @Nullable Response response) { - return request.getProtocol().getSchemeName(); - } - - @Nullable - @Override - public String getNetworkProtocolVersion(Request request, @Nullable Response response) { - return request.getProtocol().getVersion(); - } - - @Nullable - @Override - public String getServerAddress(Request request) { - if (GET_HOST_DOMAIN == null) { - return null; - } - Object call = serverCall(request); - if (call == null) { - return null; - } - try { - return (String) GET_HOST_DOMAIN.invoke(call); - } catch (Throwable e) { - return null; - } - } - - @Nullable - @Override - public Integer getServerPort(Request request) { - if (GET_SERVER_PORT == null) { - return null; - } - Object call = serverCall(request); - if (call == null) { - return null; - } - try { - return (int) GET_SERVER_PORT.invoke(call); - } catch (Throwable e) { - return null; - } - } - - @Override - @Nullable - public String getClientSocketAddress(Request request, @Nullable Response response) { - return request.getClientInfo().getAddress(); - } - - @Override - public Integer getClientSocketPort(Request request, @Nullable Response response) { - return request.getClientInfo().getPort(); - } - - @Nullable - @Override - public String getServerSocketAddress(Request request, @Nullable Response response) { + static String getServerAddress(Request request) { if (GET_SERVER_ADDRESS == null) { return null; } @@ -167,4 +93,6 @@ private static Object serverCall(Request request) { } return null; } + + private ServerCallAccess() {} } diff --git a/instrumentation/restlet/restlet-2.0/testing/src/main/groovy/io/opentelemetry/instrumentation/restlet/v2_0/AbstractRestletServerTest.groovy b/instrumentation/restlet/restlet-2.0/testing/src/main/groovy/io/opentelemetry/instrumentation/restlet/v2_0/AbstractRestletServerTest.groovy index baad1e390e7c..70f9a5d42e6e 100644 --- a/instrumentation/restlet/restlet-2.0/testing/src/main/groovy/io/opentelemetry/instrumentation/restlet/v2_0/AbstractRestletServerTest.groovy +++ b/instrumentation/restlet/restlet-2.0/testing/src/main/groovy/io/opentelemetry/instrumentation/restlet/v2_0/AbstractRestletServerTest.groovy @@ -5,7 +5,7 @@ package io.opentelemetry.instrumentation.restlet.v2_0 - +import io.opentelemetry.instrumentation.api.internal.HttpConstants import io.opentelemetry.instrumentation.test.base.HttpServerTest import io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint import org.restlet.Component @@ -176,14 +176,17 @@ abstract class AbstractRestletServerTest extends HttpServerTest { } @Override - String expectedHttpRoute(ServerEndpoint endpoint) { + String expectedHttpRoute(ServerEndpoint endpoint, String method) { + if (method == HttpConstants._OTHER) { + return getContextPath() + endpoint.path + } switch (endpoint) { case PATH_PARAM: return getContextPath() + "/path/{id}/param" case NOT_FOUND: return getContextPath() + "/*" default: - return super.expectedHttpRoute(endpoint) + return super.expectedHttpRoute(endpoint, method) } } diff --git a/instrumentation/restlet/restlet-2.0/testing/src/main/groovy/io/opentelemetry/instrumentation/restlet/v2_0/spring/AbstractSpringServerTest.groovy b/instrumentation/restlet/restlet-2.0/testing/src/main/groovy/io/opentelemetry/instrumentation/restlet/v2_0/spring/AbstractSpringServerTest.groovy index ebb939190c96..ea2f8a569516 100644 --- a/instrumentation/restlet/restlet-2.0/testing/src/main/groovy/io/opentelemetry/instrumentation/restlet/v2_0/spring/AbstractSpringServerTest.groovy +++ b/instrumentation/restlet/restlet-2.0/testing/src/main/groovy/io/opentelemetry/instrumentation/restlet/v2_0/spring/AbstractSpringServerTest.groovy @@ -5,7 +5,9 @@ package io.opentelemetry.instrumentation.restlet.v2_0.spring +import io.opentelemetry.instrumentation.api.internal.HttpConstants import io.opentelemetry.instrumentation.restlet.v2_0.AbstractRestletServerTest +import io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint import org.restlet.Component import org.restlet.Server import org.restlet.routing.Router @@ -31,4 +33,16 @@ abstract class AbstractSpringServerTest extends AbstractRestletServerTest { host.attach(router) } + @Override + String expectedHttpRoute(ServerEndpoint endpoint, String method) { + if (method == HttpConstants._OTHER) { + return getContextPath() + endpoint.path + } + return super.expectedHttpRoute(endpoint, method) + } + + @Override + int getResponseCodeOnNonStandardHttpMethod() { + 405 + } } diff --git a/instrumentation/rmi/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rmi/client/RmiClientAttributesGetter.java b/instrumentation/rmi/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rmi/client/RmiClientAttributesGetter.java index 7e4ccb02738c..1b139dacc7aa 100644 --- a/instrumentation/rmi/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rmi/client/RmiClientAttributesGetter.java +++ b/instrumentation/rmi/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rmi/client/RmiClientAttributesGetter.java @@ -5,7 +5,7 @@ package io.opentelemetry.javaagent.instrumentation.rmi.client; -import io.opentelemetry.instrumentation.api.instrumenter.rpc.RpcAttributesGetter; +import io.opentelemetry.instrumentation.api.incubator.semconv.rpc.RpcAttributesGetter; import java.lang.reflect.Method; enum RmiClientAttributesGetter implements RpcAttributesGetter { diff --git a/instrumentation/rmi/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rmi/client/RmiClientSingletons.java b/instrumentation/rmi/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rmi/client/RmiClientSingletons.java index 678f17611b3b..49316a50de33 100644 --- a/instrumentation/rmi/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rmi/client/RmiClientSingletons.java +++ b/instrumentation/rmi/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rmi/client/RmiClientSingletons.java @@ -6,10 +6,10 @@ package io.opentelemetry.javaagent.instrumentation.rmi.client; import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.instrumentation.api.incubator.semconv.rpc.RpcClientAttributesExtractor; +import io.opentelemetry.instrumentation.api.incubator.semconv.rpc.RpcSpanNameExtractor; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; import io.opentelemetry.instrumentation.api.instrumenter.SpanKindExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.rpc.RpcClientAttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.rpc.RpcSpanNameExtractor; import java.lang.reflect.Method; public final class RmiClientSingletons { diff --git a/instrumentation/rmi/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rmi/context/RmiContextPropagationInstrumentationModule.java b/instrumentation/rmi/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rmi/context/RmiContextPropagationInstrumentationModule.java index bd09178336e8..20317582d5c8 100644 --- a/instrumentation/rmi/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rmi/context/RmiContextPropagationInstrumentationModule.java +++ b/instrumentation/rmi/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rmi/context/RmiContextPropagationInstrumentationModule.java @@ -20,6 +20,13 @@ public RmiContextPropagationInstrumentationModule() { super("rmi", "rmi-context-propagation"); } + @Override + public boolean isIndyModule() { + // java.lang.IllegalAccessError: class + // io.opentelemetry.javaagent.instrumentation.rmi.context.client.RmiClientContextInstrumentation$StreamRemoteCallConstructorAdvice (in unnamed module @0x740ee00f) cannot access class sun.rmi.transport.Connection (in module java.rmi) because module java.rmi does not export sun.rmi.transport to unnamed module @0x740ee00f + return false; + } + @Override public List typeInstrumentations() { return asList(new RmiClientContextInstrumentation(), new RmiServerContextInstrumentation()); diff --git a/instrumentation/rmi/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rmi/server/RemoteServerInstrumentation.java b/instrumentation/rmi/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rmi/server/RemoteServerInstrumentation.java index cddb58191a8b..23024bacb3a3 100644 --- a/instrumentation/rmi/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rmi/server/RemoteServerInstrumentation.java +++ b/instrumentation/rmi/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rmi/server/RemoteServerInstrumentation.java @@ -17,7 +17,7 @@ import io.opentelemetry.context.Context; import io.opentelemetry.context.Scope; -import io.opentelemetry.instrumentation.api.instrumenter.util.ClassAndMethod; +import io.opentelemetry.instrumentation.api.incubator.semconv.util.ClassAndMethod; import io.opentelemetry.javaagent.bootstrap.CallDepth; import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; diff --git a/instrumentation/rmi/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rmi/server/RmiServerAttributesGetter.java b/instrumentation/rmi/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rmi/server/RmiServerAttributesGetter.java index 5f227cf96e65..ad598bb53814 100644 --- a/instrumentation/rmi/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rmi/server/RmiServerAttributesGetter.java +++ b/instrumentation/rmi/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rmi/server/RmiServerAttributesGetter.java @@ -5,8 +5,8 @@ package io.opentelemetry.javaagent.instrumentation.rmi.server; -import io.opentelemetry.instrumentation.api.instrumenter.rpc.RpcAttributesGetter; -import io.opentelemetry.instrumentation.api.instrumenter.util.ClassAndMethod; +import io.opentelemetry.instrumentation.api.incubator.semconv.rpc.RpcAttributesGetter; +import io.opentelemetry.instrumentation.api.incubator.semconv.util.ClassAndMethod; enum RmiServerAttributesGetter implements RpcAttributesGetter { INSTANCE; diff --git a/instrumentation/rmi/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rmi/server/RmiServerSingletons.java b/instrumentation/rmi/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rmi/server/RmiServerSingletons.java index ac2194c69867..87b8bd8cea20 100644 --- a/instrumentation/rmi/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rmi/server/RmiServerSingletons.java +++ b/instrumentation/rmi/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rmi/server/RmiServerSingletons.java @@ -6,11 +6,11 @@ package io.opentelemetry.javaagent.instrumentation.rmi.server; import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.instrumentation.api.incubator.semconv.rpc.RpcServerAttributesExtractor; +import io.opentelemetry.instrumentation.api.incubator.semconv.rpc.RpcSpanNameExtractor; +import io.opentelemetry.instrumentation.api.incubator.semconv.util.ClassAndMethod; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; import io.opentelemetry.instrumentation.api.instrumenter.SpanKindExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.rpc.RpcServerAttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.rpc.RpcSpanNameExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.util.ClassAndMethod; public final class RmiServerSingletons { diff --git a/instrumentation/rmi/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/rmi/RmiTest.java b/instrumentation/rmi/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/rmi/RmiTest.java index 3a93e338506c..f604e9df0382 100644 --- a/instrumentation/rmi/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/rmi/RmiTest.java +++ b/instrumentation/rmi/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/rmi/RmiTest.java @@ -15,7 +15,8 @@ import io.opentelemetry.instrumentation.test.utils.PortUtils; import io.opentelemetry.instrumentation.testing.internal.AutoCleanupExtension; import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.opentelemetry.semconv.ExceptionAttributes; +import io.opentelemetry.semconv.incubating.RpcIncubatingAttributes; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry; import java.rmi.server.UnicastRemoteObject; @@ -33,15 +34,14 @@ class RmiTest { public static final AgentInstrumentationExtension testing = AgentInstrumentationExtension.create(); - private static int registryPort; private static Registry serverRegistry; private static Registry clientRegistry; - @RegisterExtension final AutoCleanupExtension autoCleanup = AutoCleanupExtension.create(); + @RegisterExtension static final AutoCleanupExtension autoCleanup = AutoCleanupExtension.create(); @BeforeAll static void setUp() throws Exception { - registryPort = PortUtils.findOpenPort(); + int registryPort = PortUtils.findOpenPort(); serverRegistry = LocateRegistry.createRegistry(registryPort); clientRegistry = LocateRegistry.getRegistry("localhost", registryPort); } @@ -82,17 +82,17 @@ void clientCallCreatesSpans() throws Exception { .hasKind(SpanKind.CLIENT) .hasParentSpanId(trace.get(0).getSpanId()) .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.RPC_SYSTEM, "java_rmi"), - equalTo(SemanticAttributes.RPC_SERVICE, "rmi.app.Greeter"), - equalTo(SemanticAttributes.RPC_METHOD, "hello")), + equalTo(RpcIncubatingAttributes.RPC_SYSTEM, "java_rmi"), + equalTo(RpcIncubatingAttributes.RPC_SERVICE, "rmi.app.Greeter"), + equalTo(RpcIncubatingAttributes.RPC_METHOD, "hello")), span -> assertThat(span) .hasName("rmi.app.Server/hello") .hasKind(SpanKind.SERVER) .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.RPC_SYSTEM, "java_rmi"), - equalTo(SemanticAttributes.RPC_SERVICE, "rmi.app.Server"), - equalTo(SemanticAttributes.RPC_METHOD, "hello")))); + equalTo(RpcIncubatingAttributes.RPC_SYSTEM, "java_rmi"), + equalTo(RpcIncubatingAttributes.RPC_SERVICE, "rmi.app.Server"), + equalTo(RpcIncubatingAttributes.RPC_METHOD, "hello")))); } @Test @@ -119,14 +119,14 @@ void serviceThrownException() throws Exception { Throwable thrown = catchThrowableOfType( + IllegalStateException.class, () -> testing.runWithSpan( "parent", () -> { Greeter client = (Greeter) clientRegistry.lookup(Server.RMI_ID); client.exceptional(); - }), - IllegalStateException.class); + })); assertThat(testing.waitForTraces(1)) .satisfiesExactly( @@ -146,21 +146,21 @@ void serviceThrownException() throws Exception { .hasEventsSatisfyingExactly( event -> event - .hasName(SemanticAttributes.EXCEPTION_EVENT_NAME) + .hasName("exception") .hasAttributesSatisfyingExactly( equalTo( - SemanticAttributes.EXCEPTION_TYPE, + ExceptionAttributes.EXCEPTION_TYPE, thrown.getClass().getCanonicalName()), equalTo( - SemanticAttributes.EXCEPTION_MESSAGE, + ExceptionAttributes.EXCEPTION_MESSAGE, thrown.getMessage()), satisfies( - SemanticAttributes.EXCEPTION_STACKTRACE, + ExceptionAttributes.EXCEPTION_STACKTRACE, AbstractAssert::isNotNull))) .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.RPC_SYSTEM, "java_rmi"), - equalTo(SemanticAttributes.RPC_SERVICE, "rmi.app.Greeter"), - equalTo(SemanticAttributes.RPC_METHOD, "exceptional")), + equalTo(RpcIncubatingAttributes.RPC_SYSTEM, "java_rmi"), + equalTo(RpcIncubatingAttributes.RPC_SERVICE, "rmi.app.Greeter"), + equalTo(RpcIncubatingAttributes.RPC_METHOD, "exceptional")), span -> assertThat(span) .hasName("rmi.app.Server/exceptional") @@ -168,20 +168,20 @@ void serviceThrownException() throws Exception { .hasEventsSatisfyingExactly( event -> event - .hasName(SemanticAttributes.EXCEPTION_EVENT_NAME) + .hasName("exception") .hasAttributesSatisfyingExactly( equalTo( - SemanticAttributes.EXCEPTION_TYPE, + ExceptionAttributes.EXCEPTION_TYPE, thrown.getClass().getCanonicalName()), equalTo( - SemanticAttributes.EXCEPTION_MESSAGE, + ExceptionAttributes.EXCEPTION_MESSAGE, thrown.getMessage()), satisfies( - SemanticAttributes.EXCEPTION_STACKTRACE, + ExceptionAttributes.EXCEPTION_STACKTRACE, AbstractAssert::isNotNull))) .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.RPC_SYSTEM, "java_rmi"), - equalTo(SemanticAttributes.RPC_SERVICE, "rmi.app.Server"), - equalTo(SemanticAttributes.RPC_METHOD, "exceptional")))); + equalTo(RpcIncubatingAttributes.RPC_SYSTEM, "java_rmi"), + equalTo(RpcIncubatingAttributes.RPC_SERVICE, "rmi.app.Server"), + equalTo(RpcIncubatingAttributes.RPC_METHOD, "exceptional")))); } } diff --git a/instrumentation/rocketmq/rocketmq-client/rocketmq-client-4.8/README.md b/instrumentation/rocketmq/rocketmq-client/rocketmq-client-4.8/README.md index c200fdef1c86..5918e2241a78 100644 --- a/instrumentation/rocketmq/rocketmq-client/rocketmq-client-4.8/README.md +++ b/instrumentation/rocketmq/rocketmq-client/rocketmq-client-4.8/README.md @@ -1,5 +1,5 @@ # Settings for the Apache RocketMQ remoting-based client instrumentation -| System property | Type | Default | Description | -|---|---|---|---| +| System property | Type | Default | Description | +| ------------------------------------------------------------------- | ------- | ------- | --------------------------------------------------- | | `otel.instrumentation.rocketmq-client.experimental-span-attributes` | Boolean | `false` | Enable the capture of experimental span attributes. | diff --git a/instrumentation/rocketmq/rocketmq-client/rocketmq-client-4.8/javaagent/build.gradle.kts b/instrumentation/rocketmq/rocketmq-client/rocketmq-client-4.8/javaagent/build.gradle.kts index 7a7d1cc160bf..8447928232af 100644 --- a/instrumentation/rocketmq/rocketmq-client/rocketmq-client-4.8/javaagent/build.gradle.kts +++ b/instrumentation/rocketmq/rocketmq-client/rocketmq-client-4.8/javaagent/build.gradle.kts @@ -29,4 +29,9 @@ tasks.withType().configureEach { // required on jdk17 jvmArgs("--add-opens=java.base/sun.nio.ch=ALL-UNNAMED") jvmArgs("-XX:+IgnoreUnrecognizedVMOptions") + + jvmArgs("-Dotel.instrumentation.common.experimental.controller-telemetry.enabled=true") + + // with default settings tests will fail when disk is 90% full + jvmArgs("-Drocketmq.broker.diskSpaceWarningLevelRatio=1.0") } diff --git a/instrumentation/rocketmq/rocketmq-client/rocketmq-client-4.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rocketmqclient/v4_8/RocketMqClientHooks.java b/instrumentation/rocketmq/rocketmq-client/rocketmq-client-4.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rocketmqclient/v4_8/RocketMqClientHooks.java index 097c9ff64e61..7c2b958ca4f9 100644 --- a/instrumentation/rocketmq/rocketmq-client/rocketmq-client-4.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rocketmqclient/v4_8/RocketMqClientHooks.java +++ b/instrumentation/rocketmq/rocketmq-client/rocketmq-client-4.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rocketmqclient/v4_8/RocketMqClientHooks.java @@ -7,41 +7,23 @@ import io.opentelemetry.api.GlobalOpenTelemetry; import io.opentelemetry.instrumentation.rocketmqclient.v4_8.RocketMqTelemetry; +import io.opentelemetry.javaagent.bootstrap.internal.AgentInstrumentationConfig; import io.opentelemetry.javaagent.bootstrap.internal.ExperimentalConfig; -import io.opentelemetry.javaagent.bootstrap.internal.InstrumentationConfig; -import java.util.logging.Logger; import org.apache.rocketmq.client.hook.ConsumeMessageHook; import org.apache.rocketmq.client.hook.SendMessageHook; public final class RocketMqClientHooks { - private static final Logger logger = Logger.getLogger(RocketMqClientHooks.class.getName()); - @SuppressWarnings("deprecation") // call to deprecated method will be removed in the future private static final RocketMqTelemetry TELEMETRY = RocketMqTelemetry.builder(GlobalOpenTelemetry.get()) .setCapturedHeaders(ExperimentalConfig.get().getMessagingHeaders()) - .setPropagationEnabled( - InstrumentationConfig.get() - .getBoolean("otel.instrumentation.rocketmq-client.propagation", true)) .setCaptureExperimentalSpanAttributes( - InstrumentationConfig.get() + AgentInstrumentationConfig.get() .getBoolean( "otel.instrumentation.rocketmq-client.experimental-span-attributes", false)) .build(); - static { - if (InstrumentationConfig.get().getString("otel.instrumentation.rocketmq-client.propagation") - != null) { - logger.warning( - "The \"otel.instrumentation.rocketmq-client.propagation\" configuration property has" - + " been deprecated and will be removed in a future version." - + " If you have a need for this configuration property, please open an issue in the" - + " opentelemetry-java-instrumentation repository:" - + " https://github.com/open-telemetry/opentelemetry-java-instrumentation/issues"); - } - } - public static final ConsumeMessageHook CONSUME_MESSAGE_HOOK = TELEMETRY.newTracingConsumeMessageHook(); diff --git a/instrumentation/rocketmq/rocketmq-client/rocketmq-client-4.8/javaagent/src/test/groovy/io/opentelemetry/instrumentation/rocketmqclient/v4_8/RocketMqClientTest.groovy b/instrumentation/rocketmq/rocketmq-client/rocketmq-client-4.8/javaagent/src/test/groovy/io/opentelemetry/instrumentation/rocketmqclient/v4_8/RocketMqClientTest.groovy deleted file mode 100644 index f59d0859b25d..000000000000 --- a/instrumentation/rocketmq/rocketmq-client/rocketmq-client-4.8/javaagent/src/test/groovy/io/opentelemetry/instrumentation/rocketmqclient/v4_8/RocketMqClientTest.groovy +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.rocketmqclient.v4_8 - - -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) { - } -} diff --git a/instrumentation/rocketmq/rocketmq-client/rocketmq-client-4.8/javaagent/src/test/java/io/opentelemetry/instrumentation/rocketmqclient/v4_8/RocketMqClientTest.java b/instrumentation/rocketmq/rocketmq-client/rocketmq-client-4.8/javaagent/src/test/java/io/opentelemetry/instrumentation/rocketmqclient/v4_8/RocketMqClientTest.java new file mode 100644 index 000000000000..49dacda729ce --- /dev/null +++ b/instrumentation/rocketmq/rocketmq-client/rocketmq-client-4.8/javaagent/src/test/java/io/opentelemetry/instrumentation/rocketmqclient/v4_8/RocketMqClientTest.java @@ -0,0 +1,29 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.rocketmqclient.v4_8; + +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer; +import org.apache.rocketmq.client.producer.DefaultMQProducer; +import org.junit.jupiter.api.extension.RegisterExtension; + +class RocketMqClientTest extends AbstractRocketMqClientTest { + + @RegisterExtension + private static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + @Override + InstrumentationExtension testing() { + return testing; + } + + @Override + void configureMqProducer(DefaultMQProducer producer) {} + + @Override + void configureMqPushConsumer(DefaultMQPushConsumer consumer) {} +} diff --git a/instrumentation/rocketmq/rocketmq-client/rocketmq-client-4.8/library/build.gradle.kts b/instrumentation/rocketmq/rocketmq-client/rocketmq-client-4.8/library/build.gradle.kts index 892247fdf2b8..bf78d9334872 100644 --- a/instrumentation/rocketmq/rocketmq-client/rocketmq-client-4.8/library/build.gradle.kts +++ b/instrumentation/rocketmq/rocketmq-client/rocketmq-client-4.8/library/build.gradle.kts @@ -18,4 +18,7 @@ tasks.withType().configureEach { // required on jdk17 jvmArgs("--add-opens=java.base/sun.nio.ch=ALL-UNNAMED") jvmArgs("-XX:+IgnoreUnrecognizedVMOptions") + jvmArgs("-Dotel.instrumentation.common.experimental.controller-telemetry.enabled=true") + // with default settings tests will fail when disk is 90% full + jvmArgs("-Drocketmq.broker.diskSpaceWarningLevelRatio=1.0") } diff --git a/instrumentation/rocketmq/rocketmq-client/rocketmq-client-4.8/library/src/main/java/io/opentelemetry/instrumentation/rocketmqclient/v4_8/RocketMqConsumerAttributeGetter.java b/instrumentation/rocketmq/rocketmq-client/rocketmq-client-4.8/library/src/main/java/io/opentelemetry/instrumentation/rocketmqclient/v4_8/RocketMqConsumerAttributeGetter.java index b1a346998c31..d2dff33bbc6c 100644 --- a/instrumentation/rocketmq/rocketmq-client/rocketmq-client-4.8/library/src/main/java/io/opentelemetry/instrumentation/rocketmqclient/v4_8/RocketMqConsumerAttributeGetter.java +++ b/instrumentation/rocketmq/rocketmq-client/rocketmq-client-4.8/library/src/main/java/io/opentelemetry/instrumentation/rocketmqclient/v4_8/RocketMqConsumerAttributeGetter.java @@ -5,7 +5,7 @@ package io.opentelemetry.instrumentation.rocketmqclient.v4_8; -import io.opentelemetry.instrumentation.api.instrumenter.messaging.MessagingAttributesGetter; +import io.opentelemetry.instrumentation.api.incubator.semconv.messaging.MessagingAttributesGetter; import java.util.Collections; import java.util.List; import javax.annotation.Nullable; @@ -24,11 +24,22 @@ public String getDestination(MessageExt request) { return request.getTopic(); } + @Nullable + @Override + public String getDestinationTemplate(MessageExt request) { + return null; + } + @Override public boolean isTemporaryDestination(MessageExt request) { return false; } + @Override + public boolean isAnonymousDestination(MessageExt request) { + return false; + } + @Nullable @Override public String getConversationId(MessageExt request) { @@ -37,14 +48,14 @@ public String getConversationId(MessageExt request) { @Nullable @Override - public Long getMessagePayloadSize(MessageExt request) { + public Long getMessageBodySize(MessageExt request) { byte[] body = request.getBody(); return body == null ? null : (long) body.length; } @Nullable @Override - public Long getMessagePayloadCompressedSize(MessageExt request) { + public Long getMessageEnvelopeSize(MessageExt request) { return null; } @@ -54,6 +65,18 @@ public String getMessageId(MessageExt request, @Nullable Void unused) { return request.getMsgId(); } + @Nullable + @Override + public String getClientId(MessageExt request) { + return null; + } + + @Nullable + @Override + public Long getBatchMessageCount(MessageExt request, @Nullable Void unused) { + return null; + } + @Override public List getMessageHeader(MessageExt request, String name) { String value = request.getProperties().get(name); diff --git a/instrumentation/rocketmq/rocketmq-client/rocketmq-client-4.8/library/src/main/java/io/opentelemetry/instrumentation/rocketmqclient/v4_8/RocketMqConsumerExperimentalAttributeExtractor.java b/instrumentation/rocketmq/rocketmq-client/rocketmq-client-4.8/library/src/main/java/io/opentelemetry/instrumentation/rocketmqclient/v4_8/RocketMqConsumerExperimentalAttributeExtractor.java index adcfe424e6b4..8d017e75965f 100644 --- a/instrumentation/rocketmq/rocketmq-client/rocketmq-client-4.8/library/src/main/java/io/opentelemetry/instrumentation/rocketmqclient/v4_8/RocketMqConsumerExperimentalAttributeExtractor.java +++ b/instrumentation/rocketmq/rocketmq-client/rocketmq-client-4.8/library/src/main/java/io/opentelemetry/instrumentation/rocketmqclient/v4_8/RocketMqConsumerExperimentalAttributeExtractor.java @@ -9,7 +9,6 @@ import io.opentelemetry.api.common.AttributesBuilder; import io.opentelemetry.context.Context; import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; import java.net.SocketAddress; import javax.annotation.Nullable; import org.apache.rocketmq.common.message.MessageExt; @@ -18,6 +17,10 @@ enum RocketMqConsumerExperimentalAttributeExtractor implements AttributesExtractor { INSTANCE; + // copied from MessagingIncubatingAttributes + private static final AttributeKey MESSAGING_ROCKETMQ_MESSAGE_TAG = + AttributeKey.stringKey("messaging.rocketmq.message.tag"); + private static final AttributeKey MESSAGING_ROCKETMQ_QUEUE_ID = AttributeKey.longKey("messaging.rocketmq.queue_id"); private static final AttributeKey MESSAGING_ROCKETMQ_QUEUE_OFFSET = @@ -29,7 +32,7 @@ enum RocketMqConsumerExperimentalAttributeExtractor public void onStart(AttributesBuilder attributes, Context parentContext, MessageExt msg) { String tags = msg.getTags(); if (tags != null) { - attributes.put(SemanticAttributes.MESSAGING_ROCKETMQ_MESSAGE_TAG, tags); + attributes.put(MESSAGING_ROCKETMQ_MESSAGE_TAG, tags); } attributes.put(MESSAGING_ROCKETMQ_QUEUE_ID, msg.getQueueId()); attributes.put(MESSAGING_ROCKETMQ_QUEUE_OFFSET, msg.getQueueOffset()); diff --git a/instrumentation/rocketmq/rocketmq-client/rocketmq-client-4.8/library/src/main/java/io/opentelemetry/instrumentation/rocketmqclient/v4_8/RocketMqInstrumenterFactory.java b/instrumentation/rocketmq/rocketmq-client/rocketmq-client-4.8/library/src/main/java/io/opentelemetry/instrumentation/rocketmqclient/v4_8/RocketMqInstrumenterFactory.java index ddacdc576e24..f01f9b3ead35 100644 --- a/instrumentation/rocketmq/rocketmq-client/rocketmq-client-4.8/library/src/main/java/io/opentelemetry/instrumentation/rocketmqclient/v4_8/RocketMqInstrumenterFactory.java +++ b/instrumentation/rocketmq/rocketmq-client/rocketmq-client-4.8/library/src/main/java/io/opentelemetry/instrumentation/rocketmqclient/v4_8/RocketMqInstrumenterFactory.java @@ -6,19 +6,18 @@ package io.opentelemetry.instrumentation.rocketmqclient.v4_8; import static io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor.constant; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.MESSAGING_OPERATION; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.MESSAGING_SYSTEM; import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.instrumentation.api.incubator.semconv.messaging.MessageOperation; +import io.opentelemetry.instrumentation.api.incubator.semconv.messaging.MessagingAttributesExtractor; +import io.opentelemetry.instrumentation.api.incubator.semconv.messaging.MessagingAttributesGetter; +import io.opentelemetry.instrumentation.api.incubator.semconv.messaging.MessagingSpanNameExtractor; 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 io.opentelemetry.instrumentation.api.instrumenter.SpanLinksExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.messaging.MessageOperation; -import io.opentelemetry.instrumentation.api.instrumenter.messaging.MessagingAttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.messaging.MessagingAttributesGetter; -import io.opentelemetry.instrumentation.api.instrumenter.messaging.MessagingSpanNameExtractor; import io.opentelemetry.instrumentation.api.internal.PropagatorBasedSpanLinksExtractor; import java.util.List; import org.apache.rocketmq.client.hook.SendMessageContext; @@ -28,14 +27,19 @@ class RocketMqInstrumenterFactory { private static final String INSTRUMENTATION_NAME = "io.opentelemetry.rocketmq-client-4.8"; + // copied from MessagingIncubatingAttributes + private static final AttributeKey MESSAGING_OPERATION = + AttributeKey.stringKey("messaging.operation"); + private static final AttributeKey MESSAGING_SYSTEM = + AttributeKey.stringKey("messaging.system"); + static Instrumenter createProducerInstrumenter( OpenTelemetry openTelemetry, List capturedHeaders, - boolean captureExperimentalSpanAttributes, - boolean propagationEnabled) { + boolean captureExperimentalSpanAttributes) { RocketMqProducerAttributeGetter getter = RocketMqProducerAttributeGetter.INSTANCE; - MessageOperation operation = MessageOperation.SEND; + MessageOperation operation = MessageOperation.PUBLISH; InstrumenterBuilder instrumenterBuilder = Instrumenter.builder( @@ -49,18 +53,13 @@ static Instrumenter createProducerInstrumenter( RocketMqProducerExperimentalAttributeExtractor.INSTANCE); } - if (propagationEnabled) { - return instrumenterBuilder.buildProducerInstrumenter(MapSetter.INSTANCE); - } else { - return instrumenterBuilder.buildInstrumenter(SpanKindExtractor.alwaysProducer()); - } + return instrumenterBuilder.buildProducerInstrumenter(MapSetter.INSTANCE); } static RocketMqConsumerInstrumenter createConsumerInstrumenter( OpenTelemetry openTelemetry, List capturedHeaders, - boolean captureExperimentalSpanAttributes, - boolean propagationEnabled) { + boolean captureExperimentalSpanAttributes) { InstrumenterBuilder batchReceiveInstrumenterBuilder = Instrumenter.builder( @@ -70,17 +69,9 @@ static RocketMqConsumerInstrumenter createConsumerInstrumenter( return new RocketMqConsumerInstrumenter( createProcessInstrumenter( - openTelemetry, - capturedHeaders, - captureExperimentalSpanAttributes, - propagationEnabled, - false), + openTelemetry, capturedHeaders, captureExperimentalSpanAttributes, false), createProcessInstrumenter( - openTelemetry, - capturedHeaders, - captureExperimentalSpanAttributes, - propagationEnabled, - true), + openTelemetry, capturedHeaders, captureExperimentalSpanAttributes, true), batchReceiveInstrumenterBuilder.buildInstrumenter(SpanKindExtractor.alwaysConsumer())); } @@ -88,7 +79,6 @@ private static Instrumenter createProcessInstrumenter( OpenTelemetry openTelemetry, List capturedHeaders, boolean captureExperimentalSpanAttributes, - boolean propagationEnabled, boolean batch) { RocketMqConsumerAttributeGetter getter = RocketMqConsumerAttributeGetter.INSTANCE; @@ -106,10 +96,6 @@ private static Instrumenter createProcessInstrumenter( builder.addAttributesExtractor(RocketMqConsumerExperimentalAttributeExtractor.INSTANCE); } - if (!propagationEnabled) { - return builder.buildInstrumenter(SpanKindExtractor.alwaysConsumer()); - } - if (batch) { SpanLinksExtractor spanLinksExtractor = new PropagatorBasedSpanLinksExtractor<>( diff --git a/instrumentation/rocketmq/rocketmq-client/rocketmq-client-4.8/library/src/main/java/io/opentelemetry/instrumentation/rocketmqclient/v4_8/RocketMqProducerAttributeGetter.java b/instrumentation/rocketmq/rocketmq-client/rocketmq-client-4.8/library/src/main/java/io/opentelemetry/instrumentation/rocketmqclient/v4_8/RocketMqProducerAttributeGetter.java index 0bd95d85d5c4..1f8dad080078 100644 --- a/instrumentation/rocketmq/rocketmq-client/rocketmq-client-4.8/library/src/main/java/io/opentelemetry/instrumentation/rocketmqclient/v4_8/RocketMqProducerAttributeGetter.java +++ b/instrumentation/rocketmq/rocketmq-client/rocketmq-client-4.8/library/src/main/java/io/opentelemetry/instrumentation/rocketmqclient/v4_8/RocketMqProducerAttributeGetter.java @@ -5,7 +5,7 @@ package io.opentelemetry.instrumentation.rocketmqclient.v4_8; -import io.opentelemetry.instrumentation.api.instrumenter.messaging.MessagingAttributesGetter; +import io.opentelemetry.instrumentation.api.incubator.semconv.messaging.MessagingAttributesGetter; import java.util.Collections; import java.util.List; import javax.annotation.Nullable; @@ -18,37 +18,48 @@ enum RocketMqProducerAttributeGetter INSTANCE; @Override - public String getSystem(SendMessageContext sendMessageContext) { + public String getSystem(SendMessageContext request) { return "rocketmq"; } @Nullable @Override - public String getDestination(SendMessageContext sendMessageContext) { - Message message = sendMessageContext.getMessage(); + public String getDestination(SendMessageContext request) { + Message message = request.getMessage(); return message == null ? null : message.getTopic(); } + @Nullable + @Override + public String getDestinationTemplate(SendMessageContext request) { + return null; + } + @Override - public boolean isTemporaryDestination(SendMessageContext sendMessageContext) { + public boolean isTemporaryDestination(SendMessageContext request) { + return false; + } + + @Override + public boolean isAnonymousDestination(SendMessageContext request) { return false; } @Nullable @Override - public String getConversationId(SendMessageContext sendMessageContext) { + public String getConversationId(SendMessageContext request) { return null; } @Nullable @Override - public Long getMessagePayloadSize(SendMessageContext sendMessageContext) { + public Long getMessageBodySize(SendMessageContext request) { return null; } @Nullable @Override - public Long getMessagePayloadCompressedSize(SendMessageContext sendMessageContext) { + public Long getMessageEnvelopeSize(SendMessageContext request) { return null; } @@ -59,6 +70,18 @@ public String getMessageId(SendMessageContext request, @Nullable Void unused) { return sendResult == null ? null : sendResult.getMsgId(); } + @Nullable + @Override + public String getClientId(SendMessageContext request) { + return null; + } + + @Nullable + @Override + public Long getBatchMessageCount(SendMessageContext request, @Nullable Void unused) { + return null; + } + @Override public List getMessageHeader(SendMessageContext request, String name) { String value = request.getMessage().getProperties().get(name); diff --git a/instrumentation/rocketmq/rocketmq-client/rocketmq-client-4.8/library/src/main/java/io/opentelemetry/instrumentation/rocketmqclient/v4_8/RocketMqProducerExperimentalAttributeExtractor.java b/instrumentation/rocketmq/rocketmq-client/rocketmq-client-4.8/library/src/main/java/io/opentelemetry/instrumentation/rocketmqclient/v4_8/RocketMqProducerExperimentalAttributeExtractor.java index b0a55ae4a87e..11dfd668f6c5 100644 --- a/instrumentation/rocketmq/rocketmq-client/rocketmq-client-4.8/library/src/main/java/io/opentelemetry/instrumentation/rocketmqclient/v4_8/RocketMqProducerExperimentalAttributeExtractor.java +++ b/instrumentation/rocketmq/rocketmq-client/rocketmq-client-4.8/library/src/main/java/io/opentelemetry/instrumentation/rocketmqclient/v4_8/RocketMqProducerExperimentalAttributeExtractor.java @@ -9,7 +9,6 @@ import io.opentelemetry.api.common.AttributesBuilder; import io.opentelemetry.context.Context; import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; import javax.annotation.Nullable; import org.apache.rocketmq.client.hook.SendMessageContext; @@ -17,6 +16,10 @@ enum RocketMqProducerExperimentalAttributeExtractor implements AttributesExtractor { INSTANCE; + // copied from MessagingIncubatingAttributes + private static final AttributeKey MESSAGING_ROCKETMQ_MESSAGE_TAG = + AttributeKey.stringKey("messaging.rocketmq.message.tag"); + private static final AttributeKey MESSAGING_ROCKETMQ_BROKER_ADDRESS = AttributeKey.stringKey("messaging.rocketmq.broker_address"); private static final AttributeKey MESSAGING_ROCKETMQ_SEND_RESULT = @@ -28,7 +31,7 @@ public void onStart( if (request.getMessage() != null) { String tags = request.getMessage().getTags(); if (tags != null) { - attributes.put(SemanticAttributes.MESSAGING_ROCKETMQ_MESSAGE_TAG, tags); + attributes.put(MESSAGING_ROCKETMQ_MESSAGE_TAG, tags); } } String brokerAddr = request.getBrokerAddr(); diff --git a/instrumentation/rocketmq/rocketmq-client/rocketmq-client-4.8/library/src/main/java/io/opentelemetry/instrumentation/rocketmqclient/v4_8/RocketMqTelemetry.java b/instrumentation/rocketmq/rocketmq-client/rocketmq-client-4.8/library/src/main/java/io/opentelemetry/instrumentation/rocketmqclient/v4_8/RocketMqTelemetry.java index 750054563d03..e13f8eb2225e 100644 --- a/instrumentation/rocketmq/rocketmq-client/rocketmq-client-4.8/library/src/main/java/io/opentelemetry/instrumentation/rocketmqclient/v4_8/RocketMqTelemetry.java +++ b/instrumentation/rocketmq/rocketmq-client/rocketmq-client-4.8/library/src/main/java/io/opentelemetry/instrumentation/rocketmqclient/v4_8/RocketMqTelemetry.java @@ -33,14 +33,13 @@ public static RocketMqTelemetryBuilder builder(OpenTelemetry openTelemetry) { RocketMqTelemetry( OpenTelemetry openTelemetry, List capturedHeaders, - boolean captureExperimentalSpanAttributes, - boolean propagationEnabled) { + boolean captureExperimentalSpanAttributes) { rocketMqConsumerInstrumenter = RocketMqInstrumenterFactory.createConsumerInstrumenter( - openTelemetry, capturedHeaders, captureExperimentalSpanAttributes, propagationEnabled); + openTelemetry, capturedHeaders, captureExperimentalSpanAttributes); rocketMqProducerInstrumenter = RocketMqInstrumenterFactory.createProducerInstrumenter( - openTelemetry, capturedHeaders, captureExperimentalSpanAttributes, propagationEnabled); + openTelemetry, capturedHeaders, captureExperimentalSpanAttributes); } /** diff --git a/instrumentation/rocketmq/rocketmq-client/rocketmq-client-4.8/library/src/main/java/io/opentelemetry/instrumentation/rocketmqclient/v4_8/RocketMqTelemetryBuilder.java b/instrumentation/rocketmq/rocketmq-client/rocketmq-client-4.8/library/src/main/java/io/opentelemetry/instrumentation/rocketmqclient/v4_8/RocketMqTelemetryBuilder.java index 264b564f5f0a..856969aa8ade 100644 --- a/instrumentation/rocketmq/rocketmq-client/rocketmq-client-4.8/library/src/main/java/io/opentelemetry/instrumentation/rocketmqclient/v4_8/RocketMqTelemetryBuilder.java +++ b/instrumentation/rocketmq/rocketmq-client/rocketmq-client-4.8/library/src/main/java/io/opentelemetry/instrumentation/rocketmqclient/v4_8/RocketMqTelemetryBuilder.java @@ -18,7 +18,6 @@ public final class RocketMqTelemetryBuilder { private List capturedHeaders = emptyList(); private boolean captureExperimentalSpanAttributes; - private boolean propagationEnabled = true; RocketMqTelemetryBuilder(OpenTelemetry openTelemetry) { this.openTelemetry = openTelemetry; @@ -36,21 +35,6 @@ public RocketMqTelemetryBuilder setCaptureExperimentalSpanAttributes( return this; } - /** - * Sets whether the trace context should be written from producers / read from consumers for - * propagating through messaging. - * - * @deprecated if you have a need for this configuration option please open an issue in the opentelemetry-java-instrumentation - * repository. - */ - @Deprecated - @CanIgnoreReturnValue - public RocketMqTelemetryBuilder setPropagationEnabled(boolean propagationEnabled) { - this.propagationEnabled = propagationEnabled; - return this; - } - /** * Configures the messaging headers that will be captured as span attributes. * @@ -67,7 +51,6 @@ public RocketMqTelemetryBuilder setCapturedHeaders(List capturedHeaders) * RocketMqTelemetryBuilder}. */ public RocketMqTelemetry build() { - return new RocketMqTelemetry( - openTelemetry, capturedHeaders, captureExperimentalSpanAttributes, propagationEnabled); + return new RocketMqTelemetry(openTelemetry, capturedHeaders, captureExperimentalSpanAttributes); } } diff --git a/instrumentation/rocketmq/rocketmq-client/rocketmq-client-4.8/library/src/test/groovy/io/opentelemetry/instrumentation/rocketmqclient/v4_8/RocketMqClientTest.groovy b/instrumentation/rocketmq/rocketmq-client/rocketmq-client-4.8/library/src/test/groovy/io/opentelemetry/instrumentation/rocketmqclient/v4_8/RocketMqClientTest.groovy deleted file mode 100644 index ae1e73136b8a..000000000000 --- a/instrumentation/rocketmq/rocketmq-client/rocketmq-client-4.8/library/src/test/groovy/io/opentelemetry/instrumentation/rocketmqclient/v4_8/RocketMqClientTest.groovy +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.rocketmqclient.v4_8 - - -import io.opentelemetry.instrumentation.test.LibraryTestTrait -import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer -import org.apache.rocketmq.client.producer.DefaultMQProducer - -import static java.util.Collections.singletonList - -class RocketMqClientTest extends AbstractRocketMqClientTest implements LibraryTestTrait { - - @Override - void configureMQProducer(DefaultMQProducer producer) { - producer.getDefaultMQProducerImpl().registerSendMessageHook(RocketMqTelemetry.builder(openTelemetry) - .setCapturedHeaders(singletonList("test-message-header")) - .setCaptureExperimentalSpanAttributes(true) - .build().newTracingSendMessageHook()) - } - - @Override - void configureMQPushConsumer(DefaultMQPushConsumer consumer) { - consumer.getDefaultMQPushConsumerImpl().registerConsumeMessageHook(RocketMqTelemetry.builder(openTelemetry) - .setCapturedHeaders(singletonList("test-message-header")) - .setCaptureExperimentalSpanAttributes(true) - .build().newTracingConsumeMessageHook()) - } -} diff --git a/instrumentation/rocketmq/rocketmq-client/rocketmq-client-4.8/library/src/test/java/io/opentelemetry/instrumentation/rocketmqclient/v4_8/RocketMqClientTest.java b/instrumentation/rocketmq/rocketmq-client/rocketmq-client-4.8/library/src/test/java/io/opentelemetry/instrumentation/rocketmqclient/v4_8/RocketMqClientTest.java new file mode 100644 index 000000000000..28b6fcd3c63d --- /dev/null +++ b/instrumentation/rocketmq/rocketmq-client/rocketmq-client-4.8/library/src/test/java/io/opentelemetry/instrumentation/rocketmqclient/v4_8/RocketMqClientTest.java @@ -0,0 +1,53 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.rocketmqclient.v4_8; + +import static java.util.Collections.singletonList; + +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.LibraryInstrumentationExtension; +import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer; +import org.apache.rocketmq.client.producer.DefaultMQProducer; +import org.junit.jupiter.api.extension.RegisterExtension; + +class RocketMqClientTest extends AbstractRocketMqClientTest { + + @RegisterExtension + private static final InstrumentationExtension testing = LibraryInstrumentationExtension.create(); + + @Override + InstrumentationExtension testing() { + return testing; + } + + @Override + @SuppressWarnings("deprecation") + // testing instrumentation of deprecated class + void configureMqProducer(DefaultMQProducer producer) { + producer + .getDefaultMQProducerImpl() + .registerSendMessageHook( + RocketMqTelemetry.builder(testing.getOpenTelemetry()) + .setCapturedHeaders(singletonList("test-message-header")) + .setCaptureExperimentalSpanAttributes(true) + .build() + .newTracingSendMessageHook()); + } + + @Override + @SuppressWarnings("deprecation") + // testing instrumentation of deprecated class + void configureMqPushConsumer(DefaultMQPushConsumer consumer) { + consumer + .getDefaultMQPushConsumerImpl() + .registerConsumeMessageHook( + RocketMqTelemetry.builder(testing.getOpenTelemetry()) + .setCapturedHeaders(singletonList("test-message-header")) + .setCaptureExperimentalSpanAttributes(true) + .build() + .newTracingConsumeMessageHook()); + } +} diff --git a/instrumentation/rocketmq/rocketmq-client/rocketmq-client-4.8/testing/build.gradle.kts b/instrumentation/rocketmq/rocketmq-client/rocketmq-client-4.8/testing/build.gradle.kts index aadde8471a58..3b2d84ec2c60 100644 --- a/instrumentation/rocketmq/rocketmq-client/rocketmq-client-4.8/testing/build.gradle.kts +++ b/instrumentation/rocketmq/rocketmq-client/rocketmq-client-4.8/testing/build.gradle.kts @@ -7,7 +7,5 @@ dependencies { implementation("org.apache.rocketmq:rocketmq-test:4.8.0") implementation("com.google.guava:guava") - implementation("org.apache.groovy:groovy") implementation("io.opentelemetry:opentelemetry-api") - implementation("org.spockframework:spock-core") } diff --git a/instrumentation/rocketmq/rocketmq-client/rocketmq-client-4.8/testing/src/main/groovy/io/opentelemetry/instrumentation/rocketmqclient/v4_8/AbstractRocketMqClientTest.groovy b/instrumentation/rocketmq/rocketmq-client/rocketmq-client-4.8/testing/src/main/groovy/io/opentelemetry/instrumentation/rocketmqclient/v4_8/AbstractRocketMqClientTest.groovy deleted file mode 100644 index b34fd6551dc1..000000000000 --- a/instrumentation/rocketmq/rocketmq-client/rocketmq-client-4.8/testing/src/main/groovy/io/opentelemetry/instrumentation/rocketmqclient/v4_8/AbstractRocketMqClientTest.groovy +++ /dev/null @@ -1,355 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.rocketmqclient.v4_8 - -import io.opentelemetry.instrumentation.rocketmqclient.v4_8.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.client.producer.SendStatus -import org.apache.rocketmq.common.message.Message -import org.apache.rocketmq.remoting.common.RemotingHelper -import spock.lang.Shared -import spock.lang.Unroll - -import java.util.concurrent.CompletableFuture -import java.util.concurrent.TimeUnit - -import static io.opentelemetry.api.trace.SpanKind.CONSUMER -import static io.opentelemetry.api.trace.SpanKind.INTERNAL -import static io.opentelemetry.api.trace.SpanKind.PRODUCER - -//TODO add tests for propagationEnabled flag -@Unroll -abstract class AbstractRocketMqClientTest extends InstrumentationSpecification { - - @Shared - DefaultMQProducer producer - - @Shared - DefaultMQPushConsumer consumer - - @Shared - String sharedTopic - - @Shared - Message msg - - @Shared - def msgs = new ArrayList() - - @Shared - TracingMessageListener tracingMessageListener = new TracingMessageListener() - - 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, "*", tracingMessageListener) - configureMQPushConsumer(consumer) - - // for RocketMQ 5.x wait a bit to ensure that consumer is properly started up - if (Boolean.getBoolean("testLatestDeps")) { - Thread.sleep(30_000) - } - } - - def cleanupSpec() { - producer?.shutdown() - consumer?.shutdown() - BaseConf.deleteTempDir() - } - - def setup() { - tracingMessageListener.reset() - } - - def "test rocketmq produce callback"() { - CompletableFuture result = new CompletableFuture<>() - when: - producer.send(msg, new SendCallback() { - @Override - void onSuccess(SendResult sendResult) { - result.complete(sendResult) - } - - @Override - void onException(Throwable throwable) { - result.completeExceptionally(throwable) - } - }) - result.get(10, TimeUnit.SECONDS).sendStatus == SendStatus.SEND_OK - // waiting longer than assertTraces below does on its own because of CI flakiness - tracingMessageListener.waitForMessages() - - then: - assertTraces(1) { - trace(0, 3) { - span(0) { - name sharedTopic + " send" - kind PRODUCER - attributes { - "$SemanticAttributes.MESSAGING_SYSTEM" "rocketmq" - "$SemanticAttributes.MESSAGING_DESTINATION_NAME" sharedTopic - "$SemanticAttributes.MESSAGING_MESSAGE_ID" String - "$SemanticAttributes.MESSAGING_ROCKETMQ_MESSAGE_TAG" "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" "rocketmq" - "$SemanticAttributes.MESSAGING_DESTINATION_NAME" sharedTopic - "$SemanticAttributes.MESSAGING_OPERATION" "process" - "$SemanticAttributes.MESSAGING_MESSAGE_PAYLOAD_SIZE_BYTES" Long - "$SemanticAttributes.MESSAGING_MESSAGE_ID" String - "$SemanticAttributes.MESSAGING_ROCKETMQ_MESSAGE_TAG" "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") { - SendResult sendResult = producer.send(msg) - assert sendResult.sendStatus == SendStatus.SEND_OK - } - // waiting longer than assertTraces below does on its own because of CI flakiness - tracingMessageListener.waitForMessages() - - 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" "rocketmq" - "$SemanticAttributes.MESSAGING_DESTINATION_NAME" sharedTopic - "$SemanticAttributes.MESSAGING_MESSAGE_ID" String - "$SemanticAttributes.MESSAGING_ROCKETMQ_MESSAGE_TAG" "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" "rocketmq" - "$SemanticAttributes.MESSAGING_DESTINATION_NAME" sharedTopic - "$SemanticAttributes.MESSAGING_OPERATION" "process" - "$SemanticAttributes.MESSAGING_MESSAGE_PAYLOAD_SIZE_BYTES" Long - "$SemanticAttributes.MESSAGING_MESSAGE_ID" String - "$SemanticAttributes.MESSAGING_ROCKETMQ_MESSAGE_TAG" "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: - // This test assumes that messages are sent and received as a batch. Occasionally it happens - // that the messages are not received as a batch, but one by one. This doesn't match what the - // assertion expects. To reduce flakiness we retry the test when messages weren't received as - // a batch. - def maxAttempts = 5 - for (i in 1..maxAttempts) { - tracingMessageListener.reset() - - runWithSpan("parent") { - producer.send(msgs) - } - - tracingMessageListener.waitForMessages() - if (tracingMessageListener.getLastBatchSize() == 2) { - break - } else if (i < maxAttempts) { - // if messages weren't received as a batch we get 1 trace instead of 2 - ignoreTracesAndClear(1) - System.err.println("Messages weren't received as batch, retrying") - } - } - - 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" "rocketmq" - "$SemanticAttributes.MESSAGING_DESTINATION_NAME" sharedTopic - "$SemanticAttributes.MESSAGING_MESSAGE_ID" 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" "rocketmq" - "$SemanticAttributes.MESSAGING_OPERATION" "receive" - } - } - span(1) { - name sharedTopic + " process" - kind CONSUMER - attributes { - "$SemanticAttributes.MESSAGING_SYSTEM" "rocketmq" - "$SemanticAttributes.MESSAGING_DESTINATION_NAME" sharedTopic - "$SemanticAttributes.MESSAGING_OPERATION" "process" - "$SemanticAttributes.MESSAGING_MESSAGE_PAYLOAD_SIZE_BYTES" Long - "$SemanticAttributes.MESSAGING_MESSAGE_ID" String - "$SemanticAttributes.MESSAGING_ROCKETMQ_MESSAGE_TAG" "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" "rocketmq" - "$SemanticAttributes.MESSAGING_DESTINATION_NAME" sharedTopic - "$SemanticAttributes.MESSAGING_OPERATION" "process" - "$SemanticAttributes.MESSAGING_MESSAGE_PAYLOAD_SIZE_BYTES" Long - "$SemanticAttributes.MESSAGING_MESSAGE_ID" String - "$SemanticAttributes.MESSAGING_ROCKETMQ_MESSAGE_TAG" "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) - } - } - } - } - - def "capture message header as span attributes"() { - when: - runWithSpan("parent") { - def msg = new Message(sharedTopic, "TagA", ("Hello RocketMQ").getBytes(RemotingHelper.DEFAULT_CHARSET)) - msg.putUserProperty("test-message-header", "test") - SendResult sendResult = producer.send(msg) - assert sendResult.sendStatus == SendStatus.SEND_OK - } - // waiting longer than assertTraces below does on its own because of CI flakiness - tracingMessageListener.waitForMessages() - - 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" "rocketmq" - "$SemanticAttributes.MESSAGING_DESTINATION_NAME" sharedTopic - "$SemanticAttributes.MESSAGING_MESSAGE_ID" String - "$SemanticAttributes.MESSAGING_ROCKETMQ_MESSAGE_TAG" "TagA" - "messaging.rocketmq.broker_address" String - "messaging.rocketmq.send_result" "SEND_OK" - "messaging.header.test_message_header" { it == ["test"] } - } - } - span(2) { - name sharedTopic + " process" - kind CONSUMER - childOf span(1) - attributes { - "$SemanticAttributes.MESSAGING_SYSTEM" "rocketmq" - "$SemanticAttributes.MESSAGING_DESTINATION_NAME" sharedTopic - "$SemanticAttributes.MESSAGING_OPERATION" "process" - "$SemanticAttributes.MESSAGING_MESSAGE_PAYLOAD_SIZE_BYTES" Long - "$SemanticAttributes.MESSAGING_MESSAGE_ID" String - "$SemanticAttributes.MESSAGING_ROCKETMQ_MESSAGE_TAG" "TagA" - "messaging.rocketmq.broker_address" String - "messaging.rocketmq.queue_id" Long - "messaging.rocketmq.queue_offset" Long - "messaging.header.test_message_header" { it == ["test"] } - } - } - span(3) { - name "messageListener" - kind INTERNAL - childOf span(2) - } - } - } - } -} diff --git a/instrumentation/rocketmq/rocketmq-client/rocketmq-client-4.8/testing/src/main/groovy/io/opentelemetry/instrumentation/rocketmqclient/v4_8/TracingMessageListener.groovy b/instrumentation/rocketmq/rocketmq-client/rocketmq-client-4.8/testing/src/main/groovy/io/opentelemetry/instrumentation/rocketmqclient/v4_8/TracingMessageListener.groovy deleted file mode 100644 index b2fa0781bed5..000000000000 --- a/instrumentation/rocketmq/rocketmq-client/rocketmq-client-4.8/testing/src/main/groovy/io/opentelemetry/instrumentation/rocketmqclient/v4_8/TracingMessageListener.groovy +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.rocketmqclient.v4_8 - -import java.util.concurrent.TimeUnit -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 java.util.concurrent.CountDownLatch -import java.util.concurrent.atomic.AtomicInteger - -import static io.opentelemetry.instrumentation.testing.GlobalTraceUtil.runWithSpan - -class TracingMessageListener implements MessageListenerOrderly { - private AtomicInteger lastBatchSize = new AtomicInteger() - private CountDownLatch messageReceived = new CountDownLatch(1) - - @Override - ConsumeOrderlyStatus consumeMessage(List list, ConsumeOrderlyContext consumeOrderlyContext) { - lastBatchSize.set(list.size()) - messageReceived.countDown() - runWithSpan("messageListener") {} - return ConsumeOrderlyStatus.SUCCESS - } - - void reset() { - messageReceived = new CountDownLatch(1) - lastBatchSize.set(0) - } - - void waitForMessages() { - messageReceived.await(30, TimeUnit.SECONDS) - } - - int getLastBatchSize() { - return lastBatchSize.get() - } -} diff --git a/instrumentation/rocketmq/rocketmq-client/rocketmq-client-4.8/testing/src/main/java/io/opentelemetry/instrumentation/rocketmqclient/v4_8/AbstractRocketMqClientTest.java b/instrumentation/rocketmq/rocketmq-client/rocketmq-client-4.8/testing/src/main/java/io/opentelemetry/instrumentation/rocketmqclient/v4_8/AbstractRocketMqClientTest.java new file mode 100644 index 000000000000..60b277d7a46d --- /dev/null +++ b/instrumentation/rocketmq/rocketmq-client/rocketmq-client-4.8/testing/src/main/java/io/opentelemetry/instrumentation/rocketmqclient/v4_8/AbstractRocketMqClientTest.java @@ -0,0 +1,511 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.rocketmqclient.v4_8; + +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.satisfies; +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.rocketmqclient.v4_8.base.BaseConf; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.sdk.trace.data.LinkData; +import io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; +import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer; +import org.apache.rocketmq.client.exception.MQClientException; +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.client.producer.SendStatus; +import org.apache.rocketmq.common.message.Message; +import org.apache.rocketmq.remoting.exception.RemotingException; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** TODO add tests for propagationEnabled flag */ +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +abstract class AbstractRocketMqClientTest { + + private static final Logger logger = LoggerFactory.getLogger(AbstractRocketMqClientTest.class); + + private DefaultMQProducer producer; + + private DefaultMQPushConsumer consumer; + + private String sharedTopic; + + private Message msg; + + private final List msgs = new ArrayList<>(); + + private final TracingMessageListener tracingMessageListener = new TracingMessageListener(); + + abstract InstrumentationExtension testing(); + + abstract void configureMqProducer(DefaultMQProducer producer); + + abstract void configureMqPushConsumer(DefaultMQPushConsumer consumer); + + @BeforeAll + void setup() throws MQClientException, InterruptedException { + sharedTopic = BaseConf.initTopic(); + msg = new Message(sharedTopic, "TagA", "Hello RocketMQ".getBytes(Charset.defaultCharset())); + Message msg1 = + new Message(sharedTopic, "TagA", "hello world a".getBytes(Charset.defaultCharset())); + Message msg2 = + new Message(sharedTopic, "TagB", "hello world b".getBytes(Charset.defaultCharset())); + msgs.add(msg1); + msgs.add(msg2); + producer = BaseConf.getProducer(BaseConf.nsAddr); + configureMqProducer(producer); + consumer = BaseConf.getConsumer(BaseConf.nsAddr, sharedTopic, "*", tracingMessageListener); + configureMqPushConsumer(consumer); + + // for RocketMQ 5.x wait a bit to ensure that consumer is properly started up + if (Boolean.getBoolean("testLatestDeps")) { + Thread.sleep(30_000); + } + } + + @AfterAll + void cleanup() { + if (producer != null) { + producer.shutdown(); + } + if (consumer != null) { + consumer.shutdown(); + } + BaseConf.deleteTempDir(); + } + + @BeforeEach + void resetTest() { + tracingMessageListener.reset(); + } + + @Test + void testRocketmqProduceCallback() + throws RemotingException, + InterruptedException, + MQClientException, + ExecutionException, + TimeoutException { + CompletableFuture result = new CompletableFuture<>(); + producer.send( + msg, + new SendCallback() { + @Override + public void onSuccess(SendResult sendResult) { + result.complete(sendResult); + } + + @Override + public void onException(Throwable throwable) { + result.completeExceptionally(throwable); + } + }); + SendResult sendResult = result.get(10, TimeUnit.SECONDS); + assertEquals(SendStatus.SEND_OK, sendResult.getSendStatus(), "Send status should be SEND_OK"); + // waiting longer than assertTraces below does on its own because of CI flakiness + tracingMessageListener.waitForMessages(); + + testing() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName(sharedTopic + " publish") + .hasKind(SpanKind.PRODUCER) + .hasAttributesSatisfyingExactly( + equalTo(MessagingIncubatingAttributes.MESSAGING_SYSTEM, "rocketmq"), + equalTo( + MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME, + sharedTopic), + equalTo( + MessagingIncubatingAttributes.MESSAGING_OPERATION, "publish"), + satisfies( + MessagingIncubatingAttributes.MESSAGING_MESSAGE_ID, + val -> val.isInstanceOf(String.class)), + equalTo( + MessagingIncubatingAttributes.MESSAGING_ROCKETMQ_MESSAGE_TAG, + "TagA"), + satisfies( + AttributeKey.stringKey("messaging.rocketmq.broker_address"), + val -> val.isInstanceOf(String.class)), + equalTo( + AttributeKey.stringKey("messaging.rocketmq.send_result"), + "SEND_OK")), + span -> + span.hasName(sharedTopic + " process") + .hasKind(SpanKind.CONSUMER) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo(MessagingIncubatingAttributes.MESSAGING_SYSTEM, "rocketmq"), + equalTo( + MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME, + sharedTopic), + equalTo( + MessagingIncubatingAttributes.MESSAGING_OPERATION, "process"), + satisfies( + MessagingIncubatingAttributes.MESSAGING_MESSAGE_BODY_SIZE, + val -> val.isInstanceOf(Long.class)), + satisfies( + MessagingIncubatingAttributes.MESSAGING_MESSAGE_ID, + val -> val.isInstanceOf(String.class)), + equalTo( + MessagingIncubatingAttributes.MESSAGING_ROCKETMQ_MESSAGE_TAG, + "TagA"), + satisfies( + AttributeKey.stringKey("messaging.rocketmq.broker_address"), + val -> val.isInstanceOf(String.class)), + satisfies( + AttributeKey.longKey("messaging.rocketmq.queue_id"), + val -> val.isInstanceOf(Long.class)), + satisfies( + AttributeKey.longKey("messaging.rocketmq.queue_offset"), + val -> val.isInstanceOf(Long.class))), + span -> + span.hasName("messageListener") + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(1)))); + } + + @Test + void testRocketmqProduceAndConsume() throws Exception { + testing() + .runWithSpan( + "parent", + () -> { + SendResult sendResult = producer.send(msg); + assertEquals( + SendStatus.SEND_OK, sendResult.getSendStatus(), "Send status should be SEND_OK"); + }); + // waiting longer than assertTraces below does on its own because of CI flakiness + tracingMessageListener.waitForMessages(); + + testing() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasKind(SpanKind.INTERNAL), + span -> + span.hasName(sharedTopic + " publish") + .hasKind(SpanKind.PRODUCER) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo(MessagingIncubatingAttributes.MESSAGING_SYSTEM, "rocketmq"), + equalTo( + MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME, + sharedTopic), + equalTo( + MessagingIncubatingAttributes.MESSAGING_OPERATION, "publish"), + satisfies( + MessagingIncubatingAttributes.MESSAGING_MESSAGE_ID, + val -> val.isInstanceOf(String.class)), + equalTo( + MessagingIncubatingAttributes.MESSAGING_ROCKETMQ_MESSAGE_TAG, + "TagA"), + satisfies( + AttributeKey.stringKey("messaging.rocketmq.broker_address"), + val -> val.isInstanceOf(String.class)), + equalTo( + AttributeKey.stringKey("messaging.rocketmq.send_result"), + "SEND_OK")), + span -> + span.hasName(sharedTopic + " process") + .hasKind(SpanKind.CONSUMER) + .hasParent(trace.getSpan(1)) + .hasAttributesSatisfyingExactly( + equalTo(MessagingIncubatingAttributes.MESSAGING_SYSTEM, "rocketmq"), + equalTo( + MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME, + sharedTopic), + equalTo( + MessagingIncubatingAttributes.MESSAGING_OPERATION, "process"), + satisfies( + MessagingIncubatingAttributes.MESSAGING_MESSAGE_BODY_SIZE, + val -> val.isInstanceOf(Long.class)), + satisfies( + MessagingIncubatingAttributes.MESSAGING_MESSAGE_ID, + val -> val.isInstanceOf(String.class)), + equalTo( + MessagingIncubatingAttributes.MESSAGING_ROCKETMQ_MESSAGE_TAG, + "TagA"), + satisfies( + AttributeKey.stringKey("messaging.rocketmq.broker_address"), + val -> val.isInstanceOf(String.class)), + satisfies( + AttributeKey.longKey("messaging.rocketmq.queue_id"), + val -> val.isInstanceOf(Long.class)), + satisfies( + AttributeKey.longKey("messaging.rocketmq.queue_offset"), + val -> val.isInstanceOf(Long.class))), + span -> + span.hasName("messageListener") + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(2)))); + } + + @Test + void testRocketmqProduceAndBatchConsume() throws Exception { + consumer.setConsumeMessageBatchMaxSize(2); + // This test assumes that messages are sent and received as a batch. Occasionally it happens + // that the messages are not received as a batch, but one by one. This doesn't match what the + // assertion expects. To reduce flakiness we retry the test when messages weren't received as + // a batch. + int maxAttempts = 5; + for (int i = 0; i < maxAttempts; i++) { + tracingMessageListener.reset(); + + testing().runWithSpan("parent", () -> producer.send(msgs)); + + tracingMessageListener.waitForMessages(); + if (tracingMessageListener.getLastBatchSize() == 2) { + break; + } else if (i < maxAttempts) { + // if messages weren't received as a batch we get 1 trace instead of 2 + testing().waitForTraces(1); + Thread.sleep(2_000); + testing().clearData(); + logger.error("Messages weren't received as batch, retrying"); + } + } + + AtomicReference producerSpanContext = new AtomicReference<>(); + testing() + .waitAndAssertTraces( + trace -> { + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasKind(SpanKind.INTERNAL), + span -> + span.hasName(sharedTopic + " publish") + .hasKind(SpanKind.PRODUCER) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo(MessagingIncubatingAttributes.MESSAGING_SYSTEM, "rocketmq"), + equalTo( + MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME, + sharedTopic), + equalTo(MessagingIncubatingAttributes.MESSAGING_OPERATION, "publish"), + satisfies( + MessagingIncubatingAttributes.MESSAGING_MESSAGE_ID, + val -> val.isInstanceOf(String.class)), + satisfies( + AttributeKey.stringKey("messaging.rocketmq.broker_address"), + val -> val.isInstanceOf(String.class)), + equalTo( + AttributeKey.stringKey("messaging.rocketmq.send_result"), + "SEND_OK"))); + + SpanContext spanContext = trace.getSpan(1).getSpanContext(); + producerSpanContext.set( + SpanContext.createFromRemoteParent( + spanContext.getTraceId(), + spanContext.getSpanId(), + spanContext.getTraceFlags(), + spanContext.getTraceState())); + }, + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("multiple_sources receive") + .hasKind(SpanKind.CONSUMER) + .hasAttributesSatisfyingExactly( + equalTo(MessagingIncubatingAttributes.MESSAGING_SYSTEM, "rocketmq"), + equalTo( + MessagingIncubatingAttributes.MESSAGING_OPERATION, "receive")), + span -> + span.hasName(sharedTopic + " process") + .hasKind(SpanKind.CONSUMER) + .hasParent(trace.getSpan(0)) + .hasLinksSatisfying(links(producerSpanContext.get())) + .hasAttributesSatisfyingExactly( + equalTo(MessagingIncubatingAttributes.MESSAGING_SYSTEM, "rocketmq"), + equalTo( + MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME, + sharedTopic), + equalTo( + MessagingIncubatingAttributes.MESSAGING_OPERATION, "process"), + satisfies( + MessagingIncubatingAttributes.MESSAGING_MESSAGE_BODY_SIZE, + val -> val.isInstanceOf(Long.class)), + satisfies( + MessagingIncubatingAttributes.MESSAGING_MESSAGE_ID, + val -> val.isInstanceOf(String.class)), + equalTo( + MessagingIncubatingAttributes.MESSAGING_ROCKETMQ_MESSAGE_TAG, + "TagA"), + satisfies( + AttributeKey.stringKey("messaging.rocketmq.broker_address"), + val -> val.isNotEmpty()), + satisfies( + AttributeKey.longKey("messaging.rocketmq.queue_id"), + val -> val.isNotNull()), + satisfies( + AttributeKey.longKey("messaging.rocketmq.queue_offset"), + val -> val.isNotNull())), + span -> + span.hasName(sharedTopic + " process") + .hasKind(SpanKind.CONSUMER) + .hasParent(trace.getSpan(0)) + .hasLinksSatisfying(links(producerSpanContext.get())) + .hasAttributesSatisfyingExactly( + equalTo(MessagingIncubatingAttributes.MESSAGING_SYSTEM, "rocketmq"), + equalTo( + MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME, + sharedTopic), + equalTo( + MessagingIncubatingAttributes.MESSAGING_OPERATION, "process"), + satisfies( + MessagingIncubatingAttributes.MESSAGING_MESSAGE_BODY_SIZE, + val -> val.isInstanceOf(Long.class)), + satisfies( + MessagingIncubatingAttributes.MESSAGING_MESSAGE_ID, + val -> val.isInstanceOf(String.class)), + equalTo( + MessagingIncubatingAttributes.MESSAGING_ROCKETMQ_MESSAGE_TAG, + "TagB"), + satisfies( + AttributeKey.stringKey("messaging.rocketmq.broker_address"), + val -> val.isNotEmpty()), + satisfies( + AttributeKey.longKey("messaging.rocketmq.queue_id"), + val -> val.isNotNull()), + satisfies( + AttributeKey.longKey("messaging.rocketmq.queue_offset"), + val -> val.isNotNull())), + span -> + span.hasName("messageListener") + .hasParent(trace.getSpan(0)) + .hasKind(SpanKind.INTERNAL))); + } + + @Test + void captureMessageHeaderAsSpanAttributes() throws Exception { + tracingMessageListener.reset(); + testing() + .runWithSpan( + "parent", + () -> { + Message msg = + new Message( + sharedTopic, "TagA", "Hello RocketMQ".getBytes(Charset.defaultCharset())); + msg.putUserProperty("test-message-header", "test"); + SendResult sendResult = producer.send(msg); + assertEquals( + SendStatus.SEND_OK, sendResult.getSendStatus(), "Send status should be SEND_OK"); + }); + // waiting longer than assertTraces below does on its own because of CI flakiness + tracingMessageListener.waitForMessages(); + + testing() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasKind(SpanKind.INTERNAL), + span -> + span.hasName(sharedTopic + " publish") + .hasKind(SpanKind.PRODUCER) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo(MessagingIncubatingAttributes.MESSAGING_SYSTEM, "rocketmq"), + equalTo( + MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME, + sharedTopic), + equalTo( + MessagingIncubatingAttributes.MESSAGING_OPERATION, "publish"), + satisfies( + MessagingIncubatingAttributes.MESSAGING_MESSAGE_ID, + val -> val.isInstanceOf(String.class)), + equalTo( + MessagingIncubatingAttributes.MESSAGING_ROCKETMQ_MESSAGE_TAG, + "TagA"), + satisfies( + AttributeKey.stringKey("messaging.rocketmq.broker_address"), + val -> val.isInstanceOf(String.class)), + equalTo( + AttributeKey.stringKey("messaging.rocketmq.send_result"), + "SEND_OK"), + equalTo( + AttributeKey.stringArrayKey( + "messaging.header.test_message_header"), + singletonList("test"))), + span -> + span.hasName(sharedTopic + " process") + .hasKind(SpanKind.CONSUMER) + .hasParent(trace.getSpan(1)) + .hasAttributesSatisfyingExactly( + equalTo(MessagingIncubatingAttributes.MESSAGING_SYSTEM, "rocketmq"), + equalTo( + MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME, + sharedTopic), + equalTo( + MessagingIncubatingAttributes.MESSAGING_OPERATION, "process"), + satisfies( + MessagingIncubatingAttributes.MESSAGING_MESSAGE_BODY_SIZE, + val -> val.isInstanceOf(Long.class)), + satisfies( + MessagingIncubatingAttributes.MESSAGING_MESSAGE_ID, + val -> val.isInstanceOf(String.class)), + equalTo( + MessagingIncubatingAttributes.MESSAGING_ROCKETMQ_MESSAGE_TAG, + "TagA"), + satisfies( + AttributeKey.stringKey("messaging.rocketmq.broker_address"), + val -> val.isInstanceOf(String.class)), + satisfies( + AttributeKey.longKey("messaging.rocketmq.queue_id"), + val -> val.isInstanceOf(Long.class)), + satisfies( + AttributeKey.longKey("messaging.rocketmq.queue_offset"), + val -> val.isInstanceOf(Long.class)), + equalTo( + AttributeKey.stringArrayKey( + "messaging.header.test_message_header"), + singletonList("test"))), + span -> + span.hasName("messageListener") + .hasParent(trace.getSpan(2)) + .hasKind(SpanKind.INTERNAL))); + } + + private static Consumer> links(SpanContext... spanContexts) { + return links -> { + assertThat(links).hasSize(spanContexts.length); + for (SpanContext spanContext : spanContexts) { + assertThat(links) + .anySatisfy( + link -> { + assertThat(link.getSpanContext().getTraceId()) + .isEqualTo(spanContext.getTraceId()); + assertThat(link.getSpanContext().getSpanId()).isEqualTo(spanContext.getSpanId()); + assertThat(link.getSpanContext().getTraceFlags()) + .isEqualTo(spanContext.getTraceFlags()); + assertThat(link.getSpanContext().getTraceState()) + .isEqualTo(spanContext.getTraceState()); + }); + } + }; + } +} diff --git a/instrumentation/rocketmq/rocketmq-client/rocketmq-client-4.8/testing/src/main/java/io/opentelemetry/instrumentation/rocketmqclient/v4_8/TracingMessageListener.java b/instrumentation/rocketmq/rocketmq-client/rocketmq-client-4.8/testing/src/main/java/io/opentelemetry/instrumentation/rocketmqclient/v4_8/TracingMessageListener.java new file mode 100644 index 000000000000..916c9bba0032 --- /dev/null +++ b/instrumentation/rocketmq/rocketmq-client/rocketmq-client-4.8/testing/src/main/java/io/opentelemetry/instrumentation/rocketmqclient/v4_8/TracingMessageListener.java @@ -0,0 +1,45 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.rocketmqclient.v4_8; + +import static io.opentelemetry.instrumentation.testing.GlobalTraceUtil.runWithSpan; + +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +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; + +class TracingMessageListener implements MessageListenerOrderly { + + private final AtomicInteger lastBatchSize = new AtomicInteger(); + private CountDownLatch messageReceived = new CountDownLatch(1); + + @Override + public ConsumeOrderlyStatus consumeMessage( + List list, ConsumeOrderlyContext consumeOrderlyContext) { + lastBatchSize.set(list.size()); + messageReceived.countDown(); + runWithSpan("messageListener", () -> {}); + return ConsumeOrderlyStatus.SUCCESS; + } + + void reset() { + messageReceived = new CountDownLatch(1); + lastBatchSize.set(0); + } + + void waitForMessages() throws InterruptedException { + messageReceived.await(30, TimeUnit.SECONDS); + } + + int getLastBatchSize() { + return lastBatchSize.get(); + } +} diff --git a/instrumentation/rocketmq/rocketmq-client/rocketmq-client-5.0/javaagent/build.gradle.kts b/instrumentation/rocketmq/rocketmq-client/rocketmq-client-5.0/javaagent/build.gradle.kts index 2f87b75b3076..bb76d29aa456 100644 --- a/instrumentation/rocketmq/rocketmq-client/rocketmq-client-5.0/javaagent/build.gradle.kts +++ b/instrumentation/rocketmq/rocketmq-client/rocketmq-client-5.0/javaagent/build.gradle.kts @@ -30,6 +30,7 @@ tasks { excludeTestsMatching("RocketMqClientSuppressReceiveSpanTest") } jvmArgs("-Dotel.instrumentation.messaging.experimental.receive-telemetry.enabled=true") + jvmArgs("-Dotel.instrumentation.common.experimental.controller-telemetry.enabled=true") } check { diff --git a/instrumentation/rocketmq/rocketmq-client/rocketmq-client-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rocketmqclient/v5_0/ReceiveSpanFinishingCallback.java b/instrumentation/rocketmq/rocketmq-client/rocketmq-client-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rocketmqclient/v5_0/ReceiveSpanFinishingCallback.java index 94506501fda5..37d50ca66e89 100644 --- a/instrumentation/rocketmq/rocketmq-client/rocketmq-client-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rocketmqclient/v5_0/ReceiveSpanFinishingCallback.java +++ b/instrumentation/rocketmq/rocketmq-client/rocketmq-client-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rocketmqclient/v5_0/ReceiveSpanFinishingCallback.java @@ -27,6 +27,7 @@ public ReceiveSpanFinishingCallback(ReceiveMessageRequest request, Timer timer) } @Override + @SuppressWarnings("unchecked") public void onSuccess(ReceiveMessageResult receiveMessageResult) { List messageViews = receiveMessageResult.getMessageViewImpls(); // Don't create spans when no messages were received. @@ -46,7 +47,7 @@ public void onSuccess(ReceiveMessageResult receiveMessageResult) { receiveInstrumenter, parentContext, request, - null, + (List) (List) messageViews, null, timer.startTime(), timer.now()); diff --git a/instrumentation/rocketmq/rocketmq-client/rocketmq-client-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rocketmqclient/v5_0/RocketMqConsumerProcessAttributeExtractor.java b/instrumentation/rocketmq/rocketmq-client/rocketmq-client-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rocketmqclient/v5_0/RocketMqConsumerProcessAttributeExtractor.java index 8c7408b624aa..ce36d59cb5f1 100644 --- a/instrumentation/rocketmq/rocketmq-client/rocketmq-client-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rocketmqclient/v5_0/RocketMqConsumerProcessAttributeExtractor.java +++ b/instrumentation/rocketmq/rocketmq-client/rocketmq-client-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rocketmqclient/v5_0/RocketMqConsumerProcessAttributeExtractor.java @@ -5,11 +5,11 @@ package io.opentelemetry.javaagent.instrumentation.rocketmqclient.v5_0; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.MESSAGING_ROCKETMQ_CLIENT_GROUP; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.MESSAGING_ROCKETMQ_MESSAGE_DELIVERY_TIMESTAMP; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.MESSAGING_ROCKETMQ_MESSAGE_GROUP; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.MESSAGING_ROCKETMQ_MESSAGE_KEYS; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.MESSAGING_ROCKETMQ_MESSAGE_TAG; +import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_ROCKETMQ_CLIENT_GROUP; +import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_ROCKETMQ_MESSAGE_DELIVERY_TIMESTAMP; +import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_ROCKETMQ_MESSAGE_GROUP; +import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_ROCKETMQ_MESSAGE_KEYS; +import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_ROCKETMQ_MESSAGE_TAG; import io.opentelemetry.api.common.AttributesBuilder; import io.opentelemetry.context.Context; diff --git a/instrumentation/rocketmq/rocketmq-client/rocketmq-client-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rocketmqclient/v5_0/RocketMqConsumerProcessAttributeGetter.java b/instrumentation/rocketmq/rocketmq-client/rocketmq-client-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rocketmqclient/v5_0/RocketMqConsumerProcessAttributeGetter.java index df7a7bf97aa9..0f7f98b09ce2 100644 --- a/instrumentation/rocketmq/rocketmq-client/rocketmq-client-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rocketmqclient/v5_0/RocketMqConsumerProcessAttributeGetter.java +++ b/instrumentation/rocketmq/rocketmq-client/rocketmq-client-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rocketmqclient/v5_0/RocketMqConsumerProcessAttributeGetter.java @@ -5,7 +5,7 @@ package io.opentelemetry.javaagent.instrumentation.rocketmqclient.v5_0; -import io.opentelemetry.instrumentation.api.instrumenter.messaging.MessagingAttributesGetter; +import io.opentelemetry.instrumentation.api.incubator.semconv.messaging.MessagingAttributesGetter; import java.util.Collections; import java.util.List; import javax.annotation.Nullable; @@ -28,25 +28,37 @@ public String getDestination(MessageView messageView) { return messageView.getTopic(); } + @Nullable + @Override + public String getDestinationTemplate(MessageView messageView) { + return null; + } + @Override public boolean isTemporaryDestination(MessageView messageView) { return false; } + @Override + public boolean isAnonymousDestination(MessageView messageView) { + return false; + } + @Nullable @Override public String getConversationId(MessageView messageView) { return null; } + @Nullable @Override - public Long getMessagePayloadSize(MessageView messageView) { + public Long getMessageBodySize(MessageView messageView) { return (long) messageView.getBody().remaining(); } @Nullable @Override - public Long getMessagePayloadCompressedSize(MessageView messageView) { + public Long getMessageEnvelopeSize(MessageView messageView) { return null; } @@ -56,6 +68,18 @@ public String getMessageId(MessageView messageView, @Nullable ConsumeResult unus return messageView.getMessageId().toString(); } + @Nullable + @Override + public String getClientId(MessageView messageView) { + return null; + } + + @Nullable + @Override + public Long getBatchMessageCount(MessageView messageView, @Nullable ConsumeResult unused) { + return null; + } + @Override public List getMessageHeader(MessageView messageView, String name) { String value = messageView.getProperties().get(name); diff --git a/instrumentation/rocketmq/rocketmq-client/rocketmq-client-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rocketmqclient/v5_0/RocketMqConsumerReceiveAttributeExtractor.java b/instrumentation/rocketmq/rocketmq-client/rocketmq-client-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rocketmqclient/v5_0/RocketMqConsumerReceiveAttributeExtractor.java index 379ea4992eb9..c60ccd08b66c 100644 --- a/instrumentation/rocketmq/rocketmq-client/rocketmq-client-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rocketmqclient/v5_0/RocketMqConsumerReceiveAttributeExtractor.java +++ b/instrumentation/rocketmq/rocketmq-client/rocketmq-client-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rocketmqclient/v5_0/RocketMqConsumerReceiveAttributeExtractor.java @@ -5,7 +5,7 @@ package io.opentelemetry.javaagent.instrumentation.rocketmqclient.v5_0; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.MESSAGING_ROCKETMQ_CLIENT_GROUP; +import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_ROCKETMQ_CLIENT_GROUP; import apache.rocketmq.v2.ReceiveMessageRequest; import io.opentelemetry.api.common.AttributesBuilder; diff --git a/instrumentation/rocketmq/rocketmq-client/rocketmq-client-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rocketmqclient/v5_0/RocketMqConsumerReceiveAttributeGetter.java b/instrumentation/rocketmq/rocketmq-client/rocketmq-client-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rocketmqclient/v5_0/RocketMqConsumerReceiveAttributeGetter.java index 87e4b4101faf..04f14367017f 100644 --- a/instrumentation/rocketmq/rocketmq-client/rocketmq-client-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rocketmqclient/v5_0/RocketMqConsumerReceiveAttributeGetter.java +++ b/instrumentation/rocketmq/rocketmq-client/rocketmq-client-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rocketmqclient/v5_0/RocketMqConsumerReceiveAttributeGetter.java @@ -6,7 +6,7 @@ package io.opentelemetry.javaagent.instrumentation.rocketmqclient.v5_0; import apache.rocketmq.v2.ReceiveMessageRequest; -import io.opentelemetry.instrumentation.api.instrumenter.messaging.MessagingAttributesGetter; +import io.opentelemetry.instrumentation.api.incubator.semconv.messaging.MessagingAttributesGetter; import java.util.List; import javax.annotation.Nullable; import org.apache.rocketmq.client.apis.message.MessageView; @@ -27,11 +27,22 @@ public String getDestination(ReceiveMessageRequest request) { return request.getMessageQueue().getTopic().getName(); } + @Nullable + @Override + public String getDestinationTemplate(ReceiveMessageRequest request) { + return null; + } + @Override public boolean isTemporaryDestination(ReceiveMessageRequest request) { return false; } + @Override + public boolean isAnonymousDestination(ReceiveMessageRequest request) { + return false; + } + @Nullable @Override public String getConversationId(ReceiveMessageRequest request) { @@ -40,13 +51,13 @@ public String getConversationId(ReceiveMessageRequest request) { @Nullable @Override - public Long getMessagePayloadSize(ReceiveMessageRequest request) { + public Long getMessageBodySize(ReceiveMessageRequest request) { return null; } @Nullable @Override - public Long getMessagePayloadCompressedSize(ReceiveMessageRequest request) { + public Long getMessageEnvelopeSize(ReceiveMessageRequest request) { return null; } @@ -55,4 +66,17 @@ public Long getMessagePayloadCompressedSize(ReceiveMessageRequest request) { public String getMessageId(ReceiveMessageRequest request, @Nullable List unused) { return null; } + + @Nullable + @Override + public String getClientId(ReceiveMessageRequest request) { + return null; + } + + @Nullable + @Override + public Long getBatchMessageCount( + ReceiveMessageRequest request, @Nullable List messages) { + return messages != null ? (long) messages.size() : null; + } } diff --git a/instrumentation/rocketmq/rocketmq-client/rocketmq-client-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rocketmqclient/v5_0/RocketMqInstrumenterFactory.java b/instrumentation/rocketmq/rocketmq-client/rocketmq-client-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rocketmqclient/v5_0/RocketMqInstrumenterFactory.java index b2a0432ea8b6..5164fc1dbeaa 100644 --- a/instrumentation/rocketmq/rocketmq-client/rocketmq-client-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rocketmqclient/v5_0/RocketMqInstrumenterFactory.java +++ b/instrumentation/rocketmq/rocketmq-client/rocketmq-client-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rocketmqclient/v5_0/RocketMqInstrumenterFactory.java @@ -8,15 +8,16 @@ import apache.rocketmq.v2.ReceiveMessageRequest; import io.opentelemetry.api.OpenTelemetry; import io.opentelemetry.api.trace.StatusCode; +import io.opentelemetry.instrumentation.api.incubator.semconv.messaging.MessageOperation; +import io.opentelemetry.instrumentation.api.incubator.semconv.messaging.MessagingAttributesExtractor; +import io.opentelemetry.instrumentation.api.incubator.semconv.messaging.MessagingAttributesGetter; +import io.opentelemetry.instrumentation.api.incubator.semconv.messaging.MessagingSpanNameExtractor; 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 io.opentelemetry.instrumentation.api.instrumenter.SpanLinksExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.messaging.MessageOperation; -import io.opentelemetry.instrumentation.api.instrumenter.messaging.MessagingAttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.messaging.MessagingAttributesGetter; -import io.opentelemetry.instrumentation.api.instrumenter.messaging.MessagingSpanNameExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.SpanStatusExtractor; import io.opentelemetry.instrumentation.api.internal.PropagatorBasedSpanLinksExtractor; import java.util.List; import org.apache.rocketmq.client.apis.consumer.ConsumeResult; @@ -32,7 +33,7 @@ private RocketMqInstrumenterFactory() {} public static Instrumenter createProducerInstrumenter( OpenTelemetry openTelemetry, List capturedHeaders) { RocketMqProducerAttributeGetter getter = RocketMqProducerAttributeGetter.INSTANCE; - MessageOperation operation = MessageOperation.SEND; + MessageOperation operation = MessageOperation.PUBLISH; AttributesExtractor attributesExtractor = buildMessagingAttributesExtractor(getter, operation, capturedHeaders); @@ -86,8 +87,11 @@ public static Instrumenter createConsumerProcessInst .addAttributesExtractor(RocketMqConsumerProcessAttributeExtractor.INSTANCE) .setSpanStatusExtractor( (spanStatusBuilder, messageView, consumeResult, error) -> { - if (error != null || consumeResult == ConsumeResult.FAILURE) { + if (consumeResult == ConsumeResult.FAILURE) { spanStatusBuilder.setStatus(StatusCode.ERROR); + } else { + SpanStatusExtractor.getDefault() + .extract(spanStatusBuilder, messageView, consumeResult, error); } }); diff --git a/instrumentation/rocketmq/rocketmq-client/rocketmq-client-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rocketmqclient/v5_0/RocketMqProducerAttributeExtractor.java b/instrumentation/rocketmq/rocketmq-client/rocketmq-client-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rocketmqclient/v5_0/RocketMqProducerAttributeExtractor.java index f64a99b6fec2..8c77a20ba899 100644 --- a/instrumentation/rocketmq/rocketmq-client/rocketmq-client-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rocketmqclient/v5_0/RocketMqProducerAttributeExtractor.java +++ b/instrumentation/rocketmq/rocketmq-client/rocketmq-client-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rocketmqclient/v5_0/RocketMqProducerAttributeExtractor.java @@ -5,19 +5,16 @@ package io.opentelemetry.javaagent.instrumentation.rocketmqclient.v5_0; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.MESSAGING_ROCKETMQ_MESSAGE_DELIVERY_TIMESTAMP; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.MESSAGING_ROCKETMQ_MESSAGE_GROUP; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.MESSAGING_ROCKETMQ_MESSAGE_KEYS; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.MESSAGING_ROCKETMQ_MESSAGE_TAG; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.MESSAGING_ROCKETMQ_MESSAGE_TYPE; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.MessagingRocketmqMessageTypeValues.DELAY; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.MessagingRocketmqMessageTypeValues.FIFO; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.MessagingRocketmqMessageTypeValues.NORMAL; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.MessagingRocketmqMessageTypeValues.TRANSACTION; +import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_ROCKETMQ_MESSAGE_DELIVERY_TIMESTAMP; +import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_ROCKETMQ_MESSAGE_GROUP; +import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_ROCKETMQ_MESSAGE_KEYS; +import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_ROCKETMQ_MESSAGE_TAG; +import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_ROCKETMQ_MESSAGE_TYPE; import io.opentelemetry.api.common.AttributesBuilder; import io.opentelemetry.context.Context; import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; +import io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes; import java.util.ArrayList; import javax.annotation.Nullable; import org.apache.rocketmq.client.java.impl.producer.SendReceiptImpl; @@ -38,16 +35,24 @@ public void onStart( attributes.put(MESSAGING_ROCKETMQ_MESSAGE_KEYS, new ArrayList<>(message.getKeys())); switch (message.getMessageType()) { case FIFO: - attributes.put(MESSAGING_ROCKETMQ_MESSAGE_TYPE, FIFO); + attributes.put( + MESSAGING_ROCKETMQ_MESSAGE_TYPE, + MessagingIncubatingAttributes.MessagingRocketmqMessageTypeValues.FIFO); break; case DELAY: - attributes.put(MESSAGING_ROCKETMQ_MESSAGE_TYPE, DELAY); + attributes.put( + MESSAGING_ROCKETMQ_MESSAGE_TYPE, + MessagingIncubatingAttributes.MessagingRocketmqMessageTypeValues.DELAY); break; case TRANSACTION: - attributes.put(MESSAGING_ROCKETMQ_MESSAGE_TYPE, TRANSACTION); + attributes.put( + MESSAGING_ROCKETMQ_MESSAGE_TYPE, + MessagingIncubatingAttributes.MessagingRocketmqMessageTypeValues.TRANSACTION); break; default: - attributes.put(MESSAGING_ROCKETMQ_MESSAGE_TYPE, NORMAL); + attributes.put( + MESSAGING_ROCKETMQ_MESSAGE_TYPE, + MessagingIncubatingAttributes.MessagingRocketmqMessageTypeValues.NORMAL); } } diff --git a/instrumentation/rocketmq/rocketmq-client/rocketmq-client-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rocketmqclient/v5_0/RocketMqProducerAttributeGetter.java b/instrumentation/rocketmq/rocketmq-client/rocketmq-client-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rocketmqclient/v5_0/RocketMqProducerAttributeGetter.java index 0931a53c09b9..d957d639b797 100644 --- a/instrumentation/rocketmq/rocketmq-client/rocketmq-client-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rocketmqclient/v5_0/RocketMqProducerAttributeGetter.java +++ b/instrumentation/rocketmq/rocketmq-client/rocketmq-client-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rocketmqclient/v5_0/RocketMqProducerAttributeGetter.java @@ -5,7 +5,7 @@ package io.opentelemetry.javaagent.instrumentation.rocketmqclient.v5_0; -import io.opentelemetry.instrumentation.api.instrumenter.messaging.MessagingAttributesGetter; +import io.opentelemetry.instrumentation.api.incubator.semconv.messaging.MessagingAttributesGetter; import java.util.Collections; import java.util.List; import javax.annotation.Nullable; @@ -28,11 +28,22 @@ public String getDestination(PublishingMessageImpl message) { return message.getTopic(); } + @Nullable + @Override + public String getDestinationTemplate(PublishingMessageImpl message) { + return null; + } + @Override public boolean isTemporaryDestination(PublishingMessageImpl message) { return false; } + @Override + public boolean isAnonymousDestination(PublishingMessageImpl message) { + return false; + } + @Nullable @Override public String getConversationId(PublishingMessageImpl message) { @@ -40,13 +51,13 @@ public String getConversationId(PublishingMessageImpl message) { } @Override - public Long getMessagePayloadSize(PublishingMessageImpl message) { + public Long getMessageBodySize(PublishingMessageImpl message) { return (long) message.getBody().remaining(); } @Nullable @Override - public Long getMessagePayloadCompressedSize(PublishingMessageImpl message) { + public Long getMessageEnvelopeSize(PublishingMessageImpl message) { return null; } @@ -56,6 +67,19 @@ public String getMessageId(PublishingMessageImpl message, @Nullable SendReceiptI return message.getMessageId().toString(); } + @Nullable + @Override + public String getClientId(PublishingMessageImpl message) { + return null; + } + + @Nullable + @Override + public Long getBatchMessageCount( + PublishingMessageImpl publishingMessage, @Nullable SendReceiptImpl sendReceipt) { + return null; + } + @Override public List getMessageHeader(PublishingMessageImpl message, String name) { String value = message.getProperties().get(name); diff --git a/instrumentation/rocketmq/rocketmq-client/rocketmq-client-5.0/testing/build.gradle.kts b/instrumentation/rocketmq/rocketmq-client/rocketmq-client-5.0/testing/build.gradle.kts index 287d950d28de..0483951a9630 100644 --- a/instrumentation/rocketmq/rocketmq-client/rocketmq-client-5.0/testing/build.gradle.kts +++ b/instrumentation/rocketmq/rocketmq-client/rocketmq-client-5.0/testing/build.gradle.kts @@ -7,6 +7,6 @@ dependencies { // earlier versions have bugs that may make tests flaky. implementation("org.apache.rocketmq:rocketmq-client-java:5.0.2") - implementation("org.testcontainers:testcontainers:1.17.5") + implementation("org.testcontainers:testcontainers") implementation("io.opentelemetry:opentelemetry-api") } diff --git a/instrumentation/rocketmq/rocketmq-client/rocketmq-client-5.0/testing/src/main/java/io/opentelemetry/instrumentation/rocketmqclient/v5_0/AbstractRocketMqClientSuppressReceiveSpanTest.java b/instrumentation/rocketmq/rocketmq-client/rocketmq-client-5.0/testing/src/main/java/io/opentelemetry/instrumentation/rocketmqclient/v5_0/AbstractRocketMqClientSuppressReceiveSpanTest.java index 6fe8e4fdea4c..c9b0557e79a5 100644 --- a/instrumentation/rocketmq/rocketmq-client/rocketmq-client-5.0/testing/src/main/java/io/opentelemetry/instrumentation/rocketmqclient/v5_0/AbstractRocketMqClientSuppressReceiveSpanTest.java +++ b/instrumentation/rocketmq/rocketmq-client/rocketmq-client-5.0/testing/src/main/java/io/opentelemetry/instrumentation/rocketmqclient/v5_0/AbstractRocketMqClientSuppressReceiveSpanTest.java @@ -6,21 +6,21 @@ package io.opentelemetry.instrumentation.rocketmqclient.v5_0; import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.MESSAGING_DESTINATION_NAME; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.MESSAGING_MESSAGE_ID; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.MESSAGING_MESSAGE_PAYLOAD_SIZE_BYTES; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.MESSAGING_OPERATION; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.MESSAGING_ROCKETMQ_CLIENT_GROUP; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.MESSAGING_ROCKETMQ_MESSAGE_KEYS; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.MESSAGING_ROCKETMQ_MESSAGE_TAG; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.MESSAGING_ROCKETMQ_MESSAGE_TYPE; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.MESSAGING_SYSTEM; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.MessagingRocketmqMessageTypeValues.NORMAL; +import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME; +import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_MESSAGE_BODY_SIZE; +import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_MESSAGE_ID; +import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_OPERATION; +import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_ROCKETMQ_CLIENT_GROUP; +import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_ROCKETMQ_MESSAGE_KEYS; +import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_ROCKETMQ_MESSAGE_TAG; +import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_ROCKETMQ_MESSAGE_TYPE; +import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_SYSTEM; import io.opentelemetry.api.trace.SpanKind; import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; import io.opentelemetry.instrumentation.testing.util.ThrowingSupplier; import io.opentelemetry.sdk.trace.data.StatusData; +import io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes; import java.nio.charset.StandardCharsets; import java.time.Duration; import java.util.Arrays; @@ -108,20 +108,23 @@ void testSendAndConsumeMessage() throws Throwable { span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(), span -> span.hasKind(SpanKind.PRODUCER) - .hasName(topic + " send") + .hasName(topic + " publish") .hasStatus(StatusData.unset()) .hasParent(trace.getSpan(0)) .hasAttributesSatisfyingExactly( equalTo(MESSAGING_ROCKETMQ_MESSAGE_TAG, tag), equalTo(MESSAGING_ROCKETMQ_MESSAGE_KEYS, Arrays.asList(keys)), - equalTo(MESSAGING_ROCKETMQ_MESSAGE_TYPE, NORMAL), equalTo( - MESSAGING_MESSAGE_PAYLOAD_SIZE_BYTES, (long) body.length), + MESSAGING_ROCKETMQ_MESSAGE_TYPE, + MessagingIncubatingAttributes + .MessagingRocketmqMessageTypeValues.NORMAL), + equalTo(MESSAGING_MESSAGE_BODY_SIZE, (long) body.length), equalTo(MESSAGING_SYSTEM, "rocketmq"), equalTo( MESSAGING_MESSAGE_ID, sendReceipt.getMessageId().toString()), - equalTo(MESSAGING_DESTINATION_NAME, topic)), + equalTo(MESSAGING_DESTINATION_NAME, topic), + equalTo(MESSAGING_OPERATION, "publish")), span -> span.hasKind(SpanKind.CONSUMER) .hasName(topic + " process") @@ -132,8 +135,7 @@ void testSendAndConsumeMessage() throws Throwable { equalTo(MESSAGING_ROCKETMQ_CLIENT_GROUP, consumerGroup), equalTo(MESSAGING_ROCKETMQ_MESSAGE_TAG, tag), equalTo(MESSAGING_ROCKETMQ_MESSAGE_KEYS, Arrays.asList(keys)), - equalTo( - MESSAGING_MESSAGE_PAYLOAD_SIZE_BYTES, (long) body.length), + equalTo(MESSAGING_MESSAGE_BODY_SIZE, (long) body.length), equalTo(MESSAGING_SYSTEM, "rocketmq"), equalTo( MESSAGING_MESSAGE_ID, diff --git a/instrumentation/rocketmq/rocketmq-client/rocketmq-client-5.0/testing/src/main/java/io/opentelemetry/instrumentation/rocketmqclient/v5_0/AbstractRocketMqClientTest.java b/instrumentation/rocketmq/rocketmq-client/rocketmq-client-5.0/testing/src/main/java/io/opentelemetry/instrumentation/rocketmqclient/v5_0/AbstractRocketMqClientTest.java index 9d0dfcab91ff..beab651f2a8f 100644 --- a/instrumentation/rocketmq/rocketmq-client/rocketmq-client-5.0/testing/src/main/java/io/opentelemetry/instrumentation/rocketmqclient/v5_0/AbstractRocketMqClientTest.java +++ b/instrumentation/rocketmq/rocketmq-client/rocketmq-client-5.0/testing/src/main/java/io/opentelemetry/instrumentation/rocketmqclient/v5_0/AbstractRocketMqClientTest.java @@ -7,20 +7,18 @@ import static io.opentelemetry.instrumentation.testing.util.TelemetryDataUtil.orderByRootSpanKind; import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.MESSAGING_DESTINATION_NAME; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.MESSAGING_MESSAGE_ID; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.MESSAGING_MESSAGE_PAYLOAD_SIZE_BYTES; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.MESSAGING_OPERATION; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.MESSAGING_ROCKETMQ_CLIENT_GROUP; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.MESSAGING_ROCKETMQ_MESSAGE_DELIVERY_TIMESTAMP; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.MESSAGING_ROCKETMQ_MESSAGE_GROUP; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.MESSAGING_ROCKETMQ_MESSAGE_KEYS; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.MESSAGING_ROCKETMQ_MESSAGE_TAG; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.MESSAGING_ROCKETMQ_MESSAGE_TYPE; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.MESSAGING_SYSTEM; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.MessagingRocketmqMessageTypeValues.DELAY; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.MessagingRocketmqMessageTypeValues.FIFO; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.MessagingRocketmqMessageTypeValues.NORMAL; +import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_BATCH_MESSAGE_COUNT; +import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME; +import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_MESSAGE_BODY_SIZE; +import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_MESSAGE_ID; +import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_OPERATION; +import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_ROCKETMQ_CLIENT_GROUP; +import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_ROCKETMQ_MESSAGE_DELIVERY_TIMESTAMP; +import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_ROCKETMQ_MESSAGE_GROUP; +import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_ROCKETMQ_MESSAGE_KEYS; +import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_ROCKETMQ_MESSAGE_TAG; +import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_ROCKETMQ_MESSAGE_TYPE; +import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_SYSTEM; import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.trace.SpanKind; @@ -31,6 +29,7 @@ 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.incubating.MessagingIncubatingAttributes; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.time.Duration; @@ -68,6 +67,7 @@ public abstract class AbstractRocketMqClientTest { private static final String consumerGroup = "group-0"; private static final RocketMqProxyContainer container = new RocketMqProxyContainer(); + private final ClientServiceProvider provider = ClientServiceProvider.loadService(); private PushConsumer consumer; private Producer producer; @@ -414,15 +414,18 @@ private static SpanDataAssert assertProducerSpan( Arrays.asList( equalTo(MESSAGING_ROCKETMQ_MESSAGE_TAG, tag), equalTo(MESSAGING_ROCKETMQ_MESSAGE_KEYS, Arrays.asList(keys)), - equalTo(MESSAGING_ROCKETMQ_MESSAGE_TYPE, NORMAL), - equalTo(MESSAGING_MESSAGE_PAYLOAD_SIZE_BYTES, (long) body.length), + equalTo( + MESSAGING_ROCKETMQ_MESSAGE_TYPE, + MessagingIncubatingAttributes.MessagingRocketmqMessageTypeValues.NORMAL), + equalTo(MESSAGING_MESSAGE_BODY_SIZE, (long) body.length), equalTo(MESSAGING_SYSTEM, "rocketmq"), equalTo(MESSAGING_MESSAGE_ID, sendReceipt.getMessageId().toString()), - equalTo(MESSAGING_DESTINATION_NAME, topic))); + equalTo(MESSAGING_DESTINATION_NAME, topic), + equalTo(MESSAGING_OPERATION, "publish"))); attributeAssertions.addAll(Arrays.asList(extraAttributes)); return span.hasKind(SpanKind.PRODUCER) - .hasName(topic + " send") + .hasName(topic + " publish") .hasStatus(StatusData.unset()) .hasAttributesSatisfyingExactly(attributeAssertions); } @@ -442,15 +445,18 @@ private static SpanDataAssert assertProducerSpanWithFifoMessage( equalTo(MESSAGING_ROCKETMQ_MESSAGE_TAG, tag), equalTo(MESSAGING_ROCKETMQ_MESSAGE_KEYS, Arrays.asList(keys)), equalTo(MESSAGING_ROCKETMQ_MESSAGE_GROUP, messageGroup), - equalTo(MESSAGING_ROCKETMQ_MESSAGE_TYPE, FIFO), - equalTo(MESSAGING_MESSAGE_PAYLOAD_SIZE_BYTES, (long) body.length), + equalTo( + MESSAGING_ROCKETMQ_MESSAGE_TYPE, + MessagingIncubatingAttributes.MessagingRocketmqMessageTypeValues.FIFO), + equalTo(MESSAGING_MESSAGE_BODY_SIZE, (long) body.length), equalTo(MESSAGING_SYSTEM, "rocketmq"), equalTo(MESSAGING_MESSAGE_ID, sendReceipt.getMessageId().toString()), - equalTo(MESSAGING_DESTINATION_NAME, topic))); + equalTo(MESSAGING_DESTINATION_NAME, topic), + equalTo(MESSAGING_OPERATION, "publish"))); attributeAssertions.addAll(Arrays.asList(extraAttributes)); return span.hasKind(SpanKind.PRODUCER) - .hasName(topic + " send") + .hasName(topic + " publish") .hasStatus(StatusData.unset()) .hasAttributesSatisfyingExactly(attributeAssertions); } @@ -470,15 +476,18 @@ private static SpanDataAssert assertProducerSpanWithDelayMessage( equalTo(MESSAGING_ROCKETMQ_MESSAGE_TAG, tag), equalTo(MESSAGING_ROCKETMQ_MESSAGE_KEYS, Arrays.asList(keys)), equalTo(MESSAGING_ROCKETMQ_MESSAGE_DELIVERY_TIMESTAMP, deliveryTimestamp), - equalTo(MESSAGING_ROCKETMQ_MESSAGE_TYPE, DELAY), - equalTo(MESSAGING_MESSAGE_PAYLOAD_SIZE_BYTES, (long) body.length), + equalTo( + MESSAGING_ROCKETMQ_MESSAGE_TYPE, + MessagingIncubatingAttributes.MessagingRocketmqMessageTypeValues.DELAY), + equalTo(MESSAGING_MESSAGE_BODY_SIZE, (long) body.length), equalTo(MESSAGING_SYSTEM, "rocketmq"), equalTo(MESSAGING_MESSAGE_ID, sendReceipt.getMessageId().toString()), - equalTo(MESSAGING_DESTINATION_NAME, topic))); + equalTo(MESSAGING_DESTINATION_NAME, topic), + equalTo(MESSAGING_OPERATION, "publish"))); attributeAssertions.addAll(Arrays.asList(extraAttributes)); return span.hasKind(SpanKind.PRODUCER) - .hasName(topic + " send") + .hasName(topic + " publish") .hasStatus(StatusData.unset()) .hasAttributesSatisfyingExactly(attributeAssertions); } @@ -492,7 +501,8 @@ private static SpanDataAssert assertReceiveSpan( equalTo(MESSAGING_ROCKETMQ_CLIENT_GROUP, consumerGroup), equalTo(MESSAGING_SYSTEM, "rocketmq"), equalTo(MESSAGING_DESTINATION_NAME, topic), - equalTo(MESSAGING_OPERATION, "receive")); + equalTo(MESSAGING_OPERATION, "receive"), + equalTo(MESSAGING_BATCH_MESSAGE_COUNT, 1)); } private static SpanDataAssert assertProcessSpan( @@ -511,7 +521,7 @@ private static SpanDataAssert assertProcessSpan( equalTo(MESSAGING_ROCKETMQ_CLIENT_GROUP, consumerGroup), equalTo(MESSAGING_ROCKETMQ_MESSAGE_TAG, tag), equalTo(MESSAGING_ROCKETMQ_MESSAGE_KEYS, Arrays.asList(keys)), - equalTo(MESSAGING_MESSAGE_PAYLOAD_SIZE_BYTES, (long) body.length), + equalTo(MESSAGING_MESSAGE_BODY_SIZE, (long) body.length), equalTo(MESSAGING_SYSTEM, "rocketmq"), equalTo(MESSAGING_MESSAGE_ID, sendReceipt.getMessageId().toString()), equalTo(MESSAGING_DESTINATION_NAME, topic), @@ -544,7 +554,7 @@ private static SpanDataAssert assertProcessSpanWithFifoMessage( equalTo(MESSAGING_ROCKETMQ_MESSAGE_TAG, tag), equalTo(MESSAGING_ROCKETMQ_MESSAGE_KEYS, Arrays.asList(keys)), equalTo(MESSAGING_ROCKETMQ_MESSAGE_GROUP, messageGroup), - equalTo(MESSAGING_MESSAGE_PAYLOAD_SIZE_BYTES, (long) body.length), + equalTo(MESSAGING_MESSAGE_BODY_SIZE, (long) body.length), equalTo(MESSAGING_SYSTEM, "rocketmq"), equalTo(MESSAGING_MESSAGE_ID, sendReceipt.getMessageId().toString()), equalTo(MESSAGING_DESTINATION_NAME, topic), @@ -577,7 +587,7 @@ private static SpanDataAssert assertProcessSpanWithDelayMessage( equalTo(MESSAGING_ROCKETMQ_MESSAGE_TAG, tag), equalTo(MESSAGING_ROCKETMQ_MESSAGE_KEYS, Arrays.asList(keys)), equalTo(MESSAGING_ROCKETMQ_MESSAGE_DELIVERY_TIMESTAMP, deliveryTimestamp), - equalTo(MESSAGING_MESSAGE_PAYLOAD_SIZE_BYTES, (long) body.length), + equalTo(MESSAGING_MESSAGE_BODY_SIZE, (long) body.length), equalTo(MESSAGING_SYSTEM, "rocketmq"), equalTo(MESSAGING_MESSAGE_ID, sendReceipt.getMessageId().toString()), equalTo(MESSAGING_DESTINATION_NAME, topic), diff --git a/instrumentation/rocketmq/rocketmq-client/rocketmq-client-5.0/testing/src/main/java/io/opentelemetry/instrumentation/rocketmqclient/v5_0/RocketMqProxyContainer.java b/instrumentation/rocketmq/rocketmq-client/rocketmq-client-5.0/testing/src/main/java/io/opentelemetry/instrumentation/rocketmqclient/v5_0/RocketMqProxyContainer.java index 5abe16b5b674..9d959ad8c1ff 100644 --- a/instrumentation/rocketmq/rocketmq-client/rocketmq-client-5.0/testing/src/main/java/io/opentelemetry/instrumentation/rocketmqclient/v5_0/RocketMqProxyContainer.java +++ b/instrumentation/rocketmq/rocketmq-client/rocketmq-client-5.0/testing/src/main/java/io/opentelemetry/instrumentation/rocketmqclient/v5_0/RocketMqProxyContainer.java @@ -6,12 +6,15 @@ package io.opentelemetry.instrumentation.rocketmqclient.v5_0; import io.opentelemetry.instrumentation.test.utils.PortUtils; +import java.net.InetAddress; +import java.net.UnknownHostException; +import org.testcontainers.DockerClientFactory; import org.testcontainers.containers.FixedHostPortGenericContainer; import org.testcontainers.containers.GenericContainer; public class RocketMqProxyContainer { // TODO(aaron-ai): replace it by the official image. - private static final String IMAGE_NAME = "aaronai/rocketmq-proxy-it:v1.0.1"; + private static final String IMAGE_NAME = "aaronai/rocketmq-proxy-it:v1.0.2"; private final GenericContainer container; final String endpoints; @@ -23,15 +26,25 @@ public class RocketMqProxyContainer { int brokerPort = proxyPort + 1; int brokerHaPort = proxyPort + 2; int namesrvPort = proxyPort + 3; - endpoints = "127.0.0.1:" + proxyPort; + // Although this function says "IpAddress" in the name, it actually returns a hostname. + String dockerHost = DockerClientFactory.instance().dockerHostIpAddress(); + String ip; + try { + ip = InetAddress.getByName(dockerHost).getHostAddress(); + } catch (UnknownHostException e) { + throw new IllegalStateException("Could not find IP for Docker Host", e); + } container = new FixedHostPortGenericContainer(IMAGE_NAME) .withFixedExposedPort(proxyPort, proxyPort) + .withFixedExposedPort(brokerPort, brokerPort) .withEnv("rocketmq.broker.port", String.valueOf(brokerPort)) .withEnv("rocketmq.proxy.port", String.valueOf(proxyPort)) .withEnv("rocketmq.broker.ha.port", String.valueOf(brokerHaPort)) .withEnv("rocketmq.namesrv.port", String.valueOf(namesrvPort)) - .withExposedPorts(proxyPort); + .withEnv("rocketmq.proxy.ip", ip) + .withExposedPorts(proxyPort, brokerPort); + endpoints = ip + ":" + proxyPort; } void start() { diff --git a/instrumentation/runtime-telemetry/README.md b/instrumentation/runtime-telemetry/README.md new file mode 100644 index 000000000000..9baab6e1315c --- /dev/null +++ b/instrumentation/runtime-telemetry/README.md @@ -0,0 +1,9 @@ +# Settings for the Runtime Telemetry instrumentation + +| System property | Type | Default | Description | +|--------------------------------------------------------------------------|---------|---------|-------------------------------------------------------------------| +| `otel.instrumentation.runtime-telemetry.emit-experimental-telemetry` | Boolean | `false` | Enable the capture of experimental metrics. | +| `otel.instrumentation.runtime-telemetry-java17.enable-all` | Boolean | `false` | Enable the capture of all JFR based metrics. | +| `otel.instrumentation.runtime-telemetry-java17.enabled` | Boolean | `false` | Enable the capture of JFR based metrics. | +| `otel.instrumentation.runtime-telemetry.package-emitter.enabled` | Boolean | `false` | Enable creating events for JAR libraries used by the application. | +| `otel.instrumentation.runtime-telemetry.package-emitter.jars-per-second` | Integer | 10 | The number of JAR files processed per second. | diff --git a/instrumentation/runtime-telemetry/runtime-telemetry-java17/javaagent/src/main/java/io/opentelemetry/instrumentation/javaagent/runtimemetrics/java17/Java17RuntimeMetricsInstaller.java b/instrumentation/runtime-telemetry/runtime-telemetry-java17/javaagent/src/main/java/io/opentelemetry/instrumentation/javaagent/runtimemetrics/java17/Java17RuntimeMetricsInstaller.java index d54350d1102a..a01e4885c209 100644 --- a/instrumentation/runtime-telemetry/runtime-telemetry-java17/javaagent/src/main/java/io/opentelemetry/instrumentation/javaagent/runtimemetrics/java17/Java17RuntimeMetricsInstaller.java +++ b/instrumentation/runtime-telemetry/runtime-telemetry-java17/javaagent/src/main/java/io/opentelemetry/instrumentation/javaagent/runtimemetrics/java17/Java17RuntimeMetricsInstaller.java @@ -9,8 +9,10 @@ import io.opentelemetry.api.GlobalOpenTelemetry; import io.opentelemetry.api.OpenTelemetry; import io.opentelemetry.instrumentation.runtimemetrics.java17.RuntimeMetrics; +import io.opentelemetry.instrumentation.runtimemetrics.java17.RuntimeMetricsBuilder; import io.opentelemetry.javaagent.extension.AgentListener; import io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdk; +import io.opentelemetry.sdk.autoconfigure.internal.AutoConfigureUtil; import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; /** An {@link AgentListener} that enables runtime metrics during agent startup. */ @@ -19,27 +21,32 @@ public class Java17RuntimeMetricsInstaller implements AgentListener { @Override public void afterAgent(AutoConfiguredOpenTelemetrySdk autoConfiguredSdk) { - ConfigProperties config = autoConfiguredSdk.getConfig(); + ConfigProperties config = AutoConfigureUtil.getConfig(autoConfiguredSdk); OpenTelemetry openTelemetry = GlobalOpenTelemetry.get(); - RuntimeMetrics runtimeMetrics = null; + RuntimeMetricsBuilder builder = null; /* By default don't use any JFR metrics. May change this once semantic conventions are updated. If enabled, default to only the metrics not already covered by runtime-telemetry-java8 */ boolean defaultEnabled = config.getBoolean("otel.instrumentation.common.default-enabled", true); if (config.getBoolean("otel.instrumentation.runtime-telemetry-java17.enable-all", false)) { - runtimeMetrics = RuntimeMetrics.builder(openTelemetry).enableAllFeatures().build(); + builder = RuntimeMetrics.builder(openTelemetry).enableAllFeatures(); } else if (config.getBoolean("otel.instrumentation.runtime-telemetry-java17.enabled", false)) { - runtimeMetrics = RuntimeMetrics.create(openTelemetry); + builder = RuntimeMetrics.builder(openTelemetry); } else if (config.getBoolean( "otel.instrumentation.runtime-telemetry.enabled", defaultEnabled)) { // This only uses metrics gathered by JMX - runtimeMetrics = RuntimeMetrics.builder(openTelemetry).disableAllFeatures().build(); + builder = RuntimeMetrics.builder(openTelemetry).disableAllFeatures(); } - if (runtimeMetrics != null) { - RuntimeMetrics finalJfrTelemetry = runtimeMetrics; + if (builder != null) { + if (config.getBoolean( + "otel.instrumentation.runtime-telemetry.emit-experimental-telemetry", false)) { + builder.enableExperimentalJmxTelemetry(); + } + + RuntimeMetrics finalJfrTelemetry = builder.build(); Thread cleanupTelemetry = new Thread(() -> finalJfrTelemetry.close()); Runtime.getRuntime().addShutdownHook(cleanupTelemetry); } diff --git a/instrumentation/runtime-telemetry/runtime-telemetry-java17/javaagent/src/test/groovy/JmxRuntimeMetricsTest.groovy b/instrumentation/runtime-telemetry/runtime-telemetry-java17/javaagent/src/test/groovy/JmxRuntimeMetricsTest.groovy deleted file mode 100644 index 16ea3d90efe1..000000000000 --- a/instrumentation/runtime-telemetry/runtime-telemetry-java17/javaagent/src/test/groovy/JmxRuntimeMetricsTest.groovy +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification -import spock.util.concurrent.PollingConditions - -class JmxRuntimeMetricsTest extends AgentInstrumentationSpecification { - - def "test runtime metrics is enabled"() { - when: - def conditions = new PollingConditions(timeout: 10, initialDelay: 1.5, factor: 1.25) - // Force a gc to ensure gc metrics - System.gc() - - then: - conditions.eventually { - assert getMetrics().any { it.name == "process.runtime.jvm.classes.loaded" } - assert getMetrics().any { it.name == "process.runtime.jvm.classes.unloaded" } - assert getMetrics().any { it.name == "process.runtime.jvm.classes.current_loaded" } - assert getMetrics().any { it.name == "process.runtime.jvm.system.cpu.load_1m" } - assert getMetrics().any { it.name == "process.runtime.jvm.system.cpu.utilization" } - assert getMetrics().any { it.name == "process.runtime.jvm.cpu.utilization" } - assert getMetrics().any { it.name == "process.runtime.jvm.gc.duration" } - assert getMetrics().any { it.name == "process.runtime.jvm.memory.init" } - assert getMetrics().any { it.name == "process.runtime.jvm.memory.usage" } - assert getMetrics().any { it.name == "process.runtime.jvm.memory.committed" } - assert getMetrics().any { it.name == "process.runtime.jvm.memory.limit" } - assert getMetrics().any { it.name == "process.runtime.jvm.memory.usage_after_last_gc" } - assert getMetrics().any { it.name == "process.runtime.jvm.threads.count" } - assert getMetrics().any { it.name == "process.runtime.jvm.buffer.limit" } - assert getMetrics().any { it.name == "process.runtime.jvm.buffer.count" } - assert getMetrics().any { it.name == "process.runtime.jvm.buffer.usage" } - } - } -} diff --git a/instrumentation/runtime-telemetry/runtime-telemetry-java17/javaagent/src/test/java/io/opentelemetry/instrumentation/javaagent/runtimemetrics/java17/JfrRuntimeMetricsTest.java b/instrumentation/runtime-telemetry/runtime-telemetry-java17/javaagent/src/test/java/io/opentelemetry/instrumentation/javaagent/runtimemetrics/java17/JfrRuntimeMetricsTest.java index 59c5366dd14a..8f36953986ac 100644 --- a/instrumentation/runtime-telemetry/runtime-telemetry-java17/javaagent/src/test/java/io/opentelemetry/instrumentation/javaagent/runtimemetrics/java17/JfrRuntimeMetricsTest.java +++ b/instrumentation/runtime-telemetry/runtime-telemetry-java17/javaagent/src/test/java/io/opentelemetry/instrumentation/javaagent/runtimemetrics/java17/JfrRuntimeMetricsTest.java @@ -5,31 +5,17 @@ package io.opentelemetry.instrumentation.javaagent.runtimemetrics.java17; -import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; -import static org.awaitility.Awaitility.await; - -import io.opentelemetry.instrumentation.testing.AgentTestRunner; -import io.opentelemetry.sdk.metrics.data.MetricData; -import io.opentelemetry.sdk.testing.assertj.MetricAssert; -import java.util.Collection; -import java.util.function.Consumer; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; import org.junit.jupiter.api.Assumptions; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; class JfrRuntimeMetricsTest { - @SafeVarargs - private static void waitAndAssertMetrics(Consumer... assertions) { - await() - .untilAsserted( - () -> { - Collection metrics = AgentTestRunner.instance().getExportedMetrics(); - assertThat(metrics).isNotEmpty(); - for (Consumer assertion : assertions) { - assertThat(metrics).anySatisfy(metric -> assertion.accept(assertThat(metric))); - } - }); - } + + @RegisterExtension + static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); @BeforeAll static void setUp() { @@ -45,9 +31,10 @@ void shouldHaveDefaultMetrics() { // This should generate some events System.gc(); - waitAndAssertMetrics( - metric -> metric.hasName("process.runtime.jvm.cpu.longlock"), - metric -> metric.hasName("process.runtime.jvm.cpu.limit"), - metric -> metric.hasName("process.runtime.jvm.cpu.context_switch")); + testing.waitAndAssertMetrics( + "io.opentelemetry.runtime-telemetry-java17", + metric -> metric.hasName("jvm.cpu.longlock"), + metric -> metric.hasName("jvm.cpu.limit"), + metric -> metric.hasName("jvm.cpu.context_switch")); } } diff --git a/instrumentation/runtime-telemetry/runtime-telemetry-java17/javaagent/src/test/java/io/opentelemetry/instrumentation/javaagent/runtimemetrics/java17/JmxRuntimeMetricsTest.java b/instrumentation/runtime-telemetry/runtime-telemetry-java17/javaagent/src/test/java/io/opentelemetry/instrumentation/javaagent/runtimemetrics/java17/JmxRuntimeMetricsTest.java new file mode 100644 index 000000000000..523dbd205acd --- /dev/null +++ b/instrumentation/runtime-telemetry/runtime-telemetry-java17/javaagent/src/test/java/io/opentelemetry/instrumentation/javaagent/runtimemetrics/java17/JmxRuntimeMetricsTest.java @@ -0,0 +1,38 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.javaagent.runtimemetrics.java17; + +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +class JmxRuntimeMetricsTest { + + @RegisterExtension + static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + @Test + void runtimeMetricsAreEnabled() { + // Force a gc to "ensure" gc metrics + System.gc(); + + testing.waitAndAssertMetrics( + "io.opentelemetry.runtime-telemetry-java8", + metric -> metric.hasName("jvm.class.loaded"), + metric -> metric.hasName("jvm.class.unloaded"), + metric -> metric.hasName("jvm.class.count"), + metric -> metric.hasName("jvm.cpu.time"), + metric -> metric.hasName("jvm.cpu.count"), + metric -> metric.hasName("jvm.cpu.recent_utilization"), + metric -> metric.hasName("jvm.gc.duration"), + metric -> metric.hasName("jvm.memory.used"), + metric -> metric.hasName("jvm.memory.committed"), + metric -> metric.hasName("jvm.memory.limit"), + metric -> metric.hasName("jvm.memory.used_after_last_gc"), + metric -> metric.hasName("jvm.thread.count")); + } +} diff --git a/instrumentation/runtime-telemetry/runtime-telemetry-java17/library/README.md b/instrumentation/runtime-telemetry/runtime-telemetry-java17/library/README.md index b3d81e11f631..48e144d1209a 100644 --- a/instrumentation/runtime-telemetry/runtime-telemetry-java17/library/README.md +++ b/instrumentation/runtime-telemetry/runtime-telemetry-java17/library/README.md @@ -1,4 +1,3 @@ - The main entry point is the `RuntimeMetrics` class in the package `io.opentelemetry.instrumentation.runtimemetrics.java17`: ```java @@ -36,16 +35,16 @@ default, and the telemetry each produces: -| JfrFeature | Default Enabled | Metrics | -|---|---|---| -| BUFFER_METRICS | false | `process.runtime.jvm.buffer.count`, `process.runtime.jvm.buffer.limit`, `process.runtime.jvm.buffer.usage` | -| CLASS_LOAD_METRICS | false | `process.runtime.jvm.classes.current_loaded`, `process.runtime.jvm.classes.loaded`, `process.runtime.jvm.classes.unloaded` | -| CONTEXT_SWITCH_METRICS | true | `process.runtime.jvm.cpu.context_switch` | -| CPU_COUNT_METRICS | true | `process.runtime.jvm.cpu.limit` | -| CPU_UTILIZATION_METRICS | false | `process.runtime.jvm.cpu.utilization`, `process.runtime.jvm.system.cpu.utilization` | -| GC_DURATION_METRICS | false | `process.runtime.jvm.gc.duration` | -| LOCK_METRICS | true | `process.runtime.jvm.cpu.longlock` | -| MEMORY_ALLOCATION_METRICS | true | `process.runtime.jvm.memory.allocation` | -| MEMORY_POOL_METRICS | false | `process.runtime.jvm.memory.committed`, `process.runtime.jvm.memory.init`, `process.runtime.jvm.memory.limit`, `process.runtime.jvm.memory.usage`, `process.runtime.jvm.memory.usage_after_last_gc` | -| NETWORK_IO_METRICS | true | `process.runtime.jvm.network.io`, `process.runtime.jvm.network.time` | -| THREAD_METRICS | false | `process.runtime.jvm.threads.count` | +| JfrFeature | Default Enabled | Metrics | +|---------------------------|-----------------|-------------------------------------------------------------------------------------------------------------------| +| BUFFER_METRICS | `false` | `jvm.buffer.count`, `jvm.buffer.memory.limit`, `jvm.buffer.memory.usage` | +| CLASS_LOAD_METRICS | `false` | `jvm.class.count`, `jvm.class.loaded`, `jvm.class.unloaded` | +| CONTEXT_SWITCH_METRICS | `true` | `jvm.cpu.context_switch` | +| CPU_COUNT_METRICS | `true` | `jvm.cpu.limit` | +| CPU_UTILIZATION_METRICS | `false` | `jvm.cpu.recent_utilization`, `jvm.system.cpu.utilization` | +| GC_DURATION_METRICS | `false` | `jvm.gc.duration` | +| LOCK_METRICS | `true` | `jvm.cpu.longlock` | +| MEMORY_ALLOCATION_METRICS | `true` | `jvm.memory.allocation` | +| MEMORY_POOL_METRICS | `false` | `jvm.memory.committed`, `jvm.memory.init`, `jvm.memory.limit`, `jvm.memory.used`, `jvm.memory.used_after_last_gc` | +| NETWORK_IO_METRICS | `true` | `jvm.network.io`, `jvm.network.time` | +| THREAD_METRICS | `false` | `jvm.thread.count` | diff --git a/instrumentation/runtime-telemetry/runtime-telemetry-java17/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java17/RuntimeMetricsBuilder.java b/instrumentation/runtime-telemetry/runtime-telemetry-java17/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java17/RuntimeMetricsBuilder.java index 84c7ca01ad23..e73c535e5803 100644 --- a/instrumentation/runtime-telemetry/runtime-telemetry-java17/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java17/RuntimeMetricsBuilder.java +++ b/instrumentation/runtime-telemetry/runtime-telemetry-java17/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java17/RuntimeMetricsBuilder.java @@ -7,12 +7,14 @@ import com.google.errorprone.annotations.CanIgnoreReturnValue; import io.opentelemetry.api.OpenTelemetry; -import io.opentelemetry.instrumentation.runtimemetrics.java8.BufferPools; import io.opentelemetry.instrumentation.runtimemetrics.java8.Classes; import io.opentelemetry.instrumentation.runtimemetrics.java8.Cpu; import io.opentelemetry.instrumentation.runtimemetrics.java8.GarbageCollector; import io.opentelemetry.instrumentation.runtimemetrics.java8.MemoryPools; import io.opentelemetry.instrumentation.runtimemetrics.java8.Threads; +import io.opentelemetry.instrumentation.runtimemetrics.java8.internal.ExperimentalBufferPools; +import io.opentelemetry.instrumentation.runtimemetrics.java8.internal.ExperimentalCpu; +import io.opentelemetry.instrumentation.runtimemetrics.java8.internal.ExperimentalMemoryPools; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -28,6 +30,7 @@ public final class RuntimeMetricsBuilder { final EnumMap enabledFeatureMap; private boolean disableJmx = false; + private boolean enableExperimentalJmxTelemetry = false; RuntimeMetricsBuilder(OpenTelemetry openTelemetry) { this.openTelemetry = openTelemetry; @@ -73,13 +76,20 @@ public RuntimeMetricsBuilder disableFeature(JfrFeature feature) { return this; } - /** Disable telemetry collection associated with the {@link JfrFeature}. */ + /** Disable all JMX telemetry collection. */ @CanIgnoreReturnValue public RuntimeMetricsBuilder disableAllJmx() { disableJmx = true; return this; } + /** Disable telemetry collection associated with the {@link JfrFeature}. */ + @CanIgnoreReturnValue + public RuntimeMetricsBuilder enableExperimentalJmxTelemetry() { + enableExperimentalJmxTelemetry = true; + return this; + } + /** Build and start an {@link RuntimeMetrics} with the config from this builder. */ public RuntimeMetrics build() { List observables = buildObservables(); @@ -95,12 +105,16 @@ private List buildObservables() { try { // Set up metrics gathered by JMX List observables = new ArrayList<>(); - observables.addAll(BufferPools.registerObservers(openTelemetry)); observables.addAll(Classes.registerObservers(openTelemetry)); observables.addAll(Cpu.registerObservers(openTelemetry)); + observables.addAll(GarbageCollector.registerObservers(openTelemetry)); observables.addAll(MemoryPools.registerObservers(openTelemetry)); observables.addAll(Threads.registerObservers(openTelemetry)); - observables.addAll(GarbageCollector.registerObservers(openTelemetry)); + if (enableExperimentalJmxTelemetry) { + observables.addAll(ExperimentalBufferPools.registerObservers(openTelemetry)); + observables.addAll(ExperimentalCpu.registerObservers(openTelemetry)); + observables.addAll(ExperimentalMemoryPools.registerObservers(openTelemetry)); + } return observables; } catch (Exception e) { throw new IllegalStateException("Error building RuntimeMetrics", e); diff --git a/instrumentation/runtime-telemetry/runtime-telemetry-java17/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java17/internal/Constants.java b/instrumentation/runtime-telemetry/runtime-telemetry-java17/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java17/internal/Constants.java index 4752c01fda9b..d4f3e79d38e8 100644 --- a/instrumentation/runtime-telemetry/runtime-telemetry-java17/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java17/internal/Constants.java +++ b/instrumentation/runtime-telemetry/runtime-telemetry-java17/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java17/internal/Constants.java @@ -18,7 +18,7 @@ private Constants() {} public static final String ONE = "1"; public static final String HERTZ = "Hz"; public static final String BYTES = "By"; - public static final String MILLISECONDS = "ms"; + public static final String SECONDS = "s"; public static final String COMMITTED = "committed"; public static final String RESERVED = "reserved"; public static final String INITIAL_SIZE = "initialSize"; @@ -26,65 +26,67 @@ private Constants() {} public static final String COMMITTED_SIZE = "committedSize"; public static final String RESERVED_SIZE = "reservedSize"; - public static final String DAEMON = "daemon"; public static final String HEAP = "heap"; - public static final String NON_HEAP = "nonheap"; + public static final String NON_HEAP = "non_heap"; public static final String NETWORK_MODE_READ = "read"; public static final String NETWORK_MODE_WRITE = "write"; public static final String DURATION = "duration"; public static final String END_OF_MINOR_GC = "end of minor GC"; public static final String END_OF_MAJOR_GC = "end of major GC"; - public static final String METRIC_NAME_NETWORK_BYTES = "process.runtime.jvm.network.io"; - public static final String METRIC_DESCRIPTION_NETWORK_BYTES = "Network read/write bytes"; - public static final String METRIC_NAME_NETWORK_DURATION = "process.runtime.jvm.network.time"; - public static final String METRIC_DESCRIPTION_NETWORK_DURATION = "Network read/write duration"; - public static final String METRIC_NAME_COMMITTED = "process.runtime.jvm.memory.committed"; - public static final String METRIC_DESCRIPTION_COMMITTED = "Measure of memory committed"; - public static final String METRIC_NAME_MEMORY = "process.runtime.jvm.memory.usage"; - public static final String METRIC_DESCRIPTION_MEMORY = "Measure of memory used"; - public static final String METRIC_NAME_MEMORY_AFTER = - "process.runtime.jvm.memory.usage_after_last_gc"; + public static final String METRIC_NAME_NETWORK_BYTES = "jvm.network.io"; + public static final String METRIC_DESCRIPTION_NETWORK_BYTES = "Network read/write bytes."; + public static final String METRIC_NAME_NETWORK_DURATION = "jvm.network.time"; + public static final String METRIC_DESCRIPTION_NETWORK_DURATION = "Network read/write duration."; + public static final String METRIC_NAME_COMMITTED = "jvm.memory.committed"; + public static final String METRIC_DESCRIPTION_COMMITTED = "Measure of memory committed."; + public static final String METRIC_NAME_MEMORY = "jvm.memory.used"; + public static final String METRIC_DESCRIPTION_MEMORY = "Measure of memory used."; + public static final String METRIC_NAME_MEMORY_AFTER = "jvm.memory.used_after_last_gc"; public static final String METRIC_DESCRIPTION_MEMORY_AFTER = - "Measure of memory used, as measured after the most recent garbage collection event on this pool"; - public static final String METRIC_NAME_MEMORY_ALLOCATION = - "process.runtime.jvm.memory.allocation"; - public static final String METRIC_DESCRIPTION_MEMORY_ALLOCATION = "Allocation"; - public static final String METRIC_NAME_MEMORY_INIT = "process.runtime.jvm.memory.init"; - public static final String METRIC_DESCRIPTION_MEMORY_INIT = "Measure of initial memory requested"; - public static final String METRIC_NAME_MEMORY_LIMIT = "process.runtime.jvm.memory.limit"; - public static final String METRIC_DESCRIPTION_MEMORY_LIMIT = "Measure of max obtainable memory"; - public static final String METRIC_NAME_GC_DURATION = "process.runtime.jvm.gc.duration"; + "Measure of memory used, as measured after the most recent garbage collection event on this pool."; + public static final String METRIC_NAME_MEMORY_ALLOCATION = "jvm.memory.allocation"; + public static final String METRIC_DESCRIPTION_MEMORY_ALLOCATION = + "Measure of memory allocations."; + public static final String METRIC_NAME_MEMORY_INIT = "jvm.memory.init"; + public static final String METRIC_DESCRIPTION_MEMORY_INIT = + "Measure of initial memory requested."; + public static final String METRIC_NAME_MEMORY_LIMIT = "jvm.memory.limit"; + public static final String METRIC_DESCRIPTION_MEMORY_LIMIT = "Measure of max obtainable memory."; + public static final String METRIC_NAME_GC_DURATION = "jvm.gc.duration"; public static final String METRIC_DESCRIPTION_GC_DURATION = - "Duration of JVM garbage collection actions"; + "Duration of JVM garbage collection actions."; public static final AttributeKey ATTR_THREAD_NAME = AttributeKey.stringKey("thread.name"); public static final AttributeKey ATTR_ARENA_NAME = AttributeKey.stringKey("arena"); public static final AttributeKey ATTR_NETWORK_MODE = AttributeKey.stringKey("mode"); - public static final AttributeKey ATTR_TYPE = AttributeKey.stringKey("type"); - public static final AttributeKey ATTR_POOL = AttributeKey.stringKey("pool"); - public static final AttributeKey ATTR_GC = AttributeKey.stringKey("pool"); - public static final AttributeKey ATTR_ACTION = AttributeKey.stringKey("action"); - public static final AttributeKey ATTR_DAEMON = AttributeKey.booleanKey(DAEMON); + public static final AttributeKey ATTR_MEMORY_TYPE = + AttributeKey.stringKey("jvm.memory.type"); + public static final AttributeKey ATTR_MEMORY_POOL = + AttributeKey.stringKey("jvm.memory.pool.name"); + public static final AttributeKey ATTR_GC_NAME = AttributeKey.stringKey("jvm.gc.name"); + public static final AttributeKey ATTR_GC_ACTION = AttributeKey.stringKey("jvm.gc.action"); + public static final AttributeKey ATTR_DAEMON = + AttributeKey.booleanKey("jvm.thread.daemon"); public static final Attributes ATTR_PS_EDEN_SPACE = - Attributes.of(ATTR_TYPE, HEAP, ATTR_POOL, "PS Eden Space"); + Attributes.of(ATTR_MEMORY_TYPE, HEAP, ATTR_MEMORY_POOL, "PS Eden Space"); public static final Attributes ATTR_PS_SURVIVOR_SPACE = - Attributes.of(ATTR_TYPE, HEAP, ATTR_POOL, "PS Survivor Space"); + Attributes.of(ATTR_MEMORY_TYPE, HEAP, ATTR_MEMORY_POOL, "PS Survivor Space"); public static final Attributes ATTR_PS_OLD_GEN = - Attributes.of(ATTR_TYPE, HEAP, ATTR_POOL, "PS Old Gen"); + Attributes.of(ATTR_MEMORY_TYPE, HEAP, ATTR_MEMORY_POOL, "PS Old Gen"); public static final Attributes ATTR_G1_SURVIVOR_SPACE = - Attributes.of(ATTR_TYPE, HEAP, ATTR_POOL, "G1 Survivor Space"); + Attributes.of(ATTR_MEMORY_TYPE, HEAP, ATTR_MEMORY_POOL, "G1 Survivor Space"); public static final Attributes ATTR_G1_EDEN_SPACE = - Attributes.of(ATTR_TYPE, HEAP, ATTR_POOL, "G1 Eden Space"); + Attributes.of(ATTR_MEMORY_TYPE, HEAP, ATTR_MEMORY_POOL, "G1 Eden Space"); public static final Attributes ATTR_METASPACE = - Attributes.of(ATTR_TYPE, NON_HEAP, ATTR_POOL, "Metaspace"); + Attributes.of(ATTR_MEMORY_TYPE, NON_HEAP, ATTR_MEMORY_POOL, "Metaspace"); public static final Attributes ATTR_COMPRESSED_CLASS_SPACE = - Attributes.of(ATTR_TYPE, NON_HEAP, ATTR_POOL, "Compressed Class Space"); + Attributes.of(ATTR_MEMORY_TYPE, NON_HEAP, ATTR_MEMORY_POOL, "Compressed Class Space"); public static final Attributes ATTR_CODE_CACHE = - Attributes.of(ATTR_TYPE, NON_HEAP, ATTR_POOL, "CodeCache"); + Attributes.of(ATTR_MEMORY_TYPE, NON_HEAP, ATTR_MEMORY_POOL, "CodeCache"); - public static final String UNIT_CLASSES = "{classes}"; - public static final String UNIT_THREADS = "{threads}"; - public static final String UNIT_BUFFERS = "{buffers}"; + public static final String UNIT_CLASSES = "{class}"; + public static final String UNIT_THREADS = "{thread}"; + public static final String UNIT_BUFFERS = "{buffer}"; public static final String UNIT_UTILIZATION = "1"; } diff --git a/instrumentation/runtime-telemetry/runtime-telemetry-java17/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java17/internal/DurationUtil.java b/instrumentation/runtime-telemetry/runtime-telemetry-java17/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java17/internal/DurationUtil.java index ae9cfb1e0c32..ff8ddb1894de 100644 --- a/instrumentation/runtime-telemetry/runtime-telemetry-java17/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java17/internal/DurationUtil.java +++ b/instrumentation/runtime-telemetry/runtime-telemetry-java17/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java17/internal/DurationUtil.java @@ -12,13 +12,12 @@ * any time. */ public final class DurationUtil { - private static final double NANOS_PER_MILLI = 1e6; + private static final double NANOS_PER_SECOND = 1e9; - /** Returns the duration as milliseconds, with fractional part included. */ - @SuppressWarnings("TimeUnitMismatch") - public static double toMillis(Duration duration) { + /** Returns the duration as seconds, with fractional part included. */ + public static double toSeconds(Duration duration) { double epochSecs = (double) duration.getSeconds(); - return epochSecs * 1000 + duration.getNano() / NANOS_PER_MILLI; + return epochSecs + duration.getNano() / NANOS_PER_SECOND; } private DurationUtil() {} diff --git a/instrumentation/runtime-telemetry/runtime-telemetry-java17/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java17/internal/buffer/DirectBufferStatisticsHandler.java b/instrumentation/runtime-telemetry/runtime-telemetry-java17/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java17/internal/buffer/DirectBufferStatisticsHandler.java index 22dc831e716c..fa2ad512cc46 100644 --- a/instrumentation/runtime-telemetry/runtime-telemetry-java17/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java17/internal/buffer/DirectBufferStatisticsHandler.java +++ b/instrumentation/runtime-telemetry/runtime-telemetry-java17/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java17/internal/buffer/DirectBufferStatisticsHandler.java @@ -5,6 +5,7 @@ package io.opentelemetry.instrumentation.runtimemetrics.java17.internal.buffer; +import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.common.Attributes; import io.opentelemetry.api.metrics.Meter; import io.opentelemetry.instrumentation.runtimemetrics.java17.JfrFeature; @@ -21,19 +22,21 @@ * any time. */ public final class DirectBufferStatisticsHandler implements RecordedEventHandler { - private static final String METRIC_NAME_USAGE = "process.runtime.jvm.buffer.usage"; - private static final String METRIC_NAME_LIMIT = "process.runtime.jvm.buffer.limit"; - private static final String METRIC_NAME_COUNT = "process.runtime.jvm.buffer.count"; - private static final String METRIC_DESCRIPTION_USAGE = "Measure of memory used by buffers"; + private static final String METRIC_NAME_USAGE = "jvm.buffer.memory.usage"; + private static final String METRIC_NAME_LIMIT = "jvm.buffer.memory.limit"; + private static final String METRIC_NAME_COUNT = "jvm.buffer.count"; + private static final String METRIC_DESCRIPTION_USAGE = "Measure of memory used by buffers."; private static final String METRIC_DESCRIPTION_LIMIT = - "Measure of total memory capacity of buffers"; - private static final String METRIC_DESCRIPTION_COUNT = "Number of buffers in the pool"; + "Measure of total memory capacity of buffers."; + private static final String METRIC_DESCRIPTION_COUNT = "Number of buffers in the pool."; private static final String COUNT = "count"; private static final String MAX_CAPACITY = "maxCapacity"; private static final String MEMORY_USED = "memoryUsed"; private static final String EVENT_NAME = "jdk.DirectBufferStatistics"; - private static final Attributes ATTR = Attributes.of(Constants.ATTR_POOL, "direct"); + public static final AttributeKey ATTR_BUFFER_POOL = + AttributeKey.stringKey("jvm.buffer.pool.name"); + private static final Attributes ATTR = Attributes.of(ATTR_BUFFER_POOL, "direct"); private final List observables = new ArrayList<>(); diff --git a/instrumentation/runtime-telemetry/runtime-telemetry-java17/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java17/internal/classes/ClassesLoadedHandler.java b/instrumentation/runtime-telemetry/runtime-telemetry-java17/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java17/internal/classes/ClassesLoadedHandler.java index 0e80e79b1c7b..98bee7942725 100644 --- a/instrumentation/runtime-telemetry/runtime-telemetry-java17/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java17/internal/classes/ClassesLoadedHandler.java +++ b/instrumentation/runtime-telemetry/runtime-telemetry-java17/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java17/internal/classes/ClassesLoadedHandler.java @@ -21,25 +21,25 @@ */ public final class ClassesLoadedHandler implements RecordedEventHandler { /** - * process.runtime.jvm.classes.loaded is the total number of classes loaded since JVM start. See: - * https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/semantic_conventions/runtime-environment-metrics.md#jvm-metrics + * jvm.class.loaded is the total number of classes loaded since JVM start. See: + * https://github.com/open-telemetry/semantic-conventions/blob/main/docs/runtime/jvm-metrics.md */ - private static final String METRIC_NAME_LOADED = "process.runtime.jvm.classes.loaded"; + private static final String METRIC_NAME_LOADED = "jvm.class.loaded"; - private static final String METRIC_NAME_UNLOADED = "process.runtime.jvm.classes.unloaded"; + private static final String METRIC_NAME_UNLOADED = "jvm.class.unloaded"; /** - * process.runtime.jvm.classes.current_loaded is the number of classes loaded at the time of - * jdk.ClassLoadingStatistics event emission. + * jvm.class.count is the number of classes loaded at the time of jdk.ClassLoadingStatistics event + * emission. */ - private static final String METRIC_NAME_CURRENT = "process.runtime.jvm.classes.current_loaded"; + private static final String METRIC_NAME_CURRENT = "jvm.class.count"; private static final String EVENT_NAME = "jdk.ClassLoadingStatistics"; - private static final String METRIC_DESCRIPTION_CURRENT = "Number of classes currently loaded"; + private static final String METRIC_DESCRIPTION_CURRENT = "Number of classes currently loaded."; private static final String METRIC_DESCRIPTION_LOADED = - "Number of classes loaded since JVM start"; + "Number of classes loaded since JVM start."; private static final String METRIC_DESCRIPTION_UNLOADED = - "Number of classes unloaded since JVM start"; + "Number of classes unloaded since JVM start."; private final List observables = new ArrayList<>(); diff --git a/instrumentation/runtime-telemetry/runtime-telemetry-java17/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java17/internal/container/ContainerConfigurationHandler.java b/instrumentation/runtime-telemetry/runtime-telemetry-java17/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java17/internal/container/ContainerConfigurationHandler.java index 4af00f9f7834..09220d3c6beb 100644 --- a/instrumentation/runtime-telemetry/runtime-telemetry-java17/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java17/internal/container/ContainerConfigurationHandler.java +++ b/instrumentation/runtime-telemetry/runtime-telemetry-java17/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java17/internal/container/ContainerConfigurationHandler.java @@ -18,7 +18,7 @@ * any time. */ public final class ContainerConfigurationHandler implements RecordedEventHandler { - private static final String METRIC_NAME = "process.runtime.jvm.cpu.limit"; + private static final String METRIC_NAME = "jvm.cpu.limit"; private static final String EVENT_NAME = "jdk.ContainerConfiguration"; private static final String EFFECTIVE_CPU_COUNT = "effectiveCpuCount"; diff --git a/instrumentation/runtime-telemetry/runtime-telemetry-java17/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java17/internal/cpu/ContextSwitchRateHandler.java b/instrumentation/runtime-telemetry/runtime-telemetry-java17/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java17/internal/cpu/ContextSwitchRateHandler.java index e9a84387544f..39a3dc6efa7c 100644 --- a/instrumentation/runtime-telemetry/runtime-telemetry-java17/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java17/internal/cpu/ContextSwitchRateHandler.java +++ b/instrumentation/runtime-telemetry/runtime-telemetry-java17/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java17/internal/cpu/ContextSwitchRateHandler.java @@ -20,7 +20,7 @@ * any time. */ public final class ContextSwitchRateHandler implements RecordedEventHandler { - private static final String METRIC_NAME = "process.runtime.jvm.cpu.context_switch"; + private static final String METRIC_NAME = "jvm.cpu.context_switch"; private static final String EVENT_NAME = "jdk.ThreadContextSwitchRate"; private final List observables = new ArrayList<>(); diff --git a/instrumentation/runtime-telemetry/runtime-telemetry-java17/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java17/internal/cpu/LongLockHandler.java b/instrumentation/runtime-telemetry/runtime-telemetry-java17/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java17/internal/cpu/LongLockHandler.java index 27f9700434f8..8eb26fc9f67b 100644 --- a/instrumentation/runtime-telemetry/runtime-telemetry-java17/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java17/internal/cpu/LongLockHandler.java +++ b/instrumentation/runtime-telemetry/runtime-telemetry-java17/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java17/internal/cpu/LongLockHandler.java @@ -23,7 +23,7 @@ * any time. */ public final class LongLockHandler extends AbstractThreadDispatchingHandler { - private static final String METRIC_NAME = "process.runtime.jvm.cpu.longlock"; + private static final String METRIC_NAME = "jvm.cpu.longlock"; private static final String METRIC_DESCRIPTION = "Long lock times"; private static final String EVENT_NAME = "jdk.JavaMonitorWait"; @@ -35,7 +35,7 @@ public LongLockHandler(Meter meter, ThreadGrouper grouper) { meter .histogramBuilder(METRIC_NAME) .setDescription(METRIC_DESCRIPTION) - .setUnit(Constants.MILLISECONDS) + .setUnit(Constants.SECONDS) .build(); } @@ -73,7 +73,7 @@ public PerThreadLongLockHandler(DoubleHistogram histogram, String threadName) { @Override public void accept(RecordedEvent recordedEvent) { if (recordedEvent.hasField(EVENT_THREAD)) { - histogram.record(DurationUtil.toMillis(recordedEvent.getDuration()), attributes); + histogram.record(DurationUtil.toSeconds(recordedEvent.getDuration()), attributes); } // What about the class name in MONITOR_CLASS ? // We can get a stack trace from the thread on the event diff --git a/instrumentation/runtime-telemetry/runtime-telemetry-java17/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java17/internal/cpu/OverallCpuLoadHandler.java b/instrumentation/runtime-telemetry/runtime-telemetry-java17/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java17/internal/cpu/OverallCpuLoadHandler.java index 54c423b28ae7..260b26857c9e 100644 --- a/instrumentation/runtime-telemetry/runtime-telemetry-java17/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java17/internal/cpu/OverallCpuLoadHandler.java +++ b/instrumentation/runtime-telemetry/runtime-telemetry-java17/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java17/internal/cpu/OverallCpuLoadHandler.java @@ -20,11 +20,12 @@ * any time. */ public final class OverallCpuLoadHandler implements RecordedEventHandler { - private static final String METRIC_NAME_PROCESS = "process.runtime.jvm.cpu.utilization"; - private static final String METRIC_NAME_MACHINE = "process.runtime.jvm.system.cpu.utilization"; - private static final String METRIC_DESCRIPTION_PROCESS = "Recent CPU utilization for the process"; + private static final String METRIC_NAME_PROCESS = "jvm.cpu.recent_utilization"; + private static final String METRIC_NAME_MACHINE = "jvm.system.cpu.utilization"; + private static final String METRIC_DESCRIPTION_PROCESS = + "Recent CPU utilization for the process as reported by the JVM."; private static final String METRIC_DESCRIPTION_MACHINE = - "Recent CPU utilization for the whole system"; + "Recent CPU utilization for the whole system as reported by the JVM."; private static final String EVENT_NAME = "jdk.CPULoad"; private static final String JVM_USER = "jvmUser"; diff --git a/instrumentation/runtime-telemetry/runtime-telemetry-java17/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java17/internal/garbagecollection/G1GarbageCollectionHandler.java b/instrumentation/runtime-telemetry/runtime-telemetry-java17/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java17/internal/garbagecollection/G1GarbageCollectionHandler.java index e67f8fb1ba46..cc5720df2fdb 100644 --- a/instrumentation/runtime-telemetry/runtime-telemetry-java17/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java17/internal/garbagecollection/G1GarbageCollectionHandler.java +++ b/instrumentation/runtime-telemetry/runtime-telemetry-java17/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java17/internal/garbagecollection/G1GarbageCollectionHandler.java @@ -6,7 +6,7 @@ package io.opentelemetry.instrumentation.runtimemetrics.java17.internal.garbagecollection; import io.opentelemetry.api.common.Attributes; -import io.opentelemetry.api.metrics.LongHistogram; +import io.opentelemetry.api.metrics.DoubleHistogram; import io.opentelemetry.api.metrics.Meter; import io.opentelemetry.instrumentation.runtimemetrics.java17.JfrFeature; import io.opentelemetry.instrumentation.runtimemetrics.java17.internal.Constants; @@ -23,25 +23,24 @@ public final class G1GarbageCollectionHandler implements RecordedEventHandler { private static final String EVENT_NAME = "jdk.G1GarbageCollection"; private static final Attributes ATTR = Attributes.of( - Constants.ATTR_GC, + Constants.ATTR_GC_NAME, "G1 Young Generation", - Constants.ATTR_ACTION, + Constants.ATTR_GC_ACTION, Constants.END_OF_MINOR_GC); - private final LongHistogram histogram; + private final DoubleHistogram histogram; public G1GarbageCollectionHandler(Meter meter) { histogram = meter .histogramBuilder(Constants.METRIC_NAME_GC_DURATION) .setDescription(Constants.METRIC_DESCRIPTION_GC_DURATION) - .setUnit(Constants.MILLISECONDS) - .ofLongs() + .setUnit(Constants.SECONDS) .build(); } @Override public void accept(RecordedEvent ev) { - histogram.record(ev.getLong(Constants.DURATION), ATTR); + histogram.record(ev.getDouble(Constants.DURATION), ATTR); } @Override diff --git a/instrumentation/runtime-telemetry/runtime-telemetry-java17/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java17/internal/garbagecollection/OldGarbageCollectionHandler.java b/instrumentation/runtime-telemetry/runtime-telemetry-java17/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java17/internal/garbagecollection/OldGarbageCollectionHandler.java index c402450fa73a..26bff6b82108 100644 --- a/instrumentation/runtime-telemetry/runtime-telemetry-java17/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java17/internal/garbagecollection/OldGarbageCollectionHandler.java +++ b/instrumentation/runtime-telemetry/runtime-telemetry-java17/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java17/internal/garbagecollection/OldGarbageCollectionHandler.java @@ -6,7 +6,7 @@ package io.opentelemetry.instrumentation.runtimemetrics.java17.internal.garbagecollection; import io.opentelemetry.api.common.Attributes; -import io.opentelemetry.api.metrics.LongHistogram; +import io.opentelemetry.api.metrics.DoubleHistogram; import io.opentelemetry.api.metrics.Meter; import io.opentelemetry.instrumentation.runtimemetrics.java17.JfrFeature; import io.opentelemetry.instrumentation.runtimemetrics.java17.internal.Constants; @@ -22,7 +22,7 @@ public final class OldGarbageCollectionHandler implements RecordedEventHandler { private static final String EVENT_NAME = "jdk.OldGarbageCollection"; - private final LongHistogram histogram; + private final DoubleHistogram histogram; private final Attributes attributes; public OldGarbageCollectionHandler(Meter meter, String gc) { @@ -30,17 +30,17 @@ public OldGarbageCollectionHandler(Meter meter, String gc) { meter .histogramBuilder(Constants.METRIC_NAME_GC_DURATION) .setDescription(Constants.METRIC_DESCRIPTION_GC_DURATION) - .setUnit(Constants.MILLISECONDS) - .ofLongs() + .setUnit(Constants.SECONDS) .build(); // Set the attribute's GC based on which GC is being used. attributes = - Attributes.of(Constants.ATTR_GC, gc, Constants.ATTR_ACTION, Constants.END_OF_MAJOR_GC); + Attributes.of( + Constants.ATTR_GC_NAME, gc, Constants.ATTR_GC_ACTION, Constants.END_OF_MAJOR_GC); } @Override public void accept(RecordedEvent ev) { - histogram.record(ev.getLong(Constants.DURATION), attributes); + histogram.record(ev.getDouble(Constants.DURATION), attributes); } @Override diff --git a/instrumentation/runtime-telemetry/runtime-telemetry-java17/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java17/internal/garbagecollection/YoungGarbageCollectionHandler.java b/instrumentation/runtime-telemetry/runtime-telemetry-java17/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java17/internal/garbagecollection/YoungGarbageCollectionHandler.java index 88d8a1915bfd..534b131de2ee 100644 --- a/instrumentation/runtime-telemetry/runtime-telemetry-java17/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java17/internal/garbagecollection/YoungGarbageCollectionHandler.java +++ b/instrumentation/runtime-telemetry/runtime-telemetry-java17/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java17/internal/garbagecollection/YoungGarbageCollectionHandler.java @@ -6,7 +6,7 @@ package io.opentelemetry.instrumentation.runtimemetrics.java17.internal.garbagecollection; import io.opentelemetry.api.common.Attributes; -import io.opentelemetry.api.metrics.LongHistogram; +import io.opentelemetry.api.metrics.DoubleHistogram; import io.opentelemetry.api.metrics.Meter; import io.opentelemetry.instrumentation.runtimemetrics.java17.JfrFeature; import io.opentelemetry.instrumentation.runtimemetrics.java17.internal.Constants; @@ -22,7 +22,7 @@ public final class YoungGarbageCollectionHandler implements RecordedEventHandler { private static final String EVENT_NAME = "jdk.YoungGarbageCollection"; - private final LongHistogram histogram; + private final DoubleHistogram histogram; private final Attributes attributes; public YoungGarbageCollectionHandler(Meter meter, String gc) { @@ -30,18 +30,18 @@ public YoungGarbageCollectionHandler(Meter meter, String gc) { meter .histogramBuilder(Constants.METRIC_NAME_GC_DURATION) .setDescription(Constants.METRIC_DESCRIPTION_GC_DURATION) - .setUnit(Constants.MILLISECONDS) - .ofLongs() + .setUnit(Constants.SECONDS) .build(); // Set the attribute's GC based on which GC is being used. // G1 young collection is already handled by G1GarbageCollectionHandler. attributes = - Attributes.of(Constants.ATTR_GC, gc, Constants.ATTR_ACTION, Constants.END_OF_MINOR_GC); + Attributes.of( + Constants.ATTR_GC_NAME, gc, Constants.ATTR_GC_ACTION, Constants.END_OF_MINOR_GC); } @Override public void accept(RecordedEvent ev) { - histogram.record(ev.getLong(Constants.DURATION), attributes); + histogram.record(ev.getDouble(Constants.DURATION), attributes); } @Override diff --git a/instrumentation/runtime-telemetry/runtime-telemetry-java17/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java17/internal/memory/CodeCacheConfigurationHandler.java b/instrumentation/runtime-telemetry/runtime-telemetry-java17/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java17/internal/memory/CodeCacheConfigurationHandler.java index d96387941d5f..1b11c80ea4d1 100644 --- a/instrumentation/runtime-telemetry/runtime-telemetry-java17/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java17/internal/memory/CodeCacheConfigurationHandler.java +++ b/instrumentation/runtime-telemetry/runtime-telemetry-java17/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java17/internal/memory/CodeCacheConfigurationHandler.java @@ -24,7 +24,8 @@ public final class CodeCacheConfigurationHandler implements RecordedEventHandler private static final String EVENT_NAME = "jdk.CodeCacheConfiguration"; private static final Attributes ATTR = - Attributes.of(Constants.ATTR_TYPE, Constants.NON_HEAP, Constants.ATTR_POOL, "CodeCache"); + Attributes.of( + Constants.ATTR_MEMORY_TYPE, Constants.NON_HEAP, Constants.ATTR_MEMORY_POOL, "CodeCache"); private final List observables = new ArrayList<>(); diff --git a/instrumentation/runtime-telemetry/runtime-telemetry-java17/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java17/internal/memory/G1HeapSummaryHandler.java b/instrumentation/runtime-telemetry/runtime-telemetry-java17/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java17/internal/memory/G1HeapSummaryHandler.java index 3b850b128d0c..05c8a49fb5cc 100644 --- a/instrumentation/runtime-telemetry/runtime-telemetry-java17/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java17/internal/memory/G1HeapSummaryHandler.java +++ b/instrumentation/runtime-telemetry/runtime-telemetry-java17/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java17/internal/memory/G1HeapSummaryHandler.java @@ -32,9 +32,14 @@ public final class G1HeapSummaryHandler implements RecordedEventHandler { private static final String SURVIVOR_USED_SIZE = "survivorUsedSize"; private static final String WHEN = "when"; private static final Attributes ATTR_MEMORY_EDEN = - Attributes.of(Constants.ATTR_TYPE, Constants.HEAP, Constants.ATTR_POOL, "G1 Eden Space"); + Attributes.of( + Constants.ATTR_MEMORY_TYPE, Constants.HEAP, Constants.ATTR_MEMORY_POOL, "G1 Eden Space"); private static final Attributes ATTR_MEMORY_SURVIVOR = - Attributes.of(Constants.ATTR_TYPE, Constants.HEAP, Constants.ATTR_POOL, "G1 Survivor Space"); + Attributes.of( + Constants.ATTR_MEMORY_TYPE, + Constants.HEAP, + Constants.ATTR_MEMORY_POOL, + "G1 Survivor Space"); // private static final Attributes ATTR_MEMORY_OLD_USED = // Attributes.of(ATTR_TYPE, HEAP, ATTR_POOL, "G1 Old Gen"); // TODO needs jdk JFR support diff --git a/instrumentation/runtime-telemetry/runtime-telemetry-java17/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java17/internal/memory/MetaspaceSummaryHandler.java b/instrumentation/runtime-telemetry/runtime-telemetry-java17/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java17/internal/memory/MetaspaceSummaryHandler.java index f02c989f47fc..fcbe93cafbab 100644 --- a/instrumentation/runtime-telemetry/runtime-telemetry-java17/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java17/internal/memory/MetaspaceSummaryHandler.java +++ b/instrumentation/runtime-telemetry/runtime-telemetry-java17/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java17/internal/memory/MetaspaceSummaryHandler.java @@ -28,10 +28,14 @@ public final class MetaspaceSummaryHandler implements RecordedEventHandler { private static final String EVENT_NAME = "jdk.MetaspaceSummary"; private static final Attributes ATTR_MEMORY_METASPACE = - Attributes.of(Constants.ATTR_TYPE, Constants.NON_HEAP, Constants.ATTR_POOL, "Metaspace"); + Attributes.of( + Constants.ATTR_MEMORY_TYPE, Constants.NON_HEAP, Constants.ATTR_MEMORY_POOL, "Metaspace"); private static final Attributes ATTR_MEMORY_COMPRESSED_CLASS_SPACE = Attributes.of( - Constants.ATTR_TYPE, Constants.NON_HEAP, Constants.ATTR_POOL, "Compressed Class Space"); + Constants.ATTR_MEMORY_TYPE, + Constants.NON_HEAP, + Constants.ATTR_MEMORY_POOL, + "Compressed Class Space"); private final List observables = new ArrayList<>(); diff --git a/instrumentation/runtime-telemetry/runtime-telemetry-java17/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java17/internal/memory/ParallelHeapSummaryHandler.java b/instrumentation/runtime-telemetry/runtime-telemetry-java17/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java17/internal/memory/ParallelHeapSummaryHandler.java index c187f1d9401c..3ec8e57c845d 100644 --- a/instrumentation/runtime-telemetry/runtime-telemetry-java17/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java17/internal/memory/ParallelHeapSummaryHandler.java +++ b/instrumentation/runtime-telemetry/runtime-telemetry-java17/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java17/internal/memory/ParallelHeapSummaryHandler.java @@ -33,11 +33,17 @@ public final class ParallelHeapSummaryHandler implements RecordedEventHandler { private static final String WHEN = "when"; private static final String SIZE = "size"; private static final Attributes ATTR_MEMORY_EDEN = - Attributes.of(Constants.ATTR_TYPE, Constants.HEAP, Constants.ATTR_POOL, "PS Eden Space"); + Attributes.of( + Constants.ATTR_MEMORY_TYPE, Constants.HEAP, Constants.ATTR_MEMORY_POOL, "PS Eden Space"); private static final Attributes ATTR_MEMORY_SURVIVOR = - Attributes.of(Constants.ATTR_TYPE, Constants.HEAP, Constants.ATTR_POOL, "PS Survivor Space"); + Attributes.of( + Constants.ATTR_MEMORY_TYPE, + Constants.HEAP, + Constants.ATTR_MEMORY_POOL, + "PS Survivor Space"); private static final Attributes ATTR_MEMORY_OLD = - Attributes.of(Constants.ATTR_TYPE, Constants.HEAP, Constants.ATTR_POOL, "PS Old Gen"); + Attributes.of( + Constants.ATTR_MEMORY_TYPE, Constants.HEAP, Constants.ATTR_MEMORY_POOL, "PS Old Gen"); private final List observables = new ArrayList<>(); diff --git a/instrumentation/runtime-telemetry/runtime-telemetry-java17/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java17/internal/network/NetworkReadHandler.java b/instrumentation/runtime-telemetry/runtime-telemetry-java17/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java17/internal/network/NetworkReadHandler.java index 4764a09a49e3..b1b1a6eee005 100644 --- a/instrumentation/runtime-telemetry/runtime-telemetry-java17/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java17/internal/network/NetworkReadHandler.java +++ b/instrumentation/runtime-telemetry/runtime-telemetry-java17/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java17/internal/network/NetworkReadHandler.java @@ -40,7 +40,7 @@ public NetworkReadHandler(Meter meter, ThreadGrouper nameNormalizer) { meter .histogramBuilder(Constants.METRIC_NAME_NETWORK_DURATION) .setDescription(Constants.METRIC_DESCRIPTION_NETWORK_DURATION) - .setUnit(Constants.MILLISECONDS) + .setUnit(Constants.SECONDS) .build(); } @@ -81,7 +81,7 @@ public PerThreadNetworkReadHandler( @Override public void accept(RecordedEvent ev) { bytesHistogram.record(ev.getLong(BYTES_READ), attributes); - durationHistogram.record(DurationUtil.toMillis(ev.getDuration()), attributes); + durationHistogram.record(DurationUtil.toSeconds(ev.getDuration()), attributes); } } } diff --git a/instrumentation/runtime-telemetry/runtime-telemetry-java17/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java17/internal/network/NetworkWriteHandler.java b/instrumentation/runtime-telemetry/runtime-telemetry-java17/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java17/internal/network/NetworkWriteHandler.java index 792cba803014..4de58802fdcf 100644 --- a/instrumentation/runtime-telemetry/runtime-telemetry-java17/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java17/internal/network/NetworkWriteHandler.java +++ b/instrumentation/runtime-telemetry/runtime-telemetry-java17/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java17/internal/network/NetworkWriteHandler.java @@ -58,7 +58,7 @@ public NetworkWriteHandler(Meter meter, ThreadGrouper nameNormalizer) { meter .histogramBuilder(Constants.METRIC_NAME_NETWORK_DURATION) .setDescription(Constants.METRIC_DESCRIPTION_NETWORK_DURATION) - .setUnit(Constants.MILLISECONDS) + .setUnit(Constants.SECONDS) .build(); } @@ -99,7 +99,7 @@ private PerThreadNetworkWriteHandler( @Override public void accept(RecordedEvent ev) { bytesHistogram.record(ev.getLong(BYTES_WRITTEN), attributes); - durationHistogram.record(DurationUtil.toMillis(ev.getDuration()), attributes); + durationHistogram.record(DurationUtil.toSeconds(ev.getDuration()), attributes); } } } diff --git a/instrumentation/runtime-telemetry/runtime-telemetry-java17/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java17/internal/threads/ThreadCountHandler.java b/instrumentation/runtime-telemetry/runtime-telemetry-java17/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java17/internal/threads/ThreadCountHandler.java index 8d5cc57a99fc..ef741482cc3f 100644 --- a/instrumentation/runtime-telemetry/runtime-telemetry-java17/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java17/internal/threads/ThreadCountHandler.java +++ b/instrumentation/runtime-telemetry/runtime-telemetry-java17/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java17/internal/threads/ThreadCountHandler.java @@ -21,7 +21,7 @@ * any time. */ public final class ThreadCountHandler implements RecordedEventHandler { - private static final String METRIC_NAME = "process.runtime.jvm.threads.count"; + private static final String METRIC_NAME = "jvm.thread.count"; private static final String EVENT_NAME = "jdk.JavaThreadStatistics"; private static final String METRIC_DESCRIPTION = "Number of executing threads"; private static final Attributes ATTR_DAEMON_TRUE = Attributes.of(Constants.ATTR_DAEMON, true); diff --git a/instrumentation/runtime-telemetry/runtime-telemetry-java17/library/src/test/java/io/opentelemetry/instrumentation/runtimemetrics/java17/BufferMetricTest.java b/instrumentation/runtime-telemetry/runtime-telemetry-java17/library/src/test/java/io/opentelemetry/instrumentation/runtimemetrics/java17/BufferMetricTest.java index bfce75d5ab94..c794891ecb06 100644 --- a/instrumentation/runtime-telemetry/runtime-telemetry-java17/library/src/test/java/io/opentelemetry/instrumentation/runtimemetrics/java17/BufferMetricTest.java +++ b/instrumentation/runtime-telemetry/runtime-telemetry-java17/library/src/test/java/io/opentelemetry/instrumentation/runtimemetrics/java17/BufferMetricTest.java @@ -5,11 +5,11 @@ package io.opentelemetry.instrumentation.runtimemetrics.java17; -import static io.opentelemetry.instrumentation.runtimemetrics.java17.internal.Constants.ATTR_POOL; import static io.opentelemetry.instrumentation.runtimemetrics.java17.internal.Constants.BYTES; import static io.opentelemetry.instrumentation.runtimemetrics.java17.internal.Constants.UNIT_BUFFERS; import static org.assertj.core.api.Assertions.assertThat; +import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.common.Attributes; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; @@ -39,12 +39,13 @@ void shouldHaveJfrLoadedClassesCountEvents() { ByteBuffer buffer = ByteBuffer.allocateDirect(10000); buffer.put("test".getBytes(StandardCharsets.UTF_8)); - Attributes directBuffer = Attributes.of(ATTR_POOL, "direct"); + AttributeKey attrBufferPool = AttributeKey.stringKey("jvm.buffer.pool.name"); + Attributes directBuffer = Attributes.of(attrBufferPool, "direct"); jfrExtension.waitAndAssertMetrics( metric -> metric - .hasName("process.runtime.jvm.buffer.count") - .hasDescription("Number of buffers in the pool") + .hasName("jvm.buffer.count") + .hasDescription("Number of buffers in the pool.") .hasUnit(UNIT_BUFFERS) .hasLongSumSatisfying( sum -> @@ -57,8 +58,8 @@ void shouldHaveJfrLoadedClassesCountEvents() { }))), metric -> metric - .hasName("process.runtime.jvm.buffer.limit") - .hasDescription("Measure of total memory capacity of buffers") + .hasName("jvm.buffer.memory.limit") + .hasDescription("Measure of total memory capacity of buffers.") .hasUnit(BYTES) .hasLongSumSatisfying( sum -> @@ -71,8 +72,8 @@ void shouldHaveJfrLoadedClassesCountEvents() { }))), metric -> metric - .hasName("process.runtime.jvm.buffer.usage") - .hasDescription("Measure of memory used by buffers") + .hasName("jvm.buffer.memory.usage") + .hasDescription("Measure of memory used by buffers.") .hasUnit(BYTES) .hasLongSumSatisfying( sum -> diff --git a/instrumentation/runtime-telemetry/runtime-telemetry-java17/library/src/test/java/io/opentelemetry/instrumentation/runtimemetrics/java17/G1GcMemoryMetricTest.java b/instrumentation/runtime-telemetry/runtime-telemetry-java17/library/src/test/java/io/opentelemetry/instrumentation/runtimemetrics/java17/G1GcMemoryMetricTest.java index b9d5c39ff600..f341d8920b93 100644 --- a/instrumentation/runtime-telemetry/runtime-telemetry-java17/library/src/test/java/io/opentelemetry/instrumentation/runtimemetrics/java17/G1GcMemoryMetricTest.java +++ b/instrumentation/runtime-telemetry/runtime-telemetry-java17/library/src/test/java/io/opentelemetry/instrumentation/runtimemetrics/java17/G1GcMemoryMetricTest.java @@ -5,10 +5,10 @@ package io.opentelemetry.instrumentation.runtimemetrics.java17; -import static io.opentelemetry.instrumentation.runtimemetrics.java17.internal.Constants.ATTR_ACTION; import static io.opentelemetry.instrumentation.runtimemetrics.java17.internal.Constants.ATTR_G1_EDEN_SPACE; import static io.opentelemetry.instrumentation.runtimemetrics.java17.internal.Constants.ATTR_G1_SURVIVOR_SPACE; -import static io.opentelemetry.instrumentation.runtimemetrics.java17.internal.Constants.ATTR_GC; +import static io.opentelemetry.instrumentation.runtimemetrics.java17.internal.Constants.ATTR_GC_ACTION; +import static io.opentelemetry.instrumentation.runtimemetrics.java17.internal.Constants.ATTR_GC_NAME; import static io.opentelemetry.instrumentation.runtimemetrics.java17.internal.Constants.BYTES; import static io.opentelemetry.instrumentation.runtimemetrics.java17.internal.Constants.END_OF_MAJOR_GC; import static io.opentelemetry.instrumentation.runtimemetrics.java17.internal.Constants.END_OF_MINOR_GC; @@ -16,9 +16,11 @@ import static io.opentelemetry.instrumentation.runtimemetrics.java17.internal.Constants.METRIC_DESCRIPTION_GC_DURATION; import static io.opentelemetry.instrumentation.runtimemetrics.java17.internal.Constants.METRIC_DESCRIPTION_MEMORY; import static io.opentelemetry.instrumentation.runtimemetrics.java17.internal.Constants.METRIC_DESCRIPTION_MEMORY_AFTER; +import static io.opentelemetry.instrumentation.runtimemetrics.java17.internal.Constants.METRIC_NAME_COMMITTED; +import static io.opentelemetry.instrumentation.runtimemetrics.java17.internal.Constants.METRIC_NAME_GC_DURATION; import static io.opentelemetry.instrumentation.runtimemetrics.java17.internal.Constants.METRIC_NAME_MEMORY; import static io.opentelemetry.instrumentation.runtimemetrics.java17.internal.Constants.METRIC_NAME_MEMORY_AFTER; -import static io.opentelemetry.instrumentation.runtimemetrics.java17.internal.Constants.MILLISECONDS; +import static io.opentelemetry.instrumentation.runtimemetrics.java17.internal.Constants.SECONDS; import static org.assertj.core.api.Assertions.assertThat; import io.opentelemetry.api.common.Attributes; @@ -42,7 +44,7 @@ void shouldHaveMemoryMetrics() { System.gc(); // Test to make sure there's metric data for both eden and survivor spaces. // TODO: once G1 old gen usage added to jdk.G1HeapSummary (in JDK 21), test for it here too. - // TODO: needs JFR support for process.runtime.jvm.memory.limit. + // TODO: needs JFR support for jvm.memory.limit. jfrExtension.waitAndAssertMetrics( metric -> metric @@ -52,7 +54,7 @@ void shouldHaveMemoryMetrics() { .satisfies(G1GcMemoryMetricTest::hasGcAttributes), metric -> metric - .hasName("process.runtime.jvm.memory.committed") + .hasName(METRIC_NAME_COMMITTED) .hasUnit(BYTES) .hasDescription(METRIC_DESCRIPTION_COMMITTED) // TODO: need JFR support for the other G1 pools @@ -79,14 +81,14 @@ void shouldHaveGcDurationMetrics() { // TODO: Need a reliable way to test old and young gen GC in isolation. System.gc(); Attributes minorGcAttributes = - Attributes.of(ATTR_GC, "G1 Young Generation", ATTR_ACTION, END_OF_MINOR_GC); + Attributes.of(ATTR_GC_NAME, "G1 Young Generation", ATTR_GC_ACTION, END_OF_MINOR_GC); Attributes majorGcAttributes = - Attributes.of(ATTR_GC, "G1 Old Generation", ATTR_ACTION, END_OF_MAJOR_GC); + Attributes.of(ATTR_GC_NAME, "G1 Old Generation", ATTR_GC_ACTION, END_OF_MAJOR_GC); jfrExtension.waitAndAssertMetrics( metric -> metric - .hasName("process.runtime.jvm.gc.duration") - .hasUnit(MILLISECONDS) + .hasName(METRIC_NAME_GC_DURATION) + .hasUnit(SECONDS) .hasDescription(METRIC_DESCRIPTION_GC_DURATION) .satisfies( data -> diff --git a/instrumentation/runtime-telemetry/runtime-telemetry-java17/library/src/test/java/io/opentelemetry/instrumentation/runtimemetrics/java17/JfrClassesLoadedCountTest.java b/instrumentation/runtime-telemetry/runtime-telemetry-java17/library/src/test/java/io/opentelemetry/instrumentation/runtimemetrics/java17/JfrClassesLoadedCountTest.java index a8b39a07f634..4e5eb144be30 100644 --- a/instrumentation/runtime-telemetry/runtime-telemetry-java17/library/src/test/java/io/opentelemetry/instrumentation/runtimemetrics/java17/JfrClassesLoadedCountTest.java +++ b/instrumentation/runtime-telemetry/runtime-telemetry-java17/library/src/test/java/io/opentelemetry/instrumentation/runtimemetrics/java17/JfrClassesLoadedCountTest.java @@ -25,8 +25,8 @@ void shouldHaveJfrLoadedClassesCountEvents() throws Exception { jfrExtension.waitAndAssertMetrics( metric -> metric - .hasName("process.runtime.jvm.classes.loaded") - .hasDescription("Number of classes loaded since JVM start") + .hasName("jvm.class.loaded") + .hasDescription("Number of classes loaded since JVM start.") .hasUnit(UNIT_CLASSES) .hasLongSumSatisfying( sum -> @@ -38,8 +38,8 @@ void shouldHaveJfrLoadedClassesCountEvents() throws Exception { .isGreaterThanOrEqualTo(0)))), metric -> metric - .hasName("process.runtime.jvm.classes.current_loaded") - .hasDescription("Number of classes currently loaded") + .hasName("jvm.class.count") + .hasDescription("Number of classes currently loaded.") .hasUnit(UNIT_CLASSES) .hasLongSumSatisfying( sum -> @@ -51,8 +51,8 @@ void shouldHaveJfrLoadedClassesCountEvents() throws Exception { .isGreaterThanOrEqualTo(0)))), metric -> metric - .hasName("process.runtime.jvm.classes.unloaded") - .hasDescription("Number of classes unloaded since JVM start") + .hasName("jvm.class.unloaded") + .hasDescription("Number of classes unloaded since JVM start.") .hasUnit(UNIT_CLASSES) .hasLongSumSatisfying( sum -> diff --git a/instrumentation/runtime-telemetry/runtime-telemetry-java17/library/src/test/java/io/opentelemetry/instrumentation/runtimemetrics/java17/JfrCpuLockTest.java b/instrumentation/runtime-telemetry/runtime-telemetry-java17/library/src/test/java/io/opentelemetry/instrumentation/runtimemetrics/java17/JfrCpuLockTest.java index 0e10b500d770..ce16fc528457 100644 --- a/instrumentation/runtime-telemetry/runtime-telemetry-java17/library/src/test/java/io/opentelemetry/instrumentation/runtimemetrics/java17/JfrCpuLockTest.java +++ b/instrumentation/runtime-telemetry/runtime-telemetry-java17/library/src/test/java/io/opentelemetry/instrumentation/runtimemetrics/java17/JfrCpuLockTest.java @@ -5,7 +5,7 @@ package io.opentelemetry.instrumentation.runtimemetrics.java17; -import static io.opentelemetry.instrumentation.runtimemetrics.java17.internal.Constants.MILLISECONDS; +import static io.opentelemetry.instrumentation.runtimemetrics.java17.internal.Constants.SECONDS; import static org.assertj.core.api.Assertions.assertThat; import java.time.Duration; @@ -50,8 +50,8 @@ void shouldHaveLockEvents() throws Exception { jfrExtension.waitAndAssertMetrics( metric -> metric - .hasName("process.runtime.jvm.cpu.longlock") - .hasUnit(MILLISECONDS) + .hasName("jvm.cpu.longlock") + .hasUnit(SECONDS) .hasHistogramSatisfying(histogram -> {})); } } diff --git a/instrumentation/runtime-telemetry/runtime-telemetry-java17/library/src/test/java/io/opentelemetry/instrumentation/runtimemetrics/java17/JfrOverallCpuLoadHandlerTest.java b/instrumentation/runtime-telemetry/runtime-telemetry-java17/library/src/test/java/io/opentelemetry/instrumentation/runtimemetrics/java17/JfrOverallCpuLoadHandlerTest.java index cd18f1ddef99..38c8585a50d0 100644 --- a/instrumentation/runtime-telemetry/runtime-telemetry-java17/library/src/test/java/io/opentelemetry/instrumentation/runtimemetrics/java17/JfrOverallCpuLoadHandlerTest.java +++ b/instrumentation/runtime-telemetry/runtime-telemetry-java17/library/src/test/java/io/opentelemetry/instrumentation/runtimemetrics/java17/JfrOverallCpuLoadHandlerTest.java @@ -23,15 +23,16 @@ void shouldHaveCpuLoadEvents() { jfrExtension.waitAndAssertMetrics( metric -> metric - .hasName("process.runtime.jvm.cpu.utilization") + .hasName("jvm.cpu.recent_utilization") .hasUnit(UNIT_UTILIZATION) - .hasDescription("Recent CPU utilization for the process") + .hasDescription("Recent CPU utilization for the process as reported by the JVM.") .hasDoubleGaugeSatisfying(gauge -> {}), metric -> metric - .hasName("process.runtime.jvm.system.cpu.utilization") + .hasName("jvm.system.cpu.utilization") .hasUnit(UNIT_UTILIZATION) - .hasDescription("Recent CPU utilization for the whole system") + .hasDescription( + "Recent CPU utilization for the whole system as reported by the JVM.") .hasDoubleGaugeSatisfying(gauge -> {})); } } diff --git a/instrumentation/runtime-telemetry/runtime-telemetry-java17/library/src/test/java/io/opentelemetry/instrumentation/runtimemetrics/java17/JfrThreadCountTest.java b/instrumentation/runtime-telemetry/runtime-telemetry-java17/library/src/test/java/io/opentelemetry/instrumentation/runtimemetrics/java17/JfrThreadCountTest.java index 6c19780b5675..38423a9b6105 100644 --- a/instrumentation/runtime-telemetry/runtime-telemetry-java17/library/src/test/java/io/opentelemetry/instrumentation/runtimemetrics/java17/JfrThreadCountTest.java +++ b/instrumentation/runtime-telemetry/runtime-telemetry-java17/library/src/test/java/io/opentelemetry/instrumentation/runtimemetrics/java17/JfrThreadCountTest.java @@ -5,11 +5,10 @@ package io.opentelemetry.instrumentation.runtimemetrics.java17; -import static io.opentelemetry.instrumentation.runtimemetrics.java17.internal.Constants.DAEMON; +import static io.opentelemetry.instrumentation.runtimemetrics.java17.internal.Constants.ATTR_DAEMON; import static io.opentelemetry.instrumentation.runtimemetrics.java17.internal.Constants.UNIT_THREADS; import static org.assertj.core.api.Assertions.assertThat; -import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.sdk.metrics.data.LongPointData; import java.util.Objects; import org.junit.jupiter.api.Test; @@ -47,7 +46,7 @@ void shouldHaveJfrThreadCountEvents() throws Exception { jfrExtension.waitAndAssertMetrics( metric -> metric - .hasName("process.runtime.jvm.threads.count") + .hasName("jvm.thread.count") .hasUnit(UNIT_THREADS) .satisfies( data -> @@ -57,6 +56,6 @@ void shouldHaveJfrThreadCountEvents() throws Exception { } private static boolean isDaemon(LongPointData p) { - return Objects.requireNonNull(p.getAttributes().get(AttributeKey.booleanKey(DAEMON))); + return Objects.requireNonNull(p.getAttributes().get(ATTR_DAEMON)); } } diff --git a/instrumentation/runtime-telemetry/runtime-telemetry-java17/library/src/test/java/io/opentelemetry/instrumentation/runtimemetrics/java17/MetaspaceMemoryCommittedMetricTest.java b/instrumentation/runtime-telemetry/runtime-telemetry-java17/library/src/test/java/io/opentelemetry/instrumentation/runtimemetrics/java17/MetaspaceMemoryCommittedMetricTest.java index 2a553aab0def..768fca11771a 100644 --- a/instrumentation/runtime-telemetry/runtime-telemetry-java17/library/src/test/java/io/opentelemetry/instrumentation/runtimemetrics/java17/MetaspaceMemoryCommittedMetricTest.java +++ b/instrumentation/runtime-telemetry/runtime-telemetry-java17/library/src/test/java/io/opentelemetry/instrumentation/runtimemetrics/java17/MetaspaceMemoryCommittedMetricTest.java @@ -9,6 +9,7 @@ import static io.opentelemetry.instrumentation.runtimemetrics.java17.internal.Constants.ATTR_METASPACE; import static io.opentelemetry.instrumentation.runtimemetrics.java17.internal.Constants.BYTES; import static io.opentelemetry.instrumentation.runtimemetrics.java17.internal.Constants.METRIC_DESCRIPTION_COMMITTED; +import static io.opentelemetry.instrumentation.runtimemetrics.java17.internal.Constants.METRIC_NAME_COMMITTED; import static org.assertj.core.api.Assertions.assertThat; import org.junit.jupiter.api.Test; @@ -27,7 +28,7 @@ void shouldHaveMemoryCommittedMetrics() { jfrExtension.waitAndAssertMetrics( metric -> metric - .hasName("process.runtime.jvm.memory.committed") + .hasName(METRIC_NAME_COMMITTED) .hasUnit(BYTES) .hasDescription(METRIC_DESCRIPTION_COMMITTED) .satisfies( diff --git a/instrumentation/runtime-telemetry/runtime-telemetry-java17/library/src/test/java/io/opentelemetry/instrumentation/runtimemetrics/java17/MetaspaceMemoryLimitMetricTest.java b/instrumentation/runtime-telemetry/runtime-telemetry-java17/library/src/test/java/io/opentelemetry/instrumentation/runtimemetrics/java17/MetaspaceMemoryLimitMetricTest.java index 4332e60549d5..4ef4f41f6b3d 100644 --- a/instrumentation/runtime-telemetry/runtime-telemetry-java17/library/src/test/java/io/opentelemetry/instrumentation/runtimemetrics/java17/MetaspaceMemoryLimitMetricTest.java +++ b/instrumentation/runtime-telemetry/runtime-telemetry-java17/library/src/test/java/io/opentelemetry/instrumentation/runtimemetrics/java17/MetaspaceMemoryLimitMetricTest.java @@ -8,6 +8,7 @@ import static io.opentelemetry.instrumentation.runtimemetrics.java17.internal.Constants.ATTR_COMPRESSED_CLASS_SPACE; import static io.opentelemetry.instrumentation.runtimemetrics.java17.internal.Constants.BYTES; import static io.opentelemetry.instrumentation.runtimemetrics.java17.internal.Constants.METRIC_DESCRIPTION_MEMORY_LIMIT; +import static io.opentelemetry.instrumentation.runtimemetrics.java17.internal.Constants.METRIC_NAME_MEMORY_LIMIT; import static org.assertj.core.api.Assertions.assertThat; import org.junit.jupiter.api.Test; @@ -26,7 +27,7 @@ void shouldHaveMemoryLimitMetrics() { jfrExtension.waitAndAssertMetrics( metric -> metric - .hasName("process.runtime.jvm.memory.limit") + .hasName(METRIC_NAME_MEMORY_LIMIT) .hasUnit(BYTES) .hasDescription(METRIC_DESCRIPTION_MEMORY_LIMIT) .satisfies( diff --git a/instrumentation/runtime-telemetry/runtime-telemetry-java17/library/src/test/java/io/opentelemetry/instrumentation/runtimemetrics/java17/MetaspaceMemoryUsageMetricTest.java b/instrumentation/runtime-telemetry/runtime-telemetry-java17/library/src/test/java/io/opentelemetry/instrumentation/runtimemetrics/java17/MetaspaceMemoryUsageMetricTest.java index b5659d491f40..004115d11c33 100644 --- a/instrumentation/runtime-telemetry/runtime-telemetry-java17/library/src/test/java/io/opentelemetry/instrumentation/runtimemetrics/java17/MetaspaceMemoryUsageMetricTest.java +++ b/instrumentation/runtime-telemetry/runtime-telemetry-java17/library/src/test/java/io/opentelemetry/instrumentation/runtimemetrics/java17/MetaspaceMemoryUsageMetricTest.java @@ -22,7 +22,7 @@ class MetaspaceMemoryUsageMetricTest { new JfrExtension( builder -> builder.disableAllFeatures().enableFeature(JfrFeature.MEMORY_POOL_METRICS)); - /** This is a basic test for process.runtime.jvm.memory.usage. */ + /** This is a basic test for jvm.memory.used. */ @Test void shouldHaveMemoryUsageMetrics() { System.gc(); diff --git a/instrumentation/runtime-telemetry/runtime-telemetry-java17/library/src/test/java/io/opentelemetry/instrumentation/runtimemetrics/java17/PsGcMemoryMetricTest.java b/instrumentation/runtime-telemetry/runtime-telemetry-java17/library/src/test/java/io/opentelemetry/instrumentation/runtimemetrics/java17/PsGcMemoryMetricTest.java index d0aa14646b6e..eaba1c4ff9c4 100644 --- a/instrumentation/runtime-telemetry/runtime-telemetry-java17/library/src/test/java/io/opentelemetry/instrumentation/runtimemetrics/java17/PsGcMemoryMetricTest.java +++ b/instrumentation/runtime-telemetry/runtime-telemetry-java17/library/src/test/java/io/opentelemetry/instrumentation/runtimemetrics/java17/PsGcMemoryMetricTest.java @@ -5,8 +5,8 @@ package io.opentelemetry.instrumentation.runtimemetrics.java17; -import static io.opentelemetry.instrumentation.runtimemetrics.java17.internal.Constants.ATTR_ACTION; -import static io.opentelemetry.instrumentation.runtimemetrics.java17.internal.Constants.ATTR_GC; +import static io.opentelemetry.instrumentation.runtimemetrics.java17.internal.Constants.ATTR_GC_ACTION; +import static io.opentelemetry.instrumentation.runtimemetrics.java17.internal.Constants.ATTR_GC_NAME; import static io.opentelemetry.instrumentation.runtimemetrics.java17.internal.Constants.ATTR_PS_EDEN_SPACE; import static io.opentelemetry.instrumentation.runtimemetrics.java17.internal.Constants.ATTR_PS_OLD_GEN; import static io.opentelemetry.instrumentation.runtimemetrics.java17.internal.Constants.ATTR_PS_SURVIVOR_SPACE; @@ -18,9 +18,12 @@ import static io.opentelemetry.instrumentation.runtimemetrics.java17.internal.Constants.METRIC_DESCRIPTION_MEMORY; import static io.opentelemetry.instrumentation.runtimemetrics.java17.internal.Constants.METRIC_DESCRIPTION_MEMORY_AFTER; import static io.opentelemetry.instrumentation.runtimemetrics.java17.internal.Constants.METRIC_DESCRIPTION_MEMORY_LIMIT; +import static io.opentelemetry.instrumentation.runtimemetrics.java17.internal.Constants.METRIC_NAME_COMMITTED; +import static io.opentelemetry.instrumentation.runtimemetrics.java17.internal.Constants.METRIC_NAME_GC_DURATION; import static io.opentelemetry.instrumentation.runtimemetrics.java17.internal.Constants.METRIC_NAME_MEMORY; import static io.opentelemetry.instrumentation.runtimemetrics.java17.internal.Constants.METRIC_NAME_MEMORY_AFTER; -import static io.opentelemetry.instrumentation.runtimemetrics.java17.internal.Constants.MILLISECONDS; +import static io.opentelemetry.instrumentation.runtimemetrics.java17.internal.Constants.METRIC_NAME_MEMORY_LIMIT; +import static io.opentelemetry.instrumentation.runtimemetrics.java17.internal.Constants.SECONDS; import static org.assertj.core.api.Assertions.assertThat; import io.opentelemetry.api.common.Attributes; @@ -51,13 +54,13 @@ void shouldHaveMemoryMetrics() { .satisfies(PsGcMemoryMetricTest::hasGcAttributes), metric -> metric - .hasName("process.runtime.jvm.memory.committed") + .hasName(METRIC_NAME_COMMITTED) .hasUnit(BYTES) .hasDescription(METRIC_DESCRIPTION_COMMITTED) .satisfies(PsGcMemoryMetricTest::hasGcAttributes), metric -> metric - .hasName("process.runtime.jvm.memory.limit") + .hasName(METRIC_NAME_MEMORY_LIMIT) .hasUnit(BYTES) .hasDescription(METRIC_DESCRIPTION_MEMORY_LIMIT) .satisfies(PsGcMemoryMetricTest::hasGcAttributes), @@ -83,14 +86,14 @@ void shouldHaveGcDurationMetrics() { System.gc(); Attributes minorGcAttributes = - Attributes.of(ATTR_GC, "PS Scavenge", ATTR_ACTION, END_OF_MINOR_GC); + Attributes.of(ATTR_GC_NAME, "PS Scavenge", ATTR_GC_ACTION, END_OF_MINOR_GC); Attributes majorGcAttributes = - Attributes.of(ATTR_GC, "PS MarkSweep", ATTR_ACTION, END_OF_MAJOR_GC); + Attributes.of(ATTR_GC_NAME, "PS MarkSweep", ATTR_GC_ACTION, END_OF_MAJOR_GC); jfrExtension.waitAndAssertMetrics( metric -> metric - .hasName("process.runtime.jvm.gc.duration") - .hasUnit(MILLISECONDS) + .hasName(METRIC_NAME_GC_DURATION) + .hasUnit(SECONDS) .hasDescription(METRIC_DESCRIPTION_GC_DURATION) .satisfies( data -> diff --git a/instrumentation/runtime-telemetry/runtime-telemetry-java17/library/src/test/java/io/opentelemetry/instrumentation/runtimemetrics/java17/SerialGcMemoryMetricTest.java b/instrumentation/runtime-telemetry/runtime-telemetry-java17/library/src/test/java/io/opentelemetry/instrumentation/runtimemetrics/java17/SerialGcMemoryMetricTest.java index 2f0f82ea3cc0..b8e51bc1b99c 100644 --- a/instrumentation/runtime-telemetry/runtime-telemetry-java17/library/src/test/java/io/opentelemetry/instrumentation/runtimemetrics/java17/SerialGcMemoryMetricTest.java +++ b/instrumentation/runtime-telemetry/runtime-telemetry-java17/library/src/test/java/io/opentelemetry/instrumentation/runtimemetrics/java17/SerialGcMemoryMetricTest.java @@ -5,12 +5,13 @@ package io.opentelemetry.instrumentation.runtimemetrics.java17; -import static io.opentelemetry.instrumentation.runtimemetrics.java17.internal.Constants.ATTR_ACTION; -import static io.opentelemetry.instrumentation.runtimemetrics.java17.internal.Constants.ATTR_GC; +import static io.opentelemetry.instrumentation.runtimemetrics.java17.internal.Constants.ATTR_GC_ACTION; +import static io.opentelemetry.instrumentation.runtimemetrics.java17.internal.Constants.ATTR_GC_NAME; import static io.opentelemetry.instrumentation.runtimemetrics.java17.internal.Constants.END_OF_MAJOR_GC; import static io.opentelemetry.instrumentation.runtimemetrics.java17.internal.Constants.END_OF_MINOR_GC; import static io.opentelemetry.instrumentation.runtimemetrics.java17.internal.Constants.METRIC_DESCRIPTION_GC_DURATION; -import static io.opentelemetry.instrumentation.runtimemetrics.java17.internal.Constants.MILLISECONDS; +import static io.opentelemetry.instrumentation.runtimemetrics.java17.internal.Constants.METRIC_NAME_GC_DURATION; +import static io.opentelemetry.instrumentation.runtimemetrics.java17.internal.Constants.SECONDS; import static org.assertj.core.api.Assertions.assertThat; import io.opentelemetry.api.common.Attributes; @@ -34,14 +35,15 @@ void shouldHaveGcDurationMetrics() { // TODO: Need a reliable way to test old and young gen GC in isolation. // Generate some JFR events System.gc(); - Attributes minorGcAttributes = Attributes.of(ATTR_GC, "Copy", ATTR_ACTION, END_OF_MINOR_GC); + Attributes minorGcAttributes = + Attributes.of(ATTR_GC_NAME, "Copy", ATTR_GC_ACTION, END_OF_MINOR_GC); Attributes majorGcAttributes = - Attributes.of(ATTR_GC, "MarkSweepCompact", ATTR_ACTION, END_OF_MAJOR_GC); + Attributes.of(ATTR_GC_NAME, "MarkSweepCompact", ATTR_GC_ACTION, END_OF_MAJOR_GC); jfrExtension.waitAndAssertMetrics( metric -> metric - .hasName("process.runtime.jvm.gc.duration") - .hasUnit(MILLISECONDS) + .hasName(METRIC_NAME_GC_DURATION) + .hasUnit(SECONDS) .hasDescription(METRIC_DESCRIPTION_GC_DURATION) .satisfies( data -> diff --git a/instrumentation/runtime-telemetry/runtime-telemetry-java17/library/src/test/java/io/opentelemetry/instrumentation/runtimemetrics/java17/internal/DurationUtilTest.java b/instrumentation/runtime-telemetry/runtime-telemetry-java17/library/src/test/java/io/opentelemetry/instrumentation/runtimemetrics/java17/internal/DurationUtilTest.java new file mode 100644 index 000000000000..589bd85df1ca --- /dev/null +++ b/instrumentation/runtime-telemetry/runtime-telemetry-java17/library/src/test/java/io/opentelemetry/instrumentation/runtimemetrics/java17/internal/DurationUtilTest.java @@ -0,0 +1,26 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.runtimemetrics.java17.internal; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.time.Duration; +import org.junit.jupiter.api.Test; + +class DurationUtilTest { + + @Test + void shouldConvertDurationToSeconds() { + // Given + Duration duration = Duration.ofSeconds(7, 144); + + // When + double seconds = DurationUtil.toSeconds(duration); + + // Then + assertEquals(7.000000144, seconds); + } +} diff --git a/instrumentation/runtime-telemetry/runtime-telemetry-java8/javaagent/build.gradle.kts b/instrumentation/runtime-telemetry/runtime-telemetry-java8/javaagent/build.gradle.kts index 67c18d98913b..aa1653528255 100644 --- a/instrumentation/runtime-telemetry/runtime-telemetry-java8/javaagent/build.gradle.kts +++ b/instrumentation/runtime-telemetry/runtime-telemetry-java8/javaagent/build.gradle.kts @@ -6,4 +6,11 @@ dependencies { implementation(project(":instrumentation:runtime-telemetry:runtime-telemetry-java8:library")) compileOnly("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure") + compileOnly("io.opentelemetry:opentelemetry-api-incubator") +} + +tasks { + test { + jvmArgs("-Dotel.instrumentation.runtime-telemetry.package-emitter.enabled=true") + } } diff --git a/instrumentation/runtime-telemetry/runtime-telemetry-java8/javaagent/src/main/java/io/opentelemetry/instrumentation/javaagent/runtimemetrics/java8/JarAnalyzer.java b/instrumentation/runtime-telemetry/runtime-telemetry-java8/javaagent/src/main/java/io/opentelemetry/instrumentation/javaagent/runtimemetrics/java8/JarAnalyzer.java new file mode 100644 index 000000000000..868cbda2ce5f --- /dev/null +++ b/instrumentation/runtime-telemetry/runtime-telemetry-java8/javaagent/src/main/java/io/opentelemetry/instrumentation/javaagent/runtimemetrics/java8/JarAnalyzer.java @@ -0,0 +1,244 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.javaagent.runtimemetrics.java8; + +import static io.opentelemetry.instrumentation.javaagent.runtimemetrics.java8.JarDetails.EAR_EXTENSION; +import static io.opentelemetry.instrumentation.javaagent.runtimemetrics.java8.JarDetails.JAR_EXTENSION; +import static io.opentelemetry.instrumentation.javaagent.runtimemetrics.java8.JarDetails.WAR_EXTENSION; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.api.incubator.events.EventLogger; +import io.opentelemetry.api.incubator.events.GlobalEventLoggerProvider; +import io.opentelemetry.instrumentation.runtimemetrics.java8.internal.JmxRuntimeMetricsUtil; +import io.opentelemetry.sdk.common.Clock; +import io.opentelemetry.sdk.internal.DaemonThreadFactory; +import java.io.File; +import java.io.IOException; +import java.lang.instrument.ClassFileTransformer; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.security.CodeSource; +import java.security.ProtectionDomain; +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingDeque; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * {@link JarAnalyzer} is a {@link ClassFileTransformer} which processes the {@link + * ProtectionDomain} of each class loaded and emits an event with metadata about each distinct + * archive location identified. + */ +final class JarAnalyzer implements ClassFileTransformer { + + private static final Logger logger = Logger.getLogger(JarAnalyzer.class.getName()); + + private static final String EVENT_NAME_INFO = "package.info"; + static final AttributeKey PACKAGE_NAME = AttributeKey.stringKey("package.name"); + static final AttributeKey PACKAGE_VERSION = AttributeKey.stringKey("package.version"); + static final AttributeKey PACKAGE_TYPE = AttributeKey.stringKey("package.type"); + static final AttributeKey PACKAGE_DESCRIPTION = + AttributeKey.stringKey("package.description"); + static final AttributeKey PACKAGE_CHECKSUM = AttributeKey.stringKey("package.checksum"); + static final AttributeKey PACKAGE_CHECKSUM_ALGORITHM = + AttributeKey.stringKey("package.checksum_algorithm"); + static final AttributeKey PACKAGE_PATH = AttributeKey.stringKey("package.path"); + + private final Set seenUris = new HashSet<>(); + private final BlockingQueue toProcess = new LinkedBlockingDeque<>(); + + private JarAnalyzer(OpenTelemetry unused, int jarsPerSecond) { + // TODO(jack-berg): Use OpenTelemetry to obtain EventLogger when event API is stable + EventLogger eventLogger = + GlobalEventLoggerProvider.get() + .eventLoggerBuilder(JmxRuntimeMetricsUtil.getInstrumentationName()) + .setInstrumentationVersion(JmxRuntimeMetricsUtil.getInstrumentationVersion()) + .build(); + Worker worker = new Worker(eventLogger, toProcess, jarsPerSecond); + Thread workerThread = + new DaemonThreadFactory(JarAnalyzer.class.getSimpleName() + "_WorkerThread") + .newThread(worker); + workerThread.start(); + } + + /** Create {@link JarAnalyzer} and start the worker thread. */ + public static JarAnalyzer create(OpenTelemetry unused, int jarsPerSecond) { + return new JarAnalyzer(unused, jarsPerSecond); + } + + /** + * Identify the archive (JAR or WAR) associated with the {@code protectionDomain} and queue it to + * be processed if its the first time we've seen it. + */ + @Override + public byte[] transform( + ClassLoader loader, + String className, + Class classBeingRedefined, + ProtectionDomain protectionDomain, + byte[] classfileBuffer) { + handle(protectionDomain); + return null; + } + + private void handle(ProtectionDomain protectionDomain) { + if (protectionDomain == null) { + return; + } + CodeSource codeSource = protectionDomain.getCodeSource(); + if (codeSource == null) { + return; + } + URL archiveUrl = codeSource.getLocation(); + if (archiveUrl == null) { + return; + } + URI locationUri; + try { + locationUri = archiveUrl.toURI(); + } catch (URISyntaxException e) { + logger.log(Level.WARNING, "Unable to get URI for code location URL: " + archiveUrl, e); + return; + } + + if (!seenUris.add(locationUri)) { + return; + } + if ("jrt".equals(archiveUrl.getProtocol())) { + logger.log(Level.FINEST, "Skipping processing for java runtime module: {0}", archiveUrl); + return; + } + String file = archiveUrl.getFile(); + if (file.endsWith("/")) { + logger.log(Level.FINEST, "Skipping processing non-archive code location: {0}", archiveUrl); + return; + } + if (!file.endsWith(JAR_EXTENSION) + && !file.endsWith(WAR_EXTENSION) + && !file.endsWith(EAR_EXTENSION)) { + logger.log(Level.INFO, "Skipping processing unrecognized code location: {0}", archiveUrl); + return; + } + + // Payara 5 and 6 have url with file protocol that fail on openStream with + // java.io.IOException: no entry name specified + // at + // java.base/sun.net.www.protocol.jar.JarURLConnection.getInputStream(JarURLConnection.java:160) + // To avoid this here we recreate the URL when it points to a file. + if ("file".equals(archiveUrl.getProtocol())) { + try { + File archiveFile = new File(archiveUrl.toURI().getSchemeSpecificPart()); + if (archiveFile.exists() && archiveFile.isFile()) { + archiveUrl = archiveFile.toURI().toURL(); + } + } catch (Exception e) { + logger.log(Level.WARNING, "Unable to normalize location URL: " + archiveUrl, e); + } + } + + // Only code locations with .jar and .war extension should make it here + toProcess.add(archiveUrl); + } + + private static final class Worker implements Runnable { + + private final EventLogger eventLogger; + private final BlockingQueue toProcess; + private final io.opentelemetry.sdk.internal.RateLimiter rateLimiter; + + private Worker(EventLogger eventLogger, BlockingQueue toProcess, int jarsPerSecond) { + this.eventLogger = eventLogger; + this.toProcess = toProcess; + this.rateLimiter = + new io.opentelemetry.sdk.internal.RateLimiter( + jarsPerSecond, jarsPerSecond, Clock.getDefault()); + } + + /** + * Continuously poll the {@link #toProcess} for archive {@link URL}s, and process each wit + * {@link #processUrl(EventLogger, URL)}. + */ + @Override + public void run() { + while (!Thread.currentThread().isInterrupted()) { + URL archiveUrl = null; + try { + if (!rateLimiter.trySpend(1.0)) { + Thread.sleep(100); + continue; + } + archiveUrl = toProcess.poll(100, TimeUnit.MILLISECONDS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + if (archiveUrl == null) { + continue; + } + try { + // TODO(jack-berg): add ability to optionally re-process urls periodically to re-emit + // events + processUrl(eventLogger, archiveUrl); + } catch (Throwable e) { + logger.log(Level.WARNING, "Unexpected error processing archive URL: " + archiveUrl, e); + } + } + logger.warning("JarAnalyzer stopped"); + } + } + + /** + * Process the {@code archiveUrl}, extracting metadata from it and emitting an event with the + * content. + */ + static void processUrl(EventLogger eventLogger, URL archiveUrl) { + JarDetails jarDetails; + try { + jarDetails = JarDetails.forUrl(archiveUrl); + } catch (IOException e) { + logger.log(Level.WARNING, "Error reading package for archive URL: " + archiveUrl, e); + return; + } + AttributesBuilder builder = Attributes.builder(); + + String packagePath = jarDetails.packagePath(); + if (packagePath != null) { + builder.put(PACKAGE_PATH, packagePath); + } + + String packageType = jarDetails.packageType(); + if (packageType != null) { + builder.put(PACKAGE_TYPE, packageType); + } + + String packageName = jarDetails.packageName(); + if (packageName != null) { + builder.put(PACKAGE_NAME, packageName); + } + + String packageVersion = jarDetails.version(); + if (packageVersion != null) { + builder.put(PACKAGE_VERSION, packageVersion); + } + + String packageDescription = jarDetails.packageDescription(); + if (packageDescription != null) { + builder.put(PACKAGE_DESCRIPTION, packageDescription); + } + + String packageChecksum = jarDetails.computeSha1(); + builder.put(PACKAGE_CHECKSUM, packageChecksum); + builder.put(PACKAGE_CHECKSUM_ALGORITHM, "SHA1"); + + eventLogger.builder(EVENT_NAME_INFO).setAttributes(builder.build()).emit(); + } +} diff --git a/instrumentation/runtime-telemetry/runtime-telemetry-java8/javaagent/src/main/java/io/opentelemetry/instrumentation/javaagent/runtimemetrics/java8/JarAnalyzerInstaller.java b/instrumentation/runtime-telemetry/runtime-telemetry-java8/javaagent/src/main/java/io/opentelemetry/instrumentation/javaagent/runtimemetrics/java8/JarAnalyzerInstaller.java new file mode 100644 index 000000000000..ec12fa81c2f1 --- /dev/null +++ b/instrumentation/runtime-telemetry/runtime-telemetry-java8/javaagent/src/main/java/io/opentelemetry/instrumentation/javaagent/runtimemetrics/java8/JarAnalyzerInstaller.java @@ -0,0 +1,38 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.javaagent.runtimemetrics.java8; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.bootstrap.InstrumentationHolder; +import io.opentelemetry.javaagent.tooling.BeforeAgentListener; +import io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdk; +import io.opentelemetry.sdk.autoconfigure.internal.AutoConfigureUtil; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import java.lang.instrument.Instrumentation; + +/** Installs the {@link JarAnalyzer}. */ +@AutoService(BeforeAgentListener.class) +public class JarAnalyzerInstaller implements BeforeAgentListener { + + @Override + public void beforeAgent(AutoConfiguredOpenTelemetrySdk autoConfiguredOpenTelemetrySdk) { + ConfigProperties config = AutoConfigureUtil.getConfig(autoConfiguredOpenTelemetrySdk); + boolean enabled = + config.getBoolean("otel.instrumentation.runtime-telemetry.package-emitter.enabled", false); + if (!enabled) { + return; + } + Instrumentation inst = InstrumentationHolder.getInstrumentation(); + if (inst == null) { + return; + } + int jarsPerSecond = + config.getInt("otel.instrumentation.runtime-telemetry.package-emitter.jars-per-second", 10); + JarAnalyzer jarAnalyzer = + JarAnalyzer.create(autoConfiguredOpenTelemetrySdk.getOpenTelemetrySdk(), jarsPerSecond); + inst.addTransformer(jarAnalyzer); + } +} diff --git a/instrumentation/runtime-telemetry/runtime-telemetry-java8/javaagent/src/main/java/io/opentelemetry/instrumentation/javaagent/runtimemetrics/java8/JarDetails.java b/instrumentation/runtime-telemetry/runtime-telemetry-java8/javaagent/src/main/java/io/opentelemetry/instrumentation/javaagent/runtimemetrics/java8/JarDetails.java new file mode 100644 index 000000000000..88afca04c046 --- /dev/null +++ b/instrumentation/runtime-telemetry/runtime-telemetry-java8/javaagent/src/main/java/io/opentelemetry/instrumentation/javaagent/runtimemetrics/java8/JarDetails.java @@ -0,0 +1,276 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.javaagent.runtimemetrics.java8; + +import static java.util.function.Function.identity; +import static java.util.stream.Collectors.collectingAndThen; +import static java.util.stream.Collectors.toMap; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.math.BigInteger; +import java.net.URL; +import java.security.DigestInputStream; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Collections; +import java.util.Enumeration; +import java.util.Locale; +import java.util.Map; +import java.util.Properties; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.jar.JarInputStream; +import java.util.jar.Manifest; +import java.util.stream.Stream; +import javax.annotation.Nullable; + +/** + * For a given URL representing a Jar directly on the file system or embedded within another + * archive, this class provides methods which expose useful information about it. + */ +class JarDetails { + static final String JAR_EXTENSION = "jar"; + static final String WAR_EXTENSION = "war"; + static final String EAR_EXTENSION = "ear"; + private static final Map EMBEDDED_FORMAT_TO_EXTENSION = + Stream.of(JAR_EXTENSION, WAR_EXTENSION, EAR_EXTENSION) + .collect( + collectingAndThen( + toMap(ext -> ('.' + ext + "!/"), identity()), + Collections::unmodifiableMap)); + private static final ThreadLocal SHA1 = + ThreadLocal.withInitial( + () -> { + try { + return MessageDigest.getInstance("SHA1"); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException(e); + } + }); + + private final URL url; + protected final JarFile jarFile; + private final Properties pom; + private final Manifest manifest; + private final String sha1Checksum; + + private JarDetails(URL url, JarFile jarFile) throws IOException { + this.url = url; + this.jarFile = jarFile; + this.pom = getPom(); + this.manifest = getManifest(); + this.sha1Checksum = computeDigest(SHA1.get()); + } + + static JarDetails forUrl(URL url) throws IOException { + if (url.getProtocol().equals("jar")) { + String urlString = url.toExternalForm(); + String urlLower = urlString.toLowerCase(Locale.ROOT); + for (Map.Entry entry : EMBEDDED_FORMAT_TO_EXTENSION.entrySet()) { + int index = urlLower.indexOf(entry.getKey()); + if (index > 0) { + String targetEntry = urlString.substring(index + entry.getKey().length()); + JarFile jarFile = + new JarFile( + urlString.substring("jar:file:".length(), index + 1 + entry.getValue().length())); + JarEntry jarEntry = jarFile.getJarEntry(targetEntry); + return new EmbeddedJarDetails(url, jarFile, jarEntry); + } + } + } + return new JarDetails(url, new JarFile(url.getFile())); + } + + /** + * Returns the archive file name, e.g. {@code jackson-datatype-jsr310-2.15.2.jar}. Returns null if + * unable to identify the file name from {@link #url}. + */ + @Nullable + String packagePath() { + String path = url.getFile(); + int start = path.lastIndexOf(File.separator); + if (start > -1) { + return path.substring(start + 1); + } + return null; + } + + /** + * Returns the extension of the archive, e.g. {@code jar}. Returns null if unable to identify the + * extension from {@link #url} + */ + @Nullable + String packageType() { + String path = url.getFile(); + int extensionStart = path.lastIndexOf("."); + if (extensionStart > -1) { + return path.substring(extensionStart + 1); + } + return null; + } + + /** + * Returns the maven package name in the format {@code groupId:artifactId}, e.g. {@code + * com.fasterxml.jackson.datatype:jackson-datatype-jsr310}. Returns null if {@link #pom} is not + * found, or is missing groupId or artifactId properties. + */ + @Nullable + String packageName() { + if (pom == null) { + return null; + } + String groupId = pom.getProperty("groupId"); + String artifactId = pom.getProperty("artifactId"); + if (groupId != null && !groupId.isEmpty() && artifactId != null && !artifactId.isEmpty()) { + return groupId + ":" + artifactId; + } + return null; + } + + /** + * Returns the version from the pom file, e.g. {@code 2.15.2}. Returns null if {@link #pom} is not + * found or is missing version property. + */ + @Nullable + String version() { + if (pom == null) { + return null; + } + String version = pom.getProperty("version"); + if (version != null && !version.isEmpty()) { + return version; + } + return null; + } + + /** + * Returns the package description from the jar manifest "{Implementation-Title} by + * {Implementation-Vendor}", e.g. {@code Jackson datatype: JSR310 by FasterXML}. Returns null if + * {@link #manifest} is not found. + */ + @Nullable + String packageDescription() { + if (manifest == null) { + return null; + } + + java.util.jar.Attributes mainAttributes = manifest.getMainAttributes(); + String name = mainAttributes.getValue(java.util.jar.Attributes.Name.IMPLEMENTATION_TITLE); + String description = + mainAttributes.getValue(java.util.jar.Attributes.Name.IMPLEMENTATION_VENDOR); + + String packageDescription = name; + if (description != null && !description.isEmpty()) { + packageDescription += " by " + description; + } + return packageDescription; + } + + /** Returns the SHA1 hash of this file, e.g. {@code 30d16ec2aef6d8094c5e2dce1d95034ca8b6cb42}. */ + String computeSha1() { + return sha1Checksum; + } + + private String computeDigest(MessageDigest md) throws IOException { + try (InputStream inputStream = getInputStream()) { + DigestInputStream dis = new DigestInputStream(inputStream, md); + byte[] buffer = new byte[8192]; + while (dis.read(buffer) != -1) {} + byte[] digest = md.digest(); + return new BigInteger(1, digest).toString(16); + } + } + + /** + * Returns An open input stream for the associated url. It is the caller's responsibility to close + * the stream on completion. + */ + protected InputStream getInputStream() throws IOException { + return url.openStream(); + } + + @Nullable + protected Manifest getManifest() { + try { + return jarFile.getManifest(); + } catch (IOException e) { + return null; + } + } + + /** + * Returns the values from pom.properties if this file is found. If multiple pom.properties files + * are found or there is an error reading the file, return null. + */ + @Nullable + protected Properties getPom() throws IOException { + Properties pom = null; + for (Enumeration entries = jarFile.entries(); entries.hasMoreElements(); ) { + JarEntry jarEntry = entries.nextElement(); + if (jarEntry.getName().startsWith("META-INF/maven") + && jarEntry.getName().endsWith("pom.properties")) { + if (pom != null) { + // we've found multiple pom files. bail! + return null; + } + Properties props = new Properties(); + props.load(jarFile.getInputStream(jarEntry)); + pom = props; + } + } + return pom; + } + + private static class EmbeddedJarDetails extends JarDetails { + + private final JarEntry jarEntry; + + private EmbeddedJarDetails(URL url, JarFile jarFile, JarEntry jarEntry) throws IOException { + super(url, jarFile); + this.jarEntry = jarEntry; + } + + @Override + protected InputStream getInputStream() throws IOException { + return jarFile.getInputStream(jarEntry); + } + + @Override + protected Manifest getManifest() { + try (JarInputStream jarFile = new JarInputStream(getInputStream())) { + return jarFile.getManifest(); + } catch (IOException e) { + return null; + } + } + + @Override + @Nullable + protected Properties getPom() throws IOException { + Properties pom = null; + // Need to navigate inside the embedded jar which can't be done via random access. + try (JarInputStream jarFile = new JarInputStream(getInputStream())) { + for (JarEntry entry = jarFile.getNextJarEntry(); + entry != null; + entry = jarFile.getNextJarEntry()) { + if (entry.getName().startsWith("META-INF/maven") + && entry.getName().endsWith("pom.properties")) { + if (pom != null) { + // we've found multiple pom files. bail! + return null; + } + Properties props = new Properties(); + props.load(jarFile); + pom = props; + } + } + return pom; + } + } + } +} diff --git a/instrumentation/runtime-telemetry/runtime-telemetry-java8/javaagent/src/main/java/io/opentelemetry/instrumentation/javaagent/runtimemetrics/java8/Java8RuntimeMetricsInstaller.java b/instrumentation/runtime-telemetry/runtime-telemetry-java8/javaagent/src/main/java/io/opentelemetry/instrumentation/javaagent/runtimemetrics/java8/Java8RuntimeMetricsInstaller.java index 922db7312eaf..57ca0396ee99 100644 --- a/instrumentation/runtime-telemetry/runtime-telemetry-java8/javaagent/src/main/java/io/opentelemetry/instrumentation/javaagent/runtimemetrics/java8/Java8RuntimeMetricsInstaller.java +++ b/instrumentation/runtime-telemetry/runtime-telemetry-java8/javaagent/src/main/java/io/opentelemetry/instrumentation/javaagent/runtimemetrics/java8/Java8RuntimeMetricsInstaller.java @@ -8,15 +8,18 @@ import com.google.auto.service.AutoService; import io.opentelemetry.api.GlobalOpenTelemetry; import io.opentelemetry.api.OpenTelemetry; -import io.opentelemetry.instrumentation.runtimemetrics.java8.BufferPools; import io.opentelemetry.instrumentation.runtimemetrics.java8.Classes; import io.opentelemetry.instrumentation.runtimemetrics.java8.Cpu; import io.opentelemetry.instrumentation.runtimemetrics.java8.GarbageCollector; import io.opentelemetry.instrumentation.runtimemetrics.java8.MemoryPools; import io.opentelemetry.instrumentation.runtimemetrics.java8.Threads; +import io.opentelemetry.instrumentation.runtimemetrics.java8.internal.ExperimentalBufferPools; +import io.opentelemetry.instrumentation.runtimemetrics.java8.internal.ExperimentalCpu; +import io.opentelemetry.instrumentation.runtimemetrics.java8.internal.ExperimentalMemoryPools; import io.opentelemetry.instrumentation.runtimemetrics.java8.internal.JmxRuntimeMetricsUtil; import io.opentelemetry.javaagent.extension.AgentListener; import io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdk; +import io.opentelemetry.sdk.autoconfigure.internal.AutoConfigureUtil; import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; import java.util.ArrayList; import java.util.List; @@ -27,26 +30,30 @@ public class Java8RuntimeMetricsInstaller implements AgentListener { @Override public void afterAgent(AutoConfiguredOpenTelemetrySdk autoConfiguredSdk) { - ConfigProperties config = autoConfiguredSdk.getConfig(); + ConfigProperties config = AutoConfigureUtil.getConfig(autoConfiguredSdk); boolean defaultEnabled = config.getBoolean("otel.instrumentation.common.default-enabled", true); if (!config.getBoolean("otel.instrumentation.runtime-telemetry.enabled", defaultEnabled) || Double.parseDouble(System.getProperty("java.specification.version")) >= 17) { return; } + OpenTelemetry openTelemetry = GlobalOpenTelemetry.get(); List observables = new ArrayList<>(); - observables.addAll(BufferPools.registerObservers(openTelemetry)); observables.addAll(Classes.registerObservers(openTelemetry)); observables.addAll(Cpu.registerObservers(openTelemetry)); + observables.addAll(GarbageCollector.registerObservers(openTelemetry)); observables.addAll(MemoryPools.registerObservers(openTelemetry)); observables.addAll(Threads.registerObservers(openTelemetry)); - observables.addAll(GarbageCollector.registerObservers(openTelemetry)); - Thread cleanupTelemetry = - new Thread( - () -> { - JmxRuntimeMetricsUtil.closeObservers(observables); - }); + + if (config.getBoolean( + "otel.instrumentation.runtime-telemetry.emit-experimental-telemetry", false)) { + observables.addAll(ExperimentalBufferPools.registerObservers(openTelemetry)); + observables.addAll(ExperimentalCpu.registerObservers(openTelemetry)); + observables.addAll(ExperimentalMemoryPools.registerObservers(openTelemetry)); + } + + Thread cleanupTelemetry = new Thread(() -> JmxRuntimeMetricsUtil.closeObservers(observables)); Runtime.getRuntime().addShutdownHook(cleanupTelemetry); } } diff --git a/instrumentation/runtime-telemetry/runtime-telemetry-java8/javaagent/src/test/java/io/opentelemetry/instrumentation/javaagent/runtimemetrics/java8/JarAnalyzerInstallerTest.java b/instrumentation/runtime-telemetry/runtime-telemetry-java8/javaagent/src/test/java/io/opentelemetry/instrumentation/javaagent/runtimemetrics/java8/JarAnalyzerInstallerTest.java new file mode 100644 index 000000000000..5d341ef80bc2 --- /dev/null +++ b/instrumentation/runtime-telemetry/runtime-telemetry-java8/javaagent/src/test/java/io/opentelemetry/instrumentation/javaagent/runtimemetrics/java8/JarAnalyzerInstallerTest.java @@ -0,0 +1,76 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.javaagent.runtimemetrics.java8; + +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; +import static java.util.stream.Collectors.toList; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.sdk.logs.data.LogRecordData; +import java.util.List; +import org.awaitility.Awaitility; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.testcontainers.containers.GenericContainer; + +class JarAnalyzerInstallerTest { + + @RegisterExtension + static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + @Test + @SuppressWarnings("ReturnValueIgnored") + void jarAnalyzerEnabled() throws InterruptedException { + // We clear exported data before running tests. Here we load a class from testcontainers with + // the assumption that no testcontainers classes have been loaded yet, and we'll have at least + // the testcontainers jar show up in jar analyzer events. + GenericContainer.class.getName(); + + List events = + Awaitility.await() + .until( + () -> + testing.logRecords().stream() + .filter( + record -> + "package.info" + .equals( + record + .getAttributes() + .get(AttributeKey.stringKey("event.name")))) + .collect(toList()), + (eventList) -> !eventList.isEmpty()); + + assertThat(events) + .hasSizeGreaterThan(0) + .allSatisfy( + logRecord -> + assertThat(logRecord.getAttributes()) + .containsEntry("package.type", "jar") + .containsEntry("package.checksum_algorithm", "SHA1") + .hasEntrySatisfying( + AttributeKey.stringKey("package.checksum"), + value -> assertThat(value).isNotNull()) + .hasEntrySatisfying( + AttributeKey.stringKey("package.path"), + value -> assertThat(value).isNotNull()) + .satisfies( + attributes -> { + String packageName = + attributes.get(AttributeKey.stringKey("package.name")); + if (packageName != null) { + assertThat(packageName).matches(".*:.*"); + } + String packageVersion = + attributes.get(AttributeKey.stringKey("package.version")); + if (packageVersion != null) { + assertThat(packageVersion).matches(".*\\..*"); + } + })); + } +} diff --git a/instrumentation/runtime-telemetry/runtime-telemetry-java8/library/README.md b/instrumentation/runtime-telemetry/runtime-telemetry-java8/library/README.md index bd710c66ae3f..4f846856d0e8 100644 --- a/instrumentation/runtime-telemetry/runtime-telemetry-java8/library/README.md +++ b/instrumentation/runtime-telemetry/runtime-telemetry-java8/library/README.md @@ -1,6 +1,6 @@ # JVM Runtime Metrics -This module provides JVM runtime metrics as documented in the [semantic conventions](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/semantic_conventions/runtime-environment-metrics.md#jvm-metrics). +This module provides JVM runtime metrics as documented in the [semantic conventions](https://github.com/open-telemetry/semantic-conventions/blob/main/docs/runtime/jvm-metrics.md). ## Quickstart @@ -32,61 +32,54 @@ runtimeOnly("io.opentelemetry.instrumentation:opentelemetry-runtime-telemetry-ja Register observers for the desired runtime metrics: ```java -OpenTelemetry opentelemetry = // OpenTelemetry instance configured elsewhere - -BufferPools.registerObservers(opentelemetry); -Classes.registerObservers(opentelemetry); -Cpu.registerObservers(opentelemetry); -MemoryPools.registerObservers(opentelemetry); -Threads.registerObservers(opentelemetry); -GarbageCollector.registerObservers(opentelemetry); +OpenTelemetry openTelemetry = // OpenTelemetry instance configured elsewhere + +Classes.registerObservers(openTelemetry); +Cpu.registerObservers(openTelemetry); +MemoryPools.registerObservers(openTelemetry); +Threads.registerObservers(openTelemetry); +GarbageCollector.registerObservers(openTelemetry); ``` ## Garbage Collector Dependent Metrics -The attributes reported on the memory metrics (`process.runtime.jvm.memory.*`) and gc metrics (`process.runtime.jvm.gc.*`) are dependent on the garbage collector used by the application, since each garbage collector organizes memory pools differently and has different strategies for reclaiming memory during garbage collection. - -The following lists attributes reported for a variety of garbage collectors. Notice that attributes are not necessarily constant across `*.init`, `*.usage`, `*.committed`, and `*.limit` since not all memory pools report a limit. - -* CMS Garbage Collector - * `process.runtime.jvm.memory.init`: {pool=Compressed Class Space,type=non_heap}, {pool=Par Eden Space,type=heap}, {pool=Tenured Gen,type=heap}, {pool=Par Survivor Space,type=heap}, {pool=Code Cache,type=non_heap}, {pool=Metaspace,type=non_heap} - * `process.runtime.jvm.memory.usage`: {pool=Compressed Class Space,type=non_heap}, {pool=Par Eden Space,type=heap}, {pool=Tenured Gen,type=heap}, {pool=Par Survivor Space,type=heap}, {pool=Code Cache,type=non_heap}, {pool=Metaspace,type=non_heap} - * `process.runtime.jvm.memory.committed`: {pool=Compressed Class Space,type=non_heap}, {pool=Par Eden Space,type=heap}, {pool=Tenured Gen,type=heap}, {pool=Par Survivor Space,type=heap}, {pool=Code Cache,type=non_heap}, {pool=Metaspace,type=non_heap} - * `process.runtime.jvm.memory.limit`: {pool=Compressed Class Space,type=non_heap}, {pool=Par Eden Space,type=heap}, {pool=Tenured Gen,type=heap}, {pool=Par Survivor Space,type=heap}, {pool=Code Cache,type=non_heap} - * `process.runtime.jvm.memory.usage_after_last_gc`: {pool=Par Eden Space,type=heap}, {pool=Tenured Gen,type=heap}, {pool=Par Survivor Space,type=heap} - * `process.runtime.jvm.gc.duration`: {action=end of minor GC,gc=ParNew}, {action=end of major GC,gc=MarkSweepCompact} -* G1 Garbage Collector - * `process.runtime.jvm.memory.init`: {pool=G1 Survivor Space,type=heap}, {pool=G1 Eden Space,type=heap}, {pool=CodeCache,type=non_heap}, {pool=G1 Old Gen,type=heap}, {pool=Compressed Class Space,type=non_heap}, {pool=Metaspace,type=non_heap} - * `process.runtime.jvm.memory.usage`: {pool=G1 Survivor Space,type=heap}, {pool=G1 Eden Space,type=heap}, {pool=CodeCache,type=non_heap}, {pool=G1 Old Gen,type=heap}, {pool=Compressed Class Space,type=non_heap}, {pool=Metaspace,type=non_heap} - * `process.runtime.jvm.memory.committed`: {pool=G1 Survivor Space,type=heap}, {pool=G1 Eden Space,type=heap}, {pool=CodeCache,type=non_heap}, {pool=G1 Old Gen,type=heap}, {pool=Compressed Class Space,type=non_heap}, {pool=Metaspace,type=non_heap} - * `process.runtime.jvm.memory.limit`: {pool=CodeCache,type=non_heap}, {pool=G1 Old Gen,type=heap}, {pool=Compressed Class Space,type=non_heap} - * `process.runtime.jvm.memory.usage_after_last_gc`: {pool=G1 Survivor Space,type=heap}, {pool=G1 Eden Space,type=heap}, {pool=G1 Old Gen,type=heap} - * `process.runtime.jvm.gc.duration`: {action=end of minor GC,gc=G1 Young Generation}, {action=end of major GC,gc=G1 Old Generation} -* Parallel Garbage Collector - * `process.runtime.jvm.memory.init`: {pool=CodeCache,type=non_heap}, {pool=PS Survivor Space,type=heap}, {pool=PS Old Gen,type=heap}, {pool=PS Eden Space,type=heap}, {pool=Compressed Class Space,type=non_heap}, {pool=Metaspace,type=non_heap} - * `process.runtime.jvm.memory.usage`: {pool=CodeCache,type=non_heap}, {pool=PS Survivor Space,type=heap}, {pool=PS Old Gen,type=heap}, {pool=PS Eden Space,type=heap}, {pool=Compressed Class Space,type=non_heap}, {pool=Metaspace,type=non_heap} - * `process.runtime.jvm.memory.committed`: {pool=CodeCache,type=non_heap}, {pool=PS Survivor Space,type=heap}, {pool=PS Old Gen,type=heap}, {pool=PS Eden Space,type=heap}, {pool=Compressed Class Space,type=non_heap}, {pool=Metaspace,type=non_heap} - * `process.runtime.jvm.memory.limit`: {pool=CodeCache,type=non_heap}, {pool=PS Survivor Space,type=heap}, {pool=PS Old Gen,type=heap}, {pool=PS Eden Space,type=heap}, {pool=Compressed Class Space,type=non_heap} - * `process.runtime.jvm.memory.usage_after_last_gc`: {pool=PS Survivor Space,type=heap}, {pool=PS Old Gen,type=heap}, {pool=PS Eden Space,type=heap} - * `process.runtime.jvm.gc.duration`: {action=end of major GC,gc=PS MarkSweep}, {action=end of minor GC,gc=PS Scavenge} -* Serial Garbage Collector - * `process.runtime.jvm.memory.init`: {pool=CodeCache,type=non_heap}, {pool=Tenured Gen,type=heap}, {pool=Eden Space,type=heap}, {pool=Survivor Space,type=heap}, {pool=Compressed Class Space,type=non_heap}, {pool=Metaspace,type=non_heap} - * `process.runtime.jvm.memory.usage`: {pool=CodeCache,type=non_heap}, {pool=Tenured Gen,type=heap}, {pool=Eden Space,type=heap}, {pool=Survivor Space,type=heap}, {pool=Compressed Class Space,type=non_heap}, {pool=Metaspace,type=non_heap} - * `process.runtime.jvm.memory.committed`: {pool=CodeCache,type=non_heap}, {pool=Tenured Gen,type=heap}, {pool=Eden Space,type=heap}, {pool=Survivor Space,type=heap}, {pool=Compressed Class Space,type=non_heap}, {pool=Metaspace,type=non_heap} - * `process.runtime.jvm.memory.limit`: {pool=CodeCache,type=non_heap}, {pool=Tenured Gen,type=heap}, {pool=Eden Space,type=heap}, {pool=Survivor Space,type=heap}, {pool=Compressed Class Space,type=non_heap} - * `process.runtime.jvm.memory.usage_after_last_gc`: {pool=Tenured Gen,type=heap}, {pool=Eden Space,type=heap}, {pool=Survivor Space,type=heap} - * `process.runtime.jvm.gc.duration`: {action=end of minor GC,gc=Copy}, {action=end of major GC,gc=MarkSweepCompact} -* Shenandoah Garbage Collector - * `process.runtime.jvm.memory.init`: {pool=Metaspace,type=non_heap}, {pool=CodeCache,type=non_heap}, {pool=Shenandoah,type=heap}, {pool=Compressed Class Space,type=non_heap} - * `process.runtime.jvm.memory.usage`: {pool=Metaspace,type=non_heap}, {pool=CodeCache,type=non_heap}, {pool=Shenandoah,type=heap}, {pool=Compressed Class Space,type=non_heap} - * `process.runtime.jvm.memory.committed`: {pool=Metaspace,type=non_heap}, {pool=CodeCache,type=non_heap}, {pool=Shenandoah,type=heap}, {pool=Compressed Class Space,type=non_heap} - * `process.runtime.jvm.memory.limit`: {pool=CodeCache,type=non_heap}, {pool=Shenandoah,type=heap}, {pool=Compressed Class Space,type=non_heap} - * `process.runtime.jvm.memory.usage_after_last_gc`: {pool=Shenandoah,type=heap} - * `process.runtime.jvm.gc.duration`: {action=end of GC cycle,gc=Shenandoah Cycles}, {action=end of GC pause,gc=Shenandoah Pauses} -* Z Garbage Collector - * `process.runtime.jvm.memory.init`: {pool=Metaspace,type=non_heap}, {pool=CodeCache,type=non_heap}, {pool=ZHeap,type=heap}, {pool=Compressed Class Space,type=non_heap} - * `process.runtime.jvm.memory.usage`: {pool=Metaspace,type=non_heap}, {pool=CodeCache,type=non_heap}, {pool=ZHeap,type=heap}, {pool=Compressed Class Space,type=non_heap} - * `process.runtime.jvm.memory.committed`: {pool=Metaspace,type=non_heap}, {pool=CodeCache,type=non_heap}, {pool=ZHeap,type=heap}, {pool=Compressed Class Space,type=non_heap} - * `process.runtime.jvm.memory.limit`: {pool=CodeCache,type=non_heap}, {pool=ZHeap,type=heap}, {pool=Compressed Class Space,type=non_heap} - * `process.runtime.jvm.memory.usage_after_last_gc`: {pool=ZHeap,type=heap} - * `process.runtime.jvm.gc.duration`: {action=end of GC cycle,gc=ZGC Cycles}, {action=end of GC pause,gc=ZGC Pauses} +The attributes reported on the memory metrics (`jvm.memory.*`) and gc metrics (`jvm.gc.*`) are dependent on the garbage collector used by the application, since each garbage collector organizes memory pools differently and has different strategies for reclaiming memory during garbage collection. + +The following lists attributes reported for a variety of garbage collectors. Notice that attributes are not necessarily constant across `*.used`, `*.committed`, and `*.limit` since not all memory pools report a limit. + +- CMS Garbage Collector + - `jvm.memory.used`: {jvm.memory.pool.name=Compressed Class Space,jvm.memory.type=non_heap}, {jvm.memory.pool.name=Par Eden Space,jvm.memory.type=heap}, {jvm.memory.pool.name=Tenured Gen,jvm.memory.type=heap}, {jvm.memory.pool.name=Par Survivor Space,jvm.memory.type=heap}, {jvm.memory.pool.name=Code Cache,jvm.memory.type=non_heap}, {jvm.memory.pool.name=Metaspace,jvm.memory.type=non_heap} + - `jvm.memory.committed`: {jvm.memory.pool.name=Compressed Class Space,jvm.memory.type=non_heap}, {jvm.memory.pool.name=Par Eden Space,jvm.memory.type=heap}, {jvm.memory.pool.name=Tenured Gen,jvm.memory.type=heap}, {jvm.memory.pool.name=Par Survivor Space,jvm.memory.type=heap}, {jvm.memory.pool.name=Code Cache,jvm.memory.type=non_heap}, {jvm.memory.pool.name=Metaspace,jvm.memory.type=non_heap} + - `jvm.memory.limit`: {jvm.memory.pool.name=Compressed Class Space,jvm.memory.type=non_heap}, {jvm.memory.pool.name=Par Eden Space,jvm.memory.type=heap}, {jvm.memory.pool.name=Tenured Gen,jvm.memory.type=heap}, {jvm.memory.pool.name=Par Survivor Space,jvm.memory.type=heap}, {jvm.memory.pool.name=Code Cache,jvm.memory.type=non_heap} + - `jvm.memory.used_after_last_gc`: {jvm.memory.pool.name=Par Eden Space,jvm.memory.type=heap}, {jvm.memory.pool.name=Tenured Gen,jvm.memory.type=heap}, {jvm.memory.pool.name=Par Survivor Space,jvm.memory.type=heap} + - `jvm.gc.duration`: {jvm.gc.action=end of minor GC,jvm.gc.name=ParNew}, {jvm.gc.action=end of major GC,jvm.gc.name=MarkSweepCompact} +- G1 Garbage Collector + - `jvm.memory.used`: {jvm.memory.pool.name=G1 Survivor Space,jvm.memory.type=heap}, {jvm.memory.pool.name=G1 Eden Space,jvm.memory.type=heap}, {jvm.memory.pool.name=CodeCache,jvm.memory.type=non_heap}, {jvm.memory.pool.name=G1 Old Gen,jvm.memory.type=heap}, {jvm.memory.pool.name=Compressed Class Space,jvm.memory.type=non_heap}, {jvm.memory.pool.name=Metaspace,jvm.memory.type=non_heap} + - `jvm.memory.committed`: {jvm.memory.pool.name=G1 Survivor Space,jvm.memory.type=heap}, {jvm.memory.pool.name=G1 Eden Space,jvm.memory.type=heap}, {jvm.memory.pool.name=CodeCache,jvm.memory.type=non_heap}, {jvm.memory.pool.name=G1 Old Gen,jvm.memory.type=heap}, {jvm.memory.pool.name=Compressed Class Space,jvm.memory.type=non_heap}, {jvm.memory.pool.name=Metaspace,jvm.memory.type=non_heap} + - `jvm.memory.limit`: {jvm.memory.pool.name=CodeCache,jvm.memory.type=non_heap}, {jvm.memory.pool.name=G1 Old Gen,jvm.memory.type=heap}, {jvm.memory.pool.name=Compressed Class Space,jvm.memory.type=non_heap} + - `jvm.memory.used_after_last_gc`: {jvm.memory.pool.name=G1 Survivor Space,jvm.memory.type=heap}, {jvm.memory.pool.name=G1 Eden Space,jvm.memory.type=heap}, {jvm.memory.pool.name=G1 Old Gen,jvm.memory.type=heap} + - `jvm.gc.duration`: {jvm.gc.action=end of minor GC,jvm.gc.name=G1 Young Generation}, {jvm.gc.action=end of major GC,jvm.gc.name=G1 Old Generation} +- Parallel Garbage Collector + - `jvm.memory.used`: {jvm.memory.pool.name=CodeCache,jvm.memory.type=non_heap}, {jvm.memory.pool.name=PS Survivor Space,jvm.memory.type=heap}, {jvm.memory.pool.name=PS Old Gen,jvm.memory.type=heap}, {jvm.memory.pool.name=PS Eden Space,jvm.memory.type=heap}, {jvm.memory.pool.name=Compressed Class Space,jvm.memory.type=non_heap}, {jvm.memory.pool.name=Metaspace,jvm.memory.type=non_heap} + - `jvm.memory.committed`: {jvm.memory.pool.name=CodeCache,jvm.memory.type=non_heap}, {jvm.memory.pool.name=PS Survivor Space,jvm.memory.type=heap}, {jvm.memory.pool.name=PS Old Gen,jvm.memory.type=heap}, {jvm.memory.pool.name=PS Eden Space,jvm.memory.type=heap}, {jvm.memory.pool.name=Compressed Class Space,jvm.memory.type=non_heap}, {jvm.memory.pool.name=Metaspace,jvm.memory.type=non_heap} + - `jvm.memory.limit`: {jvm.memory.pool.name=CodeCache,jvm.memory.type=non_heap}, {jvm.memory.pool.name=PS Survivor Space,jvm.memory.type=heap}, {jvm.memory.pool.name=PS Old Gen,jvm.memory.type=heap}, {jvm.memory.pool.name=PS Eden Space,jvm.memory.type=heap}, {jvm.memory.pool.name=Compressed Class Space,jvm.memory.type=non_heap} + - `jvm.memory.used_after_last_gc`: {jvm.memory.pool.name=PS Survivor Space,jvm.memory.type=heap}, {jvm.memory.pool.name=PS Old Gen,jvm.memory.type=heap}, {jvm.memory.pool.name=PS Eden Space,jvm.memory.type=heap} + - `jvm.gc.duration`: {jvm.gc.action=end of major GC,jvm.gc.name=PS MarkSweep}, {jvm.gc.action=end of minor GC,jvm.gc.name=PS Scavenge} +- Serial Garbage Collector + - `jvm.memory.used`: {jvm.memory.pool.name=CodeCache,jvm.memory.type=non_heap}, {jvm.memory.pool.name=Tenured Gen,jvm.memory.type=heap}, {jvm.memory.pool.name=Eden Space,jvm.memory.type=heap}, {jvm.memory.pool.name=Survivor Space,jvm.memory.type=heap}, {jvm.memory.pool.name=Compressed Class Space,jvm.memory.type=non_heap}, {jvm.memory.pool.name=Metaspace,jvm.memory.type=non_heap} + - `jvm.memory.committed`: {jvm.memory.pool.name=CodeCache,jvm.memory.type=non_heap}, {jvm.memory.pool.name=Tenured Gen,jvm.memory.type=heap}, {jvm.memory.pool.name=Eden Space,jvm.memory.type=heap}, {jvm.memory.pool.name=Survivor Space,jvm.memory.type=heap}, {jvm.memory.pool.name=Compressed Class Space,jvm.memory.type=non_heap}, {jvm.memory.pool.name=Metaspace,jvm.memory.type=non_heap} + - `jvm.memory.limit`: {jvm.memory.pool.name=CodeCache,jvm.memory.type=non_heap}, {jvm.memory.pool.name=Tenured Gen,jvm.memory.type=heap}, {jvm.memory.pool.name=Eden Space,jvm.memory.type=heap}, {jvm.memory.pool.name=Survivor Space,jvm.memory.type=heap}, {jvm.memory.pool.name=Compressed Class Space,jvm.memory.type=non_heap} + - `jvm.memory.used_after_last_gc`: {jvm.memory.pool.name=Tenured Gen,jvm.memory.type=heap}, {jvm.memory.pool.name=Eden Space,jvm.memory.type=heap}, {jvm.memory.pool.name=Survivor Space,jvm.memory.type=heap} + - `jvm.gc.duration`: {jvm.gc.action=end of minor GC,jvm.gc.name=Copy}, {jvm.gc.action=end of major GC,jvm.gc.name=MarkSweepCompact} +- Shenandoah Garbage Collector + - `jvm.memory.used`: {jvm.memory.pool.name=Metaspace,jvm.memory.type=non_heap}, {jvm.memory.pool.name=CodeCache,jvm.memory.type=non_heap}, {jvm.memory.pool.name=Shenandoah,jvm.memory.type=heap}, {jvm.memory.pool.name=Compressed Class Space,jvm.memory.type=non_heap} + - `jvm.memory.committed`: {jvm.memory.pool.name=Metaspace,jvm.memory.type=non_heap}, {jvm.memory.pool.name=CodeCache,jvm.memory.type=non_heap}, {jvm.memory.pool.name=Shenandoah,jvm.memory.type=heap}, {jvm.memory.pool.name=Compressed Class Space,jvm.memory.type=non_heap} + - `jvm.memory.limit`: {jvm.memory.pool.name=CodeCache,jvm.memory.type=non_heap}, {jvm.memory.pool.name=Shenandoah,jvm.memory.type=heap}, {jvm.memory.pool.name=Compressed Class Space,jvm.memory.type=non_heap} + - `jvm.memory.used_after_last_gc`: {jvm.memory.pool.name=Shenandoah,jvm.memory.type=heap} + - `jvm.gc.duration`: {jvm.gc.action=end of GC cycle,jvm.gc.name=Shenandoah Cycles}, {jvm.gc.action=end of GC pause,jvm.gc.name=Shenandoah Pauses} +- Z Garbage Collector + - `jvm.memory.used`: {jvm.memory.pool.name=Metaspace,jvm.memory.type=non_heap}, {jvm.memory.pool.name=CodeCache,jvm.memory.type=non_heap}, {jvm.memory.pool.name=ZHeap,jvm.memory.type=heap}, {jvm.memory.pool.name=Compressed Class Space,jvm.memory.type=non_heap} + - `jvm.memory.committed`: {jvm.memory.pool.name=Metaspace,jvm.memory.type=non_heap}, {jvm.memory.pool.name=CodeCache,jvm.memory.type=non_heap}, {jvm.memory.pool.name=ZHeap,jvm.memory.type=heap}, {jvm.memory.pool.name=Compressed Class Space,jvm.memory.type=non_heap} + - `jvm.memory.limit`: {jvm.memory.pool.name=CodeCache,jvm.memory.type=non_heap}, {jvm.memory.pool.name=ZHeap,jvm.memory.type=heap}, {jvm.memory.pool.name=Compressed Class Space,jvm.memory.type=non_heap} + - `jvm.memory.used_after_last_gc`: {jvm.memory.pool.name=ZHeap,jvm.memory.type=heap} + - `jvm.gc.duration`: {jvm.gc.action=end of GC cycle,jvm.gc.name=ZGC Cycles}, {jvm.gc.action=end of GC pause,jvm.gc.name=ZGC Pauses} diff --git a/instrumentation/runtime-telemetry/runtime-telemetry-java8/library/build.gradle.kts b/instrumentation/runtime-telemetry/runtime-telemetry-java8/library/build.gradle.kts index 4957c47b0e47..8635d7ca532e 100644 --- a/instrumentation/runtime-telemetry/runtime-telemetry-java8/library/build.gradle.kts +++ b/instrumentation/runtime-telemetry/runtime-telemetry-java8/library/build.gradle.kts @@ -4,7 +4,6 @@ plugins { dependencies { implementation(project(":instrumentation-api")) - implementation("io.opentelemetry:opentelemetry-extension-incubator") testImplementation(project(":testing-common")) } diff --git a/instrumentation/runtime-telemetry/runtime-telemetry-java8/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java8/Classes.java b/instrumentation/runtime-telemetry/runtime-telemetry-java8/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java8/Classes.java index b9e4b891e7a9..0632ad5a3e4d 100644 --- a/instrumentation/runtime-telemetry/runtime-telemetry-java8/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java8/Classes.java +++ b/instrumentation/runtime-telemetry/runtime-telemetry-java8/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java8/Classes.java @@ -14,7 +14,10 @@ import java.util.List; /** - * Registers measurements that generate metrics about JVM classes. + * Registers measurements that generate metrics about JVM classes. The metrics generated by this + * class follow the + * stable JVM metrics semantic conventions. * *

    Example usage: * @@ -25,9 +28,9 @@ *

    Example metrics being exported: * *

    - *   process.runtime.jvm.classes.loaded 100
    - *   process.runtime.jvm.classes.unloaded 2
    - *   process.runtime.jvm.classes.current_loaded 98
    + *   jvm.class.loaded 100
    + *   jvm.class.unloaded 2
    + *   jvm.class.count 98
      * 
    */ public final class Classes { @@ -44,31 +47,32 @@ public static List registerObservers(OpenTelemetry openTelemetry) List registerObservers(OpenTelemetry openTelemetry, ClassLoadingMXBean classBean) { Meter meter = JmxRuntimeMetricsUtil.getMeter(openTelemetry); List observables = new ArrayList<>(); + observables.add( meter - .counterBuilder("process.runtime.jvm.classes.loaded") - .setDescription("Number of classes loaded since JVM start") + .counterBuilder("jvm.class.loaded") + .setDescription("Number of classes loaded since JVM start.") .setUnit("{class}") .buildWithCallback( observableMeasurement -> observableMeasurement.record(classBean.getTotalLoadedClassCount()))); - observables.add( meter - .counterBuilder("process.runtime.jvm.classes.unloaded") - .setDescription("Number of classes unloaded since JVM start") + .counterBuilder("jvm.class.unloaded") + .setDescription("Number of classes unloaded since JVM start.") .setUnit("{class}") .buildWithCallback( observableMeasurement -> observableMeasurement.record(classBean.getUnloadedClassCount()))); observables.add( meter - .upDownCounterBuilder("process.runtime.jvm.classes.current_loaded") - .setDescription("Number of classes currently loaded") + .upDownCounterBuilder("jvm.class.count") + .setDescription("Number of classes currently loaded.") .setUnit("{class}") .buildWithCallback( observableMeasurement -> observableMeasurement.record(classBean.getLoadedClassCount()))); + return observables; } diff --git a/instrumentation/runtime-telemetry/runtime-telemetry-java8/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java8/Cpu.java b/instrumentation/runtime-telemetry/runtime-telemetry-java8/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java8/Cpu.java index edfd443fd6f7..1263c1b46560 100644 --- a/instrumentation/runtime-telemetry/runtime-telemetry-java8/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java8/Cpu.java +++ b/instrumentation/runtime-telemetry/runtime-telemetry-java8/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java8/Cpu.java @@ -7,18 +7,20 @@ import io.opentelemetry.api.OpenTelemetry; import io.opentelemetry.api.metrics.Meter; +import io.opentelemetry.instrumentation.runtimemetrics.java8.internal.CpuMethods; import io.opentelemetry.instrumentation.runtimemetrics.java8.internal.JmxRuntimeMetricsUtil; -import java.lang.management.ManagementFactory; -import java.lang.management.OperatingSystemMXBean; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.function.IntSupplier; import java.util.function.Supplier; import javax.annotation.Nullable; /** - * Registers measurements that generate metrics about CPU. + * Registers measurements that generate metrics about CPU. The metrics generated by this class + * follow the + * stable JVM metrics semantic conventions. * *

    Example usage: * @@ -29,9 +31,9 @@ *

    Example metrics being exported: * *

    - *   process.runtime.jvm.system.cpu.load_1m 2.2
    - *   process.runtime.jvm.system.cpu.utilization 0.15
    - *   process.runtime.jvm.cpu.utilization 0.1
    + *   jvm.cpu.time 20.42
    + *   jvm.cpu.count 8
    + *   jvm.cpu.recent_utilization 0.1
      * 
    */ public final class Cpu { @@ -39,115 +41,65 @@ public final class Cpu { // Visible for testing static final Cpu INSTANCE = new Cpu(); - private static final String OS_BEAN_J9 = "com.ibm.lang.management.OperatingSystemMXBean"; - private static final String OS_BEAN_HOTSPOT = "com.sun.management.OperatingSystemMXBean"; - private static final String METHOD_PROCESS_CPU_LOAD = "getProcessCpuLoad"; - private static final String METHOD_CPU_LOAD = "getCpuLoad"; - private static final String METHOD_SYSTEM_CPU_LOAD = "getSystemCpuLoad"; + private static final double NANOS_PER_S = TimeUnit.SECONDS.toNanos(1); - @Nullable private static final Supplier processCpu; - @Nullable private static final Supplier systemCpu; - - static { - OperatingSystemMXBean osBean = ManagementFactory.getOperatingSystemMXBean(); - Supplier processCpuSupplier = - methodInvoker(osBean, OS_BEAN_HOTSPOT, METHOD_PROCESS_CPU_LOAD); - if (processCpuSupplier == null) { - // More users will be on hotspot than j9, so check for j9 second - processCpuSupplier = methodInvoker(osBean, OS_BEAN_J9, METHOD_PROCESS_CPU_LOAD); - } - processCpu = processCpuSupplier; - - // As of java 14, com.sun.management.OperatingSystemMXBean#getCpuLoad() is preferred and - // #getSystemCpuLoad() is deprecated - Supplier systemCpuSupplier = methodInvoker(osBean, OS_BEAN_HOTSPOT, METHOD_CPU_LOAD); - if (systemCpuSupplier == null) { - systemCpuSupplier = methodInvoker(osBean, OS_BEAN_HOTSPOT, METHOD_SYSTEM_CPU_LOAD); - } - if (systemCpuSupplier == null) { - // More users will be on hotspot than j9, so check for j9 second - systemCpuSupplier = methodInvoker(osBean, OS_BEAN_J9, METHOD_SYSTEM_CPU_LOAD); - } - systemCpu = systemCpuSupplier; - } - - /** Register observers for java runtime class metrics. */ + /** Register observers for java runtime CPU metrics. */ public static List registerObservers(OpenTelemetry openTelemetry) { return INSTANCE.registerObservers( - openTelemetry, ManagementFactory.getOperatingSystemMXBean(), systemCpu, processCpu); + openTelemetry, + Runtime.getRuntime()::availableProcessors, + CpuMethods.processCpuTime(), + CpuMethods.processCpuUtilization()); } // Visible for testing List registerObservers( OpenTelemetry openTelemetry, - OperatingSystemMXBean osBean, - @Nullable Supplier systemCpuUsage, - @Nullable Supplier processCpuUsage) { + IntSupplier availableProcessors, + @Nullable Supplier processCpuTime, + @Nullable Supplier processCpuUtilization) { Meter meter = JmxRuntimeMetricsUtil.getMeter(openTelemetry); List observables = new ArrayList<>(); - observables.add( - meter - .gaugeBuilder("process.runtime.jvm.system.cpu.load_1m") - .setDescription("Average CPU load of the whole system for the last minute") - .setUnit("1") - .buildWithCallback( - observableMeasurement -> { - double loadAverage = osBean.getSystemLoadAverage(); - if (loadAverage >= 0) { - observableMeasurement.record(loadAverage); - } - })); - if (systemCpuUsage != null) { + if (processCpuTime != null) { observables.add( meter - .gaugeBuilder("process.runtime.jvm.system.cpu.utilization") - .setDescription("Recent cpu utilization for the whole system") - .setUnit("1") + .counterBuilder("jvm.cpu.time") + .ofDoubles() + .setDescription("CPU time used by the process as reported by the JVM.") + .setUnit("s") .buildWithCallback( observableMeasurement -> { - Double cpuUsage = systemCpuUsage.get(); - if (cpuUsage != null && cpuUsage >= 0) { - observableMeasurement.record(cpuUsage); + Long cpuTimeNanos = processCpuTime.get(); + if (cpuTimeNanos != null && cpuTimeNanos >= 0) { + observableMeasurement.record(cpuTimeNanos / NANOS_PER_S); } })); } - - if (processCpuUsage != null) { + observables.add( + meter + .upDownCounterBuilder("jvm.cpu.count") + .setDescription("Number of processors available to the Java virtual machine.") + .setUnit("{cpu}") + .buildWithCallback( + observableMeasurement -> + observableMeasurement.record(availableProcessors.getAsInt()))); + if (processCpuUtilization != null) { observables.add( meter - .gaugeBuilder("process.runtime.jvm.cpu.utilization") - .setDescription("Recent cpu utilization for the process") + .gaugeBuilder("jvm.cpu.recent_utilization") + .setDescription("Recent CPU utilization for the process as reported by the JVM.") .setUnit("1") .buildWithCallback( observableMeasurement -> { - Double cpuUsage = processCpuUsage.get(); + Double cpuUsage = processCpuUtilization.get(); if (cpuUsage != null && cpuUsage >= 0) { observableMeasurement.record(cpuUsage); } })); } - return observables; - } - @Nullable - @SuppressWarnings("ReturnValueIgnored") - private static Supplier methodInvoker( - OperatingSystemMXBean osBean, String osBeanClassName, String methodName) { - try { - Class osBeanClass = Class.forName(osBeanClassName); - osBeanClass.cast(osBean); - Method method = osBeanClass.getDeclaredMethod(methodName); - return () -> { - try { - return (double) method.invoke(osBean); - } catch (IllegalAccessException | InvocationTargetException e) { - return null; - } - }; - } catch (ClassNotFoundException | ClassCastException | NoSuchMethodException e) { - return null; - } + return observables; } private Cpu() {} diff --git a/instrumentation/runtime-telemetry/runtime-telemetry-java8/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java8/GarbageCollector.java b/instrumentation/runtime-telemetry/runtime-telemetry-java8/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java8/GarbageCollector.java index 182a4e0c7e9a..deabc1e034c7 100644 --- a/instrumentation/runtime-telemetry/runtime-telemetry-java8/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java8/GarbageCollector.java +++ b/instrumentation/runtime-telemetry/runtime-telemetry-java8/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java8/GarbageCollector.java @@ -5,17 +5,16 @@ package io.opentelemetry.instrumentation.runtimemetrics.java8; -import static java.util.Collections.emptyList; +import static java.util.Arrays.asList; +import static java.util.Collections.unmodifiableList; import com.sun.management.GarbageCollectionNotificationInfo; import io.opentelemetry.api.OpenTelemetry; -import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.common.Attributes; import io.opentelemetry.api.metrics.DoubleHistogram; -import io.opentelemetry.api.metrics.DoubleHistogramBuilder; import io.opentelemetry.api.metrics.Meter; -import io.opentelemetry.extension.incubator.metrics.ExtendedDoubleHistogramBuilder; import io.opentelemetry.instrumentation.runtimemetrics.java8.internal.JmxRuntimeMetricsUtil; +import io.opentelemetry.semconv.JvmAttributes; import java.lang.management.GarbageCollectorMXBean; import java.lang.management.ManagementFactory; import java.util.ArrayList; @@ -31,13 +30,22 @@ import javax.management.openmbean.CompositeData; /** - * Registers instruments that generate metrics about JVM garbage collection. + * Registers instruments that generate metrics about JVM garbage collection. The metrics generated + * by this class follow the + * stable JVM metrics semantic conventions. * *

    Example usage: * *

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

    Example metrics being exported: + * + *

    + *   jvm.gc.duration{jvm.gc.name="G1 Young Generation",jvm.gc.action="end of minor GC"} 0.022
    + * 
    */ public final class GarbageCollector { @@ -45,8 +53,7 @@ public final class GarbageCollector { private static final double MILLIS_PER_S = TimeUnit.SECONDS.toMillis(1); - private static final AttributeKey GC_KEY = AttributeKey.stringKey("gc"); - private static final AttributeKey ACTION_KEY = AttributeKey.stringKey("action"); + static final List GC_DURATION_BUCKETS = unmodifiableList(asList(0.01, 0.1, 1., 10.)); private static final NotificationFilter GC_FILTER = notification -> @@ -76,13 +83,13 @@ static List registerObservers( Function notificationInfoExtractor) { Meter meter = JmxRuntimeMetricsUtil.getMeter(openTelemetry); - DoubleHistogramBuilder gcDurationBuilder = + DoubleHistogram gcDuration = meter - .histogramBuilder("process.runtime.jvm.gc.duration") - .setDescription("Duration of JVM garbage collection actions") - .setUnit("s"); - setGcDurationBuckets(gcDurationBuilder); - DoubleHistogram gcDuration = gcDurationBuilder.build(); + .histogramBuilder("jvm.gc.duration") + .setDescription("Duration of JVM garbage collection actions.") + .setUnit("s") + .setExplicitBucketBoundariesAdvice(GC_DURATION_BUCKETS) + .build(); List result = new ArrayList<>(); for (GarbageCollectorMXBean gcBean : gcBeans) { @@ -98,15 +105,6 @@ static List registerObservers( return result; } - private static void setGcDurationBuckets(DoubleHistogramBuilder builder) { - if (!(builder instanceof ExtendedDoubleHistogramBuilder)) { - // that shouldn't really happen - return; - } - ((ExtendedDoubleHistogramBuilder) builder) - .setAdvice(advice -> advice.setExplicitBucketBoundaries(emptyList())); - } - private static final class GcNotificationListener implements NotificationListener { private final DoubleHistogram gcDuration; @@ -124,10 +122,14 @@ private GcNotificationListener( public void handleNotification(Notification notification, Object unused) { GarbageCollectionNotificationInfo notificationInfo = notificationInfoExtractor.apply(notification); + + String gcName = notificationInfo.getGcName(); + String gcAction = notificationInfo.getGcAction(); + double duration = notificationInfo.getGcInfo().getDuration() / MILLIS_PER_S; + gcDuration.record( - notificationInfo.getGcInfo().getDuration() / MILLIS_PER_S, - Attributes.of( - GC_KEY, notificationInfo.getGcName(), ACTION_KEY, notificationInfo.getGcAction())); + duration, + Attributes.of(JvmAttributes.JVM_GC_NAME, gcName, JvmAttributes.JVM_GC_ACTION, gcAction)); } } diff --git a/instrumentation/runtime-telemetry/runtime-telemetry-java8/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java8/MemoryPools.java b/instrumentation/runtime-telemetry/runtime-telemetry-java8/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java8/MemoryPools.java index 3dd3385f66e3..0a7228a2894c 100644 --- a/instrumentation/runtime-telemetry/runtime-telemetry-java8/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java8/MemoryPools.java +++ b/instrumentation/runtime-telemetry/runtime-telemetry-java8/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java8/MemoryPools.java @@ -11,6 +11,7 @@ import io.opentelemetry.api.metrics.Meter; import io.opentelemetry.api.metrics.ObservableLongMeasurement; import io.opentelemetry.instrumentation.runtimemetrics.java8.internal.JmxRuntimeMetricsUtil; +import io.opentelemetry.semconv.JvmAttributes; import java.lang.management.ManagementFactory; import java.lang.management.MemoryPoolMXBean; import java.lang.management.MemoryType; @@ -21,7 +22,10 @@ import java.util.function.Function; /** - * Registers measurements that generate metrics about JVM memory pools. + * Registers measurements that generate metrics about JVM memory pools. The metrics generated by + * this class follow the + * stable JVM metrics semantic conventions. * *

    Example usage: * @@ -29,27 +33,19 @@ * MemoryPools.registerObservers(GlobalOpenTelemetry.get()); * } * - *

    Example metrics being exported: Component + *

    Example metrics being exported: * *

    - *   process.runtime.jvm.memory.init{type="heap",pool="G1 Eden Space"} 1000000
    - *   process.runtime.jvm.memory.usage{type="heap",pool="G1 Eden Space"} 2500000
    - *   process.runtime.jvm.memory.committed{type="heap",pool="G1 Eden Space"} 3000000
    - *   process.runtime.jvm.memory.limit{type="heap",pool="G1 Eden Space"} 4000000
    - *   process.runtime.jvm.memory.usage_after_last_gc{type="heap",pool="G1 Eden Space"} 1500000
    - *   process.runtime.jvm.memory.init{type="non_heap",pool="Metaspace"} 200
    - *   process.runtime.jvm.memory.usage{type="non_heap",pool="Metaspace"} 400
    - *   process.runtime.jvm.memory.committed{type="non_heap",pool="Metaspace"} 500
    + *   jvm.memory.used{type="heap",pool="G1 Eden Space"} 2500000
    + *   jvm.memory.committed{type="heap",pool="G1 Eden Space"} 3000000
    + *   jvm.memory.limit{type="heap",pool="G1 Eden Space"} 4000000
    + *   jvm.memory.used_after_last_gc{type="heap",pool="G1 Eden Space"} 1500000
    + *   jvm.memory.used{type="non_heap",pool="Metaspace"} 400
    + *   jvm.memory.committed{type="non_heap",pool="Metaspace"} 500
      * 
    */ public final class MemoryPools { - private static final AttributeKey TYPE_KEY = AttributeKey.stringKey("type"); - private static final AttributeKey POOL_KEY = AttributeKey.stringKey("pool"); - - private static final String HEAP = "heap"; - private static final String NON_HEAP = "non_heap"; - /** Register observers for java runtime memory metrics. */ public static List registerObservers(OpenTelemetry openTelemetry) { return registerObservers(openTelemetry, ManagementFactory.getMemoryPoolMXBeans()); @@ -60,51 +56,64 @@ static List registerObservers( OpenTelemetry openTelemetry, List poolBeans) { Meter meter = JmxRuntimeMetricsUtil.getMeter(openTelemetry); List observables = new ArrayList<>(); - observables.add( - meter - .upDownCounterBuilder("process.runtime.jvm.memory.usage") - .setDescription("Measure of memory used") - .setUnit("By") - .buildWithCallback( - callback(poolBeans, MemoryPoolMXBean::getUsage, MemoryUsage::getUsed))); observables.add( meter - .upDownCounterBuilder("process.runtime.jvm.memory.init") - .setDescription("Measure of initial memory requested") + .upDownCounterBuilder("jvm.memory.used") + .setDescription("Measure of memory used.") .setUnit("By") .buildWithCallback( - callback(poolBeans, MemoryPoolMXBean::getUsage, MemoryUsage::getInit))); - + callback( + JvmAttributes.JVM_MEMORY_POOL_NAME, + JvmAttributes.JVM_MEMORY_TYPE, + poolBeans, + MemoryPoolMXBean::getUsage, + MemoryUsage::getUsed))); observables.add( meter - .upDownCounterBuilder("process.runtime.jvm.memory.committed") - .setDescription("Measure of memory committed") + .upDownCounterBuilder("jvm.memory.committed") + .setDescription("Measure of memory committed.") .setUnit("By") .buildWithCallback( - callback(poolBeans, MemoryPoolMXBean::getUsage, MemoryUsage::getCommitted))); - + callback( + JvmAttributes.JVM_MEMORY_POOL_NAME, + JvmAttributes.JVM_MEMORY_TYPE, + poolBeans, + MemoryPoolMXBean::getUsage, + MemoryUsage::getCommitted))); observables.add( meter - .upDownCounterBuilder("process.runtime.jvm.memory.limit") - .setDescription("Measure of max obtainable memory") + .upDownCounterBuilder("jvm.memory.limit") + .setDescription("Measure of max obtainable memory.") .setUnit("By") .buildWithCallback( - callback(poolBeans, MemoryPoolMXBean::getUsage, MemoryUsage::getMax))); - + callback( + JvmAttributes.JVM_MEMORY_POOL_NAME, + JvmAttributes.JVM_MEMORY_TYPE, + poolBeans, + MemoryPoolMXBean::getUsage, + MemoryUsage::getMax))); observables.add( meter - .upDownCounterBuilder("process.runtime.jvm.memory.usage_after_last_gc") + .upDownCounterBuilder("jvm.memory.used_after_last_gc") .setDescription( - "Measure of memory used after the most recent garbage collection event on this pool") + "Measure of memory used, as measured after the most recent garbage collection event on this pool.") .setUnit("By") .buildWithCallback( - callback(poolBeans, MemoryPoolMXBean::getCollectionUsage, MemoryUsage::getUsed))); + callback( + JvmAttributes.JVM_MEMORY_POOL_NAME, + JvmAttributes.JVM_MEMORY_TYPE, + poolBeans, + MemoryPoolMXBean::getCollectionUsage, + MemoryUsage::getUsed))); + return observables; } // Visible for testing static Consumer callback( + AttributeKey poolNameKey, + AttributeKey memoryTypeKey, List poolBeans, Function memoryUsageExtractor, Function valueExtractor) { @@ -112,8 +121,8 @@ static Consumer callback( for (MemoryPoolMXBean pool : poolBeans) { attributeSets.add( Attributes.builder() - .put(POOL_KEY, pool.getName()) - .put(TYPE_KEY, memoryType(pool.getType())) + .put(poolNameKey, pool.getName()) + .put(memoryTypeKey, memoryType(pool.getType())) .build()); } @@ -137,9 +146,9 @@ static Consumer callback( private static String memoryType(MemoryType memoryType) { switch (memoryType) { case HEAP: - return HEAP; + return JvmAttributes.JvmMemoryTypeValues.HEAP; case NON_HEAP: - return NON_HEAP; + return JvmAttributes.JvmMemoryTypeValues.NON_HEAP; } return "unknown"; } diff --git a/instrumentation/runtime-telemetry/runtime-telemetry-java8/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java8/Threads.java b/instrumentation/runtime-telemetry/runtime-telemetry-java8/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java8/Threads.java index 5cc38d422187..238c15e37a74 100644 --- a/instrumentation/runtime-telemetry/runtime-telemetry-java8/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java8/Threads.java +++ b/instrumentation/runtime-telemetry/runtime-telemetry-java8/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java8/Threads.java @@ -5,18 +5,33 @@ package io.opentelemetry.instrumentation.runtimemetrics.java8; +import static java.util.Objects.requireNonNull; + import io.opentelemetry.api.OpenTelemetry; -import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.common.Attributes; import io.opentelemetry.api.metrics.Meter; +import io.opentelemetry.api.metrics.ObservableLongMeasurement; import io.opentelemetry.instrumentation.runtimemetrics.java8.internal.JmxRuntimeMetricsUtil; +import io.opentelemetry.semconv.JvmAttributes; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; import java.lang.management.ManagementFactory; +import java.lang.management.ThreadInfo; import java.lang.management.ThreadMXBean; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.function.Consumer; +import javax.annotation.Nullable; /** - * Registers measurements that generate metrics about JVM threads. + * Registers measurements that generate metrics about JVM threads. The metrics generated by this + * class follow the + * stable JVM metrics semantic conventions. * *

    Example usage: * @@ -27,8 +42,10 @@ *

    Example metrics being exported: * *

    - *   process.runtime.jvm.threads.count{daemon=true} 2
    - *   process.runtime.jvm.threads.count{daemon=false} 5
    + *   jvm.thread.count{jvm.thread.daemon=true,jvm.thread.state="waiting"} 1
    + *   jvm.thread.count{jvm.thread.daemon=true,jvm.thread.state="runnable"} 2
    + *   jvm.thread.count{jvm.thread.daemon=false,jvm.thread.state="waiting"} 2
    + *   jvm.thread.count{jvm.thread.daemon=false,jvm.thread.state="runnable"} 3
      * 
    */ public final class Threads { @@ -36,8 +53,6 @@ public final class Threads { // Visible for testing static final Threads INSTANCE = new Threads(); - static final AttributeKey DAEMON = AttributeKey.booleanKey("daemon"); - /** Register observers for java runtime class metrics. */ public static List registerObservers(OpenTelemetry openTelemetry) { return INSTANCE.registerObservers(openTelemetry, ManagementFactory.getThreadMXBean()); @@ -47,22 +62,75 @@ public static List registerObservers(OpenTelemetry openTelemetry) List registerObservers(OpenTelemetry openTelemetry, ThreadMXBean threadBean) { Meter meter = JmxRuntimeMetricsUtil.getMeter(openTelemetry); List observables = new ArrayList<>(); + observables.add( meter - .upDownCounterBuilder("process.runtime.jvm.threads.count") - .setDescription("Number of executing threads") + .upDownCounterBuilder("jvm.thread.count") + .setDescription("Number of executing platform threads.") .setUnit("{thread}") .buildWithCallback( - observableMeasurement -> { - observableMeasurement.record( - threadBean.getDaemonThreadCount(), - Attributes.builder().put(DAEMON, true).build()); - observableMeasurement.record( - threadBean.getThreadCount() - threadBean.getDaemonThreadCount(), - Attributes.builder().put(DAEMON, false).build()); - })); + isJava9OrNewer() ? java9AndNewerCallback(threadBean) : java8Callback(threadBean))); + return observables; } + @Nullable private static final MethodHandle THREAD_INFO_IS_DAEMON; + + static { + MethodHandle isDaemon; + try { + isDaemon = + MethodHandles.publicLookup() + .findVirtual(ThreadInfo.class, "isDaemon", MethodType.methodType(boolean.class)); + } catch (NoSuchMethodException | IllegalAccessException e) { + isDaemon = null; + } + THREAD_INFO_IS_DAEMON = isDaemon; + } + + private static boolean isJava9OrNewer() { + return THREAD_INFO_IS_DAEMON != null; + } + + private static Consumer java8Callback(ThreadMXBean threadBean) { + return measurement -> { + int daemonThreadCount = threadBean.getDaemonThreadCount(); + measurement.record( + daemonThreadCount, + Attributes.builder().put(JvmAttributes.JVM_THREAD_DAEMON, true).build()); + measurement.record( + threadBean.getThreadCount() - daemonThreadCount, + Attributes.builder().put(JvmAttributes.JVM_THREAD_DAEMON, false).build()); + }; + } + + private static Consumer java9AndNewerCallback( + ThreadMXBean threadBean) { + return measurement -> { + Map counts = new HashMap<>(); + long[] threadIds = threadBean.getAllThreadIds(); + for (ThreadInfo threadInfo : threadBean.getThreadInfo(threadIds)) { + if (threadInfo == null) { + continue; + } + Attributes threadAttributes = threadAttributes(threadInfo); + counts.compute(threadAttributes, (k, value) -> value == null ? 1 : value + 1); + } + counts.forEach((threadAttributes, count) -> measurement.record(count, threadAttributes)); + }; + } + + private static Attributes threadAttributes(ThreadInfo threadInfo) { + boolean isDaemon; + try { + isDaemon = (boolean) requireNonNull(THREAD_INFO_IS_DAEMON).invoke(threadInfo); + } catch (Throwable e) { + throw new IllegalStateException("Unexpected error happened during ThreadInfo#isDaemon()", e); + } + String threadState = threadInfo.getThreadState().name().toLowerCase(Locale.ROOT); + return Attributes.of( + JvmAttributes.JVM_THREAD_DAEMON, isDaemon, JvmAttributes.JVM_THREAD_STATE, threadState); + } + private Threads() {} } diff --git a/instrumentation/runtime-telemetry/runtime-telemetry-java8/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java8/internal/CpuMethods.java b/instrumentation/runtime-telemetry/runtime-telemetry-java8/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java8/internal/CpuMethods.java new file mode 100644 index 000000000000..479170865901 --- /dev/null +++ b/instrumentation/runtime-telemetry/runtime-telemetry-java8/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java8/internal/CpuMethods.java @@ -0,0 +1,103 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.runtimemetrics.java8.internal; + +import java.lang.management.ManagementFactory; +import java.lang.management.OperatingSystemMXBean; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.function.Supplier; +import javax.annotation.Nullable; + +/** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time. + */ +public final class CpuMethods { + + private static final String OS_BEAN_J9 = "com.ibm.lang.management.OperatingSystemMXBean"; + private static final String OS_BEAN_HOTSPOT = "com.sun.management.OperatingSystemMXBean"; + private static final String METHOD_PROCESS_CPU_TIME = "getProcessCpuTime"; + private static final String METHOD_PROCESS_CPU_LOAD = "getProcessCpuLoad"; + private static final String METHOD_CPU_LOAD = "getCpuLoad"; + private static final String METHOD_SYSTEM_CPU_LOAD = "getSystemCpuLoad"; + + @Nullable private static final Supplier processCpuTime; + @Nullable private static final Supplier processCpuUtilization; + @Nullable private static final Supplier systemCpuUtilization; + + static { + OperatingSystemMXBean osBean = ManagementFactory.getOperatingSystemMXBean(); + + Supplier processCpuTimeSupplier = + methodInvoker(osBean, OS_BEAN_HOTSPOT, METHOD_PROCESS_CPU_TIME, Long.class); + if (processCpuTimeSupplier == null) { + // More users will be on hotspot than j9, so check for j9 second + processCpuTimeSupplier = + methodInvoker(osBean, OS_BEAN_J9, METHOD_PROCESS_CPU_TIME, Long.class); + } + processCpuTime = processCpuTimeSupplier; + + Supplier processCpuSupplier = + methodInvoker(osBean, OS_BEAN_HOTSPOT, METHOD_PROCESS_CPU_LOAD, Double.class); + if (processCpuSupplier == null) { + // More users will be on hotspot than j9, so check for j9 second + processCpuSupplier = methodInvoker(osBean, OS_BEAN_J9, METHOD_PROCESS_CPU_LOAD, Double.class); + } + processCpuUtilization = processCpuSupplier; + + // As of java 14, com.sun.management.OperatingSystemMXBean#getCpuLoad() is preferred and + // #getSystemCpuLoad() is deprecated + Supplier systemCpuSupplier = + methodInvoker(osBean, OS_BEAN_HOTSPOT, METHOD_CPU_LOAD, Double.class); + if (systemCpuSupplier == null) { + systemCpuSupplier = + methodInvoker(osBean, OS_BEAN_HOTSPOT, METHOD_SYSTEM_CPU_LOAD, Double.class); + } + if (systemCpuSupplier == null) { + // More users will be on hotspot than j9, so check for j9 second + systemCpuSupplier = methodInvoker(osBean, OS_BEAN_J9, METHOD_SYSTEM_CPU_LOAD, Double.class); + } + systemCpuUtilization = systemCpuSupplier; + } + + @Nullable + @SuppressWarnings("ReturnValueIgnored") + private static Supplier methodInvoker( + OperatingSystemMXBean osBean, + String osBeanClassName, + String methodName, + Class returnType) { + try { + Class osBeanClass = Class.forName(osBeanClassName); + osBeanClass.cast(osBean); + Method method = osBeanClass.getDeclaredMethod(methodName); + return () -> { + try { + return returnType.cast(method.invoke(osBean)); + } catch (IllegalAccessException | InvocationTargetException e) { + return null; + } + }; + } catch (ClassNotFoundException | ClassCastException | NoSuchMethodException e) { + return null; + } + } + + public static Supplier processCpuTime() { + return processCpuTime; + } + + public static Supplier processCpuUtilization() { + return processCpuUtilization; + } + + public static Supplier systemCpuUtilization() { + return systemCpuUtilization; + } + + private CpuMethods() {} +} diff --git a/instrumentation/runtime-telemetry/runtime-telemetry-java8/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java8/BufferPools.java b/instrumentation/runtime-telemetry/runtime-telemetry-java8/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java8/internal/ExperimentalBufferPools.java similarity index 62% rename from instrumentation/runtime-telemetry/runtime-telemetry-java8/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java8/BufferPools.java rename to instrumentation/runtime-telemetry/runtime-telemetry-java8/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java8/internal/ExperimentalBufferPools.java index 2da60ef67e04..ccb5b001721b 100644 --- a/instrumentation/runtime-telemetry/runtime-telemetry-java8/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java8/BufferPools.java +++ b/instrumentation/runtime-telemetry/runtime-telemetry-java8/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java8/internal/ExperimentalBufferPools.java @@ -3,14 +3,15 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.instrumentation.runtimemetrics.java8; +package io.opentelemetry.instrumentation.runtimemetrics.java8.internal; + +import static io.opentelemetry.api.common.AttributeKey.stringKey; import io.opentelemetry.api.OpenTelemetry; import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.common.Attributes; import io.opentelemetry.api.metrics.Meter; import io.opentelemetry.api.metrics.ObservableLongMeasurement; -import io.opentelemetry.instrumentation.runtimemetrics.java8.internal.JmxRuntimeMetricsUtil; import java.lang.management.BufferPoolMXBean; import java.lang.management.ManagementFactory; import java.util.ArrayList; @@ -19,24 +20,15 @@ import java.util.function.Function; /** - * Registers measurements that generate metrics about buffer pools. - * - *

    Example usage: - * - *

    {@code
    - * BufferPools.registerObservers(GlobalOpenTelemetry.get());
    - * }
    - * - *

    Example metrics being exported: + * Registers measurements that generate experimental metrics about buffer pools. * - *

    - *   process.runtime.jvm.buffer.usage.usage{pool="buffer_pool"} 500
    - *   process.runtime.jvm.buffer.usage.max{pool="buffer_pool"} 1500
    - *   process.runtime.jvm.buffer.usage.count{pool="buffer_pool"} 15
    - * 
    + *

    This class is internal and is hence not for public use. Its APIs are unstable and can change + * at any time. */ -public final class BufferPools { - private static final AttributeKey POOL_KEY = AttributeKey.stringKey("pool"); +public final class ExperimentalBufferPools { + + private static final AttributeKey JVM_BUFFER_POOL_NAME = + stringKey("jvm.buffer.pool.name"); /** Register observers for java runtime buffer pool metrics. */ public static List registerObservers(OpenTelemetry openTelemetry) { @@ -48,26 +40,26 @@ public static List registerObservers(OpenTelemetry openTelemetry) // Visible for testing static List registerObservers( OpenTelemetry openTelemetry, List bufferBeans) { + List observables = new ArrayList<>(); Meter meter = JmxRuntimeMetricsUtil.getMeter(openTelemetry); observables.add( meter - .upDownCounterBuilder("process.runtime.jvm.buffer.usage") - .setDescription("Memory that the Java virtual machine is using for this buffer pool") + .upDownCounterBuilder("jvm.buffer.memory.usage") + .setDescription("Measure of memory used by buffers.") .setUnit("By") .buildWithCallback(callback(bufferBeans, BufferPoolMXBean::getMemoryUsed))); - observables.add( meter - .upDownCounterBuilder("process.runtime.jvm.buffer.limit") - .setDescription("Total capacity of the buffers in this pool") + .upDownCounterBuilder("jvm.buffer.memory.limit") + .setDescription("Measure of total memory capacity of buffers.") .setUnit("By") .buildWithCallback(callback(bufferBeans, BufferPoolMXBean::getTotalCapacity))); observables.add( meter - .upDownCounterBuilder("process.runtime.jvm.buffer.count") - .setDescription("The number of buffers in the pool") - .setUnit("{buffers}") + .upDownCounterBuilder("jvm.buffer.count") + .setDescription("Number of buffers in the pool.") + .setUnit("{buffer}") .buildWithCallback(callback(bufferBeans, BufferPoolMXBean::getCount))); return observables; } @@ -77,7 +69,7 @@ static Consumer callback( List bufferPools, Function extractor) { List attributeSets = new ArrayList<>(bufferPools.size()); for (BufferPoolMXBean pool : bufferPools) { - attributeSets.add(Attributes.builder().put(POOL_KEY, pool.getName()).build()); + attributeSets.add(Attributes.builder().put(JVM_BUFFER_POOL_NAME, pool.getName()).build()); } return measurement -> { for (int i = 0; i < bufferPools.size(); i++) { @@ -90,5 +82,5 @@ static Consumer callback( }; } - private BufferPools() {} + private ExperimentalBufferPools() {} } diff --git a/instrumentation/runtime-telemetry/runtime-telemetry-java8/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java8/internal/ExperimentalCpu.java b/instrumentation/runtime-telemetry/runtime-telemetry-java8/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java8/internal/ExperimentalCpu.java new file mode 100644 index 000000000000..5fd928495687 --- /dev/null +++ b/instrumentation/runtime-telemetry/runtime-telemetry-java8/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java8/internal/ExperimentalCpu.java @@ -0,0 +1,72 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.runtimemetrics.java8.internal; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.metrics.Meter; +import java.lang.management.ManagementFactory; +import java.lang.management.OperatingSystemMXBean; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Supplier; +import javax.annotation.Nullable; + +/** + * Registers measurements that generate experimental metrics about CPU. + * + *

    This class is internal and is hence not for public use. Its APIs are unstable and can change + * at any time. + */ +public final class ExperimentalCpu { + + /** Register observers for java runtime experimental CPU metrics. */ + public static List registerObservers(OpenTelemetry openTelemetry) { + return registerObservers( + openTelemetry, + ManagementFactory.getOperatingSystemMXBean(), + CpuMethods.systemCpuUtilization()); + } + + // Visible for testing + static List registerObservers( + OpenTelemetry openTelemetry, + OperatingSystemMXBean osBean, + @Nullable Supplier systemCpuUtilization) { + + Meter meter = JmxRuntimeMetricsUtil.getMeter(openTelemetry); + List observables = new ArrayList<>(); + observables.add( + meter + .gaugeBuilder("jvm.system.cpu.load_1m") + .setDescription( + "Average CPU load of the whole system for the last minute as reported by the JVM.") + .setUnit("{run_queue_item}") + .buildWithCallback( + observableMeasurement -> { + double loadAverage = osBean.getSystemLoadAverage(); + if (loadAverage >= 0) { + observableMeasurement.record(loadAverage); + } + })); + if (systemCpuUtilization != null) { + observables.add( + meter + .gaugeBuilder("jvm.system.cpu.utilization") + .setDescription("Recent CPU utilization for the whole system as reported by the JVM.") + .setUnit("1") + .buildWithCallback( + observableMeasurement -> { + Double cpuUsage = systemCpuUtilization.get(); + if (cpuUsage != null && cpuUsage >= 0) { + observableMeasurement.record(cpuUsage); + } + })); + } + return observables; + } + + private ExperimentalCpu() {} +} diff --git a/instrumentation/runtime-telemetry/runtime-telemetry-java8/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java8/internal/ExperimentalMemoryPools.java b/instrumentation/runtime-telemetry/runtime-telemetry-java8/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java8/internal/ExperimentalMemoryPools.java new file mode 100644 index 000000000000..979008c04e32 --- /dev/null +++ b/instrumentation/runtime-telemetry/runtime-telemetry-java8/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java8/internal/ExperimentalMemoryPools.java @@ -0,0 +1,86 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.runtimemetrics.java8.internal; + +import static java.util.Collections.singletonList; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.metrics.Meter; +import io.opentelemetry.api.metrics.ObservableLongMeasurement; +import io.opentelemetry.semconv.JvmAttributes; +import java.lang.management.ManagementFactory; +import java.lang.management.MemoryPoolMXBean; +import java.lang.management.MemoryType; +import java.lang.management.MemoryUsage; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; + +/** + * Registers measurements that generate experimental metrics about memory pools. + * + *

    This class is internal and is hence not for public use. Its APIs are unstable and can change + * at any time. + */ +public final class ExperimentalMemoryPools { + + /** Register observers for java runtime experimental memory metrics. */ + public static List registerObservers(OpenTelemetry openTelemetry) { + return registerObservers(openTelemetry, ManagementFactory.getMemoryPoolMXBeans()); + } + + // Visible for testing + static List registerObservers( + OpenTelemetry openTelemetry, List poolBeans) { + + Meter meter = JmxRuntimeMetricsUtil.getMeter(openTelemetry); + return singletonList( + meter + .upDownCounterBuilder("jvm.memory.init") + .setDescription("Measure of initial memory requested.") + .setUnit("By") + .buildWithCallback(callback(poolBeans))); + } + + private static Consumer callback(List poolBeans) { + List attributeSets = new ArrayList<>(poolBeans.size()); + for (MemoryPoolMXBean pool : poolBeans) { + attributeSets.add( + Attributes.builder() + .put(JvmAttributes.JVM_MEMORY_POOL_NAME, pool.getName()) + .put(JvmAttributes.JVM_MEMORY_TYPE, memoryType(pool.getType())) + .build()); + } + + return measurement -> { + for (int i = 0; i < poolBeans.size(); i++) { + Attributes attributes = attributeSets.get(i); + MemoryUsage memoryUsage = poolBeans.get(i).getUsage(); + if (memoryUsage == null) { + // JVM may return null in special cases for MemoryPoolMXBean.getUsage() + continue; + } + long value = memoryUsage.getInit(); + if (value != -1) { + measurement.record(value, attributes); + } + } + }; + } + + private static String memoryType(MemoryType memoryType) { + switch (memoryType) { + case HEAP: + return JvmAttributes.JvmMemoryTypeValues.HEAP; + case NON_HEAP: + return JvmAttributes.JvmMemoryTypeValues.NON_HEAP; + } + return "unknown"; + } + + private ExperimentalMemoryPools() {} +} diff --git a/instrumentation/runtime-telemetry/runtime-telemetry-java8/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java8/internal/JmxRuntimeMetricsUtil.java b/instrumentation/runtime-telemetry/runtime-telemetry-java8/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java8/internal/JmxRuntimeMetricsUtil.java index 7dcb0245c1b0..334d405ad26f 100644 --- a/instrumentation/runtime-telemetry/runtime-telemetry-java8/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java8/internal/JmxRuntimeMetricsUtil.java +++ b/instrumentation/runtime-telemetry/runtime-telemetry-java8/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java8/internal/JmxRuntimeMetricsUtil.java @@ -32,6 +32,15 @@ public static Meter getMeter(OpenTelemetry openTelemetry) { return meterBuilder.build(); } + public static String getInstrumentationName() { + return INSTRUMENTATION_NAME; + } + + @Nullable + public static String getInstrumentationVersion() { + return INSTRUMENTATION_VERSION; + } + public static void closeObservers(List observables) { observables.forEach( observable -> { diff --git a/instrumentation/runtime-telemetry/runtime-telemetry-java8/library/src/test/java/io/opentelemetry/instrumentation/runtimemetrics/java8/ClassesTest.java b/instrumentation/runtime-telemetry/runtime-telemetry-java8/library/src/test/java/io/opentelemetry/instrumentation/runtimemetrics/java8/ClassesStableSemconvTest.java similarity index 94% rename from instrumentation/runtime-telemetry/runtime-telemetry-java8/library/src/test/java/io/opentelemetry/instrumentation/runtimemetrics/java8/ClassesTest.java rename to instrumentation/runtime-telemetry/runtime-telemetry-java8/library/src/test/java/io/opentelemetry/instrumentation/runtimemetrics/java8/ClassesStableSemconvTest.java index 382e3eb3d0e7..e6fa5c9f1dec 100644 --- a/instrumentation/runtime-telemetry/runtime-telemetry-java8/library/src/test/java/io/opentelemetry/instrumentation/runtimemetrics/java8/ClassesTest.java +++ b/instrumentation/runtime-telemetry/runtime-telemetry-java8/library/src/test/java/io/opentelemetry/instrumentation/runtimemetrics/java8/ClassesStableSemconvTest.java @@ -20,7 +20,7 @@ import org.mockito.junit.jupiter.MockitoExtension; @ExtendWith(MockitoExtension.class) -class ClassesTest { +class ClassesStableSemconvTest { @RegisterExtension static final InstrumentationExtension testing = LibraryInstrumentationExtension.create(); @@ -37,13 +37,13 @@ void registerObservers() { testing.waitAndAssertMetrics( "io.opentelemetry.runtime-telemetry-java8", - "process.runtime.jvm.classes.loaded", + "jvm.class.loaded", metrics -> metrics.anySatisfy( metricData -> assertThat(metricData) .hasInstrumentationScope(EXPECTED_SCOPE) - .hasDescription("Number of classes loaded since JVM start") + .hasDescription("Number of classes loaded since JVM start.") .hasUnit("{class}") .hasLongSumSatisfying( sum -> @@ -53,13 +53,13 @@ void registerObservers() { point.hasValue(3).hasAttributes(Attributes.empty()))))); testing.waitAndAssertMetrics( "io.opentelemetry.runtime-telemetry-java8", - "process.runtime.jvm.classes.unloaded", + "jvm.class.unloaded", metrics -> metrics.anySatisfy( metricData -> assertThat(metricData) .hasInstrumentationScope(EXPECTED_SCOPE) - .hasDescription("Number of classes unloaded since JVM start") + .hasDescription("Number of classes unloaded since JVM start.") .hasUnit("{class}") .hasLongSumSatisfying( sum -> @@ -69,13 +69,13 @@ void registerObservers() { point.hasValue(2).hasAttributes(Attributes.empty()))))); testing.waitAndAssertMetrics( "io.opentelemetry.runtime-telemetry-java8", - "process.runtime.jvm.classes.current_loaded", + "jvm.class.count", metrics -> metrics.anySatisfy( metricData -> assertThat(metricData) .hasInstrumentationScope(EXPECTED_SCOPE) - .hasDescription("Number of classes currently loaded") + .hasDescription("Number of classes currently loaded.") .hasUnit("{class}") .hasLongSumSatisfying( sum -> diff --git a/instrumentation/runtime-telemetry/runtime-telemetry-java8/library/src/test/java/io/opentelemetry/instrumentation/runtimemetrics/java8/CpuStableSemconvTest.java b/instrumentation/runtime-telemetry/runtime-telemetry-java8/library/src/test/java/io/opentelemetry/instrumentation/runtimemetrics/java8/CpuStableSemconvTest.java new file mode 100644 index 000000000000..507602d62435 --- /dev/null +++ b/instrumentation/runtime-telemetry/runtime-telemetry-java8/library/src/test/java/io/opentelemetry/instrumentation/runtimemetrics/java8/CpuStableSemconvTest.java @@ -0,0 +1,78 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.runtimemetrics.java8; + +import static io.opentelemetry.instrumentation.runtimemetrics.java8.ScopeUtil.EXPECTED_SCOPE; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; + +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.LibraryInstrumentationExtension; +import java.util.concurrent.TimeUnit; +import java.util.function.IntSupplier; +import java.util.function.Supplier; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +class CpuStableSemconvTest { + + @RegisterExtension + static final InstrumentationExtension testing = LibraryInstrumentationExtension.create(); + + @Test + void registerObservers() { + IntSupplier availableProcessors = () -> 8; + Supplier processCpuTime = () -> TimeUnit.SECONDS.toNanos(42); + Supplier processCpuUtilization = () -> 0.05; + + Cpu.INSTANCE.registerObservers( + testing.getOpenTelemetry(), availableProcessors, processCpuTime, processCpuUtilization); + + testing.waitAndAssertMetrics( + "io.opentelemetry.runtime-telemetry-java8", + "jvm.cpu.time", + metrics -> + metrics.anySatisfy( + metricData -> + assertThat(metricData) + .hasInstrumentationScope(EXPECTED_SCOPE) + .hasDescription("CPU time used by the process as reported by the JVM.") + .hasUnit("s") + .hasDoubleSumSatisfying( + count -> + count + .isMonotonic() + .hasPointsSatisfying(point -> point.hasValue(42))))); + testing.waitAndAssertMetrics( + "io.opentelemetry.runtime-telemetry-java8", + "jvm.cpu.count", + metrics -> + metrics.anySatisfy( + metricData -> + assertThat(metricData) + .hasInstrumentationScope(EXPECTED_SCOPE) + .hasDescription( + "Number of processors available to the Java virtual machine.") + .hasUnit("{cpu}") + .hasLongSumSatisfying( + count -> + count + .isNotMonotonic() + .hasPointsSatisfying(point -> point.hasValue(8))))); + testing.waitAndAssertMetrics( + "io.opentelemetry.runtime-telemetry-java8", + "jvm.cpu.recent_utilization", + metrics -> + metrics.anySatisfy( + metricData -> + assertThat(metricData) + .hasInstrumentationScope(EXPECTED_SCOPE) + .hasDescription( + "Recent CPU utilization for the process as reported by the JVM.") + .hasUnit("1") + .hasDoubleGaugeSatisfying( + gauge -> gauge.hasPointsSatisfying(point -> point.hasValue(0.05))))); + } +} diff --git a/instrumentation/runtime-telemetry/runtime-telemetry-java8/library/src/test/java/io/opentelemetry/instrumentation/runtimemetrics/java8/GarbageCollectorTest.java b/instrumentation/runtime-telemetry/runtime-telemetry-java8/library/src/test/java/io/opentelemetry/instrumentation/runtimemetrics/java8/GarbageCollectorTest.java index aa74f48d3214..92bf23d10748 100644 --- a/instrumentation/runtime-telemetry/runtime-telemetry-java8/library/src/test/java/io/opentelemetry/instrumentation/runtimemetrics/java8/GarbageCollectorTest.java +++ b/instrumentation/runtime-telemetry/runtime-telemetry-java8/library/src/test/java/io/opentelemetry/instrumentation/runtimemetrics/java8/GarbageCollectorTest.java @@ -7,6 +7,7 @@ import static io.opentelemetry.instrumentation.runtimemetrics.java8.ScopeUtil.EXPECTED_SCOPE; import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; +import static java.util.Collections.singletonList; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; @@ -18,7 +19,6 @@ import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; import io.opentelemetry.instrumentation.testing.junit.LibraryInstrumentationExtension; import java.lang.management.GarbageCollectorMXBean; -import java.util.Collections; import java.util.concurrent.atomic.AtomicLong; import javax.management.Notification; import javax.management.NotificationEmitter; @@ -37,6 +37,9 @@ @MockitoSettings(strictness = Strictness.LENIENT) class GarbageCollectorTest { + static final double[] GC_DURATION_BUCKETS = + GarbageCollector.GC_DURATION_BUCKETS.stream().mapToDouble(d -> d).toArray(); + @RegisterExtension static final InstrumentationExtension testing = LibraryInstrumentationExtension.create(); @@ -49,7 +52,7 @@ class GarbageCollectorTest { void registerObservers() { GarbageCollector.registerObservers( testing.getOpenTelemetry(), - Collections.singletonList(gcBean), + singletonList(gcBean), GarbageCollectorTest::getGcNotificationInfo); NotificationEmitter notificationEmitter = (NotificationEmitter) gcBean; @@ -65,13 +68,13 @@ void registerObservers() { testing.waitAndAssertMetrics( "io.opentelemetry.runtime-telemetry-java8", - "process.runtime.jvm.gc.duration", + "jvm.gc.duration", metrics -> metrics.anySatisfy( metricData -> assertThat(metricData) .hasInstrumentationScope(EXPECTED_SCOPE) - .hasDescription("Duration of JVM garbage collection actions") + .hasDescription("Duration of JVM garbage collection actions.") .hasUnit("s") .hasHistogramSatisfying( histogram -> @@ -82,20 +85,20 @@ void registerObservers() { .hasSum(0.022) .hasAttributes( Attributes.builder() - .put("gc", "G1 Young Generation") - .put("action", "end of minor GC") + .put("jvm.gc.name", "G1 Young Generation") + .put("jvm.gc.action", "end of minor GC") .build()) - .hasBucketBoundaries(), + .hasBucketBoundaries(GC_DURATION_BUCKETS), point -> point .hasCount(1) .hasSum(0.011) .hasAttributes( Attributes.builder() - .put("gc", "G1 Old Generation") - .put("action", "end of major GC") + .put("jvm.gc.name", "G1 Old Generation") + .put("jvm.gc.action", "end of major GC") .build()) - .hasBucketBoundaries())))); + .hasBucketBoundaries(GC_DURATION_BUCKETS))))); } private static Notification createTestNotification( diff --git a/instrumentation/runtime-telemetry/runtime-telemetry-java8/library/src/test/java/io/opentelemetry/instrumentation/runtimemetrics/java8/MemoryPoolsTest.java b/instrumentation/runtime-telemetry/runtime-telemetry-java8/library/src/test/java/io/opentelemetry/instrumentation/runtimemetrics/java8/MemoryPoolsStableSemconvTest.java similarity index 66% rename from instrumentation/runtime-telemetry/runtime-telemetry-java8/library/src/test/java/io/opentelemetry/instrumentation/runtimemetrics/java8/MemoryPoolsTest.java rename to instrumentation/runtime-telemetry/runtime-telemetry-java8/library/src/test/java/io/opentelemetry/instrumentation/runtimemetrics/java8/MemoryPoolsStableSemconvTest.java index 2286d62163fa..391bdb7f10c9 100644 --- a/instrumentation/runtime-telemetry/runtime-telemetry-java8/library/src/test/java/io/opentelemetry/instrumentation/runtimemetrics/java8/MemoryPoolsTest.java +++ b/instrumentation/runtime-telemetry/runtime-telemetry-java8/library/src/test/java/io/opentelemetry/instrumentation/runtimemetrics/java8/MemoryPoolsStableSemconvTest.java @@ -5,8 +5,10 @@ package io.opentelemetry.instrumentation.runtimemetrics.java8; +import static io.opentelemetry.api.common.AttributeKey.stringKey; import static io.opentelemetry.instrumentation.runtimemetrics.java8.ScopeUtil.EXPECTED_SCOPE; import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; +import static java.util.Collections.singletonList; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.eq; @@ -14,7 +16,6 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.common.Attributes; import io.opentelemetry.api.metrics.ObservableLongMeasurement; import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; @@ -23,7 +24,6 @@ import java.lang.management.MemoryType; import java.lang.management.MemoryUsage; import java.util.Arrays; -import java.util.Collections; import java.util.List; import java.util.function.Consumer; import org.junit.jupiter.api.BeforeEach; @@ -38,7 +38,7 @@ @ExtendWith(MockitoExtension.class) @MockitoSettings(strictness = Strictness.LENIENT) -class MemoryPoolsTest { +class MemoryPoolsStableSemconvTest { @RegisterExtension static final InstrumentationExtension testing = LibraryInstrumentationExtension.create(); @@ -70,11 +70,9 @@ void setup() { @Test void registerObservers() { - when(heapPoolUsage.getInit()).thenReturn(10L); when(heapPoolUsage.getUsed()).thenReturn(11L); when(heapPoolUsage.getCommitted()).thenReturn(12L); when(heapPoolUsage.getMax()).thenReturn(13L); - when(nonHeapUsage.getInit()).thenReturn(14L); when(nonHeapUsage.getUsed()).thenReturn(15L); when(nonHeapUsage.getCommitted()).thenReturn(16L); when(nonHeapUsage.getMax()).thenReturn(17L); @@ -84,39 +82,13 @@ void registerObservers() { testing.waitAndAssertMetrics( "io.opentelemetry.runtime-telemetry-java8", - "process.runtime.jvm.memory.init", + "jvm.memory.used", metrics -> metrics.anySatisfy( metricData -> assertThat(metricData) .hasInstrumentationScope(EXPECTED_SCOPE) - .hasDescription("Measure of initial memory requested") - .hasUnit("By") - .hasLongSumSatisfying( - sum -> - sum.hasPointsSatisfying( - point -> - point - .hasValue(10) - .hasAttribute( - AttributeKey.stringKey("pool"), "heap_pool") - .hasAttribute(AttributeKey.stringKey("type"), "heap"), - point -> - point - .hasValue(14) - .hasAttribute( - AttributeKey.stringKey("pool"), "non_heap_pool") - .hasAttribute( - AttributeKey.stringKey("type"), "non_heap"))))); - testing.waitAndAssertMetrics( - "io.opentelemetry.runtime-telemetry-java8", - "process.runtime.jvm.memory.usage", - metrics -> - metrics.anySatisfy( - metricData -> - assertThat(metricData) - .hasInstrumentationScope(EXPECTED_SCOPE) - .hasDescription("Measure of memory used") + .hasDescription("Measure of memory used.") .hasUnit("By") .hasLongSumSatisfying( sum -> @@ -125,24 +97,24 @@ void registerObservers() { point .hasValue(11) .hasAttribute( - AttributeKey.stringKey("pool"), "heap_pool") - .hasAttribute(AttributeKey.stringKey("type"), "heap"), + stringKey("jvm.memory.pool.name"), "heap_pool") + .hasAttribute(stringKey("jvm.memory.type"), "heap"), point -> point .hasValue(15) .hasAttribute( - AttributeKey.stringKey("pool"), "non_heap_pool") + stringKey("jvm.memory.pool.name"), "non_heap_pool") .hasAttribute( - AttributeKey.stringKey("type"), "non_heap"))))); + stringKey("jvm.memory.type"), "non_heap"))))); testing.waitAndAssertMetrics( "io.opentelemetry.runtime-telemetry-java8", - "process.runtime.jvm.memory.committed", + "jvm.memory.committed", metrics -> metrics.anySatisfy( metricData -> assertThat(metricData) .hasInstrumentationScope(EXPECTED_SCOPE) - .hasDescription("Measure of memory committed") + .hasDescription("Measure of memory committed.") .hasUnit("By") .hasLongSumSatisfying( sum -> @@ -151,24 +123,24 @@ void registerObservers() { point .hasValue(12) .hasAttribute( - AttributeKey.stringKey("pool"), "heap_pool") - .hasAttribute(AttributeKey.stringKey("type"), "heap"), + stringKey("jvm.memory.pool.name"), "heap_pool") + .hasAttribute(stringKey("jvm.memory.type"), "heap"), point -> point .hasValue(16) .hasAttribute( - AttributeKey.stringKey("pool"), "non_heap_pool") + stringKey("jvm.memory.pool.name"), "non_heap_pool") .hasAttribute( - AttributeKey.stringKey("type"), "non_heap"))))); + stringKey("jvm.memory.type"), "non_heap"))))); testing.waitAndAssertMetrics( "io.opentelemetry.runtime-telemetry-java8", - "process.runtime.jvm.memory.limit", + "jvm.memory.limit", metrics -> metrics.anySatisfy( metricData -> assertThat(metricData) .hasInstrumentationScope(EXPECTED_SCOPE) - .hasDescription("Measure of max obtainable memory") + .hasDescription("Measure of max obtainable memory.") .hasUnit("By") .hasLongSumSatisfying( sum -> @@ -177,25 +149,25 @@ void registerObservers() { point .hasValue(13) .hasAttribute( - AttributeKey.stringKey("pool"), "heap_pool") - .hasAttribute(AttributeKey.stringKey("type"), "heap"), + stringKey("jvm.memory.pool.name"), "heap_pool") + .hasAttribute(stringKey("jvm.memory.type"), "heap"), point -> point .hasValue(17) .hasAttribute( - AttributeKey.stringKey("pool"), "non_heap_pool") + stringKey("jvm.memory.pool.name"), "non_heap_pool") .hasAttribute( - AttributeKey.stringKey("type"), "non_heap"))))); + stringKey("jvm.memory.type"), "non_heap"))))); testing.waitAndAssertMetrics( "io.opentelemetry.runtime-telemetry-java8", - "process.runtime.jvm.memory.usage_after_last_gc", + "jvm.memory.used_after_last_gc", metrics -> metrics.anySatisfy( metricData -> assertThat(metricData) .hasInstrumentationScope(EXPECTED_SCOPE) .hasDescription( - "Measure of memory used after the most recent garbage collection event on this pool") + "Measure of memory used, as measured after the most recent garbage collection event on this pool.") .hasUnit("By") .hasLongSumSatisfying( sum -> @@ -204,15 +176,15 @@ void registerObservers() { point .hasValue(18) .hasAttribute( - AttributeKey.stringKey("pool"), "heap_pool") - .hasAttribute(AttributeKey.stringKey("type"), "heap"), + stringKey("jvm.memory.pool.name"), "heap_pool") + .hasAttribute(stringKey("jvm.memory.type"), "heap"), point -> point .hasValue(19) .hasAttribute( - AttributeKey.stringKey("pool"), "non_heap_pool") + stringKey("jvm.memory.pool.name"), "non_heap_pool") .hasAttribute( - AttributeKey.stringKey("type"), "non_heap"))))); + stringKey("jvm.memory.type"), "non_heap"))))); } @Test @@ -221,14 +193,28 @@ void callback_Records() { when(nonHeapUsage.getUsed()).thenReturn(2L); Consumer callback = - MemoryPools.callback(beans, MemoryPoolMXBean::getUsage, MemoryUsage::getUsed); + MemoryPools.callback( + stringKey("jvm.memory.pool.name"), + stringKey("jvm.memory.type"), + beans, + MemoryPoolMXBean::getUsage, + MemoryUsage::getUsed); callback.accept(measurement); verify(measurement) - .record(1, Attributes.builder().put("pool", "heap_pool").put("type", "heap").build()); + .record( + 1, + Attributes.builder() + .put("jvm.memory.pool.name", "heap_pool") + .put("jvm.memory.type", "heap") + .build()); verify(measurement) .record( - 2, Attributes.builder().put("pool", "non_heap_pool").put("type", "non_heap").build()); + 2, + Attributes.builder() + .put("jvm.memory.pool.name", "non_heap_pool") + .put("jvm.memory.type", "non_heap") + .build()); } @Test @@ -237,11 +223,21 @@ void callback_SkipRecord() { when(nonHeapUsage.getMax()).thenReturn(-1L); Consumer callback = - MemoryPools.callback(beans, MemoryPoolMXBean::getUsage, MemoryUsage::getMax); + MemoryPools.callback( + stringKey("jvm.memory.pool.name"), + stringKey("jvm.memory.type"), + beans, + MemoryPoolMXBean::getUsage, + MemoryUsage::getMax); callback.accept(measurement); verify(measurement) - .record(1, Attributes.builder().put("pool", "heap_pool").put("type", "heap").build()); + .record( + 1, + Attributes.builder() + .put("jvm.memory.pool.name", "heap_pool") + .put("jvm.memory.type", "heap") + .build()); verify(measurement, never()).record(eq(-1), any()); } @@ -251,7 +247,9 @@ void callback_NullUsage() { Consumer callback = MemoryPools.callback( - Collections.singletonList(heapPoolBean), + stringKey("jvm.memory.pool.name"), + stringKey("jvm.memory.type"), + singletonList(heapPoolBean), MemoryPoolMXBean::getCollectionUsage, MemoryUsage::getUsed); callback.accept(measurement); diff --git a/instrumentation/runtime-telemetry/runtime-telemetry-java8/library/src/test/java/io/opentelemetry/instrumentation/runtimemetrics/java8/ScopeUtil.java b/instrumentation/runtime-telemetry/runtime-telemetry-java8/library/src/test/java/io/opentelemetry/instrumentation/runtimemetrics/java8/ScopeUtil.java index d865e067df5a..3769d2a4ce11 100644 --- a/instrumentation/runtime-telemetry/runtime-telemetry-java8/library/src/test/java/io/opentelemetry/instrumentation/runtimemetrics/java8/ScopeUtil.java +++ b/instrumentation/runtime-telemetry/runtime-telemetry-java8/library/src/test/java/io/opentelemetry/instrumentation/runtimemetrics/java8/ScopeUtil.java @@ -10,14 +10,15 @@ import io.opentelemetry.sdk.common.InstrumentationScopeInfoBuilder; import javax.annotation.Nullable; -class ScopeUtil { +public final class ScopeUtil { + private static final String INSTRUMENTATION_NAME = "io.opentelemetry.runtime-telemetry-java8"; @Nullable private static final String INSTRUMENTATION_VERSION = EmbeddedInstrumentationProperties.findVersion(INSTRUMENTATION_NAME); - static final InstrumentationScopeInfo EXPECTED_SCOPE; + public static final InstrumentationScopeInfo EXPECTED_SCOPE; static { InstrumentationScopeInfoBuilder builder = diff --git a/instrumentation/runtime-telemetry/runtime-telemetry-java8/library/src/test/java/io/opentelemetry/instrumentation/runtimemetrics/java8/ThreadsStableSemconvTest.java b/instrumentation/runtime-telemetry/runtime-telemetry-java8/library/src/test/java/io/opentelemetry/instrumentation/runtimemetrics/java8/ThreadsStableSemconvTest.java new file mode 100644 index 000000000000..5e2364b394f0 --- /dev/null +++ b/instrumentation/runtime-telemetry/runtime-telemetry-java8/library/src/test/java/io/opentelemetry/instrumentation/runtimemetrics/java8/ThreadsStableSemconvTest.java @@ -0,0 +1,149 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.runtimemetrics.java8; + +import static io.opentelemetry.instrumentation.runtimemetrics.java8.ScopeUtil.EXPECTED_SCOPE; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import io.opentelemetry.instrumentation.testing.internal.AutoCleanupExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.LibraryInstrumentationExtension; +import io.opentelemetry.semconv.JvmAttributes; +import java.lang.management.ThreadInfo; +import java.lang.management.ThreadMXBean; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledForJreRange; +import org.junit.jupiter.api.condition.EnabledOnJre; +import org.junit.jupiter.api.condition.JRE; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.mockito.Mock; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.stubbing.Answer; + +@ExtendWith(MockitoExtension.class) +class ThreadsStableSemconvTest { + + @RegisterExtension + static final InstrumentationExtension testing = LibraryInstrumentationExtension.create(); + + @RegisterExtension static final AutoCleanupExtension cleanup = AutoCleanupExtension.create(); + + @Mock private ThreadMXBean threadBean; + + @Test + @EnabledOnJre(JRE.JAVA_8) + void registerObservers_Java8() { + when(threadBean.getThreadCount()).thenReturn(7); + when(threadBean.getDaemonThreadCount()).thenReturn(2); + + Threads.INSTANCE + .registerObservers(testing.getOpenTelemetry(), threadBean) + .forEach(cleanup::deferCleanup); + + testing.waitAndAssertMetrics( + "io.opentelemetry.runtime-telemetry-java8", + "jvm.thread.count", + metrics -> + metrics.anySatisfy( + metricData -> + assertThat(metricData) + .hasInstrumentationScope(EXPECTED_SCOPE) + .hasDescription("Number of executing platform threads.") + .hasUnit("{thread}") + .hasLongSumSatisfying( + sum -> + sum.isNotMonotonic() + .hasPointsSatisfying( + point -> + point + .hasValue(2) + .hasAttributesSatisfying( + equalTo(JvmAttributes.JVM_THREAD_DAEMON, true)), + point -> + point + .hasValue(5) + .hasAttributesSatisfying( + equalTo( + JvmAttributes.JVM_THREAD_DAEMON, + false)))))); + } + + @Test + @EnabledForJreRange(min = JRE.JAVA_9) + void registerObservers_Java9AndNewer() { + ThreadInfo threadInfo1 = + mock(ThreadInfo.class, new ThreadInfoAnswer(false, Thread.State.RUNNABLE)); + ThreadInfo threadInfo2 = + mock(ThreadInfo.class, new ThreadInfoAnswer(true, Thread.State.WAITING)); + + long[] threadIds = {12, 32, 42}; + when(threadBean.getAllThreadIds()).thenReturn(threadIds); + when(threadBean.getThreadInfo(threadIds)) + .thenReturn(new ThreadInfo[] {threadInfo1, null, threadInfo2}); + + Threads.INSTANCE + .registerObservers(testing.getOpenTelemetry(), threadBean) + .forEach(cleanup::deferCleanup); + + testing.waitAndAssertMetrics( + "io.opentelemetry.runtime-telemetry-java8", + "jvm.thread.count", + metrics -> + metrics.anySatisfy( + metricData -> + assertThat(metricData) + .hasInstrumentationScope(EXPECTED_SCOPE) + .hasDescription("Number of executing platform threads.") + .hasUnit("{thread}") + .hasLongSumSatisfying( + sum -> + sum.isNotMonotonic() + .hasPointsSatisfying( + point -> + point + .hasValue(1) + .hasAttributesSatisfying( + equalTo(JvmAttributes.JVM_THREAD_DAEMON, false), + equalTo( + JvmAttributes.JVM_THREAD_STATE, + "runnable")), + point -> + point + .hasValue(1) + .hasAttributesSatisfying( + equalTo(JvmAttributes.JVM_THREAD_DAEMON, true), + equalTo( + JvmAttributes.JVM_THREAD_STATE, + "waiting")))))); + } + + static final class ThreadInfoAnswer implements Answer { + + private final boolean isDaemon; + private final Thread.State state; + + ThreadInfoAnswer(boolean isDaemon, Thread.State state) { + this.isDaemon = isDaemon; + this.state = state; + } + + @Override + public Object answer(InvocationOnMock invocation) { + String methodName = invocation.getMethod().getName(); + if (methodName.equals("isDaemon")) { + return isDaemon; + } else if (methodName.equals("getThreadState")) { + return state; + } + return null; + } + } +} diff --git a/instrumentation/runtime-telemetry/runtime-telemetry-java8/library/src/test/java/io/opentelemetry/instrumentation/runtimemetrics/java8/ThreadsTest.java b/instrumentation/runtime-telemetry/runtime-telemetry-java8/library/src/test/java/io/opentelemetry/instrumentation/runtimemetrics/java8/ThreadsTest.java deleted file mode 100644 index 5b192b59ef4a..000000000000 --- a/instrumentation/runtime-telemetry/runtime-telemetry-java8/library/src/test/java/io/opentelemetry/instrumentation/runtimemetrics/java8/ThreadsTest.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.runtimemetrics.java8; - -import static io.opentelemetry.instrumentation.runtimemetrics.java8.ScopeUtil.EXPECTED_SCOPE; -import static io.opentelemetry.instrumentation.runtimemetrics.java8.Threads.DAEMON; -import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; -import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; -import static org.mockito.Mockito.when; - -import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; -import io.opentelemetry.instrumentation.testing.junit.LibraryInstrumentationExtension; -import java.lang.management.ThreadMXBean; -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; - -@ExtendWith(MockitoExtension.class) -class ThreadsTest { - - @RegisterExtension - static final InstrumentationExtension testing = LibraryInstrumentationExtension.create(); - - @Mock private ThreadMXBean threadBean; - - @Test - void registerObservers() { - when(threadBean.getThreadCount()).thenReturn(7); - when(threadBean.getDaemonThreadCount()).thenReturn(2); - - Threads.INSTANCE.registerObservers(testing.getOpenTelemetry(), threadBean); - - testing.waitAndAssertMetrics( - "io.opentelemetry.runtime-telemetry-java8", - "process.runtime.jvm.threads.count", - metrics -> - metrics.anySatisfy( - metricData -> - assertThat(metricData) - .hasInstrumentationScope(EXPECTED_SCOPE) - .hasDescription("Number of executing threads") - .hasUnit("{thread}") - .hasLongSumSatisfying( - sum -> - sum.isNotMonotonic() - .hasPointsSatisfying( - point -> - point - .hasValue(2) - .hasAttributesSatisfying(equalTo(DAEMON, true)), - point -> - point - .hasValue(5) - .hasAttributesSatisfying( - equalTo(DAEMON, false)))))); - } -} diff --git a/instrumentation/runtime-telemetry/runtime-telemetry-java8/library/src/test/java/io/opentelemetry/instrumentation/runtimemetrics/java8/BufferPoolsTest.java b/instrumentation/runtime-telemetry/runtime-telemetry-java8/library/src/test/java/io/opentelemetry/instrumentation/runtimemetrics/java8/internal/ExperimentalBufferPoolsTest.java similarity index 77% rename from instrumentation/runtime-telemetry/runtime-telemetry-java8/library/src/test/java/io/opentelemetry/instrumentation/runtimemetrics/java8/BufferPoolsTest.java rename to instrumentation/runtime-telemetry/runtime-telemetry-java8/library/src/test/java/io/opentelemetry/instrumentation/runtimemetrics/java8/internal/ExperimentalBufferPoolsTest.java index 060bac1c356b..346c784c6f8b 100644 --- a/instrumentation/runtime-telemetry/runtime-telemetry-java8/library/src/test/java/io/opentelemetry/instrumentation/runtimemetrics/java8/BufferPoolsTest.java +++ b/instrumentation/runtime-telemetry/runtime-telemetry-java8/library/src/test/java/io/opentelemetry/instrumentation/runtimemetrics/java8/internal/ExperimentalBufferPoolsTest.java @@ -3,23 +3,23 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.instrumentation.runtimemetrics.java8; +package io.opentelemetry.instrumentation.runtimemetrics.java8.internal; +import static io.opentelemetry.api.common.AttributeKey.stringKey; import static io.opentelemetry.instrumentation.runtimemetrics.java8.ScopeUtil.EXPECTED_SCOPE; import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; +import static java.util.Collections.singletonList; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.common.Attributes; import io.opentelemetry.api.metrics.ObservableLongMeasurement; import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; import io.opentelemetry.instrumentation.testing.junit.LibraryInstrumentationExtension; import java.lang.management.BufferPoolMXBean; -import java.util.Arrays; import java.util.List; import java.util.function.Consumer; import org.junit.jupiter.api.BeforeEach; @@ -31,7 +31,7 @@ import org.mockito.junit.jupiter.MockitoExtension; @ExtendWith(MockitoExtension.class) -public class BufferPoolsTest { +class ExperimentalBufferPoolsTest { @RegisterExtension static final InstrumentationExtension testing = LibraryInstrumentationExtension.create(); @@ -43,7 +43,7 @@ public class BufferPoolsTest { @BeforeEach void setup() { when(bufferPoolBean.getName()).thenReturn("buffer_pool_1"); - beans = Arrays.asList(bufferPoolBean); + beans = singletonList(bufferPoolBean); } @Test @@ -52,18 +52,17 @@ void registerObservers() { when(bufferPoolBean.getTotalCapacity()).thenReturn(11L); when(bufferPoolBean.getCount()).thenReturn(12L); - BufferPools.registerObservers(testing.getOpenTelemetry(), beans); + ExperimentalBufferPools.registerObservers(testing.getOpenTelemetry(), beans); testing.waitAndAssertMetrics( "io.opentelemetry.runtime-telemetry-java8", - "process.runtime.jvm.buffer.usage", + "jvm.buffer.memory.usage", metrics -> metrics.anySatisfy( metricData -> assertThat(metricData) .hasInstrumentationScope(EXPECTED_SCOPE) - .hasDescription( - "Memory that the Java virtual machine is using for this buffer pool") + .hasDescription("Measure of memory used by buffers.") .hasUnit("By") .hasLongSumSatisfying( sum -> @@ -72,17 +71,17 @@ void registerObservers() { point .hasValue(10) .hasAttribute( - AttributeKey.stringKey("pool"), + stringKey("jvm.buffer.pool.name"), "buffer_pool_1"))))); testing.waitAndAssertMetrics( "io.opentelemetry.runtime-telemetry-java8", - "process.runtime.jvm.buffer.limit", + "jvm.buffer.memory.limit", metrics -> metrics.anySatisfy( metricData -> assertThat(metricData) .hasInstrumentationScope(EXPECTED_SCOPE) - .hasDescription("Total capacity of the buffers in this pool") + .hasDescription("Measure of total memory capacity of buffers.") .hasUnit("By") .hasLongSumSatisfying( sum -> @@ -91,18 +90,18 @@ void registerObservers() { point .hasValue(11) .hasAttribute( - AttributeKey.stringKey("pool"), + stringKey("jvm.buffer.pool.name"), "buffer_pool_1"))))); testing.waitAndAssertMetrics( "io.opentelemetry.runtime-telemetry-java8", - "process.runtime.jvm.buffer.count", + "jvm.buffer.count", metrics -> metrics.anySatisfy( metricData -> assertThat(metricData) .hasInstrumentationScope(EXPECTED_SCOPE) - .hasDescription("The number of buffers in the pool") - .hasUnit("{buffers}") + .hasDescription("Number of buffers in the pool.") + .hasUnit("{buffer}") .hasLongSumSatisfying( sum -> sum.hasPointsSatisfying( @@ -110,7 +109,7 @@ void registerObservers() { point .hasValue(12) .hasAttribute( - AttributeKey.stringKey("pool"), + stringKey("jvm.buffer.pool.name"), "buffer_pool_1"))))); } @@ -118,16 +117,17 @@ void registerObservers() { void callback_Records() { when(bufferPoolBean.getMemoryUsed()).thenReturn(1L); Consumer callback = - BufferPools.callback(beans, BufferPoolMXBean::getMemoryUsed); + ExperimentalBufferPools.callback(beans, BufferPoolMXBean::getMemoryUsed); callback.accept(measurement); - verify(measurement).record(1, Attributes.builder().put("pool", "buffer_pool_1").build()); + verify(measurement) + .record(1, Attributes.builder().put("jvm.buffer.pool.name", "buffer_pool_1").build()); } @Test void callback_SkipRecord() { when(bufferPoolBean.getMemoryUsed()).thenReturn(-1L); Consumer callback = - BufferPools.callback(beans, BufferPoolMXBean::getMemoryUsed); + ExperimentalBufferPools.callback(beans, BufferPoolMXBean::getMemoryUsed); callback.accept(measurement); verify(measurement, never()).record(eq(-1), any()); } diff --git a/instrumentation/runtime-telemetry/runtime-telemetry-java8/library/src/test/java/io/opentelemetry/instrumentation/runtimemetrics/java8/CpuTest.java b/instrumentation/runtime-telemetry/runtime-telemetry-java8/library/src/test/java/io/opentelemetry/instrumentation/runtimemetrics/java8/internal/ExperimentalCpuTest.java similarity index 61% rename from instrumentation/runtime-telemetry/runtime-telemetry-java8/library/src/test/java/io/opentelemetry/instrumentation/runtimemetrics/java8/CpuTest.java rename to instrumentation/runtime-telemetry/runtime-telemetry-java8/library/src/test/java/io/opentelemetry/instrumentation/runtimemetrics/java8/internal/ExperimentalCpuTest.java index 38f53768c0fb..be537e020cdb 100644 --- a/instrumentation/runtime-telemetry/runtime-telemetry-java8/library/src/test/java/io/opentelemetry/instrumentation/runtimemetrics/java8/CpuTest.java +++ b/instrumentation/runtime-telemetry/runtime-telemetry-java8/library/src/test/java/io/opentelemetry/instrumentation/runtimemetrics/java8/internal/ExperimentalCpuTest.java @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.instrumentation.runtimemetrics.java8; +package io.opentelemetry.instrumentation.runtimemetrics.java8.internal; import static io.opentelemetry.instrumentation.runtimemetrics.java8.ScopeUtil.EXPECTED_SCOPE; import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; @@ -20,7 +20,7 @@ import org.mockito.junit.jupiter.MockitoExtension; @ExtendWith(MockitoExtension.class) -class CpuTest { +class ExperimentalCpuTest { @RegisterExtension static final InstrumentationExtension testing = LibraryInstrumentationExtension.create(); @@ -30,47 +30,35 @@ class CpuTest { @Test void registerObservers() { when(osBean.getSystemLoadAverage()).thenReturn(2.2); - Supplier systemCpuUsage = () -> 0.11; - Supplier processCpuUsage = () -> 0.05; + Supplier systemCpuUtilization = () -> 0.11; - Cpu.INSTANCE.registerObservers( - testing.getOpenTelemetry(), osBean, systemCpuUsage, processCpuUsage); + ExperimentalCpu.registerObservers(testing.getOpenTelemetry(), osBean, systemCpuUtilization); testing.waitAndAssertMetrics( "io.opentelemetry.runtime-telemetry-java8", - "process.runtime.jvm.system.cpu.load_1m", + "jvm.system.cpu.load_1m", metrics -> metrics.anySatisfy( metricData -> assertThat(metricData) .hasInstrumentationScope(EXPECTED_SCOPE) - .hasDescription("Average CPU load of the whole system for the last minute") - .hasUnit("1") + .hasDescription( + "Average CPU load of the whole system for the last minute as reported by the JVM.") + .hasUnit("{run_queue_item}") .hasDoubleGaugeSatisfying( gauge -> gauge.hasPointsSatisfying(point -> point.hasValue(2.2))))); testing.waitAndAssertMetrics( "io.opentelemetry.runtime-telemetry-java8", - "process.runtime.jvm.system.cpu.utilization", + "jvm.system.cpu.utilization", metrics -> metrics.anySatisfy( metricData -> assertThat(metricData) .hasInstrumentationScope(EXPECTED_SCOPE) - .hasDescription("Recent cpu utilization for the whole system") + .hasDescription( + "Recent CPU utilization for the whole system as reported by the JVM.") .hasUnit("1") .hasDoubleGaugeSatisfying( gauge -> gauge.hasPointsSatisfying(point -> point.hasValue(0.11))))); - testing.waitAndAssertMetrics( - "io.opentelemetry.runtime-telemetry-java8", - "process.runtime.jvm.cpu.utilization", - metrics -> - metrics.anySatisfy( - metricData -> - assertThat(metricData) - .hasInstrumentationScope(EXPECTED_SCOPE) - .hasDescription("Recent cpu utilization for the process") - .hasUnit("1") - .hasDoubleGaugeSatisfying( - gauge -> gauge.hasPointsSatisfying(point -> point.hasValue(0.05))))); } } diff --git a/instrumentation/runtime-telemetry/runtime-telemetry-java8/library/src/test/java/io/opentelemetry/instrumentation/runtimemetrics/java8/internal/ExperimentalMemoryPoolsTest.java b/instrumentation/runtime-telemetry/runtime-telemetry-java8/library/src/test/java/io/opentelemetry/instrumentation/runtimemetrics/java8/internal/ExperimentalMemoryPoolsTest.java new file mode 100644 index 000000000000..80e3990d8973 --- /dev/null +++ b/instrumentation/runtime-telemetry/runtime-telemetry-java8/library/src/test/java/io/opentelemetry/instrumentation/runtimemetrics/java8/internal/ExperimentalMemoryPoolsTest.java @@ -0,0 +1,88 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.runtimemetrics.java8.internal; + +import static io.opentelemetry.api.common.AttributeKey.stringKey; +import static io.opentelemetry.instrumentation.runtimemetrics.java8.ScopeUtil.EXPECTED_SCOPE; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; +import static org.mockito.Mockito.when; + +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.LibraryInstrumentationExtension; +import java.lang.management.MemoryPoolMXBean; +import java.lang.management.MemoryType; +import java.lang.management.MemoryUsage; +import java.util.Arrays; +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.junit.jupiter.api.extension.RegisterExtension; +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 ExperimentalMemoryPoolsTest { + + @RegisterExtension + static final InstrumentationExtension testing = LibraryInstrumentationExtension.create(); + + @Mock private MemoryPoolMXBean heapPoolBean; + @Mock private MemoryPoolMXBean nonHeapPoolBean; + + @Mock private MemoryUsage heapPoolUsage; + @Mock private MemoryUsage nonHeapUsage; + + private List beans; + + @BeforeEach + void setup() { + when(heapPoolBean.getName()).thenReturn("heap_pool"); + when(heapPoolBean.getType()).thenReturn(MemoryType.HEAP); + when(heapPoolBean.getUsage()).thenReturn(heapPoolUsage); + when(nonHeapPoolBean.getName()).thenReturn("non_heap_pool"); + when(nonHeapPoolBean.getType()).thenReturn(MemoryType.NON_HEAP); + when(nonHeapPoolBean.getUsage()).thenReturn(nonHeapUsage); + beans = Arrays.asList(heapPoolBean, nonHeapPoolBean); + } + + @Test + void registerObservers() { + when(heapPoolUsage.getInit()).thenReturn(11L); + when(nonHeapUsage.getInit()).thenReturn(15L); + ExperimentalMemoryPools.registerObservers(testing.getOpenTelemetry(), beans); + + testing.waitAndAssertMetrics( + "io.opentelemetry.runtime-telemetry-java8", + "jvm.memory.init", + metrics -> + metrics.anySatisfy( + metricData -> + assertThat(metricData) + .hasInstrumentationScope(EXPECTED_SCOPE) + .hasDescription("Measure of initial memory requested.") + .hasUnit("By") + .hasLongSumSatisfying( + sum -> + sum.hasPointsSatisfying( + point -> + point + .hasValue(11) + .hasAttribute( + stringKey("jvm.memory.pool.name"), "heap_pool") + .hasAttribute(stringKey("jvm.memory.type"), "heap"), + point -> + point + .hasValue(15) + .hasAttribute( + stringKey("jvm.memory.pool.name"), "non_heap_pool") + .hasAttribute( + stringKey("jvm.memory.type"), "non_heap"))))); + } +} diff --git a/instrumentation/runtime-telemetry/runtime-telemetry-java8/testing/build.gradle.kts b/instrumentation/runtime-telemetry/runtime-telemetry-java8/testing/build.gradle.kts new file mode 100644 index 000000000000..dc808ed744aa --- /dev/null +++ b/instrumentation/runtime-telemetry/runtime-telemetry-java8/testing/build.gradle.kts @@ -0,0 +1,42 @@ +plugins { + id("otel.java-conventions") + + war +} + +dependencies { + testImplementation(project(":instrumentation:runtime-telemetry:runtime-telemetry-java8:javaagent")) + + testImplementation("io.opentelemetry:opentelemetry-sdk-testing") + + // Bring in various archives to test introspection logic + testImplementation("io.opentelemetry:opentelemetry-api") + testImplementation("io.opentelemetry:opentelemetry-api-incubator") + testImplementation("org.springframework:spring-webmvc:3.1.0.RELEASE") + testImplementation("com.google.guava:guava") +} + +tasks.war { + archiveFileName.set("app.war") + manifest { + attributes( + "Implementation-Title" to "Dummy App", + "Implementation-Vendor" to "OpenTelemetry", + ) + } +} + +tasks.named("test") { + dependsOn(tasks.getByName("war")) +} + +tasks { + withType().configureEach { + environment( + mapOf( + // Expose dummy app war location to test + "DUMMY_APP_WAR" to "${layout.buildDirectory.asFile.get()}/libs/app.war" + ) + ) + } +} diff --git a/instrumentation/redisson/redisson-3.0/javaagent/src/test/groovy/RedissonAsyncClientTest.groovy b/instrumentation/runtime-telemetry/runtime-telemetry-java8/testing/src/main/java/io/opentelemetry/instrumentation/testapp/DummyApplication.java similarity index 50% rename from instrumentation/redisson/redisson-3.0/javaagent/src/test/groovy/RedissonAsyncClientTest.groovy rename to instrumentation/runtime-telemetry/runtime-telemetry-java8/testing/src/main/java/io/opentelemetry/instrumentation/testapp/DummyApplication.java index 6d4b8c115f87..c75d1f402c20 100644 --- a/instrumentation/redisson/redisson-3.0/javaagent/src/test/groovy/RedissonAsyncClientTest.groovy +++ b/instrumentation/runtime-telemetry/runtime-telemetry-java8/testing/src/main/java/io/opentelemetry/instrumentation/testapp/DummyApplication.java @@ -3,5 +3,6 @@ * SPDX-License-Identifier: Apache-2.0 */ -class RedissonAsyncClientTest extends AbstractRedissonAsyncClientTest { -} +package io.opentelemetry.instrumentation.testapp; + +public class DummyApplication {} diff --git a/instrumentation/runtime-telemetry/runtime-telemetry-java8/testing/src/test/java/io/opentelemetry/instrumentation/javaagent/runtimemetrics/java8/JarAnalyzerTest.java b/instrumentation/runtime-telemetry/runtime-telemetry-java8/testing/src/test/java/io/opentelemetry/instrumentation/javaagent/runtimemetrics/java8/JarAnalyzerTest.java new file mode 100644 index 000000000000..4ce7cbea0a69 --- /dev/null +++ b/instrumentation/runtime-telemetry/runtime-telemetry-java8/testing/src/test/java/io/opentelemetry/instrumentation/javaagent/runtimemetrics/java8/JarAnalyzerTest.java @@ -0,0 +1,158 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.javaagent.runtimemetrics.java8; + +import static io.opentelemetry.instrumentation.javaagent.runtimemetrics.java8.JarAnalyzer.PACKAGE_CHECKSUM; +import static io.opentelemetry.instrumentation.javaagent.runtimemetrics.java8.JarAnalyzer.PACKAGE_CHECKSUM_ALGORITHM; +import static io.opentelemetry.instrumentation.javaagent.runtimemetrics.java8.JarAnalyzer.PACKAGE_DESCRIPTION; +import static io.opentelemetry.instrumentation.javaagent.runtimemetrics.java8.JarAnalyzer.PACKAGE_NAME; +import static io.opentelemetry.instrumentation.javaagent.runtimemetrics.java8.JarAnalyzer.PACKAGE_PATH; +import static io.opentelemetry.instrumentation.javaagent.runtimemetrics.java8.JarAnalyzer.PACKAGE_TYPE; +import static io.opentelemetry.instrumentation.javaagent.runtimemetrics.java8.JarAnalyzer.PACKAGE_VERSION; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.google.common.collect.ImmutableMap; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.incubator.events.EventBuilder; +import io.opentelemetry.api.incubator.events.EventLogger; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.sdk.testing.assertj.AttributesAssert; +import java.io.File; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.function.Consumer; +import java.util.stream.Stream; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.ArgumentCaptor; +import org.springframework.http.HttpRequest; + +class JarAnalyzerTest { + + @ParameterizedTest + @MethodSource("processUrlArguments") + void processUrl_EmitsEvents(URL archiveUrl, Consumer attributesConsumer) { + EventLogger eventLogger = mock(EventLogger.class); + EventBuilder builder = mock(EventBuilder.class); + when(eventLogger.builder(eq("package.info"))).thenReturn(builder); + when(builder.setAttributes(any())).thenReturn(builder); + + JarAnalyzer.processUrl(eventLogger, archiveUrl); + + ArgumentCaptor attributesArgumentCaptor = ArgumentCaptor.forClass(Attributes.class); + verify(builder).setAttributes(attributesArgumentCaptor.capture()); + + attributesConsumer.accept(assertThat(attributesArgumentCaptor.getValue())); + } + + private static Stream processUrlArguments() { + return Stream.of( + // instrumentation code + Arguments.of( + archiveUrl(JarAnalyzer.class), + assertAttributes( + attributes -> + attributes + .containsEntry(PACKAGE_TYPE, "jar") + .hasEntrySatisfying( + PACKAGE_PATH, + path -> + assertThat( + path.matches( + "opentelemetry-javaagent-runtime-telemetry-java8-[0-9a-zA-Z-\\.]+\\.jar")) + .isTrue()) + .containsEntry(PACKAGE_DESCRIPTION, "javaagent by OpenTelemetry") + .containsEntry(PACKAGE_CHECKSUM_ALGORITHM, "SHA1") + .hasEntrySatisfying( + PACKAGE_CHECKSUM, checksum -> assertThat(checksum).isNotEmpty()))), + // dummy war + Arguments.of( + archiveUrl(new File(System.getenv("DUMMY_APP_WAR"))), + assertAttributes( + attributes -> + attributes + .containsEntry(PACKAGE_TYPE, "war") + .containsEntry(PACKAGE_PATH, "app.war") + .containsEntry(PACKAGE_DESCRIPTION, "Dummy App by OpenTelemetry") + .containsEntry(PACKAGE_CHECKSUM_ALGORITHM, "SHA1") + .hasEntrySatisfying( + PACKAGE_CHECKSUM, checksum -> assertThat(checksum).isNotEmpty()))), + // io.opentelemetry:opentelemetry-api + Arguments.of( + archiveUrl(Tracer.class), + assertAttributes( + attributes -> + attributes + .containsEntry(PACKAGE_TYPE, "jar") + .hasEntrySatisfying( + PACKAGE_PATH, + path -> + assertThat(path.matches("opentelemetry-api-[0-9a-zA-Z-\\.]+\\.jar")) + .isTrue()) + .containsEntry(PACKAGE_DESCRIPTION, "all") + .containsEntry(PACKAGE_CHECKSUM_ALGORITHM, "SHA1") + .hasEntrySatisfying( + PACKAGE_CHECKSUM, checksum -> assertThat(checksum).isNotEmpty()))), + // org.springframework:spring-webmvc + Arguments.of( + archiveUrl(HttpRequest.class), + assertAttributes( + attributes -> + attributes + .containsEntry(PACKAGE_TYPE, "jar") + // TODO(jack-berg): can we extract version out of path to populate + // package.version field? + .hasEntrySatisfying( + PACKAGE_PATH, + path -> + assertThat(path.matches("spring-web-[0-9a-zA-Z-\\.]+\\.jar")) + .isTrue()) + .containsEntry(PACKAGE_DESCRIPTION, "org.springframework.web") + .containsEntry(PACKAGE_CHECKSUM_ALGORITHM, "SHA1") + .hasEntrySatisfying( + PACKAGE_CHECKSUM, checksum -> assertThat(checksum).isNotEmpty()))), + // com.google.guava:guava + Arguments.of( + archiveUrl(ImmutableMap.class), + assertAttributes( + attributes -> + attributes + .containsEntry(PACKAGE_TYPE, "jar") + .hasEntrySatisfying( + PACKAGE_PATH, + path -> + assertThat(path.matches("guava-[0-9a-zA-Z-\\.]+\\.jar")).isTrue()) + .containsEntry(PACKAGE_NAME, "com.google.guava:guava") + .hasEntrySatisfying( + PACKAGE_VERSION, version -> assertThat(version).isNotEmpty()) + .containsEntry(PACKAGE_CHECKSUM_ALGORITHM, "SHA1") + .hasEntrySatisfying( + PACKAGE_CHECKSUM, checksum -> assertThat(checksum).isNotEmpty())))); + } + + private static URL archiveUrl(File file) { + try { + return new URL("file://" + file.getAbsolutePath()); + } catch (MalformedURLException e) { + throw new IllegalArgumentException("Error creating URL for file", e); + } + } + + private static URL archiveUrl(Class clazz) { + return clazz.getProtectionDomain().getCodeSource().getLocation(); + } + + private static Consumer assertAttributes( + Consumer attributesAssert) { + return attributesAssert; + } +} diff --git a/instrumentation/rxjava/README.md b/instrumentation/rxjava/README.md index 096ffbf706f9..cafdfa41b65c 100644 --- a/instrumentation/rxjava/README.md +++ b/instrumentation/rxjava/README.md @@ -1,5 +1,5 @@ # Settings for the RxJava instrumentation -| System property | Type | Default | Description | -|---|---|---|---| +| System property | Type | Default | Description | +| ---------------------------------------------------------- | ------- | ------- | -------------------------------------------------------------------------------------- | | `otel.instrumentation.rxjava.experimental-span-attributes` | Boolean | `false` | Enable the capture of experimental span attributes for RxJava 2 and 3 instrumentation. | diff --git a/instrumentation/rxjava/rxjava-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rxjava/v2_0/TracingAssemblyActivation.java b/instrumentation/rxjava/rxjava-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rxjava/v2_0/TracingAssemblyActivation.java index bd1a4c1a6e6e..91f3b108b71a 100644 --- a/instrumentation/rxjava/rxjava-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rxjava/v2_0/TracingAssemblyActivation.java +++ b/instrumentation/rxjava/rxjava-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rxjava/v2_0/TracingAssemblyActivation.java @@ -6,7 +6,7 @@ package io.opentelemetry.javaagent.instrumentation.rxjava.v2_0; import io.opentelemetry.instrumentation.rxjava.v2_0.TracingAssembly; -import io.opentelemetry.javaagent.bootstrap.internal.InstrumentationConfig; +import io.opentelemetry.javaagent.bootstrap.internal.AgentInstrumentationConfig; import java.util.concurrent.atomic.AtomicBoolean; public final class TracingAssemblyActivation { @@ -23,7 +23,7 @@ public static void activate(Class clz) { if (activated.get(clz).compareAndSet(false, true)) { TracingAssembly.builder() .setCaptureExperimentalSpanAttributes( - InstrumentationConfig.get() + AgentInstrumentationConfig.get() .getBoolean("otel.instrumentation.rxjava.experimental-span-attributes", false)) .build() .enable(); diff --git a/instrumentation/rxjava/rxjava-2.0/javaagent/src/test/java/BaseRxJava2WithSpanTest.java b/instrumentation/rxjava/rxjava-2.0/javaagent/src/test/java/BaseRxJava2WithSpanTest.java index 9db0fb4e7601..b57ea2532c60 100644 --- a/instrumentation/rxjava/rxjava-2.0/javaagent/src/test/java/BaseRxJava2WithSpanTest.java +++ b/instrumentation/rxjava/rxjava-2.0/javaagent/src/test/java/BaseRxJava2WithSpanTest.java @@ -5,8 +5,8 @@ import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.satisfies; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.CODE_FUNCTION; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.CODE_NAMESPACE; +import static io.opentelemetry.semconv.incubating.CodeIncubatingAttributes.CODE_FUNCTION; +import static io.opentelemetry.semconv.incubating.CodeIncubatingAttributes.CODE_NAMESPACE; import static org.assertj.core.api.Assertions.assertThat; import io.opentelemetry.api.common.AttributeKey; diff --git a/instrumentation/rxjava/rxjava-2.0/library/src/test/java/RxJava2SubscriptionTest.java b/instrumentation/rxjava/rxjava-2.0/library/src/test/java/RxJava2SubscriptionTest.java index d757feee1da7..1b806e7c6f1c 100644 --- a/instrumentation/rxjava/rxjava-2.0/library/src/test/java/RxJava2SubscriptionTest.java +++ b/instrumentation/rxjava/rxjava-2.0/library/src/test/java/RxJava2SubscriptionTest.java @@ -22,7 +22,7 @@ protected InstrumentationExtension testing() { } @BeforeAll - public static void setupSpec() { + public static void setup() { tracingAssembly.enable(); } } diff --git a/instrumentation/rxjava/rxjava-2.0/library/src/test/java/RxJava2Test.java b/instrumentation/rxjava/rxjava-2.0/library/src/test/java/RxJava2Test.java index f185365f1753..0e1039a878aa 100644 --- a/instrumentation/rxjava/rxjava-2.0/library/src/test/java/RxJava2Test.java +++ b/instrumentation/rxjava/rxjava-2.0/library/src/test/java/RxJava2Test.java @@ -22,7 +22,7 @@ protected InstrumentationExtension testing() { } @BeforeAll - public void setupSpec() { + public void setup() { tracingAssembly.enable(); } } diff --git a/instrumentation/rxjava/rxjava-3-common/testing/src/main/java/io/opentelemetry/instrumentation/rxjava/v3/common/AbstractRxJava3WithSpanTest.java b/instrumentation/rxjava/rxjava-3-common/testing/src/main/java/io/opentelemetry/instrumentation/rxjava/v3/common/AbstractRxJava3WithSpanTest.java index 1c0d50bd2dab..1f06824c8fea 100644 --- a/instrumentation/rxjava/rxjava-3-common/testing/src/main/java/io/opentelemetry/instrumentation/rxjava/v3/common/AbstractRxJava3WithSpanTest.java +++ b/instrumentation/rxjava/rxjava-3-common/testing/src/main/java/io/opentelemetry/instrumentation/rxjava/v3/common/AbstractRxJava3WithSpanTest.java @@ -7,8 +7,8 @@ import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.satisfies; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.CODE_FUNCTION; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.CODE_NAMESPACE; +import static io.opentelemetry.semconv.incubating.CodeIncubatingAttributes.CODE_FUNCTION; +import static io.opentelemetry.semconv.incubating.CodeIncubatingAttributes.CODE_NAMESPACE; import static org.assertj.core.api.Assertions.assertThat; import io.opentelemetry.api.common.AttributeKey; diff --git a/instrumentation/rxjava/rxjava-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rxjava/v3_0/TracingAssemblyActivation.java b/instrumentation/rxjava/rxjava-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rxjava/v3_0/TracingAssemblyActivation.java index 69efa44cd3d1..2dd5a0696be7 100644 --- a/instrumentation/rxjava/rxjava-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rxjava/v3_0/TracingAssemblyActivation.java +++ b/instrumentation/rxjava/rxjava-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rxjava/v3_0/TracingAssemblyActivation.java @@ -6,7 +6,7 @@ package io.opentelemetry.javaagent.instrumentation.rxjava.v3_0; import io.opentelemetry.instrumentation.rxjava.v3_0.TracingAssembly; -import io.opentelemetry.javaagent.bootstrap.internal.InstrumentationConfig; +import io.opentelemetry.javaagent.bootstrap.internal.AgentInstrumentationConfig; import java.util.concurrent.atomic.AtomicBoolean; public final class TracingAssemblyActivation { @@ -23,7 +23,7 @@ public static void activate(Class clz) { if (activated.get(clz).compareAndSet(false, true)) { TracingAssembly.builder() .setCaptureExperimentalSpanAttributes( - InstrumentationConfig.get() + AgentInstrumentationConfig.get() .getBoolean("otel.instrumentation.rxjava.experimental-span-attributes", false)) .build() .enable(); diff --git a/instrumentation/rxjava/rxjava-3.0/library/src/test/java/RxJava3SubscriptionTest.java b/instrumentation/rxjava/rxjava-3.0/library/src/test/java/RxJava3SubscriptionTest.java index 7e2479a7aa60..282a731c61a3 100644 --- a/instrumentation/rxjava/rxjava-3.0/library/src/test/java/RxJava3SubscriptionTest.java +++ b/instrumentation/rxjava/rxjava-3.0/library/src/test/java/RxJava3SubscriptionTest.java @@ -23,7 +23,7 @@ protected InstrumentationExtension testing() { static TracingAssembly tracingAssembly = TracingAssembly.create(); @BeforeAll - public static void setupSpec() { + public static void setup() { tracingAssembly.enable(); } } diff --git a/instrumentation/rxjava/rxjava-3.0/library/src/test/java/RxJava3Test.java b/instrumentation/rxjava/rxjava-3.0/library/src/test/java/RxJava3Test.java index 68cd95fb1f66..540a8b8ca537 100644 --- a/instrumentation/rxjava/rxjava-3.0/library/src/test/java/RxJava3Test.java +++ b/instrumentation/rxjava/rxjava-3.0/library/src/test/java/RxJava3Test.java @@ -22,7 +22,7 @@ protected InstrumentationExtension testing() { } @BeforeAll - public void setupSpec() { + public void setup() { tracingAssembly.enable(); } } diff --git a/instrumentation/rxjava/rxjava-3.1.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rxjava/v3_1_1/TracingAssemblyActivation.java b/instrumentation/rxjava/rxjava-3.1.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rxjava/v3_1_1/TracingAssemblyActivation.java index 79caeb74001d..ca5126be9338 100644 --- a/instrumentation/rxjava/rxjava-3.1.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rxjava/v3_1_1/TracingAssemblyActivation.java +++ b/instrumentation/rxjava/rxjava-3.1.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rxjava/v3_1_1/TracingAssemblyActivation.java @@ -6,7 +6,7 @@ package io.opentelemetry.javaagent.instrumentation.rxjava.v3_1_1; import io.opentelemetry.instrumentation.rxjava.v3_1_1.TracingAssembly; -import io.opentelemetry.javaagent.bootstrap.internal.InstrumentationConfig; +import io.opentelemetry.javaagent.bootstrap.internal.AgentInstrumentationConfig; import java.util.concurrent.atomic.AtomicBoolean; public final class TracingAssemblyActivation { @@ -23,7 +23,7 @@ public static void activate(Class clz) { if (activated.get(clz).compareAndSet(false, true)) { TracingAssembly.builder() .setCaptureExperimentalSpanAttributes( - InstrumentationConfig.get() + AgentInstrumentationConfig.get() .getBoolean("otel.instrumentation.rxjava.experimental-span-attributes", false)) .build() .enable(); diff --git a/instrumentation/rxjava/rxjava-3.1.1/library/src/test/java/RxJava3SubscriptionTest.java b/instrumentation/rxjava/rxjava-3.1.1/library/src/test/java/RxJava3SubscriptionTest.java index 43f6e3988d4a..02c219ae1e66 100644 --- a/instrumentation/rxjava/rxjava-3.1.1/library/src/test/java/RxJava3SubscriptionTest.java +++ b/instrumentation/rxjava/rxjava-3.1.1/library/src/test/java/RxJava3SubscriptionTest.java @@ -23,7 +23,7 @@ protected InstrumentationExtension testing() { static TracingAssembly tracingAssembly = TracingAssembly.create(); @BeforeAll - public static void setupSpec() { + public static void setup() { tracingAssembly.enable(); } } diff --git a/instrumentation/rxjava/rxjava-3.1.1/library/src/test/java/RxJava3Test.java b/instrumentation/rxjava/rxjava-3.1.1/library/src/test/java/RxJava3Test.java index efb2f9f960a1..2f0d54a1b7da 100644 --- a/instrumentation/rxjava/rxjava-3.1.1/library/src/test/java/RxJava3Test.java +++ b/instrumentation/rxjava/rxjava-3.1.1/library/src/test/java/RxJava3Test.java @@ -22,7 +22,7 @@ protected InstrumentationExtension testing() { } @BeforeAll - public void setupSpec() { + public void setup() { tracingAssembly.enable(); } } diff --git a/instrumentation/scala-fork-join-2.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/scalaexecutors/ScalaForkJoinPoolInstrumentation.java b/instrumentation/scala-fork-join-2.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/scalaexecutors/ScalaForkJoinPoolInstrumentation.java index 6be8e8114571..04ced8953b46 100644 --- a/instrumentation/scala-fork-join-2.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/scalaexecutors/ScalaForkJoinPoolInstrumentation.java +++ b/instrumentation/scala-fork-join-2.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/scalaexecutors/ScalaForkJoinPoolInstrumentation.java @@ -41,8 +41,7 @@ public void transform(TypeTransformer transformer) { public static class SetScalaForkJoinStateAdvice { @Advice.OnMethodEnter(suppress = Throwable.class) - public static PropagatedContext enterJobSubmit( - @Advice.Argument(value = 0, readOnly = false) ForkJoinTask task) { + public static PropagatedContext enterJobSubmit(@Advice.Argument(0) ForkJoinTask task) { Context context = Java8BytecodeBridge.currentContext(); if (ExecutorAdviceHelper.shouldPropagateContext(context, task)) { VirtualField, PropagatedContext> virtualField = diff --git a/instrumentation/servlet/README.md b/instrumentation/servlet/README.md index c9c0ba87cd5e..511eb4b5cea5 100644 --- a/instrumentation/servlet/README.md +++ b/instrumentation/servlet/README.md @@ -2,10 +2,10 @@ ## Settings -| System property | Type | Default | Description | -|---|---|---|---| -| `otel.instrumentation.servlet.experimental-span-attributes` | Boolean | `false` | Enable the capture of experimental span attributes. | -| `otel.instrumentation.servlet.experimental.capture-request-parameters` | List | Empty | Request parameters to be captured (experimental). | +| System property | Type | Default | Description | +|------------------------------------------------------------------------| ------- | ------- |-----------------------------------------------------| +| `otel.instrumentation.servlet.experimental-span-attributes` | Boolean | `false` | Enable the capture of experimental span attributes. | +| `otel.instrumentation.servlet.experimental.capture-request-parameters` | List | Empty | Request parameters to be captured (experimental). | ### A word about version @@ -79,7 +79,7 @@ In order to alleviate this problem, instrumentations for specific frameworks, su _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/main/specification/trace/semantic_conventions/http.md). +[semantic conventions](https://github.com/open-telemetry/semantic-conventions/blob/main/docs/http/http-spans.md#http-server). ### Additional instrumentations diff --git a/instrumentation/servlet/servlet-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/v2_2/Servlet2HttpServletResponseInstrumentation.java b/instrumentation/servlet/servlet-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/v2_2/Servlet2HttpServletResponseInstrumentation.java index 701f2ed33132..478fbd1c2d61 100644 --- a/instrumentation/servlet/servlet-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/v2_2/Servlet2HttpServletResponseInstrumentation.java +++ b/instrumentation/servlet/servlet-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/v2_2/Servlet2HttpServletResponseInstrumentation.java @@ -27,8 +27,8 @@ /** * 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. + * io.opentelemetry.semconv.HttpAttributes#HTTP_RESPONSE_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, diff --git a/instrumentation/servlet/servlet-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/v2_2/Servlet2ResponseSendAdvice.java b/instrumentation/servlet/servlet-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/v2_2/Servlet2ResponseSendAdvice.java index b857b7d61567..b40c45ae7fac 100644 --- a/instrumentation/servlet/servlet-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/v2_2/Servlet2ResponseSendAdvice.java +++ b/instrumentation/servlet/servlet-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/v2_2/Servlet2ResponseSendAdvice.java @@ -9,7 +9,7 @@ import io.opentelemetry.context.Context; import io.opentelemetry.context.Scope; -import io.opentelemetry.instrumentation.api.instrumenter.util.ClassAndMethod; +import io.opentelemetry.instrumentation.api.incubator.semconv.util.ClassAndMethod; import io.opentelemetry.javaagent.bootstrap.CallDepth; import io.opentelemetry.javaagent.instrumentation.servlet.common.response.HttpServletResponseAdviceHelper; import javax.servlet.http.HttpServletResponse; diff --git a/instrumentation/servlet/servlet-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/v2_2/Servlet2Singletons.java b/instrumentation/servlet/servlet-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/v2_2/Servlet2Singletons.java index 447b99b4a5f5..ab34b03cd7b8 100644 --- a/instrumentation/servlet/servlet-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/v2_2/Servlet2Singletons.java +++ b/instrumentation/servlet/servlet-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/v2_2/Servlet2Singletons.java @@ -5,9 +5,9 @@ package io.opentelemetry.javaagent.instrumentation.servlet.v2_2; +import io.opentelemetry.instrumentation.api.incubator.semconv.util.ClassAndMethod; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.util.ClassAndMethod; import io.opentelemetry.javaagent.instrumentation.servlet.ServletInstrumenterBuilder; import io.opentelemetry.javaagent.instrumentation.servlet.ServletRequestContext; import io.opentelemetry.javaagent.instrumentation.servlet.ServletResponseContext; diff --git a/instrumentation/servlet/servlet-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/v2_2/Servlet2SpanNameExtractor.java b/instrumentation/servlet/servlet-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/v2_2/Servlet2SpanNameExtractor.java index 751a4629ee58..7091be708516 100644 --- a/instrumentation/servlet/servlet-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/v2_2/Servlet2SpanNameExtractor.java +++ b/instrumentation/servlet/servlet-2.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/v2_2/Servlet2SpanNameExtractor.java @@ -6,12 +6,16 @@ package io.opentelemetry.javaagent.instrumentation.servlet.v2_2; import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor; +import io.opentelemetry.javaagent.bootstrap.internal.AgentCommonConfig; import io.opentelemetry.javaagent.instrumentation.servlet.ServletAccessor; import io.opentelemetry.javaagent.instrumentation.servlet.ServletRequestContext; +import java.util.Set; public class Servlet2SpanNameExtractor implements SpanNameExtractor> { + private final ServletAccessor accessor; + private final Set knownMethods = AgentCommonConfig.get().getKnownHttpRequestMethods(); public Servlet2SpanNameExtractor(ServletAccessor accessor) { this.accessor = accessor; @@ -22,16 +26,19 @@ public String extract(ServletRequestContext requestContext) { REQUEST request = requestContext.request(); String method = accessor.getRequestMethod(request); String servletPath = accessor.getRequestServletPath(request); - if (method != null) { - if (servletPath.isEmpty()) { - return method; - } - String contextPath = accessor.getRequestContextPath(request); - if (contextPath == null || contextPath.isEmpty() || contextPath.equals("/")) { - return method + " " + servletPath; - } - return method + " " + contextPath + servletPath; + if (method == null) { + return "HTTP"; + } + if (!knownMethods.contains(method)) { + method = "HTTP"; + } + if (servletPath == null || servletPath.isEmpty()) { + return method; + } + String contextPath = accessor.getRequestContextPath(request); + if (contextPath == null || contextPath.isEmpty() || contextPath.equals("/")) { + return method + " " + servletPath; } - return "HTTP request"; + return method + " " + contextPath + servletPath; } } diff --git a/instrumentation/servlet/servlet-2.2/javaagent/src/test/groovy/HttpServletResponseTest.groovy b/instrumentation/servlet/servlet-2.2/javaagent/src/test/groovy/HttpServletResponseTest.groovy index cc22e9390a79..82e2fef4e27c 100644 --- a/instrumentation/servlet/servlet-2.2/javaagent/src/test/groovy/HttpServletResponseTest.groovy +++ b/instrumentation/servlet/servlet-2.2/javaagent/src/test/groovy/HttpServletResponseTest.groovy @@ -5,7 +5,7 @@ import io.opentelemetry.api.trace.SpanKind import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import io.opentelemetry.semconv.incubating.CodeIncubatingAttributes import javax.servlet.http.HttpServlet import spock.lang.Subject @@ -68,24 +68,24 @@ class HttpServletResponseTest extends AgentInstrumentationSpecification { name "TestResponse.sendError" childOf span(0) attributes { - "$SemanticAttributes.CODE_NAMESPACE" TestResponse.name - "$SemanticAttributes.CODE_FUNCTION" "sendError" + "$CodeIncubatingAttributes.CODE_NAMESPACE" TestResponse.name + "$CodeIncubatingAttributes.CODE_FUNCTION" "sendError" } } span(2) { name "TestResponse.sendError" childOf span(0) attributes { - "$SemanticAttributes.CODE_NAMESPACE" TestResponse.name - "$SemanticAttributes.CODE_FUNCTION" "sendError" + "$CodeIncubatingAttributes.CODE_NAMESPACE" TestResponse.name + "$CodeIncubatingAttributes.CODE_FUNCTION" "sendError" } } span(3) { name "TestResponse.sendRedirect" childOf span(0) attributes { - "$SemanticAttributes.CODE_NAMESPACE" TestResponse.name - "$SemanticAttributes.CODE_FUNCTION" "sendRedirect" + "$CodeIncubatingAttributes.CODE_NAMESPACE" TestResponse.name + "$CodeIncubatingAttributes.CODE_FUNCTION" "sendRedirect" } } } diff --git a/instrumentation/servlet/servlet-2.2/javaagent/src/test/groovy/JettyServlet2Test.groovy b/instrumentation/servlet/servlet-2.2/javaagent/src/test/groovy/JettyServlet2Test.groovy index bf3517b8d21b..ea904600b175 100644 --- a/instrumentation/servlet/servlet-2.2/javaagent/src/test/groovy/JettyServlet2Test.groovy +++ b/instrumentation/servlet/servlet-2.2/javaagent/src/test/groovy/JettyServlet2Test.groovy @@ -4,12 +4,13 @@ */ import io.opentelemetry.api.common.AttributeKey +import io.opentelemetry.instrumentation.api.internal.HttpConstants import io.opentelemetry.instrumentation.test.AgentTestTrait import io.opentelemetry.instrumentation.test.asserts.TraceAssert import io.opentelemetry.instrumentation.test.base.HttpServerTest import io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint import io.opentelemetry.sdk.trace.data.SpanData -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import io.opentelemetry.semconv.incubating.CodeIncubatingAttributes import org.eclipse.jetty.server.Response import org.eclipse.jetty.server.Server import org.eclipse.jetty.server.handler.ErrorHandler @@ -83,6 +84,9 @@ class JettyServlet2Test extends HttpServerTest implements AgentTestTrait @Override String expectedServerSpanName(ServerEndpoint endpoint, String method, @Nullable String route) { + if (method == HttpConstants._OTHER) { + return "HTTP " + endpoint.resolvePath(address).path + } switch (endpoint) { case NOT_FOUND: return method @@ -122,8 +126,8 @@ class JettyServlet2Test extends HttpServerTest implements AgentTestTrait kind INTERNAL childOf((SpanData) parent) attributes { - "$SemanticAttributes.CODE_NAMESPACE" Response.name - "$SemanticAttributes.CODE_FUNCTION" responseMethod + "$CodeIncubatingAttributes.CODE_NAMESPACE" Response.name + "$CodeIncubatingAttributes.CODE_FUNCTION" responseMethod } } } diff --git a/instrumentation/servlet/servlet-3.0/javaagent-unit-tests/src/test/java/io/opentelemetry/javaagent/instrumentation/servlet/v3_0/snippet/SnippetPrintWriterTest.java b/instrumentation/servlet/servlet-3.0/javaagent-unit-tests/src/test/java/io/opentelemetry/javaagent/instrumentation/servlet/v3_0/snippet/SnippetPrintWriterTest.java index 796665225deb..65e47c168c29 100644 --- a/instrumentation/servlet/servlet-3.0/javaagent-unit-tests/src/test/java/io/opentelemetry/javaagent/instrumentation/servlet/v3_0/snippet/SnippetPrintWriterTest.java +++ b/instrumentation/servlet/servlet-3.0/javaagent-unit-tests/src/test/java/io/opentelemetry/javaagent/instrumentation/servlet/v3_0/snippet/SnippetPrintWriterTest.java @@ -124,6 +124,22 @@ void testWriteWithOffset() throws IOException { assertThat(response.getStringContent()).isEqualTo(expectedHtml); } + @Test + void testInjectToTextHtmlWithOtherHeadStyle() throws IOException { + String snippet = "\n "; + String html = readFileAsString("beforeSnippetInjectionWithOtherHeadStyle.html"); + + InMemoryHttpServletResponse response = createInMemoryHttpServletResponse("text/html"); + Servlet3SnippetInjectingResponseWrapper responseWrapper = + new Servlet3SnippetInjectingResponseWrapper(response, snippet); + + responseWrapper.getWriter().write(html); + responseWrapper.getWriter().flush(); + + String expectedHtml = readFileAsString("afterSnippetInjectionWithOtherHeadStyle.html"); + assertThat(response.getStringContent()).isEqualTo(expectedHtml); + } + private static InMemoryHttpServletResponse createInMemoryHttpServletResponse(String contentType) { HttpServletResponse response = mock(HttpServletResponse.class); when(response.getContentType()).thenReturn(contentType); diff --git a/instrumentation/servlet/servlet-3.0/javaagent-unit-tests/src/test/java/io/opentelemetry/javaagent/instrumentation/servlet/v3_0/snippet/SnippetServletOutputStreamTest.java b/instrumentation/servlet/servlet-3.0/javaagent-unit-tests/src/test/java/io/opentelemetry/javaagent/instrumentation/servlet/v3_0/snippet/SnippetServletOutputStreamTest.java index 10ee6f14997f..d16c4c521f6c 100644 --- a/instrumentation/servlet/servlet-3.0/javaagent-unit-tests/src/test/java/io/opentelemetry/javaagent/instrumentation/servlet/v3_0/snippet/SnippetServletOutputStreamTest.java +++ b/instrumentation/servlet/servlet-3.0/javaagent-unit-tests/src/test/java/io/opentelemetry/javaagent/instrumentation/servlet/v3_0/snippet/SnippetServletOutputStreamTest.java @@ -125,6 +125,25 @@ void testHeadTagSplitAcrossTwoWrites() throws IOException { assertThat(out.getBytes()).isEqualTo(expectedSecondPart.getBytes(UTF_8)); } + @Test + void testInjectionWithOtherHeadStyle() throws IOException { + String snippet = "\n "; + byte[] html = readFileAsBytes("beforeSnippetInjectionWithOtherHeadStyle.html"); + + InjectionState obj = createInjectionStateForTesting(snippet, UTF_8); + InMemoryServletOutputStream out = new InMemoryServletOutputStream(); + + Supplier stringSupplier = snippet::toString; + OutputStreamSnippetInjectionHelper helper = + new OutputStreamSnippetInjectionHelper(stringSupplier); + boolean injected = helper.handleWrite(obj, out, html, 0, html.length); + assertThat(obj.getHeadTagBytesSeen()).isEqualTo(-1); + assertThat(injected).isEqualTo(true); + + byte[] expectedHtml = readFileAsBytes("afterSnippetInjectionWithOtherHeadStyle.html"); + assertThat(out.getBytes()).isEqualTo(expectedHtml); + } + private static InjectionState createInjectionStateForTesting(String snippet, Charset charset) { HttpServletResponse response = mock(HttpServletResponse.class); when(response.isCommitted()).thenReturn(false); diff --git a/instrumentation/servlet/servlet-3.0/javaagent-unit-tests/src/test/resources/afterSnippetInjectionWithOtherHeadStyle.html b/instrumentation/servlet/servlet-3.0/javaagent-unit-tests/src/test/resources/afterSnippetInjectionWithOtherHeadStyle.html new file mode 100644 index 000000000000..4ef9cb954d5a --- /dev/null +++ b/instrumentation/servlet/servlet-3.0/javaagent-unit-tests/src/test/resources/afterSnippetInjectionWithOtherHeadStyle.html @@ -0,0 +1,11 @@ + + + + + + Title + + + + + diff --git a/instrumentation/servlet/servlet-3.0/javaagent-unit-tests/src/test/resources/beforeSnippetInjectionWithOtherHeadStyle.html b/instrumentation/servlet/servlet-3.0/javaagent-unit-tests/src/test/resources/beforeSnippetInjectionWithOtherHeadStyle.html new file mode 100644 index 000000000000..65f97cac2408 --- /dev/null +++ b/instrumentation/servlet/servlet-3.0/javaagent-unit-tests/src/test/resources/beforeSnippetInjectionWithOtherHeadStyle.html @@ -0,0 +1,10 @@ + + + + + Title + + + + + diff --git a/instrumentation/servlet/servlet-3.0/javaagent/build.gradle.kts b/instrumentation/servlet/servlet-3.0/javaagent/build.gradle.kts index ee6fcecfeb37..7b6bafd9de42 100644 --- a/instrumentation/servlet/servlet-3.0/javaagent/build.gradle.kts +++ b/instrumentation/servlet/servlet-3.0/javaagent/build.gradle.kts @@ -37,9 +37,11 @@ dependencies { latestDepTestLibrary("org.apache.tomcat.embed:tomcat-embed-jasper:9.+") // see servlet-5.0 module } -tasks.withType().configureEach { - jvmArgs("-Dotel.instrumentation.servlet.experimental.capture-request-parameters=test-parameter") - // required on jdk17 - jvmArgs("--add-opens=java.base/java.util=ALL-UNNAMED") - jvmArgs("-XX:+IgnoreUnrecognizedVMOptions") +tasks { + withType().configureEach { + jvmArgs("-Dotel.instrumentation.servlet.experimental.capture-request-parameters=test-parameter") + // required on jdk17 + jvmArgs("--add-opens=java.base/java.util=ALL-UNNAMED") + jvmArgs("-XX:+IgnoreUnrecognizedVMOptions") + } } diff --git a/instrumentation/servlet/servlet-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/v3_0/Servlet3Accessor.java b/instrumentation/servlet/servlet-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/v3_0/Servlet3Accessor.java index 2a6b1be02fcb..8e9740a63de9 100644 --- a/instrumentation/servlet/servlet-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/v3_0/Servlet3Accessor.java +++ b/instrumentation/servlet/servlet-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/v3_0/Servlet3Accessor.java @@ -101,7 +101,9 @@ public void onError(AsyncEvent event) { @Override public void onStartAsync(AsyncEvent event) { - event.getAsyncContext().addListener(this); + event + .getAsyncContext() + .addListener(this, event.getSuppliedRequest(), event.getSuppliedResponse()); } } } diff --git a/instrumentation/servlet/servlet-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/v3_0/Servlet3InstrumentationModule.java b/instrumentation/servlet/servlet-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/v3_0/Servlet3InstrumentationModule.java index 94935ade7b7a..46cc5596a9a8 100644 --- a/instrumentation/servlet/servlet-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/v3_0/Servlet3InstrumentationModule.java +++ b/instrumentation/servlet/servlet-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/v3_0/Servlet3InstrumentationModule.java @@ -33,6 +33,12 @@ public ElementMatcher.Junction classLoaderMatcher() { return hasClassesNamed("javax.servlet.ServletRegistration"); } + @Override + public boolean isIndyModule() { + // GrailsTest fails + return false; + } + @Override public List typeInstrumentations() { return asList( diff --git a/instrumentation/servlet/servlet-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/v3_0/Servlet3ResponseSendAdvice.java b/instrumentation/servlet/servlet-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/v3_0/Servlet3ResponseSendAdvice.java index 9e9a867b8a4f..1def1f5cee04 100644 --- a/instrumentation/servlet/servlet-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/v3_0/Servlet3ResponseSendAdvice.java +++ b/instrumentation/servlet/servlet-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/v3_0/Servlet3ResponseSendAdvice.java @@ -9,7 +9,7 @@ import io.opentelemetry.context.Context; import io.opentelemetry.context.Scope; -import io.opentelemetry.instrumentation.api.instrumenter.util.ClassAndMethod; +import io.opentelemetry.instrumentation.api.incubator.semconv.util.ClassAndMethod; import io.opentelemetry.javaagent.bootstrap.CallDepth; import io.opentelemetry.javaagent.instrumentation.servlet.common.response.HttpServletResponseAdviceHelper; import javax.servlet.http.HttpServletResponse; diff --git a/instrumentation/servlet/servlet-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/v3_0/Servlet3Singletons.java b/instrumentation/servlet/servlet-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/v3_0/Servlet3Singletons.java index cadb80274913..1fcb6cb03517 100644 --- a/instrumentation/servlet/servlet-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/v3_0/Servlet3Singletons.java +++ b/instrumentation/servlet/servlet-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/v3_0/Servlet3Singletons.java @@ -5,8 +5,8 @@ package io.opentelemetry.javaagent.instrumentation.servlet.v3_0; +import io.opentelemetry.instrumentation.api.incubator.semconv.util.ClassAndMethod; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; -import io.opentelemetry.instrumentation.api.instrumenter.util.ClassAndMethod; import io.opentelemetry.instrumentation.api.util.VirtualField; import io.opentelemetry.javaagent.bootstrap.servlet.ExperimentalSnippetHolder; import io.opentelemetry.javaagent.bootstrap.servlet.MappingResolver; diff --git a/instrumentation/servlet/servlet-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/v3_0/snippet/Servlet3SnippetInjectingResponseWrapper.java b/instrumentation/servlet/servlet-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/v3_0/snippet/Servlet3SnippetInjectingResponseWrapper.java index 51432cd3d376..be732a35b2ec 100644 --- a/instrumentation/servlet/servlet-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/v3_0/snippet/Servlet3SnippetInjectingResponseWrapper.java +++ b/instrumentation/servlet/servlet-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/v3_0/snippet/Servlet3SnippetInjectingResponseWrapper.java @@ -75,19 +75,17 @@ public boolean containsHeader(String name) { @Override public void setHeader(String name, String value) { - // checking content-type is just an optimization to avoid unnecessary parsing - if ("Content-Length".equalsIgnoreCase(name) && isContentTypeTextHtml()) { - try { - contentLength = Long.parseLong(value); - } catch (NumberFormatException ex) { - logger.log(FINE, "NumberFormatException", ex); - } - } + handleHeader(name, value); super.setHeader(name, value); } @Override public void addHeader(String name, String value) { + handleHeader(name, value); + super.addHeader(name, value); + } + + private void handleHeader(String name, String value) { // checking content-type is just an optimization to avoid unnecessary parsing if ("Content-Length".equalsIgnoreCase(name) && isContentTypeTextHtml()) { try { @@ -96,7 +94,6 @@ public void addHeader(String name, String value) { logger.log(FINE, "Failed to parse the Content-Length header", ex); } } - super.addHeader(name, value); } @Override @@ -153,7 +150,8 @@ public boolean isContentTypeTextHtml() { if (contentType == null) { contentType = super.getHeader("content-type"); } - return contentType != null && contentType.startsWith("text/html"); + return contentType != null + && (contentType.startsWith("text/html") || contentType.startsWith("application/xhtml+xml")); } @Override diff --git a/instrumentation/servlet/servlet-3.0/javaagent/src/test/groovy/AbstractServlet3Test.groovy b/instrumentation/servlet/servlet-3.0/javaagent/src/test/groovy/AbstractServlet3Test.groovy index bbed9f980e17..7f990d7118ca 100644 --- a/instrumentation/servlet/servlet-3.0/javaagent/src/test/groovy/AbstractServlet3Test.groovy +++ b/instrumentation/servlet/servlet-3.0/javaagent/src/test/groovy/AbstractServlet3Test.groovy @@ -4,12 +4,13 @@ */ import io.opentelemetry.api.trace.SpanKind +import io.opentelemetry.instrumentation.api.internal.HttpConstants import io.opentelemetry.instrumentation.test.AgentTestTrait import io.opentelemetry.instrumentation.test.asserts.TraceAssert import io.opentelemetry.instrumentation.test.base.HttpServerTest import io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint import io.opentelemetry.javaagent.bootstrap.servlet.ExperimentalSnippetHolder -import io.opentelemetry.testing.internal.armeria.common.AggregatedHttpRequest +import io.opentelemetry.semconv.HttpAttributes import javax.servlet.Servlet @@ -82,21 +83,20 @@ abstract class AbstractServlet3Test extends HttpServerTest extends HttpServerTest extends HttpServerTest> httpAttributes(ServerEndpoint endpoint) { def attributes = super.httpAttributes(endpoint) - attributes.remove(SemanticAttributes.HTTP_ROUTE) + attributes.remove(HttpAttributes.HTTP_ROUTE) attributes } diff --git a/instrumentation/servlet/servlet-3.0/javaagent/src/test/groovy/TomcatServlet3Test.groovy b/instrumentation/servlet/servlet-3.0/javaagent/src/test/groovy/TomcatServlet3Test.groovy index 30824c73ea9d..d902559869fe 100644 --- a/instrumentation/servlet/servlet-3.0/javaagent/src/test/groovy/TomcatServlet3Test.groovy +++ b/instrumentation/servlet/servlet-3.0/javaagent/src/test/groovy/TomcatServlet3Test.groovy @@ -40,6 +40,11 @@ import static org.junit.jupiter.api.Assumptions.assumeTrue @Unroll abstract class TomcatServlet3Test extends AbstractServlet3Test { + static final ServerEndpoint ACCESS_LOG_SUCCESS = new ServerEndpoint("ACCESS_LOG_SUCCESS", + "success?access-log=true", SUCCESS.status, SUCCESS.body, false) + static final ServerEndpoint ACCESS_LOG_ERROR = new ServerEndpoint("ACCESS_LOG_ERROR", + "error-status?access-log=true", ERROR.status, ERROR.body, false) + @Override Throwable expectedException() { new ServletException(EXCEPTION.body) @@ -101,7 +106,6 @@ abstract class TomcatServlet3Test extends AbstractServlet3Test def setup() { accessLogValue.loggedIds.clear() - accessLogValue.disable() } @Override @@ -123,10 +127,8 @@ abstract class TomcatServlet3Test extends AbstractServlet3Test } def "access log has ids for #count requests"() { - accessLogValue.enable() - given: - def request = request(SUCCESS, method) + def request = request(ACCESS_LOG_SUCCESS, method) when: List responses = (1..count).collect { @@ -135,8 +137,8 @@ abstract class TomcatServlet3Test extends AbstractServlet3Test then: responses.each { response -> - assert response.status().code() == SUCCESS.status - assert response.contentUtf8() == SUCCESS.body + assert response.status().code() == ACCESS_LOG_SUCCESS.status + assert response.contentUtf8() == ACCESS_LOG_SUCCESS.body } and: @@ -148,7 +150,7 @@ abstract class TomcatServlet3Test extends AbstractServlet3Test (0..count - 1).each { trace(it, 2) { - serverSpan(it, 0, null, null, "GET", SUCCESS.body.length()) + serverSpan(it, 0, null, null, "GET", ACCESS_LOG_SUCCESS) controllerSpan(it, 1, span(0)) } @@ -165,14 +167,13 @@ abstract class TomcatServlet3Test extends AbstractServlet3Test def "access log has ids for error request"() { setup: assumeTrue(testError()) - accessLogValue.enable() - def request = request(ERROR, method) + def request = request(ACCESS_LOG_ERROR, method) def response = client.execute(request).aggregate().join() expect: - response.status().code() == ERROR.status - response.contentUtf8() == ERROR.body + response.status().code() == ACCESS_LOG_ERROR.status + response.contentUtf8() == ACCESS_LOG_ERROR.body and: def spanCount = 2 @@ -181,7 +182,7 @@ abstract class TomcatServlet3Test extends AbstractServlet3Test } assertTraces(1) { trace(0, spanCount) { - serverSpan(it, 0, null, null, method, response.content().length(), ERROR) + serverSpan(it, 0, null, null, method, ACCESS_LOG_ERROR) def spanIndex = 1 controllerSpan(it, spanIndex, span(spanIndex - 1)) spanIndex++ @@ -251,22 +252,13 @@ class ErrorHandlerValve extends ErrorReportValve { class TestAccessLogValve extends ValveBase implements AccessLog { final List> loggedIds = [] - volatile boolean enabled = false TestAccessLogValve() { super(true) } - void disable() { - enabled = false - } - - void enable() { - enabled = true - } - void log(Request request, Response response, long time) { - if (!enabled) { + if (request.getParameter("access-log") == null) { return } diff --git a/instrumentation/servlet/servlet-5.0/javaagent-unit-tests/src/test/java/io/opentelemetry/javaagent/instrumentation/servlet/v5_0/snippet/SnippetPrintWriterTest.java b/instrumentation/servlet/servlet-5.0/javaagent-unit-tests/src/test/java/io/opentelemetry/javaagent/instrumentation/servlet/v5_0/snippet/SnippetPrintWriterTest.java index ebe5adba05d6..efcd6a7a3285 100644 --- a/instrumentation/servlet/servlet-5.0/javaagent-unit-tests/src/test/java/io/opentelemetry/javaagent/instrumentation/servlet/v5_0/snippet/SnippetPrintWriterTest.java +++ b/instrumentation/servlet/servlet-5.0/javaagent-unit-tests/src/test/java/io/opentelemetry/javaagent/instrumentation/servlet/v5_0/snippet/SnippetPrintWriterTest.java @@ -123,6 +123,22 @@ void testWriteWithOffset() throws IOException { assertThat(response.getStringContent()).isEqualTo(expectedHtml); } + @Test + void testInjectToTextHtmlWithOtherHeadStyle() throws IOException { + String snippet = "\n "; + String html = TestUtil.readFileAsString("beforeSnippetInjectionWithOtherHeadStyle.html"); + + InMemoryHttpServletResponse response = createInMemoryHttpServletResponse("text/html"); + Servlet5SnippetInjectingResponseWrapper responseWrapper = + new Servlet5SnippetInjectingResponseWrapper(response, snippet); + + responseWrapper.getWriter().write(html); + responseWrapper.getWriter().flush(); + + String expectedHtml = TestUtil.readFileAsString("afterSnippetInjectionWithOtherHeadStyle.html"); + assertThat(response.getStringContent()).isEqualTo(expectedHtml); + } + private static InMemoryHttpServletResponse createInMemoryHttpServletResponse(String contentType) { HttpServletResponse response = mock(HttpServletResponse.class); when(response.getContentType()).thenReturn(contentType); diff --git a/instrumentation/servlet/servlet-5.0/javaagent-unit-tests/src/test/java/io/opentelemetry/javaagent/instrumentation/servlet/v5_0/snippet/SnippetServletOutputStreamTest.java b/instrumentation/servlet/servlet-5.0/javaagent-unit-tests/src/test/java/io/opentelemetry/javaagent/instrumentation/servlet/v5_0/snippet/SnippetServletOutputStreamTest.java index 29a25b646a3e..aed84df63cf0 100644 --- a/instrumentation/servlet/servlet-5.0/javaagent-unit-tests/src/test/java/io/opentelemetry/javaagent/instrumentation/servlet/v5_0/snippet/SnippetServletOutputStreamTest.java +++ b/instrumentation/servlet/servlet-5.0/javaagent-unit-tests/src/test/java/io/opentelemetry/javaagent/instrumentation/servlet/v5_0/snippet/SnippetServletOutputStreamTest.java @@ -127,6 +127,25 @@ void testHeadTagSplitAcrossTwoWrites() throws IOException { assertThat(out.getBytes()).isEqualTo(expectedSecondPart.getBytes(UTF_8)); } + @Test + void testInjectionWithOtherHeadStyle() throws IOException { + String snippet = "\n "; + byte[] html = readFileAsBytes("beforeSnippetInjectionWithOtherHeadStyle.html"); + + InjectionState obj = createInjectionStateForTesting(snippet, UTF_8); + InMemoryServletOutputStream out = new InMemoryServletOutputStream(); + + Supplier stringSupplier = snippet::toString; + OutputStreamSnippetInjectionHelper helper = + new OutputStreamSnippetInjectionHelper(stringSupplier); + boolean injected = helper.handleWrite(obj, out, html, 0, html.length); + assertThat(obj.getHeadTagBytesSeen()).isEqualTo(-1); + assertThat(injected).isEqualTo(true); + + byte[] expectedHtml = readFileAsBytes("afterSnippetInjectionWithOtherHeadStyle.html"); + assertThat(out.getBytes()).isEqualTo(expectedHtml); + } + private static InjectionState createInjectionStateForTesting(String snippet, Charset charset) { HttpServletResponse response = mock(HttpServletResponse.class); when(response.isCommitted()).thenReturn(false); diff --git a/instrumentation/servlet/servlet-5.0/javaagent-unit-tests/src/test/resources/afterSnippetInjectionWithOtherHeadStyle.html b/instrumentation/servlet/servlet-5.0/javaagent-unit-tests/src/test/resources/afterSnippetInjectionWithOtherHeadStyle.html new file mode 100644 index 000000000000..4ef9cb954d5a --- /dev/null +++ b/instrumentation/servlet/servlet-5.0/javaagent-unit-tests/src/test/resources/afterSnippetInjectionWithOtherHeadStyle.html @@ -0,0 +1,11 @@ + + + + + + Title + + + + + diff --git a/instrumentation/servlet/servlet-5.0/javaagent-unit-tests/src/test/resources/beforeSnippetInjectionWithOtherHeadStyle.html b/instrumentation/servlet/servlet-5.0/javaagent-unit-tests/src/test/resources/beforeSnippetInjectionWithOtherHeadStyle.html new file mode 100644 index 000000000000..65f97cac2408 --- /dev/null +++ b/instrumentation/servlet/servlet-5.0/javaagent-unit-tests/src/test/resources/beforeSnippetInjectionWithOtherHeadStyle.html @@ -0,0 +1,10 @@ + + + + + Title + + + + + diff --git a/instrumentation/servlet/servlet-5.0/javaagent/build.gradle.kts b/instrumentation/servlet/servlet-5.0/javaagent/build.gradle.kts index 8c5c43e9b5bf..0f7d1f12a242 100644 --- a/instrumentation/servlet/servlet-5.0/javaagent/build.gradle.kts +++ b/instrumentation/servlet/servlet-5.0/javaagent/build.gradle.kts @@ -17,7 +17,9 @@ dependencies { compileOnly("jakarta.servlet:jakarta.servlet-api:5.0.0") - testImplementation(project(":instrumentation:servlet:servlet-common:bootstrap")) + testInstrumentation(project(":instrumentation:jetty:jetty-11.0:javaagent")) + + testImplementation(project(":instrumentation:servlet:servlet-5.0:testing")) testLibrary("org.eclipse.jetty:jetty-server:11.0.0") testLibrary("org.eclipse.jetty:jetty-servlet:11.0.0") @@ -27,8 +29,11 @@ dependencies { // Tomcat 10.1 requires Java 11 latestDepTestLibrary("org.apache.tomcat.embed:tomcat-embed-core:10.0.+") latestDepTestLibrary("org.apache.tomcat.embed:tomcat-embed-jasper:10.0.+") + latestDepTestLibrary("org.eclipse.jetty:jetty-server:11.+") } -tasks.withType().configureEach { - jvmArgs("-Dotel.instrumentation.servlet.experimental.capture-request-parameters=test-parameter") +tasks { + withType().configureEach { + jvmArgs("-Dotel.instrumentation.servlet.experimental.capture-request-parameters=test-parameter") + } } diff --git a/instrumentation/servlet/servlet-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/v5_0/Servlet5Accessor.java b/instrumentation/servlet/servlet-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/v5_0/Servlet5Accessor.java index b05d37c24b03..b902885655f6 100644 --- a/instrumentation/servlet/servlet-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/v5_0/Servlet5Accessor.java +++ b/instrumentation/servlet/servlet-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/v5_0/Servlet5Accessor.java @@ -68,16 +68,6 @@ public String getRequestMethod(HttpServletRequest request) { return request.getMethod(); } - @Override - public String getRequestServerName(HttpServletRequest request) { - return request.getServerName(); - } - - @Override - public Integer getRequestServerPort(HttpServletRequest request) { - return request.getServerPort(); - } - @Override public String getRequestRemoteAddr(HttpServletRequest request) { return request.getRemoteAddr(); @@ -204,7 +194,9 @@ public void onError(AsyncEvent event) { @Override public void onStartAsync(AsyncEvent event) { - event.getAsyncContext().addListener(this); + event + .getAsyncContext() + .addListener(this, event.getSuppliedRequest(), event.getSuppliedResponse()); } } } diff --git a/instrumentation/servlet/servlet-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/v5_0/Servlet5Singletons.java b/instrumentation/servlet/servlet-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/v5_0/Servlet5Singletons.java index fbb82815a9d1..ee97922b1842 100644 --- a/instrumentation/servlet/servlet-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/v5_0/Servlet5Singletons.java +++ b/instrumentation/servlet/servlet-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/v5_0/Servlet5Singletons.java @@ -5,8 +5,8 @@ package io.opentelemetry.javaagent.instrumentation.servlet.v5_0; +import io.opentelemetry.instrumentation.api.incubator.semconv.util.ClassAndMethod; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; -import io.opentelemetry.instrumentation.api.instrumenter.util.ClassAndMethod; import io.opentelemetry.instrumentation.api.util.VirtualField; import io.opentelemetry.javaagent.bootstrap.servlet.ExperimentalSnippetHolder; import io.opentelemetry.javaagent.bootstrap.servlet.MappingResolver; diff --git a/instrumentation/servlet/servlet-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/v5_0/response/ResponseSendAdvice.java b/instrumentation/servlet/servlet-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/v5_0/response/ResponseSendAdvice.java index 7e10bd211e6a..43e334050cd5 100644 --- a/instrumentation/servlet/servlet-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/v5_0/response/ResponseSendAdvice.java +++ b/instrumentation/servlet/servlet-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/v5_0/response/ResponseSendAdvice.java @@ -9,7 +9,7 @@ import io.opentelemetry.context.Context; import io.opentelemetry.context.Scope; -import io.opentelemetry.instrumentation.api.instrumenter.util.ClassAndMethod; +import io.opentelemetry.instrumentation.api.incubator.semconv.util.ClassAndMethod; import io.opentelemetry.javaagent.bootstrap.CallDepth; import io.opentelemetry.javaagent.instrumentation.servlet.common.response.HttpServletResponseAdviceHelper; import jakarta.servlet.http.HttpServletResponse; diff --git a/instrumentation/servlet/servlet-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/v5_0/snippet/Servlet5SnippetInjectingResponseWrapper.java b/instrumentation/servlet/servlet-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/v5_0/snippet/Servlet5SnippetInjectingResponseWrapper.java index d0ef8d42e375..06d8b38e162e 100644 --- a/instrumentation/servlet/servlet-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/v5_0/snippet/Servlet5SnippetInjectingResponseWrapper.java +++ b/instrumentation/servlet/servlet-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/v5_0/snippet/Servlet5SnippetInjectingResponseWrapper.java @@ -66,19 +66,17 @@ public boolean containsHeader(String name) { @Override public void setHeader(String name, String value) { - // checking content-type is just an optimization to avoid unnecessary parsing - if ("Content-Length".equalsIgnoreCase(name) && isContentTypeTextHtml()) { - try { - contentLength = Long.parseLong(value); - } catch (NumberFormatException ex) { - logger.log(FINE, "NumberFormatException", ex); - } - } + handleHeader(name, value); super.setHeader(name, value); } @Override public void addHeader(String name, String value) { + handleHeader(name, value); + super.addHeader(name, value); + } + + private void handleHeader(String name, String value) { // checking content-type is just an optimization to avoid unnecessary parsing if ("Content-Length".equalsIgnoreCase(name) && isContentTypeTextHtml()) { try { @@ -87,7 +85,6 @@ public void addHeader(String name, String value) { logger.log(FINE, "Failed to parse the Content-Length header", ex); } } - super.addHeader(name, value); } @Override @@ -145,7 +142,8 @@ public boolean isContentTypeTextHtml() { if (contentType == null) { contentType = super.getHeader("content-type"); } - return contentType != null && contentType.startsWith("text/html"); + return contentType != null + && (contentType.startsWith("text/html") || contentType.startsWith("application/xhtml+xml")); } @Override diff --git a/instrumentation/servlet/servlet-5.0/javaagent/src/test/groovy/JettyServlet5Test.groovy b/instrumentation/servlet/servlet-5.0/javaagent/src/test/groovy/JettyServlet5Test.groovy index f9825014e63f..6b547b3dab93 100644 --- a/instrumentation/servlet/servlet-5.0/javaagent/src/test/groovy/JettyServlet5Test.groovy +++ b/instrumentation/servlet/servlet-5.0/javaagent/src/test/groovy/JettyServlet5Test.groovy @@ -3,6 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ +import io.opentelemetry.javaagent.instrumentation.servlet.v5_0.RequestDispatcherServlet import jakarta.servlet.Servlet import jakarta.servlet.ServletException import jakarta.servlet.http.HttpServletRequest @@ -10,6 +11,8 @@ import org.eclipse.jetty.server.Server import org.eclipse.jetty.server.handler.ErrorHandler import org.eclipse.jetty.servlet.ServletContextHandler import spock.lang.IgnoreIf +import test.AbstractServlet5Test +import test.TestServlet5 import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.AUTH_REQUIRED import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.CAPTURE_HEADERS diff --git a/instrumentation/servlet/servlet-5.0/javaagent/src/test/groovy/JettyServletHandlerTest.groovy b/instrumentation/servlet/servlet-5.0/javaagent/src/test/groovy/JettyServletHandlerTest.groovy index 034b2eafe002..820bc8b8c57e 100644 --- a/instrumentation/servlet/servlet-5.0/javaagent/src/test/groovy/JettyServletHandlerTest.groovy +++ b/instrumentation/servlet/servlet-5.0/javaagent/src/test/groovy/JettyServletHandlerTest.groovy @@ -5,7 +5,7 @@ import io.opentelemetry.api.common.AttributeKey import io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import io.opentelemetry.semconv.HttpAttributes import jakarta.servlet.Servlet import jakarta.servlet.ServletException import jakarta.servlet.http.HttpServletRequest @@ -13,6 +13,8 @@ import org.eclipse.jetty.server.Server import org.eclipse.jetty.server.handler.ErrorHandler import org.eclipse.jetty.servlet.ServletHandler import spock.lang.IgnoreIf +import test.AbstractServlet5Test +import test.TestServlet5 import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.EXCEPTION @@ -22,7 +24,7 @@ class JettyServletHandlerTest extends AbstractServlet5Test { @Override Set> httpAttributes(ServerEndpoint endpoint) { def attributes = super.httpAttributes(endpoint) - attributes.remove(SemanticAttributes.HTTP_ROUTE) + attributes.remove(HttpAttributes.HTTP_ROUTE) attributes } diff --git a/instrumentation/servlet/servlet-5.0/javaagent/src/test/groovy/TomcatServlet5Test.groovy b/instrumentation/servlet/servlet-5.0/javaagent/src/test/groovy/TomcatServlet5Test.groovy index 1a2d63b1f095..51375320b93e 100644 --- a/instrumentation/servlet/servlet-5.0/javaagent/src/test/groovy/TomcatServlet5Test.groovy +++ b/instrumentation/servlet/servlet-5.0/javaagent/src/test/groovy/TomcatServlet5Test.groovy @@ -5,6 +5,7 @@ import io.opentelemetry.instrumentation.test.asserts.TraceAssert import io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint +import io.opentelemetry.javaagent.instrumentation.servlet.v5_0.RequestDispatcherServlet import io.opentelemetry.testing.internal.armeria.common.AggregatedHttpResponse import jakarta.servlet.Servlet import jakarta.servlet.ServletException @@ -20,6 +21,8 @@ import org.apache.tomcat.JarScanFilter import org.apache.tomcat.JarScanType import spock.lang.Shared import spock.lang.Unroll +import test.AbstractServlet5Test +import test.TestServlet5 import java.nio.file.Files import java.util.concurrent.TimeUnit @@ -40,6 +43,11 @@ import static org.junit.jupiter.api.Assumptions.assumeTrue @Unroll abstract class TomcatServlet5Test extends AbstractServlet5Test { + static final ServerEndpoint ACCESS_LOG_SUCCESS = new ServerEndpoint("ACCESS_LOG_SUCCESS", + "success?access-log=true", SUCCESS.status, SUCCESS.body, false) + static final ServerEndpoint ACCESS_LOG_ERROR = new ServerEndpoint("ACCESS_LOG_ERROR", + "error-status?access-log=true", ERROR.status, ERROR.body, false) + @Override Throwable expectedException() { new ServletException(EXCEPTION.body) @@ -101,7 +109,6 @@ abstract class TomcatServlet5Test extends AbstractServlet5Test def setup() { accessLogValue.loggedIds.clear() - accessLogValue.disable() } @Override @@ -123,10 +130,8 @@ abstract class TomcatServlet5Test extends AbstractServlet5Test } def "access log has ids for #count requests"() { - accessLogValue.enable() - given: - def request = request(SUCCESS, method) + def request = request(ACCESS_LOG_SUCCESS, method) when: List responses = (1..count).collect { @@ -135,8 +140,8 @@ abstract class TomcatServlet5Test extends AbstractServlet5Test then: responses.each { response -> - assert response.status().code() == SUCCESS.status - assert response.contentUtf8() == SUCCESS.body + assert response.status().code() == ACCESS_LOG_SUCCESS.status + assert response.contentUtf8() == ACCESS_LOG_SUCCESS.body } and: @@ -148,7 +153,7 @@ abstract class TomcatServlet5Test extends AbstractServlet5Test (0..count - 1).each { trace(it, 2) { - serverSpan(it, 0, null, null, "GET", SUCCESS.body.length()) + serverSpan(it, 0, null, null, "GET", ACCESS_LOG_SUCCESS) controllerSpan(it, 1, span(0)) } @@ -165,14 +170,13 @@ abstract class TomcatServlet5Test extends AbstractServlet5Test def "access log has ids for error request"() { setup: assumeTrue(testError()) - accessLogValue.enable() - def request = request(ERROR, method) + def request = request(ACCESS_LOG_ERROR, method) def response = client.execute(request).aggregate().join() expect: - response.status().code() == ERROR.status - response.contentUtf8() == ERROR.body + response.status().code() == ACCESS_LOG_ERROR.status + response.contentUtf8() == ACCESS_LOG_ERROR.body and: def spanCount = 2 @@ -181,7 +185,7 @@ abstract class TomcatServlet5Test extends AbstractServlet5Test } assertTraces(1) { trace(0, spanCount) { - serverSpan(it, 0, null, null, method, response.content().length(), ERROR) + serverSpan(it, 0, null, null, method, ACCESS_LOG_ERROR) def spanIndex = 1 controllerSpan(it, spanIndex, span(spanIndex - 1)) spanIndex++ @@ -251,24 +255,16 @@ class ErrorHandlerValve extends ErrorReportValve { class TestAccessLogValve extends ValveBase implements AccessLog { final List> loggedIds = [] - volatile boolean enabled = false TestAccessLogValve() { super(true) } - void disable() { - enabled = false - } - - void enable() { - enabled = true - } - void log(Request request, Response response, long time) { - if (!enabled) { + if (request.getParameter("access-log") == null) { return } + synchronized (loggedIds) { loggedIds.add(new Tuple2(request.getAttribute("trace_id"), request.getAttribute("span_id"))) diff --git a/instrumentation/servlet/servlet-5.0/jetty12-testing/build.gradle.kts b/instrumentation/servlet/servlet-5.0/jetty12-testing/build.gradle.kts new file mode 100644 index 000000000000..610d0ef50601 --- /dev/null +++ b/instrumentation/servlet/servlet-5.0/jetty12-testing/build.gradle.kts @@ -0,0 +1,24 @@ +plugins { + id("otel.javaagent-testing") +} + +dependencies { + library("org.eclipse.jetty.ee10:jetty-ee10-servlet:12.0.6") + + testInstrumentation(project(":instrumentation:servlet:servlet-5.0:javaagent")) + testInstrumentation(project(":instrumentation:jetty:jetty-8.0:javaagent")) + testInstrumentation(project(":instrumentation:jetty:jetty-11.0:javaagent")) + testInstrumentation(project(":instrumentation:jetty:jetty-12.0:javaagent")) + + testImplementation(project(":instrumentation:servlet:servlet-5.0:testing")) +} + +otelJava { + minJavaVersionSupported.set(JavaVersion.VERSION_17) +} + +tasks { + withType().configureEach { + jvmArgs("-Dotel.instrumentation.servlet.experimental.capture-request-parameters=test-parameter") + } +} diff --git a/instrumentation/servlet/servlet-5.0/jetty12-testing/src/test/groovy/Jetty12Servlet5Test.groovy b/instrumentation/servlet/servlet-5.0/jetty12-testing/src/test/groovy/Jetty12Servlet5Test.groovy new file mode 100644 index 000000000000..545c1005e761 --- /dev/null +++ b/instrumentation/servlet/servlet-5.0/jetty12-testing/src/test/groovy/Jetty12Servlet5Test.groovy @@ -0,0 +1,241 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import io.opentelemetry.javaagent.instrumentation.servlet.v5_0.RequestDispatcherServlet +import jakarta.servlet.Servlet +import jakarta.servlet.ServletException +import org.eclipse.jetty.ee10.servlet.ServletContextHandler +import org.eclipse.jetty.server.Request +import org.eclipse.jetty.server.Response +import org.eclipse.jetty.server.Server +import org.eclipse.jetty.util.Callback +import test.AbstractServlet5Test +import test.TestServlet5 + +import java.nio.charset.StandardCharsets + +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.AUTH_REQUIRED +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.CAPTURE_HEADERS +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.CAPTURE_PARAMETERS +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.ERROR +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.EXCEPTION +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.INDEXED_CHILD +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.QUERY_PARAM +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.REDIRECT +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.SUCCESS + +abstract class Jetty12Servlet5Test extends AbstractServlet5Test { + + @Override + boolean testNotFound() { + false + } + + @Override + Throwable expectedException() { + new ServletException(EXCEPTION.body) + } + + @Override + Object startServer(int port) { + def jettyServer = new Server(port) + jettyServer.connectors.each { + it.setHost('localhost') + } + + ServletContextHandler servletContext = new ServletContextHandler(contextPath) + servletContext.errorHandler = new Request.Handler() { + + @Override + boolean handle(Request request, Response response, Callback callback) throws Exception { + String message = (String) request.getAttribute("org.eclipse.jetty.server.error_message") + if (message != null) { + response.write(true, StandardCharsets.UTF_8.encode(message), Callback.NOOP) + } + callback.succeeded() + return true + } + } + + 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) + } +} + +class JettyServlet5TestSync extends Jetty12Servlet5Test { + + @Override + Class servlet() { + TestServlet5.Sync + } +} + +class JettyServlet5TestAsync extends Jetty12Servlet5Test { + + @Override + Class servlet() { + TestServlet5.Async + } + + @Override + boolean errorEndpointUsesSendError() { + false + } +} + +class JettyServlet5TestFakeAsync extends Jetty12Servlet5Test { + + @Override + Class servlet() { + TestServlet5.FakeAsync + } +} + +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" + HTML_PRINT_WRITER.path, RequestDispatcherServlet.Forward) + addServlet(context, "/dispatch" + HTML_SERVLET_OUTPUT_STREAM.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) + addServlet(context, "/dispatch" + CAPTURE_HEADERS.path, RequestDispatcherServlet.Forward) + addServlet(context, "/dispatch" + CAPTURE_PARAMETERS.path, RequestDispatcherServlet.Forward) + addServlet(context, "/dispatch" + INDEXED_CHILD.path, RequestDispatcherServlet.Forward) + } +} + +class JettyServlet5TestInclude extends JettyDispatchTest { + @Override + Class servlet() { + TestServlet5.Sync // dispatch to sync servlet + } + + @Override + boolean testRedirect() { + false + } + + @Override + boolean testCapturedHttpHeaders() { + false + } + + @Override + boolean testError() { + false + } + + @Override + protected void setupServlets(Object context) { + super.setupServlets(context) + + addServlet(context, "/dispatch" + SUCCESS.path, RequestDispatcherServlet.Include) + addServlet(context, "/dispatch" + HTML_PRINT_WRITER.path, RequestDispatcherServlet.Include) + addServlet(context, "/dispatch" + HTML_SERVLET_OUTPUT_STREAM.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) + addServlet(context, "/dispatch" + CAPTURE_HEADERS.path, RequestDispatcherServlet.Include) + addServlet(context, "/dispatch" + CAPTURE_PARAMETERS.path, RequestDispatcherServlet.Include) + addServlet(context, "/dispatch" + INDEXED_CHILD.path, RequestDispatcherServlet.Include) + } +} + + +class JettyServlet5TestDispatchImmediate extends JettyDispatchTest { + @Override + Class servlet() { + TestServlet5.Sync + } + + @Override + protected void setupServlets(Object context) { + super.setupServlets(context) + addServlet(context, "/dispatch" + HTML_PRINT_WRITER.path, TestServlet5.DispatchImmediate) + addServlet(context, "/dispatch" + HTML_SERVLET_OUTPUT_STREAM.path, TestServlet5.DispatchImmediate) + 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" + CAPTURE_HEADERS.path, TestServlet5.DispatchImmediate) + addServlet(context, "/dispatch" + CAPTURE_PARAMETERS.path, TestServlet5.DispatchImmediate) + addServlet(context, "/dispatch" + INDEXED_CHILD.path, TestServlet5.DispatchImmediate) + addServlet(context, "/dispatch/recursive", TestServlet5.DispatchRecursive) + } +} + +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" + HTML_PRINT_WRITER.path, TestServlet5.DispatchAsync) + addServlet(context, "/dispatch" + HTML_SERVLET_OUTPUT_STREAM.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" + CAPTURE_HEADERS.path, TestServlet5.DispatchAsync) + addServlet(context, "/dispatch" + CAPTURE_PARAMETERS.path, TestServlet5.DispatchAsync) + addServlet(context, "/dispatch" + INDEXED_CHILD.path, TestServlet5.DispatchAsync) + addServlet(context, "/dispatch/recursive", TestServlet5.DispatchRecursive) + } + + @Override + boolean errorEndpointUsesSendError() { + false + } +} + +abstract class JettyDispatchTest extends Jetty12Servlet5Test { + @Override + URI buildAddress() { + return new URI("http://localhost:$port$contextPath/dispatch/") + } +} diff --git a/instrumentation/servlet/servlet-5.0/testing/build.gradle.kts b/instrumentation/servlet/servlet-5.0/testing/build.gradle.kts new file mode 100644 index 000000000000..8066fc7218b7 --- /dev/null +++ b/instrumentation/servlet/servlet-5.0/testing/build.gradle.kts @@ -0,0 +1,10 @@ +plugins { + id("otel.java-conventions") +} + +dependencies { + api(project(":testing-common")) + api(project(":instrumentation:servlet:servlet-common:bootstrap")) + + compileOnly("jakarta.servlet:jakarta.servlet-api:5.0.0") +} diff --git a/instrumentation/servlet/servlet-5.0/javaagent/src/test/groovy/AbstractServlet5Test.groovy b/instrumentation/servlet/servlet-5.0/testing/src/main/groovy/test/AbstractServlet5Test.groovy similarity index 91% rename from instrumentation/servlet/servlet-5.0/javaagent/src/test/groovy/AbstractServlet5Test.groovy rename to instrumentation/servlet/servlet-5.0/testing/src/main/groovy/test/AbstractServlet5Test.groovy index bba98aab11fe..2fd99b291082 100644 --- a/instrumentation/servlet/servlet-5.0/javaagent/src/test/groovy/AbstractServlet5Test.groovy +++ b/instrumentation/servlet/servlet-5.0/testing/src/main/groovy/test/AbstractServlet5Test.groovy @@ -3,13 +3,16 @@ * SPDX-License-Identifier: Apache-2.0 */ +package test + import io.opentelemetry.api.trace.SpanKind +import io.opentelemetry.instrumentation.api.internal.HttpConstants import io.opentelemetry.instrumentation.test.AgentTestTrait import io.opentelemetry.instrumentation.test.asserts.TraceAssert import io.opentelemetry.instrumentation.test.base.HttpServerTest import io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint import io.opentelemetry.javaagent.bootstrap.servlet.ExperimentalSnippetHolder -import io.opentelemetry.testing.internal.armeria.common.AggregatedHttpRequest +import io.opentelemetry.semconv.HttpAttributes import jakarta.servlet.Servlet import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.AUTH_REQUIRED @@ -82,14 +85,6 @@ abstract class AbstractServlet5Test extends HttpServerTest extends HttpServerTest extends HttpServerTest extends HttpServerTest HEAD_TAG_PREFIX_LENGTH && b == '>') { setHeadTagWritten(); return true; } else { @@ -64,10 +64,11 @@ private boolean inHeadTag(int b) { return true; } else if (headTagBytesSeen == 4 && b == 'd') { return true; - } else if (headTagBytesSeen == 5 && b == '>') { + } else if (headTagBytesSeen == 5 && (b == '>' || Character.isWhitespace(b))) { return true; + } else { + return headTagBytesSeen > 5; } - return false; } public SnippetInjectingResponseWrapper getWrapper() { diff --git a/instrumentation/servlet/servlet-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/BaseServletHelper.java b/instrumentation/servlet/servlet-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/BaseServletHelper.java index c03f72552eef..076c43c1c9c3 100644 --- a/instrumentation/servlet/servlet-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/BaseServletHelper.java +++ b/instrumentation/servlet/servlet-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/BaseServletHelper.java @@ -5,19 +5,20 @@ package io.opentelemetry.javaagent.instrumentation.servlet; -import static io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteSource.FILTER; -import static io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteSource.SERVLET; +import static io.opentelemetry.instrumentation.api.semconv.http.HttpServerRouteSource.SERVER; +import static io.opentelemetry.instrumentation.api.semconv.http.HttpServerRouteSource.SERVER_FILTER; import io.opentelemetry.api.trace.Span; import io.opentelemetry.api.trace.SpanContext; import io.opentelemetry.context.Context; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; import io.opentelemetry.instrumentation.api.instrumenter.LocalRootSpan; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteHolder; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRoute; +import io.opentelemetry.javaagent.bootstrap.internal.AgentCommonConfig; import io.opentelemetry.javaagent.bootstrap.servlet.AppServerBridge; import io.opentelemetry.javaagent.bootstrap.servlet.MappingResolver; import io.opentelemetry.javaagent.bootstrap.servlet.ServletContextPath; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.opentelemetry.semconv.incubating.EnduserIncubatingAttributes; import java.security.Principal; import java.util.function.Function; @@ -86,8 +87,8 @@ public Context updateContext( Context context, REQUEST request, MappingResolver mappingResolver, boolean servlet) { Context result = addServletContextPath(context, request); if (mappingResolver != null) { - HttpRouteHolder.updateHttpRoute( - result, servlet ? SERVLET : FILTER, spanNameProvider, mappingResolver, request); + HttpServerRoute.update( + result, servlet ? SERVER : SERVER_FILTER, spanNameProvider, mappingResolver, request); } return result; @@ -127,19 +128,23 @@ private void captureRequestParameters(Span serverSpan, REQUEST request) { } /** - * Capture {@link SemanticAttributes#ENDUSER_ID} as span attributes when SERVER span is not create - * by servlet instrumentation. + * Capture {@link EnduserIncubatingAttributes#ENDUSER_ID} as span attributes when SERVER span is + * not create by servlet instrumentation. * *

    When SERVER span is created by servlet instrumentation we register {@link * ServletAdditionalAttributesExtractor} as an attribute extractor. When SERVER span is not * created by servlet instrumentation we call this method on exit from the last servlet or filter. */ private void captureEnduserId(Span serverSpan, REQUEST request) { + if (!AgentCommonConfig.get().getEnduserConfig().isIdEnabled()) { + return; + } + Principal principal = accessor.getRequestUserPrincipal(request); if (principal != null) { String name = principal.getName(); if (name != null) { - serverSpan.setAttribute(SemanticAttributes.ENDUSER_ID, name); + serverSpan.setAttribute(EnduserIncubatingAttributes.ENDUSER_ID, name); } } } diff --git a/instrumentation/servlet/servlet-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/ServletAccessor.java b/instrumentation/servlet/servlet-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/ServletAccessor.java index 1b1f49b8e0c9..2504da6d4bf3 100644 --- a/instrumentation/servlet/servlet-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/ServletAccessor.java +++ b/instrumentation/servlet/servlet-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/ServletAccessor.java @@ -35,10 +35,6 @@ public interface ServletAccessor { String getRequestMethod(REQUEST request); - String getRequestServerName(REQUEST request); - - Integer getRequestServerPort(REQUEST request); - String getRequestRemoteAddr(REQUEST request); Integer getRequestRemotePort(REQUEST request); diff --git a/instrumentation/servlet/servlet-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/ServletAdditionalAttributesExtractor.java b/instrumentation/servlet/servlet-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/ServletAdditionalAttributesExtractor.java index afb1ea58c932..3ecebe26fe3b 100644 --- a/instrumentation/servlet/servlet-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/ServletAdditionalAttributesExtractor.java +++ b/instrumentation/servlet/servlet-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/ServletAdditionalAttributesExtractor.java @@ -11,8 +11,9 @@ import io.opentelemetry.api.common.AttributesBuilder; import io.opentelemetry.context.Context; import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; -import io.opentelemetry.javaagent.bootstrap.internal.InstrumentationConfig; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.opentelemetry.javaagent.bootstrap.internal.AgentCommonConfig; +import io.opentelemetry.javaagent.bootstrap.internal.AgentInstrumentationConfig; +import io.opentelemetry.semconv.incubating.EnduserIncubatingAttributes; import java.security.Principal; import javax.annotation.Nullable; @@ -20,7 +21,7 @@ public class ServletAdditionalAttributesExtractor implements AttributesExtractor< ServletRequestContext, ServletResponseContext> { private static final boolean CAPTURE_EXPERIMENTAL_SPAN_ATTRIBUTES = - InstrumentationConfig.get() + AgentInstrumentationConfig.get() .getBoolean("otel.instrumentation.servlet.experimental-span-attributes", false); private static final AttributeKey SERVLET_TIMEOUT = longKey("servlet.timeout"); @@ -43,11 +44,13 @@ public void onEnd( ServletRequestContext requestContext, @Nullable ServletResponseContext responseContext, @Nullable Throwable error) { - Principal principal = accessor.getRequestUserPrincipal(requestContext.request()); - if (principal != null) { - String name = principal.getName(); - if (name != null) { - attributes.put(SemanticAttributes.ENDUSER_ID, name); + if (AgentCommonConfig.get().getEnduserConfig().isIdEnabled()) { + Principal principal = accessor.getRequestUserPrincipal(requestContext.request()); + if (principal != null) { + String name = principal.getName(); + if (name != null) { + attributes.put(EnduserIncubatingAttributes.ENDUSER_ID, name); + } } } if (!CAPTURE_EXPERIMENTAL_SPAN_ATTRIBUTES) { diff --git a/instrumentation/servlet/servlet-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/ServletHelper.java b/instrumentation/servlet/servlet-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/ServletHelper.java index 55dcec6ece91..5df62a1f626d 100644 --- a/instrumentation/servlet/servlet-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/ServletHelper.java +++ b/instrumentation/servlet/servlet-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/ServletHelper.java @@ -14,7 +14,7 @@ public class ServletHelper extends BaseServletHelper getHttpResponseHeader( String name) { return accessor.getResponseHeaderValues(responseContext.response(), name); } + + @Nullable + @Override + public String getNetworkProtocolName( + ServletRequestContext requestContext, + @Nullable ServletResponseContext responseContext) { + String protocol = accessor.getRequestProtocol(requestContext.request()); + if (protocol != null && protocol.startsWith("HTTP/")) { + return "http"; + } + return null; + } + + @Nullable + @Override + public String getNetworkProtocolVersion( + ServletRequestContext requestContext, + @Nullable ServletResponseContext responseContext) { + String protocol = accessor.getRequestProtocol(requestContext.request()); + if (protocol != null && protocol.startsWith("HTTP/")) { + return protocol.substring("HTTP/".length()); + } + return null; + } + + @Override + @Nullable + public String getNetworkPeerAddress( + ServletRequestContext requestContext, + @Nullable ServletResponseContext response) { + return accessor.getRequestRemoteAddr(requestContext.request()); + } + + @Override + @Nullable + public Integer getNetworkPeerPort( + ServletRequestContext requestContext, + @Nullable ServletResponseContext response) { + return accessor.getRequestRemotePort(requestContext.request()); + } + + @Nullable + @Override + public String getNetworkLocalAddress( + ServletRequestContext requestContext, + @Nullable ServletResponseContext response) { + return accessor.getRequestLocalAddr(requestContext.request()); + } + + @Nullable + @Override + public Integer getNetworkLocalPort( + ServletRequestContext requestContext, + @Nullable ServletResponseContext response) { + return accessor.getRequestLocalPort(requestContext.request()); + } } diff --git a/instrumentation/servlet/servlet-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/ServletInstrumenterBuilder.java b/instrumentation/servlet/servlet-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/ServletInstrumenterBuilder.java index 434ad3a6262e..10b221f613a1 100644 --- a/instrumentation/servlet/servlet-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/ServletInstrumenterBuilder.java +++ b/instrumentation/servlet/servlet-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/ServletInstrumenterBuilder.java @@ -7,18 +7,21 @@ import com.google.errorprone.annotations.CanIgnoreReturnValue; import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.instrumentation.api.incubator.semconv.http.HttpExperimentalAttributesExtractor; +import io.opentelemetry.instrumentation.api.incubator.semconv.http.HttpServerExperimentalMetrics; import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; import io.opentelemetry.instrumentation.api.instrumenter.ContextCustomizer; 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.http.HttpRouteHolder; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpServerAttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpServerAttributesGetter; -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 io.opentelemetry.javaagent.bootstrap.internal.CommonConfig; +import io.opentelemetry.instrumentation.api.internal.InstrumenterUtil; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerAttributesExtractor; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerAttributesGetter; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerMetrics; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRoute; +import io.opentelemetry.instrumentation.api.semconv.http.HttpSpanNameExtractor; +import io.opentelemetry.instrumentation.api.semconv.http.HttpSpanStatusExtractor; +import io.opentelemetry.javaagent.bootstrap.internal.AgentCommonConfig; import java.util.ArrayList; import java.util.List; @@ -29,6 +32,8 @@ private ServletInstrumenterBuilder() {} private final List>> contextCustomizers = new ArrayList<>(); + private boolean propagateOperationListenersToOnEnd; + public static ServletInstrumenterBuilder create() { return new ServletInstrumenterBuilder<>(); } @@ -40,6 +45,12 @@ public ServletInstrumenterBuilder addContextCustomizer( return this; } + @CanIgnoreReturnValue + public ServletInstrumenterBuilder propagateOperationListenersToOnEnd() { + propagateOperationListenersToOnEnd = true; + return this; + } + public Instrumenter, ServletResponseContext> build( String instrumentationName, ServletAccessor accessor, @@ -47,8 +58,6 @@ public Instrumenter, ServletResponseContext, ServletResponseContext> httpAttributesGetter) { - ServletNetAttributesGetter netAttributesGetter = - new ServletNetAttributesGetter<>(accessor); ServletErrorCauseExtractor errorCauseExtractor = new ServletErrorCauseExtractor<>(accessor); AttributesExtractor, ServletResponseContext> @@ -60,13 +69,17 @@ public Instrumenter, ServletResponseContext, ServletResponseContext> requestParametersExtractor = new ServletRequestParametersExtractor<>(accessor); @@ -76,6 +89,14 @@ public Instrumenter, ServletResponseContext(accessor)); } @@ -84,7 +105,9 @@ public Instrumenter, ServletResponseContext, ServletResponseContext> httpAttributesGetter = new ServletHttpAttributesGetter<>(accessor); SpanNameExtractor> spanNameExtractor = - HttpSpanNameExtractor.create(httpAttributesGetter); + HttpSpanNameExtractor.builder(httpAttributesGetter) + .setKnownMethods(AgentCommonConfig.get().getKnownHttpRequestMethods()) + .build(); return build(instrumentationName, accessor, spanNameExtractor, httpAttributesGetter); } diff --git a/instrumentation/servlet/servlet-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/ServletNetAttributesGetter.java b/instrumentation/servlet/servlet-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/ServletNetAttributesGetter.java deleted file mode 100644 index a61af4f02956..000000000000 --- a/instrumentation/servlet/servlet-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/ServletNetAttributesGetter.java +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.javaagent.instrumentation.servlet; - -import io.opentelemetry.instrumentation.api.instrumenter.net.NetServerAttributesGetter; -import javax.annotation.Nullable; - -public class ServletNetAttributesGetter - implements NetServerAttributesGetter< - ServletRequestContext, ServletResponseContext> { - - private final ServletAccessor accessor; - - public ServletNetAttributesGetter(ServletAccessor accessor) { - this.accessor = accessor; - } - - @Nullable - @Override - public String getNetworkProtocolName( - ServletRequestContext requestContext, - @Nullable ServletResponseContext responseContext) { - String protocol = accessor.getRequestProtocol(requestContext.request()); - if (protocol != null && protocol.startsWith("HTTP/")) { - return "http"; - } - return null; - } - - @Nullable - @Override - public String getNetworkProtocolVersion( - ServletRequestContext requestContext, - @Nullable ServletResponseContext responseContext) { - String protocol = accessor.getRequestProtocol(requestContext.request()); - if (protocol != null && protocol.startsWith("HTTP/")) { - return protocol.substring("HTTP/".length()); - } - return null; - } - - @Nullable - @Override - public String getServerAddress(ServletRequestContext requestContext) { - return accessor.getRequestServerName(requestContext.request()); - } - - @Nullable - @Override - public Integer getServerPort(ServletRequestContext requestContext) { - return accessor.getRequestServerPort(requestContext.request()); - } - - @Override - @Nullable - public String getClientSocketAddress( - ServletRequestContext requestContext, - @Nullable ServletResponseContext response) { - return accessor.getRequestRemoteAddr(requestContext.request()); - } - - @Override - @Nullable - public Integer getClientSocketPort( - ServletRequestContext requestContext, - @Nullable ServletResponseContext response) { - return accessor.getRequestRemotePort(requestContext.request()); - } - - @Nullable - @Override - public String getServerSocketAddress( - ServletRequestContext requestContext, - @Nullable ServletResponseContext response) { - return accessor.getRequestLocalAddr(requestContext.request()); - } - - @Nullable - @Override - public Integer getServerSocketPort( - ServletRequestContext requestContext, - @Nullable ServletResponseContext response) { - return accessor.getRequestLocalPort(requestContext.request()); - } -} diff --git a/instrumentation/servlet/servlet-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/ServletRequestParametersExtractor.java b/instrumentation/servlet/servlet-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/ServletRequestParametersExtractor.java index f93c23ff4c7d..cf3c36ecc8c2 100644 --- a/instrumentation/servlet/servlet-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/ServletRequestParametersExtractor.java +++ b/instrumentation/servlet/servlet-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/ServletRequestParametersExtractor.java @@ -11,7 +11,7 @@ import io.opentelemetry.api.common.AttributesBuilder; import io.opentelemetry.context.Context; import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; -import io.opentelemetry.javaagent.bootstrap.internal.InstrumentationConfig; +import io.opentelemetry.javaagent.bootstrap.internal.AgentInstrumentationConfig; import java.util.List; import java.util.Locale; import java.util.concurrent.ConcurrentHashMap; @@ -23,7 +23,7 @@ public class ServletRequestParametersExtractor implements AttributesExtractor< ServletRequestContext, ServletResponseContext> { private static final List CAPTURE_REQUEST_PARAMETERS = - InstrumentationConfig.get() + AgentInstrumentationConfig.get() .getList( "otel.instrumentation.servlet.experimental.capture-request-parameters", emptyList()); @@ -70,14 +70,15 @@ public void onEnd( } private static AttributeKey> parameterAttributeKey(String headerName) { - return parameterKeysCache.computeIfAbsent(headerName, n -> createKey(n)); + return parameterKeysCache.computeIfAbsent( + headerName, ServletRequestParametersExtractor::createKey); } private static AttributeKey> createKey(String parameterName) { // normalize parameter name similarly as is done with header names when header values are // captured as span attributes parameterName = parameterName.toLowerCase(Locale.ROOT); - String key = "servlet.request.parameter." + parameterName.replace('-', '_'); + String key = "servlet.request.parameter." + parameterName; return AttributeKey.stringArrayKey(key); } } diff --git a/instrumentation/servlet/servlet-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/ServletSpanNameProvider.java b/instrumentation/servlet/servlet-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/ServletSpanNameProvider.java index 7c22758fcbc3..c440ca8b02de 100644 --- a/instrumentation/servlet/servlet-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/ServletSpanNameProvider.java +++ b/instrumentation/servlet/servlet-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/ServletSpanNameProvider.java @@ -6,14 +6,14 @@ package io.opentelemetry.javaagent.instrumentation.servlet; import io.opentelemetry.context.Context; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteBiGetter; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRouteBiGetter; import io.opentelemetry.javaagent.bootstrap.servlet.MappingResolver; import io.opentelemetry.javaagent.bootstrap.servlet.ServletContextPath; import javax.annotation.Nullable; /** Helper class for constructing span name for given servlet/filter mapping and request. */ public class ServletSpanNameProvider - implements HttpRouteBiGetter { + implements HttpServerRouteBiGetter { private final ServletAccessor servletAccessor; public ServletSpanNameProvider(ServletAccessor servletAccessor) { diff --git a/instrumentation/servlet/servlet-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/common/response/HttpServletResponseAdviceHelper.java b/instrumentation/servlet/servlet-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/common/response/HttpServletResponseAdviceHelper.java index c58e5d1f46a6..fa38949b8d9a 100644 --- a/instrumentation/servlet/servlet-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/common/response/HttpServletResponseAdviceHelper.java +++ b/instrumentation/servlet/servlet-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/common/response/HttpServletResponseAdviceHelper.java @@ -7,8 +7,8 @@ import io.opentelemetry.context.Context; import io.opentelemetry.context.Scope; +import io.opentelemetry.instrumentation.api.incubator.semconv.util.ClassAndMethod; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; -import io.opentelemetry.instrumentation.api.instrumenter.util.ClassAndMethod; import io.opentelemetry.javaagent.bootstrap.Java8BytecodeBridge; public class HttpServletResponseAdviceHelper { diff --git a/instrumentation/servlet/servlet-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/common/response/ResponseInstrumenterFactory.java b/instrumentation/servlet/servlet-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/common/response/ResponseInstrumenterFactory.java index 4269b7c35ffc..9820617cf74b 100644 --- a/instrumentation/servlet/servlet-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/common/response/ResponseInstrumenterFactory.java +++ b/instrumentation/servlet/servlet-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/common/response/ResponseInstrumenterFactory.java @@ -6,11 +6,11 @@ package io.opentelemetry.javaagent.instrumentation.servlet.common.response; import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.instrumentation.api.incubator.semconv.code.CodeAttributesExtractor; +import io.opentelemetry.instrumentation.api.incubator.semconv.code.CodeAttributesGetter; +import io.opentelemetry.instrumentation.api.incubator.semconv.code.CodeSpanNameExtractor; +import io.opentelemetry.instrumentation.api.incubator.semconv.util.ClassAndMethod; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; -import io.opentelemetry.instrumentation.api.instrumenter.code.CodeAttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.code.CodeAttributesGetter; -import io.opentelemetry.instrumentation.api.instrumenter.code.CodeSpanNameExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.util.ClassAndMethod; public final class ResponseInstrumenterFactory { diff --git a/instrumentation/servlet/servlet-javax-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/javax/JavaxServletAccessor.java b/instrumentation/servlet/servlet-javax-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/javax/JavaxServletAccessor.java index 7a300c16313f..db1a6198092a 100644 --- a/instrumentation/servlet/servlet-javax-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/javax/JavaxServletAccessor.java +++ b/instrumentation/servlet/servlet-javax-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/servlet/javax/JavaxServletAccessor.java @@ -55,16 +55,6 @@ public String getRequestMethod(HttpServletRequest request) { return request.getMethod(); } - @Override - public String getRequestServerName(HttpServletRequest request) { - return request.getServerName(); - } - - @Override - public Integer getRequestServerPort(HttpServletRequest request) { - return request.getServerPort(); - } - @Override public String getRequestRemoteAddr(HttpServletRequest request) { return request.getRemoteAddr(); diff --git a/instrumentation/spark-2.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/sparkjava/SparkRouteUpdater.java b/instrumentation/spark-2.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/sparkjava/SparkRouteUpdater.java index eedef6d43cb5..4b482f21d64d 100644 --- a/instrumentation/spark-2.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/sparkjava/SparkRouteUpdater.java +++ b/instrumentation/spark-2.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/sparkjava/SparkRouteUpdater.java @@ -6,8 +6,8 @@ package io.opentelemetry.javaagent.instrumentation.sparkjava; import io.opentelemetry.context.Context; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteHolder; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteSource; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRoute; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRouteSource; import javax.annotation.Nullable; import spark.routematch.RouteMatch; @@ -16,8 +16,7 @@ public final class SparkRouteUpdater { public static void updateHttpRoute(@Nullable RouteMatch routeMatch) { if (routeMatch != null) { Context context = Context.current(); - HttpRouteHolder.updateHttpRoute( - context, HttpRouteSource.CONTROLLER, (c, r) -> r.getMatchUri(), routeMatch); + HttpServerRoute.update(context, HttpServerRouteSource.CONTROLLER, routeMatch.getMatchUri()); } } diff --git a/instrumentation/spark-2.3/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/sparkjava/SparkJavaBasedTest.java b/instrumentation/spark-2.3/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/sparkjava/SparkJavaBasedTest.java index 9e5dcfa82b1e..e47834f18872 100644 --- a/instrumentation/spark-2.3/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/sparkjava/SparkJavaBasedTest.java +++ b/instrumentation/spark-2.3/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/sparkjava/SparkJavaBasedTest.java @@ -5,7 +5,6 @@ package io.opentelemetry.javaagent.instrumentation.sparkjava; -import static io.opentelemetry.api.common.AttributeKey.stringKey; import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.satisfies; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -15,7 +14,12 @@ import io.opentelemetry.instrumentation.test.utils.PortUtils; import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.opentelemetry.semconv.ClientAttributes; +import io.opentelemetry.semconv.HttpAttributes; +import io.opentelemetry.semconv.NetworkAttributes; +import io.opentelemetry.semconv.ServerAttributes; +import io.opentelemetry.semconv.UrlAttributes; +import io.opentelemetry.semconv.UserAgentAttributes; import io.opentelemetry.testing.internal.armeria.client.WebClient; import io.opentelemetry.testing.internal.armeria.common.AggregatedHttpResponse; import org.junit.jupiter.api.AfterAll; @@ -32,14 +36,14 @@ public class SparkJavaBasedTest { static WebClient client; @BeforeAll - static void setupSpec() { + static void setup() { port = PortUtils.findOpenPort(); TestSparkJavaApplication.initSpark(port); client = WebClient.of("http://localhost:" + port); } @AfterAll - static void cleanupSpec() { + static void cleanup() { Spark.stop(); } @@ -52,30 +56,27 @@ void generatesSpans() { assertEquals(content, "Hello asdf1234"); testing.waitAndAssertTraces( trace -> - trace - .hasSize(1) - .hasSpansSatisfyingExactly( - span -> - span.hasName("GET /param/:param") - .hasKind(SpanKind.SERVER) - .hasNoParent() - .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.HTTP_SCHEME, "http"), - equalTo(SemanticAttributes.HTTP_TARGET, "/param/asdf1234"), - equalTo(SemanticAttributes.HTTP_METHOD, "GET"), - equalTo(SemanticAttributes.HTTP_STATUS_CODE, 200), - satisfies( - SemanticAttributes.USER_AGENT_ORIGINAL, - val -> val.isInstanceOf(String.class)), - equalTo(SemanticAttributes.HTTP_ROUTE, "/param/:param"), - equalTo(stringKey("net.protocol.name"), "http"), - equalTo(stringKey("net.protocol.version"), "1.1"), - equalTo(SemanticAttributes.NET_HOST_NAME, "localhost"), - equalTo(SemanticAttributes.NET_HOST_PORT, port), - equalTo(SemanticAttributes.NET_SOCK_PEER_ADDR, "127.0.0.1"), - satisfies( - SemanticAttributes.NET_SOCK_PEER_PORT, - val -> val.isInstanceOf(Long.class)), - equalTo(SemanticAttributes.NET_SOCK_HOST_ADDR, "127.0.0.1")))); + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("GET /param/:param") + .hasKind(SpanKind.SERVER) + .hasNoParent() + .hasAttributesSatisfyingExactly( + equalTo(UrlAttributes.URL_SCHEME, "http"), + equalTo(UrlAttributes.URL_PATH, "/param/asdf1234"), + equalTo(HttpAttributes.HTTP_REQUEST_METHOD, "GET"), + equalTo(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, 200), + satisfies( + UserAgentAttributes.USER_AGENT_ORIGINAL, + val -> val.isInstanceOf(String.class)), + equalTo(HttpAttributes.HTTP_ROUTE, "/param/:param"), + equalTo(NetworkAttributes.NETWORK_PROTOCOL_VERSION, "1.1"), + equalTo(ServerAttributes.SERVER_ADDRESS, "localhost"), + equalTo(ServerAttributes.SERVER_PORT, port), + equalTo(ClientAttributes.CLIENT_ADDRESS, "127.0.0.1"), + equalTo(NetworkAttributes.NETWORK_PEER_ADDRESS, "127.0.0.1"), + satisfies( + NetworkAttributes.NETWORK_PEER_PORT, + val -> val.isInstanceOf(Long.class))))); } } diff --git a/instrumentation/spring/README.md b/instrumentation/spring/README.md index 58f39393d610..7e6d9cbff67a 100644 --- a/instrumentation/spring/README.md +++ b/instrumentation/spring/README.md @@ -1,981 +1,19 @@ # 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. - ## Settings -| System property | Type | Default | Description | -|---|---|---|---| -| `otel.instrumentation.spring-integration.global-channel-interceptor-patterns` | List | `*` | An array of Spring channel name patterns that will be intercepted. See [Spring Integration docs](https://docs.spring.io/spring-integration/reference/html/channel.html#global-channel-configuration-interceptors) for more details. | -| `otel.instrumentation.spring-integration.producer.enabled` | Boolean | `false` | Create producer spans when messages are sent to an output channel. Enable when you're using a messaging library that doesn't have its own instrumentation for generating producer spans. Note that the detection of output channels only works for [Spring Cloud Stream](https://spring.io/projects/spring-cloud-stream) `DirectWithAttributesChannel`. | -| `otel.instrumentation.spring-webflux.experimental-span-attributes` | Boolean | `false` | Enable the capture of experimental span attributes for Spring WebFlux version 5.0. | -| `otel.instrumentation.spring-webmvc.experimental-span-attributes` | Boolean | `false` | Enable the capture of experimental span attributes for Spring Web MVC 3.1. | - -## 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` - -#### Maven - -#### OpenTelemetry - -```xml - - io.opentelemetry - opentelemetry-api - OPENTELEMETRY_VERSION - - - io.opentelemetry - opentelemetry-sdk - OPENTELEMETRY_VERSION - -``` - -##### LoggingSpanExporter - -```xml - - io.opentelemetry - opentelemetry-exporter-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-exporter-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 logger = 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-boot-starter](starters/spring-boot-starter) to enable distributed tracing using -OpenTelemetry and export spans using the default LoggingSpanExporter. We will also use -the [opentelemetry-zipkin-spring-boot-starter](starters/zipkin-spring-boot-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` - -#### Maven - -```xml - - io.opentelemetry.instrumentation - opentelemetry-spring-boot-starter - OPENTELEMETRY_VERSION - -``` - -#### Gradle - -```gradle -implementation("io.opentelemetry.instrumentation:opentelemetry-spring-boot-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.instrumentation.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/main/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.sock.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.sock.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.sock.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.sock.peer.addr=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.sock.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.sock.peer.addr=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.sock.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.sock.peer.addr=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.sock.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.sock.peer.addr=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-spring-boot-starter - OPENTELEMETRY_VERSION - - - - - io.opentelemetry.instrumentation - opentelemetry-jaeger-spring-boot-starter - OPENTELEMETRY_VERSION - -``` - -#### Gradle - -```gradle -//opentelemetry starter with zipkin configurations -implementation("io.opentelemetry.instrumentation:opentelemetry-zipkin-spring-boot-starter:OPENTELEMETRY_VERSION") - -//opentelemetry starter with jaeger configurations -implementation("io.opentelemetry.instrumentation:opentelemetry-jaeger-spring-boot-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:4317 -#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-boot-starter` with `opentelemetry-zipkin-spring-boot-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-spring-boot-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.sock.peer.addr":"127.0.0.1", - "net.sock.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.sock.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.sock.peer.addr":"0:0:0:0:0:0:0:1", - "net.sock.peer.port":"57578", - "net.sock.family":"inet6" - "sampling.probability":"1.0" - } - } -] - -``` +| System property | Type | Default | Description | +|-------------------------------------------------------------------------------|---------|---------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `otel.instrumentation.spring-batch.item.enabled` | Boolean | `false` | Enable creating a span for each batch item. | +| `otel.instrumentation.spring-batch.experimental.chunk.new-trace` | Boolean | `false` | Enable staring a new trace for each batch chunk. | +| `otel.instrumentation.spring-batch.experimental-span-attributes` | Boolean | `false` | Enable the capture of experimental span attributes for Spring Batch version 3.0. | +| `otel.instrumentation.spring-integration.global-channel-interceptor-patterns` | List | `*` | An array of Spring channel name patterns that will be intercepted. See [Spring Integration docs](https://docs.spring.io/spring-integration/reference/channel/configuration.html#global-channel-configuration-interceptors) for more details. | +| `otel.instrumentation.spring-integration.producer.enabled` | Boolean | `false` | Create producer spans when messages are sent to an output channel. Enable when you're using a messaging library that doesn't have its own instrumentation for generating producer spans. Note that the detection of output channels only works for [Spring Cloud Stream](https://spring.io/projects/spring-cloud-stream) `DirectWithAttributesChannel`. | +| `otel.instrumentation.spring-scheduling.experimental-span-attributes` | Boolean | `false` | Enable the capture of experimental span attributes for Spring Scheduling version 3.1. | +| `otel.instrumentation.spring-webflux.experimental-span-attributes` | Boolean | `false` | Enable the capture of experimental span attributes for Spring WebFlux version 5.0. | +| `otel.instrumentation.spring-webmvc.experimental-span-attributes` | Boolean | `false` | Enable the capture of experimental span attributes for Spring Web MVC version 3.1. | diff --git a/instrumentation/spring/spring-batch-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/SpringBatchInstrumentationConfig.java b/instrumentation/spring/spring-batch-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/SpringBatchInstrumentationConfig.java index 0185b64750ed..5dac88c313fa 100644 --- a/instrumentation/spring/spring-batch-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/SpringBatchInstrumentationConfig.java +++ b/instrumentation/spring/spring-batch-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/SpringBatchInstrumentationConfig.java @@ -5,7 +5,7 @@ package io.opentelemetry.javaagent.instrumentation.spring.batch.v3_0; -import io.opentelemetry.javaagent.bootstrap.internal.InstrumentationConfig; +import io.opentelemetry.javaagent.bootstrap.internal.AgentInstrumentationConfig; public final class SpringBatchInstrumentationConfig { @@ -13,10 +13,10 @@ public final class SpringBatchInstrumentationConfig { // the item level instrumentation is very chatty so it's disabled by default private static final boolean ITEM_TRACING_ENABLED = - InstrumentationConfig.get() + AgentInstrumentationConfig.get() .getBoolean("otel.instrumentation.spring-batch.item.enabled", false); private static final boolean CREATE_ROOT_SPAN_FOR_CHUNK = - InstrumentationConfig.get() + AgentInstrumentationConfig.get() .getBoolean("otel.instrumentation.spring-batch.experimental.chunk.new-trace", false); public static String instrumentationName() { diff --git a/instrumentation/spring/spring-batch-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/job/JobSingletons.java b/instrumentation/spring/spring-batch-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/job/JobSingletons.java index 2a3acdf2eea9..1f75b22ea1b1 100644 --- a/instrumentation/spring/spring-batch-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/job/JobSingletons.java +++ b/instrumentation/spring/spring-batch-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/job/JobSingletons.java @@ -12,13 +12,13 @@ import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; import io.opentelemetry.instrumentation.api.instrumenter.InstrumenterBuilder; -import io.opentelemetry.javaagent.bootstrap.internal.InstrumentationConfig; +import io.opentelemetry.javaagent.bootstrap.internal.AgentInstrumentationConfig; import org.springframework.batch.core.JobExecution; public class JobSingletons { private static final boolean CAPTURE_EXPERIMENTAL_SPAN_ATTRIBUTES = - InstrumentationConfig.get() + AgentInstrumentationConfig.get() .getBoolean("otel.instrumentation.spring-batch.experimental-span-attributes", false); private static final Instrumenter INSTRUMENTER; diff --git a/instrumentation/spring/spring-batch-3.0/javaagent/src/test/groovy/SpringBatchTest.groovy b/instrumentation/spring/spring-batch-3.0/javaagent/src/test/groovy/SpringBatchTest.groovy deleted file mode 100644 index d91d38ca6ba9..000000000000 --- a/instrumentation/spring/spring-batch-3.0/javaagent/src/test/groovy/SpringBatchTest.groovy +++ /dev/null @@ -1,346 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -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 - -import static io.opentelemetry.api.trace.SpanKind.INTERNAL -import static io.opentelemetry.api.trace.StatusCode.ERROR -import static java.util.Collections.emptyMap - -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 - attributes { - "job.system" "spring_batch" - } - } - span(1) { - name "BatchJob taskletJob.step" - kind INTERNAL - childOf span(0) - attributes {} - } - span(2) { - name "BatchJob taskletJob.step.Tasklet" - kind INTERNAL - childOf span(1) - attributes {} - } - } - } - } - - 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 - attributes { - "job.system" "spring_batch" - } - } - span(1) { - name "BatchJob taskletJob.step" - kind INTERNAL - childOf span(0) - attributes {} - } - span(2) { - name "BatchJob taskletJob.step.Tasklet" - kind INTERNAL - childOf span(1) - status ERROR - errorEvent IllegalStateException, "fail" - attributes {} - } - } - } - } - - def "should trace chunked items job"() { - when: - runJob("itemsAndTaskletJob") - - then: - assertTraces(1) { - trace(0, 7) { - span(0) { - name "BatchJob itemsAndTaskletJob" - kind INTERNAL - attributes { - "job.system" "spring_batch" - } - } - span(1) { - name "BatchJob itemsAndTaskletJob.itemStep" - kind INTERNAL - childOf span(0) - attributes {} - } - span(2) { - name "BatchJob itemsAndTaskletJob.itemStep.Chunk" - kind INTERNAL - childOf span(1) - attributes {} - } - span(3) { - name "BatchJob itemsAndTaskletJob.itemStep.Chunk" - kind INTERNAL - childOf span(1) - attributes {} - } - span(4) { - name "BatchJob itemsAndTaskletJob.itemStep.Chunk" - kind INTERNAL - childOf span(1) - attributes {} - } - span(5) { - name "BatchJob itemsAndTaskletJob.taskletStep" - kind INTERNAL - childOf span(0) - attributes {} - } - span(6) { - name "BatchJob itemsAndTaskletJob.taskletStep.Tasklet" - kind INTERNAL - childOf span(5) - attributes {} - } - } - } - } - - def "should trace flow job"() { - when: - runJob("flowJob") - - then: - assertTraces(1) { - trace(0, 5) { - span(0) { - name "BatchJob flowJob" - kind INTERNAL - attributes { - "job.system" "spring_batch" - } - } - span(1) { - name "BatchJob flowJob.flowStep1" - kind INTERNAL - childOf span(0) - attributes {} - } - span(2) { - name "BatchJob flowJob.flowStep1.Tasklet" - kind INTERNAL - childOf span(1) - attributes {} - } - span(3) { - name "BatchJob flowJob.flowStep2" - kind INTERNAL - childOf span(0) - attributes {} - } - span(4) { - name "BatchJob flowJob.flowStep2.Tasklet" - kind INTERNAL - childOf span(3) - attributes {} - } - } - } - } - - def "should trace split flow job"() { - when: - runJob("splitJob") - - then: - assertTraces(1) { - trace(0, 5) { - span(0) { - name "BatchJob splitJob" - kind INTERNAL - attributes { - "job.system" "spring_batch" - } - } - span(1) { - name ~/BatchJob splitJob\.splitFlowStep[12]/ - kind INTERNAL - childOf span(0) - attributes {} - } - span(2) { - name ~/BatchJob splitJob\.splitFlowStep[12]\.Tasklet/ - kind INTERNAL - childOf span(1) - attributes {} - } - span(3) { - name ~/BatchJob splitJob\.splitFlowStep[12]/ - kind INTERNAL - childOf span(0) - attributes {} - } - span(4) { - name ~/BatchJob splitJob\.splitFlowStep[12]\.Tasklet/ - kind INTERNAL - childOf span(3) - attributes {} - } - } - } - } - - def "should trace job with decision"() { - when: - runJob("decisionJob") - - then: - assertTraces(1) { - trace(0, 5) { - span(0) { - name "BatchJob decisionJob" - kind INTERNAL - attributes { - "job.system" "spring_batch" - } - } - span(1) { - name "BatchJob decisionJob.decisionStepStart" - kind INTERNAL - childOf span(0) - attributes {} - } - span(2) { - name "BatchJob decisionJob.decisionStepStart.Tasklet" - kind INTERNAL - childOf span(1) - attributes {} - } - span(3) { - name "BatchJob decisionJob.decisionStepLeft" - kind INTERNAL - childOf span(0) - attributes {} - } - span(4) { - name "BatchJob decisionJob.decisionStepLeft.Tasklet" - kind INTERNAL - childOf span(3) - attributes {} - } - } - } - } - - def "should trace partitioned job"() { - when: - runJob("partitionedJob") - - then: - assertTraces(1) { - trace(0, 8) { - span(0) { - name "BatchJob partitionedJob" - kind INTERNAL - attributes { - "job.system" "spring_batch" - } - } - span(1) { - def stepName = hasPartitionManagerStep() ? "partitionManagerStep" : "partitionWorkerStep" - name "BatchJob partitionedJob.$stepName" - kind INTERNAL - childOf span(0) - attributes {} - } - span(2) { - name ~/BatchJob partitionedJob.partitionWorkerStep:partition[01]/ - kind INTERNAL - childOf span(1) - attributes {} - } - span(3) { - name ~/BatchJob partitionedJob.partitionWorkerStep:partition[01].Chunk/ - kind INTERNAL - childOf span(2) - attributes {} - } - span(4) { - name ~/BatchJob partitionedJob.partitionWorkerStep:partition[01].Chunk/ - kind INTERNAL - childOf span(2) - attributes {} - } - span(5) { - name ~/BatchJob partitionedJob.partitionWorkerStep:partition[01]/ - kind INTERNAL - childOf span(1) - attributes {} - } - span(6) { - name ~/BatchJob partitionedJob.partitionWorkerStep:partition[01].Chunk/ - kind INTERNAL - childOf span(5) - attributes {} - } - span(7) { - name ~/BatchJob partitionedJob.partitionWorkerStep:partition[01].Chunk/ - kind INTERNAL - childOf span(5) - attributes {} - } - } - } - } - - 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/instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/basic/JavaConfigBatchJobTest.java b/instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/basic/JavaConfigBatchJobTest.java new file mode 100644 index 000000000000..1401a587fa7c --- /dev/null +++ b/instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/basic/JavaConfigBatchJobTest.java @@ -0,0 +1,23 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.batch.v3_0.basic; + +import io.opentelemetry.javaagent.instrumentation.spring.batch.v3_0.runner.ApplicationConfigRunner; +import io.opentelemetry.javaagent.instrumentation.spring.batch.v3_0.runner.SpringBatchApplication; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; + +class JavaConfigBatchJobTest extends SpringBatchTest { + + @RegisterExtension + static final ApplicationConfigRunner runner = + new ApplicationConfigRunner( + () -> new AnnotationConfigApplicationContext(SpringBatchApplication.class)); + + public JavaConfigBatchJobTest() { + super(runner); + } +} diff --git a/instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/basic/JsrConfigBatchJobTest.java b/instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/basic/JsrConfigBatchJobTest.java new file mode 100644 index 000000000000..c2cc24315545 --- /dev/null +++ b/instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/basic/JsrConfigBatchJobTest.java @@ -0,0 +1,22 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.batch.v3_0.basic; + +import io.opentelemetry.javaagent.instrumentation.spring.batch.v3_0.runner.JavaxBatchConfigRunner; +import org.junit.jupiter.api.extension.RegisterExtension; + +class JsrConfigBatchJobTest extends SpringBatchTest { + @Override + protected boolean hasPartitionManagerStep() { + return false; + } + + @RegisterExtension static final JavaxBatchConfigRunner runner = new JavaxBatchConfigRunner(); + + public JsrConfigBatchJobTest() { + super(runner); + } +} diff --git a/instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/basic/SpringBatchTest.java b/instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/basic/SpringBatchTest.java new file mode 100644 index 000000000000..1465fda06559 --- /dev/null +++ b/instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/basic/SpringBatchTest.java @@ -0,0 +1,279 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.batch.v3_0.basic; + +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; +import static java.util.Collections.singletonMap; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.javaagent.instrumentation.spring.batch.v3_0.runner.JobRunner; +import io.opentelemetry.sdk.testing.assertj.SpanDataAssert; +import io.opentelemetry.sdk.testing.assertj.TraceAssert; +import io.opentelemetry.sdk.trace.data.StatusData; +import java.util.function.Consumer; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.springframework.batch.core.JobParameter; + +abstract class SpringBatchTest { + + private final JobRunner runner; + + @RegisterExtension + static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + public SpringBatchTest(JobRunner runner) { + this.runner = runner; + } + + @Test + void shouldTraceTaskletJobStep() { + runner.runJob("taskletJob"); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("BatchJob taskletJob") + .hasKind(SpanKind.INTERNAL) + .hasAttribute(AttributeKey.stringKey("job.system"), "spring_batch"), + span -> + span.hasName("BatchJob taskletJob.step") + .hasKind(SpanKind.INTERNAL) + .hasTotalAttributeCount(0) + .hasParent(trace.getSpan(0)), + span -> + span.hasName("BatchJob taskletJob.step.Tasklet") + .hasKind(SpanKind.INTERNAL) + .hasTotalAttributeCount(0) + .hasParent(trace.getSpan(1)))); + } + + @Test + void shouldHandleExceptionInTaskletJobStep() { + runner.runJob("taskletJob", singletonMap("fail", new JobParameter(1L))); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("BatchJob taskletJob") + .hasKind(SpanKind.INTERNAL) + .hasAttribute(AttributeKey.stringKey("job.system"), "spring_batch"), + span -> + span.hasName("BatchJob taskletJob.step") + .hasKind(SpanKind.INTERNAL) + .hasTotalAttributeCount(0) + .hasParent(trace.getSpan(0)), + span -> + span.hasName("BatchJob taskletJob.step.Tasklet") + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(1)) + .hasStatus(StatusData.error()) + .hasTotalAttributeCount(0) + .hasException(new IllegalStateException("fail")))); + } + + @Test + void shouldTraceChunkedItemsJob() { + runner.runJob("itemsAndTaskletJob"); + + testing.waitAndAssertTraces( + trace -> { + Consumer chunk = + span -> + span.hasName("BatchJob itemsAndTaskletJob.itemStep.Chunk") + .hasKind(SpanKind.INTERNAL) + .hasTotalAttributeCount(0) + .hasParent(trace.getSpan(1)); + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("BatchJob itemsAndTaskletJob") + .hasKind(SpanKind.INTERNAL) + .hasAttribute(AttributeKey.stringKey("job.system"), "spring_batch"), + span -> + span.hasName("BatchJob itemsAndTaskletJob.itemStep") + .hasKind(SpanKind.INTERNAL) + .hasTotalAttributeCount(0) + .hasParent(trace.getSpan(0)), + chunk, + chunk, + chunk, + span -> + span.hasName("BatchJob itemsAndTaskletJob.taskletStep") + .hasKind(SpanKind.INTERNAL) + .hasTotalAttributeCount(0) + .hasParent(trace.getSpan(0)), + span -> + span.hasName("BatchJob itemsAndTaskletJob.taskletStep.Tasklet") + .hasKind(SpanKind.INTERNAL) + .hasTotalAttributeCount(0) + .hasParent(trace.getSpan(5))); + }); + } + + @Test + void shouldTraceFlowJob() { + runner.runJob("flowJob"); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("BatchJob flowJob") + .hasKind(SpanKind.INTERNAL) + .hasAttribute(AttributeKey.stringKey("job.system"), "spring_batch"), + span -> + span.hasName("BatchJob flowJob.flowStep1") + .hasKind(SpanKind.INTERNAL) + .hasTotalAttributeCount(0) + .hasParent(trace.getSpan(0)), + span -> + span.hasName("BatchJob flowJob.flowStep1.Tasklet") + .hasKind(SpanKind.INTERNAL) + .hasTotalAttributeCount(0) + .hasParent(trace.getSpan(1)), + span -> + span.hasName("BatchJob flowJob.flowStep2") + .hasKind(SpanKind.INTERNAL) + .hasTotalAttributeCount(0) + .hasParent(trace.getSpan(0)), + span -> + span.hasName("BatchJob flowJob.flowStep2.Tasklet") + .hasKind(SpanKind.INTERNAL) + .hasTotalAttributeCount(0) + .hasParent(trace.getSpan(3)))); + } + + @Test + void shouldTraceSplitFlowJob() { + runner.runJob("splitJob"); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("BatchJob splitJob") + .hasKind(SpanKind.INTERNAL) + .hasAttribute(AttributeKey.stringKey("job.system"), "spring_batch"), + span -> + span.satisfies( + spanData -> + assertThat(spanData.getName()) + .matches("BatchJob splitJob.splitFlowStep[12]")) + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(0)), + span -> + span.satisfies( + spanData -> + assertThat(spanData.getName()) + .matches("BatchJob splitJob.splitFlowStep[12].Tasklet")) + .hasKind(SpanKind.INTERNAL) + .hasTotalAttributeCount(0) + .hasParent(trace.getSpan(1)), + span -> + span.satisfies( + spanData -> + assertThat(spanData.getName()) + .matches("BatchJob splitJob.splitFlowStep[12]")) + .hasKind(SpanKind.INTERNAL) + .hasTotalAttributeCount(0) + .hasParent(trace.getSpan(0)), + span -> + span.satisfies( + spanData -> + assertThat(spanData.getName()) + .matches("BatchJob splitJob.splitFlowStep[12].Tasklet")) + .hasKind(SpanKind.INTERNAL) + .hasTotalAttributeCount(0) + .hasParent(trace.getSpan(3)))); + } + + @Test + void shouldTraceJobWithDecision() { + runner.runJob("decisionJob"); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("BatchJob decisionJob") + .hasKind(SpanKind.INTERNAL) + .hasAttribute(AttributeKey.stringKey("job.system"), "spring_batch"), + span -> + span.hasName("BatchJob decisionJob.decisionStepStart") + .hasKind(SpanKind.INTERNAL) + .hasTotalAttributeCount(0) + .hasParent(trace.getSpan(0)), + span -> + span.hasName("BatchJob decisionJob.decisionStepStart.Tasklet") + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(1)), + span -> + span.hasName("BatchJob decisionJob.decisionStepLeft") + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(0)), + span -> + span.hasName("BatchJob decisionJob.decisionStepLeft.Tasklet") + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(3)))); + } + + @Test + void shouldTracePartitionedJob() { + runner.runJob("partitionedJob"); + + testing.waitAndAssertTraces( + trace -> { + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("BatchJob partitionedJob") + .hasKind(SpanKind.INTERNAL) + .hasAttribute(AttributeKey.stringKey("job.system"), "spring_batch"), + span -> + span.hasName( + hasPartitionManagerStep() + ? "BatchJob partitionedJob.partitionManagerStep" + : "BatchJob partitionedJob.partitionWorkerStep") + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(0)), + span -> + span.satisfies( + spanData -> + assertThat(spanData.getName()) + .matches( + "BatchJob partitionedJob.partitionWorkerStep:partition[01]")) + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(1)), + span -> partitionChunk(trace, span, 2), + span -> partitionChunk(trace, span, 2), + span -> + span.satisfies( + spanData -> + assertThat(spanData.getName()) + .matches( + "BatchJob partitionedJob.partitionWorkerStep:partition[01]")) + .hasParent(trace.getSpan(1)), + span -> partitionChunk(trace, span, 5), + span -> partitionChunk(trace, span, 5)); + }); + } + + private static void partitionChunk(TraceAssert trace, SpanDataAssert span, int index) { + span.satisfies( + spanData -> + assertThat(spanData.getName()) + .matches("BatchJob partitionedJob.partitionWorkerStep:partition[01].Chunk")) + .hasParent(trace.getSpan(index)); + } + + protected boolean hasPartitionManagerStep() { + return true; + } +} diff --git a/instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/basic/XmlConfigBatchJobTest.java b/instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/basic/XmlConfigBatchJobTest.java new file mode 100644 index 000000000000..19a395594619 --- /dev/null +++ b/instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/basic/XmlConfigBatchJobTest.java @@ -0,0 +1,21 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.batch.v3_0.basic; + +import io.opentelemetry.javaagent.instrumentation.spring.batch.v3_0.runner.ApplicationConfigRunner; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.springframework.context.support.ClassPathXmlApplicationContext; + +class XmlConfigBatchJobTest extends SpringBatchTest { + + @RegisterExtension + static final ApplicationConfigRunner runner = + new ApplicationConfigRunner(() -> new ClassPathXmlApplicationContext("spring-batch.xml")); + + public XmlConfigBatchJobTest() { + super(runner); + } +} diff --git a/instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/jsr/CustomEventChunkListener.java b/instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/jsr/CustomEventChunkListener.java new file mode 100644 index 000000000000..d0c421d968ae --- /dev/null +++ b/instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/jsr/CustomEventChunkListener.java @@ -0,0 +1,26 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.batch.v3_0.jsr; + +import io.opentelemetry.api.trace.Span; +import javax.batch.api.chunk.listener.ChunkListener; + +public class CustomEventChunkListener implements ChunkListener { + @Override + public void beforeChunk() { + Span.current().addEvent("chunk.before"); + } + + @Override + public void onError(Exception e) { + Span.current().addEvent("chunk.error"); + } + + @Override + public void afterChunk() { + Span.current().addEvent("chunk.after"); + } +} diff --git a/instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/jsr/CustomEventItemProcessListener.java b/instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/jsr/CustomEventItemProcessListener.java new file mode 100644 index 000000000000..95a37221ae21 --- /dev/null +++ b/instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/jsr/CustomEventItemProcessListener.java @@ -0,0 +1,26 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.batch.v3_0.jsr; + +import io.opentelemetry.api.trace.Span; +import javax.batch.api.chunk.listener.ItemProcessListener; + +class CustomEventItemProcessListener implements ItemProcessListener { + @Override + public void beforeProcess(Object o) { + Span.current().addEvent("item.process.before"); + } + + @Override + public void afterProcess(Object o, Object o1) { + Span.current().addEvent("item.process.after"); + } + + @Override + public void onProcessError(Object o, Exception e) { + Span.current().addEvent("item.process.error"); + } +} diff --git a/instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/jsr/CustomEventItemReadListener.java b/instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/jsr/CustomEventItemReadListener.java new file mode 100644 index 000000000000..72d64e28f308 --- /dev/null +++ b/instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/jsr/CustomEventItemReadListener.java @@ -0,0 +1,26 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.batch.v3_0.jsr; + +import io.opentelemetry.api.trace.Span; +import javax.batch.api.chunk.listener.ItemReadListener; + +class CustomEventItemReadListener implements ItemReadListener { + @Override + public void beforeRead() { + Span.current().addEvent("item.read.before"); + } + + @Override + public void afterRead(Object o) { + Span.current().addEvent("item.read.after"); + } + + @Override + public void onReadError(Exception e) { + Span.current().addEvent("item.read.error"); + } +} diff --git a/instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/jsr/CustomEventItemWriteListener.java b/instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/jsr/CustomEventItemWriteListener.java new file mode 100644 index 000000000000..063bb271cbfe --- /dev/null +++ b/instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/jsr/CustomEventItemWriteListener.java @@ -0,0 +1,27 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.batch.v3_0.jsr; + +import io.opentelemetry.api.trace.Span; +import java.util.List; +import javax.batch.api.chunk.listener.ItemWriteListener; + +class CustomEventItemWriteListener implements ItemWriteListener { + @Override + public void beforeWrite(List list) { + Span.current().addEvent("item.write.before"); + } + + @Override + public void afterWrite(List list) { + Span.current().addEvent("item.write.after"); + } + + @Override + public void onWriteError(List list, Exception e) { + Span.current().addEvent("item.write.error"); + } +} diff --git a/instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/jsr/CustomEventJobListener.java b/instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/jsr/CustomEventJobListener.java new file mode 100644 index 000000000000..0999cd12864c --- /dev/null +++ b/instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/jsr/CustomEventJobListener.java @@ -0,0 +1,21 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.batch.v3_0.jsr; + +import io.opentelemetry.api.trace.Span; +import javax.batch.api.listener.JobListener; + +class CustomEventJobListener implements JobListener { + @Override + public void beforeJob() { + Span.current().addEvent("job.before"); + } + + @Override + public void afterJob() { + Span.current().addEvent("job.after"); + } +} diff --git a/instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/jsr/CustomEventStepListener.java b/instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/jsr/CustomEventStepListener.java new file mode 100644 index 000000000000..e14081ada7aa --- /dev/null +++ b/instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/jsr/CustomEventStepListener.java @@ -0,0 +1,21 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.batch.v3_0.jsr; + +import io.opentelemetry.api.trace.Span; +import javax.batch.api.listener.StepListener; + +class CustomEventStepListener implements StepListener { + @Override + public void beforeStep() { + Span.current().addEvent("step.before"); + } + + @Override + public void afterStep() { + Span.current().addEvent("step.after"); + } +} diff --git a/instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/jsr/SingleItemReader.java b/instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/jsr/SingleItemReader.java new file mode 100644 index 000000000000..d7f334cd77d9 --- /dev/null +++ b/instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/jsr/SingleItemReader.java @@ -0,0 +1,34 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.batch.v3_0.jsr; + +import java.io.Serializable; +import java.util.concurrent.atomic.AtomicReference; +import javax.batch.api.chunk.ItemReader; + +class SingleItemReader implements ItemReader { + @Override + public void open(Serializable serializable) {} + + @Override + public void close() {} + + @Override + public Object readItem() { + return item.getAndSet(null); + } + + @Override + public Serializable checkpointInfo() { + return null; + } + + public final AtomicReference getItem() { + return item; + } + + private final AtomicReference item = new AtomicReference("42"); +} diff --git a/instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/jsr/TestBatchlet.java b/instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/jsr/TestBatchlet.java new file mode 100644 index 000000000000..813b0bbcf8d0 --- /dev/null +++ b/instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/jsr/TestBatchlet.java @@ -0,0 +1,36 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.batch.v3_0.jsr; + +import javax.batch.api.BatchProperty; +import javax.batch.api.Batchlet; +import javax.inject.Inject; + +class TestBatchlet implements Batchlet { + @Inject + @BatchProperty(name = "fail") + private String fail; + + @Override + public String process() { + if (fail != null && Integer.valueOf(fail) == 1) { + throw new IllegalStateException("fail"); + } + + return "FINISHED"; + } + + @Override + public void stop() {} + + public String getFail() { + return fail; + } + + public void setFail(String fail) { + this.fail = fail; + } +} diff --git a/instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/jsr/TestDecider.java b/instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/jsr/TestDecider.java new file mode 100644 index 000000000000..71ff22671f6b --- /dev/null +++ b/instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/jsr/TestDecider.java @@ -0,0 +1,16 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.batch.v3_0.jsr; + +import javax.batch.api.Decider; +import javax.batch.runtime.StepExecution; + +class TestDecider implements Decider { + @Override + public String decide(StepExecution[] stepExecutions) { + return "LEFT"; + } +} diff --git a/instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/jsr/TestItemProcessor.java b/instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/jsr/TestItemProcessor.java new file mode 100644 index 000000000000..3814b823ff02 --- /dev/null +++ b/instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/jsr/TestItemProcessor.java @@ -0,0 +1,16 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.batch.v3_0.jsr; + +import javax.batch.api.chunk.ItemProcessor; +import org.codehaus.groovy.runtime.DefaultGroovyMethods; + +class TestItemProcessor implements ItemProcessor { + @Override + public Object processItem(Object item) { + return Integer.parseInt(DefaultGroovyMethods.asType(item, String.class)); + } +} diff --git a/instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/jsr/TestItemReader.java b/instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/jsr/TestItemReader.java new file mode 100644 index 000000000000..41fb17e5e298 --- /dev/null +++ b/instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/jsr/TestItemReader.java @@ -0,0 +1,44 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.batch.v3_0.jsr; + +import java.io.Serializable; +import java.util.Iterator; +import java.util.List; +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 + public void open(Serializable serializable) { + itemsIt = items.iterator(); + } + + @Override + public void close() { + itemsIt = null; + } + + @Override + public Object readItem() { + if (itemsIt == null) { + return null; + } + + return itemsIt.hasNext() ? itemsIt.next() : null; + } + + @Override + public Serializable checkpointInfo() { + return null; + } +} diff --git a/instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/jsr/TestItemWriter.java b/instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/jsr/TestItemWriter.java new file mode 100644 index 000000000000..c14aeb871924 --- /dev/null +++ b/instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/jsr/TestItemWriter.java @@ -0,0 +1,34 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.batch.v3_0.jsr; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; +import javax.batch.api.chunk.ItemWriter; +import org.codehaus.groovy.runtime.DefaultGroovyMethods; + +class TestItemWriter implements ItemWriter { + private final List items = new ArrayList<>(); + + @Override + public void open(Serializable checkpoint) {} + + @Override + public void close() {} + + @Override + public void writeItems(List items) { + for (Object item : items) { + this.items.add(DefaultGroovyMethods.asType(item, Integer.class)); + } + } + + @Override + public Serializable checkpointInfo() { + return null; + } +} diff --git a/instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/jsr/TestPartitionedItemReader.java b/instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/jsr/TestPartitionedItemReader.java new file mode 100644 index 000000000000..d3b0820e2f48 --- /dev/null +++ b/instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/jsr/TestPartitionedItemReader.java @@ -0,0 +1,80 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.batch.v3_0.jsr; + +import java.io.Serializable; +import javax.batch.api.BatchProperty; +import javax.batch.api.chunk.ItemReader; +import javax.inject.Inject; + +class TestPartitionedItemReader implements ItemReader { + + @Inject + @BatchProperty(name = "start") + private String startStr; + + @Inject + @BatchProperty(name = "end") + private String endStr; + + private int start; + private int end; + + @Override + public void open(Serializable checkpoint) { + start = Integer.parseInt(startStr); + end = Integer.parseInt(endStr); + } + + @Override + public void close() {} + + @Override + public Object readItem() { + if (start >= end) { + return null; + } + + return String.valueOf(start++); + } + + @Override + public Serializable checkpointInfo() { + return null; + } + + public String getStartStr() { + return startStr; + } + + public void setStartStr(String startStr) { + this.startStr = startStr; + } + + public String getEndStr() { + return endStr; + } + + public void setEndStr(String endStr) { + this.endStr = endStr; + } + + public int getStart() { + return start; + } + + public void setStart(int start) { + this.start = start; + } + + public int getEnd() { + return end; + } + + public void setEnd(int end) { + this.end = end; + } +} diff --git a/instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/runner/ApplicationConfigRunner.java b/instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/runner/ApplicationConfigRunner.java new file mode 100644 index 000000000000..79ecbc0e428f --- /dev/null +++ b/instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/runner/ApplicationConfigRunner.java @@ -0,0 +1,62 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.batch.v3_0.runner; + +import java.util.Map; +import java.util.function.BiConsumer; +import java.util.function.Supplier; +import org.junit.jupiter.api.extension.AfterEachCallback; +import org.junit.jupiter.api.extension.BeforeEachCallback; +import org.junit.jupiter.api.extension.ExtensionContext; +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; + +public class ApplicationConfigRunner implements BeforeEachCallback, AfterEachCallback, JobRunner { + private final Supplier applicationContextFactory; + private final BiConsumer jobPostProcessor; + static JobLauncher jobLauncher; + private ConfigurableApplicationContext applicationContext; + + public ApplicationConfigRunner( + Supplier applicationContextFactory) { + this(applicationContextFactory, (jobName, job) -> {}); + } + + public ApplicationConfigRunner( + Supplier applicationContextFactory, + BiConsumer jobPostProcessor) { + this.applicationContextFactory = applicationContextFactory; + this.jobPostProcessor = jobPostProcessor; + } + + @Override + public void beforeEach(ExtensionContext context) { + applicationContext = applicationContextFactory.get(); + applicationContext.start(); + + jobLauncher = applicationContext.getBean(JobLauncher.class); + } + + @Override + public void afterEach(ExtensionContext context) { + applicationContext.stop(); + applicationContext.close(); + } + + @Override + public void runJob(String jobName, Map params) { + Job job = applicationContext.getBean(jobName, Job.class); + jobPostProcessor.accept(jobName, job); + try { + jobLauncher.run(job, new JobParameters(params)); + } catch (Exception e) { + throw new IllegalStateException(e); + } + } +} diff --git a/instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/runner/JavaxBatchConfigRunner.java b/instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/runner/JavaxBatchConfigRunner.java new file mode 100644 index 000000000000..e92886d996ff --- /dev/null +++ b/instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/runner/JavaxBatchConfigRunner.java @@ -0,0 +1,34 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.batch.v3_0.runner; + +import java.util.Map; +import java.util.Properties; +import java.util.concurrent.atomic.AtomicInteger; +import javax.batch.operations.JobOperator; +import javax.batch.runtime.BatchRuntime; +import org.junit.jupiter.api.extension.BeforeEachCallback; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.springframework.batch.core.JobParameter; + +public class JavaxBatchConfigRunner implements BeforeEachCallback, JobRunner { + static JobOperator jobOperator; + static AtomicInteger counter = new AtomicInteger(); + + @Override + public void beforeEach(ExtensionContext context) { + jobOperator = BatchRuntime.getJobOperator(); + } + + @Override + public void runJob(String jobName, Map params) { + Properties jobParams = new Properties(); + params.forEach((k, v) -> jobParams.setProperty(k, v.getValue().toString())); + // each job instance with the same name needs to be unique + jobParams.setProperty("uniqueJobIdCounter", String.valueOf(counter.getAndIncrement())); + jobOperator.start(jobName, jobParams); + } +} diff --git a/instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/runner/JobRunner.java b/instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/runner/JobRunner.java new file mode 100644 index 000000000000..7368a7491083 --- /dev/null +++ b/instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/runner/JobRunner.java @@ -0,0 +1,19 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.batch.v3_0.runner; + +import static java.util.Collections.emptyMap; + +import java.util.Map; +import org.springframework.batch.core.JobParameter; + +public interface JobRunner { + void runJob(String jobName, Map params); + + default void runJob(String jobName) { + runJob(jobName, emptyMap()); + } +} diff --git a/instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/runner/SpringBatchApplication.java b/instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/runner/SpringBatchApplication.java new file mode 100644 index 000000000000..a1d790e81b5b --- /dev/null +++ b/instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/runner/SpringBatchApplication.java @@ -0,0 +1,274 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.batch.v3_0.runner; + +import io.opentelemetry.javaagent.instrumentation.spring.batch.v3_0.springbatch.CustomEventChunkListener; +import io.opentelemetry.javaagent.instrumentation.spring.batch.v3_0.springbatch.CustomEventItemProcessListener; +import io.opentelemetry.javaagent.instrumentation.spring.batch.v3_0.springbatch.CustomEventItemReadListener; +import io.opentelemetry.javaagent.instrumentation.spring.batch.v3_0.springbatch.CustomEventItemWriteListener; +import io.opentelemetry.javaagent.instrumentation.spring.batch.v3_0.springbatch.CustomEventJobListener; +import io.opentelemetry.javaagent.instrumentation.spring.batch.v3_0.springbatch.CustomEventStepListener; +import io.opentelemetry.javaagent.instrumentation.spring.batch.v3_0.springbatch.SingleItemReader; +import io.opentelemetry.javaagent.instrumentation.spring.batch.v3_0.springbatch.TestDecider; +import io.opentelemetry.javaagent.instrumentation.spring.batch.v3_0.springbatch.TestItemProcessor; +import io.opentelemetry.javaagent.instrumentation.spring.batch.v3_0.springbatch.TestItemReader; +import io.opentelemetry.javaagent.instrumentation.spring.batch.v3_0.springbatch.TestItemWriter; +import io.opentelemetry.javaagent.instrumentation.spring.batch.v3_0.springbatch.TestPartitionedItemReader; +import io.opentelemetry.javaagent.instrumentation.spring.batch.v3_0.springbatch.TestPartitioner; +import io.opentelemetry.javaagent.instrumentation.spring.batch.v3_0.springbatch.TestSyncItemReader; +import io.opentelemetry.javaagent.instrumentation.spring.batch.v3_0.springbatch.TestTasklet; +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; + +@Configuration +@EnableBatchProcessing +public class SpringBatchApplication { + + @Autowired JobBuilderFactory jobs; + @Autowired StepBuilderFactory steps; + @Autowired JobRepository jobRepository; + + @Bean + AsyncTaskExecutor asyncTaskExecutor() { + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + executor.setCorePoolSize(10); + executor.setMaxPoolSize(10); + return executor; + } + + @Bean + JobLauncher jobLauncher() { + SimpleJobLauncher launcher = new SimpleJobLauncher(); + launcher.setJobRepository(jobRepository); + launcher.setTaskExecutor(asyncTaskExecutor()); + return launcher; + } + + // common + @Bean + ItemReader itemReader() { + return new TestItemReader(); + } + + @Bean + ItemProcessor itemProcessor() { + return new TestItemProcessor(); + } + + @Bean + ItemWriter itemWriter() { + return new TestItemWriter(); + } + + // simple tasklet job + @Bean + Job taskletJob() { + return jobs.get("taskletJob").start(step()).build(); + } + + @Bean + Step step() { + return steps.get("step").tasklet(new TestTasklet()).build(); + } + + // 2-step tasklet + chunked items job + @Bean + Job itemsAndTaskletJob() { + return jobs.get("itemsAndTaskletJob").start(itemStep()).next(taskletStep()).build(); + } + + @Bean + Step taskletStep() { + return steps.get("taskletStep").tasklet(new TestTasklet()).build(); + } + + @Bean + Step itemStep() { + return steps + .get("itemStep") + .chunk(5) + .reader(itemReader()) + .processor(itemProcessor()) + .writer(itemWriter()) + .build(); + } + + // parallel items job + @Bean + Job parallelItemsJob() { + return jobs.get("parallelItemsJob").start(parallelItemsStep()).build(); + } + + @Bean + Step parallelItemsStep() { + return 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() { + return jobs.get("flowJob").start(flow()).build().build(); + } + + @Bean + Flow flow() { + return new FlowBuilder("flow").start(flowStep1()).on("*").to(flowStep2()).build(); + } + + @Bean + Step flowStep1() { + return steps.get("flowStep1").tasklet(new TestTasklet()).build(); + } + + @Bean + Step flowStep2() { + return steps.get("flowStep2").tasklet(new TestTasklet()).build(); + } + + // split job + @Bean + Job splitJob() { + return jobs.get("splitJob") + .start(splitFlowStep1()) + .split(asyncTaskExecutor()) + .add(splitFlow2()) + .build() + .build(); + } + + @Bean + Step splitFlowStep1() { + return steps.get("splitFlowStep1").tasklet(new TestTasklet()).build(); + } + + @Bean + Flow splitFlow2() { + return new FlowBuilder("splitFlow2").start(splitFlowStep2()).build(); + } + + @Bean + Step splitFlowStep2() { + return steps.get("splitFlowStep2").tasklet(new TestTasklet()).build(); + } + + // job with decisions + @Bean + Job decisionJob() { + return jobs.get("decisionJob") + .start(decisionStepStart()) + .next(new TestDecider()) + .on("LEFT") + .to(decisionStepLeft()) + .on("RIGHT") + .to(decisionStepRight()) + .end() + .build(); + } + + @Bean + Step decisionStepStart() { + return steps.get("decisionStepStart").tasklet(new TestTasklet()).build(); + } + + @Bean + Step decisionStepLeft() { + return steps.get("decisionStepLeft").tasklet(new TestTasklet()).build(); + } + + @Bean + Step decisionStepRight() { + return steps.get("decisionStepRight").tasklet(new TestTasklet()).build(); + } + + // partitioned job + @Bean + Job partitionedJob() { + return jobs.get("partitionedJob").start(partitionManagerStep()).build(); + } + + @Bean + Step partitionManagerStep() { + return steps + .get("partitionManagerStep") + .partitioner("partitionWorkerStep", partitioner()) + .step(partitionWorkerStep()) + .gridSize(2) + .taskExecutor(asyncTaskExecutor()) + .build(); + } + + @Bean + Partitioner partitioner() { + return new TestPartitioner(); + } + + @Bean + Step partitionWorkerStep() { + return steps + .get("partitionWorkerStep") + .chunk(5) + .reader(partitionedItemReader()) + .processor(itemProcessor()) + .writer(itemWriter()) + .build(); + } + + @Bean + ItemReader partitionedItemReader() { + return new TestPartitionedItemReader(); + } + + // custom span events items job + @Bean + Job customSpanEventsItemsJob() { + return jobs.get("customSpanEventsItemsJob") + .start(customSpanEventsItemStep()) + .listener(new CustomEventJobListener()) + .build(); + } + + @Bean + Step customSpanEventsItemStep() { + return steps + .get("customSpanEventsItemStep") + .chunk(5) + .reader(new SingleItemReader()) + .processor(itemProcessor()) + .writer(itemWriter()) + .listener(new CustomEventStepListener()) + .listener(new CustomEventChunkListener()) + .listener(new CustomEventItemReadListener()) + .listener(new CustomEventItemProcessListener()) + .listener(new CustomEventItemWriteListener()) + .build(); + } +} diff --git a/instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/springbatch/CustomEventChunkListener.java b/instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/springbatch/CustomEventChunkListener.java new file mode 100644 index 000000000000..1b5dd8a15efa --- /dev/null +++ b/instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/springbatch/CustomEventChunkListener.java @@ -0,0 +1,27 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.batch.v3_0.springbatch; + +import io.opentelemetry.api.trace.Span; +import org.springframework.batch.core.ChunkListener; +import org.springframework.batch.core.scope.context.ChunkContext; + +public class CustomEventChunkListener implements ChunkListener { + @Override + public void beforeChunk(ChunkContext context) { + Span.current().addEvent("chunk.before"); + } + + @Override + public void afterChunk(ChunkContext context) { + Span.current().addEvent("chunk.after"); + } + + @Override + public void afterChunkError(ChunkContext context) { + Span.current().addEvent("chunk.error"); + } +} diff --git a/instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/springbatch/CustomEventItemProcessListener.java b/instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/springbatch/CustomEventItemProcessListener.java new file mode 100644 index 000000000000..fa37fe866810 --- /dev/null +++ b/instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/springbatch/CustomEventItemProcessListener.java @@ -0,0 +1,26 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.batch.v3_0.springbatch; + +import io.opentelemetry.api.trace.Span; +import org.springframework.batch.core.ItemProcessListener; + +public class CustomEventItemProcessListener implements ItemProcessListener { + @Override + public void beforeProcess(String item) { + Span.current().addEvent("item.process.before"); + } + + @Override + public void afterProcess(String item, Integer result) { + Span.current().addEvent("item.process.after"); + } + + @Override + public void onProcessError(String item, Exception e) { + Span.current().addEvent("item.process.error"); + } +} diff --git a/instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/springbatch/CustomEventItemReadListener.java b/instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/springbatch/CustomEventItemReadListener.java new file mode 100644 index 000000000000..9486112372d2 --- /dev/null +++ b/instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/springbatch/CustomEventItemReadListener.java @@ -0,0 +1,26 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.batch.v3_0.springbatch; + +import io.opentelemetry.api.trace.Span; +import org.springframework.batch.core.ItemReadListener; + +public class CustomEventItemReadListener implements ItemReadListener { + @Override + public void beforeRead() { + Span.current().addEvent("item.read.before"); + } + + @Override + public void afterRead(String item) { + Span.current().addEvent("item.read.after"); + } + + @Override + public void onReadError(Exception ex) { + Span.current().addEvent("item.read.error"); + } +} diff --git a/instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/springbatch/CustomEventItemWriteListener.java b/instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/springbatch/CustomEventItemWriteListener.java new file mode 100644 index 000000000000..7fee43ce01a8 --- /dev/null +++ b/instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/springbatch/CustomEventItemWriteListener.java @@ -0,0 +1,27 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.batch.v3_0.springbatch; + +import io.opentelemetry.api.trace.Span; +import java.util.List; +import org.springframework.batch.core.ItemWriteListener; + +public class CustomEventItemWriteListener implements ItemWriteListener { + @Override + public void beforeWrite(List items) { + Span.current().addEvent("item.write.before"); + } + + @Override + public void afterWrite(List items) { + Span.current().addEvent("item.write.after"); + } + + @Override + public void onWriteError(Exception exception, List items) { + Span.current().addEvent("item.write.error"); + } +} diff --git a/instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/springbatch/CustomEventJobListener.java b/instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/springbatch/CustomEventJobListener.java new file mode 100644 index 000000000000..2ccd465b6e6a --- /dev/null +++ b/instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/springbatch/CustomEventJobListener.java @@ -0,0 +1,22 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.batch.v3_0.springbatch; + +import io.opentelemetry.api.trace.Span; +import org.springframework.batch.core.JobExecution; +import org.springframework.batch.core.JobExecutionListener; + +public class CustomEventJobListener implements JobExecutionListener { + @Override + public void beforeJob(JobExecution jobExecution) { + Span.current().addEvent("job.before"); + } + + @Override + public void afterJob(JobExecution jobExecution) { + Span.current().addEvent("job.after"); + } +} diff --git a/instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/springbatch/CustomEventStepListener.java b/instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/springbatch/CustomEventStepListener.java new file mode 100644 index 000000000000..5fc343556673 --- /dev/null +++ b/instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/springbatch/CustomEventStepListener.java @@ -0,0 +1,24 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.batch.v3_0.springbatch; + +import io.opentelemetry.api.trace.Span; +import org.springframework.batch.core.ExitStatus; +import org.springframework.batch.core.StepExecution; +import org.springframework.batch.core.StepExecutionListener; + +public class CustomEventStepListener implements StepExecutionListener { + @Override + public void beforeStep(StepExecution stepExecution) { + Span.current().addEvent("step.before"); + } + + @Override + public ExitStatus afterStep(StepExecution stepExecution) { + Span.current().addEvent("step.after"); + return null; + } +} diff --git a/instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/springbatch/SingleItemReader.java b/instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/springbatch/SingleItemReader.java new file mode 100644 index 000000000000..6e4cd14b7ff1 --- /dev/null +++ b/instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/springbatch/SingleItemReader.java @@ -0,0 +1,18 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.batch.v3_0.springbatch; + +import java.util.concurrent.atomic.AtomicReference; +import org.springframework.batch.item.ItemReader; + +public class SingleItemReader implements ItemReader { + final AtomicReference item = new AtomicReference<>("42"); + + @Override + public String read() { + return item.getAndSet(null); + } +} diff --git a/instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/springbatch/TestDecider.java b/instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/springbatch/TestDecider.java new file mode 100644 index 000000000000..4911d3b794e0 --- /dev/null +++ b/instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/springbatch/TestDecider.java @@ -0,0 +1,18 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.batch.v3_0.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; + +public class TestDecider implements JobExecutionDecider { + @Override + public FlowExecutionStatus decide(JobExecution jobExecution, StepExecution stepExecution) { + return new FlowExecutionStatus("LEFT"); + } +} diff --git a/instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/springbatch/TestItemProcessor.java b/instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/springbatch/TestItemProcessor.java new file mode 100644 index 000000000000..c4aa9cbf9b13 --- /dev/null +++ b/instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/springbatch/TestItemProcessor.java @@ -0,0 +1,15 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.batch.v3_0.springbatch; + +import org.springframework.batch.item.ItemProcessor; + +public class TestItemProcessor implements ItemProcessor { + @Override + public Integer process(String item) { + return Integer.parseInt(item); + } +} diff --git a/instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/springbatch/TestItemReader.java b/instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/springbatch/TestItemReader.java new file mode 100644 index 000000000000..c66a1ff3243c --- /dev/null +++ b/instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/springbatch/TestItemReader.java @@ -0,0 +1,16 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.batch.v3_0.springbatch; + +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import org.springframework.batch.item.support.ListItemReader; + +public class TestItemReader extends ListItemReader { + public TestItemReader() { + super(IntStream.range(0, 13).mapToObj(String::valueOf).collect(Collectors.toList())); + } +} diff --git a/instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/springbatch/TestItemWriter.java b/instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/springbatch/TestItemWriter.java new file mode 100644 index 000000000000..d660c8079ea9 --- /dev/null +++ b/instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/springbatch/TestItemWriter.java @@ -0,0 +1,21 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.batch.v3_0.springbatch; + +import static java.util.Collections.synchronizedList; + +import java.util.ArrayList; +import java.util.List; +import org.springframework.batch.item.ItemWriter; + +public class TestItemWriter implements ItemWriter { + final List items = synchronizedList(new ArrayList<>()); + + @Override + public void write(List items) { + this.items.addAll(items); + } +} diff --git a/instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/springbatch/TestPartitionedItemReader.java b/instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/springbatch/TestPartitionedItemReader.java new file mode 100644 index 000000000000..18b1181a83e6 --- /dev/null +++ b/instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/springbatch/TestPartitionedItemReader.java @@ -0,0 +1,37 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.batch.v3_0.springbatch; + +import org.springframework.batch.item.ExecutionContext; +import org.springframework.batch.item.ItemReader; +import org.springframework.batch.item.ItemStream; + +public class TestPartitionedItemReader implements ItemReader, ItemStream { + ThreadLocal start = new ThreadLocal<>(); + ThreadLocal end = new ThreadLocal<>(); + + @Override + public String read() { + if (start.get() >= end.get()) { + return null; + } + Integer value = start.get(); + start.set(value + 1); + return String.valueOf(value); + } + + @Override + public void open(ExecutionContext executionContext) { + start.set(executionContext.getInt("start")); + end.set(executionContext.getInt("end")); + } + + @Override + public void update(ExecutionContext executionContext) {} + + @Override + public void close() {} +} diff --git a/instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/springbatch/TestPartitioner.java b/instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/springbatch/TestPartitioner.java new file mode 100644 index 000000000000..9f46037c9840 --- /dev/null +++ b/instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/springbatch/TestPartitioner.java @@ -0,0 +1,22 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.batch.v3_0.springbatch; + +import com.google.common.collect.ImmutableMap; +import java.util.HashMap; +import java.util.Map; +import org.springframework.batch.core.partition.support.Partitioner; +import org.springframework.batch.item.ExecutionContext; + +public class TestPartitioner implements Partitioner { + @Override + public Map partition(int gridSize) { + Map map = new HashMap<>(); + map.put("partition0", new ExecutionContext(ImmutableMap.of("start", 0, "end", 8))); + map.put("partition1", new ExecutionContext(ImmutableMap.of("start", 8, "end", 13))); + return map; + } +} diff --git a/instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/springbatch/TestSyncItemReader.java b/instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/springbatch/TestSyncItemReader.java new file mode 100644 index 000000000000..62b673ef2ea9 --- /dev/null +++ b/instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/springbatch/TestSyncItemReader.java @@ -0,0 +1,28 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.batch.v3_0.springbatch; + +import java.util.Iterator; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import org.springframework.batch.item.ItemReader; + +public class TestSyncItemReader implements ItemReader { + private final Iterator items; + + public TestSyncItemReader(int max) { + items = + IntStream.range(0, max).mapToObj(String::valueOf).collect(Collectors.toList()).iterator(); + } + + @Override + public synchronized String read() { + if (items.hasNext()) { + return items.next(); + } + return null; + } +} diff --git a/instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/springbatch/TestTasklet.java b/instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/springbatch/TestTasklet.java new file mode 100644 index 000000000000..e5cbe65f47f6 --- /dev/null +++ b/instrumentation/spring/spring-batch-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/batch/v3_0/springbatch/TestTasklet.java @@ -0,0 +1,23 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.batch.v3_0.springbatch; + +import java.util.Objects; +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; + +public class TestTasklet implements Tasklet { + @Override + public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) { + if (Objects.equals( + chunkContext.getStepContext().getStepExecution().getJobParameters().getLong("fail"), 1L)) { + throw new IllegalStateException("fail"); + } + return RepeatStatus.FINISHED; + } +} diff --git a/instrumentation/spring/spring-batch-3.0/javaagent/src/test/resources/META-INF/batch-jobs/customSpanEventsItemsJob.xml b/instrumentation/spring/spring-batch-3.0/javaagent/src/test/resources/META-INF/batch-jobs/customSpanEventsItemsJob.xml index feeff7e20a6d..0e3df085f3ed 100644 --- a/instrumentation/spring/spring-batch-3.0/javaagent/src/test/resources/META-INF/batch-jobs/customSpanEventsItemsJob.xml +++ b/instrumentation/spring/spring-batch-3.0/javaagent/src/test/resources/META-INF/batch-jobs/customSpanEventsItemsJob.xml @@ -1,20 +1,23 @@ - + - + - - - - - + + + + + - - - + + + diff --git a/instrumentation/spring/spring-batch-3.0/javaagent/src/test/resources/META-INF/batch-jobs/decisionJob.xml b/instrumentation/spring/spring-batch-3.0/javaagent/src/test/resources/META-INF/batch-jobs/decisionJob.xml index 3f2a98def83d..d6d972c159a8 100644 --- a/instrumentation/spring/spring-batch-3.0/javaagent/src/test/resources/META-INF/batch-jobs/decisionJob.xml +++ b/instrumentation/spring/spring-batch-3.0/javaagent/src/test/resources/META-INF/batch-jobs/decisionJob.xml @@ -1,16 +1,19 @@ - + - + - + - + - + - \ No newline at end of file + diff --git a/instrumentation/spring/spring-batch-3.0/javaagent/src/test/resources/META-INF/batch-jobs/flowJob.xml b/instrumentation/spring/spring-batch-3.0/javaagent/src/test/resources/META-INF/batch-jobs/flowJob.xml index a43b024d91d9..c6d647c9062e 100644 --- a/instrumentation/spring/spring-batch-3.0/javaagent/src/test/resources/META-INF/batch-jobs/flowJob.xml +++ b/instrumentation/spring/spring-batch-3.0/javaagent/src/test/resources/META-INF/batch-jobs/flowJob.xml @@ -1,11 +1,14 @@ - + - + - + diff --git a/instrumentation/spring/spring-batch-3.0/javaagent/src/test/resources/META-INF/batch-jobs/itemsAndTaskletJob.xml b/instrumentation/spring/spring-batch-3.0/javaagent/src/test/resources/META-INF/batch-jobs/itemsAndTaskletJob.xml index 7d2a890e8442..d2fc4565a4d4 100644 --- a/instrumentation/spring/spring-batch-3.0/javaagent/src/test/resources/META-INF/batch-jobs/itemsAndTaskletJob.xml +++ b/instrumentation/spring/spring-batch-3.0/javaagent/src/test/resources/META-INF/batch-jobs/itemsAndTaskletJob.xml @@ -1,14 +1,17 @@ - + - - - + + + - + - \ No newline at end of file + diff --git a/instrumentation/spring/spring-batch-3.0/javaagent/src/test/resources/META-INF/batch-jobs/partitionedJob.xml b/instrumentation/spring/spring-batch-3.0/javaagent/src/test/resources/META-INF/batch-jobs/partitionedJob.xml index 330f99c2c68e..42b4f709ec04 100644 --- a/instrumentation/spring/spring-batch-3.0/javaagent/src/test/resources/META-INF/batch-jobs/partitionedJob.xml +++ b/instrumentation/spring/spring-batch-3.0/javaagent/src/test/resources/META-INF/batch-jobs/partitionedJob.xml @@ -1,15 +1,18 @@ - + - + - - + + diff --git a/instrumentation/spring/spring-batch-3.0/javaagent/src/test/resources/META-INF/batch-jobs/splitJob.xml b/instrumentation/spring/spring-batch-3.0/javaagent/src/test/resources/META-INF/batch-jobs/splitJob.xml index a9f8356f46d5..8a274af8949c 100644 --- a/instrumentation/spring/spring-batch-3.0/javaagent/src/test/resources/META-INF/batch-jobs/splitJob.xml +++ b/instrumentation/spring/spring-batch-3.0/javaagent/src/test/resources/META-INF/batch-jobs/splitJob.xml @@ -1,14 +1,17 @@ - + - + - + diff --git a/instrumentation/spring/spring-batch-3.0/javaagent/src/test/resources/META-INF/batch-jobs/taskletJob.xml b/instrumentation/spring/spring-batch-3.0/javaagent/src/test/resources/META-INF/batch-jobs/taskletJob.xml index fe2f62430559..13043c08cdd3 100644 --- a/instrumentation/spring/spring-batch-3.0/javaagent/src/test/resources/META-INF/batch-jobs/taskletJob.xml +++ b/instrumentation/spring/spring-batch-3.0/javaagent/src/test/resources/META-INF/batch-jobs/taskletJob.xml @@ -1,10 +1,13 @@ - + - + - \ No newline at end of file + diff --git a/instrumentation/spring/spring-batch-3.0/javaagent/src/test/resources/spring-batch.xml b/instrumentation/spring/spring-batch-3.0/javaagent/src/test/resources/spring-batch.xml index e6b8ca3c78bb..0e9cf65992d7 100644 --- a/instrumentation/spring/spring-batch-3.0/javaagent/src/test/resources/spring-batch.xml +++ b/instrumentation/spring/spring-batch-3.0/javaagent/src/test/resources/spring-batch.xml @@ -90,31 +90,31 @@ writer="itemWriter"/> - - + + - + - + - + - + - + - + @@ -122,17 +122,17 @@ - + - - + + - - - - - \ No newline at end of file + + + + + diff --git a/instrumentation/spring/spring-boot-actuator-autoconfigure-2.0/javaagent/build.gradle.kts b/instrumentation/spring/spring-boot-actuator-autoconfigure-2.0/javaagent/build.gradle.kts index a40283fd27f5..589d5c510cbf 100644 --- a/instrumentation/spring/spring-boot-actuator-autoconfigure-2.0/javaagent/build.gradle.kts +++ b/instrumentation/spring/spring-boot-actuator-autoconfigure-2.0/javaagent/build.gradle.kts @@ -17,12 +17,17 @@ dependencies { library("io.micrometer:micrometer-core:1.5.0") implementation(project(":instrumentation:micrometer:micrometer-1.5:javaagent")) + + // dependency management pins logback-classic to 1.3 which is the last release that supports java 8 + latestDepTestLibrary("ch.qos.logback:logback-classic:+") } tasks.withType().configureEach { // required on jdk17 jvmArgs("--add-opens=java.base/java.lang=ALL-UNNAMED") jvmArgs("-XX:+IgnoreUnrecognizedVMOptions") + + jvmArgs("-Dotel.instrumentation.spring-boot-actuator-autoconfigure.enabled=true") } val latestDepTest = findProperty("testLatestDeps") as Boolean diff --git a/instrumentation/spring/spring-boot-actuator-autoconfigure-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/actuator/v2_0/SpringBootActuatorInstrumentationModule.java b/instrumentation/spring/spring-boot-actuator-autoconfigure-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/actuator/v2_0/SpringBootActuatorInstrumentationModule.java index 20d0aee2ad29..ce3db47750b7 100644 --- a/instrumentation/spring/spring-boot-actuator-autoconfigure-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/actuator/v2_0/SpringBootActuatorInstrumentationModule.java +++ b/instrumentation/spring/spring-boot-actuator-autoconfigure-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/actuator/v2_0/SpringBootActuatorInstrumentationModule.java @@ -12,11 +12,16 @@ import io.opentelemetry.javaagent.extension.instrumentation.HelperResourceBuilder; import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.internal.ExperimentalInstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.internal.injection.ClassInjector; +import io.opentelemetry.javaagent.extension.instrumentation.internal.injection.InjectionMode; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; import java.util.List; import net.bytebuddy.matcher.ElementMatcher; @AutoService(InstrumentationModule.class) -public class SpringBootActuatorInstrumentationModule extends InstrumentationModule { +public class SpringBootActuatorInstrumentationModule extends InstrumentationModule + implements ExperimentalInstrumentationModule { public SpringBootActuatorInstrumentationModule() { super( @@ -39,12 +44,29 @@ public void registerHelperResources(HelperResourceBuilder helperResourceBuilder) // this line will make OpenTelemetryMeterRegistryAutoConfiguration available to all // classloaders, so that the bean class loader (different from the instrumented class loader) // can load it - helperResourceBuilder.registerForAllClassLoaders( - "io/opentelemetry/javaagent/instrumentation/spring/actuator/v2_0/OpenTelemetryMeterRegistryAutoConfiguration.class"); + if (!isIndyModule()) { + // For indy module the proxy-bytecode will be injected as resource by injectClasses() + helperResourceBuilder.registerForAllClassLoaders( + "io/opentelemetry/javaagent/instrumentation/spring/actuator/v2_0/OpenTelemetryMeterRegistryAutoConfiguration.class"); + } + } + + @Override + public void injectClasses(ClassInjector injector) { + injector + .proxyBuilder( + "io.opentelemetry.javaagent.instrumentation.spring.actuator.v2_0.OpenTelemetryMeterRegistryAutoConfiguration") + .inject(InjectionMode.CLASS_AND_RESOURCE); } @Override public List typeInstrumentations() { return singletonList(new AutoConfigurationImportSelectorInstrumentation()); } + + @Override + public boolean defaultEnabled(ConfigProperties config) { + // produces a lot of metrics that are already captured - e.g. JVM memory usage + return false; + } } diff --git a/instrumentation/spring/spring-boot-autoconfigure/README.md b/instrumentation/spring/spring-boot-autoconfigure/README.md index 7811567fe87f..bb04099dab54 100644 --- a/instrumentation/spring/spring-boot-autoconfigure/README.md +++ b/instrumentation/spring/spring-boot-autoconfigure/README.md @@ -7,447 +7,4 @@ Programming, dependency injection, and bean post-processing to trace spring applications. To include all features listed below use the [opentelemetry-spring-boot-starter](../starters/spring-boot-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: `1.17.0` - -For Maven, add to your `pom.xml` dependencies: - -```xml - - - - io.opentelemetry.instrumentation - opentelemetry-spring-boot - OPENTELEMETRY_VERSION - - - - io.opentelemetry - opentelemetry-api - OPENTELEMETRY_VERSION - - - - - - - io.opentelemetry - opentelemetry-exporter-logging - OPENTELEMETRY_VERSION - - - -``` - -For Gradle, add to your dependencies: - -```groovy -//opentelemetry spring auto-configuration -implementation("io.opentelemetry.instrumentation:opentelemetry-spring-boot: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` - -For Maven, add to your `pom.xml` dependencies: - -```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-exporter-jaeger:OPENTELEMETRY_VERSION") -implementation("io.opentelemetry:opentelemetry-exporter-zipkin:OPENTELEMETRY_VERSION") -implementation("io.opentelemetry:opentelemetry-exporter-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/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/spring-web-3.1) to learn more about the OpenTelemetry RestTemplateInterceptor. - -#### Spring Web MVC Auto Configuration - -This feature autoconfigures instrumentation for Spring WebMVC controllers by adding -a [telemetry producing servlet `Filter`](../spring-webmvc/spring-webmvc-5.3/library/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/v5_3/WebMvcTelemetryProducingFilter.java) -bean to the application context. This filter decorates the request execution with an OpenTelemetry -server span, propagating the incoming tracing context if received in the HTTP request. Check -out [`opentelemetry-spring-webmvc-5.3` instrumentation library](../spring-webmvc/spring-webmvc-5.3/library) -to learn more about the OpenTelemetry Spring WebMVC instrumentation. - -#### Spring WebFlux Auto Configuration - -Provides auto-configurations for the OpenTelemetry WebClient ExchangeFilter defined -in [opentelemetry-spring-webflux-5.3](../spring-webflux/spring-webflux-5.3). 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.3](../spring-webflux/spring-webflux-5.3) 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. The arguments -to the method can be captured as attributed on the created span by annotating the method -parameters with `@SpanAttribute`. - -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.instrumentation.annotations.SpanAttribute; -import io.opentelemetry.instrumentation.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() { - } - - public void tracedMethodWithAttribute(@SpanAttribute("attributeName") String parameter) { - } -} - -``` - -#### 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.sock.peer.addr":"0:0:0:0:0:0:0:1", - "net.sock.peer.port":"33916", - "net.sock.family":"inet6" - "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 `@EnableOpenTelemetry` 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/main/exporters/otlp), [Jaeger](https://github.com/open-telemetry/opentelemetry-java/tree/main/exporters/jaeger), [Zipkin](https://github.com/open-telemetry/opentelemetry-java/tree/main/exporters/zipkin), and [Logging](https://github.com/open-telemetry/opentelemetry-java/tree/main/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 | - - - -##### Resource Properties - -| Feature | Property | Default Value | -|----------|--------------------------------------------------|------------------------| -| Resource | otel.springboot.resource.enabled | `true` | -| | otel.springboot.resource.attributes.service.name | `unknown_service:java` | -| | otel.springboot.resource.attributes | `empty map` | - -`unknown_service:java` will be used as the service-name if no value has been specified to the -property `spring.application.name` or `otel.springboot.resource.attributes.service.name` (which has -the highest priority) - -`otel.springboot.resource.attributes` supports a pattern-based resource configuration in the -application.properties like this: - -``` -otel.springboot.resource.attributes.environment=dev -otel.springboot.resource.attributes.xyz=foo -``` - -##### Exporter Properties - -| Feature | Property | Default Value | -|-----------------|-------------------------------|--------------------------------------| -| Otlp Exporter | otel.exporter.otlp.endpoint | `localhost:4317` | -| | 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 [OpenTelemetry Manual Instrumentation](https://opentelemetry.io/docs/instrumentation/java/manual/) to learn more about -using the OpenTelemetry API to instrument your code. +Documentation for OpenTelemetry Spring Auto-Configuration can be found [here](https://opentelemetry.io/docs/zero-code/java/spring-boot/#automatic-instrumentation). diff --git a/instrumentation/spring/spring-boot-autoconfigure/build.gradle.kts b/instrumentation/spring/spring-boot-autoconfigure/build.gradle.kts index 8cd1255fd92c..dafd20b4f923 100644 --- a/instrumentation/spring/spring-boot-autoconfigure/build.gradle.kts +++ b/instrumentation/spring/spring-boot-autoconfigure/build.gradle.kts @@ -1,43 +1,71 @@ plugins { id("otel.library-instrumentation") + id("otel.japicmp-conventions") } -// Name the Spring Boot modules in accordance with https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#features.developing-auto-configuration.custom-starter -base.archivesName.set("opentelemetry-spring-boot") +base.archivesName.set("opentelemetry-spring-boot-autoconfigure") group = "io.opentelemetry.instrumentation" -val versions: Map by project -val springBootVersion = versions["org.springframework.boot"] +val springBootVersion = "2.7.18" // AutoConfiguration is added in 2.7.0, but can be used with older versions + +// r2dbc-proxy is shadowed to prevent org.springframework.boot.autoconfigure.r2dbc.R2dbcAutoConfiguration +// from being loaded by Spring Boot (by the presence of META-INF/services/io.r2dbc.spi.ConnectionFactoryProvider) - even if the user doesn't want to use R2DBC. +sourceSets { + main { + val shadedDep = project(":instrumentation:r2dbc-1.0:library-instrumentation-shaded") + output.dir( + shadedDep.file("build/extracted/shadow-spring"), + "builtBy" to ":instrumentation:r2dbc-1.0:library-instrumentation-shaded:extractShadowJarSpring", + ) + } + create("javaSpring3") { + java { + setSrcDirs(listOf("src/main/javaSpring3")) + } + } +} + +configurations { + named("javaSpring3CompileOnly") { + extendsFrom(configurations["compileOnly"]) + } +} dependencies { - implementation("org.springframework.boot:spring-boot-autoconfigure:$springBootVersion") + compileOnly("org.springframework.boot:spring-boot-autoconfigure:$springBootVersion") annotationProcessor("org.springframework.boot:spring-boot-autoconfigure-processor:$springBootVersion") annotationProcessor("org.springframework.boot:spring-boot-configuration-processor:$springBootVersion") implementation("javax.validation:validation-api") implementation(project(":instrumentation-annotations-support")) implementation(project(":instrumentation:kafka:kafka-clients:kafka-clients-2.6:library")) + implementation(project(":instrumentation:mongo:mongo-3.1:library")) + compileOnly(project(path = ":instrumentation:r2dbc-1.0:library-instrumentation-shaded", configuration = "shadow")) implementation(project(":instrumentation:spring:spring-kafka-2.7:library")) implementation(project(":instrumentation:spring:spring-web:spring-web-3.1:library")) implementation(project(":instrumentation:spring:spring-webmvc:spring-webmvc-5.3:library")) - implementation(project(":instrumentation:spring:spring-webmvc:spring-webmvc-6.0:library")) compileOnly("javax.servlet:javax.servlet-api:3.1.0") - compileOnly("jakarta.servlet:jakarta.servlet-api:5.0.0") implementation(project(":instrumentation:spring:spring-webflux:spring-webflux-5.3:library")) implementation(project(":instrumentation:micrometer:micrometer-1.5:library")) + implementation(project(":instrumentation:log4j:log4j-appender-2.17:library")) + compileOnly("org.apache.logging.log4j:log4j-core:2.17.0") + implementation(project(":instrumentation:logback:logback-appender-1.0:library")) + compileOnly("ch.qos.logback:logback-classic:1.0.0") + implementation(project(":instrumentation:jdbc:library")) library("org.springframework.kafka:spring-kafka:2.9.0") library("org.springframework.boot:spring-boot-starter-actuator:$springBootVersion") library("org.springframework.boot:spring-boot-starter-aop:$springBootVersion") library("org.springframework.boot:spring-boot-starter-web:$springBootVersion") library("org.springframework.boot:spring-boot-starter-webflux:$springBootVersion") + library("org.springframework.boot:spring-boot-starter-data-mongodb:$springBootVersion") + library("org.springframework.boot:spring-boot-starter-data-r2dbc:$springBootVersion") - compileOnly("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure-spi") - compileOnly("io.opentelemetry:opentelemetry-extension-annotations") + implementation("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure") + implementation(project(":sdk-autoconfigure-support")) compileOnly("io.opentelemetry:opentelemetry-extension-trace-propagators") compileOnly("io.opentelemetry.contrib:opentelemetry-aws-xray-propagator") compileOnly("io.opentelemetry:opentelemetry-exporter-logging") - compileOnly("io.opentelemetry:opentelemetry-exporter-jaeger") compileOnly("io.opentelemetry:opentelemetry-exporter-otlp") compileOnly("io.opentelemetry:opentelemetry-exporter-zipkin") compileOnly(project(":instrumentation-annotations")) @@ -49,23 +77,32 @@ dependencies { testLibrary("org.springframework.boot:spring-boot-starter-test:$springBootVersion") { exclude("org.junit.vintage", "junit-vintage-engine") } - testImplementation("org.testcontainers:kafka") testImplementation("javax.servlet:javax.servlet-api:3.1.0") testImplementation("jakarta.servlet:jakarta.servlet-api:5.0.0") + testRuntimeOnly("com.h2database:h2:1.4.197") + testRuntimeOnly("io.r2dbc:r2dbc-h2:1.0.0.RELEASE") testImplementation(project(":testing-common")) testImplementation("io.opentelemetry:opentelemetry-sdk") testImplementation("io.opentelemetry:opentelemetry-sdk-testing") testImplementation(project(":instrumentation:resources:library")) testImplementation("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure-spi") - testImplementation("io.opentelemetry:opentelemetry-extension-annotations") testImplementation("io.opentelemetry:opentelemetry-extension-trace-propagators") testImplementation("io.opentelemetry.contrib:opentelemetry-aws-xray-propagator") testImplementation("io.opentelemetry:opentelemetry-exporter-logging") - testImplementation("io.opentelemetry:opentelemetry-exporter-jaeger") testImplementation("io.opentelemetry:opentelemetry-exporter-otlp") testImplementation("io.opentelemetry:opentelemetry-exporter-zipkin") testImplementation(project(":instrumentation-annotations")) + + // needed for the Spring Boot 3 support + implementation(project(":instrumentation:spring:spring-webmvc:spring-webmvc-6.0:library")) + + // give access to common classes, e.g. InstrumentationConfigUtil + add("javaSpring3CompileOnly", files(sourceSets.main.get().output.classesDirs)) + add("javaSpring3CompileOnly", "org.springframework.boot:spring-boot-starter-web:3.2.4") + add("javaSpring3CompileOnly", "io.opentelemetry:opentelemetry-sdk-extension-autoconfigure") + add("javaSpring3CompileOnly", project(":instrumentation:spring:spring-web:spring-web-3.1:library")) + add("javaSpring3CompileOnly", project(":instrumentation:spring:spring-webmvc:spring-webmvc-6.0:library")) } val latestDepTest = findProperty("testLatestDeps") as Boolean @@ -77,24 +114,104 @@ if (latestDepTest) { } } -tasks.compileTestJava { - options.compilerArgs.add("-parameters") +val testJavaVersion = gradle.startParameter.projectProperties["testJavaVersion"]?.let(JavaVersion::toVersion) +val testSpring3 = (testJavaVersion == null || testJavaVersion.compareTo(JavaVersion.VERSION_17) >= 0) + +testing { + suites { + val testLogbackAppender by registering(JvmTestSuite::class) { + dependencies { + implementation(project()) + implementation(project(":testing-common")) + implementation("io.opentelemetry:opentelemetry-sdk") + implementation("io.opentelemetry:opentelemetry-sdk-testing") + implementation("org.mockito:mockito-inline") + implementation("org.springframework.boot:spring-boot-autoconfigure:$springBootVersion") + + implementation(project(":instrumentation:logback:logback-appender-1.0:library")) + // using the same versions as in the spring-boot-autoconfigure + implementation("ch.qos.logback:logback-classic") { + version { + strictly("1.2.11") + } + } + implementation("org.slf4j:slf4j-api") { + version { + strictly("1.7.32") + } + } + } + } + + val testLogbackMissing by registering(JvmTestSuite::class) { + dependencies { + implementation(project()) + implementation("org.springframework.boot:spring-boot-autoconfigure:$springBootVersion") + + implementation("org.slf4j:slf4j-api") { + version { + strictly("1.7.32") + } + } + } + } + + val testSpring3 by registering(JvmTestSuite::class) { + dependencies { + implementation(project()) + implementation("org.springframework.boot:spring-boot-starter-web:3.2.4") + implementation("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure") + implementation(project(":instrumentation:spring:spring-web:spring-web-3.1:library")) + implementation(project(":instrumentation:spring:spring-webmvc:spring-webmvc-6.0:library")) + implementation("jakarta.servlet:jakarta.servlet-api:5.0.0") + implementation("org.springframework.boot:spring-boot-starter-test:3.2.4") { + exclude("org.junit.vintage", "junit-vintage-engine") + } + } + } + } +} + +configurations.configureEach { + if (name.contains("testLogbackMissing")) { + exclude("ch.qos.logback", "logback-classic") + } } -tasks.withType().configureEach { - usesService(gradle.sharedServices.registrations["testcontainersBuildService"].service) +tasks { + check { + dependsOn(testing.suites) + } + + compileTestJava { + options.compilerArgs.add("-parameters") + } + + withType().configureEach { + systemProperty("testLatestDeps", latestDepTest) - systemProperty("testLatestDeps", latestDepTest) + // required on jdk17 + jvmArgs("--add-opens=java.base/java.lang=ALL-UNNAMED") + jvmArgs("-XX:+IgnoreUnrecognizedVMOptions") + } + + named("compileJavaSpring3Java") { + sourceCompatibility = "17" + targetCompatibility = "17" + options.release.set(17) + } - // required on jdk17 - jvmArgs("--add-opens=java.base/java.lang=ALL-UNNAMED") - jvmArgs("-XX:+IgnoreUnrecognizedVMOptions") + named("compileTestSpring3Java") { + sourceCompatibility = "17" + targetCompatibility = "17" + options.release.set(17) + } + + named("testSpring3") { + isEnabled = testSpring3 + } - // disable tests on openj9 18 because they often crash JIT compiler - val testJavaVersion = gradle.startParameter.projectProperties["testJavaVersion"]?.let(JavaVersion::toVersion) - val testOnOpenJ9 = gradle.startParameter.projectProperties["testJavaVM"]?.run { this == "openj9" } - ?: false - if (testOnOpenJ9 && testJavaVersion?.majorVersion == "18") { - enabled = false + withType(Jar::class) { + from(sourceSets["javaSpring3"].output) } } diff --git a/instrumentation/spring/spring-boot-autoconfigure/gradle.properties b/instrumentation/spring/spring-boot-autoconfigure/gradle.properties new file mode 100644 index 000000000000..45d64bec279d --- /dev/null +++ b/instrumentation/spring/spring-boot-autoconfigure/gradle.properties @@ -0,0 +1 @@ +otel.stable=true diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/EnableOpenTelemetry.java b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/EnableOpenTelemetry.java deleted file mode 100644 index 8a5b2b95c9fa..000000000000 --- a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/EnableOpenTelemetry.java +++ /dev/null @@ -1,20 +0,0 @@ -/* - * 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/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/MetricExportProperties.java b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/MetricExportProperties.java deleted file mode 100644 index ef1090b68a5b..000000000000 --- a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/MetricExportProperties.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.spring.autoconfigure; - -import java.time.Duration; -import javax.annotation.Nullable; -import org.springframework.boot.context.properties.ConfigurationProperties; - -@ConfigurationProperties(prefix = "otel.metric.export") -public class MetricExportProperties { - - @Nullable private Duration interval; - - @Nullable - public Duration getInterval() { - return interval; - } - - public void setInterval(@Nullable Duration interval) { - this.interval = interval; - } -} diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/OpenTelemetryAutoConfiguration.java b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/OpenTelemetryAutoConfiguration.java index 68e1e5be6121..eaa97f6c23a4 100644 --- a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/OpenTelemetryAutoConfiguration.java +++ b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/OpenTelemetryAutoConfiguration.java @@ -5,39 +5,40 @@ package io.opentelemetry.instrumentation.spring.autoconfigure; -import io.opentelemetry.api.GlobalOpenTelemetry; import io.opentelemetry.api.OpenTelemetry; import io.opentelemetry.api.trace.TracerProvider; -import io.opentelemetry.context.propagation.ContextPropagators; -import io.opentelemetry.instrumentation.spring.autoconfigure.resources.SpringResourceConfigProperties; -import io.opentelemetry.sdk.OpenTelemetrySdk; +import io.opentelemetry.instrumentation.spring.autoconfigure.internal.MapConverter; +import io.opentelemetry.instrumentation.spring.autoconfigure.internal.SdkEnabled; +import io.opentelemetry.instrumentation.spring.autoconfigure.internal.properties.OtelResourceProperties; +import io.opentelemetry.instrumentation.spring.autoconfigure.internal.properties.OtlpExporterProperties; +import io.opentelemetry.instrumentation.spring.autoconfigure.internal.properties.PropagationProperties; +import io.opentelemetry.instrumentation.spring.autoconfigure.internal.properties.SpringConfigProperties; +import io.opentelemetry.instrumentation.spring.autoconfigure.internal.resources.DistroVersionResourceProvider; +import io.opentelemetry.instrumentation.spring.autoconfigure.internal.resources.SpringResourceProvider; +import io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdk; +import io.opentelemetry.sdk.autoconfigure.internal.AutoConfigureUtil; +import io.opentelemetry.sdk.autoconfigure.internal.ComponentLoader; +import io.opentelemetry.sdk.autoconfigure.internal.SpiHelper; import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; import io.opentelemetry.sdk.autoconfigure.spi.ResourceProvider; -import io.opentelemetry.sdk.logs.SdkLoggerProvider; -import io.opentelemetry.sdk.logs.SdkLoggerProviderBuilder; -import io.opentelemetry.sdk.logs.export.BatchLogRecordProcessor; -import io.opentelemetry.sdk.logs.export.LogRecordExporter; -import io.opentelemetry.sdk.metrics.SdkMeterProvider; -import io.opentelemetry.sdk.metrics.SdkMeterProviderBuilder; -import io.opentelemetry.sdk.metrics.export.MetricExporter; -import io.opentelemetry.sdk.metrics.export.PeriodicMetricReader; -import io.opentelemetry.sdk.metrics.export.PeriodicMetricReaderBuilder; -import io.opentelemetry.sdk.resources.Resource; -import io.opentelemetry.sdk.trace.SdkTracerProvider; -import io.opentelemetry.sdk.trace.SdkTracerProviderBuilder; -import io.opentelemetry.sdk.trace.export.BatchSpanProcessor; -import io.opentelemetry.sdk.trace.export.SpanExporter; -import io.opentelemetry.sdk.trace.samplers.Sampler; +import io.opentelemetry.sdk.autoconfigure.spi.internal.DefaultConfigProperties; import java.util.Collections; import java.util.List; -import org.springframework.beans.factory.ObjectProvider; +import java.util.Optional; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.ConfigurationPropertiesBinding; import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.info.BuildProperties; +import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; import org.springframework.core.env.Environment; -import org.springframework.expression.spel.standard.SpelExpressionParser; /** * Create {@link io.opentelemetry.api.OpenTelemetry} bean if bean is missing. @@ -47,120 +48,134 @@ *

    Updates the sampler probability for the configured {@link TracerProvider}. */ @Configuration -@EnableConfigurationProperties({MetricExportProperties.class, SamplerProperties.class}) +@EnableConfigurationProperties({ + OtlpExporterProperties.class, + OtelResourceProperties.class, + PropagationProperties.class +}) public class OpenTelemetryAutoConfiguration { public OpenTelemetryAutoConfiguration() {} @Configuration + @Conditional(SdkEnabled.class) @ConditionalOnMissingBean(OpenTelemetry.class) - @ConditionalOnProperty(name = "otel.sdk.disabled", havingValue = "false", matchIfMissing = true) - public static class OpenTelemetrySdkConfig { + static class OpenTelemetrySdkConfig { @Bean - @ConditionalOnMissingBean - public SdkTracerProvider sdkTracerProvider( - SamplerProperties samplerProperties, - ObjectProvider> spanExportersProvider, - Resource otelResource) { - SdkTracerProviderBuilder tracerProviderBuilder = SdkTracerProvider.builder(); - - spanExportersProvider.getIfAvailable(Collections::emptyList).stream() - .map(spanExporter -> BatchSpanProcessor.builder(spanExporter).build()) - .forEach(tracerProviderBuilder::addSpanProcessor); - - return tracerProviderBuilder - .setResource(otelResource) - .setSampler(Sampler.traceIdRatioBased(samplerProperties.getProbability())) - .build(); + @ConfigurationPropertiesBinding + public MapConverter mapConverter() { + // needed for otlp exporter headers and OtelResourceProperties + return new MapConverter(); } @Bean - @ConditionalOnMissingBean - public SdkLoggerProvider sdkLoggerProvider( - ObjectProvider> loggerExportersProvider, Resource otelResource) { - - SdkLoggerProviderBuilder loggerProviderBuilder = SdkLoggerProvider.builder(); - loggerProviderBuilder.setResource(otelResource); - - loggerExportersProvider - .getIfAvailable(Collections::emptyList) - .forEach( - loggerExporter -> - loggerProviderBuilder.addLogRecordProcessor( - BatchLogRecordProcessor.builder(loggerExporter).build())); - - return loggerProviderBuilder.build(); + public OpenTelemetrySdkComponentLoader openTelemetrySdkComponentLoader( + ApplicationContext applicationContext) { + return new OpenTelemetrySdkComponentLoader(applicationContext); } @Bean - @ConditionalOnMissingBean - public SdkMeterProvider sdkMeterProvider( - MetricExportProperties properties, - ObjectProvider> metricExportersProvider, - Resource otelResource) { - - SdkMeterProviderBuilder meterProviderBuilder = SdkMeterProvider.builder(); - - metricExportersProvider.getIfAvailable(Collections::emptyList).stream() - .map(metricExporter -> createPeriodicMetricReader(properties, metricExporter)) - .forEach(meterProviderBuilder::registerMetricReader); - - return meterProviderBuilder.setResource(otelResource).build(); + public ResourceProvider otelSpringResourceProvider(Optional buildProperties) { + return new SpringResourceProvider(buildProperties); } - private static PeriodicMetricReader createPeriodicMetricReader( - MetricExportProperties properties, MetricExporter metricExporter) { - PeriodicMetricReaderBuilder metricReaderBuilder = - PeriodicMetricReader.builder(metricExporter); - if (properties.getInterval() != null) { - metricReaderBuilder.setInterval(properties.getInterval()); - } - return metricReaderBuilder.build(); + @Bean + public ResourceProvider otelDistroVersionResourceProvider() { + return new DistroVersionResourceProvider(); } @Bean - @ConditionalOnMissingBean - public Resource otelResource( - Environment env, ObjectProvider> resourceProviders) { - ConfigProperties config = new SpringResourceConfigProperties(env, new SpelExpressionParser()); - Resource resource = Resource.getDefault(); - for (ResourceProvider resourceProvider : - resourceProviders.getIfAvailable(Collections::emptyList)) { - resource = resource.merge(resourceProvider.createResource(config)); - } - return resource; + public AutoConfiguredOpenTelemetrySdk autoConfiguredOpenTelemetrySdk( + Environment env, + OtlpExporterProperties otlpExporterProperties, + OtelResourceProperties resourceProperties, + PropagationProperties propagationProperties, + OpenTelemetrySdkComponentLoader componentLoader) { + + return AutoConfigureUtil.setComponentLoader( + AutoConfigureUtil.setConfigPropertiesCustomizer( + AutoConfiguredOpenTelemetrySdk.builder(), + c -> + SpringConfigProperties.create( + env, + otlpExporterProperties, + resourceProperties, + propagationProperties, + c)), + componentLoader) + .build(); } @Bean public OpenTelemetry openTelemetry( - ObjectProvider propagatorsProvider, - SdkTracerProvider tracerProvider, - SdkMeterProvider meterProvider, - SdkLoggerProvider loggerProvider) { - - ContextPropagators propagators = propagatorsProvider.getIfAvailable(ContextPropagators::noop); - - // global is needed for logging appenders - GlobalOpenTelemetry.set(OpenTelemetrySdk.builder().setLoggerProvider(loggerProvider).build()); + AutoConfiguredOpenTelemetrySdk autoConfiguredOpenTelemetrySdk) { + return autoConfiguredOpenTelemetrySdk.getOpenTelemetrySdk(); + } - return OpenTelemetrySdk.builder() - .setTracerProvider(tracerProvider) - .setMeterProvider(meterProvider) - .setLoggerProvider(loggerProvider) - .setPropagators(propagators) - .build(); + /** + * Expose the {@link ConfigProperties} bean for use in other auto-configurations. + * + *

    Not using spring boot properties directly in order to support {@link + * io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizer#addPropertiesCustomizer(Function)} + * and {@link + * io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizer#addPropertiesSupplier(Supplier)}. + */ + @Bean + public ConfigProperties otelProperties( + AutoConfiguredOpenTelemetrySdk autoConfiguredOpenTelemetrySdk) { + return AutoConfigureUtil.getConfig(autoConfiguredOpenTelemetrySdk); } } @Configuration @ConditionalOnMissingBean(OpenTelemetry.class) @ConditionalOnProperty(name = "otel.sdk.disabled", havingValue = "true") - public static class DisabledOpenTelemetrySdkConfig { + static class DisabledOpenTelemetrySdkConfig { @Bean public OpenTelemetry openTelemetry() { return OpenTelemetry.noop(); } + + @Bean + public ConfigProperties otelProperties() { + return DefaultConfigProperties.createFromMap(Collections.emptyMap()); + } + } + + @Configuration + @ConditionalOnBean(OpenTelemetry.class) + @ConditionalOnMissingBean({ConfigProperties.class}) + static class FallbackConfigProperties { + @Bean + public ConfigProperties otelProperties() { + return DefaultConfigProperties.create(Collections.emptyMap()); + } + } + + /** + * The {@link ComponentLoader} is used by the SDK autoconfiguration to load all components, e.g. + * here + */ + static class OpenTelemetrySdkComponentLoader implements ComponentLoader { + private final ApplicationContext applicationContext; + + private final SpiHelper spiHelper = + SpiHelper.create(OpenTelemetrySdkComponentLoader.class.getClassLoader()); + + public OpenTelemetrySdkComponentLoader(ApplicationContext applicationContext) { + this.applicationContext = applicationContext; + } + + @Override + public Iterable load(Class spiClass) { + List spi = spiHelper.load(spiClass); + List beans = + applicationContext.getBeanProvider(spiClass).orderedStream().collect(Collectors.toList()); + spi.addAll(beans); + return spi; + } } } diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/SamplerProperties.java b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/SamplerProperties.java deleted file mode 100644 index 7d8de1918ffb..000000000000 --- a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/SamplerProperties.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * 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/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/aspects/SdkExtensionWithSpanAspect.java b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/aspects/SdkExtensionWithSpanAspect.java deleted file mode 100644 index 5b6db318f564..000000000000 --- a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/aspects/SdkExtensionWithSpanAspect.java +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.spring.autoconfigure.aspects; - -import io.opentelemetry.api.OpenTelemetry; -import org.aspectj.lang.ProceedingJoinPoint; -import org.aspectj.lang.annotation.Around; -import org.aspectj.lang.annotation.Aspect; -import org.springframework.core.ParameterNameDiscoverer; - -@Aspect -class SdkExtensionWithSpanAspect extends WithSpanAspect { - - SdkExtensionWithSpanAspect( - OpenTelemetry openTelemetry, ParameterNameDiscoverer parameterNameDiscoverer) { - super( - openTelemetry, - parameterNameDiscoverer, - new JoinPointRequest.SdkExtensionAnnotationFactory(), - new WithSpanAspectParameterAttributeNamesExtractor - .SdkExtensionAnnotationAttributeNameSupplier()); - } - - @Override - @Around("@annotation(io.opentelemetry.extension.annotations.WithSpan)") - public Object traceMethod(ProceedingJoinPoint pjp) throws Throwable { - return super.traceMethod(pjp); - } -} diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/aspects/TraceAspectAutoConfiguration.java b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/aspects/TraceAspectAutoConfiguration.java deleted file mode 100644 index 2f568eebe7fd..000000000000 --- a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/aspects/TraceAspectAutoConfiguration.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * 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.instrumentation.annotations.WithSpan; -import org.aspectj.lang.annotation.Aspect; -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; -import org.springframework.core.DefaultParameterNameDiscoverer; -import org.springframework.core.ParameterNameDiscoverer; - -/** 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) -public class TraceAspectAutoConfiguration { - - @Bean - @ConditionalOnMissingBean - public ParameterNameDiscoverer parameterNameDiscoverer() { - return new DefaultParameterNameDiscoverer(); - } - - @Bean - @ConditionalOnClass(WithSpan.class) - public InstrumentationWithSpanAspect instrumentationWithSpanAspect( - OpenTelemetry openTelemetry, ParameterNameDiscoverer parameterNameDiscoverer) { - return new InstrumentationWithSpanAspect(openTelemetry, parameterNameDiscoverer); - } - - @Bean - @SuppressWarnings("deprecation") // instrumenting deprecated class for backwards compatibility - @ConditionalOnClass(io.opentelemetry.extension.annotations.WithSpan.class) - public SdkExtensionWithSpanAspect sdkExtensionWithSpanAspect( - OpenTelemetry openTelemetry, ParameterNameDiscoverer parameterNameDiscoverer) { - return new SdkExtensionWithSpanAspect(openTelemetry, parameterNameDiscoverer); - } -} diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/aspects/TraceAspectProperties.java b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/aspects/TraceAspectProperties.java deleted file mode 100644 index 3bd0ffb3625d..000000000000 --- a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/aspects/TraceAspectProperties.java +++ /dev/null @@ -1,22 +0,0 @@ -/* - * 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/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/exporters/jaeger/JaegerSpanExporterAutoConfiguration.java b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/exporters/jaeger/JaegerSpanExporterAutoConfiguration.java deleted file mode 100644 index dc017ff3396b..000000000000 --- a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/exporters/jaeger/JaegerSpanExporterAutoConfiguration.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.spring.autoconfigure.exporters.jaeger; - -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) -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/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/exporters/jaeger/JaegerSpanExporterProperties.java b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/exporters/jaeger/JaegerSpanExporterProperties.java deleted file mode 100644 index 9c9c0baf3b88..000000000000 --- a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/exporters/jaeger/JaegerSpanExporterProperties.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.spring.autoconfigure.exporters.jaeger; - -import java.time.Duration; -import javax.annotation.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/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/exporters/logging/LoggingExporterProperties.java b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/exporters/logging/LoggingExporterProperties.java deleted file mode 100644 index c0e8402f6b91..000000000000 --- a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/exporters/logging/LoggingExporterProperties.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * 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} and {@link - * io.opentelemetry.exporter.logging.LoggingMetricExporter}. - */ -@ConfigurationProperties(prefix = "otel.exporter.logging") -public final class LoggingExporterProperties { - - private boolean enabled = false; - private final SignalProperties traces = new SignalProperties(); - private final SignalProperties metrics = new SignalProperties(); - - public boolean isEnabled() { - return enabled; - } - - public void setEnabled(boolean enabled) { - this.enabled = enabled; - } - - public SignalProperties getTraces() { - return traces; - } - - public SignalProperties getMetrics() { - return metrics; - } - - public static class SignalProperties { - - private boolean enabled = false; - - public boolean isEnabled() { - return enabled; - } - - public void setEnabled(boolean enabled) { - this.enabled = enabled; - } - } -} diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/exporters/logging/LoggingMetricExporterAutoConfiguration.java b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/exporters/logging/LoggingMetricExporterAutoConfiguration.java deleted file mode 100644 index 860e97905331..000000000000 --- a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/exporters/logging/LoggingMetricExporterAutoConfiguration.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.spring.autoconfigure.exporters.logging; - -import io.opentelemetry.exporter.logging.LoggingMetricExporter; -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.AnyNestedCondition; -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.Conditional; -import org.springframework.context.annotation.Configuration; - -/** Configures {@link LoggingSpanExporter} bean for tracing. */ -@Configuration -@EnableConfigurationProperties(LoggingExporterProperties.class) -@AutoConfigureBefore(OpenTelemetryAutoConfiguration.class) -@Conditional(LoggingMetricExporterAutoConfiguration.AnyPropertyEnabled.class) -@ConditionalOnClass(LoggingMetricExporter.class) -public class LoggingMetricExporterAutoConfiguration { - - @Bean - public LoggingMetricExporter otelLoggingMetricExporter() { - return LoggingMetricExporter.create(); - } - - static final class AnyPropertyEnabled extends AnyNestedCondition { - - AnyPropertyEnabled() { - super(ConfigurationPhase.PARSE_CONFIGURATION); - } - - @ConditionalOnProperty("otel.exporter.logging.enabled") - static class LoggingEnabled {} - - @ConditionalOnProperty("otel.exporter.logging.metrics.enabled") - static class LoggingMetricsEnabled {} - } -} diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/exporters/logging/LoggingSpanExporterAutoConfiguration.java b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/exporters/logging/LoggingSpanExporterAutoConfiguration.java deleted file mode 100644 index a8ac9c501f34..000000000000 --- a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/exporters/logging/LoggingSpanExporterAutoConfiguration.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * 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.AnyNestedCondition; -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.Conditional; -import org.springframework.context.annotation.Configuration; - -/** Configures {@link LoggingSpanExporter} bean for tracing. */ -@Configuration -@EnableConfigurationProperties(LoggingExporterProperties.class) -@AutoConfigureBefore(OpenTelemetryAutoConfiguration.class) -@Conditional(LoggingSpanExporterAutoConfiguration.AnyPropertyEnabled.class) -@ConditionalOnClass(LoggingSpanExporter.class) -public class LoggingSpanExporterAutoConfiguration { - - @Bean - @ConditionalOnMissingBean - public LoggingSpanExporter otelLoggingSpanExporter() { - return LoggingSpanExporter.create(); - } - - static final class AnyPropertyEnabled extends AnyNestedCondition { - - AnyPropertyEnabled() { - super(ConfigurationPhase.PARSE_CONFIGURATION); - } - - @ConditionalOnProperty("otel.exporter.logging.enabled") - static class LoggingEnabled {} - - @ConditionalOnProperty("otel.exporter.logging.traces.enabled") - static class LoggingTracesEnabled {} - } -} diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/exporters/otlp/OtlpExporterProperties.java b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/exporters/otlp/OtlpExporterProperties.java deleted file mode 100644 index 6fed282b81f6..000000000000 --- a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/exporters/otlp/OtlpExporterProperties.java +++ /dev/null @@ -1,102 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.spring.autoconfigure.exporters.otlp; - -import java.time.Duration; -import javax.annotation.Nullable; -import org.springframework.boot.context.properties.ConfigurationProperties; - -/** - * Configuration for {@link io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporter} and {@link - * io.opentelemetry.exporter.otlp.metrics.OtlpGrpcMetricExporter}. - * - *

    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 OtlpExporterProperties { - - private boolean enabled = true; - @Nullable private String endpoint; - @Nullable private Duration timeout; - private final SignalProperties traces = new SignalProperties(); - private final SignalProperties metrics = new SignalProperties(); - private final SignalProperties logs = new SignalProperties(); - - 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; - } - - public SignalProperties getTraces() { - return traces; - } - - public SignalProperties getMetrics() { - return metrics; - } - - public SignalProperties getLogs() { - return logs; - } - - public static class SignalProperties { - - 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(@Nullable String endpoint) { - this.endpoint = endpoint; - } - - @Nullable - public Duration getTimeout() { - return timeout; - } - - public void setTimeout(@Nullable Duration timeout) { - this.timeout = timeout; - } - } -} diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/exporters/otlp/OtlpLoggerExporterAutoConfiguration.java b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/exporters/otlp/OtlpLoggerExporterAutoConfiguration.java deleted file mode 100644 index 02636a5a2af2..000000000000 --- a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/exporters/otlp/OtlpLoggerExporterAutoConfiguration.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.spring.autoconfigure.exporters.otlp; - -import io.opentelemetry.exporter.otlp.logs.OtlpGrpcLogRecordExporter; -import io.opentelemetry.exporter.otlp.logs.OtlpGrpcLogRecordExporterBuilder; -import io.opentelemetry.instrumentation.spring.autoconfigure.OpenTelemetryAutoConfiguration; -import java.time.Duration; -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; - -@AutoConfigureBefore(OpenTelemetryAutoConfiguration.class) -@EnableConfigurationProperties(OtlpExporterProperties.class) -@ConditionalOnProperty( - prefix = "otel.exporter.otlp", - name = {"enabled", "logs.enabled"}, - matchIfMissing = true) -@ConditionalOnClass(OtlpGrpcLogRecordExporter.class) -public class OtlpLoggerExporterAutoConfiguration { - - @Bean - @ConditionalOnMissingBean - public OtlpGrpcLogRecordExporter otelOtlpGrpcLogRecordExporter( - OtlpExporterProperties properties) { - OtlpGrpcLogRecordExporterBuilder builder = OtlpGrpcLogRecordExporter.builder(); - - String endpoint = properties.getLogs().getEndpoint(); - if (endpoint == null) { - endpoint = properties.getEndpoint(); - } - if (endpoint != null) { - builder.setEndpoint(endpoint); - } - - Duration timeout = properties.getLogs().getTimeout(); - if (timeout == null) { - timeout = properties.getTimeout(); - } - if (timeout != null) { - builder.setTimeout(timeout); - } - - return builder.build(); - } -} diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/exporters/otlp/OtlpMetricExporterAutoConfiguration.java b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/exporters/otlp/OtlpMetricExporterAutoConfiguration.java deleted file mode 100644 index e8580b50ecfa..000000000000 --- a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/exporters/otlp/OtlpMetricExporterAutoConfiguration.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.spring.autoconfigure.exporters.otlp; - -import io.opentelemetry.exporter.otlp.metrics.OtlpGrpcMetricExporter; -import io.opentelemetry.exporter.otlp.metrics.OtlpGrpcMetricExporterBuilder; -import io.opentelemetry.instrumentation.spring.autoconfigure.OpenTelemetryAutoConfiguration; -import java.time.Duration; -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; - -@Configuration -@AutoConfigureBefore(OpenTelemetryAutoConfiguration.class) -@EnableConfigurationProperties(OtlpExporterProperties.class) -@ConditionalOnProperty( - prefix = "otel.exporter.otlp", - name = {"enabled", "metrics.enabled"}, - matchIfMissing = true) -@ConditionalOnClass(OtlpGrpcMetricExporter.class) -public class OtlpMetricExporterAutoConfiguration { - - @Bean - @ConditionalOnMissingBean - public OtlpGrpcMetricExporter otelOtlpGrpcMetricExporter(OtlpExporterProperties properties) { - OtlpGrpcMetricExporterBuilder builder = OtlpGrpcMetricExporter.builder(); - - String endpoint = properties.getMetrics().getEndpoint(); - if (endpoint == null) { - endpoint = properties.getEndpoint(); - } - if (endpoint != null) { - builder.setEndpoint(endpoint); - } - - Duration timeout = properties.getMetrics().getTimeout(); - if (timeout == null) { - timeout = properties.getTimeout(); - } - if (timeout != null) { - builder.setTimeout(timeout); - } - - return builder.build(); - } -} diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/exporters/otlp/OtlpSpanExporterAutoConfiguration.java b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/exporters/otlp/OtlpSpanExporterAutoConfiguration.java deleted file mode 100644 index ace7fe720d7e..000000000000 --- a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/exporters/otlp/OtlpSpanExporterAutoConfiguration.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.spring.autoconfigure.exporters.otlp; - -import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporter; -import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporterBuilder; -import io.opentelemetry.instrumentation.spring.autoconfigure.OpenTelemetryAutoConfiguration; -import java.time.Duration; -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(OtlpExporterProperties.class) -@ConditionalOnProperty( - prefix = "otel.exporter.otlp", - name = {"enabled", "traces.enabled"}, - matchIfMissing = true) -@ConditionalOnClass(OtlpGrpcSpanExporter.class) -public class OtlpSpanExporterAutoConfiguration { - - @Bean - @ConditionalOnMissingBean - public OtlpGrpcSpanExporter otelOtlpGrpcSpanExporter(OtlpExporterProperties properties) { - OtlpGrpcSpanExporterBuilder builder = OtlpGrpcSpanExporter.builder(); - - String endpoint = properties.getTraces().getEndpoint(); - if (endpoint == null) { - endpoint = properties.getEndpoint(); - } - if (endpoint != null) { - builder.setEndpoint(endpoint); - } - - Duration timeout = properties.getTraces().getTimeout(); - if (timeout == null) { - timeout = properties.getTimeout(); - } - if (timeout != null) { - builder.setTimeout(timeout); - } - - return builder.build(); - } -} diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/exporters/zipkin/ZipkinSpanExporterAutoConfiguration.java b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/exporters/zipkin/ZipkinSpanExporterAutoConfiguration.java deleted file mode 100644 index 82ca5298af25..000000000000 --- a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/exporters/zipkin/ZipkinSpanExporterAutoConfiguration.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * 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/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/exporters/zipkin/ZipkinSpanExporterProperties.java b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/exporters/zipkin/ZipkinSpanExporterProperties.java deleted file mode 100644 index c6a7e412d8fb..000000000000 --- a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/exporters/zipkin/ZipkinSpanExporterProperties.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.spring.autoconfigure.exporters.zipkin; - -import javax.annotation.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/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/httpclients/HttpClientsProperties.java b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/httpclients/HttpClientsProperties.java deleted file mode 100644 index 05d02f0547b1..000000000000 --- a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/httpclients/HttpClientsProperties.java +++ /dev/null @@ -1,27 +0,0 @@ -/* - * 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/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/httpclients/resttemplate/RestTemplateAutoConfiguration.java b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/httpclients/resttemplate/RestTemplateAutoConfiguration.java deleted file mode 100644 index d6d1e79a1058..000000000000 --- a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/httpclients/resttemplate/RestTemplateAutoConfiguration.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * 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/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/httpclients/resttemplate/RestTemplateBeanPostProcessor.java b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/httpclients/resttemplate/RestTemplateBeanPostProcessor.java deleted file mode 100644 index 9140c0987edb..000000000000 --- a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/httpclients/resttemplate/RestTemplateBeanPostProcessor.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * 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.web.v3_1.SpringWebTelemetry; -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) { - ClientHttpRequestInterceptor interceptor = - SpringWebTelemetry.create(openTelemetry).newInterceptor(); - addRestTemplateInterceptorIfNotPresent(restTemplate, interceptor); - } - return restTemplate; - } - - private static void addRestTemplateInterceptorIfNotPresent( - RestTemplate restTemplate, ClientHttpRequestInterceptor instrumentationInterceptor) { - List restTemplateInterceptors = restTemplate.getInterceptors(); - if (restTemplateInterceptors.stream() - .noneMatch( - interceptor -> interceptor.getClass() == instrumentationInterceptor.getClass())) { - restTemplateInterceptors.add(0, instrumentationInterceptor); - } - } -} diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/httpclients/webclient/WebClientAutoConfiguration.java b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/httpclients/webclient/WebClientAutoConfiguration.java deleted file mode 100644 index 3497aae7a128..000000000000 --- a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/httpclients/webclient/WebClientAutoConfiguration.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * 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/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/httpclients/webclient/WebClientBeanPostProcessor.java b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/httpclients/webclient/WebClientBeanPostProcessor.java deleted file mode 100644 index cbcd7f94505b..000000000000 --- a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/httpclients/webclient/WebClientBeanPostProcessor.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * 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.v5_3.SpringWebfluxTelemetry; -import org.springframework.beans.factory.ObjectProvider; -import org.springframework.beans.factory.config.BeanPostProcessor; -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) { - SpringWebfluxTelemetry instrumentation = SpringWebfluxTelemetry.create(openTelemetry); - return webClientBuilder.filters(instrumentation::addClientTracingFilter); - } else { - return webClientBuilder; - } - } -} diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/ConditionalOnEnabledInstrumentation.java b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/ConditionalOnEnabledInstrumentation.java new file mode 100644 index 000000000000..7c55f430d1e4 --- /dev/null +++ b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/ConditionalOnEnabledInstrumentation.java @@ -0,0 +1,28 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.autoconfigure.internal; + +import io.opentelemetry.api.OpenTelemetry; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.context.annotation.Conditional; + +/** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time. + */ +@Target({ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@ConditionalOnBean(OpenTelemetry.class) +@Conditional({SdkEnabled.class, InstrumentationPropertyEnabled.class}) +public @interface ConditionalOnEnabledInstrumentation { + String module(); + + boolean enabledByDefault() default true; +} diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/InstrumentationPropertyEnabled.java b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/InstrumentationPropertyEnabled.java new file mode 100644 index 000000000000..87687f668b6c --- /dev/null +++ b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/InstrumentationPropertyEnabled.java @@ -0,0 +1,37 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.autoconfigure.internal; + +import java.util.Map; +import org.springframework.context.annotation.Condition; +import org.springframework.context.annotation.ConditionContext; +import org.springframework.core.type.AnnotatedTypeMetadata; + +/** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time. + */ +public class InstrumentationPropertyEnabled implements Condition { + + @Override + public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { + Map attributes = + metadata.getAnnotationAttributes(ConditionalOnEnabledInstrumentation.class.getName()); + + String name = String.format("otel.instrumentation.%s.enabled", attributes.get("module")); + Boolean explicit = context.getEnvironment().getProperty(name, Boolean.class); + if (explicit != null) { + return explicit; + } + boolean defaultValue = (boolean) attributes.get("enabledByDefault"); + if (!defaultValue) { + return false; + } + return context + .getEnvironment() + .getProperty("otel.instrumentation.common.default-enabled", Boolean.class, true); + } +} diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/MapConverter.java b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/MapConverter.java new file mode 100644 index 000000000000..86e043bf1696 --- /dev/null +++ b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/MapConverter.java @@ -0,0 +1,34 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.autoconfigure.internal; + +import io.opentelemetry.sdk.autoconfigure.spi.internal.DefaultConfigProperties; +import java.util.Collections; +import java.util.Map; +import org.springframework.core.convert.converter.Converter; + +/** + * The MapConverter class is used to convert a String to a Map. The String is expected to be in the + * format of a comma separated list of key=value pairs, e.g. key1=value1,key2=value2. + * + *

    This is the expected format for the OTEL_RESOURCE_ATTRIBUTES and + * OTEL_EXPORTER_OTLP_HEADERS environment variables. + * + *

    This class is internal and is hence not for public use. Its APIs are unstable and can change + * at any time. + */ +public class MapConverter implements Converter> { + + public static final String KEY = "key"; + + @Override + public Map convert(String source) { + DefaultConfigProperties properties = + DefaultConfigProperties.createFromMap(Collections.singletonMap(KEY, source)); + + return properties.getMap(KEY); + } +} diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/SdkEnabled.java b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/SdkEnabled.java new file mode 100644 index 000000000000..082bdebc00b7 --- /dev/null +++ b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/SdkEnabled.java @@ -0,0 +1,22 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.autoconfigure.internal; + +import org.springframework.boot.autoconfigure.condition.AnyNestedCondition; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; + +/** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time. + */ +public class SdkEnabled extends AnyNestedCondition { + public SdkEnabled() { + super(ConfigurationPhase.PARSE_CONFIGURATION); + } + + @ConditionalOnProperty(name = "otel.sdk.disabled", havingValue = "false", matchIfMissing = true) + static class NotDisabled {} +} diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/annotations/InstrumentationAnnotationsAutoConfiguration.java b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/annotations/InstrumentationAnnotationsAutoConfiguration.java new file mode 100644 index 000000000000..0e8081622388 --- /dev/null +++ b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/annotations/InstrumentationAnnotationsAutoConfiguration.java @@ -0,0 +1,36 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.annotations; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.instrumentation.annotations.WithSpan; +import io.opentelemetry.instrumentation.spring.autoconfigure.internal.ConditionalOnEnabledInstrumentation; +import org.aspectj.lang.annotation.Aspect; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.DefaultParameterNameDiscoverer; +import org.springframework.core.ParameterNameDiscoverer; + +/** + * Configures {@link WithSpanAspect} to trace bean methods annotated with {@link WithSpan}. + * + *

    This class is internal and is hence not for public use. Its APIs are unstable and can change + * at any time. + */ +@ConditionalOnEnabledInstrumentation(module = "annotations") +@ConditionalOnClass(Aspect.class) +@Configuration +public class InstrumentationAnnotationsAutoConfiguration { + private final ParameterNameDiscoverer parameterNameDiscoverer = + new DefaultParameterNameDiscoverer(); + + @Bean + @ConditionalOnClass(WithSpan.class) + InstrumentationWithSpanAspect otelInstrumentationWithSpanAspect(OpenTelemetry openTelemetry) { + return new InstrumentationWithSpanAspect(openTelemetry, parameterNameDiscoverer); + } +} diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/aspects/InstrumentationWithSpanAspect.java b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/annotations/InstrumentationWithSpanAspect.java similarity index 90% rename from instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/aspects/InstrumentationWithSpanAspect.java rename to instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/annotations/InstrumentationWithSpanAspect.java index 568f55e22e73..90c8c2e229d5 100644 --- a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/aspects/InstrumentationWithSpanAspect.java +++ b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/annotations/InstrumentationWithSpanAspect.java @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.instrumentation.spring.autoconfigure.aspects; +package io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.annotations; import io.opentelemetry.api.OpenTelemetry; import org.aspectj.lang.ProceedingJoinPoint; diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/aspects/JoinPointRequest.java b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/annotations/JoinPointRequest.java similarity index 61% rename from instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/aspects/JoinPointRequest.java rename to instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/annotations/JoinPointRequest.java index 986e8d207d2f..01a6a62d9894 100644 --- a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/aspects/JoinPointRequest.java +++ b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/annotations/JoinPointRequest.java @@ -3,11 +3,11 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.instrumentation.spring.autoconfigure.aspects; +package io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.annotations; import io.opentelemetry.api.trace.SpanKind; import io.opentelemetry.instrumentation.annotations.WithSpan; -import io.opentelemetry.instrumentation.api.instrumenter.util.SpanNames; +import io.opentelemetry.instrumentation.api.incubator.semconv.util.SpanNames; import java.lang.reflect.Method; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.reflect.MethodSignature; @@ -69,25 +69,4 @@ public JoinPointRequest create(JoinPoint joinPoint) { return new JoinPointRequest(joinPoint, method, spanName, spanKind); } } - - static final class SdkExtensionAnnotationFactory implements Factory { - - @Override - @SuppressWarnings("deprecation") // instrumenting deprecated class for backwards compatibility - public JoinPointRequest create(JoinPoint joinPoint) { - MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature(); - Method method = methodSignature.getMethod(); - - // in rare cases, when interface method does not have annotations but the implementation does, - // and the AspectJ factory is configured to proxy interfaces, this class will receive the - // abstract interface method (without annotations) instead of the implementation method (with - // annotations); these defaults prevent NPEs in this scenario - io.opentelemetry.extension.annotations.WithSpan annotation = - method.getDeclaredAnnotation(io.opentelemetry.extension.annotations.WithSpan.class); - String spanName = annotation != null ? annotation.value() : ""; - SpanKind spanKind = annotation != null ? annotation.kind() : SpanKind.INTERNAL; - - return new JoinPointRequest(joinPoint, method, spanName, spanKind); - } - } } diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/aspects/JointPointCodeAttributesExtractor.java b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/annotations/JointPointCodeAttributesExtractor.java similarity index 71% rename from instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/aspects/JointPointCodeAttributesExtractor.java rename to instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/annotations/JointPointCodeAttributesExtractor.java index 032325515c3d..c0dc1f8be230 100644 --- a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/aspects/JointPointCodeAttributesExtractor.java +++ b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/annotations/JointPointCodeAttributesExtractor.java @@ -3,9 +3,9 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.instrumentation.spring.autoconfigure.aspects; +package io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.annotations; -import io.opentelemetry.instrumentation.api.instrumenter.code.CodeAttributesGetter; +import io.opentelemetry.instrumentation.api.incubator.semconv.code.CodeAttributesGetter; enum JointPointCodeAttributesExtractor implements CodeAttributesGetter { INSTANCE; diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/aspects/WithSpanAspect.java b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/annotations/WithSpanAspect.java similarity index 90% rename from instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/aspects/WithSpanAspect.java rename to instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/annotations/WithSpanAspect.java index bd508c212c5f..f804a820924f 100644 --- a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/aspects/WithSpanAspect.java +++ b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/annotations/WithSpanAspect.java @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.instrumentation.spring.autoconfigure.aspects; +package io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.annotations; import io.opentelemetry.api.OpenTelemetry; import io.opentelemetry.api.trace.Span; @@ -13,14 +13,13 @@ import io.opentelemetry.instrumentation.api.annotation.support.MethodSpanAttributesExtractor; import io.opentelemetry.instrumentation.api.annotation.support.ParameterAttributeNamesExtractor; import io.opentelemetry.instrumentation.api.annotation.support.async.AsyncOperationEndSupport; +import io.opentelemetry.instrumentation.api.incubator.semconv.code.CodeAttributesExtractor; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; -import io.opentelemetry.instrumentation.api.instrumenter.code.CodeAttributesExtractor; import org.aspectj.lang.ProceedingJoinPoint; import org.springframework.core.ParameterNameDiscoverer; /** - * Uses Spring-AOP to wrap methods marked by {@link WithSpan} (or the deprecated {@link - * io.opentelemetry.extension.annotations.WithSpan}) in a {@link Span}. + * Uses Spring-AOP to wrap methods marked by {@link WithSpan} in a {@link Span}. * *

    Ensure methods annotated with {@link WithSpan} are implemented on beans managed by the Spring * container. diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/aspects/WithSpanAspectParameterAttributeNamesExtractor.java b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/annotations/WithSpanAspectParameterAttributeNamesExtractor.java similarity index 79% rename from instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/aspects/WithSpanAspectParameterAttributeNamesExtractor.java rename to instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/annotations/WithSpanAspectParameterAttributeNamesExtractor.java index 12cc1b99f73d..7471986cf009 100644 --- a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/aspects/WithSpanAspectParameterAttributeNamesExtractor.java +++ b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/annotations/WithSpanAspectParameterAttributeNamesExtractor.java @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.instrumentation.spring.autoconfigure.aspects; +package io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.annotations; import io.opentelemetry.instrumentation.annotations.SpanAttribute; import io.opentelemetry.instrumentation.api.annotation.support.ParameterAttributeNamesExtractor; @@ -73,18 +73,4 @@ public String spanAttributeName(Parameter parameter) { return annotation == null ? null : annotation.value(); } } - - static final class SdkExtensionAnnotationAttributeNameSupplier - implements SpanAttributeNameSupplier { - - @Nullable - @Override - @SuppressWarnings("deprecation") // instrumenting deprecated class for backwards compatibility - public String spanAttributeName(Parameter parameter) { - io.opentelemetry.extension.annotations.SpanAttribute annotation = - parameter.getDeclaredAnnotation( - io.opentelemetry.extension.annotations.SpanAttribute.class); - return annotation == null ? null : annotation.value(); - } - } } diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/jdbc/DataSourcePostProcessor.java b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/jdbc/DataSourcePostProcessor.java new file mode 100644 index 000000000000..875bfa3509b7 --- /dev/null +++ b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/jdbc/DataSourcePostProcessor.java @@ -0,0 +1,53 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.jdbc; + +import com.google.errorprone.annotations.CanIgnoreReturnValue; +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.instrumentation.jdbc.datasource.JdbcTelemetry; +import io.opentelemetry.instrumentation.spring.autoconfigure.internal.properties.InstrumentationConfigUtil; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import javax.sql.DataSource; +import org.springframework.aop.scope.ScopedProxyUtils; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.core.Ordered; + +final class DataSourcePostProcessor implements BeanPostProcessor, Ordered { + + private final ObjectProvider openTelemetryProvider; + private final ObjectProvider configPropertiesProvider; + + DataSourcePostProcessor( + ObjectProvider openTelemetryProvider, + ObjectProvider configPropertiesProvider) { + this.openTelemetryProvider = openTelemetryProvider; + this.configPropertiesProvider = configPropertiesProvider; + } + + @CanIgnoreReturnValue + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) { + // Exclude scoped proxy beans to avoid double wrapping + if (bean instanceof DataSource && !ScopedProxyUtils.isScopedTarget(beanName)) { + DataSource dataSource = (DataSource) bean; + return JdbcTelemetry.builder(openTelemetryProvider.getObject()) + .setStatementSanitizationEnabled( + InstrumentationConfigUtil.isStatementSanitizationEnabled( + configPropertiesProvider.getObject(), + "otel.instrumentation.jdbc.statement-sanitizer.enabled")) + .build() + .wrap(dataSource); + } + return bean; + } + + // To be one of the first bean post-processors to be executed + @Override + public int getOrder() { + return Ordered.LOWEST_PRECEDENCE - 20; + } +} diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/jdbc/JdbcInstrumentationAutoConfiguration.java b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/jdbc/JdbcInstrumentationAutoConfiguration.java new file mode 100644 index 000000000000..3ec55a5a06c2 --- /dev/null +++ b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/jdbc/JdbcInstrumentationAutoConfiguration.java @@ -0,0 +1,39 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.jdbc; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.instrumentation.spring.autoconfigure.internal.ConditionalOnEnabledInstrumentation; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import javax.sql.DataSource; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time. + */ +@ConditionalOnEnabledInstrumentation(module = "jdbc") +@AutoConfiguration(after = DataSourceAutoConfiguration.class) +@ConditionalOnBean({DataSource.class}) +@Configuration(proxyBeanMethods = false) +public class JdbcInstrumentationAutoConfiguration { + + // For error prone + public JdbcInstrumentationAutoConfiguration() {} + + @Bean + // static to avoid "is not eligible for getting processed by all BeanPostProcessors" warning + static DataSourcePostProcessor dataSourcePostProcessor( + ObjectProvider openTelemetryProvider, + ObjectProvider configPropertiesProvider) { + return new DataSourcePostProcessor(openTelemetryProvider, configPropertiesProvider); + } +} diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/kafka/ConcurrentKafkaListenerContainerFactoryPostProcessor.java b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/kafka/ConcurrentKafkaListenerContainerFactoryPostProcessor.java new file mode 100644 index 000000000000..0842052409e7 --- /dev/null +++ b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/kafka/ConcurrentKafkaListenerContainerFactoryPostProcessor.java @@ -0,0 +1,47 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.kafka; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.instrumentation.spring.kafka.v2_7.SpringKafkaTelemetry; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory; + +class ConcurrentKafkaListenerContainerFactoryPostProcessor implements BeanPostProcessor { + + private final ObjectProvider openTelemetryProvider; + private final ObjectProvider configPropertiesProvider; + + ConcurrentKafkaListenerContainerFactoryPostProcessor( + ObjectProvider openTelemetryProvider, + ObjectProvider configPropertiesProvider) { + this.openTelemetryProvider = openTelemetryProvider; + this.configPropertiesProvider = configPropertiesProvider; + } + + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) { + if (!(bean instanceof ConcurrentKafkaListenerContainerFactory)) { + return bean; + } + + ConcurrentKafkaListenerContainerFactory listenerContainerFactory = + (ConcurrentKafkaListenerContainerFactory) bean; + SpringKafkaTelemetry springKafkaTelemetry = + SpringKafkaTelemetry.builder(openTelemetryProvider.getObject()) + .setCaptureExperimentalSpanAttributes( + configPropertiesProvider + .getObject() + .getBoolean("otel.instrumentation.kafka.experimental-span-attributes", false)) + .build(); + listenerContainerFactory.setBatchInterceptor(springKafkaTelemetry.createBatchInterceptor()); + listenerContainerFactory.setRecordInterceptor(springKafkaTelemetry.createRecordInterceptor()); + + return listenerContainerFactory; + } +} diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/kafka/KafkaInstrumentationAutoConfiguration.java b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/kafka/KafkaInstrumentationAutoConfiguration.java similarity index 51% rename from instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/kafka/KafkaInstrumentationAutoConfiguration.java rename to instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/kafka/KafkaInstrumentationAutoConfiguration.java index 878828f5fa94..739e0275201c 100644 --- a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/kafka/KafkaInstrumentationAutoConfiguration.java +++ b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/kafka/KafkaInstrumentationAutoConfiguration.java @@ -3,35 +3,43 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.instrumentation.spring.autoconfigure.kafka; +package io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.kafka; import io.opentelemetry.api.OpenTelemetry; import io.opentelemetry.instrumentation.kafkaclients.v2_6.KafkaTelemetry; +import io.opentelemetry.instrumentation.spring.autoconfigure.internal.ConditionalOnEnabledInstrumentation; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.kafka.DefaultKafkaProducerFactoryCustomizer; -import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory; import org.springframework.kafka.core.KafkaTemplate; -@Configuration -@EnableConfigurationProperties(KafkaInstrumentationProperties.class) -@ConditionalOnProperty(name = "otel.springboot.kafka.enabled", matchIfMissing = true) +/** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time. + */ +@ConditionalOnEnabledInstrumentation(module = "kafka") @ConditionalOnClass({KafkaTemplate.class, ConcurrentKafkaListenerContainerFactory.class}) +@Configuration public class KafkaInstrumentationAutoConfiguration { @Bean - public DefaultKafkaProducerFactoryCustomizer producerInstrumentation( + DefaultKafkaProducerFactoryCustomizer otelKafkaProducerFactoryCustomizer( OpenTelemetry openTelemetry) { KafkaTelemetry kafkaTelemetry = KafkaTelemetry.create(openTelemetry); return producerFactory -> producerFactory.addPostProcessor(kafkaTelemetry::wrap); } + // static to avoid "is not eligible for getting processed by all BeanPostProcessors" warning @Bean - public ConcurrentKafkaListenerContainerFactoryPostProcessor - concurrentKafkaListenerContainerFactoryPostProcessor(OpenTelemetry openTelemetry) { - return new ConcurrentKafkaListenerContainerFactoryPostProcessor(openTelemetry); + static ConcurrentKafkaListenerContainerFactoryPostProcessor + otelKafkaListenerContainerFactoryBeanPostProcessor( + ObjectProvider openTelemetryProvider, + ObjectProvider configPropertiesProvider) { + return new ConcurrentKafkaListenerContainerFactoryPostProcessor( + openTelemetryProvider, configPropertiesProvider); } } diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/logging/LogbackAppenderApplicationListener.java b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/logging/LogbackAppenderApplicationListener.java new file mode 100644 index 000000000000..442b4715bbd9 --- /dev/null +++ b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/logging/LogbackAppenderApplicationListener.java @@ -0,0 +1,76 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.logging; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent; +import org.springframework.boot.context.logging.LoggingApplicationListener; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationEvent; +import org.springframework.context.event.GenericApplicationListener; +import org.springframework.core.ResolvableType; + +/** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time. + */ +public class LogbackAppenderApplicationListener implements GenericApplicationListener { + + private static final Class[] SOURCE_TYPES = { + SpringApplication.class, ApplicationContext.class + }; + private static final Class[] EVENT_TYPES = {ApplicationEnvironmentPreparedEvent.class}; + private static final boolean LOGBACK_PRESENT = isLogbackPresent(); + + @Override + public boolean supportsSourceType(Class sourceType) { + return isAssignableFrom(sourceType, SOURCE_TYPES); + } + + @Override + public boolean supportsEventType(ResolvableType resolvableType) { + return isAssignableFrom(resolvableType.getRawClass(), EVENT_TYPES); + } + + private static boolean isAssignableFrom(Class type, Class... supportedTypes) { + if (type != null) { + for (Class supportedType : supportedTypes) { + if (supportedType.isAssignableFrom(type)) { + return true; + } + } + } + return false; + } + + @Override + public void onApplicationEvent(ApplicationEvent event) { + if (!LOGBACK_PRESENT) { + return; + } + + // Event for which org.springframework.boot.context.logging.LoggingApplicationListener + // initializes logging + if (event instanceof ApplicationEnvironmentPreparedEvent) { + LogbackAppenderInstaller.install((ApplicationEnvironmentPreparedEvent) event); + } + } + + @Override + public int getOrder() { + return LoggingApplicationListener.DEFAULT_ORDER + 1; // To execute this listener just after + // org.springframework.boot.context.logging.LoggingApplicationListener + } + + private static boolean isLogbackPresent() { + try { + Class.forName("ch.qos.logback.core.Appender"); + return true; + } catch (ClassNotFoundException exception) { + return false; + } + } +} diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/logging/LogbackAppenderInstaller.java b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/logging/LogbackAppenderInstaller.java new file mode 100644 index 000000000000..ceda38ec66c3 --- /dev/null +++ b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/logging/LogbackAppenderInstaller.java @@ -0,0 +1,156 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.logging; + +import ch.qos.logback.classic.LoggerContext; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.Appender; +import io.opentelemetry.instrumentation.logback.appender.v1_0.OpenTelemetryAppender; +import java.util.Iterator; +import java.util.Optional; +import org.slf4j.ILoggerFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent; + +class LogbackAppenderInstaller { + + static void install(ApplicationEnvironmentPreparedEvent applicationEnvironmentPreparedEvent) { + Optional existingOpenTelemetryAppender = findOpenTelemetryAppender(); + if (existingOpenTelemetryAppender.isPresent()) { + reInitializeOpenTelemetryAppender( + existingOpenTelemetryAppender, applicationEnvironmentPreparedEvent); + } else if (isLogbackAppenderAddable(applicationEnvironmentPreparedEvent)) { + addOpenTelemetryAppender(applicationEnvironmentPreparedEvent); + } + } + + private static boolean isLogbackAppenderAddable( + ApplicationEnvironmentPreparedEvent applicationEnvironmentPreparedEvent) { + Boolean otelSdkDisableProperty = + evaluateBooleanProperty(applicationEnvironmentPreparedEvent, "otel.sdk.disabled"); + Boolean logbackInstrumentationEnabledProperty = + evaluateBooleanProperty( + applicationEnvironmentPreparedEvent, "otel.instrumentation.logback-appender.enabled"); + return otelSdkDisableProperty == null + || !otelSdkDisableProperty.booleanValue() + || logbackInstrumentationEnabledProperty == null + || logbackInstrumentationEnabledProperty.booleanValue(); + } + + private static void reInitializeOpenTelemetryAppender( + Optional existingOpenTelemetryAppender, + ApplicationEnvironmentPreparedEvent applicationEnvironmentPreparedEvent) { + OpenTelemetryAppender openTelemetryAppender = existingOpenTelemetryAppender.get(); + // The OpenTelemetry appender is stopped and restarted from the + // org.springframework.boot.context.logging.LoggingApplicationListener.initialize + // method. + // The OpenTelemetryAppender initializes the LoggingEventMapper in the start() method. So, here + // we stop the OpenTelemetry appender before its re-initialization and its restart. + openTelemetryAppender.stop(); + initializeOpenTelemetryAppenderFromProperties( + applicationEnvironmentPreparedEvent, openTelemetryAppender); + openTelemetryAppender.start(); + } + + private static void addOpenTelemetryAppender( + ApplicationEnvironmentPreparedEvent applicationEnvironmentPreparedEvent) { + ch.qos.logback.classic.Logger logger = + (ch.qos.logback.classic.Logger) + LoggerFactory.getILoggerFactory().getLogger(Logger.ROOT_LOGGER_NAME); + OpenTelemetryAppender openTelemetryAppender = new OpenTelemetryAppender(); + initializeOpenTelemetryAppenderFromProperties( + applicationEnvironmentPreparedEvent, openTelemetryAppender); + openTelemetryAppender.start(); + logger.addAppender(openTelemetryAppender); + } + + private static void initializeOpenTelemetryAppenderFromProperties( + ApplicationEnvironmentPreparedEvent applicationEnvironmentPreparedEvent, + OpenTelemetryAppender openTelemetryAppender) { + + // Implemented in the same way as the + // org.springframework.boot.context.logging.LoggingApplicationListener, config properties not + // available + Boolean codeAttribute = + evaluateBooleanProperty( + applicationEnvironmentPreparedEvent, + "otel.instrumentation.logback-appender.experimental.capture-code-attributes"); + if (codeAttribute != null) { + openTelemetryAppender.setCaptureCodeAttributes(codeAttribute.booleanValue()); + } + + Boolean markerAttribute = + evaluateBooleanProperty( + applicationEnvironmentPreparedEvent, + "otel.instrumentation.logback-appender.experimental.capture-marker-attribute"); + if (markerAttribute != null) { + openTelemetryAppender.setCaptureMarkerAttribute(markerAttribute.booleanValue()); + } + + Boolean keyValuePairAttributes = + evaluateBooleanProperty( + applicationEnvironmentPreparedEvent, + "otel.instrumentation.logback-appender.experimental.capture-key-value-pair-attributes"); + if (keyValuePairAttributes != null) { + openTelemetryAppender.setCaptureKeyValuePairAttributes(keyValuePairAttributes.booleanValue()); + } + + Boolean logAttributes = + evaluateBooleanProperty( + applicationEnvironmentPreparedEvent, + "otel.instrumentation.logback-appender.experimental-log-attributes"); + if (logAttributes != null) { + openTelemetryAppender.setCaptureExperimentalAttributes(logAttributes.booleanValue()); + } + + Boolean loggerContextAttributes = + evaluateBooleanProperty( + applicationEnvironmentPreparedEvent, + "otel.instrumentation.logback-appender.experimental.capture-logger-context-attributes"); + if (loggerContextAttributes != null) { + openTelemetryAppender.setCaptureLoggerContext(loggerContextAttributes.booleanValue()); + } + + String mdcAttributeProperty = + applicationEnvironmentPreparedEvent + .getEnvironment() + .getProperty( + "otel.instrumentation.logback-appender.experimental.capture-mdc-attributes", + String.class); + if (mdcAttributeProperty != null) { + openTelemetryAppender.setCaptureMdcAttributes(mdcAttributeProperty); + } + } + + private static Boolean evaluateBooleanProperty( + ApplicationEnvironmentPreparedEvent applicationEnvironmentPreparedEvent, String property) { + return applicationEnvironmentPreparedEvent + .getEnvironment() + .getProperty(property, Boolean.class); + } + + private static Optional findOpenTelemetryAppender() { + ILoggerFactory loggerFactorySpi = LoggerFactory.getILoggerFactory(); + if (!(loggerFactorySpi instanceof LoggerContext)) { + return Optional.empty(); + } + LoggerContext loggerContext = (LoggerContext) loggerFactorySpi; + for (ch.qos.logback.classic.Logger logger : loggerContext.getLoggerList()) { + Iterator> appenderIterator = logger.iteratorForAppenders(); + while (appenderIterator.hasNext()) { + Appender appender = appenderIterator.next(); + if (appender instanceof OpenTelemetryAppender) { + OpenTelemetryAppender openTelemetryAppender = (OpenTelemetryAppender) appender; + return Optional.of(openTelemetryAppender); + } + } + } + return Optional.empty(); + } + + private LogbackAppenderInstaller() {} +} diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/logging/OpenTelemetryAppenderAutoConfiguration.java b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/logging/OpenTelemetryAppenderAutoConfiguration.java new file mode 100644 index 000000000000..7fae726c9fb8 --- /dev/null +++ b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/logging/OpenTelemetryAppenderAutoConfiguration.java @@ -0,0 +1,53 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.logging; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.instrumentation.spring.autoconfigure.internal.ConditionalOnEnabledInstrumentation; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.context.ApplicationListener; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time. + */ +@Configuration +@SuppressWarnings("OtelPrivateConstructorForUtilityClass") +public class OpenTelemetryAppenderAutoConfiguration { + + @ConditionalOnEnabledInstrumentation(module = "log4j-appender") + @ConditionalOnClass(org.apache.logging.log4j.core.LoggerContext.class) + @Configuration + static class Log4jAppenderConfig { + + @Bean + ApplicationListener log4jOtelAppenderInitializer( + OpenTelemetry openTelemetry) { + return event -> { + io.opentelemetry.instrumentation.log4j.appender.v2_17.OpenTelemetryAppender.install( + openTelemetry); + }; + } + } + + @ConditionalOnEnabledInstrumentation(module = "logback-appender") + @ConditionalOnClass(ch.qos.logback.classic.LoggerContext.class) + @Configuration + static class LogbackAppenderConfig { + + @Bean + ApplicationListener logbackOtelAppenderInitializer( + OpenTelemetry openTelemetry) { + return event -> { + io.opentelemetry.instrumentation.logback.appender.v1_0.OpenTelemetryAppender.install( + openTelemetry); + }; + } + } +} diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/metrics/MicrometerShimAutoConfiguration.java b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/micrometer/MicrometerBridgeAutoConfiguration.java similarity index 68% rename from instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/metrics/MicrometerShimAutoConfiguration.java rename to instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/micrometer/MicrometerBridgeAutoConfiguration.java index ef08bcee061b..703051d24146 100644 --- a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/metrics/MicrometerShimAutoConfiguration.java +++ b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/micrometer/MicrometerBridgeAutoConfiguration.java @@ -3,34 +3,36 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.instrumentation.spring.autoconfigure.metrics; +package io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.micrometer; import io.micrometer.core.instrument.Clock; import io.micrometer.core.instrument.MeterRegistry; import io.opentelemetry.api.OpenTelemetry; import io.opentelemetry.instrumentation.micrometer.v1_5.OpenTelemetryMeterRegistry; +import io.opentelemetry.instrumentation.spring.autoconfigure.internal.ConditionalOnEnabledInstrumentation; import org.springframework.boot.actuate.autoconfigure.metrics.CompositeMeterRegistryAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.metrics.MetricsAutoConfiguration; import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.AutoConfigureBefore; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; 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; -@Configuration -@EnableConfigurationProperties(MicrometerShimProperties.class) -@ConditionalOnProperty(name = "otel.springboot.micrometer.enabled", matchIfMissing = true) +/** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time. + */ +@ConditionalOnEnabledInstrumentation(module = "micrometer", enabledByDefault = false) @AutoConfigureAfter(MetricsAutoConfiguration.class) @AutoConfigureBefore(CompositeMeterRegistryAutoConfiguration.class) @ConditionalOnBean(Clock.class) @ConditionalOnClass(MeterRegistry.class) -public class MicrometerShimAutoConfiguration { +@Configuration +public class MicrometerBridgeAutoConfiguration { @Bean - public MeterRegistry micrometerShim(OpenTelemetry openTelemetry, Clock micrometerClock) { + MeterRegistry otelMeterRegistry(OpenTelemetry openTelemetry, Clock micrometerClock) { return OpenTelemetryMeterRegistry.builder(openTelemetry).setClock(micrometerClock).build(); } } diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/mongo/MongoClientInstrumentationAutoConfiguration.java b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/mongo/MongoClientInstrumentationAutoConfiguration.java new file mode 100644 index 000000000000..1fbfdbfb0178 --- /dev/null +++ b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/mongo/MongoClientInstrumentationAutoConfiguration.java @@ -0,0 +1,42 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.mongo; + +import com.mongodb.MongoClientSettings; +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.instrumentation.mongo.v3_1.MongoTelemetry; +import io.opentelemetry.instrumentation.spring.autoconfigure.internal.ConditionalOnEnabledInstrumentation; +import io.opentelemetry.instrumentation.spring.autoconfigure.internal.properties.InstrumentationConfigUtil; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.mongo.MongoClientSettingsBuilderCustomizer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time. + */ +@ConditionalOnBean(OpenTelemetry.class) +@ConditionalOnClass(MongoClientSettings.class) +@ConditionalOnEnabledInstrumentation(module = "mongo") +@Configuration +public class MongoClientInstrumentationAutoConfiguration { + + @Bean + MongoClientSettingsBuilderCustomizer customizer( + OpenTelemetry openTelemetry, ConfigProperties config) { + return builder -> + builder.addCommandListener( + MongoTelemetry.builder(openTelemetry) + .setStatementSanitizationEnabled( + InstrumentationConfigUtil.isStatementSanitizationEnabled( + config, "otel.instrumentation.mongo.statement-sanitizer.enabled")) + .build() + .newCommandListener()); + } +} diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/r2dbc/R2dbcInstrumentationAutoConfiguration.java b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/r2dbc/R2dbcInstrumentationAutoConfiguration.java new file mode 100644 index 000000000000..d744b8fe1dc4 --- /dev/null +++ b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/r2dbc/R2dbcInstrumentationAutoConfiguration.java @@ -0,0 +1,37 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.r2dbc; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.instrumentation.spring.autoconfigure.internal.ConditionalOnEnabledInstrumentation; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import io.r2dbc.spi.ConnectionFactory; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time. + */ +@ConditionalOnBean(OpenTelemetry.class) +@ConditionalOnClass(ConnectionFactory.class) +@ConditionalOnEnabledInstrumentation(module = "r2dbc") +@Configuration(proxyBeanMethods = false) +public class R2dbcInstrumentationAutoConfiguration { + + public R2dbcInstrumentationAutoConfiguration() {} + + @Bean + // static to avoid "is not eligible for getting processed by all BeanPostProcessors" warning + static R2dbcInstrumentingPostProcessor r2dbcInstrumentingPostProcessor( + ObjectProvider openTelemetryProvider, + ObjectProvider configPropertiesProvider) { + return new R2dbcInstrumentingPostProcessor(openTelemetryProvider, configPropertiesProvider); + } +} diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/r2dbc/R2dbcInstrumentingPostProcessor.java b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/r2dbc/R2dbcInstrumentingPostProcessor.java new file mode 100644 index 000000000000..5d75b28946ef --- /dev/null +++ b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/r2dbc/R2dbcInstrumentingPostProcessor.java @@ -0,0 +1,58 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.r2dbc; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.instrumentation.r2dbc.v1_0.internal.shaded.R2dbcTelemetry; +import io.opentelemetry.instrumentation.spring.autoconfigure.internal.properties.InstrumentationConfigUtil; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import io.r2dbc.spi.ConnectionFactory; +import io.r2dbc.spi.ConnectionFactoryOptions; +import org.springframework.aop.scope.ScopedProxyUtils; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.boot.r2dbc.OptionsCapableConnectionFactory; + +class R2dbcInstrumentingPostProcessor implements BeanPostProcessor { + + private final ObjectProvider openTelemetryProvider; + private final ObjectProvider configPropertiesProvider; + + R2dbcInstrumentingPostProcessor( + ObjectProvider openTelemetryProvider, + ObjectProvider configPropertiesProvider) { + this.openTelemetryProvider = openTelemetryProvider; + this.configPropertiesProvider = configPropertiesProvider; + } + + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) { + if (bean instanceof ConnectionFactory && !ScopedProxyUtils.isScopedTarget(beanName)) { + ConnectionFactory connectionFactory = (ConnectionFactory) bean; + return R2dbcTelemetry.builder(openTelemetryProvider.getObject()) + .setStatementSanitizationEnabled( + InstrumentationConfigUtil.isStatementSanitizationEnabled( + configPropertiesProvider.getObject(), + "otel.instrumentation.r2dbc.statement-sanitizer.enabled")) + .build() + .wrapConnectionFactory(connectionFactory, getConnectionFactoryOptions(connectionFactory)); + } + return bean; + } + + private static ConnectionFactoryOptions getConnectionFactoryOptions( + ConnectionFactory connectionFactory) { + OptionsCapableConnectionFactory optionsCapableConnectionFactory = + OptionsCapableConnectionFactory.unwrapFrom(connectionFactory); + if (optionsCapableConnectionFactory != null) { + return optionsCapableConnectionFactory.getOptions(); + } else { + // in practice should never happen + // fall back to empty options; or reconstruct them from the R2dbcProperties + return ConnectionFactoryOptions.builder().build(); + } + } +} diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/web/RestTemplateBeanPostProcessor.java b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/web/RestTemplateBeanPostProcessor.java new file mode 100644 index 000000000000..0cda4b63d686 --- /dev/null +++ b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/web/RestTemplateBeanPostProcessor.java @@ -0,0 +1,38 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.web; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.web.client.RestTemplate; + +final class RestTemplateBeanPostProcessor implements BeanPostProcessor { + + private final ObjectProvider openTelemetryProvider; + + private final ObjectProvider configPropertiesProvider; + + RestTemplateBeanPostProcessor( + ObjectProvider openTelemetryProvider, + ObjectProvider configPropertiesProvider) { + this.openTelemetryProvider = openTelemetryProvider; + this.configPropertiesProvider = configPropertiesProvider; + } + + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) { + if (!(bean instanceof RestTemplate)) { + return bean; + } + + return RestTemplateInstrumentation.addIfNotPresent( + (RestTemplate) bean, + openTelemetryProvider.getObject(), + configPropertiesProvider.getObject()); + } +} diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/web/RestTemplateInstrumentation.java b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/web/RestTemplateInstrumentation.java new file mode 100644 index 000000000000..9465c534d0ce --- /dev/null +++ b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/web/RestTemplateInstrumentation.java @@ -0,0 +1,42 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.web; + +import com.google.errorprone.annotations.CanIgnoreReturnValue; +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.instrumentation.spring.autoconfigure.internal.properties.InstrumentationConfigUtil; +import io.opentelemetry.instrumentation.spring.web.v3_1.SpringWebTelemetry; +import io.opentelemetry.instrumentation.spring.web.v3_1.internal.WebTelemetryUtil; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import java.util.List; +import org.springframework.http.client.ClientHttpRequestInterceptor; +import org.springframework.web.client.RestTemplate; + +class RestTemplateInstrumentation { + + private RestTemplateInstrumentation() {} + + @CanIgnoreReturnValue + static RestTemplate addIfNotPresent( + RestTemplate restTemplate, OpenTelemetry openTelemetry, ConfigProperties config) { + + ClientHttpRequestInterceptor instrumentationInterceptor = + InstrumentationConfigUtil.configureClientBuilder( + config, + SpringWebTelemetry.builder(openTelemetry), + WebTelemetryUtil.getBuilderExtractor()) + .build() + .newInterceptor(); + + List restTemplateInterceptors = restTemplate.getInterceptors(); + if (restTemplateInterceptors.stream() + .noneMatch( + interceptor -> interceptor.getClass() == instrumentationInterceptor.getClass())) { + restTemplateInterceptors.add(0, instrumentationInterceptor); + } + return restTemplate; + } +} diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/web/SpringWebInstrumentationAutoConfiguration.java b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/web/SpringWebInstrumentationAutoConfiguration.java new file mode 100644 index 000000000000..c4f692607eff --- /dev/null +++ b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/web/SpringWebInstrumentationAutoConfiguration.java @@ -0,0 +1,49 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.web; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.instrumentation.spring.autoconfigure.internal.ConditionalOnEnabledInstrumentation; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.web.client.RestTemplateCustomizer; +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. + * + *

    This class is internal and is hence not for public use. Its APIs are unstable and can change + * at any time. + */ +@ConditionalOnEnabledInstrumentation(module = "spring-web") +@ConditionalOnClass(RestTemplate.class) +@Configuration +public class SpringWebInstrumentationAutoConfiguration { + + public SpringWebInstrumentationAutoConfiguration() {} + + // static to avoid "is not eligible for getting processed by all BeanPostProcessors" warning + @Bean + static RestTemplateBeanPostProcessor otelRestTemplateBeanPostProcessor( + ObjectProvider openTelemetryProvider, + ObjectProvider configPropertiesProvider) { + return new RestTemplateBeanPostProcessor(openTelemetryProvider, configPropertiesProvider); + } + + @Bean + RestTemplateCustomizer otelRestTemplateCustomizer( + ObjectProvider openTelemetryProvider, + ObjectProvider configPropertiesProvider) { + return restTemplate -> + RestTemplateInstrumentation.addIfNotPresent( + restTemplate, openTelemetryProvider.getObject(), configPropertiesProvider.getObject()); + } +} diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/webflux/SpringWebfluxInstrumentationAutoConfiguration.java b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/webflux/SpringWebfluxInstrumentationAutoConfiguration.java new file mode 100644 index 000000000000..bdf00acf732b --- /dev/null +++ b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/webflux/SpringWebfluxInstrumentationAutoConfiguration.java @@ -0,0 +1,46 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.webflux; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.instrumentation.spring.autoconfigure.internal.ConditionalOnEnabledInstrumentation; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.reactive.function.client.WebClient; +import org.springframework.web.server.WebFilter; + +/** + * Configures {@link WebClient} for tracing. + * + *

    Adds Open Telemetry instrumentation to WebClient beans after initialization. + * + *

    This class is internal and is hence not for public use. Its APIs are unstable and can change + * at any time. + */ +@ConditionalOnEnabledInstrumentation(module = "spring-webflux") +@ConditionalOnClass(WebClient.class) +@Configuration +public class SpringWebfluxInstrumentationAutoConfiguration { + + public SpringWebfluxInstrumentationAutoConfiguration() {} + + // static to avoid "is not eligible for getting processed by all BeanPostProcessors" warning + @Bean + static WebClientBeanPostProcessor otelWebClientBeanPostProcessor( + ObjectProvider openTelemetryProvider, + ObjectProvider configPropertiesProvider) { + return new WebClientBeanPostProcessor(openTelemetryProvider, configPropertiesProvider); + } + + @Bean + WebFilter telemetryFilter(OpenTelemetry openTelemetry, ConfigProperties config) { + return WebClientBeanPostProcessor.getWebfluxTelemetry(openTelemetry, config) + .createWebFilterAndRegisterReactorHook(); + } +} diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/webflux/WebClientBeanPostProcessor.java b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/webflux/WebClientBeanPostProcessor.java new file mode 100644 index 000000000000..90dde820c4a9 --- /dev/null +++ b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/webflux/WebClientBeanPostProcessor.java @@ -0,0 +1,62 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.webflux; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.instrumentation.spring.autoconfigure.internal.properties.InstrumentationConfigUtil; +import io.opentelemetry.instrumentation.spring.webflux.v5_3.SpringWebfluxTelemetry; +import io.opentelemetry.instrumentation.spring.webflux.v5_3.internal.SpringWebfluxBuilderUtil; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.web.reactive.function.client.WebClient; + +/** + * Inspired by Spring + * Cloud Sleuth. + */ +final class WebClientBeanPostProcessor implements BeanPostProcessor { + + private final ObjectProvider openTelemetryProvider; + private final ObjectProvider configPropertiesProvider; + + WebClientBeanPostProcessor( + ObjectProvider openTelemetryProvider, + ObjectProvider configPropertiesProvider) { + this.openTelemetryProvider = openTelemetryProvider; + this.configPropertiesProvider = configPropertiesProvider; + } + + static SpringWebfluxTelemetry getWebfluxTelemetry( + OpenTelemetry openTelemetry, ConfigProperties config) { + return InstrumentationConfigUtil.configureClientAndServerBuilder( + config, + SpringWebfluxTelemetry.builder(openTelemetry), + SpringWebfluxBuilderUtil.getClientBuilderExtractor(), + SpringWebfluxBuilderUtil.getServerBuilderExtractor()) + .build(); + } + + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) { + if (bean instanceof WebClient) { + WebClient webClient = (WebClient) bean; + return wrapBuilder(webClient.mutate()).build(); + } else if (bean instanceof WebClient.Builder) { + WebClient.Builder webClientBuilder = (WebClient.Builder) bean; + return wrapBuilder(webClientBuilder); + } + return bean; + } + + private WebClient.Builder wrapBuilder(WebClient.Builder webClientBuilder) { + SpringWebfluxTelemetry instrumentation = + getWebfluxTelemetry( + openTelemetryProvider.getObject(), configPropertiesProvider.getObject()); + return webClientBuilder.filters(instrumentation::addClientTracingFilter); + } +} diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/webmvc/SpringWebMvc5InstrumentationAutoConfiguration.java b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/webmvc/SpringWebMvc5InstrumentationAutoConfiguration.java new file mode 100644 index 000000000000..4d296a675478 --- /dev/null +++ b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/webmvc/SpringWebMvc5InstrumentationAutoConfiguration.java @@ -0,0 +1,40 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.webmvc; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.instrumentation.spring.autoconfigure.internal.ConditionalOnEnabledInstrumentation; +import io.opentelemetry.instrumentation.spring.autoconfigure.internal.properties.InstrumentationConfigUtil; +import io.opentelemetry.instrumentation.spring.webmvc.v5_3.SpringWebMvcTelemetry; +import io.opentelemetry.instrumentation.spring.webmvc.v5_3.internal.SpringMvcBuilderUtil; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import javax.servlet.Filter; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.filter.OncePerRequestFilter; +import org.springframework.web.servlet.DispatcherServlet; + +/** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time. + */ +@ConditionalOnEnabledInstrumentation(module = "spring-webmvc") +@ConditionalOnClass({Filter.class, OncePerRequestFilter.class, DispatcherServlet.class}) +@Configuration +@SuppressWarnings("OtelPrivateConstructorForUtilityClass") +public class SpringWebMvc5InstrumentationAutoConfiguration { + + @Bean + Filter otelWebMvcFilter(OpenTelemetry openTelemetry, ConfigProperties config) { + return InstrumentationConfigUtil.configureServerBuilder( + config, + SpringWebMvcTelemetry.builder(openTelemetry), + SpringMvcBuilderUtil.getBuilderExtractor()) + .build() + .createServletFilter(); + } +} diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/properties/ConfigPropertiesBridge.java b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/properties/ConfigPropertiesBridge.java new file mode 100644 index 000000000000..49d50f94cc5c --- /dev/null +++ b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/properties/ConfigPropertiesBridge.java @@ -0,0 +1,105 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.autoconfigure.internal.properties; + +import io.opentelemetry.instrumentation.api.incubator.config.internal.InstrumentationConfig; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigurationException; +import java.time.Duration; +import java.util.List; +import java.util.Map; +import javax.annotation.Nullable; + +final class ConfigPropertiesBridge implements InstrumentationConfig { + + private final ConfigProperties configProperties; + + public ConfigPropertiesBridge(ConfigProperties configProperties) { + this.configProperties = configProperties; + } + + @Nullable + @Override + public String getString(String name) { + try { + return configProperties.getString(name); + } catch (ConfigurationException ignored) { + return null; + } + } + + @Override + public String getString(String name, String defaultValue) { + try { + return configProperties.getString(name, defaultValue); + } catch (ConfigurationException ignored) { + return defaultValue; + } + } + + @Override + public boolean getBoolean(String name, boolean defaultValue) { + try { + return configProperties.getBoolean(name, defaultValue); + } catch (ConfigurationException ignored) { + return defaultValue; + } + } + + @Override + public int getInt(String name, int defaultValue) { + try { + return configProperties.getInt(name, defaultValue); + } catch (ConfigurationException ignored) { + return defaultValue; + } + } + + @Override + public long getLong(String name, long defaultValue) { + try { + return configProperties.getLong(name, defaultValue); + } catch (ConfigurationException ignored) { + return defaultValue; + } + } + + @Override + public double getDouble(String name, double defaultValue) { + try { + return configProperties.getDouble(name, defaultValue); + } catch (ConfigurationException ignored) { + return defaultValue; + } + } + + @Override + public Duration getDuration(String name, Duration defaultValue) { + try { + return configProperties.getDuration(name, defaultValue); + } catch (ConfigurationException ignored) { + return defaultValue; + } + } + + @Override + public List getList(String name, List defaultValue) { + try { + return configProperties.getList(name, defaultValue); + } catch (ConfigurationException ignored) { + return defaultValue; + } + } + + @Override + public Map getMap(String name, Map defaultValue) { + try { + return configProperties.getMap(name, defaultValue); + } catch (ConfigurationException ignored) { + return defaultValue; + } + } +} diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/properties/InstrumentationConfigUtil.java b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/properties/InstrumentationConfigUtil.java new file mode 100644 index 000000000000..747cf33dcb09 --- /dev/null +++ b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/properties/InstrumentationConfigUtil.java @@ -0,0 +1,63 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.autoconfigure.internal.properties; + +import com.google.errorprone.annotations.CanIgnoreReturnValue; +import io.opentelemetry.instrumentation.api.incubator.builder.internal.DefaultHttpClientInstrumenterBuilder; +import io.opentelemetry.instrumentation.api.incubator.builder.internal.DefaultHttpServerInstrumenterBuilder; +import io.opentelemetry.instrumentation.api.incubator.config.internal.CommonConfig; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import java.util.function.Function; + +/** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time. + */ +public final class InstrumentationConfigUtil { + private InstrumentationConfigUtil() {} + + @CanIgnoreReturnValue + public static + T configureClientAndServerBuilder( + ConfigProperties config, + T builder, + Function> + getClientBuilder, + Function> + getServerBuilder) { + CommonConfig commonConfig = getConfig(config); + getClientBuilder.apply(builder).configure(commonConfig); + getServerBuilder.apply(builder).configure(commonConfig); + return builder; + } + + @CanIgnoreReturnValue + public static T configureClientBuilder( + ConfigProperties config, + T builder, + Function> getBuilder) { + getBuilder.apply(builder).configure(getConfig(config)); + return builder; + } + + @CanIgnoreReturnValue + public static T configureServerBuilder( + ConfigProperties config, + T builder, + Function> getBuilder) { + getBuilder.apply(builder).configure(getConfig(config)); + return builder; + } + + private static CommonConfig getConfig(ConfigProperties config) { + return new CommonConfig(new ConfigPropertiesBridge(config)); + } + + public static boolean isStatementSanitizationEnabled(ConfigProperties config, String key) { + return config.getBoolean( + key, config.getBoolean("otel.instrumentation.common.db-statement-sanitizer.enabled", true)); + } +} diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/resources/OtelResourceProperties.java b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/properties/OtelResourceProperties.java similarity index 66% rename from instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/resources/OtelResourceProperties.java rename to instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/properties/OtelResourceProperties.java index 36169f4f8ec3..0bb967079537 100644 --- a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/resources/OtelResourceProperties.java +++ b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/properties/OtelResourceProperties.java @@ -3,13 +3,17 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.instrumentation.spring.autoconfigure.resources; +package io.opentelemetry.instrumentation.spring.autoconfigure.internal.properties; import java.util.Collections; import java.util.Map; import org.springframework.boot.context.properties.ConfigurationProperties; -@ConfigurationProperties(prefix = "otel.springboot.resource") +/** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time. + */ +@ConfigurationProperties(prefix = "otel.resource") public class OtelResourceProperties { private Map attributes = Collections.emptyMap(); diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/properties/OtlpExporterProperties.java b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/properties/OtlpExporterProperties.java new file mode 100644 index 000000000000..b159d10b80f3 --- /dev/null +++ b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/properties/OtlpExporterProperties.java @@ -0,0 +1,52 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.autoconfigure.internal.properties; + +import java.util.HashMap; +import java.util.Map; +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time. + */ +@ConfigurationProperties(prefix = "otel.exporter.otlp") +public final class OtlpExporterProperties { + + private final Map headers = new HashMap<>(); + + private final SignalProperties traces = new SignalProperties(); + private final SignalProperties metrics = new SignalProperties(); + private final SignalProperties logs = new SignalProperties(); + + public Map getHeaders() { + return headers; + } + + public SignalProperties getTraces() { + return traces; + } + + public SignalProperties getMetrics() { + return metrics; + } + + public SignalProperties getLogs() { + return logs; + } + + /** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time. + */ + public static class SignalProperties { + private final Map headers = new HashMap<>(); + + public Map getHeaders() { + return headers; + } + } +} diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/properties/PropagationProperties.java b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/properties/PropagationProperties.java new file mode 100644 index 000000000000..4b582ac3fc2c --- /dev/null +++ b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/properties/PropagationProperties.java @@ -0,0 +1,28 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.autoconfigure.internal.properties; + +import java.util.Collections; +import java.util.List; +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time. + */ +@ConfigurationProperties(prefix = "otel") +public final class PropagationProperties { + + private List propagators = Collections.emptyList(); + + public List getPropagators() { + return propagators; + } + + public void setPropagators(List propagators) { + this.propagators = propagators; + } +} diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/properties/SpringConfigProperties.java b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/properties/SpringConfigProperties.java new file mode 100644 index 000000000000..c2a4f10aca1f --- /dev/null +++ b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/properties/SpringConfigProperties.java @@ -0,0 +1,168 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.autoconfigure.internal.properties; + +import io.opentelemetry.exporter.otlp.internal.OtlpConfigUtil; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import io.opentelemetry.sdk.autoconfigure.spi.internal.DefaultConfigProperties; +import java.time.Duration; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import javax.annotation.Nullable; +import org.springframework.core.env.Environment; +import org.springframework.expression.ExpressionParser; +import org.springframework.expression.spel.standard.SpelExpressionParser; + +/** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time. + */ +public class SpringConfigProperties implements ConfigProperties { + private final Environment environment; + + private final ExpressionParser parser; + private final OtlpExporterProperties otlpExporterProperties; + private final OtelResourceProperties resourceProperties; + private final PropagationProperties propagationProperties; + private final ConfigProperties otelSdkProperties; + + public SpringConfigProperties( + Environment environment, + ExpressionParser parser, + OtlpExporterProperties otlpExporterProperties, + OtelResourceProperties resourceProperties, + PropagationProperties propagationProperties, + ConfigProperties otelSdkProperties) { + this.environment = environment; + this.parser = parser; + this.otlpExporterProperties = otlpExporterProperties; + this.resourceProperties = resourceProperties; + this.propagationProperties = propagationProperties; + this.otelSdkProperties = otelSdkProperties; + } + + // visible for testing + public static ConfigProperties create( + Environment env, + OtlpExporterProperties otlpExporterProperties, + OtelResourceProperties resourceProperties, + PropagationProperties propagationProperties, + ConfigProperties fallback) { + return new SpringConfigProperties( + env, + new SpelExpressionParser(), + otlpExporterProperties, + resourceProperties, + propagationProperties, + fallback); + } + + @Nullable + @Override + public String getString(String name) { + String value = environment.getProperty(name, String.class); + if (value == null && name.equals("otel.exporter.otlp.protocol")) { + // SDK autoconfigure module defaults to `grpc`, but this module aligns with recommendation + // in specification to default to `http/protobuf` + return OtlpConfigUtil.PROTOCOL_HTTP_PROTOBUF; + } + return or(value, otelSdkProperties.getString(name)); + } + + @Nullable + @Override + public Boolean getBoolean(String name) { + return or(environment.getProperty(name, Boolean.class), otelSdkProperties.getBoolean(name)); + } + + @Nullable + @Override + public Integer getInt(String name) { + return or(environment.getProperty(name, Integer.class), otelSdkProperties.getInt(name)); + } + + @Nullable + @Override + public Long getLong(String name) { + return or(environment.getProperty(name, Long.class), otelSdkProperties.getLong(name)); + } + + @Nullable + @Override + public Double getDouble(String name) { + return or(environment.getProperty(name, Double.class), otelSdkProperties.getDouble(name)); + } + + @SuppressWarnings("unchecked") + @Override + public List getList(String name) { + if (name.equals("otel.propagators")) { + return propagationProperties.getPropagators(); + } + + return or(environment.getProperty(name, List.class), otelSdkProperties.getList(name)); + } + + @Nullable + @Override + public Duration getDuration(String name) { + String value = getString(name); + if (value == null) { + return otelSdkProperties.getDuration(name); + } + return DefaultConfigProperties.createFromMap(Collections.singletonMap(name, value)) + .getDuration(name); + } + + @SuppressWarnings("unchecked") + @Override + public Map getMap(String name) { + Map otelSdkMap = otelSdkProperties.getMap(name); + // maps from config properties are not supported by Environment, so we have to fake it + switch (name) { + case "otel.resource.attributes": + return mergeWithOtel(resourceProperties.getAttributes(), otelSdkMap); + case "otel.exporter.otlp.headers": + return mergeWithOtel(otlpExporterProperties.getHeaders(), otelSdkMap); + case "otel.exporter.otlp.logs.headers": + return mergeWithOtel(otlpExporterProperties.getLogs().getHeaders(), otelSdkMap); + case "otel.exporter.otlp.metrics.headers": + return mergeWithOtel(otlpExporterProperties.getMetrics().getHeaders(), otelSdkMap); + case "otel.exporter.otlp.traces.headers": + return mergeWithOtel(otlpExporterProperties.getTraces().getHeaders(), otelSdkMap); + default: + break; + } + + String value = environment.getProperty(name); + if (value == null) { + return otelSdkMap; + } + return (Map) parser.parseExpression(value).getValue(); + } + + /** + * If you specify the environment variable OTEL_RESOURCE_ATTRIBUTES_POD_NAME, then + * Spring Boot will ignore OTEL_RESOURCE_ATTRIBUTES, which violates the principle of + * least surprise. This method merges the two maps, giving precedence to + * OTEL_RESOURCE_ATTRIBUTES_POD_NAME, which is more specific and which is also the value + * that Spring Boot will use (and which will honor SpEL). + */ + private static Map mergeWithOtel( + Map springMap, Map otelSdkMap) { + Map merged = new HashMap<>(otelSdkMap); + merged.putAll(springMap); + return merged; + } + + @Nullable + private static T or(@Nullable T first, @Nullable T second) { + return first != null ? first : second; + } +} diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/resources/DistroVersionResourceProvider.java b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/resources/DistroVersionResourceProvider.java new file mode 100644 index 000000000000..8ba185f97b98 --- /dev/null +++ b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/resources/DistroVersionResourceProvider.java @@ -0,0 +1,38 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.autoconfigure.internal.resources; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.instrumentation.api.internal.EmbeddedInstrumentationProperties; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import io.opentelemetry.sdk.autoconfigure.spi.ResourceProvider; +import io.opentelemetry.sdk.resources.Resource; + +/** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time. + */ +public class DistroVersionResourceProvider implements ResourceProvider { + + public static final String VERSION = + EmbeddedInstrumentationProperties.findVersion("io.opentelemetry.spring-boot-autoconfigure"); + + private static final AttributeKey TELEMETRY_DISTRO_NAME = + AttributeKey.stringKey("telemetry.distro.name"); + private static final AttributeKey TELEMETRY_DISTRO_VERSION = + AttributeKey.stringKey("telemetry.distro.version"); + + @Override + public Resource createResource(ConfigProperties config) { + return Resource.create( + Attributes.of( + TELEMETRY_DISTRO_NAME, + "opentelemetry-spring-boot-starter", + TELEMETRY_DISTRO_VERSION, + VERSION)); + } +} diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/resources/SpringResourceProvider.java b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/resources/SpringResourceProvider.java new file mode 100644 index 000000000000..5f562025764b --- /dev/null +++ b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/resources/SpringResourceProvider.java @@ -0,0 +1,47 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.autoconfigure.internal.resources; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import io.opentelemetry.sdk.autoconfigure.spi.ResourceProvider; +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.semconv.ServiceAttributes; +import java.util.Optional; +import org.springframework.boot.info.BuildProperties; + +/** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time. + */ +public class SpringResourceProvider implements ResourceProvider { + + private final Optional buildProperties; + + public SpringResourceProvider(Optional buildProperties) { + this.buildProperties = buildProperties; + } + + @Override + public Resource createResource(ConfigProperties configProperties) { + AttributesBuilder attributesBuilder = Attributes.builder(); + buildProperties + .map(BuildProperties::getName) + .ifPresent(v -> attributesBuilder.put(ServiceAttributes.SERVICE_NAME, v)); + + String springApplicationName = configProperties.getString("spring.application.name"); + if (springApplicationName != null) { + attributesBuilder.put(ServiceAttributes.SERVICE_NAME, springApplicationName); + } + + buildProperties + .map(BuildProperties::getVersion) + .ifPresent(v -> attributesBuilder.put(ServiceAttributes.SERVICE_VERSION, v)); + + return Resource.create(attributesBuilder.build()); + } +} diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/kafka/ConcurrentKafkaListenerContainerFactoryPostProcessor.java b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/kafka/ConcurrentKafkaListenerContainerFactoryPostProcessor.java deleted file mode 100644 index 4a39a5d296ef..000000000000 --- a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/kafka/ConcurrentKafkaListenerContainerFactoryPostProcessor.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.spring.autoconfigure.kafka; - -import io.opentelemetry.api.OpenTelemetry; -import io.opentelemetry.instrumentation.spring.kafka.v2_7.SpringKafkaTelemetry; -import org.springframework.beans.factory.config.BeanPostProcessor; -import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory; - -class ConcurrentKafkaListenerContainerFactoryPostProcessor implements BeanPostProcessor { - - private final OpenTelemetry openTelemetry; - - ConcurrentKafkaListenerContainerFactoryPostProcessor(OpenTelemetry openTelemetry) { - this.openTelemetry = openTelemetry; - } - - @Override - public Object postProcessAfterInitialization(Object bean, String beanName) { - if (!(bean instanceof ConcurrentKafkaListenerContainerFactory)) { - return bean; - } - - ConcurrentKafkaListenerContainerFactory listenerContainerFactory = - (ConcurrentKafkaListenerContainerFactory) bean; - SpringKafkaTelemetry springKafkaTelemetry = SpringKafkaTelemetry.create(openTelemetry); - listenerContainerFactory.setBatchInterceptor(springKafkaTelemetry.createBatchInterceptor()); - listenerContainerFactory.setRecordInterceptor(springKafkaTelemetry.createRecordInterceptor()); - - return listenerContainerFactory; - } -} diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/kafka/KafkaInstrumentationProperties.java b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/kafka/KafkaInstrumentationProperties.java deleted file mode 100644 index 73eac23125bb..000000000000 --- a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/kafka/KafkaInstrumentationProperties.java +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.spring.autoconfigure.kafka; - -import org.springframework.boot.context.properties.ConfigurationProperties; - -@ConfigurationProperties(prefix = "otel.springboot.kafka") -public class KafkaInstrumentationProperties { - - private boolean enabled = true; - - public boolean isEnabled() { - return enabled; - } - - public void setEnabled(boolean enabled) { - this.enabled = enabled; - } -} diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/metrics/MicrometerShimProperties.java b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/metrics/MicrometerShimProperties.java deleted file mode 100644 index 60e55d20b39d..000000000000 --- a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/metrics/MicrometerShimProperties.java +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.spring.autoconfigure.metrics; - -import org.springframework.boot.context.properties.ConfigurationProperties; - -@ConfigurationProperties(prefix = "otel.springboot.micrometer") -public class MicrometerShimProperties { - - private boolean enabled = true; - - public boolean isEnabled() { - return enabled; - } - - public void setEnabled(boolean enabled) { - this.enabled = enabled; - } -} diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/propagators/CompositeTextMapPropagatorFactory.java b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/propagators/CompositeTextMapPropagatorFactory.java deleted file mode 100644 index 7b418ff5b0b0..000000000000 --- a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/propagators/CompositeTextMapPropagatorFactory.java +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.spring.autoconfigure.propagators; - -import static java.util.logging.Level.WARNING; - -import io.opentelemetry.api.baggage.propagation.W3CBaggagePropagator; -import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator; -import io.opentelemetry.context.propagation.TextMapPropagator; -import io.opentelemetry.contrib.awsxray.propagator.AwsXrayPropagator; -import io.opentelemetry.extension.trace.propagation.B3Propagator; -import io.opentelemetry.extension.trace.propagation.JaegerPropagator; -import io.opentelemetry.extension.trace.propagation.OtTracePropagator; -import java.util.HashSet; -import java.util.List; -import java.util.Set; -import java.util.logging.Logger; -import org.springframework.beans.factory.BeanFactory; -import org.springframework.util.ClassUtils; - -/** Factory of composite {@link TextMapPropagator}. Defaults to W3C and BAGGAGE. */ -public final class CompositeTextMapPropagatorFactory { - - private static final Logger logger = - Logger.getLogger(CompositeTextMapPropagatorFactory.class.getName()); - - @SuppressWarnings("deprecation") // deprecated class to be updated once published in new location - static TextMapPropagator getCompositeTextMapPropagator( - BeanFactory beanFactory, List types) { - - Set propagators = new HashSet<>(); - - for (String type : types) { - switch (type) { - case "b3": - if (isOnClasspath("io.opentelemetry.extension.trace.propagation.B3Propagator")) { - propagators.add( - beanFactory - .getBeanProvider(B3Propagator.class) - .getIfAvailable(B3Propagator::injectingSingleHeader)); - } - break; - case "b3multi": - if (isOnClasspath("io.opentelemetry.extension.trace.propagation.B3Propagator")) { - propagators.add( - beanFactory - .getBeanProvider(B3Propagator.class) - .getIfAvailable(B3Propagator::injectingMultiHeaders)); - } - break; - case "jaeger": - if (isOnClasspath("io.opentelemetry.extension.trace.propagation.JaegerPropagator")) { - propagators.add( - beanFactory - .getBeanProvider(JaegerPropagator.class) - .getIfAvailable(JaegerPropagator::getInstance)); - } - break; - case "ottrace": - if (isOnClasspath("io.opentelemetry.extension.trace.propagation.OtTracerPropagator")) { - propagators.add( - beanFactory - .getBeanProvider(OtTracePropagator.class) - .getIfAvailable(OtTracePropagator::getInstance)); - } - break; - case "xray": - if (isOnClasspath("io.opentelemetry.contrib.awsxray.AwsXrayPropagator")) { - propagators.add( - beanFactory - .getBeanProvider(AwsXrayPropagator.class) - .getIfAvailable(AwsXrayPropagator::getInstance)); - } - break; - case "tracecontext": - propagators.add(W3CTraceContextPropagator.getInstance()); - break; - case "baggage": - propagators.add(W3CBaggagePropagator.getInstance()); - break; - default: - logger.log(WARNING, "Unsupported type of propagator: {0}", type); - break; - } - } - - return TextMapPropagator.composite(propagators); - } - - private static boolean isOnClasspath(String clazz) { - return ClassUtils.isPresent(clazz, null); - } - - private CompositeTextMapPropagatorFactory() {} -} diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/propagators/PropagationAutoConfiguration.java b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/propagators/PropagationAutoConfiguration.java deleted file mode 100644 index f63d93eb8955..000000000000 --- a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/propagators/PropagationAutoConfiguration.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.spring.autoconfigure.propagators; - -import io.opentelemetry.context.propagation.ContextPropagators; -import io.opentelemetry.context.propagation.TextMapPropagator; -import io.opentelemetry.instrumentation.spring.autoconfigure.OpenTelemetryAutoConfiguration; -import java.util.Collections; -import java.util.List; -import org.springframework.beans.factory.BeanFactory; -import org.springframework.beans.factory.ObjectProvider; -import org.springframework.boot.autoconfigure.AutoConfigureBefore; -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 ContextPropagators} bean for propagation. */ -@Configuration -@EnableConfigurationProperties(PropagationProperties.class) -@AutoConfigureBefore(OpenTelemetryAutoConfiguration.class) -@ConditionalOnProperty(prefix = "otel.propagation", name = "enabled", matchIfMissing = true) -public class PropagationAutoConfiguration { - - @Bean - @ConditionalOnMissingBean - ContextPropagators contextPropagators(ObjectProvider> propagators) { - List mapPropagators = propagators.getIfAvailable(Collections::emptyList); - if (mapPropagators.isEmpty()) { - return ContextPropagators.noop(); - } - return ContextPropagators.create(TextMapPropagator.composite(mapPropagators)); - } - - @Configuration - static class PropagatorsConfiguration { - - @Bean - TextMapPropagator compositeTextMapPropagator( - BeanFactory beanFactory, PropagationProperties properties) { - return CompositeTextMapPropagatorFactory.getCompositeTextMapPropagator( - beanFactory, properties.getType()); - } - } -} diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/propagators/PropagationProperties.java b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/propagators/PropagationProperties.java deleted file mode 100644 index 9aa5c072ab65..000000000000 --- a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/propagators/PropagationProperties.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.spring.autoconfigure.propagators; - -import java.util.Arrays; -import java.util.List; -import org.springframework.boot.context.properties.ConfigurationProperties; - -/** Configuration for propagators. */ -@ConfigurationProperties(prefix = "otel.propagation") -public final class PropagationProperties { - - private List type = Arrays.asList("tracecontext", "baggage"); - - public List getType() { - return type; - } - - public void setType(List type) { - this.type = type; - } -} diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/resources/OtelResourceAutoConfiguration.java b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/resources/OtelResourceAutoConfiguration.java deleted file mode 100644 index 722677db6816..000000000000 --- a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/resources/OtelResourceAutoConfiguration.java +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.spring.autoconfigure.resources; - -import io.opentelemetry.instrumentation.resources.ContainerResource; -import io.opentelemetry.instrumentation.resources.ContainerResourceProvider; -import io.opentelemetry.instrumentation.resources.HostResource; -import io.opentelemetry.instrumentation.resources.HostResourceProvider; -import io.opentelemetry.instrumentation.resources.OsResource; -import io.opentelemetry.instrumentation.resources.OsResourceProvider; -import io.opentelemetry.instrumentation.resources.ProcessResource; -import io.opentelemetry.instrumentation.resources.ProcessResourceProvider; -import io.opentelemetry.instrumentation.resources.ProcessRuntimeResource; -import io.opentelemetry.instrumentation.resources.ProcessRuntimeResourceProvider; -import io.opentelemetry.instrumentation.spring.autoconfigure.OpenTelemetryAutoConfiguration; -import io.opentelemetry.sdk.autoconfigure.spi.ResourceProvider; -import org.springframework.boot.autoconfigure.AutoConfigureBefore; -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; - -@Configuration -@EnableConfigurationProperties(OtelResourceProperties.class) -@AutoConfigureBefore(OpenTelemetryAutoConfiguration.class) -@ConditionalOnProperty(prefix = "otel.springboot.resource", name = "enabled", matchIfMissing = true) -public class OtelResourceAutoConfiguration { - - @Bean - public ResourceProvider otelResourceProvider(OtelResourceProperties otelResourceProperties) { - return new SpringResourceProvider(otelResourceProperties); - } - - @Bean - @ConditionalOnClass(OsResource.class) - public ResourceProvider otelOsResourceProvider() { - return new OsResourceProvider(); - } - - @Bean - @ConditionalOnClass(ProcessResource.class) - public ResourceProvider otelProcessResourceProvider() { - return new ProcessResourceProvider(); - } - - @Bean - @ConditionalOnClass(ProcessRuntimeResource.class) - public ResourceProvider otelProcessRuntimeResourceProvider() { - return new ProcessRuntimeResourceProvider(); - } - - @Bean - @ConditionalOnClass(HostResource.class) - public ResourceProvider otelHostResourceProvider() { - return new HostResourceProvider(); - } - - @Bean - @ConditionalOnClass(ContainerResource.class) - public ResourceProvider otelContainerResourceProvider() { - return new ContainerResourceProvider(); - } -} diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/resources/SpringResourceConfigProperties.java b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/resources/SpringResourceConfigProperties.java deleted file mode 100644 index 4ed9190ac45c..000000000000 --- a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/resources/SpringResourceConfigProperties.java +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.spring.autoconfigure.resources; - -import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; -import java.time.Duration; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import javax.annotation.Nullable; -import org.springframework.core.env.Environment; -import org.springframework.expression.ExpressionParser; - -public class SpringResourceConfigProperties implements ConfigProperties { - private final Environment environment; - - private final ExpressionParser parser; - - public SpringResourceConfigProperties(Environment environment, ExpressionParser parser) { - this.environment = environment; - this.parser = parser; - } - - @Nullable - @Override - public String getString(String name) { - return environment.getProperty(name, String.class); - } - - @Nullable - @Override - public Boolean getBoolean(String name) { - return environment.getProperty(name, Boolean.class); - } - - @Nullable - @Override - public Integer getInt(String name) { - return environment.getProperty(name, Integer.class); - } - - @Nullable - @Override - public Long getLong(String name) { - return environment.getProperty(name, Long.class); - } - - @Nullable - @Override - public Double getDouble(String name) { - return environment.getProperty(name, Double.class); - } - - @Nullable - @Override - public Duration getDuration(String name) { - return environment.getProperty(name, Duration.class); - } - - @SuppressWarnings("unchecked") - @Override - public List getList(String name) { - return (List) environment.getProperty(name, List.class); - } - - @SuppressWarnings("unchecked") - @Override - public Map getMap(String name) { - String value = environment.getProperty(name); - return (Map) parser.parseExpression(Objects.requireNonNull(value)).getValue(); - } -} diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/resources/SpringResourceProvider.java b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/resources/SpringResourceProvider.java deleted file mode 100644 index 18dcb6576752..000000000000 --- a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/resources/SpringResourceProvider.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.spring.autoconfigure.resources; - -import io.opentelemetry.api.common.Attributes; -import io.opentelemetry.api.common.AttributesBuilder; -import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; -import io.opentelemetry.sdk.autoconfigure.spi.ResourceProvider; -import io.opentelemetry.sdk.resources.Resource; -import io.opentelemetry.semconv.resource.attributes.ResourceAttributes; -import java.util.Map; - -public class SpringResourceProvider implements ResourceProvider { - - private final OtelResourceProperties otelResourceProperties; - - public SpringResourceProvider(OtelResourceProperties otelResourceProperties) { - this.otelResourceProperties = otelResourceProperties; - } - - @Override - public Resource createResource(ConfigProperties configProperties) { - String applicationName = configProperties.getString("spring.application.name"); - Map attributes = otelResourceProperties.getAttributes(); - AttributesBuilder attributesBuilder = Attributes.builder(); - attributes.forEach(attributesBuilder::put); - return defaultResource(applicationName).merge(Resource.create(attributesBuilder.build())); - } - - private static Resource defaultResource(String applicationName) { - if (applicationName == null) { - return Resource.getDefault(); - } - return Resource.getDefault() - .merge(Resource.create(Attributes.of(ResourceAttributes.SERVICE_NAME, applicationName))); - } -} diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/webmvc/WebMvcFilterAutoConfiguration.java b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/webmvc/WebMvcFilterAutoConfiguration.java deleted file mode 100644 index 3b8f0af52b8f..000000000000 --- a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/webmvc/WebMvcFilterAutoConfiguration.java +++ /dev/null @@ -1,32 +0,0 @@ -/* - * 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.v5_3.SpringWebMvcTelemetry; -import javax.servlet.Filter; -import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; -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; -import org.springframework.web.servlet.DispatcherServlet; - -/** Configures {@link SpringWebMvcTelemetry} for tracing. */ -@Configuration -@EnableConfigurationProperties(WebMvcProperties.class) -@ConditionalOnProperty(prefix = "otel.springboot.web", name = "enabled", matchIfMissing = true) -@ConditionalOnClass({Filter.class, OncePerRequestFilter.class, DispatcherServlet.class}) -@ConditionalOnBean(OpenTelemetry.class) -public class WebMvcFilterAutoConfiguration { - - @Bean - public Filter otelWebMvcInstrumentationFilter(OpenTelemetry openTelemetry) { - return SpringWebMvcTelemetry.create(openTelemetry).createServletFilter(); - } -} diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/webmvc/WebMvcFilterAutoConfigurationSpring6.java b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/webmvc/WebMvcFilterAutoConfigurationSpring6.java deleted file mode 100644 index 0eec311f6724..000000000000 --- a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/webmvc/WebMvcFilterAutoConfigurationSpring6.java +++ /dev/null @@ -1,32 +0,0 @@ -/* - * 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.v6_0.SpringWebMvcTelemetry; -import jakarta.servlet.Filter; -import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; -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; -import org.springframework.web.servlet.DispatcherServlet; - -/** Configures {@link SpringWebMvcTelemetry} for tracing. */ -@Configuration -@EnableConfigurationProperties(WebMvcProperties.class) -@ConditionalOnProperty(prefix = "otel.springboot.web", name = "enabled", matchIfMissing = true) -@ConditionalOnClass({Filter.class, OncePerRequestFilter.class, DispatcherServlet.class}) -@ConditionalOnBean(OpenTelemetry.class) -public class WebMvcFilterAutoConfigurationSpring6 { - - @Bean - public Filter otelWebMvcInstrumentationFilter(OpenTelemetry openTelemetry) { - return SpringWebMvcTelemetry.create(openTelemetry).createServletFilter(); - } -} diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/webmvc/WebMvcProperties.java b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/webmvc/WebMvcProperties.java deleted file mode 100644 index 8b7df171c50f..000000000000 --- a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/webmvc/WebMvcProperties.java +++ /dev/null @@ -1,27 +0,0 @@ -/* - * 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/instrumentation/spring/spring-boot-autoconfigure/src/main/javaSpring3/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/annotations/OpenTelemetryAnnotationsRuntimeHints.java b/instrumentation/spring/spring-boot-autoconfigure/src/main/javaSpring3/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/annotations/OpenTelemetryAnnotationsRuntimeHints.java new file mode 100644 index 000000000000..79a9fad3c069 --- /dev/null +++ b/instrumentation/spring/spring-boot-autoconfigure/src/main/javaSpring3/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/annotations/OpenTelemetryAnnotationsRuntimeHints.java @@ -0,0 +1,24 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.annotations; + +import org.springframework.aot.hint.MemberCategory; +import org.springframework.aot.hint.RuntimeHints; +import org.springframework.aot.hint.RuntimeHintsRegistrar; +import org.springframework.aot.hint.TypeReference; + +class OpenTelemetryAnnotationsRuntimeHints implements RuntimeHintsRegistrar { + + @Override + public void registerHints(RuntimeHints hints, ClassLoader classLoader) { + hints + .reflection() + .registerType( + TypeReference.of( + "io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.annotations.InstrumentationWithSpanAspect"), + hint -> hint.withMembers(MemberCategory.INVOKE_PUBLIC_METHODS)); + } +} diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/main/javaSpring3/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/web/RestClientBeanPostProcessor.java b/instrumentation/spring/spring-boot-autoconfigure/src/main/javaSpring3/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/web/RestClientBeanPostProcessor.java new file mode 100644 index 000000000000..8ebefdd655f6 --- /dev/null +++ b/instrumentation/spring/spring-boot-autoconfigure/src/main/javaSpring3/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/web/RestClientBeanPostProcessor.java @@ -0,0 +1,66 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.web; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.instrumentation.spring.autoconfigure.internal.properties.InstrumentationConfigUtil; +import io.opentelemetry.instrumentation.spring.web.v3_1.SpringWebTelemetry; +import io.opentelemetry.instrumentation.spring.web.v3_1.internal.WebTelemetryUtil; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.http.client.ClientHttpRequestInterceptor; +import org.springframework.web.client.RestClient; + +final class RestClientBeanPostProcessor implements BeanPostProcessor { + + private final ObjectProvider openTelemetryProvider; + private final ObjectProvider configPropertiesProvider; + + public RestClientBeanPostProcessor( + ObjectProvider openTelemetryProvider, + ObjectProvider configPropertiesProvider) { + this.openTelemetryProvider = openTelemetryProvider; + this.configPropertiesProvider = configPropertiesProvider; + } + + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) { + if (bean instanceof RestClient restClient) { + return addRestClientInterceptorIfNotPresent( + restClient, openTelemetryProvider.getObject(), configPropertiesProvider.getObject()); + } + return bean; + } + + private static RestClient addRestClientInterceptorIfNotPresent( + RestClient restClient, OpenTelemetry openTelemetry, ConfigProperties config) { + ClientHttpRequestInterceptor instrumentationInterceptor = getInterceptor(openTelemetry, config); + + return restClient + .mutate() + .requestInterceptors( + interceptors -> { + if (interceptors.stream() + .noneMatch( + interceptor -> + interceptor.getClass() == instrumentationInterceptor.getClass())) { + interceptors.add(0, instrumentationInterceptor); + } + }) + .build(); + } + + static ClientHttpRequestInterceptor getInterceptor( + OpenTelemetry openTelemetry, ConfigProperties config) { + return InstrumentationConfigUtil.configureClientBuilder( + config, + SpringWebTelemetry.builder(openTelemetry), + WebTelemetryUtil.getBuilderExtractor()) + .build() + .newInterceptor(); + } +} diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/main/javaSpring3/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/web/RestClientInstrumentationAutoConfiguration.java b/instrumentation/spring/spring-boot-autoconfigure/src/main/javaSpring3/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/web/RestClientInstrumentationAutoConfiguration.java new file mode 100644 index 000000000000..10c8387eecd4 --- /dev/null +++ b/instrumentation/spring/spring-boot-autoconfigure/src/main/javaSpring3/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/web/RestClientInstrumentationAutoConfiguration.java @@ -0,0 +1,50 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.web; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.instrumentation.spring.autoconfigure.internal.ConditionalOnEnabledInstrumentation; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration; +import org.springframework.boot.web.client.RestClientCustomizer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.client.RestClient; + +/** + * Configures {@link RestClient} for tracing. + * + *

    Adds Open Telemetry instrumentation to {@link RestClient} beans after initialization. + * + *

    This class is internal and is hence not for public use. Its APIs are unstable and can change + * at any time. + */ +@ConditionalOnEnabledInstrumentation(module = "spring-web") +@ConditionalOnClass(RestClient.class) +@AutoConfiguration(after = RestClientAutoConfiguration.class) +@Configuration +public class RestClientInstrumentationAutoConfiguration { + + @Bean + static RestClientBeanPostProcessor otelRestClientBeanPostProcessor( + ObjectProvider openTelemetryProvider, + ObjectProvider configPropertiesProvider) { + return new RestClientBeanPostProcessor(openTelemetryProvider, configPropertiesProvider); + } + + @Bean + RestClientCustomizer otelRestClientCustomizer( + ObjectProvider openTelemetryProvider, + ObjectProvider configPropertiesProvider) { + return builder -> + builder.requestInterceptor( + RestClientBeanPostProcessor.getInterceptor( + openTelemetryProvider.getObject(), configPropertiesProvider.getObject())); + } +} diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/main/javaSpring3/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/webmvc/SpringWebMvc6InstrumentationAutoConfiguration.java b/instrumentation/spring/spring-boot-autoconfigure/src/main/javaSpring3/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/webmvc/SpringWebMvc6InstrumentationAutoConfiguration.java new file mode 100644 index 000000000000..de4007900d5d --- /dev/null +++ b/instrumentation/spring/spring-boot-autoconfigure/src/main/javaSpring3/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/webmvc/SpringWebMvc6InstrumentationAutoConfiguration.java @@ -0,0 +1,40 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.webmvc; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.instrumentation.spring.autoconfigure.internal.ConditionalOnEnabledInstrumentation; +import io.opentelemetry.instrumentation.spring.autoconfigure.internal.properties.InstrumentationConfigUtil; +import io.opentelemetry.instrumentation.spring.webmvc.v6_0.SpringWebMvcTelemetry; +import io.opentelemetry.instrumentation.spring.webmvc.v6_0.internal.SpringMvcBuilderUtil; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import jakarta.servlet.Filter; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.filter.OncePerRequestFilter; +import org.springframework.web.servlet.DispatcherServlet; + +/** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time. + */ +@ConditionalOnEnabledInstrumentation(module = "spring-webmvc") +@ConditionalOnClass({Filter.class, OncePerRequestFilter.class, DispatcherServlet.class}) +@Configuration +@SuppressWarnings("OtelPrivateConstructorForUtilityClass") +public class SpringWebMvc6InstrumentationAutoConfiguration { + + @Bean + Filter otelWebMvcFilter(OpenTelemetry openTelemetry, ConfigProperties config) { + return InstrumentationConfigUtil.configureServerBuilder( + config, + SpringWebMvcTelemetry.builder(openTelemetry), + SpringMvcBuilderUtil.getBuilderExtractor()) + .build() + .createServletFilter(); + } +} diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/instrumentation/spring/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json new file mode 100644 index 000000000000..3b309aae4739 --- /dev/null +++ b/instrumentation/spring/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -0,0 +1,756 @@ +{ + "groups": [ + { + "name": "otel" + } + ], + "properties": [ + { + "name": "otel.attribute.count.limit", + "type": "java.lang.Integer", + "description": "The maximum number of attributes. Applies to spans, span events, span links, and logs.", + "defaultValue": 128 + }, + { + "name": "otel.attribute.value.length.limit", + "type": "java.lang.String", + "description": "The maximum length of attribute values. Applies to spans and logs. By default, there is no limit." + }, + { + "name": "otel.blrp.export.timeout", + "type": "java.lang.String", + "description": "The maximum allowed time, in milliseconds, to export OTLP log batch data.
    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.", + "defaultValue": 30000 + }, + { + "name": "otel.blrp.max.export.batch.size", + "type": "java.lang.Integer", + "description": "The maximum OTLP log batch size.", + "defaultValue": 512 + }, + { + "name": "otel.blrp.max.queue.size", + "type": "java.lang.Integer", + "description": "The maximum OTLP log batch queue size.", + "defaultValue": 2048 + }, + { + "name": "otel.blrp.schedule.delay", + "type": "java.lang.String", + "description": "The interval, in milliseconds, between two consecutive OTLP log batch exports.
    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.", + "defaultValue": 1000 + }, + { + "name": "otel.bsp.schedule.delay", + "type": "java.lang.String", + "description": "The interval, in milliseconds, between two consecutive OTLP span batch exports.
    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.", + "defaultValue": 5000 + }, + { + "name": "otel.bsp.export.timeout", + "type": "java.lang.String", + "description": "The maximum allowed time, in milliseconds, to export OTLP span batch data.
    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.", + "defaultValue": 30000 + }, + { + "name": "otel.bsp.max.export.batch.size", + "type": "java.lang.Integer", + "description": "The maximum OTLP span batch size.", + "defaultValue": 512 + }, + { + "name": "otel.bsp.max.queue.size", + "type": "java.lang.Integer", + "description": "The maximum OTLP span batch queue size.", + "defaultValue": 2048 + }, + { + "name": "otel.experimental.exporter.otlp.retry.enabled", + "type": "java.lang.Boolean", + "description": "Enable experimental retry support. See https://github.com/open-telemetry/opentelemetry-java/blob/main/sdk-extensions/autoconfigure/README.md#otlp-exporter-retry.", + "defaultValue": false + }, + { + "name": "otel.experimental.metrics.cardinality.limit", + "type": "java.lang.Integer", + "description": "If set, configure experimental cardinality limit. The value dictates the maximum number of distinct points per metric.", + "defaultValue": 2000 + }, + { + "name": "otel.experimental.resource.disabled.keys", + "type": "java.util.List", + "description": "Filter out resource entries with these keys." + }, + { + "name": "otel.exporter.otlp.certificate", + "type": "java.lang.String", + "description": "The path to the file containing trusted certificates to use when verifying an OTLP trace, metric, or log server's TLS credentials.
    The file should contain one or more X.509 certificates in PEM format.
    By default the host platform's trusted root certificates are used." + }, + { + "name": "otel.exporter.otlp.client.certificate", + "type": "java.lang.String", + "description": "The path to the file containing trusted certificates to use when verifying an OTLP trace, metric, or log client's TLS credentials.
    The file should contain one or more X.509 certificates in PEM format.
    By default, no chain file is used." + }, + { + "name": "otel.exporter.otlp.client.key", + "type": "java.lang.String", + "description": "The path to the file containing private client key to use when verifying an OTLP trace, metric, or log client's TLS credentials.
    The file should contain one private key PKCS8 PEM format.
    By default, no client key is used." + }, + { + "name": "otel.exporter.otlp.compression", + "type": "java.lang.String", + "description": "The compression type to use on OTLP trace, metric, and log requests.
    Options include gzip.
    By default, no compression will be used." + }, + { + "name": "otel.exporter.otlp.endpoint", + "type": "java.lang.String", + "description": "The OTLP traces, metrics, and logs endpoint to connect to.
    Must be a URL with a scheme of either http or https based on the use of TLS. If protocol is http/protobuf the version and signal will be appended to the path (e.g. v1/traces, v1/metrics, or v1/logs).
    Default is http://localhost:4317 when protocol is grpc, and http://localhost:4318/v1/{signal} when protocol is http/protobuf." + }, + { + "name": "otel.exporter.otlp.headers", + "type": "java.util.Map", + "description": "Request headers for OTLP trace, metric, and log requests.
    Can be either a Spring map or a key-value separated String, e.g. key1=value1,key2=value2." + }, + { + "name": "otel.exporter.otlp.logs.certificate", + "type": "java.lang.String", + "description": " The path to the file containing trusted certificates to use when verifying an OTLP log server's TLS credentials.
    The file should contain one or more X.509 certificates in PEM format.
    By default, the host platform's trusted root certificates are used." + }, + { + "name": "otel.exporter.otlp.logs.client.certificate", + "type": "java.lang.String", + "description": "The path to the file containing trusted certificates to use when verifying an OTLP log server's TLS credentials.
    The file should contain one or more X.509 certificates in PEM format.
    By default, no chain file is used." + }, + { + "name": "otel.exporter.otlp.logs.client.key", + "type": "java.lang.String", + "description": "The path to the file containing private client key to use when verifying an OTLP log client's TLS credentials.
    The file should contain one private key PKCS8 PEM format.
    By default, no client key file is used." + }, + { + "name": "otel.exporter.otlp.logs.compression", + "type": "java.lang.String", + "description": "The compression type to use on OTLP log requests.
    Options include gzip.
    By default, no compression will be used." + }, + { + "name": "otel.exporter.otlp.logs.endpoint", + "type": "java.lang.String", + "description": "The OTLP logs 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 when protocol is grpc, and http://localhost:4318/v1/logs when protocol is http/protobuf." + }, + { + "name": "otel.exporter.otlp.logs.headers", + "type": "java.util.Map", + "description": "Request headers for OTLP log requests.
    Can be either a Spring map or a key-value separated String, e.g. key1=value1,key2=value2." + }, + { + "name": "otel.exporter.otlp.logs.protocol", + "type": "java.lang.String", + "description": "The transport protocol to use on OTLP log requests.", + "defaultValue": "http/protobuf" + }, + { + "name": "otel.exporter.otlp.logs.timeout", + "type": "java.lang.String", + "description": "The maximum waiting time, in milliseconds, allowed to send each OTLP log batch.
    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.", + "defaultValue": "10000" + }, + { + "name": "otel.exporter.otlp.metrics.certificate", + "type": "java.lang.String", + "description": "The path to the file containing trusted certificates to use when verifying an OTLP metric server's TLS credentials.
    The file should contain one or more X.509 certificates in PEM format.
    By default, the host platform's trusted root certificates are used." + }, + { + "name": "otel.exporter.otlp.metrics.client.certificate", + "type": "java.lang.String", + "description": "The path to the file containing trusted certificates to use when verifying an OTLP metric server's TLS credentials.
    The file should contain one or more X.509 certificates in PEM format.
    By default, no chain file is used." + }, + { + "name": "otel.exporter.otlp.metrics.client.key", + "type": "java.lang.String", + "description": "The path to the file containing private client key to use when verifying an OTLP metric client's TLS credentials.
    The file should contain one private key PKCS8 PEM format.
    By default, no client key file is used." + }, + { + "name": "otel.exporter.otlp.metrics.compression", + "type": "java.lang.String", + "description": "The compression type to use on OTLP metric requests.
    Options include gzip.
    By default, no compression will be used." + }, + { + "name": "otel.exporter.otlp.metrics.default.histogram.aggregation", + "type": "java.lang.String", + "description": "The preferred default histogram aggregation.", + "defaultValue": "EXPLICIT_BUCKET_HISTOGRAM" + }, + { + "name": "otel.exporter.otlp.metrics.endpoint", + "type": "java.lang.String", + "description": "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 when protocol is grpc, and http://localhost:4318/v1/metrics when protocol is http/protobuf." + }, + { + "name": "otel.exporter.otlp.metrics.headers", + "type": "java.util.Map", + "description": "Request headers for OTLP metric requests.
    Can be either a Spring map or a key-value separated String, e.g. key1=value1,key2=value2." + }, + { + "name": "otel.exporter.otlp.metrics.protocol", + "type": "java.lang.String", + "description": "The transport protocol to use on OTLP metric requests.", + "defaultValue": "http/protobuf" + }, + { + "name": "otel.exporter.otlp.metrics.temporality.preference", + "type": "java.lang.String", + "description": "The preferred output aggregation temporality.", + "defaultValue": "CUMULATIVE" + }, + { + "name": "otel.exporter.otlp.metrics.timeout", + "type": "java.lang.String", + "description": "The maximum waiting time, in milliseconds, allowed to send each OTLP metric batch.
    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.", + "defaultValue": "10000" + }, + { + "name": "otel.exporter.otlp.protocol", + "type": "java.lang.String", + "description": "The transport protocol to use on OTLP trace, metric, and log requests.", + "defaultValue": "http/protobuf" + }, + { + "name": "otel.exporter.otlp.timeout", + "type": "java.lang.String", + "description": "The maximum waiting time, in milliseconds, allowed to send each OTLP trace, metric, and log batch.
    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.", + "defaultValue": "10000" + }, + { + "name": "otel.exporter.otlp.traces.certificate", + "type": "java.lang.String", + "description": "The path to the file containing trusted certificates to use when verifying an OTLP trace server's TLS credentials.
    The file should contain one or more X.509 certificates in PEM format.
    By default, the host platform's trusted root certificates are used." + }, + { + "name": "otel.exporter.otlp.traces.client.certificate", + "type": "java.lang.String", + "description": "The path to the file containing trusted certificates to use when verifying an OTLP trace server's TLS credentials.
    The file should contain one or more X.509 certificates in PEM format.
    By default no chain file is used." + }, + { + "name": "otel.exporter.otlp.traces.client.key", + "type": "java.lang.String", + "description": "The path to the file containing private client key to use when verifying an OTLP trace client's TLS credentials.
    The file should contain one private key PKCS8 PEM format.
    By default, no client key file is used." + }, + { + "name": "otel.exporter.otlp.traces.compression", + "type": "java.lang.String", + "description": "The compression type to use on OTLP trace requests.
    Options include gzip.
    By default, no compression will be used." + }, + { + "name": "otel.exporter.otlp.traces.endpoint", + "type": "java.lang.String", + "description": "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 when protocol is grpc, and http://localhost:4318/v1/traces when protocol is http/protobuf." + }, + { + "name": "otel.exporter.otlp.traces.headers", + "type": "java.util.Map", + "description": "Request headers for OTLP trace requests.
    Can be either a Spring map or a key-value separated String, e.g. key1=value1,key2=value2." + }, + { + "name": "otel.exporter.otlp.traces.protocol", + "type": "java.lang.String", + "description": "The transport protocol to use on OTLP trace requests.", + "defaultValue": "http/protobuf" + }, + { + "name": "otel.exporter.otlp.traces.timeout", + "type": "java.lang.String", + "description": "The maximum waiting time, in milliseconds, allowed to send each OTLP trace batch.
    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.", + "defaultValue": "10000" + }, + { + "name": "otel.exporter.zipkin.endpoint", + "type": "java.lang.String", + "description": "The Zipkin endpoint to connect to.
    Currently only HTTP is supported.", + "defaultValue": "http://localhost:9411/api/v2/spans" + }, + { + "name": "otel.instrumentation.annotations.enabled", + "type": "java.lang.Boolean", + "description": "Enable the @WithSpan annotation.", + "defaultValue": true + }, + { + "name": "otel.instrumentation.common.db-statement-sanitizer.enabled", + "type": "java.lang.Boolean", + "description": "Enables the DB statement sanitization.", + "defaultValue": true + }, + { + "name": "otel.instrumentation.common.default-enabled", + "type": "java.lang.Boolean", + "description": "Enables all instrumentations. Set to false to disable all instrumentations and then enable specific modules individually, e.g. otel.instrumentation.jdbc.enabled=true.", + "defaultValue": true + }, + { + "name": "otel.instrumentation.common.peer-service-mapping", + "type": "java.util.Map", + "description": "Used to specify a mapping from host names or IP addresses to peer services, as a comma-separated list of host_or_ip=user_assigned_name pairs. The peer service is added as an attribute to a span whose host or IP address match the mapping. See https://opentelemetry.io/docs/zero-code/java/agent/configuration/#peer-service-name." + }, + { + "name": "otel.instrumentation.http.client.capture-request-headers", + "type": "java.util.List", + "description": "List of HTTP request headers to capture in HTTP clients." + }, + { + "name": "otel.instrumentation.http.client.capture-response-headers", + "type": "java.util.List", + "description": "List of HTTP response headers to capture in HTTP clients." + }, + { + "name": "otel.instrumentation.http.client.emit-experimental-telemetry", + "type": "java.lang.Boolean", + "description": "Enable the capture of experimental HTTP client telemetry. Add the http.request.body.size and http.response.body.size> attributes to spans, and record the http.client.request.size and http.client.response.size metrics.", + "defaultValue": false + }, + { + "name": "otel.instrumentation.http.known-methods", + "type": "java.util.List", + "description": "Configures the instrumentation to recognize an alternative set of HTTP request methods. All other methods will be treated as _OTHER.", + "defaultValue": "CONNECT,DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT,TRACE" + }, + { + "name": "otel.instrumentation.http.server.capture-request-headers", + "type": "java.util.List", + "description": "List of HTTP request headers to capture in HTTP servers." + }, + { + "name": "otel.instrumentation.http.server.capture-response-headers", + "type": "java.util.List", + "description": "List of HTTP response headers to capture in HTTP servers." + }, + { + "name": "otel.instrumentation.http.server.emit-experimental-telemetry", + "type": "java.lang.Boolean", + "description": "Enable the capture of experimental HTTP server telemetry. Add the http.request.body.size and http.response.body.size attributes to spans, and record the http.server.request.body.size and http.server.response.body.size metrics.", + "defaultValue": false + }, + { + "name": "otel.instrumentation.jdbc.enabled", + "type": "java.lang.Boolean", + "description": "Enable the JDBC instrumentation.", + "defaultValue": true + }, + { + "name": "otel.instrumentation.jdbc.statement-sanitizer.enabled", + "type": "java.lang.Boolean", + "description": "Enables the DB statement sanitization.", + "defaultValue": true + }, + { + "name": "otel.instrumentation.kafka.enabled", + "type": "java.lang.Boolean", + "description": "Enable the Kafka instrumentation.", + "defaultValue": true + }, + { + "name": "otel.instrumentation.kafka.experimental-span-attributes", + "type": "java.lang.Boolean", + "description": "Enable the capture of experimental Kafka span attributes.", + "defaultValue": false + }, + { + "name": "otel.instrumentation.mongo.enabled", + "type": "java.lang.Boolean", + "description": "Enable the Mongo client instrumentation.", + "defaultValue": true + }, + { + "name": "otel.instrumentation.mongo.statement-sanitizer.enabled", + "type": "java.lang.Boolean", + "description": "Enables the DB statement sanitization.", + "defaultValue": true + }, + { + "name": "otel.instrumentation.log4j-appender.enabled", + "type": "java.lang.Boolean", + "description": "Enable the Log4J2 appender instrumentation.", + "defaultValue": true + }, + { + "name": "otel.instrumentation.logback-appender.enabled", + "type": "java.lang.Boolean", + "description": "Enable the Logback appender instrumentation.", + "defaultValue": true + }, + { + "name": "otel.instrumentation.logback-appender.experimental.capture-code-attributes", + "type": "java.lang.Boolean", + "description": "Enable the capture of source code attributes. Note that capturing source code attributes at logging sites might add a performance overhead.", + "defaultValue": false + }, + { + "name": "otel.instrumentation.logback-appender.experimental.capture-marker-attribute", + "type": "java.lang.Boolean", + "description": "Enable the capture of Logback markers as attributes.", + "defaultValue": false + }, + { + "name": "otel.instrumentation.logback-appender.experimental.capture-key-value-pair-attributes", + "type": "java.lang.Boolean", + "description": "Enable the capture of Logback key value pairs as attributes.", + "defaultValue": false + }, + { + "name": "otel.instrumentation.logback-appender.experimental-log-attributes", + "type": "java.lang.Boolean", + "description": "Enable the capture of experimental log attributes thread.name and thread.id.", + "defaultValue": false + }, + { + "name": "otel.instrumentation.logback-appender.experimental.capture-logger-context-attributes", + "type": "java.lang.Boolean", + "description": "Enable the capture of Logback logger context properties as attributes.", + "defaultValue": false + }, + { + "name": "otel.instrumentation.logback-appender.experimental.capture-mdc-attributes", + "type": "java.util.List", + "description": "MDC attributes to capture. Use the wildcard character * to capture all attributes." + }, + { + "name": "otel.instrumentation.micrometer.enabled", + "type": "java.lang.Boolean", + "description": "Enable the Micrometer instrumentation.", + "defaultValue": false + }, + { + "name": "otel.instrumentation.r2dbc.enabled", + "type": "java.lang.Boolean", + "description": "Enable the R2DBC (reactive JDBC) instrumentation.", + "defaultValue": true + }, + { + "name": "otel.instrumentation.r2dbc.statement-sanitizer.enabled", + "type": "java.lang.Boolean", + "description": "Enables the DB statement sanitization.", + "defaultValue": true + }, + { + "name": "otel.instrumentation.spring-web.enabled", + "type": "java.lang.Boolean", + "description": "Enable the RestTemplate instrumentation.", + "defaultValue": true + }, + { + "name": "otel.instrumentation.spring-webflux.enabled", + "type": "java.lang.Boolean", + "description": "Enable the WebClient instrumentation.", + "defaultValue": true + }, + { + "name": "otel.instrumentation.spring-webmvc.enabled", + "type": "java.lang.Boolean", + "description": "Enable the Servlet instrumentation.", + "defaultValue": true + }, + { + "name": "otel.java.enabled.resource.providers", + "type": "java.util.List", + "description": "Enables one or more ResourceProvider types. If unset, all resource providers are enabled. Each entry is the fully qualified classname of a ResourceProvider." + }, + { + "name": "otel.java.disabled.resource.providers", + "type": "java.util.List", + "description": " Disables one or more ResourceProvider types. Each entry is the fully qualified classname of a ResourceProvider." + }, + { + "name": "otel.logs.exporter", + "type": "java.util.List", + "description": "List of exporters to be used for logs.", + "defaultValue": "otlp" + }, + { + "name": "otel.metric.export.interval", + "type": "java.lang.String", + "description": "The interval, in milliseconds, between the start of two export attempts.
    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.", + "defaultValue": "60000" + }, + { + "name": "otel.metrics.exemplar.filter", + "type": "java.lang.String", + "description": "The filter for exemplar sampling.", + "defaultValue": "TRACE_BASED" + }, + { + "name": "otel.metrics.exporter", + "type": "java.util.List", + "description": "List of exporters to be used for metrics.", + "defaultValue": "otlp" + }, + { + "name": "otel.propagators", + "type": "java.util.List", + "description": "List of propagators to be used for context propagation.", + "defaultValue": "tracecontext,baggage" + }, + { + "name": "otel.resource.attributes", + "type": "java.util.Map", + "description": "Resource attributes to be added to all spans. In addition to these attributes, the resource will also include attributes discovered from the runtime, such as host.name and process.id." + }, + { + "name": "otel.sdk.disabled", + "type": "java.lang.Boolean", + "description": "Disable the OpenTelemetry Spring Starter.", + "defaultValue": false + }, + { + "name": "otel.service.name", + "type": "java.lang.String", + "description": "Specify logical service name. Takes precedence over service.name defined with otel.resource.attributes." + }, + { + "name": "otel.span.attribute.value.length.limit", + "type": "java.lang.Integer", + "description": "The maximum length of span attribute values. Takes precedence over otel.attribute.value.length.limit. By default, there is no limit." + }, + { + "name": "otel.span.attribute.count.limit", + "type": "java.lang.Integer", + "description": "The maximum number of attributes per span. Takes precedence over otel.attribute.count.limit.", + "defaultValue": 128 + }, + { + "name": "otel.span.event.count.limit", + "type": "java.lang.Integer", + "description": "The maximum number of events per span.", + "defaultValue": 128 + }, + { + "name": "otel.span.link.count.limit", + "type": "java.lang.Integer", + "description": "The maximum number of links per span.", + "defaultValue": 128 + }, + { + "name": "otel.traces.exporter", + "type": "java.util.List", + "description": "List of exporters to be used for tracing.", + "defaultValue": "otlp" + }, + { + "name": "otel.traces.sampler", + "type": "java.lang.String", + "description": "The sampler to use for tracing.", + "defaultValue": "parentbased_always_on" + }, + { + "name": "otel.traces.sampler.arg", + "type": "java.lang.Double", + "description": "An argument to the configured tracer if supported, for example a ratio.", + "defaultValue": 1.0 + } + ], + "hints": [ + { + "name": "otel.exporter.otlp.logs.protocol", + "values": [ + { + "value": "http/protobuf" + }, + { + "value": "grpc" + } + ] + }, + { + "name": "otel.exporter.otlp.metrics.protocol", + "values": [ + { + "value": "http/protobuf" + }, + { + "value": "grpc" + } + ] + }, + { + "name": "otel.exporter.otlp.protocol", + "values": [ + { + "value": "http/protobuf" + }, + { + "value": "grpc" + } + ] + }, + { + "name": "otel.exporter.otlp.traces.protocol", + "values": [ + { + "value": "http/protobuf" + }, + { + "value": "grpc" + } + ] + }, + { + "name": "otel.exporter.otlp.metrics.default.histogram.aggregation", + "values": [ + { + "value": "BASE2_EXPONENTIAL_BUCKET_HISTOGRAM" + }, + { + "value": "EXPLICIT_BUCKET_HISTOGRAM" + } + ] + }, + { + "name": "otel.exporter.otlp.metrics.temporality.preference", + "values": [ + { + "value": "CUMULATIVE", + "description": "All instruments will have cumulative temporality." + }, + { + "value": "DELTA", + "description": "Counter (sync and async) and histograms will be delta, up down counters (sync and async) will be cumulative." + }, + { + "value": "LOWMEMORY", + "description": "Sync counter and histograms will be delta, async counter and up down counters (sync and async) will be cumulative." + } + ] + }, + { + "name": "otel.logs.exporter", + "values": [ + { + "value": "console", + "description": "The console exporter prints exported logs to stdout. It's mainly used for testing and debugging." + }, + { + "value": "none", + "description": "No autoconfigured exporter." + }, + { + "value": "otlp", + "description": "OpenTelemetry Protocol (OTLP) exporter." + } + ] + }, + { + "name": "otel.metrics.exemplar.filter", + "values": [ + { + "value": "ALWAYS_ON", + "description": "Take all exemplars." + }, + { + "value": "ALWAYS_OFF", + "description": "Drop all exemplars." + }, + { + "value": "TRACE_BASED", + "description": "Choose exemplars that correspond to a sampled span." + } + ] + }, + { + "name": "otel.metrics.exporter", + "values": [ + { + "value": "console", + "description": "The console exporter prints exported metrics to stdout. It's mainly used for testing and debugging." + }, + { + "value": "none", + "description": "No autoconfigured exporter." + }, + { + "value": "otlp", + "description": "OpenTelemetry Protocol (OTLP) exporter." + } + ] + }, + { + "name": "otel.propagators", + "values": [ + { + "value": "baggage", + "description": "The Baggage propagator propagates baggage using the W3C Baggage format. See https://www.w3.org/TR/baggage/." + }, + { + "value": "b3", + "description": "The B3 propagator propagates trace context using the B3 single-header format: See https://github.com/openzipkin/b3-propagation#single-header." + }, + { + "value": "b3multi", + "description": "The B3 propagator propagates trace context using the B3 multi-header format: See https://github.com/openzipkin/b3-propagation#multiple-headers." + }, + { + "value": "jaeger", + "description": "The Jaeger propagator propagates trace context using the Jaeger format. See https://www.jaegertracing.io/docs/1.21/client-libraries/#propagation-format." + }, + { + "value": "ottrace", + "description": "The OpenTelemetry Trace Context propagator propagates trace context using the OpenTelemetry format. See https://github.com/opentracing/specification/blob/master/rfc/trace_identifiers.md." + }, + { + "value": "tracecontext", + "description": "The Trace Context propagator propagates trace context using the W3C Trace Context format (add `baggage` as well to include W3C baggage). See https://www.w3.org/TR/trace-context/." + }, + { + "value": "xray", + "description": "The AWS X-Ray propagator propagates trace context using the AWS X-Ray format. See https://docs.aws.amazon.com/xray/latest/devguide/xray-concepts.html#xray-concepts-tracingheader." + } + ] + }, + { + "name": "otel.traces.exporter", + "values": [ + { + "value": "console", + "description": "The console exporter prints the name of the span along with its attributes to stdout. It's mainly used for testing and debugging." + }, + { + "value": "none", + "description": "No autoconfigured exporter." + }, + { + "value": "otlp", + "description": "OpenTelemetry Protocol (OTLP) exporter." + }, + { + "value": "zipkin", + "description": "Zipkin exporter." + } + ] + }, + { + "name": "otel.traces.sampler", + "values": [ + { + "value": "always_on", + "description": "Keep all spans." + }, + { + "value": "always_off", + "description": "Drop all spans." + }, + { + "value": "traceidratio", + "description": "Keep a ratio of otel.traces.sampler.arg of all spans." + }, + { + "value": "parentbased_always_on", + "description": "Keep all spans where the parent is also kept. If there is no parent, keep all spans." + }, + { + "value": "parentbased_always_off", + "description": "Keep all spans where the parent is also kept. If there is no parent, drop all spans." + }, + { + "value": "parentbased_traceidratio", + "description": "Keep all spans where the parent is also kept. If there is no parent, keep a ratio of otel.traces.sampler.arg of all spans." + } + ] + } + ] +} diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/main/resources/META-INF/native-image/io.opentelemetry.instrumentation/opentelemetry-spring-boot/resource-config.json b/instrumentation/spring/spring-boot-autoconfigure/src/main/resources/META-INF/native-image/io.opentelemetry.instrumentation/opentelemetry-spring-boot/resource-config.json new file mode 100644 index 000000000000..58a8e28876bc --- /dev/null +++ b/instrumentation/spring/spring-boot-autoconfigure/src/main/resources/META-INF/native-image/io.opentelemetry.instrumentation/opentelemetry-spring-boot/resource-config.json @@ -0,0 +1,9 @@ +{ + "resources": { + "includes": [ + { + "pattern": "META-INF/io/opentelemetry/instrumentation/.*.properties" + } + ] + } +} diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories b/instrumentation/spring/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories index 2d5f5144909f..b389d6a1e381 100644 --- a/instrumentation/spring/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories +++ b/instrumentation/spring/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories @@ -1,16 +1,15 @@ org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ io.opentelemetry.instrumentation.spring.autoconfigure.OpenTelemetryAutoConfiguration,\ -io.opentelemetry.instrumentation.spring.autoconfigure.aspects.TraceAspectAutoConfiguration,\ -io.opentelemetry.instrumentation.spring.autoconfigure.exporters.jaeger.JaegerSpanExporterAutoConfiguration,\ -io.opentelemetry.instrumentation.spring.autoconfigure.exporters.logging.LoggingSpanExporterAutoConfiguration,\ -io.opentelemetry.instrumentation.spring.autoconfigure.exporters.otlp.OtlpMetricExporterAutoConfiguration,\ -io.opentelemetry.instrumentation.spring.autoconfigure.exporters.otlp.OtlpSpanExporterAutoConfiguration,\ -io.opentelemetry.instrumentation.spring.autoconfigure.exporters.otlp.OtlpLoggerExporterAutoConfiguration,\ -io.opentelemetry.instrumentation.spring.autoconfigure.exporters.zipkin.ZipkinSpanExporterAutoConfiguration,\ -io.opentelemetry.instrumentation.spring.autoconfigure.httpclients.resttemplate.RestTemplateAutoConfiguration,\ -io.opentelemetry.instrumentation.spring.autoconfigure.httpclients.webclient.WebClientAutoConfiguration,\ -io.opentelemetry.instrumentation.spring.autoconfigure.kafka.KafkaInstrumentationAutoConfiguration,\ -io.opentelemetry.instrumentation.spring.autoconfigure.metrics.MicrometerShimAutoConfiguration,\ -io.opentelemetry.instrumentation.spring.autoconfigure.propagators.PropagationAutoConfiguration,\ -io.opentelemetry.instrumentation.spring.autoconfigure.resources.OtelResourceAutoConfiguration,\ -io.opentelemetry.instrumentation.spring.autoconfigure.webmvc.WebMvcFilterAutoConfiguration +io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.annotations.InstrumentationAnnotationsAutoConfiguration,\ +io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.kafka.KafkaInstrumentationAutoConfiguration,\ +io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.mongo.MongoClientInstrumentationAutoConfiguration,\ +io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.logging.OpenTelemetryAppenderAutoConfiguration,\ +io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.jdbc.JdbcInstrumentationAutoConfiguration,\ +io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.micrometer.MicrometerBridgeAutoConfiguration,\ +io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.r2dbc.R2dbcInstrumentationAutoConfiguration,\ +io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.web.SpringWebInstrumentationAutoConfiguration,\ +io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.webflux.SpringWebfluxInstrumentationAutoConfiguration,\ +io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.webmvc.SpringWebMvc5InstrumentationAutoConfiguration + +org.springframework.context.ApplicationListener=\ +io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.logging.LogbackAppenderApplicationListener diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/main/resources/META-INF/spring/aot.factories b/instrumentation/spring/spring-boot-autoconfigure/src/main/resources/META-INF/spring/aot.factories new file mode 100644 index 000000000000..8d199e0f8345 --- /dev/null +++ b/instrumentation/spring/spring-boot-autoconfigure/src/main/resources/META-INF/spring/aot.factories @@ -0,0 +1,2 @@ +org.springframework.aot.hint.RuntimeHintsRegistrar=\ +io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.annotations.OpenTelemetryAnnotationsRuntimeHints diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/instrumentation/spring/spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports index 0e9748f20b63..227a59072e48 100644 --- a/instrumentation/spring/spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ b/instrumentation/spring/spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -1,15 +1,12 @@ io.opentelemetry.instrumentation.spring.autoconfigure.OpenTelemetryAutoConfiguration -io.opentelemetry.instrumentation.spring.autoconfigure.aspects.TraceAspectAutoConfiguration -io.opentelemetry.instrumentation.spring.autoconfigure.exporters.jaeger.JaegerSpanExporterAutoConfiguration -io.opentelemetry.instrumentation.spring.autoconfigure.exporters.logging.LoggingSpanExporterAutoConfiguration -io.opentelemetry.instrumentation.spring.autoconfigure.exporters.otlp.OtlpMetricExporterAutoConfiguration -io.opentelemetry.instrumentation.spring.autoconfigure.exporters.otlp.OtlpSpanExporterAutoConfiguration -io.opentelemetry.instrumentation.spring.autoconfigure.exporters.otlp.OtlpLoggerExporterAutoConfiguration -io.opentelemetry.instrumentation.spring.autoconfigure.exporters.zipkin.ZipkinSpanExporterAutoConfiguration -io.opentelemetry.instrumentation.spring.autoconfigure.httpclients.resttemplate.RestTemplateAutoConfiguration -io.opentelemetry.instrumentation.spring.autoconfigure.httpclients.webclient.WebClientAutoConfiguration -io.opentelemetry.instrumentation.spring.autoconfigure.kafka.KafkaInstrumentationAutoConfiguration -io.opentelemetry.instrumentation.spring.autoconfigure.metrics.MicrometerShimAutoConfiguration -io.opentelemetry.instrumentation.spring.autoconfigure.propagators.PropagationAutoConfiguration -io.opentelemetry.instrumentation.spring.autoconfigure.resources.OtelResourceAutoConfiguration -io.opentelemetry.instrumentation.spring.autoconfigure.webmvc.WebMvcFilterAutoConfigurationSpring6 +io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.annotations.InstrumentationAnnotationsAutoConfiguration +io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.kafka.KafkaInstrumentationAutoConfiguration +io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.mongo.MongoClientInstrumentationAutoConfiguration +io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.logging.OpenTelemetryAppenderAutoConfiguration +io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.jdbc.JdbcInstrumentationAutoConfiguration +io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.micrometer.MicrometerBridgeAutoConfiguration +io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.r2dbc.R2dbcInstrumentationAutoConfiguration +io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.web.SpringWebInstrumentationAutoConfiguration +io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.webflux.SpringWebfluxInstrumentationAutoConfiguration +io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.web.RestClientInstrumentationAutoConfiguration +io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.webmvc.SpringWebMvc6InstrumentationAutoConfiguration diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/OpenTelemetryAutoConfigurationTest.java b/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/OpenTelemetryAutoConfigurationTest.java index 35809251e5c8..c13eb6e2fce7 100644 --- a/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/OpenTelemetryAutoConfigurationTest.java +++ b/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/OpenTelemetryAutoConfigurationTest.java @@ -5,21 +5,18 @@ package io.opentelemetry.instrumentation.spring.autoconfigure; -import static io.opentelemetry.semconv.resource.attributes.ResourceAttributes.SERVICE_NAME; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.withSettings; -import io.opentelemetry.api.GlobalOpenTelemetry; import io.opentelemetry.api.OpenTelemetry; -import io.opentelemetry.api.common.AttributeKey; -import io.opentelemetry.instrumentation.spring.autoconfigure.resources.OtelResourceAutoConfiguration; +import io.opentelemetry.exporter.otlp.internal.OtlpSpanExporterProvider; import io.opentelemetry.sdk.OpenTelemetrySdk; -import io.opentelemetry.sdk.logs.SdkLoggerProvider; -import io.opentelemetry.sdk.metrics.SdkMeterProvider; -import io.opentelemetry.sdk.resources.Resource; -import io.opentelemetry.sdk.trace.SdkTracerProvider; -import org.junit.jupiter.api.BeforeEach; +import io.opentelemetry.sdk.autoconfigure.spi.internal.AutoConfigureListener; +import io.opentelemetry.sdk.trace.export.SpanExporter; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.mockito.Mockito; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.test.context.TestConfiguration; import org.springframework.boot.test.context.runner.ApplicationContextRunner; @@ -35,12 +32,10 @@ public OpenTelemetry customOpenTelemetry() { } } - private final ApplicationContextRunner contextRunner = new ApplicationContextRunner(); - - @BeforeEach - void resetGlobalLoggerProvider() { - GlobalOpenTelemetry.resetForTest(); - } + private final ApplicationContextRunner contextRunner = + new ApplicationContextRunner() + .withPropertyValues( + "otel.traces.exporter=none", "otel.metrics.exporter=none", "otel.logs.exporter=none"); @Test @DisplayName( @@ -54,9 +49,7 @@ void customOpenTelemetry() { assertThat(context) .hasBean("customOpenTelemetry") .doesNotHaveBean("openTelemetry") - .doesNotHaveBean("sdkTracerProvider") - .doesNotHaveBean("sdkMeterProvider") - .doesNotHaveBean("sdkLoggerProvider")); + .hasBean("otelProperties")); } @Test @@ -65,113 +58,32 @@ void customOpenTelemetry() { void initializeProvidersAndOpenTelemetry() { this.contextRunner .withConfiguration(AutoConfigurations.of(OpenTelemetryAutoConfiguration.class)) - .run( - context -> - assertThat(context) - .hasBean("openTelemetry") - .hasBean("sdkTracerProvider") - .hasBean("sdkMeterProvider") - .hasBean("sdkLoggerProvider")); + .run(context -> assertThat(context).hasBean("openTelemetry").hasBean("otelProperties")); } @Test @DisplayName( - "when Application Context DOES NOT contain OpenTelemetry bean but TracerProvider should initialize openTelemetry") + "when Application Context DOES NOT contain OpenTelemetry bean but SpanExporter should initialize openTelemetry") void initializeOpenTelemetryWithCustomProviders() { + OtlpSpanExporterProvider spanExporterProvider = + Mockito.mock( + OtlpSpanExporterProvider.class, + withSettings().extraInterfaces(AutoConfigureListener.class)); + Mockito.when(spanExporterProvider.getName()).thenReturn("custom"); + Mockito.when(spanExporterProvider.createExporter(any())) + .thenReturn(Mockito.mock(SpanExporter.class)); + this.contextRunner .withBean( - "customTracerProvider", - SdkTracerProvider.class, - () -> SdkTracerProvider.builder().build()) - .withBean( - "customMeterProvider", SdkMeterProvider.class, () -> SdkMeterProvider.builder().build()) - .withBean( - "customLoggerProvider", - SdkLoggerProvider.class, - () -> SdkLoggerProvider.builder().build()) + "customSpanExporter", + OtlpSpanExporterProvider.class, + () -> spanExporterProvider, + bd -> bd.setDestroyMethodName("")) .withConfiguration(AutoConfigurations.of(OpenTelemetryAutoConfiguration.class)) - .run( - context -> - assertThat(context) - .hasBean("openTelemetry") - .hasBean("customTracerProvider") - .doesNotHaveBean("sdkTracerProvider") - .hasBean("customMeterProvider") - .doesNotHaveBean("sdkMeterProvider") - .hasBean("customLoggerProvider") - .doesNotHaveBean("sdkLoggerProvider")); - } + .withPropertyValues("otel.traces.exporter=custom") + .run(context -> assertThat(context).hasBean("openTelemetry")); - @Test - @DisplayName( - "when spring.application.name is set value should be passed to service name attribute") - void shouldDetermineServiceNameBySpringApplicationName() { - this.contextRunner - .withPropertyValues("spring.application.name=myapp-backend") - .withConfiguration( - AutoConfigurations.of( - OtelResourceAutoConfiguration.class, OpenTelemetryAutoConfiguration.class)) - .run( - context -> { - Resource otelResource = context.getBean("otelResource", Resource.class); - - assertThat(otelResource.getAttribute(SERVICE_NAME)).isEqualTo("myapp-backend"); - }); - } - - @Test - @DisplayName( - "when spring application name and otel service name are not set service name should be default") - void hasDefaultServiceName() { - this.contextRunner - .withConfiguration( - AutoConfigurations.of( - OtelResourceAutoConfiguration.class, OpenTelemetryAutoConfiguration.class)) - .run( - context -> { - Resource otelResource = context.getBean("otelResource", Resource.class); - - assertThat(otelResource.getAttribute(SERVICE_NAME)).isEqualTo("unknown_service:java"); - }); - } - - @Test - @DisplayName("when otel service name is set it should be set as service name attribute") - void shouldDetermineServiceNameByOtelServiceName() { - this.contextRunner - .withConfiguration( - AutoConfigurations.of( - OtelResourceAutoConfiguration.class, OpenTelemetryAutoConfiguration.class)) - .withPropertyValues("otel.springboot.resource.attributes.service.name=otel-name-backend") - .run( - context -> { - Resource otelResource = context.getBean("otelResource", Resource.class); - - assertThat(otelResource.getAttribute(SERVICE_NAME)).isEqualTo("otel-name-backend"); - }); - } - - @Test - @DisplayName("when otel attributes are set in properties they should be put in resource") - void shouldInitializeAttributes() { - this.contextRunner - .withConfiguration( - AutoConfigurations.of( - OtelResourceAutoConfiguration.class, OpenTelemetryAutoConfiguration.class)) - .withPropertyValues( - "otel.springboot.resource.attributes.xyz=foo", - "otel.springboot.resource.attributes.environment=dev", - "otel.springboot.resource.attributes.service.instance.id=id-example") - .run( - context -> { - Resource otelResource = context.getBean("otelResource", Resource.class); - - assertThat(otelResource.getAttribute(AttributeKey.stringKey("environment"))) - .isEqualTo("dev"); - assertThat(otelResource.getAttribute(AttributeKey.stringKey("xyz"))).isEqualTo("foo"); - assertThat(otelResource.getAttribute(AttributeKey.stringKey("service.instance.id"))) - .isEqualTo("id-example"); - }); + Mockito.verify(spanExporterProvider).afterAutoConfigure(any()); } @Test @@ -182,10 +94,7 @@ void shouldInitializeSdkWhenNotDisabled() { .run( context -> { assertThat(context).getBean("openTelemetry").isInstanceOf(OpenTelemetrySdk.class); - assertThat(context) - .hasBean("openTelemetry") - .hasBean("sdkTracerProvider") - .hasBean("sdkMeterProvider"); + assertThat(context).hasBean("openTelemetry"); }); } @@ -195,11 +104,7 @@ void shouldInitializeNoopOpenTelemetryWhenSdkIsDisabled() { .withConfiguration(AutoConfigurations.of(OpenTelemetryAutoConfiguration.class)) .withPropertyValues("otel.sdk.disabled=true") .run( - context -> { - assertThat(context).getBean("openTelemetry").isEqualTo(OpenTelemetry.noop()); - assertThat(context) - .doesNotHaveBean("sdkTracerProvider") - .doesNotHaveBean("sdkMeterProvider"); - }); + context -> + assertThat(context).getBean("openTelemetry").isEqualTo(OpenTelemetry.noop())); } } diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/aspects/InstrumentationWithSpanAspectTest.java b/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/aspects/InstrumentationWithSpanAspectTest.java deleted file mode 100644 index 25e61b7222e2..000000000000 --- a/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/aspects/InstrumentationWithSpanAspectTest.java +++ /dev/null @@ -1,79 +0,0 @@ -/* - * 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 io.opentelemetry.api.OpenTelemetry; -import io.opentelemetry.instrumentation.annotations.SpanAttribute; -import io.opentelemetry.instrumentation.annotations.WithSpan; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionStage; -import org.springframework.core.ParameterNameDiscoverer; - -class InstrumentationWithSpanAspectTest extends AbstractWithSpanAspectTest { - - @Override - WithSpanTester newWithSpanTester() { - return new InstrumentationWithSpanTester(); - } - - @Override - WithSpanAspect newWithSpanAspect( - OpenTelemetry openTelemetry, ParameterNameDiscoverer parameterNameDiscoverer) { - return new InstrumentationWithSpanAspect(openTelemetry, parameterNameDiscoverer); - } - - static class InstrumentationWithSpanTester implements WithSpanTester { - @Override - @WithSpan - public String testWithSpan() { - return "Span with name testWithSpan was created"; - } - - @Override - @WithSpan("greatestSpanEver") - public String testWithSpanWithValue() { - return "Span with name greatestSpanEver was created"; - } - - @Override - @WithSpan - public String testWithSpanWithException() throws Exception { - throw new Exception("Test @WithSpan With Exception"); - } - - @Override - @WithSpan(kind = CLIENT) - public String testWithClientSpan() { - return "Span with name testWithClientSpan and SpanKind.CLIENT was created"; - } - - @Override - @WithSpan - public CompletionStage testAsyncCompletionStage(CompletionStage stage) { - return stage; - } - - @Override - @WithSpan - public CompletableFuture testAsyncCompletableFuture(CompletableFuture stage) { - return stage; - } - - @Override - @WithSpan - public String withSpanAttributes( - @SpanAttribute String discoveredName, - @SpanAttribute String implicitName, - @SpanAttribute("explicitName") String parameter, - @SpanAttribute("nullAttribute") String nullAttribute, - String notTraced) { - - return "hello!"; - } - } -} diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/aspects/SdkExtensionWithSpanAspectTest.java b/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/aspects/SdkExtensionWithSpanAspectTest.java deleted file mode 100644 index 4e8a8f63f798..000000000000 --- a/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/aspects/SdkExtensionWithSpanAspectTest.java +++ /dev/null @@ -1,78 +0,0 @@ -/* - * 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 io.opentelemetry.api.OpenTelemetry; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionStage; -import org.springframework.core.ParameterNameDiscoverer; - -@SuppressWarnings("deprecation") // instrumenting deprecated class for backwards compatibility -class SdkExtensionWithSpanAspectTest extends AbstractWithSpanAspectTest { - - @Override - WithSpanTester newWithSpanTester() { - return new SdkExtensionWithSpanTester(); - } - - @Override - WithSpanAspect newWithSpanAspect( - OpenTelemetry openTelemetry, ParameterNameDiscoverer parameterNameDiscoverer) { - return new SdkExtensionWithSpanAspect(openTelemetry, parameterNameDiscoverer); - } - - static class SdkExtensionWithSpanTester implements WithSpanTester { - @Override - @io.opentelemetry.extension.annotations.WithSpan - public String testWithSpan() { - return "Span with name testWithSpan was created"; - } - - @Override - @io.opentelemetry.extension.annotations.WithSpan("greatestSpanEver") - public String testWithSpanWithValue() { - return "Span with name greatestSpanEver was created"; - } - - @Override - @io.opentelemetry.extension.annotations.WithSpan - public String testWithSpanWithException() throws Exception { - throw new Exception("Test @WithSpan With Exception"); - } - - @Override - @io.opentelemetry.extension.annotations.WithSpan(kind = CLIENT) - public String testWithClientSpan() { - return "Span with name testWithClientSpan and SpanKind.CLIENT was created"; - } - - @Override - @io.opentelemetry.extension.annotations.WithSpan - public CompletionStage testAsyncCompletionStage(CompletionStage stage) { - return stage; - } - - @Override - @io.opentelemetry.extension.annotations.WithSpan - public CompletableFuture testAsyncCompletableFuture(CompletableFuture stage) { - return stage; - } - - @Override - @io.opentelemetry.extension.annotations.WithSpan - public String withSpanAttributes( - @io.opentelemetry.extension.annotations.SpanAttribute String discoveredName, - @io.opentelemetry.extension.annotations.SpanAttribute String implicitName, - @io.opentelemetry.extension.annotations.SpanAttribute("explicitName") String parameter, - @io.opentelemetry.extension.annotations.SpanAttribute("nullAttribute") String nullAttribute, - String notTraced) { - - return "hello!"; - } - } -} diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/aspects/TraceAspectAutoConfigurationTest.java b/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/aspects/TraceAspectAutoConfigurationTest.java deleted file mode 100644 index 0f61763ac496..000000000000 --- a/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/aspects/TraceAspectAutoConfigurationTest.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * 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.BeforeEach; -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)); - - @BeforeEach - void resetGlobalLoggerProvider() { - 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) - .hasBean("instrumentationWithSpanAspect") - .hasBean("sdkExtensionWithSpanAspect")); - } - - @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) - .doesNotHaveBean("instrumentationWithSpanAspect") - .doesNotHaveBean("sdkExtensionWithSpanAspect")); - } - - @Test - @DisplayName("when aspects enabled property is MISSING should initialize WithSpanAspect bean") - void noProperty() { - this.contextRunner.run( - context -> - assertThat(context) - .hasBean("instrumentationWithSpanAspect") - .hasBean("sdkExtensionWithSpanAspect")); - } -} diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/exporters/MetricExporterAutoConfigurationTest.java b/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/exporters/MetricExporterAutoConfigurationTest.java deleted file mode 100644 index 6d42070a16f2..000000000000 --- a/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/exporters/MetricExporterAutoConfigurationTest.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * 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.exporter.logging.LoggingMetricExporter; -import io.opentelemetry.exporter.otlp.metrics.OtlpGrpcMetricExporter; -import io.opentelemetry.instrumentation.spring.autoconfigure.exporters.logging.LoggingMetricExporterAutoConfiguration; -import io.opentelemetry.instrumentation.spring.autoconfigure.exporters.otlp.OtlpMetricExporterAutoConfiguration; -import org.junit.jupiter.api.Test; -import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; - -class MetricExporterAutoConfigurationTest { - - private final ApplicationContextRunner contextRunner = - new ApplicationContextRunner() - .withConfiguration( - AutoConfigurations.of( - OtlpMetricExporterAutoConfiguration.class, - LoggingMetricExporterAutoConfiguration.class)); - - @Test - void defaultConfiguration() { - contextRunner.run( - context -> { - assertThat(context.getBean("otelOtlpGrpcMetricExporter", OtlpGrpcMetricExporter.class)) - .as("OTLP exporter is enabled by default") - .isNotNull(); - assertThat(context.containsBean("otelLoggingMetricExporter")) - .as("Logging exporter is not created unless explicitly configured") - .isFalse(); - }); - } - - @Test - void loggingEnabledByConfiguration() { - contextRunner - .withPropertyValues("otel.exporter.logging.enabled=true") - .run( - context -> { - assertThat( - context.getBean("otelOtlpGrpcMetricExporter", OtlpGrpcMetricExporter.class)) - .as("OTLP exporter is present even with logging enabled") - .isNotNull(); - assertThat(context.getBean("otelLoggingMetricExporter", LoggingMetricExporter.class)) - .as("Logging exporter is explicitly enabled") - .isNotNull(); - }); - } -} diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/exporters/SpanExporterAutoConfigurationTest.java b/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/exporters/SpanExporterAutoConfigurationTest.java deleted file mode 100644 index 81f805bcc982..000000000000 --- a/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/exporters/SpanExporterAutoConfigurationTest.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * 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.exporter.logging.LoggingSpanExporter; -import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporter; -import io.opentelemetry.instrumentation.spring.autoconfigure.exporters.logging.LoggingSpanExporterAutoConfiguration; -import io.opentelemetry.instrumentation.spring.autoconfigure.exporters.otlp.OtlpSpanExporterAutoConfiguration; -import org.junit.jupiter.api.Test; -import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; - -class SpanExporterAutoConfigurationTest { - - private final ApplicationContextRunner contextRunner = - new ApplicationContextRunner() - .withConfiguration( - AutoConfigurations.of( - OtlpSpanExporterAutoConfiguration.class, - LoggingSpanExporterAutoConfiguration.class)); - - @Test - void defaultConfiguration() { - contextRunner.run( - context -> { - assertThat(context.getBean("otelOtlpGrpcSpanExporter", OtlpGrpcSpanExporter.class)) - .as("OTLP exporter is enabled by default") - .isNotNull(); - assertThat(context.containsBean("otelLoggingSpanExporter")) - .as("Logging exporter is not created unless explicitly configured") - .isFalse(); - }); - } - - @Test - void loggingEnabledByConfiguration() { - contextRunner - .withPropertyValues("otel.exporter.logging.enabled=true") - .run( - context -> { - assertThat(context.getBean("otelOtlpGrpcSpanExporter", OtlpGrpcSpanExporter.class)) - .as("OTLP exporter is present even with logging enabled") - .isNotNull(); - assertThat(context.getBean("otelLoggingSpanExporter", LoggingSpanExporter.class)) - .as("Logging exporter is explicitly enabled") - .isNotNull(); - }); - } -} diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/exporters/jaeger/JaegerSpanExporterAutoConfigurationTest.java b/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/exporters/jaeger/JaegerSpanExporterAutoConfigurationTest.java deleted file mode 100644 index b46645121c17..000000000000 --- a/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/exporters/jaeger/JaegerSpanExporterAutoConfigurationTest.java +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.spring.autoconfigure.exporters.jaeger; - -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 org.junit.jupiter.api.BeforeEach; -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)); - - @BeforeEach - void resetGlobalLoggerProvider() { - 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/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/exporters/logging/LoggingMetricExporterAutoConfigurationTest.java b/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/exporters/logging/LoggingMetricExporterAutoConfigurationTest.java deleted file mode 100644 index 62e30cdd899c..000000000000 --- a/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/exporters/logging/LoggingMetricExporterAutoConfigurationTest.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.spring.autoconfigure.exporters.logging; - -import static org.assertj.core.api.Assertions.assertThat; - -import io.opentelemetry.api.GlobalOpenTelemetry; -import io.opentelemetry.exporter.logging.LoggingMetricExporter; -import io.opentelemetry.instrumentation.spring.autoconfigure.OpenTelemetryAutoConfiguration; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; - -class LoggingMetricExporterAutoConfigurationTest { - - private final ApplicationContextRunner runner = - new ApplicationContextRunner() - .withConfiguration( - AutoConfigurations.of( - OpenTelemetryAutoConfiguration.class, - LoggingMetricExporterAutoConfiguration.class)); - - @BeforeEach - void resetGlobalLoggerProvider() { - GlobalOpenTelemetry.resetForTest(); - } - - @Test - void loggingEnabled() { - runner - .withPropertyValues("otel.exporter.logging.enabled=true") - .run( - context -> - assertThat( - context.getBean("otelLoggingMetricExporter", LoggingMetricExporter.class)) - .isNotNull()); - } - - @Test - void loggingMetricsEnabled() { - runner - .withPropertyValues("otel.exporter.logging.metrics.enabled=true") - .run( - context -> - assertThat( - context.getBean("otelLoggingMetricExporter", LoggingMetricExporter.class)) - .isNotNull()); - } - - @Test - void loggingDisabled() { - runner - .withPropertyValues("otel.exporter.logging.enabled=false") - .run(context -> assertThat(context.containsBean("otelLoggingMetricExporter")).isFalse()); - } - - @Test - void loggingMetricsDisabled() { - runner - .withPropertyValues("otel.exporter.logging.metrics.enabled=false") - .run(context -> assertThat(context.containsBean("otelLoggingMetricExporter")).isFalse()); - } - - @Test - void noProperties() { - runner.run(context -> assertThat(context.containsBean("otelLoggingMetricExporter")).isFalse()); - } -} diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/exporters/logging/LoggingSpanExporterAutoConfigurationTest.java b/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/exporters/logging/LoggingSpanExporterAutoConfigurationTest.java deleted file mode 100644 index 3f0f9c2a20fa..000000000000 --- a/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/exporters/logging/LoggingSpanExporterAutoConfigurationTest.java +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.spring.autoconfigure.exporters.logging; - -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 org.junit.jupiter.api.BeforeEach; -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)); - - @BeforeEach - void resetGlobalLoggerProvider() { - GlobalOpenTelemetry.resetForTest(); - } - - @Test - @DisplayName("when exporters are ENABLED should initialize LoggingSpanExporter bean") - void loggingEnabled() { - contextRunner - .withPropertyValues("otel.exporter.logging.enabled=true") - .run( - context -> - assertThat(context.getBean("otelLoggingSpanExporter", LoggingSpanExporter.class)) - .isNotNull()); - } - - @Test - void loggingTracesEnabled() { - contextRunner - .withPropertyValues("otel.exporter.logging.traces.enabled=true") - .run( - context -> - assertThat(context.getBean("otelLoggingSpanExporter", LoggingSpanExporter.class)) - .isNotNull()); - } - - @Test - @DisplayName("when exporters are DISABLED should NOT initialize LoggingSpanExporter bean") - void loggingDisabled() { - contextRunner - .withPropertyValues("otel.exporter.logging.enabled=false") - .run(context -> assertThat(context.containsBean("otelLoggingSpanExporter")).isFalse()); - } - - @Test - @DisplayName("when exporters are DISABLED should NOT initialize LoggingSpanExporter bean") - void loggingTracesDisabled() { - contextRunner - .withPropertyValues("otel.exporter.logging.traces.enabled=false") - .run(context -> assertThat(context.containsBean("otelLoggingSpanExporter")).isFalse()); - } - - @Test - @DisplayName( - "when exporter enabled property is MISSING should initialize LoggingSpanExporter bean") - void exporterPresentByDefault() { - contextRunner.run( - context -> assertThat(context.containsBean("otelLoggingSpanExporter")).isFalse()); - } -} diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/exporters/otlp/OtlpLogExporterAutoConfigurationTest.java b/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/exporters/otlp/OtlpLogExporterAutoConfigurationTest.java deleted file mode 100644 index 8fde124356ab..000000000000 --- a/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/exporters/otlp/OtlpLogExporterAutoConfigurationTest.java +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.spring.autoconfigure.exporters.otlp; - -import static org.assertj.core.api.Assertions.assertThat; - -import io.opentelemetry.api.GlobalOpenTelemetry; -import io.opentelemetry.exporter.otlp.logs.OtlpGrpcLogRecordExporter; -import io.opentelemetry.instrumentation.spring.autoconfigure.OpenTelemetryAutoConfiguration; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; - -class OtlpLogExporterAutoConfigurationTest { - - private final ApplicationContextRunner runner = - new ApplicationContextRunner() - .withConfiguration( - AutoConfigurations.of( - OpenTelemetryAutoConfiguration.class, OtlpLoggerExporterAutoConfiguration.class)); - - @BeforeEach - void resetGlobalLoggerProvider() { - GlobalOpenTelemetry.resetForTest(); - } - - @Test - void otlpEnabled() { - runner - .withPropertyValues("otel.exporter.otlp.enabled=true") - .run( - context -> - assertThat( - context.getBean( - "otelOtlpGrpcLogRecordExporter", OtlpGrpcLogRecordExporter.class)) - .isNotNull()); - } - - @Test - void otlpLogsEnabled() { - runner - .withPropertyValues("otel.exporter.otlp.logs.enabled=true") - .run( - context -> - assertThat( - context.getBean( - "otelOtlpGrpcLogRecordExporter", OtlpGrpcLogRecordExporter.class)) - .isNotNull()); - } - - @Test - void otlpDisabled() { - runner - .withPropertyValues("otel.exporter.otlp.enabled=false") - .run( - context -> assertThat(context.containsBean("otelOtlpGrpcLogRecordExporter")).isFalse()); - } - - @Test - void otlpLogsDisabled() { - runner - .withPropertyValues("otel.exporter.otlp.logs.enabled=false") - .run( - context -> assertThat(context.containsBean("otelOtlpGrpcLogRecordExporter")).isFalse()); - } - - @Test - void loggerPresentByDefault() { - runner.run( - context -> - assertThat( - context.getBean( - "otelOtlpGrpcLogRecordExporter", OtlpGrpcLogRecordExporter.class)) - .isNotNull()); - } -} diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/exporters/otlp/OtlpMetricExporterAutoConfigurationTest.java b/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/exporters/otlp/OtlpMetricExporterAutoConfigurationTest.java deleted file mode 100644 index 9b8ac94017c4..000000000000 --- a/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/exporters/otlp/OtlpMetricExporterAutoConfigurationTest.java +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.spring.autoconfigure.exporters.otlp; - -import static org.assertj.core.api.Assertions.assertThat; - -import io.opentelemetry.api.GlobalOpenTelemetry; -import io.opentelemetry.exporter.otlp.metrics.OtlpGrpcMetricExporter; -import io.opentelemetry.instrumentation.spring.autoconfigure.OpenTelemetryAutoConfiguration; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; - -class OtlpMetricExporterAutoConfigurationTest { - - private final ApplicationContextRunner runner = - new ApplicationContextRunner() - .withConfiguration( - AutoConfigurations.of( - OpenTelemetryAutoConfiguration.class, OtlpMetricExporterAutoConfiguration.class)); - - @BeforeEach - void resetGlobalLoggerProvider() { - GlobalOpenTelemetry.resetForTest(); - } - - @Test - void otlpEnabled() { - runner - .withPropertyValues("otel.exporter.otlp.enabled=true") - .run( - context -> - assertThat( - context.getBean("otelOtlpGrpcMetricExporter", OtlpGrpcMetricExporter.class)) - .isNotNull()); - } - - @Test - void otlpMetricsEnabled() { - runner - .withPropertyValues("otel.exporter.otlp.metrics.enabled=true") - .run( - context -> - assertThat( - context.getBean("otelOtlpGrpcMetricExporter", OtlpGrpcMetricExporter.class)) - .isNotNull()); - } - - @Test - void otlpDisabled() { - runner - .withPropertyValues("otel.exporter.otlp.enabled=false") - .run(context -> assertThat(context.containsBean("otelOtlpGrpcMetricExporter")).isFalse()); - } - - @Test - void otlpMetricsDisabled() { - runner - .withPropertyValues("otel.exporter.otlp.metrics.enabled=false") - .run(context -> assertThat(context.containsBean("otelOtlpGrpcMetricExporter")).isFalse()); - } - - @Test - void exporterPresentByDefault() { - runner.run( - context -> - assertThat(context.getBean("otelOtlpGrpcMetricExporter", OtlpGrpcMetricExporter.class)) - .isNotNull()); - } -} diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/exporters/otlp/OtlpSpanExporterAutoConfigurationTest.java b/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/exporters/otlp/OtlpSpanExporterAutoConfigurationTest.java deleted file mode 100644 index 108afea8856d..000000000000 --- a/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/exporters/otlp/OtlpSpanExporterAutoConfigurationTest.java +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.spring.autoconfigure.exporters.otlp; - -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 org.junit.jupiter.api.BeforeEach; -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 OtlpSpanExporterAutoConfiguration}. */ -class OtlpSpanExporterAutoConfigurationTest { - - private final ApplicationContextRunner contextRunner = - new ApplicationContextRunner() - .withConfiguration( - AutoConfigurations.of( - OpenTelemetryAutoConfiguration.class, OtlpSpanExporterAutoConfiguration.class)); - - @BeforeEach - void resetGlobalLoggerProvider() { - GlobalOpenTelemetry.resetForTest(); - } - - @Test - @DisplayName("when exporters are ENABLED should initialize OtlpGrpcSpanExporter bean") - void otlpEnabled() { - this.contextRunner - .withPropertyValues("otel.exporter.otlp.enabled=true") - .run( - context -> - assertThat(context.getBean("otelOtlpGrpcSpanExporter", OtlpGrpcSpanExporter.class)) - .isNotNull()); - } - - @Test - void otlpTracesEnabled() { - this.contextRunner - .withPropertyValues("otel.exporter.otlp.traces.enabled=true") - .run( - context -> - assertThat(context.getBean("otelOtlpGrpcSpanExporter", OtlpGrpcSpanExporter.class)) - .isNotNull()); - } - - @Test - @DisplayName("when exporters are DISABLED should NOT initialize OtlpGrpcSpanExporter bean") - void otlpDisabled() { - this.contextRunner - .withPropertyValues("otel.exporter.otlp.enabled=false") - .run(context -> assertThat(context.containsBean("otelOtlpGrpcSpanExporter")).isFalse()); - } - - @Test - void otlpTracesDisabled() { - this.contextRunner - .withPropertyValues("otel.exporter.otlp.traces.enabled=false") - .run(context -> assertThat(context.containsBean("otelOtlpGrpcSpanExporter")).isFalse()); - } - - @Test - @DisplayName("when otlp enabled property is MISSING should initialize OtlpGrpcSpanExporter bean") - void exporterPresentByDefault() { - this.contextRunner.run( - context -> - assertThat(context.getBean("otelOtlpGrpcSpanExporter", OtlpGrpcSpanExporter.class)) - .isNotNull()); - } -} diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/exporters/zipkin/ZipkinSpanExporterAutoConfigurationTest.java b/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/exporters/zipkin/ZipkinSpanExporterAutoConfigurationTest.java deleted file mode 100644 index f5120be939b1..000000000000 --- a/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/exporters/zipkin/ZipkinSpanExporterAutoConfigurationTest.java +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.spring.autoconfigure.exporters.zipkin; - -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 org.junit.jupiter.api.BeforeEach; -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)); - - @BeforeEach - void resetGlobalLoggerProvider() { - 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/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/httpclients/resttemplate/RestTemplateAutoConfigurationTest.java b/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/httpclients/resttemplate/RestTemplateAutoConfigurationTest.java deleted file mode 100644 index 3a98e6a3f607..000000000000 --- a/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/httpclients/resttemplate/RestTemplateAutoConfigurationTest.java +++ /dev/null @@ -1,67 +0,0 @@ -/* - * 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.BeforeEach; -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)); - - @BeforeEach - void resetGlobalLoggerProvider() { - 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/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/httpclients/resttemplate/RestTemplateBeanPostProcessorTest.java b/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/httpclients/resttemplate/RestTemplateBeanPostProcessorTest.java deleted file mode 100644 index eb2144e1e3a5..000000000000 --- a/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/httpclients/resttemplate/RestTemplateBeanPostProcessorTest.java +++ /dev/null @@ -1,103 +0,0 @@ -/* - * 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 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.http.client.ClientHttpRequestInterceptor; -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(RestTemplateBeanPostProcessorTest::isOtelInstrumentationInterceptor) - .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(RestTemplateBeanPostProcessorTest::isOtelInstrumentationInterceptor) - .count()) - .isEqualTo(0); - - verify(openTelemetryProvider, times(3)).getIfUnique(); - } - - private static boolean isOtelInstrumentationInterceptor(ClientHttpRequestInterceptor rti) { - return rti.getClass().getName().startsWith("io.opentelemetry.instrumentation"); - } -} diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/httpclients/webclient/WebClientAutoConfigurationTest.java b/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/httpclients/webclient/WebClientAutoConfigurationTest.java deleted file mode 100644 index aaa7fc2468a5..000000000000 --- a/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/httpclients/webclient/WebClientAutoConfigurationTest.java +++ /dev/null @@ -1,67 +0,0 @@ -/* - * 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.BeforeEach; -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)); - - @BeforeEach - void resetGlobalLoggerProvider() { - 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/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/httpclients/webclient/WebClientBeanPostProcessorTest.java b/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/httpclients/webclient/WebClientBeanPostProcessorTest.java deleted file mode 100644 index 79d71cca8da3..000000000000 --- a/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/httpclients/webclient/WebClientBeanPostProcessorTest.java +++ /dev/null @@ -1,141 +0,0 @@ -/* - * 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 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.ExchangeFilterFunction; -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 -> isOtelExchangeFilter(wctf)).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 -> isOtelExchangeFilter(wctf)).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 -> isOtelExchangeFilter(wctf)).count()) - .isEqualTo(1)); - - verify(openTelemetryProvider, times(3)).getIfUnique(); - } - - private static boolean isOtelExchangeFilter(ExchangeFilterFunction wctf) { - return wctf.getClass().getName().startsWith("io.opentelemetry.instrumentation"); - } -} diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/annotations/InstrumentationAnnotationsAutoConfigurationTest.java b/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/annotations/InstrumentationAnnotationsAutoConfigurationTest.java new file mode 100644 index 000000000000..f55e5c13f406 --- /dev/null +++ b/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/annotations/InstrumentationAnnotationsAutoConfigurationTest.java @@ -0,0 +1,41 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.annotations; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.OpenTelemetry; +import org.junit.jupiter.api.Test; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; + +class InstrumentationAnnotationsAutoConfigurationTest { + + private final ApplicationContextRunner contextRunner = + new ApplicationContextRunner() + .withBean(OpenTelemetry.class, OpenTelemetry::noop) + .withConfiguration( + AutoConfigurations.of(InstrumentationAnnotationsAutoConfiguration.class)); + + @Test + void instrumentationEnabled() { + contextRunner + .withPropertyValues("otel.instrumentation.annotations.enabled=true") + .run(context -> assertThat(context).hasBean("otelInstrumentationWithSpanAspect")); + } + + @Test + void instrumentationDisabled() { + contextRunner + .withPropertyValues("otel.instrumentation.annotations.enabled=false") + .run(context -> assertThat(context).doesNotHaveBean("otelInstrumentationWithSpanAspect")); + } + + @Test + void defaultConfiguration() { + contextRunner.run(context -> assertThat(context).hasBean("otelInstrumentationWithSpanAspect")); + } +} diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/aspects/AbstractWithSpanAspectTest.java b/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/annotations/InstrumentationWithSpanAspectTest.java similarity index 90% rename from instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/aspects/AbstractWithSpanAspectTest.java rename to instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/annotations/InstrumentationWithSpanAspectTest.java index 614daf2fe25b..0b4294895d7c 100644 --- a/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/aspects/AbstractWithSpanAspectTest.java +++ b/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/annotations/InstrumentationWithSpanAspectTest.java @@ -3,18 +3,20 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.instrumentation.spring.autoconfigure.aspects; +package io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.annotations; import static io.opentelemetry.api.common.AttributeKey.stringKey; import static io.opentelemetry.api.trace.SpanKind.CLIENT; import static io.opentelemetry.api.trace.SpanKind.INTERNAL; import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; import static io.opentelemetry.sdk.testing.assertj.TracesAssert.assertThat; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.CODE_FUNCTION; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.CODE_NAMESPACE; +import static io.opentelemetry.semconv.incubating.CodeIncubatingAttributes.CODE_FUNCTION; +import static io.opentelemetry.semconv.incubating.CodeIncubatingAttributes.CODE_NAMESPACE; import static org.assertj.core.api.Assertions.assertThatThrownBy; import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.instrumentation.annotations.SpanAttribute; +import io.opentelemetry.instrumentation.annotations.WithSpan; import io.opentelemetry.instrumentation.testing.junit.LibraryInstrumentationExtension; import io.opentelemetry.sdk.trace.data.SpanData; import io.opentelemetry.sdk.trace.data.StatusData; @@ -31,44 +33,23 @@ import org.springframework.aop.aspectj.annotation.AspectJProxyFactory; import org.springframework.core.ParameterNameDiscoverer; -/** Spring AOP Test for {@link WithSpanAspect}. */ -abstract class AbstractWithSpanAspectTest { +class InstrumentationWithSpanAspectTest { + @RegisterExtension static final LibraryInstrumentationExtension testing = LibraryInstrumentationExtension.create(); - private WithSpanTester withSpanTester; + private InstrumentationWithSpanTester withSpanTester; private String unproxiedTesterSimpleClassName; private String unproxiedTesterClassName; - public interface WithSpanTester { - String testWithSpan(); - - String testWithSpanWithValue(); - - String testWithSpanWithException() throws Exception; - - String testWithClientSpan(); - - CompletionStage testAsyncCompletionStage(CompletionStage stage); - - CompletableFuture testAsyncCompletableFuture(CompletableFuture stage); - - String withSpanAttributes( - String discoveredName, - String implicitName, - String parameter, - String nullAttribute, - String notTraced); + WithSpanAspect newWithSpanAspect( + OpenTelemetry openTelemetry, ParameterNameDiscoverer parameterNameDiscoverer) { + return new InstrumentationWithSpanAspect(openTelemetry, parameterNameDiscoverer); } - abstract WithSpanTester newWithSpanTester(); - - abstract WithSpanAspect newWithSpanAspect( - OpenTelemetry openTelemetry, ParameterNameDiscoverer parameterNameDiscoverer); - @BeforeEach void setup() { - WithSpanTester unproxiedTester = newWithSpanTester(); + InstrumentationWithSpanTester unproxiedTester = new InstrumentationWithSpanTester(); unproxiedTesterSimpleClassName = unproxiedTester.getClass().getSimpleName(); unproxiedTesterClassName = unproxiedTester.getClass().getName(); @@ -207,6 +188,49 @@ void withSpanAttributes() { equalTo(stringKey("explicitName"), "baz")))); } + static class InstrumentationWithSpanTester { + @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 + public CompletionStage testAsyncCompletionStage(CompletionStage stage) { + return stage; + } + + @WithSpan + public CompletableFuture testAsyncCompletableFuture(CompletableFuture stage) { + return stage; + } + + @WithSpan + public String withSpanAttributes( + @SpanAttribute String discoveredName, + @SpanAttribute String implicitName, + @SpanAttribute("explicitName") String parameter, + @SpanAttribute("nullAttribute") String nullAttribute, + String notTraced) { + + return "hello!"; + } + } + @Nested @DisplayName("with a method annotated with @WithSpan returns CompletionStage") class WithCompletionStage { diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/micrometer/MicrometerBridgeAutoConfigurationTest.java b/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/micrometer/MicrometerBridgeAutoConfigurationTest.java new file mode 100644 index 000000000000..5a32e2951d98 --- /dev/null +++ b/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/micrometer/MicrometerBridgeAutoConfigurationTest.java @@ -0,0 +1,58 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.micrometer; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.micrometer.core.instrument.MeterRegistry; +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.instrumentation.micrometer.v1_5.OpenTelemetryMeterRegistry; +import org.junit.jupiter.api.Test; +import org.springframework.boot.actuate.autoconfigure.metrics.MetricsAutoConfiguration; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; + +class MicrometerBridgeAutoConfigurationTest { + + private final ApplicationContextRunner runner = + new ApplicationContextRunner() + .withBean(OpenTelemetry.class, OpenTelemetry::noop) + .withConfiguration(AutoConfigurations.of(MicrometerBridgeAutoConfiguration.class)); + + @Test + void metricsEnabled() { + runner + .withConfiguration(AutoConfigurations.of(MetricsAutoConfiguration.class)) + .withPropertyValues("otel.instrumentation.micrometer.enabled = true") + .run( + context -> + assertThat(context.getBean("otelMeterRegistry", MeterRegistry.class)) + .isNotNull() + .isInstanceOf(OpenTelemetryMeterRegistry.class)); + } + + @Test + void metricsDisabledByDefault() { + runner + .withConfiguration(AutoConfigurations.of(MetricsAutoConfiguration.class)) + .run(context -> assertThat(context.containsBean("otelMeterRegistry")).isFalse()); + } + + @Test + void metricsDisabled() { + runner + .withConfiguration(AutoConfigurations.of(MetricsAutoConfiguration.class)) + .withPropertyValues("otel.instrumentation.micrometer.enabled = false") + .run(context -> assertThat(context.containsBean("otelMeterRegistry")).isFalse()); + } + + @Test + void noActuatorAutoConfiguration() { + runner + .withPropertyValues("otel.instrumentation.micrometer.enabled = true") + .run(context -> assertThat(context.containsBean("otelMeterRegistry")).isFalse()); + } +} diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/r2dbc/R2DbcInstrumentationAutoConfigurationTest.java b/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/r2dbc/R2DbcInstrumentationAutoConfigurationTest.java new file mode 100644 index 000000000000..3c3ec9c0d665 --- /dev/null +++ b/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/r2dbc/R2DbcInstrumentationAutoConfigurationTest.java @@ -0,0 +1,63 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.r2dbc; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.instrumentation.testing.junit.LibraryInstrumentationExtension; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import io.opentelemetry.sdk.autoconfigure.spi.internal.DefaultConfigProperties; +import io.opentelemetry.semconv.incubating.DbIncubatingAttributes; +import java.util.Collections; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.r2dbc.R2dbcAutoConfiguration; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.r2dbc.core.DatabaseClient; + +class R2DbcInstrumentationAutoConfigurationTest { + + @RegisterExtension + static final LibraryInstrumentationExtension testing = LibraryInstrumentationExtension.create(); + + private final ApplicationContextRunner runner = + new ApplicationContextRunner() + .withBean( + ConfigProperties.class, + () -> DefaultConfigProperties.createFromMap(Collections.emptyMap())) + .withConfiguration( + AutoConfigurations.of( + R2dbcInstrumentationAutoConfiguration.class, R2dbcAutoConfiguration.class)) + .withBean("openTelemetry", OpenTelemetry.class, testing::getOpenTelemetry); + + @Test + void statementSanitizerEnabledByDefault() { + runner.run( + context -> { + DatabaseClient client = context.getBean(DatabaseClient.class); + client + .sql( + "CREATE TABLE IF NOT EXISTS player(id INT NOT NULL AUTO_INCREMENT, name VARCHAR(255), age INT, PRIMARY KEY (id))") + .fetch() + .all() + .blockLast(); + client.sql("SELECT * FROM player WHERE id = 1").fetch().all().blockLast(); + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasAttribute( + DbIncubatingAttributes.DB_STATEMENT, + "CREATE TABLE IF NOT EXISTS player(id INT NOT NULL AUTO_INCREMENT, name VARCHAR(?), age INT, PRIMARY KEY (id))")), + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasAttribute( + DbIncubatingAttributes.DB_STATEMENT, + "SELECT * FROM player WHERE id = ?"))); + }); + } +} diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/web/SpringWebInstrumentationAutoConfigurationTest.java b/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/web/SpringWebInstrumentationAutoConfigurationTest.java new file mode 100644 index 000000000000..53bbd882a6d2 --- /dev/null +++ b/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/web/SpringWebInstrumentationAutoConfigurationTest.java @@ -0,0 +1,101 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.web; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import io.opentelemetry.sdk.autoconfigure.spi.internal.DefaultConfigProperties; +import java.util.Collections; +import org.junit.jupiter.api.Test; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.web.client.RestTemplate; + +class SpringWebInstrumentationAutoConfigurationTest { + + private final ApplicationContextRunner contextRunner = + new ApplicationContextRunner() + .withBean(OpenTelemetry.class, OpenTelemetry::noop) + .withBean( + ConfigProperties.class, + () -> DefaultConfigProperties.createFromMap(Collections.emptyMap())) + .withBean(RestTemplate.class, RestTemplate::new) + .withConfiguration( + AutoConfigurations.of(SpringWebInstrumentationAutoConfiguration.class)); + + /** + * Tests that users create {@link RestTemplate} bean is instrumented. + * + *

    {@code
    +   * @Bean public RestTemplate restTemplate() {
    +   *     return new RestTemplate();
    +   * }
    +   * }
    + */ + @Test + void instrumentationEnabled() { + contextRunner + .withPropertyValues("otel.instrumentation.spring-web.enabled=true") + .withPropertyValues("otel.instrumentation.common.default-enabled=false") + .run( + context -> { + assertThat( + context.getBean( + "otelRestTemplateBeanPostProcessor", RestTemplateBeanPostProcessor.class)) + .isNotNull(); + + assertThat( + context.getBean(RestTemplate.class).getInterceptors().stream() + .filter( + rti -> + rti.getClass() + .getName() + .startsWith("io.opentelemetry.instrumentation")) + .count()) + .isEqualTo(1); + }); + } + + @Test + void instrumentationDisabled() { + contextRunner + .withPropertyValues("otel.instrumentation.spring-web.enabled=false") + .run( + context -> + assertThat(context.containsBean("otelRestTemplateBeanPostProcessor")).isFalse()); + } + + @Test + void instrumentationDisabledButAllEnabled() { + contextRunner + .withPropertyValues("otel.instrumentation.spring-web.enabled=false") + .withPropertyValues("otel.instrumentation.common.default-enabled=true") + .run( + context -> + assertThat(context.containsBean("otelRestTemplateBeanPostProcessor")).isFalse()); + } + + @Test + void allInstrumentationDisabled() { + contextRunner + .withPropertyValues("otel.instrumentation.common.default-enabled=false") + .run( + context -> + assertThat(context.containsBean("otelRestTemplateBeanPostProcessor")).isFalse()); + } + + @Test + void defaultConfiguration() { + contextRunner.run( + context -> + assertThat( + context.getBean( + "otelRestTemplateBeanPostProcessor", RestTemplateBeanPostProcessor.class)) + .isNotNull()); + } +} diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/webflux/SpringWebfluxInstrumentationAutoConfigurationTest.java b/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/webflux/SpringWebfluxInstrumentationAutoConfigurationTest.java new file mode 100644 index 000000000000..d75814732785 --- /dev/null +++ b/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/webflux/SpringWebfluxInstrumentationAutoConfigurationTest.java @@ -0,0 +1,59 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.webflux; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import io.opentelemetry.sdk.autoconfigure.spi.internal.DefaultConfigProperties; +import java.util.Collections; +import org.junit.jupiter.api.Test; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; + +class SpringWebfluxInstrumentationAutoConfigurationTest { + + private final ApplicationContextRunner contextRunner = + new ApplicationContextRunner() + .withBean(OpenTelemetry.class, OpenTelemetry::noop) + .withBean( + ConfigProperties.class, + () -> DefaultConfigProperties.createFromMap(Collections.emptyMap())) + .withConfiguration( + AutoConfigurations.of(SpringWebfluxInstrumentationAutoConfiguration.class)); + + @Test + void instrumentationEnabled() { + contextRunner + .withPropertyValues("otel.instrumentation.spring-webflux.enabled=true") + .run( + context -> + assertThat( + context.getBean( + "otelWebClientBeanPostProcessor", WebClientBeanPostProcessor.class)) + .isNotNull()); + } + + @Test + void instrumentationDisabled() { + contextRunner + .withPropertyValues("otel.instrumentation.spring-webflux.enabled=false") + .run( + context -> + assertThat(context.containsBean("otelWebClientBeanPostProcessor")).isFalse()); + } + + @Test + void defaultConfiguration() { + contextRunner.run( + context -> + assertThat( + context.getBean( + "otelWebClientBeanPostProcessor", WebClientBeanPostProcessor.class)) + .isNotNull()); + } +} diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/webflux/WebClientBeanPostProcessorTest.java b/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/webflux/WebClientBeanPostProcessorTest.java new file mode 100644 index 000000000000..821f0fbfb647 --- /dev/null +++ b/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/webflux/WebClientBeanPostProcessorTest.java @@ -0,0 +1,118 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.webflux; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import io.opentelemetry.sdk.autoconfigure.spi.internal.DefaultConfigProperties; +import java.util.Collections; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.web.reactive.function.client.ExchangeFilterFunction; +import org.springframework.web.reactive.function.client.WebClient; + +class WebClientBeanPostProcessorTest { + private static final DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + + static { + beanFactory.registerSingleton("openTelemetry", OpenTelemetry.noop()); + beanFactory.registerSingleton( + "configProperties", DefaultConfigProperties.createFromMap(Collections.emptyMap())); + } + + @Test + @DisplayName( + "when processed bean is NOT of type WebClient or WebClientBuilder should return Object") + void returnsObject() { + BeanPostProcessor underTest = + new WebClientBeanPostProcessor( + beanFactory.getBeanProvider(OpenTelemetry.class), + beanFactory.getBeanProvider(ConfigProperties.class)); + + assertThat(underTest.postProcessAfterInitialization(new Object(), "testObject")) + .isExactlyInstanceOf(Object.class); + } + + @Test + @DisplayName("when processed bean is of type WebClient should return WebClient") + void returnsWebClient() { + BeanPostProcessor underTest = + new WebClientBeanPostProcessor( + beanFactory.getBeanProvider(OpenTelemetry.class), + beanFactory.getBeanProvider(ConfigProperties.class)); + + assertThat(underTest.postProcessAfterInitialization(WebClient.create(), "testWebClient")) + .isInstanceOf(WebClient.class); + } + + @Test + @DisplayName("when processed bean is of type WebClientBuilder should return WebClientBuilder") + void returnsWebClientBuilder() { + BeanPostProcessor underTest = + new WebClientBeanPostProcessor( + beanFactory.getBeanProvider(OpenTelemetry.class), + beanFactory.getBeanProvider(ConfigProperties.class)); + + assertThat( + underTest.postProcessAfterInitialization(WebClient.builder(), "testWebClientBuilder")) + .isInstanceOf(WebClient.Builder.class); + } + + @Test + @DisplayName("when processed bean is of type WebClient should add exchange filter to WebClient") + void addsExchangeFilterWebClient() { + BeanPostProcessor underTest = + new WebClientBeanPostProcessor( + beanFactory.getBeanProvider(OpenTelemetry.class), + beanFactory.getBeanProvider(ConfigProperties.class)); + + WebClient webClient = WebClient.create(); + Object processedWebClient = + underTest.postProcessAfterInitialization(webClient, "testWebClient"); + + assertThat(processedWebClient).isInstanceOf(WebClient.class); + ((WebClient) processedWebClient) + .mutate() + .filters( + functions -> + assertThat( + functions.stream() + .filter(WebClientBeanPostProcessorTest::isOtelExchangeFilter) + .count()) + .isEqualTo(1)); + } + + @Test + @DisplayName( + "when processed bean is of type WebClientBuilder should add ONE exchange filter to WebClientBuilder") + void addsExchangeFilterWebClientBuilder() { + BeanPostProcessor underTest = + new WebClientBeanPostProcessor( + beanFactory.getBeanProvider(OpenTelemetry.class), + beanFactory.getBeanProvider(ConfigProperties.class)); + + WebClient.Builder webClientBuilder = WebClient.builder(); + underTest.postProcessAfterInitialization(webClientBuilder, "testWebClientBuilder"); + underTest.postProcessAfterInitialization(webClientBuilder, "testWebClientBuilder"); + underTest.postProcessAfterInitialization(webClientBuilder, "testWebClientBuilder"); + + webClientBuilder.filters( + functions -> + assertThat( + functions.stream() + .filter(WebClientBeanPostProcessorTest::isOtelExchangeFilter) + .count()) + .isEqualTo(1)); + } + + private static boolean isOtelExchangeFilter(ExchangeFilterFunction wctf) { + return wctf.getClass().getName().startsWith("io.opentelemetry.instrumentation"); + } +} diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/webmvc/SpringWebMvcInstrumentation5AutoConfigurationTest.java b/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/webmvc/SpringWebMvcInstrumentation5AutoConfigurationTest.java new file mode 100644 index 000000000000..62112774516e --- /dev/null +++ b/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/webmvc/SpringWebMvcInstrumentation5AutoConfigurationTest.java @@ -0,0 +1,56 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.webmvc; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assumptions.assumeFalse; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import io.opentelemetry.sdk.autoconfigure.spi.internal.DefaultConfigProperties; +import java.util.Collections; +import javax.servlet.Filter; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; + +class SpringWebMvcInstrumentation5AutoConfigurationTest { + + private final ApplicationContextRunner contextRunner = + new ApplicationContextRunner() + .withBean(OpenTelemetry.class, OpenTelemetry::noop) + .withBean( + ConfigProperties.class, + () -> DefaultConfigProperties.createFromMap(Collections.emptyMap())) + .withConfiguration( + AutoConfigurations.of(SpringWebMvc5InstrumentationAutoConfiguration.class)); + + @BeforeEach + void setUp() { + assumeFalse(Boolean.getBoolean("testLatestDeps")); + } + + @Test + void instrumentationEnabled() { + contextRunner + .withPropertyValues("otel.instrumentation.spring-webmvc.enabled=true") + .run(context -> assertThat(context.getBean("otelWebMvcFilter", Filter.class)).isNotNull()); + } + + @Test + void instrumentationDisabled() { + contextRunner + .withPropertyValues("otel.instrumentation.spring-webmvc.enabled=false") + .run(context -> assertThat(context.containsBean("otelWebMvcFilter")).isFalse()); + } + + @Test + void defaultConfiguration() { + contextRunner.run( + context -> assertThat(context.getBean("otelWebMvcFilter", Filter.class)).isNotNull()); + } +} diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/properties/OtlpExporterPropertiesTest.java b/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/properties/OtlpExporterPropertiesTest.java new file mode 100644 index 000000000000..2fea20afec6e --- /dev/null +++ b/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/properties/OtlpExporterPropertiesTest.java @@ -0,0 +1,98 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.autoconfigure.internal.properties; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; + +import io.opentelemetry.instrumentation.spring.autoconfigure.OpenTelemetryAutoConfiguration; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import io.opentelemetry.sdk.autoconfigure.spi.internal.DefaultConfigProperties; +import java.util.Arrays; +import java.util.Collections; +import java.util.stream.Stream; +import org.junit.jupiter.api.DisplayName; +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; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.assertj.AssertableApplicationContext; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.core.env.Environment; +import org.springframework.expression.spel.standard.SpelExpressionParser; + +class OtlpExporterPropertiesTest { + + private static final String[] HEADER_KEYS = { + "otel.exporter.otlp.traces.headers", + "otel.exporter.otlp.metrics.headers", + "otel.exporter.otlp.logs.headers", + "otel.exporter.otlp.headers", + }; + + private final ApplicationContextRunner contextRunner = + new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(OpenTelemetryAutoConfiguration.class)) + .withPropertyValues( + "otel.traces.exporter=none", "otel.metrics.exporter=none", "otel.logs.exporter=none"); + + public static Stream headerKeys() { + return Arrays.stream(HEADER_KEYS).map(Arguments::of); + } + + @Test + @DisplayName("test all property types") + void allTypes() { + this.contextRunner + .withPropertyValues( + "otel.exporter.otlp.enabled=true", + "otel.exporter.otlp.timeout=1s", + "otel.exporter.otlp.compression=gzip") + .run( + context -> { + ConfigProperties config = getConfig(context); + assertThat(config.getString("otel.exporter.otlp.compression")).isEqualTo("gzip"); + assertThat(config.getBoolean("otel.exporter.otlp.enabled")).isTrue(); + assertThat(config.getDuration("otel.exporter.otlp.timeout")) + .isEqualByComparingTo(java.time.Duration.ofSeconds(1)); + }); + } + + @ParameterizedTest + @MethodSource("headerKeys") + @DisplayName("should map headers from spring properties") + void mapFlatHeaders(String key) { + this.contextRunner + .withSystemProperties(key + "=a=1,b=2") + .run( + context -> + assertThat(getConfig(context).getMap(key)) + .containsExactly(entry("a", "1"), entry("b", "2"))); + } + + @ParameterizedTest + @MethodSource("headerKeys") + @DisplayName("should map headers from spring application.yaml") + void mapObjectHeaders(String key) { + this.contextRunner + .withPropertyValues(key + ".a=1", key + ".b=2") + .run( + context -> + assertThat(getConfig(context).getMap(key)) + .containsExactly(entry("a", "1"), entry("b", "2"))); + } + + private static ConfigProperties getConfig(AssertableApplicationContext context) { + return new SpringConfigProperties( + context.getBean("environment", Environment.class), + new SpelExpressionParser(), + context.getBean(OtlpExporterProperties.class), + new OtelResourceProperties(), + new PropagationProperties(), + DefaultConfigProperties.createFromMap(Collections.emptyMap())); + } +} diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/resources/DistroVersionResourceProviderTest.java b/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/resources/DistroVersionResourceProviderTest.java new file mode 100644 index 000000000000..1c79c06de6f8 --- /dev/null +++ b/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/resources/DistroVersionResourceProviderTest.java @@ -0,0 +1,51 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.autoconfigure.internal.resources; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.google.common.collect.ImmutableMap; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.instrumentation.spring.autoconfigure.OpenTelemetryAutoConfiguration; +import io.opentelemetry.sdk.autoconfigure.spi.ResourceProvider; +import io.opentelemetry.sdk.autoconfigure.spi.internal.DefaultConfigProperties; +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; + +public class DistroVersionResourceProviderTest { + private final ApplicationContextRunner contextRunner = + new ApplicationContextRunner() + .withPropertyValues( + "otel.traces.exporter=none", "otel.metrics.exporter=none", "otel.logs.exporter=none") + .withConfiguration(AutoConfigurations.of(OpenTelemetryAutoConfiguration.class)); + + @Test + @DisplayName("distro version should be set") + void hasAttributes() { + + this.contextRunner.run( + context -> { + ResourceProvider resource = + context.getBean("otelDistroVersionResourceProvider", ResourceProvider.class); + + assertThat( + resource + .createResource(DefaultConfigProperties.createFromMap(ImmutableMap.of())) + .getAttributes() + .asMap()) + .containsEntry( + AttributeKey.stringKey("telemetry.distro.name"), + "opentelemetry-spring-boot-starter") + .anySatisfy( + (key, val) -> { + assertThat(key.getKey()).isEqualTo("telemetry.distro.version"); + assertThat(val).asString().isNotBlank(); + }); + }); + } +} diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/resources/SpringConfigPropertiesTest.java b/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/resources/SpringConfigPropertiesTest.java new file mode 100644 index 000000000000..97b30652aed5 --- /dev/null +++ b/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/resources/SpringConfigPropertiesTest.java @@ -0,0 +1,67 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.autoconfigure.internal.resources; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; + +import io.opentelemetry.instrumentation.spring.autoconfigure.OpenTelemetryAutoConfiguration; +import io.opentelemetry.instrumentation.spring.autoconfigure.internal.properties.OtelResourceProperties; +import io.opentelemetry.instrumentation.spring.autoconfigure.internal.properties.OtlpExporterProperties; +import io.opentelemetry.instrumentation.spring.autoconfigure.internal.properties.PropagationProperties; +import io.opentelemetry.instrumentation.spring.autoconfigure.internal.properties.SpringConfigProperties; +import io.opentelemetry.sdk.autoconfigure.spi.internal.DefaultConfigProperties; +import java.util.HashMap; +import java.util.Map; +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; +import org.springframework.core.env.Environment; +import org.springframework.expression.spel.standard.SpelExpressionParser; + +class SpringConfigPropertiesTest { + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner(); + + @Test + @DisplayName("when map is set in properties in a row it should be available in config") + void shouldInitializeAttributesByMapInArow() { + this.contextRunner + .withConfiguration(AutoConfigurations.of(OpenTelemetryAutoConfiguration.class)) + .withPropertyValues( + "otel.metrics.exporter=none", // to suppress confusing error log + "otel.logs.exporter=none", + "otel.traces.exporter=none", + "otel.resource.attributes.environment=dev", + "otel.resource.attributes.xyz=foo", + "otel.resource.attributes.service.instance.id=id-example") + .run( + context -> { + Environment env = context.getBean("environment", Environment.class); + Map fallback = new HashMap<>(); + fallback.put("fallback", "fallbackVal"); + fallback.put("otel.resource.attributes", "foo=fallback"); + + SpringConfigProperties config = + new SpringConfigProperties( + env, + new SpelExpressionParser(), + context.getBean(OtlpExporterProperties.class), + context.getBean(OtelResourceProperties.class), + context.getBean(PropagationProperties.class), + DefaultConfigProperties.createFromMap(fallback)); + + assertThat(config.getMap("otel.resource.attributes")) + .contains( + entry("environment", "dev"), + entry("xyz", "foo"), + entry("service.instance.id", "id-example"), + entry("foo", "fallback")); + + assertThat(config.getString("fallback")).isEqualTo("fallbackVal"); + }); + } +} diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/resources/SpringResourceProviderTest.java b/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/resources/SpringResourceProviderTest.java new file mode 100644 index 000000000000..b2c4bc3b521c --- /dev/null +++ b/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/resources/SpringResourceProviderTest.java @@ -0,0 +1,80 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.autoconfigure.internal.resources; + +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; + +import io.opentelemetry.instrumentation.spring.autoconfigure.OpenTelemetryAutoConfiguration; +import io.opentelemetry.instrumentation.spring.autoconfigure.internal.properties.OtelResourceProperties; +import io.opentelemetry.instrumentation.spring.autoconfigure.internal.properties.OtlpExporterProperties; +import io.opentelemetry.instrumentation.spring.autoconfigure.internal.properties.PropagationProperties; +import io.opentelemetry.instrumentation.spring.autoconfigure.internal.properties.SpringConfigProperties; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import io.opentelemetry.sdk.autoconfigure.spi.internal.DefaultConfigProperties; +import io.opentelemetry.sdk.testing.assertj.AttributesAssert; +import io.opentelemetry.semconv.ServiceAttributes; +import java.util.Collections; +import java.util.Properties; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.info.BuildProperties; +import org.springframework.boot.test.context.assertj.AssertableApplicationContext; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.core.env.Environment; + +public class SpringResourceProviderTest { + + private final ApplicationContextRunner contextRunner = + new ApplicationContextRunner() + .withPropertyValues( + "otel.traces.exporter=none", "otel.metrics.exporter=none", "otel.logs.exporter=none") + .withConfiguration(AutoConfigurations.of(OpenTelemetryAutoConfiguration.class)); + + @Test + @DisplayName( + "when spring.application.name is set value should be passed to service name attribute") + void shouldDetermineServiceNameBySpringApplicationName() { + this.contextRunner + .withPropertyValues("spring.application.name=myapp-backend") + .run( + context -> + assertResourceAttributes(context) + .containsEntry(ServiceAttributes.SERVICE_NAME, "myapp-backend")); + } + + @Test + @DisplayName( + "when spring.application.name is set value should be passed to service name attribute") + void shouldDetermineServiceNameAndVersionBySpringApplicationVersion() { + Properties properties = new Properties(); + properties.put("name", "demo"); + properties.put("version", "0.3"); + this.contextRunner + .withBean("buildProperties", BuildProperties.class, () -> new BuildProperties(properties)) + .run( + context -> + assertResourceAttributes(context) + .containsEntry(ServiceAttributes.SERVICE_NAME, "demo") + .containsEntry(ServiceAttributes.SERVICE_VERSION, "0.3")); + } + + private static AttributesAssert assertResourceAttributes(AssertableApplicationContext context) { + ConfigProperties configProperties = + SpringConfigProperties.create( + context.getBean(Environment.class), + new OtlpExporterProperties(), + new OtelResourceProperties(), + new PropagationProperties(), + DefaultConfigProperties.createFromMap(Collections.emptyMap())); + + return assertThat( + context + .getBean(SpringResourceProvider.class) + .createResource(configProperties) + .getAttributes()); + } +} diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/kafka/KafkaIntegrationTest.java b/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/kafka/KafkaIntegrationTest.java deleted file mode 100644 index a086bed7b397..000000000000 --- a/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/kafka/KafkaIntegrationTest.java +++ /dev/null @@ -1,165 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.spring.autoconfigure.kafka; - -import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; -import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.satisfies; - -import io.opentelemetry.api.OpenTelemetry; -import io.opentelemetry.api.trace.SpanKind; -import io.opentelemetry.instrumentation.testing.junit.LibraryInstrumentationExtension; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; -import java.time.Duration; -import org.apache.kafka.clients.admin.NewTopic; -import org.apache.kafka.clients.consumer.ConsumerRecord; -import org.assertj.core.api.AbstractLongAssert; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.RegisterExtension; -import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.autoconfigure.kafka.KafkaAutoConfiguration; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; -import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.kafka.annotation.KafkaListener; -import org.springframework.kafka.config.TopicBuilder; -import org.springframework.kafka.core.KafkaTemplate; -import org.testcontainers.containers.KafkaContainer; -import org.testcontainers.containers.wait.strategy.Wait; -import org.testcontainers.utility.DockerImageName; - -class KafkaIntegrationTest { - - @RegisterExtension - static final LibraryInstrumentationExtension testing = LibraryInstrumentationExtension.create(); - - static KafkaContainer kafka; - - private ApplicationContextRunner contextRunner; - - @BeforeAll - static void setUpKafka() { - kafka = - new KafkaContainer(DockerImageName.parse("confluentinc/cp-kafka:6.2.10")) - .withEnv("KAFKA_HEAP_OPTS", "-Xmx256m") - .waitingFor(Wait.forLogMessage(".*started \\(kafka.server.KafkaServer\\).*", 1)) - .withStartupTimeout(Duration.ofMinutes(1)); - kafka.start(); - } - - @AfterAll - static void tearDownKafka() { - kafka.stop(); - } - - @BeforeEach - void setUpContext() { - contextRunner = - new ApplicationContextRunner() - .withConfiguration( - AutoConfigurations.of( - KafkaAutoConfiguration.class, - KafkaInstrumentationAutoConfiguration.class, - TestConfig.class)) - .withBean("openTelemetry", OpenTelemetry.class, testing::getOpenTelemetry) - .withPropertyValues( - "spring.kafka.bootstrap-servers=" + kafka.getBootstrapServers(), - "spring.kafka.consumer.auto-offset-reset=earliest", - "spring.kafka.consumer.linger-ms=10", - "spring.kafka.listener.idle-between-polls=1000", - "spring.kafka.producer.transaction-id-prefix=test-"); - } - - @Test - void shouldInstrumentProducerAndConsumer() { - contextRunner.run(KafkaIntegrationTest::runShouldInstrumentProducerAndConsumer); - } - - // In kafka 2 ops.send is deprecated. We are using it to avoid reflection because kafka 3 also has - // ops.send, although with different return type. - @SuppressWarnings({"unchecked", "deprecation"}) - private static void runShouldInstrumentProducerAndConsumer( - ConfigurableApplicationContext applicationContext) { - KafkaTemplate kafkaTemplate = applicationContext.getBean(KafkaTemplate.class); - - testing.runWithSpan( - "producer", - () -> { - kafkaTemplate.executeInTransaction( - ops -> { - ops.send("testTopic", "10", "testSpan"); - return 0; - }); - }); - - testing.waitAndAssertTraces( - trace -> - trace.hasSpansSatisfyingExactly( - span -> span.hasName("producer"), - span -> - span.hasName("testTopic send") - .hasKind(SpanKind.PRODUCER) - .hasParent(trace.getSpan(0)) - .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.MESSAGING_SYSTEM, "kafka"), - equalTo(SemanticAttributes.MESSAGING_DESTINATION_NAME, "testTopic"), - satisfies( - SemanticAttributes.MESSAGING_KAFKA_CLIENT_ID, - stringAssert -> stringAssert.startsWith("producer")), - satisfies( - SemanticAttributes.MESSAGING_KAFKA_DESTINATION_PARTITION, - AbstractLongAssert::isNotNegative), - satisfies( - SemanticAttributes.MESSAGING_KAFKA_MESSAGE_OFFSET, - AbstractLongAssert::isNotNegative), - equalTo(SemanticAttributes.MESSAGING_KAFKA_MESSAGE_KEY, "10")), - span -> - span.hasName("testTopic process") - .hasKind(SpanKind.CONSUMER) - .hasParent(trace.getSpan(1)) - .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.MESSAGING_SYSTEM, "kafka"), - equalTo(SemanticAttributes.MESSAGING_DESTINATION_NAME, "testTopic"), - equalTo(SemanticAttributes.MESSAGING_OPERATION, "process"), - satisfies( - SemanticAttributes.MESSAGING_MESSAGE_PAYLOAD_SIZE_BYTES, - AbstractLongAssert::isNotNegative), - satisfies( - SemanticAttributes.MESSAGING_KAFKA_SOURCE_PARTITION, - AbstractLongAssert::isNotNegative), - satisfies( - SemanticAttributes.MESSAGING_KAFKA_MESSAGE_OFFSET, - AbstractLongAssert::isNotNegative), - equalTo(SemanticAttributes.MESSAGING_KAFKA_MESSAGE_KEY, "10"), - equalTo( - SemanticAttributes.MESSAGING_KAFKA_CONSUMER_GROUP, "testListener"), - satisfies( - SemanticAttributes.MESSAGING_KAFKA_CLIENT_ID, - stringAssert -> stringAssert.startsWith("consumer")), - satisfies( - SemanticAttributes.MESSAGING_CONSUMER_ID, - stringAssert -> - stringAssert.startsWith("testListener - consumer"))), - span -> span.hasName("consumer").hasParent(trace.getSpan(2)))); - } - - @Configuration - static class TestConfig { - - @Bean - public NewTopic testTopic() { - return TopicBuilder.name("testTopic").partitions(1).replicas(1).build(); - } - - @KafkaListener(id = "testListener", topics = "testTopic") - public void listener(ConsumerRecord record) { - testing.runWithSpan("consumer", () -> {}); - } - } -} diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/metrics/MicrometerShimAutoConfigurationTest.java b/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/metrics/MicrometerShimAutoConfigurationTest.java deleted file mode 100644 index 4e62b622ee6b..000000000000 --- a/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/metrics/MicrometerShimAutoConfigurationTest.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.spring.autoconfigure.metrics; - -import static org.assertj.core.api.Assertions.assertThat; - -import io.micrometer.core.instrument.MeterRegistry; -import io.opentelemetry.api.GlobalOpenTelemetry; -import io.opentelemetry.instrumentation.micrometer.v1_5.OpenTelemetryMeterRegistry; -import io.opentelemetry.instrumentation.spring.autoconfigure.OpenTelemetryAutoConfiguration; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.springframework.boot.actuate.autoconfigure.metrics.MetricsAutoConfiguration; -import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; - -class MicrometerShimAutoConfigurationTest { - - private final ApplicationContextRunner runner = - new ApplicationContextRunner() - .withConfiguration( - AutoConfigurations.of( - OpenTelemetryAutoConfiguration.class, MicrometerShimAutoConfiguration.class)); - - @BeforeEach - void resetGlobalLoggerProvider() { - GlobalOpenTelemetry.resetForTest(); - } - - @Test - void metricsEnabled() { - runner - .withConfiguration(AutoConfigurations.of(MetricsAutoConfiguration.class)) - .withPropertyValues("otel.springboot.micrometer.enabled = true") - .run( - context -> - assertThat(context.getBean("micrometerShim", MeterRegistry.class)) - .isNotNull() - .isInstanceOf(OpenTelemetryMeterRegistry.class)); - } - - @Test - void metricsEnabledByDefault() { - runner - .withConfiguration(AutoConfigurations.of(MetricsAutoConfiguration.class)) - .run( - context -> - assertThat(context.getBean("micrometerShim", MeterRegistry.class)) - .isNotNull() - .isInstanceOf(OpenTelemetryMeterRegistry.class)); - } - - @Test - void metricsDisabled() { - runner - .withConfiguration(AutoConfigurations.of(MetricsAutoConfiguration.class)) - .withPropertyValues("otel.springboot.micrometer.enabled = false") - .run(context -> assertThat(context.containsBean("micrometerShim")).isFalse()); - } - - @Test - void noActuatorAutoConfiguration() { - runner - .withPropertyValues("otel.springboot.micrometer.enabled = true") - .run(context -> assertThat(context.containsBean("micrometerShim")).isFalse()); - } -} diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/propagators/PropagationAutoConfigurationTest.java b/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/propagators/PropagationAutoConfigurationTest.java deleted file mode 100644 index b5bc165f0ced..000000000000 --- a/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/propagators/PropagationAutoConfigurationTest.java +++ /dev/null @@ -1,114 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.spring.autoconfigure.propagators; - -import static org.assertj.core.api.Assertions.assertThat; - -import io.opentelemetry.api.GlobalOpenTelemetry; -import io.opentelemetry.context.propagation.TextMapPropagator; -import io.opentelemetry.instrumentation.spring.autoconfigure.OpenTelemetryAutoConfiguration; -import org.junit.jupiter.api.BeforeEach; -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; - -class PropagationAutoConfigurationTest { - - private final ApplicationContextRunner contextRunner = - new ApplicationContextRunner() - .withConfiguration( - AutoConfigurations.of( - OpenTelemetryAutoConfiguration.class, PropagationAutoConfiguration.class)); - - @BeforeEach - void resetGlobalLoggerProvider() { - GlobalOpenTelemetry.resetForTest(); - } - - @Test - @DisplayName("when propagation is ENABLED should initialize PropagationAutoConfiguration bean") - void shouldBeConfigured() { - - this.contextRunner - .withPropertyValues("otel.propagation.enabled=true") - .run(context -> assertThat(context.containsBean("propagationAutoConfiguration")).isTrue()); - } - - @Test - @DisplayName( - "when propagation is DISABLED should NOT initialize PropagationAutoConfiguration bean") - void shouldNotBeConfigured() { - - this.contextRunner - .withPropertyValues("otel.propagation.enabled=false") - .run(context -> assertThat(context.containsBean("propagationAutoConfiguration")).isFalse()); - } - - @Test - @DisplayName( - "when propagation enabled property is MISSING should initialize PropagationAutoConfiguration bean") - void noProperty() { - this.contextRunner.run( - context -> assertThat(context.containsBean("propagationAutoConfiguration")).isTrue()); - } - - @Test - @DisplayName("when no propagators are defined should contain default propagators") - void shouldContainDefaults() { - - this.contextRunner.run( - context -> - assertThat( - context.getBean("compositeTextMapPropagator", TextMapPropagator.class).fields()) - .contains("traceparent", "baggage")); - } - - @Test - @DisplayName("when propagation is set to b3 should contain only b3 propagator") - void shouldContainB3() { - this.contextRunner - .withPropertyValues("otel.propagation.type=b3") - .run( - context -> { - TextMapPropagator compositePropagator = - context.getBean("compositeTextMapPropagator", TextMapPropagator.class); - - assertThat(compositePropagator.fields()) - .contains("b3") - .doesNotContain("baggage", "traceparent"); - }); - } - - @Test - @DisplayName("when propagation is set to unsupported value should create an empty propagator") - void shouldCreateNoop() { - - this.contextRunner - .withPropertyValues("otel.propagation.type=invalid") - .run( - context -> { - TextMapPropagator compositePropagator = - context.getBean("compositeTextMapPropagator", TextMapPropagator.class); - - assertThat(compositePropagator.fields()).isEmpty(); - }); - } - - @Test - @DisplayName("when propagation is set to some values should contain only supported values") - void shouldContainOnlySupported() { - this.contextRunner - .withPropertyValues("otel.propagation.type=invalid,b3") - .run( - context -> { - TextMapPropagator compositePropagator = - context.getBean("compositeTextMapPropagator", TextMapPropagator.class); - - assertThat(compositePropagator.fields()).containsExactly("b3"); - }); - } -} diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/propagators/PropagationPropertiesTest.java b/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/propagators/PropagationPropertiesTest.java deleted file mode 100644 index 7d0dbbe2306c..000000000000 --- a/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/propagators/PropagationPropertiesTest.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.spring.autoconfigure.propagators; - -import static org.assertj.core.api.Assertions.assertThat; - -import io.opentelemetry.api.GlobalOpenTelemetry; -import io.opentelemetry.instrumentation.spring.autoconfigure.OpenTelemetryAutoConfiguration; -import java.util.Arrays; -import org.junit.jupiter.api.BeforeEach; -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; - -public class PropagationPropertiesTest { - - private final ApplicationContextRunner contextRunner = - new ApplicationContextRunner() - .withConfiguration( - AutoConfigurations.of( - OpenTelemetryAutoConfiguration.class, PropagationAutoConfiguration.class)); - - @BeforeEach - void resetGlobalLoggerProvider() { - GlobalOpenTelemetry.resetForTest(); - } - - @Test - @DisplayName("when propagation is SET should set PropagationProperties with given propagators") - void hasType() { - - this.contextRunner - .withPropertyValues("otel.propagation.type=xray,b3") - .run( - context -> { - PropagationProperties propertiesBean = context.getBean(PropagationProperties.class); - - assertThat(propertiesBean.getType()).isEqualTo(Arrays.asList("xray", "b3")); - }); - } - - @Test - @DisplayName("when propagation is DEFAULT should set PropagationProperties to default values") - void hasDefaultTypes() { - - this.contextRunner.run( - context -> - assertThat(context.getBean(PropagationProperties.class).getType()) - .containsExactly("tracecontext", "baggage")); - } -} diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/resources/OtelResourceAutoConfigurationTest.java b/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/resources/OtelResourceAutoConfigurationTest.java deleted file mode 100644 index e0ac7e58320f..000000000000 --- a/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/resources/OtelResourceAutoConfigurationTest.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.spring.autoconfigure.resources; - -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.BeforeEach; -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; - -public class OtelResourceAutoConfigurationTest { - private final ApplicationContextRunner contextRunner = - new ApplicationContextRunner() - .withConfiguration( - AutoConfigurations.of( - OtelResourceAutoConfiguration.class, OpenTelemetryAutoConfiguration.class)); - - @BeforeEach - void resetGlobalLoggerProvider() { - GlobalOpenTelemetry.resetForTest(); - } - - @Test - @DisplayName( - "when otel.springboot.resource.enabled is set to true configuration should be initialized") - void shouldDetermineServiceNameByOtelServiceName() { - this.contextRunner - .withPropertyValues("otel.springboot.resource.enabled=true") - .run(context -> assertThat(context.containsBean("otelResourceProvider")).isTrue()); - } - - @Test - @DisplayName( - "when otel.springboot.resource.enabled is not specified configuration should be initialized") - void shouldInitAutoConfigurationByDefault() { - this.contextRunner.run( - context -> assertThat(context.containsBean("otelResourceProvider")).isTrue()); - } - - @Test - @DisplayName( - "when otel.springboot.resource.enabled is set to false configuration should NOT be initialized") - void shouldNotInitAutoConfiguration() { - this.contextRunner - .withPropertyValues("otel.springboot.resource.enabled=false") - .run(context -> assertThat(context.containsBean("otelResourceProvider")).isFalse()); - } -} diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/resources/OtelResourcePropertiesTest.java b/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/resources/OtelResourcePropertiesTest.java deleted file mode 100644 index 89881bafa64c..000000000000 --- a/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/resources/OtelResourcePropertiesTest.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.spring.autoconfigure.resources; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.entry; - -import io.opentelemetry.api.GlobalOpenTelemetry; -import org.junit.jupiter.api.BeforeEach; -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; - -public class OtelResourcePropertiesTest { - private final ApplicationContextRunner contextRunner = - new ApplicationContextRunner() - .withPropertyValues("otel.springboot.resource.enabled=true") - .withConfiguration(AutoConfigurations.of(OtelResourceAutoConfiguration.class)); - - @BeforeEach - void resetGlobalLoggerProvider() { - GlobalOpenTelemetry.resetForTest(); - } - - @Test - @DisplayName("when attributes are SET should set OtelResourceProperties with given attributes") - void hasAttributes() { - - this.contextRunner - .withPropertyValues( - "otel.springboot.resource.attributes.environment=dev", - "otel.springboot.resource.attributes.xyz=foo", - "otel.springboot.resource.attributes.service.name=backend-name", - "otel.springboot.resource.attributes.service.instance.id=id-example") - .run( - context -> { - OtelResourceProperties propertiesBean = context.getBean(OtelResourceProperties.class); - - assertThat(propertiesBean.getAttributes()) - .contains( - entry("environment", "dev"), - entry("xyz", "foo"), - entry("service.name", "backend-name"), - entry("service.instance.id", "id-example")); - }); - } - - @Test - @DisplayName("when attributes are DEFAULT should set OtelResourceProperties to default values") - void hasDefaultTypes() { - - this.contextRunner.run( - context -> - assertThat(context.getBean(OtelResourceProperties.class).getAttributes()).isEmpty()); - } -} diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/resources/SpringResourceConfigPropertiesTest.java b/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/resources/SpringResourceConfigPropertiesTest.java deleted file mode 100644 index 969a81d5a369..000000000000 --- a/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/resources/SpringResourceConfigPropertiesTest.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.spring.autoconfigure.resources; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.entry; - -import io.opentelemetry.api.GlobalOpenTelemetry; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; -import org.springframework.core.env.Environment; -import org.springframework.expression.spel.standard.SpelExpressionParser; - -class SpringResourceConfigPropertiesTest { - private final ApplicationContextRunner contextRunner = new ApplicationContextRunner(); - - @BeforeEach - void resetGlobalLoggerProvider() { - GlobalOpenTelemetry.resetForTest(); - } - - @Test - @DisplayName("when map is set in properties in a row it should be available in config") - void shouldInitializeAttributesByMapInArow() { - this.contextRunner - .withPropertyValues( - "otel.springboot.test.map={'environment':'dev','xyz':'foo','service.instance.id':'id-example'}") - .run( - context -> { - Environment env = context.getBean("environment", Environment.class); - SpringResourceConfigProperties config = - new SpringResourceConfigProperties(env, new SpelExpressionParser()); - - assertThat(config.getMap("otel.springboot.test.map")) - .contains( - entry("environment", "dev"), - entry("xyz", "foo"), - entry("service.instance.id", "id-example")); - }); - } -} diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/webmvc/WebMvcFilterAutoConfigurationSpring6Test.java b/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/webmvc/WebMvcFilterAutoConfigurationSpring6Test.java deleted file mode 100644 index 22ab4809381d..000000000000 --- a/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/webmvc/WebMvcFilterAutoConfigurationSpring6Test.java +++ /dev/null @@ -1,64 +0,0 @@ -/* - * 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 static org.junit.jupiter.api.Assumptions.assumeTrue; - -import io.opentelemetry.api.GlobalOpenTelemetry; -import io.opentelemetry.instrumentation.spring.autoconfigure.OpenTelemetryAutoConfiguration; -import jakarta.servlet.Filter; -import org.junit.jupiter.api.BeforeEach; -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 WebMvcFilterAutoConfigurationSpring6}. */ -class WebMvcFilterAutoConfigurationSpring6Test { - private final ApplicationContextRunner contextRunner = - new ApplicationContextRunner() - .withConfiguration( - AutoConfigurations.of( - OpenTelemetryAutoConfiguration.class, - WebMvcFilterAutoConfigurationSpring6.class)); - - @BeforeEach - void setUp() { - assumeTrue(Boolean.getBoolean("testLatestDeps")); - 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("otelWebMvcInstrumentationFilter", Filter.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("otelWebMvcInstrumentationFilter")).isFalse()); - } - - @Test - @DisplayName("when web property is MISSING should initialize WebMvcTracingFilter bean") - void noProperty() { - this.contextRunner.run( - context -> - assertThat(context.getBean("otelWebMvcInstrumentationFilter", Filter.class)) - .isNotNull()); - } -} diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/webmvc/WebMvcFilterAutoConfigurationTest.java b/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/webmvc/WebMvcFilterAutoConfigurationTest.java deleted file mode 100644 index 4e44a5cbcf54..000000000000 --- a/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/webmvc/WebMvcFilterAutoConfigurationTest.java +++ /dev/null @@ -1,63 +0,0 @@ -/* - * 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 static org.junit.jupiter.api.Assumptions.assumeFalse; - -import io.opentelemetry.api.GlobalOpenTelemetry; -import io.opentelemetry.instrumentation.spring.autoconfigure.OpenTelemetryAutoConfiguration; -import javax.servlet.Filter; -import org.junit.jupiter.api.BeforeEach; -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)); - - @BeforeEach - void setUp() { - assumeFalse(Boolean.getBoolean("testLatestDeps")); - 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("otelWebMvcInstrumentationFilter", Filter.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("otelWebMvcInstrumentationFilter")).isFalse()); - } - - @Test - @DisplayName("when web property is MISSING should initialize WebMvcTracingFilter bean") - void noProperty() { - this.contextRunner.run( - context -> - assertThat(context.getBean("otelWebMvcInstrumentationFilter", Filter.class)) - .isNotNull()); - } -} diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/testLogbackAppender/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/logging/LogbackAppenderTest.java b/instrumentation/spring/spring-boot-autoconfigure/src/testLogbackAppender/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/logging/LogbackAppenderTest.java new file mode 100644 index 000000000000..61200b2c6465 --- /dev/null +++ b/instrumentation/spring/spring-boot-autoconfigure/src/testLogbackAppender/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/logging/LogbackAppenderTest.java @@ -0,0 +1,113 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.logging; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.instrumentation.logback.appender.v1_0.OpenTelemetryAppender; +import io.opentelemetry.instrumentation.testing.internal.AutoCleanupExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.LibraryInstrumentationExtension; +import io.opentelemetry.sdk.logs.data.LogRecordData; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.slf4j.LoggerFactory; +import org.slf4j.MDC; +import org.springframework.boot.SpringApplication; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +class LogbackAppenderTest { + + @RegisterExtension + static final InstrumentationExtension testing = LibraryInstrumentationExtension.create(); + + @RegisterExtension static final AutoCleanupExtension cleanup = AutoCleanupExtension.create(); + + @BeforeEach + void setUp() { + // reset the appender + OpenTelemetryAppender.install(null); + } + + @Configuration + static class TestingOpenTelemetryConfiguration { + + @Bean + public OpenTelemetry openTelemetry() { + return testing.getOpenTelemetry(); + } + } + + @Test + void shouldInitializeAppender() { + Map properties = new HashMap<>(); + properties.put("logging.config", "classpath:logback-test.xml"); + properties.put( + "otel.instrumentation.logback-appender.experimental.capture-mdc-attributes", "*"); + properties.put( + "otel.instrumentation.logback-appender.experimental.capture-code-attributes", false); + + SpringApplication app = + new SpringApplication( + TestingOpenTelemetryConfiguration.class, OpenTelemetryAppenderAutoConfiguration.class); + app.setDefaultProperties(properties); + ConfigurableApplicationContext context = app.run(); + cleanup.deferCleanup(context); + + MDC.put("key1", "val1"); + MDC.put("key2", "val2"); + try { + LoggerFactory.getLogger("test").info("test log message"); + } finally { + MDC.clear(); + } + + List logRecords = testing.logRecords(); + assertThat(logRecords) + .satisfiesOnlyOnce( + // OTel appender automatically added or from an XML file, it should not + // be added a second time by LogbackAppenderApplicationListener + logRecord -> { + assertThat(logRecord.getInstrumentationScopeInfo().getName()).isEqualTo("test"); + assertThat(logRecord.getBody().asString()).contains("test log message"); + + Attributes attributes = logRecord.getAttributes(); + // key1 and key2, the code attributes should not be present because they are enabled + // in the logback.xml file but are disabled with a property + assertThat(attributes.size()).isEqualTo(2); + assertThat(attributes.asMap()) + .containsEntry(AttributeKey.stringKey("key1"), "val1") + .containsEntry(AttributeKey.stringKey("key2"), "val2"); + }); + } + + @Test + void shouldNotInitializeAppenderWhenDisabled() { + Map properties = new HashMap<>(); + properties.put("logging.config", "classpath:logback-test.xml"); + properties.put("otel.springboot.logback-appender.enabled", "false"); + + SpringApplication app = + new SpringApplication( + TestingOpenTelemetryConfiguration.class, OpenTelemetryAppenderAutoConfiguration.class); + app.setDefaultProperties(properties); + ConfigurableApplicationContext context = app.run(); + cleanup.deferCleanup(context); + + LoggerFactory.getLogger("test").info("test log message"); + + assertThat(testing.logRecords()).isEmpty(); + } +} diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/testLogbackAppender/resources/logback-test.xml b/instrumentation/spring/spring-boot-autoconfigure/src/testLogbackAppender/resources/logback-test.xml new file mode 100644 index 000000000000..9b88dc0e09ca --- /dev/null +++ b/instrumentation/spring/spring-boot-autoconfigure/src/testLogbackAppender/resources/logback-test.xml @@ -0,0 +1,21 @@ + + + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + true + + + + + + + + diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/testLogbackMissing/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/logging/LogbackMissingTest.java b/instrumentation/spring/spring-boot-autoconfigure/src/testLogbackMissing/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/logging/LogbackMissingTest.java new file mode 100644 index 000000000000..c40d881bc94b --- /dev/null +++ b/instrumentation/spring/spring-boot-autoconfigure/src/testLogbackMissing/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/logging/LogbackMissingTest.java @@ -0,0 +1,26 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.logging; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.springframework.boot.SpringApplication; +import org.springframework.context.ConfigurableApplicationContext; + +class LogbackMissingTest { + + @Test + void applicationStartsWhenLogbackIsMissing() { + // verify that logback is not present + Assertions.assertThrows( + ClassNotFoundException.class, () -> Class.forName("ch.qos.logback.core.Appender")); + + SpringApplication app = new SpringApplication(OpenTelemetryAppenderAutoConfiguration.class); + try (ConfigurableApplicationContext ignore = app.run()) { + // do nothing + } + } +} diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/testSpring3/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/web/RestClientInstrumentationAutoConfigurationTest.java b/instrumentation/spring/spring-boot-autoconfigure/src/testSpring3/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/web/RestClientInstrumentationAutoConfigurationTest.java new file mode 100644 index 000000000000..bae340b948ef --- /dev/null +++ b/instrumentation/spring/spring-boot-autoconfigure/src/testSpring3/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/web/RestClientInstrumentationAutoConfigurationTest.java @@ -0,0 +1,87 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.web; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import io.opentelemetry.sdk.autoconfigure.spi.internal.DefaultConfigProperties; +import java.util.Collections; +import org.junit.jupiter.api.Test; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.web.client.RestClient; + +class RestClientInstrumentationAutoConfigurationTest { + + private final ApplicationContextRunner contextRunner = + new ApplicationContextRunner() + .withBean(OpenTelemetry.class, OpenTelemetry::noop) + .withBean( + ConfigProperties.class, + () -> DefaultConfigProperties.createFromMap(Collections.emptyMap())) + .withBean(RestClient.class, RestClient::create) + .withConfiguration( + AutoConfigurations.of(RestClientInstrumentationAutoConfiguration.class)); + + /** + * Tests the case that users create a {@link RestClient} bean themselves. + * + *
    {@code
    +   * @Bean public RestClient restClient() {
    +   *     return new RestClient();
    +   * }
    +   * }
    + */ + @Test + void instrumentationEnabled() { + contextRunner + .withPropertyValues("otel.instrumentation.spring-web.enabled=true") + .run( + context -> { + assertThat( + context.getBean( + "otelRestClientBeanPostProcessor", RestClientBeanPostProcessor.class)) + .isNotNull(); + + context + .getBean(RestClient.class) + .mutate() + .requestInterceptors( + interceptors -> { + long count = + interceptors.stream() + .filter( + rti -> + rti.getClass() + .getName() + .startsWith("io.opentelemetry.instrumentation")) + .count(); + assertThat(count).isEqualTo(1); + }); + }); + } + + @Test + void instrumentationDisabled() { + contextRunner + .withPropertyValues("otel.instrumentation.spring-web.enabled=false") + .run( + context -> + assertThat(context.containsBean("otelRestClientBeanPostProcessor")).isFalse()); + } + + @Test + void defaultConfiguration() { + contextRunner.run( + context -> + assertThat( + context.getBean( + "otelRestClientBeanPostProcessor", RestClientBeanPostProcessor.class)) + .isNotNull()); + } +} diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/testSpring3/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/webmvc/SpringWebMvcInstrumentation6AutoConfigurationTest.java b/instrumentation/spring/spring-boot-autoconfigure/src/testSpring3/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/webmvc/SpringWebMvcInstrumentation6AutoConfigurationTest.java new file mode 100644 index 000000000000..d064262095c6 --- /dev/null +++ b/instrumentation/spring/spring-boot-autoconfigure/src/testSpring3/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/webmvc/SpringWebMvcInstrumentation6AutoConfigurationTest.java @@ -0,0 +1,56 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.webmvc; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import io.opentelemetry.sdk.autoconfigure.spi.internal.DefaultConfigProperties; +import jakarta.servlet.Filter; +import java.util.Collections; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; + +class SpringWebMvcInstrumentation6AutoConfigurationTest { + + private final ApplicationContextRunner contextRunner = + new ApplicationContextRunner() + .withBean(OpenTelemetry.class, OpenTelemetry::noop) + .withBean( + ConfigProperties.class, + () -> DefaultConfigProperties.createFromMap(Collections.emptyMap())) + .withConfiguration( + AutoConfigurations.of(SpringWebMvc6InstrumentationAutoConfiguration.class)); + + @BeforeEach + void setUp() { + assumeTrue(Boolean.getBoolean("testLatestDeps")); + } + + @Test + void instrumentationEnabled() { + this.contextRunner + .withPropertyValues("otel.instrumentation.spring-webmvc.enabled=true") + .run(context -> assertThat(context.getBean("otelWebMvcFilter", Filter.class)).isNotNull()); + } + + @Test + void instrumentationDisabled() { + this.contextRunner + .withPropertyValues("otel.instrumentation.spring-webmvc.enabled=false") + .run(context -> assertThat(context.containsBean("otelWebMvcFilter")).isFalse()); + } + + @Test + void defaultConfiguration() { + this.contextRunner.run( + context -> assertThat(context.getBean("otelWebMvcFilter", Filter.class)).isNotNull()); + } +} diff --git a/instrumentation/spring/spring-boot-resources/testing/build.gradle.kts b/instrumentation/spring/spring-boot-resources/javaagent-unit-tests/build.gradle.kts similarity index 94% rename from instrumentation/spring/spring-boot-resources/testing/build.gradle.kts rename to instrumentation/spring/spring-boot-resources/javaagent-unit-tests/build.gradle.kts index 7d9a1e72392d..d24437225593 100644 --- a/instrumentation/spring/spring-boot-resources/testing/build.gradle.kts +++ b/instrumentation/spring/spring-boot-resources/javaagent-unit-tests/build.gradle.kts @@ -6,7 +6,7 @@ dependencies { testCompileOnly("com.google.auto.service:auto-service-annotations") testImplementation("org.mockito:mockito-junit-jupiter") - testImplementation(project(":instrumentation:spring:spring-boot-resources:library")) + testImplementation(project(":instrumentation:spring:spring-boot-resources:javaagent")) testImplementation("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure-spi") testImplementation(project(path = ":smoke-tests:images:spring-boot", configuration = "springBootJar")) } diff --git a/instrumentation/spring/spring-boot-resources/testing/src/test/java/io/opentelemetry/instrumentation/spring/resources/TestBootInfClassesResource.java b/instrumentation/spring/spring-boot-resources/javaagent-unit-tests/src/test/java/io/opentelemetry/instrumentation/spring/resources/TestBootInfClassesResource.java similarity index 85% rename from instrumentation/spring/spring-boot-resources/testing/src/test/java/io/opentelemetry/instrumentation/spring/resources/TestBootInfClassesResource.java rename to instrumentation/spring/spring-boot-resources/javaagent-unit-tests/src/test/java/io/opentelemetry/instrumentation/spring/resources/TestBootInfClassesResource.java index d2a01e929d60..ec268062632c 100644 --- a/instrumentation/spring/spring-boot-resources/testing/src/test/java/io/opentelemetry/instrumentation/spring/resources/TestBootInfClassesResource.java +++ b/instrumentation/spring/spring-boot-resources/javaagent-unit-tests/src/test/java/io/opentelemetry/instrumentation/spring/resources/TestBootInfClassesResource.java @@ -5,11 +5,11 @@ package io.opentelemetry.instrumentation.spring.resources; -import static io.opentelemetry.semconv.resource.attributes.ResourceAttributes.SERVICE_NAME; import static org.assertj.core.api.Assertions.assertThat; import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.semconv.ServiceAttributes; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; @@ -28,6 +28,7 @@ void testServiceName() { SpringBootServiceNameDetector guesser = new SpringBootServiceNameDetector(); Resource result = guesser.createResource(config); - assertThat(result.getAttribute(SERVICE_NAME)).isEqualTo("otel-spring-test-app"); + assertThat(result.getAttribute(ServiceAttributes.SERVICE_NAME)) + .isEqualTo("otel-spring-test-app"); } } diff --git a/instrumentation/spring/spring-boot-resources/library/build.gradle.kts b/instrumentation/spring/spring-boot-resources/javaagent/build.gradle.kts similarity index 100% rename from instrumentation/spring/spring-boot-resources/library/build.gradle.kts rename to instrumentation/spring/spring-boot-resources/javaagent/build.gradle.kts diff --git a/instrumentation/spring/spring-boot-resources/library/src/main/java/io/opentelemetry/instrumentation/spring/resources/SpringBootServiceNameDetector.java b/instrumentation/spring/spring-boot-resources/javaagent/src/main/java/io/opentelemetry/instrumentation/spring/resources/SpringBootServiceNameDetector.java similarity index 68% rename from instrumentation/spring/spring-boot-resources/library/src/main/java/io/opentelemetry/instrumentation/spring/resources/SpringBootServiceNameDetector.java rename to instrumentation/spring/spring-boot-resources/javaagent/src/main/java/io/opentelemetry/instrumentation/spring/resources/SpringBootServiceNameDetector.java index 9eccd46b4457..0bcdb527095a 100644 --- a/instrumentation/spring/spring-boot-resources/library/src/main/java/io/opentelemetry/instrumentation/spring/resources/SpringBootServiceNameDetector.java +++ b/instrumentation/spring/spring-boot-resources/javaagent/src/main/java/io/opentelemetry/instrumentation/spring/resources/SpringBootServiceNameDetector.java @@ -6,25 +6,21 @@ package io.opentelemetry.instrumentation.spring.resources; import static java.util.logging.Level.FINE; +import static java.util.logging.Level.FINER; import com.google.auto.service.AutoService; import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; import io.opentelemetry.sdk.autoconfigure.spi.ResourceProvider; import io.opentelemetry.sdk.autoconfigure.spi.internal.ConditionalResourceProvider; import io.opentelemetry.sdk.resources.Resource; -import io.opentelemetry.semconv.resource.attributes.ResourceAttributes; +import io.opentelemetry.semconv.ServiceAttributes; import java.io.IOException; import java.io.InputStream; -import java.lang.reflect.Method; -import java.nio.file.Files; -import java.nio.file.Paths; import java.util.Map; import java.util.Objects; -import java.util.Optional; import java.util.Properties; import java.util.function.Function; import java.util.function.Supplier; -import java.util.logging.Level; import java.util.logging.Logger; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -49,6 +45,9 @@ *
  • Check for --spring.application.name program argument (not jvm arg) via ProcessHandle *
  • Check for --spring.application.name program argument via sun.java.command system property * + * + *

    Note: The spring starter already includes provider in + * io.opentelemetry.instrumentation.spring.autoconfigure.resources.SpringResourceProvider */ @AutoService(ResourceProvider.class) public class SpringBootServiceNameDetector implements ConditionalResourceProvider { @@ -73,20 +72,26 @@ public SpringBootServiceNameDetector() { @Override public Resource createResource(ConfigProperties config) { - logger.log(Level.FINER, "Performing Spring Boot service name auto-detection..."); + logger.log(FINER, "Performing Spring Boot service name auto-detection..."); // Note: The order should be consistent with the order of Spring matching, but noting // that we have "first one wins" while Spring has "last one wins". // The docs for Spring are here: // https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#features.external-config + // https://docs.spring.io/spring-cloud-commons/docs/4.0.4/reference/html/#the-bootstrap-application-context Stream> finders = Stream.of( this::findByCommandlineArgument, this::findBySystemProperties, this::findByEnvironmentVariable, this::findByCurrentDirectoryApplicationProperties, + this::findByCurrentDirectoryApplicationYml, this::findByCurrentDirectoryApplicationYaml, this::findByClasspathApplicationProperties, - this::findByClasspathApplicationYaml); + this::findByClasspathApplicationYml, + this::findByClasspathApplicationYaml, + this::findByClasspathBootstrapProperties, + this::findByClasspathBootstrapYml, + this::findByClasspathBootstrapYaml); return finders .map(Supplier::get) .filter(Objects::nonNull) @@ -94,7 +99,7 @@ public Resource createResource(ConfigProperties config) { .map( serviceName -> { logger.log(FINE, "Auto-detected Spring Boot service name: {0}", serviceName); - return Resource.builder().put(ResourceAttributes.SERVICE_NAME, serviceName).build(); + return Resource.builder().put(ServiceAttributes.SERVICE_NAME, serviceName).build(); }) .orElseGet(Resource::empty); } @@ -106,8 +111,8 @@ public boolean shouldApply(ConfigProperties config, Resource resource) { String serviceName = config.getString("otel.service.name"); Map resourceAttributes = config.getMap("otel.resource.attributes"); return serviceName == null - && !resourceAttributes.containsKey(ResourceAttributes.SERVICE_NAME.getKey()) - && "unknown_service:java".equals(resource.getAttribute(ResourceAttributes.SERVICE_NAME)); + && !resourceAttributes.containsKey(ServiceAttributes.SERVICE_NAME.getKey()) + && "unknown_service:java".equals(resource.getAttribute(ServiceAttributes.SERVICE_NAME)); } @Override @@ -119,25 +124,25 @@ public int order() { @Nullable private String findByEnvironmentVariable() { String result = system.getenv("SPRING_APPLICATION_NAME"); - logger.log(Level.FINER, "Checking for SPRING_APPLICATION_NAME in env: {0}", result); + logger.log(FINER, "Checking for SPRING_APPLICATION_NAME in env: {0}", result); return result; } @Nullable private String findBySystemProperties() { String result = system.getProperty("spring.application.name"); - logger.log(Level.FINER, "Checking for spring.application.name system property: {0}", result); + logger.log(FINER, "Checking for spring.application.name system property: {0}", result); return result; } @Nullable private String findByClasspathApplicationProperties() { - String result = readNameFromAppProperties(); - logger.log( - Level.FINER, - "Checking for spring.application.name in application.properties file: {0}", - result); - return result; + return findByClasspathPropertiesFile("application.properties"); + } + + @Nullable + private String findByClasspathBootstrapProperties() { + return findByClasspathPropertiesFile("bootstrap.properties"); } @Nullable @@ -148,27 +153,59 @@ private String findByCurrentDirectoryApplicationProperties() { } catch (Exception e) { // expected to fail sometimes } - logger.log(Level.FINER, "Checking application.properties in current dir: {0}", result); + logger.log(FINER, "Checking application.properties in current dir: {0}", result); return result; } + @Nullable + private String findByClasspathApplicationYml() { + return findByClasspathYamlFile("application.yml"); + } + + @Nullable + private String findByClasspathBootstrapYml() { + return findByClasspathYamlFile("bootstrap.yml"); + } + @Nullable private String findByClasspathApplicationYaml() { - String result = - loadFromClasspath("application.yml", SpringBootServiceNameDetector::parseNameFromYaml); - logger.log(Level.FINER, "Checking application.yml in classpath: {0}", result); + return findByClasspathYamlFile("application.yaml"); + } + + @Nullable + private String findByClasspathBootstrapYaml() { + return findByClasspathYamlFile("bootstrap.yaml"); + } + + private String findByClasspathYamlFile(String fileName) { + String result = loadFromClasspath(fileName, SpringBootServiceNameDetector::parseNameFromYaml); + if (logger.isLoggable(FINER)) { + logger.log(FINER, "Checking {0} in classpath: {1}", new Object[] {fileName, result}); + } return result; } + @Nullable + private String findByCurrentDirectoryApplicationYml() { + return findByCurrentDirectoryYamlFile("application.yml"); + } + @Nullable private String findByCurrentDirectoryApplicationYaml() { + return findByCurrentDirectoryYamlFile("application.yaml"); + } + + @Nullable + private String findByCurrentDirectoryYamlFile(String fileName) { String result = null; - try (InputStream in = system.openFile("application.yml")) { + try (InputStream in = system.openFile(fileName)) { result = parseNameFromYaml(in); } catch (Exception e) { // expected to fail sometimes } - logger.log(Level.FINER, "Checking application.yml in current dir: {0}", result); + if (logger.isLoggable(FINER)) { + logger.log(FINER, "Checking {0} in current dir: {1}", new Object[] {fileName, result}); + } return result; } @@ -203,7 +240,7 @@ private String findByCommandlineArgument() { String javaCommand = system.getProperty("sun.java.command"); result = parseNameFromCommandLine(javaCommand); } - logger.log(Level.FINER, "Checking application commandline args: {0}", result); + logger.log(FINER, "Checking application commandline args: {0}", result); return result; } @@ -239,9 +276,16 @@ private static String parseNameFromProcessArgs(String[] args) { } @Nullable - private String readNameFromAppProperties() { - return loadFromClasspath( - "application.properties", SpringBootServiceNameDetector::getAppNamePropertyFromStream); + private String findByClasspathPropertiesFile(String filename) { + String result = + loadFromClasspath(filename, SpringBootServiceNameDetector::getAppNamePropertyFromStream); + if (logger.isLoggable(FINER)) { + logger.log( + FINER, + "Checking for spring.application.name in {0} file: {1}", + new Object[] {filename, result}); + } + return result; } @Nullable @@ -259,59 +303,9 @@ private static String getAppNamePropertyFromStream(InputStream in) { @Nullable private String loadFromClasspath(String filename, Function parser) { try (InputStream in = system.openClasspathResource(filename)) { - return parser.apply(in); + return in != null ? parser.apply(in) : null; } catch (Exception e) { return null; } } - - // Exists for testing - static class SystemHelper { - private final ClassLoader classLoader; - private final boolean addBootInfPrefix; - - SystemHelper() { - ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader(); - classLoader = - contextClassLoader != null ? contextClassLoader : ClassLoader.getSystemClassLoader(); - addBootInfPrefix = classLoader.getResource("BOOT-INF/classes/") != null; - if (addBootInfPrefix) { - logger.log(Level.FINER, "Detected presence of BOOT-INF/classes/"); - } - } - - String getenv(String name) { - return System.getenv(name); - } - - String getProperty(String key) { - return System.getProperty(key); - } - - InputStream openClasspathResource(String filename) { - String path = addBootInfPrefix ? "BOOT-INF/classes/" + filename : filename; - return classLoader.getResourceAsStream(path); - } - - InputStream openFile(String filename) throws Exception { - return Files.newInputStream(Paths.get(filename)); - } - - /** - * Attempts to use ProcessHandle to get the full commandline of the current process (including - * the main method arguments). Will only succeed on java 9+. - */ - @SuppressWarnings("unchecked") - String[] attemptGetCommandLineArgsViaReflection() throws Exception { - Class clazz = Class.forName("java.lang.ProcessHandle"); - Method currentMethod = clazz.getDeclaredMethod("current"); - Method infoMethod = clazz.getDeclaredMethod("info"); - Object currentInstance = currentMethod.invoke(null); - Object info = infoMethod.invoke(currentInstance); - Class infoClass = Class.forName("java.lang.ProcessHandle$Info"); - Method argumentsMethod = infoClass.getMethod("arguments"); - Optional optionalArgs = (Optional) argumentsMethod.invoke(info); - return optionalArgs.orElse(new String[0]); - } - } } diff --git a/instrumentation/spring/spring-boot-resources/javaagent/src/main/java/io/opentelemetry/instrumentation/spring/resources/SpringBootServiceVersionDetector.java b/instrumentation/spring/spring-boot-resources/javaagent/src/main/java/io/opentelemetry/instrumentation/spring/resources/SpringBootServiceVersionDetector.java new file mode 100644 index 000000000000..865279de3064 --- /dev/null +++ b/instrumentation/spring/spring-boot-resources/javaagent/src/main/java/io/opentelemetry/instrumentation/spring/resources/SpringBootServiceVersionDetector.java @@ -0,0 +1,71 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.resources; + +import static java.util.logging.Level.FINE; + +import com.google.auto.service.AutoService; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import io.opentelemetry.sdk.autoconfigure.spi.ResourceProvider; +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.semconv.ServiceAttributes; +import java.io.IOException; +import java.io.InputStream; +import java.util.Optional; +import java.util.Properties; +import java.util.logging.Logger; + +/** + * Note: The spring starter already includes provider in + * io.opentelemetry.instrumentation.spring.autoconfigure.resources.SpringResourceProvider + */ +@AutoService(ResourceProvider.class) +public class SpringBootServiceVersionDetector implements ResourceProvider { + + private static final Logger logger = + Logger.getLogger(SpringBootServiceVersionDetector.class.getName()); + + private final SystemHelper system; + + public SpringBootServiceVersionDetector() { + this.system = new SystemHelper(); + } + + // Exists for testing + SpringBootServiceVersionDetector(SystemHelper system) { + this.system = system; + } + + @Override + public Resource createResource(ConfigProperties config) { + return getServiceVersionFromBuildInfo() + .map( + version -> { + logger.log(FINE, "Auto-detected Spring Boot service version: {0}", version); + return Resource.builder().put(ServiceAttributes.SERVICE_VERSION, version).build(); + }) + .orElseGet(Resource::empty); + } + + private Optional getServiceVersionFromBuildInfo() { + try (InputStream in = system.openClasspathResource("META-INF", "build-info.properties")) { + return in != null ? getServiceVersionPropertyFromStream(in) : Optional.empty(); + } catch (Exception e) { + return Optional.empty(); + } + } + + private static Optional getServiceVersionPropertyFromStream(InputStream in) { + Properties properties = new Properties(); + try { + // Note: load() uses ISO 8859-1 encoding, same as spring uses by default for property files + properties.load(in); + return Optional.ofNullable(properties.getProperty("build.version")); + } catch (IOException e) { + return Optional.empty(); + } + } +} diff --git a/instrumentation/spring/spring-boot-resources/javaagent/src/main/java/io/opentelemetry/instrumentation/spring/resources/SystemHelper.java b/instrumentation/spring/spring-boot-resources/javaagent/src/main/java/io/opentelemetry/instrumentation/spring/resources/SystemHelper.java new file mode 100644 index 000000000000..59606a62c296 --- /dev/null +++ b/instrumentation/spring/spring-boot-resources/javaagent/src/main/java/io/opentelemetry/instrumentation/spring/resources/SystemHelper.java @@ -0,0 +1,70 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.resources; + +import java.io.InputStream; +import java.lang.reflect.Method; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.Optional; +import java.util.logging.Level; +import java.util.logging.Logger; + +class SystemHelper { + private static final Logger logger = Logger.getLogger(SystemHelper.class.getName()); + + private final ClassLoader classLoader; + private final boolean addBootInfPrefix; + + SystemHelper() { + ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader(); + classLoader = + contextClassLoader != null ? contextClassLoader : ClassLoader.getSystemClassLoader(); + addBootInfPrefix = classLoader.getResource("BOOT-INF/classes/") != null; + if (addBootInfPrefix) { + logger.log(Level.FINER, "Detected presence of BOOT-INF/classes/"); + } + } + + String getenv(String name) { + return System.getenv(name); + } + + String getProperty(String key) { + return System.getProperty(key); + } + + InputStream openClasspathResource(String filename) { + String path = addBootInfPrefix ? "BOOT-INF/classes/" + filename : filename; + return classLoader.getResourceAsStream(path); + } + + InputStream openClasspathResource(String directory, String filename) { + String path = directory + "/" + filename; + return classLoader.getResourceAsStream(path); + } + + InputStream openFile(String filename) throws Exception { + return Files.newInputStream(Paths.get(filename)); + } + + /** + * Attempts to use ProcessHandle to get the full commandline of the current process (including the + * main method arguments). Will only succeed on java 9+. + */ + @SuppressWarnings("unchecked") + String[] attemptGetCommandLineArgsViaReflection() throws Exception { + Class clazz = Class.forName("java.lang.ProcessHandle"); + Method currentMethod = clazz.getDeclaredMethod("current"); + Method infoMethod = clazz.getDeclaredMethod("info"); + Object currentInstance = currentMethod.invoke(null); + Object info = infoMethod.invoke(currentInstance); + Class infoClass = Class.forName("java.lang.ProcessHandle$Info"); + Method argumentsMethod = infoClass.getMethod("arguments"); + Optional optionalArgs = (Optional) argumentsMethod.invoke(info); + return optionalArgs.orElse(new String[0]); + } +} diff --git a/instrumentation/spring/spring-boot-resources/library/src/test/java/io/opentelemetry/instrumentation/spring/resources/SpringBootServiceNameDetectorTest.java b/instrumentation/spring/spring-boot-resources/javaagent/src/test/java/io/opentelemetry/instrumentation/spring/resources/SpringBootServiceNameDetectorTest.java similarity index 65% rename from instrumentation/spring/spring-boot-resources/library/src/test/java/io/opentelemetry/instrumentation/spring/resources/SpringBootServiceNameDetectorTest.java rename to instrumentation/spring/spring-boot-resources/javaagent/src/test/java/io/opentelemetry/instrumentation/spring/resources/SpringBootServiceNameDetectorTest.java index 0e8d6dd18631..d8abfe890de8 100644 --- a/instrumentation/spring/spring-boot-resources/library/src/test/java/io/opentelemetry/instrumentation/spring/resources/SpringBootServiceNameDetectorTest.java +++ b/instrumentation/spring/spring-boot-resources/javaagent/src/test/java/io/opentelemetry/instrumentation/spring/resources/SpringBootServiceNameDetectorTest.java @@ -5,7 +5,6 @@ package io.opentelemetry.instrumentation.spring.resources; -import static io.opentelemetry.semconv.resource.attributes.ResourceAttributes.SERVICE_NAME; import static java.nio.charset.StandardCharsets.UTF_8; import static java.util.Collections.singletonMap; import static org.assertj.core.api.Assertions.assertThat; @@ -14,6 +13,7 @@ import io.opentelemetry.api.common.Attributes; import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.semconv.ServiceAttributes; import java.io.InputStream; import java.io.OutputStream; import java.net.URL; @@ -22,16 +22,23 @@ import java.nio.file.Paths; 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 SpringBootServiceNameDetectorTest { - static final String PROPS = "application.properties"; + static final String APPLICATION_PROPS = "application.properties"; static final String APPLICATION_YML = "application.yml"; + + static final String BOOTSTRAP_PROPS = "bootstrap.properties"; + + static final String BOOTSTRAP_YML = "bootstrap.yml"; + @Mock ConfigProperties config; - @Mock SpringBootServiceNameDetector.SystemHelper system; + @Mock SystemHelper system; @Test void findByEnvVar() { @@ -46,18 +53,28 @@ void findByEnvVar() { @Test void classpathApplicationProperties() { - when(system.openClasspathResource(PROPS)).thenReturn(openClasspathResource(PROPS)); + when(system.openClasspathResource(APPLICATION_PROPS)) + .thenReturn(openClasspathResource(APPLICATION_PROPS)); SpringBootServiceNameDetector guesser = new SpringBootServiceNameDetector(system); Resource result = guesser.createResource(config); expectServiceName(result, "dog-store"); } + @Test + void classpathBootstrapProperties() { + when(system.openClasspathResource(BOOTSTRAP_PROPS)) + .thenReturn(openClasspathResource(BOOTSTRAP_PROPS)); + SpringBootServiceNameDetector guesser = new SpringBootServiceNameDetector(system); + Resource result = guesser.createResource(config); + expectServiceName(result, "dog-store-bootstrap"); + } + @Test void propertiesFileInCurrentDir() throws Exception { - Path propsPath = Paths.get(PROPS); + Path propsPath = Paths.get(APPLICATION_PROPS); try { writeString(propsPath, "spring.application.name=fish-tank\n"); - when(system.openFile(PROPS)).thenCallRealMethod(); + when(system.openFile(APPLICATION_PROPS)).thenCallRealMethod(); SpringBootServiceNameDetector guesser = new SpringBootServiceNameDetector(system); Resource result = guesser.createResource(config); expectServiceName(result, "fish-tank"); @@ -66,18 +83,38 @@ void propertiesFileInCurrentDir() throws Exception { } } - @Test - void classpathApplicationYaml() { - when(system.openClasspathResource(APPLICATION_YML)) - .thenReturn(openClasspathResource(APPLICATION_YML)); + @ParameterizedTest + @ValueSource(strings = {"application.yaml", APPLICATION_YML}) + void classpathApplicationYaml(String fileName) { + when(system.openClasspathResource(fileName)).thenReturn(openClasspathResource(APPLICATION_YML)); SpringBootServiceNameDetector guesser = new SpringBootServiceNameDetector(system); Resource result = guesser.createResource(config); expectServiceName(result, "cat-store"); } - @Test - void classpathApplicationYamlContainingMultipleYamlDefinitions() { - when(system.openClasspathResource(APPLICATION_YML)) + @ParameterizedTest + @ValueSource(strings = {"bootstrap.yaml", BOOTSTRAP_YML}) + void classpathBootstrapYaml(String fileName) { + when(system.openClasspathResource(fileName)).thenReturn(openClasspathResource(BOOTSTRAP_YML)); + SpringBootServiceNameDetector guesser = new SpringBootServiceNameDetector(system); + Resource result = guesser.createResource(config); + expectServiceName(result, "cat-store-bootstrap"); + } + + @ParameterizedTest + @ValueSource(strings = {"bootstrap.yaml", BOOTSTRAP_YML}) + void classpathBootstrapYamlContainingMultipleYamlDefinitions(String fileName) { + when(system.openClasspathResource(fileName)) + .thenReturn(ClassLoader.getSystemClassLoader().getResourceAsStream("bootstrap-multi.yml")); + SpringBootServiceNameDetector guesser = new SpringBootServiceNameDetector(system); + Resource result = guesser.createResource(config); + expectServiceName(result, "cat-store-bootstrap"); + } + + @ParameterizedTest + @ValueSource(strings = {"application.yaml", APPLICATION_YML}) + void classpathApplicationYamlContainingMultipleYamlDefinitions(String fileName) { + when(system.openClasspathResource(fileName)) .thenReturn( ClassLoader.getSystemClassLoader().getResourceAsStream("application-multi.yml")); SpringBootServiceNameDetector guesser = new SpringBootServiceNameDetector(system); @@ -85,14 +122,15 @@ void classpathApplicationYamlContainingMultipleYamlDefinitions() { expectServiceName(result, "cat-store"); } - @Test - void yamlFileInCurrentDir() throws Exception { - Path yamlPath = Paths.get(APPLICATION_YML); + @ParameterizedTest + @ValueSource(strings = {"application.yaml", APPLICATION_YML}) + void yamlFileInCurrentDir(String fileName) throws Exception { + Path yamlPath = Paths.get(fileName); try { URL url = getClass().getClassLoader().getResource(APPLICATION_YML); String content = readString(Paths.get(url.toURI())); writeString(yamlPath, content); - when(system.openFile(APPLICATION_YML)).thenCallRealMethod(); + when(system.openFile(fileName)).thenCallRealMethod(); SpringBootServiceNameDetector guesser = new SpringBootServiceNameDetector(system); Resource result = guesser.createResource(config); expectServiceName(result, "cat-store"); @@ -117,7 +155,7 @@ void getFromCommandlineArgsWithProcessHandle() throws Exception { } @Test - void getFromCommandlineArgsWithSystemProperty() throws Exception { + void getFromCommandlineArgsWithSystemProperty() { when(system.getProperty("sun.java.command")) .thenReturn("/bin/java sweet-spring.jar --spring.application.name=bullpen --quiet=never"); SpringBootServiceNameDetector guesser = new SpringBootServiceNameDetector(system); @@ -135,7 +173,8 @@ void shouldApply() { void shouldNotApplyWhenResourceHasServiceName() { SpringBootServiceNameDetector guesser = new SpringBootServiceNameDetector(system); Resource resource = - Resource.getDefault().merge(Resource.create(Attributes.of(SERVICE_NAME, "test-service"))); + Resource.getDefault() + .merge(Resource.create(Attributes.of(ServiceAttributes.SERVICE_NAME, "test-service"))); assertThat(guesser.shouldApply(config, resource)).isFalse(); } @@ -150,12 +189,12 @@ void shouldNotApplyIfConfigHasServiceName() { void shouldNotApplyIfConfigHasServiceNameResourceAttribute() { SpringBootServiceNameDetector guesser = new SpringBootServiceNameDetector(system); when(config.getMap("otel.resource.attributes")) - .thenReturn(singletonMap(SERVICE_NAME.getKey(), "test-service")); + .thenReturn(singletonMap(ServiceAttributes.SERVICE_NAME.getKey(), "test-service")); assertThat(guesser.shouldApply(config, Resource.getDefault())).isFalse(); } private static void expectServiceName(Resource result, String expected) { - assertThat(result.getAttribute(SERVICE_NAME)).isEqualTo(expected); + assertThat(result.getAttribute(ServiceAttributes.SERVICE_NAME)).isEqualTo(expected); } private static void writeString(Path path, String value) throws Exception { diff --git a/instrumentation/spring/spring-boot-resources/javaagent/src/test/java/io/opentelemetry/instrumentation/spring/resources/SpringBootServiceVersionDetectorTest.java b/instrumentation/spring/spring-boot-resources/javaagent/src/test/java/io/opentelemetry/instrumentation/spring/resources/SpringBootServiceVersionDetectorTest.java new file mode 100644 index 000000000000..8482af0f08ee --- /dev/null +++ b/instrumentation/spring/spring-boot-resources/javaagent/src/test/java/io/opentelemetry/instrumentation/spring/resources/SpringBootServiceVersionDetectorTest.java @@ -0,0 +1,61 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.resources; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.semconv.ServiceAttributes; +import java.io.InputStream; +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 SpringBootServiceVersionDetectorTest { + + static final String BUILD_PROPS = "build-info.properties"; + static final String META_INFO = "META-INF"; + + @Mock ConfigProperties config; + @Mock SystemHelper system; + + @Test + void givenBuildVersionIsPresentInBuildInfProperties_thenReturnBuildVersion() { + when(system.openClasspathResource(META_INFO, BUILD_PROPS)) + .thenReturn(openClasspathResource(META_INFO + "/" + BUILD_PROPS)); + + SpringBootServiceVersionDetector guesser = new SpringBootServiceVersionDetector(system); + Resource result = guesser.createResource(config); + assertThat(result.getAttribute(ServiceAttributes.SERVICE_VERSION)).isEqualTo("0.0.2"); + } + + @Test + void givenBuildVersionFileNotPresent_thenReturnEmptyResource() { + when(system.openClasspathResource(META_INFO, BUILD_PROPS)).thenReturn(null); + + SpringBootServiceVersionDetector guesser = new SpringBootServiceVersionDetector(system); + Resource result = guesser.createResource(config); + assertThat(result).isEqualTo(Resource.empty()); + } + + @Test + void givenBuildVersionFileIsPresentButBuildVersionPropertyNotPresent_thenReturnEmptyResource() { + when(system.openClasspathResource(META_INFO, BUILD_PROPS)) + .thenReturn(openClasspathResource(BUILD_PROPS)); + + SpringBootServiceVersionDetector guesser = new SpringBootServiceVersionDetector(system); + Resource result = guesser.createResource(config); + assertThat(result).isEqualTo(Resource.empty()); + } + + private InputStream openClasspathResource(String resource) { + return getClass().getClassLoader().getResourceAsStream(resource); + } +} diff --git a/instrumentation/spring/spring-boot-resources/javaagent/src/test/resources/META-INF/build-info.properties b/instrumentation/spring/spring-boot-resources/javaagent/src/test/resources/META-INF/build-info.properties new file mode 100644 index 000000000000..2b810fb32475 --- /dev/null +++ b/instrumentation/spring/spring-boot-resources/javaagent/src/test/resources/META-INF/build-info.properties @@ -0,0 +1,3 @@ +build.artifact=something +build.name=some-name +build.version=0.0.2 diff --git a/instrumentation/spring/spring-boot-resources/library/src/test/resources/application-multi.yml b/instrumentation/spring/spring-boot-resources/javaagent/src/test/resources/application-multi.yml similarity index 88% rename from instrumentation/spring/spring-boot-resources/library/src/test/resources/application-multi.yml rename to instrumentation/spring/spring-boot-resources/javaagent/src/test/resources/application-multi.yml index 8edcf2baccc9..826befc2459f 100644 --- a/instrumentation/spring/spring-boot-resources/library/src/test/resources/application-multi.yml +++ b/instrumentation/spring/spring-boot-resources/javaagent/src/test/resources/application-multi.yml @@ -1,6 +1,5 @@ flib: - something: - 12 + something: 12 section: two: 2 diff --git a/instrumentation/spring/spring-boot-resources/library/src/test/resources/application.properties b/instrumentation/spring/spring-boot-resources/javaagent/src/test/resources/application.properties similarity index 100% rename from instrumentation/spring/spring-boot-resources/library/src/test/resources/application.properties rename to instrumentation/spring/spring-boot-resources/javaagent/src/test/resources/application.properties diff --git a/instrumentation/spring/spring-boot-resources/library/src/test/resources/application.yml b/instrumentation/spring/spring-boot-resources/javaagent/src/test/resources/application.yml similarity index 70% rename from instrumentation/spring/spring-boot-resources/library/src/test/resources/application.yml rename to instrumentation/spring/spring-boot-resources/javaagent/src/test/resources/application.yml index 3bfd3386b84e..87eb5ac7e8e3 100644 --- a/instrumentation/spring/spring-boot-resources/library/src/test/resources/application.yml +++ b/instrumentation/spring/spring-boot-resources/javaagent/src/test/resources/application.yml @@ -1,6 +1,5 @@ flib: - something: - 12 + something: 12 section: two: 2 @@ -11,4 +10,4 @@ server: spring: application: - name: cat-store \ No newline at end of file + name: cat-store diff --git a/instrumentation/spring/spring-boot-resources/javaagent/src/test/resources/bootstrap-multi.yml b/instrumentation/spring/spring-boot-resources/javaagent/src/test/resources/bootstrap-multi.yml new file mode 100644 index 000000000000..8b5ad5424708 --- /dev/null +++ b/instrumentation/spring/spring-boot-resources/javaagent/src/test/resources/bootstrap-multi.yml @@ -0,0 +1,9 @@ +spring: + application: + name: cat-store-bootstrap + +--- +some: + other: + property: value + diff --git a/instrumentation/spring/spring-boot-resources/javaagent/src/test/resources/bootstrap.properties b/instrumentation/spring/spring-boot-resources/javaagent/src/test/resources/bootstrap.properties new file mode 100644 index 000000000000..c4c64893bf48 --- /dev/null +++ b/instrumentation/spring/spring-boot-resources/javaagent/src/test/resources/bootstrap.properties @@ -0,0 +1 @@ +spring.application.name: dog-store-bootstrap diff --git a/instrumentation/spring/spring-boot-resources/javaagent/src/test/resources/bootstrap.yaml b/instrumentation/spring/spring-boot-resources/javaagent/src/test/resources/bootstrap.yaml new file mode 100644 index 000000000000..9e06c48c08c7 --- /dev/null +++ b/instrumentation/spring/spring-boot-resources/javaagent/src/test/resources/bootstrap.yaml @@ -0,0 +1,3 @@ +spring: + application: + name: cat-store-bootstrap diff --git a/instrumentation/spring/spring-boot-resources/javaagent/src/test/resources/bootstrap.yml b/instrumentation/spring/spring-boot-resources/javaagent/src/test/resources/bootstrap.yml new file mode 100644 index 000000000000..9e06c48c08c7 --- /dev/null +++ b/instrumentation/spring/spring-boot-resources/javaagent/src/test/resources/bootstrap.yml @@ -0,0 +1,3 @@ +spring: + application: + name: cat-store-bootstrap diff --git a/instrumentation/spring/spring-boot-resources/javaagent/src/test/resources/build-info.properties b/instrumentation/spring/spring-boot-resources/javaagent/src/test/resources/build-info.properties new file mode 100644 index 000000000000..9a0f8f1ab576 --- /dev/null +++ b/instrumentation/spring/spring-boot-resources/javaagent/src/test/resources/build-info.properties @@ -0,0 +1,2 @@ +build.artifact=something +build.name=some-name diff --git a/instrumentation/spring/spring-cloud-gateway/README.md b/instrumentation/spring/spring-cloud-gateway/README.md new file mode 100644 index 000000000000..9da9be01f443 --- /dev/null +++ b/instrumentation/spring/spring-cloud-gateway/README.md @@ -0,0 +1,5 @@ +# Settings for the Spring Cloud Gateway instrumentation + +| System property | Type | Default | Description | +|--------------------------------------------------------------------------| ------- | ------- |---------------------------------------------------------------------------------------------| +| `otel.instrumentation.spring-cloud-gateway.experimental-span-attributes` | Boolean | `false` | Enable the capture of experimental span attributes. | diff --git a/instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-2.0/javaagent/build.gradle.kts b/instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-2.0/javaagent/build.gradle.kts new file mode 100644 index 000000000000..8e21990666ba --- /dev/null +++ b/instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-2.0/javaagent/build.gradle.kts @@ -0,0 +1,46 @@ +plugins { + id("otel.javaagent-instrumentation") +} + +muzzle { + pass { + group.set("org.springframework.cloud") + module.set("spring-cloud-starter-gateway") + versions.set("[2.0.0.RELEASE,]") + } +} + +dependencies { + library("org.springframework.cloud:spring-cloud-starter-gateway:2.0.0.RELEASE") + + testInstrumentation(project(":instrumentation:netty:netty-4.1:javaagent")) + testInstrumentation(project(":instrumentation:reactor:reactor-3.1:javaagent")) + testInstrumentation(project(":instrumentation:reactor:reactor-netty:reactor-netty-1.0:javaagent")) + testInstrumentation(project(":instrumentation:spring:spring-webflux:spring-webflux-5.0:javaagent")) + + testImplementation(project(":instrumentation:spring:spring-cloud-gateway:spring-cloud-gateway-common:testing")) + + testLibrary("org.springframework.boot:spring-boot-starter-test:2.0.0.RELEASE") + + latestDepTestLibrary("org.springframework.cloud:spring-cloud-starter-gateway:2.1.+") + latestDepTestLibrary("org.springframework.boot:spring-boot-starter-test:2.1.+") +} + +tasks.withType().configureEach { + jvmArgs("-Dotel.instrumentation.spring-cloud-gateway.experimental-span-attributes=true") + + // required on jdk17 + jvmArgs("--add-opens=java.base/java.lang=ALL-UNNAMED") + jvmArgs("-XX:+IgnoreUnrecognizedVMOptions") + + jvmArgs("-Dotel.instrumentation.common.experimental.controller-telemetry.enabled=true") + + systemProperty("testLatestDeps", findProperty("testLatestDeps") as Boolean) +} + +configurations.testRuntimeClasspath { + resolutionStrategy { + force("ch.qos.logback:logback-classic:1.2.11") + force("org.slf4j:slf4j-api:1.7.36") + } +} diff --git a/instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/gateway/v2_0/GatewayInstrumentationModule.java b/instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/gateway/v2_0/GatewayInstrumentationModule.java new file mode 100644 index 000000000000..a47ca107b88f --- /dev/null +++ b/instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/gateway/v2_0/GatewayInstrumentationModule.java @@ -0,0 +1,40 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.gateway.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 io.opentelemetry.javaagent.extension.instrumentation.internal.ExperimentalInstrumentationModule; +import java.util.List; + +@AutoService(InstrumentationModule.class) +public class GatewayInstrumentationModule extends InstrumentationModule + implements ExperimentalInstrumentationModule { + + public GatewayInstrumentationModule() { + super("spring-cloud-gateway"); + } + + @Override + public List typeInstrumentations() { + return asList(new HandlerAdapterInstrumentation()); + } + + @Override + public String getModuleGroup() { + // relies on netty + return "netty"; + } + + @Override + public int order() { + // Later than Spring Webflux. + return 1; + } +} diff --git a/instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/gateway/v2_0/GatewaySingletons.java b/instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/gateway/v2_0/GatewaySingletons.java new file mode 100644 index 000000000000..df564fa4ca79 --- /dev/null +++ b/instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/gateway/v2_0/GatewaySingletons.java @@ -0,0 +1,18 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.gateway.v2_0; + +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRouteGetter; +import org.springframework.web.server.ServerWebExchange; + +public final class GatewaySingletons { + + private GatewaySingletons() {} + + public static HttpServerRouteGetter httpRouteGetter() { + return (context, exchange) -> ServerWebExchangeHelper.extractServerRoute(exchange); + } +} diff --git a/instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/gateway/v2_0/HandlerAdapterInstrumentation.java b/instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/gateway/v2_0/HandlerAdapterInstrumentation.java new file mode 100644 index 000000000000..3bcf19da9e39 --- /dev/null +++ b/instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/gateway/v2_0/HandlerAdapterInstrumentation.java @@ -0,0 +1,68 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.gateway.v2_0; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.hasClassesNamed; +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.implementsInterface; +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.context.Context; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRoute; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRouteSource; +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; + +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) { + Context context = Context.current(); + // Update route info for server span. + HttpServerRoute.update( + context, + HttpServerRouteSource.NESTED_CONTROLLER, + GatewaySingletons.httpRouteGetter(), + exchange); + // Record route info in server span. + ServerWebExchangeHelper.extractAttributes(exchange, context); + } + } +} diff --git a/instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/gateway/v2_0/ServerWebExchangeHelper.java b/instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/gateway/v2_0/ServerWebExchangeHelper.java new file mode 100644 index 000000000000..4bf8abe5fbad --- /dev/null +++ b/instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/gateway/v2_0/ServerWebExchangeHelper.java @@ -0,0 +1,99 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.gateway.v2_0; + +import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.instrumenter.LocalRootSpan; +import io.opentelemetry.javaagent.bootstrap.internal.AgentInstrumentationConfig; +import java.util.regex.Pattern; +import org.springframework.cloud.gateway.route.Route; +import org.springframework.web.server.ServerWebExchange; + +public final class ServerWebExchangeHelper { + + /** Route ID attribute key. */ + private static final AttributeKey ROUTE_ID_ATTRIBUTE = + AttributeKey.stringKey("spring-cloud-gateway.route.id"); + + /** Route URI attribute key. */ + private static final AttributeKey ROUTE_URI_ATTRIBUTE = + AttributeKey.stringKey("spring-cloud-gateway.route.uri"); + + /** Route order attribute key. */ + private static final AttributeKey ROUTE_ORDER_ATTRIBUTE = + AttributeKey.longKey("spring-cloud-gateway.route.order"); + + /** Route filter size attribute key. */ + private static final AttributeKey ROUTE_FILTER_SIZE_ATTRIBUTE = + AttributeKey.longKey("spring-cloud-gateway.route.filter.size"); + + private static final boolean CAPTURE_EXPERIMENTAL_SPAN_ATTRIBUTES; + + static { + CAPTURE_EXPERIMENTAL_SPAN_ATTRIBUTES = + AgentInstrumentationConfig.get() + .getBoolean( + "otel.instrumentation.spring-cloud-gateway.experimental-span-attributes", false); + } + + /* Regex for UUID */ + private static final Pattern UUID_REGEX = + Pattern.compile( + "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$"); + + private static final String INVALID_RANDOM_ROUTE_ID = + "org.springframework.util.AlternativeJdkIdGenerator@"; + + private ServerWebExchangeHelper() {} + + public static void extractAttributes(ServerWebExchange exchange, Context context) { + // Record route info + Route route = exchange.getAttribute(GATEWAY_ROUTE_ATTR); + if (route != null && CAPTURE_EXPERIMENTAL_SPAN_ATTRIBUTES) { + Span serverSpan = LocalRootSpan.fromContextOrNull(context); + if (serverSpan == null) { + return; + } + serverSpan.setAttribute(ROUTE_ID_ATTRIBUTE, route.getId()); + serverSpan.setAttribute(ROUTE_URI_ATTRIBUTE, route.getUri().toASCIIString()); + serverSpan.setAttribute(ROUTE_ORDER_ATTRIBUTE, route.getOrder()); + serverSpan.setAttribute(ROUTE_FILTER_SIZE_ATTRIBUTE, route.getFilters().size()); + } + } + + public static String extractServerRoute(ServerWebExchange exchange) { + Route route = exchange.getAttribute(GATEWAY_ROUTE_ATTR); + if (route != null) { + return convergeRouteId(route); + } + return null; + } + + /** + * To avoid high cardinality, we ignore random UUID generated by Spring Cloud Gateway. Spring + * Cloud Gateway generate invalid random routeID, and it is fixed until 3.1.x + * + * @see + */ + private static String convergeRouteId(Route route) { + String routeId = route.getId(); + if (routeId == null || routeId.isEmpty()) { + return null; + } + if (UUID_REGEX.matcher(routeId).matches()) { + return null; + } + if (routeId.startsWith(INVALID_RANDOM_ROUTE_ID)) { + return null; + } + return routeId; + } +} diff --git a/instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-2.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/gateway/v2_0/GatewayRouteMappingTest.java b/instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-2.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/gateway/v2_0/GatewayRouteMappingTest.java new file mode 100644 index 000000000000..d3a907ec959b --- /dev/null +++ b/instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-2.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/gateway/v2_0/GatewayRouteMappingTest.java @@ -0,0 +1,77 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.gateway.v2_0; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.spring.gateway.common.AbstractRouteMappingTest; +import io.opentelemetry.testing.internal.armeria.common.AggregatedHttpResponse; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +@ExtendWith(SpringExtension.class) +@SpringBootTest( + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + classes = { + GatewayTestApplication.class, + GatewayRouteMappingTest.ForceNettyAutoConfiguration.class + }) +class GatewayRouteMappingTest extends AbstractRouteMappingTest { + + @Test + void gatewayRouteMappingTest() { + String requestBody = "gateway"; + AggregatedHttpResponse response = client.post("/gateway/echo", requestBody).aggregate().join(); + assertThat(response.status().code()).isEqualTo(200); + assertThat(response.contentUtf8()).isEqualTo(requestBody); + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("POST path_route") + .hasKind(SpanKind.SERVER) + .hasAttributesSatisfying( + buildAttributeAssertions("path_route", "h1c://mock.response", 0, 1)), + span -> span.hasName(WEBFLUX_SPAN_NAME).hasKind(SpanKind.INTERNAL))); + } + + @Test + void gatewayRandomUuidRouteMappingTest() { + String requestBody = "gateway"; + AggregatedHttpResponse response = client.post("/uuid/echo", requestBody).aggregate().join(); + assertThat(response.status().code()).isEqualTo(200); + assertThat(response.contentUtf8()).isEqualTo(requestBody); + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("POST") + .hasKind(SpanKind.SERVER) + .hasAttributesSatisfying(buildAttributeAssertions("h1c://mock.uuid", 0, 1)), + span -> span.hasName(WEBFLUX_SPAN_NAME).hasKind(SpanKind.INTERNAL))); + } + + @Test + void gatewayFakeUuidRouteMappingTest() { + String requestBody = "gateway"; + String routeId = "ffffffff-ffff-ffff-ffff-ffff"; + AggregatedHttpResponse response = client.post("/fake/echo", requestBody).aggregate().join(); + assertThat(response.status().code()).isEqualTo(200); + assertThat(response.contentUtf8()).isEqualTo(requestBody); + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("POST " + routeId) + .hasKind(SpanKind.SERVER) + .hasAttributesSatisfying( + buildAttributeAssertions(routeId, "h1c://mock.fake", 0, 1)), + span -> span.hasName(WEBFLUX_SPAN_NAME).hasKind(SpanKind.INTERNAL))); + } +} diff --git a/instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-2.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/gateway/v2_0/GatewayTestApplication.java b/instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-2.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/gateway/v2_0/GatewayTestApplication.java new file mode 100644 index 000000000000..6daef51e2d77 --- /dev/null +++ b/instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-2.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/gateway/v2_0/GatewayTestApplication.java @@ -0,0 +1,45 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.gateway.v2_0; + +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.gateway.route.RouteLocator; +import org.springframework.cloud.gateway.route.builder.GatewayFilterSpec; +import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder; +import org.springframework.cloud.gateway.route.builder.UriSpec; +import org.springframework.context.annotation.Bean; + +@SpringBootApplication +public class GatewayTestApplication { + + @Bean + public RouteLocator customRouteLocator(RouteLocatorBuilder builder) { + // A simple echo gateway. + return builder + .routes() + .route( + "path_route", + r -> + r.path("/gateway/**") + .filters(GatewayTestApplication::echoFunc) + .uri("h1c://mock.response")) + // The routeID should be a random UUID. + .route( + r -> + r.path("/uuid/**").filters(GatewayTestApplication::echoFunc).uri("h1c://mock.uuid")) + // Seems like an uuid but not. + .route( + "ffffffff-ffff-ffff-ffff-ffff", + r -> + r.path("/fake/**").filters(GatewayTestApplication::echoFunc).uri("h1c://mock.fake")) + .build(); + } + + private static UriSpec echoFunc(GatewayFilterSpec f) { + return f.filter( + (exchange, chain) -> exchange.getResponse().writeWith(exchange.getRequest().getBody())); + } +} diff --git a/instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-2.0/javaagent/src/test/resources/logback.xml b/instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-2.0/javaagent/src/test/resources/logback.xml new file mode 100644 index 000000000000..7f2406629488 --- /dev/null +++ b/instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-2.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/instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-2.2/testing/build.gradle.kts b/instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-2.2/testing/build.gradle.kts new file mode 100644 index 000000000000..bf0a90a3cdbd --- /dev/null +++ b/instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-2.2/testing/build.gradle.kts @@ -0,0 +1,45 @@ +plugins { + id("otel.javaagent-testing") +} + +dependencies { + testInstrumentation(project(":instrumentation:spring:spring-cloud-gateway:spring-cloud-gateway-2.0:javaagent")) + testInstrumentation(project(":instrumentation:netty:netty-4.1:javaagent")) + testInstrumentation(project(":instrumentation:reactor:reactor-3.1:javaagent")) + testInstrumentation(project(":instrumentation:reactor:reactor-netty:reactor-netty-1.0:javaagent")) + testInstrumentation(project(":instrumentation:spring:spring-webflux:spring-webflux-5.0:javaagent")) + + testImplementation(project(":instrumentation:spring:spring-cloud-gateway:spring-cloud-gateway-common:testing")) + + testLibrary("org.springframework.cloud:spring-cloud-starter-gateway:2.2.0.RELEASE") + testLibrary("org.springframework.boot:spring-boot-starter-test:2.2.0.RELEASE") +} + +tasks.withType().configureEach { + jvmArgs("-Dotel.instrumentation.spring-cloud-gateway.experimental-span-attributes=true") + + // required on jdk17 + jvmArgs("--add-opens=java.base/java.lang=ALL-UNNAMED") + jvmArgs("-XX:+IgnoreUnrecognizedVMOptions") + + jvmArgs("-Dotel.instrumentation.common.experimental.controller-telemetry.enabled=true") + + systemProperty("testLatestDeps", findProperty("testLatestDeps") as Boolean) +} + +val latestDepTest = findProperty("testLatestDeps") as Boolean + +if (latestDepTest) { + // spring 6 requires java 17 + otelJava { + minJavaVersionSupported.set(JavaVersion.VERSION_17) + } +} else { + // spring 5 requires old logback (and therefore also old slf4j) + configurations.testRuntimeClasspath { + resolutionStrategy { + force("ch.qos.logback:logback-classic:1.2.11") + force("org.slf4j:slf4j-api:1.7.36") + } + } +} diff --git a/instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-2.2/testing/src/test/java/io/opentelemetry/instrumentation/spring/gateway/v2_2/Gateway22RouteMappingTest.java b/instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-2.2/testing/src/test/java/io/opentelemetry/instrumentation/spring/gateway/v2_2/Gateway22RouteMappingTest.java new file mode 100644 index 000000000000..2017cc2ace9c --- /dev/null +++ b/instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-2.2/testing/src/test/java/io/opentelemetry/instrumentation/spring/gateway/v2_2/Gateway22RouteMappingTest.java @@ -0,0 +1,44 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.gateway.v2_2; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.spring.gateway.common.AbstractRouteMappingTest; +import io.opentelemetry.testing.internal.armeria.common.AggregatedHttpResponse; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +@ExtendWith(SpringExtension.class) +@SpringBootTest( + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + classes = { + Gateway22TestApplication.class, + Gateway22RouteMappingTest.ForceNettyAutoConfiguration.class + }) +class Gateway22RouteMappingTest extends AbstractRouteMappingTest { + + @Test + void gatewayRouteMappingTest() { + String requestBody = "gateway"; + AggregatedHttpResponse response = client.post("/gateway/echo", requestBody).aggregate().join(); + assertThat(response.status().code()).isEqualTo(200); + assertThat(response.contentUtf8()).isEqualTo(requestBody); + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("POST") + .hasKind(SpanKind.SERVER) + .hasAttributesSatisfying( + // Global filter is not route filter, so filter size should be 0. + buildAttributeAssertions("h1c://mock.response", 2023, 0)), + span -> span.hasName(WEBFLUX_SPAN_NAME).hasKind(SpanKind.INTERNAL))); + } +} diff --git a/instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-2.2/testing/src/test/java/io/opentelemetry/instrumentation/spring/gateway/v2_2/Gateway22TestApplication.java b/instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-2.2/testing/src/test/java/io/opentelemetry/instrumentation/spring/gateway/v2_2/Gateway22TestApplication.java new file mode 100644 index 000000000000..b8fd00860440 --- /dev/null +++ b/instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-2.2/testing/src/test/java/io/opentelemetry/instrumentation/spring/gateway/v2_2/Gateway22TestApplication.java @@ -0,0 +1,18 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.gateway.v2_2; + +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.gateway.filter.GlobalFilter; +import org.springframework.context.annotation.Bean; + +@SpringBootApplication +public class Gateway22TestApplication { + @Bean + public GlobalFilter echoFilter() { + return (exchange, chain) -> exchange.getResponse().writeWith(exchange.getRequest().getBody()); + } +} diff --git a/instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-2.2/testing/src/test/resources/application.yml b/instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-2.2/testing/src/test/resources/application.yml new file mode 100644 index 000000000000..495bde2f5c16 --- /dev/null +++ b/instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-2.2/testing/src/test/resources/application.yml @@ -0,0 +1,8 @@ +spring: + cloud: + gateway: + routes: + - uri: h1c://mock.response + predicates: + - Path=/gateway/echo + order: 2023 diff --git a/instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-common/testing/build.gradle.kts b/instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-common/testing/build.gradle.kts new file mode 100644 index 000000000000..0a491303fd0c --- /dev/null +++ b/instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-common/testing/build.gradle.kts @@ -0,0 +1,9 @@ +plugins { + id("otel.java-conventions") +} + +dependencies { + implementation(project(":testing-common")) + + compileOnly("org.springframework.boot:spring-boot-starter-test:2.0.0.RELEASE") +} diff --git a/instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-common/testing/src/main/java/io/opentelemetry/instrumentation/spring/gateway/common/AbstractRouteMappingTest.java b/instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-common/testing/src/main/java/io/opentelemetry/instrumentation/spring/gateway/common/AbstractRouteMappingTest.java new file mode 100644 index 000000000000..b670ef420298 --- /dev/null +++ b/instrumentation/spring/spring-cloud-gateway/spring-cloud-gateway-common/testing/src/main/java/io/opentelemetry/instrumentation/spring/gateway/common/AbstractRouteMappingTest.java @@ -0,0 +1,66 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.gateway.common; + +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.sdk.testing.assertj.AttributeAssertion; +import io.opentelemetry.testing.internal.armeria.client.WebClient; +import java.util.ArrayList; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.boot.web.embedded.netty.NettyReactiveWebServerFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.util.StringUtils; + +public abstract class AbstractRouteMappingTest { + @TestConfiguration + public static class ForceNettyAutoConfiguration { + @Bean + NettyReactiveWebServerFactory nettyFactory() { + return new NettyReactiveWebServerFactory(); + } + } + + @RegisterExtension + protected static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + @Value("${local.server.port}") + private int port; + + protected WebClient client; + + protected static final String WEBFLUX_SPAN_NAME = "FilteringWebHandler.handle"; + + @BeforeEach + void beforeEach() { + client = WebClient.builder("h1c://localhost:" + port).followRedirects().build(); + } + + protected List buildAttributeAssertions( + String routeId, String uri, int order, int filterSize) { + List assertions = new ArrayList<>(); + if (!StringUtils.isEmpty(routeId)) { + assertions.add(equalTo(AttributeKey.stringKey("spring-cloud-gateway.route.id"), routeId)); + } + assertions.add(equalTo(AttributeKey.stringKey("spring-cloud-gateway.route.uri"), uri)); + assertions.add(equalTo(AttributeKey.longKey("spring-cloud-gateway.route.order"), order)); + assertions.add( + equalTo(AttributeKey.longKey("spring-cloud-gateway.route.filter.size"), filterSize)); + return assertions; + } + + protected List buildAttributeAssertions( + String uri, int order, int filterSize) { + return buildAttributeAssertions(null, uri, order, filterSize); + } +} diff --git a/instrumentation/spring/spring-core-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/core/v2_0/SimpleAsyncTaskExecutorInstrumentation.java b/instrumentation/spring/spring-core-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/core/v2_0/SimpleAsyncTaskExecutorInstrumentation.java index 014de1ad694c..8f46873404d4 100644 --- a/instrumentation/spring/spring-core-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/core/v2_0/SimpleAsyncTaskExecutorInstrumentation.java +++ b/instrumentation/spring/spring-core-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/core/v2_0/SimpleAsyncTaskExecutorInstrumentation.java @@ -44,8 +44,7 @@ public void transform(TypeTransformer transformer) { public static class ExecuteAdvice { @Advice.OnMethodEnter(suppress = Throwable.class) - public static PropagatedContext enterJobSubmit( - @Advice.Argument(value = 0, readOnly = false) Runnable task) { + public static PropagatedContext enterJobSubmit(@Advice.Argument(0) Runnable task) { Context context = Java8BytecodeBridge.currentContext(); if (ExecutorAdviceHelper.shouldPropagateContext(context, task)) { VirtualField virtualField = diff --git a/instrumentation/spring/spring-core-2.0/javaagent/src/test/java/SimpleAsyncTaskExecutorInstrumentationTest.java b/instrumentation/spring/spring-core-2.0/javaagent/src/test/java/SimpleAsyncTaskExecutorInstrumentationTest.java index 5e2baddaffd2..cf94a5a69255 100644 --- a/instrumentation/spring/spring-core-2.0/javaagent/src/test/java/SimpleAsyncTaskExecutorInstrumentationTest.java +++ b/instrumentation/spring/spring-core-2.0/javaagent/src/test/java/SimpleAsyncTaskExecutorInstrumentationTest.java @@ -64,14 +64,12 @@ private static void executeTwoTasks(ThrowingConsumer task) { }); testing.waitAndAssertTraces( trace -> - trace - .hasSize(2) - .hasSpansSatisfyingExactly( - span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(), - span -> - span.hasName("asyncChild") - .hasKind(SpanKind.INTERNAL) - .hasParent(trace.getSpan(0)))); + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(), + span -> + span.hasName("asyncChild") + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(0)))); } static class AsyncTask implements Runnable, Callable { diff --git a/instrumentation/spring/spring-data/spring-data-1.8/javaagent/build.gradle.kts b/instrumentation/spring/spring-data/spring-data-1.8/javaagent/build.gradle.kts index 61965ebc758a..d5d7900af243 100644 --- a/instrumentation/spring/spring-data/spring-data-1.8/javaagent/build.gradle.kts +++ b/instrumentation/spring/spring-data/spring-data-1.8/javaagent/build.gradle.kts @@ -26,6 +26,7 @@ muzzle { dependencies { library("org.springframework.data:spring-data-commons:1.8.0.RELEASE") compileOnly("org.springframework:spring-aop:1.2") + compileOnly(project(":instrumentation-annotations-support")) testInstrumentation(project(":instrumentation:jdbc:javaagent")) @@ -49,5 +50,6 @@ tasks { jvmArgs("--add-opens=java.base/java.lang.invoke=ALL-UNNAMED") jvmArgs("--add-opens=java.base/java.lang=ALL-UNNAMED") jvmArgs("-XX:+IgnoreUnrecognizedVMOptions") + jvmArgs("-Dotel.instrumentation.common.experimental.controller-telemetry.enabled=true") } } diff --git a/instrumentation/spring/spring-data/spring-data-1.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/data/v1_8/SpringDataInstrumentationModule.java b/instrumentation/spring/spring-data/spring-data-1.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/data/v1_8/SpringDataInstrumentationModule.java index 7ad4d949fb00..c0c1ac8ab560 100644 --- a/instrumentation/spring/spring-data/spring-data-1.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/data/v1_8/SpringDataInstrumentationModule.java +++ b/instrumentation/spring/spring-data/spring-data-1.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/data/v1_8/SpringDataInstrumentationModule.java @@ -14,7 +14,8 @@ import com.google.auto.service.AutoService; import io.opentelemetry.context.Context; import io.opentelemetry.context.Scope; -import io.opentelemetry.instrumentation.api.instrumenter.util.ClassAndMethod; +import io.opentelemetry.instrumentation.api.annotation.support.async.AsyncOperationEndSupport; +import io.opentelemetry.instrumentation.api.incubator.semconv.util.ClassAndMethod; import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; @@ -109,8 +110,8 @@ public Object invoke(MethodInvocation methodInvocation) throws Throwable { Context context = instrumenter().start(parentContext, classAndMethod); try (Scope ignored = context.makeCurrent()) { Object result = methodInvocation.proceed(); - instrumenter().end(context, classAndMethod, null, null); - return result; + return AsyncOperationEndSupport.create(instrumenter(), Void.class, method.getReturnType()) + .asyncEnd(context, classAndMethod, result, null); } catch (Throwable t) { instrumenter().end(context, classAndMethod, null, t); throw t; diff --git a/instrumentation/spring/spring-data/spring-data-1.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/data/v1_8/SpringDataSingletons.java b/instrumentation/spring/spring-data/spring-data-1.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/data/v1_8/SpringDataSingletons.java index b3ba2176a4e7..024d1e799b94 100644 --- a/instrumentation/spring/spring-data/spring-data-1.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/data/v1_8/SpringDataSingletons.java +++ b/instrumentation/spring/spring-data/spring-data-1.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/data/v1_8/SpringDataSingletons.java @@ -6,11 +6,11 @@ package io.opentelemetry.javaagent.instrumentation.spring.data.v1_8; import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.instrumentation.api.incubator.semconv.code.CodeAttributesExtractor; +import io.opentelemetry.instrumentation.api.incubator.semconv.code.CodeAttributesGetter; +import io.opentelemetry.instrumentation.api.incubator.semconv.code.CodeSpanNameExtractor; +import io.opentelemetry.instrumentation.api.incubator.semconv.util.ClassAndMethod; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; -import io.opentelemetry.instrumentation.api.instrumenter.code.CodeAttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.code.CodeAttributesGetter; -import io.opentelemetry.instrumentation.api.instrumenter.code.CodeSpanNameExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.util.ClassAndMethod; public final class SpringDataSingletons { diff --git a/instrumentation/spring/spring-data/spring-data-3.0/testing/src/test/java/SpringJpaTest.java b/instrumentation/spring/spring-data/spring-data-1.8/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/data/v1_8/SpringJpaTest.java similarity index 56% rename from instrumentation/spring/spring-data/spring-data-3.0/testing/src/test/java/SpringJpaTest.java rename to instrumentation/spring/spring-data/spring-data-1.8/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/data/v1_8/SpringJpaTest.java index ffe7702bcd56..d662e52eb3d2 100644 --- a/instrumentation/spring/spring-data/spring-data-3.0/testing/src/test/java/SpringJpaTest.java +++ b/instrumentation/spring/spring-data/spring-data-1.8/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/data/v1_8/SpringJpaTest.java @@ -3,7 +3,11 @@ * SPDX-License-Identifier: Apache-2.0 */ +package io.opentelemetry.javaagent.instrumentation.spring.data.v1_8; + +import io.opentelemetry.javaagent.instrumentation.spring.data.AbstractSpringJpaTest; import java.util.List; +import java.util.Optional; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import spring.jpa.JpaCustomer; import spring.jpa.JpaCustomerRepository; @@ -12,27 +16,27 @@ public class SpringJpaTest extends AbstractSpringJpaTest { @Override - JpaCustomer newCustomer(String firstName, String lastName) { + protected JpaCustomer newCustomer(String firstName, String lastName) { return new JpaCustomer(firstName, lastName); } @Override - Long id(JpaCustomer customer) { + protected Long id(JpaCustomer customer) { return customer.getId(); } @Override - void setFirstName(JpaCustomer customer, String firstName) { + protected void setFirstName(JpaCustomer customer, String firstName) { customer.setFirstName(firstName); } @Override - Class repositoryClass() { + protected Class repositoryClass() { return JpaCustomerRepository.class; } @Override - JpaCustomerRepository repository() { + protected JpaCustomerRepository repository() { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(JpaPersistenceConfig.class); JpaCustomerRepository repo = context.getBean(JpaCustomerRepository.class); @@ -44,12 +48,18 @@ JpaCustomerRepository repository() { } @Override - List findByLastName(JpaCustomerRepository repository, String lastName) { + protected List findByLastName(JpaCustomerRepository repository, String lastName) { return repository.findByLastName(lastName); } @Override - List findSpecialCustomers(JpaCustomerRepository repository) { + protected List findSpecialCustomers(JpaCustomerRepository repository) { return repository.findSpecialCustomers(); } + + @Override + protected Optional findOneByLastName( + JpaCustomerRepository repository, String lastName) { + return repository.findOneByLastName(lastName); + } } diff --git a/instrumentation/spring/spring-data/spring-data-1.8/javaagent/src/test/java/spring/jpa/JpaCustomer.java b/instrumentation/spring/spring-data/spring-data-1.8/javaagent/src/test/java/spring/jpa/JpaCustomer.java index f5288a98bd9d..e11bfe8053bc 100644 --- a/instrumentation/spring/spring-data/spring-data-1.8/javaagent/src/test/java/spring/jpa/JpaCustomer.java +++ b/instrumentation/spring/spring-data/spring-data-1.8/javaagent/src/test/java/spring/jpa/JpaCustomer.java @@ -5,6 +5,7 @@ package spring.jpa; +import java.util.Locale; import java.util.Objects; import javax.annotation.Nullable; import javax.persistence.Entity; @@ -55,7 +56,8 @@ public void setLastName(String lastName) { @Override public String toString() { - return String.format("Customer[id=%d, firstName='%s', lastName='%s']", id, firstName, lastName); + return String.format( + Locale.ROOT, "Customer[id=%d, firstName='%s', lastName='%s']", id, firstName, lastName); } @Override diff --git a/instrumentation/spring/spring-data/spring-data-1.8/javaagent/src/test/java/spring/jpa/JpaCustomerRepository.java b/instrumentation/spring/spring-data/spring-data-1.8/javaagent/src/test/java/spring/jpa/JpaCustomerRepository.java index 53ab7ae12df2..990ec4f101f9 100644 --- a/instrumentation/spring/spring-data/spring-data-1.8/javaagent/src/test/java/spring/jpa/JpaCustomerRepository.java +++ b/instrumentation/spring/spring-data/spring-data-1.8/javaagent/src/test/java/spring/jpa/JpaCustomerRepository.java @@ -6,9 +6,12 @@ package spring.jpa; import java.util.List; +import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; public interface JpaCustomerRepository extends JpaRepository, JpaCustomerRepositoryCustom { List findByLastName(String lastName); + + Optional findOneByLastName(String lastName); } diff --git a/instrumentation/spring/spring-data/spring-data-3.0/testing/build.gradle.kts b/instrumentation/spring/spring-data/spring-data-3.0/testing/build.gradle.kts index 988a67d4745f..d1cdb1a3b60a 100644 --- a/instrumentation/spring/spring-data/spring-data-3.0/testing/build.gradle.kts +++ b/instrumentation/spring/spring-data/spring-data-3.0/testing/build.gradle.kts @@ -4,6 +4,8 @@ plugins { dependencies { testInstrumentation(project(":instrumentation:jdbc:javaagent")) + testInstrumentation(project(":instrumentation:r2dbc-1.0:javaagent")) + testInstrumentation(project(":instrumentation:reactor:reactor-3.1:javaagent")) testInstrumentation(project(":instrumentation:spring:spring-core-2.0:javaagent")) testInstrumentation(project(":instrumentation:spring:spring-data:spring-data-1.8:javaagent")) @@ -12,19 +14,42 @@ dependencies { testLibrary("org.hibernate.orm:hibernate-core:6.0.0.Final") testLibrary("org.springframework.data:spring-data-commons:3.0.0") testLibrary("org.springframework.data:spring-data-jpa:3.0.0") + testLibrary("org.springframework.data:spring-data-r2dbc:3.0.0") testLibrary("org.springframework:spring-test:6.0.0") testImplementation("org.hsqldb:hsqldb:2.0.0") + testImplementation("com.h2database:h2:1.4.197") + testImplementation("io.r2dbc:r2dbc-h2:1.0.0.RELEASE") + + latestDepTestLibrary("org.hibernate.orm:hibernate-core:6.2.+") } otelJava { minJavaVersionSupported.set(JavaVersion.VERSION_17) } +testing { + suites { + val reactiveTest by registering(JvmTestSuite::class) { + dependencies { + implementation("org.springframework.data:spring-data-r2dbc:3.0.0") + implementation("org.testcontainers:testcontainers") + implementation("io.r2dbc:r2dbc-h2:1.0.0.RELEASE") + implementation("com.h2database:h2:1.4.197") + } + } + } +} + tasks { test { jvmArgs("--add-opens=java.base/java.lang.invoke=ALL-UNNAMED") jvmArgs("--add-opens=java.base/java.lang=ALL-UNNAMED") jvmArgs("-XX:+IgnoreUnrecognizedVMOptions") + jvmArgs("-Dotel.instrumentation.common.experimental.controller-telemetry.enabled=true") + } + + check { + dependsOn(testing.suites) } } diff --git a/instrumentation/spring/spring-data/spring-data-3.0/testing/src/reactiveTest/java/io/opentelemetry/javaagent/instrumentation/spring/data/v3_0/ReactiveSpringDataTest.java b/instrumentation/spring/spring-data/spring-data-3.0/testing/src/reactiveTest/java/io/opentelemetry/javaagent/instrumentation/spring/data/v3_0/ReactiveSpringDataTest.java new file mode 100644 index 000000000000..826bf0877497 --- /dev/null +++ b/instrumentation/spring/spring-data/spring-data-3.0/testing/src/reactiveTest/java/io/opentelemetry/javaagent/instrumentation/spring/data/v3_0/ReactiveSpringDataTest.java @@ -0,0 +1,93 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.data.v3_0; + +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; +import static io.opentelemetry.semconv.ServerAttributes.SERVER_ADDRESS; +import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DB_CONNECTION_STRING; +import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DB_NAME; +import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DB_OPERATION; +import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DB_SQL_TABLE; +import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DB_STATEMENT; +import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DB_SYSTEM; +import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DB_USER; +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.javaagent.instrumentation.spring.data.v3_0.repository.CustomerRepository; +import io.opentelemetry.javaagent.instrumentation.spring.data.v3_0.repository.PersistenceConfig; +import io.opentelemetry.semconv.incubating.CodeIncubatingAttributes; +import java.time.Duration; +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; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; + +class ReactiveSpringDataTest { + + @RegisterExtension + private static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + private static ConfigurableApplicationContext applicationContext; + private static CustomerRepository customerRepository; + + @BeforeAll + static void setUp() { + applicationContext = new AnnotationConfigApplicationContext(PersistenceConfig.class); + customerRepository = applicationContext.getBean(CustomerRepository.class); + } + + @AfterAll + static void cleanUp() { + applicationContext.close(); + } + + @SuppressWarnings("deprecation") // TODO DbIncubatingAttributes.DB_CONNECTION_STRING deprecation + @Test + void testFindAll() { + long count = + testing + .runWithSpan("parent", () -> customerRepository.findAll()) + .count() + .block(Duration.ofSeconds(30)); + assertThat(count).isEqualTo(1); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasKind(SpanKind.INTERNAL), + span -> + span.hasName("CustomerRepository.findAll") + .hasKind(SpanKind.INTERNAL) + .hasAttributesSatisfyingExactly( + equalTo( + CodeIncubatingAttributes.CODE_NAMESPACE, + CustomerRepository.class.getName()), + equalTo(CodeIncubatingAttributes.CODE_FUNCTION, "findAll")), + span -> + span.hasName("SELECT db.CUSTOMER") + .hasKind(SpanKind.CLIENT) + .hasParent(trace.getSpan(1)) + // assert that this span ends before its parent span + .satisfies( + spanData -> + assertThat(spanData.getEndEpochNanos()) + .isLessThanOrEqualTo(trace.getSpan(1).getEndEpochNanos())) + .hasAttributesSatisfyingExactly( + equalTo(DB_SYSTEM, "h2"), + equalTo(DB_NAME, "db"), + equalTo(DB_USER, "sa"), + equalTo(DB_STATEMENT, "SELECT CUSTOMER.* FROM CUSTOMER"), + equalTo(DB_OPERATION, "SELECT"), + equalTo(DB_SQL_TABLE, "CUSTOMER"), + equalTo(DB_CONNECTION_STRING, "h2:mem://localhost"), + equalTo(SERVER_ADDRESS, "localhost")))); + } +} diff --git a/instrumentation/spring/spring-data/spring-data-3.0/testing/src/reactiveTest/java/io/opentelemetry/javaagent/instrumentation/spring/data/v3_0/repository/Customer.java b/instrumentation/spring/spring-data/spring-data-3.0/testing/src/reactiveTest/java/io/opentelemetry/javaagent/instrumentation/spring/data/v3_0/repository/Customer.java new file mode 100644 index 000000000000..e1229ff4fd5e --- /dev/null +++ b/instrumentation/spring/spring-data/spring-data-3.0/testing/src/reactiveTest/java/io/opentelemetry/javaagent/instrumentation/spring/data/v3_0/repository/Customer.java @@ -0,0 +1,75 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.data.v3_0.repository; + +import java.util.Locale; +import java.util.Objects; +import javax.annotation.Nullable; +import org.springframework.data.annotation.Id; + +public class Customer { + + @Id 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( + Locale.ROOT, "Customer[id=%d, firstName='%s', lastName='%s']", id, firstName, lastName); + } + + @Override + public boolean equals(@Nullable 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/instrumentation/spring/spring-data/spring-data-3.0/testing/src/reactiveTest/java/io/opentelemetry/javaagent/instrumentation/spring/data/v3_0/repository/CustomerRepository.java b/instrumentation/spring/spring-data/spring-data-3.0/testing/src/reactiveTest/java/io/opentelemetry/javaagent/instrumentation/spring/data/v3_0/repository/CustomerRepository.java new file mode 100644 index 000000000000..ba07256a44bd --- /dev/null +++ b/instrumentation/spring/spring-data/spring-data-3.0/testing/src/reactiveTest/java/io/opentelemetry/javaagent/instrumentation/spring/data/v3_0/repository/CustomerRepository.java @@ -0,0 +1,10 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.data.v3_0.repository; + +import org.springframework.data.repository.reactive.ReactiveCrudRepository; + +public interface CustomerRepository extends ReactiveCrudRepository {} diff --git a/instrumentation/spring/spring-data/spring-data-3.0/testing/src/reactiveTest/java/io/opentelemetry/javaagent/instrumentation/spring/data/v3_0/repository/PersistenceConfig.java b/instrumentation/spring/spring-data/spring-data-3.0/testing/src/reactiveTest/java/io/opentelemetry/javaagent/instrumentation/spring/data/v3_0/repository/PersistenceConfig.java new file mode 100644 index 000000000000..7368162cd001 --- /dev/null +++ b/instrumentation/spring/spring-data/spring-data-3.0/testing/src/reactiveTest/java/io/opentelemetry/javaagent/instrumentation/spring/data/v3_0/repository/PersistenceConfig.java @@ -0,0 +1,67 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.data.v3_0.repository; + +import static io.r2dbc.spi.ConnectionFactoryOptions.DATABASE; +import static io.r2dbc.spi.ConnectionFactoryOptions.DRIVER; +import static io.r2dbc.spi.ConnectionFactoryOptions.HOST; +import static io.r2dbc.spi.ConnectionFactoryOptions.PASSWORD; +import static io.r2dbc.spi.ConnectionFactoryOptions.PROTOCOL; +import static io.r2dbc.spi.ConnectionFactoryOptions.USER; + +import io.r2dbc.spi.ConnectionFactories; +import io.r2dbc.spi.ConnectionFactory; +import io.r2dbc.spi.ConnectionFactoryOptions; +import io.r2dbc.spi.Option; +import java.nio.charset.StandardCharsets; +import org.springframework.context.annotation.Bean; +import org.springframework.core.io.ByteArrayResource; +import org.springframework.data.r2dbc.core.R2dbcEntityTemplate; +import org.springframework.data.r2dbc.dialect.H2Dialect; +import org.springframework.data.r2dbc.repository.config.EnableR2dbcRepositories; +import org.springframework.r2dbc.connection.init.ConnectionFactoryInitializer; +import org.springframework.r2dbc.connection.init.ResourceDatabasePopulator; +import org.springframework.r2dbc.core.DatabaseClient; + +@EnableR2dbcRepositories( + basePackages = "io.opentelemetry.javaagent.instrumentation.spring.data.v3_0.repository") +public class PersistenceConfig { + + @Bean + ConnectionFactory connectionFactory() { + return ConnectionFactories.find( + ConnectionFactoryOptions.builder() + .option(DRIVER, "h2") + .option(PROTOCOL, "mem") + .option(HOST, "localhost") + .option(USER, "sa") + .option(PASSWORD, "") + .option(DATABASE, "db") + .option(Option.valueOf("DB_CLOSE_DELAY"), "-1") + .build()); + } + + @Bean + ConnectionFactoryInitializer initializer(ConnectionFactory connectionFactory) { + ConnectionFactoryInitializer initializer = new ConnectionFactoryInitializer(); + initializer.setConnectionFactory(connectionFactory); + initializer.setDatabasePopulator( + new ResourceDatabasePopulator( + new ByteArrayResource( + ("CREATE TABLE customer (id INT PRIMARY KEY, firstname VARCHAR(100) NOT NULL, lastname VARCHAR(100) NOT NULL);" + + "INSERT INTO customer (id, firstname, lastname) VALUES ('1', 'First', 'Last');") + .getBytes(StandardCharsets.UTF_8)))); + + return initializer; + } + + @Bean + public R2dbcEntityTemplate r2dbcEntityTemplate(ConnectionFactory connectionFactory) { + DatabaseClient databaseClient = DatabaseClient.create(connectionFactory); + + return new R2dbcEntityTemplate(databaseClient, H2Dialect.INSTANCE); + } +} diff --git a/instrumentation/spring/spring-data/spring-data-1.8/javaagent/src/test/java/SprintJpaTest.java b/instrumentation/spring/spring-data/spring-data-3.0/testing/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/data/v3_0/SpringJpaTest.java similarity index 53% rename from instrumentation/spring/spring-data/spring-data-1.8/javaagent/src/test/java/SprintJpaTest.java rename to instrumentation/spring/spring-data/spring-data-3.0/testing/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/data/v3_0/SpringJpaTest.java index 93189f602b9c..fe4fffa719e5 100644 --- a/instrumentation/spring/spring-data/spring-data-1.8/javaagent/src/test/java/SprintJpaTest.java +++ b/instrumentation/spring/spring-data/spring-data-3.0/testing/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/data/v3_0/SpringJpaTest.java @@ -3,36 +3,40 @@ * SPDX-License-Identifier: Apache-2.0 */ +package io.opentelemetry.javaagent.instrumentation.spring.data.v3_0; + +import io.opentelemetry.javaagent.instrumentation.spring.data.AbstractSpringJpaTest; import java.util.List; +import java.util.Optional; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import spring.jpa.JpaCustomer; import spring.jpa.JpaCustomerRepository; import spring.jpa.JpaPersistenceConfig; -public class SprintJpaTest extends AbstractSpringJpaTest { +public class SpringJpaTest extends AbstractSpringJpaTest { @Override - JpaCustomer newCustomer(String firstName, String lastName) { + protected JpaCustomer newCustomer(String firstName, String lastName) { return new JpaCustomer(firstName, lastName); } @Override - Long id(JpaCustomer customer) { + protected Long id(JpaCustomer customer) { return customer.getId(); } @Override - void setFirstName(JpaCustomer customer, String firstName) { + protected void setFirstName(JpaCustomer customer, String firstName) { customer.setFirstName(firstName); } @Override - Class repositoryClass() { + protected Class repositoryClass() { return JpaCustomerRepository.class; } @Override - JpaCustomerRepository repository() { + protected JpaCustomerRepository repository() { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(JpaPersistenceConfig.class); JpaCustomerRepository repo = context.getBean(JpaCustomerRepository.class); @@ -44,12 +48,18 @@ JpaCustomerRepository repository() { } @Override - List findByLastName(JpaCustomerRepository repository, String lastName) { + protected List findByLastName(JpaCustomerRepository repository, String lastName) { return repository.findByLastName(lastName); } @Override - List findSpecialCustomers(JpaCustomerRepository repository) { + protected List findSpecialCustomers(JpaCustomerRepository repository) { return repository.findSpecialCustomers(); } + + @Override + protected Optional findOneByLastName( + JpaCustomerRepository repository, String lastName) { + return repository.findOneByLastName(lastName); + } } diff --git a/instrumentation/spring/spring-data/spring-data-3.0/testing/src/test/java/spring/jpa/JpaCustomer.java b/instrumentation/spring/spring-data/spring-data-3.0/testing/src/test/java/spring/jpa/JpaCustomer.java index 37e2f237c649..447399d961c6 100644 --- a/instrumentation/spring/spring-data/spring-data-3.0/testing/src/test/java/spring/jpa/JpaCustomer.java +++ b/instrumentation/spring/spring-data/spring-data-3.0/testing/src/test/java/spring/jpa/JpaCustomer.java @@ -9,6 +9,7 @@ import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; +import java.util.Locale; import java.util.Objects; import javax.annotation.Nullable; @@ -55,7 +56,8 @@ public void setLastName(String lastName) { @Override public String toString() { - return String.format("Customer[id=%d, firstName='%s', lastName='%s']", id, firstName, lastName); + return String.format( + Locale.ROOT, "Customer[id=%d, firstName='%s', lastName='%s']", id, firstName, lastName); } @Override diff --git a/instrumentation/spring/spring-data/spring-data-3.0/testing/src/test/java/spring/jpa/JpaCustomerRepository.java b/instrumentation/spring/spring-data/spring-data-3.0/testing/src/test/java/spring/jpa/JpaCustomerRepository.java index 53ab7ae12df2..990ec4f101f9 100644 --- a/instrumentation/spring/spring-data/spring-data-3.0/testing/src/test/java/spring/jpa/JpaCustomerRepository.java +++ b/instrumentation/spring/spring-data/spring-data-3.0/testing/src/test/java/spring/jpa/JpaCustomerRepository.java @@ -6,9 +6,12 @@ package spring.jpa; import java.util.List; +import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; public interface JpaCustomerRepository extends JpaRepository, JpaCustomerRepositoryCustom { List findByLastName(String lastName); + + Optional findOneByLastName(String lastName); } diff --git a/instrumentation/spring/spring-data/spring-data-common/testing/src/main/java/AbstractSpringJpaTest.java b/instrumentation/spring/spring-data/spring-data-common/testing/src/main/java/AbstractSpringJpaTest.java deleted file mode 100644 index 0455ee4865bc..000000000000 --- a/instrumentation/spring/spring-data/spring-data-common/testing/src/main/java/AbstractSpringJpaTest.java +++ /dev/null @@ -1,322 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; -import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.satisfies; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import io.opentelemetry.api.trace.SpanKind; -import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; -import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; -import io.opentelemetry.sdk.testing.assertj.TraceAssert; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; -import java.util.List; -import org.hibernate.Version; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.RegisterExtension; -import org.springframework.data.jpa.repository.JpaRepository; - -public abstract class AbstractSpringJpaTest< - ENTITY, REPOSITORY extends JpaRepository> { - - @RegisterExtension - private static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); - - abstract ENTITY newCustomer(String firstName, String lastName); - - abstract Long id(ENTITY customer); - - abstract void setFirstName(ENTITY customer, String firstName); - - abstract Class repositoryClass(); - - abstract REPOSITORY repository(); - - abstract List findByLastName(REPOSITORY repository, String lastName); - - abstract List findSpecialCustomers(REPOSITORY repository); - - void clearData() { - testing.clearData(); - } - - @Test - void testObjectMethod() { - REPOSITORY repo = repository(); - - testing.runWithSpan("toString test", repo::toString); - - // Asserting that a span is NOT created for toString - testing.waitAndAssertTraces( - trace -> - trace - .hasSize(1) - .hasSpansSatisfyingExactly( - span -> span.hasName("toString test").hasTotalAttributeCount(0))); - } - - static void assertHibernate4Trace(TraceAssert trace, String repoClassName) { - trace - .hasSize(2) - .hasSpansSatisfyingExactly( - span -> - span.hasName("JpaCustomerRepository.save") - .hasKind(SpanKind.INTERNAL) - .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.CODE_NAMESPACE, repoClassName), - equalTo(SemanticAttributes.CODE_FUNCTION, "save")), - span -> - span.hasName("INSERT test.JpaCustomer") - .hasKind(SpanKind.CLIENT) - .hasParent(trace.getSpan(0)) - .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.DB_SYSTEM, "hsqldb"), - equalTo(SemanticAttributes.DB_NAME, "test"), - equalTo(SemanticAttributes.DB_USER, "sa"), - equalTo(SemanticAttributes.DB_CONNECTION_STRING, "hsqldb:mem:"), - satisfies( - SemanticAttributes.DB_STATEMENT, val -> val.startsWith("insert ")), - equalTo(SemanticAttributes.DB_OPERATION, "INSERT"), - equalTo(SemanticAttributes.DB_SQL_TABLE, "JpaCustomer"))); - } - - static void assertHibernateTrace(TraceAssert trace, String repoClassName) { - trace - .hasSize(3) - .hasSpansSatisfyingExactly( - span -> - span.hasName("JpaCustomerRepository.save") - .hasKind(SpanKind.INTERNAL) - .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.CODE_NAMESPACE, repoClassName), - equalTo(SemanticAttributes.CODE_FUNCTION, "save")), - span -> - span.hasName("CALL test") - .hasKind(SpanKind.CLIENT) - .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.DB_SYSTEM, "hsqldb"), - equalTo(SemanticAttributes.DB_NAME, "test"), - equalTo(SemanticAttributes.DB_USER, "sa"), - equalTo(SemanticAttributes.DB_CONNECTION_STRING, "hsqldb:mem:"), - satisfies( - SemanticAttributes.DB_STATEMENT, - val -> val.startsWith("call next value for ")), - equalTo(SemanticAttributes.DB_OPERATION, "CALL")), - span -> - span.hasName("INSERT test.JpaCustomer") - .hasKind(SpanKind.CLIENT) - .hasParent(trace.getSpan(0)) - .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.DB_SYSTEM, "hsqldb"), - equalTo(SemanticAttributes.DB_NAME, "test"), - equalTo(SemanticAttributes.DB_USER, "sa"), - equalTo(SemanticAttributes.DB_CONNECTION_STRING, "hsqldb:mem:"), - satisfies( - SemanticAttributes.DB_STATEMENT, val -> val.startsWith("insert ")), - equalTo(SemanticAttributes.DB_OPERATION, "INSERT"), - equalTo(SemanticAttributes.DB_SQL_TABLE, "JpaCustomer"))); - } - - @Test - void testCrud() { - boolean isHibernate4 = Version.getVersionString().startsWith("4."); - REPOSITORY repo = repository(); - String repoClassName = repositoryClass().getName(); - - ENTITY customer = newCustomer("Bob", "Anonymous"); - - assertNull(id(customer)); - assertFalse(repo.findAll().iterator().hasNext()); - - testing.waitAndAssertTraces( - trace -> - trace - .hasSize(2) - .hasSpansSatisfyingExactly( - span -> - span.hasName("JpaCustomerRepository.findAll") - .hasKind(SpanKind.INTERNAL) - .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.CODE_NAMESPACE, repoClassName), - equalTo(SemanticAttributes.CODE_FUNCTION, "findAll")), - span -> - span.hasName("SELECT test.JpaCustomer") - .hasKind(SpanKind.CLIENT) - .hasParent(trace.getSpan(0)) - .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.DB_SYSTEM, "hsqldb"), - equalTo(SemanticAttributes.DB_NAME, "test"), - equalTo(SemanticAttributes.DB_USER, "sa"), - equalTo(SemanticAttributes.DB_CONNECTION_STRING, "hsqldb:mem:"), - satisfies( - SemanticAttributes.DB_STATEMENT, - val -> val.startsWith("select ")), - equalTo(SemanticAttributes.DB_OPERATION, "SELECT"), - equalTo(SemanticAttributes.DB_SQL_TABLE, "JpaCustomer")))); - clearData(); - - repo.save(customer); - assertNotNull(id(customer)); - Long savedId = id(customer); - if (isHibernate4) { - testing.waitAndAssertTraces(trace -> assertHibernate4Trace(trace, repoClassName)); - } else { - testing.waitAndAssertTraces(trace -> assertHibernateTrace(trace, repoClassName)); - } - clearData(); - - setFirstName(customer, "Bill"); - repo.save(customer); - assertEquals(id(customer), savedId); - testing.waitAndAssertTraces( - trace -> - trace - .hasSize(3) - .hasSpansSatisfyingExactly( - span -> - span.hasName("JpaCustomerRepository.save") - .hasKind(SpanKind.INTERNAL) - .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.CODE_NAMESPACE, repoClassName), - equalTo(SemanticAttributes.CODE_FUNCTION, "save")), - span -> - span.hasName("SELECT test.JpaCustomer") - .hasKind(SpanKind.CLIENT) - .hasParent(trace.getSpan(0)) - .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.DB_SYSTEM, "hsqldb"), - equalTo(SemanticAttributes.DB_NAME, "test"), - equalTo(SemanticAttributes.DB_USER, "sa"), - equalTo(SemanticAttributes.DB_CONNECTION_STRING, "hsqldb:mem:"), - satisfies( - SemanticAttributes.DB_STATEMENT, - val -> val.startsWith("select ")), - equalTo(SemanticAttributes.DB_OPERATION, "SELECT"), - equalTo(SemanticAttributes.DB_SQL_TABLE, "JpaCustomer")), - span -> - span.hasName("UPDATE test.JpaCustomer") - .hasKind(SpanKind.CLIENT) - .hasParent(trace.getSpan(0)) - .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.DB_SYSTEM, "hsqldb"), - equalTo(SemanticAttributes.DB_NAME, "test"), - equalTo(SemanticAttributes.DB_USER, "sa"), - equalTo(SemanticAttributes.DB_CONNECTION_STRING, "hsqldb:mem:"), - satisfies( - SemanticAttributes.DB_STATEMENT, - val -> val.startsWith("update ")), - equalTo(SemanticAttributes.DB_OPERATION, "UPDATE"), - equalTo(SemanticAttributes.DB_SQL_TABLE, "JpaCustomer")))); - clearData(); - - customer = findByLastName(repo, "Anonymous").get(0); - testing.waitAndAssertTraces( - trace -> - trace - .hasSize(2) - .hasSpansSatisfyingExactly( - span -> - span.hasName("JpaCustomerRepository.findByLastName") - .hasKind(SpanKind.INTERNAL) - .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.CODE_NAMESPACE, repoClassName), - equalTo(SemanticAttributes.CODE_FUNCTION, "findByLastName")), - span -> - span.hasName("SELECT test.JpaCustomer") - .hasKind(SpanKind.CLIENT) - .hasParent(trace.getSpan(0)) - .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.DB_SYSTEM, "hsqldb"), - equalTo(SemanticAttributes.DB_NAME, "test"), - equalTo(SemanticAttributes.DB_USER, "sa"), - equalTo(SemanticAttributes.DB_CONNECTION_STRING, "hsqldb:mem:"), - satisfies( - SemanticAttributes.DB_STATEMENT, - val -> val.startsWith("select ")), - equalTo(SemanticAttributes.DB_OPERATION, "SELECT"), - equalTo(SemanticAttributes.DB_SQL_TABLE, "JpaCustomer")))); - clearData(); - - repo.delete(customer); - testing.waitAndAssertTraces( - trace -> - trace - .hasSize(3) - .hasSpansSatisfyingExactly( - span -> - span.hasName("JpaCustomerRepository.delete") - .hasKind(SpanKind.INTERNAL) - .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.CODE_NAMESPACE, repoClassName), - equalTo(SemanticAttributes.CODE_FUNCTION, "delete")), - span -> - span.hasName("SELECT test.JpaCustomer") - .hasKind(SpanKind.CLIENT) - .hasParent(trace.getSpan(0)) - .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.DB_SYSTEM, "hsqldb"), - equalTo(SemanticAttributes.DB_NAME, "test"), - equalTo(SemanticAttributes.DB_USER, "sa"), - equalTo(SemanticAttributes.DB_CONNECTION_STRING, "hsqldb:mem:"), - satisfies( - SemanticAttributes.DB_STATEMENT, - val -> val.startsWith("select ")), - equalTo(SemanticAttributes.DB_OPERATION, "SELECT"), - equalTo(SemanticAttributes.DB_SQL_TABLE, "JpaCustomer")), - span -> - span.hasName("DELETE test.JpaCustomer") - .hasKind(SpanKind.CLIENT) - .hasParent(trace.getSpan(0)) - .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.DB_SYSTEM, "hsqldb"), - equalTo(SemanticAttributes.DB_NAME, "test"), - equalTo(SemanticAttributes.DB_USER, "sa"), - equalTo(SemanticAttributes.DB_CONNECTION_STRING, "hsqldb:mem:"), - satisfies( - SemanticAttributes.DB_STATEMENT, - val -> val.startsWith("delete ")), - equalTo(SemanticAttributes.DB_OPERATION, "DELETE"), - equalTo(SemanticAttributes.DB_SQL_TABLE, "JpaCustomer")))); - } - - @Test - void testCustomRepositoryMethod() { - REPOSITORY repo = repository(); - String repoClassName = repositoryClass().getName(); - List customers = findSpecialCustomers(repo); - - assertTrue(customers.isEmpty()); - - testing.waitAndAssertTraces( - trace -> - trace - .hasSize(2) - .hasSpansSatisfyingExactly( - span -> - span.hasName("JpaCustomerRepository.findSpecialCustomers") - .hasKind(SpanKind.INTERNAL) - .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.CODE_NAMESPACE, repoClassName), - equalTo(SemanticAttributes.CODE_FUNCTION, "findSpecialCustomers")), - span -> - span.hasName("SELECT test.JpaCustomer") - .hasKind(SpanKind.CLIENT) - .hasParent(trace.getSpan(0)) - .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.DB_SYSTEM, "hsqldb"), - equalTo(SemanticAttributes.DB_NAME, "test"), - equalTo(SemanticAttributes.DB_USER, "sa"), - equalTo(SemanticAttributes.DB_CONNECTION_STRING, "hsqldb:mem:"), - satisfies( - SemanticAttributes.DB_STATEMENT, - val -> val.startsWith("select ")), - equalTo(SemanticAttributes.DB_OPERATION, "SELECT"), - equalTo(SemanticAttributes.DB_SQL_TABLE, "JpaCustomer")))); - } -} diff --git a/instrumentation/spring/spring-data/spring-data-common/testing/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/data/AbstractSpringJpaTest.java b/instrumentation/spring/spring-data/spring-data-common/testing/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/data/AbstractSpringJpaTest.java new file mode 100644 index 000000000000..730fc05da074 --- /dev/null +++ b/instrumentation/spring/spring-data/spring-data-common/testing/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/data/AbstractSpringJpaTest.java @@ -0,0 +1,367 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.data; + +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.satisfies; +import static org.assertj.core.api.Assertions.catchThrowableOfType; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.sdk.testing.assertj.TraceAssert; +import io.opentelemetry.sdk.trace.data.StatusData; +import io.opentelemetry.semconv.incubating.CodeIncubatingAttributes; +import io.opentelemetry.semconv.incubating.DbIncubatingAttributes; +import java.util.List; +import java.util.Optional; +import org.hibernate.Version; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.springframework.dao.IncorrectResultSizeDataAccessException; +import org.springframework.data.jpa.repository.JpaRepository; + +public abstract class AbstractSpringJpaTest< + ENTITY, REPOSITORY extends JpaRepository> { + + @RegisterExtension + private static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + protected abstract ENTITY newCustomer(String firstName, String lastName); + + protected abstract Long id(ENTITY customer); + + protected abstract void setFirstName(ENTITY customer, String firstName); + + protected abstract Class repositoryClass(); + + protected abstract REPOSITORY repository(); + + protected abstract List findByLastName(REPOSITORY repository, String lastName); + + protected abstract List findSpecialCustomers(REPOSITORY repository); + + protected abstract Optional findOneByLastName(REPOSITORY repository, String lastName); + + protected void clearData() { + testing.clearData(); + } + + @Test + void testObjectMethod() { + REPOSITORY repo = repository(); + + testing.runWithSpan("toString test", repo::toString); + + // Asserting that a span is NOT created for toString + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("toString test").hasTotalAttributeCount(0))); + } + + @SuppressWarnings("deprecation") // TODO DbIncubatingAttributes.DB_CONNECTION_STRING deprecation + static void assertHibernate4Trace(TraceAssert trace, String repoClassName) { + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("JpaCustomerRepository.save") + .hasKind(SpanKind.INTERNAL) + .hasAttributesSatisfyingExactly( + equalTo(CodeIncubatingAttributes.CODE_NAMESPACE, repoClassName), + equalTo(CodeIncubatingAttributes.CODE_FUNCTION, "save")), + span -> + span.hasName("INSERT test.JpaCustomer") + .hasKind(SpanKind.CLIENT) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo(DbIncubatingAttributes.DB_SYSTEM, "hsqldb"), + equalTo(DbIncubatingAttributes.DB_NAME, "test"), + equalTo(DbIncubatingAttributes.DB_USER, "sa"), + equalTo(DbIncubatingAttributes.DB_CONNECTION_STRING, "hsqldb:mem:"), + satisfies( + DbIncubatingAttributes.DB_STATEMENT, val -> val.startsWith("insert ")), + equalTo(DbIncubatingAttributes.DB_OPERATION, "INSERT"), + equalTo(DbIncubatingAttributes.DB_SQL_TABLE, "JpaCustomer"))); + } + + @SuppressWarnings("deprecation") // TODO DbIncubatingAttributes.DB_CONNECTION_STRING deprecation + static void assertHibernateTrace(TraceAssert trace, String repoClassName) { + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("JpaCustomerRepository.save") + .hasKind(SpanKind.INTERNAL) + .hasAttributesSatisfyingExactly( + equalTo(CodeIncubatingAttributes.CODE_NAMESPACE, repoClassName), + equalTo(CodeIncubatingAttributes.CODE_FUNCTION, "save")), + span -> + span.hasName("CALL test") + .hasKind(SpanKind.CLIENT) + .hasAttributesSatisfyingExactly( + equalTo(DbIncubatingAttributes.DB_SYSTEM, "hsqldb"), + equalTo(DbIncubatingAttributes.DB_NAME, "test"), + equalTo(DbIncubatingAttributes.DB_USER, "sa"), + equalTo(DbIncubatingAttributes.DB_CONNECTION_STRING, "hsqldb:mem:"), + satisfies( + DbIncubatingAttributes.DB_STATEMENT, + val -> val.startsWith("call next value for ")), + equalTo(DbIncubatingAttributes.DB_OPERATION, "CALL")), + span -> + span.hasName("INSERT test.JpaCustomer") + .hasKind(SpanKind.CLIENT) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo(DbIncubatingAttributes.DB_SYSTEM, "hsqldb"), + equalTo(DbIncubatingAttributes.DB_NAME, "test"), + equalTo(DbIncubatingAttributes.DB_USER, "sa"), + equalTo(DbIncubatingAttributes.DB_CONNECTION_STRING, "hsqldb:mem:"), + satisfies( + DbIncubatingAttributes.DB_STATEMENT, val -> val.startsWith("insert ")), + equalTo(DbIncubatingAttributes.DB_OPERATION, "INSERT"), + equalTo(DbIncubatingAttributes.DB_SQL_TABLE, "JpaCustomer"))); + } + + @SuppressWarnings("deprecation") // TODO DbIncubatingAttributes.DB_CONNECTION_STRING deprecation + @Test + void testCrud() { + boolean isHibernate4 = Version.getVersionString().startsWith("4."); + REPOSITORY repo = repository(); + String repoClassName = repositoryClass().getName(); + + ENTITY customer = newCustomer("Bob", "Anonymous"); + + assertNull(id(customer)); + assertFalse(repo.findAll().iterator().hasNext()); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("JpaCustomerRepository.findAll") + .hasKind(SpanKind.INTERNAL) + .hasAttributesSatisfyingExactly( + equalTo(CodeIncubatingAttributes.CODE_NAMESPACE, repoClassName), + equalTo(CodeIncubatingAttributes.CODE_FUNCTION, "findAll")), + span -> + span.hasName("SELECT test.JpaCustomer") + .hasKind(SpanKind.CLIENT) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo(DbIncubatingAttributes.DB_SYSTEM, "hsqldb"), + equalTo(DbIncubatingAttributes.DB_NAME, "test"), + equalTo(DbIncubatingAttributes.DB_USER, "sa"), + equalTo(DbIncubatingAttributes.DB_CONNECTION_STRING, "hsqldb:mem:"), + satisfies( + DbIncubatingAttributes.DB_STATEMENT, + val -> val.startsWith("select ")), + equalTo(DbIncubatingAttributes.DB_OPERATION, "SELECT"), + equalTo(DbIncubatingAttributes.DB_SQL_TABLE, "JpaCustomer")))); + clearData(); + + repo.save(customer); + assertNotNull(id(customer)); + Long savedId = id(customer); + if (isHibernate4) { + testing.waitAndAssertTraces(trace -> assertHibernate4Trace(trace, repoClassName)); + } else { + testing.waitAndAssertTraces(trace -> assertHibernateTrace(trace, repoClassName)); + } + clearData(); + + setFirstName(customer, "Bill"); + repo.save(customer); + assertEquals(id(customer), savedId); + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("JpaCustomerRepository.save") + .hasKind(SpanKind.INTERNAL) + .hasAttributesSatisfyingExactly( + equalTo(CodeIncubatingAttributes.CODE_NAMESPACE, repoClassName), + equalTo(CodeIncubatingAttributes.CODE_FUNCTION, "save")), + span -> + span.hasName("SELECT test.JpaCustomer") + .hasKind(SpanKind.CLIENT) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo(DbIncubatingAttributes.DB_SYSTEM, "hsqldb"), + equalTo(DbIncubatingAttributes.DB_NAME, "test"), + equalTo(DbIncubatingAttributes.DB_USER, "sa"), + equalTo(DbIncubatingAttributes.DB_CONNECTION_STRING, "hsqldb:mem:"), + satisfies( + DbIncubatingAttributes.DB_STATEMENT, + val -> val.startsWith("select ")), + equalTo(DbIncubatingAttributes.DB_OPERATION, "SELECT"), + equalTo(DbIncubatingAttributes.DB_SQL_TABLE, "JpaCustomer")), + span -> + span.hasName("UPDATE test.JpaCustomer") + .hasKind(SpanKind.CLIENT) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo(DbIncubatingAttributes.DB_SYSTEM, "hsqldb"), + equalTo(DbIncubatingAttributes.DB_NAME, "test"), + equalTo(DbIncubatingAttributes.DB_USER, "sa"), + equalTo(DbIncubatingAttributes.DB_CONNECTION_STRING, "hsqldb:mem:"), + satisfies( + DbIncubatingAttributes.DB_STATEMENT, + val -> val.startsWith("update ")), + equalTo(DbIncubatingAttributes.DB_OPERATION, "UPDATE"), + equalTo(DbIncubatingAttributes.DB_SQL_TABLE, "JpaCustomer")))); + clearData(); + + customer = findByLastName(repo, "Anonymous").get(0); + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("JpaCustomerRepository.findByLastName") + .hasKind(SpanKind.INTERNAL) + .hasAttributesSatisfyingExactly( + equalTo(CodeIncubatingAttributes.CODE_NAMESPACE, repoClassName), + equalTo(CodeIncubatingAttributes.CODE_FUNCTION, "findByLastName")), + span -> + span.hasName("SELECT test.JpaCustomer") + .hasKind(SpanKind.CLIENT) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo(DbIncubatingAttributes.DB_SYSTEM, "hsqldb"), + equalTo(DbIncubatingAttributes.DB_NAME, "test"), + equalTo(DbIncubatingAttributes.DB_USER, "sa"), + equalTo(DbIncubatingAttributes.DB_CONNECTION_STRING, "hsqldb:mem:"), + satisfies( + DbIncubatingAttributes.DB_STATEMENT, + val -> val.startsWith("select ")), + equalTo(DbIncubatingAttributes.DB_OPERATION, "SELECT"), + equalTo(DbIncubatingAttributes.DB_SQL_TABLE, "JpaCustomer")))); + clearData(); + + repo.delete(customer); + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("JpaCustomerRepository.delete") + .hasKind(SpanKind.INTERNAL) + .hasAttributesSatisfyingExactly( + equalTo(CodeIncubatingAttributes.CODE_NAMESPACE, repoClassName), + equalTo(CodeIncubatingAttributes.CODE_FUNCTION, "delete")), + span -> + span.hasName("SELECT test.JpaCustomer") + .hasKind(SpanKind.CLIENT) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo(DbIncubatingAttributes.DB_SYSTEM, "hsqldb"), + equalTo(DbIncubatingAttributes.DB_NAME, "test"), + equalTo(DbIncubatingAttributes.DB_USER, "sa"), + equalTo(DbIncubatingAttributes.DB_CONNECTION_STRING, "hsqldb:mem:"), + satisfies( + DbIncubatingAttributes.DB_STATEMENT, + val -> val.startsWith("select ")), + equalTo(DbIncubatingAttributes.DB_OPERATION, "SELECT"), + equalTo(DbIncubatingAttributes.DB_SQL_TABLE, "JpaCustomer")), + span -> + span.hasName("DELETE test.JpaCustomer") + .hasKind(SpanKind.CLIENT) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo(DbIncubatingAttributes.DB_SYSTEM, "hsqldb"), + equalTo(DbIncubatingAttributes.DB_NAME, "test"), + equalTo(DbIncubatingAttributes.DB_USER, "sa"), + equalTo(DbIncubatingAttributes.DB_CONNECTION_STRING, "hsqldb:mem:"), + satisfies( + DbIncubatingAttributes.DB_STATEMENT, + val -> val.startsWith("delete ")), + equalTo(DbIncubatingAttributes.DB_OPERATION, "DELETE"), + equalTo(DbIncubatingAttributes.DB_SQL_TABLE, "JpaCustomer")))); + } + + @SuppressWarnings("deprecation") // TODO DbIncubatingAttributes.DB_CONNECTION_STRING deprecation + @Test + void testCustomRepositoryMethod() { + REPOSITORY repo = repository(); + String repoClassName = repositoryClass().getName(); + List customers = findSpecialCustomers(repo); + + assertTrue(customers.isEmpty()); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("JpaCustomerRepository.findSpecialCustomers") + .hasKind(SpanKind.INTERNAL) + .hasAttributesSatisfyingExactly( + equalTo(CodeIncubatingAttributes.CODE_NAMESPACE, repoClassName), + equalTo( + CodeIncubatingAttributes.CODE_FUNCTION, "findSpecialCustomers")), + span -> + span.hasName("SELECT test.JpaCustomer") + .hasKind(SpanKind.CLIENT) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo(DbIncubatingAttributes.DB_SYSTEM, "hsqldb"), + equalTo(DbIncubatingAttributes.DB_NAME, "test"), + equalTo(DbIncubatingAttributes.DB_USER, "sa"), + equalTo(DbIncubatingAttributes.DB_CONNECTION_STRING, "hsqldb:mem:"), + satisfies( + DbIncubatingAttributes.DB_STATEMENT, + val -> val.startsWith("select ")), + equalTo(DbIncubatingAttributes.DB_OPERATION, "SELECT"), + equalTo(DbIncubatingAttributes.DB_SQL_TABLE, "JpaCustomer")))); + } + + @SuppressWarnings("deprecation") // TODO DbIncubatingAttributes.DB_CONNECTION_STRING deprecation + @Test + void testFailedRepositoryMethod() { + // given + REPOSITORY repo = repository(); + String repoClassName = repositoryClass().getName(); + + String commonLastName = "Smith"; + repo.save(newCustomer("Alice", commonLastName)); + repo.save(newCustomer("Bob", commonLastName)); + clearData(); + + // when + IncorrectResultSizeDataAccessException expectedException = + catchThrowableOfType( + () -> findOneByLastName(repo, commonLastName), + IncorrectResultSizeDataAccessException.class); + + // then + assertNotNull(expectedException); + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("JpaCustomerRepository.findOneByLastName") + .hasKind(SpanKind.INTERNAL) + .hasStatus(StatusData.error()) + .hasException(expectedException) + .hasAttributesSatisfyingExactly( + equalTo(CodeIncubatingAttributes.CODE_NAMESPACE, repoClassName), + equalTo(CodeIncubatingAttributes.CODE_FUNCTION, "findOneByLastName")), + span -> + span.hasName("SELECT test.JpaCustomer") + .hasKind(SpanKind.CLIENT) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo(DbIncubatingAttributes.DB_SYSTEM, "hsqldb"), + equalTo(DbIncubatingAttributes.DB_NAME, "test"), + equalTo(DbIncubatingAttributes.DB_USER, "sa"), + equalTo(DbIncubatingAttributes.DB_CONNECTION_STRING, "hsqldb:mem:"), + satisfies( + DbIncubatingAttributes.DB_STATEMENT, + val -> val.startsWith("select ")), + equalTo(DbIncubatingAttributes.DB_OPERATION, "SELECT"), + equalTo(DbIncubatingAttributes.DB_SQL_TABLE, "JpaCustomer")))); + } +} diff --git a/instrumentation/spring/spring-integration-4.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/integration/v4_1/SpringIntegrationSingletons.java b/instrumentation/spring/spring-integration-4.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/integration/v4_1/SpringIntegrationSingletons.java index 25aa7e4e565a..19cbd73fcb2e 100644 --- a/instrumentation/spring/spring-integration-4.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/integration/v4_1/SpringIntegrationSingletons.java +++ b/instrumentation/spring/spring-integration-4.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/integration/v4_1/SpringIntegrationSingletons.java @@ -9,15 +9,15 @@ import io.opentelemetry.api.GlobalOpenTelemetry; import io.opentelemetry.instrumentation.spring.integration.v4_1.SpringIntegrationTelemetry; +import io.opentelemetry.javaagent.bootstrap.internal.AgentInstrumentationConfig; import io.opentelemetry.javaagent.bootstrap.internal.ExperimentalConfig; -import io.opentelemetry.javaagent.bootstrap.internal.InstrumentationConfig; import java.util.List; import org.springframework.messaging.support.ChannelInterceptor; public final class SpringIntegrationSingletons { private static final List PATTERNS = - InstrumentationConfig.get() + AgentInstrumentationConfig.get() .getList( "otel.instrumentation.spring-integration.global-channel-interceptor-patterns", singletonList("*")); @@ -26,7 +26,7 @@ public final class SpringIntegrationSingletons { SpringIntegrationTelemetry.builder(GlobalOpenTelemetry.get()) .setCapturedHeaders(ExperimentalConfig.get().getMessagingHeaders()) .setProducerSpanEnabled( - InstrumentationConfig.get() + AgentInstrumentationConfig.get() .getBoolean("otel.instrumentation.spring-integration.producer.enabled", false)) .build() .newChannelInterceptor(); diff --git a/instrumentation/spring/spring-integration-4.1/javaagent/src/test/groovy/ComplexPropagationTest.groovy b/instrumentation/spring/spring-integration-4.1/javaagent/src/test/groovy/ComplexPropagationTest.groovy deleted file mode 100644 index c5c32311b105..000000000000 --- a/instrumentation/spring/spring-integration-4.1/javaagent/src/test/groovy/ComplexPropagationTest.groovy +++ /dev/null @@ -1,13 +0,0 @@ -/* - * 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/instrumentation/spring/spring-integration-4.1/javaagent/src/test/groovy/SpringCloudStreamProducerTest.groovy b/instrumentation/spring/spring-integration-4.1/javaagent/src/test/groovy/SpringCloudStreamProducerTest.groovy deleted file mode 100644 index 07bf969f66a6..000000000000 --- a/instrumentation/spring/spring-integration-4.1/javaagent/src/test/groovy/SpringCloudStreamProducerTest.groovy +++ /dev/null @@ -1,13 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -import io.opentelemetry.instrumentation.test.AgentTestTrait - -class SpringCloudStreamProducerTest extends AbstractSpringCloudStreamProducerTest implements AgentTestTrait { - @Override - Class additionalContextClass() { - null - } -} diff --git a/instrumentation/spring/spring-integration-4.1/javaagent/src/test/groovy/SpringCloudStreamRabbitTest.groovy b/instrumentation/spring/spring-integration-4.1/javaagent/src/test/groovy/SpringCloudStreamRabbitTest.groovy deleted file mode 100644 index fa7447ea8b55..000000000000 --- a/instrumentation/spring/spring-integration-4.1/javaagent/src/test/groovy/SpringCloudStreamRabbitTest.groovy +++ /dev/null @@ -1,13 +0,0 @@ -/* - * 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/instrumentation/spring/spring-integration-4.1/javaagent/src/test/groovy/SpringIntegrationAndRabbitTest.groovy b/instrumentation/spring/spring-integration-4.1/javaagent/src/test/groovy/SpringIntegrationAndRabbitTest.groovy deleted file mode 100644 index 4df661a8b759..000000000000 --- a/instrumentation/spring/spring-integration-4.1/javaagent/src/test/groovy/SpringIntegrationAndRabbitTest.groovy +++ /dev/null @@ -1,118 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes - -import static io.opentelemetry.api.trace.SpanKind.CLIENT -import static io.opentelemetry.api.trace.SpanKind.CONSUMER -import static io.opentelemetry.api.trace.SpanKind.PRODUCER - -class SpringIntegrationAndRabbitTest extends AgentInstrumentationSpecification implements WithRabbitProducerConsumerTrait { - def setupSpec() { - startRabbit() - } - - def cleanupSpec() { - stopRabbit() - } - - def "should cooperate with existing RabbitMQ instrumentation"() { - when: - runWithSpan("parent") { - producerContext.getBean("producer", Runnable).run() - } - - then: - assertTraces(2) { - trace(0, 7) { - span(0) { - name "parent" - 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_SOCK_PEER_ADDR" { it == "127.0.0.1" || it == "0:0:0:0:0:0:0:1" || it == null } - "$SemanticAttributes.NET_SOCK_PEER_PORT" Long - "$SemanticAttributes.NET_SOCK_FAMILY" { it == SemanticAttributes.NetSockFamilyValues.INET6 || it == null } - "$SemanticAttributes.MESSAGING_SYSTEM" "rabbitmq" - } - } - span(3) { - // span created by rabbitmq instrumentation - name "testTopic send" - childOf span(1) - kind PRODUCER - attributes { - "$SemanticAttributes.NET_SOCK_PEER_ADDR" { it == "127.0.0.1" || it == "0:0:0:0:0:0:0:1" || it == null } - "$SemanticAttributes.NET_SOCK_PEER_PORT" Long - "$SemanticAttributes.NET_SOCK_FAMILY" { it == SemanticAttributes.NetSockFamilyValues.INET6 || it == null } - "$SemanticAttributes.MESSAGING_SYSTEM" "rabbitmq" - "$SemanticAttributes.MESSAGING_DESTINATION_NAME" "testTopic" - "$SemanticAttributes.MESSAGING_MESSAGE_PAYLOAD_SIZE_BYTES" Long - "$SemanticAttributes.MESSAGING_RABBITMQ_DESTINATION_ROUTING_KEY" String - } - } - // 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) - span(4) { - // span created by rabbitmq instrumentation - name ~/testTopic.anonymous.[-\w]+ process/ - childOf span(3) - kind CONSUMER - attributes { - "$SemanticAttributes.MESSAGING_SYSTEM" "rabbitmq" - "$SemanticAttributes.MESSAGING_DESTINATION_NAME" "testTopic" - "$SemanticAttributes.MESSAGING_OPERATION" "process" - "$SemanticAttributes.MESSAGING_MESSAGE_PAYLOAD_SIZE_BYTES" Long - "$SemanticAttributes.MESSAGING_RABBITMQ_DESTINATION_ROUTING_KEY" String - } - } - // spring-integration will detect that spring-rabbit has already created a consumer span and back off - span(5) { - // span created by spring-rabbit instrumentation - name "testTopic process" - childOf span(3) - kind CONSUMER - attributes { - "$SemanticAttributes.MESSAGING_SYSTEM" "rabbitmq" - "$SemanticAttributes.MESSAGING_DESTINATION_NAME" "testTopic" - "$SemanticAttributes.MESSAGING_OPERATION" "process" - "$SemanticAttributes.MESSAGING_MESSAGE_ID" String - "$SemanticAttributes.MESSAGING_MESSAGE_PAYLOAD_SIZE_BYTES" Long - } - } - 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_SOCK_PEER_ADDR" { it == "127.0.0.1" || it == "0:0:0:0:0:0:0:1" || it == null } - "$SemanticAttributes.NET_SOCK_PEER_PORT" Long - "$SemanticAttributes.NET_SOCK_FAMILY" { it == SemanticAttributes.NetSockFamilyValues.INET6 || it == null } - "$SemanticAttributes.MESSAGING_SYSTEM" "rabbitmq" - } - } - } - } - } -} diff --git a/instrumentation/spring/spring-integration-4.1/javaagent/src/test/groovy/SpringIntegrationTelemetryTest.groovy b/instrumentation/spring/spring-integration-4.1/javaagent/src/test/groovy/SpringIntegrationTelemetryTest.groovy deleted file mode 100644 index 9adea629a504..000000000000 --- a/instrumentation/spring/spring-integration-4.1/javaagent/src/test/groovy/SpringIntegrationTelemetryTest.groovy +++ /dev/null @@ -1,13 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -import io.opentelemetry.instrumentation.test.AgentTestTrait - -class SpringIntegrationTelemetryTest extends AbstractSpringIntegrationTracingTest implements AgentTestTrait { - @Override - Class additionalContextClass() { - null - } -} diff --git a/instrumentation/spring/spring-integration-4.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/integration/v4_1/ComplexPropagationTest.java b/instrumentation/spring/spring-integration-4.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/integration/v4_1/ComplexPropagationTest.java new file mode 100644 index 000000000000..27ca45e38815 --- /dev/null +++ b/instrumentation/spring/spring-integration-4.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/integration/v4_1/ComplexPropagationTest.java @@ -0,0 +1,20 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.integration.v4_1; + +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import org.junit.jupiter.api.extension.RegisterExtension; + +class ComplexPropagationTest extends AbstractComplexPropagationTest { + + @RegisterExtension + static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + ComplexPropagationTest() { + super(testing, null); + } +} diff --git a/instrumentation/spring/spring-integration-4.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/integration/v4_1/SpringCloudStreamProducerTest.java b/instrumentation/spring/spring-integration-4.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/integration/v4_1/SpringCloudStreamProducerTest.java new file mode 100644 index 000000000000..aea60a23fb7b --- /dev/null +++ b/instrumentation/spring/spring-integration-4.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/integration/v4_1/SpringCloudStreamProducerTest.java @@ -0,0 +1,20 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.integration.v4_1; + +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import org.junit.jupiter.api.extension.RegisterExtension; + +class SpringCloudStreamProducerTest extends AbstractSpringCloudStreamProducerTest { + + @RegisterExtension + static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + SpringCloudStreamProducerTest() { + super(testing, null); + } +} diff --git a/instrumentation/spring/spring-integration-4.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/integration/v4_1/SpringCloudStreamRabbitTest.java b/instrumentation/spring/spring-integration-4.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/integration/v4_1/SpringCloudStreamRabbitTest.java new file mode 100644 index 000000000000..0a48f66a0a33 --- /dev/null +++ b/instrumentation/spring/spring-integration-4.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/integration/v4_1/SpringCloudStreamRabbitTest.java @@ -0,0 +1,20 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.integration.v4_1; + +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import org.junit.jupiter.api.extension.RegisterExtension; + +class SpringCloudStreamRabbitTest extends AbstractSpringCloudStreamRabbitTest { + + @RegisterExtension + static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + SpringCloudStreamRabbitTest() { + super(testing, null); + } +} diff --git a/instrumentation/spring/spring-integration-4.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/integration/v4_1/SpringIntegrationAndRabbitTest.java b/instrumentation/spring/spring-integration-4.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/integration/v4_1/SpringIntegrationAndRabbitTest.java new file mode 100644 index 000000000000..cf1f7882ba4e --- /dev/null +++ b/instrumentation/spring/spring-integration-4.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/integration/v4_1/SpringIntegrationAndRabbitTest.java @@ -0,0 +1,155 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.integration.v4_1; + +import static io.opentelemetry.instrumentation.testing.GlobalTraceUtil.runWithSpan; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.satisfies; +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.semconv.NetworkAttributes; +import io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +class SpringIntegrationAndRabbitTest { + + @RegisterExtension RabbitExtension rabbit; + + @RegisterExtension + static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + SpringIntegrationAndRabbitTest() { + rabbit = new RabbitExtension(null); + } + + @Test + void shouldCooperateWithExistingRabbitMqInstrumentation() { + testing.waitForTraces(13); // from rabbitmq instrumentation of startup + testing.clearData(); + + runWithSpan("parent", () -> rabbit.getBean("producer", Runnable.class).run()); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasTotalAttributeCount(0), + span -> + span.hasName("producer").hasParent(trace.getSpan(0)).hasTotalAttributeCount(0), + span -> span.hasName("exchange.declare"), + span -> + span.hasName("exchange.declare") + .hasParent(trace.getSpan(1)) + .hasKind(SpanKind.CLIENT) + .hasAttributesSatisfyingExactly( + satisfies( + NetworkAttributes.NETWORK_PEER_ADDRESS, + s -> s.isIn("127.0.0.1", "0:0:0:0:0:0:0:1", null)), + satisfies( + NetworkAttributes.NETWORK_PEER_PORT, + l -> l.isInstanceOf(Long.class)), + satisfies( + NetworkAttributes.NETWORK_TYPE, s -> s.isIn("ipv4", "ipv6", null)), + equalTo(MessagingIncubatingAttributes.MESSAGING_SYSTEM, "rabbitmq")), + span -> span.hasName("queue.declare"), + span -> span.hasName("queue.bind"), + span -> + span.hasName("testTopic publish") + .hasParent(trace.getSpan(1)) + .hasKind(SpanKind.PRODUCER) + .hasAttributesSatisfyingExactly( + satisfies( + NetworkAttributes.NETWORK_PEER_ADDRESS, + s -> s.isIn("127.0.0.1", "0:0:0:0:0:0:0:1", null)), + satisfies( + NetworkAttributes.NETWORK_PEER_PORT, + l -> l.isInstanceOf(Long.class)), + satisfies( + NetworkAttributes.NETWORK_TYPE, s -> s.isIn("ipv4", "ipv6", null)), + equalTo(MessagingIncubatingAttributes.MESSAGING_SYSTEM, "rabbitmq"), + equalTo( + MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME, + "testTopic"), + equalTo(MessagingIncubatingAttributes.MESSAGING_OPERATION, "publish"), + satisfies( + MessagingIncubatingAttributes.MESSAGING_MESSAGE_BODY_SIZE, + l -> l.isInstanceOf(Long.class)), + satisfies( + MessagingIncubatingAttributes + .MESSAGING_RABBITMQ_DESTINATION_ROUTING_KEY, + s -> s.isInstanceOf(String.class))), + // 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) + span -> + span.satisfies( + spanData -> + assertThat(spanData.getName()) + .matches("testTopic.anonymous.[-\\w]+ process")) + .hasParent(trace.getSpan(6)) + .hasKind(SpanKind.CONSUMER) + .hasAttributesSatisfyingExactly( + satisfies( + NetworkAttributes.NETWORK_PEER_ADDRESS, + s -> s.isIn("127.0.0.1", "0:0:0:0:0:0:0:1", null)), + satisfies( + NetworkAttributes.NETWORK_PEER_PORT, + l -> l.isInstanceOf(Long.class)), + satisfies( + NetworkAttributes.NETWORK_TYPE, s -> s.isIn("ipv4", "ipv6", null)), + equalTo(MessagingIncubatingAttributes.MESSAGING_SYSTEM, "rabbitmq"), + equalTo( + MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME, + "testTopic"), + equalTo(MessagingIncubatingAttributes.MESSAGING_OPERATION, "process"), + satisfies( + MessagingIncubatingAttributes.MESSAGING_MESSAGE_BODY_SIZE, + l -> l.isInstanceOf(Long.class)), + satisfies( + MessagingIncubatingAttributes + .MESSAGING_RABBITMQ_DESTINATION_ROUTING_KEY, + s -> s.isInstanceOf(String.class))), + // spring-integration will detect that spring-rabbit has already created a consumer + // span and back off + span -> + span.hasName("testTopic process") + .hasParent(trace.getSpan(6)) + .hasKind(SpanKind.CONSUMER) + .hasAttributesSatisfyingExactly( + equalTo(MessagingIncubatingAttributes.MESSAGING_SYSTEM, "rabbitmq"), + equalTo( + MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME, + "testTopic"), + equalTo(MessagingIncubatingAttributes.MESSAGING_OPERATION, "process"), + satisfies( + MessagingIncubatingAttributes.MESSAGING_MESSAGE_ID, + s -> s.isInstanceOf(String.class)), + satisfies( + MessagingIncubatingAttributes.MESSAGING_MESSAGE_BODY_SIZE, + l -> l.isInstanceOf(Long.class))), + span -> + span.hasName("consumer").hasParent(trace.getSpan(8)).hasTotalAttributeCount(0)), + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("basic.ack") + .hasKind(SpanKind.CLIENT) + .hasAttributesSatisfyingExactly( + satisfies( + NetworkAttributes.NETWORK_PEER_ADDRESS, + s -> s.isIn("127.0.0.1", "0:0:0:0:0:0:0:1", null)), + satisfies( + NetworkAttributes.NETWORK_PEER_PORT, + l -> l.isInstanceOf(Long.class)), + satisfies( + NetworkAttributes.NETWORK_TYPE, s -> s.isIn("ipv4", "ipv6", null)), + equalTo(MessagingIncubatingAttributes.MESSAGING_SYSTEM, "rabbitmq")))); + } +} diff --git a/instrumentation/spring/spring-integration-4.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/integration/v4_1/SpringIntegrationTelemetryTest.java b/instrumentation/spring/spring-integration-4.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/integration/v4_1/SpringIntegrationTelemetryTest.java new file mode 100644 index 000000000000..2c4f50424ead --- /dev/null +++ b/instrumentation/spring/spring-integration-4.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/integration/v4_1/SpringIntegrationTelemetryTest.java @@ -0,0 +1,20 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.integration.v4_1; + +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import org.junit.jupiter.api.extension.RegisterExtension; + +class SpringIntegrationTelemetryTest extends AbstractSpringIntegrationTracingTest { + + @RegisterExtension + static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + SpringIntegrationTelemetryTest() { + super(testing, null); + } +} diff --git a/instrumentation/spring/spring-integration-4.1/library/build.gradle.kts b/instrumentation/spring/spring-integration-4.1/library/build.gradle.kts index 12cc81a29502..37a6a07138e1 100644 --- a/instrumentation/spring/spring-integration-4.1/library/build.gradle.kts +++ b/instrumentation/spring/spring-integration-4.1/library/build.gradle.kts @@ -36,3 +36,7 @@ configurations.testRuntimeClasspath { force("org.slf4j:slf4j-api:1.7.36") } } + +tasks.withType().configureEach { + jvmArgs("-Dotel.instrumentation.common.experimental.controller-telemetry.enabled=true") +} diff --git a/instrumentation/spring/spring-integration-4.1/library/src/main/java/io/opentelemetry/instrumentation/spring/integration/v4_1/SpringIntegrationTelemetryBuilder.java b/instrumentation/spring/spring-integration-4.1/library/src/main/java/io/opentelemetry/instrumentation/spring/integration/v4_1/SpringIntegrationTelemetryBuilder.java index 2d95cce3b6c0..c4a31384e375 100644 --- a/instrumentation/spring/spring-integration-4.1/library/src/main/java/io/opentelemetry/instrumentation/spring/integration/v4_1/SpringIntegrationTelemetryBuilder.java +++ b/instrumentation/spring/spring-integration-4.1/library/src/main/java/io/opentelemetry/instrumentation/spring/integration/v4_1/SpringIntegrationTelemetryBuilder.java @@ -10,12 +10,12 @@ import com.google.errorprone.annotations.CanIgnoreReturnValue; import io.opentelemetry.api.OpenTelemetry; import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.api.incubator.semconv.messaging.MessageOperation; +import io.opentelemetry.instrumentation.api.incubator.semconv.messaging.MessagingAttributesExtractor; +import io.opentelemetry.instrumentation.api.incubator.semconv.messaging.MessagingAttributesGetter; 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.messaging.MessageOperation; -import io.opentelemetry.instrumentation.api.instrumenter.messaging.MessagingAttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.messaging.MessagingAttributesGetter; import java.util.ArrayList; import java.util.List; @@ -71,7 +71,7 @@ private static String consumerSpanName(MessageWithChannel messageWithChannel) { } private static String producerSpanName(MessageWithChannel messageWithChannel) { - return messageWithChannel.getChannelName() + " send"; + return messageWithChannel.getChannelName() + " publish"; } /** @@ -101,7 +101,7 @@ public SpringIntegrationTelemetry build() { .addAttributesExtractor( buildMessagingAttributesExtractor( SpringMessagingAttributesGetter.INSTANCE, - MessageOperation.SEND, + MessageOperation.PUBLISH, capturedHeaders)) .buildInstrumenter(SpanKindExtractor.alwaysProducer()); return new SpringIntegrationTelemetry( diff --git a/instrumentation/spring/spring-integration-4.1/library/src/main/java/io/opentelemetry/instrumentation/spring/integration/v4_1/SpringMessagingAttributesGetter.java b/instrumentation/spring/spring-integration-4.1/library/src/main/java/io/opentelemetry/instrumentation/spring/integration/v4_1/SpringMessagingAttributesGetter.java index 1632b31f121e..47c515fe9dc8 100644 --- a/instrumentation/spring/spring-integration-4.1/library/src/main/java/io/opentelemetry/instrumentation/spring/integration/v4_1/SpringMessagingAttributesGetter.java +++ b/instrumentation/spring/spring-integration-4.1/library/src/main/java/io/opentelemetry/instrumentation/spring/integration/v4_1/SpringMessagingAttributesGetter.java @@ -5,7 +5,7 @@ package io.opentelemetry.instrumentation.spring.integration.v4_1; -import io.opentelemetry.instrumentation.api.instrumenter.messaging.MessagingAttributesGetter; +import io.opentelemetry.instrumentation.api.incubator.semconv.messaging.MessagingAttributesGetter; import java.util.Collections; import java.util.List; import javax.annotation.Nullable; @@ -27,32 +27,55 @@ public String getDestination(MessageWithChannel messageWithChannel) { return null; } + @Nullable + @Override + public String getDestinationTemplate(MessageWithChannel messageWithChannel) { + return null; + } + @Override public boolean isTemporaryDestination(MessageWithChannel messageWithChannel) { return false; } + @Override + public boolean isAnonymousDestination(MessageWithChannel messageWithChannel) { + return false; + } + @Override @Nullable public String getConversationId(MessageWithChannel messageWithChannel) { return null; } + @Nullable @Override + public Long getMessageBodySize(MessageWithChannel messageWithChannel) { + return null; + } + @Nullable - public Long getMessagePayloadSize(MessageWithChannel messageWithChannel) { + @Override + public Long getMessageEnvelopeSize(MessageWithChannel messageWithChannel) { return null; } @Override @Nullable - public Long getMessagePayloadCompressedSize(MessageWithChannel messageWithChannel) { + public String getMessageId(MessageWithChannel messageWithChannel, @Nullable Void unused) { return null; } + @Nullable @Override + public String getClientId(MessageWithChannel messageWithChannel) { + return null; + } + @Nullable - public String getMessageId(MessageWithChannel messageWithChannel, @Nullable Void unused) { + @Override + public Long getBatchMessageCount(MessageWithChannel messageWithChannel, @Nullable Void unused) { return null; } diff --git a/instrumentation/spring/spring-integration-4.1/library/src/test/groovy/ComplexPropagationTest.groovy b/instrumentation/spring/spring-integration-4.1/library/src/test/groovy/ComplexPropagationTest.groovy deleted file mode 100644 index ff40c296cf51..000000000000 --- a/instrumentation/spring/spring-integration-4.1/library/src/test/groovy/ComplexPropagationTest.groovy +++ /dev/null @@ -1,13 +0,0 @@ -/* - * 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/instrumentation/spring/spring-integration-4.1/library/src/test/groovy/GlobalInterceptorSpringConfig.groovy b/instrumentation/spring/spring-integration-4.1/library/src/test/groovy/GlobalInterceptorSpringConfig.groovy deleted file mode 100644 index a73da27e7bc5..000000000000 --- a/instrumentation/spring/spring-integration-4.1/library/src/test/groovy/GlobalInterceptorSpringConfig.groovy +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -import io.opentelemetry.api.GlobalOpenTelemetry -import io.opentelemetry.instrumentation.spring.integration.v4_1.SpringIntegrationTelemetry -import org.springframework.context.annotation.Bean -import org.springframework.context.annotation.Configuration -import org.springframework.integration.config.GlobalChannelInterceptor -import org.springframework.messaging.support.ChannelInterceptor - -import static java.util.Collections.singletonList - -@Configuration -class GlobalInterceptorSpringConfig { - - @GlobalChannelInterceptor - @Bean - ChannelInterceptor otelInterceptor() { - SpringIntegrationTelemetry.builder(GlobalOpenTelemetry.get()) - .setCapturedHeaders(singletonList("test-message-header")) - .build() - .newChannelInterceptor() - } -} diff --git a/instrumentation/spring/spring-integration-4.1/library/src/test/groovy/GlobalInterceptorWithProducerSpanSpringConfig.groovy b/instrumentation/spring/spring-integration-4.1/library/src/test/groovy/GlobalInterceptorWithProducerSpanSpringConfig.groovy deleted file mode 100644 index 92a18d24804e..000000000000 --- a/instrumentation/spring/spring-integration-4.1/library/src/test/groovy/GlobalInterceptorWithProducerSpanSpringConfig.groovy +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -import io.opentelemetry.api.GlobalOpenTelemetry -import io.opentelemetry.instrumentation.spring.integration.v4_1.SpringIntegrationTelemetry -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 GlobalInterceptorWithProducerSpanSpringConfig { - - @GlobalChannelInterceptor - @Bean - ChannelInterceptor otelInterceptor() { - SpringIntegrationTelemetry.builder(GlobalOpenTelemetry.get()) - .setProducerSpanEnabled(true) - .build() - .newChannelInterceptor() - } -} diff --git a/instrumentation/spring/spring-integration-4.1/library/src/test/groovy/SpringCloudStreamProducerTest.groovy b/instrumentation/spring/spring-integration-4.1/library/src/test/groovy/SpringCloudStreamProducerTest.groovy deleted file mode 100644 index 33427f9196b8..000000000000 --- a/instrumentation/spring/spring-integration-4.1/library/src/test/groovy/SpringCloudStreamProducerTest.groovy +++ /dev/null @@ -1,13 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -import io.opentelemetry.instrumentation.test.LibraryTestTrait - -class SpringCloudStreamProducerTest extends AbstractSpringCloudStreamProducerTest implements LibraryTestTrait { - @Override - Class additionalContextClass() { - GlobalInterceptorWithProducerSpanSpringConfig - } -} diff --git a/instrumentation/spring/spring-integration-4.1/library/src/test/groovy/SpringCloudStreamRabbitTest.groovy b/instrumentation/spring/spring-integration-4.1/library/src/test/groovy/SpringCloudStreamRabbitTest.groovy deleted file mode 100644 index e9d659f21778..000000000000 --- a/instrumentation/spring/spring-integration-4.1/library/src/test/groovy/SpringCloudStreamRabbitTest.groovy +++ /dev/null @@ -1,13 +0,0 @@ -/* - * 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/instrumentation/spring/spring-integration-4.1/library/src/test/groovy/SpringIntegrationTelemetryTest.groovy b/instrumentation/spring/spring-integration-4.1/library/src/test/groovy/SpringIntegrationTelemetryTest.groovy deleted file mode 100644 index b81fbcf40231..000000000000 --- a/instrumentation/spring/spring-integration-4.1/library/src/test/groovy/SpringIntegrationTelemetryTest.groovy +++ /dev/null @@ -1,13 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -import io.opentelemetry.instrumentation.test.LibraryTestTrait - -class SpringIntegrationTelemetryTest extends AbstractSpringIntegrationTracingTest implements LibraryTestTrait { - @Override - Class additionalContextClass() { - GlobalInterceptorSpringConfig - } -} diff --git a/instrumentation/spring/spring-integration-4.1/library/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/integration/v4_1/ComplexPropagationTest.java b/instrumentation/spring/spring-integration-4.1/library/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/integration/v4_1/ComplexPropagationTest.java new file mode 100644 index 000000000000..52bf2c4807f3 --- /dev/null +++ b/instrumentation/spring/spring-integration-4.1/library/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/integration/v4_1/ComplexPropagationTest.java @@ -0,0 +1,20 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.integration.v4_1; + +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.LibraryInstrumentationExtension; +import org.junit.jupiter.api.extension.RegisterExtension; + +class ComplexPropagationTest extends AbstractComplexPropagationTest { + + @RegisterExtension + static final InstrumentationExtension testing = LibraryInstrumentationExtension.create(); + + public ComplexPropagationTest() { + super(testing, GlobalInterceptorSpringConfig.class); + } +} diff --git a/instrumentation/spring/spring-integration-4.1/library/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/integration/v4_1/GlobalInterceptorSpringConfig.java b/instrumentation/spring/spring-integration-4.1/library/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/integration/v4_1/GlobalInterceptorSpringConfig.java new file mode 100644 index 000000000000..49e7c8c04e22 --- /dev/null +++ b/instrumentation/spring/spring-integration-4.1/library/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/integration/v4_1/GlobalInterceptorSpringConfig.java @@ -0,0 +1,28 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.integration.v4_1; + +import static java.util.Collections.singletonList; + +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.instrumentation.spring.integration.v4_1.SpringIntegrationTelemetry; +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() { + return SpringIntegrationTelemetry.builder(GlobalOpenTelemetry.get()) + .setCapturedHeaders(singletonList("test-message-header")) + .build() + .newChannelInterceptor(); + } +} diff --git a/instrumentation/spring/spring-integration-4.1/library/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/integration/v4_1/GlobalInterceptorWithProducerSpanSpringConfig.java b/instrumentation/spring/spring-integration-4.1/library/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/integration/v4_1/GlobalInterceptorWithProducerSpanSpringConfig.java new file mode 100644 index 000000000000..68784dcec635 --- /dev/null +++ b/instrumentation/spring/spring-integration-4.1/library/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/integration/v4_1/GlobalInterceptorWithProducerSpanSpringConfig.java @@ -0,0 +1,26 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.integration.v4_1; + +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.instrumentation.spring.integration.v4_1.SpringIntegrationTelemetry; +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 GlobalInterceptorWithProducerSpanSpringConfig { + + @GlobalChannelInterceptor + @Bean + ChannelInterceptor otelInterceptor() { + return SpringIntegrationTelemetry.builder(GlobalOpenTelemetry.get()) + .setProducerSpanEnabled(true) + .build() + .newChannelInterceptor(); + } +} diff --git a/instrumentation/spring/spring-integration-4.1/library/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/integration/v4_1/SpringCloudStreamProducerTest.java b/instrumentation/spring/spring-integration-4.1/library/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/integration/v4_1/SpringCloudStreamProducerTest.java new file mode 100644 index 000000000000..b2f25b143045 --- /dev/null +++ b/instrumentation/spring/spring-integration-4.1/library/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/integration/v4_1/SpringCloudStreamProducerTest.java @@ -0,0 +1,19 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.integration.v4_1; + +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.LibraryInstrumentationExtension; +import org.junit.jupiter.api.extension.RegisterExtension; + +class SpringCloudStreamProducerTest extends AbstractSpringCloudStreamProducerTest { + @RegisterExtension + static final InstrumentationExtension testing = LibraryInstrumentationExtension.create(); + + public SpringCloudStreamProducerTest() { + super(testing, GlobalInterceptorWithProducerSpanSpringConfig.class); + } +} diff --git a/instrumentation/spring/spring-integration-4.1/library/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/integration/v4_1/SpringCloudStreamRabbitTest.java b/instrumentation/spring/spring-integration-4.1/library/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/integration/v4_1/SpringCloudStreamRabbitTest.java new file mode 100644 index 000000000000..da78855adde3 --- /dev/null +++ b/instrumentation/spring/spring-integration-4.1/library/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/integration/v4_1/SpringCloudStreamRabbitTest.java @@ -0,0 +1,19 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.integration.v4_1; + +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.LibraryInstrumentationExtension; +import org.junit.jupiter.api.extension.RegisterExtension; + +class SpringCloudStreamRabbitTest extends AbstractSpringCloudStreamRabbitTest { + @RegisterExtension + static final InstrumentationExtension testing = LibraryInstrumentationExtension.create(); + + public SpringCloudStreamRabbitTest() { + super(testing, GlobalInterceptorSpringConfig.class); + } +} diff --git a/instrumentation/spring/spring-integration-4.1/library/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/integration/v4_1/SpringIntegrationTelemetryTest.java b/instrumentation/spring/spring-integration-4.1/library/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/integration/v4_1/SpringIntegrationTelemetryTest.java new file mode 100644 index 000000000000..f8b3dda8f264 --- /dev/null +++ b/instrumentation/spring/spring-integration-4.1/library/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/integration/v4_1/SpringIntegrationTelemetryTest.java @@ -0,0 +1,19 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.integration.v4_1; + +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.LibraryInstrumentationExtension; +import org.junit.jupiter.api.extension.RegisterExtension; + +class SpringIntegrationTelemetryTest extends AbstractSpringIntegrationTracingTest { + @RegisterExtension + static final InstrumentationExtension testing = LibraryInstrumentationExtension.create(); + + public SpringIntegrationTelemetryTest() { + super(testing, GlobalInterceptorSpringConfig.class); + } +} diff --git a/instrumentation/spring/spring-integration-4.1/testing/src/main/groovy/AbstractComplexPropagationTest.groovy b/instrumentation/spring/spring-integration-4.1/testing/src/main/groovy/AbstractComplexPropagationTest.groovy deleted file mode 100644 index 0d1ce31d9a9f..000000000000 --- a/instrumentation/spring/spring-integration-4.1/testing/src/main/groovy/AbstractComplexPropagationTest.groovy +++ /dev/null @@ -1,151 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -import io.opentelemetry.instrumentation.test.InstrumentationSpecification -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 - -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 static io.opentelemetry.api.trace.SpanKind.CONSUMER - -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: - sendChannel.send(MessageBuilder.withPayload("test") - .setHeader("theAnswer", "42") - .build()) - - then: - messageHandler.join() - - assertTraces(1) { - trace(0, 3) { - // there's no span in the context, so spring-integration adds a CONSUMER one - span(0) { - name "application.sendChannel process" - 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(1) { - name "application.receiveChannel process" - childOf span(0) - kind CONSUMER - } - span(2) { - name "handler" - childOf span(1) - } - } - } - - 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(destroyMethod = "shutdownNow") - 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/instrumentation/spring/spring-integration-4.1/testing/src/main/groovy/AbstractSpringCloudStreamProducerTest.groovy b/instrumentation/spring/spring-integration-4.1/testing/src/main/groovy/AbstractSpringCloudStreamProducerTest.groovy deleted file mode 100644 index bee9853d2f0c..000000000000 --- a/instrumentation/spring/spring-integration-4.1/testing/src/main/groovy/AbstractSpringCloudStreamProducerTest.groovy +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -import io.opentelemetry.instrumentation.test.InstrumentationSpecification - -import static io.opentelemetry.api.trace.SpanKind.CONSUMER -import static io.opentelemetry.api.trace.SpanKind.PRODUCER -import static org.junit.jupiter.api.Assumptions.assumeTrue - -abstract class AbstractSpringCloudStreamProducerTest extends InstrumentationSpecification implements WithRabbitProducerConsumerTrait { - private static final boolean HAS_PRODUCER_SPAN = Boolean.getBoolean("otel.instrumentation.spring-integration.producer.enabled") - - abstract Class additionalContextClass() - - def setupSpec() { - startRabbit(additionalContextClass()) - } - - def cleanupSpec() { - stopRabbit() - } - - def "has producer span"() { - assumeTrue(HAS_PRODUCER_SPAN) - - when: - producerContext.getBean("producer", Runnable).run() - - then: - assertTraces(1) { - trace(0, 4) { - span(0) { - name "producer" - } - span(1) { - name "testProducer.output send" - childOf span(0) - kind PRODUCER - } - span(2) { - name "testConsumer.input process" - childOf span(1) - kind CONSUMER - } - span(3) { - name "consumer" - childOf span(2) - } - } - } - } -} diff --git a/instrumentation/spring/spring-integration-4.1/testing/src/main/groovy/AbstractSpringCloudStreamRabbitTest.groovy b/instrumentation/spring/spring-integration-4.1/testing/src/main/groovy/AbstractSpringCloudStreamRabbitTest.groovy deleted file mode 100644 index 577a35a65691..000000000000 --- a/instrumentation/spring/spring-integration-4.1/testing/src/main/groovy/AbstractSpringCloudStreamRabbitTest.groovy +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -import io.opentelemetry.instrumentation.test.InstrumentationSpecification - -import static io.opentelemetry.api.trace.SpanKind.CONSUMER - -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, 3) { - span(0) { - name "producer" - } - span(1) { - name "testConsumer.input process" - childOf span(0) - kind CONSUMER - } - span(2) { - name "consumer" - childOf span(1) - } - } - } - } -} diff --git a/instrumentation/spring/spring-integration-4.1/testing/src/main/groovy/AbstractSpringIntegrationTracingTest.groovy b/instrumentation/spring/spring-integration-4.1/testing/src/main/groovy/AbstractSpringIntegrationTracingTest.groovy deleted file mode 100644 index 92f26657e2ae..000000000000 --- a/instrumentation/spring/spring-integration-4.1/testing/src/main/groovy/AbstractSpringIntegrationTracingTest.groovy +++ /dev/null @@ -1,284 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -import io.opentelemetry.instrumentation.test.InstrumentationSpecification -import io.opentelemetry.sdk.trace.data.SpanData -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 - -import java.util.concurrent.Executors - -import static io.opentelemetry.api.trace.SpanKind.CONSUMER - -@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: - channel.send(MessageBuilder.withPayload("test") - .build()) - - then: - def capturedMessage = messageHandler.join() - - assertTraces(1) { - trace(0, 2) { - span(0) { - name interceptorSpanName - kind CONSUMER - } - span(1) { - name "handler" - childOf span(0) - } - - def interceptorSpan = span(0) - verifyCorrectSpanWasPropagated(capturedMessage, interceptorSpan) - } - } - - cleanup: - channel.unsubscribe(messageHandler) - - where: - channelName | interceptorSpanName - "directChannel" | "application.directChannel process" - "executorChannel" | "executorChannel process" - } - - def "should not add interceptor twice"() { - given: - def channel = applicationContext.getBean("directChannel1", SubscribableChannel) - - def messageHandler = new CapturingMessageHandler() - channel.subscribe(messageHandler) - - when: - channel.send(MessageBuilder.withPayload("test") - .build()) - - then: - def capturedMessage = messageHandler.join() - - assertTraces(1) { - trace(0, 2) { - span(0) { - // the channel name is overwritten by the last bean registration - name "application.directChannel2 process" - kind CONSUMER - } - span(1) { - name "handler" - childOf span(0) - } - - def interceptorSpan = span(0) - verifyCorrectSpanWasPropagated(capturedMessage, interceptorSpan) - } - } - - cleanup: - channel.unsubscribe(messageHandler) - } - - def "should not create a span when there is already a span in the context"() { - given: - def channel = applicationContext.getBean("directChannel", SubscribableChannel) - - def messageHandler = new CapturingMessageHandler() - channel.subscribe(messageHandler) - - when: - runWithSpan("parent") { - channel.send(MessageBuilder.withPayload("test") - .build()) - } - - then: - messageHandler.join() - - assertTraces(1) { - trace(0, 2) { - span(0) { - name "parent" - } - span(1) { - name "handler" - childOf span(0) - } - } - } - - cleanup: - channel.unsubscribe(messageHandler) - } - - 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: - channel1.send(MessageBuilder.withPayload("test") - .build()) - - then: - def capturedMessage = messageHandler.join() - - assertTraces(1) { - trace(0, 2) { - span(0) { - name "application.linkedChannel1 process" - kind CONSUMER - } - span(1) { - name "handler" - childOf span(0) - } - - def lastChannelSpan = span(0) - verifyCorrectSpanWasPropagated(capturedMessage, lastChannelSpan) - } - } - - cleanup: - channel2.unsubscribe(messageHandler) - } - - def "capture message header"() { - given: - def channel = applicationContext.getBean("directChannel", SubscribableChannel) - - def messageHandler = new CapturingMessageHandler() - channel.subscribe(messageHandler) - - when: - channel.send(MessageBuilder.withPayload("test") - .setHeader("test-message-header", "test") - .build()) - - then: - def capturedMessage = messageHandler.join() - - assertTraces(1) { - trace(0, 2) { - span(0) { - name "application.directChannel process" - kind CONSUMER - } - span(1) { - name "handler" - childOf span(0) - } - - def interceptorSpan = span(0) - verifyCorrectSpanWasPropagated(capturedMessage, interceptorSpan) - } - } - - cleanup: - channel.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 { - - SubscribableChannel problematicSharedChannel = new DirectChannel() - - @Bean - SubscribableChannel directChannel() { - new DirectChannel() - } - - @Bean - SubscribableChannel directChannel1() { - problematicSharedChannel - } - - @Bean - SubscribableChannel directChannel2() { - problematicSharedChannel - } - - @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/instrumentation/spring/spring-integration-4.1/testing/src/main/groovy/CapturingMessageHandler.groovy b/instrumentation/spring/spring-integration-4.1/testing/src/main/groovy/CapturingMessageHandler.groovy deleted file mode 100644 index 5e5985853d38..000000000000 --- a/instrumentation/spring/spring-integration-4.1/testing/src/main/groovy/CapturingMessageHandler.groovy +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -import org.springframework.messaging.Message -import org.springframework.messaging.MessageHandler -import org.springframework.messaging.MessagingException - -import java.util.concurrent.CompletableFuture - -import static io.opentelemetry.instrumentation.testing.GlobalTraceUtil.runWithSpan - -class CapturingMessageHandler implements MessageHandler { - final CompletableFuture> captured = new CompletableFuture<>() - - @Override - void handleMessage(Message message) throws MessagingException { - runWithSpan("handler") { - captured.complete(message) - } - } - - Message join() { - captured.join() - } -} diff --git a/instrumentation/spring/spring-integration-4.1/testing/src/main/groovy/WithRabbitProducerConsumerTrait.groovy b/instrumentation/spring/spring-integration-4.1/testing/src/main/groovy/WithRabbitProducerConsumerTrait.groovy deleted file mode 100644 index ef72c579d3d4..000000000000 --- a/instrumentation/spring/spring-integration-4.1/testing/src/main/groovy/WithRabbitProducerConsumerTrait.groovy +++ /dev/null @@ -1,103 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -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 -import org.testcontainers.containers.wait.strategy.Wait - -import java.time.Duration - -import static io.opentelemetry.instrumentation.testing.GlobalTraceUtil.runWithSpan - -trait WithRabbitProducerConsumerTrait { - - static GenericContainer rabbitMqContainer - static ConfigurableApplicationContext producerContext - static ConfigurableApplicationContext consumerContext - - def startRabbit(Class additionalContext = null) { - rabbitMqContainer = new GenericContainer('rabbitmq:latest') - .withExposedPorts(5672) - .waitingFor(Wait.forLogMessage(".*Server startup complete.*", 1)) - .withStartupTimeout(Duration.ofMinutes(2)) - 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.host, - "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.host, - "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 { - runWithSpan("producer") { - source.output().send(MessageBuilder.withPayload("test").build()) - } - } - } - } - - @SpringBootConfiguration - @EnableAutoConfiguration - @EnableBinding(Sink) - static class ConsumerConfig { - @StreamListener(Sink.INPUT) - void consume(String ignored) { - runWithSpan("consumer") {} - } - } -} diff --git a/instrumentation/spring/spring-integration-4.1/testing/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/integration/v4_1/AbstractComplexPropagationTest.java b/instrumentation/spring/spring-integration-4.1/testing/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/integration/v4_1/AbstractComplexPropagationTest.java new file mode 100644 index 000000000000..007ec552ff45 --- /dev/null +++ b/instrumentation/spring/spring-integration-4.1/testing/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/integration/v4_1/AbstractComplexPropagationTest.java @@ -0,0 +1,162 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.integration.v4_1; + +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +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 javax.annotation.Nullable; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +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.integration.support.MessageBuilder; +import org.springframework.messaging.Message; +import org.springframework.messaging.SubscribableChannel; + +public abstract class AbstractComplexPropagationTest { + + private final Class additionalContextClass; + protected InstrumentationExtension testing; + + ConfigurableApplicationContext applicationContext; + + public AbstractComplexPropagationTest( + InstrumentationExtension testing, @Nullable Class additionalContextClass) { + this.testing = testing; + this.additionalContextClass = additionalContextClass; + } + + @BeforeEach + void setUp() { + List> contextClasses = new ArrayList<>(); + contextClasses.add(ExternalQueueConfig.class); + if (additionalContextClass != null) { + contextClasses.add(additionalContextClass); + } + SpringApplication springApplication = + new SpringApplication(contextClasses.toArray(new Class[0])); + springApplication.setDefaultProperties( + Collections.singletonMap("spring.main.web-application-type", "none")); + applicationContext = springApplication.run(); + } + + @AfterEach + void tearDown() { + if (applicationContext != null) { + applicationContext.close(); + } + } + + @Test + void shouldPropagateContextThroughAcomplexFlow() { + SubscribableChannel sendChannel = + applicationContext.getBean("sendChannel", SubscribableChannel.class); + SubscribableChannel receiveChannel = + applicationContext.getBean("receiveChannel", SubscribableChannel.class); + + CapturingMessageHandler messageHandler = new CapturingMessageHandler(); + receiveChannel.subscribe(messageHandler); + + sendChannel.send(MessageBuilder.withPayload("test").setHeader("theAnswer", "42").build()); + + messageHandler.join(); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("application.sendChannel process").hasKind(SpanKind.CONSUMER), + span -> + span.hasName("application.receiveChannel process") + .hasParent(trace.getSpan(0)) + .hasKind(SpanKind.CONSUMER), + span -> span.hasName("handler").hasParent(trace.getSpan(1)))); + + receiveChannel.unsubscribe(messageHandler); + } + + // this setup simulates separate producer/consumer and some "external" message queue in between + @SpringBootConfiguration + @EnableAutoConfiguration + static class ExternalQueueConfig { + @Bean + SubscribableChannel sendChannel() { + return new ExecutorChannel(Executors.newSingleThreadExecutor()); + } + + @Bean + SubscribableChannel receiveChannel() { + return new DirectChannel(); + } + + @Bean + BlockingQueue externalQueue() { + return new LinkedBlockingQueue<>(); + } + + @Bean(destroyMethod = "shutdownNow") + ExecutorService consumerThread() { + return Executors.newSingleThreadExecutor(); + } + + @EventListener(ApplicationReadyEvent.class) + void initialize() { + sendChannel().subscribe(message -> externalQueue().offer(Payload.from(message))); + + consumerThread() + .execute( + () -> { + while (!Thread.interrupted()) { + try { + Payload payload = externalQueue().take(); + receiveChannel().send(payload.toMessage()); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + }); + } + } + + static class Payload { + String body; + Map headers; + + Payload(String body, Map headers) { + this.body = body; + this.headers = headers; + } + + static Payload from(Message message) { + String body = (String) message.getPayload(); + Map headers = + message.getHeaders().entrySet().stream() + .filter(kv -> kv.getValue() instanceof String) + .collect(Collectors.toMap(Map.Entry::getKey, kv -> (String) kv.getValue())); + return new Payload(body, headers); + } + + Message toMessage() { + return MessageBuilder.withPayload(body).copyHeaders(headers).build(); + } + } +} diff --git a/instrumentation/spring/spring-integration-4.1/testing/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/integration/v4_1/AbstractSpringCloudStreamProducerTest.java b/instrumentation/spring/spring-integration-4.1/testing/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/integration/v4_1/AbstractSpringCloudStreamProducerTest.java new file mode 100644 index 000000000000..e0c24f5c69b2 --- /dev/null +++ b/instrumentation/spring/spring-integration-4.1/testing/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/integration/v4_1/AbstractSpringCloudStreamProducerTest.java @@ -0,0 +1,53 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.integration.v4_1; + +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +public abstract class AbstractSpringCloudStreamProducerTest { + + @RegisterExtension RabbitExtension rabbit; + + protected final InstrumentationExtension testing; + + private static final boolean HAS_PRODUCER_SPAN = + Boolean.getBoolean("otel.instrumentation.spring-integration.producer.enabled"); + + public AbstractSpringCloudStreamProducerTest( + InstrumentationExtension testing, Class additionalContextClass) { + this.testing = testing; + rabbit = new RabbitExtension(additionalContextClass); + } + + @Test + void hasProducerSpan() { + assumeTrue(HAS_PRODUCER_SPAN); + + rabbit.getBean("producer", Runnable.class).run(); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("producer").hasKind(SpanKind.INTERNAL), + span -> + span.hasName("testProducer.output publish") + .hasKind(SpanKind.PRODUCER) + .hasParent(trace.getSpan(0)), + span -> + span.hasName("testConsumer.input process") + .hasKind(SpanKind.CONSUMER) + .hasParent(trace.getSpan(1)), + span -> + span.hasName("consumer") + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(2)))); + } +} diff --git a/instrumentation/spring/spring-integration-4.1/testing/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/integration/v4_1/AbstractSpringCloudStreamRabbitTest.java b/instrumentation/spring/spring-integration-4.1/testing/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/integration/v4_1/AbstractSpringCloudStreamRabbitTest.java new file mode 100644 index 000000000000..5020e8b7abb8 --- /dev/null +++ b/instrumentation/spring/spring-integration-4.1/testing/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/integration/v4_1/AbstractSpringCloudStreamRabbitTest.java @@ -0,0 +1,42 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.integration.v4_1; + +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +public abstract class AbstractSpringCloudStreamRabbitTest { + + @RegisterExtension RabbitExtension rabbit; + + protected final InstrumentationExtension testing; + + public AbstractSpringCloudStreamRabbitTest( + InstrumentationExtension testing, Class additionalContextClass) { + this.testing = testing; + rabbit = new RabbitExtension(additionalContextClass); + } + + @Test + void shouldPropagateContextThroughRabbitMq() { + rabbit.getBean("producer", Runnable.class).run(); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("producer").hasKind(SpanKind.INTERNAL), + span -> + span.hasName("testConsumer.input process") + .hasKind(SpanKind.CONSUMER) + .hasParent(trace.getSpan(0)), + span -> + span.hasName("consumer") + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(1)))); + } +} diff --git a/instrumentation/spring/spring-integration-4.1/testing/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/integration/v4_1/AbstractSpringIntegrationTracingTest.java b/instrumentation/spring/spring-integration-4.1/testing/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/integration/v4_1/AbstractSpringIntegrationTracingTest.java new file mode 100644 index 000000000000..3bc3ae5a5eec --- /dev/null +++ b/instrumentation/spring/spring-integration-4.1/testing/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/integration/v4_1/AbstractSpringIntegrationTracingTest.java @@ -0,0 +1,257 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.integration.v4_1; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.sdk.trace.data.SpanData; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.Executors; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +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; + +abstract class AbstractSpringIntegrationTracingTest { + + protected final InstrumentationExtension testing; + + private final Class additionalContextClass; + + ConfigurableApplicationContext applicationContext; + + public AbstractSpringIntegrationTracingTest( + InstrumentationExtension testing, Class additionalContextClass) { + this.testing = testing; + this.additionalContextClass = additionalContextClass; + } + + @BeforeEach + public void setUp() { + List> contextClasses = new ArrayList<>(); + contextClasses.add(MessageChannelsConfig.class); + if (additionalContextClass != null) { + contextClasses.add(additionalContextClass); + } + SpringApplication springApplication = + new SpringApplication(contextClasses.toArray(new Class[0])); + springApplication.setDefaultProperties( + Collections.singletonMap("spring.main.web-application-type", "none")); + applicationContext = springApplication.run(); + } + + @AfterEach + public void tearDown() { + if (applicationContext != null) { + applicationContext.close(); + } + } + + @ParameterizedTest + @CsvSource( + value = { + "directChannel,application.directChannel process", + "executorChannel,executorChannel process" + }, + delimiter = ',') + public void shouldPropagateContext(String channelName, String interceptorSpanName) { + SubscribableChannel channel = + applicationContext.getBean(channelName, SubscribableChannel.class); + + CapturingMessageHandler messageHandler = new CapturingMessageHandler(); + channel.subscribe(messageHandler); + + channel.send(MessageBuilder.withPayload("test").build()); + + Message capturedMessage = messageHandler.join(); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> { + span.hasName(interceptorSpanName).hasKind(SpanKind.CONSUMER); + verifyCorrectSpanWasPropagated(capturedMessage, trace.getSpan(0)); + }, + span -> span.hasName("handler").hasParent(trace.getSpan(0)))); + + channel.unsubscribe(messageHandler); + } + + @Test + void shouldNotAddInterceptorTwice() { + SubscribableChannel channel = + applicationContext.getBean("directChannel1", SubscribableChannel.class); + + CapturingMessageHandler messageHandler = new CapturingMessageHandler(); + channel.subscribe(messageHandler); + + channel.send(MessageBuilder.withPayload("test").build()); + + Message capturedMessage = messageHandler.join(); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> { + span.hasName("application.directChannel2 process").hasKind(SpanKind.CONSUMER); + verifyCorrectSpanWasPropagated(capturedMessage, trace.getSpan(0)); + }, + span -> span.hasName("handler").hasParent(trace.getSpan(0)))); + + channel.unsubscribe(messageHandler); + } + + @Test + void shouldNotCreateAspanWhenThereIsAlreadyAspanInTheContext() { + SubscribableChannel channel = + applicationContext.getBean("directChannel", SubscribableChannel.class); + + CapturingMessageHandler messageHandler = new CapturingMessageHandler(); + channel.subscribe(messageHandler); + + testing.runWithSpan( + "parent", + () -> { + channel.send(MessageBuilder.withPayload("test").build()); + }); + + messageHandler.join(); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent"), + span -> span.hasName("handler").hasParent(trace.getSpan(0)))); + + channel.unsubscribe(messageHandler); + } + + @Test + void shouldHandleMultipleMessageChannelsInAchain() { + SubscribableChannel channel1 = + applicationContext.getBean("linkedChannel1", SubscribableChannel.class); + SubscribableChannel channel2 = + applicationContext.getBean("linkedChannel2", SubscribableChannel.class); + + CapturingMessageHandler messageHandler = new CapturingMessageHandler(); + channel2.subscribe(messageHandler); + + channel1.send(MessageBuilder.withPayload("test").build()); + + Message capturedMessage = messageHandler.join(); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> { + span.hasName("application.linkedChannel1 process").hasKind(SpanKind.CONSUMER); + verifyCorrectSpanWasPropagated(capturedMessage, trace.getSpan(0)); + }, + span -> span.hasName("handler").hasParent(trace.getSpan(0)))); + + channel2.unsubscribe(messageHandler); + } + + @Test + void captureMessageHeader() { + SubscribableChannel channel = + applicationContext.getBean("directChannel", SubscribableChannel.class); + + CapturingMessageHandler messageHandler = new CapturingMessageHandler(); + channel.subscribe(messageHandler); + + channel.send( + MessageBuilder.withPayload("test").setHeader("test-message-header", "test").build()); + + Message capturedMessage = messageHandler.join(); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> { + span.hasName("application.directChannel process").hasKind(SpanKind.CONSUMER); + verifyCorrectSpanWasPropagated(capturedMessage, trace.getSpan(0)); + }, + span -> span.hasName("handler").hasParent(trace.getSpan(0)))); + + channel.unsubscribe(messageHandler); + } + + static void verifyCorrectSpanWasPropagated(Message capturedMessage, SpanData parentSpan) { + String propagatedSpan = (String) capturedMessage.getHeaders().get("traceparent"); + assertThat(propagatedSpan).contains(parentSpan.getTraceId()); + assertThat(propagatedSpan).contains(parentSpan.getSpanId()); + } + + @SpringBootConfiguration + @EnableAutoConfiguration + public static class MessageChannelsConfig { + + SubscribableChannel problematicSharedChannel = new DirectChannel(); + + @Bean + public SubscribableChannel directChannel() { + return new DirectChannel(); + } + + @Bean + public SubscribableChannel directChannel1() { + return problematicSharedChannel; + } + + @Bean + public SubscribableChannel directChannel2() { + return problematicSharedChannel; + } + + @Bean + public SubscribableChannel executorChannel(GlobalChannelInterceptorWrapper otelInterceptor) { + ExecutorSubscribableChannel 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.getChannelInterceptor()); + } + return channel; + } + + @Bean + public SubscribableChannel linkedChannel1() { + return new DirectChannel(); + } + + @Bean + public SubscribableChannel linkedChannel2() { + return new DirectChannel(); + } + + @EventListener(ApplicationReadyEvent.class) + public void initialize() { + linkedChannel1().subscribe(message -> linkedChannel2().send(message)); + } + } +} diff --git a/instrumentation/spring/spring-integration-4.1/testing/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/integration/v4_1/CapturingMessageHandler.java b/instrumentation/spring/spring-integration-4.1/testing/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/integration/v4_1/CapturingMessageHandler.java new file mode 100644 index 000000000000..cb059f63dead --- /dev/null +++ b/instrumentation/spring/spring-integration-4.1/testing/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/integration/v4_1/CapturingMessageHandler.java @@ -0,0 +1,29 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.integration.v4_1; + +import static io.opentelemetry.instrumentation.testing.GlobalTraceUtil.runWithSpan; + +import java.util.concurrent.CompletableFuture; +import org.springframework.messaging.Message; +import org.springframework.messaging.MessageHandler; + +class CapturingMessageHandler implements MessageHandler { + final CompletableFuture> captured = new CompletableFuture<>(); + + @Override + public void handleMessage(Message message) { + runWithSpan( + "handler", + () -> { + captured.complete(message); + }); + } + + Message join() { + return captured.join(); + } +} diff --git a/instrumentation/spring/spring-integration-4.1/testing/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/integration/v4_1/RabbitExtension.java b/instrumentation/spring/spring-integration-4.1/testing/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/integration/v4_1/RabbitExtension.java new file mode 100644 index 000000000000..b1b90ad47503 --- /dev/null +++ b/instrumentation/spring/spring-integration-4.1/testing/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/integration/v4_1/RabbitExtension.java @@ -0,0 +1,135 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.integration.v4_1; + +import static io.opentelemetry.instrumentation.testing.GlobalTraceUtil.runWithSpan; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.extension.AfterEachCallback; +import org.junit.jupiter.api.extension.BeforeEachCallback; +import org.junit.jupiter.api.extension.ExtensionContext; +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; +import org.testcontainers.containers.wait.strategy.Wait; + +public class RabbitExtension implements BeforeEachCallback, AfterEachCallback { + + private GenericContainer rabbitMqContainer; + protected ConfigurableApplicationContext producerContext; + private ConfigurableApplicationContext consumerContext; + + private final Class additionalContextClass; + + public RabbitExtension(Class additionalContextClass) { + this.additionalContextClass = additionalContextClass; + } + + public T getBean(String name, Class requiredType) { + return producerContext.getBean(name, requiredType); + } + + @Override + public void beforeEach(ExtensionContext context) { + rabbitMqContainer = + new GenericContainer<>("rabbitmq:latest") + .withExposedPorts(5672) + .waitingFor(Wait.forLogMessage(".*Server startup complete.*", 1)) + .withStartupTimeout(Duration.ofMinutes(2)); + rabbitMqContainer.start(); + + SpringApplication producerApp = + new SpringApplication(getContextClasses(ProducerConfig.class, additionalContextClass)); + Map producerProperties = new HashMap<>(); + producerProperties.put("spring.application.name", "testProducer"); + producerProperties.put("spring.jmx.enabled", false); + producerProperties.put("spring.main.web-application-type", "none"); + producerProperties.put("spring.rabbitmq.host", rabbitMqContainer.getHost()); + producerProperties.put("spring.rabbitmq.port", rabbitMqContainer.getMappedPort(5672)); + producerProperties.put("spring.cloud.stream.bindings.output.destination", "testTopic"); + producerApp.setDefaultProperties(producerProperties); + producerContext = producerApp.run(); + + SpringApplication consumerApp = + new SpringApplication( + getContextClasses(ProducerConfig.ConsumerConfig.class, additionalContextClass)); + Map consumerProperties = new HashMap<>(); + consumerProperties.put("spring.application.name", "testConsumer"); + consumerProperties.put("spring.jmx.enabled", false); + consumerProperties.put("spring.main.web-application-type", "none"); + consumerProperties.put("spring.rabbitmq.host", rabbitMqContainer.getHost()); + consumerProperties.put("spring.rabbitmq.port", rabbitMqContainer.getMappedPort(5672)); + consumerProperties.put("spring.cloud.stream.bindings.input.destination", "testTopic"); + consumerApp.setDefaultProperties(consumerProperties); + consumerContext = consumerApp.run(); + } + + @Override + public void afterEach(ExtensionContext context) { + if (rabbitMqContainer != null) { + rabbitMqContainer.stop(); + } + if (producerContext != null) { + producerContext.close(); + } + if (consumerContext != null) { + consumerContext.close(); + } + } + + private static Class[] getContextClasses(Class mainContext, Class additionalContext) { + List> contextClasses = new ArrayList<>(); + contextClasses.add(mainContext); + if (additionalContext != null) { + contextClasses.add(additionalContext); + } + return contextClasses.toArray(new Class[0]); + } + + @SpringBootConfiguration + @EnableAutoConfiguration + @EnableBinding(Source.class) + static class ProducerConfig { + @Autowired Source source; + + @Bean + Runnable producer() { + return () -> + runWithSpan( + "producer", + () -> { + source.output().send(MessageBuilder.withPayload("test").build()); + }); + } + + @SpringBootConfiguration + @EnableAutoConfiguration + @EnableBinding(Sink.class) + static class ConsumerConfig { + @StreamListener(Sink.INPUT) + void consume(String ignored) { + runWithSpan( + "consumer", + () -> { + // do nothing + }); + } + } + } +} diff --git a/instrumentation/spring/spring-jms/spring-jms-2.0/javaagent/build.gradle.kts b/instrumentation/spring/spring-jms/spring-jms-2.0/javaagent/build.gradle.kts index 17c275f9db3f..dc7b41c7fdf5 100644 --- a/instrumentation/spring/spring-jms/spring-jms-2.0/javaagent/build.gradle.kts +++ b/instrumentation/spring/spring-jms/spring-jms-2.0/javaagent/build.gradle.kts @@ -14,6 +14,7 @@ muzzle { } dependencies { + bootstrap(project(":instrumentation:jms:jms-common:bootstrap")) implementation(project(":instrumentation:jms:jms-common:javaagent")) implementation(project(":instrumentation:jms:jms-1.1:javaagent")) library("org.springframework:spring-jms:2.0") @@ -22,6 +23,7 @@ dependencies { compileOnly("com.google.auto.value:auto-value-annotations") annotationProcessor("com.google.auto.value:auto-value") + testImplementation(project(":instrumentation:spring:spring-jms:spring-jms-2.0:testing")) testInstrumentation(project(":instrumentation:jms:jms-1.1:javaagent")) testImplementation("org.springframework.boot:spring-boot-starter-activemq:2.5.3") @@ -42,6 +44,7 @@ testing { suites { val testReceiveSpansDisabled by registering(JvmTestSuite::class) { dependencies { + implementation(project(":instrumentation:spring:spring-jms:spring-jms-2.0:testing")) // this is just to avoid a bit more copy-pasting implementation(project.sourceSets["test"].output) } diff --git a/instrumentation/spring/spring-jms/spring-jms-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/jms/v2_0/AbstractPollingMessageListenerContainerInstrumentation.java b/instrumentation/spring/spring-jms/spring-jms-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/jms/v2_0/AbstractPollingMessageListenerContainerInstrumentation.java new file mode 100644 index 000000000000..26484f593b60 --- /dev/null +++ b/instrumentation/spring/spring-jms/spring-jms-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/jms/v2_0/AbstractPollingMessageListenerContainerInstrumentation.java @@ -0,0 +1,53 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.jms.v2_0; + +import static io.opentelemetry.javaagent.instrumentation.spring.jms.v2_0.SpringJmsSingletons.isReceiveTelemetryEnabled; +import static net.bytebuddy.matcher.ElementMatchers.named; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.bootstrap.Java8BytecodeBridge; +import io.opentelemetry.javaagent.bootstrap.jms.JmsReceiveContextHolder; +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 AbstractPollingMessageListenerContainerInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher typeMatcher() { + return named("org.springframework.jms.listener.AbstractPollingMessageListenerContainer"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + named("receiveAndExecute"), this.getClass().getName() + "$ReceiveAndExecuteAdvice"); + } + + @SuppressWarnings("unused") + public static class ReceiveAndExecuteAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static Scope onEnter() { + if (isReceiveTelemetryEnabled()) { + Context context = JmsReceiveContextHolder.init(Java8BytecodeBridge.currentContext()); + return context.makeCurrent(); + } + return null; + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void onExit(@Advice.Enter Scope scope) { + if (scope == null) { + return; + } + scope.close(); + } + } +} diff --git a/instrumentation/spring/spring-jms/spring-jms-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/jms/v2_0/JmsDestinationAccessorInstrumentation.java b/instrumentation/spring/spring-jms/spring-jms-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/jms/v2_0/JmsDestinationAccessorInstrumentation.java new file mode 100644 index 000000000000..1598d6c7bd8f --- /dev/null +++ b/instrumentation/spring/spring-jms/spring-jms-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/jms/v2_0/JmsDestinationAccessorInstrumentation.java @@ -0,0 +1,59 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.jms.v2_0; + +import static io.opentelemetry.javaagent.instrumentation.spring.jms.v2_0.SpringJmsSingletons.isReceiveTelemetryEnabled; +import static io.opentelemetry.javaagent.instrumentation.spring.jms.v2_0.SpringJmsSingletons.receiveInstrumenter; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.returns; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.instrumentation.api.internal.InstrumenterUtil; +import io.opentelemetry.javaagent.bootstrap.Java8BytecodeBridge; +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 JmsDestinationAccessorInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher typeMatcher() { + return named("org.springframework.jms.support.destination.JmsDestinationAccessor"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + named("receiveFromConsumer").and(returns(named("javax.jms.Message"))), + this.getClass().getName() + "$ReceiveAdvice"); + } + + @SuppressWarnings("unused") + public static class ReceiveAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static Scope onEnter() { + if (isReceiveTelemetryEnabled()) { + return null; + } + // suppress receive span creation in jms instrumentation + Context context = + InstrumenterUtil.suppressSpan( + receiveInstrumenter(), Java8BytecodeBridge.currentContext(), null); + return context.makeCurrent(); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void onExit(@Advice.Enter Scope scope) { + if (scope == null) { + return; + } + scope.close(); + } + } +} diff --git a/instrumentation/spring/spring-jms/spring-jms-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/jms/v2_0/SpringJmsInstrumentationModule.java b/instrumentation/spring/spring-jms/spring-jms-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/jms/v2_0/SpringJmsInstrumentationModule.java index e05e799e82f3..b84997259016 100644 --- a/instrumentation/spring/spring-jms/spring-jms-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/jms/v2_0/SpringJmsInstrumentationModule.java +++ b/instrumentation/spring/spring-jms/spring-jms-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/jms/v2_0/SpringJmsInstrumentationModule.java @@ -6,11 +6,11 @@ package io.opentelemetry.javaagent.instrumentation.spring.jms.v2_0; import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.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.Arrays; import java.util.List; import net.bytebuddy.matcher.ElementMatcher; @@ -29,6 +29,9 @@ public ElementMatcher.Junction classLoaderMatcher() { @Override public List typeInstrumentations() { - return singletonList(new SpringJmsMessageListenerInstrumentation()); + return Arrays.asList( + new SpringJmsMessageListenerInstrumentation(), + new JmsDestinationAccessorInstrumentation(), + new AbstractPollingMessageListenerContainerInstrumentation()); } } diff --git a/instrumentation/spring/spring-jms/spring-jms-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/jms/v2_0/SpringJmsMessageListenerInstrumentation.java b/instrumentation/spring/spring-jms/spring-jms-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/jms/v2_0/SpringJmsMessageListenerInstrumentation.java index e30bb1d4e387..11f9a52bd035 100644 --- a/instrumentation/spring/spring-jms/spring-jms-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/jms/v2_0/SpringJmsMessageListenerInstrumentation.java +++ b/instrumentation/spring/spring-jms/spring-jms-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/jms/v2_0/SpringJmsMessageListenerInstrumentation.java @@ -16,6 +16,7 @@ import io.opentelemetry.context.Context; import io.opentelemetry.context.Scope; import io.opentelemetry.javaagent.bootstrap.Java8BytecodeBridge; +import io.opentelemetry.javaagent.bootstrap.jms.JmsReceiveContextHolder; import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; import io.opentelemetry.javaagent.instrumentation.jms.MessageWithDestination; @@ -59,6 +60,10 @@ public static void onEnter( @Advice.Local("otelScope") Scope scope) { Context parentContext = Java8BytecodeBridge.currentContext(); + Context receiveContext = JmsReceiveContextHolder.getReceiveContext(parentContext); + if (receiveContext != null) { + parentContext = receiveContext; + } request = MessageWithDestination.create(JavaxMessageAdapter.create(message), null); if (!listenerInstrumenter().shouldStart(parentContext, request)) { diff --git a/instrumentation/spring/spring-jms/spring-jms-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/jms/v2_0/SpringJmsSingletons.java b/instrumentation/spring/spring-jms/spring-jms-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/jms/v2_0/SpringJmsSingletons.java index 52906ff91063..034e3cdedac7 100644 --- a/instrumentation/spring/spring-jms/spring-jms-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/jms/v2_0/SpringJmsSingletons.java +++ b/instrumentation/spring/spring-jms/spring-jms-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/jms/v2_0/SpringJmsSingletons.java @@ -14,14 +14,32 @@ public final class SpringJmsSingletons { private static final String INSTRUMENTATION_NAME = "io.opentelemetry.spring-jms-2.0"; - private static final Instrumenter LISTENER_INSTRUMENTER = - new JmsInstrumenterFactory(GlobalOpenTelemetry.get(), INSTRUMENTATION_NAME) - .setCapturedHeaders(ExperimentalConfig.get().getMessagingHeaders()) - .createConsumerProcessInstrumenter(); + private static final boolean RECEIVE_TELEMETRY_ENABLED = + ExperimentalConfig.get().messagingReceiveInstrumentationEnabled(); + private static final Instrumenter LISTENER_INSTRUMENTER; + private static final Instrumenter RECEIVE_INSTRUMENTER; + + static { + JmsInstrumenterFactory factory = + new JmsInstrumenterFactory(GlobalOpenTelemetry.get(), INSTRUMENTATION_NAME) + .setCapturedHeaders(ExperimentalConfig.get().getMessagingHeaders()) + .setMessagingReceiveInstrumentationEnabled(RECEIVE_TELEMETRY_ENABLED); + + LISTENER_INSTRUMENTER = factory.createConsumerProcessInstrumenter(true); + RECEIVE_INSTRUMENTER = factory.createConsumerReceiveInstrumenter(); + } + + public static boolean isReceiveTelemetryEnabled() { + return RECEIVE_TELEMETRY_ENABLED; + } public static Instrumenter listenerInstrumenter() { return LISTENER_INSTRUMENTER; } + public static Instrumenter receiveInstrumenter() { + return RECEIVE_INSTRUMENTER; + } + private SpringJmsSingletons() {} } diff --git a/instrumentation/spring/spring-jms/spring-jms-2.0/javaagent/src/test/groovy/SpringListenerTest.groovy b/instrumentation/spring/spring-jms/spring-jms-2.0/javaagent/src/test/groovy/SpringListenerTest.groovy deleted file mode 100644 index e5db7900c1cc..000000000000 --- a/instrumentation/spring/spring-jms/spring-jms-2.0/javaagent/src/test/groovy/SpringListenerTest.groovy +++ /dev/null @@ -1,99 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -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 listener.AnnotatedListenerConfig -import listener.ManualListenerConfig -import org.springframework.context.annotation.AnnotationConfigApplicationContext -import org.springframework.jms.core.JmsTemplate - -import javax.jms.ConnectionFactory - -import static io.opentelemetry.api.trace.SpanKind.CONSUMER -import static io.opentelemetry.api.trace.SpanKind.PRODUCER - -class SpringListenerTest 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, "SpringListenerJms2", "", null, "receive") - } - trace(1, 2) { - producerSpan(it, 0, "SpringListenerJms2") - consumerSpan(it, 1, "SpringListenerJms2", "", span(0), "process") - } - } - - cleanup: - context.close() - - where: - config << [AnnotatedListenerConfig, ManualListenerConfig] - } - - static producerSpan(TraceAssert trace, int index, String destinationName, boolean testHeaders = false) { - trace.span(index) { - name destinationName + " send" - kind PRODUCER - hasNoParent() - attributes { - "$SemanticAttributes.MESSAGING_SYSTEM" "jms" - "$SemanticAttributes.MESSAGING_DESTINATION_NAME" destinationName - if (destinationName == "(temporary)") { - "$SemanticAttributes.MESSAGING_DESTINATION_TEMPORARY" true - } - "$SemanticAttributes.MESSAGING_MESSAGE_ID" String - if (testHeaders) { - "messaging.header.test_message_header" { it == ["test"] } - "messaging.header.test_message_int_header" { it == ["1234"] } - } - } - } - } - - // 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 destinationName, String messageId, Object parentOrLinkedSpan, String operation, boolean testHeaders = false) { - trace.span(index) { - name destinationName + " " + operation - kind CONSUMER - if (parentOrLinkedSpan != null) { - childOf((SpanData) parentOrLinkedSpan) - } else { - hasNoParent() - } - attributes { - "$SemanticAttributes.MESSAGING_SYSTEM" "jms" - "$SemanticAttributes.MESSAGING_DESTINATION_NAME" destinationName - "$SemanticAttributes.MESSAGING_OPERATION" 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" { it == messageId || messageId == "" } - } - if (destinationName == "(temporary)") { - "$SemanticAttributes.MESSAGING_DESTINATION_TEMPORARY" true - } - if (testHeaders) { - "messaging.header.test_message_header" { it == ["test"] } - "messaging.header.test_message_int_header" { it == ["1234"] } - } - } - } - } -} diff --git a/instrumentation/spring/spring-jms/spring-jms-2.0/javaagent/src/test/groovy/SpringTemplateTest.groovy b/instrumentation/spring/spring-jms/spring-jms-2.0/javaagent/src/test/groovy/SpringTemplateTest.groovy deleted file mode 100644 index ddf49278d9f7..000000000000 --- a/instrumentation/spring/spring-jms/spring-jms-2.0/javaagent/src/test/groovy/SpringTemplateTest.groovy +++ /dev/null @@ -1,182 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -import com.google.common.io.Files -import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification -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 org.springframework.jms.core.MessagePostProcessor -import spock.lang.Shared - -import javax.jms.Connection -import javax.jms.JMSException -import javax.jms.Message -import javax.jms.Session -import javax.jms.TextMessage -import java.util.concurrent.TimeUnit -import java.util.concurrent.atomic.AtomicReference - -import static SpringListenerTest.consumerSpan -import static SpringListenerTest.producerSpan - -class SpringTemplateTest extends AgentInstrumentationSpecification { - @Shared - HornetQServer server - @Shared - String messageText = "a message" - @Shared - JmsTemplate template - @Shared - Session session - @Shared - Connection connection - - 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)) - - 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() { - session.close() - connection.close() - 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, destinationName) - } - trace(1, 1) { - consumerSpan(it, 0, destinationName, receivedMessage.getJMSMessageID(), null, "receive") - } - } - - where: - destination | destinationName - session.createQueue("SpringTemplateJms2") | "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) - } - } - 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, destinationName, msgId.get(), null, "receive") - } - trace(1, 1) { - producerSpan(it, 0, destinationName) - } - trace(2, 1) { - consumerSpan(it, 0, "(temporary)", receivedMessage.getJMSMessageID(), null, "receive") - } - trace(3, 1) { - producerSpan(it, 0, "(temporary)") - } - } - - where: - destination | destinationName - session.createQueue("SpringTemplateJms2") | "SpringTemplateJms2" - } - - def "capture message header as span attribute"() { - setup: - template.convertAndSend(destination, messageText, new MessagePostProcessor() { - @Override - Message postProcessMessage(Message message) throws JMSException { - message.setStringProperty("test_message_header", "test") - message.setIntProperty("test_message_int_header", 1234) - return message - } - }) - TextMessage receivedMessage = template.receive(destination) - - expect: - receivedMessage.text == messageText - assertTraces(2) { - trace(0, 1) { - producerSpan(it, 0, destinationName, true) - } - trace(1, 1) { - consumerSpan(it, 0, destinationName, receivedMessage.getJMSMessageID(), null, "receive", true) - } - } - - where: - destination | destinationName - session.createQueue("SpringTemplateJms2") | "SpringTemplateJms2" - } -} diff --git a/instrumentation/spring/spring-jms/spring-jms-2.0/javaagent/src/test/groovy/listener/AbstractConfig.groovy b/instrumentation/spring/spring-jms/spring-jms-2.0/javaagent/src/test/groovy/listener/AbstractConfig.groovy deleted file mode 100644 index 2e7a99a85e09..000000000000 --- a/instrumentation/spring/spring-jms/spring-jms-2.0/javaagent/src/test/groovy/listener/AbstractConfig.groovy +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package listener - -import com.google.common.io.Files -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.jms.config.DefaultJmsListenerContainerFactory -import org.springframework.jms.config.JmsListenerContainerFactory - -import javax.annotation.PreDestroy -import javax.jms.ConnectionFactory - -class AbstractConfig { - - 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 jmsListenerContainerFactory(ConnectionFactory connectionFactory) { - DefaultJmsListenerContainerFactory factory = new DefaultJmsListenerContainerFactory() - factory.setConnectionFactory(connectionFactory) - return factory - } - - @PreDestroy - void destroy() { - server.stop() - } -} diff --git a/instrumentation/spring/spring-jms/spring-jms-2.0/javaagent/src/test/groovy/listener/AnnotatedListenerConfig.groovy b/instrumentation/spring/spring-jms/spring-jms-2.0/javaagent/src/test/groovy/listener/AnnotatedListenerConfig.groovy deleted file mode 100644 index af0cbe676d34..000000000000 --- a/instrumentation/spring/spring-jms/spring-jms-2.0/javaagent/src/test/groovy/listener/AnnotatedListenerConfig.groovy +++ /dev/null @@ -1,14 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package listener - -import org.springframework.context.annotation.ComponentScan -import org.springframework.jms.annotation.EnableJms - -@ComponentScan -@EnableJms -class AnnotatedListenerConfig extends AbstractConfig { -} diff --git a/instrumentation/spring/spring-jms/spring-jms-2.0/javaagent/src/test/groovy/listener/ManualListenerConfig.groovy b/instrumentation/spring/spring-jms/spring-jms-2.0/javaagent/src/test/groovy/listener/ManualListenerConfig.groovy deleted file mode 100644 index e2d7f6b2461f..000000000000 --- a/instrumentation/spring/spring-jms/spring-jms-2.0/javaagent/src/test/groovy/listener/ManualListenerConfig.groovy +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package listener - - -import org.springframework.jms.annotation.EnableJms -import org.springframework.jms.annotation.JmsListenerConfigurer -import org.springframework.jms.config.JmsListenerEndpoint -import org.springframework.jms.config.JmsListenerEndpointRegistrar -import org.springframework.jms.listener.AbstractMessageListenerContainer -import org.springframework.jms.listener.MessageListenerContainer -import org.springframework.jms.listener.SessionAwareMessageListener - -import javax.jms.JMSException -import javax.jms.Message -import javax.jms.Session - -@EnableJms -class ManualListenerConfig extends AbstractConfig implements JmsListenerConfigurer { - - @Override - void configureJmsListeners(JmsListenerEndpointRegistrar registrar) { - registrar.registerEndpoint(new JmsListenerEndpoint() { - @Override - String getId() { - return "testid" - } - - @Override - void setupListenerContainer(MessageListenerContainer listenerContainer) { - var container = (AbstractMessageListenerContainer) listenerContainer - container.setDestinationName("SpringListenerJms2") - container.setupMessageListener(new SessionAwareMessageListener() { - @Override - void onMessage(Message message, Session session) throws JMSException { - println "received: " + message - } - }) - } - }) - } -} diff --git a/instrumentation/spring/spring-jms/spring-jms-2.0/javaagent/src/test/groovy/listener/TestListener.groovy b/instrumentation/spring/spring-jms/spring-jms-2.0/javaagent/src/test/groovy/listener/TestListener.groovy deleted file mode 100644 index 7b97d2b6f710..000000000000 --- a/instrumentation/spring/spring-jms/spring-jms-2.0/javaagent/src/test/groovy/listener/TestListener.groovy +++ /dev/null @@ -1,18 +0,0 @@ -/* - * 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") - void receiveMessage(String message) { - println "received: " + message - } -} diff --git a/instrumentation/spring/spring-jms/spring-jms-2.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/jms/v2_0/AbstractConfig.java b/instrumentation/spring/spring-jms/spring-jms-2.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/jms/v2_0/AbstractConfig.java new file mode 100644 index 000000000000..33b64bb66f3e --- /dev/null +++ b/instrumentation/spring/spring-jms/spring-jms-2.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/jms/v2_0/AbstractConfig.java @@ -0,0 +1,81 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.jms.v2_0; + +import java.io.File; +import java.nio.file.Files; +import java.util.Collections; +import javax.annotation.PreDestroy; +import javax.jms.ConnectionFactory; +import org.hornetq.api.core.TransportConfiguration; +import org.hornetq.api.core.client.ClientSession; +import org.hornetq.api.core.client.ClientSessionFactory; +import org.hornetq.api.core.client.HornetQClient; +import org.hornetq.api.core.client.ServerLocator; +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.context.annotation.Bean; +import org.springframework.jms.config.DefaultJmsListenerContainerFactory; +import org.springframework.jms.config.JmsListenerContainerFactory; + +abstract class AbstractConfig { + + private HornetQServer server; + + @Bean + ConnectionFactory connectionFactory() throws Exception { + File tempDir = Files.createTempDirectory("tmp").toFile(); + tempDir.deleteOnExit(); + + Configuration config = new ConfigurationImpl(); + config.setBindingsDirectory(tempDir.getPath()); + config.setJournalDirectory(tempDir.getPath()); + config.setCreateBindingsDir(false); + config.setCreateJournalDir(false); + config.setSecurityEnabled(false); + config.setPersistenceEnabled(false); + config.setQueueConfigurations( + Collections.singletonList( + new CoreQueueConfiguration("someQueue", "someQueue", null, true))); + config.setAcceptorConfigurations( + Collections.singleton(new TransportConfiguration(InVMAcceptorFactory.class.getName()))); + + server = HornetQServers.newHornetQServer(config); + server.start(); + + ServerLocator serverLocator = + HornetQClient.createServerLocatorWithoutHA( + new TransportConfiguration(InVMConnectorFactory.class.getName())); + ClientSessionFactory sf = serverLocator.createSessionFactory(); + ClientSession 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.class.getName())); + } + + @Bean + JmsListenerContainerFactory jmsListenerContainerFactory(ConnectionFactory connectionFactory) { + DefaultJmsListenerContainerFactory factory = new DefaultJmsListenerContainerFactory(); + factory.setConnectionFactory(connectionFactory); + return factory; + } + + @PreDestroy + void destroy() throws Exception { + server.stop(); + } +} diff --git a/instrumentation/spring/spring-jms/spring-jms-2.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/jms/v2_0/AnnotatedListenerConfig.java b/instrumentation/spring/spring-jms/spring-jms-2.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/jms/v2_0/AnnotatedListenerConfig.java new file mode 100644 index 000000000000..007bffedc4e9 --- /dev/null +++ b/instrumentation/spring/spring-jms/spring-jms-2.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/jms/v2_0/AnnotatedListenerConfig.java @@ -0,0 +1,13 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.jms.v2_0; + +import org.springframework.context.annotation.ComponentScan; +import org.springframework.jms.annotation.EnableJms; + +@ComponentScan +@EnableJms +public class AnnotatedListenerConfig extends AbstractConfig {} diff --git a/instrumentation/spring/spring-jms/spring-jms-2.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/jms/v2_0/ManualListenerConfig.java b/instrumentation/spring/spring-jms/spring-jms-2.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/jms/v2_0/ManualListenerConfig.java new file mode 100644 index 000000000000..efc9393f5d1c --- /dev/null +++ b/instrumentation/spring/spring-jms/spring-jms-2.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/jms/v2_0/ManualListenerConfig.java @@ -0,0 +1,51 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.jms.v2_0; + +import ch.qos.logback.classic.Level; +import io.opentelemetry.instrumentation.test.utils.LoggerUtils; +import javax.jms.Message; +import org.jetbrains.annotations.NotNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.jms.annotation.EnableJms; +import org.springframework.jms.annotation.JmsListenerConfigurer; +import org.springframework.jms.config.JmsListenerEndpoint; +import org.springframework.jms.config.JmsListenerEndpointRegistrar; +import org.springframework.jms.listener.AbstractMessageListenerContainer; +import org.springframework.jms.listener.MessageListenerContainer; +import org.springframework.jms.listener.SessionAwareMessageListener; + +@EnableJms +public class ManualListenerConfig extends AbstractConfig implements JmsListenerConfigurer { + + private static final Logger logger = LoggerFactory.getLogger(ManualListenerConfig.class); + + static { + LoggerUtils.setLevel(logger, Level.INFO); + } + + @Override + public void configureJmsListeners(JmsListenerEndpointRegistrar registrar) { + registrar.registerEndpoint( + new JmsListenerEndpoint() { + @Override + public @NotNull String getId() { + return "testid"; + } + + @Override + public void setupListenerContainer(@NotNull MessageListenerContainer listenerContainer) { + AbstractMessageListenerContainer container = + (AbstractMessageListenerContainer) listenerContainer; + container.setDestinationName("SpringListenerJms2"); + container.setupMessageListener( + (SessionAwareMessageListener) + (message, session) -> logger.info("received: {}", message)); + } + }); + } +} diff --git a/instrumentation/spring/spring-jms/spring-jms-2.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/jms/v2_0/SpringListenerTest.java b/instrumentation/spring/spring-jms/spring-jms-2.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/jms/v2_0/SpringListenerTest.java new file mode 100644 index 000000000000..ca6300e57b09 --- /dev/null +++ b/instrumentation/spring/spring-jms/spring-jms-2.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/jms/v2_0/SpringListenerTest.java @@ -0,0 +1,68 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.jms.v2_0; + +import static io.opentelemetry.api.trace.SpanKind.CONSUMER; +import static io.opentelemetry.api.trace.SpanKind.PRODUCER; +import static io.opentelemetry.instrumentation.testing.util.TelemetryDataUtil.orderByRootSpanKind; + +import io.opentelemetry.instrumentation.spring.jms.v2_0.AbstractJmsTest; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.sdk.trace.data.SpanData; +import java.util.concurrent.atomic.AtomicReference; +import javax.jms.ConnectionFactory; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.jms.core.JmsTemplate; + +class SpringListenerTest extends AbstractJmsTest { + + @RegisterExtension + private static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + @ParameterizedTest + @ValueSource(classes = {AnnotatedListenerConfig.class, ManualListenerConfig.class}) + void receivingMessageInSpringListenerGeneratesSpans(Class config) { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(config); + ConnectionFactory factory = context.getBean(ConnectionFactory.class); + JmsTemplate template = new JmsTemplate(factory); + + template.convertAndSend("SpringListenerJms2", "a message"); + + AtomicReference producerSpan = new AtomicReference<>(); + testing.waitAndAssertSortedTraces( + orderByRootSpanKind(PRODUCER, CONSUMER), + trace -> { + trace.hasSpansSatisfyingExactly( + span -> assertProducerSpan(span, "SpringListenerJms2", false)); + producerSpan.set(trace.getSpan(0)); + }, + trace -> + trace.hasSpansSatisfyingExactly( + span -> + assertConsumerSpan( + span, + producerSpan.get(), + null, + "SpringListenerJms2", + "receive", + false, + null), + span -> + assertConsumerSpan( + span, + producerSpan.get(), + trace.getSpan(0), + "SpringListenerJms2", + "process", + false, + null))); + context.close(); + } +} diff --git a/instrumentation/spring/spring-jms/spring-jms-2.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/jms/v2_0/SpringTemplateTest.java b/instrumentation/spring/spring-jms/spring-jms-2.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/jms/v2_0/SpringTemplateTest.java new file mode 100644 index 000000000000..ecd8c3391090 --- /dev/null +++ b/instrumentation/spring/spring-jms/spring-jms-2.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/jms/v2_0/SpringTemplateTest.java @@ -0,0 +1,257 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.jms.v2_0; + +import static io.opentelemetry.instrumentation.testing.util.TelemetryDataUtil.orderByRootSpanName; +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.instrumentation.spring.jms.v2_0.AbstractJmsTest; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.sdk.trace.data.SpanData; +import java.io.File; +import java.nio.file.Files; +import java.util.Collections; +import java.util.Objects; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; +import javax.jms.Connection; +import javax.jms.JMSException; +import javax.jms.Message; +import javax.jms.Queue; +import javax.jms.Session; +import javax.jms.TextMessage; +import org.hornetq.api.core.TransportConfiguration; +import org.hornetq.api.core.client.ClientSession; +import org.hornetq.api.core.client.ClientSessionFactory; +import org.hornetq.api.core.client.HornetQClient; +import org.hornetq.api.core.client.ServerLocator; +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.HornetQConnectionFactory; +import org.jetbrains.annotations.NotNull; +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; +import org.springframework.jms.core.JmsTemplate; +import org.springframework.jms.core.MessagePostProcessor; + +class SpringTemplateTest extends AbstractJmsTest { + + @RegisterExtension + private static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + private static HornetQServer server; + private static final String messageText = "a message"; + private static JmsTemplate template; + private static Session session; + private static Connection connection; + + @BeforeAll + static void setup() throws Exception { + File tempDir = Files.createTempDirectory("tmp").toFile(); + tempDir.deleteOnExit(); + + Configuration config = new ConfigurationImpl(); + config.setBindingsDirectory(tempDir.getPath()); + config.setJournalDirectory(tempDir.getPath()); + config.setCreateBindingsDir(false); + config.setCreateJournalDir(false); + config.setSecurityEnabled(false); + config.setPersistenceEnabled(false); + config.setQueueConfigurations( + Collections.singletonList( + new CoreQueueConfiguration("someQueue", "someQueue", null, true))); + config.setAcceptorConfigurations( + Collections.singleton(new TransportConfiguration(InVMAcceptorFactory.class.getName()))); + + server = HornetQServers.newHornetQServer(config); + server.start(); + + ServerLocator serverLocator = + HornetQClient.createServerLocatorWithoutHA( + new TransportConfiguration(InVMConnectorFactory.class.getName())); + ClientSessionFactory sf = serverLocator.createSessionFactory(); + ClientSession clientSession = sf.createSession(false, false, false); + clientSession.createQueue("jms.queue.SpringTemplateJms2", "jms.queue.SpringTemplateJms2", true); + clientSession.close(); + sf.close(); + serverLocator.close(); + + HornetQConnectionFactory connectionFactory = + HornetQJMSClient.createConnectionFactoryWithoutHA( + JMSFactoryType.CF, new TransportConfiguration(InVMConnectorFactory.class.getName())); + + connection = connectionFactory.createConnection(); + connection.start(); + session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE); + session.run(); + + template = new JmsTemplate(connectionFactory); + template.setReceiveTimeout(TimeUnit.SECONDS.toMillis(10)); + } + + @AfterAll + static void cleanup() throws Exception { + session.close(); + connection.close(); + server.stop(); + } + + @Test + void sendingMessageToDestinationNameGeneratesSpans() throws JMSException { + Queue queue = session.createQueue("SpringTemplateJms2"); + template.convertAndSend(queue, messageText); + TextMessage receivedMessage = (TextMessage) template.receive(queue); + + assertThat(receivedMessage).isNotNull(); + assertThat(receivedMessage.getText()).isEqualTo(messageText); + + String receivedMsgId = receivedMessage.getJMSMessageID(); + AtomicReference producerSpan = new AtomicReference<>(); + testing.waitAndAssertTraces( + trace -> { + trace.hasSpansSatisfyingExactly( + span -> assertProducerSpan(span, "SpringTemplateJms2", false)); + producerSpan.set(trace.getSpan(0)); + }, + trace -> + trace.hasSpansSatisfyingExactly( + span -> + assertConsumerSpan( + span, + producerSpan.get(), + null, + "SpringTemplateJms2", + "receive", + false, + receivedMsgId))); + } + + @Test + void sendAndReceiveMessageGeneratesSpans() throws JMSException { + AtomicReference msgId = new AtomicReference<>(); + Queue queue = session.createQueue("SpringTemplateJms2"); + Runnable msgSend = + () -> { + TextMessage msg = (TextMessage) template.receive(queue); + assertThat(msg).isNotNull(); + try { + assertThat(msg.getText()).isEqualTo(messageText); + msgId.set(msg.getJMSMessageID()); + // There's a chance this might be reported last, messing up the assertion. + template.send( + msg.getJMSReplyTo(), + (session) -> + Objects.requireNonNull(template.getMessageConverter()) + .toMessage("responded!", session)); + } catch (Exception e) { + throw new RuntimeException(e); + } + }; + Thread msgSendThread = new Thread(msgSend); + msgSendThread.start(); + TextMessage receivedMessage = + (TextMessage) + template.sendAndReceive( + queue, + session -> + Objects.requireNonNull(template.getMessageConverter()) + .toMessage(messageText, session)); + + assertThat(receivedMessage).isNotNull(); + assertThat(receivedMessage.getText()).isEqualTo("responded!"); + + String receivedMsgId = receivedMessage.getJMSMessageID(); + AtomicReference producerSpan = new AtomicReference<>(); + AtomicReference tmpProducerSpan = new AtomicReference<>(); + testing.waitAndAssertSortedTraces( + orderByRootSpanName( + "SpringTemplateJms2 publish", + "SpringTemplateJms2 receive", + "(temporary) publish", + "(temporary) receive"), + trace -> { + trace.hasSpansSatisfyingExactly( + span -> assertProducerSpan(span, "SpringTemplateJms2", false)); + producerSpan.set(trace.getSpan(0)); + }, + trace -> + trace.hasSpansSatisfyingExactly( + span -> + assertConsumerSpan( + span, + producerSpan.get(), + null, + "SpringTemplateJms2", + "receive", + false, + msgId.get())), + trace -> { + trace.hasSpansSatisfyingExactly(span -> assertProducerSpan(span, "(temporary)", false)); + tmpProducerSpan.set(trace.getSpan(0)); + }, + trace -> + trace.hasSpansSatisfyingExactly( + span -> + assertConsumerSpan( + span, + tmpProducerSpan.get(), + null, + "(temporary)", + "receive", + false, + receivedMsgId))); + } + + @Test + void captureMessageHeaderAsSpanAttribute() throws JMSException { + Queue queue = session.createQueue("SpringTemplateJms2"); + template.convertAndSend( + queue, + messageText, + new MessagePostProcessor() { + @Override + public @NotNull Message postProcessMessage(@NotNull Message message) throws JMSException { + message.setStringProperty("test_message_header", "test"); + message.setIntProperty("test_message_int_header", 1234); + return message; + } + }); + TextMessage receivedMessage = (TextMessage) template.receive(queue); + + assertThat(receivedMessage).isNotNull(); + assertThat(receivedMessage.getText()).isEqualTo(messageText); + + String receivedMsgId = receivedMessage.getJMSMessageID(); + AtomicReference producerSpan = new AtomicReference<>(); + testing.waitAndAssertTraces( + trace -> { + trace.hasSpansSatisfyingExactly( + span -> assertProducerSpan(span, "SpringTemplateJms2", true)); + producerSpan.set(trace.getSpan(0)); + }, + trace -> + trace.hasSpansSatisfyingExactly( + span -> + assertConsumerSpan( + span, + producerSpan.get(), + null, + "SpringTemplateJms2", + "receive", + true, + receivedMsgId))); + } +} diff --git a/instrumentation/spring/spring-jms/spring-jms-2.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/jms/v2_0/TestListener.java b/instrumentation/spring/spring-jms/spring-jms-2.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/jms/v2_0/TestListener.java new file mode 100644 index 000000000000..77117fb3e5b6 --- /dev/null +++ b/instrumentation/spring/spring-jms/spring-jms-2.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/jms/v2_0/TestListener.java @@ -0,0 +1,28 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.jms.v2_0; + +import ch.qos.logback.classic.Level; +import io.opentelemetry.instrumentation.test.utils.LoggerUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.jms.annotation.JmsListener; +import org.springframework.stereotype.Component; + +@Component +public class TestListener { + + private static final Logger logger = LoggerFactory.getLogger(TestListener.class); + + static { + LoggerUtils.setLevel(logger, Level.INFO); + } + + @JmsListener(destination = "SpringListenerJms2") + void receiveMessage(String message) { + logger.info("received: {}", message); + } +} diff --git a/instrumentation/spring/spring-jms/spring-jms-2.0/javaagent/src/testReceiveSpansDisabled/groovy/SpringListenerSuppressReceiveSpansTest.groovy b/instrumentation/spring/spring-jms/spring-jms-2.0/javaagent/src/testReceiveSpansDisabled/groovy/SpringListenerSuppressReceiveSpansTest.groovy deleted file mode 100644 index d628dd079cf4..000000000000 --- a/instrumentation/spring/spring-jms/spring-jms-2.0/javaagent/src/testReceiveSpansDisabled/groovy/SpringListenerSuppressReceiveSpansTest.groovy +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification -import listener.AnnotatedListenerConfig -import org.springframework.context.annotation.AnnotationConfigApplicationContext -import org.springframework.jms.core.JmsTemplate - -import javax.jms.ConnectionFactory - -class SpringListenerSuppressReceiveSpansTest extends AgentInstrumentationSpecification { - def "receiving message in spring listener generates spans"() { - setup: - def context = new AnnotationConfigApplicationContext(AnnotatedListenerConfig) - def factory = context.getBean(ConnectionFactory) - def template = new JmsTemplate(factory) - - template.convertAndSend("SpringListenerJms2", "a message") - - expect: - assertTraces(1) { - trace(0, 2) { - SpringListenerTest.producerSpan(it, 0, "SpringListenerJms2") - SpringListenerTest.consumerSpan(it, 1, "SpringListenerJms2", "", span(0), "process") - } - } - - cleanup: - context.close() - } -} diff --git a/instrumentation/spring/spring-jms/spring-jms-2.0/javaagent/src/testReceiveSpansDisabled/java/io/opentelemetry/javaagent/instrumentation/spring/jms/v2_0/SpringListenerSuppressReceiveSpansTest.java b/instrumentation/spring/spring-jms/spring-jms-2.0/javaagent/src/testReceiveSpansDisabled/java/io/opentelemetry/javaagent/instrumentation/spring/jms/v2_0/SpringListenerSuppressReceiveSpansTest.java new file mode 100644 index 000000000000..affd5c39c9bf --- /dev/null +++ b/instrumentation/spring/spring-jms/spring-jms-2.0/javaagent/src/testReceiveSpansDisabled/java/io/opentelemetry/javaagent/instrumentation/spring/jms/v2_0/SpringListenerSuppressReceiveSpansTest.java @@ -0,0 +1,45 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.jms.v2_0; + +import io.opentelemetry.instrumentation.spring.jms.v2_0.AbstractJmsTest; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import javax.jms.ConnectionFactory; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.jms.core.JmsTemplate; + +class SpringListenerSuppressReceiveSpansTest extends AbstractJmsTest { + + @RegisterExtension + private static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + @Test + void receivingMessageInSpringListenerGeneratesSpans() { + AnnotationConfigApplicationContext context = + new AnnotationConfigApplicationContext(AnnotatedListenerConfig.class); + ConnectionFactory factory = context.getBean(ConnectionFactory.class); + JmsTemplate template = new JmsTemplate(factory); + + template.convertAndSend("SpringListenerJms2", "a message"); + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> assertProducerSpan(span, "SpringListenerJms2", false), + span -> + assertConsumerSpan( + span, + null, + trace.getSpan(0), + "SpringListenerJms2", + "process", + false, + null))); + context.close(); + } +} diff --git a/instrumentation/spring/spring-jms/spring-jms-2.0/testing/build.gradle.kts b/instrumentation/spring/spring-jms/spring-jms-2.0/testing/build.gradle.kts new file mode 100644 index 000000000000..023a880876af --- /dev/null +++ b/instrumentation/spring/spring-jms/spring-jms-2.0/testing/build.gradle.kts @@ -0,0 +1,7 @@ +plugins { + id("otel.java-conventions") +} + +dependencies { + implementation(project(":testing-common")) +} diff --git a/instrumentation/spring/spring-jms/spring-jms-2.0/testing/src/main/java/io/opentelemetry/instrumentation/spring/jms/v2_0/AbstractJmsTest.java b/instrumentation/spring/spring-jms/spring-jms-2.0/testing/src/main/java/io/opentelemetry/instrumentation/spring/jms/v2_0/AbstractJmsTest.java new file mode 100644 index 000000000000..6e679ecd2f3d --- /dev/null +++ b/instrumentation/spring/spring-jms/spring-jms-2.0/testing/src/main/java/io/opentelemetry/instrumentation/spring/jms/v2_0/AbstractJmsTest.java @@ -0,0 +1,117 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.jms.v2_0; + +import static io.opentelemetry.api.common.AttributeKey.stringArrayKey; +import static io.opentelemetry.api.trace.SpanKind.CONSUMER; +import static io.opentelemetry.api.trace.SpanKind.PRODUCER; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.satisfies; +import static java.util.Arrays.asList; + +import io.opentelemetry.sdk.testing.assertj.AttributeAssertion; +import io.opentelemetry.sdk.testing.assertj.SpanDataAssert; +import io.opentelemetry.sdk.trace.data.LinkData; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public abstract class AbstractJmsTest { + + protected void assertProducerSpan( + SpanDataAssert span, String destinationName, boolean testHeaders) { + List attributeAssertions = + producerAttributeAssertions(destinationName, testHeaders); + span.hasName(destinationName + " publish") + .hasKind(PRODUCER) + .hasNoParent() + .hasAttributesSatisfyingExactly(attributeAssertions); + } + + protected List producerAttributeAssertions( + String destinationName, boolean testHeaders) { + List attributeAssertions = + new ArrayList<>( + asList( + equalTo(MessagingIncubatingAttributes.MESSAGING_SYSTEM, "jms"), + equalTo(MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME, destinationName), + equalTo(MessagingIncubatingAttributes.MESSAGING_OPERATION, "publish"), + satisfies( + MessagingIncubatingAttributes.MESSAGING_MESSAGE_ID, + val -> val.isInstanceOf(String.class)))); + if (destinationName.equals("(temporary)")) { + attributeAssertions.add( + equalTo(MessagingIncubatingAttributes.MESSAGING_DESTINATION_TEMPORARY, true)); + } + if (testHeaders) { + attributeAssertions.add( + equalTo( + stringArrayKey("messaging.header.test_message_header"), + Collections.singletonList("test"))); + attributeAssertions.add( + equalTo( + stringArrayKey("messaging.header.test_message_int_header"), + Collections.singletonList("1234"))); + } + return attributeAssertions; + } + + protected void assertConsumerSpan( + SpanDataAssert span, + SpanData producer, + SpanData parent, + String destinationName, + String operation, + boolean testHeaders, + String msgId) { + span.hasName(destinationName + " " + operation).hasKind(CONSUMER); + if (parent != null) { + span.hasParent(parent); + } else { + span.hasNoParent(); + } + if (producer != null) { + span.hasLinks(LinkData.create(producer.getSpanContext())); + } + span.hasAttributesSatisfyingExactly( + consumerAttributeAssertions(destinationName, testHeaders, operation, msgId)); + } + + protected List consumerAttributeAssertions( + String destinationName, boolean testHeaders, String operation, String msgId) { + List attributeAssertions = + new ArrayList<>( + asList( + equalTo(MessagingIncubatingAttributes.MESSAGING_SYSTEM, "jms"), + equalTo(MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME, destinationName), + equalTo(MessagingIncubatingAttributes.MESSAGING_OPERATION, operation))); + if (msgId != null) { + attributeAssertions.add(equalTo(MessagingIncubatingAttributes.MESSAGING_MESSAGE_ID, msgId)); + } else { + attributeAssertions.add( + satisfies( + MessagingIncubatingAttributes.MESSAGING_MESSAGE_ID, + val -> val.isInstanceOf(String.class))); + } + if (destinationName.equals("(temporary)")) { + attributeAssertions.add( + equalTo(MessagingIncubatingAttributes.MESSAGING_DESTINATION_TEMPORARY, true)); + } + if (testHeaders) { + attributeAssertions.add( + equalTo( + stringArrayKey("messaging.header.test_message_header"), + Collections.singletonList("test"))); + attributeAssertions.add( + equalTo( + stringArrayKey("messaging.header.test_message_int_header"), + Collections.singletonList("1234"))); + } + return attributeAssertions; + } +} diff --git a/instrumentation/spring/spring-jms/spring-jms-6.0/javaagent/build.gradle.kts b/instrumentation/spring/spring-jms/spring-jms-6.0/javaagent/build.gradle.kts index c6eb9bdd6da0..a6d71b1279d7 100644 --- a/instrumentation/spring/spring-jms/spring-jms-6.0/javaagent/build.gradle.kts +++ b/instrumentation/spring/spring-jms/spring-jms-6.0/javaagent/build.gradle.kts @@ -14,6 +14,7 @@ muzzle { } dependencies { + bootstrap(project(":instrumentation:jms:jms-common:bootstrap")) implementation(project(":instrumentation:jms:jms-common:javaagent")) implementation(project(":instrumentation:jms:jms-3.0:javaagent")) @@ -34,9 +35,25 @@ otelJava { } tasks { - test { + withType().configureEach { usesService(gradle.sharedServices.registrations["testcontainersBuildService"].service) + } + + val testReceiveSpansDisabled by registering(Test::class) { + filter { + includeTestsMatching("SpringListenerSuppressReceiveSpansTest") + } + include("**/SpringListenerSuppressReceiveSpansTest.*") + } + test { + filter { + excludeTestsMatching("SpringListenerSuppressReceiveSpansTest") + } jvmArgs("-Dotel.instrumentation.messaging.experimental.receive-telemetry.enabled=true") } + + check { + dependsOn(testReceiveSpansDisabled) + } } diff --git a/instrumentation/spring/spring-jms/spring-jms-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/jms/v6_0/AbstractPollingMessageListenerContainerInstrumentation.java b/instrumentation/spring/spring-jms/spring-jms-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/jms/v6_0/AbstractPollingMessageListenerContainerInstrumentation.java new file mode 100644 index 000000000000..3f63b320738e --- /dev/null +++ b/instrumentation/spring/spring-jms/spring-jms-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/jms/v6_0/AbstractPollingMessageListenerContainerInstrumentation.java @@ -0,0 +1,53 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.jms.v6_0; + +import static io.opentelemetry.javaagent.instrumentation.spring.jms.v6_0.SpringJmsSingletons.isReceiveTelemetryEnabled; +import static net.bytebuddy.matcher.ElementMatchers.named; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.bootstrap.Java8BytecodeBridge; +import io.opentelemetry.javaagent.bootstrap.jms.JmsReceiveContextHolder; +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 AbstractPollingMessageListenerContainerInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher typeMatcher() { + return named("org.springframework.jms.listener.AbstractPollingMessageListenerContainer"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + named("receiveAndExecute"), this.getClass().getName() + "$ReceiveAndExecuteAdvice"); + } + + @SuppressWarnings("unused") + public static class ReceiveAndExecuteAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static Scope onEnter() { + if (isReceiveTelemetryEnabled()) { + Context context = JmsReceiveContextHolder.init(Java8BytecodeBridge.currentContext()); + return context.makeCurrent(); + } + return null; + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void onExit(@Advice.Enter Scope scope) { + if (scope == null) { + return; + } + scope.close(); + } + } +} diff --git a/instrumentation/spring/spring-jms/spring-jms-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/jms/v6_0/JmsDestinationAccessorInstrumentation.java b/instrumentation/spring/spring-jms/spring-jms-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/jms/v6_0/JmsDestinationAccessorInstrumentation.java new file mode 100644 index 000000000000..66a065d2d885 --- /dev/null +++ b/instrumentation/spring/spring-jms/spring-jms-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/jms/v6_0/JmsDestinationAccessorInstrumentation.java @@ -0,0 +1,59 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.jms.v6_0; + +import static io.opentelemetry.javaagent.instrumentation.spring.jms.v6_0.SpringJmsSingletons.isReceiveTelemetryEnabled; +import static io.opentelemetry.javaagent.instrumentation.spring.jms.v6_0.SpringJmsSingletons.receiveInstrumenter; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.returns; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.instrumentation.api.internal.InstrumenterUtil; +import io.opentelemetry.javaagent.bootstrap.Java8BytecodeBridge; +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 JmsDestinationAccessorInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher typeMatcher() { + return named("org.springframework.jms.support.destination.JmsDestinationAccessor"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + named("receiveFromConsumer").and(returns(named("jakarta.jms.Message"))), + this.getClass().getName() + "$ReceiveAdvice"); + } + + @SuppressWarnings("unused") + public static class ReceiveAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static Scope onEnter() { + if (isReceiveTelemetryEnabled()) { + return null; + } + // suppress receive span creation in jms instrumentation + Context context = + InstrumenterUtil.suppressSpan( + receiveInstrumenter(), Java8BytecodeBridge.currentContext(), null); + return context.makeCurrent(); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void onExit(@Advice.Enter Scope scope) { + if (scope == null) { + return; + } + scope.close(); + } + } +} diff --git a/instrumentation/spring/spring-jms/spring-jms-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/jms/v6_0/SpringJmsInstrumentationModule.java b/instrumentation/spring/spring-jms/spring-jms-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/jms/v6_0/SpringJmsInstrumentationModule.java index 0cbca1836f98..e587f13e289e 100644 --- a/instrumentation/spring/spring-jms/spring-jms-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/jms/v6_0/SpringJmsInstrumentationModule.java +++ b/instrumentation/spring/spring-jms/spring-jms-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/jms/v6_0/SpringJmsInstrumentationModule.java @@ -6,7 +6,7 @@ package io.opentelemetry.javaagent.instrumentation.spring.jms.v6_0; import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.hasClassesNamed; -import static java.util.Collections.singletonList; +import static java.util.Arrays.asList; import static net.bytebuddy.matcher.ElementMatchers.not; import com.google.auto.service.AutoService; @@ -30,6 +30,9 @@ public ElementMatcher.Junction classLoaderMatcher() { @Override public List typeInstrumentations() { - return singletonList(new SpringJmsMessageListenerInstrumentation()); + return asList( + new SpringJmsMessageListenerInstrumentation(), + new JmsDestinationAccessorInstrumentation(), + new AbstractPollingMessageListenerContainerInstrumentation()); } } diff --git a/instrumentation/spring/spring-jms/spring-jms-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/jms/v6_0/SpringJmsMessageListenerInstrumentation.java b/instrumentation/spring/spring-jms/spring-jms-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/jms/v6_0/SpringJmsMessageListenerInstrumentation.java index fd783dbbe862..988d33d78a54 100644 --- a/instrumentation/spring/spring-jms/spring-jms-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/jms/v6_0/SpringJmsMessageListenerInstrumentation.java +++ b/instrumentation/spring/spring-jms/spring-jms-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/jms/v6_0/SpringJmsMessageListenerInstrumentation.java @@ -16,6 +16,7 @@ import io.opentelemetry.context.Context; import io.opentelemetry.context.Scope; import io.opentelemetry.javaagent.bootstrap.Java8BytecodeBridge; +import io.opentelemetry.javaagent.bootstrap.jms.JmsReceiveContextHolder; import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; import io.opentelemetry.javaagent.instrumentation.jms.MessageWithDestination; @@ -59,6 +60,10 @@ public static void onEnter( @Advice.Local("otelScope") Scope scope) { Context parentContext = Java8BytecodeBridge.currentContext(); + Context receiveContext = JmsReceiveContextHolder.getReceiveContext(parentContext); + if (receiveContext != null) { + parentContext = receiveContext; + } request = MessageWithDestination.create(JakartaMessageAdapter.create(message), null); if (!listenerInstrumenter().shouldStart(parentContext, request)) { diff --git a/instrumentation/spring/spring-jms/spring-jms-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/jms/v6_0/SpringJmsSingletons.java b/instrumentation/spring/spring-jms/spring-jms-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/jms/v6_0/SpringJmsSingletons.java index 1f40560707f6..38360f67e3a6 100644 --- a/instrumentation/spring/spring-jms/spring-jms-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/jms/v6_0/SpringJmsSingletons.java +++ b/instrumentation/spring/spring-jms/spring-jms-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/jms/v6_0/SpringJmsSingletons.java @@ -14,14 +14,32 @@ public final class SpringJmsSingletons { private static final String INSTRUMENTATION_NAME = "io.opentelemetry.spring-jms-6.0"; - private static final Instrumenter LISTENER_INSTRUMENTER = - new JmsInstrumenterFactory(GlobalOpenTelemetry.get(), INSTRUMENTATION_NAME) - .setCapturedHeaders(ExperimentalConfig.get().getMessagingHeaders()) - .createConsumerProcessInstrumenter(); + private static final boolean RECEIVE_TELEMETRY_ENABLED = + ExperimentalConfig.get().messagingReceiveInstrumentationEnabled(); + private static final Instrumenter LISTENER_INSTRUMENTER; + private static final Instrumenter RECEIVE_INSTRUMENTER; + + static { + JmsInstrumenterFactory factory = + new JmsInstrumenterFactory(GlobalOpenTelemetry.get(), INSTRUMENTATION_NAME) + .setCapturedHeaders(ExperimentalConfig.get().getMessagingHeaders()) + .setMessagingReceiveInstrumentationEnabled(RECEIVE_TELEMETRY_ENABLED); + + LISTENER_INSTRUMENTER = factory.createConsumerProcessInstrumenter(true); + RECEIVE_INSTRUMENTER = factory.createConsumerReceiveInstrumenter(); + } + + public static boolean isReceiveTelemetryEnabled() { + return RECEIVE_TELEMETRY_ENABLED; + } public static Instrumenter listenerInstrumenter() { return LISTENER_INSTRUMENTER; } + public static Instrumenter receiveInstrumenter() { + return RECEIVE_INSTRUMENTER; + } + private SpringJmsSingletons() {} } diff --git a/instrumentation/spring/spring-jms/spring-jms-6.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/jms/v6_0/AbstractSpringJmsListenerTest.java b/instrumentation/spring/spring-jms/spring-jms-6.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/jms/v6_0/AbstractSpringJmsListenerTest.java new file mode 100644 index 000000000000..e7a713d1aae6 --- /dev/null +++ b/instrumentation/spring/spring-jms/spring-jms-6.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/jms/v6_0/AbstractSpringJmsListenerTest.java @@ -0,0 +1,114 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.jms.v6_0; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.params.provider.Arguments.arguments; + +import io.opentelemetry.instrumentation.testing.internal.AutoCleanupExtension; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import jakarta.jms.ConnectionFactory; +import java.time.Duration; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.stream.Stream; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.RegisterExtension; +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.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.SpringApplication; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.jms.core.JmsTemplate; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.output.Slf4jLogConsumer; +import org.testcontainers.containers.wait.strategy.Wait; + +abstract class AbstractSpringJmsListenerTest { + static final Logger logger = LoggerFactory.getLogger(AbstractSpringJmsListenerTest.class); + + @RegisterExtension + static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + @RegisterExtension static final AutoCleanupExtension cleanup = AutoCleanupExtension.create(); + + static GenericContainer broker; + + @BeforeAll + static void setUp() { + broker = + new GenericContainer<>("quay.io/artemiscloud/activemq-artemis-broker:artemis.2.27.0") + .withEnv("AMQ_USER", "test") + .withEnv("AMQ_PASSWORD", "test") + .withEnv("JAVA_TOOL_OPTIONS", "-Dbrokerconfig.maxDiskUsage=-1") + .withExposedPorts(61616, 8161) + .waitingFor(Wait.forLogMessage(".*Server is now live.*", 1)) + .withStartupTimeout(Duration.ofMinutes(2)) + .withLogConsumer(new Slf4jLogConsumer(logger)); + broker.start(); + } + + @AfterAll + static void tearDown() { + if (broker != null) { + broker.close(); + } + } + + @ArgumentsSource(SpringJmsListenerTest.ConfigClasses.class) + @ParameterizedTest + @SuppressWarnings("unchecked") + void testSpringJmsListener(Class configClass) + throws ExecutionException, InterruptedException, TimeoutException { + // given + SpringApplication app = new SpringApplication(configClass); + app.setDefaultProperties(defaultConfig()); + ConfigurableApplicationContext applicationContext = app.run(); + cleanup.deferCleanup(applicationContext); + + JmsTemplate jmsTemplate = new JmsTemplate(applicationContext.getBean(ConnectionFactory.class)); + String message = "hello there"; + + // when + testing.runWithSpan("parent", () -> jmsTemplate.convertAndSend("spring-jms-listener", message)); + + // then + CompletableFuture receivedMessage = + applicationContext.getBean("receivedMessage", CompletableFuture.class); + assertThat(receivedMessage.get(10, TimeUnit.SECONDS)).isEqualTo(message); + + assertSpringJmsListener(); + } + + abstract void assertSpringJmsListener(); + + static Map defaultConfig() { + Map props = new HashMap<>(); + props.put("spring.jmx.enabled", false); + props.put("spring.main.web-application-type", "none"); + props.put("test.broker-url", "tcp://" + broker.getHost() + ":" + broker.getMappedPort(61616)); + return props; + } + + static final class ConfigClasses implements ArgumentsProvider { + + @Override + public Stream provideArguments(ExtensionContext context) { + return Stream.of( + arguments(AnnotatedListenerConfig.class), arguments(ManualListenerConfig.class)); + } + } +} diff --git a/instrumentation/spring/spring-jms/spring-jms-6.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/jms/v6_0/SpringJmsListenerTest.java b/instrumentation/spring/spring-jms/spring-jms-6.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/jms/v6_0/SpringJmsListenerTest.java index cdb4d8c05128..d0f50978b3f0 100644 --- a/instrumentation/spring/spring-jms/spring-jms-6.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/jms/v6_0/SpringJmsListenerTest.java +++ b/instrumentation/spring/spring-jms/spring-jms-6.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/jms/v6_0/SpringJmsListenerTest.java @@ -14,139 +14,80 @@ import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.satisfies; import static java.util.Collections.singletonList; import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.params.provider.Arguments.arguments; -import io.opentelemetry.instrumentation.testing.internal.AutoCleanupExtension; -import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; -import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.opentelemetry.sdk.trace.data.LinkData; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes; import jakarta.jms.ConnectionFactory; -import java.time.Duration; -import java.util.HashMap; -import java.util.Map; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; -import java.util.stream.Stream; +import java.util.concurrent.atomic.AtomicReference; import org.assertj.core.api.AbstractStringAssert; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.extension.ExtensionContext; -import org.junit.jupiter.api.extension.RegisterExtension; 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.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.boot.SpringApplication; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.jms.core.JmsTemplate; -import org.testcontainers.containers.GenericContainer; -import org.testcontainers.containers.output.Slf4jLogConsumer; -import org.testcontainers.containers.wait.strategy.Wait; -class SpringJmsListenerTest { - - static final Logger logger = LoggerFactory.getLogger(SpringJmsListenerTest.class); - - @RegisterExtension - static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); - - @RegisterExtension static final AutoCleanupExtension cleanup = AutoCleanupExtension.create(); - - static GenericContainer broker; - - @BeforeAll - static void setUp() { - broker = - new GenericContainer<>("quay.io/artemiscloud/activemq-artemis-broker:artemis.2.27.0") - .withEnv("AMQ_USER", "test") - .withEnv("AMQ_PASSWORD", "test") - .withEnv("JAVA_TOOL_OPTIONS", "-Dbrokerconfig.maxDiskUsage=-1") - .withExposedPorts(61616, 8161) - .waitingFor(Wait.forLogMessage(".*Server is now live.*", 1)) - .withStartupTimeout(Duration.ofMinutes(2)) - .withLogConsumer(new Slf4jLogConsumer(logger)); - broker.start(); - } - - @AfterAll - static void tearDown() { - if (broker != null) { - broker.close(); - } - } - - @ArgumentsSource(ConfigClasses.class) - @ParameterizedTest - @SuppressWarnings("unchecked") - void testSpringJmsListener(Class configClass) - throws ExecutionException, InterruptedException, TimeoutException { - // given - SpringApplication app = new SpringApplication(configClass); - app.setDefaultProperties(defaultConfig()); - ConfigurableApplicationContext applicationContext = app.run(); - cleanup.deferCleanup(applicationContext); - - JmsTemplate jmsTemplate = new JmsTemplate(applicationContext.getBean(ConnectionFactory.class)); - String message = "hello there"; - - // when - testing.runWithSpan("parent", () -> jmsTemplate.convertAndSend("spring-jms-listener", message)); - - // then - CompletableFuture receivedMessage = - applicationContext.getBean("receivedMessage", CompletableFuture.class); - assertThat(receivedMessage.get(10, TimeUnit.SECONDS)).isEqualTo(message); +class SpringJmsListenerTest extends AbstractSpringJmsListenerTest { + @Override + void assertSpringJmsListener() { + AtomicReference producerSpan = new AtomicReference<>(); testing.waitAndAssertSortedTraces( orderByRootSpanKind(INTERNAL, CONSUMER), + trace -> { + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasNoParent(), + span -> + span.hasName("spring-jms-listener publish") + .hasKind(PRODUCER) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo(MessagingIncubatingAttributes.MESSAGING_SYSTEM, "jms"), + equalTo( + MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME, + "spring-jms-listener"), + equalTo(MessagingIncubatingAttributes.MESSAGING_OPERATION, "publish"), + satisfies( + MessagingIncubatingAttributes.MESSAGING_MESSAGE_ID, + AbstractStringAssert::isNotBlank))); + + producerSpan.set(trace.getSpan(1)); + }, trace -> trace.hasSpansSatisfyingExactly( - span -> span.hasName("parent").hasNoParent(), span -> - span.hasName("spring-jms-listener send") - .hasKind(PRODUCER) - .hasParent(trace.getSpan(0)) + span.hasName("spring-jms-listener receive") + .hasKind(CONSUMER) + .hasNoParent() + .hasLinks(LinkData.create(producerSpan.get().getSpanContext())) .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.MESSAGING_SYSTEM, "jms"), + equalTo(MessagingIncubatingAttributes.MESSAGING_SYSTEM, "jms"), equalTo( - SemanticAttributes.MESSAGING_DESTINATION_NAME, + MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME, "spring-jms-listener"), + equalTo(MessagingIncubatingAttributes.MESSAGING_OPERATION, "receive"), satisfies( - SemanticAttributes.MESSAGING_MESSAGE_ID, + MessagingIncubatingAttributes.MESSAGING_MESSAGE_ID, AbstractStringAssert::isNotBlank)), span -> span.hasName("spring-jms-listener process") .hasKind(CONSUMER) - .hasParent(trace.getSpan(1)) + .hasParent(trace.getSpan(0)) + .hasLinks(LinkData.create(producerSpan.get().getSpanContext())) .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.MESSAGING_SYSTEM, "jms"), + equalTo(MessagingIncubatingAttributes.MESSAGING_SYSTEM, "jms"), equalTo( - SemanticAttributes.MESSAGING_DESTINATION_NAME, + MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME, "spring-jms-listener"), - equalTo(SemanticAttributes.MESSAGING_OPERATION, "process"), + equalTo(MessagingIncubatingAttributes.MESSAGING_OPERATION, "process"), satisfies( - SemanticAttributes.MESSAGING_MESSAGE_ID, + MessagingIncubatingAttributes.MESSAGING_MESSAGE_ID, AbstractStringAssert::isNotBlank)), - span -> span.hasName("consumer").hasParent(trace.getSpan(2))), - trace -> - trace.hasSpansSatisfyingExactly( - span -> - span.hasName("spring-jms-listener receive") - .hasKind(CONSUMER) - .hasNoParent() - .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.MESSAGING_SYSTEM, "jms"), - equalTo( - SemanticAttributes.MESSAGING_DESTINATION_NAME, - "spring-jms-listener"), - equalTo(SemanticAttributes.MESSAGING_OPERATION, "receive"), - satisfies( - SemanticAttributes.MESSAGING_MESSAGE_ID, - AbstractStringAssert::isNotBlank)))); + span -> span.hasName("consumer").hasParent(trace.getSpan(1)))); } @ArgumentsSource(ConfigClasses.class) @@ -187,35 +128,38 @@ void shouldCaptureHeaders(Class configClass) trace.hasSpansSatisfyingExactly( span -> span.hasName("parent").hasNoParent(), span -> - span.hasName("spring-jms-listener send") + span.hasName("spring-jms-listener publish") .hasKind(PRODUCER) .hasParent(trace.getSpan(0)) .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.MESSAGING_SYSTEM, "jms"), + equalTo(MessagingIncubatingAttributes.MESSAGING_SYSTEM, "jms"), equalTo( - SemanticAttributes.MESSAGING_DESTINATION_NAME, + MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME, "spring-jms-listener"), + equalTo(MessagingIncubatingAttributes.MESSAGING_OPERATION, "publish"), satisfies( - SemanticAttributes.MESSAGING_MESSAGE_ID, + MessagingIncubatingAttributes.MESSAGING_MESSAGE_ID, AbstractStringAssert::isNotBlank), equalTo( stringArrayKey("messaging.header.test_message_header"), singletonList("test")), equalTo( stringArrayKey("messaging.header.test_message_int_header"), - singletonList("1234"))), + singletonList("1234")))), + trace -> + trace.hasSpansSatisfyingExactly( span -> - span.hasName("spring-jms-listener process") + span.hasName("spring-jms-listener receive") .hasKind(CONSUMER) - .hasParent(trace.getSpan(1)) + .hasNoParent() .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.MESSAGING_SYSTEM, "jms"), + equalTo(MessagingIncubatingAttributes.MESSAGING_SYSTEM, "jms"), equalTo( - SemanticAttributes.MESSAGING_DESTINATION_NAME, + MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME, "spring-jms-listener"), - equalTo(SemanticAttributes.MESSAGING_OPERATION, "process"), + equalTo(MessagingIncubatingAttributes.MESSAGING_OPERATION, "receive"), satisfies( - SemanticAttributes.MESSAGING_MESSAGE_ID, + MessagingIncubatingAttributes.MESSAGING_MESSAGE_ID, AbstractStringAssert::isNotBlank), equalTo( stringArrayKey("messaging.header.test_message_header"), @@ -223,44 +167,25 @@ void shouldCaptureHeaders(Class configClass) equalTo( stringArrayKey("messaging.header.test_message_int_header"), singletonList("1234"))), - span -> span.hasName("consumer").hasParent(trace.getSpan(2))), - trace -> - trace.hasSpansSatisfyingExactly( span -> - span.hasName("spring-jms-listener receive") + span.hasName("spring-jms-listener process") .hasKind(CONSUMER) - .hasNoParent() + .hasParent(trace.getSpan(0)) .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.MESSAGING_SYSTEM, "jms"), + equalTo(MessagingIncubatingAttributes.MESSAGING_SYSTEM, "jms"), equalTo( - SemanticAttributes.MESSAGING_DESTINATION_NAME, + MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME, "spring-jms-listener"), - equalTo(SemanticAttributes.MESSAGING_OPERATION, "receive"), + equalTo(MessagingIncubatingAttributes.MESSAGING_OPERATION, "process"), satisfies( - SemanticAttributes.MESSAGING_MESSAGE_ID, + MessagingIncubatingAttributes.MESSAGING_MESSAGE_ID, AbstractStringAssert::isNotBlank), equalTo( stringArrayKey("messaging.header.test_message_header"), singletonList("test")), equalTo( stringArrayKey("messaging.header.test_message_int_header"), - singletonList("1234"))))); - } - - private static Map defaultConfig() { - Map props = new HashMap<>(); - props.put("spring.jmx.enabled", false); - props.put("spring.main.web-application-type", "none"); - props.put("test.broker-url", "tcp://localhost:" + broker.getMappedPort(61616)); - return props; - } - - static final class ConfigClasses implements ArgumentsProvider { - - @Override - public Stream provideArguments(ExtensionContext context) { - return Stream.of( - arguments(AnnotatedListenerConfig.class), arguments(ManualListenerConfig.class)); - } + singletonList("1234"))), + span -> span.hasName("consumer").hasParent(trace.getSpan(1)))); } } diff --git a/instrumentation/spring/spring-jms/spring-jms-6.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/jms/v6_0/SpringListenerSuppressReceiveSpansTest.java b/instrumentation/spring/spring-jms/spring-jms-6.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/jms/v6_0/SpringListenerSuppressReceiveSpansTest.java new file mode 100644 index 000000000000..043cf148ab9b --- /dev/null +++ b/instrumentation/spring/spring-jms/spring-jms-6.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/jms/v6_0/SpringListenerSuppressReceiveSpansTest.java @@ -0,0 +1,53 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.jms.v6_0; + +import static io.opentelemetry.api.trace.SpanKind.CONSUMER; +import static io.opentelemetry.api.trace.SpanKind.PRODUCER; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.satisfies; + +import io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes; +import org.assertj.core.api.AbstractStringAssert; + +class SpringListenerSuppressReceiveSpansTest extends AbstractSpringJmsListenerTest { + + @Override + void assertSpringJmsListener() { + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasNoParent(), + span -> + span.hasName("spring-jms-listener publish") + .hasKind(PRODUCER) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo(MessagingIncubatingAttributes.MESSAGING_SYSTEM, "jms"), + equalTo( + MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME, + "spring-jms-listener"), + equalTo(MessagingIncubatingAttributes.MESSAGING_OPERATION, "publish"), + satisfies( + MessagingIncubatingAttributes.MESSAGING_MESSAGE_ID, + AbstractStringAssert::isNotBlank)), + span -> + span.hasName("spring-jms-listener process") + .hasKind(CONSUMER) + .hasParent(trace.getSpan(1)) + .hasTotalRecordedLinks(0) + .hasAttributesSatisfyingExactly( + equalTo(MessagingIncubatingAttributes.MESSAGING_SYSTEM, "jms"), + equalTo( + MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME, + "spring-jms-listener"), + equalTo(MessagingIncubatingAttributes.MESSAGING_OPERATION, "process"), + satisfies( + MessagingIncubatingAttributes.MESSAGING_MESSAGE_ID, + AbstractStringAssert::isNotBlank)), + span -> span.hasName("consumer").hasParent(trace.getSpan(2)))); + } +} diff --git a/instrumentation/spring/spring-kafka-2.7/javaagent/build.gradle.kts b/instrumentation/spring/spring-kafka-2.7/javaagent/build.gradle.kts index ebd165acfebf..c4e08e587c53 100644 --- a/instrumentation/spring/spring-kafka-2.7/javaagent/build.gradle.kts +++ b/instrumentation/spring/spring-kafka-2.7/javaagent/build.gradle.kts @@ -69,6 +69,7 @@ tasks { test { usesService(gradle.sharedServices.registrations["testcontainersBuildService"].service) + systemProperty("testLatestDeps", findProperty("testLatestDeps") as Boolean) jvmArgs("-Dotel.instrumentation.kafka.experimental-span-attributes=true") jvmArgs("-Dotel.instrumentation.messaging.experimental.receive-telemetry.enabled=true") } diff --git a/instrumentation/spring/spring-kafka-2.7/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/kafka/v2_7/AbstractMessageListenerContainerInstrumentation.java b/instrumentation/spring/spring-kafka-2.7/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/kafka/v2_7/AbstractMessageListenerContainerInstrumentation.java index 1b65c059fee0..a80a510990cd 100644 --- a/instrumentation/spring/spring-kafka-2.7/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/kafka/v2_7/AbstractMessageListenerContainerInstrumentation.java +++ b/instrumentation/spring/spring-kafka-2.7/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/kafka/v2_7/AbstractMessageListenerContainerInstrumentation.java @@ -16,7 +16,6 @@ import net.bytebuddy.asm.Advice; import net.bytebuddy.description.type.TypeDescription; import net.bytebuddy.matcher.ElementMatcher; -import org.springframework.kafka.listener.BatchInterceptor; import org.springframework.kafka.listener.RecordInterceptor; public class AbstractMessageListenerContainerInstrumentation implements TypeInstrumentation { @@ -28,16 +27,11 @@ public ElementMatcher typeMatcher() { @Override public void transform(TypeTransformer transformer) { - // getBatchInterceptor() is called internally by AbstractMessageListenerContainer - // implementations - transformer.applyAdviceToMethod( - named("getBatchInterceptor") - .and(isProtected()) - .and(takesArguments(0)) - .and(returns(named("org.springframework.kafka.listener.BatchInterceptor"))), - this.getClass().getName() + "$GetBatchInterceptorAdvice"); // getRecordInterceptor() is called internally by AbstractMessageListenerContainer // implementations + // for batch listeners we don't instrument getBatchInterceptor() here but instead instrument + // KafkaMessageListenerContainer$ListenerConsumer because spring doesn't always call the success + // and failure methods on a batch interceptor transformer.applyAdviceToMethod( named("getRecordInterceptor") .and(isProtected()) @@ -46,24 +40,6 @@ public void transform(TypeTransformer transformer) { this.getClass().getName() + "$GetRecordInterceptorAdvice"); } - @SuppressWarnings("unused") - public static class GetBatchInterceptorAdvice { - - @Advice.OnMethodExit(suppress = Throwable.class) - public static void onExit( - @Advice.Return(readOnly = false) BatchInterceptor interceptor) { - - if (interceptor == null - || !interceptor - .getClass() - .getName() - .equals( - "io.opentelemetry.instrumentation.spring.kafka.v2_7.InstrumentedBatchInterceptor")) { - interceptor = telemetry().createBatchInterceptor(interceptor); - } - } - } - @SuppressWarnings("unused") public static class GetRecordInterceptorAdvice { diff --git a/instrumentation/spring/spring-kafka-2.7/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/kafka/v2_7/ListenerConsumerInstrumentation.java b/instrumentation/spring/spring-kafka-2.7/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/kafka/v2_7/ListenerConsumerInstrumentation.java index 48c40f89aee8..9e9900f468d4 100644 --- a/instrumentation/spring/spring-kafka-2.7/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/kafka/v2_7/ListenerConsumerInstrumentation.java +++ b/instrumentation/spring/spring-kafka-2.7/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/kafka/v2_7/ListenerConsumerInstrumentation.java @@ -5,9 +5,16 @@ package io.opentelemetry.javaagent.instrumentation.spring.kafka.v2_7; +import static io.opentelemetry.javaagent.instrumentation.spring.kafka.v2_7.SpringKafkaSingletons.batchProcessInstrumenter; import static net.bytebuddy.matcher.ElementMatchers.isConstructor; 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.kafka.internal.KafkaConsumerContext; +import io.opentelemetry.instrumentation.kafka.internal.KafkaConsumerContextUtil; +import io.opentelemetry.instrumentation.kafka.internal.KafkaReceiveRequest; import io.opentelemetry.javaagent.bootstrap.kafka.KafkaClientsConsumerProcessTracing; import io.opentelemetry.javaagent.bootstrap.spring.SpringSchedulingTaskTracing; import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; @@ -15,6 +22,8 @@ import net.bytebuddy.asm.Advice; import net.bytebuddy.description.type.TypeDescription; import net.bytebuddy.matcher.ElementMatcher; +import org.apache.kafka.clients.consumer.Consumer; +import org.apache.kafka.clients.consumer.ConsumerRecords; public class ListenerConsumerInstrumentation implements TypeInstrumentation { @@ -29,6 +38,10 @@ public void transform(TypeTransformer transformer) { transformer.applyAdviceToMethod(named("run"), this.getClass().getName() + "$RunLoopAdvice"); transformer.applyAdviceToMethod( isConstructor(), this.getClass().getName() + "$ConstructorAdvice"); + transformer.applyAdviceToMethod( + named("invokeBatchOnMessageWithRecordsOrList") + .and(takesArgument(0, named("org.apache.kafka.clients.consumer.ConsumerRecords"))), + this.getClass().getName() + "$InvokeBatchAdvice"); } // this advice suppresses the CONSUMER spans created by the kafka-clients instrumentation @@ -60,4 +73,41 @@ public static void onExit(@Advice.Enter boolean previousValue) { SpringSchedulingTaskTracing.setEnabled(previousValue); } } + + @SuppressWarnings("unused") + public static class InvokeBatchAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter( + @Advice.Argument(0) ConsumerRecords records, + @Advice.FieldValue("consumer") Consumer consumer, + @Advice.Local("otelRequest") KafkaReceiveRequest request, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + KafkaConsumerContext consumerContext = KafkaConsumerContextUtil.get(records); + Context receiveContext = consumerContext.getContext(); + + // use the receive CONSUMER span as parent if it's available + Context parentContext = receiveContext != null ? receiveContext : Context.current(); + + request = KafkaReceiveRequest.create(records, consumer); + if (batchProcessInstrumenter().shouldStart(parentContext, request)) { + context = batchProcessInstrumenter().start(parentContext, request); + scope = context.makeCurrent(); + } + } + + @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class) + public static void onExit( + @Advice.Thrown Throwable throwable, + @Advice.Local("otelRequest") KafkaReceiveRequest request, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + if (scope == null) { + return; + } + scope.close(); + batchProcessInstrumenter().end(context, request, null, throwable); + } + } } diff --git a/instrumentation/spring/spring-kafka-2.7/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/kafka/v2_7/SpringKafkaSingletons.java b/instrumentation/spring/spring-kafka-2.7/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/kafka/v2_7/SpringKafkaSingletons.java index 89f2e63a86de..fcf6f2d164b2 100644 --- a/instrumentation/spring/spring-kafka-2.7/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/kafka/v2_7/SpringKafkaSingletons.java +++ b/instrumentation/spring/spring-kafka-2.7/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/kafka/v2_7/SpringKafkaSingletons.java @@ -6,24 +6,48 @@ package io.opentelemetry.javaagent.instrumentation.spring.kafka.v2_7; import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import io.opentelemetry.instrumentation.kafka.internal.KafkaInstrumenterFactory; +import io.opentelemetry.instrumentation.kafka.internal.KafkaReceiveRequest; import io.opentelemetry.instrumentation.spring.kafka.v2_7.SpringKafkaTelemetry; +import io.opentelemetry.instrumentation.spring.kafka.v2_7.internal.SpringKafkaErrorCauseExtractor; +import io.opentelemetry.javaagent.bootstrap.internal.AgentInstrumentationConfig; import io.opentelemetry.javaagent.bootstrap.internal.ExperimentalConfig; -import io.opentelemetry.javaagent.bootstrap.internal.InstrumentationConfig; public final class SpringKafkaSingletons { + private static final String INSTRUMENTATION_NAME = "io.opentelemetry.spring-kafka-2.7"; private static final SpringKafkaTelemetry TELEMETRY = SpringKafkaTelemetry.builder(GlobalOpenTelemetry.get()) + .setCapturedHeaders(ExperimentalConfig.get().getMessagingHeaders()) .setCaptureExperimentalSpanAttributes( - InstrumentationConfig.get() + AgentInstrumentationConfig.get() .getBoolean("otel.instrumentation.kafka.experimental-span-attributes", false)) .setMessagingReceiveInstrumentationEnabled( ExperimentalConfig.get().messagingReceiveInstrumentationEnabled()) .build(); + private static final Instrumenter BATCH_PROCESS_INSTRUMENTER; + + static { + KafkaInstrumenterFactory factory = + new KafkaInstrumenterFactory(GlobalOpenTelemetry.get(), INSTRUMENTATION_NAME) + .setCapturedHeaders(ExperimentalConfig.get().getMessagingHeaders()) + .setCaptureExperimentalSpanAttributes( + AgentInstrumentationConfig.get() + .getBoolean("otel.instrumentation.kafka.experimental-span-attributes", false)) + .setMessagingReceiveInstrumentationEnabled( + ExperimentalConfig.get().messagingReceiveInstrumentationEnabled()) + .setErrorCauseExtractor(SpringKafkaErrorCauseExtractor.INSTANCE); + BATCH_PROCESS_INSTRUMENTER = factory.createBatchProcessInstrumenter(); + } public static SpringKafkaTelemetry telemetry() { return TELEMETRY; } + public static Instrumenter batchProcessInstrumenter() { + return BATCH_PROCESS_INSTRUMENTER; + } + private SpringKafkaSingletons() {} } diff --git a/instrumentation/spring/spring-kafka-2.7/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/kafka/v2_7/SpringKafkaTest.java b/instrumentation/spring/spring-kafka-2.7/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/kafka/v2_7/SpringKafkaTest.java index f07f5b172350..edf932d59af9 100644 --- a/instrumentation/spring/spring-kafka-2.7/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/kafka/v2_7/SpringKafkaTest.java +++ b/instrumentation/spring/spring-kafka-2.7/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/kafka/v2_7/SpringKafkaTest.java @@ -14,16 +14,23 @@ import io.opentelemetry.api.trace.SpanKind; import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.sdk.testing.assertj.AttributeAssertion; +import io.opentelemetry.sdk.testing.assertj.SpanDataAssert; +import io.opentelemetry.sdk.testing.assertj.TraceAssert; 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 io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes; import io.opentelemetry.testing.AbstractSpringKafkaTest; +import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; import org.assertj.core.api.AbstractLongAssert; +import org.assertj.core.api.AbstractStringAssert; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; @@ -62,21 +69,24 @@ void shouldCreateSpansForSingleRecordProcess() { trace.hasSpansSatisfyingExactly( span -> span.hasName("producer"), span -> - span.hasName("testSingleTopic send") + span.hasName("testSingleTopic publish") .hasKind(SpanKind.PRODUCER) .hasParent(trace.getSpan(0)) .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.MESSAGING_SYSTEM, "kafka"), - equalTo(SemanticAttributes.MESSAGING_DESTINATION_NAME, "testSingleTopic"), + equalTo(MessagingIncubatingAttributes.MESSAGING_SYSTEM, "kafka"), + equalTo( + MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME, + "testSingleTopic"), + equalTo(MessagingIncubatingAttributes.MESSAGING_OPERATION, "publish"), satisfies( - SemanticAttributes.MESSAGING_KAFKA_DESTINATION_PARTITION, - AbstractLongAssert::isNotNegative), + MessagingIncubatingAttributes.MESSAGING_DESTINATION_PARTITION_ID, + AbstractStringAssert::isNotEmpty), satisfies( - SemanticAttributes.MESSAGING_KAFKA_MESSAGE_OFFSET, + MessagingIncubatingAttributes.MESSAGING_KAFKA_MESSAGE_OFFSET, AbstractLongAssert::isNotNegative), - equalTo(SemanticAttributes.MESSAGING_KAFKA_MESSAGE_KEY, "10"), + equalTo(MessagingIncubatingAttributes.MESSAGING_KAFKA_MESSAGE_KEY, "10"), satisfies( - SemanticAttributes.MESSAGING_KAFKA_CLIENT_ID, + MessagingIncubatingAttributes.MESSAGING_CLIENT_ID, stringAssert -> stringAssert.startsWith("producer")))); producer.set(trace.getSpan(1)); @@ -88,50 +98,47 @@ void shouldCreateSpansForSingleRecordProcess() { .hasKind(SpanKind.CONSUMER) .hasNoParent() .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.MESSAGING_SYSTEM, "kafka"), + equalTo(MessagingIncubatingAttributes.MESSAGING_SYSTEM, "kafka"), equalTo( - SemanticAttributes.MESSAGING_DESTINATION_NAME, "testSingleTopic"), - equalTo(SemanticAttributes.MESSAGING_OPERATION, "receive"), + MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME, + "testSingleTopic"), + equalTo(MessagingIncubatingAttributes.MESSAGING_OPERATION, "receive"), equalTo( - SemanticAttributes.MESSAGING_KAFKA_CONSUMER_GROUP, + MessagingIncubatingAttributes.MESSAGING_KAFKA_CONSUMER_GROUP, "testSingleListener"), satisfies( - SemanticAttributes.MESSAGING_KAFKA_CLIENT_ID, + MessagingIncubatingAttributes.MESSAGING_CLIENT_ID, stringAssert -> stringAssert.startsWith("consumer")), - satisfies( - SemanticAttributes.MESSAGING_CONSUMER_ID, - stringAssert -> - stringAssert.startsWith("testSingleListener - consumer"))), + equalTo( + MessagingIncubatingAttributes.MESSAGING_BATCH_MESSAGE_COUNT, 1)), span -> span.hasName("testSingleTopic process") .hasKind(SpanKind.CONSUMER) .hasParent(trace.getSpan(0)) .hasLinks(LinkData.create(producer.get().getSpanContext())) .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.MESSAGING_SYSTEM, "kafka"), + equalTo(MessagingIncubatingAttributes.MESSAGING_SYSTEM, "kafka"), equalTo( - SemanticAttributes.MESSAGING_DESTINATION_NAME, "testSingleTopic"), - equalTo(SemanticAttributes.MESSAGING_OPERATION, "process"), + MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME, + "testSingleTopic"), + equalTo(MessagingIncubatingAttributes.MESSAGING_OPERATION, "process"), satisfies( - SemanticAttributes.MESSAGING_MESSAGE_PAYLOAD_SIZE_BYTES, + MessagingIncubatingAttributes.MESSAGING_MESSAGE_BODY_SIZE, AbstractLongAssert::isNotNegative), satisfies( - SemanticAttributes.MESSAGING_KAFKA_SOURCE_PARTITION, - AbstractLongAssert::isNotNegative), + MessagingIncubatingAttributes.MESSAGING_DESTINATION_PARTITION_ID, + AbstractStringAssert::isNotEmpty), satisfies( - SemanticAttributes.MESSAGING_KAFKA_MESSAGE_OFFSET, + MessagingIncubatingAttributes.MESSAGING_KAFKA_MESSAGE_OFFSET, AbstractLongAssert::isNotNegative), - equalTo(SemanticAttributes.MESSAGING_KAFKA_MESSAGE_KEY, "10"), equalTo( - SemanticAttributes.MESSAGING_KAFKA_CONSUMER_GROUP, + MessagingIncubatingAttributes.MESSAGING_KAFKA_MESSAGE_KEY, "10"), + equalTo( + MessagingIncubatingAttributes.MESSAGING_KAFKA_CONSUMER_GROUP, "testSingleListener"), satisfies( - SemanticAttributes.MESSAGING_KAFKA_CLIENT_ID, + MessagingIncubatingAttributes.MESSAGING_CLIENT_ID, stringAssert -> stringAssert.startsWith("consumer")), - satisfies( - SemanticAttributes.MESSAGING_CONSUMER_ID, - stringAssert -> - stringAssert.startsWith("testSingleListener - consumer")), satisfies( longKey("kafka.record.queue_time_ms"), AbstractLongAssert::isNotNegative)), @@ -150,54 +157,90 @@ void shouldHandleFailureInKafkaListener() { }); }); - AtomicReference producer = new AtomicReference<>(); + Consumer receiveSpanAssert = + span -> + span.hasName("testSingleTopic receive") + .hasKind(SpanKind.CONSUMER) + .hasNoParent() + .hasAttributesSatisfyingExactly( + equalTo(MessagingIncubatingAttributes.MESSAGING_SYSTEM, "kafka"), + equalTo( + MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME, + "testSingleTopic"), + equalTo(MessagingIncubatingAttributes.MESSAGING_OPERATION, "receive"), + equalTo( + MessagingIncubatingAttributes.MESSAGING_KAFKA_CONSUMER_GROUP, + "testSingleListener"), + satisfies( + MessagingIncubatingAttributes.MESSAGING_CLIENT_ID, + stringAssert -> stringAssert.startsWith("consumer")), + equalTo(MessagingIncubatingAttributes.MESSAGING_BATCH_MESSAGE_COUNT, 1)); + List processAttributes = + Arrays.asList( + equalTo(MessagingIncubatingAttributes.MESSAGING_SYSTEM, "kafka"), + equalTo(MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME, "testSingleTopic"), + equalTo(MessagingIncubatingAttributes.MESSAGING_OPERATION, "process"), + satisfies( + MessagingIncubatingAttributes.MESSAGING_MESSAGE_BODY_SIZE, + AbstractLongAssert::isNotNegative), + satisfies( + MessagingIncubatingAttributes.MESSAGING_DESTINATION_PARTITION_ID, + AbstractStringAssert::isNotEmpty), + satisfies( + MessagingIncubatingAttributes.MESSAGING_KAFKA_MESSAGE_OFFSET, + AbstractLongAssert::isNotNegative), + equalTo(MessagingIncubatingAttributes.MESSAGING_KAFKA_MESSAGE_KEY, "10"), + equalTo( + MessagingIncubatingAttributes.MESSAGING_KAFKA_CONSUMER_GROUP, "testSingleListener"), + satisfies( + MessagingIncubatingAttributes.MESSAGING_CLIENT_ID, + stringAssert -> stringAssert.startsWith("consumer")), + satisfies(longKey("kafka.record.queue_time_ms"), AbstractLongAssert::isNotNegative)); + AtomicReference producer = new AtomicReference<>(); testing.waitAndAssertSortedTraces( orderByRootSpanKind(SpanKind.INTERNAL, SpanKind.CONSUMER), trace -> { trace.hasSpansSatisfyingExactly( span -> span.hasName("producer"), span -> - span.hasName("testSingleTopic send") + span.hasName("testSingleTopic publish") .hasKind(SpanKind.PRODUCER) .hasParent(trace.getSpan(0)) .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.MESSAGING_SYSTEM, "kafka"), - equalTo(SemanticAttributes.MESSAGING_DESTINATION_NAME, "testSingleTopic"), + equalTo(MessagingIncubatingAttributes.MESSAGING_SYSTEM, "kafka"), + equalTo( + MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME, + "testSingleTopic"), + equalTo(MessagingIncubatingAttributes.MESSAGING_OPERATION, "publish"), satisfies( - SemanticAttributes.MESSAGING_KAFKA_DESTINATION_PARTITION, - AbstractLongAssert::isNotNegative), + MessagingIncubatingAttributes.MESSAGING_DESTINATION_PARTITION_ID, + AbstractStringAssert::isNotEmpty), satisfies( - SemanticAttributes.MESSAGING_KAFKA_MESSAGE_OFFSET, + MessagingIncubatingAttributes.MESSAGING_KAFKA_MESSAGE_OFFSET, AbstractLongAssert::isNotNegative), - equalTo(SemanticAttributes.MESSAGING_KAFKA_MESSAGE_KEY, "10"), + equalTo(MessagingIncubatingAttributes.MESSAGING_KAFKA_MESSAGE_KEY, "10"), satisfies( - SemanticAttributes.MESSAGING_KAFKA_CLIENT_ID, + MessagingIncubatingAttributes.MESSAGING_CLIENT_ID, stringAssert -> stringAssert.startsWith("producer")))); producer.set(trace.getSpan(1)); }, trace -> trace.hasSpansSatisfyingExactly( + receiveSpanAssert, span -> - span.hasName("testSingleTopic receive") + span.hasName("testSingleTopic process") .hasKind(SpanKind.CONSUMER) - .hasNoParent() - .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.MESSAGING_SYSTEM, "kafka"), - equalTo( - SemanticAttributes.MESSAGING_DESTINATION_NAME, "testSingleTopic"), - equalTo(SemanticAttributes.MESSAGING_OPERATION, "receive"), - equalTo( - SemanticAttributes.MESSAGING_KAFKA_CONSUMER_GROUP, - "testSingleListener"), - satisfies( - SemanticAttributes.MESSAGING_KAFKA_CLIENT_ID, - stringAssert -> stringAssert.startsWith("consumer")), - satisfies( - SemanticAttributes.MESSAGING_CONSUMER_ID, - stringAssert -> - stringAssert.startsWith("testSingleListener - consumer"))), + .hasParent(trace.getSpan(0)) + .hasLinks(LinkData.create(producer.get().getSpanContext())) + .hasStatus(StatusData.error()) + .hasException(new IllegalArgumentException("boom")) + .hasAttributesSatisfyingExactly(processAttributes), + span -> span.hasName("consumer").hasParent(trace.getSpan(1))), + trace -> + trace.hasSpansSatisfyingExactly( + receiveSpanAssert, span -> span.hasName("testSingleTopic process") .hasKind(SpanKind.CONSUMER) @@ -205,34 +248,17 @@ void shouldHandleFailureInKafkaListener() { .hasLinks(LinkData.create(producer.get().getSpanContext())) .hasStatus(StatusData.error()) .hasException(new IllegalArgumentException("boom")) - .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.MESSAGING_SYSTEM, "kafka"), - equalTo( - SemanticAttributes.MESSAGING_DESTINATION_NAME, "testSingleTopic"), - equalTo(SemanticAttributes.MESSAGING_OPERATION, "process"), - satisfies( - SemanticAttributes.MESSAGING_MESSAGE_PAYLOAD_SIZE_BYTES, - AbstractLongAssert::isNotNegative), - satisfies( - SemanticAttributes.MESSAGING_KAFKA_SOURCE_PARTITION, - AbstractLongAssert::isNotNegative), - satisfies( - SemanticAttributes.MESSAGING_KAFKA_MESSAGE_OFFSET, - AbstractLongAssert::isNotNegative), - equalTo(SemanticAttributes.MESSAGING_KAFKA_MESSAGE_KEY, "10"), - equalTo( - SemanticAttributes.MESSAGING_KAFKA_CONSUMER_GROUP, - "testSingleListener"), - satisfies( - SemanticAttributes.MESSAGING_KAFKA_CLIENT_ID, - stringAssert -> stringAssert.startsWith("consumer")), - satisfies( - SemanticAttributes.MESSAGING_CONSUMER_ID, - stringAssert -> - stringAssert.startsWith("testSingleListener - consumer")), - satisfies( - longKey("kafka.record.queue_time_ms"), - AbstractLongAssert::isNotNegative)), + .hasAttributesSatisfyingExactly(processAttributes), + span -> span.hasName("consumer").hasParent(trace.getSpan(1))), + trace -> + trace.hasSpansSatisfyingExactly( + receiveSpanAssert, + span -> + span.hasName("testSingleTopic process") + .hasKind(SpanKind.CONSUMER) + .hasParent(trace.getSpan(0)) + .hasLinks(LinkData.create(producer.get().getSpanContext())) + .hasAttributesSatisfyingExactly(processAttributes), span -> span.hasName("consumer").hasParent(trace.getSpan(1)))); } @@ -252,38 +278,44 @@ void shouldCreateSpansForBatchReceiveAndProcess() throws InterruptedException { trace.hasSpansSatisfyingExactlyInAnyOrder( span -> span.hasName("producer"), span -> - span.hasName("testBatchTopic send") + span.hasName("testBatchTopic publish") .hasKind(SpanKind.PRODUCER) .hasParent(trace.getSpan(0)) .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.MESSAGING_SYSTEM, "kafka"), - equalTo(SemanticAttributes.MESSAGING_DESTINATION_NAME, "testBatchTopic"), + equalTo(MessagingIncubatingAttributes.MESSAGING_SYSTEM, "kafka"), + equalTo( + MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME, + "testBatchTopic"), + equalTo(MessagingIncubatingAttributes.MESSAGING_OPERATION, "publish"), satisfies( - SemanticAttributes.MESSAGING_KAFKA_DESTINATION_PARTITION, - AbstractLongAssert::isNotNegative), + MessagingIncubatingAttributes.MESSAGING_DESTINATION_PARTITION_ID, + AbstractStringAssert::isNotEmpty), satisfies( - SemanticAttributes.MESSAGING_KAFKA_MESSAGE_OFFSET, + MessagingIncubatingAttributes.MESSAGING_KAFKA_MESSAGE_OFFSET, AbstractLongAssert::isNotNegative), - equalTo(SemanticAttributes.MESSAGING_KAFKA_MESSAGE_KEY, "10"), + equalTo(MessagingIncubatingAttributes.MESSAGING_KAFKA_MESSAGE_KEY, "10"), satisfies( - SemanticAttributes.MESSAGING_KAFKA_CLIENT_ID, + MessagingIncubatingAttributes.MESSAGING_CLIENT_ID, stringAssert -> stringAssert.startsWith("producer"))), span -> - span.hasName("testBatchTopic send") + span.hasName("testBatchTopic publish") .hasKind(SpanKind.PRODUCER) .hasParent(trace.getSpan(0)) .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.MESSAGING_SYSTEM, "kafka"), - equalTo(SemanticAttributes.MESSAGING_DESTINATION_NAME, "testBatchTopic"), + equalTo(MessagingIncubatingAttributes.MESSAGING_SYSTEM, "kafka"), + equalTo( + MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME, + "testBatchTopic"), + equalTo(MessagingIncubatingAttributes.MESSAGING_OPERATION, "publish"), satisfies( - SemanticAttributes.MESSAGING_KAFKA_DESTINATION_PARTITION, - AbstractLongAssert::isNotNegative), + MessagingIncubatingAttributes.MESSAGING_DESTINATION_PARTITION_ID, + AbstractStringAssert::isNotEmpty), satisfies( - SemanticAttributes.MESSAGING_KAFKA_MESSAGE_OFFSET, + MessagingIncubatingAttributes.MESSAGING_KAFKA_MESSAGE_OFFSET, AbstractLongAssert::isNotNegative), - equalTo(SemanticAttributes.MESSAGING_KAFKA_MESSAGE_KEY, "20"), + equalTo(MessagingIncubatingAttributes.MESSAGING_KAFKA_MESSAGE_KEY, "20"), satisfies( - SemanticAttributes.MESSAGING_KAFKA_CLIENT_ID, + MessagingIncubatingAttributes.MESSAGING_CLIENT_ID, stringAssert -> stringAssert.startsWith("producer")))); producer1.set(trace.getSpan(1)); @@ -296,20 +328,19 @@ void shouldCreateSpansForBatchReceiveAndProcess() throws InterruptedException { .hasKind(SpanKind.CONSUMER) .hasNoParent() .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.MESSAGING_SYSTEM, "kafka"), + equalTo(MessagingIncubatingAttributes.MESSAGING_SYSTEM, "kafka"), equalTo( - SemanticAttributes.MESSAGING_DESTINATION_NAME, "testBatchTopic"), - equalTo(SemanticAttributes.MESSAGING_OPERATION, "receive"), + MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME, + "testBatchTopic"), + equalTo(MessagingIncubatingAttributes.MESSAGING_OPERATION, "receive"), equalTo( - SemanticAttributes.MESSAGING_KAFKA_CONSUMER_GROUP, + MessagingIncubatingAttributes.MESSAGING_KAFKA_CONSUMER_GROUP, "testBatchListener"), satisfies( - SemanticAttributes.MESSAGING_KAFKA_CLIENT_ID, + MessagingIncubatingAttributes.MESSAGING_CLIENT_ID, stringAssert -> stringAssert.startsWith("consumer")), - satisfies( - SemanticAttributes.MESSAGING_CONSUMER_ID, - stringAssert -> - stringAssert.startsWith("testBatchListener - consumer"))), + equalTo( + MessagingIncubatingAttributes.MESSAGING_BATCH_MESSAGE_COUNT, 2)), span -> span.hasName("testBatchTopic process") .hasKind(SpanKind.CONSUMER) @@ -318,20 +349,19 @@ void shouldCreateSpansForBatchReceiveAndProcess() throws InterruptedException { LinkData.create(producer1.get().getSpanContext()), LinkData.create(producer2.get().getSpanContext())) .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.MESSAGING_SYSTEM, "kafka"), + equalTo(MessagingIncubatingAttributes.MESSAGING_SYSTEM, "kafka"), equalTo( - SemanticAttributes.MESSAGING_DESTINATION_NAME, "testBatchTopic"), - equalTo(SemanticAttributes.MESSAGING_OPERATION, "process"), + MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME, + "testBatchTopic"), + equalTo(MessagingIncubatingAttributes.MESSAGING_OPERATION, "process"), equalTo( - SemanticAttributes.MESSAGING_KAFKA_CONSUMER_GROUP, + MessagingIncubatingAttributes.MESSAGING_KAFKA_CONSUMER_GROUP, "testBatchListener"), satisfies( - SemanticAttributes.MESSAGING_KAFKA_CLIENT_ID, + MessagingIncubatingAttributes.MESSAGING_CLIENT_ID, stringAssert -> stringAssert.startsWith("consumer")), - satisfies( - SemanticAttributes.MESSAGING_CONSUMER_ID, - stringAssert -> - stringAssert.startsWith("testBatchListener - consumer"))), + equalTo( + MessagingIncubatingAttributes.MESSAGING_BATCH_MESSAGE_COUNT, 2)), span -> span.hasName("consumer").hasParent(trace.getSpan(1)))); } @@ -349,74 +379,105 @@ void shouldHandleFailureInKafkaBatchListener() { AtomicReference producer = new AtomicReference<>(); - testing.waitAndAssertSortedTraces( - orderByRootSpanKind(SpanKind.INTERNAL, SpanKind.CONSUMER), + List> assertions = new ArrayList<>(); + assertions.add( trace -> { trace.hasSpansSatisfyingExactly( span -> span.hasName("producer"), span -> - span.hasName("testBatchTopic send") + span.hasName("testBatchTopic publish") .hasKind(SpanKind.PRODUCER) .hasParent(trace.getSpan(0)) .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.MESSAGING_SYSTEM, "kafka"), - equalTo(SemanticAttributes.MESSAGING_DESTINATION_NAME, "testBatchTopic"), + equalTo(MessagingIncubatingAttributes.MESSAGING_SYSTEM, "kafka"), + equalTo( + MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME, + "testBatchTopic"), + equalTo(MessagingIncubatingAttributes.MESSAGING_OPERATION, "publish"), satisfies( - SemanticAttributes.MESSAGING_KAFKA_DESTINATION_PARTITION, - AbstractLongAssert::isNotNegative), + MessagingIncubatingAttributes.MESSAGING_DESTINATION_PARTITION_ID, + AbstractStringAssert::isNotEmpty), satisfies( - SemanticAttributes.MESSAGING_KAFKA_MESSAGE_OFFSET, + MessagingIncubatingAttributes.MESSAGING_KAFKA_MESSAGE_OFFSET, AbstractLongAssert::isNotNegative), - equalTo(SemanticAttributes.MESSAGING_KAFKA_MESSAGE_KEY, "10"), + equalTo(MessagingIncubatingAttributes.MESSAGING_KAFKA_MESSAGE_KEY, "10"), satisfies( - SemanticAttributes.MESSAGING_KAFKA_CLIENT_ID, + MessagingIncubatingAttributes.MESSAGING_CLIENT_ID, stringAssert -> stringAssert.startsWith("producer")))); producer.set(trace.getSpan(1)); - }, - trace -> - trace.hasSpansSatisfyingExactly( - span -> - span.hasName("testBatchTopic receive") - .hasKind(SpanKind.CONSUMER) - .hasNoParent() - .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.MESSAGING_SYSTEM, "kafka"), - equalTo( - SemanticAttributes.MESSAGING_DESTINATION_NAME, "testBatchTopic"), - equalTo(SemanticAttributes.MESSAGING_OPERATION, "receive"), - equalTo( - SemanticAttributes.MESSAGING_KAFKA_CONSUMER_GROUP, - "testBatchListener"), - satisfies( - SemanticAttributes.MESSAGING_KAFKA_CLIENT_ID, - stringAssert -> stringAssert.startsWith("consumer")), - satisfies( - SemanticAttributes.MESSAGING_CONSUMER_ID, - stringAssert -> - stringAssert.startsWith("testBatchListener - consumer"))), - span -> - span.hasName("testBatchTopic process") - .hasKind(SpanKind.CONSUMER) - .hasParent(trace.getSpan(0)) - .hasLinks(LinkData.create(producer.get().getSpanContext())) - .hasStatus(StatusData.error()) - .hasException(new IllegalArgumentException("boom")) - .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.MESSAGING_SYSTEM, "kafka"), - equalTo( - SemanticAttributes.MESSAGING_DESTINATION_NAME, "testBatchTopic"), - equalTo(SemanticAttributes.MESSAGING_OPERATION, "process"), - equalTo( - SemanticAttributes.MESSAGING_KAFKA_CONSUMER_GROUP, - "testBatchListener"), - satisfies( - SemanticAttributes.MESSAGING_KAFKA_CLIENT_ID, - stringAssert -> stringAssert.startsWith("consumer")), - satisfies( - SemanticAttributes.MESSAGING_CONSUMER_ID, - stringAssert -> - stringAssert.startsWith("testBatchListener - consumer"))), - span -> span.hasName("consumer").hasParent(trace.getSpan(1)))); + }); + + if (Boolean.getBoolean("testLatestDeps")) { + // latest dep tests call receive once and only retry the failed process step + assertions.add( + trace -> + trace.hasSpansSatisfyingExactly( + SpringKafkaTest::assertReceiveSpan, + span -> assertProcessSpan(span, trace, producer.get(), true), + span -> span.hasName("consumer").hasParent(trace.getSpan(1)), + span -> assertProcessSpan(span, trace, producer.get(), true), + span -> span.hasName("consumer").hasParent(trace.getSpan(3)), + span -> assertProcessSpan(span, trace, producer.get(), false), + span -> span.hasName("consumer").hasParent(trace.getSpan(5)))); + } else { + assertions.addAll( + Arrays.asList( + trace -> + trace.hasSpansSatisfyingExactly( + SpringKafkaTest::assertReceiveSpan, + span -> assertProcessSpan(span, trace, producer.get(), true), + span -> span.hasName("consumer").hasParent(trace.getSpan(1))), + trace -> + trace.hasSpansSatisfyingExactly( + SpringKafkaTest::assertReceiveSpan, + span -> assertProcessSpan(span, trace, producer.get(), true), + span -> span.hasName("consumer").hasParent(trace.getSpan(1))), + trace -> + trace.hasSpansSatisfyingExactly( + SpringKafkaTest::assertReceiveSpan, + span -> assertProcessSpan(span, trace, producer.get(), false), + span -> span.hasName("consumer").hasParent(trace.getSpan(1))))); + } + + testing.waitAndAssertSortedTraces( + orderByRootSpanKind(SpanKind.INTERNAL, SpanKind.CONSUMER), assertions); + } + + private static void assertReceiveSpan(SpanDataAssert span) { + span.hasName("testBatchTopic receive") + .hasKind(SpanKind.CONSUMER) + .hasNoParent() + .hasAttributesSatisfyingExactly( + equalTo(MessagingIncubatingAttributes.MESSAGING_SYSTEM, "kafka"), + equalTo(MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME, "testBatchTopic"), + equalTo(MessagingIncubatingAttributes.MESSAGING_OPERATION, "receive"), + equalTo( + MessagingIncubatingAttributes.MESSAGING_KAFKA_CONSUMER_GROUP, "testBatchListener"), + satisfies( + MessagingIncubatingAttributes.MESSAGING_CLIENT_ID, + stringAssert -> stringAssert.startsWith("consumer")), + equalTo(MessagingIncubatingAttributes.MESSAGING_BATCH_MESSAGE_COUNT, 1)); + } + + private static void assertProcessSpan( + SpanDataAssert span, TraceAssert trace, SpanData producer, boolean failed) { + span.hasName("testBatchTopic process") + .hasKind(SpanKind.CONSUMER) + .hasParent(trace.getSpan(0)) + .hasLinks(LinkData.create(producer.getSpanContext())) + .hasAttributesSatisfyingExactly( + equalTo(MessagingIncubatingAttributes.MESSAGING_SYSTEM, "kafka"), + equalTo(MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME, "testBatchTopic"), + equalTo(MessagingIncubatingAttributes.MESSAGING_OPERATION, "process"), + equalTo( + MessagingIncubatingAttributes.MESSAGING_KAFKA_CONSUMER_GROUP, "testBatchListener"), + satisfies( + MessagingIncubatingAttributes.MESSAGING_CLIENT_ID, + stringAssert -> stringAssert.startsWith("consumer")), + equalTo(MessagingIncubatingAttributes.MESSAGING_BATCH_MESSAGE_COUNT, 1)); + if (failed) { + span.hasStatus(StatusData.error()).hasException(new IllegalArgumentException("boom")); + } } } diff --git a/instrumentation/spring/spring-kafka-2.7/javaagent/src/testNoReceiveTelemetry/java/io/opentelemetry/javaagent/instrumentation/spring/kafka/v2_7/SpringKafkaNoReceiveTelemetryTest.java b/instrumentation/spring/spring-kafka-2.7/javaagent/src/testNoReceiveTelemetry/java/io/opentelemetry/javaagent/instrumentation/spring/kafka/v2_7/SpringKafkaNoReceiveTelemetryTest.java index 4f3c74786167..e37ebe1efa28 100644 --- a/instrumentation/spring/spring-kafka-2.7/javaagent/src/testNoReceiveTelemetry/java/io/opentelemetry/javaagent/instrumentation/spring/kafka/v2_7/SpringKafkaNoReceiveTelemetryTest.java +++ b/instrumentation/spring/spring-kafka-2.7/javaagent/src/testNoReceiveTelemetry/java/io/opentelemetry/javaagent/instrumentation/spring/kafka/v2_7/SpringKafkaNoReceiveTelemetryTest.java @@ -27,4 +27,9 @@ protected InstrumentationExtension testing() { protected List> additionalSpringConfigs() { return emptyList(); } + + @Override + protected boolean isLibraryInstrumentationTest() { + return false; + } } diff --git a/instrumentation/spring/spring-kafka-2.7/library/build.gradle.kts b/instrumentation/spring/spring-kafka-2.7/library/build.gradle.kts index 66499b5c303f..ecf38fd4ce7f 100644 --- a/instrumentation/spring/spring-kafka-2.7/library/build.gradle.kts +++ b/instrumentation/spring/spring-kafka-2.7/library/build.gradle.kts @@ -10,7 +10,8 @@ dependencies { implementation(project(":instrumentation:kafka:kafka-clients:kafka-clients-common:library")) - compileOnly("org.springframework.kafka:spring-kafka:2.7.0") + // compiling against 2.8.0 to use methods that are not present in 2.7 + compileOnly("org.springframework.kafka:spring-kafka:2.8.0") testImplementation(project(":instrumentation:spring:spring-kafka-2.7:testing")) testImplementation(project(":instrumentation:kafka:kafka-clients:kafka-clients-2.6:library")) @@ -24,6 +25,7 @@ dependencies { tasks.withType().configureEach { usesService(gradle.sharedServices.registrations["testcontainersBuildService"].service) + systemProperty("testLatestDeps", findProperty("testLatestDeps") as Boolean) } val latestDepTest = findProperty("testLatestDeps") as Boolean diff --git a/instrumentation/spring/spring-kafka-2.7/library/src/main/java/io/opentelemetry/instrumentation/spring/kafka/v2_7/InstrumentedBatchInterceptor.java b/instrumentation/spring/spring-kafka-2.7/library/src/main/java/io/opentelemetry/instrumentation/spring/kafka/v2_7/InstrumentedBatchInterceptor.java index 4d30d8c97b15..2c0726c79e5b 100644 --- a/instrumentation/spring/spring-kafka-2.7/library/src/main/java/io/opentelemetry/instrumentation/spring/kafka/v2_7/InstrumentedBatchInterceptor.java +++ b/instrumentation/spring/spring-kafka-2.7/library/src/main/java/io/opentelemetry/instrumentation/spring/kafka/v2_7/InstrumentedBatchInterceptor.java @@ -12,6 +12,8 @@ import io.opentelemetry.instrumentation.kafka.internal.KafkaConsumerContext; import io.opentelemetry.instrumentation.kafka.internal.KafkaConsumerContextUtil; import io.opentelemetry.instrumentation.kafka.internal.KafkaReceiveRequest; +import io.opentelemetry.javaagent.tooling.muzzle.NoMuzzle; +import java.lang.ref.WeakReference; import javax.annotation.Nullable; import org.apache.kafka.clients.consumer.Consumer; import org.apache.kafka.clients.consumer.ConsumerRecords; @@ -21,6 +23,8 @@ final class InstrumentedBatchInterceptor implements BatchInterceptor private static final VirtualField, State> stateField = VirtualField.find(ConsumerRecords.class, State.class); + private static final ThreadLocal>> lastProcessed = + new ThreadLocal<>(); private final Instrumenter batchProcessInstrumenter; @Nullable private final BatchInterceptor decorated; @@ -37,7 +41,7 @@ public ConsumerRecords intercept(ConsumerRecords records, Consumer intercept(ConsumerRecords records, Consumer records) { + // When retrying failed listener interceptors work as expected only in the earlier versions that + // we test (e.g spring-kafka:2.7.1). In later versions interceptor isn't called at all during + // the retry, which results in missing process span, or worse, the intercept method is called, + // but neither success nor failure is called, which results in a context leak. Here we attempt + // to prevent the context leak by observing whether intercept is called with the same + // ConsumerRecords as on previous call, and if it is, we skip creating the process span. + WeakReference> reference = lastProcessed.get(); + return reference != null && reference.get() == records; + } + private static Context getParentContext(ConsumerRecords records) { KafkaConsumerContext consumerContext = KafkaConsumerContextUtil.get(records); Context receiveContext = consumerContext.getContext(); @@ -56,17 +71,23 @@ private static Context getParentContext(ConsumerRecords records) { @Override public void success(ConsumerRecords records, Consumer consumer) { - end(records, null); - if (decorated != null) { - decorated.success(records, consumer); + try { + if (decorated != null) { + decorated.success(records, consumer); + } + } finally { + end(records, null); } } @Override public void failure(ConsumerRecords records, Exception exception, Consumer consumer) { - end(records, exception); - if (decorated != null) { - decorated.failure(records, exception, consumer); + try { + if (decorated != null) { + decorated.failure(records, exception, consumer); + } + } finally { + end(records, exception); } } @@ -77,6 +98,23 @@ private void end(ConsumerRecords records, @Nullable Throwable error) { KafkaReceiveRequest request = state.request(); state.scope().close(); batchProcessInstrumenter.end(state.context(), request, null, error); + lastProcessed.set(new WeakReference<>(records)); + } + } + + @NoMuzzle // method was added in 2.8.0 + @Override + public void setupThreadState(Consumer consumer) { + if (decorated != null) { + decorated.setupThreadState(consumer); + } + } + + @NoMuzzle // method was added in 2.8.0 + @Override + public void clearThreadState(Consumer consumer) { + if (decorated != null) { + decorated.clearThreadState(consumer); } } } diff --git a/instrumentation/spring/spring-kafka-2.7/library/src/main/java/io/opentelemetry/instrumentation/spring/kafka/v2_7/InstrumentedRecordInterceptor.java b/instrumentation/spring/spring-kafka-2.7/library/src/main/java/io/opentelemetry/instrumentation/spring/kafka/v2_7/InstrumentedRecordInterceptor.java index 992a91d93701..d1d5e211fea9 100644 --- a/instrumentation/spring/spring-kafka-2.7/library/src/main/java/io/opentelemetry/instrumentation/spring/kafka/v2_7/InstrumentedRecordInterceptor.java +++ b/instrumentation/spring/spring-kafka-2.7/library/src/main/java/io/opentelemetry/instrumentation/spring/kafka/v2_7/InstrumentedRecordInterceptor.java @@ -69,17 +69,23 @@ private static Context getParentContext(ConsumerRecord record) { @Override public void success(ConsumerRecord record, Consumer consumer) { - end(record, null); - if (decorated != null) { - decorated.success(record, consumer); + try { + if (decorated != null) { + decorated.success(record, consumer); + } + } finally { + end(record, null); } } @Override public void failure(ConsumerRecord record, Exception exception, Consumer consumer) { - end(record, exception); - if (decorated != null) { - decorated.failure(record, exception, consumer); + try { + if (decorated != null) { + decorated.failure(record, exception, consumer); + } + } finally { + end(record, exception); } } @@ -92,4 +98,28 @@ private void end(ConsumerRecord record, @Nullable Throwable error) { processInstrumenter.end(state.context(), request, null, error); } } + + @NoMuzzle // method was added in 2.8.0 + @Override + public void afterRecord(ConsumerRecord record, Consumer consumer) { + if (decorated != null) { + decorated.afterRecord(record, consumer); + } + } + + @NoMuzzle // method was added in 2.8.0 + @Override + public void setupThreadState(Consumer consumer) { + if (decorated != null) { + decorated.setupThreadState(consumer); + } + } + + @NoMuzzle // method was added in 2.8.0 + @Override + public void clearThreadState(Consumer consumer) { + if (decorated != null) { + decorated.clearThreadState(consumer); + } + } } diff --git a/instrumentation/spring/spring-kafka-2.7/library/src/main/java/io/opentelemetry/instrumentation/spring/kafka/v2_7/SpringKafkaTelemetryBuilder.java b/instrumentation/spring/spring-kafka-2.7/library/src/main/java/io/opentelemetry/instrumentation/spring/kafka/v2_7/SpringKafkaTelemetryBuilder.java index b65725b5e628..dc74cd179fc2 100644 --- a/instrumentation/spring/spring-kafka-2.7/library/src/main/java/io/opentelemetry/instrumentation/spring/kafka/v2_7/SpringKafkaTelemetryBuilder.java +++ b/instrumentation/spring/spring-kafka-2.7/library/src/main/java/io/opentelemetry/instrumentation/spring/kafka/v2_7/SpringKafkaTelemetryBuilder.java @@ -10,6 +10,7 @@ import com.google.errorprone.annotations.CanIgnoreReturnValue; import io.opentelemetry.api.OpenTelemetry; import io.opentelemetry.instrumentation.kafka.internal.KafkaInstrumenterFactory; +import io.opentelemetry.instrumentation.spring.kafka.v2_7.internal.SpringKafkaErrorCauseExtractor; import java.util.List; /** A builder of {@link SpringKafkaTelemetry}. */ @@ -50,6 +51,12 @@ public SpringKafkaTelemetryBuilder setPropagationEnabled(boolean propagationEnab return this; } + /** + * Set whether to capture the consumer message receive telemetry in messaging instrumentation. + * + *

    Note that this will cause the consumer side to start a new trace, with only a span link + * connecting it to the producer trace. + */ @CanIgnoreReturnValue public SpringKafkaTelemetryBuilder setMessagingReceiveInstrumentationEnabled( boolean messagingReceiveInstrumentationEnabled) { diff --git a/instrumentation/spring/spring-kafka-2.7/library/src/main/java/io/opentelemetry/instrumentation/spring/kafka/v2_7/SpringKafkaErrorCauseExtractor.java b/instrumentation/spring/spring-kafka-2.7/library/src/main/java/io/opentelemetry/instrumentation/spring/kafka/v2_7/internal/SpringKafkaErrorCauseExtractor.java similarity index 65% rename from instrumentation/spring/spring-kafka-2.7/library/src/main/java/io/opentelemetry/instrumentation/spring/kafka/v2_7/SpringKafkaErrorCauseExtractor.java rename to instrumentation/spring/spring-kafka-2.7/library/src/main/java/io/opentelemetry/instrumentation/spring/kafka/v2_7/internal/SpringKafkaErrorCauseExtractor.java index 463ce1b13b0d..bc9a59629fb3 100644 --- a/instrumentation/spring/spring-kafka-2.7/library/src/main/java/io/opentelemetry/instrumentation/spring/kafka/v2_7/SpringKafkaErrorCauseExtractor.java +++ b/instrumentation/spring/spring-kafka-2.7/library/src/main/java/io/opentelemetry/instrumentation/spring/kafka/v2_7/internal/SpringKafkaErrorCauseExtractor.java @@ -3,12 +3,16 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.instrumentation.spring.kafka.v2_7; +package io.opentelemetry.instrumentation.spring.kafka.v2_7.internal; import io.opentelemetry.instrumentation.api.instrumenter.ErrorCauseExtractor; import org.springframework.kafka.listener.ListenerExecutionFailedException; -enum SpringKafkaErrorCauseExtractor implements ErrorCauseExtractor { +/** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time. + */ +public enum SpringKafkaErrorCauseExtractor implements ErrorCauseExtractor { INSTANCE; @Override diff --git a/instrumentation/spring/spring-kafka-2.7/library/src/test/java/io/opentelemetry/instrumentation/spring/kafka/v2_7/SpringKafkaNoReceiveTelemetryTest.java b/instrumentation/spring/spring-kafka-2.7/library/src/test/java/io/opentelemetry/instrumentation/spring/kafka/v2_7/SpringKafkaNoReceiveTelemetryTest.java index 49980d63f72a..e4e96db2926d 100644 --- a/instrumentation/spring/spring-kafka-2.7/library/src/test/java/io/opentelemetry/instrumentation/spring/kafka/v2_7/SpringKafkaNoReceiveTelemetryTest.java +++ b/instrumentation/spring/spring-kafka-2.7/library/src/test/java/io/opentelemetry/instrumentation/spring/kafka/v2_7/SpringKafkaNoReceiveTelemetryTest.java @@ -34,6 +34,11 @@ protected List> additionalSpringConfigs() { return singletonList(KafkaInstrumentationConfig.class); } + @Override + protected boolean isLibraryInstrumentationTest() { + return true; + } + @Configuration public static class KafkaInstrumentationConfig { diff --git a/instrumentation/spring/spring-kafka-2.7/testing/src/main/java/io/opentelemetry/testing/AbstractSpringKafkaNoReceiveTelemetryTest.java b/instrumentation/spring/spring-kafka-2.7/testing/src/main/java/io/opentelemetry/testing/AbstractSpringKafkaNoReceiveTelemetryTest.java index 07beb409d242..53535037d566 100644 --- a/instrumentation/spring/spring-kafka-2.7/testing/src/main/java/io/opentelemetry/testing/AbstractSpringKafkaNoReceiveTelemetryTest.java +++ b/instrumentation/spring/spring-kafka-2.7/testing/src/main/java/io/opentelemetry/testing/AbstractSpringKafkaNoReceiveTelemetryTest.java @@ -6,21 +6,28 @@ package io.opentelemetry.testing; import static io.opentelemetry.instrumentation.testing.util.TelemetryDataUtil.orderByRootSpanKind; +import static io.opentelemetry.instrumentation.testing.util.TelemetryDataUtil.orderByRootSpanName; import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.satisfies; import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.sdk.testing.assertj.AttributeAssertion; import io.opentelemetry.sdk.trace.data.SpanData; import io.opentelemetry.sdk.trace.data.StatusData; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes; +import java.util.Arrays; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicReference; import org.assertj.core.api.AbstractLongAssert; +import org.assertj.core.api.AbstractStringAssert; import org.junit.jupiter.api.Test; public abstract class AbstractSpringKafkaNoReceiveTelemetryTest extends AbstractSpringKafkaTest { + protected abstract boolean isLibraryInstrumentationTest(); + @Test void shouldCreateSpansForSingleRecordProcess() { testing() @@ -40,54 +47,59 @@ void shouldCreateSpansForSingleRecordProcess() { trace.hasSpansSatisfyingExactly( span -> span.hasName("producer"), span -> - span.hasName("testSingleTopic send") + span.hasName("testSingleTopic publish") .hasKind(SpanKind.PRODUCER) .hasParent(trace.getSpan(0)) .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.MESSAGING_SYSTEM, "kafka"), + equalTo(MessagingIncubatingAttributes.MESSAGING_SYSTEM, "kafka"), equalTo( - SemanticAttributes.MESSAGING_DESTINATION_NAME, + MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME, "testSingleTopic"), + equalTo( + MessagingIncubatingAttributes.MESSAGING_OPERATION, "publish"), satisfies( - SemanticAttributes.MESSAGING_KAFKA_CLIENT_ID, + MessagingIncubatingAttributes.MESSAGING_CLIENT_ID, stringAssert -> stringAssert.startsWith("producer")), satisfies( - SemanticAttributes.MESSAGING_KAFKA_DESTINATION_PARTITION, - AbstractLongAssert::isNotNegative), + MessagingIncubatingAttributes + .MESSAGING_DESTINATION_PARTITION_ID, + AbstractStringAssert::isNotEmpty), satisfies( - SemanticAttributes.MESSAGING_KAFKA_MESSAGE_OFFSET, + MessagingIncubatingAttributes.MESSAGING_KAFKA_MESSAGE_OFFSET, AbstractLongAssert::isNotNegative), - equalTo(SemanticAttributes.MESSAGING_KAFKA_MESSAGE_KEY, "10")), + equalTo( + MessagingIncubatingAttributes.MESSAGING_KAFKA_MESSAGE_KEY, + "10")), span -> span.hasName("testSingleTopic process") .hasKind(SpanKind.CONSUMER) .hasParent(trace.getSpan(1)) .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.MESSAGING_SYSTEM, "kafka"), + equalTo(MessagingIncubatingAttributes.MESSAGING_SYSTEM, "kafka"), equalTo( - SemanticAttributes.MESSAGING_DESTINATION_NAME, + MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME, "testSingleTopic"), - equalTo(SemanticAttributes.MESSAGING_OPERATION, "process"), + equalTo( + MessagingIncubatingAttributes.MESSAGING_OPERATION, "process"), satisfies( - SemanticAttributes.MESSAGING_MESSAGE_PAYLOAD_SIZE_BYTES, + MessagingIncubatingAttributes.MESSAGING_MESSAGE_BODY_SIZE, AbstractLongAssert::isNotNegative), satisfies( - SemanticAttributes.MESSAGING_KAFKA_SOURCE_PARTITION, - AbstractLongAssert::isNotNegative), + MessagingIncubatingAttributes + .MESSAGING_DESTINATION_PARTITION_ID, + AbstractStringAssert::isNotEmpty), satisfies( - SemanticAttributes.MESSAGING_KAFKA_MESSAGE_OFFSET, + MessagingIncubatingAttributes.MESSAGING_KAFKA_MESSAGE_OFFSET, AbstractLongAssert::isNotNegative), - equalTo(SemanticAttributes.MESSAGING_KAFKA_MESSAGE_KEY, "10"), equalTo( - SemanticAttributes.MESSAGING_KAFKA_CONSUMER_GROUP, + MessagingIncubatingAttributes.MESSAGING_KAFKA_MESSAGE_KEY, + "10"), + equalTo( + MessagingIncubatingAttributes.MESSAGING_KAFKA_CONSUMER_GROUP, "testSingleListener"), satisfies( - SemanticAttributes.MESSAGING_KAFKA_CLIENT_ID, - stringAssert -> stringAssert.startsWith("consumer")), - satisfies( - SemanticAttributes.MESSAGING_CONSUMER_ID, - stringAssert -> - stringAssert.startsWith("testSingleListener - consumer"))), + MessagingIncubatingAttributes.MESSAGING_CLIENT_ID, + stringAssert -> stringAssert.startsWith("consumer"))), span -> span.hasName("consumer").hasParent(trace.getSpan(2)))); } @@ -104,63 +116,79 @@ void shouldHandleFailureInKafkaListener() { }); }); + List processAttributes = + Arrays.asList( + equalTo(MessagingIncubatingAttributes.MESSAGING_SYSTEM, "kafka"), + equalTo(MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME, "testSingleTopic"), + equalTo(MessagingIncubatingAttributes.MESSAGING_OPERATION, "process"), + satisfies( + MessagingIncubatingAttributes.MESSAGING_MESSAGE_BODY_SIZE, + AbstractLongAssert::isNotNegative), + satisfies( + MessagingIncubatingAttributes.MESSAGING_DESTINATION_PARTITION_ID, + AbstractStringAssert::isNotEmpty), + satisfies( + MessagingIncubatingAttributes.MESSAGING_KAFKA_MESSAGE_OFFSET, + AbstractLongAssert::isNotNegative), + equalTo(MessagingIncubatingAttributes.MESSAGING_KAFKA_MESSAGE_KEY, "10"), + equalTo( + MessagingIncubatingAttributes.MESSAGING_KAFKA_CONSUMER_GROUP, "testSingleListener"), + satisfies( + MessagingIncubatingAttributes.MESSAGING_CLIENT_ID, + stringAssert -> stringAssert.startsWith("consumer"))); + testing() .waitAndAssertTraces( trace -> trace.hasSpansSatisfyingExactly( span -> span.hasName("producer"), span -> - span.hasName("testSingleTopic send") + span.hasName("testSingleTopic publish") .hasKind(SpanKind.PRODUCER) .hasParent(trace.getSpan(0)) .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.MESSAGING_SYSTEM, "kafka"), + equalTo(MessagingIncubatingAttributes.MESSAGING_SYSTEM, "kafka"), equalTo( - SemanticAttributes.MESSAGING_DESTINATION_NAME, + MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME, "testSingleTopic"), + equalTo( + MessagingIncubatingAttributes.MESSAGING_OPERATION, "publish"), satisfies( - SemanticAttributes.MESSAGING_KAFKA_CLIENT_ID, + MessagingIncubatingAttributes.MESSAGING_CLIENT_ID, stringAssert -> stringAssert.startsWith("producer")), satisfies( - SemanticAttributes.MESSAGING_KAFKA_DESTINATION_PARTITION, - AbstractLongAssert::isNotNegative), + MessagingIncubatingAttributes + .MESSAGING_DESTINATION_PARTITION_ID, + AbstractStringAssert::isNotEmpty), satisfies( - SemanticAttributes.MESSAGING_KAFKA_MESSAGE_OFFSET, + MessagingIncubatingAttributes.MESSAGING_KAFKA_MESSAGE_OFFSET, AbstractLongAssert::isNotNegative), - equalTo(SemanticAttributes.MESSAGING_KAFKA_MESSAGE_KEY, "10")), + equalTo( + MessagingIncubatingAttributes.MESSAGING_KAFKA_MESSAGE_KEY, + "10")), span -> span.hasName("testSingleTopic process") .hasKind(SpanKind.CONSUMER) .hasParent(trace.getSpan(1)) .hasStatus(StatusData.error()) .hasException(new IllegalArgumentException("boom")) - .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.MESSAGING_SYSTEM, "kafka"), - equalTo( - SemanticAttributes.MESSAGING_DESTINATION_NAME, - "testSingleTopic"), - equalTo(SemanticAttributes.MESSAGING_OPERATION, "process"), - satisfies( - SemanticAttributes.MESSAGING_MESSAGE_PAYLOAD_SIZE_BYTES, - AbstractLongAssert::isNotNegative), - satisfies( - SemanticAttributes.MESSAGING_KAFKA_SOURCE_PARTITION, - AbstractLongAssert::isNotNegative), - satisfies( - SemanticAttributes.MESSAGING_KAFKA_MESSAGE_OFFSET, - AbstractLongAssert::isNotNegative), - equalTo(SemanticAttributes.MESSAGING_KAFKA_MESSAGE_KEY, "10"), - equalTo( - SemanticAttributes.MESSAGING_KAFKA_CONSUMER_GROUP, - "testSingleListener"), - satisfies( - SemanticAttributes.MESSAGING_KAFKA_CLIENT_ID, - stringAssert -> stringAssert.startsWith("consumer")), - satisfies( - SemanticAttributes.MESSAGING_CONSUMER_ID, - stringAssert -> - stringAssert.startsWith("testSingleListener - consumer"))), - span -> span.hasName("consumer").hasParent(trace.getSpan(2)))); + .hasAttributesSatisfyingExactly(processAttributes), + span -> span.hasName("consumer").hasParent(trace.getSpan(2)), + span -> + span.hasName("testSingleTopic process") + .hasKind(SpanKind.CONSUMER) + .hasParent(trace.getSpan(1)) + .hasStatus(StatusData.error()) + .hasException(new IllegalArgumentException("boom")) + .hasAttributesSatisfyingExactly(processAttributes), + span -> span.hasName("consumer").hasParent(trace.getSpan(4)), + span -> + span.hasName("testSingleTopic process") + .hasKind(SpanKind.CONSUMER) + .hasParent(trace.getSpan(1)) + .hasStatus(StatusData.unset()) + .hasAttributesSatisfyingExactly(processAttributes), + span -> span.hasName("consumer").hasParent(trace.getSpan(6)))); } @Test @@ -180,41 +208,48 @@ void shouldCreateSpansForBatchReceiveAndProcess() throws InterruptedException { trace.hasSpansSatisfyingExactlyInAnyOrder( span -> span.hasName("producer"), span -> - span.hasName("testBatchTopic send") + span.hasName("testBatchTopic publish") .hasKind(SpanKind.PRODUCER) .hasParent(trace.getSpan(0)) .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.MESSAGING_SYSTEM, "kafka"), + equalTo(MessagingIncubatingAttributes.MESSAGING_SYSTEM, "kafka"), equalTo( - SemanticAttributes.MESSAGING_DESTINATION_NAME, "testBatchTopic"), + MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME, + "testBatchTopic"), + equalTo(MessagingIncubatingAttributes.MESSAGING_OPERATION, "publish"), satisfies( - SemanticAttributes.MESSAGING_KAFKA_CLIENT_ID, + MessagingIncubatingAttributes.MESSAGING_CLIENT_ID, stringAssert -> stringAssert.startsWith("producer")), satisfies( - SemanticAttributes.MESSAGING_KAFKA_DESTINATION_PARTITION, - AbstractLongAssert::isNotNegative), + MessagingIncubatingAttributes.MESSAGING_DESTINATION_PARTITION_ID, + AbstractStringAssert::isNotEmpty), satisfies( - SemanticAttributes.MESSAGING_KAFKA_MESSAGE_OFFSET, + MessagingIncubatingAttributes.MESSAGING_KAFKA_MESSAGE_OFFSET, AbstractLongAssert::isNotNegative), - equalTo(SemanticAttributes.MESSAGING_KAFKA_MESSAGE_KEY, "10")), + equalTo( + MessagingIncubatingAttributes.MESSAGING_KAFKA_MESSAGE_KEY, "10")), span -> - span.hasName("testBatchTopic send") + span.hasName("testBatchTopic publish") .hasKind(SpanKind.PRODUCER) .hasParent(trace.getSpan(0)) .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.MESSAGING_SYSTEM, "kafka"), + equalTo(MessagingIncubatingAttributes.MESSAGING_SYSTEM, "kafka"), equalTo( - SemanticAttributes.MESSAGING_DESTINATION_NAME, "testBatchTopic"), + MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME, + "testBatchTopic"), + equalTo(MessagingIncubatingAttributes.MESSAGING_OPERATION, "publish"), satisfies( - SemanticAttributes.MESSAGING_KAFKA_CLIENT_ID, + MessagingIncubatingAttributes.MESSAGING_CLIENT_ID, stringAssert -> stringAssert.startsWith("producer")), satisfies( - SemanticAttributes.MESSAGING_KAFKA_DESTINATION_PARTITION, - AbstractLongAssert::isNotNegative), + MessagingIncubatingAttributes.MESSAGING_DESTINATION_PARTITION_ID, + AbstractStringAssert::isNotEmpty), satisfies( - SemanticAttributes.MESSAGING_KAFKA_MESSAGE_OFFSET, + MessagingIncubatingAttributes.MESSAGING_KAFKA_MESSAGE_OFFSET, AbstractLongAssert::isNotNegative), - equalTo(SemanticAttributes.MESSAGING_KAFKA_MESSAGE_KEY, "20"))); + equalTo( + MessagingIncubatingAttributes.MESSAGING_KAFKA_MESSAGE_KEY, + "20"))); producer1.set(trace.getSpan(1)); producer2.set(trace.getSpan(2)); @@ -230,21 +265,21 @@ void shouldCreateSpansForBatchReceiveAndProcess() throws InterruptedException { producer1.get().getSpanContext(), producer2.get().getSpanContext())) .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.MESSAGING_SYSTEM, "kafka"), + equalTo(MessagingIncubatingAttributes.MESSAGING_SYSTEM, "kafka"), equalTo( - SemanticAttributes.MESSAGING_DESTINATION_NAME, + MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME, "testBatchTopic"), - equalTo(SemanticAttributes.MESSAGING_OPERATION, "process"), equalTo( - SemanticAttributes.MESSAGING_KAFKA_CONSUMER_GROUP, + MessagingIncubatingAttributes.MESSAGING_OPERATION, "process"), + equalTo( + MessagingIncubatingAttributes.MESSAGING_KAFKA_CONSUMER_GROUP, "testBatchListener"), satisfies( - SemanticAttributes.MESSAGING_KAFKA_CLIENT_ID, + MessagingIncubatingAttributes.MESSAGING_CLIENT_ID, stringAssert -> stringAssert.startsWith("consumer")), - satisfies( - SemanticAttributes.MESSAGING_CONSUMER_ID, - stringAssert -> - stringAssert.startsWith("testBatchListener - consumer"))), + equalTo( + MessagingIncubatingAttributes.MESSAGING_BATCH_MESSAGE_COUNT, + 2)), span -> span.hasName("consumer").hasParent(trace.getSpan(0)))); } @@ -263,30 +298,46 @@ void shouldHandleFailureInKafkaBatchListener() { AtomicReference producer = new AtomicReference<>(); + List processAttributes = + Arrays.asList( + equalTo(MessagingIncubatingAttributes.MESSAGING_SYSTEM, "kafka"), + equalTo(MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME, "testBatchTopic"), + equalTo(MessagingIncubatingAttributes.MESSAGING_OPERATION, "process"), + equalTo( + MessagingIncubatingAttributes.MESSAGING_KAFKA_CONSUMER_GROUP, "testBatchListener"), + satisfies( + MessagingIncubatingAttributes.MESSAGING_CLIENT_ID, + stringAssert -> stringAssert.startsWith("consumer")), + equalTo(MessagingIncubatingAttributes.MESSAGING_BATCH_MESSAGE_COUNT, 1)); + testing() .waitAndAssertSortedTraces( - orderByRootSpanKind(SpanKind.INTERNAL, SpanKind.CONSUMER), + orderByRootSpanName("producer", "testBatchTopic process", "consumer"), trace -> { trace.hasSpansSatisfyingExactly( span -> span.hasName("producer"), span -> - span.hasName("testBatchTopic send") + span.hasName("testBatchTopic publish") .hasKind(SpanKind.PRODUCER) .hasParent(trace.getSpan(0)) .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.MESSAGING_SYSTEM, "kafka"), + equalTo(MessagingIncubatingAttributes.MESSAGING_SYSTEM, "kafka"), equalTo( - SemanticAttributes.MESSAGING_DESTINATION_NAME, "testBatchTopic"), + MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME, + "testBatchTopic"), + equalTo(MessagingIncubatingAttributes.MESSAGING_OPERATION, "publish"), satisfies( - SemanticAttributes.MESSAGING_KAFKA_CLIENT_ID, + MessagingIncubatingAttributes.MESSAGING_CLIENT_ID, stringAssert -> stringAssert.startsWith("producer")), satisfies( - SemanticAttributes.MESSAGING_KAFKA_DESTINATION_PARTITION, - AbstractLongAssert::isNotNegative), + MessagingIncubatingAttributes.MESSAGING_DESTINATION_PARTITION_ID, + AbstractStringAssert::isNotEmpty), satisfies( - SemanticAttributes.MESSAGING_KAFKA_MESSAGE_OFFSET, + MessagingIncubatingAttributes.MESSAGING_KAFKA_MESSAGE_OFFSET, AbstractLongAssert::isNotNegative), - equalTo(SemanticAttributes.MESSAGING_KAFKA_MESSAGE_KEY, "10"))); + equalTo( + MessagingIncubatingAttributes.MESSAGING_KAFKA_MESSAGE_KEY, + "10"))); producer.set(trace.getSpan(1)); }, @@ -299,22 +350,40 @@ void shouldHandleFailureInKafkaBatchListener() { .hasLinksSatisfying(links(producer.get().getSpanContext())) .hasStatus(StatusData.error()) .hasException(new IllegalArgumentException("boom")) - .hasAttributesSatisfyingExactly( - equalTo(SemanticAttributes.MESSAGING_SYSTEM, "kafka"), - equalTo( - SemanticAttributes.MESSAGING_DESTINATION_NAME, - "testBatchTopic"), - equalTo(SemanticAttributes.MESSAGING_OPERATION, "process"), - equalTo( - SemanticAttributes.MESSAGING_KAFKA_CONSUMER_GROUP, - "testBatchListener"), - satisfies( - SemanticAttributes.MESSAGING_KAFKA_CLIENT_ID, - stringAssert -> stringAssert.startsWith("consumer")), - satisfies( - SemanticAttributes.MESSAGING_CONSUMER_ID, - stringAssert -> - stringAssert.startsWith("testBatchListener - consumer"))), - span -> span.hasName("consumer").hasParent(trace.getSpan(0)))); + .hasAttributesSatisfyingExactly(processAttributes), + span -> span.hasName("consumer").hasParent(trace.getSpan(0))), + trace -> { + if (isLibraryInstrumentationTest() && Boolean.getBoolean("testLatestDeps")) { + // in latest dep tests process spans are not created for retries because spring does + // not call the success/failure methods on the BatchInterceptor for reties + trace.hasSpansSatisfyingExactly(span -> span.hasName("consumer").hasNoParent()); + } else { + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("testBatchTopic process") + .hasKind(SpanKind.CONSUMER) + .hasNoParent() + .hasLinksSatisfying(links(producer.get().getSpanContext())) + .hasStatus(StatusData.error()) + .hasException(new IllegalArgumentException("boom")) + .hasAttributesSatisfyingExactly(processAttributes), + span -> span.hasName("consumer").hasParent(trace.getSpan(0))); + } + }, + trace -> { + if (isLibraryInstrumentationTest() && Boolean.getBoolean("testLatestDeps")) { + trace.hasSpansSatisfyingExactly(span -> span.hasName("consumer").hasNoParent()); + } else { + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("testBatchTopic process") + .hasKind(SpanKind.CONSUMER) + .hasNoParent() + .hasLinksSatisfying(links(producer.get().getSpanContext())) + .hasStatus(StatusData.unset()) + .hasAttributesSatisfyingExactly(processAttributes), + span -> span.hasName("consumer").hasParent(trace.getSpan(0))); + } + }); } } diff --git a/instrumentation/spring/spring-kafka-2.7/testing/src/main/java/io/opentelemetry/testing/AbstractSpringKafkaTest.java b/instrumentation/spring/spring-kafka-2.7/testing/src/main/java/io/opentelemetry/testing/AbstractSpringKafkaTest.java index 16d1fba123f5..bbc3bc9384ac 100644 --- a/instrumentation/spring/spring-kafka-2.7/testing/src/main/java/io/opentelemetry/testing/AbstractSpringKafkaTest.java +++ b/instrumentation/spring/spring-kafka-2.7/testing/src/main/java/io/opentelemetry/testing/AbstractSpringKafkaTest.java @@ -46,7 +46,7 @@ public abstract class AbstractSpringKafkaTest { @BeforeAll static void setUpKafka() { kafka = - new KafkaContainer(DockerImageName.parse("confluentinc/cp-kafka:6.2.10")) + new KafkaContainer(DockerImageName.parse("confluentinc/cp-kafka:7.7.0")) .withEnv("KAFKA_HEAP_OPTS", "-Xmx256m") .waitingFor(Wait.forLogMessage(".*started \\(kafka.server.KafkaServer\\).*", 1)) .withStartupTimeout(Duration.ofMinutes(1)); diff --git a/instrumentation/spring/spring-kafka-2.7/testing/src/main/java/io/opentelemetry/testing/BatchRecordListener.java b/instrumentation/spring/spring-kafka-2.7/testing/src/main/java/io/opentelemetry/testing/BatchRecordListener.java index b0bbeea4d205..ea3bb655b592 100644 --- a/instrumentation/spring/spring-kafka-2.7/testing/src/main/java/io/opentelemetry/testing/BatchRecordListener.java +++ b/instrumentation/spring/spring-kafka-2.7/testing/src/main/java/io/opentelemetry/testing/BatchRecordListener.java @@ -18,6 +18,7 @@ public class BatchRecordListener { private static final AtomicInteger lastBatchSize = new AtomicInteger(); private static volatile CountDownLatch messageReceived = new CountDownLatch(2); + private int failureCount; @KafkaListener( id = "testBatchListener", @@ -30,7 +31,8 @@ public void listener(List> records) { GlobalTraceUtil.runWithSpan("consumer", () -> {}); records.forEach( record -> { - if (record.value().equals("error")) { + if (record.value().equals("error") && failureCount < 2) { + failureCount++; throw new IllegalArgumentException("boom"); } }); diff --git a/instrumentation/spring/spring-kafka-2.7/testing/src/main/java/io/opentelemetry/testing/ConsumerConfig.java b/instrumentation/spring/spring-kafka-2.7/testing/src/main/java/io/opentelemetry/testing/ConsumerConfig.java index f24070bc1bda..78fb2607b349 100644 --- a/instrumentation/spring/spring-kafka-2.7/testing/src/main/java/io/opentelemetry/testing/ConsumerConfig.java +++ b/instrumentation/spring/spring-kafka-2.7/testing/src/main/java/io/opentelemetry/testing/ConsumerConfig.java @@ -49,8 +49,6 @@ public ConcurrentKafkaListenerContainerFactory batchFactory( customizerProvider) { ConcurrentKafkaListenerContainerFactory factory = new ConcurrentKafkaListenerContainerFactory<>(); - // do not retry failed records - factory.setBatchErrorHandler(new DoNothingBatchErrorHandler()); factory.setConsumerFactory(consumerFactory); factory.setBatchListener(true); factory.setAutoStartup(true); @@ -67,8 +65,6 @@ public ConcurrentKafkaListenerContainerFactory singleFactory( customizerProvider) { ConcurrentKafkaListenerContainerFactory factory = new ConcurrentKafkaListenerContainerFactory<>(); - // do not retry failed records - factory.setErrorHandler(new DoNothingErrorHandler()); factory.setConsumerFactory(consumerFactory); factory.setBatchListener(false); factory.setAutoStartup(true); diff --git a/instrumentation/spring/spring-kafka-2.7/testing/src/main/java/io/opentelemetry/testing/ErrorHandlerSetter.java b/instrumentation/spring/spring-kafka-2.7/testing/src/main/java/io/opentelemetry/testing/ErrorHandlerSetter.java new file mode 100644 index 000000000000..4d2c97c42a36 --- /dev/null +++ b/instrumentation/spring/spring-kafka-2.7/testing/src/main/java/io/opentelemetry/testing/ErrorHandlerSetter.java @@ -0,0 +1,24 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.testing; + +import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory; + +// Base classes for error handlers are missing in the latest version. Setter methods were extracted +// in ConsumerConfig to avoid verifier attempting to load these classes. +class ErrorHandlerSetter { + + private ErrorHandlerSetter() {} + + static void setBatchErrorHandler( + ConcurrentKafkaListenerContainerFactory factory) { + factory.setBatchErrorHandler(new DoNothingBatchErrorHandler()); + } + + static void setErrorHandler(ConcurrentKafkaListenerContainerFactory factory) { + factory.setErrorHandler(new DoNothingErrorHandler()); + } +} diff --git a/instrumentation/spring/spring-kafka-2.7/testing/src/main/java/io/opentelemetry/testing/SingleRecordListener.java b/instrumentation/spring/spring-kafka-2.7/testing/src/main/java/io/opentelemetry/testing/SingleRecordListener.java index bf05ad33f4bd..17f2c1057d04 100644 --- a/instrumentation/spring/spring-kafka-2.7/testing/src/main/java/io/opentelemetry/testing/SingleRecordListener.java +++ b/instrumentation/spring/spring-kafka-2.7/testing/src/main/java/io/opentelemetry/testing/SingleRecordListener.java @@ -10,6 +10,7 @@ import org.springframework.kafka.annotation.KafkaListener; public class SingleRecordListener { + private int failureCount; @KafkaListener( id = "testSingleListener", @@ -17,7 +18,8 @@ public class SingleRecordListener { containerFactory = "singleFactory") public void listener(ConsumerRecord record) { GlobalTraceUtil.runWithSpan("consumer", () -> {}); - if (record.value().equals("error")) { + if (record.value().equals("error") && failureCount < 2) { + failureCount++; throw new IllegalArgumentException("boom"); } } diff --git a/instrumentation/spring/spring-rabbit-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/rabbit/v1_0/SpringRabbitMessageAttributesGetter.java b/instrumentation/spring/spring-rabbit-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/rabbit/v1_0/SpringRabbitMessageAttributesGetter.java index 725e83858741..5db6ec646479 100644 --- a/instrumentation/spring/spring-rabbit-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/rabbit/v1_0/SpringRabbitMessageAttributesGetter.java +++ b/instrumentation/spring/spring-rabbit-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/rabbit/v1_0/SpringRabbitMessageAttributesGetter.java @@ -5,7 +5,7 @@ package io.opentelemetry.javaagent.instrumentation.spring.rabbit.v1_0; -import io.opentelemetry.instrumentation.api.instrumenter.messaging.MessagingAttributesGetter; +import io.opentelemetry.instrumentation.api.incubator.semconv.messaging.MessagingAttributesGetter; import java.util.Collections; import java.util.List; import javax.annotation.Nullable; @@ -25,11 +25,22 @@ public String getDestination(Message message) { return message.getMessageProperties().getReceivedRoutingKey(); } + @Nullable + @Override + public String getDestinationTemplate(Message message) { + return null; + } + @Override public boolean isTemporaryDestination(Message message) { return false; } + @Override + public boolean isAnonymousDestination(Message message) { + return false; + } + @Override @Nullable public String getConversationId(Message message) { @@ -37,13 +48,13 @@ public String getConversationId(Message message) { } @Override - public Long getMessagePayloadSize(Message message) { + public Long getMessageBodySize(Message message) { return message.getMessageProperties().getContentLength(); } - @Override @Nullable - public Long getMessagePayloadCompressedSize(Message message) { + @Override + public Long getMessageEnvelopeSize(Message message) { return null; } @@ -53,6 +64,18 @@ public String getMessageId(Message message, @Nullable Void unused) { return message.getMessageProperties().getMessageId(); } + @Nullable + @Override + public String getClientId(Message message) { + return null; + } + + @Nullable + @Override + public Long getBatchMessageCount(Message message, @Nullable Void unused) { + return null; + } + @Override public List getMessageHeader(Message message, String name) { Object value = message.getMessageProperties().getHeaders().get(name); diff --git a/instrumentation/spring/spring-rabbit-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/rabbit/v1_0/SpringRabbitSingletons.java b/instrumentation/spring/spring-rabbit-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/rabbit/v1_0/SpringRabbitSingletons.java index 928e4fd80ada..eae11c3df682 100644 --- a/instrumentation/spring/spring-rabbit-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/rabbit/v1_0/SpringRabbitSingletons.java +++ b/instrumentation/spring/spring-rabbit-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/rabbit/v1_0/SpringRabbitSingletons.java @@ -6,10 +6,10 @@ package io.opentelemetry.javaagent.instrumentation.spring.rabbit.v1_0; import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.instrumentation.api.incubator.semconv.messaging.MessageOperation; +import io.opentelemetry.instrumentation.api.incubator.semconv.messaging.MessagingAttributesExtractor; +import io.opentelemetry.instrumentation.api.incubator.semconv.messaging.MessagingSpanNameExtractor; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; -import io.opentelemetry.instrumentation.api.instrumenter.messaging.MessageOperation; -import io.opentelemetry.instrumentation.api.instrumenter.messaging.MessagingAttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.messaging.MessagingSpanNameExtractor; import io.opentelemetry.javaagent.bootstrap.internal.ExperimentalConfig; import org.springframework.amqp.core.Message; diff --git a/instrumentation/spring/spring-rabbit-1.0/javaagent/src/test/groovy/ContextPropagationTest.groovy b/instrumentation/spring/spring-rabbit-1.0/javaagent/src/test/groovy/ContextPropagationTest.groovy deleted file mode 100644 index 1cd7eaf4a046..000000000000 --- a/instrumentation/spring/spring-rabbit-1.0/javaagent/src/test/groovy/ContextPropagationTest.groovy +++ /dev/null @@ -1,199 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -import com.rabbitmq.client.ConnectionFactory -import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification -import io.opentelemetry.instrumentation.testing.GlobalTraceUtil -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes -import org.springframework.amqp.AmqpException -import org.springframework.amqp.core.AmqpTemplate -import org.springframework.amqp.core.Message -import org.springframework.amqp.core.MessagePostProcessor -import org.springframework.amqp.core.Queue -import org.springframework.amqp.rabbit.annotation.RabbitListener -import org.springframework.boot.SpringApplication -import org.springframework.boot.SpringBootConfiguration -import org.springframework.boot.autoconfigure.EnableAutoConfiguration -import org.springframework.context.ConfigurableApplicationContext -import org.springframework.context.annotation.Bean -import org.testcontainers.containers.GenericContainer -import org.testcontainers.containers.wait.strategy.Wait -import spock.lang.Shared -import spock.lang.Unroll - -import java.time.Duration - -import static io.opentelemetry.api.trace.SpanKind.CLIENT -import static io.opentelemetry.api.trace.SpanKind.CONSUMER -import static io.opentelemetry.api.trace.SpanKind.PRODUCER - -class ContextPropagationTest extends AgentInstrumentationSpecification { - - @Shared - GenericContainer rabbitMqContainer - @Shared - ConfigurableApplicationContext applicationContext - @Shared - ConnectionFactory connectionFactory - - def setupSpec() { - rabbitMqContainer = new GenericContainer('rabbitmq:latest') - .withExposedPorts(5672) - .waitingFor(Wait.forLogMessage(".*Server startup complete.*", 1)) - .withStartupTimeout(Duration.ofMinutes(2)) - rabbitMqContainer.start() - - def app = new SpringApplication(ConsumerConfig) - app.setDefaultProperties([ - "spring.jmx.enabled" : false, - "spring.main.web-application-type": "none", - "spring.rabbitmq.host" : rabbitMqContainer.host, - "spring.rabbitmq.port" : rabbitMqContainer.getMappedPort(5672), - ]) - applicationContext = app.run() - - connectionFactory = new ConnectionFactory( - host: rabbitMqContainer.host, - port: rabbitMqContainer.getMappedPort(5672) - ) - } - - def cleanupSpec() { - rabbitMqContainer?.stop() - applicationContext?.close() - } - - @Unroll - def "should propagate context to consumer, test headers: #testHeaders"() { - given: - def connection = connectionFactory.newConnection() - def channel = connection.createChannel() - - when: - runWithSpan("parent") { - if (testHeaders) { - applicationContext.getBean(AmqpTemplate) - .convertAndSend(ConsumerConfig.TEST_QUEUE, (Object) "test", new MessagePostProcessor() { - @Override - Message postProcessMessage(Message message) throws AmqpException { - message.getMessageProperties().setHeader("test-message-header", "test") - return message - } - }) - } else { - applicationContext.getBean(AmqpTemplate) - .convertAndSend(ConsumerConfig.TEST_QUEUE, "test") - } - } - - then: - assertTraces(2) { - trace(0, 5) { - spans.subList(2, 5).sort { - // sort "consumer" span after "testQueue process" spans - if (it.name == "consumer") { - return 2 - } - // order "testQueue process" spans - def destination = it.attributes.get(SemanticAttributes.MESSAGING_DESTINATION_NAME) - return destination == "" ? 0 : 1 - } - span(0) { - name "parent" - } - span(1) { - // created by rabbitmq instrumentation - name " send" - kind PRODUCER - childOf span(0) - attributes { - "$SemanticAttributes.NET_SOCK_PEER_ADDR" { it == "127.0.0.1" || it == null } - "$SemanticAttributes.NET_SOCK_PEER_PORT" Long - "$SemanticAttributes.MESSAGING_SYSTEM" "rabbitmq" - "$SemanticAttributes.MESSAGING_DESTINATION_NAME" "" - "$SemanticAttributes.MESSAGING_MESSAGE_PAYLOAD_SIZE_BYTES" Long - "$SemanticAttributes.MESSAGING_RABBITMQ_DESTINATION_ROUTING_KEY" String - if (testHeaders) { - "messaging.header.test_message_header" { it == ["test"] } - } - } - } - // 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) - span(2) { - // created by rabbitmq instrumentation - name "testQueue process" - kind CONSUMER - childOf span(1) - attributes { - "$SemanticAttributes.MESSAGING_SYSTEM" "rabbitmq" - "$SemanticAttributes.MESSAGING_DESTINATION_NAME" "" - "$SemanticAttributes.MESSAGING_OPERATION" "process" - "$SemanticAttributes.MESSAGING_MESSAGE_PAYLOAD_SIZE_BYTES" Long - "$SemanticAttributes.MESSAGING_RABBITMQ_DESTINATION_ROUTING_KEY" String - if (testHeaders) { - "messaging.header.test_message_header" { it == ["test"] } - } - } - } - span(3) { - // created by spring-rabbit instrumentation - name "testQueue process" - kind CONSUMER - childOf span(1) - attributes { - "$SemanticAttributes.MESSAGING_SYSTEM" "rabbitmq" - "$SemanticAttributes.MESSAGING_DESTINATION_NAME" "testQueue" - "$SemanticAttributes.MESSAGING_OPERATION" "process" - "$SemanticAttributes.MESSAGING_MESSAGE_PAYLOAD_SIZE_BYTES" Long - if (testHeaders) { - "messaging.header.test_message_header" { it == ["test"] } - } - } - } - span(4) { - name "consumer" - childOf span(3) - } - } - trace(1, 1) { - span(0) { - // created by rabbitmq instrumentation - name "basic.ack" - kind CLIENT - attributes { - "$SemanticAttributes.NET_SOCK_PEER_ADDR" { it == "127.0.0.1" || it == null } - "$SemanticAttributes.NET_SOCK_PEER_PORT" Long - "$SemanticAttributes.MESSAGING_SYSTEM" "rabbitmq" - } - } - } - } - - cleanup: - channel?.close() - connection?.close() - - where: - testHeaders << [false, true] - } - - @SpringBootConfiguration - @EnableAutoConfiguration - static class ConsumerConfig { - - static final String TEST_QUEUE = "testQueue" - - @Bean - Queue testQueue() { - new Queue(TEST_QUEUE) - } - - @RabbitListener(queues = TEST_QUEUE) - void consume(String ignored) { - GlobalTraceUtil.runWithSpan("consumer") {} - } - } -} diff --git a/instrumentation/spring/spring-rabbit-1.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/rabbit/v1_0/SpringRabbitMqTest.java b/instrumentation/spring/spring-rabbit-1.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/rabbit/v1_0/SpringRabbitMqTest.java new file mode 100644 index 000000000000..950ee620dbae --- /dev/null +++ b/instrumentation/spring/spring-rabbit-1.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/rabbit/v1_0/SpringRabbitMqTest.java @@ -0,0 +1,276 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.rabbit.v1_0; + +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.satisfies; + +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.Connection; +import com.rabbitmq.client.ConnectionFactory; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.testing.GlobalTraceUtil; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.sdk.testing.assertj.AttributeAssertion; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.semconv.NetworkAttributes; +import io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.time.Duration; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.assertj.core.api.AbstractLongAssert; +import org.assertj.core.api.AbstractStringAssert; +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; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.springframework.amqp.core.AmqpTemplate; +import org.springframework.amqp.core.AnonymousQueue; +import org.springframework.amqp.core.Queue; +import org.springframework.amqp.rabbit.annotation.RabbitListener; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.SpringBootConfiguration; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.Bean; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.wait.strategy.Wait; + +public class SpringRabbitMqTest { + + @RegisterExtension + private static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + private static GenericContainer rabbitMqContainer; + private static ConfigurableApplicationContext applicationContext; + private static ConnectionFactory connectionFactory; + + private static String ip; + + @BeforeAll + static void setUp() throws UnknownHostException { + rabbitMqContainer = + new GenericContainer<>("rabbitmq:latest") + .withExposedPorts(5672) + .waitingFor(Wait.forLogMessage(".*Server startup complete.*", 1)) + .withStartupTimeout(Duration.ofMinutes(2)); + rabbitMqContainer.start(); + + SpringApplication app = new SpringApplication(ConsumerConfig.class); + Map props = new HashMap<>(); + props.put("spring.jmx.enabled", false); + props.put("spring.main.web-application-type", "none"); + props.put("spring.rabbitmq.host", rabbitMqContainer.getHost()); + props.put("spring.rabbitmq.port", rabbitMqContainer.getMappedPort(5672)); + app.setDefaultProperties(props); + + applicationContext = app.run(); + + connectionFactory = new ConnectionFactory(); + connectionFactory.setHost(rabbitMqContainer.getHost()); + connectionFactory.setPort(rabbitMqContainer.getMappedPort(5672)); + ip = InetAddress.getByName(rabbitMqContainer.getHost()).getHostAddress(); + } + + @AfterAll + static void teardown() { + if (rabbitMqContainer != null) { + rabbitMqContainer.stop(); + } + if (applicationContext != null) { + applicationContext.close(); + } + } + + private static List getAssertions( + String destination, + String operation, + String peerAddress, + boolean routingKey, + boolean testHeaders) { + List assertions = + new ArrayList<>( + Arrays.asList( + equalTo(MessagingIncubatingAttributes.MESSAGING_SYSTEM, "rabbitmq"), + equalTo(MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME, destination), + satisfies( + MessagingIncubatingAttributes.MESSAGING_MESSAGE_BODY_SIZE, + AbstractLongAssert::isNotNegative))); + if (operation != null) { + assertions.add(equalTo(MessagingIncubatingAttributes.MESSAGING_OPERATION, operation)); + } + if (peerAddress != null) { + assertions.add(equalTo(NetworkAttributes.NETWORK_TYPE, "ipv4")); + assertions.add(equalTo(NetworkAttributes.NETWORK_PEER_ADDRESS, peerAddress)); + assertions.add( + satisfies(NetworkAttributes.NETWORK_PEER_PORT, AbstractLongAssert::isNotNegative)); + } + if (routingKey) { + assertions.add( + satisfies( + MessagingIncubatingAttributes.MESSAGING_RABBITMQ_DESTINATION_ROUTING_KEY, + AbstractStringAssert::isNotBlank)); + } + if (testHeaders) { + assertions.add( + equalTo( + AttributeKey.stringArrayKey("messaging.header.test_message_header"), + Collections.singletonList("test"))); + } + return assertions; + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + public void testContextPropagation(boolean testHeaders) throws Exception { + try (Connection connection = connectionFactory.newConnection()) { + try (Channel ignored = connection.createChannel()) { + testing.runWithSpan( + "parent", + () -> { + if (testHeaders) { + applicationContext + .getBean(AmqpTemplate.class) + .convertAndSend( + ConsumerConfig.TEST_QUEUE, + "test", + message -> { + message.getMessageProperties().setHeader("test-message-header", "test"); + return message; + }); + } else { + applicationContext + .getBean(AmqpTemplate.class) + .convertAndSend(ConsumerConfig.TEST_QUEUE, "test"); + } + }); + testing.waitAndAssertTraces( + trace -> { + trace.hasSpansSatisfyingExactlyInAnyOrder( + span -> span.hasName("parent"), + span -> + span.hasName(" publish") + .hasKind(SpanKind.PRODUCER) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + getAssertions("", "publish", ip, true, testHeaders)), + // 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) + span -> + span.hasName("testQueue process") + .hasKind(SpanKind.CONSUMER) + .hasParent(trace.getSpan(1)) + .hasAttributesSatisfyingExactly( + getAssertions("", "process", ip, true, testHeaders)), + // created by spring-rabbit instrumentation + span -> + span.hasName("testQueue process") + .hasKind(SpanKind.CONSUMER) + .hasParent(trace.getSpan(1)) + .hasAttributesSatisfyingExactly( + getAssertions("testQueue", "process", null, false, testHeaders)), + span -> { + // occasionally "testQueue process" spans have their order swapped, usually + // it would be + // 0 - parent + // 1 - publish + // 2 - testQueue process () + // 3 - testQueue process (testQueue) + // 4 - consumer + // but it could also be + // 0 - parent + // 1 - publish + // 2 - testQueue process (testQueue) + // 3 - consumer + // 4 - testQueue process () + // determine the correct parent span based on the span name + SpanData parentSpan = trace.getSpan(3); + if (!"testQueue process".equals(parentSpan.getName())) { + parentSpan = trace.getSpan(2); + } + span.hasName("consumer").hasParent(parentSpan); + }); + }, + trace -> { + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("basic.ack") + .hasKind(SpanKind.CLIENT) + .hasAttributesSatisfyingExactly( + equalTo(NetworkAttributes.NETWORK_TYPE, "ipv4"), + equalTo(NetworkAttributes.NETWORK_PEER_ADDRESS, ip), + satisfies( + NetworkAttributes.NETWORK_PEER_PORT, + AbstractLongAssert::isNotNegative), + equalTo(MessagingIncubatingAttributes.MESSAGING_SYSTEM, "rabbitmq"))); + }); + } + } + } + + @Test + public void testAnonymousQueueSpanName() throws Exception { + try (Connection connection = connectionFactory.newConnection()) { + try (Channel ignored = connection.createChannel()) { + String anonymousQueueName = applicationContext.getBean(AnonymousQueue.class).getName(); + applicationContext.getBean(AmqpTemplate.class).convertAndSend(anonymousQueueName, "test"); + applicationContext.getBean(AmqpTemplate.class).receive(anonymousQueueName, 5000); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName(" publish"), + // Verify that a constant span name is used instead of the randomly generated + // anonymous queue name + span -> + span.hasName(" process") + .hasAttribute( + equalTo( + MessagingIncubatingAttributes + .MESSAGING_RABBITMQ_DESTINATION_ROUTING_KEY, + anonymousQueueName))), + trace -> trace.hasSpansSatisfyingExactly(span -> span.hasName("basic.qos")), + trace -> trace.hasSpansSatisfyingExactly(span -> span.hasName("basic.consume")), + trace -> trace.hasSpansSatisfyingExactly(span -> span.hasName("basic.cancel")), + trace -> trace.hasSpansSatisfyingExactly(span -> span.hasName("basic.ack"))); + } + } + } + + @SpringBootConfiguration + @EnableAutoConfiguration + static class ConsumerConfig { + + static final String TEST_QUEUE = "testQueue"; + + @Bean + Queue testQueue() { + return new Queue(TEST_QUEUE); + } + + @Bean + AnonymousQueue anonymousQueue() { + return new AnonymousQueue(); + } + + @RabbitListener(queues = TEST_QUEUE) + void consume(String ignored) { + GlobalTraceUtil.runWithSpan("consumer", () -> {}); + } + } +} diff --git a/instrumentation/spring/spring-rmi-4.0/javaagent/build.gradle.kts b/instrumentation/spring/spring-rmi-4.0/javaagent/build.gradle.kts index 479d420d9c6d..0677c5651d68 100644 --- a/instrumentation/spring/spring-rmi-4.0/javaagent/build.gradle.kts +++ b/instrumentation/spring/spring-rmi-4.0/javaagent/build.gradle.kts @@ -20,6 +20,7 @@ dependencies { library("org.springframework:spring-context:4.0.0.RELEASE") library("org.springframework:spring-aop:4.0.0.RELEASE") testLibrary("org.springframework.boot:spring-boot:1.1.0.RELEASE") + testImplementation("org.apache.tomee:openejb-core:8.0.16") // rmi remoting was removed in spring 6 latestDepTestLibrary("org.springframework:spring-context:5.+") // documented limitation diff --git a/instrumentation/spring/spring-rmi-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/rmi/v4_0/SpringRmiIgnoredTypesConfigurer.java b/instrumentation/spring/spring-rmi-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/rmi/v4_0/SpringRmiIgnoredTypesConfigurer.java new file mode 100644 index 000000000000..c12ab641a0a4 --- /dev/null +++ b/instrumentation/spring/spring-rmi-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/rmi/v4_0/SpringRmiIgnoredTypesConfigurer.java @@ -0,0 +1,22 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.rmi.v4_0; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.ignore.IgnoredTypesBuilder; +import io.opentelemetry.javaagent.extension.ignore.IgnoredTypesConfigurer; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; + +@AutoService(IgnoredTypesConfigurer.class) +public class SpringRmiIgnoredTypesConfigurer implements IgnoredTypesConfigurer { + + @Override + public void configure(IgnoredTypesBuilder builder, ConfigProperties config) { + // The Spring EJB classes are ignored in the AdditionalLibraryIgnoredTypesConfigurer, but + // are required when utilizing Spring's local-slsb and remote-slsb to access EJBs through RMI. + builder.allowClass("org.springframework.ejb.access."); + } +} diff --git a/instrumentation/spring/spring-rmi-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/rmi/v4_0/SpringRmiSingletons.java b/instrumentation/spring/spring-rmi-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/rmi/v4_0/SpringRmiSingletons.java index c8d1f6f6081a..72145d72fe07 100644 --- a/instrumentation/spring/spring-rmi-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/rmi/v4_0/SpringRmiSingletons.java +++ b/instrumentation/spring/spring-rmi-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/rmi/v4_0/SpringRmiSingletons.java @@ -6,12 +6,12 @@ package io.opentelemetry.javaagent.instrumentation.spring.rmi.v4_0; import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.instrumentation.api.incubator.semconv.rpc.RpcClientAttributesExtractor; +import io.opentelemetry.instrumentation.api.incubator.semconv.rpc.RpcServerAttributesExtractor; +import io.opentelemetry.instrumentation.api.incubator.semconv.rpc.RpcSpanNameExtractor; +import io.opentelemetry.instrumentation.api.incubator.semconv.util.ClassAndMethod; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; import io.opentelemetry.instrumentation.api.instrumenter.SpanKindExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.rpc.RpcClientAttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.rpc.RpcServerAttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.rpc.RpcSpanNameExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.util.ClassAndMethod; import io.opentelemetry.javaagent.instrumentation.spring.rmi.v4_0.client.ClientAttributesGetter; import io.opentelemetry.javaagent.instrumentation.spring.rmi.v4_0.server.ServerAttributesGetter; import java.lang.reflect.Method; diff --git a/instrumentation/spring/spring-rmi-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/rmi/v4_0/client/ClientAttributesGetter.java b/instrumentation/spring/spring-rmi-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/rmi/v4_0/client/ClientAttributesGetter.java index cc40b591b169..19d31b217770 100644 --- a/instrumentation/spring/spring-rmi-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/rmi/v4_0/client/ClientAttributesGetter.java +++ b/instrumentation/spring/spring-rmi-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/rmi/v4_0/client/ClientAttributesGetter.java @@ -5,7 +5,7 @@ package io.opentelemetry.javaagent.instrumentation.spring.rmi.v4_0.client; -import io.opentelemetry.instrumentation.api.instrumenter.rpc.RpcAttributesGetter; +import io.opentelemetry.instrumentation.api.incubator.semconv.rpc.RpcAttributesGetter; import java.lang.reflect.Method; public enum ClientAttributesGetter implements RpcAttributesGetter { diff --git a/instrumentation/spring/spring-rmi-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/rmi/v4_0/client/ClientInstrumentation.java b/instrumentation/spring/spring-rmi-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/rmi/v4_0/client/ClientInstrumentation.java index 622e453f5ebf..b04daf155bd3 100644 --- a/instrumentation/spring/spring-rmi-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/rmi/v4_0/client/ClientInstrumentation.java +++ b/instrumentation/spring/spring-rmi-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/rmi/v4_0/client/ClientInstrumentation.java @@ -5,6 +5,7 @@ package io.opentelemetry.javaagent.instrumentation.spring.rmi.v4_0.client; +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.extendsClass; import static io.opentelemetry.javaagent.instrumentation.spring.rmi.v4_0.SpringRmiSingletons.clientInstrumenter; import static net.bytebuddy.matcher.ElementMatchers.isMethod; import static net.bytebuddy.matcher.ElementMatchers.named; @@ -25,7 +26,8 @@ public class ClientInstrumentation implements TypeInstrumentation { @Override public ElementMatcher typeMatcher() { - return named("org.springframework.remoting.rmi.RmiClientInterceptor"); + return named("org.springframework.remoting.rmi.RmiClientInterceptor") + .or(extendsClass(named("org.springframework.ejb.access.AbstractSlsbInvokerInterceptor"))); } @Override diff --git a/instrumentation/spring/spring-rmi-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/rmi/v4_0/server/ServerAttributesGetter.java b/instrumentation/spring/spring-rmi-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/rmi/v4_0/server/ServerAttributesGetter.java index 96cc9322636e..e7bf12be54cc 100644 --- a/instrumentation/spring/spring-rmi-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/rmi/v4_0/server/ServerAttributesGetter.java +++ b/instrumentation/spring/spring-rmi-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/rmi/v4_0/server/ServerAttributesGetter.java @@ -5,8 +5,8 @@ package io.opentelemetry.javaagent.instrumentation.spring.rmi.v4_0.server; -import io.opentelemetry.instrumentation.api.instrumenter.rpc.RpcAttributesGetter; -import io.opentelemetry.instrumentation.api.instrumenter.util.ClassAndMethod; +import io.opentelemetry.instrumentation.api.incubator.semconv.rpc.RpcAttributesGetter; +import io.opentelemetry.instrumentation.api.incubator.semconv.util.ClassAndMethod; public enum ServerAttributesGetter implements RpcAttributesGetter { INSTANCE; diff --git a/instrumentation/spring/spring-rmi-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/rmi/v4_0/server/ServerInstrumentation.java b/instrumentation/spring/spring-rmi-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/rmi/v4_0/server/ServerInstrumentation.java index f00267ddca4d..51b5f2a2d857 100644 --- a/instrumentation/spring/spring-rmi-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/rmi/v4_0/server/ServerInstrumentation.java +++ b/instrumentation/spring/spring-rmi-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/rmi/v4_0/server/ServerInstrumentation.java @@ -13,7 +13,7 @@ import io.opentelemetry.context.Context; import io.opentelemetry.context.Scope; -import io.opentelemetry.instrumentation.api.instrumenter.util.ClassAndMethod; +import io.opentelemetry.instrumentation.api.incubator.semconv.util.ClassAndMethod; import io.opentelemetry.javaagent.bootstrap.CallDepth; import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; diff --git a/instrumentation/spring/spring-rmi-4.0/javaagent/src/test/groovy/SpringRmiTest.groovy b/instrumentation/spring/spring-rmi-4.0/javaagent/src/test/groovy/SpringRmiTest.groovy deleted file mode 100644 index 994b800ddbeb..000000000000 --- a/instrumentation/spring/spring-rmi-4.0/javaagent/src/test/groovy/SpringRmiTest.groovy +++ /dev/null @@ -1,181 +0,0 @@ -/* - * 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.utils.PortUtils -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes -import org.springframework.boot.SpringApplication -import org.springframework.context.ConfigurableApplicationContext -import org.springframework.context.annotation.Bean -import org.springframework.remoting.rmi.RmiProxyFactoryBean -import org.springframework.remoting.rmi.RmiServiceExporter -import org.springframework.remoting.support.RemoteExporter -import spock.lang.Shared -import springrmi.app.SpringRmiGreeter -import springrmi.app.SpringRmiGreeterImpl - -import static io.opentelemetry.api.trace.StatusCode.ERROR - -class SpringRmiTest extends AgentInstrumentationSpecification { - - @Shared - ConfigurableApplicationContext serverAppContext - - @Shared - ConfigurableApplicationContext clientAppContext - - @Shared - static int registryPort - - static class ServerConfig { - @Bean - static RemoteExporter registerRMIExporter() { - RmiServiceExporter exporter = new RmiServiceExporter() - exporter.setServiceName("springRmiGreeter") - exporter.setServiceInterface(SpringRmiGreeter) - exporter.setService(new SpringRmiGreeterImpl()) - exporter.setRegistryPort(registryPort) - return exporter - } - } - - static class ClientConfig { - @Bean - static RmiProxyFactoryBean rmiProxy() { - RmiProxyFactoryBean bean = new RmiProxyFactoryBean() - bean.setServiceInterface(SpringRmiGreeter) - bean.setServiceUrl("rmi://localhost:" + registryPort + "/springRmiGreeter") - return bean - } - } - - def setupSpec() { - registryPort = PortUtils.findOpenPort() - - def serverApp = new SpringApplication(ServerConfig) - serverApp.setDefaultProperties([ - "spring.jmx.enabled" : false, - "spring.main.web-application-type": "none", - ]) - serverAppContext = serverApp.run() - - def clientApp = new SpringApplication(ClientConfig) - clientApp.setDefaultProperties([ - "spring.jmx.enabled" : false, - "spring.main.web-application-type": "none", - ]) - clientAppContext = clientApp.run() - } - - def cleanupSpec() { - serverAppContext.close() - clientAppContext.close() - } - - def "Client call creates spans"() { - given: - SpringRmiGreeter client = clientAppContext.getBean(SpringRmiGreeter) - when: - def response = runWithSpan("parent") { client.hello("Test Name") } - then: - response == "Hello Test Name" - assertTraces(1) { - trace(0, 3) { - span(0) { - name "parent" - kind SpanKind.INTERNAL - hasNoParent() - } - span(1) { - name "springrmi.app.SpringRmiGreeter/hello" - kind SpanKind.CLIENT - childOf span(0) - attributes { - "$SemanticAttributes.RPC_SYSTEM" "spring_rmi" - "$SemanticAttributes.RPC_SERVICE" "springrmi.app.SpringRmiGreeter" - "$SemanticAttributes.RPC_METHOD" "hello" - } - } - span(2) { - name "springrmi.app.SpringRmiGreeterImpl/hello" - kind SpanKind.SERVER - childOf span(1) - attributes { - "$SemanticAttributes.RPC_SYSTEM" "spring_rmi" - "$SemanticAttributes.RPC_SERVICE" "springrmi.app.SpringRmiGreeterImpl" - "$SemanticAttributes.RPC_METHOD" "hello" - } - } - } - } - } - - def "Throws exception"() { - given: - SpringRmiGreeter client = clientAppContext.getBean(SpringRmiGreeter) - when: - runWithSpan("parent") { client.exceptional() } - then: - def error = thrown(IllegalStateException) - assertTraces(1) { - trace(0, 3) { - span(0) { - name "parent" - kind SpanKind.INTERNAL - status ERROR - hasNoParent() - event(0) { - eventName("$SemanticAttributes.EXCEPTION_EVENT_NAME") - attributes { - "$SemanticAttributes.EXCEPTION_TYPE" error.getClass().getCanonicalName() - "$SemanticAttributes.EXCEPTION_MESSAGE" error.getMessage() - "$SemanticAttributes.EXCEPTION_STACKTRACE" String - } - } - } - span(1) { - name "springrmi.app.SpringRmiGreeter/exceptional" - kind SpanKind.CLIENT - status ERROR - childOf span(0) - event(0) { - eventName("$SemanticAttributes.EXCEPTION_EVENT_NAME") - attributes { - "$SemanticAttributes.EXCEPTION_TYPE" error.getClass().getCanonicalName() - "$SemanticAttributes.EXCEPTION_MESSAGE" error.getMessage() - "$SemanticAttributes.EXCEPTION_STACKTRACE" String - } - } - attributes { - "$SemanticAttributes.RPC_SYSTEM" "spring_rmi" - "$SemanticAttributes.RPC_SERVICE" "springrmi.app.SpringRmiGreeter" - "$SemanticAttributes.RPC_METHOD" "exceptional" - } - } - span(2) { - name "springrmi.app.SpringRmiGreeterImpl/exceptional" - kind SpanKind.SERVER - childOf span(1) - status ERROR - event(0) { - eventName("$SemanticAttributes.EXCEPTION_EVENT_NAME") - attributes { - "$SemanticAttributes.EXCEPTION_TYPE" error.getClass().getCanonicalName() - "$SemanticAttributes.EXCEPTION_MESSAGE" error.getMessage() - "$SemanticAttributes.EXCEPTION_STACKTRACE" String - } - } - attributes { - "$SemanticAttributes.RPC_SYSTEM" "spring_rmi" - "$SemanticAttributes.RPC_SERVICE" "springrmi.app.SpringRmiGreeterImpl" - "$SemanticAttributes.RPC_METHOD" "exceptional" - } - } - } - } - } - -} diff --git a/instrumentation/spring/spring-rmi-4.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/rmi/v4_0/SpringRmiTest.java b/instrumentation/spring/spring-rmi-4.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/rmi/v4_0/SpringRmiTest.java new file mode 100644 index 000000000000..2009d762b4d7 --- /dev/null +++ b/instrumentation/spring/spring-rmi-4.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/rmi/v4_0/SpringRmiTest.java @@ -0,0 +1,268 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.rmi.v4_0; + +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.satisfies; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.test.utils.PortUtils; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.sdk.testing.assertj.SpanDataAssert; +import io.opentelemetry.sdk.trace.data.StatusData; +import io.opentelemetry.semconv.ExceptionAttributes; +import io.opentelemetry.semconv.incubating.RpcIncubatingAttributes; +import java.rmi.RemoteException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; +import javax.ejb.EJBException; +import javax.ejb.embeddable.EJBContainer; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import org.springframework.boot.SpringApplication; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.support.ClassPathXmlApplicationContext; +import org.springframework.remoting.rmi.RmiProxyFactoryBean; +import org.springframework.remoting.rmi.RmiServiceExporter; +import org.springframework.remoting.support.RemoteExporter; +import org.springframework.stereotype.Component; +import springrmi.app.SpringRmiGreeter; +import springrmi.app.SpringRmiGreeterImpl; + +class SpringRmiTest { + + @RegisterExtension + static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + private static ConfigurableApplicationContext serverAppContext; + private static ConfigurableApplicationContext clientAppContext; + private static ConfigurableApplicationContext xmlAppContext; + private static EJBContainer ejbContainer; + + static int registryPort; + + @Component + private static class ServerConfig { + @Bean + static RemoteExporter registerRmiExporter() { + SpringRmiGreeter greeter = new SpringRmiGreeterImpl(); + + RmiServiceExporter exporter = new RmiServiceExporter(); + exporter.setServiceName("springRmiGreeter"); + exporter.setServiceInterface(SpringRmiGreeter.class); + exporter.setService(greeter); + exporter.setRegistryPort(registryPort); + + return exporter; + } + } + + @Component + private static class ClientConfig { + @Bean + static RmiProxyFactoryBean rmiProxy() { + RmiProxyFactoryBean bean = new RmiProxyFactoryBean(); + bean.setServiceInterface(SpringRmiGreeter.class); + bean.setServiceUrl("rmi://localhost:" + registryPort + "/springRmiGreeter"); + return bean; + } + } + + @BeforeAll + static void beforeAll() throws Exception { + registryPort = PortUtils.findOpenPort(); + + Map map = new HashMap<>(); + map.put(EJBContainer.APP_NAME, "test"); + map.put(EJBContainer.MODULES, new java.io.File("build/classes/java/test")); + ejbContainer = EJBContainer.createEJBContainer(map); + + Map props = new HashMap<>(); + props.put("spring.jmx.enabled", false); + props.put("spring.main.web-application-type", "none"); + + SpringApplication serverApp = new SpringApplication(ServerConfig.class); + serverApp.setDefaultProperties(props); + serverAppContext = serverApp.run(); + + SpringApplication clientApp = new SpringApplication(ClientConfig.class); + clientApp.setDefaultProperties(props); + clientAppContext = clientApp.run(); + + xmlAppContext = new ClassPathXmlApplicationContext("spring-rmi.xml"); + } + + @AfterAll + static void afterAll() { + serverAppContext.close(); + clientAppContext.close(); + xmlAppContext.close(); + ejbContainer.close(); + } + + @SuppressWarnings("ImmutableEnumChecker") + private enum TestSource { + RMI( + clientAppContext, + "springrmi.app.SpringRmiGreeterImpl", + "spring_rmi", + IllegalStateException.class), + EJB(xmlAppContext, "springrmi.app.ejb.SpringRmiGreeterRemote", "java_rmi", EJBException.class); + + final ApplicationContext appContext; + final String remoteClassName; + final String serverSystem; + final Class expectedException; + + TestSource( + ApplicationContext appContext, + String remoteClassName, + String serverSystem, + Class expectedException) { + this.appContext = appContext; + this.remoteClassName = remoteClassName; + this.serverSystem = serverSystem; + this.expectedException = expectedException; + } + } + + @ParameterizedTest(autoCloseArguments = false) + @EnumSource(TestSource.class) + void clientCallCreatesSpans(TestSource testSource) throws RemoteException { + SpringRmiGreeter client = testSource.appContext.getBean(SpringRmiGreeter.class); + String response = testing.runWithSpan("parent", () -> client.hello("Test Name")); + assertEquals(response, "Hello Test Name"); + testing.waitAndAssertTraces( + trace -> { + List> assertions = new ArrayList<>(); + assertions.add(span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent()); + assertions.add( + span -> + span.hasName("springrmi.app.SpringRmiGreeter/hello") + .hasKind(SpanKind.CLIENT) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfying( + equalTo(RpcIncubatingAttributes.RPC_SYSTEM, "spring_rmi"), + equalTo( + RpcIncubatingAttributes.RPC_SERVICE, + "springrmi.app.SpringRmiGreeter"), + equalTo(RpcIncubatingAttributes.RPC_METHOD, "hello"))); + if (testSource == TestSource.RMI) { + assertions.add( + span -> + span.hasName(testSource.remoteClassName + "/hello") + .hasKind(SpanKind.SERVER) + .hasParent(trace.getSpan(1)) + .hasAttributesSatisfying( + equalTo(RpcIncubatingAttributes.RPC_SYSTEM, testSource.serverSystem), + equalTo( + RpcIncubatingAttributes.RPC_SERVICE, testSource.remoteClassName), + equalTo(RpcIncubatingAttributes.RPC_METHOD, "hello"))); + } + + trace.hasSpansSatisfyingExactly(assertions); + }); + } + + @ParameterizedTest(autoCloseArguments = false) + @EnumSource(TestSource.class) + void throwsException(TestSource testSource) { + SpringRmiGreeter client = testSource.appContext.getBean(SpringRmiGreeter.class); + Throwable error = + assertThrows( + testSource.expectedException, () -> testing.runWithSpan("parent", client::exceptional)); + testing.waitAndAssertTraces( + trace -> { + List> assertions = new ArrayList<>(); + assertions.add( + span -> + span.hasName("parent") + .hasKind(SpanKind.INTERNAL) + .hasStatus(StatusData.error()) + .hasNoParent() + .hasEventsSatisfyingExactly( + event -> + event + .hasName("exception") + .hasAttributesSatisfying( + equalTo( + ExceptionAttributes.EXCEPTION_TYPE, + error.getClass().getCanonicalName()), + equalTo( + ExceptionAttributes.EXCEPTION_MESSAGE, + error.getMessage()), + satisfies( + ExceptionAttributes.EXCEPTION_STACKTRACE, + val -> val.isInstanceOf(String.class))))); + assertions.add( + span -> + span.hasName("springrmi.app.SpringRmiGreeter/exceptional") + .hasKind(SpanKind.CLIENT) + .hasStatus(StatusData.error()) + .hasParent(trace.getSpan(0)) + .hasEventsSatisfyingExactly( + event -> + event + .hasName("exception") + .hasAttributesSatisfying( + equalTo( + ExceptionAttributes.EXCEPTION_TYPE, + error.getClass().getCanonicalName()), + equalTo( + ExceptionAttributes.EXCEPTION_MESSAGE, + error.getMessage()), + satisfies( + ExceptionAttributes.EXCEPTION_STACKTRACE, + val -> val.isInstanceOf(String.class)))) + .hasAttributesSatisfying( + equalTo(RpcIncubatingAttributes.RPC_SYSTEM, "spring_rmi"), + equalTo( + RpcIncubatingAttributes.RPC_SERVICE, + "springrmi.app.SpringRmiGreeter"), + equalTo(RpcIncubatingAttributes.RPC_METHOD, "exceptional"))); + if (testSource == TestSource.RMI) { + assertions.add( + span -> + span.hasName(testSource.remoteClassName + "/exceptional") + .hasKind(SpanKind.SERVER) + .hasParent(trace.getSpan(1)) + .hasStatus(StatusData.error()) + .hasEventsSatisfyingExactly( + event -> + event + .hasName("exception") + .hasAttributesSatisfying( + equalTo( + ExceptionAttributes.EXCEPTION_TYPE, + error.getClass().getCanonicalName()), + equalTo( + ExceptionAttributes.EXCEPTION_MESSAGE, + error.getMessage()), + satisfies( + ExceptionAttributes.EXCEPTION_STACKTRACE, + val -> val.isInstanceOf(String.class)))) + .hasAttributesSatisfying( + equalTo(RpcIncubatingAttributes.RPC_SYSTEM, testSource.serverSystem), + equalTo( + RpcIncubatingAttributes.RPC_SERVICE, testSource.remoteClassName), + equalTo(RpcIncubatingAttributes.RPC_METHOD, "exceptional"))); + } + + trace.hasSpansSatisfyingExactly(assertions); + }); + } +} diff --git a/instrumentation/spring/spring-rmi-4.0/javaagent/src/test/java/springrmi/app/ejb/SpringRmiGreeterEjb.java b/instrumentation/spring/spring-rmi-4.0/javaagent/src/test/java/springrmi/app/ejb/SpringRmiGreeterEjb.java new file mode 100644 index 000000000000..09b7076b4310 --- /dev/null +++ b/instrumentation/spring/spring-rmi-4.0/javaagent/src/test/java/springrmi/app/ejb/SpringRmiGreeterEjb.java @@ -0,0 +1,13 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package springrmi.app.ejb; + +import javax.ejb.Stateless; +import springrmi.app.SpringRmiGreeterImpl; + +@Stateless +public class SpringRmiGreeterEjb extends SpringRmiGreeterImpl + implements SpringRmiGreeterEjbRemote {} diff --git a/instrumentation/spring/spring-rmi-4.0/javaagent/src/test/java/springrmi/app/ejb/SpringRmiGreeterEjbRemote.java b/instrumentation/spring/spring-rmi-4.0/javaagent/src/test/java/springrmi/app/ejb/SpringRmiGreeterEjbRemote.java new file mode 100644 index 000000000000..21ccee7f960e --- /dev/null +++ b/instrumentation/spring/spring-rmi-4.0/javaagent/src/test/java/springrmi/app/ejb/SpringRmiGreeterEjbRemote.java @@ -0,0 +1,12 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package springrmi.app.ejb; + +import javax.ejb.Remote; +import springrmi.app.SpringRmiGreeter; + +@Remote +public interface SpringRmiGreeterEjbRemote extends SpringRmiGreeter {} diff --git a/instrumentation/spring/spring-rmi-4.0/javaagent/src/test/resources/spring-rmi.xml b/instrumentation/spring/spring-rmi-4.0/javaagent/src/test/resources/spring-rmi.xml new file mode 100644 index 000000000000..b3f6f279f766 --- /dev/null +++ b/instrumentation/spring/spring-rmi-4.0/javaagent/src/test/resources/spring-rmi.xml @@ -0,0 +1,13 @@ + + + + + + + diff --git a/instrumentation/spring/spring-scheduling-3.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/scheduling/v3_1/SpringSchedulingCodeAttributesGetter.java b/instrumentation/spring/spring-scheduling-3.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/scheduling/v3_1/SpringSchedulingCodeAttributesGetter.java index 383e34ad8565..b88e0c5ee2c8 100644 --- a/instrumentation/spring/spring-scheduling-3.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/scheduling/v3_1/SpringSchedulingCodeAttributesGetter.java +++ b/instrumentation/spring/spring-scheduling-3.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/scheduling/v3_1/SpringSchedulingCodeAttributesGetter.java @@ -5,7 +5,7 @@ package io.opentelemetry.javaagent.instrumentation.spring.scheduling.v3_1; -import io.opentelemetry.instrumentation.api.instrumenter.code.CodeAttributesGetter; +import io.opentelemetry.instrumentation.api.incubator.semconv.code.CodeAttributesGetter; import org.springframework.scheduling.support.ScheduledMethodRunnable; public class SpringSchedulingCodeAttributesGetter implements CodeAttributesGetter { diff --git a/instrumentation/spring/spring-scheduling-3.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/scheduling/v3_1/SpringSchedulingSingletons.java b/instrumentation/spring/spring-scheduling-3.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/scheduling/v3_1/SpringSchedulingSingletons.java index 48f546e69954..d7d6ad54f818 100644 --- a/instrumentation/spring/spring-scheduling-3.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/scheduling/v3_1/SpringSchedulingSingletons.java +++ b/instrumentation/spring/spring-scheduling-3.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/scheduling/v3_1/SpringSchedulingSingletons.java @@ -7,17 +7,17 @@ import io.opentelemetry.api.GlobalOpenTelemetry; import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.instrumentation.api.incubator.semconv.code.CodeAttributesExtractor; +import io.opentelemetry.instrumentation.api.incubator.semconv.code.CodeSpanNameExtractor; 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.code.CodeAttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.code.CodeSpanNameExtractor; -import io.opentelemetry.javaagent.bootstrap.internal.InstrumentationConfig; +import io.opentelemetry.javaagent.bootstrap.internal.AgentInstrumentationConfig; public final class SpringSchedulingSingletons { private static final boolean CAPTURE_EXPERIMENTAL_SPAN_ATTRIBUTES = - InstrumentationConfig.get() + AgentInstrumentationConfig.get() .getBoolean("otel.instrumentation.spring-scheduling.experimental-span-attributes", false); private static final Instrumenter INSTRUMENTER; diff --git a/instrumentation/spring/spring-scheduling-3.1/javaagent/src/test/groovy/SpringSchedulingTest.groovy b/instrumentation/spring/spring-scheduling-3.1/javaagent/src/test/groovy/SpringSchedulingTest.groovy deleted file mode 100644 index ab504e1924d8..000000000000 --- a/instrumentation/spring/spring-scheduling-3.1/javaagent/src/test/groovy/SpringSchedulingTest.groovy +++ /dev/null @@ -1,163 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -import io.opentelemetry.api.trace.StatusCode -import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes -import org.springframework.context.annotation.AnnotationConfigApplicationContext - -import java.util.concurrent.CountDownLatch -import java.util.concurrent.TimeUnit - -class SpringSchedulingTest extends AgentInstrumentationSpecification { - - def "schedule one time test"() { - setup: - def context = new AnnotationConfigApplicationContext(OneTimeTaskConfig) - def task = context.getBean(OneTimeTask) - - task.blockUntilExecute() - - expect: - assert task != null - assertTraces(0) {} - } - - 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 { - "job.system" "spring_scheduling" - "code.namespace" "TriggerTask" - "code.function" "run" - } - } - } - } - } - - 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 { - "job.system" "spring_scheduling" - "code.namespace" "IntervalTask" - "code.function" "run" - } - } - } - } - } - - 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) { - name "LambdaTaskConfigurer\$\$Lambda\$.run" - hasNoParent() - attributes { - "job.system" "spring_scheduling" - "code.namespace" { it.startsWith("LambdaTaskConfigurer\$\$Lambda\$") } - "code.function" "run" - } - } - } - } - - cleanup: - context.close() - } - - // by putting the scheduled method directly on the TaskConfig, this verifies the case where the - // class is enhanced and so has a different class name, e.g. TaskConfig$$EnhancerByCGLIB$$b910c4a9 - def "schedule enhanced class test"() { - setup: - def context = new AnnotationConfigApplicationContext(EnhancedClassTaskConfig) - def latch = context.getBean(CountDownLatch) - - latch.await(5, TimeUnit.SECONDS) - - expect: - assertTraces(1) { - trace(0, 1) { - span(0) { - name "EnhancedClassTaskConfig.run" - hasNoParent() - attributes { - "job.system" "spring_scheduling" - "code.namespace" "EnhancedClassTaskConfig" - "code.function" "run" - } - } - } - } - } - - def "task with error test"() { - setup: - def context = new AnnotationConfigApplicationContext(TaskWithErrorConfig) - def task = context.getBean(TaskWithError) - - task.blockUntilExecute() - - expect: - assert task != null - assertTraces(1) { - trace(0, 2) { - span(0) { - name "TaskWithError.run" - hasNoParent() - status StatusCode.ERROR - attributes { - "job.system" "spring_scheduling" - "code.namespace" "TaskWithError" - "code.function" "run" - } - event(0) { - eventName "$SemanticAttributes.EXCEPTION_EVENT_NAME" - attributes { - "$SemanticAttributes.EXCEPTION_TYPE" IllegalStateException.getName() - "$SemanticAttributes.EXCEPTION_MESSAGE" "failure" - "$SemanticAttributes.EXCEPTION_STACKTRACE" String - } - } - } - span(1) { - name "error-handler" - childOf(span(0)) - } - } - } - } -} diff --git a/instrumentation/spring/spring-scheduling-3.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/scheduling/v3_1/SpringSchedulingTest.java b/instrumentation/spring/spring-scheduling-3.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/scheduling/v3_1/SpringSchedulingTest.java new file mode 100644 index 000000000000..eea1a23a4498 --- /dev/null +++ b/instrumentation/spring/spring-scheduling-3.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/scheduling/v3_1/SpringSchedulingTest.java @@ -0,0 +1,178 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.scheduling.v3_1; + +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.satisfies; +import static io.opentelemetry.semconv.incubating.CodeIncubatingAttributes.CODE_FUNCTION; +import static io.opentelemetry.semconv.incubating.CodeIncubatingAttributes.CODE_NAMESPACE; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.javaagent.instrumentation.spring.scheduling.v3_1.spring.component.IntervalTask; +import io.opentelemetry.javaagent.instrumentation.spring.scheduling.v3_1.spring.component.OneTimeTask; +import io.opentelemetry.javaagent.instrumentation.spring.scheduling.v3_1.spring.component.TaskWithError; +import io.opentelemetry.javaagent.instrumentation.spring.scheduling.v3_1.spring.component.TriggerTask; +import io.opentelemetry.javaagent.instrumentation.spring.scheduling.v3_1.spring.config.EnhancedClassTaskConfig; +import io.opentelemetry.javaagent.instrumentation.spring.scheduling.v3_1.spring.config.IntervalTaskConfig; +import io.opentelemetry.javaagent.instrumentation.spring.scheduling.v3_1.spring.config.LambdaTaskConfig; +import io.opentelemetry.javaagent.instrumentation.spring.scheduling.v3_1.spring.config.OneTimeTaskConfig; +import io.opentelemetry.javaagent.instrumentation.spring.scheduling.v3_1.spring.config.TaskWithErrorConfig; +import io.opentelemetry.javaagent.instrumentation.spring.scheduling.v3_1.spring.config.TriggerTaskConfig; +import io.opentelemetry.javaagent.instrumentation.spring.scheduling.v3_1.spring.service.LambdaTaskConfigurer; +import io.opentelemetry.sdk.trace.data.StatusData; +import io.opentelemetry.semconv.ExceptionAttributes; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; + +class SpringSchedulingTest { + + @RegisterExtension + private static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + @Test + void scheduleOneTimeTest() throws InterruptedException { + try (AnnotationConfigApplicationContext context = + new AnnotationConfigApplicationContext(OneTimeTaskConfig.class)) { + OneTimeTask task = context.getBean(OneTimeTask.class); + task.blockUntilExecute(); + + assertThat(task).isNotNull(); + assertThat(testing.waitForTraces(0)).isEmpty(); + } + } + + @Test + void scheduleCronExpressionTest() throws InterruptedException { + try (AnnotationConfigApplicationContext context = + new AnnotationConfigApplicationContext(TriggerTaskConfig.class)) { + TriggerTask task = context.getBean(TriggerTask.class); + task.blockUntilExecute(); + + assertThat(task).isNotNull(); + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("TriggerTask.run") + .hasNoParent() + .hasAttributesSatisfyingExactly( + equalTo(AttributeKey.stringKey("job.system"), "spring_scheduling"), + equalTo(CODE_NAMESPACE, TriggerTask.class.getName()), + equalTo(CODE_FUNCTION, "run")))); + } + } + + @Test + void scheduleIntervalTest() throws InterruptedException { + try (AnnotationConfigApplicationContext context = + new AnnotationConfigApplicationContext(IntervalTaskConfig.class)) { + IntervalTask task = context.getBean(IntervalTask.class); + task.blockUntilExecute(); + + assertThat(task).isNotNull(); + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("IntervalTask.run") + .hasNoParent() + .hasAttributesSatisfyingExactly( + equalTo(AttributeKey.stringKey("job.system"), "spring_scheduling"), + equalTo(CODE_NAMESPACE, IntervalTask.class.getName()), + equalTo(CODE_FUNCTION, "run")))); + } + } + + @Test + void scheduleLambdaTest() throws InterruptedException { + try (AnnotationConfigApplicationContext context = + new AnnotationConfigApplicationContext(LambdaTaskConfig.class)) { + LambdaTaskConfigurer configurer = context.getBean(LambdaTaskConfigurer.class); + configurer.singleUseLatch.await(2000, TimeUnit.MILLISECONDS); + + assertThat(configurer).isNotNull(); + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("LambdaTaskConfigurer$$Lambda.run") + .hasNoParent() + .hasAttributesSatisfyingExactly( + equalTo(AttributeKey.stringKey("job.system"), "spring_scheduling"), + equalTo(CODE_FUNCTION, "run"), + satisfies( + CODE_NAMESPACE, + codeNamespace -> + codeNamespace + .isNotBlank() + .startsWith( + LambdaTaskConfigurer.class.getName() + + "$$Lambda"))))); + } + } + + @Test + void scheduleEnhancedClassTest() throws InterruptedException { + try (AnnotationConfigApplicationContext context = + new AnnotationConfigApplicationContext(EnhancedClassTaskConfig.class)) { + CountDownLatch latch = context.getBean(CountDownLatch.class); + latch.await(5, TimeUnit.SECONDS); + + assertThat(latch).isNotNull(); + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("EnhancedClassTaskConfig.run") + .hasNoParent() + .hasAttributesSatisfyingExactly( + equalTo(AttributeKey.stringKey("job.system"), "spring_scheduling"), + equalTo(CODE_NAMESPACE, EnhancedClassTaskConfig.class.getName()), + equalTo(CODE_FUNCTION, "run")))); + } + } + + @Test + void taskWithErrorTest() throws InterruptedException { + try (AnnotationConfigApplicationContext context = + new AnnotationConfigApplicationContext(TaskWithErrorConfig.class)) { + TaskWithError task = context.getBean(TaskWithError.class); + task.blockUntilExecute(); + + assertThat(task).isNotNull(); + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("TaskWithError.run") + .hasNoParent() + .hasStatus(StatusData.error()) + .hasAttributesSatisfyingExactly( + equalTo(AttributeKey.stringKey("job.system"), "spring_scheduling"), + equalTo(CODE_NAMESPACE, TaskWithError.class.getName()), + equalTo(CODE_FUNCTION, "run")) + .hasEventsSatisfyingExactly( + event -> + event + .hasName("exception") + .hasAttributesSatisfying( + equalTo( + ExceptionAttributes.EXCEPTION_TYPE, + IllegalStateException.class.getName()), + equalTo(ExceptionAttributes.EXCEPTION_MESSAGE, "failure"), + satisfies( + ExceptionAttributes.EXCEPTION_STACKTRACE, + value -> value.isInstanceOf(String.class)))), + span -> span.hasName("error-handler").hasParent(trace.getSpan(0)))); + } + } +} diff --git a/instrumentation/spring/spring-scheduling-3.1/javaagent/src/test/java/IntervalTask.java b/instrumentation/spring/spring-scheduling-3.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/scheduling/v3_1/spring/component/IntervalTask.java similarity index 86% rename from instrumentation/spring/spring-scheduling-3.1/javaagent/src/test/java/IntervalTask.java rename to instrumentation/spring/spring-scheduling-3.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/scheduling/v3_1/spring/component/IntervalTask.java index d7143a0fdee3..ba4f3957401d 100644 --- a/instrumentation/spring/spring-scheduling-3.1/javaagent/src/test/java/IntervalTask.java +++ b/instrumentation/spring/spring-scheduling-3.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/scheduling/v3_1/spring/component/IntervalTask.java @@ -3,6 +3,8 @@ * SPDX-License-Identifier: Apache-2.0 */ +package io.opentelemetry.javaagent.instrumentation.spring.scheduling.v3_1.spring.component; + import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import org.springframework.scheduling.annotation.Scheduled; diff --git a/instrumentation/spring/spring-scheduling-3.1/javaagent/src/test/java/OneTimeTask.java b/instrumentation/spring/spring-scheduling-3.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/scheduling/v3_1/spring/component/OneTimeTask.java similarity index 84% rename from instrumentation/spring/spring-scheduling-3.1/javaagent/src/test/java/OneTimeTask.java rename to instrumentation/spring/spring-scheduling-3.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/scheduling/v3_1/spring/component/OneTimeTask.java index 511977a2df2a..4e36f34cdc2c 100644 --- a/instrumentation/spring/spring-scheduling-3.1/javaagent/src/test/java/OneTimeTask.java +++ b/instrumentation/spring/spring-scheduling-3.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/scheduling/v3_1/spring/component/OneTimeTask.java @@ -3,6 +3,8 @@ * SPDX-License-Identifier: Apache-2.0 */ +package io.opentelemetry.javaagent.instrumentation.spring.scheduling.v3_1.spring.component; + import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import org.springframework.stereotype.Component; diff --git a/instrumentation/spring/spring-scheduling-3.1/javaagent/src/test/java/TaskWithError.java b/instrumentation/spring/spring-scheduling-3.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/scheduling/v3_1/spring/component/TaskWithError.java similarity index 87% rename from instrumentation/spring/spring-scheduling-3.1/javaagent/src/test/java/TaskWithError.java rename to instrumentation/spring/spring-scheduling-3.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/scheduling/v3_1/spring/component/TaskWithError.java index 12a69224edc1..1d4526a96c43 100644 --- a/instrumentation/spring/spring-scheduling-3.1/javaagent/src/test/java/TaskWithError.java +++ b/instrumentation/spring/spring-scheduling-3.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/scheduling/v3_1/spring/component/TaskWithError.java @@ -3,6 +3,8 @@ * SPDX-License-Identifier: Apache-2.0 */ +package io.opentelemetry.javaagent.instrumentation.spring.scheduling.v3_1.spring.component; + import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import org.springframework.scheduling.annotation.Scheduled; diff --git a/instrumentation/spring/spring-scheduling-3.1/javaagent/src/test/java/TriggerTask.java b/instrumentation/spring/spring-scheduling-3.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/scheduling/v3_1/spring/component/TriggerTask.java similarity index 86% rename from instrumentation/spring/spring-scheduling-3.1/javaagent/src/test/java/TriggerTask.java rename to instrumentation/spring/spring-scheduling-3.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/scheduling/v3_1/spring/component/TriggerTask.java index 87003be1364f..d2e9060d00aa 100644 --- a/instrumentation/spring/spring-scheduling-3.1/javaagent/src/test/java/TriggerTask.java +++ b/instrumentation/spring/spring-scheduling-3.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/scheduling/v3_1/spring/component/TriggerTask.java @@ -3,6 +3,8 @@ * SPDX-License-Identifier: Apache-2.0 */ +package io.opentelemetry.javaagent.instrumentation.spring.scheduling.v3_1.spring.component; + import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import org.springframework.scheduling.annotation.Scheduled; diff --git a/instrumentation/spring/spring-scheduling-3.1/javaagent/src/test/java/EnhancedClassTaskConfig.java b/instrumentation/spring/spring-scheduling-3.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/scheduling/v3_1/spring/config/EnhancedClassTaskConfig.java similarity index 88% rename from instrumentation/spring/spring-scheduling-3.1/javaagent/src/test/java/EnhancedClassTaskConfig.java rename to instrumentation/spring/spring-scheduling-3.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/scheduling/v3_1/spring/config/EnhancedClassTaskConfig.java index e57516712821..f840cd61d07f 100644 --- a/instrumentation/spring/spring-scheduling-3.1/javaagent/src/test/java/EnhancedClassTaskConfig.java +++ b/instrumentation/spring/spring-scheduling-3.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/scheduling/v3_1/spring/config/EnhancedClassTaskConfig.java @@ -3,6 +3,8 @@ * SPDX-License-Identifier: Apache-2.0 */ +package io.opentelemetry.javaagent.instrumentation.spring.scheduling.v3_1.spring.config; + import java.util.concurrent.CountDownLatch; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; diff --git a/instrumentation/spring/spring-scheduling-3.1/javaagent/src/test/java/IntervalTaskConfig.java b/instrumentation/spring/spring-scheduling-3.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/scheduling/v3_1/spring/config/IntervalTaskConfig.java similarity index 68% rename from instrumentation/spring/spring-scheduling-3.1/javaagent/src/test/java/IntervalTaskConfig.java rename to instrumentation/spring/spring-scheduling-3.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/scheduling/v3_1/spring/config/IntervalTaskConfig.java index 62fd7dbbcad3..e5c69d087cc9 100644 --- a/instrumentation/spring/spring-scheduling-3.1/javaagent/src/test/java/IntervalTaskConfig.java +++ b/instrumentation/spring/spring-scheduling-3.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/scheduling/v3_1/spring/config/IntervalTaskConfig.java @@ -3,6 +3,9 @@ * SPDX-License-Identifier: Apache-2.0 */ +package io.opentelemetry.javaagent.instrumentation.spring.scheduling.v3_1.spring.config; + +import io.opentelemetry.javaagent.instrumentation.spring.scheduling.v3_1.spring.component.IntervalTask; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.scheduling.annotation.EnableScheduling; diff --git a/instrumentation/spring/spring-scheduling-3.1/javaagent/src/test/java/LambdaTaskConfig.java b/instrumentation/spring/spring-scheduling-3.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/scheduling/v3_1/spring/config/LambdaTaskConfig.java similarity index 68% rename from instrumentation/spring/spring-scheduling-3.1/javaagent/src/test/java/LambdaTaskConfig.java rename to instrumentation/spring/spring-scheduling-3.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/scheduling/v3_1/spring/config/LambdaTaskConfig.java index 4d7ec3a3477d..e77c44876808 100644 --- a/instrumentation/spring/spring-scheduling-3.1/javaagent/src/test/java/LambdaTaskConfig.java +++ b/instrumentation/spring/spring-scheduling-3.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/scheduling/v3_1/spring/config/LambdaTaskConfig.java @@ -3,6 +3,9 @@ * SPDX-License-Identifier: Apache-2.0 */ +package io.opentelemetry.javaagent.instrumentation.spring.scheduling.v3_1.spring.config; + +import io.opentelemetry.javaagent.instrumentation.spring.scheduling.v3_1.spring.service.LambdaTaskConfigurer; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.scheduling.annotation.EnableScheduling; diff --git a/instrumentation/spring/spring-scheduling-3.1/javaagent/src/test/java/OneTimeTaskConfig.java b/instrumentation/spring/spring-scheduling-3.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/scheduling/v3_1/spring/config/OneTimeTaskConfig.java similarity index 68% rename from instrumentation/spring/spring-scheduling-3.1/javaagent/src/test/java/OneTimeTaskConfig.java rename to instrumentation/spring/spring-scheduling-3.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/scheduling/v3_1/spring/config/OneTimeTaskConfig.java index fbae8dd32804..46246b9922d3 100644 --- a/instrumentation/spring/spring-scheduling-3.1/javaagent/src/test/java/OneTimeTaskConfig.java +++ b/instrumentation/spring/spring-scheduling-3.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/scheduling/v3_1/spring/config/OneTimeTaskConfig.java @@ -3,6 +3,9 @@ * SPDX-License-Identifier: Apache-2.0 */ +package io.opentelemetry.javaagent.instrumentation.spring.scheduling.v3_1.spring.config; + +import io.opentelemetry.javaagent.instrumentation.spring.scheduling.v3_1.spring.component.OneTimeTask; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.scheduling.annotation.EnableScheduling; diff --git a/instrumentation/spring/spring-scheduling-3.1/javaagent/src/test/java/TaskWithErrorConfig.java b/instrumentation/spring/spring-scheduling-3.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/scheduling/v3_1/spring/config/TaskWithErrorConfig.java similarity index 81% rename from instrumentation/spring/spring-scheduling-3.1/javaagent/src/test/java/TaskWithErrorConfig.java rename to instrumentation/spring/spring-scheduling-3.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/scheduling/v3_1/spring/config/TaskWithErrorConfig.java index da9bbded1b4a..570034f696c8 100644 --- a/instrumentation/spring/spring-scheduling-3.1/javaagent/src/test/java/TaskWithErrorConfig.java +++ b/instrumentation/spring/spring-scheduling-3.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/scheduling/v3_1/spring/config/TaskWithErrorConfig.java @@ -3,7 +3,10 @@ * SPDX-License-Identifier: Apache-2.0 */ +package io.opentelemetry.javaagent.instrumentation.spring.scheduling.v3_1.spring.config; + import io.opentelemetry.instrumentation.testing.GlobalTraceUtil; +import io.opentelemetry.javaagent.instrumentation.spring.scheduling.v3_1.spring.component.TaskWithError; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.scheduling.TaskScheduler; diff --git a/instrumentation/spring/spring-scheduling-3.1/javaagent/src/test/java/TriggerTaskConfig.java b/instrumentation/spring/spring-scheduling-3.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/scheduling/v3_1/spring/config/TriggerTaskConfig.java similarity index 68% rename from instrumentation/spring/spring-scheduling-3.1/javaagent/src/test/java/TriggerTaskConfig.java rename to instrumentation/spring/spring-scheduling-3.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/scheduling/v3_1/spring/config/TriggerTaskConfig.java index c8fa9dc79f07..f9f0dc891e94 100644 --- a/instrumentation/spring/spring-scheduling-3.1/javaagent/src/test/java/TriggerTaskConfig.java +++ b/instrumentation/spring/spring-scheduling-3.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/scheduling/v3_1/spring/config/TriggerTaskConfig.java @@ -3,6 +3,9 @@ * SPDX-License-Identifier: Apache-2.0 */ +package io.opentelemetry.javaagent.instrumentation.spring.scheduling.v3_1.spring.config; + +import io.opentelemetry.javaagent.instrumentation.spring.scheduling.v3_1.spring.component.TriggerTask; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.scheduling.annotation.EnableScheduling; diff --git a/instrumentation/spring/spring-scheduling-3.1/javaagent/src/test/java/LambdaTaskConfigurer.java b/instrumentation/spring/spring-scheduling-3.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/scheduling/v3_1/spring/service/LambdaTaskConfigurer.java similarity index 87% rename from instrumentation/spring/spring-scheduling-3.1/javaagent/src/test/java/LambdaTaskConfigurer.java rename to instrumentation/spring/spring-scheduling-3.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/scheduling/v3_1/spring/service/LambdaTaskConfigurer.java index 59051a534b46..d79607872b7a 100644 --- a/instrumentation/spring/spring-scheduling-3.1/javaagent/src/test/java/LambdaTaskConfigurer.java +++ b/instrumentation/spring/spring-scheduling-3.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/scheduling/v3_1/spring/service/LambdaTaskConfigurer.java @@ -3,6 +3,8 @@ * SPDX-License-Identifier: Apache-2.0 */ +package io.opentelemetry.javaagent.instrumentation.spring.scheduling.v3_1.spring.service; + import java.util.concurrent.CountDownLatch; import org.springframework.scheduling.annotation.SchedulingConfigurer; import org.springframework.scheduling.config.ScheduledTaskRegistrar; diff --git a/instrumentation/spring/spring-security-config-6.0/javaagent/README.md b/instrumentation/spring/spring-security-config-6.0/javaagent/README.md new file mode 100644 index 000000000000..892f5367d500 --- /dev/null +++ b/instrumentation/spring/spring-security-config-6.0/javaagent/README.md @@ -0,0 +1,14 @@ +# OpenTelemetry Javaagent Instrumentation: Spring Security Config + +Javaagent automatic instrumentation to capture `enduser.*` semantic attributes +from Spring Security `Authentication` objects. + +## Settings + +This module honors the [common `otel.instrumentation.common.enduser.*` properties](https://opentelemetry.io/docs/instrumentation/java/automatic/agent-config/#common-instrumentation-configuration) +and the following properties: + +| Property | Type | Default | Description | +|-------------------------------------------------------------------------------|---------|----------|---------------------------------------------------------------------------------------------------------| +| `otel.instrumentation.spring-security.enduser.role.granted-authority-prefix` | String | `ROLE_` | Prefix of granted authorities identifying roles to capture in the `enduser.role` semantic attribute. | +| `otel.instrumentation.spring-security.enduser.scope.granted-authority-prefix` | String | `SCOPE_` | Prefix of granted authorities identifying scopes to capture in the `enduser.scopes` semantic attribute. | diff --git a/instrumentation/spring/spring-security-config-6.0/javaagent/build.gradle.kts b/instrumentation/spring/spring-security-config-6.0/javaagent/build.gradle.kts new file mode 100644 index 000000000000..f30b3eaad1f3 --- /dev/null +++ b/instrumentation/spring/spring-security-config-6.0/javaagent/build.gradle.kts @@ -0,0 +1,38 @@ +plugins { + id("otel.javaagent-instrumentation") +} + +muzzle { + pass { + group.set("org.springframework.security") + module.set("spring-security-config") + versions.set("[6.0.0,]") + + extraDependency("jakarta.servlet:jakarta.servlet-api:6.0.0") + extraDependency("org.springframework.security:spring-security-web:6.0.0") + extraDependency("io.projectreactor:reactor-core:3.5.0") + } +} + +dependencies { + implementation(project(":instrumentation:spring:spring-security-config-6.0:library")) + + library("org.springframework.security:spring-security-config:6.0.0") + library("org.springframework.security:spring-security-web:6.0.0") + library("io.projectreactor:reactor-core:3.5.0") + + testLibrary("org.springframework:spring-test:6.0.0") + testLibrary("jakarta.servlet:jakarta.servlet-api:6.0.0") +} + +otelJava { + minJavaVersionSupported.set(JavaVersion.VERSION_17) +} + +tasks { + test { + systemProperty("otel.instrumentation.common.enduser.id.enabled", "true") + systemProperty("otel.instrumentation.common.enduser.role.enabled", "true") + systemProperty("otel.instrumentation.common.enduser.scope.enabled", "true") + } +} diff --git a/instrumentation/spring/spring-security-config-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/security/config/v6_0/EnduserAttributesCapturerSingletons.java b/instrumentation/spring/spring-security-config-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/security/config/v6_0/EnduserAttributesCapturerSingletons.java new file mode 100644 index 000000000000..9b4ab9416689 --- /dev/null +++ b/instrumentation/spring/spring-security-config-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/security/config/v6_0/EnduserAttributesCapturerSingletons.java @@ -0,0 +1,46 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.security.config.v6_0; + +import io.opentelemetry.instrumentation.spring.security.config.v6_0.EnduserAttributesCapturer; +import io.opentelemetry.javaagent.bootstrap.internal.AgentCommonConfig; +import io.opentelemetry.javaagent.bootstrap.internal.AgentInstrumentationConfig; + +public class EnduserAttributesCapturerSingletons { + + private static final EnduserAttributesCapturer ENDUSER_ATTRIBUTES_CAPTURER = + createEndUserAttributesCapturerFromConfig(); + + private EnduserAttributesCapturerSingletons() {} + + public static EnduserAttributesCapturer enduserAttributesCapturer() { + return ENDUSER_ATTRIBUTES_CAPTURER; + } + + private static EnduserAttributesCapturer createEndUserAttributesCapturerFromConfig() { + EnduserAttributesCapturer capturer = new EnduserAttributesCapturer(); + capturer.setEnduserIdEnabled(AgentCommonConfig.get().getEnduserConfig().isIdEnabled()); + capturer.setEnduserRoleEnabled(AgentCommonConfig.get().getEnduserConfig().isRoleEnabled()); + capturer.setEnduserScopeEnabled(AgentCommonConfig.get().getEnduserConfig().isScopeEnabled()); + + String rolePrefix = + AgentInstrumentationConfig.get() + .getString( + "otel.instrumentation.spring-security.enduser.role.granted-authority-prefix"); + if (rolePrefix != null) { + capturer.setRoleGrantedAuthorityPrefix(rolePrefix); + } + + String scopePrefix = + AgentInstrumentationConfig.get() + .getString( + "otel.instrumentation.spring-security.enduser.scope.granted-authority-prefix"); + if (scopePrefix != null) { + capturer.setScopeGrantedAuthorityPrefix(rolePrefix); + } + return capturer; + } +} diff --git a/instrumentation/spring/spring-security-config-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/security/config/v6_0/servlet/HttpSecurityInstrumentation.java b/instrumentation/spring/spring-security-config-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/security/config/v6_0/servlet/HttpSecurityInstrumentation.java new file mode 100644 index 000000000000..a4153cceafd6 --- /dev/null +++ b/instrumentation/spring/spring-security-config-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/security/config/v6_0/servlet/HttpSecurityInstrumentation.java @@ -0,0 +1,46 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.security.config.v6_0.servlet; + +import static io.opentelemetry.javaagent.instrumentation.spring.security.config.v6_0.EnduserAttributesCapturerSingletons.enduserAttributesCapturer; +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.takesArguments; + +import io.opentelemetry.instrumentation.spring.security.config.v6_0.servlet.EnduserAttributesHttpSecurityCustomizer; +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.security.config.annotation.web.builders.HttpSecurity; + +/** Instrumentation for {@link HttpSecurity}. */ +public class HttpSecurityInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("org.springframework.security.config.annotation.web.builders.HttpSecurity"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod().and(isProtected()).and(named("performBuild")).and(takesArguments(0)), + getClass().getName() + "$PerformBuildAdvice"); + } + + @SuppressWarnings("unused") + public static class PerformBuildAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter(@Advice.This HttpSecurity httpSecurity) { + new EnduserAttributesHttpSecurityCustomizer(enduserAttributesCapturer()) + .customize(httpSecurity); + } + } +} diff --git a/instrumentation/spring/spring-security-config-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/security/config/v6_0/servlet/SpringSecurityConfigServletInstrumentationModule.java b/instrumentation/spring/spring-security-config-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/security/config/v6_0/servlet/SpringSecurityConfigServletInstrumentationModule.java new file mode 100644 index 000000000000..926bd6d94103 --- /dev/null +++ b/instrumentation/spring/spring-security-config-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/security/config/v6_0/servlet/SpringSecurityConfigServletInstrumentationModule.java @@ -0,0 +1,54 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.security.config.v6_0.servlet; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.hasClassesNamed; +import static java.util.Collections.singletonList; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.bootstrap.internal.AgentCommonConfig; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import java.util.List; +import net.bytebuddy.matcher.ElementMatcher; + +/** Instrumentation module for servlet-based applications that use spring-security-config. */ +@AutoService(InstrumentationModule.class) +public class SpringSecurityConfigServletInstrumentationModule extends InstrumentationModule { + public SpringSecurityConfigServletInstrumentationModule() { + super("spring-security-config-servlet", "spring-security-config-servlet-6.0"); + } + + @Override + public boolean defaultEnabled(ConfigProperties config) { + return super.defaultEnabled(config) + /* + * Since the only thing this module currently does is capture enduser attributes, + * the module can be completely disabled if enduser attributes are disabled. + * + * If any functionality not related to enduser attributes is added to this module, + * then this check will need to move elsewhere to only guard the enduser attributes logic. + */ + && AgentCommonConfig.get().getEnduserConfig().isAnyEnabled(); + } + + @Override + public ElementMatcher.Junction classLoaderMatcher() { + /* + * Ensure this module is only applied to Spring Security >= 6.0, + * since Spring Security >= 6.0 uses Jakarta EE rather than Java EE, + * and this instrumentation module uses Jakarta EE. + */ + return hasClassesNamed( + "org.springframework.security.authentication.ObservationAuthenticationManager"); + } + + @Override + public List typeInstrumentations() { + return singletonList(new HttpSecurityInstrumentation()); + } +} diff --git a/instrumentation/spring/spring-security-config-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/security/config/v6_0/webflux/ServerHttpSecurityInstrumentation.java b/instrumentation/spring/spring-security-config-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/security/config/v6_0/webflux/ServerHttpSecurityInstrumentation.java new file mode 100644 index 000000000000..d75f3103f18d --- /dev/null +++ b/instrumentation/spring/spring-security-config-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/security/config/v6_0/webflux/ServerHttpSecurityInstrumentation.java @@ -0,0 +1,46 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.security.config.v6_0.webflux; + +import static io.opentelemetry.javaagent.instrumentation.spring.security.config.v6_0.EnduserAttributesCapturerSingletons.enduserAttributesCapturer; +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.instrumentation.spring.security.config.v6_0.webflux.EnduserAttributesServerHttpSecurityCustomizer; +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.security.config.web.server.ServerHttpSecurity; + +/** Instrumentation for {@link ServerHttpSecurity}. */ +public class ServerHttpSecurityInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("org.springframework.security.config.web.server.ServerHttpSecurity"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod().and(isPublic()).and(named("build")).and(takesArguments(0)), + getClass().getName() + "$BuildAdvice"); + } + + @SuppressWarnings("unused") + public static class BuildAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter(@Advice.This ServerHttpSecurity serverHttpSecurity) { + new EnduserAttributesServerHttpSecurityCustomizer(enduserAttributesCapturer()) + .customize(serverHttpSecurity); + } + } +} diff --git a/instrumentation/spring/spring-security-config-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/security/config/v6_0/webflux/SpringSecurityConfigWebFluxInstrumentationModule.java b/instrumentation/spring/spring-security-config-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/security/config/v6_0/webflux/SpringSecurityConfigWebFluxInstrumentationModule.java new file mode 100644 index 000000000000..4293e2b26d5d --- /dev/null +++ b/instrumentation/spring/spring-security-config-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/security/config/v6_0/webflux/SpringSecurityConfigWebFluxInstrumentationModule.java @@ -0,0 +1,42 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.security.config.v6_0.webflux; + +import static java.util.Collections.singletonList; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.bootstrap.internal.AgentCommonConfig; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import java.util.List; + +/** Instrumentation module for webflux-based applications that use spring-security-config. */ +@AutoService(InstrumentationModule.class) +public class SpringSecurityConfigWebFluxInstrumentationModule extends InstrumentationModule { + + public SpringSecurityConfigWebFluxInstrumentationModule() { + super("spring-security-config-webflux", "spring-security-config-webflux-6.0"); + } + + @Override + public boolean defaultEnabled(ConfigProperties config) { + return super.defaultEnabled(config) + /* + * Since the only thing this module currently does is capture enduser attributes, + * the module can be completely disabled if enduser attributes are disabled. + * + * If any functionality not related to enduser attributes is added to this module, + * then this check will need to move elsewhere to only guard the enduser attributes logic. + */ + && AgentCommonConfig.get().getEnduserConfig().isAnyEnabled(); + } + + @Override + public List typeInstrumentations() { + return singletonList(new ServerHttpSecurityInstrumentation()); + } +} diff --git a/instrumentation/spring/spring-security-config-6.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/security/config/v6_0/servlet/HttpSecurityInstrumentationTest.java b/instrumentation/spring/spring-security-config-6.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/security/config/v6_0/servlet/HttpSecurityInstrumentationTest.java new file mode 100644 index 000000000000..df936006c59c --- /dev/null +++ b/instrumentation/spring/spring-security-config-6.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/security/config/v6_0/servlet/HttpSecurityInstrumentationTest.java @@ -0,0 +1,62 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.security.config.v6_0.servlet; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.instrumentation.spring.security.config.v6_0.servlet.EnduserAttributesCapturingServletFilter; +import java.util.Collections; +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.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.ObjectPostProcessor; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.web.DefaultSecurityFilterChain; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +@ExtendWith(MockitoExtension.class) +@ExtendWith(SpringExtension.class) +class HttpSecurityInstrumentationTest { + + @Configuration + static class TestConfiguration {} + + @Mock ObjectPostProcessor objectPostProcessor; + + /** + * Ensures that {@link HttpSecurityInstrumentation} registers a {@link + * EnduserAttributesCapturingServletFilter} in the filter chain. + * + *

    Usage of the filter is covered in other unit tests. + */ + @Test + void ensureFilterRegistered(@Autowired ApplicationContext applicationContext) throws Exception { + + AuthenticationManagerBuilder authenticationBuilder = + new AuthenticationManagerBuilder(objectPostProcessor); + + HttpSecurity httpSecurity = + new HttpSecurity( + objectPostProcessor, + authenticationBuilder, + Collections.singletonMap(ApplicationContext.class, applicationContext)); + + DefaultSecurityFilterChain filterChain = httpSecurity.build(); + + assertThat(filterChain.getFilters()) + .filteredOn( + item -> + item.getClass() + .getName() + .endsWith(EnduserAttributesCapturingServletFilter.class.getSimpleName())) + .hasSize(1); + } +} diff --git a/instrumentation/spring/spring-security-config-6.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/security/config/v6_0/webflux/ServerHttpSecurityInstrumentationTest.java b/instrumentation/spring/spring-security-config-6.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/security/config/v6_0/webflux/ServerHttpSecurityInstrumentationTest.java new file mode 100644 index 000000000000..7ad60fd21f5a --- /dev/null +++ b/instrumentation/spring/spring-security-config-6.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/security/config/v6_0/webflux/ServerHttpSecurityInstrumentationTest.java @@ -0,0 +1,38 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.security.config.v6_0.webflux; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.instrumentation.spring.security.config.v6_0.webflux.EnduserAttributesCapturingWebFilter; +import org.junit.jupiter.api.Test; +import org.springframework.security.config.web.server.ServerHttpSecurity; +import org.springframework.security.web.server.SecurityWebFilterChain; + +class ServerHttpSecurityInstrumentationTest { + + /** + * Ensures that {@link ServerHttpSecurityInstrumentation} registers a {@link + * EnduserAttributesCapturingWebFilter} in the filter chain. + * + *

    Usage of the filter is covered in other unit tests. + */ + @Test + void ensureFilterRegistered() { + + ServerHttpSecurity serverHttpSecurity = ServerHttpSecurity.http(); + + SecurityWebFilterChain securityWebFilterChain = serverHttpSecurity.build(); + + assertThat(securityWebFilterChain.getWebFilters().collectList().block()) + .filteredOn( + item -> + item.getClass() + .getName() + .endsWith(EnduserAttributesCapturingWebFilter.class.getSimpleName())) + .hasSize(1); + } +} diff --git a/instrumentation/spring/spring-security-config-6.0/library/README.md b/instrumentation/spring/spring-security-config-6.0/library/README.md new file mode 100644 index 000000000000..ab735c5586b1 --- /dev/null +++ b/instrumentation/spring/spring-security-config-6.0/library/README.md @@ -0,0 +1,77 @@ +# OpenTelemetry Instrumentation: Spring Security Config + +Provides a Servlet `Filter` and a WebFlux `WebFilter` to capture `enduser.*` semantic attributes +from Spring Security `Authentication` objects. + +Also provides `Customizer` implementations to insert those filters into the filter chains created by +`HttpSecurity` and `ServerHttpSecurity`, respectively. + +## Usage in Spring WebMVC Applications + +When not using [automatic instrumentation](../javaagent/), you can enable enduser attribute capturing +for a `SecurityFilterChain` by appling an `EnduserAttributesHttpSecurityCustomizer` +to the `HttpSecurity` which constructs the `SecurityFilterChain`. + +```java +import io.opentelemetry.instrumentation.spring.security.config.v6_0.EnduserAttributesCapturer; +import io.opentelemetry.instrumentation.spring.security.config.v6_0.servlet.EnduserAttributesHttpSecurityCustomizer; + +@Configuration +@EnableWebSecurity +class MyWebSecurityConfig { + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + // First, apply application related configuration to http + + // Then, apply enduser.* attribute capturing + EnduserAttributesCapturer capturer = new EnduserAttributesCapturer(); + // Set properties of capturer. Defaults shown. + capturer.setEnduserIdEnabled(false); + capturer.setEnduserRoleEnabled(false); + capturer.setEnduserScopeEnabled(false); + capturer.setRoleGrantedAuthorityPrefix("ROLE_"); + capturer.setScopeGrantedAuthorityPrefix("SCOPE_"); + + new EnduserAttributesHttpSecurityCustomizer(capturer) + .customize(http); + + return http.build(); + } +} +``` + +## Usage in Spring WebFlux Applications + +When not using [automatic instrumentation](../javaagent/), you can enable enduser attribute capturing +for a `SecurityWebFilterChain` by appling an `EnduserAttributesServerHttpSecurityCustomizer` +to the `ServerHttpSecurity` which constructs the `SecurityWebFilterChain`. + +```java +import io.opentelemetry.instrumentation.spring.security.config.v6_0.EnduserAttributesCapturer; +import io.opentelemetry.instrumentation.spring.security.config.v6_0.webflux.EnduserAttributesServerHttpSecurityCustomizer; + +@Configuration +@EnableWebFluxSecurity +class MyWebFluxSecurityConfig { + + @Bean + public SecurityWebFilterChain filterChain(ServerHttpSecurity http) throws Exception { + // First, apply application related configuration to http + + // Then, apply enduser.* attribute capturing + EnduserAttributesCapturer capturer = new EnduserAttributesCapturer(); + // Set properties of capturer. Defaults shown. + capturer.setEnduserIdEnabled(false); + capturer.setEnduserRoleEnabled(false); + capturer.setEnduserScopeEnabled(false); + capturer.setRoleGrantedAuthorityPrefix("ROLE_"); + capturer.setScopeGrantedAuthorityPrefix("SCOPE_"); + + new EnduserAttributesServerHttpSecurityCustomizer(capturer) + .customize(http); + + return http.build(); + } +} +``` diff --git a/instrumentation/spring/spring-security-config-6.0/library/build.gradle.kts b/instrumentation/spring/spring-security-config-6.0/library/build.gradle.kts new file mode 100644 index 000000000000..368eb0272570 --- /dev/null +++ b/instrumentation/spring/spring-security-config-6.0/library/build.gradle.kts @@ -0,0 +1,20 @@ +plugins { + id("otel.library-instrumentation") +} + +dependencies { + library("org.springframework.security:spring-security-config:6.0.0") + library("org.springframework.security:spring-security-web:6.0.0") + library("org.springframework:spring-web:6.0.0") + library("io.projectreactor:reactor-core:3.5.0") + library("jakarta.servlet:jakarta.servlet-api:6.0.0") + + implementation(project(":instrumentation:reactor:reactor-3.1:library")) + + testImplementation(project(":testing-common")) + testLibrary("org.springframework:spring-test:6.0.0") +} + +otelJava { + minJavaVersionSupported.set(JavaVersion.VERSION_17) +} diff --git a/instrumentation/spring/spring-security-config-6.0/library/src/main/java/io/opentelemetry/instrumentation/spring/security/config/v6_0/EnduserAttributesCapturer.java b/instrumentation/spring/spring-security-config-6.0/library/src/main/java/io/opentelemetry/instrumentation/spring/security/config/v6_0/EnduserAttributesCapturer.java new file mode 100644 index 000000000000..dafd161a2443 --- /dev/null +++ b/instrumentation/spring/spring-security-config-6.0/library/src/main/java/io/opentelemetry/instrumentation/spring/security/config/v6_0/EnduserAttributesCapturer.java @@ -0,0 +1,155 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.security.config.v6_0; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.instrumenter.LocalRootSpan; +import java.util.Objects; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; + +/** + * Captures {@code enduser.*} semantic attributes from {@link Authentication} objects. + * + *

    After construction, you must selectively enable which attributes you want captured by calling + * the appropriate {@code setEnduser*Enabled(true)} method. + */ +public final class EnduserAttributesCapturer { + + // copied from EnduserIncubatingAttributes + private static final AttributeKey ENDUSER_ID = AttributeKey.stringKey("enduser.id"); + private static final AttributeKey ENDUSER_ROLE = AttributeKey.stringKey("enduser.role"); + private static final AttributeKey ENDUSER_SCOPE = AttributeKey.stringKey("enduser.scope"); + + private static final String DEFAULT_ROLE_PREFIX = "ROLE_"; + private static final String DEFAULT_SCOPE_PREFIX = "SCOPE_"; + + /** Determines if the {@code enduser.id} attribute should be captured. */ + private boolean enduserIdEnabled; + + /** Determines if the {@code enduser.role} attribute should be captured. */ + private boolean enduserRoleEnabled; + + /** Determines if the {@code enduser.scope} attribute should be captured. */ + private boolean enduserScopeEnabled; + + /** The prefix used to find {@link GrantedAuthority} objects for roles. */ + private String roleGrantedAuthorityPrefix = DEFAULT_ROLE_PREFIX; + + /** The prefix used to find {@link GrantedAuthority} objects for scopes. */ + private String scopeGrantedAuthorityPrefix = DEFAULT_SCOPE_PREFIX; + + /** + * Captures the {@code enduser.*} semantic attributes from the given {@link Authentication} into + * the {@link LocalRootSpan} of the given {@link Context}. + * + *

    Only the attributes enabled via the {@code setEnduser*Enabled(true)} methods are captured. + * + *

    The following attributes can be captured: + * + *

      + *
    • {@code enduser.id} - from {@link Authentication#getName()} + *
    • {@code enduser.role} - a comma-separated list from the {@link + * Authentication#getAuthorities()} with the configured {@link + * #getRoleGrantedAuthorityPrefix() role prefix} + *
    • {@code enduser.scope} - a comma-separated list from the {@link + * Authentication#getAuthorities()} with the configured {@link + * #getScopeGrantedAuthorityPrefix() scope prefix} + *
    + * + * @param otelContext the context from which the {@link LocalRootSpan} in which to capture the + * attributes will be retrieved + * @param authentication the authentication from which to determine the {@code enduser.*} + * attributes. + */ + public void captureEnduserAttributes(Context otelContext, Authentication authentication) { + if (authentication != null) { + Span localRootSpan = LocalRootSpan.fromContext(otelContext); + + if (enduserIdEnabled && authentication.getName() != null) { + localRootSpan.setAttribute(ENDUSER_ID, authentication.getName()); + } + + StringBuilder roleBuilder = null; + StringBuilder scopeBuilder = null; + if (enduserRoleEnabled || enduserScopeEnabled) { + for (GrantedAuthority authority : authentication.getAuthorities()) { + String authorityString = authority.getAuthority(); + if (enduserRoleEnabled && authorityString.startsWith(roleGrantedAuthorityPrefix)) { + roleBuilder = appendSuffix(roleGrantedAuthorityPrefix, authorityString, roleBuilder); + } else if (enduserScopeEnabled + && authorityString.startsWith(scopeGrantedAuthorityPrefix)) { + scopeBuilder = appendSuffix(scopeGrantedAuthorityPrefix, authorityString, scopeBuilder); + } + } + } + if (roleBuilder != null) { + localRootSpan.setAttribute(ENDUSER_ROLE, roleBuilder.toString()); + } + if (scopeBuilder != null) { + localRootSpan.setAttribute(ENDUSER_SCOPE, scopeBuilder.toString()); + } + } + } + + private static StringBuilder appendSuffix( + String prefix, String authorityString, StringBuilder builder) { + if (authorityString.length() > prefix.length()) { + String suffix = authorityString.substring(prefix.length()); + if (builder == null) { + builder = new StringBuilder(); + builder.append(suffix); + } else { + builder.append(",").append(suffix); + } + } + return builder; + } + + public boolean isEnduserIdEnabled() { + return enduserIdEnabled; + } + + public void setEnduserIdEnabled(boolean enduserIdEnabled) { + this.enduserIdEnabled = enduserIdEnabled; + } + + public boolean isEnduserRoleEnabled() { + return enduserRoleEnabled; + } + + public void setEnduserRoleEnabled(boolean enduserRoleEnabled) { + this.enduserRoleEnabled = enduserRoleEnabled; + } + + public boolean isEnduserScopeEnabled() { + return enduserScopeEnabled; + } + + public void setEnduserScopeEnabled(boolean enduserScopeEnabled) { + this.enduserScopeEnabled = enduserScopeEnabled; + } + + public String getRoleGrantedAuthorityPrefix() { + return roleGrantedAuthorityPrefix; + } + + public void setRoleGrantedAuthorityPrefix(String roleGrantedAuthorityPrefix) { + this.roleGrantedAuthorityPrefix = + Objects.requireNonNull(roleGrantedAuthorityPrefix, "rolePrefix must not be null"); + } + + public String getScopeGrantedAuthorityPrefix() { + return scopeGrantedAuthorityPrefix; + } + + public void setScopeGrantedAuthorityPrefix(String scopeGrantedAuthorityPrefix) { + this.scopeGrantedAuthorityPrefix = + Objects.requireNonNull(scopeGrantedAuthorityPrefix, "scopePrefix must not be null"); + } +} diff --git a/instrumentation/spring/spring-security-config-6.0/library/src/main/java/io/opentelemetry/instrumentation/spring/security/config/v6_0/servlet/EnduserAttributesCapturingServletFilter.java b/instrumentation/spring/spring-security-config-6.0/library/src/main/java/io/opentelemetry/instrumentation/spring/security/config/v6_0/servlet/EnduserAttributesCapturingServletFilter.java new file mode 100644 index 000000000000..c159576b6577 --- /dev/null +++ b/instrumentation/spring/spring-security-config-6.0/library/src/main/java/io/opentelemetry/instrumentation/spring/security/config/v6_0/servlet/EnduserAttributesCapturingServletFilter.java @@ -0,0 +1,46 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.security.config.v6_0.servlet; + +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.spring.security.config.v6_0.EnduserAttributesCapturer; +import jakarta.servlet.Filter; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import java.io.IOException; +import java.util.Objects; +import org.springframework.security.core.context.SecurityContextHolder; + +/** + * A servlet {@link Filter} that captures {@code endpoint.*} semantic attributes from the {@link + * org.springframework.security.core.Authentication} in the current {@link + * org.springframework.security.core.context.SecurityContext} retrieved from {@link + * SecurityContextHolder}. + * + *

    Inserted into the filter chain by {@link EnduserAttributesHttpSecurityCustomizer} after all + * the filters that populate the {@link org.springframework.security.core.context.SecurityContext} + * in the {@link org.springframework.security.core.context.SecurityContextHolder}. + */ +public class EnduserAttributesCapturingServletFilter implements Filter { + + private final EnduserAttributesCapturer capturer; + + public EnduserAttributesCapturingServletFilter(EnduserAttributesCapturer capturer) { + this.capturer = Objects.requireNonNull(capturer, "capturer must not be null"); + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + + capturer.captureEnduserAttributes( + Context.current(), SecurityContextHolder.getContext().getAuthentication()); + + chain.doFilter(request, response); + } +} diff --git a/instrumentation/spring/spring-security-config-6.0/library/src/main/java/io/opentelemetry/instrumentation/spring/security/config/v6_0/servlet/EnduserAttributesHttpSecurityCustomizer.java b/instrumentation/spring/spring-security-config-6.0/library/src/main/java/io/opentelemetry/instrumentation/spring/security/config/v6_0/servlet/EnduserAttributesHttpSecurityCustomizer.java new file mode 100644 index 000000000000..7480aed192ce --- /dev/null +++ b/instrumentation/spring/spring-security-config-6.0/library/src/main/java/io/opentelemetry/instrumentation/spring/security/config/v6_0/servlet/EnduserAttributesHttpSecurityCustomizer.java @@ -0,0 +1,37 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.security.config.v6_0.servlet; + +import io.opentelemetry.instrumentation.spring.security.config.v6_0.EnduserAttributesCapturer; +import java.util.Objects; +import org.springframework.security.config.Customizer; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.web.access.intercept.AuthorizationFilter; + +/** + * Customizes a {@link HttpSecurity} by inserting a {@link EnduserAttributesCapturingServletFilter} + * after all the filters that populate the {@link + * org.springframework.security.core.context.SecurityContext} in the {@link + * org.springframework.security.core.context.SecurityContextHolder}. + */ +public class EnduserAttributesHttpSecurityCustomizer implements Customizer { + + private final EnduserAttributesCapturer capturer; + + public EnduserAttributesHttpSecurityCustomizer(EnduserAttributesCapturer capturer) { + this.capturer = Objects.requireNonNull(capturer, "capturer must not be null"); + } + + @Override + public void customize(HttpSecurity httpSecurity) { + /* + * See org.springframework.security.config.annotation.web.builders.FilterOrderRegistration + * for where this appears in the chain. + */ + httpSecurity.addFilterBefore( + new EnduserAttributesCapturingServletFilter(capturer), AuthorizationFilter.class); + } +} diff --git a/instrumentation/spring/spring-security-config-6.0/library/src/main/java/io/opentelemetry/instrumentation/spring/security/config/v6_0/webflux/EnduserAttributesCapturingWebFilter.java b/instrumentation/spring/spring-security-config-6.0/library/src/main/java/io/opentelemetry/instrumentation/spring/security/config/v6_0/webflux/EnduserAttributesCapturingWebFilter.java new file mode 100644 index 000000000000..0b6c8533b698 --- /dev/null +++ b/instrumentation/spring/spring-security-config-6.0/library/src/main/java/io/opentelemetry/instrumentation/spring/security/config/v6_0/webflux/EnduserAttributesCapturingWebFilter.java @@ -0,0 +1,51 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.security.config.v6_0.webflux; + +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.reactor.v3_1.ContextPropagationOperator; +import io.opentelemetry.instrumentation.spring.security.config.v6_0.EnduserAttributesCapturer; +import java.util.Objects; +import org.springframework.security.core.context.ReactiveSecurityContextHolder; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.WebFilter; +import org.springframework.web.server.WebFilterChain; +import reactor.core.publisher.Mono; + +/** + * A {@link WebFilter} that captures {@code endpoint.*} semantic attributes from the {@link + * org.springframework.security.core.Authentication} in the current {@link + * org.springframework.security.core.context.SecurityContext} retrieved from {@link + * ReactiveSecurityContextHolder}. + * + *

    Inserted into the filter chain by {@link EnduserAttributesServerHttpSecurityCustomizer} after + * all the filters that populate the {@link + * org.springframework.security.core.context.SecurityContext} in the {@link + * org.springframework.security.core.context.ReactiveSecurityContextHolder}. + */ +public class EnduserAttributesCapturingWebFilter implements WebFilter { + + private final EnduserAttributesCapturer capturer; + + public EnduserAttributesCapturingWebFilter(EnduserAttributesCapturer capturer) { + this.capturer = Objects.requireNonNull(capturer, "capturer must not be null"); + } + + @Override + public Mono filter(ServerWebExchange exchange, WebFilterChain chain) { + + Context threadLocalOtelContext = Context.current(); + + return Mono.zip(ReactiveSecurityContextHolder.getContext(), Mono.deferContextual(Mono::just)) + .doOnNext( + t2 -> + capturer.captureEnduserAttributes( + ContextPropagationOperator.getOpenTelemetryContext( + reactor.util.context.Context.of(t2.getT2()), threadLocalOtelContext), + t2.getT1().getAuthentication())) + .then(chain.filter(exchange)); + } +} diff --git a/instrumentation/spring/spring-security-config-6.0/library/src/main/java/io/opentelemetry/instrumentation/spring/security/config/v6_0/webflux/EnduserAttributesServerHttpSecurityCustomizer.java b/instrumentation/spring/spring-security-config-6.0/library/src/main/java/io/opentelemetry/instrumentation/spring/security/config/v6_0/webflux/EnduserAttributesServerHttpSecurityCustomizer.java new file mode 100644 index 000000000000..06e036100579 --- /dev/null +++ b/instrumentation/spring/spring-security-config-6.0/library/src/main/java/io/opentelemetry/instrumentation/spring/security/config/v6_0/webflux/EnduserAttributesServerHttpSecurityCustomizer.java @@ -0,0 +1,34 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.security.config.v6_0.webflux; + +import io.opentelemetry.instrumentation.spring.security.config.v6_0.EnduserAttributesCapturer; +import java.util.Objects; +import org.springframework.security.config.Customizer; +import org.springframework.security.config.web.server.SecurityWebFiltersOrder; +import org.springframework.security.config.web.server.ServerHttpSecurity; + +/** + * Customizes a {@link ServerHttpSecurity} by inserting a {@link + * EnduserAttributesCapturingWebFilter} after all the filters that populate the {@link + * org.springframework.security.core.context.SecurityContext} in the {@link + * org.springframework.security.core.context.ReactiveSecurityContextHolder}. + */ +public class EnduserAttributesServerHttpSecurityCustomizer + implements Customizer { + + private final EnduserAttributesCapturer capturer; + + public EnduserAttributesServerHttpSecurityCustomizer(EnduserAttributesCapturer capturer) { + this.capturer = Objects.requireNonNull(capturer, "capturer must not be null"); + } + + @Override + public void customize(ServerHttpSecurity serverHttpSecurity) { + serverHttpSecurity.addFilterBefore( + new EnduserAttributesCapturingWebFilter(capturer), SecurityWebFiltersOrder.LOGOUT); + } +} diff --git a/instrumentation/spring/spring-security-config-6.0/library/src/test/java/io/opentelemetry/instrumentation/spring/security/config/v6_0/EnduserAttributesCapturerTest.java b/instrumentation/spring/spring-security-config-6.0/library/src/test/java/io/opentelemetry/instrumentation/spring/security/config/v6_0/EnduserAttributesCapturerTest.java new file mode 100644 index 000000000000..511afc49bbbb --- /dev/null +++ b/instrumentation/spring/spring-security-config-6.0/library/src/test/java/io/opentelemetry/instrumentation/spring/security/config/v6_0/EnduserAttributesCapturerTest.java @@ -0,0 +1,218 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.security.config.v6_0; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.LibraryInstrumentationExtension; +import io.opentelemetry.sdk.testing.assertj.SpanDataAssert; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.semconv.incubating.EnduserIncubatingAttributes; +import java.util.Arrays; +import java.util.function.Consumer; +import org.assertj.core.api.Condition; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken; + +public class EnduserAttributesCapturerTest { + + @RegisterExtension + static final InstrumentationExtension testing = LibraryInstrumentationExtension.create(); + + @Test + void nothingEnabled() { + EnduserAttributesCapturer capturer = new EnduserAttributesCapturer(); + + Authentication authentication = + new PreAuthenticatedAuthenticationToken( + "principal", + null, + Arrays.asList( + new SimpleGrantedAuthority("ROLE_role1"), + new SimpleGrantedAuthority("ROLE_role2"), + new SimpleGrantedAuthority("SCOPE_scope1"), + new SimpleGrantedAuthority("SCOPE_scope2"))); + + test( + capturer, + authentication, + span -> + span.doesNotHave(attribute(EnduserIncubatingAttributes.ENDUSER_ID)) + .doesNotHave(attribute(EnduserIncubatingAttributes.ENDUSER_ROLE)) + .doesNotHave(attribute(EnduserIncubatingAttributes.ENDUSER_SCOPE))); + } + + @Test + void allEnabledButNoRoles() { + EnduserAttributesCapturer capturer = new EnduserAttributesCapturer(); + capturer.setEnduserIdEnabled(true); + capturer.setEnduserRoleEnabled(true); + capturer.setEnduserScopeEnabled(true); + + Authentication authentication = + new PreAuthenticatedAuthenticationToken( + "principal", + null, + Arrays.asList( + new SimpleGrantedAuthority("SCOPE_scope1"), + new SimpleGrantedAuthority("SCOPE_scope2"))); + + test( + capturer, + authentication, + span -> + span.hasAttribute(EnduserIncubatingAttributes.ENDUSER_ID, "principal") + .doesNotHave(attribute(EnduserIncubatingAttributes.ENDUSER_ROLE)) + .hasAttribute(EnduserIncubatingAttributes.ENDUSER_SCOPE, "scope1,scope2")); + } + + @Test + void allEnabledButNoScopes() { + EnduserAttributesCapturer capturer = new EnduserAttributesCapturer(); + capturer.setEnduserIdEnabled(true); + capturer.setEnduserRoleEnabled(true); + capturer.setEnduserScopeEnabled(true); + + Authentication authentication = + new PreAuthenticatedAuthenticationToken( + "principal", + null, + Arrays.asList( + new SimpleGrantedAuthority("ROLE_role1"), + new SimpleGrantedAuthority("ROLE_role2"))); + + test( + capturer, + authentication, + span -> + span.hasAttribute(EnduserIncubatingAttributes.ENDUSER_ID, "principal") + .hasAttribute(EnduserIncubatingAttributes.ENDUSER_ROLE, "role1,role2") + .doesNotHave(attribute(EnduserIncubatingAttributes.ENDUSER_SCOPE))); + } + + @Test + void onlyEnduserIdEnabled() { + EnduserAttributesCapturer capturer = new EnduserAttributesCapturer(); + capturer.setEnduserIdEnabled(true); + + Authentication authentication = + new PreAuthenticatedAuthenticationToken( + "principal", + null, + Arrays.asList( + new SimpleGrantedAuthority("ROLE_role1"), + new SimpleGrantedAuthority("ROLE_role2"), + new SimpleGrantedAuthority("SCOPE_scope1"), + new SimpleGrantedAuthority("SCOPE_scope2"))); + + test( + capturer, + authentication, + span -> + span.hasAttribute(EnduserIncubatingAttributes.ENDUSER_ID, "principal") + .doesNotHave(attribute(EnduserIncubatingAttributes.ENDUSER_ROLE)) + .doesNotHave(attribute(EnduserIncubatingAttributes.ENDUSER_SCOPE))); + } + + @Test + void onlyEnduserRoleEnabled() { + EnduserAttributesCapturer capturer = new EnduserAttributesCapturer(); + capturer.setEnduserRoleEnabled(true); + + Authentication authentication = + new PreAuthenticatedAuthenticationToken( + "principal", + null, + Arrays.asList( + new SimpleGrantedAuthority("ROLE_role1"), + new SimpleGrantedAuthority("ROLE_role2"), + new SimpleGrantedAuthority("SCOPE_scope1"), + new SimpleGrantedAuthority("SCOPE_scope2"))); + + test( + capturer, + authentication, + span -> + span.doesNotHave(attribute(EnduserIncubatingAttributes.ENDUSER_ID)) + .hasAttribute(EnduserIncubatingAttributes.ENDUSER_ROLE, "role1,role2") + .doesNotHave(attribute(EnduserIncubatingAttributes.ENDUSER_SCOPE))); + } + + @Test + void onlyEnduserScopeEnabled() { + EnduserAttributesCapturer capturer = new EnduserAttributesCapturer(); + capturer.setEnduserScopeEnabled(true); + + Authentication authentication = + new PreAuthenticatedAuthenticationToken( + "principal", + null, + Arrays.asList( + new SimpleGrantedAuthority("ROLE_role1"), + new SimpleGrantedAuthority("ROLE_role2"), + new SimpleGrantedAuthority("SCOPE_scope1"), + new SimpleGrantedAuthority("SCOPE_scope2"))); + + test( + capturer, + authentication, + span -> + span.doesNotHave(attribute(EnduserIncubatingAttributes.ENDUSER_ID)) + .doesNotHave(attribute(EnduserIncubatingAttributes.ENDUSER_ROLE)) + .hasAttribute(EnduserIncubatingAttributes.ENDUSER_SCOPE, "scope1,scope2")); + } + + @Test + void allEnabledAndAlternatePrefix() { + EnduserAttributesCapturer capturer = new EnduserAttributesCapturer(); + capturer.setEnduserIdEnabled(true); + capturer.setEnduserRoleEnabled(true); + capturer.setEnduserScopeEnabled(true); + capturer.setRoleGrantedAuthorityPrefix("role_"); + capturer.setScopeGrantedAuthorityPrefix("scope_"); + + Authentication authentication = + new PreAuthenticatedAuthenticationToken( + "principal", + null, + Arrays.asList( + new SimpleGrantedAuthority("role_role1"), + new SimpleGrantedAuthority("role_role2"), + new SimpleGrantedAuthority("scope_scope1"), + new SimpleGrantedAuthority("scope_scope2"))); + + test( + capturer, + authentication, + span -> + span.hasAttribute(EnduserIncubatingAttributes.ENDUSER_ID, "principal") + .hasAttribute(EnduserIncubatingAttributes.ENDUSER_ROLE, "role1,role2") + .hasAttribute(EnduserIncubatingAttributes.ENDUSER_SCOPE, "scope1,scope2")); + } + + void test( + EnduserAttributesCapturer capturer, + Authentication authentication, + Consumer assertions) { + testing.runWithHttpServerSpan( + () -> { + Context otelContext = Context.current(); + capturer.captureEnduserAttributes(otelContext, authentication); + }); + + testing.waitAndAssertTraces(trace -> trace.hasSpansSatisfyingExactly(assertions)); + } + + private static Condition attribute(AttributeKey attributeKey) { + return new Condition<>( + spanData -> spanData.getAttributes().get(attributeKey) != null, + "attribute " + attributeKey); + } +} diff --git a/instrumentation/spring/spring-security-config-6.0/library/src/test/java/io/opentelemetry/instrumentation/spring/security/config/v6_0/servlet/EnduserAttributesCapturingServletFilterTest.java b/instrumentation/spring/spring-security-config-6.0/library/src/test/java/io/opentelemetry/instrumentation/spring/security/config/v6_0/servlet/EnduserAttributesCapturingServletFilterTest.java new file mode 100644 index 000000000000..0d8b2150d4f6 --- /dev/null +++ b/instrumentation/spring/spring-security-config-6.0/library/src/test/java/io/opentelemetry/instrumentation/spring/security/config/v6_0/servlet/EnduserAttributesCapturingServletFilterTest.java @@ -0,0 +1,82 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.security.config.v6_0.servlet; + +import io.opentelemetry.instrumentation.spring.security.config.v6_0.EnduserAttributesCapturer; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.LibraryInstrumentationExtension; +import io.opentelemetry.semconv.incubating.EnduserIncubatingAttributes; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import java.util.Arrays; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.springframework.mock.web.MockFilterChain; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken; + +class EnduserAttributesCapturingServletFilterTest { + + @RegisterExtension InstrumentationExtension testing = LibraryInstrumentationExtension.create(); + + /** + * Tests to ensure enduser attributes are captured. + * + *

    This just tests one scenario of {@link EnduserAttributesCapturer} to ensure that it is + * invoked properly by the filter. {@link + * io.opentelemetry.instrumentation.spring.security.config.v6_0.EnduserAttributesCapturerTest} + * tests many other scenarios. + */ + @Test + void test() throws Exception { + + EnduserAttributesCapturer capturer = new EnduserAttributesCapturer(); + capturer.setEnduserIdEnabled(true); + capturer.setEnduserRoleEnabled(true); + capturer.setEnduserScopeEnabled(true); + EnduserAttributesCapturingServletFilter filter = + new EnduserAttributesCapturingServletFilter(capturer); + + testing.runWithHttpServerSpan( + () -> { + ServletRequest request = new MockHttpServletRequest(); + ServletResponse response = new MockHttpServletResponse(); + FilterChain filterChain = new MockFilterChain(); + + SecurityContext previousSecurityContext = SecurityContextHolder.getContext(); + try { + SecurityContext securityContext = SecurityContextHolder.createEmptyContext(); + securityContext.setAuthentication( + new PreAuthenticatedAuthenticationToken( + "principal", + null, + Arrays.asList( + new SimpleGrantedAuthority("ROLE_role1"), + new SimpleGrantedAuthority("ROLE_role2"), + new SimpleGrantedAuthority("SCOPE_scope1"), + new SimpleGrantedAuthority("SCOPE_scope2")))); + SecurityContextHolder.setContext(securityContext); + + filter.doFilter(request, response, filterChain); + } finally { + SecurityContextHolder.setContext(previousSecurityContext); + } + }); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasAttribute(EnduserIncubatingAttributes.ENDUSER_ID, "principal") + .hasAttribute(EnduserIncubatingAttributes.ENDUSER_ROLE, "role1,role2") + .hasAttribute(EnduserIncubatingAttributes.ENDUSER_SCOPE, "scope1,scope2"))); + } +} diff --git a/instrumentation/spring/spring-security-config-6.0/library/src/test/java/io/opentelemetry/instrumentation/spring/security/config/v6_0/servlet/EnduserAttributesHttpSecurityCustomizerTest.java b/instrumentation/spring/spring-security-config-6.0/library/src/test/java/io/opentelemetry/instrumentation/spring/security/config/v6_0/servlet/EnduserAttributesHttpSecurityCustomizerTest.java new file mode 100644 index 000000000000..d96c14653649 --- /dev/null +++ b/instrumentation/spring/spring-security-config-6.0/library/src/test/java/io/opentelemetry/instrumentation/spring/security/config/v6_0/servlet/EnduserAttributesHttpSecurityCustomizerTest.java @@ -0,0 +1,61 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.security.config.v6_0.servlet; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.instrumentation.spring.security.config.v6_0.EnduserAttributesCapturer; +import java.util.Collections; +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.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.ObjectPostProcessor; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.web.DefaultSecurityFilterChain; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +@ExtendWith(MockitoExtension.class) +@ExtendWith(SpringExtension.class) +class EnduserAttributesHttpSecurityCustomizerTest { + + @Configuration + static class TestConfiguration {} + + @Mock ObjectPostProcessor objectPostProcessor; + + /** + * Ensures that the {@link EnduserAttributesHttpSecurityCustomizer} registers a {@link + * EnduserAttributesCapturingServletFilter} in the filter chain. + * + *

    Usage of the filter is covered in other unit tests. + */ + @Test + void ensureFilterRegistered(@Autowired ApplicationContext applicationContext) throws Exception { + + AuthenticationManagerBuilder authenticationBuilder = + new AuthenticationManagerBuilder(objectPostProcessor); + HttpSecurity httpSecurity = + new HttpSecurity( + objectPostProcessor, + authenticationBuilder, + Collections.singletonMap(ApplicationContext.class, applicationContext)); + + EnduserAttributesHttpSecurityCustomizer customizer = + new EnduserAttributesHttpSecurityCustomizer(new EnduserAttributesCapturer()); + customizer.customize(httpSecurity); + + DefaultSecurityFilterChain filterChain = httpSecurity.build(); + + assertThat(filterChain.getFilters()) + .filteredOn(EnduserAttributesCapturingServletFilter.class::isInstance) + .hasSize(1); + } +} diff --git a/instrumentation/spring/spring-security-config-6.0/library/src/test/java/io/opentelemetry/instrumentation/spring/security/config/v6_0/webflux/EnduserAttributesCapturingWebFilterTest.java b/instrumentation/spring/spring-security-config-6.0/library/src/test/java/io/opentelemetry/instrumentation/spring/security/config/v6_0/webflux/EnduserAttributesCapturingWebFilterTest.java new file mode 100644 index 000000000000..3d0e7be94d83 --- /dev/null +++ b/instrumentation/spring/spring-security-config-6.0/library/src/test/java/io/opentelemetry/instrumentation/spring/security/config/v6_0/webflux/EnduserAttributesCapturingWebFilterTest.java @@ -0,0 +1,80 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.security.config.v6_0.webflux; + +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.reactor.v3_1.ContextPropagationOperator; +import io.opentelemetry.instrumentation.spring.security.config.v6_0.EnduserAttributesCapturer; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.LibraryInstrumentationExtension; +import io.opentelemetry.semconv.incubating.EnduserIncubatingAttributes; +import java.util.Arrays; +import java.util.Collections; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.springframework.mock.http.server.reactive.MockServerHttpRequest; +import org.springframework.mock.web.server.MockServerWebExchange; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.ReactiveSecurityContextHolder; +import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken; +import org.springframework.web.server.handler.DefaultWebFilterChain; +import reactor.core.publisher.Mono; + +class EnduserAttributesCapturingWebFilterTest { + + @RegisterExtension InstrumentationExtension testing = LibraryInstrumentationExtension.create(); + + /** + * Tests to ensure enduser attributes are captured. + * + *

    This just tests one scenario of {@link EnduserAttributesCapturer} to ensure that it is + * invoked properly by the filter. {@link + * io.opentelemetry.instrumentation.spring.security.config.v6_0.EnduserAttributesCapturerTest} + * tests many other scenarios. + */ + @Test + void test() { + + EnduserAttributesCapturer capturer = new EnduserAttributesCapturer(); + capturer.setEnduserIdEnabled(true); + capturer.setEnduserRoleEnabled(true); + capturer.setEnduserScopeEnabled(true); + EnduserAttributesCapturingWebFilter filter = new EnduserAttributesCapturingWebFilter(capturer); + + testing.runWithHttpServerSpan( + () -> { + MockServerHttpRequest request = MockServerHttpRequest.get("/").build(); + MockServerWebExchange exchange = MockServerWebExchange.from(request); + DefaultWebFilterChain filterChain = + new DefaultWebFilterChain(exch -> Mono.empty(), Collections.emptyList()); + Context otelContext = Context.current(); + filter + .filter(exchange, filterChain) + .contextWrite( + ReactiveSecurityContextHolder.withAuthentication( + new PreAuthenticatedAuthenticationToken( + "principal", + null, + Arrays.asList( + new SimpleGrantedAuthority("ROLE_role1"), + new SimpleGrantedAuthority("ROLE_role2"), + new SimpleGrantedAuthority("SCOPE_scope1"), + new SimpleGrantedAuthority("SCOPE_scope2"))))) + .contextWrite( + context -> + ContextPropagationOperator.storeOpenTelemetryContext(context, otelContext)) + .block(); + }); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasAttribute(EnduserIncubatingAttributes.ENDUSER_ID, "principal") + .hasAttribute(EnduserIncubatingAttributes.ENDUSER_ROLE, "role1,role2") + .hasAttribute(EnduserIncubatingAttributes.ENDUSER_SCOPE, "scope1,scope2"))); + } +} diff --git a/instrumentation/spring/spring-security-config-6.0/library/src/test/java/io/opentelemetry/instrumentation/spring/security/config/v6_0/webflux/EnduserAttributesServerHttpSecurityCustomizerTest.java b/instrumentation/spring/spring-security-config-6.0/library/src/test/java/io/opentelemetry/instrumentation/spring/security/config/v6_0/webflux/EnduserAttributesServerHttpSecurityCustomizerTest.java new file mode 100644 index 000000000000..17fb42675f05 --- /dev/null +++ b/instrumentation/spring/spring-security-config-6.0/library/src/test/java/io/opentelemetry/instrumentation/spring/security/config/v6_0/webflux/EnduserAttributesServerHttpSecurityCustomizerTest.java @@ -0,0 +1,39 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.security.config.v6_0.webflux; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.instrumentation.spring.security.config.v6_0.EnduserAttributesCapturer; +import org.junit.jupiter.api.Test; +import org.springframework.security.config.web.server.ServerHttpSecurity; +import org.springframework.security.web.server.SecurityWebFilterChain; + +class EnduserAttributesServerHttpSecurityCustomizerTest { + + /** + * Ensures that the {@link EnduserAttributesServerHttpSecurityCustomizer} registers a {@link + * EnduserAttributesCapturingWebFilter} in the filter chain. + * + *

    Usage of the filter is covered in other unit tests. + */ + @Test + void ensureFilterRegistered() { + + ServerHttpSecurity serverHttpSecurity = ServerHttpSecurity.http(); + + EnduserAttributesServerHttpSecurityCustomizer customizer = + new EnduserAttributesServerHttpSecurityCustomizer(new EnduserAttributesCapturer()); + + customizer.customize(serverHttpSecurity); + + SecurityWebFilterChain securityWebFilterChain = serverHttpSecurity.build(); + + assertThat(securityWebFilterChain.getWebFilters().collectList().block()) + .filteredOn(EnduserAttributesCapturingWebFilter.class::isInstance) + .hasSize(1); + } +} diff --git a/instrumentation/spring/spring-web/spring-web-3.1/library/src/main/java/io/opentelemetry/instrumentation/spring/web/v3_1/SpringWebHttpAttributesGetter.java b/instrumentation/spring/spring-web/spring-web-3.1/library/src/main/java/io/opentelemetry/instrumentation/spring/web/v3_1/SpringWebHttpAttributesGetter.java index ed4624479085..b61433ddaa44 100644 --- a/instrumentation/spring/spring-web/spring-web-3.1/library/src/main/java/io/opentelemetry/instrumentation/spring/web/v3_1/SpringWebHttpAttributesGetter.java +++ b/instrumentation/spring/spring-web/spring-web-3.1/library/src/main/java/io/opentelemetry/instrumentation/spring/web/v3_1/SpringWebHttpAttributesGetter.java @@ -7,7 +7,7 @@ import static java.util.Collections.emptyList; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpClientAttributesGetter; +import io.opentelemetry.instrumentation.api.semconv.http.HttpClientAttributesGetter; import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodType; @@ -96,4 +96,15 @@ public List getHttpResponseHeader( HttpRequest httpRequest, ClientHttpResponse clientHttpResponse, String name) { return clientHttpResponse.getHeaders().getOrDefault(name, emptyList()); } + + @Override + @Nullable + public String getServerAddress(HttpRequest httpRequest) { + return httpRequest.getURI().getHost(); + } + + @Override + public Integer getServerPort(HttpRequest httpRequest) { + return httpRequest.getURI().getPort(); + } } diff --git a/instrumentation/spring/spring-web/spring-web-3.1/library/src/main/java/io/opentelemetry/instrumentation/spring/web/v3_1/SpringWebNetAttributesGetter.java b/instrumentation/spring/spring-web/spring-web-3.1/library/src/main/java/io/opentelemetry/instrumentation/spring/web/v3_1/SpringWebNetAttributesGetter.java deleted file mode 100644 index a28d73a63c21..000000000000 --- a/instrumentation/spring/spring-web/spring-web-3.1/library/src/main/java/io/opentelemetry/instrumentation/spring/web/v3_1/SpringWebNetAttributesGetter.java +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.spring.web.v3_1; - -import io.opentelemetry.instrumentation.api.instrumenter.net.NetClientAttributesGetter; -import javax.annotation.Nullable; -import org.springframework.http.HttpRequest; -import org.springframework.http.client.ClientHttpResponse; - -final class SpringWebNetAttributesGetter - implements NetClientAttributesGetter { - - @Override - @Nullable - public String getServerAddress(HttpRequest httpRequest) { - return httpRequest.getURI().getHost(); - } - - @Override - public Integer getServerPort(HttpRequest httpRequest) { - return httpRequest.getURI().getPort(); - } -} diff --git a/instrumentation/spring/spring-web/spring-web-3.1/library/src/main/java/io/opentelemetry/instrumentation/spring/web/v3_1/SpringWebTelemetry.java b/instrumentation/spring/spring-web/spring-web-3.1/library/src/main/java/io/opentelemetry/instrumentation/spring/web/v3_1/SpringWebTelemetry.java index 36209d7736cc..70cfc713df84 100644 --- a/instrumentation/spring/spring-web/spring-web-3.1/library/src/main/java/io/opentelemetry/instrumentation/spring/web/v3_1/SpringWebTelemetry.java +++ b/instrumentation/spring/spring-web/spring-web-3.1/library/src/main/java/io/opentelemetry/instrumentation/spring/web/v3_1/SpringWebTelemetry.java @@ -39,7 +39,7 @@ public static SpringWebTelemetryBuilder builder(OpenTelemetry openTelemetry) { * RestTemplate#getInterceptors()}. For example: * *

    {@code
    -   * restTemplate.getInterceptors().add(SpringWebTracing.create(openTelemetry).newInterceptor());
    +   * restTemplate.getInterceptors().add(SpringWebTelemetry.create(openTelemetry).newInterceptor());
        * }
    */ public ClientHttpRequestInterceptor newInterceptor() { diff --git a/instrumentation/spring/spring-web/spring-web-3.1/library/src/main/java/io/opentelemetry/instrumentation/spring/web/v3_1/SpringWebTelemetryBuilder.java b/instrumentation/spring/spring-web/spring-web-3.1/library/src/main/java/io/opentelemetry/instrumentation/spring/web/v3_1/SpringWebTelemetryBuilder.java index a4f3de8e1b05..36fde223c3ec 100644 --- a/instrumentation/spring/spring-web/spring-web-3.1/library/src/main/java/io/opentelemetry/instrumentation/spring/web/v3_1/SpringWebTelemetryBuilder.java +++ b/instrumentation/spring/spring-web/spring-web-3.1/library/src/main/java/io/opentelemetry/instrumentation/spring/web/v3_1/SpringWebTelemetryBuilder.java @@ -7,32 +7,35 @@ import com.google.errorprone.annotations.CanIgnoreReturnValue; import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.instrumentation.api.incubator.builder.internal.DefaultHttpClientInstrumenterBuilder; import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpClientAttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpClientAttributesExtractorBuilder; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpClientMetrics; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpSpanNameExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpSpanStatusExtractor; -import java.util.ArrayList; +import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor; +import io.opentelemetry.instrumentation.api.semconv.http.HttpClientAttributesExtractorBuilder; +import io.opentelemetry.instrumentation.spring.web.v3_1.internal.WebTelemetryUtil; import java.util.List; +import java.util.Set; +import java.util.function.Function; import org.springframework.http.HttpRequest; import org.springframework.http.client.ClientHttpResponse; /** A builder of {@link SpringWebTelemetry}. */ public final class SpringWebTelemetryBuilder { private static final String INSTRUMENTATION_NAME = "io.opentelemetry.spring-web-3.1"; + private final DefaultHttpClientInstrumenterBuilder builder; - private final OpenTelemetry openTelemetry; - private final List> additionalExtractors = - new ArrayList<>(); - private final HttpClientAttributesExtractorBuilder - httpAttributesExtractorBuilder = - HttpClientAttributesExtractor.builder( - SpringWebHttpAttributesGetter.INSTANCE, new SpringWebNetAttributesGetter()); + static { + WebTelemetryUtil.setBuilderExtractor(SpringWebTelemetryBuilder::getBuilder); + } SpringWebTelemetryBuilder(OpenTelemetry openTelemetry) { - this.openTelemetry = openTelemetry; + builder = + new DefaultHttpClientInstrumenterBuilder<>( + INSTRUMENTATION_NAME, openTelemetry, SpringWebHttpAttributesGetter.INSTANCE) + .setHeaderSetter(HttpRequestSetter.INSTANCE); + } + + private DefaultHttpClientInstrumenterBuilder getBuilder() { + return builder; } /** @@ -40,9 +43,9 @@ public final class SpringWebTelemetryBuilder { * items. */ @CanIgnoreReturnValue - public SpringWebTelemetryBuilder addAttributesExtractor( - AttributesExtractor attributesExtractor) { - additionalExtractors.add(attributesExtractor); + public SpringWebTelemetryBuilder addAttributeExtractor( + AttributesExtractor attributesExtractor) { + builder.addAttributeExtractor(attributesExtractor); return this; } @@ -53,7 +56,7 @@ public SpringWebTelemetryBuilder addAttributesExtractor( */ @CanIgnoreReturnValue public SpringWebTelemetryBuilder setCapturedRequestHeaders(List requestHeaders) { - httpAttributesExtractorBuilder.setCapturedRequestHeaders(requestHeaders); + builder.setCapturedRequestHeaders(requestHeaders); return this; } @@ -64,7 +67,50 @@ public SpringWebTelemetryBuilder setCapturedRequestHeaders(List requestH */ @CanIgnoreReturnValue public SpringWebTelemetryBuilder setCapturedResponseHeaders(List responseHeaders) { - httpAttributesExtractorBuilder.setCapturedResponseHeaders(responseHeaders); + builder.setCapturedResponseHeaders(responseHeaders); + return this; + } + + /** Sets custom {@link SpanNameExtractor} via transform function. */ + @CanIgnoreReturnValue + public SpringWebTelemetryBuilder setSpanNameExtractor( + Function< + SpanNameExtractor, + ? extends SpanNameExtractor> + spanNameExtractorTransformer) { + builder.setSpanNameExtractor(spanNameExtractorTransformer); + return this; + } + + /** + * Configures the instrumentation to recognize an alternative set of HTTP request methods. + * + *

    By default, this instrumentation defines "known" methods as the ones listed in RFC9110 and the PATCH + * method defined in RFC5789. + * + *

    Note: calling this method overrides the default known method sets completely; it does + * not supplement it. + * + * @param knownMethods A set of recognized HTTP request methods. + * @see HttpClientAttributesExtractorBuilder#setKnownMethods(Set) + */ + @CanIgnoreReturnValue + public SpringWebTelemetryBuilder setKnownMethods(Set knownMethods) { + builder.setKnownMethods(knownMethods); + return this; + } + + /** + * Configures the instrumentation to emit experimental HTTP client metrics. + * + * @param emitExperimentalHttpClientMetrics {@code true} if the experimental HTTP client metrics + * are to be emitted. + */ + @CanIgnoreReturnValue + public SpringWebTelemetryBuilder setEmitExperimentalHttpClientMetrics( + boolean emitExperimentalHttpClientMetrics) { + builder.setEmitExperimentalHttpClientMetrics(emitExperimentalHttpClientMetrics); return this; } @@ -73,19 +119,6 @@ public SpringWebTelemetryBuilder setCapturedResponseHeaders(List respons * SpringWebTelemetryBuilder}. */ public SpringWebTelemetry build() { - SpringWebHttpAttributesGetter httpAttributeGetter = SpringWebHttpAttributesGetter.INSTANCE; - - Instrumenter instrumenter = - Instrumenter.builder( - openTelemetry, - INSTRUMENTATION_NAME, - HttpSpanNameExtractor.create(httpAttributeGetter)) - .setSpanStatusExtractor(HttpSpanStatusExtractor.create(httpAttributeGetter)) - .addAttributesExtractor(httpAttributesExtractorBuilder.build()) - .addAttributesExtractors(additionalExtractors) - .addOperationMetrics(HttpClientMetrics.get()) - .buildClientInstrumenter(HttpRequestSetter.INSTANCE); - - return new SpringWebTelemetry(instrumenter); + return new SpringWebTelemetry(builder.build()); } } diff --git a/instrumentation/spring/spring-web/spring-web-3.1/library/src/main/java/io/opentelemetry/instrumentation/spring/web/v3_1/internal/WebTelemetryUtil.java b/instrumentation/spring/spring-web/spring-web-3.1/library/src/main/java/io/opentelemetry/instrumentation/spring/web/v3_1/internal/WebTelemetryUtil.java new file mode 100644 index 000000000000..443fdc86585e --- /dev/null +++ b/instrumentation/spring/spring-web/spring-web-3.1/library/src/main/java/io/opentelemetry/instrumentation/spring/web/v3_1/internal/WebTelemetryUtil.java @@ -0,0 +1,41 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.web.v3_1.internal; + +import io.opentelemetry.instrumentation.api.incubator.builder.internal.DefaultHttpClientInstrumenterBuilder; +import io.opentelemetry.instrumentation.spring.web.v3_1.SpringWebTelemetryBuilder; +import java.util.function.Function; +import org.springframework.http.HttpRequest; +import org.springframework.http.client.ClientHttpResponse; + +/** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time. + */ +public class WebTelemetryUtil { + private WebTelemetryUtil() {} + + // allows access to the private field for the spring starter + private static Function< + SpringWebTelemetryBuilder, + DefaultHttpClientInstrumenterBuilder> + builderExtractor; + + public static Function< + SpringWebTelemetryBuilder, + DefaultHttpClientInstrumenterBuilder> + getBuilderExtractor() { + return builderExtractor; + } + + public static void setBuilderExtractor( + Function< + SpringWebTelemetryBuilder, + DefaultHttpClientInstrumenterBuilder> + builderExtractor) { + WebTelemetryUtil.builderExtractor = builderExtractor; + } +} diff --git a/instrumentation/spring/spring-web/spring-web-3.1/library/src/test/java/io/opentelemetry/instrumentation/spring/web/v3_1/SpringWebInstrumentationTest.java b/instrumentation/spring/spring-web/spring-web-3.1/library/src/test/java/io/opentelemetry/instrumentation/spring/web/v3_1/SpringWebInstrumentationTest.java index cac9ee16af6f..6b40a8745475 100644 --- a/instrumentation/spring/spring-web/spring-web-3.1/library/src/test/java/io/opentelemetry/instrumentation/spring/web/v3_1/SpringWebInstrumentationTest.java +++ b/instrumentation/spring/spring-web/spring-web-3.1/library/src/test/java/io/opentelemetry/instrumentation/spring/web/v3_1/SpringWebInstrumentationTest.java @@ -5,7 +5,6 @@ package io.opentelemetry.instrumentation.spring.web.v3_1; -import static io.opentelemetry.api.common.AttributeKey.stringKey; import static java.util.Collections.singletonList; import io.opentelemetry.api.common.AttributeKey; @@ -14,6 +13,7 @@ import io.opentelemetry.instrumentation.testing.junit.http.HttpClientInstrumentationExtension; import io.opentelemetry.instrumentation.testing.junit.http.HttpClientResult; import io.opentelemetry.instrumentation.testing.junit.http.HttpClientTestOptions; +import io.opentelemetry.semconv.NetworkAttributes; import java.net.URI; import java.util.Collections; import java.util.HashSet; @@ -36,6 +36,7 @@ public class SpringWebInstrumentationTest extends AbstractHttpClientTest { Set> attributes = new HashSet<>(HttpClientTestOptions.DEFAULT_HTTP_ATTRIBUTES); - attributes.remove(stringKey("net.protocol.name")); - attributes.remove(stringKey("net.protocol.version")); + attributes.remove(NetworkAttributes.NETWORK_PROTOCOL_VERSION); return attributes; }); } diff --git a/instrumentation/spring/spring-web/spring-web-3.1/testing/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/web/SpringRestTemplateTest.java b/instrumentation/spring/spring-web/spring-web-3.1/testing/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/web/SpringRestTemplateTest.java index 9ba557466c1f..384f04d641e8 100644 --- a/instrumentation/spring/spring-web/spring-web-3.1/testing/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/web/SpringRestTemplateTest.java +++ b/instrumentation/spring/spring-web/spring-web-3.1/testing/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/web/SpringRestTemplateTest.java @@ -15,7 +15,6 @@ import java.io.InputStream; import java.net.URI; import java.util.Map; -import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.extension.RegisterExtension; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; @@ -29,14 +28,23 @@ public class SpringRestTemplateTest extends AbstractHttpClientTest request, String method, URI uri, Map headers) throws Exception { try { - return restTemplate + return getClient(uri) .exchange(uri, HttpMethod.valueOf(method), request, String.class) .getStatusCode() .value(); @@ -88,5 +96,8 @@ public void sendRequestWithCallback( @Override protected void configure(HttpClientTestOptions.Builder optionsBuilder) { optionsBuilder.setMaxRedirects(20); + + // no enum value for TEST + optionsBuilder.disableTestNonStandardHttpMethod(); } } diff --git a/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/build.gradle.kts b/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/build.gradle.kts index 32af02a255d8..ec57e007fc4e 100644 --- a/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/build.gradle.kts +++ b/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/build.gradle.kts @@ -52,6 +52,8 @@ dependencies { testInstrumentation(project(":instrumentation:reactor:reactor-3.1:javaagent")) testInstrumentation(project(":instrumentation:reactor:reactor-netty:reactor-netty-1.0:javaagent")) + testImplementation(project(":instrumentation:spring:spring-webflux:spring-webflux-5.0:testing")) + testImplementation(project(":instrumentation:spring:spring-webflux:spring-webflux-5.3:testing")) testLibrary("org.springframework.boot:spring-boot-starter-webflux:2.0.0.RELEASE") @@ -66,6 +68,7 @@ tasks.withType().configureEach { // required on jdk17 jvmArgs("--add-opens=java.base/java.lang=ALL-UNNAMED") jvmArgs("-XX:+IgnoreUnrecognizedVMOptions") + jvmArgs("-Dotel.instrumentation.common.experimental.controller-telemetry.enabled=true") systemProperty("testLatestDeps", findProperty("testLatestDeps") as Boolean) } diff --git a/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v5_0/SpringWebfluxConfig.java b/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v5_0/SpringWebfluxConfig.java index 51035617c316..02c0cff4588f 100644 --- a/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v5_0/SpringWebfluxConfig.java +++ b/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v5_0/SpringWebfluxConfig.java @@ -5,12 +5,12 @@ package io.opentelemetry.javaagent.instrumentation.spring.webflux.v5_0; -import io.opentelemetry.javaagent.bootstrap.internal.InstrumentationConfig; +import io.opentelemetry.javaagent.bootstrap.internal.AgentInstrumentationConfig; public final class SpringWebfluxConfig { private static final boolean CAPTURE_EXPERIMENTAL_SPAN_ATTRIBUTES = - InstrumentationConfig.get() + AgentInstrumentationConfig.get() .getBoolean("otel.instrumentation.spring-webflux.experimental-span-attributes", false); public static boolean captureExperimentalSpanAttributes() { diff --git a/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v5_0/client/WebClientHelper.java b/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v5_0/client/WebClientHelper.java index 0932ef16153e..99ee1a3f9273 100644 --- a/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v5_0/client/WebClientHelper.java +++ b/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v5_0/client/WebClientHelper.java @@ -7,12 +7,9 @@ import io.opentelemetry.api.GlobalOpenTelemetry; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; -import io.opentelemetry.instrumentation.api.instrumenter.net.PeerServiceAttributesExtractor; -import io.opentelemetry.instrumentation.spring.webflux.v5_3.internal.SpringWebfluxTelemetryClientBuilder; -import io.opentelemetry.instrumentation.spring.webflux.v5_3.internal.WebClientNetAttributesGetter; +import io.opentelemetry.instrumentation.spring.webflux.v5_3.internal.WebClientHttpAttributesGetter; import io.opentelemetry.instrumentation.spring.webflux.v5_3.internal.WebClientTracingFilter; -import io.opentelemetry.javaagent.bootstrap.internal.CommonConfig; -import io.opentelemetry.javaagent.bootstrap.internal.InstrumentationConfig; +import io.opentelemetry.javaagent.bootstrap.internal.JavaagentHttpClientInstrumenters; import java.util.List; import org.springframework.web.reactive.function.client.ClientRequest; import org.springframework.web.reactive.function.client.ClientResponse; @@ -21,17 +18,8 @@ public final class WebClientHelper { private static final Instrumenter instrumenter = - new SpringWebfluxTelemetryClientBuilder(GlobalOpenTelemetry.get()) - .setCapturedClientRequestHeaders(CommonConfig.get().getClientRequestHeaders()) - .setCapturedClientResponseHeaders(CommonConfig.get().getClientResponseHeaders()) - .addClientAttributesExtractor( - PeerServiceAttributesExtractor.create( - new WebClientNetAttributesGetter(), CommonConfig.get().getPeerServiceMapping())) - .setCaptureExperimentalSpanAttributes( - InstrumentationConfig.get() - .getBoolean( - "otel.instrumentation.spring-webflux.experimental-span-attributes", false)) - .build(); + JavaagentHttpClientInstrumenters.create( + "io.opentelemetry.spring-webflux-5.0", WebClientHttpAttributesGetter.INSTANCE); public static void addFilter(List exchangeFilterFunctions) { for (ExchangeFilterFunction filterFunction : exchangeFilterFunctions) { diff --git a/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v5_0/server/AdviceUtils.java b/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v5_0/server/AdviceUtils.java index bcc31c9856f3..a7af1a299f34 100644 --- a/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v5_0/server/AdviceUtils.java +++ b/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v5_0/server/AdviceUtils.java @@ -8,13 +8,16 @@ import static io.opentelemetry.javaagent.instrumentation.spring.webflux.v5_0.server.WebfluxSingletons.instrumenter; import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; import javax.annotation.Nullable; import org.springframework.web.server.ServerWebExchange; +import reactor.core.CoreSubscriber; import reactor.core.publisher.Mono; public final class AdviceUtils { - public static final String ON_SPAN_END = AdviceUtils.class.getName() + ".Context"; + public static final String ON_SPAN_END = AdviceUtils.class.getName() + ".OnSpanEnd"; + public static final String CONTEXT = AdviceUtils.class.getName() + ".Context"; public static void registerOnSpanEnd( ServerWebExchange exchange, Context context, Object handler) { @@ -38,10 +41,35 @@ private static void end(ServerWebExchange exchange, @Nullable Throwable throwabl } } + public static Mono wrapMono(Mono mono, Context context) { + if (context == null) { + return mono; + } + return new ContextMono<>(mono, context); + } + @FunctionalInterface interface OnSpanEnd { void end(Throwable throwable); } + private static class ContextMono extends Mono { + + private final Mono delegate; + private final Context parentContext; + + ContextMono(Mono delegate, Context parentContext) { + this.delegate = delegate; + this.parentContext = parentContext; + } + + @Override + public void subscribe(CoreSubscriber actual) { + try (Scope ignored = parentContext.makeCurrent()) { + delegate.subscribe(actual); + } + } + } + private AdviceUtils() {} } diff --git a/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v5_0/server/DispatcherHandlerInstrumentation.java b/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v5_0/server/DispatcherHandlerInstrumentation.java index 03de0b20d452..118b0312e77e 100644 --- a/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v5_0/server/DispatcherHandlerInstrumentation.java +++ b/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v5_0/server/DispatcherHandlerInstrumentation.java @@ -35,6 +35,11 @@ public void transform(TypeTransformer transformer) { .and(takesArgument(0, named("org.springframework.web.server.ServerWebExchange"))) .and(takesArguments(1)), this.getClass().getName() + "$HandleAdvice"); + transformer.applyAdviceToMethod( + isMethod() + .and(named("handleResult")) + .and(takesArgument(0, named("org.springframework.web.server.ServerWebExchange"))), + this.getClass().getName() + "$HandleResultAdvice"); } @SuppressWarnings("unused") @@ -53,4 +58,15 @@ public static void methodExit( } } } + + @SuppressWarnings("unused") + public static class HandleResultAdvice { + + @Advice.OnMethodExit(suppress = Throwable.class) + public static void methodExit( + @Advice.Argument(0) ServerWebExchange exchange, + @Advice.Return(readOnly = false) Mono mono) { + mono = AdviceUtils.wrapMono(mono, exchange.getAttribute(AdviceUtils.CONTEXT)); + } + } } diff --git a/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v5_0/server/HandlerAdapterInstrumentation.java b/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v5_0/server/HandlerAdapterInstrumentation.java index 9660a1c3981f..89967acc239d 100644 --- a/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v5_0/server/HandlerAdapterInstrumentation.java +++ b/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v5_0/server/HandlerAdapterInstrumentation.java @@ -19,14 +19,16 @@ import io.opentelemetry.context.Context; import io.opentelemetry.context.Scope; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteHolder; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteSource; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRoute; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRouteSource; 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.HandlerResult; import org.springframework.web.server.ServerWebExchange; +import reactor.core.publisher.Mono; public class HandlerAdapterInstrumentation implements TypeInstrumentation { @@ -69,8 +71,8 @@ public static void methodEnter( // using the last portion of the nested path. // HttpRouteSource.NESTED_CONTROLLER has useFirst false, and it will make http.route updated // twice: 1st using the last portion of the nested path, 2nd time using the full nested path. - HttpRouteHolder.updateHttpRoute( - parentContext, HttpRouteSource.NESTED_CONTROLLER, httpRouteGetter(), exchange); + HttpServerRoute.update( + parentContext, HttpServerRouteSource.NESTED_CONTROLLER, httpRouteGetter(), exchange); if (handler == null) { return; @@ -86,6 +88,7 @@ public static void methodEnter( @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) public static void methodExit( + @Advice.Return(readOnly = false) Mono mono, @Advice.Argument(0) ServerWebExchange exchange, @Advice.Argument(1) Object handler, @Advice.Thrown Throwable throwable, @@ -99,6 +102,8 @@ public static void methodExit( if (throwable != null) { instrumenter().end(context, handler, null, throwable); } else { + mono = AdviceUtils.wrapMono(mono, context); + exchange.getAttributes().put(AdviceUtils.CONTEXT, context); AdviceUtils.registerOnSpanEnd(exchange, context, handler); // span finished by wrapped Mono in DispatcherHandlerInstrumentation // the Mono is already wrapped at this point, but doesn't read the ON_SPAN_END until diff --git a/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v5_0/server/HandlerCodeAttributesGetter.java b/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v5_0/server/HandlerCodeAttributesGetter.java index 498a0cf682da..6218a761a896 100644 --- a/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v5_0/server/HandlerCodeAttributesGetter.java +++ b/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v5_0/server/HandlerCodeAttributesGetter.java @@ -5,7 +5,7 @@ package io.opentelemetry.javaagent.instrumentation.spring.webflux.v5_0.server; -import io.opentelemetry.instrumentation.api.instrumenter.code.CodeAttributesGetter; +import io.opentelemetry.instrumentation.api.incubator.semconv.code.CodeAttributesGetter; import javax.annotation.Nullable; public class HandlerCodeAttributesGetter implements CodeAttributesGetter { diff --git a/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v5_0/server/RouteOnSuccess.java b/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v5_0/server/RouteOnSuccess.java index 01bed3c5cde8..aab075ec214f 100644 --- a/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v5_0/server/RouteOnSuccess.java +++ b/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v5_0/server/RouteOnSuccess.java @@ -6,8 +6,8 @@ package io.opentelemetry.javaagent.instrumentation.spring.webflux.v5_0.server; import io.opentelemetry.context.Context; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteHolder; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteSource; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRoute; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRouteSource; import java.util.function.Consumer; import java.util.regex.Pattern; import javax.annotation.Nullable; @@ -29,7 +29,7 @@ public RouteOnSuccess(RouterFunction routerFunction) { @Override public void accept(HandlerFunction handler) { - HttpRouteHolder.updateHttpRoute(Context.current(), HttpRouteSource.CONTROLLER, route); + HttpServerRoute.update(Context.current(), HttpServerRouteSource.CONTROLLER, route); } private static String parsePredicateString(RouterFunction routerFunction) { diff --git a/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v5_0/server/WebfluxSingletons.java b/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v5_0/server/WebfluxSingletons.java index 81a9c9925d52..5114c530c3a9 100644 --- a/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v5_0/server/WebfluxSingletons.java +++ b/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v5_0/server/WebfluxSingletons.java @@ -8,7 +8,7 @@ import io.opentelemetry.api.GlobalOpenTelemetry; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; import io.opentelemetry.instrumentation.api.instrumenter.InstrumenterBuilder; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteGetter; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRouteGetter; import io.opentelemetry.javaagent.bootstrap.internal.ExperimentalConfig; import io.opentelemetry.javaagent.instrumentation.spring.webflux.v5_0.SpringWebfluxConfig; import org.springframework.web.reactive.HandlerMapping; @@ -39,7 +39,7 @@ public static Instrumenter instrumenter() { return INSTRUMENTER; } - public static HttpRouteGetter httpRouteGetter() { + public static HttpServerRouteGetter httpRouteGetter() { return (context, exchange) -> { Object bestPatternObj = exchange.getAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE); if (bestPatternObj == null) { diff --git a/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v5_0/server/WebfluxSpanNameExtractor.java b/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v5_0/server/WebfluxSpanNameExtractor.java index 99074e5d7d4f..9ab4ae97d3f5 100644 --- a/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v5_0/server/WebfluxSpanNameExtractor.java +++ b/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v5_0/server/WebfluxSpanNameExtractor.java @@ -5,9 +5,9 @@ package io.opentelemetry.javaagent.instrumentation.spring.webflux.v5_0.server; +import io.opentelemetry.instrumentation.api.incubator.semconv.code.CodeSpanNameExtractor; +import io.opentelemetry.instrumentation.api.incubator.semconv.util.SpanNames; import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.code.CodeSpanNameExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.util.SpanNames; import org.springframework.web.method.HandlerMethod; public class WebfluxSpanNameExtractor implements SpanNameExtractor { diff --git a/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v5_0/server/reactornetty/ContextHandlerInstrumentation.java b/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v5_0/server/reactornetty/ContextHandlerInstrumentation.java index 26844726a9a1..f1d6af9e73d6 100644 --- a/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v5_0/server/reactornetty/ContextHandlerInstrumentation.java +++ b/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v5_0/server/reactornetty/ContextHandlerInstrumentation.java @@ -9,12 +9,11 @@ import static net.bytebuddy.matcher.ElementMatchers.takesArgument; import io.netty.channel.Channel; -import io.opentelemetry.context.Context; import io.opentelemetry.context.Scope; -import io.opentelemetry.instrumentation.netty.v4_1.internal.AttributeKeys; +import io.opentelemetry.instrumentation.netty.v4_1.internal.ServerContext; +import io.opentelemetry.instrumentation.netty.v4_1.internal.ServerContexts; import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; -import java.util.Deque; import net.bytebuddy.asm.Advice; import net.bytebuddy.description.type.TypeDescription; import net.bytebuddy.matcher.ElementMatcher; @@ -39,10 +38,9 @@ public static class CreateOperationsAdvice { @Advice.OnMethodEnter(suppress = Throwable.class) public static Scope onEnter(@Advice.Argument(0) Channel channel) { // set context to the first unprocessed request - Deque contexts = channel.attr(AttributeKeys.SERVER_CONTEXT).get(); - Context context = contexts != null ? contexts.peekFirst() : null; - if (context != null) { - return context.makeCurrent(); + ServerContext serverContext = ServerContexts.peekFirst(channel); + if (serverContext != null) { + return serverContext.context().makeCurrent(); } return null; } diff --git a/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v5_0/server/reactornetty/HttpTrafficHandlerInstrumentation.java b/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v5_0/server/reactornetty/HttpTrafficHandlerInstrumentation.java index 70f8b7537d8d..3567a8524644 100644 --- a/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v5_0/server/reactornetty/HttpTrafficHandlerInstrumentation.java +++ b/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v5_0/server/reactornetty/HttpTrafficHandlerInstrumentation.java @@ -9,12 +9,11 @@ import static net.bytebuddy.matcher.ElementMatchers.takesNoArguments; import io.netty.channel.ChannelHandlerContext; -import io.opentelemetry.context.Context; import io.opentelemetry.context.Scope; -import io.opentelemetry.instrumentation.netty.v4_1.internal.AttributeKeys; +import io.opentelemetry.instrumentation.netty.v4_1.internal.ServerContext; +import io.opentelemetry.instrumentation.netty.v4_1.internal.ServerContexts; import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; -import java.util.Deque; import net.bytebuddy.asm.Advice; import net.bytebuddy.description.type.TypeDescription; import net.bytebuddy.matcher.ElementMatcher; @@ -40,11 +39,9 @@ public static class RunAdvice { public static Scope onEnter( @Advice.FieldValue("ctx") ChannelHandlerContext channelHandlerContext) { // set context to the first unprocessed request - Deque contexts = - channelHandlerContext.channel().attr(AttributeKeys.SERVER_CONTEXT).get(); - Context context = contexts != null ? contexts.peekFirst() : null; - if (context != null) { - return context.makeCurrent(); + ServerContext serverContext = ServerContexts.peekFirst(channelHandlerContext.channel()); + if (serverContext != null) { + return serverContext.context().makeCurrent(); } return null; } diff --git a/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v5_0/server/reactornetty/ReactorNettyInstrumentationModule.java b/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v5_0/server/reactornetty/ReactorNettyInstrumentationModule.java index 962e94995c83..8abd3d0a954e 100644 --- a/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v5_0/server/reactornetty/ReactorNettyInstrumentationModule.java +++ b/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v5_0/server/reactornetty/ReactorNettyInstrumentationModule.java @@ -8,16 +8,24 @@ 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.internal.ExperimentalInstrumentationModule; import java.util.Arrays; import java.util.List; @AutoService(InstrumentationModule.class) -public class ReactorNettyInstrumentationModule extends InstrumentationModule { +public class ReactorNettyInstrumentationModule extends InstrumentationModule + implements ExperimentalInstrumentationModule { public ReactorNettyInstrumentationModule() { super("reactor-netty", "reactor-netty-server"); } + @Override + public String getModuleGroup() { + // relies on netty + return "netty"; + } + @Override public List typeInstrumentations() { return Arrays.asList( diff --git a/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/test/groovy/SingleThreadedSpringWebfluxTest.groovy b/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/test/groovy/SingleThreadedSpringWebfluxTest.groovy deleted file mode 100644 index 13a15cbab216..000000000000 --- a/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/test/groovy/SingleThreadedSpringWebfluxTest.groovy +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -import io.opentelemetry.javaagent.instrumentation.spring.webflux.v5_0.server.SpringWebfluxTest -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/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v5_0/server/SpringThreadedSpringWebfluxTest.java b/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v5_0/server/SpringThreadedSpringWebfluxTest.java new file mode 100644 index 000000000000..f31600f3bc2e --- /dev/null +++ b/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v5_0/server/SpringThreadedSpringWebfluxTest.java @@ -0,0 +1,45 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.webflux.v5_0.server; + +import io.opentelemetry.instrumentation.spring.webflux.IpcSingleThreadNettyCustomizer; +import io.opentelemetry.instrumentation.spring.webflux.server.SingleThreadNettyCustomizer; +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.class, + SpringThreadedSpringWebfluxTest.ForceSingleThreadedNettyAutoConfiguration.class + }) +class SpringThreadedSpringWebfluxTest extends SpringWebfluxTest { + + @TestConfiguration + static class ForceSingleThreadedNettyAutoConfiguration { + @Bean + NettyReactiveWebServerFactory nettyFactory() { + NettyReactiveWebServerFactory factory = new NettyReactiveWebServerFactory(); + factory.addServerCustomizers(customizer()); + return factory; + } + } + + static NettyServerCustomizer customizer() { + if (Boolean.getBoolean("testLatestDeps")) { + return new SingleThreadNettyCustomizer(); + } + return new IpcSingleThreadNettyCustomizer(); + } +} diff --git a/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v5_0/server/SpringWebfluxTest.java b/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v5_0/server/SpringWebfluxTest.java index 68a1799baba7..2748191b7e83 100644 --- a/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v5_0/server/SpringWebfluxTest.java +++ b/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v5_0/server/SpringWebfluxTest.java @@ -9,41 +9,40 @@ import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.satisfies; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.EXCEPTION_EVENT_NAME; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.EXCEPTION_MESSAGE; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.EXCEPTION_STACKTRACE; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.EXCEPTION_TYPE; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.HTTP_METHOD; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.HTTP_ROUTE; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.HTTP_SCHEME; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.HTTP_STATUS_CODE; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.HTTP_TARGET; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.NET_HOST_NAME; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.NET_HOST_PORT; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.NET_SOCK_HOST_ADDR; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.NET_SOCK_PEER_ADDR; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.NET_SOCK_PEER_PORT; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.NET_TRANSPORT; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.NetTransportValues.IP_TCP; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.USER_AGENT_ORIGINAL; +import static io.opentelemetry.semconv.ClientAttributes.CLIENT_ADDRESS; +import static io.opentelemetry.semconv.ErrorAttributes.ERROR_TYPE; +import static io.opentelemetry.semconv.ExceptionAttributes.EXCEPTION_MESSAGE; +import static io.opentelemetry.semconv.ExceptionAttributes.EXCEPTION_STACKTRACE; +import static io.opentelemetry.semconv.ExceptionAttributes.EXCEPTION_TYPE; +import static io.opentelemetry.semconv.HttpAttributes.HTTP_REQUEST_METHOD; +import static io.opentelemetry.semconv.HttpAttributes.HTTP_RESPONSE_STATUS_CODE; +import static io.opentelemetry.semconv.HttpAttributes.HTTP_ROUTE; +import static io.opentelemetry.semconv.NetworkAttributes.NETWORK_PROTOCOL_VERSION; +import static io.opentelemetry.semconv.ServerAttributes.SERVER_ADDRESS; +import static io.opentelemetry.semconv.ServerAttributes.SERVER_PORT; +import static io.opentelemetry.semconv.UserAgentAttributes.USER_AGENT_ORIGINAL; import static org.junit.jupiter.api.Named.named; import io.opentelemetry.api.trace.SpanKind; import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.sdk.testing.assertj.EventDataAssert; import io.opentelemetry.sdk.testing.assertj.TraceAssert; import io.opentelemetry.sdk.trace.data.StatusData; +import io.opentelemetry.semconv.NetworkAttributes; +import io.opentelemetry.semconv.UrlAttributes; import io.opentelemetry.testing.internal.armeria.client.WebClient; import io.opentelemetry.testing.internal.armeria.common.AggregatedHttpResponse; import io.opentelemetry.testing.internal.armeria.common.HttpStatus; +import java.time.Duration; import java.util.Collections; import java.util.List; +import java.util.concurrent.ExecutionException; import java.util.function.Consumer; import java.util.stream.Collectors; import java.util.stream.IntStream; import java.util.stream.Stream; +import org.junit.jupiter.api.Assumptions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -112,32 +111,20 @@ void basicGetTest(Parameter parameter) { .hasKind(SpanKind.SERVER) .hasNoParent() .hasAttributesSatisfyingExactly( - equalTo(NET_TRANSPORT, IP_TCP), - equalTo(stringKey("net.protocol.name"), "http"), - equalTo(stringKey("net.protocol.version"), "1.1"), - equalTo(NET_SOCK_PEER_ADDR, "127.0.0.1"), - satisfies(NET_SOCK_PEER_PORT, val -> val.isInstanceOf(Long.class)), - equalTo(NET_SOCK_HOST_ADDR, "127.0.0.1"), - equalTo(NET_HOST_NAME, "localhost"), - satisfies(NET_HOST_PORT, val -> val.isInstanceOf(Long.class)), - equalTo(HTTP_TARGET, parameter.urlPath), - equalTo(HTTP_METHOD, "GET"), - equalTo(HTTP_STATUS_CODE, 200), - equalTo(HTTP_SCHEME, "http"), - satisfies(USER_AGENT_ORIGINAL, val -> val.isInstanceOf(String.class)), - equalTo(HTTP_ROUTE, parameter.urlPathWithVariables), + equalTo(NETWORK_PROTOCOL_VERSION, "1.1"), + equalTo(NetworkAttributes.NETWORK_PEER_ADDRESS, "127.0.0.1"), satisfies( - HTTP_REQUEST_CONTENT_LENGTH, - val -> - val.satisfiesAnyOf( - v -> assertThat(v).isInstanceOf(Long.class), - v -> assertThat(v).isNull())), - satisfies( - HTTP_RESPONSE_CONTENT_LENGTH, - val -> - val.satisfiesAnyOf( - v -> assertThat(v).isInstanceOf(Long.class), - v -> assertThat(v).isNull()))), + NetworkAttributes.NETWORK_PEER_PORT, + val -> val.isInstanceOf(Long.class)), + equalTo(SERVER_ADDRESS, "localhost"), + satisfies(SERVER_PORT, val -> val.isInstanceOf(Long.class)), + equalTo(CLIENT_ADDRESS, "127.0.0.1"), + equalTo(UrlAttributes.URL_PATH, parameter.urlPath), + equalTo(HTTP_REQUEST_METHOD, "GET"), + equalTo(HTTP_RESPONSE_STATUS_CODE, 200), + equalTo(UrlAttributes.URL_SCHEME, "http"), + satisfies(USER_AGENT_ORIGINAL, val -> val.isInstanceOf(String.class)), + equalTo(HTTP_ROUTE, parameter.urlPathWithVariables)), span -> { if (parameter.annotatedMethod == null) { // Functional API @@ -219,7 +206,15 @@ private static Stream provideParameters() { "/foo-delayed", "/foo-delayed", "getFooDelayed", - new FooModel(3L, "delayed").toString())))); + new FooModel(3L, "delayed").toString()))), + Arguments.of( + named( + "annotation API without parameters no mono", + new Parameter( + "/foo-no-mono", + "/foo-no-mono", + "getFooModelNoMono", + new FooModel(0L, "DEFAULT").toString())))); } @ParameterizedTest(name = "{index}: {0}") @@ -237,32 +232,20 @@ void getAsyncResponseTest(Parameter parameter) { .hasKind(SpanKind.SERVER) .hasNoParent() .hasAttributesSatisfyingExactly( - equalTo(NET_TRANSPORT, IP_TCP), - equalTo(stringKey("net.protocol.name"), "http"), - equalTo(stringKey("net.protocol.version"), "1.1"), - equalTo(NET_SOCK_PEER_ADDR, "127.0.0.1"), - satisfies(NET_SOCK_PEER_PORT, val -> val.isInstanceOf(Long.class)), - equalTo(NET_SOCK_HOST_ADDR, "127.0.0.1"), - equalTo(NET_HOST_NAME, "localhost"), - satisfies(NET_HOST_PORT, val -> val.isInstanceOf(Long.class)), - equalTo(HTTP_TARGET, parameter.urlPath), - equalTo(HTTP_METHOD, "GET"), - equalTo(HTTP_STATUS_CODE, 200), - equalTo(HTTP_SCHEME, "http"), - satisfies(USER_AGENT_ORIGINAL, val -> val.isInstanceOf(String.class)), - equalTo(HTTP_ROUTE, parameter.urlPathWithVariables), + equalTo(NETWORK_PROTOCOL_VERSION, "1.1"), + equalTo(NetworkAttributes.NETWORK_PEER_ADDRESS, "127.0.0.1"), satisfies( - HTTP_REQUEST_CONTENT_LENGTH, - val -> - val.satisfiesAnyOf( - v -> assertThat(v).isInstanceOf(Long.class), - v -> assertThat(v).isNull())), - satisfies( - HTTP_RESPONSE_CONTENT_LENGTH, - val -> - val.satisfiesAnyOf( - v -> assertThat(v).isInstanceOf(Long.class), - v -> assertThat(v).isNull()))), + NetworkAttributes.NETWORK_PEER_PORT, + val -> val.isInstanceOf(Long.class)), + equalTo(SERVER_ADDRESS, "localhost"), + satisfies(SERVER_PORT, val -> val.isInstanceOf(Long.class)), + equalTo(CLIENT_ADDRESS, "127.0.0.1"), + equalTo(UrlAttributes.URL_PATH, parameter.urlPath), + equalTo(HTTP_REQUEST_METHOD, "GET"), + equalTo(HTTP_RESPONSE_STATUS_CODE, 200), + equalTo(UrlAttributes.URL_SCHEME, "http"), + satisfies(USER_AGENT_ORIGINAL, val -> val.isInstanceOf(String.class)), + equalTo(HTTP_ROUTE, parameter.urlPathWithVariables)), span -> { if (parameter.annotatedMethod == null) { // Functional API @@ -284,7 +267,7 @@ void getAsyncResponseTest(Parameter parameter) { }, span -> span.hasName("tracedMethod") - .hasParent(trace.getSpan(0)) + .hasParent(trace.getSpan(1)) .hasTotalAttributeCount(0))); } @@ -357,32 +340,20 @@ void createSpanDuringHandlerFunctionTest(Parameter parameter) { .hasKind(SpanKind.SERVER) .hasNoParent() .hasAttributesSatisfyingExactly( - equalTo(NET_TRANSPORT, IP_TCP), - equalTo(stringKey("net.protocol.name"), "http"), - equalTo(stringKey("net.protocol.version"), "1.1"), - equalTo(NET_SOCK_PEER_ADDR, "127.0.0.1"), - satisfies(NET_SOCK_PEER_PORT, val -> val.isInstanceOf(Long.class)), - equalTo(NET_SOCK_HOST_ADDR, "127.0.0.1"), - equalTo(NET_HOST_NAME, "localhost"), - satisfies(NET_HOST_PORT, val -> val.isInstanceOf(Long.class)), - equalTo(HTTP_TARGET, parameter.urlPath), - equalTo(HTTP_METHOD, "GET"), - equalTo(HTTP_STATUS_CODE, 200), - equalTo(HTTP_SCHEME, "http"), - satisfies(USER_AGENT_ORIGINAL, val -> val.isInstanceOf(String.class)), - equalTo(HTTP_ROUTE, parameter.urlPathWithVariables), - satisfies( - HTTP_REQUEST_CONTENT_LENGTH, - val -> - val.satisfiesAnyOf( - v -> assertThat(v).isInstanceOf(Long.class), - v -> assertThat(v).isNull())), + equalTo(NETWORK_PROTOCOL_VERSION, "1.1"), + equalTo(NetworkAttributes.NETWORK_PEER_ADDRESS, "127.0.0.1"), satisfies( - HTTP_RESPONSE_CONTENT_LENGTH, - val -> - val.satisfiesAnyOf( - v -> assertThat(v).isInstanceOf(Long.class), - v -> assertThat(v).isNull()))), + NetworkAttributes.NETWORK_PEER_PORT, + val -> val.isInstanceOf(Long.class)), + equalTo(SERVER_ADDRESS, "localhost"), + satisfies(SERVER_PORT, val -> val.isInstanceOf(Long.class)), + equalTo(CLIENT_ADDRESS, "127.0.0.1"), + equalTo(UrlAttributes.URL_PATH, parameter.urlPath), + equalTo(HTTP_REQUEST_METHOD, "GET"), + equalTo(HTTP_RESPONSE_STATUS_CODE, 200), + equalTo(UrlAttributes.URL_SCHEME, "http"), + satisfies(USER_AGENT_ORIGINAL, val -> val.isInstanceOf(String.class)), + equalTo(HTTP_ROUTE, parameter.urlPathWithVariables)), span -> { if (parameter.annotatedMethod == null) { // Functional API @@ -404,7 +375,7 @@ void createSpanDuringHandlerFunctionTest(Parameter parameter) { }, span -> span.hasName("tracedMethod") - .hasParent(trace.getSpan(parameter.annotatedMethod != null ? 0 : 1)) + .hasParent(trace.getSpan(1)) .hasTotalAttributeCount(0))); } @@ -442,59 +413,52 @@ void get404Test() { .hasNoParent() .hasStatus(StatusData.unset()) .hasAttributesSatisfyingExactly( - equalTo(NET_TRANSPORT, IP_TCP), - equalTo(stringKey("net.protocol.name"), "http"), - equalTo(stringKey("net.protocol.version"), "1.1"), - equalTo(NET_SOCK_PEER_ADDR, "127.0.0.1"), - satisfies(NET_SOCK_PEER_PORT, val -> val.isInstanceOf(Long.class)), - equalTo(NET_SOCK_HOST_ADDR, "127.0.0.1"), - equalTo(NET_HOST_NAME, "localhost"), - satisfies(NET_HOST_PORT, val -> val.isInstanceOf(Long.class)), - equalTo(HTTP_TARGET, "/notfoundgreet"), - equalTo(HTTP_METHOD, "GET"), - equalTo(HTTP_STATUS_CODE, 404), - equalTo(HTTP_SCHEME, "http"), - satisfies(USER_AGENT_ORIGINAL, val -> val.isInstanceOf(String.class)), - equalTo(HTTP_ROUTE, "/**"), + equalTo(NETWORK_PROTOCOL_VERSION, "1.1"), + equalTo(NetworkAttributes.NETWORK_PEER_ADDRESS, "127.0.0.1"), satisfies( - HTTP_REQUEST_CONTENT_LENGTH, - val -> - val.satisfiesAnyOf( - v -> assertThat(v).isInstanceOf(Long.class), - v -> assertThat(v).isNull())), - satisfies( - HTTP_RESPONSE_CONTENT_LENGTH, - val -> - val.satisfiesAnyOf( - v -> assertThat(v).isInstanceOf(Long.class), - v -> assertThat(v).isNull()))), + NetworkAttributes.NETWORK_PEER_PORT, + val -> val.isInstanceOf(Long.class)), + equalTo(SERVER_ADDRESS, "localhost"), + satisfies(SERVER_PORT, val -> val.isInstanceOf(Long.class)), + equalTo(CLIENT_ADDRESS, "127.0.0.1"), + equalTo(UrlAttributes.URL_PATH, "/notfoundgreet"), + equalTo(HTTP_REQUEST_METHOD, "GET"), + equalTo(HTTP_RESPONSE_STATUS_CODE, 404), + equalTo(UrlAttributes.URL_SCHEME, "http"), + satisfies(USER_AGENT_ORIGINAL, val -> val.isInstanceOf(String.class)), + equalTo(HTTP_ROUTE, "/**")), span -> span.hasName("ResourceWebHandler.handle") .hasKind(SpanKind.INTERNAL) .hasParent(trace.getSpan(0)) .hasStatus(StatusData.error()) - .hasEventsSatisfyingExactly( - event -> - event - .hasName(EXCEPTION_EVENT_NAME) - .hasAttributesSatisfyingExactly( - equalTo( - EXCEPTION_TYPE, - "org.springframework.web.server.ResponseStatusException"), - satisfies( - EXCEPTION_MESSAGE, - val -> - val.containsAnyOf( - "Response status 404", "404 NOT_FOUND")), - satisfies( - EXCEPTION_STACKTRACE, - val -> val.isInstanceOf(String.class)))) + .hasEventsSatisfyingExactly(SpringWebfluxTest::resource404Exception) .hasAttributesSatisfyingExactly( equalTo( stringKey("spring-webflux.handler.type"), "org.springframework.web.reactive.resource.ResourceWebHandler")))); } + private static void resource404Exception(EventDataAssert event) { + if (Boolean.getBoolean("testLatestDeps")) { + event + .hasName("exception") + .hasAttributesSatisfyingExactly( + equalTo( + EXCEPTION_TYPE, + "org.springframework.web.reactive.resource.NoResourceFoundException"), + satisfies(EXCEPTION_MESSAGE, val -> val.isInstanceOf(String.class)), + satisfies(EXCEPTION_STACKTRACE, val -> val.isInstanceOf(String.class))); + } else { + event + .hasName("exception") + .hasAttributesSatisfyingExactly( + equalTo(EXCEPTION_TYPE, "org.springframework.web.server.ResponseStatusException"), + equalTo(EXCEPTION_MESSAGE, "Response status 404"), + satisfies(EXCEPTION_STACKTRACE, val -> val.isInstanceOf(String.class))); + } + } + @Test void basicPostTest() { String echoString = "TEST"; @@ -510,32 +474,20 @@ void basicPostTest() { .hasKind(SpanKind.SERVER) .hasNoParent() .hasAttributesSatisfyingExactly( - equalTo(NET_TRANSPORT, IP_TCP), - equalTo(stringKey("net.protocol.name"), "http"), - equalTo(stringKey("net.protocol.version"), "1.1"), - equalTo(NET_SOCK_PEER_ADDR, "127.0.0.1"), - satisfies(NET_SOCK_PEER_PORT, val -> val.isInstanceOf(Long.class)), - equalTo(NET_SOCK_HOST_ADDR, "127.0.0.1"), - equalTo(NET_HOST_NAME, "localhost"), - satisfies(NET_HOST_PORT, val -> val.isInstanceOf(Long.class)), - equalTo(HTTP_TARGET, "/echo"), - equalTo(HTTP_METHOD, "POST"), - equalTo(HTTP_STATUS_CODE, 202), - equalTo(HTTP_SCHEME, "http"), - satisfies(USER_AGENT_ORIGINAL, val -> val.isInstanceOf(String.class)), - equalTo(HTTP_ROUTE, "/echo"), + equalTo(NETWORK_PROTOCOL_VERSION, "1.1"), + equalTo(NetworkAttributes.NETWORK_PEER_ADDRESS, "127.0.0.1"), satisfies( - HTTP_REQUEST_CONTENT_LENGTH, - val -> - val.satisfiesAnyOf( - v -> assertThat(v).isInstanceOf(Long.class), - v -> assertThat(v).isNull())), - satisfies( - HTTP_RESPONSE_CONTENT_LENGTH, - val -> - val.satisfiesAnyOf( - v -> assertThat(v).isInstanceOf(Long.class), - v -> assertThat(v).isNull()))), + NetworkAttributes.NETWORK_PEER_PORT, + val -> val.isInstanceOf(Long.class)), + equalTo(SERVER_ADDRESS, "localhost"), + satisfies(SERVER_PORT, val -> val.isInstanceOf(Long.class)), + equalTo(CLIENT_ADDRESS, "127.0.0.1"), + equalTo(UrlAttributes.URL_PATH, "/echo"), + equalTo(HTTP_REQUEST_METHOD, "POST"), + equalTo(HTTP_RESPONSE_STATUS_CODE, 202), + equalTo(UrlAttributes.URL_SCHEME, "http"), + satisfies(USER_AGENT_ORIGINAL, val -> val.isInstanceOf(String.class)), + equalTo(HTTP_ROUTE, "/echo")), span -> span.hasName(EchoHandlerFunction.class.getSimpleName() + ".handle") .hasKind(SpanKind.INTERNAL) @@ -563,32 +515,21 @@ void getToBadEndpointTest(Parameter parameter) { .hasNoParent() .hasStatus(StatusData.error()) .hasAttributesSatisfyingExactly( - equalTo(NET_TRANSPORT, IP_TCP), - equalTo(stringKey("net.protocol.name"), "http"), - equalTo(stringKey("net.protocol.version"), "1.1"), - equalTo(NET_SOCK_PEER_ADDR, "127.0.0.1"), - satisfies(NET_SOCK_PEER_PORT, val -> val.isInstanceOf(Long.class)), - equalTo(NET_SOCK_HOST_ADDR, "127.0.0.1"), - equalTo(NET_HOST_NAME, "localhost"), - satisfies(NET_HOST_PORT, val -> val.isInstanceOf(Long.class)), - equalTo(HTTP_TARGET, parameter.urlPath), - equalTo(HTTP_METHOD, "GET"), - equalTo(HTTP_STATUS_CODE, 500), - equalTo(HTTP_SCHEME, "http"), + equalTo(NETWORK_PROTOCOL_VERSION, "1.1"), + equalTo(NetworkAttributes.NETWORK_PEER_ADDRESS, "127.0.0.1"), + satisfies( + NetworkAttributes.NETWORK_PEER_PORT, + val -> val.isInstanceOf(Long.class)), + equalTo(SERVER_ADDRESS, "localhost"), + satisfies(SERVER_PORT, val -> val.isInstanceOf(Long.class)), + equalTo(CLIENT_ADDRESS, "127.0.0.1"), + equalTo(UrlAttributes.URL_PATH, parameter.urlPath), + equalTo(HTTP_REQUEST_METHOD, "GET"), + equalTo(HTTP_RESPONSE_STATUS_CODE, 500), + equalTo(UrlAttributes.URL_SCHEME, "http"), satisfies(USER_AGENT_ORIGINAL, val -> val.isInstanceOf(String.class)), equalTo(HTTP_ROUTE, parameter.urlPathWithVariables), - satisfies( - HTTP_REQUEST_CONTENT_LENGTH, - val -> - val.satisfiesAnyOf( - v -> assertThat(v).isInstanceOf(Long.class), - v -> assertThat(v).isNull())), - satisfies( - HTTP_RESPONSE_CONTENT_LENGTH, - val -> - val.satisfiesAnyOf( - v -> assertThat(v).isInstanceOf(Long.class), - v -> assertThat(v).isNull()))), + equalTo(ERROR_TYPE, "500")), span -> { if (parameter.annotatedMethod == null) { // Functional API @@ -605,7 +546,7 @@ void getToBadEndpointTest(Parameter parameter) { .hasEventsSatisfyingExactly( event -> event - .hasName(EXCEPTION_EVENT_NAME) + .hasName("exception") .hasAttributesSatisfyingExactly( equalTo(EXCEPTION_TYPE, "java.lang.IllegalStateException"), equalTo(EXCEPTION_MESSAGE, "bad things happen"), @@ -655,40 +596,28 @@ void redirectTest() { .hasKind(SpanKind.SERVER) .hasNoParent() .hasAttributesSatisfyingExactly( - equalTo(NET_TRANSPORT, IP_TCP), - equalTo(stringKey("net.protocol.name"), "http"), - equalTo(stringKey("net.protocol.version"), "1.1"), - equalTo(NET_SOCK_PEER_ADDR, "127.0.0.1"), - satisfies(NET_SOCK_PEER_PORT, val -> val.isInstanceOf(Long.class)), - equalTo(NET_SOCK_HOST_ADDR, "127.0.0.1"), - equalTo(NET_HOST_NAME, "localhost"), - satisfies(NET_HOST_PORT, val -> val.isInstanceOf(Long.class)), - equalTo(HTTP_TARGET, "/double-greet-redirect"), - equalTo(HTTP_METHOD, "GET"), - equalTo(HTTP_STATUS_CODE, 307), - equalTo(HTTP_SCHEME, "http"), - satisfies(USER_AGENT_ORIGINAL, val -> val.isInstanceOf(String.class)), - equalTo(HTTP_ROUTE, "/double-greet-redirect"), - satisfies( - HTTP_REQUEST_CONTENT_LENGTH, - val -> - val.satisfiesAnyOf( - v -> assertThat(v).isInstanceOf(Long.class), - v -> assertThat(v).isNull())), + equalTo(NETWORK_PROTOCOL_VERSION, "1.1"), + equalTo(NetworkAttributes.NETWORK_PEER_ADDRESS, "127.0.0.1"), satisfies( - HTTP_RESPONSE_CONTENT_LENGTH, - val -> - val.satisfiesAnyOf( - v -> assertThat(v).isInstanceOf(Long.class), - v -> assertThat(v).isNull()))), + NetworkAttributes.NETWORK_PEER_PORT, + val -> val.isInstanceOf(Long.class)), + equalTo(SERVER_ADDRESS, "localhost"), + satisfies(SERVER_PORT, val -> val.isInstanceOf(Long.class)), + equalTo(CLIENT_ADDRESS, "127.0.0.1"), + equalTo(UrlAttributes.URL_PATH, "/double-greet-redirect"), + equalTo(HTTP_REQUEST_METHOD, "GET"), + equalTo(HTTP_RESPONSE_STATUS_CODE, 307), + equalTo(UrlAttributes.URL_SCHEME, "http"), + satisfies(USER_AGENT_ORIGINAL, val -> val.isInstanceOf(String.class)), + equalTo(HTTP_ROUTE, "/double-greet-redirect")), span -> - span.hasName("RedirectComponent$$Lambda$.handle") + span.hasName("RedirectComponent$$Lambda.handle") .hasKind(SpanKind.INTERNAL) .hasParent(trace.getSpan(0)) .hasAttributesSatisfyingExactly( satisfies( stringKey("spring-webflux.handler.type"), - val -> val.startsWith("server.RedirectComponent$$Lambda$")))), + val -> val.startsWith("server.RedirectComponent$$Lambda")))), trace -> trace.hasSpansSatisfyingExactly( span -> @@ -696,32 +625,20 @@ void redirectTest() { .hasKind(SpanKind.SERVER) .hasNoParent() .hasAttributesSatisfyingExactly( - equalTo(NET_TRANSPORT, IP_TCP), - equalTo(stringKey("net.protocol.name"), "http"), - equalTo(stringKey("net.protocol.version"), "1.1"), - equalTo(NET_SOCK_PEER_ADDR, "127.0.0.1"), - satisfies(NET_SOCK_PEER_PORT, val -> val.isInstanceOf(Long.class)), - equalTo(NET_SOCK_HOST_ADDR, "127.0.0.1"), - equalTo(NET_HOST_NAME, "localhost"), - satisfies(NET_HOST_PORT, val -> val.isInstanceOf(Long.class)), - equalTo(HTTP_TARGET, "/double-greet"), - equalTo(HTTP_METHOD, "GET"), - equalTo(HTTP_STATUS_CODE, 200), - equalTo(HTTP_SCHEME, "http"), - satisfies(USER_AGENT_ORIGINAL, val -> val.isInstanceOf(String.class)), - equalTo(HTTP_ROUTE, "/double-greet"), + equalTo(NETWORK_PROTOCOL_VERSION, "1.1"), + equalTo(NetworkAttributes.NETWORK_PEER_ADDRESS, "127.0.0.1"), satisfies( - HTTP_REQUEST_CONTENT_LENGTH, - val -> - val.satisfiesAnyOf( - v -> assertThat(v).isInstanceOf(Long.class), - v -> assertThat(v).isNull())), - satisfies( - HTTP_RESPONSE_CONTENT_LENGTH, - val -> - val.satisfiesAnyOf( - v -> assertThat(v).isInstanceOf(Long.class), - v -> assertThat(v).isNull()))), + NetworkAttributes.NETWORK_PEER_PORT, + val -> val.isInstanceOf(Long.class)), + equalTo(SERVER_ADDRESS, "localhost"), + satisfies(SERVER_PORT, val -> val.isInstanceOf(Long.class)), + equalTo(CLIENT_ADDRESS, "127.0.0.1"), + equalTo(UrlAttributes.URL_PATH, "/double-greet"), + equalTo(HTTP_REQUEST_METHOD, "GET"), + equalTo(HTTP_RESPONSE_STATUS_CODE, 200), + equalTo(UrlAttributes.URL_SCHEME, "http"), + satisfies(USER_AGENT_ORIGINAL, val -> val.isInstanceOf(String.class)), + equalTo(HTTP_ROUTE, "/double-greet")), span -> { assertThat(trace.getSpan(1).getName()) .contains(SPRING_APP_CLASS_ANON_NESTED_CLASS_PREFIX, ".handle"); @@ -760,32 +677,20 @@ void multipleGetsToDelayingRoute(Parameter parameter) { .hasKind(SpanKind.SERVER) .hasNoParent() .hasAttributesSatisfyingExactly( - equalTo(NET_TRANSPORT, IP_TCP), - equalTo(stringKey("net.protocol.name"), "http"), - equalTo(stringKey("net.protocol.version"), "1.1"), - equalTo(NET_SOCK_PEER_ADDR, "127.0.0.1"), - satisfies(NET_SOCK_PEER_PORT, val -> val.isInstanceOf(Long.class)), - equalTo(NET_SOCK_HOST_ADDR, "127.0.0.1"), - equalTo(NET_HOST_NAME, "localhost"), - satisfies(NET_HOST_PORT, val -> val.isInstanceOf(Long.class)), - equalTo(HTTP_TARGET, parameter.urlPath), - equalTo(HTTP_METHOD, "GET"), - equalTo(HTTP_STATUS_CODE, 200), - equalTo(HTTP_SCHEME, "http"), - satisfies(USER_AGENT_ORIGINAL, val -> val.isInstanceOf(String.class)), - equalTo(HTTP_ROUTE, parameter.urlPathWithVariables), + equalTo(NETWORK_PROTOCOL_VERSION, "1.1"), + equalTo(NetworkAttributes.NETWORK_PEER_ADDRESS, "127.0.0.1"), satisfies( - HTTP_REQUEST_CONTENT_LENGTH, - val -> - val.satisfiesAnyOf( - v -> assertThat(v).isInstanceOf(Long.class), - v -> assertThat(v).isNull())), - satisfies( - HTTP_RESPONSE_CONTENT_LENGTH, - val -> - val.satisfiesAnyOf( - v -> assertThat(v).isInstanceOf(Long.class), - v -> assertThat(v).isNull()))), + NetworkAttributes.NETWORK_PEER_PORT, + val -> val.isInstanceOf(Long.class)), + equalTo(SERVER_ADDRESS, "localhost"), + satisfies(SERVER_PORT, val -> val.isInstanceOf(Long.class)), + equalTo(CLIENT_ADDRESS, "127.0.0.1"), + equalTo(UrlAttributes.URL_PATH, parameter.urlPath), + equalTo(HTTP_REQUEST_METHOD, "GET"), + equalTo(HTTP_RESPONSE_STATUS_CODE, 200), + equalTo(UrlAttributes.URL_SCHEME, "http"), + satisfies(USER_AGENT_ORIGINAL, val -> val.isInstanceOf(String.class)), + equalTo(HTTP_ROUTE, parameter.urlPathWithVariables)), span -> { if (parameter.annotatedMethod == null) { // Functional API @@ -829,6 +734,59 @@ private static Stream provideMultipleDelayingRouteParameters() { new FooModel(3L, "delayed").toString())))); } + @Test + void cancelRequestTest() throws Exception { + // fails with SingleThreadedSpringWebfluxTest + Assumptions.assumeTrue(this.getClass() == SpringWebfluxTest.class); + + WebClient client = + WebClient.builder("h1c://localhost:" + port) + .responseTimeout(Duration.ofSeconds(1)) + .followRedirects() + .build(); + try { + client.get("/slow").aggregate().get(); + } catch (ExecutionException ignore) { + // ignore + } + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("GET /slow") + .hasKind(SpanKind.SERVER) + .hasNoParent() + .hasStatus(StatusData.unset()) + .hasAttributesSatisfyingExactly( + equalTo(NETWORK_PROTOCOL_VERSION, "1.1"), + equalTo(NetworkAttributes.NETWORK_PEER_ADDRESS, "127.0.0.1"), + satisfies( + NetworkAttributes.NETWORK_PEER_PORT, + val -> val.isInstanceOf(Long.class)), + equalTo(SERVER_ADDRESS, "localhost"), + satisfies(SERVER_PORT, val -> val.isInstanceOf(Long.class)), + equalTo(CLIENT_ADDRESS, "127.0.0.1"), + equalTo(UrlAttributes.URL_PATH, "/slow"), + equalTo(HTTP_REQUEST_METHOD, "GET"), + equalTo(UrlAttributes.URL_SCHEME, "http"), + satisfies(USER_AGENT_ORIGINAL, val -> val.isInstanceOf(String.class)), + equalTo(HTTP_ROUTE, "/slow"), + equalTo(ERROR_TYPE, "_OTHER")), + span -> + span.hasName("SpringWebFluxTestApplication$$Lambda.handle") + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + satisfies( + stringKey("spring-webflux.handler.type"), + value -> + value.startsWith( + "server.SpringWebFluxTestApplication$$Lambda"))))); + + SpringWebFluxTestApplication.resumeSlowRequest(); + } + private static class Parameter { Parameter( String urlPath, diff --git a/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/test/java/server/base/ControllerSpringWebFluxServerTest.java b/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v5_0/server/base/ControllerSpringWebFluxServerTest.java similarity index 76% rename from instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/test/java/server/base/ControllerSpringWebFluxServerTest.java rename to instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v5_0/server/base/ControllerSpringWebFluxServerTest.java index da9c2f9e2b1b..ea5c7be2bd1a 100644 --- a/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/test/java/server/base/ControllerSpringWebFluxServerTest.java +++ b/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v5_0/server/base/ControllerSpringWebFluxServerTest.java @@ -3,16 +3,15 @@ * SPDX-License-Identifier: Apache-2.0 */ -package server.base; +package io.opentelemetry.javaagent.instrumentation.spring.webflux.v5_0.server.base; import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.EXCEPTION; import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.NOT_FOUND; import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.satisfies; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.EXCEPTION_EVENT_NAME; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.EXCEPTION_MESSAGE; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.EXCEPTION_STACKTRACE; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.EXCEPTION_TYPE; +import static io.opentelemetry.semconv.ExceptionAttributes.EXCEPTION_MESSAGE; +import static io.opentelemetry.semconv.ExceptionAttributes.EXCEPTION_STACKTRACE; +import static io.opentelemetry.semconv.ExceptionAttributes.EXCEPTION_TYPE; import io.opentelemetry.api.trace.SpanKind; import io.opentelemetry.instrumentation.testing.junit.http.HttpServerTestOptions; @@ -37,7 +36,7 @@ protected SpanDataAssert assertHandlerSpan( span.hasEventsSatisfyingExactly( event -> event - .hasName(EXCEPTION_EVENT_NAME) + .hasName("exception") .hasAttributesSatisfyingExactly( equalTo(EXCEPTION_TYPE, "java.lang.IllegalStateException"), equalTo(EXCEPTION_MESSAGE, EXCEPTION.getBody()), @@ -48,18 +47,19 @@ protected SpanDataAssert assertHandlerSpan( span.hasEventsSatisfyingExactly( event -> event - .hasName(EXCEPTION_EVENT_NAME) + .hasName("exception") .hasAttributesSatisfyingExactly( equalTo( EXCEPTION_TYPE, - "org.springframework.web.server.ResponseStatusException"), - equalTo(EXCEPTION_MESSAGE, "404 NOT_FOUND"), + "org.springframework.web.reactive.resource.NoResourceFoundException"), + equalTo( + EXCEPTION_MESSAGE, "404 NOT_FOUND \"No static resource notFound.\""), satisfies(EXCEPTION_STACKTRACE, val -> val.isInstanceOf(String.class)))); } else { span.hasEventsSatisfyingExactly( event -> event - .hasName(EXCEPTION_EVENT_NAME) + .hasName("exception") .hasAttributesSatisfyingExactly( equalTo( EXCEPTION_TYPE, @@ -74,10 +74,16 @@ protected SpanDataAssert assertHandlerSpan( @Override protected void configure(HttpServerTestOptions options) { super.configure(options); - options.setHasHandlerAsControllerParentSpan(unused -> false); // TODO (trask) it seems like in this case ideally the controller span (which ends when the // Mono that the controller returns completes) should end before the server span (which needs // the result of the Mono) options.setVerifyServerSpanEndTime(false); + + options.setResponseCodeOnNonStandardHttpMethod(405); + + // TODO fails on java 21 + // span name set to "HTTP + // org.springframework.web.reactive.function.server.RequestPredicates$$Lambda/0x00007fa574969238@4aaf6fa2" + options.disableTestNonStandardHttpMethod(); } } diff --git a/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/test/java/server/base/DelayedControllerSpringWebFluxServerTest.java b/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v5_0/server/base/DelayedControllerSpringWebFluxServerTest.java similarity index 95% rename from instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/test/java/server/base/DelayedControllerSpringWebFluxServerTest.java rename to instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v5_0/server/base/DelayedControllerSpringWebFluxServerTest.java index 4237b4455fb5..1ef1302dca70 100644 --- a/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/test/java/server/base/DelayedControllerSpringWebFluxServerTest.java +++ b/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v5_0/server/base/DelayedControllerSpringWebFluxServerTest.java @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -package server.base; +package io.opentelemetry.javaagent.instrumentation.spring.webflux.v5_0.server.base; import io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint; import java.time.Duration; diff --git a/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/test/java/server/base/DelayedHandlerSpringWebFluxServerTest.java b/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v5_0/server/base/DelayedHandlerSpringWebFluxServerTest.java similarity index 87% rename from instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/test/java/server/base/DelayedHandlerSpringWebFluxServerTest.java rename to instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v5_0/server/base/DelayedHandlerSpringWebFluxServerTest.java index 0fdd674797cc..d513e13d18a0 100644 --- a/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/test/java/server/base/DelayedHandlerSpringWebFluxServerTest.java +++ b/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v5_0/server/base/DelayedHandlerSpringWebFluxServerTest.java @@ -3,9 +3,8 @@ * SPDX-License-Identifier: Apache-2.0 */ -package server.base; +package io.opentelemetry.javaagent.instrumentation.spring.webflux.v5_0.server.base; -import io.opentelemetry.instrumentation.testing.junit.http.HttpServerTestOptions; import io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint; import java.time.Duration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; @@ -58,10 +57,4 @@ protected Mono wrapResponse( })); } } - - @Override - protected void configure(HttpServerTestOptions options) { - super.configure(options); - options.setHasHandlerAsControllerParentSpan(unused -> false); - } } diff --git a/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/test/java/server/base/HandlerSpringWebFluxServerTest.java b/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v5_0/server/base/HandlerSpringWebFluxServerTest.java similarity index 73% rename from instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/test/java/server/base/HandlerSpringWebFluxServerTest.java rename to instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v5_0/server/base/HandlerSpringWebFluxServerTest.java index 4ad6fcff270d..637c5b8124ca 100644 --- a/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/test/java/server/base/HandlerSpringWebFluxServerTest.java +++ b/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v5_0/server/base/HandlerSpringWebFluxServerTest.java @@ -3,18 +3,18 @@ * SPDX-License-Identifier: Apache-2.0 */ -package server.base; +package io.opentelemetry.javaagent.instrumentation.spring.webflux.v5_0.server.base; import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.EXCEPTION; import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.NOT_FOUND; import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.satisfies; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.EXCEPTION_EVENT_NAME; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.EXCEPTION_MESSAGE; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.EXCEPTION_STACKTRACE; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.EXCEPTION_TYPE; +import static io.opentelemetry.semconv.ExceptionAttributes.EXCEPTION_MESSAGE; +import static io.opentelemetry.semconv.ExceptionAttributes.EXCEPTION_STACKTRACE; +import static io.opentelemetry.semconv.ExceptionAttributes.EXCEPTION_TYPE; import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.api.internal.HttpConstants; import io.opentelemetry.instrumentation.testing.junit.http.HttpServerTestOptions; import io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint; import io.opentelemetry.sdk.testing.assertj.SpanDataAssert; @@ -25,7 +25,7 @@ public abstract class HandlerSpringWebFluxServerTest extends SpringWebFluxServer @Override protected SpanDataAssert assertHandlerSpan( SpanDataAssert span, String method, ServerEndpoint endpoint) { - String handlerSpanName = ServerTestRouteFactory.class.getSimpleName() + "$$Lambda$.handle"; + String handlerSpanName = ServerTestRouteFactory.class.getSimpleName() + "$$Lambda.handle"; if (endpoint == NOT_FOUND) { handlerSpanName = "ResourceWebHandler.handle"; } @@ -35,7 +35,7 @@ protected SpanDataAssert assertHandlerSpan( span.hasEventsSatisfyingExactly( event -> event - .hasName(EXCEPTION_EVENT_NAME) + .hasName("exception") .hasAttributesSatisfyingExactly( equalTo(EXCEPTION_TYPE, "java.lang.IllegalStateException"), equalTo(EXCEPTION_MESSAGE, EXCEPTION.getBody()), @@ -46,18 +46,19 @@ protected SpanDataAssert assertHandlerSpan( span.hasEventsSatisfyingExactly( event -> event - .hasName(EXCEPTION_EVENT_NAME) + .hasName("exception") .hasAttributesSatisfyingExactly( equalTo( EXCEPTION_TYPE, - "org.springframework.web.server.ResponseStatusException"), - equalTo(EXCEPTION_MESSAGE, "404 NOT_FOUND"), + "org.springframework.web.reactive.resource.NoResourceFoundException"), + equalTo( + EXCEPTION_MESSAGE, "404 NOT_FOUND \"No static resource notFound.\""), satisfies(EXCEPTION_STACKTRACE, val -> val.isInstanceOf(String.class)))); } else { span.hasEventsSatisfyingExactly( event -> event - .hasName(EXCEPTION_EVENT_NAME) + .hasName("exception") .hasAttributesSatisfyingExactly( equalTo( EXCEPTION_TYPE, @@ -76,5 +77,15 @@ protected void configure(HttpServerTestOptions options) { // Mono that the controller returns completes) should end before the server span (which needs // the result of the Mono) options.setVerifyServerSpanEndTime(false); + + options.setResponseCodeOnNonStandardHttpMethod(404); + } + + @Override + public String expectedHttpRoute(ServerEndpoint endpoint, String method) { + if (HttpConstants._OTHER.equals(method)) { + return getContextPath() + "/**"; + } + return super.expectedHttpRoute(endpoint, method); } } diff --git a/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/test/java/server/base/ImmediateControllerSpringWebFluxServerTest.java b/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v5_0/server/base/ImmediateControllerSpringWebFluxServerTest.java similarity index 95% rename from instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/test/java/server/base/ImmediateControllerSpringWebFluxServerTest.java rename to instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v5_0/server/base/ImmediateControllerSpringWebFluxServerTest.java index 8327904044a2..ccdecadb5478 100644 --- a/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/test/java/server/base/ImmediateControllerSpringWebFluxServerTest.java +++ b/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v5_0/server/base/ImmediateControllerSpringWebFluxServerTest.java @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -package server.base; +package io.opentelemetry.javaagent.instrumentation.spring.webflux.v5_0.server.base; import io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint; import java.util.function.Supplier; diff --git a/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/test/java/server/base/ImmediateHandlerSpringWebFluxServerTest.java b/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v5_0/server/base/ImmediateHandlerSpringWebFluxServerTest.java similarity index 94% rename from instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/test/java/server/base/ImmediateHandlerSpringWebFluxServerTest.java rename to instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v5_0/server/base/ImmediateHandlerSpringWebFluxServerTest.java index 6a0e7483748a..6e0dbc0e95fe 100644 --- a/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/test/java/server/base/ImmediateHandlerSpringWebFluxServerTest.java +++ b/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v5_0/server/base/ImmediateHandlerSpringWebFluxServerTest.java @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -package server.base; +package io.opentelemetry.javaagent.instrumentation.spring.webflux.v5_0.server.base; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assumptions.assumeTrue; @@ -72,6 +72,6 @@ void nestedPath() { assertThat(response.contentUtf8()).isEqualTo(NESTED_PATH.getBody()); assertResponseHasCustomizedHeaders(response, NESTED_PATH, null); - assertTheTraces(1, null, null, null, method, NESTED_PATH, response); + assertTheTraces(1, null, null, null, method, NESTED_PATH); } } diff --git a/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/test/java/server/base/ServerTestController.java b/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v5_0/server/base/ServerTestController.java similarity index 95% rename from instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/test/java/server/base/ServerTestController.java rename to instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v5_0/server/base/ServerTestController.java index d8ab437ae9d5..812c976457c1 100644 --- a/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/test/java/server/base/ServerTestController.java +++ b/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v5_0/server/base/ServerTestController.java @@ -3,9 +3,9 @@ * SPDX-License-Identifier: Apache-2.0 */ -package server.base; +package io.opentelemetry.javaagent.instrumentation.spring.webflux.v5_0.server.base; -import static server.base.SpringWebFluxServerTest.NESTED_PATH; +import static io.opentelemetry.javaagent.instrumentation.spring.webflux.v5_0.server.base.SpringWebFluxServerTest.NESTED_PATH; import io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint; import java.net.URI; diff --git a/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/test/java/server/base/ServerTestRouteFactory.java b/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v5_0/server/base/ServerTestRouteFactory.java similarity index 95% rename from instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/test/java/server/base/ServerTestRouteFactory.java rename to instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v5_0/server/base/ServerTestRouteFactory.java index b1742c4cfff4..7d91a7aa48b4 100644 --- a/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/test/java/server/base/ServerTestRouteFactory.java +++ b/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v5_0/server/base/ServerTestRouteFactory.java @@ -3,13 +3,13 @@ * SPDX-License-Identifier: Apache-2.0 */ -package server.base; +package io.opentelemetry.javaagent.instrumentation.spring.webflux.v5_0.server.base; +import static io.opentelemetry.javaagent.instrumentation.spring.webflux.v5_0.server.base.SpringWebFluxServerTest.NESTED_PATH; import static org.springframework.web.reactive.function.server.RequestPredicates.GET; import static org.springframework.web.reactive.function.server.RequestPredicates.path; import static org.springframework.web.reactive.function.server.RouterFunctions.nest; import static org.springframework.web.reactive.function.server.RouterFunctions.route; -import static server.base.SpringWebFluxServerTest.NESTED_PATH; import io.opentelemetry.api.trace.Span; import io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint; diff --git a/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/test/java/server/base/SpringWebFluxServerTest.java b/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v5_0/server/base/SpringWebFluxServerTest.java similarity index 92% rename from instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/test/java/server/base/SpringWebFluxServerTest.java rename to instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v5_0/server/base/SpringWebFluxServerTest.java index 343be82d9a18..c1403355d593 100644 --- a/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/test/java/server/base/SpringWebFluxServerTest.java +++ b/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v5_0/server/base/SpringWebFluxServerTest.java @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -package server.base; +package io.opentelemetry.javaagent.instrumentation.spring.webflux.v5_0.server.base; import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.EXCEPTION; import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.NOT_FOUND; @@ -49,7 +49,7 @@ public void stopServer(ConfigurableApplicationContext configurableApplicationCon } @Override - public String expectedHttpRoute(ServerEndpoint endpoint) { + public String expectedHttpRoute(ServerEndpoint endpoint, String method) { if (endpoint.equals(PATH_PARAM)) { return getContextPath() + "/path/{id}/param"; } else if (endpoint.equals(NOT_FOUND)) { @@ -57,7 +57,7 @@ public String expectedHttpRoute(ServerEndpoint endpoint) { } else if (endpoint.equals(NESTED_PATH)) { return "/nestedPath/hello/world"; } - return super.expectedHttpRoute(endpoint); + return super.expectedHttpRoute(endpoint, method); } @Override diff --git a/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/test/java/server/base/package-info.java b/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v5_0/server/base/package-info.java similarity index 62% rename from instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/test/java/server/base/package-info.java rename to instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v5_0/server/base/package-info.java index 679f71c0a4af..f915cf01d493 100644 --- a/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/test/java/server/base/package-info.java +++ b/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webflux/v5_0/server/base/package-info.java @@ -2,4 +2,4 @@ * The classes in this package are specific to tests that extend {@link * io.opentelemetry.instrumentation.test.base.HttpServerTest}. */ -package server.base; +package io.opentelemetry.javaagent.instrumentation.spring.webflux.v5_0.server.base; diff --git a/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/test/java/server/SpringWebFluxTestApplication.java b/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/test/java/server/SpringWebFluxTestApplication.java index 251bce200015..0151501226ed 100644 --- a/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/test/java/server/SpringWebFluxTestApplication.java +++ b/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/test/java/server/SpringWebFluxTestApplication.java @@ -12,10 +12,11 @@ import io.opentelemetry.api.GlobalOpenTelemetry; import io.opentelemetry.api.trace.Tracer; import java.time.Duration; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; 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; @@ -25,12 +26,11 @@ import reactor.core.publisher.Mono; @SpringBootApplication -@ComponentScan( - basePackages = {"server"}, - excludeFilters = @ComponentScan.Filter(type = FilterType.REGEX, pattern = "server.base.*")) +@ComponentScan(basePackages = {"server"}) public class SpringWebFluxTestApplication { private static final Tracer tracer = GlobalOpenTelemetry.getTracer("test"); + private static final CountDownLatch slowRequestLatch = new CountDownLatch(1); @Bean RouterFunction echoRouterFunction(EchoHandler echoHandler) { @@ -71,7 +71,22 @@ RouterFunction greetRouterFunction(GreetingHandler greetingHandl greetingHandler.intResponse( Mono.just(Integer.parseInt(request.pathVariable("id"))) .delayElement(Duration.ofMillis(100)) - .map(SpringWebFluxTestApplication::tracedMethod))); + .map(SpringWebFluxTestApplication::tracedMethod))) + .andRoute( + GET("/slow"), + request -> { + try { + slowRequestLatch.await(10, TimeUnit.SECONDS); + } catch (InterruptedException e) { + throw new IllegalStateException(e); + } + return Mono.delay(Duration.ofMillis(100)) + .then(ServerResponse.ok().body(BodyInserters.fromObject("ok"))); + }); + } + + public static void resumeSlowRequest() { + slowRequestLatch.countDown(); } @Component diff --git a/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/test/java/server/TestController.java b/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/test/java/server/TestController.java index 54cef12f3821..50476def96f4 100644 --- a/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/test/java/server/TestController.java +++ b/instrumentation/spring/spring-webflux/spring-webflux-5.0/javaagent/src/test/java/server/TestController.java @@ -63,6 +63,11 @@ Mono getFooDelayedMono(@PathVariable("id") long id) { return Mono.just(id).delayElement(Duration.ofMillis(100)).map(TestController::tracedMethod); } + @GetMapping("/foo-no-mono") + FooModel getFooModelNoMono() { + return new FooModel(0L, "DEFAULT"); + } + private static FooModel tracedMethod(long id) { tracer.spanBuilder("tracedMethod").startSpan().end(); return new FooModel(id, "tracedMethod"); diff --git a/instrumentation/spring/spring-webflux/spring-webflux-5.0/testing/build.gradle.kts b/instrumentation/spring/spring-webflux/spring-webflux-5.0/testing/build.gradle.kts new file mode 100644 index 000000000000..42b60973aa11 --- /dev/null +++ b/instrumentation/spring/spring-webflux/spring-webflux-5.0/testing/build.gradle.kts @@ -0,0 +1,11 @@ +plugins { + id("otel.java-conventions") +} + +dependencies { + implementation(project(":testing-common")) + + compileOnly("org.springframework:spring-webflux:5.0.0.RELEASE") + compileOnly("org.springframework.boot:spring-boot-starter-reactor-netty:2.0.0.RELEASE") + compileOnly("org.springframework.boot:spring-boot:2.0.0.RELEASE") +} diff --git a/instrumentation/spring/spring-webflux/spring-webflux-5.0/testing/src/main/java/io/opentelemetry/instrumentation/spring/webflux/IpcSingleThreadNettyCustomizer.java b/instrumentation/spring/spring-webflux/spring-webflux-5.0/testing/src/main/java/io/opentelemetry/instrumentation/spring/webflux/IpcSingleThreadNettyCustomizer.java new file mode 100644 index 000000000000..9426fde4a4ca --- /dev/null +++ b/instrumentation/spring/spring-webflux/spring-webflux-5.0/testing/src/main/java/io/opentelemetry/instrumentation/spring/webflux/IpcSingleThreadNettyCustomizer.java @@ -0,0 +1,17 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.webflux; + +import org.springframework.boot.web.embedded.netty.NettyServerCustomizer; +import reactor.ipc.netty.http.server.HttpServerOptions; + +public class IpcSingleThreadNettyCustomizer implements NettyServerCustomizer { + + @Override + public void customize(HttpServerOptions.Builder builder) { + builder.loopResources(reactor.ipc.netty.resources.LoopResources.create("my-http", 1, true)); + } +} diff --git a/instrumentation/spring/spring-webflux/spring-webflux-5.3/library/README.md b/instrumentation/spring/spring-webflux/spring-webflux-5.3/library/README.md index 0b177cdcc6cf..a3c7f07aff7c 100644 --- a/instrumentation/spring/spring-webflux/spring-webflux-5.3/library/README.md +++ b/instrumentation/spring/spring-webflux/spring-webflux-5.3/library/README.md @@ -45,6 +45,11 @@ interface. a `WebFilter` and using the OpenTelemetry Reactor instrumentation to ensure context is passed around correctly. +### Web client instrumentation + +The `WebClient` instrumentation will emit the `error.type` attribute with value `cancelled` whenever +an outgoing HTTP request is cancelled. + ### Setup Here is how to set up client and server instrumentation respectively: diff --git a/instrumentation/spring/spring-webflux/spring-webflux-5.3/library/build.gradle.kts b/instrumentation/spring/spring-webflux/spring-webflux-5.3/library/build.gradle.kts index 1df4b62e1934..1526873f7866 100644 --- a/instrumentation/spring/spring-webflux/spring-webflux-5.3/library/build.gradle.kts +++ b/instrumentation/spring/spring-webflux/spring-webflux-5.3/library/build.gradle.kts @@ -14,6 +14,8 @@ dependencies { testLibrary("org.springframework.boot:spring-boot-starter-webflux:2.4.0") testLibrary("org.springframework.boot:spring-boot-starter-test:2.4.0") testLibrary("org.springframework.boot:spring-boot-starter-reactor-netty:2.4.0") + // can be remove after starter is update to depend on spring 6.1 + latestDepTestLibrary("org.springframework:spring-context:+") } val latestDepTest = findProperty("testLatestDeps") as Boolean diff --git a/instrumentation/spring/spring-webflux/spring-webflux-5.3/library/src/main/java/io/opentelemetry/instrumentation/spring/webflux/v5_3/SpringWebfluxTelemetryBuilder.java b/instrumentation/spring/spring-webflux/spring-webflux-5.3/library/src/main/java/io/opentelemetry/instrumentation/spring/webflux/v5_3/SpringWebfluxTelemetryBuilder.java index 4116392870ff..e004ee7e9a70 100644 --- a/instrumentation/spring/spring-webflux/spring-webflux-5.3/library/src/main/java/io/opentelemetry/instrumentation/spring/webflux/v5_3/SpringWebfluxTelemetryBuilder.java +++ b/instrumentation/spring/spring-webflux/spring-webflux-5.3/library/src/main/java/io/opentelemetry/instrumentation/spring/webflux/v5_3/SpringWebfluxTelemetryBuilder.java @@ -7,18 +7,17 @@ import com.google.errorprone.annotations.CanIgnoreReturnValue; import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.instrumentation.api.incubator.builder.internal.DefaultHttpClientInstrumenterBuilder; +import io.opentelemetry.instrumentation.api.incubator.builder.internal.DefaultHttpServerInstrumenterBuilder; import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteHolder; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpServerAttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpServerAttributesExtractorBuilder; -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 io.opentelemetry.instrumentation.spring.webflux.v5_3.internal.SpringWebfluxTelemetryClientBuilder; -import java.util.ArrayList; +import io.opentelemetry.instrumentation.api.semconv.http.HttpClientAttributesExtractorBuilder; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerAttributesExtractorBuilder; +import io.opentelemetry.instrumentation.spring.webflux.v5_3.internal.SpringWebfluxBuilderUtil; +import io.opentelemetry.instrumentation.spring.webflux.v5_3.internal.WebClientHttpAttributesGetter; import java.util.List; +import java.util.Set; +import java.util.function.Function; import org.springframework.web.reactive.function.client.ClientRequest; import org.springframework.web.reactive.function.client.ClientResponse; import org.springframework.web.server.ServerWebExchange; @@ -27,20 +26,25 @@ public final class SpringWebfluxTelemetryBuilder { private static final String INSTRUMENTATION_NAME = "io.opentelemetry.spring-webflux-5.3"; - private final OpenTelemetry openTelemetry; + private final DefaultHttpClientInstrumenterBuilder clientBuilder; + private final DefaultHttpServerInstrumenterBuilder + serverBuilder; - private final SpringWebfluxTelemetryClientBuilder clientBuilder; - - private final List> - serverAdditionalExtractors = new ArrayList<>(); - private final HttpServerAttributesExtractorBuilder - httpServerAttributesExtractorBuilder = - HttpServerAttributesExtractor.builder( - WebfluxServerHttpAttributesGetter.INSTANCE, new WebfluxServerNetAttributesGetter()); + static { + SpringWebfluxBuilderUtil.setClientBuilderExtractor( + SpringWebfluxTelemetryBuilder::getClientBuilder); + SpringWebfluxBuilderUtil.setServerBuilderExtractor( + SpringWebfluxTelemetryBuilder::getServerBuilder); + } SpringWebfluxTelemetryBuilder(OpenTelemetry openTelemetry) { - this.openTelemetry = openTelemetry; - clientBuilder = new SpringWebfluxTelemetryClientBuilder(openTelemetry); + clientBuilder = + new DefaultHttpClientInstrumenterBuilder<>( + INSTRUMENTATION_NAME, openTelemetry, WebClientHttpAttributesGetter.INSTANCE); + serverBuilder = + new DefaultHttpServerInstrumenterBuilder<>( + INSTRUMENTATION_NAME, openTelemetry, WebfluxServerHttpAttributesGetter.INSTANCE) + .setHeaderGetter(WebfluxTextMapGetter.INSTANCE); } /** @@ -50,7 +54,7 @@ public final class SpringWebfluxTelemetryBuilder { @CanIgnoreReturnValue public SpringWebfluxTelemetryBuilder addClientAttributesExtractor( AttributesExtractor attributesExtractor) { - clientBuilder.addClientAttributesExtractor(attributesExtractor); + clientBuilder.addAttributeExtractor(attributesExtractor); return this; } @@ -62,7 +66,7 @@ public SpringWebfluxTelemetryBuilder addClientAttributesExtractor( @CanIgnoreReturnValue public SpringWebfluxTelemetryBuilder setCapturedClientRequestHeaders( List requestHeaders) { - clientBuilder.setCapturedClientRequestHeaders(requestHeaders); + clientBuilder.setCapturedRequestHeaders(requestHeaders); return this; } @@ -74,19 +78,7 @@ public SpringWebfluxTelemetryBuilder setCapturedClientRequestHeaders( @CanIgnoreReturnValue public SpringWebfluxTelemetryBuilder setCapturedClientResponseHeaders( List responseHeaders) { - clientBuilder.setCapturedClientResponseHeaders(responseHeaders); - 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. - */ - @CanIgnoreReturnValue - public SpringWebfluxTelemetryBuilder setCaptureExperimentalSpanAttributes( - boolean captureExperimentalSpanAttributes) { - clientBuilder.setCaptureExperimentalSpanAttributes(captureExperimentalSpanAttributes); + clientBuilder.setCapturedResponseHeaders(responseHeaders); return this; } @@ -97,7 +89,7 @@ public SpringWebfluxTelemetryBuilder setCaptureExperimentalSpanAttributes( @CanIgnoreReturnValue public SpringWebfluxTelemetryBuilder addServerAttributesExtractor( AttributesExtractor attributesExtractor) { - serverAdditionalExtractors.add(attributesExtractor); + serverBuilder.addAttributesExtractor(attributesExtractor); return this; } @@ -110,7 +102,7 @@ public SpringWebfluxTelemetryBuilder addServerAttributesExtractor( @CanIgnoreReturnValue public SpringWebfluxTelemetryBuilder setCapturedServerRequestHeaders( List requestHeaders) { - httpServerAttributesExtractorBuilder.setCapturedRequestHeaders(requestHeaders); + serverBuilder.setCapturedRequestHeaders(requestHeaders); return this; } @@ -123,34 +115,96 @@ public SpringWebfluxTelemetryBuilder setCapturedServerRequestHeaders( @CanIgnoreReturnValue public SpringWebfluxTelemetryBuilder setCapturedServerResponseHeaders( List responseHeaders) { - httpServerAttributesExtractorBuilder.setCapturedResponseHeaders(responseHeaders); + serverBuilder.setCapturedResponseHeaders(responseHeaders); return this; } /** - * Returns a new {@link SpringWebfluxTelemetry} with the settings of this {@link - * SpringWebfluxTelemetryBuilder}. + * Configures the instrumentation to recognize an alternative set of HTTP request methods. + * + *

    By default, this instrumentation defines "known" methods as the ones listed in RFC9110 and the PATCH + * method defined in RFC5789. + * + *

    Note: calling this method overrides the default known method sets completely; it does + * not supplement it. + * + * @param knownMethods A set of recognized HTTP request methods. + * @see HttpClientAttributesExtractorBuilder#setKnownMethods(Set) + * @see HttpServerAttributesExtractorBuilder#setKnownMethods(Set) */ - public SpringWebfluxTelemetry build() { + @CanIgnoreReturnValue + public SpringWebfluxTelemetryBuilder setKnownMethods(Set knownMethods) { + clientBuilder.setKnownMethods(knownMethods); + serverBuilder.setKnownMethods(knownMethods); + return this; + } - Instrumenter clientInstrumenter = clientBuilder.build(); + /** + * Configures the instrumentation to emit experimental HTTP client metrics. + * + * @param emitExperimentalHttpClientTelemetry {@code true} if the experimental HTTP client metrics + * are to be emitted. + */ + @CanIgnoreReturnValue + public SpringWebfluxTelemetryBuilder setEmitExperimentalHttpClientTelemetry( + boolean emitExperimentalHttpClientTelemetry) { + clientBuilder.setEmitExperimentalHttpClientMetrics(emitExperimentalHttpClientTelemetry); + return this; + } - WebfluxServerHttpAttributesGetter serverAttributesGetter = - WebfluxServerHttpAttributesGetter.INSTANCE; - SpanNameExtractor serverSpanNameExtractor = - HttpSpanNameExtractor.create(serverAttributesGetter); + /** + * Configures the instrumentation to emit experimental HTTP server metrics. + * + * @param emitExperimentalHttpServerTelemetry {@code true} if the experimental HTTP server metrics + * are to be emitted. + */ + @CanIgnoreReturnValue + public SpringWebfluxTelemetryBuilder setEmitExperimentalHttpServerTelemetry( + boolean emitExperimentalHttpServerTelemetry) { + serverBuilder.setEmitExperimentalHttpServerMetrics(emitExperimentalHttpServerTelemetry); + return this; + } - Instrumenter serverInstrumenter = - Instrumenter.builder( - openTelemetry, INSTRUMENTATION_NAME, serverSpanNameExtractor) - .setSpanStatusExtractor(HttpSpanStatusExtractor.create(serverAttributesGetter)) - .addAttributesExtractor(httpServerAttributesExtractorBuilder.build()) - .addAttributesExtractors(serverAdditionalExtractors) - .addContextCustomizer(HttpRouteHolder.create(serverAttributesGetter)) - .addOperationMetrics(HttpServerMetrics.get()) - .buildServerInstrumenter(WebfluxTextMapGetter.INSTANCE); + /** Sets custom client {@link SpanNameExtractor} via transform function. */ + @CanIgnoreReturnValue + public SpringWebfluxTelemetryBuilder setClientSpanNameExtractor( + Function< + SpanNameExtractor, + ? extends SpanNameExtractor> + clientSpanNameExtractor) { + clientBuilder.setSpanNameExtractor(clientSpanNameExtractor); + return this; + } + + /** Sets custom server {@link SpanNameExtractor} via transform function. */ + @CanIgnoreReturnValue + public SpringWebfluxTelemetryBuilder setServerSpanNameExtractor( + Function< + SpanNameExtractor, + ? extends SpanNameExtractor> + serverSpanNameExtractor) { + serverBuilder.setSpanNameExtractor(serverSpanNameExtractor); + return this; + } + /** + * Returns a new {@link SpringWebfluxTelemetry} with the settings of this {@link + * SpringWebfluxTelemetryBuilder}. + */ + public SpringWebfluxTelemetry build() { return new SpringWebfluxTelemetry( - clientInstrumenter, serverInstrumenter, openTelemetry.getPropagators()); + clientBuilder.build(), + serverBuilder.build(), + clientBuilder.getOpenTelemetry().getPropagators()); + } + + private DefaultHttpClientInstrumenterBuilder getClientBuilder() { + return clientBuilder; + } + + private DefaultHttpServerInstrumenterBuilder + getServerBuilder() { + return serverBuilder; } } diff --git a/instrumentation/spring/spring-webflux/spring-webflux-5.3/library/src/main/java/io/opentelemetry/instrumentation/spring/webflux/v5_3/TelemetryProducingWebFilter.java b/instrumentation/spring/spring-webflux/spring-webflux-5.3/library/src/main/java/io/opentelemetry/instrumentation/spring/webflux/v5_3/TelemetryProducingWebFilter.java index f8977bf1a6c4..3296be05fffc 100644 --- a/instrumentation/spring/spring-webflux/spring-webflux-5.3/library/src/main/java/io/opentelemetry/instrumentation/spring/webflux/v5_3/TelemetryProducingWebFilter.java +++ b/instrumentation/spring/spring-webflux/spring-webflux-5.3/library/src/main/java/io/opentelemetry/instrumentation/spring/webflux/v5_3/TelemetryProducingWebFilter.java @@ -8,8 +8,8 @@ import io.opentelemetry.context.Context; import io.opentelemetry.context.Scope; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteHolder; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteSource; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRoute; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRouteSource; import org.reactivestreams.Subscription; import org.springframework.core.Ordered; import org.springframework.http.server.reactive.ServerHttpResponse; @@ -131,10 +131,11 @@ private void onTerminal(Context currentContext, Throwable t) { private void end(Context currentContext, Throwable t) { // Update HTTP route now, because during instrumenter.start() // the HTTP route isn't available from the exchange attributes, but is afterwards - HttpRouteHolder.updateHttpRoute( + HttpServerRoute.update( currentContext, - HttpRouteSource.CONTROLLER, - WebfluxServerHttpAttributesGetter.INSTANCE.getHttpRoute(exchange)); + HttpServerRouteSource.CONTROLLER, + (context, exchange) -> WebfluxServerHttpAttributesGetter.INSTANCE.getHttpRoute(exchange), + exchange); instrumenter.end(currentContext, exchange, exchange, t); } } diff --git a/instrumentation/spring/spring-webflux/spring-webflux-5.3/library/src/main/java/io/opentelemetry/instrumentation/spring/webflux/v5_3/WebfluxServerHttpAttributesGetter.java b/instrumentation/spring/spring-webflux/spring-webflux-5.3/library/src/main/java/io/opentelemetry/instrumentation/spring/webflux/v5_3/WebfluxServerHttpAttributesGetter.java index cd611a8cac39..73a8089878d5 100644 --- a/instrumentation/spring/spring-webflux/spring-webflux-5.3/library/src/main/java/io/opentelemetry/instrumentation/spring/webflux/v5_3/WebfluxServerHttpAttributesGetter.java +++ b/instrumentation/spring/spring-webflux/spring-webflux-5.3/library/src/main/java/io/opentelemetry/instrumentation/spring/webflux/v5_3/WebfluxServerHttpAttributesGetter.java @@ -5,7 +5,8 @@ package io.opentelemetry.instrumentation.spring.webflux.v5_3; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpServerAttributesGetter; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerAttributesGetter; +import java.net.InetSocketAddress; import java.util.Collections; import java.util.List; import javax.annotation.Nullable; @@ -19,7 +20,7 @@ enum WebfluxServerHttpAttributesGetter @Override public String getHttpRequestMethod(ServerWebExchange request) { - return request.getRequest().getMethodValue(); + return request.getRequest().getMethod().name(); } @Override @@ -77,4 +78,18 @@ public String getHttpRoute(ServerWebExchange request) { String contextPath = request.getRequest().getPath().contextPath().value(); return contextPath + (route.startsWith("/") ? route : ("/" + route)); } + + @Nullable + @Override + public InetSocketAddress getNetworkPeerInetSocketAddress( + ServerWebExchange request, @Nullable ServerWebExchange response) { + return request.getRequest().getRemoteAddress(); + } + + @Nullable + @Override + public InetSocketAddress getNetworkLocalInetSocketAddress( + ServerWebExchange request, @Nullable ServerWebExchange response) { + return request.getRequest().getLocalAddress(); + } } diff --git a/instrumentation/spring/spring-webflux/spring-webflux-5.3/library/src/main/java/io/opentelemetry/instrumentation/spring/webflux/v5_3/WebfluxServerNetAttributesGetter.java b/instrumentation/spring/spring-webflux/spring-webflux-5.3/library/src/main/java/io/opentelemetry/instrumentation/spring/webflux/v5_3/WebfluxServerNetAttributesGetter.java deleted file mode 100644 index 5c3cfd395886..000000000000 --- a/instrumentation/spring/spring-webflux/spring-webflux-5.3/library/src/main/java/io/opentelemetry/instrumentation/spring/webflux/v5_3/WebfluxServerNetAttributesGetter.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.spring.webflux.v5_3; - -import io.opentelemetry.instrumentation.api.instrumenter.net.NetServerAttributesGetter; -import java.net.InetSocketAddress; -import javax.annotation.Nullable; -import org.springframework.web.server.ServerWebExchange; - -final class WebfluxServerNetAttributesGetter - implements NetServerAttributesGetter { - - @Nullable - @Override - public String getServerAddress(ServerWebExchange request) { - return null; - } - - @Nullable - @Override - public Integer getServerPort(ServerWebExchange request) { - int port = request.getRequest().getURI().getPort(); - return port == -1 ? null : port; - } - - @Nullable - @Override - public InetSocketAddress getClientInetSocketAddress( - ServerWebExchange request, @Nullable ServerWebExchange response) { - return request.getRequest().getRemoteAddress(); - } - - @Nullable - @Override - public InetSocketAddress getServerInetSocketAddress( - ServerWebExchange request, @Nullable ServerWebExchange response) { - return request.getRequest().getLocalAddress(); - } -} diff --git a/instrumentation/spring/spring-webflux/spring-webflux-5.3/library/src/main/java/io/opentelemetry/instrumentation/spring/webflux/v5_3/internal/SpringWebfluxBuilderUtil.java b/instrumentation/spring/spring-webflux/spring-webflux-5.3/library/src/main/java/io/opentelemetry/instrumentation/spring/webflux/v5_3/internal/SpringWebfluxBuilderUtil.java new file mode 100644 index 000000000000..ec1d643b1d88 --- /dev/null +++ b/instrumentation/spring/spring-webflux/spring-webflux-5.3/library/src/main/java/io/opentelemetry/instrumentation/spring/webflux/v5_3/internal/SpringWebfluxBuilderUtil.java @@ -0,0 +1,64 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.webflux.v5_3.internal; + +import io.opentelemetry.instrumentation.api.incubator.builder.internal.DefaultHttpClientInstrumenterBuilder; +import io.opentelemetry.instrumentation.api.incubator.builder.internal.DefaultHttpServerInstrumenterBuilder; +import io.opentelemetry.instrumentation.spring.webflux.v5_3.SpringWebfluxTelemetryBuilder; +import java.util.function.Function; +import org.springframework.web.reactive.function.client.ClientRequest; +import org.springframework.web.reactive.function.client.ClientResponse; +import org.springframework.web.server.ServerWebExchange; + +/** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time. + */ +public final class SpringWebfluxBuilderUtil { + private SpringWebfluxBuilderUtil() {} + + // allows access to the private field for the spring starter + private static Function< + SpringWebfluxTelemetryBuilder, + DefaultHttpClientInstrumenterBuilder> + clientBuilderExtractor; + + // allows access to the private field for the spring starter + private static Function< + SpringWebfluxTelemetryBuilder, + DefaultHttpServerInstrumenterBuilder> + serverBuilderExtractor; + + public static Function< + SpringWebfluxTelemetryBuilder, + DefaultHttpServerInstrumenterBuilder> + getServerBuilderExtractor() { + return serverBuilderExtractor; + } + + public static void setServerBuilderExtractor( + Function< + SpringWebfluxTelemetryBuilder, + DefaultHttpServerInstrumenterBuilder> + serverBuilderExtractor) { + SpringWebfluxBuilderUtil.serverBuilderExtractor = serverBuilderExtractor; + } + + public static Function< + SpringWebfluxTelemetryBuilder, + DefaultHttpClientInstrumenterBuilder> + getClientBuilderExtractor() { + return clientBuilderExtractor; + } + + public static void setClientBuilderExtractor( + Function< + SpringWebfluxTelemetryBuilder, + DefaultHttpClientInstrumenterBuilder> + clientBuilderExtractor) { + SpringWebfluxBuilderUtil.clientBuilderExtractor = clientBuilderExtractor; + } +} diff --git a/instrumentation/spring/spring-webflux/spring-webflux-5.3/library/src/main/java/io/opentelemetry/instrumentation/spring/webflux/v5_3/internal/SpringWebfluxTelemetryClientBuilder.java b/instrumentation/spring/spring-webflux/spring-webflux-5.3/library/src/main/java/io/opentelemetry/instrumentation/spring/webflux/v5_3/internal/SpringWebfluxTelemetryClientBuilder.java deleted file mode 100644 index 0e22a6c79ecc..000000000000 --- a/instrumentation/spring/spring-webflux/spring-webflux-5.3/library/src/main/java/io/opentelemetry/instrumentation/spring/webflux/v5_3/internal/SpringWebfluxTelemetryClientBuilder.java +++ /dev/null @@ -1,121 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.spring.webflux.v5_3.internal; - -import static io.opentelemetry.instrumentation.api.instrumenter.SpanKindExtractor.alwaysClient; - -import com.google.errorprone.annotations.CanIgnoreReturnValue; -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.http.HttpClientAttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpClientAttributesExtractorBuilder; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpClientMetrics; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpSpanNameExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpSpanStatusExtractor; -import io.opentelemetry.instrumentation.spring.webflux.v5_3.SpringWebfluxTelemetry; -import java.util.ArrayList; -import java.util.List; -import org.springframework.web.reactive.function.client.ClientRequest; -import org.springframework.web.reactive.function.client.ClientResponse; - -/** - * This class is internal and is hence not for public use. Its APIs are unstable and can change at - * any time. - */ -// client builder is separate so that it can be used by javaagent instrumentation -// which supports 5.0, without triggering the server instrumentation which depends on webflux 5.3 -public final class SpringWebfluxTelemetryClientBuilder { - private static final String INSTRUMENTATION_NAME = "io.opentelemetry.spring-webflux-5.3"; - - private final OpenTelemetry openTelemetry; - private final List> - clientAdditionalExtractors = new ArrayList<>(); - private final HttpClientAttributesExtractorBuilder - httpClientAttributesExtractorBuilder = - HttpClientAttributesExtractor.builder( - WebClientHttpAttributesGetter.INSTANCE, new WebClientNetAttributesGetter()); - - private boolean captureExperimentalSpanAttributes = false; - - public SpringWebfluxTelemetryClientBuilder(OpenTelemetry openTelemetry) { - this.openTelemetry = openTelemetry; - } - - /** - * Adds an additional {@link AttributesExtractor} to invoke to set attributes to instrumented - * items for WebClient. - */ - @CanIgnoreReturnValue - public SpringWebfluxTelemetryClientBuilder addClientAttributesExtractor( - AttributesExtractor attributesExtractor) { - clientAdditionalExtractors.add(attributesExtractor); - return this; - } - - /** - * Configures the HTTP WebClient request headers that will be captured as span attributes. - * - * @param requestHeaders A list of HTTP header names. - */ - @CanIgnoreReturnValue - public SpringWebfluxTelemetryClientBuilder setCapturedClientRequestHeaders( - List requestHeaders) { - httpClientAttributesExtractorBuilder.setCapturedRequestHeaders(requestHeaders); - return this; - } - - /** - * Configures the HTTP WebClient response headers that will be captured as span attributes. - * - * @param responseHeaders A list of HTTP header names. - */ - @CanIgnoreReturnValue - public SpringWebfluxTelemetryClientBuilder setCapturedClientResponseHeaders( - List responseHeaders) { - httpClientAttributesExtractorBuilder.setCapturedResponseHeaders(responseHeaders); - 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. - */ - @CanIgnoreReturnValue - public SpringWebfluxTelemetryClientBuilder setCaptureExperimentalSpanAttributes( - boolean captureExperimentalSpanAttributes) { - this.captureExperimentalSpanAttributes = captureExperimentalSpanAttributes; - return this; - } - - /** - * Returns a new {@link SpringWebfluxTelemetry} with the settings of this {@link - * SpringWebfluxTelemetryClientBuilder}. - */ - public Instrumenter build() { - WebClientHttpAttributesGetter httpClientAttributesGetter = - WebClientHttpAttributesGetter.INSTANCE; - - InstrumenterBuilder clientBuilder = - Instrumenter.builder( - openTelemetry, - INSTRUMENTATION_NAME, - HttpSpanNameExtractor.create(httpClientAttributesGetter)) - .setSpanStatusExtractor(HttpSpanStatusExtractor.create(httpClientAttributesGetter)) - .addAttributesExtractor(httpClientAttributesExtractorBuilder.build()) - .addAttributesExtractors(clientAdditionalExtractors) - .addOperationMetrics(HttpClientMetrics.get()); - - if (captureExperimentalSpanAttributes) { - clientBuilder.addAttributesExtractor(new WebClientExperimentalAttributesExtractor()); - } - - // headers are injected elsewhere; ClientRequest is immutable - return clientBuilder.buildInstrumenter(alwaysClient()); - } -} diff --git a/instrumentation/spring/spring-webflux/spring-webflux-5.3/library/src/main/java/io/opentelemetry/instrumentation/spring/webflux/v5_3/internal/WebClientExperimentalAttributesExtractor.java b/instrumentation/spring/spring-webflux/spring-webflux-5.3/library/src/main/java/io/opentelemetry/instrumentation/spring/webflux/v5_3/internal/WebClientExperimentalAttributesExtractor.java deleted file mode 100644 index e915efedb180..000000000000 --- a/instrumentation/spring/spring-webflux/spring-webflux-5.3/library/src/main/java/io/opentelemetry/instrumentation/spring/webflux/v5_3/internal/WebClientExperimentalAttributesExtractor.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.spring.webflux.v5_3.internal; - -import static io.opentelemetry.api.common.AttributeKey.stringKey; - -import io.opentelemetry.api.common.AttributeKey; -import io.opentelemetry.api.common.AttributesBuilder; -import io.opentelemetry.context.Context; -import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; -import javax.annotation.Nullable; -import org.springframework.web.reactive.function.client.ClientRequest; -import org.springframework.web.reactive.function.client.ClientResponse; - -final class WebClientExperimentalAttributesExtractor - implements AttributesExtractor { - - private static final AttributeKey SPRING_WEBFLUX_EVENT = - stringKey("spring-webflux.event"); - private static final AttributeKey SPRING_WEBFLUX_MESSAGE = - stringKey("spring-webflux.message"); - - @Override - public void onStart(AttributesBuilder attributes, Context parentContext, ClientRequest request) {} - - @Override - public void onEnd( - AttributesBuilder attributes, - Context context, - ClientRequest request, - @Nullable ClientResponse response, - @Nullable Throwable error) { - - // no response and no error means that the request has been cancelled - if (response == null && error == null) { - attributes.put(SPRING_WEBFLUX_EVENT, "cancelled"); - attributes.put(SPRING_WEBFLUX_MESSAGE, "The subscription was cancelled"); - } - } -} diff --git a/instrumentation/spring/spring-webflux/spring-webflux-5.3/library/src/main/java/io/opentelemetry/instrumentation/spring/webflux/v5_3/internal/WebClientHttpAttributesGetter.java b/instrumentation/spring/spring-webflux/spring-webflux-5.3/library/src/main/java/io/opentelemetry/instrumentation/spring/webflux/v5_3/internal/WebClientHttpAttributesGetter.java index d76b7817f441..80c75f68a61d 100644 --- a/instrumentation/spring/spring-webflux/spring-webflux-5.3/library/src/main/java/io/opentelemetry/instrumentation/spring/webflux/v5_3/internal/WebClientHttpAttributesGetter.java +++ b/instrumentation/spring/spring-webflux/spring-webflux-5.3/library/src/main/java/io/opentelemetry/instrumentation/spring/webflux/v5_3/internal/WebClientHttpAttributesGetter.java @@ -7,13 +7,17 @@ import static java.util.Collections.emptyList; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpClientAttributesGetter; +import io.opentelemetry.instrumentation.api.semconv.http.HttpClientAttributesGetter; import java.util.List; import javax.annotation.Nullable; import org.springframework.web.reactive.function.client.ClientRequest; import org.springframework.web.reactive.function.client.ClientResponse; -enum WebClientHttpAttributesGetter +/** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time. + */ +public enum WebClientHttpAttributesGetter implements HttpClientAttributesGetter { INSTANCE; @@ -44,4 +48,27 @@ public List getHttpResponseHeader( ClientRequest request, ClientResponse response, String name) { return response.headers().header(name); } + + @Nullable + @Override + public String getServerAddress(ClientRequest request) { + return request.url().getHost(); + } + + @Override + public Integer getServerPort(ClientRequest request) { + return request.url().getPort(); + } + + @Nullable + @Override + public String getErrorType( + ClientRequest request, @Nullable ClientResponse response, @Nullable Throwable error) { + // if both response and error are null it means the request has been cancelled -- see the + // WebClientTracingFilter class + if (response == null && error == null) { + return "cancelled"; + } + return null; + } } diff --git a/instrumentation/spring/spring-webflux/spring-webflux-5.3/library/src/main/java/io/opentelemetry/instrumentation/spring/webflux/v5_3/internal/WebClientNetAttributesGetter.java b/instrumentation/spring/spring-webflux/spring-webflux-5.3/library/src/main/java/io/opentelemetry/instrumentation/spring/webflux/v5_3/internal/WebClientNetAttributesGetter.java deleted file mode 100644 index 725ff0bb0d0e..000000000000 --- a/instrumentation/spring/spring-webflux/spring-webflux-5.3/library/src/main/java/io/opentelemetry/instrumentation/spring/webflux/v5_3/internal/WebClientNetAttributesGetter.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.spring.webflux.v5_3.internal; - -import io.opentelemetry.instrumentation.api.instrumenter.net.NetClientAttributesGetter; -import javax.annotation.Nullable; -import org.springframework.web.reactive.function.client.ClientRequest; -import org.springframework.web.reactive.function.client.ClientResponse; - -/** - * This class is internal and is hence not for public use. Its APIs are unstable and can change at - * any time. - */ -public final class WebClientNetAttributesGetter - implements NetClientAttributesGetter { - - @Nullable - @Override - public String getServerAddress(ClientRequest request) { - return request.url().getHost(); - } - - @Override - public Integer getServerPort(ClientRequest request) { - return request.url().getPort(); - } -} diff --git a/instrumentation/spring/spring-webflux/spring-webflux-5.3/library/src/test/java/io/opentelemetry/instrumentation/spring/webflux/v5_3/SpringWebfluxServerInstrumentationTest.java b/instrumentation/spring/spring-webflux/spring-webflux-5.3/library/src/test/java/io/opentelemetry/instrumentation/spring/webflux/v5_3/SpringWebfluxServerInstrumentationTest.java index 0adf05481a5a..54a71116a581 100644 --- a/instrumentation/spring/spring-webflux/spring-webflux-5.3/library/src/test/java/io/opentelemetry/instrumentation/spring/webflux/v5_3/SpringWebfluxServerInstrumentationTest.java +++ b/instrumentation/spring/spring-webflux/spring-webflux-5.3/library/src/test/java/io/opentelemetry/instrumentation/spring/webflux/v5_3/SpringWebfluxServerInstrumentationTest.java @@ -5,11 +5,17 @@ package io.opentelemetry.instrumentation.spring.webflux.v5_3; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.SUCCESS; +import static org.assertj.core.api.Assertions.assertThat; + import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpServerTest; import io.opentelemetry.instrumentation.testing.junit.http.HttpServerInstrumentationExtension; import io.opentelemetry.instrumentation.testing.junit.http.HttpServerTestOptions; import io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint; +import io.opentelemetry.testing.internal.armeria.common.AggregatedHttpRequest; +import io.opentelemetry.testing.internal.armeria.common.AggregatedHttpResponse; +import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.springframework.context.ConfigurableApplicationContext; @@ -38,11 +44,26 @@ protected void configure(HttpServerTestOptions options) { options.setExpectedException(new RuntimeException(ServerEndpoint.EXCEPTION.getBody())); options.setExpectedHttpRoute( - endpoint -> { + (endpoint, method) -> { if (endpoint == ServerEndpoint.PATH_PARAM) { return CONTEXT_PATH + "/path/{id}/param"; } - return expectedHttpRoute(endpoint); + return expectedHttpRoute(endpoint, method); }); + + options.disableTestNonStandardHttpMethod(); + } + + @Test + void noMono() { + ServerEndpoint endpoint = new ServerEndpoint("NO_MONO", "no-mono", 200, "success"); + String method = "GET"; + AggregatedHttpRequest request = request(endpoint, method); + AggregatedHttpResponse response = client.execute(request).aggregate().join(); + + assertThat(response.status().code()).isEqualTo(SUCCESS.getStatus()); + assertThat(response.contentUtf8()).isEqualTo(SUCCESS.getBody()); + + assertTheTraces(1, null, null, null, method, endpoint); } } diff --git a/instrumentation/spring/spring-webflux/spring-webflux-5.3/library/src/test/java/io/opentelemetry/instrumentation/spring/webflux/v5_3/TestWebfluxSpringBootApp.java b/instrumentation/spring/spring-webflux/spring-webflux-5.3/library/src/test/java/io/opentelemetry/instrumentation/spring/webflux/v5_3/TestWebfluxSpringBootApp.java index 5437f2e481f6..c0eb9a65d91e 100644 --- a/instrumentation/spring/spring-webflux/spring-webflux-5.3/library/src/test/java/io/opentelemetry/instrumentation/spring/webflux/v5_3/TestWebfluxSpringBootApp.java +++ b/instrumentation/spring/spring-webflux/spring-webflux-5.3/library/src/test/java/io/opentelemetry/instrumentation/spring/webflux/v5_3/TestWebfluxSpringBootApp.java @@ -57,7 +57,7 @@ WebFilter telemetryFilter() { .setCapturedServerResponseHeaders( singletonList(AbstractHttpServerTest.TEST_RESPONSE_HEADER)) .build() - .createWebFilter(); + .createWebFilterAndRegisterReactorHook(); } @Controller @@ -69,10 +69,16 @@ Flux success() { return Flux.defer(() -> Flux.just(controller(SUCCESS, SUCCESS::getBody))); } + @RequestMapping("/no-mono") + @ResponseBody + String noMono() { + return controller(SUCCESS, SUCCESS::getBody); + } + @RequestMapping("/query") @ResponseBody - String query_param(@RequestParam("some") String param) { - return controller(QUERY_PARAM, () -> "some=" + param); + Mono query_param(@RequestParam("some") String param) { + return Mono.just(controller(QUERY_PARAM, () -> "some=" + param)); } @RequestMapping("/redirect") @@ -102,20 +108,21 @@ Flux> exception() throws Exception { } @RequestMapping("/captureHeaders") - ResponseEntity capture_headers( + Mono> capture_headers( @RequestHeader("X-Test-Request") String testRequestHeader) { - return controller( - CAPTURE_HEADERS, - () -> - ResponseEntity.ok() - .header("X-Test-Response", testRequestHeader) - .body(CAPTURE_HEADERS.getBody())); + return Mono.just( + controller( + CAPTURE_HEADERS, + () -> + ResponseEntity.ok() + .header("X-Test-Response", testRequestHeader) + .body(CAPTURE_HEADERS.getBody()))); } @RequestMapping("/path/{id}/param") @ResponseBody - String path_param(@PathVariable("id") int id) { - return controller(PATH_PARAM, () -> String.valueOf(id)); + Mono path_param(@PathVariable("id") int id) { + return Mono.just(controller(PATH_PARAM, () -> String.valueOf(id))); } @RequestMapping("/child") diff --git a/instrumentation/spring/spring-webflux/spring-webflux-5.3/testing/build.gradle.kts b/instrumentation/spring/spring-webflux/spring-webflux-5.3/testing/build.gradle.kts index 6b64753ea1a4..6851a0221e6a 100644 --- a/instrumentation/spring/spring-webflux/spring-webflux-5.3/testing/build.gradle.kts +++ b/instrumentation/spring/spring-webflux/spring-webflux-5.3/testing/build.gradle.kts @@ -11,4 +11,6 @@ dependencies { // latest dep tests. compileOnly("io.projectreactor.ipc:reactor-netty:0.7.0.RELEASE") compileOnly("io.projectreactor.netty:reactor-netty-http:1.0.7") + + compileOnly("org.springframework.boot:spring-boot:2.1.0.RELEASE") } diff --git a/instrumentation/spring/spring-webflux/spring-webflux-5.3/testing/src/main/java/io/opentelemetry/instrumentation/spring/webflux/client/AbstractSpringWebfluxClientInstrumentationTest.java b/instrumentation/spring/spring-webflux/spring-webflux-5.3/testing/src/main/java/io/opentelemetry/instrumentation/spring/webflux/client/AbstractSpringWebfluxClientInstrumentationTest.java index 3912f8e28eaf..d7794f3e4469 100644 --- a/instrumentation/spring/spring-webflux/spring-webflux-5.3/testing/src/main/java/io/opentelemetry/instrumentation/spring/webflux/client/AbstractSpringWebfluxClientInstrumentationTest.java +++ b/instrumentation/spring/spring-webflux/spring-webflux-5.3/testing/src/main/java/io/opentelemetry/instrumentation/spring/webflux/client/AbstractSpringWebfluxClientInstrumentationTest.java @@ -5,20 +5,32 @@ package io.opentelemetry.instrumentation.spring.webflux.client; -import static io.opentelemetry.api.common.AttributeKey.stringKey; +import static io.opentelemetry.api.trace.SpanKind.CLIENT; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; +import static java.util.Collections.emptyMap; import static java.util.Objects.requireNonNull; +import static org.assertj.core.api.Assertions.catchThrowable; import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.trace.SpanKind; import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpClientTest; import io.opentelemetry.instrumentation.testing.junit.http.HttpClientResult; import io.opentelemetry.instrumentation.testing.junit.http.HttpClientTestOptions; +import io.opentelemetry.sdk.trace.data.StatusData; +import io.opentelemetry.semconv.ErrorAttributes; +import io.opentelemetry.semconv.HttpAttributes; +import io.opentelemetry.semconv.NetworkAttributes; +import io.opentelemetry.semconv.ServerAttributes; +import io.opentelemetry.semconv.UrlAttributes; import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodType; import java.net.URI; +import java.time.Duration; import java.util.HashSet; import java.util.Map; import java.util.Set; +import org.junit.jupiter.api.Test; import org.springframework.http.HttpMethod; import org.springframework.web.reactive.function.client.ClientResponse; import org.springframework.web.reactive.function.client.WebClient; @@ -67,6 +79,9 @@ public void sendRequestWithCallback( protected void configure(HttpClientTestOptions.Builder optionsBuilder) { optionsBuilder.disableTestRedirects(); + // no enum value for non standard method + optionsBuilder.disableTestNonStandardHttpMethod(); + // timeouts leak the scope optionsBuilder.disableTestReadTimeout(); @@ -74,8 +89,7 @@ protected void configure(HttpClientTestOptions.Builder optionsBuilder) { uri -> { Set> attributes = new HashSet<>(HttpClientTestOptions.DEFAULT_HTTP_ATTRIBUTES); - attributes.remove(stringKey("net.protocol.name")); - attributes.remove(stringKey("net.protocol.version")); + attributes.remove(NetworkAttributes.NETWORK_PROTOCOL_VERSION); return attributes; }); @@ -140,4 +154,44 @@ private static int getStatusCode(ClientResponse response) { throw new AssertionError(e); } } + + @Test + void shouldEndSpanOnMonoTimeout() { + URI uri = resolveAddress("/read-timeout"); + Throwable thrown = + catchThrowable( + () -> + testing.runWithSpan( + "parent", + () -> + buildRequest("GET", uri, emptyMap()) + .exchange() + // apply Mono timeout that is way shorter than HTTP request timeout + .timeout(Duration.ofSeconds(1)) + .block())); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("parent") + .hasKind(SpanKind.INTERNAL) + .hasNoParent() + .hasStatus(StatusData.error()) + .hasException(thrown), + span -> + span.hasName("GET") + .hasKind(CLIENT) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo(HttpAttributes.HTTP_REQUEST_METHOD, "GET"), + equalTo(UrlAttributes.URL_FULL, uri.toString()), + equalTo(ServerAttributes.SERVER_ADDRESS, "localhost"), + equalTo(ServerAttributes.SERVER_PORT, uri.getPort()), + equalTo(ErrorAttributes.ERROR_TYPE, "cancelled")), + span -> + span.hasName("test-http-server") + .hasKind(SpanKind.SERVER) + .hasParent(trace.getSpan(1)))); + } } diff --git a/instrumentation/spring/spring-webflux/spring-webflux-5.3/testing/src/main/java/io/opentelemetry/instrumentation/spring/webflux/server/SingleThreadNettyCustomizer.java b/instrumentation/spring/spring-webflux/spring-webflux-5.3/testing/src/main/java/io/opentelemetry/instrumentation/spring/webflux/server/SingleThreadNettyCustomizer.java new file mode 100644 index 000000000000..d4a207bc0b8f --- /dev/null +++ b/instrumentation/spring/spring-webflux/spring-webflux-5.3/testing/src/main/java/io/opentelemetry/instrumentation/spring/webflux/server/SingleThreadNettyCustomizer.java @@ -0,0 +1,17 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.webflux.server; + +import org.springframework.boot.web.embedded.netty.NettyServerCustomizer; +import reactor.netty.http.server.HttpServer; + +public class SingleThreadNettyCustomizer implements NettyServerCustomizer { + + @Override + public HttpServer apply(HttpServer server) { + return server.runOn(reactor.netty.resources.LoopResources.create("my-http", 1, true)); + } +} diff --git a/instrumentation/spring/spring-webmvc/spring-webmvc-3.1/javaagent/build.gradle.kts b/instrumentation/spring/spring-webmvc/spring-webmvc-3.1/javaagent/build.gradle.kts index a03906d1e9b4..5b5818e627ad 100644 --- a/instrumentation/spring/spring-webmvc/spring-webmvc-3.1/javaagent/build.gradle.kts +++ b/instrumentation/spring/spring-webmvc/spring-webmvc-3.1/javaagent/build.gradle.kts @@ -43,11 +43,15 @@ dependencies { } tasks.withType().configureEach { + systemProperty("testLatestDeps", findProperty("testLatestDeps") as Boolean) + // TODO run tests both with and without experimental span attributes jvmArgs("-Dotel.instrumentation.spring-webmvc.experimental-span-attributes=true") // required on jdk17 jvmArgs("--add-opens=java.base/java.lang=ALL-UNNAMED") jvmArgs("-XX:+IgnoreUnrecognizedVMOptions") + jvmArgs("-Dotel.instrumentation.common.experimental.controller-telemetry.enabled=true") + jvmArgs("-Dotel.instrumentation.common.experimental.view-telemetry.enabled=true") } configurations.testRuntimeClasspath { diff --git a/instrumentation/spring/spring-webmvc/spring-webmvc-3.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/webmvc/v3_1/HandlerAdapterInstrumentation.java b/instrumentation/spring/spring-webmvc/spring-webmvc-3.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/webmvc/v3_1/HandlerAdapterInstrumentation.java index 898f959ed3fa..0ca4b38695c7 100644 --- a/instrumentation/spring/spring-webmvc/spring-webmvc-3.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/webmvc/v3_1/HandlerAdapterInstrumentation.java +++ b/instrumentation/spring/spring-webmvc/spring-webmvc-3.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/webmvc/v3_1/HandlerAdapterInstrumentation.java @@ -5,7 +5,7 @@ package io.opentelemetry.javaagent.instrumentation.spring.webmvc.v3_1; -import static io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteSource.CONTROLLER; +import static io.opentelemetry.instrumentation.api.semconv.http.HttpServerRouteSource.CONTROLLER; import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.hasClassesNamed; import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.implementsInterface; import static io.opentelemetry.javaagent.instrumentation.spring.webmvc.v3_1.SpringWebMvcSingletons.handlerInstrumenter; @@ -18,7 +18,7 @@ import io.opentelemetry.context.Context; import io.opentelemetry.context.Scope; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteHolder; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRoute; import io.opentelemetry.javaagent.bootstrap.Java8BytecodeBridge; import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; @@ -74,7 +74,7 @@ public static void nameResourceAndStartSpan( } // Name the parent span based on the matching pattern - HttpRouteHolder.updateHttpRoute( + HttpServerRoute.update( parentContext, CONTROLLER, SpringWebMvcServerSpanNaming.SERVER_SPAN_NAME, request); if (!handlerInstrumenter().shouldStart(parentContext, handler)) { diff --git a/instrumentation/spring/spring-webmvc/spring-webmvc-3.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/webmvc/v3_1/SpringWebMvcInstrumentationModule.java b/instrumentation/spring/spring-webmvc/spring-webmvc-3.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/webmvc/v3_1/SpringWebMvcInstrumentationModule.java index 5b4064257e8d..4fcececf341c 100644 --- a/instrumentation/spring/spring-webmvc/spring-webmvc-3.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/webmvc/v3_1/SpringWebMvcInstrumentationModule.java +++ b/instrumentation/spring/spring-webmvc/spring-webmvc-3.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/webmvc/v3_1/SpringWebMvcInstrumentationModule.java @@ -32,6 +32,11 @@ public boolean isHelperClass(String className) { "org.springframework.web.servlet.v3_1.OpenTelemetryHandlerMappingFilter"); } + @Override + public boolean isIndyModule() { + return false; + } + @Override public List typeInstrumentations() { return asList(new DispatcherServletInstrumentation(), new HandlerAdapterInstrumentation()); diff --git a/instrumentation/spring/spring-webmvc/spring-webmvc-3.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/webmvc/v3_1/SpringWebMvcServerSpanNaming.java b/instrumentation/spring/spring-webmvc/spring-webmvc-3.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/webmvc/v3_1/SpringWebMvcServerSpanNaming.java index c78ef70f2921..d9cbc74125cb 100644 --- a/instrumentation/spring/spring-webmvc/spring-webmvc-3.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/webmvc/v3_1/SpringWebMvcServerSpanNaming.java +++ b/instrumentation/spring/spring-webmvc/spring-webmvc-3.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/webmvc/v3_1/SpringWebMvcServerSpanNaming.java @@ -5,14 +5,14 @@ package io.opentelemetry.javaagent.instrumentation.spring.webmvc.v3_1; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteGetter; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRouteGetter; import io.opentelemetry.javaagent.bootstrap.servlet.ServletContextPath; import javax.servlet.http.HttpServletRequest; import org.springframework.web.servlet.HandlerMapping; public class SpringWebMvcServerSpanNaming { - public static final HttpRouteGetter SERVER_SPAN_NAME = + public static final HttpServerRouteGetter SERVER_SPAN_NAME = (context, request) -> { Object bestMatchingPattern = request.getAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE); diff --git a/instrumentation/spring/spring-webmvc/spring-webmvc-3.1/javaagent/src/main/java/org/springframework/web/servlet/v3_1/OpenTelemetryHandlerMappingFilter.java b/instrumentation/spring/spring-webmvc/spring-webmvc-3.1/javaagent/src/main/java/org/springframework/web/servlet/v3_1/OpenTelemetryHandlerMappingFilter.java index c3f11894d4d2..fae2f33b9d9c 100644 --- a/instrumentation/spring/spring-webmvc/spring-webmvc-3.1/javaagent/src/main/java/org/springframework/web/servlet/v3_1/OpenTelemetryHandlerMappingFilter.java +++ b/instrumentation/spring/spring-webmvc/spring-webmvc-3.1/javaagent/src/main/java/org/springframework/web/servlet/v3_1/OpenTelemetryHandlerMappingFilter.java @@ -5,18 +5,20 @@ package org.springframework.web.servlet.v3_1; -import static io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteSource.CONTROLLER; +import static io.opentelemetry.instrumentation.api.semconv.http.HttpServerRouteSource.CONTROLLER; import io.opentelemetry.context.Context; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteGetter; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteHolder; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRoute; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRouteGetter; import io.opentelemetry.javaagent.instrumentation.spring.webmvc.v3_1.SpringWebMvcServerSpanNaming; import java.io.IOException; import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodType; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Objects; import javax.annotation.Nullable; import javax.servlet.Filter; @@ -26,6 +28,7 @@ import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletRequestWrapper; import javax.servlet.http.HttpServletResponse; import org.springframework.core.Ordered; import org.springframework.web.servlet.HandlerExecutionChain; @@ -33,34 +36,21 @@ import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; public class OpenTelemetryHandlerMappingFilter implements Filter, Ordered { - private static final String PATH_ATTRIBUTE = getRequestPathAttribute(); private static final MethodHandle usesPathPatternsMh = getUsesPathPatternsMh(); private static final MethodHandle parseAndCacheMh = parseAndCacheMh(); - private final HttpRouteGetter serverSpanName = + private final HttpServerRouteGetter serverSpanName = (context, request) -> { - Object previousValue = null; - if (this.parseRequestPath && PATH_ATTRIBUTE != null) { - previousValue = request.getAttribute(PATH_ATTRIBUTE); + if (this.parseRequestPath) { // sets new value for PATH_ATTRIBUTE of request parseAndCache(request); } - try { - if (findMapping(request)) { - // Name the parent span based on the matching pattern - // Let the parent span resource name be set with the attribute set in findMapping. - return SpringWebMvcServerSpanNaming.SERVER_SPAN_NAME.get(context, request); - } - } finally { - // mimic spring DispatcherServlet and restore the previous value of PATH_ATTRIBUTE - if (this.parseRequestPath && PATH_ATTRIBUTE != null) { - if (previousValue == null) { - request.removeAttribute(PATH_ATTRIBUTE); - } else { - request.setAttribute(PATH_ATTRIBUTE, previousValue); - } - } + if (findMapping(request)) { + // Name the parent span based on the matching pattern + // Let the parent span resource name be set with the attribute set in findMapping. + return SpringWebMvcServerSpanNaming.SERVER_SPAN_NAME.get(context, request); } + return null; }; @@ -84,8 +74,7 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha } finally { if (handlerMappings != null) { Context context = Context.current(); - HttpRouteHolder.updateHttpRoute( - context, CONTROLLER, serverSpanName, (HttpServletRequest) request); + HttpServerRoute.update(context, CONTROLLER, serverSpanName, prepareRequest(request)); } } } @@ -93,6 +82,31 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha @Override public void destroy() {} + private static HttpServletRequest prepareRequest(ServletRequest request) { + // https://github.com/open-telemetry/opentelemetry-java-instrumentation/issues/10379 + // Finding the request handler modifies request attributes. We are wrapping the request to avoid + // this. + return new HttpServletRequestWrapper((HttpServletRequest) request) { + private final Map attributes = new HashMap<>(); + + @Override + public void setAttribute(String name, Object o) { + attributes.put(name, o); + } + + @Override + public Object getAttribute(String name) { + Object value = attributes.get(name); + return value != null ? value : super.getAttribute(name); + } + + @Override + public void removeAttribute(String name) { + attributes.remove(name); + } + }; + } + /** * 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 @@ -187,19 +201,4 @@ private static void parseAndCache(HttpServletRequest request) { throw new IllegalStateException(throwable); } } - - private static String getRequestPathAttribute() { - try { - Class pathUtilsClass = - Class.forName("org.springframework.web.util.ServletRequestPathUtils"); - return (String) - MethodHandles.lookup() - .findStaticGetter(pathUtilsClass, "PATH_ATTRIBUTE", String.class) - .invoke(); - } catch (ClassNotFoundException | NoSuchFieldException | IllegalAccessException exception) { - return null; - } catch (Throwable throwable) { - throw new IllegalStateException(throwable); - } - } } diff --git a/instrumentation/spring/spring-webmvc/spring-webmvc-3.1/javaagent/src/test/groovy/test/boot/SecurityConfig.groovy b/instrumentation/spring/spring-webmvc/spring-webmvc-3.1/javaagent/src/test/groovy/test/boot/SecurityConfig.groovy deleted file mode 100644 index fe4fd70d7ec9..000000000000 --- a/instrumentation/spring/spring-webmvc/spring-webmvc-3.1/javaagent/src/test/groovy/test/boot/SecurityConfig.groovy +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package test.boot - -import boot.SavingAuthenticationProvider -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/instrumentation/spring/spring-webmvc/spring-webmvc-3.1/javaagent/src/test/groovy/test/boot/SpringBootBasedTest.groovy b/instrumentation/spring/spring-webmvc/spring-webmvc-3.1/javaagent/src/test/groovy/test/boot/SpringBootBasedTest.groovy deleted file mode 100644 index eae63b7d233c..000000000000 --- a/instrumentation/spring/spring-webmvc/spring-webmvc-3.1/javaagent/src/test/groovy/test/boot/SpringBootBasedTest.groovy +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package test.boot - -import boot.AbstractSpringBootBasedTest - -class SpringBootBasedTest extends AbstractSpringBootBasedTest { - - Class securityConfigClass() { - SecurityConfig - } -} diff --git a/instrumentation/spring/spring-webmvc/spring-webmvc-3.1/javaagent/src/test/groovy/test/filter/ServletFilterConfig.groovy b/instrumentation/spring/spring-webmvc/spring-webmvc-3.1/javaagent/src/test/groovy/test/filter/ServletFilterConfig.groovy deleted file mode 100644 index 2b13eb1343fe..000000000000 --- a/instrumentation/spring/spring-webmvc/spring-webmvc-3.1/javaagent/src/test/groovy/test/filter/ServletFilterConfig.groovy +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package test.filter - -import io.opentelemetry.instrumentation.test.base.HttpServerTest -import io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint -import org.springframework.context.annotation.Bean -import org.springframework.context.annotation.Configuration - -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 static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.CAPTURE_HEADERS -import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.ERROR -import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.EXCEPTION -import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.INDEXED_CHILD -import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.PATH_PARAM -import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.QUERY_PARAM -import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.REDIRECT -import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.SUCCESS - -@Configuration -class ServletFilterConfig { - - @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 - ServerEndpoint endpoint = 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 CAPTURE_HEADERS: - resp.setHeader("X-Test-Response", req.getHeader("X-Test-Request")) - resp.status = endpoint.status - resp.writer.print(endpoint.body) - break - case ERROR: - resp.sendError(endpoint.status, endpoint.body) - break - case EXCEPTION: - throw new Exception(endpoint.body) - case INDEXED_CHILD: - INDEXED_CHILD.collectSpanAttributes { name -> req.getParameter(name) } - resp.writer.print(endpoint.body) - break - default: - chain.doFilter(request, response) - } - } - } - - @Override - void destroy() { - } - } - } -} diff --git a/instrumentation/spring/spring-webmvc/spring-webmvc-3.1/javaagent/src/test/groovy/test/filter/ServletFilterTest.groovy b/instrumentation/spring/spring-webmvc/spring-webmvc-3.1/javaagent/src/test/groovy/test/filter/ServletFilterTest.groovy deleted file mode 100644 index 7ebe6f98bdda..000000000000 --- a/instrumentation/spring/spring-webmvc/spring-webmvc-3.1/javaagent/src/test/groovy/test/filter/ServletFilterTest.groovy +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package test.filter - -import filter.AbstractServletFilterTest -import test.boot.SecurityConfig - -class ServletFilterTest extends AbstractServletFilterTest { - - Class securityConfigClass() { - SecurityConfig - } - - Class filterConfigClass() { - ServletFilterConfig - } -} diff --git a/instrumentation/spring/spring-webmvc/spring-webmvc-3.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webmvc/v3_1/boot/SecurityConfig.java b/instrumentation/spring/spring-webmvc/spring-webmvc-3.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webmvc/v3_1/boot/SecurityConfig.java new file mode 100644 index 000000000000..418e71794748 --- /dev/null +++ b/instrumentation/spring/spring-webmvc/spring-webmvc-3.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webmvc/v3_1/boot/SecurityConfig.java @@ -0,0 +1,67 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.webmvc.v3_1.boot; + +import io.opentelemetry.instrumentation.spring.webmvc.boot.SavingAuthenticationProvider; +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 +public 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 { + + @Override + protected void configure(HttpSecurity http) throws Exception { + http.csrf() + .disable() + .antMatcher("/basicsecured/**") + .authorizeRequests() + .antMatchers("/basicsecured/**") + .authenticated() + .and() + .httpBasic() + .and() + .authenticationProvider( + getApplicationContext().getBean(SavingAuthenticationProvider.class)); + } + } + + /** 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( + getApplicationContext().getBean(SavingAuthenticationProvider.class)); + } + } +} diff --git a/instrumentation/spring/spring-webmvc/spring-webmvc-3.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webmvc/v3_1/boot/SpringBootBasedTest.java b/instrumentation/spring/spring-webmvc/spring-webmvc-3.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webmvc/v3_1/boot/SpringBootBasedTest.java new file mode 100644 index 000000000000..2de935fc3553 --- /dev/null +++ b/instrumentation/spring/spring-webmvc/spring-webmvc-3.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webmvc/v3_1/boot/SpringBootBasedTest.java @@ -0,0 +1,62 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.webmvc.v3_1.boot; + +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.EXCEPTION; + +import com.google.common.collect.ImmutableMap; +import io.opentelemetry.instrumentation.spring.webmvc.boot.AbstractSpringBootBasedTest; +import io.opentelemetry.instrumentation.spring.webmvc.boot.AppConfig; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.http.HttpServerInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.http.HttpServerTestOptions; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.springframework.boot.SpringApplication; +import org.springframework.context.ConfigurableApplicationContext; + +class SpringBootBasedTest extends AbstractSpringBootBasedTest { + + @RegisterExtension + private static final InstrumentationExtension testing = + HttpServerInstrumentationExtension.forAgent(); + + private ConfigurableApplicationContext context; + + @Override + protected ConfigurableApplicationContext context() { + return context; + } + + @Override + protected ConfigurableApplicationContext setupServer() { + SpringApplication app = new SpringApplication(AppConfig.class, securityConfigClass()); + app.setDefaultProperties( + ImmutableMap.of( + "server.port", + port, + "server.context-path", + getContextPath(), + "server.servlet.contextPath", + getContextPath(), + "server.error.include-message", + "always")); + context = app.run(); + return context; + } + + @Override + public Class securityConfigClass() { + return SecurityConfig.class; + } + + @Override + protected void configure(HttpServerTestOptions options) { + super.configure(options); + options.setResponseCodeOnNonStandardHttpMethod( + Boolean.getBoolean("testLatestDeps") ? 500 : 200); + options.setExpectedException(new RuntimeException(EXCEPTION.getBody())); + } +} diff --git a/instrumentation/spring/spring-webmvc/spring-webmvc-3.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webmvc/v3_1/filter/ServletFilterConfig.java b/instrumentation/spring/spring-webmvc/spring-webmvc-3.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webmvc/v3_1/filter/ServletFilterConfig.java new file mode 100644 index 000000000000..a77d1516a40b --- /dev/null +++ b/instrumentation/spring/spring-webmvc/spring-webmvc-3.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webmvc/v3_1/filter/ServletFilterConfig.java @@ -0,0 +1,85 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.webmvc.v3_1.filter; + +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.INDEXED_CHILD; + +import io.opentelemetry.instrumentation.test.base.HttpServerTest; +import io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint; +import java.io.IOException; +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.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +class ServletFilterConfig { + + @Bean + Filter servletFilter() { + return new Filter() { + + @Override + public void init(FilterConfig filterConfig) throws ServletException {} + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + HttpServletRequest req = (HttpServletRequest) request; + HttpServletResponse resp = (HttpServletResponse) response; + ServerEndpoint endpoint = ServerEndpoint.forPath(req.getServletPath()); + HttpServerTest.controller( + endpoint, + () -> { + resp.setContentType("text/plain"); + switch (endpoint.name()) { + case "SUCCESS": + resp.setStatus(endpoint.getStatus()); + resp.getWriter().print(endpoint.getBody()); + break; + case "QUERY_PARAM": + resp.setStatus(endpoint.getStatus()); + resp.getWriter().print(req.getQueryString()); + break; + case "PATH_PARAM": + resp.setStatus(endpoint.getStatus()); + resp.getWriter().print(endpoint.getBody()); + break; + case "REDIRECT": + resp.sendRedirect(endpoint.getBody()); + break; + case "CAPTURE_HEADERS": + resp.setHeader("X-Test-Response", req.getHeader("X-Test-Request")); + resp.setStatus(endpoint.getStatus()); + resp.getWriter().print(endpoint.getBody()); + break; + case "ERROR": + resp.sendError(endpoint.getStatus(), endpoint.getBody()); + break; + case "EXCEPTION": + throw new Exception(endpoint.getBody()); + case "INDEXED_CHILD": + INDEXED_CHILD.collectSpanAttributes(req::getParameter); + resp.getWriter().print(endpoint.getBody()); + break; + default: + chain.doFilter(request, response); + } + return null; + }); + } + + @Override + public void destroy() {} + }; + } +} diff --git a/instrumentation/spring/spring-webmvc/spring-webmvc-3.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webmvc/v3_1/filter/ServletFilterTest.java b/instrumentation/spring/spring-webmvc/spring-webmvc-3.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webmvc/v3_1/filter/ServletFilterTest.java new file mode 100644 index 000000000000..6f46de9b8abd --- /dev/null +++ b/instrumentation/spring/spring-webmvc/spring-webmvc-3.1/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webmvc/v3_1/filter/ServletFilterTest.java @@ -0,0 +1,50 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.webmvc.v3_1.filter; + +import com.google.common.collect.ImmutableMap; +import io.opentelemetry.instrumentation.spring.webmvc.filter.AbstractServletFilterTest; +import io.opentelemetry.instrumentation.spring.webmvc.filter.FilteredAppConfig; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.http.HttpServerInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.http.HttpServerTestOptions; +import io.opentelemetry.javaagent.instrumentation.spring.webmvc.v3_1.boot.SecurityConfig; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.springframework.boot.SpringApplication; +import org.springframework.context.ConfigurableApplicationContext; + +class ServletFilterTest extends AbstractServletFilterTest { + + @RegisterExtension + private static final InstrumentationExtension testing = + HttpServerInstrumentationExtension.forAgent(); + + @Override + protected Class securityConfigClass() { + return SecurityConfig.class; + } + + @Override + protected Class filterConfigClass() { + return ServletFilterConfig.class; + } + + @Override + protected ConfigurableApplicationContext setupServer() { + SpringApplication app = + new SpringApplication(FilteredAppConfig.class, securityConfigClass(), filterConfigClass()); + app.setDefaultProperties( + ImmutableMap.of("server.port", port, "server.error.include-message", "always")); + return app.run(); + } + + @Override + protected void configure(HttpServerTestOptions options) { + super.configure(options); + options.setResponseCodeOnNonStandardHttpMethod( + Boolean.getBoolean("testLatestDeps") ? 500 : 200); + } +} diff --git a/instrumentation/spring/spring-webmvc/spring-webmvc-3.1/wildfly-testing/build.gradle.kts b/instrumentation/spring/spring-webmvc/spring-webmvc-3.1/wildfly-testing/build.gradle.kts index e8b1f0a25ffe..34a27ed93d95 100644 --- a/instrumentation/spring/spring-webmvc/spring-webmvc-3.1/wildfly-testing/build.gradle.kts +++ b/instrumentation/spring/spring-webmvc/spring-webmvc-3.1/wildfly-testing/build.gradle.kts @@ -42,14 +42,16 @@ tasks { // that breaks deploy on embedded wildfly // create a copy of logback-classic jar that does not have this file val modifyLogbackJar by registering(Jar::class) { - destinationDirectory.set(file("$buildDir/tmp")) + destinationDirectory.set(layout.buildDirectory.dir("tmp")) archiveFileName.set("logback-classic-modified.jar") exclude("/META-INF/services/javax.servlet.ServletContainerInitializer") doFirst { configurations.configureEach { if (name.lowercase().endsWith("testruntimeclasspath")) { val logbackJar = find { it.name.contains("logback-classic") } - from(zipTree(logbackJar)) + logbackJar?.let { + from(zipTree(logbackJar)) + } } } } @@ -57,7 +59,7 @@ tasks { val copyDependencies by registering(Copy::class) { // test looks for spring jars that are bundled inside deployed application from this directory - from(appLibrary).into("$buildDir/app-libs") + from(appLibrary).into(layout.buildDirectory.dir("app-libs")) } test { @@ -72,13 +74,14 @@ tasks { jvmArgs("--add-modules=java.se") // add offset to default port values jvmArgs("-Djboss.socket.binding.port-offset=300") + jvmArgs("-Dotel.instrumentation.common.experimental.controller-telemetry.enabled=true") // remove logback-classic from classpath classpath = classpath.filter { !it.absolutePath.contains("logback-classic") } // add modified copy of logback-classic to classpath - classpath = classpath.plus(files("$buildDir/tmp/logback-classic-modified.jar")) + classpath = classpath.plus(files(layout.buildDirectory.file("tmp/logback-classic-modified.jar"))) } } } diff --git a/instrumentation/spring/spring-webmvc/spring-webmvc-5.3/library/README.md b/instrumentation/spring/spring-webmvc/spring-webmvc-5.3/library/README.md index 815940802285..5be13f86ac0e 100644 --- a/instrumentation/spring/spring-webmvc/spring-webmvc-5.3/library/README.md +++ b/instrumentation/spring/spring-webmvc/spring-webmvc-5.3/library/README.md @@ -25,7 +25,7 @@ For Maven add the following to your `pom.xml`: - + io.opentelemetry opentelemetry-exporter-logging @@ -51,7 +51,7 @@ For Gradle add the following to your dependencies: implementation("io.opentelemetry.instrumentation:opentelemetry-spring-webmvc-5.3:OPENTELEMETRY_VERSION") // OpenTelemetry exporter -// replace this default exporter with your OpenTelemetry exporter (ex. otlp/zipkin/jaeger/..) +// replace this default exporter with your OpenTelemetry exporter (ex. otlp/zipkin/..) implementation("io.opentelemetry:opentelemetry-exporter-logging:OPENTELEMETRY_VERSION") // required to instrument Spring WebMVC diff --git a/instrumentation/spring/spring-webmvc/spring-webmvc-5.3/library/build.gradle.kts b/instrumentation/spring/spring-webmvc/spring-webmvc-5.3/library/build.gradle.kts index 920cebcbeab5..be39fee5a4d2 100644 --- a/instrumentation/spring/spring-webmvc/spring-webmvc-5.3/library/build.gradle.kts +++ b/instrumentation/spring/spring-webmvc/spring-webmvc-5.3/library/build.gradle.kts @@ -2,8 +2,7 @@ plugins { id("otel.library-instrumentation") } -val versions: Map by project -val springBootVersion = versions["org.springframework.boot"] +val springBootVersion = "2.6.15" dependencies { compileOnly("org.springframework:spring-webmvc:5.3.0") diff --git a/instrumentation/spring/spring-webmvc/spring-webmvc-5.3/library/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/v5_3/SpringWebMvcHttpAttributesGetter.java b/instrumentation/spring/spring-webmvc/spring-webmvc-5.3/library/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/v5_3/SpringWebMvcHttpAttributesGetter.java index cb72b288ed91..b39c5cb69f63 100644 --- a/instrumentation/spring/spring-webmvc/spring-webmvc-5.3/library/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/v5_3/SpringWebMvcHttpAttributesGetter.java +++ b/instrumentation/spring/spring-webmvc/spring-webmvc-5.3/library/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/v5_3/SpringWebMvcHttpAttributesGetter.java @@ -5,7 +5,7 @@ package io.opentelemetry.instrumentation.spring.webmvc.v5_3; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpServerAttributesGetter; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerAttributesGetter; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -80,4 +80,52 @@ public String getUrlPath(HttpServletRequest request) { public String getUrlQuery(HttpServletRequest request) { return request.getQueryString(); } + + @Nullable + @Override + public String getNetworkProtocolName( + HttpServletRequest request, @Nullable HttpServletResponse response) { + String protocol = request.getProtocol(); + if (protocol != null && protocol.startsWith("HTTP/")) { + return "http"; + } + return null; + } + + @Nullable + @Override + public String getNetworkProtocolVersion( + HttpServletRequest request, @Nullable HttpServletResponse response) { + String protocol = request.getProtocol(); + if (protocol != null && protocol.startsWith("HTTP/")) { + return protocol.substring("HTTP/".length()); + } + return null; + } + + @Override + @Nullable + public String getNetworkPeerAddress( + HttpServletRequest request, @Nullable HttpServletResponse response) { + return request.getRemoteAddr(); + } + + @Override + public Integer getNetworkPeerPort( + HttpServletRequest request, @Nullable HttpServletResponse response) { + return request.getRemotePort(); + } + + @Nullable + @Override + public String getNetworkLocalAddress( + HttpServletRequest request, @Nullable HttpServletResponse response) { + return request.getLocalAddr(); + } + + @Override + public Integer getNetworkLocalPort( + HttpServletRequest request, @Nullable HttpServletResponse respo) { + return request.getLocalPort(); + } } diff --git a/instrumentation/spring/spring-webmvc/spring-webmvc-5.3/library/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/v5_3/SpringWebMvcNetAttributesGetter.java b/instrumentation/spring/spring-webmvc/spring-webmvc-5.3/library/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/v5_3/SpringWebMvcNetAttributesGetter.java deleted file mode 100644 index 207be21c7360..000000000000 --- a/instrumentation/spring/spring-webmvc/spring-webmvc-5.3/library/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/v5_3/SpringWebMvcNetAttributesGetter.java +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.spring.webmvc.v5_3; - -import io.opentelemetry.instrumentation.api.instrumenter.net.NetServerAttributesGetter; -import javax.annotation.Nullable; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -enum SpringWebMvcNetAttributesGetter - implements NetServerAttributesGetter { - INSTANCE; - - @Nullable - @Override - public String getNetworkProtocolName( - HttpServletRequest request, @Nullable HttpServletResponse response) { - String protocol = request.getProtocol(); - if (protocol != null && protocol.startsWith("HTTP/")) { - return "http"; - } - return null; - } - - @Nullable - @Override - public String getNetworkProtocolVersion( - HttpServletRequest request, @Nullable HttpServletResponse response) { - String protocol = request.getProtocol(); - if (protocol != null && protocol.startsWith("HTTP/")) { - return protocol.substring("HTTP/".length()); - } - return null; - } - - @Nullable - @Override - public String getServerAddress(HttpServletRequest request) { - return request.getServerName(); - } - - @Override - public Integer getServerPort(HttpServletRequest request) { - return request.getServerPort(); - } - - @Override - @Nullable - public String getClientSocketAddress( - HttpServletRequest request, @Nullable HttpServletResponse response) { - return request.getRemoteAddr(); - } - - @Override - public Integer getClientSocketPort( - HttpServletRequest request, @Nullable HttpServletResponse response) { - return request.getRemotePort(); - } - - @Nullable - @Override - public String getServerSocketAddress( - HttpServletRequest request, @Nullable HttpServletResponse response) { - return request.getLocalAddr(); - } - - @Override - public Integer getServerSocketPort( - HttpServletRequest request, @Nullable HttpServletResponse respo) { - return request.getLocalPort(); - } -} diff --git a/instrumentation/spring/spring-webmvc/spring-webmvc-5.3/library/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/v5_3/SpringWebMvcTelemetryBuilder.java b/instrumentation/spring/spring-webmvc/spring-webmvc-5.3/library/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/v5_3/SpringWebMvcTelemetryBuilder.java index 89b24520eca7..4acb087b72b5 100644 --- a/instrumentation/spring/spring-webmvc/spring-webmvc-5.3/library/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/v5_3/SpringWebMvcTelemetryBuilder.java +++ b/instrumentation/spring/spring-webmvc/spring-webmvc-5.3/library/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/v5_3/SpringWebMvcTelemetryBuilder.java @@ -7,16 +7,14 @@ import com.google.errorprone.annotations.CanIgnoreReturnValue; import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.instrumentation.api.incubator.builder.internal.DefaultHttpServerInstrumenterBuilder; import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteHolder; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpServerAttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpServerAttributesExtractorBuilder; -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 io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerAttributesExtractorBuilder; +import io.opentelemetry.instrumentation.spring.webmvc.v5_3.internal.SpringMvcBuilderUtil; import java.util.List; +import java.util.Set; +import java.util.function.Function; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -25,16 +23,18 @@ public final class SpringWebMvcTelemetryBuilder { private static final String INSTRUMENTATION_NAME = "io.opentelemetry.spring-webmvc-5.3"; - private final OpenTelemetry openTelemetry; - private final List> - additionalExtractors = new ArrayList<>(); - private final HttpServerAttributesExtractorBuilder - httpAttributesExtractorBuilder = - HttpServerAttributesExtractor.builder( - SpringWebMvcHttpAttributesGetter.INSTANCE, SpringWebMvcNetAttributesGetter.INSTANCE); + private final DefaultHttpServerInstrumenterBuilder + builder; + + static { + SpringMvcBuilderUtil.setBuilderExtractor(SpringWebMvcTelemetryBuilder::getBuilder); + } SpringWebMvcTelemetryBuilder(OpenTelemetry openTelemetry) { - this.openTelemetry = openTelemetry; + builder = + new DefaultHttpServerInstrumenterBuilder<>( + INSTRUMENTATION_NAME, openTelemetry, SpringWebMvcHttpAttributesGetter.INSTANCE) + .setHeaderGetter(JavaxHttpServletRequestGetter.INSTANCE); } /** @@ -44,7 +44,7 @@ public final class SpringWebMvcTelemetryBuilder { @CanIgnoreReturnValue public SpringWebMvcTelemetryBuilder addAttributesExtractor( AttributesExtractor attributesExtractor) { - additionalExtractors.add(attributesExtractor); + builder.addAttributesExtractor(attributesExtractor); return this; } @@ -55,7 +55,7 @@ public SpringWebMvcTelemetryBuilder addAttributesExtractor( */ @CanIgnoreReturnValue public SpringWebMvcTelemetryBuilder setCapturedRequestHeaders(List requestHeaders) { - httpAttributesExtractorBuilder.setCapturedRequestHeaders(requestHeaders); + builder.setCapturedRequestHeaders(requestHeaders); return this; } @@ -66,7 +66,50 @@ public SpringWebMvcTelemetryBuilder setCapturedRequestHeaders(List reque */ @CanIgnoreReturnValue public SpringWebMvcTelemetryBuilder setCapturedResponseHeaders(List responseHeaders) { - httpAttributesExtractorBuilder.setCapturedResponseHeaders(responseHeaders); + builder.setCapturedResponseHeaders(responseHeaders); + return this; + } + + /** Sets custom {@link SpanNameExtractor} via transform function. */ + @CanIgnoreReturnValue + public SpringWebMvcTelemetryBuilder setSpanNameExtractor( + Function< + SpanNameExtractor, + ? extends SpanNameExtractor> + spanNameExtractor) { + builder.setSpanNameExtractor(spanNameExtractor); + return this; + } + + /** + * Configures the instrumentation to recognize an alternative set of HTTP request methods. + * + *

    By default, this instrumentation defines "known" methods as the ones listed in RFC9110 and the PATCH + * method defined in RFC5789. + * + *

    Note: calling this method overrides the default known method sets completely; it does + * not supplement it. + * + * @param knownMethods A set of recognized HTTP request methods. + * @see HttpServerAttributesExtractorBuilder#setKnownMethods(Set) + */ + @CanIgnoreReturnValue + public SpringWebMvcTelemetryBuilder setKnownMethods(Set knownMethods) { + builder.setKnownMethods(knownMethods); + return this; + } + + /** + * Configures the instrumentation to emit experimental HTTP server metrics. + * + * @param emitExperimentalHttpServerMetrics {@code true} if the experimental HTTP server metrics + * are to be emitted. + */ + @CanIgnoreReturnValue + public SpringWebMvcTelemetryBuilder setEmitExperimentalHttpServerMetrics( + boolean emitExperimentalHttpServerMetrics) { + builder.setEmitExperimentalHttpServerMetrics(emitExperimentalHttpServerMetrics); return this; } @@ -75,21 +118,11 @@ public SpringWebMvcTelemetryBuilder setCapturedResponseHeaders(List resp * SpringWebMvcTelemetryBuilder}. */ public SpringWebMvcTelemetry build() { - SpringWebMvcHttpAttributesGetter httpAttributesGetter = - SpringWebMvcHttpAttributesGetter.INSTANCE; - - Instrumenter instrumenter = - Instrumenter.builder( - openTelemetry, - INSTRUMENTATION_NAME, - HttpSpanNameExtractor.create(httpAttributesGetter)) - .setSpanStatusExtractor(HttpSpanStatusExtractor.create(httpAttributesGetter)) - .addAttributesExtractor(httpAttributesExtractorBuilder.build()) - .addAttributesExtractors(additionalExtractors) - .addOperationMetrics(HttpServerMetrics.get()) - .addContextCustomizer(HttpRouteHolder.create(httpAttributesGetter)) - .buildServerInstrumenter(JavaxHttpServletRequestGetter.INSTANCE); + return new SpringWebMvcTelemetry(builder.build()); + } - return new SpringWebMvcTelemetry(instrumenter); + public DefaultHttpServerInstrumenterBuilder + getBuilder() { + return builder; } } diff --git a/instrumentation/spring/spring-webmvc/spring-webmvc-5.3/library/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/v5_3/WebMvcTelemetryProducingFilter.java b/instrumentation/spring/spring-webmvc/spring-webmvc-5.3/library/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/v5_3/WebMvcTelemetryProducingFilter.java index 8be211250556..1e7c62690750 100644 --- a/instrumentation/spring/spring-webmvc/spring-webmvc-5.3/library/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/v5_3/WebMvcTelemetryProducingFilter.java +++ b/instrumentation/spring/spring-webmvc/spring-webmvc-5.3/library/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/v5_3/WebMvcTelemetryProducingFilter.java @@ -5,17 +5,24 @@ package io.opentelemetry.instrumentation.spring.webmvc.v5_3; -import static io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteSource.CONTROLLER; +import static io.opentelemetry.instrumentation.api.semconv.http.HttpServerRouteSource.CONTROLLER; import static java.util.Objects.requireNonNull; import io.opentelemetry.context.Context; import io.opentelemetry.context.Scope; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteHolder; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRoute; import java.io.IOException; +import java.util.concurrent.atomic.AtomicBoolean; +import javax.servlet.AsyncContext; +import javax.servlet.AsyncEvent; +import javax.servlet.AsyncListener; import javax.servlet.FilterChain; import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletRequestWrapper; import javax.servlet.http.HttpServletResponse; import org.springframework.core.Ordered; import org.springframework.web.filter.OncePerRequestFilter; @@ -53,18 +60,21 @@ public void doFilterInternal( } Context context = instrumenter.start(parentContext, request); + AsyncAwareHttpServletRequest asyncAwareRequest = + new AsyncAwareHttpServletRequest(request, response, context); Throwable error = null; try (Scope ignored = context.makeCurrent()) { - filterChain.doFilter(request, response); + filterChain.doFilter(asyncAwareRequest, response); } catch (Throwable t) { error = t; throw t; } finally { if (httpRouteSupport.hasMappings()) { - HttpRouteHolder.updateHttpRoute( - context, CONTROLLER, httpRouteSupport::getHttpRoute, request); + HttpServerRoute.update(context, CONTROLLER, httpRouteSupport::getHttpRoute, request); + } + if (error != null || asyncAwareRequest.isNotAsync()) { + instrumenter.end(context, request, response, error); } - instrumenter.end(context, request, response, error); } } @@ -76,4 +86,88 @@ public int getOrder() { // Run after all HIGHEST_PRECEDENCE items return Ordered.HIGHEST_PRECEDENCE + 1; } + + private class AsyncAwareHttpServletRequest extends HttpServletRequestWrapper { + private final HttpServletRequest request; + private final HttpServletResponse response; + private final Context context; + private final AtomicBoolean listenerAttached = new AtomicBoolean(); + + AsyncAwareHttpServletRequest( + HttpServletRequest request, HttpServletResponse response, Context context) { + super(request); + this.request = request; + this.response = response; + this.context = context; + } + + @Override + public AsyncContext startAsync() { + AsyncContext asyncContext = super.startAsync(); + attachListener(asyncContext); + return asyncContext; + } + + @Override + public AsyncContext startAsync(ServletRequest servletRequest, ServletResponse servletResponse) { + AsyncContext asyncContext = super.startAsync(servletRequest, servletResponse); + attachListener(asyncContext); + return asyncContext; + } + + private void attachListener(AsyncContext asyncContext) { + if (!listenerAttached.compareAndSet(false, true)) { + return; + } + + asyncContext.addListener( + new AsyncRequestCompletionListener(request, response, context), request, response); + } + + boolean isNotAsync() { + return !listenerAttached.get(); + } + } + + private class AsyncRequestCompletionListener implements AsyncListener { + private final HttpServletRequest request; + private final HttpServletResponse response; + private final Context context; + private final AtomicBoolean responseHandled = new AtomicBoolean(); + + AsyncRequestCompletionListener( + HttpServletRequest request, HttpServletResponse response, Context context) { + this.request = request; + this.response = response; + this.context = context; + } + + @Override + public void onComplete(AsyncEvent asyncEvent) { + if (responseHandled.compareAndSet(false, true)) { + instrumenter.end(context, request, response, null); + } + } + + @Override + public void onTimeout(AsyncEvent asyncEvent) { + if (responseHandled.compareAndSet(false, true)) { + instrumenter.end(context, request, response, null); + } + } + + @Override + public void onError(AsyncEvent asyncEvent) { + if (responseHandled.compareAndSet(false, true)) { + instrumenter.end(context, request, response, asyncEvent.getThrowable()); + } + } + + @Override + public void onStartAsync(AsyncEvent asyncEvent) { + asyncEvent + .getAsyncContext() + .addListener(this, asyncEvent.getSuppliedRequest(), asyncEvent.getSuppliedResponse()); + } + } } diff --git a/instrumentation/spring/spring-webmvc/spring-webmvc-5.3/library/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/v5_3/internal/SpringMvcBuilderUtil.java b/instrumentation/spring/spring-webmvc/spring-webmvc-5.3/library/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/v5_3/internal/SpringMvcBuilderUtil.java new file mode 100644 index 000000000000..74ac99e33d8a --- /dev/null +++ b/instrumentation/spring/spring-webmvc/spring-webmvc-5.3/library/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/v5_3/internal/SpringMvcBuilderUtil.java @@ -0,0 +1,41 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.webmvc.v5_3.internal; + +import io.opentelemetry.instrumentation.api.incubator.builder.internal.DefaultHttpServerInstrumenterBuilder; +import io.opentelemetry.instrumentation.spring.webmvc.v5_3.SpringWebMvcTelemetryBuilder; +import java.util.function.Function; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time. + */ +public final class SpringMvcBuilderUtil { + private SpringMvcBuilderUtil() {} + + // allows access to the private field for the spring starter + private static Function< + SpringWebMvcTelemetryBuilder, + DefaultHttpServerInstrumenterBuilder> + builderExtractor; + + public static Function< + SpringWebMvcTelemetryBuilder, + DefaultHttpServerInstrumenterBuilder> + getBuilderExtractor() { + return builderExtractor; + } + + public static void setBuilderExtractor( + Function< + SpringWebMvcTelemetryBuilder, + DefaultHttpServerInstrumenterBuilder> + builderExtractor) { + SpringMvcBuilderUtil.builderExtractor = builderExtractor; + } +} diff --git a/instrumentation/spring/spring-webmvc/spring-webmvc-5.3/library/src/test/java/io/opentelemetry/instrumentation/spring/webmvc/v5_3/TestWebSpringBootApp.java b/instrumentation/spring/spring-webmvc/spring-webmvc-5.3/library/src/test/java/io/opentelemetry/instrumentation/spring/webmvc/v5_3/TestWebSpringBootApp.java index d0175f9d0c5d..cf5d15abc869 100644 --- a/instrumentation/spring/spring-webmvc/spring-webmvc-5.3/library/src/test/java/io/opentelemetry/instrumentation/spring/webmvc/v5_3/TestWebSpringBootApp.java +++ b/instrumentation/spring/spring-webmvc/spring-webmvc-5.3/library/src/test/java/io/opentelemetry/instrumentation/spring/webmvc/v5_3/TestWebSpringBootApp.java @@ -17,8 +17,12 @@ import static java.util.Collections.singletonList; import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpServerTest; +import io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint; import java.util.Properties; +import java.util.concurrent.CompletableFuture; import javax.servlet.Filter; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @@ -37,6 +41,7 @@ @SpringBootApplication class TestWebSpringBootApp { + static final ServerEndpoint ASYNC_ENDPOINT = new ServerEndpoint("ASYNC", "async", 200, "success"); static ConfigurableApplicationContext start(int port, String contextPath) { Properties props = new Properties(); @@ -122,6 +127,26 @@ String indexed_child(@RequestParam("id") String id) { }); } + @RequestMapping("/async") + @ResponseBody + CompletableFuture async() { + Context context = Context.current(); + return CompletableFuture.supplyAsync( + () -> { + // Sleep a bit so that the future completes after the controller method. This helps to + // verify whether request ends after the future has completed not after when the + // controller method has completed. + try { + Thread.sleep(100); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + try (Scope ignored = context.makeCurrent()) { + return controller(ASYNC_ENDPOINT, ASYNC_ENDPOINT::getBody); + } + }); + } + @ExceptionHandler ResponseEntity handleException(Throwable throwable) { return new ResponseEntity<>(throwable.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR); diff --git a/instrumentation/spring/spring-webmvc/spring-webmvc-5.3/library/src/test/java/io/opentelemetry/instrumentation/spring/webmvc/v5_3/WebMvcHttpServerTest.java b/instrumentation/spring/spring-webmvc/spring-webmvc-5.3/library/src/test/java/io/opentelemetry/instrumentation/spring/webmvc/v5_3/WebMvcHttpServerTest.java index 9283a640d125..d0a69337c7de 100644 --- a/instrumentation/spring/spring-webmvc/spring-webmvc-5.3/library/src/test/java/io/opentelemetry/instrumentation/spring/webmvc/v5_3/WebMvcHttpServerTest.java +++ b/instrumentation/spring/spring-webmvc/spring-webmvc-5.3/library/src/test/java/io/opentelemetry/instrumentation/spring/webmvc/v5_3/WebMvcHttpServerTest.java @@ -5,11 +5,17 @@ package io.opentelemetry.instrumentation.spring.webmvc.v5_3; +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.instrumentation.api.internal.HttpConstants; import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpServerTest; import io.opentelemetry.instrumentation.testing.junit.http.HttpServerInstrumentationExtension; import io.opentelemetry.instrumentation.testing.junit.http.HttpServerTestOptions; import io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint; +import io.opentelemetry.testing.internal.armeria.common.AggregatedHttpRequest; +import io.opentelemetry.testing.internal.armeria.common.AggregatedHttpResponse; +import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.springframework.context.ConfigurableApplicationContext; @@ -38,11 +44,28 @@ protected void configure(HttpServerTestOptions options) { options.setTestException(false); options.setExpectedHttpRoute( - endpoint -> { + (endpoint, method) -> { if (endpoint == ServerEndpoint.PATH_PARAM) { return CONTEXT_PATH + "/path/{id}/param"; } - return expectedHttpRoute(endpoint); + if (HttpConstants._OTHER.equals(method)) { + return CONTEXT_PATH + endpoint.getPath(); + } + return expectedHttpRoute(endpoint, method); }); } + + @Test + void asyncRequest() { + ServerEndpoint endpoint = TestWebSpringBootApp.ASYNC_ENDPOINT; + String method = "GET"; + AggregatedHttpRequest request = request(endpoint, method); + AggregatedHttpResponse response = client.execute(request).aggregate().join(); + + assertThat(response.status().code()).isEqualTo(endpoint.getStatus()); + assertThat(response.contentUtf8()).isEqualTo(endpoint.getBody()); + + String spanId = assertResponseHasCustomizedHeaders(response, endpoint, null); + assertTheTraces(1, null, null, spanId, method, endpoint); + } } diff --git a/instrumentation/spring/spring-webmvc/spring-webmvc-6.0/javaagent/build.gradle.kts b/instrumentation/spring/spring-webmvc/spring-webmvc-6.0/javaagent/build.gradle.kts index 9c4425b96814..312b38583aa2 100644 --- a/instrumentation/spring/spring-webmvc/spring-webmvc-6.0/javaagent/build.gradle.kts +++ b/instrumentation/spring/spring-webmvc/spring-webmvc-6.0/javaagent/build.gradle.kts @@ -50,4 +50,8 @@ tasks.withType().configureEach { // required on jdk17 jvmArgs("--add-opens=java.base/java.lang=ALL-UNNAMED") jvmArgs("-XX:+IgnoreUnrecognizedVMOptions") + jvmArgs("-Dotel.instrumentation.common.experimental.controller-telemetry.enabled=true") + jvmArgs("-Dotel.instrumentation.common.experimental.view-telemetry.enabled=true") + + systemProperty("testLatestDeps", findProperty("testLatestDeps") as Boolean) } diff --git a/instrumentation/spring/spring-webmvc/spring-webmvc-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/webmvc/v6_0/HandlerAdapterInstrumentation.java b/instrumentation/spring/spring-webmvc/spring-webmvc-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/webmvc/v6_0/HandlerAdapterInstrumentation.java index a05e5d16268d..eb0ac052d514 100644 --- a/instrumentation/spring/spring-webmvc/spring-webmvc-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/webmvc/v6_0/HandlerAdapterInstrumentation.java +++ b/instrumentation/spring/spring-webmvc/spring-webmvc-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/webmvc/v6_0/HandlerAdapterInstrumentation.java @@ -5,7 +5,7 @@ package io.opentelemetry.javaagent.instrumentation.spring.webmvc.v6_0; -import static io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteSource.CONTROLLER; +import static io.opentelemetry.instrumentation.api.semconv.http.HttpServerRouteSource.CONTROLLER; import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.hasClassesNamed; import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.implementsInterface; import static io.opentelemetry.javaagent.instrumentation.spring.webmvc.v6_0.SpringWebMvcSingletons.handlerInstrumenter; @@ -18,7 +18,7 @@ import io.opentelemetry.context.Context; import io.opentelemetry.context.Scope; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteHolder; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRoute; import io.opentelemetry.javaagent.bootstrap.Java8BytecodeBridge; import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; @@ -74,7 +74,7 @@ public static void nameResourceAndStartSpan( } // Name the parent span based on the matching pattern - HttpRouteHolder.updateHttpRoute( + HttpServerRoute.update( parentContext, CONTROLLER, SpringWebMvcServerSpanNaming.SERVER_SPAN_NAME, request); if (!handlerInstrumenter().shouldStart(parentContext, handler)) { diff --git a/instrumentation/spring/spring-webmvc/spring-webmvc-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/webmvc/v6_0/SpringWebMvcInstrumentationModule.java b/instrumentation/spring/spring-webmvc/spring-webmvc-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/webmvc/v6_0/SpringWebMvcInstrumentationModule.java index fe2d889389a3..a3d7cef051c4 100644 --- a/instrumentation/spring/spring-webmvc/spring-webmvc-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/webmvc/v6_0/SpringWebMvcInstrumentationModule.java +++ b/instrumentation/spring/spring-webmvc/spring-webmvc-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/webmvc/v6_0/SpringWebMvcInstrumentationModule.java @@ -32,6 +32,11 @@ public boolean isHelperClass(String className) { "org.springframework.web.servlet.v6_0.OpenTelemetryHandlerMappingFilter"); } + @Override + public boolean isIndyModule() { + return false; + } + @Override public List typeInstrumentations() { return asList(new DispatcherServletInstrumentation(), new HandlerAdapterInstrumentation()); diff --git a/instrumentation/spring/spring-webmvc/spring-webmvc-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/webmvc/v6_0/SpringWebMvcServerSpanNaming.java b/instrumentation/spring/spring-webmvc/spring-webmvc-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/webmvc/v6_0/SpringWebMvcServerSpanNaming.java index 9650369f22b6..d510f674b3ef 100644 --- a/instrumentation/spring/spring-webmvc/spring-webmvc-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/webmvc/v6_0/SpringWebMvcServerSpanNaming.java +++ b/instrumentation/spring/spring-webmvc/spring-webmvc-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/webmvc/v6_0/SpringWebMvcServerSpanNaming.java @@ -5,14 +5,14 @@ package io.opentelemetry.javaagent.instrumentation.spring.webmvc.v6_0; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteGetter; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRouteGetter; import io.opentelemetry.javaagent.bootstrap.servlet.ServletContextPath; import jakarta.servlet.http.HttpServletRequest; import org.springframework.web.servlet.HandlerMapping; public class SpringWebMvcServerSpanNaming { - public static final HttpRouteGetter SERVER_SPAN_NAME = + public static final HttpServerRouteGetter SERVER_SPAN_NAME = (context, request) -> { Object bestMatchingPattern = request.getAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE); diff --git a/instrumentation/spring/spring-webmvc/spring-webmvc-6.0/javaagent/src/main/java/org/springframework/web/servlet/v6_0/OpenTelemetryHandlerMappingFilter.java b/instrumentation/spring/spring-webmvc/spring-webmvc-6.0/javaagent/src/main/java/org/springframework/web/servlet/v6_0/OpenTelemetryHandlerMappingFilter.java index c8da7124b44f..32a8782be2da 100644 --- a/instrumentation/spring/spring-webmvc/spring-webmvc-6.0/javaagent/src/main/java/org/springframework/web/servlet/v6_0/OpenTelemetryHandlerMappingFilter.java +++ b/instrumentation/spring/spring-webmvc/spring-webmvc-6.0/javaagent/src/main/java/org/springframework/web/servlet/v6_0/OpenTelemetryHandlerMappingFilter.java @@ -5,11 +5,11 @@ package org.springframework.web.servlet.v6_0; -import static io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteSource.CONTROLLER; +import static io.opentelemetry.instrumentation.api.semconv.http.HttpServerRouteSource.CONTROLLER; import io.opentelemetry.context.Context; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteGetter; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteHolder; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRoute; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRouteGetter; import io.opentelemetry.javaagent.instrumentation.spring.webmvc.v6_0.SpringWebMvcServerSpanNaming; import jakarta.servlet.Filter; import jakarta.servlet.FilterChain; @@ -18,14 +18,16 @@ import jakarta.servlet.ServletRequest; import jakarta.servlet.ServletResponse; import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletRequestWrapper; import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Objects; import javax.annotation.Nullable; import org.springframework.core.Ordered; -import org.springframework.http.server.RequestPath; import org.springframework.web.servlet.HandlerExecutionChain; import org.springframework.web.servlet.HandlerMapping; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; @@ -33,27 +35,18 @@ public class OpenTelemetryHandlerMappingFilter implements Filter, Ordered { - private final HttpRouteGetter serverSpanName = + private final HttpServerRouteGetter serverSpanName = (context, request) -> { - RequestPath previousValue = null; if (this.parseRequestPath) { - previousValue = - (RequestPath) request.getAttribute(ServletRequestPathUtils.PATH_ATTRIBUTE); // sets new value for PATH_ATTRIBUTE of request ServletRequestPathUtils.parseAndCache(request); } - try { - if (findMapping(request)) { - // Name the parent span based on the matching pattern - // Let the parent span resource name be set with the attribute set in findMapping. - return SpringWebMvcServerSpanNaming.SERVER_SPAN_NAME.get(context, request); - } - } finally { - // mimic spring DispatcherServlet and restore the previous value of PATH_ATTRIBUTE - if (this.parseRequestPath) { - ServletRequestPathUtils.setParsedRequestPath(previousValue, request); - } + if (findMapping(request)) { + // Name the parent span based on the matching pattern + // Let the parent span resource name be set with the attribute set in findMapping. + return SpringWebMvcServerSpanNaming.SERVER_SPAN_NAME.get(context, request); } + return null; }; @@ -77,8 +70,7 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha } finally { if (handlerMappings != null) { Context context = Context.current(); - HttpRouteHolder.updateHttpRoute( - context, CONTROLLER, serverSpanName, (HttpServletRequest) request); + HttpServerRoute.update(context, CONTROLLER, serverSpanName, prepareRequest(request)); } } } @@ -86,6 +78,31 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha @Override public void destroy() {} + private static HttpServletRequest prepareRequest(ServletRequest request) { + // https://github.com/open-telemetry/opentelemetry-java-instrumentation/issues/10379 + // Finding the request handler modifies request attributes. We are wrapping the request to avoid + // this. + return new HttpServletRequestWrapper((HttpServletRequest) request) { + private final Map attributes = new HashMap<>(); + + @Override + public void setAttribute(String name, Object o) { + attributes.put(name, o); + } + + @Override + public Object getAttribute(String name) { + Object value = attributes.get(name); + return value != null ? value : super.getAttribute(name); + } + + @Override + public void removeAttribute(String name) { + attributes.remove(name); + } + }; + } + /** * 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 diff --git a/instrumentation/spring/spring-webmvc/spring-webmvc-6.0/javaagent/src/test/groovy/SecurityConfig.groovy b/instrumentation/spring/spring-webmvc/spring-webmvc-6.0/javaagent/src/test/groovy/SecurityConfig.groovy deleted file mode 100644 index bf4d61c0973a..000000000000 --- a/instrumentation/spring/spring-webmvc/spring-webmvc-6.0/javaagent/src/test/groovy/SecurityConfig.groovy +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -import boot.SavingAuthenticationProvider -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.web.SecurityFilterChain - -@Configuration -@EnableWebSecurity -class SecurityConfig { - - @Bean - SavingAuthenticationProvider savingAuthenticationProvider() { - return new SavingAuthenticationProvider() - } - - /** - * Following configuration is required for unauthorised call tests (form would redirect, we need 401) - */ - @Bean - @Order(1) - SecurityFilterChain apiWebSecurity(HttpSecurity http, SavingAuthenticationProvider savingAuthenticationProvider) { - return http - .csrf().disable() - .securityMatcher("/basicsecured/**") - .authorizeHttpRequests() - .requestMatchers("/basicsecured/**").authenticated() - .and() - .httpBasic() - .and() - .authenticationProvider(savingAuthenticationProvider) - .build() - } - - /** - * Following configuration is required in order to get form login, needed by password tests - */ - @Bean - SecurityFilterChain formLoginWebSecurity(HttpSecurity http, SavingAuthenticationProvider savingAuthenticationProvider) { - return http - .csrf().disable() - .authorizeHttpRequests() - .requestMatchers("/formsecured/**").authenticated() - .anyRequest().permitAll() - .and() - .formLogin() - .and() - .authenticationProvider(savingAuthenticationProvider) - .build() - } -} diff --git a/instrumentation/spring/spring-webmvc/spring-webmvc-6.0/javaagent/src/test/groovy/ServletFilterConfig.groovy b/instrumentation/spring/spring-webmvc/spring-webmvc-6.0/javaagent/src/test/groovy/ServletFilterConfig.groovy deleted file mode 100644 index 7f4c797290d4..000000000000 --- a/instrumentation/spring/spring-webmvc/spring-webmvc-6.0/javaagent/src/test/groovy/ServletFilterConfig.groovy +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -import io.opentelemetry.instrumentation.test.base.HttpServerTest -import io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint -import org.springframework.context.annotation.Bean -import org.springframework.context.annotation.Configuration - -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.HttpServletRequest -import jakarta.servlet.http.HttpServletResponse - -import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.CAPTURE_HEADERS -import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.ERROR -import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.EXCEPTION -import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.INDEXED_CHILD -import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.PATH_PARAM -import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.QUERY_PARAM -import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.REDIRECT -import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.SUCCESS - -@Configuration -class ServletFilterConfig { - - @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 - ServerEndpoint endpoint = 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 CAPTURE_HEADERS: - resp.setHeader("X-Test-Response", req.getHeader("X-Test-Request")) - resp.status = endpoint.status - resp.writer.print(endpoint.body) - break - case ERROR: - resp.sendError(endpoint.status, endpoint.body) - break - case EXCEPTION: - throw new Exception(endpoint.body) - case INDEXED_CHILD: - INDEXED_CHILD.collectSpanAttributes { name -> req.getParameter(name) } - resp.writer.print(endpoint.body) - break - default: - chain.doFilter(request, response) - } - } - } - - @Override - void destroy() { - } - } - } -} diff --git a/instrumentation/spring/spring-webmvc/spring-webmvc-6.0/javaagent/src/test/groovy/ServletFilterTest.groovy b/instrumentation/spring/spring-webmvc/spring-webmvc-6.0/javaagent/src/test/groovy/ServletFilterTest.groovy deleted file mode 100644 index 644a6a44d21c..000000000000 --- a/instrumentation/spring/spring-webmvc/spring-webmvc-6.0/javaagent/src/test/groovy/ServletFilterTest.groovy +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -import filter.AbstractServletFilterTest - -class ServletFilterTest extends AbstractServletFilterTest { - - Class securityConfigClass() { - SecurityConfig - } - - Class filterConfigClass() { - ServletFilterConfig - } -} diff --git a/instrumentation/spring/spring-webmvc/spring-webmvc-6.0/javaagent/src/test/groovy/SpringBootBasedTest.groovy b/instrumentation/spring/spring-webmvc/spring-webmvc-6.0/javaagent/src/test/groovy/SpringBootBasedTest.groovy deleted file mode 100644 index 9b647b549687..000000000000 --- a/instrumentation/spring/spring-webmvc/spring-webmvc-6.0/javaagent/src/test/groovy/SpringBootBasedTest.groovy +++ /dev/null @@ -1,13 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -import boot.AbstractSpringBootBasedTest - -class SpringBootBasedTest extends AbstractSpringBootBasedTest { - - Class securityConfigClass() { - SecurityConfig - } -} diff --git a/instrumentation/spring/spring-webmvc/spring-webmvc-6.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webmvc/v6_0/boot/SecurityConfig.java b/instrumentation/spring/spring-webmvc/spring-webmvc-6.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webmvc/v6_0/boot/SecurityConfig.java new file mode 100644 index 000000000000..ca8eb96a5cab --- /dev/null +++ b/instrumentation/spring/spring-webmvc/spring-webmvc-6.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webmvc/v6_0/boot/SecurityConfig.java @@ -0,0 +1,57 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.webmvc.v6_0.boot; + +import io.opentelemetry.instrumentation.spring.webmvc.boot.SavingAuthenticationProvider; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.Order; +import org.springframework.security.config.Customizer; +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.configurers.AbstractHttpConfigurer; +import org.springframework.security.web.SecurityFilterChain; + +@Configuration +@EnableWebSecurity +public class SecurityConfig { + + @Bean + SavingAuthenticationProvider savingAuthenticationProvider() { + return new SavingAuthenticationProvider(); + } + + /** + * Following configuration is required for unauthorised call tests (form would redirect, we need + * 401) + */ + @Bean + @Order(1) + SecurityFilterChain apiWebSecurity( + HttpSecurity http, SavingAuthenticationProvider savingAuthenticationProvider) + throws Exception { + return http.csrf(AbstractHttpConfigurer::disable) + .securityMatcher("/basicsecured/**") + .authorizeHttpRequests(auth -> auth.requestMatchers("/basicsecured/**").authenticated()) + .authenticationProvider(savingAuthenticationProvider) + .httpBasic(Customizer.withDefaults()) + .build(); + } + + /** Following configuration is required in order to get form login, needed by password tests */ + @Bean + SecurityFilterChain formLoginWebSecurity( + HttpSecurity http, SavingAuthenticationProvider savingAuthenticationProvider) + throws Exception { + return http.csrf(AbstractHttpConfigurer::disable) + .authorizeHttpRequests( + auth -> + auth.requestMatchers("/formsecured/**").authenticated().anyRequest().permitAll()) + .authenticationProvider(savingAuthenticationProvider) + .formLogin(Customizer.withDefaults()) + .build(); + } +} diff --git a/instrumentation/spring/spring-webmvc/spring-webmvc-6.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webmvc/v6_0/boot/SpringBootBasedTest.java b/instrumentation/spring/spring-webmvc/spring-webmvc-6.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webmvc/v6_0/boot/SpringBootBasedTest.java new file mode 100644 index 000000000000..d089cc69e987 --- /dev/null +++ b/instrumentation/spring/spring-webmvc/spring-webmvc-6.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webmvc/v6_0/boot/SpringBootBasedTest.java @@ -0,0 +1,110 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.webmvc.v6_0.boot; + +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.EXCEPTION; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.satisfies; +import static io.opentelemetry.semconv.ExceptionAttributes.EXCEPTION_MESSAGE; +import static io.opentelemetry.semconv.ExceptionAttributes.EXCEPTION_STACKTRACE; +import static io.opentelemetry.semconv.ExceptionAttributes.EXCEPTION_TYPE; +import static org.assertj.core.api.Assertions.assertThat; + +import com.google.common.collect.ImmutableMap; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.spring.webmvc.boot.AbstractSpringBootBasedTest; +import io.opentelemetry.instrumentation.spring.webmvc.boot.AppConfig; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.http.HttpServerInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.http.HttpServerTestOptions; +import io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint; +import io.opentelemetry.sdk.testing.assertj.SpanDataAssert; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.sdk.trace.data.StatusData; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.springframework.boot.SpringApplication; +import org.springframework.context.ConfigurableApplicationContext; + +class SpringBootBasedTest extends AbstractSpringBootBasedTest { + + @RegisterExtension + private static final InstrumentationExtension testing = + HttpServerInstrumentationExtension.forAgent(); + + private static final boolean testLatestDeps = Boolean.getBoolean("testLatestDeps"); + + private ConfigurableApplicationContext context; + + @Override + protected ConfigurableApplicationContext context() { + return context; + } + + @Override + protected Class securityConfigClass() { + return SecurityConfig.class; + } + + @Override + protected ConfigurableApplicationContext setupServer() { + SpringApplication app = new SpringApplication(AppConfig.class, securityConfigClass()); + app.setDefaultProperties( + ImmutableMap.of( + "server.port", + port, + "server.context-path", + getContextPath(), + "server.servlet.contextPath", + getContextPath(), + "server.error.include-message", + "always")); + context = app.run(); + return context; + } + + @Override + protected SpanDataAssert assertHandlerSpan( + SpanDataAssert span, String method, ServerEndpoint endpoint) { + if (testLatestDeps && endpoint == ServerEndpoint.NOT_FOUND) { + String handlerSpanName = "ResourceHttpRequestHandler.handleRequest"; + span.hasName(handlerSpanName) + .hasKind(SpanKind.INTERNAL) + .hasStatus(StatusData.error()) + .hasEventsSatisfyingExactly( + event -> + event + .hasName("exception") + .hasAttributesSatisfyingExactly( + equalTo( + EXCEPTION_TYPE, + "org.springframework.web.servlet.resource.NoResourceFoundException"), + satisfies(EXCEPTION_MESSAGE, val -> assertThat(val).isNotNull()), + satisfies(EXCEPTION_STACKTRACE, val -> val.isInstanceOf(String.class)))); + return span; + } else { + return super.assertHandlerSpan(span, method, endpoint); + } + } + + @Override + protected SpanDataAssert assertResponseSpan( + SpanDataAssert span, SpanData parentSpan, String method, ServerEndpoint endpoint) { + if (testLatestDeps && endpoint == ServerEndpoint.NOT_FOUND) { + // not verifying the parent span, in the latest version the responseSpan is the child of the + // SERVER span, not the handler span + return super.assertResponseSpan(span, method, endpoint); + } else { + return super.assertResponseSpan(span, parentSpan, method, endpoint); + } + } + + @Override + protected void configure(HttpServerTestOptions options) { + super.configure(options); + options.setResponseCodeOnNonStandardHttpMethod(400); + options.setExpectedException(new RuntimeException(EXCEPTION.getBody())); + } +} diff --git a/instrumentation/spring/spring-webmvc/spring-webmvc-6.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webmvc/v6_0/filter/ServletFilterConfig.java b/instrumentation/spring/spring-webmvc/spring-webmvc-6.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webmvc/v6_0/filter/ServletFilterConfig.java new file mode 100644 index 000000000000..f2bc8ee660ac --- /dev/null +++ b/instrumentation/spring/spring-webmvc/spring-webmvc-6.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webmvc/v6_0/filter/ServletFilterConfig.java @@ -0,0 +1,85 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.webmvc.v6_0.filter; + +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.INDEXED_CHILD; + +import io.opentelemetry.instrumentation.test.base.HttpServerTest; +import io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint; +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.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +class ServletFilterConfig { + + @Bean + Filter servletFilter() { + return new Filter() { + + @Override + public void init(FilterConfig filterConfig) throws ServletException {} + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + HttpServletRequest req = (HttpServletRequest) request; + HttpServletResponse resp = (HttpServletResponse) response; + ServerEndpoint endpoint = ServerEndpoint.forPath(req.getServletPath()); + HttpServerTest.controller( + endpoint, + () -> { + resp.setContentType("text/plain"); + switch (endpoint.name()) { + case "SUCCESS": + resp.setStatus(endpoint.getStatus()); + resp.getWriter().print(endpoint.getBody()); + break; + case "QUERY_PARAM": + resp.setStatus(endpoint.getStatus()); + resp.getWriter().print(req.getQueryString()); + break; + case "PATH_PARAM": + resp.setStatus(endpoint.getStatus()); + resp.getWriter().print(endpoint.getBody()); + break; + case "REDIRECT": + resp.sendRedirect(endpoint.getBody()); + break; + case "CAPTURE_HEADERS": + resp.setHeader("X-Test-Response", req.getHeader("X-Test-Request")); + resp.setStatus(endpoint.getStatus()); + resp.getWriter().print(endpoint.getBody()); + break; + case "ERROR": + resp.sendError(endpoint.getStatus(), endpoint.getBody()); + break; + case "EXCEPTION": + throw new Exception(endpoint.getBody()); + case "INDEXED_CHILD": + INDEXED_CHILD.collectSpanAttributes(req::getParameter); + resp.getWriter().print(endpoint.getBody()); + break; + default: + chain.doFilter(request, response); + } + return null; + }); + } + + @Override + public void destroy() {} + }; + } +} diff --git a/instrumentation/spring/spring-webmvc/spring-webmvc-6.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webmvc/v6_0/filter/ServletFilterTest.java b/instrumentation/spring/spring-webmvc/spring-webmvc-6.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webmvc/v6_0/filter/ServletFilterTest.java new file mode 100644 index 000000000000..2b8ae89b4679 --- /dev/null +++ b/instrumentation/spring/spring-webmvc/spring-webmvc-6.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/webmvc/v6_0/filter/ServletFilterTest.java @@ -0,0 +1,101 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.webmvc.v6_0.filter; + +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.satisfies; +import static io.opentelemetry.semconv.ExceptionAttributes.EXCEPTION_MESSAGE; +import static io.opentelemetry.semconv.ExceptionAttributes.EXCEPTION_STACKTRACE; +import static io.opentelemetry.semconv.ExceptionAttributes.EXCEPTION_TYPE; +import static org.assertj.core.api.Assertions.assertThat; + +import com.google.common.collect.ImmutableMap; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.spring.webmvc.filter.AbstractServletFilterTest; +import io.opentelemetry.instrumentation.spring.webmvc.filter.FilteredAppConfig; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.http.HttpServerInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.http.HttpServerTestOptions; +import io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint; +import io.opentelemetry.javaagent.instrumentation.spring.webmvc.v6_0.boot.SecurityConfig; +import io.opentelemetry.sdk.testing.assertj.SpanDataAssert; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.sdk.trace.data.StatusData; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.springframework.boot.SpringApplication; +import org.springframework.context.ConfigurableApplicationContext; + +class ServletFilterTest extends AbstractServletFilterTest { + + private static final boolean testLatestDeps = Boolean.getBoolean("testLatestDeps"); + + @RegisterExtension + private static final InstrumentationExtension testing = + HttpServerInstrumentationExtension.forAgent(); + + @Override + protected Class securityConfigClass() { + return SecurityConfig.class; + } + + @Override + protected Class filterConfigClass() { + return ServletFilterConfig.class; + } + + @Override + protected ConfigurableApplicationContext setupServer() { + SpringApplication app = + new SpringApplication(FilteredAppConfig.class, securityConfigClass(), filterConfigClass()); + app.setDefaultProperties( + ImmutableMap.of("server.port", port, "server.error.include-message", "always")); + return app.run(); + } + + @Override + protected SpanDataAssert assertHandlerSpan( + SpanDataAssert span, String method, ServerEndpoint endpoint) { + if (testLatestDeps && endpoint == ServerEndpoint.NOT_FOUND) { + String handlerSpanName = "ResourceHttpRequestHandler.handleRequest"; + span.hasName(handlerSpanName) + .hasKind(SpanKind.INTERNAL) + .hasStatus(StatusData.error()) + .hasEventsSatisfyingExactly( + event -> + event + .hasName("exception") + .hasAttributesSatisfyingExactly( + equalTo( + EXCEPTION_TYPE, + "org.springframework.web.servlet.resource.NoResourceFoundException"), + satisfies(EXCEPTION_MESSAGE, val -> assertThat(val).isNotNull()), + satisfies(EXCEPTION_STACKTRACE, val -> val.isInstanceOf(String.class)))); + return span; + } else { + return super.assertHandlerSpan(span, method, endpoint); + } + } + + @Override + protected SpanDataAssert assertResponseSpan( + SpanDataAssert span, SpanData parentSpan, String method, ServerEndpoint endpoint) { + if (testLatestDeps && endpoint == ServerEndpoint.NOT_FOUND) { + span.satisfies(spanData -> assertThat(spanData.getName()).endsWith(".sendError")); + span.hasKind(SpanKind.INTERNAL); + // not verifying the parent span, in the latest version the responseSpan is the child of the + // SERVER span, not the handler span + return span; + } else { + return super.assertResponseSpan(span, parentSpan, method, endpoint); + } + } + + @Override + protected void configure(HttpServerTestOptions options) { + super.configure(options); + options.setResponseCodeOnNonStandardHttpMethod(400); + } +} diff --git a/instrumentation/spring/spring-webmvc/spring-webmvc-6.0/library/README.md b/instrumentation/spring/spring-webmvc/spring-webmvc-6.0/library/README.md index e0809c70f44e..f6f43aabb813 100644 --- a/instrumentation/spring/spring-webmvc/spring-webmvc-6.0/library/README.md +++ b/instrumentation/spring/spring-webmvc/spring-webmvc-6.0/library/README.md @@ -25,7 +25,7 @@ For Maven add the following to your `pom.xml`: - + io.opentelemetry opentelemetry-exporter-logging @@ -51,7 +51,7 @@ For Gradle add the following to your dependencies: implementation("io.opentelemetry.instrumentation:opentelemetry-spring-webmvc-6.0:OPENTELEMETRY_VERSION") // OpenTelemetry exporter -// replace this default exporter with your OpenTelemetry exporter (ex. otlp/zipkin/jaeger/..) +// replace this default exporter with your OpenTelemetry exporter (ex. otlp/zipkin/..) implementation("io.opentelemetry:opentelemetry-exporter-logging:OPENTELEMETRY_VERSION") // required to instrument Spring WebMVC diff --git a/instrumentation/spring/spring-webmvc/spring-webmvc-6.0/library/build.gradle.kts b/instrumentation/spring/spring-webmvc/spring-webmvc-6.0/library/build.gradle.kts index 4fb5e4f64ac6..18188322f6e3 100644 --- a/instrumentation/spring/spring-webmvc/spring-webmvc-6.0/library/build.gradle.kts +++ b/instrumentation/spring/spring-webmvc/spring-webmvc-6.0/library/build.gradle.kts @@ -15,3 +15,16 @@ dependencies { otelJava { minJavaVersionSupported.set(JavaVersion.VERSION_17) } + +tasks { + compileJava { + // We compile this module for java 8 because it is used as a dependency in spring-boot-autoconfigure. + // If this module is compiled for java 17 then gradle can figure out based on the metadata that + // spring-boot-autoconfigure has a dependency that requires 17 and fails the build when it is used + // in a project that targets an earlier java version. + // https://github.com/open-telemetry/opentelemetry-java-instrumentation/issues/9949 + sourceCompatibility = "1.8" + targetCompatibility = "1.8" + options.release.set(null as Int?) + } +} diff --git a/instrumentation/spring/spring-webmvc/spring-webmvc-6.0/library/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/v6_0/SpringWebMvcHttpAttributesGetter.java b/instrumentation/spring/spring-webmvc/spring-webmvc-6.0/library/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/v6_0/SpringWebMvcHttpAttributesGetter.java index edb8941e1470..e651499239e4 100644 --- a/instrumentation/spring/spring-webmvc/spring-webmvc-6.0/library/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/v6_0/SpringWebMvcHttpAttributesGetter.java +++ b/instrumentation/spring/spring-webmvc/spring-webmvc-6.0/library/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/v6_0/SpringWebMvcHttpAttributesGetter.java @@ -5,7 +5,7 @@ package io.opentelemetry.instrumentation.spring.webmvc.v6_0; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpServerAttributesGetter; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerAttributesGetter; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import java.util.ArrayList; @@ -80,4 +80,52 @@ public String getUrlPath(HttpServletRequest request) { public String getUrlQuery(HttpServletRequest request) { return request.getQueryString(); } + + @Nullable + @Override + public String getNetworkProtocolName( + HttpServletRequest request, @Nullable HttpServletResponse response) { + String protocol = request.getProtocol(); + if (protocol != null && protocol.startsWith("HTTP/")) { + return "http"; + } + return null; + } + + @Nullable + @Override + public String getNetworkProtocolVersion( + HttpServletRequest request, @Nullable HttpServletResponse response) { + String protocol = request.getProtocol(); + if (protocol != null && protocol.startsWith("HTTP/")) { + return protocol.substring("HTTP/".length()); + } + return null; + } + + @Override + @Nullable + public String getNetworkPeerAddress( + HttpServletRequest request, @Nullable HttpServletResponse response) { + return request.getRemoteAddr(); + } + + @Override + public Integer getNetworkPeerPort( + HttpServletRequest request, @Nullable HttpServletResponse response) { + return request.getRemotePort(); + } + + @Nullable + @Override + public String getNetworkLocalAddress( + HttpServletRequest request, @Nullable HttpServletResponse response) { + return request.getLocalAddr(); + } + + @Override + public Integer getNetworkLocalPort( + HttpServletRequest request, @Nullable HttpServletResponse response) { + return request.getLocalPort(); + } } diff --git a/instrumentation/spring/spring-webmvc/spring-webmvc-6.0/library/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/v6_0/SpringWebMvcNetAttributesGetter.java b/instrumentation/spring/spring-webmvc/spring-webmvc-6.0/library/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/v6_0/SpringWebMvcNetAttributesGetter.java deleted file mode 100644 index 6e67255f0ff6..000000000000 --- a/instrumentation/spring/spring-webmvc/spring-webmvc-6.0/library/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/v6_0/SpringWebMvcNetAttributesGetter.java +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.spring.webmvc.v6_0; - -import io.opentelemetry.instrumentation.api.instrumenter.net.NetServerAttributesGetter; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import javax.annotation.Nullable; - -enum SpringWebMvcNetAttributesGetter - implements NetServerAttributesGetter { - INSTANCE; - - @Nullable - @Override - public String getNetworkProtocolName( - HttpServletRequest request, @Nullable HttpServletResponse response) { - String protocol = request.getProtocol(); - if (protocol != null && protocol.startsWith("HTTP/")) { - return "http"; - } - return null; - } - - @Nullable - @Override - public String getNetworkProtocolVersion( - HttpServletRequest request, @Nullable HttpServletResponse response) { - String protocol = request.getProtocol(); - if (protocol != null && protocol.startsWith("HTTP/")) { - return protocol.substring("HTTP/".length()); - } - return null; - } - - @Nullable - @Override - public String getServerAddress(HttpServletRequest request) { - return request.getServerName(); - } - - @Override - public Integer getServerPort(HttpServletRequest request) { - return request.getServerPort(); - } - - @Override - @Nullable - public String getClientSocketAddress( - HttpServletRequest request, @Nullable HttpServletResponse response) { - return request.getRemoteAddr(); - } - - @Override - public Integer getClientSocketPort( - HttpServletRequest request, @Nullable HttpServletResponse response) { - return request.getRemotePort(); - } - - @Nullable - @Override - public String getServerSocketAddress( - HttpServletRequest request, @Nullable HttpServletResponse response) { - return request.getLocalAddr(); - } - - @Override - public Integer getServerSocketPort( - HttpServletRequest request, @Nullable HttpServletResponse response) { - return request.getLocalPort(); - } -} diff --git a/instrumentation/spring/spring-webmvc/spring-webmvc-6.0/library/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/v6_0/SpringWebMvcTelemetryBuilder.java b/instrumentation/spring/spring-webmvc/spring-webmvc-6.0/library/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/v6_0/SpringWebMvcTelemetryBuilder.java index 2265c1b02cc2..abf49f90fd25 100644 --- a/instrumentation/spring/spring-webmvc/spring-webmvc-6.0/library/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/v6_0/SpringWebMvcTelemetryBuilder.java +++ b/instrumentation/spring/spring-webmvc/spring-webmvc-6.0/library/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/v6_0/SpringWebMvcTelemetryBuilder.java @@ -7,34 +7,33 @@ import com.google.errorprone.annotations.CanIgnoreReturnValue; import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.instrumentation.api.incubator.builder.internal.DefaultHttpServerInstrumenterBuilder; import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteHolder; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpServerAttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpServerAttributesExtractorBuilder; -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 io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerAttributesExtractorBuilder; +import io.opentelemetry.instrumentation.spring.webmvc.v6_0.internal.SpringMvcBuilderUtil; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; -import java.util.ArrayList; import java.util.List; +import java.util.Set; +import java.util.function.Function; /** A builder of {@link SpringWebMvcTelemetry}. */ public final class SpringWebMvcTelemetryBuilder { private static final String INSTRUMENTATION_NAME = "io.opentelemetry.spring-webmvc-6.0"; + private final DefaultHttpServerInstrumenterBuilder + builder; - private final OpenTelemetry openTelemetry; - private final List> - additionalExtractors = new ArrayList<>(); - private final HttpServerAttributesExtractorBuilder - httpAttributesExtractorBuilder = - HttpServerAttributesExtractor.builder( - SpringWebMvcHttpAttributesGetter.INSTANCE, SpringWebMvcNetAttributesGetter.INSTANCE); + static { + SpringMvcBuilderUtil.setBuilderExtractor(SpringWebMvcTelemetryBuilder::getBuilder); + } SpringWebMvcTelemetryBuilder(OpenTelemetry openTelemetry) { - this.openTelemetry = openTelemetry; + builder = + new DefaultHttpServerInstrumenterBuilder<>( + INSTRUMENTATION_NAME, openTelemetry, SpringWebMvcHttpAttributesGetter.INSTANCE) + .setHeaderGetter(JakartaHttpServletRequestGetter.INSTANCE); } /** @@ -44,7 +43,7 @@ public final class SpringWebMvcTelemetryBuilder { @CanIgnoreReturnValue public SpringWebMvcTelemetryBuilder addAttributesExtractor( AttributesExtractor attributesExtractor) { - additionalExtractors.add(attributesExtractor); + builder.addAttributesExtractor(attributesExtractor); return this; } @@ -55,7 +54,7 @@ public SpringWebMvcTelemetryBuilder addAttributesExtractor( */ @CanIgnoreReturnValue public SpringWebMvcTelemetryBuilder setCapturedRequestHeaders(List requestHeaders) { - httpAttributesExtractorBuilder.setCapturedRequestHeaders(requestHeaders); + builder.setCapturedRequestHeaders(requestHeaders); return this; } @@ -66,7 +65,50 @@ public SpringWebMvcTelemetryBuilder setCapturedRequestHeaders(List reque */ @CanIgnoreReturnValue public SpringWebMvcTelemetryBuilder setCapturedResponseHeaders(List responseHeaders) { - httpAttributesExtractorBuilder.setCapturedResponseHeaders(responseHeaders); + builder.setCapturedResponseHeaders(responseHeaders); + return this; + } + + /** Sets custom {@link SpanNameExtractor} via transform function. */ + @CanIgnoreReturnValue + public SpringWebMvcTelemetryBuilder setSpanNameExtractor( + Function< + SpanNameExtractor, + ? extends SpanNameExtractor> + spanNameExtractor) { + builder.setSpanNameExtractor(spanNameExtractor); + return this; + } + + /** + * Configures the instrumentation to recognize an alternative set of HTTP request methods. + * + *

    By default, this instrumentation defines "known" methods as the ones listed in RFC9110 and the PATCH + * method defined in RFC5789. + * + *

    Note: calling this method overrides the default known method sets completely; it does + * not supplement it. + * + * @param knownMethods A set of recognized HTTP request methods. + * @see HttpServerAttributesExtractorBuilder#setKnownMethods(Set) + */ + @CanIgnoreReturnValue + public SpringWebMvcTelemetryBuilder setKnownMethods(Set knownMethods) { + builder.setKnownMethods(knownMethods); + return this; + } + + /** + * Configures the instrumentation to emit experimental HTTP server metrics. + * + * @param emitExperimentalHttpServerMetrics {@code true} if the experimental HTTP server metrics + * are to be emitted. + */ + @CanIgnoreReturnValue + public SpringWebMvcTelemetryBuilder setEmitExperimentalHttpServerMetrics( + boolean emitExperimentalHttpServerMetrics) { + builder.setEmitExperimentalHttpServerMetrics(emitExperimentalHttpServerMetrics); return this; } @@ -75,21 +117,11 @@ public SpringWebMvcTelemetryBuilder setCapturedResponseHeaders(List resp * SpringWebMvcTelemetryBuilder}. */ public SpringWebMvcTelemetry build() { - SpringWebMvcHttpAttributesGetter httpAttributesGetter = - SpringWebMvcHttpAttributesGetter.INSTANCE; - - Instrumenter instrumenter = - Instrumenter.builder( - openTelemetry, - INSTRUMENTATION_NAME, - HttpSpanNameExtractor.create(httpAttributesGetter)) - .setSpanStatusExtractor(HttpSpanStatusExtractor.create(httpAttributesGetter)) - .addAttributesExtractor(httpAttributesExtractorBuilder.build()) - .addAttributesExtractors(additionalExtractors) - .addOperationMetrics(HttpServerMetrics.get()) - .addContextCustomizer(HttpRouteHolder.create(httpAttributesGetter)) - .buildServerInstrumenter(JakartaHttpServletRequestGetter.INSTANCE); + return new SpringWebMvcTelemetry(builder.build()); + } - return new SpringWebMvcTelemetry(instrumenter); + public DefaultHttpServerInstrumenterBuilder + getBuilder() { + return builder; } } diff --git a/instrumentation/spring/spring-webmvc/spring-webmvc-6.0/library/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/v6_0/WebMvcTelemetryProducingFilter.java b/instrumentation/spring/spring-webmvc/spring-webmvc-6.0/library/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/v6_0/WebMvcTelemetryProducingFilter.java index 4047ef75495f..fb4fdc719d15 100644 --- a/instrumentation/spring/spring-webmvc/spring-webmvc-6.0/library/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/v6_0/WebMvcTelemetryProducingFilter.java +++ b/instrumentation/spring/spring-webmvc/spring-webmvc-6.0/library/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/v6_0/WebMvcTelemetryProducingFilter.java @@ -5,18 +5,25 @@ package io.opentelemetry.instrumentation.spring.webmvc.v6_0; -import static io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteSource.CONTROLLER; +import static io.opentelemetry.instrumentation.api.semconv.http.HttpServerRouteSource.CONTROLLER; import static java.util.Objects.requireNonNull; import io.opentelemetry.context.Context; import io.opentelemetry.context.Scope; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteHolder; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRoute; +import jakarta.servlet.AsyncContext; +import jakarta.servlet.AsyncEvent; +import jakarta.servlet.AsyncListener; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletRequestWrapper; import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; +import java.util.concurrent.atomic.AtomicBoolean; import org.springframework.core.Ordered; import org.springframework.web.filter.OncePerRequestFilter; @@ -53,18 +60,21 @@ public void doFilterInternal( } Context context = instrumenter.start(parentContext, request); + AsyncAwareHttpServletRequest asyncAwareRequest = + new AsyncAwareHttpServletRequest(request, response, context); Throwable error = null; try (Scope ignored = context.makeCurrent()) { - filterChain.doFilter(request, response); + filterChain.doFilter(asyncAwareRequest, response); } catch (Throwable t) { error = t; throw t; } finally { if (httpRouteSupport.hasMappings()) { - HttpRouteHolder.updateHttpRoute( - context, CONTROLLER, httpRouteSupport::getHttpRoute, request); + HttpServerRoute.update(context, CONTROLLER, httpRouteSupport::getHttpRoute, request); + } + if (error != null || asyncAwareRequest.isNotAsync()) { + instrumenter.end(context, request, response, error); } - instrumenter.end(context, request, response, error); } } @@ -76,4 +86,88 @@ public int getOrder() { // Run after all HIGHEST_PRECEDENCE items return Ordered.HIGHEST_PRECEDENCE + 1; } + + private class AsyncAwareHttpServletRequest extends HttpServletRequestWrapper { + private final HttpServletRequest request; + private final HttpServletResponse response; + private final Context context; + private final AtomicBoolean listenerAttached = new AtomicBoolean(); + + AsyncAwareHttpServletRequest( + HttpServletRequest request, HttpServletResponse response, Context context) { + super(request); + this.request = request; + this.response = response; + this.context = context; + } + + @Override + public AsyncContext startAsync() { + AsyncContext asyncContext = super.startAsync(); + attachListener(asyncContext); + return asyncContext; + } + + @Override + public AsyncContext startAsync(ServletRequest servletRequest, ServletResponse servletResponse) { + AsyncContext asyncContext = super.startAsync(servletRequest, servletResponse); + attachListener(asyncContext); + return asyncContext; + } + + private void attachListener(AsyncContext asyncContext) { + if (!listenerAttached.compareAndSet(false, true)) { + return; + } + + asyncContext.addListener( + new AsyncRequestCompletionListener(request, response, context), request, response); + } + + boolean isNotAsync() { + return !listenerAttached.get(); + } + } + + private class AsyncRequestCompletionListener implements AsyncListener { + private final HttpServletRequest request; + private final HttpServletResponse response; + private final Context context; + private final AtomicBoolean responseHandled = new AtomicBoolean(); + + AsyncRequestCompletionListener( + HttpServletRequest request, HttpServletResponse response, Context context) { + this.request = request; + this.response = response; + this.context = context; + } + + @Override + public void onComplete(AsyncEvent asyncEvent) { + if (responseHandled.compareAndSet(false, true)) { + instrumenter.end(context, request, response, null); + } + } + + @Override + public void onTimeout(AsyncEvent asyncEvent) { + if (responseHandled.compareAndSet(false, true)) { + instrumenter.end(context, request, response, null); + } + } + + @Override + public void onError(AsyncEvent asyncEvent) { + if (responseHandled.compareAndSet(false, true)) { + instrumenter.end(context, request, response, asyncEvent.getThrowable()); + } + } + + @Override + public void onStartAsync(AsyncEvent asyncEvent) { + asyncEvent + .getAsyncContext() + .addListener(this, asyncEvent.getSuppliedRequest(), asyncEvent.getSuppliedResponse()); + } + } } diff --git a/instrumentation/spring/spring-webmvc/spring-webmvc-6.0/library/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/v6_0/internal/SpringMvcBuilderUtil.java b/instrumentation/spring/spring-webmvc/spring-webmvc-6.0/library/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/v6_0/internal/SpringMvcBuilderUtil.java new file mode 100644 index 000000000000..d60102acd19a --- /dev/null +++ b/instrumentation/spring/spring-webmvc/spring-webmvc-6.0/library/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/v6_0/internal/SpringMvcBuilderUtil.java @@ -0,0 +1,41 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.webmvc.v6_0.internal; + +import io.opentelemetry.instrumentation.api.incubator.builder.internal.DefaultHttpServerInstrumenterBuilder; +import io.opentelemetry.instrumentation.spring.webmvc.v6_0.SpringWebMvcTelemetryBuilder; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.util.function.Function; + +/** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time. + */ +public final class SpringMvcBuilderUtil { + private SpringMvcBuilderUtil() {} + + // allows access to the private field for the spring starter + private static Function< + SpringWebMvcTelemetryBuilder, + DefaultHttpServerInstrumenterBuilder> + builderExtractor; + + public static Function< + SpringWebMvcTelemetryBuilder, + DefaultHttpServerInstrumenterBuilder> + getBuilderExtractor() { + return builderExtractor; + } + + public static void setBuilderExtractor( + Function< + SpringWebMvcTelemetryBuilder, + DefaultHttpServerInstrumenterBuilder> + builderExtractor) { + SpringMvcBuilderUtil.builderExtractor = builderExtractor; + } +} diff --git a/instrumentation/spring/spring-webmvc/spring-webmvc-6.0/library/src/test/java/io/opentelemetry/instrumentation/spring/webmvc/v6_0/TestWebSpringBootApp.java b/instrumentation/spring/spring-webmvc/spring-webmvc-6.0/library/src/test/java/io/opentelemetry/instrumentation/spring/webmvc/v6_0/TestWebSpringBootApp.java index 2bbe4eee79bd..e7470be6a78b 100644 --- a/instrumentation/spring/spring-webmvc/spring-webmvc-6.0/library/src/test/java/io/opentelemetry/instrumentation/spring/webmvc/v6_0/TestWebSpringBootApp.java +++ b/instrumentation/spring/spring-webmvc/spring-webmvc-6.0/library/src/test/java/io/opentelemetry/instrumentation/spring/webmvc/v6_0/TestWebSpringBootApp.java @@ -17,9 +17,13 @@ import static java.util.Collections.singletonList; import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpServerTest; +import io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint; import jakarta.servlet.Filter; import java.util.Properties; +import java.util.concurrent.CompletableFuture; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.ConfigurableApplicationContext; @@ -37,6 +41,7 @@ @SpringBootApplication class TestWebSpringBootApp { + static final ServerEndpoint ASYNC_ENDPOINT = new ServerEndpoint("ASYNC", "async", 200, "success"); static ConfigurableApplicationContext start(int port, String contextPath) { Properties props = new Properties(); @@ -122,6 +127,26 @@ String indexed_child(@RequestParam("id") String id) { }); } + @RequestMapping("/async") + @ResponseBody + CompletableFuture async() { + Context context = Context.current(); + return CompletableFuture.supplyAsync( + () -> { + // Sleep a bit so that the future completes after the controller method. This helps to + // verify whether request ends after the future has completed not after when the + // controller method has completed. + try { + Thread.sleep(100); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + try (Scope ignored = context.makeCurrent()) { + return controller(ASYNC_ENDPOINT, ASYNC_ENDPOINT::getBody); + } + }); + } + @ExceptionHandler ResponseEntity handleException(Throwable throwable) { return new ResponseEntity<>(throwable.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR); diff --git a/instrumentation/spring/spring-webmvc/spring-webmvc-6.0/library/src/test/java/io/opentelemetry/instrumentation/spring/webmvc/v6_0/WebMvcHttpServerTest.java b/instrumentation/spring/spring-webmvc/spring-webmvc-6.0/library/src/test/java/io/opentelemetry/instrumentation/spring/webmvc/v6_0/WebMvcHttpServerTest.java index 3f76d4a99496..2452fc1ef03c 100644 --- a/instrumentation/spring/spring-webmvc/spring-webmvc-6.0/library/src/test/java/io/opentelemetry/instrumentation/spring/webmvc/v6_0/WebMvcHttpServerTest.java +++ b/instrumentation/spring/spring-webmvc/spring-webmvc-6.0/library/src/test/java/io/opentelemetry/instrumentation/spring/webmvc/v6_0/WebMvcHttpServerTest.java @@ -5,11 +5,17 @@ package io.opentelemetry.instrumentation.spring.webmvc.v6_0; +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.instrumentation.api.internal.HttpConstants; import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpServerTest; import io.opentelemetry.instrumentation.testing.junit.http.HttpServerInstrumentationExtension; import io.opentelemetry.instrumentation.testing.junit.http.HttpServerTestOptions; import io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint; +import io.opentelemetry.testing.internal.armeria.common.AggregatedHttpRequest; +import io.opentelemetry.testing.internal.armeria.common.AggregatedHttpResponse; +import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.springframework.context.ConfigurableApplicationContext; @@ -38,11 +44,30 @@ protected void configure(HttpServerTestOptions options) { options.setTestException(false); options.setExpectedHttpRoute( - endpoint -> { + (endpoint, method) -> { if (endpoint == ServerEndpoint.PATH_PARAM) { return CONTEXT_PATH + "/path/{id}/param"; } - return expectedHttpRoute(endpoint); + if (HttpConstants._OTHER.equals(method)) { + return CONTEXT_PATH + endpoint.getPath(); + } + return expectedHttpRoute(endpoint, method); }); + + options.setResponseCodeOnNonStandardHttpMethod(501); + } + + @Test + void asyncRequest() { + ServerEndpoint endpoint = TestWebSpringBootApp.ASYNC_ENDPOINT; + String method = "GET"; + AggregatedHttpRequest request = request(endpoint, method); + AggregatedHttpResponse response = client.execute(request).aggregate().join(); + + assertThat(response.status().code()).isEqualTo(endpoint.getStatus()); + assertThat(response.contentUtf8()).isEqualTo(endpoint.getBody()); + + String spanId = assertResponseHasCustomizedHeaders(response, endpoint, null); + assertTheTraces(1, null, null, spanId, method, endpoint); } } diff --git a/instrumentation/spring/spring-webmvc/spring-webmvc-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/webmvc/HandlerSpanNameExtractor.java b/instrumentation/spring/spring-webmvc/spring-webmvc-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/webmvc/HandlerSpanNameExtractor.java index 42c34a7289cc..a37ebf617784 100644 --- a/instrumentation/spring/spring-webmvc/spring-webmvc-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/webmvc/HandlerSpanNameExtractor.java +++ b/instrumentation/spring/spring-webmvc/spring-webmvc-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/webmvc/HandlerSpanNameExtractor.java @@ -5,8 +5,8 @@ package io.opentelemetry.javaagent.instrumentation.spring.webmvc; +import io.opentelemetry.instrumentation.api.incubator.semconv.util.SpanNames; import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.util.SpanNames; import java.lang.reflect.Method; import javax.annotation.Nullable; import org.springframework.web.HttpRequestHandler; diff --git a/instrumentation/spring/spring-webmvc/spring-webmvc-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/webmvc/ModelAndViewAttributesExtractor.java b/instrumentation/spring/spring-webmvc/spring-webmvc-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/webmvc/ModelAndViewAttributesExtractor.java index ccfe2f8844b3..1dcece6a87d9 100644 --- a/instrumentation/spring/spring-webmvc/spring-webmvc-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/webmvc/ModelAndViewAttributesExtractor.java +++ b/instrumentation/spring/spring-webmvc/spring-webmvc-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/webmvc/ModelAndViewAttributesExtractor.java @@ -8,7 +8,7 @@ import io.opentelemetry.api.common.AttributesBuilder; import io.opentelemetry.context.Context; import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; -import io.opentelemetry.javaagent.bootstrap.internal.InstrumentationConfig; +import io.opentelemetry.javaagent.bootstrap.internal.AgentInstrumentationConfig; import javax.annotation.Nullable; import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.View; @@ -16,7 +16,7 @@ public class ModelAndViewAttributesExtractor implements AttributesExtractor { private static final boolean CAPTURE_EXPERIMENTAL_SPAN_ATTRIBUTES = - InstrumentationConfig.get() + AgentInstrumentationConfig.get() .getBoolean("otel.instrumentation.spring-webmvc.experimental-span-attributes", false); @Override diff --git a/instrumentation/spring/spring-webmvc/spring-webmvc-common/testing/build.gradle.kts b/instrumentation/spring/spring-webmvc/spring-webmvc-common/testing/build.gradle.kts index 725b5dbd7a08..c4082773d3bb 100644 --- a/instrumentation/spring/spring-webmvc/spring-webmvc-common/testing/build.gradle.kts +++ b/instrumentation/spring/spring-webmvc/spring-webmvc-common/testing/build.gradle.kts @@ -5,8 +5,6 @@ plugins { dependencies { implementation(project(":testing-common")) - implementation("org.spockframework:spock-spring") - compileOnly("org.springframework.boot:spring-boot-starter-test:1.5.17.RELEASE") compileOnly("org.springframework.boot:spring-boot-starter-web:1.5.17.RELEASE") compileOnly("org.springframework.boot:spring-boot-starter-security:1.5.17.RELEASE") diff --git a/instrumentation/spring/spring-webmvc/spring-webmvc-common/testing/src/main/groovy/boot/AbstractSpringBootBasedTest.groovy b/instrumentation/spring/spring-webmvc/spring-webmvc-common/testing/src/main/groovy/boot/AbstractSpringBootBasedTest.groovy deleted file mode 100644 index c65d4f175292..000000000000 --- a/instrumentation/spring/spring-webmvc/spring-webmvc-common/testing/src/main/groovy/boot/AbstractSpringBootBasedTest.groovy +++ /dev/null @@ -1,208 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package boot - -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.testing.junit.http.ServerEndpoint -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.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.security.web.util.OnCommittedResponseWrapper -import org.springframework.web.servlet.view.RedirectView - -import java.util.regex.Pattern - -import static io.opentelemetry.api.trace.SpanKind.INTERNAL -import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.AUTH_ERROR -import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.EXCEPTION -import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.LOGIN -import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.NOT_FOUND -import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.PATH_PARAM -import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.REDIRECT -import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.SUCCESS - -abstract class AbstractSpringBootBasedTest extends HttpServerTest implements AgentTestTrait { - - abstract Class securityConfigClass() - - @Override - ConfigurableApplicationContext startServer(int port) { - def app = new SpringApplication(AppConfig, securityConfigClass()) - 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 expectedHttpRoute(ServerEndpoint endpoint) { - switch (endpoint) { - case PATH_PARAM: - return getContextPath() + "/path/{id}/param" - case NOT_FOUND: - return getContextPath() + "/**" - case LOGIN: - return getContextPath() + "/*" - default: - return super.expectedHttpRoute(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 methodName = endpoint == NOT_FOUND ? "sendError" : "sendRedirect" - // the response wrapper class names vary depending on spring version & scenario - def namePattern = Pattern.compile("(OnCommittedResponseWrapper|HttpServletResponseWrapper|FirewalledResponse).$methodName") - trace.span(index) { - name namePattern - kind INTERNAL - attributes { - "$SemanticAttributes.CODE_NAMESPACE" { - it == OnCommittedResponseWrapper.name - || it == "org.springframework.security.web.firewall.FirewalledResponse" - || it == "jakarta.servlet.http.HttpServletResponseWrapper" - } - "$SemanticAttributes.CODE_FUNCTION" methodName - } - } - } - - @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.name - } - } - } - - @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/instrumentation/spring/spring-webmvc/spring-webmvc-common/testing/src/main/groovy/boot/SavingAuthenticationProvider.groovy b/instrumentation/spring/spring-webmvc/spring-webmvc-common/testing/src/main/groovy/boot/SavingAuthenticationProvider.groovy deleted file mode 100644 index 9ae426dc13bb..000000000000 --- a/instrumentation/spring/spring-webmvc/spring-webmvc-common/testing/src/main/groovy/boot/SavingAuthenticationProvider.groovy +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package 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/instrumentation/spring/spring-webmvc/spring-webmvc-common/testing/src/main/groovy/boot/TestController.groovy b/instrumentation/spring/spring-webmvc/spring-webmvc-common/testing/src/main/groovy/boot/TestController.groovy deleted file mode 100644 index 2489a783e6f0..000000000000 --- a/instrumentation/spring/spring-webmvc/spring-webmvc-common/testing/src/main/groovy/boot/TestController.groovy +++ /dev/null @@ -1,108 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package boot - -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.RequestHeader -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 - -import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.CAPTURE_HEADERS -import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.ERROR -import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.EXCEPTION -import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.INDEXED_CHILD -import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.PATH_PARAM -import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.QUERY_PARAM -import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.REDIRECT -import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.SUCCESS - -@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("/captureHeaders") - ResponseEntity capture_headers(@RequestHeader("X-Test-Request") String testRequestHeader) { - HttpServerTest.controller(CAPTURE_HEADERS) { - ResponseEntity.ok() - .header("X-Test-Response", testRequestHeader) - .body(CAPTURE_HEADERS.body) - } - } - - @RequestMapping("/path/{id}/param") - @ResponseBody - String path_param(@PathVariable("id") int id) { - HttpServerTest.controller(PATH_PARAM) { - id - } - } - - @RequestMapping("/child") - @ResponseBody - String indexed_child(@RequestParam("id") String id) { - HttpServerTest.controller(INDEXED_CHILD) { - INDEXED_CHILD.collectSpanAttributes { it == "id" ? id : null } - INDEXED_CHILD.body - } - } - - @ExceptionHandler - ResponseEntity handleException(Throwable throwable) { - new ResponseEntity(throwable.message, HttpStatus.INTERNAL_SERVER_ERROR) - } -} diff --git a/instrumentation/spring/spring-webmvc/spring-webmvc-common/testing/src/main/groovy/filter/AbstractServletFilterTest.groovy b/instrumentation/spring/spring-webmvc/spring-webmvc-common/testing/src/main/groovy/filter/AbstractServletFilterTest.groovy deleted file mode 100644 index 179a87e2bf17..000000000000 --- a/instrumentation/spring/spring-webmvc/spring-webmvc-common/testing/src/main/groovy/filter/AbstractServletFilterTest.groovy +++ /dev/null @@ -1,107 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package filter - -import io.opentelemetry.instrumentation.test.AgentTestTrait -import io.opentelemetry.instrumentation.test.asserts.TraceAssert -import io.opentelemetry.instrumentation.test.base.HttpServerTest -import io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint -import io.opentelemetry.sdk.trace.data.SpanData -import org.springframework.boot.SpringApplication -import org.springframework.context.ConfigurableApplicationContext - -import static io.opentelemetry.api.trace.SpanKind.INTERNAL -import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.ERROR -import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.EXCEPTION -import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.NOT_FOUND -import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.PATH_PARAM -import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.REDIRECT -import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.SUCCESS - -abstract class AbstractServletFilterTest extends HttpServerTest implements AgentTestTrait { - - abstract Class securityConfigClass() - - abstract Class filterConfigClass() - - @Override - ConfigurableApplicationContext startServer(int port) { - def app = new SpringApplication(FilteredAppConfig, securityConfigClass(), filterConfigClass()) - 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 expectedHttpRoute(ServerEndpoint endpoint) { - switch (endpoint) { - case PATH_PARAM: - return getContextPath() + "/path/{id}/param" - case NOT_FOUND: - return getContextPath() + "/**" - default: - return super.expectedHttpRoute(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/instrumentation/spring/spring-webmvc/spring-webmvc-common/testing/src/main/groovy/filter/FilteredAppConfig.groovy b/instrumentation/spring/spring-webmvc/spring-webmvc-common/testing/src/main/groovy/filter/FilteredAppConfig.groovy deleted file mode 100644 index 577006ea66f4..000000000000 --- a/instrumentation/spring/spring-webmvc/spring-webmvc-common/testing/src/main/groovy/filter/FilteredAppConfig.groovy +++ /dev/null @@ -1,132 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package filter - -import org.springframework.boot.autoconfigure.SpringBootApplication -import org.springframework.context.annotation.Bean -import org.springframework.format.FormatterRegistry -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.validation.MessageCodesResolver -import org.springframework.validation.Validator -import org.springframework.web.HttpMediaTypeNotAcceptableException -import org.springframework.web.accept.ContentNegotiationStrategy -import org.springframework.web.context.request.NativeWebRequest -import org.springframework.web.method.support.HandlerMethodArgumentResolver -import org.springframework.web.method.support.HandlerMethodReturnValueHandler -import org.springframework.web.servlet.HandlerExceptionResolver -import org.springframework.web.servlet.config.annotation.AsyncSupportConfigurer -import org.springframework.web.servlet.config.annotation.ContentNegotiationConfigurer -import org.springframework.web.servlet.config.annotation.CorsRegistry -import org.springframework.web.servlet.config.annotation.DefaultServletHandlerConfigurer -import org.springframework.web.servlet.config.annotation.InterceptorRegistry -import org.springframework.web.servlet.config.annotation.PathMatchConfigurer -import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry -import org.springframework.web.servlet.config.annotation.ViewControllerRegistry -import org.springframework.web.servlet.config.annotation.ViewResolverRegistry -import org.springframework.web.servlet.config.annotation.WebMvcConfigurer - -import java.nio.charset.StandardCharsets - -@SpringBootApplication -class FilteredAppConfig implements WebMvcConfigurer { - - @Override - void configurePathMatch(PathMatchConfigurer configurer) {} - - @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] - } - }) - } - - @Override - void configureAsyncSupport(AsyncSupportConfigurer configurer) {} - - @Override - void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {} - - @Override - void addFormatters(FormatterRegistry registry) {} - - @Override - void addInterceptors(InterceptorRegistry registry) {} - - @Override - void addResourceHandlers(ResourceHandlerRegistry registry) {} - - @Override - void addCorsMappings(CorsRegistry registry) {} - - @Override - void addViewControllers(ViewControllerRegistry registry) {} - - @Override - void configureViewResolvers(ViewResolverRegistry registry) {} - - @Override - void addArgumentResolvers(List argumentResolvers) {} - - @Override - void addReturnValueHandlers(List returnValueHandlers) {} - - @Override - void configureMessageConverters(List> converters) {} - - @Override - void extendMessageConverters(List> converters) {} - - @Override - void configureHandlerExceptionResolvers(List exceptionResolvers) {} - - @Override - void extendHandlerExceptionResolvers(List exceptionResolvers) {} - - @Override - Validator getValidator() { - return null - } - - @Override - MessageCodesResolver getMessageCodesResolver() { - return null - } - - @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") as String, StandardCharsets.UTF_8, outputMessage.getBody()) - } - } - } -} diff --git a/instrumentation/spring/spring-webmvc/spring-webmvc-common/testing/src/main/groovy/filter/TestController.groovy b/instrumentation/spring/spring-webmvc/spring-webmvc-common/testing/src/main/groovy/filter/TestController.groovy deleted file mode 100644 index 211d7c125487..000000000000 --- a/instrumentation/spring/spring-webmvc/spring-webmvc-common/testing/src/main/groovy/filter/TestController.groovy +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package 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") - } - - @RequestMapping("/captureHeaders") - ResponseEntity capture_headers() { - throw new Exception("This should not be called") - } - - @RequestMapping("/child") - @ResponseBody - ResponseEntity indexed_child(@RequestParam("id") String id) { - throw new Exception("This should not be called") - } - - - @ExceptionHandler - ResponseEntity handleException(Throwable throwable) { - new ResponseEntity(throwable.message, HttpStatus.INTERNAL_SERVER_ERROR) - } -} diff --git a/instrumentation/spring/spring-webmvc/spring-webmvc-common/testing/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/boot/AbstractSpringBootBasedTest.java b/instrumentation/spring/spring-webmvc/spring-webmvc-common/testing/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/boot/AbstractSpringBootBasedTest.java new file mode 100644 index 000000000000..0147d8f672bc --- /dev/null +++ b/instrumentation/spring/spring-webmvc/spring-webmvc-common/testing/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/boot/AbstractSpringBootBasedTest.java @@ -0,0 +1,229 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.webmvc.boot; + +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.AUTH_ERROR; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.CAPTURE_HEADERS; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.EXCEPTION; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.INDEXED_CHILD; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.LOGIN; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.NOT_FOUND; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.PATH_PARAM; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.QUERY_PARAM; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.REDIRECT; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.satisfies; +import static io.opentelemetry.semconv.ExceptionAttributes.EXCEPTION_MESSAGE; +import static io.opentelemetry.semconv.ExceptionAttributes.EXCEPTION_STACKTRACE; +import static io.opentelemetry.semconv.ExceptionAttributes.EXCEPTION_TYPE; +import static io.opentelemetry.semconv.incubating.CodeIncubatingAttributes.CODE_FUNCTION; +import static io.opentelemetry.semconv.incubating.CodeIncubatingAttributes.CODE_NAMESPACE; +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.SpanKind; +import io.opentelemetry.instrumentation.api.internal.HttpConstants; +import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpServerTest; +import io.opentelemetry.instrumentation.testing.junit.http.HttpServerTestOptions; +import io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint; +import io.opentelemetry.sdk.testing.assertj.SpanDataAssert; +import io.opentelemetry.sdk.trace.data.StatusData; +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.MediaType; +import io.opentelemetry.testing.internal.armeria.common.QueryParams; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.function.Consumer; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.security.web.util.OnCommittedResponseWrapper; +import org.springframework.web.servlet.view.RedirectView; + +public abstract class AbstractSpringBootBasedTest + extends AbstractHttpServerTest { + + protected abstract ConfigurableApplicationContext context(); + + protected abstract Class securityConfigClass(); + + @Override + protected void stopServer(ConfigurableApplicationContext ctx) { + ctx.close(); + } + + @Override + protected void configure(HttpServerTestOptions options) { + super.configure(options); + options.setContextPath("/xyz"); + options.setHasHandlerSpan(unused -> true); + options.setHasResponseSpan(endpoint -> endpoint == REDIRECT || endpoint == NOT_FOUND); + options.setTestPathParam(true); + options.setHasErrorPageSpans(endpoint -> endpoint == NOT_FOUND); + options.setHasRenderSpan(endpoint -> endpoint == REDIRECT); + } + + @Override + public String expectedHttpRoute(ServerEndpoint endpoint, String method) { + if (HttpConstants._OTHER.equals(method)) { + return getContextPath() + endpoint.getPath(); + } + switch (endpoint.name()) { + case "PATH_PARAM": + return getContextPath() + "/path/{id}/param"; + case "NOT_FOUND": + return getContextPath() + "/**"; + case "LOGIN": + return getContextPath() + "/*"; + default: + return super.expectedHttpRoute(endpoint, method); + } + } + + @Test + void testSpansWithAuthError() { + SavingAuthenticationProvider authProvider = + context().getBean(SavingAuthenticationProvider.class); + AggregatedHttpRequest request = request(AUTH_ERROR, "GET"); + + authProvider.latestAuthentications.clear(); + AggregatedHttpResponse response = client.execute(request).aggregate().join(); + + assertThat(response.status().code()).isEqualTo(401); // not secured + + testing() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> assertServerSpan(span, "GET", AUTH_ERROR, AUTH_ERROR.getStatus()), + span -> + span.satisfies( + spanData -> assertThat(spanData.getName()).endsWith(".sendError")) + .hasKind(SpanKind.INTERNAL), + span -> errorPageSpanAssertions(null, null))); + } + + @ParameterizedTest + @ValueSource(strings = {"password", "dfsdföääöüüä", "🤓"}) + void testCharacterEncodingOfTestPassword(String testPassword) { + SavingAuthenticationProvider authProvider = + context().getBean(SavingAuthenticationProvider.class); + + QueryParams form = QueryParams.of("username", "test", "password", testPassword); + AggregatedHttpRequest request = + AggregatedHttpRequest.of( + request(LOGIN, "POST").headers().toBuilder().contentType(MediaType.FORM_DATA).build(), + HttpData.ofUtf8(form.toQueryString())); + + authProvider.latestAuthentications.clear(); + AggregatedHttpResponse response = client.execute(request).aggregate().join(); + + assertThat(response.status().code()).isEqualTo(302); // redirect after success + assertThat(authProvider.latestAuthentications.get(0).getPassword()).isEqualTo(testPassword); + + testing() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> assertServerSpan(span, "POST", LOGIN, LOGIN.getStatus()), + span -> + span.satisfies( + spanData -> + assertThat(spanData.getName()).endsWith(".sendRedirect")) + .hasKind(SpanKind.INTERNAL))); + } + + @Override + protected List> errorPageSpanAssertions( + String method, ServerEndpoint endpoint) { + List> spanAssertions = new ArrayList<>(); + spanAssertions.add( + span -> + span.hasName("BasicErrorController.error") + .hasKind(SpanKind.INTERNAL) + .hasAttributesSatisfying(Attributes::isEmpty)); + return spanAssertions; + } + + @Override + protected SpanDataAssert assertResponseSpan( + SpanDataAssert span, String method, ServerEndpoint endpoint) { + String methodName = endpoint == NOT_FOUND ? "sendError" : "sendRedirect"; + if (endpoint == NOT_FOUND) { + span.satisfies(spanData -> assertThat(spanData.getName()).endsWith(".sendError")); + } else { + span.satisfies(spanData -> assertThat(spanData.getName()).endsWith(".sendRedirect")); + } + + span.hasKind(SpanKind.INTERNAL) + .hasAttributesSatisfyingExactly( + satisfies( + CODE_NAMESPACE, + val -> + val.satisfiesAnyOf( + v -> assertThat(v).isEqualTo(OnCommittedResponseWrapper.class.getName()), + v -> + assertThat(v) + .isEqualTo( + "org.springframework.security.web.firewall.FirewalledResponse"), + v -> + assertThat(v) + .isEqualTo("jakarta.servlet.http.HttpServletResponseWrapper"))), + equalTo(CODE_FUNCTION, methodName)); + return span; + } + + @Override + protected SpanDataAssert assertRenderSpan( + SpanDataAssert span, String method, ServerEndpoint endpoint) { + span.hasName("Render RedirectView") + .hasKind(SpanKind.INTERNAL) + .hasAttributesSatisfyingExactly( + equalTo( + AttributeKey.stringKey("spring-webmvc.view.type"), RedirectView.class.getName())); + return span; + } + + @Override + protected SpanDataAssert assertHandlerSpan( + SpanDataAssert span, String method, ServerEndpoint endpoint) { + String handlerSpanName = getHandlerSpanName(endpoint); + if (endpoint == NOT_FOUND) { + handlerSpanName = "ResourceHttpRequestHandler.handleRequest"; + } + span.hasName(handlerSpanName).hasKind(SpanKind.INTERNAL); + if (endpoint == EXCEPTION) { + span.hasStatus(StatusData.error()); + span.hasEventsSatisfyingExactly( + event -> + event + .hasName("exception") + .hasAttributesSatisfyingExactly( + equalTo(EXCEPTION_TYPE, "java.lang.RuntimeException"), + equalTo(EXCEPTION_MESSAGE, EXCEPTION.getBody()), + satisfies(EXCEPTION_STACKTRACE, val -> val.isInstanceOf(String.class)))); + } + return span; + } + + private static String getHandlerSpanName(ServerEndpoint endpoint) { + if (QUERY_PARAM.equals(endpoint)) { + return "TestController.queryParam"; + } else if (PATH_PARAM.equals(endpoint)) { + return "TestController.pathParam"; + } else if (CAPTURE_HEADERS.equals(endpoint)) { + return "TestController.captureHeaders"; + } else if (INDEXED_CHILD.equals(endpoint)) { + return "TestController.indexedChild"; + } + return "TestController." + endpoint.name().toLowerCase(Locale.ROOT); + } +} diff --git a/instrumentation/spring/spring-webmvc/spring-webmvc-common/testing/src/main/groovy/boot/AppConfig.groovy b/instrumentation/spring/spring-webmvc/spring-webmvc-common/testing/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/boot/AppConfig.java similarity index 65% rename from instrumentation/spring/spring-webmvc/spring-webmvc-common/testing/src/main/groovy/boot/AppConfig.groovy rename to instrumentation/spring/spring-webmvc/spring-webmvc-common/testing/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/boot/AppConfig.java index 48563b125e7b..a63e9b7d9187 100644 --- a/instrumentation/spring/spring-webmvc/spring-webmvc-common/testing/src/main/groovy/boot/AppConfig.groovy +++ b/instrumentation/spring/spring-webmvc/spring-webmvc-common/testing/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/boot/AppConfig.java @@ -3,11 +3,9 @@ * SPDX-License-Identifier: Apache-2.0 */ -package boot +package io.opentelemetry.instrumentation.spring.webmvc.boot; -import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication -class AppConfig { - -} +public class AppConfig {} diff --git a/instrumentation/spring/spring-webmvc/spring-webmvc-common/testing/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/boot/SavingAuthenticationProvider.java b/instrumentation/spring/spring-webmvc/spring-webmvc-common/testing/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/boot/SavingAuthenticationProvider.java new file mode 100644 index 000000000000..aeba91c1cf7f --- /dev/null +++ b/instrumentation/spring/spring-webmvc/spring-webmvc-common/testing/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/boot/SavingAuthenticationProvider.java @@ -0,0 +1,34 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.webmvc.boot; + +import java.util.ArrayList; +import java.util.List; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider; +import org.springframework.security.core.userdetails.UserDetails; + +public class SavingAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider { + + List latestAuthentications = new ArrayList<>(); + + @Override + protected void additionalAuthenticationChecks( + UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) { + // none + } + + @Override + protected UserDetails retrieveUser( + String username, UsernamePasswordAuthenticationToken authentication) { + TestUserDetails details = + new TestUserDetails(username, authentication.getCredentials().toString()); + + latestAuthentications.add(details); + + return details; + } +} diff --git a/instrumentation/spring/spring-webmvc/spring-webmvc-common/testing/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/boot/TestController.java b/instrumentation/spring/spring-webmvc/spring-webmvc-common/testing/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/boot/TestController.java new file mode 100644 index 000000000000..ffc8c3e26e0e --- /dev/null +++ b/instrumentation/spring/spring-webmvc/spring-webmvc-common/testing/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/boot/TestController.java @@ -0,0 +1,108 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.webmvc.boot; + +import static io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpServerTest.controller; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.CAPTURE_HEADERS; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.ERROR; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.EXCEPTION; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.INDEXED_CHILD; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.PATH_PARAM; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.QUERY_PARAM; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.REDIRECT; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.SUCCESS; + +import java.util.Objects; +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.RequestHeader; +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 +public class TestController { + + @RequestMapping("/basicsecured/endpoint") + @ResponseBody + String secureEndpoint() { + return controller(SUCCESS, SUCCESS::getBody); + } + + @RequestMapping("/success") + @ResponseBody + String success() { + return controller(SUCCESS, SUCCESS::getBody); + } + + @RequestMapping("/query") + @ResponseBody + String queryParam(@RequestParam("some") String param) { + return controller(QUERY_PARAM, () -> "some=" + param); + } + + @RequestMapping("/redirect") + @ResponseBody + RedirectView redirect() { + return controller(REDIRECT, () -> new RedirectView(REDIRECT.getBody())); + } + + @RequestMapping("/error-status") + ResponseEntity error() { + return controller( + ERROR, + () -> + ResponseEntity.status(HttpStatus.valueOf(ERROR.getStatus()).value()) + .body(ERROR.getBody())); + } + + @SuppressWarnings("ThrowSpecificExceptions") + @RequestMapping("/exception") + ResponseEntity exception() { + return controller( + EXCEPTION, + () -> { + throw new RuntimeException(EXCEPTION.getBody()); + }); + } + + @RequestMapping("/captureHeaders") + ResponseEntity captureHeaders(@RequestHeader("X-Test-Request") String testRequestHeader) { + return controller( + CAPTURE_HEADERS, + () -> + ResponseEntity.ok() + .header("X-Test-Response", testRequestHeader) + .body(CAPTURE_HEADERS.getBody())); + } + + @RequestMapping("/path/{id}/param") + @ResponseBody + String pathParam(@PathVariable("id") int id) { + return controller(PATH_PARAM, () -> String.valueOf(id)); + } + + @RequestMapping("/child") + @ResponseBody + String indexedChild(@RequestParam("id") String id) { + return controller( + INDEXED_CHILD, + () -> { + INDEXED_CHILD.collectSpanAttributes(it -> Objects.equals(it, "id") ? id : null); + return INDEXED_CHILD.getBody(); + }); + } + + @ExceptionHandler + ResponseEntity handleException(Throwable throwable) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR.value()) + .body(throwable.getMessage()); + } +} diff --git a/instrumentation/spring/spring-webmvc/spring-webmvc-common/testing/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/boot/TestUserDetails.java b/instrumentation/spring/spring-webmvc/spring-webmvc-common/testing/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/boot/TestUserDetails.java new file mode 100644 index 000000000000..b36d96346ccc --- /dev/null +++ b/instrumentation/spring/spring-webmvc/spring-webmvc-common/testing/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/boot/TestUserDetails.java @@ -0,0 +1,58 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.webmvc.boot; + +import java.util.Collection; +import java.util.Collections; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +public class TestUserDetails implements UserDetails { + + private static final long serialVersionUID = 6470776949615799570L; + private final String username; + private final String password; + + TestUserDetails(String username, String password) { + this.username = username; + this.password = password; + } + + @Override + public Collection getAuthorities() { + return Collections.emptySet(); + } + + @Override + public String getPassword() { + return password; + } + + @Override + public String getUsername() { + return username; + } + + @Override + public boolean isAccountNonExpired() { + return true; + } + + @Override + public boolean isAccountNonLocked() { + return true; + } + + @Override + public boolean isCredentialsNonExpired() { + return true; + } + + @Override + public boolean isEnabled() { + return true; + } +} diff --git a/instrumentation/spring/spring-webmvc/spring-webmvc-common/testing/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/filter/AbstractServletFilterTest.java b/instrumentation/spring/spring-webmvc/spring-webmvc-common/testing/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/filter/AbstractServletFilterTest.java new file mode 100644 index 000000000000..79a6d4275df9 --- /dev/null +++ b/instrumentation/spring/spring-webmvc/spring-webmvc-common/testing/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/filter/AbstractServletFilterTest.java @@ -0,0 +1,116 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.webmvc.filter; + +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.CAPTURE_HEADERS; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.ERROR; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.EXCEPTION; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.INDEXED_CHILD; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.NOT_FOUND; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.PATH_PARAM; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.QUERY_PARAM; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.REDIRECT; +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.api.internal.HttpConstants; +import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpServerTest; +import io.opentelemetry.instrumentation.testing.junit.http.HttpServerTestOptions; +import io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint; +import io.opentelemetry.sdk.testing.assertj.SpanDataAssert; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.function.Consumer; +import org.springframework.context.ConfigurableApplicationContext; + +public abstract class AbstractServletFilterTest + extends AbstractHttpServerTest { + + protected abstract Class securityConfigClass(); + + protected abstract Class filterConfigClass(); + + @Override + protected void stopServer(ConfigurableApplicationContext ctx) { + ctx.close(); + } + + @Override + protected void configure(HttpServerTestOptions options) { + super.configure(options); + options.setHasHandlerSpan(endpoint -> endpoint == NOT_FOUND); + options.setHasErrorPageSpans( + endpoint -> endpoint == ERROR || endpoint == EXCEPTION || endpoint == NOT_FOUND); + options.setHasResponseSpan( + endpoint -> endpoint == REDIRECT || endpoint == ERROR || endpoint == NOT_FOUND); + options.setTestPathParam(true); + } + + @Override + protected SpanDataAssert assertResponseSpan( + SpanDataAssert span, String method, ServerEndpoint endpoint) { + if (endpoint == REDIRECT) { + span.satisfies(spanData -> assertThat(spanData.getName()).endsWith(".sendRedirect")); + } else if (endpoint == ERROR || endpoint == NOT_FOUND) { + span.satisfies(spanData -> assertThat(spanData.getName()).endsWith(".sendError")); + } + span.hasKind(SpanKind.INTERNAL); + return span; + } + + @Override + protected SpanDataAssert assertHandlerSpan( + SpanDataAssert span, String method, ServerEndpoint endpoint) { + String handlerSpanName = getHandlerSpanName(endpoint); + if (endpoint == NOT_FOUND) { + handlerSpanName = "ResourceHttpRequestHandler.handleRequest"; + } + span.hasName(handlerSpanName).hasKind(SpanKind.INTERNAL); + return span; + } + + @Override + public String expectedHttpRoute(ServerEndpoint endpoint, String method) { + if (HttpConstants._OTHER.equals(method)) { + return getContextPath() + endpoint.getPath(); + } + switch (endpoint.name()) { + case "PATH_PARAM": + return getContextPath() + "/path/{id}/param"; + case "NOT_FOUND": + return getContextPath() + "/**"; + default: + return super.expectedHttpRoute(endpoint, method); + } + } + + @Override + protected List> errorPageSpanAssertions( + String method, ServerEndpoint endpoint) { + List> spanAssertions = new ArrayList<>(); + spanAssertions.add( + span -> + span.hasName("BasicErrorController.error") + .hasKind(SpanKind.INTERNAL) + .hasAttributesSatisfying(Attributes::isEmpty)); + return spanAssertions; + } + + private static String getHandlerSpanName(ServerEndpoint endpoint) { + if (QUERY_PARAM.equals(endpoint)) { + return "TestController.queryParam"; + } else if (PATH_PARAM.equals(endpoint)) { + return "TestController.pathParam"; + } else if (CAPTURE_HEADERS.equals(endpoint)) { + return "TestController.captureHeaders"; + } else if (INDEXED_CHILD.equals(endpoint)) { + return "TestController.indexedChild"; + } + return "TestController." + endpoint.name().toLowerCase(Locale.ROOT); + } +} diff --git a/instrumentation/spring/spring-webmvc/spring-webmvc-common/testing/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/filter/FilteredAppConfig.java b/instrumentation/spring/spring-webmvc/spring-webmvc-common/testing/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/filter/FilteredAppConfig.java new file mode 100644 index 000000000000..11f147632a72 --- /dev/null +++ b/instrumentation/spring/spring-webmvc/spring-webmvc-common/testing/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/filter/FilteredAppConfig.java @@ -0,0 +1,135 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.webmvc.filter; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import javax.annotation.Nullable; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; +import org.springframework.format.FormatterRegistry; +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.util.StreamUtils; +import org.springframework.validation.MessageCodesResolver; +import org.springframework.validation.Validator; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.HandlerMethodReturnValueHandler; +import org.springframework.web.servlet.HandlerExceptionResolver; +import org.springframework.web.servlet.config.annotation.AsyncSupportConfigurer; +import org.springframework.web.servlet.config.annotation.ContentNegotiationConfigurer; +import org.springframework.web.servlet.config.annotation.CorsRegistry; +import org.springframework.web.servlet.config.annotation.DefaultServletHandlerConfigurer; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.PathMatchConfigurer; +import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; +import org.springframework.web.servlet.config.annotation.ViewControllerRegistry; +import org.springframework.web.servlet.config.annotation.ViewResolverRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@SpringBootApplication +public class FilteredAppConfig implements WebMvcConfigurer { + + @Override + public void configurePathMatch(PathMatchConfigurer configurer) {} + + @Override + public void configureContentNegotiation(ContentNegotiationConfigurer configurer) { + configurer + .favorPathExtension(false) + .favorParameter(true) + .ignoreAcceptHeader(true) + .useJaf(false) + .defaultContentTypeStrategy(webRequest -> Collections.singletonList(MediaType.TEXT_PLAIN)); + } + + @Override + public void configureAsyncSupport(AsyncSupportConfigurer configurer) {} + + @Override + public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {} + + @Override + public void addFormatters(FormatterRegistry registry) {} + + @Override + public void addInterceptors(InterceptorRegistry registry) {} + + @Override + public void addResourceHandlers(ResourceHandlerRegistry registry) {} + + @Override + public void addCorsMappings(CorsRegistry registry) {} + + @Override + public void addViewControllers(ViewControllerRegistry registry) {} + + @Override + public void configureViewResolvers(ViewResolverRegistry registry) {} + + @Override + public void addArgumentResolvers(List argumentResolvers) {} + + @Override + public void addReturnValueHandlers(List returnValueHandlers) {} + + @Override + public void configureMessageConverters(List> converters) {} + + @Override + public void extendMessageConverters(List> converters) {} + + @Override + public void configureHandlerExceptionResolvers( + List exceptionResolvers) {} + + @Override + public void extendHandlerExceptionResolvers(List exceptionResolvers) {} + + @Override + public Validator getValidator() { + return null; + } + + @Override + public MessageCodesResolver getMessageCodesResolver() { + return null; + } + + @Bean + HttpMessageConverter> createPlainMapMessageConverter() { + + return new AbstractHttpMessageConverter>(MediaType.TEXT_PLAIN) { + + @Override + protected boolean supports(Class clazz) { + return Map.class.isAssignableFrom(clazz); + } + + @Nullable + @Override + protected Map readInternal( + Class> clazz, HttpInputMessage inputMessage) { + return null; + } + + @Override + protected void writeInternal( + Map stringObjectMap, HttpOutputMessage outputMessage) throws IOException { + StreamUtils.copy( + (String) stringObjectMap.get("message"), + StandardCharsets.UTF_8, + outputMessage.getBody()); + } + }; + } +} diff --git a/instrumentation/spring/spring-webmvc/spring-webmvc-common/testing/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/filter/TestController.java b/instrumentation/spring/spring-webmvc/spring-webmvc-common/testing/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/filter/TestController.java new file mode 100644 index 000000000000..60b95688289e --- /dev/null +++ b/instrumentation/spring/spring-webmvc/spring-webmvc-common/testing/src/main/java/io/opentelemetry/instrumentation/spring/webmvc/filter/TestController.java @@ -0,0 +1,74 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.webmvc.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 +public class TestController { + + @RequestMapping("/success") + @ResponseBody + String success() throws Exception { + throw new Exception("This should not be called"); + } + + @RequestMapping("/query") + @ResponseBody + String queryParam(@RequestParam("some") String param) throws Exception { + throw new Exception("This should not be called"); + } + + @RequestMapping("/path/{id}/param") + @ResponseBody + String pathParam(@PathVariable Integer id) throws Exception { + throw new Exception("This should not be called"); + } + + @RequestMapping("/redirect") + @ResponseBody + RedirectView redirect() throws Exception { + throw new Exception("This should not be called"); + } + + @RequestMapping("/error-status") + ResponseEntity error() throws Exception { + throw new Exception("This should not be called"); + } + + @RequestMapping("/exception") + ResponseEntity exception() throws Exception { + throw new Exception("This should not be called"); + } + + @RequestMapping("/captureHeaders") + ResponseEntity captureHeaders() throws Exception { + throw new Exception("This should not be called"); + } + + @RequestMapping("/child") + @ResponseBody + ResponseEntity indexedChild(@RequestParam("id") String id) throws Exception { + throw new Exception("This should not be called"); + } + + @ExceptionHandler + ResponseEntity handleException(Throwable throwable) { + return new ResponseEntity<>(throwable.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR); + } +} diff --git a/instrumentation/spring/spring-ws-2.0/javaagent/build.gradle.kts b/instrumentation/spring/spring-ws-2.0/javaagent/build.gradle.kts index d7dcde12fec6..b89aa92c3042 100644 --- a/instrumentation/spring/spring-ws-2.0/javaagent/build.gradle.kts +++ b/instrumentation/spring/spring-ws-2.0/javaagent/build.gradle.kts @@ -54,6 +54,7 @@ tasks.withType().configureEach { // required on jdk17 jvmArgs("--add-opens=java.base/java.lang=ALL-UNNAMED") jvmArgs("-XX:+IgnoreUnrecognizedVMOptions") + jvmArgs("-Dotel.instrumentation.common.experimental.controller-telemetry.enabled=true") } configurations.testRuntimeClasspath { diff --git a/instrumentation/spring/spring-ws-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/ws/v2_0/SpringWsCodeAttributesGetter.java b/instrumentation/spring/spring-ws-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/ws/v2_0/SpringWsCodeAttributesGetter.java index 27061395b1dd..6439f1aab579 100644 --- a/instrumentation/spring/spring-ws-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/ws/v2_0/SpringWsCodeAttributesGetter.java +++ b/instrumentation/spring/spring-ws-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/ws/v2_0/SpringWsCodeAttributesGetter.java @@ -5,7 +5,7 @@ package io.opentelemetry.javaagent.instrumentation.spring.ws.v2_0; -import io.opentelemetry.instrumentation.api.instrumenter.code.CodeAttributesGetter; +import io.opentelemetry.instrumentation.api.incubator.semconv.code.CodeAttributesGetter; public class SpringWsCodeAttributesGetter implements CodeAttributesGetter { diff --git a/instrumentation/spring/spring-ws-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/ws/v2_0/SpringWsInstrumentationModule.java b/instrumentation/spring/spring-ws-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/ws/v2_0/SpringWsInstrumentationModule.java index 08382bdb98bb..5b766d79f0ca 100644 --- a/instrumentation/spring/spring-ws-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/ws/v2_0/SpringWsInstrumentationModule.java +++ b/instrumentation/spring/spring-ws-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/ws/v2_0/SpringWsInstrumentationModule.java @@ -6,8 +6,10 @@ package io.opentelemetry.javaagent.instrumentation.spring.ws.v2_0; import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.bootstrap.internal.ExperimentalConfig; import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; import java.util.Collections; import java.util.List; @@ -21,4 +23,10 @@ public SpringWsInstrumentationModule() { public List typeInstrumentations() { return Collections.singletonList(new AnnotatedMethodInstrumentation()); } + + @Override + public boolean defaultEnabled(ConfigProperties config) { + // this instrumentation only produces controller telemetry + return super.defaultEnabled(config) && ExperimentalConfig.get().controllerTelemetryEnabled(); + } } diff --git a/instrumentation/spring/spring-ws-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/ws/v2_0/SpringWsSingletons.java b/instrumentation/spring/spring-ws-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/ws/v2_0/SpringWsSingletons.java index 4d74f395aeab..b21b1184675b 100644 --- a/instrumentation/spring/spring-ws-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/ws/v2_0/SpringWsSingletons.java +++ b/instrumentation/spring/spring-ws-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/ws/v2_0/SpringWsSingletons.java @@ -6,9 +6,9 @@ package io.opentelemetry.javaagent.instrumentation.spring.ws.v2_0; import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.instrumentation.api.incubator.semconv.code.CodeAttributesExtractor; +import io.opentelemetry.instrumentation.api.incubator.semconv.code.CodeSpanNameExtractor; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; -import io.opentelemetry.instrumentation.api.instrumenter.code.CodeAttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.code.CodeSpanNameExtractor; import io.opentelemetry.javaagent.bootstrap.internal.ExperimentalConfig; public class SpringWsSingletons { diff --git a/instrumentation/spring/spring-ws-2.0/javaagent/src/test/groovy/test/boot/HelloEndpoint.groovy b/instrumentation/spring/spring-ws-2.0/javaagent/src/test/groovy/test/boot/HelloEndpoint.groovy deleted file mode 100644 index 0c040cc85c75..000000000000 --- a/instrumentation/spring/spring-ws-2.0/javaagent/src/test/groovy/test/boot/HelloEndpoint.groovy +++ /dev/null @@ -1,50 +0,0 @@ -/* - * 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/instrumentation/spring/spring-ws-2.0/javaagent/src/test/groovy/test/boot/SpringWsTest.groovy b/instrumentation/spring/spring-ws-2.0/javaagent/src/test/groovy/test/boot/SpringWsTest.groovy deleted file mode 100644 index 0e3942a0d2e6..000000000000 --- a/instrumentation/spring/spring-ws-2.0/javaagent/src/test/groovy/test/boot/SpringWsTest.groovy +++ /dev/null @@ -1,160 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package test.boot - -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 - -import static io.opentelemetry.api.trace.SpanKind.INTERNAL -import static io.opentelemetry.api.trace.StatusCode.ERROR - -class SpringWsTest extends AgentInstrumentationSpecification implements HttpServerTestTrait { - - @Shared - private Jaxb2Marshaller marshaller = new Jaxb2Marshaller() - - def setupSpec() { - setupServer() - - marshaller.setPackagesToScan(ClassUtils.getPackageName(HelloRequest)) - marshaller.afterPropertiesSet() - } - - def cleanupSpec() { - cleanupServer() - } - - @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 route, Throwable exception = null) { - trace.span(index) { - hasNoParent() - name "POST " + route - 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" "test.boot.HelloEndpoint" - "$SemanticAttributes.CODE_FUNCTION" methodName - } - } - } -} diff --git a/instrumentation/spring/spring-ws-2.0/javaagent/src/test/groovy/test/boot/WebServiceConfig.groovy b/instrumentation/spring/spring-ws-2.0/javaagent/src/test/groovy/test/boot/WebServiceConfig.groovy deleted file mode 100644 index 13e9b1833302..000000000000 --- a/instrumentation/spring/spring-ws-2.0/javaagent/src/test/groovy/test/boot/WebServiceConfig.groovy +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package test.boot - -import org.springframework.boot.web.servlet.ServletRegistrationBean -import org.springframework.context.ApplicationContext -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/instrumentation/spring/spring-ws-2.0/javaagent/src/test/groovy/test/boot/AppConfig.groovy b/instrumentation/spring/spring-ws-2.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/ws/v2_0/AppConfig.java similarity index 63% rename from instrumentation/spring/spring-ws-2.0/javaagent/src/test/groovy/test/boot/AppConfig.groovy rename to instrumentation/spring/spring-ws-2.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/ws/v2_0/AppConfig.java index e9ddd37cdb59..2ffa4dd640cb 100644 --- a/instrumentation/spring/spring-ws-2.0/javaagent/src/test/groovy/test/boot/AppConfig.groovy +++ b/instrumentation/spring/spring-ws-2.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/ws/v2_0/AppConfig.java @@ -3,12 +3,10 @@ * SPDX-License-Identifier: Apache-2.0 */ -package test.boot +package io.opentelemetry.javaagent.instrumentation.spring.ws.v2_0; -import org.springframework.boot.autoconfigure.SpringBootApplication -import org.springframework.web.servlet.config.annotation.WebMvcConfigurer +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @SpringBootApplication -class AppConfig implements WebMvcConfigurer { - -} +public class AppConfig implements WebMvcConfigurer {} diff --git a/instrumentation/spring/spring-ws-2.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/ws/v2_0/HelloEndpoint.java b/instrumentation/spring/spring-ws-2.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/ws/v2_0/HelloEndpoint.java new file mode 100644 index 000000000000..0fdf3be509c5 --- /dev/null +++ b/instrumentation/spring/spring-ws-2.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/ws/v2_0/HelloEndpoint.java @@ -0,0 +1,52 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.ws.v2_0; + +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 +public class HelloEndpoint { + + private static final String NAMESPACE_URI = "http://opentelemetry.io/test/hello-web-service"; + + @PayloadRoot(namespace = NAMESPACE_URI, localPart = "helloRequest") + @ResponsePayload + public HelloResponse hello(@RequestPayload HelloRequest request) throws Exception { + return handleHello(request.getName()); + } + + @SoapAction(value = "http://opentelemetry.io/test/hello-soap-action") + @ResponsePayload + public HelloResponse helloSoapAction(@RequestPayload HelloRequestSoapAction request) + throws Exception { + return handleHello(request.getName()); + } + + @Action(value = "http://opentelemetry.io/test/hello-ws-action") + @ResponsePayload + public HelloResponse helloWsAction(@RequestPayload HelloRequestWsAction request) + throws Exception { + return handleHello(request.getName()); + } + + private static HelloResponse handleHello(String name) throws Exception { + if ("exception".equals(name)) { + throw new Exception("hello exception"); + } + HelloResponse response = new HelloResponse(); + response.setMessage("Hello " + name); + return response; + } +} diff --git a/instrumentation/spring/spring-ws-2.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/ws/v2_0/SpringWsTest.java b/instrumentation/spring/spring-ws-2.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/ws/v2_0/SpringWsTest.java new file mode 100644 index 000000000000..381549a020b5 --- /dev/null +++ b/instrumentation/spring/spring-ws-2.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/ws/v2_0/SpringWsTest.java @@ -0,0 +1,179 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.ws.v2_0; + +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.satisfies; +import static io.opentelemetry.semconv.ExceptionAttributes.EXCEPTION_MESSAGE; +import static io.opentelemetry.semconv.ExceptionAttributes.EXCEPTION_STACKTRACE; +import static io.opentelemetry.semconv.ExceptionAttributes.EXCEPTION_TYPE; +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.api.trace.SpanKind; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpServerUsingTest; +import io.opentelemetry.instrumentation.testing.junit.http.HttpServerInstrumentationExtension; +import io.opentelemetry.sdk.trace.data.StatusData; +import io.opentelemetry.semconv.incubating.CodeIncubatingAttributes; +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 java.net.URISyntaxException; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +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; + +class SpringWsTest extends AbstractHttpServerUsingTest { + + @RegisterExtension + private static final InstrumentationExtension testing = + HttpServerInstrumentationExtension.forAgent(); + + private static final Jaxb2Marshaller marshaller = new Jaxb2Marshaller(); + + @BeforeAll + void setup() { + startServer(); + } + + @AfterAll + void cleanup() { + cleanupServer(); + } + + @Override + protected ConfigurableApplicationContext setupServer() throws Exception { + SpringApplication app = new SpringApplication(AppConfig.class, WebServiceConfig.class); + app.setDefaultProperties( + ImmutableMap.of( + "server.port", + port, + "server.context-path", + getContextPath(), + "server.servlet.contextPath", + getContextPath(), + "server.error.include-message", + "always")); + marshaller.setPackagesToScan(ClassUtils.getPackageName(HelloRequest.class)); + marshaller.afterPropertiesSet(); + return app.run(); + } + + @Override + protected void stopServer(ConfigurableApplicationContext configurableApplicationContext) + throws Exception { + configurableApplicationContext.close(); + } + + @Override + protected String getContextPath() { + return "/xyz"; + } + + HelloResponse makeRequest(String methodName, String name) throws URISyntaxException { + WebServiceTemplate webServiceTemplate = new WebServiceTemplate(marshaller); + + Object request; + WebServiceMessageCallback callback = null; + if ("hello".equals(methodName)) { + HelloRequest req = new HelloRequest(); + req.setName(name); + request = req; + } else if ("helloSoapAction".equals(methodName)) { + HelloRequestSoapAction req = new HelloRequestSoapAction(); + req.setName(name); + request = req; + callback = new SoapActionCallback("http://opentelemetry.io/test/hello-soap-action"); + } else if ("helloWsAction".equals(methodName)) { + HelloRequestWsAction req = new HelloRequestWsAction(); + req.setName(name); + request = req; + 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); + } + + @ParameterizedTest + @ValueSource(strings = {"hello", "helloSoapAction", "helloWsAction"}) + void testMethodName(String methodName) throws URISyntaxException { + HelloResponse response = makeRequest(methodName, "Test"); + + assertThat(response.getMessage()).isEqualTo("Hello Test"); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasNoParent().hasName("POST /xyz/ws/*").hasKind(SpanKind.SERVER), + span -> + span.hasParent(trace.getSpan(0)) + .hasName("HelloEndpoint." + methodName) + .hasKind(SpanKind.INTERNAL) + .hasAttributesSatisfyingExactly( + equalTo( + CodeIncubatingAttributes.CODE_NAMESPACE, + "io.opentelemetry.javaagent.instrumentation.spring.ws.v2_0.HelloEndpoint"), + equalTo(CodeIncubatingAttributes.CODE_FUNCTION, methodName)))); + } + + @ParameterizedTest + @ValueSource(strings = {"hello", "helloSoapAction", "helloWsAction"}) + void testMethodNameException(String methodName) { + assertThatThrownBy(() -> makeRequest(methodName, "exception")) + .isInstanceOf(SoapFaultClientException.class) + .hasMessage("hello exception"); + + Exception expectedException = new Exception("hello exception"); + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasNoParent() + .hasName("POST /xyz/ws/*") + .hasKind(SpanKind.SERVER) + .hasStatus(StatusData.error()), + span -> + span.hasParent(trace.getSpan(0)) + .hasName("HelloEndpoint." + methodName) + .hasKind(SpanKind.INTERNAL) + .hasStatus(StatusData.error()) + .hasEventsSatisfyingExactly( + event -> + event + .hasName("exception") + .hasAttributesSatisfyingExactly( + equalTo( + EXCEPTION_TYPE, + expectedException.getClass().getCanonicalName()), + equalTo(EXCEPTION_MESSAGE, expectedException.getMessage()), + satisfies( + EXCEPTION_STACKTRACE, + val -> val.isInstanceOf(String.class)))) + .hasAttributesSatisfyingExactly( + equalTo( + CodeIncubatingAttributes.CODE_NAMESPACE, + "io.opentelemetry.javaagent.instrumentation.spring.ws.v2_0.HelloEndpoint"), + equalTo(CodeIncubatingAttributes.CODE_FUNCTION, methodName)))); + } +} diff --git a/instrumentation/spring/spring-ws-2.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/ws/v2_0/WebServiceConfig.java b/instrumentation/spring/spring-ws-2.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/ws/v2_0/WebServiceConfig.java new file mode 100644 index 000000000000..61dca42176f5 --- /dev/null +++ b/instrumentation/spring/spring-ws-2.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/ws/v2_0/WebServiceConfig.java @@ -0,0 +1,47 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.ws.v2_0; + +import org.springframework.boot.web.servlet.ServletRegistrationBean; +import org.springframework.context.ApplicationContext; +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 +public 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 helloSchema) { + DefaultWsdl11Definition wsdl11Definition = new DefaultWsdl11Definition(); + wsdl11Definition.setPortTypeName("HelloPort"); + wsdl11Definition.setLocationUri("/ws"); + wsdl11Definition.setTargetNamespace("http://opentelemetry.io/test/hello-web-service"); + wsdl11Definition.setSchema(helloSchema); + return wsdl11Definition; + } + + @Bean + XsdSchema helloSchema() { + return new SimpleXsdSchema(new ClassPathResource("hello.xsd")); + } +} diff --git a/instrumentation/spring/starters/jaeger-spring-boot-starter/README.md b/instrumentation/spring/starters/jaeger-spring-boot-starter/README.md deleted file mode 100644 index 04e16f76d166..000000000000 --- a/instrumentation/spring/starters/jaeger-spring-boot-starter/README.md +++ /dev/null @@ -1,34 +0,0 @@ -# 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/main/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` - -#### Maven - -```xml - - - io.opentelemetry.instrumentation - opentelemetry-jaeger-spring-boot-starter - OPENTELEMETRY_VERSION - - -``` - -#### Gradle - -```groovy -implementation("io.opentelemetry.instrumentation:opentelemetry-spring-boot-exporter-starter:OPENTELEMETRY_VERSION") -``` - -### Starter Guide - -Check out [OpenTelemetry Manual Instrumentation](https://opentelemetry.io/docs/instrumentation/java/manual/) to learn more about -using the OpenTelemetry API to instrument your code. diff --git a/instrumentation/spring/starters/jaeger-spring-boot-starter/build.gradle.kts b/instrumentation/spring/starters/jaeger-spring-boot-starter/build.gradle.kts deleted file mode 100644 index c199378daa1f..000000000000 --- a/instrumentation/spring/starters/jaeger-spring-boot-starter/build.gradle.kts +++ /dev/null @@ -1,15 +0,0 @@ -plugins { - id("otel.java-conventions") - id("otel.publish-conventions") -} - -group = "io.opentelemetry.instrumentation" - -val versions: Map by project -val springBootVersion = versions["org.springframework.boot"] - -dependencies { - api("org.springframework.boot:spring-boot-starter:$springBootVersion") - api(project(":instrumentation:spring:starters:spring-boot-starter")) - api("io.opentelemetry:opentelemetry-exporter-jaeger") -} diff --git a/instrumentation/spring/starters/spring-boot-starter/README.md b/instrumentation/spring/starters/spring-boot-starter/README.md index aac33afe3d71..1ed1d8153e88 100644 --- a/instrumentation/spring/starters/spring-boot-starter/README.md +++ b/instrumentation/spring/starters/spring-boot-starter/README.md @@ -1,40 +1,3 @@ # 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` - -### Maven - -Add the following dependencies to your `pom.xml` file: - -```xml - - - io.opentelemetry.instrumentation - opentelemetry-spring-boot-starter - OPENTELEMETRY_VERSION - - -``` - -### Gradle - -Add the following dependencies to your gradle.build file: - -```groovy -implementation("io.opentelemetry.instrumentation:opentelemetry-spring-boot-starter:OPENTELEMETRY_VERSION") -``` - -### Starter Guide - -Check out [OpenTelemetry Manual Instrumentation](https://opentelemetry.io/docs/instrumentation/java/manual/) to learn more about -using the OpenTelemetry API to instrument your code. +Documentation of the OpenTelemetry Spring Starter is [here](https://opentelemetry.io/docs/zero-code/java/spring-boot/). diff --git a/instrumentation/spring/starters/spring-boot-starter/build.gradle.kts b/instrumentation/spring/starters/spring-boot-starter/build.gradle.kts index cd1b989913f0..849d7f26ca69 100644 --- a/instrumentation/spring/starters/spring-boot-starter/build.gradle.kts +++ b/instrumentation/spring/starters/spring-boot-starter/build.gradle.kts @@ -1,21 +1,27 @@ plugins { id("otel.java-conventions") id("otel.publish-conventions") + id("otel.japicmp-conventions") } group = "io.opentelemetry.instrumentation" -val versions: Map by project -val springBootVersion = versions["org.springframework.boot"] +val springBootVersion = "2.6.15" dependencies { - api("org.springframework.boot:spring-boot-starter:$springBootVersion") - api("org.springframework.boot:spring-boot-starter-aop:$springBootVersion") + compileOnly("org.springframework.boot:spring-boot-starter:$springBootVersion") + compileOnly("org.springframework.boot:spring-boot-starter-aop:$springBootVersion") api(project(":instrumentation:spring:spring-boot-autoconfigure")) + api(project(":instrumentation-annotations")) + implementation(project(":instrumentation:resources:library")) + implementation("io.opentelemetry:opentelemetry-sdk-extension-incubator") api("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure-spi") api("io.opentelemetry:opentelemetry-api") api("io.opentelemetry:opentelemetry-exporter-logging") api("io.opentelemetry:opentelemetry-exporter-otlp") api("io.opentelemetry:opentelemetry-sdk") - api(project(":instrumentation-annotations")) + + implementation("io.opentelemetry.contrib:opentelemetry-aws-resources") + implementation("io.opentelemetry.contrib:opentelemetry-gcp-resources") + implementation("io.opentelemetry.contrib:opentelemetry-baggage-processor") } diff --git a/instrumentation/spring/starters/spring-boot-starter/gradle.properties b/instrumentation/spring/starters/spring-boot-starter/gradle.properties new file mode 100644 index 000000000000..45d64bec279d --- /dev/null +++ b/instrumentation/spring/starters/spring-boot-starter/gradle.properties @@ -0,0 +1 @@ +otel.stable=true diff --git a/instrumentation/spring/starters/zipkin-spring-boot-starter/README.md b/instrumentation/spring/starters/zipkin-spring-boot-starter/README.md index 7286cdebaee6..3302d231331a 100644 --- a/instrumentation/spring/starters/zipkin-spring-boot-starter/README.md +++ b/instrumentation/spring/starters/zipkin-spring-boot-starter/README.md @@ -1,36 +1,7 @@ # 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. +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/main/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. +OpenTelemetry Zipkin Exporter Starter is a starter package that includes the opentelemetry-api, opentelemetry-sdk, opentelemetry-extension-annotations, opentelemetry-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/main/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` - -#### Maven - -```xml - - - io.opentelemetry.instrumentation - opentelemetry-zipkin-spring-boot-starter - OPENTELEMETRY_VERSION - - -``` - -#### Gradle - -```groovy -implementation("io.opentelemetry.instrumentation:opentelemetry-zipkin-spring-boot-starter:OPENTELEMETRY_VERSION") -``` - -### Starter Guide - -Check out [OpenTelemetry Manual Instrumentation](https://opentelemetry.io/docs/instrumentation/java/manual/) to learn more about -using the OpenTelemetry API to instrument your code. +Documentation for the OpenTelemetry Zipkin Exporter Starter can be found [here](https://opentelemetry.io/docs/zero-code/java/spring-boot/#zipkin-starter). diff --git a/instrumentation/spring/starters/zipkin-spring-boot-starter/build.gradle.kts b/instrumentation/spring/starters/zipkin-spring-boot-starter/build.gradle.kts index 9de0496a94bb..4d9a6256ea85 100644 --- a/instrumentation/spring/starters/zipkin-spring-boot-starter/build.gradle.kts +++ b/instrumentation/spring/starters/zipkin-spring-boot-starter/build.gradle.kts @@ -5,8 +5,7 @@ plugins { group = "io.opentelemetry.instrumentation" -val versions: Map by project -val springBootVersion = versions["org.springframework.boot"] +val springBootVersion = "2.6.15" dependencies { api("org.springframework.boot:spring-boot-starter:$springBootVersion") diff --git a/instrumentation/spymemcached-2.12/README.md b/instrumentation/spymemcached-2.12/README.md index 016c7f134605..a3c74c69a1ad 100644 --- a/instrumentation/spymemcached-2.12/README.md +++ b/instrumentation/spymemcached-2.12/README.md @@ -1,5 +1,5 @@ # Settings for the Spymemcached instrumentation -| System property | Type | Default | Description | -|---|---|---|---| +| System property | Type | Default | Description | +| ---------------------------------------------------------------- | ------- | ------- | ---------------------------------------------------- | | `otel.instrumentation.spymemcached.experimental-span-attributes` | Boolean | `false` | Enables the capture of experimental span attributes. | diff --git a/instrumentation/spymemcached-2.12/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spymemcached/CompletionListener.java b/instrumentation/spymemcached-2.12/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spymemcached/CompletionListener.java index b83d15fc373a..b045a961be82 100644 --- a/instrumentation/spymemcached-2.12/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spymemcached/CompletionListener.java +++ b/instrumentation/spymemcached-2.12/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spymemcached/CompletionListener.java @@ -9,14 +9,14 @@ import io.opentelemetry.api.trace.Span; import io.opentelemetry.context.Context; -import io.opentelemetry.javaagent.bootstrap.internal.InstrumentationConfig; +import io.opentelemetry.javaagent.bootstrap.internal.AgentInstrumentationConfig; import java.util.concurrent.CancellationException; import java.util.concurrent.ExecutionException; public abstract class CompletionListener { private static final boolean CAPTURE_EXPERIMENTAL_SPAN_ATTRIBUTES = - InstrumentationConfig.get() + AgentInstrumentationConfig.get() .getBoolean("otel.instrumentation.spymemcached.experimental-span-attributes", false); private static final String DB_COMMAND_CANCELLED = "spymemcached.command.cancelled"; diff --git a/instrumentation/spymemcached-2.12/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spymemcached/SpymemcachedAttributesGetter.java b/instrumentation/spymemcached-2.12/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spymemcached/SpymemcachedAttributesGetter.java index f7b5bad904be..b5778dc96a32 100644 --- a/instrumentation/spymemcached-2.12/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spymemcached/SpymemcachedAttributesGetter.java +++ b/instrumentation/spymemcached-2.12/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spymemcached/SpymemcachedAttributesGetter.java @@ -5,7 +5,7 @@ package io.opentelemetry.javaagent.instrumentation.spymemcached; -import io.opentelemetry.instrumentation.api.instrumenter.db.DbClientAttributesGetter; +import io.opentelemetry.instrumentation.api.incubator.semconv.db.DbClientAttributesGetter; import javax.annotation.Nullable; public class SpymemcachedAttributesGetter implements DbClientAttributesGetter { diff --git a/instrumentation/spymemcached-2.12/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spymemcached/SpymemcachedSingletons.java b/instrumentation/spymemcached-2.12/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spymemcached/SpymemcachedSingletons.java index 29915cde16fa..955bd196398e 100644 --- a/instrumentation/spymemcached-2.12/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spymemcached/SpymemcachedSingletons.java +++ b/instrumentation/spymemcached-2.12/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spymemcached/SpymemcachedSingletons.java @@ -6,10 +6,10 @@ package io.opentelemetry.javaagent.instrumentation.spymemcached; import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.instrumentation.api.incubator.semconv.db.DbClientAttributesExtractor; +import io.opentelemetry.instrumentation.api.incubator.semconv.db.DbClientSpanNameExtractor; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; import io.opentelemetry.instrumentation.api.instrumenter.SpanKindExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.db.DbClientAttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.db.DbClientSpanNameExtractor; public final class SpymemcachedSingletons { private static final String INSTRUMENTATION_NAME = "io.opentelemetry.spymemcached-2.12"; diff --git a/instrumentation/spymemcached-2.12/javaagent/src/test/groovy/SpymemcachedTest.groovy b/instrumentation/spymemcached-2.12/javaagent/src/test/groovy/SpymemcachedTest.groovy index e2292021c5c2..fed320ec30b2 100644 --- a/instrumentation/spymemcached-2.12/javaagent/src/test/groovy/SpymemcachedTest.groovy +++ b/instrumentation/spymemcached-2.12/javaagent/src/test/groovy/SpymemcachedTest.groovy @@ -6,8 +6,7 @@ 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 io.opentelemetry.semconv.incubating.DbIncubatingAttributes import net.spy.memcached.CASResponse import net.spy.memcached.ConnectionFactory import net.spy.memcached.ConnectionFactoryBuilder @@ -623,19 +622,19 @@ class SpymemcachedTest extends AgentInstrumentationSpecification { } attributes { - "$SemanticAttributes.DB_SYSTEM" "memcached" - "$SemanticAttributes.DB_OPERATION" operation + "$DbIncubatingAttributes.DB_SYSTEM" "memcached" + "$DbIncubatingAttributes.DB_OPERATION" operation if (error == "canceled") { - "${CompletionListener.DB_COMMAND_CANCELLED}" true + "spymemcached.command.cancelled" true } if (result == "hit") { - "${CompletionListener.MEMCACHED_RESULT}" CompletionListener.HIT + "spymemcached.result" "hit" } if (result == "miss") { - "${CompletionListener.MEMCACHED_RESULT}" CompletionListener.MISS + "spymemcached.result" "miss" } } } diff --git a/instrumentation/struts-2.3/javaagent/build.gradle.kts b/instrumentation/struts-2.3/javaagent/build.gradle.kts index 99ac9d76db17..bdb8a442dda5 100644 --- a/instrumentation/struts-2.3/javaagent/build.gradle.kts +++ b/instrumentation/struts-2.3/javaagent/build.gradle.kts @@ -27,3 +27,7 @@ dependencies { latestDepTestLibrary("org.apache.struts:struts2-core:6.0.+") } + +tasks.withType().configureEach { + jvmArgs("-Dotel.instrumentation.common.experimental.controller-telemetry.enabled=true") +} diff --git a/instrumentation/struts-2.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/struts2/ActionInvocationInstrumentation.java b/instrumentation/struts-2.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/struts2/ActionInvocationInstrumentation.java index 3eb0fc49c625..e8ed9f464c47 100644 --- a/instrumentation/struts-2.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/struts2/ActionInvocationInstrumentation.java +++ b/instrumentation/struts-2.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/struts2/ActionInvocationInstrumentation.java @@ -5,7 +5,7 @@ package io.opentelemetry.javaagent.instrumentation.struts2; -import static io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteSource.CONTROLLER; +import static io.opentelemetry.instrumentation.api.semconv.http.HttpServerRouteSource.CONTROLLER; import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.hasClassesNamed; import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.implementsInterface; import static io.opentelemetry.javaagent.instrumentation.struts2.StrutsSingletons.instrumenter; @@ -16,7 +16,7 @@ import com.opensymphony.xwork2.ActionInvocation; import io.opentelemetry.context.Context; import io.opentelemetry.context.Scope; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteHolder; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRoute; import io.opentelemetry.javaagent.bootstrap.Java8BytecodeBridge; import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; @@ -53,7 +53,7 @@ public static void onEnter( @Advice.Local("otelScope") Scope scope) { Context parentContext = Java8BytecodeBridge.currentContext(); - HttpRouteHolder.updateHttpRoute( + HttpServerRoute.update( parentContext, CONTROLLER, StrutsServerSpanNaming.SERVER_SPAN_NAME, diff --git a/instrumentation/struts-2.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/struts2/StrutsCodeAttributesGetter.java b/instrumentation/struts-2.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/struts2/StrutsCodeAttributesGetter.java index c372ee646f4b..669682173074 100644 --- a/instrumentation/struts-2.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/struts2/StrutsCodeAttributesGetter.java +++ b/instrumentation/struts-2.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/struts2/StrutsCodeAttributesGetter.java @@ -6,7 +6,7 @@ package io.opentelemetry.javaagent.instrumentation.struts2; import com.opensymphony.xwork2.ActionInvocation; -import io.opentelemetry.instrumentation.api.instrumenter.code.CodeAttributesGetter; +import io.opentelemetry.instrumentation.api.incubator.semconv.code.CodeAttributesGetter; public class StrutsCodeAttributesGetter implements CodeAttributesGetter { diff --git a/instrumentation/struts-2.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/struts2/StrutsServerSpanNaming.java b/instrumentation/struts-2.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/struts2/StrutsServerSpanNaming.java index 06766c314328..5bf7240bf31e 100644 --- a/instrumentation/struts-2.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/struts2/StrutsServerSpanNaming.java +++ b/instrumentation/struts-2.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/struts2/StrutsServerSpanNaming.java @@ -6,12 +6,12 @@ package io.opentelemetry.javaagent.instrumentation.struts2; import com.opensymphony.xwork2.ActionProxy; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteGetter; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRouteGetter; import io.opentelemetry.javaagent.bootstrap.servlet.ServletContextPath; public class StrutsServerSpanNaming { - public static final HttpRouteGetter SERVER_SPAN_NAME = + public static final HttpServerRouteGetter SERVER_SPAN_NAME = (context, actionProxy) -> { // We take name from the config, because it contains the path pattern from the // configuration. diff --git a/instrumentation/struts-2.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/struts2/StrutsSingletons.java b/instrumentation/struts-2.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/struts2/StrutsSingletons.java index e80083091eb3..fc1afa0472b0 100644 --- a/instrumentation/struts-2.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/struts2/StrutsSingletons.java +++ b/instrumentation/struts-2.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/struts2/StrutsSingletons.java @@ -7,9 +7,9 @@ import com.opensymphony.xwork2.ActionInvocation; import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.instrumentation.api.incubator.semconv.code.CodeAttributesExtractor; +import io.opentelemetry.instrumentation.api.incubator.semconv.code.CodeSpanNameExtractor; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; -import io.opentelemetry.instrumentation.api.instrumenter.code.CodeAttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.code.CodeSpanNameExtractor; import io.opentelemetry.javaagent.bootstrap.internal.ExperimentalConfig; public class StrutsSingletons { diff --git a/instrumentation/struts-2.3/javaagent/src/test/groovy/Struts2ActionSpanTest.groovy b/instrumentation/struts-2.3/javaagent/src/test/groovy/Struts2ActionSpanTest.groovy deleted file mode 100644 index c170e84d5cb1..000000000000 --- a/instrumentation/struts-2.3/javaagent/src/test/groovy/Struts2ActionSpanTest.groovy +++ /dev/null @@ -1,168 +0,0 @@ -/* - * 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.test.AgentTestTrait -import io.opentelemetry.instrumentation.test.asserts.TraceAssert -import io.opentelemetry.instrumentation.test.base.HttpServerTest -import io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint -import io.opentelemetry.sdk.trace.data.SpanData -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes -import io.opentelemetry.struts.GreetingServlet -import org.eclipse.jetty.server.Server -import org.eclipse.jetty.server.session.HashSessionIdManager -import org.eclipse.jetty.server.session.HashSessionManager -import org.eclipse.jetty.server.session.SessionHandler -import org.eclipse.jetty.servlet.DefaultServlet -import org.eclipse.jetty.servlet.ServletContextHandler -import org.eclipse.jetty.util.resource.FileResource - -import javax.servlet.DispatcherType - -import static io.opentelemetry.api.trace.SpanKind.INTERNAL -import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.ERROR -import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.EXCEPTION -import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.NOT_FOUND -import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.PATH_PARAM -import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.REDIRECT - -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 expectedHttpRoute(ServerEndpoint endpoint) { - switch (endpoint) { - case PATH_PARAM: - return getContextPath() + "/path/{id}/param" - case NOT_FOUND: - return getContextPath() + "/*" - default: - return super.expectedHttpRoute(endpoint) - } - } - - @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" "io.opentelemetry.struts.GreetingAction" - "$SemanticAttributes.CODE_FUNCTION" 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) - - def sessionIdManager = new HashSessionIdManager() - server.setSessionIdManager(sessionIdManager) - def sessionManager = new HashSessionManager() - def sessionHandler = new SessionHandler(sessionManager) - context.setHandler(sessionHandler) - // disable adding jsessionid to url, affects redirect test - context.setInitParameter("org.eclipse.jetty.servlet.SessionIdPathParameterName", "none") - - context.addServlet(DefaultServlet, "/") - context.addServlet(GreetingServlet, "/greetingServlet") - def strutsFilterClass = null - try { - // struts 2.3 - strutsFilterClass = Class.forName("org.apache.struts2.dispatcher.ng.filter.StrutsPrepareAndExecuteFilter") - } catch (ClassNotFoundException exception) { - // struts 2.5 - strutsFilterClass = Class.forName("org.apache.struts2.dispatcher.filter.StrutsPrepareAndExecuteFilter") - } - context.addFilter(strutsFilterClass, "/*", 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) { - span(0) { - name "GET " + getContextPath() + "/dispatch" - kind SpanKind.SERVER - hasNoParent() - } - span(1) { - name "GreetingAction.dispatch_servlet" - kind INTERNAL - childOf span(0) - } - } - } - } -} diff --git a/instrumentation/struts-2.3/javaagent/src/test/java/io/opentelemetry/struts/GreetingAction.java b/instrumentation/struts-2.3/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/struts2/GreetingAction.java similarity index 97% rename from instrumentation/struts-2.3/javaagent/src/test/java/io/opentelemetry/struts/GreetingAction.java rename to instrumentation/struts-2.3/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/struts2/GreetingAction.java index 6fd550c4c514..7b17e6508de0 100644 --- a/instrumentation/struts-2.3/javaagent/src/test/java/io/opentelemetry/struts/GreetingAction.java +++ b/instrumentation/struts-2.3/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/struts2/GreetingAction.java @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.struts; +package io.opentelemetry.javaagent.instrumentation.struts2; import com.opensymphony.xwork2.ActionSupport; import io.opentelemetry.instrumentation.test.base.HttpServerTest; diff --git a/instrumentation/struts-2.3/javaagent/src/test/java/io/opentelemetry/struts/GreetingServlet.java b/instrumentation/struts-2.3/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/struts2/GreetingServlet.java similarity index 89% rename from instrumentation/struts-2.3/javaagent/src/test/java/io/opentelemetry/struts/GreetingServlet.java rename to instrumentation/struts-2.3/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/struts2/GreetingServlet.java index 5d8e491c5e45..e922d2a0237b 100644 --- a/instrumentation/struts-2.3/javaagent/src/test/java/io/opentelemetry/struts/GreetingServlet.java +++ b/instrumentation/struts-2.3/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/struts2/GreetingServlet.java @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.struts; +package io.opentelemetry.javaagent.instrumentation.struts2; import java.io.IOException; import javax.servlet.ServletException; diff --git a/instrumentation/struts-2.3/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/struts2/Struts2ActionSpanTest.java b/instrumentation/struts-2.3/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/struts2/Struts2ActionSpanTest.java new file mode 100644 index 000000000000..f54444ec1638 --- /dev/null +++ b/instrumentation/struts-2.3/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/struts2/Struts2ActionSpanTest.java @@ -0,0 +1,173 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.struts2; + +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.ERROR; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.EXCEPTION; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.NOT_FOUND; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.PATH_PARAM; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.REDIRECT; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; +import static io.opentelemetry.semconv.incubating.CodeIncubatingAttributes.CODE_FUNCTION; +import static io.opentelemetry.semconv.incubating.CodeIncubatingAttributes.CODE_NAMESPACE; +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.api.internal.HttpConstants; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpServerTest; +import io.opentelemetry.instrumentation.testing.junit.http.HttpServerInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.http.HttpServerTestOptions; +import io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint; +import io.opentelemetry.sdk.testing.assertj.SpanDataAssert; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.sdk.trace.data.StatusData; +import io.opentelemetry.testing.internal.armeria.common.AggregatedHttpResponse; +import java.util.EnumSet; +import java.util.Locale; +import javax.servlet.DispatcherType; +import javax.servlet.Filter; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.session.HashSessionIdManager; +import org.eclipse.jetty.server.session.HashSessionManager; +import org.eclipse.jetty.server.session.SessionHandler; +import org.eclipse.jetty.servlet.DefaultServlet; +import org.eclipse.jetty.servlet.ServletContextHandler; +import org.eclipse.jetty.util.resource.FileResource; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +class Struts2ActionSpanTest extends AbstractHttpServerTest { + + @RegisterExtension + public static final InstrumentationExtension testing = + HttpServerInstrumentationExtension.forAgent(); + + @Override + @SuppressWarnings("unchecked") + protected Server setupServer() throws Exception { + Server server = new Server(port); + ServletContextHandler context = new ServletContextHandler(0); + context.setContextPath(getContextPath()); + FileResource resource = new FileResource(getClass().getResource("/")); + context.setBaseResource(resource); + server.setHandler(context); + + HashSessionIdManager sessionIdManager = new HashSessionIdManager(); + server.setSessionIdManager(sessionIdManager); + HashSessionManager sessionManager = new HashSessionManager(); + SessionHandler sessionHandler = new SessionHandler(sessionManager); + context.setHandler(sessionHandler); + + // disable adding jsessionid to url, affects redirect test + context.setInitParameter("org.eclipse.jetty.servlet.SessionIdPathParameterName", "none"); + + context.addServlet(DefaultServlet.class, "/"); + context.addServlet(GreetingServlet.class, "/greetingServlet"); + Class strutsFilterClass; + try { + // struts 2.3 + strutsFilterClass = + Class.forName("org.apache.struts2.dispatcher.ng.filter.StrutsPrepareAndExecuteFilter"); + } catch (ClassNotFoundException exception) { + // struts 2.5 + strutsFilterClass = + Class.forName("org.apache.struts2.dispatcher.filter.StrutsPrepareAndExecuteFilter"); + } + context.addFilter( + (Class) strutsFilterClass, "/*", EnumSet.of(DispatcherType.REQUEST)); + + server.start(); + return server; + } + + @Override + protected void stopServer(Server server) throws Exception { + server.stop(); + server.destroy(); + } + + @Override + protected void configure(HttpServerTestOptions options) { + options.setContextPath("/context"); + options.setTestPathParam(true); + options.setTestErrorBody(false); + options.setHasHandlerSpan(endpoint -> !endpoint.equals(NOT_FOUND)); + options.setHasResponseSpan( + endpoint -> + endpoint == REDIRECT + || endpoint == ERROR + || endpoint == EXCEPTION + || endpoint == NOT_FOUND); + + options.setExpectedHttpRoute( + (ServerEndpoint endpoint, String method) -> { + if (method.equals(HttpConstants._OTHER)) { + return getContextPath() + endpoint.getPath(); + } + if (endpoint.equals(PATH_PARAM)) { + return getContextPath() + "/path/{id}/param"; + } else if (endpoint.equals(NOT_FOUND)) { + return getContextPath() + "/*"; + } else { + return super.expectedHttpRoute(endpoint, method); + } + }); + } + + @Override + protected SpanDataAssert assertResponseSpan( + SpanDataAssert span, SpanData parentSpan, String method, ServerEndpoint endpoint) { + if (endpoint.equals(REDIRECT)) { + span.satisfies(spanData -> assertThat(spanData.getName()).endsWith(".sendRedirect")); + } else if (endpoint.equals(NOT_FOUND)) { + span.satisfies(spanData -> assertThat(spanData.getName()).endsWith(".sendError")) + .hasParent(parentSpan); + } + + span.hasKind(SpanKind.INTERNAL); + return span; + } + + @Override + protected SpanDataAssert assertHandlerSpan( + SpanDataAssert span, String method, ServerEndpoint endpoint) { + span.hasName("GreetingAction." + endpoint.name().toLowerCase(Locale.ROOT)) + .hasKind(SpanKind.INTERNAL); + + if (endpoint.equals(EXCEPTION)) { + span.hasStatus(StatusData.error()).hasException(new Exception(EXCEPTION.getBody())); + } + + span.hasAttributesSatisfyingExactly( + equalTo(CODE_NAMESPACE, GreetingAction.class.getName()), + equalTo(CODE_FUNCTION, endpoint.name().toLowerCase(Locale.ROOT))); + return span; + } + + // 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. + @Test + void testDispatchToServlet() { + AggregatedHttpResponse response = + client.get(address.resolve("dispatch").toString()).aggregate().join(); + + assertThat(response.status().code()).isEqualTo(200); + assertThat(response.contentUtf8()).isEqualTo("greeting"); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("GET " + getContextPath() + "/dispatch") + .hasKind(SpanKind.SERVER) + .hasNoParent(), + span -> + span.hasName("GreetingAction.dispatch_servlet") + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(0)))); + } +} diff --git a/instrumentation/struts-2.3/javaagent/src/test/resources/struts.xml b/instrumentation/struts-2.3/javaagent/src/test/resources/struts.xml index 54fc48ed865c..3cf78125c183 100644 --- a/instrumentation/struts-2.3/javaagent/src/test/resources/struts.xml +++ b/instrumentation/struts-2.3/javaagent/src/test/resources/struts.xml @@ -26,16 +26,16 @@ - - - - - - + + + + + - - - + + + diff --git a/instrumentation/tapestry-5.4/javaagent/build.gradle.kts b/instrumentation/tapestry-5.4/javaagent/build.gradle.kts index 0c1f126d4674..65f356fda056 100644 --- a/instrumentation/tapestry-5.4/javaagent/build.gradle.kts +++ b/instrumentation/tapestry-5.4/javaagent/build.gradle.kts @@ -28,3 +28,7 @@ dependencies { testInstrumentation(project(":instrumentation:servlet:servlet-3.0:javaagent")) testInstrumentation(project(":instrumentation:servlet:servlet-javax-common:javaagent")) } + +tasks.withType().configureEach { + jvmArgs("-Dotel.instrumentation.common.experimental.controller-telemetry.enabled=true") +} diff --git a/instrumentation/tapestry-5.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/tapestry/InitializeActivePageNameInstrumentation.java b/instrumentation/tapestry-5.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/tapestry/InitializeActivePageNameInstrumentation.java index 925c65da1996..f5c971161427 100644 --- a/instrumentation/tapestry-5.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/tapestry/InitializeActivePageNameInstrumentation.java +++ b/instrumentation/tapestry-5.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/tapestry/InitializeActivePageNameInstrumentation.java @@ -5,7 +5,7 @@ package io.opentelemetry.javaagent.instrumentation.tapestry; -import static io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteSource.CONTROLLER; +import static io.opentelemetry.instrumentation.api.semconv.http.HttpServerRouteSource.CONTROLLER; import static io.opentelemetry.javaagent.bootstrap.Java8BytecodeBridge.currentContext; import static net.bytebuddy.matcher.ElementMatchers.isMethod; import static net.bytebuddy.matcher.ElementMatchers.isPublic; @@ -13,7 +13,7 @@ import static net.bytebuddy.matcher.ElementMatchers.takesArgument; import static net.bytebuddy.matcher.ElementMatchers.takesArguments; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteHolder; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRoute; import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; import net.bytebuddy.asm.Advice; @@ -58,7 +58,7 @@ public static class HandleComponentEventAdvice { @Advice.OnMethodEnter(suppress = Throwable.class) public static void onEnter(@Advice.Argument(0) ComponentEventRequestParameters parameters) { - HttpRouteHolder.updateHttpRoute( + HttpServerRoute.update( currentContext(), CONTROLLER, TapestryServerSpanNaming.SERVER_SPAN_NAME, @@ -71,7 +71,7 @@ public static class HandlePageRenderAdvice { @Advice.OnMethodEnter(suppress = Throwable.class) public static void onEnter(@Advice.Argument(0) PageRenderRequestParameters parameters) { - HttpRouteHolder.updateHttpRoute( + HttpServerRoute.update( currentContext(), CONTROLLER, TapestryServerSpanNaming.SERVER_SPAN_NAME, diff --git a/instrumentation/tapestry-5.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/tapestry/TapestryServerSpanNaming.java b/instrumentation/tapestry-5.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/tapestry/TapestryServerSpanNaming.java index c4195cf38760..2ec12e052df3 100644 --- a/instrumentation/tapestry-5.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/tapestry/TapestryServerSpanNaming.java +++ b/instrumentation/tapestry-5.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/tapestry/TapestryServerSpanNaming.java @@ -5,12 +5,12 @@ package io.opentelemetry.javaagent.instrumentation.tapestry; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteGetter; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRouteGetter; import io.opentelemetry.javaagent.bootstrap.servlet.ServletContextPath; public class TapestryServerSpanNaming { - public static final HttpRouteGetter SERVER_SPAN_NAME = + public static final HttpServerRouteGetter SERVER_SPAN_NAME = (context, pageName) -> { if (pageName == null) { return null; diff --git a/instrumentation/tapestry-5.4/javaagent/src/test/groovy/TapestryTest.groovy b/instrumentation/tapestry-5.4/javaagent/src/test/groovy/TapestryTest.groovy deleted file mode 100644 index 080cc8aee2e7..000000000000 --- a/instrumentation/tapestry-5.4/javaagent/src/test/groovy/TapestryTest.groovy +++ /dev/null @@ -1,168 +0,0 @@ -/* - * 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.testing.internal.armeria.client.WebClient -import io.opentelemetry.testing.internal.armeria.common.AggregatedHttpResponse -import org.eclipse.jetty.server.Server -import org.eclipse.jetty.util.resource.Resource -import org.eclipse.jetty.webapp.WebAppContext -import org.jsoup.Jsoup - -import static io.opentelemetry.api.trace.StatusCode.ERROR - -class TapestryTest extends AgentInstrumentationSpecification implements HttpServerTestTrait { - - def setupSpec() { - setupServer() - } - - def cleanupSpec() { - cleanupServer() - } - - @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) - .followRedirects() - .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, "GET " + getContextPath() + "/Index") - span(1) { - name "activate/Index" - kind SpanKind.INTERNAL - childOf 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, "GET " + getContextPath() + "/Index") - span(1) { - name "activate/Index" - kind SpanKind.INTERNAL - childOf span(0) - } - span(2) { - name "action/Index:start" - kind SpanKind.INTERNAL - childOf span(0) - } - span(3) { - name "Response.sendRedirect" - kind SpanKind.INTERNAL - childOf span(2) - } - } - trace(1, 2) { - serverSpan(it, 0, "GET " + getContextPath() + "/Other") - span(1) { - name "activate/Other" - kind SpanKind.INTERNAL - childOf 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 - def ex = new IllegalStateException("expected") - - assertTraces(1) { - trace(0, 3) { - span(0) { - hasNoParent() - kind SpanKind.SERVER - name "GET " + getContextPath() + "/Index" - status ERROR - } - span(1) { - name "activate/Index" - kind SpanKind.INTERNAL - childOf span(0) - } - span(2) { - name "action/Index:exception" - kind SpanKind.INTERNAL - childOf span(0) - status ERROR - errorEvent(ex.class, ex.message) - } - } - } - } -} diff --git a/instrumentation/tapestry-5.4/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/tapestry/TapestryTest.java b/instrumentation/tapestry-5.4/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/tapestry/TapestryTest.java new file mode 100644 index 000000000000..05d4d0209ee5 --- /dev/null +++ b/instrumentation/tapestry-5.4/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/tapestry/TapestryTest.java @@ -0,0 +1,153 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.tapestry; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpServerUsingTest; +import io.opentelemetry.instrumentation.testing.junit.http.HttpServerInstrumentationExtension; +import io.opentelemetry.sdk.trace.data.StatusData; +import io.opentelemetry.testing.internal.armeria.client.WebClient; +import io.opentelemetry.testing.internal.armeria.common.AggregatedHttpResponse; +import org.eclipse.jetty.server.Connector; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.util.resource.Resource; +import org.eclipse.jetty.webapp.WebAppContext; +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +class TapestryTest extends AbstractHttpServerUsingTest { + + private static WebClient client; + + @RegisterExtension + public static final InstrumentationExtension testing = + HttpServerInstrumentationExtension.forAgent(); + + @Override + protected Server setupServer() throws Exception { + WebAppContext webAppContext = new WebAppContext(); + webAppContext.setContextPath(getContextPath()); + Server jettyServer = new Server(port); + + // set up test application + webAppContext.setBaseResource(Resource.newResource("src/test/webapp")); + for (Connector connector : jettyServer.getConnectors()) { + connector.setHost("localhost"); + } + + jettyServer.setHandler(webAppContext); + jettyServer.start(); + + return jettyServer; + } + + @Override + protected void stopServer(Server server) throws Exception { + server.stop(); + } + + @Override + protected String getContextPath() { + return "/jetty-context"; + } + + @BeforeAll + void setup() { + startServer(); + client = WebClient.builder(address).followRedirects().build(); + } + + @Test + void testIndexPage() { + AggregatedHttpResponse response = client.get("/").aggregate().join(); + Document doc = Jsoup.parse(response.contentUtf8()); + + assertThat(response.status().code()).isEqualTo(200); + assertThat(doc.selectFirst("title").text()).isEqualTo("Index page"); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("GET " + getContextPath() + "/Index") + .hasNoParent() + .hasKind(SpanKind.SERVER), + span -> + span.hasName("activate/Index") + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(0)))); + } + + @Test + void testStartAction() { + AggregatedHttpResponse response = client.get("/index.start").aggregate().join(); + Document doc = Jsoup.parse(response.contentUtf8()); + + assertThat(response.status().code()).isEqualTo(200); + assertThat(doc.selectFirst("title").text()).isEqualTo("Other page"); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("GET " + getContextPath() + "/Index") + .hasNoParent() + .hasKind(SpanKind.SERVER), + span -> + span.hasName("activate/Index") + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(0)), + span -> + span.hasName("action/Index:start") + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(0)), + span -> + span.hasName("Response.sendRedirect") + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(2))), + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("GET " + getContextPath() + "/Other") + .hasNoParent() + .hasKind(SpanKind.SERVER), + span -> + span.hasName("activate/Other") + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(0)))); + } + + @Test + void testExceptionAction() { + AggregatedHttpResponse response = client.get("/index.exception").aggregate().join(); + + assertThat(response.status().code()).isEqualTo(500); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("GET " + getContextPath() + "/Index") + .hasStatus(StatusData.error()) + .hasKind(SpanKind.SERVER), + span -> + span.hasName("activate/Index") + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(0)), + span -> + span.hasName("action/Index:exception") + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(0)) + .hasStatus(StatusData.error()) + .hasException(new IllegalStateException("expected")))); + } +} diff --git a/instrumentation/tomcat/tomcat-10.0/javaagent/build.gradle.kts b/instrumentation/tomcat/tomcat-10.0/javaagent/build.gradle.kts index 953efa029c00..ad1c5946290e 100644 --- a/instrumentation/tomcat/tomcat-10.0/javaagent/build.gradle.kts +++ b/instrumentation/tomcat/tomcat-10.0/javaagent/build.gradle.kts @@ -21,8 +21,21 @@ dependencies { // Make sure nothing breaks due to both 7.0 and 10.0 modules being present together testInstrumentation(project(":instrumentation:tomcat:tomcat-7.0:javaagent")) + // testing whether instrumentation still works when javax servlet api is also present + testImplementation("javax.servlet:javax.servlet-api:3.0.1") } -tasks.withType().configureEach { - jvmArgs("-Dotel.instrumentation.servlet.experimental.capture-request-parameters=test-parameter") +tasks { + withType().configureEach { + jvmArgs("-Dotel.instrumentation.servlet.experimental.capture-request-parameters=test-parameter") + jvmArgs("-Dotel.instrumentation.common.experimental.controller-telemetry.enabled=true") + } +} + +// Tomcat 10 uses deprecation annotation methods `forRemoval()` and `since()` +// in jakarta.servlet.http.HttpServlet that don't work with Java 8 +if (findProperty("testLatestDeps") as Boolean) { + otelJava { + minJavaVersionSupported.set(JavaVersion.VERSION_11) + } } diff --git a/instrumentation/tomcat/tomcat-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/tomcat/v10_0/Tomcat10InstrumentationModule.java b/instrumentation/tomcat/tomcat-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/tomcat/v10_0/Tomcat10InstrumentationModule.java index 7f77d7485af1..0341609b0f1e 100644 --- a/instrumentation/tomcat/tomcat-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/tomcat/v10_0/Tomcat10InstrumentationModule.java +++ b/instrumentation/tomcat/tomcat-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/tomcat/v10_0/Tomcat10InstrumentationModule.java @@ -25,7 +25,13 @@ public Tomcat10InstrumentationModule() { @Override public ElementMatcher.Junction classLoaderMatcher() { // only matches tomcat 10.0+ - return hasClassesNamed("jakarta.servlet.ReadListener"); + return hasClassesNamed("jakarta.servlet.http.HttpServletRequest") + .and( + // tomcat 10 has at least one of these two classes. Cache$EvictionOrder is present in + // 10.0.0, but is removed before 10.1.0. GenericUser is added before Cache$EvictionOrder + // is removed + hasClassesNamed("org.apache.catalina.users.GenericUser") + .or(hasClassesNamed("org.apache.catalina.webresources.Cache$EvictionOrder"))); } @Override diff --git a/instrumentation/tomcat/tomcat-10.0/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/tomcat/v10_0/AsyncServlet.groovy b/instrumentation/tomcat/tomcat-10.0/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/tomcat/v10_0/AsyncServlet.groovy deleted file mode 100644 index e76556ba100c..000000000000 --- a/instrumentation/tomcat/tomcat-10.0/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/tomcat/v10_0/AsyncServlet.groovy +++ /dev/null @@ -1,83 +0,0 @@ -/* - * 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 io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint -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.CountDownLatch - -import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.CAPTURE_HEADERS -import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.ERROR -import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.EXCEPTION -import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.INDEXED_CHILD -import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.QUERY_PARAM -import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.REDIRECT -import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.SUCCESS - -@WebServlet(asyncSupported = true) -class AsyncServlet extends HttpServlet { - @Override - protected void service(HttpServletRequest req, HttpServletResponse resp) { - ServerEndpoint endpoint = ServerEndpoint.forPath(req.servletPath) - def latch = new CountDownLatch(1) - def context = req.startAsync() - if (endpoint == EXCEPTION) { - context.setTimeout(5000) - } - context.start { - try { - 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 CAPTURE_HEADERS: - resp.setHeader("X-Test-Response", req.getHeader("X-Test-Request")) - resp.status = endpoint.status - resp.writer.print(endpoint.body) - break - case ERROR: - resp.status = endpoint.status - resp.writer.print(endpoint.body) - break - case EXCEPTION: - resp.status = endpoint.status - def writer = resp.writer - writer.print(endpoint.body) - writer.close() - throw new ServletException(endpoint.body) - } - } - } finally { - // complete at the end so the server span will end after the controller span - if (endpoint != EXCEPTION) { - context.complete() - } - latch.countDown() - } - } - latch.await() - } -} diff --git a/instrumentation/tomcat/tomcat-10.0/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/tomcat/v10_0/TomcatAsyncTest.groovy b/instrumentation/tomcat/tomcat-10.0/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/tomcat/v10_0/TomcatAsyncTest.groovy deleted file mode 100644 index 5c8a4c5e49fc..000000000000 --- a/instrumentation/tomcat/tomcat-10.0/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/tomcat/v10_0/TomcatAsyncTest.groovy +++ /dev/null @@ -1,133 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.javaagent.instrumentation.tomcat.v10_0 - - -import io.opentelemetry.instrumentation.test.AgentTestTrait -import io.opentelemetry.instrumentation.test.asserts.TraceAssert -import io.opentelemetry.instrumentation.test.base.HttpServerTest -import io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint -import jakarta.servlet.Servlet -import jakarta.servlet.ServletException -import org.apache.catalina.Context -import org.apache.catalina.startup.Tomcat -import org.apache.tomcat.JarScanFilter -import org.apache.tomcat.JarScanType -import spock.lang.Unroll - -import java.nio.file.Files - -import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.AUTH_REQUIRED -import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.CAPTURE_HEADERS -import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.ERROR -import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.EXCEPTION -import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.INDEXED_CHILD -import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.NOT_FOUND -import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.QUERY_PARAM -import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.REDIRECT -import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.SUCCESS - -@Unroll -class TomcatAsyncTest extends HttpServerTest implements AgentTestTrait { - - @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 - String getContextPath() { - return "/tomcat-context" - } - - 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, CAPTURE_HEADERS.path, servlet) - addServlet(context, INDEXED_CHILD.path, servlet) - } - - void addServlet(Context servletContext, String path, Class servlet) { - String name = UUID.randomUUID() - Tomcat.addServlet(servletContext, name, servlet.newInstance()) - servletContext.addServletMappingDecoded(path, name) - } - - Class servlet() { - AsyncServlet - } - - @Override - String expectedHttpRoute(ServerEndpoint endpoint) { - switch (endpoint) { - case NOT_FOUND: - return getContextPath() + "/*" - default: - return super.expectedHttpRoute(endpoint) - } - } - - @Override - Throwable expectedException() { - new ServletException(EXCEPTION.body) - } - - @Override - boolean hasResponseSpan(ServerEndpoint endpoint) { - endpoint == NOT_FOUND || endpoint == REDIRECT - } - - @Override - void responseSpan(TraceAssert trace, int index, Object parent, String method, ServerEndpoint endpoint) { - switch (endpoint) { - case REDIRECT: - redirectSpan(trace, index, parent) - break - case NOT_FOUND: - sendErrorSpan(trace, index, parent) - break - } - } -} diff --git a/instrumentation/tomcat/tomcat-10.0/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/tomcat/v10_0/TomcatHandlerTest.groovy b/instrumentation/tomcat/tomcat-10.0/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/tomcat/v10_0/TomcatHandlerTest.groovy deleted file mode 100644 index 8fca022e1768..000000000000 --- a/instrumentation/tomcat/tomcat-10.0/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/tomcat/v10_0/TomcatHandlerTest.groovy +++ /dev/null @@ -1,120 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.javaagent.instrumentation.tomcat.v10_0 - - -import io.opentelemetry.instrumentation.test.AgentTestTrait -import io.opentelemetry.instrumentation.test.asserts.TraceAssert -import io.opentelemetry.instrumentation.test.base.HttpServerTest -import io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint -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 static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.AUTH_ERROR -import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.AUTH_REQUIRED -import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.CAPTURE_HEADERS -import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.CAPTURE_PARAMETERS -import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.ERROR -import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.EXCEPTION -import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.INDEXED_CHILD -import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.LOGIN -import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.NOT_FOUND -import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.PATH_PARAM -import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.QUERY_PARAM -import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.REDIRECT -import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.SUCCESS - -class TomcatHandlerTest extends HttpServerTest implements AgentTestTrait { - - private static final List serverEndpointsList = Arrays.asList(SUCCESS, REDIRECT, ERROR, EXCEPTION, NOT_FOUND, CAPTURE_HEADERS, CAPTURE_PARAMETERS, QUERY_PARAM, PATH_PARAM, AUTH_REQUIRED, LOGIN, AUTH_ERROR, INDEXED_CHILD) - - def "Tomcat starts"() { - expect: - getServer() != null - } - - @Override - String getContextPath() { - return "/app" - } - - @Override - boolean hasResponseCustomizer(ServerEndpoint endpoint) { - true - } - - @Override - boolean testCapturedRequestParameters() { - true - } - - @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. - serverEndpointsList.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.isError()) { - 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/instrumentation/tomcat/tomcat-10.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/tomcat/v10_0/AsyncServlet.java b/instrumentation/tomcat/tomcat-10.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/tomcat/v10_0/AsyncServlet.java new file mode 100644 index 000000000000..a76c1f9d3d2c --- /dev/null +++ b/instrumentation/tomcat/tomcat-10.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/tomcat/v10_0/AsyncServlet.java @@ -0,0 +1,82 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.tomcat.v10_0; + +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.CAPTURE_HEADERS; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.ERROR; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.EXCEPTION; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.INDEXED_CHILD; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.QUERY_PARAM; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.REDIRECT; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.SUCCESS; + +import io.opentelemetry.instrumentation.test.base.HttpServerTest; +import io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint; +import jakarta.servlet.AsyncContext; +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.PrintWriter; +import java.util.concurrent.CountDownLatch; + +@WebServlet(asyncSupported = true) +class AsyncServlet extends HttpServlet { + @Override + protected void service(HttpServletRequest req, HttpServletResponse resp) { + ServerEndpoint endpoint = ServerEndpoint.forPath(req.getServletPath()); + CountDownLatch latch = new CountDownLatch(1); + AsyncContext context = req.startAsync(); + if (endpoint == EXCEPTION) { + context.setTimeout(5000); + } + context.start( + () -> { + try { + HttpServerTest.controller( + endpoint, + () -> { + resp.setContentType("text/plain"); + if (endpoint.equals(SUCCESS) || endpoint.equals(ERROR)) { + resp.setStatus(endpoint.getStatus()); + resp.getWriter().print(endpoint.getBody()); + } else if (endpoint.equals(INDEXED_CHILD)) { + endpoint.collectSpanAttributes(x -> req.getParameter(x)); + resp.setStatus(endpoint.getStatus()); + } else if (endpoint.equals(QUERY_PARAM)) { + resp.setStatus(endpoint.getStatus()); + resp.getWriter().print(req.getQueryString()); + } else if (endpoint.equals(REDIRECT)) { + resp.sendRedirect(endpoint.getBody()); + } else if (endpoint.equals(CAPTURE_HEADERS)) { + resp.setHeader("X-Test-Response", req.getHeader("X-Test-Request")); + resp.setStatus(endpoint.getStatus()); + resp.getWriter().print(endpoint.getBody()); + } else if (endpoint.equals(EXCEPTION)) { + resp.setStatus(endpoint.getStatus()); + PrintWriter writer = resp.getWriter(); + writer.print(endpoint.getBody()); + writer.close(); + throw new ServletException(endpoint.getBody()); + } + return null; + }); + } finally { + // complete at the end so the server span will end after the controller span + if (endpoint != EXCEPTION) { + context.complete(); + } + latch.countDown(); + } + }); + try { + latch.await(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } +} diff --git a/instrumentation/tomcat/tomcat-10.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/tomcat/v10_0/ErrorHandlerValve.java b/instrumentation/tomcat/tomcat-10.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/tomcat/v10_0/ErrorHandlerValve.java new file mode 100644 index 000000000000..1b0ed2f4be2b --- /dev/null +++ b/instrumentation/tomcat/tomcat-10.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/tomcat/v10_0/ErrorHandlerValve.java @@ -0,0 +1,26 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.tomcat.v10_0; + +import java.io.IOException; +import org.apache.catalina.connector.Request; +import org.apache.catalina.connector.Response; +import org.apache.catalina.valves.ErrorReportValve; + +class ErrorHandlerValve extends ErrorReportValve { + @Override + protected void report(Request request, Response response, Throwable t) { + if (response.getStatus() < 400 || response.getContentWritten() > 0 || !response.isError()) { + return; + } + try { + response.getWriter().print(t != null ? t.getCause().getMessage() : response.getMessage()); + } 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/instrumentation/tomcat/tomcat-10.0/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/tomcat/v10_0/TestServlet.java b/instrumentation/tomcat/tomcat-10.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/tomcat/v10_0/TestServlet.java similarity index 97% rename from instrumentation/tomcat/tomcat-10.0/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/tomcat/v10_0/TestServlet.java rename to instrumentation/tomcat/tomcat-10.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/tomcat/v10_0/TestServlet.java index ce0ef83a5bf1..ab3fc6da250e 100644 --- a/instrumentation/tomcat/tomcat-10.0/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/tomcat/v10_0/TestServlet.java +++ b/instrumentation/tomcat/tomcat-10.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/tomcat/v10_0/TestServlet.java @@ -13,7 +13,7 @@ import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; -public class TestServlet extends HttpServlet { +class TestServlet extends HttpServlet { @Override protected void service(HttpServletRequest req, HttpServletResponse resp) throws IOException { diff --git a/instrumentation/tomcat/tomcat-10.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/tomcat/v10_0/TomcatAsyncTest.java b/instrumentation/tomcat/tomcat-10.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/tomcat/v10_0/TomcatAsyncTest.java new file mode 100644 index 000000000000..651308f9634b --- /dev/null +++ b/instrumentation/tomcat/tomcat-10.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/tomcat/v10_0/TomcatAsyncTest.java @@ -0,0 +1,129 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.tomcat.v10_0; + +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.AUTH_REQUIRED; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.CAPTURE_HEADERS; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.ERROR; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.EXCEPTION; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.INDEXED_CHILD; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.NOT_FOUND; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.QUERY_PARAM; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.REDIRECT; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.SUCCESS; +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.api.internal.HttpConstants; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpServerTest; +import io.opentelemetry.instrumentation.testing.junit.http.HttpServerInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.http.HttpServerTestOptions; +import io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint; +import io.opentelemetry.sdk.testing.assertj.SpanDataAssert; +import jakarta.servlet.Servlet; +import jakarta.servlet.ServletException; +import java.io.File; +import java.nio.file.Files; +import java.util.UUID; +import org.apache.catalina.Context; +import org.apache.catalina.LifecycleException; +import org.apache.catalina.startup.Tomcat; +import org.junit.jupiter.api.extension.RegisterExtension; + +class TomcatAsyncTest extends AbstractHttpServerTest { + + @RegisterExtension + static final InstrumentationExtension testing = HttpServerInstrumentationExtension.forAgent(); + + @Override + public Tomcat setupServer() throws Exception { + Tomcat tomcatServer = new Tomcat(); + File baseDir = Files.createTempDirectory("tomcat").toFile(); + baseDir.deleteOnExit(); + tomcatServer.setBaseDir(baseDir.getAbsolutePath()); + tomcatServer.setPort(port); + tomcatServer.getConnector().setEnableLookups(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(getContextPath(), applicationDir.getAbsolutePath()); + // Speed up startup by disabling jar scanning: + servletContext.getJarScanner().setJarScanFilter((jarScanType, jarName) -> false); + + setupServlets(servletContext); + tomcatServer.start(); + return tomcatServer; + } + + protected void setupServlets(Context context) throws Exception { + Class servlet = servlet(); + + addServlet(context, SUCCESS.getPath(), servlet); + addServlet(context, QUERY_PARAM.getPath(), servlet); + addServlet(context, ERROR.getPath(), servlet); + addServlet(context, EXCEPTION.getPath(), servlet); + addServlet(context, REDIRECT.getPath(), servlet); + addServlet(context, AUTH_REQUIRED.getPath(), servlet); + addServlet(context, CAPTURE_HEADERS.getPath(), servlet); + addServlet(context, INDEXED_CHILD.getPath(), servlet); + } + + void addServlet(Context servletContext, String path, Class servlet) + throws Exception { + String name = UUID.randomUUID().toString(); + Tomcat.addServlet(servletContext, name, servlet.getDeclaredConstructor().newInstance()); + servletContext.addServletMappingDecoded(path, name); + } + + Class servlet() { + return AsyncServlet.class; + } + + @Override + public void stopServer(Tomcat server) throws LifecycleException { + server.stop(); + server.destroy(); + } + + @Override + protected void configure(HttpServerTestOptions options) { + options.setContextPath("/tomcat-context"); + + options.setExpectedHttpRoute( + (ServerEndpoint endpoint, String method) -> { + if (method.equals(HttpConstants._OTHER)) { + return getContextPath() + endpoint.getPath(); + } + if (endpoint.equals(NOT_FOUND)) { + return getContextPath() + "/*"; + } + return super.expectedHttpRoute(endpoint, method); + }); + + options.setExpectedException(new ServletException(EXCEPTION.getBody())); + + options.setHasResponseSpan(endpoint -> endpoint == NOT_FOUND || endpoint == REDIRECT); + } + + @Override + protected SpanDataAssert assertResponseSpan( + SpanDataAssert span, String method, ServerEndpoint endpoint) { + if (endpoint.equals(REDIRECT)) { + span.satisfies(spanData -> assertThat(spanData.getName()).endsWith(".sendRedirect")); + } else if (endpoint.equals(NOT_FOUND)) { + span.satisfies(spanData -> assertThat(spanData.getName()).endsWith(".sendError")); + } + span.hasKind(SpanKind.INTERNAL).hasAttributesSatisfying(Attributes::isEmpty); + return span; + } +} diff --git a/instrumentation/tomcat/tomcat-10.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/tomcat/v10_0/TomcatHandlerTest.java b/instrumentation/tomcat/tomcat-10.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/tomcat/v10_0/TomcatHandlerTest.java new file mode 100644 index 000000000000..cb0d402922fa --- /dev/null +++ b/instrumentation/tomcat/tomcat-10.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/tomcat/v10_0/TomcatHandlerTest.java @@ -0,0 +1,126 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.tomcat.v10_0; + +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.AUTH_ERROR; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.AUTH_REQUIRED; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.CAPTURE_HEADERS; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.CAPTURE_PARAMETERS; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.ERROR; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.EXCEPTION; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.INDEXED_CHILD; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.LOGIN; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.NOT_FOUND; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.PATH_PARAM; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.QUERY_PARAM; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.REDIRECT; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.SUCCESS; +import static java.util.Arrays.asList; +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.api.internal.HttpConstants; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpServerTest; +import io.opentelemetry.instrumentation.testing.junit.http.HttpServerInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.http.HttpServerTestOptions; +import io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint; +import io.opentelemetry.sdk.testing.assertj.SpanDataAssert; +import java.io.File; +import java.nio.file.Files; +import java.util.List; +import org.apache.catalina.Context; +import org.apache.catalina.LifecycleException; +import org.apache.catalina.core.StandardHost; +import org.apache.catalina.startup.Tomcat; +import org.junit.jupiter.api.extension.RegisterExtension; + +class TomcatHandlerTest extends AbstractHttpServerTest { + + @RegisterExtension + static final InstrumentationExtension testing = HttpServerInstrumentationExtension.forAgent(); + + private static final List serverEndpointsList = + asList( + SUCCESS, + REDIRECT, + ERROR, + EXCEPTION, + NOT_FOUND, + CAPTURE_HEADERS, + CAPTURE_PARAMETERS, + QUERY_PARAM, + PATH_PARAM, + AUTH_REQUIRED, + LOGIN, + AUTH_ERROR, + INDEXED_CHILD); + + @Override + public Tomcat setupServer() throws Exception { + Tomcat tomcatServer = new Tomcat(); + File baseDir = Files.createTempDirectory("tomcat").toFile(); + baseDir.deleteOnExit(); + tomcatServer.setBaseDir(baseDir.getAbsolutePath()); + tomcatServer.setPort(port); + tomcatServer.getConnector(); + + Context servletContext = + tomcatServer.addContext(getContextPath(), new File(".").getAbsolutePath()); + + Tomcat.addServlet(servletContext, "testServlet", new TestServlet()); + + // Mapping servlet to /* will result in all requests have a name of just a context. + serverEndpointsList.stream() + .filter(endpoint -> !endpoint.equals(NOT_FOUND)) + .forEach( + endpoint -> servletContext.addServletMappingDecoded(endpoint.getPath(), "testServlet")); + + StandardHost host = (StandardHost) tomcatServer.getHost(); + host.setErrorReportValveClass(ErrorHandlerValve.class.getName()); + + tomcatServer.start(); + return tomcatServer; + } + + @Override + public void stopServer(Tomcat server) throws LifecycleException { + server.stop(); + server.destroy(); + } + + @Override + protected void configure(HttpServerTestOptions options) { + options.setContextPath("/app"); + options.setHasResponseCustomizer(serverEndpoint -> true); + options.setTestCaptureRequestParameters(true); + options.setTestErrorBody(false); + + options.setHasResponseSpan( + endpoint -> endpoint == REDIRECT || endpoint == ERROR || endpoint == NOT_FOUND); + + options.setExpectedHttpRoute( + (ServerEndpoint endpoint, String method) -> { + if (method.equals(HttpConstants._OTHER)) { + return getContextPath() + endpoint.getPath(); + } + return super.expectedHttpRoute(endpoint, method); + }); + } + + @Override + protected SpanDataAssert assertResponseSpan( + SpanDataAssert span, String method, ServerEndpoint endpoint) { + if (endpoint.equals(REDIRECT)) { + span.satisfies(spanData -> assertThat(spanData.getName()).endsWith(".sendRedirect")); + } else if (endpoint.equals(NOT_FOUND)) { + span.satisfies(spanData -> assertThat(spanData.getName()).endsWith(".sendError")); + } + span.hasKind(SpanKind.INTERNAL).hasAttributesSatisfying(Attributes::isEmpty); + return span; + } +} diff --git a/instrumentation/tomcat/tomcat-7.0/javaagent/build.gradle.kts b/instrumentation/tomcat/tomcat-7.0/javaagent/build.gradle.kts index 5e43f949cf93..ea631048dd0d 100644 --- a/instrumentation/tomcat/tomcat-7.0/javaagent/build.gradle.kts +++ b/instrumentation/tomcat/tomcat-7.0/javaagent/build.gradle.kts @@ -22,6 +22,8 @@ dependencies { 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")) + // testing whether instrumentation still works when jakarta servlet api is also present + testImplementation("jakarta.servlet:jakarta.servlet-api:5.0.0") testLibrary("org.apache.tomcat.embed:tomcat-embed-core:8.0.41") testLibrary("org.apache.tomcat.embed:tomcat-embed-jasper:8.0.41") @@ -30,9 +32,12 @@ dependencies { latestDepTestLibrary("org.apache.tomcat.embed:tomcat-embed-jasper:9.+") // see tomcat-10.0 module } -tasks.withType().configureEach { - jvmArgs("-Dotel.instrumentation.servlet.experimental.capture-request-parameters=test-parameter") - // required on jdk17 - jvmArgs("--add-opens=java.base/java.util=ALL-UNNAMED") - jvmArgs("-XX:+IgnoreUnrecognizedVMOptions") +tasks { + withType().configureEach { + jvmArgs("-Dotel.instrumentation.servlet.experimental.capture-request-parameters=test-parameter") + // required on jdk17 + jvmArgs("--add-opens=java.base/java.util=ALL-UNNAMED") + jvmArgs("-XX:+IgnoreUnrecognizedVMOptions") + jvmArgs("-Dotel.instrumentation.common.experimental.controller-telemetry.enabled=true") + } } diff --git a/instrumentation/tomcat/tomcat-7.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/tomcat/v7_0/Tomcat7InstrumentationModule.java b/instrumentation/tomcat/tomcat-7.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/tomcat/v7_0/Tomcat7InstrumentationModule.java index 1a3afb393f50..04e9c7a004c2 100644 --- a/instrumentation/tomcat/tomcat-7.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/tomcat/v7_0/Tomcat7InstrumentationModule.java +++ b/instrumentation/tomcat/tomcat-7.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/tomcat/v7_0/Tomcat7InstrumentationModule.java @@ -7,7 +7,6 @@ import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.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; @@ -26,7 +25,8 @@ public Tomcat7InstrumentationModule() { @Override public ElementMatcher.Junction classLoaderMatcher() { // does not match tomcat 10.0+ - return not(hasClassesNamed("jakarta.servlet.ReadListener")); + return hasClassesNamed( + "javax.servlet.http.HttpServletRequest", "org.apache.catalina.loader.Constants"); } @Override diff --git a/instrumentation/tomcat/tomcat-7.0/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/tomcat/v7_0/AsyncServlet.groovy b/instrumentation/tomcat/tomcat-7.0/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/tomcat/v7_0/AsyncServlet.groovy deleted file mode 100644 index 8e27dcc8ce88..000000000000 --- a/instrumentation/tomcat/tomcat-7.0/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/tomcat/v7_0/AsyncServlet.groovy +++ /dev/null @@ -1,82 +0,0 @@ -/* - * 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 io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint -import javax.servlet.ServletException -import javax.servlet.annotation.WebServlet -import javax.servlet.http.HttpServlet -import javax.servlet.http.HttpServletRequest -import javax.servlet.http.HttpServletResponse -import java.util.concurrent.CountDownLatch - -import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.CAPTURE_HEADERS -import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.ERROR -import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.EXCEPTION -import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.INDEXED_CHILD -import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.QUERY_PARAM -import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.REDIRECT -import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.SUCCESS - -@WebServlet(asyncSupported = true) -class AsyncServlet extends HttpServlet { - @Override - protected void service(HttpServletRequest req, HttpServletResponse resp) { - ServerEndpoint endpoint = ServerEndpoint.forPath(req.servletPath) - def latch = new CountDownLatch(1) - def context = req.startAsync() - if (endpoint == EXCEPTION) { - context.setTimeout(5000) - } - context.start { - try { - 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 CAPTURE_HEADERS: - resp.setHeader("X-Test-Response", req.getHeader("X-Test-Request")) - resp.status = endpoint.status - resp.writer.print(endpoint.body) - break - case ERROR: - resp.status = endpoint.status - resp.writer.print(endpoint.body) - break - case EXCEPTION: - resp.status = endpoint.status - def writer = resp.writer - writer.print(endpoint.body) - writer.close() - throw new ServletException(endpoint.body) - } - } - } finally { - // complete at the end so the server span will end after the controller span - if (endpoint != EXCEPTION) { - context.complete() - } - latch.countDown() - } - } - latch.await() - } -} diff --git a/instrumentation/tomcat/tomcat-7.0/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/tomcat/v7_0/ThreadPoolExecutorTest.groovy b/instrumentation/tomcat/tomcat-7.0/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/tomcat/v7_0/ThreadPoolExecutorTest.groovy deleted file mode 100644 index 517b8ad71e2f..000000000000 --- a/instrumentation/tomcat/tomcat-7.0/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/tomcat/v7_0/ThreadPoolExecutorTest.groovy +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.javaagent.instrumentation.tomcat.v7_0 - -import io.opentelemetry.api.trace.SpanKind -import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification -import org.apache.tomcat.util.threads.TaskQueue -import org.apache.tomcat.util.threads.ThreadPoolExecutor - -import java.util.concurrent.CountDownLatch -import java.util.concurrent.TimeUnit -import java.util.concurrent.atomic.AtomicBoolean - -class ThreadPoolExecutorTest extends AgentInstrumentationSpecification { - - // Test that PropagatedContext isn't cleared when ThreadPoolExecutor.execute fails with - // RejectedExecutionException - def "test tomcat thread pool"() { - setup: - def reject = new AtomicBoolean() - def queue = new TaskQueue() { - @Override - boolean offer(Runnable o) { - // TaskQueue.offer returns false when parent.getPoolSize() < parent.getMaximumPoolSize() - // here we simulate the same condition to trigger RejectedExecutionException handling in - // tomcat ThreadPoolExecutor - if (reject.get()) { - reject.set(false) - return false - } - return super.offer(o) - } - } - def pool = new ThreadPoolExecutor(1, 1, 0, TimeUnit.MILLISECONDS, queue) - queue.setParent(pool) - - CountDownLatch latch = new CountDownLatch(1) - - runWithSpan("parent") { - pool.execute(new Runnable() { - @Override - void run() { - runWithSpan("child1") { - latch.await() - } - } - }) - - reject.set(true) - pool.execute(new Runnable() { - @Override - void run() { - runWithSpan("child2") { - latch.await() - } - } - }) - } - - latch.countDown() - - expect: - assertTraces(1) { - trace(0, 3) { - span(0) { - name "parent" - kind SpanKind.INTERNAL - hasNoParent() - } - span(1) { - name "child1" - kind SpanKind.INTERNAL - childOf span(0) - } - span(2) { - name "child2" - kind SpanKind.INTERNAL - childOf span(0) - } - } - } - - cleanup: - pool.shutdown() - pool.awaitTermination(10, TimeUnit.SECONDS) - } -} diff --git a/instrumentation/tomcat/tomcat-7.0/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/tomcat/v7_0/TomcatAsyncTest.groovy b/instrumentation/tomcat/tomcat-7.0/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/tomcat/v7_0/TomcatAsyncTest.groovy deleted file mode 100644 index 5921f0174636..000000000000 --- a/instrumentation/tomcat/tomcat-7.0/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/tomcat/v7_0/TomcatAsyncTest.groovy +++ /dev/null @@ -1,133 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.javaagent.instrumentation.tomcat.v7_0 - - -import io.opentelemetry.instrumentation.test.AgentTestTrait -import io.opentelemetry.instrumentation.test.asserts.TraceAssert -import io.opentelemetry.instrumentation.test.base.HttpServerTest -import io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint -import org.apache.catalina.Context -import org.apache.catalina.startup.Tomcat -import org.apache.tomcat.JarScanFilter -import org.apache.tomcat.JarScanType -import spock.lang.Unroll - -import javax.servlet.Servlet -import javax.servlet.ServletException -import java.nio.file.Files - -import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.AUTH_REQUIRED -import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.CAPTURE_HEADERS -import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.ERROR -import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.EXCEPTION -import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.INDEXED_CHILD -import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.NOT_FOUND -import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.QUERY_PARAM -import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.REDIRECT -import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.SUCCESS - -@Unroll -class TomcatAsyncTest extends HttpServerTest implements AgentTestTrait { - - @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 - String getContextPath() { - return "/tomcat-context" - } - - 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, CAPTURE_HEADERS.path, servlet) - addServlet(context, INDEXED_CHILD.path, servlet) - } - - void addServlet(Context servletContext, String path, Class servlet) { - String name = UUID.randomUUID() - Tomcat.addServlet(servletContext, name, servlet.newInstance()) - servletContext.addServletMappingDecoded(path, name) - } - - Class servlet() { - AsyncServlet - } - - @Override - String expectedHttpRoute(ServerEndpoint endpoint) { - switch (endpoint) { - case NOT_FOUND: - return getContextPath() + "/*" - default: - return super.expectedHttpRoute(endpoint) - } - } - - @Override - Throwable expectedException() { - new ServletException(EXCEPTION.body) - } - - @Override - boolean hasResponseSpan(ServerEndpoint endpoint) { - endpoint == NOT_FOUND || endpoint == REDIRECT - } - - @Override - void responseSpan(TraceAssert trace, int index, Object parent, String method, ServerEndpoint endpoint) { - switch (endpoint) { - case REDIRECT: - redirectSpan(trace, index, parent) - break - case NOT_FOUND: - sendErrorSpan(trace, index, parent) - break - } - } -} diff --git a/instrumentation/tomcat/tomcat-7.0/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/tomcat/v7_0/TomcatHandlerTest.groovy b/instrumentation/tomcat/tomcat-7.0/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/tomcat/v7_0/TomcatHandlerTest.groovy deleted file mode 100644 index fa84544f20e7..000000000000 --- a/instrumentation/tomcat/tomcat-7.0/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/tomcat/v7_0/TomcatHandlerTest.groovy +++ /dev/null @@ -1,119 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.javaagent.instrumentation.tomcat.v7_0 - -import io.opentelemetry.instrumentation.test.AgentTestTrait -import io.opentelemetry.instrumentation.test.asserts.TraceAssert -import io.opentelemetry.instrumentation.test.base.HttpServerTest -import io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint -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 static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.AUTH_ERROR -import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.AUTH_REQUIRED -import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.CAPTURE_HEADERS -import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.CAPTURE_PARAMETERS -import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.ERROR -import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.EXCEPTION -import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.INDEXED_CHILD -import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.LOGIN -import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.NOT_FOUND -import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.PATH_PARAM -import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.QUERY_PARAM -import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.REDIRECT -import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.SUCCESS - -class TomcatHandlerTest extends HttpServerTest implements AgentTestTrait { - - private static final List serverEndpointsList = Arrays.asList(SUCCESS, REDIRECT, ERROR, EXCEPTION, NOT_FOUND, CAPTURE_HEADERS, CAPTURE_PARAMETERS, QUERY_PARAM, PATH_PARAM, AUTH_REQUIRED, LOGIN, AUTH_ERROR, INDEXED_CHILD) - - def "Tomcat starts"() { - expect: - getServer() != null - } - - @Override - String getContextPath() { - return "/app" - } - - @Override - boolean hasResponseCustomizer(ServerEndpoint endpoint) { - true - } - - @Override - boolean testCapturedRequestParameters() { - true - } - - @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. - serverEndpointsList.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.isError()) { - 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/instrumentation/tomcat/tomcat-7.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/tomcat/v7_0/AsyncServlet.java b/instrumentation/tomcat/tomcat-7.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/tomcat/v7_0/AsyncServlet.java new file mode 100644 index 000000000000..16cc92a9069f --- /dev/null +++ b/instrumentation/tomcat/tomcat-7.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/tomcat/v7_0/AsyncServlet.java @@ -0,0 +1,82 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.tomcat.v7_0; + +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.CAPTURE_HEADERS; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.ERROR; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.EXCEPTION; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.INDEXED_CHILD; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.QUERY_PARAM; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.REDIRECT; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.SUCCESS; + +import io.opentelemetry.instrumentation.test.base.HttpServerTest; +import io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint; +import java.io.PrintWriter; +import java.util.concurrent.CountDownLatch; +import javax.servlet.AsyncContext; +import javax.servlet.ServletException; +import javax.servlet.annotation.WebServlet; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +@WebServlet(asyncSupported = true) +class AsyncServlet extends HttpServlet { + @Override + protected void service(HttpServletRequest req, HttpServletResponse resp) { + ServerEndpoint endpoint = ServerEndpoint.forPath(req.getServletPath()); + CountDownLatch latch = new CountDownLatch(1); + AsyncContext context = req.startAsync(); + if (endpoint == EXCEPTION) { + context.setTimeout(5000); + } + context.start( + () -> { + try { + HttpServerTest.controller( + endpoint, + () -> { + resp.setContentType("text/plain"); + if (endpoint.equals(SUCCESS) || endpoint.equals(ERROR)) { + resp.setStatus(endpoint.getStatus()); + resp.getWriter().print(endpoint.getBody()); + } else if (endpoint.equals(INDEXED_CHILD)) { + endpoint.collectSpanAttributes(x -> req.getParameter(x)); + resp.setStatus(endpoint.getStatus()); + } else if (endpoint.equals(QUERY_PARAM)) { + resp.setStatus(endpoint.getStatus()); + resp.getWriter().print(req.getQueryString()); + } else if (endpoint.equals(REDIRECT)) { + resp.sendRedirect(endpoint.getBody()); + } else if (endpoint.equals(CAPTURE_HEADERS)) { + resp.setHeader("X-Test-Response", req.getHeader("X-Test-Request")); + resp.setStatus(endpoint.getStatus()); + resp.getWriter().print(endpoint.getBody()); + } else if (endpoint.equals(EXCEPTION)) { + resp.setStatus(endpoint.getStatus()); + PrintWriter writer = resp.getWriter(); + writer.print(endpoint.getBody()); + writer.close(); + throw new ServletException(endpoint.getBody()); + } + return null; + }); + } finally { + // complete at the end so the server span will end after the controller span + if (endpoint != EXCEPTION) { + context.complete(); + } + latch.countDown(); + } + }); + try { + latch.await(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } +} diff --git a/instrumentation/tomcat/tomcat-7.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/tomcat/v7_0/ErrorHandlerValve.java b/instrumentation/tomcat/tomcat-7.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/tomcat/v7_0/ErrorHandlerValve.java new file mode 100644 index 000000000000..d1a6880231c4 --- /dev/null +++ b/instrumentation/tomcat/tomcat-7.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/tomcat/v7_0/ErrorHandlerValve.java @@ -0,0 +1,26 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.tomcat.v7_0; + +import java.io.IOException; +import org.apache.catalina.connector.Request; +import org.apache.catalina.connector.Response; +import org.apache.catalina.valves.ErrorReportValve; + +class ErrorHandlerValve extends ErrorReportValve { + @Override + protected void report(Request request, Response response, Throwable t) { + if (response.getStatus() < 400 || response.getContentWritten() > 0 || !response.isError()) { + return; + } + try { + response.getWriter().print(t != null ? t.getCause().getMessage() : response.getMessage()); + } 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/instrumentation/tomcat/tomcat-7.0/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/tomcat/v7_0/TestServlet.java b/instrumentation/tomcat/tomcat-7.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/tomcat/v7_0/TestServlet.java similarity index 97% rename from instrumentation/tomcat/tomcat-7.0/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/tomcat/v7_0/TestServlet.java rename to instrumentation/tomcat/tomcat-7.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/tomcat/v7_0/TestServlet.java index e80df4226194..85d4bd868cda 100644 --- a/instrumentation/tomcat/tomcat-7.0/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/tomcat/v7_0/TestServlet.java +++ b/instrumentation/tomcat/tomcat-7.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/tomcat/v7_0/TestServlet.java @@ -13,7 +13,7 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -public class TestServlet extends HttpServlet { +class TestServlet extends HttpServlet { @Override protected void service(HttpServletRequest req, HttpServletResponse resp) throws IOException { diff --git a/instrumentation/tomcat/tomcat-7.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/tomcat/v7_0/ThreadPoolExecutorTest.java b/instrumentation/tomcat/tomcat-7.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/tomcat/v7_0/ThreadPoolExecutorTest.java new file mode 100644 index 000000000000..5b1a072754f0 --- /dev/null +++ b/instrumentation/tomcat/tomcat-7.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/tomcat/v7_0/ThreadPoolExecutorTest.java @@ -0,0 +1,86 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.tomcat.v7_0; + +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import org.apache.tomcat.util.threads.TaskQueue; +import org.apache.tomcat.util.threads.ThreadPoolExecutor; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +class ThreadPoolExecutorTest { + + @RegisterExtension + static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + // Test that PropagatedContext isn't cleared when ThreadPoolExecutor.execute fails with + // RejectedExecutionException + @Test + void testTomcatThreadPool() throws InterruptedException { + AtomicBoolean reject = new AtomicBoolean(); + TaskQueue queue = + new TaskQueue() { + @Override + public boolean offer(Runnable o) { + // TaskQueue.offer returns false when parent.getPoolSize() < parent.getMaximumPoolSize() + // here we simulate the same condition to trigger RejectedExecutionException handling in + // tomcat ThreadPoolExecutor + if (reject.get()) { + reject.set(false); + return false; + } + return super.offer(o); + } + }; + + ThreadPoolExecutor pool = new ThreadPoolExecutor(1, 1, 0, TimeUnit.MILLISECONDS, queue); + queue.setParent(pool); + + CountDownLatch latch = new CountDownLatch(1); + + testing.runWithSpan( + "parent", + () -> { + pool.execute( + () -> { + try { + testing.runWithSpan("child1", () -> latch.await()); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + }); + + reject.set(true); + pool.execute( + () -> { + try { + testing.runWithSpan("child2", () -> latch.await()); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + }); + }); + + latch.countDown(); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(), + span -> + span.hasName("child1").hasKind(SpanKind.INTERNAL).hasParent(trace.getSpan(0)), + span -> + span.hasName("child2").hasKind(SpanKind.INTERNAL).hasParent(trace.getSpan(0)))); + + pool.shutdown(); + pool.awaitTermination(10, TimeUnit.SECONDS); + } +} diff --git a/instrumentation/tomcat/tomcat-7.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/tomcat/v7_0/TomcatAsyncTest.java b/instrumentation/tomcat/tomcat-7.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/tomcat/v7_0/TomcatAsyncTest.java new file mode 100644 index 000000000000..3e779cd3ef0e --- /dev/null +++ b/instrumentation/tomcat/tomcat-7.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/tomcat/v7_0/TomcatAsyncTest.java @@ -0,0 +1,124 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.tomcat.v7_0; + +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.AUTH_REQUIRED; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.CAPTURE_HEADERS; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.ERROR; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.EXCEPTION; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.INDEXED_CHILD; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.NOT_FOUND; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.QUERY_PARAM; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.REDIRECT; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.SUCCESS; +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.api.internal.HttpConstants; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpServerTest; +import io.opentelemetry.instrumentation.testing.junit.http.HttpServerInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.http.HttpServerTestOptions; +import io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint; +import io.opentelemetry.sdk.testing.assertj.SpanDataAssert; +import java.io.File; +import java.nio.file.Files; +import java.util.UUID; +import javax.servlet.ServletException; +import org.apache.catalina.Context; +import org.apache.catalina.LifecycleException; +import org.apache.catalina.startup.Tomcat; +import org.junit.jupiter.api.extension.RegisterExtension; + +class TomcatAsyncTest extends AbstractHttpServerTest { + + @RegisterExtension + static final InstrumentationExtension testing = HttpServerInstrumentationExtension.forAgent(); + + @Override + public Tomcat setupServer() throws Exception { + Tomcat tomcatServer = new Tomcat(); + File baseDir = Files.createTempDirectory("tomcat").toFile(); + baseDir.deleteOnExit(); + tomcatServer.setBaseDir(baseDir.getAbsolutePath()); + tomcatServer.setPort(port); + tomcatServer.getConnector().setEnableLookups(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(getContextPath(), applicationDir.getAbsolutePath()); + // Speed up startup by disabling jar scanning: + servletContext.getJarScanner().setJarScanFilter((jarScanType, jarName) -> false); + + setupServlets(servletContext); + tomcatServer.start(); + return tomcatServer; + } + + protected void setupServlets(Context context) throws Exception { + Class servlet = AsyncServlet.class; + + addServlet(context, SUCCESS.getPath(), servlet); + addServlet(context, QUERY_PARAM.getPath(), servlet); + addServlet(context, ERROR.getPath(), servlet); + addServlet(context, EXCEPTION.getPath(), servlet); + addServlet(context, REDIRECT.getPath(), servlet); + addServlet(context, AUTH_REQUIRED.getPath(), servlet); + addServlet(context, CAPTURE_HEADERS.getPath(), servlet); + addServlet(context, INDEXED_CHILD.getPath(), servlet); + } + + void addServlet(Context servletContext, String path, Class servlet) + throws Exception { + String name = UUID.randomUUID().toString(); + Tomcat.addServlet(servletContext, name, servlet.getDeclaredConstructor().newInstance()); + servletContext.addServletMappingDecoded(path, name); + } + + @Override + public void stopServer(Tomcat server) throws LifecycleException { + server.stop(); + server.destroy(); + } + + @Override + protected void configure(HttpServerTestOptions options) { + options.setContextPath("/tomcat-context"); + + options.setExpectedHttpRoute( + (ServerEndpoint endpoint, String method) -> { + if (method.equals(HttpConstants._OTHER)) { + return getContextPath() + endpoint.getPath(); + } + if (endpoint.equals(NOT_FOUND)) { + return getContextPath() + "/*"; + } + return super.expectedHttpRoute(endpoint, method); + }); + + options.setExpectedException(new ServletException(EXCEPTION.getBody())); + + options.setHasResponseSpan(endpoint -> endpoint == NOT_FOUND || endpoint == REDIRECT); + } + + @Override + protected SpanDataAssert assertResponseSpan( + SpanDataAssert span, String method, ServerEndpoint endpoint) { + if (endpoint.equals(REDIRECT)) { + span.satisfies(spanData -> assertThat(spanData.getName()).endsWith(".sendRedirect")); + } else if (endpoint.equals(NOT_FOUND)) { + span.satisfies(spanData -> assertThat(spanData.getName()).endsWith(".sendError")); + } + span.hasKind(SpanKind.INTERNAL).hasAttributesSatisfying(Attributes::isEmpty); + return span; + } +} diff --git a/instrumentation/tomcat/tomcat-7.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/tomcat/v7_0/TomcatHandlerTest.java b/instrumentation/tomcat/tomcat-7.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/tomcat/v7_0/TomcatHandlerTest.java new file mode 100644 index 000000000000..3b02b31a5dd2 --- /dev/null +++ b/instrumentation/tomcat/tomcat-7.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/tomcat/v7_0/TomcatHandlerTest.java @@ -0,0 +1,126 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.tomcat.v7_0; + +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.AUTH_ERROR; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.AUTH_REQUIRED; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.CAPTURE_HEADERS; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.CAPTURE_PARAMETERS; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.ERROR; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.EXCEPTION; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.INDEXED_CHILD; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.LOGIN; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.NOT_FOUND; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.PATH_PARAM; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.QUERY_PARAM; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.REDIRECT; +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.SUCCESS; +import static java.util.Arrays.asList; +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.api.internal.HttpConstants; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpServerTest; +import io.opentelemetry.instrumentation.testing.junit.http.HttpServerInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.http.HttpServerTestOptions; +import io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint; +import io.opentelemetry.sdk.testing.assertj.SpanDataAssert; +import java.io.File; +import java.nio.file.Files; +import java.util.List; +import org.apache.catalina.Context; +import org.apache.catalina.LifecycleException; +import org.apache.catalina.core.StandardHost; +import org.apache.catalina.startup.Tomcat; +import org.junit.jupiter.api.extension.RegisterExtension; + +class TomcatHandlerTest extends AbstractHttpServerTest { + + @RegisterExtension + static final InstrumentationExtension testing = HttpServerInstrumentationExtension.forAgent(); + + private static final List serverEndpointsList = + asList( + SUCCESS, + REDIRECT, + ERROR, + EXCEPTION, + NOT_FOUND, + CAPTURE_HEADERS, + CAPTURE_PARAMETERS, + QUERY_PARAM, + PATH_PARAM, + AUTH_REQUIRED, + LOGIN, + AUTH_ERROR, + INDEXED_CHILD); + + @Override + public Tomcat setupServer() throws Exception { + Tomcat tomcatServer = new Tomcat(); + File baseDir = Files.createTempDirectory("tomcat").toFile(); + baseDir.deleteOnExit(); + tomcatServer.setBaseDir(baseDir.getAbsolutePath()); + tomcatServer.setPort(port); + tomcatServer.getConnector(); + + Context servletContext = + tomcatServer.addContext(getContextPath(), new File(".").getAbsolutePath()); + + Tomcat.addServlet(servletContext, "testServlet", new TestServlet()); + + // Mapping servlet to /* will result in all requests have a name of just a context. + serverEndpointsList.stream() + .filter(endpoint -> !endpoint.equals(NOT_FOUND)) + .forEach( + endpoint -> servletContext.addServletMappingDecoded(endpoint.getPath(), "testServlet")); + + StandardHost host = (StandardHost) tomcatServer.getHost(); + host.setErrorReportValveClass(ErrorHandlerValve.class.getName()); + + tomcatServer.start(); + return tomcatServer; + } + + @Override + public void stopServer(Tomcat server) throws LifecycleException { + server.stop(); + server.destroy(); + } + + @Override + protected void configure(HttpServerTestOptions options) { + options.setContextPath("/app"); + options.setHasResponseCustomizer(serverEndpoint -> true); + options.setTestCaptureRequestParameters(true); + options.setTestErrorBody(false); + + options.setHasResponseSpan( + endpoint -> endpoint == REDIRECT || endpoint == ERROR || endpoint == NOT_FOUND); + + options.setExpectedHttpRoute( + (ServerEndpoint endpoint, String method) -> { + if (method.equals(HttpConstants._OTHER)) { + return getContextPath() + endpoint.getPath(); + } + return super.expectedHttpRoute(endpoint, method); + }); + } + + @Override + protected SpanDataAssert assertResponseSpan( + SpanDataAssert span, String method, ServerEndpoint endpoint) { + if (endpoint.equals(REDIRECT)) { + span.satisfies(spanData -> assertThat(spanData.getName()).endsWith(".sendRedirect")); + } else if (endpoint.equals(NOT_FOUND)) { + span.satisfies(spanData -> assertThat(spanData.getName()).endsWith(".sendError")); + } + span.hasKind(SpanKind.INTERNAL).hasAttributesSatisfying(Attributes::isEmpty); + return span; + } +} diff --git a/instrumentation/tomcat/tomcat-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/tomcat/common/TomcatHttpAttributesGetter.java b/instrumentation/tomcat/tomcat-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/tomcat/common/TomcatHttpAttributesGetter.java index 60b89046a4b2..b8031b31b671 100644 --- a/instrumentation/tomcat/tomcat-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/tomcat/common/TomcatHttpAttributesGetter.java +++ b/instrumentation/tomcat/tomcat-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/tomcat/common/TomcatHttpAttributesGetter.java @@ -7,13 +7,16 @@ import static io.opentelemetry.javaagent.instrumentation.tomcat.common.TomcatHelper.messageBytesToString; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpServerAttributesGetter; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerAttributesGetter; +import java.util.ArrayList; import java.util.Collections; import java.util.List; import javax.annotation.Nullable; +import org.apache.coyote.ActionCode; import org.apache.coyote.Request; import org.apache.coyote.Response; import org.apache.tomcat.util.buf.MessageBytes; +import org.apache.tomcat.util.http.MimeHeaders; public class TomcatHttpAttributesGetter implements HttpServerAttributesGetter { @@ -43,7 +46,17 @@ public String getUrlQuery(Request request) { @Override public List getHttpRequestHeader(Request request, String name) { - return Collections.list(request.getMimeHeaders().values(name)); + List result = null; + MimeHeaders headers = request.getMimeHeaders(); + int i = headers.findHeader(name, 0); + while (i != -1) { + if (result == null) { + result = new ArrayList<>(); + } + result.add(messageBytesToString(headers.getValue(i))); + i = headers.findHeader(name, i + 1); + } + return result != null ? result : Collections.emptyList(); } @Override @@ -57,4 +70,52 @@ public Integer getHttpResponseStatusCode( public List getHttpResponseHeader(Request request, Response response, String name) { return Collections.list(response.getMimeHeaders().values(name)); } + + @Nullable + @Override + public String getNetworkProtocolName(Request request, @Nullable Response response) { + String protocol = messageBytesToString(request.protocol()); + if (protocol != null && protocol.startsWith("HTTP/")) { + return "http"; + } + return null; + } + + @Nullable + @Override + public String getNetworkProtocolVersion(Request request, @Nullable Response response) { + String protocol = messageBytesToString(request.protocol()); + if (protocol != null && protocol.startsWith("HTTP/")) { + return protocol.substring("HTTP/".length()); + } + return null; + } + + @Override + @Nullable + public String getNetworkPeerAddress(Request request, @Nullable Response response) { + request.action(ActionCode.REQ_HOST_ADDR_ATTRIBUTE, request); + return messageBytesToString(request.remoteAddr()); + } + + @Override + @Nullable + public Integer getNetworkPeerPort(Request request, @Nullable Response response) { + request.action(ActionCode.REQ_REMOTEPORT_ATTRIBUTE, request); + return request.getRemotePort(); + } + + @Nullable + @Override + public String getNetworkLocalAddress(Request request, @Nullable Response response) { + request.action(ActionCode.REQ_LOCAL_ADDR_ATTRIBUTE, request); + return messageBytesToString(request.localAddr()); + } + + @Nullable + @Override + public Integer getNetworkLocalPort(Request request, @Nullable Response response) { + request.action(ActionCode.REQ_LOCALPORT_ATTRIBUTE, request); + return request.getLocalPort(); + } } diff --git a/instrumentation/tomcat/tomcat-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/tomcat/common/TomcatInstrumenterFactory.java b/instrumentation/tomcat/tomcat-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/tomcat/common/TomcatInstrumenterFactory.java index 30ca39d7e730..b89d70bc77d3 100644 --- a/instrumentation/tomcat/tomcat-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/tomcat/common/TomcatInstrumenterFactory.java +++ b/instrumentation/tomcat/tomcat-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/tomcat/common/TomcatInstrumenterFactory.java @@ -6,13 +6,17 @@ package io.opentelemetry.javaagent.instrumentation.tomcat.common; import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.instrumentation.api.incubator.semconv.http.HttpExperimentalAttributesExtractor; +import io.opentelemetry.instrumentation.api.incubator.semconv.http.HttpServerExperimentalMetrics; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteHolder; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpServerAttributesExtractor; -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 io.opentelemetry.javaagent.bootstrap.internal.CommonConfig; +import io.opentelemetry.instrumentation.api.instrumenter.InstrumenterBuilder; +import io.opentelemetry.instrumentation.api.internal.InstrumenterUtil; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerAttributesExtractor; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerMetrics; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRoute; +import io.opentelemetry.instrumentation.api.semconv.http.HttpSpanNameExtractor; +import io.opentelemetry.instrumentation.api.semconv.http.HttpSpanStatusExtractor; +import io.opentelemetry.javaagent.bootstrap.internal.AgentCommonConfig; import io.opentelemetry.javaagent.bootstrap.servlet.AppServerBridge; import io.opentelemetry.javaagent.instrumentation.servlet.ServletAccessor; import io.opentelemetry.javaagent.instrumentation.servlet.ServletErrorCauseExtractor; @@ -26,27 +30,39 @@ private TomcatInstrumenterFactory() {} public static Instrumenter create( String instrumentationName, ServletAccessor accessor) { TomcatHttpAttributesGetter httpAttributesGetter = new TomcatHttpAttributesGetter(); - TomcatNetAttributesGetter netAttributesGetter = new TomcatNetAttributesGetter(); - return Instrumenter.builder( - GlobalOpenTelemetry.get(), - instrumentationName, - HttpSpanNameExtractor.create(httpAttributesGetter)) - .setSpanStatusExtractor(HttpSpanStatusExtractor.create(httpAttributesGetter)) - .setErrorCauseExtractor(new ServletErrorCauseExtractor<>(accessor)) - .addAttributesExtractor( - HttpServerAttributesExtractor.builder(httpAttributesGetter, netAttributesGetter) - .setCapturedRequestHeaders(CommonConfig.get().getServerRequestHeaders()) - .setCapturedResponseHeaders(CommonConfig.get().getServerResponseHeaders()) - .build()) - .addContextCustomizer(HttpRouteHolder.create(httpAttributesGetter)) - .addContextCustomizer( - (context, request, attributes) -> - new AppServerBridge.Builder() - .captureServletAttributes() - .recordException() - .init(context)) - .addOperationMetrics(HttpServerMetrics.get()) - .buildServerInstrumenter(TomcatRequestGetter.INSTANCE); + InstrumenterBuilder builder = + Instrumenter.builder( + GlobalOpenTelemetry.get(), + instrumentationName, + HttpSpanNameExtractor.builder(httpAttributesGetter) + .setKnownMethods(AgentCommonConfig.get().getKnownHttpRequestMethods()) + .build()) + .setSpanStatusExtractor(HttpSpanStatusExtractor.create(httpAttributesGetter)) + .setErrorCauseExtractor(new ServletErrorCauseExtractor<>(accessor)) + .addAttributesExtractor( + HttpServerAttributesExtractor.builder(httpAttributesGetter) + .setCapturedRequestHeaders(AgentCommonConfig.get().getServerRequestHeaders()) + .setCapturedResponseHeaders(AgentCommonConfig.get().getServerResponseHeaders()) + .setKnownMethods(AgentCommonConfig.get().getKnownHttpRequestMethods()) + .build()) + .addContextCustomizer( + HttpServerRoute.builder(httpAttributesGetter) + .setKnownMethods(AgentCommonConfig.get().getKnownHttpRequestMethods()) + .build()) + .addContextCustomizer( + (context, request, attributes) -> + new AppServerBridge.Builder() + .captureServletAttributes() + .recordException() + .init(context)) + .addOperationMetrics(HttpServerMetrics.get()); + if (AgentCommonConfig.get().shouldEmitExperimentalHttpServerTelemetry()) { + builder + .addAttributesExtractor(HttpExperimentalAttributesExtractor.create(httpAttributesGetter)) + .addOperationMetrics(HttpServerExperimentalMetrics.get()); + } + InstrumenterUtil.propagateOperationListenersToOnEnd(builder); + return builder.buildServerInstrumenter(TomcatRequestGetter.INSTANCE); } } diff --git a/instrumentation/tomcat/tomcat-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/tomcat/common/TomcatNetAttributesGetter.java b/instrumentation/tomcat/tomcat-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/tomcat/common/TomcatNetAttributesGetter.java deleted file mode 100644 index 92434ceced35..000000000000 --- a/instrumentation/tomcat/tomcat-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/tomcat/common/TomcatNetAttributesGetter.java +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.javaagent.instrumentation.tomcat.common; - -import static io.opentelemetry.javaagent.instrumentation.tomcat.common.TomcatHelper.messageBytesToString; - -import io.opentelemetry.instrumentation.api.instrumenter.net.NetServerAttributesGetter; -import javax.annotation.Nullable; -import org.apache.coyote.ActionCode; -import org.apache.coyote.Request; -import org.apache.coyote.Response; - -public class TomcatNetAttributesGetter implements NetServerAttributesGetter { - - @Nullable - @Override - public String getNetworkProtocolName(Request request, @Nullable Response response) { - String protocol = messageBytesToString(request.protocol()); - if (protocol != null && protocol.startsWith("HTTP/")) { - return "http"; - } - return null; - } - - @Nullable - @Override - public String getNetworkProtocolVersion(Request request, @Nullable Response response) { - String protocol = messageBytesToString(request.protocol()); - if (protocol != null && protocol.startsWith("HTTP/")) { - return protocol.substring("HTTP/".length()); - } - return null; - } - - @Nullable - @Override - public String getServerAddress(Request request) { - return messageBytesToString(request.serverName()); - } - - @Override - public Integer getServerPort(Request request) { - return request.getServerPort(); - } - - @Override - @Nullable - public String getClientSocketAddress(Request request, @Nullable Response response) { - request.action(ActionCode.REQ_HOST_ADDR_ATTRIBUTE, request); - return messageBytesToString(request.remoteAddr()); - } - - @Override - @Nullable - public Integer getClientSocketPort(Request request, @Nullable Response response) { - request.action(ActionCode.REQ_REMOTEPORT_ATTRIBUTE, request); - return request.getRemotePort(); - } - - @Nullable - @Override - public String getServerSocketAddress(Request request, @Nullable Response response) { - request.action(ActionCode.REQ_LOCAL_ADDR_ATTRIBUTE, request); - return messageBytesToString(request.localAddr()); - } - - @Nullable - @Override - public Integer getServerSocketPort(Request request, @Nullable Response response) { - request.action(ActionCode.REQ_LOCALPORT_ATTRIBUTE, request); - return request.getLocalPort(); - } -} diff --git a/instrumentation/tomcat/tomcat-jdbc/src/main/java/io/opentelemetry/javaagent/instrumentation/tomcat/jdbc/TomcatConnectionPoolMetrics.java b/instrumentation/tomcat/tomcat-jdbc/src/main/java/io/opentelemetry/javaagent/instrumentation/tomcat/jdbc/TomcatConnectionPoolMetrics.java index 551c3a9ea9de..3c7d116dc346 100644 --- a/instrumentation/tomcat/tomcat-jdbc/src/main/java/io/opentelemetry/javaagent/instrumentation/tomcat/jdbc/TomcatConnectionPoolMetrics.java +++ b/instrumentation/tomcat/tomcat-jdbc/src/main/java/io/opentelemetry/javaagent/instrumentation/tomcat/jdbc/TomcatConnectionPoolMetrics.java @@ -10,7 +10,7 @@ import io.opentelemetry.api.common.Attributes; import io.opentelemetry.api.metrics.BatchCallback; import io.opentelemetry.api.metrics.ObservableLongMeasurement; -import io.opentelemetry.instrumentation.api.metrics.db.DbConnectionPoolMetrics; +import io.opentelemetry.instrumentation.api.incubator.semconv.db.DbConnectionPoolMetrics; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import org.apache.tomcat.jdbc.pool.DataSourceProxy; diff --git a/instrumentation/tomcat/tomcat-jdbc/src/test/java/io/opentelemetry/javaagent/instrumentation/tomcat/jdbc/TomcatJdbcInstrumentationTest.java b/instrumentation/tomcat/tomcat-jdbc/src/test/java/io/opentelemetry/javaagent/instrumentation/tomcat/jdbc/TomcatJdbcInstrumentationTest.java index 5d0765d153ef..9c95a1c1d21b 100644 --- a/instrumentation/tomcat/tomcat-jdbc/src/test/java/io/opentelemetry/javaagent/instrumentation/tomcat/jdbc/TomcatJdbcInstrumentationTest.java +++ b/instrumentation/tomcat/tomcat-jdbc/src/test/java/io/opentelemetry/javaagent/instrumentation/tomcat/jdbc/TomcatJdbcInstrumentationTest.java @@ -20,7 +20,7 @@ import org.mockito.junit.jupiter.MockitoExtension; @ExtendWith(MockitoExtension.class) -public class TomcatJdbcInstrumentationTest { +class TomcatJdbcInstrumentationTest { @RegisterExtension static final AgentInstrumentationExtension testing = AgentInstrumentationExtension.create(); diff --git a/instrumentation/twilio-6.6/README.md b/instrumentation/twilio-6.6/README.md index 9bfbe30f2f31..23e186fd09c9 100644 --- a/instrumentation/twilio-6.6/README.md +++ b/instrumentation/twilio-6.6/README.md @@ -1,5 +1,5 @@ # Settings for the Twilio instrumentation -| System property | Type | Default | Description | -|---|---|---|---| +| System property | Type | Default | Description | +| ---------------------------------------------------------- | ------- | ------- | ---------------------------------------------------- | | `otel.instrumentation.twilio.experimental-span-attributes` | Boolean | `false` | Enables the capture of experimental span attributes. | diff --git a/instrumentation/twilio-6.6/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/twilio/TwilioSingletons.java b/instrumentation/twilio-6.6/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/twilio/TwilioSingletons.java index e77644850869..3c6b3296e125 100644 --- a/instrumentation/twilio-6.6/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/twilio/TwilioSingletons.java +++ b/instrumentation/twilio-6.6/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/twilio/TwilioSingletons.java @@ -8,15 +8,15 @@ import static io.opentelemetry.instrumentation.api.instrumenter.SpanKindExtractor.alwaysClient; import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.instrumentation.api.incubator.semconv.util.SpanNames; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; import io.opentelemetry.instrumentation.api.instrumenter.InstrumenterBuilder; -import io.opentelemetry.instrumentation.api.instrumenter.util.SpanNames; -import io.opentelemetry.javaagent.bootstrap.internal.InstrumentationConfig; +import io.opentelemetry.javaagent.bootstrap.internal.AgentInstrumentationConfig; public final class TwilioSingletons { private static final boolean CAPTURE_EXPERIMENTAL_SPAN_ATTRIBUTES = - InstrumentationConfig.get() + AgentInstrumentationConfig.get() .getBoolean("otel.instrumentation.twilio.experimental-span-attributes", false); private static final Instrumenter INSTRUMENTER; diff --git a/instrumentation/twilio-6.6/javaagent/src/test/groovy/test/TwilioClientTest.groovy b/instrumentation/twilio-6.6/javaagent/src/test/groovy/test/TwilioClientTest.groovy index 7cede2a78ce8..6c00a4790876 100644 --- a/instrumentation/twilio-6.6/javaagent/src/test/groovy/test/TwilioClientTest.groovy +++ b/instrumentation/twilio-6.6/javaagent/src/test/groovy/test/TwilioClientTest.groovy @@ -15,8 +15,13 @@ 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 io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import io.opentelemetry.semconv.ServerAttributes +import io.opentelemetry.semconv.ErrorAttributes +import io.opentelemetry.semconv.HttpAttributes +import io.opentelemetry.semconv.NetworkAttributes +import io.opentelemetry.semconv.UrlAttributes import org.apache.http.HttpEntity import org.apache.http.StatusLine import org.apache.http.client.methods.CloseableHttpResponse @@ -248,12 +253,11 @@ class TwilioClientTest extends AgentInstrumentationSpecification { kind CLIENT childOf span(1) attributes { - "net.protocol.name" "http" - "net.protocol.version" "1.1" - "$SemanticAttributes.NET_PEER_NAME.key" "api.twilio.com" - "$SemanticAttributes.HTTP_METHOD.key" "POST" - "$SemanticAttributes.HTTP_URL.key" "https://api.twilio.com/2010-04-01/Accounts/abc/Messages.json" - "$SemanticAttributes.HTTP_STATUS_CODE.key" 200 + "$NetworkAttributes.NETWORK_PROTOCOL_VERSION" "1.1" + "$ServerAttributes.SERVER_ADDRESS.key" "api.twilio.com" + "$HttpAttributes.HTTP_REQUEST_METHOD.key" "POST" + "$UrlAttributes.URL_FULL.key" "https://api.twilio.com/2010-04-01/Accounts/abc/Messages.json" + "$HttpAttributes.HTTP_RESPONSE_STATUS_CODE.key" 200 } } } @@ -315,12 +319,12 @@ class TwilioClientTest extends AgentInstrumentationSpecification { childOf span(1) status ERROR attributes { - "net.protocol.name" "http" - "net.protocol.version" "1.1" - "$SemanticAttributes.NET_PEER_NAME.key" "api.twilio.com" - "$SemanticAttributes.HTTP_METHOD.key" "POST" - "$SemanticAttributes.HTTP_URL.key" "https://api.twilio.com/2010-04-01/Accounts/abc/Messages.json" - "$SemanticAttributes.HTTP_STATUS_CODE.key" 500 + "$NetworkAttributes.NETWORK_PROTOCOL_VERSION" "1.1" + "$ServerAttributes.SERVER_ADDRESS.key" "api.twilio.com" + "$HttpAttributes.HTTP_REQUEST_METHOD.key" "POST" + "$UrlAttributes.URL_FULL.key" "https://api.twilio.com/2010-04-01/Accounts/abc/Messages.json" + "$HttpAttributes.HTTP_RESPONSE_STATUS_CODE.key" 500 + "$ErrorAttributes.ERROR_TYPE" "500" } } span(3) { @@ -328,12 +332,11 @@ class TwilioClientTest extends AgentInstrumentationSpecification { kind CLIENT childOf span(1) attributes { - "net.protocol.name" "http" - "net.protocol.version" "1.1" - "$SemanticAttributes.NET_PEER_NAME.key" "api.twilio.com" - "$SemanticAttributes.HTTP_METHOD.key" "POST" - "$SemanticAttributes.HTTP_URL.key" "https://api.twilio.com/2010-04-01/Accounts/abc/Messages.json" - "$SemanticAttributes.HTTP_STATUS_CODE.key" 200 + "$NetworkAttributes.NETWORK_PROTOCOL_VERSION" "1.1" + "$ServerAttributes.SERVER_ADDRESS.key" "api.twilio.com" + "$HttpAttributes.HTTP_REQUEST_METHOD.key" "POST" + "$UrlAttributes.URL_FULL.key" "https://api.twilio.com/2010-04-01/Accounts/abc/Messages.json" + "$HttpAttributes.HTTP_RESPONSE_STATUS_CODE.key" 200 } } } @@ -402,12 +405,12 @@ class TwilioClientTest extends AgentInstrumentationSpecification { childOf span(1) status ERROR attributes { - "net.protocol.name" "http" - "net.protocol.version" "1.1" - "$SemanticAttributes.NET_PEER_NAME.key" "api.twilio.com" - "$SemanticAttributes.HTTP_METHOD.key" "POST" - "$SemanticAttributes.HTTP_URL.key" "https://api.twilio.com/2010-04-01/Accounts/abc/Messages.json" - "$SemanticAttributes.HTTP_STATUS_CODE.key" 500 + "$NetworkAttributes.NETWORK_PROTOCOL_VERSION" "1.1" + "$ServerAttributes.SERVER_ADDRESS.key" "api.twilio.com" + "$HttpAttributes.HTTP_REQUEST_METHOD.key" "POST" + "$UrlAttributes.URL_FULL.key" "https://api.twilio.com/2010-04-01/Accounts/abc/Messages.json" + "$HttpAttributes.HTTP_RESPONSE_STATUS_CODE.key" 500 + "$ErrorAttributes.ERROR_TYPE" "500" } } span(3) { @@ -415,12 +418,11 @@ class TwilioClientTest extends AgentInstrumentationSpecification { kind CLIENT childOf span(1) attributes { - "net.protocol.name" "http" - "net.protocol.version" "1.1" - "$SemanticAttributes.NET_PEER_NAME.key" "api.twilio.com" - "$SemanticAttributes.HTTP_METHOD.key" "POST" - "$SemanticAttributes.HTTP_URL.key" "https://api.twilio.com/2010-04-01/Accounts/abc/Messages.json" - "$SemanticAttributes.HTTP_STATUS_CODE.key" 200 + "$NetworkAttributes.NETWORK_PROTOCOL_VERSION" "1.1" + "$ServerAttributes.SERVER_ADDRESS.key" "api.twilio.com" + "$HttpAttributes.HTTP_REQUEST_METHOD.key" "POST" + "$UrlAttributes.URL_FULL.key" "https://api.twilio.com/2010-04-01/Accounts/abc/Messages.json" + "$HttpAttributes.HTTP_RESPONSE_STATUS_CODE.key" 200 } } } diff --git a/instrumentation/undertow-1.4/javaagent/build.gradle.kts b/instrumentation/undertow-1.4/javaagent/build.gradle.kts index 1b90ab48ac8c..c220eb180396 100644 --- a/instrumentation/undertow-1.4/javaagent/build.gradle.kts +++ b/instrumentation/undertow-1.4/javaagent/build.gradle.kts @@ -20,3 +20,7 @@ dependencies { bootstrap(project(":instrumentation:servlet:servlet-common:bootstrap")) bootstrap(project(":instrumentation:undertow-1.4:bootstrap")) } + +tasks.withType().configureEach { + jvmArgs("-Dotel.instrumentation.common.experimental.controller-telemetry.enabled=true") +} diff --git a/instrumentation/undertow-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/undertow/HandlerInstrumentation.java b/instrumentation/undertow-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/undertow/HandlerInstrumentation.java index 49544426c253..41bce2bdfe85 100644 --- a/instrumentation/undertow-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/undertow/HandlerInstrumentation.java +++ b/instrumentation/undertow-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/undertow/HandlerInstrumentation.java @@ -48,12 +48,12 @@ public static class HandleRequestAdvice { @Advice.OnMethodEnter(suppress = Throwable.class) public static void onEnter( - @Advice.Argument(value = 0, readOnly = false) HttpServerExchange exchange, + @Advice.Argument(0) HttpServerExchange exchange, @Advice.Local("otelContext") Context context, @Advice.Local("otelScope") Scope scope) { Context attachedContext = helper().getServerContext(exchange); if (attachedContext != null) { - if (!Java8BytecodeBridge.currentContext().equals(attachedContext)) { + if (!helper().sameTrace(Java8BytecodeBridge.currentContext(), attachedContext)) { // request processing is dispatched to another thread scope = attachedContext.makeCurrent(); context = attachedContext; diff --git a/instrumentation/undertow-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/undertow/HttpTransferEncodingInstrumentation.java b/instrumentation/undertow-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/undertow/HttpServerConnectionInstrumentation.java similarity index 57% rename from instrumentation/undertow-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/undertow/HttpTransferEncodingInstrumentation.java rename to instrumentation/undertow-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/undertow/HttpServerConnectionInstrumentation.java index 4f6b6e46b801..2352b248b89b 100644 --- a/instrumentation/undertow-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/undertow/HttpTransferEncodingInstrumentation.java +++ b/instrumentation/undertow-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/undertow/HttpServerConnectionInstrumentation.java @@ -5,29 +5,39 @@ package io.opentelemetry.javaagent.instrumentation.undertow; +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.extendsClass; +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.hasClassesNamed; import static io.opentelemetry.javaagent.instrumentation.undertow.UndertowSingletons.helper; import static net.bytebuddy.matcher.ElementMatchers.named; import static net.bytebuddy.matcher.ElementMatchers.takesArgument; import io.opentelemetry.context.Context; +import io.opentelemetry.javaagent.bootstrap.CallDepth; import io.opentelemetry.javaagent.bootstrap.http.HttpServerResponseCustomizerHolder; import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; import io.undertow.server.HttpServerExchange; +import io.undertow.server.ServerConnection; import net.bytebuddy.asm.Advice; import net.bytebuddy.description.type.TypeDescription; import net.bytebuddy.matcher.ElementMatcher; -public class HttpTransferEncodingInstrumentation implements TypeInstrumentation { +public class HttpServerConnectionInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher classLoaderOptimization() { + return hasClassesNamed("io.undertow.server.ServerConnection"); + } + @Override public ElementMatcher typeMatcher() { - return named("io.undertow.server.protocol.http.HttpTransferEncoding"); + return extendsClass(named("io.undertow.server.ServerConnection")); } @Override public void transform(TypeTransformer transformer) { transformer.applyAdviceToMethod( - named("createSinkConduit") + named("getSinkConduit") .and(takesArgument(0, named("io.undertow.server.HttpServerExchange"))), this.getClass().getName() + "$ResponseAdvice"); } @@ -36,10 +46,22 @@ public void transform(TypeTransformer transformer) { public static class ResponseAdvice { @Advice.OnMethodEnter(suppress = Throwable.class) - public static void onEnter(@Advice.Argument(0) HttpServerExchange exchange) { + public static void onEnter( + @Advice.Argument(0) HttpServerExchange exchange, + @Advice.Local("otelCallDepth") CallDepth callDepth) { + callDepth = CallDepth.forClass(ServerConnection.class); + if (callDepth.getAndIncrement() > 0) { + return; + } + Context context = helper().getServerContext(exchange); HttpServerResponseCustomizerHolder.getCustomizer() .customize(context, exchange, UndertowHttpResponseMutator.INSTANCE); } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void onExit(@Advice.Local("otelCallDepth") CallDepth callDepth) { + callDepth.decrementAndGet(); + } } } diff --git a/instrumentation/undertow-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/undertow/HttpServerExchangeInstrumentation.java b/instrumentation/undertow-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/undertow/HttpServerExchangeInstrumentation.java index 702ec4368fd0..421bb924b730 100644 --- a/instrumentation/undertow-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/undertow/HttpServerExchangeInstrumentation.java +++ b/instrumentation/undertow-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/undertow/HttpServerExchangeInstrumentation.java @@ -38,8 +38,7 @@ public void transform(TypeTransformer transformer) { public static class DispatchAdvice { @Advice.OnMethodEnter(suppress = Throwable.class) - public static PropagatedContext enterJobSubmit( - @Advice.Argument(value = 1, readOnly = false) Runnable task) { + public static PropagatedContext enterJobSubmit(@Advice.Argument(1) Runnable task) { Context context = Java8BytecodeBridge.currentContext(); if (ExecutorAdviceHelper.shouldPropagateContext(context, task)) { VirtualField virtualField = diff --git a/instrumentation/undertow-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/undertow/UndertowHelper.java b/instrumentation/undertow-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/undertow/UndertowHelper.java index d6802bb2b8bf..f156267a47d0 100644 --- a/instrumentation/undertow-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/undertow/UndertowHelper.java +++ b/instrumentation/undertow-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/undertow/UndertowHelper.java @@ -5,6 +5,7 @@ package io.opentelemetry.javaagent.instrumentation.undertow; +import io.opentelemetry.api.trace.Span; import io.opentelemetry.context.Context; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; import io.opentelemetry.javaagent.bootstrap.servlet.AppServerBridge; @@ -85,4 +86,12 @@ private static void attachServerContext(Context context, HttpServerExchange exch AttachmentKey.class, key -> AttachmentKey.create(Context.class)); exchange.putAttachment(contextKey, context); } + + public boolean sameTrace(Context currentContext, Context attachedContext) { + return sameTrace(Span.fromContext(currentContext), Span.fromContext(attachedContext)); + } + + private static boolean sameTrace(Span oneSpan, Span otherSpan) { + return oneSpan.getSpanContext().getTraceId().equals(otherSpan.getSpanContext().getTraceId()); + } } diff --git a/instrumentation/undertow-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/undertow/UndertowHttpAttributesGetter.java b/instrumentation/undertow-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/undertow/UndertowHttpAttributesGetter.java index c54ebd27c53d..74f461ab5905 100644 --- a/instrumentation/undertow-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/undertow/UndertowHttpAttributesGetter.java +++ b/instrumentation/undertow-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/undertow/UndertowHttpAttributesGetter.java @@ -5,9 +5,11 @@ package io.opentelemetry.javaagent.instrumentation.undertow; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpServerAttributesGetter; +import io.opentelemetry.instrumentation.api.internal.HttpProtocolUtil; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerAttributesGetter; import io.undertow.server.HttpServerExchange; import io.undertow.util.HeaderValues; +import java.net.InetSocketAddress; import java.util.Collections; import java.util.List; import javax.annotation.Nullable; @@ -54,6 +56,37 @@ public String getUrlPath(HttpServerExchange exchange) { @Nullable @Override public String getUrlQuery(HttpServerExchange exchange) { - return exchange.getQueryString(); + String queryString = exchange.getQueryString(); + // getQueryString returns empty string when query string is missing, we'll return null from + // here instead to void adding empty query string attribute to the span + return !"".equals(queryString) ? queryString : null; + } + + @Nullable + @Override + public String getNetworkProtocolName( + HttpServerExchange exchange, @Nullable HttpServerExchange unused) { + return HttpProtocolUtil.getProtocol(exchange.getProtocol().toString()); + } + + @Nullable + @Override + public String getNetworkProtocolVersion( + HttpServerExchange exchange, @Nullable HttpServerExchange unused) { + return HttpProtocolUtil.getVersion(exchange.getProtocol().toString()); + } + + @Override + @Nullable + public InetSocketAddress getNetworkPeerInetSocketAddress( + HttpServerExchange exchange, @Nullable HttpServerExchange unused) { + return exchange.getConnection().getPeerAddress(InetSocketAddress.class); + } + + @Nullable + @Override + public InetSocketAddress getNetworkLocalInetSocketAddress( + HttpServerExchange exchange, @Nullable HttpServerExchange unused) { + return exchange.getConnection().getLocalAddress(InetSocketAddress.class); } } diff --git a/instrumentation/undertow-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/undertow/UndertowInstrumentationModule.java b/instrumentation/undertow-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/undertow/UndertowInstrumentationModule.java index 10871858f449..98b88e2990a0 100644 --- a/instrumentation/undertow-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/undertow/UndertowInstrumentationModule.java +++ b/instrumentation/undertow-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/undertow/UndertowInstrumentationModule.java @@ -32,6 +32,6 @@ public List typeInstrumentations() { return asList( new HandlerInstrumentation(), new HttpServerExchangeInstrumentation(), - new HttpTransferEncodingInstrumentation()); + new HttpServerConnectionInstrumentation()); } } diff --git a/instrumentation/undertow-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/undertow/UndertowNetAttributesGetter.java b/instrumentation/undertow-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/undertow/UndertowNetAttributesGetter.java deleted file mode 100644 index e89854faf872..000000000000 --- a/instrumentation/undertow-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/undertow/UndertowNetAttributesGetter.java +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.javaagent.instrumentation.undertow; - -import io.opentelemetry.instrumentation.api.instrumenter.net.NetServerAttributesGetter; -import io.undertow.server.HttpServerExchange; -import java.net.InetSocketAddress; -import javax.annotation.Nullable; - -public class UndertowNetAttributesGetter - implements NetServerAttributesGetter { - - @Nullable - @Override - public String getNetworkProtocolName( - HttpServerExchange exchange, @Nullable HttpServerExchange unused) { - String protocol = exchange.getProtocol().toString(); - if (protocol.startsWith("HTTP/")) { - return "http"; - } - return null; - } - - @Nullable - @Override - public String getNetworkProtocolVersion( - HttpServerExchange exchange, @Nullable HttpServerExchange unused) { - String protocol = exchange.getProtocol().toString(); - if (protocol.startsWith("HTTP/")) { - return protocol.substring("HTTP/".length()); - } - return null; - } - - @Nullable - @Override - public String getServerAddress(HttpServerExchange exchange) { - return exchange.getHostName(); - } - - @Nullable - @Override - public Integer getServerPort(HttpServerExchange exchange) { - return exchange.getHostPort(); - } - - @Override - @Nullable - public InetSocketAddress getClientInetSocketAddress( - HttpServerExchange exchange, @Nullable HttpServerExchange unused) { - return exchange.getConnection().getPeerAddress(InetSocketAddress.class); - } - - @Nullable - @Override - public InetSocketAddress getServerInetSocketAddress( - HttpServerExchange exchange, @Nullable HttpServerExchange unused) { - return exchange.getConnection().getLocalAddress(InetSocketAddress.class); - } -} diff --git a/instrumentation/undertow-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/undertow/UndertowSingletons.java b/instrumentation/undertow-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/undertow/UndertowSingletons.java index 7f2f786b1c37..cb4e9007afab 100644 --- a/instrumentation/undertow-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/undertow/UndertowSingletons.java +++ b/instrumentation/undertow-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/undertow/UndertowSingletons.java @@ -6,13 +6,16 @@ package io.opentelemetry.javaagent.instrumentation.undertow; import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.instrumentation.api.incubator.semconv.http.HttpExperimentalAttributesExtractor; +import io.opentelemetry.instrumentation.api.incubator.semconv.http.HttpServerExperimentalMetrics; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteHolder; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpServerAttributesExtractor; -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 io.opentelemetry.javaagent.bootstrap.internal.CommonConfig; +import io.opentelemetry.instrumentation.api.instrumenter.InstrumenterBuilder; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerAttributesExtractor; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerMetrics; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRoute; +import io.opentelemetry.instrumentation.api.semconv.http.HttpSpanNameExtractor; +import io.opentelemetry.instrumentation.api.semconv.http.HttpSpanStatusExtractor; +import io.opentelemetry.javaagent.bootstrap.internal.AgentCommonConfig; import io.opentelemetry.javaagent.bootstrap.servlet.AppServerBridge; import io.opentelemetry.javaagent.bootstrap.undertow.UndertowActiveHandlers; import io.undertow.server.HttpServerExchange; @@ -24,20 +27,25 @@ public final class UndertowSingletons { static { UndertowHttpAttributesGetter httpAttributesGetter = new UndertowHttpAttributesGetter(); - UndertowNetAttributesGetter netAttributesGetter = new UndertowNetAttributesGetter(); - INSTRUMENTER = + InstrumenterBuilder builder = Instrumenter.builder( GlobalOpenTelemetry.get(), INSTRUMENTATION_NAME, - HttpSpanNameExtractor.create(httpAttributesGetter)) + HttpSpanNameExtractor.builder(httpAttributesGetter) + .setKnownMethods(AgentCommonConfig.get().getKnownHttpRequestMethods()) + .build()) .setSpanStatusExtractor(HttpSpanStatusExtractor.create(httpAttributesGetter)) .addAttributesExtractor( - HttpServerAttributesExtractor.builder(httpAttributesGetter, netAttributesGetter) - .setCapturedRequestHeaders(CommonConfig.get().getServerRequestHeaders()) - .setCapturedResponseHeaders(CommonConfig.get().getServerResponseHeaders()) + HttpServerAttributesExtractor.builder(httpAttributesGetter) + .setCapturedRequestHeaders(AgentCommonConfig.get().getServerRequestHeaders()) + .setCapturedResponseHeaders(AgentCommonConfig.get().getServerResponseHeaders()) + .setKnownMethods(AgentCommonConfig.get().getKnownHttpRequestMethods()) + .build()) + .addContextCustomizer( + HttpServerRoute.builder(httpAttributesGetter) + .setKnownMethods(AgentCommonConfig.get().getKnownHttpRequestMethods()) .build()) - .addContextCustomizer(HttpRouteHolder.create(httpAttributesGetter)) .addContextCustomizer( (context, request, attributes) -> { // span is ended when counter reaches 0, we start from 2 which accounts for the @@ -49,8 +57,13 @@ public final class UndertowSingletons { .recordException() .init(context); }) - .addOperationMetrics(HttpServerMetrics.get()) - .buildServerInstrumenter(UndertowExchangeGetter.INSTANCE); + .addOperationMetrics(HttpServerMetrics.get()); + if (AgentCommonConfig.get().shouldEmitExperimentalHttpServerTelemetry()) { + builder + .addAttributesExtractor(HttpExperimentalAttributesExtractor.create(httpAttributesGetter)) + .addOperationMetrics(HttpServerExperimentalMetrics.get()); + } + INSTRUMENTER = builder.buildServerInstrumenter(UndertowExchangeGetter.INSTANCE); } private static final UndertowHelper HELPER = new UndertowHelper(INSTRUMENTER); diff --git a/instrumentation/undertow-1.4/javaagent/src/test/groovy/UndertowHttp2ServerTest.groovy b/instrumentation/undertow-1.4/javaagent/src/test/groovy/UndertowHttp2ServerTest.groovy new file mode 100644 index 000000000000..536e813940b0 --- /dev/null +++ b/instrumentation/undertow-1.4/javaagent/src/test/groovy/UndertowHttp2ServerTest.groovy @@ -0,0 +1,18 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import io.undertow.Undertow +import io.undertow.UndertowOptions + +class UndertowHttp2ServerTest extends UndertowServerTest { + + void configureUndertow(Undertow.Builder builder) { + builder.setServerOption(UndertowOptions.ENABLE_HTTP2, true) + } + + boolean useHttp2() { + true + } +} diff --git a/instrumentation/undertow-1.4/javaagent/src/test/groovy/UndertowServerDispatchTest.groovy b/instrumentation/undertow-1.4/javaagent/src/test/groovy/UndertowServerDispatchTest.groovy index aea4d09b07c9..13a125c2d8e6 100644 --- a/instrumentation/undertow-1.4/javaagent/src/test/groovy/UndertowServerDispatchTest.groovy +++ b/instrumentation/undertow-1.4/javaagent/src/test/groovy/UndertowServerDispatchTest.groovy @@ -7,7 +7,7 @@ import io.opentelemetry.api.common.AttributeKey import io.opentelemetry.instrumentation.test.AgentTestTrait import io.opentelemetry.instrumentation.test.base.HttpServerTest import io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import io.opentelemetry.semconv.HttpAttributes import io.undertow.Handlers import io.undertow.Undertow import io.undertow.util.Headers @@ -105,7 +105,7 @@ class UndertowServerDispatchTest extends HttpServerTest implements Age @Override Set> httpAttributes(ServerEndpoint endpoint) { def attributes = super.httpAttributes(endpoint) - attributes.remove(SemanticAttributes.HTTP_ROUTE) + attributes.remove(HttpAttributes.HTTP_ROUTE) attributes } } diff --git a/instrumentation/undertow-1.4/javaagent/src/test/groovy/UndertowServerTest.groovy b/instrumentation/undertow-1.4/javaagent/src/test/groovy/UndertowServerTest.groovy index aacc32624cc3..7d2a43bbb4f4 100644 --- a/instrumentation/undertow-1.4/javaagent/src/test/groovy/UndertowServerTest.groovy +++ b/instrumentation/undertow-1.4/javaagent/src/test/groovy/UndertowServerTest.groovy @@ -10,7 +10,12 @@ import io.opentelemetry.api.trace.StatusCode import io.opentelemetry.instrumentation.test.AgentTestTrait import io.opentelemetry.instrumentation.test.base.HttpServerTest import io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import io.opentelemetry.semconv.ServerAttributes +import io.opentelemetry.semconv.ClientAttributes +import io.opentelemetry.semconv.UserAgentAttributes +import io.opentelemetry.semconv.HttpAttributes +import io.opentelemetry.semconv.NetworkAttributes +import io.opentelemetry.semconv.UrlAttributes import io.opentelemetry.testing.internal.armeria.common.AggregatedHttpResponse import io.undertow.Handlers import io.undertow.Undertow @@ -31,72 +36,74 @@ class UndertowServerTest extends HttpServerTest implements AgentTestTr @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(CAPTURE_HEADERS.rawPath()) { exchange -> - controller(CAPTURE_HEADERS) { - exchange.setStatusCode(StatusCodes.OK) - exchange.getResponseHeaders().put(new HttpString("X-Test-Response"), exchange.getRequestHeaders().getFirst("X-Test-Request")) - exchange.getResponseSender().send(CAPTURE_HEADERS.body) - } - } - .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(INDEXED_CHILD.rawPath()) { exchange -> - controller(INDEXED_CHILD) { - INDEXED_CHILD.collectSpanAttributes { name -> exchange.getQueryParameters().get(name).peekFirst() } - exchange.getResponseSender().send(INDEXED_CHILD.body) - } - } - .addExactPath("sendResponse") { exchange -> - Span.current().addEvent("before-event") - runWithSpan("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") - runWithSpan("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() + Undertow.Builder builder = 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(CAPTURE_HEADERS.rawPath()) { exchange -> + controller(CAPTURE_HEADERS) { + exchange.setStatusCode(StatusCodes.OK) + exchange.getResponseHeaders().put(new HttpString("X-Test-Response"), exchange.getRequestHeaders().getFirst("X-Test-Request")) + exchange.getResponseSender().send(CAPTURE_HEADERS.body) + } + } + .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(INDEXED_CHILD.rawPath()) { exchange -> + controller(INDEXED_CHILD) { + INDEXED_CHILD.collectSpanAttributes { name -> exchange.getQueryParameters().get(name).peekFirst() } + exchange.getResponseSender().send(INDEXED_CHILD.body) + } + } + .addExactPath("sendResponse") { exchange -> + Span.current().addEvent("before-event") + runWithSpan("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") + runWithSpan("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") + } + ) + configureUndertow(builder) + Undertow server = builder.build() server.start() return server } @@ -106,10 +113,13 @@ class UndertowServerTest extends HttpServerTest implements AgentTestTr undertow.stop() } + void configureUndertow(Undertow.Builder builder) { + } + @Override Set> httpAttributes(ServerEndpoint endpoint) { def attributes = super.httpAttributes(endpoint) - attributes.remove(SemanticAttributes.HTTP_ROUTE) + attributes.remove(HttpAttributes.HTTP_ROUTE) attributes } @@ -142,23 +152,19 @@ class UndertowServerTest extends HttpServerTest implements AgentTestTr eventName "after-event" } + def protocolVersion = useHttp2() ? "2" : "1.1" attributes { - "$SemanticAttributes.HTTP_CLIENT_IP" TEST_CLIENT_IP - "$SemanticAttributes.HTTP_SCHEME" uri.getScheme() - "$SemanticAttributes.HTTP_TARGET" uri.getPath() - "$SemanticAttributes.HTTP_METHOD" "GET" - "$SemanticAttributes.HTTP_STATUS_CODE" 200 - "$SemanticAttributes.USER_AGENT_ORIGINAL" TEST_USER_AGENT - "$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" Long - "$SemanticAttributes.HTTP_SCHEME" "http" - "$SemanticAttributes.HTTP_TARGET" "/sendResponse" - "net.protocol.name" "http" - "net.protocol.version" "1.1" - "$SemanticAttributes.NET_HOST_NAME" uri.host - "$SemanticAttributes.NET_HOST_PORT" uri.port - "$SemanticAttributes.NET_SOCK_PEER_ADDR" "127.0.0.1" - "$SemanticAttributes.NET_SOCK_PEER_PORT" Long - "$SemanticAttributes.NET_SOCK_HOST_ADDR" "127.0.0.1" + "$ClientAttributes.CLIENT_ADDRESS" TEST_CLIENT_IP + "$UrlAttributes.URL_SCHEME" uri.getScheme() + "$UrlAttributes.URL_PATH" uri.getPath() + "$HttpAttributes.HTTP_REQUEST_METHOD" "GET" + "$HttpAttributes.HTTP_RESPONSE_STATUS_CODE" 200 + "$UserAgentAttributes.USER_AGENT_ORIGINAL" TEST_USER_AGENT + "$NetworkAttributes.NETWORK_PROTOCOL_VERSION" protocolVersion + "$ServerAttributes.SERVER_ADDRESS" uri.host + "$ServerAttributes.SERVER_PORT" uri.port + "$NetworkAttributes.NETWORK_PEER_ADDRESS" "127.0.0.1" + "$NetworkAttributes.NETWORK_PEER_PORT" Long } } span(1) { @@ -196,23 +202,19 @@ class UndertowServerTest extends HttpServerTest implements AgentTestTr } errorEvent(Exception, "exception after sending response", 2) + def protocolVersion = useHttp2() ? "2" : "1.1" attributes { - "$SemanticAttributes.HTTP_CLIENT_IP" TEST_CLIENT_IP - "$SemanticAttributes.HTTP_SCHEME" uri.getScheme() - "$SemanticAttributes.HTTP_TARGET" uri.getPath() - "$SemanticAttributes.HTTP_METHOD" "GET" - "$SemanticAttributes.HTTP_STATUS_CODE" 200 - "$SemanticAttributes.USER_AGENT_ORIGINAL" TEST_USER_AGENT - "$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" Long - "$SemanticAttributes.HTTP_SCHEME" "http" - "$SemanticAttributes.HTTP_TARGET" "/sendResponseWithException" - "net.protocol.name" "http" - "net.protocol.version" "1.1" - "$SemanticAttributes.NET_HOST_NAME" uri.host - "$SemanticAttributes.NET_HOST_PORT" uri.port - "$SemanticAttributes.NET_SOCK_PEER_ADDR" "127.0.0.1" - "$SemanticAttributes.NET_SOCK_PEER_PORT" Long - "$SemanticAttributes.NET_SOCK_HOST_ADDR" "127.0.0.1" + "$ClientAttributes.CLIENT_ADDRESS" TEST_CLIENT_IP + "$UrlAttributes.URL_SCHEME" uri.getScheme() + "$UrlAttributes.URL_PATH" uri.getPath() + "$HttpAttributes.HTTP_REQUEST_METHOD" "GET" + "$HttpAttributes.HTTP_RESPONSE_STATUS_CODE" 200 + "$UserAgentAttributes.USER_AGENT_ORIGINAL" TEST_USER_AGENT + "$NetworkAttributes.NETWORK_PROTOCOL_VERSION" protocolVersion + "$ServerAttributes.SERVER_ADDRESS" uri.host + "$ServerAttributes.SERVER_PORT" uri.port + "$NetworkAttributes.NETWORK_PEER_ADDRESS" "127.0.0.1" + "$NetworkAttributes.NETWORK_PEER_PORT" Long } } span(1) { diff --git a/instrumentation/vaadin-14.2/javaagent/build.gradle.kts b/instrumentation/vaadin-14.2/javaagent/build.gradle.kts index b44dd0314e62..dff55a64a534 100644 --- a/instrumentation/vaadin-14.2/javaagent/build.gradle.kts +++ b/instrumentation/vaadin-14.2/javaagent/build.gradle.kts @@ -59,14 +59,16 @@ testing { val vaadin14LatestTest by registering(JvmTestSuite::class) { dependencies { implementation(project(":instrumentation:vaadin-14.2:testing")) - implementation("com.vaadin:vaadin-spring-boot-starter:14.+") + // 14.12 requires license + implementation("com.vaadin:vaadin-spring-boot-starter:14.11.+") } } val vaadinLatestTest by registering(JvmTestSuite::class) { dependencies { implementation(project(":instrumentation:vaadin-14.2:testing")) - implementation("com.vaadin:vaadin-spring-boot-starter:+") + // tests fail with 24.4.1 + implementation("com.vaadin:vaadin-spring-boot-starter:24.3.13") } } } @@ -95,3 +97,7 @@ configurations.configureEach { } } } +tasks.withType().configureEach { + jvmArgs("-Dotel.instrumentation.common.experimental.controller-telemetry.enabled=true") + jvmArgs("-Dotel.instrumentation.common.experimental.view-telemetry.enabled=true") +} diff --git a/instrumentation/vaadin-14.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vaadin/ClientCallableCodeAttributesGetter.java b/instrumentation/vaadin-14.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vaadin/ClientCallableCodeAttributesGetter.java index 37c29a30846c..25eecc2ea59e 100644 --- a/instrumentation/vaadin-14.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vaadin/ClientCallableCodeAttributesGetter.java +++ b/instrumentation/vaadin-14.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vaadin/ClientCallableCodeAttributesGetter.java @@ -5,7 +5,7 @@ package io.opentelemetry.javaagent.instrumentation.vaadin; -import io.opentelemetry.instrumentation.api.instrumenter.code.CodeAttributesGetter; +import io.opentelemetry.instrumentation.api.incubator.semconv.code.CodeAttributesGetter; public class ClientCallableCodeAttributesGetter implements CodeAttributesGetter { diff --git a/instrumentation/vaadin-14.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vaadin/VaadinHandlerRequest.java b/instrumentation/vaadin-14.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vaadin/VaadinHandlerRequest.java index e4d599322f77..0a820e9aa988 100644 --- a/instrumentation/vaadin-14.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vaadin/VaadinHandlerRequest.java +++ b/instrumentation/vaadin-14.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vaadin/VaadinHandlerRequest.java @@ -6,7 +6,7 @@ package io.opentelemetry.javaagent.instrumentation.vaadin; import com.google.auto.value.AutoValue; -import io.opentelemetry.instrumentation.api.instrumenter.util.SpanNames; +import io.opentelemetry.instrumentation.api.incubator.semconv.util.SpanNames; @AutoValue public abstract class VaadinHandlerRequest { diff --git a/instrumentation/vaadin-14.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vaadin/VaadinHelper.java b/instrumentation/vaadin-14.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vaadin/VaadinHelper.java index 5dbbcc54f658..0a55dbf212b4 100644 --- a/instrumentation/vaadin-14.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vaadin/VaadinHelper.java +++ b/instrumentation/vaadin-14.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vaadin/VaadinHelper.java @@ -12,8 +12,8 @@ import com.vaadin.flow.router.Location; import io.opentelemetry.context.Context; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteHolder; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteSource; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRoute; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRouteSource; import io.opentelemetry.javaagent.bootstrap.servlet.ServletContextPath; import javax.annotation.Nullable; @@ -42,10 +42,10 @@ public void endVaadinServiceSpan( Context context, VaadinServiceRequest request, Throwable throwable) { serviceInstrumenter.end(context, request, null, throwable); - HttpRouteHolder.updateHttpRoute( + HttpServerRoute.update( context, - HttpRouteSource.CONTROLLER, - (c, req) -> getSpanNameForVaadinServiceContext(c, req), + HttpServerRouteSource.CONTROLLER, + VaadinHelper::getSpanNameForVaadinServiceContext, request); } @@ -107,10 +107,10 @@ public void updateServerSpanName(UI ui) { public void updateServerSpanName(Location location) { Context context = Context.current(); - HttpRouteHolder.updateHttpRoute( + HttpServerRoute.update( context, - HttpRouteSource.NESTED_CONTROLLER, - (c, loc) -> ServletContextPath.prepend(c, getSpanNameForLocation(loc)), + HttpServerRouteSource.NESTED_CONTROLLER, + (ctx, loc) -> ServletContextPath.prepend(ctx, getSpanNameForLocation(loc)), location); } diff --git a/instrumentation/vaadin-14.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vaadin/VaadinServiceRequest.java b/instrumentation/vaadin-14.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vaadin/VaadinServiceRequest.java index 12fbadb69b7e..02bfcf2b1a69 100644 --- a/instrumentation/vaadin-14.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vaadin/VaadinServiceRequest.java +++ b/instrumentation/vaadin-14.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vaadin/VaadinServiceRequest.java @@ -6,7 +6,7 @@ package io.opentelemetry.javaagent.instrumentation.vaadin; import com.google.auto.value.AutoValue; -import io.opentelemetry.instrumentation.api.instrumenter.util.SpanNames; +import io.opentelemetry.instrumentation.api.incubator.semconv.util.SpanNames; @AutoValue public abstract class VaadinServiceRequest { diff --git a/instrumentation/vaadin-14.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vaadin/VaadinSingletons.java b/instrumentation/vaadin-14.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vaadin/VaadinSingletons.java index 4b46dd63bf60..2422b9a1d82a 100644 --- a/instrumentation/vaadin-14.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vaadin/VaadinSingletons.java +++ b/instrumentation/vaadin-14.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vaadin/VaadinSingletons.java @@ -8,10 +8,10 @@ import com.vaadin.flow.server.communication.rpc.RpcInvocationHandler; import io.opentelemetry.api.GlobalOpenTelemetry; import io.opentelemetry.context.ContextKey; +import io.opentelemetry.instrumentation.api.incubator.semconv.code.CodeAttributesExtractor; +import io.opentelemetry.instrumentation.api.incubator.semconv.code.CodeSpanNameExtractor; +import io.opentelemetry.instrumentation.api.incubator.semconv.util.SpanNames; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; -import io.opentelemetry.instrumentation.api.instrumenter.code.CodeAttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.code.CodeSpanNameExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.util.SpanNames; import io.opentelemetry.javaagent.bootstrap.internal.ExperimentalConfig; public class VaadinSingletons { diff --git a/instrumentation/vaadin-14.2/javaagent/src/vaadin16Test/java/io/opentelemetry/javaagent/instrumentation/vaadin/Vaadin16Test.java b/instrumentation/vaadin-14.2/javaagent/src/vaadin16Test/java/io/opentelemetry/javaagent/instrumentation/vaadin/Vaadin16Test.java index 7c9ea5ac1e9a..f51f621c2c60 100644 --- a/instrumentation/vaadin-14.2/javaagent/src/vaadin16Test/java/io/opentelemetry/javaagent/instrumentation/vaadin/Vaadin16Test.java +++ b/instrumentation/vaadin-14.2/javaagent/src/vaadin16Test/java/io/opentelemetry/javaagent/instrumentation/vaadin/Vaadin16Test.java @@ -5,11 +5,10 @@ package io.opentelemetry.javaagent.instrumentation.vaadin; -import static org.assertj.core.api.Assertions.assertThat; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; import static org.awaitility.Awaitility.await; import io.opentelemetry.api.trace.SpanKind; -import io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions; import io.opentelemetry.sdk.testing.assertj.TracesAssert; import io.opentelemetry.sdk.trace.data.SpanData; import java.io.File; @@ -40,32 +39,32 @@ void assertFirstRequest() { assertThat(trace.get(0)) .satisfies( spans -> - OpenTelemetryAssertions.assertThat(spans.get(0)) + assertThat(spans.get(0)) .hasName("GET IndexHtmlRequestHandler.handleRequest") .hasNoParent() .hasKind(SpanKind.SERVER)); assertThat(trace) .anySatisfy( spans -> { - OpenTelemetryAssertions.assertThat(spans.get(0)) + assertThat(spans.get(0)) .hasName("POST " + getContextPath() + "/main") .hasNoParent() .hasKind(SpanKind.SERVER); - OpenTelemetryAssertions.assertThat(spans.get(1)) + assertThat(spans.get(1)) .hasName("SpringVaadinServletService.handleRequest") .hasParent(spans.get(0)) .hasKind(SpanKind.INTERNAL); // we don't assert all the handler spans as these vary between // vaadin versions - OpenTelemetryAssertions.assertThat(spans.get(spans.size() - 3)) + assertThat(spans.get(spans.size() - 3)) .hasName("UidlRequestHandler.handleRequest") .hasParent(spans.get(1)) .hasKind(SpanKind.INTERNAL); - OpenTelemetryAssertions.assertThat(spans.get(spans.size() - 2)) + assertThat(spans.get(spans.size() - 2)) .hasName("PublishedServerEventHandlerRpcHandler.handle") .hasParent(spans.get(spans.size() - 3)) .hasKind(SpanKind.INTERNAL); - OpenTelemetryAssertions.assertThat(spans.get(spans.size() - 1)) + assertThat(spans.get(spans.size() - 1)) .hasName("JavaScriptBootstrapUI.connectClient") .hasParent(spans.get(spans.size() - 2)) .hasKind(SpanKind.INTERNAL); diff --git a/instrumentation/vaadin-14.2/javaagent/src/vaadinLatestTest/java/io/opentelemetry/javaagent/instrumentation/vaadin/VaadinLatestTest.java b/instrumentation/vaadin-14.2/javaagent/src/vaadinLatestTest/java/io/opentelemetry/javaagent/instrumentation/vaadin/VaadinLatestTest.java index e92817a3acba..ab3fcd05974c 100644 --- a/instrumentation/vaadin-14.2/javaagent/src/vaadinLatestTest/java/io/opentelemetry/javaagent/instrumentation/vaadin/VaadinLatestTest.java +++ b/instrumentation/vaadin-14.2/javaagent/src/vaadinLatestTest/java/io/opentelemetry/javaagent/instrumentation/vaadin/VaadinLatestTest.java @@ -5,11 +5,10 @@ package io.opentelemetry.javaagent.instrumentation.vaadin; -import static org.assertj.core.api.Assertions.assertThat; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; import static org.awaitility.Awaitility.await; import io.opentelemetry.api.trace.SpanKind; -import io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions; import io.opentelemetry.sdk.testing.assertj.TracesAssert; import io.opentelemetry.sdk.trace.data.SpanData; import java.util.List; @@ -28,32 +27,32 @@ void assertFirstRequest() { assertThat(trace.get(0)) .satisfies( spans -> - OpenTelemetryAssertions.assertThat(spans.get(0)) + assertThat(spans.get(0)) .hasName("GET IndexHtmlRequestHandler.handleRequest") .hasNoParent() .hasKind(SpanKind.SERVER)); assertThat(trace) .anySatisfy( spans -> { - OpenTelemetryAssertions.assertThat(spans.get(0)) + assertThat(spans.get(0)) .hasName("POST " + getContextPath()) .hasNoParent() .hasKind(SpanKind.SERVER); - OpenTelemetryAssertions.assertThat(spans.get(1)) + assertThat(spans.get(1)) .hasName("SpringVaadinServletService.handleRequest") .hasParent(spans.get(0)) .hasKind(SpanKind.INTERNAL); // we don't assert all the handler spans as these vary between // vaadin versions - OpenTelemetryAssertions.assertThat(spans.get(spans.size() - 3)) + assertThat(spans.get(spans.size() - 3)) .hasName("UidlRequestHandler.handleRequest") .hasParent(spans.get(1)) .hasKind(SpanKind.INTERNAL); - OpenTelemetryAssertions.assertThat(spans.get(spans.size() - 2)) + assertThat(spans.get(spans.size() - 2)) .hasName("PublishedServerEventHandlerRpcHandler.handle") .hasParent(spans.get(spans.size() - 3)) .hasKind(SpanKind.INTERNAL); - OpenTelemetryAssertions.assertThat(spans.get(spans.size() - 1)) + assertThat(spans.get(spans.size() - 1)) .hasName("UI.connectClient") .hasParent(spans.get(spans.size() - 2)) .hasKind(SpanKind.INTERNAL); diff --git a/instrumentation/vaadin-14.2/testing/src/main/java/io/opentelemetry/javaagent/instrumentation/vaadin/AbstractVaadin14Test.java b/instrumentation/vaadin-14.2/testing/src/main/java/io/opentelemetry/javaagent/instrumentation/vaadin/AbstractVaadin14Test.java index fff1388fa74f..82eb7ee48dd8 100644 --- a/instrumentation/vaadin-14.2/testing/src/main/java/io/opentelemetry/javaagent/instrumentation/vaadin/AbstractVaadin14Test.java +++ b/instrumentation/vaadin-14.2/testing/src/main/java/io/opentelemetry/javaagent/instrumentation/vaadin/AbstractVaadin14Test.java @@ -5,11 +5,10 @@ package io.opentelemetry.javaagent.instrumentation.vaadin; -import static org.assertj.core.api.Assertions.assertThat; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; import static org.awaitility.Awaitility.await; import io.opentelemetry.api.trace.SpanKind; -import io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions; import io.opentelemetry.sdk.trace.data.SpanData; import java.util.List; @@ -23,17 +22,17 @@ void assertFirstRequest() { assertThat(traces.get(0)) .satisfies( spans -> { - OpenTelemetryAssertions.assertThat(spans.get(0)) + assertThat(spans.get(0)) .hasName("GET " + getContextPath() + "/main") .hasNoParent() .hasKind(SpanKind.SERVER); - OpenTelemetryAssertions.assertThat(spans.get(1)) + assertThat(spans.get(1)) .hasName("SpringVaadinServletService.handleRequest") .hasParent(spans.get(0)) .hasKind(SpanKind.INTERNAL); // we don't assert all the handler spans as these vary between // vaadin versions - OpenTelemetryAssertions.assertThat(spans.get(spans.size() - 1)) + assertThat(spans.get(spans.size() - 1)) .hasName("BootstrapHandler.handleRequest") .hasParent(spans.get(1)) .hasKind(SpanKind.INTERNAL); diff --git a/instrumentation/vaadin-14.2/testing/src/main/java/io/opentelemetry/javaagent/instrumentation/vaadin/AbstractVaadinTest.java b/instrumentation/vaadin-14.2/testing/src/main/java/io/opentelemetry/javaagent/instrumentation/vaadin/AbstractVaadinTest.java index 161a574119ed..f80635f5cae2 100644 --- a/instrumentation/vaadin-14.2/testing/src/main/java/io/opentelemetry/javaagent/instrumentation/vaadin/AbstractVaadinTest.java +++ b/instrumentation/vaadin-14.2/testing/src/main/java/io/opentelemetry/javaagent/instrumentation/vaadin/AbstractVaadinTest.java @@ -5,7 +5,7 @@ package io.opentelemetry.javaagent.instrumentation.vaadin; -import static org.assertj.core.api.Assertions.assertThat; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; import static org.awaitility.Awaitility.await; import com.vaadin.flow.server.Version; @@ -14,7 +14,6 @@ import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpServerUsingTest; import io.opentelemetry.instrumentation.testing.junit.http.HttpServerInstrumentationExtension; -import io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions; import io.opentelemetry.sdk.trace.data.SpanData; import java.io.File; import java.io.IOException; @@ -154,21 +153,21 @@ private void assertButtonClick() { assertThat(traces.get(0)) .satisfies( spans -> { - OpenTelemetryAssertions.assertThat(spans.get(0)) + assertThat(spans.get(0)) .hasName("POST " + getContextPath() + "/main") .hasNoParent() .hasKind(SpanKind.SERVER); - OpenTelemetryAssertions.assertThat(spans.get(1)) + assertThat(spans.get(1)) .hasName("SpringVaadinServletService.handleRequest") .hasParent(spans.get(0)) .hasKind(SpanKind.INTERNAL); // we don't assert all the handler spans as these vary between // vaadin versions - OpenTelemetryAssertions.assertThat(spans.get(spans.size() - 2)) + assertThat(spans.get(spans.size() - 2)) .hasName("UidlRequestHandler.handleRequest") .hasParent(spans.get(1)) .hasKind(SpanKind.INTERNAL); - OpenTelemetryAssertions.assertThat(spans.get(spans.size() - 1)) + assertThat(spans.get(spans.size() - 1)) .hasName("EventRpcHandler.handle/click") .hasParent(spans.get(spans.size() - 2)) .hasKind(SpanKind.INTERNAL); diff --git a/instrumentation/vaadin-14.2/testing/src/main/java/test/vaadin/MainView.java b/instrumentation/vaadin-14.2/testing/src/main/java/test/vaadin/MainView.java index 6bbb4eba46e5..c9a1a4ccea83 100644 --- a/instrumentation/vaadin-14.2/testing/src/main/java/test/vaadin/MainView.java +++ b/instrumentation/vaadin-14.2/testing/src/main/java/test/vaadin/MainView.java @@ -10,6 +10,7 @@ import com.vaadin.flow.component.html.Label; import com.vaadin.flow.component.orderedlayout.VerticalLayout; import com.vaadin.flow.router.Route; +import java.lang.reflect.Method; @Route("main") public class MainView extends VerticalLayout { @@ -19,8 +20,18 @@ 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 button = new Button("To other view", e -> navigate(OtherView.class)); button.setId("main.button"); add(label, button); } + + private static void navigate(Class navigationTarget) { + try { + // using reflection because return type of the method changes from void to Optional + Method method = UI.class.getMethod("navigate", Class.class); + method.invoke(UI.getCurrent(), navigationTarget); + } catch (Exception exception) { + throw new IllegalStateException(exception); + } + } } diff --git a/instrumentation/vertx/vertx-http-client/vertx-http-client-3.0/javaagent/build.gradle.kts b/instrumentation/vertx/vertx-http-client/vertx-http-client-3.0/javaagent/build.gradle.kts index 6e052d49f205..b35cab840bd8 100644 --- a/instrumentation/vertx/vertx-http-client/vertx-http-client-3.0/javaagent/build.gradle.kts +++ b/instrumentation/vertx/vertx-http-client/vertx-http-client-3.0/javaagent/build.gradle.kts @@ -36,7 +36,7 @@ dependencies { } tasks { - test { + withType().configureEach { systemProperty("testLatestDeps", findProperty("testLatestDeps") as Boolean) } } diff --git a/instrumentation/vertx/vertx-http-client/vertx-http-client-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v3_0/client/Vertx3HttpAttributesGetter.java b/instrumentation/vertx/vertx-http-client/vertx-http-client-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v3_0/client/Vertx3HttpAttributesGetter.java index e823b75c992a..fc5b55fd9609 100644 --- a/instrumentation/vertx/vertx-http-client/vertx-http-client-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v3_0/client/Vertx3HttpAttributesGetter.java +++ b/instrumentation/vertx/vertx-http-client/vertx-http-client-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v3_0/client/Vertx3HttpAttributesGetter.java @@ -8,6 +8,7 @@ import io.opentelemetry.instrumentation.api.util.VirtualField; import io.opentelemetry.javaagent.instrumentation.vertx.client.AbstractVertxHttpAttributesGetter; import io.vertx.core.http.HttpClientRequest; +import javax.annotation.Nullable; final class Vertx3HttpAttributesGetter extends AbstractVertxHttpAttributesGetter { @@ -21,11 +22,33 @@ public String getUrlFull(HttpClientRequest request) { // where relative is expected. if (!isAbsolute(uri)) { VertxRequestInfo requestInfo = requestInfoField.get(request); - uri = absoluteUri(requestInfo, uri); + if (requestInfo != null) { + uri = absoluteUri(requestInfo, uri); + } } return uri; } + @Nullable + @Override + public String getServerAddress(HttpClientRequest request) { + VertxRequestInfo requestInfo = requestInfoField.get(request); + if (requestInfo == null) { + return null; + } + return requestInfo.getHost(); + } + + @Nullable + @Override + public Integer getServerPort(HttpClientRequest request) { + VertxRequestInfo requestInfo = requestInfoField.get(request); + if (requestInfo == null) { + return null; + } + return requestInfo.getPort(); + } + private static boolean isAbsolute(String uri) { return uri.startsWith("http://") || uri.startsWith("https://"); } diff --git a/instrumentation/vertx/vertx-http-client/vertx-http-client-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v3_0/client/Vertx3NetAttributesGetter.java b/instrumentation/vertx/vertx-http-client/vertx-http-client-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v3_0/client/Vertx3NetAttributesGetter.java deleted file mode 100644 index de42ad0ec932..000000000000 --- a/instrumentation/vertx/vertx-http-client/vertx-http-client-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v3_0/client/Vertx3NetAttributesGetter.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.javaagent.instrumentation.vertx.v3_0.client; - -import io.opentelemetry.instrumentation.api.instrumenter.net.NetClientAttributesGetter; -import io.vertx.core.http.HttpClientRequest; -import io.vertx.core.http.HttpClientResponse; -import io.vertx.core.net.SocketAddress; -import javax.annotation.Nullable; - -enum Vertx3NetAttributesGetter - implements NetClientAttributesGetter { - INSTANCE; - - @Nullable - @Override - public String getServerAddress(HttpClientRequest request) { - return null; - } - - @Override - public Integer getServerPort(HttpClientRequest request) { - return null; - } - - @Nullable - @Override - public String getServerSocketDomain( - HttpClientRequest request, @Nullable HttpClientResponse response) { - if (response == null) { - return null; - } - SocketAddress socketAddress = response.netSocket().remoteAddress(); - return socketAddress == null ? null : socketAddress.host(); - } - - @Nullable - @Override - public Integer getServerSocketPort( - HttpClientRequest request, @Nullable HttpClientResponse response) { - if (response == null) { - return null; - } - SocketAddress socketAddress = response.netSocket().remoteAddress(); - return socketAddress == null ? null : socketAddress.port(); - } -} diff --git a/instrumentation/vertx/vertx-http-client/vertx-http-client-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v3_0/client/VertxClientSingletons.java b/instrumentation/vertx/vertx-http-client/vertx-http-client-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v3_0/client/VertxClientSingletons.java index d204fcaa3cc4..9153de8e39b7 100644 --- a/instrumentation/vertx/vertx-http-client/vertx-http-client-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v3_0/client/VertxClientSingletons.java +++ b/instrumentation/vertx/vertx-http-client/vertx-http-client-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v3_0/client/VertxClientSingletons.java @@ -14,9 +14,7 @@ public final class VertxClientSingletons { private static final Instrumenter INSTRUMENTER = VertxClientInstrumenterFactory.create( - "io.opentelemetry.vertx-http-client-3.0", - new Vertx3HttpAttributesGetter(), - Vertx3NetAttributesGetter.INSTANCE); + "io.opentelemetry.vertx-http-client-3.0", new Vertx3HttpAttributesGetter()); public static Instrumenter instrumenter() { return INSTRUMENTER; diff --git a/instrumentation/vertx/vertx-http-client/vertx-http-client-3.0/javaagent/src/test/groovy/client/VertxHttpClientTest.groovy b/instrumentation/vertx/vertx-http-client/vertx-http-client-3.0/javaagent/src/test/groovy/client/VertxHttpClientTest.groovy deleted file mode 100644 index e1174cf52686..000000000000 --- a/instrumentation/vertx/vertx-http-client/vertx-http-client-3.0/javaagent/src/test/groovy/client/VertxHttpClientTest.groovy +++ /dev/null @@ -1,105 +0,0 @@ -/* - * 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.testing.junit.http.HttpClientResult -import io.opentelemetry.instrumentation.testing.junit.http.SingleConnection -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes -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 spock.lang.Shared - -import java.util.concurrent.CompletableFuture - -import static io.opentelemetry.api.common.AttributeKey.stringKey - -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.requestAbs(HttpMethod.valueOf(method), "$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, HttpClientResult 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 - boolean testReadTimeout() { - false - } - - @Override - Set> httpAttributes(URI uri) { - def attributes = super.httpAttributes(uri) - attributes.remove(stringKey("net.protocol.name")) - attributes.remove(stringKey("net.protocol.version")) - attributes.remove(SemanticAttributes.NET_PEER_NAME) - attributes.remove(SemanticAttributes.NET_PEER_PORT) - return attributes - } - - @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/instrumentation/vertx/vertx-http-client/vertx-http-client-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/vertx/v3_0/client/VertxHttpClientTest.java b/instrumentation/vertx/vertx-http-client/vertx-http-client-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/vertx/v3_0/client/VertxHttpClientTest.java new file mode 100644 index 000000000000..6699579bb757 --- /dev/null +++ b/instrumentation/vertx/vertx-http-client/vertx-http-client-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/vertx/v3_0/client/VertxHttpClientTest.java @@ -0,0 +1,108 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.vertx.v3_0.client; + +import client.VertxSingleConnection; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpClientTest; +import io.opentelemetry.instrumentation.testing.junit.http.HttpClientInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.http.HttpClientResult; +import io.opentelemetry.instrumentation.testing.junit.http.HttpClientTestOptions; +import io.opentelemetry.instrumentation.testing.junit.http.SingleConnection; +import io.opentelemetry.semconv.NetworkAttributes; +import io.opentelemetry.semconv.ServerAttributes; +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.HttpMethod; +import java.net.URI; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.extension.RegisterExtension; + +class VertxHttpClientTest extends AbstractHttpClientTest { + + @RegisterExtension + static final InstrumentationExtension testing = HttpClientInstrumentationExtension.forAgent(); + + private final HttpClient httpClient = buildClient(); + + private static HttpClient buildClient() { + Vertx vertx = Vertx.vertx(new VertxOptions()); + HttpClientOptions clientOptions = + new HttpClientOptions().setConnectTimeout(Math.toIntExact(CONNECTION_TIMEOUT.toMillis())); + return vertx.createHttpClient(clientOptions); + } + + @Override + public HttpClientRequest buildRequest(String method, URI uri, Map headers) { + HttpClientRequest request = httpClient.requestAbs(HttpMethod.valueOf(method), uri.toString()); + headers.forEach(request::putHeader); + return request; + } + + private static CompletableFuture sendRequest(HttpClientRequest request) { + CompletableFuture future = new CompletableFuture<>(); + request + .handler(response -> future.complete(response.statusCode())) + .exceptionHandler(future::completeExceptionally) + .end(); + return future; + } + + @Override + public int sendRequest( + HttpClientRequest request, String method, URI uri, Map headers) + throws Exception { + // Vertx doesn't seem to provide any synchronous API so bridge through a callback + return sendRequest(request).get(30, TimeUnit.SECONDS); + } + + @Override + public void sendRequestWithCallback( + HttpClientRequest request, + String method, + URI uri, + Map headers, + HttpClientResult httpClientResult) { + sendRequest(request) + .whenComplete((status, throwable) -> httpClientResult.complete(() -> status, throwable)); + } + + @Override + protected void configure(HttpClientTestOptions.Builder optionsBuilder) { + optionsBuilder.disableTestRedirects(); + optionsBuilder.disableTestReusedRequest(); + optionsBuilder.disableTestHttps(); + optionsBuilder.disableTestReadTimeout(); + optionsBuilder.disableTestNonStandardHttpMethod(); + + optionsBuilder.setHttpAttributes( + uri -> { + Set> attributes = + new HashSet<>(HttpClientTestOptions.DEFAULT_HTTP_ATTRIBUTES); + attributes.remove(NetworkAttributes.NETWORK_PROTOCOL_VERSION); + attributes.remove(ServerAttributes.SERVER_ADDRESS); + attributes.remove(ServerAttributes.SERVER_PORT); + return attributes; + }); + + optionsBuilder.setSingleConnectionFactory(VertxHttpClientTest::createSingleConnection); + } + + private static 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/instrumentation/vertx/vertx-http-client/vertx-http-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/client/Vertx4HttpAttributesGetter.java b/instrumentation/vertx/vertx-http-client/vertx-http-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/client/Vertx4HttpAttributesGetter.java index 2788420e5157..a9c64e8cce75 100644 --- a/instrumentation/vertx/vertx-http-client/vertx-http-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/client/Vertx4HttpAttributesGetter.java +++ b/instrumentation/vertx/vertx-http-client/vertx-http-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/client/Vertx4HttpAttributesGetter.java @@ -7,6 +7,10 @@ import io.opentelemetry.javaagent.instrumentation.vertx.client.AbstractVertxHttpAttributesGetter; import io.vertx.core.http.HttpClientRequest; +import io.vertx.core.http.HttpClientResponse; +import io.vertx.core.http.HttpVersion; +import io.vertx.core.net.SocketAddress; +import javax.annotation.Nullable; final class Vertx4HttpAttributesGetter extends AbstractVertxHttpAttributesGetter { @@ -27,4 +31,62 @@ private static boolean isAbsolute(String uri) { public String getHttpRequestMethod(HttpClientRequest request) { return request.getMethod().name(); } + + @Override + public String getNetworkProtocolName( + HttpClientRequest request, @Nullable HttpClientResponse response) { + return "http"; + } + + @Nullable + @Override + public String getNetworkProtocolVersion( + HttpClientRequest request, @Nullable HttpClientResponse response) { + HttpVersion version = request.version(); + if (version == null) { + return null; + } + switch (version) { + case HTTP_1_0: + return "1.0"; + case HTTP_1_1: + return "1.1"; + case HTTP_2: + return "2"; + } + return null; + } + + @Nullable + @Override + public String getServerAddress(HttpClientRequest request) { + return request.getHost(); + } + + @Override + public Integer getServerPort(HttpClientRequest request) { + return request.getPort(); + } + + @Nullable + @Override + public String getNetworkPeerAddress( + HttpClientRequest request, @Nullable HttpClientResponse response) { + if (response == null) { + return null; + } + SocketAddress socketAddress = response.netSocket().remoteAddress(); + return socketAddress == null ? null : socketAddress.hostAddress(); + } + + @Nullable + @Override + public Integer getNetworkPeerPort( + HttpClientRequest request, @Nullable HttpClientResponse response) { + if (response == null) { + return null; + } + SocketAddress socketAddress = response.netSocket().remoteAddress(); + return socketAddress == null ? null : socketAddress.port(); + } } diff --git a/instrumentation/vertx/vertx-http-client/vertx-http-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/client/Vertx4NetAttributesGetter.java b/instrumentation/vertx/vertx-http-client/vertx-http-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/client/Vertx4NetAttributesGetter.java deleted file mode 100644 index ec2f06a6b688..000000000000 --- a/instrumentation/vertx/vertx-http-client/vertx-http-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/client/Vertx4NetAttributesGetter.java +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.javaagent.instrumentation.vertx.v4_0.client; - -import io.opentelemetry.instrumentation.api.instrumenter.net.NetClientAttributesGetter; -import io.vertx.core.http.HttpClientRequest; -import io.vertx.core.http.HttpClientResponse; -import io.vertx.core.http.HttpVersion; -import io.vertx.core.net.SocketAddress; -import javax.annotation.Nullable; - -final class Vertx4NetAttributesGetter - implements NetClientAttributesGetter { - - @Override - public String getNetworkProtocolName( - HttpClientRequest request, @Nullable HttpClientResponse response) { - return "http"; - } - - @Nullable - @Override - public String getNetworkProtocolVersion( - HttpClientRequest request, @Nullable HttpClientResponse response) { - HttpVersion version = request.version(); - if (version == null) { - return null; - } - switch (version) { - case HTTP_1_0: - return "1.0"; - case HTTP_1_1: - return "1.1"; - case HTTP_2: - return "2.0"; - } - return null; - } - - @Nullable - @Override - public String getServerAddress(HttpClientRequest request) { - return request.getHost(); - } - - @Override - public Integer getServerPort(HttpClientRequest request) { - return request.getPort(); - } - - @Nullable - @Override - public String getServerSocketAddress( - HttpClientRequest request, @Nullable HttpClientResponse response) { - if (response == null) { - return null; - } - SocketAddress socketAddress = response.netSocket().remoteAddress(); - return socketAddress == null ? null : socketAddress.hostAddress(); - } - - @Nullable - @Override - public String getServerSocketDomain( - HttpClientRequest request, @Nullable HttpClientResponse response) { - if (response == null) { - return null; - } - SocketAddress socketAddress = response.netSocket().remoteAddress(); - return socketAddress == null ? null : socketAddress.host(); - } - - @Nullable - @Override - public Integer getServerSocketPort( - HttpClientRequest request, @Nullable HttpClientResponse response) { - if (response == null) { - return null; - } - SocketAddress socketAddress = response.netSocket().remoteAddress(); - return socketAddress == null ? null : socketAddress.port(); - } -} diff --git a/instrumentation/vertx/vertx-http-client/vertx-http-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/client/VertxClientSingletons.java b/instrumentation/vertx/vertx-http-client/vertx-http-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/client/VertxClientSingletons.java index 75097937708a..1e14fa9f4f44 100644 --- a/instrumentation/vertx/vertx-http-client/vertx-http-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/client/VertxClientSingletons.java +++ b/instrumentation/vertx/vertx-http-client/vertx-http-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/client/VertxClientSingletons.java @@ -14,9 +14,7 @@ public final class VertxClientSingletons { private static final Instrumenter INSTRUMENTER = VertxClientInstrumenterFactory.create( - "io.opentelemetry.vertx-http-client-4.0", - new Vertx4HttpAttributesGetter(), - new Vertx4NetAttributesGetter()); + "io.opentelemetry.vertx-http-client-4.0", new Vertx4HttpAttributesGetter()); public static Instrumenter instrumenter() { return INSTRUMENTER; diff --git a/instrumentation/vertx/vertx-http-client/vertx-http-client-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/client/AbstractVertxHttpAttributesGetter.java b/instrumentation/vertx/vertx-http-client/vertx-http-client-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/client/AbstractVertxHttpAttributesGetter.java index fbeadf66b97c..7274e257e0b7 100644 --- a/instrumentation/vertx/vertx-http-client/vertx-http-client-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/client/AbstractVertxHttpAttributesGetter.java +++ b/instrumentation/vertx/vertx-http-client/vertx-http-client-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/client/AbstractVertxHttpAttributesGetter.java @@ -5,7 +5,7 @@ package io.opentelemetry.javaagent.instrumentation.vertx.client; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpClientAttributesGetter; +import io.opentelemetry.instrumentation.api.semconv.http.HttpClientAttributesGetter; import io.vertx.core.http.HttpClientRequest; import io.vertx.core.http.HttpClientResponse; import java.util.List; diff --git a/instrumentation/vertx/vertx-http-client/vertx-http-client-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/client/VertxClientInstrumenterFactory.java b/instrumentation/vertx/vertx-http-client/vertx-http-client-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/client/VertxClientInstrumenterFactory.java index 3dc7e8afb442..5f4801f43e13 100644 --- a/instrumentation/vertx/vertx-http-client/vertx-http-client-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/client/VertxClientInstrumenterFactory.java +++ b/instrumentation/vertx/vertx-http-client/vertx-http-client-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/client/VertxClientInstrumenterFactory.java @@ -5,43 +5,18 @@ package io.opentelemetry.javaagent.instrumentation.vertx.client; -import io.opentelemetry.api.GlobalOpenTelemetry; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; -import io.opentelemetry.instrumentation.api.instrumenter.InstrumenterBuilder; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpClientAttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpClientMetrics; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpSpanNameExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpSpanStatusExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.net.NetClientAttributesGetter; -import io.opentelemetry.instrumentation.api.instrumenter.net.PeerServiceAttributesExtractor; -import io.opentelemetry.javaagent.bootstrap.internal.CommonConfig; +import io.opentelemetry.javaagent.bootstrap.internal.JavaagentHttpClientInstrumenters; import io.vertx.core.http.HttpClientRequest; import io.vertx.core.http.HttpClientResponse; public final class VertxClientInstrumenterFactory { public static Instrumenter create( - String instrumentationName, - AbstractVertxHttpAttributesGetter httpAttributesGetter, - NetClientAttributesGetter netAttributesGetter) { + String instrumentationName, AbstractVertxHttpAttributesGetter httpAttributesGetter) { - InstrumenterBuilder builder = - Instrumenter.builder( - GlobalOpenTelemetry.get(), - instrumentationName, - HttpSpanNameExtractor.create(httpAttributesGetter)) - .setSpanStatusExtractor(HttpSpanStatusExtractor.create(httpAttributesGetter)) - .addAttributesExtractor( - HttpClientAttributesExtractor.builder(httpAttributesGetter, netAttributesGetter) - .setCapturedRequestHeaders(CommonConfig.get().getClientRequestHeaders()) - .setCapturedResponseHeaders(CommonConfig.get().getClientResponseHeaders()) - .build()) - .addAttributesExtractor( - PeerServiceAttributesExtractor.create( - netAttributesGetter, CommonConfig.get().getPeerServiceMapping())) - .addOperationMetrics(HttpClientMetrics.get()); - - return builder.buildClientInstrumenter(new HttpRequestHeaderSetter()); + return JavaagentHttpClientInstrumenters.create( + instrumentationName, httpAttributesGetter, new HttpRequestHeaderSetter()); } private VertxClientInstrumenterFactory() {} diff --git a/instrumentation/vertx/vertx-kafka-client-3.6/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/kafka/v3_6/VertxKafkaSingletons.java b/instrumentation/vertx/vertx-kafka-client-3.6/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/kafka/v3_6/VertxKafkaSingletons.java index 5b09afb40110..afdfe735b92b 100644 --- a/instrumentation/vertx/vertx-kafka-client-3.6/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/kafka/v3_6/VertxKafkaSingletons.java +++ b/instrumentation/vertx/vertx-kafka-client-3.6/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/kafka/v3_6/VertxKafkaSingletons.java @@ -10,8 +10,8 @@ import io.opentelemetry.instrumentation.kafka.internal.KafkaInstrumenterFactory; import io.opentelemetry.instrumentation.kafka.internal.KafkaProcessRequest; import io.opentelemetry.instrumentation.kafka.internal.KafkaReceiveRequest; +import io.opentelemetry.javaagent.bootstrap.internal.AgentInstrumentationConfig; import io.opentelemetry.javaagent.bootstrap.internal.ExperimentalConfig; -import io.opentelemetry.javaagent.bootstrap.internal.InstrumentationConfig; public final class VertxKafkaSingletons { @@ -25,7 +25,7 @@ public final class VertxKafkaSingletons { new KafkaInstrumenterFactory(GlobalOpenTelemetry.get(), INSTRUMENTATION_NAME) .setCapturedHeaders(ExperimentalConfig.get().getMessagingHeaders()) .setCaptureExperimentalSpanAttributes( - InstrumentationConfig.get() + AgentInstrumentationConfig.get() .getBoolean("otel.instrumentation.kafka.experimental-span-attributes", false)) .setMessagingReceiveInstrumentationEnabled( ExperimentalConfig.get().messagingReceiveInstrumentationEnabled()); diff --git a/instrumentation/vertx/vertx-kafka-client-3.6/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/vertx/kafka/v3_6/BatchRecordsVertxKafkaTest.java b/instrumentation/vertx/vertx-kafka-client-3.6/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/vertx/kafka/v3_6/BatchRecordsVertxKafkaTest.java index ebe96288d710..8e0eda18a78f 100644 --- a/instrumentation/vertx/vertx-kafka-client-3.6/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/vertx/kafka/v3_6/BatchRecordsVertxKafkaTest.java +++ b/instrumentation/vertx/vertx-kafka-client-3.6/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/vertx/kafka/v3_6/BatchRecordsVertxKafkaTest.java @@ -60,12 +60,12 @@ void shouldCreateSpansForBatchReceiveAndProcess() throws InterruptedException { trace.hasSpansSatisfyingExactlyInAnyOrder( span -> span.hasName("producer"), span -> - span.hasName("testBatchTopic send") + span.hasName("testBatchTopic publish") .hasKind(SpanKind.PRODUCER) .hasParent(trace.getSpan(0)) .hasAttributesSatisfyingExactly(sendAttributes(record1)), span -> - span.hasName("testBatchTopic send") + span.hasName("testBatchTopic publish") .hasKind(SpanKind.PRODUCER) .hasParent(trace.getSpan(0)) .hasAttributesSatisfyingExactly(sendAttributes(record2))); @@ -131,7 +131,7 @@ void shouldHandleFailureInKafkaBatchListener() throws InterruptedException { trace.hasSpansSatisfyingExactly( span -> span.hasName("producer"), span -> - span.hasName("testBatchTopic send") + span.hasName("testBatchTopic publish") .hasKind(SpanKind.PRODUCER) .hasParent(trace.getSpan(0)) .hasAttributesSatisfyingExactly(sendAttributes(record))); diff --git a/instrumentation/vertx/vertx-kafka-client-3.6/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/vertx/kafka/v3_6/SingleRecordVertxKafkaTest.java b/instrumentation/vertx/vertx-kafka-client-3.6/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/vertx/kafka/v3_6/SingleRecordVertxKafkaTest.java index 1cf2d317677b..8cfd1043f422 100644 --- a/instrumentation/vertx/vertx-kafka-client-3.6/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/vertx/kafka/v3_6/SingleRecordVertxKafkaTest.java +++ b/instrumentation/vertx/vertx-kafka-client-3.6/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/vertx/kafka/v3_6/SingleRecordVertxKafkaTest.java @@ -55,7 +55,7 @@ void shouldCreateSpansForSingleRecordProcess() throws InterruptedException { trace.hasSpansSatisfyingExactly( span -> span.hasName("producer"), span -> - span.hasName("testSingleTopic send") + span.hasName("testSingleTopic publish") .hasKind(SpanKind.PRODUCER) .hasParent(trace.getSpan(0)) .hasAttributesSatisfyingExactly(sendAttributes(record))); @@ -96,7 +96,7 @@ void shouldHandleFailureInSingleRecordHandler() throws InterruptedException { trace.hasSpansSatisfyingExactly( span -> span.hasName("producer"), span -> - span.hasName("testSingleTopic send") + span.hasName("testSingleTopic publish") .hasKind(SpanKind.PRODUCER) .hasParent(trace.getSpan(0)) .hasAttributesSatisfyingExactly(sendAttributes(record))); diff --git a/instrumentation/vertx/vertx-kafka-client-3.6/javaagent/src/testNoReceiveTelemetry/java/io/opentelemetry/javaagent/instrumentation/vertx/kafka/v3_6/NoReceiveTelemetryBatchRecordsVertxKafkaTest.java b/instrumentation/vertx/vertx-kafka-client-3.6/javaagent/src/testNoReceiveTelemetry/java/io/opentelemetry/javaagent/instrumentation/vertx/kafka/v3_6/NoReceiveTelemetryBatchRecordsVertxKafkaTest.java index f13fa40d8cb9..99ff87a9a9dc 100644 --- a/instrumentation/vertx/vertx-kafka-client-3.6/javaagent/src/testNoReceiveTelemetry/java/io/opentelemetry/javaagent/instrumentation/vertx/kafka/v3_6/NoReceiveTelemetryBatchRecordsVertxKafkaTest.java +++ b/instrumentation/vertx/vertx-kafka-client-3.6/javaagent/src/testNoReceiveTelemetry/java/io/opentelemetry/javaagent/instrumentation/vertx/kafka/v3_6/NoReceiveTelemetryBatchRecordsVertxKafkaTest.java @@ -62,7 +62,7 @@ void shouldCreateSpansForBatchReceiveAndProcess() throws InterruptedException { // first record span -> - span.hasName("testBatchTopic send") + span.hasName("testBatchTopic publish") .hasKind(SpanKind.PRODUCER) .hasParent(trace.getSpan(0)) .hasAttributesSatisfyingExactly(sendAttributes(record1)), @@ -75,7 +75,7 @@ void shouldCreateSpansForBatchReceiveAndProcess() throws InterruptedException { // second record span -> - span.hasName("testBatchTopic send") + span.hasName("testBatchTopic publish") .hasKind(SpanKind.PRODUCER) .hasParent(trace.getSpan(0)) .hasAttributesSatisfyingExactly(sendAttributes(record2)), @@ -123,7 +123,7 @@ void shouldHandleFailureInKafkaBatchListener() throws InterruptedException { trace.hasSpansSatisfyingExactly( span -> span.hasName("producer"), span -> - span.hasName("testBatchTopic send") + span.hasName("testBatchTopic publish") .hasKind(SpanKind.PRODUCER) .hasParent(trace.getSpan(0)) .hasAttributesSatisfyingExactly(sendAttributes(record)), diff --git a/instrumentation/vertx/vertx-kafka-client-3.6/javaagent/src/testNoReceiveTelemetry/java/io/opentelemetry/javaagent/instrumentation/vertx/kafka/v3_6/NoReceiveTelemetrySingleRecordVertxKafkaTest.java b/instrumentation/vertx/vertx-kafka-client-3.6/javaagent/src/testNoReceiveTelemetry/java/io/opentelemetry/javaagent/instrumentation/vertx/kafka/v3_6/NoReceiveTelemetrySingleRecordVertxKafkaTest.java index e824b55476ca..446e7356d037 100644 --- a/instrumentation/vertx/vertx-kafka-client-3.6/javaagent/src/testNoReceiveTelemetry/java/io/opentelemetry/javaagent/instrumentation/vertx/kafka/v3_6/NoReceiveTelemetrySingleRecordVertxKafkaTest.java +++ b/instrumentation/vertx/vertx-kafka-client-3.6/javaagent/src/testNoReceiveTelemetry/java/io/opentelemetry/javaagent/instrumentation/vertx/kafka/v3_6/NoReceiveTelemetrySingleRecordVertxKafkaTest.java @@ -48,7 +48,7 @@ void shouldCreateSpansForSingleRecordProcess() throws InterruptedException { trace.hasSpansSatisfyingExactly( span -> span.hasName("producer"), span -> - span.hasName("testSingleTopic send") + span.hasName("testSingleTopic publish") .hasKind(SpanKind.PRODUCER) .hasParent(trace.getSpan(0)) .hasAttributesSatisfyingExactly(sendAttributes(record)), @@ -75,7 +75,7 @@ void shouldHandleFailureInSingleRecordHandler() throws InterruptedException { trace.hasSpansSatisfyingExactly( span -> span.hasName("producer"), span -> - span.hasName("testSingleTopic send") + span.hasName("testSingleTopic publish") .hasKind(SpanKind.PRODUCER) .hasParent(trace.getSpan(0)) .hasAttributesSatisfyingExactly(sendAttributes(record)), diff --git a/instrumentation/vertx/vertx-kafka-client-3.6/testing/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/kafka/v3_6/AbstractVertxKafkaTest.java b/instrumentation/vertx/vertx-kafka-client-3.6/testing/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/kafka/v3_6/AbstractVertxKafkaTest.java index 67344abf49bb..59d3b0bbe28f 100644 --- a/instrumentation/vertx/vertx-kafka-client-3.6/testing/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/kafka/v3_6/AbstractVertxKafkaTest.java +++ b/instrumentation/vertx/vertx-kafka-client-3.6/testing/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/kafka/v3_6/AbstractVertxKafkaTest.java @@ -13,7 +13,7 @@ import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; import io.opentelemetry.sdk.testing.assertj.AttributeAssertion; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes; import io.vertx.core.AsyncResult; import io.vertx.core.Handler; import io.vertx.core.Vertx; @@ -35,6 +35,7 @@ import org.apache.kafka.common.serialization.StringDeserializer; import org.apache.kafka.common.serialization.StringSerializer; import org.assertj.core.api.AbstractLongAssert; +import org.assertj.core.api.AbstractStringAssert; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.extension.RegisterExtension; @@ -60,7 +61,7 @@ public abstract class AbstractVertxKafkaTest { @BeforeAll static void setUpAll() { kafka = - new KafkaContainer(DockerImageName.parse("confluentinc/cp-kafka:6.2.10")) + new KafkaContainer(DockerImageName.parse("confluentinc/cp-kafka:7.7.0")) .withEnv("KAFKA_HEAP_OPTS", "-Xmx256m") .withLogConsumer(new Slf4jLogConsumer(logger)) .waitingFor(Wait.forLogMessage(".*started \\(kafka.server.KafkaServer\\).*", 1)) @@ -199,20 +200,22 @@ protected static List sendAttributes( List assertions = new ArrayList<>( Arrays.asList( - equalTo(SemanticAttributes.MESSAGING_SYSTEM, "kafka"), - equalTo(SemanticAttributes.MESSAGING_DESTINATION_NAME, record.topic()), + equalTo(MessagingIncubatingAttributes.MESSAGING_SYSTEM, "kafka"), + equalTo(MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME, record.topic()), + equalTo(MessagingIncubatingAttributes.MESSAGING_OPERATION, "publish"), satisfies( - SemanticAttributes.MESSAGING_KAFKA_CLIENT_ID, + MessagingIncubatingAttributes.MESSAGING_CLIENT_ID, stringAssert -> stringAssert.startsWith("producer")), satisfies( - SemanticAttributes.MESSAGING_KAFKA_DESTINATION_PARTITION, - AbstractLongAssert::isNotNegative), + MessagingIncubatingAttributes.MESSAGING_DESTINATION_PARTITION_ID, + AbstractStringAssert::isNotEmpty), satisfies( - SemanticAttributes.MESSAGING_KAFKA_MESSAGE_OFFSET, + MessagingIncubatingAttributes.MESSAGING_KAFKA_MESSAGE_OFFSET, AbstractLongAssert::isNotNegative))); String messageKey = record.key(); if (messageKey != null) { - assertions.add(equalTo(SemanticAttributes.MESSAGING_KAFKA_MESSAGE_KEY, messageKey)); + assertions.add( + equalTo(MessagingIncubatingAttributes.MESSAGING_KAFKA_MESSAGE_KEY, messageKey)); } return assertions; } @@ -229,19 +232,18 @@ private static List batchConsumerAttributes(String topic, St List assertions = new ArrayList<>( Arrays.asList( - equalTo(SemanticAttributes.MESSAGING_SYSTEM, "kafka"), - equalTo(SemanticAttributes.MESSAGING_DESTINATION_NAME, topic), - equalTo(SemanticAttributes.MESSAGING_OPERATION, operation), + equalTo(MessagingIncubatingAttributes.MESSAGING_SYSTEM, "kafka"), + equalTo(MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME, topic), + equalTo(MessagingIncubatingAttributes.MESSAGING_OPERATION, operation), + satisfies( + MessagingIncubatingAttributes.MESSAGING_CLIENT_ID, + stringAssert -> stringAssert.startsWith("consumer")), satisfies( - SemanticAttributes.MESSAGING_KAFKA_CLIENT_ID, - stringAssert -> stringAssert.startsWith("consumer")))); - // consumer group id is not available in version 0.11 + MessagingIncubatingAttributes.MESSAGING_BATCH_MESSAGE_COUNT, + AbstractLongAssert::isPositive))); + // consumer group is not available in version 0.11 if (Boolean.getBoolean("testLatestDeps")) { - assertions.add(equalTo(SemanticAttributes.MESSAGING_KAFKA_CONSUMER_GROUP, "test")); - assertions.add( - satisfies( - SemanticAttributes.MESSAGING_CONSUMER_ID, - stringAssert -> stringAssert.startsWith("test - consumer"))); + assertions.add(equalTo(MessagingIncubatingAttributes.MESSAGING_KAFKA_CONSUMER_GROUP, "test")); } return assertions; } @@ -251,17 +253,17 @@ protected static List processAttributes( List assertions = new ArrayList<>( Arrays.asList( - equalTo(SemanticAttributes.MESSAGING_SYSTEM, "kafka"), - equalTo(SemanticAttributes.MESSAGING_DESTINATION_NAME, record.topic()), - equalTo(SemanticAttributes.MESSAGING_OPERATION, "process"), + equalTo(MessagingIncubatingAttributes.MESSAGING_SYSTEM, "kafka"), + equalTo(MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME, record.topic()), + equalTo(MessagingIncubatingAttributes.MESSAGING_OPERATION, "process"), satisfies( - SemanticAttributes.MESSAGING_KAFKA_CLIENT_ID, + MessagingIncubatingAttributes.MESSAGING_CLIENT_ID, stringAssert -> stringAssert.startsWith("consumer")), satisfies( - SemanticAttributes.MESSAGING_KAFKA_SOURCE_PARTITION, - AbstractLongAssert::isNotNegative), + MessagingIncubatingAttributes.MESSAGING_DESTINATION_PARTITION_ID, + AbstractStringAssert::isNotEmpty), satisfies( - SemanticAttributes.MESSAGING_KAFKA_MESSAGE_OFFSET, + MessagingIncubatingAttributes.MESSAGING_KAFKA_MESSAGE_OFFSET, AbstractLongAssert::isNotNegative))); if (Boolean.getBoolean("otel.instrumentation.kafka.experimental-span-attributes")) { assertions.add( @@ -269,23 +271,20 @@ protected static List processAttributes( AttributeKey.longKey("kafka.record.queue_time_ms"), AbstractLongAssert::isNotNegative)); } - // consumer group id is not available in version 0.11 + // consumer group is not available in version 0.11 if (Boolean.getBoolean("testLatestDeps")) { - assertions.add(equalTo(SemanticAttributes.MESSAGING_KAFKA_CONSUMER_GROUP, "test")); - assertions.add( - satisfies( - SemanticAttributes.MESSAGING_CONSUMER_ID, - stringAssert -> stringAssert.startsWith("test - consumer"))); + assertions.add(equalTo(MessagingIncubatingAttributes.MESSAGING_KAFKA_CONSUMER_GROUP, "test")); } String messageKey = record.key(); if (messageKey != null) { - assertions.add(equalTo(SemanticAttributes.MESSAGING_KAFKA_MESSAGE_KEY, messageKey)); + assertions.add( + equalTo(MessagingIncubatingAttributes.MESSAGING_KAFKA_MESSAGE_KEY, messageKey)); } String messageValue = record.value(); if (messageValue != null) { assertions.add( equalTo( - SemanticAttributes.MESSAGING_MESSAGE_PAYLOAD_SIZE_BYTES, + MessagingIncubatingAttributes.MESSAGING_MESSAGE_BODY_SIZE, messageValue.getBytes(StandardCharsets.UTF_8).length)); } return assertions; diff --git a/instrumentation/vertx/vertx-redis-client-4.0/javaagent/build.gradle.kts b/instrumentation/vertx/vertx-redis-client-4.0/javaagent/build.gradle.kts new file mode 100644 index 000000000000..748df2ab83cf --- /dev/null +++ b/instrumentation/vertx/vertx-redis-client-4.0/javaagent/build.gradle.kts @@ -0,0 +1,27 @@ +plugins { + id("otel.javaagent-instrumentation") +} + +muzzle { + pass { + group.set("io.vertx") + module.set("vertx-redis-client") + versions.set("[4.0.0,)") + assertInverse.set(true) + } +} + +dependencies { + library("io.vertx:vertx-redis-client:4.0.0") + compileOnly("io.vertx:vertx-codegen:4.0.0") + + testInstrumentation(project(":instrumentation:netty:netty-4.1:javaagent")) + + testLibrary("io.vertx:vertx-codegen:4.0.0") +} + +tasks { + withType().configureEach { + usesService(gradle.sharedServices.registrations["testcontainersBuildService"].service) + } +} diff --git a/instrumentation/vertx/vertx-redis-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/redis/CommandImplInstrumentation.java b/instrumentation/vertx/vertx-redis-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/redis/CommandImplInstrumentation.java new file mode 100644 index 000000000000..f62aa15d9a6a --- /dev/null +++ b/instrumentation/vertx/vertx-redis-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/redis/CommandImplInstrumentation.java @@ -0,0 +1,38 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.vertx.v4_0.redis; + +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.vertx.redis.client.impl.CommandImpl; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class CommandImplInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher typeMatcher() { + return named("io.vertx.redis.client.impl.CommandImpl"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isConstructor(), this.getClass().getName() + "$ConstructorAdvice"); + } + + @SuppressWarnings("unused") + public static class ConstructorAdvice { + @Advice.OnMethodExit(suppress = Throwable.class) + public static void onExit( + @Advice.This CommandImpl command, @Advice.Argument(0) String commandName) { + VertxRedisClientSingletons.setCommandName(command, commandName); + } + } +} diff --git a/instrumentation/vertx/vertx-redis-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/redis/RedisConnectionProviderInstrumentation.java b/instrumentation/vertx/vertx-redis-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/redis/RedisConnectionProviderInstrumentation.java new file mode 100644 index 000000000000..664acfbeaf8a --- /dev/null +++ b/instrumentation/vertx/vertx-redis-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/redis/RedisConnectionProviderInstrumentation.java @@ -0,0 +1,68 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.vertx.v4_0.redis; + +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 io.vertx.redis.client.RedisConnection; +import io.vertx.redis.client.impl.RedisStandaloneConnection; +import io.vertx.redis.client.impl.RedisURI; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class RedisConnectionProviderInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher typeMatcher() { + return named("io.vertx.redis.client.impl.RedisConnectionManager$RedisConnectionProvider"); + } + + @Override + public void transform(TypeTransformer transformer) { + // 4.1.0 + transformer.applyAdviceToMethod( + named("init").and(not(takesArgument(0, named("io.vertx.redis.client.RedisConnection")))), + this.getClass().getName() + "$InitAdvice"); + // 4.0.0 + transformer.applyAdviceToMethod( + named("init").and(takesArgument(0, named("io.vertx.redis.client.RedisConnection"))), + this.getClass().getName() + "$InitWithConnectionAdvice"); + } + + @SuppressWarnings("unused") + public static class InitAdvice { + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter(@Advice.FieldValue("redisURI") RedisURI redisUri) { + // for 4.1.0 and later we set RedisURI in a ThreadLocal that is used in advice added in + // RedisStandaloneConnectionInstrumentation that attaches RedisURI to + // RedisStandaloneConnection + VertxRedisClientSingletons.setRedisUriThreadLocal(redisUri); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void onExit() { + VertxRedisClientSingletons.setRedisUriThreadLocal(null); + } + } + + @SuppressWarnings("unused") + public static class InitWithConnectionAdvice { + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter( + @Advice.Argument(0) RedisConnection connection, + @Advice.FieldValue("redisURI") RedisURI redisUri) { + // for 4.0.x we don't need to use ThreadLocal like in 4.1.0 because in this method we have + // access to both the RedisURI and RedisConnection + if (connection instanceof RedisStandaloneConnection) { + VertxRedisClientSingletons.setRedisUri((RedisStandaloneConnection) connection, redisUri); + } + } + } +} diff --git a/instrumentation/vertx/vertx-redis-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/redis/RedisStandaloneConnectionInstrumentation.java b/instrumentation/vertx/vertx-redis-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/redis/RedisStandaloneConnectionInstrumentation.java new file mode 100644 index 000000000000..157a7b2e3677 --- /dev/null +++ b/instrumentation/vertx/vertx-redis-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/redis/RedisStandaloneConnectionInstrumentation.java @@ -0,0 +1,103 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.vertx.v4_0.redis; + +import static io.opentelemetry.javaagent.bootstrap.Java8BytecodeBridge.currentContext; +import static io.opentelemetry.javaagent.instrumentation.vertx.v4_0.redis.VertxRedisClientSingletons.instrumenter; +import static net.bytebuddy.matcher.ElementMatchers.isConstructor; +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.vertx.core.Future; +import io.vertx.core.net.NetSocket; +import io.vertx.redis.client.Request; +import io.vertx.redis.client.Response; +import io.vertx.redis.client.impl.RedisStandaloneConnection; +import io.vertx.redis.client.impl.RedisURI; +import io.vertx.redis.client.impl.RequestUtil; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class RedisStandaloneConnectionInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher typeMatcher() { + return named("io.vertx.redis.client.impl.RedisStandaloneConnection"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod(named("send"), this.getClass().getName() + "$SendAdvice"); + transformer.applyAdviceToMethod( + isConstructor(), this.getClass().getName() + "$ConstructorAdvice"); + } + + @SuppressWarnings("unused") + public static class SendAdvice { + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter( + @Advice.This RedisStandaloneConnection connection, + @Advice.Argument(0) Request request, + @Advice.FieldValue("netSocket") NetSocket netSocket, + @Advice.Local("otelRequest") VertxRedisClientRequest otelRequest, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + if (request == null) { + return; + } + + String commandName = VertxRedisClientSingletons.getCommandName(request.command()); + RedisURI redisUri = VertxRedisClientSingletons.getRedisUri(connection); + if (commandName == null || redisUri == null) { + return; + } + + otelRequest = + new VertxRedisClientRequest( + commandName, RequestUtil.getArgs(request), redisUri, netSocket); + Context parentContext = currentContext(); + if (!instrumenter().shouldStart(parentContext, otelRequest)) { + return; + } + + context = instrumenter().start(parentContext, otelRequest); + scope = context.makeCurrent(); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void onExit( + @Advice.Thrown Throwable throwable, + @Advice.Return(readOnly = false) Future responseFuture, + @Advice.Local("otelRequest") VertxRedisClientRequest otelRequest, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + if (scope == null) { + return; + } + + scope.close(); + if (throwable != null) { + instrumenter().end(context, otelRequest, null, throwable); + } else { + responseFuture = + VertxRedisClientSingletons.wrapEndSpan(responseFuture, context, otelRequest); + } + } + } + + @SuppressWarnings("unused") + public static class ConstructorAdvice { + @Advice.OnMethodExit(suppress = Throwable.class) + public static void onExit(@Advice.This RedisStandaloneConnection connection) { + // used in 4.1.0, for 4.0.0 it is set in RedisConnectionProviderInstrumentation + VertxRedisClientSingletons.setRedisUri( + connection, VertxRedisClientSingletons.getRedisUriThreadLocal()); + } + } +} diff --git a/instrumentation/vertx/vertx-redis-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/redis/VertxRedisClientAttributesExtractor.java b/instrumentation/vertx/vertx-redis-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/redis/VertxRedisClientAttributesExtractor.java new file mode 100644 index 000000000000..5203ed1e96b8 --- /dev/null +++ b/instrumentation/vertx/vertx-redis-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/redis/VertxRedisClientAttributesExtractor.java @@ -0,0 +1,34 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.vertx.v4_0.redis; + +import static io.opentelemetry.instrumentation.api.internal.AttributesExtractorUtil.internalSet; + +import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; +import io.opentelemetry.semconv.incubating.DbIncubatingAttributes; +import javax.annotation.Nullable; + +enum VertxRedisClientAttributesExtractor + implements AttributesExtractor { + INSTANCE; + + @Override + public void onStart( + AttributesBuilder attributes, Context parentContext, VertxRedisClientRequest request) { + internalSet( + attributes, DbIncubatingAttributes.DB_REDIS_DATABASE_INDEX, request.getDatabaseIndex()); + } + + @Override + public void onEnd( + AttributesBuilder attributes, + Context context, + VertxRedisClientRequest request, + @Nullable Void unused, + @Nullable Throwable error) {} +} diff --git a/instrumentation/vertx/vertx-redis-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/redis/VertxRedisClientAttributesGetter.java b/instrumentation/vertx/vertx-redis-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/redis/VertxRedisClientAttributesGetter.java new file mode 100644 index 000000000000..d662aa053ad6 --- /dev/null +++ b/instrumentation/vertx/vertx-redis-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/redis/VertxRedisClientAttributesGetter.java @@ -0,0 +1,54 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.vertx.v4_0.redis; + +import io.opentelemetry.instrumentation.api.incubator.semconv.db.DbClientAttributesGetter; +import io.opentelemetry.instrumentation.api.incubator.semconv.db.RedisCommandSanitizer; +import io.opentelemetry.javaagent.bootstrap.internal.AgentCommonConfig; +import io.opentelemetry.semconv.incubating.DbIncubatingAttributes; +import javax.annotation.Nullable; + +public enum VertxRedisClientAttributesGetter + implements DbClientAttributesGetter { + INSTANCE; + + private static final RedisCommandSanitizer sanitizer = + RedisCommandSanitizer.create(AgentCommonConfig.get().isStatementSanitizationEnabled()); + + @Override + public String getSystem(VertxRedisClientRequest request) { + return DbIncubatingAttributes.DbSystemValues.REDIS; + } + + @Override + @Nullable + public String getUser(VertxRedisClientRequest request) { + return request.getUser(); + } + + @Override + @Nullable + public String getName(VertxRedisClientRequest request) { + return null; + } + + @Override + @Nullable + public String getConnectionString(VertxRedisClientRequest request) { + return request.getConnectionString(); + } + + @Override + public String getStatement(VertxRedisClientRequest request) { + return sanitizer.sanitize(request.getCommand(), request.getArgs()); + } + + @Nullable + @Override + public String getOperation(VertxRedisClientRequest request) { + return request.getCommand(); + } +} diff --git a/instrumentation/vertx/vertx-redis-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/redis/VertxRedisClientInstrumentationModule.java b/instrumentation/vertx/vertx-redis-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/redis/VertxRedisClientInstrumentationModule.java new file mode 100644 index 000000000000..e3cab6a83ce5 --- /dev/null +++ b/instrumentation/vertx/vertx-redis-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/redis/VertxRedisClientInstrumentationModule.java @@ -0,0 +1,42 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.vertx.v4_0.redis; + +import static java.util.Arrays.asList; +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.instrumentation.internal.ExperimentalInstrumentationModule; +import java.util.List; + +@AutoService(InstrumentationModule.class) +public class VertxRedisClientInstrumentationModule extends InstrumentationModule + implements ExperimentalInstrumentationModule { + + public VertxRedisClientInstrumentationModule() { + super("vertx-redis-client", "vertx-redis-client-4.0", "vertx"); + } + + @Override + public boolean isHelperClass(String className) { + return "io.vertx.redis.client.impl.RequestUtil".equals(className); + } + + @Override + public List injectedClassNames() { + return singletonList("io.vertx.redis.client.impl.RequestUtil"); + } + + @Override + public List typeInstrumentations() { + return asList( + new RedisStandaloneConnectionInstrumentation(), + new RedisConnectionProviderInstrumentation(), + new CommandImplInstrumentation()); + } +} diff --git a/instrumentation/vertx/vertx-redis-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/redis/VertxRedisClientNetAttributesGetter.java b/instrumentation/vertx/vertx-redis-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/redis/VertxRedisClientNetAttributesGetter.java new file mode 100644 index 000000000000..9b0e12581dee --- /dev/null +++ b/instrumentation/vertx/vertx-redis-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/redis/VertxRedisClientNetAttributesGetter.java @@ -0,0 +1,41 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.vertx.v4_0.redis; + +import io.opentelemetry.instrumentation.api.semconv.network.NetworkAttributesGetter; +import io.opentelemetry.instrumentation.api.semconv.network.ServerAttributesGetter; +import javax.annotation.Nullable; + +enum VertxRedisClientNetAttributesGetter + implements + ServerAttributesGetter, + NetworkAttributesGetter { + INSTANCE; + + @Nullable + @Override + public String getServerAddress(VertxRedisClientRequest request) { + return request.getHost(); + } + + @Nullable + @Override + public Integer getServerPort(VertxRedisClientRequest request) { + return request.getPort(); + } + + @Override + @Nullable + public String getNetworkPeerAddress(VertxRedisClientRequest request, @Nullable Void unused) { + return request.getPeerAddress(); + } + + @Override + @Nullable + public Integer getNetworkPeerPort(VertxRedisClientRequest request, @Nullable Void unused) { + return request.getPeerPort(); + } +} diff --git a/instrumentation/vertx/vertx-redis-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/redis/VertxRedisClientRequest.java b/instrumentation/vertx/vertx-redis-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/redis/VertxRedisClientRequest.java new file mode 100644 index 000000000000..f202034b7849 --- /dev/null +++ b/instrumentation/vertx/vertx-redis-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/redis/VertxRedisClientRequest.java @@ -0,0 +1,65 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.vertx.v4_0.redis; + +import io.vertx.core.net.NetSocket; +import io.vertx.redis.client.impl.RedisURI; +import java.util.List; +import java.util.Locale; + +public final class VertxRedisClientRequest { + private final String command; + private final List args; + private final RedisURI redisUri; + private final NetSocket netSocket; + + public VertxRedisClientRequest( + String command, List args, RedisURI redisUri, NetSocket netSocket) { + this.command = command.toUpperCase(Locale.ROOT); + this.args = args; + this.redisUri = redisUri; + this.netSocket = netSocket; + } + + public String getCommand() { + return command; + } + + public List getArgs() { + return args; + } + + public String getUser() { + return redisUri.user(); + } + + public Long getDatabaseIndex() { + Integer select = redisUri.select(); + return select != null ? select.longValue() : null; + } + + public String getConnectionString() { + return null; + } + + public String getHost() { + return redisUri.socketAddress().host(); + } + + public Integer getPort() { + int port = redisUri.socketAddress().port(); + return port != -1 ? port : null; + } + + public String getPeerAddress() { + return netSocket.remoteAddress().hostAddress(); + } + + public Integer getPeerPort() { + int port = netSocket.remoteAddress().port(); + return port != -1 ? port : null; + } +} diff --git a/instrumentation/vertx/vertx-redis-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/redis/VertxRedisClientSingletons.java b/instrumentation/vertx/vertx-redis-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/redis/VertxRedisClientSingletons.java new file mode 100644 index 000000000000..b5164522897c --- /dev/null +++ b/instrumentation/vertx/vertx-redis-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/redis/VertxRedisClientSingletons.java @@ -0,0 +1,109 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.vertx.v4_0.redis; + +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.instrumentation.api.incubator.semconv.db.DbClientAttributesExtractor; +import io.opentelemetry.instrumentation.api.incubator.semconv.db.DbClientSpanNameExtractor; +import io.opentelemetry.instrumentation.api.incubator.semconv.net.PeerServiceAttributesExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import io.opentelemetry.instrumentation.api.instrumenter.InstrumenterBuilder; +import io.opentelemetry.instrumentation.api.instrumenter.SpanKindExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor; +import io.opentelemetry.instrumentation.api.semconv.network.NetworkAttributesExtractor; +import io.opentelemetry.instrumentation.api.semconv.network.ServerAttributesExtractor; +import io.opentelemetry.instrumentation.api.util.VirtualField; +import io.opentelemetry.javaagent.bootstrap.internal.AgentCommonConfig; +import io.vertx.core.Future; +import io.vertx.redis.client.Command; +import io.vertx.redis.client.impl.RedisStandaloneConnection; +import io.vertx.redis.client.impl.RedisURI; +import java.util.concurrent.CompletableFuture; + +public final class VertxRedisClientSingletons { + private static final String INSTRUMENTATION_NAME = "io.opentelemetry.vertx-redis-client-4.0"; + private static final Instrumenter INSTRUMENTER; + + private static final ThreadLocal redisUriThreadLocal = new ThreadLocal<>(); + private static final VirtualField commandNameField = + VirtualField.find(Command.class, String.class); + private static final VirtualField redisUriField = + VirtualField.find(RedisStandaloneConnection.class, RedisURI.class); + + static { + SpanNameExtractor spanNameExtractor = + DbClientSpanNameExtractor.create(VertxRedisClientAttributesGetter.INSTANCE); + + InstrumenterBuilder builder = + Instrumenter.builder( + GlobalOpenTelemetry.get(), INSTRUMENTATION_NAME, spanNameExtractor) + .addAttributesExtractor( + DbClientAttributesExtractor.create(VertxRedisClientAttributesGetter.INSTANCE)) + .addAttributesExtractor(VertxRedisClientAttributesExtractor.INSTANCE) + .addAttributesExtractor( + ServerAttributesExtractor.create(VertxRedisClientNetAttributesGetter.INSTANCE)) + .addAttributesExtractor( + NetworkAttributesExtractor.create(VertxRedisClientNetAttributesGetter.INSTANCE)) + .addAttributesExtractor( + PeerServiceAttributesExtractor.create( + VertxRedisClientNetAttributesGetter.INSTANCE, + AgentCommonConfig.get().getPeerServiceResolver())); + + INSTRUMENTER = builder.buildInstrumenter(SpanKindExtractor.alwaysClient()); + } + + public static Instrumenter instrumenter() { + return INSTRUMENTER; + } + + public static Future wrapEndSpan( + Future future, Context context, VertxRedisClientRequest request) { + Context parentContext = Context.current(); + CompletableFuture result = new CompletableFuture<>(); + future + .toCompletionStage() + .whenComplete( + (value, throwable) -> { + instrumenter().end(context, request, null, null); + try (Scope ignore = parentContext.makeCurrent()) { + if (throwable != null) { + result.completeExceptionally(throwable); + } else { + result.complete(value); + } + } + }); + return Future.fromCompletionStage(result); + } + + public static RedisURI getRedisUriThreadLocal() { + return redisUriThreadLocal.get(); + } + + public static void setRedisUriThreadLocal(RedisURI redisUri) { + redisUriThreadLocal.set(redisUri); + } + + public static void setCommandName(Command command, String commandName) { + commandNameField.set(command, commandName); + } + + public static String getCommandName(Command command) { + return commandNameField.get(command); + } + + public static void setRedisUri(RedisStandaloneConnection connection, RedisURI redisUri) { + redisUriField.set(connection, redisUri); + } + + public static RedisURI getRedisUri(RedisStandaloneConnection connection) { + return redisUriField.get(connection); + } + + private VertxRedisClientSingletons() {} +} diff --git a/instrumentation/vertx/vertx-redis-client-4.0/javaagent/src/main/java/io/vertx/redis/client/impl/RequestUtil.java b/instrumentation/vertx/vertx-redis-client-4.0/javaagent/src/main/java/io/vertx/redis/client/impl/RequestUtil.java new file mode 100644 index 000000000000..ad30deca7f5f --- /dev/null +++ b/instrumentation/vertx/vertx-redis-client-4.0/javaagent/src/main/java/io/vertx/redis/client/impl/RequestUtil.java @@ -0,0 +1,22 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.vertx.redis.client.impl; + +import io.vertx.redis.client.Request; +import java.util.Collections; +import java.util.List; + +public final class RequestUtil { + + public static List getArgs(Request request) { + if (request instanceof RequestImpl) { + return ((RequestImpl) request).getArgs(); + } + return Collections.emptyList(); + } + + private RequestUtil() {} +} diff --git a/instrumentation/vertx/vertx-redis-client-4.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/redis/VertxRedisClientTest.java b/instrumentation/vertx/vertx-redis-client-4.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/redis/VertxRedisClientTest.java new file mode 100644 index 000000000000..69add7333329 --- /dev/null +++ b/instrumentation/vertx/vertx-redis-client-4.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/redis/VertxRedisClientTest.java @@ -0,0 +1,214 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.vertx.v4_0.redis; + +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.sdk.testing.assertj.AttributeAssertion; +import io.opentelemetry.semconv.NetworkAttributes; +import io.opentelemetry.semconv.ServerAttributes; +import io.opentelemetry.semconv.incubating.DbIncubatingAttributes; +import io.vertx.core.Vertx; +import io.vertx.redis.client.Redis; +import io.vertx.redis.client.RedisAPI; +import io.vertx.redis.client.RedisConnection; +import java.net.InetAddress; +import java.util.Arrays; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +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; +import org.testcontainers.containers.GenericContainer; + +class VertxRedisClientTest { + @RegisterExtension + private static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + private static final GenericContainer redisServer = + new GenericContainer<>("redis:6.2.3-alpine").withExposedPorts(6379); + private static String host; + private static String ip; + private static int port; + private static Vertx vertx; + private static Redis client; + private static RedisAPI redis; + + @BeforeAll + static void setup() throws Exception { + redisServer.start(); + + host = redisServer.getHost(); + ip = InetAddress.getByName(host).getHostAddress(); + port = redisServer.getMappedPort(6379); + + vertx = Vertx.vertx(); + client = Redis.createClient(vertx, "redis://" + host + ":" + port + "/1"); + RedisConnection connection = + client.connect().toCompletionStage().toCompletableFuture().get(30, TimeUnit.SECONDS); + redis = RedisAPI.api(connection); + } + + @AfterAll + static void cleanup() { + redis.close(); + client.close(); + redisServer.stop(); + } + + @Test + void setCommand() throws Exception { + redis + .set(Arrays.asList("foo", "bar")) + .toCompletionStage() + .toCompletableFuture() + .get(30, TimeUnit.SECONDS); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("SET") + .hasKind(SpanKind.CLIENT) + .hasAttributesSatisfyingExactly(redisSpanAttributes("SET", "SET foo ?")))); + } + + @Test + void getCommand() throws Exception { + redis + .set(Arrays.asList("foo", "bar")) + .toCompletionStage() + .toCompletableFuture() + .get(30, TimeUnit.SECONDS); + String value = + redis + .get("foo") + .toCompletionStage() + .toCompletableFuture() + .get(30, TimeUnit.SECONDS) + .toString(); + + assertThat(value).isEqualTo("bar"); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("SET") + .hasKind(SpanKind.CLIENT) + .hasAttributesSatisfyingExactly(redisSpanAttributes("SET", "SET foo ?"))), + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("GET") + .hasKind(SpanKind.CLIENT) + .hasAttributesSatisfyingExactly(redisSpanAttributes("GET", "GET foo")))); + } + + @Test + void getCommandWithParent() throws Exception { + redis + .set(Arrays.asList("foo", "bar")) + .toCompletionStage() + .toCompletableFuture() + .get(30, TimeUnit.SECONDS); + + CompletableFuture future = new CompletableFuture<>(); + CompletableFuture result = + future.whenComplete((value, throwable) -> testing.runWithSpan("callback", () -> {})); + + testing.runWithSpan( + "parent", + () -> + redis + .get("foo") + .toCompletionStage() + .toCompletableFuture() + .whenComplete( + (response, throwable) -> { + if (throwable == null) { + future.complete(response.toString()); + } else { + future.completeExceptionally(throwable); + } + })); + + String value = result.get(30, TimeUnit.SECONDS); + assertThat(value).isEqualTo("bar"); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("SET") + .hasKind(SpanKind.CLIENT) + .hasAttributesSatisfyingExactly(redisSpanAttributes("SET", "SET foo ?"))), + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasKind(SpanKind.INTERNAL), + span -> + span.hasName("GET") + .hasKind(SpanKind.CLIENT) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly(redisSpanAttributes("GET", "GET foo")), + span -> + span.hasName("callback") + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(0)))); + } + + @Test + void commandWithNoArguments() throws Exception { + redis + .set(Arrays.asList("foo", "bar")) + .toCompletionStage() + .toCompletableFuture() + .get(30, TimeUnit.SECONDS); + + String value = + redis + .randomkey() + .toCompletionStage() + .toCompletableFuture() + .get(30, TimeUnit.SECONDS) + .toString(); + + assertThat(value).isEqualTo("foo"); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("SET") + .hasKind(SpanKind.CLIENT) + .hasAttributesSatisfyingExactly(redisSpanAttributes("SET", "SET foo ?"))), + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("RANDOMKEY") + .hasKind(SpanKind.CLIENT) + .hasAttributesSatisfyingExactly( + redisSpanAttributes("RANDOMKEY", "RANDOMKEY")))); + } + + private static AttributeAssertion[] redisSpanAttributes(String operation, String statement) { + return new AttributeAssertion[] { + equalTo(DbIncubatingAttributes.DB_SYSTEM, "redis"), + equalTo(DbIncubatingAttributes.DB_STATEMENT, statement), + equalTo(DbIncubatingAttributes.DB_OPERATION, operation), + equalTo(DbIncubatingAttributes.DB_REDIS_DATABASE_INDEX, 1), + equalTo(ServerAttributes.SERVER_ADDRESS, host), + equalTo(ServerAttributes.SERVER_PORT, port), + equalTo(NetworkAttributes.NETWORK_PEER_PORT, port), + equalTo(NetworkAttributes.NETWORK_PEER_ADDRESS, ip) + }; + } +} diff --git a/instrumentation/vertx/vertx-rx-java-3.5/javaagent/build.gradle.kts b/instrumentation/vertx/vertx-rx-java-3.5/javaagent/build.gradle.kts index 3b021e09e1f7..4637b180ed07 100644 --- a/instrumentation/vertx/vertx-rx-java-3.5/javaagent/build.gradle.kts +++ b/instrumentation/vertx/vertx-rx-java-3.5/javaagent/build.gradle.kts @@ -54,8 +54,10 @@ testing { } } +val testLatestDeps = findProperty("testLatestDeps") as Boolean + tasks { - if (findProperty("testLatestDeps") as Boolean) { + if (testLatestDeps) { // disable regular test running and compiling tasks when latest dep test task is run named("test") { enabled = false @@ -63,9 +65,13 @@ tasks { named("compileTestGroovy") { enabled = false } + } - check { - dependsOn(testing.suites) - } + named("latestDepTest") { + enabled = testLatestDeps + } + + check { + dependsOn(testing.suites) } } diff --git a/instrumentation/vertx/vertx-rx-java-3.5/javaagent/src/latestDepTest/groovy/VertxReactivePropagationTest.groovy b/instrumentation/vertx/vertx-rx-java-3.5/javaagent/src/latestDepTest/groovy/VertxReactivePropagationTest.groovy index 584776fea0ac..7e776c9ab177 100644 --- a/instrumentation/vertx/vertx-rx-java-3.5/javaagent/src/latestDepTest/groovy/VertxReactivePropagationTest.groovy +++ b/instrumentation/vertx/vertx-rx-java-3.5/javaagent/src/latestDepTest/groovy/VertxReactivePropagationTest.groovy @@ -9,7 +9,13 @@ import io.opentelemetry.api.trace.SpanKind 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.semconv.incubating.DbIncubatingAttributes +import io.opentelemetry.semconv.ServerAttributes +import io.opentelemetry.semconv.ClientAttributes +import io.opentelemetry.semconv.UserAgentAttributes +import io.opentelemetry.semconv.HttpAttributes +import io.opentelemetry.semconv.NetworkAttributes +import io.opentelemetry.semconv.UrlAttributes import io.opentelemetry.testing.internal.armeria.client.WebClient import io.opentelemetry.testing.internal.armeria.common.HttpRequest import io.opentelemetry.testing.internal.armeria.common.HttpRequestBuilder @@ -24,7 +30,6 @@ 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.testing.junit.http.ServerEndpoint.SUCCESS -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.NetTransportValues.IP_TCP class VertxReactivePropagationTest extends AgentInstrumentationSpecification { @Shared @@ -48,6 +53,7 @@ class VertxReactivePropagationTest extends AgentInstrumentationSpecification { //Verifies that context is correctly propagated and sql query span has correct parent. //Tests io.opentelemetry.javaagent.instrumentation.vertx.reactive.VertxRxInstrumentation + @SuppressWarnings("deprecation") // TODO DbIncubatingAttributes.DB_CONNECTION_STRING deprecation def "should propagate context over vert.x rx-java framework"() { setup: def response = client.get("/listProducts").aggregate().join() @@ -63,21 +69,18 @@ class VertxReactivePropagationTest extends AgentInstrumentationSpecification { kind SERVER hasNoParent() attributes { - "$SemanticAttributes.NET_TRANSPORT" { it == null || it == IP_TCP } - "net.protocol.name" "http" - "net.protocol.version" "1.1" - "$SemanticAttributes.NET_SOCK_PEER_ADDR" "127.0.0.1" - "$SemanticAttributes.NET_SOCK_PEER_PORT" Long - "$SemanticAttributes.NET_SOCK_HOST_ADDR" "127.0.0.1" - "$SemanticAttributes.NET_HOST_NAME" "localhost" - "$SemanticAttributes.NET_HOST_PORT" Long - "$SemanticAttributes.HTTP_TARGET" "/listProducts" - "$SemanticAttributes.HTTP_METHOD" "GET" - "$SemanticAttributes.HTTP_STATUS_CODE" 200 - "$SemanticAttributes.HTTP_SCHEME" "http" - "$SemanticAttributes.USER_AGENT_ORIGINAL" String - "$SemanticAttributes.HTTP_ROUTE" "/listProducts" - "$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" Long + "$NetworkAttributes.NETWORK_PROTOCOL_VERSION" "1.1" + "$NetworkAttributes.NETWORK_PEER_ADDRESS" "127.0.0.1" + "$NetworkAttributes.NETWORK_PEER_PORT" Long + "$ServerAttributes.SERVER_ADDRESS" "localhost" + "$ServerAttributes.SERVER_PORT" Long + "$ClientAttributes.CLIENT_ADDRESS" "127.0.0.1" + "$UrlAttributes.URL_PATH" "/listProducts" + "$HttpAttributes.HTTP_REQUEST_METHOD" "GET" + "$HttpAttributes.HTTP_RESPONSE_STATUS_CODE" 200 + "$UrlAttributes.URL_SCHEME" "http" + "$UserAgentAttributes.USER_AGENT_ORIGINAL" String + "$HttpAttributes.HTTP_ROUTE" "/listProducts" } } span(1) { @@ -95,19 +98,20 @@ class VertxReactivePropagationTest extends AgentInstrumentationSpecification { kind CLIENT childOf span(2) attributes { - "$SemanticAttributes.DB_SYSTEM" "hsqldb" - "$SemanticAttributes.DB_NAME" "test" - "$SemanticAttributes.DB_USER" "SA" - "$SemanticAttributes.DB_CONNECTION_STRING" "hsqldb:mem:" - "$SemanticAttributes.DB_STATEMENT" "SELECT id, name, price, weight FROM products" - "$SemanticAttributes.DB_OPERATION" "SELECT" - "$SemanticAttributes.DB_SQL_TABLE" "products" + "$DbIncubatingAttributes.DB_SYSTEM" "hsqldb" + "$DbIncubatingAttributes.DB_NAME" "test" + "$DbIncubatingAttributes.DB_USER" "SA" + "$DbIncubatingAttributes.DB_CONNECTION_STRING" "hsqldb:mem:" + "$DbIncubatingAttributes.DB_STATEMENT" "SELECT id, name, price, weight FROM products" + "$DbIncubatingAttributes.DB_OPERATION" "SELECT" + "$DbIncubatingAttributes.DB_SQL_TABLE" "products" } } } } } + @SuppressWarnings("deprecation") // TODO DbIncubatingAttributes.DB_CONNECTION_STRING deprecation def "should propagate context correctly over vert.x rx-java framework with high concurrency"() { setup: int count = 100 @@ -157,21 +161,19 @@ class VertxReactivePropagationTest extends AgentInstrumentationSpecification { kind SERVER childOf(span(0)) attributes { - "$SemanticAttributes.NET_TRANSPORT" { it == null || it == IP_TCP } - "net.protocol.name" "http" - "net.protocol.version" "1.1" - "$SemanticAttributes.NET_SOCK_PEER_ADDR" "127.0.0.1" - "$SemanticAttributes.NET_SOCK_PEER_PORT" Long - "$SemanticAttributes.NET_SOCK_HOST_ADDR" "127.0.0.1" - "$SemanticAttributes.NET_HOST_NAME" "localhost" - "$SemanticAttributes.NET_HOST_PORT" Long - "$SemanticAttributes.HTTP_TARGET" "$baseUrl?$TEST_REQUEST_ID_PARAMETER=$requestId" - "$SemanticAttributes.HTTP_METHOD" "GET" - "$SemanticAttributes.HTTP_STATUS_CODE" 200 - "$SemanticAttributes.HTTP_SCHEME" "http" - "$SemanticAttributes.USER_AGENT_ORIGINAL" String - "$SemanticAttributes.HTTP_ROUTE" "/listProducts" - "$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" Long + "$NetworkAttributes.NETWORK_PROTOCOL_VERSION" "1.1" + "$NetworkAttributes.NETWORK_PEER_ADDRESS" "127.0.0.1" + "$NetworkAttributes.NETWORK_PEER_PORT" Long + "$ServerAttributes.SERVER_ADDRESS" "localhost" + "$ServerAttributes.SERVER_PORT" Long + "$ClientAttributes.CLIENT_ADDRESS" "127.0.0.1" + "$UrlAttributes.URL_PATH" baseUrl + "$UrlAttributes.URL_QUERY" "$TEST_REQUEST_ID_PARAMETER=$requestId" + "$HttpAttributes.HTTP_REQUEST_METHOD" "GET" + "$HttpAttributes.HTTP_RESPONSE_STATUS_CODE" 200 + "$UrlAttributes.URL_SCHEME" "http" + "$UserAgentAttributes.USER_AGENT_ORIGINAL" String + "$HttpAttributes.HTTP_ROUTE" "/listProducts" "${TEST_REQUEST_ID_ATTRIBUTE}" requestId } } @@ -196,13 +198,13 @@ class VertxReactivePropagationTest extends AgentInstrumentationSpecification { kind CLIENT childOf(span(3)) attributes { - "$SemanticAttributes.DB_SYSTEM" "hsqldb" - "$SemanticAttributes.DB_NAME" "test" - "$SemanticAttributes.DB_USER" "SA" - "$SemanticAttributes.DB_CONNECTION_STRING" "hsqldb:mem:" - "$SemanticAttributes.DB_STATEMENT" "SELECT id AS request$requestId, name, price, weight FROM products" - "$SemanticAttributes.DB_OPERATION" "SELECT" - "$SemanticAttributes.DB_SQL_TABLE" "products" + "$DbIncubatingAttributes.DB_SYSTEM" "hsqldb" + "$DbIncubatingAttributes.DB_NAME" "test" + "$DbIncubatingAttributes.DB_USER" "SA" + "$DbIncubatingAttributes.DB_CONNECTION_STRING" "hsqldb:mem:" + "$DbIncubatingAttributes.DB_STATEMENT" "SELECT id AS request$requestId, name, price, weight FROM products" + "$DbIncubatingAttributes.DB_OPERATION" "SELECT" + "$DbIncubatingAttributes.DB_SQL_TABLE" "products" } } } diff --git a/instrumentation/vertx/vertx-rx-java-3.5/javaagent/src/latestDepTest/groovy/client/VertxRxCircuitBreakerWebClientTest.groovy b/instrumentation/vertx/vertx-rx-java-3.5/javaagent/src/latestDepTest/groovy/client/VertxRxCircuitBreakerWebClientTest.groovy index 5eec499e3209..f646d0b14c08 100644 --- a/instrumentation/vertx/vertx-rx-java-3.5/javaagent/src/latestDepTest/groovy/client/VertxRxCircuitBreakerWebClientTest.groovy +++ b/instrumentation/vertx/vertx-rx-java-3.5/javaagent/src/latestDepTest/groovy/client/VertxRxCircuitBreakerWebClientTest.groovy @@ -104,11 +104,6 @@ class VertxRxCircuitBreakerWebClientTest extends HttpClientTest> return super.httpAttributes(uri) } - @Override - String userAgent() { - return "Vert.x-WebClient" - } - @Override boolean testRedirects() { false diff --git a/instrumentation/vertx/vertx-rx-java-3.5/javaagent/src/latestDepTest/groovy/client/VertxRxWebClientTest.groovy b/instrumentation/vertx/vertx-rx-java-3.5/javaagent/src/latestDepTest/groovy/client/VertxRxWebClientTest.groovy index 991ab1244464..4abac2d6218a 100644 --- a/instrumentation/vertx/vertx-rx-java-3.5/javaagent/src/latestDepTest/groovy/client/VertxRxWebClientTest.groovy +++ b/instrumentation/vertx/vertx-rx-java-3.5/javaagent/src/latestDepTest/groovy/client/VertxRxWebClientTest.groovy @@ -90,11 +90,6 @@ class VertxRxWebClientTest extends HttpClientTest> implement return super.httpAttributes(uri) } - @Override - String userAgent() { - return "Vert.x-WebClient" - } - @Override boolean testRedirects() { false diff --git a/instrumentation/vertx/vertx-rx-java-3.5/javaagent/src/latestDepTest/groovy/server/VertxRxHttpServerTest.groovy b/instrumentation/vertx/vertx-rx-java-3.5/javaagent/src/latestDepTest/groovy/server/VertxRxHttpServerTest.groovy index d732601f5b7c..a8f6ad7e7fa7 100644 --- a/instrumentation/vertx/vertx-rx-java-3.5/javaagent/src/latestDepTest/groovy/server/VertxRxHttpServerTest.groovy +++ b/instrumentation/vertx/vertx-rx-java-3.5/javaagent/src/latestDepTest/groovy/server/VertxRxHttpServerTest.groovy @@ -5,9 +5,10 @@ package server - +import io.opentelemetry.instrumentation.api.internal.HttpConstants import io.opentelemetry.instrumentation.test.AgentTestTrait import io.opentelemetry.instrumentation.test.base.HttpServerTest +import io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint import io.vertx.core.DeploymentOptions import io.vertx.core.Promise import io.vertx.core.Vertx @@ -68,6 +69,14 @@ class VertxRxHttpServerTest extends HttpServerTest implements AgentTestTr return false } + @Override + String expectedHttpRoute(ServerEndpoint endpoint, String method) { + if (method == HttpConstants._OTHER) { + return getContextPath() + endpoint.path + } + return super.expectedHttpRoute(endpoint, method) + } + protected Class verticle() { return VertxReactiveWebServer } diff --git a/instrumentation/vertx/vertx-rx-java-3.5/javaagent/src/version35Test/groovy/VertxReactivePropagationTest.groovy b/instrumentation/vertx/vertx-rx-java-3.5/javaagent/src/version35Test/groovy/VertxReactivePropagationTest.groovy index 584776fea0ac..9490912c8f93 100644 --- a/instrumentation/vertx/vertx-rx-java-3.5/javaagent/src/version35Test/groovy/VertxReactivePropagationTest.groovy +++ b/instrumentation/vertx/vertx-rx-java-3.5/javaagent/src/version35Test/groovy/VertxReactivePropagationTest.groovy @@ -9,7 +9,13 @@ import io.opentelemetry.api.trace.SpanKind 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.semconv.incubating.DbIncubatingAttributes +import io.opentelemetry.semconv.ServerAttributes +import io.opentelemetry.semconv.ClientAttributes +import io.opentelemetry.semconv.UserAgentAttributes +import io.opentelemetry.semconv.HttpAttributes +import io.opentelemetry.semconv.NetworkAttributes +import io.opentelemetry.semconv.UrlAttributes import io.opentelemetry.testing.internal.armeria.client.WebClient import io.opentelemetry.testing.internal.armeria.common.HttpRequest import io.opentelemetry.testing.internal.armeria.common.HttpRequestBuilder @@ -24,7 +30,6 @@ 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.testing.junit.http.ServerEndpoint.SUCCESS -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.NetTransportValues.IP_TCP class VertxReactivePropagationTest extends AgentInstrumentationSpecification { @Shared @@ -48,6 +53,7 @@ class VertxReactivePropagationTest extends AgentInstrumentationSpecification { //Verifies that context is correctly propagated and sql query span has correct parent. //Tests io.opentelemetry.javaagent.instrumentation.vertx.reactive.VertxRxInstrumentation + @SuppressWarnings("deprecation") // TODO DbIncubatingAttributes.DB_CONNECTION_STRING deprecation def "should propagate context over vert.x rx-java framework"() { setup: def response = client.get("/listProducts").aggregate().join() @@ -63,21 +69,18 @@ class VertxReactivePropagationTest extends AgentInstrumentationSpecification { kind SERVER hasNoParent() attributes { - "$SemanticAttributes.NET_TRANSPORT" { it == null || it == IP_TCP } - "net.protocol.name" "http" - "net.protocol.version" "1.1" - "$SemanticAttributes.NET_SOCK_PEER_ADDR" "127.0.0.1" - "$SemanticAttributes.NET_SOCK_PEER_PORT" Long - "$SemanticAttributes.NET_SOCK_HOST_ADDR" "127.0.0.1" - "$SemanticAttributes.NET_HOST_NAME" "localhost" - "$SemanticAttributes.NET_HOST_PORT" Long - "$SemanticAttributes.HTTP_TARGET" "/listProducts" - "$SemanticAttributes.HTTP_METHOD" "GET" - "$SemanticAttributes.HTTP_STATUS_CODE" 200 - "$SemanticAttributes.HTTP_SCHEME" "http" - "$SemanticAttributes.USER_AGENT_ORIGINAL" String - "$SemanticAttributes.HTTP_ROUTE" "/listProducts" - "$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" Long + "$NetworkAttributes.NETWORK_PROTOCOL_VERSION" "1.1" + "$NetworkAttributes.NETWORK_PEER_ADDRESS" "127.0.0.1" + "$NetworkAttributes.NETWORK_PEER_PORT" Long + "$ServerAttributes.SERVER_ADDRESS" "localhost" + "$ServerAttributes.SERVER_PORT" Long + "$ClientAttributes.CLIENT_ADDRESS" "127.0.0.1" + "$UrlAttributes.URL_PATH" "/listProducts" + "$HttpAttributes.HTTP_REQUEST_METHOD" "GET" + "$HttpAttributes.HTTP_RESPONSE_STATUS_CODE" 200 + "$UrlAttributes.URL_SCHEME" "http" + "$UserAgentAttributes.USER_AGENT_ORIGINAL" String + "$HttpAttributes.HTTP_ROUTE" "/listProducts" } } span(1) { @@ -95,13 +98,13 @@ class VertxReactivePropagationTest extends AgentInstrumentationSpecification { kind CLIENT childOf span(2) attributes { - "$SemanticAttributes.DB_SYSTEM" "hsqldb" - "$SemanticAttributes.DB_NAME" "test" - "$SemanticAttributes.DB_USER" "SA" - "$SemanticAttributes.DB_CONNECTION_STRING" "hsqldb:mem:" - "$SemanticAttributes.DB_STATEMENT" "SELECT id, name, price, weight FROM products" - "$SemanticAttributes.DB_OPERATION" "SELECT" - "$SemanticAttributes.DB_SQL_TABLE" "products" + "$DbIncubatingAttributes.DB_SYSTEM" "hsqldb" + "$DbIncubatingAttributes.DB_NAME" "test" + "$DbIncubatingAttributes.DB_USER" "SA" + "$DbIncubatingAttributes.DB_CONNECTION_STRING" "hsqldb:mem:" + "$DbIncubatingAttributes.DB_STATEMENT" "SELECT id, name, price, weight FROM products" + "$DbIncubatingAttributes.DB_OPERATION" "SELECT" + "$DbIncubatingAttributes.DB_SQL_TABLE" "products" } } } @@ -157,21 +160,19 @@ class VertxReactivePropagationTest extends AgentInstrumentationSpecification { kind SERVER childOf(span(0)) attributes { - "$SemanticAttributes.NET_TRANSPORT" { it == null || it == IP_TCP } - "net.protocol.name" "http" - "net.protocol.version" "1.1" - "$SemanticAttributes.NET_SOCK_PEER_ADDR" "127.0.0.1" - "$SemanticAttributes.NET_SOCK_PEER_PORT" Long - "$SemanticAttributes.NET_SOCK_HOST_ADDR" "127.0.0.1" - "$SemanticAttributes.NET_HOST_NAME" "localhost" - "$SemanticAttributes.NET_HOST_PORT" Long - "$SemanticAttributes.HTTP_TARGET" "$baseUrl?$TEST_REQUEST_ID_PARAMETER=$requestId" - "$SemanticAttributes.HTTP_METHOD" "GET" - "$SemanticAttributes.HTTP_STATUS_CODE" 200 - "$SemanticAttributes.HTTP_SCHEME" "http" - "$SemanticAttributes.USER_AGENT_ORIGINAL" String - "$SemanticAttributes.HTTP_ROUTE" "/listProducts" - "$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" Long + "$NetworkAttributes.NETWORK_PROTOCOL_VERSION" "1.1" + "$NetworkAttributes.NETWORK_PEER_ADDRESS" "127.0.0.1" + "$NetworkAttributes.NETWORK_PEER_PORT" Long + "$ServerAttributes.SERVER_ADDRESS" "localhost" + "$ServerAttributes.SERVER_PORT" Long + "$ClientAttributes.CLIENT_ADDRESS" "127.0.0.1" + "$UrlAttributes.URL_PATH" baseUrl + "$UrlAttributes.URL_QUERY" "$TEST_REQUEST_ID_PARAMETER=$requestId" + "$HttpAttributes.HTTP_REQUEST_METHOD" "GET" + "$HttpAttributes.HTTP_RESPONSE_STATUS_CODE" 200 + "$UrlAttributes.URL_SCHEME" "http" + "$UserAgentAttributes.USER_AGENT_ORIGINAL" String + "$HttpAttributes.HTTP_ROUTE" "/listProducts" "${TEST_REQUEST_ID_ATTRIBUTE}" requestId } } @@ -196,13 +197,13 @@ class VertxReactivePropagationTest extends AgentInstrumentationSpecification { kind CLIENT childOf(span(3)) attributes { - "$SemanticAttributes.DB_SYSTEM" "hsqldb" - "$SemanticAttributes.DB_NAME" "test" - "$SemanticAttributes.DB_USER" "SA" - "$SemanticAttributes.DB_CONNECTION_STRING" "hsqldb:mem:" - "$SemanticAttributes.DB_STATEMENT" "SELECT id AS request$requestId, name, price, weight FROM products" - "$SemanticAttributes.DB_OPERATION" "SELECT" - "$SemanticAttributes.DB_SQL_TABLE" "products" + "$DbIncubatingAttributes.DB_SYSTEM" "hsqldb" + "$DbIncubatingAttributes.DB_NAME" "test" + "$DbIncubatingAttributes.DB_USER" "SA" + "$DbIncubatingAttributes.DB_CONNECTION_STRING" "hsqldb:mem:" + "$DbIncubatingAttributes.DB_STATEMENT" "SELECT id AS request$requestId, name, price, weight FROM products" + "$DbIncubatingAttributes.DB_OPERATION" "SELECT" + "$DbIncubatingAttributes.DB_SQL_TABLE" "products" } } } diff --git a/instrumentation/vertx/vertx-rx-java-3.5/javaagent/src/version35Test/groovy/client/VertxRxCircuitBreakerWebClientTest.groovy b/instrumentation/vertx/vertx-rx-java-3.5/javaagent/src/version35Test/groovy/client/VertxRxCircuitBreakerWebClientTest.groovy index 782b9d81bef4..e7168361c0e4 100644 --- a/instrumentation/vertx/vertx-rx-java-3.5/javaagent/src/version35Test/groovy/client/VertxRxCircuitBreakerWebClientTest.groovy +++ b/instrumentation/vertx/vertx-rx-java-3.5/javaagent/src/version35Test/groovy/client/VertxRxCircuitBreakerWebClientTest.groovy @@ -10,7 +10,8 @@ import io.opentelemetry.instrumentation.test.AgentTestTrait import io.opentelemetry.instrumentation.test.base.HttpClientTest import io.opentelemetry.instrumentation.testing.junit.http.HttpClientResult import io.opentelemetry.instrumentation.testing.junit.http.SingleConnection -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import io.opentelemetry.semconv.ServerAttributes +import io.opentelemetry.semconv.NetworkAttributes import io.vertx.circuitbreaker.CircuitBreakerOptions import io.vertx.core.AsyncResult import io.vertx.core.VertxOptions @@ -25,8 +26,6 @@ import spock.lang.Shared import java.util.concurrent.CompletableFuture import java.util.function.Consumer -import static io.opentelemetry.api.common.AttributeKey.stringKey - class VertxRxCircuitBreakerWebClientTest extends HttpClientTest> implements AgentTestTrait { @Shared @@ -86,11 +85,6 @@ class VertxRxCircuitBreakerWebClientTest extends HttpClientTest> } } - @Override - String userAgent() { - return "Vert.x-WebClient" - } - @Override boolean testRedirects() { false @@ -106,13 +100,17 @@ class VertxRxCircuitBreakerWebClientTest extends HttpClientTest> false } + @Override + boolean testNonStandardHttpMethod() { + false + } + @Override Set> httpAttributes(URI uri) { def attributes = super.httpAttributes(uri) - attributes.remove(stringKey("net.protocol.name")) - attributes.remove(stringKey("net.protocol.version")) - attributes.remove(SemanticAttributes.NET_PEER_NAME) - attributes.remove(SemanticAttributes.NET_PEER_PORT) + attributes.remove(NetworkAttributes.NETWORK_PROTOCOL_VERSION) + attributes.remove(ServerAttributes.SERVER_ADDRESS) + attributes.remove(ServerAttributes.SERVER_PORT) return attributes } diff --git a/instrumentation/vertx/vertx-rx-java-3.5/javaagent/src/version35Test/groovy/client/VertxRxWebClientTest.groovy b/instrumentation/vertx/vertx-rx-java-3.5/javaagent/src/version35Test/groovy/client/VertxRxWebClientTest.groovy index d65c9850cb07..695687584e85 100644 --- a/instrumentation/vertx/vertx-rx-java-3.5/javaagent/src/version35Test/groovy/client/VertxRxWebClientTest.groovy +++ b/instrumentation/vertx/vertx-rx-java-3.5/javaagent/src/version35Test/groovy/client/VertxRxWebClientTest.groovy @@ -10,7 +10,8 @@ import io.opentelemetry.instrumentation.test.AgentTestTrait import io.opentelemetry.instrumentation.test.base.HttpClientTest import io.opentelemetry.instrumentation.testing.junit.http.HttpClientResult import io.opentelemetry.instrumentation.testing.junit.http.SingleConnection -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import io.opentelemetry.semconv.ServerAttributes +import io.opentelemetry.semconv.NetworkAttributes import io.vertx.core.VertxOptions import io.vertx.core.http.HttpMethod import io.vertx.ext.web.client.WebClientOptions @@ -21,8 +22,6 @@ import io.vertx.reactivex.ext.web.client.HttpResponse import io.vertx.reactivex.ext.web.client.WebClient import spock.lang.Shared -import static io.opentelemetry.api.common.AttributeKey.stringKey - class VertxRxWebClientTest extends HttpClientTest> implements AgentTestTrait { @Shared @@ -72,11 +71,6 @@ class VertxRxWebClientTest extends HttpClientTest> implement return exception } - @Override - String userAgent() { - return "Vert.x-WebClient" - } - @Override boolean testRedirects() { false @@ -92,13 +86,17 @@ class VertxRxWebClientTest extends HttpClientTest> implement false } + @Override + boolean testNonStandardHttpMethod() { + false + } + @Override Set> httpAttributes(URI uri) { def attributes = super.httpAttributes(uri) - attributes.remove(stringKey("net.protocol.name")) - attributes.remove(stringKey("net.protocol.version")) - attributes.remove(SemanticAttributes.NET_PEER_NAME) - attributes.remove(SemanticAttributes.NET_PEER_PORT) + attributes.remove(NetworkAttributes.NETWORK_PROTOCOL_VERSION) + attributes.remove(ServerAttributes.SERVER_ADDRESS) + attributes.remove(ServerAttributes.SERVER_PORT) return attributes } diff --git a/instrumentation/vertx/vertx-rx-java-3.5/javaagent/src/version35Test/groovy/server/VertxRxHttpServerTest.groovy b/instrumentation/vertx/vertx-rx-java-3.5/javaagent/src/version35Test/groovy/server/VertxRxHttpServerTest.groovy index d74f7ab9e48b..c6f8077c21e7 100644 --- a/instrumentation/vertx/vertx-rx-java-3.5/javaagent/src/version35Test/groovy/server/VertxRxHttpServerTest.groovy +++ b/instrumentation/vertx/vertx-rx-java-3.5/javaagent/src/version35Test/groovy/server/VertxRxHttpServerTest.groovy @@ -5,9 +5,10 @@ package server - +import io.opentelemetry.instrumentation.api.internal.HttpConstants import io.opentelemetry.instrumentation.test.AgentTestTrait import io.opentelemetry.instrumentation.test.base.HttpServerTest +import io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint import io.vertx.core.DeploymentOptions import io.vertx.core.Future import io.vertx.core.Vertx @@ -68,6 +69,14 @@ class VertxRxHttpServerTest extends HttpServerTest implements AgentTestTr return false } + @Override + String expectedHttpRoute(ServerEndpoint endpoint, String method) { + if (method == HttpConstants._OTHER) { + return getContextPath() + endpoint.path + } + return super.expectedHttpRoute(endpoint, method) + } + protected Class verticle() { return VertxReactiveWebServer } diff --git a/instrumentation/vertx/vertx-sql-client-4.0/javaagent/build.gradle.kts b/instrumentation/vertx/vertx-sql-client-4.0/javaagent/build.gradle.kts index e83288d1d379..d28083b74749 100644 --- a/instrumentation/vertx/vertx-sql-client-4.0/javaagent/build.gradle.kts +++ b/instrumentation/vertx/vertx-sql-client-4.0/javaagent/build.gradle.kts @@ -12,14 +12,25 @@ muzzle { } dependencies { - library("io.vertx:vertx-sql-client:4.1.0") - compileOnly("io.vertx:vertx-codegen:4.1.0") + library("io.vertx:vertx-sql-client:4.0.0") + compileOnly("io.vertx:vertx-codegen:4.0.0") - testLibrary("io.vertx:vertx-pg-client:4.1.0") - testLibrary("io.vertx:vertx-codegen:4.1.0") - testLibrary("io.vertx:vertx-opentelemetry:4.1.0") + testInstrumentation(project(":instrumentation:netty:netty-4.1:javaagent")) + + testLibrary("io.vertx:vertx-pg-client:4.0.0") + testLibrary("io.vertx:vertx-codegen:4.0.0") } -tasks.withType().configureEach { - usesService(gradle.sharedServices.registrations["testcontainersBuildService"].service) +tasks { + withType().configureEach { + usesService(gradle.sharedServices.registrations["testcontainersBuildService"].service) + } +} + +val latestDepTest = findProperty("testLatestDeps") as Boolean +if (!latestDepTest) { + // https://bugs.openjdk.org/browse/JDK-8320431 + otelJava { + maxJavaVersionForTests.set(JavaVersion.VERSION_21) + } } diff --git a/instrumentation/vertx/vertx-sql-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/sql/HandlerWrapper.java b/instrumentation/vertx/vertx-sql-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/sql/HandlerWrapper.java new file mode 100644 index 000000000000..53e4f302c1b9 --- /dev/null +++ b/instrumentation/vertx/vertx-sql-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/sql/HandlerWrapper.java @@ -0,0 +1,35 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.vertx.v4_0.sql; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.vertx.core.Handler; + +public class HandlerWrapper implements Handler { + private final Handler delegate; + private final Context context; + + private HandlerWrapper(Handler delegate, Context context) { + this.delegate = delegate; + this.context = context; + } + + public static Handler wrap(Handler handler) { + Context current = Context.current(); + if (handler != null && !(handler instanceof HandlerWrapper) && current != Context.root()) { + handler = new HandlerWrapper<>(handler, current); + } + return handler; + } + + @Override + public void handle(T t) { + try (Scope ignore = context.makeCurrent()) { + delegate.handle(t); + } + } +} diff --git a/instrumentation/vertx/vertx-sql-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/sql/PoolInstrumentation.java b/instrumentation/vertx/vertx-sql-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/sql/PoolInstrumentation.java index bccb0d94fd8a..5513b743215d 100644 --- a/instrumentation/vertx/vertx-sql-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/sql/PoolInstrumentation.java +++ b/instrumentation/vertx/vertx-sql-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/sql/PoolInstrumentation.java @@ -5,36 +5,53 @@ package io.opentelemetry.javaagent.instrumentation.vertx.v4_0.sql; +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.hasClassesNamed; +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.implementsInterface; import static io.opentelemetry.javaagent.instrumentation.vertx.v4_0.sql.VertxSqlClientSingletons.setSqlConnectOptions; +import static net.bytebuddy.matcher.ElementMatchers.isStatic; 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 static net.bytebuddy.matcher.ElementMatchers.takesNoArguments; +import io.opentelemetry.instrumentation.api.util.VirtualField; import io.opentelemetry.javaagent.bootstrap.CallDepth; import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.vertx.core.Future; import io.vertx.sqlclient.Pool; import io.vertx.sqlclient.SqlConnectOptions; +import io.vertx.sqlclient.SqlConnection; import net.bytebuddy.asm.Advice; import net.bytebuddy.description.type.TypeDescription; import net.bytebuddy.matcher.ElementMatcher; public class PoolInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher classLoaderOptimization() { + return hasClassesNamed("io.vertx.sqlclient.Pool"); + } + @Override public ElementMatcher typeMatcher() { - return named("io.vertx.sqlclient.Pool"); + return implementsInterface(named("io.vertx.sqlclient.Pool")); } @Override public void transform(TypeTransformer transformer) { transformer.applyAdviceToMethod( named("pool") + .and(isStatic()) .and(takesArguments(3)) .and(takesArgument(1, named("io.vertx.sqlclient.SqlConnectOptions"))) .and(returns(named("io.vertx.sqlclient.Pool"))), PoolInstrumentation.class.getName() + "$PoolAdvice"); + + transformer.applyAdviceToMethod( + named("getConnection").and(takesNoArguments()).and(returns(named("io.vertx.core.Future"))), + PoolInstrumentation.class.getName() + "$GetConnectionAdvice"); } @SuppressWarnings("unused") @@ -53,12 +70,34 @@ public static void onEnter( } @Advice.OnMethodExit(suppress = Throwable.class) - public static void onExit(@Advice.Local("otelCallDepth") CallDepth callDepth) { + public static void onExit( + @Advice.Return Pool pool, + @Advice.Argument(1) SqlConnectOptions sqlConnectOptions, + @Advice.Local("otelCallDepth") CallDepth callDepth) { if (callDepth.decrementAndGet() > 0) { return; } + VirtualField virtualField = + VirtualField.find(Pool.class, SqlConnectOptions.class); + virtualField.set(pool, sqlConnectOptions); + setSqlConnectOptions(null); } } + + @SuppressWarnings("unused") + public static class GetConnectionAdvice { + @Advice.OnMethodExit(suppress = Throwable.class) + public static void onExit( + @Advice.This Pool pool, @Advice.Return(readOnly = false) Future future) { + // copy connect options stored on pool to new connection + VirtualField virtualField = + VirtualField.find(Pool.class, SqlConnectOptions.class); + SqlConnectOptions sqlConnectOptions = virtualField.get(pool); + + future = VertxSqlClientSingletons.attachConnectOptions(future, sqlConnectOptions); + future = VertxSqlClientSingletons.wrapContext(future); + } + } } diff --git a/instrumentation/vertx/vertx-sql-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/sql/QueryExecutorInstrumentation.java b/instrumentation/vertx/vertx-sql-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/sql/QueryExecutorInstrumentation.java index 34394e6b66b7..0264f925cac3 100644 --- a/instrumentation/vertx/vertx-sql-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/sql/QueryExecutorInstrumentation.java +++ b/instrumentation/vertx/vertx-sql-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/sql/QueryExecutorInstrumentation.java @@ -6,9 +6,6 @@ package io.opentelemetry.javaagent.instrumentation.vertx.v4_0.sql; import static io.opentelemetry.javaagent.bootstrap.Java8BytecodeBridge.currentContext; -import static io.opentelemetry.javaagent.instrumentation.vertx.v4_0.sql.VertxSqlClientSingletons.OTEL_CONTEXT_KEY; -import static io.opentelemetry.javaagent.instrumentation.vertx.v4_0.sql.VertxSqlClientSingletons.OTEL_PARENT_CONTEXT_KEY; -import static io.opentelemetry.javaagent.instrumentation.vertx.v4_0.sql.VertxSqlClientSingletons.OTEL_REQUEST_KEY; import static io.opentelemetry.javaagent.instrumentation.vertx.v4_0.sql.VertxSqlClientSingletons.getSqlConnectOptions; import static io.opentelemetry.javaagent.instrumentation.vertx.v4_0.sql.VertxSqlClientSingletons.instrumenter; import static net.bytebuddy.matcher.ElementMatchers.isConstructor; @@ -98,9 +95,7 @@ public static void onEnter( context = instrumenter().start(parentContext, otelRequest); scope = context.makeCurrent(); - promiseInternal.context().localContextData().put(OTEL_REQUEST_KEY, otelRequest); - promiseInternal.context().localContextData().put(OTEL_CONTEXT_KEY, context); - promiseInternal.context().localContextData().put(OTEL_PARENT_CONTEXT_KEY, parentContext); + VertxSqlClientSingletons.attachRequest(promiseInternal, otelRequest, context, parentContext); } @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) diff --git a/instrumentation/vertx/vertx-sql-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/sql/QueryResultBuilderInstrumentation.java b/instrumentation/vertx/vertx-sql-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/sql/QueryResultBuilderInstrumentation.java index bd9544417f13..a364cf873d2f 100644 --- a/instrumentation/vertx/vertx-sql-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/sql/QueryResultBuilderInstrumentation.java +++ b/instrumentation/vertx/vertx-sql-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/sql/QueryResultBuilderInstrumentation.java @@ -13,8 +13,6 @@ import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; import io.vertx.core.Promise; -import io.vertx.core.impl.ContextInternal; -import io.vertx.core.impl.future.PromiseInternal; import net.bytebuddy.asm.Advice; import net.bytebuddy.description.type.TypeDescription; import net.bytebuddy.matcher.ElementMatcher; @@ -40,12 +38,7 @@ public void transform(TypeTransformer transformer) { public static class CompleteAdvice { @Advice.OnMethodEnter(suppress = Throwable.class) public static Scope onEnter(@Advice.FieldValue("handler") Promise promise) { - if (!(promise instanceof PromiseInternal)) { - return null; - } - PromiseInternal promiseInternal = (PromiseInternal) promise; - ContextInternal contextInternal = promiseInternal.context(); - return endQuerySpan(contextInternal.localContextData(), null); + return endQuerySpan(promise, null); } @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) @@ -61,12 +54,7 @@ public static class FailAdvice { @Advice.OnMethodEnter(suppress = Throwable.class) public static Scope onEnter( @Advice.Argument(0) Throwable throwable, @Advice.FieldValue("handler") Promise promise) { - if (!(promise instanceof PromiseInternal)) { - return null; - } - PromiseInternal promiseInternal = (PromiseInternal) promise; - ContextInternal contextInternal = promiseInternal.context(); - return endQuerySpan(contextInternal.localContextData(), throwable); + return endQuerySpan(promise, throwable); } @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) diff --git a/instrumentation/vertx/vertx-sql-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/sql/SqlClientBaseInstrumentation.java b/instrumentation/vertx/vertx-sql-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/sql/SqlClientBaseInstrumentation.java index 8c8b18ad03e8..07cfb16bb56a 100644 --- a/instrumentation/vertx/vertx-sql-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/sql/SqlClientBaseInstrumentation.java +++ b/instrumentation/vertx/vertx-sql-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/sql/SqlClientBaseInstrumentation.java @@ -42,6 +42,7 @@ public static class ConstructorAdvice { @Advice.OnMethodExit(suppress = Throwable.class) public static void onExit(@Advice.This SqlClientBase sqlClientBase) { // copy connection options from ThreadLocal to VirtualField + // this virtual field is also set in VertxSqlClientSingletons.attachConnectOptions VirtualField, SqlConnectOptions> virtualField = VirtualField.find(SqlClientBase.class, SqlConnectOptions.class); virtualField.set(sqlClientBase, getSqlConnectOptions()); diff --git a/instrumentation/vertx/vertx-sql-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/sql/TransactionImplInstrumentation.java b/instrumentation/vertx/vertx-sql-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/sql/TransactionImplInstrumentation.java new file mode 100644 index 000000000000..464479dd63d6 --- /dev/null +++ b/instrumentation/vertx/vertx-sql-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/sql/TransactionImplInstrumentation.java @@ -0,0 +1,39 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.vertx.v4_0.sql; + +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.vertx.core.Handler; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class TransactionImplInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("io.vertx.sqlclient.impl.TransactionImpl"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + named("wrap").and(returns(named("io.vertx.core.Handler"))), + TransactionImplInstrumentation.class.getName() + "$WrapHandlerAdvice"); + } + + @SuppressWarnings("unused") + public static class WrapHandlerAdvice { + @Advice.OnMethodExit(suppress = Throwable.class) + public static void wrapHandler(@Advice.Return(readOnly = false) Handler handler) { + handler = HandlerWrapper.wrap(handler); + } + } +} diff --git a/instrumentation/vertx/vertx-sql-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/sql/VertxSqlClientAttributesGetter.java b/instrumentation/vertx/vertx-sql-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/sql/VertxSqlClientAttributesGetter.java index 121087fe185e..4fa4a6ea5359 100644 --- a/instrumentation/vertx/vertx-sql-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/sql/VertxSqlClientAttributesGetter.java +++ b/instrumentation/vertx/vertx-sql-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/sql/VertxSqlClientAttributesGetter.java @@ -5,7 +5,7 @@ package io.opentelemetry.javaagent.instrumentation.vertx.v4_0.sql; -import io.opentelemetry.instrumentation.api.instrumenter.db.SqlClientAttributesGetter; +import io.opentelemetry.instrumentation.api.incubator.semconv.db.SqlClientAttributesGetter; import javax.annotation.Nullable; public enum VertxSqlClientAttributesGetter diff --git a/instrumentation/vertx/vertx-sql-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/sql/VertxSqlClientInstrumentationModule.java b/instrumentation/vertx/vertx-sql-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/sql/VertxSqlClientInstrumentationModule.java index 7f71cf586bfb..448cff225ac4 100644 --- a/instrumentation/vertx/vertx-sql-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/sql/VertxSqlClientInstrumentationModule.java +++ b/instrumentation/vertx/vertx-sql-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/sql/VertxSqlClientInstrumentationModule.java @@ -6,14 +6,17 @@ package io.opentelemetry.javaagent.instrumentation.vertx.v4_0.sql; import static java.util.Arrays.asList; +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.instrumentation.internal.ExperimentalInstrumentationModule; import java.util.List; @AutoService(InstrumentationModule.class) -public class VertxSqlClientInstrumentationModule extends InstrumentationModule { +public class VertxSqlClientInstrumentationModule extends InstrumentationModule + implements ExperimentalInstrumentationModule { public VertxSqlClientInstrumentationModule() { super("vertx-sql-client", "vertx-sql-client-4.0", "vertx"); @@ -24,12 +27,18 @@ public boolean isHelperClass(String className) { return "io.vertx.sqlclient.impl.QueryExecutorUtil".equals(className); } + @Override + public List injectedClassNames() { + return singletonList("io.vertx.sqlclient.impl.QueryExecutorUtil"); + } + @Override public List typeInstrumentations() { return asList( new PoolInstrumentation(), new SqlClientBaseInstrumentation(), new QueryExecutorInstrumentation(), - new QueryResultBuilderInstrumentation()); + new QueryResultBuilderInstrumentation(), + new TransactionImplInstrumentation()); } } diff --git a/instrumentation/vertx/vertx-sql-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/sql/VertxSqlClientNetAttributesGetter.java b/instrumentation/vertx/vertx-sql-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/sql/VertxSqlClientNetAttributesGetter.java index 1fedb24cb682..f14caf7e8caf 100644 --- a/instrumentation/vertx/vertx-sql-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/sql/VertxSqlClientNetAttributesGetter.java +++ b/instrumentation/vertx/vertx-sql-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/sql/VertxSqlClientNetAttributesGetter.java @@ -5,19 +5,13 @@ package io.opentelemetry.javaagent.instrumentation.vertx.v4_0.sql; -import io.opentelemetry.instrumentation.api.instrumenter.net.NetClientAttributesGetter; +import io.opentelemetry.instrumentation.api.semconv.network.ServerAttributesGetter; import javax.annotation.Nullable; public enum VertxSqlClientNetAttributesGetter - implements NetClientAttributesGetter { + implements ServerAttributesGetter { INSTANCE; - @Nullable - @Override - public String getTransport(VertxSqlClientRequest request, @Nullable Void unused) { - return null; - } - @Nullable @Override public String getServerAddress(VertxSqlClientRequest request) { diff --git a/instrumentation/vertx/vertx-sql-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/sql/VertxSqlClientSingletons.java b/instrumentation/vertx/vertx-sql-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/sql/VertxSqlClientSingletons.java index bfbc953fbdb2..a77338e5c811 100644 --- a/instrumentation/vertx/vertx-sql-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/sql/VertxSqlClientSingletons.java +++ b/instrumentation/vertx/vertx-sql-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/sql/VertxSqlClientSingletons.java @@ -8,22 +8,24 @@ import io.opentelemetry.api.GlobalOpenTelemetry; import io.opentelemetry.context.Context; import io.opentelemetry.context.Scope; +import io.opentelemetry.instrumentation.api.incubator.semconv.db.DbClientSpanNameExtractor; +import io.opentelemetry.instrumentation.api.incubator.semconv.db.SqlClientAttributesExtractor; +import io.opentelemetry.instrumentation.api.incubator.semconv.net.PeerServiceAttributesExtractor; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; import io.opentelemetry.instrumentation.api.instrumenter.InstrumenterBuilder; import io.opentelemetry.instrumentation.api.instrumenter.SpanKindExtractor; import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.db.DbClientSpanNameExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.db.SqlClientAttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.net.NetClientAttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.net.PeerServiceAttributesExtractor; -import io.opentelemetry.javaagent.bootstrap.internal.CommonConfig; +import io.opentelemetry.instrumentation.api.semconv.network.ServerAttributesExtractor; +import io.opentelemetry.instrumentation.api.util.VirtualField; +import io.opentelemetry.javaagent.bootstrap.internal.AgentCommonConfig; +import io.vertx.core.Future; +import io.vertx.core.Promise; import io.vertx.sqlclient.SqlConnectOptions; -import java.util.Map; +import io.vertx.sqlclient.SqlConnection; +import io.vertx.sqlclient.impl.SqlClientBase; +import java.util.concurrent.CompletableFuture; public final class VertxSqlClientSingletons { - public static final String OTEL_REQUEST_KEY = "otel.request"; - public static final String OTEL_CONTEXT_KEY = "otel.context"; - public static final String OTEL_PARENT_CONTEXT_KEY = "otel.parent-context"; private static final String INSTRUMENTATION_NAME = "io.opentelemetry.vertx-sql-client-4.0"; private static final Instrumenter INSTRUMENTER; private static final ThreadLocal connectOptions = new ThreadLocal<>(); @@ -38,14 +40,14 @@ public final class VertxSqlClientSingletons { .addAttributesExtractor( SqlClientAttributesExtractor.builder(VertxSqlClientAttributesGetter.INSTANCE) .setStatementSanitizationEnabled( - CommonConfig.get().isStatementSanitizationEnabled()) + AgentCommonConfig.get().isStatementSanitizationEnabled()) .build()) .addAttributesExtractor( - NetClientAttributesExtractor.create(VertxSqlClientNetAttributesGetter.INSTANCE)) + ServerAttributesExtractor.create(VertxSqlClientNetAttributesGetter.INSTANCE)) .addAttributesExtractor( PeerServiceAttributesExtractor.create( VertxSqlClientNetAttributesGetter.INSTANCE, - CommonConfig.get().getPeerServiceMapping())); + AgentCommonConfig.get().getPeerServiceResolver())); INSTRUMENTER = builder.buildInstrumenter(SpanKindExtractor.alwaysClient()); } @@ -62,16 +64,66 @@ public static SqlConnectOptions getSqlConnectOptions() { return connectOptions.get(); } - public static Scope endQuerySpan(Map contextData, Throwable throwable) { - VertxSqlClientRequest otelRequest = - (VertxSqlClientRequest) contextData.remove(OTEL_REQUEST_KEY); - Context otelContext = (Context) contextData.remove(OTEL_CONTEXT_KEY); - Context otelParentContext = (Context) contextData.remove(OTEL_PARENT_CONTEXT_KEY); - if (otelRequest == null || otelContext == null || otelParentContext == null) { + private static final VirtualField, RequestData> requestDataField = + VirtualField.find(Promise.class, RequestData.class); + + public static void attachRequest( + Promise promise, VertxSqlClientRequest request, Context context, Context parentContext) { + requestDataField.set(promise, new RequestData(request, context, parentContext)); + } + + public static Scope endQuerySpan(Promise promise, Throwable throwable) { + RequestData requestData = requestDataField.get(promise); + if (requestData == null) { return null; } - instrumenter().end(otelContext, otelRequest, null, throwable); - return otelParentContext.makeCurrent(); + instrumenter().end(requestData.context, requestData.request, null, throwable); + return requestData.parentContext.makeCurrent(); + } + + static class RequestData { + final VertxSqlClientRequest request; + final Context context; + final Context parentContext; + + RequestData(VertxSqlClientRequest request, Context context, Context parentContext) { + this.request = request; + this.context = context; + this.parentContext = parentContext; + } + } + + // this virtual field is also used in SqlClientBase instrumentation + private static final VirtualField, SqlConnectOptions> connectOptionsField = + VirtualField.find(SqlClientBase.class, SqlConnectOptions.class); + + public static Future attachConnectOptions( + Future future, SqlConnectOptions connectOptions) { + return future.map( + sqlConnection -> { + if (sqlConnection instanceof SqlClientBase) { + connectOptionsField.set((SqlClientBase) sqlConnection, connectOptions); + } + return sqlConnection; + }); + } + + public static Future wrapContext(Future future) { + Context context = Context.current(); + CompletableFuture result = new CompletableFuture<>(); + future + .toCompletionStage() + .whenComplete( + (value, throwable) -> { + try (Scope ignore = context.makeCurrent()) { + if (throwable != null) { + result.completeExceptionally(throwable); + } else { + result.complete(value); + } + } + }); + return Future.fromCompletionStage(result); } private VertxSqlClientSingletons() {} diff --git a/instrumentation/vertx/vertx-sql-client-4.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/sql/VertxSqlClientTest.java b/instrumentation/vertx/vertx-sql-client-4.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/sql/VertxSqlClientTest.java index 28e669fd4b3f..17f318eb375f 100644 --- a/instrumentation/vertx/vertx-sql-client-4.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/sql/VertxSqlClientTest.java +++ b/instrumentation/vertx/vertx-sql-client-4.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/sql/VertxSqlClientTest.java @@ -7,21 +7,22 @@ import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.satisfies; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.DB_NAME; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.DB_OPERATION; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.DB_SQL_TABLE; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.DB_STATEMENT; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.DB_USER; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.EXCEPTION_EVENT_NAME; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.EXCEPTION_MESSAGE; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.EXCEPTION_STACKTRACE; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.EXCEPTION_TYPE; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.NET_PEER_NAME; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.NET_PEER_PORT; +import static io.opentelemetry.semconv.ExceptionAttributes.EXCEPTION_MESSAGE; +import static io.opentelemetry.semconv.ExceptionAttributes.EXCEPTION_STACKTRACE; +import static io.opentelemetry.semconv.ExceptionAttributes.EXCEPTION_TYPE; +import static io.opentelemetry.semconv.ServerAttributes.SERVER_ADDRESS; +import static io.opentelemetry.semconv.ServerAttributes.SERVER_PORT; +import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DB_NAME; +import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DB_OPERATION; +import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DB_SQL_TABLE; +import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DB_STATEMENT; +import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DB_USER; import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.testing.internal.AutoCleanupExtension; import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.sdk.testing.assertj.TraceAssert; import io.opentelemetry.sdk.trace.data.StatusData; import io.vertx.core.Vertx; import io.vertx.pgclient.PgConnectOptions; @@ -30,10 +31,16 @@ import io.vertx.sqlclient.PoolOptions; import io.vertx.sqlclient.Tuple; import java.time.Duration; +import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; +import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -53,9 +60,13 @@ class VertxSqlClientTest { @RegisterExtension private static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + @RegisterExtension + private static final AutoCleanupExtension cleanup = AutoCleanupExtension.create(); + private static GenericContainer container; private static Vertx vertx; private static Pool pool; + private static String host; private static int port; @BeforeAll @@ -70,11 +81,12 @@ static void setUp() throws Exception { .withStartupTimeout(Duration.ofMinutes(2)); container.start(); vertx = Vertx.vertx(); + host = container.getHost(); port = container.getMappedPort(5432); PgConnectOptions options = new PgConnectOptions() .setPort(port) - .setHost(container.getHost()) + .setHost(host) .setDatabase(DB) .setUser(USER_DB) .setPassword(PW_DB); @@ -130,8 +142,8 @@ void testSimpleSelect() throws Exception { equalTo(DB_STATEMENT, "select * from test"), equalTo(DB_OPERATION, "SELECT"), equalTo(DB_SQL_TABLE, "test"), - equalTo(NET_PEER_NAME, "localhost"), - equalTo(NET_PEER_PORT, port)), + equalTo(SERVER_ADDRESS, host), + equalTo(SERVER_PORT, port)), span -> span.hasName("callback") .hasKind(SpanKind.INTERNAL) @@ -171,7 +183,7 @@ void testInvalidQuery() throws Exception { .hasEventsSatisfyingExactly( event -> event - .hasName(EXCEPTION_EVENT_NAME) + .hasName("exception") .hasAttributesSatisfyingExactly( equalTo(EXCEPTION_TYPE, PgException.class.getName()), satisfies( @@ -184,8 +196,8 @@ void testInvalidQuery() throws Exception { equalTo(DB_NAME, DB), equalTo(DB_USER, USER_DB), equalTo(DB_STATEMENT, "invalid"), - equalTo(NET_PEER_NAME, "localhost"), - equalTo(NET_PEER_PORT, port)), + equalTo(SERVER_ADDRESS, host), + equalTo(SERVER_PORT, port)), span -> span.hasName("callback") .hasKind(SpanKind.INTERNAL) @@ -202,6 +214,10 @@ void testPreparedSelect() throws Exception { .toCompletableFuture() .get(30, TimeUnit.SECONDS); + assertPreparedSelect(); + } + + private static void assertPreparedSelect() { testing.waitAndAssertTraces( trace -> trace.hasSpansSatisfyingExactly( @@ -213,11 +229,11 @@ void testPreparedSelect() throws Exception { .hasAttributesSatisfyingExactly( equalTo(DB_NAME, DB), equalTo(DB_USER, USER_DB), - equalTo(DB_STATEMENT, "select * from test where id = $?"), + equalTo(DB_STATEMENT, "select * from test where id = $1"), equalTo(DB_OPERATION, "SELECT"), equalTo(DB_SQL_TABLE, "test"), - equalTo(NET_PEER_NAME, "localhost"), - equalTo(NET_PEER_PORT, port)))); + equalTo(SERVER_ADDRESS, host), + equalTo(SERVER_PORT, port)))); } @Test @@ -243,10 +259,166 @@ void testBatch() throws Exception { .hasAttributesSatisfyingExactly( equalTo(DB_NAME, DB), equalTo(DB_USER, USER_DB), - equalTo(DB_STATEMENT, "insert into test values ($?, $?) returning *"), + equalTo(DB_STATEMENT, "insert into test values ($1, $2) returning *"), equalTo(DB_OPERATION, "INSERT"), equalTo(DB_SQL_TABLE, "test"), - equalTo(NET_PEER_NAME, "localhost"), - equalTo(NET_PEER_PORT, port)))); + equalTo(SERVER_ADDRESS, host), + equalTo(SERVER_PORT, port)))); + } + + @Test + void testWithTransaction() throws Exception { + testing + .runWithSpan( + "parent", + () -> + pool.withTransaction( + conn -> + conn.preparedQuery("select * from test where id = $1") + .execute(Tuple.of(1)))) + .toCompletionStage() + .toCompletableFuture() + .get(30, TimeUnit.SECONDS); + + assertPreparedSelect(); + } + + @Test + void testWithConnection() throws Exception { + testing + .runWithSpan( + "parent", + () -> + pool.withConnection( + conn -> + conn.preparedQuery("select * from test where id = $1") + .execute(Tuple.of(1)))) + .toCompletionStage() + .toCompletableFuture() + .get(30, TimeUnit.SECONDS); + + assertPreparedSelect(); + } + + @Test + void testManyQueries() throws Exception { + int count = 50; + CountDownLatch latch = new CountDownLatch(count); + List> futureList = new ArrayList<>(); + List> resultList = new ArrayList<>(); + for (int i = 0; i < count; i++) { + CompletableFuture future = new CompletableFuture<>(); + futureList.add(future); + resultList.add( + future.whenComplete((rows, throwable) -> testing.runWithSpan("callback", () -> {}))); + } + for (CompletableFuture future : futureList) { + testing.runWithSpan( + "parent", + () -> + pool.query("select * from test") + .execute( + rowSetAsyncResult -> { + if (rowSetAsyncResult.succeeded()) { + future.complete(rowSetAsyncResult.result()); + } else { + future.completeExceptionally(rowSetAsyncResult.cause()); + } + latch.countDown(); + })); + } + latch.await(30, TimeUnit.SECONDS); + for (CompletableFuture result : resultList) { + result.get(10, TimeUnit.SECONDS); + } + + List> assertions = + Collections.nCopies( + count, + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasKind(SpanKind.INTERNAL), + span -> + span.hasName("SELECT tempdb.test") + .hasKind(SpanKind.CLIENT) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo(DB_NAME, DB), + equalTo(DB_USER, USER_DB), + equalTo(DB_STATEMENT, "select * from test"), + equalTo(DB_OPERATION, "SELECT"), + equalTo(DB_SQL_TABLE, "test"), + equalTo(SERVER_ADDRESS, host), + equalTo(SERVER_PORT, port)), + span -> + span.hasName("callback") + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(0)))); + testing.waitAndAssertTraces(assertions); + } + + @Test + void testConcurrency() throws Exception { + int count = 50; + CountDownLatch latch = new CountDownLatch(count); + List> futureList = new ArrayList<>(); + List> resultList = new ArrayList<>(); + for (int i = 0; i < count; i++) { + CompletableFuture future = new CompletableFuture<>(); + futureList.add(future); + resultList.add( + future.whenComplete((rows, throwable) -> testing.runWithSpan("callback", () -> {}))); + } + ExecutorService executorService = Executors.newFixedThreadPool(4); + cleanup.deferCleanup(() -> executorService.shutdown()); + for (CompletableFuture future : futureList) { + executorService.submit( + () -> { + testing.runWithSpan( + "parent", + () -> + pool.withConnection( + conn -> + conn.preparedQuery("select * from test where id = $1") + .execute(Tuple.of(1))) + .onComplete( + rowSetAsyncResult -> { + if (rowSetAsyncResult.succeeded()) { + future.complete(rowSetAsyncResult.result()); + } else { + future.completeExceptionally(rowSetAsyncResult.cause()); + } + latch.countDown(); + })); + }); + } + latch.await(30, TimeUnit.SECONDS); + for (CompletableFuture result : resultList) { + result.get(10, TimeUnit.SECONDS); + } + + List> assertions = + Collections.nCopies( + count, + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasKind(SpanKind.INTERNAL), + span -> + span.hasName("SELECT tempdb.test") + .hasKind(SpanKind.CLIENT) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo(DB_NAME, DB), + equalTo(DB_USER, USER_DB), + equalTo(DB_STATEMENT, "select * from test where id = $1"), + equalTo(DB_OPERATION, "SELECT"), + equalTo(DB_SQL_TABLE, "test"), + equalTo(SERVER_ADDRESS, host), + equalTo(SERVER_PORT, port)), + span -> + span.hasName("callback") + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(0)))); + testing.waitAndAssertTraces(assertions); } } diff --git a/instrumentation/vertx/vertx-web-3.0/javaagent/build.gradle.kts b/instrumentation/vertx/vertx-web-3.0/javaagent/build.gradle.kts index 3f93c7faefae..7a58ec9e2662 100644 --- a/instrumentation/vertx/vertx-web-3.0/javaagent/build.gradle.kts +++ b/instrumentation/vertx/vertx-web-3.0/javaagent/build.gradle.kts @@ -45,8 +45,10 @@ testing { } } +val testLatestDeps = findProperty("testLatestDeps") as Boolean + tasks { - if (findProperty("testLatestDeps") as Boolean) { + if (testLatestDeps) { // disable regular test running and compiling tasks when latest dep test task is run named("test") { enabled = false @@ -54,9 +56,13 @@ tasks { named("compileTestGroovy") { enabled = false } + } - check { - dependsOn(testing.suites) - } + named("latestDepTest") { + enabled = testLatestDeps + } + + check { + dependsOn(testing.suites) } } diff --git a/instrumentation/vertx/vertx-web-3.0/javaagent/src/latestDepTest/groovy/server/VertxLatestHttpServerTest.groovy b/instrumentation/vertx/vertx-web-3.0/javaagent/src/latestDepTest/groovy/server/VertxLatestHttpServerTest.groovy index 7a679da334b1..db274a9d8bed 100644 --- a/instrumentation/vertx/vertx-web-3.0/javaagent/src/latestDepTest/groovy/server/VertxLatestHttpServerTest.groovy +++ b/instrumentation/vertx/vertx-web-3.0/javaagent/src/latestDepTest/groovy/server/VertxLatestHttpServerTest.groovy @@ -5,6 +5,7 @@ package server + import io.vertx.core.AbstractVerticle class VertxLatestHttpServerTest extends AbstractVertxHttpServerTest { diff --git a/instrumentation/vertx/vertx-web-3.0/javaagent/src/latestDepTest/java/server/VertxLatestWebServer.java b/instrumentation/vertx/vertx-web-3.0/javaagent/src/latestDepTest/java/server/VertxLatestWebServer.java index db82adec66b6..af850c913aeb 100644 --- a/instrumentation/vertx/vertx-web-3.0/javaagent/src/latestDepTest/java/server/VertxLatestWebServer.java +++ b/instrumentation/vertx/vertx-web-3.0/javaagent/src/latestDepTest/java/server/VertxLatestWebServer.java @@ -25,7 +25,9 @@ public void end(HttpServerResponse response, String message) { public void start(Promise startPromise) { int port = config().getInteger(CONFIG_HTTP_SERVER_PORT); Router router = buildRouter(); + Router mainRouter = Router.router(vertx); + mainRouter.route("/vertx-app/*").subRouter(router); - vertx.createHttpServer().requestHandler(router).listen(port, it -> startPromise.complete()); + vertx.createHttpServer().requestHandler(mainRouter).listen(port, it -> startPromise.complete()); } } diff --git a/instrumentation/vertx/vertx-web-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/RoutingContextHandlerWrapper.java b/instrumentation/vertx/vertx-web-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/RoutingContextHandlerWrapper.java index cd4175286810..0caddc3dae4b 100644 --- a/instrumentation/vertx/vertx-web-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/RoutingContextHandlerWrapper.java +++ b/instrumentation/vertx/vertx-web-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/RoutingContextHandlerWrapper.java @@ -5,11 +5,15 @@ package io.opentelemetry.javaagent.instrumentation.vertx; +import static io.opentelemetry.context.ContextKey.named; + import io.opentelemetry.api.trace.Span; import io.opentelemetry.context.Context; +import io.opentelemetry.context.ContextKey; +import io.opentelemetry.context.Scope; import io.opentelemetry.instrumentation.api.instrumenter.LocalRootSpan; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteHolder; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteSource; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRoute; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRouteSource; import io.vertx.core.Handler; import io.vertx.ext.web.RoutingContext; import java.lang.reflect.InvocationTargetException; @@ -20,6 +24,8 @@ /** 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 ContextKey ROUTE_KEY = named("opentelemetry-vertx-route"); + private final Handler handler; public RoutingContextHandlerWrapper(Handler handler) { @@ -29,10 +35,13 @@ public RoutingContextHandlerWrapper(Handler handler) { @Override public void handle(RoutingContext context) { Context otelContext = Context.current(); - HttpRouteHolder.updateHttpRoute( - otelContext, HttpRouteSource.CONTROLLER, RoutingContextHandlerWrapper::getRoute, context); + String route = getRoute(otelContext, context); + if (route != null && route.endsWith("/")) { + route = route.substring(0, route.length() - 1); + } + HttpServerRoute.update(otelContext, HttpServerRouteSource.NESTED_CONTROLLER, route); - try { + try (Scope ignore = otelContext.with(ROUTE_KEY, route).makeCurrent()) { handler.handle(context); } catch (Throwable throwable) { Span serverSpan = LocalRootSpan.fromContextOrNull(otelContext); @@ -44,7 +53,9 @@ public void handle(RoutingContext context) { } private static String getRoute(Context otelContext, RoutingContext routingContext) { - return routingContext.currentRoute().getPath(); + String route = routingContext.currentRoute().getPath(); + String existingRoute = otelContext.get(ROUTE_KEY); + return existingRoute != null ? existingRoute + route : route; } private static Throwable unwrapThrowable(Throwable throwable) { diff --git a/instrumentation/vertx/vertx-web-3.0/javaagent/src/version3Test/java/server/VertxWebServer.java b/instrumentation/vertx/vertx-web-3.0/javaagent/src/version3Test/java/server/VertxWebServer.java index cf3dff4a65b7..aad9975810b2 100644 --- a/instrumentation/vertx/vertx-web-3.0/javaagent/src/version3Test/java/server/VertxWebServer.java +++ b/instrumentation/vertx/vertx-web-3.0/javaagent/src/version3Test/java/server/VertxWebServer.java @@ -25,10 +25,12 @@ public void end(HttpServerResponse response, String message) { public void start(Future startFuture) { int port = config().getInteger(CONFIG_HTTP_SERVER_PORT); Router router = buildRouter(); + Router mainRouter = Router.router(vertx); + mainRouter.mountSubRouter("/vertx-app", router); vertx .createHttpServer() - .requestHandler(router::accept) + .requestHandler(mainRouter::accept) .listen(port, it -> startFuture.complete()); } } diff --git a/instrumentation/vertx/vertx-web-3.0/testing/src/main/groovy/server/AbstractVertxHttpServerTest.groovy b/instrumentation/vertx/vertx-web-3.0/testing/src/main/groovy/server/AbstractVertxHttpServerTest.groovy index 4675dba96f50..9926eb768db3 100644 --- a/instrumentation/vertx/vertx-web-3.0/testing/src/main/groovy/server/AbstractVertxHttpServerTest.groovy +++ b/instrumentation/vertx/vertx-web-3.0/testing/src/main/groovy/server/AbstractVertxHttpServerTest.groovy @@ -5,9 +5,10 @@ package server - +import io.opentelemetry.instrumentation.api.internal.HttpConstants import io.opentelemetry.instrumentation.test.AgentTestTrait import io.opentelemetry.instrumentation.test.base.HttpServerTest +import io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint import io.vertx.core.AbstractVerticle import io.vertx.core.DeploymentOptions import io.vertx.core.Vertx @@ -56,4 +57,20 @@ abstract class AbstractVertxHttpServerTest extends HttpServerTest impleme // server spans are ended inside of the controller spans return false } + + @Override + String getContextPath() { + "/vertx-app" + } + + @Override + String expectedHttpRoute(ServerEndpoint endpoint, String method) { + if (method == HttpConstants._OTHER) { + return getContextPath() + endpoint.path + } + if (endpoint == ServerEndpoint.NOT_FOUND) { + return getContextPath() + } + return super.expectedHttpRoute(endpoint, method) + } } diff --git a/instrumentation/vibur-dbcp-11.0/library/src/main/java/io/opentelemetry/instrumentation/viburdbcp/v11_0/ConnectionPoolMetrics.java b/instrumentation/vibur-dbcp-11.0/library/src/main/java/io/opentelemetry/instrumentation/viburdbcp/v11_0/ConnectionPoolMetrics.java index 9baf96804d25..a886974d0642 100644 --- a/instrumentation/vibur-dbcp-11.0/library/src/main/java/io/opentelemetry/instrumentation/viburdbcp/v11_0/ConnectionPoolMetrics.java +++ b/instrumentation/vibur-dbcp-11.0/library/src/main/java/io/opentelemetry/instrumentation/viburdbcp/v11_0/ConnectionPoolMetrics.java @@ -9,7 +9,7 @@ import io.opentelemetry.api.common.Attributes; import io.opentelemetry.api.metrics.BatchCallback; import io.opentelemetry.api.metrics.ObservableLongMeasurement; -import io.opentelemetry.instrumentation.api.metrics.db.DbConnectionPoolMetrics; +import io.opentelemetry.instrumentation.api.incubator.semconv.db.DbConnectionPoolMetrics; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import org.vibur.dbcp.ViburDBCPDataSource; diff --git a/instrumentation/wicket-8.0/common-testing/build.gradle.kts b/instrumentation/wicket-8.0/common-testing/build.gradle.kts new file mode 100644 index 000000000000..d99731170744 --- /dev/null +++ b/instrumentation/wicket-8.0/common-testing/build.gradle.kts @@ -0,0 +1,9 @@ +plugins { + id("otel.java-conventions") +} + +dependencies { + implementation(project(":testing-common")) + implementation("org.apache.wicket:wicket:8.0.0") + implementation("org.jsoup:jsoup:1.13.1") +} diff --git a/instrumentation/wicket-8.0/common-testing/src/main/java/hello/ExceptionPage.java b/instrumentation/wicket-8.0/common-testing/src/main/java/hello/ExceptionPage.java new file mode 100644 index 000000000000..eccacc9325e0 --- /dev/null +++ b/instrumentation/wicket-8.0/common-testing/src/main/java/hello/ExceptionPage.java @@ -0,0 +1,16 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package hello; + +import org.apache.wicket.markup.html.WebPage; + +public class ExceptionPage extends WebPage { + private static final long serialVersionUID = 1L; + + public ExceptionPage() throws Exception { + throw new Exception("test exception"); + } +} diff --git a/instrumentation/wicket-8.0/common-testing/src/main/java/hello/HelloApplication.java b/instrumentation/wicket-8.0/common-testing/src/main/java/hello/HelloApplication.java new file mode 100644 index 000000000000..8b0e55e98d3e --- /dev/null +++ b/instrumentation/wicket-8.0/common-testing/src/main/java/hello/HelloApplication.java @@ -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; + +public class HelloApplication extends WebApplication { + @Override + public Class getHomePage() { + return HelloPage.class; + } + + @Override + protected void init() { + super.init(); + + mountPage("/exception", ExceptionPage.class); + } + + @Override + public RuntimeConfigurationType getConfigurationType() { + return RuntimeConfigurationType.DEPLOYMENT; + } +} diff --git a/instrumentation/wicket-8.0/common-testing/src/main/java/hello/HelloPage.java b/instrumentation/wicket-8.0/common-testing/src/main/java/hello/HelloPage.java new file mode 100644 index 000000000000..bf327b6dc38d --- /dev/null +++ b/instrumentation/wicket-8.0/common-testing/src/main/java/hello/HelloPage.java @@ -0,0 +1,17 @@ +/* + * 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; + +public class HelloPage extends WebPage { + private static final long serialVersionUID = 1L; + + public HelloPage() { + add(new Label("message", "Hello World!")); + } +} diff --git a/instrumentation/wicket-8.0/common-testing/src/main/java/io/opentelemetry/javaagent/instrumentation/wicket/AbstractWicketTest.java b/instrumentation/wicket-8.0/common-testing/src/main/java/io/opentelemetry/javaagent/instrumentation/wicket/AbstractWicketTest.java new file mode 100644 index 000000000000..dbbf92df79f0 --- /dev/null +++ b/instrumentation/wicket-8.0/common-testing/src/main/java/io/opentelemetry/javaagent/instrumentation/wicket/AbstractWicketTest.java @@ -0,0 +1,73 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.wicket; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpServerUsingTest; +import io.opentelemetry.instrumentation.testing.junit.http.HttpServerInstrumentationExtension; +import io.opentelemetry.sdk.trace.data.StatusData; +import io.opentelemetry.testing.internal.armeria.common.AggregatedHttpResponse; +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +abstract class AbstractWicketTest extends AbstractHttpServerUsingTest { + + @RegisterExtension + public static final InstrumentationExtension testing = + HttpServerInstrumentationExtension.forAgent(); + + @Override + protected String getContextPath() { + return "/jetty-context"; + } + + @BeforeAll + void setup() { + startServer(); + } + + @Test + void testHello() { + AggregatedHttpResponse response = + client.get(address.resolve("wicket-test/").toString()).aggregate().join(); + Document doc = Jsoup.parse(response.contentUtf8()); + + assertThat(response.status().code()).isEqualTo(200); + assertThat(doc.selectFirst("#message").text()).isEqualTo("Hello World!"); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("GET " + getContextPath() + "/wicket-test/hello.HelloPage") + .hasNoParent() + .hasKind(SpanKind.SERVER))); + } + + @Test + void testException() { + AggregatedHttpResponse response = + client.get(address.resolve("wicket-test/exception").toString()).aggregate().join(); + + assertThat(response.status().code()).isEqualTo(500); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("GET " + getContextPath() + "/wicket-test/hello.ExceptionPage") + .hasKind(SpanKind.SERVER) + .hasNoParent() + .hasStatus(StatusData.error()) + .hasException(new Exception("test exception")))); + } +} diff --git a/instrumentation/wicket-8.0/javaagent/src/test/resources/hello/HelloPage.html b/instrumentation/wicket-8.0/common-testing/src/main/resources/hello/HelloPage.html similarity index 60% rename from instrumentation/wicket-8.0/javaagent/src/test/resources/hello/HelloPage.html rename to instrumentation/wicket-8.0/common-testing/src/main/resources/hello/HelloPage.html index b4cd0e1be99e..1d24d824172d 100644 --- a/instrumentation/wicket-8.0/javaagent/src/test/resources/hello/HelloPage.html +++ b/instrumentation/wicket-8.0/common-testing/src/main/resources/hello/HelloPage.html @@ -1,6 +1,6 @@ - - Message goes here - + + Message goes here + diff --git a/instrumentation/wicket-8.0/javaagent/build.gradle.kts b/instrumentation/wicket-8.0/javaagent/build.gradle.kts index a87ef285f780..e0ab75135adb 100644 --- a/instrumentation/wicket-8.0/javaagent/build.gradle.kts +++ b/instrumentation/wicket-8.0/javaagent/build.gradle.kts @@ -15,12 +15,4 @@ dependencies { bootstrap(project(":instrumentation:servlet:servlet-common:bootstrap")) 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/instrumentation/wicket-8.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/wicket/RequestHandlerExecutorInstrumentation.java b/instrumentation/wicket-8.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/wicket/RequestHandlerExecutorInstrumentation.java index 720bfb841b32..26e3c80e10e3 100644 --- a/instrumentation/wicket-8.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/wicket/RequestHandlerExecutorInstrumentation.java +++ b/instrumentation/wicket-8.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/wicket/RequestHandlerExecutorInstrumentation.java @@ -5,11 +5,11 @@ package io.opentelemetry.javaagent.instrumentation.wicket; -import static io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteSource.CONTROLLER; +import static io.opentelemetry.instrumentation.api.semconv.http.HttpServerRouteSource.CONTROLLER; import static net.bytebuddy.matcher.ElementMatchers.named; import static net.bytebuddy.matcher.ElementMatchers.takesArgument; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteHolder; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRoute; import io.opentelemetry.javaagent.bootstrap.Java8BytecodeBridge; import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; @@ -39,7 +39,7 @@ public static class ExecuteAdvice { @Advice.OnMethodEnter(suppress = Throwable.class) public static void onExit(@Advice.Argument(0) IRequestHandler handler) { if (handler instanceof IPageClassRequestHandler) { - HttpRouteHolder.updateHttpRoute( + HttpServerRoute.update( Java8BytecodeBridge.currentContext(), CONTROLLER, WicketServerSpanNaming.SERVER_SPAN_NAME, diff --git a/instrumentation/wicket-8.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/wicket/WicketServerSpanNaming.java b/instrumentation/wicket-8.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/wicket/WicketServerSpanNaming.java index a2b9d9349b0b..a7df67a56fa5 100644 --- a/instrumentation/wicket-8.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/wicket/WicketServerSpanNaming.java +++ b/instrumentation/wicket-8.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/wicket/WicketServerSpanNaming.java @@ -5,14 +5,14 @@ package io.opentelemetry.javaagent.instrumentation.wicket; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteGetter; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRouteGetter; import io.opentelemetry.javaagent.bootstrap.servlet.ServletContextPath; import org.apache.wicket.core.request.handler.IPageClassRequestHandler; import org.apache.wicket.request.cycle.RequestCycle; public final class WicketServerSpanNaming { - public static final HttpRouteGetter SERVER_SPAN_NAME = + public static final HttpServerRouteGetter SERVER_SPAN_NAME = (context, handler) -> { // using class name as page name String pageName = handler.getPageClass().getName(); diff --git a/instrumentation/wicket-8.0/javaagent/src/test/groovy/WicketTest.groovy b/instrumentation/wicket-8.0/javaagent/src/test/groovy/WicketTest.groovy deleted file mode 100644 index 109576a3ba40..000000000000 --- a/instrumentation/wicket-8.0/javaagent/src/test/groovy/WicketTest.groovy +++ /dev/null @@ -1,102 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -import hello.HelloApplication -import io.opentelemetry.api.trace.SpanKind -import io.opentelemetry.api.trace.StatusCode -import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification -import io.opentelemetry.instrumentation.test.base.HttpServerTestTrait -import io.opentelemetry.testing.internal.armeria.common.AggregatedHttpResponse -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 - -import javax.servlet.DispatcherType - -class WicketTest extends AgentInstrumentationSpecification implements HttpServerTestTrait { - - def setupSpec() { - setupServer() - } - - def cleanupSpec() { - cleanupServer() - } - - @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) { - span(0) { - name "GET " + getContextPath() + "/wicket-test/hello.HelloPage" - kind SpanKind.SERVER - hasNoParent() - } - } - } - } - - def "test exception"() { - setup: - AggregatedHttpResponse response = client.get(address.resolve("wicket-test/exception").toString()).aggregate().join() - - expect: - response.status().code() == 500 - def ex = new Exception("test exception") - - assertTraces(1) { - trace(0, 1) { - span(0) { - name "GET " + getContextPath() + "/wicket-test/hello.ExceptionPage" - kind SpanKind.SERVER - hasNoParent() - status StatusCode.ERROR - errorEvent(ex.class, ex.message) - } - } - } - } -} diff --git a/instrumentation/wicket-8.0/javaagent/src/test/groovy/hello/ExceptionPage.groovy b/instrumentation/wicket-8.0/javaagent/src/test/groovy/hello/ExceptionPage.groovy deleted file mode 100644 index 2314d980489a..000000000000 --- a/instrumentation/wicket-8.0/javaagent/src/test/groovy/hello/ExceptionPage.groovy +++ /dev/null @@ -1,14 +0,0 @@ -/* - * 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/instrumentation/wicket-8.0/javaagent/src/test/groovy/hello/HelloApplication.groovy b/instrumentation/wicket-8.0/javaagent/src/test/groovy/hello/HelloApplication.groovy deleted file mode 100644 index 6f7295027696..000000000000 --- a/instrumentation/wicket-8.0/javaagent/src/test/groovy/hello/HelloApplication.groovy +++ /dev/null @@ -1,29 +0,0 @@ -/* - * 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/instrumentation/wicket-8.0/javaagent/src/test/groovy/hello/HelloPage.groovy b/instrumentation/wicket-8.0/javaagent/src/test/groovy/hello/HelloPage.groovy deleted file mode 100644 index 80e0df316e52..000000000000 --- a/instrumentation/wicket-8.0/javaagent/src/test/groovy/hello/HelloPage.groovy +++ /dev/null @@ -1,15 +0,0 @@ -/* - * 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/instrumentation/wicket-8.0/wicket10-testing/build.gradle.kts b/instrumentation/wicket-8.0/wicket10-testing/build.gradle.kts new file mode 100644 index 000000000000..6c960a530270 --- /dev/null +++ b/instrumentation/wicket-8.0/wicket10-testing/build.gradle.kts @@ -0,0 +1,20 @@ +plugins { + id("otel.javaagent-testing") +} + +dependencies { + library("org.apache.wicket:wicket:10.0.0") + + testImplementation(project(":instrumentation:wicket-8.0:common-testing")) + testImplementation("org.jsoup:jsoup:1.13.1") + testImplementation("org.eclipse.jetty:jetty-server:11.0.0") + testImplementation("org.eclipse.jetty:jetty-servlet:11.0.0") + + testInstrumentation(project(":instrumentation:wicket-8.0:javaagent")) + testInstrumentation(project(":instrumentation:servlet:servlet-5.0:javaagent")) + testInstrumentation(project(":instrumentation:servlet:servlet-javax-common:javaagent")) +} + +otelJava { + minJavaVersionSupported.set(JavaVersion.VERSION_17) +} diff --git a/instrumentation/wicket-8.0/wicket10-testing/src/test/java/io/opentelemetry/javaagent/instrumentation/wicket/WicketTest.java b/instrumentation/wicket-8.0/wicket10-testing/src/test/java/io/opentelemetry/javaagent/instrumentation/wicket/WicketTest.java new file mode 100644 index 000000000000..4e6d35644379 --- /dev/null +++ b/instrumentation/wicket-8.0/wicket10-testing/src/test/java/io/opentelemetry/javaagent/instrumentation/wicket/WicketTest.java @@ -0,0 +1,49 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.wicket; + +import hello.HelloApplication; +import jakarta.servlet.DispatcherType; +import jakarta.servlet.FilterRegistration; +import java.util.EnumSet; +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.Resource; + +class WicketTest extends AbstractWicketTest { + + @Override + protected Server setupServer() throws Exception { + Server server = new Server(port); + + ServletContextHandler context = new ServletContextHandler(0); + context.setContextPath(getContextPath()); + + Resource resource = Resource.newResource(getClass().getResource("/")); + context.setBaseResource(resource); + server.setHandler(context); + + context.addServlet(DefaultServlet.class, "/"); + FilterRegistration.Dynamic registration = + context.getServletContext().addFilter("WicketApplication", WicketFilter.class); + registration.setInitParameter("applicationClassName", HelloApplication.class.getName()); + registration.setInitParameter("filterMappingUrlPattern", "/wicket-test/*"); + registration.addMappingForUrlPatterns( + EnumSet.of(DispatcherType.REQUEST), false, "/wicket-test/*"); + + server.start(); + + return server; + } + + @Override + protected void stopServer(Server server) throws Exception { + server.stop(); + server.destroy(); + } +} diff --git a/instrumentation/wicket-8.0/wicket8-testing/build.gradle.kts b/instrumentation/wicket-8.0/wicket8-testing/build.gradle.kts new file mode 100644 index 000000000000..635199f93a25 --- /dev/null +++ b/instrumentation/wicket-8.0/wicket8-testing/build.gradle.kts @@ -0,0 +1,27 @@ +plugins { + id("otel.javaagent-testing") +} + +dependencies { + library("org.apache.wicket:wicket:8.0.0") + + testImplementation(project(":instrumentation:wicket-8.0:common-testing")) + 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:wicket-8.0:javaagent")) + testInstrumentation(project(":instrumentation:servlet:servlet-3.0:javaagent")) + testInstrumentation(project(":instrumentation:servlet:servlet-javax-common:javaagent")) + + latestDepTestLibrary("org.apache.wicket:wicket:9.+") +} + +val latestDepTest = findProperty("testLatestDeps") as Boolean + +// Wicket 9 requires Java 11 +if (latestDepTest) { + otelJava { + minJavaVersionSupported.set(JavaVersion.VERSION_11) + } +} diff --git a/instrumentation/wicket-8.0/wicket8-testing/src/test/java/io/opentelemetry/javaagent/instrumentation/wicket/WicketTest.java b/instrumentation/wicket-8.0/wicket8-testing/src/test/java/io/opentelemetry/javaagent/instrumentation/wicket/WicketTest.java new file mode 100644 index 000000000000..2ce637aa5a4c --- /dev/null +++ b/instrumentation/wicket-8.0/wicket8-testing/src/test/java/io/opentelemetry/javaagent/instrumentation/wicket/WicketTest.java @@ -0,0 +1,50 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.wicket; + +import hello.HelloApplication; +import java.util.EnumSet; +import javax.servlet.DispatcherType; +import javax.servlet.FilterRegistration; +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.eclipse.jetty.util.resource.Resource; + +class WicketTest extends AbstractWicketTest { + + @Override + protected Server setupServer() throws Exception { + Server server = new Server(port); + + ServletContextHandler context = new ServletContextHandler(0); + context.setContextPath(getContextPath()); + + Resource resource = new FileResource(getClass().getResource("/")); + context.setBaseResource(resource); + server.setHandler(context); + + context.addServlet(DefaultServlet.class, "/"); + FilterRegistration.Dynamic registration = + context.getServletContext().addFilter("WicketApplication", WicketFilter.class); + registration.setInitParameter("applicationClassName", HelloApplication.class.getName()); + registration.setInitParameter("filterMappingUrlPattern", "/wicket-test/*"); + registration.addMappingForUrlPatterns( + EnumSet.of(DispatcherType.REQUEST), false, "/wicket-test/*"); + + server.start(); + + return server; + } + + @Override + protected void stopServer(Server server) throws Exception { + server.stop(); + server.destroy(); + } +} diff --git a/instrumentation/xxl-job/README.md b/instrumentation/xxl-job/README.md new file mode 100644 index 000000000000..66f867b76fb8 --- /dev/null +++ b/instrumentation/xxl-job/README.md @@ -0,0 +1,5 @@ +# Settings for the XXL-JOB instrumentation + +| System property | Type | Default | Description | +|-------------------------------------------------------------|---------|---------|-----------------------------------------------------| +| `otel.instrumentation.xxl-job.experimental-span-attributes` | Boolean | `false` | Enable the capture of experimental span attributes. | diff --git a/instrumentation/xxl-job/xxl-job-1.9.2/javaagent/build.gradle.kts b/instrumentation/xxl-job/xxl-job-1.9.2/javaagent/build.gradle.kts new file mode 100644 index 000000000000..57cbcd6c422f --- /dev/null +++ b/instrumentation/xxl-job/xxl-job-1.9.2/javaagent/build.gradle.kts @@ -0,0 +1,36 @@ +plugins { + id("otel.javaagent-instrumentation") +} + +muzzle { + pass { + group.set("com.xuxueli") + module.set("xxl-job-core") + versions.set("[1.9.2, 2.1.2)") + assertInverse.set(true) + } +} + +dependencies { + library("com.xuxueli:xxl-job-core:1.9.2") { + exclude("org.codehaus.groovy", "groovy") + } + implementation(project(":instrumentation:xxl-job:xxl-job-common:javaagent")) + + testInstrumentation(project(":instrumentation:xxl-job:xxl-job-2.1.2:javaagent")) + testInstrumentation(project(":instrumentation:xxl-job:xxl-job-2.3.0:javaagent")) + + // It needs the javax.annotation-api in xxl-job-core 1.9.2. + testImplementation("javax.annotation:javax.annotation-api:1.3.2") + testImplementation(project(":instrumentation:xxl-job:xxl-job-common:testing")) + latestDepTestLibrary("com.xuxueli:xxl-job-core:2.1.1") { + exclude("org.codehaus.groovy", "groovy") + } +} + +tasks.withType().configureEach { + // required on jdk17 + jvmArgs("--add-opens=java.base/java.lang=ALL-UNNAMED") + jvmArgs("-XX:+IgnoreUnrecognizedVMOptions") + jvmArgs("-Dotel.instrumentation.xxl-job.experimental-span-attributes=true") +} diff --git a/instrumentation/xxl-job/xxl-job-1.9.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/xxljob/v1_9_2/GlueJobHandlerInstrumentation.java b/instrumentation/xxl-job/xxl-job-1.9.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/xxljob/v1_9_2/GlueJobHandlerInstrumentation.java new file mode 100644 index 000000000000..5c737335ebcb --- /dev/null +++ b/instrumentation/xxl-job/xxl-job-1.9.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/xxljob/v1_9_2/GlueJobHandlerInstrumentation.java @@ -0,0 +1,67 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.xxljob.v1_9_2; + +import static io.opentelemetry.javaagent.bootstrap.Java8BytecodeBridge.currentContext; +import static io.opentelemetry.javaagent.instrumentation.xxljob.v1_9_2.XxlJobSingletons.helper; +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import com.xxl.job.core.handler.IJobHandler; +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.xxljob.common.XxlJobProcessRequest; +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 GlueJobHandlerInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("com.xxl.job.core.handler.impl.GlueJobHandler"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + named("execute").and(isPublic()).and(takesArguments(String.class)), + GlueJobHandlerInstrumentation.class.getName() + "$ScheduleAdvice"); + } + + @SuppressWarnings("unused") + public static class ScheduleAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onSchedule( + @Advice.FieldValue("jobHandler") IJobHandler handler, + @Advice.Local("otelRequest") XxlJobProcessRequest request, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + Context parentContext = currentContext(); + request = XxlJobProcessRequest.createGlueJobRequest(handler); + context = helper().startSpan(parentContext, request); + if (context == null) { + return; + } + scope = context.makeCurrent(); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void stopSpan( + @Advice.Return(typing = Assigner.Typing.DYNAMIC) Object result, + @Advice.Thrown Throwable throwable, + @Advice.Local("otelRequest") XxlJobProcessRequest request, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + helper().stopSpan(result, request, throwable, scope, context); + } + } +} diff --git a/instrumentation/xxl-job/xxl-job-1.9.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/xxljob/v1_9_2/ScriptJobHandlerInstrumentation.java b/instrumentation/xxl-job/xxl-job-1.9.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/xxljob/v1_9_2/ScriptJobHandlerInstrumentation.java new file mode 100644 index 000000000000..7e947a24ffec --- /dev/null +++ b/instrumentation/xxl-job/xxl-job-1.9.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/xxljob/v1_9_2/ScriptJobHandlerInstrumentation.java @@ -0,0 +1,69 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.xxljob.v1_9_2; + +import static io.opentelemetry.javaagent.bootstrap.Java8BytecodeBridge.currentContext; +import static io.opentelemetry.javaagent.instrumentation.xxljob.v1_9_2.XxlJobSingletons.helper; +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.xxl.job.core.glue.GlueTypeEnum; +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.xxljob.common.XxlJobProcessRequest; +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 ScriptJobHandlerInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("com.xxl.job.core.handler.impl.ScriptJobHandler"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + named("execute").and(isPublic()).and(takesArguments(1).and(takesArgument(0, String.class))), + ScriptJobHandlerInstrumentation.class.getName() + "$ScheduleAdvice"); + } + + @SuppressWarnings("unused") + public static class ScheduleAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onSchedule( + @Advice.FieldValue("glueType") GlueTypeEnum glueType, + @Advice.FieldValue("jobId") int jobId, + @Advice.Local("otelRequest") XxlJobProcessRequest request, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + Context parentContext = currentContext(); + request = XxlJobProcessRequest.createScriptJobRequest(glueType, jobId); + context = helper().startSpan(parentContext, request); + if (context == null) { + return; + } + scope = context.makeCurrent(); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void stopSpan( + @Advice.Return(typing = Assigner.Typing.DYNAMIC) Object result, + @Advice.Thrown Throwable throwable, + @Advice.Local("otelRequest") XxlJobProcessRequest request, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + helper().stopSpan(result, request, throwable, scope, context); + } + } +} diff --git a/instrumentation/xxl-job/xxl-job-1.9.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/xxljob/v1_9_2/SimpleJobHandlerInstrumentation.java b/instrumentation/xxl-job/xxl-job-1.9.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/xxljob/v1_9_2/SimpleJobHandlerInstrumentation.java new file mode 100644 index 000000000000..076c90d7c6b5 --- /dev/null +++ b/instrumentation/xxl-job/xxl-job-1.9.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/xxljob/v1_9_2/SimpleJobHandlerInstrumentation.java @@ -0,0 +1,76 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.xxljob.v1_9_2; + +import static io.opentelemetry.javaagent.bootstrap.Java8BytecodeBridge.currentContext; +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.hasSuperType; +import static io.opentelemetry.javaagent.instrumentation.xxljob.common.XxlJobConstants.XXL_GLUE_JOB_HANDLER; +import static io.opentelemetry.javaagent.instrumentation.xxljob.common.XxlJobConstants.XXL_METHOD_JOB_HANDLER; +import static io.opentelemetry.javaagent.instrumentation.xxljob.common.XxlJobConstants.XXL_SCRIPT_JOB_HANDLER; +import static io.opentelemetry.javaagent.instrumentation.xxljob.v1_9_2.XxlJobSingletons.helper; +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.takesArgument; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import com.xxl.job.core.handler.IJobHandler; +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.xxljob.common.XxlJobProcessRequest; +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 SimpleJobHandlerInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return hasSuperType(named("com.xxl.job.core.handler.IJobHandler")) + .and(not(namedOneOf(XXL_GLUE_JOB_HANDLER, XXL_SCRIPT_JOB_HANDLER, XXL_METHOD_JOB_HANDLER))); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + named("execute").and(isPublic()).and(takesArguments(1).and(takesArgument(0, String.class))), + SimpleJobHandlerInstrumentation.class.getName() + "$ScheduleAdvice"); + } + + public static class ScheduleAdvice { + + @SuppressWarnings("unused") + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onSchedule( + @Advice.This IJobHandler handler, + @Advice.Local("otelRequest") XxlJobProcessRequest request, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + Context parentContext = currentContext(); + request = XxlJobProcessRequest.createSimpleJobRequest(handler); + context = helper().startSpan(parentContext, request); + if (context == null) { + return; + } + scope = context.makeCurrent(); + } + + @SuppressWarnings("unused") + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void stopSpan( + @Advice.Return(typing = Assigner.Typing.DYNAMIC) Object result, + @Advice.Thrown Throwable throwable, + @Advice.Local("otelRequest") XxlJobProcessRequest request, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + helper().stopSpan(result, request, throwable, scope, context); + } + } +} diff --git a/instrumentation/xxl-job/xxl-job-1.9.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/xxljob/v1_9_2/XxlJobInstrumentationModule.java b/instrumentation/xxl-job/xxl-job-1.9.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/xxljob/v1_9_2/XxlJobInstrumentationModule.java new file mode 100644 index 000000000000..fd72669efb73 --- /dev/null +++ b/instrumentation/xxl-job/xxl-job-1.9.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/xxljob/v1_9_2/XxlJobInstrumentationModule.java @@ -0,0 +1,38 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.xxljob.v1_9_2; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.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 java.util.List; +import net.bytebuddy.matcher.ElementMatcher; + +@AutoService(InstrumentationModule.class) +public class XxlJobInstrumentationModule extends InstrumentationModule { + + public XxlJobInstrumentationModule() { + super("xxl-job", "xxl-job-1.9.2"); + } + + @Override + public ElementMatcher.Junction classLoaderMatcher() { + // Class was added in 2.1.2 + return not(hasClassesNamed("com.xxl.job.core.handler.impl.MethodJobHandler")); + } + + @Override + public List typeInstrumentations() { + return asList( + new ScriptJobHandlerInstrumentation(), + new SimpleJobHandlerInstrumentation(), + new GlueJobHandlerInstrumentation()); + } +} diff --git a/instrumentation/xxl-job/xxl-job-1.9.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/xxljob/v1_9_2/XxlJobSingletons.java b/instrumentation/xxl-job/xxl-job-1.9.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/xxljob/v1_9_2/XxlJobSingletons.java new file mode 100644 index 000000000000..2488b109e438 --- /dev/null +++ b/instrumentation/xxl-job/xxl-job-1.9.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/xxljob/v1_9_2/XxlJobSingletons.java @@ -0,0 +1,42 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.xxljob.v1_9_2; + +import com.xxl.job.core.biz.model.ReturnT; +import com.xxl.job.core.glue.GlueTypeEnum; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import io.opentelemetry.javaagent.instrumentation.xxljob.common.XxlJobHelper; +import io.opentelemetry.javaagent.instrumentation.xxljob.common.XxlJobInstrumenterFactory; +import io.opentelemetry.javaagent.instrumentation.xxljob.common.XxlJobProcessRequest; + +public final class XxlJobSingletons { + private static final String INSTRUMENTATION_NAME = "io.opentelemetry.xxl-job-1.9.2"; + private static final Instrumenter INSTRUMENTER = + XxlJobInstrumenterFactory.create(INSTRUMENTATION_NAME); + private static final XxlJobHelper HELPER = + XxlJobHelper.create( + INSTRUMENTER, + object -> { + if (object != null && (object instanceof ReturnT)) { + ReturnT result = (ReturnT) object; + return result.getCode() == ReturnT.FAIL_CODE; + } + return false; + }); + + public static XxlJobHelper helper() { + return HELPER; + } + + @SuppressWarnings({"Unused", "ReturnValueIgnored"}) + private static void limitSupportedVersions() { + // GLUE_POWERSHELL was added in 1.9.2. Using this constant here ensures that muzzle will disable + // this instrumentation on earlier versions where this constant does not exist. + GlueTypeEnum.GLUE_POWERSHELL.name(); + } + + private XxlJobSingletons() {} +} diff --git a/instrumentation/xxl-job/xxl-job-1.9.2/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/xxljob/v1_9_2/XxlJobTest.java b/instrumentation/xxl-job/xxl-job-1.9.2/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/xxljob/v1_9_2/XxlJobTest.java new file mode 100644 index 000000000000..c09b0afb5b9b --- /dev/null +++ b/instrumentation/xxl-job/xxl-job-1.9.2/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/xxljob/v1_9_2/XxlJobTest.java @@ -0,0 +1,69 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.xxljob.v1_9_2; + +import static io.opentelemetry.instrumentation.xxljob.XxlJobTestingConstants.DEFAULT_GLUE_UPDATE_TIME; +import static io.opentelemetry.instrumentation.xxljob.XxlJobTestingConstants.GLUE_JOB_GROOVY_SOURCE_OLD; +import static io.opentelemetry.instrumentation.xxljob.XxlJobTestingConstants.GLUE_JOB_SHELL_SCRIPT; + +import com.xxl.job.core.glue.GlueFactory; +import com.xxl.job.core.glue.GlueTypeEnum; +import com.xxl.job.core.handler.IJobHandler; +import com.xxl.job.core.handler.impl.GlueJobHandler; +import com.xxl.job.core.handler.impl.ScriptJobHandler; +import io.opentelemetry.instrumentation.xxljob.AbstractXxlJobTest; +import io.opentelemetry.instrumentation.xxljob.CustomizedFailedHandler; +import io.opentelemetry.instrumentation.xxljob.SimpleCustomizedHandler; + +class XxlJobTest extends AbstractXxlJobTest { + + private static final IJobHandler GROOVY_HANDLER; + + static { + try { + GROOVY_HANDLER = GlueFactory.getInstance().loadNewInstance(GLUE_JOB_GROOVY_SOURCE_OLD); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private static final GlueJobHandler GLUE_JOB_HANDLER = + new GlueJobHandler(GROOVY_HANDLER, DEFAULT_GLUE_UPDATE_TIME); + + private static final ScriptJobHandler SCRIPT_JOB_HANDLER = + new ScriptJobHandler( + 2, DEFAULT_GLUE_UPDATE_TIME, GLUE_JOB_SHELL_SCRIPT, GlueTypeEnum.GLUE_SHELL); + + @Override + protected String getPackageName() { + return "io.opentelemetry.instrumentation.xxljob"; + } + + @Override + protected IJobHandler getGlueJobHandler() { + return GLUE_JOB_HANDLER; + } + + @Override + protected IJobHandler getScriptJobHandler() { + return SCRIPT_JOB_HANDLER; + } + + @Override + protected IJobHandler getCustomizeHandler() { + return new SimpleCustomizedHandler(); + } + + @Override + protected IJobHandler getCustomizeFailedHandler() { + return new CustomizedFailedHandler(); + } + + @Override + protected IJobHandler getMethodHandler() { + return null; + } +} diff --git a/instrumentation/xxl-job/xxl-job-2.1.2/javaagent/build.gradle.kts b/instrumentation/xxl-job/xxl-job-2.1.2/javaagent/build.gradle.kts new file mode 100644 index 000000000000..513d981448a0 --- /dev/null +++ b/instrumentation/xxl-job/xxl-job-2.1.2/javaagent/build.gradle.kts @@ -0,0 +1,34 @@ +plugins { + id("otel.javaagent-instrumentation") +} + +muzzle { + pass { + group.set("com.xuxueli") + module.set("xxl-job-core") + versions.set("[2.1.2,2.3.0)") + assertInverse.set(true) + } +} + +dependencies { + library("com.xuxueli:xxl-job-core:2.1.2") { + exclude("org.codehaus.groovy", "groovy") + } + implementation(project(":instrumentation:xxl-job:xxl-job-common:javaagent")) + + testInstrumentation(project(":instrumentation:xxl-job:xxl-job-1.9.2:javaagent")) + testInstrumentation(project(":instrumentation:xxl-job:xxl-job-2.3.0:javaagent")) + + testImplementation(project(":instrumentation:xxl-job:xxl-job-common:testing")) + latestDepTestLibrary("com.xuxueli:xxl-job-core:2.2.+") { + exclude("org.codehaus.groovy", "groovy") + } +} + +tasks.withType().configureEach { + // required on jdk17 + jvmArgs("--add-opens=java.base/java.lang=ALL-UNNAMED") + jvmArgs("-XX:+IgnoreUnrecognizedVMOptions") + jvmArgs("-Dotel.instrumentation.xxl-job.experimental-span-attributes=true") +} diff --git a/instrumentation/xxl-job/xxl-job-2.1.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/xxljob/v2_1_2/GlueJobHandlerInstrumentation.java b/instrumentation/xxl-job/xxl-job-2.1.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/xxljob/v2_1_2/GlueJobHandlerInstrumentation.java new file mode 100644 index 000000000000..2e3ddaca3ce8 --- /dev/null +++ b/instrumentation/xxl-job/xxl-job-2.1.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/xxljob/v2_1_2/GlueJobHandlerInstrumentation.java @@ -0,0 +1,65 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.xxljob.v2_1_2; + +import static io.opentelemetry.javaagent.bootstrap.Java8BytecodeBridge.currentContext; +import static io.opentelemetry.javaagent.instrumentation.xxljob.v2_1_2.XxlJobSingletons.helper; +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.named; + +import com.xxl.job.core.handler.IJobHandler; +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.xxljob.common.XxlJobProcessRequest; +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 GlueJobHandlerInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("com.xxl.job.core.handler.impl.GlueJobHandler"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + named("execute").and(isPublic()), + GlueJobHandlerInstrumentation.class.getName() + "$ScheduleAdvice"); + } + + @SuppressWarnings("unused") + public static class ScheduleAdvice { + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onSchedule( + @Advice.FieldValue("jobHandler") IJobHandler handler, + @Advice.Local("otelRequest") XxlJobProcessRequest request, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + Context parentContext = currentContext(); + request = XxlJobProcessRequest.createGlueJobRequest(handler); + context = helper().startSpan(parentContext, request); + if (context == null) { + return; + } + scope = context.makeCurrent(); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void stopSpan( + @Advice.Return(typing = Assigner.Typing.DYNAMIC) Object result, + @Advice.Thrown Throwable throwable, + @Advice.Local("otelRequest") XxlJobProcessRequest request, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + helper().stopSpan(result, request, throwable, scope, context); + } + } +} diff --git a/instrumentation/xxl-job/xxl-job-2.1.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/xxljob/v2_1_2/MethodJobHandlerInstrumentation.java b/instrumentation/xxl-job/xxl-job-2.1.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/xxljob/v2_1_2/MethodJobHandlerInstrumentation.java new file mode 100644 index 000000000000..021c3092682e --- /dev/null +++ b/instrumentation/xxl-job/xxl-job-2.1.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/xxljob/v2_1_2/MethodJobHandlerInstrumentation.java @@ -0,0 +1,66 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.xxljob.v2_1_2; + +import static io.opentelemetry.javaagent.bootstrap.Java8BytecodeBridge.currentContext; +import static io.opentelemetry.javaagent.instrumentation.xxljob.v2_1_2.XxlJobSingletons.helper; +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.xxljob.common.XxlJobProcessRequest; +import java.lang.reflect.Method; +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 MethodJobHandlerInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("com.xxl.job.core.handler.impl.MethodJobHandler"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + named("execute").and(isPublic()), + MethodJobHandlerInstrumentation.class.getName() + "$ScheduleAdvice"); + } + + @SuppressWarnings("unused") + public static class ScheduleAdvice { + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onSchedule( + @Advice.FieldValue("target") Object target, + @Advice.FieldValue("method") Method method, + @Advice.Local("otelRequest") XxlJobProcessRequest request, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + Context parentContext = currentContext(); + request = XxlJobProcessRequest.createMethodJobRequest(target, method); + context = helper().startSpan(parentContext, request); + if (context == null) { + return; + } + scope = context.makeCurrent(); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void stopSpan( + @Advice.Return(typing = Assigner.Typing.DYNAMIC) Object result, + @Advice.Thrown Throwable throwable, + @Advice.Local("otelRequest") XxlJobProcessRequest request, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + helper().stopSpan(result, request, throwable, scope, context); + } + } +} diff --git a/instrumentation/xxl-job/xxl-job-2.1.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/xxljob/v2_1_2/ScriptJobHandlerInstrumentation.java b/instrumentation/xxl-job/xxl-job-2.1.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/xxljob/v2_1_2/ScriptJobHandlerInstrumentation.java new file mode 100644 index 000000000000..8ab021d4e2df --- /dev/null +++ b/instrumentation/xxl-job/xxl-job-2.1.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/xxljob/v2_1_2/ScriptJobHandlerInstrumentation.java @@ -0,0 +1,66 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.xxljob.v2_1_2; + +import static io.opentelemetry.javaagent.bootstrap.Java8BytecodeBridge.currentContext; +import static io.opentelemetry.javaagent.instrumentation.xxljob.v2_1_2.XxlJobSingletons.helper; +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.named; + +import com.xxl.job.core.glue.GlueTypeEnum; +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.xxljob.common.XxlJobProcessRequest; +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 ScriptJobHandlerInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("com.xxl.job.core.handler.impl.ScriptJobHandler"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + named("execute").and(isPublic()), + ScriptJobHandlerInstrumentation.class.getName() + "$ScheduleAdvice"); + } + + @SuppressWarnings("unused") + public static class ScheduleAdvice { + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onSchedule( + @Advice.FieldValue("glueType") GlueTypeEnum glueType, + @Advice.FieldValue("jobId") int jobId, + @Advice.Local("otelRequest") XxlJobProcessRequest request, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + Context parentContext = currentContext(); + request = XxlJobProcessRequest.createScriptJobRequest(glueType, jobId); + context = helper().startSpan(parentContext, request); + if (context == null) { + return; + } + scope = context.makeCurrent(); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void stopSpan( + @Advice.Return(typing = Assigner.Typing.DYNAMIC) Object result, + @Advice.Thrown Throwable throwable, + @Advice.Local("otelRequest") XxlJobProcessRequest request, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + helper().stopSpan(result, request, throwable, scope, context); + } + } +} diff --git a/instrumentation/xxl-job/xxl-job-2.1.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/xxljob/v2_1_2/SimpleJobHandlerInstrumentation.java b/instrumentation/xxl-job/xxl-job-2.1.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/xxljob/v2_1_2/SimpleJobHandlerInstrumentation.java new file mode 100644 index 000000000000..132f732eb150 --- /dev/null +++ b/instrumentation/xxl-job/xxl-job-2.1.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/xxljob/v2_1_2/SimpleJobHandlerInstrumentation.java @@ -0,0 +1,74 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.xxljob.v2_1_2; + +import static io.opentelemetry.javaagent.bootstrap.Java8BytecodeBridge.currentContext; +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.hasSuperType; +import static io.opentelemetry.javaagent.instrumentation.xxljob.common.XxlJobConstants.XXL_GLUE_JOB_HANDLER; +import static io.opentelemetry.javaagent.instrumentation.xxljob.common.XxlJobConstants.XXL_METHOD_JOB_HANDLER; +import static io.opentelemetry.javaagent.instrumentation.xxljob.common.XxlJobConstants.XXL_SCRIPT_JOB_HANDLER; +import static io.opentelemetry.javaagent.instrumentation.xxljob.v2_1_2.XxlJobSingletons.helper; +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 com.xxl.job.core.handler.IJobHandler; +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.xxljob.common.XxlJobProcessRequest; +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 SimpleJobHandlerInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return hasSuperType(named("com.xxl.job.core.handler.IJobHandler")) + .and(not(namedOneOf(XXL_GLUE_JOB_HANDLER, XXL_SCRIPT_JOB_HANDLER, XXL_METHOD_JOB_HANDLER))); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + named("execute").and(isPublic()), + SimpleJobHandlerInstrumentation.class.getName() + "$ScheduleAdvice"); + } + + public static class ScheduleAdvice { + + @SuppressWarnings("unused") + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onSchedule( + @Advice.This IJobHandler handler, + @Advice.Local("otelRequest") XxlJobProcessRequest request, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + Context parentContext = currentContext(); + request = XxlJobProcessRequest.createSimpleJobRequest(handler); + context = helper().startSpan(parentContext, request); + if (context == null) { + return; + } + scope = context.makeCurrent(); + } + + @SuppressWarnings("unused") + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void stopSpan( + @Advice.Return(typing = Assigner.Typing.DYNAMIC) Object result, + @Advice.Thrown Throwable throwable, + @Advice.Local("otelRequest") XxlJobProcessRequest request, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + helper().stopSpan(result, request, throwable, scope, context); + } + } +} diff --git a/instrumentation/xxl-job/xxl-job-2.1.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/xxljob/v2_1_2/XxlJobInstrumentationModule.java b/instrumentation/xxl-job/xxl-job-2.1.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/xxljob/v2_1_2/XxlJobInstrumentationModule.java new file mode 100644 index 000000000000..f32251f71f3f --- /dev/null +++ b/instrumentation/xxl-job/xxl-job-2.1.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/xxljob/v2_1_2/XxlJobInstrumentationModule.java @@ -0,0 +1,40 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.xxljob.v2_1_2; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.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 java.util.List; +import net.bytebuddy.matcher.ElementMatcher; + +@AutoService(InstrumentationModule.class) +public class XxlJobInstrumentationModule extends InstrumentationModule { + + public XxlJobInstrumentationModule() { + super("xxl-job", "xxl-job-2.1.2"); + } + + @Override + public ElementMatcher.Junction classLoaderMatcher() { + return hasClassesNamed("com.xxl.job.core.handler.impl.MethodJobHandler") + // Class was added in 2.3.0 + .and(not(hasClassesNamed("com.xxl.job.core.context.XxlJobHelper"))); + } + + @Override + public List typeInstrumentations() { + return asList( + new MethodJobHandlerInstrumentation(), + new ScriptJobHandlerInstrumentation(), + new SimpleJobHandlerInstrumentation(), + new GlueJobHandlerInstrumentation()); + } +} diff --git a/instrumentation/xxl-job/xxl-job-2.1.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/xxljob/v2_1_2/XxlJobSingletons.java b/instrumentation/xxl-job/xxl-job-2.1.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/xxljob/v2_1_2/XxlJobSingletons.java new file mode 100644 index 000000000000..d88fa2733031 --- /dev/null +++ b/instrumentation/xxl-job/xxl-job-2.1.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/xxljob/v2_1_2/XxlJobSingletons.java @@ -0,0 +1,34 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.xxljob.v2_1_2; + +import com.xxl.job.core.biz.model.ReturnT; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import io.opentelemetry.javaagent.instrumentation.xxljob.common.XxlJobHelper; +import io.opentelemetry.javaagent.instrumentation.xxljob.common.XxlJobInstrumenterFactory; +import io.opentelemetry.javaagent.instrumentation.xxljob.common.XxlJobProcessRequest; + +public final class XxlJobSingletons { + private static final String INSTRUMENTATION_NAME = "io.opentelemetry.xxl-job-2.1.2"; + private static final Instrumenter INSTRUMENTER = + XxlJobInstrumenterFactory.create(INSTRUMENTATION_NAME); + private static final XxlJobHelper HELPER = + XxlJobHelper.create( + INSTRUMENTER, + object -> { + if (object != null && (object instanceof ReturnT)) { + ReturnT result = (ReturnT) object; + return result.getCode() == ReturnT.FAIL_CODE; + } + return false; + }); + + public static XxlJobHelper helper() { + return HELPER; + } + + private XxlJobSingletons() {} +} diff --git a/instrumentation/xxl-job/xxl-job-2.1.2/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/xxljob/v2_1_2/XxlJobTest.java b/instrumentation/xxl-job/xxl-job-2.1.2/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/xxljob/v2_1_2/XxlJobTest.java new file mode 100644 index 000000000000..27b91d2c2212 --- /dev/null +++ b/instrumentation/xxl-job/xxl-job-2.1.2/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/xxljob/v2_1_2/XxlJobTest.java @@ -0,0 +1,81 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.xxljob.v2_1_2; + +import static io.opentelemetry.instrumentation.xxljob.XxlJobTestingConstants.DEFAULT_GLUE_UPDATE_TIME; +import static io.opentelemetry.instrumentation.xxljob.XxlJobTestingConstants.GLUE_JOB_GROOVY_SOURCE_OLD; +import static io.opentelemetry.instrumentation.xxljob.XxlJobTestingConstants.GLUE_JOB_SHELL_SCRIPT; +import static io.opentelemetry.instrumentation.xxljob.XxlJobTestingConstants.METHOD_JOB_HANDLER_DESTROY_METHOD; +import static io.opentelemetry.instrumentation.xxljob.XxlJobTestingConstants.METHOD_JOB_HANDLER_INIT_METHOD; +import static io.opentelemetry.instrumentation.xxljob.XxlJobTestingConstants.METHOD_JOB_HANDLER_METHOD; +import static io.opentelemetry.instrumentation.xxljob.XxlJobTestingConstants.METHOD_JOB_HANDLER_OBJECT; + +import com.xxl.job.core.glue.GlueFactory; +import com.xxl.job.core.glue.GlueTypeEnum; +import com.xxl.job.core.handler.IJobHandler; +import com.xxl.job.core.handler.impl.GlueJobHandler; +import com.xxl.job.core.handler.impl.MethodJobHandler; +import com.xxl.job.core.handler.impl.ScriptJobHandler; +import io.opentelemetry.instrumentation.xxljob.AbstractXxlJobTest; +import io.opentelemetry.instrumentation.xxljob.CustomizedFailedHandler; +import io.opentelemetry.instrumentation.xxljob.SimpleCustomizedHandler; + +class XxlJobTest extends AbstractXxlJobTest { + + private static final MethodJobHandler METHOD_JOB_HANDLER = + new MethodJobHandler( + METHOD_JOB_HANDLER_OBJECT, + METHOD_JOB_HANDLER_METHOD, + METHOD_JOB_HANDLER_INIT_METHOD, + METHOD_JOB_HANDLER_DESTROY_METHOD); + + private static final IJobHandler GROOVY_HANDLER; + + static { + try { + GROOVY_HANDLER = GlueFactory.getInstance().loadNewInstance(GLUE_JOB_GROOVY_SOURCE_OLD); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private static final GlueJobHandler GLUE_JOB_HANDLER = + new GlueJobHandler(GROOVY_HANDLER, DEFAULT_GLUE_UPDATE_TIME); + + private static final ScriptJobHandler SCRIPT_JOB_HANDLER = + new ScriptJobHandler( + 2, DEFAULT_GLUE_UPDATE_TIME, GLUE_JOB_SHELL_SCRIPT, GlueTypeEnum.GLUE_SHELL); + + @Override + protected String getPackageName() { + return "io.opentelemetry.instrumentation.xxljob"; + } + + @Override + protected IJobHandler getGlueJobHandler() { + return GLUE_JOB_HANDLER; + } + + @Override + protected IJobHandler getScriptJobHandler() { + return SCRIPT_JOB_HANDLER; + } + + @Override + protected IJobHandler getCustomizeHandler() { + return new SimpleCustomizedHandler(); + } + + @Override + protected IJobHandler getCustomizeFailedHandler() { + return new CustomizedFailedHandler(); + } + + @Override + protected IJobHandler getMethodHandler() { + return METHOD_JOB_HANDLER; + } +} diff --git a/instrumentation/xxl-job/xxl-job-2.3.0/javaagent/build.gradle.kts b/instrumentation/xxl-job/xxl-job-2.3.0/javaagent/build.gradle.kts new file mode 100644 index 000000000000..56e5c7a630bb --- /dev/null +++ b/instrumentation/xxl-job/xxl-job-2.3.0/javaagent/build.gradle.kts @@ -0,0 +1,31 @@ +plugins { + id("otel.javaagent-instrumentation") +} + +muzzle { + pass { + group.set("com.xuxueli") + module.set("xxl-job-core") + versions.set("[2.3.0,)") + assertInverse.set(true) + } +} + +dependencies { + library("com.xuxueli:xxl-job-core:2.3.0") { + exclude("org.codehaus.groovy", "groovy") + } + implementation(project(":instrumentation:xxl-job:xxl-job-common:javaagent")) + + testInstrumentation(project(":instrumentation:xxl-job:xxl-job-2.1.2:javaagent")) + testInstrumentation(project(":instrumentation:xxl-job:xxl-job-2.3.0:javaagent")) + + testImplementation(project(":instrumentation:xxl-job:xxl-job-common:testing")) +} + +tasks.withType().configureEach { + // required on jdk17 + jvmArgs("--add-opens=java.base/java.lang=ALL-UNNAMED") + jvmArgs("-XX:+IgnoreUnrecognizedVMOptions") + jvmArgs("-Dotel.instrumentation.xxl-job.experimental-span-attributes=true") +} diff --git a/instrumentation/xxl-job/xxl-job-2.3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/xxljob/v2_3_0/GlueJobHandlerInstrumentation.java b/instrumentation/xxl-job/xxl-job-2.3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/xxljob/v2_3_0/GlueJobHandlerInstrumentation.java new file mode 100644 index 000000000000..7e67cd14e066 --- /dev/null +++ b/instrumentation/xxl-job/xxl-job-2.3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/xxljob/v2_3_0/GlueJobHandlerInstrumentation.java @@ -0,0 +1,65 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.xxljob.v2_3_0; + +import static io.opentelemetry.javaagent.bootstrap.Java8BytecodeBridge.currentContext; +import static io.opentelemetry.javaagent.instrumentation.xxljob.v2_3_0.XxlJobSingletons.helper; +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesNoArguments; + +import com.xxl.job.core.handler.IJobHandler; +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.xxljob.common.XxlJobProcessRequest; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class GlueJobHandlerInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("com.xxl.job.core.handler.impl.GlueJobHandler"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + named("execute").and(isPublic()).and(takesNoArguments()), + GlueJobHandlerInstrumentation.class.getName() + "$ScheduleAdvice"); + } + + @SuppressWarnings("unused") + public static class ScheduleAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onSchedule( + @Advice.FieldValue("jobHandler") IJobHandler handler, + @Advice.Local("otelRequest") XxlJobProcessRequest request, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + Context parentContext = currentContext(); + request = XxlJobProcessRequest.createGlueJobRequest(handler); + context = helper().startSpan(parentContext, request); + if (context == null) { + return; + } + scope = context.makeCurrent(); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void stopSpan( + @Advice.Thrown Throwable throwable, + @Advice.Local("otelRequest") XxlJobProcessRequest request, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + helper().stopSpan(request, throwable, scope, context); + } + } +} diff --git a/instrumentation/xxl-job/xxl-job-2.3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/xxljob/v2_3_0/MethodJobHandlerInstrumentation.java b/instrumentation/xxl-job/xxl-job-2.3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/xxljob/v2_3_0/MethodJobHandlerInstrumentation.java new file mode 100644 index 000000000000..04d39252acaa --- /dev/null +++ b/instrumentation/xxl-job/xxl-job-2.3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/xxljob/v2_3_0/MethodJobHandlerInstrumentation.java @@ -0,0 +1,65 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.xxljob.v2_3_0; + +import static io.opentelemetry.javaagent.bootstrap.Java8BytecodeBridge.currentContext; +import static io.opentelemetry.javaagent.instrumentation.xxljob.v2_3_0.XxlJobSingletons.helper; +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesNoArguments; + +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.xxljob.common.XxlJobProcessRequest; +import java.lang.reflect.Method; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class MethodJobHandlerInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("com.xxl.job.core.handler.impl.MethodJobHandler"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + named("execute").and(isPublic()).and(takesNoArguments()), + MethodJobHandlerInstrumentation.class.getName() + "$ScheduleAdvice"); + } + + @SuppressWarnings("unused") + public static class ScheduleAdvice { + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onSchedule( + @Advice.FieldValue("target") Object target, + @Advice.FieldValue("method") Method method, + @Advice.Local("otelRequest") XxlJobProcessRequest request, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + Context parentContext = currentContext(); + request = XxlJobProcessRequest.createMethodJobRequest(target, method); + context = helper().startSpan(parentContext, request); + if (context == null) { + return; + } + scope = context.makeCurrent(); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void stopSpan( + @Advice.Thrown Throwable throwable, + @Advice.Local("otelRequest") XxlJobProcessRequest request, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + helper().stopSpan(request, throwable, scope, context); + } + } +} diff --git a/instrumentation/xxl-job/xxl-job-2.3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/xxljob/v2_3_0/ScriptJobHandlerInstrumentation.java b/instrumentation/xxl-job/xxl-job-2.3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/xxljob/v2_3_0/ScriptJobHandlerInstrumentation.java new file mode 100644 index 000000000000..bf5fa3fa793a --- /dev/null +++ b/instrumentation/xxl-job/xxl-job-2.3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/xxljob/v2_3_0/ScriptJobHandlerInstrumentation.java @@ -0,0 +1,66 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.xxljob.v2_3_0; + +import static io.opentelemetry.javaagent.bootstrap.Java8BytecodeBridge.currentContext; +import static io.opentelemetry.javaagent.instrumentation.xxljob.v2_3_0.XxlJobSingletons.helper; +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesNoArguments; + +import com.xxl.job.core.glue.GlueTypeEnum; +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.xxljob.common.XxlJobProcessRequest; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class ScriptJobHandlerInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("com.xxl.job.core.handler.impl.ScriptJobHandler"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + named("execute").and(isPublic()).and(takesNoArguments()), + ScriptJobHandlerInstrumentation.class.getName() + "$ScheduleAdvice"); + } + + @SuppressWarnings("unused") + public static class ScheduleAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onSchedule( + @Advice.FieldValue("glueType") GlueTypeEnum glueType, + @Advice.FieldValue("jobId") int jobId, + @Advice.Local("otelRequest") XxlJobProcessRequest request, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + Context parentContext = currentContext(); + request = XxlJobProcessRequest.createScriptJobRequest(glueType, jobId); + context = helper().startSpan(parentContext, request); + if (context == null) { + return; + } + scope = context.makeCurrent(); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void stopSpan( + @Advice.Thrown Throwable throwable, + @Advice.Local("otelRequest") XxlJobProcessRequest request, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + helper().stopSpan(request, throwable, scope, context); + } + } +} diff --git a/instrumentation/xxl-job/xxl-job-2.3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/xxljob/v2_3_0/SimpleJobHandlerInstrumentation.java b/instrumentation/xxl-job/xxl-job-2.3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/xxljob/v2_3_0/SimpleJobHandlerInstrumentation.java new file mode 100644 index 000000000000..896da6b7136e --- /dev/null +++ b/instrumentation/xxl-job/xxl-job-2.3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/xxljob/v2_3_0/SimpleJobHandlerInstrumentation.java @@ -0,0 +1,73 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.xxljob.v2_3_0; + +import static io.opentelemetry.javaagent.bootstrap.Java8BytecodeBridge.currentContext; +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.hasSuperType; +import static io.opentelemetry.javaagent.instrumentation.xxljob.common.XxlJobConstants.XXL_GLUE_JOB_HANDLER; +import static io.opentelemetry.javaagent.instrumentation.xxljob.common.XxlJobConstants.XXL_METHOD_JOB_HANDLER; +import static io.opentelemetry.javaagent.instrumentation.xxljob.common.XxlJobConstants.XXL_SCRIPT_JOB_HANDLER; +import static io.opentelemetry.javaagent.instrumentation.xxljob.v2_3_0.XxlJobSingletons.helper; +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.takesNoArguments; + +import com.xxl.job.core.handler.IJobHandler; +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.xxljob.common.XxlJobProcessRequest; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class SimpleJobHandlerInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return hasSuperType(named("com.xxl.job.core.handler.IJobHandler")) + .and(not(namedOneOf(XXL_GLUE_JOB_HANDLER, XXL_SCRIPT_JOB_HANDLER, XXL_METHOD_JOB_HANDLER))); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + named("execute").and(isPublic()).and(takesNoArguments()), + SimpleJobHandlerInstrumentation.class.getName() + "$ScheduleAdvice"); + } + + public static class ScheduleAdvice { + + @SuppressWarnings("unused") + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onSchedule( + @Advice.This IJobHandler handler, + @Advice.Local("otelRequest") XxlJobProcessRequest request, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + Context parentContext = currentContext(); + request = XxlJobProcessRequest.createSimpleJobRequest(handler); + context = helper().startSpan(parentContext, request); + if (context == null) { + return; + } + scope = context.makeCurrent(); + } + + @SuppressWarnings("unused") + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void stopSpan( + @Advice.Thrown Throwable throwable, + @Advice.Local("otelRequest") XxlJobProcessRequest request, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + helper().stopSpan(request, throwable, scope, context); + } + } +} diff --git a/instrumentation/xxl-job/xxl-job-2.3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/xxljob/v2_3_0/XxlJobInstrumentationModule.java b/instrumentation/xxl-job/xxl-job-2.3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/xxljob/v2_3_0/XxlJobInstrumentationModule.java new file mode 100644 index 000000000000..3b17798a47ea --- /dev/null +++ b/instrumentation/xxl-job/xxl-job-2.3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/xxljob/v2_3_0/XxlJobInstrumentationModule.java @@ -0,0 +1,38 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.xxljob.v2_3_0; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.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 XxlJobInstrumentationModule extends InstrumentationModule { + + public XxlJobInstrumentationModule() { + super("xxl-job", "xxl-job-2.3.0"); + } + + @Override + public ElementMatcher.Junction classLoaderMatcher() { + return hasClassesNamed( + "com.xxl.job.core.handler.impl.MethodJobHandler", "com.xxl.job.core.context.XxlJobHelper"); + } + + @Override + public List typeInstrumentations() { + return asList( + new MethodJobHandlerInstrumentation(), + new ScriptJobHandlerInstrumentation(), + new SimpleJobHandlerInstrumentation(), + new GlueJobHandlerInstrumentation()); + } +} diff --git a/instrumentation/xxl-job/xxl-job-2.3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/xxljob/v2_3_0/XxlJobSingletons.java b/instrumentation/xxl-job/xxl-job-2.3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/xxljob/v2_3_0/XxlJobSingletons.java new file mode 100644 index 000000000000..884e0cd4f37d --- /dev/null +++ b/instrumentation/xxl-job/xxl-job-2.3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/xxljob/v2_3_0/XxlJobSingletons.java @@ -0,0 +1,38 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.xxljob.v2_3_0; + +import static com.xxl.job.core.context.XxlJobContext.HANDLE_COCE_SUCCESS; + +import com.xxl.job.core.context.XxlJobContext; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import io.opentelemetry.javaagent.instrumentation.xxljob.common.XxlJobHelper; +import io.opentelemetry.javaagent.instrumentation.xxljob.common.XxlJobInstrumenterFactory; +import io.opentelemetry.javaagent.instrumentation.xxljob.common.XxlJobProcessRequest; + +public final class XxlJobSingletons { + private static final String INSTRUMENTATION_NAME = "io.opentelemetry.xxl-job-2.3.0"; + private static final Instrumenter INSTRUMENTER = + XxlJobInstrumenterFactory.create(INSTRUMENTATION_NAME); + private static final XxlJobHelper HELPER = + XxlJobHelper.create( + INSTRUMENTER, + unused -> { + // From 2.3.0, XxlJobContext is used to store the result of the job execution. + XxlJobContext xxlJobContext = XxlJobContext.getXxlJobContext(); + if (xxlJobContext != null) { + int handleCode = xxlJobContext.getHandleCode(); + return handleCode != HANDLE_COCE_SUCCESS; + } + return false; + }); + + public static XxlJobHelper helper() { + return HELPER; + } + + private XxlJobSingletons() {} +} diff --git a/instrumentation/xxl-job/xxl-job-2.3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/xxljob/v2_3_0/CustomizedFailedHandler.java b/instrumentation/xxl-job/xxl-job-2.3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/xxljob/v2_3_0/CustomizedFailedHandler.java new file mode 100644 index 000000000000..55d083d176e8 --- /dev/null +++ b/instrumentation/xxl-job/xxl-job-2.3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/xxljob/v2_3_0/CustomizedFailedHandler.java @@ -0,0 +1,17 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.xxljob.v2_3_0; + +import com.xxl.job.core.context.XxlJobHelper; +import com.xxl.job.core.handler.IJobHandler; + +class CustomizedFailedHandler extends IJobHandler { + + @Override + public void execute() throws Exception { + XxlJobHelper.handleFail(); + } +} diff --git a/instrumentation/xxl-job/xxl-job-2.3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/xxljob/v2_3_0/SimpleCustomizedHandler.java b/instrumentation/xxl-job/xxl-job-2.3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/xxljob/v2_3_0/SimpleCustomizedHandler.java new file mode 100644 index 000000000000..861d5c23e9e1 --- /dev/null +++ b/instrumentation/xxl-job/xxl-job-2.3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/xxljob/v2_3_0/SimpleCustomizedHandler.java @@ -0,0 +1,17 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.xxljob.v2_3_0; + +import com.xxl.job.core.context.XxlJobHelper; +import com.xxl.job.core.handler.IJobHandler; + +class SimpleCustomizedHandler extends IJobHandler { + + @Override + public void execute() throws Exception { + XxlJobHelper.handleSuccess(); + } +} diff --git a/instrumentation/xxl-job/xxl-job-2.3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/xxljob/v2_3_0/XxlJobTest.java b/instrumentation/xxl-job/xxl-job-2.3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/xxljob/v2_3_0/XxlJobTest.java new file mode 100644 index 000000000000..ec4c3b0fb4d4 --- /dev/null +++ b/instrumentation/xxl-job/xxl-job-2.3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/xxljob/v2_3_0/XxlJobTest.java @@ -0,0 +1,79 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.xxljob.v2_3_0; + +import static io.opentelemetry.instrumentation.xxljob.XxlJobTestingConstants.DEFAULT_GLUE_UPDATE_TIME; +import static io.opentelemetry.instrumentation.xxljob.XxlJobTestingConstants.GLUE_JOB_GROOVY_SOURCE; +import static io.opentelemetry.instrumentation.xxljob.XxlJobTestingConstants.GLUE_JOB_SHELL_SCRIPT; +import static io.opentelemetry.instrumentation.xxljob.XxlJobTestingConstants.METHOD_JOB_HANDLER_DESTROY_METHOD; +import static io.opentelemetry.instrumentation.xxljob.XxlJobTestingConstants.METHOD_JOB_HANDLER_INIT_METHOD; +import static io.opentelemetry.instrumentation.xxljob.XxlJobTestingConstants.METHOD_JOB_HANDLER_METHOD; +import static io.opentelemetry.instrumentation.xxljob.XxlJobTestingConstants.METHOD_JOB_HANDLER_OBJECT; + +import com.xxl.job.core.glue.GlueFactory; +import com.xxl.job.core.glue.GlueTypeEnum; +import com.xxl.job.core.handler.IJobHandler; +import com.xxl.job.core.handler.impl.GlueJobHandler; +import com.xxl.job.core.handler.impl.MethodJobHandler; +import com.xxl.job.core.handler.impl.ScriptJobHandler; +import io.opentelemetry.instrumentation.xxljob.AbstractXxlJobTest; + +class XxlJobTest extends AbstractXxlJobTest { + + private static final MethodJobHandler METHOD_JOB_HANDLER = + new MethodJobHandler( + METHOD_JOB_HANDLER_OBJECT, + METHOD_JOB_HANDLER_METHOD, + METHOD_JOB_HANDLER_INIT_METHOD, + METHOD_JOB_HANDLER_DESTROY_METHOD); + + private static final IJobHandler GROOVY_HANDLER; + + static { + try { + GROOVY_HANDLER = GlueFactory.getInstance().loadNewInstance(GLUE_JOB_GROOVY_SOURCE); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private static final GlueJobHandler GLUE_JOB_HANDLER = + new GlueJobHandler(GROOVY_HANDLER, DEFAULT_GLUE_UPDATE_TIME); + + private static final ScriptJobHandler SCRIPT_JOB_HANDLER = + new ScriptJobHandler( + 2, DEFAULT_GLUE_UPDATE_TIME, GLUE_JOB_SHELL_SCRIPT, GlueTypeEnum.GLUE_SHELL); + + @Override + protected String getPackageName() { + return "io.opentelemetry.javaagent.instrumentation.xxljob.v2_3_0"; + } + + @Override + protected IJobHandler getGlueJobHandler() { + return GLUE_JOB_HANDLER; + } + + @Override + protected IJobHandler getScriptJobHandler() { + return SCRIPT_JOB_HANDLER; + } + + @Override + protected IJobHandler getCustomizeHandler() { + return new SimpleCustomizedHandler(); + } + + @Override + protected IJobHandler getCustomizeFailedHandler() { + return new CustomizedFailedHandler(); + } + + @Override + protected IJobHandler getMethodHandler() { + return METHOD_JOB_HANDLER; + } +} diff --git a/instrumentation/xxl-job/xxl-job-common/javaagent/build.gradle.kts b/instrumentation/xxl-job/xxl-job-common/javaagent/build.gradle.kts new file mode 100644 index 000000000000..ebc87b1bd078 --- /dev/null +++ b/instrumentation/xxl-job/xxl-job-common/javaagent/build.gradle.kts @@ -0,0 +1,6 @@ +plugins { + id("otel.javaagent-instrumentation") +} +dependencies { + compileOnly("com.xuxueli:xxl-job-core:2.1.2") +} diff --git a/instrumentation/xxl-job/xxl-job-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/xxljob/common/XxlJobCodeAttributesGetter.java b/instrumentation/xxl-job/xxl-job-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/xxljob/common/XxlJobCodeAttributesGetter.java new file mode 100644 index 000000000000..1db15ab916be --- /dev/null +++ b/instrumentation/xxl-job/xxl-job-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/xxljob/common/XxlJobCodeAttributesGetter.java @@ -0,0 +1,33 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.xxljob.common; + +import com.xxl.job.core.glue.GlueTypeEnum; +import io.opentelemetry.instrumentation.api.incubator.semconv.code.CodeAttributesGetter; +import javax.annotation.Nullable; + +class XxlJobCodeAttributesGetter implements CodeAttributesGetter { + + @Nullable + @Override + public Class getCodeClass(XxlJobProcessRequest xxlJobProcessRequest) { + GlueTypeEnum glueType = xxlJobProcessRequest.getGlueType(); + if (!glueType.isScript()) { + return xxlJobProcessRequest.getDeclaringClass(); + } + return null; + } + + @Nullable + @Override + public String getMethodName(XxlJobProcessRequest xxlJobProcessRequest) { + GlueTypeEnum glueType = xxlJobProcessRequest.getGlueType(); + if (!glueType.isScript()) { + return xxlJobProcessRequest.getMethodName(); + } + return null; + } +} diff --git a/instrumentation/xxl-job/xxl-job-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/xxljob/common/XxlJobConstants.java b/instrumentation/xxl-job/xxl-job-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/xxljob/common/XxlJobConstants.java new file mode 100644 index 000000000000..f31ac773303d --- /dev/null +++ b/instrumentation/xxl-job/xxl-job-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/xxljob/common/XxlJobConstants.java @@ -0,0 +1,17 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.xxljob.common; + +public final class XxlJobConstants { + + private XxlJobConstants() {} + + public static final String XXL_GLUE_JOB_HANDLER = "com.xxl.job.core.handler.impl.GlueJobHandler"; + public static final String XXL_SCRIPT_JOB_HANDLER = + "com.xxl.job.core.handler.impl.ScriptJobHandler"; + public static final String XXL_METHOD_JOB_HANDLER = + "com.xxl.job.core.handler.impl.MethodJobHandler"; +} diff --git a/instrumentation/xxl-job/xxl-job-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/xxljob/common/XxlJobExperimentalAttributeExtractor.java b/instrumentation/xxl-job/xxl-job-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/xxljob/common/XxlJobExperimentalAttributeExtractor.java new file mode 100644 index 000000000000..a9bceb6a2b43 --- /dev/null +++ b/instrumentation/xxl-job/xxl-job-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/xxljob/common/XxlJobExperimentalAttributeExtractor.java @@ -0,0 +1,44 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.xxljob.common; + +import com.xxl.job.core.glue.GlueTypeEnum; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; +import javax.annotation.Nullable; + +class XxlJobExperimentalAttributeExtractor + implements AttributesExtractor { + + private static final AttributeKey XXL_JOB_GLUE_TYPE = + AttributeKey.stringKey("scheduling.xxl-job.glue.type"); + + private static final AttributeKey XXL_JOB_JOB_ID = + AttributeKey.longKey("scheduling.xxl-job.job.id"); + + @Override + public void onStart( + AttributesBuilder attributes, + Context parentContext, + XxlJobProcessRequest xxlJobProcessRequest) { + GlueTypeEnum glueType = xxlJobProcessRequest.getGlueType(); + attributes.put(XXL_JOB_GLUE_TYPE, glueType.getDesc()); + // store jobId in experimental attribute for script job. + if (glueType.isScript()) { + attributes.put(XXL_JOB_JOB_ID, xxlJobProcessRequest.getJobId()); + } + } + + @Override + public void onEnd( + AttributesBuilder attributes, + Context context, + XxlJobProcessRequest xxlJobProcessRequest, + @Nullable Void unused, + @Nullable Throwable error) {} +} diff --git a/instrumentation/xxl-job/xxl-job-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/xxljob/common/XxlJobHelper.java b/instrumentation/xxl-job/xxl-job-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/xxljob/common/XxlJobHelper.java new file mode 100644 index 000000000000..3b90e1d3ad44 --- /dev/null +++ b/instrumentation/xxl-job/xxl-job-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/xxljob/common/XxlJobHelper.java @@ -0,0 +1,57 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.xxljob.common; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import java.util.function.Predicate; + +public final class XxlJobHelper { + private final Instrumenter instrumenter; + private final Predicate failedStatusPredicate; + + private XxlJobHelper( + Instrumenter instrumenter, + Predicate failedStatusPredicate) { + this.instrumenter = instrumenter; + this.failedStatusPredicate = failedStatusPredicate; + } + + public static XxlJobHelper create( + Instrumenter instrumenter, + Predicate failedStatusPredicate) { + return new XxlJobHelper(instrumenter, failedStatusPredicate); + } + + public Context startSpan(Context parentContext, XxlJobProcessRequest request) { + if (!instrumenter.shouldStart(parentContext, request)) { + return null; + } + return instrumenter.start(parentContext, request); + } + + public void stopSpan( + Object result, + XxlJobProcessRequest request, + Throwable throwable, + Scope scope, + Context context) { + if (scope == null) { + return; + } + if (failedStatusPredicate.test(result)) { + request.setFailed(); + } + scope.close(); + instrumenter.end(context, request, null, throwable); + } + + public void stopSpan( + XxlJobProcessRequest request, Throwable throwable, Scope scope, Context context) { + stopSpan(null, request, throwable, scope, context); + } +} diff --git a/instrumentation/xxl-job/xxl-job-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/xxljob/common/XxlJobInstrumenterFactory.java b/instrumentation/xxl-job/xxl-job-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/xxljob/common/XxlJobInstrumenterFactory.java new file mode 100644 index 000000000000..1544504bdac6 --- /dev/null +++ b/instrumentation/xxl-job/xxl-job-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/xxljob/common/XxlJobInstrumenterFactory.java @@ -0,0 +1,45 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.xxljob.common; + +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.trace.StatusCode; +import io.opentelemetry.instrumentation.api.incubator.semconv.code.CodeAttributesExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import io.opentelemetry.instrumentation.api.instrumenter.InstrumenterBuilder; +import io.opentelemetry.javaagent.bootstrap.internal.AgentInstrumentationConfig; + +public final class XxlJobInstrumenterFactory { + + private static final boolean CAPTURE_EXPERIMENTAL_SPAN_ATTRIBUTES = + AgentInstrumentationConfig.get() + .getBoolean("otel.instrumentation.xxl-job.experimental-span-attributes", false); + + public static Instrumenter create(String instrumentationName) { + XxlJobCodeAttributesGetter codeAttributesGetter = new XxlJobCodeAttributesGetter(); + XxlJobSpanNameExtractor spanNameExtractor = new XxlJobSpanNameExtractor(codeAttributesGetter); + InstrumenterBuilder builder = + Instrumenter.builder( + GlobalOpenTelemetry.get(), instrumentationName, spanNameExtractor) + .addAttributesExtractor(CodeAttributesExtractor.create(codeAttributesGetter)) + .setSpanStatusExtractor( + (spanStatusBuilder, xxlJobProcessRequest, response, error) -> { + if (error != null || xxlJobProcessRequest.isFailed()) { + spanStatusBuilder.setStatus(StatusCode.ERROR); + } + }); + if (CAPTURE_EXPERIMENTAL_SPAN_ATTRIBUTES) { + builder.addAttributesExtractor( + AttributesExtractor.constant(AttributeKey.stringKey("job.system"), "xxl-job")); + builder.addAttributesExtractor(new XxlJobExperimentalAttributeExtractor()); + } + return builder.buildInstrumenter(); + } + + private XxlJobInstrumenterFactory() {} +} diff --git a/instrumentation/xxl-job/xxl-job-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/xxljob/common/XxlJobProcessRequest.java b/instrumentation/xxl-job/xxl-job-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/xxljob/common/XxlJobProcessRequest.java new file mode 100644 index 000000000000..2bd2f62c661c --- /dev/null +++ b/instrumentation/xxl-job/xxl-job-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/xxljob/common/XxlJobProcessRequest.java @@ -0,0 +1,76 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.xxljob.common; + +import com.xxl.job.core.glue.GlueTypeEnum; +import com.xxl.job.core.handler.IJobHandler; +import java.lang.reflect.Method; + +public final class XxlJobProcessRequest { + + private String methodName; + private int jobId; + private Class declaringClass; + private boolean failed; + private final GlueTypeEnum glueType; + + private XxlJobProcessRequest(GlueTypeEnum glueType) { + this.glueType = glueType; + } + + public static XxlJobProcessRequest createRequestForMethod( + GlueTypeEnum glueType, Class declaringClass, String methodName) { + XxlJobProcessRequest request = new XxlJobProcessRequest(glueType); + request.declaringClass = declaringClass; + request.methodName = methodName; + + return request; + } + + public static XxlJobProcessRequest createGlueJobRequest(IJobHandler handler) { + return createRequestForMethod(GlueTypeEnum.GLUE_GROOVY, handler.getClass(), "execute"); + } + + public static XxlJobProcessRequest createScriptJobRequest(GlueTypeEnum glueType, int jobId) { + XxlJobProcessRequest request = new XxlJobProcessRequest(glueType); + request.jobId = jobId; + + return request; + } + + public static XxlJobProcessRequest createSimpleJobRequest(IJobHandler handler) { + return createRequestForMethod(GlueTypeEnum.BEAN, handler.getClass(), "execute"); + } + + public static XxlJobProcessRequest createMethodJobRequest(Object target, Method method) { + return createRequestForMethod( + GlueTypeEnum.BEAN, target.getClass(), method != null ? method.getName() : null); + } + + public void setFailed() { + failed = true; + } + + public boolean isFailed() { + return failed; + } + + public String getMethodName() { + return methodName; + } + + public int getJobId() { + return jobId; + } + + public Class getDeclaringClass() { + return declaringClass; + } + + public GlueTypeEnum getGlueType() { + return glueType; + } +} diff --git a/instrumentation/xxl-job/xxl-job-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/xxljob/common/XxlJobSpanNameExtractor.java b/instrumentation/xxl-job/xxl-job-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/xxljob/common/XxlJobSpanNameExtractor.java new file mode 100644 index 000000000000..4c1e053feb00 --- /dev/null +++ b/instrumentation/xxl-job/xxl-job-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/xxljob/common/XxlJobSpanNameExtractor.java @@ -0,0 +1,31 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.xxljob.common; + +import com.xxl.job.core.glue.GlueTypeEnum; +import io.opentelemetry.instrumentation.api.incubator.semconv.code.CodeAttributesGetter; +import io.opentelemetry.instrumentation.api.incubator.semconv.code.CodeSpanNameExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor; + +class XxlJobSpanNameExtractor implements SpanNameExtractor { + private final SpanNameExtractor codeSpanNameExtractor; + + XxlJobSpanNameExtractor(CodeAttributesGetter getter) { + codeSpanNameExtractor = CodeSpanNameExtractor.create(getter); + } + + @Override + public String extract(XxlJobProcessRequest request) { + GlueTypeEnum glueType = request.getGlueType(); + if (glueType.isScript()) { + // TODO: need to discuss a better span name for script job in the future. + // for detail can refer to + // https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/10421#discussion_r1511532584 + return glueType.getDesc(); + } + return codeSpanNameExtractor.extract(request); + } +} diff --git a/instrumentation/xxl-job/xxl-job-common/testing/build.gradle.kts b/instrumentation/xxl-job/xxl-job-common/testing/build.gradle.kts new file mode 100644 index 000000000000..0eb69a252ddf --- /dev/null +++ b/instrumentation/xxl-job/xxl-job-common/testing/build.gradle.kts @@ -0,0 +1,11 @@ +plugins { + id("otel.java-conventions") +} + +dependencies { + implementation(project(":testing-common")) + + compileOnly("com.xuxueli:xxl-job-core:2.1.2") { + exclude("org.codehaus.groovy", "groovy") + } +} diff --git a/instrumentation/xxl-job/xxl-job-common/testing/src/main/java/io/opentelemetry/instrumentation/xxljob/AbstractXxlJobTest.java b/instrumentation/xxl-job/xxl-job-common/testing/src/main/java/io/opentelemetry/instrumentation/xxljob/AbstractXxlJobTest.java new file mode 100644 index 000000000000..c8f7766e58e2 --- /dev/null +++ b/instrumentation/xxl-job/xxl-job-common/testing/src/main/java/io/opentelemetry/instrumentation/xxljob/AbstractXxlJobTest.java @@ -0,0 +1,170 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.xxljob; + +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; +import static java.util.Arrays.asList; + +import com.xxl.job.core.biz.model.TriggerParam; +import com.xxl.job.core.glue.GlueTypeEnum; +import com.xxl.job.core.handler.IJobHandler; +import com.xxl.job.core.log.XxlJobFileAppender; +import com.xxl.job.core.thread.JobThread; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.sdk.testing.assertj.AttributeAssertion; +import io.opentelemetry.sdk.trace.data.StatusData; +import java.util.ArrayList; +import java.util.List; +import org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +public abstract class AbstractXxlJobTest { + + @RegisterExtension + private static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + @BeforeAll + static void setUp() { + XxlJobFileAppender.initLogPath("build/xxljob/log"); + } + + @Test + void testGlueJob() { + JobThread jobThread = new JobThread(1, getGlueJobHandler()); + TriggerParam triggerParam = new TriggerParam(); + triggerParam.setExecutorTimeout(0); + jobThread.pushTriggerQueue(triggerParam); + jobThread.start(); + checkXxlJob( + "CustomizedGroovyHandler.execute", + StatusData.unset(), + GlueTypeEnum.GLUE_GROOVY, + "CustomizedGroovyHandler", + "execute"); + jobThread.toStop("Test finish"); + } + + @Test + void testScriptJob() { + JobThread jobThread = new JobThread(2, getScriptJobHandler()); + TriggerParam triggerParam = new TriggerParam(); + triggerParam.setExecutorParams(""); + triggerParam.setExecutorTimeout(0); + jobThread.pushTriggerQueue(triggerParam); + jobThread.start(); + checkXxlJobWithoutCodeAttributes("GLUE(Shell)", StatusData.unset(), GlueTypeEnum.GLUE_SHELL, 2); + jobThread.toStop("Test finish"); + } + + @Test + void testSimpleJob() { + JobThread jobThread = new JobThread(3, getCustomizeHandler()); + TriggerParam triggerParam = new TriggerParam(); + triggerParam.setExecutorTimeout(0); + jobThread.pushTriggerQueue(triggerParam); + jobThread.start(); + checkXxlJob( + "SimpleCustomizedHandler.execute", + StatusData.unset(), + GlueTypeEnum.BEAN, + getPackageName() + ".SimpleCustomizedHandler", + "execute"); + jobThread.toStop("Test finish"); + } + + @Test + public void testMethodJob() { + // method handle is null if test is not supported by tested version of the library + Assumptions.assumeTrue(getMethodHandler() != null); + + JobThread jobThread = new JobThread(4, getMethodHandler()); + TriggerParam triggerParam = new TriggerParam(); + triggerParam.setExecutorTimeout(0); + jobThread.pushTriggerQueue(triggerParam); + jobThread.start(); + checkXxlJob( + "ReflectObject.echo", + StatusData.unset(), + GlueTypeEnum.BEAN, + "io.opentelemetry.instrumentation.xxljob.ReflectiveMethodsFactory$ReflectObject", + "echo"); + jobThread.toStop("Test finish"); + } + + @Test + void testFailedJob() { + JobThread jobThread = new JobThread(5, getCustomizeFailedHandler()); + TriggerParam triggerParam = new TriggerParam(); + triggerParam.setExecutorTimeout(0); + jobThread.pushTriggerQueue(triggerParam); + jobThread.start(); + checkXxlJob( + "CustomizedFailedHandler.execute", + StatusData.error(), + GlueTypeEnum.BEAN, + getPackageName() + ".CustomizedFailedHandler", + "execute"); + jobThread.toStop("Test finish"); + } + + protected abstract IJobHandler getGlueJobHandler(); + + protected abstract IJobHandler getScriptJobHandler(); + + protected abstract IJobHandler getCustomizeHandler(); + + protected abstract IJobHandler getCustomizeFailedHandler(); + + protected abstract IJobHandler getMethodHandler(); + + protected abstract String getPackageName(); + + private static void checkXxlJob( + String spanName, StatusData statusData, List assertions) { + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasKind(SpanKind.INTERNAL) + .hasName(spanName) + .hasStatus(statusData) + .hasAttributesSatisfyingExactly(assertions))); + } + + private static void checkXxlJob( + String spanName, + StatusData statusData, + GlueTypeEnum glueType, + String codeNamespace, + String codeFunction) { + List attributeAssertions = new ArrayList<>(); + attributeAssertions.addAll(attributeAssertions(glueType)); + attributeAssertions.add(equalTo(AttributeKey.stringKey("code.namespace"), codeNamespace)); + attributeAssertions.add(equalTo(AttributeKey.stringKey("code.function"), codeFunction)); + + checkXxlJob(spanName, statusData, attributeAssertions); + } + + private static void checkXxlJobWithoutCodeAttributes( + String spanName, StatusData statusData, GlueTypeEnum glueType, int jobId) { + List attributeAssertions = new ArrayList<>(); + attributeAssertions.addAll(attributeAssertions(glueType)); + attributeAssertions.add(equalTo(AttributeKey.longKey("scheduling.xxl-job.job.id"), jobId)); + + checkXxlJob(spanName, statusData, attributeAssertions); + } + + private static List attributeAssertions(GlueTypeEnum glueType) { + return asList( + equalTo(AttributeKey.stringKey("job.system"), "xxl-job"), + equalTo(AttributeKey.stringKey("scheduling.xxl-job.glue.type"), glueType.getDesc())); + } +} diff --git a/instrumentation/xxl-job/xxl-job-common/testing/src/main/java/io/opentelemetry/instrumentation/xxljob/CustomizedFailedHandler.java b/instrumentation/xxl-job/xxl-job-common/testing/src/main/java/io/opentelemetry/instrumentation/xxljob/CustomizedFailedHandler.java new file mode 100644 index 000000000000..a4573ecf69a1 --- /dev/null +++ b/instrumentation/xxl-job/xxl-job-common/testing/src/main/java/io/opentelemetry/instrumentation/xxljob/CustomizedFailedHandler.java @@ -0,0 +1,17 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.xxljob; + +import com.xxl.job.core.biz.model.ReturnT; +import com.xxl.job.core.handler.IJobHandler; + +public class CustomizedFailedHandler extends IJobHandler { + + @Override + public ReturnT execute(String s) throws Exception { + return FAIL; + } +} diff --git a/instrumentation/xxl-job/xxl-job-common/testing/src/main/java/io/opentelemetry/instrumentation/xxljob/ReflectiveMethodsFactory.java b/instrumentation/xxl-job/xxl-job-common/testing/src/main/java/io/opentelemetry/instrumentation/xxljob/ReflectiveMethodsFactory.java new file mode 100644 index 000000000000..fd772a4611d3 --- /dev/null +++ b/instrumentation/xxl-job/xxl-job-common/testing/src/main/java/io/opentelemetry/instrumentation/xxljob/ReflectiveMethodsFactory.java @@ -0,0 +1,60 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.xxljob; + +import com.xxl.job.core.biz.model.ReturnT; +import java.lang.reflect.Method; + +class ReflectiveMethodsFactory { + + private ReflectiveMethodsFactory() {} + + public static class ReflectObject { + + private ReflectObject() {} + + public void initMethod() {} + + public void destroyMethod() {} + + public ReturnT echo(String param) { + return new ReturnT<>("echo: " + param); + } + } + + private static final Object SINGLETON_OBJECT = new ReflectObject(); + + static Object getTarget() { + return SINGLETON_OBJECT; + } + + static Method getMethod() { + try { + return SINGLETON_OBJECT.getClass().getMethod("echo", String.class); + } catch (Throwable t) { + // Ignore + } + return null; + } + + static Method getInitMethod() { + try { + return SINGLETON_OBJECT.getClass().getMethod("initMethod"); + } catch (Throwable t) { + // Ignore + } + return null; + } + + static Method getDestroyMethod() { + try { + return SINGLETON_OBJECT.getClass().getMethod("destroyMethod"); + } catch (Throwable t) { + // Ignore + } + return null; + } +} diff --git a/instrumentation/xxl-job/xxl-job-common/testing/src/main/java/io/opentelemetry/instrumentation/xxljob/SimpleCustomizedHandler.java b/instrumentation/xxl-job/xxl-job-common/testing/src/main/java/io/opentelemetry/instrumentation/xxljob/SimpleCustomizedHandler.java new file mode 100644 index 000000000000..78df288fc852 --- /dev/null +++ b/instrumentation/xxl-job/xxl-job-common/testing/src/main/java/io/opentelemetry/instrumentation/xxljob/SimpleCustomizedHandler.java @@ -0,0 +1,17 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.xxljob; + +import com.xxl.job.core.biz.model.ReturnT; +import com.xxl.job.core.handler.IJobHandler; + +public class SimpleCustomizedHandler extends IJobHandler { + + @Override + public ReturnT execute(String s) throws Exception { + return new ReturnT<>("Hello World"); + } +} diff --git a/instrumentation/xxl-job/xxl-job-common/testing/src/main/java/io/opentelemetry/instrumentation/xxljob/XxlJobTestingConstants.java b/instrumentation/xxl-job/xxl-job-common/testing/src/main/java/io/opentelemetry/instrumentation/xxljob/XxlJobTestingConstants.java new file mode 100644 index 000000000000..81b4e8faca49 --- /dev/null +++ b/instrumentation/xxl-job/xxl-job-common/testing/src/main/java/io/opentelemetry/instrumentation/xxljob/XxlJobTestingConstants.java @@ -0,0 +1,46 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.xxljob; + +import java.lang.reflect.Method; + +public class XxlJobTestingConstants { + + private XxlJobTestingConstants() {} + + public static final String GLUE_JOB_SHELL_SCRIPT = "echo 'hello'"; + + public static final long DEFAULT_GLUE_UPDATE_TIME = System.currentTimeMillis(); + + public static final Object METHOD_JOB_HANDLER_OBJECT = ReflectiveMethodsFactory.getTarget(); + + public static final Method METHOD_JOB_HANDLER_METHOD = ReflectiveMethodsFactory.getMethod(); + + public static final Method METHOD_JOB_HANDLER_INIT_METHOD = + ReflectiveMethodsFactory.getInitMethod(); + + public static final Method METHOD_JOB_HANDLER_DESTROY_METHOD = + ReflectiveMethodsFactory.getDestroyMethod(); + + public static final String GLUE_JOB_GROOVY_SOURCE_OLD = + "import com.xxl.job.core.handler.IJobHandler\n" + + "import com.xxl.job.core.biz.model.ReturnT\n" + + "class CustomizedGroovyHandler extends IJobHandler {\n" + + " @Override\n" + + " public ReturnT execute(String s) throws Exception {\n" + + " return new ReturnT<>(\"Hello World\")\n" + + " }\n" + + "}\n"; + + public static final String GLUE_JOB_GROOVY_SOURCE = + "import com.xxl.job.core.handler.IJobHandler\n" + + "\n" + + "class CustomizedGroovyHandler extends IJobHandler {\n" + + " @Override\n" + + " void execute() throws Exception {\n" + + " }\n" + + "}\n"; +} diff --git a/instrumentation/zio/zio-2.0/javaagent/build.gradle.kts b/instrumentation/zio/zio-2.0/javaagent/build.gradle.kts index 38e791471534..9821f649879b 100644 --- a/instrumentation/zio/zio-2.0/javaagent/build.gradle.kts +++ b/instrumentation/zio/zio-2.0/javaagent/build.gradle.kts @@ -28,6 +28,10 @@ muzzle { } } +otelJava { + maxJavaVersionSupported.set(JavaVersion.VERSION_17) +} + dependencies { compileOnly("dev.zio:zio_$scalaVersion:$zioVersion") diff --git a/instrumentation/zio/zio-2.0/javaagent/src/test/scala/io/opentelemetry/javaagent/instrumentation/zio/v2_0/ZioRuntimeInstrumentationTest.scala b/instrumentation/zio/zio-2.0/javaagent/src/test/scala/io/opentelemetry/javaagent/instrumentation/zio/v2_0/ZioRuntimeInstrumentationTest.scala index 69bf818758d0..56d71f5b26f0 100644 --- a/instrumentation/zio/zio-2.0/javaagent/src/test/scala/io/opentelemetry/javaagent/instrumentation/zio/v2_0/ZioRuntimeInstrumentationTest.scala +++ b/instrumentation/zio/zio-2.0/javaagent/src/test/scala/io/opentelemetry/javaagent/instrumentation/zio/v2_0/ZioRuntimeInstrumentationTest.scala @@ -9,11 +9,9 @@ import io.opentelemetry.instrumentation.testing.junit._ import io.opentelemetry.instrumentation.testing.util.TelemetryDataUtil.orderByRootSpanName import io.opentelemetry.javaagent.instrumentation.zio.v2_0.ZioTestFixtures._ import io.opentelemetry.sdk.testing.assertj.{SpanDataAssert, TraceAssert} -import io.opentelemetry.sdk.trace.data.SpanData import org.junit.jupiter.api.extension.RegisterExtension import org.junit.jupiter.api.{Test, TestInstance} -import java.util import java.util.function.Consumer @TestInstance(TestInstance.Lifecycle.PER_CLASS) diff --git a/javaagent-bootstrap/src/main/java/io/opentelemetry/javaagent/bootstrap/AgentInitializer.java b/javaagent-bootstrap/src/main/java/io/opentelemetry/javaagent/bootstrap/AgentInitializer.java index 4c7321415fbf..adb7668fa79b 100644 --- a/javaagent-bootstrap/src/main/java/io/opentelemetry/javaagent/bootstrap/AgentInitializer.java +++ b/javaagent-bootstrap/src/main/java/io/opentelemetry/javaagent/bootstrap/AgentInitializer.java @@ -26,6 +26,7 @@ public final class AgentInitializer { @Nullable private static ClassLoader agentClassLoader = null; @Nullable private static AgentStarter agentStarter = null; private static boolean isSecurityManagerSupportEnabled = false; + private static volatile boolean agentStarted = false; public static void initialize(Instrumentation inst, File javaagentFile, boolean fromPremain) throws Exception { @@ -51,6 +52,7 @@ public Void run() throws Exception { agentStarter = createAgentStarter(agentClassLoader, inst, javaagentFile); if (!fromPremain || !delayAgentStart()) { agentStarter.start(); + agentStarted = true; } return null; } @@ -58,7 +60,11 @@ public Void run() throws Exception { } private static void execute(PrivilegedExceptionAction action) throws Exception { - if (isSecurityManagerSupportEnabled && System.getSecurityManager() != null) { + // When security manager support is enabled we use doPrivileged even if security manager is not + // present because security manager could be installed later. ByteBuddy initialization captures + // the access control context used during transformation. If we don't use doPrivileged here then + // that context will not have the privileges if security manager is installed later. + if (isSecurityManagerSupportEnabled) { doPrivilegedExceptionAction(action); } else { action.run(); @@ -81,13 +87,13 @@ public Boolean run() { }); } - @SuppressWarnings({"deprecation", "removal"}) // AccessController is deprecated + @SuppressWarnings("removal") // AccessController is deprecated for removal private static T doPrivilegedExceptionAction(PrivilegedExceptionAction action) throws Exception { return java.security.AccessController.doPrivileged(action); } - @SuppressWarnings({"deprecation", "removal"}) // AccessController is deprecated + @SuppressWarnings("removal") // AccessController is deprecated for removal private static T doPrivileged(PrivilegedAction action) { return java.security.AccessController.doPrivileged(action); } @@ -145,11 +151,27 @@ public static void delayedStartHook() throws Exception { @Override public Void run() { agentStarter.start(); + agentStarted = true; return null; } }); } + /** + * Check whether agent has started or not along with VM. + * + *

    This method is used by + * io.opentelemetry.javaagent.tooling.AgentStarterImpl#InetAddressClassFileTransformer internally + * to check whether agent has started. + * + * @param vmStarted flag about whether VM has started or not. + * @return {@code true} if agent has started or not along with VM, {@code false} otherwise. + */ + @SuppressWarnings("unused") + public static boolean isAgentStarted(boolean vmStarted) { + return vmStarted && agentStarted; + } + public static ClassLoader getExtensionsClassLoader() { // agentStarter can be null when running tests return agentStarter != null ? agentStarter.getExtensionClassLoader() : null; diff --git a/javaagent-bootstrap/src/main/java/io/opentelemetry/javaagent/bootstrap/ExceptionLogger.java b/javaagent-bootstrap/src/main/java/io/opentelemetry/javaagent/bootstrap/ExceptionLogger.java index 950a546771df..d5320f840b97 100644 --- a/javaagent-bootstrap/src/main/java/io/opentelemetry/javaagent/bootstrap/ExceptionLogger.java +++ b/javaagent-bootstrap/src/main/java/io/opentelemetry/javaagent/bootstrap/ExceptionLogger.java @@ -7,6 +7,7 @@ import static java.util.logging.Level.FINE; +import java.util.concurrent.atomic.AtomicInteger; import java.util.logging.Logger; /** @@ -17,11 +18,18 @@ public final class ExceptionLogger { private static final Logger logger = Logger.getLogger(ExceptionLogger.class.getName()); + private static final AtomicInteger counter = new AtomicInteger(); /** See {@code io.opentelemetry.javaagent.tooling.ExceptionHandlers} for usages. */ @SuppressWarnings("unused") public static void logSuppressedError(String message, Throwable error) { logger.log(FINE, message, error); + counter.incrementAndGet(); + } + + // only used by tests + public static int getAndReset() { + return counter.getAndSet(0); } private ExceptionLogger() {} diff --git a/javaagent-bootstrap/src/main/java/io/opentelemetry/javaagent/bootstrap/IndyBootstrapDispatcher.java b/javaagent-bootstrap/src/main/java/io/opentelemetry/javaagent/bootstrap/IndyBootstrapDispatcher.java new file mode 100644 index 000000000000..efc6e4730ec7 --- /dev/null +++ b/javaagent-bootstrap/src/main/java/io/opentelemetry/javaagent/bootstrap/IndyBootstrapDispatcher.java @@ -0,0 +1,80 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.bootstrap; + +import java.lang.invoke.CallSite; +import java.lang.invoke.ConstantCallSite; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.lang.reflect.Array; + +/** + * Contains the bootstrap method for initializing invokedynamic callsites which are added via agent + * instrumentation. + */ +public class IndyBootstrapDispatcher { + + private static volatile MethodHandle bootstrap; + + private IndyBootstrapDispatcher() {} + + /** + * Initialized the invokedynamic bootstrapping method to which this class will delegate. + * + * @param bootstrapMethod the method to delegate to. Must have the same type as {@link + * #bootstrap}. + */ + public static void init(MethodHandle bootstrapMethod) { + bootstrap = bootstrapMethod; + } + + public static CallSite bootstrap( + MethodHandles.Lookup lookup, + String adviceMethodName, + MethodType adviceMethodType, + Object... args) { + CallSite callSite = null; + if (bootstrap != null) { + try { + callSite = (CallSite) bootstrap.invoke(lookup, adviceMethodName, adviceMethodType, args); + } catch (Throwable e) { + ExceptionLogger.logSuppressedError("Error bootstrapping indy instruction", e); + } + } + if (callSite == null) { + // The MethodHandle pointing to the Advice could not be created for some reason, + // fallback to a Noop MethodHandle to not crash the application + MethodHandle noop = generateNoopMethodHandle(adviceMethodType); + callSite = new ConstantCallSite(noop); + } + return callSite; + } + + // package visibility for testing + static MethodHandle generateNoopMethodHandle(MethodType methodType) { + Class returnType = methodType.returnType(); + MethodHandle noopNoArg; + if (returnType == void.class) { + noopNoArg = + MethodHandles.constant(Void.class, null).asType(MethodType.methodType(void.class)); + } else { + noopNoArg = MethodHandles.constant(returnType, getDefaultValue(returnType)); + } + return MethodHandles.dropArguments(noopNoArg, 0, methodType.parameterList()); + } + + private static Object getDefaultValue(Class classOrPrimitive) { + if (classOrPrimitive.isPrimitive()) { + // arrays of primitives are initialized with the correct primitive default value (e.g. 0 for + // int.class) + // we use this fact to generate the correct default value reflectively + return Array.get(Array.newInstance(classOrPrimitive, 1), 0); + } else { + return null; // null is the default value for reference types + } + } +} diff --git a/javaagent-bootstrap/src/test/java/io/opentelemetry/javaagent/bootstrap/IndyBootstrapDispatcherTest.java b/javaagent-bootstrap/src/test/java/io/opentelemetry/javaagent/bootstrap/IndyBootstrapDispatcherTest.java new file mode 100644 index 000000000000..54b0b0938b8a --- /dev/null +++ b/javaagent-bootstrap/src/test/java/io/opentelemetry/javaagent/bootstrap/IndyBootstrapDispatcherTest.java @@ -0,0 +1,57 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.bootstrap; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodType; +import org.junit.jupiter.api.Test; + +public class IndyBootstrapDispatcherTest { + + @Test + void testVoidNoopMethodHandle() throws Throwable { + MethodHandle noArg = generateAndCheck(MethodType.methodType(void.class)); + noArg.invokeExact(); + + MethodHandle intArg = generateAndCheck(MethodType.methodType(void.class, int.class)); + intArg.invokeExact(42); + } + + @Test + void testIntNoopMethodHandle() throws Throwable { + MethodHandle noArg = generateAndCheck(MethodType.methodType(int.class)); + assertThat((int) noArg.invokeExact()).isEqualTo(0); + + MethodHandle intArg = generateAndCheck(MethodType.methodType(int.class, int.class)); + assertThat((int) intArg.invokeExact(42)).isEqualTo(0); + } + + @Test + void testBooleanNoopMethodHandle() throws Throwable { + MethodHandle noArg = generateAndCheck(MethodType.methodType(boolean.class)); + assertThat((boolean) noArg.invokeExact()).isEqualTo(false); + + MethodHandle intArg = generateAndCheck(MethodType.methodType(boolean.class, int.class)); + assertThat((boolean) intArg.invokeExact(42)).isEqualTo(false); + } + + @Test + void testReferenceNoopMethodHandle() throws Throwable { + MethodHandle noArg = generateAndCheck(MethodType.methodType(Runnable.class)); + assertThat((Runnable) noArg.invokeExact()).isEqualTo(null); + + MethodHandle intArg = generateAndCheck(MethodType.methodType(Runnable.class, int.class)); + assertThat((Runnable) intArg.invokeExact(42)).isEqualTo(null); + } + + private static MethodHandle generateAndCheck(MethodType type) { + MethodHandle mh = IndyBootstrapDispatcher.generateNoopMethodHandle(type); + assertThat(mh.type()).isEqualTo(type); + return mh; + } +} diff --git a/javaagent-extension-api/build.gradle.kts b/javaagent-extension-api/build.gradle.kts index a08d02060dec..a63bd79094b2 100644 --- a/javaagent-extension-api/build.gradle.kts +++ b/javaagent-extension-api/build.gradle.kts @@ -12,6 +12,7 @@ dependencies { api("net.bytebuddy:byte-buddy-dep") implementation(project(":instrumentation-api")) + implementation(project(":instrumentation-api-incubator")) // autoconfigure is unstable, do not expose as api implementation("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure") diff --git a/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/bootstrap/internal/AgentCommonConfig.java b/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/bootstrap/internal/AgentCommonConfig.java new file mode 100644 index 000000000000..e5ee4b396871 --- /dev/null +++ b/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/bootstrap/internal/AgentCommonConfig.java @@ -0,0 +1,22 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.bootstrap.internal; + +import io.opentelemetry.instrumentation.api.incubator.config.internal.CommonConfig; + +/** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time. + */ +public class AgentCommonConfig { + private AgentCommonConfig() {} + + private static final CommonConfig instance = new CommonConfig(AgentInstrumentationConfig.get()); + + public static CommonConfig get() { + return instance; + } +} diff --git a/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/bootstrap/internal/AgentInstrumentationConfig.java b/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/bootstrap/internal/AgentInstrumentationConfig.java new file mode 100644 index 000000000000..081050ef76ac --- /dev/null +++ b/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/bootstrap/internal/AgentInstrumentationConfig.java @@ -0,0 +1,47 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.bootstrap.internal; + +import static java.util.Objects.requireNonNull; + +import io.opentelemetry.instrumentation.api.incubator.config.internal.InstrumentationConfig; +import java.util.logging.Logger; + +/** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time. + */ +public class AgentInstrumentationConfig { + private AgentInstrumentationConfig() {} + + private static final Logger logger = Logger.getLogger(AgentInstrumentationConfig.class.getName()); + + private static final InstrumentationConfig DEFAULT = new EmptyInstrumentationConfig(); + + // lazy initialized, so that javaagent can set it + private static volatile InstrumentationConfig instance = DEFAULT; + + /** + * Sets the instrumentation configuration singleton. This method is only supposed to be called + * once, during the agent initialization, just before {@link AgentInstrumentationConfig#get()} is + * used for the first time. + * + *

    This method is internal and is hence not for public use. Its API is unstable and can change + * at any time. + */ + public static void internalInitializeConfig(InstrumentationConfig config) { + if (instance != DEFAULT) { + logger.warning("InstrumentationConfig#instance was already set earlier"); + return; + } + instance = requireNonNull(config); + } + + /** Returns the global instrumentation configuration. */ + public static InstrumentationConfig get() { + return instance; + } +} diff --git a/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/bootstrap/internal/CommonConfig.java b/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/bootstrap/internal/CommonConfig.java deleted file mode 100644 index 7072f64809c9..000000000000 --- a/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/bootstrap/internal/CommonConfig.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.javaagent.bootstrap.internal; - -import static java.util.Collections.emptyList; -import static java.util.Collections.emptyMap; - -import java.util.List; -import java.util.Map; - -/** - * This class is internal and is hence not for public use. Its APIs are unstable and can change at - * any time. - */ -public final class CommonConfig { - - private static final CommonConfig instance = new CommonConfig(InstrumentationConfig.get()); - - public static CommonConfig get() { - return instance; - } - - private final Map peerServiceMapping; - private final List clientRequestHeaders; - private final List clientResponseHeaders; - private final List serverRequestHeaders; - private final List serverResponseHeaders; - private final boolean statementSanitizationEnabled; - - CommonConfig(InstrumentationConfig config) { - peerServiceMapping = - config.getMap("otel.instrumentation.common.peer-service-mapping", emptyMap()); - clientRequestHeaders = - config.getList("otel.instrumentation.http.capture-headers.client.request", emptyList()); - clientResponseHeaders = - config.getList("otel.instrumentation.http.capture-headers.client.response", emptyList()); - serverRequestHeaders = - config.getList("otel.instrumentation.http.capture-headers.server.request", emptyList()); - serverResponseHeaders = - config.getList("otel.instrumentation.http.capture-headers.server.response", emptyList()); - statementSanitizationEnabled = - config.getBoolean("otel.instrumentation.common.db-statement-sanitizer.enabled", true); - } - - public Map getPeerServiceMapping() { - return peerServiceMapping; - } - - public List getClientRequestHeaders() { - return clientRequestHeaders; - } - - public List getClientResponseHeaders() { - return clientResponseHeaders; - } - - public List getServerRequestHeaders() { - return serverRequestHeaders; - } - - public List getServerResponseHeaders() { - return serverResponseHeaders; - } - - public boolean isStatementSanitizationEnabled() { - return statementSanitizationEnabled; - } -} diff --git a/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/bootstrap/internal/ConfiguredResourceAttributesHolder.java b/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/bootstrap/internal/ConfiguredResourceAttributesHolder.java new file mode 100644 index 000000000000..ea7c51ff436f --- /dev/null +++ b/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/bootstrap/internal/ConfiguredResourceAttributesHolder.java @@ -0,0 +1,48 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.bootstrap.internal; + +import static io.opentelemetry.api.common.AttributeKey.stringKey; + +import io.opentelemetry.api.common.Attributes; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import javax.annotation.Nullable; + +/** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time. + */ +public final class ConfiguredResourceAttributesHolder { + + private static final Map resourceAttributes = new HashMap<>(); + + public static Map getResourceAttributes() { + return resourceAttributes; + } + + public static void initialize(Attributes resourceAttribute) { + List mdcResourceAttributes = + AgentInstrumentationConfig.get() + .getList( + "otel.instrumentation.common.mdc.resource-attributes", Collections.emptyList()); + for (String key : mdcResourceAttributes) { + String value = resourceAttribute.get(stringKey(key)); + if (value != null) { + ConfiguredResourceAttributesHolder.resourceAttributes.put(key, value); + } + } + } + + @Nullable + public static String getAttributeValue(String key) { + return resourceAttributes.get(key); + } + + private ConfiguredResourceAttributesHolder() {} +} diff --git a/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/bootstrap/internal/DeprecatedConfigProperties.java b/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/bootstrap/internal/DeprecatedConfigProperties.java index 3f9e2a7f8cf6..2709a6ab0ea2 100644 --- a/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/bootstrap/internal/DeprecatedConfigProperties.java +++ b/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/bootstrap/internal/DeprecatedConfigProperties.java @@ -5,14 +5,18 @@ package io.opentelemetry.javaagent.bootstrap.internal; +import static java.util.Collections.emptyList; import static java.util.logging.Level.WARNING; +import io.opentelemetry.instrumentation.api.incubator.config.internal.InstrumentationConfig; +import java.util.List; import java.util.logging.Logger; /** * This class is internal and is hence not for public use. Its APIs are unstable and can change at * any time. */ +@SuppressWarnings("unused") public final class DeprecatedConfigProperties { private static final Logger logger = Logger.getLogger(DeprecatedConfigProperties.class.getName()); @@ -22,14 +26,26 @@ public static boolean getBoolean( String deprecatedPropertyName, String newPropertyName, boolean defaultValue) { + warnIfUsed(config, deprecatedPropertyName, newPropertyName); + boolean value = config.getBoolean(deprecatedPropertyName, defaultValue); + return config.getBoolean(newPropertyName, value); + } + + public static List getList( + InstrumentationConfig config, String deprecatedPropertyName, String newPropertyName) { + warnIfUsed(config, deprecatedPropertyName, newPropertyName); + List value = config.getList(deprecatedPropertyName, emptyList()); + return config.getList(newPropertyName, value); + } + + private static void warnIfUsed( + InstrumentationConfig config, String deprecatedPropertyName, String newPropertyName) { if (config.getString(deprecatedPropertyName) != null) { logger.log( WARNING, "Deprecated property \"{0}\" was used; use the \"{1}\" property instead", new Object[] {deprecatedPropertyName, newPropertyName}); } - boolean value = config.getBoolean(deprecatedPropertyName, defaultValue); - return config.getBoolean(newPropertyName, value); } private DeprecatedConfigProperties() {} diff --git a/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/bootstrap/internal/EmptyInstrumentationConfig.java b/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/bootstrap/internal/EmptyInstrumentationConfig.java index ede916b3b6ad..303931c5b674 100644 --- a/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/bootstrap/internal/EmptyInstrumentationConfig.java +++ b/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/bootstrap/internal/EmptyInstrumentationConfig.java @@ -5,12 +5,13 @@ package io.opentelemetry.javaagent.bootstrap.internal; +import io.opentelemetry.instrumentation.api.incubator.config.internal.InstrumentationConfig; import java.time.Duration; import java.util.List; import java.util.Map; import javax.annotation.Nullable; -final class EmptyInstrumentationConfig extends InstrumentationConfig { +final class EmptyInstrumentationConfig implements InstrumentationConfig { @Nullable @Override diff --git a/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/bootstrap/internal/ExperimentalConfig.java b/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/bootstrap/internal/ExperimentalConfig.java index c0f19055429a..736229de4ecb 100644 --- a/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/bootstrap/internal/ExperimentalConfig.java +++ b/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/bootstrap/internal/ExperimentalConfig.java @@ -7,6 +7,7 @@ import static java.util.Collections.emptyList; +import io.opentelemetry.instrumentation.api.incubator.config.internal.InstrumentationConfig; import java.util.List; /** @@ -16,7 +17,7 @@ public final class ExperimentalConfig { private static final ExperimentalConfig instance = - new ExperimentalConfig(InstrumentationConfig.get()); + new ExperimentalConfig(AgentInstrumentationConfig.get()); private final InstrumentationConfig config; private final List messagingHeaders; @@ -34,12 +35,12 @@ public ExperimentalConfig(InstrumentationConfig config) { public boolean controllerTelemetryEnabled() { return config.getBoolean( - "otel.instrumentation.common.experimental.controller-telemetry.enabled", true); + "otel.instrumentation.common.experimental.controller-telemetry.enabled", false); } public boolean viewTelemetryEnabled() { return config.getBoolean( - "otel.instrumentation.common.experimental.view-telemetry.enabled", true); + "otel.instrumentation.common.experimental.view-telemetry.enabled", false); } public boolean messagingReceiveInstrumentationEnabled() { @@ -47,6 +48,10 @@ public boolean messagingReceiveInstrumentationEnabled() { "otel.instrumentation.messaging.experimental.receive-telemetry.enabled", false); } + public boolean indyEnabled() { + return config.getBoolean("otel.javaagent.experimental.indy", false); + } + public List getMessagingHeaders() { return messagingHeaders; } diff --git a/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/bootstrap/internal/JavaagentHttpClientInstrumenters.java b/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/bootstrap/internal/JavaagentHttpClientInstrumenters.java new file mode 100644 index 000000000000..e7331c550fff --- /dev/null +++ b/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/bootstrap/internal/JavaagentHttpClientInstrumenters.java @@ -0,0 +1,64 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.bootstrap.internal; + +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.context.propagation.TextMapSetter; +import io.opentelemetry.instrumentation.api.incubator.builder.internal.DefaultHttpClientInstrumenterBuilder; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import io.opentelemetry.instrumentation.api.instrumenter.InstrumenterBuilder; +import io.opentelemetry.instrumentation.api.semconv.http.HttpClientAttributesGetter; +import java.util.function.Consumer; + +/** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time. + */ +public final class JavaagentHttpClientInstrumenters { + + private JavaagentHttpClientInstrumenters() {} + + public static Instrumenter create( + String instrumentationName, + HttpClientAttributesGetter httpAttributesGetter) { + return create(instrumentationName, httpAttributesGetter, null); + } + + public static Instrumenter create( + String instrumentationName, + HttpClientAttributesGetter httpAttributesGetter, + TextMapSetter headerSetter) { + return create(instrumentationName, httpAttributesGetter, headerSetter, b -> {}); + } + + public static Instrumenter create( + DefaultHttpClientInstrumenterBuilder builder) { + return create(builder, customizer -> {}); + } + + public static Instrumenter create( + String instrumentationName, + HttpClientAttributesGetter httpAttributesGetter, + TextMapSetter headerSetter, + Consumer> instrumenterBuilderConsumer) { + DefaultHttpClientInstrumenterBuilder defaultHttpClientTelemetryBuilder = + new DefaultHttpClientInstrumenterBuilder<>( + instrumentationName, GlobalOpenTelemetry.get(), httpAttributesGetter); + if (headerSetter != null) { + defaultHttpClientTelemetryBuilder.setHeaderSetter(headerSetter); + } + return create(defaultHttpClientTelemetryBuilder, instrumenterBuilderConsumer); + } + + private static Instrumenter create( + DefaultHttpClientInstrumenterBuilder builder, + Consumer> builderCustomizer) { + return builder + .configure(AgentCommonConfig.get()) + .setBuilderCustomizer(builderCustomizer) + .build(); + } +} diff --git a/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/extension/instrumentation/InstrumentationModule.java b/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/extension/instrumentation/InstrumentationModule.java index f57d1e67ee51..7f404090b679 100644 --- a/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/extension/instrumentation/InstrumentationModule.java +++ b/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/extension/instrumentation/InstrumentationModule.java @@ -9,12 +9,14 @@ import static java.util.Collections.unmodifiableSet; import static net.bytebuddy.matcher.ElementMatchers.any; +import io.opentelemetry.javaagent.bootstrap.internal.ExperimentalConfig; import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; import io.opentelemetry.sdk.autoconfigure.spi.Ordered; import java.util.Collections; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; +import java.util.logging.Logger; import net.bytebuddy.matcher.ElementMatcher; /** @@ -30,6 +32,15 @@ * java.util.ServiceLoader} for more details. */ public abstract class InstrumentationModule implements Ordered { + private static final Logger logger = Logger.getLogger(InstrumentationModule.class.getName()); + private static final boolean indyEnabled; + + static { + indyEnabled = ExperimentalConfig.get().indyEnabled(); + if (indyEnabled) { + logger.info("Enabled indy for instrumentation modules"); + } + } private final Set instrumentationNames; @@ -38,6 +49,10 @@ public abstract class InstrumentationModule implements Ordered { * InstrumentationModule} must have a default constructor (for SPI), so they have to pass the * instrumentation names to the super class constructor. * + *

    When enabling or disabling the instrumentation module configuration property that + * corresponds to the main instrumentation name is considered first, after that additional + * instrumentation names are considered in the order they are listed here. + * *

    The instrumentation names should follow several rules: * *

      @@ -100,6 +115,19 @@ public boolean isHelperClass(String className) { return false; } + /** + * Note this is an experimental feature until phase 1 of + * implementing the invokedynamic based instrumentation mechanism is complete. Instrumentation + * modules that override this to true (recommended) must use the inline=false Invoke Dynamic style + * of Byte Buddy advices which calls out to helper classes in their own classloader, thus enabling + * better isolation, best practice code development, avoids shading and enables standard debugging + * techniques. The non-inlining of advice will be enforced by muzzle (TODO) + */ + public boolean isIndyModule() { + return indyEnabled; + } + /** Register resource names to inject into the user's class loader. */ public void registerHelperResources(HelperResourceBuilder helperResourceBuilder) {} diff --git a/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/extension/instrumentation/internal/AsmApi.java b/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/extension/instrumentation/internal/AsmApi.java new file mode 100644 index 000000000000..b76f4e3c1508 --- /dev/null +++ b/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/extension/instrumentation/internal/AsmApi.java @@ -0,0 +1,18 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.extension.instrumentation.internal; + +import org.objectweb.asm.Opcodes; + +/** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time. + */ +public final class AsmApi { + public static final int VERSION = Opcodes.ASM9; + + private AsmApi() {} +} diff --git a/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/extension/instrumentation/internal/ExperimentalInstrumentationModule.java b/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/extension/instrumentation/internal/ExperimentalInstrumentationModule.java new file mode 100644 index 000000000000..af8d33d78659 --- /dev/null +++ b/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/extension/instrumentation/internal/ExperimentalInstrumentationModule.java @@ -0,0 +1,64 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.extension.instrumentation.internal; + +import static java.util.Collections.emptyList; + +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.internal.injection.ClassInjector; +import java.util.Collections; +import java.util.List; + +/** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time. + */ +public interface ExperimentalInstrumentationModule { + + /** + * Only functional for Modules where {@link InstrumentationModule#isIndyModule()} returns {@code + * true}. + * + *

      Normally, helper and advice classes are loaded in a child classloader of the instrumented + * classloader. This method allows to inject classes directly into the instrumented classloader + * instead. + * + * @param injector the builder for injecting classes + */ + default void injectClasses(ClassInjector injector) {} + + /** + * Returns a list of helper classes that will be defined in the class loader of the instrumented + * library. + */ + default List injectedClassNames() { + return emptyList(); + } + + /** + * By default every InstrumentationModule is loaded by an isolated classloader, even if multiple + * modules instrument the same application classloader. + * + *

      Sometimes this is not desired, e.g. when instrumenting modular libraries such as the AWS + * SDK. In such cases the {@link InstrumentationModule}s which want to share a classloader can + * return the same group name from this method. + */ + default String getModuleGroup() { + return getClass().getName(); + } + + /** + * Some instrumentations need to invoke classes which are present both in the agent classloader + * and the instrumented application classloader. By default, the classloader of the + * instrumentation would link those against the class provided by the agent. This setting allows + * to hide packages, so that matching classes are instead used from the application classloader. + * + * @return the list of packages (without trailing dots) + */ + default List agentPackagesToHide() { + return Collections.emptyList(); + } +} diff --git a/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/extension/instrumentation/internal/injection/ClassInjector.java b/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/extension/instrumentation/internal/injection/ClassInjector.java new file mode 100644 index 000000000000..9a11cbd6c689 --- /dev/null +++ b/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/extension/instrumentation/internal/injection/ClassInjector.java @@ -0,0 +1,39 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.extension.instrumentation.internal.injection; + +/** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time. + */ +public interface ClassInjector { + + /** + * Create a builder for a proxy class which will be injected into the instrumented {@link + * ClassLoader}. The generated proxy will delegate to the original class, which is loaded in a + * separate classloader. + * + *

      This removes the need for the proxied class and its dependencies to be visible (just like + * Advices) to the instrumented ClassLoader. + * + * @param classToProxy the fully qualified name of the class for which a proxy will be generated + * @param newProxyName the fully qualified name to use for the generated proxy + * @return a builder for further customizing the proxy. {@link + * ProxyInjectionBuilder#inject(InjectionMode)} must be called to actually inject the proxy. + */ + ProxyInjectionBuilder proxyBuilder(String classToProxy, String newProxyName); + + /** + * Same as invoking {@link #proxyBuilder(String, String)}, but the resulting proxy will have the + * same name as the proxied class. + * + * @param classToProxy the fully qualified name of the class for which a proxy will be generated + * @return a builder for further customizing and injecting the proxy + */ + default ProxyInjectionBuilder proxyBuilder(String classToProxy) { + return proxyBuilder(classToProxy, classToProxy); + } +} diff --git a/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/extension/instrumentation/internal/injection/InjectionMode.java b/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/extension/instrumentation/internal/injection/InjectionMode.java new file mode 100644 index 000000000000..2eaea5f66d6c --- /dev/null +++ b/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/extension/instrumentation/internal/injection/InjectionMode.java @@ -0,0 +1,32 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.extension.instrumentation.internal.injection; + +/** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time. + */ +public enum InjectionMode { + CLASS_ONLY(true, false), + RESOURCE_ONLY(false, true), + CLASS_AND_RESOURCE(true, true); + + private final boolean injectClass; + private final boolean injectResource; + + InjectionMode(boolean injectClass, boolean injectResource) { + this.injectClass = injectClass; + this.injectResource = injectResource; + } + + public boolean shouldInjectClass() { + return injectClass; + } + + public boolean shouldInjectResource() { + return injectResource; + } +} diff --git a/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/extension/instrumentation/internal/injection/ProxyInjectionBuilder.java b/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/extension/instrumentation/internal/injection/ProxyInjectionBuilder.java new file mode 100644 index 000000000000..23d5237aabda --- /dev/null +++ b/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/extension/instrumentation/internal/injection/ProxyInjectionBuilder.java @@ -0,0 +1,15 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.extension.instrumentation.internal.injection; + +/** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time. + */ +public interface ProxyInjectionBuilder { + + void inject(InjectionMode mode); +} diff --git a/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/extension/matcher/ClassLoaderHasClassesNamedMatcher.java b/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/extension/matcher/ClassLoaderHasClassesNamedMatcher.java index 46c685b10cfc..593ba4d1bd64 100644 --- a/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/extension/matcher/ClassLoaderHasClassesNamedMatcher.java +++ b/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/extension/matcher/ClassLoaderHasClassesNamedMatcher.java @@ -9,8 +9,6 @@ import io.opentelemetry.javaagent.bootstrap.internal.ClassLoaderMatcherCacheHolder; import io.opentelemetry.javaagent.bootstrap.internal.InClassLoaderMatcher; import java.util.BitSet; -import java.util.List; -import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantReadWriteLock; @@ -31,9 +29,6 @@ class ClassLoaderHasClassesNamedMatcher extends ElementMatcher.Junction.Abstract for (int i = 0; i < resources.length; i++) { resources[i] = resources[i].replace(".", "/") + ".class"; } - if (useCache) { - Manager.INSTANCE.add(this); - } } @Override @@ -65,28 +60,18 @@ private static boolean hasResources(ClassLoader cl, String... resources) { private static class Manager { static final Manager INSTANCE = new Manager(); - private final List matchers = new CopyOnWriteArrayList<>(); // each matcher gets a two bits in BitSet, that first bit indicates whether current matcher has // been run for given class loader and the second whether it matched or not private final Cache enabled = Cache.weak(); private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); private final Lock readLock = lock.readLock(); private final Lock writeLock = lock.writeLock(); - private volatile boolean matchCalled = false; Manager() { ClassLoaderMatcherCacheHolder.addCache(enabled); } - void add(ClassLoaderHasClassesNamedMatcher matcher) { - if (matchCalled) { - throw new IllegalStateException("All matchers should be create before match is called"); - } - matchers.add(matcher); - } - boolean match(ClassLoaderHasClassesNamedMatcher matcher, ClassLoader cl) { - matchCalled = true; BitSet set = enabled.computeIfAbsent(cl, (unused) -> new BitSet(counter.get() * 2)); int matcherRunBit = 2 * matcher.index; int matchedBit = matcherRunBit + 1; diff --git a/javaagent-internal-logging-application/src/main/java/io/opentelemetry/javaagent/logging/application/ApplicationLoggerFactory.java b/javaagent-internal-logging-application/src/main/java/io/opentelemetry/javaagent/logging/application/ApplicationLoggerFactory.java index dcc8c3a6ef09..1cf774c9ced4 100644 --- a/javaagent-internal-logging-application/src/main/java/io/opentelemetry/javaagent/logging/application/ApplicationLoggerFactory.java +++ b/javaagent-internal-logging-application/src/main/java/io/opentelemetry/javaagent/logging/application/ApplicationLoggerFactory.java @@ -45,6 +45,7 @@ protected void install(InternalLogger.Factory applicationLoggerFactory) { while (inMemoryLogStore.currentSize() > 0) { inMemoryLogStore.flush(applicationLoggerFactory); } + inMemoryLogStore.setApplicationLoggerFactory(applicationLoggerFactory); // actually install the application logger - from this point, everything will be logged // directly through the application logging system diff --git a/javaagent-internal-logging-application/src/main/java/io/opentelemetry/javaagent/logging/application/InMemoryLogStore.java b/javaagent-internal-logging-application/src/main/java/io/opentelemetry/javaagent/logging/application/InMemoryLogStore.java index 291f9483f5b1..2cfa1570f83b 100644 --- a/javaagent-internal-logging-application/src/main/java/io/opentelemetry/javaagent/logging/application/InMemoryLogStore.java +++ b/javaagent-internal-logging-application/src/main/java/io/opentelemetry/javaagent/logging/application/InMemoryLogStore.java @@ -20,12 +20,22 @@ final class InMemoryLogStore { private final int limit; + private InternalLogger.Factory applicationLoggerFactory; + InMemoryLogStore(int limit) { this.limit = limit; } void write(InMemoryLog log) { synchronized (lock) { + // redirect to application logging system if it is already set up + // this is here to ensure that we don't lose any logs when switching from in memory to + // application logging + if (applicationLoggerFactory != null) { + applicationLoggerFactory.create(log.name()).log(log.level(), log.message(), log.error()); + return; + } + // just drop the log if hit the limit if (limit >= 0 && inMemoryLogs.size() >= limit) { return; @@ -49,6 +59,12 @@ void flush(InternalLogger.Factory applicationLoggerFactory) { } } + void setApplicationLoggerFactory(InternalLogger.Factory applicationLoggerFactory) { + synchronized (lock) { + this.applicationLoggerFactory = applicationLoggerFactory; + } + } + int currentSize() { synchronized (lock) { return inMemoryLogs.size(); diff --git a/javaagent-internal-logging-application/src/test/java/io/opentelemetry/javaagent/logging/application/ApplicationLoggerFactoryTest.java b/javaagent-internal-logging-application/src/test/java/io/opentelemetry/javaagent/logging/application/ApplicationLoggerFactoryTest.java index 3333cc19ad78..417631a896fe 100644 --- a/javaagent-internal-logging-application/src/test/java/io/opentelemetry/javaagent/logging/application/ApplicationLoggerFactoryTest.java +++ b/javaagent-internal-logging-application/src/test/java/io/opentelemetry/javaagent/logging/application/ApplicationLoggerFactoryTest.java @@ -60,6 +60,7 @@ void shouldOnlyInstallTheFirstBridge() { verify(logStore, times(3)).currentSize(); verify(logStore).flush(applicationLoggerBridge); + verify(logStore).setApplicationLoggerFactory(applicationLoggerBridge); verify(logStore).freeMemory(); underTest.install(applicationLoggerBridge); diff --git a/javaagent-internal-logging-simple/build.gradle.kts b/javaagent-internal-logging-simple/build.gradle.kts index 125653c86421..b87f59ec43cf 100644 --- a/javaagent-internal-logging-simple/build.gradle.kts +++ b/javaagent-internal-logging-simple/build.gradle.kts @@ -3,8 +3,7 @@ import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar plugins { id("otel.java-conventions") id("otel.publish-conventions") - - id("com.github.johnrengelman.shadow") + id("com.gradleup.shadow") } group = "io.opentelemetry.javaagent" diff --git a/javaagent-internal-logging-simple/src/main/java/io/opentelemetry/javaagent/logging/simple/Slf4jSimpleLoggingCustomizer.java b/javaagent-internal-logging-simple/src/main/java/io/opentelemetry/javaagent/logging/simple/Slf4jSimpleLoggingCustomizer.java index d99bf47885da..148f7a5ec3d7 100644 --- a/javaagent-internal-logging-simple/src/main/java/io/opentelemetry/javaagent/logging/simple/Slf4jSimpleLoggingCustomizer.java +++ b/javaagent-internal-logging-simple/src/main/java/io/opentelemetry/javaagent/logging/simple/Slf4jSimpleLoggingCustomizer.java @@ -39,6 +39,8 @@ public void init(EarlyInitAgentConfig earlyConfig) { if (earlyConfig.getBoolean("otel.javaagent.debug", false)) { setSystemPropertyDefault(SIMPLE_LOGGER_DEFAULT_LOG_LEVEL_PROPERTY, "DEBUG"); setSystemPropertyDefault(SIMPLE_LOGGER_PREFIX + "okhttp3.internal.http2", "INFO"); + setSystemPropertyDefault( + SIMPLE_LOGGER_PREFIX + "okhttp3.internal.concurrent.TaskRunner", "INFO"); } // trigger loading the provider from the agent CL diff --git a/javaagent-tooling/build.gradle.kts b/javaagent-tooling/build.gradle.kts index cbce3b82d7db..1f318ac0f203 100644 --- a/javaagent-tooling/build.gradle.kts +++ b/javaagent-tooling/build.gradle.kts @@ -13,20 +13,23 @@ dependencies { implementation(project(":javaagent-extension-api")) implementation(project(":javaagent-tooling:javaagent-tooling-java9")) implementation(project(":instrumentation-api")) + implementation(project(":instrumentation-api-incubator")) implementation(project(":instrumentation-annotations-support")) implementation(project(":muzzle")) + implementation(project(":sdk-autoconfigure-support")) implementation("io.opentelemetry:opentelemetry-api") - testImplementation("io.opentelemetry:opentelemetry-api-events") + testImplementation("io.opentelemetry:opentelemetry-api-incubator") implementation("io.opentelemetry:opentelemetry-sdk") - implementation("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure") implementation("io.opentelemetry:opentelemetry-extension-kotlin") implementation("io.opentelemetry:opentelemetry-extension-trace-propagators") // the incubator's ViewConfigCustomizer is used to support loading yaml-based metric views - implementation("io.opentelemetry:opentelemetry-sdk-extension-incubator") + implementation("io.opentelemetry:opentelemetry-sdk-extension-incubator") { + // we use byte-buddy-dep + exclude("net.bytebuddy", "byte-buddy") + } // Exporters with dependencies - implementation("io.opentelemetry:opentelemetry-exporter-jaeger") implementation("io.opentelemetry:opentelemetry-exporter-logging") implementation("io.opentelemetry:opentelemetry-exporter-otlp") implementation("io.opentelemetry:opentelemetry-exporter-logging-otlp") @@ -38,7 +41,13 @@ dependencies { implementation("io.opentelemetry.contrib:opentelemetry-aws-xray-propagator") + implementation("io.opentelemetry.contrib:opentelemetry-aws-resources") + implementation("io.opentelemetry.contrib:opentelemetry-gcp-resources") + implementation("io.opentelemetry.contrib:opentelemetry-baggage-processor") + api("net.bytebuddy:byte-buddy-dep") + implementation("org.ow2.asm:asm-tree") + implementation("org.ow2.asm:asm-util") annotationProcessor("com.google.auto.service:auto-service") compileOnly("com.google.auto.service:auto-service-annotations") @@ -77,6 +86,17 @@ testing { compileOnly("com.google.code.findbugs:annotations") } } + + val testPatchBytecodeVersion by registering(JvmTestSuite::class) { + dependencies { + implementation(project(":javaagent-bootstrap")) + implementation(project(":javaagent-tooling")) + implementation("net.bytebuddy:byte-buddy-dep") + + // Used by byte-buddy but not brought in as a transitive dependency. + compileOnly("com.google.code.findbugs:annotations") + } + } } } diff --git a/javaagent-tooling/jdk18-testing/build.gradle.kts b/javaagent-tooling/jdk18-testing/build.gradle.kts new file mode 100644 index 000000000000..ab1bf712d97f --- /dev/null +++ b/javaagent-tooling/jdk18-testing/build.gradle.kts @@ -0,0 +1,11 @@ +plugins { + id("otel.javaagent-testing") +} + +dependencies { + compileOnly("io.opentelemetry:opentelemetry-sdk-common") +} + +otelJava { + minJavaVersionSupported.set(JavaVersion.VERSION_18) +} diff --git a/javaagent-tooling/jdk18-testing/src/main/java/testing/TestResourceProvider.java b/javaagent-tooling/jdk18-testing/src/main/java/testing/TestResourceProvider.java new file mode 100644 index 000000000000..9db4d5df5006 --- /dev/null +++ b/javaagent-tooling/jdk18-testing/src/main/java/testing/TestResourceProvider.java @@ -0,0 +1,30 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package testing; + +import com.google.auto.service.AutoService; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import io.opentelemetry.sdk.autoconfigure.spi.ResourceProvider; +import io.opentelemetry.sdk.resources.Resource; +import java.net.InetAddress; +import java.net.UnknownHostException; + +@AutoService(ResourceProvider.class) +public class TestResourceProvider implements ResourceProvider { + + @Override + public Resource createResource(ConfigProperties config) { + // used in test to determine whether this method was called + System.setProperty("test.resource.provider.called", "true"); + // this call trigger loading InetAddressResolverProvider SPI on jdk 18 + try { + InetAddress.getLocalHost(); + } catch (UnknownHostException e) { + throw new IllegalStateException(e); + } + return Resource.empty(); + } +} diff --git a/javaagent-tooling/jdk18-testing/src/test/java/io/opentelemetry/javaagent/tooling/inetaddress/InetAddressResolverTest.java b/javaagent-tooling/jdk18-testing/src/test/java/io/opentelemetry/javaagent/tooling/inetaddress/InetAddressResolverTest.java new file mode 100644 index 000000000000..f21e494b5f0a --- /dev/null +++ b/javaagent-tooling/jdk18-testing/src/test/java/io/opentelemetry/javaagent/tooling/inetaddress/InetAddressResolverTest.java @@ -0,0 +1,28 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.tooling.inetaddress; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.net.InetAddress; +import org.junit.jupiter.api.Test; + +public class InetAddressResolverTest { + + @Test + void agentStartShouldNotTriggerLoadingCustomInetAddressResolvers() throws Exception { + // This system property is set in TestResourceProvider + assertThat(System.getProperty("test.resource.provider.called")).isEqualTo("true"); + // Agent start should not trigger loading (and instantiating) custom InetAddress resolvers + assertThat(TestAddressResolver.isInstantiated()).isFalse(); + + // Trigger loading (and instantiating) custom InetAddress resolvers manually + InetAddress.getAllByName("test"); + + // Verify that custom InetAddress resolver loaded and instantiated + assertThat(TestAddressResolver.isInstantiated()).isTrue(); + } +} diff --git a/javaagent-tooling/jdk18-testing/src/test/java/io/opentelemetry/javaagent/tooling/inetaddress/TestAddressResolver.java b/javaagent-tooling/jdk18-testing/src/test/java/io/opentelemetry/javaagent/tooling/inetaddress/TestAddressResolver.java new file mode 100644 index 000000000000..9c0df5aad70b --- /dev/null +++ b/javaagent-tooling/jdk18-testing/src/test/java/io/opentelemetry/javaagent/tooling/inetaddress/TestAddressResolver.java @@ -0,0 +1,39 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.tooling.inetaddress; + +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.net.spi.InetAddressResolver; +import java.util.stream.Stream; + +public class TestAddressResolver implements InetAddressResolver { + + private static volatile boolean instantiated = false; + + @SuppressWarnings("StaticAssignmentInConstructor") + public TestAddressResolver() { + TestAddressResolver.instantiated = true; + } + + public static boolean isInstantiated() { + return instantiated; + } + + @Override + public Stream lookupByName(String host, LookupPolicy lookupPolicy) + throws UnknownHostException { + if (host.equals("test")) { + return Stream.of(InetAddress.getByAddress(new byte[] {127, 0, 0, 1})); + } + throw new UnknownHostException(); + } + + @Override + public String lookupByAddress(byte[] addr) { + throw new UnsupportedOperationException(); + } +} diff --git a/javaagent-tooling/jdk18-testing/src/test/java/io/opentelemetry/javaagent/tooling/inetaddress/TestAddressResolverProvider.java b/javaagent-tooling/jdk18-testing/src/test/java/io/opentelemetry/javaagent/tooling/inetaddress/TestAddressResolverProvider.java new file mode 100644 index 000000000000..f1af2597e5c0 --- /dev/null +++ b/javaagent-tooling/jdk18-testing/src/test/java/io/opentelemetry/javaagent/tooling/inetaddress/TestAddressResolverProvider.java @@ -0,0 +1,22 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.tooling.inetaddress; + +import java.net.spi.InetAddressResolver; +import java.net.spi.InetAddressResolverProvider; + +public class TestAddressResolverProvider extends InetAddressResolverProvider { + + @Override + public InetAddressResolver get(Configuration configuration) { + return new TestAddressResolver(); + } + + @Override + public String name() { + return "Test Internet Address Resolver Provider"; + } +} diff --git a/javaagent-tooling/jdk18-testing/src/test/resources/META-INF/services/java.net.spi.InetAddressResolverProvider b/javaagent-tooling/jdk18-testing/src/test/resources/META-INF/services/java.net.spi.InetAddressResolverProvider new file mode 100644 index 000000000000..c4d2e7912b78 --- /dev/null +++ b/javaagent-tooling/jdk18-testing/src/test/resources/META-INF/services/java.net.spi.InetAddressResolverProvider @@ -0,0 +1 @@ +io.opentelemetry.javaagent.tooling.inetaddress.TestAddressResolverProvider diff --git a/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/AddThreadDetailsSpanProcessor.java b/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/AddThreadDetailsSpanProcessor.java index 426098256504..496d8580ff79 100644 --- a/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/AddThreadDetailsSpanProcessor.java +++ b/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/AddThreadDetailsSpanProcessor.java @@ -10,15 +10,15 @@ 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; +import io.opentelemetry.semconv.incubating.ThreadIncubatingAttributes; 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()); + span.setAttribute(ThreadIncubatingAttributes.THREAD_ID, currentThread.getId()); + span.setAttribute(ThreadIncubatingAttributes.THREAD_NAME, currentThread.getName()); } @Override diff --git a/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/AgentInstaller.java b/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/AgentInstaller.java index ef83c3879e52..54c6938c96d9 100644 --- a/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/AgentInstaller.java +++ b/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/AgentInstaller.java @@ -26,13 +26,13 @@ import io.opentelemetry.javaagent.bootstrap.http.HttpServerResponseCustomizer; import io.opentelemetry.javaagent.bootstrap.http.HttpServerResponseCustomizerHolder; import io.opentelemetry.javaagent.bootstrap.http.HttpServerResponseMutator; -import io.opentelemetry.javaagent.bootstrap.internal.InstrumentationConfig; +import io.opentelemetry.javaagent.bootstrap.internal.AgentInstrumentationConfig; +import io.opentelemetry.javaagent.bootstrap.internal.ConfiguredResourceAttributesHolder; import io.opentelemetry.javaagent.extension.AgentListener; import io.opentelemetry.javaagent.extension.ignore.IgnoredTypesConfigurer; import io.opentelemetry.javaagent.tooling.asyncannotationsupport.WeakRefAsyncOperationEndStrategies; import io.opentelemetry.javaagent.tooling.bootstrap.BootstrapPackagesBuilderImpl; import io.opentelemetry.javaagent.tooling.bootstrap.BootstrapPackagesConfigurer; -import io.opentelemetry.javaagent.tooling.config.AgentConfig; import io.opentelemetry.javaagent.tooling.config.ConfigPropertiesBridge; import io.opentelemetry.javaagent.tooling.config.EarlyInitAgentConfig; import io.opentelemetry.javaagent.tooling.ignore.IgnoredClassLoadersMatcher; @@ -41,6 +41,8 @@ import io.opentelemetry.javaagent.tooling.muzzle.AgentTooling; import io.opentelemetry.javaagent.tooling.util.Trie; import io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdk; +import io.opentelemetry.sdk.autoconfigure.SdkAutoconfigureAccess; +import io.opentelemetry.sdk.autoconfigure.internal.AutoConfigureUtil; import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; import java.lang.instrument.Instrumentation; import java.util.ArrayList; @@ -119,11 +121,13 @@ private static void installBytebuddyAgent( AutoConfiguredOpenTelemetrySdk autoConfiguredSdk = installOpenTelemetrySdk(extensionClassLoader); - ConfigProperties sdkConfig = autoConfiguredSdk.getConfig(); - InstrumentationConfig.internalInitializeConfig(new ConfigPropertiesBridge(sdkConfig)); + ConfigProperties sdkConfig = AutoConfigureUtil.getConfig(autoConfiguredSdk); + AgentInstrumentationConfig.internalInitializeConfig(new ConfigPropertiesBridge(sdkConfig)); copyNecessaryConfigToSystemProperties(sdkConfig); setBootstrapPackages(sdkConfig, extensionClassLoader); + ConfiguredResourceAttributesHolder.initialize( + SdkAutoconfigureAccess.getResourceAttributes(autoConfiguredSdk)); for (BeforeAgentListener agentListener : loadOrdered(BeforeAgentListener.class, extensionClassLoader)) { @@ -153,7 +157,7 @@ private static void installBytebuddyAgent( agentBuilder = configureIgnoredTypes(sdkConfig, extensionClassLoader, agentBuilder); - if (AgentConfig.isDebugModeEnabled(sdkConfig)) { + if (logger.isLoggable(FINE)) { agentBuilder = agentBuilder .with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION) @@ -196,11 +200,7 @@ private static void installBytebuddyAgent( } private static void copyNecessaryConfigToSystemProperties(ConfigProperties config) { - for (String property : - asList( - "otel.instrumentation.experimental.span-suppression-strategy", - "otel.instrumentation.http.prefer-forwarded-url-scheme", - "otel.semconv-stability.opt-in")) { + for (String property : asList("otel.instrumentation.experimental.span-suppression-strategy")) { String value = config.getString(property); if (value != null) { System.setProperty(property, value); @@ -286,7 +286,8 @@ private static void runAfterAgentListeners( // the application is already setting the global LogManager and AgentListener won't be able // to touch it due to class loader locking. boolean shouldForceSynchronousAgentListenersCalls = - autoConfiguredSdk.getConfig().getBoolean(FORCE_SYNCHRONOUS_AGENT_LISTENERS_CONFIG, false); + AutoConfigureUtil.getConfig(autoConfiguredSdk) + .getBoolean(FORCE_SYNCHRONOUS_AGENT_LISTENERS_CONFIG, false); boolean javaBefore9 = isJavaBefore9(); if (!shouldForceSynchronousAgentListenersCalls && javaBefore9 && isAppUsingCustomLogManager()) { logger.fine("Custom JUL LogManager detected: delaying AgentListener#afterAgent() calls"); diff --git a/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/AgentStarterImpl.java b/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/AgentStarterImpl.java index 83d01a2398d3..ab5cb93c22ed 100644 --- a/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/AgentStarterImpl.java +++ b/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/AgentStarterImpl.java @@ -9,6 +9,7 @@ import io.opentelemetry.instrumentation.api.internal.cache.weaklockfree.WeakConcurrentMapCleaner; import io.opentelemetry.javaagent.bootstrap.AgentInitializer; import io.opentelemetry.javaagent.bootstrap.AgentStarter; +import io.opentelemetry.javaagent.extension.instrumentation.internal.AsmApi; import io.opentelemetry.javaagent.tooling.config.EarlyInitAgentConfig; import java.io.File; import java.lang.instrument.ClassFileTransformer; @@ -67,6 +68,8 @@ public boolean delayStart() { @Override public void start() { + installTransformers(); + EarlyInitAgentConfig earlyConfig = EarlyInitAgentConfig.create(); extensionClassLoader = createExtensionClassLoader(getClass().getClassLoader(), earlyConfig); @@ -114,6 +117,14 @@ public void start() { } } + private void installTransformers() { + // prevents loading InetAddressResolverProvider SPI before agent has started + // https://github.com/open-telemetry/opentelemetry-java-instrumentation/issues/7130 + // https://github.com/open-telemetry/opentelemetry-java-instrumentation/issues/10921 + InetAddressClassFileTransformer transformer = new InetAddressClassFileTransformer(); + instrumentation.addTransformer(transformer, true); + } + @SuppressWarnings("SystemOut") private static void logUnrecognizedLoggerImplWarning(String loggerImplementationName) { System.err.println( @@ -151,7 +162,7 @@ public byte[] transform( ClassReader cr = new ClassReader(classfileBuffer); ClassWriter cw = new ClassWriter(cr, 0); ClassVisitor cv = - new ClassVisitor(Opcodes.ASM7, cw) { + new ClassVisitor(AsmApi.VERSION, cw) { @Override public MethodVisitor visitMethod( int access, String name, String descriptor, String signature, String[] exceptions) { @@ -179,4 +190,60 @@ public void visitCode() { return hookInserted ? cw.toByteArray() : null; } } + + private static class InetAddressClassFileTransformer implements ClassFileTransformer { + boolean hookInserted = false; + + @Override + public byte[] transform( + ClassLoader loader, + String className, + Class classBeingRedefined, + ProtectionDomain protectionDomain, + byte[] classfileBuffer) { + if (!"java/net/InetAddress".equals(className)) { + return null; + } + ClassReader cr = new ClassReader(classfileBuffer); + ClassWriter cw = new ClassWriter(cr, 0); + ClassVisitor cv = + new ClassVisitor(AsmApi.VERSION, cw) { + @Override + public MethodVisitor visitMethod( + int access, String name, String descriptor, String signature, String[] exceptions) { + MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions); + if (!"resolver".equals(name)) { + return mv; + } + return new MethodVisitor(api, mv) { + @Override + public void visitMethodInsn( + int opcode, + String ownerClassName, + String methodName, + String descriptor, + boolean isInterface) { + super.visitMethodInsn( + opcode, ownerClassName, methodName, descriptor, isInterface); + // rewrite Vm.isBooted() to AgentInitializer.isAgentStarted(Vm.isBooted()) + if ("jdk/internal/misc/VM".equals(ownerClassName) + && "isBooted".equals(methodName)) { + super.visitMethodInsn( + Opcodes.INVOKESTATIC, + Type.getInternalName(AgentInitializer.class), + "isAgentStarted", + "(Z)Z", + false); + hookInserted = true; + } + } + }; + } + }; + + cr.accept(cv, 0); + + return hookInserted ? cw.toByteArray() : null; + } + } } diff --git a/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/AutoVersionResourceProvider.java b/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/DistroVersionResourceProvider.java similarity index 53% rename from javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/AutoVersionResourceProvider.java rename to javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/DistroVersionResourceProvider.java index 4010f191da29..53fe1d65a5d1 100644 --- a/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/AutoVersionResourceProvider.java +++ b/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/DistroVersionResourceProvider.java @@ -5,23 +5,27 @@ package io.opentelemetry.javaagent.tooling; +import static io.opentelemetry.semconv.incubating.TelemetryIncubatingAttributes.TELEMETRY_DISTRO_NAME; +import static io.opentelemetry.semconv.incubating.TelemetryIncubatingAttributes.TELEMETRY_DISTRO_VERSION; + import com.google.auto.service.AutoService; -import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.common.Attributes; import io.opentelemetry.sdk.autoconfigure.spi.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"); +public class DistroVersionResourceProvider implements ResourceProvider { @Override public Resource createResource(ConfigProperties config) { return AgentVersion.VERSION == null ? Resource.empty() - : Resource.create(Attributes.of(TELEMETRY_AUTO_VERSION, AgentVersion.VERSION)); + : Resource.create( + Attributes.of( + TELEMETRY_DISTRO_NAME, + "opentelemetry-java-instrumentation", + TELEMETRY_DISTRO_VERSION, + AgentVersion.VERSION)); } } diff --git a/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/ExtensionClassLoader.java b/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/ExtensionClassLoader.java index 5015ce83fedc..43a77482838c 100644 --- a/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/ExtensionClassLoader.java +++ b/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/ExtensionClassLoader.java @@ -5,6 +5,7 @@ package io.opentelemetry.javaagent.tooling; +import io.opentelemetry.context.Context; import io.opentelemetry.javaagent.tooling.config.EarlyInitAgentConfig; import java.io.File; import java.io.FileOutputStream; @@ -62,10 +63,6 @@ public static ClassLoader getInstance( extensions.addAll(parseLocation(earlyConfig.getString(EXTENSIONS_CONFIG), javaagentFile)); - extensions.addAll( - parseLocation( - earlyConfig.getString("otel.javaagent.experimental.extensions"), javaagentFile)); - // TODO when logging is configured add warning about deprecated property if (extensions.isEmpty()) { @@ -168,8 +165,14 @@ private static boolean isJar(File f) { private static void addFileUrl(List result, File file) { try { - URL wrappedUrl = new URL("otel", null, -1, "/", new RemappingUrlStreamHandler(file)); - result.add(wrappedUrl); + // skip shading extension classes if opentelemetry-api is not shaded (happens when using + // disableShadowRelocate=true) + if (Context.class.getName().contains(".shaded.")) { + URL wrappedUrl = new URL("otel", null, -1, "/", new RemappingUrlStreamHandler(file)); + result.add(wrappedUrl); + } else { + result.add(file.toURI().toURL()); + } } catch (MalformedURLException ignored) { System.err.println("Ignoring " + file); } diff --git a/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/OpenTelemetryInstaller.java b/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/OpenTelemetryInstaller.java index 9b185d45dcd8..7b939c6f927c 100644 --- a/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/OpenTelemetryInstaller.java +++ b/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/OpenTelemetryInstaller.java @@ -5,8 +5,6 @@ package io.opentelemetry.javaagent.tooling; -import static java.util.Collections.singletonMap; - import io.opentelemetry.javaagent.bootstrap.OpenTelemetrySdkAccess; import io.opentelemetry.sdk.OpenTelemetrySdk; import io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdk; @@ -26,11 +24,8 @@ public static AutoConfiguredOpenTelemetrySdk installOpenTelemetrySdk( AutoConfiguredOpenTelemetrySdk autoConfiguredSdk = AutoConfiguredOpenTelemetrySdk.builder() - .setResultAsGlobal(true) + .setResultAsGlobal() .setServiceClassLoader(extensionClassLoader) - // disable the logs exporter by default for the time being - // FIXME remove this in the 2.x branch - .addPropertiesSupplier(() -> singletonMap("otel.logs.exporter", "none")) .build(); OpenTelemetrySdk sdk = autoConfiguredSdk.getOpenTelemetrySdk(); diff --git a/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/RemappingUrlConnection.java b/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/RemappingUrlConnection.java index 8d298b86a0eb..a2eb298ab53e 100644 --- a/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/RemappingUrlConnection.java +++ b/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/RemappingUrlConnection.java @@ -36,8 +36,11 @@ public class RemappingUrlConnection extends URLConnection { "#io.opentelemetry.semconv", "#io.opentelemetry.javaagent.shaded.io.opentelemetry.semconv"), rule( - "#io.opentelemetry.extension.aws", - "#io.opentelemetry.javaagent.shaded.io.opentelemetry.extension.aws"), + "#io.opentelemetry.contrib.awsxray", + "#io.opentelemetry.javaagent.shaded.io.opentelemetry.contrib.awsxray"), + rule( + "#io.opentelemetry.extension.kotlin", + "#io.opentelemetry.javaagent.shaded.io.opentelemetry.extension.kotlin"), rule("#application.io.opentelemetry", "#io.opentelemetry"), rule("#java.util.logging.Logger", "#io.opentelemetry.javaagent.bootstrap.PatchLogger")); diff --git a/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/bytebuddy/ExceptionHandlers.java b/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/bytebuddy/ExceptionHandlers.java index 0ceb02bfd383..704fec0e2950 100644 --- a/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/bytebuddy/ExceptionHandlers.java +++ b/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/bytebuddy/ExceptionHandlers.java @@ -75,6 +75,9 @@ public StackManipulation.Size apply(MethodVisitor mv, Implementation.Context con mv.visitLabel(handlerExit); if (frames) { mv.visitFrame(Opcodes.F_SAME, 0, null, 0, null); + // there may be at most one frame at given code location, we need to add an extra + // NOP instruction to ensure that there isn't a duplicate frame + mv.visitInsn(Opcodes.NOP); } return size; diff --git a/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/config/AgentConfig.java b/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/config/AgentConfig.java index 3077b56536c0..66ac96401612 100644 --- a/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/config/AgentConfig.java +++ b/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/config/AgentConfig.java @@ -11,20 +11,14 @@ public final class AgentConfig { public static boolean isInstrumentationEnabled( ConfigProperties config, Iterable instrumentationNames, 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 + ".enabled"; - boolean enabled = config.getBoolean(propertyName, defaultEnabled); - - if (defaultEnabled) { - anyEnabled &= enabled; - } else { - anyEnabled |= enabled; + Boolean enabled = config.getBoolean(propertyName); + if (enabled != null) { + return enabled; } } - return anyEnabled; + return defaultEnabled; } public static boolean isDebugModeEnabled(ConfigProperties config) { diff --git a/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/config/ConfigPropertiesBridge.java b/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/config/ConfigPropertiesBridge.java index c690921de4a8..de6b3baca6b3 100644 --- a/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/config/ConfigPropertiesBridge.java +++ b/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/config/ConfigPropertiesBridge.java @@ -5,7 +5,7 @@ package io.opentelemetry.javaagent.tooling.config; -import io.opentelemetry.javaagent.bootstrap.internal.InstrumentationConfig; +import io.opentelemetry.instrumentation.api.incubator.config.internal.InstrumentationConfig; import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; import io.opentelemetry.sdk.autoconfigure.spi.ConfigurationException; import java.time.Duration; @@ -13,7 +13,7 @@ import java.util.Map; import javax.annotation.Nullable; -public final class ConfigPropertiesBridge extends InstrumentationConfig { +public final class ConfigPropertiesBridge implements InstrumentationConfig { private final ConfigProperties configProperties; diff --git a/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/config/MethodsConfigurationParser.java b/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/config/MethodsConfigurationParser.java index a834d06361ce..44658f6e5672 100644 --- a/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/config/MethodsConfigurationParser.java +++ b/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/config/MethodsConfigurationParser.java @@ -18,18 +18,10 @@ public final class MethodsConfigurationParser { private static final Logger logger = Logger.getLogger(MethodsConfigurationParser.class.getName()); - 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 PACKAGE_CLASS_NAME_REGEX = "[\\w.$]+"; + private static final String METHOD_LIST_REGEX = "(?:\\s*\\w+\\s*,)*+(?:\\s*\\w+)?\\s*"; private static final String CONFIG_FORMAT = - "(?:\\s*" - + PACKAGE_CLASS_NAME_REGEX - + "\\[" - + METHOD_LIST_REGEX - + "]\\s*;)*\\s*" - + PACKAGE_CLASS_NAME_REGEX - + "\\[" - + METHOD_LIST_REGEX - + "]"; + PACKAGE_CLASS_NAME_REGEX + "(?:\\[" + METHOD_LIST_REGEX + "])?"; /** * This method takes a string in a form of {@code @@ -54,6 +46,10 @@ public static Map> parse(String configString) { if (classMethod.trim().isEmpty()) { continue; } + if (!classMethod.contains("[")) { + toTrace.put(classMethod.trim(), Collections.emptySet()); + continue; + } String[] splitClassMethod = classMethod.split("\\[", -1); String className = splitClassMethod[0]; String method = splitClassMethod[1].trim(); diff --git a/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/config/OtlpProtocolPropertiesSupplier.java b/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/config/OtlpProtocolPropertiesSupplier.java new file mode 100644 index 000000000000..ca624f1f7e5e --- /dev/null +++ b/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/config/OtlpProtocolPropertiesSupplier.java @@ -0,0 +1,27 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.tooling.config; + +import com.google.auto.service.AutoService; +import io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizer; +import io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizerProvider; +import java.util.Collections; + +@AutoService(AutoConfigurationCustomizerProvider.class) +public class OtlpProtocolPropertiesSupplier implements AutoConfigurationCustomizerProvider { + + @Override + public void customize(AutoConfigurationCustomizer autoConfigurationCustomizer) { + autoConfigurationCustomizer.addPropertiesSupplier( + () -> Collections.singletonMap("otel.exporter.otlp.protocol", "http/protobuf")); + } + + @Override + public int order() { + // make sure it runs BEFORE all the user-provided customizers + return Integer.MIN_VALUE; + } +} diff --git a/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/field/FieldBackedImplementationInstaller.java b/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/field/FieldBackedImplementationInstaller.java index e371badeaff5..5c1e69063fd3 100644 --- a/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/field/FieldBackedImplementationInstaller.java +++ b/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/field/FieldBackedImplementationInstaller.java @@ -14,7 +14,7 @@ import com.google.errorprone.annotations.CanIgnoreReturnValue; import io.opentelemetry.javaagent.bootstrap.InstrumentationHolder; import io.opentelemetry.javaagent.bootstrap.VirtualFieldDetector; -import io.opentelemetry.javaagent.bootstrap.internal.InstrumentationConfig; +import io.opentelemetry.javaagent.bootstrap.internal.AgentInstrumentationConfig; import io.opentelemetry.javaagent.tooling.HelperInjector; import io.opentelemetry.javaagent.tooling.TransformSafeLogger; import io.opentelemetry.javaagent.tooling.instrumentation.InstrumentationModuleInstaller; @@ -64,7 +64,7 @@ final class FieldBackedImplementationInstaller implements VirtualFieldImplementa TransformSafeLogger.getLogger(FieldBackedImplementationInstaller.class); private static final boolean FIELD_INJECTION_ENABLED = - InstrumentationConfig.get() + AgentInstrumentationConfig.get() .getBoolean("otel.javaagent.experimental.field-injection.enabled", true); private final Class instrumenterClass; @@ -111,6 +111,14 @@ public AgentBuilder.Identified.Extendable rewriteVirtualFieldsCalls( getTransformerForAsmVisitor( new VirtualFieldFindRewriter( instrumenterClass, virtualFieldMappings, virtualFieldImplementations))); + } + return builder; + } + + @Override + public AgentBuilder.Identified.Extendable injectHelperClasses( + AgentBuilder.Identified.Extendable builder) { + if (!virtualFieldMappings.isEmpty()) { builder = injectHelpersIntoBootstrapClassloader(builder); } return builder; diff --git a/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/field/NoopVirtualFieldImplementationInstaller.java b/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/field/NoopVirtualFieldImplementationInstaller.java index 1e8dd15df1e6..8eb33ec44685 100644 --- a/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/field/NoopVirtualFieldImplementationInstaller.java +++ b/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/field/NoopVirtualFieldImplementationInstaller.java @@ -21,6 +21,12 @@ public Extendable rewriteVirtualFieldsCalls(Extendable builder) { return builder; } + @Override + @CanIgnoreReturnValue + public Extendable injectHelperClasses(Extendable builder) { + return builder; + } + @Override @CanIgnoreReturnValue public Extendable injectFields(Extendable builder) { diff --git a/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/field/RealFieldInjector.java b/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/field/RealFieldInjector.java index 60e14b57fbc0..f58d4201efb4 100644 --- a/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/field/RealFieldInjector.java +++ b/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/field/RealFieldInjector.java @@ -11,6 +11,7 @@ import com.google.errorprone.annotations.CanIgnoreReturnValue; import io.opentelemetry.javaagent.bootstrap.VirtualFieldInstalledMarker; +import io.opentelemetry.javaagent.extension.instrumentation.internal.AsmApi; import io.opentelemetry.javaagent.tooling.Utils; import java.util.Arrays; import java.util.LinkedHashSet; @@ -66,7 +67,7 @@ public ClassVisitor wrap( int writerFlags, int readerFlags) { - return new ClassVisitor(Opcodes.ASM7, classVisitor) { + return new ClassVisitor(AsmApi.VERSION, classVisitor) { // We are using Object class name instead of fieldTypeName here because this gets // injected onto the bootstrap class loader where context class may be unavailable private final TypeDescription fieldType = TypeDescription.ForLoadedType.of(Object.class); diff --git a/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/field/RuntimeFieldBasedImplementationSupplier.java b/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/field/RuntimeFieldBasedImplementationSupplier.java index 66cf8ccc3e44..47cc9e4f1543 100644 --- a/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/field/RuntimeFieldBasedImplementationSupplier.java +++ b/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/field/RuntimeFieldBasedImplementationSupplier.java @@ -11,6 +11,7 @@ import io.opentelemetry.instrumentation.api.util.VirtualField; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; +import java.security.PrivilegedAction; final class RuntimeFieldBasedImplementationSupplier implements RuntimeVirtualFieldSupplier.VirtualFieldSupplier { @@ -18,9 +19,18 @@ final class RuntimeFieldBasedImplementationSupplier @Override public VirtualField find( Class type, Class fieldType) { + if (System.getSecurityManager() == null) { + return findInternal(type, fieldType); + } + return java.security.AccessController.doPrivileged( + (PrivilegedAction>) () -> findInternal(type, fieldType)); + } + + private static VirtualField findInternal( + Class type, Class fieldType) { try { String virtualFieldImplClassName = - getVirtualFieldImplementationClassName(type.getName(), fieldType.getName()); + getVirtualFieldImplementationClassName(type.getTypeName(), fieldType.getTypeName()); Class contextStoreClass = Class.forName(virtualFieldImplClassName, false, null); Method method = contextStoreClass.getMethod("getVirtualField", Class.class, Class.class); @SuppressWarnings("unchecked") diff --git a/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/field/VirtualFieldFindRewriter.java b/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/field/VirtualFieldFindRewriter.java index a7a7446eca72..ea1f2c5e5248 100644 --- a/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/field/VirtualFieldFindRewriter.java +++ b/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/field/VirtualFieldFindRewriter.java @@ -9,6 +9,7 @@ import com.google.errorprone.annotations.CanIgnoreReturnValue; import io.opentelemetry.instrumentation.api.util.VirtualField; +import io.opentelemetry.javaagent.extension.instrumentation.internal.AsmApi; import io.opentelemetry.javaagent.tooling.TransformSafeLogger; import io.opentelemetry.javaagent.tooling.Utils; import io.opentelemetry.javaagent.tooling.muzzle.VirtualFieldMappings; @@ -81,12 +82,12 @@ public ClassVisitor wrap( int writerFlags, int readerFlags) { - return new ClassVisitor(Opcodes.ASM7, classVisitor) { + return new ClassVisitor(AsmApi.VERSION, 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) { + return new MethodVisitor(api, mv) { /** The most recent objects pushed to the stack. */ private final Object[] stack = {null, null}; diff --git a/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/field/VirtualFieldImplementationInstaller.java b/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/field/VirtualFieldImplementationInstaller.java index abb8beb98bc4..84458a74b207 100644 --- a/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/field/VirtualFieldImplementationInstaller.java +++ b/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/field/VirtualFieldImplementationInstaller.java @@ -12,12 +12,17 @@ public interface VirtualFieldImplementationInstaller { /** * Rewrites {@link VirtualField#find(Class, Class)} so that they return the real implementation, - * generated by this class. Injects helper classes required to make this the actual {@link - * VirtualField} implementations work. + * generated by this class. */ AgentBuilder.Identified.Extendable rewriteVirtualFieldsCalls( AgentBuilder.Identified.Extendable builder); + /** + * Injects helper classes required to make the actual {@link VirtualField} implementations work. + */ + AgentBuilder.Identified.Extendable injectHelperClasses( + AgentBuilder.Identified.Extendable builder); + /** Injects actual fields in classes referenced by {@link VirtualField} usages. */ AgentBuilder.Identified.Extendable injectFields(AgentBuilder.Identified.Extendable builder); } diff --git a/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/field/VirtualFieldImplementationsGenerator.java b/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/field/VirtualFieldImplementationsGenerator.java index 46c77d1878ba..46f73f4e1a43 100644 --- a/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/field/VirtualFieldImplementationsGenerator.java +++ b/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/field/VirtualFieldImplementationsGenerator.java @@ -12,6 +12,7 @@ import com.google.errorprone.annotations.CanIgnoreReturnValue; import io.opentelemetry.instrumentation.api.internal.cache.Cache; import io.opentelemetry.instrumentation.api.util.VirtualField; +import io.opentelemetry.javaagent.extension.instrumentation.internal.AsmApi; import io.opentelemetry.javaagent.tooling.Utils; import io.opentelemetry.javaagent.tooling.muzzle.VirtualFieldMappings; import java.util.HashMap; @@ -109,7 +110,7 @@ public ClassVisitor wrap( MethodList methods, int writerFlags, int readerFlags) { - return new ClassVisitor(Opcodes.ASM7, classVisitor) { + return new ClassVisitor(AsmApi.VERSION, classVisitor) { private final TypeDescription accessorInterface = fieldAccessorInterfaces.find(typeName, fieldTypeName); diff --git a/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/ignore/AdditionalLibraryIgnoredTypesConfigurer.java b/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/ignore/AdditionalLibraryIgnoredTypesConfigurer.java index 2a063a1f15b7..d326d56e6e02 100644 --- a/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/ignore/AdditionalLibraryIgnoredTypesConfigurer.java +++ b/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/ignore/AdditionalLibraryIgnoredTypesConfigurer.java @@ -47,7 +47,7 @@ public void configure(IgnoredTypesBuilder builder) { .ignoreClass("org.apache.tartarus.") .ignoreClass("org.json.simple.") .ignoreClass("org.yaml.snakeyaml.") - .allowClass("org.apache.lucene.util.bkd.BKDWriter$OneDimensionBKDWriter$$Lambda$"); + .allowClass("org.apache.lucene.util.bkd.BKDWriter$OneDimensionBKDWriter$$Lambda"); builder.ignoreClass("net.sf.cglib.").allowClass("net.sf.cglib.core.internal.LoadingCache$2"); @@ -84,12 +84,12 @@ public void configure(IgnoredTypesBuilder builder) { .allowClass("org.springframework.data.convert.ClassGeneratingEntityInstantiator$") .allowClass("org.springframework.data.jpa.repository.config.InspectionClassLoader") .allowClass( - "org.springframework.data.jpa.repository.query.QueryParameterSetter$NamedOrIndexedQueryParameterSetter$$Lambda$"); + "org.springframework.data.jpa.repository.query.QueryParameterSetter$NamedOrIndexedQueryParameterSetter$$Lambda"); builder .ignoreClass("org.springframework.amqp.") .allowClass("org.springframework.amqp.rabbit.connection.") - .allowClass("org.springframework.amqp.rabbit.core.RabbitTemplate$$Lambda$") + .allowClass("org.springframework.amqp.rabbit.core.RabbitTemplate$$Lambda") .allowClass("org.springframework.amqp.rabbit.listener.AbstractMessageListenerContainer"); builder @@ -103,16 +103,18 @@ public void configure(IgnoredTypesBuilder builder) { .allowClass("org.springframework.boot.logging.logback.") .allowClass("org.springframework.boot.web.filter.") .allowClass("org.springframework.boot.web.servlet.") + .allowClass( + "org.springframework.boot.actuate.metrics.web.reactive.server.MetricsWebFilter$$Lambda") .allowClass("org.springframework.boot.autoconfigure.BackgroundPreinitializer$") .allowClass( - "org.springframework.boot.autoconfigure.cassandra.CassandraAutoConfiguration$$Lambda$") + "org.springframework.boot.autoconfigure.cassandra.CassandraAutoConfiguration$$Lambda") .allowClass("org.springframework.boot.autoconfigure.condition.OnClassCondition$") .allowClass( - "org.springframework.boot.autoconfigure.web.ResourceProperties$Cache$Cachecontrol$$Lambda$") + "org.springframework.boot.autoconfigure.web.ResourceProperties$Cache$Cachecontrol$$Lambda") .allowClass( - "org.springframework.boot.autoconfigure.web.WebProperties$Resources$Cache$Cachecontrol$$Lambda$") + "org.springframework.boot.autoconfigure.web.WebProperties$Resources$Cache$Cachecontrol$$Lambda") .allowClass("org.springframework.boot.web.embedded.netty.NettyWebServer$") - .allowClass("org.springframework.boot.web.embedded.tomcat.TomcatEmbeddedContext$$Lambda$") + .allowClass("org.springframework.boot.web.embedded.tomcat.TomcatEmbeddedContext$$Lambda") .allowClass( "org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainer$") .allowClass( @@ -143,7 +145,7 @@ public void configure(IgnoredTypesBuilder builder) { // More runnables to deal with .allowClass("org.springframework.context.support.AbstractApplicationContext$") .allowClass("org.springframework.context.support.ContextTypeMatchClassLoader") - .allowClass("org.springframework.context.support.DefaultLifecycleProcessor$$Lambda$") + .allowClass("org.springframework.context.support.DefaultLifecycleProcessor$$Lambda") // Allow instrumenting ApplicationContext implementations - to inject beans .allowClass("org.springframework.context.annotation.AnnotationConfigApplicationContext") .allowClass("org.springframework.context.support.AbstractApplicationContext") @@ -154,7 +156,8 @@ public void configure(IgnoredTypesBuilder builder) { .allowClass("org.springframework.core.task.") .allowClass("org.springframework.core.DecoratingClassLoader") .allowClass("org.springframework.core.OverridingClassLoader") - .allowClass("org.springframework.core.ReactiveAdapterRegistry$EmptyCompletableFuture"); + .allowClass("org.springframework.core.ReactiveAdapterRegistry$EmptyCompletableFuture") + .allowClass("org.springframework.core.io.buffer.DataBufferUtils$"); builder .ignoreClass("org.springframework.instrument.") @@ -163,9 +166,9 @@ public void configure(IgnoredTypesBuilder builder) { builder .ignoreClass("org.springframework.http.") - .allowClass("org.springframework.http.client.reactive.AbstractClientHttpRequest$$Lambda$") - .allowClass("org.springframework.http.client.reactive.ReactorClientHttpConnector$$Lambda$") - .allowClass("org.springframework.http.codec.multipart.FileStorage$TempFileStorage$$Lambda$") + .allowClass("org.springframework.http.client.reactive.AbstractClientHttpRequest$$Lambda") + .allowClass("org.springframework.http.client.reactive.ReactorClientHttpConnector$$Lambda") + .allowClass("org.springframework.http.codec.multipart.FileStorage$TempFileStorage$$Lambda") // There are some Mono implementation that get instrumented .allowClass("org.springframework.http.server.reactive."); @@ -173,7 +176,8 @@ public void configure(IgnoredTypesBuilder builder) { .ignoreClass("org.springframework.jms.") .allowClass("org.springframework.jms.listener.") .allowClass( - "org.springframework.jms.config.JmsListenerEndpointRegistry$AggregatingCallback"); + "org.springframework.jms.config.JmsListenerEndpointRegistry$AggregatingCallback") + .allowClass("org.springframework.jms.support.destination.JmsDestinationAccessor"); builder .ignoreClass("org.springframework.util.") @@ -239,7 +243,7 @@ public void configure(IgnoredTypesBuilder builder) { .ignoreClass("com.google.common.") .allowClass("com.google.common.util.concurrent.") .allowClass("com.google.common.base.internal.Finalizer") - .allowClass("com.google.common.base.Java8Usage$$Lambda$"); + .allowClass("com.google.common.base.Java8Usage$$Lambda"); builder .ignoreClass("com.google.inject.") @@ -271,6 +275,6 @@ public void configure(IgnoredTypesBuilder builder) { .allowClass("com.fasterxml.jackson.databind.util.internal.PrivateMaxEntriesMap$AddTask"); // kotlin, note we do not ignore kotlinx because we instrument coroutines code - builder.ignoreClass("kotlin.").allowClass("kotlin.coroutines.jvm.internal.DebugProbesKt"); + builder.ignoreClass("kotlin."); } } diff --git a/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/ignore/CommonLibraryIgnoredTypesConfigurer.java b/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/ignore/CommonLibraryIgnoredTypesConfigurer.java new file mode 100644 index 000000000000..1bcacdf2b816 --- /dev/null +++ b/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/ignore/CommonLibraryIgnoredTypesConfigurer.java @@ -0,0 +1,25 @@ +/* + * 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.javaagent.extension.ignore.IgnoredTypesBuilder; +import io.opentelemetry.javaagent.extension.ignore.IgnoredTypesConfigurer; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; + +/** + * Unlike the {@link AdditionalLibraryIgnoredTypesConfigurer}, this one is applied to all tests. It + * should only contain classes that are included in the most commonly used libraries in test (e.g. + * Spring Boot). + */ +@AutoService(IgnoredTypesConfigurer.class) +public class CommonLibraryIgnoredTypesConfigurer implements IgnoredTypesConfigurer { + + @Override + public void configure(IgnoredTypesBuilder builder, ConfigProperties config) { + builder.ignoreClass("org.springframework.boot.autoconfigure.ssl.FileWatcher$WatcherThread"); + } +} diff --git a/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/ignore/GlobalIgnoredTypesConfigurer.java b/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/ignore/GlobalIgnoredTypesConfigurer.java index 3787030cfe18..9abe23ce5f50 100644 --- a/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/ignore/GlobalIgnoredTypesConfigurer.java +++ b/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/ignore/GlobalIgnoredTypesConfigurer.java @@ -10,6 +10,7 @@ import io.opentelemetry.javaagent.extension.ignore.IgnoredTypesBuilder; import io.opentelemetry.javaagent.extension.ignore.IgnoredTypesConfigurer; import io.opentelemetry.javaagent.tooling.ExtensionClassLoader; +import io.opentelemetry.javaagent.tooling.instrumentation.indy.InstrumentationModuleClassLoader; import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; @AutoService(IgnoredTypesConfigurer.class) @@ -75,6 +76,7 @@ private static void configureIgnoredTypes(IgnoredTypesBuilder builder) { // java.lang.ClassCircularityError: java/lang/ClassLoader$1 // when SecurityManager is enabled. ClassLoader$1 is used in ClassLoader.checkPackageAccess .ignoreClass("java.lang.ClassLoader$") + .allowClass("java.lang.VirtualThread") .allowClass("java.lang.invoke.InnerClassLambdaMetafactory") // Concurrent instrumentation modifies the structure of // Cleaner class incompatibly with java9+ modules. @@ -123,8 +125,10 @@ private static void configureIgnoredClassLoaders(IgnoredTypesBuilder builder) { .ignoreClassLoader("org.openjdk.nashorn.internal.runtime.ScriptLoader") .ignoreClassLoader("org.codehaus.janino.ByteArrayClassLoader") .ignoreClassLoader("org.eclipse.persistence.internal.jaxb.JaxbClassLoader") + .ignoreClassLoader("com.alibaba.fastjson.util.ASMClassLoader") .ignoreClassLoader(AgentClassLoader.class.getName()) - .ignoreClassLoader(ExtensionClassLoader.class.getName()); + .ignoreClassLoader(ExtensionClassLoader.class.getName()) + .ignoreClassLoader(InstrumentationModuleClassLoader.class.getName()); builder .ignoreClassLoader("datadog.") diff --git a/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/ignore/IgnoredTypesMatcher.java b/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/ignore/IgnoredTypesMatcher.java index 9fdf09356df8..f199641a48f3 100644 --- a/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/ignore/IgnoredTypesMatcher.java +++ b/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/ignore/IgnoredTypesMatcher.java @@ -29,9 +29,9 @@ public boolean matches(TypeDescription target) { } // bytecode proxies typically have $$ in their name - if (name.contains("$$") && !name.contains("$$Lambda$")) { + if (name.contains("$$") && !name.contains("$$Lambda$") && !name.endsWith("$$Lambda")) { // allow scala anonymous classes - return !name.contains("$$anon$"); + return !name.contains("$$anon$") && !name.contains("$$anonfun$"); } if (name.contains("$JaxbAccessor") diff --git a/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/ignore/UserExcludedClassLoadersConfigurer.java b/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/ignore/UserExcludedClassLoadersConfigurer.java new file mode 100644 index 000000000000..d2d386ca4f7e --- /dev/null +++ b/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/ignore/UserExcludedClassLoadersConfigurer.java @@ -0,0 +1,34 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.tooling.ignore; + +import static java.util.Collections.emptyList; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.ignore.IgnoredTypesBuilder; +import io.opentelemetry.javaagent.extension.ignore.IgnoredTypesConfigurer; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import java.util.List; + +@AutoService(IgnoredTypesConfigurer.class) +public class UserExcludedClassLoadersConfigurer implements IgnoredTypesConfigurer { + + // visible for tests + static final String EXCLUDED_CLASS_LOADERS_CONFIG = "otel.javaagent.exclude-class-loaders"; + + @Override + public void configure(IgnoredTypesBuilder builder, ConfigProperties config) { + List excludedClassLoaders = config.getList(EXCLUDED_CLASS_LOADERS_CONFIG, emptyList()); + for (String excludedClassLoader : excludedClassLoaders) { + excludedClassLoader = excludedClassLoader.trim(); + // remove the trailing * + if (excludedClassLoader.endsWith("*")) { + excludedClassLoader = excludedClassLoader.substring(0, excludedClassLoader.length() - 1); + } + builder.ignoreClassLoader(excludedClassLoader); + } + } +} diff --git a/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/instrumentation/InstrumentationModuleInstaller.java b/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/instrumentation/InstrumentationModuleInstaller.java index 981b3345a1ab..b4ea4b948639 100644 --- a/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/instrumentation/InstrumentationModuleInstaller.java +++ b/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/instrumentation/InstrumentationModuleInstaller.java @@ -13,6 +13,9 @@ import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.internal.ExperimentalInstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.internal.injection.InjectionMode; +import io.opentelemetry.javaagent.tooling.HelperClassDefinition; import io.opentelemetry.javaagent.tooling.HelperInjector; import io.opentelemetry.javaagent.tooling.TransformSafeLogger; import io.opentelemetry.javaagent.tooling.Utils; @@ -20,13 +23,20 @@ import io.opentelemetry.javaagent.tooling.config.AgentConfig; import io.opentelemetry.javaagent.tooling.field.VirtualFieldImplementationInstaller; import io.opentelemetry.javaagent.tooling.field.VirtualFieldImplementationInstallerFactory; +import io.opentelemetry.javaagent.tooling.instrumentation.indy.ClassInjectorImpl; +import io.opentelemetry.javaagent.tooling.instrumentation.indy.IndyModuleRegistry; +import io.opentelemetry.javaagent.tooling.instrumentation.indy.IndyTypeTransformerImpl; +import io.opentelemetry.javaagent.tooling.instrumentation.indy.PatchByteCodeVersionTransformer; import io.opentelemetry.javaagent.tooling.muzzle.HelperResourceBuilderImpl; import io.opentelemetry.javaagent.tooling.muzzle.InstrumentationModuleMuzzle; import io.opentelemetry.javaagent.tooling.util.IgnoreFailedTypeMatcher; import io.opentelemetry.javaagent.tooling.util.NamedMatcher; import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; import java.lang.instrument.Instrumentation; +import java.util.ArrayList; +import java.util.Collections; import java.util.List; +import java.util.function.Function; import net.bytebuddy.agent.builder.AgentBuilder; import net.bytebuddy.description.annotation.AnnotationSource; import net.bytebuddy.description.type.TypeDescription; @@ -62,6 +72,109 @@ AgentBuilder install( FINE, "Instrumentation {0} is disabled", instrumentationModule.instrumentationName()); return parentAgentBuilder; } + + if (instrumentationModule.isIndyModule()) { + return installIndyModule(instrumentationModule, parentAgentBuilder, config); + } else { + return installInjectingModule(instrumentationModule, parentAgentBuilder, config); + } + } + + private AgentBuilder installIndyModule( + InstrumentationModule instrumentationModule, + AgentBuilder parentAgentBuilder, + ConfigProperties config) { + List helperClassNames = + InstrumentationModuleMuzzle.getHelperClassNames(instrumentationModule); + HelperResourceBuilderImpl helperResourceBuilder = new HelperResourceBuilderImpl(); + instrumentationModule.registerHelperResources(helperResourceBuilder); + List typeInstrumentations = instrumentationModule.typeInstrumentations(); + if (typeInstrumentations.isEmpty()) { + if (!helperClassNames.isEmpty() || !helperResourceBuilder.getResources().isEmpty()) { + logger.log( + WARNING, + "Helper classes and resources won't be injected if no types are instrumented: {0}", + instrumentationModule.instrumentationName()); + } + + return parentAgentBuilder; + } + + List injectedHelperClassNames; + if (instrumentationModule instanceof ExperimentalInstrumentationModule) { + ExperimentalInstrumentationModule experimentalInstrumentationModule = + (ExperimentalInstrumentationModule) instrumentationModule; + injectedHelperClassNames = experimentalInstrumentationModule.injectedClassNames(); + } else { + injectedHelperClassNames = Collections.emptyList(); + } + + ClassInjectorImpl injectedClassesCollector = new ClassInjectorImpl(instrumentationModule); + if (instrumentationModule instanceof ExperimentalInstrumentationModule) { + ((ExperimentalInstrumentationModule) instrumentationModule) + .injectClasses(injectedClassesCollector); + } + + MuzzleMatcher muzzleMatcher = new MuzzleMatcher(logger, instrumentationModule, config); + + Function> helperGenerator = + cl -> { + List helpers = + new ArrayList<>(injectedClassesCollector.getClassesToInject(cl)); + for (String helperName : injectedHelperClassNames) { + helpers.add( + HelperClassDefinition.create( + helperName, + instrumentationModule.getClass().getClassLoader(), + InjectionMode.CLASS_ONLY)); + } + return helpers; + }; + + AgentBuilder.Transformer helperInjector = + new HelperInjector( + instrumentationModule.instrumentationName(), + helperGenerator, + helperResourceBuilder.getResources(), + instrumentationModule.getClass().getClassLoader(), + instrumentation); + + VirtualFieldImplementationInstaller contextProvider = + virtualFieldInstallerFactory.create(instrumentationModule); + + AgentBuilder agentBuilder = parentAgentBuilder; + for (TypeInstrumentation typeInstrumentation : instrumentationModule.typeInstrumentations()) { + AgentBuilder.Identified.Extendable extendableAgentBuilder = + setTypeMatcher(agentBuilder, instrumentationModule, typeInstrumentation) + .and(muzzleMatcher) + .transform(new PatchByteCodeVersionTransformer()); + + // TODO (Jonas): we are not calling + // contextProvider.rewriteVirtualFieldsCalls(extendableAgentBuilder) anymore + // As a result the advices should store `VirtualFields` as static variables instead of having + // the lookup inline + // We need to update our documentation on that + extendableAgentBuilder = + IndyModuleRegistry.initializeModuleLoaderOnMatch( + instrumentationModule, extendableAgentBuilder); + extendableAgentBuilder = extendableAgentBuilder.transform(helperInjector); + extendableAgentBuilder = contextProvider.injectHelperClasses(extendableAgentBuilder); + IndyTypeTransformerImpl typeTransformer = + new IndyTypeTransformerImpl(extendableAgentBuilder, instrumentationModule); + typeInstrumentation.transform(typeTransformer); + extendableAgentBuilder = typeTransformer.getAgentBuilder(); + // TODO (Jonas): make instrumentation of bytecode older than 1.4 opt-in via a config option + extendableAgentBuilder = contextProvider.injectFields(extendableAgentBuilder); + + agentBuilder = extendableAgentBuilder; + } + return agentBuilder; + } + + private AgentBuilder installInjectingModule( + InstrumentationModule instrumentationModule, + AgentBuilder parentAgentBuilder, + ConfigProperties config) { List helperClassNames = InstrumentationModuleMuzzle.getHelperClassNames(instrumentationModule); HelperResourceBuilderImpl helperResourceBuilder = new HelperResourceBuilderImpl(); @@ -78,8 +191,6 @@ AgentBuilder install( return parentAgentBuilder; } - ElementMatcher.Junction moduleClassLoaderMatcher = - instrumentationModule.classLoaderMatcher(); MuzzleMatcher muzzleMatcher = new MuzzleMatcher(logger, instrumentationModule, config); AgentBuilder.Transformer helperInjector = new HelperInjector( @@ -93,35 +204,13 @@ AgentBuilder install( AgentBuilder agentBuilder = parentAgentBuilder; for (TypeInstrumentation typeInstrumentation : typeInstrumentations) { - ElementMatcher typeMatcher = - new NamedMatcher<>( - instrumentationModule.getClass().getSimpleName() - + "#" - + typeInstrumentation.getClass().getSimpleName(), - new IgnoreFailedTypeMatcher(typeInstrumentation.typeMatcher())); - ElementMatcher classLoaderMatcher = - new NamedMatcher<>( - instrumentationModule.getClass().getSimpleName() - + "#" - + typeInstrumentation.getClass().getSimpleName(), - moduleClassLoaderMatcher.and(typeInstrumentation.classLoaderOptimization())); AgentBuilder.Identified.Extendable extendableAgentBuilder = - agentBuilder - .type( - new LoggingFailSafeMatcher<>( - typeMatcher, - "Instrumentation type matcher unexpected exception: " + typeMatcher), - new LoggingFailSafeMatcher<>( - classLoaderMatcher, - "Instrumentation class loader matcher unexpected exception: " - + classLoaderMatcher)) - .and( - (typeDescription, classLoader, module, classBeingRedefined, protectionDomain) -> - classLoader == null || NOT_DECORATOR_MATCHER.matches(typeDescription)) + setTypeMatcher(agentBuilder, instrumentationModule, typeInstrumentation) .and(muzzleMatcher) .transform(ConstantAdjuster.instance()) .transform(helperInjector); + extendableAgentBuilder = contextProvider.injectHelperClasses(extendableAgentBuilder); extendableAgentBuilder = contextProvider.rewriteVirtualFieldsCalls(extendableAgentBuilder); TypeTransformerImpl typeTransformer = new TypeTransformerImpl(extendableAgentBuilder); typeInstrumentation.transform(typeTransformer); @@ -133,4 +222,37 @@ AgentBuilder install( return agentBuilder; } + + private static AgentBuilder.Identified.Narrowable setTypeMatcher( + AgentBuilder agentBuilder, + InstrumentationModule instrumentationModule, + TypeInstrumentation typeInstrumentation) { + + ElementMatcher.Junction moduleClassLoaderMatcher = + instrumentationModule.classLoaderMatcher(); + + ElementMatcher typeMatcher = + new NamedMatcher<>( + instrumentationModule.getClass().getSimpleName() + + "#" + + typeInstrumentation.getClass().getSimpleName(), + new IgnoreFailedTypeMatcher(typeInstrumentation.typeMatcher())); + ElementMatcher classLoaderMatcher = + new NamedMatcher<>( + instrumentationModule.getClass().getSimpleName() + + "#" + + typeInstrumentation.getClass().getSimpleName(), + moduleClassLoaderMatcher.and(typeInstrumentation.classLoaderOptimization())); + + return agentBuilder + .type( + new LoggingFailSafeMatcher<>( + typeMatcher, "Instrumentation type matcher unexpected exception: " + typeMatcher), + new LoggingFailSafeMatcher<>( + classLoaderMatcher, + "Instrumentation class loader matcher unexpected exception: " + classLoaderMatcher)) + .and( + (typeDescription, classLoader, module, classBeingRedefined, protectionDomain) -> + classLoader == null || NOT_DECORATOR_MATCHER.matches(typeDescription)); + } } diff --git a/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/instrumentation/MuzzleMatcher.java b/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/instrumentation/MuzzleMatcher.java index df70f987722e..fd9f84924e5f 100644 --- a/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/instrumentation/MuzzleMatcher.java +++ b/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/instrumentation/MuzzleMatcher.java @@ -14,6 +14,8 @@ import io.opentelemetry.javaagent.tooling.TransformSafeLogger; import io.opentelemetry.javaagent.tooling.Utils; import io.opentelemetry.javaagent.tooling.config.AgentConfig; +import io.opentelemetry.javaagent.tooling.instrumentation.indy.IndyModuleRegistry; +import io.opentelemetry.javaagent.tooling.instrumentation.indy.InstrumentationModuleClassLoader; import io.opentelemetry.javaagent.tooling.muzzle.Mismatch; import io.opentelemetry.javaagent.tooling.muzzle.ReferenceMatcher; import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; @@ -61,7 +63,18 @@ public boolean matches( if (classLoader == BOOTSTRAP_LOADER) { classLoader = Utils.getBootstrapProxy(); } - return matchCache.computeIfAbsent(classLoader, this::doesMatch); + if (instrumentationModule.isIndyModule()) { + return matchCache.computeIfAbsent( + classLoader, + cl -> { + InstrumentationModuleClassLoader moduleCl = + IndyModuleRegistry.createInstrumentationClassLoaderWithoutRegistration( + instrumentationModule, cl); + return doesMatch(moduleCl); + }); + } else { + return matchCache.computeIfAbsent(classLoader, this::doesMatch); + } } private boolean doesMatch(ClassLoader classLoader) { diff --git a/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/instrumentation/TypeTransformerImpl.java b/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/instrumentation/TypeTransformerImpl.java index 1f1059f7e492..e3b6285d334b 100644 --- a/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/instrumentation/TypeTransformerImpl.java +++ b/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/instrumentation/TypeTransformerImpl.java @@ -8,15 +8,23 @@ import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; import io.opentelemetry.javaagent.tooling.Utils; import io.opentelemetry.javaagent.tooling.bytebuddy.ExceptionHandlers; +import io.opentelemetry.javaagent.tooling.instrumentation.indy.ForceDynamicallyTypedAssignReturnedFactory; import net.bytebuddy.agent.builder.AgentBuilder; +import net.bytebuddy.asm.Advice; import net.bytebuddy.description.method.MethodDescription; import net.bytebuddy.matcher.ElementMatcher; final class TypeTransformerImpl implements TypeTransformer { private AgentBuilder.Identified.Extendable agentBuilder; + private final Advice.WithCustomMapping adviceMapping; TypeTransformerImpl(AgentBuilder.Identified.Extendable agentBuilder) { this.agentBuilder = agentBuilder; + adviceMapping = + Advice.withCustomMapping() + .with( + new ForceDynamicallyTypedAssignReturnedFactory( + new Advice.AssignReturned.Factory().withSuppressed(Throwable.class))); } @Override @@ -24,7 +32,7 @@ public void applyAdviceToMethod( ElementMatcher methodMatcher, String adviceClassName) { agentBuilder = agentBuilder.transform( - new AgentBuilder.Transformer.ForAdvice() + new AgentBuilder.Transformer.ForAdvice(adviceMapping) .include( Utils.getBootstrapProxy(), Utils.getAgentClassLoader(), diff --git a/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/instrumentation/indy/AdviceSignatureEraser.java b/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/instrumentation/indy/AdviceSignatureEraser.java new file mode 100644 index 000000000000..429b93d11e1e --- /dev/null +++ b/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/instrumentation/indy/AdviceSignatureEraser.java @@ -0,0 +1,106 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.tooling.instrumentation.indy; + +import io.opentelemetry.javaagent.extension.instrumentation.internal.AsmApi; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import org.objectweb.asm.AnnotationVisitor; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.ClassWriter; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.tree.ClassNode; + +/** + * This transformer is a workaround to make the life of instrumentation developers easier. + * + *

      When inserting the instrumentation and calling the advice enter/exit methods, we insert an + * invokedynamic instruction to perform this method call. This instruction has a {@link + * java.lang.invoke.MethodType} associated, which normally corresponds to the method type of the + * called advice. This can however lead to problems, when the advice method signature contains types + * which are not visible to the instrumented class. + * + *

      To prevent this, we instead associate the invokedynamic instruction with a type where all + * class reference are replaced with references to {@link Object}. To lookup the correct advice + * method, we pass the original type as string argument to the invokedynamic bootstrapping method. + * + *

      Because bytebuddy currently doesn't support this customization, we perform the type erasure on + * the .class via ASM before bytebuddy parses the advice. In addition, we insert an annotation to + * preserve the original descriptor of the method. + */ +// TODO: replace this workaround when buddy natively supports altering the MethodType for +// invokedynamic advices +class AdviceSignatureEraser { + + private static final Pattern TYPE_REFERENCE_PATTERN = Pattern.compile("L[^;]+;"); + + public static final String ORIGNINAL_DESCRIPTOR_ANNOTATION_TYPE = + "L" + OriginalDescriptor.class.getName().replace('.', '/') + ";"; + + private AdviceSignatureEraser() {} + + static byte[] transform(byte[] bytes) { + ClassReader cr = new ClassReader(bytes); + ClassWriter cw = new ClassWriter(cr, 0); + ClassNode classNode = new ClassNode(); + cr.accept(classNode, 0); + + Set methodsToTransform = listAdviceMethods(classNode); + if (methodsToTransform.isEmpty()) { + return bytes; + } + + ClassVisitor cv = + new ClassVisitor(AsmApi.VERSION, cw) { + @Override + public MethodVisitor visitMethod( + int access, String name, String descriptor, String signature, String[] exceptions) { + if (methodsToTransform.contains(name + descriptor)) { + String erased = eraseTypes(descriptor); + MethodVisitor visitor = super.visitMethod(access, name, erased, null, exceptions); + AnnotationVisitor av = + visitor.visitAnnotation(ORIGNINAL_DESCRIPTOR_ANNOTATION_TYPE, false); + av.visit("value", descriptor); + av.visitEnd(); + return visitor; + } else { + return super.visitMethod(access, name, descriptor, signature, exceptions); + } + } + }; + classNode.accept(cv); + return cw.toByteArray(); + } + + private static String eraseTypes(String descriptor) { + Matcher matcher = TYPE_REFERENCE_PATTERN.matcher(descriptor); + StringBuffer result = new StringBuffer(); + while (matcher.find()) { + String reference = matcher.group(); + if (reference.startsWith("Ljava/")) { + // do not erase java.* references + matcher.appendReplacement(result, reference); + } else { + matcher.appendReplacement(result, "Ljava/lang/Object;"); + } + } + matcher.appendTail(result); + return result.toString(); + } + + private static Set listAdviceMethods(ClassNode classNode) { + return classNode.methods.stream() + .filter( + mn -> + AdviceTransformer.hasAnnotation(mn, AdviceTransformer.ADVICE_ON_METHOD_ENTER) + || AdviceTransformer.hasAnnotation(mn, AdviceTransformer.ADVICE_ON_METHOD_EXIT)) + .map(mn -> mn.name + mn.desc) + .collect(Collectors.toSet()); + } +} diff --git a/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/instrumentation/indy/AdviceTransformer.java b/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/instrumentation/indy/AdviceTransformer.java new file mode 100644 index 000000000000..ac1f46a99e3a --- /dev/null +++ b/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/instrumentation/indy/AdviceTransformer.java @@ -0,0 +1,897 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.tooling.instrumentation.indy; + +import io.opentelemetry.javaagent.extension.instrumentation.internal.AsmApi; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.implementation.bytecode.assign.Assigner; +import org.objectweb.asm.AnnotationVisitor; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.ClassWriter; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; +import org.objectweb.asm.commons.GeneratorAdapter; +import org.objectweb.asm.commons.Method; +import org.objectweb.asm.tree.AnnotationNode; +import org.objectweb.asm.tree.ClassNode; +import org.objectweb.asm.tree.MethodNode; + +/** + * Transform inline advice to delegating advice. This transformation is best effort, it isn't able + * to transform all the advices. + */ +class AdviceTransformer { + private static final Type OBJECT_TYPE = Type.getType(Object.class); + private static final Type OBJECT_ARRAY_TYPE = Type.getType(Object[].class); + + static byte[] transform(byte[] bytes) { + ClassReader cr = new ClassReader(bytes); + ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_MAXS); + ClassNode classNode = new ClassNode(); + cr.accept(classNode, ClassReader.EXPAND_FRAMES); + + // skip the class if there aren't any methods with advice annotations or these annotations have + // set inline = false + if (!hasInlineAdvice(classNode)) { + classNode.accept(cw); + return cw.toByteArray(); + } + + // Advices already using Advice.AssignReturned are assumed to be already compatible + // Those won't be transformed except for setting inline to false + boolean justDelegateAdvice = usesAssignReturned(classNode); + + // sort enter advice method before exit advice + classNode.methods.sort( + Comparator.comparingInt( + (methodNode) -> { + if (isEnterAdvice(methodNode)) { + return 1; + } else if (isExitAdvice(methodNode)) { + return 2; + } + return 0; + })); + + TransformationContext context = new TransformationContext(); + ClassVisitor cv = + new ClassVisitor(AsmApi.VERSION, cw) { + + @Override + public MethodVisitor visitMethod( + int access, String name, String descriptor, String signature, String[] exceptions) { + ClassVisitor classVisitor = this.cv; + return new MethodNode(api, access, name, descriptor, signature, exceptions) { + @Override + public void visitEnd() { + super.visitEnd(); + if (justDelegateAdvice) { + applyAdviceDelegation( + context, this, classVisitor, exceptions.toArray(new String[0])); + } else { + instrument(context, this, classVisitor); + } + } + }; + } + }; + classNode.accept(cv); + return cw.toByteArray(); + } + + private static boolean hasInlineAdvice(ClassNode classNode) { + for (MethodNode mn : classNode.methods) { + if (hasInlineAdvice(mn)) { + return true; + } + } + + return false; + } + + private static boolean hasInlineAdvice(MethodNode methodNode) { + return hasInlineAdvice(methodNode, ADVICE_ON_METHOD_ENTER) + || hasInlineAdvice(methodNode, ADVICE_ON_METHOD_EXIT); + } + + private static boolean hasInlineAdvice(MethodNode methodNode, Type type) { + AnnotationNode annotationNode = getAnnotationNode(methodNode, type); + if (annotationNode != null) { + // delegating advice has attribute "inline" = false + // all other advice is inline + return !Boolean.FALSE.equals(getAttributeValue(annotationNode, "inline")); + } + return false; + } + + // method argument annotated with Advice.Argument or Advice.Return + private static class OutputArgument { + // index of the method argument with the annotation + final int adviceIndex; + // value of the annotation or -1 if Advice.Return or Advice.Enter + final int methodIndex; + + OutputArgument(int adviceIndex, int methodIndex) { + this.adviceIndex = adviceIndex; + this.methodIndex = methodIndex; + } + } + + // method argument annotated with Advice.Local + private static class AdviceLocal { + // index of the method argument with the annotation + final int adviceIndex; + // value of the Advice.Local annotation + final String name; + + AdviceLocal(int adviceIndex, String name) { + this.adviceIndex = adviceIndex; + this.name = name; + } + } + + private static final Type ADVICE_ARGUMENT = Type.getType(Advice.Argument.class); + + /** List of arguments annotated with {@code @Advice.Argument(readOnly = false)}. */ + private static List getWritableArguments(MethodNode source) { + List result = new ArrayList<>(); + if (source.visibleParameterAnnotations != null) { + int i = 0; + for (List list : source.visibleParameterAnnotations) { + for (AnnotationNode annotationNode : list) { + Type annotationType = Type.getType(annotationNode.desc); + if (ADVICE_ARGUMENT.equals(annotationType) && isWriteable(annotationNode)) { + Object value = getAnnotationValue(annotationNode); + if (value instanceof Integer) { + result.add(new OutputArgument(i, (Integer) value)); + } + } + } + i++; + } + } + + return result; + } + + private static final Type ADVICE_RETURN = Type.getType(Advice.Return.class); + + /** Argument annotated with {@code @Advice.Return(readOnly = false)} or {@code null}. */ + private static OutputArgument getWritableReturnValue(MethodNode source) { + if (source.visibleParameterAnnotations != null) { + int i = 0; + for (List list : source.visibleParameterAnnotations) { + for (AnnotationNode annotationNode : list) { + Type annotationType = Type.getType(annotationNode.desc); + if (ADVICE_RETURN.equals(annotationType) && isWriteable(annotationNode)) { + return new OutputArgument(i, -1); + } + } + i++; + } + } + + return null; + } + + private static final Type ADVICE_ENTER = Type.getType(Advice.Enter.class); + + /** Argument annotated with {@code @Advice.Enter} or {@code null}. */ + private static OutputArgument getEnterArgument(MethodNode source) { + Type[] argumentTypes = Type.getArgumentTypes(source.desc); + if (source.visibleParameterAnnotations != null) { + int i = 0; + for (List list : source.visibleParameterAnnotations) { + for (AnnotationNode annotationNode : list) { + Type annotationType = Type.getType(annotationNode.desc); + if (ADVICE_ENTER.equals(annotationType) + && argumentTypes[i].getDescriptor().length() > 1) { + return new OutputArgument(i, -1); + } + } + i++; + } + } + + return null; + } + + private static final Type ADVICE_LOCAL = Type.getType(Advice.Local.class); + + /** List of arguments annotated with {@code @Advice.Local}. */ + private static List getLocals(MethodNode source) { + List result = new ArrayList<>(); + if (source.visibleParameterAnnotations != null) { + int i = 0; + for (List list : source.visibleParameterAnnotations) { + for (AnnotationNode annotationNode : list) { + Type annotationType = Type.getType(annotationNode.desc); + if (ADVICE_LOCAL.equals(annotationType)) { + Object value = getAnnotationValue(annotationNode); + if (value instanceof String) { + result.add(new AdviceLocal(i, (String) value)); + } + } + } + i++; + } + } + + return result; + } + + static final Type ADVICE_ON_METHOD_ENTER = Type.getType(Advice.OnMethodEnter.class); + private static final Type ADVICE_ASSIGN_RETURNED_TO_RETURNED = + Type.getType(Advice.AssignReturned.ToReturned.class); + private static final Type ADVICE_ASSIGN_RETURNED_TO_ARGUMENTS = + Type.getType(Advice.AssignReturned.ToArguments.class); + private static final Type ADVICE_ASSIGN_RETURNED_TO_FIELDS = + Type.getType(Advice.AssignReturned.ToFields.class); + private static final Type ADVICE_ASSIGN_RETURNED_TO_ALL_ARGUMENTS = + Type.getType(Advice.AssignReturned.ToAllArguments.class); + + private static boolean usesAssignReturned(MethodNode source) { + return hasAnnotation(source, ADVICE_ASSIGN_RETURNED_TO_RETURNED) + || hasAnnotation(source, ADVICE_ASSIGN_RETURNED_TO_ARGUMENTS) + || hasAnnotation(source, ADVICE_ASSIGN_RETURNED_TO_FIELDS) + || hasAnnotation(source, ADVICE_ASSIGN_RETURNED_TO_ALL_ARGUMENTS); + } + + private static boolean usesAssignReturned(ClassNode classNode) { + for (MethodNode mn : classNode.methods) { + if (usesAssignReturned(mn)) { + return true; + } + } + return false; + } + + private static boolean isEnterAdvice(MethodNode source) { + return hasAnnotation(source, ADVICE_ON_METHOD_ENTER); + } + + static final Type ADVICE_ON_METHOD_EXIT = Type.getType(Advice.OnMethodExit.class); + + private static boolean isExitAdvice(MethodNode source) { + return hasAnnotation(source, ADVICE_ON_METHOD_EXIT); + } + + private static AnnotationNode getAnnotationNode(MethodNode source, Type type) { + if (source.visibleAnnotations != null) { + for (AnnotationNode annotationNode : source.visibleAnnotations) { + Type annotationType = Type.getType(annotationNode.desc); + if (type.equals(annotationType)) { + return annotationNode; + } + } + } + + return null; + } + + static boolean hasAnnotation(MethodNode source, Type type) { + return getAnnotationNode(source, type) != null; + } + + /** + * Transform arguments annotated with {@code @Advice.Argument(readOnly = false)}. + * + *

      {@code
      +   * void foo(@Advice.Argument(value = 0, readOnly = false) T1 foo, @Advice.Argument(value = 0, readOnly = false) T2 bar)
      +   * }
      + * + *

      is transformed to + * + *

      {@code
      +   * @Advice.AssignReturned.ToArguments({
      +   *   @Advice.AssignReturned.ToArguments.ToArgument(value = 0, index = 0, typing = DYNAMIC),
      +   *   @Advice.AssignReturned.ToArguments.ToArgument(value = 1, index = 1, typing = DYNAMIC)
      +   * })
      +   * void foo(@Advice.Argument(value = 0, readOnly = true) T1 foo, @Advice.Argument(value = 0, readOnly = true) T2 bar) {
      +   *   ...
      +   *   Object[] result = new Object[2];
      +   *   result[0] = foo;
      +   *   result[1] = bar;
      +   *   return result;
      +   * }
      +   * }
      + */ + private static MethodVisitor instrumentWritableArguments( + MethodVisitor target, + MethodNode source, + List writableArguments, + int returnIndex) { + MethodVisitor result = + new MethodVisitor(AsmApi.VERSION, target) { + @Override + public void visitCode() { + AnnotationVisitor av = + visitAnnotation(Type.getDescriptor(Advice.AssignReturned.ToArguments.class), true); + AnnotationVisitor valueArrayVisitor = av.visitArray("value"); + for (int i = 0; i < writableArguments.size(); i++) { + OutputArgument argument = writableArguments.get(i); + AnnotationVisitor valueVisitor = + valueArrayVisitor.visitAnnotation( + null, Type.getDescriptor(Advice.AssignReturned.ToArguments.ToArgument.class)); + valueVisitor.visit("value", argument.methodIndex); + valueVisitor.visit("index", returnIndex + i); + valueVisitor.visitEnum( + "typing", Type.getDescriptor(Assigner.Typing.class), "DYNAMIC"); + valueVisitor.visitEnd(); + } + valueArrayVisitor.visitEnd(); + av.visitEnd(); + super.visitCode(); + } + + @Override + public void visitInsn(int opcode) { + if (Opcodes.ARETURN == opcode) { + // expecting object array on stack + GeneratorAdapter ga = + new GeneratorAdapter(mv, source.access, source.name, source.desc); + Type[] argumentTypes = ga.getArgumentTypes(); + for (int i = 0; i < writableArguments.size(); i++) { + OutputArgument argument = writableArguments.get(i); + ga.dup(); + ga.push(returnIndex + i); + ga.loadArg(argument.adviceIndex); + ga.box(argumentTypes[argument.adviceIndex]); + ga.arrayStore(OBJECT_TYPE); + } + } + super.visitInsn(opcode); + } + }; + result = makeReadOnly(Advice.Argument.class, result); + return result; + } + + /** + * Transform arguments annotated with {@code @Advice.Return(readOnly = false)}. + * + *
      {@code
      +   * void foo(@Advice.Return(readOnly = false) T1 foo)
      +   * }
      + * + *

      is transformed to + * + *

      {@code
      +   * @Advice.AssignReturned.ToReturned(index = 0, typing = DYNAMIC)
      +   * Object[] foo(@Advice.Return(readOnly = true) T foo) {
      +   *   ...
      +   *   return new Object[] { foo };
      +   * }
      +   * }
      + */ + private static MethodVisitor instrumentWritableReturn( + MethodVisitor target, MethodNode source, OutputArgument writableReturn, int returnIndex) { + MethodVisitor result = + new MethodVisitor(AsmApi.VERSION, target) { + @Override + public void visitCode() { + AnnotationVisitor av = + visitAnnotation(Type.getDescriptor(Advice.AssignReturned.ToReturned.class), true); + av.visit("index", returnIndex); + av.visitEnum("typing", Type.getDescriptor(Assigner.Typing.class), "DYNAMIC"); + av.visitEnd(); + super.visitCode(); + } + + @Override + public void visitInsn(int opcode) { + if (Opcodes.ARETURN == opcode) { + // expecting object array on stack + GeneratorAdapter ga = + new GeneratorAdapter(mv, source.access, source.name, source.desc); + Type[] argumentTypes = ga.getArgumentTypes(); + ga.dup(); + ga.push(returnIndex); + ga.loadArg(writableReturn.adviceIndex); + ga.box(argumentTypes[writableReturn.adviceIndex]); + ga.arrayStore(OBJECT_TYPE); + } + super.visitInsn(opcode); + } + }; + result = makeReadOnly(Advice.Return.class, result); + return result; + } + + /** + * Transform arguments annotated with {@code @Advice.Local} and {@code @Advice.Enter}. + * + *
      {@code
      +   * void foo(@Advice.Local("foo") T1 foo, @Advice.Local("bar") T2 bar)
      +   * }
      + * + *

      for enter advice is transformed to + * + *

      {@code
      +   * Object[] foo(@Advice.Unused Object foo, @Advice.Unused Object bar) {
      +   *   ...
      +   *   Map result = new HashMap();
      +   *   result.put("foo", foo);
      +   *   result.put("bar", bar);
      +   *   return new Object[] { result };
      +   * }
      +   * }
      + * + *

      and for exit advice is transformed to + * + *

      {@code
      +   * void foo(@Advice.Unused Object foo, @Advice.Unused Object bar, @Advice.Enter Object[] array) {
      +   *   Map map = (Map) array[0];
      +   *   foo = (T1) map.get("foo");
      +   *   bar = (T2) map.get("bar");
      +   *   ...
      +   * }
      +   * }
      + */ + private static MethodVisitor instrumentAdviceLocals( + boolean isEnterAdvice, + MethodVisitor target, + MethodNode source, + String originalDesc, + List adviceLocals, + OutputArgument enterArgument, + int returnIndex) { + AtomicReference generatorRef = new AtomicReference<>(); + AtomicInteger dataIndex = new AtomicInteger(); + + target = + new MethodVisitor(AsmApi.VERSION, target) { + @Override + public AnnotationVisitor visitParameterAnnotation( + int parameter, String descriptor, boolean visible) { + // replace @Advice.Local with @Advice.Unused + if (Type.getDescriptor(Advice.Local.class).equals(descriptor)) { + descriptor = Type.getDescriptor(Advice.Unused.class); + } + // replace @Advice.Enter with @Advice.Unused + if (enterArgument != null + && enterArgument.adviceIndex == parameter + && Type.getDescriptor(Advice.Enter.class).equals(descriptor)) { + descriptor = Type.getDescriptor(Advice.Unused.class); + } + return super.visitParameterAnnotation(parameter, descriptor, visible); + } + + @Override + public void visitAnnotableParameterCount(int parameterCount, boolean visible) { + if (!isEnterAdvice) { + parameterCount++; + } + super.visitAnnotableParameterCount(parameterCount, visible); + } + + @Override + public void visitInsn(int opcode) { + if (isEnterAdvice && Opcodes.ARETURN == opcode) { + // expecting object array on stack + GeneratorAdapter ga = generatorRef.get(); + // duplicate array + ga.dup(); + // push array index for the map + ga.push(returnIndex); + Type hashMapType = Type.getType(HashMap.class); + Type[] argumentTypes = ga.getArgumentTypes(); + ga.newInstance(hashMapType); + ga.dup(); + ga.invokeConstructor(hashMapType, Method.getMethod("void ()")); + // stack: array, array, array index for map, map + for (AdviceLocal adviceLocal : adviceLocals) { + ga.dup(); + ga.push(adviceLocal.name); + ga.loadArg(adviceLocal.adviceIndex); + ga.box(argumentTypes[adviceLocal.adviceIndex]); + ga.invokeVirtual( + hashMapType, + Method.getMethod("java.lang.Object put(java.lang.Object, java.lang.Object)")); + // pop return value of Map.put + ga.pop(); + } + // stack: array, array, array index for map, map + // store map in the array + ga.arrayStore(OBJECT_TYPE); + // stack: array + } + super.visitInsn(opcode); + } + + @Override + public void visitCode() { + super.visitCode(); + GeneratorAdapter ga = generatorRef.get(); + Type[] argumentTypes = ga.getArgumentTypes(); + + if (isEnterAdvice) { + // we have changed the type fo method arguments annotated with @Advice.Local to Object + // here we'll load the argument, cast it to its actual type, and store it back + for (AdviceLocal adviceLocal : adviceLocals) { + ga.loadArg(adviceLocal.adviceIndex); + ga.checkCast(argumentTypes[adviceLocal.adviceIndex]); + ga.storeArg(adviceLocal.adviceIndex); + } + return; + } + + // the index of last argument where object array returned from enter advice is inserted + // (argumentTypes array does not contain the object array) + int lastArgumentIndex = argumentTypes.length; + AnnotationVisitor av = + mv.visitParameterAnnotation( + lastArgumentIndex, Type.getDescriptor(Advice.Enter.class), true); + av.visitEnd(); + + // load object array + ga.loadLocal(dataIndex.get(), OBJECT_ARRAY_TYPE); + if (enterArgument != null) { + // value for @Advice.Enter is stored as the first element + ga.dup(); + ga.push(0); + Type type = argumentTypes[enterArgument.adviceIndex]; + ga.arrayLoad(type); + ga.checkCast(type); + ga.storeArg(enterArgument.adviceIndex); + } + + // object array on stack + ga.dup(); + Type mapType = Type.getType(Map.class); + // we want the last element of the array + ga.arrayLength(); + ga.visitInsn(Opcodes.ICONST_1); + ga.visitInsn(Opcodes.ISUB); + // load map + ga.arrayLoad(mapType); + for (AdviceLocal adviceLocal : adviceLocals) { + // duplicate Map + ga.dup(); + ga.push(adviceLocal.name); + ga.invokeInterface( + mapType, Method.getMethod("java.lang.Object get(java.lang.Object)")); + ga.unbox(argumentTypes[adviceLocal.adviceIndex]); + ga.storeArg(adviceLocal.adviceIndex); + } + // pop Map + ga.pop(); + } + }; + + // pretend that this method still takes the original arguments + GeneratorAdapter ga = + new GeneratorAdapter(target, Opcodes.ACC_STATIC, source.name, originalDesc); + generatorRef.set(ga); + if (!isEnterAdvice) { + // for exit advice create a new local for the Object array we added as the last method + // argument + dataIndex.set(ga.newLocal(OBJECT_ARRAY_TYPE)); + } + + return ga; + } + + private static void instrument( + TransformationContext context, MethodNode methodNode, ClassVisitor classVisitor) { + String originalDescriptor = methodNode.desc; + String[] exceptionsArray = methodNode.exceptions.toArray(new String[0]); + + List writableArguments = getWritableArguments(methodNode); + OutputArgument writableReturn = getWritableReturnValue(methodNode); + OutputArgument enterArgument = getEnterArgument(methodNode); + List adviceLocals = getLocals(methodNode); + boolean isEnterAdvice = isEnterAdvice(methodNode); + boolean isExitAdvice = isExitAdvice(methodNode); + Type returnType = Type.getReturnType(methodNode.desc); + + // currently we don't support rewriting enter advice returning a primitive type + if (isEnterAdvice + && !(returnType.getSort() == Type.VOID + || returnType.getSort() == Type.OBJECT + || returnType.getSort() == Type.ARRAY)) { + context.disableReturnTypeChange(); + } + // context is shared by enter and exit advice, if entry advice was rejected don't attempt to + // rewrite usages of @Advice.Enter in the exit advice + if (!context.canChangeReturnType()) { + enterArgument = null; + } + + if (context.canChangeReturnType() || (isExitAdvice && Type.VOID_TYPE.equals(returnType))) { + if (!writableArguments.isEmpty() + || writableReturn != null + || !Type.VOID_TYPE.equals(returnType) + || (!adviceLocals.isEmpty() && isEnterAdvice)) { + Type[] argumentTypes = Type.getArgumentTypes(methodNode.desc); + if (!adviceLocals.isEmpty() && isEnterAdvice) { + // Set type of arguments annotated with @Advice.Local to Object. These arguments are + // likely to be helper classes which currently breaks because the invokedynamic call in + // advised class needs access to the parameter types of the advice method. + for (AdviceLocal adviceLocal : adviceLocals) { + argumentTypes[adviceLocal.adviceIndex] = OBJECT_TYPE; + } + } + + methodNode.desc = Type.getMethodDescriptor(OBJECT_ARRAY_TYPE, argumentTypes); + + MethodNode tmp = + new MethodNode( + methodNode.access, + methodNode.name, + methodNode.desc, + methodNode.signature, + exceptionsArray); + MethodVisitor mv = + instrumentOurParameters( + context, + tmp, + methodNode, + originalDescriptor, + writableArguments, + writableReturn, + adviceLocals); + methodNode.accept(mv); + + methodNode = tmp; + adviceLocals = getLocals(methodNode); + } + + // this is the only transformation that does not change the return type of the advice method, + // thus it is also the only transformation that can be applied on top of the other transforms + if ((!adviceLocals.isEmpty() || enterArgument != null) && isExitAdvice) { + // Set type of arguments annotated with @Advice.Local to Object. These arguments are likely + // to be helper classes which currently breaks because the invokedynamic call in advised + // class needs access to the parameter types of the advice method. + Type[] newArgumentTypes = Type.getArgumentTypes(methodNode.desc); + for (AdviceLocal adviceLocal : adviceLocals) { + newArgumentTypes[adviceLocal.adviceIndex] = OBJECT_TYPE; + } + if (enterArgument != null) { + newArgumentTypes[enterArgument.adviceIndex] = OBJECT_TYPE; + } + List typeList = new ArrayList<>(Arrays.asList(newArgumentTypes)); + // add Object array as the last argument, this array is used to pass info from the enter + // advice + typeList.add(OBJECT_ARRAY_TYPE); + + methodNode.desc = + Type.getMethodDescriptor( + Type.getReturnType(methodNode.desc), typeList.toArray(new Type[0])); + + MethodNode tmp = + new MethodNode( + methodNode.access, + methodNode.name, + methodNode.desc, + methodNode.signature, + exceptionsArray); + MethodVisitor mv = + instrumentAdviceLocals( + false, tmp, methodNode, originalDescriptor, adviceLocals, enterArgument, -1); + methodNode.accept(mv); + + methodNode = tmp; + } + } + + applyAdviceDelegation(context, methodNode, classVisitor, exceptionsArray); + } + + private static void applyAdviceDelegation( + TransformationContext context, + MethodNode methodNode, + ClassVisitor classVisitor, + String[] exceptionsArray) { + MethodVisitor mv = + classVisitor.visitMethod( + methodNode.access, + methodNode.name, + methodNode.desc, + methodNode.signature, + exceptionsArray); + mv = delegateAdvice(context, mv); + + methodNode.accept(mv); + } + + private static MethodVisitor instrumentOurParameters( + TransformationContext context, + MethodVisitor target, + MethodNode source, + String originalDesc, + List writableArguments, + OutputArgument writableReturn, + List adviceLocals) { + + // position 0 in enter advice is reserved for the return value of the method + // to avoid complicating things further we aren't going to figure out whether it is really used + int returnArraySize = isEnterAdvice(source) ? 1 : 0; + if (writableReturn != null) { + target = instrumentWritableReturn(target, source, writableReturn, returnArraySize); + returnArraySize++; + } + if (!writableArguments.isEmpty()) { + target = instrumentWritableArguments(target, source, writableArguments, returnArraySize); + returnArraySize += writableArguments.size(); + } + if (!adviceLocals.isEmpty() && isEnterAdvice(source)) { + target = + instrumentAdviceLocals( + true, target, source, originalDesc, adviceLocals, null, returnArraySize); + returnArraySize++; + } + target = addReturnArray(context, target, returnArraySize); + + return target; + } + + /** Return the value of the {@code readOnly} attribute of the annotation. */ + private static boolean isWriteable(AnnotationNode annotationNode) { + Object value = getAttributeValue(annotationNode, "readOnly"); + return Boolean.FALSE.equals(value); + } + + private static Object getAttributeValue(AnnotationNode annotationNode, String attributeName) { + if (annotationNode.values != null && !annotationNode.values.isEmpty()) { + List values = annotationNode.values; + for (int i = 0; i < values.size(); i += 2) { + String name = (String) values.get(i); + Object value = values.get(i + 1); + if (attributeName.equals(name)) { + return value; + } + } + } + + return null; + } + + /** Return the value of the {@code value} attribute of the annotation. */ + private static Object getAnnotationValue(AnnotationNode annotationNode) { + if (annotationNode.values != null && !annotationNode.values.isEmpty()) { + List values = annotationNode.values; + for (int i = 0; i < values.size(); i += 2) { + String attributeName = (String) values.get(i); + Object attributeValue = values.get(i + 1); + if ("value".equals(attributeName)) { + return attributeValue; + } + } + } + + return null; + } + + private static MethodVisitor addReturnArray( + TransformationContext context, MethodVisitor target, int returnArraySize) { + return new MethodVisitor(AsmApi.VERSION, target) { + @Override + public void visitInsn(int opcode) { + if (Opcodes.RETURN == opcode) { + // change the return value of the method to Object[] + GeneratorAdapter ga = new GeneratorAdapter(mv, 0, null, "()V"); + ga.push(returnArraySize); + ga.newArray(OBJECT_TYPE); + opcode = Opcodes.ARETURN; + } else if (context.canChangeReturnType() && Opcodes.ARETURN == opcode) { + // change the return value of the method to Object[] that on the 0 index contains the + // original return value + + // stack: original return value + GeneratorAdapter ga = new GeneratorAdapter(mv, 0, null, "()V"); + ga.push(returnArraySize); + ga.newArray(OBJECT_TYPE); + // stack: original return value, array + ga.dupX1(); + // stack: array, original return value, array + ga.swap(); + // stack: array, array, original return value + ga.push(0); // original return value is stored as the first element + ga.swap(); + ga.arrayStore(OBJECT_TYPE); + // stack: array + } + super.visitInsn(opcode); + } + }; + } + + /** + * If method is annotated with {@link Advice.OnMethodEnter} or {@link Advice.OnMethodExit} set + * {@code inline} attribute on the annotation to {@code false}. If method is annotated with {@link + * Advice.OnMethodEnter} and has {@code skipOn} attribute set {@code skipOnIndex} attribute on the + * annotation to {@code 0}. + */ + private static MethodVisitor delegateAdvice(TransformationContext context, MethodVisitor target) { + return new MethodVisitor(AsmApi.VERSION, target) { + @Override + public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) { + AnnotationVisitor av = super.visitAnnotation(descriptor, visible); + Type type = Type.getType(descriptor); + if (!Type.getType(Advice.OnMethodEnter.class).equals(type) + && !Type.getType(Advice.OnMethodExit.class).equals(type)) { + return av; + } + return new AnnotationVisitor(api, av) { + boolean hasInline = false; + boolean hasSkipOn = false; + + @Override + public void visit(String name, Object value) { + if ("inline".equals(name)) { + value = Boolean.FALSE; + hasInline = true; + } else if ("skipOn".equals(name) && value != void.class) { + hasSkipOn = true; + } + super.visit(name, value); + } + + @Override + public void visitEnd() { + if (!hasInline) { + visit("inline", Boolean.FALSE); + } + if (context.canChangeReturnType() && hasSkipOn) { + visit("skipOnIndex", 0); + } + super.visitEnd(); + } + }; + } + }; + } + + /** If annotation has {@code readOnly} attribute set it to {@code true}. */ + private static MethodVisitor makeReadOnly(Class annotationType, MethodVisitor target) { + return new MethodVisitor(AsmApi.VERSION, target) { + + @Override + public AnnotationVisitor visitParameterAnnotation( + int parameter, String descriptor, boolean visible) { + AnnotationVisitor av = super.visitParameterAnnotation(parameter, descriptor, visible); + Type type = Type.getType(descriptor); + if (!Type.getType(annotationType).equals(type)) { + return av; + } + return new AnnotationVisitor(api, av) { + @Override + public void visit(String name, Object value) { + if ("readOnly".equals(name)) { + value = Boolean.TRUE; + } + super.visit(name, value); + } + }; + } + }; + } + + private static class TransformationContext { + private boolean canChangeReturnType = true; + + void disableReturnTypeChange() { + canChangeReturnType = false; + } + + boolean canChangeReturnType() { + return canChangeReturnType; + } + } + + private AdviceTransformer() {} +} diff --git a/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/instrumentation/indy/ClassInjectorImpl.java b/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/instrumentation/indy/ClassInjectorImpl.java new file mode 100644 index 000000000000..ad901577ebc0 --- /dev/null +++ b/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/instrumentation/indy/ClassInjectorImpl.java @@ -0,0 +1,72 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.tooling.instrumentation.indy; + +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.internal.injection.ClassInjector; +import io.opentelemetry.javaagent.extension.instrumentation.internal.injection.InjectionMode; +import io.opentelemetry.javaagent.extension.instrumentation.internal.injection.ProxyInjectionBuilder; +import io.opentelemetry.javaagent.tooling.HelperClassDefinition; +import io.opentelemetry.javaagent.tooling.muzzle.AgentTooling; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Function; +import java.util.stream.Collectors; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.dynamic.DynamicType; +import net.bytebuddy.pool.TypePool; + +public class ClassInjectorImpl implements ClassInjector { + + private final InstrumentationModule instrumentationModule; + + private final List> classesToInject; + + private final IndyProxyFactory proxyFactory; + + public ClassInjectorImpl(InstrumentationModule module) { + instrumentationModule = module; + classesToInject = new ArrayList<>(); + proxyFactory = IndyBootstrap.getProxyFactory(module); + } + + public List getClassesToInject(ClassLoader instrumentedCl) { + return classesToInject.stream() + .map(generator -> generator.apply(instrumentedCl)) + .collect(Collectors.toList()); + } + + @Override + public ProxyInjectionBuilder proxyBuilder(String classToProxy, String newProxyName) { + return new ProxyBuilder(classToProxy, newProxyName); + } + + private class ProxyBuilder implements ProxyInjectionBuilder { + + private final String classToProxy; + private final String proxyClassName; + + ProxyBuilder(String classToProxy, String proxyClassName) { + this.classToProxy = classToProxy; + this.proxyClassName = proxyClassName; + } + + @Override + public void inject(InjectionMode mode) { + classesToInject.add( + cl -> { + InstrumentationModuleClassLoader moduleCl = + IndyModuleRegistry.getInstrumentationClassLoader(instrumentationModule, cl); + TypePool typePool = + AgentTooling.poolStrategy() + .typePool(AgentTooling.locationStrategy().classFileLocator(moduleCl), moduleCl); + TypeDescription proxiedType = typePool.describe(classToProxy).resolve(); + DynamicType.Unloaded proxy = proxyFactory.generateProxy(proxiedType, proxyClassName); + return HelperClassDefinition.create(proxy, mode); + }); + } + } +} diff --git a/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/instrumentation/indy/ForceDynamicallyTypedAssignReturnedFactory.java b/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/instrumentation/indy/ForceDynamicallyTypedAssignReturnedFactory.java new file mode 100644 index 000000000000..dd0c88bb4752 --- /dev/null +++ b/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/instrumentation/indy/ForceDynamicallyTypedAssignReturnedFactory.java @@ -0,0 +1,136 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.tooling.instrumentation.indy; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.annotation.AnnotationDescription; +import net.bytebuddy.description.annotation.AnnotationValue; +import net.bytebuddy.description.enumeration.EnumerationDescription; +import net.bytebuddy.description.method.MethodDescription; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.implementation.bytecode.assign.Assigner; +import net.bytebuddy.matcher.ElementMatchers; + +/** + * This factory is designed to wrap around {@link Advice.PostProcessor.Factory} and ensures that + * {@link net.bytebuddy.implementation.bytecode.assign.Assigner.Typing#DYNAMIC} is used everywhere. + * + *

      This helps by avoiding errors where the instrumented bytecode is suddenly unloadable due to + * incompatible assignments and preventing cluttering advice code annotations with the explicit + * typing. + */ +public class ForceDynamicallyTypedAssignReturnedFactory implements Advice.PostProcessor.Factory { + + private static final String TO_ARGUMENTS_TYPENAME = + Advice.AssignReturned.ToArguments.class.getName(); + private static final String TO_ARGUMENT_TYPENAME = + Advice.AssignReturned.ToArguments.ToArgument.class.getName(); + private static final String TO_ALL_ARGUMENTS_TYPENAME = + Advice.AssignReturned.ToAllArguments.class.getName(); + private static final String TO_THIS_TYPENAME = Advice.AssignReturned.ToThis.class.getName(); + private static final String TO_FIELDS_TYPENAME = Advice.AssignReturned.ToFields.class.getName(); + private static final String TO_FIELD_TYPENAME = + Advice.AssignReturned.ToFields.ToField.class.getName(); + private static final String TO_RETURNED_TYPENAME = + Advice.AssignReturned.ToReturned.class.getName(); + private static final String TO_THROWN_TYPENAME = Advice.AssignReturned.ToThrown.class.getName(); + private static final EnumerationDescription DYNAMIC_TYPING = + new EnumerationDescription.ForLoadedEnumeration(Assigner.Typing.DYNAMIC); + + private final Advice.PostProcessor.Factory delegate; + + public ForceDynamicallyTypedAssignReturnedFactory(Advice.PostProcessor.Factory delegate) { + this.delegate = delegate; + } + + @Override + public Advice.PostProcessor make(MethodDescription.InDefinedShape adviceMethod, boolean exit) { + return delegate.make(forceDynamicTyping(adviceMethod), exit); + } + + // Visible for testing + static MethodDescription.InDefinedShape forceDynamicTyping( + MethodDescription.InDefinedShape adviceMethod) { + return new MethodDescription.Latent( + adviceMethod.getDeclaringType(), + adviceMethod.getInternalName(), + adviceMethod.getModifiers(), + adviceMethod.getTypeVariables().asTokenList(ElementMatchers.none()), + adviceMethod.getReturnType(), + adviceMethod.getParameters().asTokenList(ElementMatchers.none()), + adviceMethod.getExceptionTypes(), + forceDynamicTyping(adviceMethod.getDeclaredAnnotations()), + adviceMethod.getDefaultValue(), + adviceMethod.getReceiverType()); + } + + private static List forceDynamicTyping( + List declaredAnnotations) { + return declaredAnnotations.stream() + .map(ForceDynamicallyTypedAssignReturnedFactory::forceDynamicTyping) + .collect(Collectors.toList()); + } + + private static AnnotationDescription forceDynamicTyping(AnnotationDescription anno) { + + String name = anno.getAnnotationType().getName(); + if (name.equals(TO_FIELD_TYPENAME) + || name.equals(TO_ARGUMENT_TYPENAME) + || name.equals(TO_THIS_TYPENAME) + || name.equals(TO_ALL_ARGUMENTS_TYPENAME) + || name.equals(TO_RETURNED_TYPENAME) + || name.equals(TO_THROWN_TYPENAME)) { + return replaceAnnotationValue( + anno, "typing", oldVal -> AnnotationValue.ForEnumerationDescription.of(DYNAMIC_TYPING)); + } else if (name.equals(TO_FIELDS_TYPENAME) || name.equals(TO_ARGUMENTS_TYPENAME)) { + return replaceAnnotationValue( + anno, + "value", + oldVal -> { + if (!oldVal.getState().isDefined()) { + return null; + } + AnnotationDescription[] resolve = (AnnotationDescription[]) oldVal.resolve(); + if (resolve.length == 0) { + return oldVal; + } + AnnotationDescription[] newValueList = + Arrays.stream(resolve) + .map(ForceDynamicallyTypedAssignReturnedFactory::forceDynamicTyping) + .toArray(AnnotationDescription[]::new); + TypeDescription subType = newValueList[0].getAnnotationType(); + return AnnotationValue.ForDescriptionArray.of(subType, newValueList); + }); + } + return anno; + } + + private static AnnotationDescription replaceAnnotationValue( + AnnotationDescription anno, + String propertyName, + Function, AnnotationValue> valueMapper) { + AnnotationValue oldValue = anno.getValue(propertyName); + AnnotationValue newValue = valueMapper.apply(oldValue); + Map> updatedValues = new HashMap<>(); + for (MethodDescription.InDefinedShape property : + anno.getAnnotationType().getDeclaredMethods()) { + AnnotationValue value = anno.getValue(property); + if (!propertyName.equals(property.getName()) && value.getState().isDefined()) { + updatedValues.put(property.getName(), value); + } + } + if (newValue != null) { + updatedValues.put(propertyName, newValue); + } + return new AnnotationDescription.Latent(anno.getAnnotationType(), updatedValues) {}; + } +} diff --git a/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/instrumentation/indy/IndyBootstrap.java b/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/instrumentation/indy/IndyBootstrap.java new file mode 100644 index 000000000000..3ec6d531138d --- /dev/null +++ b/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/instrumentation/indy/IndyBootstrap.java @@ -0,0 +1,299 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.tooling.instrumentation.indy; + +import io.opentelemetry.javaagent.bootstrap.CallDepth; +import io.opentelemetry.javaagent.bootstrap.IndyBootstrapDispatcher; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import java.lang.invoke.CallSite; +import java.lang.invoke.ConstantCallSite; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.lang.reflect.Method; +import java.security.PrivilegedAction; +import java.util.Arrays; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.annotation.Nullable; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.annotation.AnnotationDescription; +import net.bytebuddy.description.method.MethodDescription; +import net.bytebuddy.utility.JavaConstant; + +/** + * We instruct Byte Buddy (via {@link Advice.WithCustomMapping#bootstrap(java.lang.reflect.Method)}) + * to dispatch {@linkplain Advice.OnMethodEnter#inline() non-inlined advices} via an invokedynamic + * (indy) instruction. The target method is linked to a dynamically created instrumentation module + * class loader that is specific to an instrumentation module and the class loader of the + * instrumented method. + * + *

      The first invocation of an {@code INVOKEDYNAMIC} causes the JVM to dynamically link a {@link + * CallSite}. In this case, it will use the {@link #bootstrap} method to do that. This will also + * create the {@link InstrumentationModuleClassLoader}. + * + *

      + *
      + *   Bootstrap CL ←──────────────────────────── Agent CL
      + *       ↑ └───────── IndyBootstrapDispatcher ─ ↑ ──→ └────────────── {@link IndyBootstrap#bootstrap}
      + *     Ext/Platform CL               ↑          │                        ╷
      + *       ↑                           ╷          │                        ↓
      + *     System CL                     ╷          │        {@link IndyModuleRegistry#getInstrumentationClassLoader(String, ClassLoader)}
      + *       ↑                           ╷          │                        ╷
      + *     Common               linking of CallSite │                        ╷
      + *     ↑    ↑             (on first invocation) │                        ╷
      + * WebApp1  WebApp2                  ╷          │                     creates
      + *          ↑ - InstrumentedClass    ╷          │                        ╷
      + *          │                ╷       ╷          │                        ╷
      + *          │                INVOKEDYNAMIC      │                        ↓
      + *          └────────────────┼──────────────────{@link InstrumentationModuleClassLoader}
      + *                           └╶╶╶╶╶╶╶╶╶╶╶╶╶╶╶╶╶╶╶╶╶→├ AdviceClass
      + *                                                  ├ AdviceHelper
      + *                                                  └ {@link LookupExposer}
      + *
      + * Legend:
      + *  ╶╶→ method calls
      + *  ──→ class loader parent/child relationships
      + * 
      + */ +public class IndyBootstrap { + + private static final Logger logger = Logger.getLogger(IndyBootstrap.class.getName()); + + private static final Method indyBootstrapMethod; + + private static final String BOOTSTRAP_KIND_ADVICE = "advice"; + private static final String BOOTSTRAP_KIND_PROXY = "proxy"; + + private static final String PROXY_KIND_STATIC = "static"; + private static final String PROXY_KIND_CONSTRUCTOR = "constructor"; + private static final String PROXY_KIND_VIRTUAL = "virtual"; + + static { + try { + indyBootstrapMethod = + IndyBootstrapDispatcher.class.getMethod( + "bootstrap", + MethodHandles.Lookup.class, + String.class, + MethodType.class, + Object[].class); + + MethodType bootstrapMethodType = + MethodType.methodType( + ConstantCallSite.class, + MethodHandles.Lookup.class, + String.class, + MethodType.class, + Object[].class); + + IndyBootstrapDispatcher.init( + MethodHandles.lookup().findStatic(IndyBootstrap.class, "bootstrap", bootstrapMethodType)); + } catch (Exception e) { + throw new IllegalStateException(e); + } + } + + private IndyBootstrap() {} + + public static Method getIndyBootstrapMethod() { + return indyBootstrapMethod; + } + + @Nullable + @SuppressWarnings({"unused", "removal"}) // SecurityManager and AccessController are deprecated + private static ConstantCallSite bootstrap( + MethodHandles.Lookup lookup, + String adviceMethodName, + MethodType adviceMethodType, + Object[] args) { + + if (System.getSecurityManager() == null) { + return internalBootstrap(lookup, adviceMethodName, adviceMethodType, args); + } + + // callsite resolution needs privileged access to call Class#getClassLoader() and + // MethodHandles$Lookup#findStatic + return java.security.AccessController.doPrivileged( + (PrivilegedAction) + () -> internalBootstrap(lookup, adviceMethodName, adviceMethodType, args)); + } + + private static ConstantCallSite internalBootstrap( + MethodHandles.Lookup lookup, + String adviceMethodName, + MethodType adviceMethodType, + Object[] args) { + try { + String kind = (String) args[0]; + switch (kind) { + case BOOTSTRAP_KIND_ADVICE: + // See the getAdviceBootstrapArguments method for the argument definitions + return bootstrapAdvice( + lookup, + adviceMethodName, + adviceMethodType, + (String) args[1], + (String) args[2], + (String) args[3]); + case BOOTSTRAP_KIND_PROXY: + // See getProxyFactory for the argument definitions + return bootstrapProxyMethod( + lookup, + adviceMethodName, + adviceMethodType, + (String) args[1], + (String) args[2], + (String) args[3]); + default: + throw new IllegalArgumentException("Unknown bootstrapping kind: " + kind); + } + } catch (Exception e) { + logger.log(Level.SEVERE, e.getMessage(), e); + return null; + } + } + + private static ConstantCallSite bootstrapAdvice( + MethodHandles.Lookup lookup, + String adviceMethodName, + MethodType invokedynamicMethodType, + String moduleClassName, + String adviceMethodDescriptor, + String adviceClassName) + throws NoSuchMethodException, IllegalAccessException, ClassNotFoundException { + CallDepth callDepth = CallDepth.forClass(IndyBootstrap.class); + try { + if (callDepth.getAndIncrement() > 0) { + // avoid re-entrancy and stack overflow errors, which may happen when bootstrapping an + // instrumentation that also gets triggered during the bootstrap + // for example, adding correlation ids to the thread context when executing logger.debug. + logger.log( + Level.WARNING, + "Nested instrumented invokedynamic instruction linkage detected", + new Throwable()); + return null; + } + + InstrumentationModuleClassLoader instrumentationClassloader = + IndyModuleRegistry.getInstrumentationClassLoader( + moduleClassName, lookup.lookupClass().getClassLoader()); + + // Advices are not inlined. They are loaded as normal classes by the + // InstrumentationModuleClassloader and invoked via a method call from the instrumented method + Class adviceClass = instrumentationClassloader.loadClass(adviceClassName); + MethodType actualAdviceMethodType = + MethodType.fromMethodDescriptorString(adviceMethodDescriptor, instrumentationClassloader); + + MethodHandle methodHandle = + instrumentationClassloader + .getLookup() + .findStatic(adviceClass, adviceMethodName, actualAdviceMethodType) + .asType(invokedynamicMethodType); + return new ConstantCallSite(methodHandle); + } finally { + callDepth.decrementAndGet(); + } + } + + static Advice.BootstrapArgumentResolver.Factory getAdviceBootstrapArguments( + InstrumentationModule instrumentationModule) { + String moduleName = instrumentationModule.getClass().getName(); + return (adviceMethod, exit) -> + (instrumentedType, instrumentedMethod) -> + Arrays.asList( + JavaConstant.Simple.ofLoaded(BOOTSTRAP_KIND_ADVICE), + JavaConstant.Simple.ofLoaded(moduleName), + JavaConstant.Simple.ofLoaded(getOriginalSignature(adviceMethod)), + JavaConstant.Simple.ofLoaded(adviceMethod.getDeclaringType().getName())); + } + + private static String getOriginalSignature(MethodDescription.InDefinedShape adviceMethod) { + for (AnnotationDescription an : adviceMethod.getDeclaredAnnotations()) { + if (OriginalDescriptor.class.getName().equals(an.getAnnotationType().getName())) { + return (String) an.getValue("value").resolve(); + } + } + throw new IllegalStateException("OriginalSignature annotation is not present!"); + } + + private static ConstantCallSite bootstrapProxyMethod( + MethodHandles.Lookup lookup, + String proxyMethodName, + MethodType expectedMethodType, + String moduleClassName, + String proxyClassName, + String methodKind) + throws NoSuchMethodException, IllegalAccessException, ClassNotFoundException { + InstrumentationModuleClassLoader instrumentationClassloader = + IndyModuleRegistry.getInstrumentationClassLoader( + moduleClassName, lookup.lookupClass().getClassLoader()); + + Class proxiedClass = instrumentationClassloader.loadClass(proxyClassName); + + MethodHandle target; + switch (methodKind) { + case PROXY_KIND_STATIC: + target = + MethodHandles.publicLookup() + .findStatic(proxiedClass, proxyMethodName, expectedMethodType); + break; + case PROXY_KIND_CONSTRUCTOR: + target = + MethodHandles.publicLookup() + .findConstructor(proxiedClass, expectedMethodType.changeReturnType(void.class)) + .asType(expectedMethodType); // return type is the proxied class, but proxies expect + // Object + break; + case PROXY_KIND_VIRTUAL: + target = + MethodHandles.publicLookup() + .findVirtual( + proxiedClass, proxyMethodName, expectedMethodType.dropParameterTypes(0, 1)) + .asType( + expectedMethodType); // first argument type is the proxied class, but proxies + // expect Object + break; + default: + throw new IllegalStateException("unknown proxy method kind: " + methodKind); + } + return new ConstantCallSite(target); + } + + /** + * Creates a proxy factory for generating proxies for classes which are loaded by an {@link + * InstrumentationModuleClassLoader} for the provided {@link InstrumentationModule}. + * + * @param instrumentationModule the instrumentation module used to load the proxied target classes + * @return a factory for generating proxy classes + */ + public static IndyProxyFactory getProxyFactory(InstrumentationModule instrumentationModule) { + String moduleName = instrumentationModule.getClass().getName(); + return new IndyProxyFactory( + getIndyBootstrapMethod(), + (proxiedType, proxiedMethod) -> { + String methodKind; + if (proxiedMethod.isConstructor()) { + methodKind = PROXY_KIND_CONSTRUCTOR; + } else if (proxiedMethod.isMethod()) { + if (proxiedMethod.isStatic()) { + methodKind = PROXY_KIND_STATIC; + } else { + methodKind = PROXY_KIND_VIRTUAL; + } + } else { + throw new IllegalArgumentException( + "Unknown type of method: " + proxiedMethod.getName()); + } + + return Arrays.asList( + JavaConstant.Simple.ofLoaded(BOOTSTRAP_KIND_PROXY), + JavaConstant.Simple.ofLoaded(moduleName), + JavaConstant.Simple.ofLoaded(proxiedType.getName()), + JavaConstant.Simple.ofLoaded(methodKind)); + }); + } +} diff --git a/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/instrumentation/indy/IndyModuleRegistry.java b/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/instrumentation/indy/IndyModuleRegistry.java new file mode 100644 index 000000000000..7eaf7f3511b6 --- /dev/null +++ b/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/instrumentation/indy/IndyModuleRegistry.java @@ -0,0 +1,128 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.tooling.instrumentation.indy; + +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.internal.ExperimentalInstrumentationModule; +import io.opentelemetry.javaagent.tooling.util.ClassLoaderValue; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import net.bytebuddy.agent.builder.AgentBuilder; + +public class IndyModuleRegistry { + + private IndyModuleRegistry() {} + + private static final ConcurrentHashMap modulesByClassName = + new ConcurrentHashMap<>(); + + /** + * Weakly references the {@link InstrumentationModuleClassLoader}s for a given application class + * loader. The {@link InstrumentationModuleClassLoader} are kept alive by a strong reference from + * the instrumented class loader realized via {@link ClassLoaderValue}. + * + *

      The keys of the contained map are the instrumentation module group names, see {@link + * ExperimentalInstrumentationModule#getModuleGroup()}; + */ + private static final ClassLoaderValue> + instrumentationClassLoaders = new ClassLoaderValue<>(); + + public static InstrumentationModuleClassLoader getInstrumentationClassLoader( + String moduleClassName, ClassLoader instrumentedClassLoader) { + InstrumentationModule instrumentationModule = modulesByClassName.get(moduleClassName); + if (instrumentationModule == null) { + throw new IllegalArgumentException( + "No module with the class name " + modulesByClassName + " has been registered!"); + } + return getInstrumentationClassLoader(instrumentationModule, instrumentedClassLoader); + } + + public static InstrumentationModuleClassLoader getInstrumentationClassLoader( + InstrumentationModule module, ClassLoader instrumentedClassLoader) { + + String groupName = getModuleGroup(module); + + Map loadersByGroupName = + instrumentationClassLoaders.get(instrumentedClassLoader); + + if (loadersByGroupName == null) { + throw new IllegalArgumentException( + module + + " has not been initialized for class loader " + + instrumentedClassLoader + + " yet"); + } + + InstrumentationModuleClassLoader loader = loadersByGroupName.get(groupName); + if (loader == null || !loader.hasModuleInstalled(module)) { + throw new IllegalArgumentException( + module + + " has not been initialized for class loader " + + instrumentedClassLoader + + " yet"); + } + + return loader; + } + + /** + * Returns a newly created class loader containing only the provided module. Note that other + * modules from the same module group (see {@link #getModuleGroup(InstrumentationModule)}) will + * not be installed in this class loader. + */ + public static InstrumentationModuleClassLoader + createInstrumentationClassLoaderWithoutRegistration( + InstrumentationModule module, ClassLoader instrumentedClassLoader) { + // TODO: remove this method and replace usages with a custom TypePool implementation instead + ClassLoader agentOrExtensionCl = module.getClass().getClassLoader(); + InstrumentationModuleClassLoader cl = + new InstrumentationModuleClassLoader(instrumentedClassLoader, agentOrExtensionCl); + cl.installModule(module); + return cl; + } + + public static AgentBuilder.Identified.Extendable initializeModuleLoaderOnMatch( + InstrumentationModule module, AgentBuilder.Identified.Extendable agentBuilder) { + if (!module.isIndyModule()) { + throw new IllegalArgumentException("Provided module is not an indy module!"); + } + String moduleName = module.getClass().getName(); + InstrumentationModule existingRegistration = modulesByClassName.putIfAbsent(moduleName, module); + if (existingRegistration != null && existingRegistration != module) { + throw new IllegalArgumentException( + "A different module with the class name " + moduleName + " has already been registered!"); + } + return agentBuilder.transform( + (builder, typeDescription, classLoader, javaModule, protectionDomain) -> { + initializeModuleLoaderForClassLoader(module, classLoader); + return builder; + }); + } + + private static void initializeModuleLoaderForClassLoader( + InstrumentationModule module, ClassLoader classLoader) { + + ClassLoader agentOrExtensionCl = module.getClass().getClassLoader(); + + String groupName = getModuleGroup(module); + + InstrumentationModuleClassLoader moduleCl = + instrumentationClassLoaders + .computeIfAbsent(classLoader, ConcurrentHashMap::new) + .computeIfAbsent( + groupName, + unused -> new InstrumentationModuleClassLoader(classLoader, agentOrExtensionCl)); + + moduleCl.installModule(module); + } + + private static String getModuleGroup(InstrumentationModule module) { + if (module instanceof ExperimentalInstrumentationModule) { + return ((ExperimentalInstrumentationModule) module).getModuleGroup(); + } + return module.getClass().getName(); + } +} diff --git a/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/instrumentation/indy/IndyProxyFactory.java b/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/instrumentation/indy/IndyProxyFactory.java new file mode 100644 index 000000000000..0a1b774ba60f --- /dev/null +++ b/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/instrumentation/indy/IndyProxyFactory.java @@ -0,0 +1,181 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.tooling.instrumentation.indy; + +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.List; +import net.bytebuddy.ByteBuddy; +import net.bytebuddy.description.method.MethodDescription; +import net.bytebuddy.description.method.ParameterDescription; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.dynamic.DynamicType; +import net.bytebuddy.dynamic.scaffold.subclass.ConstructorStrategy; +import net.bytebuddy.implementation.FieldAccessor; +import net.bytebuddy.implementation.Implementation; +import net.bytebuddy.implementation.InvokeDynamic; +import net.bytebuddy.implementation.MethodCall; +import net.bytebuddy.implementation.bytecode.StackManipulation; +import net.bytebuddy.implementation.bytecode.member.MethodInvocation; +import net.bytebuddy.implementation.bytecode.member.MethodVariableAccess; +import net.bytebuddy.utility.JavaConstant; + +/** + * Factory for generating proxies which invoke their target via {@code INVOKEDYNAMIC}. Generated + * proxy classes have the following properties: The generated proxies have the following basic + * structure: + * + *

        + *
      • it has same superclass as the proxied class + *
      • it implements all interfaces implemented by the proxied class + *
      • for every public constructor of the proxied class, it defined a matching public constructor + * which: + *
          + *
        • invokes the default constructor of the superclass + *
        • invoked the corresponding constructor of the proxied class to generate the object to + * which the proxy delegates + *
        + *
      • it "copies" every declared static and non-static public method, the implementation will + * delegate to the corresponding method in the proxied class + *
      • all annotations on the proxied class and on its methods are copied to the proxy + *
      + * + *

      Note that only the public methods declared by the proxied class are actually proxied. + * Inherited methods are not automatically proxied. If you want those to be proxied, you'll need to + * explicitly override them in the proxied class. + */ +public class IndyProxyFactory { + + @FunctionalInterface + public interface BootstrapArgsProvider { + + /** + * Defines the additional arguments to pass to the invokedynamic bootstrap method for a given + * proxied method. The arguments have to be storable in the constant pool. + * + * @param classBeingProxied the type for which {@link + * IndyProxyFactory#generateProxy(TypeDescription, String)} was invoked + * @param proxiedMethodOrCtor the method or constructor from the proxied class for which the + * arguments are requested + * @return the arguments to pass to the bootstrap method + */ + List getBootstrapArgsForMethod( + TypeDescription classBeingProxied, MethodDescription.InDefinedShape proxiedMethodOrCtor); + } + + private static final String DELEGATE_FIELD_NAME = "delegate"; + + private final MethodDescription.InDefinedShape indyBootstrapMethod; + + private final BootstrapArgsProvider bootstrapArgsProvider; + + public IndyProxyFactory(Method bootstrapMethod, BootstrapArgsProvider bootstrapArgsProvider) { + this.indyBootstrapMethod = new MethodDescription.ForLoadedMethod(bootstrapMethod); + this.bootstrapArgsProvider = bootstrapArgsProvider; + } + + /** + * Generates a proxy. + * + * @param classToProxy the class for which a proxy will be generated + * @param proxyClassName the desired fully qualified name for the proxy class + * @return the generated proxy class + */ + public DynamicType.Unloaded generateProxy( + TypeDescription classToProxy, String proxyClassName) { + TypeDescription.Generic superClass = classToProxy.getSuperClass(); + DynamicType.Builder builder = + new ByteBuddy() + .subclass(superClass, ConstructorStrategy.Default.NO_CONSTRUCTORS) + .implement(classToProxy.getInterfaces()) + .name(proxyClassName) + .annotateType(classToProxy.getDeclaredAnnotations()) + .defineField(DELEGATE_FIELD_NAME, Object.class, Modifier.PRIVATE | Modifier.FINAL); + + for (MethodDescription.InDefinedShape method : classToProxy.getDeclaredMethods()) { + if (method.isPublic()) { + if (method.isConstructor()) { + List bootstrapArgs = + bootstrapArgsProvider.getBootstrapArgsForMethod(classToProxy, method); + builder = createProxyConstructor(superClass, method, bootstrapArgs, builder); + } else if (method.isMethod()) { + List bootstrapArgs = + bootstrapArgsProvider.getBootstrapArgsForMethod(classToProxy, method); + builder = createProxyMethod(method, bootstrapArgs, builder); + } + } + } + return builder.make(); + } + + private DynamicType.Builder createProxyMethod( + MethodDescription.InDefinedShape proxiedMethod, + List bootstrapArgs, + DynamicType.Builder builder) { + InvokeDynamic body = InvokeDynamic.bootstrap(indyBootstrapMethod, bootstrapArgs); + if (!proxiedMethod.isStatic()) { + body = body.withField(DELEGATE_FIELD_NAME); + } + body = body.withMethodArguments(); + int modifiers = Modifier.PUBLIC | (proxiedMethod.isStatic() ? Modifier.STATIC : 0); + return createProxyMethodOrConstructor( + proxiedMethod, + builder.defineMethod(proxiedMethod.getName(), proxiedMethod.getReturnType(), modifiers), + body); + } + + private DynamicType.Builder createProxyConstructor( + TypeDescription.Generic superClass, + MethodDescription.InDefinedShape proxiedConstructor, + List bootstrapArgs, + DynamicType.Builder builder) { + MethodDescription defaultSuperCtor = findDefaultConstructor(superClass); + + Implementation.Composable fieldAssignment = + FieldAccessor.ofField(DELEGATE_FIELD_NAME) + .setsValue( + new StackManipulation.Compound( + MethodVariableAccess.allArgumentsOf(proxiedConstructor), + MethodInvocation.invoke(indyBootstrapMethod) + .dynamic( + "ctor", // the actual method name is not allowed by the verifier + TypeDescription.ForLoadedType.of(Object.class), + proxiedConstructor.getParameters().asTypeList().asErasures(), + bootstrapArgs)), + Object.class); + Implementation.Composable ctorBody = + MethodCall.invoke(defaultSuperCtor).andThen(fieldAssignment); + return createProxyMethodOrConstructor( + proxiedConstructor, builder.defineConstructor(Modifier.PUBLIC), ctorBody); + } + + private static MethodDescription findDefaultConstructor(TypeDescription.Generic superClass) { + return superClass.getDeclaredMethods().stream() + .filter(MethodDescription::isConstructor) + .filter(constructor -> constructor.getParameters().isEmpty()) + .findFirst() + .orElseThrow( + () -> + new IllegalArgumentException( + "Superclass of provided type does not define a default constructor")); + } + + private static DynamicType.Builder createProxyMethodOrConstructor( + MethodDescription.InDefinedShape method, + DynamicType.Builder.MethodDefinition.ParameterDefinition methodDef, + Implementation methodBody) { + for (ParameterDescription param : method.getParameters()) { + methodDef = + methodDef + .withParameter(param.getType(), param.getName(), param.getModifiers()) + .annotateParameter(param.getDeclaredAnnotations()); + } + return methodDef + .throwing(method.getExceptionTypes()) + .intercept(methodBody) + .annotateMethod(method.getDeclaredAnnotations()); + } +} diff --git a/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/instrumentation/indy/IndyTypeTransformerImpl.java b/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/instrumentation/indy/IndyTypeTransformerImpl.java new file mode 100644 index 000000000000..df7f67ded219 --- /dev/null +++ b/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/instrumentation/indy/IndyTypeTransformerImpl.java @@ -0,0 +1,125 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.tooling.instrumentation.indy; + +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.tooling.bytebuddy.ExceptionHandlers; +import java.io.FileOutputStream; +import java.io.IOException; +import net.bytebuddy.agent.builder.AgentBuilder; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.method.MethodDescription; +import net.bytebuddy.dynamic.ClassFileLocator; +import net.bytebuddy.dynamic.ClassFileLocator.Resolution; +import net.bytebuddy.matcher.ElementMatcher; + +public final class IndyTypeTransformerImpl implements TypeTransformer { + // path (with trailing slash) to dump transformed advice class to + private static final String DUMP_PATH = null; + private final Advice.WithCustomMapping adviceMapping; + private AgentBuilder.Identified.Extendable agentBuilder; + private final InstrumentationModule instrumentationModule; + + public IndyTypeTransformerImpl( + AgentBuilder.Identified.Extendable agentBuilder, InstrumentationModule module) { + this.agentBuilder = agentBuilder; + this.instrumentationModule = module; + this.adviceMapping = + Advice.withCustomMapping() + .with( + new ForceDynamicallyTypedAssignReturnedFactory( + new Advice.AssignReturned.Factory().withSuppressed(Throwable.class))) + .bootstrap( + IndyBootstrap.getIndyBootstrapMethod(), + IndyBootstrap.getAdviceBootstrapArguments(instrumentationModule)); + } + + @Override + public void applyAdviceToMethod( + ElementMatcher methodMatcher, String adviceClassName) { + agentBuilder = + agentBuilder.transform( + new AgentBuilder.Transformer.ForAdvice(adviceMapping) + .advice(methodMatcher, adviceClassName) + .include(getAdviceLocator(instrumentationModule.getClass().getClassLoader())) + .withExceptionHandler(ExceptionHandlers.defaultExceptionHandler())); + } + + private static ClassFileLocator getAdviceLocator(ClassLoader classLoader) { + ClassFileLocator classFileLocator = ClassFileLocator.ForClassLoader.of(classLoader); + return new AdviceLocator(classFileLocator); + } + + @Override + public void applyTransformer(AgentBuilder.Transformer transformer) { + agentBuilder = agentBuilder.transform(transformer); + } + + public AgentBuilder.Identified.Extendable getAgentBuilder() { + return agentBuilder; + } + + private static class AdviceLocator implements ClassFileLocator { + private final ClassFileLocator delegate; + + AdviceLocator(ClassFileLocator delegate) { + this.delegate = delegate; + } + + @Override + public Resolution locate(String name) throws IOException { + Resolution resolution = delegate.locate(name); + return new AdviceTransformingResolution(name, resolution); + } + + @Override + public void close() throws IOException { + delegate.close(); + } + } + + private static class AdviceTransformingResolution implements Resolution { + private final String name; + private final Resolution delegate; + + AdviceTransformingResolution(String name, Resolution delegate) { + this.name = name; + this.delegate = delegate; + } + + @Override + public boolean isResolved() { + return delegate.isResolved(); + } + + @Override + public byte[] resolve() { + byte[] bytes = delegate.resolve(); + byte[] result = AdviceTransformer.transform(bytes); + if (result != null) { + dump(name, result); + InstrumentationModuleClassLoader.bytecodeOverride.put(name.replace('/', '.'), result); + } else { + result = bytes; + } + result = AdviceSignatureEraser.transform(result); + return result; + } + } + + private static void dump(String name, byte[] bytes) { + if (DUMP_PATH == null) { + return; + } + try (FileOutputStream fos = + new FileOutputStream(DUMP_PATH + name.replace('/', '.') + ".class")) { + fos.write(bytes); + } catch (IOException e) { + throw new IllegalStateException(e); + } + } +} diff --git a/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/instrumentation/indy/InstrumentationModuleClassLoader.java b/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/instrumentation/indy/InstrumentationModuleClassLoader.java new file mode 100644 index 000000000000..b4d2477dd57e --- /dev/null +++ b/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/instrumentation/indy/InstrumentationModuleClassLoader.java @@ -0,0 +1,387 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.tooling.instrumentation.indy; + +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.extension.instrumentation.internal.ExperimentalInstrumentationModule; +import io.opentelemetry.javaagent.tooling.BytecodeWithUrl; +import io.opentelemetry.javaagent.tooling.muzzle.InstrumentationModuleMuzzle; +import java.io.IOException; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.net.URL; +import java.security.PrivilegedAction; +import java.security.ProtectionDomain; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; +import javax.annotation.Nullable; +import net.bytebuddy.agent.builder.AgentBuilder; +import net.bytebuddy.description.method.MethodDescription; +import net.bytebuddy.matcher.ElementMatcher; +import net.bytebuddy.matcher.StringMatcher; + +/** + * Class loader used to load the helper classes from {@link + * io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule}s, so that those + * classes have access to both the agent/extension classes and the instrumented application classes. + * + *

      This class loader implements the following classloading delegation strategy: + * + *

        + *
      • First, injected classes are considered (usually the helper classes from the + * InstrumentationModule) + *
      • Next, the class loader looks in the agent or extension class loader, depending on where the + * InstrumentationModule comes from + *
      • Finally, the instrumented application class loader is checked for the class + *
      + * + *

      In addition, this class loader ensures that the lookup of corresponding .class resources + * follow the same delegation strategy, so that bytecode inspection tools work correctly. + */ +public class InstrumentationModuleClassLoader extends ClassLoader { + + static { + ClassLoader.registerAsParallelCapable(); + } + + private static final ClassLoader BOOT_LOADER = new ClassLoader() {}; + + private static final Map ALWAYS_INJECTED_CLASSES = + Collections.singletonMap( + LookupExposer.class.getName(), BytecodeWithUrl.create(LookupExposer.class).cached()); + private static final ProtectionDomain PROTECTION_DOMAIN = getProtectionDomain(); + private static final MethodHandle FIND_PACKAGE_METHOD = getFindPackageMethod(); + + private final Map additionalInjectedClasses; + private final ClassLoader agentOrExtensionCl; + private volatile MethodHandles.Lookup cachedLookup; + + @Nullable private final ClassLoader instrumentedCl; + + /** + * Only class names matching this matcher will be attempted to be loaded from the {@link + * #agentOrExtensionCl}. If a class is requested and it does not match this matcher, the lookup in + * {@link #agentOrExtensionCl} will be skipped. + */ + private final ElementMatcher agentClassNamesMatcher; + + /** + * Mutable set of packages from the agent classloader to hide. So even if a class matches {@link + * #agentClassNamesMatcher}, it will not be attempted to be loaded from the agent classloader if + * it is from any of these packages. + */ + private final Set hiddenAgentPackages; + + private final Set installedModules; + + public InstrumentationModuleClassLoader( + ClassLoader instrumentedCl, ClassLoader agentOrExtensionCl) { + this( + instrumentedCl, + agentOrExtensionCl, + new StringMatcher("io.opentelemetry.javaagent", StringMatcher.Mode.STARTS_WITH)); + } + + InstrumentationModuleClassLoader( + @Nullable ClassLoader instrumentedCl, + ClassLoader agentOrExtensionCl, + ElementMatcher classesToLoadFromAgentOrExtensionCl) { + // agent/extension-class loader is "main"-parent, but class lookup is overridden + super(agentOrExtensionCl); + additionalInjectedClasses = new ConcurrentHashMap<>(); + installedModules = Collections.newSetFromMap(new ConcurrentHashMap<>()); + this.agentOrExtensionCl = agentOrExtensionCl; + this.instrumentedCl = instrumentedCl; + this.agentClassNamesMatcher = classesToLoadFromAgentOrExtensionCl; + this.hiddenAgentPackages = Collections.newSetFromMap(new ConcurrentHashMap<>()); + } + + /** + * Provides a Lookup within this class loader. See {@link LookupExposer} for the details. + * + * @return a lookup capable of accessing public types in this class loader + */ + public MethodHandles.Lookup getLookup() { + if (cachedLookup == null) { + // Load the injected copy of LookupExposer and invoke it + try { + MethodType getLookupType = MethodType.methodType(MethodHandles.Lookup.class); + // we don't mind the race condition causing the initialization to run multiple times here + Class lookupExposer = loadClass(LookupExposer.class.getName()); + // Note: we must use MethodHandles instead of reflection here to avoid a recursion + // for our internal ReflectionInstrumentationModule which instruments reflection methods + cachedLookup = + (MethodHandles.Lookup) + MethodHandles.publicLookup() + .findStatic(lookupExposer, "getLookup", getLookupType) + .invoke(); + } catch (Throwable e) { + throw new IllegalStateException(e); + } + } + return cachedLookup; + } + + public synchronized void installModule(InstrumentationModule module) { + if (module.getClass().getClassLoader() != agentOrExtensionCl) { + throw new IllegalArgumentException( + module.getClass().getName() + " is not loaded by " + agentOrExtensionCl); + } + if (!installedModules.add(module)) { + return; + } + Map classesToInject = + getClassesToInject(module).stream() + .collect( + Collectors.toMap( + className -> className, + className -> BytecodeWithUrl.create(className, agentOrExtensionCl))); + installInjectedClasses(classesToInject); + if (module instanceof ExperimentalInstrumentationModule) { + hiddenAgentPackages.addAll( + ((ExperimentalInstrumentationModule) module).agentPackagesToHide()); + } + } + + public synchronized boolean hasModuleInstalled(InstrumentationModule module) { + return installedModules.contains(module); + } + + // Visible for testing + synchronized void installInjectedClasses(Map classesToInject) { + classesToInject.forEach(additionalInjectedClasses::putIfAbsent); + } + + private static Set getClassesToInject(InstrumentationModule module) { + Set toInject = new HashSet<>(InstrumentationModuleMuzzle.getHelperClassNames(module)); + // TODO (Jonas): Make muzzle include advice classes as helper classes + // so that we don't have to include them here + toInject.addAll(getModuleAdviceNames(module)); + if (module instanceof ExperimentalInstrumentationModule) { + toInject.removeAll(((ExperimentalInstrumentationModule) module).injectedClassNames()); + } + return toInject; + } + + private static Set getModuleAdviceNames(InstrumentationModule module) { + Set adviceNames = new HashSet<>(); + TypeTransformer nameCollector = + new TypeTransformer() { + @Override + public void applyAdviceToMethod( + ElementMatcher methodMatcher, String adviceClassName) { + adviceNames.add(adviceClassName); + } + + @Override + public void applyTransformer(AgentBuilder.Transformer transformer) {} + }; + for (TypeInstrumentation instr : module.typeInstrumentations()) { + instr.transform(nameCollector); + } + return adviceNames; + } + + public static final Map bytecodeOverride = new ConcurrentHashMap<>(); + + @Override + @SuppressWarnings("removal") // AccessController is deprecated for removal + protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException { + synchronized (getClassLoadingLock(name)) { + Class result = findLoadedClass(name); + + // This CL is self-first: Injected class are loaded BEFORE a parent lookup + if (result == null) { + BytecodeWithUrl injected = getInjectedClass(name); + if (injected != null) { + byte[] bytecode = + bytecodeOverride.get(name) != null + ? bytecodeOverride.get(name) + : injected.getBytecode(); + if (System.getSecurityManager() == null) { + result = defineClassWithPackage(name, bytecode); + } else { + result = + java.security.AccessController.doPrivileged( + (PrivilegedAction>) () -> defineClassWithPackage(name, bytecode)); + } + } + } + if (result == null && shouldLoadFromAgent(name)) { + result = tryLoad(agentOrExtensionCl, name); + } + if (result == null) { + result = tryLoad(instrumentedCl, name); + } + + if (result != null) { + if (resolve) { + resolveClass(result); + } + return result; + } else { + throw new ClassNotFoundException(name); + } + } + } + + private boolean shouldLoadFromAgent(String dotClassName) { + if (!agentClassNamesMatcher.matches(dotClassName)) { + return false; + } + for (String packageName : hiddenAgentPackages) { + if (dotClassName.startsWith(packageName)) { + return false; + } + } + return true; + } + + private static Class tryLoad(@Nullable ClassLoader cl, String name) { + try { + return Class.forName(name, false, cl); + } catch (ClassNotFoundException e) { + return null; + } + } + + @Override + public URL getResource(String resourceName) { + String className = resourceToClassName(resourceName); + if (className == null) { + // delegate to just the default parent (the agent class loader) + return super.getResource(resourceName); + } + // for classes use the same precedence as in loadClass + BytecodeWithUrl injected = getInjectedClass(className); + if (injected != null) { + return injected.getUrl(); + } + URL fromAgentCl = agentOrExtensionCl.getResource(resourceName); + if (fromAgentCl != null) { + return fromAgentCl; + } + + if (instrumentedCl != null) { + return instrumentedCl.getResource(resourceName); + } else { + return BOOT_LOADER.getResource(resourceName); + } + } + + @Override + public Enumeration getResources(String resourceName) throws IOException { + String className = resourceToClassName(resourceName); + if (className == null) { + return super.getResources(resourceName); + } + URL resource = getResource(resourceName); + List result = + resource != null ? Collections.singletonList(resource) : Collections.emptyList(); + return Collections.enumeration(result); + } + + @Nullable + private static String resourceToClassName(String resourceName) { + if (!resourceName.endsWith(".class")) { + return null; + } + String className = resourceName; + if (className.startsWith("/")) { + className = className.substring(1); + } + className = className.replace('/', '.'); + className = className.substring(0, className.length() - ".class".length()); + return className; + } + + @Nullable + private BytecodeWithUrl getInjectedClass(String name) { + BytecodeWithUrl alwaysInjected = ALWAYS_INJECTED_CLASSES.get(name); + if (alwaysInjected != null) { + return alwaysInjected; + } + return additionalInjectedClasses.get(name); + } + + private Class defineClassWithPackage(String name, byte[] bytecode) { + int lastDotIndex = name.lastIndexOf('.'); + if (lastDotIndex != -1) { + String packageName = name.substring(0, lastDotIndex); + safeDefinePackage(packageName); + } + return defineClass(name, bytecode, 0, bytecode.length, PROTECTION_DOMAIN); + } + + private void safeDefinePackage(String packageName) { + if (findPackage(packageName) == null) { + try { + definePackage(packageName, null, null, null, null, null, null, null); + } catch (IllegalArgumentException e) { + // Can happen if two classes from the same package are loaded concurrently + if (findPackage(packageName) == null) { + // package still doesn't exist, the IllegalArgumentException must be for a different + // reason than a race condition + throw e; + } + } + } + } + + /** + * Invokes {@link #getPackage(String)} for Java 8 and {@link #getDefinedPackage(String)} for Java + * 9+. + * + *

      Package-private for testing. + * + * @param name the name of the package find + * @return the found package or null if it was not found. + */ + @SuppressWarnings({"deprecation", "InvalidLink"}) + Package findPackage(String name) { + try { + return (Package) FIND_PACKAGE_METHOD.invoke(this, name); + } catch (Throwable t) { + throw new IllegalStateException(t); + } + } + + @SuppressWarnings("removal") // AccessController is deprecated for removal + private static ProtectionDomain getProtectionDomain() { + if (System.getSecurityManager() == null) { + return InstrumentationModuleClassLoader.class.getProtectionDomain(); + } + return java.security.AccessController.doPrivileged( + (PrivilegedAction) + ((Class) InstrumentationModuleClassLoader.class)::getProtectionDomain); + } + + private static MethodHandle getFindPackageMethod() { + MethodType methodType = MethodType.methodType(Package.class, String.class); + MethodHandles.Lookup lookup = MethodHandles.lookup(); + try { + return lookup.findVirtual(ClassLoader.class, "getDefinedPackage", methodType); + } catch (NoSuchMethodException | IllegalAccessException e) { + // In Java 8 getDefinedPackage does not exist (HotSpot) or is not accessible (OpenJ9) + try { + return lookup.findVirtual(ClassLoader.class, "getPackage", methodType); + } catch (NoSuchMethodException ex) { + throw new IllegalStateException("expected method to always exist!", ex); + } catch (IllegalAccessException ex2) { + throw new IllegalStateException("Method should be accessible from here", ex2); + } + } + } +} diff --git a/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/instrumentation/indy/LookupExposer.java b/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/instrumentation/indy/LookupExposer.java new file mode 100644 index 000000000000..fc9f55026626 --- /dev/null +++ b/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/instrumentation/indy/LookupExposer.java @@ -0,0 +1,27 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.tooling.instrumentation.indy; + +import java.lang.invoke.MethodHandles; + +/** + * This class is injected into every {@link InstrumentationModuleClassLoader} so that the bootstrap + * can use a {@link MethodHandles.Lookup} with a lookup class from within the {@link + * InstrumentationModuleClassLoader}, instead of calling {@link MethodHandles#lookup()} which uses + * the caller class as the lookup class. + * + *

      This circumvents a nasty JVM bug that's described here. The error is reproduced in + * {@code InstrumentationModuleClassLoaderTest} + */ +public class LookupExposer { + + private LookupExposer() {} + + public static MethodHandles.Lookup getLookup() { + return MethodHandles.lookup(); + } +} diff --git a/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/instrumentation/indy/OriginalDescriptor.java b/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/instrumentation/indy/OriginalDescriptor.java new file mode 100644 index 000000000000..2104696965fc --- /dev/null +++ b/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/instrumentation/indy/OriginalDescriptor.java @@ -0,0 +1,10 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.tooling.instrumentation.indy; + +public @interface OriginalDescriptor { + String value(); +} diff --git a/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/instrumentation/indy/PatchByteCodeVersionTransformer.java b/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/instrumentation/indy/PatchByteCodeVersionTransformer.java new file mode 100644 index 000000000000..cc5a4f9d7245 --- /dev/null +++ b/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/instrumentation/indy/PatchByteCodeVersionTransformer.java @@ -0,0 +1,103 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.tooling.instrumentation.indy; + +import static org.objectweb.asm.ClassWriter.COMPUTE_FRAMES; + +import java.security.ProtectionDomain; +import net.bytebuddy.ClassFileVersion; +import net.bytebuddy.agent.builder.AgentBuilder; +import net.bytebuddy.asm.Advice; +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.dynamic.DynamicType; +import net.bytebuddy.implementation.Implementation; +import net.bytebuddy.pool.TypePool; +import net.bytebuddy.utility.JavaModule; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.commons.JSRInlinerAdapter; + +/** + * Patches the class file version to 51 (Java 7) in order to support injecting {@code INVOKEDYNAMIC} + * instructions via {@link Advice.WithCustomMapping#bootstrap} which is important for indy plugins. + */ +public class PatchByteCodeVersionTransformer implements AgentBuilder.Transformer { + + private static boolean isAtLeastJava7(TypeDescription typeDescription) { + ClassFileVersion classFileVersion = typeDescription.getClassFileVersion(); + return classFileVersion != null && classFileVersion.getJavaVersion() >= 7; + } + + @Override + public DynamicType.Builder transform( + DynamicType.Builder builder, + TypeDescription typeDescription, + ClassLoader classLoader, + JavaModule javaModule, + ProtectionDomain protectionDomain) { + + if (isAtLeastJava7(typeDescription)) { + // we can avoid the expensive stack frame re-computation if stack frames are already present + // in the bytecode. + return builder; + } + return builder.visit( + new AsmVisitorWrapper.AbstractBase() { + @Override + public ClassVisitor wrap( + TypeDescription typeDescription, + ClassVisitor classVisitor, + Implementation.Context context, + TypePool typePool, + FieldList fieldList, + MethodList methodList, + int writerFlags, + int readerFlags) { + + return new ClassVisitor(Opcodes.ASM7, classVisitor) { + + @Override + public void visit( + int version, + int access, + String name, + String signature, + String superName, + String[] interfaces) { + + super.visit(Opcodes.V1_7, access, name, signature, superName, interfaces); + } + + @Override + public MethodVisitor visitMethod( + int access, + String name, + String descriptor, + String signature, + String[] exceptions) { + + MethodVisitor methodVisitor = + super.visitMethod(access, name, descriptor, signature, exceptions); + return new JSRInlinerAdapter( + methodVisitor, access, name, descriptor, signature, exceptions); + } + }; + } + + @Override + public int mergeWriter(int flags) { + // class files with version < Java 7 don't require a stack frame map + // as we're patching the version to at least 7, we have to compute the frames + return flags | COMPUTE_FRAMES; + } + }); + } +} diff --git a/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/util/ClassLoaderMap.java b/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/util/ClassLoaderMap.java new file mode 100644 index 000000000000..aedadef84eac --- /dev/null +++ b/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/util/ClassLoaderMap.java @@ -0,0 +1,89 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.tooling.util; + +import io.opentelemetry.instrumentation.api.internal.cache.Cache; +import java.lang.ref.WeakReference; +import java.lang.reflect.Field; +import java.util.Collections; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Supplier; +import net.bytebuddy.ByteBuddy; +import net.bytebuddy.description.modifier.Ownership; +import net.bytebuddy.description.modifier.Visibility; +import net.bytebuddy.dynamic.loading.ClassLoadingStrategy; + +class ClassLoaderMap { + private static final Cache>> data = Cache.weak(); + + public static Object get(ClassLoader classLoader, Object key) { + return getClassLoaderData(classLoader, false).get(key); + } + + public static void put(ClassLoader classLoader, Object key, Object value) { + getClassLoaderData(classLoader, true).put(key, value); + } + + public static Object computeIfAbsent( + ClassLoader classLoader, Object key, Supplier value) { + return getClassLoaderData(classLoader, true).computeIfAbsent(key, unused -> value.get()); + } + + private static Map getClassLoaderData( + ClassLoader classLoader, boolean initialize) { + classLoader = maskNullClassLoader(classLoader); + WeakReference> weakReference = data.get(classLoader); + Map map = weakReference != null ? weakReference.get() : null; + if (map == null) { + // skip setting up the map if get was called + if (!initialize) { + return Collections.emptyMap(); + } + map = createMap(classLoader); + data.put(classLoader, new WeakReference<>(map)); + } + return map; + } + + @SuppressWarnings("unchecked") + private static Map createMap(ClassLoader classLoader) { + // generate a class with a single static field named "data" and define it in the given class + // loader + Class clazz = + new ByteBuddy() + .subclass(Object.class) + .name( + "io.opentelemetry.javaagent.ClassLoaderData$$" + + Integer.toHexString(System.identityHashCode(classLoader))) + .defineField("data", Object.class, Ownership.STATIC, Visibility.PUBLIC) + .make() + .load(classLoader, ClassLoadingStrategy.Default.INJECTION.allowExistingTypes()) + .getLoaded(); + Map map; + try { + Field field = clazz.getField("data"); + synchronized (classLoader) { + map = (Map) field.get(classLoader); + if (map == null) { + map = new ConcurrentHashMap<>(); + field.set(null, map); + } + } + } catch (Exception exception) { + throw new IllegalStateException(exception); + } + return map; + } + + private static final ClassLoader BOOT_LOADER = new ClassLoader(null) {}; + + private static ClassLoader maskNullClassLoader(ClassLoader classLoader) { + return classLoader == null ? BOOT_LOADER : classLoader; + } + + private ClassLoaderMap() {} +} diff --git a/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/util/ClassLoaderValue.java b/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/util/ClassLoaderValue.java new file mode 100644 index 000000000000..6288d8f97217 --- /dev/null +++ b/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/util/ClassLoaderValue.java @@ -0,0 +1,31 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.tooling.util; + +import java.util.function.Supplier; + +/** + * Associate value with a class loader. Added value will behave as if it was stored in a field in + * the class loader object, meaning that the value can be garbage collected once the class loader is + * garbage collected and referencing the class loader from the value will not prevent garbage + * collector from collecting the class loader. + */ +public final class ClassLoaderValue { + + @SuppressWarnings("unchecked") + public T get(ClassLoader classLoader) { + return (T) ClassLoaderMap.get(classLoader, this); + } + + public void put(ClassLoader classLoader, T value) { + ClassLoaderMap.put(classLoader, this, value); + } + + @SuppressWarnings("unchecked") + public T computeIfAbsent(ClassLoader classLoader, Supplier value) { + return (T) ClassLoaderMap.computeIfAbsent(classLoader, this, value); + } +} diff --git a/javaagent-tooling/src/main/java/io/opentelemetry/sdk/autoconfigure/SdkAutoconfigureAccess.java b/javaagent-tooling/src/main/java/io/opentelemetry/sdk/autoconfigure/SdkAutoconfigureAccess.java new file mode 100644 index 000000000000..477f5bcbdee4 --- /dev/null +++ b/javaagent-tooling/src/main/java/io/opentelemetry/sdk/autoconfigure/SdkAutoconfigureAccess.java @@ -0,0 +1,16 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.autoconfigure; + +import io.opentelemetry.api.common.Attributes; + +public final class SdkAutoconfigureAccess { + public static Attributes getResourceAttributes(AutoConfiguredOpenTelemetrySdk sdk) { + return sdk.getResource().getAttributes(); + } + + private SdkAutoconfigureAccess() {} +} diff --git a/javaagent-tooling/src/main/java/net/bytebuddy/agent/builder/AgentBuilderUtil.java b/javaagent-tooling/src/main/java/net/bytebuddy/agent/builder/AgentBuilderUtil.java index cdaffd95aeb1..d596dc81ecf3 100644 --- a/javaagent-tooling/src/main/java/net/bytebuddy/agent/builder/AgentBuilderUtil.java +++ b/javaagent-tooling/src/main/java/net/bytebuddy/agent/builder/AgentBuilderUtil.java @@ -24,6 +24,7 @@ import net.bytebuddy.matcher.ElementMatcher; import net.bytebuddy.matcher.ErasureMatcher; import net.bytebuddy.matcher.HasSuperClassMatcher; +import net.bytebuddy.matcher.HasSuperTypeMatcher; import net.bytebuddy.matcher.NameMatcher; import net.bytebuddy.matcher.StringMatcher; import net.bytebuddy.matcher.StringSetMatcher; @@ -42,6 +43,8 @@ public class AgentBuilderUtil { private static final Field nameMatcherField = getField(NameMatcher.class, "matcher"); private static final Field hasSuperClassMatcherField = getField(HasSuperClassMatcher.class, "matcher"); + private static final Field hasSuperTypeMatcherField = + getField(HasSuperTypeMatcher.class, "matcher"); private static final Field erasureMatcherField = getField(ErasureMatcher.class, "matcher"); private static final Field conjunctionMatchersField = getField(ElementMatcher.Junction.Conjunction.class, "matchers"); @@ -160,6 +163,8 @@ private static Result inspect(ElementMatcher matcher) throws Exception { return result; } else if (matcher instanceof HasSuperClassMatcher) { return Result.subtype(inspect(getDelegateMatcher((HasSuperClassMatcher) matcher))); + } else if (matcher instanceof HasSuperTypeMatcher) { + return Result.subtype(inspect(getDelegateMatcher((HasSuperTypeMatcher) matcher))); } else if (matcher instanceof ErasureMatcher) { return inspect(getDelegateMatcher((ErasureMatcher) matcher)); } else if (matcher instanceof NameMatcher) { @@ -257,6 +262,11 @@ private static ElementMatcher getDelegateMatcher(HasSuperClassMatcher matc return (ElementMatcher) hasSuperClassMatcherField.get(matcher); } + private static ElementMatcher getDelegateMatcher(HasSuperTypeMatcher matcher) + throws Exception { + return (ElementMatcher) hasSuperTypeMatcherField.get(matcher); + } + private static ElementMatcher getDelegateMatcher(ErasureMatcher matcher) throws Exception { return (ElementMatcher) erasureMatcherField.get(matcher); } diff --git a/javaagent-tooling/src/test/groovy/io/opentelemetry/javaagent/test/HelperInjectionTest.groovy b/javaagent-tooling/src/test/groovy/io/opentelemetry/javaagent/test/HelperInjectionTest.groovy index 1bada094858b..7f2882806cd5 100644 --- a/javaagent-tooling/src/test/groovy/io/opentelemetry/javaagent/test/HelperInjectionTest.groovy +++ b/javaagent-tooling/src/test/groovy/io/opentelemetry/javaagent/test/HelperInjectionTest.groovy @@ -17,6 +17,7 @@ import net.bytebuddy.dynamic.loading.ClassInjector import spock.lang.Specification import java.lang.ref.WeakReference +import java.time.Duration import java.util.concurrent.atomic.AtomicReference import static io.opentelemetry.instrumentation.test.utils.ClasspathUtils.isClassLoaded @@ -53,7 +54,7 @@ class HelperInjectionTest extends Specification { def ref = new WeakReference(emptyLoader.get()) emptyLoader.set(null) - awaitGc(ref) + awaitGc(ref, Duration.ofSeconds(10)) then: "HelperInjector doesn't prevent it from being collected" null == ref.get() @@ -100,7 +101,7 @@ class HelperInjectionTest extends Specification { def injectorRef = new WeakReference(injector.get()) injector.set(null) - awaitGc(injectorRef) + awaitGc(injectorRef, Duration.ofSeconds(10)) then: null == injectorRef.get() @@ -109,7 +110,7 @@ class HelperInjectionTest extends Specification { def loaderRef = new WeakReference(emptyLoader.get()) emptyLoader.set(null) - awaitGc(loaderRef) + awaitGc(loaderRef, Duration.ofSeconds(10)) then: null == loaderRef.get() diff --git a/javaagent-tooling/src/test/groovy/io/opentelemetry/javaagent/tooling/AddThreadDetailsSpanProcessorTest.groovy b/javaagent-tooling/src/test/groovy/io/opentelemetry/javaagent/tooling/AddThreadDetailsSpanProcessorTest.groovy index 8aeacb7f47f8..d3a8a8c933db 100644 --- a/javaagent-tooling/src/test/groovy/io/opentelemetry/javaagent/tooling/AddThreadDetailsSpanProcessorTest.groovy +++ b/javaagent-tooling/src/test/groovy/io/opentelemetry/javaagent/tooling/AddThreadDetailsSpanProcessorTest.groovy @@ -7,7 +7,7 @@ package io.opentelemetry.javaagent.tooling import io.opentelemetry.context.Context import io.opentelemetry.sdk.trace.ReadWriteSpan -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import io.opentelemetry.semconv.incubating.ThreadIncubatingAttributes import spock.lang.Specification class AddThreadDetailsSpanProcessorTest extends Specification { @@ -29,7 +29,7 @@ class AddThreadDetailsSpanProcessorTest extends Specification { processor.onStart(Context.root(), span) then: - 1 * span.setAttribute(SemanticAttributes.THREAD_ID, currentThreadId) - 1 * span.setAttribute(SemanticAttributes.THREAD_NAME, currentThreadName) + 1 * span.setAttribute(ThreadIncubatingAttributes.THREAD_ID, currentThreadId) + 1 * span.setAttribute(ThreadIncubatingAttributes.THREAD_NAME, currentThreadName) } } diff --git a/javaagent-tooling/src/test/groovy/io/opentelemetry/javaagent/tooling/OpenTelemetryInstallerTest.groovy b/javaagent-tooling/src/test/groovy/io/opentelemetry/javaagent/tooling/OpenTelemetryInstallerTest.groovy index dc3533d55e92..069ce16b88a4 100755 --- a/javaagent-tooling/src/test/groovy/io/opentelemetry/javaagent/tooling/OpenTelemetryInstallerTest.groovy +++ b/javaagent-tooling/src/test/groovy/io/opentelemetry/javaagent/tooling/OpenTelemetryInstallerTest.groovy @@ -7,19 +7,19 @@ package io.opentelemetry.javaagent.tooling import io.opentelemetry.api.GlobalOpenTelemetry import io.opentelemetry.api.OpenTelemetry -import io.opentelemetry.api.events.GlobalEventEmitterProvider +import io.opentelemetry.api.incubator.events.GlobalEventLoggerProvider import spock.lang.Specification class OpenTelemetryInstallerTest extends Specification { void setup() { GlobalOpenTelemetry.resetForTest() - GlobalEventEmitterProvider.resetForTest() + GlobalEventLoggerProvider.resetForTest() } void cleanup() { GlobalOpenTelemetry.resetForTest() - GlobalEventEmitterProvider.resetForTest() + GlobalEventLoggerProvider.resetForTest() } def "should initialize GlobalOpenTelemetry"() { @@ -31,13 +31,4 @@ class OpenTelemetryInstallerTest extends Specification { GlobalOpenTelemetry.get() != OpenTelemetry.noop() } - def "should disable the logs exporter by default"() { - when: - def autoConfiguredSdk = OpenTelemetryInstaller.installOpenTelemetrySdk(OpenTelemetryInstaller.classLoader) - - then: - autoConfiguredSdk != null - autoConfiguredSdk.config.getString("otel.logs.exporter") == "none" - } - } diff --git a/javaagent-tooling/src/test/groovy/io/opentelemetry/javaagent/tooling/config/MethodsConfigurationParserTest.groovy b/javaagent-tooling/src/test/groovy/io/opentelemetry/javaagent/tooling/config/MethodsConfigurationParserTest.groovy index 3f8cbf4c9bca..1fe11b29b7f4 100644 --- a/javaagent-tooling/src/test/groovy/io/opentelemetry/javaagent/tooling/config/MethodsConfigurationParserTest.groovy +++ b/javaagent-tooling/src/test/groovy/io/opentelemetry/javaagent/tooling/config/MethodsConfigurationParserTest.groovy @@ -8,6 +8,8 @@ package io.opentelemetry.javaagent.tooling.config import spock.lang.Specification +import static java.util.Collections.emptySet + class MethodsConfigurationParserTest extends Specification { def "test configuration #value"() { @@ -18,7 +20,7 @@ class MethodsConfigurationParserTest extends Specification { value | expected null | [:] " " | [:] - "some.package.ClassName" | [:] + "some.package.ClassName" | ["some.package.ClassName":emptySet()] "some.package.ClassName[ , ]" | [:] "some.package.ClassName[ , method]" | [:] "some.package.Class\$Name[ method , ]" | ["some.package.Class\$Name": ["method"].toSet()] diff --git a/javaagent-tooling/src/test/java/io/opentelemetry/javaagent/tooling/config/AgentConfigTest.java b/javaagent-tooling/src/test/java/io/opentelemetry/javaagent/tooling/config/AgentConfigTest.java index e4b36be6c51a..ab428499269d 100644 --- a/javaagent-tooling/src/test/java/io/opentelemetry/javaagent/tooling/config/AgentConfigTest.java +++ b/javaagent-tooling/src/test/java/io/opentelemetry/javaagent/tooling/config/AgentConfigTest.java @@ -25,16 +25,14 @@ class AgentConfigTest { @ArgumentsSource(InstrumentationEnabledParams.class) void testIsInstrumentationEnabled( @SuppressWarnings("unused") String description, - boolean firstEnabled, - boolean secondEnabled, + Boolean firstEnabled, + Boolean secondEnabled, boolean defaultEnabled, boolean expected) { ConfigProperties config = mock(ConfigProperties.class); - when(config.getBoolean("otel.instrumentation.first.enabled", defaultEnabled)) - .thenReturn(firstEnabled); - when(config.getBoolean("otel.instrumentation.second.enabled", defaultEnabled)) - .thenReturn(secondEnabled); + when(config.getBoolean("otel.instrumentation.first.enabled")).thenReturn(firstEnabled); + when(config.getBoolean("otel.instrumentation.second.enabled")).thenReturn(secondEnabled); assertEquals( expected, @@ -49,13 +47,41 @@ public Stream provideArguments(ExtensionContext context) { return Stream.of( Arguments.of( "enabled by default, both instrumentations are off", false, false, true, false), - Arguments.of("enabled by default, one instrumentation is on", true, false, true, false), + Arguments.of("enabled by default, first instrumentation is on", true, null, true, true), + Arguments.of("enabled by default, second instrumentation is on", null, true, true, true), Arguments.of("enabled by default, both instrumentations are on", true, true, true, true), + Arguments.of( + "enabled by default, first instrumentation is off, second is on", + false, + true, + true, + false), + Arguments.of( + "enabled by default, first instrumentation is on, second is off", + true, + false, + true, + true), + Arguments.of("enabled by default", null, null, true, true), Arguments.of( "disabled by default, both instrumentations are off", false, false, false, false), - Arguments.of("disabled by default, one instrumentation is on", true, false, false, true), + Arguments.of("disabled by default, first instrumentation is on", true, null, false, true), + Arguments.of( + "disabled by default, second instrumentation is on", null, true, false, true), + Arguments.of("disabled by default, both instrumentation are on", true, true, false, true), + Arguments.of( + "disabled by default, first instrumentation is off, second is on", + false, + true, + false, + false), Arguments.of( - "disabled by default, both instrumentation are on", true, true, false, true)); + "disabled by default, first instrumentation is on, second is off", + true, + false, + false, + true), + Arguments.of("disabled by default", null, null, false, false)); } } } diff --git a/javaagent-tooling/src/test/java/io/opentelemetry/javaagent/tooling/config/ConfigurationPropertiesSupplierTest.java b/javaagent-tooling/src/test/java/io/opentelemetry/javaagent/tooling/config/ConfigurationPropertiesSupplierTest.java index 5bc5af79faec..2b0ae675ecdc 100644 --- a/javaagent-tooling/src/test/java/io/opentelemetry/javaagent/tooling/config/ConfigurationPropertiesSupplierTest.java +++ b/javaagent-tooling/src/test/java/io/opentelemetry/javaagent/tooling/config/ConfigurationPropertiesSupplierTest.java @@ -10,9 +10,10 @@ import static org.assertj.core.api.Assertions.assertThat; import io.opentelemetry.api.GlobalOpenTelemetry; -import io.opentelemetry.api.events.GlobalEventEmitterProvider; +import io.opentelemetry.api.incubator.events.GlobalEventLoggerProvider; import io.opentelemetry.javaagent.tooling.OpenTelemetryInstaller; import io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdk; +import io.opentelemetry.sdk.autoconfigure.internal.AutoConfigureUtil; import io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizer; import io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizerProvider; import java.io.IOException; @@ -32,7 +33,7 @@ class ConfigurationPropertiesSupplierTest { @AfterAll static void cleanUp() { GlobalOpenTelemetry.resetForTest(); - GlobalEventEmitterProvider.resetForTest(); + GlobalEventLoggerProvider.resetForTest(); } // regression for https://github.com/open-telemetry/opentelemetry-java-instrumentation/issues/6696 @@ -49,7 +50,8 @@ void fileConfigOverwritesUserPropertiesSupplier(@TempDir Path tempDir) throws IO OpenTelemetryInstaller.installOpenTelemetrySdk(this.getClass().getClassLoader()); // then - assertThat(autoConfiguredSdk.getConfig().getString("custom.key")).isEqualTo("42"); + assertThat(AutoConfigureUtil.getConfig(autoConfiguredSdk).getString("custom.key")) + .isEqualTo("42"); } // SPI used in test diff --git a/javaagent-tooling/src/test/java/io/opentelemetry/javaagent/tooling/config/OtlpProtocolPropertiesSupplierTest.java b/javaagent-tooling/src/test/java/io/opentelemetry/javaagent/tooling/config/OtlpProtocolPropertiesSupplierTest.java new file mode 100644 index 000000000000..a2ce1a654eb9 --- /dev/null +++ b/javaagent-tooling/src/test/java/io/opentelemetry/javaagent/tooling/config/OtlpProtocolPropertiesSupplierTest.java @@ -0,0 +1,53 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.tooling.config; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.api.incubator.events.GlobalEventLoggerProvider; +import io.opentelemetry.javaagent.tooling.OpenTelemetryInstaller; +import io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdk; +import io.opentelemetry.sdk.autoconfigure.internal.AutoConfigureUtil; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.junitpioneer.jupiter.SetSystemProperty; + +class OtlpProtocolPropertiesSupplierTest { + + @AfterEach + void cleanUp() { + GlobalOpenTelemetry.resetForTest(); + GlobalEventLoggerProvider.resetForTest(); + } + + @SetSystemProperty( + key = "otel.exporter.otlp.protocol", + value = "grpc") // user explicitly sets grpc + @Test + void keepUserOtlpProtocolConfiguration() { + // when + AutoConfiguredOpenTelemetrySdk autoConfiguredSdk = + OpenTelemetryInstaller.installOpenTelemetrySdk(this.getClass().getClassLoader()); + + // then + assertThat( + AutoConfigureUtil.getConfig(autoConfiguredSdk).getString("otel.exporter.otlp.protocol")) + .isEqualTo("grpc"); + } + + @Test + void defaultHttpProtobufOtlpProtocolConfiguration() { + // when + AutoConfiguredOpenTelemetrySdk autoConfiguredSdk = + OpenTelemetryInstaller.installOpenTelemetrySdk(this.getClass().getClassLoader()); + + // then + assertThat( + AutoConfigureUtil.getConfig(autoConfiguredSdk).getString("otel.exporter.otlp.protocol")) + .isEqualTo("http/protobuf"); + } +} diff --git a/javaagent-tooling/src/test/java/io/opentelemetry/javaagent/tooling/ignore/UserExcludedClassLoadersConfigurerTest.java b/javaagent-tooling/src/test/java/io/opentelemetry/javaagent/tooling/ignore/UserExcludedClassLoadersConfigurerTest.java new file mode 100644 index 000000000000..166b702b98d3 --- /dev/null +++ b/javaagent-tooling/src/test/java/io/opentelemetry/javaagent/tooling/ignore/UserExcludedClassLoadersConfigurerTest.java @@ -0,0 +1,55 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.tooling.ignore; + +import static io.opentelemetry.javaagent.tooling.ignore.UserExcludedClassLoadersConfigurer.EXCLUDED_CLASS_LOADERS_CONFIG; +import static java.util.Arrays.asList; +import static java.util.Collections.emptyList; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +import io.opentelemetry.javaagent.extension.ignore.IgnoredTypesBuilder; +import io.opentelemetry.javaagent.extension.ignore.IgnoredTypesConfigurer; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +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 UserExcludedClassLoadersConfigurerTest { + + @Mock ConfigProperties config; + @Mock IgnoredTypesBuilder builder; + + IgnoredTypesConfigurer underTest = new UserExcludedClassLoadersConfigurer(); + + @Test + void shouldAddNothingToBuilderWhenPropertyIsEmpty() { + // when + underTest.configure(builder, config); + + // then + verifyNoInteractions(builder); + } + + @Test + void shouldIgnoreClassesAndPackages() { + // given + when(config.getList(EXCLUDED_CLASS_LOADERS_CONFIG, emptyList())) + .thenReturn( + asList("com.example.IgnoredClass", "com.example.ignored.*", "com.another_ignore")); + + // when + underTest.configure(builder, config); + + // then + verify(builder).ignoreClassLoader("com.example.IgnoredClass"); + verify(builder).ignoreClassLoader("com.example.ignored."); + verify(builder).ignoreClassLoader("com.another_ignore"); + } +} diff --git a/javaagent-tooling/src/test/java/io/opentelemetry/javaagent/tooling/instrumentation/indy/ForceDynamicallyTypedAssignReturnedFactoryTest.java b/javaagent-tooling/src/test/java/io/opentelemetry/javaagent/tooling/instrumentation/indy/ForceDynamicallyTypedAssignReturnedFactoryTest.java new file mode 100644 index 000000000000..ccba634f03ae --- /dev/null +++ b/javaagent-tooling/src/test/java/io/opentelemetry/javaagent/tooling/instrumentation/indy/ForceDynamicallyTypedAssignReturnedFactoryTest.java @@ -0,0 +1,113 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.tooling.instrumentation.indy; + +import static org.assertj.core.api.Assertions.assertThat; + +import net.bytebuddy.asm.Advice; +import net.bytebuddy.asm.Advice.AssignReturned; +import net.bytebuddy.asm.Advice.AssignReturned.ToArguments.ToArgument; +import net.bytebuddy.asm.Advice.AssignReturned.ToFields.ToField; +import net.bytebuddy.description.annotation.AnnotationDescription; +import net.bytebuddy.description.method.MethodDescription; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.implementation.bytecode.assign.Assigner; +import org.junit.jupiter.api.Test; + +public class ForceDynamicallyTypedAssignReturnedFactoryTest { + + @AssignReturned.ToFields(@ToField(value = "foo", index = 42)) + @AssignReturned.ToArguments(@ToArgument(value = 3, index = 7)) + @AssignReturned.ToReturned(index = 4) + @AssignReturned.ToThrown(index = 5) + @AssignReturned.ToThis(index = 6) + @AssignReturned.ToAllArguments(index = 7) + @Advice.OnMethodEnter + static void testMethod() {} + + @Test + public void checkTypingMadeDynamic() { + MethodDescription.InDefinedShape original = + TypeDescription.ForLoadedType.of(ForceDynamicallyTypedAssignReturnedFactoryTest.class) + .getDeclaredMethods() + .stream() + .filter(method -> method.getName().equals("testMethod")) + .findFirst() + .get(); + + ClassLoader cl = ForceDynamicallyTypedAssignReturnedFactoryTest.class.getClassLoader(); + + MethodDescription modified = + ForceDynamicallyTypedAssignReturnedFactory.forceDynamicTyping(original); + assertThat(modified.getDeclaredAnnotations()) + .hasSize(7) + .anySatisfy( + toFields -> { + assertThat(toFields.getAnnotationType().getName()) + .isEqualTo(AssignReturned.ToFields.class.getName()); + assertThat((AnnotationDescription[]) toFields.getValue("value").resolve()) + .hasSize(1) + .anySatisfy( + toField -> { + assertThat(toField.getValue("value").resolve()).isEqualTo("foo"); + assertThat(toField.getValue("index").resolve()).isEqualTo(42); + assertThat(toField.getValue("typing").load(cl).resolve()) + .isEqualTo(Assigner.Typing.DYNAMIC); + }); + }) + .anySatisfy( + toArguments -> { + assertThat(toArguments.getAnnotationType().getName()) + .isEqualTo(AssignReturned.ToArguments.class.getName()); + assertThat((AnnotationDescription[]) toArguments.getValue("value").resolve()) + .hasSize(1) + .anySatisfy( + toArgument -> { + assertThat(toArgument.getValue("value").resolve()).isEqualTo(3); + assertThat(toArgument.getValue("index").resolve()).isEqualTo(7); + assertThat(toArgument.getValue("typing").load(cl).resolve()) + .isEqualTo(Assigner.Typing.DYNAMIC); + }); + }) + .anySatisfy( + toReturned -> { + assertThat(toReturned.getAnnotationType().getName()) + .isEqualTo(AssignReturned.ToReturned.class.getName()); + assertThat(toReturned.getValue("index").resolve()).isEqualTo(4); + assertThat(toReturned.getValue("typing").load(cl).resolve()) + .isEqualTo(Assigner.Typing.DYNAMIC); + }) + .anySatisfy( + toThrown -> { + assertThat(toThrown.getAnnotationType().getName()) + .isEqualTo(AssignReturned.ToThrown.class.getName()); + assertThat(toThrown.getValue("index").resolve()).isEqualTo(5); + assertThat(toThrown.getValue("typing").load(cl).resolve()) + .isEqualTo(Assigner.Typing.DYNAMIC); + }) + .anySatisfy( + toThis -> { + assertThat(toThis.getAnnotationType().getName()) + .isEqualTo(AssignReturned.ToThis.class.getName()); + assertThat(toThis.getValue("index").resolve()).isEqualTo(6); + assertThat(toThis.getValue("typing").load(cl).resolve()) + .isEqualTo(Assigner.Typing.DYNAMIC); + }) + .anySatisfy( + toAllArguments -> { + assertThat(toAllArguments.getAnnotationType().getName()) + .isEqualTo(AssignReturned.ToAllArguments.class.getName()); + assertThat(toAllArguments.getValue("index").resolve()).isEqualTo(7); + assertThat(toAllArguments.getValue("typing").load(cl).resolve()) + .isEqualTo(Assigner.Typing.DYNAMIC); + }) + .anySatisfy( + onMethodEnter -> { + assertThat(onMethodEnter.getAnnotationType().getName()) + .isEqualTo(Advice.OnMethodEnter.class.getName()); + }); + } +} diff --git a/javaagent-tooling/src/test/java/io/opentelemetry/javaagent/tooling/instrumentation/indy/IndyProxyFactoryTest.java b/javaagent-tooling/src/test/java/io/opentelemetry/javaagent/tooling/instrumentation/indy/IndyProxyFactoryTest.java new file mode 100644 index 000000000000..c5d1c287013b --- /dev/null +++ b/javaagent-tooling/src/test/java/io/opentelemetry/javaagent/tooling/instrumentation/indy/IndyProxyFactoryTest.java @@ -0,0 +1,330 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.tooling.instrumentation.indy; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.javaagent.tooling.instrumentation.indy.dummies.DummyAnnotation; +import java.lang.invoke.CallSite; +import java.lang.invoke.ConstantCallSite; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.Callable; +import net.bytebuddy.description.method.MethodDescription; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.dynamic.DynamicType; +import net.bytebuddy.utility.JavaConstant; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +public class IndyProxyFactoryTest { + + private static IndyProxyFactory proxyFactory; + + @BeforeAll + public static void init() throws Exception { + Method bootstrap = + IndyProxyFactoryTest.class.getMethod( + "indyBootstrap", + MethodHandles.Lookup.class, + String.class, + MethodType.class, + Object[].class); + proxyFactory = new IndyProxyFactory(bootstrap, IndyProxyFactoryTest::bootstrapArgsGenerator); + } + + public static CallSite indyBootstrap( + MethodHandles.Lookup lookup, String methodName, MethodType methodType, Object... args) { + + try { + String delegateClassName = (String) args[0]; + String kind = (String) args[1]; + + Class proxiedClass = Class.forName(delegateClassName); + + MethodHandle target; + + switch (kind) { + case "static": + target = MethodHandles.publicLookup().findStatic(proxiedClass, methodName, methodType); + break; + case "constructor": + target = + MethodHandles.publicLookup() + .findConstructor(proxiedClass, methodType.changeReturnType(void.class)) + .asType(methodType); + break; + case "virtual": + target = + MethodHandles.publicLookup() + .findVirtual(proxiedClass, methodName, methodType.dropParameterTypes(0, 1)) + .asType(methodType); + break; + default: + throw new IllegalStateException("unknown kind"); + } + return new ConstantCallSite(target); + } catch (Exception e) { + throw new IllegalStateException(e); + } + } + + private static List bootstrapArgsGenerator( + TypeDescription proxiedType, MethodDescription.InDefinedShape proxiedMethod) { + String kind = "virtual"; + if (proxiedMethod.isConstructor()) { + kind = "constructor"; + } else if (proxiedMethod.isStatic()) { + kind = "static"; + } + return Arrays.asList( + JavaConstant.Simple.ofLoaded(proxiedType.getName()), JavaConstant.Simple.ofLoaded(kind)); + } + + public static class StatefulObj { + + static StatefulObj lastCreatedInstance; + + int counter = 0; + + public StatefulObj() { + lastCreatedInstance = this; + } + + public void increaseCounter() { + counter++; + } + } + + @Test + void verifyDelegateInstantiation() throws Exception { + Class proxy = generateProxy(StatefulObj.class); + Constructor ctor = proxy.getConstructor(); + Method increaseCounter = proxy.getMethod("increaseCounter"); + + Object proxyA = ctor.newInstance(); + StatefulObj delegateA = StatefulObj.lastCreatedInstance; + + Object proxyB = ctor.newInstance(); + StatefulObj delegateB = StatefulObj.lastCreatedInstance; + + assertThat(delegateA).isNotNull(); + assertThat(delegateB).isNotNull(); + assertThat(delegateA).isNotSameAs(delegateB); + + increaseCounter.invoke(proxyA); + assertThat(delegateA.counter).isEqualTo(1); + assertThat(delegateB.counter).isEqualTo(0); + + increaseCounter.invoke(proxyB); + increaseCounter.invoke(proxyB); + assertThat(delegateA.counter).isEqualTo(1); + assertThat(delegateB.counter).isEqualTo(2); + } + + public static class UtilityWithPrivateCtor { + + private UtilityWithPrivateCtor() {} + + public static String utilityMethod() { + return "util"; + } + } + + @Test + void proxyClassWithoutConstructor() throws Exception { + Class proxy = generateProxy(UtilityWithPrivateCtor.class); + + // Not legal in Java code but legal in JVM bytecode + assertThat(proxy.getConstructors()).isEmpty(); + + assertThat(proxy.getMethod("utilityMethod").invoke(null)).isEqualTo("util"); + } + + @DummyAnnotation("type") + public static class AnnotationRetention { + + @DummyAnnotation("constructor") + public AnnotationRetention(@DummyAnnotation("constructor_param") String someValue) {} + + @DummyAnnotation("virtual") + public void virtualMethod(@DummyAnnotation("virtual_param") String someValue) {} + + @DummyAnnotation("static") + public static void staticMethod(@DummyAnnotation("static_param") String someValue) {} + } + + @Test + void verifyAnnotationsRetained() throws Exception { + + Class proxy = generateProxy(AnnotationRetention.class); + + assertThat(proxy.getAnnotation(DummyAnnotation.class)) + .isNotNull() + .extracting(DummyAnnotation::value) + .isEqualTo("type"); + + Constructor ctor = proxy.getConstructor(String.class); + assertThat(ctor.getAnnotation(DummyAnnotation.class)) + .isNotNull() + .extracting(DummyAnnotation::value) + .isEqualTo("constructor"); + assertThat(ctor.getParameters()[0].getAnnotation(DummyAnnotation.class)) + .isNotNull() + .extracting(DummyAnnotation::value) + .isEqualTo("constructor_param"); + + Method virtualMethod = proxy.getMethod("virtualMethod", String.class); + assertThat(virtualMethod.getAnnotation(DummyAnnotation.class)) + .isNotNull() + .extracting(DummyAnnotation::value) + .isEqualTo("virtual"); + assertThat(virtualMethod.getParameters()[0].getAnnotation(DummyAnnotation.class)) + .isNotNull() + .extracting(DummyAnnotation::value) + .isEqualTo("virtual_param"); + + Method staticMethod = proxy.getMethod("staticMethod", String.class); + assertThat(staticMethod.getAnnotation(DummyAnnotation.class)) + .isNotNull() + .extracting(DummyAnnotation::value) + .isEqualTo("static"); + assertThat(staticMethod.getParameters()[0].getAnnotation(DummyAnnotation.class)) + .isNotNull() + .extracting(DummyAnnotation::value) + .isEqualTo("static_param"); + + staticMethod.invoke(null, "blub"); + virtualMethod.invoke(ctor.newInstance("bla"), "blub"); + } + + public static class CustomSuperClass { + + int inheritedFromSuperclassCount = 0; + + protected void overrideMe() {} + + public void inheritedFromSuperclass() { + inheritedFromSuperclassCount++; + } + } + + public static interface CustomSuperInterface extends Runnable { + + default void inheritedDefault() { + if (this instanceof WithSuperTypes) { + ((WithSuperTypes) this).inheritedDefaultCount++; + } + } + } + + public static class WithSuperTypes extends CustomSuperClass + implements CustomSuperInterface, Callable { + + static WithSuperTypes lastCreatedInstance; + + public WithSuperTypes() { + lastCreatedInstance = this; + } + + int runInvocCount = 0; + int callInvocCount = 0; + int overrideMeInvocCount = 0; + + int inheritedDefaultCount = 0; + + @Override + public void run() { + runInvocCount++; + } + + @Override + public String call() throws Exception { + callInvocCount++; + return "foo"; + } + + @Override + public void overrideMe() { + overrideMeInvocCount++; + } + } + + @Test + @SuppressWarnings("unchecked") + void verifySuperTypes() throws Exception { + Object proxy = generateProxy(WithSuperTypes.class).getConstructor().newInstance(); + WithSuperTypes proxied = WithSuperTypes.lastCreatedInstance; + + ((Runnable) proxy).run(); + assertThat(proxied.runInvocCount).isEqualTo(1); + + ((Callable) proxy).call(); + assertThat(proxied.callInvocCount).isEqualTo(1); + + ((CustomSuperClass) proxy).overrideMe(); + assertThat(proxied.overrideMeInvocCount).isEqualTo(1); + + // Non-overidden, inherited methods are not proxied + ((CustomSuperClass) proxy).inheritedFromSuperclass(); + assertThat(proxied.inheritedFromSuperclassCount).isEqualTo(0); + ((CustomSuperInterface) proxy).inheritedDefault(); + assertThat(proxied.inheritedDefaultCount).isEqualTo(0); + } + + @SuppressWarnings({"unused", "MethodCanBeStatic"}) + public static class IgnoreNonPublicMethods { + + public IgnoreNonPublicMethods() {} + + protected IgnoreNonPublicMethods(int arg) {} + + IgnoreNonPublicMethods(int arg1, int arg2) {} + + private IgnoreNonPublicMethods(int arg1, int arg2, int arg3) {} + + public void publicMethod() {} + + public static void publicStaticMethod() {} + + protected void protectedMethod() {} + + protected static void protectedStaticMethod() {} + + void packageMethod() {} + + static void packageStaticMethod() {} + + private void privateMethod() {} + + private static void privateStaticMethod() {} + } + + @Test + void verifyNonPublicMembersIgnored() throws Exception { + Class proxy = generateProxy(IgnoreNonPublicMethods.class); + + assertThat(proxy.getConstructors()).hasSize(1); + assertThat(proxy.getDeclaredMethods()) + .hasSize(2) + .anySatisfy(method -> assertThat(method.getName()).isEqualTo("publicMethod")) + .anySatisfy(method -> assertThat(method.getName()).isEqualTo("publicStaticMethod")); + } + + private static Class generateProxy(Class clazz) { + DynamicType.Unloaded unloaded = + proxyFactory.generateProxy( + TypeDescription.ForLoadedType.of(clazz), clazz.getName() + "Proxy"); + // Uncomment the following line to view the generated bytecode if needed + // unloaded.saveIn(new File("generated_proxies")); + return unloaded.load(clazz.getClassLoader()).getLoaded(); + } +} diff --git a/javaagent-tooling/src/test/java/io/opentelemetry/javaagent/tooling/instrumentation/indy/InstrumentationModuleClassLoaderTest.java b/javaagent-tooling/src/test/java/io/opentelemetry/javaagent/tooling/instrumentation/indy/InstrumentationModuleClassLoaderTest.java new file mode 100644 index 000000000000..9a5c4158914f --- /dev/null +++ b/javaagent-tooling/src/test/java/io/opentelemetry/javaagent/tooling/instrumentation/indy/InstrumentationModuleClassLoaderTest.java @@ -0,0 +1,294 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.tooling.instrumentation.indy; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.internal.ExperimentalInstrumentationModule; +import io.opentelemetry.javaagent.tooling.BytecodeWithUrl; +import io.opentelemetry.javaagent.tooling.instrumentation.indy.dummies.Bar; +import io.opentelemetry.javaagent.tooling.instrumentation.indy.dummies.Foo; +import java.io.IOException; +import java.io.OutputStream; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.lang.reflect.Modifier; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.jar.JarEntry; +import java.util.jar.JarOutputStream; +import net.bytebuddy.ByteBuddy; +import net.bytebuddy.dynamic.ClassFileLocator; +import net.bytebuddy.implementation.FixedValue; +import net.bytebuddy.matcher.ElementMatchers; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +@SuppressWarnings("ClassNamedLikeTypeParameter") +class InstrumentationModuleClassLoaderTest { + + @Test + void checkLookup() throws Throwable { + Map toInject = new HashMap<>(); + toInject.put(Foo.class.getName(), BytecodeWithUrl.create(Foo.class)); + toInject.put(Bar.class.getName(), BytecodeWithUrl.create(Bar.class)); + + ClassLoader dummyParent = new URLClassLoader(new URL[] {}, null); + + InstrumentationModuleClassLoader m1 = + new InstrumentationModuleClassLoader(dummyParent, dummyParent, ElementMatchers.any()); + m1.installInjectedClasses(toInject); + + InstrumentationModuleClassLoader m2 = + new InstrumentationModuleClassLoader(dummyParent, dummyParent, ElementMatchers.any()); + m2.installInjectedClasses(toInject); + + // MethodHandles.publicLookup() always succeeds on the first invocation + lookupAndInvokeFoo(m1); + // MethodHandles.publicLookup() fails on the second invocation, + // even though the classes are loaded from an isolated class loader hierarchy + lookupAndInvokeFoo(m2); + } + + private static void lookupAndInvokeFoo(InstrumentationModuleClassLoader classLoader) + throws Throwable { + Class fooClass = classLoader.loadClass(Foo.class.getName()); + MethodHandles.Lookup lookup; + // using public lookup fails with LinkageError on second invocation - is this a (known) JVM bug? + // The failure only occurs on certain JVM versions, e.g. Java 8 and 11 + // lookup = MethodHandles.publicLookup(); + lookup = classLoader.getLookup(); + MethodHandle methodHandle = + lookup.findStatic( + fooClass, + "foo", + MethodType.methodType(String.class, classLoader.loadClass(Bar.class.getName()))); + assertThat(methodHandle.invoke((Bar) null)).isEqualTo("foo"); + } + + @Test + void checkInjectedClassesHavePackage() throws Throwable { + Map toInject = new HashMap<>(); + toInject.put(A.class.getName(), BytecodeWithUrl.create(A.class)); + toInject.put(B.class.getName(), BytecodeWithUrl.create(B.class)); + String packageName = A.class.getName().substring(0, A.class.getName().lastIndexOf('.')); + + ClassLoader dummyParent = new URLClassLoader(new URL[] {}, null); + InstrumentationModuleClassLoader m1 = + new InstrumentationModuleClassLoader(dummyParent, dummyParent, ElementMatchers.any()); + m1.installInjectedClasses(toInject); + + Class injected = Class.forName(A.class.getName(), true, m1); + // inject two classes from the same package to trigger errors if we try to redefine the package + Class.forName(B.class.getName(), true, m1); + + assertThat(injected.getClassLoader()).isSameAs(m1); + Package clPackage = m1.findPackage(packageName); + Package classPackage = injected.getPackage(); + + assertThat(classPackage).isNotNull(); + assertThat(clPackage).isNotNull(); + assertThat(classPackage).isSameAs(clPackage); + } + + @Test + void checkClassLookupPrecedence(@TempDir Path tempDir) throws Exception { + + Map appClasses = copyClassesWithMarker("app-cl", A.class, B.class, C.class); + Map agentClasses = copyClassesWithMarker("agent-cl", B.class, C.class); + Map moduleClasses = + copyClassesWithMarker("module-cl", A.class, B.class, C.class, D.class); + + Path appJar = tempDir.resolve("dummy-app.jar"); + createJar(appClasses, appJar); + + Path agentJar = tempDir.resolve("dummy-agent.jar"); + createJar(agentClasses, agentJar); + + Path moduleJar = tempDir.resolve("instrumentation-module.jar"); + createJar(moduleClasses, moduleJar); + + URLClassLoader appCl = new URLClassLoader(new URL[] {appJar.toUri().toURL()}, null); + URLClassLoader agentCl = new URLClassLoader(new URL[] {agentJar.toUri().toURL()}, null); + URLClassLoader moduleSourceCl = new URLClassLoader(new URL[] {moduleJar.toUri().toURL()}, null); + + try { + Map toInject = new HashMap<>(); + toInject.put(C.class.getName(), BytecodeWithUrl.create(C.class.getName(), moduleSourceCl)); + + InstrumentationModuleClassLoader moduleCl = + new InstrumentationModuleClassLoader(appCl, agentCl, ElementMatchers.any()); + moduleCl.installInjectedClasses(toInject); + + // Verify precedence for classloading + Class clA = moduleCl.loadClass(A.class.getName()); + assertThat(getMarkerValue(clA)).isEqualTo("app-cl"); + assertThat(clA.getClassLoader()).isSameAs(appCl); + + Class clB = moduleCl.loadClass(B.class.getName()); + assertThat(getMarkerValue(clB)).isEqualTo("agent-cl"); + assertThat(clB.getClassLoader()).isSameAs(agentCl); + + Class clC = moduleCl.loadClass(C.class.getName()); + assertThat(getMarkerValue(clC)).isEqualTo("module-cl"); + assertThat(clC.getClassLoader()) + .isSameAs(moduleCl); // class must be copied, therefore moduleCL + + assertThatThrownBy(() -> moduleCl.loadClass(D.class.getName())) + .isInstanceOf(ClassNotFoundException.class); + + // Verify precedence for looking up .class resources + URL resourceA = moduleCl.getResource(getClassFile(A.class)); + assertThat(resourceA.toString()).startsWith("jar:file:" + appJar); + assertThat(Collections.list(moduleCl.getResources(getClassFile(A.class)))) + .containsExactly(resourceA); + assertThat(moduleCl.getResourceAsStream(getClassFile(A.class))) + .hasBinaryContent(appClasses.get(A.class.getName())); + + URL resourceB = moduleCl.getResource(getClassFile(B.class)); + assertThat(resourceB.toString()).startsWith("jar:file:" + agentJar); + assertThat(Collections.list(moduleCl.getResources(getClassFile(B.class)))) + .containsExactly(resourceB); + assertThat(moduleCl.getResourceAsStream(getClassFile(B.class))) + .hasBinaryContent(agentClasses.get(B.class.getName())); + + URL resourceC = moduleCl.getResource(getClassFile(C.class)); + assertThat(resourceC.toString()).startsWith("jar:file:" + moduleJar); + assertThat(Collections.list(moduleCl.getResources(getClassFile(C.class)))) + .containsExactly(resourceC); + assertThat(moduleCl.getResourceAsStream(getClassFile(C.class))) + .hasBinaryContent(moduleClasses.get(C.class.getName())); + assertThat(moduleCl.getResource("/" + getClassFile(C.class))).isEqualTo(resourceC); + + assertThat(moduleCl.getResource(D.class.getName())).isNull(); + assertThat(moduleCl.getResourceAsStream(D.class.getName())).isNull(); + assertThat(Collections.list(moduleCl.getResources(D.class.getName()))).isEmpty(); + + // And finally verify that our resource handling does what it is supposed to do: + // Provide the correct bytecode sources when looking up bytecode with bytebuddy (or similar + // tools) + + assertThat(ClassFileLocator.ForClassLoader.read(clA)) + .isEqualTo(appClasses.get(A.class.getName())); + assertThat(ClassFileLocator.ForClassLoader.read(clB)) + .isEqualTo(agentClasses.get(B.class.getName())); + assertThat(ClassFileLocator.ForClassLoader.read(clC)) + .isEqualTo(moduleClasses.get(C.class.getName())); + + } finally { + appCl.close(); + agentCl.close(); + moduleSourceCl.close(); + } + } + + public static class HidingModule extends InstrumentationModule + implements ExperimentalInstrumentationModule { + + List hiddenPackages = new ArrayList<>(); + + public HidingModule() { + super("hiding-module"); + } + + @Override + public List typeInstrumentations() { + return Collections.emptyList(); + } + + @Override + public List agentPackagesToHide() { + return hiddenPackages; + } + } + + @Test + public void testAgentClassHiding() throws ClassNotFoundException { + HidingModule module = new HidingModule(); + + ClassLoader agentCl = HideMe.class.getClassLoader(); + + InstrumentationModuleClassLoader nothingHidden = + new InstrumentationModuleClassLoader(null, agentCl, ElementMatchers.any()); + nothingHidden.installModule(module); + + assertThat(nothingHidden.loadClass(HideMe.class.getName())).isSameAs(HideMe.class); + + module.hiddenPackages.add(HideMe.class.getPackage().getName()); + InstrumentationModuleClassLoader classHidden = + new InstrumentationModuleClassLoader(null, agentCl, ElementMatchers.any()); + classHidden.installModule(module); + + assertThatThrownBy(() -> classHidden.loadClass(HideMe.class.getName())) + .isInstanceOf(ClassNotFoundException.class); + } + + public static class HideMe {} + + private static String getClassFile(Class cl) { + return cl.getName().replace('.', '/') + ".class"; + } + + private static Map copyClassesWithMarker(String marker, Class... toCopy) { + + Map classes = new HashMap<>(); + for (Class clazz : toCopy) { + classes.put(clazz.getName(), copyClassWithMarker(clazz, marker)); + } + return classes; + } + + private static byte[] copyClassWithMarker(Class original, String markerValue) { + return new ByteBuddy() + .redefine(original) + .defineMethod("marker", String.class, Modifier.PUBLIC | Modifier.STATIC) + .intercept(FixedValue.value(markerValue)) + .make() + .getBytes(); + } + + private static String getMarkerValue(Class clazz) { + try { + return (String) clazz.getMethod("marker").invoke(null); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private static void createJar(Map classNameToBytecode, Path jarFilePath) { + try (OutputStream fileOut = Files.newOutputStream(jarFilePath)) { + try (JarOutputStream jarOut = new JarOutputStream(fileOut)) { + for (String clName : classNameToBytecode.keySet()) { + String classFile = clName.replace('.', '/') + ".class"; + jarOut.putNextEntry(new JarEntry(classFile)); + jarOut.write(classNameToBytecode.get(clName)); + jarOut.closeEntry(); + } + } + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + public static class A {} + + public static class B {} + + public static class C {} + + public static class D {} +} diff --git a/javaagent-tooling/src/test/java/io/opentelemetry/javaagent/tooling/instrumentation/indy/dummies/Bar.java b/javaagent-tooling/src/test/java/io/opentelemetry/javaagent/tooling/instrumentation/indy/dummies/Bar.java new file mode 100644 index 000000000000..2d417b7db03e --- /dev/null +++ b/javaagent-tooling/src/test/java/io/opentelemetry/javaagent/tooling/instrumentation/indy/dummies/Bar.java @@ -0,0 +1,8 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.tooling.instrumentation.indy.dummies; + +public class Bar {} diff --git a/javaagent-tooling/src/test/java/io/opentelemetry/javaagent/tooling/instrumentation/indy/dummies/DummyAnnotation.java b/javaagent-tooling/src/test/java/io/opentelemetry/javaagent/tooling/instrumentation/indy/dummies/DummyAnnotation.java new file mode 100644 index 000000000000..8ce3e917040b --- /dev/null +++ b/javaagent-tooling/src/test/java/io/opentelemetry/javaagent/tooling/instrumentation/indy/dummies/DummyAnnotation.java @@ -0,0 +1,14 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.tooling.instrumentation.indy.dummies; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +@Retention(RetentionPolicy.RUNTIME) +public @interface DummyAnnotation { + String value(); +} diff --git a/javaagent-tooling/src/test/java/io/opentelemetry/javaagent/tooling/instrumentation/indy/dummies/Foo.java b/javaagent-tooling/src/test/java/io/opentelemetry/javaagent/tooling/instrumentation/indy/dummies/Foo.java new file mode 100644 index 000000000000..da531b545a51 --- /dev/null +++ b/javaagent-tooling/src/test/java/io/opentelemetry/javaagent/tooling/instrumentation/indy/dummies/Foo.java @@ -0,0 +1,14 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.tooling.instrumentation.indy.dummies; + +public class Foo { + private Foo() {} + + public static String foo(Bar bar) { + return "foo"; + } +} diff --git a/javaagent-tooling/src/test/java/io/opentelemetry/javaagent/tooling/util/ClassLoaderValueTest.java b/javaagent-tooling/src/test/java/io/opentelemetry/javaagent/tooling/util/ClassLoaderValueTest.java new file mode 100644 index 000000000000..db8ba91f92e6 --- /dev/null +++ b/javaagent-tooling/src/test/java/io/opentelemetry/javaagent/tooling/util/ClassLoaderValueTest.java @@ -0,0 +1,66 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.tooling.util; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.instrumentation.test.utils.GcUtils; +import java.lang.ref.WeakReference; +import java.time.Duration; +import java.util.concurrent.TimeoutException; +import org.junit.jupiter.api.Test; + +class ClassLoaderValueTest { + + @Test + void testValue() { + testClassLoader(this.getClass().getClassLoader()); + testClassLoader(null); + } + + void testClassLoader(ClassLoader classLoader) { + ClassLoaderValue value1 = new ClassLoaderValue<>(); + value1.put(classLoader, "value"); + assertThat(value1.get(classLoader)).isEqualTo("value"); + + ClassLoaderValue value2 = new ClassLoaderValue<>(); + String value = "value"; + String ret1 = value2.computeIfAbsent(classLoader, () -> value); + String ret2 = + value2.computeIfAbsent( + classLoader, + () -> { + throw new IllegalStateException("Shouldn't be invoked"); + }); + assertThat(ret1).isSameAs(value); + assertThat(ret2).isSameAs(value); + assertThat(value2.get(classLoader)).isSameAs(value); + } + + @Test + void testGc() throws InterruptedException, TimeoutException { + ClassLoader testClassLoader = new ClassLoader() {}; + ClassLoaderValue classLoaderValue = new ClassLoaderValue<>(); + Value value = new Value(); + classLoaderValue.put(testClassLoader, value); + WeakReference valueWeakReference = new WeakReference<>(value); + WeakReference classLoaderWeakReference = new WeakReference<>(testClassLoader); + + assertThat(classLoaderWeakReference.get()).isNotNull(); + assertThat(valueWeakReference.get()).isNotNull(); + + value = null; + testClassLoader = null; + + GcUtils.awaitGc(classLoaderWeakReference, Duration.ofSeconds(10)); + GcUtils.awaitGc(valueWeakReference, Duration.ofSeconds(10)); + + assertThat(classLoaderWeakReference.get()).isNull(); + assertThat(valueWeakReference.get()).isNull(); + } + + private static class Value {} +} diff --git a/javaagent-tooling/src/test/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizerProvider b/javaagent-tooling/src/test/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizerProvider index b3f48d038005..2c506cefe2ba 100644 --- a/javaagent-tooling/src/test/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizerProvider +++ b/javaagent-tooling/src/test/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizerProvider @@ -1,2 +1,3 @@ io.opentelemetry.javaagent.tooling.config.ConfigurationPropertiesSupplier +io.opentelemetry.javaagent.tooling.config.OtlpProtocolPropertiesSupplier io.opentelemetry.javaagent.tooling.config.ConfigurationPropertiesSupplierTest$UserCustomPropertiesSupplier diff --git a/javaagent-tooling/src/testPatchBytecodeVersion/java/io/opentelemetry/javaagent/tooling/instrumentation/indy/ComputeFramesAsmVisitorWrapper.java b/javaagent-tooling/src/testPatchBytecodeVersion/java/io/opentelemetry/javaagent/tooling/instrumentation/indy/ComputeFramesAsmVisitorWrapper.java new file mode 100644 index 000000000000..52aa114ce938 --- /dev/null +++ b/javaagent-tooling/src/testPatchBytecodeVersion/java/io/opentelemetry/javaagent/tooling/instrumentation/indy/ComputeFramesAsmVisitorWrapper.java @@ -0,0 +1,36 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.tooling.instrumentation.indy; + +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.ClassWriter; +import net.bytebuddy.pool.TypePool; +import org.objectweb.asm.ClassVisitor; + +public class ComputeFramesAsmVisitorWrapper extends AsmVisitorWrapper.AbstractBase { + @Override + public int mergeWriter(int flags) { + return super.mergeWriter(flags | ClassWriter.COMPUTE_FRAMES); + } + + @Override + public ClassVisitor wrap( + TypeDescription instrumentedType, + ClassVisitor classVisitor, + Implementation.Context implementationContext, + TypePool typePool, + FieldList fields, + MethodList methods, + int writerFlags, + int readerFlags) { + return classVisitor; + } +} diff --git a/javaagent-tooling/src/testPatchBytecodeVersion/java/io/opentelemetry/javaagent/tooling/instrumentation/indy/OldBytecode.java b/javaagent-tooling/src/testPatchBytecodeVersion/java/io/opentelemetry/javaagent/tooling/instrumentation/indy/OldBytecode.java new file mode 100644 index 000000000000..86070a0106c0 --- /dev/null +++ b/javaagent-tooling/src/testPatchBytecodeVersion/java/io/opentelemetry/javaagent/tooling/instrumentation/indy/OldBytecode.java @@ -0,0 +1,150 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.tooling.instrumentation.indy; + +import java.lang.reflect.Modifier; +import net.bytebuddy.ByteBuddy; +import net.bytebuddy.ClassFileVersion; +import net.bytebuddy.asm.AsmVisitorWrapper; +import net.bytebuddy.description.method.MethodDescription; +import net.bytebuddy.dynamic.DynamicType; +import net.bytebuddy.dynamic.scaffold.InstrumentedType; +import net.bytebuddy.implementation.Implementation; +import net.bytebuddy.implementation.bytecode.ByteCodeAppender; +import org.objectweb.asm.Label; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; + +public class OldBytecode { + + private OldBytecode() {} + + /** + * Generates and run a simple class with a {@link #toString()} implementation as if it had been + * compiled on an older java compiler + * + * @param className class name + * @param version bytecode version + * @return "toString" + */ + public static String generateAndRun(String className, ClassFileVersion version) { + try (DynamicType.Unloaded unloadedClass = makeClass(className, version)) { + Class generatedClass = unloadedClass.load(OldBytecode.class.getClassLoader()).getLoaded(); + + return generatedClass.getConstructor().newInstance().toString(); + + } catch (Throwable e) { + throw new RuntimeException(e); + } + } + + private static DynamicType.Unloaded makeClass( + String className, ClassFileVersion version) { + return new ByteBuddy(version) + .subclass(Object.class) + // required otherwise stack frames aren't computed when needed + .visit( + version.isAtLeast(ClassFileVersion.JAVA_V7) + ? new ComputeFramesAsmVisitorWrapper() + : AsmVisitorWrapper.NoOp.INSTANCE) + .name(className) + .defineMethod("toString", String.class, Modifier.PUBLIC) + .intercept(new ToStringMethod()) + .make(); + } + + private static class ToStringMethod implements Implementation, ByteCodeAppender { + + @Override + public ByteCodeAppender appender(Target implementationTarget) { + return this; + } + + @Override + public InstrumentedType prepare(InstrumentedType instrumentedType) { + return instrumentedType; + } + + @Override + public Size apply( + MethodVisitor methodVisitor, + Context implementationContext, + MethodDescription instrumentedMethod) { + + // Bytecode archeology: + // + // JSR and RET bytecode instructions were used to create "subroutines". Those were used + // in try/catch blocks as an attempt to avoid some bytecode duplication, this was later + // replaced with inlining. + // Starting from Java 5, no java compiler is expected to issue bytecode containing them and + // the JVM bytecode validation will reject it. + // + // Java 7 bytecode introduced the concept of "stack map frames", which describe the types of + // the objects that are stored on the stack during method body execution. + // + // As a consequence, the code below allows to test the following combinations: + // - java 1 to java 4 bytecode with JSR/RET opcodes + // - java 5 and java 6 bytecode without stack map frames + // - java 7 and later bytecode with stack map frames, those are automatically added by the + // ComputeFramesAsmVisitorWrapper. + // + boolean useJsrRet = + implementationContext.getClassFileVersion().isLessThan(ClassFileVersion.JAVA_V5); + + if (useJsrRet) { + // return "toString"; + // + // using obsolete JSR/RET instructions + Label target = new Label(); + methodVisitor.visitJumpInsn(Opcodes.JSR, target); + + methodVisitor.visitVarInsn(Opcodes.ALOAD, 1); + methodVisitor.visitInsn(Opcodes.ARETURN); + methodVisitor.visitLabel(target); + methodVisitor.visitVarInsn(Opcodes.ASTORE, 2); + methodVisitor.visitLdcInsn("toString"); + methodVisitor.visitVarInsn(Opcodes.ASTORE, 1); + methodVisitor.visitVarInsn(Opcodes.RET, 2); + return new Size(1, 3); + } else { + // try { + // return "toString"; + // } catch (Throwable e) { + // return e.getMessage(); + // } + // + // the Throwable exception is added to stack map frames with java7+, and needs to be + // added when upgrading the bytecode + Label start = new Label(); + Label end = new Label(); + Label handler = new Label(); + + methodVisitor.visitTryCatchBlock( + start, end, handler, Type.getInternalName(Throwable.class)); + methodVisitor.visitLabel(start); + methodVisitor.visitLdcInsn("toString"); + methodVisitor.visitLabel(end); + + methodVisitor.visitInsn(Opcodes.ARETURN); + + methodVisitor.visitLabel(handler); + methodVisitor.visitVarInsn(Opcodes.ASTORE, 1); + methodVisitor.visitVarInsn(Opcodes.ALOAD, 1); + + methodVisitor.visitMethodInsn( + Opcodes.INVOKEVIRTUAL, + Type.getInternalName(Throwable.class), + "getMessage", + Type.getMethodDescriptor(Type.getType(String.class)), + false); + methodVisitor.visitInsn(Opcodes.ARETURN); + + return new Size(1, 2); + } + } + } +} diff --git a/javaagent-tooling/src/testPatchBytecodeVersion/java/io/opentelemetry/javaagent/tooling/instrumentation/indy/PatchBytecodeVersionTest.java b/javaagent-tooling/src/testPatchBytecodeVersion/java/io/opentelemetry/javaagent/tooling/instrumentation/indy/PatchBytecodeVersionTest.java new file mode 100644 index 000000000000..f8b1a4297f03 --- /dev/null +++ b/javaagent-tooling/src/testPatchBytecodeVersion/java/io/opentelemetry/javaagent/tooling/instrumentation/indy/PatchBytecodeVersionTest.java @@ -0,0 +1,172 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.tooling.instrumentation.indy; + +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.nameStartsWith; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Stream; +import net.bytebuddy.ClassFileVersion; +import net.bytebuddy.agent.ByteBuddyAgent; +import net.bytebuddy.agent.builder.AgentBuilder; +import net.bytebuddy.agent.builder.ResettableClassFileTransformer; +import net.bytebuddy.description.method.MethodDescription; +import net.bytebuddy.dynamic.ClassFileLocator; +import net.bytebuddy.matcher.ElementMatcher; +import net.bytebuddy.utility.JavaModule; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.io.TempDir; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +class PatchBytecodeVersionTest { + + private static final String BYTEBUDDY_DUMP = "net.bytebuddy.dump"; + public static final String ORIGINAL_SUFFIX = "-original"; + public static final String CLASSNAME_PREFIX = "oldbytecode_"; + private static ResettableClassFileTransformer transformer; + + private static final Logger logger = LoggerFactory.getLogger(PatchBytecodeVersionTest.class); + + private static Path tempDir; + + @BeforeAll + static void setUp(@TempDir Path temp) { + + assertThat(temp).isEmptyDirectory(); + tempDir = temp; + System.setProperty(BYTEBUDDY_DUMP, temp.toString()); + + AgentBuilder builder = + new AgentBuilder.Default() + .disableClassFormatChanges() + .with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION) + .with( + new AgentBuilder.Listener.Adapter() { + @Override + public void onError( + String typeName, + ClassLoader classLoader, + JavaModule module, + boolean loaded, + Throwable throwable) { + logger.error("Transformation error", throwable); + } + }) + .type(nameStartsWith(CLASSNAME_PREFIX)) + .transform(new PatchByteCodeVersionTransformer()) + .transform(transformerFor(isMethod().and(named("toString")))); + + ByteBuddyAgent.install(); + transformer = builder.installOn(ByteBuddyAgent.getInstrumentation()); + } + + @AfterAll + static void tearDown() { + transformer.reset( + ByteBuddyAgent.getInstrumentation(), AgentBuilder.RedefinitionStrategy.RETRANSFORMATION); + + System.clearProperty(BYTEBUDDY_DUMP); + } + + @ParameterizedTest + @MethodSource("bytecodeVersions") + void upgradeBytecode(ClassFileVersion version) { + + String className = CLASSNAME_PREFIX + version.getMinorMajorVersion(); + + int startCount = PatchTestAdvice.invocationCount.get(); + assertThat(PatchTestAdvice.invocationCount.get()).isEqualTo(startCount); + + assertThat(OldBytecode.generateAndRun(className, version)).isEqualTo("toString"); + + assertThat(PatchTestAdvice.invocationCount.get()).isEqualTo(startCount + 1); + + Path instrumentedClass; + Path instrumentedClassOriginal; + + try (Stream files = + Files.find( + tempDir, + 1, + (path, attr) -> { + String fileName = path.getFileName().toString(); + return Files.isRegularFile(path) + && fileName.startsWith(className) + && fileName.contains(ORIGINAL_SUFFIX); + })) { + + instrumentedClassOriginal = files.findFirst().orElseThrow(IllegalStateException::new); + String upgradedClassFileName = + instrumentedClassOriginal.getFileName().toString().replace(ORIGINAL_SUFFIX, ""); + instrumentedClass = instrumentedClassOriginal.resolveSibling(upgradedClassFileName); + + } catch (IOException e) { + throw new IllegalStateException(e); + } + + assertThat(instrumentedClass).exists(); + assertThat(instrumentedClassOriginal).exists(); + + assertThat(getBytecodeVersion(instrumentedClassOriginal)) + .describedAs( + "expected original bytecode for class '%s' in '%s' should have been compiled for %s", + className, instrumentedClassOriginal.toAbsolutePath(), version) + .isEqualTo(version); + + if (version.isLessThan(ClassFileVersion.JAVA_V7)) { + assertThat(getBytecodeVersion(instrumentedClass)) + .describedAs( + "expected instrumented bytecode for class '%s' in '%s' should have been upgraded to Java 7", + className, instrumentedClass.toAbsolutePath()) + .isEqualTo(ClassFileVersion.JAVA_V7); + } else { + assertThat(getBytecodeVersion(instrumentedClass)) + .describedAs("original bytecode version shouldn't be altered") + .isEqualTo(version); + } + } + + static List bytecodeVersions() { + return Arrays.asList( + ClassFileVersion.JAVA_V1, + ClassFileVersion.JAVA_V2, + ClassFileVersion.JAVA_V3, + ClassFileVersion.JAVA_V4, + ClassFileVersion.JAVA_V5, + ClassFileVersion.JAVA_V6, + // Java 7 and later should not be upgraded + ClassFileVersion.JAVA_V7, + ClassFileVersion.JAVA_V8); + } + + private static ClassFileVersion getBytecodeVersion(Path file) { + try { + return ClassFileVersion.ofClassFile(Files.readAllBytes(file)); + } catch (IOException e) { + throw new IllegalStateException(e); + } + } + + private static AgentBuilder.Transformer.ForAdvice transformerFor( + ElementMatcher.Junction methodMatcher) { + return new AgentBuilder.Transformer.ForAdvice() + .with( + new AgentBuilder.LocationStrategy.Simple( + ClassFileLocator.ForClassLoader.of(PatchTestAdvice.class.getClassLoader()))) + .advice(methodMatcher, PatchTestAdvice.class.getName()); + } +} diff --git a/javaagent-tooling/src/testPatchBytecodeVersion/java/io/opentelemetry/javaagent/tooling/instrumentation/indy/PatchTestAdvice.java b/javaagent-tooling/src/testPatchBytecodeVersion/java/io/opentelemetry/javaagent/tooling/instrumentation/indy/PatchTestAdvice.java new file mode 100644 index 000000000000..d0ce6000c730 --- /dev/null +++ b/javaagent-tooling/src/testPatchBytecodeVersion/java/io/opentelemetry/javaagent/tooling/instrumentation/indy/PatchTestAdvice.java @@ -0,0 +1,20 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.tooling.instrumentation.indy; + +import java.util.concurrent.atomic.AtomicInteger; +import net.bytebuddy.asm.Advice; + +@SuppressWarnings("unused") +public class PatchTestAdvice { + + public static final AtomicInteger invocationCount = new AtomicInteger(0); + + @Advice.OnMethodExit(suppress = Throwable.class) + public static void onExit() { + invocationCount.incrementAndGet(); + } +} diff --git a/javaagent/build.gradle.kts b/javaagent/build.gradle.kts index 2c46e0f4d0ab..ef7a6369eac0 100644 --- a/javaagent/build.gradle.kts +++ b/javaagent/build.gradle.kts @@ -1,6 +1,10 @@ import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar import com.github.jk1.license.filter.LicenseBundleNormalizer import com.github.jk1.license.render.InventoryMarkdownReportRenderer +import org.spdx.sbom.gradle.SpdxSbomTask +import java.nio.file.Files +import java.util.UUID +import java.util.regex.Pattern plugins { id("com.github.jk1.dependency-license-report") @@ -8,6 +12,7 @@ plugins { id("otel.java-conventions") id("otel.publish-conventions") id("io.opentelemetry.instrumentation.javaagent-shadowing") + id("org.spdx.sbom") } description = "OpenTelemetry Javaagent" @@ -34,22 +39,23 @@ val javaagentLibs by configurations.creating { listOf(baseJavaagentLibs, javaagentLibs).forEach { it.run { exclude("io.opentelemetry", "opentelemetry-api") - exclude("io.opentelemetry", "opentelemetry-api-events") - exclude("io.opentelemetry", "opentelemetry-semconv") - // metrics advice API - exclude("io.opentelemetry", "opentelemetry-extension-incubator") + exclude("io.opentelemetry.semconv", "opentelemetry-semconv") + exclude("io.opentelemetry.semconv", "opentelemetry-semconv-incubating") + // events API and metrics advice API + exclude("io.opentelemetry", "opentelemetry-api-incubator") } } val licenseReportDependencies by configurations.creating { extendsFrom(bootstrapLibs) + extendsFrom(baseJavaagentLibs) } dependencies { bootstrapLibs(project(":instrumentation-api")) - // opentelemetry-api is an api dependency of :instrumentation-api, but opentelemetry-api-events is not - bootstrapLibs("io.opentelemetry:opentelemetry-api-events") - bootstrapLibs(project(":instrumentation-api-semconv")) + // opentelemetry-api is an api dependency of :instrumentation-api, but opentelemetry-api-incubator is not + bootstrapLibs("io.opentelemetry:opentelemetry-api-incubator") + bootstrapLibs(project(":instrumentation-api-incubator")) bootstrapLibs(project(":instrumentation-annotations-support")) bootstrapLibs(project(":javaagent-bootstrap")) @@ -83,19 +89,12 @@ dependencies { // concurrentlinkedhashmap-lru and weak-lock-free are copied in to the instrumentation-api module licenseReportDependencies("com.googlecode.concurrentlinkedhashmap:concurrentlinkedhashmap-lru:1.4.2") licenseReportDependencies("com.blogspot.mydailyjava:weak-lock-free:0.18") - // 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-internal-logging-application")) - licenseReportDependencies(project(":javaagent-internal-logging-simple")) - licenseReportDependencies(project(":javaagent-extension-api")) + licenseReportDependencies(project(":javaagent-internal-logging-simple")) // need the non-shadow versions testCompileOnly(project(":javaagent-bootstrap")) testCompileOnly(project(":javaagent-extension-api")) - testImplementation("com.google.guava:guava") - testImplementation("io.opentelemetry:opentelemetry-sdk") + testImplementation(project(":testing-common")) testImplementation("io.opentracing.contrib.dropwizard:dropwizard-opentracing:0.2.2") } @@ -243,10 +242,29 @@ tasks { delete(rootProject.file("licenses")) } - val generateLicenseReportEnabled = gradle.startParameter.taskNames.any { it.equals("generateLicenseReport") } + val removeLicenseDate by registering { + // removing the license report date makes it idempotent + doLast { + val filePath = rootDir.toPath().resolve("licenses").resolve("licenses.md") + if (Files.exists(filePath)) { + val datePattern = Pattern.compile("^_[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2} .*_$") + val lines = Files.readAllLines(filePath) + // 4th line contains the timestamp of when the license report was generated, replace it with + // an empty line + if (lines.size > 3 && datePattern.matcher(lines[3]).matches()) { + lines[3] = "" + Files.write(filePath, lines) + } + } + } + } + + val generateLicenseReportEnabled = + gradle.startParameter.taskNames.any { it.equals("generateLicenseReport") } named("generateLicenseReport").configure { dependsOn(cleanLicenses) finalizedBy(":spotlessApply") + finalizedBy(removeLicenseDate) // disable licence report generation unless this task is explicitly run // the files produced by this task are used by other tasks without declaring them as dependency // which gradle considers an error @@ -276,6 +294,39 @@ with(components["java"] as AdhocComponentWithVariants) { } } +spdxSbom { + targets { + // Create a target to match the published jar name. + // This is used for the task name (spdxSbomFor) + // and output file (.spdx.json). + create("opentelemetry-javaagent") { + configurations.set(listOf("baseJavaagentLibs")) + scm { + uri.set("https://github.com/" + System.getenv("GITHUB_REPOSITORY")) + revision.set(System.getenv("GITHUB_SHA")) + } + document { + name.set("opentelemetry-javaagent") + namespace.set("https://opentelemetry.io/spdx/" + UUID.randomUUID()) + } + } + } +} +tasks.withType { + dependsOn("spdxSbom") +} +project.afterEvaluate { + tasks.withType().configureEach { + mustRunAfter(tasks.withType()) + } + tasks.withType().configureEach { + this.publication.artifact("${layout.buildDirectory.get()}/spdx/opentelemetry-javaagent.spdx.json") { + classifier = "spdx" + extension = "json" + } + } +} + licenseReport { outputDir = rootProject.file("licenses").absolutePath @@ -316,7 +367,7 @@ fun CopySpec.isolateClasses(jar: Provider) { fun ShadowJar.excludeBootstrapClasses() { dependencies { exclude(project(":instrumentation-api")) - exclude(project(":instrumentation-api-semconv")) + exclude(project(":instrumentation-api-incubator")) exclude(project(":instrumentation-annotations-support")) exclude(project(":javaagent-bootstrap")) } diff --git a/javaagent/src/test/groovy/io/opentelemetry/javaagent/classloading/ClassLoadingTest.groovy b/javaagent/src/test/groovy/io/opentelemetry/javaagent/classloading/ClassLoadingTest.groovy index 5373c2dfc769..b0fdaa91a7c4 100644 --- a/javaagent/src/test/groovy/io/opentelemetry/javaagent/classloading/ClassLoadingTest.groovy +++ b/javaagent/src/test/groovy/io/opentelemetry/javaagent/classloading/ClassLoadingTest.groovy @@ -5,12 +5,13 @@ package io.opentelemetry.javaagent.classloading +import io.opentelemetry.instrumentation.test.utils.GcUtils import io.opentelemetry.javaagent.ClassToInstrument import io.opentelemetry.javaagent.ClassToInstrumentChild -import io.opentelemetry.javaagent.util.GcUtils import spock.lang.Specification import java.lang.ref.WeakReference +import java.time.Duration import static io.opentelemetry.javaagent.IntegrationTestUtils.createJarWithClasses @@ -43,7 +44,7 @@ class ClassLoadingTest extends Specification { loader.loadClass(ClassToInstrument.getName()) loader = null - GcUtils.awaitGc(ref) + GcUtils.awaitGc(ref, Duration.ofSeconds(10)) then: null == ref.get() diff --git a/javaagent/src/test/java/io/opentelemetry/javaagent/ResourceProviderTest.java b/javaagent/src/test/java/io/opentelemetry/javaagent/ResourceProviderTest.java new file mode 100644 index 000000000000..43402d2ae302 --- /dev/null +++ b/javaagent/src/test/java/io/opentelemetry/javaagent/ResourceProviderTest.java @@ -0,0 +1,43 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.ServiceLoader; +import org.junit.jupiter.api.Test; + +public class ResourceProviderTest { + + @Test + void resourceProviderOrder() throws Exception { + boolean containerProviderFound = false; + boolean awsProviderFound = false; + // verify that aws resource provider is found after the regular container provider + // provider that is found later can overrider values from previous providers + Class resourceProviderClass = + Class.forName( + "io.opentelemetry.sdk.autoconfigure.spi.ResourceProvider", + false, + IntegrationTestUtils.getAgentClassLoader()); + for (Object resourceProvider : + ServiceLoader.load(resourceProviderClass, IntegrationTestUtils.getAgentClassLoader())) { + Class clazz = resourceProvider.getClass(); + if (clazz + .getName() + .equals("io.opentelemetry.instrumentation.resources.ContainerResourceProvider")) { + containerProviderFound = true; + assertFalse(awsProviderFound); + } else if (clazz.getName().startsWith("io.opentelemetry.contrib.aws.resource.")) { + awsProviderFound = true; + assertTrue(containerProviderFound); + } + } + assertTrue(containerProviderFound); + assertTrue(awsProviderFound); + } +} diff --git a/javaagent/src/test/java/io/opentelemetry/javaagent/util/GcUtils.java b/javaagent/src/test/java/io/opentelemetry/javaagent/util/GcUtils.java deleted file mode 100644 index c3dafe56bcf6..000000000000 --- a/javaagent/src/test/java/io/opentelemetry/javaagent/util/GcUtils.java +++ /dev/null @@ -1,22 +0,0 @@ -/* - * 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/licenses/byte-buddy-dep-1.14.5.jar/META-INF/LICENSE b/licenses/byte-buddy-dep-1.15.1.jar/META-INF/LICENSE similarity index 100% rename from licenses/byte-buddy-dep-1.14.5.jar/META-INF/LICENSE rename to licenses/byte-buddy-dep-1.15.1.jar/META-INF/LICENSE diff --git a/licenses/byte-buddy-dep-1.14.5.jar/META-INF/NOTICE b/licenses/byte-buddy-dep-1.15.1.jar/META-INF/NOTICE similarity index 100% rename from licenses/byte-buddy-dep-1.14.5.jar/META-INF/NOTICE rename to licenses/byte-buddy-dep-1.15.1.jar/META-INF/NOTICE diff --git a/licenses/jackson-core-2.15.2.jar/META-INF/LICENSE b/licenses/jackson-annotations-2.17.2.jar/META-INF/LICENSE similarity index 100% rename from licenses/jackson-core-2.15.2.jar/META-INF/LICENSE rename to licenses/jackson-annotations-2.17.2.jar/META-INF/LICENSE diff --git a/licenses/jackson-annotations-2.17.2.jar/META-INF/NOTICE b/licenses/jackson-annotations-2.17.2.jar/META-INF/NOTICE new file mode 100644 index 000000000000..738b11fda42c --- /dev/null +++ b/licenses/jackson-annotations-2.17.2.jar/META-INF/NOTICE @@ -0,0 +1,21 @@ +# Jackson JSON processor + +Jackson is a high-performance, Free/Open Source JSON processing library. +It was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and has +been in development since 2007. +It is currently developed by a community of developers. + +## Copyright + +Copyright 2007-, Tatu Saloranta (tatu.saloranta@iki.fi) + +## Licensing + +Jackson 2.x core and extension components are licensed under Apache License 2.0 +To find the details that apply to this artifact see the accompanying LICENSE file. + +## Credits + +A list of contributors may be found from CREDITS(-2.x) file, which is included +in some artifacts (usually source distributions); but is always available +from the source code management (SCM) system project uses. diff --git a/licenses/zipkin-reporter-2.16.3.jar/META-INF/LICENSE b/licenses/jackson-core-2.17.2.jar/META-INF/LICENSE similarity index 99% rename from licenses/zipkin-reporter-2.16.3.jar/META-INF/LICENSE rename to licenses/jackson-core-2.17.2.jar/META-INF/LICENSE index 8dada3edaf50..d64569567334 100644 --- a/licenses/zipkin-reporter-2.16.3.jar/META-INF/LICENSE +++ b/licenses/jackson-core-2.17.2.jar/META-INF/LICENSE @@ -1,3 +1,4 @@ + Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ @@ -178,7 +179,7 @@ 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 "{}" + 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 @@ -186,7 +187,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright {yyyy} {name of copyright owner} + 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. diff --git a/licenses/jackson-core-2.15.2.jar/META-INF/NOTICE b/licenses/jackson-core-2.17.2.jar/META-INF/NOTICE similarity index 100% rename from licenses/jackson-core-2.15.2.jar/META-INF/NOTICE rename to licenses/jackson-core-2.17.2.jar/META-INF/NOTICE diff --git a/licenses/zipkin-sender-okhttp3-2.16.3.jar/META-INF/LICENSE b/licenses/jackson-databind-2.17.2.jar/META-INF/LICENSE similarity index 99% rename from licenses/zipkin-sender-okhttp3-2.16.3.jar/META-INF/LICENSE rename to licenses/jackson-databind-2.17.2.jar/META-INF/LICENSE index 8dada3edaf50..d64569567334 100644 --- a/licenses/zipkin-sender-okhttp3-2.16.3.jar/META-INF/LICENSE +++ b/licenses/jackson-databind-2.17.2.jar/META-INF/LICENSE @@ -1,3 +1,4 @@ + Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ @@ -178,7 +179,7 @@ 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 "{}" + 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 @@ -186,7 +187,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright {yyyy} {name of copyright owner} + 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. diff --git a/licenses/jackson-databind-2.17.2.jar/META-INF/NOTICE b/licenses/jackson-databind-2.17.2.jar/META-INF/NOTICE new file mode 100644 index 000000000000..738b11fda42c --- /dev/null +++ b/licenses/jackson-databind-2.17.2.jar/META-INF/NOTICE @@ -0,0 +1,21 @@ +# Jackson JSON processor + +Jackson is a high-performance, Free/Open Source JSON processing library. +It was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and has +been in development since 2007. +It is currently developed by a community of developers. + +## Copyright + +Copyright 2007-, Tatu Saloranta (tatu.saloranta@iki.fi) + +## Licensing + +Jackson 2.x core and extension components are licensed under Apache License 2.0 +To find the details that apply to this artifact see the accompanying LICENSE file. + +## Credits + +A list of contributors may be found from CREDITS(-2.x) file, which is included +in some artifacts (usually source distributions); but is always available +from the source code management (SCM) system project uses. diff --git a/licenses/jackson-jr-objects-2.15.2.jar/META-INF/LICENSE b/licenses/jackson-dataformat-yaml-2.17.2.jar/META-INF/LICENSE similarity index 78% rename from licenses/jackson-jr-objects-2.15.2.jar/META-INF/LICENSE rename to licenses/jackson-dataformat-yaml-2.17.2.jar/META-INF/LICENSE index 56dcbf4258f9..cd0270b9fb5b 100644 --- a/licenses/jackson-jr-objects-2.15.2.jar/META-INF/LICENSE +++ b/licenses/jackson-dataformat-yaml-2.17.2.jar/META-INF/LICENSE @@ -1,4 +1,4 @@ -This copy of Jackson-jr library is licensed under the +This copy of Jackson JSON processor YAML module is licensed under the Apache (Software) License, version 2.0 ("the License"). See the License for details about distribution rights, and the specific rights regarding derivative works. diff --git a/licenses/jackson-jr-objects-2.15.2.jar/META-INF/NOTICE b/licenses/jackson-dataformat-yaml-2.17.2.jar/META-INF/NOTICE similarity index 89% rename from licenses/jackson-jr-objects-2.15.2.jar/META-INF/NOTICE rename to licenses/jackson-dataformat-yaml-2.17.2.jar/META-INF/NOTICE index d55c59a0d506..cbc9447242b6 100644 --- a/licenses/jackson-jr-objects-2.15.2.jar/META-INF/NOTICE +++ b/licenses/jackson-dataformat-yaml-2.17.2.jar/META-INF/NOTICE @@ -5,6 +5,10 @@ It was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and has been in development since 2007. It is currently developed by a community of developers. +## Copyright + +Copyright 2007-, Tatu Saloranta (tatu.saloranta@iki.fi) + ## Licensing Jackson components are licensed under Apache (Software) License, version 2.0, diff --git a/licenses/licenses.md b/licenses/licenses.md index 6b5c7401c11e..8d979cb97ee1 100644 --- a/licenses/licenses.md +++ b/licenses/licenses.md @@ -1,7 +1,7 @@ # javaagent ## Dependency License Report -_2023-06-17 09:51:16 UTC_ + ## Apache License, Version 2.0 **1** **Group:** `com.blogspot.mydailyjava` **Name:** `weak-lock-free` **Version:** `0.18` @@ -9,238 +9,320 @@ _2023-06-17 09:51:16 UTC_ > - **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.fasterxml.jackson.core` **Name:** `jackson-core` **Version:** `2.15.2` +**2** **Group:** `com.fasterxml.jackson.core` **Name:** `jackson-annotations` **Version:** `2.17.2` +> - **Project URL**: [https://github.com/FasterXML/jackson](https://github.com/FasterXML/jackson) +> - **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**: [jackson-annotations-2.17.2.jar/META-INF/LICENSE](jackson-annotations-2.17.2.jar/META-INF/LICENSE) + - [jackson-annotations-2.17.2.jar/META-INF/NOTICE](jackson-annotations-2.17.2.jar/META-INF/NOTICE) + +**3** **Group:** `com.fasterxml.jackson.core` **Name:** `jackson-core` **Version:** `2.17.2` > - **Project URL**: [https://github.com/FasterXML/jackson-core](https://github.com/FasterXML/jackson-core) > - **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**: [jackson-core-2.15.2.jar/META-INF/LICENSE](jackson-core-2.15.2.jar/META-INF/LICENSE) - - [jackson-core-2.15.2.jar/META-INF/NOTICE](jackson-core-2.15.2.jar/META-INF/NOTICE) +> - **Embedded license files**: [jackson-core-2.17.2.jar/META-INF/LICENSE](jackson-core-2.17.2.jar/META-INF/LICENSE) + - [jackson-core-2.17.2.jar/META-INF/NOTICE](jackson-core-2.17.2.jar/META-INF/NOTICE) -**3** **Group:** `com.fasterxml.jackson.jr` **Name:** `jackson-jr-objects` **Version:** `2.15.2` -> - **Project URL**: [https://github.com/FasterXML/jackson-jr](https://github.com/FasterXML/jackson-jr) +**4** **Group:** `com.fasterxml.jackson.core` **Name:** `jackson-databind` **Version:** `2.17.2` +> - **Project URL**: [https://github.com/FasterXML/jackson](https://github.com/FasterXML/jackson) > - **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**: [jackson-jr-objects-2.15.2.jar/META-INF/LICENSE](jackson-jr-objects-2.15.2.jar/META-INF/LICENSE) - - [jackson-jr-objects-2.15.2.jar/META-INF/NOTICE](jackson-jr-objects-2.15.2.jar/META-INF/NOTICE) +> - **Embedded license files**: [jackson-databind-2.17.2.jar/META-INF/LICENSE](jackson-databind-2.17.2.jar/META-INF/LICENSE) + - [jackson-databind-2.17.2.jar/META-INF/NOTICE](jackson-databind-2.17.2.jar/META-INF/NOTICE) + +**5** **Group:** `com.fasterxml.jackson.dataformat` **Name:** `jackson-dataformat-yaml` **Version:** `2.17.2` +> - **Project URL**: [https://github.com/FasterXML/jackson-dataformats-text](https://github.com/FasterXML/jackson-dataformats-text) +> - **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**: [jackson-dataformat-yaml-2.17.2.jar/META-INF/LICENSE](jackson-dataformat-yaml-2.17.2.jar/META-INF/LICENSE) + - [jackson-dataformat-yaml-2.17.2.jar/META-INF/NOTICE](jackson-dataformat-yaml-2.17.2.jar/META-INF/NOTICE) + +**6** **Group:** `com.google.cloud.opentelemetry` **Name:** `detector-resources-support` **Version:** `0.31.0` +> - **POM Project URL**: [https://github.com/GoogleCloudPlatform/opentelemetry-operations-java](https://github.com/GoogleCloudPlatform/opentelemetry-operations-java) +> - **POM License**: Apache License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0](https://www.apache.org/licenses/LICENSE-2.0) -**4** **Group:** `com.googlecode.concurrentlinkedhashmap` **Name:** `concurrentlinkedhashmap-lru` **Version:** `1.4.2` +**7** **Group:** `com.googlecode.concurrentlinkedhashmap` **Name:** `concurrentlinkedhashmap-lru` **Version:** `1.4.2` > - **Manifest License**: Apache License, Version 2.0 (Not Packaged) > - **POM Project URL**: [http://code.google.com/p/concurrentlinkedhashmap](http://code.google.com/p/concurrentlinkedhashmap) > - **POM License**: Apache License, Version 2.0 - [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0) -**5** **Group:** `com.squareup.okhttp3` **Name:** `okhttp` **Version:** `4.11.0` +**8** **Group:** `com.squareup.okhttp3` **Name:** `okhttp` **Version:** `4.12.0` > - **POM Project URL**: [https://square.github.io/okhttp/](https://square.github.io/okhttp/) -> - **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-4.11.0.jar/okhttp3/internal/publicsuffix/NOTICE](okhttp-4.11.0.jar/okhttp3/internal/publicsuffix/NOTICE) +> - **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**: [okhttp-4.12.0.jar/okhttp3/internal/publicsuffix/NOTICE](okhttp-4.12.0.jar/okhttp3/internal/publicsuffix/NOTICE) -**6** **Group:** `com.squareup.okio` **Name:** `okio-jvm` **Version:** `3.2.0` +**9** **Group:** `com.squareup.okio` **Name:** `okio` **Version:** `3.9.0` > - **POM Project URL**: [https://github.com/square/okio/](https://github.com/square/okio/) -> - **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:** `io.opentelemetry` **Name:** `opentelemetry-api` **Version:** `1.27.0` -> - **POM Project URL**: [https://github.com/open-telemetry/opentelemetry-java](https://github.com/open-telemetry/opentelemetry-java) > - **POM License**: Apache License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0](https://www.apache.org/licenses/LICENSE-2.0) -**8** **Group:** `io.opentelemetry` **Name:** `opentelemetry-api-events` **Version:** `1.27.0-alpha` -> - **POM Project URL**: [https://github.com/open-telemetry/opentelemetry-java](https://github.com/open-telemetry/opentelemetry-java) +**10** **Group:** `com.squareup.okio` **Name:** `okio-jvm` **Version:** `3.9.0` +> - **POM Project URL**: [https://github.com/square/okio/](https://github.com/square/okio/) > - **POM License**: Apache License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0](https://www.apache.org/licenses/LICENSE-2.0) -**9** **Group:** `io.opentelemetry` **Name:** `opentelemetry-context` **Version:** `1.27.0` +**11** **Group:** `io.opentelemetry` **Name:** `opentelemetry-api` **Version:** `1.41.0` > - **POM Project URL**: [https://github.com/open-telemetry/opentelemetry-java](https://github.com/open-telemetry/opentelemetry-java) -> - **POM License**: Apache License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0](https://www.apache.org/licenses/LICENSE-2.0) +> - **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:** `io.opentelemetry` **Name:** `opentelemetry-exporter-common` **Version:** `1.27.0` +**12** **Group:** `io.opentelemetry` **Name:** `opentelemetry-api-incubator` **Version:** `1.41.0-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) -**11** **Group:** `io.opentelemetry` **Name:** `opentelemetry-exporter-jaeger` **Version:** `1.27.0` +**13** **Group:** `io.opentelemetry` **Name:** `opentelemetry-context` **Version:** `1.41.0` > - **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) -**12** **Group:** `io.opentelemetry` **Name:** `opentelemetry-exporter-logging` **Version:** `1.27.0` +**14** **Group:** `io.opentelemetry` **Name:** `opentelemetry-exporter-common` **Version:** `1.41.0` > - **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) +> - **POM License**: Apache License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0](https://www.apache.org/licenses/LICENSE-2.0) -**13** **Group:** `io.opentelemetry` **Name:** `opentelemetry-exporter-logging-otlp` **Version:** `1.27.0` +**15** **Group:** `io.opentelemetry` **Name:** `opentelemetry-exporter-logging` **Version:** `1.41.0` > - **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) +> - **POM License**: Apache License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0](https://www.apache.org/licenses/LICENSE-2.0) -**14** **Group:** `io.opentelemetry` **Name:** `opentelemetry-exporter-otlp` **Version:** `1.27.0` +**16** **Group:** `io.opentelemetry` **Name:** `opentelemetry-exporter-logging-otlp` **Version:** `1.41.0` > - **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) +> - **POM License**: Apache License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0](https://www.apache.org/licenses/LICENSE-2.0) -**15** **Group:** `io.opentelemetry` **Name:** `opentelemetry-exporter-otlp-common` **Version:** `1.27.0` +**17** **Group:** `io.opentelemetry` **Name:** `opentelemetry-exporter-otlp` **Version:** `1.41.0` > - **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) +> - **POM License**: Apache License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0](https://www.apache.org/licenses/LICENSE-2.0) -**16** **Group:** `io.opentelemetry` **Name:** `opentelemetry-exporter-prometheus` **Version:** `1.27.0-alpha` +**18** **Group:** `io.opentelemetry` **Name:** `opentelemetry-exporter-otlp-common` **Version:** `1.41.0` > - **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) +> - **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:** `io.opentelemetry` **Name:** `opentelemetry-exporter-zipkin` **Version:** `1.27.0` +**19** **Group:** `io.opentelemetry` **Name:** `opentelemetry-exporter-prometheus` **Version:** `1.41.0-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) +> - **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:** `io.opentelemetry` **Name:** `opentelemetry-extension-incubator` **Version:** `1.27.0-alpha` +**20** **Group:** `io.opentelemetry` **Name:** `opentelemetry-exporter-sender-okhttp` **Version:** `1.41.0` > - **POM Project URL**: [https://github.com/open-telemetry/opentelemetry-java](https://github.com/open-telemetry/opentelemetry-java) > - **POM License**: Apache License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0](https://www.apache.org/licenses/LICENSE-2.0) -**19** **Group:** `io.opentelemetry` **Name:** `opentelemetry-extension-kotlin` **Version:** `1.27.0` +**21** **Group:** `io.opentelemetry` **Name:** `opentelemetry-exporter-zipkin` **Version:** `1.41.0` > - **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) +> - **POM License**: Apache License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0](https://www.apache.org/licenses/LICENSE-2.0) -**20** **Group:** `io.opentelemetry` **Name:** `opentelemetry-extension-trace-propagators` **Version:** `1.27.0` +**22** **Group:** `io.opentelemetry` **Name:** `opentelemetry-extension-kotlin` **Version:** `1.41.0` > - **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) +> - **POM License**: Apache License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0](https://www.apache.org/licenses/LICENSE-2.0) -**21** **Group:** `io.opentelemetry` **Name:** `opentelemetry-sdk` **Version:** `1.27.0` +**23** **Group:** `io.opentelemetry` **Name:** `opentelemetry-extension-trace-propagators` **Version:** `1.41.0` > - **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) +> - **POM License**: Apache License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0](https://www.apache.org/licenses/LICENSE-2.0) -**22** **Group:** `io.opentelemetry` **Name:** `opentelemetry-sdk-common` **Version:** `1.27.0` +**24** **Group:** `io.opentelemetry` **Name:** `opentelemetry-sdk` **Version:** `1.41.0` > - **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) +> - **POM License**: Apache License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0](https://www.apache.org/licenses/LICENSE-2.0) -**23** **Group:** `io.opentelemetry` **Name:** `opentelemetry-sdk-extension-autoconfigure` **Version:** `1.27.0-alpha` +**25** **Group:** `io.opentelemetry` **Name:** `opentelemetry-sdk-common` **Version:** `1.41.0` > - **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) +> - **POM License**: Apache License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0](https://www.apache.org/licenses/LICENSE-2.0) -**24** **Group:** `io.opentelemetry` **Name:** `opentelemetry-sdk-extension-autoconfigure-spi` **Version:** `1.27.0` +**26** **Group:** `io.opentelemetry` **Name:** `opentelemetry-sdk-extension-autoconfigure` **Version:** `1.41.0` > - **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) +> - **POM License**: Apache License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0](https://www.apache.org/licenses/LICENSE-2.0) -**25** **Group:** `io.opentelemetry` **Name:** `opentelemetry-sdk-extension-incubator` **Version:** `1.27.0-alpha` +**27** **Group:** `io.opentelemetry` **Name:** `opentelemetry-sdk-extension-autoconfigure-spi` **Version:** `1.41.0` > - **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) +> - **POM License**: Apache License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0](https://www.apache.org/licenses/LICENSE-2.0) -**26** **Group:** `io.opentelemetry` **Name:** `opentelemetry-sdk-extension-jaeger-remote-sampler` **Version:** `1.27.0` +**28** **Group:** `io.opentelemetry` **Name:** `opentelemetry-sdk-extension-incubator` **Version:** `1.41.0-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) +> - **POM License**: Apache License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0](https://www.apache.org/licenses/LICENSE-2.0) -**27** **Group:** `io.opentelemetry` **Name:** `opentelemetry-sdk-logs` **Version:** `1.27.0` +**29** **Group:** `io.opentelemetry` **Name:** `opentelemetry-sdk-extension-jaeger-remote-sampler` **Version:** `1.41.0` > - **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) +> - **POM License**: Apache License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0](https://www.apache.org/licenses/LICENSE-2.0) -**28** **Group:** `io.opentelemetry` **Name:** `opentelemetry-sdk-metrics` **Version:** `1.27.0` +**30** **Group:** `io.opentelemetry` **Name:** `opentelemetry-sdk-logs` **Version:** `1.41.0` > - **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) +> - **POM License**: Apache License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0](https://www.apache.org/licenses/LICENSE-2.0) -**29** **Group:** `io.opentelemetry` **Name:** `opentelemetry-sdk-trace` **Version:** `1.27.0` +**31** **Group:** `io.opentelemetry` **Name:** `opentelemetry-sdk-metrics` **Version:** `1.41.0` > - **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) +> - **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.opentelemetry` **Name:** `opentelemetry-semconv` **Version:** `1.27.0-alpha` +**32** **Group:** `io.opentelemetry` **Name:** `opentelemetry-sdk-trace` **Version:** `1.41.0` > - **POM Project URL**: [https://github.com/open-telemetry/opentelemetry-java](https://github.com/open-telemetry/opentelemetry-java) > - **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.opentelemetry.contrib` **Name:** `opentelemetry-aws-xray-propagator` **Version:** `1.27.0-alpha` +**33** **Group:** `io.opentelemetry.contrib` **Name:** `opentelemetry-aws-resources` **Version:** `1.38.0-alpha` +> - **POM Project URL**: [https://github.com/open-telemetry/opentelemetry-java-contrib](https://github.com/open-telemetry/opentelemetry-java-contrib) +> - **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.opentelemetry.contrib` **Name:** `opentelemetry-aws-xray-propagator` **Version:** `1.38.0-alpha` +> - **POM Project URL**: [https://github.com/open-telemetry/opentelemetry-java-contrib](https://github.com/open-telemetry/opentelemetry-java-contrib) +> - **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.opentelemetry.contrib` **Name:** `opentelemetry-baggage-processor` **Version:** `1.38.0-alpha` +> - **POM Project URL**: [https://github.com/open-telemetry/opentelemetry-java-contrib](https://github.com/open-telemetry/opentelemetry-java-contrib) +> - **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.opentelemetry.contrib` **Name:** `opentelemetry-gcp-resources` **Version:** `1.38.0-alpha` > - **POM Project URL**: [https://github.com/open-telemetry/opentelemetry-java-contrib](https://github.com/open-telemetry/opentelemetry-java-contrib) +> - **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.opentelemetry.semconv` **Name:** `opentelemetry-semconv` **Version:** `1.25.0-alpha` +> - **POM Project URL**: [https://github.com/open-telemetry/semantic-conventions-java](https://github.com/open-telemetry/semantic-conventions-java) > - **POM License**: Apache License, Version 2.0 - [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0) -**32** **Group:** `io.zipkin.reporter2` **Name:** `zipkin-reporter` **Version:** `2.16.3` +**38** **Group:** `io.opentelemetry.semconv` **Name:** `opentelemetry-semconv-incubating` **Version:** `1.25.0-alpha` +> - **POM Project URL**: [https://github.com/open-telemetry/semantic-conventions-java](https://github.com/open-telemetry/semantic-conventions-java) +> - **POM License**: Apache License, Version 2.0 - [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0) + +**39** **Group:** `io.prometheus` **Name:** `prometheus-metrics-config` **Version:** `1.3.1` +> - **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.prometheus` **Name:** `prometheus-metrics-exporter-common` **Version:** `1.3.1` +> - **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.prometheus` **Name:** `prometheus-metrics-exporter-httpserver` **Version:** `1.3.1` +> - **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.prometheus` **Name:** `prometheus-metrics-exposition-formats` **Version:** `1.3.1` +> - **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.prometheus` **Name:** `prometheus-metrics-model` **Version:** `1.3.1` +> - **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.prometheus` **Name:** `prometheus-metrics-shaded-protobuf` **Version:** `1.3.1` +> - **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.zipkin.reporter2` **Name:** `zipkin-reporter` **Version:** `3.4.0` > - **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) +> - **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**: [zipkin-reporter-3.4.0.jar/META-INF/LICENSE](zipkin-reporter-3.4.0.jar/META-INF/LICENSE) -**33** **Group:** `io.zipkin.reporter2` **Name:** `zipkin-sender-okhttp3` **Version:** `2.16.3` +**46** **Group:** `io.zipkin.reporter2` **Name:** `zipkin-sender-okhttp3` **Version:** `3.4.0` > - **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) +> - **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**: [zipkin-sender-okhttp3-3.4.0.jar/META-INF/LICENSE](zipkin-sender-okhttp3-3.4.0.jar/META-INF/LICENSE) -**34** **Group:** `io.zipkin.zipkin2` **Name:** `zipkin` **Version:** `2.23.2` +**47** **Group:** `io.zipkin.zipkin2` **Name:** `zipkin` **Version:** `2.27.1` > - **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) +> - **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**: [zipkin-2.27.1.jar/META-INF/LICENSE](zipkin-2.27.1.jar/META-INF/LICENSE) -**35** **Group:** `net.bytebuddy` **Name:** `byte-buddy-dep` **Version:** `1.14.5` +**48** **Group:** `net.bytebuddy` **Name:** `byte-buddy-dep` **Version:** `1.15.1` > - **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-dep-1.14.5.jar/META-INF/LICENSE](byte-buddy-dep-1.14.5.jar/META-INF/LICENSE) - - [byte-buddy-dep-1.14.5.jar/META-INF/NOTICE](byte-buddy-dep-1.14.5.jar/META-INF/NOTICE) +> - **Embedded license files**: [byte-buddy-dep-1.15.1.jar/META-INF/LICENSE](byte-buddy-dep-1.15.1.jar/META-INF/LICENSE) + - [byte-buddy-dep-1.15.1.jar/META-INF/NOTICE](byte-buddy-dep-1.15.1.jar/META-INF/NOTICE) -**36** **Group:** `org.jetbrains` **Name:** `annotations` **Version:** `13.0` +**49** **Group:** `org.jetbrains` **Name:** `annotations` **Version:** `13.0` > - **POM Project URL**: [http://www.jetbrains.org](http://www.jetbrains.org) -> - **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**: Apache License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0](https://www.apache.org/licenses/LICENSE-2.0) -**37** **Group:** `org.jetbrains.kotlin` **Name:** `kotlin-stdlib` **Version:** `1.8.22` +**50** **Group:** `org.jetbrains.kotlin` **Name:** `kotlin-stdlib` **Version:** `2.0.10` > - **POM Project URL**: [https://kotlinlang.org/](https://kotlinlang.org/) -> - **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**: Apache License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0](https://www.apache.org/licenses/LICENSE-2.0) -**38** **Group:** `org.jetbrains.kotlin` **Name:** `kotlin-stdlib-common` **Version:** `1.8.22` +**51** **Group:** `org.jetbrains.kotlin` **Name:** `kotlin-stdlib-jdk7` **Version:** `2.0.10` > - **POM Project URL**: [https://kotlinlang.org/](https://kotlinlang.org/) -> - **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**: Apache License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0](https://www.apache.org/licenses/LICENSE-2.0) -**39** **Group:** `org.jetbrains.kotlin` **Name:** `kotlin-stdlib-jdk7` **Version:** `1.8.22` +**52** **Group:** `org.jetbrains.kotlin` **Name:** `kotlin-stdlib-jdk8` **Version:** `2.0.10` > - **POM Project URL**: [https://kotlinlang.org/](https://kotlinlang.org/) -> - **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**: Apache License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0](https://www.apache.org/licenses/LICENSE-2.0) -**40** **Group:** `org.jetbrains.kotlin` **Name:** `kotlin-stdlib-jdk8` **Version:** `1.8.22` -> - **POM Project URL**: [https://kotlinlang.org/](https://kotlinlang.org/) -> - **POM License**: Apache License, Version 2.0 - [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0) +**53** **Group:** `org.ow2.asm` **Name:** `asm` **Version:** `9.7` +> - **Manifest Project URL**: [http://asm.ow2.org](http://asm.ow2.org) +> - **Manifest License**: The 3-Clause BSD License (Not Packaged) +> - **POM Project URL**: [http://asm.ow2.io/](http://asm.ow2.io/) +> - **POM License**: Apache License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0](https://www.apache.org/licenses/LICENSE-2.0) +> - **POM License**: The 3-Clause BSD License - [https://opensource.org/licenses/BSD-3-Clause](https://opensource.org/licenses/BSD-3-Clause) -**41** **Group:** `org.ow2.asm` **Name:** `asm` **Version:** `9.5` +**54** **Group:** `org.ow2.asm` **Name:** `asm-analysis` **Version:** `9.7` > - **Manifest Project URL**: [http://asm.ow2.org](http://asm.ow2.org) > - **Manifest License**: The 3-Clause BSD License (Not Packaged) > - **POM Project URL**: [http://asm.ow2.io/](http://asm.ow2.io/) -> - **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**: Apache License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0](https://www.apache.org/licenses/LICENSE-2.0) > - **POM License**: The 3-Clause BSD License - [https://opensource.org/licenses/BSD-3-Clause](https://opensource.org/licenses/BSD-3-Clause) -**42** **Group:** `org.ow2.asm` **Name:** `asm-commons` **Version:** `9.5` +**55** **Group:** `org.ow2.asm` **Name:** `asm-commons` **Version:** `9.7` > - **Manifest Project URL**: [http://asm.ow2.org](http://asm.ow2.org) > - **Manifest License**: The 3-Clause BSD License (Not Packaged) > - **POM Project URL**: [http://asm.ow2.io/](http://asm.ow2.io/) -> - **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**: Apache License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0](https://www.apache.org/licenses/LICENSE-2.0) > - **POM License**: The 3-Clause BSD License - [https://opensource.org/licenses/BSD-3-Clause](https://opensource.org/licenses/BSD-3-Clause) -**43** **Group:** `org.ow2.asm` **Name:** `asm-tree` **Version:** `9.5` +**56** **Group:** `org.ow2.asm` **Name:** `asm-tree` **Version:** `9.7` > - **Manifest Project URL**: [http://asm.ow2.org](http://asm.ow2.org) > - **Manifest License**: The 3-Clause BSD License (Not Packaged) > - **POM Project URL**: [http://asm.ow2.io/](http://asm.ow2.io/) -> - **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**: Apache License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0](https://www.apache.org/licenses/LICENSE-2.0) > - **POM License**: The 3-Clause BSD License - [https://opensource.org/licenses/BSD-3-Clause](https://opensource.org/licenses/BSD-3-Clause) -**44** **Group:** `org.snakeyaml` **Name:** `snakeyaml-engine` **Version:** `2.6` +**57** **Group:** `org.ow2.asm` **Name:** `asm-util` **Version:** `9.7` +> - **Manifest Project URL**: [http://asm.ow2.org](http://asm.ow2.org) +> - **Manifest License**: The 3-Clause BSD License (Not Packaged) +> - **POM Project URL**: [http://asm.ow2.io/](http://asm.ow2.io/) +> - **POM License**: Apache License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0](https://www.apache.org/licenses/LICENSE-2.0) +> - **POM License**: The 3-Clause BSD License - [https://opensource.org/licenses/BSD-3-Clause](https://opensource.org/licenses/BSD-3-Clause) + +**58** **Group:** `org.snakeyaml` **Name:** `snakeyaml-engine` **Version:** `2.7` > - **Manifest License**: Apache License, Version 2.0 (Not Packaged) > - **POM Project URL**: [https://bitbucket.org/snakeyaml/snakeyaml-engine](https://bitbucket.org/snakeyaml/snakeyaml-engine) > - **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:** `org.yaml` **Name:** `snakeyaml` **Version:** `2.2` +> - **Manifest License**: Apache License, Version 2.0 (Not Packaged) +> - **POM Project URL**: [https://bitbucket.org/snakeyaml/snakeyaml](https://bitbucket.org/snakeyaml/snakeyaml) +> - **POM License**: Apache License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0](https://www.apache.org/licenses/LICENSE-2.0) + ## MIT License -**45** **Group:** `org.slf4j` **Name:** `slf4j-api` **Version:** `2.0.7` +**60** **Group:** `org.slf4j` **Name:** `slf4j-api` **Version:** `2.0.16` > - **Project URL**: [http://www.slf4j.org](http://www.slf4j.org) > - **POM License**: MIT License - [https://opensource.org/licenses/MIT](https://opensource.org/licenses/MIT) -> - **Embedded license files**: [slf4j-api-2.0.7.jar/META-INF/LICENSE.txt](slf4j-api-2.0.7.jar/META-INF/LICENSE.txt) +> - **Embedded license files**: [slf4j-api-2.0.16.jar/META-INF/LICENSE.txt](slf4j-api-2.0.16.jar/META-INF/LICENSE.txt) -**46** **Group:** `org.slf4j` **Name:** `slf4j-simple` **Version:** `2.0.7` +**61** **Group:** `org.slf4j` **Name:** `slf4j-simple` **Version:** `2.0.16` > - **Project URL**: [http://www.slf4j.org](http://www.slf4j.org) > - **POM License**: MIT License - [https://opensource.org/licenses/MIT](https://opensource.org/licenses/MIT) -> - **Embedded license files**: [slf4j-simple-2.0.7.jar/META-INF/LICENSE.txt](slf4j-simple-2.0.7.jar/META-INF/LICENSE.txt) +> - **Embedded license files**: [slf4j-simple-2.0.16.jar/META-INF/LICENSE.txt](slf4j-simple-2.0.16.jar/META-INF/LICENSE.txt) ## The 3-Clause BSD License -**47** **Group:** `org.ow2.asm` **Name:** `asm` **Version:** `9.5` +**62** **Group:** `org.ow2.asm` **Name:** `asm` **Version:** `9.7` > - **Manifest Project URL**: [http://asm.ow2.org](http://asm.ow2.org) > - **Manifest License**: The 3-Clause BSD License (Not Packaged) > - **POM Project URL**: [http://asm.ow2.io/](http://asm.ow2.io/) -> - **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**: Apache License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0](https://www.apache.org/licenses/LICENSE-2.0) > - **POM License**: The 3-Clause BSD License - [https://opensource.org/licenses/BSD-3-Clause](https://opensource.org/licenses/BSD-3-Clause) -**48** **Group:** `org.ow2.asm` **Name:** `asm-commons` **Version:** `9.5` +**63** **Group:** `org.ow2.asm` **Name:** `asm-analysis` **Version:** `9.7` > - **Manifest Project URL**: [http://asm.ow2.org](http://asm.ow2.org) > - **Manifest License**: The 3-Clause BSD License (Not Packaged) > - **POM Project URL**: [http://asm.ow2.io/](http://asm.ow2.io/) -> - **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**: Apache License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0](https://www.apache.org/licenses/LICENSE-2.0) > - **POM License**: The 3-Clause BSD License - [https://opensource.org/licenses/BSD-3-Clause](https://opensource.org/licenses/BSD-3-Clause) -**49** **Group:** `org.ow2.asm` **Name:** `asm-tree` **Version:** `9.5` +**64** **Group:** `org.ow2.asm` **Name:** `asm-commons` **Version:** `9.7` > - **Manifest Project URL**: [http://asm.ow2.org](http://asm.ow2.org) > - **Manifest License**: The 3-Clause BSD License (Not Packaged) > - **POM Project URL**: [http://asm.ow2.io/](http://asm.ow2.io/) -> - **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**: Apache License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0](https://www.apache.org/licenses/LICENSE-2.0) > - **POM License**: The 3-Clause BSD License - [https://opensource.org/licenses/BSD-3-Clause](https://opensource.org/licenses/BSD-3-Clause) -## Unknown +**65** **Group:** `org.ow2.asm` **Name:** `asm-tree` **Version:** `9.7` +> - **Manifest Project URL**: [http://asm.ow2.org](http://asm.ow2.org) +> - **Manifest License**: The 3-Clause BSD License (Not Packaged) +> - **POM Project URL**: [http://asm.ow2.io/](http://asm.ow2.io/) +> - **POM License**: Apache License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0](https://www.apache.org/licenses/LICENSE-2.0) +> - **POM License**: The 3-Clause BSD License - [https://opensource.org/licenses/BSD-3-Clause](https://opensource.org/licenses/BSD-3-Clause) -**50** **Group:** `com.squareup.okio` **Name:** `okio` **Version:** `3.2.0` +**66** **Group:** `org.ow2.asm` **Name:** `asm-util` **Version:** `9.7` +> - **Manifest Project URL**: [http://asm.ow2.org](http://asm.ow2.org) +> - **Manifest License**: The 3-Clause BSD License (Not Packaged) +> - **POM Project URL**: [http://asm.ow2.io/](http://asm.ow2.io/) +> - **POM License**: Apache License, Version 2.0 - [https://www.apache.org/licenses/LICENSE-2.0](https://www.apache.org/licenses/LICENSE-2.0) +> - **POM License**: The 3-Clause BSD License - [https://opensource.org/licenses/BSD-3-Clause](https://opensource.org/licenses/BSD-3-Clause) diff --git a/licenses/okhttp-4.11.0.jar/okhttp3/internal/publicsuffix/NOTICE b/licenses/okhttp-4.12.0.jar/okhttp3/internal/publicsuffix/NOTICE similarity index 100% rename from licenses/okhttp-4.11.0.jar/okhttp3/internal/publicsuffix/NOTICE rename to licenses/okhttp-4.12.0.jar/okhttp3/internal/publicsuffix/NOTICE diff --git a/licenses/slf4j-simple-2.0.7.jar/META-INF/LICENSE.txt b/licenses/slf4j-api-2.0.16.jar/META-INF/LICENSE.txt similarity index 95% rename from licenses/slf4j-simple-2.0.7.jar/META-INF/LICENSE.txt rename to licenses/slf4j-api-2.0.16.jar/META-INF/LICENSE.txt index e4079f54f6c0..1a3d053237be 100644 --- a/licenses/slf4j-simple-2.0.7.jar/META-INF/LICENSE.txt +++ b/licenses/slf4j-api-2.0.16.jar/META-INF/LICENSE.txt @@ -1,4 +1,4 @@ -Copyright (c) 2004-2023 QOS.ch +Copyright (c) 2004-2022 QOS.ch Sarl (Switzerland) All rights reserved. Permission is hereby granted, free of charge, to any person obtaining diff --git a/licenses/slf4j-api-2.0.7.jar/META-INF/LICENSE.txt b/licenses/slf4j-simple-2.0.16.jar/META-INF/LICENSE.txt similarity index 95% rename from licenses/slf4j-api-2.0.7.jar/META-INF/LICENSE.txt rename to licenses/slf4j-simple-2.0.16.jar/META-INF/LICENSE.txt index e4079f54f6c0..1a3d053237be 100644 --- a/licenses/slf4j-api-2.0.7.jar/META-INF/LICENSE.txt +++ b/licenses/slf4j-simple-2.0.16.jar/META-INF/LICENSE.txt @@ -1,4 +1,4 @@ -Copyright (c) 2004-2023 QOS.ch +Copyright (c) 2004-2022 QOS.ch Sarl (Switzerland) All rights reserved. Permission is hereby granted, free of charge, to any person obtaining diff --git a/licenses/zipkin-2.23.2.jar/META-INF/LICENSE b/licenses/zipkin-2.27.1.jar/META-INF/LICENSE similarity index 100% rename from licenses/zipkin-2.23.2.jar/META-INF/LICENSE rename to licenses/zipkin-2.27.1.jar/META-INF/LICENSE diff --git a/licenses/zipkin-reporter-3.4.0.jar/META-INF/LICENSE b/licenses/zipkin-reporter-3.4.0.jar/META-INF/LICENSE new file mode 100644 index 000000000000..f88137fc2797 --- /dev/null +++ b/licenses/zipkin-reporter-3.4.0.jar/META-INF/LICENSE @@ -0,0 +1,211 @@ + 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 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/licenses/zipkin-sender-okhttp3-3.4.0.jar/META-INF/LICENSE b/licenses/zipkin-sender-okhttp3-3.4.0.jar/META-INF/LICENSE new file mode 100644 index 000000000000..f88137fc2797 --- /dev/null +++ b/licenses/zipkin-sender-okhttp3-3.4.0.jar/META-INF/LICENSE @@ -0,0 +1,211 @@ + 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 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/muzzle/build.gradle.kts b/muzzle/build.gradle.kts index bd25aeff7ed8..5e8f5ff9b9f6 100644 --- a/muzzle/build.gradle.kts +++ b/muzzle/build.gradle.kts @@ -18,6 +18,7 @@ dependencies { implementation(project(":javaagent-bootstrap")) implementation(project(":instrumentation-api")) + implementation(project(":instrumentation-api-incubator")) implementation(project(":javaagent-extension-api")) // Used by byte-buddy but not brought in as a transitive dependency. diff --git a/muzzle/src/main/java/io/opentelemetry/javaagent/tooling/ByteArrayUrl.java b/muzzle/src/main/java/io/opentelemetry/javaagent/tooling/ByteArrayUrl.java new file mode 100644 index 000000000000..ccbce0f16a6f --- /dev/null +++ b/muzzle/src/main/java/io/opentelemetry/javaagent/tooling/ByteArrayUrl.java @@ -0,0 +1,104 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.tooling; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLConnection; +import java.net.URLEncoder; +import java.net.URLStreamHandler; +import java.nio.charset.StandardCharsets; +import java.security.Permission; +import java.security.PrivilegedAction; + +public class ByteArrayUrl { + + private ByteArrayUrl() {} + + private static final String URL_SCHEMA = "x-otel-binary"; + + @SuppressWarnings("removal") + public static URL create(String contentName, byte[] data) { + if (System.getSecurityManager() != null) { + return java.security.AccessController.doPrivileged( + (PrivilegedAction) + () -> { + return doCreate(contentName, data); + }); + } else { + return doCreate(contentName, data); + } + } + + private static URL doCreate(String contentName, byte[] data) { + try { + String file = URLEncoder.encode(contentName, StandardCharsets.UTF_8.toString()); + return new URL(URL_SCHEMA, null, -1, file, new ByteArrayUrlStreamHandler(data)); + } catch (MalformedURLException e) { + throw new IllegalArgumentException("Failed to generate URL for the provided arguments", e); + } catch (UnsupportedEncodingException e) { + throw new IllegalStateException(e); + } + } + + /** + * This class is based on ByteBuddy {@link + * net.bytebuddy.dynamic.loading.ByteArrayClassLoader.PersistenceHandler}. + */ + private static class ByteArrayUrlStreamHandler extends URLStreamHandler { + + /** The binary representation of a type's class file. */ + private final byte[] binaryRepresentation; + + /** + * Creates a new byte array URL stream handler. + * + * @param binaryRepresentation The binary representation of a type's class file. + */ + private ByteArrayUrlStreamHandler(byte[] binaryRepresentation) { + this.binaryRepresentation = binaryRepresentation; + } + + @Override + protected URLConnection openConnection(URL url) { + return new ByteArrayUrlConnection(url); + } + + private class ByteArrayUrlConnection extends URLConnection { + + private final InputStream inputStream; + + protected ByteArrayUrlConnection(URL url) { + super(url); + inputStream = new ByteArrayInputStream(binaryRepresentation); + } + + @Override + public void connect() { + connected = true; + } + + @Override + public InputStream getInputStream() { + connect(); // Mimics the semantics of an actual URL connection. + return inputStream; + } + + @Override + public Permission getPermission() { + return null; + } + + @Override + public long getContentLengthLong() { + return binaryRepresentation.length; + } + } + } +} diff --git a/muzzle/src/main/java/io/opentelemetry/javaagent/tooling/BytecodeWithUrl.java b/muzzle/src/main/java/io/opentelemetry/javaagent/tooling/BytecodeWithUrl.java new file mode 100644 index 000000000000..82edcc6cc256 --- /dev/null +++ b/muzzle/src/main/java/io/opentelemetry/javaagent/tooling/BytecodeWithUrl.java @@ -0,0 +1,195 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.tooling; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import net.bytebuddy.dynamic.DynamicType; +import net.bytebuddy.utility.StreamDrainer; + +/** + * Provides the bytecode and the original resource URL for loaded and not-yet loaded classes. The + * implementation is based on {@link net.bytebuddy.dynamic.ClassFileLocator.ForClassLoader}, with + * the difference that it preserves the original classfile resource URL. In addition {@link + * BytecodeWithUrl} created can be generated for runtime generated types, this class will take care + * of runtime generating URLs for those. + */ +public abstract class BytecodeWithUrl { + + private BytecodeWithUrl() {} + + /** + * Provides a URL pointing to the specific classfile. + * + * @return the URL + */ + public abstract URL getUrl(); + + /** + * Provides the bytecode of the class. The result is the same as calling {@link URL#openStream()} + * on {@link #getUrl()} and draining that stream. + * + * @return the bytecode of the class. + */ + public abstract byte[] getBytecode(); + + /** + * Creates a cached copy of this {@link BytecodeWithUrl}. The cached copy eagerly loads the + * bytecode, so that {@link #getBytecode()} is guaranteed to not cause any IO. This comes at the + * cost of a higher heap consumption, as the bytecode is kept in memory. + * + * @return an ClassFileSource implementing the described caching behaviour. + */ + public abstract BytecodeWithUrl cached(); + + /** + * Creates a {@link BytecodeWithUrl} for the class with the provided fully qualified name. The + * .class file for the provided classname must be available as a resource in the provided + * classloader. The class is guaranteed to not be loaded during this process. + * + * @param className the fully qualified name of the class to copy + * @param classLoader the classloader + * @return the ClassCopySource which can be used to copy the provided class to other classloaders. + */ + public static BytecodeWithUrl create(String className, ClassLoader classLoader) { + if (classLoader == null) { + throw new IllegalArgumentException( + "Copying classes from the bootstrap classloader is not supported!"); + } + String classFileName = className.replace('.', '/') + ".class"; + return new Lazy(classLoader, classFileName); + } + + /** + * Same as {@link #create(String, ClassLoader)}, but easier to use for already loaded classes. + * + * @param loadedClass the class to copy + * @return the ClassCopySource which can be used to copy the provided class to other classloaders. + */ + public static BytecodeWithUrl create(Class loadedClass) { + return create(loadedClass.getName(), loadedClass.getClassLoader()); + } + + /** + * Creates a {@link BytecodeWithUrl} for a runtime-generated type. It will also provide an + * artificially generated {@link URL} pointing to the in-memory bytecode. + * + * @param className the name of the class represented by the provided bytecode + * @param bytecode the bytecode of the class + * @return the {@link BytecodeWithUrl} referring to this dynamically generated class + */ + public static BytecodeWithUrl create(String className, byte[] bytecode) { + return new ForDynamicType(className, bytecode); + } + + /** + * Invokes {@link #create(String, byte[])} for the provided dynamic type. + * + * @param dynamicType the type to generate the {@link BytecodeWithUrl} + * @return the {@link BytecodeWithUrl} referring to this dynamically generated type + */ + public static BytecodeWithUrl create(DynamicType.Unloaded dynamicType) { + String className = dynamicType.getTypeDescription().getName(); + return new ForDynamicType(className, dynamicType.getBytes()); + } + + private static class Lazy extends BytecodeWithUrl { + + private final ClassLoader classLoader; + private final String resourceName; + + private Lazy(ClassLoader classLoader, String resourceName) { + this.classLoader = classLoader; + this.resourceName = resourceName; + } + + @Override + public URL getUrl() { + URL url = classLoader.getResource(resourceName); + if (url == null) { + throw new IllegalStateException( + "Classfile " + resourceName + " does not exist in the provided classloader!"); + } + return url; + } + + @Override + public byte[] getBytecode() { + try (InputStream bytecodeStream = getUrl().openStream()) { + return StreamDrainer.DEFAULT.drain(bytecodeStream); + } catch (IOException e) { + throw new IllegalStateException("Failed to read classfile URL", e); + } + } + + @Override + public BytecodeWithUrl cached() { + return new Cached(this); + } + } + + private static class Cached extends BytecodeWithUrl { + + private final URL classFileUrl; + + private final byte[] cachedByteCode; + + private Cached(BytecodeWithUrl.Lazy from) { + classFileUrl = from.getUrl(); + cachedByteCode = from.getBytecode(); + } + + @Override + public URL getUrl() { + return classFileUrl; + } + + @Override + public byte[] getBytecode() { + return cachedByteCode; + } + + @Override + public BytecodeWithUrl cached() { + return this; + } + } + + private static class ForDynamicType extends BytecodeWithUrl { + + private final byte[] byteCode; + private final String className; + private volatile URL generatedUrl; + + private ForDynamicType(String className, byte[] byteCode) { + this.byteCode = byteCode; + this.className = className; + } + + @Override + public URL getUrl() { + if (generatedUrl == null) { + synchronized (this) { + if (generatedUrl == null) { + generatedUrl = ByteArrayUrl.create(className, byteCode); + } + } + } + return generatedUrl; + } + + @Override + public byte[] getBytecode() { + return byteCode; + } + + @Override + public BytecodeWithUrl cached() { + return this; // this type already holds the bytecode in-memory + } + } +} diff --git a/muzzle/src/main/java/io/opentelemetry/javaagent/tooling/HelperClassDefinition.java b/muzzle/src/main/java/io/opentelemetry/javaagent/tooling/HelperClassDefinition.java new file mode 100644 index 000000000000..703033b6f085 --- /dev/null +++ b/muzzle/src/main/java/io/opentelemetry/javaagent/tooling/HelperClassDefinition.java @@ -0,0 +1,53 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.tooling; + +import io.opentelemetry.javaagent.extension.instrumentation.internal.injection.InjectionMode; +import net.bytebuddy.dynamic.DynamicType; + +public class HelperClassDefinition { + + private final String className; + private final BytecodeWithUrl bytecode; + private final InjectionMode injectionMode; + + private HelperClassDefinition( + String className, BytecodeWithUrl bytecode, InjectionMode injectionMode) { + this.className = className; + this.bytecode = bytecode; + this.injectionMode = injectionMode; + } + + public static HelperClassDefinition create( + String className, BytecodeWithUrl bytecode, InjectionMode injectionMode) { + return new HelperClassDefinition(className, bytecode, injectionMode); + } + + public static HelperClassDefinition create( + DynamicType.Unloaded type, InjectionMode injectionMode) { + String name = type.getTypeDescription().getName(); + BytecodeWithUrl code = BytecodeWithUrl.create(type); + return create(name, code, injectionMode); + } + + public static HelperClassDefinition create( + String className, ClassLoader copyFrom, InjectionMode injectionMode) { + BytecodeWithUrl code = BytecodeWithUrl.create(className, copyFrom); + return create(className, code, injectionMode); + } + + public String getClassName() { + return className; + } + + public BytecodeWithUrl getBytecode() { + return bytecode; + } + + public InjectionMode getInjectionMode() { + return injectionMode; + } +} diff --git a/muzzle/src/main/java/io/opentelemetry/javaagent/tooling/HelperInjector.java b/muzzle/src/main/java/io/opentelemetry/javaagent/tooling/HelperInjector.java index 4860a9491ddd..b5464273b60b 100644 --- a/muzzle/src/main/java/io/opentelemetry/javaagent/tooling/HelperInjector.java +++ b/muzzle/src/main/java/io/opentelemetry/javaagent/tooling/HelperInjector.java @@ -12,6 +12,7 @@ import io.opentelemetry.instrumentation.api.internal.cache.Cache; import io.opentelemetry.javaagent.bootstrap.HelperResources; import io.opentelemetry.javaagent.bootstrap.InjectedClassHelper; +import io.opentelemetry.javaagent.extension.instrumentation.internal.injection.InjectionMode; import io.opentelemetry.javaagent.tooling.muzzle.HelperResource; import java.io.File; import java.io.IOException; @@ -23,18 +24,16 @@ 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.ConcurrentHashMap; +import java.util.function.Function; import java.util.function.Supplier; +import java.util.stream.Collectors; import javax.annotation.Nullable; 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; @@ -90,14 +89,12 @@ Class inject(ClassLoader classLoader, String className) { private final String requestingName; - private final Set helperClassNames; + private final Function> helperClassesGenerator; private final List helperResources; @Nullable private final ClassLoader helpersSource; @Nullable private final Instrumentation instrumentation; - private final Map> dynamicTypeMap = new LinkedHashMap<>(); private final Cache injectedClassLoaders = Cache.weak(); - private final Cache resourcesInjectedClassLoaders = Cache.weak(); /** * Construct HelperInjector. @@ -118,23 +115,31 @@ public HelperInjector( Instrumentation instrumentation) { this.requestingName = requestingName; - this.helperClassNames = new LinkedHashSet<>(helperClassNames); + List helpers = + helperClassNames.stream() + .map( + className -> + HelperClassDefinition.create( + className, helpersSource, InjectionMode.CLASS_ONLY)) + .collect(Collectors.toList()); + + this.helperClassesGenerator = (cl) -> helpers; this.helperResources = helperResources; this.helpersSource = helpersSource; this.instrumentation = instrumentation; } - private HelperInjector( + public HelperInjector( String requestingName, - Map> helperMap, + Function> helperClassesGenerators, + List helperResources, + ClassLoader helpersSource, Instrumentation instrumentation) { this.requestingName = requestingName; - this.helperClassNames = helperMap.keySet(); - this.dynamicTypeMap.putAll(helperMap); - - this.helperResources = Collections.emptyList(); - this.helpersSource = null; + this.helperClassesGenerator = helperClassesGenerators; + this.helperResources = helperResources; + this.helpersSource = helpersSource; this.instrumentation = instrumentation; } @@ -142,43 +147,20 @@ public static HelperInjector forDynamicTypes( String requestingName, Collection> helpers, Instrumentation instrumentation) { - Map> bytes = new HashMap<>(helpers.size()); - for (DynamicType.Unloaded helper : helpers) { - bytes.put(helper.getTypeDescription().getName(), helper::getBytes); - } - return new HelperInjector(requestingName, bytes, instrumentation); + + List helperDefinitions = + helpers.stream() + .map(helperType -> HelperClassDefinition.create(helperType, InjectionMode.CLASS_ONLY)) + .collect(Collectors.toList()); + + return new HelperInjector( + requestingName, cl -> helperDefinitions, Collections.emptyList(), null, instrumentation); } public static void setHelperInjectorListener(HelperInjectorListener listener) { helperInjectorListener = listener; } - private Map> getHelperMap() { - if (dynamicTypeMap.isEmpty()) { - Map> result = new LinkedHashMap<>(); - - for (String helperClassName : helperClassNames) { - result.put( - helperClassName, - () -> { - try (ClassFileLocator locator = ClassFileLocator.ForClassLoader.of(helpersSource)) { - return locator.locate(helperClassName).resolve(); - } catch (IOException exception) { - if (logger.isLoggable(SEVERE)) { - logger.log( - SEVERE, "Failed to read {0}", new Object[] {helperClassName}, exception); - } - throw new IllegalStateException("Failed to read " + helperClassName, exception); - } - }); - } - - return result; - } else { - return dynamicTypeMap; - } - } - @Override @CanIgnoreReturnValue public DynamicType.Builder transform( @@ -187,114 +169,137 @@ public DynamicType.Builder transform( ClassLoader classLoader, JavaModule javaModule, ProtectionDomain protectionDomain) { - if (!helperClassNames.isEmpty()) { - injectHelperClasses(typeDescription, classLoader); - } - - if (classLoader != null && helpersSource != null && !helperResources.isEmpty()) { - injectHelperResources(classLoader); - } - - return builder; - } - - private void injectHelperResources(ClassLoader classLoader) { - resourcesInjectedClassLoaders.computeIfAbsent( - classLoader, + injectedClassLoaders.computeIfAbsent( + maskNullClassLoader(classLoader), cl -> { - for (HelperResource helperResource : helperResources) { - List resources; - try { - resources = - Collections.list(helpersSource.getResources(helperResource.getAgentPath())); - } catch (IOException e) { - logger.log( - SEVERE, - "Unexpected exception occurred when loading resources {}; skipping", - new Object[] {helperResource.getAgentPath()}, - e); - continue; - } - if (resources.isEmpty()) { - logger.log( - FINE, - "Helper resources {0} requested but not found.", - helperResource.getAgentPath()); - continue; - } - - if (helperResource.allClassLoaders()) { - logger.log( - FINE, - "Injecting resources onto all classloaders: {0}", - helperResource.getApplicationPath()); - HelperResources.registerForAllClassLoaders( - helperResource.getApplicationPath(), resources); - } else { - if (logger.isLoggable(FINE)) { - logger.log( - FINE, - "Injecting resources onto class loader {0} -> {1}", - new Object[] {classLoader, helperResource.getApplicationPath()}); - } - HelperResources.register(classLoader, helperResource.getApplicationPath(), resources); - } + List helpers = + helperClassesGenerator.apply(unmaskNullClassLoader(cl)); + + LinkedHashMap> classesToInject = + helpers.stream() + .filter(helper -> helper.getInjectionMode().shouldInjectClass()) + .collect( + Collectors.toMap( + HelperClassDefinition::getClassName, + helper -> () -> helper.getBytecode().getBytecode(), + (a, b) -> { + throw new IllegalStateException( + "Duplicate classnames for helper class detected!"); + }, + LinkedHashMap::new)); + + Map classResourcesToInject = + helpers.stream() + .filter(helper -> helper.getInjectionMode().shouldInjectResource()) + .collect( + Collectors.toMap( + helper -> helper.getClassName().replace('.', '/') + ".class", + helper -> helper.getBytecode().getUrl())); + + injectHelperClasses(typeDescription, cl, classesToInject); + if (!isBootClassLoader(cl)) { + injectHelperResources(cl, classResourcesToInject); } - return true; }); + return builder; } - private void injectHelperClasses(TypeDescription typeDescription, ClassLoader classLoader) { - classLoader = maskNullClassLoader(classLoader); + private void injectHelperResources( + ClassLoader classLoader, Map additionalResources) { + for (HelperResource helperResource : helperResources) { + List resources; + try { + resources = Collections.list(helpersSource.getResources(helperResource.getAgentPath())); + } catch (IOException e) { + logger.log( + SEVERE, + "Unexpected exception occurred when loading resources {}; skipping", + new Object[] {helperResource.getAgentPath()}, + e); + continue; + } + if (resources.isEmpty()) { + logger.log( + FINE, "Helper resources {0} requested but not found.", helperResource.getAgentPath()); + continue; + } + + if (helperResource.allClassLoaders()) { + logger.log( + FINE, + "Injecting resources onto all classloaders: {0}", + helperResource.getApplicationPath()); + HelperResources.registerForAllClassLoaders(helperResource.getApplicationPath(), resources); + } else { + injectResourceToClassloader(classLoader, helperResource.getApplicationPath(), resources); + } + } + additionalResources.forEach( + (path, url) -> + injectResourceToClassloader(classLoader, path, Collections.singletonList(url))); + } + + private static void injectResourceToClassloader( + ClassLoader classLoader, String path, List resources) { + if (logger.isLoggable(FINE)) { + logger.log( + FINE, + "Injecting resources onto class loader {0} -> {1}", + new Object[] {classLoader, path}); + } + HelperResources.register(classLoader, path, resources); + } + + @SuppressWarnings("NonApiType") + private void injectHelperClasses( + TypeDescription typeDescription, + ClassLoader classLoader, + LinkedHashMap> classnameToBytes) { + if (classnameToBytes.isEmpty()) { + return; + } if (classLoader == BOOTSTRAP_CLASSLOADER_PLACEHOLDER && instrumentation == null) { logger.log( SEVERE, "Cannot inject helpers into the bootstrap class loader without an instance of Instrumentation. Programmer error!"); return; } + try { + if (logger.isLoggable(FINE)) { + logger.log( + FINE, + "Injecting classes onto class loader {0} -> {1}", + new Object[] {classLoader, classnameToBytes.keySet()}); + } - injectedClassLoaders.computeIfAbsent( - classLoader, - cl -> { - try { - if (logger.isLoggable(FINE)) { - logger.log( - FINE, - "Injecting classes onto class loader {0} -> {1}", - new Object[] {cl, helperClassNames}); - } - - Map> classnameToBytes = getHelperMap(); - Map map = - helperInjectors.computeIfAbsent(cl, (unused) -> new ConcurrentHashMap<>()); - for (Map.Entry> entry : classnameToBytes.entrySet()) { - // for boot loader we use a placeholder injector, we only need these classes to be - // in the injected classes map to later tell which of the classes are injected - HelperClassInjector injector = - isBootClassLoader(cl) - ? BOOT_CLASS_INJECTOR - : new HelperClassInjector(entry.getValue()); - map.put(entry.getKey(), injector); - } - - // For boot loader we define the classes immediately. For other loaders we load them - // from the loadClass method of the class loader. - if (isBootClassLoader(cl)) { - injectBootstrapClassLoader(classnameToBytes); - } - } catch (Exception e) { - if (logger.isLoggable(SEVERE)) { - logger.log( - SEVERE, - "Error preparing helpers while processing {0} for {1}. Failed to inject helper classes into instance {2}", - new Object[] {typeDescription, requestingName, cl}, - e); - } - throw new IllegalStateException(e); - } - return true; - }); + Map map = + helperInjectors.computeIfAbsent(classLoader, (unused) -> new ConcurrentHashMap<>()); + for (Map.Entry> entry : classnameToBytes.entrySet()) { + // for boot loader we use a placeholder injector, we only need these classes to be + // in the injected classes map to later tell which of the classes are injected + HelperClassInjector injector = + isBootClassLoader(classLoader) + ? BOOT_CLASS_INJECTOR + : new HelperClassInjector(entry.getValue()); + map.put(entry.getKey(), injector); + } + + // For boot loader we define the classes immediately. For other loaders we load them + // from the loadClass method of the class loader. + if (isBootClassLoader(classLoader)) { + injectBootstrapClassLoader(classnameToBytes); + } + } catch (Exception e) { + if (logger.isLoggable(SEVERE)) { + logger.log( + SEVERE, + "Error preparing helpers while processing {0} for {1}. Failed to inject helper classes into instance {2}", + new Object[] {typeDescription, requestingName, classLoader}, + e); + } + throw new IllegalStateException(e); + } } private static Map resolve(Map> classes) { @@ -351,6 +356,10 @@ private static ClassLoader maskNullClassLoader(ClassLoader classLoader) { return classLoader != null ? classLoader : BOOTSTRAP_CLASSLOADER_PLACEHOLDER; } + private static ClassLoader unmaskNullClassLoader(ClassLoader classLoader) { + return isBootClassLoader(classLoader) ? null : classLoader; + } + private static boolean isBootClassLoader(ClassLoader classLoader) { return classLoader == BOOTSTRAP_CLASSLOADER_PLACEHOLDER; } @@ -403,7 +412,7 @@ Class inject(ClassLoader classLoader, String className) { } } - @SuppressWarnings({"deprecation", "removal"}) // AccessController is deprecated + @SuppressWarnings("removal") // AccessController is deprecated for removal private static T execute(PrivilegedAction action) { if (System.getSecurityManager() != null) { return java.security.AccessController.doPrivileged(action); diff --git a/muzzle/src/main/java/io/opentelemetry/javaagent/tooling/muzzle/AgentCachingPoolStrategy.java b/muzzle/src/main/java/io/opentelemetry/javaagent/tooling/muzzle/AgentCachingPoolStrategy.java index afc19d21511b..a9385f8bd45c 100644 --- a/muzzle/src/main/java/io/opentelemetry/javaagent/tooling/muzzle/AgentCachingPoolStrategy.java +++ b/muzzle/src/main/java/io/opentelemetry/javaagent/tooling/muzzle/AgentCachingPoolStrategy.java @@ -9,7 +9,7 @@ import io.opentelemetry.instrumentation.api.internal.cache.Cache; import io.opentelemetry.javaagent.bootstrap.InstrumentationHolder; import io.opentelemetry.javaagent.bootstrap.VirtualFieldAccessorMarker; -import io.opentelemetry.javaagent.bootstrap.internal.InstrumentationConfig; +import io.opentelemetry.javaagent.bootstrap.internal.AgentInstrumentationConfig; import java.lang.instrument.Instrumentation; import java.lang.ref.WeakReference; import java.lang.reflect.Method; @@ -53,7 +53,7 @@ public class AgentCachingPoolStrategy implements AgentBuilder.PoolStrategy { // others to avoid creation of synthetic accessors private static final boolean REFLECTION_ENABLED = - InstrumentationConfig.get() + AgentInstrumentationConfig.get() .getBoolean("otel.instrumentation.internal-reflection.enabled", true); private static final Method findLoadedClassMethod = getFindLoadedClassMethod(); diff --git a/muzzle/src/main/java/io/opentelemetry/javaagent/tooling/muzzle/ReferenceCollectingClassVisitor.java b/muzzle/src/main/java/io/opentelemetry/javaagent/tooling/muzzle/ReferenceCollectingClassVisitor.java index 0efe16b37323..7ac429efa628 100644 --- a/muzzle/src/main/java/io/opentelemetry/javaagent/tooling/muzzle/ReferenceCollectingClassVisitor.java +++ b/muzzle/src/main/java/io/opentelemetry/javaagent/tooling/muzzle/ReferenceCollectingClassVisitor.java @@ -7,6 +7,7 @@ import com.google.common.collect.EvictingQueue; import io.opentelemetry.instrumentation.api.util.VirtualField; +import io.opentelemetry.javaagent.extension.instrumentation.internal.AsmApi; import io.opentelemetry.javaagent.tooling.muzzle.references.ClassRef; import io.opentelemetry.javaagent.tooling.muzzle.references.ClassRefBuilder; import io.opentelemetry.javaagent.tooling.muzzle.references.Flag; @@ -90,6 +91,9 @@ private static MinimumVisibilityFlag computeMinimumFieldAccess(Type from, Type t private static MinimumVisibilityFlag computeMinimumMethodAccess(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 @@ -123,7 +127,7 @@ private static Type underlyingType(Type type) { ReferenceCollectingClassVisitor( HelperClassPredicate helperClassPredicate, boolean isAdviceClass) { - super(Opcodes.ASM7); + super(AsmApi.VERSION); this.helperClassPredicate = helperClassPredicate; this.isAdviceClass = isAdviceClass; } @@ -249,7 +253,7 @@ public MethodVisitor visitMethod( MethodVisitor methodVisitor = super.visitMethod(access, name, descriptor, signature, exceptions); MethodVisitor methodNode = - new MethodNode(Opcodes.ASM9, access, name, descriptor, signature, exceptions) { + new MethodNode(AsmApi.VERSION, access, name, descriptor, signature, exceptions) { @Override public void visitEnd() { super.visitEnd(); @@ -309,7 +313,7 @@ private class AdviceReferenceMethodVisitor extends MethodVisitor { private int currentLineNumber = -1; public AdviceReferenceMethodVisitor(MethodVisitor methodVisitor) { - super(Opcodes.ASM7, methodVisitor); + super(AsmApi.VERSION, methodVisitor); } @Override @@ -523,7 +527,7 @@ private class VirtualFieldCollectingMethodVisitor extends MethodVisitor { private final EvictingQueue lastTwoClassConstants = EvictingQueue.create(2); VirtualFieldCollectingMethodVisitor(MethodVisitor methodVisitor) { - super(Opcodes.ASM7, methodVisitor); + super(AsmApi.VERSION, methodVisitor); } @Override diff --git a/muzzle/src/main/java/io/opentelemetry/javaagent/tooling/muzzle/ReferenceMatcher.java b/muzzle/src/main/java/io/opentelemetry/javaagent/tooling/muzzle/ReferenceMatcher.java index 9bd7b2435502..0726c92c4a05 100644 --- a/muzzle/src/main/java/io/opentelemetry/javaagent/tooling/muzzle/ReferenceMatcher.java +++ b/muzzle/src/main/java/io/opentelemetry/javaagent/tooling/muzzle/ReferenceMatcher.java @@ -294,13 +294,19 @@ private static MethodDescription.InDefinedShape findMethod( return null; } + TypeDescription superType = null; if (typeOnClasspath.getSuperClass() != null) { - MethodDescription.InDefinedShape methodOnSupertype = - findMethod(methodRef, typeOnClasspath.getSuperClass().asErasure()); + superType = typeOnClasspath.getSuperClass().asErasure(); + } else if (!"java.lang.Object".equals(typeOnClasspath.getName())) { + superType = TypeDescription.ForLoadedType.of(Object.class); + } + if (superType != null) { + MethodDescription.InDefinedShape methodOnSupertype = findMethod(methodRef, superType); if (methodOnSupertype != null) { return methodOnSupertype; } } + for (TypeDescription.Generic interfaceType : typeOnClasspath.getInterfaces()) { MethodDescription.InDefinedShape methodOnSupertype = findMethod(methodRef, interfaceType.asErasure()); diff --git a/muzzle/src/main/java/io/opentelemetry/javaagent/tooling/muzzle/generation/MuzzleCodeGenerator.java b/muzzle/src/main/java/io/opentelemetry/javaagent/tooling/muzzle/generation/MuzzleCodeGenerator.java index bc1345831300..0fdccccb9a67 100644 --- a/muzzle/src/main/java/io/opentelemetry/javaagent/tooling/muzzle/generation/MuzzleCodeGenerator.java +++ b/muzzle/src/main/java/io/opentelemetry/javaagent/tooling/muzzle/generation/MuzzleCodeGenerator.java @@ -10,6 +10,7 @@ import com.google.errorprone.annotations.CanIgnoreReturnValue; import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.internal.AsmApi; import io.opentelemetry.javaagent.tooling.muzzle.HelperResource; import io.opentelemetry.javaagent.tooling.muzzle.HelperResourceBuilderImpl; import io.opentelemetry.javaagent.tooling.muzzle.InstrumentationModuleMuzzle; @@ -98,7 +99,7 @@ private static class GenerateMuzzleMethodsAndFields extends ClassVisitor { private boolean generateVirtualFieldsMethod = true; public GenerateMuzzleMethodsAndFields(ClassVisitor classVisitor, URLClassLoader classLoader) { - super(Opcodes.ASM7, classVisitor); + super(AsmApi.VERSION, classVisitor); this.classLoader = classLoader; } diff --git a/muzzle/src/test/java/io/opentelemetry/javaagent/tooling/ByteArrayUrlTest.java b/muzzle/src/test/java/io/opentelemetry/javaagent/tooling/ByteArrayUrlTest.java new file mode 100644 index 000000000000..7b56a2c8a3ab --- /dev/null +++ b/muzzle/src/test/java/io/opentelemetry/javaagent/tooling/ByteArrayUrlTest.java @@ -0,0 +1,30 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.tooling; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +import java.net.URL; +import java.net.URLConnection; +import org.junit.jupiter.api.Test; + +public class ByteArrayUrlTest { + + @Test + public void testUrlCreation() throws Exception { + byte[] content = new byte[] {1, 2, 3, 4}; + + URL url = ByteArrayUrl.create("my.data$foo", content); + + assertThat(url).hasPath("my.data%24foo").hasProtocol("x-otel-binary"); + + URLConnection connection = url.openConnection(); + assertThat(connection.getContentLengthLong()).isEqualTo(4); + assertThat(connection.getContentLength()).isEqualTo(4); + + assertThat(connection.getInputStream()).hasBinaryContent(content); + } +} diff --git a/muzzle/src/test/java/io/opentelemetry/javaagent/tooling/muzzle/MuzzleWeakReferenceTestUtil.java b/muzzle/src/test/java/io/opentelemetry/javaagent/tooling/muzzle/MuzzleWeakReferenceTestUtil.java index d23ad66a6ec2..40695dccd65b 100644 --- a/muzzle/src/test/java/io/opentelemetry/javaagent/tooling/muzzle/MuzzleWeakReferenceTestUtil.java +++ b/muzzle/src/test/java/io/opentelemetry/javaagent/tooling/muzzle/MuzzleWeakReferenceTestUtil.java @@ -9,7 +9,9 @@ import java.lang.ref.WeakReference; import java.net.URL; import java.net.URLClassLoader; +import java.time.Duration; import java.util.Collections; +import java.util.concurrent.TimeoutException; import muzzle.TestClasses.MethodBodyAdvice; public class MuzzleWeakReferenceTestUtil { @@ -17,7 +19,8 @@ public class MuzzleWeakReferenceTestUtil { // 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 { + public static boolean classLoaderRefIsGarbageCollected() + throws InterruptedException, TimeoutException { ClassLoader loader = new URLClassLoader(new URL[0], null); WeakReference clRef = new WeakReference<>(loader); ReferenceCollector collector = new ReferenceCollector(className -> false); @@ -27,7 +30,7 @@ public static boolean classLoaderRefIsGarbageCollected() throws InterruptedExcep Collections.emptyList(), collector.getReferences(), className -> false); refMatcher.getMismatchedReferenceSources(loader); loader = null; - GcUtils.awaitGc(clRef); + GcUtils.awaitGc(clRef, Duration.ofSeconds(10)); return clRef.get() == null; } diff --git a/muzzle/src/test/java/io/opentelemetry/javaagent/tooling/muzzle/ReferenceCollectorTest.java b/muzzle/src/test/java/io/opentelemetry/javaagent/tooling/muzzle/ReferenceCollectorTest.java index 9ad2fcfa8a97..d9d49612c18a 100644 --- a/muzzle/src/test/java/io/opentelemetry/javaagent/tooling/muzzle/ReferenceCollectorTest.java +++ b/muzzle/src/test/java/io/opentelemetry/javaagent/tooling/muzzle/ReferenceCollectorTest.java @@ -8,6 +8,7 @@ import static io.opentelemetry.javaagent.tooling.muzzle.references.Flag.MinimumVisibilityFlag.PACKAGE_OR_HIGHER; import static io.opentelemetry.javaagent.tooling.muzzle.references.Flag.MinimumVisibilityFlag.PROTECTED_OR_HIGHER; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.Assertions.entry; import external.instrumentation.ExternalHelper; @@ -30,7 +31,7 @@ import muzzle.TestClasses.LdcAdvice; import muzzle.TestClasses.MethodBodyAdvice; import muzzle.TestClasses.Nested; -import org.assertj.core.api.Assertions; +import muzzle.other.OtherTestClasses; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; @@ -71,16 +72,15 @@ public void methodBodyCreatesReferences() { refB, "method", "(Ljava/lang/String;)Ljava/lang/String;", - PROTECTED_OR_HIGHER, + PACKAGE_OR_HIGHER, OwnershipFlag.NON_STATIC); - assertMethod( - refB, "methodWithPrimitives", "(Z)V", PROTECTED_OR_HIGHER, OwnershipFlag.NON_STATIC); - assertMethod(refB, "staticMethod", "()V", PROTECTED_OR_HIGHER, OwnershipFlag.STATIC); + assertMethod(refB, "methodWithPrimitives", "(Z)V", PACKAGE_OR_HIGHER, OwnershipFlag.NON_STATIC); + assertMethod(refB, "staticMethod", "()V", PACKAGE_OR_HIGHER, OwnershipFlag.STATIC); assertMethod( refB, "methodWithArrays", "([Ljava/lang/String;)[Ljava/lang/Object;", - PROTECTED_OR_HIGHER, + PACKAGE_OR_HIGHER, OwnershipFlag.NON_STATIC); // field refs @@ -93,7 +93,7 @@ public void methodBodyCreatesReferences() { @Test public void protectedRefTest() { ReferenceCollector collector = new ReferenceCollector(s -> false); - collector.collectReferencesFromAdvice(Nested.B2.class.getName()); + collector.collectReferencesFromAdvice(OtherTestClasses.Nested.B2.class.getName()); collector.prune(); Map references = collector.getReferences(); @@ -137,21 +137,21 @@ public void invokedynamicCreatesReferences() { references.get("muzzle.TestClasses$Nested$SomeImplementation"), "someMethod", "()V", - PROTECTED_OR_HIGHER, + PACKAGE_OR_HIGHER, OwnershipFlag.NON_STATIC); assertThat(references).containsKey("muzzle.TestClasses$Nested$B"); assertMethod( references.get("muzzle.TestClasses$Nested$B"), "staticMethod", "()V", - PROTECTED_OR_HIGHER, + PACKAGE_OR_HIGHER, OwnershipFlag.STATIC); assertThat(references).containsKey("muzzle.TestClasses$Nested$A"); assertMethod( references.get("muzzle.TestClasses$Nested$A"), "", "()V", - PROTECTED_OR_HIGHER, + PACKAGE_OR_HIGHER, OwnershipFlag.NON_STATIC); } @@ -369,7 +369,7 @@ public void shouldNotCollectVirtualFieldsForInvalidScenario( @SuppressWarnings("unused") String desc, String adviceClassName) { ReferenceCollector collector = new ReferenceCollector(s -> false); - Assertions.assertThatExceptionOfType(MuzzleCompilationException.class) + assertThatExceptionOfType(MuzzleCompilationException.class) .isThrownBy( () -> { collector.collectReferencesFromAdvice(adviceClassName); diff --git a/muzzle/src/test/java/muzzle/TestClasses.java b/muzzle/src/test/java/muzzle/TestClasses.java index 8b6b6dde089a..5bcee8518d05 100644 --- a/muzzle/src/test/java/muzzle/TestClasses.java +++ b/muzzle/src/test/java/muzzle/TestClasses.java @@ -109,6 +109,7 @@ public static boolean instanceofMethod(Object a) { } public static class InvokeDynamicAdvice { + @SuppressWarnings("UnnecessaryMethodReference") public static Nested.SomeInterface invokeDynamicMethod(Nested.SomeImplementation a) { Runnable staticMethod = Nested.B::staticMethod; Runnable constructorMethod = Nested.A::new; diff --git a/muzzle/src/test/java/muzzle/other/OtherTestClasses.java b/muzzle/src/test/java/muzzle/other/OtherTestClasses.java new file mode 100644 index 000000000000..7c26ebea664f --- /dev/null +++ b/muzzle/src/test/java/muzzle/other/OtherTestClasses.java @@ -0,0 +1,26 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package muzzle.other; + +import muzzle.TestClasses; + +// to testing protected methods we need to have a test classes in different packages +@SuppressWarnings("unused") +public class OtherTestClasses { + + @SuppressWarnings("ClassNamedLikeTypeParameter") + public static class Nested { + public static class B2 extends TestClasses.Nested.B { + public void stuff() { + super.protectedMethod(); + } + } + + private Nested() {} + } + + private OtherTestClasses() {} +} diff --git a/opentelemetry-api-shaded-for-instrumenting/build.gradle.kts b/opentelemetry-api-shaded-for-instrumenting/build.gradle.kts index 6fb5e5f32bb6..1f188a09a255 100644 --- a/opentelemetry-api-shaded-for-instrumenting/build.gradle.kts +++ b/opentelemetry-api-shaded-for-instrumenting/build.gradle.kts @@ -1,8 +1,7 @@ import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar plugins { - id("com.github.johnrengelman.shadow") - + id("com.gradleup.shadow") id("otel.java-conventions") } @@ -31,7 +30,34 @@ val v1_27Deps by configurations.creating { // exclude the bom added by dependencyManagement exclude("io.opentelemetry", "opentelemetry-bom") } - +val v1_31Deps by configurations.creating { + isCanBeResolved = true + isCanBeConsumed = false + // exclude the bom added by dependencyManagement + exclude("io.opentelemetry", "opentelemetry-bom") + exclude("io.opentelemetry", "opentelemetry-bom-alpha") +} +val v1_32Deps by configurations.creating { + isCanBeResolved = true + isCanBeConsumed = false + // exclude the bom added by dependencyManagement + exclude("io.opentelemetry", "opentelemetry-bom") + exclude("io.opentelemetry", "opentelemetry-bom-alpha") +} +val v1_37Deps by configurations.creating { + isCanBeResolved = true + isCanBeConsumed = false + // exclude the bom added by dependencyManagement + exclude("io.opentelemetry", "opentelemetry-bom") + exclude("io.opentelemetry", "opentelemetry-bom-alpha") +} +val v1_38Deps by configurations.creating { + isCanBeResolved = true + isCanBeConsumed = false + // exclude the bom added by dependencyManagement + exclude("io.opentelemetry", "opentelemetry-bom") + exclude("io.opentelemetry", "opentelemetry-bom-alpha") +} // configuration for publishing the shadowed artifact val v1_10 by configurations.creating { isCanBeConsumed = true @@ -45,6 +71,22 @@ val v1_27 by configurations.creating { isCanBeConsumed = true isCanBeResolved = false } +val v1_31 by configurations.creating { + isCanBeConsumed = true + isCanBeResolved = false +} +val v1_32 by configurations.creating { + isCanBeConsumed = true + isCanBeResolved = false +} +val v1_37 by configurations.creating { + isCanBeConsumed = true + isCanBeResolved = false +} +val v1_38 by configurations.creating { + isCanBeConsumed = true + isCanBeResolved = false +} dependencies { latestDeps("io.opentelemetry:opentelemetry-api") @@ -65,6 +107,42 @@ dependencies { strictly("1.27.0") } } + v1_31Deps("io.opentelemetry:$it") { + version { + strictly("1.31.0") + } + } + v1_32Deps("io.opentelemetry:$it") { + version { + strictly("1.32.0") + } + } + } + + listOf("opentelemetry-extension-incubator").forEach { + v1_31Deps("io.opentelemetry:$it") { + version { + strictly("1.31.0-alpha") + } + } + v1_32Deps("io.opentelemetry:$it") { + version { + strictly("1.32.0-alpha") + } + } + } + + listOf("opentelemetry-api-incubator").forEach { + v1_37Deps("io.opentelemetry:$it") { + version { + strictly("1.37.0-alpha") + } + } + v1_38Deps("io.opentelemetry:$it") { + version { + strictly("1.38.0-alpha") + } + } } } @@ -92,10 +170,30 @@ tasks { configurations = listOf(v1_27Deps) archiveClassifier.set("v1_27") } + val v1_31Shadow by registering(ShadowJar::class) { + configurations = listOf(v1_31Deps) + archiveClassifier.set("v1_31") + } + val v1_32Shadow by registering(ShadowJar::class) { + configurations = listOf(v1_32Deps) + archiveClassifier.set("v1_32") + } + val v1_37Shadow by registering(ShadowJar::class) { + configurations = listOf(v1_37Deps) + archiveClassifier.set("v1_37") + } + val v1_38Shadow by registering(ShadowJar::class) { + configurations = listOf(v1_38Deps) + archiveClassifier.set("v1_38") + } artifacts { add(v1_10.name, v1_10Shadow) add(v1_15.name, v1_15Shadow) add(v1_27.name, v1_27Shadow) + add(v1_31.name, v1_31Shadow) + add(v1_32.name, v1_32Shadow) + add(v1_37.name, v1_37Shadow) + add(v1_38.name, v1_38Shadow) } } diff --git a/opentelemetry-ext-annotations-shaded-for-instrumenting/build.gradle.kts b/opentelemetry-ext-annotations-shaded-for-instrumenting/build.gradle.kts index d0c92d3611ba..a95aca5f8e1b 100644 --- a/opentelemetry-ext-annotations-shaded-for-instrumenting/build.gradle.kts +++ b/opentelemetry-ext-annotations-shaded-for-instrumenting/build.gradle.kts @@ -1,6 +1,5 @@ plugins { - id("com.github.johnrengelman.shadow") - + id("com.gradleup.shadow") id("otel.java-conventions") } diff --git a/opentelemetry-instrumentation-annotations-shaded-for-instrumenting/build.gradle.kts b/opentelemetry-instrumentation-annotations-shaded-for-instrumenting/build.gradle.kts index 3dc29118754b..177ebb817e6d 100644 --- a/opentelemetry-instrumentation-annotations-shaded-for-instrumenting/build.gradle.kts +++ b/opentelemetry-instrumentation-annotations-shaded-for-instrumenting/build.gradle.kts @@ -1,6 +1,5 @@ plugins { - id("com.github.johnrengelman.shadow") - + id("com.gradleup.shadow") id("otel.java-conventions") } diff --git a/opentelemetry-instrumentation-api-shaded-for-instrumenting/build.gradle.kts b/opentelemetry-instrumentation-api-shaded-for-instrumenting/build.gradle.kts index 64450dad1ae3..5d44310162f7 100644 --- a/opentelemetry-instrumentation-api-shaded-for-instrumenting/build.gradle.kts +++ b/opentelemetry-instrumentation-api-shaded-for-instrumenting/build.gradle.kts @@ -1,6 +1,5 @@ plugins { - id("com.github.johnrengelman.shadow") - + id("com.gradleup.shadow") id("otel.java-conventions") } diff --git a/sdk-autoconfigure-support/build.gradle.kts b/sdk-autoconfigure-support/build.gradle.kts new file mode 100644 index 000000000000..7a2e5e185912 --- /dev/null +++ b/sdk-autoconfigure-support/build.gradle.kts @@ -0,0 +1,13 @@ +plugins { + id("otel.java-conventions") + id("otel.publish-conventions") +} + +group = "io.opentelemetry.instrumentation" + +dependencies { + api("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure") + + compileOnly("com.google.code.findbugs:annotations") + testCompileOnly("com.google.code.findbugs:annotations") +} diff --git a/sdk-autoconfigure-support/src/main/java/io/opentelemetry/instrumentation/resources/ResourceProviderPropertiesCustomizer.java b/sdk-autoconfigure-support/src/main/java/io/opentelemetry/instrumentation/resources/ResourceProviderPropertiesCustomizer.java new file mode 100644 index 000000000000..416e6e16c27c --- /dev/null +++ b/sdk-autoconfigure-support/src/main/java/io/opentelemetry/instrumentation/resources/ResourceProviderPropertiesCustomizer.java @@ -0,0 +1,100 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.resources; + +import io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizer; +import io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizerProvider; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +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 javax.annotation.Nullable; + +public class ResourceProviderPropertiesCustomizer implements AutoConfigurationCustomizerProvider { + + private static final Map DISABLED_BY_DEFAULT_RESOURCE_PROVIDERS = new HashMap<>(); + + static { + DISABLED_BY_DEFAULT_RESOURCE_PROVIDERS.put( + "io.opentelemetry.contrib.aws.resource.BeanstalkResourceProvider", "aws"); + DISABLED_BY_DEFAULT_RESOURCE_PROVIDERS.put( + "io.opentelemetry.contrib.aws.resource.Ec2ResourceProvider", "aws"); + DISABLED_BY_DEFAULT_RESOURCE_PROVIDERS.put( + "io.opentelemetry.contrib.aws.resource.EcsResourceProvider", "aws"); + DISABLED_BY_DEFAULT_RESOURCE_PROVIDERS.put( + "io.opentelemetry.contrib.aws.resource.EksResourceProvider", "aws"); + DISABLED_BY_DEFAULT_RESOURCE_PROVIDERS.put( + "io.opentelemetry.contrib.aws.resource.LambdaResourceProvider", "aws"); + DISABLED_BY_DEFAULT_RESOURCE_PROVIDERS.put( + "io.opentelemetry.contrib.gcp.resource.GCPResourceProvider", "gcp"); + // for testing + DISABLED_BY_DEFAULT_RESOURCE_PROVIDERS.put( + "io.opentelemetry.instrumentation.resources.ResourceProviderPropertiesCustomizerTest$Provider", + "test"); + } + + static final String DISABLED_KEY = "otel.java.disabled.resource.providers"; + static final String ENABLED_KEY = "otel.java.enabled.resource.providers"; + + @Override + public void customize(AutoConfigurationCustomizer autoConfigurationCustomizer) { + autoConfigurationCustomizer.addPropertiesCustomizer(this::customize); + } + + // VisibleForTesting + Map customize(ConfigProperties config) { + Set enabledProviders = new HashSet<>(config.getList(ENABLED_KEY)); + + List enabled = new ArrayList<>(); + List disabled = new ArrayList<>(); + + for (Map.Entry providerEntry : + DISABLED_BY_DEFAULT_RESOURCE_PROVIDERS.entrySet()) { + String providerName = providerEntry.getKey(); + String providerGroup = providerEntry.getValue(); + Boolean explictEnabled = + config.getBoolean(String.format("otel.resource.providers.%s.enabled", providerGroup)); + + if (isEnabled(providerName, enabledProviders, explictEnabled)) { + enabled.add(providerName); + } else { + disabled.add(providerName); + } + } + + if (!enabledProviders.isEmpty()) { + // users has requested specific providers to be enabled only + enabled.addAll(enabledProviders); + return Collections.singletonMap(ENABLED_KEY, String.join(",", enabled)); + } + + if (disabled.isEmpty()) { + // all providers that are disabled by default are enabled, no need to set any properties + return Collections.emptyMap(); + } + + disabled.addAll(config.getList(DISABLED_KEY)); + return Collections.singletonMap(DISABLED_KEY, String.join(",", disabled)); + } + + private static boolean isEnabled( + String className, Set enabledProviders, @Nullable Boolean explicitEnabled) { + if (explicitEnabled != null) { + return explicitEnabled; + } + return !enabledProviders.isEmpty() && enabledProviders.contains(className); + } + + @Override + public int order() { + // make sure it runs AFTER all the user-provided customizers + return Integer.MAX_VALUE; + } +} diff --git a/sdk-autoconfigure-support/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizerProvider b/sdk-autoconfigure-support/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizerProvider new file mode 100644 index 000000000000..3f8716ab23aa --- /dev/null +++ b/sdk-autoconfigure-support/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizerProvider @@ -0,0 +1 @@ +io.opentelemetry.instrumentation.resources.ResourceProviderPropertiesCustomizer diff --git a/sdk-autoconfigure-support/src/test/java/io/opentelemetry/instrumentation/resources/ResourceProviderPropertiesCustomizerTest.java b/sdk-autoconfigure-support/src/test/java/io/opentelemetry/instrumentation/resources/ResourceProviderPropertiesCustomizerTest.java new file mode 100644 index 000000000000..18928d915917 --- /dev/null +++ b/sdk-autoconfigure-support/src/test/java/io/opentelemetry/instrumentation/resources/ResourceProviderPropertiesCustomizerTest.java @@ -0,0 +1,140 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.resources; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdk; +import io.opentelemetry.sdk.autoconfigure.SdkAutoconfigureAccess; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import io.opentelemetry.sdk.autoconfigure.spi.ResourceProvider; +import io.opentelemetry.sdk.resources.Resource; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.stream.Stream; +import javax.annotation.Nullable; +import org.assertj.core.util.Strings; +import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.TestFactory; + +public class ResourceProviderPropertiesCustomizerTest { + + public static final class Provider implements ResourceProvider { + @Override + public Resource createResource(ConfigProperties config) { + return Resource.create(Attributes.of(AttributeKey.stringKey("key"), "value")); + } + } + + private static class EnabledTestCase { + private final String name; + private final boolean result; + private final Set enabledProviders; + private final Set disabledProviders; + private final Boolean explicitEnabled; + + private EnabledTestCase( + String name, + boolean result, + Set enabledProviders, + Set disabledProviders, + @Nullable Boolean explicitEnabled) { + this.name = name; + this.result = result; + this.enabledProviders = enabledProviders; + this.disabledProviders = disabledProviders; + this.explicitEnabled = explicitEnabled; + } + } + + @SuppressWarnings("BooleanParameter") + @TestFactory + Stream enabledTestCases() { + String className = + "io.opentelemetry.instrumentation.resources.ResourceProviderPropertiesCustomizerTest$Provider"; + return Stream.of( + new EnabledTestCase( + "explicitEnabled", true, Collections.emptySet(), Collections.emptySet(), true), + new EnabledTestCase( + "explicitEnabledFalse", + false, + Collections.emptySet(), + Collections.emptySet(), + false), + new EnabledTestCase( + "enabledProvidersEmpty", + false, + Collections.emptySet(), + Collections.emptySet(), + null), + new EnabledTestCase( + "enabledProvidersContains", + true, + Collections.singleton(className), + Collections.emptySet(), + null), + new EnabledTestCase( + "enabledProvidersNotContains", + false, + Collections.singleton("otherClassName"), + Collections.emptySet(), + null), + new EnabledTestCase( + "disabledProvidersContains", + false, + Collections.emptySet(), + Collections.singleton(className), + null), + new EnabledTestCase( + "disabledProvidersNotContains", + false, + Collections.emptySet(), + Collections.singleton("otherClassName"), + null), + new EnabledTestCase( + "defaultEnabledFalse", false, Collections.emptySet(), Collections.emptySet(), null)) + .map( + tc -> + DynamicTest.dynamicTest( + tc.name, + () -> { + Map props = new HashMap<>(); + props.put( + ResourceProviderPropertiesCustomizer.ENABLED_KEY, + Strings.join(tc.enabledProviders).with(",")); + props.put( + ResourceProviderPropertiesCustomizer.DISABLED_KEY, + Strings.join(tc.disabledProviders).with(",")); + + if (tc.explicitEnabled != null) { + props.put( + "otel.resource.providers.test.enabled", + Boolean.toString(tc.explicitEnabled)); + } + + props.put("otel.traces.exporter", "none"); + props.put("otel.metrics.exporter", "none"); + props.put("otel.logs.exporter", "none"); + + Attributes attributes = + SdkAutoconfigureAccess.getResourceAttributes( + AutoConfiguredOpenTelemetrySdk.builder() + .addPropertiesSupplier(() -> props) + .build()); + + if (tc.result) { + assertThat(attributes.get(AttributeKey.stringKey("key"))) + .isEqualTo("value"); + } else { + assertThat(attributes.get(AttributeKey.stringKey("key"))).isNull(); + } + })); + } +} diff --git a/sdk-autoconfigure-support/src/test/java/io/opentelemetry/sdk/autoconfigure/SdkAutoconfigureAccess.java b/sdk-autoconfigure-support/src/test/java/io/opentelemetry/sdk/autoconfigure/SdkAutoconfigureAccess.java new file mode 100644 index 000000000000..477f5bcbdee4 --- /dev/null +++ b/sdk-autoconfigure-support/src/test/java/io/opentelemetry/sdk/autoconfigure/SdkAutoconfigureAccess.java @@ -0,0 +1,16 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.autoconfigure; + +import io.opentelemetry.api.common.Attributes; + +public final class SdkAutoconfigureAccess { + public static Attributes getResourceAttributes(AutoConfiguredOpenTelemetrySdk sdk) { + return sdk.getResource().getAttributes(); + } + + private SdkAutoconfigureAccess() {} +} diff --git a/sdk-autoconfigure-support/src/test/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.ResourceProvider b/sdk-autoconfigure-support/src/test/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.ResourceProvider new file mode 100644 index 000000000000..243cb5615b57 --- /dev/null +++ b/sdk-autoconfigure-support/src/test/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.ResourceProvider @@ -0,0 +1 @@ +io.opentelemetry.instrumentation.resources.ResourceProviderPropertiesCustomizerTest$Provider diff --git a/settings.gradle.kts b/settings.gradle.kts index e376d02ed66e..e174c99547b5 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,22 +1,26 @@ pluginManagement { plugins { - id("com.bmuschko.docker-remote-api") version "8.1.0" - id("com.github.ben-manes.versions") version "0.47.0" - id("com.github.jk1.dependency-license-report") version "2.4" - id("com.google.cloud.tools.jib") version "3.3.2" - id("com.gradle.plugin-publish") version "1.2.0" - id("io.github.gradle-nexus.publish-plugin") version "1.3.0" - id("org.jetbrains.kotlin.jvm") version "1.8.22" - id("org.xbib.gradle.plugin.jflex") version "3.0.0" + id("com.github.jk1.dependency-license-report") version "2.9" + id("com.google.cloud.tools.jib") version "3.4.3" + id("com.gradle.plugin-publish") version "1.2.2" + id("io.github.gradle-nexus.publish-plugin") version "2.0.0" + id("org.jetbrains.kotlin.jvm") version "2.0.20" + id("org.xbib.gradle.plugin.jflex") version "3.0.2" id("org.unbroken-dome.xjc") version "2.0.0" - id("org.graalvm.buildtools.native") version "0.9.23" + id("org.graalvm.buildtools.native") version "0.10.3" } } plugins { - id("com.gradle.enterprise") version "3.13.4" - id("com.gradle.common-custom-user-data-gradle-plugin") version "1.11" - id("org.gradle.toolchains.foojay-resolver-convention") version "0.5.0" + id("com.gradle.develocity") version "3.18.1" + id("com.gradle.common-custom-user-data-gradle-plugin") version "2.0.2" + id("org.gradle.toolchains.foojay-resolver-convention") version "0.8.0" + // this can't live in pluginManagement currently due to + // https://github.com/bmuschko/gradle-docker-plugin/issues/1123 + // in particular, these commands are failing (reproducible locally): + // ./gradlew :smoke-tests:images:servlet:buildLinuxTestImages pushMatrix -PsmokeTestServer=jetty + // ./gradlew :smoke-tests:images:servlet:buildWindowsTestImages pushMatrix -PsmokeTestServer=jetty + id("com.bmuschko.docker-remote-api") version "9.4.0" apply false } dependencyResolutionManagement { @@ -24,6 +28,23 @@ dependencyResolutionManagement { mavenCentral() mavenLocal() } + + versionCatalogs { + fun addSpringBootCatalog(name: String, minVersion: String, maxVersion: String) { + val latestDepTest = gradle.startParameter.projectProperties["testLatestDeps"] == "true" + create(name) { + val version = + gradle.startParameter.projectProperties["${name}Version"] + ?: (if (latestDepTest) maxVersion else minVersion) + plugin("versions", "org.springframework.boot").version(version) + } + } + // r2dbc is not compatible with earlier versions + addSpringBootCatalog("springBoot2", "2.6.15", "2.+") + // spring boot 3.0 is not compatible with graalvm native image + addSpringBootCatalog("springBoot31", "3.1.0", "3.+") + addSpringBootCatalog("springBoot32", "3.2.0", "3.+") + } } val gradleEnterpriseServer = "https://ge.opentelemetry.io" @@ -34,30 +55,32 @@ val geAccessKey = System.getenv("GRADLE_ENTERPRISE_ACCESS_KEY") ?: "" val useScansGradleCom = isCI && geAccessKey.isEmpty() if (useScansGradleCom) { - gradleEnterprise { + develocity { buildScan { - termsOfServiceUrl = "https://gradle.com/terms-of-service" - termsOfServiceAgree = "yes" - isUploadInBackground = !isCI - publishAlways() + termsOfUseUrl = "https://gradle.com/help/legal-terms-of-use" + termsOfUseAgree = "yes" + uploadInBackground = !isCI capture { - isTaskInputFiles = true + fileFingerprints = true + } + + buildScanPublished { + File("build-scan.txt").printWriter().use { writer -> + writer.println(buildScanUri) + } } } } } else { - gradleEnterprise { + develocity { server = gradleEnterpriseServer buildScan { - isUploadInBackground = !isCI - - this as com.gradle.enterprise.gradleplugin.internal.extension.BuildScanExtensionWithHiddenFeatures - publishIfAuthenticated() - publishAlways() + uploadInBackground = !isCI + publishing.onlyIf { it.isAuthenticated } capture { - isTaskInputFiles = true + fileFingerprints = true } gradle.startParameter.projectProperties["testJavaVersion"]?.let { tag(it) } @@ -65,19 +88,18 @@ if (useScansGradleCom) { gradle.startParameter.projectProperties["smokeTestSuite"]?.let { value("Smoke test suite", it) } + + buildScanPublished { + File("build-scan.txt").printWriter().use { writer -> + writer.println(buildScanUri) + } + } } } -} -val geCacheUsername = System.getenv("GE_CACHE_USERNAME") ?: "" -val geCachePassword = System.getenv("GE_CACHE_PASSWORD") ?: "" -buildCache { - remote { - url = uri("$gradleEnterpriseServer/cache/") - isPush = isCI && geCacheUsername.isNotEmpty() - credentials { - username = geCacheUsername - password = geCachePassword + buildCache { + remote(develocity.buildCache) { + isPush = isCI && geAccessKey.isNotEmpty() } } } @@ -99,14 +121,16 @@ include(":javaagent-bootstrap") include(":javaagent-extension-api") include(":javaagent-tooling") include(":javaagent-tooling:javaagent-tooling-java9") +include(":javaagent-tooling:jdk18-testing") include(":javaagent-internal-logging-application") include(":javaagent-internal-logging-simple") include(":javaagent") +include(":sdk-autoconfigure-support") include(":bom") include(":bom-alpha") include(":instrumentation-api") -include(":instrumentation-api-semconv") +include(":instrumentation-api-incubator") include(":instrumentation-annotations") include(":instrumentation-annotations-support") include(":instrumentation-annotations-support-testing") @@ -122,427 +146,491 @@ include(":testing-common:library-for-integration-tests") // smoke tests include(":smoke-tests") +include(":smoke-tests:images:early-jdk8") include(":smoke-tests:images:fake-backend") include(":smoke-tests:images:grpc") include(":smoke-tests:images:play") include(":smoke-tests:images:quarkus") include(":smoke-tests:images:security-manager") include(":smoke-tests:images:servlet") -hideFromDependabot(":smoke-tests:images:servlet:servlet-3.0") -hideFromDependabot(":smoke-tests:images:servlet:servlet-5.0") -hideFromDependabot(":smoke-tests:images:spring-boot") +include(":smoke-tests:images:servlet:servlet-3.0") +include(":smoke-tests:images:servlet:servlet-5.0") +include(":smoke-tests:images:spring-boot") -hideFromDependabot("instrumentation:akka:akka-actor-2.3:javaagent") -hideFromDependabot(":instrumentation:akka:akka-actor-fork-join-2.5:javaagent") -hideFromDependabot(":instrumentation:akka:akka-http-10.0:javaagent") -hideFromDependabot(":instrumentation:apache-dbcp-2.0:javaagent") -hideFromDependabot(":instrumentation:apache-dbcp-2.0:library") -hideFromDependabot(":instrumentation:apache-dbcp-2.0:testing") -hideFromDependabot(":instrumentation:apache-dubbo-2.7:javaagent") -hideFromDependabot(":instrumentation:apache-dubbo-2.7:library-autoconfigure") -hideFromDependabot(":instrumentation:apache-dubbo-2.7:testing") -hideFromDependabot(":instrumentation:apache-httpasyncclient-4.1:javaagent") -hideFromDependabot(":instrumentation:apache-httpclient:apache-httpclient-2.0:javaagent") -hideFromDependabot(":instrumentation:apache-httpclient:apache-httpclient-4.0:javaagent") -hideFromDependabot(":instrumentation:apache-httpclient:apache-httpclient-4.3:library") -hideFromDependabot(":instrumentation:apache-httpclient:apache-httpclient-4.3:testing") -hideFromDependabot(":instrumentation:apache-httpclient:apache-httpclient-5.0:javaagent") -hideFromDependabot(":instrumentation:armeria-1.3:javaagent") -hideFromDependabot(":instrumentation:armeria-1.3:library") -hideFromDependabot(":instrumentation:armeria-1.3:testing") -hideFromDependabot(":instrumentation:async-http-client:async-http-client-1.9:javaagent") -hideFromDependabot(":instrumentation:async-http-client:async-http-client-2.0:javaagent") -hideFromDependabot(":instrumentation:aws-lambda:aws-lambda-core-1.0:javaagent") -hideFromDependabot(":instrumentation:aws-lambda:aws-lambda-core-1.0:library") -hideFromDependabot(":instrumentation:aws-lambda:aws-lambda-core-1.0:testing") -hideFromDependabot(":instrumentation:aws-lambda:aws-lambda-events-2.2:javaagent") -hideFromDependabot(":instrumentation:aws-lambda:aws-lambda-events-2.2:library") -hideFromDependabot(":instrumentation:aws-lambda:aws-lambda-events-2.2:testing") -hideFromDependabot(":instrumentation:aws-sdk:aws-sdk-1.11:javaagent") -hideFromDependabot(":instrumentation:aws-sdk:aws-sdk-1.11:library") -hideFromDependabot(":instrumentation:aws-sdk:aws-sdk-1.11:library-autoconfigure") -hideFromDependabot(":instrumentation:aws-sdk:aws-sdk-1.11:testing") -hideFromDependabot(":instrumentation:aws-sdk:aws-sdk-2.2:javaagent") -hideFromDependabot(":instrumentation:aws-sdk:aws-sdk-2.2:library") -hideFromDependabot(":instrumentation:aws-sdk:aws-sdk-2.2:library-autoconfigure") -hideFromDependabot(":instrumentation:aws-sdk:aws-sdk-2.2:testing") -hideFromDependabot(":instrumentation:azure-core:azure-core-1.14:javaagent") -hideFromDependabot(":instrumentation:azure-core:azure-core-1.14:library-instrumentation-shaded") -hideFromDependabot(":instrumentation:azure-core:azure-core-1.19:javaagent") -hideFromDependabot(":instrumentation:azure-core:azure-core-1.19:library-instrumentation-shaded") -hideFromDependabot(":instrumentation:azure-core:azure-core-1.36:javaagent") -hideFromDependabot(":instrumentation:azure-core:azure-core-1.36:library-instrumentation-shaded") -hideFromDependabot(":instrumentation:camel-2.20:javaagent") -hideFromDependabot(":instrumentation:camel-2.20:javaagent-unit-tests") -hideFromDependabot(":instrumentation:cassandra:cassandra-3.0:javaagent") -hideFromDependabot(":instrumentation:cassandra:cassandra-4.0:javaagent") -hideFromDependabot(":instrumentation:cassandra:cassandra-4.4:javaagent") -hideFromDependabot(":instrumentation:cassandra:cassandra-4.4:library") -hideFromDependabot(":instrumentation:cassandra:cassandra-4.4:testing") -hideFromDependabot(":instrumentation:cassandra:cassandra-4-common:testing") -hideFromDependabot(":instrumentation:cdi-testing") -hideFromDependabot(":instrumentation:graphql-java-12.0:javaagent") -hideFromDependabot(":instrumentation:graphql-java-12.0:library") -hideFromDependabot(":instrumentation:graphql-java-12.0:testing") -hideFromDependabot(":instrumentation:internal:internal-application-logger:bootstrap") -hideFromDependabot(":instrumentation:internal:internal-application-logger:javaagent") -hideFromDependabot(":instrumentation:internal:internal-class-loader:javaagent") -hideFromDependabot(":instrumentation:internal:internal-class-loader:javaagent-integration-tests") -hideFromDependabot(":instrumentation:internal:internal-eclipse-osgi-3.6:javaagent") -hideFromDependabot(":instrumentation:internal:internal-lambda:javaagent") -hideFromDependabot(":instrumentation:internal:internal-lambda-java9:javaagent") -hideFromDependabot(":instrumentation:internal:internal-reflection:javaagent") -hideFromDependabot(":instrumentation:internal:internal-reflection:javaagent-integration-tests") -hideFromDependabot(":instrumentation:internal:internal-url-class-loader:javaagent") -hideFromDependabot(":instrumentation:internal:internal-url-class-loader:javaagent-integration-tests") -hideFromDependabot(":instrumentation:c3p0-0.9:javaagent") -hideFromDependabot(":instrumentation:c3p0-0.9:library") -hideFromDependabot(":instrumentation:c3p0-0.9:testing") -hideFromDependabot(":instrumentation:couchbase:couchbase-2.0:javaagent") -hideFromDependabot(":instrumentation:couchbase:couchbase-2.6:javaagent") -hideFromDependabot(":instrumentation:couchbase:couchbase-2-common:javaagent") -hideFromDependabot(":instrumentation:couchbase:couchbase-2-common:javaagent-unit-tests") -hideFromDependabot(":instrumentation:couchbase:couchbase-3.1:javaagent") -hideFromDependabot(":instrumentation:couchbase:couchbase-3.1:tracing-opentelemetry-shaded") -hideFromDependabot(":instrumentation:couchbase:couchbase-3.1.6:javaagent") -hideFromDependabot(":instrumentation:couchbase:couchbase-3.1.6:tracing-opentelemetry-shaded") -hideFromDependabot(":instrumentation:couchbase:couchbase-3.2:javaagent") -hideFromDependabot(":instrumentation:couchbase:couchbase-3.2:tracing-opentelemetry-shaded") -hideFromDependabot(":instrumentation:couchbase:couchbase-common:testing") -hideFromDependabot(":instrumentation:dropwizard:dropwizard-metrics-4.0:javaagent") -hideFromDependabot(":instrumentation:dropwizard:dropwizard-views-0.7:javaagent") -hideFromDependabot(":instrumentation:dropwizard:dropwizard-testing") -hideFromDependabot(":instrumentation:elasticsearch:elasticsearch-rest-common:javaagent") -hideFromDependabot(":instrumentation:opensearch:opensearch-rest-common:javaagent") -hideFromDependabot(":instrumentation:elasticsearch:elasticsearch-rest-5.0:javaagent") -hideFromDependabot(":instrumentation:elasticsearch:elasticsearch-rest-6.4:javaagent") -hideFromDependabot(":instrumentation:elasticsearch:elasticsearch-rest-7.0:javaagent") -hideFromDependabot(":instrumentation:opensearch:opensearch-rest-1.0:javaagent") -hideFromDependabot(":instrumentation:elasticsearch:elasticsearch-transport-common:javaagent") -hideFromDependabot(":instrumentation:elasticsearch:elasticsearch-transport-common:testing") -hideFromDependabot(":instrumentation:elasticsearch:elasticsearch-transport-5.0:javaagent") -hideFromDependabot(":instrumentation:elasticsearch:elasticsearch-transport-5.3:javaagent") -hideFromDependabot(":instrumentation:elasticsearch:elasticsearch-transport-6.0:javaagent") -hideFromDependabot(":instrumentation:executors:bootstrap") -hideFromDependabot(":instrumentation:executors:javaagent") -hideFromDependabot(":instrumentation:executors:testing") -hideFromDependabot(":instrumentation:external-annotations:javaagent") -hideFromDependabot(":instrumentation:external-annotations:javaagent-unit-tests") -hideFromDependabot(":instrumentation:finatra-2.9:javaagent") -hideFromDependabot(":instrumentation:geode-1.4:javaagent") -hideFromDependabot(":instrumentation:google-http-client-1.19:javaagent") -hideFromDependabot(":instrumentation:grails-3.0:javaagent") -hideFromDependabot(":instrumentation:grizzly-2.3:javaagent") -hideFromDependabot(":instrumentation:grpc-1.6:javaagent") -hideFromDependabot(":instrumentation:grpc-1.6:library") -hideFromDependabot(":instrumentation:grpc-1.6:testing") -hideFromDependabot(":instrumentation:guava-10.0:javaagent") -hideFromDependabot(":instrumentation:guava-10.0:library") -hideFromDependabot(":instrumentation:gwt-2.0:javaagent") -hideFromDependabot(":instrumentation:hibernate:hibernate-3.3:javaagent") -hideFromDependabot(":instrumentation:hibernate:hibernate-4.0:javaagent") -hideFromDependabot(":instrumentation:hibernate:hibernate-6.0:javaagent") -hideFromDependabot(":instrumentation:hibernate:hibernate-6.0:spring-testing") -hideFromDependabot(":instrumentation:hibernate:hibernate-common:javaagent") -hideFromDependabot(":instrumentation:hibernate:hibernate-procedure-call-4.3:javaagent") -hideFromDependabot(":instrumentation:hikaricp-3.0:javaagent") -hideFromDependabot(":instrumentation:hikaricp-3.0:library") -hideFromDependabot(":instrumentation:hikaricp-3.0:testing") -hideFromDependabot(":instrumentation:http-url-connection:javaagent") -hideFromDependabot(":instrumentation:hystrix-1.4:javaagent") -hideFromDependabot(":instrumentation:java-http-client:javaagent") -hideFromDependabot(":instrumentation:java-http-client:library") -hideFromDependabot(":instrumentation:java-http-client:testing") -hideFromDependabot(":instrumentation:java-util-logging:javaagent") -hideFromDependabot(":instrumentation:java-util-logging:shaded-stub-for-instrumenting") -hideFromDependabot(":instrumentation:jaxrs:jaxrs-common:bootstrap") -hideFromDependabot(":instrumentation:jaxrs:jaxrs-common:javaagent") -hideFromDependabot(":instrumentation:jaxrs:jaxrs-common:testing") -hideFromDependabot(":instrumentation:jaxrs:jaxrs-1.0:javaagent") -hideFromDependabot(":instrumentation:jaxrs:jaxrs-2.0:jaxrs-2.0-annotations:javaagent") -hideFromDependabot(":instrumentation:jaxrs:jaxrs-2.0:jaxrs-2.0-arquillian-testing") -hideFromDependabot(":instrumentation:jaxrs:jaxrs-2.0:jaxrs-2.0-common:javaagent") -hideFromDependabot(":instrumentation:jaxrs:jaxrs-2.0:jaxrs-2.0-cxf-3.2:javaagent") -hideFromDependabot(":instrumentation:jaxrs:jaxrs-2.0:jaxrs-2.0-jersey-2.0:javaagent") -hideFromDependabot(":instrumentation:jaxrs:jaxrs-2.0:jaxrs-2.0-payara-testing") -hideFromDependabot(":instrumentation:jaxrs:jaxrs-2.0:jaxrs-2.0-resteasy-3.0:javaagent") -hideFromDependabot(":instrumentation:jaxrs:jaxrs-2.0:jaxrs-2.0-resteasy-3.1:javaagent") -hideFromDependabot(":instrumentation:jaxrs:jaxrs-2.0:jaxrs-2.0-resteasy-common:javaagent") -hideFromDependabot(":instrumentation:jaxrs:jaxrs-2.0:jaxrs-2.0-common:testing") -hideFromDependabot(":instrumentation:jaxrs:jaxrs-2.0:jaxrs-2.0-tomee-testing") -hideFromDependabot(":instrumentation:jaxrs:jaxrs-2.0:jaxrs-2.0-wildfly-testing") -hideFromDependabot(":instrumentation:jaxrs:jaxrs-3.0:jaxrs-3.0-annotations:javaagent") -hideFromDependabot(":instrumentation:jaxrs:jaxrs-3.0:jaxrs-3.0-common:javaagent") -hideFromDependabot(":instrumentation:jaxrs:jaxrs-3.0:jaxrs-3.0-common:testing") -hideFromDependabot(":instrumentation:jaxrs:jaxrs-3.0:jaxrs-3.0-jersey-3.0:javaagent") -hideFromDependabot(":instrumentation:jaxrs:jaxrs-3.0:jaxrs-3.0-resteasy-6.0:javaagent") -hideFromDependabot(":instrumentation:jaxrs-client:jaxrs-client-1.1:javaagent") -hideFromDependabot(":instrumentation:jaxrs-client:jaxrs-client-2.0-testing") -hideFromDependabot(":instrumentation:jaxws:jaxws-2.0:javaagent") -hideFromDependabot(":instrumentation:jaxws:jaxws-2.0-arquillian-testing") -hideFromDependabot(":instrumentation:jaxws:jaxws-2.0-axis2-1.6:javaagent") -hideFromDependabot(":instrumentation:jaxws:jaxws-2.0-cxf-3.0:javaagent") -hideFromDependabot(":instrumentation:jaxws:jaxws-2.0-cxf-3.0:javaagent-unit-tests") -hideFromDependabot(":instrumentation:jaxws:jaxws-2.0-metro-2.2:javaagent") -hideFromDependabot(":instrumentation:jaxws:jaxws-2.0-common-testing") -hideFromDependabot(":instrumentation:jaxws:jaxws-2.0-tomee-testing") -hideFromDependabot(":instrumentation:jaxws:jaxws-2.0-wildfly-testing") -hideFromDependabot(":instrumentation:jaxws:jaxws-common:javaagent") -hideFromDependabot(":instrumentation:jaxws:jaxws-jws-api-1.1:javaagent") -hideFromDependabot(":instrumentation:jboss-logmanager:jboss-logmanager-appender-1.1:javaagent") -hideFromDependabot(":instrumentation:jboss-logmanager:jboss-logmanager-mdc-1.1:javaagent") -hideFromDependabot(":instrumentation:jdbc:bootstrap") -hideFromDependabot(":instrumentation:jdbc:javaagent") -hideFromDependabot(":instrumentation:jdbc:library") -hideFromDependabot(":instrumentation:jdbc:testing") -hideFromDependabot(":instrumentation:jedis:jedis-1.4:javaagent") -hideFromDependabot(":instrumentation:jedis:jedis-3.0:javaagent") -hideFromDependabot(":instrumentation:jedis:jedis-4.0:javaagent") -hideFromDependabot(":instrumentation:jedis:jedis-common:javaagent") -hideFromDependabot(":instrumentation:jetty:jetty-8.0:javaagent") -hideFromDependabot(":instrumentation:jetty:jetty-11.0:javaagent") -hideFromDependabot(":instrumentation:jetty:jetty-common:javaagent") -hideFromDependabot(":instrumentation:jetty-httpclient:jetty-httpclient-9.2:javaagent") -hideFromDependabot(":instrumentation:jetty-httpclient:jetty-httpclient-9.2:library") -hideFromDependabot(":instrumentation:jetty-httpclient:jetty-httpclient-9.2:testing") -hideFromDependabot(":instrumentation:jms:jms-1.1:javaagent") -hideFromDependabot(":instrumentation:jms:jms-3.0:javaagent") -hideFromDependabot(":instrumentation:jms:jms-common:javaagent") -hideFromDependabot(":instrumentation:jms:jms-common:javaagent-unit-tests") -hideFromDependabot(":instrumentation:jmx-metrics:javaagent") -hideFromDependabot(":instrumentation:jmx-metrics:library") -hideFromDependabot(":instrumentation:jodd-http-4.2:javaagent") -hideFromDependabot(":instrumentation:jsf:jsf-javax-common:javaagent") -hideFromDependabot(":instrumentation:jsf:jsf-javax-common:testing") -hideFromDependabot(":instrumentation:jsf:jsf-jakarta-common:javaagent") -hideFromDependabot(":instrumentation:jsf:jsf-jakarta-common:testing") -hideFromDependabot(":instrumentation:jsf:jsf-mojarra-1.2:javaagent") -hideFromDependabot(":instrumentation:jsf:jsf-mojarra-3.0:javaagent") -hideFromDependabot(":instrumentation:jsf:jsf-myfaces-1.2:javaagent") -hideFromDependabot(":instrumentation:jsf:jsf-myfaces-3.0:javaagent") -hideFromDependabot(":instrumentation:jsp-2.3:javaagent") -hideFromDependabot(":instrumentation:kafka:kafka-clients:kafka-clients-0.11:bootstrap") -hideFromDependabot(":instrumentation:kafka:kafka-clients:kafka-clients-0.11:javaagent") -hideFromDependabot(":instrumentation:kafka:kafka-clients:kafka-clients-0.11:testing") -hideFromDependabot(":instrumentation:kafka:kafka-clients:kafka-clients-2.6:library") -hideFromDependabot(":instrumentation:kafka:kafka-clients:kafka-clients-common:library") -hideFromDependabot(":instrumentation:kafka:kafka-streams-0.11:javaagent") -hideFromDependabot(":instrumentation:kotlinx-coroutines:javaagent") -hideFromDependabot(":instrumentation:ktor:ktor-1.0:library") -hideFromDependabot(":instrumentation:ktor:ktor-2.0:library") -hideFromDependabot(":instrumentation:ktor:ktor-common:library") -hideFromDependabot(":instrumentation:kubernetes-client-7.0:javaagent") -hideFromDependabot(":instrumentation:kubernetes-client-7.0:javaagent-unit-tests") -hideFromDependabot(":instrumentation:lettuce:lettuce-common:library") -hideFromDependabot(":instrumentation:lettuce:lettuce-4.0:javaagent") -hideFromDependabot(":instrumentation:lettuce:lettuce-5.0:javaagent") -hideFromDependabot(":instrumentation:lettuce:lettuce-5.1:javaagent") -hideFromDependabot(":instrumentation:lettuce:lettuce-5.1:library") -hideFromDependabot(":instrumentation:lettuce:lettuce-5.1:testing") -hideFromDependabot(":instrumentation:liberty:compile-stub") -hideFromDependabot(":instrumentation:liberty:liberty-20.0:javaagent") -hideFromDependabot(":instrumentation:liberty:liberty-dispatcher-20.0:javaagent") -hideFromDependabot(":instrumentation:log4j:log4j-appender-1.2:javaagent") -hideFromDependabot(":instrumentation:log4j:log4j-mdc-1.2:javaagent") -hideFromDependabot(":instrumentation:log4j:log4j-context-data:log4j-context-data-2.7:javaagent") -hideFromDependabot(":instrumentation:log4j:log4j-context-data:log4j-context-data-2.17:javaagent") -hideFromDependabot(":instrumentation:log4j:log4j-context-data:log4j-context-data-2.17:library-autoconfigure") -hideFromDependabot(":instrumentation:log4j:log4j-context-data:log4j-context-data-common:testing") -hideFromDependabot(":instrumentation:log4j:log4j-appender-2.17:javaagent") -hideFromDependabot(":instrumentation:log4j:log4j-appender-2.17:library") -hideFromDependabot(":instrumentation:logback:logback-appender-1.0:javaagent") -hideFromDependabot(":instrumentation:logback:logback-appender-1.0:library") -hideFromDependabot(":instrumentation:logback:logback-mdc-1.0:javaagent") -hideFromDependabot(":instrumentation:logback:logback-mdc-1.0:library") -hideFromDependabot(":instrumentation:logback:logback-mdc-1.0:testing") -hideFromDependabot(":instrumentation:methods:javaagent") -hideFromDependabot(":instrumentation:micrometer:micrometer-1.5:javaagent") -hideFromDependabot(":instrumentation:micrometer:micrometer-1.5:library") -hideFromDependabot(":instrumentation:micrometer:micrometer-1.5:testing") -hideFromDependabot(":instrumentation:mongo:mongo-3.1:javaagent") -hideFromDependabot(":instrumentation:mongo:mongo-3.1:library") -hideFromDependabot(":instrumentation:mongo:mongo-3.1:testing") -hideFromDependabot(":instrumentation:mongo:mongo-3.7:javaagent") -hideFromDependabot(":instrumentation:mongo:mongo-4.0:javaagent") -hideFromDependabot(":instrumentation:mongo:mongo-async-3.3:javaagent") -hideFromDependabot(":instrumentation:mongo:mongo-common:testing") -hideFromDependabot(":instrumentation:netty:netty-3.8:javaagent") -hideFromDependabot(":instrumentation:netty:netty-4.0:javaagent") -hideFromDependabot(":instrumentation:netty:netty-4.1:javaagent") -hideFromDependabot(":instrumentation:netty:netty-4.1:library") -hideFromDependabot(":instrumentation:netty:netty-4.1:testing") -hideFromDependabot(":instrumentation:netty:netty-4-common:javaagent") -hideFromDependabot(":instrumentation:netty:netty-4-common:library") -hideFromDependabot(":instrumentation:netty:netty-common:library") -hideFromDependabot(":instrumentation:okhttp:okhttp-2.2:javaagent") -hideFromDependabot(":instrumentation:okhttp:okhttp-3.0:javaagent") -hideFromDependabot(":instrumentation:okhttp:okhttp-3.0:library") -hideFromDependabot(":instrumentation:okhttp:okhttp-3.0:testing") -hideFromDependabot(":instrumentation:opencensus-shim:testing") -hideFromDependabot(":instrumentation:opentelemetry-api:opentelemetry-api-1.0:javaagent") -hideFromDependabot(":instrumentation:opentelemetry-api:opentelemetry-api-1.4:javaagent") -hideFromDependabot(":instrumentation:opentelemetry-api:opentelemetry-api-1.10:javaagent") -hideFromDependabot(":instrumentation:opentelemetry-api:opentelemetry-api-1.15:javaagent") -hideFromDependabot(":instrumentation:opentelemetry-api:opentelemetry-api-1.27:javaagent") -hideFromDependabot(":instrumentation:opentelemetry-extension-annotations-1.0:javaagent") -hideFromDependabot(":instrumentation:opentelemetry-extension-kotlin-1.0:javaagent") -hideFromDependabot(":instrumentation:opentelemetry-instrumentation-annotations-1.16:javaagent") -hideFromDependabot(":instrumentation:opentelemetry-instrumentation-api:javaagent") -hideFromDependabot(":instrumentation:opentelemetry-instrumentation-api:testing") -hideFromDependabot(":instrumentation:oracle-ucp-11.2:javaagent") -hideFromDependabot(":instrumentation:oracle-ucp-11.2:library") -hideFromDependabot(":instrumentation:oracle-ucp-11.2:testing") -hideFromDependabot(":instrumentation:oshi:javaagent") -hideFromDependabot(":instrumentation:oshi:library") -hideFromDependabot(":instrumentation:oshi:testing") -hideFromDependabot(":instrumentation:payara:javaagent") -hideFromDependabot(":instrumentation:play:play-mvc:play-mvc-2.4:javaagent") -hideFromDependabot(":instrumentation:play:play-mvc:play-mvc-2.6:javaagent") -hideFromDependabot(":instrumentation:play:play-ws:play-ws-1.0:javaagent") -hideFromDependabot(":instrumentation:play:play-ws:play-ws-2.0:javaagent") -hideFromDependabot(":instrumentation:play:play-ws:play-ws-2.1:javaagent") -hideFromDependabot(":instrumentation:play:play-ws:play-ws-common:javaagent") -hideFromDependabot(":instrumentation:play:play-ws:play-ws-common:testing") -hideFromDependabot(":instrumentation:pulsar:pulsar-2.8:javaagent") -hideFromDependabot(":instrumentation:pulsar:pulsar-2.8:javaagent-unit-tests") -hideFromDependabot(":instrumentation:quartz-2.0:javaagent") -hideFromDependabot(":instrumentation:quartz-2.0:library") -hideFromDependabot(":instrumentation:quartz-2.0:testing") -hideFromDependabot(":instrumentation:r2dbc-1.0:javaagent") -hideFromDependabot(":instrumentation:r2dbc-1.0:library") -hideFromDependabot(":instrumentation:r2dbc-1.0:library-instrumentation-shaded") -hideFromDependabot(":instrumentation:r2dbc-1.0:testing") -hideFromDependabot(":instrumentation:rabbitmq-2.7:javaagent") -hideFromDependabot(":instrumentation:ratpack:ratpack-1.4:javaagent") -hideFromDependabot(":instrumentation:ratpack:ratpack-1.4:testing") -hideFromDependabot(":instrumentation:ratpack:ratpack-1.7:library") -hideFromDependabot(":instrumentation:reactor:reactor-3.1:javaagent") -hideFromDependabot(":instrumentation:reactor:reactor-3.1:library") -hideFromDependabot(":instrumentation:reactor:reactor-3.1:testing") -hideFromDependabot(":instrumentation:reactor:reactor-kafka-1.0:javaagent") -hideFromDependabot(":instrumentation:reactor:reactor-kafka-1.0:testing") -hideFromDependabot(":instrumentation:reactor:reactor-netty:reactor-netty-0.9:javaagent") -hideFromDependabot(":instrumentation:reactor:reactor-netty:reactor-netty-1.0:javaagent") -hideFromDependabot(":instrumentation:reactor:reactor-netty:reactor-netty-1.0:javaagent-unit-tests") -hideFromDependabot(":instrumentation:rediscala-1.8:javaagent") -hideFromDependabot(":instrumentation:redisson:redisson-3.0:javaagent") -hideFromDependabot(":instrumentation:redisson:redisson-3.17:javaagent") -hideFromDependabot(":instrumentation:redisson:redisson-common:javaagent") -hideFromDependabot(":instrumentation:redisson:redisson-common:testing") -hideFromDependabot(":instrumentation:resources:library") -hideFromDependabot(":instrumentation:restlet:restlet-1.1:javaagent") -hideFromDependabot(":instrumentation:restlet:restlet-1.1:library") -hideFromDependabot(":instrumentation:restlet:restlet-1.1:testing") -hideFromDependabot(":instrumentation:restlet:restlet-2.0:javaagent") -hideFromDependabot(":instrumentation:restlet:restlet-2.0:library") -hideFromDependabot(":instrumentation:restlet:restlet-2.0:testing") -hideFromDependabot(":instrumentation:rmi:bootstrap") -hideFromDependabot(":instrumentation:rmi:javaagent") -hideFromDependabot(":instrumentation:rocketmq:rocketmq-client:rocketmq-client-4.8:javaagent") -hideFromDependabot(":instrumentation:rocketmq:rocketmq-client:rocketmq-client-4.8:library") -hideFromDependabot(":instrumentation:rocketmq:rocketmq-client:rocketmq-client-4.8:testing") -hideFromDependabot(":instrumentation:rocketmq:rocketmq-client:rocketmq-client-5.0:javaagent") -hideFromDependabot(":instrumentation:rocketmq:rocketmq-client:rocketmq-client-5.0:testing") -hideFromDependabot(":instrumentation:runtime-telemetry:runtime-telemetry-java8:javaagent") -hideFromDependabot(":instrumentation:runtime-telemetry:runtime-telemetry-java8:library") -hideFromDependabot(":instrumentation:runtime-telemetry:runtime-telemetry-java17:javaagent") -hideFromDependabot(":instrumentation:runtime-telemetry:runtime-telemetry-java17:library") -hideFromDependabot(":instrumentation:rxjava:rxjava-1.0:library") -hideFromDependabot(":instrumentation:rxjava:rxjava-2.0:library") -hideFromDependabot(":instrumentation:rxjava:rxjava-2.0:testing") -hideFromDependabot(":instrumentation:rxjava:rxjava-2.0:javaagent") -hideFromDependabot(":instrumentation:rxjava:rxjava-3.0:library") -hideFromDependabot(":instrumentation:rxjava:rxjava-3.0:javaagent") -hideFromDependabot(":instrumentation:rxjava:rxjava-3.1.1:library") -hideFromDependabot(":instrumentation:rxjava:rxjava-3.1.1:javaagent") -hideFromDependabot(":instrumentation:rxjava:rxjava-3-common:library") -hideFromDependabot(":instrumentation:rxjava:rxjava-3-common:testing") -hideFromDependabot(":instrumentation:quarkus-resteasy-reactive:javaagent") -hideFromDependabot(":instrumentation:quarkus-resteasy-reactive:common-testing") -hideFromDependabot(":instrumentation:quarkus-resteasy-reactive:quarkus2-testing") -hideFromDependabot(":instrumentation:quarkus-resteasy-reactive:quarkus3-testing") -hideFromDependabot(":instrumentation:scala-fork-join-2.8:javaagent") -hideFromDependabot(":instrumentation:servlet:servlet-common:bootstrap") -hideFromDependabot(":instrumentation:servlet:servlet-common:javaagent") -hideFromDependabot(":instrumentation:servlet:servlet-javax-common:javaagent") -hideFromDependabot(":instrumentation:servlet:servlet-2.2:javaagent") -hideFromDependabot(":instrumentation:servlet:servlet-3.0:javaagent") -hideFromDependabot(":instrumentation:servlet:servlet-3.0:javaagent-unit-tests") -hideFromDependabot(":instrumentation:servlet:servlet-5.0:javaagent") -hideFromDependabot(":instrumentation:servlet:servlet-5.0:javaagent-unit-tests") -hideFromDependabot(":instrumentation:spark-2.3:javaagent") -hideFromDependabot(":instrumentation:spring:spring-batch-3.0:javaagent") -hideFromDependabot(":instrumentation:spring:spring-boot-actuator-autoconfigure-2.0:javaagent") -hideFromDependabot(":instrumentation:spring:spring-boot-resources:library") -hideFromDependabot(":instrumentation:spring:spring-boot-resources:testing") -hideFromDependabot(":instrumentation:spring:spring-core-2.0:javaagent") -hideFromDependabot(":instrumentation:spring:spring-data:spring-data-1.8:javaagent") -hideFromDependabot(":instrumentation:spring:spring-data:spring-data-3.0:testing") -hideFromDependabot(":instrumentation:spring:spring-data:spring-data-common:testing") -hideFromDependabot(":instrumentation:spring:spring-integration-4.1:javaagent") -hideFromDependabot(":instrumentation:spring:spring-integration-4.1:library") -hideFromDependabot(":instrumentation:spring:spring-integration-4.1:testing") -hideFromDependabot(":instrumentation:spring:spring-jms:spring-jms-2.0:javaagent") -hideFromDependabot(":instrumentation:spring:spring-jms:spring-jms-6.0:javaagent") -hideFromDependabot(":instrumentation:spring:spring-kafka-2.7:javaagent") -hideFromDependabot(":instrumentation:spring:spring-kafka-2.7:library") -hideFromDependabot(":instrumentation:spring:spring-kafka-2.7:testing") -hideFromDependabot(":instrumentation:spring:spring-rabbit-1.0:javaagent") -hideFromDependabot(":instrumentation:spring:spring-rmi-4.0:javaagent") -hideFromDependabot(":instrumentation:spring:spring-scheduling-3.1:bootstrap") -hideFromDependabot(":instrumentation:spring:spring-scheduling-3.1:javaagent") -hideFromDependabot(":instrumentation:spring:spring-web:spring-web-3.1:javaagent") -hideFromDependabot(":instrumentation:spring:spring-web:spring-web-3.1:library") -hideFromDependabot(":instrumentation:spring:spring-web:spring-web-3.1:testing") -hideFromDependabot(":instrumentation:spring:spring-web:spring-web-6.0:javaagent") -hideFromDependabot(":instrumentation:spring:spring-webmvc:spring-webmvc-3.1:javaagent") -hideFromDependabot(":instrumentation:spring:spring-webmvc:spring-webmvc-3.1:wildfly-testing") -hideFromDependabot(":instrumentation:spring:spring-webmvc:spring-webmvc-5.3:library") -hideFromDependabot(":instrumentation:spring:spring-webmvc:spring-webmvc-6.0:javaagent") -hideFromDependabot(":instrumentation:spring:spring-webmvc:spring-webmvc-6.0:library") -hideFromDependabot(":instrumentation:spring:spring-webmvc:spring-webmvc-common:javaagent") -hideFromDependabot(":instrumentation:spring:spring-webmvc:spring-webmvc-common:testing") -hideFromDependabot(":instrumentation:spring:spring-webflux:spring-webflux-5.0:javaagent") -hideFromDependabot(":instrumentation:spring:spring-webflux:spring-webflux-5.3:testing") -hideFromDependabot(":instrumentation:spring:spring-webflux:spring-webflux-5.3:library") -hideFromDependabot(":instrumentation:spring:spring-ws-2.0:javaagent") -hideFromDependabot(":instrumentation:spring:spring-boot-autoconfigure") -hideFromDependabot(":instrumentation:spring:starters:spring-boot-starter") -hideFromDependabot(":instrumentation:spring:starters:jaeger-spring-boot-starter") -hideFromDependabot(":instrumentation:spring:starters:zipkin-spring-boot-starter") -hideFromDependabot(":instrumentation:spymemcached-2.12:javaagent") -hideFromDependabot(":instrumentation:struts-2.3:javaagent") -hideFromDependabot(":instrumentation:tapestry-5.4:javaagent") -hideFromDependabot(":instrumentation:tomcat:tomcat-7.0:javaagent") -hideFromDependabot(":instrumentation:tomcat:tomcat-10.0:javaagent") -hideFromDependabot(":instrumentation:tomcat:tomcat-common:javaagent") -hideFromDependabot(":instrumentation:tomcat:tomcat-jdbc") -hideFromDependabot(":instrumentation:twilio-6.6:javaagent") -hideFromDependabot(":instrumentation:undertow-1.4:bootstrap") -hideFromDependabot(":instrumentation:undertow-1.4:javaagent") -hideFromDependabot(":instrumentation:vaadin-14.2:javaagent") -hideFromDependabot(":instrumentation:vaadin-14.2:testing") -hideFromDependabot(":instrumentation:vertx:vertx-http-client:vertx-http-client-3.0:javaagent") -hideFromDependabot(":instrumentation:vertx:vertx-http-client:vertx-http-client-4.0:javaagent") -hideFromDependabot(":instrumentation:vertx:vertx-http-client:vertx-http-client-common:javaagent") -hideFromDependabot(":instrumentation:vertx:vertx-kafka-client-3.6:javaagent") -hideFromDependabot(":instrumentation:vertx:vertx-kafka-client-3.6:testing") -hideFromDependabot(":instrumentation:vertx:vertx-rx-java-3.5:javaagent") -hideFromDependabot(":instrumentation:vertx:vertx-sql-client-4.0:javaagent") -hideFromDependabot(":instrumentation:vertx:vertx-web-3.0:javaagent") -hideFromDependabot(":instrumentation:vertx:vertx-web-3.0:testing") -hideFromDependabot(":instrumentation:vibur-dbcp-11.0:javaagent") -hideFromDependabot(":instrumentation:vibur-dbcp-11.0:library") -hideFromDependabot(":instrumentation:vibur-dbcp-11.0:testing") -hideFromDependabot(":instrumentation:wicket-8.0:javaagent") -hideFromDependabot(":instrumentation:zio:zio-2.0:javaagent") +include(":smoke-tests-otel-starter:spring-smoke-testing") +include(":smoke-tests-otel-starter:spring-boot-2") +include(":smoke-tests-otel-starter:spring-boot-3") +include(":smoke-tests-otel-starter:spring-boot-3.2") +include(":smoke-tests-otel-starter:spring-boot-common") +include(":smoke-tests-otel-starter:spring-boot-reactive-2") +include(":smoke-tests-otel-starter:spring-boot-reactive-3") +include(":smoke-tests-otel-starter:spring-boot-reactive-common") + +include(":instrumentation:akka:akka-actor-2.3:javaagent") +include(":instrumentation:akka:akka-actor-fork-join-2.5:javaagent") +include(":instrumentation:akka:akka-http-10.0:javaagent") +include(":instrumentation:alibaba-druid-1.0:javaagent") +include(":instrumentation:alibaba-druid-1.0:library") +include(":instrumentation:alibaba-druid-1.0:testing") +include(":instrumentation:apache-dbcp-2.0:javaagent") +include(":instrumentation:apache-dbcp-2.0:library") +include(":instrumentation:apache-dbcp-2.0:testing") +include(":instrumentation:apache-dubbo-2.7:javaagent") +include(":instrumentation:apache-dubbo-2.7:library-autoconfigure") +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-4.3:library") +include(":instrumentation:apache-httpclient:apache-httpclient-4.3:testing") +include(":instrumentation:apache-httpclient:apache-httpclient-5.0:javaagent") +include(":instrumentation:apache-httpclient:apache-httpclient-5.2:library") +include(":instrumentation:apache-shenyu-2.4:javaagent") +include(":instrumentation:armeria:armeria-1.3:javaagent") +include(":instrumentation:armeria:armeria-1.3:library") +include(":instrumentation:armeria:armeria-1.3:testing") +include(":instrumentation:armeria:armeria-grpc-1.14:javaagent") +include(":instrumentation:async-http-client:async-http-client-1.9:javaagent") +include(":instrumentation:async-http-client:async-http-client-2.0:javaagent") +include(":instrumentation:aws-lambda:aws-lambda-core-1.0:javaagent") +include(":instrumentation:aws-lambda:aws-lambda-core-1.0:library") +include(":instrumentation:aws-lambda:aws-lambda-core-1.0:testing") +include(":instrumentation:aws-lambda:aws-lambda-events-2.2:javaagent") +include(":instrumentation:aws-lambda:aws-lambda-events-2.2:library") +include(":instrumentation:aws-lambda:aws-lambda-events-2.2:testing") +include(":instrumentation:aws-sdk:aws-sdk-1.11:javaagent") +include(":instrumentation:aws-sdk:aws-sdk-1.11:library") +include(":instrumentation:aws-sdk:aws-sdk-1.11:library-autoconfigure") +include(":instrumentation:aws-sdk:aws-sdk-1.11:testing") +include(":instrumentation:aws-sdk:aws-sdk-2.2:javaagent") +include(":instrumentation:aws-sdk:aws-sdk-2.2:library") +include(":instrumentation:aws-sdk:aws-sdk-2.2:library-autoconfigure") +include(":instrumentation:aws-sdk:aws-sdk-2.2:testing") +include(":instrumentation:azure-core:azure-core-1.14:javaagent") +include(":instrumentation:azure-core:azure-core-1.14:library-instrumentation-shaded") +include(":instrumentation:azure-core:azure-core-1.19:javaagent") +include(":instrumentation:azure-core:azure-core-1.19:library-instrumentation-shaded") +include(":instrumentation:azure-core:azure-core-1.36:javaagent") +include(":instrumentation:azure-core:azure-core-1.36:library-instrumentation-shaded") +include(":instrumentation:c3p0-0.9:javaagent") +include(":instrumentation:c3p0-0.9:library") +include(":instrumentation:c3p0-0.9:testing") +include(":instrumentation:camel-2.20:javaagent") +include(":instrumentation:camel-2.20:javaagent-unit-tests") +include(":instrumentation:cassandra:cassandra-3.0:javaagent") +include(":instrumentation:cassandra:cassandra-4.0:javaagent") +include(":instrumentation:cassandra:cassandra-4.4:javaagent") +include(":instrumentation:cassandra:cassandra-4.4:library") +include(":instrumentation:cassandra:cassandra-4.4:testing") +include(":instrumentation:cassandra:cassandra-4-common:testing") +include(":instrumentation:cdi-testing") +include(":instrumentation:clickhouse-client-0.5:javaagent") +include(":instrumentation:couchbase:couchbase-2.0:javaagent") +include(":instrumentation:couchbase:couchbase-2.6:javaagent") +include(":instrumentation:couchbase:couchbase-2-common:javaagent") +include(":instrumentation:couchbase:couchbase-2-common:javaagent-unit-tests") +include(":instrumentation:couchbase:couchbase-3.1:javaagent") +include(":instrumentation:couchbase:couchbase-3.1:tracing-opentelemetry-shaded") +include(":instrumentation:couchbase:couchbase-3.1.6:javaagent") +include(":instrumentation:couchbase:couchbase-3.1.6:tracing-opentelemetry-shaded") +include(":instrumentation:couchbase:couchbase-3.2:javaagent") +include(":instrumentation:couchbase:couchbase-3.2:tracing-opentelemetry-shaded") +include(":instrumentation:couchbase:couchbase-common:testing") +include(":instrumentation:dropwizard:dropwizard-metrics-4.0:javaagent") +include(":instrumentation:dropwizard:dropwizard-testing") +include(":instrumentation:dropwizard:dropwizard-views-0.7:javaagent") +include(":instrumentation:elasticsearch:elasticsearch-api-client-7.16:javaagent") +include(":instrumentation:elasticsearch:elasticsearch-api-client-7.16:javaagent-unit-tests") +include(":instrumentation:elasticsearch:elasticsearch-rest-5.0:javaagent") +include(":instrumentation:elasticsearch:elasticsearch-rest-6.4:javaagent") +include(":instrumentation:elasticsearch:elasticsearch-rest-7.0:javaagent") +include(":instrumentation:elasticsearch:elasticsearch-rest-7.0:library") +include(":instrumentation:elasticsearch:elasticsearch-rest-common:javaagent") +include(":instrumentation:elasticsearch:elasticsearch-rest-common:library") +include(":instrumentation:elasticsearch:elasticsearch-transport-5.0:javaagent") +include(":instrumentation:elasticsearch:elasticsearch-transport-5.3:javaagent") +include(":instrumentation:elasticsearch:elasticsearch-transport-6.0:javaagent") +include(":instrumentation:elasticsearch:elasticsearch-transport-common:javaagent") +include(":instrumentation:elasticsearch:elasticsearch-transport-common:testing") +include(":instrumentation:executors:bootstrap") +include(":instrumentation:executors:javaagent") +include(":instrumentation:executors:jdk21-testing") +include(":instrumentation:executors:testing") +include(":instrumentation:external-annotations:javaagent") +include(":instrumentation:external-annotations:javaagent-unit-tests") +include(":instrumentation:finagle-http-23.11:javaagent") +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:graphql-java:graphql-java-12.0:javaagent") +include(":instrumentation:graphql-java:graphql-java-12.0:library") +include(":instrumentation:graphql-java:graphql-java-20.0:javaagent") +include(":instrumentation:graphql-java:graphql-java-20.0:library") +include(":instrumentation:graphql-java:graphql-java-common:library") +include(":instrumentation:graphql-java:graphql-java-common:testing") +include(":instrumentation:grizzly-2.3: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:hibernate:hibernate-3.3:javaagent") +include(":instrumentation:hibernate:hibernate-4.0:javaagent") +include(":instrumentation:hibernate:hibernate-6.0:javaagent") +include(":instrumentation:hibernate:hibernate-6.0:spring-testing") +include(":instrumentation:hibernate:hibernate-common:javaagent") +include(":instrumentation:hibernate:hibernate-procedure-call-4.3:javaagent") +include(":instrumentation:hibernate:hibernate-reactive-1.0:javaagent") +include(":instrumentation:hikaricp-3.0:javaagent") +include(":instrumentation:hikaricp-3.0:library") +include(":instrumentation:hikaricp-3.0:testing") +include(":instrumentation:http-url-connection:javaagent") +include(":instrumentation:hystrix-1.4:javaagent") +include(":instrumentation:influxdb-2.4:javaagent") +include(":instrumentation:internal:internal-application-logger:bootstrap") +include(":instrumentation:internal:internal-application-logger: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-lambda:javaagent") +include(":instrumentation:internal:internal-lambda-java9:javaagent") +include(":instrumentation:internal:internal-reflection:javaagent") +include(":instrumentation:internal:internal-reflection:javaagent-integration-tests") +include(":instrumentation:internal:internal-url-class-loader:javaagent") +include(":instrumentation:internal:internal-url-class-loader:javaagent-integration-tests") +include(":instrumentation:java-http-client:javaagent") +include(":instrumentation:java-http-client:library") +include(":instrumentation:java-http-client:testing") +include(":instrumentation:java-util-logging:javaagent") +include(":instrumentation:java-util-logging:shaded-stub-for-instrumenting") +include(":instrumentation:javalin-5.0:javaagent") +include(":instrumentation:jaxrs:jaxrs-1.0:javaagent") +include(":instrumentation:jaxrs:jaxrs-2.0:jaxrs-2.0-annotations: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-common:testing") +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-tomee-testing") +include(":instrumentation:jaxrs:jaxrs-2.0:jaxrs-2.0-wildfly-testing") +include(":instrumentation:jaxrs:jaxrs-3.0:jaxrs-3.0-annotations:javaagent") +include(":instrumentation:jaxrs:jaxrs-3.0:jaxrs-3.0-common:javaagent") +include(":instrumentation:jaxrs:jaxrs-3.0:jaxrs-3.0-common:testing") +include(":instrumentation:jaxrs:jaxrs-3.0:jaxrs-3.0-jersey-3.0:javaagent") +include(":instrumentation:jaxrs:jaxrs-3.0:jaxrs-3.0-resteasy-6.0:javaagent") +include(":instrumentation:jaxrs:jaxrs-common:bootstrap") +include(":instrumentation:jaxrs:jaxrs-common:javaagent") +include(":instrumentation:jaxrs:jaxrs-common:testing") +include(":instrumentation:jaxrs-client:jaxrs-client-1.1-testing") +include(":instrumentation:jaxrs-client:jaxrs-client-2.0-testing") +include(":instrumentation:jaxws:jaxws-2.0:javaagent") +include(":instrumentation:jaxws:jaxws-2.0-arquillian-testing") +include(":instrumentation:jaxws:jaxws-2.0-axis2-1.6:javaagent") +include(":instrumentation:jaxws:jaxws-2.0-common-testing") +include(":instrumentation:jaxws:jaxws-2.0-metro-2.2-testing") +include(":instrumentation:jaxws:jaxws-2.0-tomee-testing") +include(":instrumentation:jaxws:jaxws-2.0-wildfly-testing") +include(":instrumentation:jaxws:jaxws-3.0-common-testing") +include(":instrumentation:jaxws:jaxws-3.0-cxf-4.0-testing") +include(":instrumentation:jaxws:jaxws-3.0-metro-2.2-testing") +include(":instrumentation:jaxws:jaxws-common:javaagent") +include(":instrumentation:jaxws:jaxws-cxf-3.0:javaagent") +include(":instrumentation:jaxws:jaxws-cxf-3.0:javaagent-unit-tests") +include(":instrumentation:jaxws:jaxws-jws-api-1.1:javaagent") +include(":instrumentation:jaxws:jaxws-metro-2.2:javaagent") +include(":instrumentation:jboss-logmanager:jboss-logmanager-appender-1.1:javaagent") +include(":instrumentation:jboss-logmanager:jboss-logmanager-mdc-1.1:javaagent") +include(":instrumentation:jdbc:bootstrap") +include(":instrumentation:jdbc:javaagent") +include(":instrumentation:jdbc:library") +include(":instrumentation:jdbc:testing") +include(":instrumentation:jedis:jedis-1.4:javaagent") +include(":instrumentation:jedis:jedis-1.4:testing") +include(":instrumentation:jedis:jedis-3.0:javaagent") +include(":instrumentation:jedis:jedis-4.0:javaagent") +include(":instrumentation:jedis:jedis-common:javaagent") +include(":instrumentation:jetty:jetty-8.0:javaagent") +include(":instrumentation:jetty:jetty-11.0:javaagent") +include(":instrumentation:jetty:jetty-12.0:javaagent") +include(":instrumentation:jetty:jetty-common:javaagent") +include(":instrumentation:jetty-httpclient:jetty-httpclient-9.2:javaagent") +include(":instrumentation:jetty-httpclient:jetty-httpclient-9.2:library") +include(":instrumentation:jetty-httpclient:jetty-httpclient-9.2:testing") +include(":instrumentation:jetty-httpclient:jetty-httpclient-12.0:javaagent") +include(":instrumentation:jetty-httpclient:jetty-httpclient-12.0:library") +include(":instrumentation:jetty-httpclient:jetty-httpclient-12.0:testing") +include(":instrumentation:jms:jms-1.1:javaagent") +include(":instrumentation:jms:jms-3.0:javaagent") +include(":instrumentation:jms:jms-common:bootstrap") +include(":instrumentation:jms:jms-common:javaagent") +include(":instrumentation:jms:jms-common:javaagent-unit-tests") +include(":instrumentation:jmx-metrics:javaagent") +include(":instrumentation:jmx-metrics:library") +include(":instrumentation:jodd-http-4.2:javaagent") +include(":instrumentation:jodd-http-4.2:javaagent-unit-tests") +include(":instrumentation:jsf:jsf-jakarta-common:javaagent") +include(":instrumentation:jsf:jsf-jakarta-common:testing") +include(":instrumentation:jsf:jsf-javax-common:javaagent") +include(":instrumentation:jsf:jsf-javax-common:testing") +include(":instrumentation:jsf:jsf-mojarra-1.2:javaagent") +include(":instrumentation:jsf:jsf-mojarra-3.0:javaagent") +include(":instrumentation:jsf:jsf-myfaces-1.2:javaagent") +include(":instrumentation:jsf:jsf-myfaces-3.0:javaagent") +include(":instrumentation:jsp-2.3:javaagent") +include(":instrumentation:kafka:kafka-clients:kafka-clients-0.11:bootstrap") +include(":instrumentation:kafka:kafka-clients:kafka-clients-0.11:javaagent") +include(":instrumentation:kafka:kafka-clients:kafka-clients-0.11:testing") +include(":instrumentation:kafka:kafka-clients:kafka-clients-2.6:library") +include(":instrumentation:kafka:kafka-clients:kafka-clients-common:library") +include(":instrumentation:kafka:kafka-streams-0.11:javaagent") +include(":instrumentation:kotlinx-coroutines:kotlinx-coroutines-1.0:javaagent") +include(":instrumentation:kotlinx-coroutines:kotlinx-coroutines-flow-1.3:javaagent") +include(":instrumentation:kotlinx-coroutines:kotlinx-coroutines-flow-1.3:javaagent-kotlin") +include(":instrumentation:ktor:ktor-1.0:library") +include(":instrumentation:ktor:ktor-2.0:javaagent") +include(":instrumentation:ktor:ktor-2.0:library") +include(":instrumentation:ktor:ktor-2.0:testing") +include(":instrumentation:ktor:ktor-common:library") +include(":instrumentation:kubernetes-client-7.0:javaagent") +include(":instrumentation:kubernetes-client-7.0:javaagent-unit-tests") +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:lettuce:lettuce-common:library") +include(":instrumentation:liberty:compile-stub") +include(":instrumentation:liberty:liberty-20.0:javaagent") +include(":instrumentation:liberty:liberty-dispatcher-20.0:javaagent") +include(":instrumentation:log4j:log4j-appender-1.2:javaagent") +include(":instrumentation:log4j:log4j-appender-2.17:javaagent") +include(":instrumentation:log4j:log4j-appender-2.17:library") +include(":instrumentation:log4j:log4j-context-data:log4j-context-data-2.7:javaagent") +include(":instrumentation:log4j:log4j-context-data:log4j-context-data-2.17:javaagent") +include(":instrumentation:log4j:log4j-context-data:log4j-context-data-2.17:library-autoconfigure") +include(":instrumentation:log4j:log4j-context-data:log4j-context-data-common:testing") +include(":instrumentation:log4j:log4j-mdc-1.2:javaagent") +include(":instrumentation:logback:logback-appender-1.0:javaagent") +include(":instrumentation:logback:logback-appender-1.0:library") +include(":instrumentation:logback:logback-mdc-1.0:javaagent") +include(":instrumentation:logback:logback-mdc-1.0:library") +include(":instrumentation:logback:logback-mdc-1.0:testing") +include(":instrumentation:methods:javaagent") +include(":instrumentation:micrometer:micrometer-1.5:javaagent") +include(":instrumentation:micrometer:micrometer-1.5:library") +include(":instrumentation:micrometer:micrometer-1.5:testing") +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-common:testing") +include(":instrumentation:mybatis-3.2:javaagent") +include(":instrumentation:netty:netty-3.8:javaagent") +include(":instrumentation:netty:netty-4.0:javaagent") +include(":instrumentation:netty:netty-4.1:javaagent") +include(":instrumentation:netty:netty-4.1:library") +include(":instrumentation:netty:netty-4.1:testing") +include(":instrumentation:netty:netty-4-common:javaagent") +include(":instrumentation:netty:netty-4-common:library") +include(":instrumentation:netty:netty-common:library") +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:opencensus-shim:testing") +include(":instrumentation:opensearch:opensearch-rest-1.0:javaagent") +include(":instrumentation:opensearch:opensearch-rest-common:javaagent") +include(":instrumentation:opentelemetry-api:opentelemetry-api-1.0:javaagent") +include(":instrumentation:opentelemetry-api:opentelemetry-api-1.4:javaagent") +include(":instrumentation:opentelemetry-api:opentelemetry-api-1.10:javaagent") +include(":instrumentation:opentelemetry-api:opentelemetry-api-1.15:javaagent") +include(":instrumentation:opentelemetry-api:opentelemetry-api-1.27:javaagent") +include(":instrumentation:opentelemetry-api:opentelemetry-api-1.31:javaagent") +include(":instrumentation:opentelemetry-api:opentelemetry-api-1.32:javaagent") +include(":instrumentation:opentelemetry-api:opentelemetry-api-1.37:javaagent") +include(":instrumentation:opentelemetry-api:opentelemetry-api-1.38:javaagent") +include(":instrumentation:opentelemetry-extension-annotations-1.0:javaagent") +include(":instrumentation:opentelemetry-extension-kotlin-1.0:javaagent") +include(":instrumentation:opentelemetry-instrumentation-annotations-1.16:javaagent") +include(":instrumentation:opentelemetry-instrumentation-api:javaagent") +include(":instrumentation:opentelemetry-instrumentation-api:testing") +include(":instrumentation:oracle-ucp-11.2:javaagent") +include(":instrumentation:oracle-ucp-11.2:library") +include(":instrumentation:oracle-ucp-11.2:testing") +include(":instrumentation:oshi:javaagent") +include(":instrumentation:oshi:library") +include(":instrumentation:oshi:testing") +include(":instrumentation:payara:javaagent") +include(":instrumentation:pekko:pekko-actor-1.0:javaagent") +include(":instrumentation:pekko:pekko-http-1.0:javaagent") +include(":instrumentation:play:play-mvc:play-mvc-2.4:javaagent") +include(":instrumentation:play:play-mvc:play-mvc-2.6:javaagent") +include(":instrumentation:play:play-ws:play-ws-1.0:javaagent") +include(":instrumentation:play:play-ws:play-ws-2.0:javaagent") +include(":instrumentation:play:play-ws:play-ws-2.1:javaagent") +include(":instrumentation:play:play-ws:play-ws-common:javaagent") +include(":instrumentation:play:play-ws:play-ws-common:testing") +include(":instrumentation:pulsar:pulsar-2.8:javaagent") +include(":instrumentation:pulsar:pulsar-2.8:javaagent-unit-tests") +include(":instrumentation:quarkus-resteasy-reactive:common-testing") +include(":instrumentation:quarkus-resteasy-reactive:javaagent") +include(":instrumentation:quarkus-resteasy-reactive:quarkus2-testing") +include(":instrumentation:quarkus-resteasy-reactive:quarkus3-testing") +include(":instrumentation:quartz-2.0:javaagent") +include(":instrumentation:quartz-2.0:library") +include(":instrumentation:quartz-2.0:testing") +include(":instrumentation:r2dbc-1.0:javaagent") +include(":instrumentation:r2dbc-1.0:library") +include(":instrumentation:r2dbc-1.0:library-instrumentation-shaded") +include(":instrumentation:r2dbc-1.0:testing") +include(":instrumentation:rabbitmq-2.7:javaagent") +include(":instrumentation:ratpack:ratpack-1.4:javaagent") +include(":instrumentation:ratpack:ratpack-1.4:testing") +include(":instrumentation:ratpack:ratpack-1.7:library") +include(":instrumentation:reactor:reactor-3.1:javaagent") +include(":instrumentation:reactor:reactor-3.1:library") +include(":instrumentation:reactor:reactor-3.1:testing") +include(":instrumentation:reactor:reactor-3.4:javaagent") +include(":instrumentation:reactor:reactor-kafka-1.0:javaagent") +include(":instrumentation:reactor:reactor-kafka-1.0:testing") +include(":instrumentation:reactor:reactor-netty:reactor-netty-0.9:javaagent") +include(":instrumentation:reactor:reactor-netty:reactor-netty-1.0:javaagent") +include(":instrumentation:reactor:reactor-netty:reactor-netty-1.0:javaagent-unit-tests") +include(":instrumentation:rediscala-1.8:javaagent") +include(":instrumentation:redisson:redisson-3.0:javaagent") +include(":instrumentation:redisson:redisson-3.17:javaagent") +include(":instrumentation:redisson:redisson-common:javaagent") +include(":instrumentation:redisson:redisson-common:testing") +include(":instrumentation:resources:library") +include(":instrumentation:restlet:restlet-1.1:javaagent") +include(":instrumentation:restlet:restlet-1.1:library") +include(":instrumentation:restlet:restlet-1.1:testing") +include(":instrumentation:restlet:restlet-2.0:javaagent") +include(":instrumentation:restlet:restlet-2.0:library") +include(":instrumentation:restlet:restlet-2.0:testing") +include(":instrumentation:rmi:bootstrap") +include(":instrumentation:rmi:javaagent") +include(":instrumentation:rocketmq:rocketmq-client:rocketmq-client-4.8:javaagent") +include(":instrumentation:rocketmq:rocketmq-client:rocketmq-client-4.8:library") +include(":instrumentation:rocketmq:rocketmq-client:rocketmq-client-4.8:testing") +include(":instrumentation:rocketmq:rocketmq-client:rocketmq-client-5.0:javaagent") +include(":instrumentation:rocketmq:rocketmq-client:rocketmq-client-5.0:testing") +include(":instrumentation:runtime-telemetry:runtime-telemetry-java8:javaagent") +include(":instrumentation:runtime-telemetry:runtime-telemetry-java8:library") +include(":instrumentation:runtime-telemetry:runtime-telemetry-java8:testing") +include(":instrumentation:runtime-telemetry:runtime-telemetry-java17:javaagent") +include(":instrumentation:runtime-telemetry:runtime-telemetry-java17:library") +include(":instrumentation:rxjava:rxjava-1.0:library") +include(":instrumentation:rxjava:rxjava-2.0:javaagent") +include(":instrumentation:rxjava:rxjava-2.0:library") +include(":instrumentation:rxjava:rxjava-2.0:testing") +include(":instrumentation:rxjava:rxjava-3.0:javaagent") +include(":instrumentation:rxjava:rxjava-3.0:library") +include(":instrumentation:rxjava:rxjava-3.1.1:javaagent") +include(":instrumentation:rxjava:rxjava-3.1.1:library") +include(":instrumentation:rxjava:rxjava-3-common:library") +include(":instrumentation:rxjava:rxjava-3-common:testing") +include(":instrumentation:scala-fork-join-2.8:javaagent") +include(":instrumentation:servlet:servlet-2.2:javaagent") +include(":instrumentation:servlet:servlet-3.0:javaagent") +include(":instrumentation:servlet:servlet-3.0:javaagent-unit-tests") +include(":instrumentation:servlet:servlet-5.0:javaagent") +include(":instrumentation:servlet:servlet-5.0:javaagent-unit-tests") +include(":instrumentation:servlet:servlet-5.0:jetty12-testing") +include(":instrumentation:servlet:servlet-5.0:testing") +include(":instrumentation:servlet:servlet-common:bootstrap") +include(":instrumentation:servlet:servlet-common:javaagent") +include(":instrumentation:servlet:servlet-javax-common:javaagent") +include(":instrumentation:spark-2.3:javaagent") +include(":instrumentation:spring:spring-batch-3.0:javaagent") +include(":instrumentation:spring:spring-boot-actuator-autoconfigure-2.0:javaagent") +include(":instrumentation:spring:spring-boot-autoconfigure") +include(":instrumentation:spring:spring-boot-resources:javaagent") +include(":instrumentation:spring:spring-boot-resources:javaagent-unit-tests") +include(":instrumentation:spring:spring-cloud-gateway:spring-cloud-gateway-2.0:javaagent") +include(":instrumentation:spring:spring-cloud-gateway:spring-cloud-gateway-2.2:testing") +include(":instrumentation:spring:spring-cloud-gateway:spring-cloud-gateway-common:testing") +include(":instrumentation:spring:spring-core-2.0:javaagent") +include(":instrumentation:spring:spring-data:spring-data-1.8:javaagent") +include(":instrumentation:spring:spring-data:spring-data-3.0:testing") +include(":instrumentation:spring:spring-data:spring-data-common:testing") +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-jms:spring-jms-2.0:javaagent") +include(":instrumentation:spring:spring-jms:spring-jms-2.0:testing") +include(":instrumentation:spring:spring-jms:spring-jms-6.0:javaagent") +include(":instrumentation:spring:spring-kafka-2.7:javaagent") +include(":instrumentation:spring:spring-kafka-2.7:library") +include(":instrumentation:spring:spring-kafka-2.7:testing") +include(":instrumentation:spring:spring-rabbit-1.0:javaagent") +include(":instrumentation:spring:spring-rmi-4.0:javaagent") +include(":instrumentation:spring:spring-scheduling-3.1:bootstrap") +include(":instrumentation:spring:spring-scheduling-3.1:javaagent") +include(":instrumentation:spring:spring-security-config-6.0:javaagent") +include(":instrumentation:spring:spring-security-config-6.0:library") +include(":instrumentation:spring:spring-web:spring-web-3.1:javaagent") +include(":instrumentation:spring:spring-web:spring-web-3.1:library") +include(":instrumentation:spring:spring-web:spring-web-3.1:testing") +include(":instrumentation:spring:spring-web:spring-web-6.0:javaagent") +include(":instrumentation:spring:spring-webflux:spring-webflux-5.0:javaagent") +include(":instrumentation:spring:spring-webflux:spring-webflux-5.0:testing") +include(":instrumentation:spring:spring-webflux:spring-webflux-5.3:library") +include(":instrumentation:spring:spring-webflux:spring-webflux-5.3:testing") +include(":instrumentation:spring:spring-webmvc:spring-webmvc-3.1:javaagent") +include(":instrumentation:spring:spring-webmvc:spring-webmvc-3.1:wildfly-testing") +include(":instrumentation:spring:spring-webmvc:spring-webmvc-5.3:library") +include(":instrumentation:spring:spring-webmvc:spring-webmvc-6.0:javaagent") +include(":instrumentation:spring:spring-webmvc:spring-webmvc-6.0:library") +include(":instrumentation:spring:spring-webmvc:spring-webmvc-common:javaagent") +include(":instrumentation:spring:spring-webmvc:spring-webmvc-common:testing") +include(":instrumentation:spring:spring-ws-2.0:javaagent") +include(":instrumentation:spring:starters:spring-boot-starter") +include(":instrumentation:spring:starters:zipkin-spring-boot-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:tomcat:tomcat-jdbc") +include(":instrumentation:twilio-6.6:javaagent") +include(":instrumentation:undertow-1.4:bootstrap") +include(":instrumentation:undertow-1.4:javaagent") +include(":instrumentation:vaadin-14.2:javaagent") +include(":instrumentation:vaadin-14.2:testing") +include(":instrumentation:vertx:vertx-http-client:vertx-http-client-3.0:javaagent") +include(":instrumentation:vertx:vertx-http-client:vertx-http-client-4.0:javaagent") +include(":instrumentation:vertx:vertx-http-client:vertx-http-client-common:javaagent") +include(":instrumentation:vertx:vertx-kafka-client-3.6:javaagent") +include(":instrumentation:vertx:vertx-kafka-client-3.6:testing") +include(":instrumentation:vertx:vertx-redis-client-4.0:javaagent") +include(":instrumentation:vertx:vertx-rx-java-3.5:javaagent") +include(":instrumentation:vertx:vertx-sql-client-4.0:javaagent") +include(":instrumentation:vertx:vertx-web-3.0:javaagent") +include(":instrumentation:vertx:vertx-web-3.0:testing") +include(":instrumentation:vibur-dbcp-11.0:javaagent") +include(":instrumentation:vibur-dbcp-11.0:library") +include(":instrumentation:vibur-dbcp-11.0:testing") +include(":instrumentation:wicket-8.0:common-testing") +include(":instrumentation:wicket-8.0:javaagent") +include(":instrumentation:wicket-8.0:wicket8-testing") +include(":instrumentation:wicket-8.0:wicket10-testing") +include(":instrumentation:xxl-job:xxl-job-1.9.2:javaagent") +include(":instrumentation:xxl-job:xxl-job-2.1.2:javaagent") +include(":instrumentation:xxl-job:xxl-job-2.3.0:javaagent") +include(":instrumentation:xxl-job:xxl-job-common:javaagent") +include(":instrumentation:xxl-job:xxl-job-common:testing") +include(":instrumentation:zio:zio-2.0:javaagent") // benchmark include(":benchmark-overhead-jmh") include(":benchmark-jfr-analyzer") - -// this effectively hides the submodule from dependabot because dependabot only regex parses gradle -// files looking for certain patterns -fun hideFromDependabot(projectPath: String) { - include(projectPath) -} diff --git a/smoke-tests-otel-starter/spring-boot-2/build.gradle.kts b/smoke-tests-otel-starter/spring-boot-2/build.gradle.kts new file mode 100644 index 000000000000..9fe628dd698b --- /dev/null +++ b/smoke-tests-otel-starter/spring-boot-2/build.gradle.kts @@ -0,0 +1,36 @@ +plugins { + id("otel.java-conventions") + alias(springBoot2.plugins.versions) +} + +description = "smoke-tests-otel-starter-spring-boot-2" + +dependencies { + implementation("org.springframework.boot:spring-boot-starter-web") + implementation("org.springframework.boot:spring-boot-starter-data-jdbc") + runtimeOnly("com.h2database:h2") + implementation("org.apache.commons:commons-dbcp2") + implementation("org.springframework.kafka:spring-kafka") + implementation("org.springframework.boot:spring-boot-starter-data-mongodb") + implementation("org.springframework.boot:spring-boot-starter-aop") + implementation(platform(org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES)) + + implementation(project(":smoke-tests-otel-starter:spring-boot-common")) + + testImplementation("org.testcontainers:junit-jupiter") + testImplementation("org.testcontainers:kafka") + testImplementation("org.testcontainers:mongodb") + testImplementation("org.springframework.boot:spring-boot-starter-test") +} + +springBoot { + mainClass = "io.opentelemetry.spring.smoketest.OtelSpringStarterSmokeTestApplication" +} + +configurations.configureEach { + resolutionStrategy { + // our dependency management pins to a version that is not compatible with spring boot 2.7 + force("ch.qos.logback:logback-classic:1.2.13") + force("org.slf4j:slf4j-api:1.7.36") + } +} diff --git a/smoke-tests-otel-starter/spring-boot-2/src/main/java/io/opentelemetry/spring/smoketest/OtelSpringStarterSmokeTestApplication.java b/smoke-tests-otel-starter/spring-boot-2/src/main/java/io/opentelemetry/spring/smoketest/OtelSpringStarterSmokeTestApplication.java new file mode 100644 index 000000000000..0a43366e77b7 --- /dev/null +++ b/smoke-tests-otel-starter/spring-boot-2/src/main/java/io/opentelemetry/spring/smoketest/OtelSpringStarterSmokeTestApplication.java @@ -0,0 +1,19 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.spring.smoketest; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class OtelSpringStarterSmokeTestApplication { + + public OtelSpringStarterSmokeTestApplication() {} + + public static void main(String[] args) { + SpringApplication.run(OtelSpringStarterSmokeTestApplication.class); + } +} diff --git a/smoke-tests-otel-starter/spring-boot-2/src/test/java/io/opentelemetry/spring/smoketest/KafkaSpringStarterSmokeTest.java b/smoke-tests-otel-starter/spring-boot-2/src/test/java/io/opentelemetry/spring/smoketest/KafkaSpringStarterSmokeTest.java new file mode 100644 index 000000000000..cf3fd36ffe98 --- /dev/null +++ b/smoke-tests-otel-starter/spring-boot-2/src/test/java/io/opentelemetry/spring/smoketest/KafkaSpringStarterSmokeTest.java @@ -0,0 +1,11 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.spring.smoketest; + +import org.junit.jupiter.api.condition.DisabledInNativeImage; + +@DisabledInNativeImage // See GraalVmNativeKafkaSpringStarterSmokeTest for the GraalVM native test +public class KafkaSpringStarterSmokeTest extends AbstractJvmKafkaSpringStarterSmokeTest {} diff --git a/smoke-tests-otel-starter/spring-boot-2/src/test/java/io/opentelemetry/spring/smoketest/MongoSpringStarterSmokeTest.java b/smoke-tests-otel-starter/spring-boot-2/src/test/java/io/opentelemetry/spring/smoketest/MongoSpringStarterSmokeTest.java new file mode 100644 index 000000000000..ff24abe1c0a6 --- /dev/null +++ b/smoke-tests-otel-starter/spring-boot-2/src/test/java/io/opentelemetry/spring/smoketest/MongoSpringStarterSmokeTest.java @@ -0,0 +1,11 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.spring.smoketest; + +import org.junit.jupiter.api.condition.DisabledInNativeImage; + +@DisabledInNativeImage // See GraalVmNativeMongodbSpringStarterSmokeTest for the GraalVM native test +public class MongoSpringStarterSmokeTest extends AbstractJvmMongodbSpringStarterSmokeTest {} diff --git a/smoke-tests-otel-starter/spring-boot-2/src/test/java/io/opentelemetry/spring/smoketest/OtelSpringStarterSmokeTest.java b/smoke-tests-otel-starter/spring-boot-2/src/test/java/io/opentelemetry/spring/smoketest/OtelSpringStarterSmokeTest.java new file mode 100644 index 000000000000..315e8155413b --- /dev/null +++ b/smoke-tests-otel-starter/spring-boot-2/src/test/java/io/opentelemetry/spring/smoketest/OtelSpringStarterSmokeTest.java @@ -0,0 +1,21 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.spring.smoketest; + +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest( + classes = { + OtelSpringStarterSmokeTestApplication.class, + AbstractOtelSpringStarterSmokeTest.TestConfiguration.class, + SpringSmokeOtelConfiguration.class + }, + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + properties = { + // The headers are simply set here to make sure that headers can be parsed + "otel.exporter.otlp.headers.c=3" + }) +class OtelSpringStarterSmokeTest extends AbstractOtelSpringStarterSmokeTest {} diff --git a/smoke-tests-otel-starter/spring-boot-3.2/build.gradle.kts b/smoke-tests-otel-starter/spring-boot-3.2/build.gradle.kts new file mode 100644 index 000000000000..8b125966863b --- /dev/null +++ b/smoke-tests-otel-starter/spring-boot-3.2/build.gradle.kts @@ -0,0 +1,77 @@ +plugins { + id("otel.java-conventions") + alias(springBoot32.plugins.versions) + id("org.graalvm.buildtools.native") +} + +description = "smoke-tests-otel-starter-spring-boot-3.2" + +otelJava { + minJavaVersionSupported.set(JavaVersion.VERSION_17) +} + +dependencies { + implementation("org.springframework.boot:spring-boot-starter-web") + implementation("org.springframework.boot:spring-boot-starter-data-jdbc") + runtimeOnly("com.h2database:h2") + implementation("org.apache.commons:commons-dbcp2") + implementation("org.springframework.kafka:spring-kafka") + implementation("org.springframework.boot:spring-boot-starter-data-mongodb") + implementation("org.springframework.boot:spring-boot-starter-aop") + implementation(platform(org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES)) + + implementation(project(":smoke-tests-otel-starter:spring-boot-common")) + testImplementation("org.springframework.boot:spring-boot-starter-test") +} + +springBoot { + mainClass = "io.opentelemetry.spring.smoketest.OtelSpringStarterSmokeTestApplication" +} + +tasks { + compileAotJava { + with(options) { + compilerArgs.add("-Xlint:-deprecation,-unchecked,none") + // To disable warnings/failure coming from the Java compiler during the Spring AOT processing + // -deprecation,-unchecked and none are required (none is not enough) + } + } + compileAotTestJava { + with(options) { + compilerArgs.add("-Xlint:-deprecation,-unchecked,none") + // To disable warnings/failure coming from the Java compiler during the Spring AOT processing + // -deprecation,-unchecked and none are required (none is not enough) + } + } + checkstyleAot { + isEnabled = false + } + checkstyleAotTest { + isEnabled = false + } +} + +// To be able to execute the tests as GraalVM native executables +configurations.configureEach { + exclude("org.apache.groovy", "groovy") + exclude("org.apache.groovy", "groovy-json") + exclude("org.spockframework", "spock-core") +} + +graalvmNative { + binaries.all { + // Workaround for https://github.com/junit-team/junit5/issues/3405 + buildArgs.add("--initialize-at-build-time=org.junit.platform.launcher.core.LauncherConfig") + buildArgs.add("--initialize-at-build-time=org.junit.jupiter.engine.config.InstantiatingConfigurationParameterConverter") + } + + // See https://github.com/graalvm/native-build-tools/issues/572 + metadataRepository { + enabled.set(false) + } + + tasks.test { + useJUnitPlatform() + setForkEvery(1) + } +} diff --git a/smoke-tests-otel-starter/spring-boot-3.2/src/main/java/io/opentelemetry/spring/smoketest/OtelSpringStarterSmokeTestApplication.java b/smoke-tests-otel-starter/spring-boot-3.2/src/main/java/io/opentelemetry/spring/smoketest/OtelSpringStarterSmokeTestApplication.java new file mode 100644 index 000000000000..a06749118064 --- /dev/null +++ b/smoke-tests-otel-starter/spring-boot-3.2/src/main/java/io/opentelemetry/spring/smoketest/OtelSpringStarterSmokeTestApplication.java @@ -0,0 +1,21 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.spring.smoketest; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.ImportRuntimeHints; + +@SpringBootApplication +@ImportRuntimeHints(RuntimeHints.class) +public class OtelSpringStarterSmokeTestApplication { + + public OtelSpringStarterSmokeTestApplication() {} + + public static void main(String[] args) { + SpringApplication.run(OtelSpringStarterSmokeTestApplication.class); + } +} diff --git a/smoke-tests-otel-starter/spring-boot-3.2/src/main/java/io/opentelemetry/spring/smoketest/RuntimeHints.java b/smoke-tests-otel-starter/spring-boot-3.2/src/main/java/io/opentelemetry/spring/smoketest/RuntimeHints.java new file mode 100644 index 000000000000..ab181ee823d5 --- /dev/null +++ b/smoke-tests-otel-starter/spring-boot-3.2/src/main/java/io/opentelemetry/spring/smoketest/RuntimeHints.java @@ -0,0 +1,31 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.spring.smoketest; + +import org.springframework.aot.hint.MemberCategory; +import org.springframework.aot.hint.RuntimeHintsRegistrar; +import org.springframework.aot.hint.TypeReference; + +// Necessary for GraalVM native test +public class RuntimeHints implements RuntimeHintsRegistrar { + + @Override + public void registerHints( + org.springframework.aot.hint.RuntimeHints hints, ClassLoader classLoader) { + hints.resources().registerResourceBundle("org.apache.commons.dbcp2.LocalStrings"); + + // To avoid Spring native issue with MongoDB: java.lang.ClassNotFoundException: + // org.springframework.data.mongodb.core.aggregation.AggregationOperation + hints + .reflection() + .registerType( + TypeReference.of( + "org.springframework.data.mongodb.core.aggregation.AggregationOperation"), + hint -> { + hint.withMembers(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS); + }); + } +} diff --git a/smoke-tests-otel-starter/spring-boot-3.2/src/test/java/io/opentelemetry/spring/smoketest/OtelSpringStarterSmokeTest.java b/smoke-tests-otel-starter/spring-boot-3.2/src/test/java/io/opentelemetry/spring/smoketest/OtelSpringStarterSmokeTest.java new file mode 100644 index 000000000000..f01bafb6e22e --- /dev/null +++ b/smoke-tests-otel-starter/spring-boot-3.2/src/test/java/io/opentelemetry/spring/smoketest/OtelSpringStarterSmokeTest.java @@ -0,0 +1,58 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.spring.smoketest; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.sdk.testing.assertj.TraceAssert; +import io.opentelemetry.semconv.HttpAttributes; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.web.client.RestClient; + +@SpringBootTest( + classes = { + OtelSpringStarterSmokeTestApplication.class, + AbstractOtelSpringStarterSmokeTest.TestConfiguration.class, + SpringSmokeOtelConfiguration.class + }, + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +class OtelSpringStarterSmokeTest extends AbstractSpringStarterSmokeTest { + + @Autowired RestClient.Builder restClientBuilder; + @LocalServerPort private int port; + + @Test + void restClient() { + testing.clearAllExportedData(); + + RestClient client = restClientBuilder.baseUrl("http://localhost:" + port).build(); + assertThat( + client + .get() + .uri(OtelSpringStarterSmokeTestController.PING) + .retrieve() + .body(String.class)) + .isEqualTo("pong"); + + if (System.getProperty("org.graalvm.nativeimage.imagecode") != null) { + // ignore the trace for creating the db table + testing.waitAndAssertTraces(trace -> {}, OtelSpringStarterSmokeTest::assertClient); + } else { + testing.waitAndAssertTraces(OtelSpringStarterSmokeTest::assertClient); + } + } + + private static void assertClient(TraceAssert traceAssert) { + traceAssert.hasSpansSatisfyingExactly( + span -> HttpSpanDataAssert.create(span).assertClientGetRequest("/ping"), + span -> span.hasKind(SpanKind.SERVER).hasAttribute(HttpAttributes.HTTP_ROUTE, "/ping"), + span -> withSpanAssert(span)); + } +} diff --git a/smoke-tests-otel-starter/spring-boot-3/build.gradle.kts b/smoke-tests-otel-starter/spring-boot-3/build.gradle.kts new file mode 100644 index 000000000000..78e01e6685e1 --- /dev/null +++ b/smoke-tests-otel-starter/spring-boot-3/build.gradle.kts @@ -0,0 +1,80 @@ +plugins { + id("otel.java-conventions") + alias(springBoot31.plugins.versions) + id("org.graalvm.buildtools.native") +} + +description = "smoke-tests-otel-starter-spring-boot-3" + +otelJava { + minJavaVersionSupported.set(JavaVersion.VERSION_17) +} + +dependencies { + implementation("org.springframework.boot:spring-boot-starter-web") + implementation("org.springframework.boot:spring-boot-starter-data-jdbc") + runtimeOnly("com.h2database:h2") + implementation("org.apache.commons:commons-dbcp2") + implementation("org.springframework.kafka:spring-kafka") + implementation("org.springframework.boot:spring-boot-starter-data-mongodb") + implementation("org.springframework.boot:spring-boot-starter-aop") + implementation(platform(org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES)) + + implementation(project(":smoke-tests-otel-starter:spring-boot-common")) + testImplementation("org.testcontainers:junit-jupiter") + testImplementation("org.testcontainers:kafka") + testImplementation("org.testcontainers:mongodb") + testImplementation("org.springframework.boot:spring-boot-starter-test") +} + +springBoot { + mainClass = "io.opentelemetry.spring.smoketest.OtelSpringStarterSmokeTestApplication" +} + +tasks { + compileAotJava { + with(options) { + compilerArgs.add("-Xlint:-deprecation,-unchecked,none") + // To disable warnings/failure coming from the Java compiler during the Spring AOT processing + // -deprecation,-unchecked and none are required (none is not enough) + } + } + compileAotTestJava { + with(options) { + compilerArgs.add("-Xlint:-deprecation,-unchecked,none") + // To disable warnings/failure coming from the Java compiler during the Spring AOT processing + // -deprecation,-unchecked and none are required (none is not enough) + } + } + checkstyleAot { + isEnabled = false + } + checkstyleAotTest { + isEnabled = false + } +} + +// To be able to execute the tests as GraalVM native executables +configurations.configureEach { + exclude("org.apache.groovy", "groovy") + exclude("org.apache.groovy", "groovy-json") + exclude("org.spockframework", "spock-core") +} + +graalvmNative { + binaries.all { + // Workaround for https://github.com/junit-team/junit5/issues/3405 + buildArgs.add("--initialize-at-build-time=org.junit.platform.launcher.core.LauncherConfig") + buildArgs.add("--initialize-at-build-time=org.junit.jupiter.engine.config.InstantiatingConfigurationParameterConverter") + } + + // See https://github.com/graalvm/native-build-tools/issues/572 + metadataRepository { + enabled.set(false) + } + + tasks.test { + useJUnitPlatform() + setForkEvery(1) + } +} diff --git a/smoke-tests-otel-starter/spring-boot-3/src/main/java/io/opentelemetry/spring/smoketest/OtelSpringStarterSmokeTestApplication.java b/smoke-tests-otel-starter/spring-boot-3/src/main/java/io/opentelemetry/spring/smoketest/OtelSpringStarterSmokeTestApplication.java new file mode 100644 index 000000000000..a06749118064 --- /dev/null +++ b/smoke-tests-otel-starter/spring-boot-3/src/main/java/io/opentelemetry/spring/smoketest/OtelSpringStarterSmokeTestApplication.java @@ -0,0 +1,21 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.spring.smoketest; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.ImportRuntimeHints; + +@SpringBootApplication +@ImportRuntimeHints(RuntimeHints.class) +public class OtelSpringStarterSmokeTestApplication { + + public OtelSpringStarterSmokeTestApplication() {} + + public static void main(String[] args) { + SpringApplication.run(OtelSpringStarterSmokeTestApplication.class); + } +} diff --git a/smoke-tests-otel-starter/spring-boot-3/src/main/java/io/opentelemetry/spring/smoketest/RuntimeHints.java b/smoke-tests-otel-starter/spring-boot-3/src/main/java/io/opentelemetry/spring/smoketest/RuntimeHints.java new file mode 100644 index 000000000000..85da65c58417 --- /dev/null +++ b/smoke-tests-otel-starter/spring-boot-3/src/main/java/io/opentelemetry/spring/smoketest/RuntimeHints.java @@ -0,0 +1,29 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.spring.smoketest; + +import org.springframework.aot.hint.MemberCategory; +import org.springframework.aot.hint.RuntimeHintsRegistrar; +import org.springframework.aot.hint.TypeReference; + +// Necessary for GraalVM native test +public class RuntimeHints implements RuntimeHintsRegistrar { + + @Override + public void registerHints( + org.springframework.aot.hint.RuntimeHints hints, ClassLoader classLoader) { + hints.resources().registerResourceBundle("org.apache.commons.dbcp2.LocalStrings"); + + // To avoid Spring native issue with MongoDB: java.lang.ClassNotFoundException: + // org.springframework.data.mongodb.core.aggregation.AggregationOperation + hints + .reflection() + .registerType( + TypeReference.of( + "org.springframework.data.mongodb.core.aggregation.AggregationOperation"), + hint -> hint.withMembers(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS)); + } +} diff --git a/smoke-tests-otel-starter/spring-boot-3/src/test/java/io/opentelemetry/spring/smoketest/GraalVmNativeKafkaSpringStarterSmokeTest.java b/smoke-tests-otel-starter/spring-boot-3/src/test/java/io/opentelemetry/spring/smoketest/GraalVmNativeKafkaSpringStarterSmokeTest.java new file mode 100644 index 000000000000..b64e19ce8c8c --- /dev/null +++ b/smoke-tests-otel-starter/spring-boot-3/src/test/java/io/opentelemetry/spring/smoketest/GraalVmNativeKafkaSpringStarterSmokeTest.java @@ -0,0 +1,32 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.spring.smoketest; + +import org.junit.jupiter.api.condition.EnabledInNativeImage; +import org.springframework.boot.test.context.SpringBootTest; + +/** + * GraalVM native image doesn't support Testcontainers in our case, so the docker container is + * started manually before running the tests. + * + *

      In other cases, it does work, e.g. here, + * it's not yet clear why it doesn't work in our case. + * + *

      In CI, this is done in reusable-native-tests.yml. If you want to run the tests locally, you + * need to start the container manually: see .github/workflows/reusable-native-tests.yml for the + * command. + */ +@SpringBootTest( + classes = { + OtelSpringStarterSmokeTestApplication.class, + SpringSmokeOtelConfiguration.class, + AbstractKafkaSpringStarterSmokeTest.KafkaConfig.class + }, + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@EnabledInNativeImage // see JvmMongodbSpringStarterSmokeTest for the JVM test +@RequiresDockerCompose +public class GraalVmNativeKafkaSpringStarterSmokeTest extends AbstractKafkaSpringStarterSmokeTest {} diff --git a/smoke-tests-otel-starter/spring-boot-3/src/test/java/io/opentelemetry/spring/smoketest/GraalVmNativeMongodbSpringStarterSmokeTest.java b/smoke-tests-otel-starter/spring-boot-3/src/test/java/io/opentelemetry/spring/smoketest/GraalVmNativeMongodbSpringStarterSmokeTest.java new file mode 100644 index 000000000000..18a8a46dda0a --- /dev/null +++ b/smoke-tests-otel-starter/spring-boot-3/src/test/java/io/opentelemetry/spring/smoketest/GraalVmNativeMongodbSpringStarterSmokeTest.java @@ -0,0 +1,29 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.spring.smoketest; + +import org.junit.jupiter.api.condition.EnabledInNativeImage; +import org.springframework.boot.test.context.SpringBootTest; + +/** + * GraalVM native image doesn't support Testcontainers in our case, so the docker container is + * started manually before running the tests. + * + *

      In other cases, it does work, e.g. here, + * it's not yet clear why it doesn't work in our case. + * + *

      In CI, this is done in reusable-native-tests.yml. If you want to run the tests locally, you + * need to start the container manually: see .github/workflows/reusable-native-tests.yml for the + * command. + */ +@SpringBootTest( + classes = {OtelSpringStarterSmokeTestApplication.class, SpringSmokeOtelConfiguration.class}, + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@EnabledInNativeImage // see JvmMongodbSpringStarterSmokeTest for the JVM test +@RequiresDockerCompose +public class GraalVmNativeMongodbSpringStarterSmokeTest + extends AbstractMongodbSpringStarterSmokeTest {} diff --git a/smoke-tests-otel-starter/spring-boot-3/src/test/java/io/opentelemetry/spring/smoketest/KafkaSpringStarterSmokeTest.java b/smoke-tests-otel-starter/spring-boot-3/src/test/java/io/opentelemetry/spring/smoketest/KafkaSpringStarterSmokeTest.java new file mode 100644 index 000000000000..cf3fd36ffe98 --- /dev/null +++ b/smoke-tests-otel-starter/spring-boot-3/src/test/java/io/opentelemetry/spring/smoketest/KafkaSpringStarterSmokeTest.java @@ -0,0 +1,11 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.spring.smoketest; + +import org.junit.jupiter.api.condition.DisabledInNativeImage; + +@DisabledInNativeImage // See GraalVmNativeKafkaSpringStarterSmokeTest for the GraalVM native test +public class KafkaSpringStarterSmokeTest extends AbstractJvmKafkaSpringStarterSmokeTest {} diff --git a/smoke-tests-otel-starter/spring-boot-3/src/test/java/io/opentelemetry/spring/smoketest/MongoSpringStarterSmokeTest.java b/smoke-tests-otel-starter/spring-boot-3/src/test/java/io/opentelemetry/spring/smoketest/MongoSpringStarterSmokeTest.java new file mode 100644 index 000000000000..ff24abe1c0a6 --- /dev/null +++ b/smoke-tests-otel-starter/spring-boot-3/src/test/java/io/opentelemetry/spring/smoketest/MongoSpringStarterSmokeTest.java @@ -0,0 +1,11 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.spring.smoketest; + +import org.junit.jupiter.api.condition.DisabledInNativeImage; + +@DisabledInNativeImage // See GraalVmNativeMongodbSpringStarterSmokeTest for the GraalVM native test +public class MongoSpringStarterSmokeTest extends AbstractJvmMongodbSpringStarterSmokeTest {} diff --git a/smoke-tests-otel-starter/spring-boot-3/src/test/java/io/opentelemetry/spring/smoketest/OtelSpringStarterDisabledSmokeTest.java b/smoke-tests-otel-starter/spring-boot-3/src/test/java/io/opentelemetry/spring/smoketest/OtelSpringStarterDisabledSmokeTest.java new file mode 100644 index 000000000000..29bae87fa73c --- /dev/null +++ b/smoke-tests-otel-starter/spring-boot-3/src/test/java/io/opentelemetry/spring/smoketest/OtelSpringStarterDisabledSmokeTest.java @@ -0,0 +1,26 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.spring.smoketest; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.DisabledInNativeImage; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest( + classes = { + OtelSpringStarterSmokeTestApplication.class, + AbstractOtelSpringStarterSmokeTest.TestConfiguration.class + }, + properties = {"otel.sdk.disabled=true"}) +@DisabledInNativeImage // Without this the native tests in the OtelSpringStarterSmokeTest class will +// fail with org.h2.jdbc.JdbcSQLSyntaxErrorException: Table "CUSTOMER" already exists +class OtelSpringStarterDisabledSmokeTest { + + @Test + void shouldStartApplication() { + // make sure we can still start the application with the disabled property + } +} diff --git a/smoke-tests-otel-starter/spring-boot-3/src/test/java/io/opentelemetry/spring/smoketest/OtelSpringStarterSmokeTest.java b/smoke-tests-otel-starter/spring-boot-3/src/test/java/io/opentelemetry/spring/smoketest/OtelSpringStarterSmokeTest.java new file mode 100644 index 000000000000..315e8155413b --- /dev/null +++ b/smoke-tests-otel-starter/spring-boot-3/src/test/java/io/opentelemetry/spring/smoketest/OtelSpringStarterSmokeTest.java @@ -0,0 +1,21 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.spring.smoketest; + +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest( + classes = { + OtelSpringStarterSmokeTestApplication.class, + AbstractOtelSpringStarterSmokeTest.TestConfiguration.class, + SpringSmokeOtelConfiguration.class + }, + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + properties = { + // The headers are simply set here to make sure that headers can be parsed + "otel.exporter.otlp.headers.c=3" + }) +class OtelSpringStarterSmokeTest extends AbstractOtelSpringStarterSmokeTest {} diff --git a/smoke-tests-otel-starter/spring-boot-3/src/test/java/io/opentelemetry/spring/smoketest/OtelSpringStarterUserDataSourceBeanTest.java b/smoke-tests-otel-starter/spring-boot-3/src/test/java/io/opentelemetry/spring/smoketest/OtelSpringStarterUserDataSourceBeanTest.java new file mode 100644 index 000000000000..023fcf68c89c --- /dev/null +++ b/smoke-tests-otel-starter/spring-boot-3/src/test/java/io/opentelemetry/spring/smoketest/OtelSpringStarterUserDataSourceBeanTest.java @@ -0,0 +1,14 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.spring.smoketest; + +import org.junit.jupiter.api.condition.DisabledInNativeImage; +import org.springframework.test.context.ActiveProfiles; + +@DisabledInNativeImage // Spring native does not support the profile setting at runtime: +// https://docs.spring.io/spring-boot/docs/current/reference/html/howto.html#howto.aot.conditions +@ActiveProfiles(value = "user-has-defined-datasource-bean") +class OtelSpringStarterUserDataSourceBeanTest extends OtelSpringStarterSmokeTest {} diff --git a/smoke-tests-otel-starter/spring-boot-common/build.gradle.kts b/smoke-tests-otel-starter/spring-boot-common/build.gradle.kts new file mode 100644 index 000000000000..1e4df320a25f --- /dev/null +++ b/smoke-tests-otel-starter/spring-boot-common/build.gradle.kts @@ -0,0 +1,32 @@ +import org.springframework.boot.gradle.tasks.bundling.BootJar + +plugins { + id("otel.java-conventions") + id("org.springframework.boot") version "2.6.15" +} + +description = "smoke-tests-otel-starter-spring-boot-common" + +dependencies { + // spring dependencies are compile only to enable testing against different versions of spring + compileOnly(platform(org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES)) + compileOnly("org.springframework.boot:spring-boot-starter-web") + compileOnly("org.springframework.boot:spring-boot-starter-test") + compileOnly("org.springframework.boot:spring-boot-starter-data-jdbc") + compileOnly("org.apache.commons:commons-dbcp2") + compileOnly("org.springframework.kafka:spring-kafka") + compileOnly("org.springframework.boot:spring-boot-starter-data-mongodb") + compileOnly("org.testcontainers:junit-jupiter") + compileOnly("org.testcontainers:kafka") + compileOnly("org.testcontainers:mongodb") + compileOnly("org.springframework.boot:spring-boot-starter-aop") + + api(project(":smoke-tests-otel-starter:spring-smoke-testing")) + + implementation("io.opentelemetry:opentelemetry-extension-trace-propagators") + implementation(project(":instrumentation:spring:starters:spring-boot-starter")) +} + +tasks.withType { + enabled = false +} diff --git a/smoke-tests-otel-starter/spring-boot-common/src/main/java/io/opentelemetry/spring/smoketest/AbstractJvmKafkaSpringStarterSmokeTest.java b/smoke-tests-otel-starter/spring-boot-common/src/main/java/io/opentelemetry/spring/smoketest/AbstractJvmKafkaSpringStarterSmokeTest.java new file mode 100644 index 000000000000..73a801d96365 --- /dev/null +++ b/smoke-tests-otel-starter/spring-boot-common/src/main/java/io/opentelemetry/spring/smoketest/AbstractJvmKafkaSpringStarterSmokeTest.java @@ -0,0 +1,77 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.spring.smoketest; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.instrumentation.spring.autoconfigure.OpenTelemetryAutoConfiguration; +import io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.kafka.KafkaInstrumentationAutoConfiguration; +import java.time.Duration; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.kafka.KafkaAutoConfiguration; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.kafka.core.KafkaTemplate; +import org.testcontainers.containers.KafkaContainer; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.utility.DockerImageName; + +/** Spring has a test container integration, but that doesn't work for Spring Boot 2 */ +public class AbstractJvmKafkaSpringStarterSmokeTest extends AbstractKafkaSpringStarterSmokeTest { + static KafkaContainer kafka; + + private ApplicationContextRunner contextRunner; + + @BeforeAll + static void setUpKafka() { + kafka = + new KafkaContainer(DockerImageName.parse("confluentinc/cp-kafka:7.7.0")) + .withEnv("KAFKA_HEAP_OPTS", "-Xmx256m") + .waitingFor(Wait.forLogMessage(".*started \\(kafka.server.KafkaServer\\).*", 1)) + .withStartupTimeout(Duration.ofMinutes(1)); + kafka.start(); + } + + @AfterAll + static void tearDownKafka() { + kafka.stop(); + } + + @BeforeEach + void setUpContext() { + contextRunner = + new ApplicationContextRunner() + .withAllowBeanDefinitionOverriding(true) + .withConfiguration( + AutoConfigurations.of( + OpenTelemetryAutoConfiguration.class, + SpringSmokeOtelConfiguration.class, + KafkaAutoConfiguration.class, + KafkaInstrumentationAutoConfiguration.class, + KafkaConfig.class)) + .withPropertyValues( + "otel.instrumentation.kafka.experimental-span-attributes=true", + "spring.kafka.bootstrap-servers=" + kafka.getBootstrapServers(), + "spring.kafka.consumer.auto-offset-reset=earliest", + "spring.kafka.consumer.linger-ms=10", + "spring.kafka.listener.idle-between-polls=1000", + "spring.kafka.producer.transaction-id-prefix=test-"); + } + + @SuppressWarnings("unchecked") + @Override + @Test + void shouldInstrumentProducerAndConsumer() { + contextRunner.run( + applicationContext -> { + testing = new SpringSmokeTestRunner(applicationContext.getBean(OpenTelemetry.class)); + kafkaTemplate = applicationContext.getBean(KafkaTemplate.class); + super.shouldInstrumentProducerAndConsumer(); + }); + } +} diff --git a/smoke-tests-otel-starter/spring-boot-common/src/main/java/io/opentelemetry/spring/smoketest/AbstractJvmMongodbSpringStarterSmokeTest.java b/smoke-tests-otel-starter/spring-boot-common/src/main/java/io/opentelemetry/spring/smoketest/AbstractJvmMongodbSpringStarterSmokeTest.java new file mode 100644 index 000000000000..5915f8fca138 --- /dev/null +++ b/smoke-tests-otel-starter/spring-boot-common/src/main/java/io/opentelemetry/spring/smoketest/AbstractJvmMongodbSpringStarterSmokeTest.java @@ -0,0 +1,65 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.spring.smoketest; + +import com.mongodb.client.MongoClient; +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.instrumentation.spring.autoconfigure.OpenTelemetryAutoConfiguration; +import io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.mongo.MongoClientInstrumentationAutoConfiguration; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.testcontainers.containers.MongoDBContainer; +import org.testcontainers.junit.jupiter.Container; + +/** Spring has a test container integration, but that doesn't work for Spring Boot 2 */ +public class AbstractJvmMongodbSpringStarterSmokeTest + extends AbstractMongodbSpringStarterSmokeTest { + + @Container static MongoDBContainer container; + + private ApplicationContextRunner contextRunner; + + @BeforeAll + static void setUpContainer() { + container = new MongoDBContainer("mongo:4.0"); + container.start(); + } + + @AfterAll + static void tearDownContainer() { + container.stop(); + } + + @BeforeEach + void setUpContext() { + contextRunner = + new ApplicationContextRunner() + .withAllowBeanDefinitionOverriding(true) + .withConfiguration( + AutoConfigurations.of( + OpenTelemetryAutoConfiguration.class, + SpringSmokeOtelConfiguration.class, + MongoAutoConfiguration.class, + MongoClientInstrumentationAutoConfiguration.class)) + .withPropertyValues("spring.data.mongodb.uri=" + container.getReplicaSetUrl()); + } + + @Override + @Test + void mongodb() { + contextRunner.run( + applicationContext -> { + testing = new SpringSmokeTestRunner(applicationContext.getBean(OpenTelemetry.class)); + mongoClient = applicationContext.getBean(MongoClient.class); + super.mongodb(); + }); + } +} diff --git a/smoke-tests-otel-starter/spring-boot-common/src/main/java/io/opentelemetry/spring/smoketest/AbstractKafkaSpringStarterSmokeTest.java b/smoke-tests-otel-starter/spring-boot-common/src/main/java/io/opentelemetry/spring/smoketest/AbstractKafkaSpringStarterSmokeTest.java new file mode 100644 index 000000000000..28a4d8980d9a --- /dev/null +++ b/smoke-tests-otel-starter/spring-boot-common/src/main/java/io/opentelemetry/spring/smoketest/AbstractKafkaSpringStarterSmokeTest.java @@ -0,0 +1,128 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.spring.smoketest; + +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.satisfies; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes; +import org.apache.kafka.clients.admin.NewTopic; +import org.apache.kafka.clients.consumer.ConsumerRecord; +import org.assertj.core.api.AbstractLongAssert; +import org.assertj.core.api.AbstractStringAssert; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.kafka.annotation.KafkaListener; +import org.springframework.kafka.config.TopicBuilder; +import org.springframework.kafka.core.KafkaTemplate; + +abstract class AbstractKafkaSpringStarterSmokeTest extends AbstractSpringStarterSmokeTest { + + @Autowired protected KafkaTemplate kafkaTemplate; + + @Test + void shouldInstrumentProducerAndConsumer() { + testing.runWithSpan( + "producer", + () -> { + kafkaTemplate.executeInTransaction( + ops -> { + // return type is incompatible between Spring Boot 2 and 3 + try { + ops.getClass() + .getDeclaredMethod("send", String.class, Object.class, Object.class) + .invoke(ops, "testTopic", "10", "testSpan"); + } catch (Exception e) { + throw new IllegalStateException(e); + } + return 0; + }); + }); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("producer"), + span -> + span.hasName("testTopic publish") + .hasKind(SpanKind.PRODUCER) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo(MessagingIncubatingAttributes.MESSAGING_SYSTEM, "kafka"), + equalTo( + MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME, + "testTopic"), + equalTo(MessagingIncubatingAttributes.MESSAGING_OPERATION, "publish"), + satisfies( + MessagingIncubatingAttributes.MESSAGING_CLIENT_ID, + stringAssert -> stringAssert.startsWith("producer")), + satisfies( + MessagingIncubatingAttributes.MESSAGING_DESTINATION_PARTITION_ID, + AbstractStringAssert::isNotEmpty), + satisfies( + MessagingIncubatingAttributes.MESSAGING_KAFKA_MESSAGE_OFFSET, + AbstractLongAssert::isNotNegative), + equalTo( + MessagingIncubatingAttributes.MESSAGING_KAFKA_MESSAGE_KEY, "10")), + span -> + span.hasName("testTopic process") + .hasKind(SpanKind.CONSUMER) + .hasParent(trace.getSpan(1)) + .hasAttributesSatisfying( + equalTo(MessagingIncubatingAttributes.MESSAGING_SYSTEM, "kafka"), + equalTo( + MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME, + "testTopic"), + equalTo(MessagingIncubatingAttributes.MESSAGING_OPERATION, "process"), + satisfies( + MessagingIncubatingAttributes.MESSAGING_MESSAGE_BODY_SIZE, + AbstractLongAssert::isNotNegative), + satisfies( + MessagingIncubatingAttributes.MESSAGING_DESTINATION_PARTITION_ID, + AbstractStringAssert::isNotEmpty), + satisfies( + MessagingIncubatingAttributes.MESSAGING_KAFKA_MESSAGE_OFFSET, + AbstractLongAssert::isNotNegative), + equalTo( + MessagingIncubatingAttributes.MESSAGING_KAFKA_MESSAGE_KEY, "10"), + equalTo( + MessagingIncubatingAttributes.MESSAGING_KAFKA_CONSUMER_GROUP, + "testListener"), + satisfies( + AttributeKey.longKey("kafka.record.queue_time_ms"), + AbstractLongAssert::isNotNegative), + satisfies( + MessagingIncubatingAttributes.MESSAGING_CLIENT_ID, + stringAssert -> stringAssert.startsWith("consumer"))), + span -> span.hasName("consumer").hasParent(trace.getSpan(2)))); + } + + @Configuration + public static class KafkaConfig { + + @Autowired OpenTelemetry openTelemetry; + + @Bean + public NewTopic testTopic() { + return TopicBuilder.name("testTopic").partitions(1).replicas(1).build(); + } + + @KafkaListener(id = "testListener", topics = "testTopic") + public void listener(ConsumerRecord record) { + openTelemetry + .getTracer("consumer", "1.0") + .spanBuilder("consumer") + .setSpanKind(SpanKind.CONSUMER) + .startSpan() + .end(); + } + } +} diff --git a/smoke-tests-otel-starter/spring-boot-common/src/main/java/io/opentelemetry/spring/smoketest/AbstractMongodbSpringStarterSmokeTest.java b/smoke-tests-otel-starter/spring-boot-common/src/main/java/io/opentelemetry/spring/smoketest/AbstractMongodbSpringStarterSmokeTest.java new file mode 100644 index 000000000000..60c6baa51089 --- /dev/null +++ b/smoke-tests-otel-starter/spring-boot-common/src/main/java/io/opentelemetry/spring/smoketest/AbstractMongodbSpringStarterSmokeTest.java @@ -0,0 +1,38 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.spring.smoketest; + +import com.mongodb.client.MongoClient; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.semconv.incubating.DbIncubatingAttributes; +import java.util.ArrayList; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +abstract class AbstractMongodbSpringStarterSmokeTest extends AbstractSpringStarterSmokeTest { + + @Autowired protected MongoClient mongoClient; + + @Test + void mongodb() { + testing.runWithSpan( + "server", + () -> { + mongoClient.listDatabaseNames().into(new ArrayList<>()); + }); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("server"), + span -> + span.hasKind(SpanKind.CLIENT) + .hasName("listDatabases admin") + .hasAttribute( + DbIncubatingAttributes.DB_SYSTEM, + DbIncubatingAttributes.DbSystemValues.MONGODB))); + } +} diff --git a/smoke-tests-otel-starter/spring-boot-common/src/main/java/io/opentelemetry/spring/smoketest/AbstractOtelSpringStarterSmokeTest.java b/smoke-tests-otel-starter/spring-boot-common/src/main/java/io/opentelemetry/spring/smoketest/AbstractOtelSpringStarterSmokeTest.java new file mode 100644 index 000000000000..4e802dd3bccb --- /dev/null +++ b/smoke-tests-otel-starter/spring-boot-common/src/main/java/io/opentelemetry/spring/smoketest/AbstractOtelSpringStarterSmokeTest.java @@ -0,0 +1,240 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.spring.smoketest; + +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.satisfies; +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.SpanKind; +import io.opentelemetry.instrumentation.spring.autoconfigure.internal.properties.OtelResourceProperties; +import io.opentelemetry.instrumentation.spring.autoconfigure.internal.properties.OtlpExporterProperties; +import io.opentelemetry.instrumentation.spring.autoconfigure.internal.properties.PropagationProperties; +import io.opentelemetry.instrumentation.spring.autoconfigure.internal.properties.SpringConfigProperties; +import io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizerProvider; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import io.opentelemetry.sdk.autoconfigure.spi.internal.DefaultConfigProperties; +import io.opentelemetry.sdk.logs.data.LogRecordData; +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.semconv.ClientAttributes; +import io.opentelemetry.semconv.HttpAttributes; +import io.opentelemetry.semconv.ServerAttributes; +import io.opentelemetry.semconv.UrlAttributes; +import io.opentelemetry.semconv.incubating.CodeIncubatingAttributes; +import io.opentelemetry.semconv.incubating.DbIncubatingAttributes; +import io.opentelemetry.semconv.incubating.ServiceIncubatingAttributes; +import java.util.Collections; +import java.util.List; +import org.assertj.core.api.AbstractCharSequenceAssert; +import org.assertj.core.api.AbstractIterableAssert; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.event.EventListener; +import org.springframework.core.annotation.Order; +import org.springframework.core.env.Environment; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.web.client.RestTemplate; + +/** + * This test class enforces the order of the tests to make sure that {@link #shouldSendTelemetry()}, + * which asserts the telemetry data from the application startup, is executed first. + */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class AbstractOtelSpringStarterSmokeTest extends AbstractSpringStarterSmokeTest { + + @Autowired private TestRestTemplate testRestTemplate; + + @Autowired private Environment environment; + @Autowired private PropagationProperties propagationProperties; + @Autowired private OtelResourceProperties otelResourceProperties; + @Autowired private OtlpExporterProperties otlpExporterProperties; + @Autowired private RestTemplateBuilder restTemplateBuilder; + @Autowired private JdbcTemplate jdbcTemplate; + + // can't use @LocalServerPort annotation since it moved packages between Spring Boot 2 and 3 + @Value("${local.server.port}") + private int port; + + @Configuration(proxyBeanMethods = false) + static class TestConfiguration { + @Autowired private ObjectProvider jdbcTemplate; + + @EventListener(ApplicationReadyEvent.class) + public void loadData() { + jdbcTemplate + .getObject() + .execute( + "create table customer (id bigint not null, name varchar not null, primary key (id))"); + } + + @Bean + @Order(1) + AutoConfigurationCustomizerProvider hiddenPropagatorCustomizer() { + return customizer -> + customizer.addResourceCustomizer( + (resource, config) -> + resource.merge( + Resource.create( + Attributes.of( + AttributeKey.booleanKey("keyFromResourceCustomizer"), false)))); + } + + @Bean + @Order(2) + AutoConfigurationCustomizerProvider propagatorCustomizer() { + return customizer -> + customizer.addResourceCustomizer( + (resource, config) -> + resource.merge( + Resource.create( + Attributes.of( + AttributeKey.booleanKey("keyFromResourceCustomizer"), true)))); + } + } + + @Test + void propertyConversion() { + ConfigProperties configProperties = + SpringConfigProperties.create( + environment, + otlpExporterProperties, + otelResourceProperties, + propagationProperties, + DefaultConfigProperties.createFromMap( + Collections.singletonMap("otel.exporter.otlp.headers", "a=1,b=2"))); + assertThat(configProperties.getMap("otel.exporter.otlp.headers")) + .containsEntry("a", "1") + .containsEntry("b", "2") + .containsEntry("c", "3"); + assertThat(configProperties.getList("otel.propagators")).containsExactly("b3"); + } + + @Test + @org.junit.jupiter.api.Order(1) + void shouldSendTelemetry() { + testRestTemplate.getForObject(OtelSpringStarterSmokeTestController.PING, String.class); + + // Span + testing.waitAndAssertTraces( + traceAssert -> + traceAssert.hasSpansSatisfyingExactly( + spanDataAssert -> + spanDataAssert + .hasKind(SpanKind.CLIENT) + .hasAttribute( + DbIncubatingAttributes.DB_STATEMENT, + "create table customer (id bigint not null, name varchar not null, primary key (id))")), + traceAssert -> + traceAssert.hasSpansSatisfyingExactly( + clientSpan -> + clientSpan + .hasKind(SpanKind.CLIENT) + .hasAttributesSatisfying( + satisfies( + UrlAttributes.URL_FULL, + stringAssert -> stringAssert.endsWith("/ping")), + equalTo(ServerAttributes.SERVER_ADDRESS, "localhost"), + satisfies( + ServerAttributes.SERVER_PORT, + integerAssert -> integerAssert.isNotZero())), + serverSpan -> + HttpSpanDataAssert.create(serverSpan) + .assertServerGetRequest("/ping") + .hasResourceSatisfying( + r -> + r.hasAttribute( + AttributeKey.booleanKey("keyFromResourceCustomizer"), true) + .hasAttribute( + AttributeKey.stringKey("attributeFromYaml"), "true") + .hasAttribute( + satisfies( + ServiceIncubatingAttributes.SERVICE_INSTANCE_ID, + AbstractCharSequenceAssert::isNotBlank))) + .hasAttributesSatisfying( + equalTo(HttpAttributes.HTTP_REQUEST_METHOD, "GET"), + equalTo(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, 200L), + equalTo(HttpAttributes.HTTP_ROUTE, "/ping"), + equalTo(ServerAttributes.SERVER_ADDRESS, "localhost"), + equalTo(ClientAttributes.CLIENT_ADDRESS, "127.0.0.1"), + satisfies( + ServerAttributes.SERVER_PORT, + integerAssert -> integerAssert.isNotZero())), + span -> withSpanAssert(span))); + + // Metric + testing.waitAndAssertMetrics( + OtelSpringStarterSmokeTestController.METER_SCOPE_NAME, + OtelSpringStarterSmokeTestController.TEST_HISTOGRAM, + AbstractIterableAssert::isNotEmpty); + + // Log + List exportedLogRecords = testing.getExportedLogRecords(); + assertThat(exportedLogRecords).as("No log record exported.").isNotEmpty(); + if (System.getProperty("org.graalvm.nativeimage.imagecode") == null) { + // log records differ in native image mode due to different startup timing + LogRecordData firstLog = exportedLogRecords.get(0); + assertThat(firstLog.getBody().asString()) + .as("Should instrument logs") + .startsWith("Starting ") + .contains(this.getClass().getSimpleName()); + assertThat(firstLog.getAttributes().asMap()) + .as("Should capture code attributes") + .containsEntry( + CodeIncubatingAttributes.CODE_NAMESPACE, + "org.springframework.boot.StartupInfoLogger"); + } + } + + @Test + void databaseQuery() { + testing.clearAllExportedData(); + + testing.runWithSpan( + "server", + () -> { + jdbcTemplate.query( + "select name from customer where id = 1", (rs, rowNum) -> rs.getString("name")); + }); + + // 1 is not replaced by ?, otel.instrumentation.common.db-statement-sanitizer.enabled=false + testing.waitAndAssertTraces( + traceAssert -> + traceAssert.hasSpansSatisfyingExactly( + span -> span.hasName("server"), + span -> span.satisfies(s -> assertThat(s.getName()).endsWith(".getConnection")), + span -> + span.hasKind(SpanKind.CLIENT) + .hasAttribute( + DbIncubatingAttributes.DB_STATEMENT, + "select name from customer where id = 1"))); + } + + @Test + void restTemplate() { + testing.clearAllExportedData(); + + RestTemplate restTemplate = restTemplateBuilder.rootUri("http://localhost:" + port).build(); + restTemplate.getForObject(OtelSpringStarterSmokeTestController.PING, String.class); + testing.waitAndAssertTraces( + traceAssert -> + traceAssert.hasSpansSatisfyingExactly( + span -> HttpSpanDataAssert.create(span).assertClientGetRequest("/ping"), + span -> + span.hasKind(SpanKind.SERVER).hasAttribute(HttpAttributes.HTTP_ROUTE, "/ping"), + span -> withSpanAssert(span))); + } +} diff --git a/smoke-tests-otel-starter/spring-boot-common/src/main/java/io/opentelemetry/spring/smoketest/DatasourceConfig.java b/smoke-tests-otel-starter/spring-boot-common/src/main/java/io/opentelemetry/spring/smoketest/DatasourceConfig.java new file mode 100644 index 000000000000..8788de040b17 --- /dev/null +++ b/smoke-tests-otel-starter/spring-boot-common/src/main/java/io/opentelemetry/spring/smoketest/DatasourceConfig.java @@ -0,0 +1,28 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.spring.smoketest; + +import io.opentelemetry.api.OpenTelemetry; +import javax.sql.DataSource; +import org.apache.commons.dbcp2.BasicDataSource; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; + +@Configuration +@Profile("user-has-defined-datasource-bean") +public class DatasourceConfig { + + @Bean + public DataSource dataSource(OpenTelemetry openTelemetry) { + BasicDataSource dataSource = new BasicDataSource(); + dataSource.setDriverClassName("org.h2.Driver"); + dataSource.setUrl("jdbc:h2:mem:db"); + dataSource.setUsername("username"); + dataSource.setPassword("pwd"); + return dataSource; + } +} diff --git a/smoke-tests-otel-starter/spring-boot-common/src/main/java/io/opentelemetry/spring/smoketest/OtelSpringStarterSmokeTestController.java b/smoke-tests-otel-starter/spring-boot-common/src/main/java/io/opentelemetry/spring/smoketest/OtelSpringStarterSmokeTestController.java new file mode 100644 index 000000000000..7d02c29d5fc1 --- /dev/null +++ b/smoke-tests-otel-starter/spring-boot-common/src/main/java/io/opentelemetry/spring/smoketest/OtelSpringStarterSmokeTestController.java @@ -0,0 +1,38 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.spring.smoketest; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.metrics.LongHistogram; +import io.opentelemetry.api.metrics.Meter; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class OtelSpringStarterSmokeTestController { + + public static final String PING = "/ping"; + public static final String REST_CLIENT = "/rest-client"; + public static final String REST_TEMPLATE = "/rest-template"; + public static final String TEST_HISTOGRAM = "histogram-test-otel-spring-starter"; + public static final String METER_SCOPE_NAME = "scope"; + private final LongHistogram histogram; + private final SpringComponent component; + + public OtelSpringStarterSmokeTestController( + OpenTelemetry openTelemetry, SpringComponent springComponent) { + Meter meter = openTelemetry.getMeter(METER_SCOPE_NAME); + histogram = meter.histogramBuilder(TEST_HISTOGRAM).ofLongs().build(); + this.component = springComponent; + } + + @GetMapping(PING) + public String ping() { + histogram.record(10); + component.withSpanMethod("from-controller"); + return "pong"; + } +} diff --git a/smoke-tests-otel-starter/spring-boot-common/src/main/java/io/opentelemetry/spring/smoketest/SpringComponent.java b/smoke-tests-otel-starter/spring-boot-common/src/main/java/io/opentelemetry/spring/smoketest/SpringComponent.java new file mode 100644 index 000000000000..072b39393b01 --- /dev/null +++ b/smoke-tests-otel-starter/spring-boot-common/src/main/java/io/opentelemetry/spring/smoketest/SpringComponent.java @@ -0,0 +1,18 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.spring.smoketest; + +import io.opentelemetry.instrumentation.annotations.SpanAttribute; +import io.opentelemetry.instrumentation.annotations.WithSpan; +import org.springframework.stereotype.Component; + +@Component +public class SpringComponent { + + @SuppressWarnings("MethodCanBeStatic") + @WithSpan + public void withSpanMethod(@SpanAttribute String paramName) {} +} diff --git a/smoke-tests-otel-starter/spring-boot-common/src/main/resources/META-INF/native-image/reflect-config.json b/smoke-tests-otel-starter/spring-boot-common/src/main/resources/META-INF/native-image/reflect-config.json new file mode 100644 index 000000000000..ab4757f6961c --- /dev/null +++ b/smoke-tests-otel-starter/spring-boot-common/src/main/resources/META-INF/native-image/reflect-config.json @@ -0,0 +1,1248 @@ +[ + { + "condition": { + "typeReachable": "org.apache.commons.pool2.impl.BaseGenericObjectPool" + }, + "methods": [ + { + "name": "", + "parameterTypes": [] + } + ], + "name": "org.apache.commons.pool2.impl.DefaultEvictionPolicy" + } +, + { + "condition": { + "typeReachable": "com.zaxxer.hikari.util.ConcurrentBag" + }, + "name": "[Lcom.zaxxer.hikari.util.ConcurrentBag$IConcurrentBagEntry;" + }, + { + "condition": { + "typeReachable": "com.zaxxer.hikari.pool.PoolEntry" + }, + "name": "[Ljava.sql.Statement;" + }, + { + "condition": { + "typeReachable": "com.zaxxer.hikari.util.FastList" + }, + "name": "[Ljava.sql.Statement;" + }, + { + "condition": { + "typeReachable": "com.zaxxer.hikari.HikariConfig" + }, + "name": "com.zaxxer.hikari.HikariConfig", + "allDeclaredFields": true, + "queryAllPublicMethods": true, + "methods": [ + { + "name": "setAutoCommit", + "parameterTypes": [ + "boolean" + ] + }, + { + "name": "setConnectionTestQuery", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setDataSourceClassName", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setMinimumIdle", + "parameterTypes": [ + "int" + ] + } + ] + }, + { + "condition": { + "typeReachable": "com.zaxxer.hikari.HikariJNDIFactory" + }, + "name": "com.zaxxer.hikari.HikariConfig", + "queryAllPublicMethods": true, + "methods": [ + { + "name": "setDataSourceJNDI", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setDriverClassName", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setJdbcUrl", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setMaxLifetime", + "parameterTypes": [ + "long" + ] + }, + { + "name": "setMaximumPoolSize", + "parameterTypes": [ + "int" + ] + }, + { + "name": "setPassword", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setUsername", + "parameterTypes": [ + "java.lang.String" + ] + } + ] + }, + { + "condition": { + "typeReachable": "com.zaxxer.hikari.hibernate.HikariConfigurationUtil" + }, + "name": "com.zaxxer.hikari.HikariConfig", + "queryAllPublicMethods": true, + "methods": [ + { + "name": "setAutoCommit", + "parameterTypes": [ + "boolean" + ] + }, + { + "name": "setConnectionTestQuery", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setDataSourceClassName", + "parameterTypes": [ + "java.lang.String" + ] + } + ] + }, + { + "condition": { + "typeReachable": "com.zaxxer.hikari.util.PropertyElf" + }, + "name": "com.zaxxer.hikari.HikariConfig", + "methods": [ + { + "name": "getCatalog", + "parameterTypes": [] + }, + { + "name": "getConnectionInitSql", + "parameterTypes": [] + }, + { + "name": "getConnectionTestQuery", + "parameterTypes": [] + }, + { + "name": "getConnectionTimeout", + "parameterTypes": [] + }, + { + "name": "getDataSource", + "parameterTypes": [] + }, + { + "name": "getDataSourceClassName", + "parameterTypes": [] + }, + { + "name": "getDataSourceJNDI", + "parameterTypes": [] + }, + { + "name": "getDataSourceProperties", + "parameterTypes": [] + }, + { + "name": "getDriverClassName", + "parameterTypes": [] + }, + { + "name": "getExceptionOverrideClassName", + "parameterTypes": [] + }, + { + "name": "getHealthCheckProperties", + "parameterTypes": [] + }, + { + "name": "getHealthCheckRegistry", + "parameterTypes": [] + }, + { + "name": "getIdleTimeout", + "parameterTypes": [] + }, + { + "name": "getInitializationFailTimeout", + "parameterTypes": [] + }, + { + "name": "getJdbcUrl", + "parameterTypes": [] + }, + { + "name": "getKeepaliveTime", + "parameterTypes": [] + }, + { + "name": "getLeakDetectionThreshold", + "parameterTypes": [] + }, + { + "name": "getMaxLifetime", + "parameterTypes": [] + }, + { + "name": "getMaximumPoolSize", + "parameterTypes": [] + }, + { + "name": "getMetricRegistry", + "parameterTypes": [] + }, + { + "name": "getMetricsTrackerFactory", + "parameterTypes": [] + }, + { + "name": "getMinimumIdle", + "parameterTypes": [] + }, + { + "name": "getPassword", + "parameterTypes": [] + }, + { + "name": "getPoolName", + "parameterTypes": [] + }, + { + "name": "getScheduledExecutor", + "parameterTypes": [] + }, + { + "name": "getScheduledExecutorService", + "parameterTypes": [] + }, + { + "name": "getSchema", + "parameterTypes": [] + }, + { + "name": "getThreadFactory", + "parameterTypes": [] + }, + { + "name": "getTransactionIsolation", + "parameterTypes": [] + }, + { + "name": "getUsername", + "parameterTypes": [] + }, + { + "name": "getValidationTimeout", + "parameterTypes": [] + }, + { + "name": "isAllowPoolSuspension", + "parameterTypes": [] + }, + { + "name": "isAutoCommit", + "parameterTypes": [] + }, + { + "name": "isInitializationFailFast", + "parameterTypes": [] + }, + { + "name": "isIsolateInternalQueries", + "parameterTypes": [] + }, + { + "name": "isJdbc4ConnectionTest", + "parameterTypes": [] + }, + { + "name": "isReadOnly", + "parameterTypes": [] + }, + { + "name": "isRegisterMbeans", + "parameterTypes": [] + } + ], + "queriedMethods": [ + { + "name": "setAllowPoolSuspension", + "parameterTypes": [ + "boolean" + ] + }, + { + "name": "setAutoCommit", + "parameterTypes": [ + "boolean" + ] + }, + { + "name": "setCatalog", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setConnectionInitSql", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setConnectionTestQuery", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setConnectionTimeout", + "parameterTypes": [ + "long" + ] + }, + { + "name": "setDataSource", + "parameterTypes": [ + "javax.sql.DataSource" + ] + }, + { + "name": "setDataSourceClassName", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setDataSourceJNDI", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setDataSourceProperties", + "parameterTypes": [ + "java.util.Properties" + ] + }, + { + "name": "setDriverClassName", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setExceptionOverrideClassName", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setHealthCheckProperties", + "parameterTypes": [ + "java.util.Properties" + ] + }, + { + "name": "setHealthCheckRegistry", + "parameterTypes": [ + "java.lang.Object" + ] + }, + { + "name": "setIdleTimeout", + "parameterTypes": [ + "long" + ] + }, + { + "name": "setInitializationFailTimeout", + "parameterTypes": [ + "long" + ] + }, + { + "name": "setInitializationFailFast", + "parameterTypes": [ + "boolean" + ] + }, + { + "name": "setIsolateInternalQueries", + "parameterTypes": [ + "boolean" + ] + }, + { + "name": "setJdbc4ConnectionTest", + "parameterTypes": [ + "boolean" + ] + }, + { + "name": "setJdbcUrl", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setKeepaliveTime", + "parameterTypes": [ + "long" + ] + }, + { + "name": "setLeakDetectionThreshold", + "parameterTypes": [ + "long" + ] + }, + { + "name": "setMaxLifetime", + "parameterTypes": [ + "long" + ] + }, + { + "name": "setMaximumPoolSize", + "parameterTypes": [ + "int" + ] + }, + { + "name": "setMetricRegistry", + "parameterTypes": [ + "java.lang.Object" + ] + }, + { + "name": "setMetricsTrackerFactory", + "parameterTypes": [ + "com.zaxxer.hikari.metrics.MetricsTrackerFactory" + ] + }, + { + "name": "setMinimumIdle", + "parameterTypes": [ + "int" + ] + }, + { + "name": "setPassword", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setPoolName", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setReadOnly", + "parameterTypes": [ + "boolean" + ] + }, + { + "name": "setRegisterMbeans", + "parameterTypes": [ + "boolean" + ] + }, + { + "name": "setScheduledExecutor", + "parameterTypes": [ + "java.util.concurrent.ScheduledExecutorService" + ] + }, + { + "name": "setScheduledExecutorService", + "parameterTypes": [ + "java.util.concurrent.ScheduledThreadPoolExecutor" + ] + }, + { + "name": "setSchema", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setThreadFactory", + "parameterTypes": [ + "java.util.concurrent.ThreadFactory" + ] + }, + { + "name": "setTransactionIsolation", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setUsername", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "setValidationTimeout", + "parameterTypes": [ + "long" + ] + } + ] + }, + { + "condition": { + "typeReachable": "com.zaxxer.hikari.util.UtilityElf" + }, + "name": "java.sql.Connection", + "fields": [] + }, + { + "condition": { + "typeReachable": "com.zaxxer.hikari.util.Sequence$Factory" + }, + "name": "java.util.concurrent.atomic.LongAdder" + }, + { + "name": "org.apache.kafka.clients.consumer.CooperativeStickyAssignor", + "condition": { + "typeReachable": "org.apache.kafka.clients.consumer.ConsumerPartitionAssignor" + }, + "methods": [ + { + "name": "", + "parameterTypes": [ + + ] + } + ] + }, + { + "name": "org.apache.kafka.clients.consumer.RangeAssignor", + "condition": { + "typeReachable": "org.apache.kafka.clients.consumer.ConsumerPartitionAssignor" + }, + "methods": [ + { + "name": "", + "parameterTypes": [ + + ] + } + ] + }, + { + "name": "org.apache.kafka.clients.consumer.RoundRobinAssignor", + "condition": { + "typeReachable": "org.apache.kafka.clients.consumer.ConsumerPartitionAssignor" + }, + "methods": [ + { + "name": "", + "parameterTypes": [ + + ] + } + ] + }, + { + "name": "org.apache.kafka.clients.consumer.StickyAssignor", + "condition": { + "typeReachable": "org.apache.kafka.clients.consumer.ConsumerPartitionAssignor" + }, + "methods": [ + { + "name": "", + "parameterTypes": [ + + ] + } + ] + }, + { + "name": "org.apache.kafka.clients.producer.RoundRobinPartitioner", + "condition": { + "typeReachable": "org.apache.kafka.clients.producer.KafkaProducer" + }, + "methods": [ + { + "name": "", + "parameterTypes": [ + + ] + } + ] + }, + { + "name": "org.apache.kafka.common.metrics.JmxReporter", + "condition": { + "typeReachable": "org.apache.kafka.common.utils.Utils" + }, + "methods": [ + { + "name": "", + "parameterTypes": [ + + ] + } + ] + }, + { + "name": "org.apache.kafka.common.security.authenticator.AbstractLogin$DefaultLoginCallbackHandler", + "condition": { + "typeReachable": "org.apache.kafka.common.security.authenticator.LoginManager" + }, + "methods": [ + { + "name": "", + "parameterTypes": [ + + ] + } + ] + }, + { + "name": "org.apache.kafka.common.security.authenticator.DefaultLogin", + "condition": { + "typeReachable": "org.apache.kafka.common.security.authenticator.LoginManager" + }, + "methods": [ + { + "name": "", + "parameterTypes": [ + + ] + } + ] + }, + { + "name": "org.apache.kafka.common.security.authenticator.SaslClientCallbackHandler", + "condition": { + "typeReachable": "org.apache.kafka.common.network.SaslChannelBuilder" + }, + "methods": [ + { + "name": "", + "parameterTypes": [ + + ] + } + ] + }, + { + "name": "org.apache.kafka.common.security.plain.PlainLoginModule", + "condition": { + "typeReachable": "org.apache.kafka.common.security.authenticator.AbstractLogin" + }, + "methods": [ + { + "name": "", + "parameterTypes": [ + + ] + } + ] + }, + { + "name": "org.apache.kafka.common.serialization.BooleanDeserializer", + "condition": { + "typeReachable": "org.apache.kafka.clients.consumer.KafkaConsumer" + }, + "methods": [ + { + "name": "", + "parameterTypes": [ + + ] + } + ] + }, + { + "name": "org.apache.kafka.common.serialization.BooleanSerializer", + "condition": { + "typeReachable": "org.apache.kafka.clients.producer.KafkaProducer" + }, + "methods": [ + { + "name": "", + "parameterTypes": [ + + ] + } + ] + }, + { + "name": "org.apache.kafka.common.serialization.ByteArrayDeserializer", + "condition": { + "typeReachable": "org.apache.kafka.clients.consumer.KafkaConsumer" + }, + "methods": [ + { + "name": "", + "parameterTypes": [ + + ] + } + ] + }, + { + "name": "org.apache.kafka.common.serialization.ByteArraySerializer", + "condition": { + "typeReachable": "org.apache.kafka.clients.producer.KafkaProducer" + }, + "methods": [ + { + "name": "", + "parameterTypes": [ + + ] + } + ] + }, + { + "name": "org.apache.kafka.common.serialization.ByteBufferDeserializer", + "condition": { + "typeReachable": "org.apache.kafka.clients.consumer.KafkaConsumer" + }, + "methods": [ + { + "name": "", + "parameterTypes": [ + + ] + } + ] + }, + { + "name": "org.apache.kafka.common.serialization.ByteBufferSerializer", + "condition": { + "typeReachable": "org.apache.kafka.clients.producer.KafkaProducer" + }, + "methods": [ + { + "name": "", + "parameterTypes": [ + + ] + } + ] + }, + { + "name": "org.apache.kafka.common.serialization.BytesDeserializer", + "condition": { + "typeReachable": "org.apache.kafka.clients.consumer.KafkaConsumer" + }, + "methods": [ + { + "name": "", + "parameterTypes": [ + + ] + } + ] + }, + { + "name": "org.apache.kafka.common.serialization.BytesSerializer", + "condition": { + "typeReachable": "org.apache.kafka.clients.producer.KafkaProducer" + }, + "methods": [ + { + "name": "", + "parameterTypes": [ + + ] + } + ] + }, + { + "name": "org.apache.kafka.common.serialization.DoubleDeserializer", + "condition": { + "typeReachable": "org.apache.kafka.clients.consumer.KafkaConsumer" + }, + "methods": [ + { + "name": "", + "parameterTypes": [ + + ] + } + ] + }, + { + "name": "org.apache.kafka.common.serialization.DoubleSerializer", + "condition": { + "typeReachable": "org.apache.kafka.clients.producer.KafkaProducer" + }, + "methods": [ + { + "name": "", + "parameterTypes": [ + + ] + } + ] + }, + { + "name": "org.apache.kafka.common.serialization.FloatDeserializer", + "condition": { + "typeReachable": "org.apache.kafka.clients.consumer.KafkaConsumer" + }, + "methods": [ + { + "name": "", + "parameterTypes": [ + + ] + } + ] + }, + { + "name": "org.apache.kafka.common.serialization.FloatSerializer", + "condition": { + "typeReachable": "org.apache.kafka.clients.producer.KafkaProducer" + }, + "methods": [ + { + "name": "", + "parameterTypes": [ + + ] + } + ] + }, + { + "name": "org.apache.kafka.common.serialization.IntegerDeserializer", + "condition": { + "typeReachable": "org.apache.kafka.clients.consumer.KafkaConsumer" + }, + "methods": [ + { + "name": "", + "parameterTypes": [ + + ] + } + ] + }, + { + "name": "org.apache.kafka.common.serialization.IntegerSerializer", + "condition": { + "typeReachable": "org.apache.kafka.clients.producer.KafkaProducer" + }, + "methods": [ + { + "name": "", + "parameterTypes": [ + + ] + } + ] + }, + { + "name": "org.apache.kafka.common.serialization.ListDeserializer", + "condition": { + "typeReachable": "org.apache.kafka.clients.consumer.KafkaConsumer" + }, + "methods": [ + { + "name": "", + "parameterTypes": [ + + ] + } + ] + }, + { + "name": "org.apache.kafka.common.serialization.ListSerializer", + "condition": { + "typeReachable": "org.apache.kafka.clients.producer.KafkaProducer" + }, + "methods": [ + { + "name": "", + "parameterTypes": [ + + ] + } + ] + }, + { + "name": "org.apache.kafka.common.serialization.LongDeserializer", + "condition": { + "typeReachable": "org.apache.kafka.clients.consumer.KafkaConsumer" + }, + "methods": [ + { + "name": "", + "parameterTypes": [ + + ] + } + ] + }, + { + "name": "org.apache.kafka.common.serialization.LongSerializer", + "condition": { + "typeReachable": "org.apache.kafka.clients.producer.KafkaProducer" + }, + "methods": [ + { + "name": "", + "parameterTypes": [ + + ] + } + ] + }, + { + "name": "org.apache.kafka.common.serialization.Serdes$BooleanSerde", + "condition": { + "typeReachable": "org.apache.kafka.common.serialization.ListDeserializer" + }, + "methods": [ + { + "name": "", + "parameterTypes": [ + + ] + } + ] + }, + { + "name": "org.apache.kafka.common.serialization.Serdes$BooleanSerde", + "condition": { + "typeReachable": "org.apache.kafka.common.serialization.ListSerializer" + }, + "methods": [ + { + "name": "", + "parameterTypes": [ + + ] + } + ] + }, + { + "name": "org.apache.kafka.common.serialization.Serdes$ByteArraySerde", + "condition": { + "typeReachable": "org.apache.kafka.common.serialization.ListSerializer" + }, + "methods": [ + { + "name": "", + "parameterTypes": [ + + ] + } + ] + }, + { + "name": "org.apache.kafka.common.serialization.Serdes$ByteBufferSerde", + "condition": { + "typeReachable": "org.apache.kafka.common.serialization.ListSerializer" + }, + "methods": [ + { + "name": "", + "parameterTypes": [ + + ] + } + ] + }, + { + "name": "org.apache.kafka.common.serialization.Serdes$BytesSerde", + "condition": { + "typeReachable": "org.apache.kafka.common.serialization.ListSerializer" + }, + "methods": [ + { + "name": "", + "parameterTypes": [ + + ] + } + ] + }, + { + "name": "org.apache.kafka.common.serialization.Serdes$DoubleSerde", + "condition": { + "typeReachable": "org.apache.kafka.common.serialization.ListSerializer" + }, + "methods": [ + { + "name": "", + "parameterTypes": [ + + ] + } + ] + }, + { + "name": "org.apache.kafka.common.serialization.Serdes$FloatSerde", + "condition": { + "typeReachable": "org.apache.kafka.common.serialization.ListSerializer" + }, + "methods": [ + { + "name": "", + "parameterTypes": [ + + ] + } + ] + }, + { + "name": "org.apache.kafka.common.serialization.Serdes$IntegerSerde", + "condition": { + "typeReachable": "org.apache.kafka.common.serialization.ListSerializer" + }, + "methods": [ + { + "name": "", + "parameterTypes": [ + + ] + } + ] + }, + { + "name": "org.apache.kafka.common.serialization.Serdes$LongSerde", + "condition": { + "typeReachable": "org.apache.kafka.common.serialization.ListSerializer" + }, + "methods": [ + { + "name": "", + "parameterTypes": [ + + ] + } + ] + }, + { + "name": "org.apache.kafka.common.serialization.Serdes$ShortSerde", + "condition": { + "typeReachable": "org.apache.kafka.common.serialization.ListSerializer" + }, + "methods": [ + { + "name": "", + "parameterTypes": [ + + ] + } + ] + }, + { + "name": "org.apache.kafka.common.serialization.Serdes$StringSerde", + "condition": { + "typeReachable": "org.apache.kafka.common.serialization.ListSerializer" + }, + "methods": [ + { + "name": "", + "parameterTypes": [ + + ] + } + ] + }, + { + "name": "org.apache.kafka.common.serialization.Serdes$UUIDSerde", + "condition": { + "typeReachable": "org.apache.kafka.common.serialization.ListSerializer" + }, + "methods": [ + { + "name": "", + "parameterTypes": [ + + ] + } + ] + }, + { + "name": "org.apache.kafka.common.serialization.Serdes$VoidSerde", + "condition": { + "typeReachable": "org.apache.kafka.common.serialization.ListSerializer" + }, + "methods": [ + { + "name": "", + "parameterTypes": [ + + ] + } + ] + }, + { + "name": "org.apache.kafka.common.serialization.ShortDeserializer", + "condition": { + "typeReachable": "org.apache.kafka.clients.consumer.KafkaConsumer" + }, + "methods": [ + { + "name": "", + "parameterTypes": [ + + ] + } + ] + }, + { + "name": "org.apache.kafka.common.serialization.ShortSerializer", + "condition": { + "typeReachable": "org.apache.kafka.clients.producer.KafkaProducer" + }, + "methods": [ + { + "name": "", + "parameterTypes": [ + + ] + } + ] + }, + { + "name": "org.apache.kafka.common.serialization.StringDeserializer", + "condition": { + "typeReachable": "org.apache.kafka.clients.consumer.KafkaConsumer" + }, + "methods": [ + { + "name": "", + "parameterTypes": [ + + ] + } + ] + }, + { + "name": "org.apache.kafka.common.serialization.StringSerializer", + "condition": { + "typeReachable": "org.apache.kafka.clients.producer.KafkaProducer" + }, + "methods": [ + { + "name": "", + "parameterTypes": [ + + ] + } + ] + }, + { + "name": "org.apache.kafka.common.serialization.UUIDDeserializer", + "condition": { + "typeReachable": "org.apache.kafka.clients.consumer.KafkaConsumer" + }, + "methods": [ + { + "name": "", + "parameterTypes": [ + + ] + } + ] + }, + { + "name": "org.apache.kafka.common.serialization.UUIDSerializer", + "condition": { + "typeReachable": "org.apache.kafka.clients.producer.KafkaProducer" + }, + "methods": [ + { + "name": "", + "parameterTypes": [ + + ] + } + ] + }, + { + "name": "org.apache.kafka.common.serialization.VoidDeserializer", + "condition": { + "typeReachable": "org.apache.kafka.clients.consumer.KafkaConsumer" + }, + "methods": [ + { + "name": "", + "parameterTypes": [ + + ] + } + ] + }, + { + "name": "org.apache.kafka.common.serialization.VoidSerializer", + "condition": { + "typeReachable": "org.apache.kafka.clients.producer.KafkaProducer" + }, + "methods": [ + { + "name": "", + "parameterTypes": [ + + ] + } + ] + }, + { + "name": "org.apache.kafka.common.utils.AppInfoParser$AppInfo", + "queryAllPublicConstructors": true, + "condition": { + "typeReachable": "org.apache.kafka.common.utils.AppInfoParser" + } + }, + { + "name": "org.apache.kafka.common.utils.AppInfoParser$AppInfoMBean", + "queryAllPublicMethods": true, + "condition": { + "typeReachable": "org.apache.kafka.common.utils.AppInfoParser" + } + } +] diff --git a/smoke-tests-otel-starter/spring-boot-common/src/main/resources/application.yaml b/smoke-tests-otel-starter/spring-boot-common/src/main/resources/application.yaml new file mode 100644 index 000000000000..1995223b3841 --- /dev/null +++ b/smoke-tests-otel-starter/spring-boot-common/src/main/resources/application.yaml @@ -0,0 +1,30 @@ +otel: + instrumentation: + common: + db-statement-sanitizer: + enabled: false + kafka: + experimental-span-attributes: true + logback-appender: + experimental: + capture-code-attributes: true + http: + client: + emit-experimental-telemetry: true + server: + emit-experimental-telemetry: true + propagators: + - b3 + resource: + attributes: + attributeFromYaml: true # boolean will be automatically converted to string by spring + +spring: + kafka: + bootstrap-servers: localhost:9094 + consumer: + auto-offset-reset: earliest + listener: + idle-between-polls: 1000 + producer: + transaction-id-prefix: test- diff --git a/smoke-tests-otel-starter/spring-boot-reactive-2/build.gradle.kts b/smoke-tests-otel-starter/spring-boot-reactive-2/build.gradle.kts new file mode 100644 index 000000000000..83a9d245567c --- /dev/null +++ b/smoke-tests-otel-starter/spring-boot-reactive-2/build.gradle.kts @@ -0,0 +1,33 @@ +plugins { + id("otel.java-conventions") + alias(springBoot2.plugins.versions) +} + +description = "smoke-tests-otel-starter-spring-boot-reactive-2" + +dependencies { + implementation(project(":instrumentation:spring:starters:spring-boot-starter")) + implementation(platform(org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES)) + + implementation(project(":smoke-tests-otel-starter:spring-boot-reactive-common")) + implementation("org.springframework.boot:spring-boot-starter-webflux") + implementation("org.springframework.boot:spring-boot-starter-data-r2dbc") + + runtimeOnly("com.h2database:h2") + runtimeOnly("io.r2dbc:r2dbc-h2") + + testImplementation("org.springframework.boot:spring-boot-starter-test") + testImplementation("io.projectreactor:reactor-test") +} + +springBoot { + mainClass = "io.opentelemetry.spring.smoketest.OtelReactiveSpringStarterSmokeTestApplication" +} + +configurations.configureEach { + resolutionStrategy { + // our dependency management pins to a version that is not compatible with spring boot 2.7 + force("ch.qos.logback:logback-classic:1.2.13") + force("org.slf4j:slf4j-api:1.7.36") + } +} diff --git a/smoke-tests-otel-starter/spring-boot-reactive-2/src/test/java/io/opentelemetry/spring/smoketest/OtelReactiveSpringStarterSmokeTest.java b/smoke-tests-otel-starter/spring-boot-reactive-2/src/test/java/io/opentelemetry/spring/smoketest/OtelReactiveSpringStarterSmokeTest.java new file mode 100644 index 000000000000..cd3fe887671d --- /dev/null +++ b/smoke-tests-otel-starter/spring-boot-reactive-2/src/test/java/io/opentelemetry/spring/smoketest/OtelReactiveSpringStarterSmokeTest.java @@ -0,0 +1,8 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.spring.smoketest; + +class OtelReactiveSpringStarterSmokeTest extends AbstractOtelReactiveSpringStarterSmokeTest {} diff --git a/smoke-tests-otel-starter/spring-boot-reactive-3/build.gradle.kts b/smoke-tests-otel-starter/spring-boot-reactive-3/build.gradle.kts new file mode 100644 index 000000000000..86d5f82dda5e --- /dev/null +++ b/smoke-tests-otel-starter/spring-boot-reactive-3/build.gradle.kts @@ -0,0 +1,78 @@ +plugins { + id("otel.java-conventions") + alias(springBoot31.plugins.versions) + id("org.graalvm.buildtools.native") +} + +description = "smoke-tests-otel-starter-spring-boot-reactive-3" + +otelJava { + minJavaVersionSupported.set(JavaVersion.VERSION_17) +} + +dependencies { + implementation(project(":instrumentation:spring:starters:spring-boot-starter")) + implementation(platform(org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES)) + + implementation(project(":smoke-tests-otel-starter:spring-boot-reactive-common")) + implementation("org.springframework.boot:spring-boot-starter-webflux") + implementation("org.springframework.boot:spring-boot-starter-data-r2dbc") + + runtimeOnly("com.h2database:h2") + runtimeOnly("io.r2dbc:r2dbc-h2") + + testImplementation("org.springframework.boot:spring-boot-starter-test") + testImplementation("io.projectreactor:reactor-test") +} + +springBoot { + mainClass = "io.opentelemetry.spring.smoketest.OtelReactiveSpringStarterSmokeTestApplication" +} + +tasks { + compileAotJava { + with(options) { + compilerArgs.add("-Xlint:-deprecation,-unchecked,none") + // To disable warnings/failure coming from the Java compiler during the Spring AOT processing + // -deprecation,-unchecked and none are required (none is not enough) + } + } + compileAotTestJava { + with(options) { + compilerArgs.add("-Xlint:-deprecation,-unchecked,none") + // To disable warnings/failure coming from the Java compiler during the Spring AOT processing + // -deprecation,-unchecked and none are required (none is not enough) + } + } + checkstyleAot { + isEnabled = false + } + checkstyleAotTest { + isEnabled = false + } +} + +// To be able to execute the tests as GraalVM native executables +configurations.configureEach { + exclude("org.apache.groovy", "groovy") + exclude("org.apache.groovy", "groovy-json") + exclude("org.spockframework", "spock-core") +} + +graalvmNative { + binaries.all { + // Workaround for https://github.com/junit-team/junit5/issues/3405 + buildArgs.add("--initialize-at-build-time=org.junit.platform.launcher.core.LauncherConfig") + buildArgs.add("--initialize-at-build-time=org.junit.jupiter.engine.config.InstantiatingConfigurationParameterConverter") + } + + // See https://github.com/graalvm/native-build-tools/issues/572 + metadataRepository { + enabled.set(false) + } + + tasks.test { + useJUnitPlatform() + setForkEvery(1) + } +} diff --git a/smoke-tests-otel-starter/spring-boot-reactive-3/src/test/java/io/opentelemetry/spring/smoketest/OtelReactiveSpringStarterSmokeTest.java b/smoke-tests-otel-starter/spring-boot-reactive-3/src/test/java/io/opentelemetry/spring/smoketest/OtelReactiveSpringStarterSmokeTest.java new file mode 100644 index 000000000000..cd3fe887671d --- /dev/null +++ b/smoke-tests-otel-starter/spring-boot-reactive-3/src/test/java/io/opentelemetry/spring/smoketest/OtelReactiveSpringStarterSmokeTest.java @@ -0,0 +1,8 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.spring.smoketest; + +class OtelReactiveSpringStarterSmokeTest extends AbstractOtelReactiveSpringStarterSmokeTest {} diff --git a/smoke-tests-otel-starter/spring-boot-reactive-common/build.gradle.kts b/smoke-tests-otel-starter/spring-boot-reactive-common/build.gradle.kts new file mode 100644 index 000000000000..77eeb8ea3d0d --- /dev/null +++ b/smoke-tests-otel-starter/spring-boot-reactive-common/build.gradle.kts @@ -0,0 +1,22 @@ +import org.springframework.boot.gradle.tasks.bundling.BootJar + +plugins { + id("otel.java-conventions") + id("org.springframework.boot") version "2.6.15" +} + +description = "smoke-tests-otel-starter-spring-boot-reactive-common" + +dependencies { + // spring dependencies are compile only to enable testing against different versions of spring + compileOnly(platform(org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES)) + compileOnly("org.springframework.boot:spring-boot-starter-web") + compileOnly("org.springframework.boot:spring-boot-starter-webflux") + compileOnly("org.springframework.boot:spring-boot-starter-test") + compileOnly("org.springframework.boot:spring-boot-starter-data-r2dbc") + api(project(":smoke-tests-otel-starter:spring-smoke-testing")) +} + +tasks.withType { + enabled = false +} diff --git a/smoke-tests-otel-starter/spring-boot-reactive-common/src/main/java/io/opentelemetry/spring/smoketest/AbstractOtelReactiveSpringStarterSmokeTest.java b/smoke-tests-otel-starter/spring-boot-reactive-common/src/main/java/io/opentelemetry/spring/smoketest/AbstractOtelReactiveSpringStarterSmokeTest.java new file mode 100644 index 000000000000..8de8f4fc0ac5 --- /dev/null +++ b/smoke-tests-otel-starter/spring-boot-reactive-common/src/main/java/io/opentelemetry/spring/smoketest/AbstractOtelReactiveSpringStarterSmokeTest.java @@ -0,0 +1,71 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.spring.smoketest; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.semconv.incubating.DbIncubatingAttributes; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.web.reactive.function.client.WebClient; + +@SpringBootTest( + classes = { + OtelReactiveSpringStarterSmokeTestApplication.class, + SpringSmokeOtelConfiguration.class + }, + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +public class AbstractOtelReactiveSpringStarterSmokeTest extends AbstractSpringStarterSmokeTest { + + // can't use @LocalServerPort annotation since it moved packages between Spring Boot 2 and 3 + @Value("${local.server.port}") + int serverPort; + + @Autowired WebClient.Builder webClientBuilder; + private WebClient webClient; + + @BeforeEach + void setUp() { + webClient = webClientBuilder.baseUrl("http://localhost:" + serverPort).build(); + } + + @Test + void webClientAndWebFluxAndR2dbc() { + webClient + .get() + .uri(OtelReactiveSpringStarterSmokeTestController.WEBFLUX) + .retrieve() + .bodyToFlux(String.class) + .blockLast(); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly(span -> span.hasName("CREATE TABLE testdb.player")), + trace -> + trace.hasSpansSatisfyingExactly( + span -> HttpSpanDataAssert.create(span).assertClientGetRequest("/webflux"), + span -> HttpSpanDataAssert.create(span).assertServerGetRequest("/webflux"), + span -> + span.hasKind(SpanKind.CLIENT) + .satisfies( + s -> + assertThat(s.getName()) + .isEqualToIgnoringCase("SELECT testdb.PLAYER")) + .hasAttribute(DbIncubatingAttributes.DB_NAME, "testdb") + // 2 is not replaced by ?, + // otel.instrumentation.common.db-statement-sanitizer.enabled=false + .hasAttributesSatisfying( + a -> + assertThat(a.get(DbIncubatingAttributes.DB_STATEMENT)) + .isEqualToIgnoringCase( + "SELECT PLAYER.* FROM PLAYER WHERE PLAYER.ID = $1 LIMIT 2")) + .hasAttribute(DbIncubatingAttributes.DB_SYSTEM, "h2"))); + } +} diff --git a/smoke-tests-otel-starter/spring-boot-reactive-common/src/main/java/io/opentelemetry/spring/smoketest/OtelReactiveSpringStarterSmokeTestApplication.java b/smoke-tests-otel-starter/spring-boot-reactive-common/src/main/java/io/opentelemetry/spring/smoketest/OtelReactiveSpringStarterSmokeTestApplication.java new file mode 100644 index 000000000000..366264114749 --- /dev/null +++ b/smoke-tests-otel-starter/spring-boot-reactive-common/src/main/java/io/opentelemetry/spring/smoketest/OtelReactiveSpringStarterSmokeTestApplication.java @@ -0,0 +1,19 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.spring.smoketest; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class OtelReactiveSpringStarterSmokeTestApplication { + + public OtelReactiveSpringStarterSmokeTestApplication() {} + + public static void main(String[] args) { + SpringApplication.run(OtelReactiveSpringStarterSmokeTestApplication.class); + } +} diff --git a/smoke-tests-otel-starter/spring-boot-reactive-common/src/main/java/io/opentelemetry/spring/smoketest/OtelReactiveSpringStarterSmokeTestController.java b/smoke-tests-otel-starter/spring-boot-reactive-common/src/main/java/io/opentelemetry/spring/smoketest/OtelReactiveSpringStarterSmokeTestController.java new file mode 100644 index 000000000000..2eebba8d6bb8 --- /dev/null +++ b/smoke-tests-otel-starter/spring-boot-reactive-common/src/main/java/io/opentelemetry/spring/smoketest/OtelReactiveSpringStarterSmokeTestController.java @@ -0,0 +1,28 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.spring.smoketest; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; +import reactor.core.publisher.Mono; + +@RestController +public class OtelReactiveSpringStarterSmokeTestController { + + public static final String WEBFLUX = "/webflux"; + private final PlayerRepository playerRepository; + + public OtelReactiveSpringStarterSmokeTestController(PlayerRepository playerRepository) { + this.playerRepository = playerRepository; + } + + @GetMapping(WEBFLUX) + public Mono webflux() { + return playerRepository + .findById(1) + .map(player -> "Player: " + player.getName() + " Age: " + player.getAge()); + } +} diff --git a/smoke-tests-otel-starter/spring-boot-reactive-common/src/main/java/io/opentelemetry/spring/smoketest/Player.java b/smoke-tests-otel-starter/spring-boot-reactive-common/src/main/java/io/opentelemetry/spring/smoketest/Player.java new file mode 100644 index 000000000000..7d14fcf5537b --- /dev/null +++ b/smoke-tests-otel-starter/spring-boot-reactive-common/src/main/java/io/opentelemetry/spring/smoketest/Player.java @@ -0,0 +1,34 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.spring.smoketest; + +import org.springframework.data.annotation.Id; + +public class Player { + @Id Integer id; + String name; + Integer age; + + public Player() {} + + public Player(Integer id, String name, Integer age) { + this.id = id; + this.name = name; + this.age = age; + } + + public Integer getId() { + return id; + } + + public String getName() { + return name; + } + + public Integer getAge() { + return age; + } +} diff --git a/smoke-tests-otel-starter/spring-boot-reactive-common/src/main/java/io/opentelemetry/spring/smoketest/PlayerRepository.java b/smoke-tests-otel-starter/spring-boot-reactive-common/src/main/java/io/opentelemetry/spring/smoketest/PlayerRepository.java new file mode 100644 index 000000000000..65af6a0503e1 --- /dev/null +++ b/smoke-tests-otel-starter/spring-boot-reactive-common/src/main/java/io/opentelemetry/spring/smoketest/PlayerRepository.java @@ -0,0 +1,10 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.spring.smoketest; + +import org.springframework.data.repository.reactive.ReactiveCrudRepository; + +public interface PlayerRepository extends ReactiveCrudRepository {} diff --git a/smoke-tests-otel-starter/spring-boot-reactive-common/src/main/resources/application.yaml b/smoke-tests-otel-starter/spring-boot-reactive-common/src/main/resources/application.yaml new file mode 100644 index 000000000000..26b301f52d79 --- /dev/null +++ b/smoke-tests-otel-starter/spring-boot-reactive-common/src/main/resources/application.yaml @@ -0,0 +1,17 @@ +otel: + instrumentation: + common: + db-statement-sanitizer: + enabled: false + http: + client: + emit-experimental-telemetry: true + server: + emit-experimental-telemetry: true + +spring: + r2dbc: + url: r2dbc:h2:mem:///testdb + jpa: + hibernate: + ddl-auto: create diff --git a/smoke-tests-otel-starter/spring-boot-reactive-common/src/main/resources/schema.sql b/smoke-tests-otel-starter/spring-boot-reactive-common/src/main/resources/schema.sql new file mode 100644 index 000000000000..3700951f3c81 --- /dev/null +++ b/smoke-tests-otel-starter/spring-boot-reactive-common/src/main/resources/schema.sql @@ -0,0 +1 @@ +CREATE TABLE IF NOT EXISTS player(id INT NOT NULL AUTO_INCREMENT, name VARCHAR(255), age INT, PRIMARY KEY (id)); diff --git a/smoke-tests-otel-starter/spring-smoke-testing/build.gradle.kts b/smoke-tests-otel-starter/spring-smoke-testing/build.gradle.kts new file mode 100644 index 000000000000..6b2eae7f32ab --- /dev/null +++ b/smoke-tests-otel-starter/spring-smoke-testing/build.gradle.kts @@ -0,0 +1,21 @@ +import org.springframework.boot.gradle.tasks.bundling.BootJar + +plugins { + id("otel.java-conventions") + id("org.springframework.boot") version "2.6.15" +} + +description = "smoke-tests-otel-starter-spring-smoke-testing" + +dependencies { + // spring dependencies are compile only to enable testing against different versions of spring + compileOnly(platform(org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES)) + compileOnly("org.springframework.boot:spring-boot-starter") + compileOnly("org.springframework.boot:spring-boot-starter-test") + api(project(":testing-common")) + api("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure-spi") +} + +tasks.withType { + enabled = false +} diff --git a/smoke-tests-otel-starter/spring-smoke-testing/src/main/java/io/opentelemetry/spring/smoketest/AbstractSpringStarterSmokeTest.java b/smoke-tests-otel-starter/spring-smoke-testing/src/main/java/io/opentelemetry/spring/smoketest/AbstractSpringStarterSmokeTest.java new file mode 100644 index 000000000000..7ca5cab29615 --- /dev/null +++ b/smoke-tests-otel-starter/spring-smoke-testing/src/main/java/io/opentelemetry/spring/smoketest/AbstractSpringStarterSmokeTest.java @@ -0,0 +1,71 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.spring.smoketest; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.sdk.testing.assertj.SpanDataAssert; +import java.util.Arrays; +import java.util.List; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.system.CapturedOutput; +import org.springframework.boot.test.system.OutputCaptureExtension; + +@ExtendWith(OutputCaptureExtension.class) +public abstract class AbstractSpringStarterSmokeTest { + + private static final List IGNORED_WARNINGS = + Arrays.asList( + "Unable to load io.netty.resolver.dns.macos.MacOSDnsServerAddressStreamProvider", + "The architecture 'amd64' for image"); + + @Autowired protected OpenTelemetry openTelemetry; + + protected SpringSmokeTestRunner testing; + + @BeforeAll + static void beforeAll() { + SpringSmokeTestRunner.resetExporters(); + } + + @BeforeEach + void setUpTesting() { + if (openTelemetry != null) { + // @Autowired doesn't work in all tests, e.g. AbstractJvmKafkaSpringStarterSmokeTest + // those tests have to manage the testing instance, + // themselves because they don't use @SpringBootTest + testing = new SpringSmokeTestRunner(openTelemetry); + } + } + + @AfterEach + void checkSpringLogs(CapturedOutput output) { + // warnings are emitted if the auto-configuration have non-fatal problems + assertThat(output) + // not a warning in Spring Boot 2 + .doesNotContain("is not eligible for getting processed by all BeanPostProcessors") + // only look for WARN and ERROR log level, e.g. [Test worker] WARN + .satisfies( + s -> { + for (String line : s.toString().split("\n")) { + if (IGNORED_WARNINGS.stream().noneMatch(line::contains)) { + assertThat(line).doesNotContain("] WARN").doesNotContain("] ERROR"); + } + } + }); + } + + static SpanDataAssert withSpanAssert(SpanDataAssert span) { + return span.hasName("SpringComponent.withSpanMethod") + .hasAttribute(AttributeKey.stringKey("paramName"), "from-controller"); + } +} diff --git a/smoke-tests-otel-starter/spring-smoke-testing/src/main/java/io/opentelemetry/spring/smoketest/HttpSpanDataAssert.java b/smoke-tests-otel-starter/spring-smoke-testing/src/main/java/io/opentelemetry/spring/smoketest/HttpSpanDataAssert.java new file mode 100644 index 000000000000..4f6018d46167 --- /dev/null +++ b/smoke-tests-otel-starter/spring-smoke-testing/src/main/java/io/opentelemetry/spring/smoketest/HttpSpanDataAssert.java @@ -0,0 +1,62 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.spring.smoketest; + +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.satisfies; + +import com.google.errorprone.annotations.CanIgnoreReturnValue; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.sdk.testing.assertj.ResourceAssert; +import io.opentelemetry.sdk.testing.assertj.SpanDataAssert; +import io.opentelemetry.semconv.HttpAttributes; +import io.opentelemetry.semconv.UrlAttributes; +import io.opentelemetry.semconv.incubating.HttpIncubatingAttributes; +import java.util.function.Consumer; +import org.assertj.core.api.AbstractLongAssert; + +public final class HttpSpanDataAssert { + + private final SpanDataAssert span; + + private HttpSpanDataAssert(SpanDataAssert span) { + this.span = span; + } + + public static HttpSpanDataAssert create(SpanDataAssert serverSpan) { + return new HttpSpanDataAssert(serverSpan); + } + + @CanIgnoreReturnValue + public HttpSpanDataAssert assertClientGetRequest(String path) { + span.hasKind(SpanKind.CLIENT) + .hasAttributesSatisfying( + satisfies(UrlAttributes.URL_FULL, a -> a.endsWith(path)), + // this attribute is set by the experimental http instrumentation + satisfies( + HttpIncubatingAttributes.HTTP_RESPONSE_BODY_SIZE, + AbstractLongAssert::isNotNegative)); + return this; + } + + @CanIgnoreReturnValue + public HttpSpanDataAssert assertServerGetRequest(String route) { + span.hasKind(SpanKind.SERVER) + .hasAttributesSatisfying( + equalTo(HttpAttributes.HTTP_REQUEST_METHOD, "GET"), + equalTo(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, 200L), + equalTo(HttpAttributes.HTTP_ROUTE, route), + // this attribute is set by the experimental http instrumentation + satisfies( + HttpIncubatingAttributes.HTTP_RESPONSE_BODY_SIZE, + AbstractLongAssert::isNotNegative)); + return this; + } + + public SpanDataAssert hasResourceSatisfying(Consumer resource) { + return span.hasResourceSatisfying(resource); + } +} diff --git a/smoke-tests-otel-starter/spring-smoke-testing/src/main/java/io/opentelemetry/spring/smoketest/RequiresDockerCompose.java b/smoke-tests-otel-starter/spring-smoke-testing/src/main/java/io/opentelemetry/spring/smoketest/RequiresDockerCompose.java new file mode 100644 index 000000000000..0fde682563cc --- /dev/null +++ b/smoke-tests-otel-starter/spring-smoke-testing/src/main/java/io/opentelemetry/spring/smoketest/RequiresDockerCompose.java @@ -0,0 +1,25 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.spring.smoketest; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; + +@Target({ElementType.TYPE, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@EnabledIfEnvironmentVariable( + named = "DOCKER_COMPOSE_TEST", + matches = "true", + disabledReason = + "Testcontainers does not work in some cases with GraalVM native images. " + + "A container has to be started manually. " + + "So, an environment variable is used to disable the test by default.") +public @interface RequiresDockerCompose {} diff --git a/smoke-tests-otel-starter/spring-smoke-testing/src/main/java/io/opentelemetry/spring/smoketest/SpringSmokeOtelConfiguration.java b/smoke-tests-otel-starter/spring-smoke-testing/src/main/java/io/opentelemetry/spring/smoketest/SpringSmokeOtelConfiguration.java new file mode 100644 index 000000000000..45fa45cf62f1 --- /dev/null +++ b/smoke-tests-otel-starter/spring-smoke-testing/src/main/java/io/opentelemetry/spring/smoketest/SpringSmokeOtelConfiguration.java @@ -0,0 +1,94 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.spring.smoketest; + +import com.google.common.collect.ImmutableMap; +import io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizerProvider; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import io.opentelemetry.sdk.autoconfigure.spi.logs.ConfigurableLogRecordExporterProvider; +import io.opentelemetry.sdk.autoconfigure.spi.metrics.ConfigurableMetricExporterProvider; +import io.opentelemetry.sdk.autoconfigure.spi.traces.ConfigurableSpanExporterProvider; +import io.opentelemetry.sdk.logs.export.LogRecordExporter; +import io.opentelemetry.sdk.metrics.export.MetricExporter; +import io.opentelemetry.sdk.trace.export.SpanExporter; +import java.util.Map; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration(proxyBeanMethods = false) +public class SpringSmokeOtelConfiguration { + + private static final String MEMORY_EXPORTER = "memory"; + + @Bean + AutoConfigurationCustomizerProvider autoConfigurationCustomizerProvider() { + return provider -> provider.addPropertiesSupplier(SpringSmokeOtelConfiguration::getProperties); + } + + private static Map getProperties() { + return ImmutableMap.of( + // We set the export interval of the metrics to 100 ms. The default value is 1 minute. + "otel.metric.export.interval", + "100", + // We set the export interval of the spans to 100 ms. The default value is 5 seconds. + "otel.bsp.schedule.delay", + "100", + // We set the export interval of the logs to 100 ms. The default value is 1 second. + "otel.blrp.schedule.delay", + "100", + "otel.traces.exporter", + MEMORY_EXPORTER, + "otel.metrics.exporter", + MEMORY_EXPORTER, + "otel.logs.exporter", + MEMORY_EXPORTER); + } + + @Bean + ConfigurableMetricExporterProvider otlpMetricExporterProvider() { + return new ConfigurableMetricExporterProvider() { + @Override + public MetricExporter createExporter(ConfigProperties configProperties) { + return SpringSmokeTestRunner.testMetricExporter; + } + + @Override + public String getName() { + return MEMORY_EXPORTER; + } + }; + } + + @Bean + ConfigurableSpanExporterProvider otlpSpanExporterProvider() { + return new ConfigurableSpanExporterProvider() { + @Override + public SpanExporter createExporter(ConfigProperties configProperties) { + return SpringSmokeTestRunner.testSpanExporter; + } + + @Override + public String getName() { + return MEMORY_EXPORTER; + } + }; + } + + @Bean + ConfigurableLogRecordExporterProvider otlpLogRecordExporterProvider() { + return new ConfigurableLogRecordExporterProvider() { + @Override + public LogRecordExporter createExporter(ConfigProperties configProperties) { + return SpringSmokeTestRunner.testLogRecordExporter; + } + + @Override + public String getName() { + return MEMORY_EXPORTER; + } + }; + } +} diff --git a/smoke-tests-otel-starter/spring-smoke-testing/src/main/java/io/opentelemetry/spring/smoketest/SpringSmokeTestRunner.java b/smoke-tests-otel-starter/spring-smoke-testing/src/main/java/io/opentelemetry/spring/smoketest/SpringSmokeTestRunner.java new file mode 100644 index 000000000000..471016ee883e --- /dev/null +++ b/smoke-tests-otel-starter/spring-smoke-testing/src/main/java/io/opentelemetry/spring/smoketest/SpringSmokeTestRunner.java @@ -0,0 +1,78 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.spring.smoketest; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.instrumentation.testing.InstrumentationTestRunner; +import io.opentelemetry.sdk.logs.data.LogRecordData; +import io.opentelemetry.sdk.metrics.data.AggregationTemporality; +import io.opentelemetry.sdk.metrics.data.MetricData; +import io.opentelemetry.sdk.testing.exporter.InMemoryLogRecordExporter; +import io.opentelemetry.sdk.testing.exporter.InMemoryMetricExporter; +import io.opentelemetry.sdk.testing.exporter.InMemorySpanExporter; +import io.opentelemetry.sdk.trace.data.SpanData; +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 SpringSmokeTestRunner extends InstrumentationTestRunner { + + static InMemorySpanExporter testSpanExporter; + static InMemoryMetricExporter testMetricExporter; + static InMemoryLogRecordExporter testLogRecordExporter; + + static OpenTelemetry openTelemetry; + + public SpringSmokeTestRunner(OpenTelemetry openTelemetry) { + super(openTelemetry); + } + + static void resetExporters() { + testSpanExporter = InMemorySpanExporter.create(); + testMetricExporter = InMemoryMetricExporter.create(AggregationTemporality.DELTA); + testLogRecordExporter = InMemoryLogRecordExporter.create(); + } + + @Override + public void beforeTestClass() {} + + @Override + public void afterTestClass() {} + + @Override + public void clearAllExportedData() { + testSpanExporter.reset(); + testMetricExporter.reset(); + testLogRecordExporter.reset(); + } + + @Override + public OpenTelemetry getOpenTelemetry() { + return openTelemetry; + } + + @Override + public List getExportedSpans() { + return testSpanExporter.getFinishedSpanItems(); + } + + @Override + public List getExportedMetrics() { + return testMetricExporter.getFinishedMetricItems(); + } + + @Override + public List getExportedLogRecords() { + return testLogRecordExporter.getFinishedLogRecordItems(); + } + + @Override + public boolean forceFlushCalled() { + throw new UnsupportedOperationException(); + } +} diff --git a/smoke-tests/build.gradle.kts b/smoke-tests/build.gradle.kts index d8e58387fd09..2004e1e85640 100644 --- a/smoke-tests/build.gradle.kts +++ b/smoke-tests/build.gradle.kts @@ -15,7 +15,7 @@ otelJava { maxJavaVersionForTests.set(JavaVersion.VERSION_11) } -val dockerJavaVersion = "3.3.1" +val dockerJavaVersion = "3.4.0" dependencies { testCompileOnly("com.google.auto.value:auto-value-annotations") testAnnotationProcessor("com.google.auto.value:auto-value") @@ -23,13 +23,13 @@ dependencies { api("org.spockframework:spock-core") api(project(":testing-common")) - implementation(platform("io.grpc:grpc-bom:1.56.0")) + implementation(platform("io.grpc:grpc-bom:1.66.0")) implementation("org.slf4j:slf4j-api") implementation("io.opentelemetry:opentelemetry-api") implementation("io.opentelemetry.proto:opentelemetry-proto") implementation("org.testcontainers:testcontainers") implementation("com.fasterxml.jackson.core:jackson-databind") - implementation("com.google.protobuf:protobuf-java-util:3.23.3") + implementation("com.google.protobuf:protobuf-java-util:3.25.4") implementation("io.grpc:grpc-netty-shaded") implementation("io.grpc:grpc-protobuf") implementation("io.grpc:grpc-stub") diff --git a/smoke-tests/images/early-jdk8/Dockerfile b/smoke-tests/images/early-jdk8/Dockerfile new file mode 100644 index 000000000000..0476bce8d9ec --- /dev/null +++ b/smoke-tests/images/early-jdk8/Dockerfile @@ -0,0 +1,16 @@ +# https://github.com/zulu-openjdk/zulu-openjdk/blob/master/ubuntu/8u412-8.78/Dockerfile +FROM ubuntu:noble + +ENV LANG='en_US.UTF-8' LANGUAGE='en_US:en' LC_ALL='en_US.UTF-8' + +RUN apt-get -qq update && \ + apt-get -qq -y --no-install-recommends install software-properties-common locales curl tzdata unzip && \ + echo "en_US.UTF-8 UTF-8" >> /etc/locale.gen && \ + locale-gen en_US.UTF-8 && \ + curl -sLO https://cdn.azul.com/zulu/bin/zulu1.8.0_31-8.5.0.1-x86lx64.zip && \ + unzip zulu1.8.0_31-8.5.0.1-x86lx64.zip -d /opt && \ + apt-get -qq -y purge --auto-remove software-properties-common curl unzip && \ + rm zulu1.8.0_31-8.5.0.1-x86lx64.zip + +ENV JAVA_HOME=/opt/zulu1.8.0_31-8.5.0.1-x86lx64 +ENV PATH="${PATH}:/opt/zulu1.8.0_31-8.5.0.1-x86lx64/bin" diff --git a/smoke-tests/images/early-jdk8/build.gradle.kts b/smoke-tests/images/early-jdk8/build.gradle.kts new file mode 100644 index 000000000000..d583cd6f53fd --- /dev/null +++ b/smoke-tests/images/early-jdk8/build.gradle.kts @@ -0,0 +1,35 @@ +import com.bmuschko.gradle.docker.tasks.image.DockerBuildImage +import com.bmuschko.gradle.docker.tasks.image.DockerPushImage +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter + +plugins { + id("com.bmuschko.docker-remote-api") +} + +val extraTag = findProperty("extraTag") + ?: DateTimeFormatter.ofPattern("yyyyMMdd.HHmmSS").format(LocalDateTime.now()) + +tasks { + val dockerWorkingDir = layout.buildDirectory.dir("docker") + + val imagePrepare by registering(Copy::class) { + into(dockerWorkingDir) + from("Dockerfile") + } + + val imageBuild by registering(DockerBuildImage::class) { + dependsOn(imagePrepare) + inputDir.set(dockerWorkingDir) + + images.add("ghcr.io/open-telemetry/opentelemetry-java-instrumentation/smoke-test-zulu-openjdk-8u31:$extraTag") + dockerFile.set(dockerWorkingDir.get().file("Dockerfile")) + } + + val dockerPush by registering(DockerPushImage::class) { + group = "publishing" + description = "Push all Docker images" + dependsOn(imageBuild) + images.add("ghcr.io/open-telemetry/opentelemetry-java-instrumentation/smoke-test-zulu-openjdk-8u31:$extraTag") + } +} diff --git a/smoke-tests/images/fake-backend/build.gradle.kts b/smoke-tests/images/fake-backend/build.gradle.kts index 5baad1474ef0..040a51daf41f 100644 --- a/smoke-tests/images/fake-backend/build.gradle.kts +++ b/smoke-tests/images/fake-backend/build.gradle.kts @@ -5,14 +5,13 @@ import java.time.format.DateTimeFormatter plugins { id("otel.java-conventions") - id("com.bmuschko.docker-remote-api") - id("com.github.johnrengelman.shadow") + id("com.gradleup.shadow") id("com.google.cloud.tools.jib") } dependencies { - implementation("com.linecorp.armeria:armeria-grpc:1.24.0") + implementation("com.linecorp.armeria:armeria-grpc:1.30.0") implementation("io.opentelemetry.proto:opentelemetry-proto") runtimeOnly("org.slf4j:slf4j-simple") } @@ -26,7 +25,7 @@ jib { } // windows containers are built manually since jib does not support windows containers yet -val backendDockerBuildDir = file("$buildDir/docker-backend") +val backendDockerBuildDir = layout.buildDirectory.dir("docker-backend") tasks { withType().configureEach { @@ -59,7 +58,7 @@ tasks { inputDir.set(backendDockerBuildDir) images.add("ghcr.io/open-telemetry/opentelemetry-java-instrumentation/smoke-test-fake-backend-windows:$extraTag") - dockerFile.set(File(backendDockerBuildDir, "windows.dockerfile")) + dockerFile.set(File(backendDockerBuildDir.get().asFile, "windows.dockerfile")) } val dockerPush by registering(DockerPushImage::class) { diff --git a/smoke-tests/images/grpc/build.gradle.kts b/smoke-tests/images/grpc/build.gradle.kts index da6db1b70bac..5544f0b74bed 100644 --- a/smoke-tests/images/grpc/build.gradle.kts +++ b/smoke-tests/images/grpc/build.gradle.kts @@ -8,15 +8,15 @@ plugins { } dependencies { - implementation(platform("io.grpc:grpc-bom:1.56.0")) + implementation(platform("io.grpc:grpc-bom:1.66.0")) implementation(platform("io.opentelemetry:opentelemetry-bom:1.0.0")) implementation(platform("io.opentelemetry:opentelemetry-bom-alpha:1.0.0-alpha")) - implementation(platform("org.apache.logging.log4j:log4j-bom:2.20.0")) + implementation(platform("org.apache.logging.log4j:log4j-bom:2.24.0")) implementation("io.grpc:grpc-netty-shaded") implementation("io.grpc:grpc-protobuf") implementation("io.grpc:grpc-stub") - implementation("io.opentelemetry.proto:opentelemetry-proto:0.20.0-alpha") + implementation("io.opentelemetry.proto:opentelemetry-proto") implementation(project(":instrumentation-annotations")) implementation("org.apache.logging.log4j:log4j-core") diff --git a/smoke-tests/images/play/build.gradle.kts b/smoke-tests/images/play/build.gradle.kts index 76cd89f7320a..f311ff1b07d6 100644 --- a/smoke-tests/images/play/build.gradle.kts +++ b/smoke-tests/images/play/build.gradle.kts @@ -6,10 +6,10 @@ plugins { id("otel.spotless-conventions") id("com.google.cloud.tools.jib") - id("org.gradle.playframework") version "0.13" + id("org.gradle.playframework") version "0.14" } -val playVer = "2.8.19" +val playVer = "2.8.22" val scalaVer = "2.12" play { diff --git a/smoke-tests/images/quarkus/build.gradle.kts b/smoke-tests/images/quarkus/build.gradle.kts index 3a2b9651c42f..5d5b77edd586 100644 --- a/smoke-tests/images/quarkus/build.gradle.kts +++ b/smoke-tests/images/quarkus/build.gradle.kts @@ -12,11 +12,11 @@ plugins { id("otel.java-conventions") id("com.google.cloud.tools.jib") - id("io.quarkus") version "3.1.2.Final" + id("io.quarkus") version "3.14.2" } dependencies { - implementation(enforcedPlatform("io.quarkus.platform:quarkus-bom:3.1.2.Final")) + implementation(enforcedPlatform("io.quarkus:quarkus-bom:3.14.2")) implementation("io.quarkus:quarkus-resteasy") } diff --git a/smoke-tests/images/servlet/build.gradle.kts b/smoke-tests/images/servlet/build.gradle.kts index 3814786cf145..3614722c487b 100644 --- a/smoke-tests/images/servlet/build.gradle.kts +++ b/smoke-tests/images/servlet/build.gradle.kts @@ -1,6 +1,6 @@ import com.bmuschko.gradle.docker.tasks.image.DockerBuildImage import com.bmuschko.gradle.docker.tasks.image.DockerPushImage -import org.gradle.configurationcache.extensions.capitalized +import org.apache.commons.lang.StringUtils plugins { id("otel.spotless-conventions") @@ -24,54 +24,52 @@ val extraTag = findProperty("extraTag") // Dockerfile name, args key passes raw arguments to docker build val targets = mapOf( "jetty" to listOf( - ImageTarget(listOf("9.4.51"), listOf("hotspot"), listOf("8", "11", "17", "20", "21"), mapOf("sourceVersion" to "9.4.51.v20230217")), - ImageTarget(listOf("9.4.51"), listOf("openj9"), listOf("8", "11", "17", "18"), mapOf("sourceVersion" to "9.4.51.v20230217")), - ImageTarget(listOf("10.0.15"), listOf("hotspot"), listOf("11", "17", "20", "21"), mapOf("sourceVersion" to "10.0.7")), - ImageTarget(listOf("10.0.15"), listOf("openj9"), listOf("11", "17", "18"), mapOf("sourceVersion" to "10.0.7")), - ImageTarget(listOf("11.0.15"), listOf("hotspot"), listOf("11", "17", "20", "21"), mapOf("sourceVersion" to "11.0.7"), "servlet-5.0"), - ImageTarget(listOf("11.0.15"), listOf("openj9"), listOf("11", "17", "18"), mapOf("sourceVersion" to "11.0.7"), "servlet-5.0"), + ImageTarget(listOf("9.4.53"), listOf("hotspot", "openj9"), listOf("8", "11", "17", "21"), mapOf("sourceVersion" to "9.4.53.v20231009")), + ImageTarget(listOf("10.0.19"), listOf("hotspot", "openj9"), listOf("11", "17", "21"), mapOf("sourceVersion" to "10.0.19")), + ImageTarget(listOf("11.0.19"), listOf("hotspot", "openj9"), listOf("11", "17", "21"), mapOf("sourceVersion" to "11.0.19"), "servlet-5.0"), + ImageTarget(listOf("12.0.6"), listOf("hotspot", "openj9"), listOf("17", "21"), mapOf("sourceVersion" to "12.0.6"), "servlet-5.0"), ), "liberty" to listOf( ImageTarget(listOf("20.0.0.12"), listOf("hotspot", "openj9"), listOf("8", "11"), mapOf("release" to "2020-11-11_0736")), - ImageTarget(listOf("21.0.0.12"), listOf("hotspot"), listOf("8", "11", "17"), mapOf("release" to "2021-11-17_1256")), - ImageTarget(listOf("21.0.0.12"), listOf("openj9"), listOf("8", "11", "17"), mapOf("release" to "2021-11-17_1256")), + ImageTarget(listOf("21.0.0.12"), listOf("hotspot", "openj9"), listOf("8", "11", "17"), mapOf("release" to "2021-11-17_1256")), // Java 19 is not supported until 22.0.0.10 - ImageTarget(listOf("22.0.0.12"), listOf("hotspot"), listOf("8", "11", "17", "20"), mapOf("release" to "22.0.0.12")), - ImageTarget(listOf("22.0.0.12"), listOf("openj9"), listOf("8", "11", "17"), mapOf("release" to "22.0.0.12")), - ImageTarget(listOf("23.0.0.3"), listOf("hotspot"), listOf("8", "11", "17", "20"), mapOf("release" to "23.0.0.3")), - ImageTarget(listOf("23.0.0.3"), listOf("openj9"), listOf("8", "11", "17"), mapOf("release" to "23.0.0.3")), + ImageTarget(listOf("22.0.0.12"), listOf("hotspot", "openj9"), listOf("8", "11", "17"), mapOf("release" to "22.0.0.12")), + // Java 21 is not supported until 23.0.0.3 - despite that only 20 seems to work + ImageTarget(listOf("23.0.0.12"), listOf("hotspot", "openj9"), listOf("8", "11", "17", "20"), mapOf("release" to "23.0.0.12")), ), "payara" to listOf( - ImageTarget(listOf("5.2020.6"), listOf("hotspot", "openj9"), listOf("8", "11")), - ImageTarget(listOf("5.2021.8"), listOf("hotspot", "openj9"), listOf("8", "11")), - ImageTarget(listOf("6.2023.4"), listOf("hotspot", "openj9"), listOf("11", "17"), war = "servlet-5.0") + ImageTarget(listOf("5.2020.6", "5.2021.8"), listOf("hotspot", "openj9"), listOf("8", "11")), + // Test application is not deployed when server is sarted with hotspot jdk version 21 + ImageTarget(listOf("6.2023.12"), listOf("hotspot"), listOf("11", "17"), war = "servlet-5.0"), + ImageTarget(listOf("6.2023.12"), listOf("openj9"), listOf("11", "17", "21"), war = "servlet-5.0") ), "tomcat" to listOf( ImageTarget(listOf("7.0.109"), listOf("hotspot", "openj9"), listOf("8"), mapOf("majorVersion" to "7")), - ImageTarget(listOf("8.5.88"), listOf("hotspot"), listOf("8", "11", "17", "20", "21"), mapOf("majorVersion" to "8")), - ImageTarget(listOf("8.5.88"), listOf("openj9"), listOf("8", "11", "17", "18"), mapOf("majorVersion" to "8")), - ImageTarget(listOf("9.0.74"), listOf("hotspot"), listOf("8", "11", "17", "20", "21"), mapOf("majorVersion" to "9")), - ImageTarget(listOf("9.0.74"), listOf("openj9"), listOf("8", "11", "17", "18"), mapOf("majorVersion" to "9")), - ImageTarget(listOf("10.1.8"), listOf("hotspot"), listOf("11", "17", "20", "21"), mapOf("majorVersion" to "10"), "servlet-5.0"), - ImageTarget(listOf("10.1.8"), listOf("openj9"), listOf("11", "17", "18"), mapOf("majorVersion" to "10"), "servlet-5.0"), + ImageTarget(listOf("8.5.98"), listOf("hotspot", "openj9"), listOf("8", "11", "17", "21"), mapOf("majorVersion" to "8")), + ImageTarget(listOf("9.0.85"), listOf("hotspot", "openj9"), listOf("8", "11", "17", "21"), mapOf("majorVersion" to "9")), + ImageTarget(listOf("10.1.18"), listOf("hotspot", "openj9"), listOf("11", "17", "21"), mapOf("majorVersion" to "10"), "servlet-5.0"), ), "tomee" to listOf( - ImageTarget(listOf("7.0.9"), listOf("hotspot", "openj9"), listOf("8")), - ImageTarget(listOf("7.1.4"), listOf("hotspot", "openj9"), listOf("8")), - ImageTarget(listOf("8.0.14"), listOf("hotspot"), listOf("8", "11", "17", "20", "21")), - ImageTarget(listOf("8.0.14"), listOf("openj9"), listOf("8", "11", "17", "18")), - ImageTarget(listOf("9.0.0"), listOf("hotspot"), listOf("11", "17", "20", "21"), war = "servlet-5.0"), - ImageTarget(listOf("9.0.0"), listOf("openj9"), listOf("11", "17", "18"), war = "servlet-5.0"), + ImageTarget(listOf("7.0.9", "7.1.4"), listOf("hotspot", "openj9"), listOf("8")), + ImageTarget(listOf("8.0.16"), listOf("hotspot", "openj9"), listOf("8", "11", "17", "21")), + ImageTarget(listOf("9.1.2"), listOf("hotspot", "openj9"), listOf("11", "17", "21"), war = "servlet-5.0"), ), "websphere" to listOf( ImageTarget(listOf("8.5.5.22", "9.0.5.14"), listOf("openj9"), listOf("8"), windows = false), ), "wildfly" to listOf( ImageTarget(listOf("13.0.0.Final"), listOf("hotspot", "openj9"), listOf("8")), - ImageTarget(listOf("17.0.1.Final", "21.0.0.Final"), listOf("hotspot"), listOf("8", "11", "17", "20", "21")), - ImageTarget(listOf("17.0.1.Final", "21.0.0.Final"), listOf("openj9"), listOf("8", "11", "17", "18")), - ImageTarget(listOf("28.0.0.Final"), listOf("hotspot"), listOf("11", "17", "20", "21"), war = "servlet-5.0"), - ImageTarget(listOf("28.0.0.Final"), listOf("openj9"), listOf("11", "17", "18"), war = "servlet-5.0"), + ImageTarget( + listOf("17.0.1.Final", "21.0.0.Final"), + listOf("hotspot", "openj9"), + listOf("8", "11", "17", "21") + ), + ImageTarget( + listOf("28.0.1.Final", "29.0.1.Final", "30.0.1.Final"), + listOf("hotspot", "openj9"), + listOf("11", "17", "21"), + war = "servlet-5.0" + ), ), ) @@ -106,7 +104,7 @@ tasks { continue } println(server) - val serverName = server.capitalized() + val serverName = StringUtils.capitalize(server) for (entry in matrices) { for (version in entry.version) { val dotIndex = version.indexOf('.') @@ -136,7 +134,7 @@ fun configureImage( isWindows: Boolean ): String { // Using separate build directory for different image - val dockerWorkingDir = file("$buildDir/docker-$server-$version-jdk$jdk-$vm-$warProject") + val dockerWorkingDir = layout.buildDirectory.dir("docker-$server-$version-jdk$jdk-$vm-$warProject") val dockerFileName = "$dockerfile.${if (isWindows) "windows." else ""}dockerfile" val platformSuffix = if (isWindows) "-windows" else "" @@ -167,7 +165,6 @@ fun configureImage( } else if (vm == "openj9") { if (isWindows) { // ibm-semeru-runtimes doesn't publish windows images - // adoptopenjdk doesn't publish Windows 2022 images (and is deprecated) throw GradleException("Unexpected vm: $vm") } else { "ibm-semeru-runtimes:open-$jdk-jdk" @@ -201,7 +198,7 @@ fun configureImage( inputDir.set(dockerWorkingDir) images.add(image) - dockerFile.set(File(dockerWorkingDir, dockerFileName)) + dockerFile.set(File(dockerWorkingDir.get().asFile, dockerFileName)) buildArgs.set(extraArgs + mapOf("jdk" to jdk, "vm" to vm, "version" to version, "jdkImage" to jdkImage)) doLast { matrix.add(image) @@ -232,7 +229,6 @@ fun createDockerTasks(parentTask: TaskProvider, isWindows: Boolean) { for (vm in entry.vm) { if (vm == "openj9" && isWindows) { // ibm-semeru-runtimes doesn't publish windows images - // adoptopenjdk is deprecated and doesn't publish Windows 2022 images continue } for (jdk in entry.jdk) { diff --git a/smoke-tests/images/servlet/servlet-3.0/src/main/java/io/opentelemetry/smoketest/matrix/AsyncGreetingServlet.java b/smoke-tests/images/servlet/servlet-3.0/src/main/java/io/opentelemetry/smoketest/matrix/AsyncGreetingServlet.java index 7282dc4f666c..4a1a4e11a06c 100644 --- a/smoke-tests/images/servlet/servlet-3.0/src/main/java/io/opentelemetry/smoketest/matrix/AsyncGreetingServlet.java +++ b/smoke-tests/images/servlet/servlet-3.0/src/main/java/io/opentelemetry/smoketest/matrix/AsyncGreetingServlet.java @@ -6,17 +6,21 @@ package io.opentelemetry.smoketest.matrix; import java.util.concurrent.BlockingQueue; +import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; import javax.servlet.AsyncContext; +import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @SuppressWarnings("SystemOut") -public class AsyncGreetingServlet extends GreetingServlet { +public class AsyncGreetingServlet extends HttpServlet { private static final long serialVersionUID = 1L; + private static final String LATCH_KEY = "LATCH_KEY"; private static final BlockingQueue jobQueue = new LinkedBlockingQueue<>(); private static final ExecutorService executor = Executors.newFixedThreadPool(2); @@ -48,11 +52,29 @@ public void destroy() { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) { + CountDownLatch latch = null; System.err.println("start async request"); AsyncContext ac = req.startAsync(req, resp); + boolean isPayara = + "org.apache.catalina.connector.AsyncContextImpl".equals(ac.getClass().getName()); + if (isPayara) { + latch = new CountDownLatch(1); + req.setAttribute(LATCH_KEY, latch); + } System.err.println("add async request to queue"); jobQueue.add(ac); System.err.println("async request added to queue"); + // Payara has a race condition between exiting from servlet and calling AsyncContext.dispatch + // from background thread which can result in dispatch not happening. To work around this we + // wait on payara for the dispatch call and only after that exit from servlet code. + if (isPayara) { + try { + latch.await(30, TimeUnit.SECONDS); + System.err.println("latch released"); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } } private static void handleRequest(AsyncContext ac) { @@ -65,5 +87,9 @@ private static void handleRequest(AsyncContext ac) { throwable.printStackTrace(); throw throwable; } + CountDownLatch latch = (CountDownLatch) ac.getRequest().getAttribute(LATCH_KEY); + if (latch != null) { + latch.countDown(); + } } } diff --git a/smoke-tests/images/servlet/servlet-5.0/src/main/java/io/opentelemetry/smoketest/matrix/AsyncGreetingServlet.java b/smoke-tests/images/servlet/servlet-5.0/src/main/java/io/opentelemetry/smoketest/matrix/AsyncGreetingServlet.java index 4995451a4cb4..7a7054e959c8 100644 --- a/smoke-tests/images/servlet/servlet-5.0/src/main/java/io/opentelemetry/smoketest/matrix/AsyncGreetingServlet.java +++ b/smoke-tests/images/servlet/servlet-5.0/src/main/java/io/opentelemetry/smoketest/matrix/AsyncGreetingServlet.java @@ -6,16 +6,21 @@ package io.opentelemetry.smoketest.matrix; import jakarta.servlet.AsyncContext; +import jakarta.servlet.http.HttpServlet; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import java.util.concurrent.BlockingQueue; +import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; @SuppressWarnings("SystemOut") -public class AsyncGreetingServlet extends GreetingServlet { +public class AsyncGreetingServlet extends HttpServlet { private static final long serialVersionUID = 1L; + + private static final String LATCH_KEY = "LATCH_KEY"; private static final BlockingQueue jobQueue = new LinkedBlockingQueue<>(); private static final ExecutorService executor = Executors.newFixedThreadPool(2); @@ -47,11 +52,29 @@ public void destroy() { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) { + CountDownLatch latch = null; System.err.println("start async request"); AsyncContext ac = req.startAsync(req, resp); + boolean isPayara = + "org.apache.catalina.connector.AsyncContextImpl".equals(ac.getClass().getName()); + if (isPayara) { + latch = new CountDownLatch(1); + req.setAttribute(LATCH_KEY, latch); + } System.err.println("add async request to queue"); jobQueue.add(ac); System.err.println("async request added to queue"); + // Payara has a race condition between exiting from servlet and calling AsyncContext.dispatch + // from background thread which can result in dispatch not happening. To work around this we + // wait on payara for the dispatch call and only after that exit from servlet code. + if (isPayara) { + try { + latch.await(30, TimeUnit.SECONDS); + System.err.println("latch released"); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } } private static void handleRequest(AsyncContext ac) { @@ -64,5 +87,9 @@ private static void handleRequest(AsyncContext ac) { throwable.printStackTrace(); throw throwable; } + CountDownLatch latch = (CountDownLatch) ac.getRequest().getAttribute(LATCH_KEY); + if (latch != null) { + latch.countDown(); + } } } diff --git a/smoke-tests/images/servlet/src/jetty.dockerfile b/smoke-tests/images/servlet/src/jetty.dockerfile index 4dea5a5e683a..45be3e3079e5 100644 --- a/smoke-tests/images/servlet/src/jetty.dockerfile +++ b/smoke-tests/images/servlet/src/jetty.dockerfile @@ -15,6 +15,7 @@ RUN mkdir $JETTY_BASE && \ cd $JETTY_BASE && \ # depending on Jetty version one of the following commands should succeed java -jar /server/start.jar --add-module=ext,server,jsp,resources,deploy,jstl,websocket,http || \ + java -jar /server/start.jar --add-module=ext,server,ee10-jsp,resources,ee10-deploy,ee10-jstl,http || \ java -jar /server/start.jar --add-to-start=ext,server,jsp,resources,deploy,jstl,websocket,http WORKDIR $JETTY_BASE diff --git a/smoke-tests/images/servlet/src/jetty.windows.dockerfile b/smoke-tests/images/servlet/src/jetty.windows.dockerfile index f8010752e852..230589cda0b9 100644 --- a/smoke-tests/images/servlet/src/jetty.windows.dockerfile +++ b/smoke-tests/images/servlet/src/jetty.windows.dockerfile @@ -18,7 +18,8 @@ ENV JETTY_BASE=/base WORKDIR $JETTY_BASE # depending on Jetty version one of the following commands should succeed RUN java -jar /server/start.jar --add-module=ext,server,jsp,resources,deploy,jstl,websocket,http; \ - if ($LASTEXITCODE -ne 0) { java -jar /server/start.jar --add-to-start=ext,server,jsp,resources,deploy,jstl,websocket,http } + if ($LASTEXITCODE -ne 0) { java -jar /server/start.jar --add-module=ext,server,ee10-jsp,resources,ee10-deploy,ee10-jstl,http; \ + if ($LASTEXITCODE -ne 0) { java -jar /server/start.jar --add-to-start=ext,server,jsp,resources,deploy,jstl,websocket,http }} CMD java -jar /server/start.jar diff --git a/smoke-tests/images/servlet/src/wildfly.dockerfile b/smoke-tests/images/servlet/src/wildfly.dockerfile index dd03cebd1e40..8831c9b4756b 100644 --- a/smoke-tests/images/servlet/src/wildfly.dockerfile +++ b/smoke-tests/images/servlet/src/wildfly.dockerfile @@ -8,7 +8,7 @@ ARG baseDownloadUrl # 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 && \ +RUN groupadd -r jboss -g 1001 && useradd -u 1001 -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 diff --git a/smoke-tests/images/spring-boot/build.gradle.kts b/smoke-tests/images/spring-boot/build.gradle.kts index b9ee6e581468..2dc247e3064e 100644 --- a/smoke-tests/images/spring-boot/build.gradle.kts +++ b/smoke-tests/images/spring-boot/build.gradle.kts @@ -5,12 +5,12 @@ plugins { id("otel.java-conventions") id("com.google.cloud.tools.jib") - id("org.springframework.boot") version "2.6.6" + id("org.springframework.boot") version "2.6.15" } dependencies { implementation(platform("io.opentelemetry:opentelemetry-bom:1.0.0")) - implementation(platform("org.springframework.boot:spring-boot-dependencies:2.6.6")) + implementation(platform("org.springframework.boot:spring-boot-dependencies:2.6.15")) implementation("io.opentelemetry:opentelemetry-api") implementation(project(":instrumentation-annotations")) @@ -20,7 +20,7 @@ dependencies { configurations.runtimeClasspath { resolutionStrategy { // requires old logback (and therefore also old slf4j) - force("ch.qos.logback:logback-classic:1.2.11") + force("ch.qos.logback:logback-classic:1.2.13") force("org.slf4j:slf4j-api:1.7.36") } } @@ -37,6 +37,11 @@ java { targetCompatibility = JavaVersion.VERSION_1_8 } +springBoot { + buildInfo { + } +} + jib { from.image = "openjdk:$targetJDK" to.image = "ghcr.io/open-telemetry/opentelemetry-java-instrumentation/smoke-test-spring-boot:jdk$targetJDK-$tag" diff --git a/smoke-tests/src/test/groovy/io/opentelemetry/smoketest/AppServerTest.groovy b/smoke-tests/src/test/groovy/io/opentelemetry/smoketest/AppServerTest.groovy index 6fccace0d826..bbb520c764fe 100644 --- a/smoke-tests/src/test/groovy/io/opentelemetry/smoketest/AppServerTest.groovy +++ b/smoke-tests/src/test/groovy/io/opentelemetry/smoketest/AppServerTest.groovy @@ -6,15 +6,18 @@ package io.opentelemetry.smoketest import io.opentelemetry.proto.trace.v1.Span +import io.opentelemetry.semconv.ClientAttributes +import io.opentelemetry.semconv.NetworkAttributes +import io.opentelemetry.semconv.UrlAttributes import spock.lang.Shared import spock.lang.Unroll import java.util.jar.Attributes import java.util.jar.JarFile -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 io.opentelemetry.semconv.incubating.OsIncubatingAttributes.OS_TYPE +import static io.opentelemetry.semconv.incubating.OsIncubatingAttributes.OsTypeValues.LINUX +import static io.opentelemetry.semconv.incubating.OsIncubatingAttributes.OsTypeValues.WINDOWS import static org.junit.Assume.assumeFalse import static org.junit.Assume.assumeTrue @@ -26,6 +29,8 @@ abstract class AppServerTest extends SmokeTest { @Shared boolean isWindows + private static final String TELEMETRY_DISTRO_VERSION = "telemetry.distro.version" + def setupSpec() { (serverVersion, jdk) = getAppServer() isWindows = System.getProperty("os.name").toLowerCase().contains("windows") && @@ -54,7 +59,7 @@ abstract class AppServerTest extends SmokeTest { @Override protected String getTargetImage(String jdk, String serverVersion, boolean windows) { String platformSuffix = windows ? "-windows" : "" - String extraTag = "20230614.5268785245" + String extraTag = "20240216.7928274208" String fullSuffix = "${serverVersion}-jdk$jdk$platformSuffix-$extraTag" return getTargetImagePrefix() + ":" + fullSuffix } @@ -85,6 +90,10 @@ abstract class AppServerTest extends SmokeTest { true } + boolean testJsp() { + 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) { @@ -112,20 +121,22 @@ abstract class AppServerTest extends SmokeTest { traces.countSpansByName(getSpanName('/app/headers')) == 1 and: "The span for the initial web request" - traces.countFilteredAttributes("http.target", "/app/greeting") == 1 + traces.countFilteredAttributes(UrlAttributes.URL_PATH.key, "/app/greeting") == 1 and: "Client span for the remote call" - traces.countFilteredAttributes("http.url", "http://localhost:8080/app/headers") == 1 + traces.countFilteredAttributes(UrlAttributes.URL_FULL.key, "http://localhost:8080/app/headers") == 1 and: "Server span for the remote call" - traces.countFilteredAttributes("http.target", "/app/headers") == 1 + traces.countFilteredAttributes(UrlAttributes.URL_PATH.key, "/app/headers") == 1 + + and: "Number of spans with client address" + traces.countFilteredAttributes(ClientAttributes.CLIENT_ADDRESS.key, "127.0.0.1") == 1 and: "Number of spans with http protocol version" - traces.countFilteredAttributes("net.protocol.name", "http") == 3 - traces.countFilteredAttributes("net.protocol.version", "1.1") == 3 + traces.countFilteredAttributes(NetworkAttributes.NETWORK_PROTOCOL_VERSION.key, "1.1") == 3 and: "Number of spans tagged with current otel library version" - traces.countFilteredResourceAttributes("telemetry.auto.version", currentAgentVersion) == 3 + traces.countFilteredResourceAttributes(TELEMETRY_DISTRO_VERSION, currentAgentVersion) == 3 and: "Number of spans tagged with expected OS type" traces.countFilteredResourceAttributes(OS_TYPE.key, isWindows ? WINDOWS : LINUX) == 3 @@ -157,10 +168,10 @@ abstract class AppServerTest extends SmokeTest { traces.countSpansByName(getSpanName('/app/hello.txt')) == 1 and: "The span for the initial web request" - traces.countFilteredAttributes("http.target", "/app/hello.txt") == 1 + traces.countFilteredAttributes(UrlAttributes.URL_PATH.key, "/app/hello.txt") == 1 and: "Number of spans tagged with current otel library version" - traces.countFilteredResourceAttributes("telemetry.auto.version", currentAgentVersion) == 1 + traces.countFilteredResourceAttributes(TELEMETRY_DISTRO_VERSION, currentAgentVersion) == 1 and: "Number of spans tagged with expected OS type" traces.countFilteredResourceAttributes(OS_TYPE.key, isWindows ? WINDOWS : LINUX) == 1 @@ -191,10 +202,10 @@ abstract class AppServerTest extends SmokeTest { traces.countSpansByName(getSpanName('/app/file-that-does-not-exist')) == 1 and: "The span for the initial web request" - traces.countFilteredAttributes("http.target", "/app/file-that-does-not-exist") == 1 + traces.countFilteredAttributes(UrlAttributes.URL_PATH.key, "/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() + traces.countFilteredResourceAttributes(TELEMETRY_DISTRO_VERSION, currentAgentVersion) == traces.countSpans() and: "Number of spans tagged with expected OS type" traces.countFilteredResourceAttributes(OS_TYPE.key, isWindows ? WINDOWS : LINUX) == traces.countSpans() @@ -227,14 +238,13 @@ abstract class AppServerTest extends SmokeTest { traces.countSpansByName(getSpanName('/app/WEB-INF/web.xml')) == 1 and: "The span for the initial web request" - traces.countFilteredAttributes("http.target", "/app/WEB-INF/web.xml") == 1 + traces.countFilteredAttributes(UrlAttributes.URL_PATH.key, "/app/WEB-INF/web.xml") == 1 and: "Number of spans with http protocol version" - traces.countFilteredAttributes("net.protocol.name", "http") == 1 - traces.countFilteredAttributes("net.protocol.version", "1.1") == 1 + traces.countFilteredAttributes(NetworkAttributes.NETWORK_PROTOCOL_VERSION.key, "1.1") == 1 and: "Number of spans tagged with current otel library version" - traces.countFilteredResourceAttributes("telemetry.auto.version", currentAgentVersion) == traces.countSpans() + traces.countFilteredResourceAttributes(TELEMETRY_DISTRO_VERSION, currentAgentVersion) == traces.countSpans() and: "Number of spans tagged with expected OS type" traces.countFilteredResourceAttributes(OS_TYPE.key, isWindows ? WINDOWS : LINUX) == traces.countSpans() @@ -270,10 +280,10 @@ abstract class AppServerTest extends SmokeTest { traces.countFilteredEventAttributes('exception.message', 'This is expected') == 1 and: "The span for the initial web request" - traces.countFilteredAttributes("http.target", "/app/exception") == 1 + traces.countFilteredAttributes(UrlAttributes.URL_PATH.key, "/app/exception") == 1 and: "Number of spans tagged with current otel library version" - traces.countFilteredResourceAttributes("telemetry.auto.version", currentAgentVersion) == traces.countSpans() + traces.countFilteredResourceAttributes(TELEMETRY_DISTRO_VERSION, currentAgentVersion) == traces.countSpans() and: "Number of spans tagged with expected OS type" traces.countFilteredResourceAttributes(OS_TYPE.key, isWindows ? WINDOWS : LINUX) == traces.countSpans() @@ -305,14 +315,13 @@ abstract class AppServerTest extends SmokeTest { 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.target", "/this-is-definitely-not-there-but-there-should-be-a-trace-nevertheless") == 1 + traces.countFilteredAttributes(UrlAttributes.URL_PATH.key, "/this-is-definitely-not-there-but-there-should-be-a-trace-nevertheless") == 1 and: "Number of spans with http protocol version" - traces.countFilteredAttributes("net.protocol.name", "http") == 1 - traces.countFilteredAttributes("net.protocol.version", "1.1") == 1 + traces.countFilteredAttributes(NetworkAttributes.NETWORK_PROTOCOL_VERSION.key, "1.1") == 1 and: "Number of spans tagged with current otel library version" - traces.countFilteredResourceAttributes("telemetry.auto.version", currentAgentVersion) == traces.countSpans() + traces.countFilteredResourceAttributes(TELEMETRY_DISTRO_VERSION, currentAgentVersion) == traces.countSpans() and: "Number of spans tagged with expected OS type" traces.countFilteredResourceAttributes(OS_TYPE.key, isWindows ? WINDOWS : LINUX) == traces.countSpans() @@ -347,20 +356,19 @@ abstract class AppServerTest extends SmokeTest { traces.countSpansByName(getSpanName('/app/headers')) == 1 and: "The span for the initial web request" - traces.countFilteredAttributes("http.target", "/app/asyncgreeting") == 1 + traces.countFilteredAttributes(UrlAttributes.URL_PATH.key, "/app/asyncgreeting") == 1 and: "Client span for the remote call" - traces.countFilteredAttributes("http.url", "http://localhost:8080/app/headers") == 1 + traces.countFilteredAttributes(UrlAttributes.URL_FULL.key, "http://localhost:8080/app/headers") == 1 and: "Server span for the remote call" - traces.countFilteredAttributes("http.target", "/app/headers") == 1 + traces.countFilteredAttributes(UrlAttributes.URL_PATH.key, "/app/headers") == 1 and: "Number of spans with http protocol version" - traces.countFilteredAttributes("net.protocol.name", "http") == 3 - traces.countFilteredAttributes("net.protocol.version", "1.1") == 3 + traces.countFilteredAttributes(NetworkAttributes.NETWORK_PROTOCOL_VERSION.key, "1.1") == 3 and: "Number of spans tagged with current otel library version" - traces.countFilteredResourceAttributes("telemetry.auto.version", currentAgentVersion) == 3 + traces.countFilteredResourceAttributes(TELEMETRY_DISTRO_VERSION, currentAgentVersion) == 3 and: "Number of spans tagged with expected OS type" traces.countFilteredResourceAttributes(OS_TYPE.key, isWindows ? WINDOWS : LINUX) == 3 @@ -371,6 +379,8 @@ abstract class AppServerTest extends SmokeTest { @Unroll def "JSP smoke test for Snippet Injection"() { + assumeTrue(testJsp()) + when: def response = client().get("/app/jsp").aggregate().join() TraceInspector traces = new TraceInspector(waitForTraces()) diff --git a/smoke-tests/src/test/groovy/io/opentelemetry/smoketest/CrashEarlyJdk8Test.groovy b/smoke-tests/src/test/groovy/io/opentelemetry/smoketest/CrashEarlyJdk8Test.groovy index 92541b66d136..c6f79a2fea6d 100644 --- a/smoke-tests/src/test/groovy/io/opentelemetry/smoketest/CrashEarlyJdk8Test.groovy +++ b/smoke-tests/src/test/groovy/io/opentelemetry/smoketest/CrashEarlyJdk8Test.groovy @@ -33,7 +33,7 @@ class CrashEarlyJdk8Test extends Specification { def "test crash on early jdk8"() { setup: GenericContainer target = - new GenericContainer<>(DockerImageName.parse("azul/zulu-openjdk:8u31")) + new GenericContainer<>(DockerImageName.parse("ghcr.io/open-telemetry/opentelemetry-java-instrumentation/smoke-test-zulu-openjdk-8u31:20240709.9848833570")) .withStartupTimeout(Duration.ofMinutes(5)) .withLogConsumer(new Slf4jLogConsumer(logger)) .withCopyFileToContainer( diff --git a/smoke-tests/src/test/groovy/io/opentelemetry/smoketest/GrpcSmokeTest.groovy b/smoke-tests/src/test/groovy/io/opentelemetry/smoketest/GrpcSmokeTest.groovy index 63db1e8c2e31..66c3970353ba 100644 --- a/smoke-tests/src/test/groovy/io/opentelemetry/smoketest/GrpcSmokeTest.groovy +++ b/smoke-tests/src/test/groovy/io/opentelemetry/smoketest/GrpcSmokeTest.groovy @@ -51,7 +51,7 @@ class GrpcSmokeTest extends SmokeTest { countSpansByName(traces, 'opentelemetry.proto.collector.trace.v1.TraceService/Export') == 1 countSpansByName(traces, 'TestService.withSpan') == 1 - [currentAgentVersion] as Set == findResourceAttribute(traces, "telemetry.auto.version") + [currentAgentVersion] as Set == findResourceAttribute(traces, "telemetry.distro.version") .map { it.stringValue } .collect(toSet()) diff --git a/smoke-tests/src/test/groovy/io/opentelemetry/smoketest/JettyJpmsSmokeTest.groovy b/smoke-tests/src/test/groovy/io/opentelemetry/smoketest/JettyJpmsSmokeTest.groovy index e8c6416faa40..551b56d2fa3b 100644 --- a/smoke-tests/src/test/groovy/io/opentelemetry/smoketest/JettyJpmsSmokeTest.groovy +++ b/smoke-tests/src/test/groovy/io/opentelemetry/smoketest/JettyJpmsSmokeTest.groovy @@ -15,15 +15,12 @@ abstract class JettyJpmsSmokeTest extends JettySmokeTest { } } -@AppServer(version = "11.0.15", jdk = "11") +@AppServer(version = "11.0.19", jdk = "11") class Jetty11JpmsJdk11 extends JettyJpmsSmokeTest { } -@AppServer(version = "11.0.15", jdk = "17") +@AppServer(version = "11.0.19", jdk = "17") class Jetty11JpmsJdk17 extends JettyJpmsSmokeTest { } -@AppServer(version = "11.0.15", jdk = "20") -class Jetty11JpmsJdk20 extends JettyJpmsSmokeTest { -} -@AppServer(version = "11.0.15", jdk = "21") +@AppServer(version = "11.0.19", jdk = "21") class Jetty11JpmsJdk21 extends JettyJpmsSmokeTest { } diff --git a/smoke-tests/src/test/groovy/io/opentelemetry/smoketest/JettySmokeTest.groovy b/smoke-tests/src/test/groovy/io/opentelemetry/smoketest/JettySmokeTest.groovy index fa0cd96ce4b3..c750dd221754 100644 --- a/smoke-tests/src/test/groovy/io/opentelemetry/smoketest/JettySmokeTest.groovy +++ b/smoke-tests/src/test/groovy/io/opentelemetry/smoketest/JettySmokeTest.groovy @@ -19,94 +19,98 @@ abstract class JettySmokeTest extends AppServerTest { } } -@AppServer(version = "9.4.51", jdk = "8") +@AppServer(version = "9.4.53", jdk = "8") class Jetty9Jdk8 extends JettySmokeTest { } -@AppServer(version = "9.4.51", jdk = "11") -class Jetty9Jdk11 extends JettySmokeTest { +@AppServer(version = "9.4.53", jdk = "8-openj9") +class Jetty9Jdk8Openj9 extends JettySmokeTest { } -@AppServer(version = "9.4.51", jdk = "17") -class Jetty9Jdk17 extends JettySmokeTest { +@AppServer(version = "9.4.53", jdk = "11") +class Jetty9Jdk11 extends JettySmokeTest { } -@AppServer(version = "9.4.51", jdk = "20") -class Jetty9Jdk20 extends JettySmokeTest { +@AppServer(version = "9.4.53", jdk = "11-openj9") +class Jetty9Jdk11Openj9 extends JettySmokeTest { } -@AppServer(version = "9.4.51", jdk = "21") -class Jetty9Jdk21 extends JettySmokeTest { +@AppServer(version = "9.4.53", jdk = "17") +class Jetty9Jdk17 extends JettySmokeTest { } -@AppServer(version = "9.4.51", jdk = "8-openj9") -class Jetty9Jdk8Openj9 extends JettySmokeTest { +@AppServer(version = "9.4.53", jdk = "17-openj9") +class Jetty9Jdk17Openj9 extends JettySmokeTest { } -@AppServer(version = "9.4.51", jdk = "11-openj9") -class Jetty9Jdk11Openj9 extends JettySmokeTest { +@AppServer(version = "9.4.53", jdk = "21") +class Jetty9Jdk21 extends JettySmokeTest { } -@AppServer(version = "9.4.51", jdk = "17-openj9") -class Jetty9Jdk17Openj9 extends JettySmokeTest { +@AppServer(version = "9.4.53", jdk = "21-openj9") +class Jetty9Jdk21Openj9 extends JettySmokeTest { } -@AppServer(version = "9.4.51", jdk = "18-openj9") -class Jetty9Jdk18Openj9 extends JettySmokeTest { +@AppServer(version = "10.0.19", jdk = "11") +class Jetty10Jdk11 extends JettySmokeTest { } -@AppServer(version = "10.0.15", jdk = "11") -class Jetty10Jdk11 extends JettySmokeTest { +@AppServer(version = "10.0.19", jdk = "11-openj9") +class Jetty10Jdk11Openj9 extends JettySmokeTest { } -@AppServer(version = "10.0.15", jdk = "17") +@AppServer(version = "10.0.19", jdk = "17") class Jetty10Jdk17 extends JettySmokeTest { } -@AppServer(version = "10.0.15", jdk = "20") -class Jetty10Jdk20 extends JettySmokeTest { +@AppServer(version = "10.0.19", jdk = "17-openj9") +class Jetty10Jdk17Openj9 extends JettySmokeTest { } -@AppServer(version = "10.0.15", jdk = "21") +@AppServer(version = "10.0.19", jdk = "21") class Jetty10Jdk21 extends JettySmokeTest { } -@AppServer(version = "10.0.15", jdk = "11-openj9") -class Jetty10Jdk11Openj9 extends JettySmokeTest { -} - -@AppServer(version = "10.0.15", jdk = "17-openj9") -class Jetty10Jdk17Openj9 extends JettySmokeTest { +@AppServer(version = "10.0.19", jdk = "21-openj9") +class Jetty10Jdk21Openj9 extends JettySmokeTest { } -@AppServer(version = "10.0.15", jdk = "18-openj9") -class Jetty10Jdk18Openj9 extends JettySmokeTest { +@AppServer(version = "11.0.19", jdk = "11") +class Jetty11Jdk11 extends JettySmokeTest { } -@AppServer(version = "11.0.15", jdk = "11") -class Jetty11Jdk11 extends JettySmokeTest { +@AppServer(version = "11.0.19", jdk = "11-openj9") +class Jetty11Jdk11Openj9 extends JettySmokeTest { } -@AppServer(version = "11.0.15", jdk = "17") +@AppServer(version = "11.0.19", jdk = "17") class Jetty11Jdk17 extends JettySmokeTest { } -@AppServer(version = "11.0.15", jdk = "20") -class Jetty11Jdk20 extends JettySmokeTest { +@AppServer(version = "11.0.19", jdk = "17-openj9") +class Jetty11Jdk17Openj9 extends JettySmokeTest { } -@AppServer(version = "11.0.15", jdk = "21") +@AppServer(version = "11.0.19", jdk = "21") class Jetty11Jdk21 extends JettySmokeTest { } -@AppServer(version = "11.0.15", jdk = "11-openj9") -class Jetty11Jdk11Openj9 extends JettySmokeTest { +@AppServer(version = "11.0.19", jdk = "21-openj9") +class Jetty11Jdk21Openj9 extends JettySmokeTest { } -@AppServer(version = "11.0.15", jdk = "17-openj9") -class Jetty11Jdk17Openj9 extends JettySmokeTest { +@AppServer(version = "12.0.6", jdk = "17") +class Jetty12Jdk17 extends JettySmokeTest { +} + +@AppServer(version = "12.0.6", jdk = "17-openj9") +class Jetty12Jdk17Openj9 extends JettySmokeTest { +} + +@AppServer(version = "12.0.6", jdk = "21") +class Jetty12Jdk21 extends JettySmokeTest { } -@AppServer(version = "11.0.15", jdk = "18-openj9") -class Jetty11Jdk18Openj9 extends JettySmokeTest { +@AppServer(version = "12.0.6", jdk = "21-openj9") +class Jetty12Jdk21Openj9 extends JettySmokeTest { } diff --git a/smoke-tests/src/test/groovy/io/opentelemetry/smoketest/LibertyServletOnlySmokeTest.groovy b/smoke-tests/src/test/groovy/io/opentelemetry/smoketest/LibertyServletOnlySmokeTest.groovy index 4fcc8c05d2c8..aea3d9d04bcf 100644 --- a/smoke-tests/src/test/groovy/io/opentelemetry/smoketest/LibertyServletOnlySmokeTest.groovy +++ b/smoke-tests/src/test/groovy/io/opentelemetry/smoketest/LibertyServletOnlySmokeTest.groovy @@ -36,6 +36,6 @@ class LibertyServletOnly21Jdk11 extends LibertySmokeTest { class LibertyServletOnly22Jdk11 extends LibertySmokeTest { } -@AppServer(version = "23.0.0.3", jdk = "11") +@AppServer(version = "23.0.0.12", jdk = "11") class LibertyServletOnly23Jdk11 extends LibertySmokeTest { } diff --git a/smoke-tests/src/test/groovy/io/opentelemetry/smoketest/LibertySmokeTest.groovy b/smoke-tests/src/test/groovy/io/opentelemetry/smoketest/LibertySmokeTest.groovy index 262ddeaffff7..1fe98edb6803 100644 --- a/smoke-tests/src/test/groovy/io/opentelemetry/smoketest/LibertySmokeTest.groovy +++ b/smoke-tests/src/test/groovy/io/opentelemetry/smoketest/LibertySmokeTest.groovy @@ -39,22 +39,22 @@ class Liberty20Jdk11Openj9 extends LibertySmokeTest { class Liberty21Jdk8 extends LibertySmokeTest { } -@AppServer(version = "21.0.0.12", jdk = "11") -class Liberty21Jdk11 extends LibertySmokeTest { -} - -@AppServer(version = "21.0.0.12", jdk = "17") -class Liberty21Jdk17 extends LibertySmokeTest { -} - @AppServer(version = "21.0.0.12", jdk = "8-openj9") class Liberty21Jdk8Openj9 extends LibertySmokeTest { } +@AppServer(version = "21.0.0.12", jdk = "11") +class Liberty21Jdk11 extends LibertySmokeTest { +} + @AppServer(version = "21.0.0.12", jdk = "11-openj9") class Liberty21Jdk11Openj9 extends LibertySmokeTest { } +@AppServer(version = "21.0.0.12", jdk = "17") +class Liberty21Jdk17 extends LibertySmokeTest { +} + @AppServer(version = "21.0.0.12", jdk = "17-openj9") class Liberty21Jdk17Openj9 extends LibertySmokeTest { } @@ -63,54 +63,62 @@ class Liberty21Jdk17Openj9 extends LibertySmokeTest { class Liberty22Jdk8 extends LibertySmokeTest { } -@AppServer(version = "22.0.0.12", jdk = "11") -class Liberty22Jdk11 extends LibertySmokeTest { -} - -@AppServer(version = "22.0.0.12", jdk = "17") -class Liberty22Jdk17 extends LibertySmokeTest { -} - -@AppServer(version = "22.0.0.12", jdk = "20") -class Liberty22Jdk20 extends LibertySmokeTest { -} - @AppServer(version = "22.0.0.12", jdk = "8-openj9") class Liberty22Jdk8Openj9 extends LibertySmokeTest { } +@AppServer(version = "22.0.0.12", jdk = "11") +class Liberty22Jdk11 extends LibertySmokeTest { +} + @AppServer(version = "22.0.0.12", jdk = "11-openj9") class Liberty22Jdk11Openj9 extends LibertySmokeTest { } +@AppServer(version = "22.0.0.12", jdk = "17") +class Liberty22Jdk17 extends LibertySmokeTest { +} + @AppServer(version = "22.0.0.12", jdk = "17-openj9") class Liberty22Jdk17Openj9 extends LibertySmokeTest { } -@AppServer(version = "23.0.0.3", jdk = "8") +@AppServer(version = "23.0.0.12", jdk = "8") class Liberty23Jdk8 extends LibertySmokeTest { + @Override + boolean testJsp() { + false + } } -@AppServer(version = "23.0.0.3", jdk = "11") +@AppServer(version = "23.0.0.12", jdk = "8-openj9") +class Liberty23Jdk8Openj9 extends LibertySmokeTest { + @Override + boolean testJsp() { + false + } +} + +@AppServer(version = "23.0.0.12", jdk = "11") class Liberty23Jdk11 extends LibertySmokeTest { } -@AppServer(version = "23.0.0.3", jdk = "17") -class Liberty23Jdk17 extends LibertySmokeTest { +@AppServer(version = "23.0.0.12", jdk = "11-openj9") +class Liberty23Jdk11Openj9 extends LibertySmokeTest { } -@AppServer(version = "23.0.0.3", jdk = "20") -class Liberty23Jdk20 extends LibertySmokeTest { +@AppServer(version = "23.0.0.12", jdk = "17") +class Liberty23Jdk17 extends LibertySmokeTest { } -@AppServer(version = "23.0.0.3", jdk = "8-openj9") -class Liberty23Jdk8Openj9 extends LibertySmokeTest { +@AppServer(version = "23.0.0.12", jdk = "17-openj9") +class Liberty23Jdk17Openj9 extends LibertySmokeTest { } -@AppServer(version = "23.0.0.3", jdk = "11-openj9") -class Liberty23Jdk11Openj9 extends LibertySmokeTest { +@AppServer(version = "23.0.0.12", jdk = "20") +class Liberty23Jdk20 extends LibertySmokeTest { } -@AppServer(version = "23.0.0.3", jdk = "17-openj9") -class Liberty23Jdk17Openj9 extends LibertySmokeTest { +@AppServer(version = "23.0.0.12", jdk = "20-openj9") +class Liberty23Jdk20Openj9 extends LibertySmokeTest { } diff --git a/smoke-tests/src/test/groovy/io/opentelemetry/smoketest/LogsSmokeTest.groovy b/smoke-tests/src/test/groovy/io/opentelemetry/smoketest/LogsSmokeTest.groovy index 21791699c9fa..4818a056936b 100644 --- a/smoke-tests/src/test/groovy/io/opentelemetry/smoketest/LogsSmokeTest.groovy +++ b/smoke-tests/src/test/groovy/io/opentelemetry/smoketest/LogsSmokeTest.groovy @@ -20,11 +20,6 @@ class LogsSmokeTest extends SmokeTest { "ghcr.io/open-telemetry/opentelemetry-java-instrumentation/smoke-test-spring-boot:jdk$jdk-20211213.1570880324" } - @Override - protected Map getExtraEnv() { - return Map.of("OTEL_LOGS_EXPORTER", "otlp") - } - @Override protected TargetWaitStrategy getWaitStrategy() { return new TargetWaitStrategy.Log(Duration.ofMinutes(1), ".*Started SpringbootApplication in.*") diff --git a/smoke-tests/src/test/groovy/io/opentelemetry/smoketest/PayaraSmokeTest.groovy b/smoke-tests/src/test/groovy/io/opentelemetry/smoketest/PayaraSmokeTest.groovy index 25ee133e2396..435a1d6cf9db 100644 --- a/smoke-tests/src/test/groovy/io/opentelemetry/smoketest/PayaraSmokeTest.groovy +++ b/smoke-tests/src/test/groovy/io/opentelemetry/smoketest/PayaraSmokeTest.groovy @@ -42,14 +42,14 @@ abstract class PayaraSmokeTest extends AppServerTest { class Payara52020Jdk8 extends PayaraSmokeTest { } -@AppServer(version = "5.2020.6", jdk = "11") -class Payara52020Jdk11 extends PayaraSmokeTest { -} - @AppServer(version = "5.2020.6", jdk = "8-openj9") class Payara52020Jdk8Openj9 extends PayaraSmokeTest { } +@AppServer(version = "5.2020.6", jdk = "11") +class Payara52020Jdk11 extends PayaraSmokeTest { +} + @AppServer(version = "5.2020.6", jdk = "11-openj9") class Payara52020Jdk11Openj9 extends PayaraSmokeTest { } @@ -58,30 +58,34 @@ class Payara52020Jdk11Openj9 extends PayaraSmokeTest { class Payara52021Jdk8 extends PayaraSmokeTest { } -@AppServer(version = "5.2021.8", jdk = "11") -class Payara52021Jdk11 extends PayaraSmokeTest { -} - @AppServer(version = "5.2021.8", jdk = "8-openj9") class Payara52021Jdk8Openj9 extends PayaraSmokeTest { } +@AppServer(version = "5.2021.8", jdk = "11") +class Payara52021Jdk11 extends PayaraSmokeTest { +} + @AppServer(version = "5.2021.8", jdk = "11-openj9") class Payara52021Jdk11Openj9 extends PayaraSmokeTest { } -@AppServer(version = "6.2023.4", jdk = "11") +@AppServer(version = "6.2023.12", jdk = "11") class Payara6Jdk11 extends PayaraSmokeTest { } -@AppServer(version = "6.2023.4", jdk = "11-openj9") +@AppServer(version = "6.2023.12", jdk = "11-openj9") class Payara6Jdk11Openj9 extends PayaraSmokeTest { } -@AppServer(version = "6.2023.4", jdk = "17") +@AppServer(version = "6.2023.12", jdk = "17") class Payara6Jdk17 extends PayaraSmokeTest { } -@AppServer(version = "6.2023.4", jdk = "17-openj9") +@AppServer(version = "6.2023.12", jdk = "17-openj9") class Payara6Jdk17Openj9 extends PayaraSmokeTest { } + +@AppServer(version = "6.2023.12", jdk = "21-openj9") +class Payara6Jdk21Openj9 extends PayaraSmokeTest { +} diff --git a/smoke-tests/src/test/groovy/io/opentelemetry/smoketest/PlaySmokeTest.groovy b/smoke-tests/src/test/groovy/io/opentelemetry/smoketest/PlaySmokeTest.groovy index 50fd148e772e..51a0a39ed808 100644 --- a/smoke-tests/src/test/groovy/io/opentelemetry/smoketest/PlaySmokeTest.groovy +++ b/smoke-tests/src/test/groovy/io/opentelemetry/smoketest/PlaySmokeTest.groovy @@ -6,7 +6,7 @@ package io.opentelemetry.smoketest import io.opentelemetry.proto.collector.trace.v1.ExportTraceServiceRequest -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import io.opentelemetry.semconv.HttpAttributes import spock.lang.IgnoreIf import java.time.Duration @@ -37,7 +37,7 @@ class PlaySmokeTest extends SmokeTest { countSpansByName(traces, 'GET /welcome') == 1 // SERVER span countSpansByName(traces, '/welcome') == 1 // INTERNAL span - new TraceInspector(traces).countFilteredAttributes(SemanticAttributes.HTTP_ROUTE.key, "/welcome") == 1 + new TraceInspector(traces).countFilteredAttributes(HttpAttributes.HTTP_ROUTE.key, "/welcome") == 1 cleanup: stopTarget() diff --git a/smoke-tests/src/test/groovy/io/opentelemetry/smoketest/PrometheusSmokeTest.groovy b/smoke-tests/src/test/groovy/io/opentelemetry/smoketest/PrometheusSmokeTest.groovy index 640dc5e5a629..3fe7da6e3eed 100644 --- a/smoke-tests/src/test/groovy/io/opentelemetry/smoketest/PrometheusSmokeTest.groovy +++ b/smoke-tests/src/test/groovy/io/opentelemetry/smoketest/PrometheusSmokeTest.groovy @@ -42,9 +42,9 @@ class PrometheusSmokeTest extends SmokeTest { then: def prometheusClient = WebClient.of("h1c://localhost:${containerManager.getTargetMappedPort(9090)}") - def prometheusData = prometheusClient.get("/").aggregate().join().contentUtf8() + def prometheusData = prometheusClient.get("/metrics").aggregate().join().contentUtf8() - prometheusData.contains("process_runtime_jvm_memory_usage") + prometheusData.contains("jvm_memory_used") cleanup: stopTarget() diff --git a/smoke-tests/src/test/groovy/io/opentelemetry/smoketest/PropagationTest.groovy b/smoke-tests/src/test/groovy/io/opentelemetry/smoketest/PropagationTest.groovy index f32f0c0029fa..690fdc556c39 100644 --- a/smoke-tests/src/test/groovy/io/opentelemetry/smoketest/PropagationTest.groovy +++ b/smoke-tests/src/test/groovy/io/opentelemetry/smoketest/PropagationTest.groovy @@ -78,14 +78,6 @@ class B3MultiPropagationTest extends PropagationTest { } } -@IgnoreIf({ useWindowsContainers() }) -class JaegerPropagationTest extends PropagationTest { - @Override - protected Map getExtraEnv() { - return ["otel.propagators": "jaeger"] - } -} - @IgnoreIf({ useWindowsContainers() }) class OtTracePropagationTest extends SmokeTest { @Override diff --git a/smoke-tests/src/test/groovy/io/opentelemetry/smoketest/QuarkusSmokeTest.groovy b/smoke-tests/src/test/groovy/io/opentelemetry/smoketest/QuarkusSmokeTest.groovy index d6b380771428..e8d87c20c527 100644 --- a/smoke-tests/src/test/groovy/io/opentelemetry/smoketest/QuarkusSmokeTest.groovy +++ b/smoke-tests/src/test/groovy/io/opentelemetry/smoketest/QuarkusSmokeTest.groovy @@ -5,7 +5,7 @@ package io.opentelemetry.smoketest -import io.opentelemetry.proto.collector.trace.v1.ExportTraceServiceRequest +import io.opentelemetry.semconv.ServiceAttributes import spock.lang.IgnoreIf import spock.lang.Unroll @@ -14,7 +14,6 @@ import java.util.jar.Attributes import java.util.jar.JarFile import static io.opentelemetry.smoketest.TestContainerManager.useWindowsContainers -import static java.util.stream.Collectors.toSet @IgnoreIf({ useWindowsContainers() }) class QuarkusSmokeTest extends SmokeTest { @@ -28,6 +27,11 @@ class QuarkusSmokeTest extends SmokeTest { return new TargetWaitStrategy.Log(Duration.ofMinutes(1), ".*Listening on.*") } + @Override + protected boolean getSetServiceName() { + return false + } + @Unroll def "quarkus smoke test on JDK #jdk"(int jdk) { setup: @@ -37,15 +41,16 @@ class QuarkusSmokeTest extends SmokeTest { when: client().get("/hello").aggregate().join() - Collection traces = waitForTraces() + TraceInspector traces = new TraceInspector(waitForTraces()) + + then: "Expected span names" + traces.countSpansByName('GET /hello') == 1 - then: - countSpansByName(traces, 'GET /hello') == 1 - countSpansByName(traces, 'HelloResource.hello') == 1 + and: "telemetry.distro.version is set" + traces.countFilteredResourceAttributes("telemetry.distro.version", currentAgentVersion) == 1 - [currentAgentVersion] as Set == findResourceAttribute(traces, "telemetry.auto.version") - .map { it.stringValue } - .collect(toSet()) + and: "service.name is detected from manifest" + traces.countFilteredResourceAttributes(ServiceAttributes.SERVICE_NAME.key, "smoke-test-quarkus-images") == 1 cleanup: stopTarget() diff --git a/smoke-tests/src/test/groovy/io/opentelemetry/smoketest/SpringBootSmokeTest.groovy b/smoke-tests/src/test/groovy/io/opentelemetry/smoketest/SpringBootSmokeTest.groovy index eeb463d8d17c..41cca83f26a7 100644 --- a/smoke-tests/src/test/groovy/io/opentelemetry/smoketest/SpringBootSmokeTest.groovy +++ b/smoke-tests/src/test/groovy/io/opentelemetry/smoketest/SpringBootSmokeTest.groovy @@ -21,7 +21,7 @@ import static java.util.stream.Collectors.toSet class SpringBootSmokeTest extends SmokeTest { protected String getTargetImage(String jdk) { - "ghcr.io/open-telemetry/opentelemetry-java-instrumentation/smoke-test-spring-boot:jdk$jdk-20230321.4484174638" + "ghcr.io/open-telemetry/opentelemetry-java-instrumentation/smoke-test-spring-boot:jdk$jdk-20230920.6251727205" } @Override @@ -31,7 +31,7 @@ class SpringBootSmokeTest extends SmokeTest { @Override protected Map getExtraEnv() { - return Collections.singletonMap("OTEL_METRICS_EXPORTER", "otlp") + return ["OTEL_METRICS_EXPORTER": "otlp", "OTEL_RESOURCE_ATTRIBUTES": "foo=bar"] } @Override @@ -52,7 +52,6 @@ class SpringBootSmokeTest extends SmokeTest { then: "spans are exported" response.contentUtf8() == "Hi!" countSpansByName(traces, 'GET /greeting') == 1 - countSpansByName(traces, 'WebController.greeting') == 1 countSpansByName(traces, 'WebController.withSpan') == 1 then: "thread details are recorded" @@ -60,7 +59,7 @@ class SpringBootSmokeTest extends SmokeTest { .allMatch { it.attributesList.stream().map { it.key }.collect(toSet()).containsAll(["thread.id", "thread.name"]) } then: "correct agent version is captured in the resource" - [currentAgentVersion] as Set == findResourceAttribute(traces, "telemetry.auto.version") + [currentAgentVersion] as Set == findResourceAttribute(traces, "telemetry.distro.version") .map { it.stringValue } .collect(toSet()) @@ -82,10 +81,17 @@ class SpringBootSmokeTest extends SmokeTest { then: "JVM metrics are exported" def metrics = new MetricsInspector(waitForMetrics()) - metrics.hasMetricsNamed("process.runtime.jvm.memory.init") - metrics.hasMetricsNamed("process.runtime.jvm.memory.usage") - metrics.hasMetricsNamed("process.runtime.jvm.memory.committed") - metrics.hasMetricsNamed("process.runtime.jvm.memory.limit") + metrics.hasMetricsNamed("jvm.memory.used") + metrics.hasMetricsNamed("jvm.memory.committed") + metrics.hasMetricsNamed("jvm.memory.limit") + metrics.hasMetricsNamed("jvm.memory.used_after_last_gc") + + then: "resource attributes are read from the environment" + def foo = findResourceAttribute(traces, "foo") + .map { it.stringValue } + .findAny() + foo.isPresent() + foo.get() == "bar" then: "service name is autodetected" def serviceName = findResourceAttribute(traces, "service.name") @@ -94,6 +100,13 @@ class SpringBootSmokeTest extends SmokeTest { serviceName.isPresent() serviceName.get() == "otel-spring-test-app" + then: "service version is autodetected" + def serviceVersion = findResourceAttribute(traces, "service.version") + .map { it.stringValue } + .findAny() + serviceVersion.isPresent() + serviceVersion.get() == "1.31.0-alpha-SNAPSHOT" + cleanup: stopTarget() diff --git a/smoke-tests/src/test/groovy/io/opentelemetry/smoketest/SpringBootWithSamplingSmokeTest.groovy b/smoke-tests/src/test/groovy/io/opentelemetry/smoketest/SpringBootWithSamplingSmokeTest.groovy index a61d1407fae5..e69c7271f964 100644 --- a/smoke-tests/src/test/groovy/io/opentelemetry/smoketest/SpringBootWithSamplingSmokeTest.groovy +++ b/smoke-tests/src/test/groovy/io/opentelemetry/smoketest/SpringBootWithSamplingSmokeTest.groovy @@ -47,7 +47,6 @@ class SpringBootWithSamplingSmokeTest extends SmokeTest { 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, 'GET /greeting') - (SAMPLER_PROBABILITY * NUM_TRIES)) <= ALLOWED_DEVIATION cleanup: diff --git a/smoke-tests/src/test/groovy/io/opentelemetry/smoketest/TomcatSmokeTest.groovy b/smoke-tests/src/test/groovy/io/opentelemetry/smoketest/TomcatSmokeTest.groovy index 573ef4be36ab..5a89c2bbd84a 100644 --- a/smoke-tests/src/test/groovy/io/opentelemetry/smoketest/TomcatSmokeTest.groovy +++ b/smoke-tests/src/test/groovy/io/opentelemetry/smoketest/TomcatSmokeTest.groovy @@ -32,102 +32,90 @@ class Tomcat7Jdk8 extends TomcatSmokeTest { class Tomcat7Jdk8Openj9 extends TomcatSmokeTest { } -@AppServer(version = "8.5.88", jdk = "8") +@AppServer(version = "8.5.98", jdk = "8") class Tomcat8Jdk8 extends TomcatSmokeTest { } -@AppServer(version = "8.5.88", jdk = "11") -class Tomcat8Jdk11 extends TomcatSmokeTest { -} - -@AppServer(version = "8.5.88", jdk = "17") -class Tomcat8Jdk17 extends TomcatSmokeTest { +@AppServer(version = "8.5.98", jdk = "8-openj9") +class Tomcat8Jdk8Openj9 extends TomcatSmokeTest { } -@AppServer(version = "8.5.88", jdk = "20") -class Tomcat8Jdk20 extends TomcatSmokeTest { +@AppServer(version = "8.5.98", jdk = "11") +class Tomcat8Jdk11 extends TomcatSmokeTest { } -@AppServer(version = "8.5.88", jdk = "21") -class Tomcat8Jdk21 extends TomcatSmokeTest { +@AppServer(version = "8.5.98", jdk = "11-openj9") +class Tomcat8Jdk11Openj9 extends TomcatSmokeTest { } -@AppServer(version = "8.5.88", jdk = "8-openj9") -class Tomcat8Jdk8Openj9 extends TomcatSmokeTest { +@AppServer(version = "8.5.98", jdk = "17") +class Tomcat8Jdk17 extends TomcatSmokeTest { } -@AppServer(version = "8.5.88", jdk = "11-openj9") -class Tomcat8Jdk11Openj9 extends TomcatSmokeTest { +@AppServer(version = "8.5.98", jdk = "17-openj9") +class Tomcat8Jdk17Openj9 extends TomcatSmokeTest { } -@AppServer(version = "8.5.88", jdk = "17-openj9") -class Tomcat8Jdk17Openj9 extends TomcatSmokeTest { +@AppServer(version = "8.5.98", jdk = "21") +class Tomcat8Jdk21 extends TomcatSmokeTest { } -@AppServer(version = "8.5.88", jdk = "18-openj9") -class Tomcat8Jdk18Openj9 extends TomcatSmokeTest { +@AppServer(version = "8.5.98", jdk = "21-openj9") +class Tomcat8Jdk21Openj9 extends TomcatSmokeTest { } -@AppServer(version = "9.0.74", jdk = "8") +@AppServer(version = "9.0.85", jdk = "8") class Tomcat9Jdk8 extends TomcatSmokeTest { } -@AppServer(version = "9.0.74", jdk = "11") -class Tomcat9Jdk11 extends TomcatSmokeTest { +@AppServer(version = "9.0.85", jdk = "8-openj9") +class Tomcat9Jdk8Openj9 extends TomcatSmokeTest { } -@AppServer(version = "9.0.74", jdk = "17") -class Tomcat9Jdk17 extends TomcatSmokeTest { +@AppServer(version = "9.0.85", jdk = "11") +class Tomcat9Jdk11 extends TomcatSmokeTest { } -@AppServer(version = "9.0.74", jdk = "20") -class Tomcat9Jdk20 extends TomcatSmokeTest { +@AppServer(version = "9.0.85", jdk = "11-openj9") +class Tomcat9Jdk11Openj9 extends TomcatSmokeTest { } -@AppServer(version = "9.0.74", jdk = "21") -class Tomcat9Jdk21 extends TomcatSmokeTest { +@AppServer(version = "9.0.85", jdk = "17") +class Tomcat9Jdk17 extends TomcatSmokeTest { } -@AppServer(version = "9.0.74", jdk = "8-openj9") -class Tomcat9Jdk8Openj9 extends TomcatSmokeTest { +@AppServer(version = "9.0.85", jdk = "17-openj9") +class Tomcat9Jdk17Openj9 extends TomcatSmokeTest { } -@AppServer(version = "9.0.74", jdk = "11-openj9") -class Tomcat9Jdk11Openj9 extends TomcatSmokeTest { +@AppServer(version = "9.0.85", jdk = "21") +class Tomcat9Jdk21 extends TomcatSmokeTest { } -@AppServer(version = "9.0.74", jdk = "17-openj9") -class Tomcat9Jdk17Openj9 extends TomcatSmokeTest { +@AppServer(version = "9.0.85", jdk = "21-openj9") +class Tomcat9Jdk21Openj9 extends TomcatSmokeTest { } -@AppServer(version = "9.0.74", jdk = "18-openj9") -class Tomcat9Jdk18Openj9 extends TomcatSmokeTest { +@AppServer(version = "10.1.18", jdk = "11") +class Tomcat10Jdk11 extends TomcatSmokeTest { } -@AppServer(version = "10.1.8", jdk = "11") -class Tomcat10Jdk11 extends TomcatSmokeTest { +@AppServer(version = "10.1.18", jdk = "11-openj9") +class Tomcat10Jdk11Openj9 extends TomcatSmokeTest { } -@AppServer(version = "10.1.8", jdk = "17") +@AppServer(version = "10.1.18", jdk = "17") class Tomcat10Jdk17 extends TomcatSmokeTest { } -@AppServer(version = "10.1.8", jdk = "20") -class Tomcat10Jdk20 extends TomcatSmokeTest { +@AppServer(version = "10.1.18", jdk = "17-openj9") +class Tomcat10Jdk17Openj9 extends TomcatSmokeTest { } -@AppServer(version = "10.1.8", jdk = "21") +@AppServer(version = "10.1.18", jdk = "21") class Tomcat10Jdk21 extends TomcatSmokeTest { } -@AppServer(version = "10.1.8", jdk = "11-openj9") -class Tomcat10Jdk11Openj9 extends TomcatSmokeTest { -} - -@AppServer(version = "10.1.8", jdk = "17-openj9") -class Tomcat10Jdk17Openj9 extends TomcatSmokeTest { -} - -@AppServer(version = "10.1.8", jdk = "18-openj9") -class Tomcat10Jdk18Openj9 extends TomcatSmokeTest { +@AppServer(version = "10.1.18", jdk = "21-openj9") +class Tomcat10Jdk21Openj9 extends TomcatSmokeTest { } diff --git a/smoke-tests/src/test/groovy/io/opentelemetry/smoketest/TomeeSmokeTest.groovy b/smoke-tests/src/test/groovy/io/opentelemetry/smoketest/TomeeSmokeTest.groovy index 63e047be7f36..f25ca753a945 100644 --- a/smoke-tests/src/test/groovy/io/opentelemetry/smoketest/TomeeSmokeTest.groovy +++ b/smoke-tests/src/test/groovy/io/opentelemetry/smoketest/TomeeSmokeTest.groovy @@ -44,66 +44,58 @@ class Tomee71Jdk8 extends TomeeSmokeTest { class Tomee71Jdk8Openj9 extends TomeeSmokeTest { } -@AppServer(version = "8.0.14", jdk = "8") +@AppServer(version = "8.0.16", jdk = "8") class Tomee8Jdk8 extends TomeeSmokeTest { } -@AppServer(version = "8.0.14", jdk = "11") -class Tomee8Jdk11 extends TomeeSmokeTest { +@AppServer(version = "8.0.16", jdk = "8-openj9") +class Tomee8Jdk8Openj9 extends TomeeSmokeTest { } -@AppServer(version = "8.0.14", jdk = "17") -class Tomee8Jdk17 extends TomeeSmokeTest { +@AppServer(version = "8.0.16", jdk = "11") +class Tomee8Jdk11 extends TomeeSmokeTest { } -@AppServer(version = "8.0.14", jdk = "20") -class Tomee8Jdk20 extends TomeeSmokeTest { +@AppServer(version = "8.0.16", jdk = "11-openj9") +class Tomee8Jdk11Openj9 extends TomeeSmokeTest { } -@AppServer(version = "8.0.14", jdk = "21") -class Tomee8Jdk21 extends TomeeSmokeTest { +@AppServer(version = "8.0.16", jdk = "17") +class Tomee8Jdk17 extends TomeeSmokeTest { } -@AppServer(version = "8.0.14", jdk = "8-openj9") -class Tomee8Jdk8Openj9 extends TomeeSmokeTest { +@AppServer(version = "8.0.16", jdk = "17-openj9") +class Tomee8Jdk17Openj9 extends TomeeSmokeTest { } -@AppServer(version = "8.0.14", jdk = "11-openj9") -class Tomee8Jdk11Openj9 extends TomeeSmokeTest { +@AppServer(version = "8.0.16", jdk = "21") +class Tomee8Jdk21 extends TomeeSmokeTest { } -@AppServer(version = "8.0.14", jdk = "17-openj9") -class Tomee8Jdk17Openj9 extends TomeeSmokeTest { +@AppServer(version = "8.0.16", jdk = "21-openj9") +class Tomee8Jdk21Openj9 extends TomeeSmokeTest { } -@AppServer(version = "8.0.14", jdk = "18-openj9") -class Tomee8Jdk18Openj9 extends TomeeSmokeTest { +@AppServer(version = "9.1.2", jdk = "11") +class Tomee9Jdk11 extends TomeeSmokeTest { } -@AppServer(version = "9.0.0", jdk = "11") -class Tomee9Jdk11 extends TomeeSmokeTest { +@AppServer(version = "9.1.2", jdk = "11-openj9") +class Tomee9Jdk11Openj9 extends TomeeSmokeTest { } -@AppServer(version = "9.0.0", jdk = "17") +@AppServer(version = "9.1.2", jdk = "17") class Tomee9Jdk17 extends TomeeSmokeTest { } -@AppServer(version = "9.0.0", jdk = "20") -class Tomee9Jdk20 extends TomeeSmokeTest { +@AppServer(version = "9.1.2", jdk = "17-openj9") +class Tomee9Jdk17Openj9 extends TomeeSmokeTest { } -@AppServer(version = "9.0.0", jdk = "21") +@AppServer(version = "9.1.2", jdk = "21") class Tomee9Jdk21 extends TomeeSmokeTest { } -@AppServer(version = "9.0.0", jdk = "11-openj9") -class Tomee9Jdk11Openj9 extends TomeeSmokeTest { -} - -@AppServer(version = "9.0.0", jdk = "17-openj9") -class Tomee9Jdk17Openj9 extends TomeeSmokeTest { -} - -@AppServer(version = "9.0.0", jdk = "18-openj9") -class Tomee9Jdk18Openj9 extends TomeeSmokeTest { +@AppServer(version = "9.1.2", jdk = "21-openj9") +class Tomee9Jdk21Openj9 extends TomeeSmokeTest { } diff --git a/smoke-tests/src/test/groovy/io/opentelemetry/smoketest/WildflySmokeTest.groovy b/smoke-tests/src/test/groovy/io/opentelemetry/smoketest/WildflySmokeTest.groovy index b762310e4101..e310a0b60ff9 100644 --- a/smoke-tests/src/test/groovy/io/opentelemetry/smoketest/WildflySmokeTest.groovy +++ b/smoke-tests/src/test/groovy/io/opentelemetry/smoketest/WildflySmokeTest.groovy @@ -32,98 +32,134 @@ class Wildfly13Jdk8Openj9 extends WildflySmokeTest { class Wildfly17Jdk8 extends WildflySmokeTest { } +@AppServer(version = "17.0.1.Final", jdk = "8-openj9") +class Wildfly17Jdk8Openj9 extends WildflySmokeTest { +} + @AppServer(version = "17.0.1.Final", jdk = "11") class Wildfly17Jdk11 extends WildflySmokeTest { } +@AppServer(version = "17.0.1.Final", jdk = "11-openj9") +class Wildfly17Jdk11Openj9 extends WildflySmokeTest { +} + @AppServer(version = "17.0.1.Final", jdk = "17") class Wildfly17Jdk17 extends WildflySmokeTest { } -@AppServer(version = "17.0.1.Final", jdk = "20") -class Wildfly17Jdk20 extends WildflySmokeTest { +@AppServer(version = "17.0.1.Final", jdk = "17-openj9") +class Wildfly17Jdk17Openj9 extends WildflySmokeTest { } @AppServer(version = "17.0.1.Final", jdk = "21") class Wildfly17Jdk21 extends WildflySmokeTest { } +@AppServer(version = "17.0.1.Final", jdk = "21-openj9") +class Wildfly17Jdk21Openj9 extends WildflySmokeTest { +} + @AppServer(version = "21.0.0.Final", jdk = "8") class Wildfly21Jdk8 extends WildflySmokeTest { } +@AppServer(version = "21.0.0.Final", jdk = "8-openj9") +class Wildfly21Jdk8Openj9 extends WildflySmokeTest { +} + @AppServer(version = "21.0.0.Final", jdk = "11") class Wildfly21Jdk11 extends WildflySmokeTest { } +@AppServer(version = "21.0.0.Final", jdk = "11-openj9") +class Wildfly21Jdk11Openj9 extends WildflySmokeTest { +} + @AppServer(version = "21.0.0.Final", jdk = "17") class Wildfly21Jdk17 extends WildflySmokeTest { } -@AppServer(version = "21.0.0.Final", jdk = "20") -class Wildfly21Jdk20 extends WildflySmokeTest { +@AppServer(version = "21.0.0.Final", jdk = "17-openj9") +class Wildfly21Jdk17Openj9 extends WildflySmokeTest { } @AppServer(version = "21.0.0.Final", jdk = "21") class Wildfly21Jdk21 extends WildflySmokeTest { } -@AppServer(version = "28.0.0.Final", jdk = "11") +@AppServer(version = "21.0.0.Final", jdk = "21-openj9") +class Wildfly21Jdk21Openj9 extends WildflySmokeTest { +} + +@AppServer(version = "28.0.1.Final", jdk = "11") class Wildfly28Jdk11 extends WildflySmokeTest { } -@AppServer(version = "28.0.0.Final", jdk = "17") +@AppServer(version = "28.0.1.Final", jdk = "11-openj9") +class Wildfly28Jdk11Openj9 extends WildflySmokeTest { +} + +@AppServer(version = "28.0.1.Final", jdk = "17") class Wildfly28Jdk17 extends WildflySmokeTest { } -@AppServer(version = "28.0.0.Final", jdk = "20") -class Wildfly28Jdk20 extends WildflySmokeTest { +@AppServer(version = "28.0.1.Final", jdk = "17-openj9") +class Wildfly28Jdk17Openj9 extends WildflySmokeTest { } -@AppServer(version = "28.0.0.Final", jdk = "21") +@AppServer(version = "28.0.1.Final", jdk = "21") class Wildfly28Jdk21 extends WildflySmokeTest { } -@AppServer(version = "17.0.1.Final", jdk = "8-openj9") -class Wildfly17Jdk8Openj9 extends WildflySmokeTest { +@AppServer(version = "28.0.1.Final", jdk = "21-openj9") +class Wildfly28Jdk21Openj9 extends WildflySmokeTest { } -@AppServer(version = "17.0.1.Final", jdk = "11-openj9") -class Wildfly17Jdk11Openj9 extends WildflySmokeTest { +@AppServer(version = "29.0.1.Final", jdk = "11") +class Wildfly29Jdk11 extends WildflySmokeTest { } -@AppServer(version = "17.0.1.Final", jdk = "17-openj9") -class Wildfly17Jdk17Openj9 extends WildflySmokeTest { +@AppServer(version = "29.0.1.Final", jdk = "11-openj9") +class Wildfly29Jdk11Openj9 extends WildflySmokeTest { } -@AppServer(version = "17.0.1.Final", jdk = "18-openj9") -class Wildfly17Jdk18Openj9 extends WildflySmokeTest { +@AppServer(version = "29.0.1.Final", jdk = "17") +class Wildfly29Jdk17 extends WildflySmokeTest { } -@AppServer(version = "21.0.0.Final", jdk = "8-openj9") -class Wildfly21Jdk8Openj9 extends WildflySmokeTest { +@AppServer(version = "29.0.1.Final", jdk = "17-openj9") +class Wildfly29Jdk17Openj9 extends WildflySmokeTest { } -@AppServer(version = "21.0.0.Final", jdk = "11-openj9") -class Wildfly21Jdk11Openj9 extends WildflySmokeTest { +@AppServer(version = "29.0.1.Final", jdk = "21") +class Wildfly29Jdk21 extends WildflySmokeTest { } -@AppServer(version = "21.0.0.Final", jdk = "17-openj9") -class Wildfly21Jdk17Openj9 extends WildflySmokeTest { +@AppServer(version = "29.0.1.Final", jdk = "21-openj9") +class Wildfly29Jdk21Openj9 extends WildflySmokeTest { } -@AppServer(version = "21.0.0.Final", jdk = "18-openj9") -class Wildfly21Jdk18Openj9 extends WildflySmokeTest { +@AppServer(version = "30.0.1.Final", jdk = "11") +class Wildfly30Jdk11 extends WildflySmokeTest { } -@AppServer(version = "28.0.0.Final", jdk = "11-openj9") -class Wildfly28Jdk11Openj9 extends WildflySmokeTest { +@AppServer(version = "30.0.1.Final", jdk = "11-openj9") +class Wildfly30Jdk11Openj9 extends WildflySmokeTest { } -@AppServer(version = "28.0.0.Final", jdk = "17-openj9") -class Wildfly28Jdk17Openj9 extends WildflySmokeTest { +@AppServer(version = "30.0.1.Final", jdk = "17") +class Wildfly30Jdk17 extends WildflySmokeTest { +} + +@AppServer(version = "30.0.1.Final", jdk = "17-openj9") +class Wildfly30Jdk17Openj9 extends WildflySmokeTest { +} + +@AppServer(version = "30.0.1.Final", jdk = "21") +class Wildfly30Jdk21 extends WildflySmokeTest { } -@AppServer(version = "28.0.0.Final", jdk = "18-openj9") -class Wildfly28Jdk18Openj9 extends WildflySmokeTest { +@AppServer(version = "30.0.1.Final", jdk = "21-openj9") +class Wildfly30Jdk21Openj9 extends WildflySmokeTest { } diff --git a/smoke-tests/src/test/java/io/opentelemetry/smoketest/AbstractTestContainerManager.java b/smoke-tests/src/test/java/io/opentelemetry/smoketest/AbstractTestContainerManager.java index 2a68f49b2e43..111a6709933a 100644 --- a/smoke-tests/src/test/java/io/opentelemetry/smoketest/AbstractTestContainerManager.java +++ b/smoke-tests/src/test/java/io/opentelemetry/smoketest/AbstractTestContainerManager.java @@ -23,7 +23,18 @@ protected Map getAgentEnvironment( // while modern JVMs understand linux container memory limits, they do not understand windows // container memory limits yet, so we need to explicitly set max heap in order to prevent the // JVM from taking too much memory and hitting the windows container memory limit - environment.put(jvmArgsEnvVarName, "-Xmx512m -javaagent:/" + TARGET_AGENT_FILENAME); + environment.put( + jvmArgsEnvVarName, + "-Xmx512m -javaagent:/" + + TARGET_AGENT_FILENAME + // Liberty20Jdk11, Payara6Jdk11 and Payara6Jdk17 fail with + // java.util.zip.ZipException: Invalid CEN header (invalid zip64 extra data field size) + + " -Djdk.util.zip.disableZip64ExtraFieldValidation=true"); + + // TODO (heya) update smoke tests to run using http/protobuf + // in the meantime, force smoke tests to use grpc protocol for all exporters + environment.put("OTEL_EXPORTER_OTLP_PROTOCOL", "grpc"); + environment.put("OTEL_BSP_MAX_EXPORT_BATCH_SIZE", "1"); environment.put("OTEL_BSP_SCHEDULE_DELAY", "10ms"); environment.put("OTEL_METRIC_EXPORT_INTERVAL", "1000"); @@ -33,6 +44,7 @@ protected Map getAgentEnvironment( } environment.put("OTEL_JAVAAGENT_DEBUG", "true"); environment.put("OTEL_EXPERIMENTAL_JAVASCRIPT_SNIPPET", ""); + environment.put("OTEL_INSTRUMENTATION_RUNTIME_TELEMETRY_PACKAGE_EMITTER_ENABLED", "true"); return environment; } diff --git a/smoke-tests/src/test/java/io/opentelemetry/smoketest/windows/WindowsTestContainerManager.java b/smoke-tests/src/test/java/io/opentelemetry/smoketest/windows/WindowsTestContainerManager.java index 0026380032d2..cdb4f41f2647 100644 --- a/smoke-tests/src/test/java/io/opentelemetry/smoketest/windows/WindowsTestContainerManager.java +++ b/smoke-tests/src/test/java/io/opentelemetry/smoketest/windows/WindowsTestContainerManager.java @@ -341,6 +341,8 @@ private static Waiter createTargetWaiter(TargetWaitStrategy strategy) { // https://github.com/google/error-prone/issues/3090 @SuppressWarnings("MethodCanBeStatic") private interface Waiter { + // errorprone 2.24.0 flags logHandler as unused + @SuppressWarnings("UnusedVariable") default void configureLogger(ContainerLogHandler logHandler) {} void waitFor(Container container); diff --git a/testing-common/build.gradle.kts b/testing-common/build.gradle.kts index 803e8f50d492..d787b154ae0a 100644 --- a/testing-common/build.gradle.kts +++ b/testing-common/build.gradle.kts @@ -34,9 +34,9 @@ dependencies { api("org.junit.jupiter:junit-jupiter-params") api("io.opentelemetry:opentelemetry-api") - api("io.opentelemetry:opentelemetry-semconv") api("io.opentelemetry:opentelemetry-sdk") api("io.opentelemetry:opentelemetry-sdk-testing") + api("io.opentelemetry.semconv:opentelemetry-semconv-incubating") api(project(":instrumentation-api")) api("org.assertj:assertj-core") @@ -47,6 +47,7 @@ dependencies { api("org.slf4j:slf4j-api") compileOnly(project(":testing:armeria-shaded-for-testing", configuration = "shadow")) + compileOnly(project(":javaagent-bootstrap")) compileOnly("com.google.auto.value:auto-value-annotations") annotationProcessor("com.google.auto.value:auto-value") @@ -59,7 +60,7 @@ dependencies { implementation("org.slf4j:jcl-over-slf4j") implementation("org.slf4j:jul-to-slf4j") implementation("io.opentelemetry:opentelemetry-exporter-logging") - implementation(project(":instrumentation-api-semconv")) + api(project(":instrumentation-api-incubator")) annotationProcessor("com.google.auto.service:auto-service") compileOnly("com.google.auto.service:auto-service") diff --git a/testing-common/integration-tests/build.gradle.kts b/testing-common/integration-tests/build.gradle.kts index d503cf2b9bec..54283321482a 100644 --- a/testing-common/integration-tests/build.gradle.kts +++ b/testing-common/integration-tests/build.gradle.kts @@ -3,7 +3,8 @@ plugins { } dependencies { - implementation(project(":testing-common:library-for-integration-tests")) + compileOnly(project(":testing-common:library-for-integration-tests")) + testImplementation(project(":testing-common:library-for-integration-tests")) testCompileOnly(project(":instrumentation-api")) testCompileOnly(project(":javaagent-tooling")) @@ -45,10 +46,27 @@ tasks { jvmArgs("-XX:+IgnoreUnrecognizedVMOptions") } + val testIndyModuleOldBytecodeInstrumentation by registering(Test::class) { + filter { + includeTestsMatching("InstrumentOldBytecode") + } + include("**/InstrumentOldBytecode.*") + jvmArgs("-Dotel.instrumentation.inline-ibm-resource-level.enabled=false") + } + + val testInlineModuleOldBytecodeInstrumentation by registering(Test::class) { + filter { + includeTestsMatching("InstrumentOldBytecode") + } + include("**/InstrumentOldBytecode.*") + jvmArgs("-Dotel.instrumentation.indy-ibm-resource-level.enabled=false") + } + test { filter { excludeTestsMatching("context.FieldInjectionDisabledTest") excludeTestsMatching("context.FieldBackedImplementationTest") + excludeTestsMatching("InstrumentOldBytecode") } // this is needed for AgentInstrumentationSpecificationTest jvmArgs("-Dotel.javaagent.exclude-classes=config.exclude.packagename.*,config.exclude.SomeClass,config.exclude.SomeClass\$NestedClass") diff --git a/testing-common/integration-tests/src/main/java/IndyIbmResourceLevelInstrumentationModule.java b/testing-common/integration-tests/src/main/java/IndyIbmResourceLevelInstrumentationModule.java new file mode 100644 index 000000000000..9dee76b8ac1b --- /dev/null +++ b/testing-common/integration-tests/src/main/java/IndyIbmResourceLevelInstrumentationModule.java @@ -0,0 +1,28 @@ +/* + * 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 IndyIbmResourceLevelInstrumentationModule extends InstrumentationModule { + public IndyIbmResourceLevelInstrumentationModule() { + super("indy-ibm-resource-level"); + } + + @Override + public boolean isIndyModule() { + return true; + } + + @Override + public List typeInstrumentations() { + return singletonList(new IndyResourceLevelInstrumentation()); + } +} diff --git a/testing-common/integration-tests/src/main/java/IndyResourceLevelInstrumentation.java b/testing-common/integration-tests/src/main/java/IndyResourceLevelInstrumentation.java new file mode 100644 index 000000000000..d66fe5b9a992 --- /dev/null +++ b/testing-common/integration-tests/src/main/java/IndyResourceLevelInstrumentation.java @@ -0,0 +1,34 @@ +/* + * 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 IndyResourceLevelInstrumentation 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"); + } + + @SuppressWarnings("unused") + public static class ToStringAdvice { + @Advice.OnMethodExit(suppress = Throwable.class, inline = false) + @Advice.AssignReturned.ToReturned + public static String toStringReplace() { + return "instrumented"; + } + } +} diff --git a/testing-common/integration-tests/src/main/java/IbmResourceLevelInstrumentationModule.java b/testing-common/integration-tests/src/main/java/InlineIbmResourceLevelInstrumentationModule.java similarity index 65% rename from testing-common/integration-tests/src/main/java/IbmResourceLevelInstrumentationModule.java rename to testing-common/integration-tests/src/main/java/InlineIbmResourceLevelInstrumentationModule.java index 518ec9c585ad..4e06e733b6f5 100644 --- a/testing-common/integration-tests/src/main/java/IbmResourceLevelInstrumentationModule.java +++ b/testing-common/integration-tests/src/main/java/InlineIbmResourceLevelInstrumentationModule.java @@ -11,13 +11,13 @@ import java.util.List; @AutoService(InstrumentationModule.class) -public class IbmResourceLevelInstrumentationModule extends InstrumentationModule { - public IbmResourceLevelInstrumentationModule() { - super(IbmResourceLevelInstrumentationModule.class.getName()); +public class InlineIbmResourceLevelInstrumentationModule extends InstrumentationModule { + public InlineIbmResourceLevelInstrumentationModule() { + super("inline-ibm-resource-level"); } @Override public List typeInstrumentations() { - return singletonList(new ResourceLevelInstrumentation()); + return singletonList(new InlineResourceLevelInstrumentation()); } } diff --git a/testing-common/integration-tests/src/main/java/ResourceLevelInstrumentation.java b/testing-common/integration-tests/src/main/java/InlineResourceLevelInstrumentation.java similarity index 92% rename from testing-common/integration-tests/src/main/java/ResourceLevelInstrumentation.java rename to testing-common/integration-tests/src/main/java/InlineResourceLevelInstrumentation.java index e362a0612871..1e4f074e94af 100644 --- a/testing-common/integration-tests/src/main/java/ResourceLevelInstrumentation.java +++ b/testing-common/integration-tests/src/main/java/InlineResourceLevelInstrumentation.java @@ -11,7 +11,7 @@ import net.bytebuddy.description.type.TypeDescription; import net.bytebuddy.matcher.ElementMatcher; -public class ResourceLevelInstrumentation implements TypeInstrumentation { +public class InlineResourceLevelInstrumentation implements TypeInstrumentation { @Override public ElementMatcher typeMatcher() { return named("com.ibm.as400.resource.ResourceLevel"); diff --git a/testing-common/integration-tests/src/main/java/field/VirtualFieldTestHelper.java b/testing-common/integration-tests/src/main/java/field/VirtualFieldTestHelper.java new file mode 100644 index 000000000000..82549acc44db --- /dev/null +++ b/testing-common/integration-tests/src/main/java/field/VirtualFieldTestHelper.java @@ -0,0 +1,36 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package field; + +import io.opentelemetry.instrumentation.api.util.VirtualField; +import library.VirtualFieldTestClass; + +public final class VirtualFieldTestHelper { + + private VirtualFieldTestHelper() {} + + public static void test() { + VirtualFieldTestClass instance = new VirtualFieldTestClass(); + { + VirtualField field = + VirtualField.find(VirtualFieldTestClass.class, String.class); + field.set(instance, "test"); + field.get(instance); + } + { + VirtualField field = + VirtualField.find(VirtualFieldTestClass.class, String[].class); + field.set(instance, new String[] {"test"}); + field.get(instance); + } + { + VirtualField field = + VirtualField.find(VirtualFieldTestClass.class, String[][].class); + field.set(instance, new String[][] {new String[] {"test"}}); + field.get(instance); + } + } +} diff --git a/testing-common/integration-tests/src/main/java/field/VirtualFieldTestInstrumentationModule.java b/testing-common/integration-tests/src/main/java/field/VirtualFieldTestInstrumentationModule.java new file mode 100644 index 000000000000..3a81438eb0e7 --- /dev/null +++ b/testing-common/integration-tests/src/main/java/field/VirtualFieldTestInstrumentationModule.java @@ -0,0 +1,59 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package field; + +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.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +@AutoService(InstrumentationModule.class) +public class VirtualFieldTestInstrumentationModule extends InstrumentationModule { + public VirtualFieldTestInstrumentationModule() { + super("virtual-field-test"); + } + + @Override + public List typeInstrumentations() { + return singletonList(new TestInstrumentation()); + } + + @Override + public boolean isHelperClass(String className) { + return className.startsWith("field.VirtualFieldTestHelper"); + } + + private static class TestInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("field.VirtualFieldTest"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + named("virtualFieldTestMethod"), + VirtualFieldTestInstrumentationModule.class.getName() + "$TestAdvice"); + } + } + + @SuppressWarnings("unused") + public static class TestAdvice { + @Advice.OnMethodExit + public static void onExit(@Advice.Return(readOnly = false) boolean result) { + VirtualFieldTestHelper.test(); + result = true; + } + } +} diff --git a/testing-common/integration-tests/src/main/java/indy/ProxyMe.java b/testing-common/integration-tests/src/main/java/indy/ProxyMe.java new file mode 100644 index 000000000000..8ab7387ad30e --- /dev/null +++ b/testing-common/integration-tests/src/main/java/indy/ProxyMe.java @@ -0,0 +1,21 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package indy; + +import java.util.concurrent.Callable; +import library.MyProxySuperclass; + +public class ProxyMe extends MyProxySuperclass implements Callable { + + @Override + public String call() { + return "Hi from ProxyMe"; + } + + public static String staticHello() { + return "Hi from static"; + } +} diff --git a/testing-common/integration-tests/src/main/java/io/opentelemetry/javaagent/IndyInstrumentationTestModule.java b/testing-common/integration-tests/src/main/java/io/opentelemetry/javaagent/IndyInstrumentationTestModule.java new file mode 100644 index 000000000000..c6424d8fea65 --- /dev/null +++ b/testing-common/integration-tests/src/main/java/io/opentelemetry/javaagent/IndyInstrumentationTestModule.java @@ -0,0 +1,210 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent; + +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 io.opentelemetry.javaagent.extension.instrumentation.internal.ExperimentalInstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.internal.injection.ClassInjector; +import io.opentelemetry.javaagent.extension.instrumentation.internal.injection.InjectionMode; +import java.util.Collections; +import java.util.List; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.asm.Advice.AssignReturned.ToArguments.ToArgument; +import net.bytebuddy.asm.Advice.AssignReturned.ToFields.ToField; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +@AutoService(InstrumentationModule.class) +public class IndyInstrumentationTestModule extends InstrumentationModule + implements ExperimentalInstrumentationModule { + + public IndyInstrumentationTestModule() { + super("indy-test"); + } + + @Override + public boolean isIndyModule() { + return true; + } + + @Override + public List typeInstrumentations() { + return Collections.singletonList(new Instrumentation()); + } + + @Override + public boolean isHelperClass(String className) { + return className.equals(LocalHelper.class.getName()); + } + + @Override + public List getAdditionalHelperClassNames() { + // TODO: should not be needed as soon as we automatically add proxied classes to the muzzle root + // set + return Collections.singletonList("indy.ProxyMe"); + } + + @Override + public void injectClasses(ClassInjector injector) { + injector.proxyBuilder("indy.ProxyMe", "foo.bar.Proxy").inject(InjectionMode.CLASS_AND_RESOURCE); + } + + public static class Instrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("indy.IndyInstrumentationTest"); + } + + @Override + public void transform(TypeTransformer transformer) { + String prefix = getClass().getName(); + transformer.applyAdviceToMethod( + named("assignToFieldViaReturn"), prefix + "$AssignFieldViaReturnAdvice"); + transformer.applyAdviceToMethod( + named("assignToFieldViaArray"), prefix + "$AssignFieldViaArrayAdvice"); + transformer.applyAdviceToMethod( + named("assignToArgumentViaReturn"), prefix + "$AssignArgumentViaReturnAdvice"); + transformer.applyAdviceToMethod( + named("assignToArgumentViaArray"), prefix + "$AssignArgumentViaArrayAdvice"); + transformer.applyAdviceToMethod( + named("assignToReturnViaReturn"), prefix + "$AssignReturnViaReturnAdvice"); + transformer.applyAdviceToMethod( + named("assignToReturnViaArray"), prefix + "$AssignReturnViaArrayAdvice"); + transformer.applyAdviceToMethod(named("getHelperClass"), prefix + "$GetHelperClassAdvice"); + transformer.applyAdviceToMethod(named("exceptionPlease"), prefix + "$ThrowExceptionAdvice"); + transformer.applyAdviceToMethod( + named("noExceptionPlease"), prefix + "$SuppressExceptionAdvice"); + transformer.applyAdviceToMethod( + named("instrumentWithErasedTypes"), prefix + "$SignatureErasureAdvice"); + } + + @SuppressWarnings({"unused"}) + public static class AssignFieldViaReturnAdvice { + + @Advice.OnMethodEnter(inline = false) + @Advice.AssignReturned.ToFields(@ToField(value = "privateField")) + public static String onEnter(@Advice.Argument(0) String toAssign) { + return toAssign; + } + } + + @SuppressWarnings({"unused"}) + public static class AssignFieldViaArrayAdvice { + + @Advice.OnMethodEnter(inline = false) + @Advice.AssignReturned.ToFields(@ToField(value = "privateField", index = 1)) + public static Object[] onEnter(@Advice.Argument(0) String toAssign) { + return new Object[] {"ignoreme", toAssign}; + } + } + + @SuppressWarnings({"unused"}) + public static class AssignArgumentViaReturnAdvice { + + @Advice.OnMethodEnter(inline = false) + @Advice.AssignReturned.ToArguments(@ToArgument(value = 0)) + public static String onEnter(@Advice.Argument(1) String toAssign) { + return toAssign; + } + } + + @SuppressWarnings({"unused"}) + public static class AssignArgumentViaArrayAdvice { + + @Advice.OnMethodEnter(inline = false) + @Advice.AssignReturned.ToArguments(@ToArgument(value = 0, index = 1)) + public static Object[] onEnter(@Advice.Argument(1) String toAssign) { + return new Object[] {"ignoreme", toAssign}; + } + } + + @SuppressWarnings({"unused"}) + public static class AssignReturnViaReturnAdvice { + + @Advice.OnMethodExit(inline = false) + @Advice.AssignReturned.ToReturned + public static String onExit(@Advice.Argument(0) String toAssign) { + return toAssign; + } + } + + @SuppressWarnings({"unused"}) + public static class AssignReturnViaArrayAdvice { + + @Advice.OnMethodExit(inline = false) + @Advice.AssignReturned.ToReturned(index = 1) + public static Object[] onExit(@Advice.Argument(0) String toAssign) { + return new Object[] {"ignoreme", toAssign}; + } + } + + @SuppressWarnings({"unused"}) + public static class GetHelperClassAdvice { + + @Advice.OnMethodExit(inline = false) + @Advice.AssignReturned.ToReturned + public static Class onExit(@Advice.Argument(0) boolean localHelper) { + if (localHelper) { + return LocalHelper.class; + } else { + return GlobalHelper.class; + } + } + } + + @SuppressWarnings({"unused", "ThrowSpecificExceptions"}) + public static class ThrowExceptionAdvice { + @Advice.OnMethodExit(inline = false) + public static void onMethodExit() { + throw new RuntimeException("This exception should not be suppressed"); + } + } + + @SuppressWarnings({"unused", "ThrowSpecificExceptions"}) + public static class SuppressExceptionAdvice { + @Advice.OnMethodEnter(suppress = Throwable.class, inline = false) + public static void onMethodEnter() { + throw new RuntimeException("This exception should be suppressed"); + } + + @Advice.AssignReturned.ToReturned + @Advice.OnMethodExit( + suppress = Throwable.class, + onThrowable = Throwable.class, + inline = false) + public static void onMethodExit(@Advice.Thrown Throwable throwable) { + throw new RuntimeException("This exception should be suppressed"); + } + } + + @SuppressWarnings({"unused", "ThrowSpecificExceptions"}) + public static class SignatureErasureAdvice { + @Advice.OnMethodEnter(suppress = Throwable.class, inline = false) + public static LocalHelper onMethodEnter() { + return new LocalHelper(); + } + + @Advice.AssignReturned.ToReturned + @Advice.OnMethodExit( + suppress = Throwable.class, + onThrowable = Throwable.class, + inline = false) + public static LocalHelper onMethodExit(@Advice.Enter LocalHelper enterVal) { + return enterVal; + } + } + } + + public static class GlobalHelper {} + + public static class LocalHelper {} +} diff --git a/testing-common/integration-tests/src/test/groovy/AgentInstrumentationSpecificationTest.groovy b/testing-common/integration-tests/src/test/groovy/AgentInstrumentationSpecificationTest.groovy index 5692e7daa4fb..530221e4553f 100644 --- a/testing-common/integration-tests/src/test/groovy/AgentInstrumentationSpecificationTest.groovy +++ b/testing-common/integration-tests/src/test/groovy/AgentInstrumentationSpecificationTest.groovy @@ -64,7 +64,7 @@ class AgentInstrumentationSpecificationTest extends AgentInstrumentationSpecific def "excluded classes are not instrumented"() { when: runWithSpan("parent") { - subject.run() + subject.getConstructor().newInstance().run() } then: @@ -83,13 +83,13 @@ class AgentInstrumentationSpecificationTest extends AgentInstrumentationSpecific } 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 + subject | spanName + config.SomeClass | "SomeClass.run" + config.SomeClass.NestedClass | "NestedClass.run" + config.exclude.SomeClass | null + config.exclude.SomeClass.NestedClass | null + config.exclude.packagename.SomeClass | null + config.exclude.packagename.SomeClass.NestedClass | null } def "test unblocked by completed span"() { diff --git a/testing-common/integration-tests/src/test/groovy/context/FieldBackedImplementationTest.groovy b/testing-common/integration-tests/src/test/groovy/context/FieldBackedImplementationTest.groovy index d45bf8323877..b74ed226b4ea 100644 --- a/testing-common/integration-tests/src/test/groovy/context/FieldBackedImplementationTest.groovy +++ b/testing-common/integration-tests/src/test/groovy/context/FieldBackedImplementationTest.groovy @@ -22,6 +22,7 @@ import java.lang.ref.WeakReference import java.lang.reflect.Field import java.lang.reflect.Method import java.lang.reflect.Modifier +import java.time.Duration import java.util.concurrent.atomic.AtomicReference // this test is run using @@ -182,7 +183,7 @@ class FieldBackedImplementationTest extends AgentInstrumentationSpecification { int count = keyValue.get().incrementContextCount() WeakReference instanceRef = new WeakReference(keyValue.get()) keyValue.set(null) - GcUtils.awaitGc(instanceRef) + GcUtils.awaitGc(instanceRef, Duration.ofSeconds(10)) then: instanceRef.get() == null diff --git a/testing-common/integration-tests/src/test/java/field/VirtualFieldTest.java b/testing-common/integration-tests/src/test/java/field/VirtualFieldTest.java new file mode 100644 index 000000000000..8126f8b6a485 --- /dev/null +++ b/testing-common/integration-tests/src/test/java/field/VirtualFieldTest.java @@ -0,0 +1,22 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package field; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +class VirtualFieldTest { + + @Test + void testVirtualFields() { + Assertions.assertTrue(virtualFieldTestMethod()); + } + + // instrumented by VirtualFieldTestInstrumentationModule + private static boolean virtualFieldTestMethod() { + return false; + } +} diff --git a/testing-common/integration-tests/src/test/java/indy/IndyInstrumentationTest.java b/testing-common/integration-tests/src/test/java/indy/IndyInstrumentationTest.java new file mode 100644 index 000000000000..ad39c24e57ea --- /dev/null +++ b/testing-common/integration-tests/src/test/java/indy/IndyInstrumentationTest.java @@ -0,0 +1,170 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package indy; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.javaagent.testing.common.TestAgentListenerAccess; +import java.io.InputStream; +import java.lang.reflect.Field; +import java.util.concurrent.Callable; +import library.MyProxySuperclass; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.testcontainers.shaded.org.apache.commons.io.IOUtils; + +@SuppressWarnings({"unused", "MethodCanBeStatic"}) +public class IndyInstrumentationTest { + + @RegisterExtension + static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + private String privateField; + + // The following methods are instrumented by the IndyInstrumentationTestModule + + private void assignToFieldViaReturn(String toAssign) {} + + private void assignToFieldViaArray(String toAssign) {} + + private String assignToArgumentViaReturn(String a, String toAssign) { + return "Arg:" + a; + } + + private String assignToArgumentViaArray(String a, String toAssign) { + return "Arg:" + a; + } + + private String assignToReturnViaReturn(String toAssign) { + return "replace_me"; + } + + private String assignToReturnViaArray(String toAssign) { + return "replace_me"; + } + + private String noExceptionPlease(String s) { + return s + "_no_exception"; + } + + private void exceptionPlease() {} + + private Class getHelperClass(boolean local) { + return null; + } + + private Object instrumentWithErasedTypes() { + return "replace_me"; + } + + @AfterEach + public void reset() { + privateField = null; + } + + @Test + void testAssignToFieldViaReturn() { + assignToFieldViaReturn("field_val"); + assertThat(privateField).isEqualTo("field_val"); + } + + @Test + void testAssignToFieldViaArray() { + assignToFieldViaArray("array_field_val"); + assertThat(privateField).isEqualTo("array_field_val"); + } + + @Test + void testAssignToArgumentViaReturn() { + String value = assignToArgumentViaReturn("", "arg_val"); + assertThat(value).isEqualTo("Arg:arg_val"); + } + + @Test + void testAssignToArgumentViaArray() { + String value = assignToArgumentViaArray("", "arg_array_val"); + assertThat(value).isEqualTo("Arg:arg_array_val"); + } + + @Test + void testAssignToReturnViaReturn() { + String value = assignToReturnViaReturn("ret_val"); + assertThat(value).isEqualTo("ret_val"); + } + + @Test + void testAssignToReturnViaArray() { + String value = assignToReturnViaReturn("ret_array_val"); + assertThat(value).isEqualTo("ret_array_val"); + } + + @Test + void testSuppressException() { + assertThat(noExceptionPlease("foo")).isEqualTo("foo_no_exception"); + assertThat(TestAgentListenerAccess.getAndResetAdviceFailureCount()).isEqualTo(2); + } + + @Test + void testThrowExceptionIntoUserCode() { + assertThatThrownBy(this::exceptionPlease).isInstanceOf(RuntimeException.class); + } + + @Test + void testAdviceSignatureReferenceInternalHelper() { + Object result = instrumentWithErasedTypes(); + assertThat(result.getClass().getName()).contains("LocalHelper"); + } + + @Test + void testHelperClassLoading() { + Class localHelper = getHelperClass(true); + assertThat(localHelper.getName()).endsWith("LocalHelper"); + assertThat(localHelper.getClassLoader().getClass().getName()) + .endsWith("InstrumentationModuleClassLoader"); + + Class globalHelper = getHelperClass(false); + assertThat(globalHelper.getName()).endsWith("GlobalHelper"); + assertThat(globalHelper.getClassLoader().getClass().getName()).endsWith("AgentClassLoader"); + } + + @Test + @SuppressWarnings("unchecked") + void testProxyInjection() throws Exception { + Class proxyClass = Class.forName("foo.bar.Proxy"); + + // create an instance and invoke static & non-static methods + // this verifies that our invokedynamic bootstrapping works for constructors, static and + // non-static methods + + Object proxyInstance = proxyClass.getConstructor().newInstance(); + assertThat(proxyInstance).isInstanceOf(Callable.class); + assertThat(proxyInstance).isInstanceOf(MyProxySuperclass.class); + + String invocResult = ((Callable) proxyInstance).call(); + assertThat(invocResult).isEqualTo("Hi from ProxyMe"); + + String staticResult = (String) proxyClass.getMethod("staticHello").invoke(null); + assertThat(staticResult).isEqualTo("Hi from static"); + + Field delegateField = proxyClass.getDeclaredField("delegate"); + delegateField.setAccessible(true); + Object delegate = delegateField.get(proxyInstance); + + ClassLoader delegateCl = delegate.getClass().getClassLoader(); + assertThat(delegate.getClass().getName()).isEqualTo("indy.ProxyMe"); + assertThat(delegateCl.getClass().getName()).endsWith("InstrumentationModuleClassLoader"); + + // Ensure that the bytecode of the proxy is injected as a resource + InputStream res = + IndyInstrumentationTest.class.getClassLoader().getResourceAsStream("foo/bar/Proxy.class"); + byte[] bytecode = IOUtils.toByteArray(res); + assertThat(bytecode).startsWith(0xCA, 0xFE, 0xBA, 0xBE); + } +} diff --git a/testing-common/library-for-integration-tests/src/main/java/library/MyProxySuperclass.java b/testing-common/library-for-integration-tests/src/main/java/library/MyProxySuperclass.java new file mode 100644 index 000000000000..6c17e3d38590 --- /dev/null +++ b/testing-common/library-for-integration-tests/src/main/java/library/MyProxySuperclass.java @@ -0,0 +1,12 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package library; + +/** + * Custom superclass only living in the instrumented {@link ClassLoader} to be used for testing the + * proxying mechanism. + */ +public class MyProxySuperclass {} diff --git a/instrumentation/redisson/redisson-3.0/javaagent/src/test/groovy/RedissonClientTest.groovy b/testing-common/library-for-integration-tests/src/main/java/library/VirtualFieldTestClass.java similarity index 57% rename from instrumentation/redisson/redisson-3.0/javaagent/src/test/groovy/RedissonClientTest.groovy rename to testing-common/library-for-integration-tests/src/main/java/library/VirtualFieldTestClass.java index ac39d6519a03..1f37c5cd2316 100644 --- a/instrumentation/redisson/redisson-3.0/javaagent/src/test/groovy/RedissonClientTest.groovy +++ b/testing-common/library-for-integration-tests/src/main/java/library/VirtualFieldTestClass.java @@ -3,5 +3,6 @@ * SPDX-License-Identifier: Apache-2.0 */ -class RedissonClientTest extends AbstractRedissonClientTest { -} +package library; + +public class VirtualFieldTestClass {} diff --git a/testing-common/src/main/groovy/io/opentelemetry/instrumentation/test/asserts/SpanAssert.groovy b/testing-common/src/main/groovy/io/opentelemetry/instrumentation/test/asserts/SpanAssert.groovy index 9a841295caa3..0b1e3468e8a6 100644 --- a/testing-common/src/main/groovy/io/opentelemetry/instrumentation/test/asserts/SpanAssert.groovy +++ b/testing-common/src/main/groovy/io/opentelemetry/instrumentation/test/asserts/SpanAssert.groovy @@ -12,7 +12,7 @@ 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 io.opentelemetry.semconv.ExceptionAttributes import java.util.regex.Pattern @@ -147,11 +147,11 @@ class SpanAssert { def errorEventWithAnyMessage(Class expectedClass) { event(0) { - eventName(SemanticAttributes.EXCEPTION_EVENT_NAME) + eventName("exception") attributes { - "$SemanticAttributes.EXCEPTION_TYPE" expectedClass.canonicalName - "$SemanticAttributes.EXCEPTION_STACKTRACE" String - "$SemanticAttributes.EXCEPTION_MESSAGE" { it != null } + "$ExceptionAttributes.EXCEPTION_TYPE" expectedClass.canonicalName + "$ExceptionAttributes.EXCEPTION_STACKTRACE" String + "$ExceptionAttributes.EXCEPTION_MESSAGE" { it != null } } } } @@ -162,12 +162,12 @@ class SpanAssert { def errorEvent(Class errorClass, expectedMessage, int index) { event(index) { - eventName(SemanticAttributes.EXCEPTION_EVENT_NAME) + eventName("exception") attributes { - "$SemanticAttributes.EXCEPTION_TYPE" errorClass.canonicalName - "$SemanticAttributes.EXCEPTION_STACKTRACE" String + "$ExceptionAttributes.EXCEPTION_TYPE" errorClass.canonicalName + "$ExceptionAttributes.EXCEPTION_STACKTRACE" String if (expectedMessage != null) { - "$SemanticAttributes.EXCEPTION_MESSAGE" expectedMessage + "$ExceptionAttributes.EXCEPTION_MESSAGE" expectedMessage } } } diff --git a/testing-common/src/main/groovy/io/opentelemetry/instrumentation/test/base/HttpClientTest.groovy b/testing-common/src/main/groovy/io/opentelemetry/instrumentation/test/base/HttpClientTest.groovy index 296414a5faa2..6a076cfcab17 100644 --- a/testing-common/src/main/groovy/io/opentelemetry/instrumentation/test/base/HttpClientTest.groovy +++ b/testing-common/src/main/groovy/io/opentelemetry/instrumentation/test/base/HttpClientTest.groovy @@ -7,20 +7,20 @@ package io.opentelemetry.instrumentation.test.base import io.opentelemetry.api.common.AttributeKey import io.opentelemetry.api.trace.SpanId +import io.opentelemetry.instrumentation.api.internal.HttpConstants import io.opentelemetry.instrumentation.test.InstrumentationSpecification import io.opentelemetry.instrumentation.test.asserts.TraceAssert import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpClientTest import io.opentelemetry.instrumentation.testing.junit.http.HttpClientResult -import io.opentelemetry.instrumentation.testing.junit.http.HttpClientTestServer import io.opentelemetry.instrumentation.testing.junit.http.HttpClientTestOptions +import io.opentelemetry.instrumentation.testing.junit.http.HttpClientTestServer import io.opentelemetry.instrumentation.testing.junit.http.SingleConnection -import io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions import io.opentelemetry.sdk.trace.data.SpanData import spock.lang.Requires import spock.lang.Shared import spock.lang.Unroll -import static org.junit.jupiter.api.Assumptions.assumeFalse +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat import static org.junit.jupiter.api.Assumptions.assumeTrue @Unroll @@ -129,7 +129,6 @@ abstract class HttpClientTest extends InstrumentationSpecification { protected void configure(HttpClientTestOptions.Builder optionsBuilder) { optionsBuilder.setHttpAttributes(HttpClientTest.this.&httpAttributes) optionsBuilder.setResponseCodeOnRedirectError(responseCodeOnRedirectError()) - optionsBuilder.setUserAgent(HttpClientTest.this.userAgent()) optionsBuilder.setClientSpanErrorMapper(HttpClientTest.this.&clientSpanError) optionsBuilder.setSingleConnectionFactory(HttpClientTest.this.&createSingleConnection) optionsBuilder.setExpectedClientSpanNameMapper(HttpClientTest.this.&expectedClientSpanName) @@ -144,8 +143,8 @@ abstract class HttpClientTest extends InstrumentationSpecification { optionsBuilder.setTestHttps(HttpClientTest.this.testHttps()) optionsBuilder.setTestCallback(HttpClientTest.this.testCallback()) optionsBuilder.setTestCallbackWithParent(HttpClientTest.this.testCallbackWithParent()) - optionsBuilder.setTestCallbackWithImplicitParent(HttpClientTest.this.testCallbackWithImplicitParent()) optionsBuilder.setTestErrorWithCallback(HttpClientTest.this.testErrorWithCallback()) + optionsBuilder.setTestNonStandardHttpMethod(HttpClientTest.this.testNonStandardHttpMethod()) } } @@ -183,6 +182,12 @@ abstract class HttpClientTest extends InstrumentationSpecification { path << ["/success", "/success?with=params"] } + def "request with non-standard http method"() { + assumeTrue(testNonStandardHttpMethod()) + expect: + junitTest.requestWithNonStandardHttpMethod() + } + def "basic #method request with parent"() { expect: junitTest.successfulRequestWithParent(method) @@ -219,7 +224,6 @@ abstract class HttpClientTest extends InstrumentationSpecification { def "trace request with callback and no parent"() { assumeTrue(testCallback()) - assumeFalse(testCallbackWithImplicitParent()) expect: try { junitTest.requestWithCallbackAndNoParent() @@ -227,13 +231,6 @@ abstract class HttpClientTest extends InstrumentationSpecification { } } - def "trace request with callback and implicit parent"() { - assumeTrue(testCallback()) - assumeTrue(testCallbackWithImplicitParent()) - expect: - junitTest.requestWithCallbackAndImplicitParent() - } - def "basic request with 1 redirect"() { assumeTrue(testRedirects()) expect: @@ -322,6 +319,11 @@ abstract class HttpClientTest extends InstrumentationSpecification { junitTest.httpsRequest() } + def "http client metrics"() { + expect: + junitTest.httpClientMetrics() + } + /** * This test fires a large number of concurrent requests. * Each request first hits a HTTP server and then makes another client request. @@ -352,6 +354,10 @@ abstract class HttpClientTest extends InstrumentationSpecification { junitTest.highConcurrencyOnSingleConnection() } + def "http client span ends after headers are received"() { + junitTest.spanEndsAfterHeadersReceived() + } + // 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) @@ -359,17 +365,13 @@ abstract class HttpClientTest extends InstrumentationSpecification { } protected String expectedClientSpanName(URI uri, String method) { - return method + return HttpConstants._OTHER == method ? "HTTP" : method } Integer responseCodeOnRedirectError() { return 302 } - String userAgent() { - return null - } - /** A list of additional HTTP client span attributes extracted by the instrumentation per URI. */ Set> httpAttributes(URI uri) { new HashSet<>(HttpClientTestOptions.DEFAULT_HTTP_ATTRIBUTES) @@ -432,17 +434,14 @@ abstract class HttpClientTest extends InstrumentationSpecification { true } - boolean testCallbackWithImplicitParent() { - // depending on async behavior callback can be executed within - // parent span scope or outside of the scope, e.g. in reactor-netty or spring - // callback is correlated. - false - } - boolean testErrorWithCallback() { return true } + boolean testNonStandardHttpMethod() { + true + } + Throwable clientSpanError(URI uri, Throwable exception) { return exception } @@ -450,7 +449,7 @@ abstract class HttpClientTest extends InstrumentationSpecification { final void clientSpan(TraceAssert trace, int index, Object parentSpan, String method = "GET", URI uri = resolveAddress("/success"), Integer responseCode = 200) { trace.assertedIndexes.add(index) def spanData = trace.span(index) - def assertion = junitTest.assertClientSpan(OpenTelemetryAssertions.assertThat(spanData), uri, method, responseCode, null) + def assertion = junitTest.assertClientSpan(assertThat(spanData), uri, method, responseCode, null) if (parentSpan == null) { assertion.hasParentSpanId(SpanId.invalid) } else { @@ -461,7 +460,7 @@ abstract class HttpClientTest extends InstrumentationSpecification { final void serverSpan(TraceAssert trace, int index, Object parentSpan = null) { trace.assertedIndexes.add(index) def spanData = trace.span(index) - def assertion = junitTest.assertServerSpan(OpenTelemetryAssertions.assertThat(spanData)) + def assertion = junitTest.assertServerSpan(assertThat(spanData)) if (parentSpan == null) { assertion.hasParentSpanId(SpanId.invalid) } else { diff --git a/testing-common/src/main/groovy/io/opentelemetry/instrumentation/test/base/HttpServerTest.groovy b/testing-common/src/main/groovy/io/opentelemetry/instrumentation/test/base/HttpServerTest.groovy index 48910dcfb5b0..51ebea5841f1 100644 --- a/testing-common/src/main/groovy/io/opentelemetry/instrumentation/test/base/HttpServerTest.groovy +++ b/testing-common/src/main/groovy/io/opentelemetry/instrumentation/test/base/HttpServerTest.groovy @@ -9,17 +9,17 @@ import io.opentelemetry.api.common.AttributeKey import io.opentelemetry.api.trace.Span import io.opentelemetry.api.trace.SpanId import io.opentelemetry.api.trace.SpanKind +import io.opentelemetry.instrumentation.api.internal.HttpConstants import io.opentelemetry.instrumentation.test.InstrumentationSpecification import io.opentelemetry.instrumentation.test.asserts.TraceAssert import io.opentelemetry.instrumentation.testing.GlobalTraceUtil import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpServerTest import io.opentelemetry.instrumentation.testing.junit.http.HttpServerTestOptions import io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint -import io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions import io.opentelemetry.sdk.trace.data.SpanData -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import io.opentelemetry.semconv.HttpAttributes +import io.opentelemetry.semconv.NetworkAttributes 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 spock.lang.Shared import spock.lang.Unroll @@ -27,12 +27,9 @@ import spock.lang.Unroll import javax.annotation.Nullable import java.util.concurrent.Callable -import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.EXCEPTION -import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.INDEXED_CHILD -import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.NOT_FOUND -import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.PATH_PARAM -import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.QUERY_PARAM -import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.SUCCESS +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.* +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat +import static org.junit.jupiter.api.Assumptions.assumeFalse import static org.junit.jupiter.api.Assumptions.assumeTrue @Unroll @@ -49,12 +46,19 @@ abstract class HttpServerTest extends InstrumentationSpecification imple } String expectedServerSpanName(ServerEndpoint endpoint, String method, @Nullable String route) { + if (method == HttpConstants._OTHER) { + method = "HTTP" + } return route == null ? method : method + " " + route } - String expectedHttpRoute(ServerEndpoint endpoint) { + String expectedHttpRoute(ServerEndpoint endpoint, String method) { // no need to compute route if we're not expecting it - if (!httpAttributes(endpoint).contains(SemanticAttributes.HTTP_ROUTE)) { + if (!httpAttributes(endpoint).contains(HttpAttributes.HTTP_ROUTE)) { + return null + } + + if (method == HttpConstants._OTHER) { return null } @@ -72,6 +76,10 @@ abstract class HttpServerTest extends InstrumentationSpecification imple return "" } + boolean useHttp2() { + false + } + boolean hasHandlerSpan(ServerEndpoint endpoint) { false } @@ -96,6 +104,10 @@ abstract class HttpServerTest extends InstrumentationSpecification imple 1 } + int getResponseCodeOnNonStandardHttpMethod() { + SUCCESS.status + } + boolean hasErrorPageSpans(ServerEndpoint endpoint) { false } @@ -148,6 +160,10 @@ abstract class HttpServerTest extends InstrumentationSpecification imple true } + boolean testNonStandardHttpMethod() { + true + } + boolean verifyServerSpanEndTime() { return true } @@ -155,8 +171,8 @@ abstract class HttpServerTest extends InstrumentationSpecification imple /** A list of additional HTTP server span attributes extracted by the instrumentation per URI. */ Set> httpAttributes(ServerEndpoint endpoint) { [ - SemanticAttributes.HTTP_ROUTE, - SemanticAttributes.NET_SOCK_PEER_PORT + HttpAttributes.HTTP_ROUTE, + NetworkAttributes.NETWORK_PEER_PORT ] as Set } @@ -195,8 +211,8 @@ abstract class HttpServerTest extends InstrumentationSpecification imple options.expectedServerSpanNameMapper = { endpoint, method, route -> HttpServerTest.this.expectedServerSpanName(endpoint, method, route) } - options.expectedHttpRoute = { endpoint -> - HttpServerTest.this.expectedHttpRoute(endpoint) + options.expectedHttpRoute = { endpoint, method -> + HttpServerTest.this.expectedHttpRoute(endpoint, method) } options.contextPath = getContextPath() options.httpAttributes = { endpoint -> @@ -212,6 +228,7 @@ abstract class HttpServerTest extends InstrumentationSpecification imple options.sockPeerAddr = { endpoint -> HttpServerTest.this.sockPeerAddr(endpoint) } + options.responseCodeOnNonStandardHttpMethod = getResponseCodeOnNonStandardHttpMethod() options.testRedirect = testRedirect() options.testError = testError() @@ -222,6 +239,10 @@ abstract class HttpServerTest extends InstrumentationSpecification imple options.testCaptureHttpHeaders = testCapturedHttpHeaders() options.testCaptureRequestParameters = testCapturedRequestParameters() options.testHttpPipelining = testHttpPipelining() + if (!testNonStandardHttpMethod()) { + options.disableTestNonStandardHttpMethod() + } + options.useHttp2 = useHttp2() } // Override trace assertion method. We can call java assertions from groovy but not the other @@ -234,9 +255,8 @@ abstract class HttpServerTest extends InstrumentationSpecification imple String parentId, String spanId, String method, - ServerEndpoint endpoint, - AggregatedHttpResponse response) { - HttpServerTest.this.assertTheTraces(size, traceId, parentId, spanId, method, endpoint, response) + ServerEndpoint endpoint) { + HttpServerTest.this.assertTheTraces(size, traceId, parentId, spanId, method, endpoint) } @Override @@ -314,6 +334,11 @@ abstract class HttpServerTest extends InstrumentationSpecification imple junitTest.captureRequestParameters() } + def "http server metrics"() { + expect: + junitTest.httpServerMetrics() + } + def "high concurrency test"() { expect: junitTest.highConcurrency() @@ -321,10 +346,20 @@ abstract class HttpServerTest extends InstrumentationSpecification imple def "http pipelining test"() { assumeTrue(testHttpPipelining()) + // test uses http 1.1 + assumeFalse(useHttp2()) expect: junitTest.httpPipelining() } + def "non standard http method"() { + assumeTrue(testNonStandardHttpMethod()) + // test uses http 1.1 + assumeFalse(useHttp2()) + expect: + junitTest.requestWithNonStandardHttpMethod() + } + void assertHighConcurrency(int count) { def endpoint = INDEXED_CHILD assertTraces(count) { @@ -360,7 +395,7 @@ abstract class HttpServerTest extends InstrumentationSpecification imple //FIXME: add tests for POST with large/chunked data - void assertTheTraces(int size, String traceID = null, String parentID = null, String spanID = null, String method = "GET", ServerEndpoint endpoint = SUCCESS, AggregatedHttpResponse response = null) { + void assertTheTraces(int size, String traceID = null, String parentID = null, String spanID = null, String method = "GET", ServerEndpoint endpoint = SUCCESS) { def spanCount = 1 // server span if (hasResponseSpan(endpoint)) { spanCount++ @@ -386,7 +421,7 @@ abstract class HttpServerTest extends InstrumentationSpecification imple assert it.span(0).endEpochNanos - it.span(index).endEpochNanos >= 0 } } - serverSpan(it, spanIndex++, traceID, parentID, method, response?.content()?.length(), endpoint, spanID) + this.serverSpan(it, spanIndex++, traceID, parentID, method, endpoint, spanID) if (hasHandlerSpan(endpoint)) { handlerSpan(it, spanIndex++, span(0), method, endpoint) } @@ -415,7 +450,7 @@ abstract class HttpServerTest extends InstrumentationSpecification imple void controllerSpan(TraceAssert trace, int index, Object parent, Throwable expectedException = null) { trace.assertedIndexes.add(index) def spanData = trace.span(index) - def assertion = junitTest.assertControllerSpan(OpenTelemetryAssertions.assertThat(spanData), expectedException) + def assertion = junitTest.assertControllerSpan(assertThat(spanData), expectedException) if (parent == null) { assertion.hasParentSpanId(SpanId.invalid) } else { @@ -459,10 +494,10 @@ abstract class HttpServerTest extends InstrumentationSpecification imple } } - void serverSpan(TraceAssert trace, int index, String traceID = null, String parentID = null, String method = "GET", Long responseContentLength = null, ServerEndpoint endpoint = SUCCESS, String spanID = null) { + void serverSpan(TraceAssert trace, int index, String traceID = null, String parentID = null, String method = "GET", ServerEndpoint endpoint = SUCCESS, String spanID = null) { trace.assertedIndexes.add(index) def spanData = trace.span(index) - def assertion = junitTest.assertServerSpan(OpenTelemetryAssertions.assertThat(spanData), method, endpoint) + def assertion = junitTest.assertServerSpan(assertThat(spanData), method, endpoint, endpoint.status) if (parentID == null) { assertion.hasParentSpanId(SpanId.invalid) } else { @@ -479,7 +514,7 @@ abstract class HttpServerTest extends InstrumentationSpecification imple void indexedServerSpan(TraceAssert trace, int index, Object parent, int requestId) { trace.assertedIndexes.add(index) def spanData = trace.span(index) - def assertion = junitTest.assertIndexedServerSpan(OpenTelemetryAssertions.assertThat(spanData), requestId) + def assertion = junitTest.assertIndexedServerSpan(assertThat(spanData), requestId) if (parent == null) { assertion.hasParentSpanId(SpanId.invalid) } else { @@ -490,7 +525,7 @@ abstract class HttpServerTest extends InstrumentationSpecification imple void indexedControllerSpan(TraceAssert trace, int index, Object parent, int requestId) { trace.assertedIndexes.add(index) def spanData = trace.span(index) - def assertion = junitTest.assertIndexedControllerSpan(OpenTelemetryAssertions.assertThat(spanData), requestId) + def assertion = junitTest.assertIndexedControllerSpan(assertThat(spanData), requestId) if (parent == null) { assertion.hasParentSpanId(SpanId.invalid) } else { diff --git a/testing-common/src/main/java/io/opentelemetry/instrumentation/test/utils/GcUtils.java b/testing-common/src/main/java/io/opentelemetry/instrumentation/test/utils/GcUtils.java index d8f6866c17fc..cd79578fbe86 100644 --- a/testing-common/src/main/java/io/opentelemetry/instrumentation/test/utils/GcUtils.java +++ b/testing-common/src/main/java/io/opentelemetry/instrumentation/test/utils/GcUtils.java @@ -6,23 +6,42 @@ package io.opentelemetry.instrumentation.test.utils; import java.lang.ref.WeakReference; +import java.time.Duration; +import java.time.temporal.ChronoUnit; +import java.util.concurrent.TimeoutException; public final class GcUtils { - public static void awaitGc() throws InterruptedException { + private static final StringBuilder garbage = new StringBuilder(); + + public static void awaitGc(Duration timeout) throws InterruptedException, TimeoutException { Object obj = new Object(); WeakReference ref = new WeakReference<>(obj); obj = null; - awaitGc(ref); + awaitGc(ref, timeout); } - public static void awaitGc(WeakReference ref) throws InterruptedException { - while (ref.get() != null) { + public static void awaitGc(WeakReference ref, Duration timeout) + throws InterruptedException, TimeoutException { + long start = System.currentTimeMillis(); + while (ref.get() != null + && !timeout.minus(System.currentTimeMillis() - start, ChronoUnit.MILLIS).isNegative()) { if (Thread.interrupted()) { throw new InterruptedException(); } + // generate garbage to give gc some work + for (int i = 0; i < 26; i++) { + if (garbage.length() == 0) { + garbage.append("ab"); + } else { + garbage.append(garbage); + } + } + garbage.setLength(0); System.gc(); - System.runFinalization(); + } + if (ref.get() != null) { + throw new TimeoutException("reference was not cleared in time"); } } diff --git a/testing-common/src/main/java/io/opentelemetry/instrumentation/test/utils/PortUtils.java b/testing-common/src/main/java/io/opentelemetry/instrumentation/test/utils/PortUtils.java index acddb36b765f..8ba88a186434 100644 --- a/testing-common/src/main/java/io/opentelemetry/instrumentation/test/utils/PortUtils.java +++ b/testing-common/src/main/java/io/opentelemetry/instrumentation/test/utils/PortUtils.java @@ -25,6 +25,7 @@ public static int findOpenPort() { return portAllocator.getPort(); } + @SuppressWarnings("AddressSelection") private static boolean isPortOpen(int port) { try (Socket socket = new Socket((String) null, port)) { return true; diff --git a/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/AgentTestRunner.java b/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/AgentTestRunner.java index 553e9bab9138..f8648ccc6416 100644 --- a/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/AgentTestRunner.java +++ b/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/AgentTestRunner.java @@ -55,6 +55,8 @@ public void afterTestClass() { assert TestAgentListenerAccess.getInstrumentationErrorCount() == 0 : TestAgentListenerAccess.getInstrumentationErrorCount() + " Instrumentation errors during test"; + int adviceFailureCount = TestAgentListenerAccess.getAndResetAdviceFailureCount(); + assert adviceFailureCount == 0 : adviceFailureCount + " Advice failures during test"; int muzzleFailureCount = TestAgentListenerAccess.getAndResetMuzzleFailureCount(); assert muzzleFailureCount == 0 : muzzleFailureCount + " Muzzle failures during test"; // additional library ignores are ignored during tests, because they can make it really diff --git a/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/InstrumentationTestRunner.java b/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/InstrumentationTestRunner.java index 65e7d016b1b0..8f68cce5c084 100644 --- a/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/InstrumentationTestRunner.java +++ b/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/InstrumentationTestRunner.java @@ -5,6 +5,7 @@ package io.opentelemetry.instrumentation.testing; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; import static org.awaitility.Awaitility.await; import io.opentelemetry.api.OpenTelemetry; @@ -13,17 +14,21 @@ import io.opentelemetry.instrumentation.testing.util.ThrowingSupplier; import io.opentelemetry.sdk.logs.data.LogRecordData; import io.opentelemetry.sdk.metrics.data.MetricData; +import io.opentelemetry.sdk.testing.assertj.MetricAssert; import io.opentelemetry.sdk.testing.assertj.TraceAssert; import io.opentelemetry.sdk.testing.assertj.TracesAssert; import io.opentelemetry.sdk.trace.data.SpanData; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.Comparator; import java.util.List; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.function.Consumer; +import java.util.stream.Collectors; import javax.annotation.Nullable; +import org.assertj.core.api.ListAssert; import org.awaitility.core.ConditionTimeoutException; /** @@ -78,6 +83,12 @@ public final void waitAndAssertSortedTraces( waitAndAssertTraces(traceComparator, Arrays.asList(assertions), true); } + public final void waitAndAssertSortedTraces( + Comparator> traceComparator, + Iterable> assertions) { + waitAndAssertTraces(traceComparator, assertions, true); + } + @SafeVarargs @SuppressWarnings("varargs") public final void waitAndAssertTracesWithoutScopeVersionVerification( @@ -110,12 +121,21 @@ private > void waitAndAssertTraces( try { await() .untilAsserted(() -> doAssertTraces(traceComparator, assertionsList, verifyScopeVersion)); - } catch (ConditionTimeoutException e) { - // Don't throw this failure since the stack is the awaitility thread, causing confusion. - // Instead, just assert one more time on the test thread, which will fail with a better stack - // trace. - // TODO(anuraaga): There is probably a better way to do this. - doAssertTraces(traceComparator, assertionsList, verifyScopeVersion); + } catch (Throwable t) { + // awaitility is doing a jmx call that is not implemented in GraalVM: + // call: + // https://github.com/awaitility/awaitility/blob/fbe16add874b4260dd240108304d5c0be84eabc8/awaitility/src/main/java/org/awaitility/core/ConditionAwaiter.java#L157 + // see https://github.com/oracle/graal/issues/6101 (spring boot graal native image) + if (t.getClass().getName().equals("com.oracle.svm.core.jdk.UnsupportedFeatureError") + || t instanceof ConditionTimeoutException) { + // Don't throw this failure since the stack is the awaitility thread, causing confusion. + // Instead, just assert one more time on the test thread, which will fail with a better + // stack trace. + // TODO(anuraaga): There is probably a better way to do this. + doAssertTraces(traceComparator, assertionsList, verifyScopeVersion); + } else { + throw t; + } } } @@ -133,6 +153,45 @@ private > void doAssertTraces( TracesAssert.assertThat(traces).hasTracesSatisfyingExactly(assertionsList); } + /** + * Waits for the assertion applied to all metrics of the given instrumentation and metric name to + * pass. + */ + public final void waitAndAssertMetrics( + String instrumentationName, String metricName, Consumer> assertion) { + await() + .untilAsserted( + () -> + assertion.accept( + assertThat(getExportedMetrics()) + .filteredOn( + data -> + data.getInstrumentationScopeInfo() + .getName() + .equals(instrumentationName) + && data.getName().equals(metricName)))); + } + + @SafeVarargs + public final void waitAndAssertMetrics( + String instrumentationName, Consumer... assertions) { + await() + .untilAsserted( + () -> { + Collection metrics = instrumentationMetrics(instrumentationName); + assertThat(metrics).isNotEmpty(); + for (Consumer assertion : assertions) { + assertThat(metrics).anySatisfy(metric -> assertion.accept(assertThat(metric))); + } + }); + } + + private List instrumentationMetrics(String instrumentationName) { + return getExportedMetrics().stream() + .filter(m -> m.getInstrumentationScopeInfo().getName().equals(instrumentationName)) + .collect(Collectors.toList()); + } + /** * Runs the provided {@code callback} inside the scope of an INTERNAL span with name {@code * spanName}. diff --git a/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/LibraryTestRunner.java b/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/LibraryTestRunner.java index e22444cb99b0..c70a9d73b4ab 100644 --- a/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/LibraryTestRunner.java +++ b/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/LibraryTestRunner.java @@ -13,12 +13,15 @@ import io.opentelemetry.exporter.logging.LoggingSpanExporter; import io.opentelemetry.sdk.OpenTelemetrySdk; import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.logs.SdkLoggerProvider; import io.opentelemetry.sdk.logs.data.LogRecordData; +import io.opentelemetry.sdk.logs.export.SimpleLogRecordProcessor; import io.opentelemetry.sdk.metrics.SdkMeterProvider; import io.opentelemetry.sdk.metrics.data.AggregationTemporality; import io.opentelemetry.sdk.metrics.data.MetricData; import io.opentelemetry.sdk.metrics.export.MetricReader; import io.opentelemetry.sdk.metrics.export.PeriodicMetricReader; +import io.opentelemetry.sdk.testing.exporter.InMemoryLogRecordExporter; import io.opentelemetry.sdk.testing.exporter.InMemoryMetricExporter; import io.opentelemetry.sdk.testing.exporter.InMemorySpanExporter; import io.opentelemetry.sdk.trace.ReadWriteSpan; @@ -28,7 +31,6 @@ import io.opentelemetry.sdk.trace.data.SpanData; import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor; import java.time.Duration; -import java.util.Collections; import java.util.List; import java.util.concurrent.TimeUnit; @@ -41,6 +43,7 @@ public final class LibraryTestRunner extends InstrumentationTestRunner { private static final OpenTelemetrySdk openTelemetry; private static final InMemorySpanExporter testSpanExporter; private static final InMemoryMetricExporter testMetricExporter; + private static final InMemoryLogRecordExporter testLogRecordExporter; private static final MetricReader metricReader; private static boolean forceFlushCalled; @@ -49,6 +52,7 @@ public final class LibraryTestRunner extends InstrumentationTestRunner { testSpanExporter = InMemorySpanExporter.create(); testMetricExporter = InMemoryMetricExporter.create(AggregationTemporality.DELTA); + testLogRecordExporter = InMemoryLogRecordExporter.create(); metricReader = PeriodicMetricReader.builder(testMetricExporter) @@ -66,6 +70,10 @@ public final class LibraryTestRunner extends InstrumentationTestRunner { .addSpanProcessor(SimpleSpanProcessor.create(testSpanExporter)) .build()) .setMeterProvider(SdkMeterProvider.builder().registerMetricReader(metricReader).build()) + .setLoggerProvider( + SdkLoggerProvider.builder() + .addLogRecordProcessor(SimpleLogRecordProcessor.create(testLogRecordExporter)) + .build()) .setPropagators(ContextPropagators.create(W3CTraceContextPropagator.getInstance())) .buildAndRegisterGlobal(); } @@ -98,6 +106,7 @@ public void clearAllExportedData() { openTelemetry.getSdkMeterProvider().forceFlush().join(10, TimeUnit.SECONDS); testSpanExporter.reset(); testMetricExporter.reset(); + testLogRecordExporter.reset(); forceFlushCalled = false; } @@ -123,8 +132,7 @@ public List getExportedMetrics() { @Override public List getExportedLogRecords() { - // no logs support yet - return Collections.emptyList(); + return testLogRecordExporter.getFinishedLogRecordItems(); } @Override diff --git a/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/TestInstrumenters.java b/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/TestInstrumenters.java index 0f5ef5d7fa41..d23fc3efbe5f 100644 --- a/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/TestInstrumenters.java +++ b/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/TestInstrumenters.java @@ -18,13 +18,12 @@ 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.http.HttpRouteHolder; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpServerAttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpServerAttributesGetter; -import io.opentelemetry.instrumentation.api.instrumenter.http.HttpSpanNameExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.net.NetServerAttributesGetter; import io.opentelemetry.instrumentation.api.internal.SpanKey; import io.opentelemetry.instrumentation.api.internal.SpanKeyProvider; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerAttributesExtractor; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerAttributesGetter; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRoute; +import io.opentelemetry.instrumentation.api.semconv.http.HttpSpanNameExtractor; import io.opentelemetry.instrumentation.testing.util.ThrowingSupplier; import java.util.List; import javax.annotation.Nullable; @@ -50,11 +49,9 @@ final class TestInstrumenters { Instrumenter.builder( openTelemetry, "test", HttpSpanNameExtractor.create(HttpServerGetter.INSTANCE)) // cover both semconv and span-kind strategies - .addAttributesExtractor( - HttpServerAttributesExtractor.create( - HttpServerGetter.INSTANCE, NetServerGetter.INSTANCE)) + .addAttributesExtractor(HttpServerAttributesExtractor.create(HttpServerGetter.INSTANCE)) .addAttributesExtractor(new SpanKeyAttributesExtractor(SpanKey.KIND_SERVER)) - .addContextCustomizer(HttpRouteHolder.create(HttpServerGetter.INSTANCE)) + .addContextCustomizer(HttpServerRoute.create(HttpServerGetter.INSTANCE)) .buildInstrumenter(SpanKindExtractor.alwaysServer()); } @@ -181,20 +178,4 @@ public String getUrlQuery(String s) { return null; } } - - private enum NetServerGetter implements NetServerAttributesGetter { - INSTANCE; - - @Nullable - @Override - public String getServerAddress(String unused) { - return null; - } - - @Nullable - @Override - public Integer getServerPort(String unused) { - return null; - } - } } diff --git a/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/junit/InstrumentationExtension.java b/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/junit/InstrumentationExtension.java index 5900b842397f..d66c9c86552d 100644 --- a/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/junit/InstrumentationExtension.java +++ b/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/junit/InstrumentationExtension.java @@ -5,6 +5,7 @@ package io.opentelemetry.instrumentation.testing.junit; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; @@ -18,10 +19,15 @@ import io.opentelemetry.sdk.OpenTelemetrySdk; import io.opentelemetry.sdk.logs.data.LogRecordData; import io.opentelemetry.sdk.metrics.data.MetricData; +import io.opentelemetry.sdk.testing.assertj.LogRecordDataAssert; +import io.opentelemetry.sdk.testing.assertj.MetricAssert; import io.opentelemetry.sdk.testing.assertj.TraceAssert; import io.opentelemetry.sdk.trace.data.SpanData; import java.time.Duration; +import java.util.ArrayList; +import java.util.Arrays; import java.util.Comparator; +import java.util.Iterator; import java.util.List; import java.util.function.Consumer; import org.assertj.core.api.ListAssert; @@ -58,6 +64,7 @@ public void afterEach(ExtensionContext context) throws Exception { @Override public void afterAll(ExtensionContext extensionContext) throws Exception { + testRunner.clearAllExportedData(); testRunner.afterTestClass(); } @@ -85,19 +92,15 @@ public List logRecords() { * Waits for the assertion applied to all metrics of the given instrumentation and metric name to * pass. */ - public void waitAndAssertMetrics( + public final void waitAndAssertMetrics( String instrumentationName, String metricName, Consumer> assertion) { - await() - .untilAsserted( - () -> - assertion.accept( - assertThat(metrics()) - .filteredOn( - data -> - data.getInstrumentationScopeInfo() - .getName() - .equals(instrumentationName) - && data.getName().equals(metricName)))); + testRunner.waitAndAssertMetrics(instrumentationName, metricName, assertion); + } + + @SafeVarargs + public final void waitAndAssertMetrics( + String instrumentationName, Consumer... assertions) { + testRunner.waitAndAssertMetrics(instrumentationName, assertions); } /** @@ -139,6 +142,12 @@ public final void waitAndAssertSortedTraces( testRunner.waitAndAssertSortedTraces(traceComparator, assertions); } + public final void waitAndAssertSortedTraces( + Comparator> traceComparator, + Iterable> assertions) { + testRunner.waitAndAssertSortedTraces(traceComparator, assertions); + } + @SafeVarargs @SuppressWarnings("varargs") public final void waitAndAssertTracesWithoutScopeVersionVerification( @@ -156,6 +165,27 @@ public final void waitAndAssertTraces(Iterable> testRunner.waitAndAssertTraces(assertions); } + private void doWaitAndAssertLogRecords(List> assertions) { + List logRecordDataList = waitForLogRecords(assertions.size()); + Iterator> assertionIterator = assertions.iterator(); + for (LogRecordData logRecordData : logRecordDataList) { + assertionIterator.next().accept(assertThat(logRecordData)); + } + } + + public final void waitAndAssertLogRecords( + Iterable> assertions) { + List> assertionsList = new ArrayList<>(); + assertions.forEach(assertionsList::add); + doWaitAndAssertLogRecords(assertionsList); + } + + @SafeVarargs + @SuppressWarnings("varargs") + public final void waitAndAssertLogRecords(Consumer... assertions) { + doWaitAndAssertLogRecords(Arrays.asList(assertions)); + } + /** * Runs the provided {@code callback} inside the scope of an INTERNAL span with name {@code * spanName}. diff --git a/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/junit/http/AbstractHttpClientTest.java b/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/junit/http/AbstractHttpClientTest.java index 27f1bca7fb97..4d9fa69793c8 100644 --- a/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/junit/http/AbstractHttpClientTest.java +++ b/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/junit/http/AbstractHttpClientTest.java @@ -5,29 +5,34 @@ package io.opentelemetry.instrumentation.testing.junit.http; +import static io.opentelemetry.instrumentation.testing.util.TelemetryDataUtil.comparingRootSpanAttribute; +import static io.opentelemetry.instrumentation.testing.util.TelemetryDataUtil.orderByRootSpanName; import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.NetTransportValues.IP_TCP; +import static java.util.Arrays.asList; import static java.util.Collections.singletonList; import static org.assertj.core.api.Assertions.catchThrowable; -import static org.junit.Assume.assumeFalse; import static org.junit.jupiter.api.Assumptions.assumeTrue; import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.trace.Span; import io.opentelemetry.api.trace.SpanKind; -import io.opentelemetry.instrumentation.api.instrumenter.net.internal.NetAttributes; +import io.opentelemetry.instrumentation.api.internal.HttpConstants; import io.opentelemetry.instrumentation.test.utils.PortUtils; import io.opentelemetry.instrumentation.testing.InstrumentationTestRunner; import io.opentelemetry.sdk.testing.assertj.SpanDataAssert; import io.opentelemetry.sdk.testing.assertj.TraceAssert; import io.opentelemetry.sdk.trace.data.SpanData; import io.opentelemetry.sdk.trace.data.StatusData; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.opentelemetry.semconv.ErrorAttributes; +import io.opentelemetry.semconv.HttpAttributes; +import io.opentelemetry.semconv.NetworkAttributes; +import io.opentelemetry.semconv.ServerAttributes; +import io.opentelemetry.semconv.UrlAttributes; +import io.opentelemetry.semconv.UserAgentAttributes; import java.net.URI; import java.time.Duration; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -38,6 +43,8 @@ import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; import java.util.stream.Collectors; import java.util.stream.IntStream; @@ -110,11 +117,33 @@ void successfulGetRequest(String path) throws Exception { assertThat(responseCode).isEqualTo(200); testing.waitAndAssertTraces( - trace -> { - trace.hasSpansSatisfyingExactly( - span -> assertClientSpan(span, uri, method, responseCode, null).hasNoParent(), - span -> assertServerSpan(span).hasParent(trace.getSpan(0))); - }); + trace -> + trace.hasSpansSatisfyingExactly( + span -> + assertClientSpan(span, uri, method, responseCode, null) + .hasNoParent() + .hasStatus(StatusData.unset()), + span -> assertServerSpan(span).hasParent(trace.getSpan(0)))); + } + + @Test + void requestWithNonStandardHttpMethod() throws Exception { + assumeTrue(options.getTestNonStandardHttpMethod()); + + URI uri = resolveAddress("/success"); + String method = "TEST"; + int responseCode = doRequest(method, uri); + + assertThat(responseCode) + .isEqualTo("2".equals(options.getHttpProtocolVersion().apply(uri)) ? 400 : 405); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + assertClientSpan(span, uri, HttpConstants._OTHER, responseCode, null) + .hasNoParent() + .hasAttribute(HttpAttributes.HTTP_REQUEST_METHOD_ORIGINAL, method))); } @ParameterizedTest @@ -162,7 +191,8 @@ void shouldSuppressNestedClientSpanIfAlreadyUnderParentClientSpan(String method) assertThat(responseCode).isEqualTo(200); - testing.waitAndAssertTraces( + testing.waitAndAssertSortedTraces( + orderByRootSpanName("parent-client-span", "test-http-server"), trace -> trace.hasSpansSatisfyingExactly( span -> span.hasName("parent-client-span").hasKind(SpanKind.CLIENT).hasNoParent()), @@ -199,7 +229,6 @@ void requestWithCallbackAndParent() throws Throwable { @Test void requestWithCallbackAndNoParent() throws Throwable { assumeTrue(options.getTestCallback()); - assumeFalse(options.getTestCallbackWithImplicitParent()); String method = "GET"; URI uri = resolveAddress("/success"); @@ -220,29 +249,6 @@ void requestWithCallbackAndNoParent() throws Throwable { span -> span.hasName("callback").hasKind(SpanKind.INTERNAL).hasNoParent())); } - @Test - void requestWithCallbackAndImplicitParent() throws Throwable { - assumeTrue(options.getTestCallbackWithImplicitParent()); - - String method = "GET"; - URI uri = resolveAddress("/success"); - - HttpClientResult result = - doRequestWithCallback(method, uri, () -> testing.runWithSpan("callback", () -> {})); - - assertThat(result.get()).isEqualTo(200); - - testing.waitAndAssertTraces( - trace -> - trace.hasSpansSatisfyingExactly( - span -> assertClientSpan(span, uri, method, 200, null).hasNoParent(), - span -> assertServerSpan(span).hasParent(trace.getSpan(0)), - span -> - span.hasName("callback") - .hasKind(SpanKind.INTERNAL) - .hasParent(trace.getSpan(0)))); - } - @Test void basicRequestWith1Redirect() throws Exception { assumeTrue(options.getTestRedirects()); @@ -255,7 +261,8 @@ void basicRequestWith1Redirect() throws Exception { assertThat(responseCode).isEqualTo(200); if (options.isLowLevelInstrumentation()) { - testing.waitAndAssertTraces( + testing.waitAndAssertSortedTraces( + comparingRootSpanAttribute(HttpAttributes.HTTP_REQUEST_RESEND_COUNT), trace -> { trace.hasSpansSatisfyingExactly( span -> @@ -294,7 +301,8 @@ void basicRequestWith2Redirects() throws Exception { assertThat(responseCode).isEqualTo(200); if (options.isLowLevelInstrumentation()) { - testing.waitAndAssertTraces( + testing.waitAndAssertSortedTraces( + comparingRootSpanAttribute(HttpAttributes.HTTP_REQUEST_RESEND_COUNT), trace -> { trace.hasSpansSatisfyingExactly( span -> @@ -352,7 +360,8 @@ void circularRedirects() { Throwable clientError = options.getClientSpanErrorMapper().apply(uri, ex); if (options.isLowLevelInstrumentation()) { - testing.waitAndAssertTraces( + testing.waitAndAssertSortedTraces( + comparingRootSpanAttribute(HttpAttributes.HTTP_REQUEST_RESEND_COUNT), IntStream.range(0, options.getMaxRedirects()) .mapToObj(i -> makeCircularRedirectAssertForLolLevelTrace(uri, method, i)) .collect(Collectors.toList())); @@ -398,7 +407,8 @@ void redirectToSecuredCopiesAuthHeader() throws Exception { assertThat(responseCode).isEqualTo(200); if (options.isLowLevelInstrumentation()) { - testing.waitAndAssertTraces( + testing.waitAndAssertSortedTraces( + comparingRootSpanAttribute(HttpAttributes.HTTP_REQUEST_RESEND_COUNT), trace -> { trace.hasSpansSatisfyingExactly( span -> @@ -509,16 +519,18 @@ void captureHttpHeaders() throws Exception { testing.waitAndAssertTraces( trace -> { trace.hasSpansSatisfyingExactly( - span -> { - assertClientSpan(span, uri, method, responseCode, null).hasNoParent(); - span.hasAttributesSatisfying( - equalTo( - AttributeKey.stringArrayKey("http.request.header.x_test_request"), - singletonList("test")), - equalTo( - AttributeKey.stringArrayKey("http.response.header.x_test_response"), - singletonList("test"))); - }, + span -> + assertClientSpan(span, uri, method, responseCode, null) + .hasNoParent() + .hasAttributesSatisfying( + asList( + equalTo( + AttributeKey.stringArrayKey("http.request.header.x-test-request"), + singletonList("test")), + equalTo( + AttributeKey.stringArrayKey( + "http.response.header.x-test-response"), + singletonList("test")))), span -> assertServerSpan(span).hasParent(trace.getSpan(0))); }); } @@ -583,25 +595,14 @@ void connectionErrorUnopenedPortWithCallback() throws Exception { testing.waitAndAssertTraces( trace -> { - List> spanAsserts = - Arrays.asList( - span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(), - span -> - assertClientSpan(span, uri, method, null, null) - .hasParent(trace.getSpan(0)) - .hasException(clientError), - span -> - span.hasName("callback") - .hasKind(SpanKind.INTERNAL) - .hasParent(trace.getSpan(0))); - boolean jdk8 = Objects.equals(System.getProperty("java.specification.version"), "1.8"); - if (jdk8) { - // on some netty based http clients order of `CONNECT` and `callback` spans isn't - // guaranteed when running on jdk8 - trace.hasSpansSatisfyingExactlyInAnyOrder(spanAsserts); - } else { - trace.hasSpansSatisfyingExactly(spanAsserts); - } + trace.hasSpansSatisfyingExactlyInAnyOrder( + span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(), + span -> + assertClientSpan(span, uri, method, null, null) + .hasParent(trace.getSpan(0)) + .hasException(clientError), + span -> + span.hasName("callback").hasKind(SpanKind.INTERNAL).hasParent(trace.getSpan(0))); }); } @@ -696,6 +697,38 @@ void httpsRequest() throws Exception { }); } + @Test + void httpClientMetrics() throws Exception { + URI uri = resolveAddress("/success"); + String method = "GET"; + int responseCode = doRequest(method, uri); + + assertThat(responseCode).isEqualTo(200); + + AtomicReference instrumentationName = new AtomicReference<>(); + testing.waitAndAssertTraces( + trace -> { + instrumentationName.set(trace.getSpan(0).getInstrumentationScopeInfo().getName()); + trace.hasSpansSatisfyingExactly( + span -> assertClientSpan(span, uri, method, responseCode, null).hasNoParent(), + span -> assertServerSpan(span).hasParent(trace.getSpan(0))); + }); + + testing.waitAndAssertMetrics( + instrumentationName.get(), + "http.client.request.duration", + metrics -> + metrics.anySatisfy( + metric -> + assertThat(metric) + .hasDescription("Duration of HTTP client requests.") + .hasUnit("s") + .hasHistogramSatisfying( + histogram -> + histogram.hasPointsSatisfying( + point -> point.hasSumGreaterThan(0.0))))); + } + /** * 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 @@ -926,6 +959,75 @@ void highConcurrencyOnSingleConnection() { pool.shutdown(); } + @Test + void spanEndsAfterBodyReceived() throws Exception { + assumeTrue(options.isSpanEndsAfterBody()); + + String method = "GET"; + URI uri = resolveAddress("/long-request"); + + int responseCode = + doRequest( + method, + uri, + // the time that server waits before completing the response + Collections.singletonMap("delay", String.valueOf(TimeUnit.SECONDS.toMillis(1)))); + + assertThat(responseCode).isEqualTo(200); + + testing.waitAndAssertTraces( + trace -> { + trace.hasSpansSatisfyingExactly( + span -> + assertClientSpan(span, uri, method, 200, null) + .hasNoParent() + .hasStatus(StatusData.unset()), + span -> assertServerSpan(span).hasParent(trace.getSpan(0))); + SpanData span = trace.getSpan(0); + // make sure the span is at least as long as the delay we set when sending the request + assertThat( + span.getEndEpochNanos() - span.getStartEpochNanos() + >= TimeUnit.SECONDS.toNanos(1)) + .describedAs("Span duration should be at least 1s") + .isTrue(); + }); + } + + @Test + void spanEndsAfterHeadersReceived() throws Exception { + assumeTrue(options.isSpanEndsAfterHeaders()); + + String method = "GET"; + URI uri = resolveAddress("/long-request"); + + int responseCode = + doRequest( + method, + uri, + // the time that server waits before completing the response, we expect the response + // headers to arrive much sooner + Collections.singletonMap("delay", String.valueOf(TimeUnit.SECONDS.toMillis(2)))); + + assertThat(responseCode).isEqualTo(200); + + testing.waitAndAssertTraces( + trace -> { + trace.hasSpansSatisfyingExactly( + span -> + assertClientSpan(span, uri, method, 200, null) + .hasNoParent() + .hasStatus(StatusData.unset()), + span -> assertServerSpan(span).hasParent(trace.getSpan(0))); + SpanData span = trace.getSpan(0); + // verify that the span length is less than the delay used to complete the response body + assertThat( + span.getEndEpochNanos() - span.getStartEpochNanos() + <= TimeUnit.SECONDS.toNanos(2)) + .describedAs("Span duration should be less than 2s") + .isTrue(); + }); + } + // Visible for spock bridge. SpanDataAssert assertClientSpan( SpanDataAssert span, @@ -938,103 +1040,80 @@ SpanDataAssert assertClientSpan( .hasKind(SpanKind.CLIENT) .hasAttributesSatisfying( attrs -> { - // TODO: Move to test knob rather than always treating as optional - if (attrs.get(SemanticAttributes.NET_TRANSPORT) != null) { - assertThat(attrs).containsEntry(SemanticAttributes.NET_TRANSPORT, IP_TCP); - } - if (httpClientAttributes.contains(NetAttributes.NET_PROTOCOL_NAME)) { - assertThat(attrs).containsEntry(NetAttributes.NET_PROTOCOL_NAME, "http"); - } - if (httpClientAttributes.contains(NetAttributes.NET_PROTOCOL_VERSION)) { - // TODO(anuraaga): Support HTTP/2 - assertThat(attrs).containsEntry(NetAttributes.NET_PROTOCOL_VERSION, "1.1"); + // we're opting out of these attributes in the new semconv + assertThat(attrs) + .doesNotContainKey(NetworkAttributes.NETWORK_TRANSPORT) + .doesNotContainKey(NetworkAttributes.NETWORK_TYPE) + .doesNotContainKey(NetworkAttributes.NETWORK_PROTOCOL_NAME); + if (httpClientAttributes.contains(NetworkAttributes.NETWORK_PROTOCOL_VERSION)) { + assertThat(attrs) + .containsEntry( + NetworkAttributes.NETWORK_PROTOCOL_VERSION, + options.getHttpProtocolVersion().apply(uri)); } - if (httpClientAttributes.contains(SemanticAttributes.NET_PEER_NAME)) { - assertThat(attrs).containsEntry(SemanticAttributes.NET_PEER_NAME, uri.getHost()); + + if (httpClientAttributes.contains(ServerAttributes.SERVER_ADDRESS)) { + assertThat(attrs).containsEntry(ServerAttributes.SERVER_ADDRESS, uri.getHost()); } - if (httpClientAttributes.contains(SemanticAttributes.NET_PEER_PORT)) { + if (httpClientAttributes.contains(ServerAttributes.SERVER_PORT)) { int uriPort = uri.getPort(); - // default values are ignored - if (uriPort <= 0 || uriPort == 80 || uriPort == 443) { - assertThat(attrs).doesNotContainKey(SemanticAttributes.NET_PEER_PORT); + if (uriPort <= 0) { + if (attrs.get(ServerAttributes.SERVER_PORT) != null) { + int effectivePort = "https".equals(uri.getScheme()) ? 443 : 80; + assertThat(attrs).containsEntry(ServerAttributes.SERVER_PORT, effectivePort); + } + // alternatively, peer port is not emitted -- and that's fine too } else { - assertThat(attrs).containsEntry(SemanticAttributes.NET_PEER_PORT, uriPort); + assertThat(attrs).containsEntry(ServerAttributes.SERVER_PORT, uriPort); } } - if (uri.getPort() == PortUtils.UNUSABLE_PORT || uri.getHost().equals("192.0.2.1")) { - // In these cases the peer connection is not established, so the HTTP client should - // not report any socket-level attributes - assertThat(attrs) - .doesNotContainKey("net.sock.family") - // TODO netty sometimes reports net.sock.peer.addr in connection error test - // .doesNotContainKey("net.sock.peer.addr") - .doesNotContainKey("net.sock.peer.name") - .doesNotContainKey("net.sock.peer.port"); - - } else { + if (uri.getPort() != PortUtils.UNUSABLE_PORT && !uri.getHost().equals("192.0.2.1")) { // TODO: Move to test knob rather than always treating as optional - if (attrs.get(SemanticAttributes.NET_SOCK_PEER_ADDR) != null) { + if (attrs.get(NetworkAttributes.NETWORK_PEER_ADDRESS) != null) { assertThat(attrs) - .containsEntry(SemanticAttributes.NET_SOCK_PEER_ADDR, "127.0.0.1"); + .hasEntrySatisfying( + NetworkAttributes.NETWORK_PEER_ADDRESS, + addr -> assertThat(addr).isIn("127.0.0.1", "0:0:0:0:0:0:0:1")); } - if (attrs.get(SemanticAttributes.NET_SOCK_PEER_PORT) != null) { + if (attrs.get(NetworkAttributes.NETWORK_PEER_PORT) != null) { assertThat(attrs) .containsEntry( - SemanticAttributes.NET_SOCK_PEER_PORT, + NetworkAttributes.NETWORK_PEER_PORT, Objects.equals(uri.getScheme(), "https") ? server.httpsPort() : server.httpPort()); } } - if (httpClientAttributes.contains(SemanticAttributes.HTTP_URL)) { - assertThat(attrs).containsEntry(SemanticAttributes.HTTP_URL, uri.toString()); + if (httpClientAttributes.contains(UrlAttributes.URL_FULL)) { + assertThat(attrs).containsEntry(UrlAttributes.URL_FULL, uri.toString()); } - if (httpClientAttributes.contains(SemanticAttributes.HTTP_METHOD)) { - assertThat(attrs).containsEntry(SemanticAttributes.HTTP_METHOD, method); - } - if (httpClientAttributes.contains(SemanticAttributes.USER_AGENT_ORIGINAL)) { - String userAgent = options.getUserAgent(); - if (userAgent != null - || attrs.get(SemanticAttributes.USER_AGENT_ORIGINAL) != null) { - assertThat(attrs) - .hasEntrySatisfying( - SemanticAttributes.USER_AGENT_ORIGINAL, - actual -> { - if (userAgent != null) { - assertThat(actual).startsWith(userAgent); - } else { - assertThat(actual).isNull(); - } - }); - } - } - if (attrs.get(SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH) != null) { - assertThat(attrs) - .hasEntrySatisfying( - SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH, - length -> assertThat(length).isNotNegative()); - } - if (attrs.get(SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH) != null) { - assertThat(attrs) - .hasEntrySatisfying( - SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH, - length -> assertThat(length).isNotNegative()); + if (httpClientAttributes.contains(HttpAttributes.HTTP_REQUEST_METHOD)) { + assertThat(attrs).containsEntry(HttpAttributes.HTTP_REQUEST_METHOD, method); } + // opt-in, not collected by default + assertThat(attrs).doesNotContainKey(UserAgentAttributes.USER_AGENT_ORIGINAL); + if (responseCode != null) { assertThat(attrs) - .containsEntry(SemanticAttributes.HTTP_STATUS_CODE, (long) responseCode); + .containsEntry(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, (long) responseCode); + if (responseCode >= 400) { + assertThat(attrs) + .containsEntry(ErrorAttributes.ERROR_TYPE, String.valueOf(responseCode)); + } } else { - assertThat(attrs).doesNotContainKey(SemanticAttributes.HTTP_STATUS_CODE); + assertThat(attrs).doesNotContainKey(HttpAttributes.HTTP_RESPONSE_STATUS_CODE); + // TODO: add more detailed assertions, per url + assertThat(attrs).containsKey(ErrorAttributes.ERROR_TYPE); } if (resendCount != null) { assertThat(attrs) - .containsEntry(SemanticAttributes.HTTP_RESEND_COUNT, (long) resendCount); + .containsEntry(HttpAttributes.HTTP_REQUEST_RESEND_COUNT, (long) resendCount); } else { - assertThat(attrs).doesNotContainKey(SemanticAttributes.HTTP_RESEND_COUNT); + assertThat(attrs).doesNotContainKey(HttpAttributes.HTTP_REQUEST_RESEND_COUNT); } }); } diff --git a/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/junit/http/AbstractHttpServerTest.java b/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/junit/http/AbstractHttpServerTest.java index c6d302659179..74c9e6c67af2 100644 --- a/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/junit/http/AbstractHttpServerTest.java +++ b/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/junit/http/AbstractHttpServerTest.java @@ -12,11 +12,11 @@ import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.INDEXED_CHILD; import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.NOT_FOUND; import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.PATH_PARAM; -import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.QUERY_PARAM; import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.REDIRECT; import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.SUCCESS; import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; +import static org.junit.jupiter.api.Assumptions.assumeFalse; import static org.junit.jupiter.api.Assumptions.assumeTrue; import com.google.errorprone.annotations.CanIgnoreReturnValue; @@ -27,13 +27,19 @@ import io.opentelemetry.context.Context; import io.opentelemetry.context.propagation.TextMapPropagator; import io.opentelemetry.context.propagation.TextMapSetter; -import io.opentelemetry.instrumentation.api.instrumenter.net.internal.NetAttributes; +import io.opentelemetry.instrumentation.api.internal.HttpConstants; import io.opentelemetry.instrumentation.testing.GlobalTraceUtil; import io.opentelemetry.sdk.testing.assertj.SpanDataAssert; import io.opentelemetry.sdk.testing.assertj.TraceAssert; import io.opentelemetry.sdk.trace.data.SpanData; import io.opentelemetry.sdk.trace.data.StatusData; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.opentelemetry.semconv.ClientAttributes; +import io.opentelemetry.semconv.ErrorAttributes; +import io.opentelemetry.semconv.HttpAttributes; +import io.opentelemetry.semconv.NetworkAttributes; +import io.opentelemetry.semconv.ServerAttributes; +import io.opentelemetry.semconv.UrlAttributes; +import io.opentelemetry.semconv.UserAgentAttributes; import io.opentelemetry.testing.internal.armeria.common.AggregatedHttpRequest; import io.opentelemetry.testing.internal.armeria.common.AggregatedHttpResponse; import io.opentelemetry.testing.internal.armeria.common.HttpData; @@ -67,6 +73,7 @@ import java.util.Set; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; import java.util.function.Supplier; import java.util.stream.Stream; @@ -117,7 +124,12 @@ public static T controller(ServerEndpoint endpoint, Supplier closure) { } protected AggregatedHttpRequest request(ServerEndpoint uri, String method) { - return AggregatedHttpRequest.of(HttpMethod.valueOf(method), resolveAddress(uri)); + return AggregatedHttpRequest.of( + HttpMethod.valueOf(method), resolveAddress(uri, getProtocolPrefix())); + } + + private String getProtocolPrefix() { + return options.useHttp2 ? "h2c://" : "h1c://"; } @ParameterizedTest @@ -136,7 +148,7 @@ void successfulGetRequest(int count) { assertResponseHasCustomizedHeaders(response, SUCCESS, null); } - assertTheTraces(count, null, null, null, method, SUCCESS, responses.get(0)); + assertTheTraces(count, null, null, null, method, SUCCESS); } @Test @@ -157,7 +169,7 @@ void successfulGetRequestWithParent() { assertThat(response.contentUtf8()).isEqualTo(SUCCESS.getBody()); String spanId = assertResponseHasCustomizedHeaders(response, SUCCESS, traceId); - assertTheTraces(1, traceId, parentId, spanId, "GET", SUCCESS, response); + assertTheTraces(1, traceId, parentId, spanId, "GET", SUCCESS); } @Test @@ -176,7 +188,7 @@ void tracingHeaderIsCaseInsensitive() { assertThat(response.contentUtf8()).isEqualTo(SUCCESS.getBody()); String spanId = assertResponseHasCustomizedHeaders(response, SUCCESS, traceId); - assertTheTraces(1, traceId, parentId, spanId, "GET", SUCCESS, response); + assertTheTraces(1, traceId, parentId, spanId, "GET", SUCCESS); } @ParameterizedTest @@ -190,7 +202,7 @@ void requestWithQueryString(ServerEndpoint endpoint) { assertThat(response.contentUtf8()).isEqualTo(endpoint.getBody()); String spanId = assertResponseHasCustomizedHeaders(response, endpoint, null); - assertTheTraces(1, null, null, spanId, method, endpoint, response); + assertTheTraces(1, null, null, spanId, method, endpoint); } private static Stream provideServerEndpoints() { @@ -214,7 +226,7 @@ void requestWithRedirect() { .isEqualTo(address.resolve(REDIRECT.getBody()).toString())); String spanId = assertResponseHasCustomizedHeaders(response, REDIRECT, null); - assertTheTraces(1, null, null, spanId, method, REDIRECT, response); + assertTheTraces(1, null, null, spanId, method, REDIRECT); } @Test @@ -231,7 +243,7 @@ void requestWithError() { } String spanId = assertResponseHasCustomizedHeaders(response, ERROR, null); - assertTheTraces(1, null, null, spanId, method, ERROR, response); + assertTheTraces(1, null, null, spanId, method, ERROR); } @Test @@ -249,7 +261,7 @@ void requestWithException() { assertThat(response.status().code()).isEqualTo(EXCEPTION.getStatus()); String spanId = assertResponseHasCustomizedHeaders(response, EXCEPTION, null); - assertTheTraces(1, null, null, spanId, method, EXCEPTION, response); + assertTheTraces(1, null, null, spanId, method, EXCEPTION); } finally { Awaitility.reset(); } @@ -266,7 +278,7 @@ void requestForNotFound() { assertThat(response.status().code()).isEqualTo(NOT_FOUND.getStatus()); String spanId = assertResponseHasCustomizedHeaders(response, NOT_FOUND, null); - assertTheTraces(1, null, null, spanId, method, NOT_FOUND, response); + assertTheTraces(1, null, null, spanId, method, NOT_FOUND); } @Test @@ -281,7 +293,7 @@ void requestWithPathParameter() { assertThat(response.contentUtf8()).isEqualTo(PATH_PARAM.getBody()); String spanId = assertResponseHasCustomizedHeaders(response, PATH_PARAM, null); - assertTheTraces(1, null, null, spanId, method, PATH_PARAM, response); + assertTheTraces(1, null, null, spanId, method, PATH_PARAM); } @Test @@ -300,7 +312,7 @@ void captureHttpHeaders() { assertThat(response.headers().get("X-Test-Response")).isEqualTo("test"); String spanId = assertResponseHasCustomizedHeaders(response, CAPTURE_HEADERS, null); - assertTheTraces(1, null, null, spanId, "GET", CAPTURE_HEADERS, response); + assertTheTraces(1, null, null, spanId, "GET", CAPTURE_HEADERS); } @Test @@ -310,7 +322,8 @@ void captureRequestParameters() { QueryParams formBody = QueryParams.builder().add("test-parameter", "test value õäöü").build(); AggregatedHttpRequest request = AggregatedHttpRequest.of( - RequestHeaders.builder(HttpMethod.POST, resolveAddress(CAPTURE_PARAMETERS)) + RequestHeaders.builder( + HttpMethod.POST, resolveAddress(CAPTURE_PARAMETERS, getProtocolPrefix())) .contentType(MediaType.FORM_DATA) .build(), HttpData.ofUtf8(formBody.toQueryString())); @@ -320,7 +333,39 @@ void captureRequestParameters() { assertThat(response.contentUtf8()).isEqualTo(CAPTURE_PARAMETERS.getBody()); String spanId = assertResponseHasCustomizedHeaders(response, CAPTURE_PARAMETERS, null); - assertTheTraces(1, null, null, spanId, "POST", CAPTURE_PARAMETERS, response); + assertTheTraces(1, null, null, spanId, "POST", CAPTURE_PARAMETERS); + } + + @Test + void httpServerMetrics() { + String method = "GET"; + AggregatedHttpRequest request = request(SUCCESS, method); + AggregatedHttpResponse response = client.execute(request).aggregate().join(); + + assertThat(response.status().code()).isEqualTo(SUCCESS.getStatus()); + assertThat(response.contentUtf8()).isEqualTo(SUCCESS.getBody()); + + AtomicReference instrumentationName = new AtomicReference<>(); + testing.waitAndAssertTraces( + trace -> { + instrumentationName.set(trace.getSpan(0).getInstrumentationScopeInfo().getName()); + trace.anySatisfy( + spanData -> assertServerSpan(assertThat(spanData), method, SUCCESS, SUCCESS.status)); + }); + + testing.waitAndAssertMetrics( + instrumentationName.get(), + "http.server.request.duration", + metrics -> + metrics.anySatisfy( + metric -> + assertThat(metric) + .hasDescription("Duration of HTTP server requests.") + .hasUnit("s") + .hasHistogramSatisfying( + histogram -> + histogram.hasPointsSatisfying( + point -> point.hasSumGreaterThan(0.0))))); } /** @@ -351,7 +396,7 @@ void highConcurrency() throws InterruptedException { 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://")) + .get(endpoint.resolvePath(address).toString().replace("http://", getProtocolPrefix())) .queryParam(ServerEndpoint.ID_PARAMETER_NAME, index); testing.runWithSpan( @@ -373,6 +418,8 @@ void highConcurrency() throws InterruptedException { @Test void httpPipelining() throws InterruptedException { assumeTrue(options.testHttpPipelining); + // test uses http 1.1 + assumeFalse(options.useHttp2); int count = 10; CountDownLatch countDownLatch = new CountDownLatch(count); @@ -435,6 +482,55 @@ protected void channelRead0( } } + @Test + void requestWithNonStandardHttpMethod() throws InterruptedException { + assumeTrue(options.testNonStandardHttpMethod); + // test uses http 1.1 + assumeFalse(options.useHttp2); + + EventLoopGroup eventLoopGroup = new NioEventLoopGroup(); + try { + Bootstrap bootstrap = buildBootstrap(eventLoopGroup); + Channel channel = bootstrap.connect(address.getHost(), port).sync().channel(); + + String method = "TEST"; + DefaultFullHttpRequest request = + new DefaultFullHttpRequest( + HttpVersion.HTTP_1_1, + new io.opentelemetry.testing.internal.io.netty.handler.codec.http.HttpMethod(method), + SUCCESS.resolvePath(address).getPath(), + Unpooled.EMPTY_BUFFER); + request.headers().set(HttpHeaderNames.HOST, address.getHost() + ":" + port); + request.headers().set(HttpHeaderNames.USER_AGENT, TEST_USER_AGENT); + request.headers().set(HttpHeaderNames.X_FORWARDED_FOR, TEST_CLIENT_IP); + + testing + .getOpenTelemetry() + .getPropagators() + .getTextMapPropagator() + .inject( + Context.current(), + request, + (carrier, key, value) -> carrier.headers().set(key, value)); + channel.writeAndFlush(request); + + // TODO: add stricter assertions; could be difficult with the groovy code still in place + // though + testing.waitAndAssertTraces( + trace -> + trace.anySatisfy( + span -> + assertServerSpan( + assertThat(span), + HttpConstants._OTHER, + SUCCESS, + options.responseCodeOnNonStandardHttpMethod) + .hasAttribute(HttpAttributes.HTTP_REQUEST_METHOD_ORIGINAL, method))); + } finally { + eventLoopGroup.shutdownGracefully().await(10, TimeUnit.SECONDS); + } + } + private static Bootstrap buildBootstrap(EventLoopGroup eventLoopGroup) { Bootstrap bootstrap = new Bootstrap(); bootstrap @@ -480,15 +576,11 @@ protected void assertHighConcurrency(int count) { span -> assertHandlerSpan(span, "GET", endpoint).hasParent(trace.getSpan(1))); } - int parentIndex = spanAssertions.size() - 2; - if (options.hasHandlerAsControllerParentSpan.test(endpoint)) { - parentIndex = parentIndex + 1; - } - int finalParentIndex = parentIndex; + int parentIndex = spanAssertions.size() - 1; spanAssertions.add( span -> assertIndexedControllerSpan(span, requestId) - .hasParent(trace.getSpan(finalParentIndex))); + .hasParent(trace.getSpan(parentIndex))); trace.hasSpansSatisfyingExactly(spanAssertions); }); @@ -523,8 +615,7 @@ protected void assertTheTraces( String parentId, String spanId, String method, - ServerEndpoint endpoint, - AggregatedHttpResponse response) { + ServerEndpoint endpoint) { List> assertions = new ArrayList<>(); for (int i = 0; i < size; i++) { assertions.add( @@ -532,7 +623,7 @@ protected void assertTheTraces( List> spanAssertions = new ArrayList<>(); spanAssertions.add( span -> { - assertServerSpan(span, method, endpoint); + assertServerSpan(span, method, endpoint, endpoint.status); if (traceId != null) { span.hasTraceId(traceId); } @@ -556,8 +647,7 @@ protected void assertTheTraces( if (endpoint != NOT_FOUND) { int parentIndex = 0; - if (options.hasHandlerSpan.test(endpoint) - && options.hasHandlerAsControllerParentSpan.test(endpoint)) { + if (options.hasHandlerSpan.test(endpoint)) { parentIndex = spanAssertions.size() - 1; } int finalParentIndex = parentIndex; @@ -567,15 +657,15 @@ protected void assertTheTraces( span, endpoint == EXCEPTION ? options.expectedException : null); span.hasParent(trace.getSpan(finalParentIndex)); }); + if (options.hasRenderSpan.test(endpoint)) { + spanAssertions.add(span -> assertRenderSpan(span, method, endpoint)); + } } if (options.hasResponseSpan.test(endpoint)) { int parentIndex = spanAssertions.size() - 1; spanAssertions.add( - span -> { - assertResponseSpan(span, method, endpoint); - span.hasParent(trace.getSpan(parentIndex)); - }); + span -> assertResponseSpan(span, trace.getSpan(parentIndex), method, endpoint)); } if (options.hasErrorPageSpans.test(endpoint)) { @@ -616,12 +706,25 @@ protected SpanDataAssert assertHandlerSpan( "assertHandlerSpan not implemented in " + getClass().getName()); } + @CanIgnoreReturnValue + protected SpanDataAssert assertResponseSpan( + SpanDataAssert span, SpanData parentSpan, String method, ServerEndpoint endpoint) { + span.hasParent(parentSpan); + return assertResponseSpan(span, method, endpoint); + } + protected SpanDataAssert assertResponseSpan( SpanDataAssert span, String method, ServerEndpoint endpoint) { throw new UnsupportedOperationException( "assertResponseSpan not implemented in " + getClass().getName()); } + protected SpanDataAssert assertRenderSpan( + SpanDataAssert span, String method, ServerEndpoint endpoint) { + throw new UnsupportedOperationException( + "assertRenderSpan not implemented in " + getClass().getName()); + } + protected List> errorPageSpanAssertions( String method, ServerEndpoint endpoint) { throw new UnsupportedOperationException( @@ -630,14 +733,14 @@ protected List> errorPageSpanAssertions( @CanIgnoreReturnValue protected SpanDataAssert assertServerSpan( - SpanDataAssert span, String method, ServerEndpoint endpoint) { + SpanDataAssert span, String method, ServerEndpoint endpoint, int statusCode) { Set> httpAttributes = options.httpAttributes.apply(endpoint); - String expectedRoute = options.expectedHttpRoute.apply(endpoint); - String name = getString(method, endpoint, expectedRoute); + String expectedRoute = options.expectedHttpRoute.apply(endpoint, method); + String name = options.expectedServerSpanNameMapper.apply(endpoint, method, expectedRoute); span.hasName(name).hasKind(SpanKind.SERVER); - if (endpoint.status >= 500) { + if (statusCode >= 500) { span.hasStatus(StatusData.error()); } @@ -647,116 +750,90 @@ protected SpanDataAssert assertServerSpan( span.hasAttributesSatisfying( attrs -> { - if (httpAttributes.contains(SemanticAttributes.NET_TRANSPORT)) { + // we're opting out of these attributes in the new semconv + assertThat(attrs) + .doesNotContainKey(NetworkAttributes.NETWORK_TRANSPORT) + .doesNotContainKey(NetworkAttributes.NETWORK_TYPE) + .doesNotContainKey(NetworkAttributes.NETWORK_PROTOCOL_NAME); + + if (attrs.get(NetworkAttributes.NETWORK_PROTOCOL_VERSION) != null) { assertThat(attrs) .containsEntry( - SemanticAttributes.NET_TRANSPORT, SemanticAttributes.NetTransportValues.IP_TCP); + NetworkAttributes.NETWORK_PROTOCOL_VERSION, options.useHttp2 ? "2" : "1.1"); } - assertThat(attrs).containsEntry(SemanticAttributes.NET_HOST_NAME, "localhost"); + assertThat(attrs).containsEntry(ServerAttributes.SERVER_ADDRESS, "localhost"); // TODO: Move to test knob rather than always treating as optional // TODO: once httpAttributes test knob is used, verify default port values - if (attrs.get(SemanticAttributes.NET_HOST_PORT) != null) { - assertThat(attrs).containsEntry(SemanticAttributes.NET_HOST_PORT, port); + if (attrs.get(ServerAttributes.SERVER_PORT) != null) { + assertThat(attrs).containsEntry(ServerAttributes.SERVER_PORT, port); + } + + if (attrs.get(NetworkAttributes.NETWORK_PEER_ADDRESS) != null) { + assertThat(attrs) + .containsEntry( + NetworkAttributes.NETWORK_PEER_ADDRESS, options.sockPeerAddr.apply(endpoint)); } - if (attrs.get(SemanticAttributes.NET_SOCK_PEER_PORT) != null) { + if (attrs.get(NetworkAttributes.NETWORK_PEER_PORT) != null) { assertThat(attrs) .hasEntrySatisfying( - SemanticAttributes.NET_SOCK_PEER_PORT, + NetworkAttributes.NETWORK_PEER_PORT, value -> assertThat(value) .isInstanceOf(Long.class) .isNotEqualTo(Long.valueOf(port))); } - if (attrs.get(SemanticAttributes.NET_SOCK_PEER_ADDR) != null) { - assertThat(attrs) - .containsEntry( - SemanticAttributes.NET_SOCK_PEER_ADDR, options.sockPeerAddr.apply(endpoint)); - } - if (attrs.get(SemanticAttributes.NET_SOCK_HOST_ADDR) != null) { - assertThat(attrs).containsEntry(SemanticAttributes.NET_SOCK_HOST_ADDR, "127.0.0.1"); - } - assertThat(attrs) - .hasEntrySatisfying( - SemanticAttributes.HTTP_CLIENT_IP, - entry -> - assertThat(entry) - .satisfiesAnyOf( - value -> assertThat(value).isNull(), - value -> assertThat(value).isEqualTo(TEST_CLIENT_IP))); - assertThat(attrs).containsEntry(SemanticAttributes.HTTP_METHOD, method); - assertThat(attrs).containsEntry(SemanticAttributes.HTTP_STATUS_CODE, endpoint.status); - - if (attrs.get(NetAttributes.NET_PROTOCOL_NAME) != null) { - assertThat(attrs).containsEntry(NetAttributes.NET_PROTOCOL_NAME, "http"); - } - if (attrs.get(NetAttributes.NET_PROTOCOL_VERSION) != null) { - assertThat(attrs) - .hasEntrySatisfying( - NetAttributes.NET_PROTOCOL_VERSION, - entry -> assertThat(entry).isIn("1.1", "2.0")); + assertThat(attrs).containsEntry(ClientAttributes.CLIENT_ADDRESS, TEST_CLIENT_IP); + // client.port is opt-in + assertThat(attrs).doesNotContainKey(ClientAttributes.CLIENT_PORT); + + assertThat(attrs).containsEntry(HttpAttributes.HTTP_REQUEST_METHOD, method); + + assertThat(attrs).containsEntry(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, statusCode); + if (statusCode >= 500) { + assertThat(attrs).containsEntry(ErrorAttributes.ERROR_TYPE, String.valueOf(statusCode)); } - assertThat(attrs).containsEntry(SemanticAttributes.USER_AGENT_ORIGINAL, TEST_USER_AGENT); - assertThat(attrs).containsEntry(SemanticAttributes.HTTP_SCHEME, "http"); + assertThat(attrs).containsEntry(UserAgentAttributes.USER_AGENT_ORIGINAL, TEST_USER_AGENT); + + assertThat(attrs).containsEntry(UrlAttributes.URL_SCHEME, "http"); if (endpoint != INDEXED_CHILD) { assertThat(attrs) - .containsEntry( - SemanticAttributes.HTTP_TARGET, - endpoint.resolvePath(address).getPath() - + (endpoint == QUERY_PARAM ? "?" + endpoint.body : "")); + .containsEntry(UrlAttributes.URL_PATH, endpoint.resolvePath(address).getPath()); + if (endpoint.getQuery() != null) { + assertThat(attrs).containsEntry(UrlAttributes.URL_QUERY, endpoint.getQuery()); + } } - if (attrs.get(SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH) != null) { - assertThat(attrs) - .hasEntrySatisfying( - SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH, - entry -> assertThat(entry).isNotNegative()); - } - if (attrs.get(SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH) != null) { - assertThat(attrs) - .hasEntrySatisfying( - SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH, - entry -> assertThat(entry).isNotNegative()); - } - if (httpAttributes.contains(SemanticAttributes.HTTP_ROUTE) && expectedRoute != null) { - assertThat(attrs).containsEntry(SemanticAttributes.HTTP_ROUTE, expectedRoute); + if (httpAttributes.contains(HttpAttributes.HTTP_ROUTE) && expectedRoute != null) { + assertThat(attrs).containsEntry(HttpAttributes.HTTP_ROUTE, expectedRoute); } if (endpoint == CAPTURE_HEADERS) { assertThat(attrs) - .containsEntry("http.request.header.x_test_request", new String[] {"test"}); + .containsEntry("http.request.header.x-test-request", new String[] {"test"}); assertThat(attrs) - .containsEntry("http.response.header.x_test_response", new String[] {"test"}); + .containsEntry("http.response.header.x-test-response", new String[] {"test"}); } if (endpoint == CAPTURE_PARAMETERS) { assertThat(attrs) .containsEntry( - "servlet.request.parameter.test_parameter", new String[] {"test value õäöü"}); + "servlet.request.parameter.test-parameter", new String[] {"test value õäöü"}); } }); return span; } - private String getString(String method, ServerEndpoint endpoint, String expectedRoute) { - String name = options.expectedServerSpanNameMapper.apply(endpoint, method, expectedRoute); - return name; - } - @CanIgnoreReturnValue protected SpanDataAssert assertIndexedServerSpan(SpanDataAssert span, int requestId) { ServerEndpoint endpoint = INDEXED_CHILD; String method = "GET"; - assertServerSpan(span, method, endpoint); - - span.hasAttributesSatisfying( - equalTo( - SemanticAttributes.HTTP_TARGET, - endpoint.resolvePath(address).getPath() + "?id=" + requestId)); - - return span; + return assertServerSpan(span, method, endpoint, endpoint.status) + .hasAttributesSatisfying( + equalTo(UrlAttributes.URL_PATH, endpoint.resolvePath(address).getPath()), + equalTo(UrlAttributes.URL_QUERY, "id=" + requestId)); } @CanIgnoreReturnValue @@ -774,9 +851,13 @@ public String expectedServerSpanName( endpoint, method, route); } - public String expectedHttpRoute(ServerEndpoint endpoint) { + public String expectedHttpRoute(ServerEndpoint endpoint, String method) { // no need to compute route if we're not expecting it - if (!options.httpAttributes.apply(endpoint).contains(SemanticAttributes.HTTP_ROUTE)) { + if (!options.httpAttributes.apply(endpoint).contains(HttpAttributes.HTTP_ROUTE)) { + return null; + } + + if (HttpConstants._OTHER.equals(method)) { return null; } diff --git a/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/junit/http/AbstractHttpServerUsingTest.java b/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/junit/http/AbstractHttpServerUsingTest.java index d63a8dbb3cec..daa7aa8e582a 100644 --- a/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/junit/http/AbstractHttpServerUsingTest.java +++ b/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/junit/http/AbstractHttpServerUsingTest.java @@ -27,9 +27,9 @@ public abstract class AbstractHttpServerUsingTest { public int port; public URI address; - protected abstract SERVER setupServer(); + protected abstract SERVER setupServer() throws Exception; - protected abstract void stopServer(SERVER server); + protected abstract void stopServer(SERVER server) throws Exception; protected final InstrumentationTestRunner testing() { return testing; @@ -40,7 +40,11 @@ protected void startServer() { address = buildAddress(); } - server = setupServer(); + try { + server = setupServer(); + } catch (Exception exception) { + throw new IllegalStateException("Failed to start server", exception); + } if (server != null) { logger.info( getClass().getName() @@ -57,7 +61,11 @@ protected void cleanupServer() { logger.info(getClass().getName() + " can't stop null server"); return; } - stopServer(server); + try { + stopServer(server); + } catch (Exception exception) { + throw new IllegalStateException("Failed to stop server", exception); + } server = null; logger.info(getClass().getName() + " http server stopped at: http://localhost:" + port + "/"); } @@ -79,9 +87,13 @@ void verifyExtension() { } protected String resolveAddress(ServerEndpoint uri) { + return resolveAddress(uri, "h1c://"); + } + + protected String resolveAddress(ServerEndpoint uri, String protocolPrefix) { String url = uri.resolvePath(address).toString(); // Force HTTP/1 via h1c so upgrade requests don't show up as traces - url = url.replace("http://", "h1c://"); + url = url.replace("http://", protocolPrefix); if (uri.getQuery() != null) { url += "?" + uri.getQuery(); } diff --git a/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/junit/http/HttpClientTestOptions.java b/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/junit/http/HttpClientTestOptions.java index 4878e4fd046a..4500176bbf0c 100644 --- a/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/junit/http/HttpClientTestOptions.java +++ b/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/junit/http/HttpClientTestOptions.java @@ -8,8 +8,11 @@ import com.google.auto.value.AutoValue; import com.google.errorprone.annotations.CanIgnoreReturnValue; import io.opentelemetry.api.common.AttributeKey; -import io.opentelemetry.instrumentation.api.instrumenter.net.internal.NetAttributes; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.opentelemetry.instrumentation.api.internal.HttpConstants; +import io.opentelemetry.semconv.HttpAttributes; +import io.opentelemetry.semconv.NetworkAttributes; +import io.opentelemetry.semconv.ServerAttributes; +import io.opentelemetry.semconv.UrlAttributes; import io.opentelemetry.testing.internal.armeria.common.HttpStatus; import java.net.URI; import java.util.Arrays; @@ -22,20 +25,19 @@ @AutoValue public abstract class HttpClientTestOptions { + public static final Set> DEFAULT_HTTP_ATTRIBUTES = Collections.unmodifiableSet( new HashSet<>( Arrays.asList( - NetAttributes.NET_PROTOCOL_NAME, - NetAttributes.NET_PROTOCOL_VERSION, - SemanticAttributes.NET_PEER_NAME, - SemanticAttributes.NET_PEER_PORT, - SemanticAttributes.HTTP_URL, - SemanticAttributes.HTTP_METHOD, - SemanticAttributes.USER_AGENT_ORIGINAL))); + NetworkAttributes.NETWORK_PROTOCOL_VERSION, + ServerAttributes.SERVER_ADDRESS, + ServerAttributes.SERVER_PORT, + UrlAttributes.URL_FULL, + HttpAttributes.HTTP_REQUEST_METHOD))); public static final BiFunction DEFAULT_EXPECTED_CLIENT_SPAN_NAME_MAPPER = - (uri, method) -> method; + (uri, method) -> HttpConstants._OTHER.equals(method) ? "HTTP" : method; public static final int FOUND_STATUS_CODE = HttpStatus.FOUND.code(); @@ -44,9 +46,6 @@ public abstract class HttpClientTestOptions { @Nullable public abstract Integer getResponseCodeOnRedirectError(); - @Nullable - public abstract String getUserAgent(); - public abstract BiFunction getClientSpanErrorMapper(); /** @@ -86,13 +85,23 @@ public boolean isLowLevelInstrumentation() { public abstract boolean getTestCallbackWithParent(); - // depending on async behavior callback can be executed within - // parent span scope or outside of the scope, e.g. in reactor-netty or spring - // callback is correlated. - public abstract boolean getTestCallbackWithImplicitParent(); - public abstract boolean getTestErrorWithCallback(); + public abstract boolean getTestNonStandardHttpMethod(); + + public abstract Function getHttpProtocolVersion(); + + @Nullable + abstract SpanEndsAfterType getSpanEndsAfterType(); + + public boolean isSpanEndsAfterHeaders() { + return getSpanEndsAfterType() == SpanEndsAfterType.HEADERS; + } + + public boolean isSpanEndsAfterBody() { + return getSpanEndsAfterType() == SpanEndsAfterType.BODY; + } + static Builder builder() { return new AutoValue_HttpClientTestOptions.Builder().withDefaults(); } @@ -104,11 +113,11 @@ public interface Builder { default Builder withDefaults() { return setHttpAttributes(x -> DEFAULT_HTTP_ATTRIBUTES) .setResponseCodeOnRedirectError(FOUND_STATUS_CODE) - .setUserAgent(null) .setClientSpanErrorMapper((uri, exception) -> exception) .setSingleConnectionFactory((host, port) -> null) .setExpectedClientSpanNameMapper(DEFAULT_EXPECTED_CLIENT_SPAN_NAME_MAPPER) .setInstrumentationType(HttpClientInstrumentationType.HIGH_LEVEL) + .setSpanEndsAfterType(SpanEndsAfterType.HEADERS) .setTestWithClientParent(true) .setTestRedirects(true) .setTestCircularRedirects(true) @@ -120,16 +129,15 @@ default Builder withDefaults() { .setTestHttps(true) .setTestCallback(true) .setTestCallbackWithParent(true) - .setTestCallbackWithImplicitParent(false) - .setTestErrorWithCallback(true); + .setTestErrorWithCallback(true) + .setTestNonStandardHttpMethod(true) + .setHttpProtocolVersion(uri -> "1.1"); } Builder setHttpAttributes(Function>> value); Builder setResponseCodeOnRedirectError(Integer value); - Builder setUserAgent(String value); - Builder setClientSpanErrorMapper(BiFunction value); Builder setSingleConnectionFactory(BiFunction value); @@ -138,6 +146,8 @@ default Builder withDefaults() { Builder setInstrumentationType(HttpClientInstrumentationType instrumentationType); + Builder setSpanEndsAfterType(SpanEndsAfterType spanEndsAfterType); + Builder setTestWithClientParent(boolean value); Builder setTestRedirects(boolean value); @@ -160,10 +170,12 @@ default Builder withDefaults() { Builder setTestCallbackWithParent(boolean value); - Builder setTestCallbackWithImplicitParent(boolean value); - Builder setTestErrorWithCallback(boolean value); + Builder setTestNonStandardHttpMethod(boolean value); + + Builder setHttpProtocolVersion(Function value); + @CanIgnoreReturnValue default Builder disableTestWithClientParent() { return setTestWithClientParent(false); @@ -220,8 +232,8 @@ default Builder disableTestErrorWithCallback() { } @CanIgnoreReturnValue - default Builder enableTestCallbackWithImplicitParent() { - return setTestCallbackWithImplicitParent(true); + default Builder disableTestNonStandardHttpMethod() { + return setTestNonStandardHttpMethod(false); } @CanIgnoreReturnValue @@ -229,6 +241,16 @@ default Builder markAsLowLevelInstrumentation() { return setInstrumentationType(HttpClientInstrumentationType.LOW_LEVEL); } + @CanIgnoreReturnValue + default Builder disableTestSpanEndsAfter() { + return setSpanEndsAfterType(null); + } + + @CanIgnoreReturnValue + default Builder spanEndsAfterBody() { + return setSpanEndsAfterType(SpanEndsAfterType.BODY); + } + HttpClientTestOptions build(); } @@ -241,4 +263,11 @@ enum HttpClientInstrumentationType { /** Creates a single span for the topmost HTTP client operation. */ HIGH_LEVEL } + + enum SpanEndsAfterType { + /** HTTP client span ends when headers are received. */ + HEADERS, + /** HTTP client span ends when the body is received */ + BODY + } } diff --git a/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/junit/http/HttpClientTestServer.java b/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/junit/http/HttpClientTestServer.java index e70c6516cabd..5f8e57217b73 100644 --- a/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/junit/http/HttpClientTestServer.java +++ b/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/junit/http/HttpClientTestServer.java @@ -17,6 +17,7 @@ import io.opentelemetry.instrumentation.test.server.http.RequestContextGetter; import io.opentelemetry.testing.internal.armeria.common.HttpData; import io.opentelemetry.testing.internal.armeria.common.HttpResponse; +import io.opentelemetry.testing.internal.armeria.common.HttpResponseWriter; import io.opentelemetry.testing.internal.armeria.common.HttpStatus; import io.opentelemetry.testing.internal.armeria.common.ResponseHeaders; import io.opentelemetry.testing.internal.armeria.common.ResponseHeadersBuilder; @@ -29,6 +30,7 @@ import java.nio.file.Paths; import java.security.KeyStore; import java.time.Duration; +import java.util.concurrent.TimeUnit; import javax.net.ssl.KeyManagerFactory; public final class HttpClientTestServer extends ServerExtension { @@ -55,7 +57,7 @@ protected void configure(ServerBuilder sb) throws Exception { .https(0) .tls(kmf) // not cleaning idle connections so eagerly helps minimize failures in HTTP client tests - .idleTimeout(Duration.ofSeconds(30)) + .idleTimeout(Duration.ofSeconds(60)) .service( "/success", (ctx, req) -> { @@ -99,6 +101,29 @@ protected void configure(ServerBuilder sb) throws Exception { "/read-timeout", (ctx, req) -> HttpResponse.delayed(HttpResponse.of(HttpStatus.OK), Duration.ofSeconds(20))) + .service( + "/long-request", + (ctx, req) -> { + HttpResponseWriter writer = HttpResponse.streaming(); + writer.write(ResponseHeaders.of(HttpStatus.OK)); + writer.write(HttpData.ofUtf8("Hello")); + + long delay = TimeUnit.SECONDS.toMillis(1); + String delayString = req.headers().get("delay"); + if (delayString != null) { + delay = Long.parseLong(delayString); + } + ctx.eventLoop() + .schedule( + () -> { + writer.write(HttpData.ofUtf8("World")); + writer.close(); + }, + delay, + TimeUnit.MILLISECONDS); + + return writer; + }) .decorator( (delegate, ctx, req) -> { for (String field : openTelemetry.getPropagators().getTextMapPropagator().fields()) { diff --git a/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/junit/http/HttpServerTestOptions.java b/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/junit/http/HttpServerTestOptions.java index 7cc031ea5b62..970184967589 100644 --- a/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/junit/http/HttpServerTestOptions.java +++ b/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/junit/http/HttpServerTestOptions.java @@ -9,11 +9,14 @@ import com.google.errorprone.annotations.CanIgnoreReturnValue; import io.opentelemetry.api.common.AttributeKey; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.opentelemetry.instrumentation.api.internal.HttpConstants; +import io.opentelemetry.semconv.HttpAttributes; +import io.opentelemetry.semconv.ServerAttributes; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.Set; +import java.util.function.BiFunction; import java.util.function.Function; import java.util.function.Predicate; import javax.annotation.Nullable; @@ -22,23 +25,29 @@ public final class HttpServerTestOptions { public static final Set> DEFAULT_HTTP_ATTRIBUTES = Collections.unmodifiableSet( - new HashSet<>( - Arrays.asList(SemanticAttributes.HTTP_ROUTE, SemanticAttributes.NET_PEER_PORT))); + new HashSet<>(Arrays.asList(HttpAttributes.HTTP_ROUTE, ServerAttributes.SERVER_PORT))); public static final SpanNameMapper DEFAULT_EXPECTED_SERVER_SPAN_NAME_MAPPER = - (uri, method, route) -> route == null ? method : method + " " + route; + (uri, method, route) -> { + if (HttpConstants._OTHER.equals(method)) { + method = "HTTP"; + } + return route == null ? method : method + " " + route; + }; Function>> httpAttributes = unused -> DEFAULT_HTTP_ATTRIBUTES; SpanNameMapper expectedServerSpanNameMapper = DEFAULT_EXPECTED_SERVER_SPAN_NAME_MAPPER; - Function expectedHttpRoute = unused -> null; + BiFunction expectedHttpRoute = (endpoint, method) -> null; Function sockPeerAddr = unused -> "127.0.0.1"; String contextPath = ""; Throwable expectedException = new Exception(EXCEPTION.body); + // we're calling /success in the test, and most servers respond with 200 anyway + int responseCodeOnNonStandardHttpMethod = ServerEndpoint.SUCCESS.status; Predicate hasHandlerSpan = unused -> false; Predicate hasResponseSpan = unused -> false; + Predicate hasRenderSpan = unused -> false; Predicate hasErrorPageSpans = unused -> false; - Predicate hasHandlerAsControllerParentSpan = unused -> true; Predicate hasResponseCustomizer = unused -> false; Predicate hasExceptionOnServerSpan = endpoint -> !hasHandlerSpan.test(endpoint); @@ -52,7 +61,9 @@ public final class HttpServerTestOptions { boolean testCaptureHttpHeaders = true; boolean testCaptureRequestParameters = false; boolean testHttpPipelining = true; + boolean testNonStandardHttpMethod = true; boolean verifyServerSpanEndTime = true; + boolean useHttp2 = false; HttpServerTestOptions() {} @@ -72,7 +83,7 @@ public HttpServerTestOptions setExpectedServerSpanNameMapper( @CanIgnoreReturnValue public HttpServerTestOptions setExpectedHttpRoute( - Function expectedHttpRoute) { + BiFunction expectedHttpRoute) { this.expectedHttpRoute = expectedHttpRoute; return this; } @@ -95,6 +106,13 @@ public HttpServerTestOptions setExpectedException(Throwable expectedException) { return this; } + @CanIgnoreReturnValue + public HttpServerTestOptions setResponseCodeOnNonStandardHttpMethod( + int responseCodeOnNonStandardHttpMethod) { + this.responseCodeOnNonStandardHttpMethod = responseCodeOnNonStandardHttpMethod; + return this; + } + @CanIgnoreReturnValue public HttpServerTestOptions setHasHandlerSpan(Predicate hasHandlerSpan) { this.hasHandlerSpan = hasHandlerSpan; @@ -108,15 +126,14 @@ public HttpServerTestOptions setHasResponseSpan(Predicate hasRes } @CanIgnoreReturnValue - public HttpServerTestOptions setHasErrorPageSpans(Predicate hasErrorPageSpans) { - this.hasErrorPageSpans = hasErrorPageSpans; + public HttpServerTestOptions setHasRenderSpan(Predicate hasRenderSpan) { + this.hasRenderSpan = hasRenderSpan; return this; } @CanIgnoreReturnValue - public HttpServerTestOptions setHasHandlerAsControllerParentSpan( - Predicate hasHandlerAsControllerParentSpan) { - this.hasHandlerAsControllerParentSpan = hasHandlerAsControllerParentSpan; + public HttpServerTestOptions setHasErrorPageSpans(Predicate hasErrorPageSpans) { + this.hasErrorPageSpans = hasErrorPageSpans; return this; } @@ -189,12 +206,30 @@ public HttpServerTestOptions setTestHttpPipelining(boolean testHttpPipelining) { return this; } + // TODO: convert make this class follow the same pattern as HttpClientTestOptions + @CanIgnoreReturnValue + public HttpServerTestOptions disableTestNonStandardHttpMethod() { + this.testNonStandardHttpMethod = false; + return this; + } + @CanIgnoreReturnValue public HttpServerTestOptions setVerifyServerSpanEndTime(boolean verifyServerSpanEndTime) { this.verifyServerSpanEndTime = verifyServerSpanEndTime; return this; } + @CanIgnoreReturnValue + public HttpServerTestOptions setUseHttp2(boolean useHttp2) { + this.useHttp2 = useHttp2; + return this; + } + + @CanIgnoreReturnValue + public HttpServerTestOptions useHttp2() { + return setUseHttp2(true); + } + @FunctionalInterface public interface SpanNameMapper { diff --git a/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/junit/http/ServerEndpoint.java b/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/junit/http/ServerEndpoint.java index 9a7a4e4a156f..405496b36265 100644 --- a/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/junit/http/ServerEndpoint.java +++ b/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/junit/http/ServerEndpoint.java @@ -81,6 +81,10 @@ public String name() { } public ServerEndpoint(String name, String uri, int status, String body) { + this(name, uri, status, body, true); + } + + public ServerEndpoint(String name, String uri, int status, String body, boolean registerPath) { this.name = name; this.uriObj = URI.create(uri); this.path = uriObj.getPath(); @@ -88,7 +92,9 @@ public ServerEndpoint(String name, String uri, int status, String body) { this.fragment = uriObj.getFragment(); this.status = status; this.body = body; - PATH_MAP.put(this.getPath(), this); + if (registerPath) { + PATH_MAP.put(this.getPath(), this); + } } public String getPath() { diff --git a/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/util/TelemetryDataUtil.java b/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/util/TelemetryDataUtil.java index 791fc68fce24..830b3fcfdcee 100644 --- a/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/util/TelemetryDataUtil.java +++ b/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/util/TelemetryDataUtil.java @@ -8,8 +8,10 @@ import static java.util.stream.Collectors.toList; import static org.assertj.core.api.Assertions.assertThat; +import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.trace.SpanId; import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.sdk.common.InstrumentationScopeInfo; import io.opentelemetry.sdk.trace.data.SpanData; import java.util.ArrayList; import java.util.Arrays; @@ -34,6 +36,13 @@ public static Comparator> orderByRootSpanName(String... names) { return Comparator.comparing(span -> list.indexOf(span.get(0).getName())); } + public static > Comparator> comparingRootSpanAttribute( + AttributeKey key) { + return Comparator.comparing( + span -> span.get(0).getAttributes().get(key), + Comparator.nullsFirst(Comparator.naturalOrder())); + } + public static List> groupTraces(List spans) { List> traces = new ArrayList<>( @@ -83,10 +92,13 @@ public static List> waitForTraces( public static void assertScopeVersion(List> traces) { for (List trace : traces) { for (SpanData span : trace) { - if (!span.getInstrumentationScopeInfo().getName().equals("test")) { - assertThat(span.getInstrumentationScopeInfo().getVersion()) + InstrumentationScopeInfo scopeInfo = span.getInstrumentationScopeInfo(); + if (!scopeInfo.getName().equals("test")) { + assertThat(scopeInfo.getVersion()) .as( - "Instrumentation version was empty; make sure that the instrumentation name matches the gradle module name") + "Instrumentation version of module %s was empty; make sure that the " + + "instrumentation name matches the gradle module name", + scopeInfo.getName()) .isNotNull(); } } diff --git a/testing-common/src/main/java/io/opentelemetry/javaagent/testing/common/TestAgentListenerAccess.java b/testing-common/src/main/java/io/opentelemetry/javaagent/testing/common/TestAgentListenerAccess.java index 39d43122b285..39624e0c7ed7 100644 --- a/testing-common/src/main/java/io/opentelemetry/javaagent/testing/common/TestAgentListenerAccess.java +++ b/testing-common/src/main/java/io/opentelemetry/javaagent/testing/common/TestAgentListenerAccess.java @@ -7,6 +7,7 @@ import static java.lang.invoke.MethodType.methodType; +import io.opentelemetry.javaagent.bootstrap.ExceptionLogger; import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; import java.util.List; @@ -105,5 +106,9 @@ public static void addSkipErrorCondition(BiFunction } } + public static int getAndResetAdviceFailureCount() { + return ExceptionLogger.getAndReset(); + } + private TestAgentListenerAccess() {} } diff --git a/testing-common/src/test/java/io/opentelemetry/instrumentation/testing/junit/LibraryInstrumentationExtensionTest.java b/testing-common/src/test/java/io/opentelemetry/instrumentation/testing/junit/LibraryInstrumentationExtensionTest.java index ffbdf71b4a56..7c6c9bfa6070 100644 --- a/testing-common/src/test/java/io/opentelemetry/instrumentation/testing/junit/LibraryInstrumentationExtensionTest.java +++ b/testing-common/src/test/java/io/opentelemetry/instrumentation/testing/junit/LibraryInstrumentationExtensionTest.java @@ -36,13 +36,10 @@ void shouldCollectTraces() { // then List> traces = testing.waitForTraces(1); assertThat(traces) - .hasSize(1) .hasTracesSatisfyingExactly( trace -> - trace - .hasSize(2) - .hasSpansSatisfyingExactly( - parentSpan -> parentSpan.hasName("parent"), - childSpan -> childSpan.hasName("child"))); + trace.hasSpansSatisfyingExactly( + parentSpan -> parentSpan.hasName("parent"), + childSpan -> childSpan.hasName("child"))); } } diff --git a/testing/agent-exporter/build.gradle.kts b/testing/agent-exporter/build.gradle.kts index 063a7d3a01dc..f42ab5055727 100644 --- a/testing/agent-exporter/build.gradle.kts +++ b/testing/agent-exporter/build.gradle.kts @@ -1,5 +1,5 @@ plugins { - id("com.github.johnrengelman.shadow") + id("com.gradleup.shadow") id("otel.java-conventions") } diff --git a/testing/agent-exporter/src/main/java/io/opentelemetry/javaagent/testing/exporter/AgentTestingCustomizer.java b/testing/agent-exporter/src/main/java/io/opentelemetry/javaagent/testing/exporter/AgentTestingCustomizer.java index 3c4d7aef6923..058ad0e38d6d 100644 --- a/testing/agent-exporter/src/main/java/io/opentelemetry/javaagent/testing/exporter/AgentTestingCustomizer.java +++ b/testing/agent-exporter/src/main/java/io/opentelemetry/javaagent/testing/exporter/AgentTestingCustomizer.java @@ -35,14 +35,34 @@ static void reset() { @Override public void customize(AutoConfigurationCustomizer autoConfigurationCustomizer) { autoConfigurationCustomizer.addTracerProviderCustomizer( - (tracerProvider, config) -> tracerProvider.addSpanProcessor(spanProcessor)); + (tracerProvider, config) -> { + // span processor is responsible for exporting spans, not adding it disables exporting of + // the spans + if (config.getBoolean("testing.exporter.enabled", true)) { + return tracerProvider.addSpanProcessor(spanProcessor); + } + return tracerProvider; + }); autoConfigurationCustomizer.addMeterProviderCustomizer( - (meterProvider, config) -> meterProvider.registerMetricReader(metricReader)); + (meterProvider, config) -> { + // metric reader is responsible for exporting metrics, not adding it disables exporting of + // the metrics + if (config.getBoolean("testing.exporter.enabled", true)) { + return meterProvider.registerMetricReader(metricReader); + } + return meterProvider; + }); autoConfigurationCustomizer.addLoggerProviderCustomizer( - (logProvider, config) -> - logProvider.addLogRecordProcessor( - SimpleLogRecordProcessor.create(AgentTestingExporterFactory.logExporter))); + (logProvider, config) -> { + // log record processor is responsible for exporting logs, not adding it disables + // exporting of the logs + if (config.getBoolean("testing.exporter.enabled", true)) { + return logProvider.addLogRecordProcessor( + SimpleLogRecordProcessor.create(AgentTestingExporterFactory.logExporter)); + } + return logProvider; + }); } } diff --git a/testing/agent-exporter/src/main/java/io/opentelemetry/javaagent/testing/http/CapturedHttpHeadersTestConfigSupplier.java b/testing/agent-exporter/src/main/java/io/opentelemetry/javaagent/testing/http/CapturedHttpHeadersTestConfigSupplier.java index 2485e32f9506..dce4ac5701c6 100644 --- a/testing/agent-exporter/src/main/java/io/opentelemetry/javaagent/testing/http/CapturedHttpHeadersTestConfigSupplier.java +++ b/testing/agent-exporter/src/main/java/io/opentelemetry/javaagent/testing/http/CapturedHttpHeadersTestConfigSupplier.java @@ -22,10 +22,10 @@ public void customize(AutoConfigurationCustomizer autoConfiguration) { private static Map getTestProperties() { Map testConfig = new HashMap<>(); - testConfig.put("otel.instrumentation.http.capture-headers.client.request", "X-Test-Request"); - testConfig.put("otel.instrumentation.http.capture-headers.client.response", "X-Test-Response"); - testConfig.put("otel.instrumentation.http.capture-headers.server.request", "X-Test-Request"); - testConfig.put("otel.instrumentation.http.capture-headers.server.response", "X-Test-Response"); + testConfig.put("otel.instrumentation.http.client.capture-request-headers", "X-Test-Request"); + testConfig.put("otel.instrumentation.http.client.capture-response-headers", "X-Test-Response"); + testConfig.put("otel.instrumentation.http.server.capture-request-headers", "X-Test-Request"); + testConfig.put("otel.instrumentation.http.server.capture-response-headers", "X-Test-Response"); return testConfig; } } diff --git a/testing/agent-for-testing/build.gradle.kts b/testing/agent-for-testing/build.gradle.kts index 457c38cba5ca..5f9df0af2c28 100644 --- a/testing/agent-for-testing/build.gradle.kts +++ b/testing/agent-for-testing/build.gradle.kts @@ -44,7 +44,6 @@ tasks { afterEvaluate { withType().configureEach { jvmArgs("-Dotel.javaagent.debug=true") - jvmArgs("-Dotel.metrics.exporter=otlp") jvmArgumentProviders.add(JavaagentProvider(jar.flatMap { it.archiveFile })) } diff --git a/testing/armeria-shaded-for-testing/build.gradle.kts b/testing/armeria-shaded-for-testing/build.gradle.kts index 481fd608e578..50a806548ad1 100644 --- a/testing/armeria-shaded-for-testing/build.gradle.kts +++ b/testing/armeria-shaded-for-testing/build.gradle.kts @@ -1,11 +1,10 @@ plugins { - id("com.github.johnrengelman.shadow") - + id("com.gradleup.shadow") id("otel.java-conventions") } dependencies { - implementation("com.linecorp.armeria:armeria-junit5:1.24.0") + implementation("com.linecorp.armeria:armeria-junit5:1.30.0") } tasks { @@ -19,6 +18,7 @@ tasks { // Ensures tests are not affected by Armeria instrumentation relocate("com.linecorp.armeria", "io.opentelemetry.testing.internal.armeria") relocate("com.fasterxml.jackson", "io.opentelemetry.testing.internal.jackson") + relocate("net.bytebuddy", "io.opentelemetry.testing.internal.bytebuddy") // Allows tests of Netty instrumentations which would otherwise conflict. // The relocation must end with io.netty to allow Netty to detect shaded native libraries. diff --git a/version.gradle.kts b/version.gradle.kts index c1c97dc25c8d..62a705c21c34 100644 --- a/version.gradle.kts +++ b/version.gradle.kts @@ -1,5 +1,5 @@ -val stableVersion = "1.28.0-SNAPSHOT" -val alphaVersion = "1.28.0-alpha-SNAPSHOT" +val stableVersion = "2.8.0-SNAPSHOT" +val alphaVersion = "2.8.0-alpha-SNAPSHOT" allprojects { if (findProperty("otel.stable") != "true") {